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.
@@ -0,0 +1,304 @@
1
+ import Fraction, {} 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 class IntervalSet {
9
+ ratios;
10
+ constructor(ratios) {
11
+ this.ratios = new Map();
12
+ if (!ratios)
13
+ return;
14
+ for (const r of ratios) {
15
+ this.add(r);
16
+ }
17
+ }
18
+ /**
19
+ * Add a ratio to the set.
20
+ * If ratio already exists, it replaces the existing one.
21
+ */
22
+ add(ratio) {
23
+ const f = new Fraction(ratio);
24
+ this.ratios.set(f.toFraction(), f);
25
+ return this;
26
+ }
27
+ /**
28
+ * Check if the set contains this ratio
29
+ */
30
+ has(ratio) {
31
+ const key = new Fraction(ratio).toFraction();
32
+ return this.ratios.has(key);
33
+ }
34
+ /**
35
+ * Remove a ratio from the set
36
+ * @returns true if ratio was removed, false if it didn't exist
37
+ */
38
+ delete(ratio) {
39
+ const key = new Fraction(ratio).toFraction();
40
+ this.ratios.delete(key);
41
+ return this;
42
+ }
43
+ /**
44
+ * Get a ratio from the set
45
+ * @returns Fraction object or undefined if not found
46
+ */
47
+ get(ratio) {
48
+ const key = new Fraction(ratio).toFraction();
49
+ return this.ratios.get(key);
50
+ }
51
+ /**
52
+ * Number of ratios in the set
53
+ */
54
+ get size() {
55
+ return this.ratios.size;
56
+ }
57
+ /**
58
+ * Check if the set is empty
59
+ */
60
+ isEmpty() {
61
+ return this.ratios.size === 0;
62
+ }
63
+ /**
64
+ * Get all ratios as an array
65
+ */
66
+ getRatios() {
67
+ return Array.from(this.ratios.values()).sort((a, b) => a.compare(b));
68
+ }
69
+ /**
70
+ * Get the smallest ratio in the set
71
+ */
72
+ min() {
73
+ return this.getRatios().at(0);
74
+ }
75
+ /**
76
+ * Get the largest ratio in the set
77
+ */
78
+ max() {
79
+ return this.getRatios().at(-1);
80
+ }
81
+ /**
82
+ * Clear all ratios from the set
83
+ */
84
+ clear() {
85
+ this.ratios.clear();
86
+ return this;
87
+ }
88
+ /**
89
+ * Create a copy of this interval set
90
+ */
91
+ clone() {
92
+ return new IntervalSet(Array.from(this.ratios.values()));
93
+ }
94
+ /**
95
+ * Iterate over all ratios
96
+ */
97
+ forEach(callback) {
98
+ this.ratios.forEach(callback);
99
+ }
100
+ /**
101
+ * Convert to Spectrum with equal amplitudes
102
+ * @param fundamentalHz - Optional fundamental frequency in Hz
103
+ * @param amplitude - Amplitude for all harmonics (default 1)
104
+ */
105
+ toSpectrum(amp) {
106
+ const spectrum = new Spectrum();
107
+ this.getRatios().forEach((ratio, index) => {
108
+ spectrum.add(ratio, amp(ratio, index));
109
+ });
110
+ return spectrum;
111
+ }
112
+ /**
113
+ * Get ratios as array of strings
114
+ */
115
+ toStrings() {
116
+ return this.getRatios().map(r => r.toFraction());
117
+ }
118
+ /**
119
+ * Get ratios as array of decimal numbers
120
+ */
121
+ toNumbers() {
122
+ return this.getRatios().map(r => r.valueOf());
123
+ }
124
+ /**
125
+ * Check if this set equals another set
126
+ */
127
+ equals(other) {
128
+ if (this.size !== other.size)
129
+ return false;
130
+ for (const ratio of this.getRatios()) {
131
+ if (!other.has(ratio))
132
+ return false;
133
+ }
134
+ return true;
135
+ }
136
+ /**
137
+ * Calculate GCD of all ratios
138
+ */
139
+ getGCD() {
140
+ const ratios = this.getRatios();
141
+ if (ratios.length === 0)
142
+ return undefined;
143
+ let gcd = ratios[0];
144
+ for (let i = 1; i < ratios.length; i++) {
145
+ gcd = gcd.gcd(ratios[i]);
146
+ }
147
+ return gcd;
148
+ }
149
+ /**
150
+ * Convert an array of ratios to an array of intervals (differences between consecutive ratios).
151
+ * The first interval is always 1, and each subsequent interval is the current ratio divided by the previous ratio.
152
+ *
153
+ * @param ratios - Array of Fraction objects representing ratios
154
+ * @returns Array of Fraction objects representing intervals, starting with 1
155
+ *
156
+ * @example
157
+ * // If ratios are [1, 5/4, 3/2, 2], intervals will be [1, 5/4, 6/5, 4/3]
158
+ * const ratios = [new Fraction(1), new Fraction(5, 4), new Fraction(3, 2), new Fraction(2)];
159
+ * const intervals = getSweepIntervals(ratios);
160
+ */
161
+ toSweepIntervals() {
162
+ const ratios = this.getRatios();
163
+ const intervals = [];
164
+ if (ratios.length === 0)
165
+ return intervals;
166
+ intervals.push(new Fraction(1));
167
+ for (let i = 1; i < ratios.length; i++) {
168
+ intervals.push(ratios[i].div(ratios[i - 1]));
169
+ }
170
+ return intervals;
171
+ }
172
+ minMax(min, max) {
173
+ const minFraction = new Fraction(min);
174
+ const maxFraction = new Fraction(max);
175
+ if (minFraction.compare(maxFraction) > 0) {
176
+ throw new Error('min must be less than or equal to max');
177
+ }
178
+ for (const ratio of this.ratios.values()) {
179
+ if (ratio.compare(minFraction) < 0 || ratio.compare(maxFraction) > 0) {
180
+ this.ratios.delete(ratio.toFraction());
181
+ }
182
+ }
183
+ return this;
184
+ }
185
+ /**
186
+ * Generate all unique ratios between min and max with denominators up to maxDenominator.
187
+ *
188
+ * This creates all possible fractions n/d where:
189
+ * - min <= n/d <= max
190
+ * - 1 <= d <= maxDenominator
191
+ * - 1 <= n
192
+ * - n and d are coprime (simplified fractions only)
193
+ *
194
+ * The Map in IntervalSet automatically handles deduplication of equivalent fractions.
195
+ *
196
+ * @param min - Minimum ratio (inclusive)
197
+ * @param max - Maximum ratio (inclusive)
198
+ * @param maxDenominator - Maximum denominator for generated fractions
199
+ *
200
+ * @example
201
+ * // All simple ratios in one octave
202
+ * IntervalSetFactory.range(1, 2, 5)
203
+ * // Returns: 1/1, 6/5, 5/4, 4/3, 3/2, 8/5, 5/3, 2/1
204
+ *
205
+ * @example
206
+ * // Fine divisions for analysis (100 steps per octave)
207
+ * IntervalSetFactory.range(1, 2, 100)
208
+ *
209
+ * @example
210
+ * // Ratios across 1.5 octaves
211
+ * IntervalSetFactory.range(1, 3, 12)
212
+ */
213
+ static range(min, max, maxDenominator) {
214
+ if (maxDenominator < 1) {
215
+ throw new Error('maxDenominator must be at least 1');
216
+ }
217
+ const minFraction = new Fraction(min);
218
+ const maxFraction = new Fraction(max);
219
+ if (minFraction.compare(maxFraction) > 0) {
220
+ throw new Error('min must be less than or equal to max');
221
+ }
222
+ const intervalSet = new IntervalSet();
223
+ for (let denominator = 1; denominator <= maxDenominator; denominator++) {
224
+ const minNumerator = Math.max(1, Math.ceil(minFraction.valueOf() * denominator));
225
+ const maxNumerator = Math.floor(maxFraction.valueOf() * denominator);
226
+ for (let numerator = minNumerator; numerator <= maxNumerator; numerator++) {
227
+ const fraction = new Fraction(numerator, denominator);
228
+ if (fraction.compare(minFraction) >= 0 && fraction.compare(maxFraction) <= 0) {
229
+ intervalSet.add(fraction);
230
+ }
231
+ }
232
+ }
233
+ return intervalSet;
234
+ }
235
+ /**
236
+ * Generate affinitive tuning intervals that contain all ratios between harmonics of two spectra.
237
+ * For each pair of harmonics (one from each spectrum), calculates the ratio between them.
238
+ * Useful for analyzing harmonic relationships and consonance between two sounds.
239
+ *
240
+ * @param spectrum1 - First spectrum
241
+ * @param spectrum2 - Second spectrum
242
+ * @returns IntervalSet containing all unique ratios between the spectra's harmonics
243
+ */
244
+ static affinitive(context, complement) {
245
+ const intervalSet = new IntervalSet();
246
+ const harmonics1 = context.getHarmonics();
247
+ const harmonics2 = complement.getHarmonics();
248
+ // Calculate ratio between every pair of harmonics
249
+ for (const h1 of harmonics1) {
250
+ for (const h2 of harmonics2) {
251
+ const ratio = h2.frequency.div(h1.frequency);
252
+ intervalSet.add(ratio);
253
+ }
254
+ }
255
+ return intervalSet;
256
+ }
257
+ /**
258
+ * Add intermediate intervals between existing ratios to create a smooth curve.
259
+ * Uses logarithmic spacing to ensure perceptually uniform distribution.
260
+ * A gap of 1→2 gets the same density as 2→4 (both are octaves).
261
+ *
262
+ * @param maxGapCents - Maximum allowed gap in cents (e.g., 10 for 10-cent steps)
263
+ * 1200 cents = 1 octave, 100 cents ≈ 1 semitone
264
+ * @returns this (for chaining)
265
+ *
266
+ * @example
267
+ * const intervals = new IntervalSet([1, 2, 4]);
268
+ * intervals.densify(100); // Max 100-cent (1 semitone) gaps
269
+ * // Adds same number of points between 1→2 as between 2→4
270
+ *
271
+ * @example
272
+ * // For dissonance curves - logarithmic spacing
273
+ * const extrema = IntervalSet.affinitive(spectrum1, spectrum2);
274
+ * extrema.densify(10); // ~10-cent resolution for smooth curve
275
+ */
276
+ densify(maxGapCents) {
277
+ if (maxGapCents <= 0) {
278
+ throw new Error('maxGapCents must be positive');
279
+ }
280
+ // Convert cents to ratio (1 cent = 2^(1/1200))
281
+ const maxGapRatio = Math.pow(2, maxGapCents / 1200);
282
+ // Keep adding intervals until all gaps are small enough
283
+ let needsMorePoints = true;
284
+ while (needsMorePoints) {
285
+ const sorted = this.getRatios();
286
+ needsMorePoints = false;
287
+ // Check each adjacent pair
288
+ for (let i = 0; i < sorted.length - 1; i++) {
289
+ const left = sorted[i];
290
+ const right = sorted[i + 1];
291
+ // Calculate logarithmic gap (ratio of ratios)
292
+ const gapRatio = right.div(left).valueOf();
293
+ // If gap is too large, add the geometric mean
294
+ if (gapRatio > maxGapRatio) {
295
+ // Geometric mean: sqrt(a * b) - the logarithmic midpoint
296
+ const geometricMean = Math.sqrt(left.valueOf() * right.valueOf());
297
+ this.add(geometricMean);
298
+ needsMorePoints = true;
299
+ }
300
+ }
301
+ }
302
+ return this;
303
+ }
304
+ }
@@ -0,0 +1,127 @@
1
+ import Fraction, { type FractionInput } from 'fraction.js';
2
+ import { Harmonic } from './Harmonic';
3
+ import { type HarmonicData, type SpectrumData } from '../lib';
4
+ /**
5
+ * Represents a spectrum as a collection of harmonics.
6
+ * Uses Map to ensure no duplicate frequencies.
7
+ * Key is the frequency as a string (e.g., "3/2") for exact matching.
8
+ *
9
+ * @example
10
+ * // Empty spectrum
11
+ * const s1 = new Spectrum();
12
+ *
13
+ * // Add harmonics using various representations
14
+ * s1.add(new Harmonic(3, 0.5, 0));
15
+ * s1.add(2, 0.3, Math.PI);
16
+ * s1.add({ frequency: '440', amplitude: 0.2, phase: Math.PI / 2 });
17
+ *
18
+ * @example
19
+ * // From SpectrumData
20
+ * const s2 = new Spectrum([
21
+ * { frequency: '110', amplitude: 0.5, phase: 0 },
22
+ * { frequency: '220', amplitude: 0.3, phase: Math.PI }
23
+ * ]);
24
+ */
25
+ export declare class Spectrum {
26
+ private harmonics;
27
+ constructor(data?: SpectrumData);
28
+ /**
29
+ * Number of harmonics in spectrum
30
+ */
31
+ get size(): number;
32
+ /**
33
+ * Add a harmonic to the spectrum.
34
+ * If harmonic with same frequency exists, replaces it.
35
+ */
36
+ private addHarmonic;
37
+ /**
38
+ * Convenience method to add harmonic from various input types
39
+ */
40
+ add(harmonic: Harmonic): this;
41
+ add(data: HarmonicData): this;
42
+ add(frequency: FractionInput, amplitude?: number, phase?: number): this;
43
+ /**
44
+ * Get harmonic by frequency (FractionInput) or Harmonic class
45
+ */
46
+ get(harmonicOrFrequency: Harmonic | FractionInput): Harmonic | undefined;
47
+ /**
48
+ * Check if spectrum contains a harmonic with by frequency (FractionInput) or Harmonic class
49
+ */
50
+ has(harmonicOrFrequency: Harmonic | FractionInput): boolean;
51
+ /**
52
+ * Remove harmonic by frequency (FractionInput) or Harmonic class
53
+ */
54
+ remove(harmonicOrFrequency: Harmonic | FractionInput): this;
55
+ /**
56
+ * Get harmonics sorted by frequency (ascending)
57
+ */
58
+ getHarmonics(): Harmonic[];
59
+ /**
60
+ * Get all frequencies as an array
61
+ */
62
+ getFrequenciesAsNumbers(): number[];
63
+ /**
64
+ * TODO: UNTESTED
65
+ * Get all keys (frequency strings) as an unsorted array.
66
+ * Useful for iterating over harmonics.
67
+ */
68
+ getKeys(): string[];
69
+ /**
70
+ * Check if spectrum is empty
71
+ */
72
+ isEmpty(): boolean;
73
+ /**
74
+ * Clear all harmonics
75
+ */
76
+ clear(): this;
77
+ /**
78
+ * Method to transpose a single harmonic in place
79
+ * Removes the old harmonic and adds the transposed one
80
+ */
81
+ transposeHarmonic(harmonicOrFrequency: Harmonic | FractionInput, ratio: FractionInput): this;
82
+ /**
83
+ * Transpose entire spectrum by a ratio in place
84
+ */
85
+ transpose(ratio: FractionInput): this;
86
+ toTransposed(ratio: FractionInput): Spectrum;
87
+ /**
88
+ * Scale all amplitudes by a factor
89
+ */
90
+ scaleAmplitudes(factor: number): this;
91
+ /**
92
+ * Get the lowest frequency harmonic
93
+ */
94
+ getLowestHarmonic(): Harmonic | undefined;
95
+ /**
96
+ * Get the highest frequency harmonic
97
+ */
98
+ getHighestHarmonic(): Harmonic | undefined;
99
+ /**
100
+ * Calculate GCD of all frequency ratios
101
+ */
102
+ private getGCD;
103
+ /**
104
+ * Calculate period of the resulting wave
105
+ */
106
+ getPeriod(): Fraction | undefined;
107
+ /**
108
+ * Create a copy of this spectrum
109
+ */
110
+ clone(): Spectrum;
111
+ /**
112
+ * Iterate over harmonics
113
+ */
114
+ forEach(callback: (harmonic: Harmonic, key: string) => void): void;
115
+ /**
116
+ * Serialize spectrum to JSON
117
+ */
118
+ toJSON(): SpectrumData;
119
+ /**
120
+ * Create harmonic spectrum (110 Hz, 220 Hz, 330 Hz, 440 Hz, ...)
121
+ */
122
+ static harmonic(count: number, fundamentalHz: FractionInput): Spectrum;
123
+ /**
124
+ * Create spectrum from arrays of arbitrary frequencies, amplitudes and phases
125
+ */
126
+ static fromFrequencies(frequencies: FractionInput[], amplitudes?: number[], phases?: number[]): Spectrum;
127
+ }
@@ -0,0 +1,279 @@
1
+ import Fraction, {} from 'fraction.js';
2
+ import { Harmonic } from './Harmonic';
3
+ import { isHarmonicData } from '../lib';
4
+ /**
5
+ * Represents a spectrum as a collection of harmonics.
6
+ * Uses Map to ensure no duplicate frequencies.
7
+ * Key is the frequency as a string (e.g., "3/2") for exact matching.
8
+ *
9
+ * @example
10
+ * // Empty spectrum
11
+ * const s1 = new Spectrum();
12
+ *
13
+ * // Add harmonics using various representations
14
+ * s1.add(new Harmonic(3, 0.5, 0));
15
+ * s1.add(2, 0.3, Math.PI);
16
+ * s1.add({ frequency: '440', amplitude: 0.2, phase: Math.PI / 2 });
17
+ *
18
+ * @example
19
+ * // From SpectrumData
20
+ * const s2 = new Spectrum([
21
+ * { frequency: '110', amplitude: 0.5, phase: 0 },
22
+ * { frequency: '220', amplitude: 0.3, phase: Math.PI }
23
+ * ]);
24
+ */
25
+ export class Spectrum {
26
+ harmonics;
27
+ constructor(data) {
28
+ this.harmonics = new Map();
29
+ if (data) {
30
+ for (const harmonicData of data) {
31
+ const harmonic = new Harmonic(harmonicData);
32
+ this.addHarmonic(harmonic);
33
+ }
34
+ }
35
+ }
36
+ /**
37
+ * Number of harmonics in spectrum
38
+ */
39
+ get size() {
40
+ return this.harmonics.size;
41
+ }
42
+ /**
43
+ * Add a harmonic to the spectrum.
44
+ * If harmonic with same frequency exists, replaces it.
45
+ */
46
+ addHarmonic(harmonic) {
47
+ const key = harmonic.frequencyStr;
48
+ this.harmonics.set(key, harmonic);
49
+ return this;
50
+ }
51
+ add(harmonicOrDataOrFreq, amplitude, phase) {
52
+ if (harmonicOrDataOrFreq instanceof Harmonic) {
53
+ // Harmonic case
54
+ this.addHarmonic(harmonicOrDataOrFreq);
55
+ }
56
+ else if (isHarmonicData(harmonicOrDataOrFreq)) {
57
+ // HarmonicData case
58
+ const harmonic = new Harmonic(harmonicOrDataOrFreq);
59
+ this.addHarmonic(harmonic);
60
+ }
61
+ else {
62
+ // FractionInput case: frequency, amplitude, phase
63
+ const harmonic = new Harmonic(harmonicOrDataOrFreq, amplitude ?? 1, phase ?? 0);
64
+ this.addHarmonic(harmonic);
65
+ }
66
+ return this;
67
+ }
68
+ /**
69
+ * Get harmonic by frequency (FractionInput) or Harmonic class
70
+ */
71
+ get(harmonicOrFrequency) {
72
+ if (harmonicOrFrequency instanceof Harmonic) {
73
+ const key = harmonicOrFrequency.frequencyStr;
74
+ return this.harmonics.get(key);
75
+ }
76
+ else {
77
+ const key = new Fraction(harmonicOrFrequency).toFraction();
78
+ return this.harmonics.get(key);
79
+ }
80
+ }
81
+ /**
82
+ * Check if spectrum contains a harmonic with by frequency (FractionInput) or Harmonic class
83
+ */
84
+ has(harmonicOrFrequency) {
85
+ if (harmonicOrFrequency instanceof Harmonic) {
86
+ const key = harmonicOrFrequency.frequencyStr;
87
+ return this.harmonics.has(key);
88
+ }
89
+ else {
90
+ const key = new Fraction(harmonicOrFrequency).toFraction();
91
+ return this.harmonics.has(key);
92
+ }
93
+ }
94
+ /**
95
+ * Remove harmonic by frequency (FractionInput) or Harmonic class
96
+ */
97
+ remove(harmonicOrFrequency) {
98
+ if (harmonicOrFrequency instanceof Harmonic) {
99
+ const key = harmonicOrFrequency.frequencyStr;
100
+ this.harmonics.delete(key);
101
+ }
102
+ else {
103
+ const key = new Fraction(harmonicOrFrequency).toFraction();
104
+ this.harmonics.delete(key);
105
+ }
106
+ return this;
107
+ }
108
+ /**
109
+ * Get harmonics sorted by frequency (ascending)
110
+ */
111
+ getHarmonics() {
112
+ return Array.from(this.harmonics.values()).sort((a, b) => a.compareFrequency(b));
113
+ }
114
+ /**
115
+ * Get all frequencies as an array
116
+ */
117
+ getFrequenciesAsNumbers() {
118
+ return this.getHarmonics().map(h => h.frequencyNum);
119
+ }
120
+ /**
121
+ * TODO: UNTESTED
122
+ * Get all keys (frequency strings) as an unsorted array.
123
+ * Useful for iterating over harmonics.
124
+ */
125
+ getKeys() {
126
+ return Array.from(this.harmonics.keys());
127
+ }
128
+ /**
129
+ * Check if spectrum is empty
130
+ */
131
+ isEmpty() {
132
+ return this.size === 0;
133
+ }
134
+ /**
135
+ * Clear all harmonics
136
+ */
137
+ clear() {
138
+ this.harmonics.clear();
139
+ return this;
140
+ }
141
+ /**
142
+ * Method to transpose a single harmonic in place
143
+ * Removes the old harmonic and adds the transposed one
144
+ */
145
+ transposeHarmonic(harmonicOrFrequency, ratio) {
146
+ const oldHarmonic = this.get(harmonicOrFrequency);
147
+ if (!oldHarmonic)
148
+ return this;
149
+ const oldKey = oldHarmonic.frequencyStr;
150
+ const newHarmonic = oldHarmonic.toTransposed(ratio);
151
+ const newKey = newHarmonic.frequencyStr;
152
+ if (oldKey === newKey)
153
+ return this;
154
+ this.harmonics.delete(oldKey);
155
+ this.harmonics.set(newKey, newHarmonic);
156
+ return this;
157
+ }
158
+ /**
159
+ * Transpose entire spectrum by a ratio in place
160
+ */
161
+ transpose(ratio) {
162
+ const r = new Fraction(ratio);
163
+ const ratioValue = r.valueOf();
164
+ if (ratioValue === 1)
165
+ return this;
166
+ if (ratioValue <= 0)
167
+ throw new Error('Ratio must be greater than 0');
168
+ const allHarmonics = this.getHarmonics();
169
+ // If transposing up, start from highest frequency and go down
170
+ if (ratioValue > 1) {
171
+ for (let i = allHarmonics.length - 1; i >= 0; i--) {
172
+ this.transposeHarmonic(allHarmonics[i], r);
173
+ }
174
+ return this;
175
+ }
176
+ for (let i = 0; i < allHarmonics.length; i++) {
177
+ this.transposeHarmonic(allHarmonics[i], r);
178
+ }
179
+ return this;
180
+ }
181
+ toTransposed(ratio) {
182
+ return this.clone().transpose(ratio);
183
+ }
184
+ /**
185
+ * Scale all amplitudes by a factor
186
+ */
187
+ scaleAmplitudes(factor) {
188
+ for (const harmonic of this.harmonics.values()) {
189
+ harmonic.scale(factor);
190
+ }
191
+ return this;
192
+ }
193
+ /**
194
+ * Get the lowest frequency harmonic
195
+ */
196
+ getLowestHarmonic() {
197
+ const sorted = this.getHarmonics();
198
+ return sorted[0];
199
+ }
200
+ /**
201
+ * Get the highest frequency harmonic
202
+ */
203
+ getHighestHarmonic() {
204
+ const sorted = this.getHarmonics();
205
+ return sorted[sorted.length - 1];
206
+ }
207
+ /**
208
+ * Calculate GCD of all frequency ratios
209
+ */
210
+ getGCD() {
211
+ const frequencies = this.getHarmonics().map(h => h.frequency);
212
+ if (frequencies.length === 0)
213
+ return undefined;
214
+ let gcd = frequencies[0];
215
+ for (let i = 1; i < frequencies.length; i++) {
216
+ const freq = frequencies[i];
217
+ if (freq)
218
+ gcd = gcd.gcd(freq);
219
+ }
220
+ return gcd;
221
+ }
222
+ /**
223
+ * Calculate period of the resulting wave
224
+ */
225
+ getPeriod() {
226
+ const gcd = this.getGCD();
227
+ if (!gcd)
228
+ return undefined;
229
+ return gcd.inverse();
230
+ }
231
+ /**
232
+ * Create a copy of this spectrum
233
+ */
234
+ clone() {
235
+ const copy = new Spectrum();
236
+ for (const harmonic of this.harmonics.values()) {
237
+ copy.addHarmonic(harmonic.clone());
238
+ }
239
+ return copy;
240
+ }
241
+ /**
242
+ * Iterate over harmonics
243
+ */
244
+ forEach(callback) {
245
+ this.harmonics.forEach(callback);
246
+ }
247
+ /**
248
+ * Serialize spectrum to JSON
249
+ */
250
+ toJSON() {
251
+ const data = [];
252
+ for (const harmonic of this.harmonics.values()) {
253
+ data.push(harmonic.toJSON());
254
+ }
255
+ return data;
256
+ }
257
+ /**
258
+ * Create harmonic spectrum (110 Hz, 220 Hz, 330 Hz, 440 Hz, ...)
259
+ */
260
+ static harmonic(count, fundamentalHz) {
261
+ const spectrum = new Spectrum();
262
+ for (let i = 1; i <= count; i++) {
263
+ spectrum.add(new Fraction(fundamentalHz).mul(i), 1 / i);
264
+ }
265
+ return spectrum;
266
+ }
267
+ /**
268
+ * Create spectrum from arrays of arbitrary frequencies, amplitudes and phases
269
+ */
270
+ static fromFrequencies(frequencies, amplitudes, phases) {
271
+ const spectrum = new Spectrum();
272
+ frequencies.forEach((frequency, i) => {
273
+ const amp = amplitudes?.[i] ?? 1;
274
+ const phase = phases?.[i] ?? 0;
275
+ spectrum.add(frequency, amp, phase);
276
+ });
277
+ return spectrum;
278
+ }
279
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Harmonic';
2
+ export * from './Spectrum';
3
+ export * from './IntervalSet';