sethares-dissonance 2.0.1 → 2.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/README.md +105 -6
- package/dist/classes/DissonanceCurve.d.ts +15 -6
- package/dist/classes/DissonanceCurve.js +27 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,15 +1,114 @@
|
|
|
1
1
|
# sethares-dissonance
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A JavaScript/TypeScript library for analyzing **dissonance curves** using William Sethares' sensory dissonance model. Dissonance curves show how sensory dissonance varies across a range of musical intervals, based on the spectral content of sounds.
|
|
4
|
+
|
|
5
|
+
For the theoretical foundation and detailed explanation of the model, see the book **[_Tuning, Timbre, Spectrum, Scale_](https://www.williamsethares.com/ttss.html)** by William Sethares.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
4
8
|
|
|
5
9
|
```bash
|
|
6
|
-
bun
|
|
10
|
+
bun add sethares-dissonance
|
|
11
|
+
# or
|
|
12
|
+
npm install sethares-dissonance
|
|
7
13
|
```
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
## Usage
|
|
10
16
|
|
|
11
|
-
```
|
|
12
|
-
|
|
17
|
+
```ts
|
|
18
|
+
import { DissonanceCurve, Spectrum } from "sethares-dissonance";
|
|
19
|
+
|
|
20
|
+
const context = Spectrum.harmonicSeries(10, 440);
|
|
21
|
+
const complement = Spectrum.harmonicSeries(10, 440);
|
|
22
|
+
|
|
23
|
+
const curve = new DissonanceCurve({
|
|
24
|
+
context,
|
|
25
|
+
complement,
|
|
26
|
+
start: 1,
|
|
27
|
+
end: 2,
|
|
28
|
+
maxDenominator: 60,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const points = curve.points;
|
|
32
|
+
const plotData = curve.plot();
|
|
13
33
|
```
|
|
14
34
|
|
|
15
|
-
|
|
35
|
+
## React
|
|
36
|
+
|
|
37
|
+
Because DissonanceCurve class mutates data in place, it is not directly suited for React applications as reference to the instance does not change. React will not detect dissonance curve mutations in dependency arrays or state and thus will not rerender. You can solve it in two ways.
|
|
38
|
+
|
|
39
|
+
### 1. Create new instances
|
|
40
|
+
|
|
41
|
+
Create a fresh `DissonanceCurve` whenever options change. `useMemo` ensures a new instance is created when dependencies change, so React detects the update and re-renders.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { useMemo } from "react";
|
|
45
|
+
import { DissonanceCurve, Spectrum } from "sethares-dissonance";
|
|
46
|
+
|
|
47
|
+
function DissonanceChart({ context, complement, start = 1, end = 2 }) {
|
|
48
|
+
const curve = useMemo(
|
|
49
|
+
() =>
|
|
50
|
+
new DissonanceCurve({
|
|
51
|
+
context,
|
|
52
|
+
complement,
|
|
53
|
+
start,
|
|
54
|
+
end,
|
|
55
|
+
maxDenominator: 60,
|
|
56
|
+
}),
|
|
57
|
+
[context, complement, start, end],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const plotData = curve.plot();
|
|
61
|
+
return <svg>{/* render plotData */}</svg>;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This solution ok, but there is a downside that `curve` object contains methods that can mutate the internal state which will not be registered by React, thus leading to bugs.
|
|
66
|
+
|
|
67
|
+
### 2. Use read-only wrappers (Recommended)
|
|
68
|
+
|
|
69
|
+
This method relies on creating new instances of read-only objects rather than new DissonanceCurve instances. It is demonstrated in the following `useDissonanceCurve` hook. Memoize options with `useMemo` when they contain objects (e.g. `Spectrum`) to avoid recalculating on every render.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
"use client";
|
|
73
|
+
|
|
74
|
+
import { useMemo, useState } from "react";
|
|
75
|
+
import {
|
|
76
|
+
DissonanceCurve,
|
|
77
|
+
type DissonanceCurveOptions,
|
|
78
|
+
} from "sethares-dissonance";
|
|
79
|
+
import type { ReadOnlyDissonanceCurve } from "sethares-dissonance";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read-only view of DissonanceCurve without the recalculate method.
|
|
83
|
+
* Use this type when exposing the curve from hooks to prevent external mutation.
|
|
84
|
+
*/
|
|
85
|
+
export type ReadOnlyDissonanceCurve = Partial<Omit<
|
|
86
|
+
DissonanceCurve,
|
|
87
|
+
"recalculate"
|
|
88
|
+
>>;
|
|
89
|
+
|
|
90
|
+
function createReadOnlyWrapper(
|
|
91
|
+
curve: DissonanceCurve,
|
|
92
|
+
): ReadOnlyDissonanceCurve {
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
get maxDissonance() {
|
|
96
|
+
return curve.maxDissonance;
|
|
97
|
+
},
|
|
98
|
+
plotCents: () => curve.plotCents(),
|
|
99
|
+
findNearestPoint: (ratio) => curve.findNearestPoint(ratio),
|
|
100
|
+
// .. can add more methods if needed, aboid those that cause mutations
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function useDissonanceCurve(
|
|
105
|
+
options: DissonanceCurveOptions,
|
|
106
|
+
): ReadOnlyDissonanceCurve {
|
|
107
|
+
const [curve] = useState(() => new DissonanceCurve(options));
|
|
108
|
+
|
|
109
|
+
return useMemo(() => {
|
|
110
|
+
curve.recalculate(options);
|
|
111
|
+
return createReadOnlyWrapper(curve);
|
|
112
|
+
}, [options, curve]);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fraction, type FractionInput } from "fraction.js";
|
|
2
|
-
import { type DissonanceParams, type DissonanceCurvePoint, type DissonanceCurveData } from "../lib";
|
|
3
2
|
import { Spectrum } from "tuning-core";
|
|
3
|
+
import type { DissonanceParams, DissonanceCurvePoint, DissonanceCurveData } from "../lib";
|
|
4
4
|
export type DissonanceCurveOptions = DissonanceParams & {
|
|
5
5
|
context: Spectrum;
|
|
6
6
|
complement: Spectrum;
|
|
@@ -41,12 +41,21 @@ export type DissonanceCurveOptions = DissonanceParams & {
|
|
|
41
41
|
*/
|
|
42
42
|
export declare class DissonanceCurve {
|
|
43
43
|
private _data;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
start: Fraction;
|
|
45
|
+
end: Fraction;
|
|
46
|
+
context: Spectrum;
|
|
47
|
+
complement: Spectrum;
|
|
48
|
+
maxDissonance: number;
|
|
49
49
|
constructor(opts: DissonanceCurveOptions);
|
|
50
|
+
/**
|
|
51
|
+
* Recalculate the dissonance curve with new options.
|
|
52
|
+
* Updates the curve in place.
|
|
53
|
+
*/
|
|
54
|
+
recalculate(opts: DissonanceCurveOptions): void;
|
|
55
|
+
/**
|
|
56
|
+
* Build _data from public props (context, complement, start, end).
|
|
57
|
+
*/
|
|
58
|
+
private build;
|
|
50
59
|
/**
|
|
51
60
|
* Get all dissonance curve points sorted by interval ratio.
|
|
52
61
|
* @returns Array of DissonanceCurvePoint objects sorted in ascending order by interval
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fraction } from "fraction.js";
|
|
2
|
-
import { getSetharesDissonance, } from "../lib";
|
|
3
2
|
import { ratioToCents, Spectrum, IntervalSet } from "tuning-core";
|
|
3
|
+
import { getSetharesDissonance } from "../lib";
|
|
4
4
|
const DEFAULT_COLUMN_DELIMITER = ",";
|
|
5
5
|
const DEFAULT_ROW_DELIMITER = "\n";
|
|
6
6
|
/**
|
|
@@ -49,15 +49,38 @@ export class DissonanceCurve {
|
|
|
49
49
|
this.end = new Fraction(end ?? 2);
|
|
50
50
|
if (this.start.compare(this.end) > 0)
|
|
51
51
|
throw Error("startCents should be less or equal to endCents");
|
|
52
|
-
|
|
52
|
+
this.build(dissonanceParams);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Recalculate the dissonance curve with new options.
|
|
56
|
+
* Updates the curve in place.
|
|
57
|
+
*/
|
|
58
|
+
recalculate(opts) {
|
|
59
|
+
const { context, complement, start, end, maxDenominator, ...dissonanceParams } = opts;
|
|
60
|
+
this.context = context;
|
|
61
|
+
this.complement = complement;
|
|
62
|
+
this.start = new Fraction(start ?? 1);
|
|
63
|
+
this.end = new Fraction(end ?? 2);
|
|
64
|
+
if (this.start.compare(this.end) > 0)
|
|
65
|
+
throw Error("startCents should be less or equal to endCents");
|
|
66
|
+
this.build(dissonanceParams);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Build _data from public props (context, complement, start, end).
|
|
70
|
+
*/
|
|
71
|
+
build(dissonanceParams) {
|
|
72
|
+
this._data.clear();
|
|
73
|
+
let maxDissonance = 0;
|
|
74
|
+
const intervals = IntervalSet.affinitive(this.context.size > 1 ? this.context : Spectrum.harmonic(2, 1), this.complement.size > 1 ? this.complement : Spectrum.harmonic(2, 1)).densify(20);
|
|
53
75
|
const ratios = intervals.getRatios();
|
|
54
76
|
for (let i = 0; i < ratios.length; i++) {
|
|
55
77
|
const interval = ratios[i];
|
|
56
78
|
const dissonance = getSetharesDissonance(this.context, this.complement.toTransposed(interval), dissonanceParams);
|
|
57
|
-
if (dissonance >
|
|
58
|
-
|
|
79
|
+
if (dissonance > maxDissonance)
|
|
80
|
+
maxDissonance = dissonance;
|
|
59
81
|
this._data.set(interval.toFraction(), { interval, dissonance });
|
|
60
82
|
}
|
|
83
|
+
this.maxDissonance = maxDissonance;
|
|
61
84
|
}
|
|
62
85
|
/**
|
|
63
86
|
* Get all dissonance curve points sorted by interval ratio.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sethares-dissonance",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"devDependencies": {
|
|
6
6
|
"@types/bun": "latest"
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "npx tsc",
|
|
19
|
-
"
|
|
19
|
+
"release": "./release.sh"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"fraction.js": "^5.3.4",
|
|
23
|
-
"tuning-core": "^1.0
|
|
23
|
+
"tuning-core": "^1.1.0"
|
|
24
24
|
}
|
|
25
25
|
}
|