@pinkpixel/haku 0.2.0 → 1.0.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 (115) hide show
  1. package/README.md +5 -2
  2. package/dist/cli.mjs +2 -2
  3. package/dist/{mcp-DG9U-P0N.mjs → mcp-DsSn95Rn.mjs} +1 -1
  4. package/dist/samples/drums/cabasa_v0_r1.ogg +0 -0
  5. package/dist/samples/drums/cabasa_v0_r2.ogg +0 -0
  6. package/dist/samples/drums/cabasa_v1_r1.ogg +0 -0
  7. package/dist/samples/drums/cabasa_v1_r2.ogg +0 -0
  8. package/dist/samples/drums/shaker_v0_r1.ogg +0 -0
  9. package/dist/samples/drums/shaker_v0_r2.ogg +0 -0
  10. package/dist/samples/drums/shaker_v1_r1.ogg +0 -0
  11. package/dist/samples/drums/shaker_v1_r2.ogg +0 -0
  12. package/dist/samples/guitar/v0_31.ogg +0 -0
  13. package/dist/samples/guitar/v0_32.ogg +0 -0
  14. package/dist/samples/guitar/v0_33.ogg +0 -0
  15. package/dist/samples/guitar/v0_34.ogg +0 -0
  16. package/dist/samples/guitar/v0_35.ogg +0 -0
  17. package/dist/samples/guitar/v0_36.ogg +0 -0
  18. package/dist/samples/guitar/v0_37.ogg +0 -0
  19. package/dist/samples/guitar/v0_38.ogg +0 -0
  20. package/dist/samples/guitar/v0_39.ogg +0 -0
  21. package/dist/samples/guitar/v0_40.ogg +0 -0
  22. package/dist/samples/guitar/v0_41.ogg +0 -0
  23. package/dist/samples/guitar/v0_43.ogg +0 -0
  24. package/dist/samples/guitar/v0_45.ogg +0 -0
  25. package/dist/samples/guitar/v0_47.ogg +0 -0
  26. package/dist/samples/guitar/v0_48.ogg +0 -0
  27. package/dist/samples/guitar/v0_50.ogg +0 -0
  28. package/dist/samples/guitar/v0_52.ogg +0 -0
  29. package/dist/samples/guitar/v0_53.ogg +0 -0
  30. package/dist/samples/guitar/v0_54.ogg +0 -0
  31. package/dist/samples/guitar/v0_55.ogg +0 -0
  32. package/dist/samples/guitar/v0_56.ogg +0 -0
  33. package/dist/samples/guitar/v0_57.ogg +0 -0
  34. package/dist/samples/guitar/v0_58.ogg +0 -0
  35. package/dist/samples/guitar/v0_59.ogg +0 -0
  36. package/dist/samples/guitar/v0_60.ogg +0 -0
  37. package/dist/samples/guitar/v0_61.ogg +0 -0
  38. package/dist/samples/guitar/v0_62.ogg +0 -0
  39. package/dist/samples/guitar/v0_63.ogg +0 -0
  40. package/dist/samples/guitar/v0_64.ogg +0 -0
  41. package/dist/samples/guitar/v0_65.ogg +0 -0
  42. package/dist/samples/guitar/v0_66.ogg +0 -0
  43. package/dist/samples/guitar/v0_67.ogg +0 -0
  44. package/dist/samples/guitar/v0_69.ogg +0 -0
  45. package/dist/samples/guitar/v0_70.ogg +0 -0
  46. package/dist/samples/guitar/v0_71.ogg +0 -0
  47. package/dist/samples/guitar/v0_72.ogg +0 -0
  48. package/dist/samples/guitar/v0_73.ogg +0 -0
  49. package/dist/samples/guitar/v0_74.ogg +0 -0
  50. package/dist/samples/guitar/v0_75.ogg +0 -0
  51. package/dist/samples/guitar/v0_76.ogg +0 -0
  52. package/dist/samples/guitar/v0_77.ogg +0 -0
  53. package/dist/samples/guitar/v0_78.ogg +0 -0
  54. package/dist/samples/guitar/v0_79.ogg +0 -0
  55. package/dist/samples/guitar/v0_80.ogg +0 -0
  56. package/dist/samples/guitar/v0_81.ogg +0 -0
  57. package/dist/samples/guitar/v0_82.ogg +0 -0
  58. package/dist/samples/guitar/v0_83.ogg +0 -0
  59. package/dist/samples/guitar/v0_84.ogg +0 -0
  60. package/dist/samples/kalimba/v0_44.ogg +0 -0
  61. package/dist/samples/kalimba/v0_46.ogg +0 -0
  62. package/dist/samples/kalimba/v0_49.ogg +0 -0
  63. package/dist/samples/kalimba/v0_51.ogg +0 -0
  64. package/dist/samples/kalimba/v0_53.ogg +0 -0
  65. package/dist/samples/kalimba/v0_56.ogg +0 -0
  66. package/dist/samples/kalimba/v0_59.ogg +0 -0
  67. package/dist/samples/kalimba/v0_61.ogg +0 -0
  68. package/dist/samples/kalimba/v0_64.ogg +0 -0
  69. package/dist/samples/kalimba/v0_65.ogg +0 -0
  70. package/dist/samples/kalimba/v0_67.ogg +0 -0
  71. package/dist/samples/kalimba/v0_71.ogg +0 -0
  72. package/dist/samples/kalimba/v0_73.ogg +0 -0
  73. package/dist/samples/kalimba/v0_74.ogg +0 -0
  74. package/dist/samples/kalimba/v0_75.ogg +0 -0
  75. package/dist/samples/kalimba/v0_77.ogg +0 -0
  76. package/dist/samples/kalimba/v0_78.ogg +0 -0
  77. package/dist/samples/kalimba/v0_79.ogg +0 -0
  78. package/dist/samples/kalimba/v0_82.ogg +0 -0
  79. package/dist/samples/kalimba/v0_83.ogg +0 -0
  80. package/dist/samples/manifest.json +181 -0
  81. package/dist/samples/vibes/v0_53.ogg +0 -0
  82. package/dist/samples/vibes/v0_57.ogg +0 -0
  83. package/dist/samples/vibes/v0_60.ogg +0 -0
  84. package/dist/samples/vibes/v0_64.ogg +0 -0
  85. package/dist/samples/vibes/v0_67.ogg +0 -0
  86. package/dist/samples/vibes/v0_71.ogg +0 -0
  87. package/dist/samples/vibes/v0_74.ogg +0 -0
  88. package/dist/samples/vibes/v0_77.ogg +0 -0
  89. package/dist/samples/vibes/v0_81.ogg +0 -0
  90. package/dist/samples/vibes/v0_84.ogg +0 -0
  91. package/dist/samples/vibes/v0_88.ogg +0 -0
  92. package/dist/samples/vibes/v1_53.ogg +0 -0
  93. package/dist/samples/vibes/v1_57.ogg +0 -0
  94. package/dist/samples/vibes/v1_60.ogg +0 -0
  95. package/dist/samples/vibes/v1_64.ogg +0 -0
  96. package/dist/samples/vibes/v1_67.ogg +0 -0
  97. package/dist/samples/vibes/v1_71.ogg +0 -0
  98. package/dist/samples/vibes/v1_74.ogg +0 -0
  99. package/dist/samples/vibes/v1_77.ogg +0 -0
  100. package/dist/samples/vibes/v1_81.ogg +0 -0
  101. package/dist/samples/vibes/v1_84.ogg +0 -0
  102. package/dist/samples/vibes/v1_88.ogg +0 -0
  103. package/dist/{src-dP0jXhPP.mjs → src-Bz-0ulVm.mjs} +169 -22
  104. package/dist/{src-CgZeiBJc.mjs → src-DNP73wX3.mjs} +49 -2
  105. package/dist/{tools-_xkF2Yfv.mjs → tools-CO-tdB7d.mjs} +140 -50
  106. package/dist/web/assets/esm-J-U37_Wq.js +210 -0
  107. package/dist/web/assets/index-BkzOde44.js +65 -0
  108. package/dist/web/assets/index-D0m3L8JK.css +1 -0
  109. package/dist/web/assets/rolldown-runtime-BWaJ3C7D.js +1 -0
  110. package/dist/web/assets/src-m48ItyJA.js +1 -0
  111. package/dist/web/index.html +3 -2
  112. package/dist/web/sw.js +2 -2
  113. package/package.json +1 -1
  114. package/dist/web/assets/index-BmW6jd5Q.js +0 -272
  115. package/dist/web/assets/index-C7U7z4um.css +0 -1
@@ -775,18 +775,36 @@ const INSTRUMENTS = {
775
775
  kind: "tonal",
776
776
  gm: 9
777
777
  },
778
+ vibes: {
779
+ label: "Vibraphone",
780
+ range: [53, 86],
781
+ kind: "tonal",
782
+ gm: 11
783
+ },
778
784
  marimba: {
779
785
  label: "Marimba",
780
786
  range: [55, 84],
781
787
  kind: "tonal",
782
788
  gm: 12
783
789
  },
790
+ kalimba: {
791
+ label: "Kalimba",
792
+ range: [60, 83],
793
+ kind: "tonal",
794
+ gm: 108
795
+ },
784
796
  harp: {
785
797
  label: "Harp",
786
798
  range: [48, 84],
787
799
  kind: "tonal",
788
800
  gm: 46
789
801
  },
802
+ guitar: {
803
+ label: "Nylon guitar",
804
+ range: [45, 79],
805
+ kind: "tonal",
806
+ gm: 24
807
+ },
790
808
  pad: {
791
809
  label: "Warm pad",
792
810
  range: [55, 79],
@@ -799,6 +817,18 @@ const INSTRUMENTS = {
799
817
  kind: "tonal",
800
818
  gm: 24
801
819
  },
820
+ celesta: {
821
+ label: "Celesta",
822
+ range: [60, 96],
823
+ kind: "tonal",
824
+ gm: 8
825
+ },
826
+ musicbox: {
827
+ label: "Music box",
828
+ range: [64, 96],
829
+ kind: "tonal",
830
+ gm: 10
831
+ },
802
832
  bass: {
803
833
  label: "Round bass",
804
834
  range: [31, 55],
@@ -890,6 +920,14 @@ const DRUM_PIECES = {
890
920
  tom: {
891
921
  label: "Tom",
892
922
  gm: 45
923
+ },
924
+ shaker: {
925
+ label: "Shaker",
926
+ gm: 82
927
+ },
928
+ cabasa: {
929
+ label: "Cabasa",
930
+ gm: 69
893
931
  }
894
932
  };
895
933
  const DRUM_PIECE_IDS = Object.keys(DRUM_PIECES);
@@ -918,7 +956,9 @@ const COMP_STYLES = [
918
956
  "sustain",
919
957
  "lazy",
920
958
  "stabs",
921
- "arp"
959
+ "arp",
960
+ "rolled",
961
+ "shells"
922
962
  ];
923
963
  const BASS_STYLES = [
924
964
  "roots",
@@ -930,14 +970,18 @@ const CHORD_INSTRUMENTS = [
930
970
  "wurli",
931
971
  "fm-ep",
932
972
  "pad",
933
- "harp"
973
+ "harp",
974
+ "vibes",
975
+ "guitar"
934
976
  ];
935
977
  const GROOVE_IDS = [
936
978
  "dusty",
937
979
  "brushes",
938
980
  "halftime",
939
981
  "bossa",
940
- "sparse"
982
+ "sparse",
983
+ "jazz-ride",
984
+ "lazy-kick"
941
985
  ];
942
986
  const WINDOW_SCENES = [
943
987
  "rain",
@@ -1751,8 +1795,17 @@ function closeVoicing(order, bottom) {
1751
1795
  }
1752
1796
  return notes;
1753
1797
  }
1754
- function candidateVoicings(rootPc, quality, range = VOICING_RANGE) {
1755
- const pcs = [...new Set(QUALITIES[quality].voicing.map((i) => mod12(rootPc + i)))];
1798
+ /**
1799
+ * The intervals a comping voicing uses. Rootless leaves the root to the bass, the way jazz pianists
1800
+ * comp, as long as at least three notes are left (plain triads keep their root).
1801
+ */
1802
+ function voicingIntervals(quality, rootless = false) {
1803
+ const all = QUALITIES[quality].voicing;
1804
+ const without = all.filter((i) => mod12(i) !== 0);
1805
+ return rootless && without.length >= 3 ? without : all;
1806
+ }
1807
+ function candidateVoicings(rootPc, quality, range = VOICING_RANGE, rootless = false) {
1808
+ const pcs = [...new Set(voicingIntervals(quality, rootless).map((i) => mod12(rootPc + i)))];
1756
1809
  const out = [];
1757
1810
  for (let r = 0; r < pcs.length; r++) {
1758
1811
  const order = [...pcs.slice(r), ...pcs.slice(0, r)];
@@ -1781,8 +1834,8 @@ function roughness(v) {
1781
1834
  if (v[v.length - 1] > 74) penalty += 1.5;
1782
1835
  return penalty;
1783
1836
  }
1784
- function voiceChord(rootPc, quality, prev, range = VOICING_RANGE) {
1785
- const candidates = candidateVoicings(rootPc, quality, range);
1837
+ function voiceChord(rootPc, quality, prev, range = VOICING_RANGE, rootless = false) {
1838
+ const candidates = candidateVoicings(rootPc, quality, range, rootless);
1786
1839
  let best = candidates[0] ?? [];
1787
1840
  let bestCost = Infinity;
1788
1841
  for (const v of candidates) {
@@ -1819,6 +1872,11 @@ const GROOVES = {
1819
1872
  kick: "o.......o.......",
1820
1873
  brush: "g...X..g....X.g.",
1821
1874
  brushhat: "x...x.o.x...x.o."
1875
+ },
1876
+ turnaround: {
1877
+ kick: "o.......o.....o.",
1878
+ brush: "g...X..g....XgXg",
1879
+ brushhat: "x...x.o.x...x..."
1822
1880
  }
1823
1881
  },
1824
1882
  halftime: {
@@ -1842,7 +1900,14 @@ const GROOVES = {
1842
1900
  bar: {
1843
1901
  kick: "x..ox..ox..ox..o",
1844
1902
  rim: "x..x..x...x..x..",
1845
- hat: "o.g.o.g.o.g.o.g."
1903
+ hat: "o.g.o.g.o.g.o.g.",
1904
+ cabasa: ".g.o.g.o.g.o.g.o"
1905
+ },
1906
+ turnaround: {
1907
+ kick: "x..ox..ox..ox..o",
1908
+ rim: "..x..x...x..x...",
1909
+ hat: "o.g.o.g.o.g.o.g.",
1910
+ cabasa: ".g.o.g.o.g.o.gxo"
1846
1911
  }
1847
1912
  },
1848
1913
  sparse: {
@@ -1852,6 +1917,46 @@ const GROOVES = {
1852
1917
  kick: "x.......o.......",
1853
1918
  rim: "........o.......",
1854
1919
  ride: "o...o...o...o..."
1920
+ },
1921
+ turnaround: {
1922
+ kick: "x.......o.....o.",
1923
+ rim: "........o...o...",
1924
+ ride: "o...o...o...o.o."
1925
+ }
1926
+ },
1927
+ "jazz-ride": {
1928
+ label: "Jazzy ride",
1929
+ kit: "sticks",
1930
+ bar: {
1931
+ kick: "g...g...g...g...",
1932
+ ride: "x...X..ox...X..o",
1933
+ hat: "....o.......o...",
1934
+ snare: "..........g....."
1935
+ },
1936
+ turnaround: {
1937
+ kick: "g...g...g...g...",
1938
+ ride: "x...X..ox...X...",
1939
+ hat: "....o.......o...",
1940
+ snare: "......g...g..xg."
1941
+ }
1942
+ },
1943
+ "lazy-kick": {
1944
+ label: "Lazy kick",
1945
+ kit: "sticks",
1946
+ bar: {
1947
+ kick: "X.....x...X..x..",
1948
+ snare: "....X.......X...",
1949
+ shaker: "xgogxgogxgogxgog"
1950
+ },
1951
+ turnaround: {
1952
+ kick: "X.....x...X.....",
1953
+ snare: "....X.......X.gg",
1954
+ shaker: "xgogxgogxgogx...",
1955
+ openhat: "..............o."
1956
+ },
1957
+ lag: {
1958
+ kick: .06,
1959
+ snare: .03
1855
1960
  }
1856
1961
  }
1857
1962
  };
@@ -1875,6 +1980,12 @@ function grooveBar(id, turnaround) {
1875
1980
  }
1876
1981
  return hits.sort((a, b) => a.beat - b.beat);
1877
1982
  }
1983
+ /** Pushes the band's drum hits behind the grid by the groove's lag. Run after swing. */
1984
+ function applyGrooveLag(events, id) {
1985
+ const lag = GROOVES[id].lag;
1986
+ if (!lag) return;
1987
+ for (const e of events) if (e.track === "backing:drums" && e.drum) e.time += lag[e.drum] ?? 0;
1988
+ }
1878
1989
  // ../../packages/core/src/engine/backing.ts
1879
1990
  /** Lays the progression across the scene, looping it if it's shorter than the scene. */
1880
1991
  function placeChords(progression, key, totalBeats) {
@@ -1894,6 +2005,16 @@ function placeChords(progression, key, totalBeats) {
1894
2005
  }
1895
2006
  return out;
1896
2007
  }
2008
+ /** Splits a chord into whole bars and hits each one the same way, clipped to the chord's length. */
2009
+ function perBar(beats, pattern) {
2010
+ const hits = [];
2011
+ for (let bar = 0; bar < beats; bar += 4) for (const h of pattern) if (bar + h.at < beats) hits.push({
2012
+ ...h,
2013
+ at: bar + h.at,
2014
+ len: Math.min(h.len, beats - bar - h.at)
2015
+ });
2016
+ return hits;
2017
+ }
1897
2018
  /** Hit times (beat offset, length) for one chord, by comping style. */
1898
2019
  function compRhythm(style, beats) {
1899
2020
  switch (style) {
@@ -1907,19 +2028,44 @@ function compRhythm(style, beats) {
1907
2028
  len: beats - .3,
1908
2029
  strum: .035
1909
2030
  }];
1910
- case "stabs": {
1911
- const hits = [];
1912
- for (let bar = 0; bar < beats; bar += 4) for (const [at, len] of [
1913
- [0, .9],
1914
- [1.5, .4],
1915
- [3, .4]
1916
- ]) if (bar + at < beats) hits.push({
1917
- at: bar + at,
1918
- len: Math.min(len, beats - bar - at),
2031
+ case "stabs": return perBar(beats, [
2032
+ {
2033
+ at: 0,
2034
+ len: .9,
1919
2035
  strum: .01
1920
- });
1921
- return hits;
1922
- }
2036
+ },
2037
+ {
2038
+ at: 1.5,
2039
+ len: .4,
2040
+ strum: .01
2041
+ },
2042
+ {
2043
+ at: 3,
2044
+ len: .4,
2045
+ strum: .01
2046
+ }
2047
+ ]);
2048
+ case "rolled": return perBar(beats, [{
2049
+ at: 0,
2050
+ len: 2.4,
2051
+ strum: .08
2052
+ }, {
2053
+ at: 2.5,
2054
+ len: 1.5,
2055
+ strum: .07,
2056
+ vel: .4
2057
+ }]);
2058
+ case "shells": return perBar(beats, [{
2059
+ at: 0,
2060
+ len: 1.2,
2061
+ strum: .012,
2062
+ vel: .52
2063
+ }, {
2064
+ at: 1.5,
2065
+ len: 2.2,
2066
+ strum: .012,
2067
+ vel: .44
2068
+ }]);
1923
2069
  case "arp": return [];
1924
2070
  }
1925
2071
  }
@@ -1932,7 +2078,7 @@ function compChords(placed, backing, carry) {
1932
2078
  brightness: .6
1933
2079
  };
1934
2080
  for (const p of placed) {
1935
- const voicing = voiceChord(p.rootPc, p.chord.quality, carry.voicing);
2081
+ const voicing = voiceChord(p.rootPc, p.chord.quality, carry.voicing, VOICING_RANGE, backing.style === "shells");
1936
2082
  carry.voicing = voicing;
1937
2083
  if (backing.style === "arp") {
1938
2084
  const order = [...voicing, ...voicing.slice(1, -1).reverse()];
@@ -1952,7 +2098,7 @@ function compChords(placed, backing, carry) {
1952
2098
  time: p.start + at,
1953
2099
  duration: Math.max(.1, hit.len - i * hit.strum),
1954
2100
  midi,
1955
- velocity: .5 - i * .03
2101
+ velocity: (hit.vel ?? .5) - i * .03
1956
2102
  });
1957
2103
  });
1958
2104
  }
@@ -2140,6 +2286,7 @@ function sceneEvents(project, scene, { offset = 0, pass = 0, carry = newCarry()
2140
2286
  const kept = events.filter((e) => e.time < totalBeats);
2141
2287
  for (const e of kept) e.duration = Math.min(e.duration, totalBeats - e.time);
2142
2288
  applySwing(kept, project.swing);
2289
+ if (backing && scene.backing.drums.enabled) applyGrooveLag(kept, scene.backing.drums.groove);
2143
2290
  applyHumanize(kept, project.humanize, hashSeed(project.seed, scene.id), pass);
2144
2291
  for (const e of kept) e.time += offset;
2145
2292
  const flats = prefersFlats(project.key);
@@ -1,4 +1,4 @@
1
- import { S as DRUM_PIECE_IDS, T as INSTRUMENTS, i as buildSceneScore, p as createRng, r as buildArrangementScore } from "./src-dP0jXhPP.mjs";
1
+ import { S as DRUM_PIECE_IDS, T as INSTRUMENTS, i as buildSceneScore, p as createRng, r as buildArrangementScore } from "./src-Bz-0ulVm.mjs";
2
2
  import * as Tone from "tone";
3
3
  // ../../packages/core/src/engine/types.ts
4
4
  function beatsToSeconds(beats, bpm) {
@@ -44,8 +44,13 @@ const INSTRUMENT_TRIM_DB = {
44
44
  glock: -9,
45
45
  marimba: -2,
46
46
  harp: -3,
47
+ vibes: -9,
48
+ kalimba: -3,
49
+ guitar: -9,
47
50
  pad: -11,
48
51
  pluck: -6,
52
+ celesta: -4.5,
53
+ musicbox: -3,
49
54
  bass: -19,
50
55
  drums: 0
51
56
  };
@@ -66,7 +71,9 @@ const DRUM_TRIM_DB = {
66
71
  brushhat: 2,
67
72
  openhat: -12,
68
73
  ride: -10,
69
- tom: -4
74
+ tom: -4,
75
+ shaker: -3,
76
+ cabasa: -10
70
77
  };
71
78
  /** Reverb return level at reverb = 1. */
72
79
  const REVERB_RETURN = .55;
@@ -172,6 +179,46 @@ function synthVoice(instrument) {
172
179
  });
173
180
  filter.Q.value = .5;
174
181
  break;
182
+ case "celesta":
183
+ synth = new Tone.PolySynth(Tone.FMSynth, {
184
+ harmonicity: 1,
185
+ modulationIndex: 16,
186
+ oscillator: { type: "sine" },
187
+ modulation: { type: "sine" },
188
+ envelope: {
189
+ attack: .002,
190
+ decay: 2.2,
191
+ sustain: 0,
192
+ release: 1.2
193
+ },
194
+ modulationEnvelope: {
195
+ attack: .001,
196
+ decay: .35,
197
+ sustain: .01,
198
+ release: .5
199
+ }
200
+ });
201
+ break;
202
+ case "musicbox":
203
+ synth = new Tone.PolySynth(Tone.FMSynth, {
204
+ harmonicity: 6.27,
205
+ modulationIndex: 24,
206
+ oscillator: { type: "sine" },
207
+ modulation: { type: "sine" },
208
+ envelope: {
209
+ attack: .001,
210
+ decay: 1.5,
211
+ sustain: 0,
212
+ release: .7
213
+ },
214
+ modulationEnvelope: {
215
+ attack: .001,
216
+ decay: .5,
217
+ sustain: 0,
218
+ release: .4
219
+ }
220
+ });
221
+ break;
175
222
  case "pluck":
176
223
  synth = new Tone.PolySynth(Tone.Synth, {
177
224
  oscillator: { type: "triangle" },
@@ -1,4 +1,4 @@
1
- import { A as PROGRESSIONS, C as GROOVE_IDS, D as RAIN_KINDS, E as INSTRUMENT_IDS, F as prefersFlats, I as scalePcs, L as mod12, M as resolveProgression, N as MODES, O as WINDOW_SCENES, P as keyLabel, R as noteName, T as INSTRUMENTS, _ as BASS_STYLES, a as layerRange, b as COMP_STYLES, c as createProject, d as findScene, f as parseProject, g as getMood, h as MOOD_IDS, i as buildSceneScore, j as progressionText, k as drumRows, l as createScene, m as makeId, n as describeProject, o as pitchToY, r as buildArrangementScore, s as createLayer, t as scoreToMidi, u as findLayer, v as BRUSH_IDS, w as HARMONY_MODES, x as DRUM_KITS, y as CHORD_INSTRUMENTS, z as parseNote } from "./src-dP0jXhPP.mjs";
1
+ import { A as PROGRESSIONS, C as GROOVE_IDS, D as RAIN_KINDS, E as INSTRUMENT_IDS, F as prefersFlats, I as scalePcs, L as mod12, M as resolveProgression, N as MODES, O as WINDOW_SCENES, P as keyLabel, R as noteName, T as INSTRUMENTS, _ as BASS_STYLES, a as layerRange, b as COMP_STYLES, c as createProject, d as findScene, f as parseProject, g as getMood, h as MOOD_IDS, i as buildSceneScore, j as progressionText, k as drumRows, l as createScene, m as makeId, n as describeProject, o as pitchToY, r as buildArrangementScore, s as createLayer, t as scoreToMidi, u as findLayer, v as BRUSH_IDS, w as HARMONY_MODES, x as DRUM_KITS, y as CHORD_INSTRUMENTS, z as parseNote } from "./src-Bz-0ulVm.mjs";
2
2
  import { readFile, rename, stat, writeFile } from "node:fs/promises";
3
3
  import { delimiter, extname, join, normalize, resolve, sep } from "node:path";
4
4
  import { z } from "zod";
@@ -960,7 +960,7 @@ The canvas:
960
960
 
961
961
  A good loop:
962
962
  1. describe to see what's there. set_mood picks a whole vibe in one go: ${MOOD_IDS.join(", ")}.
963
- 2. add_layer for each part (piano, wurli, fm-ep, glock, marimba, harp, pad, pluck, bass, drums).
963
+ 2. add_layer for each part (piano, wurli, fm-ep, glock, vibes, marimba, kalimba, harp, guitar, pad, pluck, celesta, musicbox, bass, drums).
964
964
  3. Draw with draw_shape, draw_curve, or draw_line. Use batch to send many commands as one step.
965
965
  4. Check your work: describe (text piano roll), snapshot (a picture), analyze (chord fit, clashes, loudness).
966
966
  5. play to listen, export for WAV, MIDI, or the project file.
@@ -1176,7 +1176,7 @@ function loadAudio() {
1176
1176
  kit ??= (async () => {
1177
1177
  const node = await import("./node-CVxAljK8.mjs");
1178
1178
  node.setupNodeAudio();
1179
- const audio = await import("./src-CgZeiBJc.mjs");
1179
+ const audio = await import("./src-DNP73wX3.mjs");
1180
1180
  const dir = samplesDir();
1181
1181
  const bank = await audio.loadSampleBank(await node.readManifest(dir), node.nodeDecoder(dir));
1182
1182
  return {
@@ -1193,7 +1193,7 @@ function loadAudio() {
1193
1193
  return kit;
1194
1194
  }
1195
1195
  // src/version.ts
1196
- const VERSION = "0.2.0";
1196
+ const VERSION = "1.0.0";
1197
1197
  // src/web.ts
1198
1198
  const WEB_HOST = "127.0.0.1";
1199
1199
  const WEB_PORT = 5177;
@@ -1376,20 +1376,8 @@ function snapshotSvg(project, opts = {}) {
1376
1376
  out.push(`<text x="${x0}" y="29" font-size="17" font-weight="600" fill="${C.text}">${esc(project.name)}</text>`);
1377
1377
  const info = `Scene ${scene.name} · ${scene.bars} bars · ${keyLabel(project.key)} · ${project.bpm} BPM`;
1378
1378
  out.push(`<text x="${x1}" y="29" font-size="13" fill="${C.muted}" text-anchor="end">${esc(info)}</text>`);
1379
- out.push(`<rect x="${x0}" y="${header}" width="${cw}" height="${strip}" fill="${C.sunken}" rx="4"/>`);
1380
- for (const [i, chord] of score.chords.entries()) {
1381
- const cx = bx(chord.time);
1382
- const w = bx(chord.time + chord.duration) - cx;
1383
- if (i % 2 === 1) out.push(`<rect x="${n(cx)}" y="${header}" width="${n(w)}" height="${strip}" fill="${C.raised}" opacity="0.5"/>`);
1384
- out.push(`<text x="${n(cx + 9)}" y="66" font-size="12.5" font-weight="600" fill="${C.muted}">${esc(chord.symbol)}</text>`);
1385
- }
1386
- out.push(`<rect x="${x0}" y="${top}" width="${cw}" height="${ch}" fill="${C.sunken}" opacity="0.55"/>`);
1387
- for (let b = 0; b <= beats; b++) {
1388
- const gx = n(bx(b));
1389
- const bar = b % 4 === 0;
1390
- out.push(`<line x1="${gx}" y1="${bar ? header : top}" x2="${gx}" y2="${bottom}" stroke="${bar ? C.strong : C.border}" opacity="${bar ? .9 : .45}"/>`);
1391
- if (bar && b > 0) out.push(`<text x="${n(gx - 6)}" y="${bottom - 6}" font-size="10" fill="${C.faint}" text-anchor="end">${b / 4}</text>`);
1392
- }
1379
+ chordStrip(out, score, bx, x0, cw, header, strip);
1380
+ grid(out, beats, bx, x0, cw, header, top, bottom);
1393
1381
  if (focus && INSTRUMENTS[focus.instrument].kind === "drums") {
1394
1382
  const rows = drumRows(focus.kit);
1395
1383
  rows.forEach((piece, i) => {
@@ -1408,27 +1396,7 @@ function snapshotSvg(project, opts = {}) {
1408
1396
  const layers = [...scene.layers].sort((a, b) => Number(a.id === focus?.id) - Number(b.id === focus?.id));
1409
1397
  for (const layer of layers) {
1410
1398
  const front = layer.id === focus?.id;
1411
- const drums = INSTRUMENTS[layer.instrument].kind === "drums";
1412
- const range = layerRange(layer);
1413
- const rows = drumRows(layer.kit);
1414
- out.push(`<g fill="${layer.color}" opacity="${front ? .75 : .32}">`);
1415
- for (const e of score.events) {
1416
- if (e.track !== layer.id) continue;
1417
- let ny;
1418
- let nh;
1419
- if (drums) {
1420
- ny = 1 - (Math.max(0, rows.indexOf(e.drum)) + 1) / rows.length;
1421
- nh = 1 / rows.length;
1422
- } else {
1423
- nh = 1 / (range[1] - range[0]);
1424
- ny = pitchToY(e.midi, range) - nh / 2;
1425
- }
1426
- const rx = bx(e.time);
1427
- const rw = Math.max(3, bx(e.time + e.duration) - rx - 1.5);
1428
- const rh = Math.max(4, nh * ch - 1.5);
1429
- out.push(`<rect x="${n(rx)}" y="${n(top + ny * ch)}" width="${n(rw)}" height="${n(rh)}" rx="${n(Math.min(3, rh / 2))}"/>`);
1430
- }
1431
- out.push("</g>");
1399
+ notes(out, layer, score, bx, top, ch, front ? .75 : .32);
1432
1400
  for (const s of layer.strokes) {
1433
1401
  const width = n(2 + s.size * 10);
1434
1402
  const opacity = front ? .85 : .2;
@@ -1455,6 +1423,118 @@ function snapshotSvg(project, opts = {}) {
1455
1423
  out.push("</svg>");
1456
1424
  return out.join("\n");
1457
1425
  }
1426
+ /** The whole arrangement: song order on top, then one panel per scene, all on the same time scale. */
1427
+ function songSnapshotSvg(project, opts = {}) {
1428
+ const W = opts.width ?? 1200;
1429
+ const pad = 20;
1430
+ const header = 46;
1431
+ const order = 34;
1432
+ const label = 26;
1433
+ const strip = 24;
1434
+ const canvas = 120;
1435
+ const gap = 16;
1436
+ const x0 = pad;
1437
+ const x1 = W - pad;
1438
+ const cw = x1 - x0;
1439
+ const items = project.arrangement.flatMap((item) => {
1440
+ const scene = project.scenes.find((s) => s.id === item.sceneId);
1441
+ return scene ? [{
1442
+ scene,
1443
+ repeat: item.repeat
1444
+ }] : [];
1445
+ });
1446
+ const scenes = [...new Map(items.map((i) => [i.scene.id, i.scene])).values()];
1447
+ const unused = project.scenes.filter((s) => !scenes.includes(s));
1448
+ const plays = (s) => items.filter((i) => i.scene.id === s.id).reduce((sum, i) => sum + i.repeat, 0);
1449
+ const totalBars = items.reduce((sum, i) => sum + i.scene.bars * i.repeat, 0);
1450
+ const maxBeats = Math.max(4, ...scenes.map((s) => s.bars * 4));
1451
+ const bx = (b) => x0 + b / maxBeats * cw;
1452
+ const panelTop = 102;
1453
+ const panel = 186;
1454
+ const H = panelTop + scenes.length * panel + pad - gap;
1455
+ const out = [];
1456
+ out.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family="${FONT}">`);
1457
+ out.push(`<rect width="${W}" height="${H}" fill="${C.bg}"/>`);
1458
+ out.push(`<text x="${x0}" y="29" font-size="17" font-weight="600" fill="${C.text}">${esc(project.name)}</text>`);
1459
+ const info = `Song · ${`${scenes.length} ${scenes.length === 1 ? "scene" : "scenes"}`} · ${totalBars} bars · ${keyLabel(project.key)} · ${project.bpm} BPM`;
1460
+ out.push(`<text x="${x1}" y="29" font-size="13" fill="${C.muted}" text-anchor="end">${esc(info)}</text>`);
1461
+ let ox = x0;
1462
+ for (const [i, item] of items.entries()) {
1463
+ const w = item.scene.bars * item.repeat * cw / Math.max(1, totalBars);
1464
+ out.push(`<rect x="${n(ox)}" y="${header}" width="${n(Math.max(1, w - 2))}" height="${order}" rx="4" fill="${i % 2 ? C.raised : C.sunken}" stroke="${C.border}"/>`);
1465
+ const text = item.repeat > 1 ? `${item.scene.name} ×${item.repeat}` : item.scene.name;
1466
+ if (w > text.length * 7.5 + 14) out.push(`<text x="${n(ox + 9)}" y="68" font-size="12.5" font-weight="600" fill="${C.text}">${esc(text)}</text>`);
1467
+ ox += w;
1468
+ }
1469
+ for (const [i, scene] of scenes.entries()) {
1470
+ const y = panelTop + i * panel;
1471
+ const score = buildSceneScore(project, scene.id);
1472
+ const beats = scene.bars * 4;
1473
+ const sx1 = bx(beats);
1474
+ const top = y + label + strip;
1475
+ const bottom = top + canvas;
1476
+ const times = plays(scene);
1477
+ const ids = new Set(scene.layers.map((l) => l.id));
1478
+ const noteTotal = score.events.filter((e) => ids.has(e.track)).length;
1479
+ const head = `Scene ${scene.name} · ${scene.bars} ${scene.bars === 1 ? "bar" : "bars"} · plays ${times}×`;
1480
+ const tail = `${scene.layers.length} ${scene.layers.length === 1 ? "layer" : "layers"} · ${noteTotal} notes`;
1481
+ out.push(`<text x="${x0}" y="${y + 17}" font-size="13" font-weight="600" fill="${C.text}">${esc(head)}</text>`);
1482
+ out.push(`<text x="${x1}" y="${y + 17}" font-size="12" fill="${C.muted}" text-anchor="end">${esc(tail)}</text>`);
1483
+ chordStrip(out, score, bx, x0, sx1 - x0, y + label, strip);
1484
+ grid(out, beats, bx, x0, sx1 - x0, y + label, top, bottom);
1485
+ for (const layer of scene.layers) notes(out, layer, score, bx, top, canvas, layer.muted ? .25 : .7);
1486
+ if (!scene.layers.length) out.push(`<text x="30" y="${top + 22}" font-size="12" fill="${C.faint}">No layers yet</text>`);
1487
+ }
1488
+ out.push("</svg>");
1489
+ return {
1490
+ svg: out.join("\n"),
1491
+ scenes,
1492
+ unused
1493
+ };
1494
+ }
1495
+ function chordStrip(out, score, bx, x0, width, y, h) {
1496
+ out.push(`<rect x="${x0}" y="${y}" width="${n(width)}" height="${h}" fill="${C.sunken}" rx="4"/>`);
1497
+ for (const [i, chord] of score.chords.entries()) {
1498
+ const cx = bx(chord.time);
1499
+ const w = bx(chord.time + chord.duration) - cx;
1500
+ if (i % 2 === 1) out.push(`<rect x="${n(cx)}" y="${y}" width="${n(w)}" height="${h}" fill="${C.raised}" opacity="0.5"/>`);
1501
+ out.push(`<text x="${n(cx + 9)}" y="${n(y + h / 2 + 5)}" font-size="12.5" font-weight="600" fill="${C.muted}">${esc(chord.symbol)}</text>`);
1502
+ }
1503
+ }
1504
+ /** The canvas background and beat lines. Bar lines run up through the chord strip from stripTop. */
1505
+ function grid(out, beats, bx, x0, width, stripTop, top, bottom) {
1506
+ out.push(`<rect x="${x0}" y="${top}" width="${n(width)}" height="${bottom - top}" fill="${C.sunken}" opacity="0.55"/>`);
1507
+ for (let b = 0; b <= beats; b++) {
1508
+ const gx = n(bx(b));
1509
+ const bar = b % 4 === 0;
1510
+ out.push(`<line x1="${gx}" y1="${bar ? stripTop : top}" x2="${gx}" y2="${bottom}" stroke="${bar ? C.strong : C.border}" opacity="${bar ? .9 : .45}"/>`);
1511
+ if (bar && b > 0) out.push(`<text x="${n(gx - 6)}" y="${bottom - 6}" font-size="10" fill="${C.faint}" text-anchor="end">${b / 4}</text>`);
1512
+ }
1513
+ }
1514
+ /** One layer's notes as pills: drums on their rows, tonal parts by pitch across the layer's range. */
1515
+ function notes(out, layer, score, bx, top, ch, opacity) {
1516
+ const drums = INSTRUMENTS[layer.instrument].kind === "drums";
1517
+ const range = layerRange(layer);
1518
+ const rows = drumRows(layer.kit);
1519
+ out.push(`<g fill="${layer.color}" opacity="${opacity}">`);
1520
+ for (const e of score.events) {
1521
+ if (e.track !== layer.id) continue;
1522
+ let ny;
1523
+ let nh;
1524
+ if (drums) {
1525
+ ny = 1 - (Math.max(0, rows.indexOf(e.drum)) + 1) / rows.length;
1526
+ nh = 1 / rows.length;
1527
+ } else {
1528
+ nh = 1 / (range[1] - range[0]);
1529
+ ny = pitchToY(e.midi, range) - nh / 2;
1530
+ }
1531
+ const rx = bx(e.time);
1532
+ const rw = Math.max(3, bx(e.time + e.duration) - rx - 1.5);
1533
+ const rh = Math.max(4, nh * ch - 1.5);
1534
+ out.push(`<rect x="${n(rx)}" y="${n(top + ny * ch)}" width="${n(rw)}" height="${n(rh)}" rx="${n(Math.min(3, rh / 2))}"/>`);
1535
+ }
1536
+ out.push("</g>");
1537
+ }
1458
1538
  /** PNG bytes. resvg is a native module, so it's only loaded when a PNG is actually asked for. */
1459
1539
  async function snapshotPng(svg, width) {
1460
1540
  const { Resvg } = await import("@resvg/resvg-js");
@@ -1741,25 +1821,35 @@ const stop = define({
1741
1821
  const snapshot = define({
1742
1822
  name: "snapshot",
1743
1823
  title: "Snapshot",
1744
- description: "A picture of a scene: chords along the top, each layer's notes and strokes, and a legend. Returns a PNG. Pass out to also save it (.png or .svg).",
1824
+ description: "A picture of a scene: chords along the top, each layer's notes and strokes, and a legend. With scope song, the whole arrangement instead: the song order on top, then the chords and notes of each scene in it. Returns a PNG. Pass out to also save it (.png or .svg).",
1745
1825
  readOnly: true,
1746
1826
  input: z.object({
1747
1827
  file,
1828
+ scope,
1748
1829
  scene: sceneArg,
1749
- layer: z.string().optional().describe("Layer to bring to the front, with its pitch guides"),
1830
+ layer: z.string().optional().describe("Layer to bring to the front, with its pitch guides. Scene scope only."),
1750
1831
  width: z.number().int().min(400).max(2400).optional().describe("Image width in pixels. Default 1200."),
1751
1832
  out: z.string().optional().describe("Also save the picture here (.png or .svg)")
1752
1833
  }),
1753
- async run({ file: path, scene, layer, width = 1200, out }, ctx) {
1834
+ async run({ file: path, scope = "scene", scene, layer, width = 1200, out }, ctx) {
1754
1835
  const project = await (await targetFor(ctx, path)).read();
1755
- const s = sceneOf(project, scene);
1756
- const svg = snapshotSvg(project, {
1757
- scene: s.id,
1758
- layer,
1759
- width,
1760
- height: Math.round(width * .53)
1761
- });
1762
- const notes = [`Snapshot of scene ${s.name} in "${project.name}"`];
1836
+ let svg;
1837
+ const notes = [];
1838
+ if (scope === "song") {
1839
+ const shot = songSnapshotSvg(project, { width });
1840
+ svg = shot.svg;
1841
+ notes.push(`Snapshot of the song in "${project.name}" (${shot.scenes.map((s) => s.name).join(", ")})`);
1842
+ if (shot.unused.length) notes.push(`not in the song: ${shot.unused.map((s) => s.name).join(", ")}`);
1843
+ } else {
1844
+ const s = sceneOf(project, scene);
1845
+ svg = snapshotSvg(project, {
1846
+ scene: s.id,
1847
+ layer,
1848
+ width,
1849
+ height: Math.round(width * .53)
1850
+ });
1851
+ notes.push(`Snapshot of scene ${s.name} in "${project.name}"`);
1852
+ }
1763
1853
  if (out?.toLowerCase().endsWith(".svg")) {
1764
1854
  const full = resolve(ctx.cwd, out);
1765
1855
  await writeFile(full, svg);