@hraness/dawg 0.5.0 → 0.6.1

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 (124) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/DAWG.md +592 -49
  3. package/README.md +4 -4
  4. package/core/chords.ts +492 -4
  5. package/core/diff.ts +9 -0
  6. package/core/expression.ts +143 -6
  7. package/core/fx.ts +689 -2
  8. package/core/granular.ts +619 -0
  9. package/core/instruments.ts +442 -0
  10. package/core/keys.ts +955 -0
  11. package/core/resonators.ts +862 -0
  12. package/core/score.ts +529 -8
  13. package/core/sdk/eval-child.ts +2 -0
  14. package/core/sdk/print.ts +247 -16
  15. package/core/sdk/sync-instruments.ts +58 -0
  16. package/core/sdk/v1.ts +2417 -40
  17. package/core/sections.ts +44 -10
  18. package/core/strings.ts +1080 -0
  19. package/core/winds.ts +652 -0
  20. package/guides/automation.md +1 -0
  21. package/guides/chords.md +3 -1
  22. package/guides/effects.md +5 -2
  23. package/guides/media.md +1 -1
  24. package/guides/performance.md +3 -0
  25. package/guides/resample.md +26 -0
  26. package/guides/sounds.md +7 -2
  27. package/guides/tempo.md +1 -0
  28. package/package.json +1 -1
  29. package/src/agent/agent.ts +13 -0
  30. package/src/agent/chord-tools.ts +153 -1
  31. package/src/agent/expression-tools.ts +91 -0
  32. package/src/agent/granular-tools.ts +138 -0
  33. package/src/agent/models.ts +4 -4
  34. package/src/agent/ops.ts +42 -2
  35. package/src/agent/preview-tool.ts +7 -1
  36. package/src/agent/resample-tool.ts +131 -0
  37. package/src/agent/tools.ts +726 -17
  38. package/src/agent/xcb-agent.ts +13 -0
  39. package/src/audio/arrange.ts +42 -3
  40. package/src/audio/dsp/bank.ts +233 -0
  41. package/src/audio/dsp/envelope.ts +161 -0
  42. package/src/audio/dsp/fft.ts +6 -0
  43. package/src/audio/dsp/filters.ts +57 -0
  44. package/src/audio/dsp/interp.ts +112 -0
  45. package/src/audio/dsp/modal.ts +684 -0
  46. package/src/audio/dsp/onset.ts +197 -0
  47. package/src/audio/dsp/oversample.ts +202 -0
  48. package/src/audio/dsp/rng.ts +37 -0
  49. package/src/audio/dsp/shape.ts +81 -0
  50. package/src/audio/dsp/shift.ts +256 -0
  51. package/src/audio/dsp/stft.ts +73 -0
  52. package/src/audio/dsp/window.ts +77 -0
  53. package/src/audio/effects/chain.ts +23 -3
  54. package/src/audio/effects/common.ts +14 -0
  55. package/src/audio/effects/convolution.ts +106 -1
  56. package/src/audio/effects/gaze.ts +354 -0
  57. package/src/audio/effects/rig/cab.ts +99 -0
  58. package/src/audio/effects/rig/filters.ts +152 -0
  59. package/src/audio/effects/rig/gate.ts +39 -0
  60. package/src/audio/effects/rig/head.ts +487 -0
  61. package/src/audio/effects/rig/index.ts +170 -0
  62. package/src/audio/effects/rig/section.ts +78 -0
  63. package/src/audio/effects/rig/stomp.ts +238 -0
  64. package/src/audio/engine.ts +56 -6
  65. package/src/audio/fit.ts +447 -0
  66. package/src/audio/granular.ts +995 -0
  67. package/src/audio/instrument-check.ts +137 -0
  68. package/src/audio/instruments.ts +140 -0
  69. package/src/audio/keys/dsp.ts +323 -0
  70. package/src/audio/keys/electric.ts +420 -0
  71. package/src/audio/keys/engine.ts +485 -0
  72. package/src/audio/keys/organ.ts +1335 -0
  73. package/src/audio/keys/piano.ts +476 -0
  74. package/src/audio/keys/sympathetic.ts +127 -0
  75. package/src/audio/live-worker.ts +56 -0
  76. package/src/audio/live.ts +361 -15
  77. package/src/audio/loudness.ts +65 -20
  78. package/src/audio/preview.ts +17 -1
  79. package/src/audio/resample.ts +285 -0
  80. package/src/audio/resonators.ts +295 -0
  81. package/src/audio/sampler.ts +328 -35
  82. package/src/audio/samples.ts +56 -8
  83. package/src/audio/strings/body.ts +263 -0
  84. package/src/audio/strings/bow.ts +656 -0
  85. package/src/audio/strings/engine.ts +615 -0
  86. package/src/audio/strings/loop.ts +119 -0
  87. package/src/audio/strings/measure.test-helpers.ts +200 -0
  88. package/src/audio/strings/pluck.ts +354 -0
  89. package/src/audio/warp.ts +61 -0
  90. package/src/audio/wav.ts +191 -19
  91. package/src/audio/winds/engine.ts +302 -0
  92. package/src/audio/winds/filters.ts +153 -0
  93. package/src/audio/winds/pitch.ts +104 -0
  94. package/src/audio/winds/trim.ts +80 -0
  95. package/src/audio/winds/trims.ts +917 -0
  96. package/src/audio/winds/voice.ts +479 -0
  97. package/src/commands/expression.ts +111 -23
  98. package/src/commands/fit.ts +135 -0
  99. package/src/commands/fx.ts +35 -1
  100. package/src/commands/granular.ts +432 -0
  101. package/src/commands/help.ts +159 -5
  102. package/src/commands/keys.ts +629 -0
  103. package/src/commands/modal.ts +310 -0
  104. package/src/commands/resample.ts +281 -0
  105. package/src/commands/rig.ts +264 -0
  106. package/src/commands/sample.ts +52 -2
  107. package/src/commands/shift.ts +119 -0
  108. package/src/commands/string.ts +201 -0
  109. package/src/commands/strum.ts +473 -0
  110. package/src/commands/time.ts +4 -1
  111. package/src/commands/wind.ts +244 -0
  112. package/src/main.ts +339 -15
  113. package/src/project/check.ts +17 -0
  114. package/src/render.ts +49 -4
  115. package/src/session/presence.ts +16 -3
  116. package/src/tui/audition.ts +2 -2
  117. package/src/tui/granular-menu.ts +278 -0
  118. package/src/tui/menu.ts +934 -9
  119. package/src/tui/modal-menu.ts +145 -0
  120. package/src/tui/performance-menu.ts +42 -0
  121. package/src/tui/play-chords.ts +104 -3
  122. package/src/tui/play-mode.ts +1 -0
  123. package/src/tui/play-session.ts +70 -2
  124. package/src/tui/wind-menu.ts +144 -0
package/core/sdk/print.ts CHANGED
@@ -8,11 +8,26 @@
8
8
  */
9
9
 
10
10
  import { DRUM_VOICES, isDrumInstrument } from "../drums.ts";
11
+ import type { TrackGranular } from "../granular.ts";
11
12
  import { DEFAULT_FIXED_VELOCITY } from "../expression.ts";
12
13
  import type { RhythmRow } from "../euclid.ts";
13
- import { FX_LANES, fxSpec, type FxName, type FxValues } from "../fx.ts";
14
+ import {
15
+ FX_LANES,
16
+ RIG_STAGES,
17
+ fxSpec,
18
+ rigPresetKeys,
19
+ rigPresetOf,
20
+ type FxName,
21
+ type FxValues,
22
+ } from "../fx.ts";
14
23
  import { MASTER_SPECS, MASTER_UNITS, type SongMaster } from "../master.ts";
15
24
  import { midiToPitch } from "../pitch.ts";
25
+ import {
26
+ MODAL_INSTRUMENT,
27
+ MODAL_PARAMS,
28
+ type TrackModal,
29
+ } from "../resonators.ts";
30
+ import { WIND_INSTRUMENT, WIND_PARAMS, type TrackWind } from "../winds.ts";
16
31
  import { rhythmVoicePitch, rowInSync } from "../rhythm.ts";
17
32
  import {
18
33
  BUILTIN_TABLE_PREFIX,
@@ -35,6 +50,7 @@ import {
35
50
  import { trackSlug } from "../slug.ts";
36
51
  import { barStartTick } from "../tempo.ts";
37
52
  import type { Tuning } from "../tuning.ts";
53
+ import { DEFAULT_STRING_PRESET, type TrackString } from "../strings.ts";
38
54
  import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY } from "./v1.ts";
39
55
 
40
56
  const WIDTH = 80;
@@ -137,6 +153,10 @@ const RESERVED = new Set([
137
153
  "every",
138
154
  "sampler",
139
155
  "wavetable",
156
+ "rig",
157
+ "granular",
158
+ "modal",
159
+ "wind",
140
160
  "slices",
141
161
  "euclid",
142
162
  "euclidRot",
@@ -381,22 +401,62 @@ export function printTrack(score: TrackScore, track: Track): string {
381
401
  used.add("note");
382
402
  return printNote(score, note, !kit && !slots, INDENT + INDENT);
383
403
  });
404
+ const grained =
405
+ track.instrument === "granular" && track.granular !== undefined;
406
+ if (grained) used.add("granular");
384
407
  if (track.sampler) used.add("sampler");
385
408
  const wavetable = isWavetableInstrument(track.instrument)
386
409
  ? wavetableOf(track)
387
410
  : undefined;
388
411
  if (wavetable) used.add("wavetable");
412
+ const stringed = track.instrument === "string" && track.string !== undefined;
413
+ if (stringed) used.add("stringed");
414
+ const modalCall =
415
+ track.instrument === MODAL_INSTRUMENT && track.modal
416
+ ? printModal(track.modal, INDENT)
417
+ : undefined;
418
+ if (modalCall?.startsWith("modal(")) used.add("modal");
419
+ const windCall =
420
+ track.instrument === WIND_INSTRUMENT && track.wind
421
+ ? printWind(track.wind, INDENT)
422
+ : undefined;
423
+ if (windCall?.startsWith("wind(")) used.add("wind");
389
424
 
390
425
  const entries: string[] = [
391
426
  `id: ${str(track.id)}`,
392
427
  `name: ${str(track.name)}`,
393
428
  ];
394
- if (track.sampler) {
429
+ if (grained) {
430
+ entries.push(`instrument: ${printGranular(track.granular!, INDENT)}`);
431
+ // The sampler a granular track keeps for `grain off`.
432
+ if (track.sampler)
433
+ entries.push(`sampler: ${printSampler(track.sampler, INDENT)}`);
434
+ } else if (track.sampler) {
395
435
  entries.push(`instrument: ${printSampler(track.sampler, INDENT)}`);
396
436
  } else if (wavetable) {
397
437
  entries.push(`instrument: ${printWavetable(wavetable, INDENT)}`);
438
+ } else if (stringed) {
439
+ entries.push(`instrument: ${printStringed(track.string!, INDENT)}`);
440
+ } else if (modalCall) {
441
+ entries.push(`instrument: ${modalCall}`);
442
+ } else if (windCall) {
443
+ entries.push(`instrument: ${windCall}`);
398
444
  } else entries.push(`instrument: ${str(track.instrument)}`);
399
445
  if (track.kit) entries.push(`kit: ${str(track.kit)}`);
446
+ if (track.granular && !grained)
447
+ entries.push(
448
+ `granular: ${obj(
449
+ [
450
+ ...(track.granular.preset
451
+ ? [["preset", str(track.granular.preset)] as const]
452
+ : []),
453
+ ...granularEntries(track.granular, INDENT),
454
+ ],
455
+ INDENT,
456
+ "granular: ".length,
457
+ 1,
458
+ )}`,
459
+ );
400
460
  if (track.time) {
401
461
  const beats = (ticks: number) => num(ticks / score.ticksPerBeat);
402
462
  const fields: (readonly [string, string])[] = [];
@@ -480,18 +540,31 @@ export function printTrack(score: TrackScore, track: Track): string {
480
540
  )}`,
481
541
  );
482
542
  if (track.fx) {
483
- const effects = Object.entries(track.fx) as [FxName, FxValues][];
543
+ // A whole rig preset prints as `...rig("crunch")` (SDK 1.22.0).
544
+ const rigName = rigPresetOf(track.fx);
545
+ if (rigName) used.add("rig");
546
+ const effects = (Object.entries(track.fx) as [FxName, FxValues][]).filter(
547
+ ([effect]) =>
548
+ !rigName ||
549
+ !(
550
+ (RIG_STAGES as readonly string[]).includes(effect) ||
551
+ rigPresetKeys(rigName).includes(effect)
552
+ ),
553
+ );
484
554
  const inner = INDENT + INDENT;
485
- const body = effects.map(([effect, values]) => {
486
- // Only values that differ from the default: decoding fills the rest.
487
- const params = Object.entries(fxSpec(effect).params)
488
- .filter(
489
- ([key, spec]) =>
490
- values[key] !== undefined && values[key] !== spec.default,
491
- )
492
- .map(([key]) => [key, value(values[key]!)] as const);
493
- return `${inner}${effect}: ${params.length === 0 ? "{}" : obj(params, inner, `${effect}: `.length, 1)},`;
494
- });
555
+ const rigLine = rigName ? [`${inner}...rig(${str(rigName)}),`] : [];
556
+ const body = rigLine.concat(
557
+ effects.map(([effect, values]) => {
558
+ // Only values that differ from the default: decoding fills the rest.
559
+ const params = Object.entries(fxSpec(effect).params)
560
+ .filter(
561
+ ([key, spec]) =>
562
+ values[key] !== undefined && values[key] !== spec.default,
563
+ )
564
+ .map(([key]) => [key, value(values[key]!)] as const);
565
+ return `${inner}${effect}: ${params.length === 0 ? "{}" : obj(params, inner, `${effect}: `.length, 1)},`;
566
+ }),
567
+ );
495
568
  entries.push(`fx: {\n${body.join("\n")}\n${INDENT}}`);
496
569
  }
497
570
  if (track.synth) {
@@ -506,6 +579,21 @@ export function printTrack(score: TrackScore, track: Track): string {
506
579
  );
507
580
  entries.push(`synth: ${obj(params, INDENT, "synth: ".length, 1)}`);
508
581
  }
582
+ if (track.string && !stringed) {
583
+ const params = Object.entries(track.string).map(
584
+ ([key, v]) => [key, value(v!)] as const,
585
+ );
586
+ entries.push(`string: ${obj(params, INDENT, "string: ".length, 1)}`);
587
+ }
588
+ if (track.keys) {
589
+ // `{}` is meaningful: it turns the modelled piano on.
590
+ const params = Object.entries(track.keys).map(
591
+ ([key, v]) => [key, value(v)] as const,
592
+ );
593
+ entries.push(
594
+ `keys: ${params.length === 0 ? "{}" : obj(params, INDENT, "keys: ".length, 1)}`,
595
+ );
596
+ }
509
597
  entries.push(...performanceEntries(score, track));
510
598
  const lanes: [string, readonly AutomationPoint[] | undefined][] = [
511
599
  ["volume", track.volumeAutomation],
@@ -575,6 +663,11 @@ export function printTrack(score: TrackScore, track: Track): string {
575
663
  "every",
576
664
  "sampler",
577
665
  "wavetable",
666
+ "stringed",
667
+ "rig",
668
+ "granular",
669
+ "modal",
670
+ "wind",
578
671
  "euclid",
579
672
  "grid",
580
673
  ]
@@ -725,13 +818,15 @@ function performanceEntries(score: TrackScore, track: Track): string[] {
725
818
  1,
726
819
  )}`,
727
820
  );
728
- if (track.pedal) {
729
- const events = track.pedal.map(
821
+ for (const key of ["pedal", "softPedal", "sostenuto"] as const) {
822
+ const lane = track[key];
823
+ if (!lane) continue;
824
+ const events = lane.map(
730
825
  (event) =>
731
826
  `[${num(event.tick / score.ticksPerBeat)}, ${str(event.state)}]`,
732
827
  );
733
828
  entries.push(
734
- `pedal: ${list(events, INDENT, "pedal: ".length, 1, events.length > 1)}`,
829
+ `${key}: ${list(events, INDENT, `${key}: `.length, 1, events.length > 1)}`,
735
830
  );
736
831
  }
737
832
  if (track.velocityCurve)
@@ -768,6 +863,20 @@ function performanceEntries(score: TrackScore, track: Track): string[] {
768
863
  )}`,
769
864
  );
770
865
  }
866
+ if (track.guitar) {
867
+ const { tune, capo, hand, ring, position } = track.guitar;
868
+ const fields: [string, string][] = [];
869
+ if (tune !== undefined)
870
+ fields.push([
871
+ "tune",
872
+ typeof tune === "string" ? str(tune) : `[${tune.map(num).join(", ")}]`,
873
+ ]);
874
+ if (capo !== undefined) fields.push(["capo", num(capo)]);
875
+ if (hand !== undefined) fields.push(["hand", num(hand)]);
876
+ if (ring !== undefined) fields.push(["ring", num(ring)]);
877
+ if (position !== undefined) fields.push(["position", num(position)]);
878
+ entries.push(`guitar: ${obj(fields, INDENT, "guitar: ".length, 1)}`);
879
+ }
771
880
  return entries;
772
881
  }
773
882
 
@@ -859,6 +968,108 @@ function printSampler(sampler: Sampler, indent: string): string {
859
968
  return `sampler(\n${inner}${shifted},\n${inner}{ mode: ${str(sampler.mode)} },\n${indent})`;
860
969
  }
861
970
 
971
+ /** `stringed("sitar", { buzz: 0.8 })`: the preset, then its overrides. */
972
+ function printStringed(settings: TrackString, indent: string): string {
973
+ const name = str(settings.preset ?? DEFAULT_STRING_PRESET);
974
+ const params = Object.entries(settings)
975
+ .filter(([key]) => key !== "preset")
976
+ .map(([key, v]) => [key, value(v!)] as const);
977
+ if (params.length === 0) return `stringed(${name})`;
978
+ const inline = `stringed(${name}, { ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
979
+ if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
980
+ return inline;
981
+ const inner = indent + INDENT;
982
+ return `stringed(${name}, {\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
983
+ }
984
+
985
+ /** `[key, literal]` for each stored granular field but the preset. */
986
+ function granularEntries(
987
+ settings: TrackGranular,
988
+ indent: string,
989
+ ): (readonly [string, string])[] {
990
+ return Object.entries(settings)
991
+ .filter(([key]) => key !== "preset")
992
+ .map(([key, v]) => [
993
+ key,
994
+ typeof v === "object"
995
+ ? printSample(v as SampleRef, indent + INDENT, `${key}: `.length)
996
+ : value(v as number | string | boolean),
997
+ ]);
998
+ }
999
+
1000
+ /** `granular("cloud", { scan: 0.1 })`: the preset, then its overrides. */
1001
+ function printGranular(settings: TrackGranular, indent: string): string {
1002
+ const inner = indent + INDENT;
1003
+ const params = granularEntries(settings, indent);
1004
+ const name = settings.preset ? str(settings.preset) : undefined;
1005
+ if (params.length === 0) return name ? `granular(${name})` : "granular()";
1006
+ const head = name ? `granular(${name}, ` : "granular(";
1007
+ const inline = `${head}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
1008
+ if (
1009
+ !inline.includes("\n") &&
1010
+ indent.length + "instrument: ".length + inline.length + 1 <= WIDTH
1011
+ )
1012
+ return inline;
1013
+ return `${head}{\n${params.map(([k, v]) => property(k, v, inner)).join("\n")}\n${indent}})`;
1014
+ }
1015
+
1016
+ /**
1017
+ * A modal track's instrument: the preset word alone (`"vibes"`) when there
1018
+ * are no overrides, else `modal("vibes", { motor: 4 })` with keys in
1019
+ * `MODAL_PARAMS` order. `marimba` is a legacy word, so the modal marimba
1020
+ * always prints as `modal("marimba")`.
1021
+ */
1022
+ function printModal(settings: TrackModal, indent: string): string {
1023
+ const params: [string, string][] = [];
1024
+ for (const key of Object.keys(MODAL_PARAMS) as (keyof TrackModal)[]) {
1025
+ const value = settings[key];
1026
+ if (value === undefined) continue;
1027
+ params.push([key, typeof value === "number" ? num(value) : str(value)]);
1028
+ }
1029
+ if (settings.pair !== undefined) params.push(["pair", str(settings.pair)]);
1030
+ const preset = settings.preset;
1031
+ if (params.length === 0 && preset && preset !== "marimba") return str(preset);
1032
+ const head = preset ? str(preset) : "";
1033
+ if (params.length === 0) return `modal(${head})`;
1034
+ const lead = head ? `${head}, ` : "";
1035
+ const inline = `modal(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
1036
+ if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
1037
+ return inline;
1038
+ const inner = indent + INDENT;
1039
+ return `modal(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
1040
+ }
1041
+
1042
+ /**
1043
+ * A wind track's instrument: the preset word alone (`"flute"`) when there
1044
+ * are no overrides, else `wind("sax", { breath: 0.8 })` with keys in
1045
+ * `WIND_PARAMS` order. Every wind preset word is also an instrument word.
1046
+ */
1047
+ function printWind(settings: TrackWind, indent: string): string {
1048
+ const params: [string, string][] = [];
1049
+ for (const key of Object.keys(WIND_PARAMS)) {
1050
+ const value = (settings as Record<string, unknown>)[key];
1051
+ if (value === undefined) continue;
1052
+ params.push([
1053
+ key,
1054
+ typeof value === "number"
1055
+ ? num(value)
1056
+ : typeof value === "boolean"
1057
+ ? String(value)
1058
+ : str(String(value)),
1059
+ ]);
1060
+ }
1061
+ const preset = settings.preset;
1062
+ if (params.length === 0 && preset) return str(preset);
1063
+ const head = preset ? str(preset) : "";
1064
+ if (params.length === 0) return `wind(${head})`;
1065
+ const lead = head ? `${head}, ` : "";
1066
+ const inline = `wind(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
1067
+ if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
1068
+ return inline;
1069
+ const inner = indent + INDENT;
1070
+ return `wind(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
1071
+ }
1072
+
862
1073
  /** `wavetable("basic", { wt: 0.5 })`; pack tables keep their pin. */
863
1074
  function printWavetable(settings: TrackWavetable, indent: string): string {
864
1075
  const src = settings.table.src;
@@ -915,6 +1126,26 @@ function printSample(ref: SampleRef, indent: string, prefix: number): string {
915
1126
  if (ref.accelerate !== undefined)
916
1127
  entries.push(["accelerate", num(ref.accelerate)]);
917
1128
  if (ref.squiz !== undefined) entries.push(["squiz", num(ref.squiz)]);
1129
+ if (ref.bpm !== undefined) entries.push(["bpm", num(ref.bpm)]);
1130
+ if (ref.fitmode !== undefined) entries.push(["fitmode", str(ref.fitmode)]);
1131
+ if (ref.len !== undefined) entries.push(["len", num(ref.len)]);
1132
+ if (ref.shift !== undefined) entries.push(["shift", num(ref.shift)]);
1133
+ if (ref.formant !== undefined) entries.push(["formant", num(ref.formant)]);
1134
+ if (ref.fadeInTime !== undefined)
1135
+ entries.push(["fadeInTime", num(ref.fadeInTime)]);
1136
+ if (ref.fadeTime !== undefined) entries.push(["fadeTime", num(ref.fadeTime)]);
1137
+ if (ref.from !== undefined) {
1138
+ const from = ref.from;
1139
+ const parts: [string, string][] = [["source", str(from.source)]];
1140
+ if (from.section !== undefined) parts.push(["section", str(from.section)]);
1141
+ if (from.bars !== undefined)
1142
+ parts.push(["bars", `[${num(from.bars[0])}, ${num(from.bars[1])}]`]);
1143
+ parts.push(["score", str(from.score)]);
1144
+ entries.push(["from", obj(parts, indent + INDENT, "from: ".length, 1)]);
1145
+ }
1146
+ if (ref.vel !== undefined)
1147
+ entries.push(["vel", `[${num(ref.vel[0])}, ${num(ref.vel[1])}]`]);
1148
+ if (ref.rr !== undefined) entries.push(["rr", str(ref.rr)]);
918
1149
  if (entries.length === 1) return str(ref.src);
919
1150
  return obj(entries, indent, prefix, 1);
920
1151
  }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The SDK is one vendored file with no imports, so `core/sdk/v1.ts` carries
3
+ * a copy of the instrument word resolver (`core/instruments.ts`) between two
4
+ * marker lines. `bun core/sdk/sync-instruments.ts` rewrites it and
5
+ * `sync-instruments.test.ts` fails when it is stale. Same scheme as
6
+ * `sync-chords.ts`.
7
+ */
8
+
9
+ import { readFile, writeFile } from "node:fs/promises";
10
+ import { join } from "node:path";
11
+ import { format } from "prettier";
12
+
13
+ export const BEGIN_MARKER =
14
+ "// BEGIN instrument words: generated from core/instruments.ts by core/sdk/sync-instruments.ts";
15
+ export const END_MARKER = "// END instrument words";
16
+
17
+ const ROOT = join(import.meta.dir, "..", "..");
18
+ export const INSTRUMENTS_PATH = join(ROOT, "core", "instruments.ts");
19
+ export const SDK_PATH = join(ROOT, "core", "sdk", "v1.ts");
20
+
21
+ /** The resolver block (markers included) for a `core/instruments.ts` source. */
22
+ export function instrumentBlock(source: string): string {
23
+ const start = source.indexOf("// ---- instrument words");
24
+ if (start < 0) throw new Error("core/instruments.ts layout changed");
25
+ const body = source
26
+ .slice(source.indexOf("\n", start) + 1)
27
+ .replace(/^export /gm, "")
28
+ .trim();
29
+ return `${BEGIN_MARKER}\n${body}\n${END_MARKER}`;
30
+ }
31
+
32
+ /** `sdk` with its resolver block replaced, formatted like the repo. */
33
+ export async function syncedSdk(
34
+ sdk: string,
35
+ instruments: string,
36
+ ): Promise<string> {
37
+ const from = sdk.indexOf(BEGIN_MARKER);
38
+ const to = sdk.indexOf(END_MARKER);
39
+ if (from < 0 || to < 0)
40
+ throw new Error("v1.ts has no instrument word markers");
41
+ const next =
42
+ sdk.slice(0, from) +
43
+ instrumentBlock(instruments) +
44
+ sdk.slice(to + END_MARKER.length);
45
+ return format(next, { parser: "typescript", filepath: SDK_PATH });
46
+ }
47
+
48
+ if (import.meta.main) {
49
+ const [sdk, instruments] = await Promise.all([
50
+ readFile(SDK_PATH, "utf8"),
51
+ readFile(INSTRUMENTS_PATH, "utf8"),
52
+ ]);
53
+ const next = await syncedSdk(sdk, instruments);
54
+ if (next !== sdk) await writeFile(SDK_PATH, next);
55
+ console.log(
56
+ next === sdk ? "v1.ts instrument words up to date" : "v1.ts updated",
57
+ );
58
+ }