@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.
Files changed (275) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +655 -141
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +759 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -99
  8. package/core/expression.ts +158 -6
  9. package/core/fx.ts +354 -32
  10. package/core/granular.ts +91 -0
  11. package/core/instruments.ts +180 -0
  12. package/core/keys.ts +573 -4
  13. package/core/loop.ts +5 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/resonators.ts +306 -4
  17. package/core/routing.ts +165 -0
  18. package/core/score.ts +1181 -16
  19. package/core/sdk/eval-child.ts +7 -2
  20. package/core/sdk/eval.ts +35 -6
  21. package/core/sdk/print.ts +354 -6
  22. package/core/sdk/sync-lyrics.ts +49 -0
  23. package/core/sdk/v1.ts +3160 -69
  24. package/core/sections.ts +298 -24
  25. package/core/sing.ts +815 -0
  26. package/core/strings.ts +237 -2
  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/core/winds.ts +652 -0
  50. package/guides/agent.md +29 -0
  51. package/guides/arrange.md +30 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -6
  54. package/guides/chords.md +16 -14
  55. package/guides/effects.md +17 -15
  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 +12 -6
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/performance.md +15 -10
  65. package/guides/play.md +21 -13
  66. package/guides/project.md +25 -8
  67. package/guides/providers.md +20 -14
  68. package/guides/resample.md +32 -0
  69. package/guides/rhythm.md +17 -13
  70. package/guides/sessions.md +17 -7
  71. package/guides/show-me.md +31 -0
  72. package/guides/sound.md +25 -9
  73. package/guides/sounds.md +15 -12
  74. package/guides/styles.md +31 -0
  75. package/guides/tempo.md +15 -10
  76. package/guides/tracks.md +15 -10
  77. package/guides/tuning.md +32 -0
  78. package/guides/voice.md +31 -0
  79. package/guides/web-search.md +18 -8
  80. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  81. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  82. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  83. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  84. package/native/prebuilt/manifest.json +21 -0
  85. package/package.json +5 -2
  86. package/src/agent/agent.ts +131 -13
  87. package/src/agent/calibration-tools.ts +53 -0
  88. package/src/agent/chord-tools.ts +153 -1
  89. package/src/agent/clip-tools.ts +453 -0
  90. package/src/agent/command-agent.ts +369 -0
  91. package/src/agent/drum-tools.ts +2 -2
  92. package/src/agent/expression-tools.ts +91 -0
  93. package/src/agent/gateway.ts +246 -60
  94. package/src/agent/models.ts +53 -12
  95. package/src/agent/ops.ts +15 -1
  96. package/src/agent/pack-tools.ts +1 -1
  97. package/src/agent/planner.ts +13 -0
  98. package/src/agent/portable-schema.ts +80 -0
  99. package/src/agent/preview-tool.ts +5 -1
  100. package/src/agent/provider.ts +22 -8
  101. package/src/agent/resample-tool.ts +131 -0
  102. package/src/agent/rhythm-tools.ts +1 -1
  103. package/src/agent/section-tools.ts +1 -1
  104. package/src/agent/show-me.ts +497 -0
  105. package/src/agent/steer.ts +15 -0
  106. package/src/agent/style-tools.ts +217 -0
  107. package/src/agent/tool-error.ts +12 -0
  108. package/src/agent/tools.ts +307 -37
  109. package/src/agent/usage.ts +2 -2
  110. package/src/agent/voice-tools.ts +925 -0
  111. package/src/agent/xcb-agent.ts +16 -6
  112. package/src/argv.ts +38 -0
  113. package/src/audio/analysis.ts +253 -0
  114. package/src/audio/arrange.ts +58 -3
  115. package/src/audio/autotune-engine.ts +101 -0
  116. package/src/audio/autotune.ts +640 -0
  117. package/src/audio/clips.ts +240 -0
  118. package/src/audio/doctor.ts +86 -0
  119. package/src/audio/dsp/bandbank.ts +138 -0
  120. package/src/audio/dsp/envelope.ts +171 -0
  121. package/src/audio/dsp/follow.ts +120 -0
  122. package/src/audio/dsp/formant.ts +427 -0
  123. package/src/audio/dsp/glottal.ts +243 -0
  124. package/src/audio/dsp/interp.ts +7 -2
  125. package/src/audio/dsp/lpc.ts +50 -0
  126. package/src/audio/dsp/periodicity.ts +59 -0
  127. package/src/audio/dsp/pitch.ts +995 -0
  128. package/src/audio/dsp/psola.ts +199 -0
  129. package/src/audio/dsp/shift.ts +256 -0
  130. package/src/audio/effects/chain.ts +21 -4
  131. package/src/audio/effects/common.ts +57 -0
  132. package/src/audio/effects/convolution.ts +113 -5
  133. package/src/audio/effects/filter.ts +48 -69
  134. package/src/audio/effects/formant.ts +263 -0
  135. package/src/audio/effects/gaze.ts +354 -0
  136. package/src/audio/engine.ts +206 -33
  137. package/src/audio/fit.ts +80 -3
  138. package/src/audio/granular.ts +352 -21
  139. package/src/audio/instrument-check.ts +67 -47
  140. package/src/audio/instruments.ts +36 -3
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +427 -0
  143. package/src/audio/keys/engine.ts +156 -20
  144. package/src/audio/keys/organ.ts +1335 -0
  145. package/src/audio/keys/piano.ts +72 -8
  146. package/src/audio/keys/sympathetic.ts +127 -0
  147. package/src/audio/kits.ts +135 -6
  148. package/src/audio/live-worker.ts +3 -1
  149. package/src/audio/live.ts +247 -26
  150. package/src/audio/native.ts +615 -0
  151. package/src/audio/preview.ts +45 -3
  152. package/src/audio/render-worker.ts +2 -0
  153. package/src/audio/renderer.ts +2 -0
  154. package/src/audio/resample.ts +286 -0
  155. package/src/audio/resonators.ts +10 -2
  156. package/src/audio/sampler.ts +252 -24
  157. package/src/audio/samples.ts +46 -3
  158. package/src/audio/sing/analysis.ts +193 -0
  159. package/src/audio/sing/engine.ts +949 -0
  160. package/src/audio/strings/body.ts +29 -16
  161. package/src/audio/strings/bow.ts +699 -0
  162. package/src/audio/strings/engine.ts +274 -22
  163. package/src/audio/strings/measure.test-helpers.ts +2 -0
  164. package/src/audio/synth/oscillators.ts +31 -21
  165. package/src/audio/synth/voice.ts +34 -1
  166. package/src/audio/vocoder/bank.ts +314 -0
  167. package/src/audio/vocoder/carrier.ts +165 -0
  168. package/src/audio/vocoder/control.ts +68 -0
  169. package/src/audio/vocoder/detect.ts +50 -0
  170. package/src/audio/vocoder/index.ts +304 -0
  171. package/src/audio/vocoder/talkbox.ts +143 -0
  172. package/src/audio/wav.ts +608 -85
  173. package/src/audio/winds/engine.ts +306 -0
  174. package/src/audio/winds/filters.ts +153 -0
  175. package/src/audio/winds/pitch.ts +104 -0
  176. package/src/audio/winds/trim.ts +104 -0
  177. package/src/audio/winds/trims.ts +917 -0
  178. package/src/audio/winds/trims1.ts +297 -0
  179. package/src/audio/winds/voice.ts +492 -0
  180. package/src/auth/cli.ts +38 -36
  181. package/src/auth/credentials.ts +30 -1
  182. package/src/auth/login.ts +15 -9
  183. package/src/auth/tui.ts +19 -10
  184. package/src/commands/arrange.ts +44 -29
  185. package/src/commands/autotune.ts +421 -0
  186. package/src/commands/calibration.ts +74 -0
  187. package/src/commands/clips.ts +887 -0
  188. package/src/commands/drums.ts +3 -2
  189. package/src/commands/edit.ts +11 -4
  190. package/src/commands/expression.ts +104 -23
  191. package/src/commands/formant.ts +221 -0
  192. package/src/commands/fx.ts +105 -33
  193. package/src/commands/grammar.ts +558 -0
  194. package/src/commands/granular.ts +7 -2
  195. package/src/commands/help.ts +652 -339
  196. package/src/commands/history.ts +18 -0
  197. package/src/commands/keys.ts +293 -17
  198. package/src/commands/modal.ts +67 -4
  199. package/src/commands/music.ts +1 -1
  200. package/src/commands/nearest.ts +53 -0
  201. package/src/commands/pack.ts +9 -2
  202. package/src/commands/param-range.ts +56 -0
  203. package/src/commands/parses.ts +130 -0
  204. package/src/commands/progression.ts +170 -0
  205. package/src/commands/resample.ts +281 -0
  206. package/src/commands/rhythm.ts +3 -0
  207. package/src/commands/rig.ts +8 -25
  208. package/src/commands/sample.ts +49 -2
  209. package/src/commands/shift.ts +119 -0
  210. package/src/commands/sing.ts +478 -0
  211. package/src/commands/string.ts +28 -2
  212. package/src/commands/strum.ts +485 -0
  213. package/src/commands/style.ts +415 -0
  214. package/src/commands/time.ts +6 -3
  215. package/src/commands/tuning.ts +3 -3
  216. package/src/commands/vocal-pitch.ts +616 -0
  217. package/src/commands/vocal.ts +147 -0
  218. package/src/commands/vocoder.ts +627 -0
  219. package/src/commands/wind.ts +244 -0
  220. package/src/fs/durable.ts +50 -0
  221. package/src/lang/glossary.ts +493 -0
  222. package/src/launch-args.ts +163 -0
  223. package/src/main.ts +1521 -255
  224. package/src/media/cli.ts +20 -3
  225. package/src/media/import.ts +3 -1
  226. package/src/project/check.ts +26 -3
  227. package/src/project/clip-pins.ts +72 -0
  228. package/src/project/init.ts +23 -8
  229. package/src/project/sync.ts +418 -86
  230. package/src/render.ts +36 -1
  231. package/src/session/daemon.ts +3 -0
  232. package/src/session/meta.ts +14 -0
  233. package/src/session/origin.ts +154 -0
  234. package/src/session/port.ts +22 -4
  235. package/src/session/presence.ts +50 -7
  236. package/src/session/protocol.ts +5 -1
  237. package/src/session/rebase.ts +18 -5
  238. package/src/session/receipt.ts +258 -0
  239. package/src/session/store.ts +65 -43
  240. package/src/tui/arrange-menu.ts +65 -31
  241. package/src/tui/audition.ts +1 -1
  242. package/src/tui/euclid.ts +18 -13
  243. package/src/tui/fader.ts +228 -41
  244. package/src/tui/granular-menu.ts +2 -4
  245. package/src/tui/menu-clips.ts +297 -0
  246. package/src/tui/menu-time.ts +20 -13
  247. package/src/tui/menu-voice.ts +405 -0
  248. package/src/tui/menu.ts +1377 -193
  249. package/src/tui/modal-menu.ts +38 -11
  250. package/src/tui/performance-menu.ts +46 -1
  251. package/src/tui/play-chords.ts +108 -5
  252. package/src/tui/play-mode.ts +16 -1
  253. package/src/tui/play-session.ts +81 -16
  254. package/src/tui/sing-menu.ts +278 -0
  255. package/src/tui/style-menu.ts +104 -0
  256. package/src/tui/vocoder-menu.ts +244 -0
  257. package/src/tui/wind-menu.ts +144 -0
  258. package/src/version.ts +8 -0
  259. package/src/web/fetch.ts +115 -29
  260. package/tui/activity.ts +180 -9
  261. package/tui/app.ts +274 -40
  262. package/tui/clip-row.ts +132 -0
  263. package/tui/delight.ts +144 -0
  264. package/tui/drawer.ts +70 -22
  265. package/tui/frame-gate.ts +76 -0
  266. package/tui/grammar.ts +112 -63
  267. package/tui/guide.ts +42 -4
  268. package/tui/highway.ts +269 -25
  269. package/tui/hints.ts +192 -0
  270. package/tui/input.ts +60 -9
  271. package/tui/keys.ts +1 -1
  272. package/tui/play-strip.ts +68 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +144 -14
  275. package/tui/theme.ts +27 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Audio clips (0.7): files placed on a track's timeline, summed into the
3
+ * track's dry buffer before its effect chain (sources.md section 7). The
4
+ * pure placement and section cuts live in core/clips.ts; this module reads
5
+ * the decoded audio, places it through the tempo map, resamples it to the
6
+ * render rate (4-point Hermite), applies the take's clock-drift stretch,
7
+ * reverse, gain and equal-power fades, and multiplies by the track's
8
+ * volume the way every voice does. A clip whose audio did not load renders
9
+ * silence (the loader reported it as a warning).
10
+ */
11
+ import {
12
+ CLIP_VOICE_PREFIX,
13
+ DEFAULT_CLIP_FADE,
14
+ clipSongTick,
15
+ resolveClipLengths,
16
+ } from "../../core/clips.ts";
17
+ import type { AudioClip, Take, Track, TrackScore } from "../../core/score.ts";
18
+ import { autotuneClip } from "./autotune.ts";
19
+ import { sampleKey, type DecodedSample, type SampleBank } from "./samples.ts";
20
+ import type { SampleWarp } from "./warp.ts";
21
+
22
+ /** What clip rendering reads of the render context. */
23
+ export type ClipContext = Readonly<{
24
+ sampleRate: number;
25
+ samples: number;
26
+ samplesPerTick: number;
27
+ warp?: SampleWarp;
28
+ }>;
29
+
30
+ /** True when a track has clips that sound. */
31
+ export function hasClips(track: Track | undefined): boolean {
32
+ return track?.clips?.some((clip) => !clip.mute) ?? false;
33
+ }
34
+
35
+ /** The decoded audio of a clip, if it loaded. */
36
+ export function clipAudio(
37
+ bank: SampleBank | undefined,
38
+ trackId: string,
39
+ clip: AudioClip,
40
+ ): DecodedSample | undefined {
41
+ if (!bank) return undefined;
42
+ const exact = bank.voices.get(
43
+ sampleKey(trackId, `${CLIP_VOICE_PREFIX}${clip.id}`),
44
+ );
45
+ if (exact) return exact;
46
+ // A section or form pass of a clip is `<id>~<n>`: it plays the audio
47
+ // the bank loaded for `<id>` from the unsliced score.
48
+ const pass = clip.id.lastIndexOf("~");
49
+ return pass > 0
50
+ ? bank.voices.get(
51
+ sampleKey(trackId, `${CLIP_VOICE_PREFIX}${clip.id.slice(0, pass)}`),
52
+ )
53
+ : undefined;
54
+ }
55
+
56
+ /**
57
+ * `score` with each loaded clip's missing `dur` resolved from its file
58
+ * (`resolveClipLengths`), so section, form and window cuts see its real
59
+ * end. A score without clips, or without a bank, comes back as it is.
60
+ */
61
+ export function withClipLengths(
62
+ score: TrackScore,
63
+ bank: SampleBank | undefined,
64
+ ): TrackScore {
65
+ if (!bank || !score.tracks.some((track) => track.clips)) return score;
66
+ return resolveClipLengths(score, (trackId, clip) => {
67
+ const audio = clipAudio(bank, trackId, clip);
68
+ return audio && audio.frames > 0
69
+ ? audio.frames / audio.sampleRate
70
+ : undefined;
71
+ });
72
+ }
73
+
74
+ /** Fractional render sample where a score tick sounds. */
75
+ function sampleAt(context: ClipContext, tick: number): number {
76
+ return context.warp
77
+ ? context.warp.sample(tick)
78
+ : tick * context.samplesPerTick;
79
+ }
80
+
81
+ /** The take a clip plays from, if any. */
82
+ function takeOf(track: Track, clip: AudioClip): Take | undefined {
83
+ if (clip.take === undefined) return undefined;
84
+ return track.takes?.find((take) => take.name === clip.take);
85
+ }
86
+
87
+ /** Catmull-Rom (4-point Hermite) read at a fractional frame, 0 outside. */
88
+ function readHermite(data: Float32Array, position: number): number {
89
+ const base = Math.floor(position);
90
+ const t = position - base;
91
+ const at = (index: number) =>
92
+ index >= 0 && index < data.length ? data[index]! : 0;
93
+ const y0 = at(base - 1);
94
+ const y1 = at(base);
95
+ const y2 = at(base + 1);
96
+ const y3 = at(base + 2);
97
+ const c1 = 0.5 * (y2 - y0);
98
+ const c2 = y0 - 2.5 * y1 + 2 * y2 - 0.5 * y3;
99
+ const c3 = 0.5 * (y3 - y0) + 1.5 * (y1 - y2);
100
+ return ((c3 * t + c2) * t + c1) * t + y1;
101
+ }
102
+
103
+ /** Equal-power fade gain for `x` in 0..1 (sin of a quarter turn). */
104
+ export function equalPowerFade(x: number): number {
105
+ if (x <= 0) return 0;
106
+ if (x >= 1) return 1;
107
+ return Math.sin((x * Math.PI) / 2);
108
+ }
109
+
110
+ /**
111
+ * Sum `track`'s clips into `target` (the mono dry buffer). `gainAt(tick)`
112
+ * is the track volume with automation; `tickAt(sample)` maps a render
113
+ * sample back to a score tick for it. Returns the clips that did not
114
+ * load (they stay silent). With `score` and a `Track.autotune` (0.7), each
115
+ * clip plays retuned (`autotuneClip`, keyed by its offset and nudge);
116
+ * reversed clips play untuned.
117
+ */
118
+ export function renderClips(
119
+ target: Float64Array,
120
+ track: Track,
121
+ context: ClipContext,
122
+ bank: SampleBank | undefined,
123
+ gainAt: (tick: number) => number,
124
+ score?: TrackScore,
125
+ ): string[] {
126
+ const missing: string[] = [];
127
+ if (!track.clips) return missing;
128
+ const { sampleRate, samples } = context;
129
+ const tickAt = (index: number) =>
130
+ context.warp ? context.warp.tick(index) : index / context.samplesPerTick;
131
+ for (const clip of track.clips) {
132
+ if (clip.mute) continue;
133
+ const audio = clipAudio(bank, track.id, clip);
134
+ if (!audio || audio.frames === 0) {
135
+ missing.push(clip.id);
136
+ continue;
137
+ }
138
+ const take = takeOf(track, clip);
139
+ // A take recorded on a drifting clock (`ppm`) plays slightly faster or
140
+ // slower so it stays on the grid; `nudge` moves it in milliseconds.
141
+ const drift = 1 + (take?.ppm ?? 0) / 1e6;
142
+ const nudge = (take?.nudge ?? 0) / 1000;
143
+ const fileSeconds = audio.frames / audio.sampleRate;
144
+ const offset = Math.min(fileSeconds, clip.offset ?? 0);
145
+ const length = Math.max(
146
+ 0,
147
+ Math.min(
148
+ clip.dur ?? Number.POSITIVE_INFINITY,
149
+ (fileSeconds - offset) / drift,
150
+ ),
151
+ );
152
+ if (length <= 0) continue;
153
+ const start =
154
+ sampleAt(context, clipSongTick(clip, track.time)) + nudge * sampleRate;
155
+ const total = length * sampleRate;
156
+ const first = Math.max(0, Math.ceil(start));
157
+ const last = Math.min(samples, Math.ceil(start + total));
158
+ if (first >= last) continue;
159
+ const step = (audio.sampleRate / sampleRate) * drift;
160
+ const begin = offset * audio.sampleRate;
161
+ const span = length * audio.sampleRate * drift;
162
+ const fadeIn = Math.min(clip.fadeInTime ?? DEFAULT_CLIP_FADE, length / 2);
163
+ const fadeOut = Math.min(clip.fadeTime ?? DEFAULT_CLIP_FADE, length / 2);
164
+ const fadeInFrames = fadeIn * sampleRate;
165
+ const fadeOutFrames = fadeOut * sampleRate;
166
+ const gain = clip.gain ?? 1;
167
+ // 0.7 autotune: the clip's audio retuned at the song second its
168
+ // offset sounds (`start` already holds the nudge).
169
+ const tuned =
170
+ track.autotune && score && !clip.rev
171
+ ? autotuneClip(
172
+ score,
173
+ track,
174
+ {
175
+ sha256: audio.sha256,
176
+ sampleRate: audio.sampleRate,
177
+ mono: audio.mono,
178
+ },
179
+ start / sampleRate,
180
+ offset,
181
+ clip.id,
182
+ 0,
183
+ nudge,
184
+ )
185
+ : undefined;
186
+ const data = tuned ? tuned.mono : audio.mono;
187
+ const shiftFrom = tuned ? tuned.from : 0;
188
+ // Track volume (with automation) is read once per 32-sample block.
189
+ let blockGain = 0;
190
+ for (let index = first; index < last; index += 1) {
191
+ if ((index - first) % 32 === 0) blockGain = gainAt(tickAt(index));
192
+ const elapsed = index - start;
193
+ let position = elapsed * step;
194
+ if (position >= span) break;
195
+ position = clip.rev ? begin + span - 1 - position : begin + position;
196
+ let shape = gain * blockGain;
197
+ if (fadeInFrames > 0 && elapsed < fadeInFrames)
198
+ shape *= equalPowerFade(elapsed / fadeInFrames);
199
+ const remaining = total - elapsed;
200
+ if (fadeOutFrames > 0 && remaining < fadeOutFrames)
201
+ shape *= equalPowerFade(remaining / fadeOutFrames);
202
+ target[index]! += readHermite(data, position - shiftFrom) * shape;
203
+ }
204
+ }
205
+ return missing;
206
+ }
207
+
208
+ /**
209
+ * Stem-cache key part for a track's clips: each clip's sha256 (the one
210
+ * loaded, so an edited file re-renders), placement, cut, gain, fades and
211
+ * reverse, and the drift of the take it plays from.
212
+ */
213
+ export function clipsDigest(
214
+ track: Track,
215
+ bank: SampleBank | undefined,
216
+ guide = false,
217
+ ): string | undefined {
218
+ if (!track.clips || track.clips.length === 0) return undefined;
219
+ return (
220
+ (guide ? "guide|" : "") +
221
+ track.clips
222
+ .map((clip) => {
223
+ const take = takeOf(track, clip);
224
+ return [
225
+ clip.id,
226
+ clipAudio(bank, track.id, clip)?.sha256 ?? `missing:${clip.sha256}`,
227
+ clip.startTick,
228
+ clip.offset ?? "",
229
+ clip.dur ?? "",
230
+ clip.gain ?? "",
231
+ clip.fadeInTime ?? "",
232
+ clip.fadeTime ?? "",
233
+ clip.rev ? "r" : "",
234
+ clip.mute ? "m" : "",
235
+ take ? `${take.ppm ?? 0}/${take.nudge ?? 0}` : "",
236
+ ].join(",");
237
+ })
238
+ .join(";")
239
+ );
240
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `dawg doctor`: how dawg makes sound on this machine. The backend and why
3
+ * it was chosen, the native sink's verification (or why it fell back), the
4
+ * audio devices, and the play-mode lead.
5
+ */
6
+ import {
7
+ AudioEngine,
8
+ detectAudioBackend,
9
+ type AudioBackendInfo,
10
+ } from "./engine.ts";
11
+ import { PREBUILT_DIR, nativeTarget } from "./native.ts";
12
+ import { DEFAULT_SAMPLE_RATE } from "./wav.ts";
13
+
14
+ export type DoctorReport = Readonly<{
15
+ backend: AudioBackendInfo["backend"];
16
+ detail: string;
17
+ native: Readonly<{
18
+ target: string | null;
19
+ dir: string;
20
+ loaded: boolean;
21
+ reason?: string;
22
+ }>;
23
+ playLeadMs: number;
24
+ outputs: readonly string[];
25
+ inputs: readonly string[];
26
+ }>;
27
+
28
+ export function audioDoctor(info = detectAudioBackend()): DoctorReport {
29
+ const engine = new AudioEngine({ info, worker: false, timer: false });
30
+ const playLeadMs = engine.playLeadMs;
31
+ void engine.dispose();
32
+ const list = (input: boolean) =>
33
+ info.native
34
+ ?.devices(input)
35
+ .map(
36
+ (d) =>
37
+ `${d.name}${d.default ? " (default)" : ""} · ${d.channels} ch · ${d.rate} Hz`,
38
+ ) ?? [];
39
+ return {
40
+ backend: info.backend,
41
+ detail: info.detail,
42
+ native: {
43
+ target: nativeTarget() ?? null,
44
+ dir: PREBUILT_DIR,
45
+ loaded: info.backend === "native",
46
+ ...(info.nativeUnavailable ? { reason: info.nativeUnavailable } : {}),
47
+ },
48
+ playLeadMs,
49
+ outputs: list(false),
50
+ inputs: list(true),
51
+ };
52
+ }
53
+
54
+ export function formatAudioDoctor(report: DoctorReport): string[] {
55
+ const lines = [
56
+ `audio: ${report.backend} · ${report.detail}`,
57
+ report.native.loaded
58
+ ? `native sink: loaded (${report.native.target})`
59
+ : `native sink: not in use · ${report.native.reason ?? "another backend was forced"} · falls back to ffplay, sox or afplay`,
60
+ `play lead: ${report.playLeadMs} ms · render rate ${DEFAULT_SAMPLE_RATE} Hz`,
61
+ ];
62
+ if (report.outputs.length > 0)
63
+ lines.push("outputs:", ...report.outputs.map((d) => ` ${d}`));
64
+ if (report.inputs.length > 0)
65
+ lines.push("inputs:", ...report.inputs.map((d) => ` ${d}`));
66
+ return lines;
67
+ }
68
+
69
+ export async function runDoctorCommand(
70
+ args: readonly string[],
71
+ stdout: { write(text: string): unknown },
72
+ ): Promise<number> {
73
+ if (args.some((arg) => arg === "--help" || arg === "-h")) {
74
+ stdout.write(
75
+ "usage: dawg doctor [--json] audio backend, native sink, devices\n",
76
+ );
77
+ return 0;
78
+ }
79
+ const report = audioDoctor();
80
+ stdout.write(
81
+ args.includes("--json")
82
+ ? `${JSON.stringify(report, null, 2)}\n`
83
+ : `${formatAudioDoctor(report).join("\n")}\n`,
84
+ );
85
+ return 0;
86
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Band-pass banks (0.7 vocoder): RBJ biquads, their group delay and a
3
+ * sample-rate-independent band layout. Band centres and widths depend only
4
+ * on the band count, range and width, never on the sample rate: a band whose
5
+ * centre lies above 0.45 sr is muted instead, so a 22 050 Hz audition and a
6
+ * 48 kHz export have the same layout.
7
+ */
8
+
9
+ export type Biquad = Readonly<{
10
+ b0: number;
11
+ b1: number;
12
+ b2: number;
13
+ a1: number;
14
+ a2: number;
15
+ }>;
16
+
17
+ /** RBJ constant-0-dB-peak band-pass, bandwidth in octaves, pre-warped. */
18
+ export function bandpass(
19
+ hz: number,
20
+ octaves: number,
21
+ sampleRate: number,
22
+ ): Biquad {
23
+ const w = (2 * Math.PI * hz) / sampleRate;
24
+ const s = Math.sin(w);
25
+ const alpha = s * Math.sinh(((Math.LN2 / 2) * octaves * w) / s);
26
+ const a0 = 1 + alpha;
27
+ return {
28
+ b0: alpha / a0,
29
+ b1: 0,
30
+ b2: -alpha / a0,
31
+ a1: (-2 * Math.cos(w)) / a0,
32
+ a2: (1 - alpha) / a0,
33
+ };
34
+ }
35
+
36
+ /** RBJ Butterworth high-pass (Q = 1/sqrt 2). */
37
+ export function highpass(hz: number, sampleRate: number): Biquad {
38
+ const w = (2 * Math.PI * hz) / sampleRate;
39
+ const alpha = Math.sin(w) / (2 * Math.SQRT1_2);
40
+ const c = Math.cos(w);
41
+ const a0 = 1 + alpha;
42
+ return {
43
+ b0: (1 + c) / 2 / a0,
44
+ b1: -(1 + c) / a0,
45
+ b2: (1 + c) / 2 / a0,
46
+ a1: (-2 * c) / a0,
47
+ a2: (1 - alpha) / a0,
48
+ };
49
+ }
50
+
51
+ /** Group delay in samples of one biquad at angle `w` (numeric phase slope). */
52
+ export function groupDelay(f: Biquad, w: number): number {
53
+ const phase = (x: number): number => {
54
+ const nr = f.b0 + f.b1 * Math.cos(x) + f.b2 * Math.cos(2 * x);
55
+ const ni = -(f.b1 * Math.sin(x) + f.b2 * Math.sin(2 * x));
56
+ const dr = 1 + f.a1 * Math.cos(x) + f.a2 * Math.cos(2 * x);
57
+ const di = -(f.a1 * Math.sin(x) + f.a2 * Math.sin(2 * x));
58
+ return Math.atan2(ni, nr) - Math.atan2(di, dr);
59
+ };
60
+ const d = 1e-5;
61
+ let dp = phase(w + d) - phase(w - d);
62
+ while (dp > Math.PI) dp -= 2 * Math.PI;
63
+ while (dp < -Math.PI) dp += 2 * Math.PI;
64
+ return -dp / (2 * d);
65
+ }
66
+
67
+ /** Runs a biquad over `x` in place (transposed direct form II). */
68
+ export function runBiquad(f: Biquad, x: Float64Array): void {
69
+ let z1 = 0;
70
+ let z2 = 0;
71
+ const { b0, b1, b2, a1, a2 } = f;
72
+ for (let i = 0; i < x.length; i += 1) {
73
+ const v = x[i]!;
74
+ const y = b0 * v + z1;
75
+ z1 = b1 * v - a1 * y + z2;
76
+ z2 = b2 * v - a2 * y;
77
+ x[i] = y;
78
+ }
79
+ }
80
+
81
+ export type BandLayout = Readonly<{
82
+ bands: number;
83
+ lo: number;
84
+ hi: number;
85
+ width: number;
86
+ }>;
87
+
88
+ /**
89
+ * Log-spaced centres from `lo` to `hi`. Each of the two cascaded sections is
90
+ * about 1.55x wider than the target so the pair's -3 dB width matches
91
+ * spacing x width.
92
+ */
93
+ export function bandLayout(p: BandLayout): {
94
+ centres: number[];
95
+ ratio: number;
96
+ octaves: number;
97
+ } {
98
+ const n = Math.max(2, Math.round(p.bands));
99
+ const ratio = (p.hi / p.lo) ** (1 / (n - 1));
100
+ const centres = Array.from({ length: n }, (_, k) => p.lo * ratio ** k);
101
+ return { centres, ratio, octaves: Math.log2(ratio) * p.width * 1.55 };
102
+ }
103
+
104
+ export type BankBand = Readonly<{
105
+ hz: number;
106
+ live: boolean;
107
+ filter: Biquad;
108
+ /** Modulator advance: the pair's group delay at hz plus the attack. */
109
+ advance: number;
110
+ }>;
111
+
112
+ /**
113
+ * The bank at one sample rate. Each band's modulator advance cancels the
114
+ * analysis filters' group delay and the follower attack, so consonants land
115
+ * on the beat.
116
+ */
117
+ export function bankDesign(
118
+ p: BandLayout & { attack?: number },
119
+ sampleRate: number,
120
+ ): { bands: BankBand[]; ratio: number } {
121
+ const { centres, ratio, octaves } = bandLayout(p);
122
+ const bands = centres.map((hz) => {
123
+ const live = hz <= 0.45 * sampleRate;
124
+ const filter = bandpass(
125
+ Math.min(hz, 0.45 * sampleRate),
126
+ octaves,
127
+ sampleRate,
128
+ );
129
+ const advance = live
130
+ ? Math.round(
131
+ 2 * groupDelay(filter, (2 * Math.PI * hz) / sampleRate) +
132
+ (p.attack ?? 0) * sampleRate,
133
+ )
134
+ : 0;
135
+ return { hz, live, filter, advance };
136
+ });
137
+ return { bands, ratio };
138
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Cepstral spectral envelope (0.6 B2): the smooth outline of a magnitude
3
+ * spectrum, used by `shift.ts` to hold formants still while the pitch moves.
4
+ * Log magnitude, inverse transform to the real cepstrum, keep the
5
+ * quefrencies below the lifter, forward transform, exponentiate
6
+ * (Oppenheim and Schafer, *Discrete-Time Signal Processing*, ch. 13;
7
+ * J. O. Smith, *Spectral Audio Signal Processing*, "Cepstral Windowing";
8
+ * Röbel and Rodet, "Efficient spectral envelope estimation and its
9
+ * application to pitch shifting and envelope preservation", DAFx 2005, for
10
+ * the true-envelope iteration).
11
+ */
12
+ import { fftInPlace } from "./fft.ts";
13
+ import { inverse } from "./stft.ts";
14
+
15
+ /**
16
+ * Default lifter: quefrencies below 2 ms (measured on a synthetic vowel:
17
+ * with 16 true-envelope passes F1 stays within 3% after +-7 and +-12 st at
18
+ * 110 Hz; 1 ms smeared F1 by up to 15%).
19
+ */
20
+ export const ENVELOPE_LIFTER_SECONDS = 0.002;
21
+ /** True-envelope passes used by `pitchShift` when keeping formants. */
22
+ export const ENVELOPE_ITERATIONS = 16;
23
+
24
+ /** Adaptive lifter as a fraction of the cepstral period. */
25
+ export const PERIOD_FRACTION = 0.25;
26
+
27
+ /** Scratch buffers for one frame size, reused across frames. */
28
+ export type EnvelopeScratch = Readonly<{
29
+ re: Float64Array;
30
+ im: Float64Array;
31
+ log: Float64Array;
32
+ }>;
33
+
34
+ export function envelopeScratch(n: number): EnvelopeScratch {
35
+ return Object.freeze({
36
+ re: new Float64Array(n),
37
+ im: new Float64Array(n),
38
+ log: new Float64Array(n / 2 + 1),
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Writes the cepstral envelope of `mag` (bins 0..n/2 of an n-point
44
+ * spectrum) into `out` (length n/2 + 1). `lifter` is in samples (at least
45
+ * 4). With `iterations` > 0 it is the true envelope (Röbel and Rodet 2005):
46
+ * the log spectrum is raised to the smoothed curve wherever it lies below
47
+ * it and smoothed again, so the envelope rides the harmonic peaks instead
48
+ * of the valleys between them. With `periodRange` (samples) the lifter
49
+ * shrinks to a quarter of the frame's cepstral pitch period when that is
50
+ * shorter, so higher voices keep their harmonics out of the envelope.
51
+ */
52
+ export function cepstralEnvelope(
53
+ mag: Float64Array,
54
+ n: number,
55
+ lifter: number,
56
+ out: Float64Array,
57
+ scratch: EnvelopeScratch = envelopeScratch(n),
58
+ iterations = 0,
59
+ periodRange?: readonly [number, number],
60
+ ): Float64Array {
61
+ const half = n / 2;
62
+ const { re, im, log } = scratch;
63
+ let peak = 0;
64
+ for (let k = 0; k <= half; k += 1) peak = Math.max(peak, mag[k]!);
65
+ // Floor 120 dB under the frame's peak (silence stays finite).
66
+ const floor = peak * 1e-6 + 1e-12;
67
+ for (let k = 0; k <= half; k += 1) log[k] = Math.log(mag[k]! + floor);
68
+ let cut = Math.max(4, Math.min(half, Math.round(lifter)));
69
+ for (let pass = 0; pass <= iterations; pass += 1) {
70
+ for (let k = 0; k < n; k += 1) {
71
+ re[k] = log[k <= half ? k : n - k]!;
72
+ im[k] = 0;
73
+ }
74
+ inverse(re, im);
75
+ if (pass === 0 && periodRange) {
76
+ // Pitch-adaptive lifter: a quarter of the period found as the
77
+ // strongest cepstral peak in range, never longer than `lifter`.
78
+ const [lo, hi] = periodRange;
79
+ let q = Math.max(2, Math.round(lo));
80
+ for (let i = q; i <= Math.min(half, Math.round(hi)); i += 1)
81
+ if (re[i]! > re[q]!) q = i;
82
+ cut = Math.max(4, Math.min(cut, Math.round(q * PERIOD_FRACTION)));
83
+ }
84
+ for (let q = cut; q <= n - cut; q += 1) {
85
+ re[q] = 0;
86
+ im[q] = 0;
87
+ }
88
+ fftInPlace(re, im);
89
+ if (pass < iterations)
90
+ for (let k = 0; k <= half; k += 1) log[k] = Math.max(log[k]!, re[k]!);
91
+ }
92
+ for (let k = 0; k <= half; k += 1) out[k] = Math.exp(re[k]!);
93
+ return out;
94
+ }
95
+
96
+ /**
97
+ * A peak joins the envelope only within this factor (-9 dB) of the smooth
98
+ * cepstral envelope: harmonics sit on it, while window sidelobes and breath
99
+ * noise in the valleys between harmonics sit far below. Without this the
100
+ * envelope followed the source's harmonic comb, and `formant 0` imposed the
101
+ * old pitch on the shifted one (pitch.md 3.5).
102
+ */
103
+ const HARMONIC_PEAK_FLOOR = 10 ** (-9 / 20);
104
+
105
+ /**
106
+ * Harmonic-peak envelope (0.6.1): log magnitude interpolated linearly
107
+ * between the spectral peaks (each refined by a parabola to its true bin
108
+ * and height) of `mag` that stand within `range` of the
109
+ * frame's loudest, held flat past the first and last. Between harmonics it
110
+ * follows the line joining them instead of the smoothed valley, so it stays
111
+ * on the formant at any pitch where the cepstral envelope (whose lifter
112
+ * must shrink with the period) starts to smear or ripple. `smooth` (an
113
+ * envelope from `cepstralEnvelope`) is used where no peak stands.
114
+ * Measured with `formant 0` on a synthetic vowel: F1 within 1.1% of the
115
+ * same vowel sung at the new pitch for f0 110-330 Hz at +-7 and +12 st
116
+ * (the cepstral envelope alone: -29% to +46% above 200 Hz).
117
+ */
118
+ export function peakEnvelope(
119
+ mag: Float64Array,
120
+ half: number,
121
+ out: Float64Array,
122
+ smooth: Float64Array,
123
+ peaks: number[],
124
+ range = 1e-4,
125
+ ): Float64Array {
126
+ let top = 0;
127
+ for (let k = 0; k <= half; k += 1) top = Math.max(top, mag[k]!);
128
+ peaks.length = 0;
129
+ const floor = top * range;
130
+ for (let k = 2; k <= half - 2; k += 1) {
131
+ const v = mag[k]!;
132
+ if (
133
+ v > floor &&
134
+ v >= smooth[k]! * HARMONIC_PEAK_FLOOR &&
135
+ v > mag[k - 1]! &&
136
+ v >= mag[k + 1]! &&
137
+ v > mag[k - 2]! &&
138
+ v >= mag[k + 2]!
139
+ )
140
+ peaks.push(k);
141
+ }
142
+ if (peaks.length < 2) {
143
+ out.set(smooth.subarray(0, half + 1));
144
+ return out;
145
+ }
146
+ // Each peak refined by a parabola on log magnitude: its true frequency
147
+ // (fractional bin) and height.
148
+ const at = (i: number) => {
149
+ const k = peaks[i]!;
150
+ const a = Math.log(mag[k - 1]! + 1e-30);
151
+ const b = Math.log(mag[k]! + 1e-30);
152
+ const c = Math.log(mag[k + 1]! + 1e-30);
153
+ const d = a - 2 * b + c;
154
+ const delta =
155
+ d < 0 ? Math.max(-0.5, Math.min(0.5, (0.5 * (a - c)) / d)) : 0;
156
+ return [k + delta, b - 0.25 * (a - c) * delta] as const;
157
+ };
158
+ let j = 0;
159
+ let [x0, y0] = at(0);
160
+ let [x1, y1] = at(1);
161
+ for (let k = 0; k <= half; k += 1) {
162
+ while (j + 2 < peaks.length && x1 <= k) {
163
+ j += 1;
164
+ [x0, y0] = [x1, y1];
165
+ [x1, y1] = at(j + 1);
166
+ }
167
+ const t = Math.max(0, Math.min(1, (k - x0) / (x1 - x0)));
168
+ out[k] = Math.exp(y0 + (y1 - y0) * t);
169
+ }
170
+ return out;
171
+ }