@hraness/dawg 0.4.1 → 0.6.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 (166) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/DAWG.md +551 -33
  3. package/README.md +4 -4
  4. package/core/chords.ts +288 -7
  5. package/core/diff.ts +41 -12
  6. package/core/expression.ts +1241 -0
  7. package/core/fx.ts +437 -2
  8. package/core/granular.ts +528 -0
  9. package/core/instruments.ts +281 -0
  10. package/core/keys.ts +386 -0
  11. package/core/loop.ts +23 -0
  12. package/core/master.ts +455 -0
  13. package/core/midi.ts +452 -0
  14. package/core/resonators.ts +574 -0
  15. package/core/rhythm.ts +7 -2
  16. package/core/score.ts +717 -62
  17. package/core/sdk/eval-child.ts +40 -3
  18. package/core/sdk/print.ts +614 -27
  19. package/core/sdk/sync-instruments.ts +58 -0
  20. package/core/sdk/v1.ts +2824 -45
  21. package/core/sections.ts +2072 -0
  22. package/core/strings.ts +845 -0
  23. package/core/synth.ts +11 -1
  24. package/core/tempo.ts +1318 -0
  25. package/core/tuning.ts +1180 -0
  26. package/guides/audition.md +26 -0
  27. package/guides/automation.md +26 -0
  28. package/guides/chords.md +28 -0
  29. package/guides/effects.md +29 -0
  30. package/guides/faders.md +29 -0
  31. package/guides/files.md +28 -0
  32. package/guides/getting-started.md +26 -0
  33. package/guides/index.ts +75 -0
  34. package/guides/keys.md +27 -0
  35. package/guides/media.md +27 -0
  36. package/guides/mix.md +23 -0
  37. package/guides/music.md +14 -0
  38. package/guides/notes.md +26 -0
  39. package/guides/performance.md +26 -0
  40. package/guides/play.md +24 -0
  41. package/guides/project.md +13 -0
  42. package/guides/providers.md +25 -0
  43. package/guides/rhythm.md +26 -0
  44. package/guides/sessions.md +21 -0
  45. package/guides/sound.md +15 -0
  46. package/guides/sounds.md +29 -0
  47. package/guides/tempo.md +27 -0
  48. package/guides/tracks.md +25 -0
  49. package/guides/web-search.md +21 -0
  50. package/package.json +3 -1
  51. package/src/agent/agent.ts +9 -0
  52. package/src/agent/brief.ts +77 -3
  53. package/src/agent/chord-tools.ts +9 -1
  54. package/src/agent/expression-tools.ts +336 -0
  55. package/src/agent/granular-tools.ts +138 -0
  56. package/src/agent/master-tools.ts +299 -0
  57. package/src/agent/models.ts +4 -4
  58. package/src/agent/ops.ts +68 -6
  59. package/src/agent/planner.ts +45 -2
  60. package/src/agent/preview-tool.ts +27 -4
  61. package/src/agent/section-tools.ts +411 -0
  62. package/src/agent/time-tools.ts +290 -0
  63. package/src/agent/tools.ts +574 -16
  64. package/src/agent/tuning-tools.ts +301 -0
  65. package/src/agent/xcb-agent.ts +9 -0
  66. package/src/audio/arrange.ts +489 -0
  67. package/src/audio/audition.ts +16 -3
  68. package/src/audio/click.ts +113 -1
  69. package/src/audio/clock.ts +71 -5
  70. package/src/audio/dsp/bank.ts +233 -0
  71. package/src/audio/dsp/fft.ts +6 -0
  72. package/src/audio/dsp/filters.ts +57 -0
  73. package/src/audio/dsp/interp.ts +112 -0
  74. package/src/audio/dsp/modal.ts +684 -0
  75. package/src/audio/dsp/onset.ts +197 -0
  76. package/src/audio/dsp/oversample.ts +202 -0
  77. package/src/audio/dsp/rng.ts +37 -0
  78. package/src/audio/dsp/shape.ts +81 -0
  79. package/src/audio/dsp/stft.ts +73 -0
  80. package/src/audio/dsp/window.ts +77 -0
  81. package/src/audio/effects/bus.ts +5 -4
  82. package/src/audio/effects/chain.ts +6 -1
  83. package/src/audio/effects/common.ts +30 -1
  84. package/src/audio/effects/dynamics.ts +2 -1
  85. package/src/audio/effects/filter.ts +5 -3
  86. package/src/audio/effects/modulation.ts +5 -1
  87. package/src/audio/effects/rig/cab.ts +99 -0
  88. package/src/audio/effects/rig/filters.ts +152 -0
  89. package/src/audio/effects/rig/gate.ts +39 -0
  90. package/src/audio/effects/rig/head.ts +487 -0
  91. package/src/audio/effects/rig/index.ts +170 -0
  92. package/src/audio/effects/rig/section.ts +78 -0
  93. package/src/audio/effects/rig/stomp.ts +238 -0
  94. package/src/audio/effects/space.ts +92 -2
  95. package/src/audio/engine.ts +100 -21
  96. package/src/audio/fit.ts +402 -0
  97. package/src/audio/granular.ts +664 -0
  98. package/src/audio/instrument-check.ts +133 -0
  99. package/src/audio/instruments.ts +111 -0
  100. package/src/audio/keys/dsp.ts +323 -0
  101. package/src/audio/keys/engine.ts +361 -0
  102. package/src/audio/keys/piano.ts +432 -0
  103. package/src/audio/live-worker.ts +54 -0
  104. package/src/audio/live.ts +263 -17
  105. package/src/audio/loudness.ts +596 -0
  106. package/src/audio/master.ts +660 -0
  107. package/src/audio/measure-worker.ts +45 -0
  108. package/src/audio/measure.ts +110 -0
  109. package/src/audio/player.ts +11 -6
  110. package/src/audio/preview.ts +76 -14
  111. package/src/audio/render-worker.ts +6 -1
  112. package/src/audio/renderer.ts +7 -1
  113. package/src/audio/resonators.ts +287 -0
  114. package/src/audio/sampler.ts +269 -34
  115. package/src/audio/samples.ts +30 -7
  116. package/src/audio/strings/body.ts +250 -0
  117. package/src/audio/strings/engine.ts +369 -0
  118. package/src/audio/strings/loop.ts +119 -0
  119. package/src/audio/strings/measure.test-helpers.ts +198 -0
  120. package/src/audio/strings/pluck.ts +354 -0
  121. package/src/audio/synth/voice.ts +60 -13
  122. package/src/audio/synth/zzfx.ts +10 -4
  123. package/src/audio/warp.ts +147 -0
  124. package/src/audio/wav.ts +377 -30
  125. package/src/commands/arrange.ts +949 -0
  126. package/src/commands/expression.ts +941 -0
  127. package/src/commands/fit.ts +135 -0
  128. package/src/commands/fx.ts +31 -0
  129. package/src/commands/granular.ts +427 -0
  130. package/src/commands/help.ts +355 -27
  131. package/src/commands/keys.ts +353 -0
  132. package/src/commands/master.ts +361 -0
  133. package/src/commands/modal.ts +247 -0
  134. package/src/commands/music.ts +6 -1
  135. package/src/commands/rig.ts +260 -0
  136. package/src/commands/sample.ts +3 -0
  137. package/src/commands/string.ts +175 -0
  138. package/src/commands/synth.ts +11 -1
  139. package/src/commands/time.ts +967 -0
  140. package/src/commands/tuning.ts +490 -0
  141. package/src/main.ts +919 -41
  142. package/src/project/check.ts +13 -0
  143. package/src/render.ts +139 -8
  144. package/src/session/daemon.ts +18 -8
  145. package/src/session/naming.ts +8 -1
  146. package/src/session/rebase.ts +21 -0
  147. package/src/tui/arrange-menu.ts +390 -0
  148. package/src/tui/audition.ts +38 -5
  149. package/src/tui/fader.ts +409 -0
  150. package/src/tui/granular-menu.ts +278 -0
  151. package/src/tui/menu-time.ts +401 -0
  152. package/src/tui/menu.ts +967 -23
  153. package/src/tui/modal-menu.ts +118 -0
  154. package/src/tui/performance-menu.ts +235 -0
  155. package/src/tui/play-chords.ts +3 -1
  156. package/src/tui/play-mode.ts +150 -6
  157. package/src/tui/play-session.ts +459 -43
  158. package/tui/app.ts +262 -13
  159. package/tui/arrange-strip.ts +174 -0
  160. package/tui/drawer.ts +478 -0
  161. package/tui/grammar.ts +63 -1
  162. package/tui/guide.ts +351 -0
  163. package/tui/highway.ts +87 -4
  164. package/tui/hits.ts +68 -0
  165. package/tui/input.ts +7 -0
  166. package/tui/keys.ts +72 -0
package/README.md CHANGED
@@ -15,17 +15,17 @@ curl -fsSL https://dawg.sh/install | sh
15
15
  or install the release tarball from GitHub directly:
16
16
 
17
17
  ```sh
18
- bun add -g https://github.com/hraness/dawg/releases/download/v0.4.1/hraness-dawg-0.4.1.tgz
18
+ bun add -g https://github.com/hraness/dawg/releases/download/v0.6.0/hraness-dawg-0.6.0.tgz
19
19
  dawg --help
20
20
  ```
21
21
 
22
22
  Each [release](https://github.com/hraness/dawg/releases) is immutable and ships the tarball, a `SHA256SUMS` file and a build provenance attestation. To check a download before installing it:
23
23
 
24
24
  ```sh
25
- gh release download v0.4.1 --repo hraness/dawg
25
+ gh release download v0.6.0 --repo hraness/dawg
26
26
  shasum -a 256 -c SHA256SUMS
27
- gh attestation verify hraness-dawg-0.4.1.tgz --repo hraness/dawg
28
- bun add -g "$PWD/hraness-dawg-0.4.1.tgz"
27
+ gh attestation verify hraness-dawg-0.6.0.tgz --repo hraness/dawg
28
+ bun add -g "$PWD/hraness-dawg-0.6.0.tgz"
29
29
  ```
30
30
 
31
31
  dawg is also on npm as [`@hraness/dawg`](https://www.npmjs.com/package/@hraness/dawg):
package/core/chords.ts CHANGED
@@ -410,6 +410,8 @@ export const MODES = Object.freeze({
410
410
  mixolydian: [0, 2, 4, 5, 7, 9, 10],
411
411
  locrian: [0, 1, 3, 5, 6, 8, 10],
412
412
  "harmonic-minor": [0, 2, 3, 5, 7, 8, 11],
413
+ "melodic-minor": [0, 2, 3, 5, 7, 9, 11],
414
+ "phrygian-dominant": [0, 1, 4, 5, 7, 8, 10],
413
415
  } as const);
414
416
  export type ModeName = keyof typeof MODES;
415
417
  export const MODE_NAMES = Object.keys(MODES) as ModeName[];
@@ -432,25 +434,302 @@ const MODE_ALIASES: Readonly<Record<string, ModeName>> = Object.freeze({
432
434
  "harmonic-minor": "harmonic-minor",
433
435
  "harmonic minor": "harmonic-minor",
434
436
  harmonic: "harmonic-minor",
437
+ "melodic-minor": "melodic-minor",
438
+ "melodic minor": "melodic-minor",
439
+ melodic: "melodic-minor",
440
+ "jazz minor": "melodic-minor",
441
+ "phrygian-dominant": "phrygian-dominant",
442
+ "phrygian dominant": "phrygian-dominant",
443
+ freygish: "phrygian-dominant",
444
+ spanish: "phrygian-dominant",
445
+ ajam: "major",
446
+ mahur: "major",
447
+ bilawal: "major",
435
448
  });
436
449
 
437
- export type Key = Readonly<{ tonic: number; mode: ModeName }>;
450
+ export type ScaleFamily =
451
+ "pentatonic" | "blues" | "maqam" | "dastgah" | "raga" | "messiaen";
438
452
 
439
453
  /**
440
- * Parse a key: `C`, `c major`, `Am`, `a minor`, `F# dorian`, `Eb mixo`.
441
- * Accepts the `<note> <mode>` form `core/key.ts` writes.
454
+ * Scales beyond the chord modes, for keys such as `D bayati`, `C yaman` or
455
+ * `C messiaen-3`. `steps` are semitones above the tonic and may be
456
+ * fractional (a quarter tone is .5); `mode` is the seven-note mode the
457
+ * chord engine harmonizes with (the closest one; see DAWG.md). A raga's
458
+ * `intonation` is each step's traditional just pitch in cents (shruti
459
+ * offsets), which its named tuning applies (`tuning yaman`): Pythagorean
460
+ * ati-komal re and dha for Bhairavi, Bhairav, Purvi and Todi, the high
461
+ * tivra ma (729/512) for Yaman, 9/5 komal ni for Kafi, after Daniélou and
462
+ * Jairazbhoy. Maqam and dastgah quarter tones follow the 24-tone convention;
463
+ * Segah and Sikah start on a half-flat note, so their tonic is the key.
464
+ */
465
+ export type ScaleInfo = Readonly<{
466
+ steps: readonly number[];
467
+ mode: ModeName;
468
+ family: ScaleFamily;
469
+ intonation?: readonly number[];
470
+ aliases?: readonly string[];
471
+ }>;
472
+
473
+ export const SCALES = Object.freeze({
474
+ "major-pentatonic": {
475
+ steps: [0, 2, 4, 7, 9],
476
+ mode: "major",
477
+ family: "pentatonic",
478
+ aliases: ["pentatonic", "major pentatonic", "pent"],
479
+ },
480
+ "minor-pentatonic": {
481
+ steps: [0, 3, 5, 7, 10],
482
+ mode: "minor",
483
+ family: "pentatonic",
484
+ aliases: ["minor pentatonic", "m pentatonic", "min pentatonic"],
485
+ },
486
+ blues: {
487
+ steps: [0, 3, 5, 6, 7, 10],
488
+ mode: "minor",
489
+ family: "blues",
490
+ aliases: ["minor blues"],
491
+ },
492
+ "major-blues": {
493
+ steps: [0, 2, 3, 4, 7, 9],
494
+ mode: "major",
495
+ family: "blues",
496
+ aliases: ["major blues"],
497
+ },
498
+ hijaz: {
499
+ steps: [0, 1, 4, 5, 7, 8, 10],
500
+ mode: "phrygian-dominant",
501
+ family: "maqam",
502
+ },
503
+ bayati: {
504
+ steps: [0, 1.5, 3, 5, 7, 8, 10],
505
+ mode: "phrygian",
506
+ family: "maqam",
507
+ },
508
+ rast: { steps: [0, 2, 3.5, 5, 7, 9, 10.5], mode: "major", family: "maqam" },
509
+ saba: { steps: [0, 1.5, 3, 4, 7, 8, 10], mode: "phrygian", family: "maqam" },
510
+ kurd: { steps: [0, 1, 3, 5, 7, 8, 10], mode: "phrygian", family: "maqam" },
511
+ nahawand: {
512
+ steps: [0, 2, 3, 5, 7, 8, 11],
513
+ mode: "harmonic-minor",
514
+ family: "maqam",
515
+ },
516
+ sikah: {
517
+ steps: [0, 1.5, 3.5, 5.5, 7, 8.5, 10.5],
518
+ mode: "phrygian",
519
+ family: "maqam",
520
+ aliases: ["sika"],
521
+ },
522
+ huzam: {
523
+ steps: [0, 1.5, 3.5, 4.5, 7.5, 8.5, 10.5],
524
+ mode: "phrygian",
525
+ family: "maqam",
526
+ aliases: ["houzam"],
527
+ },
528
+ nikriz: { steps: [0, 2, 3, 6, 7, 9, 10], mode: "dorian", family: "maqam" },
529
+ shur: {
530
+ steps: [0, 1.5, 3, 5, 7, 8, 10],
531
+ mode: "phrygian",
532
+ family: "dastgah",
533
+ },
534
+ homayoun: {
535
+ steps: [0, 1.5, 4, 5, 7, 8, 10],
536
+ mode: "phrygian-dominant",
537
+ family: "dastgah",
538
+ aliases: ["homayun"],
539
+ },
540
+ chahargah: {
541
+ steps: [0, 1.5, 4, 5, 7, 8.5, 11],
542
+ mode: "phrygian-dominant",
543
+ family: "dastgah",
544
+ aliases: ["chahar-gah"],
545
+ },
546
+ segah: {
547
+ steps: [0, 1.5, 3.5, 5, 6.5, 8.5, 10.5],
548
+ mode: "phrygian",
549
+ family: "dastgah",
550
+ aliases: ["sehgah", "se-gah"],
551
+ },
552
+ nava: {
553
+ steps: [0, 2, 3.5, 5, 7, 8, 10],
554
+ mode: "minor",
555
+ family: "dastgah",
556
+ },
557
+ yaman: {
558
+ steps: [0, 2, 4, 6, 7, 9, 11],
559
+ mode: "lydian",
560
+ family: "raga",
561
+ intonation: [0, 203.91, 386.31, 611.73, 701.96, 884.36, 1088.27],
562
+ aliases: ["kalyan", "yaman kalyan"],
563
+ },
564
+ bhairav: {
565
+ steps: [0, 1, 4, 5, 7, 8, 11],
566
+ mode: "phrygian-dominant",
567
+ family: "raga",
568
+ intonation: [0, 90.22, 386.31, 498.04, 701.96, 792.18, 1088.27],
569
+ },
570
+ kafi: {
571
+ steps: [0, 2, 3, 5, 7, 9, 10],
572
+ mode: "dorian",
573
+ family: "raga",
574
+ intonation: [0, 203.91, 315.64, 498.04, 701.96, 884.36, 1017.6],
575
+ },
576
+ bhairavi: {
577
+ steps: [0, 1, 3, 5, 7, 8, 10],
578
+ mode: "phrygian",
579
+ family: "raga",
580
+ intonation: [0, 90.22, 294.13, 498.04, 701.96, 792.18, 996.09],
581
+ },
582
+ asavari: {
583
+ steps: [0, 2, 3, 5, 7, 8, 10],
584
+ mode: "minor",
585
+ family: "raga",
586
+ intonation: [0, 203.91, 315.64, 498.04, 701.96, 813.69, 996.09],
587
+ },
588
+ khamaj: {
589
+ steps: [0, 2, 4, 5, 7, 9, 10],
590
+ mode: "mixolydian",
591
+ family: "raga",
592
+ intonation: [0, 203.91, 386.31, 498.04, 701.96, 884.36, 996.09],
593
+ },
594
+ todi: {
595
+ steps: [0, 1, 3, 6, 7, 8, 11],
596
+ mode: "phrygian",
597
+ family: "raga",
598
+ intonation: [0, 95, 294, 606, 702, 792, 1107],
599
+ },
600
+ purvi: {
601
+ steps: [0, 1, 4, 6, 7, 8, 11],
602
+ mode: "phrygian-dominant",
603
+ family: "raga",
604
+ intonation: [0, 90.22, 386.31, 590.22, 701.96, 792.18, 1088.27],
605
+ },
606
+ marwa: {
607
+ steps: [0, 1, 4, 6, 9, 11],
608
+ mode: "lydian",
609
+ family: "raga",
610
+ intonation: [0, 111.73, 386.31, 590.22, 884.36, 1088.27],
611
+ },
612
+ darbari: {
613
+ steps: [0, 2, 3, 5, 7, 8, 10],
614
+ mode: "minor",
615
+ family: "raga",
616
+ intonation: [0, 203.91, 294.13, 498.04, 701.96, 792.18, 996.09],
617
+ aliases: ["darbari kanada"],
618
+ },
619
+ malkauns: {
620
+ steps: [0, 3, 5, 8, 10],
621
+ mode: "minor",
622
+ family: "raga",
623
+ intonation: [0, 315.64, 498.04, 813.69, 996.09],
624
+ },
625
+ bhupali: {
626
+ steps: [0, 2, 4, 7, 9],
627
+ mode: "major",
628
+ family: "raga",
629
+ intonation: [0, 203.91, 386.31, 701.96, 884.36],
630
+ },
631
+ durga: {
632
+ steps: [0, 2, 5, 7, 9],
633
+ mode: "major",
634
+ family: "raga",
635
+ intonation: [0, 203.91, 498.04, 701.96, 884.36],
636
+ },
637
+ "messiaen-1": {
638
+ steps: [0, 2, 4, 6, 8, 10],
639
+ mode: "lydian",
640
+ family: "messiaen",
641
+ aliases: ["whole-tone", "whole tone", "wholetone"],
642
+ },
643
+ "messiaen-2": {
644
+ steps: [0, 1, 3, 4, 6, 7, 9, 10],
645
+ mode: "mixolydian",
646
+ family: "messiaen",
647
+ aliases: ["octatonic", "diminished", "half-whole"],
648
+ },
649
+ "messiaen-3": {
650
+ steps: [0, 2, 3, 4, 6, 7, 8, 10, 11],
651
+ mode: "minor",
652
+ family: "messiaen",
653
+ },
654
+ "messiaen-4": {
655
+ steps: [0, 1, 2, 5, 6, 7, 8, 11],
656
+ mode: "harmonic-minor",
657
+ family: "messiaen",
658
+ },
659
+ "messiaen-5": {
660
+ steps: [0, 1, 5, 6, 7, 11],
661
+ mode: "lydian",
662
+ family: "messiaen",
663
+ },
664
+ "messiaen-6": {
665
+ steps: [0, 2, 4, 5, 6, 8, 10, 11],
666
+ mode: "major",
667
+ family: "messiaen",
668
+ },
669
+ "messiaen-7": {
670
+ steps: [0, 1, 2, 3, 5, 6, 7, 8, 9, 11],
671
+ mode: "harmonic-minor",
672
+ family: "messiaen",
673
+ },
674
+ } as const satisfies Record<string, ScaleInfo>);
675
+ export type ScaleName = keyof typeof SCALES;
676
+ export const SCALE_NAMES = Object.keys(SCALES) as ScaleName[];
677
+
678
+ const SCALE_ALIASES: Readonly<Record<string, ScaleName>> = (() => {
679
+ const aliases: Record<string, ScaleName> = {};
680
+ for (const name of SCALE_NAMES) {
681
+ const info: ScaleInfo = SCALES[name];
682
+ aliases[name] = name;
683
+ aliases[name.replace(/-/g, " ")] = name;
684
+ for (const alias of info.aliases ?? []) aliases[alias] = name;
685
+ }
686
+ return Object.freeze(aliases);
687
+ })();
688
+
689
+ /** The library scale named `text` (case and `-`/space insensitive). */
690
+ export function scaleNamed(text: string): ScaleName | undefined {
691
+ const word = text.trim().toLowerCase().replace(/\s+/g, " ");
692
+ return SCALE_ALIASES[word] ?? SCALE_ALIASES[word.replace(/ /g, "-")];
693
+ }
694
+
695
+ /**
696
+ * A key: a tonic and the seven-note `mode` the chord engine uses, plus the
697
+ * library `scale` when the key names one (`D bayati`, `C yaman`).
698
+ */
699
+ export type Key = Readonly<{
700
+ tonic: number;
701
+ mode: ModeName;
702
+ scale?: ScaleName;
703
+ }>;
704
+
705
+ /**
706
+ * Parse a key: `C`, `c major`, `Am`, `a minor`, `F# dorian`, `Eb mixo`,
707
+ * `D bayati`, `C messiaen-3`. Accepts the `<note> <mode>` form
708
+ * `core/key.ts` writes.
442
709
  */
443
710
  export function parseKey(text: string | null | undefined): Key | undefined {
444
711
  if (typeof text !== "string" || text.length > 40) return undefined;
445
712
  const match = text
446
713
  .trim()
447
- .match(/^([a-gA-G])(#|b|♯|♭)?\s*(m(?![a-z])|[a-zA-Z][a-zA-Z -]*)?$/);
714
+ .match(/^([a-gA-G])(#|b|♯|♭)?\s*(m(?![a-z])|[a-zA-Z][a-zA-Z0-9 -]*)?$/);
448
715
  if (!match) return undefined;
449
716
  const tonic = parsePitchClass(`${match[1]}${match[2] ?? ""}`);
717
+ if (tonic === undefined) return undefined;
450
718
  const word = (match[3] ?? "").trim();
451
719
  const mode = MODE_ALIASES[word === "m" ? "m" : word.toLowerCase()];
452
- if (tonic === undefined || mode === undefined) return undefined;
453
- return Object.freeze({ tonic, mode });
720
+ if (mode !== undefined) return Object.freeze({ tonic, mode });
721
+ const scale = scaleNamed(word);
722
+ if (scale === undefined) return undefined;
723
+ return Object.freeze({ tonic, mode: SCALES[scale].mode, scale });
724
+ }
725
+
726
+ /**
727
+ * Steps of the key's whole scale in semitones above the tonic (fractional
728
+ * for quarter tones): the library scale when the key names one, else the
729
+ * mode.
730
+ */
731
+ export function scaleSteps(key: Key): number[] {
732
+ return [...(key.scale ? SCALES[key.scale].steps : MODES[key.mode])];
454
733
  }
455
734
 
456
735
  /** True when names in the key read better with flats (F, Bb, Eb, d minor…). */
@@ -465,6 +744,8 @@ export function keyUsesFlats(key: Key): boolean {
465
744
  minor: 9,
466
745
  locrian: 11,
467
746
  "harmonic-minor": 9,
747
+ "melodic-minor": 9,
748
+ "phrygian-dominant": 4,
468
749
  };
469
750
  const parent = mod12(key.tonic - parentOffset[key.mode]);
470
751
  return [5, 10, 3, 8, 1].includes(parent);
@@ -472,7 +753,7 @@ export function keyUsesFlats(key: Key): boolean {
472
753
 
473
754
  /** `C major`, `F# dorian`, `Bb minor`. */
474
755
  export function keyName(key: Key): string {
475
- return `${noteName(key.tonic, keyUsesFlats(key))} ${key.mode}`;
756
+ return `${noteName(key.tonic, keyUsesFlats(key))} ${key.scale ?? key.mode}`;
476
757
  }
477
758
 
478
759
  /** Pitch classes of the key's scale, tonic first. */
package/core/diff.ts CHANGED
@@ -10,10 +10,15 @@
10
10
  * canonical order, so insertion order never matters).
11
11
  */
12
12
 
13
+ import {
14
+ NOTE_EXPRESSION_FIELDS,
15
+ TRACK_PERFORMANCE_FIELDS,
16
+ } from "./expression.ts";
13
17
  import {
14
18
  applyScoreOperation,
15
19
  TrackScore,
16
20
  type Note,
21
+ type NotePatch,
17
22
  type ScoreOperation,
18
23
  type Track,
19
24
  type TrackPatch,
@@ -35,6 +40,10 @@ const EFFECTS = [
35
40
  "fxAutomation",
36
41
  "synth",
37
42
  "wavetable",
43
+ "string",
44
+ "granular",
45
+ "keys",
46
+ "modal",
38
47
  ] as const;
39
48
  const LANES = [
40
49
  "volumeAutomation",
@@ -62,6 +71,23 @@ export function diffScores(
62
71
  if (a.tempoBpm !== b.tempoBpm)
63
72
  ops.push({ type: "setTempo", tempoBpm: b.tempoBpm });
64
73
  if (a.key !== b.key) ops.push({ type: "setKey", key: b.key });
74
+ if (!deepEqual(a.time, b.time))
75
+ ops.push({ type: "setTime", time: b.time ?? null });
76
+ if (!deepEqual(a.tuning, b.tuning))
77
+ ops.push({ type: "setTuning", tuning: b.tuning ?? null });
78
+ if (!deepEqual(a.master, b.master))
79
+ ops.push({ type: "setMaster", master: b.master ?? null });
80
+ if (
81
+ !deepEqual(a.sections, b.sections) ||
82
+ !deepEqual(a.form, b.form) ||
83
+ a.loopSection !== b.loopSection
84
+ )
85
+ ops.push({
86
+ type: "setSections",
87
+ sections: b.sections,
88
+ form: b.form,
89
+ ...(b.loopSection === undefined ? {} : { loopSection: b.loopSection }),
90
+ });
65
91
 
66
92
  const aTracks = new Map(a.tracks.map((track) => [track.id, track]));
67
93
  const bTracks = new Map(b.tracks.map((track) => [track.id, track]));
@@ -133,15 +159,8 @@ export function applyScoreOperations(
133
159
  return operations.reduce(applyScoreOperation, score);
134
160
  }
135
161
 
136
- function notePatch(
137
- a: Note,
138
- b: Note,
139
- ):
140
- | Readonly<
141
- Partial<Pick<Note, "startTick" | "durationTicks" | "pitch" | "velocity">>
142
- >
143
- | undefined {
144
- const patch: Record<string, number> = {};
162
+ function notePatch(a: Note, b: Note): NotePatch | undefined {
163
+ const patch: Record<string, unknown> = {};
145
164
  for (const key of [
146
165
  "startTick",
147
166
  "durationTicks",
@@ -149,7 +168,10 @@ function notePatch(
149
168
  "velocity",
150
169
  ] as const)
151
170
  if (a[key] !== b[key]) patch[key] = b[key];
152
- return Object.keys(patch).length > 0 ? patch : undefined;
171
+ for (const key of NOTE_EXPRESSION_FIELDS)
172
+ if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
173
+ if ((a.cents ?? 0) !== (b.cents ?? 0)) patch.cents = b.cents ?? 0;
174
+ return Object.keys(patch).length > 0 ? (patch as NotePatch) : undefined;
153
175
  }
154
176
 
155
177
  function trackPatch(a: Track, b: Track): TrackPatch | undefined {
@@ -159,7 +181,13 @@ function trackPatch(a: Track, b: Track): TrackPatch | undefined {
159
181
  if ((a.solo ?? false) !== (b.solo ?? false)) patch.solo = b.solo ?? false;
160
182
  for (const key of EFFECTS)
161
183
  if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
162
- for (const key of ["rhythm", "kit"] as const)
184
+ for (const key of [
185
+ "rhythm",
186
+ "kit",
187
+ "time",
188
+ "tuning",
189
+ ...TRACK_PERFORMANCE_FIELDS,
190
+ ] as const)
163
191
  if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
164
192
  for (const key of LANES)
165
193
  if (!deepEqual(a[key] ?? [], b[key] ?? [])) patch[key] = b[key] ?? [];
@@ -235,7 +263,8 @@ export function adoptNoteIds(
235
263
  }
236
264
 
237
265
  function exactKey(note: Note): string {
238
- return `${note.trackId}|${note.pitch}|${note.startTick}|${note.durationTicks}|${note.velocity}`;
266
+ const cents = note.cents ? `|${note.cents}` : "";
267
+ return `${note.trackId}|${note.pitch}|${note.startTick}|${note.durationTicks}|${note.velocity}${cents}`;
239
268
  }
240
269
 
241
270
  function looseKey(note: Note): string {