@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
@@ -167,6 +167,8 @@ export type TrackGlide = Readonly<{ time: number; mode: GlideMode }>;
167
167
  export const DEFAULT_GLIDE_SECONDS = 0.06;
168
168
 
169
169
  export const PEDAL_STATES = ["down", "half", "up"] as const;
170
+ /** Sostenuto has no half position (0.6.1). */
171
+ export const SOSTENUTO_STATES = ["down", "up"] as const;
170
172
  export type PedalState = (typeof PEDAL_STATES)[number];
171
173
 
172
174
  /** A sustain pedal (MIDI CC64) change at a tick. */
@@ -386,16 +388,18 @@ export function normalizeTrackGlide(value: unknown): TrackGlide | undefined {
386
388
  export function normalizePedal(
387
389
  value: unknown,
388
390
  maxTick: number,
391
+ label = "pedal",
392
+ states: readonly PedalState[] = PEDAL_STATES,
389
393
  ): readonly PedalEvent[] | undefined {
390
394
  if (value === undefined || value === null) return undefined;
391
395
  if (!Array.isArray(value) || value.length > EXPRESSION_LIMITS.maxPedalEvents)
392
396
  throw new ExpressionValidationError(
393
- `pedal must be an array of at most ${EXPRESSION_LIMITS.maxPedalEvents} events`,
397
+ `${label} must be an array of at most ${EXPRESSION_LIMITS.maxPedalEvents} events`,
394
398
  );
395
399
  const byTick = new Map<number, PedalEvent>();
396
400
  for (const item of value as unknown[]) {
397
- const event = record(item, "pedal event");
398
- onlyKeys(event, ["tick", "state"], "pedal event");
401
+ const event = record(item, `${label} event`);
402
+ onlyKeys(event, ["tick", "state"], `${label} event`);
399
403
  const tick = event.tick;
400
404
  if (
401
405
  typeof tick !== "number" ||
@@ -404,11 +408,11 @@ export function normalizePedal(
404
408
  tick > maxTick
405
409
  )
406
410
  throw new ExpressionValidationError(
407
- `pedal tick must be an integer 0..${maxTick}`,
411
+ `${label} tick must be an integer 0..${maxTick}`,
408
412
  );
409
- if (!PEDAL_STATES.includes(event.state as PedalState))
413
+ if (!states.includes(event.state as PedalState))
410
414
  throw new ExpressionValidationError(
411
- `pedal state must be one of ${PEDAL_STATES.join(", ")}`,
415
+ `${label} state must be one of ${states.join(", ")}`,
412
416
  );
413
417
  // The last event at a tick wins.
414
418
  byTick.set(tick, Object.freeze({ tick, state: event.state as PedalState }));
@@ -551,6 +555,80 @@ export function pedalStateAt(
551
555
  return state;
552
556
  }
553
557
 
558
+ /** A stretch of song ticks `[from, to)` placed at `offset` in a slice. */
559
+ export type PedalPiece = Readonly<{ from: number; to: number; offset: number }>;
560
+
561
+ /**
562
+ * Pedal events for a slice made of `pieces` (a render window, a section
563
+ * pass, an audition region): the state in force at each piece's start is
564
+ * restated at its offset (`carry`, for sustain and una corda; sostenuto
565
+ * passes false, since a pedal already down latches no key struck later),
566
+ * events inside the piece move by `offset - from`, and a pedal still down
567
+ * at the piece's end is lifted there. Undefined when nothing is left.
568
+ */
569
+ export function slicePedal(
570
+ events: readonly PedalEvent[] | undefined,
571
+ pieces: readonly PedalPiece[],
572
+ carry = true,
573
+ ): readonly PedalEvent[] | undefined {
574
+ if (!events || events.length === 0) return events;
575
+ const byTick = new Map<number, PedalEvent>();
576
+ const put = (tick: number, state: PedalState) =>
577
+ byTick.set(tick, Object.freeze({ tick, state }));
578
+ for (const { from, to, offset } of pieces) {
579
+ let state: PedalState = "up";
580
+ if (carry)
581
+ for (const event of events) {
582
+ if (event.tick >= from) break;
583
+ state = event.state;
584
+ }
585
+ // Restate up too: the previous piece may have ended with it down.
586
+ if (state !== "up" || byTick.size > 0) put(offset, state);
587
+ for (const event of events) {
588
+ if (event.tick < from || event.tick >= to) continue;
589
+ put(event.tick - from + offset, event.state);
590
+ state = event.state;
591
+ }
592
+ if (state !== "up" && Number.isFinite(to)) put(offset + to - from, "up");
593
+ }
594
+ const out = [...byTick.values()].sort((a, b) => a.tick - b.tick);
595
+ // Drop events that restate the state already in force.
596
+ const kept: PedalEvent[] = [];
597
+ let last: PedalState = "up";
598
+ for (const event of out)
599
+ if (event.state !== last) {
600
+ kept.push(event);
601
+ last = event.state;
602
+ }
603
+ return kept.length > 0 ? Object.freeze(kept) : undefined;
604
+ }
605
+
606
+ /** The track with its three pedal lanes cut to `pieces` (see slicePedal). */
607
+ export function slicePedals<T extends PedalTrack>(
608
+ track: T,
609
+ pieces: readonly PedalPiece[],
610
+ ): T {
611
+ if (!track.pedal && !track.softPedal && !track.sostenuto) return track;
612
+ const out: Record<string, unknown> = { ...track };
613
+ for (const [field, carry] of [
614
+ ["pedal", true],
615
+ ["softPedal", true],
616
+ ["sostenuto", false],
617
+ ] as const) {
618
+ if (!track[field]) continue;
619
+ const next = slicePedal(track[field], pieces, carry);
620
+ if (next) out[field] = next;
621
+ else delete out[field];
622
+ }
623
+ return out as T;
624
+ }
625
+
626
+ type PedalTrack = Readonly<{
627
+ pedal?: readonly PedalEvent[];
628
+ softPedal?: readonly PedalEvent[];
629
+ sostenuto?: readonly PedalEvent[];
630
+ }>;
631
+
554
632
  // ---------------------------------------------------------------------------
555
633
  // Performance
556
634
 
@@ -674,6 +752,7 @@ export function hasTrackPerformance(track: Track | undefined): boolean {
674
752
  track !== undefined &&
675
753
  (track.glide !== undefined ||
676
754
  track.pedal !== undefined ||
755
+ track.sostenuto !== undefined ||
677
756
  track.velocityCurve !== undefined ||
678
757
  track.humanize !== undefined)
679
758
  );
@@ -869,6 +948,64 @@ export function performNotes(
869
948
  };
870
949
  });
871
950
  }
951
+ // 3b. Sostenuto (0.6.1): a key held (written length after articulation)
952
+ // when the sostenuto goes down keeps its damper up until the sostenuto
953
+ // lifts; keys struck later damp at their own release (or the sustain
954
+ // pedal's). The rod keeps a caught damper raised, so the same pitch
955
+ // struck again while that sostenuto is down rings to the lift too; the
956
+ // earlier instance stops at the restrike so voices do not pile up. A key
957
+ // still held through a lift and a new press is caught again.
958
+ const sostenuto = track?.sostenuto;
959
+ if (sostenuto) {
960
+ const spans: { down: number; lift: number; pitches: Set<number> }[] = [];
961
+ let open: number | undefined;
962
+ for (const event of sostenuto) {
963
+ if (event.state === "down" && open === undefined) open = event.tick;
964
+ else if (event.state === "up" && open !== undefined) {
965
+ spans.push({ down: open, lift: event.tick, pitches: new Set() });
966
+ open = undefined;
967
+ }
968
+ }
969
+ if (open !== undefined)
970
+ spans.push({ down: open, lift: timing.endTick, pitches: new Set() });
971
+ const caughtBy = working.map((item) => {
972
+ const keyUp = item.start + item.bendLength;
973
+ return spans.filter((span) => {
974
+ const held = item.start <= span.down && span.down < keyUp;
975
+ if (held) span.pitches.add(item.note.pitch);
976
+ return held;
977
+ });
978
+ });
979
+ working = working.map((item, index) => {
980
+ const keyUp = item.start + item.bendLength;
981
+ let lift = -Infinity;
982
+ for (const span of caughtBy[index]!) lift = Math.max(lift, span.lift);
983
+ for (const span of spans)
984
+ if (
985
+ span.pitches.has(item.note.pitch) &&
986
+ span.down < item.start &&
987
+ item.start < span.lift
988
+ )
989
+ lift = Math.max(lift, span.lift);
990
+ if (lift === -Infinity) return item;
991
+ const current = item.start + item.duration;
992
+ let end = Math.max(current, Math.min(lift, timing.endTick));
993
+ if (end <= current) return item;
994
+ const restrike = working.find(
995
+ (other) =>
996
+ other !== item &&
997
+ other.note.pitch === item.note.pitch &&
998
+ other.start > item.start &&
999
+ other.start < end,
1000
+ );
1001
+ if (restrike) end = Math.max(current, restrike.start);
1002
+ return {
1003
+ ...item,
1004
+ duration: end - item.start,
1005
+ pedalled: end > keyUp || item.pedalled === true,
1006
+ };
1007
+ });
1008
+ }
872
1009
  // 4. Glide and monophony (on the written timing).
873
1010
  let performed = glideAndMono(
874
1011
  track,