sethares-dissonance 0.0.1 → 0.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.
@@ -1,45 +1,57 @@
1
- import { getSetharesDissonance, transpose, type Spectrum } from "../lib/utils";
2
-
3
- type DissonanceCurveOptions = {
4
- context: Spectrum;
5
- compliment: Spectrum;
6
- precision?: number;
1
+ import {
2
+ getSetharesDissonance,
3
+ SETHARES_DISSONANCE_PARAMS,
4
+ transpose,
5
+ type SpectrumPartial,
6
+ } from "../lib";
7
+
8
+ export type DissonanceCurveOptions = Partial<
9
+ typeof SETHARES_DISSONANCE_PARAMS
10
+ > & {
11
+ context: SpectrumPartial[];
12
+ complement: SpectrumPartial[];
13
+ step?: number;
7
14
  rangeMin?: number;
8
15
  rangeMax?: number;
9
16
  };
10
17
 
11
18
  export class DissonanceCurve {
12
19
  private _data: Map<number, number> = new Map();
13
- private step: number;
14
20
 
15
21
  public readonly rangeMin: NonNullable<DissonanceCurveOptions["rangeMin"]>;
16
22
  public readonly rangeMax: NonNullable<DissonanceCurveOptions["rangeMax"]>;
17
- public readonly precision: NonNullable<DissonanceCurveOptions["precision"]>;
23
+ public readonly step: NonNullable<DissonanceCurveOptions["step"]>;
18
24
  public readonly context: DissonanceCurveOptions["context"];
19
- public readonly compliment: DissonanceCurveOptions["compliment"];
25
+ public readonly complement: DissonanceCurveOptions["complement"];
20
26
  public readonly maxDissonance: number = 0;
21
27
  public readonly change: number = 0;
22
28
 
23
29
  constructor(opts: DissonanceCurveOptions) {
24
- this.context = opts.context;
25
- this.compliment = opts.compliment;
26
-
27
- this.rangeMin = opts.rangeMin ?? 0;
28
- this.rangeMax = opts.rangeMax ?? 1200;
29
- this.precision = opts.precision ?? 1;
30
+ const {
31
+ context,
32
+ complement,
33
+ rangeMin,
34
+ rangeMax,
35
+ step,
36
+ ...dissonanceParams
37
+ } = opts;
38
+
39
+ this.context = context;
40
+ this.complement = complement;
41
+
42
+ this.rangeMin = rangeMin ?? 0;
43
+ this.rangeMax = rangeMax ?? 1200;
44
+ this.step = step ?? 1;
30
45
 
31
46
  if (this.rangeMin > this.rangeMax)
32
47
  throw Error("rangeMin should be less or equal to rangeMax");
33
- if (this.precision <= 0)
34
- throw Error("precision should be greater than zero");
35
-
36
- this.step =
37
- (this.rangeMax - this.rangeMin) / (this.rangeMax * this.precision);
48
+ if (this.step <= 0) throw Error("precision should be greater than zero");
38
49
 
39
50
  for (let cent = this.rangeMin; cent <= this.rangeMax; cent += this.step) {
40
51
  const dissonance = getSetharesDissonance(
41
52
  this.context,
42
- transpose(this.compliment, cent)
53
+ transpose(this.complement, cent),
54
+ dissonanceParams
43
55
  );
44
56
 
45
57
  if (dissonance > this.maxDissonance) this.maxDissonance = dissonance;
@@ -52,6 +64,10 @@ export class DissonanceCurve {
52
64
  return Array.from(this._data.entries()).sort((a, b) => a[0] - b[0]);
53
65
  }
54
66
 
67
+ public get(cent: number) {
68
+ return this._data.get(cent);
69
+ }
70
+
55
71
  private getRowString(row: Array<number | string>) {
56
72
  if (row.length === 0) return "";
57
73
 
package/lib/utils.ts CHANGED
@@ -1,16 +1,25 @@
1
- export type Spectrum = {
2
- freq: number;
3
- loudness: number;
4
- }[];
1
+ export type SpectrumPartial = {
2
+ rate: number;
3
+ amplitude: number;
4
+ phase?: number;
5
+ };
6
+
7
+ const NORMALISATION_PRESSURE_UNIT = 2.8284271247461905;
5
8
 
6
- /** The formula to calculate loudness from amplitude proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
7
- export function getSetharesLoudness(amplitude: number): number {
8
- const P_e = amplitude / Math.SQRT2;
9
- const P_ref = 20;
9
+ /**
10
+ * The formula to calculate loudness from amplitude. The converstion to SPL (phons) is done according to Sethares in the appendix "How to Draw Dissonance Curves". The converstion to loudness is done according to https://sengpielaudio.com/calculatorSonephon.htm
11
+ * @param {number} amplitude - the normalized peak value where 0.001 corresponds to SPL of 40 db or 1 sone and 1 to SPL of 100db and 64 sones. Normalisation is done for the simplicity of providing harmonic spectrum with amplitudes 1, 1/2, 1/3, ...
12
+ */
13
+ export function getLoudness(amplitude: number): number {
14
+ const rms = amplitude / Math.SQRT2;
15
+ const pressure = rms * NORMALISATION_PRESSURE_UNIT;
16
+ const referencePressure = 2e-5;
10
17
 
11
- const SPL = 20 * Math.log10(P_e / P_ref);
18
+ const phons = 20 * Math.log10(pressure / referencePressure);
12
19
 
13
- return 2 ** (SPL / 10) / 16;
20
+ if (phons < 8) return 0;
21
+ if (phons < 40) return Math.pow(phons / 40, 2.86) - 0.005;
22
+ return Math.pow(2, (phons - 40) / 10);
14
23
  }
15
24
 
16
25
  /** Fitting parameters proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
@@ -22,21 +31,26 @@ export const SETHARES_DISSONANCE_PARAMS = {
22
31
  x_star: 0.24,
23
32
  };
24
33
 
34
+ export type SetharesDissonanceParams = Partial<
35
+ typeof SETHARES_DISSONANCE_PARAMS
36
+ >;
37
+
25
38
  /** The formula to calculate sensory dissoannce proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
26
39
  export function getPlompLeveltDissonance(
27
- freq1: number,
28
- freq2: number,
29
- loudness1: number,
30
- loudness2: number,
40
+ partial1: SpectrumPartial,
41
+ partial2: SpectrumPartial,
31
42
  params = SETHARES_DISSONANCE_PARAMS
32
43
  ): number {
33
- if (freq1 === freq2) return 0;
44
+ if (partial1.rate === partial2.rate) return 0;
34
45
 
35
- const minLoudness = Math.min(loudness1, loudness2);
46
+ const minLoudness = Math.min(
47
+ getLoudness(partial1.amplitude),
48
+ getLoudness(partial2.amplitude)
49
+ );
36
50
  if (minLoudness <= 0) return 0;
37
51
 
38
- const minFrequency = Math.min(freq1, freq2);
39
- const frequencyDifference = Math.abs(freq1 - freq2);
52
+ const minFrequency = Math.min(partial1.rate, partial2.rate);
53
+ const frequencyDifference = Math.abs(partial1.rate - partial2.rate);
40
54
 
41
55
  if (minFrequency <= 0) return 0;
42
56
 
@@ -50,8 +64,8 @@ export function getPlompLeveltDissonance(
50
64
  }
51
65
 
52
66
  export function getIntrinsicDissonance(
53
- spectrum: Spectrum,
54
- params = SETHARES_DISSONANCE_PARAMS
67
+ spectrum: SpectrumPartial[],
68
+ params?: SetharesDissonanceParams
55
69
  ) {
56
70
  let dissonance = 0;
57
71
 
@@ -60,13 +74,10 @@ export function getIntrinsicDissonance(
60
74
  const partial1 = spectrum[i]!;
61
75
  const partial2 = spectrum[j]!;
62
76
 
63
- dissonance += getPlompLeveltDissonance(
64
- partial1.freq,
65
- partial2.freq,
66
- partial1.loudness,
67
- partial2.loudness,
68
- params
69
- );
77
+ dissonance += getPlompLeveltDissonance(partial1, partial2, {
78
+ ...SETHARES_DISSONANCE_PARAMS,
79
+ ...params,
80
+ });
70
81
  }
71
82
  }
72
83
 
@@ -74,26 +85,23 @@ export function getIntrinsicDissonance(
74
85
  }
75
86
 
76
87
  export function getSetharesDissonance(
77
- spectrum1: Spectrum,
78
- spectrum2: Spectrum,
79
- params = SETHARES_DISSONANCE_PARAMS
88
+ spectrum1: SpectrumPartial[],
89
+ spectrum2: SpectrumPartial[],
90
+ params?: SetharesDissonanceParams
80
91
  ) {
81
92
  let dissonance =
82
93
  getIntrinsicDissonance(spectrum1, params) +
83
94
  getIntrinsicDissonance(spectrum2, params);
84
95
 
85
96
  for (let i = 0; i < spectrum1.length; i++) {
86
- for (let j = i + 1; j < spectrum2.length; j++) {
97
+ for (let j = 0; j < spectrum2.length; j++) {
87
98
  const partial1 = spectrum1[i]!;
88
99
  const partial2 = spectrum2[j]!;
89
100
 
90
- dissonance += getPlompLeveltDissonance(
91
- partial1.freq,
92
- partial2.freq,
93
- partial1.loudness,
94
- partial2.loudness,
95
- params
96
- );
101
+ dissonance += getPlompLeveltDissonance(partial1, partial2, {
102
+ ...SETHARES_DISSONANCE_PARAMS,
103
+ ...params,
104
+ });
97
105
  }
98
106
  }
99
107
 
@@ -105,16 +113,16 @@ export function ratioToCents(ratio: number): number {
105
113
  }
106
114
 
107
115
  export function centsToRatio(cents: number): number {
108
- return 2 ** (cents / 1200);
116
+ return Math.pow(2, cents / 1200);
109
117
  }
110
118
 
111
- export function transpose(spectrum: Spectrum, cents: number) {
112
- const result: Spectrum = [];
119
+ export function transpose(partials: SpectrumPartial[], cents: number) {
120
+ const result: SpectrumPartial[] = [];
113
121
 
114
- for (const partial of spectrum) {
122
+ for (const partial of partials) {
115
123
  result.push({
116
- freq: partial.freq * centsToRatio(cents),
117
- loudness: partial.loudness,
124
+ rate: partial.rate * centsToRatio(cents),
125
+ amplitude: partial.amplitude,
118
126
  });
119
127
  }
120
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sethares-dissonance",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "devDependencies": {
@@ -0,0 +1,12 @@
1
+ import { expect, test, describe } from "bun:test";
2
+
3
+ import * as Utils from "../lib/utils";
4
+
5
+ describe("lib/utils", () => {
6
+ test("Should calculate loudness", () => {
7
+ const amplitudes = [1, 0.001, 0.000025];
8
+ const expectedOutcome = [64, 1, 0];
9
+
10
+ expect(amplitudes.map(Utils.getLoudness)).toEqual(expectedOutcome);
11
+ });
12
+ });