0ad1ec1midiboot: in-browser MIDI workstation with pluggable instrumentsJeremy Magland 1// Pure per-note synthesis. Every function here is deterministic (noise
2// comes from a PRNG seeded by pitch) and free of Web Audio, so the node
3// test harness can hash the actual audio.
5export function midiToFreq(pitch: number): number {
6 return 440 * Math.pow(2, (pitch - 69) / 12);
7}
9export function mulberry32(seed: number): () => number {
10 let a = seed >>> 0;
11 return () => {
12 a |= 0;
13 a = (a + 0x6d2b79f5) | 0;
14 let t = Math.imul(a ^ (a >>> 15), 1 | a);
15 t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
16 return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
17 };
18}
20// Karplus-Strong plucked string. Rings past the nominal duration with a
21// release envelope so a short note still sounds like a pluck.
22export function pluckNote(sr: number, pitch: number, velocity: number, durationSec: number): Float32Array {
23 const f0 = midiToFreq(pitch);
24 const period = Math.max(2, Math.round(sr / f0));
25 const tail = 1.2;
26 const n = Math.max(period + 1, Math.floor(sr * (durationSec + tail)));
27 const out = new Float32Array(n);
28 const delay = new Float32Array(period);
29 const rng = mulberry32(0x9e3779b9 ^ pitch);
30 for (let i = 0; i < period; i++) delay[i] = rng() * 2 - 1;
31 const g = 0.996;
32 let idx = 0;
33 for (let i = 0; i < n; i++) {
34 const cur = delay[idx];
35 const nxt = delay[(idx + 1) % period];
36 out[i] = cur;
37 delay[idx] = g * 0.5 * (cur + nxt);
38 idx = (idx + 1) % period;
39 }
40 const relStart = Math.floor(durationSec * sr);
41 for (let i = 0; i < n; i++) {
42 let env = 1;
43 if (i > relStart) env = Math.exp(-(i - relStart) / (0.12 * sr));
44 out[i] *= env;
45 }
46 scale(out, 0.6 * velocity);
47 return out;
48}
50// Two-operator FM electric piano: octave modulator with a decaying index.
51export function epianoNote(sr: number, pitch: number, velocity: number, durationSec: number): Float32Array {
52 const f0 = midiToFreq(pitch);
53 const tail = 0.8;
54 const n = Math.floor(sr * (durationSec + tail));
55 const out = new Float32Array(n);
56 const wc = (2 * Math.PI * f0) / sr;
57 const wm = 2 * wc;
58 const relStart = durationSec * sr;
59 for (let i = 0; i < n; i++) {
60 const t = i / sr;
61 const idxEnv = (1.2 + 2.5 * velocity) * Math.exp(-t * 4);
62 let env = Math.min(1, i / (0.002 * sr)) * Math.exp(-t * 1.8);
63 if (i > relStart) env *= Math.exp(-(i - relStart) / (0.07 * sr));
64 out[i] = env * Math.sin(wc * i + idxEnv * Math.sin(wm * i));
65 }
66 scale(out, 0.5 * velocity);
67 return out;
68}
70// Square-ish lead: summed odd harmonics through a one-pole lowpass whose
71// cutoff falls with time, with an ADSR gated by the note duration.
72export function leadNote(sr: number, pitch: number, velocity: number, durationSec: number): Float32Array {
73 const f0 = midiToFreq(pitch);
74 const tail = 0.35;
75 const n = Math.floor(sr * (durationSec + tail));
76 const out = new Float32Array(n);
77 const w = (2 * Math.PI * f0) / sr;
78 const nyq = sr / 2;
79 let y = 0;
80 const relStart = durationSec * sr;
81 for (let i = 0; i < n; i++) {
82 const t = i / sr;
83 let x = 0;
84 for (let k = 1; k <= 15; k += 2) {
85 if (f0 * k > nyq * 0.9) break;
86 x += Math.sin(w * k * i) / k;
87 }
88 const cutoff = 500 + (1500 + 5000 * velocity) * Math.exp(-t * 3);
89 const a = 1 - Math.exp((-2 * Math.PI * cutoff) / sr);
90 y += a * (x - y);
91 const atk = Math.min(1, i / (0.005 * sr));
92 let env = atk * (0.7 + 0.3 * Math.exp(-t * 6));
93 if (i > relStart) env *= Math.exp(-(i - relStart) / (0.06 * sr));
94 out[i] = y * env;
95 }
96 scale(out, 0.45 * velocity);
97 return out;
98}
100// General MIDI drum kit, keyed by pitch. Duration is ignored: a struck
101// drum runs to its own decay.
102export function drumNote(sr: number, pitch: number, velocity: number, _durationSec: number): Float32Array {
103 const rng = mulberry32(0x51ed270b ^ pitch);
104 const make = (len: number, fn: (t: number, i: number, noise: () => number) => number) => {
105 const n = Math.floor(sr * len);
106 const out = new Float32Array(n);
107 for (let i = 0; i < n; i++) out[i] = fn(i / sr, i, () => rng() * 2 - 1);
108 return out;
109 };
110 let buf: Float32Array;
111 if (pitch === 35 || pitch === 36) {
112 // kick: falling sine sweep
113 let phase = 0;
114 buf = make(0.5, (t) => {
115 const f = 40 + 120 * Math.exp(-t * 28);
116 phase += (2 * Math.PI * f) / sr;
117 return Math.sin(phase) * Math.exp(-t * 9);
118 });
119 } else if (pitch === 38 || pitch === 40) {
120 // snare: tone plus noise burst
121 buf = make(0.3, (t, i, noise) => Math.sin((2 * Math.PI * 190 * i) / sr) * 0.4 * Math.exp(-t * 30) + noise() * 0.9 * Math.exp(-t * 18));
122 } else if (pitch === 39) {
123 // clap
124 buf = make(0.25, (t, _i, noise) => noise() * (Math.exp(-t * 40) + 0.6 * Math.exp(-Math.max(0, t - 0.012) * 40) + 0.35 * Math.exp(-Math.max(0, t - 0.024) * 30)));
125 } else if (pitch === 42 || pitch === 44) {
126 // closed hat: highpassed noise
127 buf = hp(make(0.1, (t, _i, noise) => noise() * Math.exp(-t * 60)));
128 } else if (pitch === 46) {
129 // open hat
130 buf = hp(make(0.6, (t, _i, noise) => noise() * Math.exp(-t * 8)));
131 } else if (pitch === 49 || pitch === 57 || pitch === 55 || pitch === 52) {
132 // crashes
133 buf = hp(make(1.6, (t, _i, noise) => noise() * Math.exp(-t * 3)));
134 } else if (pitch === 51 || pitch === 59 || pitch === 53) {
135 // ride
136 buf = hp(make(1.0, (t, i, noise) => noise() * 0.5 * Math.exp(-t * 5) + Math.sin((2 * Math.PI * 5200 * i) / sr) * 0.1 * Math.exp(-t * 6)));
137 } else if (pitch >= 41 && pitch <= 50) {
138 // toms: pitch-mapped sweep
139 const f0 = 70 + (pitch - 41) * 14;
140 let phase = 0;
141 buf = make(0.45, (t) => {
142 const f = f0 * (1 + 0.6 * Math.exp(-t * 20));
143 phase += (2 * Math.PI * f) / sr;
144 return Math.sin(phase) * Math.exp(-t * 11);
145 });
146 } else {
147 // default percussion tick
148 buf = make(0.12, (t, _i, noise) => noise() * Math.exp(-t * 45));
149 }
150 scale(buf, 0.7 * velocity);
151 return buf;
152}
154function hp(buf: Float32Array): Float32Array {
155 let lp = 0;
156 for (let i = 0; i < buf.length; i++) {
157 lp += 0.15 * (buf[i] - lp);
158 buf[i] = buf[i] - lp;
159 }
160 return buf;
161}
163function scale(buf: Float32Array, s: number) {
164 for (let i = 0; i < buf.length; i++) buf[i] *= s;
165}