@hraness/dawg 0.6.1 → 0.8.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.
Files changed (277) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/DAWG.md +454 -140
  3. package/README.md +30 -26
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +271 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +184 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/ids.ts +52 -0
  11. package/core/instruments.ts +19 -0
  12. package/core/keys.ts +3 -3
  13. package/core/loop.ts +8 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/range.ts +581 -0
  17. package/core/resonators.ts +16 -2
  18. package/core/routing.ts +165 -0
  19. package/core/score.ts +955 -14
  20. package/core/sdk/eval-child.ts +7 -2
  21. package/core/sdk/eval.ts +35 -6
  22. package/core/sdk/print.ts +287 -5
  23. package/core/sdk/sync-lyrics.ts +49 -0
  24. package/core/sdk/v1.ts +2132 -46
  25. package/core/sections.ts +480 -35
  26. package/core/sing.ts +815 -0
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/guides/agent.md +29 -0
  50. package/guides/arrange.md +31 -0
  51. package/guides/audio.md +31 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -7
  54. package/guides/chords.md +15 -15
  55. package/guides/effects.md +17 -16
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +15 -7
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/panes.md +32 -0
  65. package/guides/performance.md +15 -13
  66. package/guides/play.md +21 -13
  67. package/guides/project.md +26 -8
  68. package/guides/providers.md +20 -14
  69. package/guides/resample.md +15 -9
  70. package/guides/rhythm.md +18 -13
  71. package/guides/sessions.md +17 -7
  72. package/guides/show-me.md +31 -0
  73. package/guides/sound.md +25 -9
  74. package/guides/sounds.md +15 -13
  75. package/guides/styles.md +31 -0
  76. package/guides/tape.md +32 -0
  77. package/guides/tempo.md +15 -10
  78. package/guides/tracks.md +15 -10
  79. package/guides/tuning.md +32 -0
  80. package/guides/voice.md +31 -0
  81. package/guides/web-search.md +18 -8
  82. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  83. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  84. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  85. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  86. package/native/prebuilt/manifest.json +21 -0
  87. package/package.json +6 -2
  88. package/src/agent/agent.ts +130 -18
  89. package/src/agent/calibration-tools.ts +53 -0
  90. package/src/agent/clip-tools.ts +453 -0
  91. package/src/agent/command-agent.ts +378 -0
  92. package/src/agent/drum-tools.ts +2 -2
  93. package/src/agent/expression-tools.ts +1 -1
  94. package/src/agent/gateway.ts +246 -60
  95. package/src/agent/models.ts +53 -12
  96. package/src/agent/ops.ts +27 -1
  97. package/src/agent/pack-tools.ts +1 -1
  98. package/src/agent/planner.ts +25 -0
  99. package/src/agent/portable-schema.ts +80 -0
  100. package/src/agent/preview-tool.ts +4 -1
  101. package/src/agent/provider.ts +22 -8
  102. package/src/agent/range-tools.ts +216 -0
  103. package/src/agent/rhythm-tools.ts +1 -1
  104. package/src/agent/section-tools.ts +1 -1
  105. package/src/agent/show-me.ts +497 -0
  106. package/src/agent/steer.ts +15 -0
  107. package/src/agent/style-tools.ts +217 -0
  108. package/src/agent/tool-error.ts +12 -0
  109. package/src/agent/tools.ts +110 -23
  110. package/src/agent/usage.ts +2 -2
  111. package/src/agent/voice-tools.ts +925 -0
  112. package/src/agent/xcb-agent.ts +13 -10
  113. package/src/argv.ts +38 -0
  114. package/src/audio/analysis.ts +253 -0
  115. package/src/audio/arrange.ts +49 -5
  116. package/src/audio/audio-command.ts +218 -0
  117. package/src/audio/autotune-engine.ts +101 -0
  118. package/src/audio/autotune.ts +640 -0
  119. package/src/audio/clips.ts +240 -0
  120. package/src/audio/devices.ts +264 -0
  121. package/src/audio/doctor.ts +157 -0
  122. package/src/audio/dsp/bandbank.ts +138 -0
  123. package/src/audio/dsp/envelope.ts +10 -0
  124. package/src/audio/dsp/follow.ts +120 -0
  125. package/src/audio/dsp/formant.ts +427 -0
  126. package/src/audio/dsp/glottal.ts +243 -0
  127. package/src/audio/dsp/interp.ts +7 -2
  128. package/src/audio/dsp/lpc.ts +50 -0
  129. package/src/audio/dsp/periodicity.ts +59 -0
  130. package/src/audio/dsp/pitch.ts +995 -0
  131. package/src/audio/dsp/psola.ts +199 -0
  132. package/src/audio/effects/chain.ts +3 -1
  133. package/src/audio/effects/common.ts +43 -0
  134. package/src/audio/effects/convolution.ts +7 -4
  135. package/src/audio/effects/filter.ts +48 -69
  136. package/src/audio/effects/formant.ts +263 -0
  137. package/src/audio/engine.ts +360 -40
  138. package/src/audio/fit.ts +35 -3
  139. package/src/audio/instrument-check.ts +59 -43
  140. package/src/audio/instruments.ts +4 -0
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +8 -1
  143. package/src/audio/keys/engine.ts +13 -1
  144. package/src/audio/keys/piano.ts +22 -2
  145. package/src/audio/kits.ts +135 -6
  146. package/src/audio/live.ts +114 -20
  147. package/src/audio/native.ts +615 -0
  148. package/src/audio/preview.ts +30 -2
  149. package/src/audio/render-worker.ts +2 -0
  150. package/src/audio/renderer.ts +2 -0
  151. package/src/audio/resample.ts +2 -1
  152. package/src/audio/sampler.ts +85 -4
  153. package/src/audio/samples.ts +20 -2
  154. package/src/audio/sing/analysis.ts +193 -0
  155. package/src/audio/sing/engine.ts +949 -0
  156. package/src/audio/strings/bow.ts +48 -5
  157. package/src/audio/strings/engine.ts +8 -2
  158. package/src/audio/synth/oscillators.ts +31 -21
  159. package/src/audio/synth/voice.ts +34 -1
  160. package/src/audio/vocoder/bank.ts +314 -0
  161. package/src/audio/vocoder/carrier.ts +165 -0
  162. package/src/audio/vocoder/control.ts +68 -0
  163. package/src/audio/vocoder/detect.ts +50 -0
  164. package/src/audio/vocoder/index.ts +304 -0
  165. package/src/audio/vocoder/talkbox.ts +143 -0
  166. package/src/audio/wav.ts +500 -77
  167. package/src/audio/winds/engine.ts +5 -1
  168. package/src/audio/winds/trim.ts +28 -4
  169. package/src/audio/winds/trims1.ts +297 -0
  170. package/src/audio/winds/voice.ts +15 -2
  171. package/src/auth/cli.ts +38 -36
  172. package/src/auth/credentials.ts +30 -1
  173. package/src/auth/login.ts +15 -9
  174. package/src/auth/tui.ts +19 -10
  175. package/src/commands/arrange.ts +82 -32
  176. package/src/commands/autotune.ts +421 -0
  177. package/src/commands/calibration.ts +74 -0
  178. package/src/commands/clips.ts +887 -0
  179. package/src/commands/drums.ts +3 -2
  180. package/src/commands/edit.ts +11 -4
  181. package/src/commands/expression.ts +1 -1
  182. package/src/commands/formant.ts +221 -0
  183. package/src/commands/fx.ts +101 -32
  184. package/src/commands/grammar.ts +560 -0
  185. package/src/commands/help.ts +774 -366
  186. package/src/commands/history.ts +139 -10
  187. package/src/commands/keys.ts +25 -8
  188. package/src/commands/modal.ts +1 -1
  189. package/src/commands/music.ts +1 -1
  190. package/src/commands/nearest.ts +53 -0
  191. package/src/commands/pack.ts +9 -2
  192. package/src/commands/param-range.ts +56 -0
  193. package/src/commands/parses.ts +135 -0
  194. package/src/commands/progression.ts +170 -0
  195. package/src/commands/range.ts +763 -0
  196. package/src/commands/resample.ts +13 -10
  197. package/src/commands/rhythm.ts +3 -0
  198. package/src/commands/rig.ts +3 -24
  199. package/src/commands/sample.ts +11 -1
  200. package/src/commands/sing.ts +474 -0
  201. package/src/commands/strum.ts +13 -1
  202. package/src/commands/style.ts +415 -0
  203. package/src/commands/time.ts +6 -3
  204. package/src/commands/tuning.ts +3 -3
  205. package/src/commands/vocal-pitch.ts +617 -0
  206. package/src/commands/vocal.ts +147 -0
  207. package/src/commands/vocoder.ts +627 -0
  208. package/src/commands/wind.ts +2 -2
  209. package/src/fs/durable.ts +50 -0
  210. package/src/identity/actor.ts +127 -0
  211. package/src/lang/glossary.ts +511 -0
  212. package/src/launch-args.ts +267 -0
  213. package/src/main.ts +2508 -308
  214. package/src/media/cli.ts +20 -3
  215. package/src/media/import.ts +3 -1
  216. package/src/project/check.ts +21 -2
  217. package/src/project/clip-pins.ts +72 -0
  218. package/src/project/init.ts +23 -8
  219. package/src/project/sync.ts +418 -86
  220. package/src/render.ts +20 -1
  221. package/src/session/client.ts +107 -3
  222. package/src/session/clipboard.ts +79 -0
  223. package/src/session/daemon.ts +199 -5
  224. package/src/session/live-host.ts +232 -0
  225. package/src/session/meta.ts +14 -0
  226. package/src/session/origin.ts +174 -0
  227. package/src/session/port.ts +109 -14
  228. package/src/session/presence.ts +34 -4
  229. package/src/session/protocol.ts +365 -7
  230. package/src/session/rebase.ts +63 -12
  231. package/src/session/receipt.ts +265 -0
  232. package/src/session/shared-live.ts +131 -0
  233. package/src/session/store.ts +126 -45
  234. package/src/tui/arrange-menu.ts +225 -23
  235. package/src/tui/audition.ts +1 -1
  236. package/src/tui/euclid.ts +113 -27
  237. package/src/tui/fader.ts +282 -42
  238. package/src/tui/granular-menu.ts +2 -4
  239. package/src/tui/knob-fields.ts +107 -0
  240. package/src/tui/knob-map.ts +198 -0
  241. package/src/tui/menu-clips.ts +297 -0
  242. package/src/tui/menu-time.ts +20 -13
  243. package/src/tui/menu-voice.ts +405 -0
  244. package/src/tui/menu.ts +987 -179
  245. package/src/tui/modal-menu.ts +6 -6
  246. package/src/tui/performance-menu.ts +5 -2
  247. package/src/tui/play-chords.ts +4 -2
  248. package/src/tui/play-mode.ts +15 -1
  249. package/src/tui/play-session.ts +243 -28
  250. package/src/tui/sing-menu.ts +278 -0
  251. package/src/tui/style-menu.ts +104 -0
  252. package/src/tui/tape-mode.ts +580 -0
  253. package/src/tui/tape-view.ts +263 -0
  254. package/src/tui/vocoder-menu.ts +244 -0
  255. package/src/tui/wind-menu.ts +3 -3
  256. package/src/version.ts +8 -0
  257. package/src/web/fetch.ts +115 -29
  258. package/tui/activity.ts +180 -9
  259. package/tui/app.ts +491 -84
  260. package/tui/clip-row.ts +132 -0
  261. package/tui/delight.ts +193 -0
  262. package/tui/drawer.ts +233 -27
  263. package/tui/frame-gate.ts +76 -0
  264. package/tui/grammar.ts +143 -65
  265. package/tui/guide.ts +50 -5
  266. package/tui/highway.ts +309 -30
  267. package/tui/hints.ts +197 -0
  268. package/tui/hits.ts +7 -1
  269. package/tui/input.ts +60 -9
  270. package/tui/keys.ts +1 -1
  271. package/tui/knobs.ts +268 -0
  272. package/tui/play-strip.ts +80 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +151 -18
  275. package/tui/tape.ts +439 -0
  276. package/tui/text.ts +35 -2
  277. package/tui/theme.ts +46 -1
@@ -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
+ }
@@ -39,6 +39,7 @@ import { barAt, barStartTick, songTimeSlice } from "../../core/tempo.ts";
39
39
  import { interpolateAutomation } from "./effects/common.ts";
40
40
  import { loopedSection, scoreBeatAt } from "./arrange.ts";
41
41
  import { RENDER_CHANNELS } from "./wav.ts";
42
+ import { resolveTrackRef, trackRefs } from "../../core/routing.ts";
42
43
 
43
44
  /** Longest preview loop, in bars. */
44
45
  export const MAX_PREVIEW_BARS = 4;
@@ -327,10 +328,18 @@ export function previewScore(
327
328
  const end = barStartTick(score, startBar + bars);
328
329
  const context = options.context === true;
329
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);
330
334
  const tracks = score.tracks
331
- .filter((candidate) => context || candidate.id === trackId)
335
+ .filter(
336
+ (candidate) =>
337
+ context || candidate.id === trackId || feeders.has(candidate.id),
338
+ )
332
339
  .map((candidate) => {
333
340
  const sliced = sliceTrack(candidate, start, end);
341
+ if (feeders.has(candidate.id))
342
+ return { ...sliced, muted: true, solo: false };
334
343
  if (candidate.id !== trackId) return sliced;
335
344
  // The focused track always sounds, even when muted or not soloed.
336
345
  return {
@@ -344,7 +353,7 @@ export function previewScore(
344
353
  const notes: NoteInput[] = score.notes
345
354
  .filter(
346
355
  (note) =>
347
- (context || note.trackId === trackId) &&
356
+ (context || note.trackId === trackId || feeders.has(note.trackId)) &&
348
357
  !(replaced && note.trackId === trackId) &&
349
358
  note.startTick >= start &&
350
359
  note.startTick < end,
@@ -384,6 +393,25 @@ export function previewScore(
384
393
  };
385
394
  }
386
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
+
387
415
  /**
388
416
  * The master a preview plays: the song's, with its loudness target swapped
389
417
  * for a fixed limiter drive (see PreviewOptions.masterGainDb).
@@ -50,6 +50,8 @@ self.onmessage = async (event: MessageEvent<RenderRequest>) => {
50
50
  const audio = renderArranged(renderer, parsed, {
51
51
  sampleRate,
52
52
  loop: true,
53
+ // The live loop sounds a vocal track's guide notes (0.7 clips).
54
+ guide: true,
53
55
  samples,
54
56
  });
55
57
  const reply: RenderReply = {
@@ -87,6 +87,8 @@ export class LoopRenderer {
87
87
  const audio = renderArranged(this.inline, score, {
88
88
  sampleRate: this.sampleRate,
89
89
  loop: true,
90
+ // The live loop sounds a vocal track's guide notes (0.7 clips).
91
+ guide: true,
90
92
  samples,
91
93
  });
92
94
  return {