@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
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native audio sink (native/sink, Rust + cpal) loaded through bun:ffi.
|
|
3
|
+
*
|
|
4
|
+
* The engine still renders and mixes every sample in TypeScript; the sink
|
|
5
|
+
* only plays them from a lock-free ring drained by the device's own audio
|
|
6
|
+
* callback (and captures input into another ring). That removes the stdin
|
|
7
|
+
* pipe and the external player's buffer from the output path.
|
|
8
|
+
*
|
|
9
|
+
* The library ships prebuilt per platform under `native/prebuilt/<target>/`
|
|
10
|
+
* with a `manifest.json` of sha256 sums; it is verified before dlopen and
|
|
11
|
+
* never compiled on install. Anything missing or wrong falls back to the
|
|
12
|
+
* stdin players, and the reason is reported (`dawg doctor`, `/auth`).
|
|
13
|
+
*/
|
|
14
|
+
import { dlopen as ffiOpen, FFIType as T, ptr as ffiPtr } from "bun:ffi";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { encodeWav } from "./wav.ts";
|
|
20
|
+
|
|
21
|
+
/** The C ABI shape this loader speaks (native/sink `ABI_VERSION`). */
|
|
22
|
+
export const SINK_ABI_VERSION = 1;
|
|
23
|
+
const STAT_COUNT = 12;
|
|
24
|
+
|
|
25
|
+
/** Prebuilt targets: `<platform>-<arch>`. */
|
|
26
|
+
export const NATIVE_TARGETS = Object.freeze([
|
|
27
|
+
"darwin-arm64",
|
|
28
|
+
"darwin-x64",
|
|
29
|
+
"linux-x64",
|
|
30
|
+
"linux-arm64",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export function nativeTarget(
|
|
34
|
+
platform: string = process.platform,
|
|
35
|
+
arch: string = process.arch,
|
|
36
|
+
): string | undefined {
|
|
37
|
+
const target = `${platform}-${arch}`;
|
|
38
|
+
return NATIVE_TARGETS.includes(target) ? target : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function libraryFileName(platform: string = process.platform): string {
|
|
42
|
+
return platform === "darwin" ? "libdawg_sink.dylib" : "libdawg_sink.so";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Where the package keeps prebuilt libraries and their manifest. */
|
|
46
|
+
export const PREBUILT_DIR = join(
|
|
47
|
+
import.meta.dir,
|
|
48
|
+
"..",
|
|
49
|
+
"..",
|
|
50
|
+
"native",
|
|
51
|
+
"prebuilt",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export type NativeManifest = Readonly<{
|
|
55
|
+
abi: number;
|
|
56
|
+
libraries: Readonly<Record<string, { file: string; sha256: string }>>;
|
|
57
|
+
}>;
|
|
58
|
+
|
|
59
|
+
/** Device stats, in the order `dawg_sink_stats` writes them. */
|
|
60
|
+
export type SinkStats = Readonly<{
|
|
61
|
+
/** Frames queued in the ring (output) or captured and unread (input). */
|
|
62
|
+
queued: number;
|
|
63
|
+
/** Output: stream frames elapsed, starved ones included; input: captured. */
|
|
64
|
+
frames: number;
|
|
65
|
+
/** Output: underruns; input: frames dropped on a full ring. */
|
|
66
|
+
xruns: number;
|
|
67
|
+
/** Device latency (output: callback to speaker; input: mic to callback). */
|
|
68
|
+
latencyNs: number;
|
|
69
|
+
/** Host clock (`clockNs`) at the most recent callback. */
|
|
70
|
+
callbackNs: number;
|
|
71
|
+
/** Host time the first frame of that callback plays (or was captured). */
|
|
72
|
+
edgeNs: number;
|
|
73
|
+
/** `frames` at that edge. */
|
|
74
|
+
framesAtEdge: number;
|
|
75
|
+
bufferFrames: number;
|
|
76
|
+
rate: number;
|
|
77
|
+
channels: number;
|
|
78
|
+
deviceRate: number;
|
|
79
|
+
failed: boolean;
|
|
80
|
+
}>;
|
|
81
|
+
|
|
82
|
+
export type SinkDevice = Readonly<{
|
|
83
|
+
name: string;
|
|
84
|
+
default: boolean;
|
|
85
|
+
channels: number;
|
|
86
|
+
rate: number;
|
|
87
|
+
}>;
|
|
88
|
+
|
|
89
|
+
export type OpenRequest = Readonly<{
|
|
90
|
+
/** Device name; undefined or "default" is the system default, "null" headless. */
|
|
91
|
+
device?: string;
|
|
92
|
+
rate: number;
|
|
93
|
+
channels: number;
|
|
94
|
+
/** Device buffer frames (0 keeps the device default). */
|
|
95
|
+
bufferFrames?: number;
|
|
96
|
+
ringFrames: number;
|
|
97
|
+
}>;
|
|
98
|
+
|
|
99
|
+
export interface SinkEndpoint {
|
|
100
|
+
/** Queue interleaved samples (output); returns samples accepted. */
|
|
101
|
+
write(samples: Float32Array): number;
|
|
102
|
+
/** Read captured interleaved samples (input); returns samples read. */
|
|
103
|
+
read(out: Float32Array): number;
|
|
104
|
+
stats(): SinkStats;
|
|
105
|
+
clear(): number;
|
|
106
|
+
close(): void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The sink as TypeScript sees it, over real ffi or a test fake. */
|
|
110
|
+
export interface SinkLibrary {
|
|
111
|
+
readonly abiVersion: number;
|
|
112
|
+
clockNs(): number;
|
|
113
|
+
lastError(): string;
|
|
114
|
+
devices(input: boolean): SinkDevice[];
|
|
115
|
+
openOutput(request: OpenRequest): SinkEndpoint | undefined;
|
|
116
|
+
openInput(request: OpenRequest): SinkEndpoint | undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The raw symbols, as `bun:ffi` dlopen returns them (or a fake). */
|
|
120
|
+
export type SinkSymbols = Readonly<{
|
|
121
|
+
dawg_sink_abi_version: () => number;
|
|
122
|
+
dawg_sink_clock_ns: () => number | bigint;
|
|
123
|
+
dawg_sink_last_error: (out: unknown, len: number) => number | bigint;
|
|
124
|
+
dawg_sink_devices: (
|
|
125
|
+
input: number,
|
|
126
|
+
out: unknown,
|
|
127
|
+
len: number,
|
|
128
|
+
) => number | bigint;
|
|
129
|
+
dawg_sink_open: (...args: unknown[]) => unknown;
|
|
130
|
+
dawg_capture_open: (...args: unknown[]) => unknown;
|
|
131
|
+
dawg_sink_write: (
|
|
132
|
+
sink: unknown,
|
|
133
|
+
samples: unknown,
|
|
134
|
+
count: number,
|
|
135
|
+
) => number | bigint;
|
|
136
|
+
dawg_capture_read: (
|
|
137
|
+
capture: unknown,
|
|
138
|
+
out: unknown,
|
|
139
|
+
count: number,
|
|
140
|
+
) => number | bigint;
|
|
141
|
+
dawg_sink_stats: (endpoint: unknown, out: unknown) => number;
|
|
142
|
+
dawg_sink_clear: (endpoint: unknown) => number | bigint;
|
|
143
|
+
dawg_sink_close: (endpoint: unknown) => void;
|
|
144
|
+
}>;
|
|
145
|
+
|
|
146
|
+
export type Dlopen = (path: string) => SinkSymbols;
|
|
147
|
+
|
|
148
|
+
/** `bun:ffi` signatures for every exported symbol. */
|
|
149
|
+
const OPEN_ARGS = [T.ptr, T.u32, T.u32, T.u32, T.u32];
|
|
150
|
+
const ffiDlopen: Dlopen = (path) =>
|
|
151
|
+
ffiOpen(path, {
|
|
152
|
+
dawg_sink_abi_version: { args: [], returns: T.u32 },
|
|
153
|
+
dawg_sink_clock_ns: { args: [], returns: T.u64 },
|
|
154
|
+
dawg_sink_last_error: { args: [T.ptr, T.u64], returns: T.u64 },
|
|
155
|
+
dawg_sink_devices: { args: [T.i32, T.ptr, T.u64], returns: T.u64 },
|
|
156
|
+
dawg_sink_open: { args: OPEN_ARGS, returns: T.ptr },
|
|
157
|
+
dawg_capture_open: { args: OPEN_ARGS, returns: T.ptr },
|
|
158
|
+
dawg_sink_write: { args: [T.ptr, T.ptr, T.u64], returns: T.u64 },
|
|
159
|
+
dawg_capture_read: { args: [T.ptr, T.ptr, T.u64], returns: T.u64 },
|
|
160
|
+
dawg_sink_stats: { args: [T.ptr, T.ptr], returns: T.i32 },
|
|
161
|
+
dawg_sink_clear: { args: [T.ptr], returns: T.u64 },
|
|
162
|
+
dawg_sink_close: { args: [T.ptr], returns: T.void },
|
|
163
|
+
}).symbols as unknown as SinkSymbols;
|
|
164
|
+
|
|
165
|
+
/** Wrap raw symbols in the typed `SinkLibrary` (pointers via `ptr`). */
|
|
166
|
+
export function bindSink(
|
|
167
|
+
symbols: SinkSymbols,
|
|
168
|
+
ptr: (view: ArrayBufferView) => unknown,
|
|
169
|
+
): SinkLibrary {
|
|
170
|
+
const n = (value: number | bigint): number => Number(value);
|
|
171
|
+
const text = (
|
|
172
|
+
fill: (out: unknown, len: number) => number | bigint,
|
|
173
|
+
): string => {
|
|
174
|
+
let size = 256;
|
|
175
|
+
for (;;) {
|
|
176
|
+
const buffer = new Uint8Array(size);
|
|
177
|
+
const full = n(fill(ptr(buffer), size));
|
|
178
|
+
if (full < size)
|
|
179
|
+
return new TextDecoder().decode(buffer.subarray(0, full));
|
|
180
|
+
size = full + 1;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const endpoint = (handle: unknown): SinkEndpoint => {
|
|
184
|
+
const stats = new BigUint64Array(STAT_COUNT);
|
|
185
|
+
let open = true;
|
|
186
|
+
return {
|
|
187
|
+
write: (samples) =>
|
|
188
|
+
open && samples.length > 0
|
|
189
|
+
? n(symbols.dawg_sink_write(handle, ptr(samples), samples.length))
|
|
190
|
+
: 0,
|
|
191
|
+
read: (out) =>
|
|
192
|
+
open && out.length > 0
|
|
193
|
+
? n(symbols.dawg_capture_read(handle, ptr(out), out.length))
|
|
194
|
+
: 0,
|
|
195
|
+
stats: () => {
|
|
196
|
+
if (open) symbols.dawg_sink_stats(handle, ptr(stats));
|
|
197
|
+
const v = Array.from(stats, Number);
|
|
198
|
+
return {
|
|
199
|
+
queued: v[0]!,
|
|
200
|
+
frames: v[1]!,
|
|
201
|
+
xruns: v[2]!,
|
|
202
|
+
latencyNs: v[3]!,
|
|
203
|
+
callbackNs: v[4]!,
|
|
204
|
+
edgeNs: v[5]!,
|
|
205
|
+
framesAtEdge: v[6]!,
|
|
206
|
+
bufferFrames: v[7]!,
|
|
207
|
+
rate: v[8]!,
|
|
208
|
+
channels: v[9]!,
|
|
209
|
+
deviceRate: v[10]!,
|
|
210
|
+
failed: v[11] !== 0 || !open,
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
clear: () => (open ? n(symbols.dawg_sink_clear(handle)) : 0),
|
|
214
|
+
close: () => {
|
|
215
|
+
if (!open) return;
|
|
216
|
+
open = false;
|
|
217
|
+
symbols.dawg_sink_close(handle);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
const opener =
|
|
222
|
+
(fn: SinkSymbols["dawg_sink_open"]) =>
|
|
223
|
+
(request: OpenRequest): SinkEndpoint | undefined => {
|
|
224
|
+
const name = Buffer.from(`${request.device ?? "default"}\0`);
|
|
225
|
+
const handle = fn(
|
|
226
|
+
ptr(name),
|
|
227
|
+
request.rate,
|
|
228
|
+
request.channels,
|
|
229
|
+
request.bufferFrames ?? 0,
|
|
230
|
+
request.ringFrames,
|
|
231
|
+
);
|
|
232
|
+
return handle ? endpoint(handle) : undefined;
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
abiVersion: symbols.dawg_sink_abi_version(),
|
|
236
|
+
clockNs: () => n(symbols.dawg_sink_clock_ns()),
|
|
237
|
+
lastError: () => text((out, len) => symbols.dawg_sink_last_error(out, len)),
|
|
238
|
+
devices: (input) =>
|
|
239
|
+
text((out, len) => symbols.dawg_sink_devices(input ? 1 : 0, out, len))
|
|
240
|
+
.split("\n")
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
.map((line) => {
|
|
243
|
+
const [name = "", isDefault, channels, rate] = line.split("\t");
|
|
244
|
+
return {
|
|
245
|
+
name,
|
|
246
|
+
default: isDefault === "1",
|
|
247
|
+
channels: Number(channels),
|
|
248
|
+
rate: Number(rate),
|
|
249
|
+
};
|
|
250
|
+
}),
|
|
251
|
+
openOutput: opener(symbols.dawg_sink_open),
|
|
252
|
+
openInput: opener(symbols.dawg_capture_open),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export type NativeProbe =
|
|
257
|
+
| Readonly<{ ok: true; library: SinkLibrary; path: string; detail: string }>
|
|
258
|
+
| Readonly<{ ok: false; reason: string }>;
|
|
259
|
+
|
|
260
|
+
export type ProbeOptions = Readonly<{
|
|
261
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
262
|
+
platform?: string;
|
|
263
|
+
arch?: string;
|
|
264
|
+
/** Directory holding `manifest.json` and `<target>/<lib>`. */
|
|
265
|
+
dir?: string;
|
|
266
|
+
dlopen?: Dlopen;
|
|
267
|
+
ptr?: (view: ArrayBufferView) => unknown;
|
|
268
|
+
}>;
|
|
269
|
+
|
|
270
|
+
function sha256File(path: string): string {
|
|
271
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Find, verify and load the sink. `DAWG_AUDIO_NATIVE=0` turns it off;
|
|
276
|
+
* `DAWG_SINK_LIB=<path>` loads a local build unverified (development).
|
|
277
|
+
*/
|
|
278
|
+
export function probeNativeSink(options: ProbeOptions = {}): NativeProbe {
|
|
279
|
+
const env = options.env ?? process.env;
|
|
280
|
+
const platform = options.platform ?? process.platform;
|
|
281
|
+
if (env.DAWG_AUDIO_NATIVE === "0")
|
|
282
|
+
return { ok: false, reason: "DAWG_AUDIO_NATIVE=0" };
|
|
283
|
+
const override = env.DAWG_SINK_LIB?.trim();
|
|
284
|
+
let path: string;
|
|
285
|
+
let detail: string;
|
|
286
|
+
if (override) {
|
|
287
|
+
if (!existsSync(override))
|
|
288
|
+
return { ok: false, reason: `DAWG_SINK_LIB ${override} not found` };
|
|
289
|
+
path = override;
|
|
290
|
+
detail = `${override} (DAWG_SINK_LIB, unverified)`;
|
|
291
|
+
} else {
|
|
292
|
+
const target = nativeTarget(platform, options.arch ?? process.arch);
|
|
293
|
+
if (!target)
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
reason: `no prebuilt native sink for ${platform}-${options.arch ?? process.arch}`,
|
|
297
|
+
};
|
|
298
|
+
const dir = options.dir ?? PREBUILT_DIR;
|
|
299
|
+
let manifest: NativeManifest;
|
|
300
|
+
try {
|
|
301
|
+
manifest = JSON.parse(readFileSync(join(dir, "manifest.json"), "utf8"));
|
|
302
|
+
} catch {
|
|
303
|
+
return { ok: false, reason: "native sink not installed (no manifest)" };
|
|
304
|
+
}
|
|
305
|
+
const entry = manifest.libraries?.[target];
|
|
306
|
+
if (!entry || manifest.abi !== SINK_ABI_VERSION)
|
|
307
|
+
return { ok: false, reason: `no native sink for ${target} in manifest` };
|
|
308
|
+
path = join(dir, entry.file);
|
|
309
|
+
if (!existsSync(path))
|
|
310
|
+
return { ok: false, reason: `native sink ${entry.file} missing` };
|
|
311
|
+
const actual = sha256File(path);
|
|
312
|
+
if (actual !== entry.sha256)
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
reason: `native sink ${entry.file} sha256 mismatch (refusing to load)`,
|
|
316
|
+
};
|
|
317
|
+
detail = `${target} sha256 ${actual.slice(0, 12)} verified`;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const dlopen = options.dlopen ?? ffiDlopen;
|
|
321
|
+
const ptr = options.ptr ?? (ffiPtr as (view: ArrayBufferView) => unknown);
|
|
322
|
+
const library = bindSink(dlopen(path), ptr);
|
|
323
|
+
if (library.abiVersion !== SINK_ABI_VERSION)
|
|
324
|
+
return {
|
|
325
|
+
ok: false,
|
|
326
|
+
reason: `native sink ABI ${library.abiVersion}, expected ${SINK_ABI_VERSION}`,
|
|
327
|
+
};
|
|
328
|
+
return { ok: true, library, path, detail };
|
|
329
|
+
} catch (error) {
|
|
330
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
reason: `native sink failed to load: ${message.slice(0, 120)}`,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Stream timing for one buffer, on the engine's monotonic ms clock: the
|
|
340
|
+
* host time of the latest device callback, when its first frame is heard,
|
|
341
|
+
* and the device latencies. Networked sessions align on these.
|
|
342
|
+
*/
|
|
343
|
+
export type AudioTiming = Readonly<{
|
|
344
|
+
/** Monotonic ms of the latest device callback. */
|
|
345
|
+
callbackMs: number;
|
|
346
|
+
/** Monotonic ms the engine stream frame `frame` is heard at. */
|
|
347
|
+
heardMs: number;
|
|
348
|
+
frame: number;
|
|
349
|
+
outputLatencyMs: number;
|
|
350
|
+
inputLatencyMs?: number;
|
|
351
|
+
underruns: number;
|
|
352
|
+
queuedFrames: number;
|
|
353
|
+
bufferFrames: number;
|
|
354
|
+
deviceRate: number;
|
|
355
|
+
}>;
|
|
356
|
+
|
|
357
|
+
/** Maps the sink's host clock onto the engine's monotonic ms clock. */
|
|
358
|
+
export function clockOffsetMs(
|
|
359
|
+
library: SinkLibrary,
|
|
360
|
+
now: () => number = () => performance.now(),
|
|
361
|
+
): number {
|
|
362
|
+
const before = now();
|
|
363
|
+
const ns = library.clockNs();
|
|
364
|
+
const after = now();
|
|
365
|
+
return (before + after) / 2 - ns / 1e6;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** What the engine asks a device-clocked player (see `NativePlayer`). */
|
|
369
|
+
export type PlayerClock = Readonly<{
|
|
370
|
+
/** Frames queued ahead of the device. */
|
|
371
|
+
queuedFrames: () => number;
|
|
372
|
+
/**
|
|
373
|
+
* Stream frames the device played as silence since the last call (an
|
|
374
|
+
* underrun); the engine skips its cursor by as much to stay on time.
|
|
375
|
+
*/
|
|
376
|
+
slipFrames: () => number;
|
|
377
|
+
/** Monotonic ms engine stream frame `frame` is heard at. */
|
|
378
|
+
frameMs: (frame: number) => number;
|
|
379
|
+
timing: () => AudioTiming;
|
|
380
|
+
}>;
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* A `PlayerProcess` over the sink: s16le chunks from the engine become f32
|
|
384
|
+
* (exactly `v / 32768`, so what plays is sample-identical to the stream the
|
|
385
|
+
* stdin players get). A device failure ends it like a dead player process,
|
|
386
|
+
* so the engine's respawn path reopens the (possibly new) default device.
|
|
387
|
+
*/
|
|
388
|
+
export class NativePlayer {
|
|
389
|
+
public readonly pid = 0;
|
|
390
|
+
public readonly exited: Promise<number>;
|
|
391
|
+
public readonly clock: PlayerClock;
|
|
392
|
+
public readonly stdin: { write(chunk: Uint8Array): void; end(): void };
|
|
393
|
+
private resolveExit: (code: number) => void = () => undefined;
|
|
394
|
+
private done = false;
|
|
395
|
+
/** Engine stream frames written so far. */
|
|
396
|
+
private written = 0;
|
|
397
|
+
/** Device stream index minus engine stream frame, at the last check. */
|
|
398
|
+
private offset = 0;
|
|
399
|
+
/** Set by the first write: frames the device played before it don't count. */
|
|
400
|
+
private anchored = false;
|
|
401
|
+
private readonly offsetMs: number;
|
|
402
|
+
|
|
403
|
+
public constructor(
|
|
404
|
+
private readonly endpoint: SinkEndpoint,
|
|
405
|
+
private readonly library: SinkLibrary,
|
|
406
|
+
private readonly channels: number,
|
|
407
|
+
private readonly rate: number,
|
|
408
|
+
now: () => number = () => performance.now(),
|
|
409
|
+
) {
|
|
410
|
+
this.exited = new Promise((resolve) => (this.resolveExit = resolve));
|
|
411
|
+
this.offsetMs = clockOffsetMs(library, now);
|
|
412
|
+
this.stdin = {
|
|
413
|
+
write: (chunk) => this.write(chunk),
|
|
414
|
+
end: () => undefined,
|
|
415
|
+
};
|
|
416
|
+
this.clock = {
|
|
417
|
+
queuedFrames: () => {
|
|
418
|
+
const stats = this.endpoint.stats();
|
|
419
|
+
// A lost device ends the player; the engine respawns on the default.
|
|
420
|
+
if (stats.failed) this.kill(1);
|
|
421
|
+
return stats.queued;
|
|
422
|
+
},
|
|
423
|
+
slipFrames: () => {
|
|
424
|
+
if (!this.anchored) return 0;
|
|
425
|
+
const stats = this.endpoint.stats();
|
|
426
|
+
const offset = stats.frames + stats.queued - this.written;
|
|
427
|
+
const slip = Math.max(0, offset - this.offset);
|
|
428
|
+
this.offset = Math.max(this.offset, offset);
|
|
429
|
+
return slip;
|
|
430
|
+
},
|
|
431
|
+
frameMs: (frame) => this.frameMs(frame, this.endpoint.stats()),
|
|
432
|
+
timing: () => {
|
|
433
|
+
const stats = this.endpoint.stats();
|
|
434
|
+
const frame = Math.max(0, stats.framesAtEdge - this.offset);
|
|
435
|
+
return {
|
|
436
|
+
callbackMs: stats.callbackNs / 1e6 + this.offsetMs,
|
|
437
|
+
heardMs: this.frameMs(frame, stats),
|
|
438
|
+
frame,
|
|
439
|
+
outputLatencyMs: stats.latencyNs / 1e6,
|
|
440
|
+
underruns: stats.xruns,
|
|
441
|
+
queuedFrames: stats.queued,
|
|
442
|
+
bufferFrames: stats.bufferFrames,
|
|
443
|
+
deviceRate: stats.deviceRate,
|
|
444
|
+
};
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private frameMs(frame: number, stats: SinkStats): number {
|
|
450
|
+
return (
|
|
451
|
+
stats.edgeNs / 1e6 +
|
|
452
|
+
this.offsetMs +
|
|
453
|
+
((frame + this.offset - stats.framesAtEdge) * 1000) / this.rate
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private write(chunk: Uint8Array): void {
|
|
458
|
+
if (this.done) return;
|
|
459
|
+
if (this.endpoint.stats().failed) {
|
|
460
|
+
this.kill(1);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (!this.anchored) {
|
|
464
|
+
const stats = this.endpoint.stats();
|
|
465
|
+
this.offset = stats.frames + stats.queued;
|
|
466
|
+
this.anchored = true;
|
|
467
|
+
}
|
|
468
|
+
const pcm = new Int16Array(
|
|
469
|
+
chunk.buffer,
|
|
470
|
+
chunk.byteOffset,
|
|
471
|
+
chunk.byteLength >> 1,
|
|
472
|
+
);
|
|
473
|
+
const samples = new Float32Array(pcm.length);
|
|
474
|
+
for (let index = 0; index < pcm.length; index += 1)
|
|
475
|
+
samples[index] = pcm[index]! / 32768;
|
|
476
|
+
const accepted = this.endpoint.write(samples);
|
|
477
|
+
// A full ring drops the tail; count it as written so timing holds.
|
|
478
|
+
this.written += pcm.length / this.channels;
|
|
479
|
+
if (accepted < samples.length)
|
|
480
|
+
this.offset -= (samples.length - accepted) / this.channels;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
public kill(code = 0): void {
|
|
484
|
+
if (this.done) return;
|
|
485
|
+
this.done = true;
|
|
486
|
+
this.endpoint.close();
|
|
487
|
+
this.resolveExit(code);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Ring sizing: two seconds holds the engine's one-second cap plus lead. */
|
|
492
|
+
export function openNativePlayer(
|
|
493
|
+
library: SinkLibrary,
|
|
494
|
+
options: Readonly<{
|
|
495
|
+
rate: number;
|
|
496
|
+
channels: number;
|
|
497
|
+
device?: string;
|
|
498
|
+
bufferFrames?: number;
|
|
499
|
+
/** The engine's monotonic ms clock (timing is reported on it). */
|
|
500
|
+
now?: () => number;
|
|
501
|
+
}>,
|
|
502
|
+
): NativePlayer {
|
|
503
|
+
const endpoint = library.openOutput({
|
|
504
|
+
...(options.device ? { device: options.device } : {}),
|
|
505
|
+
rate: options.rate,
|
|
506
|
+
channels: options.channels,
|
|
507
|
+
bufferFrames: options.bufferFrames ?? 0,
|
|
508
|
+
ringFrames: options.rate * 2,
|
|
509
|
+
});
|
|
510
|
+
if (!endpoint)
|
|
511
|
+
throw new Error(library.lastError() || "native sink open failed");
|
|
512
|
+
return new NativePlayer(
|
|
513
|
+
endpoint,
|
|
514
|
+
library,
|
|
515
|
+
options.channels,
|
|
516
|
+
options.rate,
|
|
517
|
+
options.now,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Input capture for recorded takes: the device fills a ring, `drain` pulls
|
|
523
|
+
* what has arrived. The sink keeps no score state; `writeTake` stores the
|
|
524
|
+
* audio as a sha256-named WAV and the caller commits one `setClips` op.
|
|
525
|
+
*/
|
|
526
|
+
export class NativeCapture {
|
|
527
|
+
private readonly chunks: Float32Array[] = [];
|
|
528
|
+
private readonly scratch: Float32Array;
|
|
529
|
+
|
|
530
|
+
public constructor(
|
|
531
|
+
private readonly endpoint: SinkEndpoint,
|
|
532
|
+
public readonly rate: number,
|
|
533
|
+
public readonly channels: number,
|
|
534
|
+
) {
|
|
535
|
+
this.scratch = new Float32Array(rate * channels);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
public static open(
|
|
539
|
+
library: SinkLibrary,
|
|
540
|
+
options: Readonly<{
|
|
541
|
+
rate: number;
|
|
542
|
+
channels: number;
|
|
543
|
+
device?: string;
|
|
544
|
+
bufferFrames?: number;
|
|
545
|
+
}>,
|
|
546
|
+
): NativeCapture {
|
|
547
|
+
const endpoint = library.openInput({
|
|
548
|
+
...(options.device ? { device: options.device } : {}),
|
|
549
|
+
rate: options.rate,
|
|
550
|
+
channels: options.channels,
|
|
551
|
+
bufferFrames: options.bufferFrames ?? 0,
|
|
552
|
+
ringFrames: options.rate * 4,
|
|
553
|
+
});
|
|
554
|
+
if (!endpoint)
|
|
555
|
+
throw new Error(library.lastError() || "native capture open failed");
|
|
556
|
+
return new NativeCapture(endpoint, options.rate, options.channels);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Pull everything captured so far; returns samples added. */
|
|
560
|
+
public drain(): number {
|
|
561
|
+
let total = 0;
|
|
562
|
+
for (;;) {
|
|
563
|
+
const got = this.endpoint.read(this.scratch);
|
|
564
|
+
if (got === 0) return total;
|
|
565
|
+
this.chunks.push(this.scratch.slice(0, got));
|
|
566
|
+
total += got;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
public stats(): SinkStats {
|
|
571
|
+
return this.endpoint.stats();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Stop and return every captured sample, interleaved. */
|
|
575
|
+
public close(): Float32Array {
|
|
576
|
+
this.drain();
|
|
577
|
+
this.endpoint.close();
|
|
578
|
+
const out = new Float32Array(
|
|
579
|
+
this.chunks.reduce((sum, c) => sum + c.length, 0),
|
|
580
|
+
);
|
|
581
|
+
let offset = 0;
|
|
582
|
+
for (const chunk of this.chunks) {
|
|
583
|
+
out.set(chunk, offset);
|
|
584
|
+
offset += chunk.length;
|
|
585
|
+
}
|
|
586
|
+
return out;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Store a take as `tracks/<slug>/takes/<sha256>.wav` (16-bit PCM). Returns
|
|
592
|
+
* the project-relative `src` and its `sha256` for the caller's `setClips`.
|
|
593
|
+
*/
|
|
594
|
+
export async function writeTake(
|
|
595
|
+
projectRoot: string,
|
|
596
|
+
trackSlug: string,
|
|
597
|
+
samples: Float32Array,
|
|
598
|
+
rate: number,
|
|
599
|
+
channels: number,
|
|
600
|
+
): Promise<{ src: string; sha256: string }> {
|
|
601
|
+
const pcm = new Int16Array(samples.length);
|
|
602
|
+
for (let index = 0; index < samples.length; index += 1)
|
|
603
|
+
pcm[index] = Math.max(
|
|
604
|
+
-32768,
|
|
605
|
+
Math.min(32767, Math.round(samples[index]! * 32768)),
|
|
606
|
+
);
|
|
607
|
+
const bytes = encodeWav(pcm, rate, channels);
|
|
608
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
609
|
+
const src = `tracks/${trackSlug}/takes/${sha256}.wav`;
|
|
610
|
+
await mkdir(join(projectRoot, "tracks", trackSlug, "takes"), {
|
|
611
|
+
recursive: true,
|
|
612
|
+
});
|
|
613
|
+
await writeFile(join(projectRoot, src), bytes);
|
|
614
|
+
return { src, sha256 };
|
|
615
|
+
}
|
package/src/audio/preview.ts
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* wavetables so position changes are audible.
|
|
17
17
|
*/
|
|
18
18
|
import { isDrumInstrument } from "../../core/drums.ts";
|
|
19
|
+
import { slicePedals } from "../../core/expression.ts";
|
|
20
|
+
import {
|
|
21
|
+
isStringTrack,
|
|
22
|
+
resolveString,
|
|
23
|
+
stringPresetOf,
|
|
24
|
+
} from "../../core/strings.ts";
|
|
19
25
|
import type { SongMaster } from "../../core/master.ts";
|
|
20
26
|
import { MODES, parseKey } from "../../core/chords.ts";
|
|
21
27
|
import { rhythmVoicePitch } from "../../core/rhythm.ts";
|
|
@@ -33,6 +39,7 @@ import { barAt, barStartTick, songTimeSlice } from "../../core/tempo.ts";
|
|
|
33
39
|
import { interpolateAutomation } from "./effects/common.ts";
|
|
34
40
|
import { loopedSection, scoreBeatAt } from "./arrange.ts";
|
|
35
41
|
import { RENDER_CHANNELS } from "./wav.ts";
|
|
42
|
+
import { resolveTrackRef, trackRefs } from "../../core/routing.ts";
|
|
36
43
|
|
|
37
44
|
/** Longest preview loop, in bars. */
|
|
38
45
|
export const MAX_PREVIEW_BARS = 4;
|
|
@@ -131,6 +138,14 @@ export function phraseRole(track: Track): PhraseRole {
|
|
|
131
138
|
if (isWavetableInstrument(track.instrument)) return "drone";
|
|
132
139
|
// Grain clouds are textures: held notes let them evolve.
|
|
133
140
|
if (track.instrument === "granular") return "drone";
|
|
141
|
+
if (isStringTrack(track) && resolveString(track.string).exciter === "bow") {
|
|
142
|
+
// Bowed: low strings walk a line, solo strings sing a legato line,
|
|
143
|
+
// sections and pizz/trem hold chords.
|
|
144
|
+
const preset = stringPresetOf(track.string);
|
|
145
|
+
if (/^(contrabass|contrabasses|cello|cellos)$/.test(preset)) return "riff";
|
|
146
|
+
if (/^(violin|viola|fiddle|erhu|kamancheh)$/.test(preset)) return "lead";
|
|
147
|
+
return "chord";
|
|
148
|
+
}
|
|
134
149
|
const words = `${track.instrument} ${track.name} ${track.id}`.toLowerCase();
|
|
135
150
|
if (/\b(bass|sub|808)/.test(words)) return "riff";
|
|
136
151
|
if (/\b(lead|arp|pluck|saw|square|pulse|z_|mono|melody|hook)/.test(words))
|
|
@@ -287,7 +302,7 @@ function sliceTrack(track: Track, start: number, end: number): Track {
|
|
|
287
302
|
lanes[name] = sliceLane(points, start, end);
|
|
288
303
|
sliced.fxAutomation = lanes;
|
|
289
304
|
}
|
|
290
|
-
return sliced as Track;
|
|
305
|
+
return slicePedals(sliced as Track, [{ from: start, to: end, offset: 0 }]);
|
|
291
306
|
}
|
|
292
307
|
|
|
293
308
|
/**
|
|
@@ -313,10 +328,18 @@ export function previewScore(
|
|
|
313
328
|
const end = barStartTick(score, startBar + bars);
|
|
314
329
|
const context = options.context === true;
|
|
315
330
|
const anySolo = score.tracks.some((candidate) => candidate.solo === true);
|
|
331
|
+
// A solo preview keeps the tracks the focused one reads audio from (a
|
|
332
|
+
// vocoder's src), muted: they feed it without being heard.
|
|
333
|
+
const feeders = context ? new Set<string>() : audioFeeders(score, trackId);
|
|
316
334
|
const tracks = score.tracks
|
|
317
|
-
.filter(
|
|
335
|
+
.filter(
|
|
336
|
+
(candidate) =>
|
|
337
|
+
context || candidate.id === trackId || feeders.has(candidate.id),
|
|
338
|
+
)
|
|
318
339
|
.map((candidate) => {
|
|
319
340
|
const sliced = sliceTrack(candidate, start, end);
|
|
341
|
+
if (feeders.has(candidate.id))
|
|
342
|
+
return { ...sliced, muted: true, solo: false };
|
|
320
343
|
if (candidate.id !== trackId) return sliced;
|
|
321
344
|
// The focused track always sounds, even when muted or not soloed.
|
|
322
345
|
return {
|
|
@@ -330,7 +353,7 @@ export function previewScore(
|
|
|
330
353
|
const notes: NoteInput[] = score.notes
|
|
331
354
|
.filter(
|
|
332
355
|
(note) =>
|
|
333
|
-
(context || note.trackId === trackId) &&
|
|
356
|
+
(context || note.trackId === trackId || feeders.has(note.trackId)) &&
|
|
334
357
|
!(replaced && note.trackId === trackId) &&
|
|
335
358
|
note.startTick >= start &&
|
|
336
359
|
note.startTick < end,
|
|
@@ -370,6 +393,25 @@ export function previewScore(
|
|
|
370
393
|
};
|
|
371
394
|
}
|
|
372
395
|
|
|
396
|
+
/** Every track `trackId` reads audio from, transitively (vocoder src). */
|
|
397
|
+
function audioFeeders(score: TrackScore, trackId: string): Set<string> {
|
|
398
|
+
const found = new Set<string>();
|
|
399
|
+
const queue = [trackId];
|
|
400
|
+
while (queue.length > 0) {
|
|
401
|
+
const id = queue.pop();
|
|
402
|
+
const track = score.tracks.find((candidate) => candidate.id === id);
|
|
403
|
+
if (!track) continue;
|
|
404
|
+
for (const ref of trackRefs(track)) {
|
|
405
|
+
if (ref.kind !== "audio") continue;
|
|
406
|
+
const source = resolveTrackRef(score, ref.trackId);
|
|
407
|
+
if (!source || source.id === trackId || found.has(source.id)) continue;
|
|
408
|
+
found.add(source.id);
|
|
409
|
+
queue.push(source.id);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return found;
|
|
413
|
+
}
|
|
414
|
+
|
|
373
415
|
/**
|
|
374
416
|
* The master a preview plays: the song's, with its loudness target swapped
|
|
375
417
|
* for a fixed limiter drive (see PreviewOptions.masterGainDb).
|