tuning-core 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/LICENSE +21 -0
- package/README.md +15 -0
- package/benches/fraction-performance.bench.ts +141 -0
- package/benches/spectrum-serialization.bench.ts +167 -0
- package/classes/Harmonic.ts +183 -0
- package/classes/IntervalSet.ts +361 -0
- package/classes/README.md +239 -0
- package/classes/Spectrum.ts +333 -0
- package/classes/index.ts +3 -0
- package/index.ts +2 -0
- package/lib/index.ts +2 -0
- package/lib/types.ts +13 -0
- package/lib/utils.ts +47 -0
- package/package.json +21 -0
- package/tests/Harmonic.test.ts +516 -0
- package/tests/IntervalSet.test.ts +728 -0
- package/tests/Spectrum.test.ts +710 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import Fraction from 'fraction.js';
|
|
3
|
+
import { Harmonic, Spectrum } from '../classes';
|
|
4
|
+
|
|
5
|
+
describe('Spectrum instatnce', () => {
|
|
6
|
+
describe('constructor', () => {
|
|
7
|
+
test('creates empty spectrum', () => {
|
|
8
|
+
const s = new Spectrum();
|
|
9
|
+
expect(s.size).toBe(0);
|
|
10
|
+
expect(s.isEmpty()).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('creates spectrum from SpectrumData', () => {
|
|
14
|
+
const s = new Spectrum([
|
|
15
|
+
{ frequency: { n: 100, d: 1 }, amplitude: 0.5, phase: 0 },
|
|
16
|
+
{ frequency: { n: 200, d: 1 }, amplitude: 0.3, phase: Math.PI }
|
|
17
|
+
]);
|
|
18
|
+
expect(s.size).toBe(2);
|
|
19
|
+
expect(s.has(100)).toBe(true);
|
|
20
|
+
expect(s.has(200)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('deserializes harmonics correctly', () => {
|
|
24
|
+
const s = new Spectrum([
|
|
25
|
+
{ frequency: { n: 100, d: 1 }, amplitude: 0.8, phase: 0 },
|
|
26
|
+
{ frequency: { n: 200, d: 1 }, amplitude: 0.5, phase: Math.PI / 2 }
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const h1 = s.get(100);
|
|
30
|
+
expect(h1?.amplitude).toBe(0.8);
|
|
31
|
+
expect(h1?.phase).toBe(0);
|
|
32
|
+
|
|
33
|
+
const h2 = s.get(200);
|
|
34
|
+
expect(h2?.amplitude).toBe(0.5);
|
|
35
|
+
expect(h2?.phase).toBe(Math.PI / 2);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('add', () => {
|
|
40
|
+
test('adds harmonic from Harmonic object', () => {
|
|
41
|
+
const s = new Spectrum();
|
|
42
|
+
const h = new Harmonic(100, 1);
|
|
43
|
+
s.add(h);
|
|
44
|
+
expect(s.size).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('adds harmonic from HarmonicData', () => {
|
|
48
|
+
const s = new Spectrum();
|
|
49
|
+
s.add({ frequency: { n: 200, d: 1 }, amplitude: 0.5, phase: 0 });
|
|
50
|
+
expect(s.size).toBe(1);
|
|
51
|
+
const h = s.get(200);
|
|
52
|
+
expect(h?.amplitude).toBe(0.5);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('adds harmonic from parameters (Fraction)', () => {
|
|
56
|
+
const s = new Spectrum();
|
|
57
|
+
s.add(new Fraction(200), 0.5, Math.PI);
|
|
58
|
+
expect(s.size).toBe(1);
|
|
59
|
+
const h = s.get(200);
|
|
60
|
+
expect(h?.amplitude).toBe(0.5);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('adds harmonic from parameters (number)', () => {
|
|
64
|
+
const s = new Spectrum();
|
|
65
|
+
s.add(200, 0.8);
|
|
66
|
+
expect(s.size).toBe(1);
|
|
67
|
+
const h = s.get(200);
|
|
68
|
+
expect(h?.amplitude).toBe(0.8);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('adds harmonic from parameters (string)', () => {
|
|
72
|
+
const s = new Spectrum();
|
|
73
|
+
s.add('300', 0.6);
|
|
74
|
+
expect(s.size).toBe(1);
|
|
75
|
+
const h = s.get(300);
|
|
76
|
+
expect(h?.amplitude).toBe(0.6);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('uses default amplitude and phase', () => {
|
|
80
|
+
const s = new Spectrum();
|
|
81
|
+
s.add(100);
|
|
82
|
+
const h = s.get(100);
|
|
83
|
+
expect(h?.amplitude).toBe(1);
|
|
84
|
+
expect(h?.phase).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('adds multiple harmonics', () => {
|
|
88
|
+
const s = new Spectrum();
|
|
89
|
+
s.add(100, 1);
|
|
90
|
+
s.add(200, 0.5);
|
|
91
|
+
s.add(300, 0.33);
|
|
92
|
+
expect(s.size).toBe(3);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('replaces harmonic with same frequency', () => {
|
|
96
|
+
const s = new Spectrum();
|
|
97
|
+
s.add('300', 0.5);
|
|
98
|
+
s.add(300, 0.8); // Same frequency, different amplitude
|
|
99
|
+
|
|
100
|
+
expect(s.size).toBe(1);
|
|
101
|
+
const h = s.get(300);
|
|
102
|
+
expect(h?.amplitude).toBe(0.8);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('treats equivalent fractions as same key', () => {
|
|
106
|
+
const s = new Spectrum();
|
|
107
|
+
s.add('6/4', 0.5);
|
|
108
|
+
s.add('3/2', 0.8);
|
|
109
|
+
|
|
110
|
+
expect(s.size).toBe(1); // Should replace, as 6/4 = 3/2
|
|
111
|
+
const h = s.get('3/2');
|
|
112
|
+
expect(h?.amplitude).toBe(0.8);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('get', () => {
|
|
117
|
+
test('retrieves harmonic by Harmonic object', () => {
|
|
118
|
+
const s = new Spectrum();
|
|
119
|
+
const h1 = new Harmonic(200, 0.5);
|
|
120
|
+
s.add(h1);
|
|
121
|
+
const h2 = s.get(h1);
|
|
122
|
+
expect(h2?.amplitude).toBe(0.5);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('retrieves harmonic by Fraction', () => {
|
|
126
|
+
const s = new Spectrum();
|
|
127
|
+
s.add(new Fraction(3, 2), 0.5);
|
|
128
|
+
const h = s.get(new Fraction(3, 2));
|
|
129
|
+
expect(h?.amplitude).toBe(0.5);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('retrieves harmonic by number', () => {
|
|
133
|
+
const s = new Spectrum();
|
|
134
|
+
s.add(300, 0.8);
|
|
135
|
+
const h = s.get(300);
|
|
136
|
+
expect(h?.amplitude).toBe(0.8);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('retrieves harmonic by string', () => {
|
|
140
|
+
const s = new Spectrum();
|
|
141
|
+
s.add('300', 0.6);
|
|
142
|
+
const h = s.get('300');
|
|
143
|
+
expect(h?.amplitude).toBe(0.6);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('returns undefined for non-existent harmonic', () => {
|
|
147
|
+
const s = new Spectrum();
|
|
148
|
+
s.add(100, 1);
|
|
149
|
+
const h = s.get(200);
|
|
150
|
+
expect(h).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('finds harmonic using equivalent fraction', () => {
|
|
154
|
+
const s = new Spectrum();
|
|
155
|
+
s.add('3/2', 0.5);
|
|
156
|
+
const h = s.get('6/4');
|
|
157
|
+
expect(h?.amplitude).toBe(0.5);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('has', () => {
|
|
162
|
+
test('returns true for existing harmonic by Harmonic object', () => {
|
|
163
|
+
const s = new Spectrum();
|
|
164
|
+
const h = new Harmonic(200, 1);
|
|
165
|
+
s.add(h);
|
|
166
|
+
expect(s.has(h)).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('returns true for existing harmonic by frequency', () => {
|
|
170
|
+
const s = new Spectrum();
|
|
171
|
+
s.add(200, 1);
|
|
172
|
+
expect(s.has(200)).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('returns false for non-existent harmonic', () => {
|
|
176
|
+
const s = new Spectrum();
|
|
177
|
+
s.add(100, 1);
|
|
178
|
+
expect(s.has(200)).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('finds harmonic using equivalent fraction', () => {
|
|
182
|
+
const s = new Spectrum();
|
|
183
|
+
s.add('3/2', 1);
|
|
184
|
+
expect(s.has(1.5)).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('remove', () => {
|
|
189
|
+
test('removes existing harmonic by Harmonic object', () => {
|
|
190
|
+
const s = new Spectrum();
|
|
191
|
+
const h = new Harmonic(100, 1);
|
|
192
|
+
s.add(h).add(200, 0.5)
|
|
193
|
+
|
|
194
|
+
s.remove(h);
|
|
195
|
+
|
|
196
|
+
expect(s.has(h)).toBe(false);
|
|
197
|
+
expect(s.size).toBe(1);
|
|
198
|
+
expect(s.has(200)).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('removes existing harmonic by frequency', () => {
|
|
202
|
+
const s = new Spectrum();
|
|
203
|
+
s.add(100, 1).add(200, 0.5);
|
|
204
|
+
|
|
205
|
+
s.remove(100);
|
|
206
|
+
|
|
207
|
+
expect(s.size).toBe(1);
|
|
208
|
+
expect(s.has(100)).toBe(false);
|
|
209
|
+
expect(s.has(200)).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('getHarmonics', () => {
|
|
214
|
+
test('returns all harmonics as array, sorted by frequency', () => {
|
|
215
|
+
const s = new Spectrum();
|
|
216
|
+
s.add(2, 0.33);
|
|
217
|
+
s.add(1, 1);
|
|
218
|
+
s.add('3/2', 0.5);
|
|
219
|
+
|
|
220
|
+
const harmonics = s.getHarmonics();
|
|
221
|
+
expect(harmonics.length).toBe(3);
|
|
222
|
+
expect(harmonics.every(h => h instanceof Harmonic)).toBe(true);
|
|
223
|
+
// Should be sorted
|
|
224
|
+
expect(harmonics[0]?.frequencyNum).toBe(1);
|
|
225
|
+
expect(harmonics[1]?.frequencyNum).toBe(1.5);
|
|
226
|
+
expect(harmonics[2]?.frequencyNum).toBe(2);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('returns empty array for empty spectrum', () => {
|
|
230
|
+
const s = new Spectrum();
|
|
231
|
+
expect(s.getHarmonics()).toEqual([]);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('getFrequenciesAsNumbers', () => {
|
|
236
|
+
test('returns all frequencies as number array', () => {
|
|
237
|
+
const s = new Spectrum();
|
|
238
|
+
s.add(100, 1);
|
|
239
|
+
s.add('200', 0.5);
|
|
240
|
+
|
|
241
|
+
const freqs = s.getFrequenciesAsNumbers();
|
|
242
|
+
expect(freqs.length).toBe(2);
|
|
243
|
+
expect(freqs).toContain(100);
|
|
244
|
+
expect(freqs).toContain(200);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('size and isEmpty', () => {
|
|
249
|
+
test('size returns correct count', () => {
|
|
250
|
+
const s = new Spectrum();
|
|
251
|
+
expect(s.size).toBe(0);
|
|
252
|
+
s.add(100, 1);
|
|
253
|
+
expect(s.size).toBe(1);
|
|
254
|
+
s.add(200, 1);
|
|
255
|
+
expect(s.size).toBe(2);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('isEmpty returns true for empty spectrum', () => {
|
|
259
|
+
const s = new Spectrum();
|
|
260
|
+
expect(s.isEmpty()).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('isEmpty returns false for non-empty spectrum', () => {
|
|
264
|
+
const s = new Spectrum();
|
|
265
|
+
s.add(100, 1);
|
|
266
|
+
expect(s.isEmpty()).toBe(false);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('clear', () => {
|
|
271
|
+
test('removes all harmonics', () => {
|
|
272
|
+
const s = new Spectrum();
|
|
273
|
+
s.add(100, 1);
|
|
274
|
+
s.add(200, 1);
|
|
275
|
+
s.add(300, 1);
|
|
276
|
+
|
|
277
|
+
s.clear();
|
|
278
|
+
expect(s.size).toBe(0);
|
|
279
|
+
expect(s.isEmpty()).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('transpose', () => {
|
|
284
|
+
test('transposes all harmonics by ratio', () => {
|
|
285
|
+
const s = new Spectrum();
|
|
286
|
+
s.add(100, 1);
|
|
287
|
+
s.add(200, 0.5);
|
|
288
|
+
|
|
289
|
+
s.transpose('3/2');
|
|
290
|
+
|
|
291
|
+
expect(s.has(150)).toBe(true);
|
|
292
|
+
expect(s.has(300)).toBe(true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('preserves amplitudes', () => {
|
|
296
|
+
const s = new Spectrum();
|
|
297
|
+
s.add(100, 0.8);
|
|
298
|
+
s.transpose(2);
|
|
299
|
+
|
|
300
|
+
const h = s.get(200);
|
|
301
|
+
expect(h?.amplitude).toBe(0.8);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('handles transpose by 1 (no change)', () => {
|
|
305
|
+
const s = new Spectrum();
|
|
306
|
+
s.add(100, 0.5);
|
|
307
|
+
s.transpose(1);
|
|
308
|
+
|
|
309
|
+
expect(s.has(100)).toBe(true);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('handles fractional transpose', () => {
|
|
313
|
+
const s = new Spectrum();
|
|
314
|
+
s.add(200, 1);
|
|
315
|
+
s.transpose('3/4');
|
|
316
|
+
|
|
317
|
+
expect(s.has(150)).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('throws error for non-positive ratio', () => {
|
|
321
|
+
const s = new Spectrum();
|
|
322
|
+
s.add(100, 1);
|
|
323
|
+
expect(() => s.transpose(0)).toThrow('Ratio must be greater than 0');
|
|
324
|
+
expect(() => s.transpose(-1)).toThrow('Ratio must be greater than 0');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('toTransposed', () => {
|
|
329
|
+
test('creates new transposed spectrum without modifying original', () => {
|
|
330
|
+
const s1 = new Spectrum();
|
|
331
|
+
s1.add(100, 1);
|
|
332
|
+
s1.add(200, 0.5);
|
|
333
|
+
|
|
334
|
+
const s2 = s1.toTransposed('3/2');
|
|
335
|
+
|
|
336
|
+
expect(s1.has(100)).toBe(true);
|
|
337
|
+
expect(s1.has(200)).toBe(true);
|
|
338
|
+
expect(s2.has(150)).toBe(true);
|
|
339
|
+
expect(s2.has(300)).toBe(true);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('modyfying cope does not modify original', () => {
|
|
343
|
+
const s1 = new Spectrum();
|
|
344
|
+
s1.add(100, 1);
|
|
345
|
+
s1.add(200, 0.5);
|
|
346
|
+
|
|
347
|
+
const s2 = s1.toTransposed('1');
|
|
348
|
+
s2.transpose('3/2');
|
|
349
|
+
|
|
350
|
+
expect(s1.has(100)).toBe(true);
|
|
351
|
+
expect(s1.has(200)).toBe(true);
|
|
352
|
+
expect(s2.has(150)).toBe(true);
|
|
353
|
+
expect(s2.has(300)).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe('scaleAmplitudes', () => {
|
|
358
|
+
test('scales all amplitudes by factor', () => {
|
|
359
|
+
const s = new Spectrum();
|
|
360
|
+
s.add(100, 0.8);
|
|
361
|
+
s.add(200, 0.4);
|
|
362
|
+
|
|
363
|
+
s.scaleAmplitudes(0.5);
|
|
364
|
+
|
|
365
|
+
expect(s.get(100)?.amplitude).toBe(0.4);
|
|
366
|
+
expect(s.get(200)?.amplitude).toBe(0.2);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('preserves frequencies', () => {
|
|
370
|
+
const s = new Spectrum();
|
|
371
|
+
s.add(100, 1);
|
|
372
|
+
s.scaleAmplitudes(0.5);
|
|
373
|
+
|
|
374
|
+
expect(s.has(100)).toBe(true);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('getLowestHarmonic', () => {
|
|
379
|
+
test('returns harmonic with lowest frequency', () => {
|
|
380
|
+
const s = new Spectrum();
|
|
381
|
+
s.add(200, 1);
|
|
382
|
+
s.add(150, 1);
|
|
383
|
+
s.add(300, 1);
|
|
384
|
+
|
|
385
|
+
const lowest = s.getLowestHarmonic();
|
|
386
|
+
expect(lowest?.frequencyStr).toBe("150");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('returns undefined for empty spectrum', () => {
|
|
390
|
+
const s = new Spectrum();
|
|
391
|
+
expect(s.getLowestHarmonic()).toBeUndefined();
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe('getHighestHarmonic', () => {
|
|
396
|
+
test('returns harmonic with highest frequency', () => {
|
|
397
|
+
const s = new Spectrum();
|
|
398
|
+
s.add(100, 1);
|
|
399
|
+
s.add(600, 1);
|
|
400
|
+
s.add(300, 1);
|
|
401
|
+
|
|
402
|
+
const highest = s.getHighestHarmonic();
|
|
403
|
+
expect(highest?.frequencyNum).toBe(600);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('returns undefined for empty spectrum', () => {
|
|
407
|
+
const s = new Spectrum();
|
|
408
|
+
expect(s.getHighestHarmonic()).toBeUndefined();
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('getPeriod', () => {
|
|
413
|
+
test('calculates period from GCD', () => {
|
|
414
|
+
const s = new Spectrum();
|
|
415
|
+
s.add(100, 1);
|
|
416
|
+
s.add(200, 1);
|
|
417
|
+
s.add(300, 1);
|
|
418
|
+
|
|
419
|
+
const period = s.getPeriod();
|
|
420
|
+
expect(period?.toFraction()).toBe('1/100');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test('returns undefined for empty spectrum', () => {
|
|
424
|
+
const s = new Spectrum();
|
|
425
|
+
expect(s.getPeriod()).toBeUndefined();
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe('clone', () => {
|
|
430
|
+
test('creates independent copy', () => {
|
|
431
|
+
const s1 = new Spectrum();
|
|
432
|
+
s1.add(100, 0.8);
|
|
433
|
+
s1.add(200, 0.5);
|
|
434
|
+
|
|
435
|
+
const s2 = s1.clone();
|
|
436
|
+
|
|
437
|
+
expect(s2.size).toBe(2);
|
|
438
|
+
expect(s2.has(100)).toBe(true);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('clone is independent of original', () => {
|
|
442
|
+
const s1 = new Spectrum();
|
|
443
|
+
s1.add(100, 1);
|
|
444
|
+
|
|
445
|
+
const s2 = s1.clone();
|
|
446
|
+
s2.add(200, 0.5);
|
|
447
|
+
|
|
448
|
+
expect(s1.size).toBe(1);
|
|
449
|
+
expect(s2.size).toBe(2);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('harmonics in clone are independent', () => {
|
|
453
|
+
const s1 = new Spectrum();
|
|
454
|
+
s1.add(100, 0.5);
|
|
455
|
+
|
|
456
|
+
const s2 = s1.clone();
|
|
457
|
+
const h2 = s2.get(100);
|
|
458
|
+
h2?.setAmplitude(0.8);
|
|
459
|
+
|
|
460
|
+
expect(s1.get(100)?.amplitude).toBe(0.5);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
describe('forEach', () => {
|
|
465
|
+
test('iterates over all harmonics', () => {
|
|
466
|
+
const s = new Spectrum();
|
|
467
|
+
s.add(100, 1);
|
|
468
|
+
s.add(200, 0.5);
|
|
469
|
+
|
|
470
|
+
const frequencies: string[] = [];
|
|
471
|
+
s.forEach((h, key) => {
|
|
472
|
+
frequencies.push(key);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
expect(frequencies.length).toBe(2);
|
|
476
|
+
expect(frequencies).toContain('100');
|
|
477
|
+
expect(frequencies).toContain('200');
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
describe('toJSON', () => {
|
|
482
|
+
test('serializes spectrum to SpectrumData', () => {
|
|
483
|
+
const s = new Spectrum();
|
|
484
|
+
s.add(100, 0.8, 0);
|
|
485
|
+
s.add(200, 0.5, 3.14);
|
|
486
|
+
|
|
487
|
+
const json = s.toJSON();
|
|
488
|
+
expect(json).toEqual([
|
|
489
|
+
{ frequency: { n: 100, d: 1 }, amplitude: 0.8, phase: 0 },
|
|
490
|
+
{ frequency: { n: 200, d: 1 }, amplitude: 0.5, phase: 3.14 }
|
|
491
|
+
]);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test('round-trip serialization', () => {
|
|
495
|
+
const s1 = new Spectrum();
|
|
496
|
+
s1.add(100, 0.8, 0);
|
|
497
|
+
s1.add(200, 0.5, 3.14);
|
|
498
|
+
|
|
499
|
+
const json = s1.toJSON();
|
|
500
|
+
const s2 = new Spectrum(json);
|
|
501
|
+
|
|
502
|
+
expect(s2.size).toBe(2);
|
|
503
|
+
expect(s2.get(100)?.amplitude).toBe(0.8);
|
|
504
|
+
expect(s2.get(200)?.amplitude).toBe(0.5);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
describe('edge cases', () => {
|
|
509
|
+
test('handles large number of harmonics', () => {
|
|
510
|
+
const s = new Spectrum();
|
|
511
|
+
for (let i = 1; i <= 100; i++) {
|
|
512
|
+
s.add(i, 1 / i);
|
|
513
|
+
}
|
|
514
|
+
expect(s.size).toBe(100);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test('handles very small amplitude values', () => {
|
|
518
|
+
const s = new Spectrum();
|
|
519
|
+
s.add(1, 0.000001);
|
|
520
|
+
expect(s.get(1)?.amplitude).toBe(0.000001);
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe('Spectrum static methods', () => {
|
|
526
|
+
describe('harmonic', () => {
|
|
527
|
+
test('creates harmonic series with correct count', () => {
|
|
528
|
+
const s = Spectrum.harmonic(5, 100);
|
|
529
|
+
expect(s.size).toBe(5);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test('creates harmonics with correct frequencies as ratios', () => {
|
|
533
|
+
const s = Spectrum.harmonic(3, 100);
|
|
534
|
+
expect(s.has(100)).toBe(true);
|
|
535
|
+
expect(s.has(200)).toBe(true);
|
|
536
|
+
expect(s.has(300)).toBe(true);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test('applies natural amplitude decay (1/n)', () => {
|
|
540
|
+
const s = Spectrum.harmonic(3, 100);
|
|
541
|
+
expect(s.get(100)?.amplitude).toBe(1);
|
|
542
|
+
expect(s.get(200)?.amplitude).toBe(0.5);
|
|
543
|
+
expect(s.get(300)?.amplitude).toBeCloseTo(0.333, 2);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test('handles single harmonic', () => {
|
|
547
|
+
const s = Spectrum.harmonic(1, 100);
|
|
548
|
+
expect(s.size).toBe(1);
|
|
549
|
+
expect(s.has(100)).toBe(true);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('handles large count', () => {
|
|
553
|
+
const s = Spectrum.harmonic(10000, 1);
|
|
554
|
+
expect(s.size).toBe(10000);
|
|
555
|
+
expect(s.has(10000)).toBe(true);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test('creates ratios correctly', () => {
|
|
559
|
+
const s = Spectrum.harmonic(5, 100);
|
|
560
|
+
const harmonics = s.getHarmonics();
|
|
561
|
+
expect(harmonics[0]?.frequencyNum).toBe(100);
|
|
562
|
+
expect(harmonics[1]?.frequencyNum).toBe(200);
|
|
563
|
+
expect(harmonics[2]?.frequencyNum).toBe(300);
|
|
564
|
+
expect(harmonics[3]?.frequencyNum).toBe(400);
|
|
565
|
+
expect(harmonics[4]?.frequencyNum).toBe(500);
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
describe('fromFrequencies', () => {
|
|
570
|
+
test('creates spectrum from Fraction array', () => {
|
|
571
|
+
const ratios = [new Fraction(100), new Fraction(200), new Fraction(300)];
|
|
572
|
+
const s = Spectrum.fromFrequencies(ratios);
|
|
573
|
+
|
|
574
|
+
expect(s.size).toBe(3);
|
|
575
|
+
expect(s.has(200)).toBe(true);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test('creates spectrum from number array', () => {
|
|
579
|
+
const s = Spectrum.fromFrequencies([100, 200, 300]);
|
|
580
|
+
|
|
581
|
+
expect(s.size).toBe(3);
|
|
582
|
+
expect(s.has('200')).toBe(true);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test('creates spectrum from string array', () => {
|
|
586
|
+
const s = Spectrum.fromFrequencies(['100', '200', '300']);
|
|
587
|
+
|
|
588
|
+
expect(s.size).toBe(3);
|
|
589
|
+
expect(s.has('200')).toBe(true);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test('uses default amplitude of 1 when not provided', () => {
|
|
593
|
+
const s = Spectrum.fromFrequencies([100, 200]);
|
|
594
|
+
expect(s.get(100)?.amplitude).toBe(1);
|
|
595
|
+
expect(s.get(200)?.amplitude).toBe(1);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test('uses provided amplitudes', () => {
|
|
599
|
+
const s = Spectrum.fromFrequencies([100, 200], [0.8, 0.5]);
|
|
600
|
+
expect(s.get(100)?.amplitude).toBe(0.8);
|
|
601
|
+
expect(s.get(200)?.amplitude).toBe(0.5);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test('uses default phase of 0 when not provided', () => {
|
|
605
|
+
const s = Spectrum.fromFrequencies([100, 200]);
|
|
606
|
+
expect(s.get(100)?.phase).toBe(0);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test('uses provided phases', () => {
|
|
610
|
+
const s = Spectrum.fromFrequencies([100, 200], [1, 1], [Math.PI, Math.PI / 2]);
|
|
611
|
+
expect(s.get(100)?.phase).toBe(Math.PI);
|
|
612
|
+
expect(s.get(200)?.phase).toBe(Math.PI / 2);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test('handles mixed input types', () => {
|
|
616
|
+
const s = Spectrum.fromFrequencies([100, '200', new Fraction(300)]);
|
|
617
|
+
expect(s.size).toBe(3);
|
|
618
|
+
expect(s.has('100')).toBe(true);
|
|
619
|
+
expect(s.has('200')).toBe(true);
|
|
620
|
+
expect(s.has('300')).toBe(true);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test('handles empty array', () => {
|
|
624
|
+
const s = Spectrum.fromFrequencies([]);
|
|
625
|
+
expect(s.size).toBe(0);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test('handles partial amplitude array', () => {
|
|
629
|
+
const s = Spectrum.fromFrequencies([100, 200, 300], [0.8]);
|
|
630
|
+
expect(s.get(100)?.amplitude).toBe(0.8);
|
|
631
|
+
expect(s.get(200)?.amplitude).toBe(1);
|
|
632
|
+
expect(s.get(300)?.amplitude).toBe(1);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test('handles partial phase array', () => {
|
|
636
|
+
const s = Spectrum.fromFrequencies([100, 200, 300], undefined, [Math.PI]);
|
|
637
|
+
expect(s.get(100)?.phase).toBe(Math.PI);
|
|
638
|
+
expect(s.get(200)?.phase).toBe(0);
|
|
639
|
+
expect(s.get(300)?.phase).toBe(0);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
describe('integration tests', () => {
|
|
644
|
+
test('can transpose factory-created spectrum', () => {
|
|
645
|
+
const s = Spectrum.harmonic(3, 100);
|
|
646
|
+
s.transpose('3/2');
|
|
647
|
+
|
|
648
|
+
expect(s.has(100)).toBe(false);
|
|
649
|
+
expect(s.has(150)).toBe(true);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test('can clone factory-created spectrum', () => {
|
|
653
|
+
const s1 = Spectrum.harmonic(5, 100);
|
|
654
|
+
const s2 = s1.clone();
|
|
655
|
+
|
|
656
|
+
expect(s2.size).toBe(s1.size);
|
|
657
|
+
expect(s2.has(100)).toBe(true);
|
|
658
|
+
expect(s2.has(200)).toBe(true);
|
|
659
|
+
expect(s2.has(300)).toBe(true);
|
|
660
|
+
expect(s2.has(400)).toBe(true);
|
|
661
|
+
expect(s2.has(500)).toBe(true);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test('can scale amplitudes', () => {
|
|
665
|
+
const s = Spectrum.harmonic(2, 100);
|
|
666
|
+
s.scaleAmplitudes(0.5);
|
|
667
|
+
|
|
668
|
+
expect(s.get(100)?.amplitude).toBe(0.5);
|
|
669
|
+
expect(s.get(200)?.amplitude).toBe(0.25);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
describe('edge cases', () => {
|
|
674
|
+
test('harmonic with count 0 creates empty spectrum', () => {
|
|
675
|
+
const s = Spectrum.harmonic(0, 100);
|
|
676
|
+
expect(s.size).toBe(0);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test('fromFrequencies handles duplicate ratios', () => {
|
|
680
|
+
const s = Spectrum.fromFrequencies([1, '3/2', 1.5], [0.8, 0.5, 0.6]);
|
|
681
|
+
expect(s.size).toBe(2); // 1 and 3/2 (1.5 is duplicate)
|
|
682
|
+
expect(s.get('3/2')?.amplitude).toBe(0.6); // Last one wins
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test('large harmonic series is sorted correctly', () => {
|
|
686
|
+
const s = Spectrum.harmonic(1, 10000);
|
|
687
|
+
const sorted = s.getHarmonics();
|
|
688
|
+
|
|
689
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
690
|
+
expect(sorted[i]?.compareFrequency(sorted[i - 1]!)).toBeGreaterThan(0);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
describe('musical examples', () => {
|
|
696
|
+
test('creates major triad', () => {
|
|
697
|
+
const s = Spectrum.fromFrequencies([1, '5/4', '3/2'], [1, 1, 1]);
|
|
698
|
+
expect(s.size).toBe(3);
|
|
699
|
+
expect(s.has(1)).toBe(true);
|
|
700
|
+
expect(s.has('5/4')).toBe(true);
|
|
701
|
+
expect(s.has('3/2')).toBe(true);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test('creates minor triad', () => {
|
|
705
|
+
const s = Spectrum.fromFrequencies([1, '6/5', '3/2']);
|
|
706
|
+
expect(s.size).toBe(3);
|
|
707
|
+
expect(s.has('6/5')).toBe(true);
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
});
|