@onjmin/dtm 0.1.0 → 0.1.4

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/dist/index.mjs CHANGED
@@ -190,7 +190,7 @@ var buildChordPlacements = (options) => {
190
190
  if (notes.length === 0) return;
191
191
  const startStep = barIndex * chordLength;
192
192
  notes.forEach((noteOffset, i) => {
193
- const stepOffset = i * 2;
193
+ const stepOffset = i * 3;
194
194
  placements.push({
195
195
  startStep: startStep + stepOffset,
196
196
  pitch: C3 + noteOffset + offset,
@@ -285,7 +285,10 @@ var buildUI = (target, options) => {
285
285
  <div class="dtm-tracks" data-dtm="track-tabs"></div>
286
286
 
287
287
  <div class="dtm-roll-wrap">
288
- <div class="dtm-roll" data-dtm="roll"><div data-dtm="wrapper" style="position:absolute;inset:0;"></div></div>
288
+ <div class="dtm-roll" data-dtm="roll">
289
+ <div data-dtm="wrapper" style="position:absolute;inset:0;"></div>
290
+ <div class="dtm-overlay" data-dtm="overlay" hidden><div class="dtm-spinner"></div></div>
291
+ </div>
289
292
  <div class="dtm-vscroll" data-dtm="vscroll"><div class="dtm-vscroll-thumb" data-dtm="vscroll-thumb"></div></div>
290
293
  </div>
291
294
  <div class="dtm-hscroll" data-dtm="hscroll"><div class="dtm-hscroll-thumb" data-dtm="hscroll-thumb"></div></div>
@@ -418,8 +421,6 @@ var buildUI = (target, options) => {
418
421
  </div>
419
422
  </div>
420
423
  </details>
421
-
422
- <div class="dtm-overlay" data-dtm="overlay" hidden><div class="dtm-spinner"></div></div>
423
424
  </div>`;
424
425
  const root = q(target, '[data-dtm="root"]');
425
426
  const sel = (name) => q(root, `[data-dtm="${name}"]`);
@@ -608,6 +609,1155 @@ var DRUM_PATTERNS = {
608
609
  ]
609
610
  };
610
611
 
612
+ // node_modules/.pnpm/@onjmin+koe@1.0.3/node_modules/@onjmin/koe/dist/index.js
613
+ var MAGIC = 1263486208;
614
+ function parseKoeHeader(headerBytes) {
615
+ const view = new DataView(headerBytes);
616
+ if (view.byteLength < 8 || view.getUint32(0, false) !== MAGIC) {
617
+ throw new Error("Not a .koe file (bad magic)");
618
+ }
619
+ return { jsonLength: view.getUint32(4, true) };
620
+ }
621
+ var pcmBase = (jsonLength) => 8 + jsonLength;
622
+ var BlobVoiceSource = class {
623
+ constructor(blob, base) {
624
+ this.blob = blob;
625
+ this.base = base;
626
+ }
627
+ blob;
628
+ base;
629
+ readBytes(offset, length) {
630
+ const start = this.base + offset;
631
+ return this.blob.slice(start, start + length).arrayBuffer();
632
+ }
633
+ };
634
+ var RangeVoiceSource = class {
635
+ constructor(url, base) {
636
+ this.url = url;
637
+ this.base = base;
638
+ }
639
+ url;
640
+ base;
641
+ async readBytes(offset, length) {
642
+ const start = this.base + offset;
643
+ const res = await fetch(this.url, {
644
+ headers: { Range: `bytes=${start}-${start + length - 1}` }
645
+ });
646
+ if (!res.ok && res.status !== 206) {
647
+ throw new Error(`.koe range request failed: ${res.status}`);
648
+ }
649
+ return res.arrayBuffer();
650
+ }
651
+ };
652
+ async function rangeFetch(url, start, length) {
653
+ const res = await fetch(url, {
654
+ headers: { Range: `bytes=${start}-${start + length - 1}` }
655
+ });
656
+ if (!res.ok && res.status !== 206)
657
+ throw new Error(`.koe fetch failed: ${res.status}`);
658
+ return res.arrayBuffer();
659
+ }
660
+ var VoiceBank = class _VoiceBank {
661
+ constructor(manifest, source) {
662
+ this.manifest = manifest;
663
+ this.source = source;
664
+ }
665
+ manifest;
666
+ source;
667
+ /**
668
+ * Parse a .koe archive header + manifest and bind a lazy PCM source.
669
+ * @param koe a Blob/File of the .koe archive, or a URL (served with Range support)
670
+ */
671
+ static async load(koe) {
672
+ if (typeof koe === "string") {
673
+ const header2 = await rangeFetch(koe, 0, 8);
674
+ const { jsonLength: jsonLength2 } = parseKoeHeader(header2);
675
+ const json2 = await rangeFetch(koe, 8, jsonLength2);
676
+ const manifest2 = JSON.parse(new TextDecoder().decode(json2));
677
+ return new _VoiceBank(
678
+ manifest2,
679
+ new RangeVoiceSource(koe, pcmBase(jsonLength2))
680
+ );
681
+ }
682
+ const header = await koe.slice(0, 8).arrayBuffer();
683
+ const { jsonLength } = parseKoeHeader(header);
684
+ const json = await koe.slice(8, 8 + jsonLength).arrayBuffer();
685
+ const manifest = JSON.parse(new TextDecoder().decode(json));
686
+ return new _VoiceBank(
687
+ manifest,
688
+ new BlobVoiceSource(koe, pcmBase(jsonLength))
689
+ );
690
+ }
691
+ /** True if the bank contains a phoneme under this alias. */
692
+ has(phoneme) {
693
+ return this.manifest.phonemes[phoneme] !== void 0;
694
+ }
695
+ /**
696
+ * Raw Int16 PCM bytes (48 kHz / mono) for a phoneme, or null if unknown.
697
+ * The returned ArrayBuffer is freshly allocated and safe to transfer to a
698
+ * worker / AudioWorklet.
699
+ */
700
+ async readPcmBytes(phoneme) {
701
+ const entry = this.manifest.phonemes[phoneme];
702
+ if (!entry) return null;
703
+ return this.source.readBytes(entry.offset, entry.length * 2);
704
+ }
705
+ /**
706
+ * A phoneme's PCM as a Float64Array normalised to [-1, 1], or null if unknown.
707
+ * Intended for external analysis / resynthesis such as the WORLD vocoder.
708
+ */
709
+ async getPcm(phoneme) {
710
+ const buf = await this.readPcmBytes(phoneme);
711
+ if (!buf) return null;
712
+ const int16 = new Int16Array(buf);
713
+ const f64 = new Float64Array(int16.length);
714
+ for (let i = 0; i < int16.length; i++) f64[i] = int16[i] / 32768;
715
+ return f64;
716
+ }
717
+ };
718
+ var WORLDLINE_SAMPLE_RATE = 48e3;
719
+ var MIN_WORLDLINE_SAMPLES = 4096;
720
+ var SYNTH_REQ_SIZE = 120;
721
+ var WL_FRAME_MS = 10;
722
+ var samplesToMs = (samples) => samples / WORLDLINE_SAMPLE_RATE * 1e3;
723
+ function leadInFromEntry(entry) {
724
+ return {
725
+ preMs: samplesToMs(entry.pre || 0),
726
+ consonantMs: samplesToMs(entry.consonant || 0)
727
+ };
728
+ }
729
+ var moduleCache = /* @__PURE__ */ new Map();
730
+ function injectScript(src) {
731
+ return new Promise((resolve, reject) => {
732
+ const existing = document.querySelector(
733
+ `script[data-koe-worldline="${src}"]`
734
+ );
735
+ if (existing) {
736
+ resolve();
737
+ return;
738
+ }
739
+ const s = document.createElement("script");
740
+ s.src = src;
741
+ s.dataset.koeWorldline = src;
742
+ s.onload = () => resolve();
743
+ s.onerror = () => reject(new Error(`worldline: failed to load ${src}`));
744
+ document.head.appendChild(s);
745
+ });
746
+ }
747
+ function loadWasm(scriptUrl) {
748
+ const cached = moduleCache.get(scriptUrl);
749
+ if (cached) return cached;
750
+ const baseUrl = scriptUrl.slice(0, scriptUrl.lastIndexOf("/") + 1);
751
+ const instantiate = () => {
752
+ const factory = globalThis.WorldlineModule;
753
+ if (!factory)
754
+ throw new Error(
755
+ "worldline: WorldlineModule global was not defined by the script"
756
+ );
757
+ return factory({ locateFile: (f) => baseUrl + f });
758
+ };
759
+ let promise;
760
+ if (typeof document !== "undefined") {
761
+ promise = injectScript(scriptUrl).then(instantiate);
762
+ } else if (typeof globalThis.importScripts === "function") {
763
+ promise = Promise.resolve().then(() => {
764
+ globalThis.importScripts(scriptUrl);
765
+ return instantiate();
766
+ });
767
+ } else {
768
+ return Promise.reject(
769
+ new Error(
770
+ "Worldline.load requires a DOM or a classic Web Worker (importScripts) to load worldline.js"
771
+ )
772
+ );
773
+ }
774
+ moduleCache.set(scriptUrl, promise);
775
+ return promise;
776
+ }
777
+ var Worldline = class _Worldline {
778
+ constructor(wasm) {
779
+ this.wasm = wasm;
780
+ }
781
+ wasm;
782
+ sampleRate = WORLDLINE_SAMPLE_RATE;
783
+ /**
784
+ * Load + instantiate the worldline WASM module (deduped per scriptUrl).
785
+ *
786
+ * Works on the main thread (loads via `<script>`) and inside a classic Web
787
+ * Worker (loads via `importScripts`), so the heavy synthesis can run
788
+ * off-thread. The matching `worldline.wasm` is fetched next to scriptUrl.
789
+ */
790
+ static async load(options) {
791
+ return new _Worldline(await loadWasm(options.scriptUrl));
792
+ }
793
+ /**
794
+ * Render one note to Float32 PCM at 48 kHz.
795
+ *
796
+ * The output buffer is laid out as [lead-in/consonant ≈ preMs][vowel ≈
797
+ * durationMs], rendered from sample offset 0 (no leading silence). The vowel
798
+ * onset (the "beat") sits at ≈ preMs into the buffer, so a sequencer should
799
+ * place the buffer at `beatTime − preMs` and may trim/crossfade the lead-in.
800
+ *
801
+ * No internal crossfade is applied — apply fades externally.
802
+ *
803
+ * @returns Float32 PCM, or null when `pcm` is shorter than
804
+ * {@link MIN_WORLDLINE_SAMPLES} (too short for stable F0 analysis).
805
+ */
806
+ renderNote(params) {
807
+ const { pcm, pitch, durationMs, preMs, consonantMs, tempo = 120 } = params;
808
+ if (!pcm || pcm.length < MIN_WORLDLINE_SAMPLES) return null;
809
+ const WL = this.wasm;
810
+ const FS = WORLDLINE_SAMPLE_RATE;
811
+ const midiNote = Math.round(69 + 12 * Math.log2(pitch / 440));
812
+ const posMs = 0;
813
+ const reqLen = preMs + durationMs;
814
+ const cutMs = WL_FRAME_MS * 2;
815
+ const ps = WL._PhraseSynthNew();
816
+ if (!ps) return null;
817
+ const reqPtr = WL._malloc(SYNTH_REQ_SIZE);
818
+ if (!reqPtr) {
819
+ WL._PhraseSynthDelete(ps);
820
+ return null;
821
+ }
822
+ const samplePtr = WL._malloc(pcm.length * 8);
823
+ if (!samplePtr) {
824
+ WL._free(reqPtr);
825
+ WL._PhraseSynthDelete(ps);
826
+ return null;
827
+ }
828
+ WL.HEAPF64.set(pcm, samplePtr >> 3);
829
+ const sv = (off, val, type) => WL.setValue(reqPtr + off, val, type);
830
+ sv(0, FS, "i32");
831
+ sv(4, pcm.length, "i32");
832
+ sv(8, samplePtr, "*");
833
+ sv(12, 0, "i32");
834
+ sv(16, 0, "*");
835
+ sv(20, midiNote, "i32");
836
+ sv(24, 100, "double");
837
+ sv(32, 0, "double");
838
+ sv(40, reqLen, "double");
839
+ sv(48, consonantMs, "double");
840
+ sv(56, cutMs, "double");
841
+ sv(64, 100, "double");
842
+ sv(72, 0, "double");
843
+ sv(80, tempo, "double");
844
+ sv(88, 0, "i32");
845
+ sv(92, 0, "*");
846
+ sv(96, 0, "i32");
847
+ sv(100, 0, "i32");
848
+ sv(104, 100, "i32");
849
+ sv(108, 0, "i32");
850
+ sv(112, 0, "i32");
851
+ sv(116, 100, "i32");
852
+ WL._PhraseSynthAddRequest(ps, reqPtr, posMs, 0, reqLen, 0, 0, 0);
853
+ WL._free(samplePtr);
854
+ WL._free(reqPtr);
855
+ const totalMs = posMs + reqLen + WL_FRAME_MS * 2;
856
+ const nFrames = Math.ceil(totalMs / WL_FRAME_MS) + 4;
857
+ const f0Arr = new Float64Array(nFrames).fill(pitch);
858
+ const gArr = new Float64Array(nFrames).fill(0.5);
859
+ const tArr = new Float64Array(nFrames).fill(0.5);
860
+ const bArr = new Float64Array(nFrames).fill(0.5);
861
+ const vArr = new Float64Array(nFrames).fill(1);
862
+ const f0Ptr = WL._malloc(nFrames * 8);
863
+ const gPtr = WL._malloc(nFrames * 8);
864
+ const tPtr = WL._malloc(nFrames * 8);
865
+ const bPtr = WL._malloc(nFrames * 8);
866
+ const vPtr = WL._malloc(nFrames * 8);
867
+ if (!f0Ptr || !gPtr || !tPtr || !bPtr || !vPtr) {
868
+ if (f0Ptr) WL._free(f0Ptr);
869
+ if (gPtr) WL._free(gPtr);
870
+ if (tPtr) WL._free(tPtr);
871
+ if (bPtr) WL._free(bPtr);
872
+ if (vPtr) WL._free(vPtr);
873
+ WL._PhraseSynthDelete(ps);
874
+ return null;
875
+ }
876
+ WL.HEAPF64.set(f0Arr, f0Ptr >> 3);
877
+ WL.HEAPF64.set(gArr, gPtr >> 3);
878
+ WL.HEAPF64.set(tArr, tPtr >> 3);
879
+ WL.HEAPF64.set(bArr, bPtr >> 3);
880
+ WL.HEAPF64.set(vArr, vPtr >> 3);
881
+ WL._PhraseSynthSetCurves(
882
+ ps,
883
+ f0Ptr,
884
+ gPtr,
885
+ tPtr,
886
+ bPtr,
887
+ vPtr,
888
+ nFrames,
889
+ WL_FRAME_MS
890
+ );
891
+ WL._free(f0Ptr);
892
+ WL._free(gPtr);
893
+ WL._free(tPtr);
894
+ WL._free(bPtr);
895
+ WL._free(vPtr);
896
+ const yPtrPtr = WL._malloc(4);
897
+ if (!yPtrPtr) {
898
+ WL._PhraseSynthDelete(ps);
899
+ return null;
900
+ }
901
+ const outLen = WL._PhraseSynthSynth(ps, yPtrPtr, 0);
902
+ const yPtr = WL.getValue(yPtrPtr, "*");
903
+ const audio = outLen > 0 ? new Float32Array(WL.HEAPF32.buffer, yPtr, outLen).slice() : null;
904
+ WL._free(yPtrPtr);
905
+ WL._PhraseSynthDelete(ps);
906
+ return audio;
907
+ }
908
+ };
909
+
910
+ // src/types.ts
911
+ var DEFAULT_VOCAL_VOLUME = 200;
912
+ var DEFAULT_BPM = 120;
913
+ var DEFAULT_GATE = 100;
914
+ var DEFAULT_PAN = 64;
915
+ var DEFAULT_VELOCITY = 100;
916
+ var DEFAULT_PLAYBACK_VELOCITY = 127;
917
+ var DEFAULT_STEPS_PER_BAR = 192;
918
+
919
+ // src/lyrics.ts
920
+ var kanaTable = {
921
+ \u3042: ["", "a"],
922
+ \u3044: ["", "i"],
923
+ \u3046: ["", "u"],
924
+ \u3048: ["", "e"],
925
+ \u304A: ["", "o"],
926
+ \u304B: ["k", "a"],
927
+ \u304D: ["k", "i"],
928
+ \u304F: ["k", "u"],
929
+ \u3051: ["k", "e"],
930
+ \u3053: ["k", "o"],
931
+ \u3055: ["s", "a"],
932
+ \u3057: ["sh", "i"],
933
+ \u3059: ["s", "u"],
934
+ \u305B: ["s", "e"],
935
+ \u305D: ["s", "o"],
936
+ \u305F: ["t", "a"],
937
+ \u3061: ["ch", "i"],
938
+ \u3064: ["ts", "u"],
939
+ \u3066: ["t", "e"],
940
+ \u3068: ["t", "o"],
941
+ \u306A: ["n", "a"],
942
+ \u306B: ["n", "i"],
943
+ \u306C: ["n", "u"],
944
+ \u306D: ["n", "e"],
945
+ \u306E: ["n", "o"],
946
+ \u306F: ["h", "a"],
947
+ \u3072: ["h", "i"],
948
+ \u3075: ["f", "u"],
949
+ \u3078: ["h", "e"],
950
+ \u307B: ["h", "o"],
951
+ \u307E: ["m", "a"],
952
+ \u307F: ["m", "i"],
953
+ \u3080: ["m", "u"],
954
+ \u3081: ["m", "e"],
955
+ \u3082: ["m", "o"],
956
+ \u3084: ["y", "a"],
957
+ \u3086: ["y", "u"],
958
+ \u3088: ["y", "o"],
959
+ \u3089: ["r", "a"],
960
+ \u308A: ["r", "i"],
961
+ \u308B: ["r", "u"],
962
+ \u308C: ["r", "e"],
963
+ \u308D: ["r", "o"],
964
+ \u308F: ["w", "a"],
965
+ \u3092: ["w", "o"],
966
+ \u304C: ["g", "a"],
967
+ \u304E: ["g", "i"],
968
+ \u3050: ["g", "u"],
969
+ \u3052: ["g", "e"],
970
+ \u3054: ["g", "o"],
971
+ \u3056: ["z", "a"],
972
+ \u3058: ["j", "i"],
973
+ \u305A: ["z", "u"],
974
+ \u305C: ["z", "e"],
975
+ \u305E: ["z", "o"],
976
+ \u3060: ["d", "a"],
977
+ \u3062: ["j", "i"],
978
+ \u3065: ["z", "u"],
979
+ \u3067: ["d", "e"],
980
+ \u3069: ["d", "o"],
981
+ \u3070: ["b", "a"],
982
+ \u3073: ["b", "i"],
983
+ \u3076: ["b", "u"],
984
+ \u3079: ["b", "e"],
985
+ \u307C: ["b", "o"],
986
+ \u3071: ["p", "a"],
987
+ \u3074: ["p", "i"],
988
+ \u3077: ["p", "u"],
989
+ \u307A: ["p", "e"],
990
+ \u307D: ["p", "o"],
991
+ \u3093: ["N", "N"]
992
+ };
993
+ var SMALL_KANA = "\u3041\u3043\u3045\u3047\u3049\u3083\u3085\u3087\u3063";
994
+ var VOWEL_KANA = {
995
+ a: "\u3042",
996
+ i: "\u3044",
997
+ u: "\u3046",
998
+ e: "\u3048",
999
+ o: "\u304A"
1000
+ };
1001
+ var sanitizeText = (text) => text.normalize("NFKC").replace(/[ァ-ヶ]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 96)).replace(/[^ぁ-ゖー]/g, "");
1002
+ var splitSyllables = (text) => {
1003
+ const result = [];
1004
+ for (const ch of text) {
1005
+ if (result.length > 0 && SMALL_KANA.includes(ch)) {
1006
+ result[result.length - 1] += ch;
1007
+ } else {
1008
+ result.push(ch);
1009
+ }
1010
+ }
1011
+ return result;
1012
+ };
1013
+ var kanaToVowel = (kana) => {
1014
+ if (/[ぁゃ]/.test(kana)) return "a";
1015
+ if (/[ぃ]/.test(kana)) return "i";
1016
+ if (/[ぅゅ]/.test(kana)) return "u";
1017
+ if (/[ぇ]/.test(kana)) return "e";
1018
+ if (/[ぉょ]/.test(kana)) return "o";
1019
+ if (/[あかさたなはまやらわがざだばぱ]/.test(kana)) return "a";
1020
+ if (/[いきしちにひみりぎじぢびぴ]/.test(kana)) return "i";
1021
+ if (/[うくすつぬふむゆるぐずづぶぷ]/.test(kana)) return "u";
1022
+ if (/[えけせてねへめれげぜでべぺ]/.test(kana)) return "e";
1023
+ if (/[おこそとのほもよろごぞどぼぽ]/.test(kana)) return "o";
1024
+ return "";
1025
+ };
1026
+ var analyzeSyllable = (syllable) => {
1027
+ if (syllable === "\u30FC") return { kana: syllable, consonant: "-", vowel: "-" };
1028
+ if (syllable === "\u3063") return { kana: syllable, consonant: "Q", vowel: "" };
1029
+ const head = syllable[0];
1030
+ const row = kanaTable[head];
1031
+ const consonant = row ? row[0] : "";
1032
+ let vowel = row ? row[1] : kanaToVowel(head);
1033
+ if (syllable.length === 2 && syllable[1] !== "\u3063") {
1034
+ const v = kanaToVowel(syllable[1]);
1035
+ if (v) vowel = v;
1036
+ }
1037
+ return { kana: syllable, consonant, vowel };
1038
+ };
1039
+ var resolveLongVowels = (syllables) => {
1040
+ const result = [];
1041
+ let prevVowel = "";
1042
+ for (const syl of syllables) {
1043
+ if (syl.consonant === "-") {
1044
+ if (!prevVowel) continue;
1045
+ result.push({
1046
+ kana: VOWEL_KANA[prevVowel] ?? syl.kana,
1047
+ consonant: "",
1048
+ vowel: prevVowel
1049
+ });
1050
+ continue;
1051
+ }
1052
+ if (syl.vowel && syl.vowel !== "N") prevVowel = syl.vowel;
1053
+ result.push(syl);
1054
+ }
1055
+ return result;
1056
+ };
1057
+ var normalizeLyrics = (text) => resolveLongVowels(splitSyllables(sanitizeText(text)).map(analyzeSyllable));
1058
+ var normalizeLyricLines = (lines) => {
1059
+ const syllables = [];
1060
+ const lineBreaks = [];
1061
+ for (const line of lines) {
1062
+ const part = normalizeLyrics(line);
1063
+ if (part.length === 0) continue;
1064
+ if (syllables.length > 0) lineBreaks.push(syllables.length);
1065
+ syllables.push(...part);
1066
+ }
1067
+ return { syllables, lineBreaks };
1068
+ };
1069
+ var LYRIC_LINE = /^@@(\d+)\s+(.*)$/;
1070
+ var isLyricContinuation = (seg) => !/^[@#]/.test(seg);
1071
+ var splitSegments = (mml) => mml.split(/[;\n\r]+/).map((s) => s.trim()).filter((s) => s.length > 0);
1072
+ var clamp = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
1073
+ var MAX_VOCAL_VOLUME = 400;
1074
+ var VOCAL_BOOST_DB_PER_PERCENT = 0.08;
1075
+ var vocalVolumeToGain = (v) => {
1076
+ if (v <= 0) return 0;
1077
+ if (v <= 100) return v / 100;
1078
+ return 10 ** ((v - 100) * VOCAL_BOOST_DB_PER_PERCENT / 20);
1079
+ };
1080
+ var parseLyrics = (mml) => {
1081
+ const tracks = /* @__PURE__ */ new Map();
1082
+ const segments = splitSegments(mml);
1083
+ for (let i = 0; i < segments.length; i++) {
1084
+ const m = segments[i].match(LYRIC_LINE);
1085
+ if (!m) continue;
1086
+ const trackId = Number.parseInt(m[1], 10);
1087
+ const tokens = m[2].trim().split(/\s+/);
1088
+ const modelToken = tokens.shift() ?? "";
1089
+ let volume = DEFAULT_VOCAL_VOLUME;
1090
+ let gate = 100;
1091
+ let pan = 64;
1092
+ let octave = 0;
1093
+ const colon = modelToken.indexOf(":");
1094
+ const model = (colon === -1 ? modelToken : modelToken.slice(0, colon)).toLowerCase();
1095
+ if (colon !== -1) {
1096
+ const v = Number.parseInt(modelToken.slice(colon + 1), 10);
1097
+ if (Number.isFinite(v)) volume = clamp(v, 0, MAX_VOCAL_VOLUME);
1098
+ }
1099
+ const metaTokens = [modelToken];
1100
+ while (tokens.length > 0) {
1101
+ const v = /^v(\d+)$/.exec(tokens[0]);
1102
+ const q2 = /^q(\d+)$/.exec(tokens[0]);
1103
+ const p = /^p(\d+)$/.exec(tokens[0]);
1104
+ const o = /^o(-?\d+)$/.exec(tokens[0]);
1105
+ if (v) {
1106
+ volume = clamp(Number.parseInt(v[1], 10), 0, MAX_VOCAL_VOLUME);
1107
+ } else if (q2) {
1108
+ gate = clamp(Number.parseInt(q2[1], 10), 0, 100);
1109
+ } else if (p) {
1110
+ pan = clamp(Number.parseInt(p[1], 10), 0, 127);
1111
+ } else if (o) {
1112
+ octave = clamp(Number.parseInt(o[1], 10), -2, 2);
1113
+ } else {
1114
+ break;
1115
+ }
1116
+ metaTokens.push(tokens.shift());
1117
+ }
1118
+ const lyricLines = [tokens.join(" ")];
1119
+ while (i + 1 < segments.length && isLyricContinuation(segments[i + 1])) {
1120
+ lyricLines.push(segments[++i]);
1121
+ }
1122
+ const { syllables, lineBreaks } = normalizeLyricLines(lyricLines);
1123
+ tracks.set(trackId, {
1124
+ trackId,
1125
+ model,
1126
+ volume,
1127
+ gate,
1128
+ pan,
1129
+ octave,
1130
+ syllables,
1131
+ metaText: metaTokens.join(" "),
1132
+ ...lineBreaks.length > 0 ? { lineBreaks } : {}
1133
+ });
1134
+ }
1135
+ return tracks;
1136
+ };
1137
+ var stripLyrics = (mml) => {
1138
+ const segments = splitSegments(mml);
1139
+ const kept = [];
1140
+ for (let i = 0; i < segments.length; i++) {
1141
+ if (LYRIC_LINE.test(segments[i])) {
1142
+ while (i + 1 < segments.length && isLyricContinuation(segments[i + 1]))
1143
+ i++;
1144
+ continue;
1145
+ }
1146
+ kept.push(segments[i]);
1147
+ }
1148
+ return kept.join("\n");
1149
+ };
1150
+ var panToStereo = (pan) => Math.max(-1, Math.min(1, (pan - 64) / 64));
1151
+ var createLyricsConductor = (lyrics) => {
1152
+ const pointers = /* @__PURE__ */ new Map();
1153
+ const consume = (trackId) => {
1154
+ const track = lyrics.get(trackId);
1155
+ if (!track || track.syllables.length === 0) return null;
1156
+ const ptr = pointers.get(trackId) ?? 0;
1157
+ const syllable = track.syllables[ptr];
1158
+ if (!syllable) return null;
1159
+ pointers.set(trackId, ptr + 1);
1160
+ return {
1161
+ model: track.model,
1162
+ syllable,
1163
+ volume: vocalVolumeToGain(track.volume ?? DEFAULT_VOCAL_VOLUME),
1164
+ gate: (track.gate ?? DEFAULT_GATE) / 100,
1165
+ pan: panToStereo(track.pan ?? DEFAULT_PAN)
1166
+ };
1167
+ };
1168
+ const reset = () => pointers.clear();
1169
+ return { consume, reset };
1170
+ };
1171
+ var FORMANTS = {
1172
+ a: [800, 1200],
1173
+ i: [300, 2300],
1174
+ u: [350, 800],
1175
+ e: [500, 1900],
1176
+ o: [500, 900],
1177
+ // 撥音(ん)は鼻音寄りの低フォルマント
1178
+ N: [250, 1e3]
1179
+ };
1180
+ var midiToFreq = (m) => 440 * 2 ** ((m - 69) / 12);
1181
+ var createKlattVoice = (ctx, destination) => {
1182
+ const active = /* @__PURE__ */ new Set();
1183
+ const voice = (syllable, e) => {
1184
+ const t0 = ctx.currentTime + e.when;
1185
+ const peak = Math.max(1e-4, e.volume);
1186
+ if (syllable.vowel === "" || syllable.consonant === "Q") return;
1187
+ const [f1, f2] = FORMANTS[syllable.vowel] ?? FORMANTS.a;
1188
+ const attack = 0.02;
1189
+ const release = 0.06;
1190
+ const sustainEnd = t0 + Math.max(attack + 0.02, e.duration);
1191
+ let panner = null;
1192
+ let out = destination;
1193
+ if (typeof ctx.createStereoPanner === "function") {
1194
+ panner = ctx.createStereoPanner();
1195
+ panner.pan.value = Math.max(-1, Math.min(1, e.pan ?? 0));
1196
+ panner.connect(destination);
1197
+ out = panner;
1198
+ }
1199
+ const osc = ctx.createOscillator();
1200
+ osc.type = "sawtooth";
1201
+ osc.frequency.value = midiToFreq(e.pitch);
1202
+ const makeFormant = (freq, q2, gainScale) => {
1203
+ const filter = ctx.createBiquadFilter();
1204
+ filter.type = "bandpass";
1205
+ filter.frequency.value = freq;
1206
+ filter.Q.value = q2;
1207
+ const g = ctx.createGain();
1208
+ g.gain.value = gainScale;
1209
+ osc.connect(filter).connect(g);
1210
+ return g;
1211
+ };
1212
+ const env = ctx.createGain();
1213
+ env.gain.setValueAtTime(1e-4, t0);
1214
+ env.gain.exponentialRampToValueAtTime(peak, t0 + attack);
1215
+ env.gain.setValueAtTime(peak, sustainEnd);
1216
+ env.gain.exponentialRampToValueAtTime(1e-4, sustainEnd + release);
1217
+ const MAKEUP = 4;
1218
+ makeFormant(f1, 6, MAKEUP).connect(env);
1219
+ makeFormant(f2, 9, MAKEUP * 0.7).connect(env);
1220
+ env.connect(out);
1221
+ const fricatives = /* @__PURE__ */ new Set(["s", "sh", "ch", "ts", "h", "f"]);
1222
+ if (fricatives.has(syllable.consonant)) {
1223
+ const dur = 0.05;
1224
+ const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
1225
+ const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
1226
+ const data = buffer.getChannelData(0);
1227
+ for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
1228
+ const src = ctx.createBufferSource();
1229
+ src.buffer = buffer;
1230
+ const hp = ctx.createBiquadFilter();
1231
+ hp.type = "highpass";
1232
+ hp.frequency.value = syllable.consonant === "sh" ? 3e3 : 4500;
1233
+ const ng = ctx.createGain();
1234
+ ng.gain.setValueAtTime(peak * 0.5, t0);
1235
+ ng.gain.exponentialRampToValueAtTime(1e-4, t0 + dur);
1236
+ src.connect(hp).connect(ng).connect(out);
1237
+ src.start(t0);
1238
+ src.stop(t0 + dur);
1239
+ active.add(src);
1240
+ src.onended = () => {
1241
+ active.delete(src);
1242
+ src.disconnect();
1243
+ hp.disconnect();
1244
+ ng.disconnect();
1245
+ };
1246
+ }
1247
+ osc.start(t0);
1248
+ osc.stop(sustainEnd + release + 0.02);
1249
+ active.add(osc);
1250
+ osc.onended = () => {
1251
+ active.delete(osc);
1252
+ osc.disconnect();
1253
+ panner?.disconnect();
1254
+ };
1255
+ };
1256
+ voice.stopAll = () => {
1257
+ for (const n of active) {
1258
+ try {
1259
+ n.stop();
1260
+ } catch {
1261
+ }
1262
+ n.disconnect();
1263
+ }
1264
+ active.clear();
1265
+ };
1266
+ return voice;
1267
+ };
1268
+ var KOE_BASE_URL = "https://pub-12482a6b5cbc4c9e906b2e1904cabae5.r2.dev";
1269
+ var KOE_VOICEBANKS = {
1270
+ tsukuyomi: "\u3064\u304F\u3088\u307F\u3061\u3083\u3093.koe",
1271
+ rino: "\u6625\u97F3\u30EA\u30CEver0.3.koe",
1272
+ roze: "\u675F\u97F3\u30ED\u30BCver0.\uFF151(\u591A\u97F3\u968E).koe",
1273
+ ruko_male: "\u6B32\u97F3\u30EB\u30B3\u2642\u9023\u7D9A\u97F3Ver.1.03.koe",
1274
+ ruko_female: "\u6B32\u97F3\u30EB\u30B3\u2640\u6B4C\u9023\u7D9A\u97F3\u666E1.00.koe",
1275
+ teto: "\u91CD\u97F3\u30C6\u30C8\u5358\u72EC\u97F3.koe",
1276
+ shiyo: "\u9769\u547D\u30B7\u30E8.koe"
1277
+ };
1278
+ var KOE_VOICEBANK_LABELS = {
1279
+ tsukuyomi: "\u3064\u304F\u3088\u307F\u3061\u3083\u3093",
1280
+ rino: "\u6625\u97F3\u30EA\u30CE",
1281
+ roze: "\u675F\u97F3\u30ED\u30BC",
1282
+ ruko_male: "\u6B32\u97F3\u30EB\u30B3\u2642",
1283
+ ruko_female: "\u6B32\u97F3\u30EB\u30B3\u2640",
1284
+ teto: "\u91CD\u97F3\u30C6\u30C8",
1285
+ shiyo: "\u9769\u547D\u30B7\u30E8"
1286
+ };
1287
+ var VOICE_IMAGE_KEY = {
1288
+ klatt: "puyuyu",
1289
+ tsukuyomi: "tsukuyomi",
1290
+ rino: "rino",
1291
+ roze: "roze",
1292
+ ruko_male: "ruko",
1293
+ ruko_female: "ruko",
1294
+ teto: "teto",
1295
+ shiyo: "shiyo"
1296
+ };
1297
+ var KOE_VOICEBANK_TERMS = {
1298
+ tsukuyomi: "https://tyc.rei-yumesaki.net/material/utau/terms/",
1299
+ rino: "https://hatenakun1.github.io/halunelino/",
1300
+ roze: "https://tabaneroze.ninja-web.net/terms-of-use.html",
1301
+ ruko_male: "https://long-sleeper.net/index.php?id=22",
1302
+ ruko_female: "https://long-sleeper.net/index.php?id=22",
1303
+ teto: "https://kasaneteto.jp/guidelines/voice.html",
1304
+ shiyo: "https://kakumeisiyo.my.canva.site/dagkuyjwycs"
1305
+ };
1306
+ var koeUrl = (name, base = KOE_BASE_URL) => `${base}/${encodeURIComponent(name)}`;
1307
+ var DEFAULT_WORLDLINE_SCRIPT = "https://onjmin.github.io/koe/demo/world/worldline.js";
1308
+ var KOE_SAMPLE_RATE = 48e3;
1309
+ var expandSeparators = (candidate) => candidate.includes(" ") ? [candidate, candidate.replace(/ /g, "\u3000"), candidate.replace(/ /g, "")] : [candidate];
1310
+ var PITCH_SUFFIX = /_([A-G][#b]?-?\d+)$/;
1311
+ var NAME_SEMITONE = {
1312
+ c: 0,
1313
+ d: 2,
1314
+ e: 4,
1315
+ f: 5,
1316
+ g: 7,
1317
+ a: 9,
1318
+ b: 11
1319
+ };
1320
+ var pitchTokenToMidi = (token) => {
1321
+ const m = /^([A-Ga-g])([#b]?)(-?\d+)$/.exec(token);
1322
+ if (!m) return null;
1323
+ let semi = NAME_SEMITONE[m[1].toLowerCase()];
1324
+ if (m[2] === "#") semi++;
1325
+ else if (m[2] === "b") semi--;
1326
+ return (Number.parseInt(m[3], 10) + 1) * 12 + semi;
1327
+ };
1328
+ var collectPitchTokens = (aliases) => {
1329
+ const seen = /* @__PURE__ */ new Map();
1330
+ for (const a of aliases) {
1331
+ const m = PITCH_SUFFIX.exec(a);
1332
+ if (!m || seen.has(m[1])) continue;
1333
+ const midi = pitchTokenToMidi(m[1]);
1334
+ if (midi != null) seen.set(m[1], midi);
1335
+ }
1336
+ return [...seen].map(([token, midi]) => ({ token, midi }));
1337
+ };
1338
+ var resolveKoeAlias = (hasAlias, pitchTokens, syl, prevVowel, noteNum) => {
1339
+ const kana = syl.kana;
1340
+ const cons = syl.consonant === "N" ? "n" : syl.consonant;
1341
+ const vow = syl.vowel === "N" ? "" : syl.vowel;
1342
+ const romaji = `${cons}${vow}` || vow;
1343
+ const pv = prevVowel || "-";
1344
+ const raw = [
1345
+ // 連続音(VCV): 直前母音つき
1346
+ `${pv} ${kana}`,
1347
+ `${pv} ${romaji}`,
1348
+ // 単独音 / CVVC
1349
+ kana,
1350
+ romaji
1351
+ ];
1352
+ const vk = VOWEL_KANA[syl.vowel];
1353
+ if (vk) raw.push(`${pv} ${vk}`, vk, syl.vowel);
1354
+ if (syl.vowel === "N") raw.push("\u3093", "n", "N", `${pv} \u3093`);
1355
+ const seen = /* @__PURE__ */ new Set();
1356
+ const tryAlias = (candidate) => {
1357
+ for (const v of expandSeparators(candidate)) {
1358
+ if (seen.has(v)) continue;
1359
+ seen.add(v);
1360
+ if (hasAlias(v)) return v;
1361
+ }
1362
+ return null;
1363
+ };
1364
+ if (pitchTokens.length) {
1365
+ const nearest = pitchTokens.slice().sort((a, b) => Math.abs(a.midi - noteNum) - Math.abs(b.midi - noteNum));
1366
+ for (const { token } of nearest) {
1367
+ for (const base of raw) {
1368
+ const hit = tryAlias(`${base}_${token}`);
1369
+ if (hit) return hit;
1370
+ }
1371
+ }
1372
+ }
1373
+ for (const base of raw) {
1374
+ const hit = tryAlias(base);
1375
+ if (hit) return hit;
1376
+ }
1377
+ return null;
1378
+ };
1379
+ var createLocalBackend = async (options) => {
1380
+ const bank = await VoiceBank.load(options.koe);
1381
+ const worldline = options.lightweight ? null : await Worldline.load({
1382
+ scriptUrl: options.worldlineScriptUrl ?? DEFAULT_WORLDLINE_SCRIPT
1383
+ }).catch(() => null);
1384
+ const pcmCache = /* @__PURE__ */ new Map();
1385
+ const getPcm = (alias) => {
1386
+ let p = pcmCache.get(alias);
1387
+ if (!p) {
1388
+ p = bank.getPcm(alias);
1389
+ pcmCache.set(alias, p);
1390
+ }
1391
+ return p;
1392
+ };
1393
+ const renderAlias = async (alias, pitch, durationMs) => {
1394
+ const pcm = await getPcm(alias);
1395
+ if (!pcm || pcm.length === 0) return null;
1396
+ const entry = bank.manifest.phonemes[alias];
1397
+ const lead = leadInFromEntry(entry);
1398
+ const targetHz = midiToFreq(pitch);
1399
+ if (worldline) {
1400
+ const audio = worldline.renderNote({
1401
+ pcm,
1402
+ pitch: targetHz,
1403
+ durationMs,
1404
+ ...lead
1405
+ });
1406
+ if (audio) return { pcm: audio, preSec: lead.preMs / 1e3, rate: 1 };
1407
+ }
1408
+ const rate = entry.pitch > 0 ? targetHz / entry.pitch : 1;
1409
+ return {
1410
+ pcm: Float32Array.from(pcm),
1411
+ preSec: entry.pre / KOE_SAMPLE_RATE / rate,
1412
+ rate
1413
+ };
1414
+ };
1415
+ return {
1416
+ hasAlias: (a) => bank.has(a),
1417
+ pitchTokens: collectPitchTokens(Object.keys(bank.manifest.phonemes)),
1418
+ renderAlias,
1419
+ dispose: () => {
1420
+ }
1421
+ };
1422
+ };
1423
+ var spawnVoiceWorker = async (url) => {
1424
+ const sameOrigin = new URL(url, location.href).origin === location.origin;
1425
+ if (sameOrigin) return new Worker(url);
1426
+ const text = await fetch(url).then((r) => r.text());
1427
+ return new Worker(
1428
+ URL.createObjectURL(new Blob([text], { type: "text/javascript" }))
1429
+ );
1430
+ };
1431
+ var createWorkerBackend = async (workerUrl, options) => {
1432
+ const worker = await spawnVoiceWorker(workerUrl);
1433
+ const aliasSet = /* @__PURE__ */ new Set();
1434
+ const pending = /* @__PURE__ */ new Map();
1435
+ let reqId = 0;
1436
+ let onReady = null;
1437
+ let onFail = null;
1438
+ worker.onmessage = (ev) => {
1439
+ const m = ev.data;
1440
+ if (m.type === "ready") {
1441
+ for (const a of m.aliases) aliasSet.add(a);
1442
+ onReady?.();
1443
+ } else if (m.type === "error") {
1444
+ onFail?.(new Error(m.message));
1445
+ } else if (m.type === "rendered") {
1446
+ const cb = pending.get(m.id);
1447
+ if (cb) {
1448
+ pending.delete(m.id);
1449
+ cb(m);
1450
+ }
1451
+ }
1452
+ };
1453
+ worker.onerror = (e) => {
1454
+ const ev = e;
1455
+ onFail?.(new Error(ev.message || ev.error || `Event: ${ev.type}`));
1456
+ };
1457
+ await new Promise((resolve, reject) => {
1458
+ onReady = resolve;
1459
+ onFail = reject;
1460
+ worker.postMessage({
1461
+ type: "init",
1462
+ koe: options.koe,
1463
+ worldlineScriptUrl: options.worldlineScriptUrl ?? DEFAULT_WORLDLINE_SCRIPT,
1464
+ lightweight: !!options.lightweight
1465
+ });
1466
+ });
1467
+ onReady = null;
1468
+ onFail = null;
1469
+ const renderAlias = (alias, pitch, durationMs) => new Promise((resolve) => {
1470
+ const id = ++reqId;
1471
+ pending.set(
1472
+ id,
1473
+ (m) => resolve(
1474
+ m.pcm ? { pcm: m.pcm, preSec: m.preSec ?? 0, rate: m.rate ?? 1 } : null
1475
+ )
1476
+ );
1477
+ worker.postMessage({
1478
+ type: "render",
1479
+ id,
1480
+ alias,
1481
+ pitch,
1482
+ durationMs
1483
+ });
1484
+ });
1485
+ return {
1486
+ hasAlias: (a) => aliasSet.has(a),
1487
+ pitchTokens: collectPitchTokens(aliasSet),
1488
+ renderAlias,
1489
+ dispose: () => worker.terminate()
1490
+ };
1491
+ };
1492
+ var createKoeVoice = async (ctx, destination, options) => {
1493
+ const backend = options.voiceWorkerUrl ? await createWorkerBackend(options.voiceWorkerUrl, options) : await createLocalBackend(options);
1494
+ const renderCache = /* @__PURE__ */ new Map();
1495
+ const inflight = /* @__PURE__ */ new Map();
1496
+ const active = /* @__PURE__ */ new Set();
1497
+ let prevVowel = "";
1498
+ const keyOf = (alias, pitch, durationMs) => `${alias}|${pitch}|${Math.round(durationMs / 10) * 10}`;
1499
+ const renderInto = (alias, pitch, durationMs) => {
1500
+ const key = keyOf(alias, pitch, durationMs);
1501
+ const existing = renderCache.get(key);
1502
+ if (existing !== void 0) return Promise.resolve(existing);
1503
+ const flying = inflight.get(key);
1504
+ if (flying) return flying;
1505
+ const p = (async () => {
1506
+ const out = await backend.renderAlias(alias, pitch, durationMs);
1507
+ let rendered = null;
1508
+ if (out) {
1509
+ const buf = ctx.createBuffer(1, out.pcm.length, KOE_SAMPLE_RATE);
1510
+ buf.copyToChannel(out.pcm, 0);
1511
+ rendered = { audio: buf, preSec: out.preSec, rate: out.rate };
1512
+ }
1513
+ renderCache.set(key, rendered);
1514
+ inflight.delete(key);
1515
+ return rendered;
1516
+ })();
1517
+ inflight.set(key, p);
1518
+ return p;
1519
+ };
1520
+ const LEADCAP_S = 0.09;
1521
+ const schedule = (r, t0, peak, pan) => {
1522
+ let out = destination;
1523
+ let panner = null;
1524
+ if (typeof ctx.createStereoPanner === "function") {
1525
+ panner = ctx.createStereoPanner();
1526
+ panner.pan.value = Math.max(-1, Math.min(1, pan));
1527
+ panner.connect(destination);
1528
+ out = panner;
1529
+ }
1530
+ const src = ctx.createBufferSource();
1531
+ src.buffer = r.audio;
1532
+ src.playbackRate.value = r.rate;
1533
+ const effPre = Math.min(r.preSec, LEADCAP_S);
1534
+ const skipS = r.preSec - effPre;
1535
+ const startAt = Math.max(ctx.currentTime + 1e-3, t0 - effPre);
1536
+ const playDurSec = r.audio.duration / r.rate - skipS;
1537
+ const endAt = startAt + playDurSec;
1538
+ const attack = 0.01;
1539
+ const release = 0.04;
1540
+ const env = ctx.createGain();
1541
+ env.gain.setValueAtTime(1e-4, startAt);
1542
+ env.gain.exponentialRampToValueAtTime(peak, startAt + attack);
1543
+ const fadeStart = Math.max(startAt + attack, endAt - release);
1544
+ env.gain.setValueAtTime(peak, fadeStart);
1545
+ env.gain.exponentialRampToValueAtTime(1e-4, endAt);
1546
+ src.connect(env).connect(out);
1547
+ src.start(startAt, skipS);
1548
+ src.stop(endAt + 0.02);
1549
+ active.add(src);
1550
+ src.onended = () => {
1551
+ active.delete(src);
1552
+ src.disconnect();
1553
+ env.disconnect();
1554
+ panner?.disconnect();
1555
+ };
1556
+ };
1557
+ const model = (syllable, e) => {
1558
+ if (syllable.consonant === "Q" || syllable.vowel === "") return;
1559
+ const alias = resolveKoeAlias(
1560
+ backend.hasAlias,
1561
+ backend.pitchTokens,
1562
+ syllable,
1563
+ prevVowel,
1564
+ e.pitch
1565
+ );
1566
+ if (syllable.vowel && syllable.vowel !== "N") prevVowel = syllable.vowel;
1567
+ if (!alias) return;
1568
+ const t0 = ctx.currentTime + e.when;
1569
+ const peak = Math.max(1e-4, e.volume);
1570
+ const pan = e.pan ?? 0;
1571
+ const durationMs = Math.max(60, e.duration * 1e3);
1572
+ void renderInto(alias, e.pitch, durationMs).then((r) => {
1573
+ if (r) schedule(r, t0, peak, pan);
1574
+ });
1575
+ };
1576
+ model.renderToCache = async (syllable, prevVowelArg, pitch, durationMs) => {
1577
+ if (syllable.consonant === "Q" || syllable.vowel === "") return null;
1578
+ const alias = resolveKoeAlias(
1579
+ backend.hasAlias,
1580
+ backend.pitchTokens,
1581
+ syllable,
1582
+ prevVowelArg,
1583
+ pitch
1584
+ );
1585
+ if (!alias) return null;
1586
+ const dMs = Math.max(60, durationMs);
1587
+ const r = await renderInto(alias, pitch, dMs);
1588
+ return r ? keyOf(alias, pitch, dMs) : null;
1589
+ };
1590
+ model.scheduleCached = (key, t0, peak, pan) => {
1591
+ const r = renderCache.get(key);
1592
+ if (r) schedule(r, t0, peak, pan);
1593
+ };
1594
+ model.stopAll = () => {
1595
+ for (const src of active) {
1596
+ try {
1597
+ src.stop();
1598
+ } catch {
1599
+ }
1600
+ src.disconnect();
1601
+ }
1602
+ active.clear();
1603
+ };
1604
+ model.reset = () => {
1605
+ prevVowel = "";
1606
+ };
1607
+ return model;
1608
+ };
1609
+ var PREWARM_NOTES = 3;
1610
+ var STREAM_LOOKAHEAD_SEC = 1.5;
1611
+ var STREAM_POLL_MS = 100;
1612
+ var FALLBACK_MODEL = "klatt";
1613
+ var createSingingVoices = (ctx, destination, options = {}) => {
1614
+ const catalog = {};
1615
+ for (const [k, file] of Object.entries(KOE_VOICEBANKS))
1616
+ catalog[k] = koeUrl(file);
1617
+ for (const [k, v] of Object.entries(options.voicebanks ?? {}))
1618
+ catalog[k.toLowerCase()] = v;
1619
+ let streamSession = 0;
1620
+ const loaded = /* @__PURE__ */ new Map([
1621
+ [FALLBACK_MODEL, createKlattVoice(ctx, destination)]
1622
+ ]);
1623
+ const loading = /* @__PURE__ */ new Map();
1624
+ const load = (model) => {
1625
+ const m = model.toLowerCase();
1626
+ const ready = loaded.get(m);
1627
+ if (ready) return Promise.resolve(ready);
1628
+ const inflight = loading.get(m);
1629
+ if (inflight) return inflight;
1630
+ const koe = catalog[m];
1631
+ if (!koe) return Promise.resolve(null);
1632
+ const p = (async () => (
1633
+ // URL文字列はそのまま渡す。koe側が VoiceBank.load 内で HTTP Range により
1634
+ // マニフェストだけ先読みし、音素PCMは歌う直前にオンデマンド取得する
1635
+ // (= 初回に .koe 全体をDLしない。モバイル初回ロードの待ちを解消)。
1636
+ // Blob/File が直接渡されたケース(ローカル読み込み)はそのまま BlobVoiceSource。
1637
+ createKoeVoice(ctx, destination, {
1638
+ koe,
1639
+ worldlineScriptUrl: options.worldlineScriptUrl,
1640
+ lightweight: options.lightweight,
1641
+ voiceWorkerUrl: options.voiceWorkerUrl
1642
+ })
1643
+ ))().then((v) => {
1644
+ loaded.set(m, v);
1645
+ return v;
1646
+ }).catch((err) => {
1647
+ console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`, err);
1648
+ return null;
1649
+ });
1650
+ loading.set(m, p);
1651
+ return p;
1652
+ };
1653
+ const loadModels = async (models) => {
1654
+ const set = /* @__PURE__ */ new Set();
1655
+ for (const m of models) if (m) set.add(m.toLowerCase());
1656
+ await Promise.all([...set].map((m) => load(m)));
1657
+ };
1658
+ const forEachSungNote = (track, fn) => {
1659
+ let prevVowel = "";
1660
+ for (const note of track.notes) {
1661
+ const syl = note.syllable;
1662
+ if (syl.consonant === "Q" || syl.vowel === "") continue;
1663
+ fn(note, prevVowel);
1664
+ if (syl.vowel && syl.vowel !== "N") prevVowel = syl.vowel;
1665
+ }
1666
+ };
1667
+ const warm = async (tracks, count = PREWARM_NOTES) => {
1668
+ const promises = [];
1669
+ for (const track of tracks) {
1670
+ const m = loaded.get(track.model.toLowerCase());
1671
+ if (!m?.renderToCache) continue;
1672
+ let n = 0;
1673
+ forEachSungNote(track, (note, prevVowel) => {
1674
+ if (n >= count) return;
1675
+ n++;
1676
+ promises.push(
1677
+ m.renderToCache?.(
1678
+ note.syllable,
1679
+ prevVowel,
1680
+ note.pitch,
1681
+ note.durationSec * 1e3
1682
+ ) ?? Promise.resolve(null)
1683
+ );
1684
+ });
1685
+ }
1686
+ await Promise.all(promises);
1687
+ };
1688
+ const startStream = (tracks, anchorTime) => {
1689
+ const session = ++streamSession;
1690
+ const items = [];
1691
+ for (const track of tracks) {
1692
+ const model = loaded.get(track.model.toLowerCase());
1693
+ if (!model) continue;
1694
+ forEachSungNote(track, (note, prevVowel) => {
1695
+ items.push({
1696
+ model,
1697
+ note,
1698
+ prevVowel,
1699
+ volume: track.volume,
1700
+ pan: track.pan
1701
+ });
1702
+ });
1703
+ }
1704
+ items.sort((a, b) => a.note.startSec - b.note.startSec);
1705
+ void (async () => {
1706
+ for (const item of items) {
1707
+ if (session !== streamSession) return;
1708
+ while (item.note.startSec - (ctx.currentTime - anchorTime) > STREAM_LOOKAHEAD_SEC) {
1709
+ await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_MS));
1710
+ if (session !== streamSession) return;
1711
+ }
1712
+ const t0 = anchorTime + item.note.startSec;
1713
+ const peak = Math.max(1e-4, item.volume);
1714
+ const { model, note } = item;
1715
+ if (model.renderToCache && model.scheduleCached) {
1716
+ const key = await model.renderToCache(
1717
+ note.syllable,
1718
+ item.prevVowel,
1719
+ note.pitch,
1720
+ note.durationSec * 1e3
1721
+ );
1722
+ if (session !== streamSession) return;
1723
+ if (key) model.scheduleCached(key, t0, peak, item.pan);
1724
+ } else {
1725
+ const when = t0 - ctx.currentTime;
1726
+ model(note.syllable, {
1727
+ trackId: "",
1728
+ pitch: note.pitch,
1729
+ velocity: 100,
1730
+ volume: peak,
1731
+ when,
1732
+ duration: note.durationSec,
1733
+ pan: item.pan
1734
+ });
1735
+ }
1736
+ await new Promise((resolve) => setTimeout(resolve, 0));
1737
+ }
1738
+ })();
1739
+ };
1740
+ const stopStream = () => {
1741
+ streamSession++;
1742
+ for (const v of loaded.values()) v.stopAll?.();
1743
+ };
1744
+ const reset = () => {
1745
+ stopStream();
1746
+ for (const v of loaded.values()) v.reset?.();
1747
+ };
1748
+ return { loadModels, warm, startStream, stopStream, reset };
1749
+ };
1750
+ var createVoiceRegistry = (models = {}, fallback = "klatt") => {
1751
+ const sing = (model, syllable, e) => {
1752
+ const fn = models[model] ?? models[fallback];
1753
+ fn?.(syllable, e);
1754
+ };
1755
+ const register = (name, m) => {
1756
+ models[name.toLowerCase()] = m;
1757
+ };
1758
+ return { sing, register };
1759
+ };
1760
+
611
1761
  // src/macros.ts
612
1762
  var SCALES = [
613
1763
  [0, 2, 4, 5, 7, 9, 11],
@@ -1014,7 +2164,7 @@ var exportMIDI = (options) => {
1014
2164
  (n.startStep + (n.durationSteps || 1)) * tickPerStep
1015
2165
  );
1016
2166
  const vel = Math.round(
1017
- (n.velocity ?? 100) * (track.volume || 100) / 100
2167
+ (n.velocity ?? DEFAULT_VELOCITY) * (track.volume ?? 100) / 100
1018
2168
  );
1019
2169
  events.push({ t: startTick, m: [144 | ch & 15, n.pitch, vel] });
1020
2170
  events.push({ t: endTick, m: [144 | ch & 15, n.pitch, 0] });
@@ -1569,7 +2719,7 @@ var MMLCore = class _MMLCore {
1569
2719
  startStep: step,
1570
2720
  durationSteps: options.noteLengthSteps,
1571
2721
  pitch,
1572
- velocity: options.velocity ?? 100
2722
+ velocity: options.velocity ?? DEFAULT_VELOCITY
1573
2723
  };
1574
2724
  this.notes.push(newNote);
1575
2725
  }
@@ -1656,6 +2806,8 @@ var MMLCore = class _MMLCore {
1656
2806
  const config = getRenderConfig();
1657
2807
  const total = config.stepsPerBar;
1658
2808
  const candidates = [
2809
+ { dur: "1.", s: total * 1.5 },
2810
+ // 付点全音符(最長。これ以上はタイ未対応のため表現不可)
1659
2811
  { dur: "1", s: total / 1 },
1660
2812
  { dur: "2.", s: total / 2 * 1.5 },
1661
2813
  { dur: "2", s: total / 2 },
@@ -1716,97 +2868,72 @@ var MMLCore = class _MMLCore {
1716
2868
  return { text: `o${octave}${name}`, currentOctave: octave };
1717
2869
  }
1718
2870
  /**
1719
- * MML生成(1/2小節パターンスキャン方式)
2871
+ * MML生成(単一パス・発音順スキャン)
2872
+ *
2873
+ * 以前は1/2小節ごとのウィンドウで走査していたが、それだと半小節境界をまたぐ音符が
2874
+ * 境界で切り詰められて「ぶつ切り」になり、境界直前に始まる音符は隙間が潰れて欠落し、
2875
+ * 歌詞(@@n)の音節割り当てがずれていた。
2876
+ * 全ノートを発音順に一度で処理し、次の発音までの距離だけを上限として
2877
+ * 各音符の長さを忠実に出力する(次の音符がなければ曲末まで伸ばせる)。
1720
2878
  */
1721
2879
  generateMML = (volumeOverride) => {
1722
2880
  const config = getRenderConfig();
1723
2881
  const vol = volumeOverride ?? this.volume;
1724
- const HALF_BAR = config.stepsPerBar / 2;
1725
- const header = `t${this.tempo} q50 v${vol}`;
2882
+ const header = `t${this.tempo} v${vol}`;
1726
2883
  const segments = [];
1727
2884
  let lastOctave = -1;
1728
2885
  let currentCursor = 0;
1729
2886
  if (this.notes.length === 0) return header;
1730
- const lastNote = this.notes[this.notes.length - 1];
1731
- const endStep = lastNote.startStep + lastNote.durationSteps;
1732
- const totalSteps = Math.ceil(endStep / HALF_BAR) * HALF_BAR;
1733
- for (let windowStart = 0; windowStart < totalSteps; windowStart += HALF_BAR) {
1734
- const windowEnd = windowStart + HALF_BAR;
1735
- const windowNotes = this.notes.filter(
1736
- (n) => n.startStep >= windowStart && n.startStep < windowEnd
1737
- );
1738
- if (windowNotes.length === 0) {
1739
- while (currentCursor < windowEnd) {
1740
- const gap = windowEnd - currentCursor;
1741
- if (gap <= 2) {
1742
- currentCursor = windowEnd;
1743
- break;
1744
- }
1745
- const { dur, steps } = this.findBestFitDuration(gap);
1746
- segments.push(`r${dur}`);
1747
- currentCursor += steps;
1748
- }
1749
- continue;
1750
- }
1751
- const notesByStep = /* @__PURE__ */ new Map();
1752
- windowNotes.forEach((n) => {
1753
- const list = notesByStep.get(n.startStep) || [];
1754
- list.push(n);
1755
- notesByStep.set(n.startStep, list);
1756
- });
1757
- const sortedSteps = Array.from(notesByStep.keys()).sort((a, b) => a - b);
1758
- for (let i = 0; i < sortedSteps.length; i++) {
1759
- const startStep = sortedSteps[i];
1760
- const notes = notesByStep.get(startStep);
1761
- if (!notes) continue;
1762
- while (currentCursor < startStep) {
1763
- const gap = startStep - currentCursor;
1764
- if (gap <= 2) {
1765
- currentCursor = startStep;
1766
- break;
1767
- }
1768
- const { dur, steps } = this.findBestFitDuration(gap);
1769
- segments.push(`r${dur}`);
1770
- currentCursor += steps;
1771
- }
1772
- const nextStart = sortedSteps[i + 1] ?? windowEnd;
1773
- const physicsLimit = nextStart - currentCursor;
1774
- const MIN_STEP = config.stepsPerBar / 64;
1775
- if (physicsLimit < MIN_STEP) {
1776
- currentCursor = startStep;
1777
- continue;
1778
- }
1779
- const idealDuration = notes[0].durationSteps;
1780
- const durStr = this.stepsToMMLDuration(idealDuration, physicsLimit);
1781
- const actualStepGenerated = this.getStepFromDottedMML(durStr);
1782
- if (notes.length > 1) {
1783
- const noteStrs = notes.map((n) => {
1784
- const oct = Math.floor(n.pitch / 12) - 1;
1785
- const name = PITCH_MAP[n.pitch % 12];
1786
- return `o${oct}${name}`;
1787
- });
1788
- segments.push(`[${noteStrs.join("")}]${durStr}`);
1789
- } else {
1790
- const { text, currentOctave } = this.getNoteWithOctave(
1791
- notes[0].pitch,
1792
- lastOctave
1793
- );
1794
- segments.push(`${text}${durStr}`);
1795
- lastOctave = currentOctave;
1796
- }
1797
- currentCursor += actualStepGenerated;
1798
- }
1799
- while (currentCursor < windowEnd) {
1800
- const gap = windowEnd - currentCursor;
1801
- if (gap <= 2) {
1802
- currentCursor = windowEnd;
1803
- break;
1804
- }
2887
+ const endStep = Math.max(
2888
+ ...this.notes.map((n) => n.startStep + n.durationSteps)
2889
+ );
2890
+ const notesByStep = /* @__PURE__ */ new Map();
2891
+ for (const n of this.notes) {
2892
+ const list = notesByStep.get(n.startStep) ?? [];
2893
+ list.push(n);
2894
+ notesByStep.set(n.startStep, list);
2895
+ }
2896
+ const sortedSteps = Array.from(notesByStep.keys()).sort((a, b) => a - b);
2897
+ const MIN_STEP = config.stepsPerBar / 64;
2898
+ const fillRests = (until) => {
2899
+ while (until - currentCursor >= MIN_STEP) {
2900
+ const gap = until - currentCursor;
1805
2901
  const { dur, steps } = this.findBestFitDuration(gap);
1806
2902
  segments.push(`r${dur}`);
1807
2903
  currentCursor += steps;
1808
2904
  }
2905
+ };
2906
+ for (let i = 0; i < sortedSteps.length; i++) {
2907
+ const startStep = sortedSteps[i];
2908
+ const notes = notesByStep.get(startStep);
2909
+ if (!notes) continue;
2910
+ fillRests(startStep);
2911
+ const nextStart = sortedSteps[i + 1] ?? endStep;
2912
+ const physicsLimit = nextStart - currentCursor;
2913
+ if (physicsLimit < MIN_STEP) {
2914
+ continue;
2915
+ }
2916
+ const idealDuration = notes[0].durationSteps;
2917
+ const durStr = this.stepsToMMLDuration(idealDuration, physicsLimit);
2918
+ const actualStepGenerated = this.getStepFromDottedMML(durStr);
2919
+ if (notes.length > 1) {
2920
+ const noteStrs = notes.map((n) => {
2921
+ const oct = Math.floor(n.pitch / 12) - 1;
2922
+ const name = PITCH_MAP[n.pitch % 12];
2923
+ return `o${oct}${name}`;
2924
+ });
2925
+ segments.push(`[${noteStrs.join("")}]${durStr}`);
2926
+ } else {
2927
+ const { text, currentOctave } = this.getNoteWithOctave(
2928
+ notes[0].pitch,
2929
+ lastOctave
2930
+ );
2931
+ segments.push(`${text}${durStr}`);
2932
+ lastOctave = currentOctave;
2933
+ }
2934
+ currentCursor += actualStepGenerated;
1809
2935
  }
2936
+ fillRests(endStep);
1810
2937
  return `${header} ${segments.join(" ")}`;
1811
2938
  };
1812
2939
  /**
@@ -1884,13 +3011,51 @@ var PITCH_MAP2 = {
1884
3011
  a: 9,
1885
3012
  b: 11
1886
3013
  };
1887
- var TRACK_INDEX_COUNT = 4;
3014
+ var clamp2 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
3015
+ var META_DIRECTIVE = /#(inst|drum|volume)=([\w-]+)/gi;
3016
+ var parseMmlMeta = (mml) => {
3017
+ const meta = {};
3018
+ for (const m of mml.matchAll(META_DIRECTIVE)) {
3019
+ const key = m[1].toLowerCase();
3020
+ if (key === "inst") meta.instrument = m[2];
3021
+ else if (key === "drum") meta.drum = m[2];
3022
+ else if (key === "volume") {
3023
+ const v = Number.parseInt(m[2], 10);
3024
+ if (!Number.isNaN(v)) meta.volume = v;
3025
+ }
3026
+ }
3027
+ return meta;
3028
+ };
3029
+ var stripMmlMeta = (mml) => mml.replace(META_DIRECTIVE, "");
3030
+ var formatMmlMeta = (meta) => {
3031
+ const parts = [];
3032
+ if (meta.instrument) parts.push(`#inst=${meta.instrument}`);
3033
+ if (meta.drum) parts.push(`#drum=${meta.drum}`);
3034
+ if (meta.volume !== void 0) parts.push(`#volume=${meta.volume}`);
3035
+ return parts.join(" ");
3036
+ };
1888
3037
  var parseMML = (mml, options = {}) => {
1889
- const stepsPerBar = options.stepsPerBar ?? 192;
3038
+ const stepsPerBar = options.stepsPerBar ?? DEFAULT_STEPS_PER_BAR;
3039
+ const collectTokens = options.collectTokens ?? false;
3040
+ const collectLyrics = options.collectLyrics ?? false;
3041
+ const clampTrackCount = options.clampTrackCount;
1890
3042
  const placements = [];
3043
+ const tokenTracks = /* @__PURE__ */ new Map();
1891
3044
  let bpm = null;
1892
- if (!mml) return { placements, bpm };
1893
- const fullMML = mml.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").replace(/[\n\r]+/g, " ").trim();
3045
+ if (!mml) {
3046
+ return {
3047
+ placements,
3048
+ bpm,
3049
+ tokenTracks: collectTokens ? tokenTracks : void 0,
3050
+ lyrics: collectLyrics ? /* @__PURE__ */ new Map() : void 0,
3051
+ meta: {}
3052
+ };
3053
+ }
3054
+ const noComments = mml.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
3055
+ const meta = parseMmlMeta(noComments);
3056
+ const noMeta = stripMmlMeta(noComments);
3057
+ const lyrics = collectLyrics ? parseLyrics(noMeta) : void 0;
3058
+ const fullMML = stripLyrics(noMeta).replace(/[\n\r]+/g, " ").trim();
1894
3059
  const parts = fullMML.split(/(@\d+)/).filter((p) => p.trim().length > 0);
1895
3060
  let trackIndex = 0;
1896
3061
  let octave = 4;
@@ -1900,7 +3065,7 @@ var parseMML = (mml, options = {}) => {
1900
3065
  const part = rawPart.trim();
1901
3066
  if (part.startsWith("@")) {
1902
3067
  let idx = Number.parseInt(part.substring(1), 10);
1903
- if (idx >= TRACK_INDEX_COUNT) idx = 2;
3068
+ if (clampTrackCount !== void 0 && idx >= clampTrackCount) idx = 2;
1904
3069
  trackIndex = idx;
1905
3070
  octave = 4;
1906
3071
  currentStep = 0;
@@ -1909,13 +3074,27 @@ var parseMML = (mml, options = {}) => {
1909
3074
  }
1910
3075
  const body = part.replace(/\s+/g, "").toLowerCase();
1911
3076
  let j = 0;
3077
+ const pushTok = (type, start, dur, from) => {
3078
+ if (!collectTokens) return;
3079
+ let arr = tokenTracks.get(trackIndex);
3080
+ if (!arr) {
3081
+ arr = [];
3082
+ tokenTracks.set(trackIndex, arr);
3083
+ }
3084
+ arr.push({
3085
+ text: body.slice(from, j),
3086
+ startStep: start,
3087
+ durationSteps: dur,
3088
+ type
3089
+ });
3090
+ };
1912
3091
  const parseLength = () => {
1913
3092
  let numStr = "";
1914
3093
  while (j < body.length && /\d/.test(body[j])) {
1915
3094
  numStr += body[j];
1916
3095
  j++;
1917
3096
  }
1918
- const len = numStr ? Number.parseInt(numStr, 10) : baseLength;
3097
+ const len = numStr ? clamp2(Number.parseInt(numStr, 10), 1, 64) : baseLength;
1919
3098
  let steps = Math.round(stepsPerBar / len);
1920
3099
  while (j < body.length && body[j] === ".") {
1921
3100
  steps = Math.round(steps * 1.5);
@@ -1925,6 +3104,7 @@ var parseMML = (mml, options = {}) => {
1925
3104
  };
1926
3105
  while (j < body.length) {
1927
3106
  const ch = body[j];
3107
+ const tokStart = j;
1928
3108
  if (ch === "o") {
1929
3109
  j++;
1930
3110
  let numStr = "";
@@ -1932,13 +3112,16 @@ var parseMML = (mml, options = {}) => {
1932
3112
  numStr += body[j];
1933
3113
  j++;
1934
3114
  }
1935
- octave = Number.parseInt(numStr, 10) || 4;
3115
+ octave = numStr ? clamp2(Number.parseInt(numStr, 10), 0, 8) : 4;
3116
+ pushTok("octave", currentStep, 0, tokStart);
1936
3117
  } else if (ch === ">") {
1937
- octave++;
3118
+ octave = Math.min(8, octave + 1);
1938
3119
  j++;
3120
+ pushTok("shift", currentStep, 0, tokStart);
1939
3121
  } else if (ch === "<") {
1940
- octave--;
3122
+ octave = Math.max(0, octave - 1);
1941
3123
  j++;
3124
+ pushTok("shift", currentStep, 0, tokStart);
1942
3125
  } else if (ch === "l") {
1943
3126
  j++;
1944
3127
  let numStr = "";
@@ -1946,11 +3129,15 @@ var parseMML = (mml, options = {}) => {
1946
3129
  numStr += body[j];
1947
3130
  j++;
1948
3131
  }
1949
- baseLength = Number.parseInt(numStr, 10) || 16;
3132
+ baseLength = clamp2(Number.parseInt(numStr, 10) || 16, 1, 64);
3133
+ pushTok("length", currentStep, 0, tokStart);
1950
3134
  } else if (ch === "r") {
1951
3135
  j++;
1952
- currentStep += parseLength();
1953
- } else if (ch === "t" || ch === "v" || ch === "q") {
3136
+ const restStart = currentStep;
3137
+ const restSteps = parseLength();
3138
+ pushTok("rest", restStart, restSteps, tokStart);
3139
+ currentStep += restSteps;
3140
+ } else if (ch === "t" || ch === "v" || ch === "q" || ch === "p") {
1954
3141
  j++;
1955
3142
  let numStr = "";
1956
3143
  while (j < body.length && /\d/.test(body[j])) {
@@ -1958,8 +3145,9 @@ var parseMML = (mml, options = {}) => {
1958
3145
  j++;
1959
3146
  }
1960
3147
  if (ch === "t" && trackIndex === 0 && numStr) {
1961
- bpm = Number.parseInt(numStr, 10);
3148
+ bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
1962
3149
  }
3150
+ pushTok("ctrl", currentStep, 0, tokStart);
1963
3151
  } else if (ch === "[") {
1964
3152
  j++;
1965
3153
  const chordNotes = [];
@@ -1978,10 +3166,10 @@ var parseMML = (mml, options = {}) => {
1978
3166
  }
1979
3167
  chordNotes.push((octave + 1) * 12 + pitch);
1980
3168
  } else if (c === ">") {
1981
- octave++;
3169
+ octave = Math.min(8, octave + 1);
1982
3170
  j++;
1983
3171
  } else if (c === "<") {
1984
- octave--;
3172
+ octave = Math.max(0, octave - 1);
1985
3173
  j++;
1986
3174
  } else if (c === "o") {
1987
3175
  j++;
@@ -1990,7 +3178,7 @@ var parseMML = (mml, options = {}) => {
1990
3178
  numStr += body[j];
1991
3179
  j++;
1992
3180
  }
1993
- octave = Number.parseInt(numStr, 10) || 4;
3181
+ octave = numStr ? clamp2(Number.parseInt(numStr, 10), 0, 8) : 4;
1994
3182
  } else {
1995
3183
  j++;
1996
3184
  }
@@ -2005,6 +3193,7 @@ var parseMML = (mml, options = {}) => {
2005
3193
  durationSteps: Math.max(1, steps)
2006
3194
  });
2007
3195
  }
3196
+ pushTok("chord", currentStep, Math.max(1, steps), tokStart);
2008
3197
  currentStep += steps;
2009
3198
  octave = savedOctave;
2010
3199
  } else if (Object.hasOwn(PITCH_MAP2, ch)) {
@@ -2025,18 +3214,25 @@ var parseMML = (mml, options = {}) => {
2025
3214
  pitch: midiPitch,
2026
3215
  durationSteps: Math.max(1, steps)
2027
3216
  });
3217
+ pushTok("note", currentStep, Math.max(1, steps), tokStart);
2028
3218
  currentStep += steps;
2029
3219
  } else {
2030
3220
  j++;
2031
3221
  }
2032
3222
  }
2033
3223
  }
2034
- return { placements, bpm };
3224
+ return {
3225
+ placements,
3226
+ bpm,
3227
+ tokenTracks: collectTokens ? tokenTracks : void 0,
3228
+ lyrics,
3229
+ meta
3230
+ };
2035
3231
  };
2036
3232
 
2037
3233
  // src/sequencer.ts
2038
3234
  var STEPS_PER_BEAT2 = 48;
2039
- var PLAN_TIME = 0.1;
3235
+ var PLAN_TIME = 0.5;
2040
3236
  var TICK_INTERVAL_MS = 20;
2041
3237
  var createSequencer = (options) => {
2042
3238
  let timeline = [];
@@ -2046,15 +3242,18 @@ var createSequencer = (options) => {
2046
3242
  let animationId = null;
2047
3243
  let active = false;
2048
3244
  let fromStepValue = 0;
3245
+ let trackVolumeMap = /* @__PURE__ */ new Map();
2049
3246
  const secondsPerStep = () => 60 / options.getBpm() / STEPS_PER_BEAT2;
2050
3247
  const buildTimeline = (fromStep) => {
2051
3248
  timeline = [];
3249
+ trackVolumeMap = /* @__PURE__ */ new Map();
2052
3250
  const sps = secondsPerStep();
2053
3251
  for (const track of options.getTracks()) {
3252
+ trackVolumeMap.set(track.id, track.volume);
2054
3253
  for (const note of track.notes) {
2055
3254
  const relativeStart = note.startStep - fromStep;
2056
3255
  if (relativeStart < 0) continue;
2057
- const velocity = note.velocity ?? 127;
3256
+ const velocity = note.velocity ?? DEFAULT_PLAYBACK_VELOCITY;
2058
3257
  timeline.push({
2059
3258
  trackId: track.id,
2060
3259
  pitch: note.pitch,
@@ -2071,6 +3270,9 @@ var createSequencer = (options) => {
2071
3270
  const sps = secondsPerStep();
2072
3271
  const time = options.getAudioTime() - startTime;
2073
3272
  const soloId = options.getSoloTrackId();
3273
+ for (const track of options.getTracks()) {
3274
+ trackVolumeMap.set(track.id, track.volume);
3275
+ }
2074
3276
  while (nowIndex < timeline.length) {
2075
3277
  const ev = timeline[nowIndex];
2076
3278
  if (soloId && ev.trackId !== soloId) {
@@ -2081,11 +3283,12 @@ var createSequencer = (options) => {
2081
3283
  if (_when > PLAN_TIME) break;
2082
3284
  nowIndex++;
2083
3285
  const velocityVolume = ev.velocity / 127;
3286
+ const currentVolume = (trackVolumeMap.get(ev.trackId) ?? ev.volume * 100) / 100;
2084
3287
  options.onPlayNote({
2085
3288
  trackId: ev.trackId,
2086
3289
  pitch: ev.pitch,
2087
3290
  velocity: ev.velocity,
2088
- volume: ev.volume * velocityVolume,
3291
+ volume: currentVolume * velocityVolume,
2089
3292
  when: Math.max(0, _when),
2090
3293
  duration: ev.duration
2091
3294
  });
@@ -2136,13 +3339,14 @@ var createSequencer = (options) => {
2136
3339
  }
2137
3340
  active = false;
2138
3341
  };
3342
+ const START_DELAY = 0.1;
2139
3343
  const start = (fromStep) => {
2140
3344
  stop();
2141
3345
  fromStepValue = fromStep ?? options.getPlayStartStep();
2142
3346
  buildTimeline(fromStepValue);
2143
3347
  if (timeline.length === 0 && !options.getDrumPattern()?.length) return;
2144
3348
  active = true;
2145
- startTime = options.getAudioTime();
3349
+ startTime = options.getAudioTime() + START_DELAY;
2146
3350
  nowIndex = 0;
2147
3351
  intervalId = setInterval(scheduleTick, TICK_INTERVAL_MS);
2148
3352
  animationId = requestAnimationFrame(animate);
@@ -2150,7 +3354,8 @@ var createSequencer = (options) => {
2150
3354
  return {
2151
3355
  start,
2152
3356
  stop,
2153
- isActive: () => active
3357
+ isActive: () => active,
3358
+ getStartTime: () => startTime
2154
3359
  };
2155
3360
  };
2156
3361
 
@@ -2612,10 +3817,12 @@ var DAW_CSS = `
2612
3817
 
2613
3818
  /* \u2500\u2500\u2500 \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 \u2500\u2500\u2500 */
2614
3819
  .dtm-overlay {
2615
- position: fixed; inset: 0; z-index: 1000;
3820
+ position: absolute; inset: 0; z-index: 1000;
2616
3821
  background: rgba(0,0,0,.92);
2617
3822
  display: flex; align-items: center; justify-content: center;
2618
3823
  flex-direction: column; gap: 14px;
3824
+ pointer-events: auto;
3825
+ cursor: wait;
2619
3826
  }
2620
3827
  .dtm-overlay[hidden] { display: none; }
2621
3828
  .dtm-overlay::before {
@@ -2643,18 +3850,206 @@ var DAW_CSS = `
2643
3850
  animation: dtm-load 1.6s steps(8) infinite;
2644
3851
  }
2645
3852
  @keyframes dtm-load { 0%{width:0} 100%{width:100%} }
3853
+ /* \u9032\u6357\u304C\u78BA\u5B9A\u3057\u305F\u3089\u7121\u9650\u30EB\u30FC\u30D7\u6F14\u51FA\u3092\u6B62\u3081\u3001\u5B9F\u6E2C\u5024\u3067\u5857\u308A\u3064\u3076\u3059 */
3854
+ .dtm-spinner--determinate::after { display: none; }
3855
+ .dtm-spinner-fill {
3856
+ position: absolute;
3857
+ left: 0; top: 0; height: 100%;
3858
+ width: 0;
3859
+ background: var(--dtm-primary);
3860
+ transition: width .12s steps(8);
3861
+ }
3862
+ .dtm-loading-label {
3863
+ font-family: var(--dtm-font);
3864
+ font-size: 11px;
3865
+ color: var(--dtm-primary);
3866
+ letter-spacing: .15em;
3867
+ min-height: 1em;
3868
+ }
3869
+
3870
+ @keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
3871
+ .dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
3872
+
3873
+ .dtm-hidden { display: none !important; }
3874
+ .dtm-grow { flex: 1 1 auto; }
3875
+ .dtm-lyric-icon {
3876
+ flex: 0 0 auto;
3877
+ width: 20px;
3878
+ height: 20px;
3879
+ border-radius: 50%;
3880
+ object-fit: cover;
3881
+ }
2646
3882
 
2647
- @keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
2648
- .dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
2649
-
2650
- .dtm-hidden { display: none !important; }
2651
- .dtm-grow { flex: 1 1 auto; }
2652
-
2653
3883
  /* \u2500\u2500\u2500 \u5E83\u5E45\u62E1\u5F35 \u2500\u2500\u2500 */
2654
3884
  @media (min-width: 768px) {
2655
3885
  .dtm-daw { gap: 8px; padding: 10px; }
2656
3886
  .dtm-roll { height: 420px; }
2657
3887
  }
3888
+
3889
+ /* ====================================================
3890
+ MML PLAYER \u2014 \u518D\u751F\u5C02\u7528\u30D3\u30E5\u30FC\uFF08mountMmlPlayer\uFF09
3891
+ ==================================================== */
3892
+ .dtm-player {
3893
+ display: flex;
3894
+ flex-direction: column;
3895
+ gap: var(--dtm-gap);
3896
+ padding: var(--dtm-gap);
3897
+ background: var(--dtm-deep);
3898
+ border: 2px solid var(--dtm-border2);
3899
+ box-shadow: 4px 4px 0 var(--c-black);
3900
+ }
3901
+ .dtm-player-head {
3902
+ display: flex;
3903
+ align-items: center;
3904
+ gap: 8px;
3905
+ flex-wrap: wrap;
3906
+ }
3907
+ .dtm-player-play {
3908
+ flex: 0 0 auto;
3909
+ width: 28px;
3910
+ height: 28px;
3911
+ display: flex;
3912
+ align-items: center;
3913
+ justify-content: center;
3914
+ background: var(--dtm-primary);
3915
+ color: var(--dtm-pfg);
3916
+ border: 2px solid var(--c-black);
3917
+ box-shadow: 2px 2px 0 var(--c-black);
3918
+ cursor: pointer;
3919
+ padding: 0;
3920
+ }
3921
+ .dtm-player-play:active { transform: translate(2px, 2px); box-shadow: none; }
3922
+ .dtm-player-play--stop { background: var(--dtm-danger); }
3923
+ .dtm-player-play:disabled { opacity: 0.4; cursor: default; }
3924
+ .dtm-player-beat-row {
3925
+ display: flex;
3926
+ align-items: center;
3927
+ gap: 4px;
3928
+ }
3929
+ .dtm-player-beat-dot {
3930
+ width: 8px;
3931
+ height: 8px;
3932
+ border-radius: 50%;
3933
+ background: var(--dtm-border2);
3934
+ transition: background 0.06s;
3935
+ }
3936
+ .dtm-player-beat-dot--on { background: var(--dtm-primary); }
3937
+ .dtm-player-bar {
3938
+ font-family: 'k8x12', monospace;
3939
+ font-size: 11px;
3940
+ color: var(--dtm-text);
3941
+ min-width: 2em;
3942
+ margin-left: 4px;
3943
+ }
3944
+ .dtm-player-dots {
3945
+ margin-left: auto;
3946
+ display: flex;
3947
+ align-items: center;
3948
+ gap: 6px;
3949
+ }
3950
+ .dtm-player-dot { width: 8px; height: 8px; display: inline-block; }
3951
+ .dtm-player-mml-header {
3952
+ margin-left: auto;
3953
+ display: flex;
3954
+ align-items: center;
3955
+ gap: 4px;
3956
+ }
3957
+ .dtm-player-mml-link {
3958
+ font-family: 'k8x12', monospace;
3959
+ font-size: 10px;
3960
+ color: var(--dtm-muted);
3961
+ text-decoration: none;
3962
+ white-space: nowrap;
3963
+ }
3964
+ .dtm-player-mml-link:hover { color: var(--dtm-primary); }
3965
+ .dtm-player-emoji {
3966
+ display: inline-flex;
3967
+ align-items: center;
3968
+ justify-content: center;
3969
+ width: 20px;
3970
+ height: 20px;
3971
+ border-radius: 50%;
3972
+ font-size: 18px;
3973
+ line-height: 1;
3974
+ user-select: none;
3975
+ }
3976
+ @keyframes dtm-emoji-jump {
3977
+ 0% { transform: translateY(0); }
3978
+ 35% { transform: translateY(-5px); }
3979
+ 65% { transform: translateY(-5px); }
3980
+ 100% { transform: translateY(0); }
3981
+ }
3982
+ .dtm-player-emoji--jump {
3983
+ animation: dtm-emoji-jump 0.18s ease-out forwards;
3984
+ }
3985
+ .dtm-player-chip {
3986
+ font-family: 'k8x12', monospace;
3987
+ font-size: 9px;
3988
+ color: var(--dtm-text);
3989
+ background: var(--dtm-border2);
3990
+ padding: 2px 6px;
3991
+ white-space: nowrap;
3992
+ }
3993
+ .dtm-player-body {
3994
+ position: relative; /* \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4\u306E\u57FA\u6E96\u3002\u30EC\u30FC\u30F3\u7FA4\u3060\u3051\u3092\u8986\u3046 */
3995
+ display: flex;
3996
+ flex-direction: column;
3997
+ gap: var(--dtm-gap);
3998
+ }
3999
+ .dtm-player-lane-row {
4000
+ display: flex;
4001
+ align-items: stretch;
4002
+ gap: 6px;
4003
+ }
4004
+ .dtm-player-lane-label {
4005
+ flex: 0 0 auto;
4006
+ width: 16px;
4007
+ display: flex;
4008
+ flex-direction: column;
4009
+ align-items: center;
4010
+ gap: 2px;
4011
+ padding-top: 4px;
4012
+ }
4013
+ .dtm-player-lane-label--btn {
4014
+ cursor: pointer;
4015
+ user-select: none;
4016
+ }
4017
+ .dtm-player-lane-label--btn:hover { opacity: 0.7; }
4018
+ .dtm-player-lane-label--muted { opacity: 0.3; }
4019
+ .dtm-player-lane-no {
4020
+ font-family: 'k8x12', monospace;
4021
+ font-size: 9px;
4022
+ color: var(--dtm-muted);
4023
+ }
4024
+ .dtm-player-lane {
4025
+ position: relative; /* \u30C8\u30FC\u30AF\u30F3\u306E offsetParent \u3092\u30EC\u30FC\u30F3\u306B\u56FA\u5B9A\u3057\u3001\u4E2D\u592E\u5BC4\u305B\u8A08\u7B97\u3092\u6B63\u3059 */
4026
+ flex: 1 1 auto;
4027
+ overflow-x: auto;
4028
+ white-space: nowrap;
4029
+ background: var(--c-black);
4030
+ border: 2px solid var(--dtm-border2);
4031
+ padding: 6px;
4032
+ scrollbar-width: none;
4033
+ }
4034
+ .dtm-player-lane::-webkit-scrollbar { display: none; }
4035
+ .dtm-tk {
4036
+ font-family: 'k8x12', monospace;
4037
+ font-size: 12px;
4038
+ color: var(--dtm-text);
4039
+ }
4040
+ .dtm-tk--rest { color: var(--dtm-muted); }
4041
+ .dtm-tk--octave,
4042
+ .dtm-tk--shift,
4043
+ .dtm-tk--length,
4044
+ .dtm-tk--ctrl { color: var(--dtm-border2); }
4045
+ .dtm-tk--lyric { color: var(--dtm-text); letter-spacing: 1px; }
4046
+ .dtm-tk--break { color: var(--dtm-muted); opacity: 0.7; margin: 0 2px; }
4047
+ .dtm-tk--meta { color: var(--dtm-border2); margin-right: 4px; }
4048
+ .dtm-tk.is-active {
4049
+ background: var(--tk, var(--dtm-primary));
4050
+ color: var(--c-black);
4051
+ font-weight: bold;
4052
+ }
2658
4053
  `;
2659
4054
  var injectStyles = (doc = document) => {
2660
4055
  if (doc.getElementById(STYLE_ID)) return;
@@ -2663,6 +4058,77 @@ var injectStyles = (doc = document) => {
2663
4058
  style.textContent = DAW_CSS;
2664
4059
  doc.head.appendChild(style);
2665
4060
  };
4061
+ var showLoadingOverlay = (container) => {
4062
+ const origPos = container.style.position;
4063
+ const computed = window.getComputedStyle(container).position;
4064
+ if (computed === "static") {
4065
+ container.style.position = "relative";
4066
+ }
4067
+ const doc = container.ownerDocument ?? document;
4068
+ const overlay = doc.createElement("div");
4069
+ overlay.className = "dtm-overlay";
4070
+ const spinner = doc.createElement("div");
4071
+ spinner.className = "dtm-spinner";
4072
+ const fill = doc.createElement("i");
4073
+ fill.className = "dtm-spinner-fill";
4074
+ spinner.appendChild(fill);
4075
+ overlay.appendChild(spinner);
4076
+ const label = doc.createElement("div");
4077
+ label.className = "dtm-loading-label";
4078
+ overlay.appendChild(label);
4079
+ container.appendChild(overlay);
4080
+ const setProgress = (done, total) => {
4081
+ if (total > 0) {
4082
+ const pct = Math.max(0, Math.min(100, Math.round(done / total * 100)));
4083
+ spinner.classList.add("dtm-spinner--determinate");
4084
+ fill.style.width = `${pct}%`;
4085
+ label.textContent = `${done} / ${total} (${pct}%)`;
4086
+ } else {
4087
+ spinner.classList.remove("dtm-spinner--determinate");
4088
+ fill.style.width = "0";
4089
+ label.textContent = "";
4090
+ }
4091
+ };
4092
+ return {
4093
+ remove: () => {
4094
+ overlay.remove();
4095
+ container.style.position = origPos;
4096
+ },
4097
+ setProgress
4098
+ };
4099
+ };
4100
+
4101
+ // assets/puyuyu.png
4102
+ var puyuyu_default = "data:image/png;base64,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";
4103
+
4104
+ // assets/rino.png
4105
+ var rino_default = "data:image/png;base64,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";
4106
+
4107
+ // assets/roze.png
4108
+ var roze_default = "data:image/png;base64,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";
4109
+
4110
+ // assets/ruko.png
4111
+ var ruko_default = "data:image/png;base64,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";
4112
+
4113
+ // assets/shiyo.png
4114
+ var shiyo_default = "data:image/png;base64,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";
4115
+
4116
+ // assets/teto.png
4117
+ var teto_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYAAAAGACAYAAACkx7W/AAAQAElEQVR4AezXsYod1xkH8LMySIqfIHZcuEwhYhCBkE6NUYp9AmOISz2AAs4DGAWkB0jpKqQIabxNksYkgaQwCwEVfoCQtMGFkd1s7mhXtne9d++9M/PNnHO+n7lHu3vvzHfO9/tm949vFf/1LnC2abDZ9fj+8Vnkatnm4uybL14ExgkIgHFu7iJAgEDzAgKg+RFqgMBIAbelFxAA6R8BAAQIZBUQAFknr28CBNILCIC0j4DGCRDILiAAsj8B+idAIK2AAEg7eo0TIJBV4FXfAuCVhK8ECBBIJiAAkg1cuwQIEHglIABeSfhKIIuAPglcCAiACwhfCBAgkE1AAGSbuH4JECBwISAALiDyfNEpAQIEzgUEwLmDfwkQIJBOQACkG7mGCRDIKnC1bwFwVcTPBAgQSCIgAJIMWpsECBC4KiAAror4mUCvAvoicEVAAFwB8SMBAgSyCAiALJPWJwECBK4ICIArIP3+qDMCBAhcFhAAlz38RIAAgTQCAiDNqDVKgEBWgW19LxEAZ5vNrVJWMXh8/7i0vB7cuV0iV8s2w9n9bq3ze/Ud98237b6WCIB2dZycAAECHQsIgI6HqzUCLwX8Q2CLgADYAuNtAgQI9C4gAHqfsP4IECCwRUAAbIHp522dECBA4HoBAXC9i3cJECDQvYAA6H7EGiRAIKvArr4FwC4hnxMgQKBTAQHQ6WC1RYAAgV0CAmCXkM8JtCrg3AR2CAiAHUA+JkCAQK8CAqDXyeqLAAECOwQEwA6gdj92cgIECNwsIABu9vEpAQIEuhUQAN2OVmMECGQV2LdvAbCvlOsIECDQmYAA6Gyg2iFAgMC+AgJgXynXEWhFwDkJ7CkgAPaEchkBAgR6ExAAvU1UPwQIENhTQADsCdXOZU5KgACB/QQEwH5OriJAgEB3AgKgu5FqiACBrAKH9j0EwNnmprD1+P5xsdYzeHDndolcH7x+t0Sut1+7VSJX5NmH2pH2Q22/W+v9bg32kX87L2pvvsS9hgCIq64yAQIECFQrIACqHY2DEThQwOUEDhQQAAeCuZwAAQK9CAiAXiapDwIECBwoIAAOBKv3cicjQIDAYQIC4DAvVxMgQKAbAQHQzSg1QoBAVoGxfQuAsXLuI0CAQOMCAqDxATo+AQIExgoIgLFy7iNQi4BzEBgpIABGwrmNAAECrQsIgNYn6PwECBAYKSAARsLVc5uTECBAYJyAABjn5i4CBAg0LyAAmh+hBggQyCowtW8BMFXQ/QQIEGhUQAA0OjjHJkCAwFQBATBV0P0E1hKwL4GJAgJgIqDbCRAg0KqAAGh1cs5NgACBiQICYCLgerfbmQABAtMEBMA0P3cTIECgWQEB0OzoHJwAgawCc/V96/H94xK5Hty5XSLXB6/fLZFrLuhtdSJthtpvv3arRK5tfXn/XCDSfqg9zDhynXcR92/k7+5QO9JmqB35t3OoHSd/Xtn/AZw7+JcAAQLpBARAupFruHkBDRCYSUAAzASpDAECBFoTEACtTcx5CRAgMJOAAJgJcrkydiJAgMA8AgJgHkdVCBAg0JyAAGhuZA5MgEBWgbn7FgBzi6pHgACBRgQEQCODckwCBAjMLSAA5hZVj0CUgLoEZhYQADODKkeAAIFWBARAK5NyTgIECMwsIABmBo0rpzIBAgTmFRAA83qqRoAAgWYEBEAzo3JQAgSyCkT1LQCiZNUlQIBA5QICoPIBOR4BAgSiBARAlKy6BOYSUIdAkIAACIJVlgABArULCIDaJ+R8BAgQCBIQAEGw85VViQABAjECAiDGVVUCBAhULyAAqh+RAxIgkFUguu/wAPj0q69L5IoGiq7/w5/9pESuj798USLXi5/eKy2vSJuhdrRN5LMz1I5+/qPrR/7tGWpHnz+6fngARDegPgECBAiMExAA49zcRSBewA4EggUEQDCw8gQIEKhVQADUOhnnIkCAQLCAAAgGHl/enQQIEIgVEACxvqoTIECgWgEBUO1oHIwAgawCS/UtAJaStg8BAgQqExAAlQ3EcQgQILCUgABYSto+BPYVcB2BhQQEwELQtiFAgEBtAgKgtok4DwECBBYSEAALQe+/jSsJECCwjIAAWMbZLgQIEKhOQABUNxIHIkAgq8DSfQuApcXtR4AAgUoEBEAlg3AMAgQILC0gAJYWtx+BbQLeJ7CwgABYGNx2BAgQqEVAANQyCecgQIDAwgICYGHw7dv5hAABAssKCIBlve1GgACBagQEQDWjcBACBLIKrNV3eAA8uHO7RK614Oba9/d//axErqdPjkvkijz7UPvuZ89L5Iq0GWoPPUSuSJuh9lzP+Vp1Iv/2DLXX6muufcMDYK6DqkOAAAEC8woIgHk9VSNwuIA7CKwkIABWgrctAQIE1hYQAGtPwP4ECBBYSUAArAT/7ba+I0CAwDoCAmAdd7sSIEBgdQEBsPoIHIAAgawCa/ctANaegP0JECCwkoAAWAnetgQIEFhbQACsPQH75xXQOYGVBQTAygOwPQECBNYSEABryduXAAECKwsIgNUGYGMCBAisKyAA1vW3OwECBFYTEACr0duYAIGsArX0LQBqmYRzECBAYGEBAbAwuO0IECBQi4AAqGUSzpFHQKcEKhEQAJUMwjEIECCwtIAAWFrcfgQIEKhEQAAsPggbEiBAoA4BAVDHHJyCAAECiwsIgMXJbUiAQFaB2voOD4BPv/q6RK7aQA89z9MnxyVyHT18VCLX008+KpHr4y9flMhV7v28RK5npyclckXaDLUPfZ5ruz7yb89Qu7Z+Dz1PeAAceiDXEyBAgMAyAgJgGWe7ECiFAYHKBARAZQNxHAIECCwlIACWkrYPAQIEKhMQAIsNxEYECBCoS0AA1DUPpyFAgMBiAgJgMWobESCQVaDWvgVArZNxLgIECAQLCIBgYOUJECBQq4AAqHUyztWPgE4IVCogACodjGMRIEAgWkAARAurT4AAgUoFBED4YGxAgACBOgUEQJ1zcSoCBAiECwiAcGIbECCQVaD2vgVA7RNyPgIECAQJCIAgWGUJECBQu4AAqH1CzteugJMTqFxAAFQ+IMcjQIBAlIAAiJJVlwABApULCICwASlMgACBugUEQN3zcToCBAiECQiAMFqFCRDIKtBK3+EB8ODO7RK5oqGfnZ6UyBV9/tbrP/3koxK5mvd5clyeBq7IZ3+oHe0f+bdnqB19/uj64QEQ3YD6BAgQIDBOQACMc3MXge0CPiHQiIAAaGRQjkmAAIG5BQTA3KLqESBAoBEBATD7oBQkQIBAGwICoI05OSUBAgRmFxAAs5MqSIBAVoHW+hYArU3MeQkQIDCTgACYCVIZAgQItCYgAFqbmPPWK+BkBBoTEACNDcxxCRAgMJeAAJhLUh0CBAg0JiAAZhuYQgQIEGhLQAC0NS+nJUCAwGwCAmA2SoUIEMgq0GrfAqDVyTk3AQIEJgoIgImAbidAgECrAgKg1ck5dz0CTkKgUQEB0OjgHJsAAQJTBQTAVEH3EyBAoFEBATB5cAoQIECgTQEB0ObcnJoAAQKTBQTAZEIFCBDIKtB637eenZ6UyBUNdO/vfyiR6/H94xK5jh4+KpHr7E+/LZErer6t14+0H2pHPjtD7chnf6gd+bs71I5+fiL/dg61o8/v/wCihdUnQIBApQICoNLBOFYDAo5IoHEBAdD4AB2fAAECYwUEwFg59xEgQKBxAQEweoBuJECAQNsCAqDt+Tk9AQIERgsIgNF0biRAIKtAL30LgF4mqQ8CBAgcKCAADgRzOQECBHoREAC9TFIfywnYiUAnAgKgk0FqgwABAocKCIBDxVxPgACBTgQEwMGDdAMBAgT6EBAAfcxRFwQIEDhYQAAcTOYGAgSyCvTWtwDobaL6IUCAwJ4CAmBPKJcRIECgNwEB0NtE9RMnoDKBzgQEQGcD1Q4BAgT2FRAA+0q5jgABAp0JCIC9B+pCAgQI9CUgAPqap24IECCwt4AA2JvKhQQIZBXote8hAI42zYWt43/8sUSuyLMPtZ+dnpTI9fj+cYlcRw8fldD1o3fKUeAqzz8pkSvy7C9rB/tHPjtD7chnf6g9/I5Frsi/PUPtyLNf1N58iXsNARBXXWUCBAgQqFZAAFQ7GgerRsBBCHQqIAA6Hay2CBAgsEtAAOwS8jkBAgQ6FRAAOwfrAgIECPQpIAD6nKuuCBAgsFNAAOwkcgEBAlkFeu9bAPQ+Yf0RIEBgi4AA2ALjbQIECPQuIAB6n7D+xgu4k0DnAgKg8wFrjwABAtsEBMA2Ge8TIECgcwEBsHXAPiBAgEDfAgKg7/nqjgABAlsFBMBWGh8QIJBVIEvfAiDLpPVJgACBKwIC4AqIHwkQIJBFQABkmbQ+9xdwJYEkAgIgyaC1SYAAgasCAuCqiJ8JECCQREAAfG/Q3iBAgEAOAQGQY866JECAwPcEBMD3SLxBgEBWgWx9LxEARxvUyHW2qR+23n3vwxK5nj45LpEr8uxD7XsP3i+R61e//meJXJFnH2oPRpEr8tkZam9+t1p/Rf7tGWo37XOr6dM7PAECBAiMFhAAo+nc2J2AhggkExAAyQauXQIECLwSEACvJHwlQIBAMgEB8M3AfUOAAIFcAgIg17x1S4AAgW8EBMA3FL4hQCCrQNa+BUDWyeubAIH0AgIg/SMAgACBrAICIOvk9f2tgO8IJBUQAEkHr20CBAgIAM8AAQIEkgoIgJJ08tomQCC9gABI/wgAIEAgq4AAyDp5fRMgULITCIDsT4D+CRBIKyAA0o5e4wQIZBcQANmfgMz9651AcgEBkPwB0D4BAnkFBEDe2eucAIHkAokDIPnktU+AQHoBAZD+EQBAgEBWAQGQdfL6JpBYQOvnAksEwNlmq7D17nsflsi1OXvo6+jhoxK5Qg+/Kf7Gm2+VyPXB63dL5Io8+1B7QxT6inx2htr3HrxfItcGJ+xvw0XtzRevbQK3tn3gfQIECBDoW0AA9D1f3V0n4D0CBF4KCICXDP4hQIBAPgEBkG/mOiZAgMBLgYQB8LJv/xAgQCC9gABI/wgAIEAgq4AAyDp5fRNIKKDlywIC4LKHnwgQIJBGQACkGbVGCRAgcFlAAFz28FPPAnojQOCSgAC4xOEHAgQI5BEQAHlmrVMCBAhcEkgUAJf69gMBAgTSCwiA9I8AAAIEsgoIgKyT1zeBRAJavV5AAFzv4l0CBAh0LyAAuh+xBgkQIHC9gAC43sW7PQnohQCBawUEwLUs3iRAgED/AgKg/xnrkAABAtcKJAiAa/v2JgECBNILCID0jwAAAgSyCgiArJPXN4EEAlq8WUAA3OxT/vzLt0PXju0nf/zf//y7RK7JB9xR4OMvX5TItWP7yR9H2g+1Jx9QgdQCAiD1+DVPgEBmAQGQefq9964/AgRuFBAAN/L4kAABAv0KCIB+Z6szAgQI3CjQcQDc2LcPCRAgkF5AAKR/BAAQIJBVQABknby+CXQsoLX9HvQLCwAABHlJREFUBATAfk6uIkCAQHcCAqC7kWqIAAEC+wkIgP2cXNWSgLMSILCXgADYi8lFBAgQ6E9AAPQ3Ux0RIEBgL4EOA2Cvvl1EgACB9AICIP0jAIAAgawCAiDr5PVNoEMBLR0mIAAO83I1AQIEuhEQAN2MUiMECBA4TEAAHObl6poFnI0AgYMEBMBBXC4mQIBAPwICoJ9Z6oQAAQIHCXQUAAf17WICBAikFxAA6R8BAAQIZBUQAFknr28CHQloZZzAEgFwtDla2PrL735TItfRw0clcr373oclcr3x5lslcm1mG/r614/vlcj1zufPS+T6xRf/K5Er8tkZakeefagd+vAovlNgiQDYeQgXECBAgMDyAgJgeXM7zi2gHgECowQEwCg2NxEgQKB9AQHQ/gx1QIAAgVECHQTAqL7dRIAAgfQCAiD9IwCAAIGsAgIg6+T1TaADAS1MExAA0/zcTYAAgWYFBECzo3NwAgQITBMQANP83L2mgL0JEJgkIAAm8bmZAAEC7QoIgHZn5+QECBCYJNBwAEzq280ECBBILyAA0j8CAAgQyCogALJOXt8EGhZw9HkEBMA8jqoQIECgOQEB0NzIHJgAAQLzCAiAeRxVWVLAXgQIzCIgAGZhVIQAAQLtCQiA9mbmxAQIEJhFoMEAmKVvRQgQIJBeQACkfwQAECCQVUAAZJ28vgk0KODI8wr0EABHG5LItSkf93rn8+fFWs/gbz/4orS8op+dZ6cnJXJtfrMif3eH2pstvLYJ9BAA23rzPgECBAjcICAAbsDxUWUCjkOAwKwCAmBWTsUIECDQjoAAaGdWTkqAAIFZBRoKgFn7VowAAQLpBQRA+kcAAAECWQUEQNbJ65tAQwKOGiMgAGJcVSVAgED1AgKg+hE5IAECBGIEBECMq6pzCqhFgECIgAAIYVWUAAEC9QsIgPpn5IQECBAIEWggAEL6VpQAAQLpBQRA+kcAAAECWQUEQNbJ65tAAwKOGCsgAGJ9VSdAgEC1AgKg2tE4GAECBGIFBECsr+pTBNxLgECogAAI5VWcAAEC9QoIgHpn42QECBAIFag4AEL7VpwAAQLpBQRA+kcAAAECWQUEQNbJ65tAxQKOtoyAANjtfLS5JGw9Oz05Cl5lU986PbnWYDPbpl/Rs93ghD37F7U3X7zWEhAAa8nblwABAisLCICVB2D7awS8RYDAIgICYBFmmxAgQKA+AQFQ30yciAABAosIVBgAi/RtEwIECKQXEADpHwEABAhkFRAAWSevbwIVCjjSsgICYFlvuxEgQKAaAQFQzSgchAABAssKCIBlve12k4DPCBBYVEAALMptMwIECNQjIADqmYWTECBAYFGBigJg0b5tRoAAgfQCAiD9IwCAAIGsAgIg6+T1TaAiAUdZR0AArONuVwIECKwuIABWH4EDECBAYB0BAbCOu12/K+B7AgRWERAAq7DblAABAusLCID1Z+AEBAgQWEWgggBYpW+bEiBAIL2AAEj/CAAgQCCrgADIOnl9E6hAwBHWFfg/AAAA//9B7gmMAAAABklEQVQDAHVQvGH0+qUKAAAAAElFTkSuQmCC";
4118
+
4119
+ // assets/tsukuyomi.png
4120
+ var tsukuyomi_default = "data:image/png;base64,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";
4121
+
4122
+ // src/voice-images.ts
4123
+ var VOICE_IMAGES = {
4124
+ puyuyu: puyuyu_default,
4125
+ rino: rino_default,
4126
+ roze: roze_default,
4127
+ ruko: ruko_default,
4128
+ shiyo: shiyo_default,
4129
+ teto: teto_default,
4130
+ tsukuyomi: tsukuyomi_default
4131
+ };
2666
4132
 
2667
4133
  // src/daw.ts
2668
4134
  var BASE_STEP_WIDTH = 0.5;
@@ -2812,7 +4278,13 @@ var TRACKS_ADVANCED = [
2812
4278
  }
2813
4279
  ];
2814
4280
  var DEFAULT_TRACKS = TRACKS_SIMPLE;
2815
- var clamp = (v, min, max) => Math.min(Math.max(v, min), max);
4281
+ var LYRIC_MODELS = ["klatt", ...Object.keys(KOE_VOICEBANKS)];
4282
+ var LYRIC_MODEL_LABELS = {
4283
+ klatt: "\u8EFD\u91CF\u30ED\u30DC\u58F0",
4284
+ ...KOE_VOICEBANK_LABELS
4285
+ };
4286
+ var lyricModelLabel = (model) => LYRIC_MODEL_LABELS[model] ?? model;
4287
+ var clamp3 = (v, min, max) => Math.min(Math.max(v, min), max);
2816
4288
  var mountDAW = (target, options = {}) => {
2817
4289
  injectStyles();
2818
4290
  const getAudioTime = options.getAudioTime ?? (() => performance.now() / 1e3);
@@ -2824,7 +4296,7 @@ var mountDAW = (target, options = {}) => {
2824
4296
  tracks: trackConfigs,
2825
4297
  drumPatternNames: Object.keys(drumPatterns),
2826
4298
  defaultDrumPattern: drumPatterns.dance ? "dance" : Object.keys(drumPatterns)[0] ?? "none",
2827
- defaultBpm: options.defaultBpm ?? 120,
4299
+ defaultBpm: options.defaultBpm ?? DEFAULT_BPM,
2828
4300
  showMidi,
2829
4301
  showChord
2830
4302
  });
@@ -2839,10 +4311,11 @@ var mountDAW = (target, options = {}) => {
2839
4311
  const leftPaddingSteps = renderConfig.stepsPerBar * 16;
2840
4312
  let zoomX = 100;
2841
4313
  let zoomY = 100;
2842
- let bpm = options.defaultBpm ?? 120;
4314
+ let bpm = options.defaultBpm ?? DEFAULT_BPM;
2843
4315
  let masterVolume = 50;
2844
4316
  let drumVolume = 80;
2845
4317
  let currentDrumPattern = refs.drumSelect.value;
4318
+ let currentInstrument = "";
2846
4319
  let activeTrackId = trackConfigs[0].id;
2847
4320
  let activeToolMode = "pen";
2848
4321
  let currentInsertLength = 48;
@@ -2852,6 +4325,7 @@ var mountDAW = (target, options = {}) => {
2852
4325
  let currentOffsetY = (104 - 1 - 60) * renderConfig.keyHeight - 215;
2853
4326
  let playStartStep = 0;
2854
4327
  let isSolo = false;
4328
+ let lyricTrackIndices = /* @__PURE__ */ new Set();
2855
4329
  let playbackState = "stopped";
2856
4330
  let pausedPlayStep = 0;
2857
4331
  let currentPlayStep = 0;
@@ -2878,9 +4352,36 @@ var mountDAW = (target, options = {}) => {
2878
4352
  volume: config.volume,
2879
4353
  savedChordInput: "",
2880
4354
  savedChordPattern: "block",
2881
- savedChordRoot: 0
4355
+ savedChordRoot: 0,
4356
+ lyrics: "",
4357
+ lyricModel: "",
4358
+ // 既定は「なし」(歌わない)
4359
+ vocalVolume: DEFAULT_VOCAL_VOLUME,
4360
+ vocalGate: 100,
4361
+ vocalPan: 64,
4362
+ vocalOctave: 0
2882
4363
  }));
2883
4364
  };
4365
+ const buildLyricsMap = () => {
4366
+ const map = /* @__PURE__ */ new Map();
4367
+ trackStates.forEach((t, i) => {
4368
+ const model = t.lyricModel.trim();
4369
+ const text = t.lyrics.trim();
4370
+ if (!model || !text) return;
4371
+ const syllables = normalizeLyrics(text);
4372
+ if (syllables.length === 0) return;
4373
+ map.set(i, {
4374
+ trackId: i,
4375
+ model: model.toLowerCase(),
4376
+ volume: t.vocalVolume,
4377
+ gate: t.vocalGate,
4378
+ pan: t.vocalPan,
4379
+ octave: t.vocalOctave,
4380
+ syllables
4381
+ });
4382
+ });
4383
+ return map;
4384
+ };
2884
4385
  const getActive = () => trackStates.find((t) => t.config.id === activeTrackId) ?? trackStates[0];
2885
4386
  const getMaxNoteStep = () => {
2886
4387
  let maxStep = renderConfig.stepsPerBar * 4;
@@ -2984,7 +4485,7 @@ var mountDAW = (target, options = {}) => {
2984
4485
  );
2985
4486
  const ratio = currentOffsetX / maxOffsetX;
2986
4487
  refs.hScrollThumb.style.width = `${thumbW}px`;
2987
- refs.hScrollThumb.style.left = `${clamp(ratio * (sbW - thumbW), 0, sbW - thumbW)}px`;
4488
+ refs.hScrollThumb.style.left = `${clamp3(ratio * (sbW - thumbW), 0, sbW - thumbW)}px`;
2988
4489
  }
2989
4490
  const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
2990
4491
  const sbH = refs.vScroll.clientHeight;
@@ -3043,9 +4544,9 @@ var mountDAW = (target, options = {}) => {
3043
4544
  if (maxOffsetX <= 0) return;
3044
4545
  const rect = refs.hScroll.getBoundingClientRect();
3045
4546
  const thumbW = Number.parseFloat(refs.hScrollThumb.style.width) || 40;
3046
- const x = clamp(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
4547
+ const x = clamp3(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
3047
4548
  const ratio = x / (rect.width - thumbW);
3048
- currentOffsetX = clamp(ratio * maxOffsetX, 0, maxOffsetX);
4549
+ currentOffsetX = clamp3(ratio * maxOffsetX, 0, maxOffsetX);
3049
4550
  setDrawOffset(currentOffsetX, currentOffsetY);
3050
4551
  redrawAll();
3051
4552
  };
@@ -3054,9 +4555,9 @@ var mountDAW = (target, options = {}) => {
3054
4555
  if (maxOffset <= 0) return;
3055
4556
  const rect = refs.vScroll.getBoundingClientRect();
3056
4557
  const thumbH = Number.parseFloat(refs.vScrollThumb.style.height) || 40;
3057
- const y = clamp(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
4558
+ const y = clamp3(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
3058
4559
  const ratio = y / (rect.height - thumbH);
3059
- currentOffsetY = clamp(ratio * maxOffset, 0, maxOffset);
4560
+ currentOffsetY = clamp3(ratio * maxOffset, 0, maxOffset);
3060
4561
  setDrawOffset(currentOffsetX, currentOffsetY);
3061
4562
  redrawAll();
3062
4563
  };
@@ -3338,7 +4839,7 @@ var mountDAW = (target, options = {}) => {
3338
4839
  "wheel",
3339
4840
  (event) => {
3340
4841
  event.preventDefault();
3341
- currentOffsetY = clamp(
4842
+ currentOffsetY = clamp3(
3342
4843
  currentOffsetY + event.deltaY,
3343
4844
  0,
3344
4845
  getMaxOffsetY()
@@ -3386,7 +4887,7 @@ var mountDAW = (target, options = {}) => {
3386
4887
  const centerKey = (currentOffsetY + canvas.height / 2) / renderConfig.keyHeight;
3387
4888
  renderConfig.keyHeight = BASE_KEY_HEIGHT * zoomY / 100;
3388
4889
  refs.zoomYLabel.textContent = `${zoomY}%`;
3389
- currentOffsetY = clamp(
4890
+ currentOffsetY = clamp3(
3390
4891
  centerKey * renderConfig.keyHeight - canvas.height / 2,
3391
4892
  0,
3392
4893
  getMaxOffsetY()
@@ -3394,6 +4895,13 @@ var mountDAW = (target, options = {}) => {
3394
4895
  setDrawOffset(currentOffsetX, currentOffsetY);
3395
4896
  redrawAll();
3396
4897
  };
4898
+ const getViewState = () => ({
4899
+ zoomX,
4900
+ zoomY,
4901
+ decomposeChord: refs.decomposeChordToggle.checked,
4902
+ ignoreChordHeavy: refs.ignoreChordHeavyToggle.checked
4903
+ });
4904
+ const notifyViewState = () => options.onViewStateChange?.(getViewState());
3397
4905
  const dispatchNote = (trackId, pitch, trackVol, velocity, when, duration) => {
3398
4906
  const volume = trackVol / 100 * (velocity / 127) * (masterVolume / 100);
3399
4907
  options.onPlayNote?.({ trackId, pitch, velocity, volume, when, duration });
@@ -3410,8 +4918,11 @@ var mountDAW = (target, options = {}) => {
3410
4918
  getSoloTrackId: () => isSolo ? activeTrackId : null,
3411
4919
  getAudioTime,
3412
4920
  onPlayNote: (e) => {
3413
- const volume = e.volume * (masterVolume / 100);
3414
- options.onPlayNote?.({ ...e, volume });
4921
+ const idx = trackStates.findIndex((t) => t.config.id === e.trackId);
4922
+ if (idx >= 0 && lyricTrackIndices.has(idx) && options.singingVoices) {
4923
+ return;
4924
+ }
4925
+ options.onPlayNote?.({ ...e, volume: e.volume * (masterVolume / 100) });
3415
4926
  },
3416
4927
  onPlayDrum: (e) => {
3417
4928
  const velocity = e.velocity * (drumVolume / 100) * (masterVolume / 100);
@@ -3437,10 +4948,53 @@ var mountDAW = (target, options = {}) => {
3437
4948
  },
3438
4949
  stepsPerBar: renderConfig.stepsPerBar
3439
4950
  });
3440
- const play = () => {
4951
+ const play = async () => {
3441
4952
  options.onResumeAudio?.();
3442
4953
  if (playbackState === "playing") return;
3443
4954
  const fromStep = playbackState === "paused" ? pausedPlayStep : playStartStep;
4955
+ options.singingVoices?.reset();
4956
+ const lyricMap = buildLyricsMap();
4957
+ lyricTrackIndices = new Set(lyricMap.keys());
4958
+ const secondsPerStep = 60 / bpm / 48;
4959
+ const streamTracks = options.singingVoices ? [...lyricMap.values()].map((lt) => {
4960
+ const trackState = trackStates[lt.trackId];
4961
+ const sorted = [...trackState?.core.getNotes() ?? []].sort(
4962
+ (a, b) => a.startStep - b.startStep
4963
+ );
4964
+ const gate = (lt.gate ?? DEFAULT_GATE) / 100;
4965
+ const semis = (lt.octave ?? 0) * 12;
4966
+ const count = Math.min(sorted.length, lt.syllables.length);
4967
+ const notes = [];
4968
+ for (let i = 0; i < count; i++) {
4969
+ const n = sorted[i];
4970
+ if (n.startStep < fromStep) continue;
4971
+ notes.push({
4972
+ syllable: lt.syllables[i],
4973
+ pitch: n.pitch + semis,
4974
+ startSec: (n.startStep - fromStep) * secondsPerStep,
4975
+ durationSec: n.durationSteps * secondsPerStep * gate
4976
+ });
4977
+ }
4978
+ return {
4979
+ model: lt.model,
4980
+ volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (masterVolume / 100),
4981
+ pan: panToStereo(lt.pan ?? DEFAULT_PAN),
4982
+ notes
4983
+ };
4984
+ }) : [];
4985
+ const voices = options.singingVoices;
4986
+ const streaming = !!voices && streamTracks.some((t) => t.notes.length > 0);
4987
+ if (streaming && voices) {
4988
+ const overlay = showLoadingOverlay(refs.rollContainer);
4989
+ try {
4990
+ await voices.loadModels(streamTracks.map((t) => t.model));
4991
+ await voices.warm(streamTracks);
4992
+ } catch (err) {
4993
+ console.warn("[dtm] voice preload failed", err);
4994
+ } finally {
4995
+ overlay.remove();
4996
+ }
4997
+ }
3444
4998
  if (playbackState !== "paused") {
3445
4999
  const canvas = getGridCanvas();
3446
5000
  currentOffsetX = Math.max(
@@ -3451,17 +5005,22 @@ var mountDAW = (target, options = {}) => {
3451
5005
  }
3452
5006
  playbackState = "playing";
3453
5007
  sequencer.start(fromStep);
5008
+ if (streaming && voices) {
5009
+ voices.startStream(streamTracks, sequencer.getStartTime());
5010
+ }
3454
5011
  updateTransport();
3455
5012
  };
3456
5013
  const pause = () => {
3457
5014
  if (playbackState !== "playing") return;
3458
5015
  pausedPlayStep = currentPlayStep;
3459
5016
  sequencer.stop();
5017
+ options.singingVoices?.stopStream();
3460
5018
  playbackState = "paused";
3461
5019
  updateTransport();
3462
5020
  };
3463
5021
  const stop = () => {
3464
5022
  sequencer.stop();
5023
+ options.singingVoices?.stopStream();
3465
5024
  playbackState = "stopped";
3466
5025
  currentPlayStep = 0;
3467
5026
  updateTransport();
@@ -3511,6 +5070,158 @@ var mountDAW = (target, options = {}) => {
3511
5070
  active.core.setVolume(active.volume);
3512
5071
  volLabel.textContent = String(active.volume);
3513
5072
  });
5073
+ const lyricDiv = document.createElement("div");
5074
+ lyricDiv.className = "dtm-row";
5075
+ lyricDiv.style.flexDirection = "column";
5076
+ lyricDiv.style.alignItems = "stretch";
5077
+ lyricDiv.innerHTML = `
5078
+ <div class="dtm-row">
5079
+ <span class="dtm-label">\u266A \u6B4C\u8A5E</span>
5080
+ <select class="dtm-select" data-dtm="lyric-model" aria-label="\u6B4C\u5531\u30E2\u30C7\u30EB"></select>
5081
+ <img class="dtm-lyric-icon dtm-hidden" data-dtm="lyric-icon" width="20" height="20" alt="" draggable="false">
5082
+ <select class="dtm-select" data-dtm="lyric-octave" aria-label="\u30AA\u30AF\u30BF\u30FC\u30D6\uFF08\u97F3\u6E90\u306E\u5F97\u610F\u97F3\u57DF\u306B\u5408\u308F\u305B\u308B\uFF09" title="\u30AA\u30AF\u30BF\u30FC\u30D6">
5083
+ <option value="2">+2 oct</option>
5084
+ <option value="1">+1 oct</option>
5085
+ <option value="0">\xB10 oct</option>
5086
+ <option value="-1">-1 oct</option>
5087
+ <option value="-2">-2 oct</option>
5088
+ </select>
5089
+ <span class="dtm-label dtm-grow" data-dtm="lyric-count" style="text-align:right"></span>
5090
+ </div>
5091
+ <div class="dtm-row dtm-hidden" data-dtm="lyric-terms" style="font-size:10px;gap:4px;color:var(--dtm-warn)">
5092
+ <span>\u4F7F\u7528\u6642\u306B\u306F</span>
5093
+ <a data-dtm="lyric-terms-link" target="_blank" rel="noopener" style="color:var(--dtm-primary);text-decoration:underline"></a>
5094
+ <span>\u306E\u5229\u7528\u898F\u7D04\u306B\u5F93\u3063\u3066\u304F\u3060\u3055\u3044</span>
5095
+ </div>
5096
+ <div class="dtm-row" data-dtm="lyric-body" style="flex-direction:column;align-items:stretch">
5097
+ <div class="dtm-row">
5098
+ <span class="dtm-label">\u58F0\u91CF</span>
5099
+ <input type="range" class="dtm-range dtm-grow" data-dtm="lyric-vol" min="0" max="${MAX_VOCAL_VOLUME}" aria-label="\u6B4C\u5531\u306E\u58F0\u91CF\uFF08100=\u7B49\u500D\u3001100\u8D85\u3067\u30D6\u30FC\u30B9\u30C8\u3001\u65E2\u5B9A200\uFF09">
5100
+ <span class="dtm-label" data-dtm="lyric-vol-label"></span>
5101
+ </div>
5102
+ <div class="dtm-row">
5103
+ <span class="dtm-label">\u5B9A\u4F4D</span>
5104
+ <input type="range" class="dtm-range dtm-grow" data-dtm="lyric-pan" min="0" max="127" aria-label="\u6B4C\u5531\u306E\u30B9\u30C6\u30EC\u30AA\u5B9A\u4F4D\uFF08\u5DE6\u53F3\uFF09">
5105
+ <span class="dtm-label" data-dtm="lyric-pan-label"></span>
5106
+ </div>
5107
+ <textarea class="dtm-textarea" data-dtm="lyric-input" rows="2" placeholder="\u3072\u3089\u304C\u306A\u30FB\u30AB\u30BF\u30AB\u30CA\u3067\u6B4C\u8A5E\uFF08\u4F8B: \u3069\u308C\u307F\u3075\u3041\u305D\u3089\u3057\u3069\uFF09"></textarea>
5108
+ </div>`;
5109
+ refs.trackBody.appendChild(lyricDiv);
5110
+ const lyricModelSel = lyricDiv.querySelector(
5111
+ '[data-dtm="lyric-model"]'
5112
+ );
5113
+ const lyricOctaveSel = lyricDiv.querySelector(
5114
+ '[data-dtm="lyric-octave"]'
5115
+ );
5116
+ const lyricIcon = lyricDiv.querySelector(
5117
+ '[data-dtm="lyric-icon"]'
5118
+ );
5119
+ const lyricBody = lyricDiv.querySelector(
5120
+ '[data-dtm="lyric-body"]'
5121
+ );
5122
+ const lyricInput = lyricDiv.querySelector(
5123
+ '[data-dtm="lyric-input"]'
5124
+ );
5125
+ const lyricCount = lyricDiv.querySelector(
5126
+ '[data-dtm="lyric-count"]'
5127
+ );
5128
+ const lyricVol = lyricDiv.querySelector(
5129
+ '[data-dtm="lyric-vol"]'
5130
+ );
5131
+ const lyricVolLabel = lyricDiv.querySelector(
5132
+ '[data-dtm="lyric-vol-label"]'
5133
+ );
5134
+ const lyricPan = lyricDiv.querySelector(
5135
+ '[data-dtm="lyric-pan"]'
5136
+ );
5137
+ const lyricPanLabel = lyricDiv.querySelector(
5138
+ '[data-dtm="lyric-pan-label"]'
5139
+ );
5140
+ const lyricTerms = lyricDiv.querySelector(
5141
+ '[data-dtm="lyric-terms"]'
5142
+ );
5143
+ const lyricTermsLink = lyricDiv.querySelector(
5144
+ '[data-dtm="lyric-terms-link"]'
5145
+ );
5146
+ const fmtPan = (pan) => pan === 64 ? "C" : pan < 64 ? `L${64 - pan}` : `R${pan - 64}`;
5147
+ const addOpt = (value, label) => {
5148
+ const o = document.createElement("option");
5149
+ o.value = value;
5150
+ o.textContent = label;
5151
+ lyricModelSel.appendChild(o);
5152
+ };
5153
+ addOpt("", "\u306A\u3057");
5154
+ for (const m of LYRIC_MODELS) addOpt(m, lyricModelLabel(m));
5155
+ if (active.lyricModel && !LYRIC_MODELS.includes(active.lyricModel)) {
5156
+ addOpt(active.lyricModel, lyricModelLabel(active.lyricModel));
5157
+ }
5158
+ lyricModelSel.value = active.lyricModel;
5159
+ lyricOctaveSel.value = String(active.vocalOctave);
5160
+ lyricInput.value = active.lyrics;
5161
+ lyricVol.value = String(active.vocalVolume);
5162
+ lyricVolLabel.textContent = String(active.vocalVolume);
5163
+ lyricPan.value = String(active.vocalPan);
5164
+ lyricPanLabel.textContent = fmtPan(active.vocalPan);
5165
+ const updateLyricCount = () => {
5166
+ const n = normalizeLyrics(lyricInput.value).length;
5167
+ lyricCount.textContent = active.lyricModel && n > 0 ? `${n}\u97F3\u7BC0` : "";
5168
+ };
5169
+ const syncLyricTerms = () => {
5170
+ const url = active.lyricModel ? KOE_VOICEBANK_TERMS[active.lyricModel] : void 0;
5171
+ if (url) {
5172
+ const label = lyricModelLabel(active.lyricModel);
5173
+ lyricTermsLink.textContent = `${label}UTAU\u97F3\u6E90`;
5174
+ lyricTermsLink.href = url;
5175
+ lyricTerms.classList.remove("dtm-hidden");
5176
+ } else {
5177
+ lyricTerms.classList.add("dtm-hidden");
5178
+ }
5179
+ };
5180
+ const syncLyricIcon = () => {
5181
+ const imgKey = active.lyricModel ? VOICE_IMAGE_KEY[active.lyricModel.toLowerCase()] : void 0;
5182
+ const src = imgKey ? VOICE_IMAGES[imgKey] : void 0;
5183
+ if (src) {
5184
+ lyricIcon.src = src;
5185
+ lyricIcon.classList.remove("dtm-hidden");
5186
+ } else {
5187
+ lyricIcon.removeAttribute("src");
5188
+ lyricIcon.classList.add("dtm-hidden");
5189
+ }
5190
+ };
5191
+ const syncLyricVisibility = () => {
5192
+ lyricBody.style.display = active.lyricModel ? "" : "none";
5193
+ lyricOctaveSel.style.display = active.lyricModel ? "" : "none";
5194
+ updateLyricCount();
5195
+ syncLyricTerms();
5196
+ syncLyricIcon();
5197
+ };
5198
+ syncLyricVisibility();
5199
+ lyricModelSel.addEventListener("change", () => {
5200
+ active.lyricModel = lyricModelSel.value;
5201
+ syncLyricVisibility();
5202
+ });
5203
+ lyricOctaveSel.addEventListener("change", () => {
5204
+ active.vocalOctave = Number.parseInt(lyricOctaveSel.value, 10);
5205
+ });
5206
+ lyricInput.addEventListener("input", () => {
5207
+ active.lyrics = lyricInput.value;
5208
+ updateLyricCount();
5209
+ });
5210
+ lyricVol.addEventListener("input", () => {
5211
+ active.vocalVolume = Number.parseInt(lyricVol.value, 10);
5212
+ lyricVolLabel.textContent = lyricVol.value;
5213
+ });
5214
+ lyricPan.addEventListener("input", () => {
5215
+ active.vocalPan = Number.parseInt(lyricPan.value, 10);
5216
+ lyricPanLabel.textContent = fmtPan(active.vocalPan);
5217
+ });
5218
+ lyricPanLabel.style.cursor = "pointer";
5219
+ lyricPanLabel.title = "\u30BF\u30C3\u30D7\u3067\u4E2D\u592E(C)\u3078";
5220
+ lyricPanLabel.addEventListener("click", () => {
5221
+ active.vocalPan = 64;
5222
+ lyricPan.value = "64";
5223
+ lyricPanLabel.textContent = fmtPan(64);
5224
+ });
3514
5225
  if (active.config.id === "chord" && showChord) {
3515
5226
  const div = document.createElement("div");
3516
5227
  div.className = "dtm-row";
@@ -3598,6 +5309,11 @@ var mountDAW = (target, options = {}) => {
3598
5309
  const barLimitBars = Number(refs.barLimitSelect.value);
3599
5310
  const limitSteps = barLimitBars > 0 ? barLimitBars * renderConfig.stepsPerBar : Infinity;
3600
5311
  const clipNotes = (notes) => limitSteps === Infinity ? notes : notes.filter((n) => n.startStep < limitSteps);
5312
+ const metaLine = formatMmlMeta({
5313
+ instrument: currentInstrument || void 0,
5314
+ drum: currentDrumPattern !== "none" ? currentDrumPattern : void 0,
5315
+ volume: masterVolume
5316
+ });
3601
5317
  if (refs.decomposeChordToggle.checked) {
3602
5318
  const ignoreHeavy = refs.ignoreChordHeavyToggle.checked;
3603
5319
  const targetStates = ignoreHeavy ? trackStates.filter((t) => !isChordHeavyTrack(t.core.getNotes())) : trackStates;
@@ -3607,12 +5323,14 @@ var mountDAW = (target, options = {}) => {
3607
5323
  );
3608
5324
  const monoTracks = decomposeToMonophonic(allNotes);
3609
5325
  const refCore = trackStates[0].core;
3610
- const full2 = monoTracks.map(
5326
+ const decomposedFull = monoTracks.map(
3611
5327
  (notes, i) => `@${i} ${refCore.getMMLFromNotes(notes, bpm, 100).trim()}`
3612
- ).join(";\n");
3613
- const minified2 = monoTracks.map(
5328
+ );
5329
+ const decomposedMini = monoTracks.map(
3614
5330
  (notes, i) => `@${i}${refCore.getMMLFromNotes(notes, bpm, 100).trim().replace(/\s+/g, "")}`
3615
- ).join(";");
5331
+ );
5332
+ const full2 = [metaLine, ...decomposedFull].filter((s) => s.length > 0).join(";\n");
5333
+ const minified2 = [metaLine, ...decomposedMini].filter((s) => s.length > 0).join(";");
3616
5334
  return {
3617
5335
  full: full2,
3618
5336
  minified: minified2,
@@ -3621,12 +5339,32 @@ var mountDAW = (target, options = {}) => {
3621
5339
  barLimit: barLimitBars
3622
5340
  };
3623
5341
  }
3624
- const full = trackStates.map(
5342
+ const trackLines = trackStates.map(
3625
5343
  (t, i) => `@${i} ${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim()}`
3626
- ).join(";\n");
3627
- const minified = trackStates.map(
5344
+ );
5345
+ const trackLinesMini = trackStates.map(
3628
5346
  (t, i) => `@${i}${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim().replace(/\s+/g, "")}`
3629
- ).join(";");
5347
+ );
5348
+ const lyricLines = trackStates.map((t, i) => ({
5349
+ i,
5350
+ text: t.lyrics.trim(),
5351
+ model: t.lyricModel.trim(),
5352
+ vol: t.vocalVolume,
5353
+ gate: t.vocalGate,
5354
+ pan: t.vocalPan,
5355
+ oct: t.vocalOctave
5356
+ })).filter((x) => x.model.length > 0 && x.text.length > 0).map((x) => {
5357
+ const params = [
5358
+ x.vol === DEFAULT_VOCAL_VOLUME ? "" : `v${x.vol}`,
5359
+ x.gate === 100 ? "" : `q${x.gate}`,
5360
+ x.pan === 64 ? "" : `p${x.pan}`,
5361
+ x.oct === 0 ? "" : `o${x.oct}`
5362
+ ].filter((s) => s.length > 0).join(" ");
5363
+ const head = params ? `${x.model} ${params}` : x.model;
5364
+ return `@@${x.i} ${head} ${x.text}`;
5365
+ });
5366
+ const full = [metaLine, ...trackLines, ...lyricLines].filter((s) => s.length > 0).join(";\n");
5367
+ const minified = [metaLine, ...trackLinesMini, ...lyricLines].filter((s) => s.length > 0).join(";");
3630
5368
  return {
3631
5369
  full,
3632
5370
  minified,
@@ -3647,6 +5385,34 @@ var mountDAW = (target, options = {}) => {
3647
5385
  refs.outputContainer.classList.remove("dtm-hidden");
3648
5386
  updateUndoRedo();
3649
5387
  };
5388
+ const getFirstDetectedPitch = () => {
5389
+ let minStep = Number.MAX_SAFE_INTEGER;
5390
+ let candidateNotes = [];
5391
+ for (const t of trackStates) {
5392
+ for (const note of t.core.getNotes()) {
5393
+ if (note.startStep < minStep) {
5394
+ minStep = note.startStep;
5395
+ candidateNotes = [note];
5396
+ } else if (note.startStep === minStep) {
5397
+ candidateNotes.push(note);
5398
+ }
5399
+ }
5400
+ }
5401
+ if (candidateNotes.length === 0) return null;
5402
+ const sum = candidateNotes.reduce((acc, note) => acc + note.pitch, 0);
5403
+ return Math.round(sum / candidateNotes.length);
5404
+ };
5405
+ const centerPitch = (pitch) => {
5406
+ const canvas = getGridCanvas();
5407
+ const yIndex = renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart);
5408
+ const logicalY = yIndex * renderConfig.keyHeight;
5409
+ currentOffsetY = clamp3(
5410
+ logicalY - (canvas.height - renderConfig.keyHeight) / 2,
5411
+ 0,
5412
+ getMaxOffsetY()
5413
+ );
5414
+ setDrawOffset(currentOffsetX, currentOffsetY);
5415
+ };
3650
5416
  const clearAll = () => {
3651
5417
  for (const t of trackStates) {
3652
5418
  t.core.resetHistory();
@@ -3658,8 +5424,45 @@ var mountDAW = (target, options = {}) => {
3658
5424
  if (!mml) return;
3659
5425
  clearAll();
3660
5426
  for (const t of trackStates) t.core.setLoadMode(true);
3661
- const { placements, bpm: parsedBpm } = parseMML(mml, {
3662
- stepsPerBar: renderConfig.stepsPerBar
5427
+ const {
5428
+ placements,
5429
+ bpm: parsedBpm,
5430
+ lyrics,
5431
+ meta
5432
+ } = parseMML(mml, {
5433
+ stepsPerBar: renderConfig.stepsPerBar,
5434
+ collectLyrics: true,
5435
+ // このDAWのトラック数を超えるチャンネルはベースへ畳み込む(従来挙動)
5436
+ clampTrackCount: trackStates.length
5437
+ });
5438
+ currentInstrument = meta.instrument ?? "";
5439
+ if (meta.drum && drumPatterns[meta.drum]) {
5440
+ currentDrumPattern = meta.drum;
5441
+ refs.drumSelect.value = meta.drum;
5442
+ options.onDrumChange?.(meta.drum);
5443
+ }
5444
+ if (meta.volume !== void 0) {
5445
+ masterVolume = meta.volume;
5446
+ refs.masterVolume.value = String(meta.volume);
5447
+ refs.masterVolumeLabel.textContent = `${meta.volume}%`;
5448
+ }
5449
+ for (const t of trackStates) {
5450
+ t.lyrics = "";
5451
+ t.lyricModel = "";
5452
+ t.vocalVolume = DEFAULT_VOCAL_VOLUME;
5453
+ t.vocalGate = 100;
5454
+ t.vocalPan = 64;
5455
+ t.vocalOctave = 0;
5456
+ }
5457
+ lyrics?.forEach((lt, idx) => {
5458
+ const t = trackStates[idx];
5459
+ if (!t) return;
5460
+ t.lyrics = lt.syllables.map((s) => s.kana).join("");
5461
+ t.lyricModel = lt.model;
5462
+ t.vocalVolume = lt.volume;
5463
+ t.vocalGate = lt.gate;
5464
+ t.vocalPan = lt.pan;
5465
+ t.vocalOctave = lt.octave ?? 0;
3663
5466
  });
3664
5467
  for (const p of placements) {
3665
5468
  const t = trackStates[p.trackIndex];
@@ -3675,8 +5478,14 @@ var mountDAW = (target, options = {}) => {
3675
5478
  }
3676
5479
  playStartStep = 0;
3677
5480
  currentOffsetX = 0;
3678
- setDrawOffset(currentOffsetX, currentOffsetY);
5481
+ const firstPitch = getFirstDetectedPitch();
5482
+ if (firstPitch !== null) {
5483
+ centerPitch(firstPitch);
5484
+ } else {
5485
+ setDrawOffset(currentOffsetX, currentOffsetY);
5486
+ }
3679
5487
  redrawAll();
5488
+ updateTrackPanel();
3680
5489
  updateUndoRedo();
3681
5490
  };
3682
5491
  const applyChord = () => {
@@ -3736,7 +5545,12 @@ var mountDAW = (target, options = {}) => {
3736
5545
  }
3737
5546
  playStartStep = 0;
3738
5547
  currentOffsetX = 0;
3739
- setDrawOffset(currentOffsetX, currentOffsetY);
5548
+ const firstPitch = getFirstDetectedPitch();
5549
+ if (firstPitch !== null) {
5550
+ centerPitch(firstPitch);
5551
+ } else {
5552
+ setDrawOffset(currentOffsetX, currentOffsetY);
5553
+ }
3740
5554
  redrawAll();
3741
5555
  updateUndoRedo();
3742
5556
  };
@@ -3800,25 +5614,32 @@ var mountDAW = (target, options = {}) => {
3800
5614
  refs.zoomXIn.addEventListener("click", () => {
3801
5615
  zoomX = Math.min(200, zoomX + 25);
3802
5616
  applyZoomX();
5617
+ notifyViewState();
3803
5618
  });
3804
5619
  refs.zoomXOut.addEventListener("click", () => {
3805
5620
  zoomX = Math.max(25, zoomX - 25);
3806
5621
  applyZoomX();
5622
+ notifyViewState();
3807
5623
  });
3808
5624
  refs.zoomYIn.addEventListener("click", () => {
3809
5625
  zoomY = Math.min(200, zoomY + 25);
3810
5626
  applyZoomY();
5627
+ notifyViewState();
3811
5628
  });
3812
5629
  refs.zoomYOut.addEventListener("click", () => {
3813
5630
  zoomY = Math.max(50, zoomY - 25);
3814
5631
  applyZoomY();
5632
+ notifyViewState();
3815
5633
  });
5634
+ refs.decomposeChordToggle.addEventListener("change", notifyViewState);
5635
+ refs.ignoreChordHeavyToggle.addEventListener("change", notifyViewState);
3816
5636
  refs.masterVolume.addEventListener("input", () => {
3817
5637
  masterVolume = Number.parseInt(refs.masterVolume.value, 10) || 0;
3818
5638
  refs.masterVolumeLabel.textContent = `${masterVolume}%`;
3819
5639
  });
3820
5640
  refs.drumSelect.addEventListener("change", () => {
3821
5641
  currentDrumPattern = refs.drumSelect.value;
5642
+ options.onDrumChange?.(currentDrumPattern);
3822
5643
  });
3823
5644
  refs.drumVolume.addEventListener("input", () => {
3824
5645
  drumVolume = Number.parseInt(refs.drumVolume.value, 10) || 0;
@@ -4008,6 +5829,33 @@ var mountDAW = (target, options = {}) => {
4008
5829
  pause,
4009
5830
  stop,
4010
5831
  getMML: generateMML,
5832
+ setInstrument: (name) => {
5833
+ currentInstrument = name;
5834
+ },
5835
+ getDrum: () => currentDrumPattern,
5836
+ setDrum: (name) => {
5837
+ if (name !== "none" && !drumPatterns[name]) return;
5838
+ currentDrumPattern = name;
5839
+ refs.drumSelect.value = name;
5840
+ options.onDrumChange?.(name);
5841
+ },
5842
+ getViewState,
5843
+ setViewState: (state) => {
5844
+ if (typeof state.zoomX === "number") {
5845
+ zoomX = clamp3(state.zoomX, 25, 200);
5846
+ applyZoomX();
5847
+ }
5848
+ if (typeof state.zoomY === "number") {
5849
+ zoomY = clamp3(state.zoomY, 50, 200);
5850
+ applyZoomY();
5851
+ }
5852
+ if (typeof state.decomposeChord === "boolean") {
5853
+ refs.decomposeChordToggle.checked = state.decomposeChord;
5854
+ }
5855
+ if (typeof state.ignoreChordHeavy === "boolean") {
5856
+ refs.ignoreChordHeavyToggle.checked = state.ignoreChordHeavy;
5857
+ }
5858
+ },
4011
5859
  loadMML,
4012
5860
  loadMIDI,
4013
5861
  exportMIDI: exportMIDI2,
@@ -4128,6 +5976,529 @@ var INSTRUMENT_PRESETS = {
4128
5976
  }
4129
5977
  };
4130
5978
 
5979
+ // src/mml-player.ts
5980
+ var STEPS_PER_BEAT3 = 48;
5981
+ var STEPS_PER_BAR = 192;
5982
+ var DEFAULT_TRACK_COLORS = ["#00e436", "#29adff", "#ff77a8", "#ffec27"];
5983
+ var activePlayer = null;
5984
+ var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
5985
+ var mountMmlPlayer = (target, mml, options = {}) => {
5986
+ injectStyles(target.ownerDocument ?? document);
5987
+ const {
5988
+ placements,
5989
+ bpm: parsedBpm,
5990
+ tokenTracks,
5991
+ lyrics,
5992
+ meta
5993
+ } = parseMML(mml, {
5994
+ collectTokens: true,
5995
+ collectLyrics: true
5996
+ });
5997
+ const lyricTracks = lyrics ?? /* @__PURE__ */ new Map();
5998
+ const bpm = parsedBpm ?? options.defaultBpm ?? DEFAULT_BPM;
5999
+ const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
6000
+ const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
6001
+ const trackVolume = meta.volume ?? options.volume ?? 100;
6002
+ const colors = options.trackColors ?? DEFAULT_TRACK_COLORS;
6003
+ const useSynth = options.synth ?? !options.onPlayNote;
6004
+ const secondsPerStep = 60 / bpm / STEPS_PER_BEAT3;
6005
+ const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
6006
+ (a, b) => a - b
6007
+ );
6008
+ const seqTracks = trackIndices.map((index) => {
6009
+ let id = 0;
6010
+ const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
6011
+ id: id++,
6012
+ startStep: p.startStep,
6013
+ durationSteps: p.durationSteps,
6014
+ pitch: p.pitch,
6015
+ velocity: 100
6016
+ }));
6017
+ return { id: String(index), volume: trackVolume, notes };
6018
+ });
6019
+ const colorOf = (index) => colors[index % colors.length] ?? DEFAULT_TRACK_COLORS[0];
6020
+ let audioCtx = null;
6021
+ const ensureCtx = () => {
6022
+ if (!audioCtx) audioCtx = new AudioContext();
6023
+ return audioCtx;
6024
+ };
6025
+ const synthPlay = (e) => {
6026
+ const ctx = ensureCtx();
6027
+ const osc = ctx.createOscillator();
6028
+ const gain = ctx.createGain();
6029
+ osc.type = "square";
6030
+ osc.frequency.value = freqFromPitch(e.pitch);
6031
+ const t0 = ctx.currentTime + e.when;
6032
+ const peak = Math.max(1e-4, 0.06 * e.volume * 1.5);
6033
+ gain.gain.setValueAtTime(peak, t0);
6034
+ gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
6035
+ osc.connect(gain);
6036
+ if (typeof ctx.createStereoPanner === "function" && e.pan) {
6037
+ const panner = ctx.createStereoPanner();
6038
+ panner.pan.value = Math.max(-1, Math.min(1, e.pan));
6039
+ gain.connect(panner);
6040
+ panner.connect(ctx.destination);
6041
+ } else {
6042
+ gain.connect(ctx.destination);
6043
+ }
6044
+ osc.start(t0);
6045
+ osc.stop(t0 + e.duration + 0.02);
6046
+ };
6047
+ const drumSynth = (e) => {
6048
+ const ctx = ensureCtx();
6049
+ const t0 = ctx.currentTime + e.when;
6050
+ const vol = Math.max(1e-4, Math.min(1, e.velocity));
6051
+ const isKick = e.pitch === 35 || e.pitch === 36;
6052
+ const isSnareLike = e.pitch === 38 || e.pitch === 39 || e.pitch === 40;
6053
+ if (isKick) {
6054
+ const osc = ctx.createOscillator();
6055
+ const g2 = ctx.createGain();
6056
+ osc.frequency.setValueAtTime(150, t0);
6057
+ osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
6058
+ g2.gain.setValueAtTime(vol * 0.9, t0);
6059
+ g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
6060
+ osc.connect(g2).connect(ctx.destination);
6061
+ osc.start(t0);
6062
+ osc.stop(t0 + 0.2);
6063
+ osc.onended = () => osc.disconnect();
6064
+ return;
6065
+ }
6066
+ const dur = isSnareLike ? 0.18 : 0.05;
6067
+ const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
6068
+ const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
6069
+ const data = buffer.getChannelData(0);
6070
+ for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
6071
+ const src = ctx.createBufferSource();
6072
+ src.buffer = buffer;
6073
+ const filter = ctx.createBiquadFilter();
6074
+ filter.type = isSnareLike ? "bandpass" : "highpass";
6075
+ filter.frequency.value = isSnareLike ? 2e3 : 8e3;
6076
+ const g = ctx.createGain();
6077
+ g.gain.setValueAtTime(vol * (isSnareLike ? 0.7 : 0.4), t0);
6078
+ g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
6079
+ src.connect(filter).connect(g).connect(ctx.destination);
6080
+ src.start(t0);
6081
+ src.stop(t0 + dur);
6082
+ src.onended = () => {
6083
+ src.disconnect();
6084
+ filter.disconnect();
6085
+ g.disconnect();
6086
+ };
6087
+ };
6088
+ let voices = null;
6089
+ const ensureVoices = () => {
6090
+ if (options.singingVoices) return options.singingVoices;
6091
+ if (!voices) {
6092
+ const ctx = ensureCtx();
6093
+ voices = createSingingVoices(ctx, ctx.destination);
6094
+ }
6095
+ return voices;
6096
+ };
6097
+ const voicesAvailable = useSynth || !!options.singingVoices;
6098
+ const peekVoices = () => options.singingVoices ?? voices;
6099
+ const getAudioTime = () => {
6100
+ if (useSynth) return ensureCtx().currentTime;
6101
+ return options.getAudioTime?.() ?? performance.now() / 1e3;
6102
+ };
6103
+ const doc = target.ownerDocument ?? document;
6104
+ const root = doc.createElement("div");
6105
+ root.className = "dtm-daw dtm-player";
6106
+ const head = doc.createElement("div");
6107
+ head.className = "dtm-player-head";
6108
+ const playBtn = doc.createElement("button");
6109
+ playBtn.type = "button";
6110
+ playBtn.className = "dtm-player-play";
6111
+ playBtn.innerHTML = icon("play", 12);
6112
+ playBtn.disabled = trackIndices.length === 0;
6113
+ const mmlHeader = doc.createElement("div");
6114
+ mmlHeader.className = "dtm-player-mml-header";
6115
+ const emojiEls = [];
6116
+ const emojiByTrack = /* @__PURE__ */ new Map();
6117
+ for (const index of trackIndices) {
6118
+ const em = doc.createElement("span");
6119
+ em.className = "dtm-player-emoji";
6120
+ em.style.backgroundColor = colorOf(index);
6121
+ em.textContent = "\u{1F97A}";
6122
+ mmlHeader.appendChild(em);
6123
+ emojiEls.push(em);
6124
+ emojiByTrack.set(index, em);
6125
+ }
6126
+ const promotedToImage = /* @__PURE__ */ new Set();
6127
+ for (const [index, lt] of lyricTracks) {
6128
+ const em = emojiByTrack.get(index);
6129
+ if (!em) continue;
6130
+ const imgKey = VOICE_IMAGE_KEY[lt.model.toLowerCase()];
6131
+ const src = imgKey ? VOICE_IMAGES[imgKey] : void 0;
6132
+ if (!src) continue;
6133
+ const img = doc.createElement("img");
6134
+ img.src = src;
6135
+ img.width = 20;
6136
+ img.height = 20;
6137
+ img.style.borderRadius = "50%";
6138
+ img.style.objectFit = "cover";
6139
+ img.draggable = false;
6140
+ promotedToImage.add(em);
6141
+ em.textContent = "";
6142
+ em.appendChild(img);
6143
+ }
6144
+ const JUMP_DEDUPE_MS = 50;
6145
+ const lastJumpAt = /* @__PURE__ */ new WeakMap();
6146
+ const jumpEmoji = (em) => {
6147
+ const now = performance.now();
6148
+ const prev = lastJumpAt.get(em);
6149
+ if (prev !== void 0 && now - prev < JUMP_DEDUPE_MS) return;
6150
+ lastJumpAt.set(em, now);
6151
+ em.classList.remove("dtm-player-emoji--jump");
6152
+ void em.offsetWidth;
6153
+ em.classList.add("dtm-player-emoji--jump");
6154
+ };
6155
+ const jumpTimers = [];
6156
+ const jumpEmojiAt = (em, when) => {
6157
+ if (when <= 0) {
6158
+ jumpEmoji(em);
6159
+ return;
6160
+ }
6161
+ jumpTimers.push(setTimeout(() => jumpEmoji(em), when * 1e3));
6162
+ };
6163
+ const clearJumpTimers = () => {
6164
+ for (const t of jumpTimers) clearTimeout(t);
6165
+ jumpTimers.length = 0;
6166
+ };
6167
+ const blinkTimers = [];
6168
+ const scheduleBlink = (em) => {
6169
+ const delay = 2e3 + Math.random() * 5e3;
6170
+ const t = setTimeout(() => {
6171
+ if (promotedToImage.has(em)) return;
6172
+ em.textContent = "\u{1F60C}";
6173
+ const t2 = setTimeout(
6174
+ () => {
6175
+ if (promotedToImage.has(em)) return;
6176
+ em.textContent = "\u{1F97A}";
6177
+ scheduleBlink(em);
6178
+ },
6179
+ 200 + Math.random() * 150
6180
+ );
6181
+ blinkTimers.push(t2);
6182
+ }, delay);
6183
+ blinkTimers.push(t);
6184
+ };
6185
+ for (const em of emojiEls) scheduleBlink(em);
6186
+ const dots = doc.createElement("div");
6187
+ dots.className = "dtm-player-dots";
6188
+ dots.style.display = "none";
6189
+ for (const index of trackIndices) {
6190
+ const dot = doc.createElement("span");
6191
+ dot.className = "dtm-player-dot";
6192
+ dot.style.backgroundColor = colorOf(index);
6193
+ dots.appendChild(dot);
6194
+ }
6195
+ const beatRow = doc.createElement("div");
6196
+ beatRow.className = "dtm-player-beat-row";
6197
+ const beatDots = [];
6198
+ for (let i = 0; i < 4; i++) {
6199
+ const d = doc.createElement("span");
6200
+ d.className = "dtm-player-beat-dot";
6201
+ beatRow.appendChild(d);
6202
+ beatDots.push(d);
6203
+ }
6204
+ const barEl = doc.createElement("span");
6205
+ barEl.className = "dtm-player-bar";
6206
+ barEl.textContent = "-";
6207
+ beatRow.appendChild(barEl);
6208
+ const chips = [];
6209
+ const makeChip = (label) => {
6210
+ const chip = doc.createElement("span");
6211
+ chip.className = "dtm-player-chip";
6212
+ chip.textContent = label;
6213
+ chips.push(chip);
6214
+ return chip;
6215
+ };
6216
+ if (meta.instrument) makeChip(`\u266A ${meta.instrument}`);
6217
+ if (meta.drum) makeChip(`\u{1F941} ${meta.drum}${drumPattern ? "" : " (?)"}`);
6218
+ if (meta.volume !== void 0) makeChip(`\u{1F50A} ${meta.volume}%`);
6219
+ head.append(playBtn, beatRow, ...chips, dots, mmlHeader);
6220
+ root.appendChild(head);
6221
+ const body = doc.createElement("div");
6222
+ body.className = "dtm-player-body";
6223
+ root.appendChild(body);
6224
+ const mutedTracks = /* @__PURE__ */ new Set();
6225
+ const laneViews = [];
6226
+ for (const index of trackIndices) {
6227
+ const lyricTrack = lyricTracks.get(index);
6228
+ const isLyricLane = !!lyricTrack && lyricTrack.syllables.length > 0;
6229
+ const row = doc.createElement("div");
6230
+ row.className = "dtm-player-lane-row";
6231
+ const label = doc.createElement("div");
6232
+ label.className = "dtm-player-lane-label dtm-player-lane-label--btn";
6233
+ const swatch = doc.createElement("span");
6234
+ swatch.className = "dtm-player-dot";
6235
+ swatch.style.backgroundColor = colorOf(index);
6236
+ const no = doc.createElement("span");
6237
+ no.className = "dtm-player-lane-no";
6238
+ no.textContent = `@${index}`;
6239
+ label.append(swatch, no);
6240
+ label.addEventListener("click", () => {
6241
+ if (mutedTracks.has(index)) {
6242
+ mutedTracks.delete(index);
6243
+ label.classList.remove("dtm-player-lane-label--muted");
6244
+ } else {
6245
+ mutedTracks.add(index);
6246
+ label.classList.add("dtm-player-lane-label--muted");
6247
+ }
6248
+ });
6249
+ const lane = doc.createElement("div");
6250
+ lane.className = "dtm-player-lane";
6251
+ lane.style.setProperty("--tk", colorOf(index));
6252
+ const laneTokens = [];
6253
+ if (isLyricLane) {
6254
+ const notes = placements.filter((p) => p.trackIndex === index).sort((a, b) => a.startStep - b.startStep);
6255
+ const gateScale = (lyricTrack.gate ?? DEFAULT_GATE) / 100;
6256
+ const breaks = new Set(lyricTrack.lineBreaks ?? []);
6257
+ if (lyricTrack.metaText) {
6258
+ const metaEl = doc.createElement("span");
6259
+ metaEl.className = "dtm-tk dtm-tk--meta";
6260
+ metaEl.textContent = lyricTrack.metaText;
6261
+ lane.appendChild(metaEl);
6262
+ }
6263
+ const count = Math.min(notes.length, lyricTrack.syllables.length);
6264
+ for (let i = 0; i < count; i++) {
6265
+ const note = notes[i];
6266
+ if (breaks.has(i)) {
6267
+ const br = doc.createElement("span");
6268
+ br.className = "dtm-tk dtm-tk--break";
6269
+ br.textContent = "\\n";
6270
+ lane.appendChild(br);
6271
+ }
6272
+ const span = doc.createElement("span");
6273
+ span.className = "dtm-tk dtm-tk--lyric";
6274
+ span.textContent = lyricTrack.syllables[i].kana;
6275
+ lane.appendChild(span);
6276
+ laneTokens.push({
6277
+ el: span,
6278
+ startStep: note.startStep,
6279
+ durationSteps: Math.max(
6280
+ 1,
6281
+ Math.round(note.durationSteps * gateScale)
6282
+ )
6283
+ });
6284
+ }
6285
+ } else {
6286
+ const tokens = tokenTracks?.get(index) ?? [];
6287
+ for (const tok of tokens) {
6288
+ const span = doc.createElement("span");
6289
+ span.className = `dtm-tk dtm-tk--${tok.type}`;
6290
+ span.textContent = tok.text;
6291
+ lane.appendChild(span);
6292
+ if (tok.durationSteps > 0) {
6293
+ laneTokens.push({
6294
+ el: span,
6295
+ startStep: tok.startStep,
6296
+ durationSteps: tok.durationSteps
6297
+ });
6298
+ }
6299
+ }
6300
+ }
6301
+ row.append(label, lane);
6302
+ body.appendChild(row);
6303
+ laneViews.push({ lane, tokens: laneTokens });
6304
+ }
6305
+ const termsModels = [
6306
+ ...new Set([...lyricTracks.values()].map((lt) => lt.model))
6307
+ ].filter((model) => KOE_VOICEBANK_TERMS[model]);
6308
+ if (termsModels.length > 0) {
6309
+ const termsDiv = doc.createElement("div");
6310
+ termsDiv.className = "dtm-player-terms";
6311
+ termsDiv.style.fontSize = "10px";
6312
+ termsDiv.style.color = "var(--dtm-warn)";
6313
+ termsDiv.style.display = "flex";
6314
+ termsDiv.style.flexDirection = "column";
6315
+ termsDiv.style.gap = "4px";
6316
+ termsDiv.style.marginTop = "4px";
6317
+ termsDiv.style.padding = "0 4px";
6318
+ for (const model of termsModels) {
6319
+ const termsRow = doc.createElement("div");
6320
+ termsRow.style.display = "flex";
6321
+ termsRow.style.alignItems = "center";
6322
+ termsRow.style.gap = "4px";
6323
+ termsRow.style.flexWrap = "wrap";
6324
+ const label = KOE_VOICEBANK_LABELS[model] ?? model;
6325
+ const url = KOE_VOICEBANK_TERMS[model];
6326
+ const span1 = doc.createElement("span");
6327
+ span1.textContent = "\u4F7F\u7528\u6642\u306B\u306F";
6328
+ const a = doc.createElement("a");
6329
+ a.textContent = `${label}UTAU\u97F3\u6E90`;
6330
+ a.href = url;
6331
+ a.target = "_blank";
6332
+ a.rel = "noopener";
6333
+ a.style.color = "var(--dtm-primary)";
6334
+ a.style.textDecoration = "underline";
6335
+ const span2 = doc.createElement("span");
6336
+ span2.textContent = "\u306E\u5229\u7528\u898F\u7D04\u306B\u5F93\u3063\u3066\u304F\u3060\u3055\u3044";
6337
+ termsRow.append(span1, a, span2);
6338
+ termsDiv.appendChild(termsRow);
6339
+ }
6340
+ root.appendChild(termsDiv);
6341
+ }
6342
+ target.appendChild(root);
6343
+ const autoScroll = (lane, el) => {
6344
+ if (el.offsetWidth === 0 || lane.clientWidth === 0) return;
6345
+ const elementCenter = el.offsetLeft + el.offsetWidth / 2;
6346
+ const maxScroll = Math.max(0, lane.scrollWidth - lane.clientWidth);
6347
+ const next = elementCenter - lane.clientWidth / 2;
6348
+ lane.scrollLeft = Math.max(0, Math.min(next, maxScroll));
6349
+ };
6350
+ const renderPlayhead = (step) => {
6351
+ const beatIndex = Math.floor(step / STEPS_PER_BEAT3) % 4;
6352
+ for (let i = 0; i < 4; i++)
6353
+ beatDots[i].classList.toggle("dtm-player-beat-dot--on", i === beatIndex);
6354
+ barEl.textContent = String(Math.floor(step / STEPS_PER_BAR) + 1);
6355
+ for (const view of laneViews) {
6356
+ let active = null;
6357
+ for (const t of view.tokens) {
6358
+ const on = step >= t.startStep && step < t.startStep + t.durationSteps;
6359
+ t.el.classList.toggle("is-active", on);
6360
+ if (on && !active) active = t;
6361
+ }
6362
+ if (active) autoScroll(view.lane, active.el);
6363
+ }
6364
+ };
6365
+ const resetPlayhead = () => {
6366
+ for (const d of beatDots) d.classList.remove("dtm-player-beat-dot--on");
6367
+ barEl.textContent = "-";
6368
+ for (const view of laneViews) {
6369
+ for (const t of view.tokens) t.el.classList.remove("is-active");
6370
+ view.lane.scrollLeft = 0;
6371
+ }
6372
+ };
6373
+ const seq = createSequencer({
6374
+ getTracks: () => seqTracks,
6375
+ getBpm: () => bpm,
6376
+ getPlayStartStep: () => 0,
6377
+ getDrumPattern: () => drumPattern,
6378
+ getSoloTrackId: () => null,
6379
+ getAudioTime,
6380
+ onPlayNote: (e) => {
6381
+ const trackIdx = Number(e.trackId);
6382
+ if (mutedTracks.has(trackIdx)) return;
6383
+ const em = emojiByTrack.get(trackIdx);
6384
+ if (em) jumpEmojiAt(em, e.when);
6385
+ if (lyricTracks.has(trackIdx)) return;
6386
+ options.onPlayNote?.(e);
6387
+ if (useSynth) synthPlay(e);
6388
+ },
6389
+ onPlayDrum: (e) => {
6390
+ const em = emojiEls[0];
6391
+ if (em) jumpEmojiAt(em, e.when);
6392
+ const velocity = e.velocity * (trackVolume / 100);
6393
+ options.onPlayDrum?.({ ...e, velocity });
6394
+ if (useSynth) drumSynth({ ...e, velocity });
6395
+ },
6396
+ onTick: (step) => {
6397
+ renderPlayhead(step);
6398
+ },
6399
+ onEnd: () => finish(),
6400
+ stepsPerBar: STEPS_PER_BAR
6401
+ });
6402
+ let playing = false;
6403
+ const setPlayingUI = (on) => {
6404
+ playing = on;
6405
+ playBtn.innerHTML = icon(on ? "stop" : "play", 12);
6406
+ playBtn.classList.toggle("dtm-player-play--stop", on);
6407
+ };
6408
+ const finish = () => {
6409
+ setPlayingUI(false);
6410
+ clearJumpTimers();
6411
+ resetPlayhead();
6412
+ if (activePlayer === instance) activePlayer = null;
6413
+ };
6414
+ const buildStreamTracks = () => [...lyricTracks.entries()].map(([index, lt]) => {
6415
+ const seqTrack = seqTracks.find((t) => Number(t.id) === index);
6416
+ const sorted = [...seqTrack?.notes ?? []].sort(
6417
+ (a, b) => a.startStep - b.startStep
6418
+ );
6419
+ const gate = (lt.gate ?? DEFAULT_GATE) / 100;
6420
+ const semis = (lt.octave ?? 0) * 12;
6421
+ const count = Math.min(sorted.length, lt.syllables.length);
6422
+ const notes = [];
6423
+ for (let i = 0; i < count; i++) {
6424
+ const n = sorted[i];
6425
+ notes.push({
6426
+ syllable: lt.syllables[i],
6427
+ pitch: n.pitch + semis,
6428
+ startSec: n.startStep * secondsPerStep,
6429
+ durationSec: n.durationSteps * secondsPerStep * gate
6430
+ });
6431
+ }
6432
+ return {
6433
+ model: lt.model,
6434
+ volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (trackVolume / 100),
6435
+ pan: panToStereo(lt.pan ?? DEFAULT_PAN),
6436
+ notes
6437
+ };
6438
+ });
6439
+ const startWhenReady = async () => {
6440
+ const streaming = voicesAvailable && lyricTracks.size > 0;
6441
+ const tracks = streaming ? buildStreamTracks() : [];
6442
+ if (streaming) {
6443
+ const v = ensureVoices();
6444
+ const overlay = showLoadingOverlay(body);
6445
+ try {
6446
+ await v.loadModels(tracks.map((t) => t.model));
6447
+ await v.warm(tracks);
6448
+ } catch (err) {
6449
+ console.warn("[dtm] voice preload failed", err);
6450
+ } finally {
6451
+ overlay.remove();
6452
+ }
6453
+ if (!playing || activePlayer !== instance) return;
6454
+ }
6455
+ seq.start(0);
6456
+ if (streaming) ensureVoices().startStream(tracks, seq.getStartTime());
6457
+ };
6458
+ const play = () => {
6459
+ if (playing || trackIndices.length === 0) return;
6460
+ if (activePlayer && activePlayer !== instance) activePlayer.stop();
6461
+ activePlayer = instance;
6462
+ setPlayingUI(true);
6463
+ void options.onResumeAudio?.();
6464
+ if (useSynth) {
6465
+ const ctx = ensureCtx();
6466
+ if (ctx.state === "suspended") void ctx.resume();
6467
+ }
6468
+ if (voicesAvailable && lyricTracks.size > 0) ensureVoices().reset();
6469
+ void startWhenReady();
6470
+ };
6471
+ const stop = () => {
6472
+ if (!playing) return;
6473
+ seq.stop();
6474
+ peekVoices()?.stopStream();
6475
+ finish();
6476
+ };
6477
+ playBtn.addEventListener("click", () => {
6478
+ if (playing) stop();
6479
+ else play();
6480
+ });
6481
+ const destroy = () => {
6482
+ seq.stop();
6483
+ peekVoices()?.stopStream();
6484
+ if (activePlayer === instance) activePlayer = null;
6485
+ if (audioCtx) {
6486
+ void audioCtx.close();
6487
+ audioCtx = null;
6488
+ }
6489
+ for (const t of blinkTimers) clearTimeout(t);
6490
+ clearJumpTimers();
6491
+ root.remove();
6492
+ };
6493
+ const instance = {
6494
+ play,
6495
+ stop,
6496
+ isPlaying: () => playing,
6497
+ destroy
6498
+ };
6499
+ return instance;
6500
+ };
6501
+
4131
6502
  // src/piano-roll.ts
4132
6503
  var createPianoRoll = (options, handlers) => {
4133
6504
  const {
@@ -4412,23 +6783,43 @@ var createPianoRoll = (options, handlers) => {
4412
6783
  };
4413
6784
  export {
4414
6785
  DAW_CSS,
6786
+ DEFAULT_BPM,
6787
+ DEFAULT_GATE,
6788
+ DEFAULT_PAN,
6789
+ DEFAULT_PLAYBACK_VELOCITY,
6790
+ DEFAULT_STEPS_PER_BAR,
6791
+ DEFAULT_VELOCITY,
6792
+ DEFAULT_VOCAL_VOLUME,
4415
6793
  DRUM_FONT,
4416
6794
  DRUM_KEYS,
4417
6795
  DRUM_PATTERNS,
4418
6796
  INSTRUMENT_PRESETS,
6797
+ KOE_BASE_URL,
6798
+ KOE_VOICEBANKS,
6799
+ KOE_VOICEBANK_LABELS,
6800
+ KOE_VOICEBANK_TERMS,
4419
6801
  LinkedList,
6802
+ MAX_VOCAL_VOLUME,
4420
6803
  MMLCore,
4421
6804
  PITCH_MAP,
6805
+ PREWARM_NOTES,
4422
6806
  TRACKS_ADVANCED,
4423
6807
  TRACKS_SIMPLE,
6808
+ VOICE_IMAGE_KEY,
4424
6809
  analyzeMidiTracks,
4425
6810
  applyHarmonicFilter,
4426
6811
  applyMonophonic,
4427
6812
  buildChordPlacements,
4428
6813
  buildNameToKeyMapping,
6814
+ collectPitchTokens,
4429
6815
  createAudioContext,
6816
+ createKlattVoice,
6817
+ createKoeVoice,
6818
+ createLyricsConductor,
4430
6819
  createPianoRoll,
4431
6820
  createSequencer,
6821
+ createSingingVoices,
6822
+ createVoiceRegistry,
4432
6823
  decomposeToMonophonic,
4433
6824
  drawGrid,
4434
6825
  drawHeader,
@@ -4440,6 +6831,7 @@ export {
4440
6831
  extractMidiPlacements,
4441
6832
  extractMidiPlacementsByTrack,
4442
6833
  fetchSoundFontList,
6834
+ formatMmlMeta,
4443
6835
  generateRandomPattern,
4444
6836
  getDrawOffset,
4445
6837
  getGridCanvas,
@@ -4453,10 +6845,19 @@ export {
4453
6845
  init,
4454
6846
  injectStyles,
4455
6847
  isChordHeavyTrack,
6848
+ koeUrl,
4456
6849
  mountDAW,
6850
+ mountMmlPlayer,
6851
+ normalizeLyrics,
4457
6852
  onClick,
6853
+ panToStereo,
6854
+ parseLyrics,
4458
6855
  parseMML,
6856
+ parseMmlMeta,
4459
6857
  setDrawOffset,
4460
6858
  setupRecorder,
4461
- shiftNotes
6859
+ shiftNotes,
6860
+ stripLyrics,
6861
+ stripMmlMeta,
6862
+ vocalVolumeToGain
4462
6863
  };