sethares-dissonance 3.0.0 → 5.0.0-beta.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.
@@ -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,21 @@ 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 a target range.
78
+ *
79
+ * **Destructive:** This method mutates the curve permanently. All dissonance values
80
+ * are scaled from [0, maxDissonance] to [min, max], and the original raw values
81
+ * cannot be 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 in the [min, max] range.
85
+ *
86
+ * @param min - Target minimum value (default: 0)
87
+ * @param max - Target maximum value (default: 1)
88
+ * @returns `this` for method chaining
89
+ */
90
+ normalize(min?: number, max?: number): this;
73
91
  /**
74
92
  * Get plot points with intervals as ratio values (decimal numbers).
75
93
  * @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 phantomHarmonics = SpectrumWithLoudness.harmonic(dissonanceParams.phantomHarmonicsNumber + 1, 1, true);
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 phantomHarmonics = SpectrumWithLoudness.harmonic(dissonanceParams.phantomHarmonicsNumber + 1, 1, true);
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,35 @@ 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 a target range.
122
+ *
123
+ * **Destructive:** This method mutates the curve permanently. All dissonance values
124
+ * are scaled from [0, maxDissonance] to [min, max], and the original raw values
125
+ * cannot be 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 in the [min, max] range.
129
+ *
130
+ * @param min - Target minimum value (default: 0)
131
+ * @param max - Target maximum value (default: 1)
132
+ * @returns `this` for method chaining
133
+ */
134
+ normalize(min = 0, max = 1) {
135
+ if (max <= min)
136
+ throw new Error('max must be greater than min');
137
+ if (this.maxDissonance === 0)
138
+ return this;
139
+ const range = max - min;
140
+ for (const [key, point] of this._data) {
141
+ this._data.set(key, {
142
+ interval: point.interval,
143
+ dissonance: (point.dissonance / this.maxDissonance) * range + min,
144
+ });
145
+ }
146
+ this.maxDissonance = max;
147
+ return this;
148
+ }
114
149
  /**
115
150
  * Get plot points with intervals as ratio values (decimal numbers).
116
151
  * @returns Array of [ratio, dissonance] tuples suitable for plotting
@@ -1,9 +1,40 @@
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: number;
5
- s2: number;
6
- b1: number;
7
- b2: number;
8
- x_star: number;
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;
10
+ /** At 0: no decay (constant magnitude). Higher values = faster decay with minFrequency. */
11
+ readonly magnitudeFrequencyDecay: 0;
9
12
  };
13
+ export declare const DEFAULT_FIRST_ORDER_DISSONANCE_PARAMS: {
14
+ s1: 0.021;
15
+ s2: 19;
16
+ b1: 3.5;
17
+ b2: 5.75;
18
+ x_star: 0.24;
19
+ magnitude: 1;
20
+ magnitudeFrequencyDecay: 0;
21
+ };
22
+ export declare const DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS: {
23
+ magnitude: number;
24
+ s1: 0.021;
25
+ s2: 19;
26
+ b1: 3.5;
27
+ b2: 5.75;
28
+ x_star: 0.24;
29
+ magnitudeFrequencyDecay: 0;
30
+ };
31
+ export declare const DEFAULT_THIRD_ORDER_DISSONANCE_PARAMS: {
32
+ magnitude: number;
33
+ s1: 0.021;
34
+ s2: 19;
35
+ b1: 3.5;
36
+ b2: 5.75;
37
+ x_star: 0.24;
38
+ magnitudeFrequencyDecay: 0;
39
+ };
40
+ export declare const DEFAULT_PHANTOM_HARMONICS_NUMBER = 0;
package/dist/lib/const.js CHANGED
@@ -6,4 +6,11 @@ export const SETHARES_DISSONANCE_PARAMS = {
6
6
  b1: 3.5,
7
7
  b2: 5.75,
8
8
  x_star: 0.24,
9
+ magnitude: 1,
10
+ /** At 0: no decay (constant magnitude). Higher values = faster decay with minFrequency. */
11
+ magnitudeFrequencyDecay: 0,
9
12
  };
13
+ export const DEFAULT_FIRST_ORDER_DISSONANCE_PARAMS = { ...SETHARES_DISSONANCE_PARAMS };
14
+ export const DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS = { ...SETHARES_DISSONANCE_PARAMS, magnitude: 0.25 };
15
+ export const DEFAULT_THIRD_ORDER_DISSONANCE_PARAMS = { ...SETHARES_DISSONANCE_PARAMS, magnitude: 0.1 };
16
+ export const DEFAULT_PHANTOM_HARMONICS_NUMBER = 0;
@@ -1,11 +1,14 @@
1
1
  import type Fraction from "fraction.js";
2
2
  import type { SETHARES_DISSONANCE_PARAMS } from "./const";
3
- export type SetharesDissonanceParams = Partial<typeof SETHARES_DISSONANCE_PARAMS>;
4
- export type DissonanceParams = SetharesDissonanceParams & {
5
- firstOrderContribution: number;
6
- secondOrderContribution: number;
7
- thirdOrderContribution: number;
8
- phantomHarmonicsNumber: number;
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;
@@ -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?: SetharesDissonanceParams & {
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: firstOrderDissonance
13
- * - One phantom, one non-phantom: secondOrderDissonance
14
- * - Both phantom: thirdOrderDissonance
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,17 +1,18 @@
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 contribution = params?.contribution ?? 1;
8
- if (contribution <= 0)
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;
12
12
  const s2 = params?.s2 ?? SETHARES_DISSONANCE_PARAMS.s2;
13
13
  const b1 = params?.b1 ?? SETHARES_DISSONANCE_PARAMS.b1;
14
14
  const b2 = params?.b2 ?? SETHARES_DISSONANCE_PARAMS.b2;
15
+ const magnitudeFrequencyDecay = params?.magnitudeFrequencyDecay ?? SETHARES_DISSONANCE_PARAMS.magnitudeFrequencyDecay;
15
16
  if (h1.frequency.equals(h2.frequency))
16
17
  return 0;
17
18
  const minLoudness = Math.min("loudness" in h1 ? h1.loudness : getLoudness(h1.amplitude), "loudness" in h2 ? h2.loudness : getLoudness(h2.amplitude));
@@ -20,39 +21,38 @@ export function getPlompLeveltDissonance(h1, h2, params) {
20
21
  const minFrequency = Math.min(h1.frequencyNum, h2.frequencyNum);
21
22
  if (minFrequency <= 0)
22
23
  return 0;
24
+ const magnitudeDecayFactor = Math.exp(-magnitudeFrequencyDecay * (minFrequency / 1000));
23
25
  const frequencyDifference = Math.abs(h1.frequencyNum - h2.frequencyNum);
24
26
  const s = x_star / (s1 * minFrequency + s2);
25
- return (contribution *
27
+ return (magnitude *
28
+ magnitudeDecayFactor *
26
29
  minLoudness *
27
30
  (Math.exp(-1 * b1 * s * frequencyDifference) -
28
31
  Math.exp(-1 * b2 * s * frequencyDifference)));
29
32
  }
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
33
  /**
43
34
  * Find dissonance between two harmonics based on phantom status.
44
- * - Both non-phantom: firstOrderDissonance
45
- * - One phantom, one non-phantom: secondOrderDissonance
46
- * - Both phantom: thirdOrderDissonance
35
+ * - Both non-phantom: first order
36
+ * - One phantom, one non-phantom: second order
37
+ * - Both phantom: third order
47
38
  */
48
39
  export function getSensoryDissonance(h1, h2, params) {
49
40
  if (!h1.phantom && !h2.phantom) {
50
- return firstOrderDissonance(h1, h2, params);
41
+ return getPlompLeveltDissonance(h1, h2, {
42
+ ...DEFAULT_FIRST_ORDER_DISSONANCE_PARAMS,
43
+ ...params?.firstOrderDissonance,
44
+ });
51
45
  }
52
46
  if (h1.phantom !== h2.phantom) {
53
- return secondOrderDissonance(h1, h2, params);
47
+ return getPlompLeveltDissonance(h1, h2, {
48
+ ...DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS,
49
+ ...params?.secondOrderDissonance,
50
+ });
54
51
  }
55
- return thirdOrderDissonance(h1, h2, params);
52
+ return getPlompLeveltDissonance(h1, h2, {
53
+ ...DEFAULT_THIRD_ORDER_DISSONANCE_PARAMS,
54
+ ...params?.thirdOrderDissonance,
55
+ });
56
56
  }
57
57
  /**
58
58
  * 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(6, 440);
4
- const complement = Spectrum.harmonic(6, 440);
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
- firstOrderContribution: 1,
11
- secondOrderContribution: 0.3,
12
- thirdOrderContribution: 0.1,
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());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sethares-dissonance",
3
- "version": "3.0.0",
3
+ "version": "5.0.0-beta.0",
4
4
  "type": "module",
5
5
  "devDependencies": {
6
6
  "@types/bun": "latest"