@hraness/dawg 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/DAWG.md +454 -140
  3. package/README.md +30 -26
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +271 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +184 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/ids.ts +52 -0
  11. package/core/instruments.ts +19 -0
  12. package/core/keys.ts +3 -3
  13. package/core/loop.ts +8 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/range.ts +581 -0
  17. package/core/resonators.ts +16 -2
  18. package/core/routing.ts +165 -0
  19. package/core/score.ts +955 -14
  20. package/core/sdk/eval-child.ts +7 -2
  21. package/core/sdk/eval.ts +35 -6
  22. package/core/sdk/print.ts +287 -5
  23. package/core/sdk/sync-lyrics.ts +49 -0
  24. package/core/sdk/v1.ts +2132 -46
  25. package/core/sections.ts +480 -35
  26. package/core/sing.ts +815 -0
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/guides/agent.md +29 -0
  50. package/guides/arrange.md +31 -0
  51. package/guides/audio.md +31 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -7
  54. package/guides/chords.md +15 -15
  55. package/guides/effects.md +17 -16
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +15 -7
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/panes.md +32 -0
  65. package/guides/performance.md +15 -13
  66. package/guides/play.md +21 -13
  67. package/guides/project.md +26 -8
  68. package/guides/providers.md +20 -14
  69. package/guides/resample.md +15 -9
  70. package/guides/rhythm.md +18 -13
  71. package/guides/sessions.md +17 -7
  72. package/guides/show-me.md +31 -0
  73. package/guides/sound.md +25 -9
  74. package/guides/sounds.md +15 -13
  75. package/guides/styles.md +31 -0
  76. package/guides/tape.md +32 -0
  77. package/guides/tempo.md +15 -10
  78. package/guides/tracks.md +15 -10
  79. package/guides/tuning.md +32 -0
  80. package/guides/voice.md +31 -0
  81. package/guides/web-search.md +18 -8
  82. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  83. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  84. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  85. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  86. package/native/prebuilt/manifest.json +21 -0
  87. package/package.json +6 -2
  88. package/src/agent/agent.ts +130 -18
  89. package/src/agent/calibration-tools.ts +53 -0
  90. package/src/agent/clip-tools.ts +453 -0
  91. package/src/agent/command-agent.ts +378 -0
  92. package/src/agent/drum-tools.ts +2 -2
  93. package/src/agent/expression-tools.ts +1 -1
  94. package/src/agent/gateway.ts +246 -60
  95. package/src/agent/models.ts +53 -12
  96. package/src/agent/ops.ts +27 -1
  97. package/src/agent/pack-tools.ts +1 -1
  98. package/src/agent/planner.ts +25 -0
  99. package/src/agent/portable-schema.ts +80 -0
  100. package/src/agent/preview-tool.ts +4 -1
  101. package/src/agent/provider.ts +22 -8
  102. package/src/agent/range-tools.ts +216 -0
  103. package/src/agent/rhythm-tools.ts +1 -1
  104. package/src/agent/section-tools.ts +1 -1
  105. package/src/agent/show-me.ts +497 -0
  106. package/src/agent/steer.ts +15 -0
  107. package/src/agent/style-tools.ts +217 -0
  108. package/src/agent/tool-error.ts +12 -0
  109. package/src/agent/tools.ts +110 -23
  110. package/src/agent/usage.ts +2 -2
  111. package/src/agent/voice-tools.ts +925 -0
  112. package/src/agent/xcb-agent.ts +13 -10
  113. package/src/argv.ts +38 -0
  114. package/src/audio/analysis.ts +253 -0
  115. package/src/audio/arrange.ts +49 -5
  116. package/src/audio/audio-command.ts +218 -0
  117. package/src/audio/autotune-engine.ts +101 -0
  118. package/src/audio/autotune.ts +640 -0
  119. package/src/audio/clips.ts +240 -0
  120. package/src/audio/devices.ts +264 -0
  121. package/src/audio/doctor.ts +157 -0
  122. package/src/audio/dsp/bandbank.ts +138 -0
  123. package/src/audio/dsp/envelope.ts +10 -0
  124. package/src/audio/dsp/follow.ts +120 -0
  125. package/src/audio/dsp/formant.ts +427 -0
  126. package/src/audio/dsp/glottal.ts +243 -0
  127. package/src/audio/dsp/interp.ts +7 -2
  128. package/src/audio/dsp/lpc.ts +50 -0
  129. package/src/audio/dsp/periodicity.ts +59 -0
  130. package/src/audio/dsp/pitch.ts +995 -0
  131. package/src/audio/dsp/psola.ts +199 -0
  132. package/src/audio/effects/chain.ts +3 -1
  133. package/src/audio/effects/common.ts +43 -0
  134. package/src/audio/effects/convolution.ts +7 -4
  135. package/src/audio/effects/filter.ts +48 -69
  136. package/src/audio/effects/formant.ts +263 -0
  137. package/src/audio/engine.ts +360 -40
  138. package/src/audio/fit.ts +35 -3
  139. package/src/audio/instrument-check.ts +59 -43
  140. package/src/audio/instruments.ts +4 -0
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +8 -1
  143. package/src/audio/keys/engine.ts +13 -1
  144. package/src/audio/keys/piano.ts +22 -2
  145. package/src/audio/kits.ts +135 -6
  146. package/src/audio/live.ts +114 -20
  147. package/src/audio/native.ts +615 -0
  148. package/src/audio/preview.ts +30 -2
  149. package/src/audio/render-worker.ts +2 -0
  150. package/src/audio/renderer.ts +2 -0
  151. package/src/audio/resample.ts +2 -1
  152. package/src/audio/sampler.ts +85 -4
  153. package/src/audio/samples.ts +20 -2
  154. package/src/audio/sing/analysis.ts +193 -0
  155. package/src/audio/sing/engine.ts +949 -0
  156. package/src/audio/strings/bow.ts +48 -5
  157. package/src/audio/strings/engine.ts +8 -2
  158. package/src/audio/synth/oscillators.ts +31 -21
  159. package/src/audio/synth/voice.ts +34 -1
  160. package/src/audio/vocoder/bank.ts +314 -0
  161. package/src/audio/vocoder/carrier.ts +165 -0
  162. package/src/audio/vocoder/control.ts +68 -0
  163. package/src/audio/vocoder/detect.ts +50 -0
  164. package/src/audio/vocoder/index.ts +304 -0
  165. package/src/audio/vocoder/talkbox.ts +143 -0
  166. package/src/audio/wav.ts +500 -77
  167. package/src/audio/winds/engine.ts +5 -1
  168. package/src/audio/winds/trim.ts +28 -4
  169. package/src/audio/winds/trims1.ts +297 -0
  170. package/src/audio/winds/voice.ts +15 -2
  171. package/src/auth/cli.ts +38 -36
  172. package/src/auth/credentials.ts +30 -1
  173. package/src/auth/login.ts +15 -9
  174. package/src/auth/tui.ts +19 -10
  175. package/src/commands/arrange.ts +82 -32
  176. package/src/commands/autotune.ts +421 -0
  177. package/src/commands/calibration.ts +74 -0
  178. package/src/commands/clips.ts +887 -0
  179. package/src/commands/drums.ts +3 -2
  180. package/src/commands/edit.ts +11 -4
  181. package/src/commands/expression.ts +1 -1
  182. package/src/commands/formant.ts +221 -0
  183. package/src/commands/fx.ts +101 -32
  184. package/src/commands/grammar.ts +560 -0
  185. package/src/commands/help.ts +774 -366
  186. package/src/commands/history.ts +139 -10
  187. package/src/commands/keys.ts +25 -8
  188. package/src/commands/modal.ts +1 -1
  189. package/src/commands/music.ts +1 -1
  190. package/src/commands/nearest.ts +53 -0
  191. package/src/commands/pack.ts +9 -2
  192. package/src/commands/param-range.ts +56 -0
  193. package/src/commands/parses.ts +135 -0
  194. package/src/commands/progression.ts +170 -0
  195. package/src/commands/range.ts +763 -0
  196. package/src/commands/resample.ts +13 -10
  197. package/src/commands/rhythm.ts +3 -0
  198. package/src/commands/rig.ts +3 -24
  199. package/src/commands/sample.ts +11 -1
  200. package/src/commands/sing.ts +474 -0
  201. package/src/commands/strum.ts +13 -1
  202. package/src/commands/style.ts +415 -0
  203. package/src/commands/time.ts +6 -3
  204. package/src/commands/tuning.ts +3 -3
  205. package/src/commands/vocal-pitch.ts +617 -0
  206. package/src/commands/vocal.ts +147 -0
  207. package/src/commands/vocoder.ts +627 -0
  208. package/src/commands/wind.ts +2 -2
  209. package/src/fs/durable.ts +50 -0
  210. package/src/identity/actor.ts +127 -0
  211. package/src/lang/glossary.ts +511 -0
  212. package/src/launch-args.ts +267 -0
  213. package/src/main.ts +2508 -308
  214. package/src/media/cli.ts +20 -3
  215. package/src/media/import.ts +3 -1
  216. package/src/project/check.ts +21 -2
  217. package/src/project/clip-pins.ts +72 -0
  218. package/src/project/init.ts +23 -8
  219. package/src/project/sync.ts +418 -86
  220. package/src/render.ts +20 -1
  221. package/src/session/client.ts +107 -3
  222. package/src/session/clipboard.ts +79 -0
  223. package/src/session/daemon.ts +199 -5
  224. package/src/session/live-host.ts +232 -0
  225. package/src/session/meta.ts +14 -0
  226. package/src/session/origin.ts +174 -0
  227. package/src/session/port.ts +109 -14
  228. package/src/session/presence.ts +34 -4
  229. package/src/session/protocol.ts +365 -7
  230. package/src/session/rebase.ts +63 -12
  231. package/src/session/receipt.ts +265 -0
  232. package/src/session/shared-live.ts +131 -0
  233. package/src/session/store.ts +126 -45
  234. package/src/tui/arrange-menu.ts +225 -23
  235. package/src/tui/audition.ts +1 -1
  236. package/src/tui/euclid.ts +113 -27
  237. package/src/tui/fader.ts +282 -42
  238. package/src/tui/granular-menu.ts +2 -4
  239. package/src/tui/knob-fields.ts +107 -0
  240. package/src/tui/knob-map.ts +198 -0
  241. package/src/tui/menu-clips.ts +297 -0
  242. package/src/tui/menu-time.ts +20 -13
  243. package/src/tui/menu-voice.ts +405 -0
  244. package/src/tui/menu.ts +987 -179
  245. package/src/tui/modal-menu.ts +6 -6
  246. package/src/tui/performance-menu.ts +5 -2
  247. package/src/tui/play-chords.ts +4 -2
  248. package/src/tui/play-mode.ts +15 -1
  249. package/src/tui/play-session.ts +243 -28
  250. package/src/tui/sing-menu.ts +278 -0
  251. package/src/tui/style-menu.ts +104 -0
  252. package/src/tui/tape-mode.ts +580 -0
  253. package/src/tui/tape-view.ts +263 -0
  254. package/src/tui/vocoder-menu.ts +244 -0
  255. package/src/tui/wind-menu.ts +3 -3
  256. package/src/version.ts +8 -0
  257. package/src/web/fetch.ts +115 -29
  258. package/tui/activity.ts +180 -9
  259. package/tui/app.ts +491 -84
  260. package/tui/clip-row.ts +132 -0
  261. package/tui/delight.ts +193 -0
  262. package/tui/drawer.ts +233 -27
  263. package/tui/frame-gate.ts +76 -0
  264. package/tui/grammar.ts +143 -65
  265. package/tui/guide.ts +50 -5
  266. package/tui/highway.ts +309 -30
  267. package/tui/hints.ts +197 -0
  268. package/tui/hits.ts +7 -1
  269. package/tui/input.ts +60 -9
  270. package/tui/keys.ts +1 -1
  271. package/tui/knobs.ts +268 -0
  272. package/tui/play-strip.ts +80 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +151 -18
  275. package/tui/tape.ts +439 -0
  276. package/tui/text.ts +35 -2
  277. package/tui/theme.ts +46 -1
package/core/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,34 +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
- "softPedal",
48
- "sostenuto",
49
- // f061-guitar: fretting setup.
50
- "guitar",
51
- "wind",
52
- ] as const;
53
- const LANES = [
54
- "volumeAutomation",
55
- "panAutomation",
56
- "filterAutomation",
57
- "resonanceAutomation",
58
- "delayFeedbackAutomation",
59
- "delayMixAutomation",
60
- "wtAutomation",
61
- ] 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
+ }
62
133
 
63
134
  /** Operations turning `a` into `b`; empty when they are equal. */
64
135
  export function diffScores(
@@ -69,31 +140,7 @@ export function diffScores(
69
140
  throw new DiffError(
70
141
  `ticksPerBeat differs (${a.ticksPerBeat} → ${b.ticksPerBeat}); the session resolution is fixed`,
71
142
  );
72
- const ops: ScoreOperation[] = [];
73
- if (a.beatsPerBar !== b.beatsPerBar)
74
- ops.push({ type: "setMeter", beatsPerBar: b.beatsPerBar });
75
- if (a.bars !== b.bars) ops.push({ type: "setBars", bars: b.bars });
76
- if (a.tempoBpm !== b.tempoBpm)
77
- ops.push({ type: "setTempo", tempoBpm: b.tempoBpm });
78
- if (a.key !== b.key) ops.push({ type: "setKey", key: b.key });
79
- if (!deepEqual(a.time, b.time))
80
- ops.push({ type: "setTime", time: b.time ?? null });
81
- if (!deepEqual(a.tuning, b.tuning))
82
- ops.push({ type: "setTuning", tuning: b.tuning ?? null });
83
- if (!deepEqual(a.master, b.master))
84
- ops.push({ type: "setMaster", master: b.master ?? null });
85
- if (
86
- !deepEqual(a.sections, b.sections) ||
87
- !deepEqual(a.form, b.form) ||
88
- a.loopSection !== b.loopSection
89
- )
90
- ops.push({
91
- type: "setSections",
92
- sections: b.sections,
93
- form: b.form,
94
- ...(b.loopSection === undefined ? {} : { loopSection: b.loopSection }),
95
- });
96
-
143
+ const ops: ScoreOperation[] = [...songOperations(a, b)];
97
144
  const aTracks = new Map(a.tracks.map((track) => [track.id, track]));
98
145
  const bTracks = new Map(b.tracks.map((track) => [track.id, track]));
99
146
  const aNotes = new Map(a.notes.map((note) => [note.id, note]));
@@ -101,32 +148,26 @@ export function diffScores(
101
148
 
102
149
  const updates: ScoreOperation[] = [];
103
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>();
104
154
  for (const note of a.notes) {
105
- 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
+ }
106
159
  const next = bNotes.get(note.id);
107
160
  if (!next || next.trackId !== note.trackId) {
108
161
  ops.push({ type: "removeNote", noteId: note.id });
162
+ gone.add(note.id);
109
163
  continue;
110
164
  }
111
165
  const patch = notePatch(note, next);
112
166
  if (patch) updates.push({ type: "updateNote", noteId: note.id, patch });
113
167
  }
114
- for (const note of b.notes) {
115
- const previous = aNotes.get(note.id);
116
- if (
117
- previous &&
118
- previous.trackId === note.trackId &&
119
- bTracks.has(note.trackId)
120
- )
121
- continue;
122
- if (
123
- previous &&
124
- previous.trackId === note.trackId &&
125
- !aTracks.has(note.trackId)
126
- )
127
- continue;
128
- additions.push({ type: "addNote", note });
129
- }
168
+ for (const note of b.notes)
169
+ if (!aNotes.has(note.id) || gone.has(note.id))
170
+ additions.push({ type: "addNote", note });
130
171
 
131
172
  for (const track of a.tracks)
132
173
  if (!bTracks.has(track.id))
@@ -156,6 +197,71 @@ export function diffScores(
156
197
  return Object.freeze(ops);
157
198
  }
158
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
+ if (!deepEqual(song.loop, b.loop))
261
+ emit({ type: "setLoop", loop: b.loop ?? null });
262
+ return ops;
263
+ }
264
+
159
265
  /** Applies operations in order; the inverse of `diffScores`. */
160
266
  export function applyScoreOperations(
161
267
  score: TrackScore,
@@ -165,43 +271,17 @@ export function applyScoreOperations(
165
271
  }
166
272
 
167
273
  function notePatch(a: Note, b: Note): NotePatch | undefined {
168
- const patch: Record<string, unknown> = {};
169
- for (const key of [
170
- "startTick",
171
- "durationTicks",
172
- "pitch",
173
- "velocity",
174
- ] as const)
175
- if (a[key] !== b[key]) patch[key] = b[key];
176
- for (const key of NOTE_EXPRESSION_FIELDS)
177
- if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
178
- if ((a.cents ?? 0) !== (b.cents ?? 0)) patch.cents = b.cents ?? 0;
179
- return Object.keys(patch).length > 0 ? (patch as NotePatch) : undefined;
274
+ return fieldPatch(NOTE_FIELDS, a, b) as NotePatch | undefined;
180
275
  }
181
276
 
182
277
  function trackPatch(a: Track, b: Track): TrackPatch | undefined {
183
- const patch: Record<string, unknown> = {};
184
- for (const key of ["name", "instrument", "muted", "volume", "pan"] as const)
185
- if (a[key] !== b[key]) patch[key] = b[key];
186
- if ((a.solo ?? false) !== (b.solo ?? false)) patch.solo = b.solo ?? false;
187
- for (const key of EFFECTS)
188
- if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
189
- for (const key of [
190
- "rhythm",
191
- "kit",
192
- "time",
193
- "tuning",
194
- ...TRACK_PERFORMANCE_FIELDS,
195
- ] as const)
196
- if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
197
- for (const key of LANES)
198
- if (!deepEqual(a[key] ?? [], b[key] ?? [])) patch[key] = b[key] ?? [];
199
- return Object.keys(patch).length > 0 ? (patch as TrackPatch) : undefined;
278
+ return fieldPatch(TRACK_FIELDS, a, b) as TrackPatch | undefined;
200
279
  }
201
280
 
202
281
  /** Structural equality for plain JSON-like data. */
203
282
  export function deepEqual(a: unknown, b: unknown): boolean {
204
- if (Object.is(a, b)) return true;
283
+ // === so -0 equals 0: a printed song.ts reads -0 back as 0.
284
+ if (a === b) return true;
205
285
  if (
206
286
  typeof a !== "object" ||
207
287
  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;
@@ -325,6 +332,12 @@ export function normalizeVibrato(value: unknown): NoteVibrato | undefined {
325
332
  return Object.freeze({ rate, depth, ...(delay > 0 ? { delay } : {}) });
326
333
  }
327
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
+
328
341
  /** Validates a note's expression fields; only the set ones are returned. */
329
342
  export function normalizeNoteExpression(
330
343
  input: Readonly<Record<string, unknown>>,
@@ -334,12 +347,14 @@ export function normalizeNoteExpression(
334
347
  const bend = normalizeBend(input.bend);
335
348
  const vibrato = normalizeVibrato(input.vibrato);
336
349
  const humanize = normalizeNoteHumanize(input.humanize);
350
+ const drift = normalizeNoteDrift(input.drift);
337
351
  return {
338
352
  ...(articulation ? { articulation } : {}),
339
353
  ...(glide !== undefined ? { glide } : {}),
340
354
  ...(bend ? { bend } : {}),
341
355
  ...(vibrato ? { vibrato } : {}),
342
356
  ...(humanize ? { humanize } : {}),
357
+ ...(drift !== undefined ? { drift } : {}),
343
358
  };
344
359
  }
345
360
 
package/core/fx.ts CHANGED
@@ -13,10 +13,12 @@
13
13
  */
14
14
 
15
15
  import { MODAL_LANE_PARAMS, WIND_LANE_PARAMS } from "./resonators.ts";
16
+ import { VOCODER_LANE_PARAMS } from "./vocoder.ts";
16
17
  import { SYNTH_LANE_PARAMS } from "./synth.ts";
17
18
  import { STRING_LANE_PARAMS } from "./strings.ts";
18
19
  import { KEYS_LANE_PARAMS } from "./keys.ts";
19
20
  import { GRANULAR_LANE_PARAMS } from "./granular.ts";
21
+ import { SING_LANE_PARAMS } from "./sing.ts";
20
22
  import {
21
23
  FxValidationError,
22
24
  normalizeParam,
@@ -49,6 +51,8 @@ export const FX_CHAIN = Object.freeze([
49
51
  "filter",
50
52
  "djf",
51
53
  "autofilter",
54
+ // 0.7 formant shift at constant pitch (src/audio/effects/formant.ts).
55
+ "formant",
52
56
  "vowel",
53
57
  "crush",
54
58
  "distort",
@@ -110,7 +114,7 @@ const sync = (fallback: number, strudel?: readonly string[]): NumberParam => ({
110
114
  default: fallback,
111
115
  step: 0.25,
112
116
  unit: "beats",
113
- doc: "LFO cycle length in beats (tempo-synced); 0 uses rate in Hz",
117
+ doc: "LFO period in beats (tempo-synced); 0 uses rate in Hz",
114
118
  ...(strudel ? { strudel } : {}),
115
119
  });
116
120
  const mix = (fallback: number, strudel?: readonly string[]): NumberParam => ({
@@ -124,6 +128,25 @@ const mix = (fallback: number, strudel?: readonly string[]): NumberParam => ({
124
128
  ...(strudel ? { strudel } : {}),
125
129
  });
126
130
 
131
+ /** Vowels of the `vowel` effect (src/audio/dsp/formant.ts tables). */
132
+ export const VOWEL_VALUES = Object.freeze([
133
+ "a",
134
+ "e",
135
+ "i",
136
+ "o",
137
+ "u",
138
+ "ae",
139
+ "aa",
140
+ "oe",
141
+ "ue",
142
+ "y",
143
+ "uh",
144
+ "un",
145
+ "en",
146
+ "an",
147
+ "on",
148
+ ] as const);
149
+
127
150
  /**
128
151
  * Effects stored under `track.fx`. Every number is bounded; enabling an
129
152
  * effect stores all of its parameters so a document says exactly what plays.
@@ -131,7 +154,7 @@ const mix = (fallback: number, strudel?: readonly string[]): NumberParam => ({
131
154
  export const FX_SPECS = Object.freeze({
132
155
  autofilter: {
133
156
  label: "auto filter",
134
- doc: "filter whose cutoff an LFO sweeps around a centre, optionally opened by the input level",
157
+ doc: "filter whose cutoff an LFO sweeps around a center, optionally opened by the input level",
135
158
  simple: ["type", "cutoff", "depth", "sync", "shape"],
136
159
  strudel:
137
160
  "none built in (Strudel fakes it with lpf(sine.range(..)).seg(n)); names follow lpf/lpq",
@@ -151,7 +174,7 @@ export const FX_SPECS = Object.freeze({
151
174
  step: "log",
152
175
  unit: "Hz",
153
176
  automate: true,
154
- doc: "centre cutoff",
177
+ doc: "center cutoff",
155
178
  strudel: ["lpf", "cutoff"],
156
179
  },
157
180
  resonance: {
@@ -172,7 +195,7 @@ export const FX_SPECS = Object.freeze({
172
195
  step: 0.25,
173
196
  unit: "oct",
174
197
  automate: true,
175
- doc: "sweep width in octaves around the centre",
198
+ doc: "sweep width in octaves around the center",
176
199
  },
177
200
  sync: sync(4),
178
201
  rate: rate(0.5),
@@ -216,11 +239,30 @@ export const FX_SPECS = Object.freeze({
216
239
  default: 0.5,
217
240
  step: 0.02,
218
241
  automate: true,
219
- doc: "0 dark … 0.5 open … 1 thin",
242
+ doc: "0 dark (20 Hz low-pass) … 0.5 open … 1 thin (10 kHz high-pass)",
220
243
  strudel: ["djf"],
221
244
  },
222
245
  },
223
246
  },
247
+ formant: {
248
+ label: "formant",
249
+ doc: "formant shift at constant pitch: moves the spectral envelope (throat or gender knob), any source",
250
+ simple: ["shift", "mix"],
251
+ strudel: "none (not a Strudel concept)",
252
+ params: {
253
+ shift: {
254
+ kind: "number",
255
+ min: -12,
256
+ max: 12,
257
+ default: 0,
258
+ step: 0.5,
259
+ unit: "st",
260
+ automate: true,
261
+ doc: "semitones the formants move (negative deeper, positive smaller); pitch stays",
262
+ },
263
+ mix: mix(1),
264
+ },
265
+ },
224
266
  vowel: {
225
267
  label: "vowel",
226
268
  doc: "formant filter bank: five band-passes per vowel",
@@ -229,28 +271,30 @@ export const FX_SPECS = Object.freeze({
229
271
  params: {
230
272
  vowel: {
231
273
  kind: "enum",
232
- values: [
233
- "a",
234
- "e",
235
- "i",
236
- "o",
237
- "u",
238
- "ae",
239
- "aa",
240
- "oe",
241
- "ue",
242
- "y",
243
- "uh",
244
- "un",
245
- "en",
246
- "an",
247
- "on",
248
- ],
274
+ values: VOWEL_VALUES,
249
275
  default: "a",
250
276
  doc: "vowel formants (a e i o u plus the extended set)",
251
277
  strudel: ["vowel"],
252
278
  },
253
279
  mix: mix(1),
280
+ // 0.7: optional morph target; absent keeps the static bank exactly.
281
+ to: {
282
+ kind: "enum",
283
+ values: VOWEL_VALUES,
284
+ default: "a",
285
+ optional: true,
286
+ doc: "vowel to morph toward (absent: no morph)",
287
+ },
288
+ morph: {
289
+ kind: "number",
290
+ min: 0,
291
+ max: 1,
292
+ default: 0,
293
+ step: 0.05,
294
+ optional: true,
295
+ automate: true,
296
+ doc: "position between vowel (0) and to (1), log-frequency formant morph",
297
+ },
254
298
  },
255
299
  },
256
300
  crush: {
@@ -467,7 +511,7 @@ export const FX_SPECS = Object.freeze({
467
511
  step: 1,
468
512
  unit: "dB",
469
513
  optional: true,
470
- doc: "noise gate threshold before the amp (absent: no gate)",
514
+ doc: "noise gate threshold before the amp (absent: no gate; 0 dB gates all)",
471
515
  },
472
516
  level: {
473
517
  kind: "number",
@@ -498,7 +542,7 @@ export const FX_SPECS = Object.freeze({
498
542
  max: 1,
499
543
  default: 0.3,
500
544
  step: 0.05,
501
- doc: "microphone position: 0 centre (bright) .. 1 edge (dark)",
545
+ doc: "microphone position: 0 center (bright) .. 1 edge (dark)",
502
546
  },
503
547
  mix: mix(1),
504
548
  },
@@ -628,7 +672,7 @@ export const FX_SPECS = Object.freeze({
628
672
  max: 1,
629
673
  default: 0.6,
630
674
  step: 0.05,
631
- doc: "0 centred .. 1 the two takes hard left and right",
675
+ doc: "0 centered .. 1 the two takes hard left and right",
632
676
  },
633
677
  },
634
678
  },
@@ -771,7 +815,7 @@ export const FX_SPECS = Object.freeze({
771
815
  default: 1000,
772
816
  step: "log",
773
817
  unit: "Hz",
774
- doc: "sweep centre",
818
+ doc: "sweep center",
775
819
  strudel: ["phasercenter", "phc"],
776
820
  },
777
821
  sweep: {
@@ -955,7 +999,7 @@ export const TRACK_EFFECT_SPECS = Object.freeze({
955
999
  step: "log",
956
1000
  unit: "Hz",
957
1001
  automate: true,
958
- doc: "cutoff (lpf/hpf) or centre (bpf) frequency",
1002
+ doc: "cutoff (lpf/hpf) or center (bpf) frequency",
959
1003
  strudel: [
960
1004
  "lpf",
961
1005
  "cutoff",
@@ -1221,7 +1265,7 @@ export function normalizeFx(input: unknown): TrackFx | undefined {
1221
1265
 
1222
1266
  /** Automation lane name of an `fx` parameter, e.g. `distort-drive`. */
1223
1267
  export type FxLane =
1224
- `${FxName | "reverb" | "synth" | "string" | "keys" | "modal" | "grain" | "wind"}-${string}`;
1268
+ `${FxName | "reverb" | "synth" | "string" | "keys" | "modal" | "grain" | "wind" | "sing" | "vocoder"}-${string}`;
1225
1269
 
1226
1270
  /** Every automatable `fx` parameter as `{ lane, effect, param, spec }`. */
1227
1271
  export const FX_LANES: readonly Readonly<{
@@ -1234,7 +1278,9 @@ export const FX_LANES: readonly Readonly<{
1234
1278
  | "keys"
1235
1279
  | "modal"
1236
1280
  | "grain"
1237
- | "wind";
1281
+ | "wind"
1282
+ | "sing"
1283
+ | "vocoder";
1238
1284
  param: string;
1239
1285
  spec: NumberParam;
1240
1286
  }>[] = Object.freeze([
@@ -1309,6 +1355,24 @@ export const FX_LANES: readonly Readonly<{
1309
1355
  spec,
1310
1356
  }),
1311
1357
  ),
1358
+ // Sing parameters (core/sing.ts, 0.7), read every 32-sample control tick.
1359
+ ...SING_LANE_PARAMS.map(({ param, spec }) =>
1360
+ Object.freeze({
1361
+ lane: `sing-${param}` as FxLane,
1362
+ effect: "sing" as const,
1363
+ param,
1364
+ spec,
1365
+ }),
1366
+ ),
1367
+ // Vocoder (core/vocoder.ts, 0.7), read per 32-sample block on the song grid.
1368
+ ...VOCODER_LANE_PARAMS.map(({ param, spec }) =>
1369
+ Object.freeze({
1370
+ lane: `vocoder-${param}` as FxLane,
1371
+ effect: "vocoder" as const,
1372
+ param,
1373
+ spec,
1374
+ }),
1375
+ ),
1312
1376
  ]);
1313
1377
 
1314
1378
  /**
@@ -1355,6 +1419,12 @@ export const FX_PRESETS: Readonly<
1355
1419
  resonance: 0.6,
1356
1420
  },
1357
1421
  },
1422
+ formant: {
1423
+ deep: { shift: -4 },
1424
+ giant: { shift: -8 },
1425
+ bright: { shift: 3 },
1426
+ tiny: { shift: 7 },
1427
+ },
1358
1428
  vowel: { a: { vowel: "a" }, o: { vowel: "o" }, ee: { vowel: "i" } },
1359
1429
  crush: {
1360
1430
  "8-bit": { bits: 8, coarse: 1 },