@pinkpixel/haku 0.1.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 (121) hide show
  1. package/README.md +18 -11
  2. package/dist/cli.mjs +38 -4
  3. package/dist/{mcp-DDuNECfs.mjs → mcp-DsSn95Rn.mjs} +3 -2
  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-D5fW7AC1.mjs → src-Bz-0ulVm.mjs} +179 -23
  104. package/dist/{src-JrRbTOJC.mjs → src-DNP73wX3.mjs} +60 -3
  105. package/dist/{version-DoZ2etXG.mjs → tools-CO-tdB7d.mjs} +321 -82
  106. package/dist/web/assets/esm-J-U37_Wq.js +210 -0
  107. package/dist/web/assets/figtree-latin-ext-wght-normal-DCwSJGxG.woff2 +0 -0
  108. package/dist/web/assets/figtree-latin-wght-normal-D_ZTVpCC.woff2 +0 -0
  109. package/dist/web/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
  110. package/dist/web/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
  111. package/dist/web/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
  112. package/dist/web/assets/index-BkzOde44.js +65 -0
  113. package/dist/web/assets/index-D0m3L8JK.css +1 -0
  114. package/dist/web/assets/rolldown-runtime-BWaJ3C7D.js +1 -0
  115. package/dist/web/assets/src-m48ItyJA.js +1 -0
  116. package/dist/web/icon-192.png +0 -0
  117. package/dist/web/icon-512.png +0 -0
  118. package/dist/web/index.html +19 -0
  119. package/dist/web/manifest.webmanifest +24 -0
  120. package/dist/web/sw.js +37 -0
  121. package/package.json +2 -2
@@ -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",
@@ -1412,11 +1456,20 @@ function beatStrength(beat) {
1412
1456
  if (Math.abs(inBar - Math.round(inBar)) > 1e-6) return "weak";
1413
1457
  return Math.round(inBar) % 2 === 0 ? "strong" : "medium";
1414
1458
  }
1459
+ /**
1460
+ * The key's scale bent to fit the chord. When a chord brings in a note from outside the key (G# in
1461
+ * E7 over A minor), scale tones a half step from it drop out, so G and G# never both show up.
1462
+ * Chord tones always stay.
1463
+ */
1464
+ function fittedScale(scale, chord) {
1465
+ const outside = chord.filter((pc) => !scale.includes(pc));
1466
+ return scale.filter((pc) => chord.includes(pc) || !outside.some((o) => mod12(pc - o) === 1 || mod12(o - pc) === 1));
1467
+ }
1415
1468
  /** The pitch classes a note is allowed to land on in this context. */
1416
1469
  function allowedPcs(ctx) {
1417
1470
  const tonic = tonicPc(ctx.key);
1418
- const scale = scalePcs(ctx.key);
1419
1471
  const chord = chordPcs(mod12(tonic + ctx.chord.root), ctx.chord.quality);
1472
+ const scale = ctx.mode === "smart" ? fittedScale(scalePcs(ctx.key), chord) : scalePcs(ctx.key);
1420
1473
  switch (ctx.mode) {
1421
1474
  case "chromatic": return [
1422
1475
  0,
@@ -1742,8 +1795,17 @@ function closeVoicing(order, bottom) {
1742
1795
  }
1743
1796
  return notes;
1744
1797
  }
1745
- function candidateVoicings(rootPc, quality, range = VOICING_RANGE) {
1746
- 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)))];
1747
1809
  const out = [];
1748
1810
  for (let r = 0; r < pcs.length; r++) {
1749
1811
  const order = [...pcs.slice(r), ...pcs.slice(0, r)];
@@ -1772,8 +1834,8 @@ function roughness(v) {
1772
1834
  if (v[v.length - 1] > 74) penalty += 1.5;
1773
1835
  return penalty;
1774
1836
  }
1775
- function voiceChord(rootPc, quality, prev, range = VOICING_RANGE) {
1776
- 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);
1777
1839
  let best = candidates[0] ?? [];
1778
1840
  let bestCost = Infinity;
1779
1841
  for (const v of candidates) {
@@ -1810,6 +1872,11 @@ const GROOVES = {
1810
1872
  kick: "o.......o.......",
1811
1873
  brush: "g...X..g....X.g.",
1812
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..."
1813
1880
  }
1814
1881
  },
1815
1882
  halftime: {
@@ -1833,7 +1900,14 @@ const GROOVES = {
1833
1900
  bar: {
1834
1901
  kick: "x..ox..ox..ox..o",
1835
1902
  rim: "x..x..x...x..x..",
1836
- 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"
1837
1911
  }
1838
1912
  },
1839
1913
  sparse: {
@@ -1843,6 +1917,46 @@ const GROOVES = {
1843
1917
  kick: "x.......o.......",
1844
1918
  rim: "........o.......",
1845
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
1846
1960
  }
1847
1961
  }
1848
1962
  };
@@ -1866,6 +1980,12 @@ function grooveBar(id, turnaround) {
1866
1980
  }
1867
1981
  return hits.sort((a, b) => a.beat - b.beat);
1868
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
+ }
1869
1989
  // ../../packages/core/src/engine/backing.ts
1870
1990
  /** Lays the progression across the scene, looping it if it's shorter than the scene. */
1871
1991
  function placeChords(progression, key, totalBeats) {
@@ -1885,6 +2005,16 @@ function placeChords(progression, key, totalBeats) {
1885
2005
  }
1886
2006
  return out;
1887
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
+ }
1888
2018
  /** Hit times (beat offset, length) for one chord, by comping style. */
1889
2019
  function compRhythm(style, beats) {
1890
2020
  switch (style) {
@@ -1898,19 +2028,44 @@ function compRhythm(style, beats) {
1898
2028
  len: beats - .3,
1899
2029
  strum: .035
1900
2030
  }];
1901
- case "stabs": {
1902
- const hits = [];
1903
- for (let bar = 0; bar < beats; bar += 4) for (const [at, len] of [
1904
- [0, .9],
1905
- [1.5, .4],
1906
- [3, .4]
1907
- ]) if (bar + at < beats) hits.push({
1908
- at: bar + at,
1909
- len: Math.min(len, beats - bar - at),
2031
+ case "stabs": return perBar(beats, [
2032
+ {
2033
+ at: 0,
2034
+ len: .9,
1910
2035
  strum: .01
1911
- });
1912
- return hits;
1913
- }
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
+ }]);
1914
2069
  case "arp": return [];
1915
2070
  }
1916
2071
  }
@@ -1923,7 +2078,7 @@ function compChords(placed, backing, carry) {
1923
2078
  brightness: .6
1924
2079
  };
1925
2080
  for (const p of placed) {
1926
- 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");
1927
2082
  carry.voicing = voicing;
1928
2083
  if (backing.style === "arp") {
1929
2084
  const order = [...voicing, ...voicing.slice(1, -1).reverse()];
@@ -1943,7 +2098,7 @@ function compChords(placed, backing, carry) {
1943
2098
  time: p.start + at,
1944
2099
  duration: Math.max(.1, hit.len - i * hit.strum),
1945
2100
  midi,
1946
- velocity: .5 - i * .03
2101
+ velocity: (hit.vel ?? .5) - i * .03
1947
2102
  });
1948
2103
  });
1949
2104
  }
@@ -2131,6 +2286,7 @@ function sceneEvents(project, scene, { offset = 0, pass = 0, carry = newCarry()
2131
2286
  const kept = events.filter((e) => e.time < totalBeats);
2132
2287
  for (const e of kept) e.duration = Math.min(e.duration, totalBeats - e.time);
2133
2288
  applySwing(kept, project.swing);
2289
+ if (backing && scene.backing.drums.enabled) applyGrooveLag(kept, scene.backing.drums.groove);
2134
2290
  applyHumanize(kept, project.humanize, hashSeed(project.seed, scene.id), pass);
2135
2291
  for (const e of kept) e.time += offset;
2136
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-D5fW7AC1.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" },
@@ -550,7 +597,17 @@ function loopScore(score, loops) {
550
597
  function scoreFor(project, opts = {}) {
551
598
  return opts.scope === "song" ? buildArrangementScore(project) : loopScore(buildSceneScore(project, opts.sceneId), opts.loops ?? 1);
552
599
  }
553
- async function renderProject(project, bank, opts = {}) {
600
+ /**
601
+ * Tone.Offline swaps Tone's global context while it runs, so two overlapping renders would build
602
+ * nodes on each other's context. Renders wait their turn instead.
603
+ */
604
+ let queue = Promise.resolve();
605
+ function renderProject(project, bank, opts = {}) {
606
+ const run = queue.then(() => renderNow(project, bank, opts));
607
+ queue = run.catch(() => {});
608
+ return run;
609
+ }
610
+ async function renderNow(project, bank, opts) {
554
611
  const score = scoreFor(project, opts);
555
612
  const sampleRate = opts.sampleRate ?? 44100;
556
613
  const seconds = LEAD_IN + beatsToSeconds(score.beats, score.bpm) + (opts.tail ?? 3);