polyphon 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +374 -2
- package/README.zh-TW.md +375 -0
- package/dist/engine/automation.d.ts +24 -0
- package/dist/engine/automation.d.ts.map +1 -0
- package/dist/engine/automation.js +55 -0
- package/dist/engine/automation.js.map +1 -0
- package/dist/engine/compile.d.ts +36 -0
- package/dist/engine/compile.d.ts.map +1 -0
- package/dist/engine/compile.js +103 -0
- package/dist/engine/compile.js.map +1 -0
- package/dist/engine/jukebox.d.ts +113 -0
- package/dist/engine/jukebox.d.ts.map +1 -0
- package/dist/engine/jukebox.js +287 -0
- package/dist/engine/jukebox.js.map +1 -0
- package/dist/engine/mixer.d.ts +29 -0
- package/dist/engine/mixer.d.ts.map +1 -0
- package/dist/engine/mixer.js +141 -0
- package/dist/engine/mixer.js.map +1 -0
- package/dist/engine/output.d.ts +14 -0
- package/dist/engine/output.d.ts.map +1 -0
- package/dist/engine/output.js +51 -0
- package/dist/engine/output.js.map +1 -0
- package/dist/engine/playback.d.ts +97 -0
- package/dist/engine/playback.d.ts.map +1 -0
- package/dist/engine/playback.js +311 -0
- package/dist/engine/playback.js.map +1 -0
- package/dist/engine/prepare.d.ts +6 -0
- package/dist/engine/prepare.d.ts.map +1 -0
- package/dist/engine/prepare.js +12 -0
- package/dist/engine/prepare.js.map +1 -0
- package/dist/engine/render.d.ts +14 -0
- package/dist/engine/render.d.ts.map +1 -0
- package/dist/engine/render.js +60 -0
- package/dist/engine/render.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/instruments/bowed.d.ts +357 -0
- package/dist/instruments/bowed.d.ts.map +1 -0
- package/dist/instruments/bowed.js +502 -0
- package/dist/instruments/bowed.js.map +1 -0
- package/dist/instruments/builtin.d.ts +261 -0
- package/dist/instruments/builtin.d.ts.map +1 -0
- package/dist/instruments/builtin.js +160 -0
- package/dist/instruments/builtin.js.map +1 -0
- package/dist/instruments/definition.d.ts +46 -0
- package/dist/instruments/definition.d.ts.map +1 -0
- package/dist/instruments/definition.js +5 -0
- package/dist/instruments/definition.js.map +1 -0
- package/dist/instruments/dsp/ambience.d.ts +17 -0
- package/dist/instruments/dsp/ambience.d.ts.map +1 -0
- package/dist/instruments/dsp/ambience.js +146 -0
- package/dist/instruments/dsp/ambience.js.map +1 -0
- package/dist/instruments/dsp/core.d.ts +22 -0
- package/dist/instruments/dsp/core.d.ts.map +1 -0
- package/dist/instruments/dsp/core.js +100 -0
- package/dist/instruments/dsp/core.js.map +1 -0
- package/dist/instruments/dsp/drums.d.ts +112 -0
- package/dist/instruments/dsp/drums.d.ts.map +1 -0
- package/dist/instruments/dsp/drums.js +566 -0
- package/dist/instruments/dsp/drums.js.map +1 -0
- package/dist/instruments/dsp/modal.d.ts +30 -0
- package/dist/instruments/dsp/modal.d.ts.map +1 -0
- package/dist/instruments/dsp/modal.js +54 -0
- package/dist/instruments/dsp/modal.js.map +1 -0
- package/dist/instruments/dsp/piano.d.ts +9 -0
- package/dist/instruments/dsp/piano.d.ts.map +1 -0
- package/dist/instruments/dsp/piano.js +230 -0
- package/dist/instruments/dsp/piano.js.map +1 -0
- package/dist/instruments/dsp/pluck.d.ts +53 -0
- package/dist/instruments/dsp/pluck.d.ts.map +1 -0
- package/dist/instruments/dsp/pluck.js +195 -0
- package/dist/instruments/dsp/pluck.js.map +1 -0
- package/dist/instruments/dsp/reverb.d.ts +16 -0
- package/dist/instruments/dsp/reverb.d.ts.map +1 -0
- package/dist/instruments/dsp/reverb.js +47 -0
- package/dist/instruments/dsp/reverb.js.map +1 -0
- package/dist/instruments/dsp/staccato.d.ts +18 -0
- package/dist/instruments/dsp/staccato.d.ts.map +1 -0
- package/dist/instruments/dsp/staccato.js +147 -0
- package/dist/instruments/dsp/staccato.js.map +1 -0
- package/dist/instruments/play.d.ts +21 -0
- package/dist/instruments/play.d.ts.map +1 -0
- package/dist/instruments/play.js +140 -0
- package/dist/instruments/play.js.map +1 -0
- package/dist/instruments/registry.d.ts +63 -0
- package/dist/instruments/registry.d.ts.map +1 -0
- package/dist/instruments/registry.js +50 -0
- package/dist/instruments/registry.js.map +1 -0
- package/dist/instruments/samples.d.ts +30 -0
- package/dist/instruments/samples.d.ts.map +1 -0
- package/dist/instruments/samples.js +109 -0
- package/dist/instruments/samples.js.map +1 -0
- package/dist/instruments/voices.d.ts +76 -0
- package/dist/instruments/voices.d.ts.map +1 -0
- package/dist/instruments/voices.js +1061 -0
- package/dist/instruments/voices.js.map +1 -0
- package/dist/midi.d.ts +15 -0
- package/dist/midi.d.ts.map +1 -0
- package/dist/midi.js +280 -0
- package/dist/midi.js.map +1 -0
- package/dist/notation/format.d.ts +12 -0
- package/dist/notation/format.d.ts.map +1 -0
- package/dist/notation/format.js +102 -0
- package/dist/notation/format.js.map +1 -0
- package/dist/notation/harmony.d.ts +83 -0
- package/dist/notation/harmony.d.ts.map +1 -0
- package/dist/notation/harmony.js +191 -0
- package/dist/notation/harmony.js.map +1 -0
- package/dist/notation/melody.d.ts +25 -0
- package/dist/notation/melody.d.ts.map +1 -0
- package/dist/notation/melody.js +138 -0
- package/dist/notation/melody.js.map +1 -0
- package/dist/notation/part.d.ts +34 -0
- package/dist/notation/part.d.ts.map +1 -0
- package/dist/notation/part.js +15 -0
- package/dist/notation/part.js.map +1 -0
- package/dist/notation/transform.d.ts +28 -0
- package/dist/notation/transform.d.ts.map +1 -0
- package/dist/notation/transform.js +67 -0
- package/dist/notation/transform.js.map +1 -0
- package/dist/song.d.ts +140 -0
- package/dist/song.d.ts.map +1 -0
- package/dist/song.js +45 -0
- package/dist/song.js.map +1 -0
- package/dist/tempo.d.ts +36 -0
- package/dist/tempo.d.ts.map +1 -0
- package/dist/tempo.js +81 -0
- package/dist/tempo.js.map +1 -0
- package/dist/theory.d.ts +70 -0
- package/dist/theory.d.ts.map +1 -0
- package/dist/theory.js +183 -0
- package/dist/theory.js.map +1 -0
- package/dist/transcribe.d.ts +81 -0
- package/dist/transcribe.d.ts.map +1 -0
- package/dist/transcribe.js +433 -0
- package/dist/transcribe.js.map +1 -0
- package/package.json +57 -4
- package/src/engine/automation.ts +65 -0
- package/src/engine/compile.ts +124 -0
- package/src/engine/jukebox.ts +322 -0
- package/src/engine/mixer.ts +166 -0
- package/src/engine/output.ts +59 -0
- package/src/engine/playback.ts +349 -0
- package/src/engine/prepare.ts +15 -0
- package/src/engine/render.ts +71 -0
- package/src/index.ts +28 -0
- package/src/instruments/bowed.ts +660 -0
- package/src/instruments/builtin.ts +180 -0
- package/src/instruments/definition.ts +52 -0
- package/src/instruments/dsp/ambience.ts +142 -0
- package/src/instruments/dsp/core.ts +97 -0
- package/src/instruments/dsp/drums.ts +565 -0
- package/src/instruments/dsp/modal.ts +76 -0
- package/src/instruments/dsp/piano.ts +226 -0
- package/src/instruments/dsp/pluck.ts +240 -0
- package/src/instruments/dsp/reverb.ts +56 -0
- package/src/instruments/dsp/staccato.ts +141 -0
- package/src/instruments/play.ts +151 -0
- package/src/instruments/registry.ts +100 -0
- package/src/instruments/samples.ts +119 -0
- package/src/instruments/voices.ts +1168 -0
- package/src/midi.ts +285 -0
- package/src/notation/format.ts +96 -0
- package/src/notation/harmony.ts +233 -0
- package/src/notation/melody.ts +133 -0
- package/src/notation/part.ts +42 -0
- package/src/notation/transform.ts +78 -0
- package/src/song.ts +170 -0
- package/src/tempo.ts +108 -0
- package/src/theory.ts +208 -0
- package/src/transcribe.ts +487 -0
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
// Live-synthesized sustained instruments: flutes, reeds, saxophones, brass, choir, organ, accordion, throat singing (khoomei), synthesizers
|
|
2
|
+
// (supersaw, leads, plucks, pads, basses), distorted guitar, ambience.
|
|
3
|
+
// Each note builds a small graph of Web Audio nodes; sampled instruments (plucked strings, bells, drums) are rendered offline in dsp/ instead.
|
|
4
|
+
import { rng } from './dsp/core.js';
|
|
5
|
+
// ───────────────────────── Shared parts ─────────────────────────
|
|
6
|
+
const waves = new WeakMap();
|
|
7
|
+
/** A periodic wave from a custom harmonic table (organ, accordion), starting `phase` (a fraction of a cycle) into it */
|
|
8
|
+
function wave(ctx, key, amps, phase = 0) {
|
|
9
|
+
let m = waves.get(ctx);
|
|
10
|
+
if (!m)
|
|
11
|
+
waves.set(ctx, (m = new Map()));
|
|
12
|
+
const k = phase ? `${key}@${phase}` : key;
|
|
13
|
+
let w = m.get(k);
|
|
14
|
+
if (!w) {
|
|
15
|
+
const real = new Float32Array(amps.length + 1);
|
|
16
|
+
const imag = new Float32Array(amps.length + 1);
|
|
17
|
+
amps.forEach((a, i) => {
|
|
18
|
+
real[i + 1] = a * Math.sin(2 * Math.PI * (i + 1) * phase);
|
|
19
|
+
imag[i + 1] = a * Math.cos(2 * Math.PI * (i + 1) * phase);
|
|
20
|
+
});
|
|
21
|
+
w = ctx.createPeriodicWave(real, imag);
|
|
22
|
+
m.set(k, w);
|
|
23
|
+
}
|
|
24
|
+
return w;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Baked waveform: compute the formant / body response straight into the amplitude of each harmonic, saving a filter (much cheaper on CPU).
|
|
28
|
+
* amp(k, f) returns the amplitude of the k-th harmonic (at frequency f); cached once per pitch.
|
|
29
|
+
*/
|
|
30
|
+
function baked(ctx, key, f0, amp, maxF = 9000) {
|
|
31
|
+
const semi = Math.round(12 * Math.log2(f0 / 440));
|
|
32
|
+
const k = `${key}@${semi}`;
|
|
33
|
+
let m = waves.get(ctx);
|
|
34
|
+
if (!m)
|
|
35
|
+
waves.set(ctx, (m = new Map()));
|
|
36
|
+
let w = m.get(k);
|
|
37
|
+
if (!w) {
|
|
38
|
+
const fr = 440 * Math.pow(2, semi / 12);
|
|
39
|
+
const n = Math.max(1, Math.min(96, Math.floor(maxF / fr)));
|
|
40
|
+
const real = new Float32Array(n + 1);
|
|
41
|
+
const imag = new Float32Array(n + 1);
|
|
42
|
+
for (let i = 1; i <= n; i++)
|
|
43
|
+
imag[i] = amp(i, i * fr);
|
|
44
|
+
w = ctx.createPeriodicWave(real, imag);
|
|
45
|
+
m.set(k, w);
|
|
46
|
+
}
|
|
47
|
+
return w;
|
|
48
|
+
}
|
|
49
|
+
/** Formant (Lorentzian peak): [center, bandwidth, gain] */
|
|
50
|
+
const peaks = (f, fs) => {
|
|
51
|
+
let a = 0;
|
|
52
|
+
for (const [F, bw, g] of fs)
|
|
53
|
+
a += g / (1 + ((f - F) / (bw / 2)) ** 2);
|
|
54
|
+
return a;
|
|
55
|
+
};
|
|
56
|
+
const noiseBufs = new WeakMap();
|
|
57
|
+
function noise(ctx) {
|
|
58
|
+
let b = noiseBufs.get(ctx);
|
|
59
|
+
if (!b) {
|
|
60
|
+
b = ctx.createBuffer(1, ctx.sampleRate * 3, ctx.sampleRate);
|
|
61
|
+
const d = b.getChannelData(0);
|
|
62
|
+
const r = rng(99);
|
|
63
|
+
for (let i = 0; i < d.length; i++)
|
|
64
|
+
d[i] = r();
|
|
65
|
+
noiseBufs.set(ctx, b);
|
|
66
|
+
}
|
|
67
|
+
return b;
|
|
68
|
+
}
|
|
69
|
+
function noiseSrc(ctx, t, end) {
|
|
70
|
+
const s = ctx.createBufferSource();
|
|
71
|
+
s.buffer = noise(ctx);
|
|
72
|
+
s.loop = true;
|
|
73
|
+
s.start(t, Math.random() * 2);
|
|
74
|
+
s.stop(end);
|
|
75
|
+
return s;
|
|
76
|
+
}
|
|
77
|
+
const breathBufs = new WeakMap();
|
|
78
|
+
/** Pre-filtered breath noise (a wide band-pass at 1.5-5 kHz), shared by the wind instruments, saving a filter on every note */
|
|
79
|
+
function breathSrc(ctx, t, end) {
|
|
80
|
+
let b = breathBufs.get(ctx);
|
|
81
|
+
if (!b) {
|
|
82
|
+
b = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
|
|
83
|
+
const d = b.getChannelData(0);
|
|
84
|
+
const r = rng(77);
|
|
85
|
+
let lp = 0;
|
|
86
|
+
let hp = 0;
|
|
87
|
+
for (let i = 0; i < d.length; i++) {
|
|
88
|
+
const x = r();
|
|
89
|
+
lp += 0.45 * (x - lp);
|
|
90
|
+
hp += 0.08 * (lp - hp);
|
|
91
|
+
d[i] = (lp - hp) * 2.2;
|
|
92
|
+
}
|
|
93
|
+
breathBufs.set(ctx, b);
|
|
94
|
+
}
|
|
95
|
+
const s = ctx.createBufferSource();
|
|
96
|
+
s.buffer = b;
|
|
97
|
+
s.loop = true;
|
|
98
|
+
s.start(t, Math.random() * 1.8);
|
|
99
|
+
s.stop(end);
|
|
100
|
+
return s;
|
|
101
|
+
}
|
|
102
|
+
function osc(ctx, type, f, t, end, detune = 0) {
|
|
103
|
+
const o = ctx.createOscillator();
|
|
104
|
+
if (typeof type === 'string')
|
|
105
|
+
o.type = type;
|
|
106
|
+
else
|
|
107
|
+
o.setPeriodicWave(type);
|
|
108
|
+
o.frequency.setValueAtTime(f, t);
|
|
109
|
+
if (detune)
|
|
110
|
+
o.detune.setValueAtTime(detune, t);
|
|
111
|
+
o.start(t);
|
|
112
|
+
o.stop(end);
|
|
113
|
+
return o;
|
|
114
|
+
}
|
|
115
|
+
function filt(ctx, type, f, q = 0.7, gain = 0) {
|
|
116
|
+
const b = ctx.createBiquadFilter();
|
|
117
|
+
b.type = type;
|
|
118
|
+
b.frequency.value = f;
|
|
119
|
+
b.Q.value = q;
|
|
120
|
+
if (gain)
|
|
121
|
+
b.gain.value = gain;
|
|
122
|
+
return b;
|
|
123
|
+
}
|
|
124
|
+
function gainNode(ctx, v = 1) {
|
|
125
|
+
const g = ctx.createGain();
|
|
126
|
+
g.gain.value = v;
|
|
127
|
+
return g;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* ADSR: attack, decay to the sustain ratio, release starting at t+dur. All use explicit ramps (different implementations compute the same result). Returns the time the sound actually ends.
|
|
131
|
+
* The release falls exponentially to floor (a level, not a ratio: a louder peak falls faster in dB); a voice whose gain is normalized per note passes peak·1e-3 to keep its release independent of that.
|
|
132
|
+
*/
|
|
133
|
+
function adsr(g, t, dur, peak, a, d, s, r, floor = 1e-4) {
|
|
134
|
+
const p = g.gain;
|
|
135
|
+
const aa = Math.min(a, Math.max(0.003, dur * 0.9));
|
|
136
|
+
const rel = t + Math.max(dur, aa + 0.005);
|
|
137
|
+
const sus = Math.max(floor, peak * s);
|
|
138
|
+
p.setValueAtTime(0, t);
|
|
139
|
+
p.linearRampToValueAtTime(peak, t + aa);
|
|
140
|
+
const dEnd = t + aa + Math.max(0.001, d);
|
|
141
|
+
let lv = peak;
|
|
142
|
+
if (d > 0 && s < 1) {
|
|
143
|
+
if (rel > dEnd) {
|
|
144
|
+
p.exponentialRampToValueAtTime(sus, dEnd);
|
|
145
|
+
lv = sus;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
lv = peak * Math.pow(sus / peak, (rel - t - aa) / (dEnd - t - aa));
|
|
149
|
+
p.exponentialRampToValueAtTime(Math.max(floor, lv), rel);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
p.setValueAtTime(Math.max(floor, lv), rel);
|
|
153
|
+
p.exponentialRampToValueAtTime(floor, rel + r);
|
|
154
|
+
p.setValueAtTime(0, rel + r + 0.005);
|
|
155
|
+
return rel + r + 0.02;
|
|
156
|
+
}
|
|
157
|
+
/** Smoothly fade to 0 (the target of setTargetAtTime cannot be 0: some implementations diverge) */
|
|
158
|
+
function fadeTo0(p, t, tau) {
|
|
159
|
+
p.setTargetAtTime(1e-4, t, tau);
|
|
160
|
+
p.setValueAtTime(0, t + tau * 9);
|
|
161
|
+
}
|
|
162
|
+
/** Apply glides to a frequency parameter */
|
|
163
|
+
function applyBends(p, c, ratio = 1, glide = 0.07) {
|
|
164
|
+
if (!c.bends)
|
|
165
|
+
return;
|
|
166
|
+
for (const [at, f] of c.bends)
|
|
167
|
+
p.setTargetAtTime(f * ratio, c.t + at, glide / 3);
|
|
168
|
+
}
|
|
169
|
+
/** Delayed vibrato: after delay seconds, wobble at depth cents and rate Hz */
|
|
170
|
+
function vibrato(ctx, targets, t, end, rate, cents, delay) {
|
|
171
|
+
if (cents <= 0)
|
|
172
|
+
return;
|
|
173
|
+
const lfo = osc(ctx, 'sine', rate * (0.96 + Math.random() * 0.08), t, end);
|
|
174
|
+
const g = ctx.createGain();
|
|
175
|
+
g.gain.setValueAtTime(0, t);
|
|
176
|
+
g.gain.linearRampToValueAtTime(0, t + delay * 0.5);
|
|
177
|
+
g.gain.linearRampToValueAtTime(cents, t + delay + 0.3);
|
|
178
|
+
lfo.connect(g);
|
|
179
|
+
for (const p of targets)
|
|
180
|
+
g.connect(p);
|
|
181
|
+
}
|
|
182
|
+
function panned(ctx, dest, pan) {
|
|
183
|
+
if (!pan || !ctx.createStereoPanner)
|
|
184
|
+
return dest;
|
|
185
|
+
const p = ctx.createStereoPanner();
|
|
186
|
+
p.pan.value = pan;
|
|
187
|
+
p.connect(dest);
|
|
188
|
+
return p;
|
|
189
|
+
}
|
|
190
|
+
// ───────────────────────── Lip and reed valves ─────────────────────────
|
|
191
|
+
//
|
|
192
|
+
// A brass player's lips and a saxophone's reed are valves that let the air into the instrument in puffs, once per cycle. Blown softly the valve
|
|
193
|
+
// barely closes and the flow is nearly a sine; blown hard it slams shut, the puffs grow short and sharp, and (in brass) nonlinear steepening along
|
|
194
|
+
// the bore adds more: the spectrum fills with harmonics. Below forte the n-th harmonic rises about n dB for every dB of the fundamental (Benade),
|
|
195
|
+
// so a crescendo opens up the tone, and the upper harmonics of a forte note sit 20-40 dB higher, relative to the fundamental, than in a piano note.
|
|
196
|
+
// The valve is a sine through the waveshaper x / (1 - Cx): for a sine of amplitude a the n-th harmonic falls as rⁿ, with r = (1 - √(1 - a²)) / a,
|
|
197
|
+
// about a/2 when soft and close to 1 when loud (Beauchamp's nonlinear brass synthesis). The drive is set per note so the harmonics fall as e^(-f/F)
|
|
198
|
+
// in hertz, with F growing with velocity. The envelope drives the waveshaper too: the harmonics come in one after another as the attack rises
|
|
199
|
+
// (Risset's trumpet analysis), with a brief bright blip, and the tone darkens as it dies away.
|
|
200
|
+
// An air jet blown across an edge (the flutes) is a nonlinear drive of the same kind: Fletcher measured that from piano to forte a flute's fundamental
|
|
201
|
+
// grows about 10 dB while its second and third harmonics grow 20-30 dB. Because the harmonics fall off in hertz, a low note is rich and a high one
|
|
202
|
+
// nearly a sine. The curve's odd and even parts can be weighted apart: x(1 + eCx) / (1 - C²x²) gives odd harmonics rⁿ and even ones e·rⁿ
|
|
203
|
+
// (e = 1 is x / (1 - Cx)), for a bore that favours the odd ones.
|
|
204
|
+
const VALVE_C = 0.99995;
|
|
205
|
+
/** Highest a used (the harmonics then fall 0.3 dB each, enough for a forte pedal note), and the drive that gives it */
|
|
206
|
+
const VALVE_A = 0.9995;
|
|
207
|
+
const VALVE_MAX = VALVE_A / VALVE_C;
|
|
208
|
+
/** The even harmonics' share, in the steps the curves are cached at */
|
|
209
|
+
const evenStep = (e) => Math.round(Math.min(1, Math.max(0, e)) * 20) / 20;
|
|
210
|
+
const valveTables = new Map();
|
|
211
|
+
/** The valve's waveshaper curve x(1 + eCx) / (1 - C²x²), e being the even harmonics' share */
|
|
212
|
+
function valveCurve(even) {
|
|
213
|
+
const e = evenStep(even);
|
|
214
|
+
let c = valveTables.get(e);
|
|
215
|
+
if (!c) {
|
|
216
|
+
// Fine enough to draw the sharp puffs of a loud low note
|
|
217
|
+
const n = 32768;
|
|
218
|
+
c = new Float32Array(n);
|
|
219
|
+
for (let i = 0; i < n; i++) {
|
|
220
|
+
const x = (i / (n - 1)) * 2 - 1;
|
|
221
|
+
c[i] = (x * (1 + e * VALVE_C * x)) / (1 - VALVE_C * VALVE_C * x * x);
|
|
222
|
+
}
|
|
223
|
+
valveTables.set(e, c);
|
|
224
|
+
}
|
|
225
|
+
return c;
|
|
226
|
+
}
|
|
227
|
+
/** Valve drive (input amplitude) for harmonics falling as e^(-f/F) */
|
|
228
|
+
function valveDrive(f0, F) {
|
|
229
|
+
const r = Math.exp(-f0 / Math.max(1, F));
|
|
230
|
+
return Math.min(VALVE_A, (2 * r) / (1 + r * r)) / VALVE_C;
|
|
231
|
+
}
|
|
232
|
+
/** Predicted RMS of the valve's output at drive A after a band-pass with first-order corners lo and hi (Hz) and an optional eq(f) gain, for level matching */
|
|
233
|
+
function valveRms(sr, f0, A, lo, hi, even = 1, eq) {
|
|
234
|
+
const a = A * VALVE_C;
|
|
235
|
+
const r = (1 - Math.sqrt(1 - a * a)) / a;
|
|
236
|
+
let p = 0;
|
|
237
|
+
let rn = 1;
|
|
238
|
+
for (let n = 1; n * f0 < sr * 0.45 && rn > 1e-4; n++) {
|
|
239
|
+
rn *= r;
|
|
240
|
+
const w = n * f0;
|
|
241
|
+
const h = (((lo + hi) * w) / Math.sqrt((w * w + lo * lo) * (w * w + hi * hi))) * (n % 2 ? 1 : evenStep(even)) * (eq ? eq(w) : 1);
|
|
242
|
+
p += rn * rn * h * h;
|
|
243
|
+
}
|
|
244
|
+
return Math.sqrt(p * 2) / (VALVE_C * Math.sqrt(1 - a * a)) || 1;
|
|
245
|
+
}
|
|
246
|
+
/** A first-order high-pass at lo times a first-order low-pass at hi, as one band-pass biquad (Q below 0.5: two real poles) */
|
|
247
|
+
function bandOf(ctx, lo, hi) {
|
|
248
|
+
return filt(ctx, 'bandpass', Math.sqrt(lo * hi), Math.sqrt(lo * hi) / (lo + hi));
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* One valve: a sine through the waveshaper. Its drive starts dark, reaches a bright blip at the end of the attack, settles, and falls back in the
|
|
252
|
+
* release. Soft notes sit where the drive is nearly linear, so the start and the release are also held to at least 40% and 70% of the settled drive.
|
|
253
|
+
*/
|
|
254
|
+
function valve(ctx, v) {
|
|
255
|
+
const { f, t0, F } = v;
|
|
256
|
+
const drive = (k) => Math.min(VALVE_MAX - (v.headroom ?? 0), valveDrive(f, F * k));
|
|
257
|
+
const s = osc(ctx, 'sine', f, t0, v.end);
|
|
258
|
+
const pre = gainNode(ctx, 0);
|
|
259
|
+
const p = pre.gain;
|
|
260
|
+
p.setValueAtTime(Math.max(drive(v.start ?? 0.3), drive(1) * 0.4), t0);
|
|
261
|
+
p.linearRampToValueAtTime(drive(v.blip ?? 1.1), t0 + v.att);
|
|
262
|
+
p.setTargetAtTime(drive(1), t0 + v.att, 0.08);
|
|
263
|
+
p.setTargetAtTime(Math.max(drive(0.5), drive(1) * 0.7), v.off, v.rel);
|
|
264
|
+
const ws = ctx.createWaveShaper();
|
|
265
|
+
ws.curve = valveCurve(v.even ?? 1);
|
|
266
|
+
s.connect(pre).connect(ws);
|
|
267
|
+
return { s, pre, out: ws };
|
|
268
|
+
}
|
|
269
|
+
/** When adsr starts the release */
|
|
270
|
+
const releaseAt = (t, dur, att) => t + Math.max(dur, Math.min(att, Math.max(0.003, dur * 0.9)) + 0.005);
|
|
271
|
+
// ───────────────────────── Woodwinds ─────────────────────────
|
|
272
|
+
/**
|
|
273
|
+
* Flute family: an air jet driving a pipe (see the valves above), so the low register is rich and the top nearly a sine, and the tone opens up with
|
|
274
|
+
* dynamics. F: how fast the harmonics fall at velocity 1 (Hz); even: the even harmonics' share (a pipe stopped at the bottom, like the pan pipes,
|
|
275
|
+
* has mostly odd ones); breath noise and the "chiff" of the attack; the pitch starts a little sharp; a delayed vibrato.
|
|
276
|
+
*/
|
|
277
|
+
function flute(F, even, breath, chiff, vibDepth, level) {
|
|
278
|
+
return (c) => {
|
|
279
|
+
const { ctx, t, f, vel, o } = c;
|
|
280
|
+
const a = o.attack ?? 0.05;
|
|
281
|
+
const r = o.release ?? 0.12;
|
|
282
|
+
const g = gainNode(ctx, 0);
|
|
283
|
+
const Fv = F * Math.pow(Math.max(0.05, vel), 1.3) * (o.bright ?? 1);
|
|
284
|
+
const norm = valveRms(ctx.sampleRate, f, valveDrive(f, Fv), 120, 9000, even);
|
|
285
|
+
const peak = (vel * level) / norm;
|
|
286
|
+
const end = adsr(g, t, c.dur, peak, a, 0.2, 0.88, r, peak * 1e-3);
|
|
287
|
+
const band = bandOf(ctx, 120, 9000);
|
|
288
|
+
band.connect(g).connect(c.out);
|
|
289
|
+
const v = valve(ctx, { f, t0: t, end, F: Fv, att: a, off: releaseAt(t, c.dur, a), rel: r, start: 0.6, blip: 1.05, even });
|
|
290
|
+
v.out.connect(band);
|
|
291
|
+
const s = v.s;
|
|
292
|
+
// The pitch starts slightly high and falls down to settle
|
|
293
|
+
s.detune.setValueAtTime(18, t);
|
|
294
|
+
s.detune.setTargetAtTime(0, t, 0.02);
|
|
295
|
+
// Breath noise and the attack "chiff" share one stretch of pre-filtered noise (scaled to sit against the tone as before: norm·√2)
|
|
296
|
+
const ng = gainNode(ctx, 0);
|
|
297
|
+
const k = norm * Math.SQRT2;
|
|
298
|
+
ng.gain.setValueAtTime(chiff * vel * 1.2 * k, t);
|
|
299
|
+
ng.gain.linearRampToValueAtTime(breath * (o.breath ?? 1) * k, t + 0.05);
|
|
300
|
+
breathSrc(ctx, t, end).connect(ng).connect(g);
|
|
301
|
+
vibrato(ctx, [s.detune], t, end, o.vibRate ?? 5.2, o.vib ?? vibDepth, o.vibDelay ?? 0.28);
|
|
302
|
+
applyBends(s.frequency, c, 1, o.glide ?? 0.06);
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Reed woodwinds (clarinet, oboe, bassoon): the reed is a valve (see above), so the tone opens up with dynamics. F: how fast the harmonics fall at
|
|
307
|
+
* velocity 1 and middle C (Hz); even(f): the even harmonics' share at pitch f; lo, hi: the radiation's rise and the roll-off (Hz); formants:
|
|
308
|
+
* [frequency, Q, gain dB]; perPitch: how much F follows the pitch (0: the fall-off is fixed in hertz; the clarinet's odd harmonics fall off
|
|
309
|
+
* more per harmonic than per hertz, roughly as a square wave's do below the cutoff).
|
|
310
|
+
* The clarinet's cylindrical bore resonates only at odd multiples below the cutoff of its open tone holes (about 1.5-2 kHz): its low register is
|
|
311
|
+
* mostly odd harmonics, and the even ones come back as the second harmonic nears the cutoff, in the upper register. The oboe's and bassoon's
|
|
312
|
+
* conical bores keep them all.
|
|
313
|
+
*/
|
|
314
|
+
function reed(F, even, lo, hi, formants, vibDepth, level, perPitch = 0) {
|
|
315
|
+
return (c) => {
|
|
316
|
+
const { ctx, t, f, vel, o } = c;
|
|
317
|
+
const att = o.attack ?? 0.035;
|
|
318
|
+
const rel = o.release ?? 0.1;
|
|
319
|
+
const e = even(f);
|
|
320
|
+
const g = gainNode(ctx, 0);
|
|
321
|
+
const Fv = F * Math.pow(Math.max(0.05, vel), 1.3) * Math.pow(f / 262, perPitch) * (o.bright ?? 1);
|
|
322
|
+
const peak = (vel * level) / valveRms(ctx.sampleRate, f, valveDrive(f, Fv), lo, hi, e);
|
|
323
|
+
const end = adsr(g, t, c.dur, peak, att, 0.15, 0.85, rel, peak * 1e-3);
|
|
324
|
+
const band = bandOf(ctx, lo, hi);
|
|
325
|
+
let node = band;
|
|
326
|
+
for (const [ff, q, gain] of formants)
|
|
327
|
+
node = node.connect(filt(ctx, 'peaking', ff, q, gain));
|
|
328
|
+
node.connect(g).connect(c.out);
|
|
329
|
+
const v = valve(ctx, { f, t0: t, end, F: Fv, att, off: releaseAt(t, c.dur, att), rel, even: e });
|
|
330
|
+
v.out.connect(band);
|
|
331
|
+
vibrato(ctx, [v.s.detune], t, end, 5, o.vib ?? vibDepth, 0.35);
|
|
332
|
+
applyBends(v.s.frequency, c, 1, o.glide ?? 0.05);
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
/** The clarinet's even harmonics: weak while the second harmonic sits below the tone-hole cutoff (about 1.7 kHz), returning above it */
|
|
336
|
+
const clarinetEven = (f) => {
|
|
337
|
+
const q = (2 * f) / 1700;
|
|
338
|
+
return 0.04 + (0.6 * q * q) / (1 + q * q);
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* Saxophone: a reed on a conical bore. The cone's resonances line up with every harmonic, so the reed's puffs keep them all (the clarinet's
|
|
342
|
+
* cylinder favours the odd ones), and the reed is the same kind of valve as a brass player's lips: mellow when soft, bright and edgy when loud.
|
|
343
|
+
* The body gives two formants (alto about 550 Hz and 1.5-2.5 kHz, tenor lower: f1 and f2) with a dip between them. The breath's edge grows
|
|
344
|
+
* with velocity, and a loud note growls a little: the throat roughens the airflow at about 30 Hz, which shakes the drive and so both the
|
|
345
|
+
* loudness and the brightness. Accented notes are tongued fast and scoop up from below; held notes get a late vibrato.
|
|
346
|
+
* o.growl scales the growl (0 for none) and o.breath the breath noise.
|
|
347
|
+
*/
|
|
348
|
+
function saxV(F, f1, f2, level) {
|
|
349
|
+
// The two formants as peaking filters (gain in dB): their response, for level matching across the range
|
|
350
|
+
const formant = (w, fc, q) => 9 / (1 + q * q * (w / fc - fc / w) ** 2);
|
|
351
|
+
const eq = (w) => Math.pow(10, (formant(w, f1, 1.6) + formant(w, f2, 1.3)) / 20);
|
|
352
|
+
return (c) => {
|
|
353
|
+
const { ctx, t, f, vel, o } = c;
|
|
354
|
+
const att = o.attack ?? 0.018;
|
|
355
|
+
const rel = o.release ?? 0.12;
|
|
356
|
+
const g = gainNode(ctx, 0);
|
|
357
|
+
const lo = f1 * 0.45;
|
|
358
|
+
const body = bandOf(ctx, lo, 5000);
|
|
359
|
+
body.connect(filt(ctx, 'peaking', f1, 1.6, 9)).connect(filt(ctx, 'peaking', f2, 1.3, 9)).connect(g).connect(c.out);
|
|
360
|
+
// The saxophone's tone changes more with dynamics than the others': mellow when soft, edgy when loud
|
|
361
|
+
const Fv = F * Math.pow(Math.max(0.05, vel), 1.6) * (o.bright ?? 1);
|
|
362
|
+
// How far into forte: sets the growl, the breath edge and the scoop
|
|
363
|
+
const loud = Math.min(1, Math.max(0, (vel - 0.72) / 0.28));
|
|
364
|
+
// Near the curve's pole a small change of drive is a large change of tone, so the growl takes a share of the room left below it
|
|
365
|
+
const A = valveDrive(f, Fv);
|
|
366
|
+
const growl = 0.6 * loud * Math.min(1, o.growl ?? 1) * (VALVE_MAX - A);
|
|
367
|
+
const norm = valveRms(ctx.sampleRate, f, Math.min(VALVE_MAX - growl, A), lo, 5000, 1, eq);
|
|
368
|
+
const peak = (vel * level) / norm;
|
|
369
|
+
const end = adsr(g, t, c.dur, peak, att, 0.2, 0.85, rel, peak * 1e-3);
|
|
370
|
+
const v = valve(ctx, { f, t0: t, end, F: Fv, att, off: releaseAt(t, c.dur, att), rel, blip: 1.15, headroom: growl });
|
|
371
|
+
v.out.connect(body);
|
|
372
|
+
// Scoop: an accented note starts up to 70 cents flat and is lipped up to pitch
|
|
373
|
+
v.s.detune.setValueAtTime(-10 - 60 * loud, t);
|
|
374
|
+
v.s.detune.setTargetAtTime(0, t + 0.01, 0.025);
|
|
375
|
+
if (growl > 0)
|
|
376
|
+
osc(ctx, 'sine', 27 + Math.random() * 7, t, end).connect(gainNode(ctx, growl)).connect(v.pre.gain);
|
|
377
|
+
// Breath: a puff as the tongue releases the reed, then the edge of the air in the mouthpiece, stronger the harder it is blown
|
|
378
|
+
const ng = gainNode(ctx, 0);
|
|
379
|
+
const edge = (o.breath ?? 1) * norm * 0.04 * (0.3 + loud);
|
|
380
|
+
ng.gain.setValueAtTime(edge * 4, t);
|
|
381
|
+
ng.gain.setTargetAtTime(edge, t + 0.01, 0.02);
|
|
382
|
+
breathSrc(ctx, t, end).connect(ng).connect(g);
|
|
383
|
+
vibrato(ctx, [v.s.detune], t, end, o.vibRate ?? 5.3, o.vib ?? 16, o.vibDelay ?? 0.4);
|
|
384
|
+
applyBends(v.s.frequency, c, 1, o.glide ?? 0.05);
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
// ───────────────────────── Brass ─────────────────────────
|
|
388
|
+
//
|
|
389
|
+
// Brass: a lip valve (see above), the bell and the mouthpiece cup. The bell radiates the low harmonics poorly (rising about 6 dB per octave up to
|
|
390
|
+
// its cutoff, then flat) and the cup rolls off the top: a first-order high-pass and a first-order low-pass. With the valve's fall-off they place
|
|
391
|
+
// the formant: around 1.2-1.5 kHz for the trumpet, 500-600 Hz for the trombone, 300-400 Hz for the horn (whose bell faces away from the
|
|
392
|
+
// listener and loses the highs above about 1.3 kHz), about 230 Hz for the tuba.
|
|
393
|
+
/**
|
|
394
|
+
* Brass. F: how fast the harmonics fall at velocity 1 (Hz, larger is brighter); lo, hi: the bell's radiation cutoff and the cup's low-pass (Hz);
|
|
395
|
+
* players: 1 for a soloist, more for a section; o.bright scales the brightness.
|
|
396
|
+
* A section's players are independent: each enters 4-30 ms apart, sits a few cents off and blows a little harder or softer,
|
|
397
|
+
* so the section shimmers rather than beating like phase-locked copies.
|
|
398
|
+
*/
|
|
399
|
+
function brassV(F, lo, hi, players, vibDepth, level) {
|
|
400
|
+
return (c) => {
|
|
401
|
+
const { ctx, t, f, vel, o } = c;
|
|
402
|
+
const att = o.attack ?? 0.045;
|
|
403
|
+
const rel = o.release ?? 0.14;
|
|
404
|
+
const g = gainNode(ctx, 0);
|
|
405
|
+
const bell = bandOf(ctx, lo, hi);
|
|
406
|
+
bell.connect(g).connect(c.out);
|
|
407
|
+
// Brightness: the harmonics' fall-off F grows with velocity
|
|
408
|
+
const Fv = F * Math.pow(Math.max(0.05, vel), 1.3) * (o.bright ?? 1);
|
|
409
|
+
const peak = (vel * level) / valveRms(ctx.sampleRate, f, valveDrive(f, Fv), lo, hi);
|
|
410
|
+
const end = adsr(g, t, c.dur, peak, att, 0.25, 0.8, rel, peak * 1e-3);
|
|
411
|
+
const off = releaseAt(t, c.dur, att);
|
|
412
|
+
const oscs = [];
|
|
413
|
+
for (let i = 0; i < players; i++) {
|
|
414
|
+
const solo = players === 1;
|
|
415
|
+
const t0 = t + (solo || i === 0 ? 0 : 0.004 + Math.random() * 0.026);
|
|
416
|
+
const d = solo ? 0 : (i / (players - 1) - 0.5) * 10 + (Math.random() - 0.5) * 6;
|
|
417
|
+
const v = valve(ctx, { f, t0, end, F: Fv * (solo ? 1 : 0.85 + Math.random() * 0.3), att, off, rel });
|
|
418
|
+
// The lips start a little flat and settle onto the note
|
|
419
|
+
v.s.detune.setValueAtTime(d - 25, t0);
|
|
420
|
+
v.s.detune.linearRampToValueAtTime(d, t0 + 0.04);
|
|
421
|
+
v.out.connect(solo ? bell : panned(ctx, bell, (i / (players - 1) - 0.5) * 0.5));
|
|
422
|
+
oscs.push(v.s);
|
|
423
|
+
}
|
|
424
|
+
vibrato(ctx, oscs.map((s) => s.detune), t, end, 5, o.vib ?? vibDepth, 0.35);
|
|
425
|
+
oscs.forEach((s) => applyBends(s.frequency, c, 1, o.glide ?? 0.07));
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const harmonics = (n, pow, odd = 1) => Array.from({ length: n }, (_, i) => ((i + 1) % 2 === 0 ? odd : 1) / Math.pow(i + 1, pow));
|
|
429
|
+
// ───────────────────────── Voices ─────────────────────────
|
|
430
|
+
/** Vowel formants: [frequency, bandwidth, gain] */
|
|
431
|
+
const VOWELS = {
|
|
432
|
+
a: [[800, 80, 1], [1150, 90, 0.5], [2900, 120, 0.1], [3900, 130, 0.06]],
|
|
433
|
+
o: [[450, 70, 1], [800, 80, 0.3], [2830, 100, 0.05]],
|
|
434
|
+
u: [[325, 50, 1], [700, 60, 0.15], [2530, 170, 0.03]],
|
|
435
|
+
e: [[400, 60, 1], [1600, 80, 0.25], [2700, 120, 0.12]],
|
|
436
|
+
i: [[300, 50, 1], [2200, 90, 0.2], [3000, 120, 0.15]],
|
|
437
|
+
/** Male voice */
|
|
438
|
+
A: [[650, 80, 1], [1080, 90, 0.5], [2650, 120, 0.2], [2900, 130, 0.12]],
|
|
439
|
+
O: [[400, 70, 1], [750, 80, 0.4], [2400, 100, 0.08]],
|
|
440
|
+
U: [[350, 60, 1], [600, 60, 0.2], [2400, 170, 0.03]],
|
|
441
|
+
};
|
|
442
|
+
/** Choir: vowel formants baked into the waveform; n voices, each detuned and with vibrato (two LFOs, one positive and one inverted), spread left and right */
|
|
443
|
+
function choirV(vowel, n, spread, vibDepth, breathy = 0) {
|
|
444
|
+
const fs = VOWELS[vowel].map(([F, bw, g]) => [F, bw * 3, g]);
|
|
445
|
+
return (c) => {
|
|
446
|
+
const { ctx, t, f, vel, o } = c;
|
|
447
|
+
const g = gainNode(ctx, 0);
|
|
448
|
+
const end = adsr(g, t, c.dur, vel * 0.5, o.attack ?? 0.25, 0.3, 0.9, o.release ?? 0.5);
|
|
449
|
+
g.connect(c.out);
|
|
450
|
+
const w = baked(ctx, `v${vowel}`, f, (k, hz) => peaks(hz, fs) / Math.pow(k, 0.6) + 0.002 / k, 7000);
|
|
451
|
+
const voices = [];
|
|
452
|
+
for (let i = 0; i < n; i++) {
|
|
453
|
+
const det = n === 1 ? 0 : (i / (n - 1) - 0.5) * 2 * spread;
|
|
454
|
+
const s = osc(ctx, w, f, t, end, det);
|
|
455
|
+
s.connect(panned(ctx, g, n === 1 ? 0 : (i / (n - 1) - 0.5) * 0.8));
|
|
456
|
+
applyBends(s.frequency, c, 1, o.glide ?? 0.12);
|
|
457
|
+
voices.push(s);
|
|
458
|
+
}
|
|
459
|
+
const depth = o.vib ?? vibDepth;
|
|
460
|
+
if (depth > 0) {
|
|
461
|
+
const l1 = osc(ctx, 'sine', 4.9 + Math.random() * 0.3, t, end);
|
|
462
|
+
const l2 = osc(ctx, 'sine', 5.6 + Math.random() * 0.3, t, end);
|
|
463
|
+
voices.forEach((v, i) => {
|
|
464
|
+
const lg = ctx.createGain();
|
|
465
|
+
lg.gain.setValueAtTime(0, t);
|
|
466
|
+
lg.gain.linearRampToValueAtTime(depth * (i % 3 === 2 ? -1 : 1), t + 0.5);
|
|
467
|
+
(i % 2 ? l2 : l1).connect(lg).connect(v.detune);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (breathy > 0) {
|
|
471
|
+
const nn = noiseSrc(ctx, t, end);
|
|
472
|
+
nn.connect(filt(ctx, 'bandpass', 2500, 0.8)).connect(gainNode(ctx, breathy)).connect(g);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/** Throat singing (khoomei): a sustained low note, with a narrow band-pass wandering along the harmonic series to sing a whistle-like second melody */
|
|
477
|
+
const throat = (c) => {
|
|
478
|
+
const { ctx, t, f, vel, o } = c;
|
|
479
|
+
const g = gainNode(ctx, 0);
|
|
480
|
+
const end = adsr(g, t, c.dur, vel * 0.5, o.attack ?? 0.3, 0.2, 0.9, o.release ?? 0.4);
|
|
481
|
+
g.connect(c.out);
|
|
482
|
+
const s = osc(ctx, 'sawtooth', f, t, end);
|
|
483
|
+
const s2 = osc(ctx, 'sawtooth', f, t, end, 5);
|
|
484
|
+
const src = gainNode(ctx, 0.5);
|
|
485
|
+
s.connect(src);
|
|
486
|
+
s2.connect(src);
|
|
487
|
+
// Chest resonance
|
|
488
|
+
src.connect(filt(ctx, 'bandpass', 420, 2)).connect(gainNode(ctx, 1.3)).connect(g);
|
|
489
|
+
src.connect(filt(ctx, 'lowpass', 300, 0.7)).connect(gainNode(ctx, 0.5)).connect(g);
|
|
490
|
+
// Whistle overtones: a narrow band-pass moving between the 6th and 12th harmonics
|
|
491
|
+
const bp = filt(ctx, 'bandpass', f * 8, 40);
|
|
492
|
+
const bp2 = filt(ctx, 'bandpass', f * 8, 40);
|
|
493
|
+
src.connect(bp).connect(bp2).connect(gainNode(ctx, 22 * (o.whistle ?? 1))).connect(g);
|
|
494
|
+
const seq = [8, 9, 10, 9, 8, 12, 10, 8];
|
|
495
|
+
const step = Math.max(0.35, c.dur / seq.length);
|
|
496
|
+
let tt = t;
|
|
497
|
+
for (let i = 0; tt < end; i++, tt += step) {
|
|
498
|
+
const h = seq[i % seq.length] * f;
|
|
499
|
+
bp.frequency.setTargetAtTime(h, tt, 0.06);
|
|
500
|
+
bp2.frequency.setTargetAtTime(h, tt, 0.06);
|
|
501
|
+
}
|
|
502
|
+
vibrato(ctx, [s.detune, s2.detune], t, end, 4.4, 4, 0.5);
|
|
503
|
+
};
|
|
504
|
+
// ───────────────────────── Keyboards and reed organs ─────────────────────────
|
|
505
|
+
/** Organ: stops (overtones) + the airflow noise of the attack; o.full=1 opens all the stops (adds the 16' and mixture stops) */
|
|
506
|
+
const organ = (c) => {
|
|
507
|
+
const { ctx, t, f, vel, o } = c;
|
|
508
|
+
const full = (o.full ?? 0) > 0;
|
|
509
|
+
const g = gainNode(ctx, 0);
|
|
510
|
+
const end = adsr(g, t, c.dur, vel * 0.14, 0.04, 0, 1, o.release ?? 0.3);
|
|
511
|
+
const lp = filt(ctx, 'lowpass', full ? 9000 : 5000, 0.5);
|
|
512
|
+
lp.connect(g).connect(c.out);
|
|
513
|
+
const flue = full ? [1, 0.8, 0.45, 0.6, 0, 0.35, 0, 0.3, 0, 0, 0, 0.18, 0, 0, 0, 0.12] : [1, 0.55, 0.2, 0.3, 0, 0.12, 0, 0.1];
|
|
514
|
+
const w = wave(ctx, full ? 'orgF' : 'orgP', flue);
|
|
515
|
+
osc(ctx, w, f, t, end).connect(lp);
|
|
516
|
+
// A slightly detuned second rank of stops (celeste)
|
|
517
|
+
osc(ctx, w, f, t, end, 3).connect(panned(ctx, lp, 0.3));
|
|
518
|
+
if (full)
|
|
519
|
+
osc(ctx, 'sine', f / 2, t, end).connect(gainNode(ctx, 0.7)).connect(lp);
|
|
520
|
+
const cn = noiseSrc(ctx, t, t + 0.12);
|
|
521
|
+
const cg = gainNode(ctx, 0);
|
|
522
|
+
cg.gain.setValueAtTime(0.1 * vel, t);
|
|
523
|
+
fadeTo0(cg.gain, t + 0.01, 0.025);
|
|
524
|
+
cn.connect(filt(ctx, 'bandpass', f * 3, 2)).connect(cg).connect(c.out);
|
|
525
|
+
};
|
|
526
|
+
/** Accordion (musette): two ranks of reeds detuned against each other beat in a "wah-wah", plus the bellows' dynamics */
|
|
527
|
+
const accordion = (c) => {
|
|
528
|
+
const { ctx, t, f, vel, o } = c;
|
|
529
|
+
const g = gainNode(ctx, 0);
|
|
530
|
+
const end = adsr(g, t, c.dur, vel * 0.1, o.attack ?? 0.03, 0.2, 0.85, o.release ?? 0.08);
|
|
531
|
+
const bp = filt(ctx, 'peaking', 1400, 1, 5);
|
|
532
|
+
const lp = filt(ctx, 'lowpass', 4200, 0.6);
|
|
533
|
+
bp.connect(lp).connect(g).connect(c.out);
|
|
534
|
+
const w = wave(ctx, 'reed', harmonics(14, 0.75));
|
|
535
|
+
for (const d of [-(o.musette ?? 13), 0, o.musette ?? 13])
|
|
536
|
+
osc(ctx, w, f, t, end, d).connect(bp);
|
|
537
|
+
};
|
|
538
|
+
/** Bagpipe / drone: a narrow-band sawtooth, very long and steady */
|
|
539
|
+
const drone = (c) => {
|
|
540
|
+
const { ctx, t, f, vel, o } = c;
|
|
541
|
+
const g = gainNode(ctx, 0);
|
|
542
|
+
const end = adsr(g, t, c.dur, vel * 0.12, o.attack ?? 0.4, 0, 1, o.release ?? 0.6);
|
|
543
|
+
const bp = filt(ctx, 'bandpass', f * 3, 0.9);
|
|
544
|
+
const lp = filt(ctx, 'lowpass', 2500, 0.5);
|
|
545
|
+
bp.connect(lp).connect(g).connect(c.out);
|
|
546
|
+
osc(ctx, 'sawtooth', f, t, end).connect(bp);
|
|
547
|
+
osc(ctx, 'sawtooth', f * 1.5, t, end, 2).connect(gainNode(ctx, 0.5)).connect(bp);
|
|
548
|
+
};
|
|
549
|
+
// ───────────────────────── Synthesizers ─────────────────────────
|
|
550
|
+
//
|
|
551
|
+
// Subtractive synthesis as analog polysynths do it. The oscillators run free: each note starts them at a random point of their cycle and a cent
|
|
552
|
+
// or two off pitch, so repeated notes are never quite the same. The filter is a four-pole low-pass (24 dB/oct, like a Moog or Roland transistor
|
|
553
|
+
// ladder) whose cutoff follows the keyboard and whose envelope sweeps it in octaves, as an exponential converter does, opening further the harder
|
|
554
|
+
// a note is played. A stereo voice is two channel strips, left and right, each a mono filter and amplifier.
|
|
555
|
+
// Every note unplugs itself from its destination when it ends: Chrome keeps processing a finished note's nodes for as long as they stay connected
|
|
556
|
+
// (until garbage collection reaches them), and a song that left its notes plugged in grew heavier with every note (ten times the CPU by bar 16).
|
|
557
|
+
const SAW = harmonics(1024, 1);
|
|
558
|
+
const SQUARE = harmonics(1024, 1, 0);
|
|
559
|
+
/** An oscillator that runs free: it starts at one of 16 points of its cycle and up to `wobble` cents off pitch, both at random */
|
|
560
|
+
function vco(ctx, kind, f, t, end, cents = 0, wobble = 2) {
|
|
561
|
+
const phase = Math.floor(Math.random() * 16) / 16;
|
|
562
|
+
return osc(ctx, wave(ctx, kind, kind === 'saw' ? SAW : SQUARE, phase), f, t, end, cents + (Math.random() - 0.5) * 2 * wobble);
|
|
563
|
+
}
|
|
564
|
+
/** A four-pole low-pass (24 dB/oct, like a transistor ladder): a gentle biquad into one carrying the resonance q (dB) */
|
|
565
|
+
function ladder(ctx, cut, q) {
|
|
566
|
+
const a = filt(ctx, 'lowpass', cut, -5.4);
|
|
567
|
+
const b = filt(ctx, 'lowpass', cut, q);
|
|
568
|
+
a.connect(b);
|
|
569
|
+
return [a, b];
|
|
570
|
+
}
|
|
571
|
+
/** A cutoff that follows the keyboard: cut (Hz) at the pitch ref, moving `track` octaves for every octave played */
|
|
572
|
+
const keyCut = (cut, f, ref, track = 0.5) => Math.min(20000, cut * Math.pow(f / ref, track));
|
|
573
|
+
/** Overdrive curve (each computed only once) */
|
|
574
|
+
const curves = new Map();
|
|
575
|
+
function tanhCurve(drive, n, norm = true) {
|
|
576
|
+
const k = `${drive}|${n}|${norm}`;
|
|
577
|
+
let c = curves.get(k);
|
|
578
|
+
if (!c) {
|
|
579
|
+
c = new Float32Array(n);
|
|
580
|
+
for (let i = 0; i < n; i++)
|
|
581
|
+
c[i] = Math.tanh(((i / (n - 1)) * 2 - 1) * drive) / (norm ? Math.tanh(drive) : 1);
|
|
582
|
+
curves.set(k, c);
|
|
583
|
+
}
|
|
584
|
+
return c;
|
|
585
|
+
}
|
|
586
|
+
/** Soft clipping (tanh) at the given drive, keeping a full-scale peak at full scale: 1 rounds the peaks, 4 is a crunchy overdrive */
|
|
587
|
+
function saturator(ctx, drive) {
|
|
588
|
+
const ws = ctx.createWaveShaper();
|
|
589
|
+
ws.curve = tanhCurve(drive, 1024);
|
|
590
|
+
return ws;
|
|
591
|
+
}
|
|
592
|
+
/** One channel strip of a synth voice, mono from end to end: input level, four-pole low-pass, soft clipping and high-pass if asked, amplifier, pan */
|
|
593
|
+
function strip(ctx, out, s) {
|
|
594
|
+
const input = gainNode(ctx, s.level);
|
|
595
|
+
const lp = ladder(ctx, s.cut, s.q);
|
|
596
|
+
const amp = gainNode(ctx, 0);
|
|
597
|
+
let node = input.connect(lp[0]).connect(lp[1]);
|
|
598
|
+
if (s.drive)
|
|
599
|
+
node = node.connect(saturator(ctx, s.drive));
|
|
600
|
+
if (s.hp)
|
|
601
|
+
node = node.connect(filt(ctx, 'highpass', s.hp, -3));
|
|
602
|
+
const pan = panned(ctx, out, s.pan);
|
|
603
|
+
node.connect(amp).connect(pan);
|
|
604
|
+
return { input, lp, amp, out: pan === out ? amp : pan };
|
|
605
|
+
}
|
|
606
|
+
/** A stereo voice's strips, left and right */
|
|
607
|
+
const stereo = (ctx, out, s) => [-1, 1].map((pan) => strip(ctx, out, { ...s, pan }));
|
|
608
|
+
/** The amplifier envelope (adsr) on every strip; returns when the sound ends */
|
|
609
|
+
function amps(strips, t, dur, peak, a, d, s, r) {
|
|
610
|
+
let end = t;
|
|
611
|
+
for (const st of strips)
|
|
612
|
+
end = adsr(st.amp, t, dur, peak, a, d, s, r);
|
|
613
|
+
return end;
|
|
614
|
+
}
|
|
615
|
+
/** Filter envelope on every strip, in octaves on the filters' detune: up oct octaves over att seconds, then back toward sus of that with time constant dec */
|
|
616
|
+
function sweep(strips, t, oct, att, dec, sus = 0) {
|
|
617
|
+
for (const { lp } of strips) {
|
|
618
|
+
for (const { detune: p } of lp) {
|
|
619
|
+
p.setValueAtTime(0, t);
|
|
620
|
+
p.linearRampToValueAtTime(oct * 1200, t + att);
|
|
621
|
+
p.setTargetAtTime(oct * 1200 * sus, t + att, dec);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/** Unplug a note's nodes from its destination once its oscillator src has stopped (see above) */
|
|
626
|
+
function unplug(src, nodes) {
|
|
627
|
+
src.onended = () => {
|
|
628
|
+
for (const n of nodes)
|
|
629
|
+
n.disconnect();
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
/** A gain feeding every strip: the centre of a stereo voice */
|
|
633
|
+
function centre(ctx, strips, level) {
|
|
634
|
+
const g = gainNode(ctx, level);
|
|
635
|
+
for (const s of strips)
|
|
636
|
+
g.connect(s.input);
|
|
637
|
+
return g;
|
|
638
|
+
}
|
|
639
|
+
/** The JP-8000's six side saws: how far each sits from the pitch at full Detune, as a fraction of it (Szabo's measurements) */
|
|
640
|
+
const SUPERSAW = [-0.11002313, -0.06288439, -0.01952356, 0.01991221, 0.06216538, 0.10745242];
|
|
641
|
+
/** Szabo's fit of the Detune knob (0-1) to the amount of detune, highest power first: gentle over most of the travel, steep at the top */
|
|
642
|
+
const SUPERSAW_DETUNE = [10028.7312891634, -50818.8652045924, 111363.4808729368, -138150.6761080548, 106649.6679158292, -53046.9642751875, 17019.951858008, -3425.0836591318, 404.2703938388, -24.1878824391, 0.6717417634, 0.0030115596];
|
|
643
|
+
/**
|
|
644
|
+
* Supersaw (Roland JP-8000), after Adam Szabo's measurements ("How to Emulate the Super Saw", 2010): seven free-running saws, the six side ones
|
|
645
|
+
* detuned along the knob's curve and coming up as the centre one fades (Mix), and a high-pass at the fundamental that keeps the stack from turning
|
|
646
|
+
* to mud. The side saws alternate left and right (each side gets flat and sharp ones), through a four-pole low-pass with a short sweep for bite.
|
|
647
|
+
* Trance leads and big-room chord stabs. o.detune 0-1 (default 0.6), o.mix 0-1 (default 0.75), o.cut (Hz around C5, default 7000), o.q, o.attack, o.release, o.glide, o.vib.
|
|
648
|
+
*/
|
|
649
|
+
const supersaw = (c) => {
|
|
650
|
+
const { ctx, t, f, vel, touch, o } = c;
|
|
651
|
+
const x = Math.min(1, Math.max(0, o.detune ?? 0.6));
|
|
652
|
+
const amount = SUPERSAW_DETUNE.reduce((a, k) => a * x + k, 0);
|
|
653
|
+
const mix = Math.min(1, Math.max(0, o.mix ?? 0.75));
|
|
654
|
+
const mid = 0.99785 - 0.55366 * mix;
|
|
655
|
+
const side = -0.73764 * mix * mix + 1.2841 * mix + 0.044372;
|
|
656
|
+
// Each side hears the centre saw and three side ones: hold its power whatever the mix
|
|
657
|
+
const level = side / Math.sqrt(mid * mid + 3 * side * side);
|
|
658
|
+
const S = stereo(ctx, c.out, { level, cut: keyCut(o.cut ?? 7000, f, 523), q: o.q ?? 0, hp: f });
|
|
659
|
+
const end = amps(S, t, c.dur, vel * 0.2, o.attack ?? 0.003, 0.5, 0.7, o.release ?? 0.25);
|
|
660
|
+
sweep(S, t, 1.5 * touch, 0.003, 0.15);
|
|
661
|
+
const middle = centre(ctx, S, mid / side);
|
|
662
|
+
const saws = [0, ...SUPERSAW].map((r, i) => {
|
|
663
|
+
const s = vco(ctx, 'saw', f, t, end, 1200 * Math.log2(1 + r * amount), 1);
|
|
664
|
+
s.connect(i ? S[i % 2].input : middle);
|
|
665
|
+
applyBends(s.frequency, c, 1, o.glide ?? 0.08);
|
|
666
|
+
return s;
|
|
667
|
+
});
|
|
668
|
+
vibrato(ctx, saws.map((s) => s.detune), t, end, 5.5, o.vib ?? 0, 0.25);
|
|
669
|
+
unplug(saws[0], S.map((s) => s.out));
|
|
670
|
+
};
|
|
671
|
+
/**
|
|
672
|
+
* Synth lead, a Minimoog-style monosynth voice: a saw in the centre and two more a few cents either side (left and right) over a square an octave
|
|
673
|
+
* down, a four-pole low-pass whose envelope opens further the harder a note is played, a soft-clipping output stage, a slow analog drift, glide
|
|
674
|
+
* and a delayed vibrato. o.cut (Hz around C5, default 3200), o.q (resonance dB, default 3), o.env (filter decay s, default 0.25), o.vib (cents, default 12), o.attack, o.release, o.glide.
|
|
675
|
+
*/
|
|
676
|
+
const synthLead = (c) => {
|
|
677
|
+
const { ctx, t, f, vel, touch, o } = c;
|
|
678
|
+
const S = stereo(ctx, c.out, { level: 0.3, cut: keyCut(o.cut ?? 3200, f, 523), q: o.q ?? 3, drive: 2 });
|
|
679
|
+
const end = amps(S, t, c.dur, vel * 0.3, o.attack ?? 0.005, 0.3, 0.8, o.release ?? 0.12);
|
|
680
|
+
sweep(S, t, 1 + 1.5 * touch, 0.004, o.env ?? 0.25);
|
|
681
|
+
const middle = centre(ctx, S, 1.3);
|
|
682
|
+
const oscs = [vco(ctx, 'saw', f, t, end), vco(ctx, 'square', f, t, end, -1200), vco(ctx, 'saw', f, t, end, -8), vco(ctx, 'saw', f, t, end, 8)];
|
|
683
|
+
oscs[0].connect(middle);
|
|
684
|
+
oscs[1].connect(gainNode(ctx, 0.4)).connect(middle);
|
|
685
|
+
oscs[2].connect(S[0].input);
|
|
686
|
+
oscs[3].connect(S[1].input);
|
|
687
|
+
vibrato(ctx, oscs.map((s) => s.detune), t, end, 5.6, o.vib ?? 12, 0.3);
|
|
688
|
+
vibrato(ctx, [oscs[2].detune, oscs[3].detune], t, end, 0.1 + Math.random() * 0.3, 3, 0);
|
|
689
|
+
for (const s of oscs)
|
|
690
|
+
applyBends(s.frequency, c, 1, o.glide ?? 0.06);
|
|
691
|
+
unplug(oscs[0], S.map((s) => s.out));
|
|
692
|
+
};
|
|
693
|
+
/**
|
|
694
|
+
* Pluck, the short bright notes of trance and house arpeggios: three saws (centre, a few cents flat left and sharp right) into a four-pole low-pass
|
|
695
|
+
* whose envelope snaps shut, harder notes opening it further, and an amplifier that dies away with no sustain.
|
|
696
|
+
* o.cut (Hz the filter closes to, around C5, default 500), o.env (filter decay s, default 0.1), o.q (resonance dB, default 5), o.release (default 0.3).
|
|
697
|
+
*/
|
|
698
|
+
const pluck = (c) => {
|
|
699
|
+
const { ctx, t, f, vel, touch, o } = c;
|
|
700
|
+
const S = stereo(ctx, c.out, { level: 0.35, cut: keyCut(o.cut ?? 500, f, 523, 0.8), q: o.q ?? 5 });
|
|
701
|
+
const end = amps(S, t, c.dur, vel * 0.3, 0.002, 0.7, 0, o.release ?? 0.3);
|
|
702
|
+
sweep(S, t, 2.5 + 2 * touch, 0.002, o.env ?? 0.1);
|
|
703
|
+
const mid = vco(ctx, 'saw', f, t, end);
|
|
704
|
+
mid.connect(centre(ctx, S, 1.3));
|
|
705
|
+
vco(ctx, 'saw', f, t, end, -10).connect(S[0].input);
|
|
706
|
+
vco(ctx, 'saw', f, t, end, 10).connect(S[1].input);
|
|
707
|
+
unplug(mid, S.map((s) => s.out));
|
|
708
|
+
};
|
|
709
|
+
/**
|
|
710
|
+
* Analog pad in the manner of a Juno-60: a saw in the centre and two more left and right whose pitch one slow triangle sweeps in opposite
|
|
711
|
+
* directions (what the Juno's bucket-brigade chorus does to its delayed copies, about ±7 cents), into a four-pole low-pass that breathes
|
|
712
|
+
* slowly around its cutoff. cut: the cutoff (Hz at middle C); sub: the level of a sine an octave down. o.cut, o.attack (default 0.9), o.release (default 1.2).
|
|
713
|
+
*/
|
|
714
|
+
function analogPad(cut, sub) {
|
|
715
|
+
return (c) => {
|
|
716
|
+
const { ctx, t, f, vel, o } = c;
|
|
717
|
+
const S = stereo(ctx, c.out, { level: 0.4, cut: keyCut(o.cut ?? cut, f, 262), q: 1 });
|
|
718
|
+
const end = amps(S, t, c.dur, vel * 0.2, o.attack ?? 0.9, 0, 1, o.release ?? 1.2);
|
|
719
|
+
const mid = vco(ctx, 'saw', f, t, end);
|
|
720
|
+
mid.connect(centre(ctx, S, 1));
|
|
721
|
+
unplug(mid, S.map((s) => s.out));
|
|
722
|
+
const [left, right] = [vco(ctx, 'saw', f, t, end, -4), vco(ctx, 'saw', f, t, end, 4)];
|
|
723
|
+
left.connect(S[0].input);
|
|
724
|
+
right.connect(S[1].input);
|
|
725
|
+
const chorus = osc(ctx, 'triangle', 0.5 + Math.random() * 0.4, t, end);
|
|
726
|
+
chorus.connect(gainNode(ctx, 7)).connect(left.detune);
|
|
727
|
+
chorus.connect(gainNode(ctx, -7)).connect(right.detune);
|
|
728
|
+
// Breathing: the cutoff drifts a third of an octave either way
|
|
729
|
+
const breath = gainNode(ctx, 400);
|
|
730
|
+
osc(ctx, 'sine', 0.08 + Math.random() * 0.08, t, end).connect(breath);
|
|
731
|
+
for (const s of S)
|
|
732
|
+
for (const { detune } of s.lp)
|
|
733
|
+
breath.connect(detune);
|
|
734
|
+
if (sub) {
|
|
735
|
+
const s = osc(ctx, 'sine', f / 2, t, end).connect(gainNode(ctx, sub * 0.4));
|
|
736
|
+
for (const st of S)
|
|
737
|
+
s.connect(st.amp);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
/** Glass pad: a sine frequency-modulated by its octave, its brightness breathing slowly, and a shimmer two octaves up on the right */
|
|
742
|
+
const glassPad = (c) => {
|
|
743
|
+
const { ctx, t, f, vel, o } = c;
|
|
744
|
+
const g = gainNode(ctx, 0);
|
|
745
|
+
const end = adsr(g, t, c.dur, vel * 0.16, o.attack ?? 0.9, 0, 1, o.release ?? 1.2);
|
|
746
|
+
g.connect(c.out);
|
|
747
|
+
const s = osc(ctx, 'sine', f, t, end);
|
|
748
|
+
const m = osc(ctx, 'sine', f * 2.001, t, end);
|
|
749
|
+
const mg = gainNode(ctx, f * 0.6);
|
|
750
|
+
m.connect(mg).connect(s.frequency);
|
|
751
|
+
const sh = osc(ctx, 'sine', f * 4, t, end, 6);
|
|
752
|
+
s.connect(panned(ctx, g, -0.3));
|
|
753
|
+
sh.connect(gainNode(ctx, 0.12)).connect(panned(ctx, g, 0.3));
|
|
754
|
+
osc(ctx, 'sine', 0.18 + Math.random() * 0.1, t, end).connect(gainNode(ctx, f * 0.4)).connect(mg.gain);
|
|
755
|
+
unplug(s, [g]);
|
|
756
|
+
};
|
|
757
|
+
/** Air pad: noise through narrow band-passes at the note and its octave, a different stretch of noise on each side */
|
|
758
|
+
const airPad = (c) => {
|
|
759
|
+
const { ctx, t, f, vel, o } = c;
|
|
760
|
+
for (const pan of [-1, 1]) {
|
|
761
|
+
const g = gainNode(ctx, 0);
|
|
762
|
+
const end = adsr(g, t, c.dur, vel * 0.64, o.attack ?? 0.9, 0, 1, o.release ?? 1.2);
|
|
763
|
+
const side = panned(ctx, c.out, pan);
|
|
764
|
+
g.connect(side);
|
|
765
|
+
const n = noiseSrc(ctx, t, end);
|
|
766
|
+
n.connect(filt(ctx, 'bandpass', f, 60)).connect(gainNode(ctx, 1.2)).connect(g);
|
|
767
|
+
n.connect(filt(ctx, 'bandpass', f * 2, 60)).connect(gainNode(ctx, 0.5)).connect(g);
|
|
768
|
+
unplug(n, [side]);
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
/** Glass harmonica: two nearly unison sines beat against each other, slow attack */
|
|
772
|
+
const glass = (c) => {
|
|
773
|
+
const { ctx, t, f, vel, o } = c;
|
|
774
|
+
const g = gainNode(ctx, 0);
|
|
775
|
+
const end = adsr(g, t, c.dur, vel * 0.22, o.attack ?? 0.18, 0.5, 0.7, o.release ?? 0.8);
|
|
776
|
+
g.connect(c.out);
|
|
777
|
+
const s = osc(ctx, 'sine', f, t, end);
|
|
778
|
+
s.connect(panned(ctx, g, -0.25));
|
|
779
|
+
osc(ctx, 'sine', f, t, end, 7).connect(panned(ctx, g, 0.25));
|
|
780
|
+
osc(ctx, 'sine', f * 2, t, end, 3).connect(gainNode(ctx, 0.18)).connect(g);
|
|
781
|
+
unplug(s, [g]);
|
|
782
|
+
};
|
|
783
|
+
/** Distorted guitar: root + fifth + octave into overdrive, then through a cabinet; o.mute=1 is palm muting */
|
|
784
|
+
const powerGtr = (c) => {
|
|
785
|
+
const { ctx, t, f, vel, o } = c;
|
|
786
|
+
const mute = (o.mute ?? 0) > 0;
|
|
787
|
+
const g = gainNode(ctx, 0);
|
|
788
|
+
const end = mute ? adsr(g, t, Math.min(c.dur, 0.12), vel * 0.14, 0.004, 0.08, 0.3, 0.06) : adsr(g, t, c.dur, vel * 0.13, 0.005, 0.8, 0.6, 0.12);
|
|
789
|
+
const ws = ctx.createWaveShaper();
|
|
790
|
+
ws.curve = tanhCurve(o.drive ?? 7, 1024);
|
|
791
|
+
const pre = gainNode(ctx, 0.5);
|
|
792
|
+
const cab = filt(ctx, 'lowpass', mute ? 1500 : 3400, 1.1);
|
|
793
|
+
const hp = filt(ctx, 'highpass', 90, 0.7);
|
|
794
|
+
const mid = filt(ctx, 'peaking', 750, 1, -5);
|
|
795
|
+
pre.connect(ws).connect(hp).connect(mid).connect(cab).connect(g).connect(c.out);
|
|
796
|
+
const saws = [[1, -5], [1.5, 4], [2, 3], [1, 8]].map(([ratio, det]) => osc(ctx, 'sawtooth', f * ratio, t, end, det));
|
|
797
|
+
for (const s of saws)
|
|
798
|
+
s.connect(pre);
|
|
799
|
+
unplug(saws[0], [g]);
|
|
800
|
+
};
|
|
801
|
+
// ───────────────────────── Bass ─────────────────────────
|
|
802
|
+
//
|
|
803
|
+
// The synth basses are mono below 120 Hz (the reese's wide saws are high-passed above it, and a clean sine carries its bottom): a club system
|
|
804
|
+
// needs one centred low end, and a phone speaker sums the two sides anyway.
|
|
805
|
+
/** Fingerstyle bass: sine + triangle, a plucked-string low-pass envelope */
|
|
806
|
+
const fingerBass = (c) => {
|
|
807
|
+
const { ctx, t, f, vel, o } = c;
|
|
808
|
+
const g = gainNode(ctx, 0);
|
|
809
|
+
const end = adsr(g, t, c.dur, vel * 0.5, 0.006, 0.6, 0.55, o.release ?? 0.08);
|
|
810
|
+
const lp = filt(ctx, 'lowpass', 400, 1);
|
|
811
|
+
lp.frequency.setValueAtTime(900 + vel * 900, t);
|
|
812
|
+
lp.frequency.setTargetAtTime(350, t, 0.12);
|
|
813
|
+
lp.connect(g).connect(c.out);
|
|
814
|
+
const s = osc(ctx, 'sine', f, t, end);
|
|
815
|
+
s.connect(lp);
|
|
816
|
+
osc(ctx, 'triangle', f, t, end).connect(gainNode(ctx, 0.6)).connect(lp);
|
|
817
|
+
osc(ctx, 'sawtooth', f, t, end).connect(gainNode(ctx, 0.12)).connect(lp);
|
|
818
|
+
unplug(s, [g]);
|
|
819
|
+
};
|
|
820
|
+
/**
|
|
821
|
+
* Synth bass, the off-beat bass of house and trance: two saws a few cents apart and a square through a four-pole low-pass whose envelope goes
|
|
822
|
+
* "bwow" (harder notes open it further) and a soft clipper for weight, over a clean sine at the fundamental.
|
|
823
|
+
* o.cut (Hz the filter settles to at C2, following the keyboard; default 180), o.q (resonance dB, default 6), o.env (filter decay s, default 0.08), o.release, o.glide.
|
|
824
|
+
*/
|
|
825
|
+
const synthBass = (c) => {
|
|
826
|
+
const { ctx, t, f, vel, touch, o } = c;
|
|
827
|
+
const S = [strip(ctx, c.out, { pan: 0, level: 0.35, cut: keyCut(o.cut ?? 180, f, 65.4, 1), q: o.q ?? 6, drive: 2 })];
|
|
828
|
+
const end = amps(S, t, c.dur, vel * 0.5, 0.003, 0.3, 0.75, o.release ?? 0.06);
|
|
829
|
+
sweep(S, t, 2 + 2 * touch, 0.003, o.env ?? 0.08);
|
|
830
|
+
const oscs = [vco(ctx, 'saw', f, t, end, -6), vco(ctx, 'saw', f, t, end, 6), vco(ctx, 'square', f, t, end)];
|
|
831
|
+
for (const s of oscs)
|
|
832
|
+
s.connect(S[0].input);
|
|
833
|
+
const sine = osc(ctx, 'sine', f, t, end);
|
|
834
|
+
sine.connect(gainNode(ctx, 0.5)).connect(S[0].amp);
|
|
835
|
+
for (const s of [...oscs, sine])
|
|
836
|
+
applyBends(s.frequency, c, 1, o.glide ?? 0.07);
|
|
837
|
+
unplug(sine, [S[0].out]);
|
|
838
|
+
};
|
|
839
|
+
/**
|
|
840
|
+
* Reese bass (after Kevin Saunderson's "Just Want Another Chance", 1988): saws detuned against each other, whose slowly drifting beats make the
|
|
841
|
+
* note churn. A close pair in the centre and a wider pair left and right, through a four-pole low-pass, soft clipping and a high-pass that leaves
|
|
842
|
+
* the bottom to a clean mono sine. o.detune (cents, default 16), o.cut (Hz, default 1600), o.q, o.attack, o.release, o.glide.
|
|
843
|
+
*/
|
|
844
|
+
const reese = (c) => {
|
|
845
|
+
const { ctx, t, f, vel, o } = c;
|
|
846
|
+
const d = o.detune ?? 16;
|
|
847
|
+
const S = stereo(ctx, c.out, { level: 0.3, cut: o.cut ?? 1600, q: o.q ?? 2, drive: 2.5, hp: 160 });
|
|
848
|
+
const end = amps(S, t, c.dur, vel * 0.5, o.attack ?? 0.01, 0.5, 0.85, o.release ?? 0.12);
|
|
849
|
+
const middle = centre(ctx, S, 1);
|
|
850
|
+
const saws = [[-d / 3, middle], [d / 3, middle], [-d, S[0].input], [d, S[1].input]].map(([cents, to]) => {
|
|
851
|
+
const s = vco(ctx, 'saw', f, t, end, cents);
|
|
852
|
+
s.connect(to);
|
|
853
|
+
return s;
|
|
854
|
+
});
|
|
855
|
+
// The beats drift: one saw of each pair wanders by a third of the detune
|
|
856
|
+
vibrato(ctx, [saws[0].detune, saws[2].detune], t, end, 0.15 + Math.random() * 0.2, d / 3, 0);
|
|
857
|
+
const sub = osc(ctx, 'sine', f, t, end);
|
|
858
|
+
const subLevel = gainNode(ctx, 0.6);
|
|
859
|
+
sub.connect(subLevel);
|
|
860
|
+
for (const s of S)
|
|
861
|
+
subLevel.connect(s.amp);
|
|
862
|
+
for (const s of [...saws, sub])
|
|
863
|
+
applyBends(s.frequency, c, 1, o.glide ?? 0.08);
|
|
864
|
+
unplug(sub, S.map((s) => s.out));
|
|
865
|
+
};
|
|
866
|
+
/**
|
|
867
|
+
* Acid bass (Roland TB-303): one saw (o.square = 1: a square) into a resonant low-pass swept down from every note, with no keyboard tracking.
|
|
868
|
+
* An accented note (velocity above 0.9: ! in a melody) is louder and its sweep higher, shorter and rounded at the top (the 303's accent circuit),
|
|
869
|
+
* which sets the resonance squealing; a glide (>) slides into the next note in about 60 ms without restarting the envelopes, like the 303's
|
|
870
|
+
* Slide. Soft clipping on the output, as acid lines are usually overdriven, and the output's coupling high-pass.
|
|
871
|
+
* o.cut (Hz, default 300), o.q (resonance dB, default 15), o.env (filter decay s, default 0.3), o.square, o.glide, o.release.
|
|
872
|
+
*/
|
|
873
|
+
const acid = (c) => {
|
|
874
|
+
const { ctx, t, f, vel, touch, o } = c;
|
|
875
|
+
const accent = touch > 0.9;
|
|
876
|
+
const S = [strip(ctx, c.out, { pan: 0, level: 0.5, cut: o.cut ?? 300, q: o.q ?? 15, drive: 2.5, hp: 35 })];
|
|
877
|
+
const end = amps(S, t, c.dur, vel * (accent ? 0.4 : 0.3), 0.003, 1.5, 0.5, o.release ?? 0.02);
|
|
878
|
+
const dec = o.env ?? 0.3;
|
|
879
|
+
sweep(S, t, accent ? 3.5 : 2.5, accent ? 0.03 : 0.003, accent ? Math.min(dec, 0.12) : dec);
|
|
880
|
+
const s = vco(ctx, o.square ? 'square' : 'saw', f, t, end, 0, 1);
|
|
881
|
+
s.connect(S[0].input);
|
|
882
|
+
applyBends(s.frequency, c, 1, o.glide ?? 0.06);
|
|
883
|
+
unplug(s, [S[0].out]);
|
|
884
|
+
};
|
|
885
|
+
/**
|
|
886
|
+
* 808 bass: the Roland TR-808 bass drum played as a tuned bass, as in trap and future bass. A sine that starts an octave high and drops to pitch
|
|
887
|
+
* in about 40 ms (the punch), then a long slow decay, driven into a soft clipper whose third harmonic carries it on small speakers and fades as
|
|
888
|
+
* the note decays. Mono, with nothing below the fundamental. o.drive (default 2; 0.5 is nearly clean), o.glide (default 0.12), o.release.
|
|
889
|
+
*/
|
|
890
|
+
const bass808 = (c) => {
|
|
891
|
+
const { ctx, t, f, vel, o } = c;
|
|
892
|
+
const env = gainNode(ctx, 0);
|
|
893
|
+
const end = adsr(env, t, c.dur, 1, 0.002, 2.5, 0.3, o.release ?? 0.15);
|
|
894
|
+
const s = osc(ctx, 'sine', 2 * f, t, end);
|
|
895
|
+
s.frequency.setTargetAtTime(f, t, 0.012);
|
|
896
|
+
applyBends(s.frequency, c, 1, o.glide ?? 0.12);
|
|
897
|
+
const out = gainNode(ctx, vel * 0.5);
|
|
898
|
+
s.connect(env).connect(saturator(ctx, o.drive ?? 2)).connect(out).connect(c.out);
|
|
899
|
+
unplug(s, [out]);
|
|
900
|
+
};
|
|
901
|
+
/** Sub-bass: a pure sine */
|
|
902
|
+
const subBass = (c) => {
|
|
903
|
+
const { ctx, t, f, vel, o } = c;
|
|
904
|
+
const g = gainNode(ctx, 0);
|
|
905
|
+
const end = adsr(g, t, c.dur, vel * 0.55, o.attack ?? 0.01, 0, 1, o.release ?? 0.1);
|
|
906
|
+
g.connect(c.out);
|
|
907
|
+
const s = osc(ctx, 'sine', f, t, end);
|
|
908
|
+
s.connect(g);
|
|
909
|
+
applyBends(s.frequency, c);
|
|
910
|
+
unplug(s, [g]);
|
|
911
|
+
};
|
|
912
|
+
/** Distorted bass: two saws and a square an octave down, overdriven, then a four-pole low-pass; a clean sine an octave down under it. o.cut (Hz, default 1400), o.q, o.release, o.glide */
|
|
913
|
+
const distBass = (c) => {
|
|
914
|
+
const { ctx, t, f, vel, o } = c;
|
|
915
|
+
const S = [strip(ctx, c.out, { pan: 0, level: 1, cut: o.cut ?? 1400, q: o.q ?? 2 })];
|
|
916
|
+
const end = amps(S, t, c.dur, vel * 0.26, 0.005, 0.3, 0.8, o.release ?? 0.06);
|
|
917
|
+
const pre = gainNode(ctx, 0.45);
|
|
918
|
+
pre.connect(saturator(ctx, 4)).connect(S[0].input);
|
|
919
|
+
const oscs = [vco(ctx, 'saw', f, t, end, -6), vco(ctx, 'saw', f, t, end, 6), vco(ctx, 'square', f, t, end, -1200)];
|
|
920
|
+
for (const s of oscs)
|
|
921
|
+
s.connect(pre);
|
|
922
|
+
const sine = osc(ctx, 'sine', f / 2, t, end);
|
|
923
|
+
sine.connect(gainNode(ctx, 0.6)).connect(S[0].amp);
|
|
924
|
+
for (const s of oscs)
|
|
925
|
+
applyBends(s.frequency, c, 1, o.glide ?? 0.07);
|
|
926
|
+
applyBends(sine.frequency, c, 0.5, o.glide ?? 0.07);
|
|
927
|
+
unplug(sine, [S[0].out]);
|
|
928
|
+
};
|
|
929
|
+
// ───────────────────────── Ambience ─────────────────────────
|
|
930
|
+
/** Birdsong: random sine glide chirps */
|
|
931
|
+
const birds = (c) => {
|
|
932
|
+
const { ctx, t, vel } = c;
|
|
933
|
+
const end = t + c.dur;
|
|
934
|
+
const r = rng(Math.floor(t * 53) + 7);
|
|
935
|
+
for (let tt = t + Math.abs(r()) * 1.5; tt < end - 0.4; tt += 0.6 + Math.abs(r()) * 2.8) {
|
|
936
|
+
const base = 2600 + Math.abs(r()) * 2400;
|
|
937
|
+
const notes = 2 + Math.floor(Math.abs(r()) * 4);
|
|
938
|
+
const pan = r() * 0.8;
|
|
939
|
+
for (let k = 0; k < notes; k++) {
|
|
940
|
+
const st = tt + k * (0.07 + Math.abs(r()) * 0.05);
|
|
941
|
+
const s = osc(ctx, 'sine', base, st, st + 0.09);
|
|
942
|
+
s.frequency.setValueAtTime(base * (1 + r() * 0.1), st);
|
|
943
|
+
s.frequency.exponentialRampToValueAtTime(base * (1.3 + r() * 0.3), st + 0.06);
|
|
944
|
+
const g = gainNode(ctx, 0);
|
|
945
|
+
g.gain.setValueAtTime(0, st);
|
|
946
|
+
g.gain.linearRampToValueAtTime(vel * 0.05, st + 0.01);
|
|
947
|
+
g.gain.linearRampToValueAtTime(0, st + 0.08);
|
|
948
|
+
s.connect(g).connect(panned(ctx, c.out, pan));
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
/**
|
|
953
|
+
* White noise swept from `from` to `to` Hz over [t, end], each side of the stereo field its own noise: a high-pass under a rising sweep,
|
|
954
|
+
* a low-pass over a falling one, resonant so that it whistles at the edge of the sweep
|
|
955
|
+
*/
|
|
956
|
+
function noiseSweep(ctx, out, t, end, from, to) {
|
|
957
|
+
for (const side of [-0.8, 0.8]) {
|
|
958
|
+
const f = filt(ctx, to > from ? 'highpass' : 'lowpass', from, 4);
|
|
959
|
+
f.frequency.setValueAtTime(from, t);
|
|
960
|
+
f.frequency.exponentialRampToValueAtTime(to, end);
|
|
961
|
+
noiseSrc(ctx, t, end + 0.05).connect(f).connect(panned(ctx, out, side));
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Riser (uplifter) for a build-up: wide white noise through a resonant high-pass sweeping up (params from → to, Hz), and a detuned sawtooth
|
|
966
|
+
* stack climbing two octaves to an octave above the note under an opening low-pass. It swells to its peak as the note ends and stops dead,
|
|
967
|
+
* leaving the first beat of the drop clear
|
|
968
|
+
*/
|
|
969
|
+
const riser = (c) => {
|
|
970
|
+
const { ctx, t, f, vel, o } = c;
|
|
971
|
+
const end = t + c.dur;
|
|
972
|
+
const g = gainNode(ctx, 0);
|
|
973
|
+
g.gain.setValueAtTime(vel * 0.008, t);
|
|
974
|
+
g.gain.exponentialRampToValueAtTime(vel * 0.25, end);
|
|
975
|
+
g.gain.linearRampToValueAtTime(0, end + 0.02);
|
|
976
|
+
g.connect(c.out);
|
|
977
|
+
noiseSweep(ctx, g, t, end, o.from ?? 260, o.to ?? 7000);
|
|
978
|
+
const lp = filt(ctx, 'lowpass', 500, 1);
|
|
979
|
+
lp.frequency.setValueAtTime(500, t);
|
|
980
|
+
lp.frequency.exponentialRampToValueAtTime(9000, end);
|
|
981
|
+
lp.connect(gainNode(ctx, 0.1)).connect(g);
|
|
982
|
+
// An unpitched part (a rhythm) climbs to C5
|
|
983
|
+
const top = f > 20 ? f * 2 : 523;
|
|
984
|
+
for (const cents of [-15, 0, 15]) {
|
|
985
|
+
const s = osc(ctx, 'sawtooth', top / 4, t, end + 0.05, cents);
|
|
986
|
+
s.frequency.exponentialRampToValueAtTime(top, end);
|
|
987
|
+
s.connect(lp);
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
/** Downlifter, the riser in reverse, for the bars after a drop or into a breakdown: wide white noise under a resonant low-pass sweeping down (params from → to, Hz) as it fades away */
|
|
991
|
+
const downlifter = (c) => {
|
|
992
|
+
const { ctx, t, vel, o } = c;
|
|
993
|
+
const end = t + c.dur;
|
|
994
|
+
const g = gainNode(ctx, 0);
|
|
995
|
+
g.gain.setValueAtTime(0, t);
|
|
996
|
+
g.gain.linearRampToValueAtTime(vel * 0.3, t + 0.01);
|
|
997
|
+
g.gain.exponentialRampToValueAtTime(vel * 3e-3, end);
|
|
998
|
+
g.gain.linearRampToValueAtTime(0, end + 0.02);
|
|
999
|
+
g.connect(c.out);
|
|
1000
|
+
noiseSweep(ctx, g, t, end, o.from ?? 9000, o.to ?? 150);
|
|
1001
|
+
};
|
|
1002
|
+
// ───────────────────────── Catalog ─────────────────────────
|
|
1003
|
+
export const VOICES = {
|
|
1004
|
+
flute: flute(1900, 1, 0.05, 0.25, 11, 0.22),
|
|
1005
|
+
/** Pan flute: stopped pipes (odd harmonics), lots of breath, a very noticeable chiff on the attack */
|
|
1006
|
+
panflute: flute(1230, 0.15, 0.12, 0.55, 8, 0.21),
|
|
1007
|
+
/** Ocarina: a vessel (Helmholtz) resonator with no overtones of its own, nearly a sine */
|
|
1008
|
+
ocarina: flute(540, 1, 0.025, 0.1, 6, 0.22),
|
|
1009
|
+
/** Tin whistle: bright, thin, with lots of ornaments */
|
|
1010
|
+
whistle: flute(2200, 1, 0.06, 0.35, 9, 0.22),
|
|
1011
|
+
clarinet: reed(845, clarinetEven, 100, 3000, [[1500, 1, 2]], 5, 0.15, 0.67),
|
|
1012
|
+
oboe: reed(2500, () => 1, 500, 6000, [[1100, 1.8, 7], [2800, 2, 5]], 10, 0.059),
|
|
1013
|
+
bassoon: reed(700, () => 1, 150, 4000, [[500, 1.5, 6], [1150, 2, 4]], 6, 0.064),
|
|
1014
|
+
horn: brassV(1100, 400, 1300, 1, 5, 0.15),
|
|
1015
|
+
horns: brassV(1100, 400, 1300, 2, 4, 0.15),
|
|
1016
|
+
trumpet: brassV(1700, 1300, 6000, 1, 7, 0.115),
|
|
1017
|
+
trumpets: brassV(1700, 1300, 6000, 3, 5, 0.13),
|
|
1018
|
+
trombone: brassV(1700, 700, 4000, 1, 3, 0.16),
|
|
1019
|
+
tuba: brassV(500, 150, 1000, 1, 2, 0.145),
|
|
1020
|
+
/** Saxophones: alto and tenor */
|
|
1021
|
+
sax: saxV(4200, 560, 1900, 0.17),
|
|
1022
|
+
tenorSax: saxV(3600, 420, 1450, 0.17),
|
|
1023
|
+
/** Choir (female voices on the vowels a, o, u) and male voices (A, O, U) */
|
|
1024
|
+
choir: choirV('a', 3, 12, 16),
|
|
1025
|
+
choirOo: choirV('u', 3, 10, 12),
|
|
1026
|
+
choirO: choirV('o', 3, 10, 14),
|
|
1027
|
+
/** Male choir */
|
|
1028
|
+
menChoir: choirV('A', 3, 14, 10),
|
|
1029
|
+
/** Monk chant: a male o with almost no vibrato */
|
|
1030
|
+
chant: choirV('O', 3, 6, 2),
|
|
1031
|
+
/** Solo soprano (aria) */
|
|
1032
|
+
soprano: choirV('a', 1, 0, 45, 0.02),
|
|
1033
|
+
throat,
|
|
1034
|
+
organ,
|
|
1035
|
+
/** Organ with all stops */
|
|
1036
|
+
organFull: (c) => organ({ ...c, o: { full: 1, ...c.o } }),
|
|
1037
|
+
accordion,
|
|
1038
|
+
drone,
|
|
1039
|
+
warmPad: analogPad(1300, 0),
|
|
1040
|
+
glassPad,
|
|
1041
|
+
/** Dark pad: the analog pad nearly closed, over a sine an octave down */
|
|
1042
|
+
darkPad: analogPad(700, 0.6),
|
|
1043
|
+
airPad,
|
|
1044
|
+
glass,
|
|
1045
|
+
supersaw,
|
|
1046
|
+
synthLead,
|
|
1047
|
+
pluck,
|
|
1048
|
+
powerGtr,
|
|
1049
|
+
muteGtr: (c) => powerGtr({ ...c, o: { mute: 1, ...c.o } }),
|
|
1050
|
+
bass: fingerBass,
|
|
1051
|
+
synthBass,
|
|
1052
|
+
reese,
|
|
1053
|
+
acid,
|
|
1054
|
+
bass808,
|
|
1055
|
+
sub: subBass,
|
|
1056
|
+
distBass,
|
|
1057
|
+
birds,
|
|
1058
|
+
riser,
|
|
1059
|
+
downlifter,
|
|
1060
|
+
};
|
|
1061
|
+
//# sourceMappingURL=voices.js.map
|