import type { Instrument, LiveVoice, NoteSpec, RenderRequest } from './types.ts'; import { drumNote, epianoNote, leadNote, pluckNote } from './dsp.ts'; // Built-in instruments, each constructed from a pure per-note DSP function. // The render path sums per-note buffers; the live path plays one note's // buffer through a gain node whose release implements noteOff. A custom // instrument with its own rendering logic does not have to go through // this helper; it only has to satisfy the Instrument interface. export type NoteDsp = (sr: number, pitch: number, velocity: number, durationSec: number) => Float32Array; interface DspOpts { sustains: boolean; // if true, live voices are rendered long and gated by noteOff liveDurationSec?: number; } export function instrumentFromNoteDsp(id: string, name: string, dsp: NoteDsp, opts: DspOpts): Instrument { return { id, name, render(req: RenderRequest): Float32Array { const out = new Float32Array(req.lengthSamples); for (const note of req.notes) { const buf = dsp(req.sampleRate, note.pitch, note.velocity, note.durationSec); const off = Math.round(note.timeSec * req.sampleRate); const end = Math.min(out.length, off + buf.length); for (let i = Math.max(0, off); i < end; i++) out[i] += buf[i - off]; } return out; }, noteOn(ctx, dest, pitch, velocity, whenSec): LiveVoice { const dur = opts.sustains ? (opts.liveDurationSec ?? 20) : 0.25; const data = dsp(ctx.sampleRate, pitch, velocity, dur); const buffer = ctx.createBuffer(1, data.length, ctx.sampleRate); buffer.copyToChannel(data as Float32Array, 0); const src = ctx.createBufferSource(); src.buffer = buffer; const gain = ctx.createGain(); src.connect(gain); gain.connect(dest); src.start(whenSec); let done = false; const finish = (fadeSec: number) => { if (done) return; done = true; const t = Math.max(ctx.currentTime, whenSec); gain.gain.setValueAtTime(gain.gain.value, t); gain.gain.setTargetAtTime(0, t, fadeSec / 4); src.stop(t + fadeSec + 0.1); }; src.onended = () => { done = true; }; return { noteOff: () => { if (opts.sustains) finish(0.15); }, cut: () => finish(0.02), }; }, }; } export const builtinInstruments: Instrument[] = [ instrumentFromNoteDsp('pluck', 'Pluck (Karplus-Strong)', pluckNote, { sustains: true, liveDurationSec: 3 }), instrumentFromNoteDsp('epiano', 'E-Piano (FM)', epianoNote, { sustains: true, liveDurationSec: 8 }), instrumentFromNoteDsp('lead', 'Lead (square)', leadNote, { sustains: true, liveDurationSec: 20 }), instrumentFromNoteDsp('drumkit', 'Drum kit (GM)', drumNote, { sustains: false }), ]; export function noteSpecVelocity(velocity127: number): number { return Math.max(0, Math.min(1, velocity127 / 127)); } export type { NoteSpec };