@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
package/core/clips.ts ADDED
@@ -0,0 +1,499 @@
1
+ /**
2
+ * Audio clips and lyrics (0.7 clips lane): the pure, render-free half.
3
+ * Placement on the tempo map, the section cuts (`mapClips`, `sliceClips`)
4
+ * that sections, forms and render windows apply beside `slicePedals`,
5
+ * clip repeats, dB gain helpers and the lyric grammar that `/lyrics`, the
6
+ * SDK's `lyrics()` and `set_lyrics` share. The renderer is
7
+ * src/audio/clips.ts.
8
+ */
9
+ import type { AudioClip, Note, SampleRef, Track } from "./score.ts";
10
+ import { SCORE_LIMITS, TrackScore } from "./score.ts";
11
+ import {
12
+ secondsAtTick,
13
+ tickAtSeconds,
14
+ type TimeScore,
15
+ type TrackTime,
16
+ } from "./tempo.ts";
17
+
18
+ /** Sample-bank voice name prefix a clip's audio loads under. */
19
+ export const CLIP_VOICE_PREFIX = "clip:";
20
+
21
+ /** The sample reference a clip's audio loads through. */
22
+ export function clipSampleRef(clip: AudioClip): SampleRef {
23
+ return { src: clip.src, sha256: clip.sha256 };
24
+ }
25
+
26
+ /** The instrument whose notes are guides: silent in a render (0.7). */
27
+ export const GUIDE_INSTRUMENT = "vocal";
28
+
29
+ /** True for an instrument whose notes only guide its clips. */
30
+ export function isGuideInstrument(instrument: string | undefined): boolean {
31
+ return instrument === GUIDE_INSTRUMENT;
32
+ }
33
+
34
+ /**
35
+ * The `vocal` default chain (sources.md section 4): a 90 Hz high-pass for
36
+ * rumble and plosives, a 3:1 compressor, a short plate. Fills only the
37
+ * fields a track does not set, so choosing `vocal` never undoes a mix.
38
+ */
39
+ export function vocalChainPatch(
40
+ track: Readonly<Pick<Track, "filter" | "fx" | "reverb">> | undefined,
41
+ ): Partial<Pick<Track, "filter" | "fx" | "reverb">> {
42
+ const patch: Partial<Pick<Track, "filter" | "fx" | "reverb">> = {};
43
+ if (!track?.filter)
44
+ Object.assign(patch, {
45
+ filter: { cutoff: 90, resonance: 0, type: "hpf" },
46
+ });
47
+ if (!track?.fx?.compressor)
48
+ Object.assign(patch, {
49
+ fx: {
50
+ ...(track?.fx ?? {}),
51
+ compressor: {
52
+ threshold: -18,
53
+ ratio: 3,
54
+ attack: 0.01,
55
+ release: 0.15,
56
+ makeup: 3,
57
+ },
58
+ },
59
+ });
60
+ if (!track?.reverb)
61
+ Object.assign(patch, {
62
+ reverb: { mix: 0.14, size: 0.4, ir: { src: "builtin:plate" } },
63
+ });
64
+ return patch;
65
+ }
66
+
67
+ /** A clip's fade when it sets none: 5 ms, so a cut never clicks. */
68
+ export const DEFAULT_CLIP_FADE = 0.005;
69
+ /** The fade a section, form or window edge puts on a clip it cuts. */
70
+ export const CLIP_EDGE_FADE = 0.005;
71
+ /** Peak an imported clip's gain targets, in dBFS. */
72
+ export const IMPORT_PEAK_DBFS = -6;
73
+ /** The clip gain range every surface accepts, in dB (/clip, menu, tools). */
74
+ export const CLIP_GAIN_MIN_DB = -60;
75
+ export const CLIP_GAIN_MAX_DB = 12;
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Gain in dB (commands and tools speak dB; the score stores linear)
79
+
80
+ /** Linear clip gain for `db` dB, clamped to the clip gain range. */
81
+ export function dbToClipGain(db: number): number {
82
+ return Math.max(0, Math.min(SCORE_LIMITS.maxClipGain, 10 ** (db / 20)));
83
+ }
84
+
85
+ /** dB for a linear clip gain (`-inf` for 0), rounded to 0.1 dB. */
86
+ export function clipGainDb(gain: number): number {
87
+ if (gain <= 0) return -Infinity;
88
+ return Math.round(20 * Math.log10(gain) * 10) / 10;
89
+ }
90
+
91
+ /** Clip gain that brings a file peaking at `peak` (linear) to -6 dBFS. */
92
+ export function importGain(peak: number): number | undefined {
93
+ if (!(peak > 0)) return undefined;
94
+ const gain = 10 ** (IMPORT_PEAK_DBFS / 20) / peak;
95
+ const rounded = Math.round(gain * 1000) / 1000;
96
+ if (Math.abs(rounded - 1) < 0.001) return undefined;
97
+ return Math.max(0.001, Math.min(SCORE_LIMITS.maxClipGain, rounded));
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Placement
102
+
103
+ /**
104
+ * A clip's start in song ticks: its `startTick`, moved by the track's
105
+ * tempo ratio and phase the way the first pass of a note is
106
+ * (`phase + start / rate`). Clips do not repeat with a track `cycle`;
107
+ * `repeatAudio` and `/clip repeat` write repeats out.
108
+ */
109
+ export function clipSongTick(clip: AudioClip, time?: TrackTime): number {
110
+ if (!time) return clip.startTick;
111
+ const rate = time.rate ?? 1;
112
+ return Math.max(0, (time.phase ?? 0) + clip.startTick / rate);
113
+ }
114
+
115
+ /** Seconds a clip plays: `dur`, else the file from `offset` to its end. */
116
+ export function clipLength(
117
+ clip: AudioClip,
118
+ fileSeconds: number = SCORE_LIMITS.maxClipSeconds,
119
+ ): number {
120
+ const rest = Math.max(0, fileSeconds - (clip.offset ?? 0));
121
+ return clip.dur === undefined ? rest : Math.min(clip.dur, rest);
122
+ }
123
+
124
+ /**
125
+ * `score` with every clip that has no `dur` given its real length,
126
+ * `seconds(trackId, clip) - offset`, where the file's length is known
127
+ * (its audio loaded). Section, form and window cuts need the real end of
128
+ * a clip: a reversed clip's cut moves its offset from the far end.
129
+ * Clips whose audio is missing are left as they are (they render silent).
130
+ */
131
+ export function resolveClipLengths(
132
+ score: TrackScore,
133
+ seconds: (trackId: string, clip: AudioClip) => number | undefined,
134
+ ): TrackScore {
135
+ let changed = false;
136
+ const tracks = score.tracks.map((track) => {
137
+ if (!track.clips?.some((clip) => clip.dur === undefined)) return track;
138
+ const clips = track.clips.map((clip) => {
139
+ if (clip.dur !== undefined) return clip;
140
+ const file = seconds(track.id, clip);
141
+ if (file === undefined || !(file > 0)) return clip;
142
+ const dur =
143
+ Math.round(Math.max(0, file - (clip.offset ?? 0)) * 1e6) / 1e6;
144
+ if (!(dur > 0)) return clip;
145
+ changed = true;
146
+ return { ...clip, dur };
147
+ });
148
+ return { ...track, clips };
149
+ });
150
+ return changed ? new TrackScore({ ...score.toJSON(), tracks }) : score;
151
+ }
152
+
153
+ /** Song seconds where a clip starts and stops on `score`'s tempo map. */
154
+ export function clipSpan(
155
+ score: TimeScore,
156
+ clip: AudioClip,
157
+ fileSeconds?: number,
158
+ time?: TrackTime,
159
+ ): { start: number; end: number } {
160
+ const start = secondsAtTick(score, clipSongTick(clip, time));
161
+ return { start, end: start + clipLength(clip, fileSeconds) };
162
+ }
163
+
164
+ /** The song tick where a clip stops (its end on the tempo map). */
165
+ export function clipEndTick(
166
+ score: TimeScore,
167
+ clip: AudioClip,
168
+ fileSeconds?: number,
169
+ ): number {
170
+ return tickAtSeconds(score, clipSpan(score, clip, fileSeconds).end);
171
+ }
172
+
173
+ /** Track time baked into clip starts (for `bakeTrackTime`). */
174
+ export function bakeClipTime(track: Track): Track {
175
+ if (!track.clips || !track.time) return track;
176
+ const time = track.time;
177
+ return {
178
+ ...track,
179
+ clips: track.clips.map((clip) => ({
180
+ ...clip,
181
+ startTick: Math.round(clipSongTick(clip, time)),
182
+ })),
183
+ };
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Section cuts
188
+
189
+ /** `fn` over a track's clips; `undefined` drops a clip. */
190
+ export function mapClips(
191
+ track: Track,
192
+ fn: (clip: AudioClip) => AudioClip | undefined,
193
+ ): Track {
194
+ if (!track.clips) return track;
195
+ let changed = false;
196
+ const out: AudioClip[] = [];
197
+ for (const clip of track.clips) {
198
+ const next = fn(clip);
199
+ if (next !== clip) changed = true;
200
+ if (next) out.push(next);
201
+ }
202
+ if (!changed) return track;
203
+ const { clips: _clips, ...rest } = track;
204
+ return out.length > 0 ? { ...rest, clips: out } : rest;
205
+ }
206
+
207
+ /** One stretch of source ticks `[from, to)` played at output tick `offset`. */
208
+ export type ClipPiece = Readonly<{
209
+ from: number;
210
+ to: number;
211
+ offset: number;
212
+ /** The piece's section mutes this track. */
213
+ mute?: boolean;
214
+ /** The piece's section variation gain. */
215
+ gain?: number;
216
+ /**
217
+ * The piece's start is a render window's seam, not a section edge: a
218
+ * clip sounding across it plays on without a fade.
219
+ */
220
+ seam?: boolean;
221
+ }>;
222
+
223
+ /**
224
+ * Cut `clips` to `pieces`, as `slicePedals` cuts pedals: a clip starting in
225
+ * a piece moves with it; a clip still sounding where a piece starts plays
226
+ * on from the matching point of the file; a piece's end cuts it. Pieces
227
+ * that run on in both source and output (a form playing a section into the
228
+ * next one as written) join first, so straight-through music is never cut.
229
+ * Each cut gets a 5 ms equal-power fade. Durations follow `time`, the
230
+ * source score's tempo map (audio plays at natural speed).
231
+ */
232
+ export function sliceClips(
233
+ clips: readonly AudioClip[] | undefined,
234
+ pieces: readonly ClipPiece[],
235
+ time: TimeScore,
236
+ ): AudioClip[] | undefined {
237
+ if (!clips || clips.length === 0) return undefined;
238
+ const joined: ClipPiece[] = [];
239
+ for (const piece of pieces) {
240
+ const last = joined.at(-1);
241
+ if (
242
+ last &&
243
+ last.to === piece.from &&
244
+ last.offset + (last.to - last.from) === piece.offset &&
245
+ (last.mute ?? false) === (piece.mute ?? false) &&
246
+ (last.gain ?? 1) === (piece.gain ?? 1)
247
+ )
248
+ joined[joined.length - 1] = { ...last, to: piece.to };
249
+ else joined.push(piece);
250
+ }
251
+ const out: AudioClip[] = [];
252
+ const ids = new Map<string, number>();
253
+ for (const piece of joined) {
254
+ if (piece.mute || piece.to <= piece.from) continue;
255
+ const pieceStart = secondsAtTick(time, piece.from);
256
+ const pieceEnd = secondsAtTick(time, piece.to);
257
+ for (const clip of clips) {
258
+ const { start, end } = clipSpan(time, clip);
259
+ if (start >= pieceEnd || end <= pieceStart) continue;
260
+ if (clip.startTick >= piece.to) continue;
261
+ const cut = cutClip(
262
+ clip,
263
+ start,
264
+ end,
265
+ pieceStart,
266
+ pieceEnd,
267
+ piece.seam === true,
268
+ );
269
+ if (!cut) continue;
270
+ const pass = (ids.get(clip.id) ?? 0) + 1;
271
+ ids.set(clip.id, pass);
272
+ const gain =
273
+ piece.gain === undefined
274
+ ? cut.gain
275
+ : Math.min(SCORE_LIMITS.maxClipGain, (cut.gain ?? 1) * piece.gain);
276
+ out.push({
277
+ ...cut,
278
+ id: pass === 1 ? clip.id : clipPassId(clip.id, pass),
279
+ startTick:
280
+ clip.startTick >= piece.from
281
+ ? piece.offset + (clip.startTick - piece.from)
282
+ : piece.offset,
283
+ ...(gain === undefined || gain === 1 ? {} : { gain }),
284
+ });
285
+ }
286
+ }
287
+ return out.length > 0 ? out : undefined;
288
+ }
289
+
290
+ function clipPassId(id: string, pass: number): string {
291
+ const suffix = `~${pass}`;
292
+ return `${id.slice(0, SCORE_LIMITS.maxIdLength - suffix.length)}${suffix}`;
293
+ }
294
+
295
+ /**
296
+ * `clip` (sounding `start..end` song seconds) cut to `from..to`, with edge
297
+ * fades where it is cut. Undefined when nothing is left.
298
+ */
299
+ export function cutClip(
300
+ clip: AudioClip,
301
+ start: number,
302
+ end: number,
303
+ from: number,
304
+ to: number,
305
+ seam = false,
306
+ ): AudioClip | undefined {
307
+ const head = Math.max(0, from - start);
308
+ const tail = Math.max(0, end - to);
309
+ if (head < 1e-9 && tail < 1e-9) return clip;
310
+ const length = end - start - head - tail;
311
+ if (length < 0.001) return undefined;
312
+ const offset = clip.offset ?? 0;
313
+ // A reversed clip plays offset+dur back to offset: cutting its head
314
+ // shortens it from the far end of the window, cutting its tail moves
315
+ // the window's start.
316
+ // A reversed clip with no `dur` has an unknown end (its audio did not
317
+ // load, so it renders silent): its offset is left alone rather than
318
+ // measured from a made-up end. Loaded clips are resolved to a real
319
+ // `dur` first (`resolveClipLengths`).
320
+ const nextOffset = clip.rev
321
+ ? clip.dur === undefined
322
+ ? offset
323
+ : offset + tail
324
+ : offset + head;
325
+ const rounded = (value: number) => Math.round(value * 1e6) / 1e6;
326
+ const fit = (fade: number | undefined, cut: boolean) => {
327
+ const value = cut ? CLIP_EDGE_FADE : (fade ?? DEFAULT_CLIP_FADE);
328
+ return Math.min(value, length / 2);
329
+ };
330
+ // A seam continues the clip mid-play: what is left of its fade-in, as
331
+ // a fade it can no longer express, is dropped (fades are 5 ms).
332
+ const fadeInTime =
333
+ seam && head > 1e-9 ? 0 : fit(clip.fadeInTime, head > 1e-9);
334
+ const fadeTime = fit(clip.fadeTime, tail > 1e-9);
335
+ return {
336
+ ...clip,
337
+ ...(nextOffset > 0 ? { offset: rounded(nextOffset) } : {}),
338
+ dur: rounded(length),
339
+ ...(head > 1e-9 || clip.fadeInTime !== undefined ? { fadeInTime } : {}),
340
+ ...(tail > 1e-9 || clip.fadeTime !== undefined ? { fadeTime } : {}),
341
+ };
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // Repeats and ids
346
+
347
+ /** An unused clip id on `track`: `base`, `base2`, `base3`, ... */
348
+ /**
349
+ * `clip` on `track` cut in two at song tick `tick` (5 ms equal-power fades
350
+ * at the cut), the tail under a fresh id. `fileSeconds` is the file's
351
+ * length when known, so a reversed clip without `dur` is cut against its
352
+ * real end. A string says why it cannot split there.
353
+ */
354
+ export function splitClip(
355
+ score: TimeScore,
356
+ track: Track,
357
+ clip: AudioClip,
358
+ tick: number,
359
+ fileSeconds?: number,
360
+ ): { head: AudioClip; tail: AudioClip } | string {
361
+ const whole =
362
+ clip.rev && clip.dur === undefined && fileSeconds !== undefined
363
+ ? { ...clip, dur: Math.max(0.001, fileSeconds - (clip.offset ?? 0)) }
364
+ : clip;
365
+ const { start, end } = clipSpan(score, whole, fileSeconds, track.time);
366
+ const at = secondsAtTick(score, tick);
367
+ if (at <= start + 0.001 || at >= end - 0.001)
368
+ return `clip ${clip.id} is not sounding there`;
369
+ const head = cutClip(whole, start, end, start, at);
370
+ const tail = cutClip(whole, start, end, at, end);
371
+ if (!head || !tail) return `clip ${clip.id}: too short to split there`;
372
+ return {
373
+ head,
374
+ tail: {
375
+ ...tail,
376
+ id: nextClipId(track, clip.id),
377
+ startTick: Math.round(tickAtSeconds(score, at)),
378
+ },
379
+ };
380
+ }
381
+
382
+ export function nextClipId(track: Track, base = "clip"): string {
383
+ const taken = new Set((track.clips ?? []).map((clip) => clip.id));
384
+ const stem = base.slice(0, SCORE_LIMITS.maxIdLength - 4) || "clip";
385
+ if (!taken.has(stem)) return stem;
386
+ for (let n = 2; ; n += 1) if (!taken.has(`${stem}${n}`)) return `${stem}${n}`;
387
+ }
388
+
389
+ /**
390
+ * Copies of `clip` every `every` ticks after it while they start before
391
+ * `until` (a tick), with ids `<id>-r2`, `<id>-r3`, ... The clip itself is
392
+ * not included.
393
+ */
394
+ export function repeatClip(
395
+ clip: AudioClip,
396
+ every: number,
397
+ until: number,
398
+ taken: ReadonlySet<string> = new Set(),
399
+ ): AudioClip[] {
400
+ if (!(every > 0)) return [];
401
+ const out: AudioClip[] = [];
402
+ const used = new Set(taken);
403
+ let pass = 2;
404
+ for (
405
+ let tick = clip.startTick + every;
406
+ tick < until && out.length < SCORE_LIMITS.maxClipsPerTrack;
407
+ tick += every
408
+ ) {
409
+ let id = `${clip.id.slice(0, SCORE_LIMITS.maxIdLength - 5)}-r${pass}`;
410
+ for (let extra = 2; used.has(id); extra += 1)
411
+ id = `${clip.id.slice(0, SCORE_LIMITS.maxIdLength - 8)}-r${pass}.${extra}`;
412
+ used.add(id);
413
+ out.push({ ...clip, id, startTick: Math.round(tick) });
414
+ pass += 1;
415
+ }
416
+ return out;
417
+ }
418
+
419
+ export {
420
+ parseLyric,
421
+ autoSyllabify,
422
+ assignLyrics,
423
+ type LyricToken,
424
+ type LyricAssignment,
425
+ } from "./lyrics.ts";
426
+
427
+ /** The lyric text the notes carry, in time order (`_` holds kept). */
428
+ export function lyricText(
429
+ notes: readonly Pick<Note, "startTick" | "pitch" | "lyric">[],
430
+ ): string {
431
+ return [...notes]
432
+ .filter((note) => note.lyric !== undefined)
433
+ .sort((a, b) => a.startTick - b.startTick || a.pitch - b.pitch)
434
+ .map((note) => note.lyric)
435
+ .join(" ");
436
+ }
437
+
438
+ // ---------------------------------------------------------------------------
439
+ // Bar edits (insertBars, deleteBars, copyBars move clips as they move notes)
440
+
441
+ /** Clips starting at or after `at` move `shift` ticks later. */
442
+ export function insertClipBars(track: Track, at: number, shift: number): Track {
443
+ return mapClips(track, (clip) =>
444
+ clip.startTick >= at
445
+ ? { ...clip, startTick: clip.startTick + shift }
446
+ : clip,
447
+ );
448
+ }
449
+
450
+ /**
451
+ * Bars `[at, end)` go: a clip starting there goes with them, a later clip
452
+ * moves back. A clip starting earlier keeps its place and plays on.
453
+ */
454
+ export function deleteClipBars(track: Track, at: number, end: number): Track {
455
+ return mapClips(track, (clip) =>
456
+ clip.startTick >= end
457
+ ? { ...clip, startTick: clip.startTick - (end - at) }
458
+ : clip.startTick >= at
459
+ ? undefined
460
+ : clip,
461
+ );
462
+ }
463
+
464
+ /**
465
+ * Clips starting in `[from, from + length)` are copied to `to` with fresh
466
+ * ids; clips that started in the destination are replaced.
467
+ */
468
+ export function copyClipBars(
469
+ track: Track,
470
+ from: number,
471
+ length: number,
472
+ to: number,
473
+ ): Track {
474
+ if (!track.clips) return track;
475
+ const sources = track.clips.filter(
476
+ (clip) => clip.startTick >= from && clip.startTick < from + length,
477
+ );
478
+ if (sources.length === 0) return track;
479
+ const kept = track.clips.filter(
480
+ (clip) => clip.startTick < to || clip.startTick >= to + length,
481
+ );
482
+ const used = new Set(kept.map((clip) => clip.id));
483
+ const fresh = (id: string): string => {
484
+ const stem = id.replace(/-c\d+$/u, "").slice(0, 24);
485
+ for (let n = 2; ; n += 1) {
486
+ const next = `${stem}-c${n}`;
487
+ if (!used.has(next) && !sources.some((clip) => clip.id === next)) {
488
+ used.add(next);
489
+ return next;
490
+ }
491
+ }
492
+ };
493
+ const copies = sources.map((clip) => ({
494
+ ...clip,
495
+ id: fresh(clip.id),
496
+ startTick: clip.startTick - from + to,
497
+ }));
498
+ return { ...track, clips: [...kept, ...copies] };
499
+ }