@hraness/dawg 0.6.1 → 0.7.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/CHANGELOG.md +92 -0
- package/DAWG.md +343 -116
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +832 -13
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +276 -3
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +1849 -42
- package/core/sections.ts +292 -26
- package/core/sing.ts +815 -0
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -13
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +126 -14
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +12 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +4 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +108 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +11 -7
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +37 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +10 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +160 -28
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +558 -0
- package/src/commands/help.ts +610 -380
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +8 -8
- package/src/commands/modal.ts +1 -1
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sing.ts +478 -0
- package/src/commands/strum.ts +13 -1
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +2 -2
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1397 -253
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +20 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +759 -177
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +47 -7
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +3 -3
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formant shift at constant pitch (0.7 `fx.formant`): the throat or gender
|
|
3
|
+
* knob. Per STFT frame the cepstral spectral envelope E is taken and each
|
|
4
|
+
* bin is scaled by E(k / a) / E(k), a = 2^(shift / 12), with its phase left
|
|
5
|
+
* untouched, then the frames overlap-add. The pitch never moves, so no
|
|
6
|
+
* phase vocoder is needed (Röbel and Rodet, "Efficient spectral envelope
|
|
7
|
+
* estimation and its application to pitch shifting and envelope
|
|
8
|
+
* preservation", DAFx 2005; Little AlterBoy and VocalSynth's formant knob).
|
|
9
|
+
*
|
|
10
|
+
* The lifter adapts to the voice: 0.75 periods of a 5-frame median
|
|
11
|
+
* autocorrelation f0, clamped to 1-2 ms, 1 ms when unvoiced. A fixed 2 ms
|
|
12
|
+
* lifter resolves single harmonics above about 500 Hz and makes high
|
|
13
|
+
* voices worse than no processing (design formant.md section 2).
|
|
14
|
+
*
|
|
15
|
+
* Frames sit at multiples of the hop in absolute track samples (an
|
|
16
|
+
* arranged window passes its origin as `seedSeconds`), and a window padded
|
|
17
|
+
* back by `FORMANT_PAD` samples and forward by one frame equals the same
|
|
18
|
+
* span of a full render bit for bit.
|
|
19
|
+
*/
|
|
20
|
+
import type { FxValues } from "../../../core/fx.ts";
|
|
21
|
+
import type { Track } from "../../../core/score.ts";
|
|
22
|
+
import { cepstralEnvelope, envelopeScratch } from "../dsp/envelope.ts";
|
|
23
|
+
import { fftInPlace } from "../dsp/fft.ts";
|
|
24
|
+
import { hann, HANN_SQUARED_OLA_QUARTER } from "../dsp/stft.ts";
|
|
25
|
+
import { fxReader, type EffectContext } from "./common.ts";
|
|
26
|
+
|
|
27
|
+
/** Per-bin gain clamp (24 dB), as shift.ts holds formants. */
|
|
28
|
+
const MAX_GAIN = 10 ** (24 / 20);
|
|
29
|
+
/** Lifter periods and clamps (design formant.md section 2). */
|
|
30
|
+
const LIFTER_PERIODS = 0.75;
|
|
31
|
+
const LIFTER_MIN_SECONDS = 0.001;
|
|
32
|
+
const LIFTER_MAX_SECONDS = 0.002;
|
|
33
|
+
/** Frames in the f0 median (this one and the four before it). */
|
|
34
|
+
const F0_MEDIAN = 5;
|
|
35
|
+
const F0_MIN_HZ = 60;
|
|
36
|
+
const F0_MAX_HZ = 1500;
|
|
37
|
+
/** Below this normalized autocorrelation a frame counts as unvoiced. */
|
|
38
|
+
const VOICED = 0.45;
|
|
39
|
+
/** The first peak within this fraction of the best is the period. */
|
|
40
|
+
const FIRST_PEAK = 0.85;
|
|
41
|
+
|
|
42
|
+
/** Frame length: 1024 at or below 24 kHz, 2048 above. */
|
|
43
|
+
export function formantFrame(sampleRate: number): number {
|
|
44
|
+
return sampleRate > 24_000 ? 2048 : 1024;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Samples a window render must start before its first output sample (4
|
|
49
|
+
* hops for the f0 median plus one frame), and it renders one frame past
|
|
50
|
+
* its last.
|
|
51
|
+
*/
|
|
52
|
+
export function formantPad(sampleRate: number): number {
|
|
53
|
+
const n = formantFrame(sampleRate);
|
|
54
|
+
return n + 4 * (n / 4);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type FormantShiftOptions = Readonly<{
|
|
58
|
+
/** Wet amount 0..1 (1: fully shifted). */
|
|
59
|
+
mix?: number | ((index: number) => number);
|
|
60
|
+
/** Absolute track sample of `x[0]`; frames anchor at hop multiples of it. */
|
|
61
|
+
origin?: number;
|
|
62
|
+
}>;
|
|
63
|
+
|
|
64
|
+
type Analysis = {
|
|
65
|
+
n: number;
|
|
66
|
+
hop: number;
|
|
67
|
+
win: Float64Array;
|
|
68
|
+
wac: Float64Array;
|
|
69
|
+
minLag: number;
|
|
70
|
+
maxLag: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const analyses = new Map<number, Analysis>();
|
|
74
|
+
|
|
75
|
+
function analysisFor(sampleRate: number): Analysis {
|
|
76
|
+
let a = analyses.get(sampleRate);
|
|
77
|
+
if (a) return a;
|
|
78
|
+
const n = formantFrame(sampleRate);
|
|
79
|
+
const win = hann(n);
|
|
80
|
+
const minLag = Math.floor(sampleRate / F0_MAX_HZ);
|
|
81
|
+
const maxLag = Math.min(n / 2 - 1, Math.ceil(sampleRate / F0_MIN_HZ));
|
|
82
|
+
// The window's own autocorrelation, divided out so long lags are not
|
|
83
|
+
// biased down (Boersma 1993).
|
|
84
|
+
const wac = new Float64Array(maxLag + 1);
|
|
85
|
+
for (let t = 0; t <= maxLag; t += 1) {
|
|
86
|
+
let sum = 0;
|
|
87
|
+
for (let i = 0; i + t < n; i += 1) sum += win[i]! * win[i + t]!;
|
|
88
|
+
wac[t] = sum;
|
|
89
|
+
}
|
|
90
|
+
a = { n, hop: n / 4, win, wac, minLag, maxLag };
|
|
91
|
+
analyses.set(sampleRate, a);
|
|
92
|
+
return a;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One frame's f0 from its power spectrum (0: unvoiced). */
|
|
96
|
+
function frameF0(
|
|
97
|
+
mag: Float64Array,
|
|
98
|
+
a: Analysis,
|
|
99
|
+
sampleRate: number,
|
|
100
|
+
re: Float64Array,
|
|
101
|
+
im: Float64Array,
|
|
102
|
+
): number {
|
|
103
|
+
const { n, wac, minLag, maxLag } = a;
|
|
104
|
+
const half = n / 2;
|
|
105
|
+
for (let k = 0; k <= half; k += 1) {
|
|
106
|
+
re[k] = mag[k]! * mag[k]!;
|
|
107
|
+
im[k] = 0;
|
|
108
|
+
if (k > 0 && k < half) {
|
|
109
|
+
re[n - k] = re[k]!;
|
|
110
|
+
im[n - k] = 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
fftInPlace(re, im, true);
|
|
114
|
+
const r0 = re[0]!;
|
|
115
|
+
if (r0 <= 1e-12) return 0;
|
|
116
|
+
const w0 = wac[0]! / r0;
|
|
117
|
+
for (let t = minLag - 1; t <= maxLag; t += 1) re[t] = (re[t]! * w0) / wac[t]!;
|
|
118
|
+
let best = 0;
|
|
119
|
+
for (let t = minLag; t <= maxLag; t += 1) best = Math.max(best, re[t]!);
|
|
120
|
+
if (best < VOICED) return 0;
|
|
121
|
+
for (let t = minLag + 1; t < maxLag; t += 1) {
|
|
122
|
+
const v = re[t]!;
|
|
123
|
+
if (v >= FIRST_PEAK * best && v >= re[t - 1]! && v >= re[t + 1]!) {
|
|
124
|
+
const l = re[t - 1]!;
|
|
125
|
+
const r = re[t + 1]!;
|
|
126
|
+
const d = l - 2 * v + r;
|
|
127
|
+
const offset = d < 0 ? (0.5 * (l - r)) / d : 0;
|
|
128
|
+
return sampleRate / (t + offset);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Shift the formants of `x` by `shift` semitones (a number, or a function
|
|
136
|
+
* of the buffer index read once per hop) at constant pitch. Returns a new
|
|
137
|
+
* buffer; `shift` 0 everywhere returns a copy of `x`.
|
|
138
|
+
*/
|
|
139
|
+
export function formantShift(
|
|
140
|
+
x: Float64Array,
|
|
141
|
+
sampleRate: number,
|
|
142
|
+
shift: number | ((index: number) => number),
|
|
143
|
+
options: FormantShiftOptions = {},
|
|
144
|
+
): Float64Array {
|
|
145
|
+
if (shift === 0) return Float64Array.from(x);
|
|
146
|
+
const a = analysisFor(sampleRate);
|
|
147
|
+
const { n, hop, win } = a;
|
|
148
|
+
const half = n / 2;
|
|
149
|
+
const origin = Math.round(options.origin ?? 0);
|
|
150
|
+
const out = new Float64Array(x.length);
|
|
151
|
+
const re = new Float64Array(n);
|
|
152
|
+
const im = new Float64Array(n);
|
|
153
|
+
const acRe = new Float64Array(n);
|
|
154
|
+
const acIm = new Float64Array(n);
|
|
155
|
+
const mag = new Float64Array(half + 1);
|
|
156
|
+
const env = new Float64Array(half + 1);
|
|
157
|
+
const scratch = envelopeScratch(n);
|
|
158
|
+
const history = new Float64Array(F0_MEDIAN);
|
|
159
|
+
const sorted = new Float64Array(F0_MEDIAN);
|
|
160
|
+
let frames = 0;
|
|
161
|
+
const ola = 1 / HANN_SQUARED_OLA_QUARTER;
|
|
162
|
+
const auto = typeof shift === "function";
|
|
163
|
+
// First frame: the latest hop multiple (absolute) whose frame still
|
|
164
|
+
// reaches x[0].
|
|
165
|
+
const firstAbs = Math.floor((origin - n + 1 + (hop - 1)) / hop) * hop;
|
|
166
|
+
for (let start = firstAbs - origin; start < x.length; start += hop) {
|
|
167
|
+
const st = auto ? shift(Math.max(0, start + half)) : shift;
|
|
168
|
+
for (let i = 0; i < n; i += 1) {
|
|
169
|
+
const j = start + i;
|
|
170
|
+
re[i] = j >= 0 && j < x.length ? x[j]! * win[i]! : 0;
|
|
171
|
+
im[i] = 0;
|
|
172
|
+
}
|
|
173
|
+
fftInPlace(re, im);
|
|
174
|
+
for (let k = 0; k <= half; k += 1) mag[k] = Math.hypot(re[k]!, im[k]!);
|
|
175
|
+
// Every frame joins the f0 history, so a window and the full render
|
|
176
|
+
// agree whatever the shift was.
|
|
177
|
+
history[frames % F0_MEDIAN] = frameF0(mag, a, sampleRate, acRe, acIm);
|
|
178
|
+
frames += 1;
|
|
179
|
+
if (st === 0) {
|
|
180
|
+
fftInPlace(re, im, true);
|
|
181
|
+
} else {
|
|
182
|
+
const m = Math.min(frames, F0_MEDIAN);
|
|
183
|
+
for (let i = 0; i < m; i += 1) sorted[i] = history[i]!;
|
|
184
|
+
for (let i = 1; i < m; i += 1)
|
|
185
|
+
for (let j = i; j > 0 && sorted[j - 1]! > sorted[j]!; j -= 1) {
|
|
186
|
+
const tmp = sorted[j]!;
|
|
187
|
+
sorted[j] = sorted[j - 1]!;
|
|
188
|
+
sorted[j - 1] = tmp;
|
|
189
|
+
}
|
|
190
|
+
const f0 = sorted[m >> 1]!;
|
|
191
|
+
const seconds =
|
|
192
|
+
f0 > 0
|
|
193
|
+
? Math.min(
|
|
194
|
+
LIFTER_MAX_SECONDS,
|
|
195
|
+
Math.max(LIFTER_MIN_SECONDS, LIFTER_PERIODS / f0),
|
|
196
|
+
)
|
|
197
|
+
: LIFTER_MIN_SECONDS;
|
|
198
|
+
const lifter = Math.max(4, Math.round(seconds * sampleRate));
|
|
199
|
+
cepstralEnvelope(mag, n, lifter, env, scratch);
|
|
200
|
+
const ratio = 2 ** (-st / 12);
|
|
201
|
+
for (let k = 0; k <= half; k += 1) {
|
|
202
|
+
const source = k * ratio;
|
|
203
|
+
const i0 = Math.floor(source);
|
|
204
|
+
const f = source - i0;
|
|
205
|
+
const e =
|
|
206
|
+
i0 >= half
|
|
207
|
+
? env[half]!
|
|
208
|
+
: env[i0]! + (env[Math.min(half, i0 + 1)]! - env[i0]!) * f;
|
|
209
|
+
const g = Math.min(
|
|
210
|
+
MAX_GAIN,
|
|
211
|
+
Math.max(1 / MAX_GAIN, e / Math.max(env[k]!, 1e-12)),
|
|
212
|
+
);
|
|
213
|
+
re[k] = re[k]! * g;
|
|
214
|
+
im[k] = im[k]! * g;
|
|
215
|
+
if (k > 0 && k < half) {
|
|
216
|
+
re[n - k] = re[k]!;
|
|
217
|
+
im[n - k] = -im[k]!;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
fftInPlace(re, im, true);
|
|
221
|
+
}
|
|
222
|
+
for (let i = 0; i < n; i += 1) {
|
|
223
|
+
const j = start + i;
|
|
224
|
+
if (j >= 0 && j < x.length)
|
|
225
|
+
out[j] = out[j]! + (re[i]! / n) * win[i]! * ola;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const mix = options.mix ?? 1;
|
|
229
|
+
if (typeof mix === "function")
|
|
230
|
+
for (let i = 0; i < x.length; i += 1)
|
|
231
|
+
out[i] = x[i]! + (out[i]! - x[i]!) * mix(i);
|
|
232
|
+
else if (mix < 1)
|
|
233
|
+
for (let i = 0; i < x.length; i += 1)
|
|
234
|
+
out[i] = x[i]! + (out[i]! - x[i]!) * mix;
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** `fx.formant`: formant shift with mix, both automatable. */
|
|
239
|
+
export function applyFormant(
|
|
240
|
+
buffer: Float64Array,
|
|
241
|
+
track: Track,
|
|
242
|
+
values: FxValues,
|
|
243
|
+
context: EffectContext,
|
|
244
|
+
): void {
|
|
245
|
+
const read = fxReader(track, "formant", values, context);
|
|
246
|
+
const shift = read.param("shift");
|
|
247
|
+
const mix = read.param("mix");
|
|
248
|
+
if (!shift.automated && shift.fallback === 0) return;
|
|
249
|
+
if (!mix.automated && mix.fallback <= 0) return;
|
|
250
|
+
const origin = context.seedSeconds
|
|
251
|
+
? Math.round(context.seedSeconds * context.sampleRate)
|
|
252
|
+
: 0;
|
|
253
|
+
const out = formantShift(
|
|
254
|
+
buffer,
|
|
255
|
+
context.sampleRate,
|
|
256
|
+
shift.automated ? (index) => shift.at(index) : shift.fallback,
|
|
257
|
+
{
|
|
258
|
+
origin,
|
|
259
|
+
mix: mix.automated ? (index) => mix.at(index) : mix.fallback,
|
|
260
|
+
},
|
|
261
|
+
);
|
|
262
|
+
buffer.set(out);
|
|
263
|
+
}
|
package/src/audio/engine.ts
CHANGED
|
@@ -16,6 +16,14 @@ import { levelOf, type SoundLevel } from "./preview.ts";
|
|
|
16
16
|
import { DEFAULT_SAMPLE_RATE, RENDER_CHANNELS } from "./wav.ts";
|
|
17
17
|
import { playbackTime } from "./arrange.ts";
|
|
18
18
|
import { transportMapFor, type TransportMap } from "./clock.ts";
|
|
19
|
+
import {
|
|
20
|
+
openNativePlayer,
|
|
21
|
+
probeNativeSink,
|
|
22
|
+
type AudioTiming,
|
|
23
|
+
type NativeProbe,
|
|
24
|
+
type PlayerClock,
|
|
25
|
+
type SinkLibrary,
|
|
26
|
+
} from "./native.ts";
|
|
19
27
|
|
|
20
28
|
/**
|
|
21
29
|
* How dawgd (or a file-mode window) makes sound.
|
|
@@ -30,7 +38,8 @@ import { transportMapFor, type TransportMap } from "./clock.ts";
|
|
|
30
38
|
* s16le from stdin; tests use it to record the exact byte stream.
|
|
31
39
|
* - `none` makes no sound; the transport clock still runs.
|
|
32
40
|
*/
|
|
33
|
-
export type AudioBackend =
|
|
41
|
+
export type AudioBackend =
|
|
42
|
+
"native" | "ffplay" | "sox" | "afplay" | "command" | "none";
|
|
34
43
|
|
|
35
44
|
export type AudioBackendInfo = Readonly<{
|
|
36
45
|
backend: AudioBackend;
|
|
@@ -40,6 +49,10 @@ export type AudioBackendInfo = Readonly<{
|
|
|
40
49
|
command?: readonly string[];
|
|
41
50
|
/** Human-readable reason the backend was chosen. */
|
|
42
51
|
detail: string;
|
|
52
|
+
/** The loaded sink, for the `native` backend. */
|
|
53
|
+
native?: SinkLibrary;
|
|
54
|
+
/** Why the native sink is not in use, when another backend was picked. */
|
|
55
|
+
nativeUnavailable?: string;
|
|
43
56
|
}>;
|
|
44
57
|
|
|
45
58
|
type Which = (binary: string) => string | null;
|
|
@@ -49,8 +62,17 @@ export type DetectOptions = Readonly<{
|
|
|
49
62
|
which?: Which;
|
|
50
63
|
platform?: NodeJS.Platform;
|
|
51
64
|
sampleRate?: number;
|
|
65
|
+
/** Native sink probe; `false` skips it (tests). Default: the real one. */
|
|
66
|
+
native?: (() => NativeProbe) | false;
|
|
52
67
|
}>;
|
|
53
68
|
|
|
69
|
+
let cachedProbe: NativeProbe | undefined;
|
|
70
|
+
/** The real probe, once per process (dlopen is not repeated). */
|
|
71
|
+
function defaultNativeProbe(): NativeProbe {
|
|
72
|
+
cachedProbe ??= probeNativeSink();
|
|
73
|
+
return cachedProbe;
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
/** Streaming argv for the known stdin players. */
|
|
55
77
|
export function streamingCommand(
|
|
56
78
|
backend: "ffplay" | "sox",
|
|
@@ -99,8 +121,9 @@ export function streamingCommand(
|
|
|
99
121
|
|
|
100
122
|
/**
|
|
101
123
|
* Pick the audio backend. Order: DAWG_AUDIO=0 (none), DAWG_AUDIO_PLAYER
|
|
102
|
-
* (explicit stdin command), DAWG_AUDIO_BACKEND (forced name), then
|
|
103
|
-
* sox `play`, and afplay (macOS) in that order.
|
|
124
|
+
* (explicit stdin command), DAWG_AUDIO_BACKEND (forced name), then the
|
|
125
|
+
* native sink, ffplay, sox `play`, and afplay (macOS) in that order. When
|
|
126
|
+
* the native sink cannot load, the fallback's detail says why.
|
|
104
127
|
*/
|
|
105
128
|
export function detectAudioBackend(
|
|
106
129
|
options: DetectOptions = {},
|
|
@@ -128,13 +151,40 @@ export function detectAudioBackend(
|
|
|
128
151
|
}
|
|
129
152
|
const forced = env.DAWG_AUDIO_BACKEND?.trim().toLowerCase();
|
|
130
153
|
const candidates: AudioBackend[] =
|
|
154
|
+
forced === "native" ||
|
|
131
155
|
forced === "ffplay" ||
|
|
132
156
|
forced === "sox" ||
|
|
133
157
|
forced === "afplay" ||
|
|
134
158
|
forced === "none"
|
|
135
159
|
? [forced]
|
|
136
|
-
: ["ffplay", "sox", "afplay"];
|
|
160
|
+
: ["native", "ffplay", "sox", "afplay"];
|
|
161
|
+
let unavailable: string | undefined;
|
|
162
|
+
const why = (info: AudioBackendInfo): AudioBackendInfo =>
|
|
163
|
+
unavailable
|
|
164
|
+
? {
|
|
165
|
+
...info,
|
|
166
|
+
detail: `${info.detail} · native sink unavailable: ${unavailable}`,
|
|
167
|
+
nativeUnavailable: unavailable,
|
|
168
|
+
}
|
|
169
|
+
: info;
|
|
137
170
|
for (const candidate of candidates) {
|
|
171
|
+
if (candidate === "native") {
|
|
172
|
+
if (options.native === false) continue;
|
|
173
|
+
const probe = options.native
|
|
174
|
+
? options.native()
|
|
175
|
+
: env === process.env && options.platform === undefined
|
|
176
|
+
? defaultNativeProbe()
|
|
177
|
+
: probeNativeSink({ env, platform });
|
|
178
|
+
if (probe.ok)
|
|
179
|
+
return {
|
|
180
|
+
backend: "native",
|
|
181
|
+
streaming: true,
|
|
182
|
+
native: probe.library,
|
|
183
|
+
detail: `native sink ${probe.detail} (device callback)`,
|
|
184
|
+
};
|
|
185
|
+
unavailable = probe.reason;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
138
188
|
if (candidate === "none")
|
|
139
189
|
return {
|
|
140
190
|
backend: "none",
|
|
@@ -144,26 +194,26 @@ export function detectAudioBackend(
|
|
|
144
194
|
if (candidate === "ffplay" || candidate === "sox") {
|
|
145
195
|
const binary = which(candidate === "ffplay" ? "ffplay" : "play");
|
|
146
196
|
if (!binary) continue;
|
|
147
|
-
return {
|
|
197
|
+
return why({
|
|
148
198
|
backend: candidate,
|
|
149
199
|
streaming: true,
|
|
150
200
|
command: streamingCommand(candidate, binary, sampleRate),
|
|
151
201
|
detail: `${binary} (gapless stream)`,
|
|
152
|
-
};
|
|
202
|
+
});
|
|
153
203
|
}
|
|
154
204
|
const binary = platform === "darwin" ? which("afplay") : null;
|
|
155
205
|
if (binary)
|
|
156
|
-
return {
|
|
206
|
+
return why({
|
|
157
207
|
backend: "afplay",
|
|
158
208
|
streaming: false,
|
|
159
209
|
detail: `${binary} (re-render on edit; install ffmpeg or sox for gapless playback)`,
|
|
160
|
-
};
|
|
210
|
+
});
|
|
161
211
|
}
|
|
162
|
-
return {
|
|
212
|
+
return why({
|
|
163
213
|
backend: "none",
|
|
164
214
|
streaming: false,
|
|
165
215
|
detail: "no player found (install ffmpeg or sox)",
|
|
166
|
-
};
|
|
216
|
+
});
|
|
167
217
|
}
|
|
168
218
|
|
|
169
219
|
/** One status line for `dawg auth status` and the header. */
|
|
@@ -205,8 +255,21 @@ type PlayerProcess = {
|
|
|
205
255
|
readonly stdin: Sink;
|
|
206
256
|
readonly exited: Promise<number>;
|
|
207
257
|
kill(): void;
|
|
258
|
+
/** Device-clocked players (the native sink) pace the pump themselves. */
|
|
259
|
+
readonly clock?: PlayerClock;
|
|
208
260
|
};
|
|
209
261
|
|
|
262
|
+
/** Play-mode lead on the native sink: the ring only covers pump jitter. */
|
|
263
|
+
export const NATIVE_PLAY_LEAD_MS = 15;
|
|
264
|
+
/** Pump interval on the native sink. */
|
|
265
|
+
export const NATIVE_TICK_MS = 5;
|
|
266
|
+
|
|
267
|
+
function envNumber(value: string | undefined): number | undefined {
|
|
268
|
+
if (value === undefined || value.trim() === "") return undefined;
|
|
269
|
+
const number = Number(value);
|
|
270
|
+
return Number.isFinite(number) && number >= 0 ? number : undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
210
273
|
export type AudioEngineOptions = Readonly<{
|
|
211
274
|
lockPath?: string;
|
|
212
275
|
info?: AudioBackendInfo;
|
|
@@ -228,6 +291,12 @@ export type AudioEngineOptions = Readonly<{
|
|
|
228
291
|
respawnMs?: number;
|
|
229
292
|
/** Player lifecycle notices worth a status line. */
|
|
230
293
|
onStatus?: (status: AudioStatus) => void;
|
|
294
|
+
/** Play-mode lead (default 60 ms; 15 ms on the native sink). */
|
|
295
|
+
playLeadMs?: number;
|
|
296
|
+
/** Native sink output device name (default: system default). */
|
|
297
|
+
device?: string;
|
|
298
|
+
/** Native sink device buffer frames (default 128; 0 = device default). */
|
|
299
|
+
bufferFrames?: number;
|
|
231
300
|
}>;
|
|
232
301
|
|
|
233
302
|
export type AudioStatus = Readonly<{
|
|
@@ -240,7 +309,13 @@ export type AudioStatus = Readonly<{
|
|
|
240
309
|
const MAX_RESPAWNS = 3;
|
|
241
310
|
/** A player that ran this long before dying resets the respawn budget. */
|
|
242
311
|
const RESPAWN_RESET_MS = 10_000;
|
|
243
|
-
/**
|
|
312
|
+
/**
|
|
313
|
+
* Default queue lead: the edit-to-audible floor while playing. 100 ms leaves
|
|
314
|
+
* 80 ms of slack over the 20 ms pump tick for main-thread stalls (a frame
|
|
315
|
+
* costs about 1 ms; play mode already runs at 60 ms).
|
|
316
|
+
*/
|
|
317
|
+
const DEFAULT_LEAD_MS = 100;
|
|
318
|
+
/** Inline renders grow the lead to this multiple of the last render time. */
|
|
244
319
|
const LEAD_RENDER_FACTOR = 1.5;
|
|
245
320
|
|
|
246
321
|
type Loop = Readonly<{
|
|
@@ -334,8 +409,8 @@ type PlayRequest = {
|
|
|
334
409
|
*
|
|
335
410
|
* Timing: the engine is paced by the same monotonic clock as the transport.
|
|
336
411
|
* Frame `n` of the stream belongs to wall time `start + n / rate`, and the
|
|
337
|
-
* engine keeps a lead of audio queued ahead of now:
|
|
338
|
-
*
|
|
412
|
+
* engine keeps a lead of audio queued ahead of now: `leadMs`, or 1.5x the
|
|
413
|
+
* last render time when renders run inline (no worker) and are slower. The device
|
|
339
414
|
* plays the queue at its own rate, so what you hear trails the transport by
|
|
340
415
|
* the player's fixed output latency and never accumulates drift from
|
|
341
416
|
* re-renders. Renders run in a worker; an edit re-renders off-thread and
|
|
@@ -351,6 +426,8 @@ export class AudioEngine {
|
|
|
351
426
|
private leadFrames: number;
|
|
352
427
|
private readonly defaultLeadFrames: number;
|
|
353
428
|
private readonly tickMs: number;
|
|
429
|
+
/** The lead play mode and auditions pin (see `setLeadMs`). */
|
|
430
|
+
public readonly playLeadMs: number;
|
|
354
431
|
private readonly now: () => number;
|
|
355
432
|
private readonly useTimer: boolean;
|
|
356
433
|
private readonly spawnPlayer: (command: readonly string[]) => PlayerProcess;
|
|
@@ -401,17 +478,37 @@ export class AudioEngine {
|
|
|
401
478
|
options.info ?? detectAudioBackend({ sampleRate: this.sampleRate });
|
|
402
479
|
this.lock = new PlaybackLock(options.lockPath);
|
|
403
480
|
this.leadFrames = Math.round(
|
|
404
|
-
((options.leadMs ??
|
|
481
|
+
((options.leadMs ?? DEFAULT_LEAD_MS) * this.sampleRate) / 1000,
|
|
405
482
|
);
|
|
406
483
|
this.defaultLeadFrames = this.leadFrames;
|
|
407
484
|
this.fadeFrames = Math.max(
|
|
408
485
|
1,
|
|
409
486
|
Math.round(SWAP_FADE_SECONDS * this.sampleRate),
|
|
410
487
|
);
|
|
411
|
-
|
|
488
|
+
const native =
|
|
489
|
+
this.info.backend === "native" ? this.info.native : undefined;
|
|
490
|
+
this.tickMs = options.tickMs ?? (native ? NATIVE_TICK_MS : 20);
|
|
491
|
+
this.playLeadMs =
|
|
492
|
+
options.playLeadMs ??
|
|
493
|
+
envNumber(process.env.DAWG_PLAY_LEAD_MS) ??
|
|
494
|
+
(native ? NATIVE_PLAY_LEAD_MS : 60);
|
|
412
495
|
this.now = options.now ?? (() => performance.now());
|
|
413
496
|
this.useTimer = options.timer ?? true;
|
|
414
|
-
|
|
497
|
+
const device = options.device ?? process.env.DAWG_AUDIO_DEVICE?.trim();
|
|
498
|
+
const bufferFrames =
|
|
499
|
+
options.bufferFrames ?? envNumber(process.env.DAWG_AUDIO_BUFFER) ?? 128;
|
|
500
|
+
this.spawnPlayer =
|
|
501
|
+
options.spawn ??
|
|
502
|
+
(native
|
|
503
|
+
? () =>
|
|
504
|
+
openNativePlayer(native, {
|
|
505
|
+
rate: this.sampleRate,
|
|
506
|
+
channels: RENDER_CHANNELS,
|
|
507
|
+
...(device ? { device } : {}),
|
|
508
|
+
bufferFrames,
|
|
509
|
+
now: this.now,
|
|
510
|
+
})
|
|
511
|
+
: spawnStdinPlayer);
|
|
415
512
|
this.respawnMs = options.respawnMs ?? 250;
|
|
416
513
|
this.onStatus = options.onStatus;
|
|
417
514
|
this.renderer = new LoopRenderer({
|
|
@@ -452,6 +549,17 @@ export class AudioEngine {
|
|
|
452
549
|
: Math.max(1, Math.round((ms * this.sampleRate) / 1000));
|
|
453
550
|
}
|
|
454
551
|
|
|
552
|
+
/**
|
|
553
|
+
* A few words on how play mode sounds, for the play strip: the native
|
|
554
|
+
* sink, or the stdin player and why the sink is not in use.
|
|
555
|
+
*/
|
|
556
|
+
public get audioNote(): string {
|
|
557
|
+
if (this.info.backend === "native") return "native sink";
|
|
558
|
+
return this.info.nativeUnavailable
|
|
559
|
+
? `${this.info.backend} · no native sink`
|
|
560
|
+
: this.info.backend;
|
|
561
|
+
}
|
|
562
|
+
|
|
455
563
|
/** Lead in milliseconds: how far ahead of now a new live voice sounds. */
|
|
456
564
|
public get leadMs(): number {
|
|
457
565
|
return (this.lead * 1000) / this.sampleRate;
|
|
@@ -619,14 +727,14 @@ export class AudioEngine {
|
|
|
619
727
|
|
|
620
728
|
/** Frames kept queued ahead of the clock right now. */
|
|
621
729
|
public get lead(): number {
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
730
|
+
// Off-thread renders never stall the pump: a slow render delays the
|
|
731
|
+
// swap, not the stream, so only inline renders grow the lead (every
|
|
732
|
+
// frame of lead is a frame of edit latency).
|
|
733
|
+
const adaptive = this.renderer.offThread
|
|
734
|
+
? 0
|
|
735
|
+
: Math.round(
|
|
736
|
+
(LEAD_RENDER_FACTOR * this.lastRenderMs * this.sampleRate) / 1000,
|
|
737
|
+
);
|
|
630
738
|
// The pump never queues more than a second, so the lead stays under it.
|
|
631
739
|
return Math.min(
|
|
632
740
|
Math.max(this.leadFrames, adaptive),
|
|
@@ -719,7 +827,18 @@ export class AudioEngine {
|
|
|
719
827
|
);
|
|
720
828
|
}
|
|
721
829
|
|
|
830
|
+
/**
|
|
831
|
+
* Stream timing on the native sink: the host time of the latest device
|
|
832
|
+
* callback, when a frame is heard, and the device latency. Undefined on
|
|
833
|
+
* the stdin players (their output latency is unknown).
|
|
834
|
+
*/
|
|
835
|
+
public get timing(): AudioTiming | undefined {
|
|
836
|
+
return this.child?.clock?.timing();
|
|
837
|
+
}
|
|
838
|
+
|
|
722
839
|
private queuedFrames(): number {
|
|
840
|
+
const clock = this.child?.clock;
|
|
841
|
+
if (clock) return clock.queuedFrames();
|
|
723
842
|
const elapsed = Math.floor(
|
|
724
843
|
((this.now() - this.startMs) * this.sampleRate) / 1000,
|
|
725
844
|
);
|
|
@@ -781,10 +900,21 @@ export class AudioEngine {
|
|
|
781
900
|
const child = this.child;
|
|
782
901
|
const loop = this.loop;
|
|
783
902
|
if (!child || (!loop && !this.monitoring)) return;
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
903
|
+
let remaining: number;
|
|
904
|
+
if (child.clock) {
|
|
905
|
+
// Device-clocked: top the ring up to the lead. Frames the device
|
|
906
|
+
// played as silence (an underrun) are skipped so the loop stays on
|
|
907
|
+
// the transport's beat.
|
|
908
|
+
const slip = child.clock.slipFrames();
|
|
909
|
+
if (slip > 0 && loop) this.cursor = (this.cursor + slip) % loop.frames;
|
|
910
|
+
if (slip > 0) this.endFades();
|
|
911
|
+
remaining = this.lead - child.clock.queuedFrames();
|
|
912
|
+
} else {
|
|
913
|
+
const due =
|
|
914
|
+
Math.floor(((this.now() - this.startMs) * this.sampleRate) / 1000) +
|
|
915
|
+
this.lead;
|
|
916
|
+
remaining = due - this.written;
|
|
917
|
+
}
|
|
788
918
|
if (remaining <= 0) return;
|
|
789
919
|
// Never queue more than one second at once, e.g. after a stalled loop.
|
|
790
920
|
if (remaining > this.sampleRate) {
|
|
@@ -864,6 +994,8 @@ export class AudioEngine {
|
|
|
864
994
|
|
|
865
995
|
/** Wall time (monotonic ms) stream frame `frame` sounds at. */
|
|
866
996
|
private frameMs(frame: number): number {
|
|
997
|
+
const clock = this.child?.clock;
|
|
998
|
+
if (clock) return clock.frameMs(frame);
|
|
867
999
|
return this.startMs + (frame * 1000) / this.sampleRate;
|
|
868
1000
|
}
|
|
869
1001
|
|
package/src/audio/fit.ts
CHANGED
|
@@ -53,7 +53,12 @@ const onsetCache = new Map<string, readonly number[]>();
|
|
|
53
53
|
/** Background jobs by cache key, with the window they fit (one per window). */
|
|
54
54
|
const pending = new Map<
|
|
55
55
|
string,
|
|
56
|
-
{
|
|
56
|
+
{
|
|
57
|
+
job: Generator<void, Float32Array>;
|
|
58
|
+
window: string;
|
|
59
|
+
/** Another stage's cache (0.7 autotune) takes the result instead. */
|
|
60
|
+
done?: (out: Float32Array) => void;
|
|
61
|
+
}
|
|
57
62
|
>();
|
|
58
63
|
let live = false;
|
|
59
64
|
/** True when the current live render left a voice silent while fitting. */
|
|
@@ -191,6 +196,32 @@ export function shiftedBuffer(
|
|
|
191
196
|
return out;
|
|
192
197
|
}
|
|
193
198
|
|
|
199
|
+
/** True inside `withLiveFit` (0.7: other stages defer long work too). */
|
|
200
|
+
export function liveFitActive(): boolean {
|
|
201
|
+
return live;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Runs `job` between live blocks like a long fit and hands the result to
|
|
206
|
+
* `done` (the caller's own cache); `onFitReady` fires when it lands and
|
|
207
|
+
* the live render reports `fitting` until then (0.7 autotune).
|
|
208
|
+
*/
|
|
209
|
+
export function deferLiveJob<T>(
|
|
210
|
+
key: string,
|
|
211
|
+
job: () => Generator<void, T>,
|
|
212
|
+
done: (out: T) => void,
|
|
213
|
+
): void {
|
|
214
|
+
missed = true;
|
|
215
|
+
if (pending.has(key)) return;
|
|
216
|
+
// The scheduler only steps the job and hands its value to `done`.
|
|
217
|
+
pending.set(key, {
|
|
218
|
+
job: job() as unknown as Generator<void, Float32Array>,
|
|
219
|
+
window: key,
|
|
220
|
+
done: done as unknown as (out: Float32Array) => void,
|
|
221
|
+
});
|
|
222
|
+
schedule();
|
|
223
|
+
}
|
|
224
|
+
|
|
194
225
|
function drain(job: Generator<void, Float32Array>): Float32Array {
|
|
195
226
|
for (;;) {
|
|
196
227
|
const step = job.next();
|
|
@@ -204,13 +235,14 @@ function schedule(): void {
|
|
|
204
235
|
timer = undefined;
|
|
205
236
|
const next = pending.entries().next();
|
|
206
237
|
if (next.done) return;
|
|
207
|
-
const [key, { job }] = next.value;
|
|
238
|
+
const [key, { job, done }] = next.value;
|
|
208
239
|
const until = performance.now() + 8;
|
|
209
240
|
for (;;) {
|
|
210
241
|
const step = job.next();
|
|
211
242
|
if (step.done) {
|
|
212
243
|
pending.delete(key);
|
|
213
|
-
|
|
244
|
+
if (done) done(step.value);
|
|
245
|
+
else remember(key, step.value);
|
|
214
246
|
for (const listener of listeners) listener();
|
|
215
247
|
break;
|
|
216
248
|
}
|