@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
@@ -18,6 +18,7 @@
18
18
  * maximum weight.
19
19
  */
20
20
  import { resolveInstrumentWord } from "./instruments.ts";
21
+ import { WIND_INSTRUMENT, windPresetFor, type TrackWind } from "./winds.ts";
21
22
  import {
22
23
  FxValidationError,
23
24
  isRecord,
@@ -84,6 +85,22 @@ export const MODAL_PRESET_NAMES = Object.freeze([
84
85
  "bowl",
85
86
  "gong",
86
87
  "timpani",
88
+ // f061-gamelan-winds: gamelan, small bells and frame drums (lane A2).
89
+ "crotales",
90
+ "musicbox",
91
+ "toypiano",
92
+ "saron",
93
+ "demung",
94
+ "slenthem",
95
+ "gangsa",
96
+ "gender",
97
+ "bonang",
98
+ "kenong",
99
+ "kethuk",
100
+ "kempul",
101
+ "daf",
102
+ "bodhran",
103
+ "tabla",
87
104
  ] as const);
88
105
  export type ModalPresetName = (typeof MODAL_PRESET_NAMES)[number];
89
106
 
@@ -99,8 +116,29 @@ export const MODAL_ALIASES: Readonly<Record<string, ModalPresetName>> =
99
116
  singingbowl: "bowl",
100
117
  kettledrum: "timpani",
101
118
  tubularbells: "chimes",
119
+ // f061-gamelan-winds. `gamelan` is a browse group, not an alias.
120
+ crotale: "crotales",
121
+ musicalbox: "musicbox",
122
+ framedrum: "bodhran",
102
123
  });
103
124
 
125
+ /**
126
+ * The gamelan presets, in browse order (the `gamelan` hint opens these
127
+ * under Mallets and bells).
128
+ */
129
+ export const GAMELAN_PRESETS: readonly ModalPresetName[] = Object.freeze([
130
+ "saron",
131
+ "demung",
132
+ "slenthem",
133
+ "gangsa",
134
+ "gender",
135
+ "bonang",
136
+ "kenong",
137
+ "kethuk",
138
+ "kempul",
139
+ "gong",
140
+ ]);
141
+
104
142
  const num = (
105
143
  min: number,
106
144
  max: number,
@@ -364,6 +402,186 @@ export const MODAL_PRESETS: Readonly<Record<ModalPresetName, ModalPreset>> =
364
402
  "pedal timpani, felt sticks, a small strike glide",
365
403
  "Beethoven, Brahms, Shostakovich, orchestral",
366
404
  ),
405
+ // f061-gamelan-winds (lane A2): appended; rows above are unchanged.
406
+ // Gamelan ranges follow the instruments' slendro/pelog octaves; tune
407
+ // them with `tuning slendro` or `tuning pelog` (core/tuning.ts).
408
+ crotales: preset(
409
+ {
410
+ body: "crotale",
411
+ ring: 6,
412
+ tilt: 0.4,
413
+ hardness: 0.85,
414
+ position: 0.5,
415
+ gain: 1.6,
416
+ },
417
+ [84, 108],
418
+ "antique cymbals, long shimmering ring",
419
+ "Messiaen, ambient",
420
+ ),
421
+ musicbox: preset(
422
+ {
423
+ body: "musicbox",
424
+ ring: 1.8,
425
+ tilt: 0.7,
426
+ hardness: 1,
427
+ position: 0.8,
428
+ gain: 1.7,
429
+ },
430
+ [60, 100],
431
+ "plucked steel comb",
432
+ "ambient, OPN, A. G. Cook",
433
+ ),
434
+ toypiano: preset(
435
+ {
436
+ body: "toypiano",
437
+ ring: 1,
438
+ tilt: 0.6,
439
+ hardness: 0.9,
440
+ position: 0.6,
441
+ click: 0.3,
442
+ gain: 2,
443
+ },
444
+ [60, 96],
445
+ "hammered toy-piano rods, clanky",
446
+ "hyperpop, A. G. Cook, Four Tet",
447
+ ),
448
+ saron: preset(
449
+ {
450
+ body: "saron",
451
+ ring: 2.5,
452
+ tilt: 0.6,
453
+ hardness: 0.85,
454
+ position: 0.5,
455
+ damp: 0.85,
456
+ release: 0.1,
457
+ },
458
+ [60, 84],
459
+ "saron barung: bronze keys, horn mallet, the balungan melody; damped at the next note (tutupan)",
460
+ "Javanese gamelan, Reich",
461
+ ),
462
+ demung: preset(
463
+ {
464
+ body: "saron",
465
+ ring: 3.2,
466
+ tilt: 0.6,
467
+ hardness: 0.75,
468
+ position: 0.5,
469
+ damp: 0.8,
470
+ release: 0.12,
471
+ },
472
+ [48, 72],
473
+ "demung: the saron an octave lower, fuller",
474
+ "Javanese gamelan",
475
+ ),
476
+ slenthem: preset(
477
+ { body: "gender", ring: 4.5, tilt: 0.5, hardness: 0.25, position: 0.5 },
478
+ [43, 60],
479
+ "slenthem: low bronze keys over tubes, padded mallet",
480
+ "Javanese gamelan, ambient",
481
+ ),
482
+ gangsa: preset(
483
+ {
484
+ body: "saron",
485
+ ring: 2.2,
486
+ tilt: 0.6,
487
+ hardness: 0.9,
488
+ position: 0.5,
489
+ ombak: 7,
490
+ damp: 0.85,
491
+ release: 0.1,
492
+ },
493
+ [60, 89],
494
+ "Balinese metallophone: pengumbang and pengisep beating at 7 Hz (ombak)",
495
+ "Balinese gamelan, Lopatin, Four Tet",
496
+ ),
497
+ gender: preset(
498
+ { body: "gender", ring: 5, tilt: 0.5, hardness: 0.3, position: 0.5 },
499
+ [48, 84],
500
+ "gender: thin bronze keys over tubes, padded mallets, long ring",
501
+ "Javanese gamelan, ambient",
502
+ ),
503
+ bonang: preset(
504
+ {
505
+ body: "bonang",
506
+ ring: 1.8,
507
+ tilt: 0.7,
508
+ hardness: 0.6,
509
+ position: 0.5,
510
+ damp: 0.7,
511
+ release: 0.12,
512
+ },
513
+ [55, 84],
514
+ "bonang: small kettle gongs in a row, padded sticks",
515
+ "Javanese gamelan, Reich",
516
+ ),
517
+ kenong: preset(
518
+ { body: "bonang", ring: 4, tilt: 0.5, hardness: 0.45, position: 0.5 },
519
+ [55, 72],
520
+ "kenong: large kettle gong that marks the phrase",
521
+ "Javanese gamelan",
522
+ ),
523
+ kethuk: preset(
524
+ {
525
+ body: "bonang",
526
+ ring: 0.5,
527
+ tilt: 0.7,
528
+ hardness: 0.5,
529
+ position: 0.5,
530
+ damp: 1,
531
+ release: 0.08,
532
+ },
533
+ [55, 67],
534
+ "kethuk: small kettle gong, struck and damped (tutupan)",
535
+ "Javanese gamelan",
536
+ ),
537
+ kempul: preset(
538
+ { body: "kempul", ring: 6, tilt: 0.4, hardness: 0.25, position: 0.5 },
539
+ [43, 67],
540
+ "kempul: hanging gong, slow beating",
541
+ "Javanese gamelan",
542
+ ),
543
+ daf: preset(
544
+ {
545
+ body: "frame",
546
+ ring: 0.5,
547
+ tilt: 0.8,
548
+ hardness: 0.6,
549
+ position: 0.85,
550
+ buzz: 0.6,
551
+ },
552
+ [43, 55],
553
+ "Persian frame drum with ring jingles",
554
+ "Persian, Sufi",
555
+ ),
556
+ bodhran: preset(
557
+ {
558
+ body: "frame",
559
+ ring: 0.35,
560
+ tilt: 0.9,
561
+ hardness: 0.5,
562
+ position: 0.8,
563
+ damp: 0.4,
564
+ gain: 2,
565
+ },
566
+ [40, 52],
567
+ "Irish frame drum, tipper",
568
+ "celtic",
569
+ ),
570
+ tabla: preset(
571
+ {
572
+ body: "tabla",
573
+ ring: 1.2,
574
+ tilt: 0.5,
575
+ hardness: 0.7,
576
+ position: 0.75,
577
+ strikebend: 0.4,
578
+ strikedecay: 0.12,
579
+ gain: 2,
580
+ },
581
+ [57, 69],
582
+ "dayan: tuned treble drum with near-harmonic syahi modes",
583
+ "Hindustani, Talvin Singh, Four Tet",
584
+ ),
367
585
  });
368
586
 
369
587
  /** Default preset of a `modal` field that names none. */
@@ -388,6 +606,13 @@ export type TrackModal = Readonly<{
388
606
  strikedecay?: number;
389
607
  gain?: number;
390
608
  body?: ModalBody;
609
+ /**
610
+ * f061-gamelan-winds: the id of this track's partner in an ombak pair.
611
+ * A paired track (the pengisep) plays one voice `ombak` Hz above the
612
+ * pitch instead of its own twin, so it beats against its partner (the
613
+ * pengumbang). Only this track's render changes.
614
+ */
615
+ pair?: string;
391
616
  }>;
392
617
 
393
618
  export function isModalPreset(name: string): name is ModalPresetName {
@@ -425,8 +650,17 @@ export function normalizeModal(input: unknown): TrackModal | undefined {
425
650
  throw new FxValidationError("track modal must be an object or null");
426
651
  const params: Record<string, unknown> = {};
427
652
  let presetName: ModalPresetName | undefined;
653
+ let pair: string | undefined;
428
654
  for (const [key, value] of Object.entries(input)) {
429
655
  if (value === undefined || value === null) continue;
656
+ if (key === "pair") {
657
+ if (typeof value !== "string" || !PAIR_ID.test(value))
658
+ throw new FxValidationError(
659
+ "modal pair must be a track id (letters, digits, - or _)",
660
+ );
661
+ pair = value;
662
+ continue;
663
+ }
430
664
  if (key === "preset") {
431
665
  if (typeof value !== "string")
432
666
  throw new FxValidationError(
@@ -451,6 +685,7 @@ export function normalizeModal(input: unknown): TrackModal | undefined {
451
685
  return Object.freeze({
452
686
  ...(presetName ? { preset: presetName } : {}),
453
687
  ...values,
688
+ ...(pair === undefined ? {} : { pair }),
454
689
  }) as TrackModal;
455
690
  }
456
691
 
@@ -468,7 +703,12 @@ export function modalSettings(
468
703
  if (modal?.mallet) out.hardness = MODAL_MALLETS[modal.mallet];
469
704
  if (modal)
470
705
  for (const [key, value] of Object.entries(modal))
471
- if (key !== "preset" && key !== "mallet" && value !== undefined)
706
+ if (
707
+ key !== "preset" &&
708
+ key !== "mallet" &&
709
+ key !== "pair" &&
710
+ value !== undefined
711
+ )
472
712
  out[key] = value;
473
713
  if (lane)
474
714
  for (const key of MODAL_LANE_PARAMS) {
@@ -496,7 +736,10 @@ export const MAX_RESONATOR_TAIL = 30;
496
736
  /** Voices one modal track sounds at once; the oldest onset is stolen. */
497
737
  export const MAX_RESONATOR_VOICES = 32;
498
738
  /** Joins the stem cache key so mode-table changes invalidate stems. */
499
- export const RESONATOR_TABLE_VERSION = 1;
739
+ export const RESONATOR_TABLE_VERSION = 2;
740
+
741
+ /** A modal `pair` value: a track id. */
742
+ const PAIR_ID = /^[A-Za-z0-9_-]{1,64}$/;
500
743
 
501
744
  /**
502
745
  * A modal track's ring-out after its last note-off, in seconds: the preset
@@ -524,8 +767,17 @@ export function modalTailSeconds(
524
767
  export function instrumentPatchForWord(word: string): {
525
768
  instrument: string;
526
769
  modal?: TrackModal;
770
+ wind?: TrackWind;
527
771
  } {
528
772
  const meaning = resolveInstrumentWord(word);
773
+ // f061-gamelan-winds: `flute`, `sax`, `tuba` play a wind preset.
774
+ if (meaning?.field === "wind" && meaning.instrument === WIND_INSTRUMENT) {
775
+ const preset = meaning.preset ? windPresetFor(meaning.preset) : undefined;
776
+ return {
777
+ instrument: WIND_INSTRUMENT,
778
+ wind: Object.freeze(preset ? { preset } : {}) as TrackWind,
779
+ };
780
+ }
529
781
  if (meaning?.field === "modal" && meaning.instrument === MODAL_INSTRUMENT) {
530
782
  const preset = meaning.preset ? modalPresetFor(meaning.preset) : undefined;
531
783
  return {
@@ -538,7 +790,40 @@ export function instrumentPatchForWord(word: string): {
538
790
 
539
791
  /** True for a word that resolves to the modal engine (`vibes`, `gong`). */
540
792
  export function isModalWord(word: string): boolean {
541
- return resolveInstrumentWord(word)?.instrument === MODAL_INSTRUMENT;
793
+ const meaning = resolveInstrumentWord(word);
794
+ return (
795
+ meaning?.instrument === MODAL_INSTRUMENT ||
796
+ (meaning?.field === "wind" && meaning.instrument === WIND_INSTRUMENT)
797
+ );
798
+ }
799
+
800
+ /**
801
+ * `dawg check` notes for ombak pairs whose partner is missing or is not a
802
+ * modal track: the pengisep then plays ombak Hz sharp with nothing to beat
803
+ * against.
804
+ */
805
+ export function modalPairWarnings(
806
+ tracks: readonly Readonly<{
807
+ id: string;
808
+ instrument: string;
809
+ modal?: Readonly<{ pair?: string }>;
810
+ }>[],
811
+ ): string[] {
812
+ const out: string[] = [];
813
+ for (const track of tracks) {
814
+ const pair = track.modal?.pair;
815
+ if (pair === undefined) continue;
816
+ const partner = tracks.find((candidate) => candidate.id === pair);
817
+ if (!partner)
818
+ out.push(
819
+ `track ${track.id}: modal pair "${pair}" names no track; it plays ombak Hz sharp alone`,
820
+ );
821
+ else if (partner.instrument !== MODAL_INSTRUMENT || !partner.modal)
822
+ out.push(
823
+ `track ${track.id}: modal pair "${pair}" is not a modal track; give it the same preset (modal gangsa)`,
824
+ );
825
+ }
826
+ return out;
542
827
  }
543
828
 
544
829
  /**
@@ -572,3 +857,6 @@ export function legacyResonatorWarnings(
572
857
  }
573
858
  return out;
574
859
  }
860
+
861
+ // f061-gamelan-winds: blown instruments live in core/winds.ts.
862
+ export * from "./winds.ts";