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,565 @@
|
|
|
1
|
+
// Drum kit and percussion: membrane modes for drum bodies, filtered noise for snares and shakers, blooming bands of noise and 808-style metal for cymbals.
|
|
2
|
+
// Then the drum machines of electronic dance music (the TR-808 and TR-909 circuits, a modern layered kick) and its impact.
|
|
3
|
+
|
|
4
|
+
import { Biquad, normalize, rng, tailFade } from './core.ts';
|
|
5
|
+
import { modal, type ModalPartial } from './modal.ts';
|
|
6
|
+
|
|
7
|
+
type Gen = (sr: number, m: number, seed: number) => Float32Array | [Float32Array, Float32Array];
|
|
8
|
+
|
|
9
|
+
function buf(sr: number, len: number): Float32Array {
|
|
10
|
+
return new Float32Array(Math.floor(sr * len));
|
|
11
|
+
}
|
|
12
|
+
const decay = (sr: number, t60: number) => Math.pow(10, -3 / (t60 * sr));
|
|
13
|
+
/** The pitch of a tuned drum: the note it is given, or its own when the part has none */
|
|
14
|
+
const pitchOf = (m: number, hz: number): number => (m ? 440 * Math.pow(2, (m - 69) / 12) : hz);
|
|
15
|
+
|
|
16
|
+
/** Swept sine (the drum body) */
|
|
17
|
+
function sweep(y: Float32Array, sr: number, f0: number, f1: number, tf: number, amp: number, t60: number, delay = 0): void {
|
|
18
|
+
let ph = 0;
|
|
19
|
+
let env = amp;
|
|
20
|
+
const k = decay(sr, t60);
|
|
21
|
+
for (let i = Math.floor(delay * sr); i < y.length; i++) {
|
|
22
|
+
const t = i / sr - delay;
|
|
23
|
+
const f = f1 + (f0 - f1) * Math.exp(-t / tf);
|
|
24
|
+
ph += (2 * Math.PI * f) / sr;
|
|
25
|
+
y[i] += Math.sin(ph) * env;
|
|
26
|
+
env *= k;
|
|
27
|
+
if (env < 1e-6) break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One hit of filtered noise */
|
|
32
|
+
function noiseHit(y: Float32Array, sr: number, seed: number, filters: [type: 'lp' | 'hp' | 'bp', f: number, q?: number][], amp: number, t60: number, attack = 0.0005, delay = 0): void {
|
|
33
|
+
const r = rng(seed);
|
|
34
|
+
const fl = filters.map(([t, f, q]) => new Biquad(sr, t, f, q ?? 0.8));
|
|
35
|
+
const k = decay(sr, t60);
|
|
36
|
+
let env = amp;
|
|
37
|
+
const a = Math.max(1, attack * sr);
|
|
38
|
+
const d0 = Math.floor(delay * sr);
|
|
39
|
+
for (let i = d0; i < y.length; i++) {
|
|
40
|
+
let x = r();
|
|
41
|
+
for (const f of fl) x = f.run(x);
|
|
42
|
+
const j = i - d0;
|
|
43
|
+
y[i] += x * env * (j < a ? j / a : 1);
|
|
44
|
+
if (j >= a) env *= k;
|
|
45
|
+
if (env < 1e-6) break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 808-style metallic tone: six square waves (cymbal, tambourine) */
|
|
50
|
+
function metal(y: Float32Array, sr: number, base: number, amp: number, t60: number, hp: number, delay = 0): void {
|
|
51
|
+
const ratios = [1, 1.483, 1.804, 2.546, 2.63, 3.897];
|
|
52
|
+
const k = decay(sr, t60);
|
|
53
|
+
const phs = ratios.map(() => 0);
|
|
54
|
+
const bpf = new Biquad(sr, 'hp', hp, 0.7);
|
|
55
|
+
const bp2 = new Biquad(sr, 'hp', hp, 0.7);
|
|
56
|
+
let env = amp;
|
|
57
|
+
for (let i = Math.floor(delay * sr); i < y.length; i++) {
|
|
58
|
+
let x = 0;
|
|
59
|
+
for (let j = 0; j < ratios.length; j++) {
|
|
60
|
+
phs[j] += (base * ratios[j]) / sr;
|
|
61
|
+
x += phs[j] % 1 < 0.5 ? 1 : -1;
|
|
62
|
+
}
|
|
63
|
+
y[i] += bp2.run(bpf.run(x / 6)) * env;
|
|
64
|
+
env *= k;
|
|
65
|
+
if (env < 1e-6) break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A band of a cymbal's wash: [centre Hz, level, bloom seconds, T60 seconds] */
|
|
70
|
+
type WashBand = [number, number, number, number];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A cymbal's wash: its thousands of closely spaced modes, as bands of noise, with metal's square-wave tones mixed into the bands above 3 kHz.
|
|
74
|
+
* The stick excites the low modes first and the plate's nonlinearity hands the energy upward over the first tens of milliseconds (the build-up:
|
|
75
|
+
* the higher a band, the later it blooms), while damping grows with frequency, so the highs also die first and the tail settles into a lower hum.
|
|
76
|
+
*/
|
|
77
|
+
function wash(y: Float32Array, sr: number, seed: number, base: number, metalMix: number, bands: WashBand[]): void {
|
|
78
|
+
const n = y.length;
|
|
79
|
+
const r = rng(seed);
|
|
80
|
+
const nz = new Float32Array(n);
|
|
81
|
+
const sq = new Float32Array(n);
|
|
82
|
+
for (let i = 0; i < n; i++) nz[i] = r();
|
|
83
|
+
for (const ratio of [1, 1.483, 1.804, 2.546, 2.63, 3.897]) {
|
|
84
|
+
const d = (base * ratio) / sr;
|
|
85
|
+
let ph = 0;
|
|
86
|
+
for (let i = 0; i < n; i++) {
|
|
87
|
+
ph += d;
|
|
88
|
+
if (ph >= 1) ph -= 1;
|
|
89
|
+
sq[i] += ph < 0.5 ? 1 / 6 : -1 / 6;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const [fc, level, bloom, t60] of bands) {
|
|
93
|
+
const a = new Biquad(sr, 'bp', fc, 0.9);
|
|
94
|
+
const b = new Biquad(sr, 'bp', fc, 0.9);
|
|
95
|
+
const mix = fc >= 3000 ? metalMix : 0;
|
|
96
|
+
const k = decay(sr, t60);
|
|
97
|
+
const kr = Math.exp(-1 / (bloom * sr));
|
|
98
|
+
let env = level;
|
|
99
|
+
let rise = 1;
|
|
100
|
+
for (let i = 0; i < n && env > 1e-6; i++) {
|
|
101
|
+
y[i] += b.run(a.run(nz[i] + mix * sq[i])) * env * (1 - rise);
|
|
102
|
+
env *= k;
|
|
103
|
+
rise *= kr;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A well-mixed number in [0, 1) for each variant (the first outputs of rng for nearby seeds are alike) */
|
|
109
|
+
function vary(s: number): number {
|
|
110
|
+
const r = rng((s * 2654435761) >>> 0);
|
|
111
|
+
r();
|
|
112
|
+
return (r() + 1) / 2;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Bessel function of the first kind J_m(x): the shape of a drumhead's modes (integral form) */
|
|
116
|
+
function besselJ(m: number, x: number): number {
|
|
117
|
+
let s = 0;
|
|
118
|
+
for (let i = 0; i < 48; i++) {
|
|
119
|
+
const t = (Math.PI * (i + 0.5)) / 48;
|
|
120
|
+
s += Math.cos(m * t - x * Math.sin(t));
|
|
121
|
+
}
|
|
122
|
+
return s / 48;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A drumhead mode: [frequency ratio, order m, Bessel zero j_mn, radiated weight, T60 seconds] */
|
|
126
|
+
type HeadMode = [number, number, number, number, number];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A struck drumhead: the modes of a circular membrane, each excited by how far it moves where the stick lands (J_m(j_mn·r/a), r/a = x),
|
|
130
|
+
* all gliding down together. The strike stretches the head and raises its tension with the square of the displacement,
|
|
131
|
+
* so the pitch starts a little high and settles about twice as fast as the sound dies (time constant ≈ T60/14).
|
|
132
|
+
*/
|
|
133
|
+
function head(y: Float32Array, sr: number, f: number, x: number, modes: HeadMode[], glide: number): void {
|
|
134
|
+
const ref = Math.abs(besselJ(modes[0][1], modes[0][2] * x));
|
|
135
|
+
const tf = modes[0][4] / 14;
|
|
136
|
+
for (const [ratio, m, j, w, t60] of modes) sweep(y, sr, f * ratio * glide, f * ratio, tf, (w * Math.abs(besselJ(m, j * x))) / ref, t60);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sat = (y: Float32Array, drive: number): Float32Array => {
|
|
140
|
+
for (let i = 0; i < y.length; i++) y[i] = Math.tanh(y[i] * drive) / Math.tanh(drive);
|
|
141
|
+
return y;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A drum machine's tuned body: a sine at f Hz, raised at the start by its pitch envelope (each [depth, seconds] adds f·depth that dies away
|
|
146
|
+
* with that time constant). It starts `delay` seconds in, holds its level for `hold` seconds, then decays to T60
|
|
147
|
+
*/
|
|
148
|
+
function chirp(y: Float32Array, sr: number, f: number, drops: [number, number][], amp: number, t60: number, hold = 0, delay = 0): void {
|
|
149
|
+
let ph = 0;
|
|
150
|
+
let env = amp;
|
|
151
|
+
const k = decay(sr, t60);
|
|
152
|
+
const d = Math.floor(delay * sr);
|
|
153
|
+
for (let i = d; i < y.length && env > 1e-6; i++) {
|
|
154
|
+
const t = (i - d) / sr;
|
|
155
|
+
let r = 1;
|
|
156
|
+
for (const [depth, tau] of drops) r += depth * Math.exp(-t / tau);
|
|
157
|
+
y[i] += Math.sin(ph) * env;
|
|
158
|
+
ph += (2 * Math.PI * f * r) / sr;
|
|
159
|
+
if (t >= hold) env *= k;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The TR-909's cymbals and hi-hats are recordings stored at 6 bits and played through an analog envelope: the recording is quantized at
|
|
165
|
+
* full level and the decay comes after, so the grit fades with the sound but stays loud against the recording's own quiet tail (its sizzle)
|
|
166
|
+
*/
|
|
167
|
+
function rom6(y: Float32Array, sr: number, t60: number): Float32Array {
|
|
168
|
+
normalize(y, 1);
|
|
169
|
+
const k = decay(sr, t60);
|
|
170
|
+
let env = 1;
|
|
171
|
+
for (let i = 0; i < y.length; i++) {
|
|
172
|
+
y[i] = (Math.round(y[i] * 32) / 32) * env;
|
|
173
|
+
env *= k;
|
|
174
|
+
}
|
|
175
|
+
return tailFade(y, sr);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Noise through a low-pass (resonant at q) that closes from f0 to f1 Hz with time constant tf, under a decay to T60: the whoosh of a falling sweep */
|
|
179
|
+
function falling(y: Float32Array, sr: number, seed: number, f0: number, f1: number, tf: number, q: number, amp: number, t60: number): void {
|
|
180
|
+
const r = rng(seed);
|
|
181
|
+
const lp = new Biquad(sr, 'lp', f0, q);
|
|
182
|
+
const k = decay(sr, t60);
|
|
183
|
+
let env = amp;
|
|
184
|
+
for (let i = 0; i < y.length && env > 1e-6; i++) {
|
|
185
|
+
if (i % 32 === 0) lp.set('lp', f1 + (f0 - f1) * Math.exp(-i / sr / tf), q);
|
|
186
|
+
y[i] += lp.run(r()) * env;
|
|
187
|
+
env *= k;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const DRUMS = {
|
|
192
|
+
/**
|
|
193
|
+
* Kick: a beater on a big, slack head. The body settles at about 52 Hz after a short glide (the strike stretches the head),
|
|
194
|
+
* and the beater's impact is a brief click around 3 kHz, the part of a kick that is heard through a mix.
|
|
195
|
+
*/
|
|
196
|
+
kick: (sr, _m, s) => {
|
|
197
|
+
const y = buf(sr, 0.5);
|
|
198
|
+
sweep(y, sr, 130, 52, 0.02, 1, 0.45);
|
|
199
|
+
noiseHit(y, sr, s, [['lp', 6000]], 0.25, 0.012);
|
|
200
|
+
noiseHit(y, sr, s + 1, [['bp', 3200, 0.9]], 0.8 + 0.3 * vary(s), 0.008);
|
|
201
|
+
return sat(y, 1.6);
|
|
202
|
+
},
|
|
203
|
+
/** Long-tailed bass drum (the ground rumble of electronic and epic music) */
|
|
204
|
+
boom: (sr, _m, s) => {
|
|
205
|
+
const y = buf(sr, 1.6);
|
|
206
|
+
sweep(y, sr, 110, 34, 0.09, 1, 1.4);
|
|
207
|
+
noiseHit(y, sr, s, [['lp', 900]], 0.3, 0.3, 0.002);
|
|
208
|
+
return sat(y, 1.3);
|
|
209
|
+
},
|
|
210
|
+
snare: (sr, _m, s) => {
|
|
211
|
+
const y = buf(sr, 0.45);
|
|
212
|
+
sweep(y, sr, 260, 180, 0.02, 0.55, 0.14);
|
|
213
|
+
sweep(y, sr, 400, 330, 0.02, 0.2, 0.08);
|
|
214
|
+
noiseHit(y, sr, s, [['hp', 1400], ['lp', 9500]], 0.75, 0.28);
|
|
215
|
+
return sat(y, 1.4);
|
|
216
|
+
},
|
|
217
|
+
/** Field snare (march, epic): the long tail of the snare wires */
|
|
218
|
+
field: (sr, _m, s) => {
|
|
219
|
+
const y = buf(sr, 0.7);
|
|
220
|
+
sweep(y, sr, 230, 170, 0.03, 0.4, 0.2);
|
|
221
|
+
noiseHit(y, sr, s, [['hp', 900], ['lp', 7000], ['bp', 3200, 0.6]], 0.8, 0.55);
|
|
222
|
+
return y;
|
|
223
|
+
},
|
|
224
|
+
rim: (sr, _m, s) => {
|
|
225
|
+
const y = buf(sr, 0.15);
|
|
226
|
+
sweep(y, sr, 520, 500, 0.01, 0.5, 0.05);
|
|
227
|
+
noiseHit(y, sr, s, [['bp', 1800, 2]], 0.8, 0.03);
|
|
228
|
+
return y;
|
|
229
|
+
},
|
|
230
|
+
clap: (sr, _m, s) => {
|
|
231
|
+
const y = buf(sr, 0.5);
|
|
232
|
+
for (let i = 0; i < 4; i++) noiseHit(y, sr, s + i, [['bp', 1250, 1.4]], i === 3 ? 0.9 : 0.55, i === 3 ? 0.25 : 0.02, 0.0005, i * 0.009);
|
|
233
|
+
return y;
|
|
234
|
+
},
|
|
235
|
+
hat: (sr, _m, s) => {
|
|
236
|
+
const y = buf(sr, 0.12);
|
|
237
|
+
metal(y, sr, 330, 0.7, 0.05, 7000);
|
|
238
|
+
noiseHit(y, sr, s, [['hp', 8000]], 0.25, 0.04);
|
|
239
|
+
return y;
|
|
240
|
+
},
|
|
241
|
+
/** Open hi-hat: two small, thin cymbals rattling against each other; their wash blooms within a few milliseconds and the highest band fades first */
|
|
242
|
+
ohat: (sr, _m, s) => {
|
|
243
|
+
const y = buf(sr, 0.6);
|
|
244
|
+
wash(y, sr, s, 330, 1.5, [[2500, 0.02, 0.0005, 0.7], [4500, 0.055, 0.002, 0.6], [7000, 0.17, 0.005, 0.5], [10000, 0.35, 0.01, 0.42], [14000, 0.48, 0.018, 0.34]]);
|
|
245
|
+
noiseHit(y, sr, s + 1, [['hp', 7000]], 0.16, 0.05);
|
|
246
|
+
return tailFade(y, sr);
|
|
247
|
+
},
|
|
248
|
+
/** Ride: a thick cymbal played on the bow. The stick's ping and the bell's tone come first; the wash builds up slowly under them and its highs fade first */
|
|
249
|
+
ride: (sr, _m, s) => {
|
|
250
|
+
const y = buf(sr, 2.2);
|
|
251
|
+
wash(y, sr, s, 410, 1, [[1500, 0.03, 0.001, 2.4], [3000, 0.13, 0.005, 2.2], [5000, 0.22, 0.02, 1.9], [8000, 0.3, 0.045, 1.5], [12000, 0.25, 0.07, 1.1]]);
|
|
252
|
+
sweep(y, sr, 3400, 3400, 1, 0.08, 1.2);
|
|
253
|
+
noiseHit(y, sr, s + 1, [['bp', 5000, 0.8]], 0.3, 0.02);
|
|
254
|
+
return tailFade(y, sr);
|
|
255
|
+
},
|
|
256
|
+
/** Crash: the stick's strike, then the wash blooms upward over the first ~100 ms; the highs fade within about a second and a low hum rings on */
|
|
257
|
+
crash: (sr, _m, s) => {
|
|
258
|
+
const y = buf(sr, 3.2);
|
|
259
|
+
wash(y, sr, s, 290, 0.6, [[450, 0.16, 0.0005, 4], [900, 0.23, 0.001, 3.6], [1800, 0.45, 0.002, 3.2], [3500, 0.9, 0.006, 2.8], [6000, 1, 0.02, 2.2], [9000, 0.95, 0.035, 1.5], [13000, 0.8, 0.05, 1]]);
|
|
260
|
+
noiseHit(y, sr, s + 1, [['bp', 2500, 0.7]], 0.6, 0.03);
|
|
261
|
+
return tailFade(y, sr);
|
|
262
|
+
},
|
|
263
|
+
/** Reversed cymbal: sucked in toward the next beat. A different crash on each side, so it swells wide */
|
|
264
|
+
swell: (sr, m, s): [Float32Array, Float32Array] => {
|
|
265
|
+
const side = (seed: number): Float32Array => {
|
|
266
|
+
const y: Float32Array = DRUMS.crash(sr, m, seed).slice(0, Math.floor(sr * 1.6));
|
|
267
|
+
y.reverse();
|
|
268
|
+
const n = Math.floor(sr * 0.01);
|
|
269
|
+
for (let i = 0; i < n; i++) y[y.length - 1 - i] *= i / n;
|
|
270
|
+
return y;
|
|
271
|
+
};
|
|
272
|
+
return [side(s), side(s + 7)];
|
|
273
|
+
},
|
|
274
|
+
shaker: (sr, _m, s) => {
|
|
275
|
+
const y = buf(sr, 0.14);
|
|
276
|
+
noiseHit(y, sr, s, [['bp', 6500, 1.1], ['hp', 3500]], 0.9, 0.07, 0.012);
|
|
277
|
+
return y;
|
|
278
|
+
},
|
|
279
|
+
tamb: (sr, _m, s) => {
|
|
280
|
+
const y = buf(sr, 0.35);
|
|
281
|
+
for (let i = 0; i < 3; i++) metal(y, sr, 520 + i * 37, 0.3, 0.18, 7500, i * 0.006);
|
|
282
|
+
noiseHit(y, sr, s, [['hp', 6000]], 0.3, 0.16);
|
|
283
|
+
return y;
|
|
284
|
+
},
|
|
285
|
+
/**
|
|
286
|
+
* Taiko: two thick cowhide heads on a hollowed trunk, struck near the centre with heavy bachi. The deep (0,1) mode carries the sound,
|
|
287
|
+
* the (1,1) and (2,1) above it fade sooner, the slack hide makes the pitch sag as the strike's stretch relaxes, and the hardwood bachi slaps the hide.
|
|
288
|
+
*/
|
|
289
|
+
taiko: (sr, m, s) => {
|
|
290
|
+
const y = buf(sr, 1.4);
|
|
291
|
+
const f = pitchOf(m, 62);
|
|
292
|
+
head(y, sr, f, 0.15 + 0.2 * vary(s), [[1, 0, 2.405, 1, 1.1], [1.52, 1, 3.832, 0.6, 0.4], [2.1, 2, 5.136, 0.3, 0.25]], 1.12);
|
|
293
|
+
noiseHit(y, sr, s, [['lp', 900]], 0.5, 0.08);
|
|
294
|
+
noiseHit(y, sr, s + 1, [['bp', 1800, 0.8]], 0.9, 0.02);
|
|
295
|
+
return sat(y, 1.5);
|
|
296
|
+
},
|
|
297
|
+
/** Frame drum (a tambourine without jingles, the hand drum): a low thud + the slap of fingers */
|
|
298
|
+
frame: (sr, m, s) => {
|
|
299
|
+
const y = buf(sr, 0.6);
|
|
300
|
+
const f = pitchOf(m, 95);
|
|
301
|
+
sweep(y, sr, f * 1.3, f, 0.02, 0.8, 0.45);
|
|
302
|
+
noiseHit(y, sr, s, [['bp', 1400, 1]], 0.35, 0.03);
|
|
303
|
+
return y;
|
|
304
|
+
},
|
|
305
|
+
/** Darbuka: below C4 is the doum (low), above is the tek (high, crisp) */
|
|
306
|
+
darbuka: (sr, m, s) => {
|
|
307
|
+
const y = buf(sr, 0.5);
|
|
308
|
+
if (m && m < 60) {
|
|
309
|
+
sweep(y, sr, 150, 105, 0.02, 1, 0.4);
|
|
310
|
+
noiseHit(y, sr, s, [['lp', 1200]], 0.2, 0.03);
|
|
311
|
+
} else {
|
|
312
|
+
sweep(y, sr, 900, 820, 0.01, 0.4, 0.06);
|
|
313
|
+
noiseHit(y, sr, s, [['bp', 3200, 1.4]], 0.9, 0.025);
|
|
314
|
+
}
|
|
315
|
+
return y;
|
|
316
|
+
},
|
|
317
|
+
/**
|
|
318
|
+
* Tom: a stick on a tensioned head. The (0,1) mode is the pitch; the (1,1), (2,1), (0,2) and (3,1) modes above it (ideal-membrane ratios) die sooner
|
|
319
|
+
* and make it a drum rather than a sine. The strike's stretch raises the pitch about two semitones at first; the stick's tip adds a short slap.
|
|
320
|
+
*/
|
|
321
|
+
tom: (sr, m, s) => {
|
|
322
|
+
const y = buf(sr, 0.8);
|
|
323
|
+
const f = pitchOf(m, 110);
|
|
324
|
+
// Bigger, lower toms ring longer
|
|
325
|
+
const t = 0.55 * Math.pow(110 / f, 0.3);
|
|
326
|
+
// The stick lands between the centre and halfway out
|
|
327
|
+
head(y, sr, f, 0.3 + 0.25 * vary(s), [[1, 0, 2.405, 1, t], [1.59, 1, 3.832, 0.5, t * 0.6], [2.14, 2, 5.136, 0.35, t * 0.45], [2.3, 0, 5.52, 0.4, t * 0.35], [2.65, 3, 6.38, 0.25, t * 0.35]], 1.12);
|
|
328
|
+
noiseHit(y, sr, s, [['bp', 1100, 0.8]], 0.2, 0.05);
|
|
329
|
+
noiseHit(y, sr, s + 1, [['bp', 2800, 0.8]], 1.3, 0.015);
|
|
330
|
+
return sat(y, 1.3);
|
|
331
|
+
},
|
|
332
|
+
/**
|
|
333
|
+
* Timpani: the air in the kettle loads the head so that its (1,1) to (5,1) modes fall at about 1 : 1.5 : 1.98 : 2.44 : 2.9 (Rossing),
|
|
334
|
+
* a near-harmonic series that gives the drum its pitch; the (1,1) rings longest (2-3 s, longer on the big drums).
|
|
335
|
+
* The symmetric modes pump air in and out of the kettle and die within a few tenths of a second: with the felt, they are the low thud of the stroke.
|
|
336
|
+
* Each variant lands at a slightly different distance from the rim, which changes how strongly each mode is excited (J_m(j_m1·r/a)), so a roll does not repeat itself.
|
|
337
|
+
*/
|
|
338
|
+
timpani: (sr, m, s) => {
|
|
339
|
+
const f = pitchOf(m, 73);
|
|
340
|
+
// Strike point, as a fraction of the radius from the centre: a hand's breadth in from the rim
|
|
341
|
+
const x = 0.6 + 0.1 * vary(s);
|
|
342
|
+
const t11 = 2.4 * Math.pow(110 / f, 0.3);
|
|
343
|
+
// [ratio, order m, Bessel zero j_m1, radiated weight, T60 as a fraction of the (1,1)'s]; the weight is what is left after the felt and the radiation efficiency of each mode
|
|
344
|
+
const series: [number, number, number, number, number][] = [[1, 1, 3.832, 1, 1], [1.5, 2, 5.136, 0.6, 0.65], [1.98, 3, 6.38, 0.45, 0.45], [2.44, 4, 7.588, 0.35, 0.33], [2.9, 5, 8.771, 0.25, 0.25]];
|
|
345
|
+
const ref = besselJ(1, 3.832 * x);
|
|
346
|
+
const parts: ModalPartial[] = series.map(([ratio, mm, j, w, td]) => [ratio, (w * besselJ(mm, j * x)) / ref, t11 * td]);
|
|
347
|
+
// The inharmonic (0,2) and (2,2) modes: excited, but quickly damped by the air
|
|
348
|
+
parts.push([1.74, 0.3 * Math.abs(besselJ(0, 5.52 * x) / ref), 0.4], [2.2, 0.12, 0.35]);
|
|
349
|
+
return modal(sr, f, parts, { len: Math.min(4, t11 + 0.4), bend: 1.03, bendT: 0.03, click: { amp: 0.22, t: 0.08, hz: 1.4, q: 0.8 }, seed: s });
|
|
350
|
+
},
|
|
351
|
+
woodblock: (sr, m, s) => {
|
|
352
|
+
const f = pitchOf(m, 1100);
|
|
353
|
+
return modal(sr, f, [[1, 1, 0.09], [2.53, 0.3, 0.05]], { len: 0.2, click: { amp: 0.3, t: 0.01, hz: 3000 }, seed: s });
|
|
354
|
+
},
|
|
355
|
+
clave: (sr, _m, s) => modal(sr, 2500, [[1, 1, 0.12], [2.7, 0.2, 0.05]], { len: 0.2, seed: s }),
|
|
356
|
+
/** Anvil: a smith's metallic strike (by pitch) */
|
|
357
|
+
anvil: (sr, m, s) => {
|
|
358
|
+
const f = pitchOf(m, 1320);
|
|
359
|
+
return modal(sr, f, [[1, 1, 1.8], [2.03, 0.5, 1.2], [3.31, 0.35, 0.8], [4.62, 0.2, 0.5], [6.9, 0.12, 0.3]], { len: 2, beat: 3, click: { amp: 0.8, t: 0.012, hz: 5000, q: 0.6 }, seed: s });
|
|
360
|
+
},
|
|
361
|
+
/**
|
|
362
|
+
* Triangle: a steel rod bent into three sides, struck with a steel beater. Its modes are a free bar's (1 : 2.76 : 5.40 : 8.93 …), each
|
|
363
|
+
* split in two by the bends (vibrating in the plane and across it), so it has no clear pitch; the strongest lie between 2 and 10 kHz
|
|
364
|
+
* and ring for seconds, the highest fade first, and the beater's strike clicks
|
|
365
|
+
*/
|
|
366
|
+
triangle: (sr, _m, s) => {
|
|
367
|
+
const r = rng(s + 3);
|
|
368
|
+
const parts: ModalPartial[] = [];
|
|
369
|
+
for (const [i, ratio] of [1, 2.756, 5.404, 8.933, 13.34, 18.64, 24.81, 31.87, 39.81].entries()) {
|
|
370
|
+
const amp = i === 0 ? 0.15 : i < 6 ? 1 : 0.6;
|
|
371
|
+
const t60 = i < 2 ? 3 : 6 - i * 0.55;
|
|
372
|
+
for (const split of [1, 1.04 + r() * 0.02]) parts.push([ratio * split, amp * (0.8 + 0.2 * r()), t60 * (1 + 0.15 * r())]);
|
|
373
|
+
}
|
|
374
|
+
return modal(sr, 405 + r() * 8, parts, { len: 5, beat: 2, click: { amp: 0.5, t: 0.002, hz: 9000, q: 0.7 }, seed: s });
|
|
375
|
+
},
|
|
376
|
+
/** Gong (tam-tam): a metallic cloud that slowly blooms */
|
|
377
|
+
gong: (sr, m, s) => {
|
|
378
|
+
const f = pitchOf(m, 90);
|
|
379
|
+
const r = rng(s + 5);
|
|
380
|
+
const parts: ModalPartial[] = [];
|
|
381
|
+
for (let i = 0; i < 26; i++) parts.push([1 + i * 0.37 + r() * 0.15 + (i > 6 ? i * i * 0.02 : 0), 1 / (1 + i * 0.25), 5 - i * 0.12]);
|
|
382
|
+
const y = modal(sr, f, parts, { len: 6, attack: 0.25, beat: 4, seed: s });
|
|
383
|
+
return y;
|
|
384
|
+
},
|
|
385
|
+
/** Thunder: distant rolling low frequencies */
|
|
386
|
+
thunder: (sr, _m, s) => {
|
|
387
|
+
const y = buf(sr, 5);
|
|
388
|
+
const r = rng(s);
|
|
389
|
+
const lp = new Biquad(sr, 'lp', 260, 0.7);
|
|
390
|
+
const lp2 = new Biquad(sr, 'lp', 400, 0.7);
|
|
391
|
+
let env = 0;
|
|
392
|
+
let target = 1;
|
|
393
|
+
let brown = 0;
|
|
394
|
+
for (let i = 0; i < y.length; i++) {
|
|
395
|
+
const t = i / sr;
|
|
396
|
+
if (i % 2400 === 0) target = Math.max(0, 1 - t / 4.5) * (0.4 + 0.6 * Math.abs(r()));
|
|
397
|
+
env += (target - env) * 0.0008;
|
|
398
|
+
brown = brown * 0.985 + r() * 0.15;
|
|
399
|
+
y[i] = lp2.run(lp.run(brown)) * env * (t < 0.05 ? t / 0.05 : 1);
|
|
400
|
+
}
|
|
401
|
+
noiseHit(y, sr, s + 3, [['bp', 700, 0.6]], 0.25, 0.4, 0.004);
|
|
402
|
+
return normalize(y);
|
|
403
|
+
},
|
|
404
|
+
/** Close thunderclap: first a tearing crack, then right after a dull boom, then rolling out into several waves of thunder from near to far */
|
|
405
|
+
thunderclap: (sr, _m, s) => {
|
|
406
|
+
const y = buf(sr, 6);
|
|
407
|
+
const r = rng(s);
|
|
408
|
+
noiseHit(y, sr, s, [['hp', 900]], 1, 0.35, 0.0005);
|
|
409
|
+
let t = 0.01;
|
|
410
|
+
for (let i = 0; i < 14; i++) {
|
|
411
|
+
t += 0.008 + Math.abs(r()) * 0.03 * (1 + i * 0.25);
|
|
412
|
+
noiseHit(y, sr, s + 10 + i, [['bp', 1800 + Math.abs(r()) * 3000, 0.9]], 0.5 * Math.pow(0.85, i), 0.06, 0.0003, t);
|
|
413
|
+
}
|
|
414
|
+
noiseHit(y, sr, s + 40, [['lp', 180], ['lp', 180]], 1.4, 1.6, 0.004, 0.02);
|
|
415
|
+
const lp = new Biquad(sr, 'lp', 320, 0.7);
|
|
416
|
+
const lp2 = new Biquad(sr, 'lp', 500, 0.7);
|
|
417
|
+
let env = 0;
|
|
418
|
+
let target = 1;
|
|
419
|
+
let brown = 0;
|
|
420
|
+
for (let i = 0; i < y.length; i++) {
|
|
421
|
+
const tt = i / sr;
|
|
422
|
+
if (i % 3000 === 0) target = Math.max(0, 1 - tt / 5.5) * (0.3 + 0.7 * Math.abs(r()));
|
|
423
|
+
env += (target - env) * 0.0006;
|
|
424
|
+
brown = brown * 0.985 + r() * 0.15;
|
|
425
|
+
y[i] += lp2.run(lp.run(brown)) * env * 2.2 * Math.min(1, tt / 0.15);
|
|
426
|
+
}
|
|
427
|
+
return normalize(sat(y, 1.2));
|
|
428
|
+
},
|
|
429
|
+
/** Chains clashing */
|
|
430
|
+
chain: (sr, _m, s) => {
|
|
431
|
+
const y = buf(sr, 0.6);
|
|
432
|
+
const r = rng(s);
|
|
433
|
+
for (let i = 0; i < 9; i++) {
|
|
434
|
+
const t = Math.abs(r()) * 0.35;
|
|
435
|
+
const f = 2200 + Math.abs(r()) * 3000;
|
|
436
|
+
const part = modal(sr, f, [[1, 1, 0.12], [1.7, 0.6, 0.08], [2.9, 0.3, 0.05]], { len: 0.2, seed: s + i });
|
|
437
|
+
const off = Math.floor(t * sr);
|
|
438
|
+
for (let j = 0; j < part.length && off + j < y.length; j++) y[off + j] += part[j] * (0.3 + 0.2 * Math.abs(r()));
|
|
439
|
+
}
|
|
440
|
+
return normalize(y);
|
|
441
|
+
},
|
|
442
|
+
/** Finger castanets / finger snap */
|
|
443
|
+
snap: (sr, _m, s) => {
|
|
444
|
+
const y = buf(sr, 0.12);
|
|
445
|
+
noiseHit(y, sr, s, [['bp', 2600, 3]], 1, 0.025);
|
|
446
|
+
return y;
|
|
447
|
+
},
|
|
448
|
+
/** A brush sweeping past, like a maidenhair fern (jazz brush) */
|
|
449
|
+
brush: (sr, _m, s) => {
|
|
450
|
+
const y = buf(sr, 0.3);
|
|
451
|
+
noiseHit(y, sr, s, [['bp', 4200, 0.7]], 0.8, 0.18, 0.03);
|
|
452
|
+
return y;
|
|
453
|
+
},
|
|
454
|
+
/** Water drop */
|
|
455
|
+
drip: (sr, m, s) => {
|
|
456
|
+
const y = buf(sr, 0.4);
|
|
457
|
+
const f = pitchOf(m, 1400);
|
|
458
|
+
let ph = 0;
|
|
459
|
+
let env = 1;
|
|
460
|
+
const k = decay(sr, 0.12);
|
|
461
|
+
for (let i = 0; i < y.length; i++) {
|
|
462
|
+
const t = i / sr;
|
|
463
|
+
ph += (2 * Math.PI * f * (1 + 1.6 * Math.exp(-t / 0.012))) / sr;
|
|
464
|
+
y[i] = Math.sin(ph) * env * Math.min(1, t / 0.001);
|
|
465
|
+
env *= k;
|
|
466
|
+
}
|
|
467
|
+
void s;
|
|
468
|
+
return y;
|
|
469
|
+
},
|
|
470
|
+
// ── Drum machines and electronic dance music
|
|
471
|
+
/**
|
|
472
|
+
* TR-808 bass drum: a bridged-T resonator near 50 Hz (tunable) rung by a 1 ms trigger pulse. For the first few milliseconds the attack
|
|
473
|
+
* circuit raises its frequency, so the note opens with a fast cycle near 150 Hz (the click that cuts through a mix); then it booms on
|
|
474
|
+
* for over a second, its pitch sighing down from about 65 Hz as it fades. The pitch envelope is fitted to recordings of an 808.
|
|
475
|
+
* Give it notes to play it as a bassline
|
|
476
|
+
*/
|
|
477
|
+
kick808: (sr, m) => {
|
|
478
|
+
const y = buf(sr, 2);
|
|
479
|
+
chirp(y, sr, pitchOf(m, 50), [[4.8, 0.0027], [0.3, 0.04]], 1, 1.6);
|
|
480
|
+
return tailFade(sat(y, 1.5), sr);
|
|
481
|
+
},
|
|
482
|
+
/**
|
|
483
|
+
* TR-909 bass drum: a VCO whose pitch envelope dives from about 290 Hz to 50 Hz (tunable) with a 7 ms time constant, which is the punch
|
|
484
|
+
* (fitted to a recording of a 909); a waveshaper rounds it into a not-quite sine, and the attack control adds a click of noise just ahead of it
|
|
485
|
+
*/
|
|
486
|
+
kick909: (sr, m, s) => {
|
|
487
|
+
const y = buf(sr, 0.8);
|
|
488
|
+
chirp(y, sr, pitchOf(m, 50), [[4.9, 0.007]], 1, 0.4, 0, 0.0015);
|
|
489
|
+
sat(y, 1.6);
|
|
490
|
+
noiseHit(y, sr, s, [['hp', 900], ['lp', 8000]], 1.3, 0.008);
|
|
491
|
+
return tailFade(y, sr);
|
|
492
|
+
},
|
|
493
|
+
/**
|
|
494
|
+
* The modern EDM kick, built in layers: a punch where the pitch dives from 400 Hz past 100 Hz in about 15 ms into a sub body at 50 Hz
|
|
495
|
+
* (tunable), driven into a soft clipper so it holds full level for its first 60 ms and then falls away quickly to leave room for the bass,
|
|
496
|
+
* and on top a click of noise at 2-7 kHz, which is what is heard of a kick on small speakers
|
|
497
|
+
*/
|
|
498
|
+
bigKick: (sr, m, s) => {
|
|
499
|
+
const y = buf(sr, 0.6);
|
|
500
|
+
chirp(y, sr, pitchOf(m, 50), [[6, 0.006], [1, 0.03]], 1, 0.35, 0.06, 0.001);
|
|
501
|
+
sat(y, 2.2);
|
|
502
|
+
noiseHit(y, sr, s, [['hp', 2000], ['lp', 7000]], 1, 0.016);
|
|
503
|
+
return tailFade(y, sr);
|
|
504
|
+
},
|
|
505
|
+
/**
|
|
506
|
+
* TR-909 snare: two oscillators for the shell (185 and 330 Hz), each starting higher and dropping within about 10 ms, under bright
|
|
507
|
+
* high-passed noise for the wires (the snappy control); short and crisp, so it also suits the fast rolls of a build-up
|
|
508
|
+
*/
|
|
509
|
+
snare909: (sr, _m, s) => {
|
|
510
|
+
const y = buf(sr, 0.45);
|
|
511
|
+
chirp(y, sr, 185, [[0.6, 0.012]], 0.7, 0.12);
|
|
512
|
+
chirp(y, sr, 330, [[0.3, 0.012]], 0.35, 0.08);
|
|
513
|
+
noiseHit(y, sr, s, [['hp', 1500], ['lp', 13000]], 0.85, 0.25);
|
|
514
|
+
noiseHit(y, sr, s + 1, [['bp', 4500, 0.9]], 0.4, 0.06);
|
|
515
|
+
return sat(y, 1.8);
|
|
516
|
+
},
|
|
517
|
+
/**
|
|
518
|
+
* TR-808 / 909 hand clap: white noise through a band-pass near 1.1 kHz, opened by a sawtooth envelope three times about 11 ms apart
|
|
519
|
+
* (hands not quite together), then once more into a short reverberant tail. The tail is drawn anew for each channel, so it spreads wide
|
|
520
|
+
*/
|
|
521
|
+
clap909: (sr, _m, s) => {
|
|
522
|
+
const y = buf(sr, 0.7);
|
|
523
|
+
const gap = 0.011 + 0.002 * vary(s);
|
|
524
|
+
for (let i = 0; i < 4; i++) noiseHit(y, sr, s + i, [['bp', 1100, 1.2], ['hp', 700]], 2, i < 3 ? 0.02 : 0.06, 0.0003, i * gap);
|
|
525
|
+
const side = (seed: number): Float32Array => {
|
|
526
|
+
const c = y.slice();
|
|
527
|
+
noiseHit(c, sr, seed, [['bp', 1200, 0.8], ['hp', 600]], 0.6, 0.45, 0.002, 3 * gap);
|
|
528
|
+
return tailFade(sat(c, 1.5), sr);
|
|
529
|
+
};
|
|
530
|
+
return [side(s + 10), side(s + 20)];
|
|
531
|
+
},
|
|
532
|
+
/**
|
|
533
|
+
* TR-909 closed hi-hat. The 909 plays a 6-bit recording of a real hat (see rom6): dense noise above 6 kHz centred near 10 kHz, with
|
|
534
|
+
* a shimmer of the 808's six square waves (an octave up) under it, cut short by the decay
|
|
535
|
+
*/
|
|
536
|
+
hat909: (sr, _m, s) => {
|
|
537
|
+
const y = buf(sr, 0.25);
|
|
538
|
+
noiseHit(y, sr, s, [['hp', 6000], ['hp', 6000], ['bp', 10000, 0.6]], 1, 0.25);
|
|
539
|
+
metal(y, sr, 410.6, 0.25, 0.2, 6000);
|
|
540
|
+
return rom6(y, sr, 0.09);
|
|
541
|
+
},
|
|
542
|
+
/** TR-909 open hi-hat: a 6-bit recording of an open hat, its wash blooming within a few milliseconds, faded out over half a second */
|
|
543
|
+
ohat909: (sr, _m, s) => {
|
|
544
|
+
const y = buf(sr, 0.8);
|
|
545
|
+
wash(y, sr, s, 410.6, 1.2, [[5500, 0.1, 0.001, 1.6], [8000, 0.3, 0.002, 1.5], [11000, 0.45, 0.004, 1.3], [15000, 0.35, 0.006, 1]]);
|
|
546
|
+
noiseHit(y, sr, s + 1, [['hp', 6000]], 0.2, 0.03);
|
|
547
|
+
return rom6(y, sr, 0.8);
|
|
548
|
+
},
|
|
549
|
+
/**
|
|
550
|
+
* Impact, the hit on the first beat of a drop: a sub thump that dives from about 160 Hz to 36 Hz and rumbles on for two seconds, and a
|
|
551
|
+
* burst of noise whose low-pass closes from bright to dark (a downward whoosh), clipped together. The noise is drawn anew for each
|
|
552
|
+
* channel, so the tail is wide
|
|
553
|
+
*/
|
|
554
|
+
impact: (sr, _m, s) => {
|
|
555
|
+
const y = buf(sr, 3);
|
|
556
|
+
chirp(y, sr, 36, [[3, 0.03], [0.5, 0.25]], 1, 2.4);
|
|
557
|
+
noiseHit(y, sr, s, [['lp', 300]], 0.4, 0.15);
|
|
558
|
+
const side = (seed: number): Float32Array => {
|
|
559
|
+
const c = y.slice();
|
|
560
|
+
falling(c, sr, seed, 10000, 200, 0.6, 1.5, 0.9, 2.2);
|
|
561
|
+
return tailFade(sat(c, 1.6), sr);
|
|
562
|
+
};
|
|
563
|
+
return [side(s + 1), side(s + 2)];
|
|
564
|
+
},
|
|
565
|
+
} satisfies Record<string, Gen>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Modal synthesis: a sum of exponentially decaying partials. Bells, mallets, kalimba, music box.
|
|
2
|
+
|
|
3
|
+
import { Biquad, normalize, rng, tailFade } from './core.ts';
|
|
4
|
+
|
|
5
|
+
/** [frequency ratio, amplitude, decay seconds (T60)] */
|
|
6
|
+
export type ModalPartial = [number, number, number];
|
|
7
|
+
|
|
8
|
+
export interface ModalOpts {
|
|
9
|
+
len: number;
|
|
10
|
+
/** Attack time (seconds) */
|
|
11
|
+
attack?: number;
|
|
12
|
+
/** Strike noise: amplitude, decay seconds, band-pass center (as a multiple of the fundamental, or absolute Hz) */
|
|
13
|
+
click?: { amp: number; t: number; hz: number; q?: number };
|
|
14
|
+
/** Pitch bend down on the attack (ratio; returns to 1 within bendT seconds) */
|
|
15
|
+
bend?: number;
|
|
16
|
+
bendT?: number;
|
|
17
|
+
/** Tremolo (amplitude modulation): rate in Hz, depth 0-1 */
|
|
18
|
+
trem?: [number, number];
|
|
19
|
+
/** Add a detuned copy of each partial (cents), to produce a bell's beating */
|
|
20
|
+
beat?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Mallet contact (Hz): a mallet stays on the bar for a moment (a millisecond or so for yarn and felt, far less for brass), and a force that
|
|
23
|
+
* lasts τ has little energy above about 1/(2τ). Partials above this frequency are weakened 12 dB per octave, so the same mallet draws a rich
|
|
24
|
+
* tone from a low bar and an almost pure one from a high bar.
|
|
25
|
+
*/
|
|
26
|
+
mallet?: number;
|
|
27
|
+
seed?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function modal(sr: number, f: number, parts: ModalPartial[], o: ModalOpts): Float32Array {
|
|
31
|
+
const n = Math.floor(o.len * sr);
|
|
32
|
+
const y = new Float32Array(n);
|
|
33
|
+
const nyq = sr * 0.45;
|
|
34
|
+
const att = Math.max(1, Math.floor((o.attack ?? 0.001) * sr));
|
|
35
|
+
const r = rng(o.seed ?? Math.round(f * 13));
|
|
36
|
+
for (const [ratio, a, t60] of parts) {
|
|
37
|
+
const pf = f * ratio;
|
|
38
|
+
if (pf >= nyq) continue;
|
|
39
|
+
const amp = o.mallet ? a / (1 + (pf / o.mallet) ** 2) : a;
|
|
40
|
+
const copies = o.beat ? [1, Math.pow(2, o.beat / 1200)] : [1];
|
|
41
|
+
for (const cm of copies) {
|
|
42
|
+
const w0 = (2 * Math.PI * pf * cm) / sr;
|
|
43
|
+
const k = Math.pow(10, -3 / (t60 * sr));
|
|
44
|
+
let env = amp / copies.length;
|
|
45
|
+
let ph = r() * 0.3;
|
|
46
|
+
for (let i = 0; i < n; i++) {
|
|
47
|
+
let w = w0;
|
|
48
|
+
if (o.bend && o.bendT) {
|
|
49
|
+
const tt = i / sr;
|
|
50
|
+
w = w0 * (1 + (o.bend - 1) * Math.exp(-tt / o.bendT));
|
|
51
|
+
}
|
|
52
|
+
ph += w;
|
|
53
|
+
y[i] += Math.sin(ph) * env;
|
|
54
|
+
env *= k;
|
|
55
|
+
if (env < 1e-6) break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (let i = 0; i < att && i < n; i++) y[i] *= i / att;
|
|
60
|
+
if (o.click) {
|
|
61
|
+
const c = o.click;
|
|
62
|
+
const hz = c.hz < 40 ? c.hz * f : c.hz;
|
|
63
|
+
const bp = new Biquad(sr, 'bp', hz, c.q ?? 1.2);
|
|
64
|
+
const k = Math.pow(10, -3 / (c.t * sr));
|
|
65
|
+
let env = c.amp;
|
|
66
|
+
for (let i = 0; i < n && env > 1e-5; i++) {
|
|
67
|
+
y[i] += bp.run(r()) * env;
|
|
68
|
+
env *= k;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (o.trem) {
|
|
72
|
+
const [rate, depth] = o.trem;
|
|
73
|
+
for (let i = 0; i < n; i++) y[i] *= 1 - depth * 0.5 * (1 - Math.cos((2 * Math.PI * rate * i) / sr));
|
|
74
|
+
}
|
|
75
|
+
return tailFade(normalize(y), sr);
|
|
76
|
+
}
|