@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.js CHANGED
@@ -21,23 +21,43 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  DAW_CSS: () => DAW_CSS,
24
+ DEFAULT_BPM: () => DEFAULT_BPM,
25
+ DEFAULT_GATE: () => DEFAULT_GATE,
26
+ DEFAULT_PAN: () => DEFAULT_PAN,
27
+ DEFAULT_PLAYBACK_VELOCITY: () => DEFAULT_PLAYBACK_VELOCITY,
28
+ DEFAULT_STEPS_PER_BAR: () => DEFAULT_STEPS_PER_BAR,
29
+ DEFAULT_VELOCITY: () => DEFAULT_VELOCITY,
30
+ DEFAULT_VOCAL_VOLUME: () => DEFAULT_VOCAL_VOLUME,
24
31
  DRUM_FONT: () => DRUM_FONT,
25
32
  DRUM_KEYS: () => DRUM_KEYS,
26
33
  DRUM_PATTERNS: () => DRUM_PATTERNS,
27
34
  INSTRUMENT_PRESETS: () => INSTRUMENT_PRESETS,
35
+ KOE_BASE_URL: () => KOE_BASE_URL,
36
+ KOE_VOICEBANKS: () => KOE_VOICEBANKS,
37
+ KOE_VOICEBANK_LABELS: () => KOE_VOICEBANK_LABELS,
38
+ KOE_VOICEBANK_TERMS: () => KOE_VOICEBANK_TERMS,
28
39
  LinkedList: () => LinkedList,
40
+ MAX_VOCAL_VOLUME: () => MAX_VOCAL_VOLUME,
29
41
  MMLCore: () => MMLCore,
30
42
  PITCH_MAP: () => PITCH_MAP,
43
+ PREWARM_NOTES: () => PREWARM_NOTES,
31
44
  TRACKS_ADVANCED: () => TRACKS_ADVANCED,
32
45
  TRACKS_SIMPLE: () => TRACKS_SIMPLE,
46
+ VOICE_IMAGE_KEY: () => VOICE_IMAGE_KEY,
33
47
  analyzeMidiTracks: () => analyzeMidiTracks,
34
48
  applyHarmonicFilter: () => applyHarmonicFilter,
35
49
  applyMonophonic: () => applyMonophonic,
36
50
  buildChordPlacements: () => buildChordPlacements,
37
51
  buildNameToKeyMapping: () => buildNameToKeyMapping,
52
+ collectPitchTokens: () => collectPitchTokens,
38
53
  createAudioContext: () => createAudioContext,
54
+ createKlattVoice: () => createKlattVoice,
55
+ createKoeVoice: () => createKoeVoice,
56
+ createLyricsConductor: () => createLyricsConductor,
39
57
  createPianoRoll: () => createPianoRoll,
40
58
  createSequencer: () => createSequencer,
59
+ createSingingVoices: () => createSingingVoices,
60
+ createVoiceRegistry: () => createVoiceRegistry,
41
61
  decomposeToMonophonic: () => decomposeToMonophonic,
42
62
  drawGrid: () => drawGrid,
43
63
  drawHeader: () => drawHeader,
@@ -49,6 +69,7 @@ __export(index_exports, {
49
69
  extractMidiPlacements: () => extractMidiPlacements,
50
70
  extractMidiPlacementsByTrack: () => extractMidiPlacementsByTrack,
51
71
  fetchSoundFontList: () => fetchSoundFontList,
72
+ formatMmlMeta: () => formatMmlMeta,
52
73
  generateRandomPattern: () => generateRandomPattern,
53
74
  getDrawOffset: () => getDrawOffset,
54
75
  getGridCanvas: () => getGridCanvas,
@@ -62,12 +83,21 @@ __export(index_exports, {
62
83
  init: () => init,
63
84
  injectStyles: () => injectStyles,
64
85
  isChordHeavyTrack: () => isChordHeavyTrack,
86
+ koeUrl: () => koeUrl,
65
87
  mountDAW: () => mountDAW,
88
+ mountMmlPlayer: () => mountMmlPlayer,
89
+ normalizeLyrics: () => normalizeLyrics,
66
90
  onClick: () => onClick,
91
+ panToStereo: () => panToStereo,
92
+ parseLyrics: () => parseLyrics,
67
93
  parseMML: () => parseMML,
94
+ parseMmlMeta: () => parseMmlMeta,
68
95
  setDrawOffset: () => setDrawOffset,
69
96
  setupRecorder: () => setupRecorder,
70
- shiftNotes: () => shiftNotes
97
+ shiftNotes: () => shiftNotes,
98
+ stripLyrics: () => stripLyrics,
99
+ stripMmlMeta: () => stripMmlMeta,
100
+ vocalVolumeToGain: () => vocalVolumeToGain
71
101
  });
72
102
  module.exports = __toCommonJS(index_exports);
73
103
 
@@ -263,7 +293,7 @@ var buildChordPlacements = (options) => {
263
293
  if (notes.length === 0) return;
264
294
  const startStep = barIndex * chordLength;
265
295
  notes.forEach((noteOffset, i) => {
266
- const stepOffset = i * 2;
296
+ const stepOffset = i * 3;
267
297
  placements.push({
268
298
  startStep: startStep + stepOffset,
269
299
  pitch: C3 + noteOffset + offset,
@@ -358,7 +388,10 @@ var buildUI = (target, options) => {
358
388
  <div class="dtm-tracks" data-dtm="track-tabs"></div>
359
389
 
360
390
  <div class="dtm-roll-wrap">
361
- <div class="dtm-roll" data-dtm="roll"><div data-dtm="wrapper" style="position:absolute;inset:0;"></div></div>
391
+ <div class="dtm-roll" data-dtm="roll">
392
+ <div data-dtm="wrapper" style="position:absolute;inset:0;"></div>
393
+ <div class="dtm-overlay" data-dtm="overlay" hidden><div class="dtm-spinner"></div></div>
394
+ </div>
362
395
  <div class="dtm-vscroll" data-dtm="vscroll"><div class="dtm-vscroll-thumb" data-dtm="vscroll-thumb"></div></div>
363
396
  </div>
364
397
  <div class="dtm-hscroll" data-dtm="hscroll"><div class="dtm-hscroll-thumb" data-dtm="hscroll-thumb"></div></div>
@@ -491,8 +524,6 @@ var buildUI = (target, options) => {
491
524
  </div>
492
525
  </div>
493
526
  </details>
494
-
495
- <div class="dtm-overlay" data-dtm="overlay" hidden><div class="dtm-spinner"></div></div>
496
527
  </div>`;
497
528
  const root = q(target, '[data-dtm="root"]');
498
529
  const sel = (name) => q(root, `[data-dtm="${name}"]`);
@@ -681,6 +712,1155 @@ var DRUM_PATTERNS = {
681
712
  ]
682
713
  };
683
714
 
715
+ // node_modules/.pnpm/@onjmin+koe@1.0.3/node_modules/@onjmin/koe/dist/index.js
716
+ var MAGIC = 1263486208;
717
+ function parseKoeHeader(headerBytes) {
718
+ const view = new DataView(headerBytes);
719
+ if (view.byteLength < 8 || view.getUint32(0, false) !== MAGIC) {
720
+ throw new Error("Not a .koe file (bad magic)");
721
+ }
722
+ return { jsonLength: view.getUint32(4, true) };
723
+ }
724
+ var pcmBase = (jsonLength) => 8 + jsonLength;
725
+ var BlobVoiceSource = class {
726
+ constructor(blob, base) {
727
+ this.blob = blob;
728
+ this.base = base;
729
+ }
730
+ blob;
731
+ base;
732
+ readBytes(offset, length) {
733
+ const start = this.base + offset;
734
+ return this.blob.slice(start, start + length).arrayBuffer();
735
+ }
736
+ };
737
+ var RangeVoiceSource = class {
738
+ constructor(url, base) {
739
+ this.url = url;
740
+ this.base = base;
741
+ }
742
+ url;
743
+ base;
744
+ async readBytes(offset, length) {
745
+ const start = this.base + offset;
746
+ const res = await fetch(this.url, {
747
+ headers: { Range: `bytes=${start}-${start + length - 1}` }
748
+ });
749
+ if (!res.ok && res.status !== 206) {
750
+ throw new Error(`.koe range request failed: ${res.status}`);
751
+ }
752
+ return res.arrayBuffer();
753
+ }
754
+ };
755
+ async function rangeFetch(url, start, length) {
756
+ const res = await fetch(url, {
757
+ headers: { Range: `bytes=${start}-${start + length - 1}` }
758
+ });
759
+ if (!res.ok && res.status !== 206)
760
+ throw new Error(`.koe fetch failed: ${res.status}`);
761
+ return res.arrayBuffer();
762
+ }
763
+ var VoiceBank = class _VoiceBank {
764
+ constructor(manifest, source) {
765
+ this.manifest = manifest;
766
+ this.source = source;
767
+ }
768
+ manifest;
769
+ source;
770
+ /**
771
+ * Parse a .koe archive header + manifest and bind a lazy PCM source.
772
+ * @param koe a Blob/File of the .koe archive, or a URL (served with Range support)
773
+ */
774
+ static async load(koe) {
775
+ if (typeof koe === "string") {
776
+ const header2 = await rangeFetch(koe, 0, 8);
777
+ const { jsonLength: jsonLength2 } = parseKoeHeader(header2);
778
+ const json2 = await rangeFetch(koe, 8, jsonLength2);
779
+ const manifest2 = JSON.parse(new TextDecoder().decode(json2));
780
+ return new _VoiceBank(
781
+ manifest2,
782
+ new RangeVoiceSource(koe, pcmBase(jsonLength2))
783
+ );
784
+ }
785
+ const header = await koe.slice(0, 8).arrayBuffer();
786
+ const { jsonLength } = parseKoeHeader(header);
787
+ const json = await koe.slice(8, 8 + jsonLength).arrayBuffer();
788
+ const manifest = JSON.parse(new TextDecoder().decode(json));
789
+ return new _VoiceBank(
790
+ manifest,
791
+ new BlobVoiceSource(koe, pcmBase(jsonLength))
792
+ );
793
+ }
794
+ /** True if the bank contains a phoneme under this alias. */
795
+ has(phoneme) {
796
+ return this.manifest.phonemes[phoneme] !== void 0;
797
+ }
798
+ /**
799
+ * Raw Int16 PCM bytes (48 kHz / mono) for a phoneme, or null if unknown.
800
+ * The returned ArrayBuffer is freshly allocated and safe to transfer to a
801
+ * worker / AudioWorklet.
802
+ */
803
+ async readPcmBytes(phoneme) {
804
+ const entry = this.manifest.phonemes[phoneme];
805
+ if (!entry) return null;
806
+ return this.source.readBytes(entry.offset, entry.length * 2);
807
+ }
808
+ /**
809
+ * A phoneme's PCM as a Float64Array normalised to [-1, 1], or null if unknown.
810
+ * Intended for external analysis / resynthesis such as the WORLD vocoder.
811
+ */
812
+ async getPcm(phoneme) {
813
+ const buf = await this.readPcmBytes(phoneme);
814
+ if (!buf) return null;
815
+ const int16 = new Int16Array(buf);
816
+ const f64 = new Float64Array(int16.length);
817
+ for (let i = 0; i < int16.length; i++) f64[i] = int16[i] / 32768;
818
+ return f64;
819
+ }
820
+ };
821
+ var WORLDLINE_SAMPLE_RATE = 48e3;
822
+ var MIN_WORLDLINE_SAMPLES = 4096;
823
+ var SYNTH_REQ_SIZE = 120;
824
+ var WL_FRAME_MS = 10;
825
+ var samplesToMs = (samples) => samples / WORLDLINE_SAMPLE_RATE * 1e3;
826
+ function leadInFromEntry(entry) {
827
+ return {
828
+ preMs: samplesToMs(entry.pre || 0),
829
+ consonantMs: samplesToMs(entry.consonant || 0)
830
+ };
831
+ }
832
+ var moduleCache = /* @__PURE__ */ new Map();
833
+ function injectScript(src) {
834
+ return new Promise((resolve, reject) => {
835
+ const existing = document.querySelector(
836
+ `script[data-koe-worldline="${src}"]`
837
+ );
838
+ if (existing) {
839
+ resolve();
840
+ return;
841
+ }
842
+ const s = document.createElement("script");
843
+ s.src = src;
844
+ s.dataset.koeWorldline = src;
845
+ s.onload = () => resolve();
846
+ s.onerror = () => reject(new Error(`worldline: failed to load ${src}`));
847
+ document.head.appendChild(s);
848
+ });
849
+ }
850
+ function loadWasm(scriptUrl) {
851
+ const cached = moduleCache.get(scriptUrl);
852
+ if (cached) return cached;
853
+ const baseUrl = scriptUrl.slice(0, scriptUrl.lastIndexOf("/") + 1);
854
+ const instantiate = () => {
855
+ const factory = globalThis.WorldlineModule;
856
+ if (!factory)
857
+ throw new Error(
858
+ "worldline: WorldlineModule global was not defined by the script"
859
+ );
860
+ return factory({ locateFile: (f) => baseUrl + f });
861
+ };
862
+ let promise;
863
+ if (typeof document !== "undefined") {
864
+ promise = injectScript(scriptUrl).then(instantiate);
865
+ } else if (typeof globalThis.importScripts === "function") {
866
+ promise = Promise.resolve().then(() => {
867
+ globalThis.importScripts(scriptUrl);
868
+ return instantiate();
869
+ });
870
+ } else {
871
+ return Promise.reject(
872
+ new Error(
873
+ "Worldline.load requires a DOM or a classic Web Worker (importScripts) to load worldline.js"
874
+ )
875
+ );
876
+ }
877
+ moduleCache.set(scriptUrl, promise);
878
+ return promise;
879
+ }
880
+ var Worldline = class _Worldline {
881
+ constructor(wasm) {
882
+ this.wasm = wasm;
883
+ }
884
+ wasm;
885
+ sampleRate = WORLDLINE_SAMPLE_RATE;
886
+ /**
887
+ * Load + instantiate the worldline WASM module (deduped per scriptUrl).
888
+ *
889
+ * Works on the main thread (loads via `<script>`) and inside a classic Web
890
+ * Worker (loads via `importScripts`), so the heavy synthesis can run
891
+ * off-thread. The matching `worldline.wasm` is fetched next to scriptUrl.
892
+ */
893
+ static async load(options) {
894
+ return new _Worldline(await loadWasm(options.scriptUrl));
895
+ }
896
+ /**
897
+ * Render one note to Float32 PCM at 48 kHz.
898
+ *
899
+ * The output buffer is laid out as [lead-in/consonant ≈ preMs][vowel ≈
900
+ * durationMs], rendered from sample offset 0 (no leading silence). The vowel
901
+ * onset (the "beat") sits at ≈ preMs into the buffer, so a sequencer should
902
+ * place the buffer at `beatTime − preMs` and may trim/crossfade the lead-in.
903
+ *
904
+ * No internal crossfade is applied — apply fades externally.
905
+ *
906
+ * @returns Float32 PCM, or null when `pcm` is shorter than
907
+ * {@link MIN_WORLDLINE_SAMPLES} (too short for stable F0 analysis).
908
+ */
909
+ renderNote(params) {
910
+ const { pcm, pitch, durationMs, preMs, consonantMs, tempo = 120 } = params;
911
+ if (!pcm || pcm.length < MIN_WORLDLINE_SAMPLES) return null;
912
+ const WL = this.wasm;
913
+ const FS = WORLDLINE_SAMPLE_RATE;
914
+ const midiNote = Math.round(69 + 12 * Math.log2(pitch / 440));
915
+ const posMs = 0;
916
+ const reqLen = preMs + durationMs;
917
+ const cutMs = WL_FRAME_MS * 2;
918
+ const ps = WL._PhraseSynthNew();
919
+ if (!ps) return null;
920
+ const reqPtr = WL._malloc(SYNTH_REQ_SIZE);
921
+ if (!reqPtr) {
922
+ WL._PhraseSynthDelete(ps);
923
+ return null;
924
+ }
925
+ const samplePtr = WL._malloc(pcm.length * 8);
926
+ if (!samplePtr) {
927
+ WL._free(reqPtr);
928
+ WL._PhraseSynthDelete(ps);
929
+ return null;
930
+ }
931
+ WL.HEAPF64.set(pcm, samplePtr >> 3);
932
+ const sv = (off, val, type) => WL.setValue(reqPtr + off, val, type);
933
+ sv(0, FS, "i32");
934
+ sv(4, pcm.length, "i32");
935
+ sv(8, samplePtr, "*");
936
+ sv(12, 0, "i32");
937
+ sv(16, 0, "*");
938
+ sv(20, midiNote, "i32");
939
+ sv(24, 100, "double");
940
+ sv(32, 0, "double");
941
+ sv(40, reqLen, "double");
942
+ sv(48, consonantMs, "double");
943
+ sv(56, cutMs, "double");
944
+ sv(64, 100, "double");
945
+ sv(72, 0, "double");
946
+ sv(80, tempo, "double");
947
+ sv(88, 0, "i32");
948
+ sv(92, 0, "*");
949
+ sv(96, 0, "i32");
950
+ sv(100, 0, "i32");
951
+ sv(104, 100, "i32");
952
+ sv(108, 0, "i32");
953
+ sv(112, 0, "i32");
954
+ sv(116, 100, "i32");
955
+ WL._PhraseSynthAddRequest(ps, reqPtr, posMs, 0, reqLen, 0, 0, 0);
956
+ WL._free(samplePtr);
957
+ WL._free(reqPtr);
958
+ const totalMs = posMs + reqLen + WL_FRAME_MS * 2;
959
+ const nFrames = Math.ceil(totalMs / WL_FRAME_MS) + 4;
960
+ const f0Arr = new Float64Array(nFrames).fill(pitch);
961
+ const gArr = new Float64Array(nFrames).fill(0.5);
962
+ const tArr = new Float64Array(nFrames).fill(0.5);
963
+ const bArr = new Float64Array(nFrames).fill(0.5);
964
+ const vArr = new Float64Array(nFrames).fill(1);
965
+ const f0Ptr = WL._malloc(nFrames * 8);
966
+ const gPtr = WL._malloc(nFrames * 8);
967
+ const tPtr = WL._malloc(nFrames * 8);
968
+ const bPtr = WL._malloc(nFrames * 8);
969
+ const vPtr = WL._malloc(nFrames * 8);
970
+ if (!f0Ptr || !gPtr || !tPtr || !bPtr || !vPtr) {
971
+ if (f0Ptr) WL._free(f0Ptr);
972
+ if (gPtr) WL._free(gPtr);
973
+ if (tPtr) WL._free(tPtr);
974
+ if (bPtr) WL._free(bPtr);
975
+ if (vPtr) WL._free(vPtr);
976
+ WL._PhraseSynthDelete(ps);
977
+ return null;
978
+ }
979
+ WL.HEAPF64.set(f0Arr, f0Ptr >> 3);
980
+ WL.HEAPF64.set(gArr, gPtr >> 3);
981
+ WL.HEAPF64.set(tArr, tPtr >> 3);
982
+ WL.HEAPF64.set(bArr, bPtr >> 3);
983
+ WL.HEAPF64.set(vArr, vPtr >> 3);
984
+ WL._PhraseSynthSetCurves(
985
+ ps,
986
+ f0Ptr,
987
+ gPtr,
988
+ tPtr,
989
+ bPtr,
990
+ vPtr,
991
+ nFrames,
992
+ WL_FRAME_MS
993
+ );
994
+ WL._free(f0Ptr);
995
+ WL._free(gPtr);
996
+ WL._free(tPtr);
997
+ WL._free(bPtr);
998
+ WL._free(vPtr);
999
+ const yPtrPtr = WL._malloc(4);
1000
+ if (!yPtrPtr) {
1001
+ WL._PhraseSynthDelete(ps);
1002
+ return null;
1003
+ }
1004
+ const outLen = WL._PhraseSynthSynth(ps, yPtrPtr, 0);
1005
+ const yPtr = WL.getValue(yPtrPtr, "*");
1006
+ const audio = outLen > 0 ? new Float32Array(WL.HEAPF32.buffer, yPtr, outLen).slice() : null;
1007
+ WL._free(yPtrPtr);
1008
+ WL._PhraseSynthDelete(ps);
1009
+ return audio;
1010
+ }
1011
+ };
1012
+
1013
+ // src/types.ts
1014
+ var DEFAULT_VOCAL_VOLUME = 200;
1015
+ var DEFAULT_BPM = 120;
1016
+ var DEFAULT_GATE = 100;
1017
+ var DEFAULT_PAN = 64;
1018
+ var DEFAULT_VELOCITY = 100;
1019
+ var DEFAULT_PLAYBACK_VELOCITY = 127;
1020
+ var DEFAULT_STEPS_PER_BAR = 192;
1021
+
1022
+ // src/lyrics.ts
1023
+ var kanaTable = {
1024
+ \u3042: ["", "a"],
1025
+ \u3044: ["", "i"],
1026
+ \u3046: ["", "u"],
1027
+ \u3048: ["", "e"],
1028
+ \u304A: ["", "o"],
1029
+ \u304B: ["k", "a"],
1030
+ \u304D: ["k", "i"],
1031
+ \u304F: ["k", "u"],
1032
+ \u3051: ["k", "e"],
1033
+ \u3053: ["k", "o"],
1034
+ \u3055: ["s", "a"],
1035
+ \u3057: ["sh", "i"],
1036
+ \u3059: ["s", "u"],
1037
+ \u305B: ["s", "e"],
1038
+ \u305D: ["s", "o"],
1039
+ \u305F: ["t", "a"],
1040
+ \u3061: ["ch", "i"],
1041
+ \u3064: ["ts", "u"],
1042
+ \u3066: ["t", "e"],
1043
+ \u3068: ["t", "o"],
1044
+ \u306A: ["n", "a"],
1045
+ \u306B: ["n", "i"],
1046
+ \u306C: ["n", "u"],
1047
+ \u306D: ["n", "e"],
1048
+ \u306E: ["n", "o"],
1049
+ \u306F: ["h", "a"],
1050
+ \u3072: ["h", "i"],
1051
+ \u3075: ["f", "u"],
1052
+ \u3078: ["h", "e"],
1053
+ \u307B: ["h", "o"],
1054
+ \u307E: ["m", "a"],
1055
+ \u307F: ["m", "i"],
1056
+ \u3080: ["m", "u"],
1057
+ \u3081: ["m", "e"],
1058
+ \u3082: ["m", "o"],
1059
+ \u3084: ["y", "a"],
1060
+ \u3086: ["y", "u"],
1061
+ \u3088: ["y", "o"],
1062
+ \u3089: ["r", "a"],
1063
+ \u308A: ["r", "i"],
1064
+ \u308B: ["r", "u"],
1065
+ \u308C: ["r", "e"],
1066
+ \u308D: ["r", "o"],
1067
+ \u308F: ["w", "a"],
1068
+ \u3092: ["w", "o"],
1069
+ \u304C: ["g", "a"],
1070
+ \u304E: ["g", "i"],
1071
+ \u3050: ["g", "u"],
1072
+ \u3052: ["g", "e"],
1073
+ \u3054: ["g", "o"],
1074
+ \u3056: ["z", "a"],
1075
+ \u3058: ["j", "i"],
1076
+ \u305A: ["z", "u"],
1077
+ \u305C: ["z", "e"],
1078
+ \u305E: ["z", "o"],
1079
+ \u3060: ["d", "a"],
1080
+ \u3062: ["j", "i"],
1081
+ \u3065: ["z", "u"],
1082
+ \u3067: ["d", "e"],
1083
+ \u3069: ["d", "o"],
1084
+ \u3070: ["b", "a"],
1085
+ \u3073: ["b", "i"],
1086
+ \u3076: ["b", "u"],
1087
+ \u3079: ["b", "e"],
1088
+ \u307C: ["b", "o"],
1089
+ \u3071: ["p", "a"],
1090
+ \u3074: ["p", "i"],
1091
+ \u3077: ["p", "u"],
1092
+ \u307A: ["p", "e"],
1093
+ \u307D: ["p", "o"],
1094
+ \u3093: ["N", "N"]
1095
+ };
1096
+ var SMALL_KANA = "\u3041\u3043\u3045\u3047\u3049\u3083\u3085\u3087\u3063";
1097
+ var VOWEL_KANA = {
1098
+ a: "\u3042",
1099
+ i: "\u3044",
1100
+ u: "\u3046",
1101
+ e: "\u3048",
1102
+ o: "\u304A"
1103
+ };
1104
+ var sanitizeText = (text) => text.normalize("NFKC").replace(/[ァ-ヶ]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 96)).replace(/[^ぁ-ゖー]/g, "");
1105
+ var splitSyllables = (text) => {
1106
+ const result = [];
1107
+ for (const ch of text) {
1108
+ if (result.length > 0 && SMALL_KANA.includes(ch)) {
1109
+ result[result.length - 1] += ch;
1110
+ } else {
1111
+ result.push(ch);
1112
+ }
1113
+ }
1114
+ return result;
1115
+ };
1116
+ var kanaToVowel = (kana) => {
1117
+ if (/[ぁゃ]/.test(kana)) return "a";
1118
+ if (/[ぃ]/.test(kana)) return "i";
1119
+ if (/[ぅゅ]/.test(kana)) return "u";
1120
+ if (/[ぇ]/.test(kana)) return "e";
1121
+ if (/[ぉょ]/.test(kana)) return "o";
1122
+ if (/[あかさたなはまやらわがざだばぱ]/.test(kana)) return "a";
1123
+ if (/[いきしちにひみりぎじぢびぴ]/.test(kana)) return "i";
1124
+ if (/[うくすつぬふむゆるぐずづぶぷ]/.test(kana)) return "u";
1125
+ if (/[えけせてねへめれげぜでべぺ]/.test(kana)) return "e";
1126
+ if (/[おこそとのほもよろごぞどぼぽ]/.test(kana)) return "o";
1127
+ return "";
1128
+ };
1129
+ var analyzeSyllable = (syllable) => {
1130
+ if (syllable === "\u30FC") return { kana: syllable, consonant: "-", vowel: "-" };
1131
+ if (syllable === "\u3063") return { kana: syllable, consonant: "Q", vowel: "" };
1132
+ const head = syllable[0];
1133
+ const row = kanaTable[head];
1134
+ const consonant = row ? row[0] : "";
1135
+ let vowel = row ? row[1] : kanaToVowel(head);
1136
+ if (syllable.length === 2 && syllable[1] !== "\u3063") {
1137
+ const v = kanaToVowel(syllable[1]);
1138
+ if (v) vowel = v;
1139
+ }
1140
+ return { kana: syllable, consonant, vowel };
1141
+ };
1142
+ var resolveLongVowels = (syllables) => {
1143
+ const result = [];
1144
+ let prevVowel = "";
1145
+ for (const syl of syllables) {
1146
+ if (syl.consonant === "-") {
1147
+ if (!prevVowel) continue;
1148
+ result.push({
1149
+ kana: VOWEL_KANA[prevVowel] ?? syl.kana,
1150
+ consonant: "",
1151
+ vowel: prevVowel
1152
+ });
1153
+ continue;
1154
+ }
1155
+ if (syl.vowel && syl.vowel !== "N") prevVowel = syl.vowel;
1156
+ result.push(syl);
1157
+ }
1158
+ return result;
1159
+ };
1160
+ var normalizeLyrics = (text) => resolveLongVowels(splitSyllables(sanitizeText(text)).map(analyzeSyllable));
1161
+ var normalizeLyricLines = (lines) => {
1162
+ const syllables = [];
1163
+ const lineBreaks = [];
1164
+ for (const line of lines) {
1165
+ const part = normalizeLyrics(line);
1166
+ if (part.length === 0) continue;
1167
+ if (syllables.length > 0) lineBreaks.push(syllables.length);
1168
+ syllables.push(...part);
1169
+ }
1170
+ return { syllables, lineBreaks };
1171
+ };
1172
+ var LYRIC_LINE = /^@@(\d+)\s+(.*)$/;
1173
+ var isLyricContinuation = (seg) => !/^[@#]/.test(seg);
1174
+ var splitSegments = (mml) => mml.split(/[;\n\r]+/).map((s) => s.trim()).filter((s) => s.length > 0);
1175
+ var clamp = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
1176
+ var MAX_VOCAL_VOLUME = 400;
1177
+ var VOCAL_BOOST_DB_PER_PERCENT = 0.08;
1178
+ var vocalVolumeToGain = (v) => {
1179
+ if (v <= 0) return 0;
1180
+ if (v <= 100) return v / 100;
1181
+ return 10 ** ((v - 100) * VOCAL_BOOST_DB_PER_PERCENT / 20);
1182
+ };
1183
+ var parseLyrics = (mml) => {
1184
+ const tracks = /* @__PURE__ */ new Map();
1185
+ const segments = splitSegments(mml);
1186
+ for (let i = 0; i < segments.length; i++) {
1187
+ const m = segments[i].match(LYRIC_LINE);
1188
+ if (!m) continue;
1189
+ const trackId = Number.parseInt(m[1], 10);
1190
+ const tokens = m[2].trim().split(/\s+/);
1191
+ const modelToken = tokens.shift() ?? "";
1192
+ let volume = DEFAULT_VOCAL_VOLUME;
1193
+ let gate = 100;
1194
+ let pan = 64;
1195
+ let octave = 0;
1196
+ const colon = modelToken.indexOf(":");
1197
+ const model = (colon === -1 ? modelToken : modelToken.slice(0, colon)).toLowerCase();
1198
+ if (colon !== -1) {
1199
+ const v = Number.parseInt(modelToken.slice(colon + 1), 10);
1200
+ if (Number.isFinite(v)) volume = clamp(v, 0, MAX_VOCAL_VOLUME);
1201
+ }
1202
+ const metaTokens = [modelToken];
1203
+ while (tokens.length > 0) {
1204
+ const v = /^v(\d+)$/.exec(tokens[0]);
1205
+ const q2 = /^q(\d+)$/.exec(tokens[0]);
1206
+ const p = /^p(\d+)$/.exec(tokens[0]);
1207
+ const o = /^o(-?\d+)$/.exec(tokens[0]);
1208
+ if (v) {
1209
+ volume = clamp(Number.parseInt(v[1], 10), 0, MAX_VOCAL_VOLUME);
1210
+ } else if (q2) {
1211
+ gate = clamp(Number.parseInt(q2[1], 10), 0, 100);
1212
+ } else if (p) {
1213
+ pan = clamp(Number.parseInt(p[1], 10), 0, 127);
1214
+ } else if (o) {
1215
+ octave = clamp(Number.parseInt(o[1], 10), -2, 2);
1216
+ } else {
1217
+ break;
1218
+ }
1219
+ metaTokens.push(tokens.shift());
1220
+ }
1221
+ const lyricLines = [tokens.join(" ")];
1222
+ while (i + 1 < segments.length && isLyricContinuation(segments[i + 1])) {
1223
+ lyricLines.push(segments[++i]);
1224
+ }
1225
+ const { syllables, lineBreaks } = normalizeLyricLines(lyricLines);
1226
+ tracks.set(trackId, {
1227
+ trackId,
1228
+ model,
1229
+ volume,
1230
+ gate,
1231
+ pan,
1232
+ octave,
1233
+ syllables,
1234
+ metaText: metaTokens.join(" "),
1235
+ ...lineBreaks.length > 0 ? { lineBreaks } : {}
1236
+ });
1237
+ }
1238
+ return tracks;
1239
+ };
1240
+ var stripLyrics = (mml) => {
1241
+ const segments = splitSegments(mml);
1242
+ const kept = [];
1243
+ for (let i = 0; i < segments.length; i++) {
1244
+ if (LYRIC_LINE.test(segments[i])) {
1245
+ while (i + 1 < segments.length && isLyricContinuation(segments[i + 1]))
1246
+ i++;
1247
+ continue;
1248
+ }
1249
+ kept.push(segments[i]);
1250
+ }
1251
+ return kept.join("\n");
1252
+ };
1253
+ var panToStereo = (pan) => Math.max(-1, Math.min(1, (pan - 64) / 64));
1254
+ var createLyricsConductor = (lyrics) => {
1255
+ const pointers = /* @__PURE__ */ new Map();
1256
+ const consume = (trackId) => {
1257
+ const track = lyrics.get(trackId);
1258
+ if (!track || track.syllables.length === 0) return null;
1259
+ const ptr = pointers.get(trackId) ?? 0;
1260
+ const syllable = track.syllables[ptr];
1261
+ if (!syllable) return null;
1262
+ pointers.set(trackId, ptr + 1);
1263
+ return {
1264
+ model: track.model,
1265
+ syllable,
1266
+ volume: vocalVolumeToGain(track.volume ?? DEFAULT_VOCAL_VOLUME),
1267
+ gate: (track.gate ?? DEFAULT_GATE) / 100,
1268
+ pan: panToStereo(track.pan ?? DEFAULT_PAN)
1269
+ };
1270
+ };
1271
+ const reset = () => pointers.clear();
1272
+ return { consume, reset };
1273
+ };
1274
+ var FORMANTS = {
1275
+ a: [800, 1200],
1276
+ i: [300, 2300],
1277
+ u: [350, 800],
1278
+ e: [500, 1900],
1279
+ o: [500, 900],
1280
+ // 撥音(ん)は鼻音寄りの低フォルマント
1281
+ N: [250, 1e3]
1282
+ };
1283
+ var midiToFreq = (m) => 440 * 2 ** ((m - 69) / 12);
1284
+ var createKlattVoice = (ctx, destination) => {
1285
+ const active = /* @__PURE__ */ new Set();
1286
+ const voice = (syllable, e) => {
1287
+ const t0 = ctx.currentTime + e.when;
1288
+ const peak = Math.max(1e-4, e.volume);
1289
+ if (syllable.vowel === "" || syllable.consonant === "Q") return;
1290
+ const [f1, f2] = FORMANTS[syllable.vowel] ?? FORMANTS.a;
1291
+ const attack = 0.02;
1292
+ const release = 0.06;
1293
+ const sustainEnd = t0 + Math.max(attack + 0.02, e.duration);
1294
+ let panner = null;
1295
+ let out = destination;
1296
+ if (typeof ctx.createStereoPanner === "function") {
1297
+ panner = ctx.createStereoPanner();
1298
+ panner.pan.value = Math.max(-1, Math.min(1, e.pan ?? 0));
1299
+ panner.connect(destination);
1300
+ out = panner;
1301
+ }
1302
+ const osc = ctx.createOscillator();
1303
+ osc.type = "sawtooth";
1304
+ osc.frequency.value = midiToFreq(e.pitch);
1305
+ const makeFormant = (freq, q2, gainScale) => {
1306
+ const filter = ctx.createBiquadFilter();
1307
+ filter.type = "bandpass";
1308
+ filter.frequency.value = freq;
1309
+ filter.Q.value = q2;
1310
+ const g = ctx.createGain();
1311
+ g.gain.value = gainScale;
1312
+ osc.connect(filter).connect(g);
1313
+ return g;
1314
+ };
1315
+ const env = ctx.createGain();
1316
+ env.gain.setValueAtTime(1e-4, t0);
1317
+ env.gain.exponentialRampToValueAtTime(peak, t0 + attack);
1318
+ env.gain.setValueAtTime(peak, sustainEnd);
1319
+ env.gain.exponentialRampToValueAtTime(1e-4, sustainEnd + release);
1320
+ const MAKEUP = 4;
1321
+ makeFormant(f1, 6, MAKEUP).connect(env);
1322
+ makeFormant(f2, 9, MAKEUP * 0.7).connect(env);
1323
+ env.connect(out);
1324
+ const fricatives = /* @__PURE__ */ new Set(["s", "sh", "ch", "ts", "h", "f"]);
1325
+ if (fricatives.has(syllable.consonant)) {
1326
+ const dur = 0.05;
1327
+ const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
1328
+ const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
1329
+ const data = buffer.getChannelData(0);
1330
+ for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
1331
+ const src = ctx.createBufferSource();
1332
+ src.buffer = buffer;
1333
+ const hp = ctx.createBiquadFilter();
1334
+ hp.type = "highpass";
1335
+ hp.frequency.value = syllable.consonant === "sh" ? 3e3 : 4500;
1336
+ const ng = ctx.createGain();
1337
+ ng.gain.setValueAtTime(peak * 0.5, t0);
1338
+ ng.gain.exponentialRampToValueAtTime(1e-4, t0 + dur);
1339
+ src.connect(hp).connect(ng).connect(out);
1340
+ src.start(t0);
1341
+ src.stop(t0 + dur);
1342
+ active.add(src);
1343
+ src.onended = () => {
1344
+ active.delete(src);
1345
+ src.disconnect();
1346
+ hp.disconnect();
1347
+ ng.disconnect();
1348
+ };
1349
+ }
1350
+ osc.start(t0);
1351
+ osc.stop(sustainEnd + release + 0.02);
1352
+ active.add(osc);
1353
+ osc.onended = () => {
1354
+ active.delete(osc);
1355
+ osc.disconnect();
1356
+ panner?.disconnect();
1357
+ };
1358
+ };
1359
+ voice.stopAll = () => {
1360
+ for (const n of active) {
1361
+ try {
1362
+ n.stop();
1363
+ } catch {
1364
+ }
1365
+ n.disconnect();
1366
+ }
1367
+ active.clear();
1368
+ };
1369
+ return voice;
1370
+ };
1371
+ var KOE_BASE_URL = "https://pub-12482a6b5cbc4c9e906b2e1904cabae5.r2.dev";
1372
+ var KOE_VOICEBANKS = {
1373
+ tsukuyomi: "\u3064\u304F\u3088\u307F\u3061\u3083\u3093.koe",
1374
+ rino: "\u6625\u97F3\u30EA\u30CEver0.3.koe",
1375
+ roze: "\u675F\u97F3\u30ED\u30BCver0.\uFF151(\u591A\u97F3\u968E).koe",
1376
+ ruko_male: "\u6B32\u97F3\u30EB\u30B3\u2642\u9023\u7D9A\u97F3Ver.1.03.koe",
1377
+ ruko_female: "\u6B32\u97F3\u30EB\u30B3\u2640\u6B4C\u9023\u7D9A\u97F3\u666E1.00.koe",
1378
+ teto: "\u91CD\u97F3\u30C6\u30C8\u5358\u72EC\u97F3.koe",
1379
+ shiyo: "\u9769\u547D\u30B7\u30E8.koe"
1380
+ };
1381
+ var KOE_VOICEBANK_LABELS = {
1382
+ tsukuyomi: "\u3064\u304F\u3088\u307F\u3061\u3083\u3093",
1383
+ rino: "\u6625\u97F3\u30EA\u30CE",
1384
+ roze: "\u675F\u97F3\u30ED\u30BC",
1385
+ ruko_male: "\u6B32\u97F3\u30EB\u30B3\u2642",
1386
+ ruko_female: "\u6B32\u97F3\u30EB\u30B3\u2640",
1387
+ teto: "\u91CD\u97F3\u30C6\u30C8",
1388
+ shiyo: "\u9769\u547D\u30B7\u30E8"
1389
+ };
1390
+ var VOICE_IMAGE_KEY = {
1391
+ klatt: "puyuyu",
1392
+ tsukuyomi: "tsukuyomi",
1393
+ rino: "rino",
1394
+ roze: "roze",
1395
+ ruko_male: "ruko",
1396
+ ruko_female: "ruko",
1397
+ teto: "teto",
1398
+ shiyo: "shiyo"
1399
+ };
1400
+ var KOE_VOICEBANK_TERMS = {
1401
+ tsukuyomi: "https://tyc.rei-yumesaki.net/material/utau/terms/",
1402
+ rino: "https://hatenakun1.github.io/halunelino/",
1403
+ roze: "https://tabaneroze.ninja-web.net/terms-of-use.html",
1404
+ ruko_male: "https://long-sleeper.net/index.php?id=22",
1405
+ ruko_female: "https://long-sleeper.net/index.php?id=22",
1406
+ teto: "https://kasaneteto.jp/guidelines/voice.html",
1407
+ shiyo: "https://kakumeisiyo.my.canva.site/dagkuyjwycs"
1408
+ };
1409
+ var koeUrl = (name, base = KOE_BASE_URL) => `${base}/${encodeURIComponent(name)}`;
1410
+ var DEFAULT_WORLDLINE_SCRIPT = "https://onjmin.github.io/koe/demo/world/worldline.js";
1411
+ var KOE_SAMPLE_RATE = 48e3;
1412
+ var expandSeparators = (candidate) => candidate.includes(" ") ? [candidate, candidate.replace(/ /g, "\u3000"), candidate.replace(/ /g, "")] : [candidate];
1413
+ var PITCH_SUFFIX = /_([A-G][#b]?-?\d+)$/;
1414
+ var NAME_SEMITONE = {
1415
+ c: 0,
1416
+ d: 2,
1417
+ e: 4,
1418
+ f: 5,
1419
+ g: 7,
1420
+ a: 9,
1421
+ b: 11
1422
+ };
1423
+ var pitchTokenToMidi = (token) => {
1424
+ const m = /^([A-Ga-g])([#b]?)(-?\d+)$/.exec(token);
1425
+ if (!m) return null;
1426
+ let semi = NAME_SEMITONE[m[1].toLowerCase()];
1427
+ if (m[2] === "#") semi++;
1428
+ else if (m[2] === "b") semi--;
1429
+ return (Number.parseInt(m[3], 10) + 1) * 12 + semi;
1430
+ };
1431
+ var collectPitchTokens = (aliases) => {
1432
+ const seen = /* @__PURE__ */ new Map();
1433
+ for (const a of aliases) {
1434
+ const m = PITCH_SUFFIX.exec(a);
1435
+ if (!m || seen.has(m[1])) continue;
1436
+ const midi = pitchTokenToMidi(m[1]);
1437
+ if (midi != null) seen.set(m[1], midi);
1438
+ }
1439
+ return [...seen].map(([token, midi]) => ({ token, midi }));
1440
+ };
1441
+ var resolveKoeAlias = (hasAlias, pitchTokens, syl, prevVowel, noteNum) => {
1442
+ const kana = syl.kana;
1443
+ const cons = syl.consonant === "N" ? "n" : syl.consonant;
1444
+ const vow = syl.vowel === "N" ? "" : syl.vowel;
1445
+ const romaji = `${cons}${vow}` || vow;
1446
+ const pv = prevVowel || "-";
1447
+ const raw = [
1448
+ // 連続音(VCV): 直前母音つき
1449
+ `${pv} ${kana}`,
1450
+ `${pv} ${romaji}`,
1451
+ // 単独音 / CVVC
1452
+ kana,
1453
+ romaji
1454
+ ];
1455
+ const vk = VOWEL_KANA[syl.vowel];
1456
+ if (vk) raw.push(`${pv} ${vk}`, vk, syl.vowel);
1457
+ if (syl.vowel === "N") raw.push("\u3093", "n", "N", `${pv} \u3093`);
1458
+ const seen = /* @__PURE__ */ new Set();
1459
+ const tryAlias = (candidate) => {
1460
+ for (const v of expandSeparators(candidate)) {
1461
+ if (seen.has(v)) continue;
1462
+ seen.add(v);
1463
+ if (hasAlias(v)) return v;
1464
+ }
1465
+ return null;
1466
+ };
1467
+ if (pitchTokens.length) {
1468
+ const nearest = pitchTokens.slice().sort((a, b) => Math.abs(a.midi - noteNum) - Math.abs(b.midi - noteNum));
1469
+ for (const { token } of nearest) {
1470
+ for (const base of raw) {
1471
+ const hit = tryAlias(`${base}_${token}`);
1472
+ if (hit) return hit;
1473
+ }
1474
+ }
1475
+ }
1476
+ for (const base of raw) {
1477
+ const hit = tryAlias(base);
1478
+ if (hit) return hit;
1479
+ }
1480
+ return null;
1481
+ };
1482
+ var createLocalBackend = async (options) => {
1483
+ const bank = await VoiceBank.load(options.koe);
1484
+ const worldline = options.lightweight ? null : await Worldline.load({
1485
+ scriptUrl: options.worldlineScriptUrl ?? DEFAULT_WORLDLINE_SCRIPT
1486
+ }).catch(() => null);
1487
+ const pcmCache = /* @__PURE__ */ new Map();
1488
+ const getPcm = (alias) => {
1489
+ let p = pcmCache.get(alias);
1490
+ if (!p) {
1491
+ p = bank.getPcm(alias);
1492
+ pcmCache.set(alias, p);
1493
+ }
1494
+ return p;
1495
+ };
1496
+ const renderAlias = async (alias, pitch, durationMs) => {
1497
+ const pcm = await getPcm(alias);
1498
+ if (!pcm || pcm.length === 0) return null;
1499
+ const entry = bank.manifest.phonemes[alias];
1500
+ const lead = leadInFromEntry(entry);
1501
+ const targetHz = midiToFreq(pitch);
1502
+ if (worldline) {
1503
+ const audio = worldline.renderNote({
1504
+ pcm,
1505
+ pitch: targetHz,
1506
+ durationMs,
1507
+ ...lead
1508
+ });
1509
+ if (audio) return { pcm: audio, preSec: lead.preMs / 1e3, rate: 1 };
1510
+ }
1511
+ const rate = entry.pitch > 0 ? targetHz / entry.pitch : 1;
1512
+ return {
1513
+ pcm: Float32Array.from(pcm),
1514
+ preSec: entry.pre / KOE_SAMPLE_RATE / rate,
1515
+ rate
1516
+ };
1517
+ };
1518
+ return {
1519
+ hasAlias: (a) => bank.has(a),
1520
+ pitchTokens: collectPitchTokens(Object.keys(bank.manifest.phonemes)),
1521
+ renderAlias,
1522
+ dispose: () => {
1523
+ }
1524
+ };
1525
+ };
1526
+ var spawnVoiceWorker = async (url) => {
1527
+ const sameOrigin = new URL(url, location.href).origin === location.origin;
1528
+ if (sameOrigin) return new Worker(url);
1529
+ const text = await fetch(url).then((r) => r.text());
1530
+ return new Worker(
1531
+ URL.createObjectURL(new Blob([text], { type: "text/javascript" }))
1532
+ );
1533
+ };
1534
+ var createWorkerBackend = async (workerUrl, options) => {
1535
+ const worker = await spawnVoiceWorker(workerUrl);
1536
+ const aliasSet = /* @__PURE__ */ new Set();
1537
+ const pending = /* @__PURE__ */ new Map();
1538
+ let reqId = 0;
1539
+ let onReady = null;
1540
+ let onFail = null;
1541
+ worker.onmessage = (ev) => {
1542
+ const m = ev.data;
1543
+ if (m.type === "ready") {
1544
+ for (const a of m.aliases) aliasSet.add(a);
1545
+ onReady?.();
1546
+ } else if (m.type === "error") {
1547
+ onFail?.(new Error(m.message));
1548
+ } else if (m.type === "rendered") {
1549
+ const cb = pending.get(m.id);
1550
+ if (cb) {
1551
+ pending.delete(m.id);
1552
+ cb(m);
1553
+ }
1554
+ }
1555
+ };
1556
+ worker.onerror = (e) => {
1557
+ const ev = e;
1558
+ onFail?.(new Error(ev.message || ev.error || `Event: ${ev.type}`));
1559
+ };
1560
+ await new Promise((resolve, reject) => {
1561
+ onReady = resolve;
1562
+ onFail = reject;
1563
+ worker.postMessage({
1564
+ type: "init",
1565
+ koe: options.koe,
1566
+ worldlineScriptUrl: options.worldlineScriptUrl ?? DEFAULT_WORLDLINE_SCRIPT,
1567
+ lightweight: !!options.lightweight
1568
+ });
1569
+ });
1570
+ onReady = null;
1571
+ onFail = null;
1572
+ const renderAlias = (alias, pitch, durationMs) => new Promise((resolve) => {
1573
+ const id = ++reqId;
1574
+ pending.set(
1575
+ id,
1576
+ (m) => resolve(
1577
+ m.pcm ? { pcm: m.pcm, preSec: m.preSec ?? 0, rate: m.rate ?? 1 } : null
1578
+ )
1579
+ );
1580
+ worker.postMessage({
1581
+ type: "render",
1582
+ id,
1583
+ alias,
1584
+ pitch,
1585
+ durationMs
1586
+ });
1587
+ });
1588
+ return {
1589
+ hasAlias: (a) => aliasSet.has(a),
1590
+ pitchTokens: collectPitchTokens(aliasSet),
1591
+ renderAlias,
1592
+ dispose: () => worker.terminate()
1593
+ };
1594
+ };
1595
+ var createKoeVoice = async (ctx, destination, options) => {
1596
+ const backend = options.voiceWorkerUrl ? await createWorkerBackend(options.voiceWorkerUrl, options) : await createLocalBackend(options);
1597
+ const renderCache = /* @__PURE__ */ new Map();
1598
+ const inflight = /* @__PURE__ */ new Map();
1599
+ const active = /* @__PURE__ */ new Set();
1600
+ let prevVowel = "";
1601
+ const keyOf = (alias, pitch, durationMs) => `${alias}|${pitch}|${Math.round(durationMs / 10) * 10}`;
1602
+ const renderInto = (alias, pitch, durationMs) => {
1603
+ const key = keyOf(alias, pitch, durationMs);
1604
+ const existing = renderCache.get(key);
1605
+ if (existing !== void 0) return Promise.resolve(existing);
1606
+ const flying = inflight.get(key);
1607
+ if (flying) return flying;
1608
+ const p = (async () => {
1609
+ const out = await backend.renderAlias(alias, pitch, durationMs);
1610
+ let rendered = null;
1611
+ if (out) {
1612
+ const buf = ctx.createBuffer(1, out.pcm.length, KOE_SAMPLE_RATE);
1613
+ buf.copyToChannel(out.pcm, 0);
1614
+ rendered = { audio: buf, preSec: out.preSec, rate: out.rate };
1615
+ }
1616
+ renderCache.set(key, rendered);
1617
+ inflight.delete(key);
1618
+ return rendered;
1619
+ })();
1620
+ inflight.set(key, p);
1621
+ return p;
1622
+ };
1623
+ const LEADCAP_S = 0.09;
1624
+ const schedule = (r, t0, peak, pan) => {
1625
+ let out = destination;
1626
+ let panner = null;
1627
+ if (typeof ctx.createStereoPanner === "function") {
1628
+ panner = ctx.createStereoPanner();
1629
+ panner.pan.value = Math.max(-1, Math.min(1, pan));
1630
+ panner.connect(destination);
1631
+ out = panner;
1632
+ }
1633
+ const src = ctx.createBufferSource();
1634
+ src.buffer = r.audio;
1635
+ src.playbackRate.value = r.rate;
1636
+ const effPre = Math.min(r.preSec, LEADCAP_S);
1637
+ const skipS = r.preSec - effPre;
1638
+ const startAt = Math.max(ctx.currentTime + 1e-3, t0 - effPre);
1639
+ const playDurSec = r.audio.duration / r.rate - skipS;
1640
+ const endAt = startAt + playDurSec;
1641
+ const attack = 0.01;
1642
+ const release = 0.04;
1643
+ const env = ctx.createGain();
1644
+ env.gain.setValueAtTime(1e-4, startAt);
1645
+ env.gain.exponentialRampToValueAtTime(peak, startAt + attack);
1646
+ const fadeStart = Math.max(startAt + attack, endAt - release);
1647
+ env.gain.setValueAtTime(peak, fadeStart);
1648
+ env.gain.exponentialRampToValueAtTime(1e-4, endAt);
1649
+ src.connect(env).connect(out);
1650
+ src.start(startAt, skipS);
1651
+ src.stop(endAt + 0.02);
1652
+ active.add(src);
1653
+ src.onended = () => {
1654
+ active.delete(src);
1655
+ src.disconnect();
1656
+ env.disconnect();
1657
+ panner?.disconnect();
1658
+ };
1659
+ };
1660
+ const model = (syllable, e) => {
1661
+ if (syllable.consonant === "Q" || syllable.vowel === "") return;
1662
+ const alias = resolveKoeAlias(
1663
+ backend.hasAlias,
1664
+ backend.pitchTokens,
1665
+ syllable,
1666
+ prevVowel,
1667
+ e.pitch
1668
+ );
1669
+ if (syllable.vowel && syllable.vowel !== "N") prevVowel = syllable.vowel;
1670
+ if (!alias) return;
1671
+ const t0 = ctx.currentTime + e.when;
1672
+ const peak = Math.max(1e-4, e.volume);
1673
+ const pan = e.pan ?? 0;
1674
+ const durationMs = Math.max(60, e.duration * 1e3);
1675
+ void renderInto(alias, e.pitch, durationMs).then((r) => {
1676
+ if (r) schedule(r, t0, peak, pan);
1677
+ });
1678
+ };
1679
+ model.renderToCache = async (syllable, prevVowelArg, pitch, durationMs) => {
1680
+ if (syllable.consonant === "Q" || syllable.vowel === "") return null;
1681
+ const alias = resolveKoeAlias(
1682
+ backend.hasAlias,
1683
+ backend.pitchTokens,
1684
+ syllable,
1685
+ prevVowelArg,
1686
+ pitch
1687
+ );
1688
+ if (!alias) return null;
1689
+ const dMs = Math.max(60, durationMs);
1690
+ const r = await renderInto(alias, pitch, dMs);
1691
+ return r ? keyOf(alias, pitch, dMs) : null;
1692
+ };
1693
+ model.scheduleCached = (key, t0, peak, pan) => {
1694
+ const r = renderCache.get(key);
1695
+ if (r) schedule(r, t0, peak, pan);
1696
+ };
1697
+ model.stopAll = () => {
1698
+ for (const src of active) {
1699
+ try {
1700
+ src.stop();
1701
+ } catch {
1702
+ }
1703
+ src.disconnect();
1704
+ }
1705
+ active.clear();
1706
+ };
1707
+ model.reset = () => {
1708
+ prevVowel = "";
1709
+ };
1710
+ return model;
1711
+ };
1712
+ var PREWARM_NOTES = 3;
1713
+ var STREAM_LOOKAHEAD_SEC = 1.5;
1714
+ var STREAM_POLL_MS = 100;
1715
+ var FALLBACK_MODEL = "klatt";
1716
+ var createSingingVoices = (ctx, destination, options = {}) => {
1717
+ const catalog = {};
1718
+ for (const [k, file] of Object.entries(KOE_VOICEBANKS))
1719
+ catalog[k] = koeUrl(file);
1720
+ for (const [k, v] of Object.entries(options.voicebanks ?? {}))
1721
+ catalog[k.toLowerCase()] = v;
1722
+ let streamSession = 0;
1723
+ const loaded = /* @__PURE__ */ new Map([
1724
+ [FALLBACK_MODEL, createKlattVoice(ctx, destination)]
1725
+ ]);
1726
+ const loading = /* @__PURE__ */ new Map();
1727
+ const load = (model) => {
1728
+ const m = model.toLowerCase();
1729
+ const ready = loaded.get(m);
1730
+ if (ready) return Promise.resolve(ready);
1731
+ const inflight = loading.get(m);
1732
+ if (inflight) return inflight;
1733
+ const koe = catalog[m];
1734
+ if (!koe) return Promise.resolve(null);
1735
+ const p = (async () => (
1736
+ // URL文字列はそのまま渡す。koe側が VoiceBank.load 内で HTTP Range により
1737
+ // マニフェストだけ先読みし、音素PCMは歌う直前にオンデマンド取得する
1738
+ // (= 初回に .koe 全体をDLしない。モバイル初回ロードの待ちを解消)。
1739
+ // Blob/File が直接渡されたケース(ローカル読み込み)はそのまま BlobVoiceSource。
1740
+ createKoeVoice(ctx, destination, {
1741
+ koe,
1742
+ worldlineScriptUrl: options.worldlineScriptUrl,
1743
+ lightweight: options.lightweight,
1744
+ voiceWorkerUrl: options.voiceWorkerUrl
1745
+ })
1746
+ ))().then((v) => {
1747
+ loaded.set(m, v);
1748
+ return v;
1749
+ }).catch((err) => {
1750
+ console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`, err);
1751
+ return null;
1752
+ });
1753
+ loading.set(m, p);
1754
+ return p;
1755
+ };
1756
+ const loadModels = async (models) => {
1757
+ const set = /* @__PURE__ */ new Set();
1758
+ for (const m of models) if (m) set.add(m.toLowerCase());
1759
+ await Promise.all([...set].map((m) => load(m)));
1760
+ };
1761
+ const forEachSungNote = (track, fn) => {
1762
+ let prevVowel = "";
1763
+ for (const note of track.notes) {
1764
+ const syl = note.syllable;
1765
+ if (syl.consonant === "Q" || syl.vowel === "") continue;
1766
+ fn(note, prevVowel);
1767
+ if (syl.vowel && syl.vowel !== "N") prevVowel = syl.vowel;
1768
+ }
1769
+ };
1770
+ const warm = async (tracks, count = PREWARM_NOTES) => {
1771
+ const promises = [];
1772
+ for (const track of tracks) {
1773
+ const m = loaded.get(track.model.toLowerCase());
1774
+ if (!m?.renderToCache) continue;
1775
+ let n = 0;
1776
+ forEachSungNote(track, (note, prevVowel) => {
1777
+ if (n >= count) return;
1778
+ n++;
1779
+ promises.push(
1780
+ m.renderToCache?.(
1781
+ note.syllable,
1782
+ prevVowel,
1783
+ note.pitch,
1784
+ note.durationSec * 1e3
1785
+ ) ?? Promise.resolve(null)
1786
+ );
1787
+ });
1788
+ }
1789
+ await Promise.all(promises);
1790
+ };
1791
+ const startStream = (tracks, anchorTime) => {
1792
+ const session = ++streamSession;
1793
+ const items = [];
1794
+ for (const track of tracks) {
1795
+ const model = loaded.get(track.model.toLowerCase());
1796
+ if (!model) continue;
1797
+ forEachSungNote(track, (note, prevVowel) => {
1798
+ items.push({
1799
+ model,
1800
+ note,
1801
+ prevVowel,
1802
+ volume: track.volume,
1803
+ pan: track.pan
1804
+ });
1805
+ });
1806
+ }
1807
+ items.sort((a, b) => a.note.startSec - b.note.startSec);
1808
+ void (async () => {
1809
+ for (const item of items) {
1810
+ if (session !== streamSession) return;
1811
+ while (item.note.startSec - (ctx.currentTime - anchorTime) > STREAM_LOOKAHEAD_SEC) {
1812
+ await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_MS));
1813
+ if (session !== streamSession) return;
1814
+ }
1815
+ const t0 = anchorTime + item.note.startSec;
1816
+ const peak = Math.max(1e-4, item.volume);
1817
+ const { model, note } = item;
1818
+ if (model.renderToCache && model.scheduleCached) {
1819
+ const key = await model.renderToCache(
1820
+ note.syllable,
1821
+ item.prevVowel,
1822
+ note.pitch,
1823
+ note.durationSec * 1e3
1824
+ );
1825
+ if (session !== streamSession) return;
1826
+ if (key) model.scheduleCached(key, t0, peak, item.pan);
1827
+ } else {
1828
+ const when = t0 - ctx.currentTime;
1829
+ model(note.syllable, {
1830
+ trackId: "",
1831
+ pitch: note.pitch,
1832
+ velocity: 100,
1833
+ volume: peak,
1834
+ when,
1835
+ duration: note.durationSec,
1836
+ pan: item.pan
1837
+ });
1838
+ }
1839
+ await new Promise((resolve) => setTimeout(resolve, 0));
1840
+ }
1841
+ })();
1842
+ };
1843
+ const stopStream = () => {
1844
+ streamSession++;
1845
+ for (const v of loaded.values()) v.stopAll?.();
1846
+ };
1847
+ const reset = () => {
1848
+ stopStream();
1849
+ for (const v of loaded.values()) v.reset?.();
1850
+ };
1851
+ return { loadModels, warm, startStream, stopStream, reset };
1852
+ };
1853
+ var createVoiceRegistry = (models = {}, fallback = "klatt") => {
1854
+ const sing = (model, syllable, e) => {
1855
+ const fn = models[model] ?? models[fallback];
1856
+ fn?.(syllable, e);
1857
+ };
1858
+ const register = (name, m) => {
1859
+ models[name.toLowerCase()] = m;
1860
+ };
1861
+ return { sing, register };
1862
+ };
1863
+
684
1864
  // src/macros.ts
685
1865
  var SCALES = [
686
1866
  [0, 2, 4, 5, 7, 9, 11],
@@ -1087,7 +2267,7 @@ var exportMIDI = (options) => {
1087
2267
  (n.startStep + (n.durationSteps || 1)) * tickPerStep
1088
2268
  );
1089
2269
  const vel = Math.round(
1090
- (n.velocity ?? 100) * (track.volume || 100) / 100
2270
+ (n.velocity ?? DEFAULT_VELOCITY) * (track.volume ?? 100) / 100
1091
2271
  );
1092
2272
  events.push({ t: startTick, m: [144 | ch & 15, n.pitch, vel] });
1093
2273
  events.push({ t: endTick, m: [144 | ch & 15, n.pitch, 0] });
@@ -1642,7 +2822,7 @@ var MMLCore = class _MMLCore {
1642
2822
  startStep: step,
1643
2823
  durationSteps: options.noteLengthSteps,
1644
2824
  pitch,
1645
- velocity: options.velocity ?? 100
2825
+ velocity: options.velocity ?? DEFAULT_VELOCITY
1646
2826
  };
1647
2827
  this.notes.push(newNote);
1648
2828
  }
@@ -1729,6 +2909,8 @@ var MMLCore = class _MMLCore {
1729
2909
  const config = getRenderConfig();
1730
2910
  const total = config.stepsPerBar;
1731
2911
  const candidates = [
2912
+ { dur: "1.", s: total * 1.5 },
2913
+ // 付点全音符(最長。これ以上はタイ未対応のため表現不可)
1732
2914
  { dur: "1", s: total / 1 },
1733
2915
  { dur: "2.", s: total / 2 * 1.5 },
1734
2916
  { dur: "2", s: total / 2 },
@@ -1789,97 +2971,72 @@ var MMLCore = class _MMLCore {
1789
2971
  return { text: `o${octave}${name}`, currentOctave: octave };
1790
2972
  }
1791
2973
  /**
1792
- * MML生成(1/2小節パターンスキャン方式)
2974
+ * MML生成(単一パス・発音順スキャン)
2975
+ *
2976
+ * 以前は1/2小節ごとのウィンドウで走査していたが、それだと半小節境界をまたぐ音符が
2977
+ * 境界で切り詰められて「ぶつ切り」になり、境界直前に始まる音符は隙間が潰れて欠落し、
2978
+ * 歌詞(@@n)の音節割り当てがずれていた。
2979
+ * 全ノートを発音順に一度で処理し、次の発音までの距離だけを上限として
2980
+ * 各音符の長さを忠実に出力する(次の音符がなければ曲末まで伸ばせる)。
1793
2981
  */
1794
2982
  generateMML = (volumeOverride) => {
1795
2983
  const config = getRenderConfig();
1796
2984
  const vol = volumeOverride ?? this.volume;
1797
- const HALF_BAR = config.stepsPerBar / 2;
1798
- const header = `t${this.tempo} q50 v${vol}`;
2985
+ const header = `t${this.tempo} v${vol}`;
1799
2986
  const segments = [];
1800
2987
  let lastOctave = -1;
1801
2988
  let currentCursor = 0;
1802
2989
  if (this.notes.length === 0) return header;
1803
- const lastNote = this.notes[this.notes.length - 1];
1804
- const endStep = lastNote.startStep + lastNote.durationSteps;
1805
- const totalSteps = Math.ceil(endStep / HALF_BAR) * HALF_BAR;
1806
- for (let windowStart = 0; windowStart < totalSteps; windowStart += HALF_BAR) {
1807
- const windowEnd = windowStart + HALF_BAR;
1808
- const windowNotes = this.notes.filter(
1809
- (n) => n.startStep >= windowStart && n.startStep < windowEnd
1810
- );
1811
- if (windowNotes.length === 0) {
1812
- while (currentCursor < windowEnd) {
1813
- const gap = windowEnd - currentCursor;
1814
- if (gap <= 2) {
1815
- currentCursor = windowEnd;
1816
- break;
1817
- }
1818
- const { dur, steps } = this.findBestFitDuration(gap);
1819
- segments.push(`r${dur}`);
1820
- currentCursor += steps;
1821
- }
1822
- continue;
1823
- }
1824
- const notesByStep = /* @__PURE__ */ new Map();
1825
- windowNotes.forEach((n) => {
1826
- const list = notesByStep.get(n.startStep) || [];
1827
- list.push(n);
1828
- notesByStep.set(n.startStep, list);
1829
- });
1830
- const sortedSteps = Array.from(notesByStep.keys()).sort((a, b) => a - b);
1831
- for (let i = 0; i < sortedSteps.length; i++) {
1832
- const startStep = sortedSteps[i];
1833
- const notes = notesByStep.get(startStep);
1834
- if (!notes) continue;
1835
- while (currentCursor < startStep) {
1836
- const gap = startStep - currentCursor;
1837
- if (gap <= 2) {
1838
- currentCursor = startStep;
1839
- break;
1840
- }
1841
- const { dur, steps } = this.findBestFitDuration(gap);
1842
- segments.push(`r${dur}`);
1843
- currentCursor += steps;
1844
- }
1845
- const nextStart = sortedSteps[i + 1] ?? windowEnd;
1846
- const physicsLimit = nextStart - currentCursor;
1847
- const MIN_STEP = config.stepsPerBar / 64;
1848
- if (physicsLimit < MIN_STEP) {
1849
- currentCursor = startStep;
1850
- continue;
1851
- }
1852
- const idealDuration = notes[0].durationSteps;
1853
- const durStr = this.stepsToMMLDuration(idealDuration, physicsLimit);
1854
- const actualStepGenerated = this.getStepFromDottedMML(durStr);
1855
- if (notes.length > 1) {
1856
- const noteStrs = notes.map((n) => {
1857
- const oct = Math.floor(n.pitch / 12) - 1;
1858
- const name = PITCH_MAP[n.pitch % 12];
1859
- return `o${oct}${name}`;
1860
- });
1861
- segments.push(`[${noteStrs.join("")}]${durStr}`);
1862
- } else {
1863
- const { text, currentOctave } = this.getNoteWithOctave(
1864
- notes[0].pitch,
1865
- lastOctave
1866
- );
1867
- segments.push(`${text}${durStr}`);
1868
- lastOctave = currentOctave;
1869
- }
1870
- currentCursor += actualStepGenerated;
1871
- }
1872
- while (currentCursor < windowEnd) {
1873
- const gap = windowEnd - currentCursor;
1874
- if (gap <= 2) {
1875
- currentCursor = windowEnd;
1876
- break;
1877
- }
2990
+ const endStep = Math.max(
2991
+ ...this.notes.map((n) => n.startStep + n.durationSteps)
2992
+ );
2993
+ const notesByStep = /* @__PURE__ */ new Map();
2994
+ for (const n of this.notes) {
2995
+ const list = notesByStep.get(n.startStep) ?? [];
2996
+ list.push(n);
2997
+ notesByStep.set(n.startStep, list);
2998
+ }
2999
+ const sortedSteps = Array.from(notesByStep.keys()).sort((a, b) => a - b);
3000
+ const MIN_STEP = config.stepsPerBar / 64;
3001
+ const fillRests = (until) => {
3002
+ while (until - currentCursor >= MIN_STEP) {
3003
+ const gap = until - currentCursor;
1878
3004
  const { dur, steps } = this.findBestFitDuration(gap);
1879
3005
  segments.push(`r${dur}`);
1880
3006
  currentCursor += steps;
1881
3007
  }
3008
+ };
3009
+ for (let i = 0; i < sortedSteps.length; i++) {
3010
+ const startStep = sortedSteps[i];
3011
+ const notes = notesByStep.get(startStep);
3012
+ if (!notes) continue;
3013
+ fillRests(startStep);
3014
+ const nextStart = sortedSteps[i + 1] ?? endStep;
3015
+ const physicsLimit = nextStart - currentCursor;
3016
+ if (physicsLimit < MIN_STEP) {
3017
+ continue;
3018
+ }
3019
+ const idealDuration = notes[0].durationSteps;
3020
+ const durStr = this.stepsToMMLDuration(idealDuration, physicsLimit);
3021
+ const actualStepGenerated = this.getStepFromDottedMML(durStr);
3022
+ if (notes.length > 1) {
3023
+ const noteStrs = notes.map((n) => {
3024
+ const oct = Math.floor(n.pitch / 12) - 1;
3025
+ const name = PITCH_MAP[n.pitch % 12];
3026
+ return `o${oct}${name}`;
3027
+ });
3028
+ segments.push(`[${noteStrs.join("")}]${durStr}`);
3029
+ } else {
3030
+ const { text, currentOctave } = this.getNoteWithOctave(
3031
+ notes[0].pitch,
3032
+ lastOctave
3033
+ );
3034
+ segments.push(`${text}${durStr}`);
3035
+ lastOctave = currentOctave;
3036
+ }
3037
+ currentCursor += actualStepGenerated;
1882
3038
  }
3039
+ fillRests(endStep);
1883
3040
  return `${header} ${segments.join(" ")}`;
1884
3041
  };
1885
3042
  /**
@@ -1957,13 +3114,51 @@ var PITCH_MAP2 = {
1957
3114
  a: 9,
1958
3115
  b: 11
1959
3116
  };
1960
- var TRACK_INDEX_COUNT = 4;
3117
+ var clamp2 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
3118
+ var META_DIRECTIVE = /#(inst|drum|volume)=([\w-]+)/gi;
3119
+ var parseMmlMeta = (mml) => {
3120
+ const meta = {};
3121
+ for (const m of mml.matchAll(META_DIRECTIVE)) {
3122
+ const key = m[1].toLowerCase();
3123
+ if (key === "inst") meta.instrument = m[2];
3124
+ else if (key === "drum") meta.drum = m[2];
3125
+ else if (key === "volume") {
3126
+ const v = Number.parseInt(m[2], 10);
3127
+ if (!Number.isNaN(v)) meta.volume = v;
3128
+ }
3129
+ }
3130
+ return meta;
3131
+ };
3132
+ var stripMmlMeta = (mml) => mml.replace(META_DIRECTIVE, "");
3133
+ var formatMmlMeta = (meta) => {
3134
+ const parts = [];
3135
+ if (meta.instrument) parts.push(`#inst=${meta.instrument}`);
3136
+ if (meta.drum) parts.push(`#drum=${meta.drum}`);
3137
+ if (meta.volume !== void 0) parts.push(`#volume=${meta.volume}`);
3138
+ return parts.join(" ");
3139
+ };
1961
3140
  var parseMML = (mml, options = {}) => {
1962
- const stepsPerBar = options.stepsPerBar ?? 192;
3141
+ const stepsPerBar = options.stepsPerBar ?? DEFAULT_STEPS_PER_BAR;
3142
+ const collectTokens = options.collectTokens ?? false;
3143
+ const collectLyrics = options.collectLyrics ?? false;
3144
+ const clampTrackCount = options.clampTrackCount;
1963
3145
  const placements = [];
3146
+ const tokenTracks = /* @__PURE__ */ new Map();
1964
3147
  let bpm = null;
1965
- if (!mml) return { placements, bpm };
1966
- const fullMML = mml.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").replace(/[\n\r]+/g, " ").trim();
3148
+ if (!mml) {
3149
+ return {
3150
+ placements,
3151
+ bpm,
3152
+ tokenTracks: collectTokens ? tokenTracks : void 0,
3153
+ lyrics: collectLyrics ? /* @__PURE__ */ new Map() : void 0,
3154
+ meta: {}
3155
+ };
3156
+ }
3157
+ const noComments = mml.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
3158
+ const meta = parseMmlMeta(noComments);
3159
+ const noMeta = stripMmlMeta(noComments);
3160
+ const lyrics = collectLyrics ? parseLyrics(noMeta) : void 0;
3161
+ const fullMML = stripLyrics(noMeta).replace(/[\n\r]+/g, " ").trim();
1967
3162
  const parts = fullMML.split(/(@\d+)/).filter((p) => p.trim().length > 0);
1968
3163
  let trackIndex = 0;
1969
3164
  let octave = 4;
@@ -1973,7 +3168,7 @@ var parseMML = (mml, options = {}) => {
1973
3168
  const part = rawPart.trim();
1974
3169
  if (part.startsWith("@")) {
1975
3170
  let idx = Number.parseInt(part.substring(1), 10);
1976
- if (idx >= TRACK_INDEX_COUNT) idx = 2;
3171
+ if (clampTrackCount !== void 0 && idx >= clampTrackCount) idx = 2;
1977
3172
  trackIndex = idx;
1978
3173
  octave = 4;
1979
3174
  currentStep = 0;
@@ -1982,13 +3177,27 @@ var parseMML = (mml, options = {}) => {
1982
3177
  }
1983
3178
  const body = part.replace(/\s+/g, "").toLowerCase();
1984
3179
  let j = 0;
3180
+ const pushTok = (type, start, dur, from) => {
3181
+ if (!collectTokens) return;
3182
+ let arr = tokenTracks.get(trackIndex);
3183
+ if (!arr) {
3184
+ arr = [];
3185
+ tokenTracks.set(trackIndex, arr);
3186
+ }
3187
+ arr.push({
3188
+ text: body.slice(from, j),
3189
+ startStep: start,
3190
+ durationSteps: dur,
3191
+ type
3192
+ });
3193
+ };
1985
3194
  const parseLength = () => {
1986
3195
  let numStr = "";
1987
3196
  while (j < body.length && /\d/.test(body[j])) {
1988
3197
  numStr += body[j];
1989
3198
  j++;
1990
3199
  }
1991
- const len = numStr ? Number.parseInt(numStr, 10) : baseLength;
3200
+ const len = numStr ? clamp2(Number.parseInt(numStr, 10), 1, 64) : baseLength;
1992
3201
  let steps = Math.round(stepsPerBar / len);
1993
3202
  while (j < body.length && body[j] === ".") {
1994
3203
  steps = Math.round(steps * 1.5);
@@ -1998,6 +3207,7 @@ var parseMML = (mml, options = {}) => {
1998
3207
  };
1999
3208
  while (j < body.length) {
2000
3209
  const ch = body[j];
3210
+ const tokStart = j;
2001
3211
  if (ch === "o") {
2002
3212
  j++;
2003
3213
  let numStr = "";
@@ -2005,13 +3215,16 @@ var parseMML = (mml, options = {}) => {
2005
3215
  numStr += body[j];
2006
3216
  j++;
2007
3217
  }
2008
- octave = Number.parseInt(numStr, 10) || 4;
3218
+ octave = numStr ? clamp2(Number.parseInt(numStr, 10), 0, 8) : 4;
3219
+ pushTok("octave", currentStep, 0, tokStart);
2009
3220
  } else if (ch === ">") {
2010
- octave++;
3221
+ octave = Math.min(8, octave + 1);
2011
3222
  j++;
3223
+ pushTok("shift", currentStep, 0, tokStart);
2012
3224
  } else if (ch === "<") {
2013
- octave--;
3225
+ octave = Math.max(0, octave - 1);
2014
3226
  j++;
3227
+ pushTok("shift", currentStep, 0, tokStart);
2015
3228
  } else if (ch === "l") {
2016
3229
  j++;
2017
3230
  let numStr = "";
@@ -2019,11 +3232,15 @@ var parseMML = (mml, options = {}) => {
2019
3232
  numStr += body[j];
2020
3233
  j++;
2021
3234
  }
2022
- baseLength = Number.parseInt(numStr, 10) || 16;
3235
+ baseLength = clamp2(Number.parseInt(numStr, 10) || 16, 1, 64);
3236
+ pushTok("length", currentStep, 0, tokStart);
2023
3237
  } else if (ch === "r") {
2024
3238
  j++;
2025
- currentStep += parseLength();
2026
- } else if (ch === "t" || ch === "v" || ch === "q") {
3239
+ const restStart = currentStep;
3240
+ const restSteps = parseLength();
3241
+ pushTok("rest", restStart, restSteps, tokStart);
3242
+ currentStep += restSteps;
3243
+ } else if (ch === "t" || ch === "v" || ch === "q" || ch === "p") {
2027
3244
  j++;
2028
3245
  let numStr = "";
2029
3246
  while (j < body.length && /\d/.test(body[j])) {
@@ -2031,8 +3248,9 @@ var parseMML = (mml, options = {}) => {
2031
3248
  j++;
2032
3249
  }
2033
3250
  if (ch === "t" && trackIndex === 0 && numStr) {
2034
- bpm = Number.parseInt(numStr, 10);
3251
+ bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
2035
3252
  }
3253
+ pushTok("ctrl", currentStep, 0, tokStart);
2036
3254
  } else if (ch === "[") {
2037
3255
  j++;
2038
3256
  const chordNotes = [];
@@ -2051,10 +3269,10 @@ var parseMML = (mml, options = {}) => {
2051
3269
  }
2052
3270
  chordNotes.push((octave + 1) * 12 + pitch);
2053
3271
  } else if (c === ">") {
2054
- octave++;
3272
+ octave = Math.min(8, octave + 1);
2055
3273
  j++;
2056
3274
  } else if (c === "<") {
2057
- octave--;
3275
+ octave = Math.max(0, octave - 1);
2058
3276
  j++;
2059
3277
  } else if (c === "o") {
2060
3278
  j++;
@@ -2063,7 +3281,7 @@ var parseMML = (mml, options = {}) => {
2063
3281
  numStr += body[j];
2064
3282
  j++;
2065
3283
  }
2066
- octave = Number.parseInt(numStr, 10) || 4;
3284
+ octave = numStr ? clamp2(Number.parseInt(numStr, 10), 0, 8) : 4;
2067
3285
  } else {
2068
3286
  j++;
2069
3287
  }
@@ -2078,6 +3296,7 @@ var parseMML = (mml, options = {}) => {
2078
3296
  durationSteps: Math.max(1, steps)
2079
3297
  });
2080
3298
  }
3299
+ pushTok("chord", currentStep, Math.max(1, steps), tokStart);
2081
3300
  currentStep += steps;
2082
3301
  octave = savedOctave;
2083
3302
  } else if (Object.hasOwn(PITCH_MAP2, ch)) {
@@ -2098,18 +3317,25 @@ var parseMML = (mml, options = {}) => {
2098
3317
  pitch: midiPitch,
2099
3318
  durationSteps: Math.max(1, steps)
2100
3319
  });
3320
+ pushTok("note", currentStep, Math.max(1, steps), tokStart);
2101
3321
  currentStep += steps;
2102
3322
  } else {
2103
3323
  j++;
2104
3324
  }
2105
3325
  }
2106
3326
  }
2107
- return { placements, bpm };
3327
+ return {
3328
+ placements,
3329
+ bpm,
3330
+ tokenTracks: collectTokens ? tokenTracks : void 0,
3331
+ lyrics,
3332
+ meta
3333
+ };
2108
3334
  };
2109
3335
 
2110
3336
  // src/sequencer.ts
2111
3337
  var STEPS_PER_BEAT2 = 48;
2112
- var PLAN_TIME = 0.1;
3338
+ var PLAN_TIME = 0.5;
2113
3339
  var TICK_INTERVAL_MS = 20;
2114
3340
  var createSequencer = (options) => {
2115
3341
  let timeline = [];
@@ -2119,15 +3345,18 @@ var createSequencer = (options) => {
2119
3345
  let animationId = null;
2120
3346
  let active = false;
2121
3347
  let fromStepValue = 0;
3348
+ let trackVolumeMap = /* @__PURE__ */ new Map();
2122
3349
  const secondsPerStep = () => 60 / options.getBpm() / STEPS_PER_BEAT2;
2123
3350
  const buildTimeline = (fromStep) => {
2124
3351
  timeline = [];
3352
+ trackVolumeMap = /* @__PURE__ */ new Map();
2125
3353
  const sps = secondsPerStep();
2126
3354
  for (const track of options.getTracks()) {
3355
+ trackVolumeMap.set(track.id, track.volume);
2127
3356
  for (const note of track.notes) {
2128
3357
  const relativeStart = note.startStep - fromStep;
2129
3358
  if (relativeStart < 0) continue;
2130
- const velocity = note.velocity ?? 127;
3359
+ const velocity = note.velocity ?? DEFAULT_PLAYBACK_VELOCITY;
2131
3360
  timeline.push({
2132
3361
  trackId: track.id,
2133
3362
  pitch: note.pitch,
@@ -2144,6 +3373,9 @@ var createSequencer = (options) => {
2144
3373
  const sps = secondsPerStep();
2145
3374
  const time = options.getAudioTime() - startTime;
2146
3375
  const soloId = options.getSoloTrackId();
3376
+ for (const track of options.getTracks()) {
3377
+ trackVolumeMap.set(track.id, track.volume);
3378
+ }
2147
3379
  while (nowIndex < timeline.length) {
2148
3380
  const ev = timeline[nowIndex];
2149
3381
  if (soloId && ev.trackId !== soloId) {
@@ -2154,11 +3386,12 @@ var createSequencer = (options) => {
2154
3386
  if (_when > PLAN_TIME) break;
2155
3387
  nowIndex++;
2156
3388
  const velocityVolume = ev.velocity / 127;
3389
+ const currentVolume = (trackVolumeMap.get(ev.trackId) ?? ev.volume * 100) / 100;
2157
3390
  options.onPlayNote({
2158
3391
  trackId: ev.trackId,
2159
3392
  pitch: ev.pitch,
2160
3393
  velocity: ev.velocity,
2161
- volume: ev.volume * velocityVolume,
3394
+ volume: currentVolume * velocityVolume,
2162
3395
  when: Math.max(0, _when),
2163
3396
  duration: ev.duration
2164
3397
  });
@@ -2209,13 +3442,14 @@ var createSequencer = (options) => {
2209
3442
  }
2210
3443
  active = false;
2211
3444
  };
3445
+ const START_DELAY = 0.1;
2212
3446
  const start = (fromStep) => {
2213
3447
  stop();
2214
3448
  fromStepValue = fromStep ?? options.getPlayStartStep();
2215
3449
  buildTimeline(fromStepValue);
2216
3450
  if (timeline.length === 0 && !options.getDrumPattern()?.length) return;
2217
3451
  active = true;
2218
- startTime = options.getAudioTime();
3452
+ startTime = options.getAudioTime() + START_DELAY;
2219
3453
  nowIndex = 0;
2220
3454
  intervalId = setInterval(scheduleTick, TICK_INTERVAL_MS);
2221
3455
  animationId = requestAnimationFrame(animate);
@@ -2223,7 +3457,8 @@ var createSequencer = (options) => {
2223
3457
  return {
2224
3458
  start,
2225
3459
  stop,
2226
- isActive: () => active
3460
+ isActive: () => active,
3461
+ getStartTime: () => startTime
2227
3462
  };
2228
3463
  };
2229
3464
 
@@ -2685,10 +3920,12 @@ var DAW_CSS = `
2685
3920
 
2686
3921
  /* \u2500\u2500\u2500 \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 \u2500\u2500\u2500 */
2687
3922
  .dtm-overlay {
2688
- position: fixed; inset: 0; z-index: 1000;
3923
+ position: absolute; inset: 0; z-index: 1000;
2689
3924
  background: rgba(0,0,0,.92);
2690
3925
  display: flex; align-items: center; justify-content: center;
2691
3926
  flex-direction: column; gap: 14px;
3927
+ pointer-events: auto;
3928
+ cursor: wait;
2692
3929
  }
2693
3930
  .dtm-overlay[hidden] { display: none; }
2694
3931
  .dtm-overlay::before {
@@ -2716,18 +3953,206 @@ var DAW_CSS = `
2716
3953
  animation: dtm-load 1.6s steps(8) infinite;
2717
3954
  }
2718
3955
  @keyframes dtm-load { 0%{width:0} 100%{width:100%} }
3956
+ /* \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 */
3957
+ .dtm-spinner--determinate::after { display: none; }
3958
+ .dtm-spinner-fill {
3959
+ position: absolute;
3960
+ left: 0; top: 0; height: 100%;
3961
+ width: 0;
3962
+ background: var(--dtm-primary);
3963
+ transition: width .12s steps(8);
3964
+ }
3965
+ .dtm-loading-label {
3966
+ font-family: var(--dtm-font);
3967
+ font-size: 11px;
3968
+ color: var(--dtm-primary);
3969
+ letter-spacing: .15em;
3970
+ min-height: 1em;
3971
+ }
3972
+
3973
+ @keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
3974
+ .dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
3975
+
3976
+ .dtm-hidden { display: none !important; }
3977
+ .dtm-grow { flex: 1 1 auto; }
3978
+ .dtm-lyric-icon {
3979
+ flex: 0 0 auto;
3980
+ width: 20px;
3981
+ height: 20px;
3982
+ border-radius: 50%;
3983
+ object-fit: cover;
3984
+ }
2719
3985
 
2720
- @keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
2721
- .dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
2722
-
2723
- .dtm-hidden { display: none !important; }
2724
- .dtm-grow { flex: 1 1 auto; }
2725
-
2726
3986
  /* \u2500\u2500\u2500 \u5E83\u5E45\u62E1\u5F35 \u2500\u2500\u2500 */
2727
3987
  @media (min-width: 768px) {
2728
3988
  .dtm-daw { gap: 8px; padding: 10px; }
2729
3989
  .dtm-roll { height: 420px; }
2730
3990
  }
3991
+
3992
+ /* ====================================================
3993
+ MML PLAYER \u2014 \u518D\u751F\u5C02\u7528\u30D3\u30E5\u30FC\uFF08mountMmlPlayer\uFF09
3994
+ ==================================================== */
3995
+ .dtm-player {
3996
+ display: flex;
3997
+ flex-direction: column;
3998
+ gap: var(--dtm-gap);
3999
+ padding: var(--dtm-gap);
4000
+ background: var(--dtm-deep);
4001
+ border: 2px solid var(--dtm-border2);
4002
+ box-shadow: 4px 4px 0 var(--c-black);
4003
+ }
4004
+ .dtm-player-head {
4005
+ display: flex;
4006
+ align-items: center;
4007
+ gap: 8px;
4008
+ flex-wrap: wrap;
4009
+ }
4010
+ .dtm-player-play {
4011
+ flex: 0 0 auto;
4012
+ width: 28px;
4013
+ height: 28px;
4014
+ display: flex;
4015
+ align-items: center;
4016
+ justify-content: center;
4017
+ background: var(--dtm-primary);
4018
+ color: var(--dtm-pfg);
4019
+ border: 2px solid var(--c-black);
4020
+ box-shadow: 2px 2px 0 var(--c-black);
4021
+ cursor: pointer;
4022
+ padding: 0;
4023
+ }
4024
+ .dtm-player-play:active { transform: translate(2px, 2px); box-shadow: none; }
4025
+ .dtm-player-play--stop { background: var(--dtm-danger); }
4026
+ .dtm-player-play:disabled { opacity: 0.4; cursor: default; }
4027
+ .dtm-player-beat-row {
4028
+ display: flex;
4029
+ align-items: center;
4030
+ gap: 4px;
4031
+ }
4032
+ .dtm-player-beat-dot {
4033
+ width: 8px;
4034
+ height: 8px;
4035
+ border-radius: 50%;
4036
+ background: var(--dtm-border2);
4037
+ transition: background 0.06s;
4038
+ }
4039
+ .dtm-player-beat-dot--on { background: var(--dtm-primary); }
4040
+ .dtm-player-bar {
4041
+ font-family: 'k8x12', monospace;
4042
+ font-size: 11px;
4043
+ color: var(--dtm-text);
4044
+ min-width: 2em;
4045
+ margin-left: 4px;
4046
+ }
4047
+ .dtm-player-dots {
4048
+ margin-left: auto;
4049
+ display: flex;
4050
+ align-items: center;
4051
+ gap: 6px;
4052
+ }
4053
+ .dtm-player-dot { width: 8px; height: 8px; display: inline-block; }
4054
+ .dtm-player-mml-header {
4055
+ margin-left: auto;
4056
+ display: flex;
4057
+ align-items: center;
4058
+ gap: 4px;
4059
+ }
4060
+ .dtm-player-mml-link {
4061
+ font-family: 'k8x12', monospace;
4062
+ font-size: 10px;
4063
+ color: var(--dtm-muted);
4064
+ text-decoration: none;
4065
+ white-space: nowrap;
4066
+ }
4067
+ .dtm-player-mml-link:hover { color: var(--dtm-primary); }
4068
+ .dtm-player-emoji {
4069
+ display: inline-flex;
4070
+ align-items: center;
4071
+ justify-content: center;
4072
+ width: 20px;
4073
+ height: 20px;
4074
+ border-radius: 50%;
4075
+ font-size: 18px;
4076
+ line-height: 1;
4077
+ user-select: none;
4078
+ }
4079
+ @keyframes dtm-emoji-jump {
4080
+ 0% { transform: translateY(0); }
4081
+ 35% { transform: translateY(-5px); }
4082
+ 65% { transform: translateY(-5px); }
4083
+ 100% { transform: translateY(0); }
4084
+ }
4085
+ .dtm-player-emoji--jump {
4086
+ animation: dtm-emoji-jump 0.18s ease-out forwards;
4087
+ }
4088
+ .dtm-player-chip {
4089
+ font-family: 'k8x12', monospace;
4090
+ font-size: 9px;
4091
+ color: var(--dtm-text);
4092
+ background: var(--dtm-border2);
4093
+ padding: 2px 6px;
4094
+ white-space: nowrap;
4095
+ }
4096
+ .dtm-player-body {
4097
+ 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 */
4098
+ display: flex;
4099
+ flex-direction: column;
4100
+ gap: var(--dtm-gap);
4101
+ }
4102
+ .dtm-player-lane-row {
4103
+ display: flex;
4104
+ align-items: stretch;
4105
+ gap: 6px;
4106
+ }
4107
+ .dtm-player-lane-label {
4108
+ flex: 0 0 auto;
4109
+ width: 16px;
4110
+ display: flex;
4111
+ flex-direction: column;
4112
+ align-items: center;
4113
+ gap: 2px;
4114
+ padding-top: 4px;
4115
+ }
4116
+ .dtm-player-lane-label--btn {
4117
+ cursor: pointer;
4118
+ user-select: none;
4119
+ }
4120
+ .dtm-player-lane-label--btn:hover { opacity: 0.7; }
4121
+ .dtm-player-lane-label--muted { opacity: 0.3; }
4122
+ .dtm-player-lane-no {
4123
+ font-family: 'k8x12', monospace;
4124
+ font-size: 9px;
4125
+ color: var(--dtm-muted);
4126
+ }
4127
+ .dtm-player-lane {
4128
+ 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 */
4129
+ flex: 1 1 auto;
4130
+ overflow-x: auto;
4131
+ white-space: nowrap;
4132
+ background: var(--c-black);
4133
+ border: 2px solid var(--dtm-border2);
4134
+ padding: 6px;
4135
+ scrollbar-width: none;
4136
+ }
4137
+ .dtm-player-lane::-webkit-scrollbar { display: none; }
4138
+ .dtm-tk {
4139
+ font-family: 'k8x12', monospace;
4140
+ font-size: 12px;
4141
+ color: var(--dtm-text);
4142
+ }
4143
+ .dtm-tk--rest { color: var(--dtm-muted); }
4144
+ .dtm-tk--octave,
4145
+ .dtm-tk--shift,
4146
+ .dtm-tk--length,
4147
+ .dtm-tk--ctrl { color: var(--dtm-border2); }
4148
+ .dtm-tk--lyric { color: var(--dtm-text); letter-spacing: 1px; }
4149
+ .dtm-tk--break { color: var(--dtm-muted); opacity: 0.7; margin: 0 2px; }
4150
+ .dtm-tk--meta { color: var(--dtm-border2); margin-right: 4px; }
4151
+ .dtm-tk.is-active {
4152
+ background: var(--tk, var(--dtm-primary));
4153
+ color: var(--c-black);
4154
+ font-weight: bold;
4155
+ }
2731
4156
  `;
2732
4157
  var injectStyles = (doc = document) => {
2733
4158
  if (doc.getElementById(STYLE_ID)) return;
@@ -2736,6 +4161,77 @@ var injectStyles = (doc = document) => {
2736
4161
  style.textContent = DAW_CSS;
2737
4162
  doc.head.appendChild(style);
2738
4163
  };
4164
+ var showLoadingOverlay = (container) => {
4165
+ const origPos = container.style.position;
4166
+ const computed = window.getComputedStyle(container).position;
4167
+ if (computed === "static") {
4168
+ container.style.position = "relative";
4169
+ }
4170
+ const doc = container.ownerDocument ?? document;
4171
+ const overlay = doc.createElement("div");
4172
+ overlay.className = "dtm-overlay";
4173
+ const spinner = doc.createElement("div");
4174
+ spinner.className = "dtm-spinner";
4175
+ const fill = doc.createElement("i");
4176
+ fill.className = "dtm-spinner-fill";
4177
+ spinner.appendChild(fill);
4178
+ overlay.appendChild(spinner);
4179
+ const label = doc.createElement("div");
4180
+ label.className = "dtm-loading-label";
4181
+ overlay.appendChild(label);
4182
+ container.appendChild(overlay);
4183
+ const setProgress = (done, total) => {
4184
+ if (total > 0) {
4185
+ const pct = Math.max(0, Math.min(100, Math.round(done / total * 100)));
4186
+ spinner.classList.add("dtm-spinner--determinate");
4187
+ fill.style.width = `${pct}%`;
4188
+ label.textContent = `${done} / ${total} (${pct}%)`;
4189
+ } else {
4190
+ spinner.classList.remove("dtm-spinner--determinate");
4191
+ fill.style.width = "0";
4192
+ label.textContent = "";
4193
+ }
4194
+ };
4195
+ return {
4196
+ remove: () => {
4197
+ overlay.remove();
4198
+ container.style.position = origPos;
4199
+ },
4200
+ setProgress
4201
+ };
4202
+ };
4203
+
4204
+ // assets/puyuyu.png
4205
+ var puyuyu_default = "data:image/png;base64,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";
4206
+
4207
+ // assets/rino.png
4208
+ var rino_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYAAAAGACAYAAACkx7W/AAAQAElEQVR4AezXT6hnZRkH8HdcBEIrF0FwS2FAQUihlUggw9AEIrhQQYKUJIRxY1vbRQsXQeiighZBEG1EQpCJEUSEQYZoo1GkICXotkWboM2NO3NHvXfu797fn/Oc8z7v8xHeufd3fue87/N8njPzxTua/xYVeOLFl/Yj19Vr1/ctBtu+A5Hv5hx7L/qXO8HhAiDBkJRIgACBCAEBEKFqTwIZBNRYXkAAlH8FABAgUFVAAFSdvL4JECgvIADKvgIaJ0CguoAAqP4G6J8AgbICAqDs6DVOgEBVgVt9C4BbEn4SIECgmIAAKDZw7RIgQOCWgAC4JeEngSoC+iRwKCAADiH8IECAQDUBAVBt4volQIDAoYAAOISo80OnBAgQuCkgAG46+JMAAQLlBARAuZFrmACBqgLH+xYAx0V8JkCAQBEBAVBk0NokQIDAcQEBcFzEZwKjCuiLwDEBAXAMxEcCBAhUERAAVSatTwIECBwTEADHQMb9qDMCBAgcFRAARz18IkCAQBkBAVBm1BolQKCqwKq+BcAqmcPrT7z40n7kev6px1vkOmzDDwJbCUS+m3PsHfl392DvrVA7ekgAdDQMpRAgQGBOAQEwp7azCCwh4EwCKwQEwAoYlwkQIDC6gAAYfcL6I0CAwAoBAbACZpzLOiFAgMDJAgLgZBdXCRAgMLyAABh+xBokQKCqwFl9C4CzhHxPgACBQQUEwKCD1RYBAgTOEhAAZwn5nkBWAXUTOENAAJwB5GsCBAiMKiAARp2svggQIHCGgAA4Ayjv1yonQIDA6QIC4HQf3xIgQGBYAQEw7Gg1RoBAVYF1+xYA60q5jwABAoMJCIDBBqodAgQIrCsgANaVch+BLALqJLCmgABYE8ptBAgQGE1AAIw2Uf0QIEBgTQEBsCZUnttUSoAAgfUEBMB6Tu4iQIDAcAICYLiRaogAgaoCm/adPgCuXru+H7mef+rxFrk2HZj7CYwk8MD5u1vkGskqopf0ARCBYk8CBAhUEBAAFaasxxoCuiSwoYAA2BDM7QQIEBhFQACMMkl9ECBAYEMBAbAhWL+3q4wAAQKbCQiAzbzcTYAAgWEEBMAwo9QIAQJVBbbtWwBsK+c5AgQIJBcQAMkHqHwCBAhsKyAAtpXzHIFeBNRBYEsBAbAlnMcIECCQXUAAZJ+g+gkQILClgADYEq6fx1RCgACB7QQEwHZuniJAgEB6AQGQfoQaIECgqsCufQuAXQU9T4AAgaQCAiDp4JRNgACBXQUEwK6CniewlIBzCewoIAB2BPQ4AQIEsgoIgKyTUzcBAgR2FBAAOwIu97iTCRAgsJuAANjNz9MECBBIKyAA0o5O4QQIVBWYqu/wALh67fp+5Hrg/N0t85pqkKv2yWxzUPuqvrJcP+jBWv13NMscR60zPABGhdMXAQIEsgsIgOwTVH89AR0TmEhAAEwEaRsCBAhkExAA2SamXgIECEwkIAAmgpxvGycRIEBgGgEBMI2jXQgQIJBOQACkG5mCCRCoKjB13wJgalH7ESBAIImAAEgyKGUSIEBgagEBMLWo/QhECdiXwMQCAmBiUNsRIEAgi4AAyDIpdRIgQGBiAQEwMWjcdnYmQIDAtAICYFpPuxEgQCCNgABIMyqFEiBQVSCqbwEQJWtfAgQIdC4gADofkPIIECAQJSAAomTtS2AqAfsQCBIQAEGwtiVAgEDvAgKg9wmpjwABAkECAiAIdrpt7USAAIEYAQEQ42pXAgQIdC8gALofkQIJEKgqEN13+gD44ONPWuZ16Z69Frn++Ke3WuSKto+0mWPvSHt7n/1uP//U4y1yPfHiS/uRSwBEC9ifAAECRQXS/x9A0blpu4KAHgkECwiAYGDbEyBAoFcBAdDrZNRFgACBYAEBEAy8/faeJECAQKyAAIj1tTsBAgS6FRAA3Y5GYQQIVBWYq28BMJe0cwgQINCZgADobCDKIUCAwFwCAmAuaecQWFfAfQRmEhAAM0E7hgABAr0JCIDeJqIeAgQIzCQgAGaCXv8YdxIgQGAeAQEwj7NTCBAg0J2AAOhuJAoiQKCqwNx9C4C5xZ1HgACBTgQEQCeDUAYBAgTmFhAAc4s7j8AqAdcJzCwgAGYGdxwBAgR6ERAAvUxCHQQIEJhZQADMDL76ON8QIEBgXgEBMK+30wgQINCNgADoZhQKIUCgqsBSfacPgI8/+rBlXksNfqpzo+2nqnOpfS5futgi11J9OXcMgfQBMMYYdEGAAIH5BQTA/OZOJHBUwCcCCwkIgIXgHUuAAIGlBQTA0hNwPgECBBYSEAALwX9xrN8IECCwjIAAWMbdqQQIEFhcQAAsPgIFECBQVWDpvgXA0hNwPgECBBYSEAALwTuWAAECSwsIgKUn4Py6AjonsLCAAFh4AI4nQIDAUgICYCl55xIgQGBhAQGw2AAcTIAAgWUFBMCy/k4nQIDAYgICYDF6BxMgUFWgl74FQC+TUAcBAgRmFhAAM4M7jgABAr0ICIBeJqGOOgI6JdCJgADoZBDKIECAwNwCAmBucecRIECgEwEBMPsgHEiAAIE+BARAH3NQBQECBGYXEACzkzuQAIGqAr31HR4AH3/0YYtc0aAvPPdsi1zn9vZa5Iqs/WDvaP/o/SPt59j7YAaZ1/l772uRK/r9yb5/eABkB1I/AQIERhUQAKNOVl/9CaiIQGcCAqCzgSiHAAECcwkIgLmknUOAAIHOBATAbANxEAECBPoSEAB9zUM1BAgQmE1AAMxG7SACBKoK9Nq3AOh1MuoiQIBAsIAACAa2PQECBHoVEAC9TkZd4wjohECnAgKg08EoiwABAtECAiBa2P4ECBDoVEAAhA/GAQQIEOhTQAD0ORdVESBAIFxAAIQTO4AAgaoCvfctAHqfkPoIECAQJCAAgmBtS4AAgd4FBEDvE1JfXgGVE+hcQAB0PiDlESBAIEpAAETJ2pcAAQKdCwiAsAHZmAABAn0LCIC+56M6AgQIhAkIgDBaGxMgUFUgS9/pA+CF555tkWt/f79FrrsefLhlXpcvXWyR69zeXotckbM92DvzbA9qz/IP2ao6L92z1yLXqnOzXE8fAFmg1UmAAIHeBARAbxNRT34BHRBIIiAAkgxKmQQIEJhaQABMLWo/AgQIJBEQAJMPyoYECBDIISAAcsxJlQQIEJhcQABMTmpDAgSqCmTrWwBkm5h6CRAgMJGAAJgI0jYECBDIJiAAsk1Mvf0KqIxAMgEBkGxgyiVAgMBUAgJgKkn7ECBAIJmAAJhsYDYiQIBALgEBkGteqiVAgMBkAgJgMkobESBQVSBr3wIg6+TUTYAAgR0FBMCOgB4nQIBAVgEBkHVy6u5HQCUEkgoIgKSDUzYBAgR2FRAAuwp6ngABAkkFBMDOg7MBAQIEcgoIgJxzUzUBAgR2FhAAOxPagACBqgLZ+04fAL/67e9a5Ioe8Fe/faFlXtE++59+2iJXdP2ZZ3tQ+9Vr11vkivZ/78qVFrmi64/eP30ARAPZnwABAqMKCIBRJ6uveAEnEEguIACSD1D5BAgQ2FZAAGwr5zkCBAgkFxAAWw/QgwQIEMgtIAByz0/1BAgQ2FpAAGxN50ECBKoKjNK3ABhlkvogQIDAhgICYEMwtxMgQGAUAQEwyiT1MZ+AkwgMIiAABhmkNggQILCpgADYVMz9BAgQGERAAGw8SA8QIEBgDAEBMMYcdUGAAIGNBQTAxmQeIECgqsBofQuA0SaqHwIECKwpIADWhHIbAQIERhMQAKNNVD9xAnYmMJiAABhsoNohQIDAugICYF0p9xEgQGAwAQGw9kDdSIAAgbEEBMBY89QNAQIE1hYQAGtTuZEAgaoCo/YdHgDn772vRa7owTz545+0yPXJzy63zCvaP3z/zz5rLXBlnu1B7dH+l+7Za5HrF3/7Z4tcr7/68rnIFe0fHgDRDdifAAECBLYTEADbuXmqkoBeCQwqIAAGHay2CBAgcJaAADhLyPcECBAYVEAAnDlYNxAgQGBMAQEw5lx1RYAAgTMFBMCZRG4gQKCqwOh9C4DRJ6w/AgQIrBAQACtgXCZAgMDoAgJg9Anrb3sBTxIYXEAADD5g7REgQGCVgABYJeM6AQIEBhcQACsH7AsCBAiMLSAAxp6v7ggQILBSQACspPEFAQJVBar0LQCqTFqfBAgQOCYgAI6B+EiAAIEqAgKgyqT1ub6AOwkUERAARQatTQIECBwXEADHRXwmQIBAEQEBcNugXSBAgEANAQFQY866JECAwG0CAuA2EhcIEKgqUK3v8AD43nceOhe53n7/Hy1yRb8Q71250iJXdP32X1bgyZ//skWu37z2Rotcb/3r0xa5lp1O/6eHB0D/BCokQIBATQEBUHPuuj5JwDUCxQQEQLGBa5cAAQK3BATALQk/CRAgUExAAHw+cL8QIECgloAAqDVv3RIgQOBzAQHwOYVfCBCoKlC1bwFQdfL6JkCgvIAAKP8KACBAoKqAAKg6eX1/IeA3AkUFBEDRwWubAAECAsA7QIAAgaICAqAVnby2CRAoLyAAyr8CAAgQqCogAKpOXt8ECLTqBAKg+hugfwIEygoIgLKj1zgBAtUFBED1N6By/3onUFxAABR/AbRPgEBdAQFQd/Y6J0CguEDhACg+ee0TIFBeQACUfwUAECBQVUAAVJ28vgkUFtD6TYH0AfD6qy+fi1w3mfL++d6VKy3z+t+bb7bMK9o++s3889/faJHrRz99rkWuyH8bDvaO9o/eP30ARAPZnwABAqMKCIBRJ6uv1QK+IUDghoAAuMHgDwIECNQTEAD1Zq5jAgQI3BAoGAA3+vYHAQIEygsIgPKvAAACBKoKCICqk9c3gYICWj4qIACOevhEgACBMgICoMyoNUqAAIGjAgLgqIdPIwvojQCBIwIC4AiHDwQIEKgjIADqzFqnBAgQOCJQKACO9O0DAQIEygsIgPKvAAACBKoKCICqk9c3gUICWj1ZQACc7OIqAQIEhhcQAMOPWIMECBA4WUAAnOzi6kgCeiFA4EQBAXAii4sECBAYX0AAjD9jHRIgQOBEgQIBcGLfLhIgQKC8gAAo/woAIECgqoAAqDp5fRMoIKDF0wXCA+Ab371/P/O68+v/aZHr6ddeaZHr1/9+v2Vef7njjha5vvLYYy1yRdtHvpsHe5/+z0f/32b+t2eO2sMDoP9XRIUECBCoKSAAas69Rte6JEDgVAEBcCqPLwkQIDCugAAYd7Y6I0CAwKkCAwfAqX37kgABAuUFBED5VwAAAQJVBQRA1cnrm8DAAlpbT0AArOfkLgIECAwnIACGG6mGCBAgsJ6AAFjPyV2ZBNRKgMBaAgJgLSY3ESBAYDwBATDeTHVEgACBtQQGDIC1+nYTAQIEygsIgPKvAAACBKoKCICqk9c3gQEFtLSZgADYzMvdBAgQGEZAAAwzSo0QIEBgMwEBsJmXu3sWUBsBAhsJCICNuNxMgACBcQQEwDiz1AkBepV9igAAA9pJREFUAgQ2EhgoADbq280ECBAoLyAAyr8CAAgQqCogAKpOXt8EBhLQynYCAmA7N08dCly+68EWuR5+9NEWuX7w+5db5Dpk8mOFwCMXL7TI9f1v3dkyrxVsk10WAJNR2ogAAQK5BARArnmp9iQB1wgQ2EpAAGzF5iECBAjkFxAA+WeoAwIECGwlMEAAbNW3hwgQIFBeQACUfwUAECBQVUAAVJ28vgkMIKCF3QQEwG5+niZAgEBaAQGQdnQKJ0CAwG4CAmA3P08vKeBsAgR2EhAAO/F5mAABAnkFBEDe2amcAAECOwkkDoCd+vYwAQIEygsIgPKvAAACBKoKCICqk9c3gcQCSp9GQABM42gXAgQIpBMQAOlGpmACBAhMIyAApnG0y5wCziJAYBIBATAJo00IECCQT0AA5JuZigkQIDCJQMIAmKRvmxAgQKC8gAAo/woAIECgqoAAqDp5fRNIKKDkaQXSB8AjFy+0yPXu2++0yDXtOOff7enXXmmR65s/vNQiV+RsD/aefyK5Tnzm/g9a5Prsaw+1yJVL+/Zq0wfA7S25QoAAAQLrCAiAdZTc04eAKggQmFRAAEzKaTMCBAjkERAAeWalUgIECEwqkCgAJu3bZgQIECgvIADKvwIACBCoKiAAqk5e3wQSCSg1RkAAxLjalQABAt0LCIDuR6RAAgQIxAgIgBhXu04pYC8CBEIEBEAIq00JECDQv4AA6H9GKiRAgECIQIIACOnbpgQIECgvIADKvwIACBCoKiAAqk5e3wQSCCgxVkAAxPranQABAt0KCIBuR6MwAgQIxAoIgFhfu+8i4FkCBEIFBEAor80JECDQr4AA6Hc2KiNAgECoQMcBENq3zQkQIFBeQACUfwUAECBQVUAAVJ28vgl0LKC0eQQEwDzOi53y7tvvtMi1WGNJDo60n2PvRy5eaJEreozP3P9Bi1x/+Ot/W+SK9hEA0cL2J0CAQKcCAqDTwZQuS/MECMwiIABmYXYIAQIE+hMQAP3NREUECBCYRaDDAJilb4cQIECgvIAAKP8KACBAoKqAAKg6eX0T6FBASfMKCIB5vZ1GgACBbgQEQDejUAgBAgTmFRAA83o77TQB3xEgMKuAAJiV22EECBDoR0AA9DMLlRAgQGBWgY4CYNa+HUaAAIHyAgKg/CsAgACBqgICoOrk9U2gIwGlLCMgAJZxdyoBAgQWFxAAi49AAQQIEFhGQAAs4+7ULwv4nQCBRQQEwCLsDiVAgMDyAgJg+RmogAABAosIdBAAi/TtUAIECJQXEADlXwEABAhUFRAAVSevbwIdCChhWYH/AwAA//8Rf+q5AAAABklEQVQDAK3dkYeXtKENAAAAAElFTkSuQmCC";
4209
+
4210
+ // assets/roze.png
4211
+ var roze_default = "data:image/png;base64,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";
4212
+
4213
+ // assets/ruko.png
4214
+ var ruko_default = "data:image/png;base64,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";
4215
+
4216
+ // assets/shiyo.png
4217
+ var shiyo_default = "data:image/png;base64,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";
4218
+
4219
+ // assets/teto.png
4220
+ var teto_default = "data:image/png;base64,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";
4221
+
4222
+ // assets/tsukuyomi.png
4223
+ var tsukuyomi_default = "data:image/png;base64,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";
4224
+
4225
+ // src/voice-images.ts
4226
+ var VOICE_IMAGES = {
4227
+ puyuyu: puyuyu_default,
4228
+ rino: rino_default,
4229
+ roze: roze_default,
4230
+ ruko: ruko_default,
4231
+ shiyo: shiyo_default,
4232
+ teto: teto_default,
4233
+ tsukuyomi: tsukuyomi_default
4234
+ };
2739
4235
 
2740
4236
  // src/daw.ts
2741
4237
  var BASE_STEP_WIDTH = 0.5;
@@ -2885,7 +4381,13 @@ var TRACKS_ADVANCED = [
2885
4381
  }
2886
4382
  ];
2887
4383
  var DEFAULT_TRACKS = TRACKS_SIMPLE;
2888
- var clamp = (v, min, max) => Math.min(Math.max(v, min), max);
4384
+ var LYRIC_MODELS = ["klatt", ...Object.keys(KOE_VOICEBANKS)];
4385
+ var LYRIC_MODEL_LABELS = {
4386
+ klatt: "\u8EFD\u91CF\u30ED\u30DC\u58F0",
4387
+ ...KOE_VOICEBANK_LABELS
4388
+ };
4389
+ var lyricModelLabel = (model) => LYRIC_MODEL_LABELS[model] ?? model;
4390
+ var clamp3 = (v, min, max) => Math.min(Math.max(v, min), max);
2889
4391
  var mountDAW = (target, options = {}) => {
2890
4392
  injectStyles();
2891
4393
  const getAudioTime = options.getAudioTime ?? (() => performance.now() / 1e3);
@@ -2897,7 +4399,7 @@ var mountDAW = (target, options = {}) => {
2897
4399
  tracks: trackConfigs,
2898
4400
  drumPatternNames: Object.keys(drumPatterns),
2899
4401
  defaultDrumPattern: drumPatterns.dance ? "dance" : Object.keys(drumPatterns)[0] ?? "none",
2900
- defaultBpm: options.defaultBpm ?? 120,
4402
+ defaultBpm: options.defaultBpm ?? DEFAULT_BPM,
2901
4403
  showMidi,
2902
4404
  showChord
2903
4405
  });
@@ -2912,10 +4414,11 @@ var mountDAW = (target, options = {}) => {
2912
4414
  const leftPaddingSteps = renderConfig.stepsPerBar * 16;
2913
4415
  let zoomX = 100;
2914
4416
  let zoomY = 100;
2915
- let bpm = options.defaultBpm ?? 120;
4417
+ let bpm = options.defaultBpm ?? DEFAULT_BPM;
2916
4418
  let masterVolume = 50;
2917
4419
  let drumVolume = 80;
2918
4420
  let currentDrumPattern = refs.drumSelect.value;
4421
+ let currentInstrument = "";
2919
4422
  let activeTrackId = trackConfigs[0].id;
2920
4423
  let activeToolMode = "pen";
2921
4424
  let currentInsertLength = 48;
@@ -2925,6 +4428,7 @@ var mountDAW = (target, options = {}) => {
2925
4428
  let currentOffsetY = (104 - 1 - 60) * renderConfig.keyHeight - 215;
2926
4429
  let playStartStep = 0;
2927
4430
  let isSolo = false;
4431
+ let lyricTrackIndices = /* @__PURE__ */ new Set();
2928
4432
  let playbackState = "stopped";
2929
4433
  let pausedPlayStep = 0;
2930
4434
  let currentPlayStep = 0;
@@ -2951,9 +4455,36 @@ var mountDAW = (target, options = {}) => {
2951
4455
  volume: config.volume,
2952
4456
  savedChordInput: "",
2953
4457
  savedChordPattern: "block",
2954
- savedChordRoot: 0
4458
+ savedChordRoot: 0,
4459
+ lyrics: "",
4460
+ lyricModel: "",
4461
+ // 既定は「なし」(歌わない)
4462
+ vocalVolume: DEFAULT_VOCAL_VOLUME,
4463
+ vocalGate: 100,
4464
+ vocalPan: 64,
4465
+ vocalOctave: 0
2955
4466
  }));
2956
4467
  };
4468
+ const buildLyricsMap = () => {
4469
+ const map = /* @__PURE__ */ new Map();
4470
+ trackStates.forEach((t, i) => {
4471
+ const model = t.lyricModel.trim();
4472
+ const text = t.lyrics.trim();
4473
+ if (!model || !text) return;
4474
+ const syllables = normalizeLyrics(text);
4475
+ if (syllables.length === 0) return;
4476
+ map.set(i, {
4477
+ trackId: i,
4478
+ model: model.toLowerCase(),
4479
+ volume: t.vocalVolume,
4480
+ gate: t.vocalGate,
4481
+ pan: t.vocalPan,
4482
+ octave: t.vocalOctave,
4483
+ syllables
4484
+ });
4485
+ });
4486
+ return map;
4487
+ };
2957
4488
  const getActive = () => trackStates.find((t) => t.config.id === activeTrackId) ?? trackStates[0];
2958
4489
  const getMaxNoteStep = () => {
2959
4490
  let maxStep = renderConfig.stepsPerBar * 4;
@@ -3057,7 +4588,7 @@ var mountDAW = (target, options = {}) => {
3057
4588
  );
3058
4589
  const ratio = currentOffsetX / maxOffsetX;
3059
4590
  refs.hScrollThumb.style.width = `${thumbW}px`;
3060
- refs.hScrollThumb.style.left = `${clamp(ratio * (sbW - thumbW), 0, sbW - thumbW)}px`;
4591
+ refs.hScrollThumb.style.left = `${clamp3(ratio * (sbW - thumbW), 0, sbW - thumbW)}px`;
3061
4592
  }
3062
4593
  const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
3063
4594
  const sbH = refs.vScroll.clientHeight;
@@ -3116,9 +4647,9 @@ var mountDAW = (target, options = {}) => {
3116
4647
  if (maxOffsetX <= 0) return;
3117
4648
  const rect = refs.hScroll.getBoundingClientRect();
3118
4649
  const thumbW = Number.parseFloat(refs.hScrollThumb.style.width) || 40;
3119
- const x = clamp(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
4650
+ const x = clamp3(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
3120
4651
  const ratio = x / (rect.width - thumbW);
3121
- currentOffsetX = clamp(ratio * maxOffsetX, 0, maxOffsetX);
4652
+ currentOffsetX = clamp3(ratio * maxOffsetX, 0, maxOffsetX);
3122
4653
  setDrawOffset(currentOffsetX, currentOffsetY);
3123
4654
  redrawAll();
3124
4655
  };
@@ -3127,9 +4658,9 @@ var mountDAW = (target, options = {}) => {
3127
4658
  if (maxOffset <= 0) return;
3128
4659
  const rect = refs.vScroll.getBoundingClientRect();
3129
4660
  const thumbH = Number.parseFloat(refs.vScrollThumb.style.height) || 40;
3130
- const y = clamp(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
4661
+ const y = clamp3(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
3131
4662
  const ratio = y / (rect.height - thumbH);
3132
- currentOffsetY = clamp(ratio * maxOffset, 0, maxOffset);
4663
+ currentOffsetY = clamp3(ratio * maxOffset, 0, maxOffset);
3133
4664
  setDrawOffset(currentOffsetX, currentOffsetY);
3134
4665
  redrawAll();
3135
4666
  };
@@ -3411,7 +4942,7 @@ var mountDAW = (target, options = {}) => {
3411
4942
  "wheel",
3412
4943
  (event) => {
3413
4944
  event.preventDefault();
3414
- currentOffsetY = clamp(
4945
+ currentOffsetY = clamp3(
3415
4946
  currentOffsetY + event.deltaY,
3416
4947
  0,
3417
4948
  getMaxOffsetY()
@@ -3459,7 +4990,7 @@ var mountDAW = (target, options = {}) => {
3459
4990
  const centerKey = (currentOffsetY + canvas.height / 2) / renderConfig.keyHeight;
3460
4991
  renderConfig.keyHeight = BASE_KEY_HEIGHT * zoomY / 100;
3461
4992
  refs.zoomYLabel.textContent = `${zoomY}%`;
3462
- currentOffsetY = clamp(
4993
+ currentOffsetY = clamp3(
3463
4994
  centerKey * renderConfig.keyHeight - canvas.height / 2,
3464
4995
  0,
3465
4996
  getMaxOffsetY()
@@ -3467,6 +4998,13 @@ var mountDAW = (target, options = {}) => {
3467
4998
  setDrawOffset(currentOffsetX, currentOffsetY);
3468
4999
  redrawAll();
3469
5000
  };
5001
+ const getViewState = () => ({
5002
+ zoomX,
5003
+ zoomY,
5004
+ decomposeChord: refs.decomposeChordToggle.checked,
5005
+ ignoreChordHeavy: refs.ignoreChordHeavyToggle.checked
5006
+ });
5007
+ const notifyViewState = () => options.onViewStateChange?.(getViewState());
3470
5008
  const dispatchNote = (trackId, pitch, trackVol, velocity, when, duration) => {
3471
5009
  const volume = trackVol / 100 * (velocity / 127) * (masterVolume / 100);
3472
5010
  options.onPlayNote?.({ trackId, pitch, velocity, volume, when, duration });
@@ -3483,8 +5021,11 @@ var mountDAW = (target, options = {}) => {
3483
5021
  getSoloTrackId: () => isSolo ? activeTrackId : null,
3484
5022
  getAudioTime,
3485
5023
  onPlayNote: (e) => {
3486
- const volume = e.volume * (masterVolume / 100);
3487
- options.onPlayNote?.({ ...e, volume });
5024
+ const idx = trackStates.findIndex((t) => t.config.id === e.trackId);
5025
+ if (idx >= 0 && lyricTrackIndices.has(idx) && options.singingVoices) {
5026
+ return;
5027
+ }
5028
+ options.onPlayNote?.({ ...e, volume: e.volume * (masterVolume / 100) });
3488
5029
  },
3489
5030
  onPlayDrum: (e) => {
3490
5031
  const velocity = e.velocity * (drumVolume / 100) * (masterVolume / 100);
@@ -3510,10 +5051,53 @@ var mountDAW = (target, options = {}) => {
3510
5051
  },
3511
5052
  stepsPerBar: renderConfig.stepsPerBar
3512
5053
  });
3513
- const play = () => {
5054
+ const play = async () => {
3514
5055
  options.onResumeAudio?.();
3515
5056
  if (playbackState === "playing") return;
3516
5057
  const fromStep = playbackState === "paused" ? pausedPlayStep : playStartStep;
5058
+ options.singingVoices?.reset();
5059
+ const lyricMap = buildLyricsMap();
5060
+ lyricTrackIndices = new Set(lyricMap.keys());
5061
+ const secondsPerStep = 60 / bpm / 48;
5062
+ const streamTracks = options.singingVoices ? [...lyricMap.values()].map((lt) => {
5063
+ const trackState = trackStates[lt.trackId];
5064
+ const sorted = [...trackState?.core.getNotes() ?? []].sort(
5065
+ (a, b) => a.startStep - b.startStep
5066
+ );
5067
+ const gate = (lt.gate ?? DEFAULT_GATE) / 100;
5068
+ const semis = (lt.octave ?? 0) * 12;
5069
+ const count = Math.min(sorted.length, lt.syllables.length);
5070
+ const notes = [];
5071
+ for (let i = 0; i < count; i++) {
5072
+ const n = sorted[i];
5073
+ if (n.startStep < fromStep) continue;
5074
+ notes.push({
5075
+ syllable: lt.syllables[i],
5076
+ pitch: n.pitch + semis,
5077
+ startSec: (n.startStep - fromStep) * secondsPerStep,
5078
+ durationSec: n.durationSteps * secondsPerStep * gate
5079
+ });
5080
+ }
5081
+ return {
5082
+ model: lt.model,
5083
+ volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (masterVolume / 100),
5084
+ pan: panToStereo(lt.pan ?? DEFAULT_PAN),
5085
+ notes
5086
+ };
5087
+ }) : [];
5088
+ const voices = options.singingVoices;
5089
+ const streaming = !!voices && streamTracks.some((t) => t.notes.length > 0);
5090
+ if (streaming && voices) {
5091
+ const overlay = showLoadingOverlay(refs.rollContainer);
5092
+ try {
5093
+ await voices.loadModels(streamTracks.map((t) => t.model));
5094
+ await voices.warm(streamTracks);
5095
+ } catch (err) {
5096
+ console.warn("[dtm] voice preload failed", err);
5097
+ } finally {
5098
+ overlay.remove();
5099
+ }
5100
+ }
3517
5101
  if (playbackState !== "paused") {
3518
5102
  const canvas = getGridCanvas();
3519
5103
  currentOffsetX = Math.max(
@@ -3524,17 +5108,22 @@ var mountDAW = (target, options = {}) => {
3524
5108
  }
3525
5109
  playbackState = "playing";
3526
5110
  sequencer.start(fromStep);
5111
+ if (streaming && voices) {
5112
+ voices.startStream(streamTracks, sequencer.getStartTime());
5113
+ }
3527
5114
  updateTransport();
3528
5115
  };
3529
5116
  const pause = () => {
3530
5117
  if (playbackState !== "playing") return;
3531
5118
  pausedPlayStep = currentPlayStep;
3532
5119
  sequencer.stop();
5120
+ options.singingVoices?.stopStream();
3533
5121
  playbackState = "paused";
3534
5122
  updateTransport();
3535
5123
  };
3536
5124
  const stop = () => {
3537
5125
  sequencer.stop();
5126
+ options.singingVoices?.stopStream();
3538
5127
  playbackState = "stopped";
3539
5128
  currentPlayStep = 0;
3540
5129
  updateTransport();
@@ -3584,6 +5173,158 @@ var mountDAW = (target, options = {}) => {
3584
5173
  active.core.setVolume(active.volume);
3585
5174
  volLabel.textContent = String(active.volume);
3586
5175
  });
5176
+ const lyricDiv = document.createElement("div");
5177
+ lyricDiv.className = "dtm-row";
5178
+ lyricDiv.style.flexDirection = "column";
5179
+ lyricDiv.style.alignItems = "stretch";
5180
+ lyricDiv.innerHTML = `
5181
+ <div class="dtm-row">
5182
+ <span class="dtm-label">\u266A \u6B4C\u8A5E</span>
5183
+ <select class="dtm-select" data-dtm="lyric-model" aria-label="\u6B4C\u5531\u30E2\u30C7\u30EB"></select>
5184
+ <img class="dtm-lyric-icon dtm-hidden" data-dtm="lyric-icon" width="20" height="20" alt="" draggable="false">
5185
+ <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">
5186
+ <option value="2">+2 oct</option>
5187
+ <option value="1">+1 oct</option>
5188
+ <option value="0">\xB10 oct</option>
5189
+ <option value="-1">-1 oct</option>
5190
+ <option value="-2">-2 oct</option>
5191
+ </select>
5192
+ <span class="dtm-label dtm-grow" data-dtm="lyric-count" style="text-align:right"></span>
5193
+ </div>
5194
+ <div class="dtm-row dtm-hidden" data-dtm="lyric-terms" style="font-size:10px;gap:4px;color:var(--dtm-warn)">
5195
+ <span>\u4F7F\u7528\u6642\u306B\u306F</span>
5196
+ <a data-dtm="lyric-terms-link" target="_blank" rel="noopener" style="color:var(--dtm-primary);text-decoration:underline"></a>
5197
+ <span>\u306E\u5229\u7528\u898F\u7D04\u306B\u5F93\u3063\u3066\u304F\u3060\u3055\u3044</span>
5198
+ </div>
5199
+ <div class="dtm-row" data-dtm="lyric-body" style="flex-direction:column;align-items:stretch">
5200
+ <div class="dtm-row">
5201
+ <span class="dtm-label">\u58F0\u91CF</span>
5202
+ <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">
5203
+ <span class="dtm-label" data-dtm="lyric-vol-label"></span>
5204
+ </div>
5205
+ <div class="dtm-row">
5206
+ <span class="dtm-label">\u5B9A\u4F4D</span>
5207
+ <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">
5208
+ <span class="dtm-label" data-dtm="lyric-pan-label"></span>
5209
+ </div>
5210
+ <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>
5211
+ </div>`;
5212
+ refs.trackBody.appendChild(lyricDiv);
5213
+ const lyricModelSel = lyricDiv.querySelector(
5214
+ '[data-dtm="lyric-model"]'
5215
+ );
5216
+ const lyricOctaveSel = lyricDiv.querySelector(
5217
+ '[data-dtm="lyric-octave"]'
5218
+ );
5219
+ const lyricIcon = lyricDiv.querySelector(
5220
+ '[data-dtm="lyric-icon"]'
5221
+ );
5222
+ const lyricBody = lyricDiv.querySelector(
5223
+ '[data-dtm="lyric-body"]'
5224
+ );
5225
+ const lyricInput = lyricDiv.querySelector(
5226
+ '[data-dtm="lyric-input"]'
5227
+ );
5228
+ const lyricCount = lyricDiv.querySelector(
5229
+ '[data-dtm="lyric-count"]'
5230
+ );
5231
+ const lyricVol = lyricDiv.querySelector(
5232
+ '[data-dtm="lyric-vol"]'
5233
+ );
5234
+ const lyricVolLabel = lyricDiv.querySelector(
5235
+ '[data-dtm="lyric-vol-label"]'
5236
+ );
5237
+ const lyricPan = lyricDiv.querySelector(
5238
+ '[data-dtm="lyric-pan"]'
5239
+ );
5240
+ const lyricPanLabel = lyricDiv.querySelector(
5241
+ '[data-dtm="lyric-pan-label"]'
5242
+ );
5243
+ const lyricTerms = lyricDiv.querySelector(
5244
+ '[data-dtm="lyric-terms"]'
5245
+ );
5246
+ const lyricTermsLink = lyricDiv.querySelector(
5247
+ '[data-dtm="lyric-terms-link"]'
5248
+ );
5249
+ const fmtPan = (pan) => pan === 64 ? "C" : pan < 64 ? `L${64 - pan}` : `R${pan - 64}`;
5250
+ const addOpt = (value, label) => {
5251
+ const o = document.createElement("option");
5252
+ o.value = value;
5253
+ o.textContent = label;
5254
+ lyricModelSel.appendChild(o);
5255
+ };
5256
+ addOpt("", "\u306A\u3057");
5257
+ for (const m of LYRIC_MODELS) addOpt(m, lyricModelLabel(m));
5258
+ if (active.lyricModel && !LYRIC_MODELS.includes(active.lyricModel)) {
5259
+ addOpt(active.lyricModel, lyricModelLabel(active.lyricModel));
5260
+ }
5261
+ lyricModelSel.value = active.lyricModel;
5262
+ lyricOctaveSel.value = String(active.vocalOctave);
5263
+ lyricInput.value = active.lyrics;
5264
+ lyricVol.value = String(active.vocalVolume);
5265
+ lyricVolLabel.textContent = String(active.vocalVolume);
5266
+ lyricPan.value = String(active.vocalPan);
5267
+ lyricPanLabel.textContent = fmtPan(active.vocalPan);
5268
+ const updateLyricCount = () => {
5269
+ const n = normalizeLyrics(lyricInput.value).length;
5270
+ lyricCount.textContent = active.lyricModel && n > 0 ? `${n}\u97F3\u7BC0` : "";
5271
+ };
5272
+ const syncLyricTerms = () => {
5273
+ const url = active.lyricModel ? KOE_VOICEBANK_TERMS[active.lyricModel] : void 0;
5274
+ if (url) {
5275
+ const label = lyricModelLabel(active.lyricModel);
5276
+ lyricTermsLink.textContent = `${label}UTAU\u97F3\u6E90`;
5277
+ lyricTermsLink.href = url;
5278
+ lyricTerms.classList.remove("dtm-hidden");
5279
+ } else {
5280
+ lyricTerms.classList.add("dtm-hidden");
5281
+ }
5282
+ };
5283
+ const syncLyricIcon = () => {
5284
+ const imgKey = active.lyricModel ? VOICE_IMAGE_KEY[active.lyricModel.toLowerCase()] : void 0;
5285
+ const src = imgKey ? VOICE_IMAGES[imgKey] : void 0;
5286
+ if (src) {
5287
+ lyricIcon.src = src;
5288
+ lyricIcon.classList.remove("dtm-hidden");
5289
+ } else {
5290
+ lyricIcon.removeAttribute("src");
5291
+ lyricIcon.classList.add("dtm-hidden");
5292
+ }
5293
+ };
5294
+ const syncLyricVisibility = () => {
5295
+ lyricBody.style.display = active.lyricModel ? "" : "none";
5296
+ lyricOctaveSel.style.display = active.lyricModel ? "" : "none";
5297
+ updateLyricCount();
5298
+ syncLyricTerms();
5299
+ syncLyricIcon();
5300
+ };
5301
+ syncLyricVisibility();
5302
+ lyricModelSel.addEventListener("change", () => {
5303
+ active.lyricModel = lyricModelSel.value;
5304
+ syncLyricVisibility();
5305
+ });
5306
+ lyricOctaveSel.addEventListener("change", () => {
5307
+ active.vocalOctave = Number.parseInt(lyricOctaveSel.value, 10);
5308
+ });
5309
+ lyricInput.addEventListener("input", () => {
5310
+ active.lyrics = lyricInput.value;
5311
+ updateLyricCount();
5312
+ });
5313
+ lyricVol.addEventListener("input", () => {
5314
+ active.vocalVolume = Number.parseInt(lyricVol.value, 10);
5315
+ lyricVolLabel.textContent = lyricVol.value;
5316
+ });
5317
+ lyricPan.addEventListener("input", () => {
5318
+ active.vocalPan = Number.parseInt(lyricPan.value, 10);
5319
+ lyricPanLabel.textContent = fmtPan(active.vocalPan);
5320
+ });
5321
+ lyricPanLabel.style.cursor = "pointer";
5322
+ lyricPanLabel.title = "\u30BF\u30C3\u30D7\u3067\u4E2D\u592E(C)\u3078";
5323
+ lyricPanLabel.addEventListener("click", () => {
5324
+ active.vocalPan = 64;
5325
+ lyricPan.value = "64";
5326
+ lyricPanLabel.textContent = fmtPan(64);
5327
+ });
3587
5328
  if (active.config.id === "chord" && showChord) {
3588
5329
  const div = document.createElement("div");
3589
5330
  div.className = "dtm-row";
@@ -3671,6 +5412,11 @@ var mountDAW = (target, options = {}) => {
3671
5412
  const barLimitBars = Number(refs.barLimitSelect.value);
3672
5413
  const limitSteps = barLimitBars > 0 ? barLimitBars * renderConfig.stepsPerBar : Infinity;
3673
5414
  const clipNotes = (notes) => limitSteps === Infinity ? notes : notes.filter((n) => n.startStep < limitSteps);
5415
+ const metaLine = formatMmlMeta({
5416
+ instrument: currentInstrument || void 0,
5417
+ drum: currentDrumPattern !== "none" ? currentDrumPattern : void 0,
5418
+ volume: masterVolume
5419
+ });
3674
5420
  if (refs.decomposeChordToggle.checked) {
3675
5421
  const ignoreHeavy = refs.ignoreChordHeavyToggle.checked;
3676
5422
  const targetStates = ignoreHeavy ? trackStates.filter((t) => !isChordHeavyTrack(t.core.getNotes())) : trackStates;
@@ -3680,12 +5426,14 @@ var mountDAW = (target, options = {}) => {
3680
5426
  );
3681
5427
  const monoTracks = decomposeToMonophonic(allNotes);
3682
5428
  const refCore = trackStates[0].core;
3683
- const full2 = monoTracks.map(
5429
+ const decomposedFull = monoTracks.map(
3684
5430
  (notes, i) => `@${i} ${refCore.getMMLFromNotes(notes, bpm, 100).trim()}`
3685
- ).join(";\n");
3686
- const minified2 = monoTracks.map(
5431
+ );
5432
+ const decomposedMini = monoTracks.map(
3687
5433
  (notes, i) => `@${i}${refCore.getMMLFromNotes(notes, bpm, 100).trim().replace(/\s+/g, "")}`
3688
- ).join(";");
5434
+ );
5435
+ const full2 = [metaLine, ...decomposedFull].filter((s) => s.length > 0).join(";\n");
5436
+ const minified2 = [metaLine, ...decomposedMini].filter((s) => s.length > 0).join(";");
3689
5437
  return {
3690
5438
  full: full2,
3691
5439
  minified: minified2,
@@ -3694,12 +5442,32 @@ var mountDAW = (target, options = {}) => {
3694
5442
  barLimit: barLimitBars
3695
5443
  };
3696
5444
  }
3697
- const full = trackStates.map(
5445
+ const trackLines = trackStates.map(
3698
5446
  (t, i) => `@${i} ${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim()}`
3699
- ).join(";\n");
3700
- const minified = trackStates.map(
5447
+ );
5448
+ const trackLinesMini = trackStates.map(
3701
5449
  (t, i) => `@${i}${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim().replace(/\s+/g, "")}`
3702
- ).join(";");
5450
+ );
5451
+ const lyricLines = trackStates.map((t, i) => ({
5452
+ i,
5453
+ text: t.lyrics.trim(),
5454
+ model: t.lyricModel.trim(),
5455
+ vol: t.vocalVolume,
5456
+ gate: t.vocalGate,
5457
+ pan: t.vocalPan,
5458
+ oct: t.vocalOctave
5459
+ })).filter((x) => x.model.length > 0 && x.text.length > 0).map((x) => {
5460
+ const params = [
5461
+ x.vol === DEFAULT_VOCAL_VOLUME ? "" : `v${x.vol}`,
5462
+ x.gate === 100 ? "" : `q${x.gate}`,
5463
+ x.pan === 64 ? "" : `p${x.pan}`,
5464
+ x.oct === 0 ? "" : `o${x.oct}`
5465
+ ].filter((s) => s.length > 0).join(" ");
5466
+ const head = params ? `${x.model} ${params}` : x.model;
5467
+ return `@@${x.i} ${head} ${x.text}`;
5468
+ });
5469
+ const full = [metaLine, ...trackLines, ...lyricLines].filter((s) => s.length > 0).join(";\n");
5470
+ const minified = [metaLine, ...trackLinesMini, ...lyricLines].filter((s) => s.length > 0).join(";");
3703
5471
  return {
3704
5472
  full,
3705
5473
  minified,
@@ -3720,6 +5488,34 @@ var mountDAW = (target, options = {}) => {
3720
5488
  refs.outputContainer.classList.remove("dtm-hidden");
3721
5489
  updateUndoRedo();
3722
5490
  };
5491
+ const getFirstDetectedPitch = () => {
5492
+ let minStep = Number.MAX_SAFE_INTEGER;
5493
+ let candidateNotes = [];
5494
+ for (const t of trackStates) {
5495
+ for (const note of t.core.getNotes()) {
5496
+ if (note.startStep < minStep) {
5497
+ minStep = note.startStep;
5498
+ candidateNotes = [note];
5499
+ } else if (note.startStep === minStep) {
5500
+ candidateNotes.push(note);
5501
+ }
5502
+ }
5503
+ }
5504
+ if (candidateNotes.length === 0) return null;
5505
+ const sum = candidateNotes.reduce((acc, note) => acc + note.pitch, 0);
5506
+ return Math.round(sum / candidateNotes.length);
5507
+ };
5508
+ const centerPitch = (pitch) => {
5509
+ const canvas = getGridCanvas();
5510
+ const yIndex = renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart);
5511
+ const logicalY = yIndex * renderConfig.keyHeight;
5512
+ currentOffsetY = clamp3(
5513
+ logicalY - (canvas.height - renderConfig.keyHeight) / 2,
5514
+ 0,
5515
+ getMaxOffsetY()
5516
+ );
5517
+ setDrawOffset(currentOffsetX, currentOffsetY);
5518
+ };
3723
5519
  const clearAll = () => {
3724
5520
  for (const t of trackStates) {
3725
5521
  t.core.resetHistory();
@@ -3731,8 +5527,45 @@ var mountDAW = (target, options = {}) => {
3731
5527
  if (!mml) return;
3732
5528
  clearAll();
3733
5529
  for (const t of trackStates) t.core.setLoadMode(true);
3734
- const { placements, bpm: parsedBpm } = parseMML(mml, {
3735
- stepsPerBar: renderConfig.stepsPerBar
5530
+ const {
5531
+ placements,
5532
+ bpm: parsedBpm,
5533
+ lyrics,
5534
+ meta
5535
+ } = parseMML(mml, {
5536
+ stepsPerBar: renderConfig.stepsPerBar,
5537
+ collectLyrics: true,
5538
+ // このDAWのトラック数を超えるチャンネルはベースへ畳み込む(従来挙動)
5539
+ clampTrackCount: trackStates.length
5540
+ });
5541
+ currentInstrument = meta.instrument ?? "";
5542
+ if (meta.drum && drumPatterns[meta.drum]) {
5543
+ currentDrumPattern = meta.drum;
5544
+ refs.drumSelect.value = meta.drum;
5545
+ options.onDrumChange?.(meta.drum);
5546
+ }
5547
+ if (meta.volume !== void 0) {
5548
+ masterVolume = meta.volume;
5549
+ refs.masterVolume.value = String(meta.volume);
5550
+ refs.masterVolumeLabel.textContent = `${meta.volume}%`;
5551
+ }
5552
+ for (const t of trackStates) {
5553
+ t.lyrics = "";
5554
+ t.lyricModel = "";
5555
+ t.vocalVolume = DEFAULT_VOCAL_VOLUME;
5556
+ t.vocalGate = 100;
5557
+ t.vocalPan = 64;
5558
+ t.vocalOctave = 0;
5559
+ }
5560
+ lyrics?.forEach((lt, idx) => {
5561
+ const t = trackStates[idx];
5562
+ if (!t) return;
5563
+ t.lyrics = lt.syllables.map((s) => s.kana).join("");
5564
+ t.lyricModel = lt.model;
5565
+ t.vocalVolume = lt.volume;
5566
+ t.vocalGate = lt.gate;
5567
+ t.vocalPan = lt.pan;
5568
+ t.vocalOctave = lt.octave ?? 0;
3736
5569
  });
3737
5570
  for (const p of placements) {
3738
5571
  const t = trackStates[p.trackIndex];
@@ -3748,8 +5581,14 @@ var mountDAW = (target, options = {}) => {
3748
5581
  }
3749
5582
  playStartStep = 0;
3750
5583
  currentOffsetX = 0;
3751
- setDrawOffset(currentOffsetX, currentOffsetY);
5584
+ const firstPitch = getFirstDetectedPitch();
5585
+ if (firstPitch !== null) {
5586
+ centerPitch(firstPitch);
5587
+ } else {
5588
+ setDrawOffset(currentOffsetX, currentOffsetY);
5589
+ }
3752
5590
  redrawAll();
5591
+ updateTrackPanel();
3753
5592
  updateUndoRedo();
3754
5593
  };
3755
5594
  const applyChord = () => {
@@ -3809,7 +5648,12 @@ var mountDAW = (target, options = {}) => {
3809
5648
  }
3810
5649
  playStartStep = 0;
3811
5650
  currentOffsetX = 0;
3812
- setDrawOffset(currentOffsetX, currentOffsetY);
5651
+ const firstPitch = getFirstDetectedPitch();
5652
+ if (firstPitch !== null) {
5653
+ centerPitch(firstPitch);
5654
+ } else {
5655
+ setDrawOffset(currentOffsetX, currentOffsetY);
5656
+ }
3813
5657
  redrawAll();
3814
5658
  updateUndoRedo();
3815
5659
  };
@@ -3873,25 +5717,32 @@ var mountDAW = (target, options = {}) => {
3873
5717
  refs.zoomXIn.addEventListener("click", () => {
3874
5718
  zoomX = Math.min(200, zoomX + 25);
3875
5719
  applyZoomX();
5720
+ notifyViewState();
3876
5721
  });
3877
5722
  refs.zoomXOut.addEventListener("click", () => {
3878
5723
  zoomX = Math.max(25, zoomX - 25);
3879
5724
  applyZoomX();
5725
+ notifyViewState();
3880
5726
  });
3881
5727
  refs.zoomYIn.addEventListener("click", () => {
3882
5728
  zoomY = Math.min(200, zoomY + 25);
3883
5729
  applyZoomY();
5730
+ notifyViewState();
3884
5731
  });
3885
5732
  refs.zoomYOut.addEventListener("click", () => {
3886
5733
  zoomY = Math.max(50, zoomY - 25);
3887
5734
  applyZoomY();
5735
+ notifyViewState();
3888
5736
  });
5737
+ refs.decomposeChordToggle.addEventListener("change", notifyViewState);
5738
+ refs.ignoreChordHeavyToggle.addEventListener("change", notifyViewState);
3889
5739
  refs.masterVolume.addEventListener("input", () => {
3890
5740
  masterVolume = Number.parseInt(refs.masterVolume.value, 10) || 0;
3891
5741
  refs.masterVolumeLabel.textContent = `${masterVolume}%`;
3892
5742
  });
3893
5743
  refs.drumSelect.addEventListener("change", () => {
3894
5744
  currentDrumPattern = refs.drumSelect.value;
5745
+ options.onDrumChange?.(currentDrumPattern);
3895
5746
  });
3896
5747
  refs.drumVolume.addEventListener("input", () => {
3897
5748
  drumVolume = Number.parseInt(refs.drumVolume.value, 10) || 0;
@@ -4081,6 +5932,33 @@ var mountDAW = (target, options = {}) => {
4081
5932
  pause,
4082
5933
  stop,
4083
5934
  getMML: generateMML,
5935
+ setInstrument: (name) => {
5936
+ currentInstrument = name;
5937
+ },
5938
+ getDrum: () => currentDrumPattern,
5939
+ setDrum: (name) => {
5940
+ if (name !== "none" && !drumPatterns[name]) return;
5941
+ currentDrumPattern = name;
5942
+ refs.drumSelect.value = name;
5943
+ options.onDrumChange?.(name);
5944
+ },
5945
+ getViewState,
5946
+ setViewState: (state) => {
5947
+ if (typeof state.zoomX === "number") {
5948
+ zoomX = clamp3(state.zoomX, 25, 200);
5949
+ applyZoomX();
5950
+ }
5951
+ if (typeof state.zoomY === "number") {
5952
+ zoomY = clamp3(state.zoomY, 50, 200);
5953
+ applyZoomY();
5954
+ }
5955
+ if (typeof state.decomposeChord === "boolean") {
5956
+ refs.decomposeChordToggle.checked = state.decomposeChord;
5957
+ }
5958
+ if (typeof state.ignoreChordHeavy === "boolean") {
5959
+ refs.ignoreChordHeavyToggle.checked = state.ignoreChordHeavy;
5960
+ }
5961
+ },
4084
5962
  loadMML,
4085
5963
  loadMIDI,
4086
5964
  exportMIDI: exportMIDI2,
@@ -4201,6 +6079,529 @@ var INSTRUMENT_PRESETS = {
4201
6079
  }
4202
6080
  };
4203
6081
 
6082
+ // src/mml-player.ts
6083
+ var STEPS_PER_BEAT3 = 48;
6084
+ var STEPS_PER_BAR = 192;
6085
+ var DEFAULT_TRACK_COLORS = ["#00e436", "#29adff", "#ff77a8", "#ffec27"];
6086
+ var activePlayer = null;
6087
+ var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
6088
+ var mountMmlPlayer = (target, mml, options = {}) => {
6089
+ injectStyles(target.ownerDocument ?? document);
6090
+ const {
6091
+ placements,
6092
+ bpm: parsedBpm,
6093
+ tokenTracks,
6094
+ lyrics,
6095
+ meta
6096
+ } = parseMML(mml, {
6097
+ collectTokens: true,
6098
+ collectLyrics: true
6099
+ });
6100
+ const lyricTracks = lyrics ?? /* @__PURE__ */ new Map();
6101
+ const bpm = parsedBpm ?? options.defaultBpm ?? DEFAULT_BPM;
6102
+ const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
6103
+ const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
6104
+ const trackVolume = meta.volume ?? options.volume ?? 100;
6105
+ const colors = options.trackColors ?? DEFAULT_TRACK_COLORS;
6106
+ const useSynth = options.synth ?? !options.onPlayNote;
6107
+ const secondsPerStep = 60 / bpm / STEPS_PER_BEAT3;
6108
+ const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
6109
+ (a, b) => a - b
6110
+ );
6111
+ const seqTracks = trackIndices.map((index) => {
6112
+ let id = 0;
6113
+ const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
6114
+ id: id++,
6115
+ startStep: p.startStep,
6116
+ durationSteps: p.durationSteps,
6117
+ pitch: p.pitch,
6118
+ velocity: 100
6119
+ }));
6120
+ return { id: String(index), volume: trackVolume, notes };
6121
+ });
6122
+ const colorOf = (index) => colors[index % colors.length] ?? DEFAULT_TRACK_COLORS[0];
6123
+ let audioCtx = null;
6124
+ const ensureCtx = () => {
6125
+ if (!audioCtx) audioCtx = new AudioContext();
6126
+ return audioCtx;
6127
+ };
6128
+ const synthPlay = (e) => {
6129
+ const ctx = ensureCtx();
6130
+ const osc = ctx.createOscillator();
6131
+ const gain = ctx.createGain();
6132
+ osc.type = "square";
6133
+ osc.frequency.value = freqFromPitch(e.pitch);
6134
+ const t0 = ctx.currentTime + e.when;
6135
+ const peak = Math.max(1e-4, 0.06 * e.volume * 1.5);
6136
+ gain.gain.setValueAtTime(peak, t0);
6137
+ gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
6138
+ osc.connect(gain);
6139
+ if (typeof ctx.createStereoPanner === "function" && e.pan) {
6140
+ const panner = ctx.createStereoPanner();
6141
+ panner.pan.value = Math.max(-1, Math.min(1, e.pan));
6142
+ gain.connect(panner);
6143
+ panner.connect(ctx.destination);
6144
+ } else {
6145
+ gain.connect(ctx.destination);
6146
+ }
6147
+ osc.start(t0);
6148
+ osc.stop(t0 + e.duration + 0.02);
6149
+ };
6150
+ const drumSynth = (e) => {
6151
+ const ctx = ensureCtx();
6152
+ const t0 = ctx.currentTime + e.when;
6153
+ const vol = Math.max(1e-4, Math.min(1, e.velocity));
6154
+ const isKick = e.pitch === 35 || e.pitch === 36;
6155
+ const isSnareLike = e.pitch === 38 || e.pitch === 39 || e.pitch === 40;
6156
+ if (isKick) {
6157
+ const osc = ctx.createOscillator();
6158
+ const g2 = ctx.createGain();
6159
+ osc.frequency.setValueAtTime(150, t0);
6160
+ osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
6161
+ g2.gain.setValueAtTime(vol * 0.9, t0);
6162
+ g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
6163
+ osc.connect(g2).connect(ctx.destination);
6164
+ osc.start(t0);
6165
+ osc.stop(t0 + 0.2);
6166
+ osc.onended = () => osc.disconnect();
6167
+ return;
6168
+ }
6169
+ const dur = isSnareLike ? 0.18 : 0.05;
6170
+ const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
6171
+ const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
6172
+ const data = buffer.getChannelData(0);
6173
+ for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
6174
+ const src = ctx.createBufferSource();
6175
+ src.buffer = buffer;
6176
+ const filter = ctx.createBiquadFilter();
6177
+ filter.type = isSnareLike ? "bandpass" : "highpass";
6178
+ filter.frequency.value = isSnareLike ? 2e3 : 8e3;
6179
+ const g = ctx.createGain();
6180
+ g.gain.setValueAtTime(vol * (isSnareLike ? 0.7 : 0.4), t0);
6181
+ g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
6182
+ src.connect(filter).connect(g).connect(ctx.destination);
6183
+ src.start(t0);
6184
+ src.stop(t0 + dur);
6185
+ src.onended = () => {
6186
+ src.disconnect();
6187
+ filter.disconnect();
6188
+ g.disconnect();
6189
+ };
6190
+ };
6191
+ let voices = null;
6192
+ const ensureVoices = () => {
6193
+ if (options.singingVoices) return options.singingVoices;
6194
+ if (!voices) {
6195
+ const ctx = ensureCtx();
6196
+ voices = createSingingVoices(ctx, ctx.destination);
6197
+ }
6198
+ return voices;
6199
+ };
6200
+ const voicesAvailable = useSynth || !!options.singingVoices;
6201
+ const peekVoices = () => options.singingVoices ?? voices;
6202
+ const getAudioTime = () => {
6203
+ if (useSynth) return ensureCtx().currentTime;
6204
+ return options.getAudioTime?.() ?? performance.now() / 1e3;
6205
+ };
6206
+ const doc = target.ownerDocument ?? document;
6207
+ const root = doc.createElement("div");
6208
+ root.className = "dtm-daw dtm-player";
6209
+ const head = doc.createElement("div");
6210
+ head.className = "dtm-player-head";
6211
+ const playBtn = doc.createElement("button");
6212
+ playBtn.type = "button";
6213
+ playBtn.className = "dtm-player-play";
6214
+ playBtn.innerHTML = icon("play", 12);
6215
+ playBtn.disabled = trackIndices.length === 0;
6216
+ const mmlHeader = doc.createElement("div");
6217
+ mmlHeader.className = "dtm-player-mml-header";
6218
+ const emojiEls = [];
6219
+ const emojiByTrack = /* @__PURE__ */ new Map();
6220
+ for (const index of trackIndices) {
6221
+ const em = doc.createElement("span");
6222
+ em.className = "dtm-player-emoji";
6223
+ em.style.backgroundColor = colorOf(index);
6224
+ em.textContent = "\u{1F97A}";
6225
+ mmlHeader.appendChild(em);
6226
+ emojiEls.push(em);
6227
+ emojiByTrack.set(index, em);
6228
+ }
6229
+ const promotedToImage = /* @__PURE__ */ new Set();
6230
+ for (const [index, lt] of lyricTracks) {
6231
+ const em = emojiByTrack.get(index);
6232
+ if (!em) continue;
6233
+ const imgKey = VOICE_IMAGE_KEY[lt.model.toLowerCase()];
6234
+ const src = imgKey ? VOICE_IMAGES[imgKey] : void 0;
6235
+ if (!src) continue;
6236
+ const img = doc.createElement("img");
6237
+ img.src = src;
6238
+ img.width = 20;
6239
+ img.height = 20;
6240
+ img.style.borderRadius = "50%";
6241
+ img.style.objectFit = "cover";
6242
+ img.draggable = false;
6243
+ promotedToImage.add(em);
6244
+ em.textContent = "";
6245
+ em.appendChild(img);
6246
+ }
6247
+ const JUMP_DEDUPE_MS = 50;
6248
+ const lastJumpAt = /* @__PURE__ */ new WeakMap();
6249
+ const jumpEmoji = (em) => {
6250
+ const now = performance.now();
6251
+ const prev = lastJumpAt.get(em);
6252
+ if (prev !== void 0 && now - prev < JUMP_DEDUPE_MS) return;
6253
+ lastJumpAt.set(em, now);
6254
+ em.classList.remove("dtm-player-emoji--jump");
6255
+ void em.offsetWidth;
6256
+ em.classList.add("dtm-player-emoji--jump");
6257
+ };
6258
+ const jumpTimers = [];
6259
+ const jumpEmojiAt = (em, when) => {
6260
+ if (when <= 0) {
6261
+ jumpEmoji(em);
6262
+ return;
6263
+ }
6264
+ jumpTimers.push(setTimeout(() => jumpEmoji(em), when * 1e3));
6265
+ };
6266
+ const clearJumpTimers = () => {
6267
+ for (const t of jumpTimers) clearTimeout(t);
6268
+ jumpTimers.length = 0;
6269
+ };
6270
+ const blinkTimers = [];
6271
+ const scheduleBlink = (em) => {
6272
+ const delay = 2e3 + Math.random() * 5e3;
6273
+ const t = setTimeout(() => {
6274
+ if (promotedToImage.has(em)) return;
6275
+ em.textContent = "\u{1F60C}";
6276
+ const t2 = setTimeout(
6277
+ () => {
6278
+ if (promotedToImage.has(em)) return;
6279
+ em.textContent = "\u{1F97A}";
6280
+ scheduleBlink(em);
6281
+ },
6282
+ 200 + Math.random() * 150
6283
+ );
6284
+ blinkTimers.push(t2);
6285
+ }, delay);
6286
+ blinkTimers.push(t);
6287
+ };
6288
+ for (const em of emojiEls) scheduleBlink(em);
6289
+ const dots = doc.createElement("div");
6290
+ dots.className = "dtm-player-dots";
6291
+ dots.style.display = "none";
6292
+ for (const index of trackIndices) {
6293
+ const dot = doc.createElement("span");
6294
+ dot.className = "dtm-player-dot";
6295
+ dot.style.backgroundColor = colorOf(index);
6296
+ dots.appendChild(dot);
6297
+ }
6298
+ const beatRow = doc.createElement("div");
6299
+ beatRow.className = "dtm-player-beat-row";
6300
+ const beatDots = [];
6301
+ for (let i = 0; i < 4; i++) {
6302
+ const d = doc.createElement("span");
6303
+ d.className = "dtm-player-beat-dot";
6304
+ beatRow.appendChild(d);
6305
+ beatDots.push(d);
6306
+ }
6307
+ const barEl = doc.createElement("span");
6308
+ barEl.className = "dtm-player-bar";
6309
+ barEl.textContent = "-";
6310
+ beatRow.appendChild(barEl);
6311
+ const chips = [];
6312
+ const makeChip = (label) => {
6313
+ const chip = doc.createElement("span");
6314
+ chip.className = "dtm-player-chip";
6315
+ chip.textContent = label;
6316
+ chips.push(chip);
6317
+ return chip;
6318
+ };
6319
+ if (meta.instrument) makeChip(`\u266A ${meta.instrument}`);
6320
+ if (meta.drum) makeChip(`\u{1F941} ${meta.drum}${drumPattern ? "" : " (?)"}`);
6321
+ if (meta.volume !== void 0) makeChip(`\u{1F50A} ${meta.volume}%`);
6322
+ head.append(playBtn, beatRow, ...chips, dots, mmlHeader);
6323
+ root.appendChild(head);
6324
+ const body = doc.createElement("div");
6325
+ body.className = "dtm-player-body";
6326
+ root.appendChild(body);
6327
+ const mutedTracks = /* @__PURE__ */ new Set();
6328
+ const laneViews = [];
6329
+ for (const index of trackIndices) {
6330
+ const lyricTrack = lyricTracks.get(index);
6331
+ const isLyricLane = !!lyricTrack && lyricTrack.syllables.length > 0;
6332
+ const row = doc.createElement("div");
6333
+ row.className = "dtm-player-lane-row";
6334
+ const label = doc.createElement("div");
6335
+ label.className = "dtm-player-lane-label dtm-player-lane-label--btn";
6336
+ const swatch = doc.createElement("span");
6337
+ swatch.className = "dtm-player-dot";
6338
+ swatch.style.backgroundColor = colorOf(index);
6339
+ const no = doc.createElement("span");
6340
+ no.className = "dtm-player-lane-no";
6341
+ no.textContent = `@${index}`;
6342
+ label.append(swatch, no);
6343
+ label.addEventListener("click", () => {
6344
+ if (mutedTracks.has(index)) {
6345
+ mutedTracks.delete(index);
6346
+ label.classList.remove("dtm-player-lane-label--muted");
6347
+ } else {
6348
+ mutedTracks.add(index);
6349
+ label.classList.add("dtm-player-lane-label--muted");
6350
+ }
6351
+ });
6352
+ const lane = doc.createElement("div");
6353
+ lane.className = "dtm-player-lane";
6354
+ lane.style.setProperty("--tk", colorOf(index));
6355
+ const laneTokens = [];
6356
+ if (isLyricLane) {
6357
+ const notes = placements.filter((p) => p.trackIndex === index).sort((a, b) => a.startStep - b.startStep);
6358
+ const gateScale = (lyricTrack.gate ?? DEFAULT_GATE) / 100;
6359
+ const breaks = new Set(lyricTrack.lineBreaks ?? []);
6360
+ if (lyricTrack.metaText) {
6361
+ const metaEl = doc.createElement("span");
6362
+ metaEl.className = "dtm-tk dtm-tk--meta";
6363
+ metaEl.textContent = lyricTrack.metaText;
6364
+ lane.appendChild(metaEl);
6365
+ }
6366
+ const count = Math.min(notes.length, lyricTrack.syllables.length);
6367
+ for (let i = 0; i < count; i++) {
6368
+ const note = notes[i];
6369
+ if (breaks.has(i)) {
6370
+ const br = doc.createElement("span");
6371
+ br.className = "dtm-tk dtm-tk--break";
6372
+ br.textContent = "\\n";
6373
+ lane.appendChild(br);
6374
+ }
6375
+ const span = doc.createElement("span");
6376
+ span.className = "dtm-tk dtm-tk--lyric";
6377
+ span.textContent = lyricTrack.syllables[i].kana;
6378
+ lane.appendChild(span);
6379
+ laneTokens.push({
6380
+ el: span,
6381
+ startStep: note.startStep,
6382
+ durationSteps: Math.max(
6383
+ 1,
6384
+ Math.round(note.durationSteps * gateScale)
6385
+ )
6386
+ });
6387
+ }
6388
+ } else {
6389
+ const tokens = tokenTracks?.get(index) ?? [];
6390
+ for (const tok of tokens) {
6391
+ const span = doc.createElement("span");
6392
+ span.className = `dtm-tk dtm-tk--${tok.type}`;
6393
+ span.textContent = tok.text;
6394
+ lane.appendChild(span);
6395
+ if (tok.durationSteps > 0) {
6396
+ laneTokens.push({
6397
+ el: span,
6398
+ startStep: tok.startStep,
6399
+ durationSteps: tok.durationSteps
6400
+ });
6401
+ }
6402
+ }
6403
+ }
6404
+ row.append(label, lane);
6405
+ body.appendChild(row);
6406
+ laneViews.push({ lane, tokens: laneTokens });
6407
+ }
6408
+ const termsModels = [
6409
+ ...new Set([...lyricTracks.values()].map((lt) => lt.model))
6410
+ ].filter((model) => KOE_VOICEBANK_TERMS[model]);
6411
+ if (termsModels.length > 0) {
6412
+ const termsDiv = doc.createElement("div");
6413
+ termsDiv.className = "dtm-player-terms";
6414
+ termsDiv.style.fontSize = "10px";
6415
+ termsDiv.style.color = "var(--dtm-warn)";
6416
+ termsDiv.style.display = "flex";
6417
+ termsDiv.style.flexDirection = "column";
6418
+ termsDiv.style.gap = "4px";
6419
+ termsDiv.style.marginTop = "4px";
6420
+ termsDiv.style.padding = "0 4px";
6421
+ for (const model of termsModels) {
6422
+ const termsRow = doc.createElement("div");
6423
+ termsRow.style.display = "flex";
6424
+ termsRow.style.alignItems = "center";
6425
+ termsRow.style.gap = "4px";
6426
+ termsRow.style.flexWrap = "wrap";
6427
+ const label = KOE_VOICEBANK_LABELS[model] ?? model;
6428
+ const url = KOE_VOICEBANK_TERMS[model];
6429
+ const span1 = doc.createElement("span");
6430
+ span1.textContent = "\u4F7F\u7528\u6642\u306B\u306F";
6431
+ const a = doc.createElement("a");
6432
+ a.textContent = `${label}UTAU\u97F3\u6E90`;
6433
+ a.href = url;
6434
+ a.target = "_blank";
6435
+ a.rel = "noopener";
6436
+ a.style.color = "var(--dtm-primary)";
6437
+ a.style.textDecoration = "underline";
6438
+ const span2 = doc.createElement("span");
6439
+ span2.textContent = "\u306E\u5229\u7528\u898F\u7D04\u306B\u5F93\u3063\u3066\u304F\u3060\u3055\u3044";
6440
+ termsRow.append(span1, a, span2);
6441
+ termsDiv.appendChild(termsRow);
6442
+ }
6443
+ root.appendChild(termsDiv);
6444
+ }
6445
+ target.appendChild(root);
6446
+ const autoScroll = (lane, el) => {
6447
+ if (el.offsetWidth === 0 || lane.clientWidth === 0) return;
6448
+ const elementCenter = el.offsetLeft + el.offsetWidth / 2;
6449
+ const maxScroll = Math.max(0, lane.scrollWidth - lane.clientWidth);
6450
+ const next = elementCenter - lane.clientWidth / 2;
6451
+ lane.scrollLeft = Math.max(0, Math.min(next, maxScroll));
6452
+ };
6453
+ const renderPlayhead = (step) => {
6454
+ const beatIndex = Math.floor(step / STEPS_PER_BEAT3) % 4;
6455
+ for (let i = 0; i < 4; i++)
6456
+ beatDots[i].classList.toggle("dtm-player-beat-dot--on", i === beatIndex);
6457
+ barEl.textContent = String(Math.floor(step / STEPS_PER_BAR) + 1);
6458
+ for (const view of laneViews) {
6459
+ let active = null;
6460
+ for (const t of view.tokens) {
6461
+ const on = step >= t.startStep && step < t.startStep + t.durationSteps;
6462
+ t.el.classList.toggle("is-active", on);
6463
+ if (on && !active) active = t;
6464
+ }
6465
+ if (active) autoScroll(view.lane, active.el);
6466
+ }
6467
+ };
6468
+ const resetPlayhead = () => {
6469
+ for (const d of beatDots) d.classList.remove("dtm-player-beat-dot--on");
6470
+ barEl.textContent = "-";
6471
+ for (const view of laneViews) {
6472
+ for (const t of view.tokens) t.el.classList.remove("is-active");
6473
+ view.lane.scrollLeft = 0;
6474
+ }
6475
+ };
6476
+ const seq = createSequencer({
6477
+ getTracks: () => seqTracks,
6478
+ getBpm: () => bpm,
6479
+ getPlayStartStep: () => 0,
6480
+ getDrumPattern: () => drumPattern,
6481
+ getSoloTrackId: () => null,
6482
+ getAudioTime,
6483
+ onPlayNote: (e) => {
6484
+ const trackIdx = Number(e.trackId);
6485
+ if (mutedTracks.has(trackIdx)) return;
6486
+ const em = emojiByTrack.get(trackIdx);
6487
+ if (em) jumpEmojiAt(em, e.when);
6488
+ if (lyricTracks.has(trackIdx)) return;
6489
+ options.onPlayNote?.(e);
6490
+ if (useSynth) synthPlay(e);
6491
+ },
6492
+ onPlayDrum: (e) => {
6493
+ const em = emojiEls[0];
6494
+ if (em) jumpEmojiAt(em, e.when);
6495
+ const velocity = e.velocity * (trackVolume / 100);
6496
+ options.onPlayDrum?.({ ...e, velocity });
6497
+ if (useSynth) drumSynth({ ...e, velocity });
6498
+ },
6499
+ onTick: (step) => {
6500
+ renderPlayhead(step);
6501
+ },
6502
+ onEnd: () => finish(),
6503
+ stepsPerBar: STEPS_PER_BAR
6504
+ });
6505
+ let playing = false;
6506
+ const setPlayingUI = (on) => {
6507
+ playing = on;
6508
+ playBtn.innerHTML = icon(on ? "stop" : "play", 12);
6509
+ playBtn.classList.toggle("dtm-player-play--stop", on);
6510
+ };
6511
+ const finish = () => {
6512
+ setPlayingUI(false);
6513
+ clearJumpTimers();
6514
+ resetPlayhead();
6515
+ if (activePlayer === instance) activePlayer = null;
6516
+ };
6517
+ const buildStreamTracks = () => [...lyricTracks.entries()].map(([index, lt]) => {
6518
+ const seqTrack = seqTracks.find((t) => Number(t.id) === index);
6519
+ const sorted = [...seqTrack?.notes ?? []].sort(
6520
+ (a, b) => a.startStep - b.startStep
6521
+ );
6522
+ const gate = (lt.gate ?? DEFAULT_GATE) / 100;
6523
+ const semis = (lt.octave ?? 0) * 12;
6524
+ const count = Math.min(sorted.length, lt.syllables.length);
6525
+ const notes = [];
6526
+ for (let i = 0; i < count; i++) {
6527
+ const n = sorted[i];
6528
+ notes.push({
6529
+ syllable: lt.syllables[i],
6530
+ pitch: n.pitch + semis,
6531
+ startSec: n.startStep * secondsPerStep,
6532
+ durationSec: n.durationSteps * secondsPerStep * gate
6533
+ });
6534
+ }
6535
+ return {
6536
+ model: lt.model,
6537
+ volume: vocalVolumeToGain(lt.volume ?? DEFAULT_VOCAL_VOLUME) * (trackVolume / 100),
6538
+ pan: panToStereo(lt.pan ?? DEFAULT_PAN),
6539
+ notes
6540
+ };
6541
+ });
6542
+ const startWhenReady = async () => {
6543
+ const streaming = voicesAvailable && lyricTracks.size > 0;
6544
+ const tracks = streaming ? buildStreamTracks() : [];
6545
+ if (streaming) {
6546
+ const v = ensureVoices();
6547
+ const overlay = showLoadingOverlay(body);
6548
+ try {
6549
+ await v.loadModels(tracks.map((t) => t.model));
6550
+ await v.warm(tracks);
6551
+ } catch (err) {
6552
+ console.warn("[dtm] voice preload failed", err);
6553
+ } finally {
6554
+ overlay.remove();
6555
+ }
6556
+ if (!playing || activePlayer !== instance) return;
6557
+ }
6558
+ seq.start(0);
6559
+ if (streaming) ensureVoices().startStream(tracks, seq.getStartTime());
6560
+ };
6561
+ const play = () => {
6562
+ if (playing || trackIndices.length === 0) return;
6563
+ if (activePlayer && activePlayer !== instance) activePlayer.stop();
6564
+ activePlayer = instance;
6565
+ setPlayingUI(true);
6566
+ void options.onResumeAudio?.();
6567
+ if (useSynth) {
6568
+ const ctx = ensureCtx();
6569
+ if (ctx.state === "suspended") void ctx.resume();
6570
+ }
6571
+ if (voicesAvailable && lyricTracks.size > 0) ensureVoices().reset();
6572
+ void startWhenReady();
6573
+ };
6574
+ const stop = () => {
6575
+ if (!playing) return;
6576
+ seq.stop();
6577
+ peekVoices()?.stopStream();
6578
+ finish();
6579
+ };
6580
+ playBtn.addEventListener("click", () => {
6581
+ if (playing) stop();
6582
+ else play();
6583
+ });
6584
+ const destroy = () => {
6585
+ seq.stop();
6586
+ peekVoices()?.stopStream();
6587
+ if (activePlayer === instance) activePlayer = null;
6588
+ if (audioCtx) {
6589
+ void audioCtx.close();
6590
+ audioCtx = null;
6591
+ }
6592
+ for (const t of blinkTimers) clearTimeout(t);
6593
+ clearJumpTimers();
6594
+ root.remove();
6595
+ };
6596
+ const instance = {
6597
+ play,
6598
+ stop,
6599
+ isPlaying: () => playing,
6600
+ destroy
6601
+ };
6602
+ return instance;
6603
+ };
6604
+
4204
6605
  // src/piano-roll.ts
4205
6606
  var createPianoRoll = (options, handlers) => {
4206
6607
  const {
@@ -4486,23 +6887,43 @@ var createPianoRoll = (options, handlers) => {
4486
6887
  // Annotate the CommonJS export names for ESM import in node:
4487
6888
  0 && (module.exports = {
4488
6889
  DAW_CSS,
6890
+ DEFAULT_BPM,
6891
+ DEFAULT_GATE,
6892
+ DEFAULT_PAN,
6893
+ DEFAULT_PLAYBACK_VELOCITY,
6894
+ DEFAULT_STEPS_PER_BAR,
6895
+ DEFAULT_VELOCITY,
6896
+ DEFAULT_VOCAL_VOLUME,
4489
6897
  DRUM_FONT,
4490
6898
  DRUM_KEYS,
4491
6899
  DRUM_PATTERNS,
4492
6900
  INSTRUMENT_PRESETS,
6901
+ KOE_BASE_URL,
6902
+ KOE_VOICEBANKS,
6903
+ KOE_VOICEBANK_LABELS,
6904
+ KOE_VOICEBANK_TERMS,
4493
6905
  LinkedList,
6906
+ MAX_VOCAL_VOLUME,
4494
6907
  MMLCore,
4495
6908
  PITCH_MAP,
6909
+ PREWARM_NOTES,
4496
6910
  TRACKS_ADVANCED,
4497
6911
  TRACKS_SIMPLE,
6912
+ VOICE_IMAGE_KEY,
4498
6913
  analyzeMidiTracks,
4499
6914
  applyHarmonicFilter,
4500
6915
  applyMonophonic,
4501
6916
  buildChordPlacements,
4502
6917
  buildNameToKeyMapping,
6918
+ collectPitchTokens,
4503
6919
  createAudioContext,
6920
+ createKlattVoice,
6921
+ createKoeVoice,
6922
+ createLyricsConductor,
4504
6923
  createPianoRoll,
4505
6924
  createSequencer,
6925
+ createSingingVoices,
6926
+ createVoiceRegistry,
4506
6927
  decomposeToMonophonic,
4507
6928
  drawGrid,
4508
6929
  drawHeader,
@@ -4514,6 +6935,7 @@ var createPianoRoll = (options, handlers) => {
4514
6935
  extractMidiPlacements,
4515
6936
  extractMidiPlacementsByTrack,
4516
6937
  fetchSoundFontList,
6938
+ formatMmlMeta,
4517
6939
  generateRandomPattern,
4518
6940
  getDrawOffset,
4519
6941
  getGridCanvas,
@@ -4527,10 +6949,19 @@ var createPianoRoll = (options, handlers) => {
4527
6949
  init,
4528
6950
  injectStyles,
4529
6951
  isChordHeavyTrack,
6952
+ koeUrl,
4530
6953
  mountDAW,
6954
+ mountMmlPlayer,
6955
+ normalizeLyrics,
4531
6956
  onClick,
6957
+ panToStereo,
6958
+ parseLyrics,
4532
6959
  parseMML,
6960
+ parseMmlMeta,
4533
6961
  setDrawOffset,
4534
6962
  setupRecorder,
4535
- shiftNotes
6963
+ shiftNotes,
6964
+ stripLyrics,
6965
+ stripMmlMeta,
6966
+ vocalVolumeToGain
4536
6967
  });