0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 1import type { Instrument, LiveVoice, NoteSpec, RenderRequest } from './types.ts';
2import { drumNote, epianoNote, leadNote, pluckNote } from './dsp.ts';
4// Built-in instruments, each constructed from a pure per-note DSP function.
5// The render path sums per-note buffers; the live path plays one note's
6// buffer through a gain node whose release implements noteOff. A custom
7// instrument with its own rendering logic does not have to go through
8// this helper; it only has to satisfy the Instrument interface.
10export type NoteDsp = (sr: number, pitch: number, velocity: number, durationSec: number) => Float32Array;
12interface DspOpts {
13 sustains: boolean; // if true, live voices are rendered long and gated by noteOff
14 liveDurationSec?: number;
15}
17export function instrumentFromNoteDsp(id: string, name: string, dsp: NoteDsp, opts: DspOpts): Instrument {
18 return {
19 id,
20 name,
21 render(req: RenderRequest): Float32Array {
22 const out = new Float32Array(req.lengthSamples);
23 for (const note of req.notes) {
24 const buf = dsp(req.sampleRate, note.pitch, note.velocity, note.durationSec);
25 const off = Math.round(note.timeSec * req.sampleRate);
26 const end = Math.min(out.length, off + buf.length);
27 for (let i = Math.max(0, off); i < end; i++) out[i] += buf[i - off];
28 }
29 return out;
30 },
31 noteOn(ctx, dest, pitch, velocity, whenSec): LiveVoice {
32 const dur = opts.sustains ? (opts.liveDurationSec ?? 20) : 0.25;
33 const data = dsp(ctx.sampleRate, pitch, velocity, dur);
34 const buffer = ctx.createBuffer(1, data.length, ctx.sampleRate);
35 buffer.copyToChannel(data as Float32Array<ArrayBuffer>, 0);
36 const src = ctx.createBufferSource();
37 src.buffer = buffer;
38 const gain = ctx.createGain();
39 src.connect(gain);
40 gain.connect(dest);
41 src.start(whenSec);
42 let done = false;
43 const finish = (fadeSec: number) => {
44 if (done) return;
45 done = true;
46 const t = Math.max(ctx.currentTime, whenSec);
47 gain.gain.setValueAtTime(gain.gain.value, t);
48 gain.gain.setTargetAtTime(0, t, fadeSec / 4);
49 src.stop(t + fadeSec + 0.1);
50 };
51 src.onended = () => { done = true; };
52 return {
53 noteOff: () => { if (opts.sustains) finish(0.15); },
54 cut: () => finish(0.02),
55 };
56 },
57 };
58}
60export const builtinInstruments: Instrument[] = [
61 instrumentFromNoteDsp('pluck', 'Pluck (Karplus-Strong)', pluckNote, { sustains: true, liveDurationSec: 3 }),
62 instrumentFromNoteDsp('epiano', 'E-Piano (FM)', epianoNote, { sustains: true, liveDurationSec: 8 }),
63 instrumentFromNoteDsp('lead', 'Lead (square)', leadNote, { sustains: true, liveDurationSec: 20 }),
64 instrumentFromNoteDsp('drumkit', 'Drum kit (GM)', drumNote, { sustains: false }),
65];
67export function noteSpecVelocity(velocity127: number): number {
68 return Math.max(0, Math.min(1, velocity127 / 127));
69}
71export type { NoteSpec };