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.
@@ -0,0 +1,516 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import Fraction from 'fraction.js';
3
+ import { Harmonic } from '../classes';
4
+
5
+ describe('Harmonic', () => {
6
+ describe('constructor', () => {
7
+ test('creates harmonic with Fraction', () => {
8
+ const h = new Harmonic(new Fraction(3, 2), 0.5, Math.PI);
9
+ expect(h.frequencyStr).toBe('3/2');
10
+ expect(h.amplitude).toBe(0.5);
11
+ expect(h.phase).toBe(Math.PI);
12
+ });
13
+
14
+ test('creates harmonic with number', () => {
15
+ const h = new Harmonic(1.5, 0.5);
16
+ expect(h.frequencyNum).toBe(1.5);
17
+ expect(h.amplitude).toBe(0.5);
18
+ expect(h.phase).toBe(0);
19
+ });
20
+
21
+ test('creates harmonic with fraction string', () => {
22
+ const h = new Harmonic('3/2', 0.8);
23
+ expect(h.frequencyStr).toBe('3/2');
24
+ expect(h.amplitude).toBe(0.8);
25
+ });
26
+
27
+ test('uses default amplitude of 1', () => {
28
+ const h = new Harmonic(1);
29
+ expect(h.amplitude).toBe(1);
30
+ });
31
+
32
+ test('uses default phase of 0', () => {
33
+ const h = new Harmonic(1, 0.5);
34
+ expect(h.phase).toBe(0);
35
+ });
36
+
37
+ test('throws error for zero frequency', () => {
38
+ expect(() => new Harmonic(0, 1)).toThrow('Frequency must be positive');
39
+ });
40
+
41
+ test('throws error for negative frequency', () => {
42
+ expect(() => new Harmonic(-1, 1)).toThrow('Frequency must be positive');
43
+ });
44
+
45
+ test('throws error for amplitude < 0', () => {
46
+ expect(() => new Harmonic(1, -0.1)).toThrow('Amplitude must be between 0 and 1');
47
+ });
48
+
49
+ test('throws error for amplitude > 1', () => {
50
+ expect(() => new Harmonic(1, 1.1)).toThrow('Amplitude must be between 0 and 1');
51
+ });
52
+
53
+ test('throws error for phase < 0', () => {
54
+ expect(() => new Harmonic(1, 1, -0.1)).toThrow('Phase must be between 0 and 2π');
55
+ });
56
+
57
+ test('throws error for phase >= 2π', () => {
58
+ expect(() => new Harmonic(1, 1, 2 * Math.PI)).toThrow('Phase must be between 0 and 2π');
59
+ });
60
+
61
+ test('allows phase exactly at 0', () => {
62
+ const h = new Harmonic(1, 1, 0);
63
+ expect(h.phase).toBe(0);
64
+ });
65
+
66
+ test('allows phase just below 2π', () => {
67
+ const phase = 2 * Math.PI - 0.0001;
68
+ const h = new Harmonic(1, 1, phase);
69
+ expect(h.phase).toBe(phase);
70
+ });
71
+
72
+ test('creates harmonic with HarmonicData object', () => {
73
+ const data = {
74
+ frequency: { n: 3, d: 2 },
75
+ amplitude: 0.5,
76
+ phase: Math.PI,
77
+ };
78
+ const h = new Harmonic(data);
79
+ expect(h.frequencyStr).toBe('3/2');
80
+ expect(h.amplitude).toBe(0.5);
81
+ expect(h.phase).toBe(Math.PI);
82
+ });
83
+
84
+ test('creates harmonic with HarmonicData using Fraction format', () => {
85
+ const data = {
86
+ frequency: { n: 1761, d: 4 }, // 440.25
87
+ amplitude: 0.8,
88
+ phase: 0,
89
+ };
90
+ const h = new Harmonic(data);
91
+ expect(h.frequencyNum).toBe(440.25);
92
+ expect(h.amplitude).toBe(0.8);
93
+ expect(h.phase).toBe(0);
94
+ });
95
+ });
96
+
97
+ describe('setFrequency', () => {
98
+ test('updates frequency with Fraction', () => {
99
+ const h = new Harmonic(1, 1);
100
+ h.setFrequency(new Fraction(5, 4));
101
+ expect(h.frequencyStr).toBe('5/4');
102
+ });
103
+
104
+ test('updates frequency with number', () => {
105
+ const h = new Harmonic(1, 1);
106
+ h.setFrequency(2.5);
107
+ expect(h.frequencyNum).toBe(2.5);
108
+ });
109
+
110
+ test('updates frequency with string', () => {
111
+ const h = new Harmonic(1, 1);
112
+ h.setFrequency('7/4');
113
+ expect(h.frequencyStr).toBe('7/4');
114
+ });
115
+
116
+ test('throws error for zero frequency', () => {
117
+ const h = new Harmonic(1, 1);
118
+ expect(() => h.setFrequency(0)).toThrow('Frequency must be positive');
119
+ });
120
+
121
+ test('throws error for negative frequency', () => {
122
+ const h = new Harmonic(1, 1);
123
+ expect(() => h.setFrequency(-1)).toThrow('Frequency must be positive');
124
+ });
125
+
126
+ test('returns this for method chaining', () => {
127
+ const h = new Harmonic(1, 1);
128
+ const result = h.setFrequency('3/2');
129
+ expect(result).toBe(h);
130
+ expect(h.frequencyStr).toBe('3/2');
131
+ });
132
+ });
133
+
134
+ describe('setAmplitude', () => {
135
+ test('updates amplitude', () => {
136
+ const h = new Harmonic(1, 0.5);
137
+ h.setAmplitude(0.8);
138
+ expect(h.amplitude).toBe(0.8);
139
+ });
140
+
141
+ test('allows amplitude of 0', () => {
142
+ const h = new Harmonic(1, 1);
143
+ h.setAmplitude(0);
144
+ expect(h.amplitude).toBe(0);
145
+ });
146
+
147
+ test('allows amplitude of 1', () => {
148
+ const h = new Harmonic(1, 0.5);
149
+ h.setAmplitude(1);
150
+ expect(h.amplitude).toBe(1);
151
+ });
152
+
153
+ test('throws error for negative amplitude', () => {
154
+ const h = new Harmonic(1, 1);
155
+ expect(() => h.setAmplitude(-0.1)).toThrow('Amplitude must be between 0 and 1');
156
+ });
157
+
158
+ test('throws error for amplitude > 1', () => {
159
+ const h = new Harmonic(1, 1);
160
+ expect(() => h.setAmplitude(1.5)).toThrow('Amplitude must be between 0 and 1');
161
+ });
162
+
163
+ test('returns this for method chaining', () => {
164
+ const h = new Harmonic(1, 0.5);
165
+ const result = h.setAmplitude(0.8);
166
+ expect(result).toBe(h);
167
+ expect(h.amplitude).toBe(0.8);
168
+ });
169
+ });
170
+
171
+ describe('setPhase', () => {
172
+ test('updates phase', () => {
173
+ const h = new Harmonic(1, 1, 0);
174
+ h.setPhase(Math.PI);
175
+ expect(h.phase).toBe(Math.PI);
176
+ });
177
+
178
+ test('allows phase of 0', () => {
179
+ const h = new Harmonic(1, 1, Math.PI);
180
+ h.setPhase(0);
181
+ expect(h.phase).toBe(0);
182
+ });
183
+
184
+ test('throws error for negative phase', () => {
185
+ const h = new Harmonic(1, 1);
186
+ expect(() => h.setPhase(-0.1)).toThrow('Phase must be between 0 and 2π');
187
+ });
188
+
189
+ test('throws error for phase >= 2π', () => {
190
+ const h = new Harmonic(1, 1);
191
+ expect(() => h.setPhase(2 * Math.PI)).toThrow('Phase must be between 0 and 2π');
192
+ });
193
+
194
+ test('returns this for method chaining', () => {
195
+ const h = new Harmonic(1, 1, 0);
196
+ const result = h.setPhase(Math.PI);
197
+ expect(result).toBe(h);
198
+ expect(h.phase).toBe(Math.PI);
199
+ });
200
+ });
201
+
202
+ describe('transpose', () => {
203
+ test('multiplies frequency by ratio (Fraction)', () => {
204
+ const h = new Harmonic(1, 1);
205
+ h.transpose(new Fraction(3, 2));
206
+ expect(h.frequencyStr).toBe('3/2');
207
+ });
208
+
209
+ test('multiplies frequency by ratio (number)', () => {
210
+ const h = new Harmonic(2, 1);
211
+ h.transpose(1.5);
212
+ expect(h.frequencyNum).toBe(3);
213
+ });
214
+
215
+ test('multiplies frequency by ratio (string)', () => {
216
+ const h = new Harmonic('4/3', 1);
217
+ h.transpose('3/2');
218
+ expect(h.frequencyStr).toBe('2');
219
+ });
220
+
221
+ test('preserves amplitude and phase', () => {
222
+ const h = new Harmonic(1, 0.5, Math.PI / 2);
223
+ h.transpose(2);
224
+ expect(h.amplitude).toBe(0.5);
225
+ expect(h.phase).toBe(Math.PI / 2);
226
+ });
227
+
228
+ test('chains multiple transpositions correctly', () => {
229
+ const h = new Harmonic(1, 1);
230
+ h.transpose('3/2');
231
+ h.transpose('4/3');
232
+ expect(h.frequencyStr).toBe('2');
233
+ });
234
+
235
+ test('returns this for method chaining', () => {
236
+ const h = new Harmonic(1, 1);
237
+ const result = h.transpose('3/2');
238
+ expect(result).toBe(h);
239
+ expect(h.frequencyStr).toBe('3/2');
240
+ });
241
+ });
242
+
243
+ describe('toTransposed', () => {
244
+ test('creates transposed copy without modifying original', () => {
245
+ const h1 = new Harmonic(1, 1);
246
+ const h2 = h1.toTransposed('3/2');
247
+
248
+ expect(h1.frequencyStr).toBe('1');
249
+ expect(h2.frequencyStr).toBe('3/2');
250
+ expect(h2).not.toBe(h1);
251
+ });
252
+
253
+ test('preserves amplitude and phase', () => {
254
+ const h1 = new Harmonic(1, 0.5, Math.PI);
255
+ const h2 = h1.toTransposed(2);
256
+
257
+ expect(h2.amplitude).toBe(0.5);
258
+ expect(h2.phase).toBe(Math.PI);
259
+ });
260
+
261
+ test('toTransposed is independent of original', () => {
262
+ const h1 = new Harmonic(1, 0.5);
263
+ const h2 = h1.toTransposed('3/2');
264
+
265
+ h2.setAmplitude(0.8);
266
+ h2.transpose(2);
267
+ h2.setPhase(Math.PI);
268
+
269
+ expect(h1.amplitude).toBe(0.5);
270
+ expect(h1.frequencyNum).toBe(1);
271
+ expect(h1.phase).toBe(0);
272
+ });
273
+ });
274
+
275
+ describe('scale', () => {
276
+ test('multiplies amplitude by factor', () => {
277
+ const h = new Harmonic(1, 0.8);
278
+ h.scale(0.5);
279
+ expect(h.amplitude).toBe(0.4);
280
+ });
281
+
282
+ test('clamps to 0 if result is negative', () => {
283
+ const h = new Harmonic(1, 0.5);
284
+ h.scale(-2);
285
+ expect(h.amplitude).toBe(0);
286
+ });
287
+
288
+ test('clamps to 1 if result > 1', () => {
289
+ const h = new Harmonic(1, 0.8);
290
+ h.scale(2);
291
+ expect(h.amplitude).toBe(1);
292
+ });
293
+
294
+ test('allows scaling to exactly 0', () => {
295
+ const h = new Harmonic(1, 1);
296
+ h.scale(0);
297
+ expect(h.amplitude).toBe(0);
298
+ });
299
+
300
+ test('preserves frequency and phase', () => {
301
+ const h = new Harmonic('3/2', 0.5, Math.PI);
302
+ h.scale(0.5);
303
+ expect(h.frequencyStr).toBe('3/2');
304
+ expect(h.phase).toBe(Math.PI);
305
+ });
306
+
307
+ test('returns this for method chaining', () => {
308
+ const h = new Harmonic(1, 0.8);
309
+ const result = h.scale(0.5);
310
+ expect(result).toBe(h);
311
+ expect(h.amplitude).toBe(0.4);
312
+ });
313
+ });
314
+
315
+ describe('frequency getter', () => {
316
+ test('returns Fraction object', () => {
317
+ const h = new Harmonic('3/2', 1);
318
+ const fraction = h.frequency;
319
+ expect(fraction.toFraction()).toBe('3/2');
320
+ expect(fraction.valueOf()).toBe(1.5);
321
+ });
322
+
323
+ test('returns Fraction for whole number', () => {
324
+ const h = new Harmonic(2, 1);
325
+ const fraction = h.frequency;
326
+ expect(fraction.toFraction()).toBe('2');
327
+ expect(fraction.valueOf()).toBe(2);
328
+ });
329
+
330
+ test('handles complex fractions', () => {
331
+ const h = new Harmonic('7/4', 1);
332
+ const fraction = h.frequency;
333
+ expect(fraction.toFraction()).toBe('7/4');
334
+ expect(fraction.valueOf()).toBe(1.75);
335
+ });
336
+ });
337
+
338
+ describe('frequencyNum getter', () => {
339
+ test('returns numeric value', () => {
340
+ const h = new Harmonic('3/2', 1);
341
+ expect(h.frequencyNum).toBe(1.5);
342
+ });
343
+
344
+ test('returns integer for whole numbers', () => {
345
+ const h = new Harmonic(2, 1);
346
+ expect(h.frequencyNum).toBe(2);
347
+ });
348
+
349
+ test('handles decimal input', () => {
350
+ const h = new Harmonic(1.5, 1);
351
+ expect(h.frequencyNum).toBe(1.5);
352
+ });
353
+ });
354
+
355
+ describe('frequencyStr getter', () => {
356
+ test('returns simplified fraction string', () => {
357
+ const h = new Harmonic('6/4', 1);
358
+ expect(h.frequencyStr).toBe('3/2');
359
+ });
360
+
361
+ test('returns integer for whole numbers', () => {
362
+ const h = new Harmonic(2, 1);
363
+ expect(h.frequencyStr).toBe('2');
364
+ });
365
+
366
+ test('handles decimal input', () => {
367
+ const h = new Harmonic(1.5, 1);
368
+ expect(h.frequencyStr).toBe('3/2');
369
+ });
370
+ });
371
+
372
+ describe('clone', () => {
373
+ test('creates independent copy', () => {
374
+ const h1 = new Harmonic('3/2', 0.5, Math.PI / 2);
375
+ const h2 = h1.clone();
376
+
377
+ expect(h2.frequencyStr).toBe(h1.frequencyStr);
378
+ expect(h2.amplitude).toBe(h1.amplitude);
379
+ expect(h2.phase).toBe(h1.phase);
380
+ });
381
+
382
+ test('clone is independent of original', () => {
383
+ const h1 = new Harmonic(1, 0.5);
384
+ const h2 = h1.clone();
385
+
386
+ h2.setAmplitude(0.8);
387
+ h2.transpose(2);
388
+ h2.setPhase(Math.PI);
389
+
390
+ expect(h1.amplitude).toBe(0.5);
391
+ expect(h1.frequencyNum).toBe(1);
392
+ expect(h1.phase).toBe(0);
393
+ });
394
+ });
395
+
396
+ describe('compareFrequency', () => {
397
+ test('returns negative when this < other', () => {
398
+ const h1 = new Harmonic(1, 1);
399
+ const h2 = new Harmonic(2, 1);
400
+ expect(h1.compareFrequency(h2)).toBeLessThan(0);
401
+ });
402
+
403
+ test('returns positive when this > other', () => {
404
+ const h1 = new Harmonic(2, 1);
405
+ const h2 = new Harmonic(1, 1);
406
+ expect(h1.compareFrequency(h2)).toBeGreaterThan(0);
407
+ });
408
+
409
+ test('returns 0 when frequencies are equal', () => {
410
+ const h1 = new Harmonic('3/2', 1);
411
+ const h2 = new Harmonic(1.5, 1);
412
+ expect(h1.compareFrequency(h2)).toBe(0);
413
+ });
414
+
415
+ test('handles fraction comparison correctly', () => {
416
+ const h1 = new Harmonic('5/4', 1);
417
+ const h2 = new Harmonic('4/3', 1);
418
+ expect(h1.compareFrequency(h2)).toBeLessThan(0); // 5/4 < 4/3
419
+ });
420
+ });
421
+
422
+ describe('toJSON', () => {
423
+ test('serializes harmonic to HarmonicData object', () => {
424
+ const h = new Harmonic('3/2', 0.5, Math.PI);
425
+ const data = h.toJSON();
426
+
427
+ expect(data.frequency).toEqual({ n: 3, d: 2 });
428
+ expect(data.amplitude).toBe(0.5);
429
+ expect(data.phase).toBe(Math.PI);
430
+ });
431
+
432
+ test('serializes whole number frequency', () => {
433
+ const h = new Harmonic(2, 0.8, 0);
434
+ const data = h.toJSON();
435
+
436
+ expect(data.frequency).toEqual({ n: 2, d: 1 });
437
+ expect(data.amplitude).toBe(0.8);
438
+ expect(data.phase).toBe(0);
439
+ });
440
+
441
+ test('can be used to recreate harmonic', () => {
442
+ const h1 = new Harmonic('5/4', 0.7, 1.5);
443
+ const data = h1.toJSON();
444
+ const h2 = new Harmonic(data);
445
+
446
+ expect(h2.frequencyStr).toBe(h1.frequencyStr);
447
+ expect(h2.amplitude).toBe(h1.amplitude);
448
+ expect(h2.phase).toBe(h1.phase);
449
+ });
450
+ });
451
+
452
+ describe('edge cases', () => {
453
+ test('handles very small fractions', () => {
454
+ const h = new Harmonic('1/1000', 0.5);
455
+ expect(h.frequencyNum).toBe(0.001);
456
+ });
457
+
458
+ test('handles very large fractions', () => {
459
+ const h = new Harmonic('100000/1', 0.5);
460
+ expect(h.frequencyNum).toBe(100000);
461
+ });
462
+
463
+ test('handles irrational numbers (approximated)', () => {
464
+ const h = new Harmonic(Math.PI, 0.5);
465
+ expect(h.frequencyNum).toBeCloseTo(Math.PI, 10);
466
+ });
467
+
468
+ test('handles amplitude at boundaries', () => {
469
+ const h1 = new Harmonic(1, 0);
470
+ const h2 = new Harmonic(1, 1);
471
+ expect(h1.amplitude).toBe(0);
472
+ expect(h2.amplitude).toBe(1);
473
+ });
474
+
475
+ test('handles phase near boundaries', () => {
476
+ const phase = 2 * Math.PI - 0.000001;
477
+ const h = new Harmonic(1, 1, phase);
478
+ expect(h.phase).toBeCloseTo(phase, 6);
479
+ });
480
+ });
481
+
482
+ describe('method chaining', () => {
483
+ test('can chain multiple setter methods', () => {
484
+ const h = new Harmonic(1, 1)
485
+ .setFrequency('3/2')
486
+ .setAmplitude(0.8)
487
+ .setPhase(Math.PI / 2);
488
+
489
+ expect(h.frequencyStr).toBe('3/2');
490
+ expect(h.amplitude).toBe(0.8);
491
+ expect(h.phase).toBe(Math.PI / 2);
492
+ });
493
+
494
+ test('can chain transpose and scale', () => {
495
+ const h = new Harmonic(1, 0.5)
496
+ .transpose('3/2')
497
+ .scale(0.8);
498
+
499
+ expect(h.frequencyStr).toBe('3/2');
500
+ expect(h.amplitude).toBe(0.4);
501
+ });
502
+
503
+ test('can chain all mutator methods', () => {
504
+ const h = new Harmonic(1, 1)
505
+ .setFrequency('5/4')
506
+ .setAmplitude(0.7)
507
+ .setPhase(Math.PI)
508
+ .transpose('4/3')
509
+ .scale(0.9);
510
+
511
+ expect(h.frequencyStr).toBe('5/3');
512
+ expect(h.amplitude).toBeCloseTo(0.63);
513
+ expect(h.phase).toBe(Math.PI);
514
+ });
515
+ });
516
+ });