@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.
- package/README.md +5 -2
- package/dist/cli.mjs +2 -2
- package/dist/{mcp-DG9U-P0N.mjs → mcp-DsSn95Rn.mjs} +1 -1
- package/dist/samples/drums/cabasa_v0_r1.ogg +0 -0
- package/dist/samples/drums/cabasa_v0_r2.ogg +0 -0
- package/dist/samples/drums/cabasa_v1_r1.ogg +0 -0
- package/dist/samples/drums/cabasa_v1_r2.ogg +0 -0
- package/dist/samples/drums/shaker_v0_r1.ogg +0 -0
- package/dist/samples/drums/shaker_v0_r2.ogg +0 -0
- package/dist/samples/drums/shaker_v1_r1.ogg +0 -0
- package/dist/samples/drums/shaker_v1_r2.ogg +0 -0
- package/dist/samples/guitar/v0_31.ogg +0 -0
- package/dist/samples/guitar/v0_32.ogg +0 -0
- package/dist/samples/guitar/v0_33.ogg +0 -0
- package/dist/samples/guitar/v0_34.ogg +0 -0
- package/dist/samples/guitar/v0_35.ogg +0 -0
- package/dist/samples/guitar/v0_36.ogg +0 -0
- package/dist/samples/guitar/v0_37.ogg +0 -0
- package/dist/samples/guitar/v0_38.ogg +0 -0
- package/dist/samples/guitar/v0_39.ogg +0 -0
- package/dist/samples/guitar/v0_40.ogg +0 -0
- package/dist/samples/guitar/v0_41.ogg +0 -0
- package/dist/samples/guitar/v0_43.ogg +0 -0
- package/dist/samples/guitar/v0_45.ogg +0 -0
- package/dist/samples/guitar/v0_47.ogg +0 -0
- package/dist/samples/guitar/v0_48.ogg +0 -0
- package/dist/samples/guitar/v0_50.ogg +0 -0
- package/dist/samples/guitar/v0_52.ogg +0 -0
- package/dist/samples/guitar/v0_53.ogg +0 -0
- package/dist/samples/guitar/v0_54.ogg +0 -0
- package/dist/samples/guitar/v0_55.ogg +0 -0
- package/dist/samples/guitar/v0_56.ogg +0 -0
- package/dist/samples/guitar/v0_57.ogg +0 -0
- package/dist/samples/guitar/v0_58.ogg +0 -0
- package/dist/samples/guitar/v0_59.ogg +0 -0
- package/dist/samples/guitar/v0_60.ogg +0 -0
- package/dist/samples/guitar/v0_61.ogg +0 -0
- package/dist/samples/guitar/v0_62.ogg +0 -0
- package/dist/samples/guitar/v0_63.ogg +0 -0
- package/dist/samples/guitar/v0_64.ogg +0 -0
- package/dist/samples/guitar/v0_65.ogg +0 -0
- package/dist/samples/guitar/v0_66.ogg +0 -0
- package/dist/samples/guitar/v0_67.ogg +0 -0
- package/dist/samples/guitar/v0_69.ogg +0 -0
- package/dist/samples/guitar/v0_70.ogg +0 -0
- package/dist/samples/guitar/v0_71.ogg +0 -0
- package/dist/samples/guitar/v0_72.ogg +0 -0
- package/dist/samples/guitar/v0_73.ogg +0 -0
- package/dist/samples/guitar/v0_74.ogg +0 -0
- package/dist/samples/guitar/v0_75.ogg +0 -0
- package/dist/samples/guitar/v0_76.ogg +0 -0
- package/dist/samples/guitar/v0_77.ogg +0 -0
- package/dist/samples/guitar/v0_78.ogg +0 -0
- package/dist/samples/guitar/v0_79.ogg +0 -0
- package/dist/samples/guitar/v0_80.ogg +0 -0
- package/dist/samples/guitar/v0_81.ogg +0 -0
- package/dist/samples/guitar/v0_82.ogg +0 -0
- package/dist/samples/guitar/v0_83.ogg +0 -0
- package/dist/samples/guitar/v0_84.ogg +0 -0
- package/dist/samples/kalimba/v0_44.ogg +0 -0
- package/dist/samples/kalimba/v0_46.ogg +0 -0
- package/dist/samples/kalimba/v0_49.ogg +0 -0
- package/dist/samples/kalimba/v0_51.ogg +0 -0
- package/dist/samples/kalimba/v0_53.ogg +0 -0
- package/dist/samples/kalimba/v0_56.ogg +0 -0
- package/dist/samples/kalimba/v0_59.ogg +0 -0
- package/dist/samples/kalimba/v0_61.ogg +0 -0
- package/dist/samples/kalimba/v0_64.ogg +0 -0
- package/dist/samples/kalimba/v0_65.ogg +0 -0
- package/dist/samples/kalimba/v0_67.ogg +0 -0
- package/dist/samples/kalimba/v0_71.ogg +0 -0
- package/dist/samples/kalimba/v0_73.ogg +0 -0
- package/dist/samples/kalimba/v0_74.ogg +0 -0
- package/dist/samples/kalimba/v0_75.ogg +0 -0
- package/dist/samples/kalimba/v0_77.ogg +0 -0
- package/dist/samples/kalimba/v0_78.ogg +0 -0
- package/dist/samples/kalimba/v0_79.ogg +0 -0
- package/dist/samples/kalimba/v0_82.ogg +0 -0
- package/dist/samples/kalimba/v0_83.ogg +0 -0
- package/dist/samples/manifest.json +181 -0
- package/dist/samples/vibes/v0_53.ogg +0 -0
- package/dist/samples/vibes/v0_57.ogg +0 -0
- package/dist/samples/vibes/v0_60.ogg +0 -0
- package/dist/samples/vibes/v0_64.ogg +0 -0
- package/dist/samples/vibes/v0_67.ogg +0 -0
- package/dist/samples/vibes/v0_71.ogg +0 -0
- package/dist/samples/vibes/v0_74.ogg +0 -0
- package/dist/samples/vibes/v0_77.ogg +0 -0
- package/dist/samples/vibes/v0_81.ogg +0 -0
- package/dist/samples/vibes/v0_84.ogg +0 -0
- package/dist/samples/vibes/v0_88.ogg +0 -0
- package/dist/samples/vibes/v1_53.ogg +0 -0
- package/dist/samples/vibes/v1_57.ogg +0 -0
- package/dist/samples/vibes/v1_60.ogg +0 -0
- package/dist/samples/vibes/v1_64.ogg +0 -0
- package/dist/samples/vibes/v1_67.ogg +0 -0
- package/dist/samples/vibes/v1_71.ogg +0 -0
- package/dist/samples/vibes/v1_74.ogg +0 -0
- package/dist/samples/vibes/v1_77.ogg +0 -0
- package/dist/samples/vibes/v1_81.ogg +0 -0
- package/dist/samples/vibes/v1_84.ogg +0 -0
- package/dist/samples/vibes/v1_88.ogg +0 -0
- package/dist/{src-dP0jXhPP.mjs → src-Bz-0ulVm.mjs} +169 -22
- package/dist/{src-CgZeiBJc.mjs → src-DNP73wX3.mjs} +49 -2
- package/dist/{tools-_xkF2Yfv.mjs → tools-CO-tdB7d.mjs} +140 -50
- package/dist/web/assets/esm-J-U37_Wq.js +210 -0
- package/dist/web/assets/index-BkzOde44.js +65 -0
- package/dist/web/assets/index-D0m3L8JK.css +1 -0
- package/dist/web/assets/rolldown-runtime-BWaJ3C7D.js +1 -0
- package/dist/web/assets/src-m48ItyJA.js +1 -0
- package/dist/web/index.html +3 -2
- package/dist/web/sw.js +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BmW6jd5Q.js +0 -272
- 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
|
-
|
|
1755
|
-
|
|
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
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
|
1380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
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);
|