sethares-dissonance 0.1.2 → 0.2.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/dist/classes/DissonanceCurve.d.ts +23 -0
- package/dist/classes/DissonanceCurve.js +57 -0
- package/dist/classes/index.d.ts +1 -0
- package/dist/classes/index.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/utils.d.ts +32 -0
- package/dist/lib/utils.js +85 -0
- package/dist/tests/utils.test.d.ts +1 -0
- package/dist/tests/utils.test.js +16 -0
- package/package.json +7 -1
- package/classes/DissonanceCurve.ts +0 -99
- package/classes/index.ts +0 -1
- package/index.ts +0 -2
- package/lib/index.ts +0 -1
- package/lib/utils.ts +0 -130
- package/tests/utils.test.ts +0 -21
- package/tsconfig.json +0 -29
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SETHARES_DISSONANCE_PARAMS, type SpectrumPartial } from "../lib";
|
|
2
|
+
export type DissonanceCurveOptions = Partial<typeof SETHARES_DISSONANCE_PARAMS> & {
|
|
3
|
+
context: SpectrumPartial[];
|
|
4
|
+
complement: SpectrumPartial[];
|
|
5
|
+
step?: number;
|
|
6
|
+
rangeMin?: number;
|
|
7
|
+
rangeMax?: number;
|
|
8
|
+
};
|
|
9
|
+
export declare class DissonanceCurve {
|
|
10
|
+
private _data;
|
|
11
|
+
readonly rangeMin: NonNullable<DissonanceCurveOptions["rangeMin"]>;
|
|
12
|
+
readonly rangeMax: NonNullable<DissonanceCurveOptions["rangeMax"]>;
|
|
13
|
+
readonly step: NonNullable<DissonanceCurveOptions["step"]>;
|
|
14
|
+
readonly context: DissonanceCurveOptions["context"];
|
|
15
|
+
readonly complement: DissonanceCurveOptions["complement"];
|
|
16
|
+
readonly maxDissonance: number;
|
|
17
|
+
readonly change: number;
|
|
18
|
+
constructor(opts: DissonanceCurveOptions);
|
|
19
|
+
get points(): [number, number][];
|
|
20
|
+
get(cent: number): number | undefined;
|
|
21
|
+
private getRowString;
|
|
22
|
+
toFileString(): string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getSetharesDissonance, SETHARES_DISSONANCE_PARAMS, transpose } from "../lib";
|
|
2
|
+
export class DissonanceCurve {
|
|
3
|
+
_data = new Map();
|
|
4
|
+
rangeMin;
|
|
5
|
+
rangeMax;
|
|
6
|
+
step;
|
|
7
|
+
context;
|
|
8
|
+
complement;
|
|
9
|
+
maxDissonance = 0;
|
|
10
|
+
change = 0;
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
const { context, complement, rangeMin, rangeMax, step, ...dissonanceParams } = opts;
|
|
13
|
+
this.context = context;
|
|
14
|
+
this.complement = complement;
|
|
15
|
+
this.rangeMin = rangeMin ?? 0;
|
|
16
|
+
this.rangeMax = rangeMax ?? 1200;
|
|
17
|
+
this.step = step ?? 1;
|
|
18
|
+
if (this.rangeMin > this.rangeMax)
|
|
19
|
+
throw Error("rangeMin should be less or equal to rangeMax");
|
|
20
|
+
if (this.step <= 0)
|
|
21
|
+
throw Error("precision should be greater than zero");
|
|
22
|
+
for (let cent = this.rangeMin; cent <= this.rangeMax; cent += this.step) {
|
|
23
|
+
const dissonance = getSetharesDissonance(this.context, transpose(this.complement, cent), dissonanceParams);
|
|
24
|
+
if (dissonance > this.maxDissonance)
|
|
25
|
+
this.maxDissonance = dissonance;
|
|
26
|
+
this._data.set(cent, dissonance);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
get points() {
|
|
30
|
+
return Array.from(this._data.entries()).sort((a, b) => a[0] - b[0]);
|
|
31
|
+
}
|
|
32
|
+
get(cent) {
|
|
33
|
+
return this._data.get(cent);
|
|
34
|
+
}
|
|
35
|
+
getRowString(row) {
|
|
36
|
+
if (row.length === 0)
|
|
37
|
+
return "";
|
|
38
|
+
let result = `${row[0]}`;
|
|
39
|
+
for (let i = 1; i < row.length; i += 1) {
|
|
40
|
+
result += `\t${row[i]}`;
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
toFileString() {
|
|
45
|
+
if (this._data.size === 0)
|
|
46
|
+
return "";
|
|
47
|
+
const headerRow = this.getRowString([
|
|
48
|
+
"Interval (cents)",
|
|
49
|
+
"Sensory dissonance",
|
|
50
|
+
]);
|
|
51
|
+
let result = headerRow + "\n";
|
|
52
|
+
for (const point of this.points) {
|
|
53
|
+
result += this.getRowString(point);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./DissonanceCurve";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./DissonanceCurve";
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./utils";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./utils";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type SpectrumPartial = {
|
|
2
|
+
rate: number;
|
|
3
|
+
amplitude: number;
|
|
4
|
+
phase?: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* 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
|
|
8
|
+
* @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, ...
|
|
9
|
+
*/
|
|
10
|
+
export declare function getLoudness(amplitude: number): number;
|
|
11
|
+
/** Fitting parameters proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
|
|
12
|
+
export declare const SETHARES_DISSONANCE_PARAMS: {
|
|
13
|
+
s1: number;
|
|
14
|
+
s2: number;
|
|
15
|
+
b1: number;
|
|
16
|
+
b2: number;
|
|
17
|
+
x_star: number;
|
|
18
|
+
};
|
|
19
|
+
export type SetharesDissonanceParams = Partial<typeof SETHARES_DISSONANCE_PARAMS>;
|
|
20
|
+
/** The formula to calculate sensory dissoannce proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
|
|
21
|
+
export declare function getPlompLeveltDissonance(partial1: SpectrumPartial, partial2: SpectrumPartial, params?: {
|
|
22
|
+
s1: number;
|
|
23
|
+
s2: number;
|
|
24
|
+
b1: number;
|
|
25
|
+
b2: number;
|
|
26
|
+
x_star: number;
|
|
27
|
+
}): number;
|
|
28
|
+
export declare function getIntrinsicDissonance(spectrum: SpectrumPartial[], params?: SetharesDissonanceParams): number;
|
|
29
|
+
export declare function getSetharesDissonance(spectrum1: SpectrumPartial[], spectrum2: SpectrumPartial[], params?: SetharesDissonanceParams): number;
|
|
30
|
+
export declare function ratioToCents(ratio: number): number;
|
|
31
|
+
export declare function centsToRatio(cents: number): number;
|
|
32
|
+
export declare function transpose(partials: SpectrumPartial[], cents: number): SpectrumPartial[];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const NORMALISATION_PRESSURE_UNIT = 2.8284271247461905;
|
|
2
|
+
/**
|
|
3
|
+
* 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
|
|
4
|
+
* @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, ...
|
|
5
|
+
*/
|
|
6
|
+
export function getLoudness(amplitude) {
|
|
7
|
+
const rms = amplitude / Math.SQRT2;
|
|
8
|
+
const pressure = rms * NORMALISATION_PRESSURE_UNIT;
|
|
9
|
+
const referencePressure = 2e-5;
|
|
10
|
+
const phons = 20 * Math.log10(pressure / referencePressure);
|
|
11
|
+
if (phons < 8)
|
|
12
|
+
return 0;
|
|
13
|
+
if (phons < 40)
|
|
14
|
+
return Math.pow(phons / 40, 2.86) - 0.005;
|
|
15
|
+
return Math.pow(2, (phons - 40) / 10);
|
|
16
|
+
}
|
|
17
|
+
/** Fitting parameters proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
|
|
18
|
+
export const SETHARES_DISSONANCE_PARAMS = {
|
|
19
|
+
s1: 0.021,
|
|
20
|
+
s2: 19,
|
|
21
|
+
b1: 3.5,
|
|
22
|
+
b2: 5.75,
|
|
23
|
+
x_star: 0.24,
|
|
24
|
+
};
|
|
25
|
+
/** The formula to calculate sensory dissoannce proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
|
|
26
|
+
export function getPlompLeveltDissonance(partial1, partial2, params = SETHARES_DISSONANCE_PARAMS) {
|
|
27
|
+
if (partial1.rate === partial2.rate)
|
|
28
|
+
return 0;
|
|
29
|
+
const minLoudness = Math.min(getLoudness(partial1.amplitude), getLoudness(partial2.amplitude));
|
|
30
|
+
if (minLoudness <= 0)
|
|
31
|
+
return 0;
|
|
32
|
+
const minFrequency = Math.min(partial1.rate, partial2.rate);
|
|
33
|
+
const frequencyDifference = Math.abs(partial1.rate - partial2.rate);
|
|
34
|
+
if (minFrequency <= 0)
|
|
35
|
+
return 0;
|
|
36
|
+
const s = params.x_star / (params.s1 * minFrequency + params.s2);
|
|
37
|
+
return (minLoudness *
|
|
38
|
+
(Math.exp(-1 * params.b1 * s * frequencyDifference) -
|
|
39
|
+
Math.exp(-1 * params.b2 * s * frequencyDifference)));
|
|
40
|
+
}
|
|
41
|
+
export function getIntrinsicDissonance(spectrum, params) {
|
|
42
|
+
let dissonance = 0;
|
|
43
|
+
for (let i = 0; i < spectrum.length; i++) {
|
|
44
|
+
for (let j = i + 1; j < spectrum.length; j++) {
|
|
45
|
+
const partial1 = spectrum[i];
|
|
46
|
+
const partial2 = spectrum[j];
|
|
47
|
+
dissonance += getPlompLeveltDissonance(partial1, partial2, {
|
|
48
|
+
...SETHARES_DISSONANCE_PARAMS,
|
|
49
|
+
...params,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return dissonance;
|
|
54
|
+
}
|
|
55
|
+
export function getSetharesDissonance(spectrum1, spectrum2, params) {
|
|
56
|
+
let dissonance = getIntrinsicDissonance(spectrum1, params) +
|
|
57
|
+
getIntrinsicDissonance(spectrum2, params);
|
|
58
|
+
for (let i = 0; i < spectrum1.length; i++) {
|
|
59
|
+
for (let j = 0; j < spectrum2.length; j++) {
|
|
60
|
+
const partial1 = spectrum1[i];
|
|
61
|
+
const partial2 = spectrum2[j];
|
|
62
|
+
dissonance += getPlompLeveltDissonance(partial1, partial2, {
|
|
63
|
+
...SETHARES_DISSONANCE_PARAMS,
|
|
64
|
+
...params,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return dissonance;
|
|
69
|
+
}
|
|
70
|
+
export function ratioToCents(ratio) {
|
|
71
|
+
return ratio > 0 ? 1200 * Math.log2(ratio) : 0;
|
|
72
|
+
}
|
|
73
|
+
export function centsToRatio(cents) {
|
|
74
|
+
return Math.pow(2, cents / 1200);
|
|
75
|
+
}
|
|
76
|
+
export function transpose(partials, cents) {
|
|
77
|
+
const result = [];
|
|
78
|
+
for (const partial of partials) {
|
|
79
|
+
result.push({
|
|
80
|
+
rate: partial.rate * centsToRatio(cents),
|
|
81
|
+
amplitude: partial.amplitude,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import * as Utils from "../lib/utils";
|
|
3
|
+
import { DissonanceCurve } from "../classes";
|
|
4
|
+
describe("lib/utils", () => {
|
|
5
|
+
test("Should calculate loudness", () => {
|
|
6
|
+
const amplitudes = [1, 0.001, 0.000025];
|
|
7
|
+
const expectedOutcome = [64, 1, 0];
|
|
8
|
+
expect(amplitudes.map(Utils.getLoudness)).toEqual(expectedOutcome);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
describe("classes/DissoannceCurve", () => {
|
|
12
|
+
test("Should construct Dissoancne curve", () => {
|
|
13
|
+
const curve = new DissonanceCurve({ context: [], complement: [] });
|
|
14
|
+
expect(curve).toBeInstanceOf(DissonanceCurve);
|
|
15
|
+
});
|
|
16
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sethares-dissonance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"devDependencies": {
|
|
@@ -8,5 +8,11 @@
|
|
|
8
8
|
},
|
|
9
9
|
"peerDependencies": {
|
|
10
10
|
"typescript": "^5"
|
|
11
|
+
},
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"files": ["./dist"],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "npx tsc"
|
|
11
17
|
}
|
|
12
18
|
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getSetharesDissonance,
|
|
3
|
-
SETHARES_DISSONANCE_PARAMS,
|
|
4
|
-
transpose,
|
|
5
|
-
} from "../lib";
|
|
6
|
-
import type { SpectrumPartial } from "../lib";
|
|
7
|
-
|
|
8
|
-
export type DissonanceCurveOptions = Partial<
|
|
9
|
-
typeof SETHARES_DISSONANCE_PARAMS
|
|
10
|
-
> & {
|
|
11
|
-
context: SpectrumPartial[];
|
|
12
|
-
complement: SpectrumPartial[];
|
|
13
|
-
step?: number;
|
|
14
|
-
rangeMin?: number;
|
|
15
|
-
rangeMax?: number;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export class DissonanceCurve {
|
|
19
|
-
private _data: Map<number, number> = new Map();
|
|
20
|
-
|
|
21
|
-
public readonly rangeMin: NonNullable<DissonanceCurveOptions["rangeMin"]>;
|
|
22
|
-
public readonly rangeMax: NonNullable<DissonanceCurveOptions["rangeMax"]>;
|
|
23
|
-
public readonly step: NonNullable<DissonanceCurveOptions["step"]>;
|
|
24
|
-
public readonly context: DissonanceCurveOptions["context"];
|
|
25
|
-
public readonly complement: DissonanceCurveOptions["complement"];
|
|
26
|
-
public readonly maxDissonance: number = 0;
|
|
27
|
-
public readonly change: number = 0;
|
|
28
|
-
|
|
29
|
-
constructor(opts: DissonanceCurveOptions) {
|
|
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;
|
|
45
|
-
|
|
46
|
-
if (this.rangeMin > this.rangeMax)
|
|
47
|
-
throw Error("rangeMin should be less or equal to rangeMax");
|
|
48
|
-
if (this.step <= 0) throw Error("precision should be greater than zero");
|
|
49
|
-
|
|
50
|
-
for (let cent = this.rangeMin; cent <= this.rangeMax; cent += this.step) {
|
|
51
|
-
const dissonance = getSetharesDissonance(
|
|
52
|
-
this.context,
|
|
53
|
-
transpose(this.complement, cent),
|
|
54
|
-
dissonanceParams
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
if (dissonance > this.maxDissonance) this.maxDissonance = dissonance;
|
|
58
|
-
|
|
59
|
-
this._data.set(cent, dissonance);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
public get points() {
|
|
64
|
-
return Array.from(this._data.entries()).sort((a, b) => a[0] - b[0]);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
public get(cent: number) {
|
|
68
|
-
return this._data.get(cent);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private getRowString(row: Array<number | string>) {
|
|
72
|
-
if (row.length === 0) return "";
|
|
73
|
-
|
|
74
|
-
let result = `${row[0]}`;
|
|
75
|
-
|
|
76
|
-
for (let i = 1; i < row.length; i += 1) {
|
|
77
|
-
result += `\t${row[i]}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
public toFileString() {
|
|
84
|
-
if (this._data.size === 0) return "";
|
|
85
|
-
|
|
86
|
-
const headerRow = this.getRowString([
|
|
87
|
-
"Interval (cents)",
|
|
88
|
-
"Sensory dissonance",
|
|
89
|
-
]);
|
|
90
|
-
|
|
91
|
-
let result = headerRow + "\n";
|
|
92
|
-
|
|
93
|
-
for (const point of this.points) {
|
|
94
|
-
result += this.getRowString(point);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return result;
|
|
98
|
-
}
|
|
99
|
-
}
|
package/classes/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./DissonanceCurve"
|
package/index.ts
DELETED
package/lib/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./utils"
|
package/lib/utils.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
export type SpectrumPartial = {
|
|
2
|
-
rate: number;
|
|
3
|
-
amplitude: number;
|
|
4
|
-
phase?: number;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
const NORMALISATION_PRESSURE_UNIT = 2.8284271247461905;
|
|
8
|
-
|
|
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;
|
|
17
|
-
|
|
18
|
-
const phons = 20 * Math.log10(pressure / referencePressure);
|
|
19
|
-
|
|
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);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Fitting parameters proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
|
|
26
|
-
export const SETHARES_DISSONANCE_PARAMS = {
|
|
27
|
-
s1: 0.021,
|
|
28
|
-
s2: 19,
|
|
29
|
-
b1: 3.5,
|
|
30
|
-
b2: 5.75,
|
|
31
|
-
x_star: 0.24,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type SetharesDissonanceParams = Partial<
|
|
35
|
-
typeof SETHARES_DISSONANCE_PARAMS
|
|
36
|
-
>;
|
|
37
|
-
|
|
38
|
-
/** The formula to calculate sensory dissoannce proposed by Sethares in the appendix "How to Draw Dissonance Curves" */
|
|
39
|
-
export function getPlompLeveltDissonance(
|
|
40
|
-
partial1: SpectrumPartial,
|
|
41
|
-
partial2: SpectrumPartial,
|
|
42
|
-
params = SETHARES_DISSONANCE_PARAMS
|
|
43
|
-
): number {
|
|
44
|
-
if (partial1.rate === partial2.rate) return 0;
|
|
45
|
-
|
|
46
|
-
const minLoudness = Math.min(
|
|
47
|
-
getLoudness(partial1.amplitude),
|
|
48
|
-
getLoudness(partial2.amplitude)
|
|
49
|
-
);
|
|
50
|
-
if (minLoudness <= 0) return 0;
|
|
51
|
-
|
|
52
|
-
const minFrequency = Math.min(partial1.rate, partial2.rate);
|
|
53
|
-
const frequencyDifference = Math.abs(partial1.rate - partial2.rate);
|
|
54
|
-
|
|
55
|
-
if (minFrequency <= 0) return 0;
|
|
56
|
-
|
|
57
|
-
const s = params.x_star / (params.s1 * minFrequency + params.s2);
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
minLoudness *
|
|
61
|
-
(Math.exp(-1 * params.b1 * s * frequencyDifference) -
|
|
62
|
-
Math.exp(-1 * params.b2 * s * frequencyDifference))
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function getIntrinsicDissonance(
|
|
67
|
-
spectrum: SpectrumPartial[],
|
|
68
|
-
params?: SetharesDissonanceParams
|
|
69
|
-
) {
|
|
70
|
-
let dissonance = 0;
|
|
71
|
-
|
|
72
|
-
for (let i = 0; i < spectrum.length; i++) {
|
|
73
|
-
for (let j = i + 1; j < spectrum.length; j++) {
|
|
74
|
-
const partial1 = spectrum[i]!;
|
|
75
|
-
const partial2 = spectrum[j]!;
|
|
76
|
-
|
|
77
|
-
dissonance += getPlompLeveltDissonance(partial1, partial2, {
|
|
78
|
-
...SETHARES_DISSONANCE_PARAMS,
|
|
79
|
-
...params,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return dissonance;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function getSetharesDissonance(
|
|
88
|
-
spectrum1: SpectrumPartial[],
|
|
89
|
-
spectrum2: SpectrumPartial[],
|
|
90
|
-
params?: SetharesDissonanceParams
|
|
91
|
-
) {
|
|
92
|
-
let dissonance =
|
|
93
|
-
getIntrinsicDissonance(spectrum1, params) +
|
|
94
|
-
getIntrinsicDissonance(spectrum2, params);
|
|
95
|
-
|
|
96
|
-
for (let i = 0; i < spectrum1.length; i++) {
|
|
97
|
-
for (let j = 0; j < spectrum2.length; j++) {
|
|
98
|
-
const partial1 = spectrum1[i]!;
|
|
99
|
-
const partial2 = spectrum2[j]!;
|
|
100
|
-
|
|
101
|
-
dissonance += getPlompLeveltDissonance(partial1, partial2, {
|
|
102
|
-
...SETHARES_DISSONANCE_PARAMS,
|
|
103
|
-
...params,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return dissonance;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function ratioToCents(ratio: number): number {
|
|
112
|
-
return ratio > 0 ? 1200 * Math.log2(ratio) : 0;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function centsToRatio(cents: number): number {
|
|
116
|
-
return Math.pow(2, cents / 1200);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function transpose(partials: SpectrumPartial[], cents: number) {
|
|
120
|
-
const result: SpectrumPartial[] = [];
|
|
121
|
-
|
|
122
|
-
for (const partial of partials) {
|
|
123
|
-
result.push({
|
|
124
|
-
rate: partial.rate * centsToRatio(cents),
|
|
125
|
-
amplitude: partial.amplitude,
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return result;
|
|
130
|
-
}
|
package/tests/utils.test.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { expect, test, describe } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import * as Utils from "../lib/utils";
|
|
4
|
-
import { DissonanceCurve } from "../classes";
|
|
5
|
-
|
|
6
|
-
describe("lib/utils", () => {
|
|
7
|
-
test("Should calculate loudness", () => {
|
|
8
|
-
const amplitudes = [1, 0.001, 0.000025];
|
|
9
|
-
const expectedOutcome = [64, 1, 0];
|
|
10
|
-
|
|
11
|
-
expect(amplitudes.map(Utils.getLoudness)).toEqual(expectedOutcome);
|
|
12
|
-
});
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
describe("classes/DissoannceCurve", () => {
|
|
16
|
-
test("Should construct Dissoancne curve", () => {
|
|
17
|
-
const curve = new DissonanceCurve({context: [], complement: []})
|
|
18
|
-
|
|
19
|
-
expect(curve).toBeInstanceOf(DissonanceCurve);
|
|
20
|
-
});
|
|
21
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
// Environment setup & latest features
|
|
4
|
-
"lib": ["ESNext"],
|
|
5
|
-
"target": "ESNext",
|
|
6
|
-
"module": "Preserve",
|
|
7
|
-
"moduleDetection": "force",
|
|
8
|
-
"jsx": "react-jsx",
|
|
9
|
-
"allowJs": true,
|
|
10
|
-
|
|
11
|
-
// Bundler mode
|
|
12
|
-
"moduleResolution": "bundler",
|
|
13
|
-
"allowImportingTsExtensions": true,
|
|
14
|
-
"verbatimModuleSyntax": true,
|
|
15
|
-
"noEmit": true,
|
|
16
|
-
|
|
17
|
-
// Best practices
|
|
18
|
-
"strict": true,
|
|
19
|
-
"skipLibCheck": true,
|
|
20
|
-
"noFallthroughCasesInSwitch": true,
|
|
21
|
-
"noUncheckedIndexedAccess": true,
|
|
22
|
-
"noImplicitOverride": true,
|
|
23
|
-
|
|
24
|
-
// Some stricter flags (disabled by default)
|
|
25
|
-
"noUnusedLocals": false,
|
|
26
|
-
"noUnusedParameters": false,
|
|
27
|
-
"noPropertyAccessFromIndexSignature": false
|
|
28
|
-
}
|
|
29
|
-
}
|