tuning-core 1.0.1 → 1.0.2
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/Harmonic.d.ts +92 -0
- package/dist/classes/Harmonic.js +164 -0
- package/dist/classes/IntervalSet.d.ts +157 -0
- package/dist/classes/IntervalSet.js +304 -0
- package/dist/classes/Spectrum.d.ts +127 -0
- package/dist/classes/Spectrum.js +279 -0
- package/dist/classes/index.js +3 -0
- package/dist/index.js +2 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/types.d.ts +12 -0
- package/dist/lib/types.js +0 -0
- package/dist/lib/utils.d.ts +20 -0
- package/dist/lib/utils.js +38 -0
- package/package.json +5 -2
- package/benches/fraction-performance.bench.ts +0 -141
- package/benches/spectrum-serialization.bench.ts +0 -167
- package/classes/Harmonic.ts +0 -183
- package/classes/IntervalSet.ts +0 -361
- package/classes/README.md +0 -239
- package/classes/Spectrum.ts +0 -333
- package/lib/types.ts +0 -13
- package/lib/utils.ts +0 -47
- package/tests/Harmonic.test.ts +0 -516
- package/tests/IntervalSet.test.ts +0 -728
- package/tests/Spectrum.test.ts +0 -710
- package/tsconfig.json +0 -26
- /package/{classes/index.ts → dist/classes/index.d.ts} +0 -0
- /package/{index.ts → dist/index.d.ts} +0 -0
- /package/{lib/index.ts → dist/lib/index.d.ts} +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import Fraction, { type FractionInput } from 'fraction.js';
|
|
2
|
+
import { type HarmonicData } from '../lib';
|
|
3
|
+
/**
|
|
4
|
+
* Represents a single partial in an arbitrary spectrum.
|
|
5
|
+
* The name Harmonic does not imply a harmonic series but chosen over
|
|
6
|
+
* Partial to avoid coflict with the TypeScript's Partial type.
|
|
7
|
+
* Mutable for better real-time performance.
|
|
8
|
+
* Frequencies are stored as rational numbers for exact tuning mathematics.
|
|
9
|
+
*
|
|
10
|
+
* The constructor accepts either a HarmonicData object or individual parameters.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Using HarmonicData object
|
|
14
|
+
* const harmonic1 = new Harmonic({
|
|
15
|
+
* frequency: 440,
|
|
16
|
+
* amplitude: 0.5,
|
|
17
|
+
* phase: Math.PI
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Using individual parameters
|
|
22
|
+
* const harmonic2 = new Harmonic(440, 0.5, Math.PI);
|
|
23
|
+
* const harmonic3 = new Harmonic("440"); // amplitude defaults to 1, phase defaults to 0
|
|
24
|
+
* const harmonic4 = new Harmonic("440 1/3"); // frequency as a string with whole and fractional part
|
|
25
|
+
* const harmonic4 = new Harmonic([107, 100]); // frequency as a fraction of two integers 107/100
|
|
26
|
+
* // Note that frequency is treated as a rational number from Fraction.js lib
|
|
27
|
+
* // so it accepts FractionInput type
|
|
28
|
+
*/
|
|
29
|
+
export declare class Harmonic {
|
|
30
|
+
private _frequency;
|
|
31
|
+
private _amplitude;
|
|
32
|
+
private _phase;
|
|
33
|
+
/**
|
|
34
|
+
* Get frequency as a Fraction
|
|
35
|
+
*/
|
|
36
|
+
get frequency(): Fraction;
|
|
37
|
+
/**
|
|
38
|
+
* Get frequency as a float number
|
|
39
|
+
*/
|
|
40
|
+
get frequencyNum(): number;
|
|
41
|
+
/**
|
|
42
|
+
* Get frequency as a simplified fraction string (e.g., "3/2")
|
|
43
|
+
*/
|
|
44
|
+
get frequencyStr(): string;
|
|
45
|
+
/**
|
|
46
|
+
* Get amplitude (0-1 normalized)
|
|
47
|
+
*/
|
|
48
|
+
get amplitude(): number;
|
|
49
|
+
/**
|
|
50
|
+
* Get phase (0-2π radians)
|
|
51
|
+
*/
|
|
52
|
+
get phase(): number;
|
|
53
|
+
constructor(data: HarmonicData);
|
|
54
|
+
constructor(frequency: FractionInput, amplitude?: number, phase?: number);
|
|
55
|
+
private validate;
|
|
56
|
+
/**
|
|
57
|
+
* Sets frequency to the provided value
|
|
58
|
+
*/
|
|
59
|
+
setFrequency(frequency: FractionInput): this;
|
|
60
|
+
/**
|
|
61
|
+
* Sets amplitude (0-1)
|
|
62
|
+
*/
|
|
63
|
+
setAmplitude(amplitude: number): this;
|
|
64
|
+
/**
|
|
65
|
+
* Sets phase (0-2π)
|
|
66
|
+
*/
|
|
67
|
+
setPhase(phase: number): this;
|
|
68
|
+
/**
|
|
69
|
+
* Multiply frequency by a ratio (for transposition)
|
|
70
|
+
*/
|
|
71
|
+
transpose(ratio: FractionInput): this;
|
|
72
|
+
/**
|
|
73
|
+
* Multiply frequency by a ratio (for transposition)
|
|
74
|
+
*/
|
|
75
|
+
toTransposed(ratio: FractionInput): Harmonic;
|
|
76
|
+
/**
|
|
77
|
+
* Scale amplitude by a factor
|
|
78
|
+
*/
|
|
79
|
+
scale(factor: number): this;
|
|
80
|
+
/**
|
|
81
|
+
* Create a copy of this harmonic
|
|
82
|
+
*/
|
|
83
|
+
clone(): Harmonic;
|
|
84
|
+
/**
|
|
85
|
+
* Compare frequencies (for sorting)
|
|
86
|
+
*/
|
|
87
|
+
compareFrequency(other: Harmonic): number;
|
|
88
|
+
/**
|
|
89
|
+
* Serializes the harmonic to a HarmonicData object
|
|
90
|
+
*/
|
|
91
|
+
toJSON(): HarmonicData;
|
|
92
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import Fraction, {} from 'fraction.js';
|
|
2
|
+
import { isHarmonicData } from '../lib';
|
|
3
|
+
/**
|
|
4
|
+
* Represents a single partial in an arbitrary spectrum.
|
|
5
|
+
* The name Harmonic does not imply a harmonic series but chosen over
|
|
6
|
+
* Partial to avoid coflict with the TypeScript's Partial type.
|
|
7
|
+
* Mutable for better real-time performance.
|
|
8
|
+
* Frequencies are stored as rational numbers for exact tuning mathematics.
|
|
9
|
+
*
|
|
10
|
+
* The constructor accepts either a HarmonicData object or individual parameters.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Using HarmonicData object
|
|
14
|
+
* const harmonic1 = new Harmonic({
|
|
15
|
+
* frequency: 440,
|
|
16
|
+
* amplitude: 0.5,
|
|
17
|
+
* phase: Math.PI
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Using individual parameters
|
|
22
|
+
* const harmonic2 = new Harmonic(440, 0.5, Math.PI);
|
|
23
|
+
* const harmonic3 = new Harmonic("440"); // amplitude defaults to 1, phase defaults to 0
|
|
24
|
+
* const harmonic4 = new Harmonic("440 1/3"); // frequency as a string with whole and fractional part
|
|
25
|
+
* const harmonic4 = new Harmonic([107, 100]); // frequency as a fraction of two integers 107/100
|
|
26
|
+
* // Note that frequency is treated as a rational number from Fraction.js lib
|
|
27
|
+
* // so it accepts FractionInput type
|
|
28
|
+
*/
|
|
29
|
+
export class Harmonic {
|
|
30
|
+
_frequency; // Frequency as ratio (e.g., 3/2 for perfect fifth)
|
|
31
|
+
_amplitude; // 0-1 normalized
|
|
32
|
+
_phase; // 0-2π radians
|
|
33
|
+
/**
|
|
34
|
+
* Get frequency as a Fraction
|
|
35
|
+
*/
|
|
36
|
+
get frequency() {
|
|
37
|
+
return this._frequency;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get frequency as a float number
|
|
41
|
+
*/
|
|
42
|
+
get frequencyNum() {
|
|
43
|
+
return this._frequency.valueOf();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get frequency as a simplified fraction string (e.g., "3/2")
|
|
47
|
+
*/
|
|
48
|
+
get frequencyStr() {
|
|
49
|
+
return this._frequency.toFraction();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get amplitude (0-1 normalized)
|
|
53
|
+
*/
|
|
54
|
+
get amplitude() {
|
|
55
|
+
return this._amplitude;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get phase (0-2π radians)
|
|
59
|
+
*/
|
|
60
|
+
get phase() {
|
|
61
|
+
return this._phase;
|
|
62
|
+
}
|
|
63
|
+
constructor(frequencyOrData, amplitude = 1, phase = 0) {
|
|
64
|
+
if (isHarmonicData(frequencyOrData)) {
|
|
65
|
+
// HarmonicData case
|
|
66
|
+
this._frequency = new Fraction(frequencyOrData.frequency);
|
|
67
|
+
this._amplitude = frequencyOrData.amplitude;
|
|
68
|
+
this._phase = frequencyOrData.phase;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Original case: frequency, amplitude, phase
|
|
72
|
+
this._frequency = new Fraction(frequencyOrData);
|
|
73
|
+
this._amplitude = amplitude ?? 1;
|
|
74
|
+
this._phase = phase ?? 0;
|
|
75
|
+
}
|
|
76
|
+
this.validate();
|
|
77
|
+
}
|
|
78
|
+
validate() {
|
|
79
|
+
if (this._frequency.compare(0) <= 0) {
|
|
80
|
+
throw new Error('Frequency must be positive');
|
|
81
|
+
}
|
|
82
|
+
if (this._amplitude < 0 || this._amplitude > 1) {
|
|
83
|
+
throw new Error('Amplitude must be between 0 and 1');
|
|
84
|
+
}
|
|
85
|
+
if (this._phase < 0 || this._phase >= 2 * Math.PI) {
|
|
86
|
+
throw new Error('Phase must be between 0 and 2π');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Sets frequency to the provided value
|
|
91
|
+
*/
|
|
92
|
+
setFrequency(frequency) {
|
|
93
|
+
this._frequency = new Fraction(frequency);
|
|
94
|
+
if (this._frequency.compare(0) <= 0) {
|
|
95
|
+
throw new Error('Frequency must be positive');
|
|
96
|
+
}
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Sets amplitude (0-1)
|
|
101
|
+
*/
|
|
102
|
+
setAmplitude(amplitude) {
|
|
103
|
+
if (amplitude < 0 || amplitude > 1) {
|
|
104
|
+
throw new Error('Amplitude must be between 0 and 1');
|
|
105
|
+
}
|
|
106
|
+
this._amplitude = amplitude;
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Sets phase (0-2π)
|
|
111
|
+
*/
|
|
112
|
+
setPhase(phase) {
|
|
113
|
+
if (phase < 0 || phase >= 2 * Math.PI) {
|
|
114
|
+
throw new Error('Phase must be between 0 and 2π');
|
|
115
|
+
}
|
|
116
|
+
this._phase = phase;
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Multiply frequency by a ratio (for transposition)
|
|
121
|
+
*/
|
|
122
|
+
transpose(ratio) {
|
|
123
|
+
this._frequency = this._frequency.mul(ratio);
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Multiply frequency by a ratio (for transposition)
|
|
128
|
+
*/
|
|
129
|
+
toTransposed(ratio) {
|
|
130
|
+
return this.clone().transpose(ratio);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Scale amplitude by a factor
|
|
134
|
+
*/
|
|
135
|
+
scale(factor) {
|
|
136
|
+
this._amplitude = Math.max(0, Math.min(1, this._amplitude * factor));
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Create a copy of this harmonic
|
|
141
|
+
*/
|
|
142
|
+
clone() {
|
|
143
|
+
return new Harmonic(this._frequency, this._amplitude, this._phase);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Compare frequencies (for sorting)
|
|
147
|
+
*/
|
|
148
|
+
compareFrequency(other) {
|
|
149
|
+
return this._frequency.compare(other._frequency);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Serializes the harmonic to a HarmonicData object
|
|
153
|
+
*/
|
|
154
|
+
toJSON() {
|
|
155
|
+
return {
|
|
156
|
+
frequency: {
|
|
157
|
+
n: Number(this._frequency.n),
|
|
158
|
+
d: Number(this._frequency.d),
|
|
159
|
+
},
|
|
160
|
+
amplitude: this._amplitude,
|
|
161
|
+
phase: this._phase,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import Fraction, { type FractionInput } from 'fraction.js';
|
|
2
|
+
import { Spectrum } from './Spectrum';
|
|
3
|
+
/**
|
|
4
|
+
* Represents a set of frequency ratios (intervals).
|
|
5
|
+
* Uses Map to ensure no duplicate ratios.
|
|
6
|
+
* Useful for building tuning systems, scales, and analyzing interval collections.
|
|
7
|
+
*/
|
|
8
|
+
export declare class IntervalSet {
|
|
9
|
+
private ratios;
|
|
10
|
+
constructor(ratios?: FractionInput[]);
|
|
11
|
+
/**
|
|
12
|
+
* Add a ratio to the set.
|
|
13
|
+
* If ratio already exists, it replaces the existing one.
|
|
14
|
+
*/
|
|
15
|
+
add(ratio: FractionInput): this;
|
|
16
|
+
/**
|
|
17
|
+
* Check if the set contains this ratio
|
|
18
|
+
*/
|
|
19
|
+
has(ratio: FractionInput): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Remove a ratio from the set
|
|
22
|
+
* @returns true if ratio was removed, false if it didn't exist
|
|
23
|
+
*/
|
|
24
|
+
delete(ratio: FractionInput): this;
|
|
25
|
+
/**
|
|
26
|
+
* Get a ratio from the set
|
|
27
|
+
* @returns Fraction object or undefined if not found
|
|
28
|
+
*/
|
|
29
|
+
get(ratio: FractionInput): Fraction | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Number of ratios in the set
|
|
32
|
+
*/
|
|
33
|
+
get size(): number;
|
|
34
|
+
/**
|
|
35
|
+
* Check if the set is empty
|
|
36
|
+
*/
|
|
37
|
+
isEmpty(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Get all ratios as an array
|
|
40
|
+
*/
|
|
41
|
+
getRatios(): Fraction[];
|
|
42
|
+
/**
|
|
43
|
+
* Get the smallest ratio in the set
|
|
44
|
+
*/
|
|
45
|
+
min(): Fraction | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Get the largest ratio in the set
|
|
48
|
+
*/
|
|
49
|
+
max(): Fraction | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Clear all ratios from the set
|
|
52
|
+
*/
|
|
53
|
+
clear(): this;
|
|
54
|
+
/**
|
|
55
|
+
* Create a copy of this interval set
|
|
56
|
+
*/
|
|
57
|
+
clone(): IntervalSet;
|
|
58
|
+
/**
|
|
59
|
+
* Iterate over all ratios
|
|
60
|
+
*/
|
|
61
|
+
forEach(callback: (ratio: Fraction, key: string) => void): void;
|
|
62
|
+
/**
|
|
63
|
+
* Convert to Spectrum with equal amplitudes
|
|
64
|
+
* @param fundamentalHz - Optional fundamental frequency in Hz
|
|
65
|
+
* @param amplitude - Amplitude for all harmonics (default 1)
|
|
66
|
+
*/
|
|
67
|
+
toSpectrum(amp: (ratio: Fraction, index: number) => number): Spectrum;
|
|
68
|
+
/**
|
|
69
|
+
* Get ratios as array of strings
|
|
70
|
+
*/
|
|
71
|
+
toStrings(): string[];
|
|
72
|
+
/**
|
|
73
|
+
* Get ratios as array of decimal numbers
|
|
74
|
+
*/
|
|
75
|
+
toNumbers(): number[];
|
|
76
|
+
/**
|
|
77
|
+
* Check if this set equals another set
|
|
78
|
+
*/
|
|
79
|
+
equals(other: IntervalSet): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Calculate GCD of all ratios
|
|
82
|
+
*/
|
|
83
|
+
private getGCD;
|
|
84
|
+
/**
|
|
85
|
+
* Convert an array of ratios to an array of intervals (differences between consecutive ratios).
|
|
86
|
+
* The first interval is always 1, and each subsequent interval is the current ratio divided by the previous ratio.
|
|
87
|
+
*
|
|
88
|
+
* @param ratios - Array of Fraction objects representing ratios
|
|
89
|
+
* @returns Array of Fraction objects representing intervals, starting with 1
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* // If ratios are [1, 5/4, 3/2, 2], intervals will be [1, 5/4, 6/5, 4/3]
|
|
93
|
+
* const ratios = [new Fraction(1), new Fraction(5, 4), new Fraction(3, 2), new Fraction(2)];
|
|
94
|
+
* const intervals = getSweepIntervals(ratios);
|
|
95
|
+
*/
|
|
96
|
+
toSweepIntervals(): Fraction[];
|
|
97
|
+
minMax(min: FractionInput, max: FractionInput): this;
|
|
98
|
+
/**
|
|
99
|
+
* Generate all unique ratios between min and max with denominators up to maxDenominator.
|
|
100
|
+
*
|
|
101
|
+
* This creates all possible fractions n/d where:
|
|
102
|
+
* - min <= n/d <= max
|
|
103
|
+
* - 1 <= d <= maxDenominator
|
|
104
|
+
* - 1 <= n
|
|
105
|
+
* - n and d are coprime (simplified fractions only)
|
|
106
|
+
*
|
|
107
|
+
* The Map in IntervalSet automatically handles deduplication of equivalent fractions.
|
|
108
|
+
*
|
|
109
|
+
* @param min - Minimum ratio (inclusive)
|
|
110
|
+
* @param max - Maximum ratio (inclusive)
|
|
111
|
+
* @param maxDenominator - Maximum denominator for generated fractions
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* // All simple ratios in one octave
|
|
115
|
+
* IntervalSetFactory.range(1, 2, 5)
|
|
116
|
+
* // Returns: 1/1, 6/5, 5/4, 4/3, 3/2, 8/5, 5/3, 2/1
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* // Fine divisions for analysis (100 steps per octave)
|
|
120
|
+
* IntervalSetFactory.range(1, 2, 100)
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* // Ratios across 1.5 octaves
|
|
124
|
+
* IntervalSetFactory.range(1, 3, 12)
|
|
125
|
+
*/
|
|
126
|
+
static range(min: FractionInput, max: FractionInput, maxDenominator: number): IntervalSet;
|
|
127
|
+
/**
|
|
128
|
+
* Generate affinitive tuning intervals that contain all ratios between harmonics of two spectra.
|
|
129
|
+
* For each pair of harmonics (one from each spectrum), calculates the ratio between them.
|
|
130
|
+
* Useful for analyzing harmonic relationships and consonance between two sounds.
|
|
131
|
+
*
|
|
132
|
+
* @param spectrum1 - First spectrum
|
|
133
|
+
* @param spectrum2 - Second spectrum
|
|
134
|
+
* @returns IntervalSet containing all unique ratios between the spectra's harmonics
|
|
135
|
+
*/
|
|
136
|
+
static affinitive(context: Spectrum, complement: Spectrum): IntervalSet;
|
|
137
|
+
/**
|
|
138
|
+
* Add intermediate intervals between existing ratios to create a smooth curve.
|
|
139
|
+
* Uses logarithmic spacing to ensure perceptually uniform distribution.
|
|
140
|
+
* A gap of 1→2 gets the same density as 2→4 (both are octaves).
|
|
141
|
+
*
|
|
142
|
+
* @param maxGapCents - Maximum allowed gap in cents (e.g., 10 for 10-cent steps)
|
|
143
|
+
* 1200 cents = 1 octave, 100 cents ≈ 1 semitone
|
|
144
|
+
* @returns this (for chaining)
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* const intervals = new IntervalSet([1, 2, 4]);
|
|
148
|
+
* intervals.densify(100); // Max 100-cent (1 semitone) gaps
|
|
149
|
+
* // Adds same number of points between 1→2 as between 2→4
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* // For dissonance curves - logarithmic spacing
|
|
153
|
+
* const extrema = IntervalSet.affinitive(spectrum1, spectrum2);
|
|
154
|
+
* extrema.densify(10); // ~10-cent resolution for smooth curve
|
|
155
|
+
*/
|
|
156
|
+
densify(maxGapCents: number): this;
|
|
157
|
+
}
|