@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/diff.ts CHANGED
@@ -10,10 +10,6 @@
10
10
  * canonical order, so insertion order never matters).
11
11
  */
12
12
 
13
- import {
14
- NOTE_EXPRESSION_FIELDS,
15
- TRACK_PERFORMANCE_FIELDS,
16
- } from "./expression.ts";
17
13
  import {
18
14
  applyScoreOperation,
19
15
  TrackScore,
@@ -31,29 +27,109 @@ export class DiffError extends Error {
31
27
  }
32
28
  }
33
29
 
34
- const EFFECTS = [
35
- "filter",
36
- "delay",
37
- "reverb",
38
- "sampler",
39
- "fx",
40
- "fxAutomation",
41
- "synth",
42
- "wavetable",
43
- "string",
44
- "granular",
45
- "keys",
46
- "modal",
47
- ] as const;
48
- const LANES = [
49
- "volumeAutomation",
50
- "panAutomation",
51
- "filterAutomation",
52
- "resonanceAutomation",
53
- "delayFeedbackAutomation",
54
- "delayMixAutomation",
55
- "wtAutomation",
56
- ] as const;
30
+ /**
31
+ * How each field diffs: `same` is identity (never patched), `set` patches
32
+ * the new value, `clear` patches the new value or null, `zero` treats
33
+ * absent as 0, `flag` treats absent as false and `lane` treats absent as
34
+ * []. The records are keyed by every field of `Note` and `Track`, so a new
35
+ * field fails typechecking until it is classified here.
36
+ */
37
+ type FieldRule = "same" | "set" | "clear" | "zero" | "flag" | "lane";
38
+
39
+ const NOTE_FIELDS: { readonly [K in keyof Note]-?: FieldRule } = {
40
+ id: "same",
41
+ trackId: "same",
42
+ startTick: "set",
43
+ durationTicks: "set",
44
+ pitch: "set",
45
+ velocity: "set",
46
+ cents: "zero",
47
+ lyric: "clear",
48
+ vowel: "clear",
49
+ articulation: "clear",
50
+ glide: "clear",
51
+ bend: "clear",
52
+ vibrato: "clear",
53
+ humanize: "clear",
54
+ drift: "clear",
55
+ };
56
+
57
+ const TRACK_FIELDS: { readonly [K in keyof Track]-?: FieldRule } = {
58
+ id: "same",
59
+ name: "set",
60
+ instrument: "set",
61
+ muted: "set",
62
+ volume: "set",
63
+ pan: "set",
64
+ solo: "flag",
65
+ volumeAutomation: "lane",
66
+ panAutomation: "lane",
67
+ filterAutomation: "lane",
68
+ resonanceAutomation: "lane",
69
+ delayFeedbackAutomation: "lane",
70
+ delayMixAutomation: "lane",
71
+ wtAutomation: "lane",
72
+ filter: "clear",
73
+ delay: "clear",
74
+ reverb: "clear",
75
+ fx: "clear",
76
+ fxAutomation: "clear",
77
+ synth: "clear",
78
+ wavetable: "clear",
79
+ sampler: "clear",
80
+ rhythm: "clear",
81
+ kit: "clear",
82
+ time: "clear",
83
+ tuning: "clear",
84
+ string: "clear",
85
+ granular: "clear",
86
+ keys: "clear",
87
+ modal: "clear",
88
+ softPedal: "clear",
89
+ sostenuto: "clear",
90
+ guitar: "clear",
91
+ wind: "clear",
92
+ sing: "clear",
93
+ vocoder: "clear",
94
+ clips: "clear",
95
+ takes: "clear",
96
+ autotune: "clear",
97
+ glide: "clear",
98
+ pedal: "clear",
99
+ velocityCurve: "clear",
100
+ humanize: "clear",
101
+ };
102
+
103
+ /** Field names of `Note` and `Track`, for tests that check completeness. */
104
+ export const NOTE_FIELD_NAMES = Object.freeze(
105
+ Object.keys(NOTE_FIELDS),
106
+ ) as readonly (keyof Note)[];
107
+ export const TRACK_FIELD_NAMES = Object.freeze(
108
+ Object.keys(TRACK_FIELDS),
109
+ ) as readonly (keyof Track)[];
110
+
111
+ function fieldPatch(
112
+ rules: Readonly<Record<string, FieldRule>>,
113
+ a: Readonly<Record<string, unknown>>,
114
+ b: Readonly<Record<string, unknown>>,
115
+ ): Record<string, unknown> | undefined {
116
+ const patch: Record<string, unknown> = {};
117
+ for (const [key, rule] of Object.entries(rules)) {
118
+ const before = a[key];
119
+ const after = b[key];
120
+ if (rule === "same") continue;
121
+ if (rule === "set") {
122
+ if (!deepEqual(before, after)) patch[key] = after;
123
+ } else if (rule === "zero") {
124
+ if (!deepEqual(before ?? 0, after ?? 0)) patch[key] = after ?? 0;
125
+ } else if (rule === "flag") {
126
+ if ((before ?? false) !== (after ?? false)) patch[key] = after ?? false;
127
+ } else if (rule === "lane") {
128
+ if (!deepEqual(before ?? [], after ?? [])) patch[key] = after ?? [];
129
+ } else if (!deepEqual(before, after)) patch[key] = after ?? null;
130
+ }
131
+ return Object.keys(patch).length > 0 ? patch : undefined;
132
+ }
57
133
 
58
134
  /** Operations turning `a` into `b`; empty when they are equal. */
59
135
  export function diffScores(
@@ -64,31 +140,7 @@ export function diffScores(
64
140
  throw new DiffError(
65
141
  `ticksPerBeat differs (${a.ticksPerBeat} → ${b.ticksPerBeat}); the session resolution is fixed`,
66
142
  );
67
- const ops: ScoreOperation[] = [];
68
- if (a.beatsPerBar !== b.beatsPerBar)
69
- ops.push({ type: "setMeter", beatsPerBar: b.beatsPerBar });
70
- if (a.bars !== b.bars) ops.push({ type: "setBars", bars: b.bars });
71
- if (a.tempoBpm !== b.tempoBpm)
72
- ops.push({ type: "setTempo", tempoBpm: b.tempoBpm });
73
- if (a.key !== b.key) ops.push({ type: "setKey", key: b.key });
74
- if (!deepEqual(a.time, b.time))
75
- ops.push({ type: "setTime", time: b.time ?? null });
76
- if (!deepEqual(a.tuning, b.tuning))
77
- ops.push({ type: "setTuning", tuning: b.tuning ?? null });
78
- if (!deepEqual(a.master, b.master))
79
- ops.push({ type: "setMaster", master: b.master ?? null });
80
- if (
81
- !deepEqual(a.sections, b.sections) ||
82
- !deepEqual(a.form, b.form) ||
83
- a.loopSection !== b.loopSection
84
- )
85
- ops.push({
86
- type: "setSections",
87
- sections: b.sections,
88
- form: b.form,
89
- ...(b.loopSection === undefined ? {} : { loopSection: b.loopSection }),
90
- });
91
-
143
+ const ops: ScoreOperation[] = [...songOperations(a, b)];
92
144
  const aTracks = new Map(a.tracks.map((track) => [track.id, track]));
93
145
  const bTracks = new Map(b.tracks.map((track) => [track.id, track]));
94
146
  const aNotes = new Map(a.notes.map((note) => [note.id, note]));
@@ -96,32 +148,26 @@ export function diffScores(
96
148
 
97
149
  const updates: ScoreOperation[] = [];
98
150
  const additions: ScoreOperation[] = [];
151
+ // Notes that leave `a`: dropped with their track, or removed one by one
152
+ // (a note may name a track the score lacks, so check `a` too).
153
+ const gone = new Set<string>();
99
154
  for (const note of a.notes) {
100
- if (!bTracks.has(note.trackId)) continue; // removed with its track
155
+ if (aTracks.has(note.trackId) && !bTracks.has(note.trackId)) {
156
+ gone.add(note.id); // removed with its track
157
+ continue;
158
+ }
101
159
  const next = bNotes.get(note.id);
102
160
  if (!next || next.trackId !== note.trackId) {
103
161
  ops.push({ type: "removeNote", noteId: note.id });
162
+ gone.add(note.id);
104
163
  continue;
105
164
  }
106
165
  const patch = notePatch(note, next);
107
166
  if (patch) updates.push({ type: "updateNote", noteId: note.id, patch });
108
167
  }
109
- for (const note of b.notes) {
110
- const previous = aNotes.get(note.id);
111
- if (
112
- previous &&
113
- previous.trackId === note.trackId &&
114
- bTracks.has(note.trackId)
115
- )
116
- continue;
117
- if (
118
- previous &&
119
- previous.trackId === note.trackId &&
120
- !aTracks.has(note.trackId)
121
- )
122
- continue;
123
- additions.push({ type: "addNote", note });
124
- }
168
+ for (const note of b.notes)
169
+ if (!aNotes.has(note.id) || gone.has(note.id))
170
+ additions.push({ type: "addNote", note });
125
171
 
126
172
  for (const track of a.tracks)
127
173
  if (!bTracks.has(track.id))
@@ -151,6 +197,69 @@ export function diffScores(
151
197
  return Object.freeze(ops);
152
198
  }
153
199
 
200
+ /**
201
+ * Song settings, ordered so every step is valid on its own: constraints
202
+ * that go away are dropped first (sections before meter changes, fermatas
203
+ * before the tempo or meter that time them) and constraints that arrive
204
+ * are added last. Steps are checked on a copy without tracks or notes,
205
+ * because song settings never depend on them.
206
+ */
207
+ function songOperations(a: TrackScore, b: TrackScore): ScoreOperation[] {
208
+ const ops: ScoreOperation[] = [];
209
+ let song = new TrackScore({ ...a.toJSON(), tracks: [], notes: [] });
210
+ const emit = (operation: ScoreOperation): void => {
211
+ song = applyScoreOperation(song, operation);
212
+ ops.push(operation);
213
+ };
214
+ const sectionsDiffer = (): boolean =>
215
+ !deepEqual(song.sections, b.sections) ||
216
+ !deepEqual(song.form, b.form) ||
217
+ song.loopSection !== b.loopSection;
218
+ const setSections = (): void =>
219
+ emit({
220
+ type: "setSections",
221
+ sections: b.sections,
222
+ form: b.form,
223
+ ...(b.loopSection === undefined ? {} : { loopSection: b.loopSection }),
224
+ });
225
+ if (b.sections.length === 0 && sectionsDiffer()) setSections();
226
+ const retimed =
227
+ song.beatsPerBar !== b.beatsPerBar ||
228
+ song.bars !== b.bars ||
229
+ song.tempoBpm !== b.tempoBpm;
230
+ if (retimed && song.time?.fermatas && !deepEqual(song.time, b.time)) {
231
+ const { fermatas: _fermatas, ...rest } = b.time ?? {};
232
+ const interim = Object.keys(rest).length > 0 ? rest : null;
233
+ if (!deepEqual(song.time, interim ?? undefined))
234
+ emit({ type: "setTime", time: interim });
235
+ } else if (retimed && song.time?.fermatas) {
236
+ // Same fermatas before and after, but timed by a new tempo or meter.
237
+ const { fermatas: _fermatas, ...rest } = song.time;
238
+ emit({
239
+ type: "setTime",
240
+ time: Object.keys(rest).length > 0 ? rest : null,
241
+ });
242
+ }
243
+ if (song.beatsPerBar !== b.beatsPerBar)
244
+ emit({ type: "setMeter", beatsPerBar: b.beatsPerBar });
245
+ if (song.bars !== b.bars) emit({ type: "setBars", bars: b.bars });
246
+ if (song.tempoBpm !== b.tempoBpm)
247
+ emit({ type: "setTempo", tempoBpm: b.tempoBpm });
248
+ if (song.key !== b.key) emit({ type: "setKey", key: b.key });
249
+ if (!deepEqual(song.time, b.time))
250
+ emit({ type: "setTime", time: b.time ?? null });
251
+ if (!deepEqual(song.tuning, b.tuning))
252
+ emit({ type: "setTuning", tuning: b.tuning ?? null });
253
+ if (!deepEqual(song.master, b.master))
254
+ emit({ type: "setMaster", master: b.master ?? null });
255
+ if ((song.calibration ?? 0) !== (b.calibration ?? 0))
256
+ emit({ type: "setCalibration", calibration: b.calibration ?? null });
257
+ if (!deepEqual(song.style, b.style))
258
+ emit({ type: "setStyle", style: b.style ?? null });
259
+ if (sectionsDiffer()) setSections();
260
+ return ops;
261
+ }
262
+
154
263
  /** Applies operations in order; the inverse of `diffScores`. */
155
264
  export function applyScoreOperations(
156
265
  score: TrackScore,
@@ -160,43 +269,17 @@ export function applyScoreOperations(
160
269
  }
161
270
 
162
271
  function notePatch(a: Note, b: Note): NotePatch | undefined {
163
- const patch: Record<string, unknown> = {};
164
- for (const key of [
165
- "startTick",
166
- "durationTicks",
167
- "pitch",
168
- "velocity",
169
- ] as const)
170
- if (a[key] !== b[key]) patch[key] = b[key];
171
- for (const key of NOTE_EXPRESSION_FIELDS)
172
- if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
173
- if ((a.cents ?? 0) !== (b.cents ?? 0)) patch.cents = b.cents ?? 0;
174
- return Object.keys(patch).length > 0 ? (patch as NotePatch) : undefined;
272
+ return fieldPatch(NOTE_FIELDS, a, b) as NotePatch | undefined;
175
273
  }
176
274
 
177
275
  function trackPatch(a: Track, b: Track): TrackPatch | undefined {
178
- const patch: Record<string, unknown> = {};
179
- for (const key of ["name", "instrument", "muted", "volume", "pan"] as const)
180
- if (a[key] !== b[key]) patch[key] = b[key];
181
- if ((a.solo ?? false) !== (b.solo ?? false)) patch.solo = b.solo ?? false;
182
- for (const key of EFFECTS)
183
- if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
184
- for (const key of [
185
- "rhythm",
186
- "kit",
187
- "time",
188
- "tuning",
189
- ...TRACK_PERFORMANCE_FIELDS,
190
- ] as const)
191
- if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
192
- for (const key of LANES)
193
- if (!deepEqual(a[key] ?? [], b[key] ?? [])) patch[key] = b[key] ?? [];
194
- return Object.keys(patch).length > 0 ? (patch as TrackPatch) : undefined;
276
+ return fieldPatch(TRACK_FIELDS, a, b) as TrackPatch | undefined;
195
277
  }
196
278
 
197
279
  /** Structural equality for plain JSON-like data. */
198
280
  export function deepEqual(a: unknown, b: unknown): boolean {
199
- if (Object.is(a, b)) return true;
281
+ // === so -0 equals 0: a printed song.ts reads -0 back as 0.
282
+ if (a === b) return true;
200
283
  if (
201
284
  typeof a !== "object" ||
202
285
  typeof b !== "object" ||
@@ -125,6 +125,11 @@ export type NoteExpression = Readonly<{
125
125
  * track's humanize seed, 1 when it has none). `{}` keeps the note exact.
126
126
  */
127
127
  humanize?: NoteHumanize;
128
+ /**
129
+ * Optional (0.7): on an autotune guide note, the share of slow pitch drift
130
+ * removed (0..1), overriding the track's `autotune.drift` for this note.
131
+ */
132
+ drift?: number;
128
133
  }>;
129
134
 
130
135
  /** Per-note humanize: amounts as in `Humanize`, absent ones 0. */
@@ -141,6 +146,7 @@ export type NoteExpressionPatch = Readonly<{
141
146
  bend?: readonly BendPoint[] | null;
142
147
  vibrato?: NoteVibrato | null;
143
148
  humanize?: NoteHumanize | null;
149
+ drift?: number | null;
144
150
  }>;
145
151
 
146
152
  export const NOTE_EXPRESSION_FIELDS = [
@@ -149,6 +155,7 @@ export const NOTE_EXPRESSION_FIELDS = [
149
155
  "bend",
150
156
  "vibrato",
151
157
  "humanize",
158
+ "drift",
152
159
  ] as const;
153
160
 
154
161
  export const GLIDE_MODES = ["legato", "mono", "poly"] as const;
@@ -167,6 +174,8 @@ export type TrackGlide = Readonly<{ time: number; mode: GlideMode }>;
167
174
  export const DEFAULT_GLIDE_SECONDS = 0.06;
168
175
 
169
176
  export const PEDAL_STATES = ["down", "half", "up"] as const;
177
+ /** Sostenuto has no half position (0.6.1). */
178
+ export const SOSTENUTO_STATES = ["down", "up"] as const;
170
179
  export type PedalState = (typeof PEDAL_STATES)[number];
171
180
 
172
181
  /** A sustain pedal (MIDI CC64) change at a tick. */
@@ -323,6 +332,12 @@ export function normalizeVibrato(value: unknown): NoteVibrato | undefined {
323
332
  return Object.freeze({ rate, depth, ...(delay > 0 ? { delay } : {}) });
324
333
  }
325
334
 
335
+ /** A guide note's autotune drift share (0.7), 0..1. */
336
+ export function normalizeNoteDrift(value: unknown): number | undefined {
337
+ if (value === undefined || value === null) return undefined;
338
+ return finite(value, "note drift", 0, 1);
339
+ }
340
+
326
341
  /** Validates a note's expression fields; only the set ones are returned. */
327
342
  export function normalizeNoteExpression(
328
343
  input: Readonly<Record<string, unknown>>,
@@ -332,12 +347,14 @@ export function normalizeNoteExpression(
332
347
  const bend = normalizeBend(input.bend);
333
348
  const vibrato = normalizeVibrato(input.vibrato);
334
349
  const humanize = normalizeNoteHumanize(input.humanize);
350
+ const drift = normalizeNoteDrift(input.drift);
335
351
  return {
336
352
  ...(articulation ? { articulation } : {}),
337
353
  ...(glide !== undefined ? { glide } : {}),
338
354
  ...(bend ? { bend } : {}),
339
355
  ...(vibrato ? { vibrato } : {}),
340
356
  ...(humanize ? { humanize } : {}),
357
+ ...(drift !== undefined ? { drift } : {}),
341
358
  };
342
359
  }
343
360
 
@@ -386,16 +403,18 @@ export function normalizeTrackGlide(value: unknown): TrackGlide | undefined {
386
403
  export function normalizePedal(
387
404
  value: unknown,
388
405
  maxTick: number,
406
+ label = "pedal",
407
+ states: readonly PedalState[] = PEDAL_STATES,
389
408
  ): readonly PedalEvent[] | undefined {
390
409
  if (value === undefined || value === null) return undefined;
391
410
  if (!Array.isArray(value) || value.length > EXPRESSION_LIMITS.maxPedalEvents)
392
411
  throw new ExpressionValidationError(
393
- `pedal must be an array of at most ${EXPRESSION_LIMITS.maxPedalEvents} events`,
412
+ `${label} must be an array of at most ${EXPRESSION_LIMITS.maxPedalEvents} events`,
394
413
  );
395
414
  const byTick = new Map<number, PedalEvent>();
396
415
  for (const item of value as unknown[]) {
397
- const event = record(item, "pedal event");
398
- onlyKeys(event, ["tick", "state"], "pedal event");
416
+ const event = record(item, `${label} event`);
417
+ onlyKeys(event, ["tick", "state"], `${label} event`);
399
418
  const tick = event.tick;
400
419
  if (
401
420
  typeof tick !== "number" ||
@@ -404,11 +423,11 @@ export function normalizePedal(
404
423
  tick > maxTick
405
424
  )
406
425
  throw new ExpressionValidationError(
407
- `pedal tick must be an integer 0..${maxTick}`,
426
+ `${label} tick must be an integer 0..${maxTick}`,
408
427
  );
409
- if (!PEDAL_STATES.includes(event.state as PedalState))
428
+ if (!states.includes(event.state as PedalState))
410
429
  throw new ExpressionValidationError(
411
- `pedal state must be one of ${PEDAL_STATES.join(", ")}`,
430
+ `${label} state must be one of ${states.join(", ")}`,
412
431
  );
413
432
  // The last event at a tick wins.
414
433
  byTick.set(tick, Object.freeze({ tick, state: event.state as PedalState }));
@@ -551,6 +570,80 @@ export function pedalStateAt(
551
570
  return state;
552
571
  }
553
572
 
573
+ /** A stretch of song ticks `[from, to)` placed at `offset` in a slice. */
574
+ export type PedalPiece = Readonly<{ from: number; to: number; offset: number }>;
575
+
576
+ /**
577
+ * Pedal events for a slice made of `pieces` (a render window, a section
578
+ * pass, an audition region): the state in force at each piece's start is
579
+ * restated at its offset (`carry`, for sustain and una corda; sostenuto
580
+ * passes false, since a pedal already down latches no key struck later),
581
+ * events inside the piece move by `offset - from`, and a pedal still down
582
+ * at the piece's end is lifted there. Undefined when nothing is left.
583
+ */
584
+ export function slicePedal(
585
+ events: readonly PedalEvent[] | undefined,
586
+ pieces: readonly PedalPiece[],
587
+ carry = true,
588
+ ): readonly PedalEvent[] | undefined {
589
+ if (!events || events.length === 0) return events;
590
+ const byTick = new Map<number, PedalEvent>();
591
+ const put = (tick: number, state: PedalState) =>
592
+ byTick.set(tick, Object.freeze({ tick, state }));
593
+ for (const { from, to, offset } of pieces) {
594
+ let state: PedalState = "up";
595
+ if (carry)
596
+ for (const event of events) {
597
+ if (event.tick >= from) break;
598
+ state = event.state;
599
+ }
600
+ // Restate up too: the previous piece may have ended with it down.
601
+ if (state !== "up" || byTick.size > 0) put(offset, state);
602
+ for (const event of events) {
603
+ if (event.tick < from || event.tick >= to) continue;
604
+ put(event.tick - from + offset, event.state);
605
+ state = event.state;
606
+ }
607
+ if (state !== "up" && Number.isFinite(to)) put(offset + to - from, "up");
608
+ }
609
+ const out = [...byTick.values()].sort((a, b) => a.tick - b.tick);
610
+ // Drop events that restate the state already in force.
611
+ const kept: PedalEvent[] = [];
612
+ let last: PedalState = "up";
613
+ for (const event of out)
614
+ if (event.state !== last) {
615
+ kept.push(event);
616
+ last = event.state;
617
+ }
618
+ return kept.length > 0 ? Object.freeze(kept) : undefined;
619
+ }
620
+
621
+ /** The track with its three pedal lanes cut to `pieces` (see slicePedal). */
622
+ export function slicePedals<T extends PedalTrack>(
623
+ track: T,
624
+ pieces: readonly PedalPiece[],
625
+ ): T {
626
+ if (!track.pedal && !track.softPedal && !track.sostenuto) return track;
627
+ const out: Record<string, unknown> = { ...track };
628
+ for (const [field, carry] of [
629
+ ["pedal", true],
630
+ ["softPedal", true],
631
+ ["sostenuto", false],
632
+ ] as const) {
633
+ if (!track[field]) continue;
634
+ const next = slicePedal(track[field], pieces, carry);
635
+ if (next) out[field] = next;
636
+ else delete out[field];
637
+ }
638
+ return out as T;
639
+ }
640
+
641
+ type PedalTrack = Readonly<{
642
+ pedal?: readonly PedalEvent[];
643
+ softPedal?: readonly PedalEvent[];
644
+ sostenuto?: readonly PedalEvent[];
645
+ }>;
646
+
554
647
  // ---------------------------------------------------------------------------
555
648
  // Performance
556
649
 
@@ -674,6 +767,7 @@ export function hasTrackPerformance(track: Track | undefined): boolean {
674
767
  track !== undefined &&
675
768
  (track.glide !== undefined ||
676
769
  track.pedal !== undefined ||
770
+ track.sostenuto !== undefined ||
677
771
  track.velocityCurve !== undefined ||
678
772
  track.humanize !== undefined)
679
773
  );
@@ -869,6 +963,64 @@ export function performNotes(
869
963
  };
870
964
  });
871
965
  }
966
+ // 3b. Sostenuto (0.6.1): a key held (written length after articulation)
967
+ // when the sostenuto goes down keeps its damper up until the sostenuto
968
+ // lifts; keys struck later damp at their own release (or the sustain
969
+ // pedal's). The rod keeps a caught damper raised, so the same pitch
970
+ // struck again while that sostenuto is down rings to the lift too; the
971
+ // earlier instance stops at the restrike so voices do not pile up. A key
972
+ // still held through a lift and a new press is caught again.
973
+ const sostenuto = track?.sostenuto;
974
+ if (sostenuto) {
975
+ const spans: { down: number; lift: number; pitches: Set<number> }[] = [];
976
+ let open: number | undefined;
977
+ for (const event of sostenuto) {
978
+ if (event.state === "down" && open === undefined) open = event.tick;
979
+ else if (event.state === "up" && open !== undefined) {
980
+ spans.push({ down: open, lift: event.tick, pitches: new Set() });
981
+ open = undefined;
982
+ }
983
+ }
984
+ if (open !== undefined)
985
+ spans.push({ down: open, lift: timing.endTick, pitches: new Set() });
986
+ const caughtBy = working.map((item) => {
987
+ const keyUp = item.start + item.bendLength;
988
+ return spans.filter((span) => {
989
+ const held = item.start <= span.down && span.down < keyUp;
990
+ if (held) span.pitches.add(item.note.pitch);
991
+ return held;
992
+ });
993
+ });
994
+ working = working.map((item, index) => {
995
+ const keyUp = item.start + item.bendLength;
996
+ let lift = -Infinity;
997
+ for (const span of caughtBy[index]!) lift = Math.max(lift, span.lift);
998
+ for (const span of spans)
999
+ if (
1000
+ span.pitches.has(item.note.pitch) &&
1001
+ span.down < item.start &&
1002
+ item.start < span.lift
1003
+ )
1004
+ lift = Math.max(lift, span.lift);
1005
+ if (lift === -Infinity) return item;
1006
+ const current = item.start + item.duration;
1007
+ let end = Math.max(current, Math.min(lift, timing.endTick));
1008
+ if (end <= current) return item;
1009
+ const restrike = working.find(
1010
+ (other) =>
1011
+ other !== item &&
1012
+ other.note.pitch === item.note.pitch &&
1013
+ other.start > item.start &&
1014
+ other.start < end,
1015
+ );
1016
+ if (restrike) end = Math.max(current, restrike.start);
1017
+ return {
1018
+ ...item,
1019
+ duration: end - item.start,
1020
+ pedalled: end > keyUp || item.pedalled === true,
1021
+ };
1022
+ });
1023
+ }
872
1024
  // 4. Glide and monophony (on the written timing).
873
1025
  let performed = glideAndMono(
874
1026
  track,