Skip to content

Commit 2b0d38d

Browse files
authored
Merge pull request #17 from adewale/claude/display-roadmap-Osuh7
Phase 22-23: Complete Synthesis Engine + Percussion Expansion
2 parents cc24236 + 7b964e8 commit 2b0d38d

File tree

13 files changed

+1803
-263
lines changed

13 files changed

+1803
-263
lines changed

app/scripts/session-api.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ interface ValidationError {
5757
}
5858

5959
const VALID_SAMPLE_IDS = [
60-
// Drums
60+
// Core kit
6161
'kick', 'snare', 'hihat', 'clap', 'tom', 'rim', 'cowbell', 'openhat',
62+
// World/Latin percussion (Phase 23)
63+
'shaker', 'conga', 'tambourine', 'clave', 'cabasa', 'woodblock',
6264
// Bass/Synth samples
63-
'bass', 'sub',
65+
'bass', 'subbass',
6466
// Melodic samples
6567
'lead', 'pluck', 'chord', 'pad',
6668
// FX

app/scripts/sessions/extended-afrobeat.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@
131131
{
132132
"id": "keys-afro",
133133
"name": "Keys",
134-
"sampleId": "synth:piano",
134+
"sampleId": "sampled:piano",
135135
"steps": [
136136
false, false, false, true, false, false, false, false, false, true, false, false,
137137
false, false, false, true, false, false, false, false, false, true, false, true,
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
{
2+
"name": "Latin Percussion Showcase",
3+
"description": "Demonstrates all 6 new Phase 23 percussion instruments: shaker, conga, tambourine, clave, cabasa, and woodblock. Features authentic Latin rhythms with polyrhythmic interplay.",
4+
"tracks": [
5+
{
6+
"id": "kick-foundation",
7+
"name": "Kick",
8+
"sampleId": "kick",
9+
"steps": [true, false, false, false, false, false, true, false, false, false, false, false, true, false, false, false],
10+
"parameterLocks": [{"volume": 1.0}, null, null, null, null, null, {"volume": 0.8}, null, null, null, null, null, {"volume": 0.9}, null, null, null],
11+
"volume": 0.85,
12+
"muted": false,
13+
"playbackMode": "oneshot",
14+
"transpose": 0,
15+
"stepCount": 16
16+
},
17+
{
18+
"id": "clave-pattern",
19+
"name": "Clave",
20+
"sampleId": "clave",
21+
"steps": [true, false, false, true, false, false, true, false, false, false, true, false, true, false, false, false],
22+
"parameterLocks": [{"volume": 0.9}, null, null, {"volume": 0.7}, null, null, {"volume": 0.8}, null, null, null, {"volume": 0.7}, null, {"volume": 0.9}, null, null, null],
23+
"volume": 0.6,
24+
"muted": false,
25+
"playbackMode": "oneshot",
26+
"transpose": 0,
27+
"stepCount": 16
28+
},
29+
{
30+
"id": "conga-high",
31+
"name": "Conga Hi",
32+
"sampleId": "conga",
33+
"steps": [false, false, true, false, true, false, false, true, false, false, true, false, true, false, false, true],
34+
"parameterLocks": [null, null, {"pitch": 7, "volume": 0.8}, null, {"pitch": 5, "volume": 0.7}, null, null, {"pitch": 7, "volume": 0.6}, null, null, {"pitch": 5, "volume": 0.8}, null, {"pitch": 7, "volume": 0.7}, null, null, {"pitch": 5, "volume": 0.6}],
35+
"volume": 0.65,
36+
"muted": false,
37+
"playbackMode": "oneshot",
38+
"transpose": 0,
39+
"stepCount": 16
40+
},
41+
{
42+
"id": "conga-low",
43+
"name": "Conga Lo",
44+
"sampleId": "conga",
45+
"steps": [true, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false],
46+
"parameterLocks": [{"pitch": -5, "volume": 0.9}, null, null, null, null, {"pitch": -7, "volume": 0.7}, null, null, {"pitch": -5, "volume": 0.8}, null, null, null, null, {"pitch": -7, "volume": 0.7}, null, null],
47+
"volume": 0.7,
48+
"muted": false,
49+
"playbackMode": "oneshot",
50+
"transpose": 0,
51+
"stepCount": 16
52+
},
53+
{
54+
"id": "shaker-groove",
55+
"name": "Shaker",
56+
"sampleId": "shaker",
57+
"steps": [true, false, true, true, false, true, true, false, true, true, false, true, true, false, true, true],
58+
"parameterLocks": [{"volume": 0.9}, null, {"volume": 0.5}, {"volume": 0.7}, null, {"volume": 0.5}, {"volume": 0.8}, null, {"volume": 0.5}, {"volume": 0.7}, null, {"volume": 0.5}, {"volume": 0.9}, null, {"volume": 0.5}, {"volume": 0.7}],
59+
"volume": 0.45,
60+
"muted": false,
61+
"playbackMode": "oneshot",
62+
"transpose": 0,
63+
"stepCount": 16
64+
},
65+
{
66+
"id": "tambourine-accent",
67+
"name": "Tamb",
68+
"sampleId": "tambourine",
69+
"steps": [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false],
70+
"parameterLocks": [null, null, null, null, {"volume": 1.0}, null, null, null, null, null, null, null, {"volume": 0.9}, null, null, null],
71+
"volume": 0.55,
72+
"muted": false,
73+
"playbackMode": "oneshot",
74+
"transpose": 0,
75+
"stepCount": 16
76+
},
77+
{
78+
"id": "cabasa-texture",
79+
"name": "Cabasa",
80+
"sampleId": "cabasa",
81+
"steps": [false, true, false, true, false, true, false, true, false, true, false, true, false, true, false, true],
82+
"parameterLocks": [null, {"volume": 0.6}, null, {"volume": 0.4}, null, {"volume": 0.5}, null, {"volume": 0.4}, null, {"volume": 0.6}, null, {"volume": 0.4}, null, {"volume": 0.5}, null, {"volume": 0.4}],
83+
"volume": 0.4,
84+
"muted": false,
85+
"playbackMode": "oneshot",
86+
"transpose": 0,
87+
"stepCount": 16
88+
},
89+
{
90+
"id": "woodblock-accent",
91+
"name": "Wood",
92+
"sampleId": "woodblock",
93+
"steps": [false, false, false, false, false, false, false, false, true, false, false, false, false, false, true, false],
94+
"parameterLocks": [null, null, null, null, null, null, null, null, {"volume": 0.8, "pitch": 3}, null, null, null, null, null, {"volume": 0.7, "pitch": 0}, null],
95+
"volume": 0.5,
96+
"muted": false,
97+
"playbackMode": "oneshot",
98+
"transpose": 0,
99+
"stepCount": 16
100+
},
101+
{
102+
"id": "bass-montuno",
103+
"name": "Bass",
104+
"sampleId": "synth:bass",
105+
"steps": [true, false, false, true, false, false, true, false, false, true, false, false, true, false, true, false],
106+
"parameterLocks": [{"pitch": 0}, null, null, {"pitch": 0}, null, null, {"pitch": 3}, null, null, {"pitch": 5}, null, null, {"pitch": 3}, null, {"pitch": 0}, null],
107+
"volume": 0.7,
108+
"muted": false,
109+
"playbackMode": "oneshot",
110+
"transpose": -12,
111+
"stepCount": 16
112+
}
113+
],
114+
"tempo": 105,
115+
"swing": 12
116+
}

app/scripts/sessions/progressive-house-build.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@
125125
{
126126
"id": "piano-chords",
127127
"name": "Piano Chords",
128-
"sampleId": "synth:piano",
128+
"sampleId": "sampled:piano",
129129
"steps": [
130130
false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false,
131131
false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false,

app/src/audio/instrument-routing.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ const ALL_TONE_SYNTHS = Object.values(TONE_SYNTH_CATEGORIES).flat();
5555
const ALL_ADVANCED_SYNTHS = Object.values(ADVANCED_SYNTH_CATEGORIES).flat();
5656

5757
describe('Comprehensive Instrument Routing', () => {
58-
describe('Procedural Samples (16 samples)', () => {
59-
it('should have 16 procedural samples', () => {
60-
expect(ALL_PROCEDURAL_SAMPLES.length).toBe(16);
58+
describe('Procedural Samples (22 samples)', () => {
59+
it('should have 22 procedural samples', () => {
60+
expect(ALL_PROCEDURAL_SAMPLES.length).toBe(22);
6161
});
6262

6363
it('all procedural samples should route to sample engine', () => {
@@ -82,7 +82,7 @@ describe('Comprehensive Instrument Routing', () => {
8282
engine: getInstrumentEngine(id),
8383
}));
8484
// This logs for manual verification and serves as documentation
85-
expect(samples.length).toBe(16);
85+
expect(samples.length).toBe(22);
8686
});
8787
});
8888

@@ -250,15 +250,15 @@ describe('Comprehensive Instrument Routing', () => {
250250
});
251251

252252
describe('Total Instrument Count', () => {
253-
it('should have 68 total instruments (16 + 32 + 11 + 8 + 1)', () => {
253+
it('should have 74 total instruments (22 + 32 + 11 + 8 + 1)', () => {
254254
const total =
255255
ALL_PROCEDURAL_SAMPLES.length +
256256
ALL_SYNTH_PRESETS.length +
257257
ALL_TONE_SYNTHS.length +
258258
ALL_ADVANCED_SYNTHS.length +
259259
SAMPLED_INSTRUMENTS.length;
260260

261-
expect(total).toBe(68);
261+
expect(total).toBe(74);
262262
});
263263
});
264264

app/src/audio/samples.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ describe('Sample ID parity', () => {
5757
});
5858

5959
it('should have expected sample count', () => {
60-
// 8 drums + 2 bass + 4 synth + 2 fx = 16 samples
61-
expect(allSampleIds.length).toBe(16);
62-
expect(SAMPLE_CATEGORIES.drums.length).toBe(8);
60+
// 14 drums + 2 bass + 4 synth + 2 fx = 22 samples
61+
expect(allSampleIds.length).toBe(22);
62+
expect(SAMPLE_CATEGORIES.drums.length).toBe(14);
6363
expect(SAMPLE_CATEGORIES.bass.length).toBe(2);
6464
expect(SAMPLE_CATEGORIES.synth.length).toBe(4);
6565
expect(SAMPLE_CATEGORIES.fx.length).toBe(2);

app/src/audio/samples.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,49 @@ export async function createSynthesizedSamples(
6565
url: '',
6666
});
6767

68+
// === WORLD/LATIN PERCUSSION ===
69+
samples.set('shaker', {
70+
id: 'shaker',
71+
name: 'Shaker',
72+
buffer: await createShaker(audioContext),
73+
url: '',
74+
});
75+
76+
samples.set('conga', {
77+
id: 'conga',
78+
name: 'Conga',
79+
buffer: await createConga(audioContext),
80+
url: '',
81+
});
82+
83+
samples.set('tambourine', {
84+
id: 'tambourine',
85+
name: 'Tambourine',
86+
buffer: await createTambourine(audioContext),
87+
url: '',
88+
});
89+
90+
samples.set('clave', {
91+
id: 'clave',
92+
name: 'Clave',
93+
buffer: await createClave(audioContext),
94+
url: '',
95+
});
96+
97+
samples.set('cabasa', {
98+
id: 'cabasa',
99+
name: 'Cabasa',
100+
buffer: await createCabasa(audioContext),
101+
url: '',
102+
});
103+
104+
samples.set('woodblock', {
105+
id: 'woodblock',
106+
name: 'Woodblock',
107+
buffer: await createWoodblock(audioContext),
108+
url: '',
109+
});
110+
68111
// === BASS ===
69112
samples.set('bass', {
70113
id: 'bass',
@@ -284,6 +327,141 @@ async function createOpenHat(ctx: AudioContext): Promise<AudioBuffer> {
284327
return buffer;
285328
}
286329

330+
// === World/Latin Percussion ===
331+
332+
async function createShaker(ctx: AudioContext): Promise<AudioBuffer> {
333+
const duration = 0.15;
334+
const sampleRate = ctx.sampleRate;
335+
const length = Math.floor(duration * sampleRate);
336+
const buffer = ctx.createBuffer(1, length, sampleRate);
337+
const data = buffer.getChannelData(0);
338+
339+
for (let i = 0; i < length; i++) {
340+
const t = i / sampleRate;
341+
// High-frequency noise with fast attack/decay
342+
const noise = Math.random() * 2 - 1;
343+
const envelope = Math.exp(-t * 25) * (1 - Math.exp(-t * 500));
344+
// Simple highpass approximation
345+
const filtered = noise * 0.7 + (Math.random() * 0.6 - 0.3);
346+
data[i] = filtered * envelope * 0.6;
347+
}
348+
349+
return buffer;
350+
}
351+
352+
async function createConga(ctx: AudioContext): Promise<AudioBuffer> {
353+
const duration = 0.4;
354+
const sampleRate = ctx.sampleRate;
355+
const length = Math.floor(duration * sampleRate);
356+
const buffer = ctx.createBuffer(1, length, sampleRate);
357+
const data = buffer.getChannelData(0);
358+
359+
for (let i = 0; i < length; i++) {
360+
const t = i / sampleRate;
361+
// Pitched membrane sound with slight pitch drop
362+
const freq = 200 * Math.exp(-t * 3);
363+
const fundamental = Math.sin(2 * Math.PI * freq * t);
364+
// Add harmonics for wood/skin character
365+
const harmonic2 = Math.sin(2 * Math.PI * freq * 2.3 * t) * 0.3;
366+
const harmonic3 = Math.sin(2 * Math.PI * freq * 3.1 * t) * 0.15;
367+
// Attack transient (slap)
368+
const slap = (Math.random() * 2 - 1) * Math.exp(-t * 100) * 0.4;
369+
// Envelope
370+
const envelope = Math.exp(-t * 6);
371+
data[i] = (fundamental + harmonic2 + harmonic3 + slap) * envelope * 0.7;
372+
}
373+
374+
return buffer;
375+
}
376+
377+
async function createTambourine(ctx: AudioContext): Promise<AudioBuffer> {
378+
const duration = 0.25;
379+
const sampleRate = ctx.sampleRate;
380+
const length = Math.floor(duration * sampleRate);
381+
const buffer = ctx.createBuffer(1, length, sampleRate);
382+
const data = buffer.getChannelData(0);
383+
384+
for (let i = 0; i < length; i++) {
385+
const t = i / sampleRate;
386+
// Metallic jingles (multiple inharmonic frequencies)
387+
const jingle1 = Math.sin(2 * Math.PI * 2100 * t);
388+
const jingle2 = Math.sin(2 * Math.PI * 3400 * t);
389+
const jingle3 = Math.sin(2 * Math.PI * 4800 * t);
390+
const jingle4 = Math.sin(2 * Math.PI * 6200 * t);
391+
// Noise component for stick hit
392+
const noise = (Math.random() * 2 - 1) * Math.exp(-t * 50);
393+
// Envelope with sustain for jingles
394+
const envelope = Math.exp(-t * 8);
395+
const jingles = (jingle1 + jingle2 * 0.7 + jingle3 * 0.5 + jingle4 * 0.3) * 0.15;
396+
data[i] = (jingles + noise * 0.3) * envelope;
397+
}
398+
399+
return buffer;
400+
}
401+
402+
async function createClave(ctx: AudioContext): Promise<AudioBuffer> {
403+
const duration = 0.12;
404+
const sampleRate = ctx.sampleRate;
405+
const length = Math.floor(duration * sampleRate);
406+
const buffer = ctx.createBuffer(1, length, sampleRate);
407+
const data = buffer.getChannelData(0);
408+
409+
for (let i = 0; i < length; i++) {
410+
const t = i / sampleRate;
411+
// Two-tone wooden click (like two sticks hitting)
412+
const freq1 = 2500;
413+
const freq2 = 3200;
414+
const tone1 = Math.sin(2 * Math.PI * freq1 * t);
415+
const tone2 = Math.sin(2 * Math.PI * freq2 * t) * 0.6;
416+
// Very fast decay
417+
const envelope = Math.exp(-t * 40);
418+
data[i] = (tone1 + tone2) * envelope * 0.6;
419+
}
420+
421+
return buffer;
422+
}
423+
424+
async function createCabasa(ctx: AudioContext): Promise<AudioBuffer> {
425+
const duration = 0.08;
426+
const sampleRate = ctx.sampleRate;
427+
const length = Math.floor(duration * sampleRate);
428+
const buffer = ctx.createBuffer(1, length, sampleRate);
429+
const data = buffer.getChannelData(0);
430+
431+
for (let i = 0; i < length; i++) {
432+
const t = i / sampleRate;
433+
// Very high frequency noise burst
434+
const noise = Math.random() * 2 - 1;
435+
// Very fast attack and decay
436+
const envelope = Math.exp(-t * 60) * (1 - Math.exp(-t * 2000));
437+
data[i] = noise * envelope * 0.5;
438+
}
439+
440+
return buffer;
441+
}
442+
443+
async function createWoodblock(ctx: AudioContext): Promise<AudioBuffer> {
444+
const duration = 0.15;
445+
const sampleRate = ctx.sampleRate;
446+
const length = Math.floor(duration * sampleRate);
447+
const buffer = ctx.createBuffer(1, length, sampleRate);
448+
const data = buffer.getChannelData(0);
449+
450+
for (let i = 0; i < length; i++) {
451+
const t = i / sampleRate;
452+
// Resonant filtered click
453+
const freq = 800;
454+
const fundamental = Math.sin(2 * Math.PI * freq * t);
455+
const harmonic = Math.sin(2 * Math.PI * freq * 2.7 * t) * 0.4;
456+
// Sharp attack, medium decay with resonance
457+
const envelope = Math.exp(-t * 20);
458+
const attack = Math.exp(-t * 200);
459+
data[i] = (fundamental + harmonic) * envelope * (0.7 + attack * 0.3);
460+
}
461+
462+
return buffer;
463+
}
464+
287465
// === Bass ===
288466

289467
async function createBass(ctx: AudioContext): Promise<AudioBuffer> {

0 commit comments

Comments
 (0)