/** * Placeholder drum voices on an OfflineAudioContext: a pitch-dropping sine * for the kick and tom, filtered noise for the snare and hat. These exist * so the capture-classify-arrange loop is playable end to end; the * instrawave renderer replaces them behind the same interface. */ import type { NoteEvent, SynthPreset } from './types.ts'; const SR = 44100; export async function renderSynth( events: NoteEvent[], preset: (voiceId: string) => SynthPreset, seconds: number, ): Promise { const len = Math.max(1, Math.ceil(seconds * SR)); const ctx = new OfflineAudioContext(1, len, SR); const noise = noiseBuffer(ctx); for (const ev of events) { const g = Math.max(1e-4, Math.min(1, ev.gain)); switch (preset(ev.voiceId)) { case 'kick': kick(ctx, ev.t, g); break; case 'snare': snare(ctx, ev.t, g, noise); break; case 'hat': hat(ctx, ev.t, g, noise); break; case 'tom': tom(ctx, ev.t, g); break; } } return ctx.startRendering(); } function noiseBuffer(ctx: OfflineAudioContext): AudioBuffer { const buf = ctx.createBuffer(1, SR, SR); const x = buf.getChannelData(0); let s = 22222; // deterministic; renders are reproducible for (let i = 0; i < x.length; i++) { s = (s * 1103515245 + 12345) & 0x7fffffff; x[i] = (s / 0x3fffffff) - 1; } return buf; } function env(ctx: OfflineAudioContext, t: number, g: number, decay: number): GainNode { const gain = ctx.createGain(); gain.gain.setValueAtTime(g, t); gain.gain.exponentialRampToValueAtTime(1e-4, t + decay); gain.connect(ctx.destination); return gain; } function kick(ctx: OfflineAudioContext, t: number, g: number): void { const osc = ctx.createOscillator(); osc.frequency.setValueAtTime(150, t); osc.frequency.exponentialRampToValueAtTime(46, t + 0.09); osc.connect(env(ctx, t, g, 0.45)); osc.start(t); osc.stop(t + 0.5); } function tom(ctx: OfflineAudioContext, t: number, g: number): void { const osc = ctx.createOscillator(); osc.frequency.setValueAtTime(150, t); osc.frequency.exponentialRampToValueAtTime(98, t + 0.12); osc.connect(env(ctx, t, g * 0.9, 0.3)); osc.start(t); osc.stop(t + 0.35); } function snare(ctx: OfflineAudioContext, t: number, g: number, noise: AudioBuffer): void { const tone = ctx.createOscillator(); tone.type = 'triangle'; tone.frequency.value = 185; tone.connect(env(ctx, t, g * 0.4, 0.12)); tone.start(t); tone.stop(t + 0.15); const n = ctx.createBufferSource(); n.buffer = noise; const bp = ctx.createBiquadFilter(); bp.type = 'highpass'; bp.frequency.value = 900; n.connect(bp).connect(env(ctx, t, g * 0.7, 0.2)); n.start(t, 0, 0.25); } function hat(ctx: OfflineAudioContext, t: number, g: number, noise: AudioBuffer): void { const n = ctx.createBufferSource(); n.buffer = noise; const hp = ctx.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 7000; n.connect(hp).connect(env(ctx, t, g * 0.5, 0.06)); n.start(t, 0, 0.1); }