1/**
2 * Placeholder drum voices on an OfflineAudioContext: a pitch-dropping sine
3 * for the kick and tom, filtered noise for the snare and hat. These exist
4 * so the capture-classify-arrange loop is playable end to end; the
5 * instrawave renderer replaces them behind the same interface.
6 */
7import type { NoteEvent, SynthPreset } from './types.ts';
9const SR = 44100;
11export async function renderSynth(
12 events: NoteEvent[],
13 preset: (voiceId: string) => SynthPreset,
14 seconds: number,
15): Promise<AudioBuffer> {
16 const len = Math.max(1, Math.ceil(seconds * SR));
17 const ctx = new OfflineAudioContext(1, len, SR);
18 const noise = noiseBuffer(ctx);
19 for (const ev of events) {
20 const g = Math.max(1e-4, Math.min(1, ev.gain));
21 switch (preset(ev.voiceId)) {
22 case 'kick': kick(ctx, ev.t, g); break;
23 case 'snare': snare(ctx, ev.t, g, noise); break;
24 case 'hat': hat(ctx, ev.t, g, noise); break;
25 case 'tom': tom(ctx, ev.t, g); break;
26 }
27 }
28 return ctx.startRendering();
29}
31function noiseBuffer(ctx: OfflineAudioContext): AudioBuffer {
32 const buf = ctx.createBuffer(1, SR, SR);
33 const x = buf.getChannelData(0);
34 let s = 22222; // deterministic; renders are reproducible
35 for (let i = 0; i < x.length; i++) {
36 s = (s * 1103515245 + 12345) & 0x7fffffff;
37 x[i] = (s / 0x3fffffff) - 1;
38 }
39 return buf;
40}
42function env(ctx: OfflineAudioContext, t: number, g: number, decay: number): GainNode {
43 const gain = ctx.createGain();
44 gain.gain.setValueAtTime(g, t);
45 gain.gain.exponentialRampToValueAtTime(1e-4, t + decay);
46 gain.connect(ctx.destination);
47 return gain;
48}
50function kick(ctx: OfflineAudioContext, t: number, g: number): void {
51 const osc = ctx.createOscillator();
52 osc.frequency.setValueAtTime(150, t);
53 osc.frequency.exponentialRampToValueAtTime(46, t + 0.09);
54 osc.connect(env(ctx, t, g, 0.45));
55 osc.start(t);
56 osc.stop(t + 0.5);
57}
59function tom(ctx: OfflineAudioContext, t: number, g: number): void {
60 const osc = ctx.createOscillator();
61 osc.frequency.setValueAtTime(150, t);
62 osc.frequency.exponentialRampToValueAtTime(98, t + 0.12);
63 osc.connect(env(ctx, t, g * 0.9, 0.3));
64 osc.start(t);
65 osc.stop(t + 0.35);
66}
68function snare(ctx: OfflineAudioContext, t: number, g: number, noise: AudioBuffer): void {
69 const tone = ctx.createOscillator();
70 tone.type = 'triangle';
71 tone.frequency.value = 185;
72 tone.connect(env(ctx, t, g * 0.4, 0.12));
73 tone.start(t);
74 tone.stop(t + 0.15);
75 const n = ctx.createBufferSource();
76 n.buffer = noise;
77 const bp = ctx.createBiquadFilter();
78 bp.type = 'highpass';
79 bp.frequency.value = 900;
80 n.connect(bp).connect(env(ctx, t, g * 0.7, 0.2));
81 n.start(t, 0, 0.25);
82}
84function hat(ctx: OfflineAudioContext, t: number, g: number, noise: AudioBuffer): void {
85 const n = ctx.createBufferSource();
86 n.buffer = noise;
87 const hp = ctx.createBiquadFilter();
88 hp.type = 'highpass';
89 hp.frequency.value = 7000;
90 n.connect(hp).connect(env(ctx, t, g * 0.5, 0.06));
91 n.start(t, 0, 0.1);
92}