@hraness/dawg 0.5.0 → 0.6.1
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/CHANGELOG.md +63 -0
- package/DAWG.md +592 -49
- package/README.md +4 -4
- package/core/chords.ts +492 -4
- package/core/diff.ts +9 -0
- package/core/expression.ts +143 -6
- package/core/fx.ts +689 -2
- package/core/granular.ts +619 -0
- package/core/instruments.ts +442 -0
- package/core/keys.ts +955 -0
- package/core/resonators.ts +862 -0
- package/core/score.ts +529 -8
- package/core/sdk/eval-child.ts +2 -0
- package/core/sdk/print.ts +247 -16
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2417 -40
- package/core/sections.ts +44 -10
- package/core/strings.ts +1080 -0
- package/core/winds.ts +652 -0
- package/guides/automation.md +1 -0
- package/guides/chords.md +3 -1
- package/guides/effects.md +5 -2
- package/guides/media.md +1 -1
- package/guides/performance.md +3 -0
- package/guides/resample.md +26 -0
- package/guides/sounds.md +7 -2
- package/guides/tempo.md +1 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +13 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +42 -2
- package/src/agent/preview-tool.ts +7 -1
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/tools.ts +726 -17
- package/src/agent/xcb-agent.ts +13 -0
- package/src/audio/arrange.ts +42 -3
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/envelope.ts +161 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/chain.ts +23 -3
- package/src/audio/effects/common.ts +14 -0
- package/src/audio/effects/convolution.ts +106 -1
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/engine.ts +56 -6
- package/src/audio/fit.ts +447 -0
- package/src/audio/granular.ts +995 -0
- package/src/audio/instrument-check.ts +137 -0
- package/src/audio/instruments.ts +140 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/electric.ts +420 -0
- package/src/audio/keys/engine.ts +485 -0
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +476 -0
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/live-worker.ts +56 -0
- package/src/audio/live.ts +361 -15
- package/src/audio/loudness.ts +65 -20
- package/src/audio/preview.ts +17 -1
- package/src/audio/resample.ts +285 -0
- package/src/audio/resonators.ts +295 -0
- package/src/audio/sampler.ts +328 -35
- package/src/audio/samples.ts +56 -8
- package/src/audio/strings/body.ts +263 -0
- package/src/audio/strings/bow.ts +656 -0
- package/src/audio/strings/engine.ts +615 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +200 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/warp.ts +61 -0
- package/src/audio/wav.ts +191 -19
- package/src/audio/winds/engine.ts +302 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +80 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/voice.ts +479 -0
- package/src/commands/expression.ts +111 -23
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +35 -1
- package/src/commands/granular.ts +432 -0
- package/src/commands/help.ts +159 -5
- package/src/commands/keys.ts +629 -0
- package/src/commands/modal.ts +310 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rig.ts +264 -0
- package/src/commands/sample.ts +52 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/string.ts +201 -0
- package/src/commands/strum.ts +473 -0
- package/src/commands/time.ts +4 -1
- package/src/commands/wind.ts +244 -0
- package/src/main.ts +339 -15
- package/src/project/check.ts +17 -0
- package/src/render.ts +49 -4
- package/src/session/presence.ts +16 -3
- package/src/tui/audition.ts +2 -2
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu.ts +934 -9
- package/src/tui/modal-menu.ts +145 -0
- package/src/tui/performance-menu.ts +42 -0
- package/src/tui/play-chords.ts +104 -3
- package/src/tui/play-mode.ts +1 -0
- package/src/tui/play-session.ts +70 -2
- package/src/tui/wind-menu.ts +144 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Electric keys (0.6.1 keys-electric lane): the Rhodes-style tine piano
|
|
3
|
+
* (`epiano`), the Wurlitzer-style reed piano (`wurli`) and the clavinet
|
|
4
|
+
* (`clav`), ported from the reviewed prototype
|
|
5
|
+
* (`.plans/dawg-06/proto/keys/keys.ts`, `TineVoice` and `ClavVoice`) with
|
|
6
|
+
* the review's round-2 fixes: the pickups run at twice the sample rate
|
|
7
|
+
* (band-limited both ways, so a hard-driven tine does not alias), the
|
|
8
|
+
* epiano carries its own suitcase stereo vibrato (`vibe`), the wurli its
|
|
9
|
+
* tremolo (`trem`), and clav pickup positions vary per track (seeded).
|
|
10
|
+
*
|
|
11
|
+
* Tine: a clamped-free cantilever, three modes at 1, 6.267 and 17.547 times
|
|
12
|
+
* the fundamental plus the tone bar, read through an electromagnetic pickup
|
|
13
|
+
* whose flux falls off with the tine's distance from the pole piece
|
|
14
|
+
* (1 / (1 + d^2)), differentiated (Faraday). After Shear and Wright, "The
|
|
15
|
+
* Electromagnetically Sustained Rhodes Piano" (NIME 2011) and Gabrielli et
|
|
16
|
+
* al., "Physical modeling of the Rhodes piano" (DAFx-10). Reed: the
|
|
17
|
+
* electrostatic pickup of the Wurlitzer 200 reads capacitance, 1 / (1 - k x),
|
|
18
|
+
* not a derivative; a peak at 1.1 kHz gives the nasal case. Clav: modal
|
|
19
|
+
* string struck near its end by a tangent, two pickup combs sin(n pi p) at
|
|
20
|
+
* neck and bridge, the mute slider damping upper partials and a small
|
|
21
|
+
* negative re-pluck when the string leaves the anvil (Gabrielli, Välimäki,
|
|
22
|
+
* Penttinen, Squartini and Bilbao, EURASIP JASP 2013:103).
|
|
23
|
+
*/
|
|
24
|
+
import { DcBlock, OnePole } from "../dsp/filters.ts";
|
|
25
|
+
import { Oversample2x } from "../dsp/oversample.ts";
|
|
26
|
+
import { unit } from "../dsp/rng.ts";
|
|
27
|
+
import {
|
|
28
|
+
Biquad2,
|
|
29
|
+
Burst,
|
|
30
|
+
clamp,
|
|
31
|
+
hammerPulse,
|
|
32
|
+
lerp,
|
|
33
|
+
LN1000,
|
|
34
|
+
ModeBank,
|
|
35
|
+
panGains,
|
|
36
|
+
pulseMag,
|
|
37
|
+
TAU,
|
|
38
|
+
} from "./dsp.ts";
|
|
39
|
+
|
|
40
|
+
/** Resolved electric voice parameters (core/keys.ts `KEYS_PARAMS`). */
|
|
41
|
+
export type ElectricParams = Readonly<{
|
|
42
|
+
kind: "epiano" | "wurli" | "clav";
|
|
43
|
+
hardness: number;
|
|
44
|
+
touch: number;
|
|
45
|
+
decay: number;
|
|
46
|
+
release: number;
|
|
47
|
+
width: number;
|
|
48
|
+
bark: number;
|
|
49
|
+
bell: number;
|
|
50
|
+
/** Output low-pass in Hz; 0 is off. */
|
|
51
|
+
tone: number;
|
|
52
|
+
pickup: "neck" | "bridge" | "both" | "out";
|
|
53
|
+
mute: number;
|
|
54
|
+
}>;
|
|
55
|
+
|
|
56
|
+
export type ElectricNoteOn = Readonly<{
|
|
57
|
+
pitch: number;
|
|
58
|
+
hz: number;
|
|
59
|
+
velocity: number;
|
|
60
|
+
/** Track seed hash (clav pickup positions are per track). */
|
|
61
|
+
trackSeed: number;
|
|
62
|
+
noteSeed: string;
|
|
63
|
+
}>;
|
|
64
|
+
|
|
65
|
+
/** Clamped-free cantilever mode ratios. */
|
|
66
|
+
const BEAM = [1, 6.267, 17.547] as const;
|
|
67
|
+
/** Rest position of the tine relative to the pickup pole. */
|
|
68
|
+
const X0 = 0.4;
|
|
69
|
+
|
|
70
|
+
/** The voice interface the keys engine drives (shared with `PianoVoice`). */
|
|
71
|
+
export type KeysVoice = {
|
|
72
|
+
readonly gl: number;
|
|
73
|
+
readonly gr: number;
|
|
74
|
+
readonly released: boolean;
|
|
75
|
+
noteOff(): void;
|
|
76
|
+
setCents(cents: number): void;
|
|
77
|
+
damp(tau: number): void;
|
|
78
|
+
process(out: Float64Array, offset: number, count: number): boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Seconds a released tine or reed takes to fall 60 dB. */
|
|
82
|
+
export function tineReleaseT60(pitch: number, release: number): number {
|
|
83
|
+
return release * (0.09 + 0.25 * clamp((60 - pitch) / 32, 0, 1));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Seconds a released clav string takes to fall 60 dB (the yarn damper). */
|
|
87
|
+
export function clavReleaseT60(release: number): number {
|
|
88
|
+
return release * 0.06;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Tine (epiano) and reed (wurli) voice. */
|
|
92
|
+
export class TineVoice implements KeysVoice {
|
|
93
|
+
private readonly bank: ModeBank;
|
|
94
|
+
private readonly pulse: Float64Array;
|
|
95
|
+
private pos = 0;
|
|
96
|
+
private readonly over = new Oversample2x();
|
|
97
|
+
private readonly dc: DcBlock;
|
|
98
|
+
private readonly lp: OnePole | undefined;
|
|
99
|
+
private readonly peak: Biquad2 | undefined;
|
|
100
|
+
private readonly norm: number;
|
|
101
|
+
readonly gl: number;
|
|
102
|
+
readonly gr: number;
|
|
103
|
+
private tmp = new Float64Array(0);
|
|
104
|
+
private readonly bursts: Burst[] = [];
|
|
105
|
+
private readonly rhodes: boolean;
|
|
106
|
+
/** Pickup state at the doubled rate. */
|
|
107
|
+
private prevX = 0;
|
|
108
|
+
private prevPhi = 0;
|
|
109
|
+
private releasedFlag = false;
|
|
110
|
+
private age = 0;
|
|
111
|
+
private readonly shape: (x: number) => number;
|
|
112
|
+
|
|
113
|
+
constructor(
|
|
114
|
+
private readonly note: ElectricNoteOn,
|
|
115
|
+
private readonly p: ElectricParams,
|
|
116
|
+
private readonly sr: number,
|
|
117
|
+
) {
|
|
118
|
+
const v = clamp(lerp(0.8, note.velocity, p.touch), 0.01, 1);
|
|
119
|
+
const key = note.pitch;
|
|
120
|
+
const f = note.hz;
|
|
121
|
+
const rhodes = p.kind === "epiano";
|
|
122
|
+
this.rhodes = rhodes;
|
|
123
|
+
const T = rhodes
|
|
124
|
+
? p.decay * 9 * 2 ** (-(key - 36) / 26)
|
|
125
|
+
: p.decay * 4.5 * 2 ** (-(key - 40) / 26);
|
|
126
|
+
const bell =
|
|
127
|
+
p.bell *
|
|
128
|
+
(rhodes ? 0.04 + 0.22 * v * v : 0.02 + 0.08 * v * v) *
|
|
129
|
+
2 ** (-(key - 60) / 30);
|
|
130
|
+
const drive = lerp(0.35, rhodes ? 1.6 : 1.5, p.bark);
|
|
131
|
+
const A = (0.15 + 0.85 * v) * drive;
|
|
132
|
+
// Hardness scales the hammer contact (0.5 is the prototype's).
|
|
133
|
+
const contact = lerp(2.2, 0.9, v) * (1.4 - 0.8 * p.hardness);
|
|
134
|
+
this.pulse = hammerPulse(Math.max(2, Math.round(contact * 1e-3 * sr)), 1.5);
|
|
135
|
+
this.bank = new ModeBank(4);
|
|
136
|
+
this.bank.add(f, T, A * 0.85, 1, sr);
|
|
137
|
+
// Tone bar: a slightly sharp, longer partner (the Rhodes tuning fork).
|
|
138
|
+
if (rhodes) this.bank.add(f * 2 ** (0.4 / 1200), T * 1.3, A * 0.15, 1, sr);
|
|
139
|
+
this.bank.add(f * BEAM[1], 0.35 * 2 ** (-(key - 60) / 24), A * bell, 1, sr);
|
|
140
|
+
if (rhodes) this.bank.add(f * BEAM[2], 0.06, A * bell * 0.3 * v, 1, sr);
|
|
141
|
+
// Normalise by the pickup's small-signal slope at the doubled rate. The
|
|
142
|
+
// tine's 0.36 puts a velocity-0.8 C-major triad near -6.5 dBFS, the
|
|
143
|
+
// house level (the reed's 0.32 already sits there).
|
|
144
|
+
const w = (TAU * f) / (2 * sr);
|
|
145
|
+
this.norm =
|
|
146
|
+
(rhodes ? 0.36 : 0.32) /
|
|
147
|
+
((rhodes ? (2 * X0) / (1 + X0 * X0) ** 2 : 0.5) * w);
|
|
148
|
+
this.prevPhi = this.pickup(0);
|
|
149
|
+
this.dc = new DcBlock(sr, 8);
|
|
150
|
+
this.lp = p.tone > 0 ? new OnePole(p.tone, sr) : undefined;
|
|
151
|
+
this.peak = rhodes
|
|
152
|
+
? undefined
|
|
153
|
+
: new Biquad2().set("peak", 1100, 0.9, 4, sr);
|
|
154
|
+
[this.gl, this.gr] = panGains(p.width * clamp((key - 60) / 30, -1, 1));
|
|
155
|
+
this.bursts.push(
|
|
156
|
+
new Burst(
|
|
157
|
+
`${note.noteSeed}:tine`,
|
|
158
|
+
rhodes ? 1800 : 1200,
|
|
159
|
+
1.2,
|
|
160
|
+
0.002,
|
|
161
|
+
0.01 * v * v,
|
|
162
|
+
sr,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
// The pickup at 2x: linear interpolation of the modal output between
|
|
166
|
+
// base-rate samples is done by the half-band interpolator; this runs on
|
|
167
|
+
// each doubled-rate sample in order, so it may keep state.
|
|
168
|
+
this.shape = (x: number) => {
|
|
169
|
+
this.prevX = x;
|
|
170
|
+
const phi = this.pickup(x);
|
|
171
|
+
const dy = rhodes ? -(phi - this.prevPhi) : phi - this.prevPhi;
|
|
172
|
+
this.prevPhi = phi;
|
|
173
|
+
const y = dy * this.norm;
|
|
174
|
+
return rhodes ? y : Math.tanh(1.3 * y) / 1.3;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private pickup(x: number): number {
|
|
179
|
+
if (this.rhodes) {
|
|
180
|
+
const d = x - X0;
|
|
181
|
+
return 1 / (1 + d * d);
|
|
182
|
+
}
|
|
183
|
+
return 1 / (1 - 0.5 * Math.min(x, 1.6));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
get released(): boolean {
|
|
187
|
+
return this.releasedFlag;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
noteOff(): void {
|
|
191
|
+
if (this.releasedFlag) return;
|
|
192
|
+
this.releasedFlag = true;
|
|
193
|
+
const t = tineReleaseT60(this.note.pitch, this.p.release);
|
|
194
|
+
this.bank.damp(() => t, this.sr);
|
|
195
|
+
this.bursts.push(
|
|
196
|
+
new Burst(
|
|
197
|
+
`${this.note.noteSeed}:damper`,
|
|
198
|
+
180,
|
|
199
|
+
1,
|
|
200
|
+
0.02,
|
|
201
|
+
0.006 * this.note.velocity,
|
|
202
|
+
this.sr,
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
setCents(cents: number): void {
|
|
208
|
+
this.bank.retune(2 ** (cents / 1200));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
damp(tau: number): void {
|
|
212
|
+
this.bank.damp(() => LN1000 * tau, this.sr);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
process(out: Float64Array, offset: number, count: number): boolean {
|
|
216
|
+
if (this.tmp.length < count) this.tmp = new Float64Array(count);
|
|
217
|
+
const tmp = this.tmp;
|
|
218
|
+
tmp.fill(0, 0, count);
|
|
219
|
+
let done = 0;
|
|
220
|
+
if (this.pos < this.pulse.length) {
|
|
221
|
+
done = Math.min(count, this.pulse.length - this.pos);
|
|
222
|
+
this.bank.run(tmp, 0, done, this.pulse, this.pos);
|
|
223
|
+
this.pos += done;
|
|
224
|
+
}
|
|
225
|
+
if (done < count) this.bank.run(tmp, done, count - done, null, 0);
|
|
226
|
+
for (let i = 0; i < count; i += 1) {
|
|
227
|
+
let y = this.over.process(tmp[i]!, this.shape);
|
|
228
|
+
y = this.dc.process(y);
|
|
229
|
+
if (this.peak) y = this.peak.process(y);
|
|
230
|
+
if (this.lp) y = this.lp.process(y);
|
|
231
|
+
for (const b of this.bursts) if (!b.done) y += b.next();
|
|
232
|
+
out[offset + i]! += y;
|
|
233
|
+
}
|
|
234
|
+
this.age += count;
|
|
235
|
+
if (this.age % 256 < count) this.bank.cull(1e-6);
|
|
236
|
+
// The oversampler and DC blocker hold a short tail after the modes go.
|
|
237
|
+
return (
|
|
238
|
+
this.bank.n > 0 ||
|
|
239
|
+
this.bursts.some((b) => !b.done) ||
|
|
240
|
+
Math.abs(this.prevX) > 1e-9
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Clav pickup positions (fractions of the string from the bridge end), with
|
|
247
|
+
* a seeded per-track spread of +/-6%: two clavinets never sound identical.
|
|
248
|
+
*/
|
|
249
|
+
export function clavPositions(trackSeed: number): {
|
|
250
|
+
neck: number;
|
|
251
|
+
bridge: number;
|
|
252
|
+
tangent: number;
|
|
253
|
+
} {
|
|
254
|
+
const spread = (slot: number) => 1 + 0.12 * (unit(trackSeed, 0, slot) - 0.5);
|
|
255
|
+
return {
|
|
256
|
+
neck: 0.19 * spread(0),
|
|
257
|
+
bridge: 0.065 * spread(1),
|
|
258
|
+
tangent: 0.04 * spread(2),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export class ClavVoice implements KeysVoice {
|
|
263
|
+
private readonly bank: ModeBank;
|
|
264
|
+
private pulse: Float64Array;
|
|
265
|
+
private pos = 0;
|
|
266
|
+
private readonly lp: OnePole | undefined;
|
|
267
|
+
private readonly hp: Biquad2;
|
|
268
|
+
readonly gl: number;
|
|
269
|
+
readonly gr: number;
|
|
270
|
+
private tmp = new Float64Array(0);
|
|
271
|
+
private releasedFlag = false;
|
|
272
|
+
private age = 0;
|
|
273
|
+
|
|
274
|
+
constructor(
|
|
275
|
+
note: ElectricNoteOn,
|
|
276
|
+
private readonly p: ElectricParams,
|
|
277
|
+
private readonly sr: number,
|
|
278
|
+
) {
|
|
279
|
+
const v = clamp(lerp(0.8, note.velocity, p.touch), 0.01, 1);
|
|
280
|
+
const key = note.pitch;
|
|
281
|
+
const B = 1.5e-4 * 2 ** ((key - 48) / 16);
|
|
282
|
+
// Partial 1 sits exactly at the tuned pitch.
|
|
283
|
+
const f0 = note.hz / Math.sqrt(1 + B);
|
|
284
|
+
const T = p.decay * 5 * 2 ** (-(key - 40) / 24);
|
|
285
|
+
const { neck, bridge, tangent } = clavPositions(note.trackSeed);
|
|
286
|
+
const contact = lerp(0.6, 0.25, v) * (1.4 - 0.8 * p.hardness);
|
|
287
|
+
this.pulse = hammerPulse(Math.max(2, Math.round(contact * 1e-3 * sr)), 1.2);
|
|
288
|
+
const bank = new ModeBank(80);
|
|
289
|
+
const modes: [number, number, number][] = [];
|
|
290
|
+
let energy = 0;
|
|
291
|
+
for (let n = 1; n <= 80; n += 1) {
|
|
292
|
+
const fn = n * f0 * Math.sqrt(1 + B * n * n);
|
|
293
|
+
if (fn >= 0.45 * sr) break;
|
|
294
|
+
const a = Math.sin(n * Math.PI * neck);
|
|
295
|
+
const b = Math.sin(n * Math.PI * bridge);
|
|
296
|
+
const w =
|
|
297
|
+
p.pickup === "neck"
|
|
298
|
+
? a
|
|
299
|
+
: p.pickup === "bridge"
|
|
300
|
+
? b * 2
|
|
301
|
+
: p.pickup === "out"
|
|
302
|
+
? a - b
|
|
303
|
+
: (a + b) * 0.7;
|
|
304
|
+
const u = Math.sin(n * Math.PI * tangent) * w;
|
|
305
|
+
const t60 =
|
|
306
|
+
(T / (1 + (fn / 3000) ** 2)) *
|
|
307
|
+
(1 - 0.85 * p.mute * Math.min(1, (n - 1) / 4));
|
|
308
|
+
modes.push([fn, t60, u]);
|
|
309
|
+
// Level is set on what the pickup hears through the tangent's
|
|
310
|
+
// contact spectrum, so every pickup switch sits within about 1.5 dB
|
|
311
|
+
// of the others (a C-major triad at velocity 0.8 near -6 dBFS).
|
|
312
|
+
energy += (u * pulseMag(this.pulse, (2 * Math.PI * fn) / sr)) ** 2;
|
|
313
|
+
}
|
|
314
|
+
// Out of phase leaves a peakier, thinner sum: +1.6 dB brings its peak
|
|
315
|
+
// level to the other pickups' (measured at C3, velocity 0.8).
|
|
316
|
+
const trim = p.pickup === "out" ? 1.2 : 1;
|
|
317
|
+
const G = (0.146 * trim * v ** 1.2) / Math.sqrt(Math.max(1e-12, energy));
|
|
318
|
+
for (const [fn, t60, u] of modes)
|
|
319
|
+
bank.add(fn, Math.max(0.02, t60), u * G, 1, sr);
|
|
320
|
+
this.bank = bank;
|
|
321
|
+
this.lp = p.tone > 0 ? new OnePole(p.tone, sr) : undefined;
|
|
322
|
+
this.hp = new Biquad2().set("hpf", 70, 0.7, 0, sr);
|
|
323
|
+
[this.gl, this.gr] = panGains(p.width * clamp((key - 60) / 30, -1, 1));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
get released(): boolean {
|
|
327
|
+
return this.releasedFlag;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
noteOff(): void {
|
|
331
|
+
if (this.releasedFlag) return;
|
|
332
|
+
this.releasedFlag = true;
|
|
333
|
+
// The string leaves the anvil: a small negative pluck, then the yarn damps it.
|
|
334
|
+
const plunk = hammerPulse(Math.max(2, Math.round(0.3e-3 * this.sr)), 1);
|
|
335
|
+
for (let i = 0; i < plunk.length; i += 1) plunk[i]! *= -0.35;
|
|
336
|
+
this.pulse = plunk;
|
|
337
|
+
this.pos = 0;
|
|
338
|
+
const t = clavReleaseT60(this.p.release);
|
|
339
|
+
this.bank.damp(() => t, this.sr);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
setCents(cents: number): void {
|
|
343
|
+
this.bank.retune(2 ** (cents / 1200));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
damp(tau: number): void {
|
|
347
|
+
this.bank.damp(() => LN1000 * tau, this.sr);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
process(out: Float64Array, offset: number, count: number): boolean {
|
|
351
|
+
if (this.tmp.length < count) this.tmp = new Float64Array(count);
|
|
352
|
+
const tmp = this.tmp;
|
|
353
|
+
tmp.fill(0, 0, count);
|
|
354
|
+
let done = 0;
|
|
355
|
+
if (this.pos < this.pulse.length) {
|
|
356
|
+
done = Math.min(count, this.pulse.length - this.pos);
|
|
357
|
+
this.bank.run(tmp, 0, done, this.pulse, this.pos);
|
|
358
|
+
this.pos += done;
|
|
359
|
+
}
|
|
360
|
+
if (done < count) this.bank.run(tmp, done, count - done, null, 0);
|
|
361
|
+
for (let i = 0; i < count; i += 1) {
|
|
362
|
+
let y = this.hp.process(tmp[i]!);
|
|
363
|
+
if (this.lp) y = this.lp.process(y);
|
|
364
|
+
out[offset + i]! += y;
|
|
365
|
+
}
|
|
366
|
+
this.age += count;
|
|
367
|
+
if (this.age % 256 < count) this.bank.cull(1e-6);
|
|
368
|
+
return this.bank.n > 0;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** A voice for an electric family. */
|
|
373
|
+
export function electricVoice(
|
|
374
|
+
note: ElectricNoteOn,
|
|
375
|
+
p: ElectricParams,
|
|
376
|
+
sr: number,
|
|
377
|
+
): KeysVoice {
|
|
378
|
+
return p.kind === "clav"
|
|
379
|
+
? new ClavVoice(note, p, sr)
|
|
380
|
+
: new TineVoice(note, p, sr);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The electric post stage, run on the summed track: the epiano's suitcase
|
|
385
|
+
* stereo vibrato (antiphase left/right gain, `vibe` depth at `vibehz`) and
|
|
386
|
+
* the wurli's tremolo (`trem` depth at 5.6 Hz, both channels). The phase is
|
|
387
|
+
* song time (`startSample` of the buffer at the song's sample 0), so a loop
|
|
388
|
+
* and a full render agree. `depthAt(sample)` reads the lane.
|
|
389
|
+
*/
|
|
390
|
+
export function electricPost(
|
|
391
|
+
left: Float64Array,
|
|
392
|
+
right: Float64Array,
|
|
393
|
+
kind: ElectricParams["kind"],
|
|
394
|
+
rateHz: number,
|
|
395
|
+
depthAt: (sample: number) => number,
|
|
396
|
+
sr: number,
|
|
397
|
+
startSample = 0,
|
|
398
|
+
): void {
|
|
399
|
+
if (kind === "clav") return;
|
|
400
|
+
const block = 32;
|
|
401
|
+
for (let i = 0; i < left.length; i += block) {
|
|
402
|
+
const depth = clamp(depthAt(i), 0, 1);
|
|
403
|
+
const end = Math.min(left.length, i + block);
|
|
404
|
+
if (depth <= 0) continue;
|
|
405
|
+
for (let s = i; s < end; s += 1) {
|
|
406
|
+
const lfo = Math.sin((TAU * rateHz * (s + startSample)) / sr);
|
|
407
|
+
if (kind === "epiano") {
|
|
408
|
+
// Constant-power autopan between the two speakers of the suitcase.
|
|
409
|
+
const pan = depth * lfo;
|
|
410
|
+
const a = ((pan + 1) * Math.PI) / 4;
|
|
411
|
+
left[s]! *= Math.SQRT2 * Math.cos(a);
|
|
412
|
+
right[s]! *= Math.SQRT2 * Math.sin(a);
|
|
413
|
+
} else {
|
|
414
|
+
const gain = 1 - 0.5 * depth * (1 - lfo);
|
|
415
|
+
left[s]! *= gain;
|
|
416
|
+
right[s]! *= gain;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|