sethares-dissonance 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/classes/DissonanceCurve.d.ts +16 -0
- package/dist/classes/DissonanceCurve.js +32 -2
- package/dist/lib/const.d.ts +31 -5
- package/dist/lib/const.js +5 -0
- package/dist/lib/types.d.ts +9 -6
- package/dist/lib/utils.d.ts +4 -6
- package/dist/lib/utils.js +19 -22
- package/dist/manual-testing/index.js +6 -6
- package/package.json +1 -1
|
@@ -38,6 +38,9 @@ export type DissonanceCurveOptions = DissonanceParams & {
|
|
|
38
38
|
*
|
|
39
39
|
* // Get plot data with cents
|
|
40
40
|
* const plotDataCents = curve.plotCents();
|
|
41
|
+
*
|
|
42
|
+
* // Normalize dissonance to 0–1 range (destructive, mutates in place)
|
|
43
|
+
* curve.normalize();
|
|
41
44
|
* ```
|
|
42
45
|
*/
|
|
43
46
|
export declare class DissonanceCurve {
|
|
@@ -70,6 +73,19 @@ export declare class DissonanceCurve {
|
|
|
70
73
|
* @returns The DissonanceCurvePoint for the given ratio, or undefined if not found
|
|
71
74
|
*/
|
|
72
75
|
get(ratio: FractionInput): DissonanceCurvePoint | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Normalize dissonance values in place to the range [0, 1].
|
|
78
|
+
*
|
|
79
|
+
* **Destructive:** This method mutates the curve permanently. All dissonance values
|
|
80
|
+
* are divided by the maximum dissonance, and the original raw values cannot be
|
|
81
|
+
* recovered. Use {@link recalculate} to rebuild the curve with raw values.
|
|
82
|
+
*
|
|
83
|
+
* After normalization, `points`, `get`, `plot`, `plotCents`, `toJSON`, and
|
|
84
|
+
* `toString` will all return normalized dissonance values (0–1 range).
|
|
85
|
+
*
|
|
86
|
+
* @returns `this` for method chaining
|
|
87
|
+
*/
|
|
88
|
+
normalize(): this;
|
|
73
89
|
/**
|
|
74
90
|
* Get plot points with intervals as ratio values (decimal numbers).
|
|
75
91
|
* @returns Array of [ratio, dissonance] tuples suitable for plotting
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fraction } from "fraction.js";
|
|
2
2
|
import { ratioToCents, Spectrum, IntervalSet } from "tuning-core";
|
|
3
3
|
import { getSetharesDissonance, getIntrinsicDissonance } from "../lib";
|
|
4
|
+
import { DEFAULT_PHANTOM_HARMONICS_NUMBER } from "../lib/const";
|
|
4
5
|
import { SpectrumWithLoudness } from "./private/SpectrumWithLoudness";
|
|
5
6
|
const DEFAULT_COLUMN_DELIMITER = ",";
|
|
6
7
|
const DEFAULT_ROW_DELIMITER = "\n";
|
|
@@ -33,6 +34,9 @@ const DEFAULT_ROW_DELIMITER = "\n";
|
|
|
33
34
|
*
|
|
34
35
|
* // Get plot data with cents
|
|
35
36
|
* const plotDataCents = curve.plotCents();
|
|
37
|
+
*
|
|
38
|
+
* // Normalize dissonance to 0–1 range (destructive, mutates in place)
|
|
39
|
+
* curve.normalize();
|
|
36
40
|
* ```
|
|
37
41
|
*/
|
|
38
42
|
export class DissonanceCurve {
|
|
@@ -45,7 +49,8 @@ export class DissonanceCurve {
|
|
|
45
49
|
maxDissonance = 0;
|
|
46
50
|
constructor(opts) {
|
|
47
51
|
const { context, complement, start, end, maxGapCents = 20, ...dissonanceParams } = opts;
|
|
48
|
-
const
|
|
52
|
+
const phantomCount = (dissonanceParams.phantomHarmonicsNumber ?? DEFAULT_PHANTOM_HARMONICS_NUMBER) + 1;
|
|
53
|
+
const phantomHarmonics = SpectrumWithLoudness.harmonic(phantomCount, 1, true);
|
|
49
54
|
this.context = new SpectrumWithLoudness(context).mul(phantomHarmonics);
|
|
50
55
|
this.complement = new SpectrumWithLoudness(complement).mul(phantomHarmonics);
|
|
51
56
|
this.start = new Fraction(start ?? 1);
|
|
@@ -61,7 +66,8 @@ export class DissonanceCurve {
|
|
|
61
66
|
*/
|
|
62
67
|
recalculate(opts) {
|
|
63
68
|
const { context, complement, start, end, maxGapCents = 20, ...dissonanceParams } = opts;
|
|
64
|
-
const
|
|
69
|
+
const phantomCount = (dissonanceParams.phantomHarmonicsNumber ?? DEFAULT_PHANTOM_HARMONICS_NUMBER) + 1;
|
|
70
|
+
const phantomHarmonics = SpectrumWithLoudness.harmonic(phantomCount, 1, true);
|
|
65
71
|
this.context = new SpectrumWithLoudness(context).mul(phantomHarmonics);
|
|
66
72
|
this.complement = new SpectrumWithLoudness(complement).mul(phantomHarmonics);
|
|
67
73
|
this.start = new Fraction(start ?? 1);
|
|
@@ -111,6 +117,30 @@ export class DissonanceCurve {
|
|
|
111
117
|
get(ratio) {
|
|
112
118
|
return this._data.get(new Fraction(ratio).toFraction());
|
|
113
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Normalize dissonance values in place to the range [0, 1].
|
|
122
|
+
*
|
|
123
|
+
* **Destructive:** This method mutates the curve permanently. All dissonance values
|
|
124
|
+
* are divided by the maximum dissonance, and the original raw values cannot be
|
|
125
|
+
* recovered. Use {@link recalculate} to rebuild the curve with raw values.
|
|
126
|
+
*
|
|
127
|
+
* After normalization, `points`, `get`, `plot`, `plotCents`, `toJSON`, and
|
|
128
|
+
* `toString` will all return normalized dissonance values (0–1 range).
|
|
129
|
+
*
|
|
130
|
+
* @returns `this` for method chaining
|
|
131
|
+
*/
|
|
132
|
+
normalize() {
|
|
133
|
+
if (this.maxDissonance === 0)
|
|
134
|
+
return this;
|
|
135
|
+
for (const [key, point] of this._data) {
|
|
136
|
+
this._data.set(key, {
|
|
137
|
+
interval: point.interval,
|
|
138
|
+
dissonance: point.dissonance / this.maxDissonance,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
this.maxDissonance = 1;
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
114
144
|
/**
|
|
115
145
|
* Get plot points with intervals as ratio values (decimal numbers).
|
|
116
146
|
* @returns Array of [ratio, dissonance] tuples suitable for plotting
|
package/dist/lib/const.d.ts
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
export declare const NORMALISATION_PRESSURE_UNIT = 2.8284271247461903;
|
|
2
2
|
/** Fitting parameters proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
|
|
3
3
|
export declare const SETHARES_DISSONANCE_PARAMS: {
|
|
4
|
-
s1:
|
|
5
|
-
s2:
|
|
6
|
-
b1:
|
|
7
|
-
b2:
|
|
8
|
-
x_star:
|
|
4
|
+
readonly s1: 0.021;
|
|
5
|
+
readonly s2: 19;
|
|
6
|
+
readonly b1: 3.5;
|
|
7
|
+
readonly b2: 5.75;
|
|
8
|
+
readonly x_star: 0.24;
|
|
9
|
+
readonly magnitude: 1;
|
|
9
10
|
};
|
|
11
|
+
export declare const DEFAULT_FIRST_ORDER_DISSONANCE_PARAMS: {
|
|
12
|
+
s1: 0.021;
|
|
13
|
+
s2: 19;
|
|
14
|
+
b1: 3.5;
|
|
15
|
+
b2: 5.75;
|
|
16
|
+
x_star: 0.24;
|
|
17
|
+
magnitude: 1;
|
|
18
|
+
};
|
|
19
|
+
export declare const DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS: {
|
|
20
|
+
magnitude: number;
|
|
21
|
+
s1: 0.021;
|
|
22
|
+
s2: 19;
|
|
23
|
+
b1: 3.5;
|
|
24
|
+
b2: 5.75;
|
|
25
|
+
x_star: 0.24;
|
|
26
|
+
};
|
|
27
|
+
export declare const DEFAULT_THIRD_ORDER_DISSONANCE_PARAMS: {
|
|
28
|
+
magnitude: number;
|
|
29
|
+
s1: 0.021;
|
|
30
|
+
s2: 19;
|
|
31
|
+
b1: 3.5;
|
|
32
|
+
b2: 5.75;
|
|
33
|
+
x_star: 0.24;
|
|
34
|
+
};
|
|
35
|
+
export declare const DEFAULT_PHANTOM_HARMONICS_NUMBER = 0;
|
package/dist/lib/const.js
CHANGED
|
@@ -6,4 +6,9 @@ export const SETHARES_DISSONANCE_PARAMS = {
|
|
|
6
6
|
b1: 3.5,
|
|
7
7
|
b2: 5.75,
|
|
8
8
|
x_star: 0.24,
|
|
9
|
+
magnitude: 1,
|
|
9
10
|
};
|
|
11
|
+
export const DEFAULT_FIRST_ORDER_DISSONANCE_PARAMS = { ...SETHARES_DISSONANCE_PARAMS };
|
|
12
|
+
export const DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS = { ...SETHARES_DISSONANCE_PARAMS, magnitude: 0.25 };
|
|
13
|
+
export const DEFAULT_THIRD_ORDER_DISSONANCE_PARAMS = { ...SETHARES_DISSONANCE_PARAMS, magnitude: 0.1 };
|
|
14
|
+
export const DEFAULT_PHANTOM_HARMONICS_NUMBER = 0;
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type Fraction from "fraction.js";
|
|
2
2
|
import type { SETHARES_DISSONANCE_PARAMS } from "./const";
|
|
3
|
-
|
|
4
|
-
export type
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/** Params for Plomp-Levelt formula; all numeric to allow overrides */
|
|
4
|
+
export type SetharesDissonanceParams = Partial<{
|
|
5
|
+
[K in keyof typeof SETHARES_DISSONANCE_PARAMS]: number;
|
|
6
|
+
}>;
|
|
7
|
+
export type DissonanceParams = {
|
|
8
|
+
firstOrderDissonance?: SetharesDissonanceParams;
|
|
9
|
+
secondOrderDissonance?: SetharesDissonanceParams;
|
|
10
|
+
thirdOrderDissonance?: SetharesDissonanceParams;
|
|
11
|
+
phantomHarmonicsNumber?: number;
|
|
9
12
|
};
|
|
10
13
|
export type DissonanceCurvePoint = {
|
|
11
14
|
interval: Fraction;
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -4,14 +4,12 @@ import type { HarmonicWithLoudness } from "../classes/private/HarmonicWithLoudne
|
|
|
4
4
|
import { SpectrumWithLoudness } from "../classes/private/SpectrumWithLoudness";
|
|
5
5
|
export { getLoudness } from "./loudness";
|
|
6
6
|
/** The formula to calculate sensory dissoannce proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
|
|
7
|
-
export declare function getPlompLeveltDissonance(h1: Harmonic, h2: Harmonic, params
|
|
8
|
-
contribution?: number;
|
|
9
|
-
}): number;
|
|
7
|
+
export declare function getPlompLeveltDissonance(h1: Harmonic, h2: Harmonic, params: SetharesDissonanceParams): number;
|
|
10
8
|
/**
|
|
11
9
|
* Find dissonance between two harmonics based on phantom status.
|
|
12
|
-
* - Both non-phantom:
|
|
13
|
-
* - One phantom, one non-phantom:
|
|
14
|
-
* - Both phantom:
|
|
10
|
+
* - Both non-phantom: first order
|
|
11
|
+
* - One phantom, one non-phantom: second order
|
|
12
|
+
* - Both phantom: third order
|
|
15
13
|
*/
|
|
16
14
|
export declare function getSensoryDissonance(h1: HarmonicWithLoudness, h2: HarmonicWithLoudness, params?: DissonanceParams): number;
|
|
17
15
|
/**
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { SETHARES_DISSONANCE_PARAMS } from "./const";
|
|
1
|
+
import { DEFAULT_FIRST_ORDER_DISSONANCE_PARAMS, DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS, DEFAULT_THIRD_ORDER_DISSONANCE_PARAMS, SETHARES_DISSONANCE_PARAMS, } from "./const";
|
|
2
2
|
import { getLoudness } from "./loudness";
|
|
3
3
|
import { SpectrumWithLoudness } from "../classes/private/SpectrumWithLoudness";
|
|
4
4
|
export { getLoudness } from "./loudness";
|
|
5
5
|
/** The formula to calculate sensory dissoannce proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
|
|
6
6
|
export function getPlompLeveltDissonance(h1, h2, params) {
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
7
|
+
const magnitude = params?.magnitude ?? SETHARES_DISSONANCE_PARAMS.magnitude;
|
|
8
|
+
if (magnitude <= 0)
|
|
9
9
|
return 0;
|
|
10
10
|
const x_star = params?.x_star ?? SETHARES_DISSONANCE_PARAMS.x_star;
|
|
11
11
|
const s1 = params?.s1 ?? SETHARES_DISSONANCE_PARAMS.s1;
|
|
@@ -22,37 +22,34 @@ export function getPlompLeveltDissonance(h1, h2, params) {
|
|
|
22
22
|
return 0;
|
|
23
23
|
const frequencyDifference = Math.abs(h1.frequencyNum - h2.frequencyNum);
|
|
24
24
|
const s = x_star / (s1 * minFrequency + s2);
|
|
25
|
-
return (
|
|
25
|
+
return (magnitude *
|
|
26
26
|
minLoudness *
|
|
27
27
|
(Math.exp(-1 * b1 * s * frequencyDifference) -
|
|
28
28
|
Math.exp(-1 * b2 * s * frequencyDifference)));
|
|
29
29
|
}
|
|
30
|
-
/** For now identical to getPlompLeveltDissonance */
|
|
31
|
-
function firstOrderDissonance(h1, h2, params) {
|
|
32
|
-
return getPlompLeveltDissonance(h1, h2, { ...params, contribution: params?.firstOrderContribution });
|
|
33
|
-
}
|
|
34
|
-
/** For now identical to getPlompLeveltDissonance */
|
|
35
|
-
function secondOrderDissonance(h1, h2, params) {
|
|
36
|
-
return getPlompLeveltDissonance(h1, h2, { ...params, contribution: params?.secondOrderContribution });
|
|
37
|
-
}
|
|
38
|
-
/** For now identical to getPlompLeveltDissonance */
|
|
39
|
-
function thirdOrderDissonance(h1, h2, params) {
|
|
40
|
-
return getPlompLeveltDissonance(h1, h2, { ...params, contribution: params?.thirdOrderContribution });
|
|
41
|
-
}
|
|
42
30
|
/**
|
|
43
31
|
* Find dissonance between two harmonics based on phantom status.
|
|
44
|
-
* - Both non-phantom:
|
|
45
|
-
* - One phantom, one non-phantom:
|
|
46
|
-
* - Both phantom:
|
|
32
|
+
* - Both non-phantom: first order
|
|
33
|
+
* - One phantom, one non-phantom: second order
|
|
34
|
+
* - Both phantom: third order
|
|
47
35
|
*/
|
|
48
36
|
export function getSensoryDissonance(h1, h2, params) {
|
|
49
37
|
if (!h1.phantom && !h2.phantom) {
|
|
50
|
-
return
|
|
38
|
+
return getPlompLeveltDissonance(h1, h2, {
|
|
39
|
+
...DEFAULT_FIRST_ORDER_DISSONANCE_PARAMS,
|
|
40
|
+
...params?.firstOrderDissonance,
|
|
41
|
+
});
|
|
51
42
|
}
|
|
52
43
|
if (h1.phantom !== h2.phantom) {
|
|
53
|
-
return
|
|
44
|
+
return getPlompLeveltDissonance(h1, h2, {
|
|
45
|
+
...DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS,
|
|
46
|
+
...params?.secondOrderDissonance,
|
|
47
|
+
});
|
|
54
48
|
}
|
|
55
|
-
return
|
|
49
|
+
return getPlompLeveltDissonance(h1, h2, {
|
|
50
|
+
...DEFAULT_THIRD_ORDER_DISSONANCE_PARAMS,
|
|
51
|
+
...params?.thirdOrderDissonance,
|
|
52
|
+
});
|
|
56
53
|
}
|
|
57
54
|
/**
|
|
58
55
|
* Calculate the intrinsic dissonance of a spectrum.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { Spectrum } from "tuning-core";
|
|
2
2
|
import { DissonanceCurve } from "../classes";
|
|
3
|
-
const context = Spectrum.harmonic(
|
|
4
|
-
const complement = Spectrum.harmonic(
|
|
3
|
+
const context = Spectrum.harmonic(1, 440);
|
|
4
|
+
const complement = Spectrum.harmonic(1, 440);
|
|
5
5
|
const curve = new DissonanceCurve({
|
|
6
6
|
context,
|
|
7
7
|
complement,
|
|
8
8
|
start: 1,
|
|
9
9
|
end: 2,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
firstOrderDissonance: { magnitude: 1 },
|
|
11
|
+
secondOrderDissonance: { magnitude: 0.3 },
|
|
12
|
+
thirdOrderDissonance: { magnitude: 0.1 },
|
|
13
13
|
phantomHarmonicsNumber: 2,
|
|
14
14
|
maxGapCents: 20,
|
|
15
15
|
});
|
|
16
|
-
Bun.write("./manual-testing/curve.csv", curve.toCsvFileBuffer());
|
|
16
|
+
Bun.write("./manual-testing/curve.csv", curve.normalize().toCsvFileBuffer());
|