-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstrument-routing.test.ts
More file actions
332 lines (287 loc) · 11.3 KB
/
instrument-routing.test.ts
File metadata and controls
332 lines (287 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
/**
* Comprehensive Instrument Routing Tests
*
* Verifies that ALL instruments are correctly routed to their playback methods.
* This ensures no instrument is "silently broken" - each must route correctly.
*
* Instruments are categorized into 4 engines:
* 1. Procedural samples (kick, snare, etc.) - playSample()
* 2. Web Audio synths (synth:*) - playSynthNote()
* 3. Tone.js synths (tone:*) - playToneSynth()
* 4. Advanced synths (advanced:*) - playAdvancedSynth()
* 5. Sampled instruments (piano) - sampledInstrumentRegistry
*/
import { describe, it, expect } from 'vitest';
import { SAMPLE_CATEGORIES } from '../types';
import {
SAMPLE_NAMES,
SYNTH_CATEGORIES,
SYNTH_NAMES,
TONE_SYNTH_CATEGORIES,
TONE_SYNTH_NAMES,
ADVANCED_SYNTH_CATEGORIES,
ADVANCED_SYNTH_NAMES,
} from '../components/sample-constants';
import { SYNTH_PRESETS } from './synth';
import { ADVANCED_SYNTH_PRESETS } from './advancedSynth';
import { isSampledInstrument, SAMPLED_INSTRUMENTS } from './sampled-instrument';
/**
* Helper to determine which engine handles a sampleId
*/
function getInstrumentEngine(sampleId: string): 'sample' | 'synth' | 'tone' | 'advanced' | 'sampled' {
if (sampleId.startsWith('synth:')) return 'synth';
if (sampleId.startsWith('tone:')) return 'tone';
if (sampleId.startsWith('advanced:')) return 'advanced';
if (isSampledInstrument(sampleId)) return 'sampled';
return 'sample';
}
/**
* Get preset ID from sampleId (strips prefix)
*/
function getPresetId(sampleId: string): string {
if (sampleId.startsWith('synth:')) return sampleId.replace('synth:', '');
if (sampleId.startsWith('tone:')) return sampleId.replace('tone:', '');
if (sampleId.startsWith('advanced:')) return sampleId.replace('advanced:', '');
return sampleId;
}
// Collect ALL instruments from sample-constants
const ALL_PROCEDURAL_SAMPLES = Object.values(SAMPLE_CATEGORIES).flat();
const ALL_SYNTH_PRESETS = Object.values(SYNTH_CATEGORIES).flat();
const ALL_TONE_SYNTHS = Object.values(TONE_SYNTH_CATEGORIES).flat();
const ALL_ADVANCED_SYNTHS = Object.values(ADVANCED_SYNTH_CATEGORIES).flat();
describe('Comprehensive Instrument Routing', () => {
describe('Procedural Samples (22 samples)', () => {
it('should have 22 procedural samples', () => {
expect(ALL_PROCEDURAL_SAMPLES.length).toBe(22);
});
it('all procedural samples should route to sample engine', () => {
for (const sampleId of ALL_PROCEDURAL_SAMPLES) {
const engine = getInstrumentEngine(sampleId);
expect(engine).toBe('sample');
}
});
it('all procedural samples should have display names', () => {
for (const sampleId of ALL_PROCEDURAL_SAMPLES) {
expect(SAMPLE_NAMES[sampleId]).toBeDefined();
expect(typeof SAMPLE_NAMES[sampleId]).toBe('string');
expect(SAMPLE_NAMES[sampleId].length).toBeGreaterThan(0);
}
});
it('lists all procedural samples for verification', () => {
const samples = ALL_PROCEDURAL_SAMPLES.map(id => ({
id,
name: SAMPLE_NAMES[id],
engine: getInstrumentEngine(id),
}));
// This logs for manual verification and serves as documentation
expect(samples.length).toBe(22);
});
});
describe('Web Audio Synths (32 presets)', () => {
it('should have 32 synth presets in SYNTH_PRESETS', () => {
expect(Object.keys(SYNTH_PRESETS).length).toBe(32);
});
it('all synth sampleIds should route to synth engine', () => {
for (const sampleId of ALL_SYNTH_PRESETS) {
const engine = getInstrumentEngine(sampleId);
expect(engine).toBe('synth');
}
});
it('all synth sampleIds should have valid presets', () => {
for (const sampleId of ALL_SYNTH_PRESETS) {
const presetId = getPresetId(sampleId);
expect(SYNTH_PRESETS[presetId]).toBeDefined();
}
});
it('all synth sampleIds should have display names', () => {
for (const sampleId of ALL_SYNTH_PRESETS) {
expect(SYNTH_NAMES[sampleId]).toBeDefined();
expect(typeof SYNTH_NAMES[sampleId]).toBe('string');
}
});
it('SYNTH_PRESETS and SYNTH_CATEGORIES should be in sync', () => {
const categoryPresets = ALL_SYNTH_PRESETS.map(id => getPresetId(id));
const presetKeys = Object.keys(SYNTH_PRESETS);
// All presets in categories should exist in SYNTH_PRESETS
for (const preset of categoryPresets) {
expect(presetKeys).toContain(preset);
}
});
});
describe('Tone.js Synths (11 presets)', () => {
it('should have 11 Tone.js synth presets', () => {
expect(ALL_TONE_SYNTHS.length).toBe(11);
});
it('all tone sampleIds should route to tone engine', () => {
for (const sampleId of ALL_TONE_SYNTHS) {
const engine = getInstrumentEngine(sampleId);
expect(engine).toBe('tone');
}
});
it('all tone sampleIds should have display names', () => {
for (const sampleId of ALL_TONE_SYNTHS) {
expect(TONE_SYNTH_NAMES[sampleId]).toBeDefined();
expect(typeof TONE_SYNTH_NAMES[sampleId]).toBe('string');
}
});
it('lists all Tone.js synths for verification', () => {
const synths = ALL_TONE_SYNTHS.map(id => ({
id,
name: TONE_SYNTH_NAMES[id],
engine: getInstrumentEngine(id),
}));
expect(synths.length).toBe(11);
});
});
describe('Advanced Synths (8 presets)', () => {
it('should have 8 advanced synth presets', () => {
expect(ALL_ADVANCED_SYNTHS.length).toBe(8);
});
it('should have 8 presets in ADVANCED_SYNTH_PRESETS', () => {
expect(Object.keys(ADVANCED_SYNTH_PRESETS).length).toBe(8);
});
it('all advanced sampleIds should route to advanced engine', () => {
for (const sampleId of ALL_ADVANCED_SYNTHS) {
const engine = getInstrumentEngine(sampleId);
expect(engine).toBe('advanced');
}
});
it('all advanced sampleIds should have valid presets', () => {
for (const sampleId of ALL_ADVANCED_SYNTHS) {
const presetId = getPresetId(sampleId);
expect(ADVANCED_SYNTH_PRESETS[presetId]).toBeDefined();
}
});
it('all advanced sampleIds should have display names', () => {
for (const sampleId of ALL_ADVANCED_SYNTHS) {
expect(ADVANCED_SYNTH_NAMES[sampleId]).toBeDefined();
expect(typeof ADVANCED_SYNTH_NAMES[sampleId]).toBe('string');
}
});
it('ADVANCED_SYNTH_PRESETS and ADVANCED_SYNTH_CATEGORIES should be in sync', () => {
const categoryPresets = ALL_ADVANCED_SYNTHS.map(id => getPresetId(id));
const presetKeys = Object.keys(ADVANCED_SYNTH_PRESETS);
// All presets in categories should exist in ADVANCED_SYNTH_PRESETS
for (const preset of categoryPresets) {
expect(presetKeys).toContain(preset);
}
// All ADVANCED_SYNTH_PRESETS should be in categories
for (const preset of presetKeys) {
expect(categoryPresets).toContain(preset);
}
});
it('lists all Advanced synths with their preset details', () => {
const synths = ALL_ADVANCED_SYNTHS.map(id => {
const presetId = getPresetId(id);
const preset = ADVANCED_SYNTH_PRESETS[presetId];
return {
id,
presetId,
name: ADVANCED_SYNTH_NAMES[id],
presetName: preset?.name,
engine: getInstrumentEngine(id),
hasOsc1: preset?.oscillator1?.level > 0,
hasOsc2: preset?.oscillator2?.level > 0,
ampSustain: preset?.amplitudeEnvelope?.sustain,
};
});
// Verify each has at least one active oscillator
for (const synth of synths) {
expect(synth.hasOsc1 || synth.hasOsc2).toBe(true);
}
// Verify each has positive sustain (so we hear the note)
for (const synth of synths) {
expect(synth.ampSustain).toBeGreaterThan(0);
}
});
});
describe('Sampled Instruments (1 instrument)', () => {
it('should have piano as sampled instrument', () => {
expect(SAMPLED_INSTRUMENTS).toContain('piano');
});
it('should have exactly 1 sampled instrument currently', () => {
expect(SAMPLED_INSTRUMENTS.length).toBe(1);
});
it('piano should route to sampled engine', () => {
expect(getInstrumentEngine('piano')).toBe('sampled');
});
it('isSampledInstrument should identify piano', () => {
expect(isSampledInstrument('piano')).toBe(true);
});
it('isSampledInstrument should reject synth presets', () => {
expect(isSampledInstrument('synth:bass')).toBe(false);
expect(isSampledInstrument('tone:fm-epiano')).toBe(false);
expect(isSampledInstrument('advanced:supersaw')).toBe(false);
});
});
describe('Total Instrument Count', () => {
it('should have 74 total instruments (22 + 32 + 11 + 8 + 1)', () => {
const total =
ALL_PROCEDURAL_SAMPLES.length +
ALL_SYNTH_PRESETS.length +
ALL_TONE_SYNTHS.length +
ALL_ADVANCED_SYNTHS.length +
SAMPLED_INSTRUMENTS.length;
expect(total).toBe(74);
});
});
describe('No Missing Instruments', () => {
it('every SYNTH_NAMES entry should have a valid preset', () => {
for (const [sampleId] of Object.entries(SYNTH_NAMES)) {
const presetId = getPresetId(sampleId);
expect(SYNTH_PRESETS[presetId]).toBeDefined();
}
});
it('every ADVANCED_SYNTH_NAMES entry should have a valid preset', () => {
for (const [sampleId] of Object.entries(ADVANCED_SYNTH_NAMES)) {
const presetId = getPresetId(sampleId);
expect(ADVANCED_SYNTH_PRESETS[presetId]).toBeDefined();
}
});
});
describe('CRITICAL: Advanced Synth Preset Validation', () => {
// These tests verify the presets themselves are valid
it('all advanced presets should have amplitude envelope with attack > 0', () => {
for (const [_id, preset] of Object.entries(ADVANCED_SYNTH_PRESETS)) {
expect(preset.amplitudeEnvelope.attack).toBeGreaterThan(0);
}
});
it('all advanced presets should have amplitude envelope with sustain > 0', () => {
for (const [_id, preset] of Object.entries(ADVANCED_SYNTH_PRESETS)) {
expect(preset.amplitudeEnvelope.sustain).toBeGreaterThan(0);
}
});
it('all advanced presets should have at least one active oscillator', () => {
for (const [_id, preset] of Object.entries(ADVANCED_SYNTH_PRESETS)) {
const totalLevel = preset.oscillator1.level + preset.oscillator2.level;
expect(totalLevel).toBeGreaterThan(0);
}
});
it('all advanced presets should have filter frequency above 20Hz', () => {
for (const [_id, preset] of Object.entries(ADVANCED_SYNTH_PRESETS)) {
expect(preset.filter.frequency).toBeGreaterThanOrEqual(20);
}
});
});
});
describe('Instrument Prefix Uniqueness', () => {
it('no instrument should match multiple prefixes', () => {
const allInstruments = [
...ALL_PROCEDURAL_SAMPLES,
...ALL_SYNTH_PRESETS,
...ALL_TONE_SYNTHS,
...ALL_ADVANCED_SYNTHS,
...SAMPLED_INSTRUMENTS,
];
for (const id of allInstruments) {
const matches = [
id.startsWith('synth:'),
id.startsWith('tone:'),
id.startsWith('advanced:'),
isSampledInstrument(id),
].filter(Boolean);
// Should match at most 1 prefix (or 0 for procedural samples)
expect(matches.length).toBeLessThanOrEqual(1);
}
});
});