@hraness/dawg 0.6.0 → 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 +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- 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/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- 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 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- 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 -12
- 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 +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- 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 +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -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 +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- 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 +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -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 +171 -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/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- 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 +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- 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 +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- 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/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- 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 +244 -0
- 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 +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- 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 +36 -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 +50 -7
- 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 +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- 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 +144 -0
- 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
package/src/audio/engine.ts
CHANGED
|
@@ -9,12 +9,21 @@ 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";
|
|
15
16
|
import { DEFAULT_SAMPLE_RATE, RENDER_CHANNELS } from "./wav.ts";
|
|
16
17
|
import { playbackTime } from "./arrange.ts";
|
|
17
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";
|
|
18
27
|
|
|
19
28
|
/**
|
|
20
29
|
* How dawgd (or a file-mode window) makes sound.
|
|
@@ -29,7 +38,8 @@ import { transportMapFor, type TransportMap } from "./clock.ts";
|
|
|
29
38
|
* s16le from stdin; tests use it to record the exact byte stream.
|
|
30
39
|
* - `none` makes no sound; the transport clock still runs.
|
|
31
40
|
*/
|
|
32
|
-
export type AudioBackend =
|
|
41
|
+
export type AudioBackend =
|
|
42
|
+
"native" | "ffplay" | "sox" | "afplay" | "command" | "none";
|
|
33
43
|
|
|
34
44
|
export type AudioBackendInfo = Readonly<{
|
|
35
45
|
backend: AudioBackend;
|
|
@@ -39,6 +49,10 @@ export type AudioBackendInfo = Readonly<{
|
|
|
39
49
|
command?: readonly string[];
|
|
40
50
|
/** Human-readable reason the backend was chosen. */
|
|
41
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;
|
|
42
56
|
}>;
|
|
43
57
|
|
|
44
58
|
type Which = (binary: string) => string | null;
|
|
@@ -48,8 +62,17 @@ export type DetectOptions = Readonly<{
|
|
|
48
62
|
which?: Which;
|
|
49
63
|
platform?: NodeJS.Platform;
|
|
50
64
|
sampleRate?: number;
|
|
65
|
+
/** Native sink probe; `false` skips it (tests). Default: the real one. */
|
|
66
|
+
native?: (() => NativeProbe) | false;
|
|
51
67
|
}>;
|
|
52
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
|
+
|
|
53
76
|
/** Streaming argv for the known stdin players. */
|
|
54
77
|
export function streamingCommand(
|
|
55
78
|
backend: "ffplay" | "sox",
|
|
@@ -98,8 +121,9 @@ export function streamingCommand(
|
|
|
98
121
|
|
|
99
122
|
/**
|
|
100
123
|
* Pick the audio backend. Order: DAWG_AUDIO=0 (none), DAWG_AUDIO_PLAYER
|
|
101
|
-
* (explicit stdin command), DAWG_AUDIO_BACKEND (forced name), then
|
|
102
|
-
* 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.
|
|
103
127
|
*/
|
|
104
128
|
export function detectAudioBackend(
|
|
105
129
|
options: DetectOptions = {},
|
|
@@ -127,13 +151,40 @@ export function detectAudioBackend(
|
|
|
127
151
|
}
|
|
128
152
|
const forced = env.DAWG_AUDIO_BACKEND?.trim().toLowerCase();
|
|
129
153
|
const candidates: AudioBackend[] =
|
|
154
|
+
forced === "native" ||
|
|
130
155
|
forced === "ffplay" ||
|
|
131
156
|
forced === "sox" ||
|
|
132
157
|
forced === "afplay" ||
|
|
133
158
|
forced === "none"
|
|
134
159
|
? [forced]
|
|
135
|
-
: ["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;
|
|
136
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
|
+
}
|
|
137
188
|
if (candidate === "none")
|
|
138
189
|
return {
|
|
139
190
|
backend: "none",
|
|
@@ -143,26 +194,26 @@ export function detectAudioBackend(
|
|
|
143
194
|
if (candidate === "ffplay" || candidate === "sox") {
|
|
144
195
|
const binary = which(candidate === "ffplay" ? "ffplay" : "play");
|
|
145
196
|
if (!binary) continue;
|
|
146
|
-
return {
|
|
197
|
+
return why({
|
|
147
198
|
backend: candidate,
|
|
148
199
|
streaming: true,
|
|
149
200
|
command: streamingCommand(candidate, binary, sampleRate),
|
|
150
201
|
detail: `${binary} (gapless stream)`,
|
|
151
|
-
};
|
|
202
|
+
});
|
|
152
203
|
}
|
|
153
204
|
const binary = platform === "darwin" ? which("afplay") : null;
|
|
154
205
|
if (binary)
|
|
155
|
-
return {
|
|
206
|
+
return why({
|
|
156
207
|
backend: "afplay",
|
|
157
208
|
streaming: false,
|
|
158
209
|
detail: `${binary} (re-render on edit; install ffmpeg or sox for gapless playback)`,
|
|
159
|
-
};
|
|
210
|
+
});
|
|
160
211
|
}
|
|
161
|
-
return {
|
|
212
|
+
return why({
|
|
162
213
|
backend: "none",
|
|
163
214
|
streaming: false,
|
|
164
215
|
detail: "no player found (install ffmpeg or sox)",
|
|
165
|
-
};
|
|
216
|
+
});
|
|
166
217
|
}
|
|
167
218
|
|
|
168
219
|
/** One status line for `dawg auth status` and the header. */
|
|
@@ -204,8 +255,21 @@ type PlayerProcess = {
|
|
|
204
255
|
readonly stdin: Sink;
|
|
205
256
|
readonly exited: Promise<number>;
|
|
206
257
|
kill(): void;
|
|
258
|
+
/** Device-clocked players (the native sink) pace the pump themselves. */
|
|
259
|
+
readonly clock?: PlayerClock;
|
|
207
260
|
};
|
|
208
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
|
+
|
|
209
273
|
export type AudioEngineOptions = Readonly<{
|
|
210
274
|
lockPath?: string;
|
|
211
275
|
info?: AudioBackendInfo;
|
|
@@ -227,6 +291,12 @@ export type AudioEngineOptions = Readonly<{
|
|
|
227
291
|
respawnMs?: number;
|
|
228
292
|
/** Player lifecycle notices worth a status line. */
|
|
229
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;
|
|
230
300
|
}>;
|
|
231
301
|
|
|
232
302
|
export type AudioStatus = Readonly<{
|
|
@@ -239,7 +309,13 @@ export type AudioStatus = Readonly<{
|
|
|
239
309
|
const MAX_RESPAWNS = 3;
|
|
240
310
|
/** A player that ran this long before dying resets the respawn budget. */
|
|
241
311
|
const RESPAWN_RESET_MS = 10_000;
|
|
242
|
-
/**
|
|
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. */
|
|
243
319
|
const LEAD_RENDER_FACTOR = 1.5;
|
|
244
320
|
|
|
245
321
|
type Loop = Readonly<{
|
|
@@ -308,8 +384,16 @@ type LiveVoice = {
|
|
|
308
384
|
/** Fade-out frames left once released early; undefined while sounding. */
|
|
309
385
|
fadeLeft: number | undefined;
|
|
310
386
|
fadeFrames: number;
|
|
387
|
+
/** Rings to its end on note-off (0.6.1 modal ring-out). */
|
|
388
|
+
ringOut?: boolean;
|
|
311
389
|
};
|
|
312
390
|
|
|
391
|
+
/**
|
|
392
|
+
* At most this many ring-out voices sound at once; the oldest beyond it fades
|
|
393
|
+
* over `RING_OUT_FADE_SECONDS` (spec 7.3: 32 resonator voices).
|
|
394
|
+
*/
|
|
395
|
+
export const MAX_RING_OUT_VOICES = 32;
|
|
396
|
+
|
|
313
397
|
type PlayRequest = {
|
|
314
398
|
score: TrackScore;
|
|
315
399
|
beat: number | undefined;
|
|
@@ -325,8 +409,8 @@ type PlayRequest = {
|
|
|
325
409
|
*
|
|
326
410
|
* Timing: the engine is paced by the same monotonic clock as the transport.
|
|
327
411
|
* Frame `n` of the stream belongs to wall time `start + n / rate`, and the
|
|
328
|
-
* engine keeps a lead of audio queued ahead of now:
|
|
329
|
-
*
|
|
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
|
|
330
414
|
* plays the queue at its own rate, so what you hear trails the transport by
|
|
331
415
|
* the player's fixed output latency and never accumulates drift from
|
|
332
416
|
* re-renders. Renders run in a worker; an edit re-renders off-thread and
|
|
@@ -342,6 +426,8 @@ export class AudioEngine {
|
|
|
342
426
|
private leadFrames: number;
|
|
343
427
|
private readonly defaultLeadFrames: number;
|
|
344
428
|
private readonly tickMs: number;
|
|
429
|
+
/** The lead play mode and auditions pin (see `setLeadMs`). */
|
|
430
|
+
public readonly playLeadMs: number;
|
|
345
431
|
private readonly now: () => number;
|
|
346
432
|
private readonly useTimer: boolean;
|
|
347
433
|
private readonly spawnPlayer: (command: readonly string[]) => PlayerProcess;
|
|
@@ -392,17 +478,37 @@ export class AudioEngine {
|
|
|
392
478
|
options.info ?? detectAudioBackend({ sampleRate: this.sampleRate });
|
|
393
479
|
this.lock = new PlaybackLock(options.lockPath);
|
|
394
480
|
this.leadFrames = Math.round(
|
|
395
|
-
((options.leadMs ??
|
|
481
|
+
((options.leadMs ?? DEFAULT_LEAD_MS) * this.sampleRate) / 1000,
|
|
396
482
|
);
|
|
397
483
|
this.defaultLeadFrames = this.leadFrames;
|
|
398
484
|
this.fadeFrames = Math.max(
|
|
399
485
|
1,
|
|
400
486
|
Math.round(SWAP_FADE_SECONDS * this.sampleRate),
|
|
401
487
|
);
|
|
402
|
-
|
|
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);
|
|
403
495
|
this.now = options.now ?? (() => performance.now());
|
|
404
496
|
this.useTimer = options.timer ?? true;
|
|
405
|
-
|
|
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);
|
|
406
512
|
this.respawnMs = options.respawnMs ?? 250;
|
|
407
513
|
this.onStatus = options.onStatus;
|
|
408
514
|
this.renderer = new LoopRenderer({
|
|
@@ -443,6 +549,17 @@ export class AudioEngine {
|
|
|
443
549
|
: Math.max(1, Math.round((ms * this.sampleRate) / 1000));
|
|
444
550
|
}
|
|
445
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
|
+
|
|
446
563
|
/** Lead in milliseconds: how far ahead of now a new live voice sounds. */
|
|
447
564
|
public get leadMs(): number {
|
|
448
565
|
return (this.lead * 1000) / this.sampleRate;
|
|
@@ -531,14 +648,18 @@ export class AudioEngine {
|
|
|
531
648
|
existing.frames = note.frames;
|
|
532
649
|
existing.fadeLeft = undefined;
|
|
533
650
|
existing.fadeFrames = this.releaseFrames(note);
|
|
534
|
-
|
|
651
|
+
existing.ringOut = note.ringOut === true;
|
|
652
|
+
} else {
|
|
535
653
|
this.voices.set(id, {
|
|
536
654
|
pcm: note.pcm,
|
|
537
655
|
frames: note.frames,
|
|
538
656
|
position: 0,
|
|
539
657
|
fadeLeft: undefined,
|
|
540
658
|
fadeFrames: this.releaseFrames(note),
|
|
659
|
+
...(note.ringOut ? { ringOut: true } : {}),
|
|
541
660
|
});
|
|
661
|
+
if (note.ringOut) this.capRingOut();
|
|
662
|
+
}
|
|
542
663
|
// Write what is due now so the voice joins the very next chunk.
|
|
543
664
|
const first = this.written;
|
|
544
665
|
this.pump();
|
|
@@ -553,11 +674,39 @@ export class AudioEngine {
|
|
|
553
674
|
);
|
|
554
675
|
}
|
|
555
676
|
|
|
556
|
-
/**
|
|
557
|
-
|
|
677
|
+
/**
|
|
678
|
+
* Past `MAX_RING_OUT_VOICES` ringing voices, fade the oldest still at full
|
|
679
|
+
* level over `RING_OUT_FADE_SECONDS` (Map order is start order).
|
|
680
|
+
*/
|
|
681
|
+
private capRingOut(): void {
|
|
682
|
+
let ringing = 0;
|
|
683
|
+
for (const voice of this.voices.values())
|
|
684
|
+
if (voice.ringOut && voice.fadeLeft === undefined) ringing += 1;
|
|
685
|
+
for (const voice of this.voices.values()) {
|
|
686
|
+
if (ringing <= MAX_RING_OUT_VOICES) break;
|
|
687
|
+
if (!voice.ringOut || voice.fadeLeft !== undefined) continue;
|
|
688
|
+
this.fadeRingOut(voice);
|
|
689
|
+
ringing -= 1;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private fadeRingOut(voice: LiveVoice): void {
|
|
694
|
+
voice.fadeFrames = Math.max(
|
|
695
|
+
1,
|
|
696
|
+
Math.round(this.sampleRate * RING_OUT_FADE_SECONDS),
|
|
697
|
+
);
|
|
698
|
+
voice.fadeLeft = voice.fadeFrames;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Fade a live voice out over ~10 ms (sustain lifted, mode left). A
|
|
703
|
+
* ring-out voice keeps ringing; `noteOff(id, true)` silences it too.
|
|
704
|
+
*/
|
|
705
|
+
public noteOff(id: number, force = false): void {
|
|
558
706
|
const voice = this.voices.get(id);
|
|
559
|
-
if (voice
|
|
560
|
-
|
|
707
|
+
if (!voice || voice.fadeLeft !== undefined) return;
|
|
708
|
+
if (!voice.ringOut) voice.fadeLeft = voice.fadeFrames;
|
|
709
|
+
else if (force) this.fadeRingOut(voice);
|
|
561
710
|
}
|
|
562
711
|
|
|
563
712
|
/** Frames of live voice `id` already mixed, or undefined once finished. */
|
|
@@ -578,14 +727,14 @@ export class AudioEngine {
|
|
|
578
727
|
|
|
579
728
|
/** Frames kept queued ahead of the clock right now. */
|
|
580
729
|
public get lead(): number {
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
+
);
|
|
589
738
|
// The pump never queues more than a second, so the lead stays under it.
|
|
590
739
|
return Math.min(
|
|
591
740
|
Math.max(this.leadFrames, adaptive),
|
|
@@ -678,7 +827,18 @@ export class AudioEngine {
|
|
|
678
827
|
);
|
|
679
828
|
}
|
|
680
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
|
+
|
|
681
839
|
private queuedFrames(): number {
|
|
840
|
+
const clock = this.child?.clock;
|
|
841
|
+
if (clock) return clock.queuedFrames();
|
|
682
842
|
const elapsed = Math.floor(
|
|
683
843
|
((this.now() - this.startMs) * this.sampleRate) / 1000,
|
|
684
844
|
);
|
|
@@ -740,10 +900,21 @@ export class AudioEngine {
|
|
|
740
900
|
const child = this.child;
|
|
741
901
|
const loop = this.loop;
|
|
742
902
|
if (!child || (!loop && !this.monitoring)) return;
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
+
}
|
|
747
918
|
if (remaining <= 0) return;
|
|
748
919
|
// Never queue more than one second at once, e.g. after a stalled loop.
|
|
749
920
|
if (remaining > this.sampleRate) {
|
|
@@ -823,6 +994,8 @@ export class AudioEngine {
|
|
|
823
994
|
|
|
824
995
|
/** Wall time (monotonic ms) stream frame `frame` sounds at. */
|
|
825
996
|
private frameMs(frame: number): number {
|
|
997
|
+
const clock = this.child?.clock;
|
|
998
|
+
if (clock) return clock.frameMs(frame);
|
|
826
999
|
return this.startMs + (frame * 1000) / this.sampleRate;
|
|
827
1000
|
}
|
|
828
1001
|
|
package/src/audio/fit.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
sampleAt,
|
|
32
32
|
} from "./dsp/stft.ts";
|
|
33
33
|
import { hermite4 } from "./dsp/interp.ts";
|
|
34
|
+
import { pitchShiftJob } from "./dsp/shift.ts";
|
|
34
35
|
import type { FitMap } from "./warp.ts";
|
|
35
36
|
|
|
36
37
|
export type FitAlgorithm = "repitch" | "beats" | "tones";
|
|
@@ -52,7 +53,12 @@ const onsetCache = new Map<string, readonly number[]>();
|
|
|
52
53
|
/** Background jobs by cache key, with the window they fit (one per window). */
|
|
53
54
|
const pending = new Map<
|
|
54
55
|
string,
|
|
55
|
-
{
|
|
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
|
+
}
|
|
56
62
|
>();
|
|
57
63
|
let live = false;
|
|
58
64
|
/** True when the current live render left a voice silent while fitting. */
|
|
@@ -146,6 +152,76 @@ export function fittedBuffer(
|
|
|
146
152
|
return out;
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Live renders pitch-shift buffers up to this long synchronously; longer
|
|
157
|
+
* ones run between blocks while the voice plays a repitch. A phase-vocoder
|
|
158
|
+
* shift with formant keeping costs about 30 ms per second of 22.05 kHz
|
|
159
|
+
* audio, so only very short sounds fit the 10 ms note-on budget.
|
|
160
|
+
*/
|
|
161
|
+
export const LIVE_SYNC_SHIFT_SECONDS = 0.1;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* `buffer` pitch-shifted by `semitones` (formant semitones, 0 keeps),
|
|
165
|
+
* cached under `key` with the fits (0.6.1 `shift`, `formant`). Returns
|
|
166
|
+
* undefined only in a live render while a long shift is still running.
|
|
167
|
+
*/
|
|
168
|
+
export function shiftedBuffer(
|
|
169
|
+
key: string,
|
|
170
|
+
buffer: Float32Array,
|
|
171
|
+
sampleRate: number,
|
|
172
|
+
semitones: number,
|
|
173
|
+
formant: number | undefined,
|
|
174
|
+
): Float32Array | undefined {
|
|
175
|
+
const hit = cache.get(key);
|
|
176
|
+
if (hit) {
|
|
177
|
+
cache.delete(key);
|
|
178
|
+
cache.set(key, hit);
|
|
179
|
+
return hit;
|
|
180
|
+
}
|
|
181
|
+
const work = () =>
|
|
182
|
+
pitchShiftJob(buffer, sampleRate, semitones, {
|
|
183
|
+
...(formant === undefined ? {} : { formant }),
|
|
184
|
+
});
|
|
185
|
+
if (live && buffer.length > LIVE_SYNC_SHIFT_SECONDS * sampleRate) {
|
|
186
|
+
missed = true;
|
|
187
|
+
if (!pending.has(key)) {
|
|
188
|
+
pending.set(key, { job: work(), window: key });
|
|
189
|
+
schedule();
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
pending.delete(key);
|
|
194
|
+
const out = drain(work());
|
|
195
|
+
remember(key, out);
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
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
|
+
|
|
149
225
|
function drain(job: Generator<void, Float32Array>): Float32Array {
|
|
150
226
|
for (;;) {
|
|
151
227
|
const step = job.next();
|
|
@@ -159,13 +235,14 @@ function schedule(): void {
|
|
|
159
235
|
timer = undefined;
|
|
160
236
|
const next = pending.entries().next();
|
|
161
237
|
if (next.done) return;
|
|
162
|
-
const [key, { job }] = next.value;
|
|
238
|
+
const [key, { job, done }] = next.value;
|
|
163
239
|
const until = performance.now() + 8;
|
|
164
240
|
for (;;) {
|
|
165
241
|
const step = job.next();
|
|
166
242
|
if (step.done) {
|
|
167
243
|
pending.delete(key);
|
|
168
|
-
|
|
244
|
+
if (done) done(step.value);
|
|
245
|
+
else remember(key, step.value);
|
|
169
246
|
for (const listener of listeners) listener();
|
|
170
247
|
break;
|
|
171
248
|
}
|