@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
@@ -18,6 +18,8 @@
18
18
  * maximum weight.
19
19
  */
20
20
  import { resolveInstrumentWord } from "./instruments.ts";
21
+ import { WIND_INSTRUMENT, windPresetFor, type TrackWind } from "./winds.ts";
22
+ import { SING_INSTRUMENT, isSingPreset, type TrackSing } from "./sing.ts";
21
23
  import {
22
24
  FxValidationError,
23
25
  isRecord,
@@ -84,6 +86,22 @@ export const MODAL_PRESET_NAMES = Object.freeze([
84
86
  "bowl",
85
87
  "gong",
86
88
  "timpani",
89
+ // f061-gamelan-winds: gamelan, small bells and frame drums (lane A2).
90
+ "crotales",
91
+ "musicbox",
92
+ "toypiano",
93
+ "saron",
94
+ "demung",
95
+ "slenthem",
96
+ "gangsa",
97
+ "gender",
98
+ "bonang",
99
+ "kenong",
100
+ "kethuk",
101
+ "kempul",
102
+ "daf",
103
+ "bodhran",
104
+ "tabla",
87
105
  ] as const);
88
106
  export type ModalPresetName = (typeof MODAL_PRESET_NAMES)[number];
89
107
 
@@ -99,8 +117,29 @@ export const MODAL_ALIASES: Readonly<Record<string, ModalPresetName>> =
99
117
  singingbowl: "bowl",
100
118
  kettledrum: "timpani",
101
119
  tubularbells: "chimes",
120
+ // f061-gamelan-winds. `gamelan` is a browse group, not an alias.
121
+ crotale: "crotales",
122
+ musicalbox: "musicbox",
123
+ framedrum: "bodhran",
102
124
  });
103
125
 
126
+ /**
127
+ * The gamelan presets, in browse order (the `gamelan` hint opens these
128
+ * under Mallets and bells).
129
+ */
130
+ export const GAMELAN_PRESETS: readonly ModalPresetName[] = Object.freeze([
131
+ "saron",
132
+ "demung",
133
+ "slenthem",
134
+ "gangsa",
135
+ "gender",
136
+ "bonang",
137
+ "kenong",
138
+ "kethuk",
139
+ "kempul",
140
+ "gong",
141
+ ]);
142
+
104
143
  const num = (
105
144
  min: number,
106
145
  max: number,
@@ -137,7 +176,7 @@ export const MODAL_PARAMS: Readonly<Record<string, ParamSpec>> = Object.freeze({
137
176
  hardness: num(0, 1, 0.4, 0.05, "mallet hardness: 0 yarn/felt, 1 brass", {
138
177
  automate: true,
139
178
  }),
140
- position: num(0, 1, 0.42, 0.05, "strike point: 0 end or edge, 0.5 centre", {
179
+ position: num(0, 1, 0.42, 0.05, "strike point: 0 end or edge, 0.5 center", {
141
180
  automate: true,
142
181
  }),
143
182
  ring: num(0.05, 30, 1.6, "log", "ring time (T60) at middle C", {
@@ -364,6 +403,186 @@ export const MODAL_PRESETS: Readonly<Record<ModalPresetName, ModalPreset>> =
364
403
  "pedal timpani, felt sticks, a small strike glide",
365
404
  "Beethoven, Brahms, Shostakovich, orchestral",
366
405
  ),
406
+ // f061-gamelan-winds (lane A2): appended; rows above are unchanged.
407
+ // Gamelan ranges follow the instruments' slendro/pelog octaves; tune
408
+ // them with `tuning slendro` or `tuning pelog` (core/tuning.ts).
409
+ crotales: preset(
410
+ {
411
+ body: "crotale",
412
+ ring: 6,
413
+ tilt: 0.4,
414
+ hardness: 0.85,
415
+ position: 0.5,
416
+ gain: 1.6,
417
+ },
418
+ [84, 108],
419
+ "antique cymbals, long shimmering ring",
420
+ "Messiaen, ambient",
421
+ ),
422
+ musicbox: preset(
423
+ {
424
+ body: "musicbox",
425
+ ring: 1.8,
426
+ tilt: 0.7,
427
+ hardness: 1,
428
+ position: 0.8,
429
+ gain: 1.7,
430
+ },
431
+ [60, 100],
432
+ "plucked steel comb",
433
+ "ambient, OPN, A. G. Cook",
434
+ ),
435
+ toypiano: preset(
436
+ {
437
+ body: "toypiano",
438
+ ring: 1,
439
+ tilt: 0.6,
440
+ hardness: 0.9,
441
+ position: 0.6,
442
+ click: 0.3,
443
+ gain: 2,
444
+ },
445
+ [60, 96],
446
+ "hammered toy-piano rods, clanky",
447
+ "hyperpop, A. G. Cook, Four Tet",
448
+ ),
449
+ saron: preset(
450
+ {
451
+ body: "saron",
452
+ ring: 2.5,
453
+ tilt: 0.6,
454
+ hardness: 0.85,
455
+ position: 0.5,
456
+ damp: 0.85,
457
+ release: 0.1,
458
+ },
459
+ [60, 84],
460
+ "saron barung: bronze keys, horn mallet, the balungan melody; damped at the next note (tutupan)",
461
+ "Javanese gamelan, Reich",
462
+ ),
463
+ demung: preset(
464
+ {
465
+ body: "saron",
466
+ ring: 3.2,
467
+ tilt: 0.6,
468
+ hardness: 0.75,
469
+ position: 0.5,
470
+ damp: 0.8,
471
+ release: 0.12,
472
+ },
473
+ [48, 72],
474
+ "demung: the saron an octave lower, fuller",
475
+ "Javanese gamelan",
476
+ ),
477
+ slenthem: preset(
478
+ { body: "gender", ring: 4.5, tilt: 0.5, hardness: 0.25, position: 0.5 },
479
+ [43, 60],
480
+ "slenthem: low bronze keys over tubes, padded mallet",
481
+ "Javanese gamelan, ambient",
482
+ ),
483
+ gangsa: preset(
484
+ {
485
+ body: "saron",
486
+ ring: 2.2,
487
+ tilt: 0.6,
488
+ hardness: 0.9,
489
+ position: 0.5,
490
+ ombak: 7,
491
+ damp: 0.85,
492
+ release: 0.1,
493
+ },
494
+ [60, 89],
495
+ "Balinese metallophone: pengumbang and pengisep beating at 7 Hz (ombak)",
496
+ "Balinese gamelan, Lopatin, Four Tet",
497
+ ),
498
+ gender: preset(
499
+ { body: "gender", ring: 5, tilt: 0.5, hardness: 0.3, position: 0.5 },
500
+ [48, 84],
501
+ "gender: thin bronze keys over tubes, padded mallets, long ring",
502
+ "Javanese gamelan, ambient",
503
+ ),
504
+ bonang: preset(
505
+ {
506
+ body: "bonang",
507
+ ring: 1.8,
508
+ tilt: 0.7,
509
+ hardness: 0.6,
510
+ position: 0.5,
511
+ damp: 0.7,
512
+ release: 0.12,
513
+ },
514
+ [55, 84],
515
+ "bonang: small kettle gongs in a row, padded sticks",
516
+ "Javanese gamelan, Reich",
517
+ ),
518
+ kenong: preset(
519
+ { body: "bonang", ring: 4, tilt: 0.5, hardness: 0.45, position: 0.5 },
520
+ [55, 72],
521
+ "kenong: large kettle gong that marks the phrase",
522
+ "Javanese gamelan",
523
+ ),
524
+ kethuk: preset(
525
+ {
526
+ body: "bonang",
527
+ ring: 0.5,
528
+ tilt: 0.7,
529
+ hardness: 0.5,
530
+ position: 0.5,
531
+ damp: 1,
532
+ release: 0.08,
533
+ },
534
+ [55, 67],
535
+ "kethuk: small kettle gong, struck and damped (tutupan)",
536
+ "Javanese gamelan",
537
+ ),
538
+ kempul: preset(
539
+ { body: "kempul", ring: 6, tilt: 0.4, hardness: 0.25, position: 0.5 },
540
+ [43, 67],
541
+ "kempul: hanging gong, slow beating",
542
+ "Javanese gamelan",
543
+ ),
544
+ daf: preset(
545
+ {
546
+ body: "frame",
547
+ ring: 0.5,
548
+ tilt: 0.8,
549
+ hardness: 0.6,
550
+ position: 0.85,
551
+ buzz: 0.6,
552
+ },
553
+ [43, 55],
554
+ "Persian frame drum with ring jingles",
555
+ "Persian, Sufi",
556
+ ),
557
+ bodhran: preset(
558
+ {
559
+ body: "frame",
560
+ ring: 0.35,
561
+ tilt: 0.9,
562
+ hardness: 0.5,
563
+ position: 0.8,
564
+ damp: 0.4,
565
+ gain: 2,
566
+ },
567
+ [40, 52],
568
+ "Irish frame drum, tipper",
569
+ "celtic",
570
+ ),
571
+ tabla: preset(
572
+ {
573
+ body: "tabla",
574
+ ring: 1.2,
575
+ tilt: 0.5,
576
+ hardness: 0.7,
577
+ position: 0.75,
578
+ strikebend: 0.4,
579
+ strikedecay: 0.12,
580
+ gain: 2,
581
+ },
582
+ [57, 69],
583
+ "dayan: tuned treble drum with near-harmonic syahi modes",
584
+ "Hindustani, Talvin Singh, Four Tet",
585
+ ),
367
586
  });
368
587
 
369
588
  /** Default preset of a `modal` field that names none. */
@@ -388,6 +607,13 @@ export type TrackModal = Readonly<{
388
607
  strikedecay?: number;
389
608
  gain?: number;
390
609
  body?: ModalBody;
610
+ /**
611
+ * f061-gamelan-winds: the id of this track's partner in an ombak pair.
612
+ * A paired track (the pengisep) plays one voice `ombak` Hz above the
613
+ * pitch instead of its own twin, so it beats against its partner (the
614
+ * pengumbang). Only this track's render changes.
615
+ */
616
+ pair?: string;
391
617
  }>;
392
618
 
393
619
  export function isModalPreset(name: string): name is ModalPresetName {
@@ -425,8 +651,17 @@ export function normalizeModal(input: unknown): TrackModal | undefined {
425
651
  throw new FxValidationError("track modal must be an object or null");
426
652
  const params: Record<string, unknown> = {};
427
653
  let presetName: ModalPresetName | undefined;
654
+ let pair: string | undefined;
428
655
  for (const [key, value] of Object.entries(input)) {
429
656
  if (value === undefined || value === null) continue;
657
+ if (key === "pair") {
658
+ if (typeof value !== "string" || !PAIR_ID.test(value))
659
+ throw new FxValidationError(
660
+ "modal pair must be a track id (letters, digits, - or _)",
661
+ );
662
+ pair = value;
663
+ continue;
664
+ }
430
665
  if (key === "preset") {
431
666
  if (typeof value !== "string")
432
667
  throw new FxValidationError(
@@ -451,6 +686,7 @@ export function normalizeModal(input: unknown): TrackModal | undefined {
451
686
  return Object.freeze({
452
687
  ...(presetName ? { preset: presetName } : {}),
453
688
  ...values,
689
+ ...(pair === undefined ? {} : { pair }),
454
690
  }) as TrackModal;
455
691
  }
456
692
 
@@ -468,7 +704,12 @@ export function modalSettings(
468
704
  if (modal?.mallet) out.hardness = MODAL_MALLETS[modal.mallet];
469
705
  if (modal)
470
706
  for (const [key, value] of Object.entries(modal))
471
- if (key !== "preset" && key !== "mallet" && value !== undefined)
707
+ if (
708
+ key !== "preset" &&
709
+ key !== "mallet" &&
710
+ key !== "pair" &&
711
+ value !== undefined
712
+ )
472
713
  out[key] = value;
473
714
  if (lane)
474
715
  for (const key of MODAL_LANE_PARAMS) {
@@ -496,7 +737,10 @@ export const MAX_RESONATOR_TAIL = 30;
496
737
  /** Voices one modal track sounds at once; the oldest onset is stolen. */
497
738
  export const MAX_RESONATOR_VOICES = 32;
498
739
  /** Joins the stem cache key so mode-table changes invalidate stems. */
499
- export const RESONATOR_TABLE_VERSION = 1;
740
+ export const RESONATOR_TABLE_VERSION = 2;
741
+
742
+ /** A modal `pair` value: a track id. */
743
+ const PAIR_ID = /^[A-Za-z0-9_-]{1,64}$/;
500
744
 
501
745
  /**
502
746
  * A modal track's ring-out after its last note-off, in seconds: the preset
@@ -524,8 +768,29 @@ export function modalTailSeconds(
524
768
  export function instrumentPatchForWord(word: string): {
525
769
  instrument: string;
526
770
  modal?: TrackModal;
771
+ wind?: TrackWind;
772
+ sing?: TrackSing;
527
773
  } {
528
774
  const meaning = resolveInstrumentWord(word);
775
+ // f07-sing: `choir`, `aah`, `khoomei` sing a preset.
776
+ if (meaning?.field === "sing" && meaning.instrument === SING_INSTRUMENT) {
777
+ const preset =
778
+ meaning.preset && isSingPreset(meaning.preset)
779
+ ? meaning.preset
780
+ : undefined;
781
+ return {
782
+ instrument: SING_INSTRUMENT,
783
+ sing: Object.freeze(preset ? { preset } : {}) as TrackSing,
784
+ };
785
+ }
786
+ // f061-gamelan-winds: `flute`, `sax`, `tuba` play a wind preset.
787
+ if (meaning?.field === "wind" && meaning.instrument === WIND_INSTRUMENT) {
788
+ const preset = meaning.preset ? windPresetFor(meaning.preset) : undefined;
789
+ return {
790
+ instrument: WIND_INSTRUMENT,
791
+ wind: Object.freeze(preset ? { preset } : {}) as TrackWind,
792
+ };
793
+ }
529
794
  if (meaning?.field === "modal" && meaning.instrument === MODAL_INSTRUMENT) {
530
795
  const preset = meaning.preset ? modalPresetFor(meaning.preset) : undefined;
531
796
  return {
@@ -538,7 +803,41 @@ export function instrumentPatchForWord(word: string): {
538
803
 
539
804
  /** True for a word that resolves to the modal engine (`vibes`, `gong`). */
540
805
  export function isModalWord(word: string): boolean {
541
- return resolveInstrumentWord(word)?.instrument === MODAL_INSTRUMENT;
806
+ const meaning = resolveInstrumentWord(word);
807
+ return (
808
+ meaning?.instrument === MODAL_INSTRUMENT ||
809
+ (meaning?.field === "wind" && meaning.instrument === WIND_INSTRUMENT) ||
810
+ (meaning?.field === "sing" && meaning.instrument === SING_INSTRUMENT)
811
+ );
812
+ }
813
+
814
+ /**
815
+ * `dawg check` notes for ombak pairs whose partner is missing or is not a
816
+ * modal track: the pengisep then plays ombak Hz sharp with nothing to beat
817
+ * against.
818
+ */
819
+ export function modalPairWarnings(
820
+ tracks: readonly Readonly<{
821
+ id: string;
822
+ instrument: string;
823
+ modal?: Readonly<{ pair?: string }>;
824
+ }>[],
825
+ ): string[] {
826
+ const out: string[] = [];
827
+ for (const track of tracks) {
828
+ const pair = track.modal?.pair;
829
+ if (pair === undefined) continue;
830
+ const partner = tracks.find((candidate) => candidate.id === pair);
831
+ if (!partner)
832
+ out.push(
833
+ `track ${track.id}: modal pair "${pair}" names no track; it plays ombak Hz sharp alone`,
834
+ );
835
+ else if (partner.instrument !== MODAL_INSTRUMENT || !partner.modal)
836
+ out.push(
837
+ `track ${track.id}: modal pair "${pair}" is not a modal track; give it the same preset (modal gangsa)`,
838
+ );
839
+ }
840
+ return out;
542
841
  }
543
842
 
544
843
  /**
@@ -572,3 +871,6 @@ export function legacyResonatorWarnings(
572
871
  }
573
872
  return out;
574
873
  }
874
+
875
+ // f061-gamelan-winds: blown instruments live in core/winds.ts.
876
+ export * from "./winds.ts";
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Cross-track references (0.7). A track field that names another track (the
3
+ * vocoder's `src`, autotune's `from`, 0.7.1 harmony's `guide`) registers
4
+ * here once, at module load, with the edges it reads and how it forgets a
5
+ * removed track. `removeTrack` then drops dangling references, the renderer
6
+ * orders stems with `routingOrder`, and stem digests include the notes a
7
+ * track follows through `notesDigestInputs`.
8
+ *
9
+ * Audio edges carry rendered sound (the target must render first, and a
10
+ * cycle is an error). Notes edges only read the other track's notes, so
11
+ * they never order rendering and never form a cycle.
12
+ */
13
+ import {
14
+ registerTrackRefDrop,
15
+ ScoreValidationError,
16
+ type Note,
17
+ type Track,
18
+ type TrackScore,
19
+ } from "./score.ts";
20
+ import { trackSlug } from "./slug.ts";
21
+
22
+ export type TrackRefKind = "audio" | "notes";
23
+
24
+ /** One reference from a track to another track. */
25
+ export type TrackRef = Readonly<{ trackId: string; kind: TrackRefKind }>;
26
+
27
+ export type TrackRefSpec = Readonly<{
28
+ /** The tracks `track` references through this field. */
29
+ refs(track: Track): readonly TrackRef[];
30
+ /** `track` without its references to `removedId`. */
31
+ drop(track: Track, removedId: string): Track;
32
+ }>;
33
+
34
+ const specs = new Map<string, TrackRefSpec>();
35
+
36
+ /** Registers a referencing field once; registering it again replaces it. */
37
+ export function registerTrackRefs(field: string, spec: TrackRefSpec): void {
38
+ specs.set(field, spec);
39
+ registerTrackRefDrop(field, spec.drop);
40
+ }
41
+
42
+ /** Every reference `track` holds, in field registration order. */
43
+ export function trackRefs(track: Track): readonly TrackRef[] {
44
+ const refs: TrackRef[] = [];
45
+ for (const spec of specs.values()) refs.push(...spec.refs(track));
46
+ return refs;
47
+ }
48
+
49
+ /**
50
+ * Finds a track by id, then by name slug (`Lead Vox` and `lead-vox` both
51
+ * match). Returns undefined when nothing matches.
52
+ */
53
+ export function resolveTrackRef(
54
+ score: TrackScore,
55
+ idOrSlug: string,
56
+ ): Track | undefined {
57
+ const byId = score.tracks.find((track) => track.id === idOrSlug);
58
+ if (byId) return byId;
59
+ const slug = trackSlug(idOrSlug);
60
+ return score.tracks.find((track) => trackSlug(track.name) === slug);
61
+ }
62
+
63
+ /**
64
+ * Track ids in render order: every audio source before the tracks that
65
+ * read it, otherwise score order. References to missing tracks are ignored.
66
+ * A cycle throws a ScoreValidationError that names it (`a -> b -> a`).
67
+ */
68
+ export function routingOrder(score: TrackScore): readonly string[] {
69
+ const ids = new Set(score.tracks.map((track) => track.id));
70
+ const sources = new Map<string, string[]>();
71
+ for (const track of score.tracks)
72
+ sources.set(
73
+ track.id,
74
+ trackRefs(track)
75
+ .filter((ref) => ref.kind === "audio" && ids.has(ref.trackId))
76
+ .map((ref) => ref.trackId),
77
+ );
78
+ const order: string[] = [];
79
+ const state = new Map<string, "visiting" | "done">();
80
+ const stack: string[] = [];
81
+ const visit = (id: string): void => {
82
+ const seen = state.get(id);
83
+ if (seen === "done") return;
84
+ if (seen === "visiting") {
85
+ const cycle = [...stack.slice(stack.indexOf(id)), id];
86
+ throw new ScoreValidationError(
87
+ `track routing has a cycle: ${cycle.join(" -> ")}`,
88
+ "routing-cycle",
89
+ );
90
+ }
91
+ state.set(id, "visiting");
92
+ stack.push(id);
93
+ for (const source of sources.get(id) ?? []) visit(source);
94
+ stack.pop();
95
+ state.set(id, "done");
96
+ order.push(id);
97
+ };
98
+ for (const track of score.tracks) visit(track.id);
99
+ return order;
100
+ }
101
+
102
+ /**
103
+ * The notes a track's render depends on through notes edges, by referenced
104
+ * track id (sorted, deduplicated), for its stem digest. Missing tracks and
105
+ * self references contribute nothing.
106
+ */
107
+ export function notesDigestInputs(
108
+ score: TrackScore,
109
+ trackId: string,
110
+ ): readonly Readonly<{ trackId: string; notes: readonly Note[] }>[] {
111
+ const track = score.tracks.find((candidate) => candidate.id === trackId);
112
+ if (!track) return [];
113
+ const ids = new Set(score.tracks.map((candidate) => candidate.id));
114
+ const followed = [
115
+ ...new Set(
116
+ trackRefs(track)
117
+ .filter(
118
+ (ref) =>
119
+ ref.kind === "notes" &&
120
+ ref.trackId !== trackId &&
121
+ ids.has(ref.trackId),
122
+ )
123
+ .map((ref) => ref.trackId),
124
+ ),
125
+ ].sort();
126
+ return followed.map((id) => ({
127
+ trackId: id,
128
+ notes: score.notes.filter((note) => note.trackId === id),
129
+ }));
130
+ }
131
+
132
+ /**
133
+ * The vocoder's modulator (0.7): `vocoder.src` is an audio edge, so the
134
+ * modulator renders first and removing it drops the `src` (the carrier then
135
+ * plays dry until a new source is set).
136
+ */
137
+ registerTrackRefs("vocoder", {
138
+ refs: (track) =>
139
+ track.vocoder?.src !== undefined
140
+ ? [{ trackId: track.vocoder.src, kind: "audio" }]
141
+ : [],
142
+ drop: (track, removedId) => {
143
+ if (track.vocoder?.src !== removedId) return track;
144
+ const { src: _src, ...rest } = track.vocoder;
145
+ return { ...track, vocoder: rest };
146
+ },
147
+ });
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Registered reference fields (one block per lane).
151
+
152
+ // autotune: `Track.autotune.from` follows another track's notes.
153
+ registerTrackRefs("autotune.from", {
154
+ refs: (track) =>
155
+ track.autotune?.from !== undefined
156
+ ? [{ trackId: track.autotune.from, kind: "notes" }]
157
+ : [],
158
+ drop: (track, removedId) => {
159
+ if (track.autotune?.from !== removedId) return track;
160
+ // Without its melody the track follows its own notes; `to` and the
161
+ // preset stay as the user set them.
162
+ const { from: _from, ...autotune } = track.autotune;
163
+ return { ...track, autotune };
164
+ },
165
+ });