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.
@@ -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 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,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
@@ -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: 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;
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;
@@ -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,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 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;
@@ -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 (contribution *
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: firstOrderDissonance
45
- * - One phantom, one non-phantom: secondOrderDissonance
46
- * - Both phantom: thirdOrderDissonance
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 firstOrderDissonance(h1, h2, params);
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 secondOrderDissonance(h1, h2, params);
44
+ return getPlompLeveltDissonance(h1, h2, {
45
+ ...DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS,
46
+ ...params?.secondOrderDissonance,
47
+ });
54
48
  }
55
- return thirdOrderDissonance(h1, h2, params);
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(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": "4.0.0",
4
4
  "type": "module",
5
5
  "devDependencies": {
6
6
  "@types/bun": "latest"