@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
package/core/sections.ts CHANGED
@@ -13,8 +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
- import { modalSettings } from "./resonators.ts";
18
+ import { modalSettings, windSettings } from "./resonators.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";
18
29
  import {
19
30
  SCORE_LIMITS,
20
31
  withNoteCap,
@@ -22,6 +33,7 @@ import {
22
33
  TrackScore,
23
34
  isSamplerInstrument,
24
35
  wavetableOf,
36
+ type AudioClip,
25
37
  type AutomationPoint,
26
38
  type FormEntry,
27
39
  type Note,
@@ -31,10 +43,10 @@ import {
31
43
  } from "./score.ts";
32
44
  import {
33
45
  barStartTick,
34
- bpmAtTick,
35
46
  hasTrackTime,
36
47
  normalizeSongTime,
37
48
  performedNotes,
49
+ timeMapFor,
38
50
  type Fermata,
39
51
  type SongTime,
40
52
  type TempoEvent,
@@ -60,6 +72,11 @@ export function sliceSongTime(
60
72
  };
61
73
  const clampBpm = (bpm: number) =>
62
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;
63
80
  const sorted = [...pieces].sort((a, b) => a.offset - b.offset);
64
81
  const first = sorted[0];
65
82
  const tempoBpm = clampBpm(
@@ -146,7 +163,10 @@ export function bakeTrackTime(
146
163
  () =>
147
164
  new TrackScore({
148
165
  ...score.toJSON(),
149
- 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
+ }),
150
170
  notes,
151
171
  }),
152
172
  );
@@ -393,8 +413,73 @@ export function applySectionChanges(score: TrackScore): TrackScore {
393
413
  }
394
414
  notes.push(cut);
395
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
+ });
396
445
  if (!changed) return score;
397
- 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));
398
483
  }
399
484
 
400
485
  /**
@@ -432,7 +517,14 @@ export function sectionScore(score: TrackScore, section: Section): TrackScore {
432
517
  time: timed.time ?? null,
433
518
  bars: section.bars,
434
519
  tracks: score.tracks.map((track) =>
435
- mapAutomation(track, (points) => cropPoints(points, from, to)),
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,
527
+ ),
436
528
  ),
437
529
  notes,
438
530
  sections: [],
@@ -598,6 +690,77 @@ function checkBars(bars: number): void {
598
690
  );
599
691
  }
600
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
+
601
764
  /** Insert `count` empty bars at `atBar`, shifting later music and sections. */
602
765
  export function insertBars(
603
766
  score: TrackScore,
@@ -609,16 +772,32 @@ export function insertBars(
609
772
  const ticks = barTicks(score);
610
773
  const at = atBar * ticks;
611
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
+ ]);
612
779
  return new TrackScore({
613
780
  ...score.toJSON(),
781
+ tempoBpm: ripple.tempoBpm,
782
+ time: ripple.time,
614
783
  bars: score.bars + count,
615
784
  tracks: score.tracks.map((track) =>
616
- 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
+ ),
617
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.
618
795
  notes: score.notes.map((note) =>
619
796
  note.startTick >= at
620
797
  ? { ...note, startTick: note.startTick + shift }
621
- : note,
798
+ : note.startTick + note.durationTicks > at
799
+ ? { ...note, durationTicks: note.durationTicks + shift }
800
+ : note,
622
801
  ),
623
802
  sections: score.sections.map((section) =>
624
803
  section.startBar >= atBar
@@ -636,6 +815,12 @@ export function deleteBars(
636
815
  atBar: number,
637
816
  count: number,
638
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;
639
824
  if (count <= 0) return score;
640
825
  if (count >= score.bars)
641
826
  throw new ScoreValidationError("a song keeps at least one bar");
@@ -643,16 +828,21 @@ export function deleteBars(
643
828
  const at = atBar * ticks;
644
829
  const end = (atBar + count) * ticks;
645
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;
646
836
  const notes: Note[] = [];
647
837
  for (const note of score.notes) {
648
- if (note.startTick >= end)
649
- notes.push({ ...note, startTick: note.startTick - shift });
650
- else if (note.startTick < at)
651
- notes.push(
652
- note.startTick + note.durationTicks > at
653
- ? { ...note, durationTicks: at - note.startTick }
654
- : note,
655
- );
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
+ );
656
846
  }
657
847
  const sections: Section[] = [];
658
848
  for (const section of score.sections) {
@@ -673,11 +863,23 @@ export function deleteBars(
673
863
  }
674
864
  }
675
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
+ ]);
676
870
  return new TrackScore({
677
871
  ...score.toJSON(),
872
+ tempoBpm: ripple.tempoBpm,
873
+ time: ripple.time,
678
874
  bars: score.bars - count,
679
875
  tracks: score.tracks.map((track) =>
680
- 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
+ ),
681
883
  ),
682
884
  notes,
683
885
  sections,
@@ -861,6 +1063,15 @@ export function duplicateSection(
861
1063
  if (options.as !== undefined && findSection(score, options.as))
862
1064
  throw new ScoreValidationError(`a section named ${options.as} exists`);
863
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
+ );
864
1075
  const opened = insertBars(score, at, section.bars);
865
1076
  const source =
866
1077
  section.startBar >= at ? section.startBar + section.bars : section.startBar;
@@ -904,10 +1115,26 @@ export function copyBars(
904
1115
  Math.min(note.durationTicks, from + length - note.startTick),
905
1116
  ),
906
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
+ ]);
907
1125
  return new TrackScore({
908
1126
  ...score.toJSON(),
1127
+ tempoBpm: ripple.tempoBpm,
1128
+ time: ripple.time,
909
1129
  tracks: score.tracks.map((track) =>
910
- 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
+ ),
911
1138
  ),
912
1139
  notes: [...kept, ...copies],
913
1140
  });
@@ -1231,17 +1458,38 @@ export function arrangedSlice(
1231
1458
  ): TrackScore {
1232
1459
  const ticks = barTicks(score);
1233
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 })[] = [];
1234
1465
  const end = fromBar + bars;
1466
+ let previous: FormSegment | undefined;
1235
1467
  for (const segment of formSegments(score)) {
1236
1468
  const segEnd = segment.startBar + segment.bars;
1469
+ const before = previous;
1470
+ previous = segment;
1237
1471
  if (segEnd <= fromBar || segment.startBar >= end) continue;
1238
1472
  const start = Math.max(segment.startBar, fromBar);
1239
1473
  const stop = Math.min(segEnd, end);
1240
1474
  const from = (segment.section.startBar + start - segment.startBar) * ticks;
1241
- pieces.push({
1475
+ const piece = {
1242
1476
  from,
1243
1477
  to: from + (stop - start) * ticks,
1244
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
+ : {}),
1245
1493
  });
1246
1494
  }
1247
1495
  const shift = fromBar * ticks;
@@ -1251,7 +1499,16 @@ export function arrangedSlice(
1251
1499
  tempoBpm: timed.tempoBpm,
1252
1500
  time: timed.time ?? null,
1253
1501
  bars,
1254
- tracks: score.tracks.map((track) => sliceLanes(track, pieces)),
1502
+ tracks: score.tracks.map((track) =>
1503
+ sliceTrackClips(
1504
+ slicePedals(sliceLanes(track, pieces), pieces),
1505
+ clipPieces.map(({ section, ...piece }) => ({
1506
+ ...piece,
1507
+ ...clipPieceState(section, track.id),
1508
+ })),
1509
+ score,
1510
+ ),
1511
+ ),
1255
1512
  notes: arrangedNotes(score, fromBar, fromBar + noteBars).map((note) => ({
1256
1513
  ...note,
1257
1514
  startTick: note.startTick - shift,
@@ -1333,7 +1590,13 @@ function laneFallback(track: Track, lane: string): number | undefined {
1333
1590
  ? track.keys
1334
1591
  : spec.effect === "modal"
1335
1592
  ? track.modal && modalSettings(track.modal)
1336
- : (track.fx as Record<string, unknown> | undefined)?.[spec.effect]
1593
+ : spec.effect === "wind"
1594
+ ? track.wind && windSettings(track.wind)
1595
+ : spec.effect === "sing"
1596
+ ? track.sing && resolveSing(track.sing)
1597
+ : (track.fx as Record<string, unknown> | undefined)?.[
1598
+ spec.effect
1599
+ ]
1337
1600
  ) as Record<string, unknown> | undefined;
1338
1601
  const stored = source?.[spec.param];
1339
1602
  return typeof stored === "number" ? stored : spec.spec.default;
@@ -1356,6 +1619,8 @@ const TOM = DRUM_VOICES.find((voice) => voice.voice === "tom")!.pitch;
1356
1619
  const TOMS = [50, 47, TOM] as const;
1357
1620
  const KICK = DRUM_VOICES.find((voice) => voice.voice === "kick")!.pitch;
1358
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;
1359
1624
 
1360
1625
  /** Generator tracks, reused by later builds and drops. */
1361
1626
  export const GENERATOR_TRACKS = Object.freeze({
@@ -1947,7 +2212,10 @@ export type FillOptions = Readonly<{
1947
2212
  /** Beats the fill lasts, default 1. */
1948
2213
  beats?: number;
1949
2214
  style?: FillStyle;
1950
- /** 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
+ */
1951
2219
  crash?: boolean;
1952
2220
  }>;
1953
2221
 
@@ -1992,6 +2260,12 @@ export function generateFill(
1992
2260
  drums = GENERATOR_TRACKS.fills;
1993
2261
  }
1994
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;
1995
2269
  const clamped: string[] = [];
1996
2270
  const beats = clampNote(
1997
2271
  options.beats ?? 1,
@@ -2022,8 +2296,8 @@ export function generateFill(
2022
2296
  : SNARE
2023
2297
  : progress < 0.25
2024
2298
  ? SNARE
2025
- : // High, mid, low tom down the fill (GM 50/47/45); the
2026
- // 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.
2027
2301
  TOMS[Math.min(2, Math.floor(((progress - 0.25) / 0.75) * 3))]!;
2028
2302
  fill.push({
2029
2303
  id: nextId(),
@@ -2035,7 +2309,7 @@ export function generateFill(
2035
2309
  });
2036
2310
  }
2037
2311
  if ((options.crash ?? true) && end < next.bars * ticks)
2038
- for (const pitch of [OPEN_HAT, KICK])
2312
+ for (const pitch of [crash, KICK])
2039
2313
  fill.push({
2040
2314
  id: nextId(),
2041
2315
  trackId: drums,