import type { Instrument, LiveVoice, NoteSpec, TrackRenderRequest, TrackRenderResult } from './types.ts'; import { brassNote, drumNote, epianoNote, leadNote, organNote, pianoNote, pluckNote, stringsNote, synthBassNote } 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, renderTrack(req: TrackRenderRequest): Promise { // lengthSamples is a minimum: a release that outlives the last note // is kept rather than clipped. const rendered: { off: number; buf: Float32Array }[] = []; let length = 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); rendered.push({ off, buf }); length = Math.max(length, off + buf.length); } const out = new Float32Array(length); for (const r of rendered) { const end = Math.min(out.length, r.off + r.buf.length); for (let i = Math.max(0, r.off); i < end; i++) out[i] += r.buf[i - r.off]; } return Promise.resolve({ audio: 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), }; }, }; } // The cheap synthetic set: one instrument per broad General MIDI family, // so an imported MIDI file is playable without a soundfont. export const builtinInstruments: Instrument[] = [ instrumentFromNoteDsp('piano', 'Piano (FM)', pianoNote, { sustains: true, liveDurationSec: 10 }), instrumentFromNoteDsp('epiano', 'E-Piano (FM)', epianoNote, { sustains: true, liveDurationSec: 8 }), instrumentFromNoteDsp('organ', 'Organ (drawbar)', organNote, { sustains: true, liveDurationSec: 20 }), instrumentFromNoteDsp('pluck', 'Pluck (Karplus-Strong)', pluckNote, { sustains: true, liveDurationSec: 3 }), instrumentFromNoteDsp('synthbass', 'Synth Bass', synthBassNote, { sustains: true, liveDurationSec: 6 }), instrumentFromNoteDsp('strings', 'Strings (pad)', stringsNote, { sustains: true, liveDurationSec: 20 }), instrumentFromNoteDsp('brass', 'Brass (saw)', brassNote, { sustains: true, liveDurationSec: 20 }), 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 };