@hraness/dawg 0.6.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 (93) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/DAWG.md +338 -51
  3. package/README.md +4 -4
  4. package/core/chords.ts +492 -4
  5. package/core/diff.ts +5 -0
  6. package/core/expression.ts +143 -6
  7. package/core/fx.ts +258 -6
  8. package/core/granular.ts +91 -0
  9. package/core/instruments.ts +161 -0
  10. package/core/keys.ts +573 -4
  11. package/core/resonators.ts +291 -3
  12. package/core/score.ts +349 -3
  13. package/core/sdk/print.ts +78 -3
  14. package/core/sdk/v1.ts +1320 -36
  15. package/core/sections.ts +12 -4
  16. package/core/strings.ts +237 -2
  17. package/core/winds.ts +652 -0
  18. package/guides/automation.md +1 -0
  19. package/guides/chords.md +3 -1
  20. package/guides/effects.md +3 -2
  21. package/guides/media.md +1 -1
  22. package/guides/performance.md +3 -0
  23. package/guides/resample.md +26 -0
  24. package/guides/sounds.md +7 -6
  25. package/package.json +1 -1
  26. package/src/agent/agent.ts +8 -2
  27. package/src/agent/chord-tools.ts +153 -1
  28. package/src/agent/expression-tools.ts +91 -0
  29. package/src/agent/ops.ts +4 -1
  30. package/src/agent/preview-tool.ts +2 -1
  31. package/src/agent/resample-tool.ts +131 -0
  32. package/src/agent/tools.ts +200 -15
  33. package/src/agent/xcb-agent.ts +8 -2
  34. package/src/audio/arrange.ts +21 -0
  35. package/src/audio/dsp/envelope.ts +161 -0
  36. package/src/audio/dsp/shift.ts +256 -0
  37. package/src/audio/effects/chain.ts +18 -3
  38. package/src/audio/effects/common.ts +14 -0
  39. package/src/audio/effects/convolution.ts +106 -1
  40. package/src/audio/effects/gaze.ts +354 -0
  41. package/src/audio/engine.ts +46 -5
  42. package/src/audio/fit.ts +45 -0
  43. package/src/audio/granular.ts +352 -21
  44. package/src/audio/instrument-check.ts +9 -5
  45. package/src/audio/instruments.ts +32 -3
  46. package/src/audio/keys/electric.ts +420 -0
  47. package/src/audio/keys/engine.ts +143 -19
  48. package/src/audio/keys/organ.ts +1335 -0
  49. package/src/audio/keys/piano.ts +50 -6
  50. package/src/audio/keys/sympathetic.ts +127 -0
  51. package/src/audio/live-worker.ts +3 -1
  52. package/src/audio/live.ts +136 -9
  53. package/src/audio/preview.ts +15 -1
  54. package/src/audio/resample.ts +285 -0
  55. package/src/audio/resonators.ts +10 -2
  56. package/src/audio/sampler.ts +168 -21
  57. package/src/audio/samples.ts +26 -1
  58. package/src/audio/strings/body.ts +29 -16
  59. package/src/audio/strings/bow.ts +656 -0
  60. package/src/audio/strings/engine.ts +266 -20
  61. package/src/audio/strings/measure.test-helpers.ts +2 -0
  62. package/src/audio/wav.ts +119 -19
  63. package/src/audio/winds/engine.ts +302 -0
  64. package/src/audio/winds/filters.ts +153 -0
  65. package/src/audio/winds/pitch.ts +104 -0
  66. package/src/audio/winds/trim.ts +80 -0
  67. package/src/audio/winds/trims.ts +917 -0
  68. package/src/audio/winds/voice.ts +479 -0
  69. package/src/commands/expression.ts +103 -22
  70. package/src/commands/fx.ts +4 -1
  71. package/src/commands/granular.ts +7 -2
  72. package/src/commands/help.ts +91 -8
  73. package/src/commands/keys.ts +287 -11
  74. package/src/commands/modal.ts +67 -4
  75. package/src/commands/resample.ts +281 -0
  76. package/src/commands/rig.ts +5 -1
  77. package/src/commands/sample.ts +49 -2
  78. package/src/commands/shift.ts +119 -0
  79. package/src/commands/string.ts +28 -2
  80. package/src/commands/strum.ts +473 -0
  81. package/src/commands/wind.ts +244 -0
  82. package/src/main.ts +129 -7
  83. package/src/project/check.ts +5 -1
  84. package/src/render.ts +16 -0
  85. package/src/session/presence.ts +16 -3
  86. package/src/tui/audition.ts +1 -1
  87. package/src/tui/menu.ts +637 -35
  88. package/src/tui/modal-menu.ts +34 -7
  89. package/src/tui/performance-menu.ts +42 -0
  90. package/src/tui/play-chords.ts +104 -3
  91. package/src/tui/play-mode.ts +1 -0
  92. package/src/tui/play-session.ts +34 -9
  93. package/src/tui/wind-menu.ts +144 -0
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { EnumParam, NumberParam } from "../../core/params.ts";
7
7
  import {
8
+ GAMELAN_PRESETS,
8
9
  MODAL_INSTRUMENT,
9
10
  MODAL_MALLET_NAMES,
10
11
  MODAL_PARAMS,
@@ -12,6 +13,7 @@ import {
12
13
  MODAL_PRESET_NAMES,
13
14
  MODAL_SIMPLE,
14
15
  modalSettings,
16
+ type ModalPresetName,
15
17
  } from "../../core/resonators.ts";
16
18
  import type { Track } from "../../core/score.ts";
17
19
  import { num, specStep, type MenuNode } from "./menu.ts";
@@ -24,13 +26,38 @@ export function malletsMenu(): MenuNode {
24
26
  label: "Mallets and bells",
25
27
  detail: `${MODAL_PRESET_NAMES.length} modal presets · marimba, vibes, gong …`,
26
28
  help: "struck bars, bells and bowls on the modal resonator engine",
27
- build: () =>
28
- MODAL_PRESET_NAMES.map((name): MenuNode => ({
29
- kind: "action",
30
- label: `${name} ${MODAL_PRESETS[name].doc}`,
31
- command: `modal ${name}`,
32
- help: MODAL_PRESETS[name].styles,
33
- })),
29
+ build: () => [
30
+ // The core presets keep their rows (gong too); the gamelan bronzes
31
+ // added in 0.6.1 sit in their own sub-group.
32
+ ...MODAL_PRESET_NAMES.filter(
33
+ (name) => name === "gong" || !GAMELAN_PRESETS.includes(name),
34
+ ).map(presetRow),
35
+ gamelanMenu(),
36
+ ],
37
+ };
38
+ }
39
+
40
+ function presetRow(name: ModalPresetName): MenuNode {
41
+ return {
42
+ kind: "action",
43
+ label: `${name} ${MODAL_PRESETS[name].doc}`,
44
+ command: `modal ${name}`,
45
+ help: MODAL_PRESETS[name].styles,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Mallets and bells › Gamelan (f061-gamelan-winds): the Javanese and
51
+ * Balinese bronzes. `modal gamelan` points here.
52
+ */
53
+ export function gamelanMenu(): MenuNode {
54
+ return {
55
+ kind: "menu",
56
+ id: "gamelan",
57
+ label: "Gamelan",
58
+ detail: `${GAMELAN_PRESETS.length} bronzes · saron, gangsa (ombak), gong …`,
59
+ help: "Javanese and Balinese gamelan; pair two gangsa tracks with modal pair, tune with tuning slendro or pelog",
60
+ build: () => GAMELAN_PRESETS.map(presetRow),
34
61
  };
35
62
  }
36
63
 
@@ -12,6 +12,7 @@ import {
12
12
  VELOCITY_CURVES,
13
13
  } from "../../core/expression.ts";
14
14
  import type { Note, Track } from "../../core/score.ts";
15
+ import { isPianoFamily } from "../../core/keys.ts";
15
16
  import {
16
17
  BEND_SHAPES,
17
18
  DEFAULT_HUMANIZE,
@@ -50,12 +51,50 @@ export function performanceDetail(track: Track | undefined): string {
50
51
  const parts = [
51
52
  track.glide ? `glide ${track.glide.mode}` : "",
52
53
  track.pedal?.length ? "pedal" : "",
54
+ track.softPedal?.length ? "soft" : "",
55
+ track.sostenuto?.length ? "sost" : "",
53
56
  track.velocityCurve ? `vel ${track.velocityCurve.curve}` : "",
54
57
  track.humanize ? "humanized" : "",
55
58
  ].filter(Boolean);
56
59
  return parts.length ? parts.join(" · ") : "as written";
57
60
  }
58
61
 
62
+ /** Soft pedal and Sostenuto rows: off, the current lane, or the whole loop. */
63
+ function pianoPedalNodes(track: Track): MenuNode[] {
64
+ const rows = [
65
+ {
66
+ lane: "soft",
67
+ label: "soft pedal",
68
+ events: track.softPedal,
69
+ help: "una corda: the hammers shift to strike fewer strings, softer and darker; read at each note's onset",
70
+ },
71
+ {
72
+ lane: "sost",
73
+ label: "sostenuto",
74
+ events: track.sostenuto,
75
+ help: "holds only the keys already down when it presses (the middle pedal); later notes damp normally",
76
+ },
77
+ ] as const;
78
+ return rows
79
+ .filter((row) => isPianoFamily(track.instrument) || row.events?.length)
80
+ .map((row): MenuNode => {
81
+ const value = row.events?.length ? `${row.events.length} events` : "off";
82
+ return {
83
+ kind: "choice",
84
+ label: row.label,
85
+ value,
86
+ options: ["off", ...(value === "off" ? [] : [value]), "bars"],
87
+ command: (option) =>
88
+ option === "bars"
89
+ ? `pedal ${row.lane} bars`
90
+ : option === "off"
91
+ ? `pedal ${row.lane} off`
92
+ : `pedal ${row.lane}`,
93
+ help: row.help,
94
+ };
95
+ });
96
+ }
97
+
59
98
  export function performanceNodes(context: MenuContext): MenuNode[] {
60
99
  const track = context.score.tracks.find(
61
100
  (item) => item.id === context.trackId,
@@ -148,6 +187,9 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
148
187
  : "pedal",
149
188
  help: "bars replaces the lane with a re-pedal on each downbeat; in play mode press Tab (latch) or hold Shift while recording",
150
189
  },
190
+ // keys-electric (0.6.1): the piano's other two pedals, on modelled piano
191
+ // tracks (or wherever a lane is already set).
192
+ ...pianoPedalNodes(track),
151
193
  {
152
194
  kind: "choice",
153
195
  label: "velocity curve",
@@ -35,6 +35,10 @@ import {
35
35
  EXTENSIONS,
36
36
  MAX_VOICING_STEP,
37
37
  PERFORM_MODES,
38
+ DEFAULT_STROKE_SPEED,
39
+ STROKE_PATTERN_NAMES,
40
+ strokeGrid,
41
+ type PerformOptions,
38
42
  PROGRESSION_PRESETS,
39
43
  PROGRESSION_STYLES,
40
44
  SPREADS,
@@ -100,6 +104,10 @@ export type ChordSettings = {
100
104
  /** `none` or a PROGRESSION_PRESETS name; drives the "next" suggestion. */
101
105
  preset: string;
102
106
  style: ProgressionStyle;
107
+ /** Guitar perform mode: a STROKE_PATTERNS name or a D U d u x - . grid. */
108
+ strokes: string;
109
+ /** Guitar perform mode: ms a full six-string down stroke takes. */
110
+ speed: number;
103
111
  };
104
112
 
105
113
  export function defaultChordSettings(): ChordSettings {
@@ -116,9 +124,45 @@ export function defaultChordSettings(): ChordSettings {
116
124
  sevenths: false,
117
125
  preset: "none",
118
126
  style: "pop",
127
+ strokes: "down",
128
+ speed: DEFAULT_STROKE_SPEED * 1000,
119
129
  };
120
130
  }
121
131
 
132
+ /** Play mode's `[` / `]` strum speed step in ms. */
133
+ export const STROKE_SPEED_STEP = 5;
134
+
135
+ /** Strum speed bounds in ms (0 = every string at once). */
136
+ export const STROKE_SPEED_MS = Object.freeze({ min: 0, max: 200 });
137
+
138
+ /**
139
+ * A strum speed from `/chords speed` text: milliseconds (`22`, `22ms`) or
140
+ * beats (`1/32b`, `0.05b`) at `bpm`. Undefined when not a speed.
141
+ */
142
+ export function parseStrokeSpeed(
143
+ text: string | undefined,
144
+ bpm: number,
145
+ ): number | undefined {
146
+ const raw = (text ?? "").trim().toLowerCase();
147
+ const beats = /^(\d+(?:\.\d+)?)(?:\/(\d+))?\s*(?:b|beats?)$/.exec(raw);
148
+ let ms: number;
149
+ if (beats) {
150
+ const value = Number(beats[1]) / (beats[2] ? Number(beats[2]) : 1);
151
+ ms = (value * 60_000) / bpm;
152
+ } else {
153
+ const plain = /^(\d+(?:\.\d+)?)\s*(?:ms)?$/.exec(raw);
154
+ if (!plain) return undefined;
155
+ ms = Number(plain[1]);
156
+ }
157
+ if (
158
+ !Number.isFinite(ms) ||
159
+ ms < STROKE_SPEED_MS.min ||
160
+ ms > STROKE_SPEED_MS.max
161
+ )
162
+ return undefined;
163
+ return Math.round(ms * 10) / 10;
164
+ }
165
+
122
166
  /**
123
167
  * Tracks where auto chords are the default: anything pitched that is not a
124
168
  * bass, a drum kit or a one-shot sampler.
@@ -236,6 +280,16 @@ export class ChordPad {
236
280
  s.perform = PERFORM_MODES[(index + 1) % PERFORM_MODES.length]!;
237
281
  return { type: "status", status: `perform ${s.perform}` };
238
282
  }
283
+ if ((value === "[" || value === "]") && s.perform === "guitar") {
284
+ // Strum speed: slower ([) or faster (]) in 5 ms steps.
285
+ const wanted =
286
+ s.speed + (value === "[" ? STROKE_SPEED_STEP : -STROKE_SPEED_STEP);
287
+ s.speed = Math.max(
288
+ STROKE_SPEED_MS.min,
289
+ Math.min(STROKE_SPEED_MS.max, Math.round(wanted)),
290
+ );
291
+ return { type: "status", status: `strum speed ${s.speed}ms` };
292
+ }
239
293
  if (value === "b") {
240
294
  const index = BASS_MODES.indexOf(s.bass);
241
295
  s.bass = BASS_MODES[(index + 1) % BASS_MODES.length]!;
@@ -392,6 +446,9 @@ export class ChordPad {
392
446
  { key: "0", label: "clear", on: false },
393
447
  { key: "-=", label: `voicing ${signed(s.inversion)}`, on: false },
394
448
  { key: "9", label: perform, on: false },
449
+ ...(s.perform === "guitar"
450
+ ? [{ key: "[]", label: `speed ${s.speed}ms`, on: false }]
451
+ : []),
395
452
  { key: "b", label: `bass ${s.bass}`, on: false },
396
453
  { key: "n", label: "next", on: false },
397
454
  { key: "q", label: s.mode === "auto" ? "auto" : "manual", on: false },
@@ -412,6 +469,8 @@ export class ChordPad {
412
469
  if (s.spread !== "close") parts.push(s.spread);
413
470
  if (s.perform === "pattern")
414
471
  parts.push(`pattern ${patternLabel(s.pattern)}`);
472
+ else if (s.perform === "guitar")
473
+ parts.push(`guitar ${s.strokes} ${s.speed}ms`);
415
474
  else if (s.perform !== "block") parts.push(s.perform);
416
475
  if (s.bass !== "off") parts.push(`bass ${s.bass}`);
417
476
  return parts.join(" · ");
@@ -435,15 +494,18 @@ function signed(value: number): string {
435
494
  /**
436
495
  * `/chords …`: `auto|manual|off`, `voicing <-12..12>`, `spread <close|open|
437
496
  * wide>`, `bass off|chords|unison|single|solo` (`on` = chords), `perform
438
- * <mode>`, `pattern <1..13|name>`, `rate <grid|1/8…>`, `octaves
497
+ * <mode>`, `pattern <1..13|name>`, `strokes <name|D-DU-UDU>` (guitar
498
+ * mode), `speed <ms|1/32b>`, `rate <grid|1/8…>`, `octaves
439
499
  * <1..4>`, `sevenths on|off`, `preset <name|none>`, `style <pop|jazz|…>`.
440
500
  * Mutates `settings`; returns the receipt or an error.
441
501
  */
442
502
  export function applyChordsCommand(
443
503
  settings: ChordSettings,
444
504
  argument: string,
505
+ bpm = 120,
445
506
  ): { ok: boolean; message: string } {
446
- const words = argument.trim().toLowerCase().split(/\s+/).filter(Boolean);
507
+ const raw = argument.trim().split(/\s+/).filter(Boolean);
508
+ const words = raw.map((word) => word.toLowerCase());
447
509
  const [head, value] = words;
448
510
  const done = (message: string) => ({ ok: true, message });
449
511
  const bad = (message: string) => ({ ok: false, message });
@@ -505,6 +567,28 @@ export function applyChordsCommand(
505
567
  return bad(`rate takes ${ARP_RATES.join("|")}`);
506
568
  settings.rate = value as ArpRate;
507
569
  return done(`chords rate ${value}`);
570
+ case "strokes": {
571
+ // Case matters: D is a full down stroke, d a light one.
572
+ const grid = raw.slice(1).join("");
573
+ if (!strokeGrid(grid))
574
+ return bad(
575
+ `strokes takes ${STROKE_PATTERN_NAMES.join("|")} or a grid of D U d u x - .`,
576
+ );
577
+ settings.strokes = (STROKE_PATTERN_NAMES as readonly string[]).includes(
578
+ grid.toLowerCase(),
579
+ )
580
+ ? grid.toLowerCase()
581
+ : grid;
582
+ settings.perform = "guitar";
583
+ return done(`chords strokes ${settings.strokes}`);
584
+ }
585
+ case "speed": {
586
+ const ms = parseStrokeSpeed(words.slice(1).join(""), bpm);
587
+ if (ms === undefined)
588
+ return bad("speed takes 0..200 ms (22, 22ms) or beats (1/32b)");
589
+ settings.speed = ms;
590
+ return done(`chords speed ${ms}ms`);
591
+ }
508
592
  case "octaves": {
509
593
  const n = Number(value);
510
594
  if (!Number.isInteger(n) || n < 1 || n > 4)
@@ -526,7 +610,7 @@ export function applyChordsCommand(
526
610
  return done(`chords style ${value}`);
527
611
  default:
528
612
  return bad(
529
- "/chords auto|manual|off · voicing · spread · bass · perform · pattern · rate · octaves · sevenths · preset · style",
613
+ "/chords auto|manual|off · voicing · spread · bass · perform · pattern · strokes · speed · rate · octaves · sevenths · preset · style",
530
614
  );
531
615
  }
532
616
  }
@@ -539,6 +623,8 @@ export function chordsSummary(settings: ChordSettings): string {
539
623
  s.spread,
540
624
  s.perform,
541
625
  `pattern ${patternLabel(s.pattern)}`,
626
+ `strokes ${s.strokes}`,
627
+ `speed ${s.speed}ms`,
542
628
  `rate ${s.rate}`,
543
629
  `octaves ${s.octaves}`,
544
630
  `bass ${s.bass}`,
@@ -603,6 +689,7 @@ export function chordPhrase(
603
689
  seed: 1,
604
690
  velocity: 0.8,
605
691
  ...(mode === "pattern" ? { pattern: settings.pattern } : {}),
692
+ ...(mode === "guitar" ? guitarPerform(settings, score, track) : {}),
606
693
  },
607
694
  });
608
695
  // With chords off a key plays one note: the root, in the chord's octave.
@@ -630,3 +717,17 @@ export function chordPhrase(
630
717
  }
631
718
  return notes;
632
719
  }
720
+
721
+ /** Perform options for the guitar mode: strokes, speed, tempo and fretting. */
722
+ export function guitarPerform(
723
+ settings: Pick<ChordSettings, "strokes" | "speed">,
724
+ score: Pick<TrackScore, "tempoBpm">,
725
+ track: Pick<Track, "guitar"> | undefined,
726
+ ): Partial<PerformOptions> {
727
+ return {
728
+ strokes: settings.strokes,
729
+ speed: settings.speed / 1000,
730
+ tempo: score.tempoBpm,
731
+ ...(track?.guitar ? { guitar: track.guitar } : {}),
732
+ };
733
+ }
@@ -12,6 +12,7 @@
12
12
  * Shift sustain while held Tab sustain latch Esc leave the mode
13
13
  * (while recording, sustain changes record as track pedal events)
14
14
  * I scale degrees: the home row plays the key's scale (any tuning)
15
+ * [ / ] strum slower / faster (chords perform guitar, 5 ms steps)
15
16
  *
16
17
  * Terminals report key-down only, so a held key is synthesized from the
17
18
  * keyboard's auto-repeat: a press sounds for the gate (one grid step by
@@ -59,12 +59,14 @@ import {
59
59
  DEFAULT_STRUM,
60
60
  perform,
61
61
  type PerformMode,
62
+ type PerformOptions,
62
63
  type PerformedNote,
63
64
  } from "../../core/chords.ts";
64
65
  import {
65
66
  ChordPad,
66
67
  chordCapable,
67
68
  defaultChordSettings,
69
+ guitarPerform,
68
70
  songKey,
69
71
  type ChordSettings,
70
72
  type PlayedChord,
@@ -181,6 +183,8 @@ export type RecordedChord = Readonly<{
181
183
  seed: number;
182
184
  /** CHORD_PATTERNS name when `mode` is `pattern`. */
183
185
  pattern?: string;
186
+ /** Guitar mode: strokes, speed, tempo, fretting and the chord root. */
187
+ guitar?: Partial<PerformOptions>;
184
188
  }>;
185
189
 
186
190
  /** A chord sounding live: its voices start and stop from `tick()`. */
@@ -643,16 +647,21 @@ export class PlaySession {
643
647
  // The full pass runs on a worker, in key order, so a strummed chord or a
644
648
  // fast run never stalls key handling behind an oversampled render.
645
649
  if (pcm.partial)
646
- void this.fullRenderer.full(synth, request).then((full) => {
647
- // Not after note-off: a swap would cancel the release fade.
648
- if (
649
- full &&
650
- this.windows.get(id) === false &&
651
- engine.voicePosition?.(id) !== undefined
650
+ void this.fullRenderer
651
+ .full(
652
+ synth,
653
+ pcm.clock === undefined ? request : { ...request, clock: pcm.clock },
652
654
  )
653
- engine.noteOn(id, full);
654
- this.windows.delete(id);
655
- });
655
+ .then((full) => {
656
+ // Not after note-off: a swap would cancel the release fade.
657
+ if (
658
+ full &&
659
+ this.windows.get(id) === false &&
660
+ engine.voicePosition?.(id) !== undefined
661
+ )
662
+ engine.noteOn(id, full);
663
+ this.windows.delete(id);
664
+ });
656
665
  }
657
666
 
658
667
  private release(ids: readonly number[], atMs: number): void {
@@ -729,6 +738,20 @@ export class PlaySession {
729
738
  octaves: settings.octaves,
730
739
  seed,
731
740
  ...(mode === "pattern" ? { pattern: settings.pattern } : {}),
741
+ ...(mode === "guitar" ? { guitar: this.guitarOptions(played) } : {}),
742
+ };
743
+ }
744
+
745
+ /** Guitar-mode perform options for a played chord (strokes, speed, frets). */
746
+ private guitarOptions(played: PlayedChord): Partial<PerformOptions> {
747
+ return {
748
+ ...guitarPerform(
749
+ this.chords.settings,
750
+ { tempoBpm: this.currentBpm() },
751
+ this.trackData(),
752
+ ),
753
+ root: played.chord.root,
754
+ ...(played.chord.bass !== undefined ? { slash: played.chord.bass } : {}),
732
755
  };
733
756
  }
734
757
 
@@ -749,6 +772,7 @@ export class PlaySession {
749
772
  seed: note.id,
750
773
  pattern: settings.pattern,
751
774
  velocity: 1,
775
+ ...(mode === "guitar" ? this.guitarOptions(played) : {}),
752
776
  });
753
777
  if (played.bass !== undefined)
754
778
  plan.push({
@@ -1213,6 +1237,7 @@ export function chordNotes(
1213
1237
  seed: chord.seed,
1214
1238
  ...(chord.pattern !== undefined ? { pattern: chord.pattern } : {}),
1215
1239
  velocity: 1,
1240
+ ...chord.guitar,
1216
1241
  }).map((note) => ({
1217
1242
  pitch: note.pitch,
1218
1243
  velocity: scaleVelocity(press.velocity, note.velocity),
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Ctrl-k menu rows for the wind engine (0.6.1): the "Winds and brass" group
3
+ * under Sound › browse sounds, and the WIND_PARAMS rows under Sound ›
4
+ * Parameters on a wind-engine track. Every row runs a `wind …` command.
5
+ */
6
+ import type { EnumParam, NumberParam } from "../../core/params.ts";
7
+ import type { Track } from "../../core/score.ts";
8
+ import {
9
+ DEFAULT_WIND_PRESET,
10
+ WIND_GROUPS,
11
+ WIND_INSTRUMENT,
12
+ WIND_PARAMS,
13
+ WIND_PRESETS,
14
+ WIND_PRESET_NAMES,
15
+ WIND_SIMPLE,
16
+ windSettings,
17
+ type WindPresetName,
18
+ } from "../../core/winds.ts";
19
+ import { num, specStep, type MenuNode } from "./menu.ts";
20
+
21
+ function presetRow(name: WindPresetName): MenuNode {
22
+ return {
23
+ kind: "action",
24
+ label: `${name.padEnd(13)} ${WIND_PRESETS[name].doc}`,
25
+ command: `wind ${name}`,
26
+ help: WIND_PRESETS[name].styles,
27
+ };
28
+ }
29
+
30
+ /** Sound › browse sounds › Winds and brass: flutes, reeds, brass. */
31
+ export function windsMenu(): MenuNode {
32
+ return {
33
+ kind: "menu",
34
+ id: "winds",
35
+ label: "Winds and brass",
36
+ detail: `${WIND_PRESET_NAMES.length} blown · flute, sax, trumpet …`,
37
+ help: "breath-driven waveguides: flutes, reeds and brass; wind players 4 for a section",
38
+ build: () =>
39
+ WIND_GROUPS.map((group): MenuNode => ({
40
+ kind: "menu",
41
+ id: `winds:${group.label.toLowerCase()}`,
42
+ label: group.label,
43
+ detail: group.presets.slice(0, 3).join(", ") + " …",
44
+ help: `${group.label.toLowerCase()} on the wind engine`,
45
+ build: () => group.presets.map(presetRow),
46
+ })),
47
+ };
48
+ }
49
+
50
+ function numberRow(
51
+ name: string,
52
+ spec: NumberParam,
53
+ value: number,
54
+ fallback: number,
55
+ ): MenuNode {
56
+ return {
57
+ kind: "number",
58
+ label: name,
59
+ value,
60
+ min: spec.min,
61
+ max: spec.max,
62
+ step: specStep(spec),
63
+ format: (v) => (spec.unit ? `${num(v)} ${spec.unit}` : num(v)),
64
+ command: (v) => `wind ${name} ${num(v)}`,
65
+ reset: `wind ${name} off`,
66
+ help: `${spec.doc} · preset ${num(fallback)} · x resets`,
67
+ };
68
+ }
69
+
70
+ /** Sound › Parameters rows on a wind-engine track. */
71
+ export function windParameterNodes(track: Track): MenuNode[] {
72
+ if (track.instrument !== WIND_INSTRUMENT || !track.wind) return [];
73
+ const wind = track.wind;
74
+ const preset = WIND_PRESETS[wind.preset ?? DEFAULT_WIND_PRESET].settings;
75
+ const resolved = windSettings(wind) as Readonly<
76
+ Record<string, number | string | boolean>
77
+ >;
78
+ const fallback = preset as Readonly<
79
+ Record<string, number | string | boolean>
80
+ >;
81
+ const row = (name: string): MenuNode | undefined => {
82
+ const spec = WIND_PARAMS[name]!;
83
+ const own = (wind as Record<string, unknown>)[name];
84
+ if (spec.kind === "enum")
85
+ return {
86
+ kind: "choice",
87
+ label: name,
88
+ value: own === undefined ? "preset" : String(own),
89
+ options: ["preset", ...(spec as EnumParam).values],
90
+ command: (option) =>
91
+ option === "preset" ? `wind ${name} unset` : `wind ${name} ${option}`,
92
+ help: `${spec.doc} · preset ${String(fallback[name])}`,
93
+ };
94
+ if (spec.kind === "boolean")
95
+ return {
96
+ kind: "choice",
97
+ label: name,
98
+ value: own === undefined ? "preset" : own ? "on" : "off",
99
+ options: ["preset", "on", "off"],
100
+ command: (option) =>
101
+ option === "preset" ? `wind ${name} unset` : `wind ${name} ${option}`,
102
+ help: `${spec.doc} · preset ${fallback[name] ? "on" : "off"}`,
103
+ };
104
+ return numberRow(
105
+ name,
106
+ spec,
107
+ Number(resolved[name]),
108
+ Number(fallback[name]),
109
+ );
110
+ };
111
+ const nodes: MenuNode[] = [
112
+ {
113
+ kind: "choice",
114
+ label: "preset",
115
+ value: wind.preset ?? DEFAULT_WIND_PRESET,
116
+ options: WIND_PRESET_NAMES,
117
+ command: (option) => `wind preset ${option}`,
118
+ help: "the instrument: bore, exciter, range and mute",
119
+ },
120
+ ];
121
+ for (const name of WIND_SIMPLE) {
122
+ const node = row(name);
123
+ if (node) nodes.push(node);
124
+ }
125
+ const rest = Object.keys(WIND_PARAMS).filter(
126
+ (name) => !(WIND_SIMPLE as readonly string[]).includes(name),
127
+ );
128
+ nodes.push({
129
+ kind: "menu",
130
+ id: "wind:advanced",
131
+ label: "advanced",
132
+ detail: rest.join(", "),
133
+ help: "every wind parameter",
134
+ build: () => rest.flatMap((name) => row(name) ?? []),
135
+ });
136
+ if (Object.keys(wind).some((key) => key !== "preset"))
137
+ nodes.push({
138
+ kind: "action",
139
+ label: "reset to preset",
140
+ command: "wind reset",
141
+ help: "clear overrides, keep the preset",
142
+ });
143
+ return nodes;
144
+ }