@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,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One oversampler per nonlinear rig section (stomp, head): 4x at 22.05 kHz
|
|
3
|
+
* and below, 2x at 44.1/48 kHz, from the shared half-band primitives in
|
|
4
|
+
* src/audio/dsp/oversample.ts. The section's linear-phase delay is removed
|
|
5
|
+
* so a rigged track stays on the grid.
|
|
6
|
+
*/
|
|
7
|
+
import { Adaa1, logCosh } from "../../dsp/shape.ts";
|
|
8
|
+
import { Oversample2x, Oversample4x } from "../../dsp/oversample.ts";
|
|
9
|
+
|
|
10
|
+
/** Oversampling factor of a nonlinear section at `sampleRate`. */
|
|
11
|
+
export function sectionFactor(sampleRate: number): 2 | 4 {
|
|
12
|
+
return sampleRate <= 32_000 ? 4 : 2;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Oversampler = { process(x: number, fn: (x: number) => number): number };
|
|
16
|
+
|
|
17
|
+
function oversampler(factor: 2 | 4): Oversampler {
|
|
18
|
+
return factor === 4 ? new Oversample4x() : new Oversample2x();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const latencies = new Map<number, number>();
|
|
22
|
+
|
|
23
|
+
/** Base-rate delay of the up/down pair (impulse peak), measured once. */
|
|
24
|
+
export function sectionLatency(factor: 2 | 4): number {
|
|
25
|
+
const known = latencies.get(factor);
|
|
26
|
+
if (known !== undefined) return known;
|
|
27
|
+
const os = oversampler(factor);
|
|
28
|
+
let peak = 0;
|
|
29
|
+
let at = 0;
|
|
30
|
+
for (let i = 0; i < 256; i += 1) {
|
|
31
|
+
const y = Math.abs(os.process(i === 0 ? 1 : 0, (x) => x));
|
|
32
|
+
if (y > peak) {
|
|
33
|
+
peak = y;
|
|
34
|
+
at = i;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
latencies.set(factor, at);
|
|
38
|
+
return at;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Runs `fn` over `buffer` at the oversampled rate, in place and latency
|
|
43
|
+
* compensated. `control(i)` runs at the base rate before sample i.
|
|
44
|
+
*/
|
|
45
|
+
export function runSection(
|
|
46
|
+
buffer: Float64Array,
|
|
47
|
+
factor: 2 | 4,
|
|
48
|
+
fn: (x: number) => number,
|
|
49
|
+
control?: (index: number) => void,
|
|
50
|
+
): void {
|
|
51
|
+
const os = oversampler(factor);
|
|
52
|
+
const latency = sectionLatency(factor);
|
|
53
|
+
const n = buffer.length;
|
|
54
|
+
for (let i = 0; i < n + latency; i += 1) {
|
|
55
|
+
if (control && i < n) control(i);
|
|
56
|
+
const y = os.process(i < n ? buffer[i]! : 0, fn);
|
|
57
|
+
if (i >= latency) buffer[i - latency] = y;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Static curves with closed-form antiderivatives for first-order ADAA.
|
|
62
|
+
|
|
63
|
+
/** Triode-like asymmetry: the positive half clips harder (grid current). */
|
|
64
|
+
const ASYM_B = 0.35;
|
|
65
|
+
const ASYM_T = Math.tanh(ASYM_B);
|
|
66
|
+
const asymF = (x: number) => Math.tanh(x + ASYM_B) - ASYM_T;
|
|
67
|
+
const asymAF = (x: number) => logCosh(x + ASYM_B) - ASYM_T * x;
|
|
68
|
+
|
|
69
|
+
export const adaaTanh = () => new Adaa1(Math.tanh, logCosh);
|
|
70
|
+
export const adaaTriode = () => new Adaa1(asymF, asymAF);
|
|
71
|
+
/** Full-wave rectifier (octave up): |x|, antiderivative x|x|/2. */
|
|
72
|
+
export const adaaRectify = () =>
|
|
73
|
+
new Adaa1(
|
|
74
|
+
(x) => Math.abs(x),
|
|
75
|
+
(x) => 0.5 * x * Math.abs(x),
|
|
76
|
+
);
|
|
77
|
+
/** RAT diode pair: tanh of a cubic-boosted input (no closed-form F). */
|
|
78
|
+
export const ratClip = (x: number) => Math.tanh(x + 0.18 * x * x * x);
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stomp boxes (`fx.stomp`): fuzz (Big Muff family), face (Fuzz Face), od
|
|
3
|
+
* (Tube Screamer), rat (RAT) and octave (Octavia). Circuit-inspired
|
|
4
|
+
* per-sample models ported from the 0.6 guitar prototype, run inside one
|
|
5
|
+
* oversampler with ADAA shapers.
|
|
6
|
+
*/
|
|
7
|
+
import type { Adaa1 } from "../../dsp/shape.ts";
|
|
8
|
+
import { Dc, Hp1, Lp1, clamp, dbToGain } from "./filters.ts";
|
|
9
|
+
import {
|
|
10
|
+
adaaRectify,
|
|
11
|
+
adaaTanh,
|
|
12
|
+
adaaTriode,
|
|
13
|
+
ratClip,
|
|
14
|
+
runSection,
|
|
15
|
+
} from "./section.ts";
|
|
16
|
+
|
|
17
|
+
export const STOMP_TYPES = ["fuzz", "face", "od", "rat", "octave"] as const;
|
|
18
|
+
export type StompType = (typeof STOMP_TYPES)[number];
|
|
19
|
+
|
|
20
|
+
export type StompSettings = {
|
|
21
|
+
gain: number;
|
|
22
|
+
tone: number;
|
|
23
|
+
level: number;
|
|
24
|
+
octave: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** A stomp circuit at the oversampled rate `sr`. */
|
|
28
|
+
export type StompCircuit = {
|
|
29
|
+
process(x: number): number;
|
|
30
|
+
/** Re-reads gain, tone and level (control rate, automated only). */
|
|
31
|
+
set(settings: StompSettings): void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function stompCircuit(
|
|
35
|
+
type: string,
|
|
36
|
+
initial: StompSettings,
|
|
37
|
+
sr: number,
|
|
38
|
+
): StompCircuit {
|
|
39
|
+
switch (type) {
|
|
40
|
+
case "fuzz":
|
|
41
|
+
return fuzz(initial, sr);
|
|
42
|
+
case "face":
|
|
43
|
+
return face(initial, sr);
|
|
44
|
+
case "rat":
|
|
45
|
+
return rat(initial, sr);
|
|
46
|
+
case "octave":
|
|
47
|
+
return octave(initial, sr);
|
|
48
|
+
default:
|
|
49
|
+
return od(initial, sr);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fuzz(initial: StompSettings, sr: number): StompCircuit {
|
|
54
|
+
// Input HPF, two cascaded clipping stages with interstage lowpass, then
|
|
55
|
+
// the passive LP/HP tone blend (the Muff's mid scoop).
|
|
56
|
+
const hp = new Hp1(90, sr);
|
|
57
|
+
const s1 = adaaTanh();
|
|
58
|
+
const s2 = adaaTanh();
|
|
59
|
+
const lpA = new Lp1(3200, sr);
|
|
60
|
+
const hpB = new Hp1(60, sr);
|
|
61
|
+
const lpB = new Lp1(2600, sr);
|
|
62
|
+
const toneLp = new Lp1(800, sr);
|
|
63
|
+
const toneHp = new Hp1(1600, sr);
|
|
64
|
+
let pre = 1;
|
|
65
|
+
let tone = 0.5;
|
|
66
|
+
let out = 1;
|
|
67
|
+
const set = (p: StompSettings) => {
|
|
68
|
+
pre = 2 ** (0.5 + 0.55 * p.gain);
|
|
69
|
+
tone = clamp(p.tone, 0, 1);
|
|
70
|
+
out = dbToGain(p.level) * 0.7;
|
|
71
|
+
};
|
|
72
|
+
set(initial);
|
|
73
|
+
return {
|
|
74
|
+
set,
|
|
75
|
+
process(input) {
|
|
76
|
+
let x = hp.process(input) * pre;
|
|
77
|
+
x = lpA.process(s1.process(x)) * 6;
|
|
78
|
+
x = lpB.process(s2.process(hpB.process(x)));
|
|
79
|
+
const y = (1 - tone) * toneLp.process(x) + tone * toneHp.process(x) * 1.6;
|
|
80
|
+
return y * out;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function face(initial: StompSettings, sr: number): StompCircuit {
|
|
86
|
+
// Asymmetric and round; cleans up as the input drops.
|
|
87
|
+
const hp = new Hp1(40, sr);
|
|
88
|
+
const s = adaaTriode();
|
|
89
|
+
const dc = new Dc(sr, 12);
|
|
90
|
+
const lp = new Lp1(2500, sr);
|
|
91
|
+
let pre = 1;
|
|
92
|
+
let out = 1;
|
|
93
|
+
const set = (p: StompSettings) => {
|
|
94
|
+
pre = 2 ** (0.3 + 0.5 * p.gain);
|
|
95
|
+
lp.set(2500 + 4000 * clamp(p.tone, 0, 1), sr);
|
|
96
|
+
out = dbToGain(p.level) * 0.8;
|
|
97
|
+
};
|
|
98
|
+
set(initial);
|
|
99
|
+
return {
|
|
100
|
+
set,
|
|
101
|
+
process(input) {
|
|
102
|
+
return lp.process(dc.process(s.process(hp.process(input) * pre))) * out;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function od(initial: StompSettings, sr: number): StompCircuit {
|
|
108
|
+
// Clean path plus a soft-clipped 720 Hz high-passed gain path (diodes in
|
|
109
|
+
// the op-amp feedback), then the tone lowpass.
|
|
110
|
+
const hp = new Hp1(720, sr);
|
|
111
|
+
const lpGain = new Lp1(5600, sr);
|
|
112
|
+
const s = adaaTanh();
|
|
113
|
+
const tone = new Lp1(700, sr);
|
|
114
|
+
let pre = 1;
|
|
115
|
+
let out = 1;
|
|
116
|
+
const set = (p: StompSettings) => {
|
|
117
|
+
pre = 1 + 10 ** (p.gain / 5);
|
|
118
|
+
tone.set(700 * 2 ** (3 * clamp(p.tone, 0, 1)), sr);
|
|
119
|
+
out = dbToGain(p.level) * 0.6;
|
|
120
|
+
};
|
|
121
|
+
set(initial);
|
|
122
|
+
return {
|
|
123
|
+
set,
|
|
124
|
+
process(x) {
|
|
125
|
+
const y = x + s.process(lpGain.process(hp.process(x) * pre)) * 0.5;
|
|
126
|
+
return tone.process(y) * out;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function rat(initial: StompSettings, sr: number): StompCircuit {
|
|
132
|
+
// Op-amp gain whose bandwidth shrinks with gain (LM308 GBW), hard diode
|
|
133
|
+
// clip to ground, then the passive filter.
|
|
134
|
+
const hp = new Hp1(60, sr);
|
|
135
|
+
const lpOp = new Lp1(20_000, sr);
|
|
136
|
+
const filter = new Lp1(475, sr);
|
|
137
|
+
let gain = 1;
|
|
138
|
+
let out = 1;
|
|
139
|
+
const set = (p: StompSettings) => {
|
|
140
|
+
gain = Math.min(1 + 2 ** (0.67 * p.gain + 1.5), 2000);
|
|
141
|
+
lpOp.set(clamp(1.0e6 / 3 / gain, 900, 20_000), sr);
|
|
142
|
+
filter.set(475 * 2 ** (6 * clamp(p.tone, 0, 1)), sr);
|
|
143
|
+
out = dbToGain(p.level) * 0.55;
|
|
144
|
+
};
|
|
145
|
+
set(initial);
|
|
146
|
+
return {
|
|
147
|
+
set,
|
|
148
|
+
process(input) {
|
|
149
|
+
const x = lpOp.process(hp.process(input) * gain * 0.02);
|
|
150
|
+
return filter.process(ratClip(x)) * out;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function octave(initial: StompSettings, sr: number): StompCircuit {
|
|
156
|
+
// Rectified (octave-up) path blended into a fuzz.
|
|
157
|
+
const hp = new Hp1(120, sr);
|
|
158
|
+
const rect: Adaa1 = adaaRectify();
|
|
159
|
+
const dc = new Dc(sr, 20);
|
|
160
|
+
const s = adaaTanh();
|
|
161
|
+
const lp = new Lp1(3000, sr);
|
|
162
|
+
let pre = 1;
|
|
163
|
+
let mix = 0.7;
|
|
164
|
+
let out = 1;
|
|
165
|
+
const set = (p: StompSettings) => {
|
|
166
|
+
pre = 2 ** (0.5 + 0.5 * p.gain);
|
|
167
|
+
lp.set(3000 + 4000 * clamp(p.tone, 0, 1), sr);
|
|
168
|
+
mix = clamp(p.octave, 0, 1);
|
|
169
|
+
out = dbToGain(p.level) * 0.7;
|
|
170
|
+
};
|
|
171
|
+
set(initial);
|
|
172
|
+
return {
|
|
173
|
+
set,
|
|
174
|
+
process(input) {
|
|
175
|
+
const x = hp.process(input);
|
|
176
|
+
const o = dc.process(rect.process(x * 2)) * 2;
|
|
177
|
+
return lp.process(s.process(((1 - mix) * x + mix * o) * pre)) * out;
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Stomp calibration input: a 196 Hz (G3) sine at guitar level. */
|
|
183
|
+
export const STOMP_CALIBRATION_DBFS = -18;
|
|
184
|
+
|
|
185
|
+
const stompMakeups = new Map<string, number>();
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Linear gain that brings a pedal at `settings` (level taken as 0 dB) back
|
|
189
|
+
* to the calibration sine's level, so switching or kicking on a stomp does
|
|
190
|
+
* not jump the track by 15-25 dB; `level` stays a trim on top. Measured on
|
|
191
|
+
* the circuit itself, deterministic, cached per settings and rate.
|
|
192
|
+
*/
|
|
193
|
+
export function stompMakeup(
|
|
194
|
+
type: string,
|
|
195
|
+
settings: StompSettings,
|
|
196
|
+
sampleRate: number,
|
|
197
|
+
factor: 2 | 4,
|
|
198
|
+
): number {
|
|
199
|
+
const key = `${sampleRate}|${factor}|${type}|${settings.gain}|${settings.tone}|${settings.octave}`;
|
|
200
|
+
const known = stompMakeups.get(key);
|
|
201
|
+
if (known !== undefined) return known;
|
|
202
|
+
const amplitude = 10 ** (STOMP_CALIBRATION_DBFS / 20) * Math.SQRT2;
|
|
203
|
+
const settle = Math.round(sampleRate * 0.1);
|
|
204
|
+
const length = settle + Math.round(sampleRate * 0.1);
|
|
205
|
+
const buffer = new Float64Array(length);
|
|
206
|
+
const w = (2 * Math.PI * 196) / sampleRate;
|
|
207
|
+
for (let i = 0; i < length; i += 1) buffer[i] = amplitude * Math.sin(w * i);
|
|
208
|
+
const circuit = stompCircuit(
|
|
209
|
+
type,
|
|
210
|
+
{ ...settings, level: 0 },
|
|
211
|
+
sampleRate * factor,
|
|
212
|
+
);
|
|
213
|
+
runSection(buffer, factor, (x) => circuit.process(x));
|
|
214
|
+
let sum = 0;
|
|
215
|
+
for (let i = settle; i < length; i += 1) sum += buffer[i]! * buffer[i]!;
|
|
216
|
+
const rms = Math.sqrt(sum / (length - settle));
|
|
217
|
+
const target = 10 ** (STOMP_CALIBRATION_DBFS / 20);
|
|
218
|
+
const makeup = rms > 1e-12 ? target / rms : 1;
|
|
219
|
+
stompMakeups.set(key, makeup);
|
|
220
|
+
if (stompMakeups.size > 4096)
|
|
221
|
+
stompMakeups.delete(stompMakeups.keys().next().value!);
|
|
222
|
+
return makeup;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Makeup for an automated gain: linear between whole steps. */
|
|
226
|
+
export function stompMakeupAt(
|
|
227
|
+
type: string,
|
|
228
|
+
settings: StompSettings,
|
|
229
|
+
sampleRate: number,
|
|
230
|
+
factor: 2 | 4,
|
|
231
|
+
): number {
|
|
232
|
+
const g = clamp(settings.gain, 0, 10);
|
|
233
|
+
const g0 = Math.min(9, Math.floor(g));
|
|
234
|
+
const at = (gain: number) =>
|
|
235
|
+
stompMakeup(type, { ...settings, gain }, sampleRate, factor);
|
|
236
|
+
const a = at(g0);
|
|
237
|
+
return a + (at(g0 + 1) - a) * (g - g0);
|
|
238
|
+
}
|
package/src/audio/engine.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type ClickLevel,
|
|
10
10
|
} from "./click.ts";
|
|
11
11
|
import type { LiveNotePcm } from "./live.ts";
|
|
12
|
+
import { RING_OUT_FADE_SECONDS } from "./instruments.ts";
|
|
12
13
|
import { measureLoudness, pcmChannels } from "./loudness.ts";
|
|
13
14
|
import type { MasterReport } from "./master.ts";
|
|
14
15
|
import { levelOf, type SoundLevel } from "./preview.ts";
|
|
@@ -308,8 +309,16 @@ type LiveVoice = {
|
|
|
308
309
|
/** Fade-out frames left once released early; undefined while sounding. */
|
|
309
310
|
fadeLeft: number | undefined;
|
|
310
311
|
fadeFrames: number;
|
|
312
|
+
/** Rings to its end on note-off (0.6.1 modal ring-out). */
|
|
313
|
+
ringOut?: boolean;
|
|
311
314
|
};
|
|
312
315
|
|
|
316
|
+
/**
|
|
317
|
+
* At most this many ring-out voices sound at once; the oldest beyond it fades
|
|
318
|
+
* over `RING_OUT_FADE_SECONDS` (spec 7.3: 32 resonator voices).
|
|
319
|
+
*/
|
|
320
|
+
export const MAX_RING_OUT_VOICES = 32;
|
|
321
|
+
|
|
313
322
|
type PlayRequest = {
|
|
314
323
|
score: TrackScore;
|
|
315
324
|
beat: number | undefined;
|
|
@@ -530,25 +539,66 @@ export class AudioEngine {
|
|
|
530
539
|
existing.pcm = note.pcm;
|
|
531
540
|
existing.frames = note.frames;
|
|
532
541
|
existing.fadeLeft = undefined;
|
|
533
|
-
|
|
542
|
+
existing.fadeFrames = this.releaseFrames(note);
|
|
543
|
+
existing.ringOut = note.ringOut === true;
|
|
544
|
+
} else {
|
|
534
545
|
this.voices.set(id, {
|
|
535
546
|
pcm: note.pcm,
|
|
536
547
|
frames: note.frames,
|
|
537
548
|
position: 0,
|
|
538
549
|
fadeLeft: undefined,
|
|
539
|
-
fadeFrames:
|
|
550
|
+
fadeFrames: this.releaseFrames(note),
|
|
551
|
+
...(note.ringOut ? { ringOut: true } : {}),
|
|
540
552
|
});
|
|
553
|
+
if (note.ringOut) this.capRingOut();
|
|
554
|
+
}
|
|
541
555
|
// Write what is due now so the voice joins the very next chunk.
|
|
542
556
|
const first = this.written;
|
|
543
557
|
this.pump();
|
|
544
558
|
return this.frameMs(first);
|
|
545
559
|
}
|
|
546
560
|
|
|
547
|
-
/**
|
|
548
|
-
|
|
561
|
+
/** A live voice's note-off fade: 10 ms, or the instrument's release. */
|
|
562
|
+
private releaseFrames(note: LiveNotePcm): number {
|
|
563
|
+
return Math.max(
|
|
564
|
+
1,
|
|
565
|
+
Math.round(this.sampleRate * (note.releaseSeconds ?? 0.01)),
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Past `MAX_RING_OUT_VOICES` ringing voices, fade the oldest still at full
|
|
571
|
+
* level over `RING_OUT_FADE_SECONDS` (Map order is start order).
|
|
572
|
+
*/
|
|
573
|
+
private capRingOut(): void {
|
|
574
|
+
let ringing = 0;
|
|
575
|
+
for (const voice of this.voices.values())
|
|
576
|
+
if (voice.ringOut && voice.fadeLeft === undefined) ringing += 1;
|
|
577
|
+
for (const voice of this.voices.values()) {
|
|
578
|
+
if (ringing <= MAX_RING_OUT_VOICES) break;
|
|
579
|
+
if (!voice.ringOut || voice.fadeLeft !== undefined) continue;
|
|
580
|
+
this.fadeRingOut(voice);
|
|
581
|
+
ringing -= 1;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private fadeRingOut(voice: LiveVoice): void {
|
|
586
|
+
voice.fadeFrames = Math.max(
|
|
587
|
+
1,
|
|
588
|
+
Math.round(this.sampleRate * RING_OUT_FADE_SECONDS),
|
|
589
|
+
);
|
|
590
|
+
voice.fadeLeft = voice.fadeFrames;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Fade a live voice out over ~10 ms (sustain lifted, mode left). A
|
|
595
|
+
* ring-out voice keeps ringing; `noteOff(id, true)` silences it too.
|
|
596
|
+
*/
|
|
597
|
+
public noteOff(id: number, force = false): void {
|
|
549
598
|
const voice = this.voices.get(id);
|
|
550
|
-
if (voice
|
|
551
|
-
|
|
599
|
+
if (!voice || voice.fadeLeft !== undefined) return;
|
|
600
|
+
if (!voice.ringOut) voice.fadeLeft = voice.fadeFrames;
|
|
601
|
+
else if (force) this.fadeRingOut(voice);
|
|
552
602
|
}
|
|
553
603
|
|
|
554
604
|
/** Frames of live voice `id` already mixed, or undefined once finished. */
|