Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/scripts/session-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ interface ValidationError {
}

const VALID_SAMPLE_IDS = [
// Drums
// Core kit
'kick', 'snare', 'hihat', 'clap', 'tom', 'rim', 'cowbell', 'openhat',
// World/Latin percussion (Phase 23)
'shaker', 'conga', 'tambourine', 'clave', 'cabasa', 'woodblock',
// Bass/Synth samples
'bass', 'sub',
'bass', 'subbass',
// Melodic samples
'lead', 'pluck', 'chord', 'pad',
// FX
Expand Down
2 changes: 1 addition & 1 deletion app/scripts/sessions/extended-afrobeat.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
{
"id": "keys-afro",
"name": "Keys",
"sampleId": "synth:piano",
"sampleId": "sampled:piano",
"steps": [
false, false, false, true, false, false, false, false, false, true, false, false,
false, false, false, true, false, false, false, false, false, true, false, true,
Expand Down
116 changes: 116 additions & 0 deletions app/scripts/sessions/latin-percussion-showcase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{
"name": "Latin Percussion Showcase",
"description": "Demonstrates all 6 new Phase 23 percussion instruments: shaker, conga, tambourine, clave, cabasa, and woodblock. Features authentic Latin rhythms with polyrhythmic interplay.",
"tracks": [
{
"id": "kick-foundation",
"name": "Kick",
"sampleId": "kick",
"steps": [true, false, false, false, false, false, true, false, false, false, false, false, true, false, false, false],
"parameterLocks": [{"volume": 1.0}, null, null, null, null, null, {"volume": 0.8}, null, null, null, null, null, {"volume": 0.9}, null, null, null],
"volume": 0.85,
"muted": false,
"playbackMode": "oneshot",
"transpose": 0,
"stepCount": 16
},
{
"id": "clave-pattern",
"name": "Clave",
"sampleId": "clave",
"steps": [true, false, false, true, false, false, true, false, false, false, true, false, true, false, false, false],
"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],
"volume": 0.6,
"muted": false,
"playbackMode": "oneshot",
"transpose": 0,
"stepCount": 16
},
{
"id": "conga-high",
"name": "Conga Hi",
"sampleId": "conga",
"steps": [false, false, true, false, true, false, false, true, false, false, true, false, true, false, false, true],
"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}],
"volume": 0.65,
"muted": false,
"playbackMode": "oneshot",
"transpose": 0,
"stepCount": 16
},
{
"id": "conga-low",
"name": "Conga Lo",
"sampleId": "conga",
"steps": [true, false, false, false, false, true, false, false, true, false, false, false, false, true, false, false],
"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],
"volume": 0.7,
"muted": false,
"playbackMode": "oneshot",
"transpose": 0,
"stepCount": 16
},
{
"id": "shaker-groove",
"name": "Shaker",
"sampleId": "shaker",
"steps": [true, false, true, true, false, true, true, false, true, true, false, true, true, false, true, true],
"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}],
"volume": 0.45,
"muted": false,
"playbackMode": "oneshot",
"transpose": 0,
"stepCount": 16
},
{
"id": "tambourine-accent",
"name": "Tamb",
"sampleId": "tambourine",
"steps": [false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false],
"parameterLocks": [null, null, null, null, {"volume": 1.0}, null, null, null, null, null, null, null, {"volume": 0.9}, null, null, null],
"volume": 0.55,
"muted": false,
"playbackMode": "oneshot",
"transpose": 0,
"stepCount": 16
},
{
"id": "cabasa-texture",
"name": "Cabasa",
"sampleId": "cabasa",
"steps": [false, true, false, true, false, true, false, true, false, true, false, true, false, true, false, true],
"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}],
"volume": 0.4,
"muted": false,
"playbackMode": "oneshot",
"transpose": 0,
"stepCount": 16
},
{
"id": "woodblock-accent",
"name": "Wood",
"sampleId": "woodblock",
"steps": [false, false, false, false, false, false, false, false, true, false, false, false, false, false, true, false],
"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],
"volume": 0.5,
"muted": false,
"playbackMode": "oneshot",
"transpose": 0,
"stepCount": 16
},
{
"id": "bass-montuno",
"name": "Bass",
"sampleId": "synth:bass",
"steps": [true, false, false, true, false, false, true, false, false, true, false, false, true, false, true, false],
"parameterLocks": [{"pitch": 0}, null, null, {"pitch": 0}, null, null, {"pitch": 3}, null, null, {"pitch": 5}, null, null, {"pitch": 3}, null, {"pitch": 0}, null],
"volume": 0.7,
"muted": false,
"playbackMode": "oneshot",
"transpose": -12,
"stepCount": 16
}
],
"tempo": 105,
"swing": 12
}
2 changes: 1 addition & 1 deletion app/scripts/sessions/progressive-house-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
{
"id": "piano-chords",
"name": "Piano Chords",
"sampleId": "synth:piano",
"sampleId": "sampled:piano",
"steps": [
false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false,
false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false,
Expand Down
12 changes: 6 additions & 6 deletions app/src/audio/instrument-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ 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 (16 samples)', () => {
it('should have 16 procedural samples', () => {
expect(ALL_PROCEDURAL_SAMPLES.length).toBe(16);
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', () => {
Expand All @@ -82,7 +82,7 @@ describe('Comprehensive Instrument Routing', () => {
engine: getInstrumentEngine(id),
}));
// This logs for manual verification and serves as documentation
expect(samples.length).toBe(16);
expect(samples.length).toBe(22);
});
});

Expand Down Expand Up @@ -250,15 +250,15 @@ describe('Comprehensive Instrument Routing', () => {
});

describe('Total Instrument Count', () => {
it('should have 68 total instruments (16 + 32 + 11 + 8 + 1)', () => {
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(68);
expect(total).toBe(74);
});
});

Expand Down
6 changes: 3 additions & 3 deletions app/src/audio/samples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ describe('Sample ID parity', () => {
});

it('should have expected sample count', () => {
// 8 drums + 2 bass + 4 synth + 2 fx = 16 samples
expect(allSampleIds.length).toBe(16);
expect(SAMPLE_CATEGORIES.drums.length).toBe(8);
// 14 drums + 2 bass + 4 synth + 2 fx = 22 samples
expect(allSampleIds.length).toBe(22);
expect(SAMPLE_CATEGORIES.drums.length).toBe(14);
expect(SAMPLE_CATEGORIES.bass.length).toBe(2);
expect(SAMPLE_CATEGORIES.synth.length).toBe(4);
expect(SAMPLE_CATEGORIES.fx.length).toBe(2);
Expand Down
178 changes: 178 additions & 0 deletions app/src/audio/samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,49 @@ export async function createSynthesizedSamples(
url: '',
});

// === WORLD/LATIN PERCUSSION ===
samples.set('shaker', {
id: 'shaker',
name: 'Shaker',
buffer: await createShaker(audioContext),
url: '',
});

samples.set('conga', {
id: 'conga',
name: 'Conga',
buffer: await createConga(audioContext),
url: '',
});

samples.set('tambourine', {
id: 'tambourine',
name: 'Tambourine',
buffer: await createTambourine(audioContext),
url: '',
});

samples.set('clave', {
id: 'clave',
name: 'Clave',
buffer: await createClave(audioContext),
url: '',
});

samples.set('cabasa', {
id: 'cabasa',
name: 'Cabasa',
buffer: await createCabasa(audioContext),
url: '',
});

samples.set('woodblock', {
id: 'woodblock',
name: 'Woodblock',
buffer: await createWoodblock(audioContext),
url: '',
});

// === BASS ===
samples.set('bass', {
id: 'bass',
Expand Down Expand Up @@ -284,6 +327,141 @@ async function createOpenHat(ctx: AudioContext): Promise<AudioBuffer> {
return buffer;
}

// === World/Latin Percussion ===

async function createShaker(ctx: AudioContext): Promise<AudioBuffer> {
const duration = 0.15;
const sampleRate = ctx.sampleRate;
const length = Math.floor(duration * sampleRate);
const buffer = ctx.createBuffer(1, length, sampleRate);
const data = buffer.getChannelData(0);

for (let i = 0; i < length; i++) {
const t = i / sampleRate;
// High-frequency noise with fast attack/decay
const noise = Math.random() * 2 - 1;
const envelope = Math.exp(-t * 25) * (1 - Math.exp(-t * 500));
// Simple highpass approximation
const filtered = noise * 0.7 + (Math.random() * 0.6 - 0.3);
data[i] = filtered * envelope * 0.6;
}

return buffer;
}

async function createConga(ctx: AudioContext): Promise<AudioBuffer> {
const duration = 0.4;
const sampleRate = ctx.sampleRate;
const length = Math.floor(duration * sampleRate);
const buffer = ctx.createBuffer(1, length, sampleRate);
const data = buffer.getChannelData(0);

for (let i = 0; i < length; i++) {
const t = i / sampleRate;
// Pitched membrane sound with slight pitch drop
const freq = 200 * Math.exp(-t * 3);
const fundamental = Math.sin(2 * Math.PI * freq * t);
// Add harmonics for wood/skin character
const harmonic2 = Math.sin(2 * Math.PI * freq * 2.3 * t) * 0.3;
const harmonic3 = Math.sin(2 * Math.PI * freq * 3.1 * t) * 0.15;
// Attack transient (slap)
const slap = (Math.random() * 2 - 1) * Math.exp(-t * 100) * 0.4;
// Envelope
const envelope = Math.exp(-t * 6);
data[i] = (fundamental + harmonic2 + harmonic3 + slap) * envelope * 0.7;
}

return buffer;
}

async function createTambourine(ctx: AudioContext): Promise<AudioBuffer> {
const duration = 0.25;
const sampleRate = ctx.sampleRate;
const length = Math.floor(duration * sampleRate);
const buffer = ctx.createBuffer(1, length, sampleRate);
const data = buffer.getChannelData(0);

for (let i = 0; i < length; i++) {
const t = i / sampleRate;
// Metallic jingles (multiple inharmonic frequencies)
const jingle1 = Math.sin(2 * Math.PI * 2100 * t);
const jingle2 = Math.sin(2 * Math.PI * 3400 * t);
const jingle3 = Math.sin(2 * Math.PI * 4800 * t);
const jingle4 = Math.sin(2 * Math.PI * 6200 * t);
// Noise component for stick hit
const noise = (Math.random() * 2 - 1) * Math.exp(-t * 50);
// Envelope with sustain for jingles
const envelope = Math.exp(-t * 8);
const jingles = (jingle1 + jingle2 * 0.7 + jingle3 * 0.5 + jingle4 * 0.3) * 0.15;
data[i] = (jingles + noise * 0.3) * envelope;
}

return buffer;
}

async function createClave(ctx: AudioContext): Promise<AudioBuffer> {
const duration = 0.12;
const sampleRate = ctx.sampleRate;
const length = Math.floor(duration * sampleRate);
const buffer = ctx.createBuffer(1, length, sampleRate);
const data = buffer.getChannelData(0);

for (let i = 0; i < length; i++) {
const t = i / sampleRate;
// Two-tone wooden click (like two sticks hitting)
const freq1 = 2500;
const freq2 = 3200;
const tone1 = Math.sin(2 * Math.PI * freq1 * t);
const tone2 = Math.sin(2 * Math.PI * freq2 * t) * 0.6;
// Very fast decay
const envelope = Math.exp(-t * 40);
data[i] = (tone1 + tone2) * envelope * 0.6;
}

return buffer;
}

async function createCabasa(ctx: AudioContext): Promise<AudioBuffer> {
const duration = 0.08;
const sampleRate = ctx.sampleRate;
const length = Math.floor(duration * sampleRate);
const buffer = ctx.createBuffer(1, length, sampleRate);
const data = buffer.getChannelData(0);

for (let i = 0; i < length; i++) {
const t = i / sampleRate;
// Very high frequency noise burst
const noise = Math.random() * 2 - 1;
// Very fast attack and decay
const envelope = Math.exp(-t * 60) * (1 - Math.exp(-t * 2000));
data[i] = noise * envelope * 0.5;
}

return buffer;
}

async function createWoodblock(ctx: AudioContext): Promise<AudioBuffer> {
const duration = 0.15;
const sampleRate = ctx.sampleRate;
const length = Math.floor(duration * sampleRate);
const buffer = ctx.createBuffer(1, length, sampleRate);
const data = buffer.getChannelData(0);

for (let i = 0; i < length; i++) {
const t = i / sampleRate;
// Resonant filtered click
const freq = 800;
const fundamental = Math.sin(2 * Math.PI * freq * t);
const harmonic = Math.sin(2 * Math.PI * freq * 2.7 * t) * 0.4;
// Sharp attack, medium decay with resonance
const envelope = Math.exp(-t * 20);
const attack = Math.exp(-t * 200);
data[i] = (fundamental + harmonic) * envelope * (0.7 + attack * 0.3);
}

return buffer;
}

// === Bass ===

async function createBass(ctx: AudioContext): Promise<AudioBuffer> {
Expand Down
Loading