@onjmin/dtm 0.1.0 → 0.1.1

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
@@ -25,9 +25,14 @@ __export(index_exports, {
25
25
  DRUM_KEYS: () => DRUM_KEYS,
26
26
  DRUM_PATTERNS: () => DRUM_PATTERNS,
27
27
  INSTRUMENT_PRESETS: () => INSTRUMENT_PRESETS,
28
+ KOE_BASE_URL: () => KOE_BASE_URL,
29
+ KOE_VOICEBANKS: () => KOE_VOICEBANKS,
30
+ KOE_VOICEBANK_LABELS: () => KOE_VOICEBANK_LABELS,
28
31
  LinkedList: () => LinkedList,
32
+ MAX_VOCAL_VOLUME: () => MAX_VOCAL_VOLUME,
29
33
  MMLCore: () => MMLCore,
30
34
  PITCH_MAP: () => PITCH_MAP,
35
+ PREWARM_NOTES: () => PREWARM_NOTES,
31
36
  TRACKS_ADVANCED: () => TRACKS_ADVANCED,
32
37
  TRACKS_SIMPLE: () => TRACKS_SIMPLE,
33
38
  analyzeMidiTracks: () => analyzeMidiTracks,
@@ -35,9 +40,15 @@ __export(index_exports, {
35
40
  applyMonophonic: () => applyMonophonic,
36
41
  buildChordPlacements: () => buildChordPlacements,
37
42
  buildNameToKeyMapping: () => buildNameToKeyMapping,
43
+ collectPitchTokens: () => collectPitchTokens,
38
44
  createAudioContext: () => createAudioContext,
45
+ createKlattVoice: () => createKlattVoice,
46
+ createKoeVoice: () => createKoeVoice,
47
+ createLyricsConductor: () => createLyricsConductor,
39
48
  createPianoRoll: () => createPianoRoll,
40
49
  createSequencer: () => createSequencer,
50
+ createSingingVoices: () => createSingingVoices,
51
+ createVoiceRegistry: () => createVoiceRegistry,
41
52
  decomposeToMonophonic: () => decomposeToMonophonic,
42
53
  drawGrid: () => drawGrid,
43
54
  drawHeader: () => drawHeader,
@@ -49,6 +60,7 @@ __export(index_exports, {
49
60
  extractMidiPlacements: () => extractMidiPlacements,
50
61
  extractMidiPlacementsByTrack: () => extractMidiPlacementsByTrack,
51
62
  fetchSoundFontList: () => fetchSoundFontList,
63
+ formatMmlMeta: () => formatMmlMeta,
52
64
  generateRandomPattern: () => generateRandomPattern,
53
65
  getDrawOffset: () => getDrawOffset,
54
66
  getGridCanvas: () => getGridCanvas,
@@ -62,12 +74,21 @@ __export(index_exports, {
62
74
  init: () => init,
63
75
  injectStyles: () => injectStyles,
64
76
  isChordHeavyTrack: () => isChordHeavyTrack,
77
+ koeUrl: () => koeUrl,
65
78
  mountDAW: () => mountDAW,
79
+ mountMmlPlayer: () => mountMmlPlayer,
80
+ normalizeLyrics: () => normalizeLyrics,
66
81
  onClick: () => onClick,
82
+ panToStereo: () => panToStereo,
83
+ parseLyrics: () => parseLyrics,
67
84
  parseMML: () => parseMML,
85
+ parseMmlMeta: () => parseMmlMeta,
68
86
  setDrawOffset: () => setDrawOffset,
69
87
  setupRecorder: () => setupRecorder,
70
- shiftNotes: () => shiftNotes
88
+ shiftNotes: () => shiftNotes,
89
+ stripLyrics: () => stripLyrics,
90
+ stripMmlMeta: () => stripMmlMeta,
91
+ vocalVolumeToGain: () => vocalVolumeToGain
71
92
  });
72
93
  module.exports = __toCommonJS(index_exports);
73
94
 
@@ -263,7 +284,7 @@ var buildChordPlacements = (options) => {
263
284
  if (notes.length === 0) return;
264
285
  const startStep = barIndex * chordLength;
265
286
  notes.forEach((noteOffset, i) => {
266
- const stepOffset = i * 2;
287
+ const stepOffset = i * 3;
267
288
  placements.push({
268
289
  startStep: startStep + stepOffset,
269
290
  pitch: C3 + noteOffset + offset,
@@ -681,6 +702,1123 @@ var DRUM_PATTERNS = {
681
702
  ]
682
703
  };
683
704
 
705
+ // node_modules/.pnpm/@onjmin+koe@1.0.3/node_modules/@onjmin/koe/dist/index.js
706
+ var MAGIC = 1263486208;
707
+ function parseKoeHeader(headerBytes) {
708
+ const view = new DataView(headerBytes);
709
+ if (view.byteLength < 8 || view.getUint32(0, false) !== MAGIC) {
710
+ throw new Error("Not a .koe file (bad magic)");
711
+ }
712
+ return { jsonLength: view.getUint32(4, true) };
713
+ }
714
+ var pcmBase = (jsonLength) => 8 + jsonLength;
715
+ var BlobVoiceSource = class {
716
+ constructor(blob, base) {
717
+ this.blob = blob;
718
+ this.base = base;
719
+ }
720
+ blob;
721
+ base;
722
+ readBytes(offset, length) {
723
+ const start = this.base + offset;
724
+ return this.blob.slice(start, start + length).arrayBuffer();
725
+ }
726
+ };
727
+ var RangeVoiceSource = class {
728
+ constructor(url, base) {
729
+ this.url = url;
730
+ this.base = base;
731
+ }
732
+ url;
733
+ base;
734
+ async readBytes(offset, length) {
735
+ const start = this.base + offset;
736
+ const res = await fetch(this.url, {
737
+ headers: { Range: `bytes=${start}-${start + length - 1}` }
738
+ });
739
+ if (!res.ok && res.status !== 206) {
740
+ throw new Error(`.koe range request failed: ${res.status}`);
741
+ }
742
+ return res.arrayBuffer();
743
+ }
744
+ };
745
+ async function rangeFetch(url, start, length) {
746
+ const res = await fetch(url, {
747
+ headers: { Range: `bytes=${start}-${start + length - 1}` }
748
+ });
749
+ if (!res.ok && res.status !== 206)
750
+ throw new Error(`.koe fetch failed: ${res.status}`);
751
+ return res.arrayBuffer();
752
+ }
753
+ var VoiceBank = class _VoiceBank {
754
+ constructor(manifest, source) {
755
+ this.manifest = manifest;
756
+ this.source = source;
757
+ }
758
+ manifest;
759
+ source;
760
+ /**
761
+ * Parse a .koe archive header + manifest and bind a lazy PCM source.
762
+ * @param koe a Blob/File of the .koe archive, or a URL (served with Range support)
763
+ */
764
+ static async load(koe) {
765
+ if (typeof koe === "string") {
766
+ const header2 = await rangeFetch(koe, 0, 8);
767
+ const { jsonLength: jsonLength2 } = parseKoeHeader(header2);
768
+ const json2 = await rangeFetch(koe, 8, jsonLength2);
769
+ const manifest2 = JSON.parse(new TextDecoder().decode(json2));
770
+ return new _VoiceBank(
771
+ manifest2,
772
+ new RangeVoiceSource(koe, pcmBase(jsonLength2))
773
+ );
774
+ }
775
+ const header = await koe.slice(0, 8).arrayBuffer();
776
+ const { jsonLength } = parseKoeHeader(header);
777
+ const json = await koe.slice(8, 8 + jsonLength).arrayBuffer();
778
+ const manifest = JSON.parse(new TextDecoder().decode(json));
779
+ return new _VoiceBank(
780
+ manifest,
781
+ new BlobVoiceSource(koe, pcmBase(jsonLength))
782
+ );
783
+ }
784
+ /** True if the bank contains a phoneme under this alias. */
785
+ has(phoneme) {
786
+ return this.manifest.phonemes[phoneme] !== void 0;
787
+ }
788
+ /**
789
+ * Raw Int16 PCM bytes (48 kHz / mono) for a phoneme, or null if unknown.
790
+ * The returned ArrayBuffer is freshly allocated and safe to transfer to a
791
+ * worker / AudioWorklet.
792
+ */
793
+ async readPcmBytes(phoneme) {
794
+ const entry = this.manifest.phonemes[phoneme];
795
+ if (!entry) return null;
796
+ return this.source.readBytes(entry.offset, entry.length * 2);
797
+ }
798
+ /**
799
+ * A phoneme's PCM as a Float64Array normalised to [-1, 1], or null if unknown.
800
+ * Intended for external analysis / resynthesis such as the WORLD vocoder.
801
+ */
802
+ async getPcm(phoneme) {
803
+ const buf = await this.readPcmBytes(phoneme);
804
+ if (!buf) return null;
805
+ const int16 = new Int16Array(buf);
806
+ const f64 = new Float64Array(int16.length);
807
+ for (let i = 0; i < int16.length; i++) f64[i] = int16[i] / 32768;
808
+ return f64;
809
+ }
810
+ };
811
+ var WORLDLINE_SAMPLE_RATE = 48e3;
812
+ var MIN_WORLDLINE_SAMPLES = 4096;
813
+ var SYNTH_REQ_SIZE = 120;
814
+ var WL_FRAME_MS = 10;
815
+ var samplesToMs = (samples) => samples / WORLDLINE_SAMPLE_RATE * 1e3;
816
+ function leadInFromEntry(entry) {
817
+ return {
818
+ preMs: samplesToMs(entry.pre || 0),
819
+ consonantMs: samplesToMs(entry.consonant || 0)
820
+ };
821
+ }
822
+ var moduleCache = /* @__PURE__ */ new Map();
823
+ function injectScript(src) {
824
+ return new Promise((resolve, reject) => {
825
+ const existing = document.querySelector(
826
+ `script[data-koe-worldline="${src}"]`
827
+ );
828
+ if (existing) {
829
+ resolve();
830
+ return;
831
+ }
832
+ const s = document.createElement("script");
833
+ s.src = src;
834
+ s.dataset.koeWorldline = src;
835
+ s.onload = () => resolve();
836
+ s.onerror = () => reject(new Error(`worldline: failed to load ${src}`));
837
+ document.head.appendChild(s);
838
+ });
839
+ }
840
+ function loadWasm(scriptUrl) {
841
+ const cached = moduleCache.get(scriptUrl);
842
+ if (cached) return cached;
843
+ const baseUrl = scriptUrl.slice(0, scriptUrl.lastIndexOf("/") + 1);
844
+ const instantiate = () => {
845
+ const factory = globalThis.WorldlineModule;
846
+ if (!factory)
847
+ throw new Error(
848
+ "worldline: WorldlineModule global was not defined by the script"
849
+ );
850
+ return factory({ locateFile: (f) => baseUrl + f });
851
+ };
852
+ let promise;
853
+ if (typeof document !== "undefined") {
854
+ promise = injectScript(scriptUrl).then(instantiate);
855
+ } else if (typeof globalThis.importScripts === "function") {
856
+ promise = Promise.resolve().then(() => {
857
+ globalThis.importScripts(scriptUrl);
858
+ return instantiate();
859
+ });
860
+ } else {
861
+ return Promise.reject(
862
+ new Error(
863
+ "Worldline.load requires a DOM or a classic Web Worker (importScripts) to load worldline.js"
864
+ )
865
+ );
866
+ }
867
+ moduleCache.set(scriptUrl, promise);
868
+ return promise;
869
+ }
870
+ var Worldline = class _Worldline {
871
+ constructor(wasm) {
872
+ this.wasm = wasm;
873
+ }
874
+ wasm;
875
+ sampleRate = WORLDLINE_SAMPLE_RATE;
876
+ /**
877
+ * Load + instantiate the worldline WASM module (deduped per scriptUrl).
878
+ *
879
+ * Works on the main thread (loads via `<script>`) and inside a classic Web
880
+ * Worker (loads via `importScripts`), so the heavy synthesis can run
881
+ * off-thread. The matching `worldline.wasm` is fetched next to scriptUrl.
882
+ */
883
+ static async load(options) {
884
+ return new _Worldline(await loadWasm(options.scriptUrl));
885
+ }
886
+ /**
887
+ * Render one note to Float32 PCM at 48 kHz.
888
+ *
889
+ * The output buffer is laid out as [lead-in/consonant ≈ preMs][vowel ≈
890
+ * durationMs], rendered from sample offset 0 (no leading silence). The vowel
891
+ * onset (the "beat") sits at ≈ preMs into the buffer, so a sequencer should
892
+ * place the buffer at `beatTime − preMs` and may trim/crossfade the lead-in.
893
+ *
894
+ * No internal crossfade is applied — apply fades externally.
895
+ *
896
+ * @returns Float32 PCM, or null when `pcm` is shorter than
897
+ * {@link MIN_WORLDLINE_SAMPLES} (too short for stable F0 analysis).
898
+ */
899
+ renderNote(params) {
900
+ const { pcm, pitch, durationMs, preMs, consonantMs, tempo = 120 } = params;
901
+ if (!pcm || pcm.length < MIN_WORLDLINE_SAMPLES) return null;
902
+ const WL = this.wasm;
903
+ const FS = WORLDLINE_SAMPLE_RATE;
904
+ const midiNote = Math.round(69 + 12 * Math.log2(pitch / 440));
905
+ const posMs = 0;
906
+ const reqLen = preMs + durationMs;
907
+ const cutMs = WL_FRAME_MS * 2;
908
+ const ps = WL._PhraseSynthNew();
909
+ if (!ps) return null;
910
+ const reqPtr = WL._malloc(SYNTH_REQ_SIZE);
911
+ if (!reqPtr) {
912
+ WL._PhraseSynthDelete(ps);
913
+ return null;
914
+ }
915
+ const samplePtr = WL._malloc(pcm.length * 8);
916
+ if (!samplePtr) {
917
+ WL._free(reqPtr);
918
+ WL._PhraseSynthDelete(ps);
919
+ return null;
920
+ }
921
+ WL.HEAPF64.set(pcm, samplePtr >> 3);
922
+ const sv = (off, val, type) => WL.setValue(reqPtr + off, val, type);
923
+ sv(0, FS, "i32");
924
+ sv(4, pcm.length, "i32");
925
+ sv(8, samplePtr, "*");
926
+ sv(12, 0, "i32");
927
+ sv(16, 0, "*");
928
+ sv(20, midiNote, "i32");
929
+ sv(24, 100, "double");
930
+ sv(32, 0, "double");
931
+ sv(40, reqLen, "double");
932
+ sv(48, consonantMs, "double");
933
+ sv(56, cutMs, "double");
934
+ sv(64, 100, "double");
935
+ sv(72, 0, "double");
936
+ sv(80, tempo, "double");
937
+ sv(88, 0, "i32");
938
+ sv(92, 0, "*");
939
+ sv(96, 0, "i32");
940
+ sv(100, 0, "i32");
941
+ sv(104, 100, "i32");
942
+ sv(108, 0, "i32");
943
+ sv(112, 0, "i32");
944
+ sv(116, 100, "i32");
945
+ WL._PhraseSynthAddRequest(ps, reqPtr, posMs, 0, reqLen, 0, 0, 0);
946
+ WL._free(samplePtr);
947
+ WL._free(reqPtr);
948
+ const totalMs = posMs + reqLen + WL_FRAME_MS * 2;
949
+ const nFrames = Math.ceil(totalMs / WL_FRAME_MS) + 4;
950
+ const f0Arr = new Float64Array(nFrames).fill(pitch);
951
+ const gArr = new Float64Array(nFrames).fill(0.5);
952
+ const tArr = new Float64Array(nFrames).fill(0.5);
953
+ const bArr = new Float64Array(nFrames).fill(0.5);
954
+ const vArr = new Float64Array(nFrames).fill(1);
955
+ const f0Ptr = WL._malloc(nFrames * 8);
956
+ const gPtr = WL._malloc(nFrames * 8);
957
+ const tPtr = WL._malloc(nFrames * 8);
958
+ const bPtr = WL._malloc(nFrames * 8);
959
+ const vPtr = WL._malloc(nFrames * 8);
960
+ if (!f0Ptr || !gPtr || !tPtr || !bPtr || !vPtr) {
961
+ if (f0Ptr) WL._free(f0Ptr);
962
+ if (gPtr) WL._free(gPtr);
963
+ if (tPtr) WL._free(tPtr);
964
+ if (bPtr) WL._free(bPtr);
965
+ if (vPtr) WL._free(vPtr);
966
+ WL._PhraseSynthDelete(ps);
967
+ return null;
968
+ }
969
+ WL.HEAPF64.set(f0Arr, f0Ptr >> 3);
970
+ WL.HEAPF64.set(gArr, gPtr >> 3);
971
+ WL.HEAPF64.set(tArr, tPtr >> 3);
972
+ WL.HEAPF64.set(bArr, bPtr >> 3);
973
+ WL.HEAPF64.set(vArr, vPtr >> 3);
974
+ WL._PhraseSynthSetCurves(
975
+ ps,
976
+ f0Ptr,
977
+ gPtr,
978
+ tPtr,
979
+ bPtr,
980
+ vPtr,
981
+ nFrames,
982
+ WL_FRAME_MS
983
+ );
984
+ WL._free(f0Ptr);
985
+ WL._free(gPtr);
986
+ WL._free(tPtr);
987
+ WL._free(bPtr);
988
+ WL._free(vPtr);
989
+ const yPtrPtr = WL._malloc(4);
990
+ if (!yPtrPtr) {
991
+ WL._PhraseSynthDelete(ps);
992
+ return null;
993
+ }
994
+ const outLen = WL._PhraseSynthSynth(ps, yPtrPtr, 0);
995
+ const yPtr = WL.getValue(yPtrPtr, "*");
996
+ const audio = outLen > 0 ? new Float32Array(WL.HEAPF32.buffer, yPtr, outLen).slice() : null;
997
+ WL._free(yPtrPtr);
998
+ WL._PhraseSynthDelete(ps);
999
+ return audio;
1000
+ }
1001
+ };
1002
+
1003
+ // src/lyrics.ts
1004
+ var kanaTable = {
1005
+ \u3042: ["", "a"],
1006
+ \u3044: ["", "i"],
1007
+ \u3046: ["", "u"],
1008
+ \u3048: ["", "e"],
1009
+ \u304A: ["", "o"],
1010
+ \u304B: ["k", "a"],
1011
+ \u304D: ["k", "i"],
1012
+ \u304F: ["k", "u"],
1013
+ \u3051: ["k", "e"],
1014
+ \u3053: ["k", "o"],
1015
+ \u3055: ["s", "a"],
1016
+ \u3057: ["sh", "i"],
1017
+ \u3059: ["s", "u"],
1018
+ \u305B: ["s", "e"],
1019
+ \u305D: ["s", "o"],
1020
+ \u305F: ["t", "a"],
1021
+ \u3061: ["ch", "i"],
1022
+ \u3064: ["ts", "u"],
1023
+ \u3066: ["t", "e"],
1024
+ \u3068: ["t", "o"],
1025
+ \u306A: ["n", "a"],
1026
+ \u306B: ["n", "i"],
1027
+ \u306C: ["n", "u"],
1028
+ \u306D: ["n", "e"],
1029
+ \u306E: ["n", "o"],
1030
+ \u306F: ["h", "a"],
1031
+ \u3072: ["h", "i"],
1032
+ \u3075: ["f", "u"],
1033
+ \u3078: ["h", "e"],
1034
+ \u307B: ["h", "o"],
1035
+ \u307E: ["m", "a"],
1036
+ \u307F: ["m", "i"],
1037
+ \u3080: ["m", "u"],
1038
+ \u3081: ["m", "e"],
1039
+ \u3082: ["m", "o"],
1040
+ \u3084: ["y", "a"],
1041
+ \u3086: ["y", "u"],
1042
+ \u3088: ["y", "o"],
1043
+ \u3089: ["r", "a"],
1044
+ \u308A: ["r", "i"],
1045
+ \u308B: ["r", "u"],
1046
+ \u308C: ["r", "e"],
1047
+ \u308D: ["r", "o"],
1048
+ \u308F: ["w", "a"],
1049
+ \u3092: ["w", "o"],
1050
+ \u304C: ["g", "a"],
1051
+ \u304E: ["g", "i"],
1052
+ \u3050: ["g", "u"],
1053
+ \u3052: ["g", "e"],
1054
+ \u3054: ["g", "o"],
1055
+ \u3056: ["z", "a"],
1056
+ \u3058: ["j", "i"],
1057
+ \u305A: ["z", "u"],
1058
+ \u305C: ["z", "e"],
1059
+ \u305E: ["z", "o"],
1060
+ \u3060: ["d", "a"],
1061
+ \u3062: ["j", "i"],
1062
+ \u3065: ["z", "u"],
1063
+ \u3067: ["d", "e"],
1064
+ \u3069: ["d", "o"],
1065
+ \u3070: ["b", "a"],
1066
+ \u3073: ["b", "i"],
1067
+ \u3076: ["b", "u"],
1068
+ \u3079: ["b", "e"],
1069
+ \u307C: ["b", "o"],
1070
+ \u3071: ["p", "a"],
1071
+ \u3074: ["p", "i"],
1072
+ \u3077: ["p", "u"],
1073
+ \u307A: ["p", "e"],
1074
+ \u307D: ["p", "o"],
1075
+ \u3093: ["N", "N"]
1076
+ };
1077
+ var SMALL_KANA = "\u3041\u3043\u3045\u3047\u3049\u3083\u3085\u3087\u3063";
1078
+ var VOWEL_KANA = {
1079
+ a: "\u3042",
1080
+ i: "\u3044",
1081
+ u: "\u3046",
1082
+ e: "\u3048",
1083
+ o: "\u304A"
1084
+ };
1085
+ var sanitizeText = (text) => text.normalize("NFKC").replace(/[ァ-ヶ]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 96)).replace(/[^ぁ-ゖー]/g, "");
1086
+ var splitSyllables = (text) => {
1087
+ const result = [];
1088
+ for (const ch of text) {
1089
+ if (result.length > 0 && SMALL_KANA.includes(ch)) {
1090
+ result[result.length - 1] += ch;
1091
+ } else {
1092
+ result.push(ch);
1093
+ }
1094
+ }
1095
+ return result;
1096
+ };
1097
+ var kanaToVowel = (kana) => {
1098
+ if (/[ぁゃ]/.test(kana)) return "a";
1099
+ if (/[ぃ]/.test(kana)) return "i";
1100
+ if (/[ぅゅ]/.test(kana)) return "u";
1101
+ if (/[ぇ]/.test(kana)) return "e";
1102
+ if (/[ぉょ]/.test(kana)) return "o";
1103
+ if (/[あかさたなはまやらわがざだばぱ]/.test(kana)) return "a";
1104
+ if (/[いきしちにひみりぎじぢびぴ]/.test(kana)) return "i";
1105
+ if (/[うくすつぬふむゆるぐずづぶぷ]/.test(kana)) return "u";
1106
+ if (/[えけせてねへめれげぜでべぺ]/.test(kana)) return "e";
1107
+ if (/[おこそとのほもよろごぞどぼぽ]/.test(kana)) return "o";
1108
+ return "";
1109
+ };
1110
+ var analyzeSyllable = (syllable) => {
1111
+ if (syllable === "\u30FC") return { kana: syllable, consonant: "-", vowel: "-" };
1112
+ if (syllable === "\u3063") return { kana: syllable, consonant: "Q", vowel: "" };
1113
+ const head = syllable[0];
1114
+ const row = kanaTable[head];
1115
+ const consonant = row ? row[0] : "";
1116
+ let vowel = row ? row[1] : kanaToVowel(head);
1117
+ if (syllable.length === 2 && syllable[1] !== "\u3063") {
1118
+ const v = kanaToVowel(syllable[1]);
1119
+ if (v) vowel = v;
1120
+ }
1121
+ return { kana: syllable, consonant, vowel };
1122
+ };
1123
+ var resolveLongVowels = (syllables) => {
1124
+ const result = [];
1125
+ let prevVowel = "";
1126
+ for (const syl of syllables) {
1127
+ if (syl.consonant === "-") {
1128
+ if (!prevVowel) continue;
1129
+ result.push({
1130
+ kana: VOWEL_KANA[prevVowel] ?? syl.kana,
1131
+ consonant: "",
1132
+ vowel: prevVowel
1133
+ });
1134
+ continue;
1135
+ }
1136
+ if (syl.vowel && syl.vowel !== "N") prevVowel = syl.vowel;
1137
+ result.push(syl);
1138
+ }
1139
+ return result;
1140
+ };
1141
+ var normalizeLyrics = (text) => resolveLongVowels(splitSyllables(sanitizeText(text)).map(analyzeSyllable));
1142
+ var normalizeLyricLines = (lines) => {
1143
+ const syllables = [];
1144
+ const lineBreaks = [];
1145
+ for (const line of lines) {
1146
+ const part = normalizeLyrics(line);
1147
+ if (part.length === 0) continue;
1148
+ if (syllables.length > 0) lineBreaks.push(syllables.length);
1149
+ syllables.push(...part);
1150
+ }
1151
+ return { syllables, lineBreaks };
1152
+ };
1153
+ var LYRIC_LINE = /^@@(\d+)\s+(.*)$/;
1154
+ var isLyricContinuation = (seg) => !/^[@#]/.test(seg);
1155
+ var splitSegments = (mml) => mml.split(/[;\n\r]+/).map((s) => s.trim()).filter((s) => s.length > 0);
1156
+ var clamp = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
1157
+ var MAX_VOCAL_VOLUME = 400;
1158
+ var VOCAL_BOOST_DB_PER_PERCENT = 0.08;
1159
+ var vocalVolumeToGain = (v) => {
1160
+ if (v <= 0) return 0;
1161
+ if (v <= 100) return v / 100;
1162
+ return 10 ** ((v - 100) * VOCAL_BOOST_DB_PER_PERCENT / 20);
1163
+ };
1164
+ var parseLyrics = (mml) => {
1165
+ const tracks = /* @__PURE__ */ new Map();
1166
+ const segments = splitSegments(mml);
1167
+ for (let i = 0; i < segments.length; i++) {
1168
+ const m = segments[i].match(LYRIC_LINE);
1169
+ if (!m) continue;
1170
+ const trackId = Number.parseInt(m[1], 10);
1171
+ const tokens = m[2].trim().split(/\s+/);
1172
+ const modelToken = tokens.shift() ?? "";
1173
+ let volume = 300;
1174
+ let gate = 100;
1175
+ let pan = 64;
1176
+ const colon = modelToken.indexOf(":");
1177
+ const model = (colon === -1 ? modelToken : modelToken.slice(0, colon)).toLowerCase();
1178
+ if (colon !== -1) {
1179
+ const v = Number.parseInt(modelToken.slice(colon + 1), 10);
1180
+ if (Number.isFinite(v)) volume = clamp(v, 0, MAX_VOCAL_VOLUME);
1181
+ }
1182
+ const metaTokens = [modelToken];
1183
+ while (tokens.length > 0) {
1184
+ const v = /^v(\d+)$/.exec(tokens[0]);
1185
+ const q2 = /^q(\d+)$/.exec(tokens[0]);
1186
+ const p = /^p(\d+)$/.exec(tokens[0]);
1187
+ if (v) {
1188
+ volume = clamp(Number.parseInt(v[1], 10), 0, MAX_VOCAL_VOLUME);
1189
+ } else if (q2) {
1190
+ gate = clamp(Number.parseInt(q2[1], 10), 0, 100);
1191
+ } else if (p) {
1192
+ pan = clamp(Number.parseInt(p[1], 10), 0, 127);
1193
+ } else {
1194
+ break;
1195
+ }
1196
+ metaTokens.push(tokens.shift());
1197
+ }
1198
+ const lyricLines = [tokens.join(" ")];
1199
+ while (i + 1 < segments.length && isLyricContinuation(segments[i + 1])) {
1200
+ lyricLines.push(segments[++i]);
1201
+ }
1202
+ const { syllables, lineBreaks } = normalizeLyricLines(lyricLines);
1203
+ tracks.set(trackId, {
1204
+ trackId,
1205
+ model,
1206
+ volume,
1207
+ gate,
1208
+ pan,
1209
+ syllables,
1210
+ metaText: metaTokens.join(" "),
1211
+ ...lineBreaks.length > 0 ? { lineBreaks } : {}
1212
+ });
1213
+ }
1214
+ return tracks;
1215
+ };
1216
+ var stripLyrics = (mml) => {
1217
+ const segments = splitSegments(mml);
1218
+ const kept = [];
1219
+ for (let i = 0; i < segments.length; i++) {
1220
+ if (LYRIC_LINE.test(segments[i])) {
1221
+ while (i + 1 < segments.length && isLyricContinuation(segments[i + 1]))
1222
+ i++;
1223
+ continue;
1224
+ }
1225
+ kept.push(segments[i]);
1226
+ }
1227
+ return kept.join("\n");
1228
+ };
1229
+ var panToStereo = (pan) => Math.max(-1, Math.min(1, (pan - 64) / 64));
1230
+ var createLyricsConductor = (lyrics) => {
1231
+ const pointers = /* @__PURE__ */ new Map();
1232
+ const consume = (trackId) => {
1233
+ const track = lyrics.get(trackId);
1234
+ if (!track || track.syllables.length === 0) return null;
1235
+ const ptr = pointers.get(trackId) ?? 0;
1236
+ const syllable = track.syllables[ptr];
1237
+ if (!syllable) return null;
1238
+ pointers.set(trackId, ptr + 1);
1239
+ return {
1240
+ model: track.model,
1241
+ syllable,
1242
+ volume: vocalVolumeToGain(track.volume ?? 300),
1243
+ gate: (track.gate ?? 100) / 100,
1244
+ pan: panToStereo(track.pan ?? 64)
1245
+ };
1246
+ };
1247
+ const reset = () => pointers.clear();
1248
+ return { consume, reset };
1249
+ };
1250
+ var FORMANTS = {
1251
+ a: [800, 1200],
1252
+ i: [300, 2300],
1253
+ u: [350, 800],
1254
+ e: [500, 1900],
1255
+ o: [500, 900],
1256
+ // 撥音(ん)は鼻音寄りの低フォルマント
1257
+ N: [250, 1e3]
1258
+ };
1259
+ var midiToFreq = (m) => 440 * 2 ** ((m - 69) / 12);
1260
+ var createKlattVoice = (ctx, destination) => {
1261
+ const active = /* @__PURE__ */ new Set();
1262
+ const voice = (syllable, e) => {
1263
+ const t0 = ctx.currentTime + e.when;
1264
+ const peak = Math.max(1e-4, e.volume);
1265
+ if (syllable.vowel === "" || syllable.consonant === "Q") return;
1266
+ const [f1, f2] = FORMANTS[syllable.vowel] ?? FORMANTS.a;
1267
+ const attack = 0.02;
1268
+ const release = 0.06;
1269
+ const sustainEnd = t0 + Math.max(attack + 0.02, e.duration);
1270
+ let panner = null;
1271
+ let out = destination;
1272
+ if (typeof ctx.createStereoPanner === "function") {
1273
+ panner = ctx.createStereoPanner();
1274
+ panner.pan.value = Math.max(-1, Math.min(1, e.pan ?? 0));
1275
+ panner.connect(destination);
1276
+ out = panner;
1277
+ }
1278
+ const osc = ctx.createOscillator();
1279
+ osc.type = "sawtooth";
1280
+ osc.frequency.value = midiToFreq(e.pitch);
1281
+ const makeFormant = (freq, q2, gainScale) => {
1282
+ const filter = ctx.createBiquadFilter();
1283
+ filter.type = "bandpass";
1284
+ filter.frequency.value = freq;
1285
+ filter.Q.value = q2;
1286
+ const g = ctx.createGain();
1287
+ g.gain.value = gainScale;
1288
+ osc.connect(filter).connect(g);
1289
+ return g;
1290
+ };
1291
+ const env = ctx.createGain();
1292
+ env.gain.setValueAtTime(1e-4, t0);
1293
+ env.gain.exponentialRampToValueAtTime(peak, t0 + attack);
1294
+ env.gain.setValueAtTime(peak, sustainEnd);
1295
+ env.gain.exponentialRampToValueAtTime(1e-4, sustainEnd + release);
1296
+ const MAKEUP = 4;
1297
+ makeFormant(f1, 6, MAKEUP).connect(env);
1298
+ makeFormant(f2, 9, MAKEUP * 0.7).connect(env);
1299
+ env.connect(out);
1300
+ const fricatives = /* @__PURE__ */ new Set(["s", "sh", "ch", "ts", "h", "f"]);
1301
+ if (fricatives.has(syllable.consonant)) {
1302
+ const dur = 0.05;
1303
+ const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
1304
+ const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
1305
+ const data = buffer.getChannelData(0);
1306
+ for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
1307
+ const src = ctx.createBufferSource();
1308
+ src.buffer = buffer;
1309
+ const hp = ctx.createBiquadFilter();
1310
+ hp.type = "highpass";
1311
+ hp.frequency.value = syllable.consonant === "sh" ? 3e3 : 4500;
1312
+ const ng = ctx.createGain();
1313
+ ng.gain.setValueAtTime(peak * 0.5, t0);
1314
+ ng.gain.exponentialRampToValueAtTime(1e-4, t0 + dur);
1315
+ src.connect(hp).connect(ng).connect(out);
1316
+ src.start(t0);
1317
+ src.stop(t0 + dur);
1318
+ active.add(src);
1319
+ src.onended = () => {
1320
+ active.delete(src);
1321
+ src.disconnect();
1322
+ hp.disconnect();
1323
+ ng.disconnect();
1324
+ };
1325
+ }
1326
+ osc.start(t0);
1327
+ osc.stop(sustainEnd + release + 0.02);
1328
+ active.add(osc);
1329
+ osc.onended = () => {
1330
+ active.delete(osc);
1331
+ osc.disconnect();
1332
+ panner?.disconnect();
1333
+ };
1334
+ };
1335
+ voice.stopAll = () => {
1336
+ for (const n of active) {
1337
+ try {
1338
+ n.stop();
1339
+ } catch {
1340
+ }
1341
+ n.disconnect();
1342
+ }
1343
+ active.clear();
1344
+ };
1345
+ return voice;
1346
+ };
1347
+ var KOE_BASE_URL = "https://pub-12482a6b5cbc4c9e906b2e1904cabae5.r2.dev";
1348
+ var KOE_VOICEBANKS = {
1349
+ tsukuyomi: "\u3064\u304F\u3088\u307F\u3061\u3083\u3093.koe",
1350
+ rino: "\u6625\u97F3\u30EA\u30CEver0.3.koe",
1351
+ roze: "\u675F\u97F3\u30ED\u30BCver0.\uFF151(\u591A\u97F3\u968E).koe",
1352
+ ruko: "\u6B32\u97F3\u30EB\u30B3\u2640\u6B4C\u9023\u7D9A\u97F3\u666E1.00.koe",
1353
+ teto: "\u91CD\u97F3\u30C6\u30C8\u5358\u72EC\u97F3.koe",
1354
+ shiyo: "\u9769\u547D\u30B7\u30E8.koe"
1355
+ };
1356
+ var KOE_VOICEBANK_LABELS = {
1357
+ tsukuyomi: "\u3064\u304F\u3088\u307F\u3061\u3083\u3093",
1358
+ rino: "\u6625\u97F3\u30EA\u30CE",
1359
+ roze: "\u675F\u97F3\u30ED\u30BC",
1360
+ ruko: "\u6B32\u97F3\u30EB\u30B3",
1361
+ teto: "\u91CD\u97F3\u30C6\u30C8",
1362
+ shiyo: "\u9769\u547D\u30B7\u30E8"
1363
+ };
1364
+ var koeUrl = (name, base = KOE_BASE_URL) => `${base}/${encodeURIComponent(name)}`;
1365
+ var DEFAULT_WORLDLINE_SCRIPT = "https://onjmin.github.io/koe/demo/world/worldline.js";
1366
+ var KOE_SAMPLE_RATE = 48e3;
1367
+ var expandSeparators = (candidate) => candidate.includes(" ") ? [candidate, candidate.replace(/ /g, "\u3000"), candidate.replace(/ /g, "")] : [candidate];
1368
+ var PITCH_SUFFIX = /_([A-G][#b]?-?\d+)$/;
1369
+ var NAME_SEMITONE = {
1370
+ c: 0,
1371
+ d: 2,
1372
+ e: 4,
1373
+ f: 5,
1374
+ g: 7,
1375
+ a: 9,
1376
+ b: 11
1377
+ };
1378
+ var pitchTokenToMidi = (token) => {
1379
+ const m = /^([A-Ga-g])([#b]?)(-?\d+)$/.exec(token);
1380
+ if (!m) return null;
1381
+ let semi = NAME_SEMITONE[m[1].toLowerCase()];
1382
+ if (m[2] === "#") semi++;
1383
+ else if (m[2] === "b") semi--;
1384
+ return (Number.parseInt(m[3], 10) + 1) * 12 + semi;
1385
+ };
1386
+ var collectPitchTokens = (aliases) => {
1387
+ const seen = /* @__PURE__ */ new Map();
1388
+ for (const a of aliases) {
1389
+ const m = PITCH_SUFFIX.exec(a);
1390
+ if (!m || seen.has(m[1])) continue;
1391
+ const midi = pitchTokenToMidi(m[1]);
1392
+ if (midi != null) seen.set(m[1], midi);
1393
+ }
1394
+ return [...seen].map(([token, midi]) => ({ token, midi }));
1395
+ };
1396
+ var resolveKoeAlias = (hasAlias, pitchTokens, syl, prevVowel, noteNum) => {
1397
+ const kana = syl.kana;
1398
+ const cons = syl.consonant === "N" ? "n" : syl.consonant;
1399
+ const vow = syl.vowel === "N" ? "" : syl.vowel;
1400
+ const romaji = `${cons}${vow}` || vow;
1401
+ const pv = prevVowel || "-";
1402
+ const raw = [
1403
+ // 連続音(VCV): 直前母音つき
1404
+ `${pv} ${kana}`,
1405
+ `${pv} ${romaji}`,
1406
+ // 単独音 / CVVC
1407
+ kana,
1408
+ romaji
1409
+ ];
1410
+ const vk = VOWEL_KANA[syl.vowel];
1411
+ if (vk) raw.push(`${pv} ${vk}`, vk, syl.vowel);
1412
+ if (syl.vowel === "N") raw.push("\u3093", "n", "N", `${pv} \u3093`);
1413
+ const seen = /* @__PURE__ */ new Set();
1414
+ const tryAlias = (candidate) => {
1415
+ for (const v of expandSeparators(candidate)) {
1416
+ if (seen.has(v)) continue;
1417
+ seen.add(v);
1418
+ if (hasAlias(v)) return v;
1419
+ }
1420
+ return null;
1421
+ };
1422
+ if (pitchTokens.length) {
1423
+ const nearest = pitchTokens.slice().sort((a, b) => Math.abs(a.midi - noteNum) - Math.abs(b.midi - noteNum));
1424
+ for (const { token } of nearest) {
1425
+ for (const base of raw) {
1426
+ const hit = tryAlias(`${base}_${token}`);
1427
+ if (hit) return hit;
1428
+ }
1429
+ }
1430
+ }
1431
+ for (const base of raw) {
1432
+ const hit = tryAlias(base);
1433
+ if (hit) return hit;
1434
+ }
1435
+ return null;
1436
+ };
1437
+ var createLocalBackend = async (options) => {
1438
+ const bank = await VoiceBank.load(options.koe);
1439
+ const worldline = options.lightweight ? null : await Worldline.load({
1440
+ scriptUrl: options.worldlineScriptUrl ?? DEFAULT_WORLDLINE_SCRIPT
1441
+ }).catch(() => null);
1442
+ const pcmCache = /* @__PURE__ */ new Map();
1443
+ const getPcm = (alias) => {
1444
+ let p = pcmCache.get(alias);
1445
+ if (!p) {
1446
+ p = bank.getPcm(alias);
1447
+ pcmCache.set(alias, p);
1448
+ }
1449
+ return p;
1450
+ };
1451
+ const renderAlias = async (alias, pitch, durationMs) => {
1452
+ const pcm = await getPcm(alias);
1453
+ if (!pcm || pcm.length === 0) return null;
1454
+ const entry = bank.manifest.phonemes[alias];
1455
+ const lead = leadInFromEntry(entry);
1456
+ const targetHz = midiToFreq(pitch);
1457
+ if (worldline) {
1458
+ const audio = worldline.renderNote({
1459
+ pcm,
1460
+ pitch: targetHz,
1461
+ durationMs,
1462
+ ...lead
1463
+ });
1464
+ if (audio) return { pcm: audio, preSec: lead.preMs / 1e3, rate: 1 };
1465
+ }
1466
+ const rate = entry.pitch > 0 ? targetHz / entry.pitch : 1;
1467
+ return {
1468
+ pcm: Float32Array.from(pcm),
1469
+ preSec: entry.pre / KOE_SAMPLE_RATE / rate,
1470
+ rate
1471
+ };
1472
+ };
1473
+ return {
1474
+ hasAlias: (a) => bank.has(a),
1475
+ pitchTokens: collectPitchTokens(Object.keys(bank.manifest.phonemes)),
1476
+ renderAlias,
1477
+ dispose: () => {
1478
+ }
1479
+ };
1480
+ };
1481
+ var spawnVoiceWorker = async (url) => {
1482
+ const sameOrigin = new URL(url, location.href).origin === location.origin;
1483
+ if (sameOrigin) return new Worker(url);
1484
+ const text = await fetch(url).then((r) => r.text());
1485
+ return new Worker(
1486
+ URL.createObjectURL(new Blob([text], { type: "text/javascript" }))
1487
+ );
1488
+ };
1489
+ var createWorkerBackend = async (workerUrl, options) => {
1490
+ const worker = await spawnVoiceWorker(workerUrl);
1491
+ const aliasSet = /* @__PURE__ */ new Set();
1492
+ const pending = /* @__PURE__ */ new Map();
1493
+ let reqId = 0;
1494
+ let onReady = null;
1495
+ let onFail = null;
1496
+ worker.onmessage = (ev) => {
1497
+ const m = ev.data;
1498
+ if (m.type === "ready") {
1499
+ for (const a of m.aliases) aliasSet.add(a);
1500
+ onReady?.();
1501
+ } else if (m.type === "error") {
1502
+ onFail?.(new Error(m.message));
1503
+ } else if (m.type === "rendered") {
1504
+ const cb = pending.get(m.id);
1505
+ if (cb) {
1506
+ pending.delete(m.id);
1507
+ cb(m);
1508
+ }
1509
+ }
1510
+ };
1511
+ worker.onerror = (e) => onFail?.(
1512
+ new Error(`voice worker error: ${e.message || e}`)
1513
+ );
1514
+ await new Promise((resolve, reject) => {
1515
+ onReady = resolve;
1516
+ onFail = reject;
1517
+ worker.postMessage({
1518
+ type: "init",
1519
+ koe: options.koe,
1520
+ worldlineScriptUrl: options.worldlineScriptUrl ?? DEFAULT_WORLDLINE_SCRIPT,
1521
+ lightweight: !!options.lightweight
1522
+ });
1523
+ });
1524
+ onReady = null;
1525
+ onFail = null;
1526
+ const renderAlias = (alias, pitch, durationMs) => new Promise((resolve) => {
1527
+ const id = ++reqId;
1528
+ pending.set(
1529
+ id,
1530
+ (m) => resolve(
1531
+ m.pcm ? { pcm: m.pcm, preSec: m.preSec ?? 0, rate: m.rate ?? 1 } : null
1532
+ )
1533
+ );
1534
+ worker.postMessage({
1535
+ type: "render",
1536
+ id,
1537
+ alias,
1538
+ pitch,
1539
+ durationMs
1540
+ });
1541
+ });
1542
+ return {
1543
+ hasAlias: (a) => aliasSet.has(a),
1544
+ pitchTokens: collectPitchTokens(aliasSet),
1545
+ renderAlias,
1546
+ dispose: () => worker.terminate()
1547
+ };
1548
+ };
1549
+ var createKoeVoice = async (ctx, destination, options) => {
1550
+ const backend = options.voiceWorkerUrl ? await createWorkerBackend(options.voiceWorkerUrl, options) : await createLocalBackend(options);
1551
+ const renderCache = /* @__PURE__ */ new Map();
1552
+ const inflight = /* @__PURE__ */ new Map();
1553
+ const active = /* @__PURE__ */ new Set();
1554
+ let prevVowel = "";
1555
+ const keyOf = (alias, pitch, durationMs) => `${alias}|${pitch}|${Math.round(durationMs / 10) * 10}`;
1556
+ const renderInto = (alias, pitch, durationMs) => {
1557
+ const key = keyOf(alias, pitch, durationMs);
1558
+ const existing = renderCache.get(key);
1559
+ if (existing !== void 0) return Promise.resolve(existing);
1560
+ const flying = inflight.get(key);
1561
+ if (flying) return flying;
1562
+ const p = (async () => {
1563
+ const out = await backend.renderAlias(alias, pitch, durationMs);
1564
+ let rendered = null;
1565
+ if (out) {
1566
+ const buf = ctx.createBuffer(1, out.pcm.length, KOE_SAMPLE_RATE);
1567
+ buf.copyToChannel(out.pcm, 0);
1568
+ rendered = { audio: buf, preSec: out.preSec, rate: out.rate };
1569
+ }
1570
+ renderCache.set(key, rendered);
1571
+ inflight.delete(key);
1572
+ return rendered;
1573
+ })();
1574
+ inflight.set(key, p);
1575
+ return p;
1576
+ };
1577
+ const LEADCAP_S = 0.09;
1578
+ const schedule = (r, t0, peak, pan) => {
1579
+ let out = destination;
1580
+ let panner = null;
1581
+ if (typeof ctx.createStereoPanner === "function") {
1582
+ panner = ctx.createStereoPanner();
1583
+ panner.pan.value = Math.max(-1, Math.min(1, pan));
1584
+ panner.connect(destination);
1585
+ out = panner;
1586
+ }
1587
+ const src = ctx.createBufferSource();
1588
+ src.buffer = r.audio;
1589
+ src.playbackRate.value = r.rate;
1590
+ const effPre = Math.min(r.preSec, LEADCAP_S);
1591
+ const skipS = r.preSec - effPre;
1592
+ const startAt = Math.max(ctx.currentTime + 1e-3, t0 - effPre);
1593
+ const playDurSec = r.audio.duration / r.rate - skipS;
1594
+ const endAt = startAt + playDurSec;
1595
+ const attack = 0.01;
1596
+ const release = 0.04;
1597
+ const env = ctx.createGain();
1598
+ env.gain.setValueAtTime(1e-4, startAt);
1599
+ env.gain.exponentialRampToValueAtTime(peak, startAt + attack);
1600
+ const fadeStart = Math.max(startAt + attack, endAt - release);
1601
+ env.gain.setValueAtTime(peak, fadeStart);
1602
+ env.gain.exponentialRampToValueAtTime(1e-4, endAt);
1603
+ src.connect(env).connect(out);
1604
+ src.start(startAt, skipS);
1605
+ src.stop(endAt + 0.02);
1606
+ active.add(src);
1607
+ src.onended = () => {
1608
+ active.delete(src);
1609
+ src.disconnect();
1610
+ env.disconnect();
1611
+ panner?.disconnect();
1612
+ };
1613
+ };
1614
+ const model = (syllable, e) => {
1615
+ if (syllable.consonant === "Q" || syllable.vowel === "") return;
1616
+ const alias = resolveKoeAlias(
1617
+ backend.hasAlias,
1618
+ backend.pitchTokens,
1619
+ syllable,
1620
+ prevVowel,
1621
+ e.pitch
1622
+ );
1623
+ if (syllable.vowel && syllable.vowel !== "N") prevVowel = syllable.vowel;
1624
+ if (!alias) return;
1625
+ const t0 = ctx.currentTime + e.when;
1626
+ const peak = Math.max(1e-4, e.volume);
1627
+ const pan = e.pan ?? 0;
1628
+ const durationMs = Math.max(60, e.duration * 1e3);
1629
+ void renderInto(alias, e.pitch, durationMs).then((r) => {
1630
+ if (r) schedule(r, t0, peak, pan);
1631
+ });
1632
+ };
1633
+ model.renderToCache = async (syllable, prevVowelArg, pitch, durationMs) => {
1634
+ if (syllable.consonant === "Q" || syllable.vowel === "") return null;
1635
+ const alias = resolveKoeAlias(
1636
+ backend.hasAlias,
1637
+ backend.pitchTokens,
1638
+ syllable,
1639
+ prevVowelArg,
1640
+ pitch
1641
+ );
1642
+ if (!alias) return null;
1643
+ const dMs = Math.max(60, durationMs);
1644
+ const r = await renderInto(alias, pitch, dMs);
1645
+ return r ? keyOf(alias, pitch, dMs) : null;
1646
+ };
1647
+ model.scheduleCached = (key, t0, peak, pan) => {
1648
+ const r = renderCache.get(key);
1649
+ if (r) schedule(r, t0, peak, pan);
1650
+ };
1651
+ model.stopAll = () => {
1652
+ for (const src of active) {
1653
+ try {
1654
+ src.stop();
1655
+ } catch {
1656
+ }
1657
+ src.disconnect();
1658
+ }
1659
+ active.clear();
1660
+ };
1661
+ model.reset = () => {
1662
+ prevVowel = "";
1663
+ };
1664
+ return model;
1665
+ };
1666
+ var PREWARM_NOTES = 3;
1667
+ var STREAM_LOOKAHEAD_SEC = 1.5;
1668
+ var STREAM_POLL_MS = 100;
1669
+ var FALLBACK_MODEL = "klatt";
1670
+ var createSingingVoices = (ctx, destination, options = {}) => {
1671
+ const catalog = {};
1672
+ for (const [k, file] of Object.entries(KOE_VOICEBANKS))
1673
+ catalog[k] = koeUrl(file);
1674
+ for (const [k, v] of Object.entries(options.voicebanks ?? {}))
1675
+ catalog[k.toLowerCase()] = v;
1676
+ let streamSession = 0;
1677
+ const loaded = /* @__PURE__ */ new Map([
1678
+ [FALLBACK_MODEL, createKlattVoice(ctx, destination)]
1679
+ ]);
1680
+ const loading = /* @__PURE__ */ new Map();
1681
+ const load = (model) => {
1682
+ const m = model.toLowerCase();
1683
+ const ready = loaded.get(m);
1684
+ if (ready) return Promise.resolve(ready);
1685
+ const inflight = loading.get(m);
1686
+ if (inflight) return inflight;
1687
+ const koe = catalog[m];
1688
+ if (!koe) return Promise.resolve(null);
1689
+ const p = (async () => {
1690
+ let source = koe;
1691
+ if (typeof source === "string") {
1692
+ const res = await fetch(source);
1693
+ if (!res.ok) {
1694
+ throw new Error(`fetch failed: ${res.statusText}`);
1695
+ }
1696
+ source = await res.blob();
1697
+ }
1698
+ return createKoeVoice(ctx, destination, {
1699
+ koe: source,
1700
+ worldlineScriptUrl: options.worldlineScriptUrl,
1701
+ lightweight: options.lightweight,
1702
+ voiceWorkerUrl: options.voiceWorkerUrl
1703
+ });
1704
+ })().then((v) => {
1705
+ loaded.set(m, v);
1706
+ return v;
1707
+ }).catch((err) => {
1708
+ console.warn(`[dtm] koe\u97F3\u6E90 "${m}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F`, err);
1709
+ return null;
1710
+ });
1711
+ loading.set(m, p);
1712
+ return p;
1713
+ };
1714
+ const loadModels = async (models) => {
1715
+ const set = /* @__PURE__ */ new Set();
1716
+ for (const m of models) if (m) set.add(m.toLowerCase());
1717
+ await Promise.all([...set].map((m) => load(m)));
1718
+ };
1719
+ const forEachSungNote = (track, fn) => {
1720
+ let prevVowel = "";
1721
+ for (const note of track.notes) {
1722
+ const syl = note.syllable;
1723
+ if (syl.consonant === "Q" || syl.vowel === "") continue;
1724
+ fn(note, prevVowel);
1725
+ if (syl.vowel && syl.vowel !== "N") prevVowel = syl.vowel;
1726
+ }
1727
+ };
1728
+ const warm = async (tracks, count = PREWARM_NOTES) => {
1729
+ const promises = [];
1730
+ for (const track of tracks) {
1731
+ const m = loaded.get(track.model.toLowerCase());
1732
+ if (!m?.renderToCache) continue;
1733
+ let n = 0;
1734
+ forEachSungNote(track, (note, prevVowel) => {
1735
+ if (n >= count) return;
1736
+ n++;
1737
+ promises.push(
1738
+ m.renderToCache?.(
1739
+ note.syllable,
1740
+ prevVowel,
1741
+ note.pitch,
1742
+ note.durationSec * 1e3
1743
+ ) ?? Promise.resolve(null)
1744
+ );
1745
+ });
1746
+ }
1747
+ await Promise.all(promises);
1748
+ };
1749
+ const startStream = (tracks, anchorTime) => {
1750
+ const session = ++streamSession;
1751
+ const items = [];
1752
+ for (const track of tracks) {
1753
+ const model = loaded.get(track.model.toLowerCase());
1754
+ if (!model) continue;
1755
+ forEachSungNote(track, (note, prevVowel) => {
1756
+ items.push({
1757
+ model,
1758
+ note,
1759
+ prevVowel,
1760
+ volume: track.volume,
1761
+ pan: track.pan
1762
+ });
1763
+ });
1764
+ }
1765
+ items.sort((a, b) => a.note.startSec - b.note.startSec);
1766
+ void (async () => {
1767
+ for (const item of items) {
1768
+ if (session !== streamSession) return;
1769
+ while (item.note.startSec - (ctx.currentTime - anchorTime) > STREAM_LOOKAHEAD_SEC) {
1770
+ await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_MS));
1771
+ if (session !== streamSession) return;
1772
+ }
1773
+ const t0 = anchorTime + item.note.startSec;
1774
+ const peak = Math.max(1e-4, item.volume);
1775
+ const { model, note } = item;
1776
+ if (model.renderToCache && model.scheduleCached) {
1777
+ const key = await model.renderToCache(
1778
+ note.syllable,
1779
+ item.prevVowel,
1780
+ note.pitch,
1781
+ note.durationSec * 1e3
1782
+ );
1783
+ if (session !== streamSession) return;
1784
+ if (key) model.scheduleCached(key, t0, peak, item.pan);
1785
+ } else {
1786
+ const when = t0 - ctx.currentTime;
1787
+ model(note.syllable, {
1788
+ trackId: "",
1789
+ pitch: note.pitch,
1790
+ velocity: 100,
1791
+ volume: peak,
1792
+ when,
1793
+ duration: note.durationSec,
1794
+ pan: item.pan
1795
+ });
1796
+ }
1797
+ await new Promise((resolve) => setTimeout(resolve, 0));
1798
+ }
1799
+ })();
1800
+ };
1801
+ const stopStream = () => {
1802
+ streamSession++;
1803
+ for (const v of loaded.values()) v.stopAll?.();
1804
+ };
1805
+ const reset = () => {
1806
+ stopStream();
1807
+ for (const v of loaded.values()) v.reset?.();
1808
+ };
1809
+ return { loadModels, warm, startStream, stopStream, reset };
1810
+ };
1811
+ var createVoiceRegistry = (models = {}, fallback = "klatt") => {
1812
+ const sing = (model, syllable, e) => {
1813
+ const fn = models[model] ?? models[fallback];
1814
+ fn?.(syllable, e);
1815
+ };
1816
+ const register = (name, m) => {
1817
+ models[name.toLowerCase()] = m;
1818
+ };
1819
+ return { sing, register };
1820
+ };
1821
+
684
1822
  // src/macros.ts
685
1823
  var SCALES = [
686
1824
  [0, 2, 4, 5, 7, 9, 11],
@@ -1729,6 +2867,8 @@ var MMLCore = class _MMLCore {
1729
2867
  const config = getRenderConfig();
1730
2868
  const total = config.stepsPerBar;
1731
2869
  const candidates = [
2870
+ { dur: "1.", s: total * 1.5 },
2871
+ // 付点全音符(最長。これ以上はタイ未対応のため表現不可)
1732
2872
  { dur: "1", s: total / 1 },
1733
2873
  { dur: "2.", s: total / 2 * 1.5 },
1734
2874
  { dur: "2", s: total / 2 },
@@ -1789,97 +2929,77 @@ var MMLCore = class _MMLCore {
1789
2929
  return { text: `o${octave}${name}`, currentOctave: octave };
1790
2930
  }
1791
2931
  /**
1792
- * MML生成(1/2小節パターンスキャン方式)
2932
+ * MML生成(単一パス・発音順スキャン)
2933
+ *
2934
+ * 以前は1/2小節ごとのウィンドウで走査していたが、それだと半小節境界をまたぐ音符が
2935
+ * 境界で切り詰められて「ぶつ切り」になり、境界直前に始まる音符は隙間が潰れて欠落し、
2936
+ * 歌詞(@@n)の音節割り当てがずれていた。
2937
+ * 全ノートを発音順に一度で処理し、次の発音までの距離だけを上限として
2938
+ * 各音符の長さを忠実に出力する(次の音符がなければ曲末まで伸ばせる)。
1793
2939
  */
1794
2940
  generateMML = (volumeOverride) => {
1795
2941
  const config = getRenderConfig();
1796
2942
  const vol = volumeOverride ?? this.volume;
1797
- const HALF_BAR = config.stepsPerBar / 2;
1798
- const header = `t${this.tempo} q50 v${vol}`;
2943
+ const header = `t${this.tempo} v${vol}`;
1799
2944
  const segments = [];
1800
2945
  let lastOctave = -1;
1801
2946
  let currentCursor = 0;
1802
2947
  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;
2948
+ const endStep = Math.max(
2949
+ ...this.notes.map((n) => n.startStep + n.durationSteps)
2950
+ );
2951
+ const notesByStep = /* @__PURE__ */ new Map();
2952
+ for (const n of this.notes) {
2953
+ const list = notesByStep.get(n.startStep) ?? [];
2954
+ list.push(n);
2955
+ notesByStep.set(n.startStep, list);
2956
+ }
2957
+ const sortedSteps = Array.from(notesByStep.keys()).sort((a, b) => a - b);
2958
+ const fillRests = (until) => {
2959
+ while (currentCursor < until) {
2960
+ const gap = until - currentCursor;
1874
2961
  if (gap <= 2) {
1875
- currentCursor = windowEnd;
2962
+ currentCursor = until;
1876
2963
  break;
1877
2964
  }
1878
2965
  const { dur, steps } = this.findBestFitDuration(gap);
1879
2966
  segments.push(`r${dur}`);
1880
2967
  currentCursor += steps;
1881
2968
  }
2969
+ };
2970
+ const MIN_STEP = config.stepsPerBar / 64;
2971
+ for (let i = 0; i < sortedSteps.length; i++) {
2972
+ const startStep = sortedSteps[i];
2973
+ const notes = notesByStep.get(startStep);
2974
+ if (!notes) continue;
2975
+ fillRests(startStep);
2976
+ const nextStart = sortedSteps[i + 1] ?? endStep;
2977
+ const physicsLimit = nextStart - currentCursor;
2978
+ if (physicsLimit < MIN_STEP) {
2979
+ currentCursor = startStep;
2980
+ continue;
2981
+ }
2982
+ const idealDuration = notes[0].durationSteps;
2983
+ const durStr = this.stepsToMMLDuration(idealDuration, physicsLimit);
2984
+ const actualStepGenerated = this.getStepFromDottedMML(durStr);
2985
+ if (notes.length > 1) {
2986
+ const noteStrs = notes.map((n) => {
2987
+ const oct = Math.floor(n.pitch / 12) - 1;
2988
+ const name = PITCH_MAP[n.pitch % 12];
2989
+ return `o${oct}${name}`;
2990
+ });
2991
+ segments.push(`[${noteStrs.join("")}]${durStr}`);
2992
+ } else {
2993
+ const { text, currentOctave } = this.getNoteWithOctave(
2994
+ notes[0].pitch,
2995
+ lastOctave
2996
+ );
2997
+ segments.push(`${text}${durStr}`);
2998
+ lastOctave = currentOctave;
2999
+ }
3000
+ currentCursor += actualStepGenerated;
1882
3001
  }
3002
+ fillRests(endStep);
1883
3003
  return `${header} ${segments.join(" ")}`;
1884
3004
  };
1885
3005
  /**
@@ -1957,13 +3077,51 @@ var PITCH_MAP2 = {
1957
3077
  a: 9,
1958
3078
  b: 11
1959
3079
  };
1960
- var TRACK_INDEX_COUNT = 4;
3080
+ var clamp2 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
3081
+ var META_DIRECTIVE = /#(inst|drum|volume)=([\w-]+)/gi;
3082
+ var parseMmlMeta = (mml) => {
3083
+ const meta = {};
3084
+ for (const m of mml.matchAll(META_DIRECTIVE)) {
3085
+ const key = m[1].toLowerCase();
3086
+ if (key === "inst") meta.instrument = m[2];
3087
+ else if (key === "drum") meta.drum = m[2];
3088
+ else if (key === "volume") {
3089
+ const v = Number.parseInt(m[2], 10);
3090
+ if (!Number.isNaN(v)) meta.volume = v;
3091
+ }
3092
+ }
3093
+ return meta;
3094
+ };
3095
+ var stripMmlMeta = (mml) => mml.replace(META_DIRECTIVE, "");
3096
+ var formatMmlMeta = (meta) => {
3097
+ const parts = [];
3098
+ if (meta.instrument) parts.push(`#inst=${meta.instrument}`);
3099
+ if (meta.drum) parts.push(`#drum=${meta.drum}`);
3100
+ if (meta.volume !== void 0) parts.push(`#volume=${meta.volume}`);
3101
+ return parts.join(" ");
3102
+ };
1961
3103
  var parseMML = (mml, options = {}) => {
1962
3104
  const stepsPerBar = options.stepsPerBar ?? 192;
3105
+ const collectTokens = options.collectTokens ?? false;
3106
+ const collectLyrics = options.collectLyrics ?? false;
3107
+ const clampTrackCount = options.clampTrackCount;
1963
3108
  const placements = [];
3109
+ const tokenTracks = /* @__PURE__ */ new Map();
1964
3110
  let bpm = null;
1965
- if (!mml) return { placements, bpm };
1966
- const fullMML = mml.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").replace(/[\n\r]+/g, " ").trim();
3111
+ if (!mml) {
3112
+ return {
3113
+ placements,
3114
+ bpm,
3115
+ tokenTracks: collectTokens ? tokenTracks : void 0,
3116
+ lyrics: collectLyrics ? /* @__PURE__ */ new Map() : void 0,
3117
+ meta: {}
3118
+ };
3119
+ }
3120
+ const noComments = mml.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
3121
+ const meta = parseMmlMeta(noComments);
3122
+ const noMeta = stripMmlMeta(noComments);
3123
+ const lyrics = collectLyrics ? parseLyrics(noMeta) : void 0;
3124
+ const fullMML = stripLyrics(noMeta).replace(/[\n\r]+/g, " ").trim();
1967
3125
  const parts = fullMML.split(/(@\d+)/).filter((p) => p.trim().length > 0);
1968
3126
  let trackIndex = 0;
1969
3127
  let octave = 4;
@@ -1973,7 +3131,7 @@ var parseMML = (mml, options = {}) => {
1973
3131
  const part = rawPart.trim();
1974
3132
  if (part.startsWith("@")) {
1975
3133
  let idx = Number.parseInt(part.substring(1), 10);
1976
- if (idx >= TRACK_INDEX_COUNT) idx = 2;
3134
+ if (clampTrackCount !== void 0 && idx >= clampTrackCount) idx = 2;
1977
3135
  trackIndex = idx;
1978
3136
  octave = 4;
1979
3137
  currentStep = 0;
@@ -1982,13 +3140,27 @@ var parseMML = (mml, options = {}) => {
1982
3140
  }
1983
3141
  const body = part.replace(/\s+/g, "").toLowerCase();
1984
3142
  let j = 0;
3143
+ const pushTok = (type, start, dur, from) => {
3144
+ if (!collectTokens) return;
3145
+ let arr = tokenTracks.get(trackIndex);
3146
+ if (!arr) {
3147
+ arr = [];
3148
+ tokenTracks.set(trackIndex, arr);
3149
+ }
3150
+ arr.push({
3151
+ text: body.slice(from, j),
3152
+ startStep: start,
3153
+ durationSteps: dur,
3154
+ type
3155
+ });
3156
+ };
1985
3157
  const parseLength = () => {
1986
3158
  let numStr = "";
1987
3159
  while (j < body.length && /\d/.test(body[j])) {
1988
3160
  numStr += body[j];
1989
3161
  j++;
1990
3162
  }
1991
- const len = numStr ? Number.parseInt(numStr, 10) : baseLength;
3163
+ const len = numStr ? clamp2(Number.parseInt(numStr, 10), 1, 64) : baseLength;
1992
3164
  let steps = Math.round(stepsPerBar / len);
1993
3165
  while (j < body.length && body[j] === ".") {
1994
3166
  steps = Math.round(steps * 1.5);
@@ -1998,6 +3170,7 @@ var parseMML = (mml, options = {}) => {
1998
3170
  };
1999
3171
  while (j < body.length) {
2000
3172
  const ch = body[j];
3173
+ const tokStart = j;
2001
3174
  if (ch === "o") {
2002
3175
  j++;
2003
3176
  let numStr = "";
@@ -2005,13 +3178,16 @@ var parseMML = (mml, options = {}) => {
2005
3178
  numStr += body[j];
2006
3179
  j++;
2007
3180
  }
2008
- octave = Number.parseInt(numStr, 10) || 4;
3181
+ octave = clamp2(Number.parseInt(numStr, 10) || 4, 0, 8);
3182
+ pushTok("octave", currentStep, 0, tokStart);
2009
3183
  } else if (ch === ">") {
2010
3184
  octave++;
2011
3185
  j++;
3186
+ pushTok("shift", currentStep, 0, tokStart);
2012
3187
  } else if (ch === "<") {
2013
3188
  octave--;
2014
3189
  j++;
3190
+ pushTok("shift", currentStep, 0, tokStart);
2015
3191
  } else if (ch === "l") {
2016
3192
  j++;
2017
3193
  let numStr = "";
@@ -2019,11 +3195,15 @@ var parseMML = (mml, options = {}) => {
2019
3195
  numStr += body[j];
2020
3196
  j++;
2021
3197
  }
2022
- baseLength = Number.parseInt(numStr, 10) || 16;
3198
+ baseLength = clamp2(Number.parseInt(numStr, 10) || 16, 1, 64);
3199
+ pushTok("length", currentStep, 0, tokStart);
2023
3200
  } else if (ch === "r") {
2024
3201
  j++;
2025
- currentStep += parseLength();
2026
- } else if (ch === "t" || ch === "v" || ch === "q") {
3202
+ const restStart = currentStep;
3203
+ const restSteps = parseLength();
3204
+ pushTok("rest", restStart, restSteps, tokStart);
3205
+ currentStep += restSteps;
3206
+ } else if (ch === "t" || ch === "v" || ch === "q" || ch === "p") {
2027
3207
  j++;
2028
3208
  let numStr = "";
2029
3209
  while (j < body.length && /\d/.test(body[j])) {
@@ -2031,8 +3211,9 @@ var parseMML = (mml, options = {}) => {
2031
3211
  j++;
2032
3212
  }
2033
3213
  if (ch === "t" && trackIndex === 0 && numStr) {
2034
- bpm = Number.parseInt(numStr, 10);
3214
+ bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
2035
3215
  }
3216
+ pushTok("ctrl", currentStep, 0, tokStart);
2036
3217
  } else if (ch === "[") {
2037
3218
  j++;
2038
3219
  const chordNotes = [];
@@ -2063,7 +3244,7 @@ var parseMML = (mml, options = {}) => {
2063
3244
  numStr += body[j];
2064
3245
  j++;
2065
3246
  }
2066
- octave = Number.parseInt(numStr, 10) || 4;
3247
+ octave = clamp2(Number.parseInt(numStr, 10) || 4, 0, 8);
2067
3248
  } else {
2068
3249
  j++;
2069
3250
  }
@@ -2078,6 +3259,7 @@ var parseMML = (mml, options = {}) => {
2078
3259
  durationSteps: Math.max(1, steps)
2079
3260
  });
2080
3261
  }
3262
+ pushTok("chord", currentStep, Math.max(1, steps), tokStart);
2081
3263
  currentStep += steps;
2082
3264
  octave = savedOctave;
2083
3265
  } else if (Object.hasOwn(PITCH_MAP2, ch)) {
@@ -2098,18 +3280,25 @@ var parseMML = (mml, options = {}) => {
2098
3280
  pitch: midiPitch,
2099
3281
  durationSteps: Math.max(1, steps)
2100
3282
  });
3283
+ pushTok("note", currentStep, Math.max(1, steps), tokStart);
2101
3284
  currentStep += steps;
2102
3285
  } else {
2103
3286
  j++;
2104
3287
  }
2105
3288
  }
2106
3289
  }
2107
- return { placements, bpm };
3290
+ return {
3291
+ placements,
3292
+ bpm,
3293
+ tokenTracks: collectTokens ? tokenTracks : void 0,
3294
+ lyrics,
3295
+ meta
3296
+ };
2108
3297
  };
2109
3298
 
2110
3299
  // src/sequencer.ts
2111
3300
  var STEPS_PER_BEAT2 = 48;
2112
- var PLAN_TIME = 0.1;
3301
+ var PLAN_TIME = 0.5;
2113
3302
  var TICK_INTERVAL_MS = 20;
2114
3303
  var createSequencer = (options) => {
2115
3304
  let timeline = [];
@@ -2119,11 +3308,14 @@ var createSequencer = (options) => {
2119
3308
  let animationId = null;
2120
3309
  let active = false;
2121
3310
  let fromStepValue = 0;
3311
+ let trackVolumeMap = /* @__PURE__ */ new Map();
2122
3312
  const secondsPerStep = () => 60 / options.getBpm() / STEPS_PER_BEAT2;
2123
3313
  const buildTimeline = (fromStep) => {
2124
3314
  timeline = [];
3315
+ trackVolumeMap = /* @__PURE__ */ new Map();
2125
3316
  const sps = secondsPerStep();
2126
3317
  for (const track of options.getTracks()) {
3318
+ trackVolumeMap.set(track.id, track.volume);
2127
3319
  for (const note of track.notes) {
2128
3320
  const relativeStart = note.startStep - fromStep;
2129
3321
  if (relativeStart < 0) continue;
@@ -2144,6 +3336,9 @@ var createSequencer = (options) => {
2144
3336
  const sps = secondsPerStep();
2145
3337
  const time = options.getAudioTime() - startTime;
2146
3338
  const soloId = options.getSoloTrackId();
3339
+ for (const track of options.getTracks()) {
3340
+ trackVolumeMap.set(track.id, track.volume);
3341
+ }
2147
3342
  while (nowIndex < timeline.length) {
2148
3343
  const ev = timeline[nowIndex];
2149
3344
  if (soloId && ev.trackId !== soloId) {
@@ -2154,11 +3349,12 @@ var createSequencer = (options) => {
2154
3349
  if (_when > PLAN_TIME) break;
2155
3350
  nowIndex++;
2156
3351
  const velocityVolume = ev.velocity / 127;
3352
+ const currentVolume = (trackVolumeMap.get(ev.trackId) ?? ev.volume * 100) / 100;
2157
3353
  options.onPlayNote({
2158
3354
  trackId: ev.trackId,
2159
3355
  pitch: ev.pitch,
2160
3356
  velocity: ev.velocity,
2161
- volume: ev.volume * velocityVolume,
3357
+ volume: currentVolume * velocityVolume,
2162
3358
  when: Math.max(0, _when),
2163
3359
  duration: ev.duration
2164
3360
  });
@@ -2209,13 +3405,14 @@ var createSequencer = (options) => {
2209
3405
  }
2210
3406
  active = false;
2211
3407
  };
3408
+ const START_DELAY = 0.1;
2212
3409
  const start = (fromStep) => {
2213
3410
  stop();
2214
3411
  fromStepValue = fromStep ?? options.getPlayStartStep();
2215
3412
  buildTimeline(fromStepValue);
2216
3413
  if (timeline.length === 0 && !options.getDrumPattern()?.length) return;
2217
3414
  active = true;
2218
- startTime = options.getAudioTime();
3415
+ startTime = options.getAudioTime() + START_DELAY;
2219
3416
  nowIndex = 0;
2220
3417
  intervalId = setInterval(scheduleTick, TICK_INTERVAL_MS);
2221
3418
  animationId = requestAnimationFrame(animate);
@@ -2223,7 +3420,8 @@ var createSequencer = (options) => {
2223
3420
  return {
2224
3421
  start,
2225
3422
  stop,
2226
- isActive: () => active
3423
+ isActive: () => active,
3424
+ getStartTime: () => startTime
2227
3425
  };
2228
3426
  };
2229
3427
 
@@ -2685,10 +3883,12 @@ var DAW_CSS = `
2685
3883
 
2686
3884
  /* \u2500\u2500\u2500 \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 \u2500\u2500\u2500 */
2687
3885
  .dtm-overlay {
2688
- position: fixed; inset: 0; z-index: 1000;
3886
+ position: absolute; inset: 0; z-index: 1000;
2689
3887
  background: rgba(0,0,0,.92);
2690
3888
  display: flex; align-items: center; justify-content: center;
2691
3889
  flex-direction: column; gap: 14px;
3890
+ pointer-events: auto;
3891
+ cursor: wait;
2692
3892
  }
2693
3893
  .dtm-overlay[hidden] { display: none; }
2694
3894
  .dtm-overlay::before {
@@ -2716,6 +3916,22 @@ var DAW_CSS = `
2716
3916
  animation: dtm-load 1.6s steps(8) infinite;
2717
3917
  }
2718
3918
  @keyframes dtm-load { 0%{width:0} 100%{width:100%} }
3919
+ /* \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 */
3920
+ .dtm-spinner--determinate::after { display: none; }
3921
+ .dtm-spinner-fill {
3922
+ position: absolute;
3923
+ left: 0; top: 0; height: 100%;
3924
+ width: 0;
3925
+ background: var(--dtm-primary);
3926
+ transition: width .12s steps(8);
3927
+ }
3928
+ .dtm-loading-label {
3929
+ font-family: var(--dtm-font);
3930
+ font-size: 11px;
3931
+ color: var(--dtm-primary);
3932
+ letter-spacing: .15em;
3933
+ min-height: 1em;
3934
+ }
2719
3935
 
2720
3936
  @keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
2721
3937
  .dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
@@ -2728,6 +3944,112 @@ var DAW_CSS = `
2728
3944
  .dtm-daw { gap: 8px; padding: 10px; }
2729
3945
  .dtm-roll { height: 420px; }
2730
3946
  }
3947
+
3948
+ /* ====================================================
3949
+ MML PLAYER \u2014 \u518D\u751F\u5C02\u7528\u30D3\u30E5\u30FC\uFF08mountMmlPlayer\uFF09
3950
+ ==================================================== */
3951
+ .dtm-player {
3952
+ display: flex;
3953
+ flex-direction: column;
3954
+ gap: var(--dtm-gap);
3955
+ padding: var(--dtm-gap);
3956
+ background: var(--dtm-deep);
3957
+ border: 2px solid var(--dtm-border2);
3958
+ box-shadow: 4px 4px 0 var(--c-black);
3959
+ }
3960
+ .dtm-player-head {
3961
+ display: flex;
3962
+ align-items: center;
3963
+ gap: 8px;
3964
+ flex-wrap: wrap;
3965
+ }
3966
+ .dtm-player-play {
3967
+ flex: 0 0 auto;
3968
+ width: 28px;
3969
+ height: 28px;
3970
+ display: flex;
3971
+ align-items: center;
3972
+ justify-content: center;
3973
+ background: var(--dtm-primary);
3974
+ color: var(--dtm-pfg);
3975
+ border: 2px solid var(--c-black);
3976
+ box-shadow: 2px 2px 0 var(--c-black);
3977
+ cursor: pointer;
3978
+ padding: 0;
3979
+ }
3980
+ .dtm-player-play:active { transform: translate(2px, 2px); box-shadow: none; }
3981
+ .dtm-player-play--stop { background: var(--dtm-danger); }
3982
+ .dtm-player-play:disabled { opacity: 0.4; cursor: default; }
3983
+ .dtm-player-tempo,
3984
+ .dtm-player-time {
3985
+ font-family: 'k8x12', monospace;
3986
+ font-size: 12px;
3987
+ color: var(--dtm-muted);
3988
+ }
3989
+ .dtm-player-time { color: var(--dtm-text); min-width: 3em; }
3990
+ .dtm-player-dots {
3991
+ margin-left: auto;
3992
+ display: flex;
3993
+ align-items: center;
3994
+ gap: 6px;
3995
+ }
3996
+ .dtm-player-dot { width: 8px; height: 8px; display: inline-block; }
3997
+ .dtm-player-chip {
3998
+ font-family: 'k8x12', monospace;
3999
+ font-size: 9px;
4000
+ color: var(--dtm-text);
4001
+ background: var(--dtm-border2);
4002
+ padding: 2px 6px;
4003
+ white-space: nowrap;
4004
+ }
4005
+ .dtm-player-lane-row {
4006
+ display: flex;
4007
+ align-items: stretch;
4008
+ gap: 6px;
4009
+ }
4010
+ .dtm-player-lane-label {
4011
+ flex: 0 0 auto;
4012
+ width: 16px;
4013
+ display: flex;
4014
+ flex-direction: column;
4015
+ align-items: center;
4016
+ gap: 2px;
4017
+ padding-top: 4px;
4018
+ }
4019
+ .dtm-player-lane-no {
4020
+ font-family: 'k8x12', monospace;
4021
+ font-size: 9px;
4022
+ color: var(--dtm-muted);
4023
+ }
4024
+ .dtm-player-lane {
4025
+ position: relative; /* \u30C8\u30FC\u30AF\u30F3\u306E offsetParent \u3092\u30EC\u30FC\u30F3\u306B\u56FA\u5B9A\u3057\u3001\u4E2D\u592E\u5BC4\u305B\u8A08\u7B97\u3092\u6B63\u3059 */
4026
+ flex: 1 1 auto;
4027
+ overflow-x: auto;
4028
+ white-space: nowrap;
4029
+ background: var(--c-black);
4030
+ border: 2px solid var(--dtm-border2);
4031
+ padding: 6px;
4032
+ scrollbar-width: none;
4033
+ }
4034
+ .dtm-player-lane::-webkit-scrollbar { display: none; }
4035
+ .dtm-tk {
4036
+ font-family: 'k8x12', monospace;
4037
+ font-size: 12px;
4038
+ color: var(--dtm-text);
4039
+ }
4040
+ .dtm-tk--rest { color: var(--dtm-muted); }
4041
+ .dtm-tk--octave,
4042
+ .dtm-tk--shift,
4043
+ .dtm-tk--length,
4044
+ .dtm-tk--ctrl { color: var(--dtm-border2); }
4045
+ .dtm-tk--lyric { color: var(--dtm-text); letter-spacing: 1px; }
4046
+ .dtm-tk--break { color: var(--dtm-muted); opacity: 0.7; margin: 0 2px; }
4047
+ .dtm-tk--meta { color: var(--dtm-border2); margin-right: 4px; }
4048
+ .dtm-tk.is-active {
4049
+ background: var(--tk, var(--dtm-primary));
4050
+ color: var(--c-black);
4051
+ font-weight: bold;
4052
+ }
2731
4053
  `;
2732
4054
  var injectStyles = (doc = document) => {
2733
4055
  if (doc.getElementById(STYLE_ID)) return;
@@ -2736,6 +4058,45 @@ var injectStyles = (doc = document) => {
2736
4058
  style.textContent = DAW_CSS;
2737
4059
  doc.head.appendChild(style);
2738
4060
  };
4061
+ var showLoadingOverlay = (container) => {
4062
+ const origPos = container.style.position;
4063
+ const computed = window.getComputedStyle(container).position;
4064
+ if (computed === "static") {
4065
+ container.style.position = "relative";
4066
+ }
4067
+ const doc = container.ownerDocument ?? document;
4068
+ const overlay = doc.createElement("div");
4069
+ overlay.className = "dtm-overlay";
4070
+ const spinner = doc.createElement("div");
4071
+ spinner.className = "dtm-spinner";
4072
+ const fill = doc.createElement("i");
4073
+ fill.className = "dtm-spinner-fill";
4074
+ spinner.appendChild(fill);
4075
+ overlay.appendChild(spinner);
4076
+ const label = doc.createElement("div");
4077
+ label.className = "dtm-loading-label";
4078
+ overlay.appendChild(label);
4079
+ container.appendChild(overlay);
4080
+ const setProgress = (done, total) => {
4081
+ if (total > 0) {
4082
+ const pct = Math.max(0, Math.min(100, Math.round(done / total * 100)));
4083
+ spinner.classList.add("dtm-spinner--determinate");
4084
+ fill.style.width = `${pct}%`;
4085
+ label.textContent = `${done} / ${total} (${pct}%)`;
4086
+ } else {
4087
+ spinner.classList.remove("dtm-spinner--determinate");
4088
+ fill.style.width = "0";
4089
+ label.textContent = "";
4090
+ }
4091
+ };
4092
+ return {
4093
+ remove: () => {
4094
+ overlay.remove();
4095
+ container.style.position = origPos;
4096
+ },
4097
+ setProgress
4098
+ };
4099
+ };
2739
4100
 
2740
4101
  // src/daw.ts
2741
4102
  var BASE_STEP_WIDTH = 0.5;
@@ -2885,7 +4246,13 @@ var TRACKS_ADVANCED = [
2885
4246
  }
2886
4247
  ];
2887
4248
  var DEFAULT_TRACKS = TRACKS_SIMPLE;
2888
- var clamp = (v, min, max) => Math.min(Math.max(v, min), max);
4249
+ var LYRIC_MODELS = ["klatt", ...Object.keys(KOE_VOICEBANKS)];
4250
+ var LYRIC_MODEL_LABELS = {
4251
+ klatt: "\u8EFD\u91CF\u30ED\u30DC\u58F0",
4252
+ ...KOE_VOICEBANK_LABELS
4253
+ };
4254
+ var lyricModelLabel = (model) => LYRIC_MODEL_LABELS[model] ?? model;
4255
+ var clamp3 = (v, min, max) => Math.min(Math.max(v, min), max);
2889
4256
  var mountDAW = (target, options = {}) => {
2890
4257
  injectStyles();
2891
4258
  const getAudioTime = options.getAudioTime ?? (() => performance.now() / 1e3);
@@ -2916,6 +4283,7 @@ var mountDAW = (target, options = {}) => {
2916
4283
  let masterVolume = 50;
2917
4284
  let drumVolume = 80;
2918
4285
  let currentDrumPattern = refs.drumSelect.value;
4286
+ let currentInstrument = "";
2919
4287
  let activeTrackId = trackConfigs[0].id;
2920
4288
  let activeToolMode = "pen";
2921
4289
  let currentInsertLength = 48;
@@ -2925,6 +4293,7 @@ var mountDAW = (target, options = {}) => {
2925
4293
  let currentOffsetY = (104 - 1 - 60) * renderConfig.keyHeight - 215;
2926
4294
  let playStartStep = 0;
2927
4295
  let isSolo = false;
4296
+ let lyricTrackIndices = /* @__PURE__ */ new Set();
2928
4297
  let playbackState = "stopped";
2929
4298
  let pausedPlayStep = 0;
2930
4299
  let currentPlayStep = 0;
@@ -2951,9 +4320,34 @@ var mountDAW = (target, options = {}) => {
2951
4320
  volume: config.volume,
2952
4321
  savedChordInput: "",
2953
4322
  savedChordPattern: "block",
2954
- savedChordRoot: 0
4323
+ savedChordRoot: 0,
4324
+ lyrics: "",
4325
+ lyricModel: "",
4326
+ // 既定は「なし」(歌わない)
4327
+ vocalVolume: 300,
4328
+ vocalGate: 100,
4329
+ vocalPan: 64
2955
4330
  }));
2956
4331
  };
4332
+ const buildLyricsMap = () => {
4333
+ const map = /* @__PURE__ */ new Map();
4334
+ trackStates.forEach((t, i) => {
4335
+ const model = t.lyricModel.trim();
4336
+ const text = t.lyrics.trim();
4337
+ if (!model || !text) return;
4338
+ const syllables = normalizeLyrics(text);
4339
+ if (syllables.length === 0) return;
4340
+ map.set(i, {
4341
+ trackId: i,
4342
+ model: model.toLowerCase(),
4343
+ volume: t.vocalVolume,
4344
+ gate: t.vocalGate,
4345
+ pan: t.vocalPan,
4346
+ syllables
4347
+ });
4348
+ });
4349
+ return map;
4350
+ };
2957
4351
  const getActive = () => trackStates.find((t) => t.config.id === activeTrackId) ?? trackStates[0];
2958
4352
  const getMaxNoteStep = () => {
2959
4353
  let maxStep = renderConfig.stepsPerBar * 4;
@@ -3057,7 +4451,7 @@ var mountDAW = (target, options = {}) => {
3057
4451
  );
3058
4452
  const ratio = currentOffsetX / maxOffsetX;
3059
4453
  refs.hScrollThumb.style.width = `${thumbW}px`;
3060
- refs.hScrollThumb.style.left = `${clamp(ratio * (sbW - thumbW), 0, sbW - thumbW)}px`;
4454
+ refs.hScrollThumb.style.left = `${clamp3(ratio * (sbW - thumbW), 0, sbW - thumbW)}px`;
3061
4455
  }
3062
4456
  const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
3063
4457
  const sbH = refs.vScroll.clientHeight;
@@ -3116,9 +4510,9 @@ var mountDAW = (target, options = {}) => {
3116
4510
  if (maxOffsetX <= 0) return;
3117
4511
  const rect = refs.hScroll.getBoundingClientRect();
3118
4512
  const thumbW = Number.parseFloat(refs.hScrollThumb.style.width) || 40;
3119
- const x = clamp(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
4513
+ const x = clamp3(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
3120
4514
  const ratio = x / (rect.width - thumbW);
3121
- currentOffsetX = clamp(ratio * maxOffsetX, 0, maxOffsetX);
4515
+ currentOffsetX = clamp3(ratio * maxOffsetX, 0, maxOffsetX);
3122
4516
  setDrawOffset(currentOffsetX, currentOffsetY);
3123
4517
  redrawAll();
3124
4518
  };
@@ -3127,9 +4521,9 @@ var mountDAW = (target, options = {}) => {
3127
4521
  if (maxOffset <= 0) return;
3128
4522
  const rect = refs.vScroll.getBoundingClientRect();
3129
4523
  const thumbH = Number.parseFloat(refs.vScrollThumb.style.height) || 40;
3130
- const y = clamp(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
4524
+ const y = clamp3(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
3131
4525
  const ratio = y / (rect.height - thumbH);
3132
- currentOffsetY = clamp(ratio * maxOffset, 0, maxOffset);
4526
+ currentOffsetY = clamp3(ratio * maxOffset, 0, maxOffset);
3133
4527
  setDrawOffset(currentOffsetX, currentOffsetY);
3134
4528
  redrawAll();
3135
4529
  };
@@ -3411,7 +4805,7 @@ var mountDAW = (target, options = {}) => {
3411
4805
  "wheel",
3412
4806
  (event) => {
3413
4807
  event.preventDefault();
3414
- currentOffsetY = clamp(
4808
+ currentOffsetY = clamp3(
3415
4809
  currentOffsetY + event.deltaY,
3416
4810
  0,
3417
4811
  getMaxOffsetY()
@@ -3459,7 +4853,7 @@ var mountDAW = (target, options = {}) => {
3459
4853
  const centerKey = (currentOffsetY + canvas.height / 2) / renderConfig.keyHeight;
3460
4854
  renderConfig.keyHeight = BASE_KEY_HEIGHT * zoomY / 100;
3461
4855
  refs.zoomYLabel.textContent = `${zoomY}%`;
3462
- currentOffsetY = clamp(
4856
+ currentOffsetY = clamp3(
3463
4857
  centerKey * renderConfig.keyHeight - canvas.height / 2,
3464
4858
  0,
3465
4859
  getMaxOffsetY()
@@ -3483,8 +4877,11 @@ var mountDAW = (target, options = {}) => {
3483
4877
  getSoloTrackId: () => isSolo ? activeTrackId : null,
3484
4878
  getAudioTime,
3485
4879
  onPlayNote: (e) => {
3486
- const volume = e.volume * (masterVolume / 100);
3487
- options.onPlayNote?.({ ...e, volume });
4880
+ const idx = trackStates.findIndex((t) => t.config.id === e.trackId);
4881
+ if (idx >= 0 && lyricTrackIndices.has(idx) && options.singingVoices) {
4882
+ return;
4883
+ }
4884
+ options.onPlayNote?.({ ...e, volume: e.volume * (masterVolume / 100) });
3488
4885
  },
3489
4886
  onPlayDrum: (e) => {
3490
4887
  const velocity = e.velocity * (drumVolume / 100) * (masterVolume / 100);
@@ -3510,10 +4907,52 @@ var mountDAW = (target, options = {}) => {
3510
4907
  },
3511
4908
  stepsPerBar: renderConfig.stepsPerBar
3512
4909
  });
3513
- const play = () => {
4910
+ const play = async () => {
3514
4911
  options.onResumeAudio?.();
3515
4912
  if (playbackState === "playing") return;
3516
4913
  const fromStep = playbackState === "paused" ? pausedPlayStep : playStartStep;
4914
+ options.singingVoices?.reset();
4915
+ const lyricMap = buildLyricsMap();
4916
+ lyricTrackIndices = new Set(lyricMap.keys());
4917
+ const secondsPerStep = 60 / bpm / 48;
4918
+ const streamTracks = options.singingVoices ? [...lyricMap.values()].map((lt) => {
4919
+ const trackState = trackStates[lt.trackId];
4920
+ const sorted = [...trackState?.core.getNotes() ?? []].sort(
4921
+ (a, b) => a.startStep - b.startStep
4922
+ );
4923
+ const gate = (lt.gate ?? 100) / 100;
4924
+ const count = Math.min(sorted.length, lt.syllables.length);
4925
+ const notes = [];
4926
+ for (let i = 0; i < count; i++) {
4927
+ const n = sorted[i];
4928
+ if (n.startStep < fromStep) continue;
4929
+ notes.push({
4930
+ syllable: lt.syllables[i],
4931
+ pitch: n.pitch,
4932
+ startSec: (n.startStep - fromStep) * secondsPerStep,
4933
+ durationSec: n.durationSteps * secondsPerStep * gate
4934
+ });
4935
+ }
4936
+ return {
4937
+ model: lt.model,
4938
+ volume: vocalVolumeToGain(lt.volume ?? 300) * (masterVolume / 100),
4939
+ pan: panToStereo(lt.pan ?? 64),
4940
+ notes
4941
+ };
4942
+ }) : [];
4943
+ const voices = options.singingVoices;
4944
+ const streaming = !!voices && streamTracks.some((t) => t.notes.length > 0);
4945
+ if (streaming && voices) {
4946
+ const overlay = showLoadingOverlay(target);
4947
+ try {
4948
+ await voices.loadModels(streamTracks.map((t) => t.model));
4949
+ await voices.warm(streamTracks);
4950
+ } catch (err) {
4951
+ console.warn("[dtm] voice preload failed", err);
4952
+ } finally {
4953
+ overlay.remove();
4954
+ }
4955
+ }
3517
4956
  if (playbackState !== "paused") {
3518
4957
  const canvas = getGridCanvas();
3519
4958
  currentOffsetX = Math.max(
@@ -3524,17 +4963,22 @@ var mountDAW = (target, options = {}) => {
3524
4963
  }
3525
4964
  playbackState = "playing";
3526
4965
  sequencer.start(fromStep);
4966
+ if (streaming && voices) {
4967
+ voices.startStream(streamTracks, sequencer.getStartTime());
4968
+ }
3527
4969
  updateTransport();
3528
4970
  };
3529
4971
  const pause = () => {
3530
4972
  if (playbackState !== "playing") return;
3531
4973
  pausedPlayStep = currentPlayStep;
3532
4974
  sequencer.stop();
4975
+ options.singingVoices?.stopStream();
3533
4976
  playbackState = "paused";
3534
4977
  updateTransport();
3535
4978
  };
3536
4979
  const stop = () => {
3537
4980
  sequencer.stop();
4981
+ options.singingVoices?.stopStream();
3538
4982
  playbackState = "stopped";
3539
4983
  currentPlayStep = 0;
3540
4984
  updateTransport();
@@ -3584,6 +5028,104 @@ var mountDAW = (target, options = {}) => {
3584
5028
  active.core.setVolume(active.volume);
3585
5029
  volLabel.textContent = String(active.volume);
3586
5030
  });
5031
+ const lyricDiv = document.createElement("div");
5032
+ lyricDiv.className = "dtm-row";
5033
+ lyricDiv.style.flexDirection = "column";
5034
+ lyricDiv.style.alignItems = "stretch";
5035
+ lyricDiv.innerHTML = `
5036
+ <div class="dtm-row">
5037
+ <span class="dtm-label">\u266A \u6B4C\u8A5E</span>
5038
+ <select class="dtm-select" data-dtm="lyric-model" aria-label="\u6B4C\u5531\u30E2\u30C7\u30EB"></select>
5039
+ <span class="dtm-label dtm-grow" data-dtm="lyric-count" style="text-align:right"></span>
5040
+ </div>
5041
+ <div class="dtm-row" data-dtm="lyric-body" style="flex-direction:column;align-items:stretch">
5042
+ <div class="dtm-row">
5043
+ <span class="dtm-label">\u58F0\u91CF</span>
5044
+ <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\u5B9A300\uFF09">
5045
+ <span class="dtm-label" data-dtm="lyric-vol-label"></span>
5046
+ </div>
5047
+ <div class="dtm-row">
5048
+ <span class="dtm-label">\u5B9A\u4F4D</span>
5049
+ <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">
5050
+ <span class="dtm-label" data-dtm="lyric-pan-label"></span>
5051
+ </div>
5052
+ <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>
5053
+ </div>`;
5054
+ refs.trackBody.appendChild(lyricDiv);
5055
+ const lyricModelSel = lyricDiv.querySelector(
5056
+ '[data-dtm="lyric-model"]'
5057
+ );
5058
+ const lyricBody = lyricDiv.querySelector(
5059
+ '[data-dtm="lyric-body"]'
5060
+ );
5061
+ const lyricInput = lyricDiv.querySelector(
5062
+ '[data-dtm="lyric-input"]'
5063
+ );
5064
+ const lyricCount = lyricDiv.querySelector(
5065
+ '[data-dtm="lyric-count"]'
5066
+ );
5067
+ const lyricVol = lyricDiv.querySelector(
5068
+ '[data-dtm="lyric-vol"]'
5069
+ );
5070
+ const lyricVolLabel = lyricDiv.querySelector(
5071
+ '[data-dtm="lyric-vol-label"]'
5072
+ );
5073
+ const lyricPan = lyricDiv.querySelector(
5074
+ '[data-dtm="lyric-pan"]'
5075
+ );
5076
+ const lyricPanLabel = lyricDiv.querySelector(
5077
+ '[data-dtm="lyric-pan-label"]'
5078
+ );
5079
+ const fmtPan = (pan) => pan === 64 ? "C" : pan < 64 ? `L${64 - pan}` : `R${pan - 64}`;
5080
+ const addOpt = (value, label) => {
5081
+ const o = document.createElement("option");
5082
+ o.value = value;
5083
+ o.textContent = label;
5084
+ lyricModelSel.appendChild(o);
5085
+ };
5086
+ addOpt("", "\u306A\u3057");
5087
+ for (const m of LYRIC_MODELS) addOpt(m, lyricModelLabel(m));
5088
+ if (active.lyricModel && !LYRIC_MODELS.includes(active.lyricModel)) {
5089
+ addOpt(active.lyricModel, lyricModelLabel(active.lyricModel));
5090
+ }
5091
+ lyricModelSel.value = active.lyricModel;
5092
+ lyricInput.value = active.lyrics;
5093
+ lyricVol.value = String(active.vocalVolume);
5094
+ lyricVolLabel.textContent = String(active.vocalVolume);
5095
+ lyricPan.value = String(active.vocalPan);
5096
+ lyricPanLabel.textContent = fmtPan(active.vocalPan);
5097
+ const updateLyricCount = () => {
5098
+ const n = normalizeLyrics(lyricInput.value).length;
5099
+ lyricCount.textContent = active.lyricModel && n > 0 ? `${n}\u97F3\u7BC0` : "";
5100
+ };
5101
+ const syncLyricVisibility = () => {
5102
+ lyricBody.style.display = active.lyricModel ? "" : "none";
5103
+ updateLyricCount();
5104
+ };
5105
+ syncLyricVisibility();
5106
+ lyricModelSel.addEventListener("change", () => {
5107
+ active.lyricModel = lyricModelSel.value;
5108
+ syncLyricVisibility();
5109
+ });
5110
+ lyricInput.addEventListener("input", () => {
5111
+ active.lyrics = lyricInput.value;
5112
+ updateLyricCount();
5113
+ });
5114
+ lyricVol.addEventListener("input", () => {
5115
+ active.vocalVolume = Number.parseInt(lyricVol.value, 10);
5116
+ lyricVolLabel.textContent = lyricVol.value;
5117
+ });
5118
+ lyricPan.addEventListener("input", () => {
5119
+ active.vocalPan = Number.parseInt(lyricPan.value, 10);
5120
+ lyricPanLabel.textContent = fmtPan(active.vocalPan);
5121
+ });
5122
+ lyricPanLabel.style.cursor = "pointer";
5123
+ lyricPanLabel.title = "\u30BF\u30C3\u30D7\u3067\u4E2D\u592E(C)\u3078";
5124
+ lyricPanLabel.addEventListener("click", () => {
5125
+ active.vocalPan = 64;
5126
+ lyricPan.value = "64";
5127
+ lyricPanLabel.textContent = fmtPan(64);
5128
+ });
3587
5129
  if (active.config.id === "chord" && showChord) {
3588
5130
  const div = document.createElement("div");
3589
5131
  div.className = "dtm-row";
@@ -3671,6 +5213,11 @@ var mountDAW = (target, options = {}) => {
3671
5213
  const barLimitBars = Number(refs.barLimitSelect.value);
3672
5214
  const limitSteps = barLimitBars > 0 ? barLimitBars * renderConfig.stepsPerBar : Infinity;
3673
5215
  const clipNotes = (notes) => limitSteps === Infinity ? notes : notes.filter((n) => n.startStep < limitSteps);
5216
+ const metaLine = formatMmlMeta({
5217
+ instrument: currentInstrument || void 0,
5218
+ drum: currentDrumPattern !== "none" ? currentDrumPattern : void 0,
5219
+ volume: masterVolume
5220
+ });
3674
5221
  if (refs.decomposeChordToggle.checked) {
3675
5222
  const ignoreHeavy = refs.ignoreChordHeavyToggle.checked;
3676
5223
  const targetStates = ignoreHeavy ? trackStates.filter((t) => !isChordHeavyTrack(t.core.getNotes())) : trackStates;
@@ -3680,12 +5227,14 @@ var mountDAW = (target, options = {}) => {
3680
5227
  );
3681
5228
  const monoTracks = decomposeToMonophonic(allNotes);
3682
5229
  const refCore = trackStates[0].core;
3683
- const full2 = monoTracks.map(
5230
+ const decomposedFull = monoTracks.map(
3684
5231
  (notes, i) => `@${i} ${refCore.getMMLFromNotes(notes, bpm, 100).trim()}`
3685
- ).join(";\n");
3686
- const minified2 = monoTracks.map(
5232
+ );
5233
+ const decomposedMini = monoTracks.map(
3687
5234
  (notes, i) => `@${i}${refCore.getMMLFromNotes(notes, bpm, 100).trim().replace(/\s+/g, "")}`
3688
- ).join(";");
5235
+ );
5236
+ const full2 = [metaLine, ...decomposedFull].filter((s) => s.length > 0).join(";\n");
5237
+ const minified2 = [metaLine, ...decomposedMini].filter((s) => s.length > 0).join(";");
3689
5238
  return {
3690
5239
  full: full2,
3691
5240
  minified: minified2,
@@ -3694,12 +5243,30 @@ var mountDAW = (target, options = {}) => {
3694
5243
  barLimit: barLimitBars
3695
5244
  };
3696
5245
  }
3697
- const full = trackStates.map(
5246
+ const trackLines = trackStates.map(
3698
5247
  (t, i) => `@${i} ${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim()}`
3699
- ).join(";\n");
3700
- const minified = trackStates.map(
5248
+ );
5249
+ const trackLinesMini = trackStates.map(
3701
5250
  (t, i) => `@${i}${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim().replace(/\s+/g, "")}`
3702
- ).join(";");
5251
+ );
5252
+ const lyricLines = trackStates.map((t, i) => ({
5253
+ i,
5254
+ text: t.lyrics.trim(),
5255
+ model: t.lyricModel.trim(),
5256
+ vol: t.vocalVolume,
5257
+ gate: t.vocalGate,
5258
+ pan: t.vocalPan
5259
+ })).filter((x) => x.model.length > 0 && x.text.length > 0).map((x) => {
5260
+ const params = [
5261
+ x.vol === 300 ? "" : `v${x.vol}`,
5262
+ x.gate === 100 ? "" : `q${x.gate}`,
5263
+ x.pan === 64 ? "" : `p${x.pan}`
5264
+ ].filter((s) => s.length > 0).join(" ");
5265
+ const head = params ? `${x.model} ${params}` : x.model;
5266
+ return `@@${x.i} ${head} ${x.text}`;
5267
+ });
5268
+ const full = [metaLine, ...trackLines, ...lyricLines].filter((s) => s.length > 0).join(";\n");
5269
+ const minified = [metaLine, ...trackLinesMini, ...lyricLines].filter((s) => s.length > 0).join(";");
3703
5270
  return {
3704
5271
  full,
3705
5272
  minified,
@@ -3720,6 +5287,34 @@ var mountDAW = (target, options = {}) => {
3720
5287
  refs.outputContainer.classList.remove("dtm-hidden");
3721
5288
  updateUndoRedo();
3722
5289
  };
5290
+ const getFirstDetectedPitch = () => {
5291
+ let minStep = Number.MAX_SAFE_INTEGER;
5292
+ let candidateNotes = [];
5293
+ for (const t of trackStates) {
5294
+ for (const note of t.core.getNotes()) {
5295
+ if (note.startStep < minStep) {
5296
+ minStep = note.startStep;
5297
+ candidateNotes = [note];
5298
+ } else if (note.startStep === minStep) {
5299
+ candidateNotes.push(note);
5300
+ }
5301
+ }
5302
+ }
5303
+ if (candidateNotes.length === 0) return null;
5304
+ const sum = candidateNotes.reduce((acc, note) => acc + note.pitch, 0);
5305
+ return Math.round(sum / candidateNotes.length);
5306
+ };
5307
+ const centerPitch = (pitch) => {
5308
+ const canvas = getGridCanvas();
5309
+ const yIndex = renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart);
5310
+ const logicalY = yIndex * renderConfig.keyHeight;
5311
+ currentOffsetY = clamp3(
5312
+ logicalY - (canvas.height - renderConfig.keyHeight) / 2,
5313
+ 0,
5314
+ getMaxOffsetY()
5315
+ );
5316
+ setDrawOffset(currentOffsetX, currentOffsetY);
5317
+ };
3723
5318
  const clearAll = () => {
3724
5319
  for (const t of trackStates) {
3725
5320
  t.core.resetHistory();
@@ -3731,8 +5326,42 @@ var mountDAW = (target, options = {}) => {
3731
5326
  if (!mml) return;
3732
5327
  clearAll();
3733
5328
  for (const t of trackStates) t.core.setLoadMode(true);
3734
- const { placements, bpm: parsedBpm } = parseMML(mml, {
3735
- stepsPerBar: renderConfig.stepsPerBar
5329
+ const {
5330
+ placements,
5331
+ bpm: parsedBpm,
5332
+ lyrics,
5333
+ meta
5334
+ } = parseMML(mml, {
5335
+ stepsPerBar: renderConfig.stepsPerBar,
5336
+ collectLyrics: true,
5337
+ // このDAWのトラック数を超えるチャンネルはベースへ畳み込む(従来挙動)
5338
+ clampTrackCount: trackStates.length
5339
+ });
5340
+ currentInstrument = meta.instrument ?? "";
5341
+ if (meta.drum && drumPatterns[meta.drum]) {
5342
+ currentDrumPattern = meta.drum;
5343
+ refs.drumSelect.value = meta.drum;
5344
+ }
5345
+ if (meta.volume !== void 0) {
5346
+ masterVolume = meta.volume;
5347
+ refs.masterVolume.value = String(meta.volume);
5348
+ refs.masterVolumeLabel.textContent = `${meta.volume}%`;
5349
+ }
5350
+ for (const t of trackStates) {
5351
+ t.lyrics = "";
5352
+ t.lyricModel = "";
5353
+ t.vocalVolume = 300;
5354
+ t.vocalGate = 100;
5355
+ t.vocalPan = 64;
5356
+ }
5357
+ lyrics?.forEach((lt, idx) => {
5358
+ const t = trackStates[idx];
5359
+ if (!t) return;
5360
+ t.lyrics = lt.syllables.map((s) => s.kana).join("");
5361
+ t.lyricModel = lt.model;
5362
+ t.vocalVolume = lt.volume;
5363
+ t.vocalGate = lt.gate;
5364
+ t.vocalPan = lt.pan;
3736
5365
  });
3737
5366
  for (const p of placements) {
3738
5367
  const t = trackStates[p.trackIndex];
@@ -3748,8 +5377,14 @@ var mountDAW = (target, options = {}) => {
3748
5377
  }
3749
5378
  playStartStep = 0;
3750
5379
  currentOffsetX = 0;
3751
- setDrawOffset(currentOffsetX, currentOffsetY);
5380
+ const firstPitch = getFirstDetectedPitch();
5381
+ if (firstPitch !== null) {
5382
+ centerPitch(firstPitch);
5383
+ } else {
5384
+ setDrawOffset(currentOffsetX, currentOffsetY);
5385
+ }
3752
5386
  redrawAll();
5387
+ updateTrackPanel();
3753
5388
  updateUndoRedo();
3754
5389
  };
3755
5390
  const applyChord = () => {
@@ -3809,7 +5444,12 @@ var mountDAW = (target, options = {}) => {
3809
5444
  }
3810
5445
  playStartStep = 0;
3811
5446
  currentOffsetX = 0;
3812
- setDrawOffset(currentOffsetX, currentOffsetY);
5447
+ const firstPitch = getFirstDetectedPitch();
5448
+ if (firstPitch !== null) {
5449
+ centerPitch(firstPitch);
5450
+ } else {
5451
+ setDrawOffset(currentOffsetX, currentOffsetY);
5452
+ }
3813
5453
  redrawAll();
3814
5454
  updateUndoRedo();
3815
5455
  };
@@ -4081,6 +5721,9 @@ var mountDAW = (target, options = {}) => {
4081
5721
  pause,
4082
5722
  stop,
4083
5723
  getMML: generateMML,
5724
+ setInstrument: (name) => {
5725
+ currentInstrument = name;
5726
+ },
4084
5727
  loadMML,
4085
5728
  loadMIDI,
4086
5729
  exportMIDI: exportMIDI2,
@@ -4201,6 +5844,388 @@ var INSTRUMENT_PRESETS = {
4201
5844
  }
4202
5845
  };
4203
5846
 
5847
+ // src/mml-player.ts
5848
+ var STEPS_PER_BEAT3 = 48;
5849
+ var STEPS_PER_BAR = 192;
5850
+ var DEFAULT_TRACK_COLORS = ["#00e436", "#29adff", "#ff77a8", "#ffec27"];
5851
+ var activePlayer = null;
5852
+ var formatTime = (seconds) => {
5853
+ const m = Math.floor(seconds / 60);
5854
+ const s = Math.floor(seconds % 60);
5855
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
5856
+ };
5857
+ var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
5858
+ var mountMmlPlayer = (target, mml, options = {}) => {
5859
+ injectStyles(target.ownerDocument ?? document);
5860
+ const {
5861
+ placements,
5862
+ bpm: parsedBpm,
5863
+ tokenTracks,
5864
+ lyrics,
5865
+ meta
5866
+ } = parseMML(mml, {
5867
+ collectTokens: true,
5868
+ collectLyrics: true
5869
+ });
5870
+ const lyricTracks = lyrics ?? /* @__PURE__ */ new Map();
5871
+ const bpm = parsedBpm ?? options.defaultBpm ?? 120;
5872
+ const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
5873
+ const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
5874
+ const trackVolume = meta.volume ?? options.volume ?? 100;
5875
+ const colors = options.trackColors ?? DEFAULT_TRACK_COLORS;
5876
+ const useSynth = options.synth ?? !options.onPlayNote;
5877
+ const secondsPerStep = 60 / bpm / STEPS_PER_BEAT3;
5878
+ const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
5879
+ (a, b) => a - b
5880
+ );
5881
+ const seqTracks = trackIndices.map((index) => {
5882
+ let id = 0;
5883
+ const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
5884
+ id: id++,
5885
+ startStep: p.startStep,
5886
+ durationSteps: p.durationSteps,
5887
+ pitch: p.pitch,
5888
+ velocity: 100
5889
+ }));
5890
+ return { id: String(index), volume: trackVolume, notes };
5891
+ });
5892
+ const colorOf = (index) => colors[index % colors.length] ?? DEFAULT_TRACK_COLORS[0];
5893
+ let audioCtx = null;
5894
+ const ensureCtx = () => {
5895
+ if (!audioCtx) audioCtx = new AudioContext();
5896
+ return audioCtx;
5897
+ };
5898
+ const synthPlay = (e) => {
5899
+ const ctx = ensureCtx();
5900
+ const osc = ctx.createOscillator();
5901
+ const gain = ctx.createGain();
5902
+ osc.type = "square";
5903
+ osc.frequency.value = freqFromPitch(e.pitch);
5904
+ const t0 = ctx.currentTime + e.when;
5905
+ const peak = Math.max(1e-4, 0.06 * e.volume * 1.5);
5906
+ gain.gain.setValueAtTime(peak, t0);
5907
+ gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
5908
+ osc.connect(gain);
5909
+ if (typeof ctx.createStereoPanner === "function" && e.pan) {
5910
+ const panner = ctx.createStereoPanner();
5911
+ panner.pan.value = Math.max(-1, Math.min(1, e.pan));
5912
+ gain.connect(panner);
5913
+ panner.connect(ctx.destination);
5914
+ } else {
5915
+ gain.connect(ctx.destination);
5916
+ }
5917
+ osc.start(t0);
5918
+ osc.stop(t0 + e.duration + 0.02);
5919
+ };
5920
+ const drumSynth = (e) => {
5921
+ const ctx = ensureCtx();
5922
+ const t0 = ctx.currentTime + e.when;
5923
+ const vol = Math.max(1e-4, Math.min(1, e.velocity));
5924
+ const isKick = e.pitch === 35 || e.pitch === 36;
5925
+ const isSnareLike = e.pitch === 38 || e.pitch === 39 || e.pitch === 40;
5926
+ if (isKick) {
5927
+ const osc = ctx.createOscillator();
5928
+ const g2 = ctx.createGain();
5929
+ osc.frequency.setValueAtTime(150, t0);
5930
+ osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
5931
+ g2.gain.setValueAtTime(vol * 0.9, t0);
5932
+ g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
5933
+ osc.connect(g2).connect(ctx.destination);
5934
+ osc.start(t0);
5935
+ osc.stop(t0 + 0.2);
5936
+ osc.onended = () => osc.disconnect();
5937
+ return;
5938
+ }
5939
+ const dur = isSnareLike ? 0.18 : 0.05;
5940
+ const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
5941
+ const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
5942
+ const data = buffer.getChannelData(0);
5943
+ for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
5944
+ const src = ctx.createBufferSource();
5945
+ src.buffer = buffer;
5946
+ const filter = ctx.createBiquadFilter();
5947
+ filter.type = isSnareLike ? "bandpass" : "highpass";
5948
+ filter.frequency.value = isSnareLike ? 2e3 : 8e3;
5949
+ const g = ctx.createGain();
5950
+ g.gain.setValueAtTime(vol * (isSnareLike ? 0.7 : 0.4), t0);
5951
+ g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
5952
+ src.connect(filter).connect(g).connect(ctx.destination);
5953
+ src.start(t0);
5954
+ src.stop(t0 + dur);
5955
+ src.onended = () => {
5956
+ src.disconnect();
5957
+ filter.disconnect();
5958
+ g.disconnect();
5959
+ };
5960
+ };
5961
+ let voices = null;
5962
+ const ensureVoices = () => {
5963
+ if (options.singingVoices) return options.singingVoices;
5964
+ if (!voices) {
5965
+ const ctx = ensureCtx();
5966
+ voices = createSingingVoices(ctx, ctx.destination);
5967
+ }
5968
+ return voices;
5969
+ };
5970
+ const voicesAvailable = useSynth || !!options.singingVoices;
5971
+ const peekVoices = () => options.singingVoices ?? voices;
5972
+ const getAudioTime = () => {
5973
+ if (useSynth) return ensureCtx().currentTime;
5974
+ return options.getAudioTime?.() ?? performance.now() / 1e3;
5975
+ };
5976
+ const doc = target.ownerDocument ?? document;
5977
+ const root = doc.createElement("div");
5978
+ root.className = "dtm-daw dtm-player";
5979
+ const head = doc.createElement("div");
5980
+ head.className = "dtm-player-head";
5981
+ const playBtn = doc.createElement("button");
5982
+ playBtn.type = "button";
5983
+ playBtn.className = "dtm-player-play";
5984
+ playBtn.innerHTML = icon("play", 12);
5985
+ playBtn.disabled = trackIndices.length === 0;
5986
+ const tempoEl = doc.createElement("span");
5987
+ tempoEl.className = "dtm-player-tempo";
5988
+ tempoEl.textContent = `\u2669=${bpm}`;
5989
+ const timeEl = doc.createElement("span");
5990
+ timeEl.className = "dtm-player-time";
5991
+ timeEl.textContent = "00:00";
5992
+ const dots = doc.createElement("div");
5993
+ dots.className = "dtm-player-dots";
5994
+ for (const index of trackIndices) {
5995
+ const dot = doc.createElement("span");
5996
+ dot.className = "dtm-player-dot";
5997
+ dot.style.backgroundColor = colorOf(index);
5998
+ dots.appendChild(dot);
5999
+ }
6000
+ head.append(playBtn, tempoEl, timeEl);
6001
+ const addChip = (label) => {
6002
+ const chip = doc.createElement("span");
6003
+ chip.className = "dtm-player-chip";
6004
+ chip.textContent = label;
6005
+ head.appendChild(chip);
6006
+ };
6007
+ if (meta.instrument) addChip(`\u266A ${meta.instrument}`);
6008
+ if (meta.drum) addChip(`\u{1F941} ${meta.drum}${drumPattern ? "" : " (?)"}`);
6009
+ if (meta.volume !== void 0) addChip(`\u{1F50A} ${meta.volume}%`);
6010
+ head.appendChild(dots);
6011
+ root.appendChild(head);
6012
+ const laneViews = [];
6013
+ for (const index of trackIndices) {
6014
+ const lyricTrack = lyricTracks.get(index);
6015
+ const isLyricLane = !!lyricTrack && lyricTrack.syllables.length > 0;
6016
+ const row = doc.createElement("div");
6017
+ row.className = "dtm-player-lane-row";
6018
+ const label = doc.createElement("div");
6019
+ label.className = "dtm-player-lane-label";
6020
+ const swatch = doc.createElement("span");
6021
+ swatch.className = "dtm-player-dot";
6022
+ swatch.style.backgroundColor = colorOf(index);
6023
+ const no = doc.createElement("span");
6024
+ no.className = "dtm-player-lane-no";
6025
+ no.textContent = `@${index}`;
6026
+ label.append(swatch, no);
6027
+ const lane = doc.createElement("div");
6028
+ lane.className = "dtm-player-lane";
6029
+ lane.style.setProperty("--tk", colorOf(index));
6030
+ const laneTokens = [];
6031
+ if (isLyricLane) {
6032
+ const notes = placements.filter((p) => p.trackIndex === index).sort((a, b) => a.startStep - b.startStep);
6033
+ const gateScale = (lyricTrack.gate ?? 100) / 100;
6034
+ const breaks = new Set(lyricTrack.lineBreaks ?? []);
6035
+ if (lyricTrack.metaText) {
6036
+ const metaEl = doc.createElement("span");
6037
+ metaEl.className = "dtm-tk dtm-tk--meta";
6038
+ metaEl.textContent = lyricTrack.metaText;
6039
+ lane.appendChild(metaEl);
6040
+ }
6041
+ const count = Math.min(notes.length, lyricTrack.syllables.length);
6042
+ for (let i = 0; i < count; i++) {
6043
+ const note = notes[i];
6044
+ if (breaks.has(i)) {
6045
+ const br = doc.createElement("span");
6046
+ br.className = "dtm-tk dtm-tk--break";
6047
+ br.textContent = "\\n";
6048
+ lane.appendChild(br);
6049
+ }
6050
+ const span = doc.createElement("span");
6051
+ span.className = "dtm-tk dtm-tk--lyric";
6052
+ span.textContent = lyricTrack.syllables[i].kana;
6053
+ lane.appendChild(span);
6054
+ laneTokens.push({
6055
+ el: span,
6056
+ startStep: note.startStep,
6057
+ durationSteps: Math.max(
6058
+ 1,
6059
+ Math.round(note.durationSteps * gateScale)
6060
+ )
6061
+ });
6062
+ }
6063
+ } else {
6064
+ const tokens = tokenTracks?.get(index) ?? [];
6065
+ for (const tok of tokens) {
6066
+ const span = doc.createElement("span");
6067
+ span.className = `dtm-tk dtm-tk--${tok.type}`;
6068
+ span.textContent = tok.text;
6069
+ lane.appendChild(span);
6070
+ if (tok.durationSteps > 0) {
6071
+ laneTokens.push({
6072
+ el: span,
6073
+ startStep: tok.startStep,
6074
+ durationSteps: tok.durationSteps
6075
+ });
6076
+ }
6077
+ }
6078
+ }
6079
+ row.append(label, lane);
6080
+ root.appendChild(row);
6081
+ laneViews.push({ lane, tokens: laneTokens });
6082
+ }
6083
+ target.appendChild(root);
6084
+ const autoScroll = (lane, el) => {
6085
+ if (el.offsetWidth === 0 || lane.clientWidth === 0) return;
6086
+ const elementCenter = el.offsetLeft + el.offsetWidth / 2;
6087
+ const maxScroll = Math.max(0, lane.scrollWidth - lane.clientWidth);
6088
+ const next = elementCenter - lane.clientWidth / 2;
6089
+ lane.scrollLeft = Math.max(0, Math.min(next, maxScroll));
6090
+ };
6091
+ const renderPlayhead = (step) => {
6092
+ timeEl.textContent = formatTime(Math.max(0, step) * secondsPerStep);
6093
+ for (const view of laneViews) {
6094
+ let active = null;
6095
+ for (const t of view.tokens) {
6096
+ const on = step >= t.startStep && step < t.startStep + t.durationSteps;
6097
+ t.el.classList.toggle("is-active", on);
6098
+ if (on && !active) active = t;
6099
+ }
6100
+ if (active) autoScroll(view.lane, active.el);
6101
+ }
6102
+ };
6103
+ const resetPlayhead = () => {
6104
+ timeEl.textContent = "00:00";
6105
+ for (const view of laneViews) {
6106
+ for (const t of view.tokens) t.el.classList.remove("is-active");
6107
+ view.lane.scrollLeft = 0;
6108
+ }
6109
+ };
6110
+ const seq = createSequencer({
6111
+ getTracks: () => seqTracks,
6112
+ getBpm: () => bpm,
6113
+ getPlayStartStep: () => 0,
6114
+ getDrumPattern: () => drumPattern,
6115
+ getSoloTrackId: () => null,
6116
+ getAudioTime,
6117
+ onPlayNote: (e) => {
6118
+ if (lyricTracks.has(Number(e.trackId))) return;
6119
+ options.onPlayNote?.(e);
6120
+ if (useSynth) synthPlay(e);
6121
+ },
6122
+ onPlayDrum: (e) => {
6123
+ const velocity = e.velocity * (trackVolume / 100);
6124
+ options.onPlayDrum?.({ ...e, velocity });
6125
+ if (useSynth) drumSynth({ ...e, velocity });
6126
+ },
6127
+ onTick: (step) => {
6128
+ renderPlayhead(step);
6129
+ },
6130
+ onEnd: () => finish(),
6131
+ stepsPerBar: STEPS_PER_BAR
6132
+ });
6133
+ let playing = false;
6134
+ const setPlayingUI = (on) => {
6135
+ playing = on;
6136
+ playBtn.innerHTML = icon(on ? "stop" : "play", 12);
6137
+ playBtn.classList.toggle("dtm-player-play--stop", on);
6138
+ };
6139
+ const finish = () => {
6140
+ setPlayingUI(false);
6141
+ resetPlayhead();
6142
+ if (activePlayer === instance) activePlayer = null;
6143
+ };
6144
+ const buildStreamTracks = () => [...lyricTracks.entries()].map(([index, lt]) => {
6145
+ const seqTrack = seqTracks.find((t) => Number(t.id) === index);
6146
+ const sorted = [...seqTrack?.notes ?? []].sort(
6147
+ (a, b) => a.startStep - b.startStep
6148
+ );
6149
+ const gate = (lt.gate ?? 100) / 100;
6150
+ const count = Math.min(sorted.length, lt.syllables.length);
6151
+ const notes = [];
6152
+ for (let i = 0; i < count; i++) {
6153
+ const n = sorted[i];
6154
+ notes.push({
6155
+ syllable: lt.syllables[i],
6156
+ pitch: n.pitch,
6157
+ startSec: n.startStep * secondsPerStep,
6158
+ durationSec: n.durationSteps * secondsPerStep * gate
6159
+ });
6160
+ }
6161
+ return {
6162
+ model: lt.model,
6163
+ volume: vocalVolumeToGain(lt.volume ?? 300) * (trackVolume / 100),
6164
+ pan: panToStereo(lt.pan ?? 64),
6165
+ notes
6166
+ };
6167
+ });
6168
+ const startWhenReady = async () => {
6169
+ const streaming = voicesAvailable && lyricTracks.size > 0;
6170
+ const tracks = streaming ? buildStreamTracks() : [];
6171
+ if (streaming) {
6172
+ const v = ensureVoices();
6173
+ const overlay = showLoadingOverlay(root);
6174
+ try {
6175
+ await v.loadModels(tracks.map((t) => t.model));
6176
+ await v.warm(tracks);
6177
+ } catch (err) {
6178
+ console.warn("[dtm] voice preload failed", err);
6179
+ } finally {
6180
+ overlay.remove();
6181
+ }
6182
+ if (!playing || activePlayer !== instance) return;
6183
+ }
6184
+ seq.start(0);
6185
+ if (streaming) ensureVoices().startStream(tracks, seq.getStartTime());
6186
+ };
6187
+ const play = () => {
6188
+ if (playing || trackIndices.length === 0) return;
6189
+ if (activePlayer && activePlayer !== instance) activePlayer.stop();
6190
+ activePlayer = instance;
6191
+ setPlayingUI(true);
6192
+ void options.onResumeAudio?.();
6193
+ if (useSynth) {
6194
+ const ctx = ensureCtx();
6195
+ if (ctx.state === "suspended") void ctx.resume();
6196
+ }
6197
+ if (voicesAvailable && lyricTracks.size > 0) ensureVoices().reset();
6198
+ void startWhenReady();
6199
+ };
6200
+ const stop = () => {
6201
+ if (!playing) return;
6202
+ seq.stop();
6203
+ peekVoices()?.stopStream();
6204
+ finish();
6205
+ };
6206
+ playBtn.addEventListener("click", () => {
6207
+ if (playing) stop();
6208
+ else play();
6209
+ });
6210
+ const destroy = () => {
6211
+ seq.stop();
6212
+ peekVoices()?.stopStream();
6213
+ if (activePlayer === instance) activePlayer = null;
6214
+ if (audioCtx) {
6215
+ void audioCtx.close();
6216
+ audioCtx = null;
6217
+ }
6218
+ root.remove();
6219
+ };
6220
+ const instance = {
6221
+ play,
6222
+ stop,
6223
+ isPlaying: () => playing,
6224
+ destroy
6225
+ };
6226
+ return instance;
6227
+ };
6228
+
4204
6229
  // src/piano-roll.ts
4205
6230
  var createPianoRoll = (options, handlers) => {
4206
6231
  const {
@@ -4490,9 +6515,14 @@ var createPianoRoll = (options, handlers) => {
4490
6515
  DRUM_KEYS,
4491
6516
  DRUM_PATTERNS,
4492
6517
  INSTRUMENT_PRESETS,
6518
+ KOE_BASE_URL,
6519
+ KOE_VOICEBANKS,
6520
+ KOE_VOICEBANK_LABELS,
4493
6521
  LinkedList,
6522
+ MAX_VOCAL_VOLUME,
4494
6523
  MMLCore,
4495
6524
  PITCH_MAP,
6525
+ PREWARM_NOTES,
4496
6526
  TRACKS_ADVANCED,
4497
6527
  TRACKS_SIMPLE,
4498
6528
  analyzeMidiTracks,
@@ -4500,9 +6530,15 @@ var createPianoRoll = (options, handlers) => {
4500
6530
  applyMonophonic,
4501
6531
  buildChordPlacements,
4502
6532
  buildNameToKeyMapping,
6533
+ collectPitchTokens,
4503
6534
  createAudioContext,
6535
+ createKlattVoice,
6536
+ createKoeVoice,
6537
+ createLyricsConductor,
4504
6538
  createPianoRoll,
4505
6539
  createSequencer,
6540
+ createSingingVoices,
6541
+ createVoiceRegistry,
4506
6542
  decomposeToMonophonic,
4507
6543
  drawGrid,
4508
6544
  drawHeader,
@@ -4514,6 +6550,7 @@ var createPianoRoll = (options, handlers) => {
4514
6550
  extractMidiPlacements,
4515
6551
  extractMidiPlacementsByTrack,
4516
6552
  fetchSoundFontList,
6553
+ formatMmlMeta,
4517
6554
  generateRandomPattern,
4518
6555
  getDrawOffset,
4519
6556
  getGridCanvas,
@@ -4527,10 +6564,19 @@ var createPianoRoll = (options, handlers) => {
4527
6564
  init,
4528
6565
  injectStyles,
4529
6566
  isChordHeavyTrack,
6567
+ koeUrl,
4530
6568
  mountDAW,
6569
+ mountMmlPlayer,
6570
+ normalizeLyrics,
4531
6571
  onClick,
6572
+ panToStereo,
6573
+ parseLyrics,
4532
6574
  parseMML,
6575
+ parseMmlMeta,
4533
6576
  setDrawOffset,
4534
6577
  setupRecorder,
4535
- shiftNotes
6578
+ shiftNotes,
6579
+ stripLyrics,
6580
+ stripMmlMeta,
6581
+ vocalVolumeToGain
4536
6582
  });