@hraness/dawg 0.6.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/DAWG.md +338 -51
  3. package/README.md +4 -4
  4. package/core/chords.ts +492 -4
  5. package/core/diff.ts +5 -0
  6. package/core/expression.ts +143 -6
  7. package/core/fx.ts +258 -6
  8. package/core/granular.ts +91 -0
  9. package/core/instruments.ts +161 -0
  10. package/core/keys.ts +573 -4
  11. package/core/resonators.ts +291 -3
  12. package/core/score.ts +349 -3
  13. package/core/sdk/print.ts +78 -3
  14. package/core/sdk/v1.ts +1320 -36
  15. package/core/sections.ts +12 -4
  16. package/core/strings.ts +237 -2
  17. package/core/winds.ts +652 -0
  18. package/guides/automation.md +1 -0
  19. package/guides/chords.md +3 -1
  20. package/guides/effects.md +3 -2
  21. package/guides/media.md +1 -1
  22. package/guides/performance.md +3 -0
  23. package/guides/resample.md +26 -0
  24. package/guides/sounds.md +7 -6
  25. package/package.json +1 -1
  26. package/src/agent/agent.ts +8 -2
  27. package/src/agent/chord-tools.ts +153 -1
  28. package/src/agent/expression-tools.ts +91 -0
  29. package/src/agent/ops.ts +4 -1
  30. package/src/agent/preview-tool.ts +2 -1
  31. package/src/agent/resample-tool.ts +131 -0
  32. package/src/agent/tools.ts +200 -15
  33. package/src/agent/xcb-agent.ts +8 -2
  34. package/src/audio/arrange.ts +21 -0
  35. package/src/audio/dsp/envelope.ts +161 -0
  36. package/src/audio/dsp/shift.ts +256 -0
  37. package/src/audio/effects/chain.ts +18 -3
  38. package/src/audio/effects/common.ts +14 -0
  39. package/src/audio/effects/convolution.ts +106 -1
  40. package/src/audio/effects/gaze.ts +354 -0
  41. package/src/audio/engine.ts +46 -5
  42. package/src/audio/fit.ts +45 -0
  43. package/src/audio/granular.ts +352 -21
  44. package/src/audio/instrument-check.ts +9 -5
  45. package/src/audio/instruments.ts +32 -3
  46. package/src/audio/keys/electric.ts +420 -0
  47. package/src/audio/keys/engine.ts +143 -19
  48. package/src/audio/keys/organ.ts +1335 -0
  49. package/src/audio/keys/piano.ts +50 -6
  50. package/src/audio/keys/sympathetic.ts +127 -0
  51. package/src/audio/live-worker.ts +3 -1
  52. package/src/audio/live.ts +136 -9
  53. package/src/audio/preview.ts +15 -1
  54. package/src/audio/resample.ts +285 -0
  55. package/src/audio/resonators.ts +10 -2
  56. package/src/audio/sampler.ts +168 -21
  57. package/src/audio/samples.ts +26 -1
  58. package/src/audio/strings/body.ts +29 -16
  59. package/src/audio/strings/bow.ts +656 -0
  60. package/src/audio/strings/engine.ts +266 -20
  61. package/src/audio/strings/measure.test-helpers.ts +2 -0
  62. package/src/audio/wav.ts +119 -19
  63. package/src/audio/winds/engine.ts +302 -0
  64. package/src/audio/winds/filters.ts +153 -0
  65. package/src/audio/winds/pitch.ts +104 -0
  66. package/src/audio/winds/trim.ts +80 -0
  67. package/src/audio/winds/trims.ts +917 -0
  68. package/src/audio/winds/voice.ts +479 -0
  69. package/src/commands/expression.ts +103 -22
  70. package/src/commands/fx.ts +4 -1
  71. package/src/commands/granular.ts +7 -2
  72. package/src/commands/help.ts +91 -8
  73. package/src/commands/keys.ts +287 -11
  74. package/src/commands/modal.ts +67 -4
  75. package/src/commands/resample.ts +281 -0
  76. package/src/commands/rig.ts +5 -1
  77. package/src/commands/sample.ts +49 -2
  78. package/src/commands/shift.ts +119 -0
  79. package/src/commands/string.ts +28 -2
  80. package/src/commands/strum.ts +473 -0
  81. package/src/commands/wind.ts +244 -0
  82. package/src/main.ts +129 -7
  83. package/src/project/check.ts +5 -1
  84. package/src/render.ts +16 -0
  85. package/src/session/presence.ts +16 -3
  86. package/src/tui/audition.ts +1 -1
  87. package/src/tui/menu.ts +637 -35
  88. package/src/tui/modal-menu.ts +34 -7
  89. package/src/tui/performance-menu.ts +42 -0
  90. package/src/tui/play-chords.ts +104 -3
  91. package/src/tui/play-mode.ts +1 -0
  92. package/src/tui/play-session.ts +34 -9
  93. package/src/tui/wind-menu.ts +144 -0
@@ -0,0 +1,479 @@
1
+ /**
2
+ * One blown voice: a digital waveguide bore driven by a reed table
3
+ * (clarinet, `reed`), a conical reed (`sax`, also oboe and bassoon), an
4
+ * air jet (`jet`, flutes; open or stopped) or a lip resonator (`lips`,
5
+ * brass). After Cook and Scavone's STK (Clarinet, Saxofony, Flute, Brass;
6
+ * https://github.com/thestk/stk) and J. O. Smith, Physical Audio Signal
7
+ * Processing (https://ccrma.stanford.edu/~jos/pasp/), ported from the
8
+ * reviewed prototype (.plans/dawg-06/proto/resonators/winds.ts) with the
9
+ * review's fixes:
10
+ *
11
+ * - jet: a two-period loop (bore 1.5 periods, jet 0.5) less the loop
12
+ * low-pass and DC-blocker phase delays, with a jet offset and an edge
13
+ * tap so open pipes get even harmonics; stopped pipes are an inverting
14
+ * half-period bore (odd harmonics).
15
+ * - jet, reed and sax run at twice the rate with first-order ADAA on their
16
+ * nonlinearity and the 47-tap half-band decimator; brass runs at 1x.
17
+ * - Thiran fractional delays, loop cutoff and the trim are retuned every
18
+ * 32-sample control tick (bends, glides, slurs, vibrato).
19
+ * - The oscillation pressure keeps a 0.45 floor for stable pitch; a
20
+ * separate breath gain (30 dB over breath 0..1) makes swells audible.
21
+ *
22
+ * Deterministic: noise from `seededRandom` keyed by the note.
23
+ */
24
+ import type { WindSettings } from "../../../core/resonators.ts";
25
+ import { Decimate2 } from "../dsp/oversample.ts";
26
+ import { seededRandom } from "../dsp/rng.ts";
27
+ import { DcBlock, OnePole } from "../dsp/filters.ts";
28
+ import {
29
+ WindBiquad,
30
+ LoopDelay,
31
+ dcBlockDelay,
32
+ onePoleDelay,
33
+ } from "./filters.ts";
34
+
35
+ /** Control tick in base-rate samples. */
36
+ export const WIND_BLOCK = 32;
37
+
38
+ /** Pitch target over the voice: the first entry at 0, slurs after it. */
39
+ export type WindSegment = Readonly<{ at: number; hz: number }>;
40
+
41
+ export type WindNote = Readonly<{
42
+ /** Pitches in base-rate samples from the voice start; [0] at 0. */
43
+ segments: readonly WindSegment[];
44
+ /** Breath duration (note-off) in base-rate samples. */
45
+ length: number;
46
+ velocity: number;
47
+ seed: string;
48
+ /** Extra breath for accent/marcato (+), less for ghost (-). */
49
+ push?: number;
50
+ /** Pitch offset in cents at t seconds (bends, glides). */
51
+ cents?: (t: number) => number;
52
+ /** Automation at t seconds; returns undefined for a parameter with no lane. */
53
+ lane?: (param: string, t: number) => number | undefined;
54
+ }>;
55
+
56
+ /** Per-voice trims: cents as a function of sounding Hz, output gain. */
57
+ export type WindTrim = Readonly<{
58
+ cents: (hz: number) => number;
59
+ level: number;
60
+ /** Loudness curve over the range: a gain factor at `hz` (default 1). */
61
+ gain?: (hz: number) => number;
62
+ }>;
63
+
64
+ const NO_TRIM: WindTrim = { cents: () => 0, level: 1 };
65
+
66
+ const SLUR_SECONDS = 0.03;
67
+ /** First-order ADAA delays its shaper's output by half a sample. */
68
+ const ADAA_DELAY = 0.5;
69
+ const clamp = (x: number, lo: number, hi: number) =>
70
+ x < lo ? lo : x > hi ? hi : x;
71
+
72
+ // Reed table g(p) = p * clamp(0.7 - s p, -1, 1) and its antiderivative (ADAA).
73
+ function reedG(p: number, s: number): number {
74
+ return p * clamp(0.7 - s * p, -1, 1);
75
+ }
76
+ function reedF(p: number, s: number): number {
77
+ const p1 = -0.3 / s;
78
+ const p2 = 1.7 / s;
79
+ const mid = (x: number) => 0.35 * x * x - (s * x * x * x) / 3;
80
+ if (p < p1) return 0.5 * p * p + mid(p1) - 0.5 * p1 * p1;
81
+ if (p > p2) return -0.5 * p * p + mid(p2) + 0.5 * p2 * p2;
82
+ return mid(p);
83
+ }
84
+ // Jet table j(x) = clamp(x^3 - x, -1, 1) and its antiderivative.
85
+ const JET_EDGE = 1.3247179572447458; // real root of x^3 - x - 1
86
+ function jetG(x: number): number {
87
+ return clamp(x * x * x - x, -1, 1);
88
+ }
89
+ function jetF(x: number): number {
90
+ const a = Math.abs(x);
91
+ const poly = (v: number) => 0.25 * v ** 4 - 0.5 * v * v;
92
+ if (a <= JET_EDGE) return poly(x);
93
+ return poly(JET_EDGE) + (a - JET_EDGE);
94
+ }
95
+
96
+ /**
97
+ * STK's lip filter: a two-pole resonance at the lip frequency with no
98
+ * zeros (BiQuad::setResonance, unnormalised), so the mouth pressure's DC
99
+ * sets the lips' operating point and the squared area mapping has gain.
100
+ */
101
+ class LipResonator {
102
+ private a1 = 0;
103
+ private a2 = 0;
104
+ private y1 = 0;
105
+ private y2 = 0;
106
+ private g = 1;
107
+ set(hz: number, q: number, gain: number, sampleRate: number): void {
108
+ const radius = Math.exp((-Math.PI * hz) / (q * sampleRate));
109
+ this.a1 = -2 * radius * Math.cos((2 * Math.PI * hz) / sampleRate);
110
+ this.a2 = radius * radius;
111
+ this.g = gain * (1 + this.a1 + this.a2);
112
+ }
113
+ process(x: number): number {
114
+ const y = this.g * x - this.a1 * this.y1 - this.a2 * this.y2;
115
+ this.y2 = this.y1;
116
+ this.y1 = y;
117
+ return y;
118
+ }
119
+ }
120
+
121
+ /** First-order ADAA around a shaper with a parameter. */
122
+ class Adaa {
123
+ private x1 = 0;
124
+ private F1: number;
125
+ constructor(
126
+ private readonly f: (x: number) => number,
127
+ private readonly F: (x: number) => number,
128
+ ) {
129
+ this.F1 = F(0);
130
+ }
131
+ process(x: number): number {
132
+ const Fx = this.F(x);
133
+ const dx = x - this.x1;
134
+ const y =
135
+ Math.abs(dx) < 1e-6 ? this.f(0.5 * (x + this.x1)) : (Fx - this.F1) / dx;
136
+ this.x1 = x;
137
+ this.F1 = Fx;
138
+ return y;
139
+ }
140
+ }
141
+
142
+ export class WindVoice {
143
+ /** Base-rate sample after which the voice is silent. */
144
+ readonly endAt: number;
145
+ private readonly rate: number;
146
+ private readonly os: boolean;
147
+ private readonly random: () => number;
148
+ private readonly d0: LoopDelay;
149
+ private readonly d1: LoopDelay;
150
+ private readonly loopLp: OnePole;
151
+ private readonly dc: DcBlock;
152
+ private readonly outDc: DcBlock;
153
+ private readonly noiseLp: OnePole;
154
+ private readonly lip = new LipResonator();
155
+ private readonly shelf = new WindBiquad();
156
+ /** Reed, sax and lips brightness: a pitch-tracked tilt outside the loop. */
157
+ private readonly tone = new WindBiquad();
158
+ private readonly mutes: WindBiquad[] = [];
159
+ private readonly wahFilter = new WindBiquad();
160
+ private readonly decimate: Decimate2 | undefined;
161
+ private readonly shaper: Adaa | undefined;
162
+ private readonly reedSlope: number;
163
+ private settings: WindSettings;
164
+ private n = 0;
165
+ private envelope = 0;
166
+ private oz1 = 0;
167
+ private hz: number;
168
+ private slurFrom = 0;
169
+ private slurAt = -1;
170
+ private segment = 0;
171
+ private pressure = 0;
172
+ private breathGain = 1;
173
+ private wah = 1;
174
+ /** Output gain the loudness curve asks for, and its smoothed value. */
175
+ private gainTarget = 1;
176
+ private gainNow = -1;
177
+ private readonly growlHz: number;
178
+
179
+ constructor(
180
+ base: WindSettings,
181
+ private readonly note: WindNote,
182
+ private readonly sampleRate: number,
183
+ private readonly trim: WindTrim = NO_TRIM,
184
+ ) {
185
+ this.settings = base;
186
+ const p = base;
187
+ this.os = p.model !== "lips";
188
+ this.rate = this.os ? 2 * sampleRate : sampleRate;
189
+ this.random = seededRandom(`${note.seed}:wind`);
190
+ let lowest = Infinity;
191
+ for (const s of note.segments) lowest = Math.min(lowest, s.hz);
192
+ lowest = Math.max(15, lowest) * 0.5; // room for a -1 octave bend
193
+ const maxDelay = (this.rate / lowest) * 2.2 + 16;
194
+ this.d0 = new LoopDelay(maxDelay);
195
+ this.d1 = new LoopDelay(maxDelay);
196
+ this.loopLp = new OnePole(4000, this.rate);
197
+ this.dc = new DcBlock(this.rate, 20);
198
+ this.outDc = new DcBlock(sampleRate, 20);
199
+ this.noiseLp = new OnePole(2500, this.rate);
200
+ this.decimate = this.os ? new Decimate2() : undefined;
201
+ this.reedSlope = 0.3 * (0.6 + 0.8 * p.reed);
202
+ const s = this.reedSlope;
203
+ if (p.model === "reed")
204
+ this.shaper = new Adaa(
205
+ (x) => reedG(x, s),
206
+ (x) => reedF(x, s),
207
+ );
208
+ else if (p.model === "jet") this.shaper = new Adaa(jetG, jetF);
209
+ this.hz = note.segments[0]!.hz;
210
+ this.growlHz = 70 + 50 * this.random();
211
+ // Dynamics: a 1.5 kHz shelf of (v - 0.6) k dB; brass blares, flutes barely change.
212
+ const k = { jet: 8, reed: 10, sax: 14, lips: 22 }[p.model];
213
+ const v = clamp(note.velocity + (note.push ?? 0), 0, 1);
214
+ this.shelf.set("highshelf", 1500, 0.7, (v - 0.6) * k, sampleRate);
215
+ this.setupMute(sampleRate);
216
+ this.endAt = note.length + Math.round((p.release * 3 + 0.05) * sampleRate);
217
+ this.control(0);
218
+ }
219
+
220
+ private setupMute(sr: number): void {
221
+ const add = (
222
+ type: Parameters<WindBiquad["set"]>[0],
223
+ hz: number,
224
+ q: number,
225
+ db: number,
226
+ ) => this.mutes.push(new WindBiquad().set(type, hz, q, db, sr));
227
+ switch (this.settings.mute) {
228
+ case "straight":
229
+ add("hpf", 450, 0.7, 0);
230
+ add("peak", 1800, 2, 9);
231
+ add("lpf", 7000, 0.7, 0);
232
+ break;
233
+ case "cup":
234
+ add("hpf", 220, 0.7, 0);
235
+ add("peak", 850, 1.5, 5);
236
+ add("lpf", 2200, 0.8, 0);
237
+ break;
238
+ case "harmon":
239
+ add("hpf", 700, 0.8, 0);
240
+ add("peak", 1650, 4, 14);
241
+ add("lpf", 4200, 1, 0);
242
+ break;
243
+ default:
244
+ break;
245
+ }
246
+ }
247
+
248
+ /** Control tick: lanes, pitch (slur, cents, vibrato, trim), delays, breath. */
249
+ private control(n: number): void {
250
+ const { note, sampleRate: sr, rate } = this;
251
+ const t = n / sr;
252
+ let p = this.settings;
253
+ if (note.lane) {
254
+ const next: Record<string, unknown> = { ...p };
255
+ let changed = false;
256
+ for (const key of ["breath", "noise", "wah", "growl", "flutter"]) {
257
+ const value = note.lane(key, t);
258
+ if (value !== undefined) {
259
+ next[key] = clamp(value, 0, 1);
260
+ changed = true;
261
+ }
262
+ }
263
+ if (changed) p = this.settings = next as WindSettings;
264
+ }
265
+ // Slurs: a short log-frequency glide into each later segment.
266
+ while (
267
+ this.segment + 1 < note.segments.length &&
268
+ note.segments[this.segment + 1]!.at <= n
269
+ ) {
270
+ this.segment += 1;
271
+ this.slurFrom = this.hz;
272
+ this.slurAt = note.segments[this.segment]!.at;
273
+ }
274
+ const target = note.segments[this.segment]!.hz;
275
+ let hz = target;
276
+ if (this.slurAt >= 0) {
277
+ const u = (n - this.slurAt) / (SLUR_SECONDS * sr);
278
+ if (u >= 1) this.slurAt = -1;
279
+ else hz = this.slurFrom * (target / this.slurFrom) ** u;
280
+ }
281
+ this.hz = hz;
282
+ this.gainTarget = this.trim.gain ? this.trim.gain(hz) : 1;
283
+ if (this.gainNow < 0) this.gainNow = this.gainTarget;
284
+ const cents = note.cents ? note.cents(t) : 0;
285
+ const vib =
286
+ p.vibmod > 0
287
+ ? p.vibmod * Math.sin(2 * Math.PI * p.vib * t) * Math.min(1, t / 0.3)
288
+ : 0;
289
+ const f0 = hz * 2 ** (cents / 1200 + vib / 12);
290
+ const f = f0 * 2 ** (this.trim.cents(f0) / 1200);
291
+ const w = (2 * Math.PI * f) / rate;
292
+ const period = rate / f;
293
+ switch (p.model) {
294
+ case "reed":
295
+ // Closed-open cylinder: inverting reflection, round trip = 2 x loop;
296
+ // the one-zero averager adds half a sample.
297
+ this.d0.set(period / 2 - 0.5 - ADAA_DELAY, w);
298
+ break;
299
+ case "sax": {
300
+ // Reed position 0.35 along the bore: measured over every semitone of
301
+ // sax, altosax, barisax, oboe and bassoon at 22.05, 44.1 and 48 kHz
302
+ // it never flips register (STK's 0.2 flips at 6 of 36 sax notes,
303
+ // the spec's register guard).
304
+ const position = 0.35;
305
+ const total = period - 0.5;
306
+ this.d0.set((1 - position) * total, w);
307
+ this.d1.set(position * total, w);
308
+ break;
309
+ }
310
+ case "jet": {
311
+ // Loop low-pass corner tracks the pitch: brighter above.
312
+ const corner = clamp(f * (3 + 9 * p.bright), 800, 0.4 * rate);
313
+ this.loopLp.set(corner, rate);
314
+ const filters =
315
+ onePoleDelay(this.loopLp.p, w) +
316
+ dcBlockDelay(this.dc.r, w) +
317
+ ADAA_DELAY;
318
+ if (p.stopped) {
319
+ this.d0.set(0.5 * period - filters, w);
320
+ this.d1.set(0.5 * period * (0.9 + 0.2 * p.reed), w);
321
+ } else {
322
+ this.d0.set(1.5 * period - filters, w);
323
+ this.d1.set(0.5 * period * (0.9 + 0.2 * p.reed), w);
324
+ }
325
+ break;
326
+ }
327
+ case "lips":
328
+ // STK Brass: a bore of two periods sounding its 2nd mode and a lip
329
+ // resonance a little below f (constant Q 12, unit DC gain x 3 so
330
+ // every register speaks); the trim removes the pull (+15..45 c).
331
+ this.d0.set(2 * period - dcBlockDelay(this.dc.r, w), w);
332
+ this.lip.set(f * (0.95 + 0.04 * (p.reed - 0.5)), 12, 3, rate);
333
+ break;
334
+ }
335
+ // Brightness for the reed, sax and lip bores: a high shelf from about
336
+ // the 4th harmonic, +-9 dB around bright 0.5. It sits outside the loop
337
+ // (after the bore, before the mute), so pitch and the trims stay put;
338
+ // the jet carries bright in its loop low-pass instead.
339
+ if (p.model !== "jet")
340
+ this.tone.set(
341
+ "highshelf",
342
+ clamp(4 * hz, 500, 0.4 * sr),
343
+ 0.7,
344
+ (p.bright - 0.5) * 18,
345
+ sr,
346
+ );
347
+ // Oscillation pressure keeps a floor; breath gain follows breath.
348
+ const v = clamp(note.velocity + (note.push ?? 0), 0, 1);
349
+ const drive = clamp(p.breath * (0.55 + 0.45 * v), 0, 1);
350
+ const floor = 0.45;
351
+ switch (p.model) {
352
+ case "reed":
353
+ this.pressure = floor + 0.45 * drive;
354
+ break;
355
+ case "sax":
356
+ this.pressure = floor + 0.45 * drive;
357
+ break;
358
+ case "jet":
359
+ this.pressure = 0.9 + 0.35 * drive;
360
+ break;
361
+ case "lips":
362
+ this.pressure = 0.6 + 0.5 * drive;
363
+ break;
364
+ }
365
+ this.breathGain = 10 ** ((30 * (p.breath - 0.6)) / 20);
366
+ // Plunger: wah, opened per note by wahenv.
367
+ if (p.mute === "plunger") {
368
+ const open = Math.min(1, t / 0.15);
369
+ this.wah = p.wah + p.wahenv * (1 - p.wah) * open;
370
+ this.wahFilter.set("lpf", 400 + 4600 * this.wah * this.wah, 1.4, 0, sr);
371
+ }
372
+ }
373
+
374
+ /** One internal-rate sample of the bore. */
375
+ private tick(breath: number): number {
376
+ const p = this.settings;
377
+ switch (p.model) {
378
+ case "reed": {
379
+ const bore = this.d0.read();
380
+ const oz = 0.5 * (bore + this.oz1);
381
+ this.oz1 = bore;
382
+ const pdiff = -0.95 * oz - breath;
383
+ this.d0.push(breath + this.shaper!.process(pdiff));
384
+ return bore;
385
+ }
386
+ case "sax": {
387
+ const a = this.d0.read();
388
+ const b = this.d1.read();
389
+ const oz = 0.5 * (a + this.oz1);
390
+ this.oz1 = a;
391
+ const temp = -0.95 * oz;
392
+ const frame = temp - b;
393
+ const pdiff = breath - frame;
394
+ this.d1.push(temp);
395
+ // Plain reed table (no ADAA): measured, ADAA's half-sample average
396
+ // destabilises the conical loop above ~300 Hz; 2x keeps aliasing low.
397
+ this.d0.push(
398
+ breath - pdiff * clamp(0.7 + this.reedSlope * pdiff, -1, 1) - temp,
399
+ );
400
+ return frame;
401
+ }
402
+ case "jet": {
403
+ const boreOut = this.d0.read();
404
+ const temp = this.dc.process(-this.loopLp.process(boreOut));
405
+ const jet = this.d1.read();
406
+ this.d1.push(breath - 0.5 * temp);
407
+ // Jet offset: the jet hits the edge off-centre, giving even harmonics.
408
+ const offset = p.stopped ? 0 : 0.12;
409
+ const jt = this.shaper!.process(jet + offset) - jetG(offset);
410
+ this.d0.push(jt + 0.5 * temp);
411
+ // Edge tap (open pipes): some of the jet itself is heard.
412
+ return 0.3 * boreOut + (p.stopped ? 0 : 0.12 * jt);
413
+ }
414
+ case "lips": {
415
+ const bore = 0.85 * this.d0.read();
416
+ const mouth = 0.3 * breath;
417
+ let delta = this.lip.process(mouth - bore);
418
+ delta *= delta;
419
+ if (delta > 1) delta = 1;
420
+ const frame = delta * mouth + (1 - delta) * bore;
421
+ this.d0.push(this.dc.process(frame));
422
+ return bore * 2;
423
+ }
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Adds `count` base-rate samples into `out` from `offset`; false once the
429
+ * voice has finished.
430
+ */
431
+ process(out: Float64Array, offset: number, count: number): boolean {
432
+ const { sampleRate: sr, note } = this;
433
+ let p = this.settings;
434
+ const attack = Math.max(1, p.attack * sr);
435
+ const release = Math.max(1, p.release * sr);
436
+ const level = this.trim.level * p.gain;
437
+ for (let i = 0; i < count; i += 1) {
438
+ const n = this.n;
439
+ if (n >= this.endAt) return false;
440
+ if (n % WIND_BLOCK === 0 && n > 0) {
441
+ this.control(n);
442
+ p = this.settings;
443
+ }
444
+ if (n < note.length)
445
+ this.envelope = Math.min(1, this.envelope + 1 / attack);
446
+ else this.envelope = Math.max(0, this.envelope - 1 / release);
447
+ const t = n / sr;
448
+ let amp = this.pressure * this.envelope;
449
+ if (p.vibmod > 0) amp *= 1 + 0.04 * Math.sin(2 * Math.PI * p.vib * t);
450
+ if (p.flutter > 0)
451
+ amp *=
452
+ 1 - p.flutter * 0.5 * (0.5 + 0.5 * Math.sin(2 * Math.PI * 26 * t));
453
+ if (p.growl > 0)
454
+ amp *= 1 + p.growl * 0.25 * Math.sin(2 * Math.PI * this.growlHz * t);
455
+ let y: number;
456
+ if (this.os) {
457
+ const a = this.tick(this.breath(amp, p));
458
+ const b = this.tick(this.breath(amp, p));
459
+ y = this.decimate!.process(a, b);
460
+ } else y = this.tick(this.breath(amp, p));
461
+ if (p.growl > 0) y = Math.tanh(y * (1 + 2 * p.growl)) / (1 + p.growl);
462
+ if (p.model !== "jet") y = this.tone.process(y);
463
+ for (const filter of this.mutes) y = filter.process(y);
464
+ if (p.mute === "plunger") y = this.wahFilter.process(y);
465
+ y = this.shelf.process(y);
466
+ this.gainNow += (this.gainTarget - this.gainNow) * 0.002;
467
+ out[offset + i]! +=
468
+ this.outDc.process(y) * level * this.gainNow * this.breathGain;
469
+ this.n += 1;
470
+ }
471
+ return true;
472
+ }
473
+
474
+ /** Breath pressure with low-passed noise. */
475
+ private breath(amp: number, p: WindSettings): number {
476
+ const noise = this.noiseLp.process(this.random() * 2 - 1);
477
+ return amp * (1 + p.noise * 1.6 * noise);
478
+ }
479
+ }