@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/README.md +37 -37
- package/dist/index.d.mts +456 -3
- package/dist/index.d.ts +456 -3
- package/dist/index.js +2165 -119
- package/dist/index.mjs +2143 -118
- package/dist/voice-worker.js +382 -0
- package/package.json +5 -2
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 *
|
|
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
|
|
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
|
|
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
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
const
|
|
1809
|
-
|
|
1810
|
-
);
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
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 =
|
|
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
|
|
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)
|
|
1966
|
-
|
|
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 >=
|
|
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
|
-
|
|
2026
|
-
|
|
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 {
|
|
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.
|
|
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:
|
|
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:
|
|
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
|
|
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 = `${
|
|
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 =
|
|
4513
|
+
const x = clamp3(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
|
|
3120
4514
|
const ratio = x / (rect.width - thumbW);
|
|
3121
|
-
currentOffsetX =
|
|
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 =
|
|
4524
|
+
const y = clamp3(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
|
|
3131
4525
|
const ratio = y / (rect.height - thumbH);
|
|
3132
|
-
currentOffsetY =
|
|
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 =
|
|
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 =
|
|
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
|
|
3487
|
-
|
|
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
|
|
5230
|
+
const decomposedFull = monoTracks.map(
|
|
3684
5231
|
(notes, i) => `@${i} ${refCore.getMMLFromNotes(notes, bpm, 100).trim()}`
|
|
3685
|
-
)
|
|
3686
|
-
const
|
|
5232
|
+
);
|
|
5233
|
+
const decomposedMini = monoTracks.map(
|
|
3687
5234
|
(notes, i) => `@${i}${refCore.getMMLFromNotes(notes, bpm, 100).trim().replace(/\s+/g, "")}`
|
|
3688
|
-
)
|
|
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
|
|
5246
|
+
const trackLines = trackStates.map(
|
|
3698
5247
|
(t, i) => `@${i} ${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim()}`
|
|
3699
|
-
)
|
|
3700
|
-
const
|
|
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
|
-
)
|
|
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 {
|
|
3735
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|