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.
- package/classes/DissonanceCurve.ts +37 -21
- package/lib/utils.ts +52 -44
- package/package.json +1 -1
- package/tests/utils.test.ts +12 -0
|
@@ -1,45 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
23
|
+
public readonly step: NonNullable<DissonanceCurveOptions["step"]>;
|
|
18
24
|
public readonly context: DissonanceCurveOptions["context"];
|
|
19
|
-
public readonly
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
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.
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
18
|
+
const phons = 20 * Math.log10(pressure / referencePressure);
|
|
12
19
|
|
|
13
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
loudness1: number,
|
|
30
|
-
loudness2: number,
|
|
40
|
+
partial1: SpectrumPartial,
|
|
41
|
+
partial2: SpectrumPartial,
|
|
31
42
|
params = SETHARES_DISSONANCE_PARAMS
|
|
32
43
|
): number {
|
|
33
|
-
if (
|
|
44
|
+
if (partial1.rate === partial2.rate) return 0;
|
|
34
45
|
|
|
35
|
-
const minLoudness = Math.min(
|
|
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(
|
|
39
|
-
const frequencyDifference = Math.abs(
|
|
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:
|
|
54
|
-
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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:
|
|
78
|
-
spectrum2:
|
|
79
|
-
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 =
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
116
|
+
return Math.pow(2, cents / 1200);
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
export function transpose(
|
|
112
|
-
const result:
|
|
119
|
+
export function transpose(partials: SpectrumPartial[], cents: number) {
|
|
120
|
+
const result: SpectrumPartial[] = [];
|
|
113
121
|
|
|
114
|
-
for (const partial of
|
|
122
|
+
for (const partial of partials) {
|
|
115
123
|
result.push({
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
rate: partial.rate * centsToRatio(cents),
|
|
125
|
+
amplitude: partial.amplitude,
|
|
118
126
|
});
|
|
119
127
|
}
|
|
120
128
|
|
package/package.json
CHANGED
|
@@ -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
|
+
});
|