@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
@@ -0,0 +1,244 @@
1
+ /**
2
+ * The `wind` prompt command (0.6.1): blown waveguides on the focused track
3
+ * (core/winds.ts). The menu and the `set_wind` agent tool build the same
4
+ * edits.
5
+ *
6
+ * wind show the track's preset and overrides
7
+ * wind <preset> play a preset: flute recorder whistle
8
+ * ney shakuhachi panpipe suling bansuri
9
+ * clarinet bassclarinet oboe bassoon sax
10
+ * altosax barisax trumpet harmon plunger
11
+ * trombone tuba horn (and aliases such
12
+ * as saxophone frenchhorn panflute)
13
+ * wind <preset> <param> <value>… a preset with overrides
14
+ * wind preset <name> the same, spelled out
15
+ * wind mute <open|straight|cup|harmon|plunger>
16
+ * wind <param> <value> [<param> <value>…] breath 0.8, players 4 …
17
+ * wind <param> off back to the preset's value
18
+ * wind reset clear overrides (keep the preset)
19
+ * wind off back to the legacy wind tone
20
+ * wind list | presets wind the presets with one-line characters
21
+ *
22
+ * Any edit turns the track into a wind-engine track (`instrument "wind"`
23
+ * plus a `wind` field); a stored `wind` with no field keeps the legacy tone.
24
+ * One `updateTrack` revision and one undo step per command.
25
+ */
26
+ import { FxValidationError } from "../../core/params.ts";
27
+ import {
28
+ DEFAULT_WIND_PRESET,
29
+ normalizeWind,
30
+ WIND_INSTRUMENT,
31
+ WIND_PARAMS,
32
+ WIND_PRESETS,
33
+ WIND_PRESET_NAMES,
34
+ WIND_SIMPLE,
35
+ windParamName,
36
+ windPresetFor,
37
+ type TrackWind,
38
+ } from "../../core/resonators.ts";
39
+ import {
40
+ ScoreValidationError,
41
+ updateTrack,
42
+ type TrackScore,
43
+ } from "../../core/score.ts";
44
+ import { nearest } from "./nearest.ts";
45
+ import { parseParamValue } from "./fx.ts";
46
+
47
+ export type WindCommand =
48
+ | { type: "wind-show" }
49
+ | { type: "wind-list" }
50
+ | { type: "wind-reset" }
51
+ | { type: "wind-off" }
52
+ | { type: "wind-preset"; preset: string }
53
+ | { type: "wind-usage"; message: string }
54
+ | {
55
+ type: "wind-set";
56
+ /** A preset applied before the values (`wind trumpet mute harmon`). */
57
+ preset?: string;
58
+ /** `null` returns a parameter to the preset's value. */
59
+ values: Readonly<Record<string, number | string | boolean | null>>;
60
+ };
61
+
62
+ export const WIND_USAGE =
63
+ "wind <preset> | wind <param> <value> | wind mute <name> | wind reset | wind off | wind presets";
64
+
65
+ function rangeOf(name: string): string {
66
+ const spec = WIND_PARAMS[name]!;
67
+ if (spec.kind === "number") return `${name} is ${spec.min}..${spec.max}`;
68
+ if (spec.kind === "enum") return `${name} is one of ${spec.values.join(" ")}`;
69
+ return `${name} is on or off`;
70
+ }
71
+
72
+ export function parseWindCommand(prompt: string): WindCommand | undefined {
73
+ const words = prompt.trim().toLowerCase().split(/\s+/);
74
+ if (words[0] === "presets" && words[1] === "wind" && words.length === 2)
75
+ return { type: "wind-list" };
76
+ if (words[0] !== "wind") return undefined;
77
+ if (words.length === 1) return { type: "wind-show" };
78
+ if (prompt.length > 512) return undefined;
79
+ const rest = words.slice(1);
80
+ if (rest.length === 1 && rest[0] === "reset") return { type: "wind-reset" };
81
+ if (rest.length === 1 && rest[0] === "off") return { type: "wind-off" };
82
+ if (rest.length === 1 && (rest[0] === "list" || rest[0] === "presets"))
83
+ return { type: "wind-list" };
84
+ if (rest[0] === "preset") {
85
+ const preset = rest.length === 2 ? windPresetFor(rest[1]!) : undefined;
86
+ return preset
87
+ ? { type: "wind-preset", preset }
88
+ : {
89
+ type: "wind-usage",
90
+ message: `wind preset is one of ${WIND_PRESET_NAMES.join(" ")}`,
91
+ };
92
+ }
93
+ if (rest.length === 1) {
94
+ const preset = windPresetFor(rest[0]!);
95
+ if (preset) return { type: "wind-preset", preset };
96
+ const mute = WIND_PARAMS.mute;
97
+ if (mute?.kind === "enum" && mute.values.includes(rest[0]!))
98
+ return { type: "wind-set", values: { mute: rest[0]! } };
99
+ return { type: "wind-usage", message: WIND_USAGE };
100
+ }
101
+ // `wind <preset> <param> <value>…`: the preset, then its overrides.
102
+ const lead = rest.length % 2 === 1 ? windPresetFor(rest[0]!) : undefined;
103
+ if (lead) rest.shift();
104
+ if (rest.length % 2 !== 0) return { type: "wind-usage", message: WIND_USAGE };
105
+ const values: Record<string, number | string | boolean | null> = {};
106
+ for (let index = 0; index < rest.length; index += 2) {
107
+ const name = windParamName(rest[index]!);
108
+ if (!name) {
109
+ const word = rest[index]!.slice(0, 24);
110
+ const near = nearest(word, Object.keys(WIND_PARAMS));
111
+ return {
112
+ type: "wind-usage",
113
+ message: `wind has no parameter ${word}${near ? ` · did you mean ${near}?` : ""} · ${WIND_SIMPLE.join(" ")} …`,
114
+ };
115
+ }
116
+ const word = rest[index + 1]!;
117
+ if (word === "off" && name !== "stopped") {
118
+ values[name] = null;
119
+ continue;
120
+ }
121
+ if (word === "unset") {
122
+ values[name] = null;
123
+ continue;
124
+ }
125
+ const value = parseParamValue(WIND_PARAMS[name]!, word);
126
+ if (value === undefined)
127
+ return { type: "wind-usage", message: rangeOf(name) };
128
+ values[name] = value;
129
+ }
130
+ return lead
131
+ ? { type: "wind-set", preset: lead, values }
132
+ : { type: "wind-set", values };
133
+ }
134
+
135
+ /** `preset sax · breath 0.8 · players 4`, overrides in canonical order. */
136
+ export function describeWind(wind: TrackWind | undefined): string {
137
+ if (!wind) return "not a wind-engine track";
138
+ const preset = wind.preset ?? DEFAULT_WIND_PRESET;
139
+ const overrides = Object.entries(wind)
140
+ .filter(([key]) => key !== "preset")
141
+ .map(([key, value]) => `${key} ${value}`);
142
+ return [`preset ${preset}`, ...overrides].join(" · ");
143
+ }
144
+
145
+ /** One line per preset, for `wind list` and the agent brief. */
146
+ export function windListLines(): string[] {
147
+ return WIND_PRESET_NAMES.map(
148
+ (name) => `${name.padEnd(13)} ${WIND_PRESETS[name].doc}`,
149
+ );
150
+ }
151
+
152
+ export type WindResult = Readonly<{
153
+ ok: boolean;
154
+ message: string;
155
+ next?: TrackScore;
156
+ kind?: string;
157
+ payload?: Record<string, unknown>;
158
+ }>;
159
+
160
+ /**
161
+ * The wind field a command leaves on a track (pure; the agent tool shares
162
+ * it). A preset switch keeps the overrides; `reset` keeps the preset.
163
+ */
164
+ export function nextWind(
165
+ current: TrackWind | undefined,
166
+ command: Extract<
167
+ WindCommand,
168
+ { type: "wind-reset" | "wind-preset" | "wind-set" }
169
+ >,
170
+ ): TrackWind {
171
+ if (command.type === "wind-reset")
172
+ return normalizeWind(current?.preset ? { preset: current.preset } : {})!;
173
+ const base: Record<string, unknown> = { ...current };
174
+ if (command.type === "wind-preset") base.preset = command.preset;
175
+ else {
176
+ if (command.preset) base.preset = command.preset;
177
+ for (const [key, value] of Object.entries(command.values)) {
178
+ if (value === null) delete base[key];
179
+ else base[key] = value;
180
+ }
181
+ }
182
+ return normalizeWind(base)!;
183
+ }
184
+
185
+ export function applyWindCommand(
186
+ score: TrackScore,
187
+ trackId: string,
188
+ command: WindCommand,
189
+ ): WindResult {
190
+ if (command.type === "wind-usage")
191
+ return { ok: false, message: `wind · ${command.message}` };
192
+ if (command.type === "wind-list")
193
+ return {
194
+ ok: true,
195
+ message: `wind presets · ${WIND_PRESET_NAMES.join(" ")}`,
196
+ };
197
+ const track = score.tracks.find((candidate) => candidate.id === trackId);
198
+ if (!track) return { ok: false, message: `no track · ${trackId}` };
199
+ const current = track.instrument === WIND_INSTRUMENT ? track.wind : undefined;
200
+ if (command.type === "wind-show")
201
+ return {
202
+ ok: true,
203
+ message: current
204
+ ? `wind · ${describeWind(current)}`
205
+ : `wind · ${trackId} plays ${track.instrument} · wind <preset> to switch (${WIND_PRESET_NAMES.join(" ")})`,
206
+ };
207
+ if (track.sampler || track.instrument === "kit")
208
+ return {
209
+ ok: false,
210
+ message: `wind · ${trackId} is a ${track.sampler ? "sampler" : "drum"} track`,
211
+ };
212
+ if (command.type === "wind-off") {
213
+ if (!current) return { ok: true, message: "wind · already off" };
214
+ // The stored word stays `wind`, which is the legacy tone without a field.
215
+ const next = updateTrack(score, trackId, { wind: null });
216
+ return {
217
+ ok: true,
218
+ message: `wind · off (legacy wind tone) · wind ${current.preset ?? DEFAULT_WIND_PRESET} turns it back on`,
219
+ next,
220
+ kind: "score.wind",
221
+ payload: { trackId, instrument: WIND_INSTRUMENT, wind: null },
222
+ };
223
+ }
224
+ let wind: TrackWind;
225
+ let next: TrackScore;
226
+ try {
227
+ wind = nextWind(current, command);
228
+ next = updateTrack(score, trackId, { instrument: WIND_INSTRUMENT, wind });
229
+ } catch (error) {
230
+ if (
231
+ error instanceof ScoreValidationError ||
232
+ error instanceof FxValidationError
233
+ )
234
+ return { ok: false, message: `wind · ${error.message}` };
235
+ throw error;
236
+ }
237
+ return {
238
+ ok: true,
239
+ message: `wind · ${describeWind(wind)}`,
240
+ next,
241
+ kind: "score.wind",
242
+ payload: { trackId, instrument: WIND_INSTRUMENT, wind },
243
+ };
244
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Crash-safe file replacement: write a uniquely named temp file, fsync it,
3
+ * rename it over the target and fsync the parent directory so the rename
4
+ * itself survives a power loss. A reader never sees a partial file.
5
+ */
6
+
7
+ import { mkdir, open, rename, rm } from "node:fs/promises";
8
+ import { dirname } from "node:path";
9
+
10
+ let counter = 0;
11
+
12
+ export async function durableWrite(
13
+ path: string,
14
+ contents: string,
15
+ options: Readonly<{ mode?: number; mkdir?: boolean }> = {},
16
+ ): Promise<void> {
17
+ const dir = dirname(path);
18
+ if (options.mkdir !== false) await mkdir(dir, { recursive: true });
19
+ const temporary = `${path}.${process.pid}.${++counter}.tmp`;
20
+ const handle = await open(temporary, "w", options.mode ?? 0o644);
21
+ try {
22
+ await handle.writeFile(contents, "utf8");
23
+ await handle.sync();
24
+ } catch (error) {
25
+ await handle.close().catch(() => undefined);
26
+ await rm(temporary, { force: true }).catch(() => undefined);
27
+ throw error;
28
+ }
29
+ await handle.close();
30
+ try {
31
+ await rename(temporary, path);
32
+ } catch (error) {
33
+ await rm(temporary, { force: true }).catch(() => undefined);
34
+ throw error;
35
+ }
36
+ await syncDirectory(dir);
37
+ }
38
+
39
+ /** Best effort: some platforms refuse fsync on a directory handle. */
40
+ export async function syncDirectory(dir: string): Promise<void> {
41
+ let handle: Awaited<ReturnType<typeof open>> | undefined;
42
+ try {
43
+ handle = await open(dir, "r");
44
+ await handle.sync();
45
+ } catch {
46
+ // EISDIR/EPERM/EINVAL on platforms without directory fsync.
47
+ } finally {
48
+ await handle?.close().catch(() => undefined);
49
+ }
50
+ }