sethares-dissonance 5.0.0 → 5.1.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.
@@ -14,6 +14,7 @@ export declare class SpectrumWithLoudness extends Spectrum {
14
14
  toTransposed(ratio: FractionInput): SpectrumWithLoudness;
15
15
  toJSON(): SpectrumWithLoudnessData;
16
16
  get(frequency: FractionInput): HarmonicWithLoudness | undefined;
17
+ static fromFrequencies(frequencies: FractionInput[], amplitudes?: number[], phases?: number[]): SpectrumWithLoudness;
17
18
  /**
18
19
  * Multiply this spectrum by another spectrum.
19
20
  * Each harmonic of this spectrum is multiplied by each harmonic of the other
@@ -28,4 +29,5 @@ export declare class SpectrumWithLoudness extends Spectrum {
28
29
  */
29
30
  mul(other: SpectrumWithLoudness): SpectrumWithLoudness;
30
31
  static harmonic(count: number, fundamentalHz: FractionInput, phantom?: boolean): SpectrumWithLoudness;
32
+ static stretched(count: number, fundamentalHz: FractionInput, pseudoOctaveRatio: FractionInput, phantom?: boolean): SpectrumWithLoudness;
31
33
  }
@@ -51,6 +51,13 @@ export class SpectrumWithLoudness extends Spectrum {
51
51
  get(frequency) {
52
52
  return super.get(frequency);
53
53
  }
54
+ static fromFrequencies(frequencies, amplitudes, phases) {
55
+ const spectrum = new SpectrumWithLoudness();
56
+ for (let i = 0; i < frequencies.length; i++) {
57
+ spectrum.add(new HarmonicWithLoudness(frequencies[i], amplitudes?.[i] ?? 1, phases?.[i] ?? 0, false));
58
+ }
59
+ return spectrum;
60
+ }
54
61
  /**
55
62
  * Multiply this spectrum by another spectrum.
56
63
  * Each harmonic of this spectrum is multiplied by each harmonic of the other
@@ -102,4 +109,12 @@ export class SpectrumWithLoudness extends Spectrum {
102
109
  }
103
110
  return result;
104
111
  }
112
+ static stretched(count, fundamentalHz, pseudoOctaveRatio, phantom) {
113
+ const spectrum = super.stretched(count, fundamentalHz, pseudoOctaveRatio);
114
+ const result = new SpectrumWithLoudness();
115
+ for (const harmonic of spectrum.getHarmonics()) {
116
+ result.add(new HarmonicWithLoudness({ harmonic, phantom: phantom ?? false }));
117
+ }
118
+ return result;
119
+ }
105
120
  }
@@ -9,6 +9,8 @@ export declare const SETHARES_DISSONANCE_PARAMS: {
9
9
  readonly magnitude: 1;
10
10
  /** At 0: no decay (constant magnitude). Higher values = faster decay with minFrequency. */
11
11
  readonly magnitudeFrequencyDecay: 0;
12
+ /** At 0: flat (no mid-frequency notch). At 1: full cut at the notch center. */
13
+ readonly phantomNotchDepth: 0;
12
14
  };
13
15
  export declare const DEFAULT_FIRST_ORDER_DISSONANCE_PARAMS: {
14
16
  s1: 0.021;
@@ -18,6 +20,7 @@ export declare const DEFAULT_FIRST_ORDER_DISSONANCE_PARAMS: {
18
20
  x_star: 0.24;
19
21
  magnitude: 1;
20
22
  magnitudeFrequencyDecay: 0;
23
+ phantomNotchDepth: 0;
21
24
  };
22
25
  export declare const DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS: {
23
26
  magnitude: number;
@@ -27,10 +30,12 @@ export declare const DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS: {
27
30
  b1: 3.5;
28
31
  b2: 5.75;
29
32
  x_star: 0.24;
33
+ phantomNotchDepth: 0;
30
34
  };
31
35
  export declare const DEFAULT_THIRD_ORDER_DISSONANCE_PARAMS: {
32
36
  magnitude: number;
33
37
  magnitudeFrequencyDecay: number;
38
+ phantomNotchDepth: number;
34
39
  s1: 0.021;
35
40
  s2: 19;
36
41
  b1: 3.5;
package/dist/lib/const.js CHANGED
@@ -9,8 +9,15 @@ export const SETHARES_DISSONANCE_PARAMS = {
9
9
  magnitude: 1,
10
10
  /** At 0: no decay (constant magnitude). Higher values = faster decay with minFrequency. */
11
11
  magnitudeFrequencyDecay: 0,
12
+ /** At 0: flat (no mid-frequency notch). At 1: full cut at the notch center. */
13
+ phantomNotchDepth: 0,
12
14
  };
13
15
  export const DEFAULT_FIRST_ORDER_DISSONANCE_PARAMS = { ...SETHARES_DISSONANCE_PARAMS };
14
16
  export const DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS = { ...SETHARES_DISSONANCE_PARAMS, magnitude: 0.75, magnitudeFrequencyDecay: 0.75 };
15
- export const DEFAULT_THIRD_ORDER_DISSONANCE_PARAMS = { ...SETHARES_DISSONANCE_PARAMS, magnitude: 0.5, magnitudeFrequencyDecay: 1.75 };
17
+ export const DEFAULT_THIRD_ORDER_DISSONANCE_PARAMS = {
18
+ ...SETHARES_DISSONANCE_PARAMS,
19
+ magnitude: 0.5,
20
+ magnitudeFrequencyDecay: 0,
21
+ phantomNotchDepth: 1,
22
+ };
16
23
  export const DEFAULT_PHANTOM_HARMONICS_NUMBER = 3;
@@ -3,6 +3,8 @@ import type { DissonanceParams, SetharesDissonanceParams } from "./types";
3
3
  import type { HarmonicWithLoudness } from "../classes/private/HarmonicWithLoudness";
4
4
  import { SpectrumWithLoudness } from "../classes/private/SpectrumWithLoudness";
5
5
  export { getLoudness } from "./loudness";
6
+ export declare function getExponentialMagnitudeDecayFactor(minFrequency: number, decay: number): number;
7
+ export declare function getGaussianNotchMagnitudeDecayFactor(maxFrequency: number, depth: number): number;
6
8
  /** The formula to calculate sensory dissoannce proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
7
9
  export declare function getPlompLeveltDissonance(h1: Harmonic, h2: Harmonic, params: SetharesDissonanceParams): number;
8
10
  /**
package/dist/lib/utils.js CHANGED
@@ -2,6 +2,21 @@ import { DEFAULT_FIRST_ORDER_DISSONANCE_PARAMS, DEFAULT_SECOND_ORDER_DISSONANCE_
2
2
  import { getLoudness } from "./loudness";
3
3
  import { SpectrumWithLoudness } from "../classes/private/SpectrumWithLoudness";
4
4
  export { getLoudness } from "./loudness";
5
+ export function getExponentialMagnitudeDecayFactor(minFrequency, decay) {
6
+ return Math.exp(-decay * (minFrequency / 1000));
7
+ }
8
+ /** Third-order (both phantom): log-Gaussian notch. Lower q = narrower notch. */
9
+ const PHANTOM_NOTCH_CENTER_HZ = 880;
10
+ const PHANTOM_NOTCH_Q = 0.75;
11
+ /** Transposes notch for 2nd×3rd phantom pairs: 1760 Hz max → 880 Hz effective center. */
12
+ const PHANTOM_NOTCH_FREQUENCY_MULTIPLIER = 2;
13
+ export function getGaussianNotchMagnitudeDecayFactor(maxFrequency, depth) {
14
+ if (depth <= 0)
15
+ return 1;
16
+ const logRatio = Math.log(maxFrequency / (PHANTOM_NOTCH_CENTER_HZ * PHANTOM_NOTCH_FREQUENCY_MULTIPLIER));
17
+ const notchShape = Math.exp(-(logRatio ** 2) / (2 * PHANTOM_NOTCH_Q ** 2));
18
+ return 1 - depth * notchShape;
19
+ }
5
20
  /** The formula to calculate sensory dissoannce proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
6
21
  export function getPlompLeveltDissonance(h1, h2, params) {
7
22
  const magnitude = params?.magnitude ?? SETHARES_DISSONANCE_PARAMS.magnitude;
@@ -13,15 +28,18 @@ export function getPlompLeveltDissonance(h1, h2, params) {
13
28
  const b1 = params?.b1 ?? SETHARES_DISSONANCE_PARAMS.b1;
14
29
  const b2 = params?.b2 ?? SETHARES_DISSONANCE_PARAMS.b2;
15
30
  const magnitudeFrequencyDecay = params?.magnitudeFrequencyDecay ?? SETHARES_DISSONANCE_PARAMS.magnitudeFrequencyDecay;
31
+ const phantomNotchDepth = params?.phantomNotchDepth ?? SETHARES_DISSONANCE_PARAMS.phantomNotchDepth;
16
32
  if (h1.frequency.equals(h2.frequency))
17
33
  return 0;
18
34
  const minLoudness = Math.min("loudness" in h1 ? h1.loudness : getLoudness(h1.amplitude), "loudness" in h2 ? h2.loudness : getLoudness(h2.amplitude));
19
35
  if (minLoudness <= 0)
20
36
  return 0;
21
37
  const minFrequency = Math.min(h1.frequencyNum, h2.frequencyNum);
38
+ const maxFrequency = Math.max(h1.frequencyNum, h2.frequencyNum);
22
39
  if (minFrequency <= 0)
23
40
  return 0;
24
- const magnitudeDecayFactor = Math.exp(-magnitudeFrequencyDecay * (minFrequency / 1000));
41
+ const magnitudeDecayFactor = getExponentialMagnitudeDecayFactor(minFrequency, magnitudeFrequencyDecay) *
42
+ getGaussianNotchMagnitudeDecayFactor(maxFrequency, phantomNotchDepth);
25
43
  const frequencyDifference = Math.abs(h1.frequencyNum - h2.frequencyNum);
26
44
  const s = x_star / (s1 * minFrequency + s2);
27
45
  return (magnitude *
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS, DEFAULT_THIRD_ORDER_DISSONANCE_PARAMS } from "../lib/const";
2
+ import { getExponentialMagnitudeDecayFactor, getGaussianNotchMagnitudeDecayFactor, } from "../lib/utils";
3
+ const exponentialDecay = DEFAULT_SECOND_ORDER_DISSONANCE_PARAMS.magnitudeFrequencyDecay;
4
+ const notchDepth = DEFAULT_THIRD_ORDER_DISSONANCE_PARAMS.phantomNotchDepth;
5
+ const frequencies = new Set();
6
+ for (let f = 50; f <= 2000; f += 10) {
7
+ frequencies.add(f);
8
+ }
9
+ for (const f of [147, 440, 880, 1320, 1760]) {
10
+ frequencies.add(f);
11
+ }
12
+ const sortedFrequencies = [...frequencies].sort((a, b) => a - b);
13
+ const lines = ["max_frequency_hz,exponential,gaussian_notch,combined_third_order"];
14
+ for (const frequency of sortedFrequencies) {
15
+ const exponential = getExponentialMagnitudeDecayFactor(frequency, exponentialDecay);
16
+ const gaussianNotch = getGaussianNotchMagnitudeDecayFactor(frequency, notchDepth);
17
+ const combined = exponential * gaussianNotch;
18
+ lines.push(`${frequency},${exponential},${gaussianNotch},${combined}`);
19
+ }
20
+ Bun.write("./manual-testing/decay-factors.csv", lines.join("\n"));
@@ -1,16 +1,41 @@
1
1
  import { Spectrum } from "tuning-core";
2
2
  import { DissonanceCurve } from "../classes";
3
- const context = Spectrum.harmonic(1, 440);
4
- const complement = Spectrum.harmonic(1, 440);
5
- const curve = new DissonanceCurve({
6
- context,
7
- complement,
3
+ const spectrum_140 = Spectrum.harmonic(1, 140);
4
+ const spectrum_440 = Spectrum.harmonic(1, 440);
5
+ const spectrum_1320 = Spectrum.harmonic(1, 1320);
6
+ const curve_140 = new DissonanceCurve({
7
+ context: spectrum_140,
8
+ complement: spectrum_140.clone(),
8
9
  start: 1,
9
10
  end: 2,
10
11
  firstOrderDissonance: { magnitude: 1 },
11
12
  secondOrderDissonance: { magnitude: 0.3 },
12
13
  thirdOrderDissonance: { magnitude: 0.1 },
13
- phantomHarmonicsNumber: 2,
14
- maxGapCents: 20,
14
+ phantomHarmonicsNumber: 3,
15
+ maxGapCents: 1,
15
16
  });
16
- Bun.write("./manual-testing/curve.csv", curve.normalize().toCsvFileBuffer());
17
+ const curve_440 = new DissonanceCurve({
18
+ context: spectrum_440,
19
+ complement: spectrum_440.clone(),
20
+ start: 1,
21
+ end: 2,
22
+ firstOrderDissonance: { magnitude: 1 },
23
+ secondOrderDissonance: { magnitude: 0.3 },
24
+ thirdOrderDissonance: { magnitude: 0.1 },
25
+ phantomHarmonicsNumber: 3,
26
+ maxGapCents: 1,
27
+ });
28
+ const curve_1320 = new DissonanceCurve({
29
+ context: spectrum_1320,
30
+ complement: spectrum_1320.clone(),
31
+ start: 1,
32
+ end: 2,
33
+ firstOrderDissonance: { magnitude: 1 },
34
+ secondOrderDissonance: { magnitude: 0.3 },
35
+ thirdOrderDissonance: { magnitude: 0.1 },
36
+ phantomHarmonicsNumber: 3,
37
+ maxGapCents: 1,
38
+ });
39
+ Bun.write("./manual-testing/curve_140.csv", curve_140.normalize().toCsvFileBuffer());
40
+ Bun.write("./manual-testing/curve_440.csv", curve_440.normalize().toCsvFileBuffer());
41
+ Bun.write("./manual-testing/curve_1320.csv", curve_1320.normalize().toCsvFileBuffer());
package/dist/test.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/test.js ADDED
@@ -0,0 +1,17 @@
1
+ import { Spectrum } from "tuning-core";
2
+ import { SpectrumWithLoudness } from "./classes/private/SpectrumWithLoudness";
3
+ import { getIntrinsicDissonance } from "./lib";
4
+ const harmonic = SpectrumWithLoudness.harmonic(6, 261.63);
5
+ const stretchedOld = SpectrumWithLoudness.fromFrequencies([
6
+ 261.63, 544.851, 835.837, 1134.665, 1448.987, 1740.652
7
+ ], [
8
+ 1, 0.560, 0.4, 0.314, 0.259, 0.224
9
+ ]);
10
+ const stretchedNew = Spectrum.stretched(6, 261.63, 544.851 / 261.63);
11
+ const phantomHarmonics = SpectrumWithLoudness.harmonic(3, 1, true);
12
+ const harmonicDissonance = getIntrinsicDissonance(harmonic.mul(phantomHarmonics));
13
+ const stretchedOldDissonance = getIntrinsicDissonance(stretchedOld.mul(phantomHarmonics));
14
+ const stretchedNewDissonance = getIntrinsicDissonance(stretchedOld.mul(phantomHarmonics));
15
+ console.log('stretchedNewDissonance', stretchedNewDissonance);
16
+ console.log('stretchedOldDissonance', stretchedOldDissonance);
17
+ console.log('harmonicDissonance', harmonicDissonance);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sethares-dissonance",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "type": "module",
5
5
  "devDependencies": {
6
6
  "@types/bun": "latest"
@@ -21,6 +21,6 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "fraction.js": "^5.3.4",
24
- "tuning-core": "^1.2.0"
24
+ "tuning-core": "^1.5.2"
25
25
  }
26
26
  }