@hraness/dawg 0.6.1 → 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 (253) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/DAWG.md +343 -116
  3. package/README.md +27 -25
  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 +182 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/instruments.ts +19 -0
  11. package/core/keys.ts +3 -3
  12. package/core/loop.ts +5 -0
  13. package/core/lyrics.ts +297 -0
  14. package/core/master.ts +3 -3
  15. package/core/resonators.ts +16 -2
  16. package/core/routing.ts +165 -0
  17. package/core/score.ts +832 -13
  18. package/core/sdk/eval-child.ts +7 -2
  19. package/core/sdk/eval.ts +35 -6
  20. package/core/sdk/print.ts +276 -3
  21. package/core/sdk/sync-lyrics.ts +49 -0
  22. package/core/sdk/v1.ts +1849 -42
  23. package/core/sections.ts +292 -26
  24. package/core/sing.ts +815 -0
  25. package/core/style-provenance.ts +80 -0
  26. package/core/styles/africa-mena-southasia.ts +2893 -0
  27. package/core/styles/americas.ts +3810 -0
  28. package/core/styles/art.ts +4993 -0
  29. package/core/styles/base.ts +123 -0
  30. package/core/styles/cycles.ts +106 -0
  31. package/core/styles/electronic.ts +2723 -0
  32. package/core/styles/europe-asia-pacific.ts +2838 -0
  33. package/core/styles/excerpt.ts +29 -0
  34. package/core/styles/gamelan.ts +283 -0
  35. package/core/styles/generate.ts +2199 -0
  36. package/core/styles/index.ts +515 -0
  37. package/core/styles/parts.ts +106 -0
  38. package/core/styles/pop.ts +3189 -0
  39. package/core/styles/rock.ts +2993 -0
  40. package/core/styles/roots.ts +4175 -0
  41. package/core/styles/schema.ts +429 -0
  42. package/core/styles/taxonomy.ts +940 -0
  43. package/core/styles/validate.ts +528 -0
  44. package/core/tempo.ts +32 -2
  45. package/core/tuning.ts +19 -3
  46. package/core/vocoder.ts +524 -0
  47. package/guides/agent.md +29 -0
  48. package/guides/arrange.md +30 -0
  49. package/guides/audition.md +20 -14
  50. package/guides/automation.md +12 -7
  51. package/guides/chords.md +15 -15
  52. package/guides/effects.md +17 -16
  53. package/guides/faders.md +20 -16
  54. package/guides/files.md +13 -9
  55. package/guides/getting-started.md +15 -11
  56. package/guides/keys.md +19 -15
  57. package/guides/media.md +17 -12
  58. package/guides/mix.md +12 -6
  59. package/guides/music.md +23 -8
  60. package/guides/notes.md +15 -9
  61. package/guides/performance.md +15 -13
  62. package/guides/play.md +21 -13
  63. package/guides/project.md +25 -8
  64. package/guides/providers.md +20 -14
  65. package/guides/resample.md +15 -9
  66. package/guides/rhythm.md +17 -13
  67. package/guides/sessions.md +17 -7
  68. package/guides/show-me.md +31 -0
  69. package/guides/sound.md +25 -9
  70. package/guides/sounds.md +15 -13
  71. package/guides/styles.md +31 -0
  72. package/guides/tempo.md +15 -10
  73. package/guides/tracks.md +15 -10
  74. package/guides/tuning.md +32 -0
  75. package/guides/voice.md +31 -0
  76. package/guides/web-search.md +18 -8
  77. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  78. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  79. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  80. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  81. package/native/prebuilt/manifest.json +21 -0
  82. package/package.json +5 -2
  83. package/src/agent/agent.ts +126 -14
  84. package/src/agent/calibration-tools.ts +53 -0
  85. package/src/agent/clip-tools.ts +453 -0
  86. package/src/agent/command-agent.ts +369 -0
  87. package/src/agent/drum-tools.ts +2 -2
  88. package/src/agent/expression-tools.ts +1 -1
  89. package/src/agent/gateway.ts +246 -60
  90. package/src/agent/models.ts +53 -12
  91. package/src/agent/ops.ts +12 -1
  92. package/src/agent/pack-tools.ts +1 -1
  93. package/src/agent/planner.ts +13 -0
  94. package/src/agent/portable-schema.ts +80 -0
  95. package/src/agent/preview-tool.ts +4 -1
  96. package/src/agent/provider.ts +22 -8
  97. package/src/agent/rhythm-tools.ts +1 -1
  98. package/src/agent/section-tools.ts +1 -1
  99. package/src/agent/show-me.ts +497 -0
  100. package/src/agent/steer.ts +15 -0
  101. package/src/agent/style-tools.ts +217 -0
  102. package/src/agent/tool-error.ts +12 -0
  103. package/src/agent/tools.ts +108 -23
  104. package/src/agent/usage.ts +2 -2
  105. package/src/agent/voice-tools.ts +925 -0
  106. package/src/agent/xcb-agent.ts +11 -7
  107. package/src/argv.ts +38 -0
  108. package/src/audio/analysis.ts +253 -0
  109. package/src/audio/arrange.ts +37 -3
  110. package/src/audio/autotune-engine.ts +101 -0
  111. package/src/audio/autotune.ts +640 -0
  112. package/src/audio/clips.ts +240 -0
  113. package/src/audio/doctor.ts +86 -0
  114. package/src/audio/dsp/bandbank.ts +138 -0
  115. package/src/audio/dsp/envelope.ts +10 -0
  116. package/src/audio/dsp/follow.ts +120 -0
  117. package/src/audio/dsp/formant.ts +427 -0
  118. package/src/audio/dsp/glottal.ts +243 -0
  119. package/src/audio/dsp/interp.ts +7 -2
  120. package/src/audio/dsp/lpc.ts +50 -0
  121. package/src/audio/dsp/periodicity.ts +59 -0
  122. package/src/audio/dsp/pitch.ts +995 -0
  123. package/src/audio/dsp/psola.ts +199 -0
  124. package/src/audio/effects/chain.ts +3 -1
  125. package/src/audio/effects/common.ts +43 -0
  126. package/src/audio/effects/convolution.ts +7 -4
  127. package/src/audio/effects/filter.ts +48 -69
  128. package/src/audio/effects/formant.ts +263 -0
  129. package/src/audio/engine.ts +160 -28
  130. package/src/audio/fit.ts +35 -3
  131. package/src/audio/instrument-check.ts +59 -43
  132. package/src/audio/instruments.ts +4 -0
  133. package/src/audio/keys/calibration.ts +56 -0
  134. package/src/audio/keys/electric.ts +8 -1
  135. package/src/audio/keys/engine.ts +13 -1
  136. package/src/audio/keys/piano.ts +22 -2
  137. package/src/audio/kits.ts +135 -6
  138. package/src/audio/live.ts +114 -20
  139. package/src/audio/native.ts +615 -0
  140. package/src/audio/preview.ts +30 -2
  141. package/src/audio/render-worker.ts +2 -0
  142. package/src/audio/renderer.ts +2 -0
  143. package/src/audio/resample.ts +2 -1
  144. package/src/audio/sampler.ts +85 -4
  145. package/src/audio/samples.ts +20 -2
  146. package/src/audio/sing/analysis.ts +193 -0
  147. package/src/audio/sing/engine.ts +949 -0
  148. package/src/audio/strings/bow.ts +48 -5
  149. package/src/audio/strings/engine.ts +8 -2
  150. package/src/audio/synth/oscillators.ts +31 -21
  151. package/src/audio/synth/voice.ts +34 -1
  152. package/src/audio/vocoder/bank.ts +314 -0
  153. package/src/audio/vocoder/carrier.ts +165 -0
  154. package/src/audio/vocoder/control.ts +68 -0
  155. package/src/audio/vocoder/detect.ts +50 -0
  156. package/src/audio/vocoder/index.ts +304 -0
  157. package/src/audio/vocoder/talkbox.ts +143 -0
  158. package/src/audio/wav.ts +500 -77
  159. package/src/audio/winds/engine.ts +5 -1
  160. package/src/audio/winds/trim.ts +28 -4
  161. package/src/audio/winds/trims1.ts +297 -0
  162. package/src/audio/winds/voice.ts +15 -2
  163. package/src/auth/cli.ts +38 -36
  164. package/src/auth/credentials.ts +30 -1
  165. package/src/auth/login.ts +15 -9
  166. package/src/auth/tui.ts +19 -10
  167. package/src/commands/arrange.ts +44 -29
  168. package/src/commands/autotune.ts +421 -0
  169. package/src/commands/calibration.ts +74 -0
  170. package/src/commands/clips.ts +887 -0
  171. package/src/commands/drums.ts +3 -2
  172. package/src/commands/edit.ts +11 -4
  173. package/src/commands/expression.ts +1 -1
  174. package/src/commands/formant.ts +221 -0
  175. package/src/commands/fx.ts +101 -32
  176. package/src/commands/grammar.ts +558 -0
  177. package/src/commands/help.ts +610 -380
  178. package/src/commands/history.ts +18 -0
  179. package/src/commands/keys.ts +8 -8
  180. package/src/commands/modal.ts +1 -1
  181. package/src/commands/music.ts +1 -1
  182. package/src/commands/nearest.ts +53 -0
  183. package/src/commands/pack.ts +9 -2
  184. package/src/commands/param-range.ts +56 -0
  185. package/src/commands/parses.ts +130 -0
  186. package/src/commands/progression.ts +170 -0
  187. package/src/commands/rhythm.ts +3 -0
  188. package/src/commands/rig.ts +3 -24
  189. package/src/commands/sing.ts +478 -0
  190. package/src/commands/strum.ts +13 -1
  191. package/src/commands/style.ts +415 -0
  192. package/src/commands/time.ts +6 -3
  193. package/src/commands/tuning.ts +3 -3
  194. package/src/commands/vocal-pitch.ts +616 -0
  195. package/src/commands/vocal.ts +147 -0
  196. package/src/commands/vocoder.ts +627 -0
  197. package/src/commands/wind.ts +2 -2
  198. package/src/fs/durable.ts +50 -0
  199. package/src/lang/glossary.ts +493 -0
  200. package/src/launch-args.ts +163 -0
  201. package/src/main.ts +1397 -253
  202. package/src/media/cli.ts +20 -3
  203. package/src/media/import.ts +3 -1
  204. package/src/project/check.ts +21 -2
  205. package/src/project/clip-pins.ts +72 -0
  206. package/src/project/init.ts +23 -8
  207. package/src/project/sync.ts +418 -86
  208. package/src/render.ts +20 -1
  209. package/src/session/daemon.ts +3 -0
  210. package/src/session/meta.ts +14 -0
  211. package/src/session/origin.ts +154 -0
  212. package/src/session/port.ts +22 -4
  213. package/src/session/presence.ts +34 -4
  214. package/src/session/protocol.ts +5 -1
  215. package/src/session/rebase.ts +18 -5
  216. package/src/session/receipt.ts +258 -0
  217. package/src/session/store.ts +65 -43
  218. package/src/tui/arrange-menu.ts +65 -31
  219. package/src/tui/audition.ts +1 -1
  220. package/src/tui/euclid.ts +18 -13
  221. package/src/tui/fader.ts +228 -41
  222. package/src/tui/granular-menu.ts +2 -4
  223. package/src/tui/menu-clips.ts +297 -0
  224. package/src/tui/menu-time.ts +20 -13
  225. package/src/tui/menu-voice.ts +405 -0
  226. package/src/tui/menu.ts +759 -177
  227. package/src/tui/modal-menu.ts +6 -6
  228. package/src/tui/performance-menu.ts +5 -2
  229. package/src/tui/play-chords.ts +4 -2
  230. package/src/tui/play-mode.ts +15 -1
  231. package/src/tui/play-session.ts +47 -7
  232. package/src/tui/sing-menu.ts +278 -0
  233. package/src/tui/style-menu.ts +104 -0
  234. package/src/tui/vocoder-menu.ts +244 -0
  235. package/src/tui/wind-menu.ts +3 -3
  236. package/src/version.ts +8 -0
  237. package/src/web/fetch.ts +115 -29
  238. package/tui/activity.ts +180 -9
  239. package/tui/app.ts +274 -40
  240. package/tui/clip-row.ts +132 -0
  241. package/tui/delight.ts +144 -0
  242. package/tui/drawer.ts +70 -22
  243. package/tui/frame-gate.ts +76 -0
  244. package/tui/grammar.ts +112 -63
  245. package/tui/guide.ts +42 -4
  246. package/tui/highway.ts +269 -25
  247. package/tui/hints.ts +192 -0
  248. package/tui/input.ts +60 -9
  249. package/tui/keys.ts +1 -1
  250. package/tui/play-strip.ts +68 -14
  251. package/tui/prompt.ts +1 -1
  252. package/tui/screen.ts +144 -14
  253. package/tui/theme.ts +27 -0
package/core/sections.ts CHANGED
@@ -13,9 +13,19 @@
13
13
  */
14
14
  import { DRUM_VOICES, isDrumInstrument } from "./drums.ts";
15
15
  import { FX_LANES } from "./fx.ts";
16
+ import { synthKit } from "./kits.ts";
16
17
  import { resolveString } from "./strings.ts";
17
18
  import { modalSettings, windSettings } from "./resonators.ts";
18
- import { slicePedals } from "./expression.ts";
19
+ import { resolveSing } from "./sing.ts";
20
+ import { slicePedals, type PedalEvent, type PedalState } from "./expression.ts";
21
+ import {
22
+ bakeClipTime,
23
+ copyClipBars,
24
+ deleteClipBars,
25
+ insertClipBars,
26
+ sliceClips,
27
+ type ClipPiece,
28
+ } from "./clips.ts";
19
29
  import {
20
30
  SCORE_LIMITS,
21
31
  withNoteCap,
@@ -23,6 +33,7 @@ import {
23
33
  TrackScore,
24
34
  isSamplerInstrument,
25
35
  wavetableOf,
36
+ type AudioClip,
26
37
  type AutomationPoint,
27
38
  type FormEntry,
28
39
  type Note,
@@ -32,10 +43,10 @@ import {
32
43
  } from "./score.ts";
33
44
  import {
34
45
  barStartTick,
35
- bpmAtTick,
36
46
  hasTrackTime,
37
47
  normalizeSongTime,
38
48
  performedNotes,
49
+ timeMapFor,
39
50
  type Fermata,
40
51
  type SongTime,
41
52
  type TempoEvent,
@@ -61,6 +72,11 @@ export function sliceSongTime(
61
72
  };
62
73
  const clampBpm = (bpm: number) =>
63
74
  Math.min(SCORE_LIMITS.maxTempoBpm, Math.max(SCORE_LIMITS.minTempoBpm, bpm));
75
+ // The written tempo: a fermata's hold belongs to its own beat and moves
76
+ // with it, so it must not leak into the tempo restated at a seam.
77
+ const map = timeMapFor(score);
78
+ const bpmAtTick = (_score: TrackScore, tick: number) =>
79
+ map ? map.tempoAt(tick) : score.tempoBpm;
64
80
  const sorted = [...pieces].sort((a, b) => a.offset - b.offset);
65
81
  const first = sorted[0];
66
82
  const tempoBpm = clampBpm(
@@ -147,7 +163,10 @@ export function bakeTrackTime(
147
163
  () =>
148
164
  new TrackScore({
149
165
  ...score.toJSON(),
150
- tracks: score.tracks.map(({ time: _time, ...track }) => track),
166
+ tracks: score.tracks.map((track) => {
167
+ const { time: _time, ...rest } = bakeClipTime(track);
168
+ return rest;
169
+ }),
151
170
  notes,
152
171
  }),
153
172
  );
@@ -394,8 +413,73 @@ export function applySectionChanges(score: TrackScore): TrackScore {
394
413
  }
395
414
  notes.push(cut);
396
415
  }
416
+ // Clips (0.7): a section that mutes a track silences its clips there, its
417
+ // variation gain scales them; a clip crossing such an edge is cut.
418
+ const total = Math.max(
419
+ score.bars,
420
+ ...score.sections.map((s) => s.startBar + s.bars),
421
+ );
422
+ const clipTracks = score.tracks.map((track) => {
423
+ if (!track.clips) return track;
424
+ const pieces: ClipPiece[] = [];
425
+ for (let bar = 0; bar < total; bar += 1) {
426
+ const section = sectionAtBar(score, bar);
427
+ pieces.push({
428
+ from: bar * ticks,
429
+ to: (bar + 1) * ticks,
430
+ offset: bar * ticks,
431
+ ...clipPieceState(section, track.id),
432
+ });
433
+ }
434
+ // The last bar runs on, so a clip sounding past the end is not cut.
435
+ if (pieces.length > 0)
436
+ pieces[pieces.length - 1] = {
437
+ ...pieces[pieces.length - 1]!,
438
+ to: SCORE_LIMITS.maxTick + 1,
439
+ };
440
+ const next = withClips(track, sliceClips(track.clips, pieces, score));
441
+ if (JSON.stringify(next.clips) !== JSON.stringify(track.clips))
442
+ changed = true;
443
+ return next;
444
+ });
397
445
  if (!changed) return score;
398
- return new TrackScore({ ...score.toJSON(), notes, sections: [], form: [] });
446
+ return new TrackScore({
447
+ ...score.toJSON(),
448
+ tracks: clipTracks,
449
+ notes,
450
+ sections: [],
451
+ form: [],
452
+ });
453
+ }
454
+
455
+ /** A section's mute and variation gain for one track's clips. */
456
+ function clipPieceState(
457
+ section: Section | undefined,
458
+ trackId: string,
459
+ ): Pick<ClipPiece, "mute" | "gain"> {
460
+ if (!section) return {};
461
+ if (section.mute?.includes(trackId)) return { mute: true };
462
+ const gain = section.vary?.[trackId]?.gain;
463
+ return gain === undefined || gain === 1 ? {} : { gain };
464
+ }
465
+
466
+ /** `track` with `clips` (absent when empty). */
467
+ function withClips(
468
+ track: Track,
469
+ clips: readonly AudioClip[] | undefined,
470
+ ): Track {
471
+ const { clips: _clips, ...rest } = track;
472
+ return clips && clips.length > 0 ? { ...rest, clips } : rest;
473
+ }
474
+
475
+ /** A track's clips cut to `pieces` (sections, forms, windows). */
476
+ function sliceTrackClips(
477
+ track: Track,
478
+ pieces: readonly ClipPiece[],
479
+ time: TrackScore,
480
+ ): Track {
481
+ if (!track.clips) return track;
482
+ return withClips(track, sliceClips(track.clips, pieces, time));
399
483
  }
400
484
 
401
485
  /**
@@ -433,9 +517,13 @@ export function sectionScore(score: TrackScore, section: Section): TrackScore {
433
517
  time: timed.time ?? null,
434
518
  bars: section.bars,
435
519
  tracks: score.tracks.map((track) =>
436
- slicePedals(
437
- mapAutomation(track, (points) => cropPoints(points, from, to)),
438
- [{ from, to, offset: 0 }],
520
+ sliceTrackClips(
521
+ slicePedals(
522
+ mapAutomation(track, (points) => cropPoints(points, from, to)),
523
+ [{ from, to, offset: 0 }],
524
+ ),
525
+ [{ from, to, offset: 0, ...clipPieceState(section, track.id) }],
526
+ score,
439
527
  ),
440
528
  ),
441
529
  notes,
@@ -602,6 +690,77 @@ function checkBars(bars: number): void {
602
690
  );
603
691
  }
604
692
 
693
+ /**
694
+ * The song tempo map, fermatas and pedal lanes for a ripple edit that lays
695
+ * `pieces` of the score end to end: they move with the music they belong
696
+ * to, as notes and automation do.
697
+ */
698
+ function rippleTime(
699
+ score: TrackScore,
700
+ pieces: readonly { from: number; to: number; offset: number }[],
701
+ ): Readonly<{
702
+ tempoBpm: number;
703
+ time: SongTime | null;
704
+ pedals: (track: Track) => Track;
705
+ }> {
706
+ const kept = pieces.filter((piece) => piece.to > piece.from);
707
+ const timed = sliceSongTime(
708
+ score,
709
+ kept.map((piece) => ({
710
+ ...piece,
711
+ to: Math.min(piece.to, SCORE_LIMITS.maxTick + 1),
712
+ })),
713
+ );
714
+ return {
715
+ tempoBpm: timed.tempoBpm,
716
+ time: timed.time ?? null,
717
+ pedals: (track) => {
718
+ if (!track.pedal && !track.softPedal && !track.sostenuto) return track;
719
+ const out: Record<string, unknown> = { ...track };
720
+ for (const field of ["pedal", "softPedal", "sostenuto"] as const) {
721
+ const events = track[field];
722
+ if (!events) continue;
723
+ const next = ripplePedal(events, kept);
724
+ if (next) out[field] = next;
725
+ else delete out[field];
726
+ }
727
+ return out as Track;
728
+ },
729
+ };
730
+ }
731
+
732
+ /**
733
+ * A pedal lane for `pieces` laid end to end with no lift at the seams (unlike
734
+ * a render slice): each piece starts in the state its source was in, and a
735
+ * gap left between pieces (inserted bars) holds the state before it.
736
+ */
737
+ function ripplePedal(
738
+ events: readonly PedalEvent[],
739
+ pieces: readonly { from: number; to: number; offset: number }[],
740
+ ): readonly PedalEvent[] | undefined {
741
+ const byTick = new Map<number, PedalState>();
742
+ for (const { from, to, offset } of pieces) {
743
+ let state: PedalState = "up";
744
+ for (const event of events) {
745
+ if (event.tick > from) break;
746
+ state = event.state;
747
+ }
748
+ byTick.set(offset, state);
749
+ for (const event of events)
750
+ if (event.tick > from && event.tick < to)
751
+ byTick.set(event.tick - from + offset, event.state);
752
+ }
753
+ const out: PedalEvent[] = [];
754
+ let last: PedalState = "up";
755
+ for (const tick of [...byTick.keys()].sort((a, b) => a - b)) {
756
+ const state = byTick.get(tick)!;
757
+ if (state === last) continue;
758
+ out.push({ tick, state });
759
+ last = state;
760
+ }
761
+ return out.length > 0 ? out : undefined;
762
+ }
763
+
605
764
  /** Insert `count` empty bars at `atBar`, shifting later music and sections. */
606
765
  export function insertBars(
607
766
  score: TrackScore,
@@ -613,16 +772,32 @@ export function insertBars(
613
772
  const ticks = barTicks(score);
614
773
  const at = atBar * ticks;
615
774
  const shift = count * ticks;
775
+ const ripple = rippleTime(score, [
776
+ { from: 0, to: at, offset: 0 },
777
+ { from: at, to: Infinity, offset: at + shift },
778
+ ]);
616
779
  return new TrackScore({
617
780
  ...score.toJSON(),
781
+ tempoBpm: ripple.tempoBpm,
782
+ time: ripple.time,
618
783
  bars: score.bars + count,
619
784
  tracks: score.tracks.map((track) =>
620
- mapAutomation(track, (points) => insertPoints(points, at, shift)),
785
+ insertClipBars(
786
+ mapAutomation(ripple.pedals(track), (points) =>
787
+ insertPoints(points, at, shift),
788
+ ),
789
+ at,
790
+ shift,
791
+ ),
621
792
  ),
793
+ // A note's start and end stay with the music they sit in, so one held
794
+ // across the insertion point sounds on through the new bars.
622
795
  notes: score.notes.map((note) =>
623
796
  note.startTick >= at
624
797
  ? { ...note, startTick: note.startTick + shift }
625
- : note,
798
+ : note.startTick + note.durationTicks > at
799
+ ? { ...note, durationTicks: note.durationTicks + shift }
800
+ : note,
626
801
  ),
627
802
  sections: score.sections.map((section) =>
628
803
  section.startBar >= atBar
@@ -640,6 +815,12 @@ export function deleteBars(
640
815
  atBar: number,
641
816
  count: number,
642
817
  ): TrackScore {
818
+ if (!Number.isInteger(atBar) || atBar < 0 || !Number.isInteger(count))
819
+ throw new ScoreValidationError(
820
+ `bars to delete must start at a whole bar inside the song`,
821
+ );
822
+ // Only bars inside the song can go (a section may sit past its end).
823
+ count = Math.min(atBar + count, score.bars) - atBar;
643
824
  if (count <= 0) return score;
644
825
  if (count >= score.bars)
645
826
  throw new ScoreValidationError("a song keeps at least one bar");
@@ -647,16 +828,21 @@ export function deleteBars(
647
828
  const at = atBar * ticks;
648
829
  const end = (atBar + count) * ticks;
649
830
  const shift = count * ticks;
831
+ // Each note's start and end stay with the music they sit in; the part
832
+ // inside the deleted bars goes, so a note running out of them keeps its
833
+ // tail and one running into them is cut where they start.
834
+ const place = (tick: number) =>
835
+ tick < at ? tick : tick < end ? at : tick - shift;
650
836
  const notes: Note[] = [];
651
837
  for (const note of score.notes) {
652
- if (note.startTick >= end)
653
- notes.push({ ...note, startTick: note.startTick - shift });
654
- else if (note.startTick < at)
655
- notes.push(
656
- note.startTick + note.durationTicks > at
657
- ? { ...note, durationTicks: at - note.startTick }
658
- : note,
659
- );
838
+ const startTick = place(note.startTick);
839
+ const stop = place(note.startTick + note.durationTicks);
840
+ if (stop <= startTick) continue;
841
+ notes.push(
842
+ startTick === note.startTick && stop - startTick === note.durationTicks
843
+ ? note
844
+ : { ...note, startTick, durationTicks: stop - startTick },
845
+ );
660
846
  }
661
847
  const sections: Section[] = [];
662
848
  for (const section of score.sections) {
@@ -677,11 +863,23 @@ export function deleteBars(
677
863
  }
678
864
  }
679
865
  const names = new Set(sections.map((section) => foldName(section.name)));
866
+ const ripple = rippleTime(score, [
867
+ { from: 0, to: at, offset: 0 },
868
+ { from: end, to: Infinity, offset: at },
869
+ ]);
680
870
  return new TrackScore({
681
871
  ...score.toJSON(),
872
+ tempoBpm: ripple.tempoBpm,
873
+ time: ripple.time,
682
874
  bars: score.bars - count,
683
875
  tracks: score.tracks.map((track) =>
684
- mapAutomation(track, (points) => deletePoints(points, at, shift)),
876
+ deleteClipBars(
877
+ mapAutomation(ripple.pedals(track), (points) =>
878
+ deletePoints(points, at, shift),
879
+ ),
880
+ at,
881
+ end,
882
+ ),
685
883
  ),
686
884
  notes,
687
885
  sections,
@@ -865,6 +1063,15 @@ export function duplicateSection(
865
1063
  if (options.as !== undefined && findSection(score, options.as))
866
1064
  throw new ScoreValidationError(`a section named ${options.as} exists`);
867
1065
  const at = options.toBar ?? section.startBar + section.bars;
1066
+ if (
1067
+ !Number.isInteger(at) ||
1068
+ at < 0 ||
1069
+ at > score.bars ||
1070
+ (at > section.startBar && at < section.startBar + section.bars)
1071
+ )
1072
+ throw new ScoreValidationError(
1073
+ `a copy of ${section.name} goes at a bar 1..${score.bars + 1} outside it`,
1074
+ );
868
1075
  const opened = insertBars(score, at, section.bars);
869
1076
  const source =
870
1077
  section.startBar >= at ? section.startBar + section.bars : section.startBar;
@@ -908,10 +1115,26 @@ export function copyBars(
908
1115
  Math.min(note.durationTicks, from + length - note.startTick),
909
1116
  ),
910
1117
  }));
1118
+ // The copied bars bring their tempo, fermatas and pedal; the music after
1119
+ // them goes on as it was.
1120
+ const ripple = rippleTime(score, [
1121
+ { from: 0, to, offset: 0 },
1122
+ { from, to: from + length, offset: to },
1123
+ { from: to + length, to: Infinity, offset: to + length },
1124
+ ]);
911
1125
  return new TrackScore({
912
1126
  ...score.toJSON(),
1127
+ tempoBpm: ripple.tempoBpm,
1128
+ time: ripple.time,
913
1129
  tracks: score.tracks.map((track) =>
914
- mapAutomation(track, (points) => copyPoints(points, from, length, to)),
1130
+ copyClipBars(
1131
+ mapAutomation(ripple.pedals(track), (points) =>
1132
+ copyPoints(points, from, length, to),
1133
+ ),
1134
+ from,
1135
+ length,
1136
+ to,
1137
+ ),
915
1138
  ),
916
1139
  notes: [...kept, ...copies],
917
1140
  });
@@ -1235,17 +1458,38 @@ export function arrangedSlice(
1235
1458
  ): TrackScore {
1236
1459
  const ticks = barTicks(score);
1237
1460
  const pieces: { from: number; to: number; offset: number }[] = [];
1461
+ // Clips (0.7) are cut per section pass; a window's own edges inside a
1462
+ // pass (or where the form plays straight on) are seams, never cuts, so a
1463
+ // windowed render plays them as one pass does.
1464
+ const clipPieces: (ClipPiece & { section: Section })[] = [];
1238
1465
  const end = fromBar + bars;
1466
+ let previous: FormSegment | undefined;
1239
1467
  for (const segment of formSegments(score)) {
1240
1468
  const segEnd = segment.startBar + segment.bars;
1469
+ const before = previous;
1470
+ previous = segment;
1241
1471
  if (segEnd <= fromBar || segment.startBar >= end) continue;
1242
1472
  const start = Math.max(segment.startBar, fromBar);
1243
1473
  const stop = Math.min(segEnd, end);
1244
1474
  const from = (segment.section.startBar + start - segment.startBar) * ticks;
1245
- pieces.push({
1475
+ const piece = {
1246
1476
  from,
1247
1477
  to: from + (stop - start) * ticks,
1248
1478
  offset: (start - fromBar) * ticks,
1479
+ };
1480
+ pieces.push(piece);
1481
+ const straight =
1482
+ before !== undefined &&
1483
+ before.section.startBar + before.bars === segment.section.startBar;
1484
+ clipPieces.push({
1485
+ ...piece,
1486
+ section: segment.section,
1487
+ ...(stop < segEnd ? { to: from + (segEnd - start) * ticks } : {}),
1488
+ ...(clipPieces.length === 0 &&
1489
+ fromBar > 0 &&
1490
+ (start > segment.startBar || straight)
1491
+ ? { seam: true }
1492
+ : {}),
1249
1493
  });
1250
1494
  }
1251
1495
  const shift = fromBar * ticks;
@@ -1256,7 +1500,14 @@ export function arrangedSlice(
1256
1500
  time: timed.time ?? null,
1257
1501
  bars,
1258
1502
  tracks: score.tracks.map((track) =>
1259
- slicePedals(sliceLanes(track, pieces), pieces),
1503
+ sliceTrackClips(
1504
+ slicePedals(sliceLanes(track, pieces), pieces),
1505
+ clipPieces.map(({ section, ...piece }) => ({
1506
+ ...piece,
1507
+ ...clipPieceState(section, track.id),
1508
+ })),
1509
+ score,
1510
+ ),
1260
1511
  ),
1261
1512
  notes: arrangedNotes(score, fromBar, fromBar + noteBars).map((note) => ({
1262
1513
  ...note,
@@ -1341,7 +1592,11 @@ function laneFallback(track: Track, lane: string): number | undefined {
1341
1592
  ? track.modal && modalSettings(track.modal)
1342
1593
  : spec.effect === "wind"
1343
1594
  ? track.wind && windSettings(track.wind)
1344
- : (track.fx as Record<string, unknown> | undefined)?.[spec.effect]
1595
+ : spec.effect === "sing"
1596
+ ? track.sing && resolveSing(track.sing)
1597
+ : (track.fx as Record<string, unknown> | undefined)?.[
1598
+ spec.effect
1599
+ ]
1345
1600
  ) as Record<string, unknown> | undefined;
1346
1601
  const stored = source?.[spec.param];
1347
1602
  return typeof stored === "number" ? stored : spec.spec.default;
@@ -1364,6 +1619,8 @@ const TOM = DRUM_VOICES.find((voice) => voice.voice === "tom")!.pitch;
1364
1619
  const TOMS = [50, 47, TOM] as const;
1365
1620
  const KICK = DRUM_VOICES.find((voice) => voice.voice === "kick")!.pitch;
1366
1621
  const OPEN_HAT = DRUM_VOICES.find((voice) => voice.voice === "openhat")!.pitch;
1622
+ /** GM crash 1, which calibration 1+ renders as a cymbal. */
1623
+ const CRASH = 49;
1367
1624
 
1368
1625
  /** Generator tracks, reused by later builds and drops. */
1369
1626
  export const GENERATOR_TRACKS = Object.freeze({
@@ -1955,7 +2212,10 @@ export type FillOptions = Readonly<{
1955
2212
  /** Beats the fill lasts, default 1. */
1956
2213
  beats?: number;
1957
2214
  style?: FillStyle;
1958
- /** Crash (open hat with kick) on the next downbeat, default on. */
2215
+ /**
2216
+ * Crash with kick on the next downbeat, default on: GM 49 on a
2217
+ * calibrated song's synth kit, else an open hat.
2218
+ */
1959
2219
  crash?: boolean;
1960
2220
  }>;
1961
2221
 
@@ -2000,6 +2260,12 @@ export function generateFill(
2000
2260
  drums = GENERATOR_TRACKS.fills;
2001
2261
  }
2002
2262
  const style = options.style ?? "toms";
2263
+ // Sample kits and legacy songs have no cymbal voice: keep the open hat.
2264
+ const kit = next.tracks.find((track) => track.id === drums)?.kit;
2265
+ const crash =
2266
+ (next.calibration ?? 0) >= 1 && (kit === undefined || synthKit(kit))
2267
+ ? CRASH
2268
+ : OPEN_HAT;
2003
2269
  const clamped: string[] = [];
2004
2270
  const beats = clampNote(
2005
2271
  options.beats ?? 1,
@@ -2030,8 +2296,8 @@ export function generateFill(
2030
2296
  : SNARE
2031
2297
  : progress < 0.25
2032
2298
  ? SNARE
2033
- : // High, mid, low tom down the fill (GM 50/47/45); the
2034
- // built-in kit folds all three onto its tom.
2299
+ : // High, mid, low tom down the fill (GM 50/47/45); a
2300
+ // calibrated song pitches them, older songs fold them.
2035
2301
  TOMS[Math.min(2, Math.floor(((progress - 0.25) / 0.75) * 3))]!;
2036
2302
  fill.push({
2037
2303
  id: nextId(),
@@ -2043,7 +2309,7 @@ export function generateFill(
2043
2309
  });
2044
2310
  }
2045
2311
  if ((options.crash ?? true) && end < next.bars * ticks)
2046
- for (const pitch of [OPEN_HAT, KICK])
2312
+ for (const pitch of [crash, KICK])
2047
2313
  fill.push({
2048
2314
  id: nextId(),
2049
2315
  trackId: drums,