@onjmin/dtm 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1121,6 +1121,139 @@ type Sequencer = {
1121
1121
  };
1122
1122
  declare const createSequencer: (options: SequencerOptions) => Sequencer;
1123
1123
 
1124
+ /**
1125
+ * createDtmStudio — 「import して関数1つ」で鳴る、全部入りスタジオ(Layer 3)。
1126
+ *
1127
+ * @onjmin/dtm 本体(mountDAW / mountMmlPlayer)は発音を持たない注入式設計で、
1128
+ * 楽器・ドラムの実音は外部 SoundFont(rpgen3)に委ねる。デモ index.html では
1129
+ * その配線(AudioContext・SoundFontロード・歌声ワーカー・録音・MIDI/コード解析)を
1130
+ * 手書きしていたが、本モジュールはそれを丸ごと内包する。
1131
+ *
1132
+ * const studio = await createDtmStudio();
1133
+ * studio.mountEditor(editorEl, { initialMML }); // 編集UI(音・歌声込み)
1134
+ * studio.mountPlayer(playerEl, mml); // 再生専用UI(音・歌声込み)
1135
+ *
1136
+ * 何も渡さなければ rpgen3 SoundFont を実行時にCDNから動的importし、歌声合成ワーカーは
1137
+ * パッケージ同梱の dist/voice-worker.js を用いる。エンジンやURLは options で差し替え可能。
1138
+ */
1139
+
1140
+ type SoundFontInstance = {
1141
+ play: (o: {
1142
+ ctx: AudioContext;
1143
+ destination: AudioNode;
1144
+ pitch: number;
1145
+ volume: number;
1146
+ when: number;
1147
+ duration: number;
1148
+ }) => void;
1149
+ };
1150
+ type SoundFontEngine = {
1151
+ load: (o: {
1152
+ ctx: AudioContext;
1153
+ fontName: string;
1154
+ url: string;
1155
+ }) => Promise<SoundFontInstance>;
1156
+ toURL: (fullName: string) => string;
1157
+ };
1158
+ type SoundFontDrumEngine = {
1159
+ load: (o: {
1160
+ ctx: AudioContext;
1161
+ font: string;
1162
+ id: string;
1163
+ keys: number[];
1164
+ }) => Promise<void>;
1165
+ play: (o: {
1166
+ ctx: AudioContext;
1167
+ destination: AudioNode;
1168
+ pitch: number;
1169
+ volume: number;
1170
+ when: number;
1171
+ duration: number;
1172
+ }) => void;
1173
+ font: unknown;
1174
+ };
1175
+ type SoundFontListEngine = {
1176
+ init: () => void;
1177
+ onload: (cb: () => void) => void;
1178
+ };
1179
+ /** 注入で差し替え可能な外部エンジン群(未指定なら CDN から取得)。 */
1180
+ type DtmStudioEngines = {
1181
+ SoundFont?: SoundFontEngine;
1182
+ SoundFont_drum?: SoundFontDrumEngine;
1183
+ SoundFont_list?: SoundFontListEngine;
1184
+ parseChord?: DawOptions["parseChord"];
1185
+ parseChords?: DawOptions["parseChords"];
1186
+ parseMidi?: DawOptions["parseMidi"];
1187
+ };
1188
+ /** 既定の取得元URL(rpgen3 / jsDelivr)。options.cdn で個別に上書きできる。 */
1189
+ declare const DEFAULT_CDN: {
1190
+ readonly soundFont: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont.mjs";
1191
+ readonly soundFontDrum: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_drum.mjs";
1192
+ readonly soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs";
1193
+ readonly parseChord: "https://rpgen3.github.io/piano/mjs/parseChord.mjs";
1194
+ readonly parseChords: "https://rpgen3.github.io/piano/mjs/parseChords.mjs";
1195
+ readonly midiParser: "https://cdn.jsdelivr.net/npm/midi-parser-js@4.0.4/+esm";
1196
+ };
1197
+ type DtmStudioOptions = {
1198
+ /** 既存の AudioContext を使う(未指定なら内部生成)。 */
1199
+ audioContext?: AudioContext;
1200
+ /** マスター音量 0-1(楽器・歌声)。既定 1。 */
1201
+ masterVolume?: number;
1202
+ /** ドラム音量 0-1。既定 1。 */
1203
+ drumVolume?: number;
1204
+ /**
1205
+ * 歌声合成ワーカー(voice-worker.js)のURL。
1206
+ * 既定はパッケージ同梱の dist/voice-worker.js。
1207
+ * `null` を渡すとワーカーを使わず klatt のみ(koe音源はメインスレッド合成)。
1208
+ */
1209
+ voiceWorkerUrl?: string | null;
1210
+ /** 初期の楽器プリセットキー(INSTRUMENT_PRESETS)。既定 "retro_game"。 */
1211
+ defaultPreset?: string;
1212
+ /** 外部エンジンの注入(指定したものは CDN 取得をスキップ)。 */
1213
+ engines?: DtmStudioEngines;
1214
+ /** CDN URL の上書き。 */
1215
+ cdn?: Partial<typeof DEFAULT_CDN>;
1216
+ /** 有効化する機能。既定はすべて true。 */
1217
+ features?: {
1218
+ /** 録音→WAVダウンロード(編集UIの録音ボタン)。 */
1219
+ recorder?: boolean;
1220
+ /** MIDIファイル読み込み。 */
1221
+ midi?: boolean;
1222
+ /** コード入力(和音)。 */
1223
+ chord?: boolean;
1224
+ /** 編集UIに楽器プリセット選択UIを差し込む。 */
1225
+ presetUI?: boolean;
1226
+ };
1227
+ };
1228
+ /** 編集UIのマウント時オプション(DawOptions を一部上書きできる)。 */
1229
+ type MountEditorOptions = Partial<DawOptions> & {
1230
+ /** このエディタで読み込む楽器プリセット(未指定なら studio の defaultPreset)。 */
1231
+ preset?: string;
1232
+ /** プリセット選択UIを出すか(未指定なら studio の features.presetUI)。 */
1233
+ presetUI?: boolean;
1234
+ };
1235
+ /** 再生UIのマウント時オプション(MmlPlayerOptions を一部上書きできる)。 */
1236
+ type MountPlayerOptions = Partial<MmlPlayerOptions>;
1237
+ type DtmStudio = {
1238
+ /** 内部で使用している AudioContext。 */
1239
+ audioContext: AudioContext;
1240
+ /** 歌声合成ヘルパ(klatt + koe音源)。 */
1241
+ singingVoices: SingingVoices;
1242
+ /** 編集UI(mountDAW)を音・歌声込みでマウントする。 */
1243
+ mountEditor: (target: HTMLElement, options?: MountEditorOptions) => DawInstance;
1244
+ /** 再生専用UI(mountMmlPlayer)を音・歌声込みでマウントする。 */
1245
+ mountPlayer: (target: HTMLElement, mml: string, options?: MountPlayerOptions) => MmlPlayerInstance;
1246
+ /** 楽器プリセットを(指定トラックぶん)ロードする。 */
1247
+ loadPreset: (presetKey: string, trackIds?: string[]) => Promise<void>;
1248
+ /** AudioContext を閉じ、生成物を破棄する。 */
1249
+ dispose: () => void;
1250
+ };
1251
+ /**
1252
+ * 全部入りスタジオを生成する。SoundFontエンジン・ドラム音源・楽器プリセットの
1253
+ * 初期ロードまで待ってから解決するため、await して使う。
1254
+ */
1255
+ declare const createDtmStudio: (options?: DtmStudioOptions) => Promise<DtmStudio>;
1256
+
1124
1257
  /**
1125
1258
  * mountDAW が注入する自己完結スタイル。
1126
1259
  * すべてのクラスは `dtm-` プレフィックスでスコープし、ホスト側CSSと衝突しにくくする。
@@ -1133,4 +1266,4 @@ declare const DAW_CSS = "\n@font-face {\n font-family: 'k8x12';\n src: url('ht
1133
1266
  */
1134
1267
  declare const injectStyles: (doc?: Document) => void;
1135
1268
 
1136
- export { type AddNoteOptions, type ApplyChordOptions, type ChordPatternType, type ChordPlacement, type ConsumedSyllable, type CoreEventHandlers, DAW_CSS, DEFAULT_BPM, DEFAULT_GATE, DEFAULT_PAN, DEFAULT_PLAYBACK_VELOCITY, DEFAULT_STEPS_PER_BAR, DEFAULT_VELOCITY, DEFAULT_VOCAL_VOLUME, DRUM_FONT, DRUM_KEYS, DRUM_PATTERNS, type DawInstance, type DawOptions, type DawViewState, type DrumPattern, type ExportMidiOptions, INSTRUMENT_PRESETS, type InstrumentPreset, KOE_BASE_URL, KOE_VOICEBANKS, KOE_VOICEBANK_LABELS, KOE_VOICEBANK_TERMS, type KoeVoiceOptions, LinkedList, type LyricSyllable, type LyricTrack, type LyricsConductor, MAX_VOCAL_VOLUME, MMLCore, type MMLDisplayToken, type MMLNotePlacement, type MidiExtraction, type MidiNotePlacement, type MidiTrackAnalysis, type MmlMeta, type MmlPlayerInstance, type MmlPlayerOptions, type Note, PITCH_MAP, PREWARM_NOTES, type ParseChordFn, type ParseChordsFn, type ParseMMLOptions, type ParseMidiFn, type ParsedMML, type PianoRollInstance, type PianoRollOptions, type PitchToken, type PlayDrumEvent, type PlayNoteEvent, type PlaybackState, type PreviewSoundCallback, type RenderConfig, type Sequencer, type SequencerOptions, type SequencerTrack, type SingingVoices, type SingingVoicesOptions, type StreamVoiceNote, type StreamVoiceTrack, TRACKS_ADVANCED, TRACKS_SIMPLE, type ToolMode, type TrackConfig, VOICE_IMAGE_KEY, type VoiceModel, type VoiceRegistry, analyzeMidiTracks, applyHarmonicFilter, applyMonophonic, buildChordPlacements, buildNameToKeyMapping, collectPitchTokens, createAudioContext, createKlattVoice, createKoeVoice, createLyricsConductor, createPianoRoll, createSequencer, createSingingVoices, createVoiceRegistry, decomposeToMonophonic, drawGrid, drawHeader, drawKeyboard, drawNotes, drawSelectedNotes, drawSelectionRect, exportMIDI, extractMidiPlacements, extractMidiPlacementsByTrack, fetchSoundFontList, formatMmlMeta, generateRandomPattern, getDrawOffset, getGridCanvas, getGridContext, getGridPosition, getHeaderCanvas, getMidiBPM, getRenderConfig, getXY, icon, init, injectStyles, isChordHeavyTrack, koeUrl, mountDAW, mountMmlPlayer, normalizeLyrics, onClick, panToStereo, parseLyrics, parseMML, parseMmlMeta, setDrawOffset, setupRecorder, shiftNotes, stripLyrics, stripMmlMeta, vocalVolumeToGain };
1269
+ export { type AddNoteOptions, type ApplyChordOptions, type ChordPatternType, type ChordPlacement, type ConsumedSyllable, type CoreEventHandlers, DAW_CSS, DEFAULT_BPM, DEFAULT_GATE, DEFAULT_PAN, DEFAULT_PLAYBACK_VELOCITY, DEFAULT_STEPS_PER_BAR, DEFAULT_VELOCITY, DEFAULT_VOCAL_VOLUME, DRUM_FONT, DRUM_KEYS, DRUM_PATTERNS, type DawInstance, type DawOptions, type DawViewState, type DrumPattern, type DtmStudio, type DtmStudioEngines, type DtmStudioOptions, type ExportMidiOptions, INSTRUMENT_PRESETS, type InstrumentPreset, KOE_BASE_URL, KOE_VOICEBANKS, KOE_VOICEBANK_LABELS, KOE_VOICEBANK_TERMS, type KoeVoiceOptions, LinkedList, type LyricSyllable, type LyricTrack, type LyricsConductor, MAX_VOCAL_VOLUME, MMLCore, type MMLDisplayToken, type MMLNotePlacement, type MidiExtraction, type MidiNotePlacement, type MidiTrackAnalysis, type MmlMeta, type MmlPlayerInstance, type MmlPlayerOptions, type MountEditorOptions, type MountPlayerOptions, type Note, PITCH_MAP, PREWARM_NOTES, type ParseChordFn, type ParseChordsFn, type ParseMMLOptions, type ParseMidiFn, type ParsedMML, type PianoRollInstance, type PianoRollOptions, type PitchToken, type PlayDrumEvent, type PlayNoteEvent, type PlaybackState, type PreviewSoundCallback, type RenderConfig, type Sequencer, type SequencerOptions, type SequencerTrack, type SingingVoices, type SingingVoicesOptions, type StreamVoiceNote, type StreamVoiceTrack, TRACKS_ADVANCED, TRACKS_SIMPLE, type ToolMode, type TrackConfig, VOICE_IMAGE_KEY, type VoiceModel, type VoiceRegistry, analyzeMidiTracks, applyHarmonicFilter, applyMonophonic, buildChordPlacements, buildNameToKeyMapping, collectPitchTokens, createAudioContext, createDtmStudio, createKlattVoice, createKoeVoice, createLyricsConductor, createPianoRoll, createSequencer, createSingingVoices, createVoiceRegistry, decomposeToMonophonic, drawGrid, drawHeader, drawKeyboard, drawNotes, drawSelectedNotes, drawSelectionRect, exportMIDI, extractMidiPlacements, extractMidiPlacementsByTrack, fetchSoundFontList, formatMmlMeta, generateRandomPattern, getDrawOffset, getGridCanvas, getGridContext, getGridPosition, getHeaderCanvas, getMidiBPM, getRenderConfig, getXY, icon, init, injectStyles, isChordHeavyTrack, koeUrl, mountDAW, mountMmlPlayer, normalizeLyrics, onClick, panToStereo, parseLyrics, parseMML, parseMmlMeta, setDrawOffset, setupRecorder, shiftNotes, stripLyrics, stripMmlMeta, vocalVolumeToGain };
package/dist/index.d.ts CHANGED
@@ -1121,6 +1121,139 @@ type Sequencer = {
1121
1121
  };
1122
1122
  declare const createSequencer: (options: SequencerOptions) => Sequencer;
1123
1123
 
1124
+ /**
1125
+ * createDtmStudio — 「import して関数1つ」で鳴る、全部入りスタジオ(Layer 3)。
1126
+ *
1127
+ * @onjmin/dtm 本体(mountDAW / mountMmlPlayer)は発音を持たない注入式設計で、
1128
+ * 楽器・ドラムの実音は外部 SoundFont(rpgen3)に委ねる。デモ index.html では
1129
+ * その配線(AudioContext・SoundFontロード・歌声ワーカー・録音・MIDI/コード解析)を
1130
+ * 手書きしていたが、本モジュールはそれを丸ごと内包する。
1131
+ *
1132
+ * const studio = await createDtmStudio();
1133
+ * studio.mountEditor(editorEl, { initialMML }); // 編集UI(音・歌声込み)
1134
+ * studio.mountPlayer(playerEl, mml); // 再生専用UI(音・歌声込み)
1135
+ *
1136
+ * 何も渡さなければ rpgen3 SoundFont を実行時にCDNから動的importし、歌声合成ワーカーは
1137
+ * パッケージ同梱の dist/voice-worker.js を用いる。エンジンやURLは options で差し替え可能。
1138
+ */
1139
+
1140
+ type SoundFontInstance = {
1141
+ play: (o: {
1142
+ ctx: AudioContext;
1143
+ destination: AudioNode;
1144
+ pitch: number;
1145
+ volume: number;
1146
+ when: number;
1147
+ duration: number;
1148
+ }) => void;
1149
+ };
1150
+ type SoundFontEngine = {
1151
+ load: (o: {
1152
+ ctx: AudioContext;
1153
+ fontName: string;
1154
+ url: string;
1155
+ }) => Promise<SoundFontInstance>;
1156
+ toURL: (fullName: string) => string;
1157
+ };
1158
+ type SoundFontDrumEngine = {
1159
+ load: (o: {
1160
+ ctx: AudioContext;
1161
+ font: string;
1162
+ id: string;
1163
+ keys: number[];
1164
+ }) => Promise<void>;
1165
+ play: (o: {
1166
+ ctx: AudioContext;
1167
+ destination: AudioNode;
1168
+ pitch: number;
1169
+ volume: number;
1170
+ when: number;
1171
+ duration: number;
1172
+ }) => void;
1173
+ font: unknown;
1174
+ };
1175
+ type SoundFontListEngine = {
1176
+ init: () => void;
1177
+ onload: (cb: () => void) => void;
1178
+ };
1179
+ /** 注入で差し替え可能な外部エンジン群(未指定なら CDN から取得)。 */
1180
+ type DtmStudioEngines = {
1181
+ SoundFont?: SoundFontEngine;
1182
+ SoundFont_drum?: SoundFontDrumEngine;
1183
+ SoundFont_list?: SoundFontListEngine;
1184
+ parseChord?: DawOptions["parseChord"];
1185
+ parseChords?: DawOptions["parseChords"];
1186
+ parseMidi?: DawOptions["parseMidi"];
1187
+ };
1188
+ /** 既定の取得元URL(rpgen3 / jsDelivr)。options.cdn で個別に上書きできる。 */
1189
+ declare const DEFAULT_CDN: {
1190
+ readonly soundFont: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont.mjs";
1191
+ readonly soundFontDrum: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_drum.mjs";
1192
+ readonly soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs";
1193
+ readonly parseChord: "https://rpgen3.github.io/piano/mjs/parseChord.mjs";
1194
+ readonly parseChords: "https://rpgen3.github.io/piano/mjs/parseChords.mjs";
1195
+ readonly midiParser: "https://cdn.jsdelivr.net/npm/midi-parser-js@4.0.4/+esm";
1196
+ };
1197
+ type DtmStudioOptions = {
1198
+ /** 既存の AudioContext を使う(未指定なら内部生成)。 */
1199
+ audioContext?: AudioContext;
1200
+ /** マスター音量 0-1(楽器・歌声)。既定 1。 */
1201
+ masterVolume?: number;
1202
+ /** ドラム音量 0-1。既定 1。 */
1203
+ drumVolume?: number;
1204
+ /**
1205
+ * 歌声合成ワーカー(voice-worker.js)のURL。
1206
+ * 既定はパッケージ同梱の dist/voice-worker.js。
1207
+ * `null` を渡すとワーカーを使わず klatt のみ(koe音源はメインスレッド合成)。
1208
+ */
1209
+ voiceWorkerUrl?: string | null;
1210
+ /** 初期の楽器プリセットキー(INSTRUMENT_PRESETS)。既定 "retro_game"。 */
1211
+ defaultPreset?: string;
1212
+ /** 外部エンジンの注入(指定したものは CDN 取得をスキップ)。 */
1213
+ engines?: DtmStudioEngines;
1214
+ /** CDN URL の上書き。 */
1215
+ cdn?: Partial<typeof DEFAULT_CDN>;
1216
+ /** 有効化する機能。既定はすべて true。 */
1217
+ features?: {
1218
+ /** 録音→WAVダウンロード(編集UIの録音ボタン)。 */
1219
+ recorder?: boolean;
1220
+ /** MIDIファイル読み込み。 */
1221
+ midi?: boolean;
1222
+ /** コード入力(和音)。 */
1223
+ chord?: boolean;
1224
+ /** 編集UIに楽器プリセット選択UIを差し込む。 */
1225
+ presetUI?: boolean;
1226
+ };
1227
+ };
1228
+ /** 編集UIのマウント時オプション(DawOptions を一部上書きできる)。 */
1229
+ type MountEditorOptions = Partial<DawOptions> & {
1230
+ /** このエディタで読み込む楽器プリセット(未指定なら studio の defaultPreset)。 */
1231
+ preset?: string;
1232
+ /** プリセット選択UIを出すか(未指定なら studio の features.presetUI)。 */
1233
+ presetUI?: boolean;
1234
+ };
1235
+ /** 再生UIのマウント時オプション(MmlPlayerOptions を一部上書きできる)。 */
1236
+ type MountPlayerOptions = Partial<MmlPlayerOptions>;
1237
+ type DtmStudio = {
1238
+ /** 内部で使用している AudioContext。 */
1239
+ audioContext: AudioContext;
1240
+ /** 歌声合成ヘルパ(klatt + koe音源)。 */
1241
+ singingVoices: SingingVoices;
1242
+ /** 編集UI(mountDAW)を音・歌声込みでマウントする。 */
1243
+ mountEditor: (target: HTMLElement, options?: MountEditorOptions) => DawInstance;
1244
+ /** 再生専用UI(mountMmlPlayer)を音・歌声込みでマウントする。 */
1245
+ mountPlayer: (target: HTMLElement, mml: string, options?: MountPlayerOptions) => MmlPlayerInstance;
1246
+ /** 楽器プリセットを(指定トラックぶん)ロードする。 */
1247
+ loadPreset: (presetKey: string, trackIds?: string[]) => Promise<void>;
1248
+ /** AudioContext を閉じ、生成物を破棄する。 */
1249
+ dispose: () => void;
1250
+ };
1251
+ /**
1252
+ * 全部入りスタジオを生成する。SoundFontエンジン・ドラム音源・楽器プリセットの
1253
+ * 初期ロードまで待ってから解決するため、await して使う。
1254
+ */
1255
+ declare const createDtmStudio: (options?: DtmStudioOptions) => Promise<DtmStudio>;
1256
+
1124
1257
  /**
1125
1258
  * mountDAW が注入する自己完結スタイル。
1126
1259
  * すべてのクラスは `dtm-` プレフィックスでスコープし、ホスト側CSSと衝突しにくくする。
@@ -1133,4 +1266,4 @@ declare const DAW_CSS = "\n@font-face {\n font-family: 'k8x12';\n src: url('ht
1133
1266
  */
1134
1267
  declare const injectStyles: (doc?: Document) => void;
1135
1268
 
1136
- export { type AddNoteOptions, type ApplyChordOptions, type ChordPatternType, type ChordPlacement, type ConsumedSyllable, type CoreEventHandlers, DAW_CSS, DEFAULT_BPM, DEFAULT_GATE, DEFAULT_PAN, DEFAULT_PLAYBACK_VELOCITY, DEFAULT_STEPS_PER_BAR, DEFAULT_VELOCITY, DEFAULT_VOCAL_VOLUME, DRUM_FONT, DRUM_KEYS, DRUM_PATTERNS, type DawInstance, type DawOptions, type DawViewState, type DrumPattern, type ExportMidiOptions, INSTRUMENT_PRESETS, type InstrumentPreset, KOE_BASE_URL, KOE_VOICEBANKS, KOE_VOICEBANK_LABELS, KOE_VOICEBANK_TERMS, type KoeVoiceOptions, LinkedList, type LyricSyllable, type LyricTrack, type LyricsConductor, MAX_VOCAL_VOLUME, MMLCore, type MMLDisplayToken, type MMLNotePlacement, type MidiExtraction, type MidiNotePlacement, type MidiTrackAnalysis, type MmlMeta, type MmlPlayerInstance, type MmlPlayerOptions, type Note, PITCH_MAP, PREWARM_NOTES, type ParseChordFn, type ParseChordsFn, type ParseMMLOptions, type ParseMidiFn, type ParsedMML, type PianoRollInstance, type PianoRollOptions, type PitchToken, type PlayDrumEvent, type PlayNoteEvent, type PlaybackState, type PreviewSoundCallback, type RenderConfig, type Sequencer, type SequencerOptions, type SequencerTrack, type SingingVoices, type SingingVoicesOptions, type StreamVoiceNote, type StreamVoiceTrack, TRACKS_ADVANCED, TRACKS_SIMPLE, type ToolMode, type TrackConfig, VOICE_IMAGE_KEY, type VoiceModel, type VoiceRegistry, analyzeMidiTracks, applyHarmonicFilter, applyMonophonic, buildChordPlacements, buildNameToKeyMapping, collectPitchTokens, createAudioContext, createKlattVoice, createKoeVoice, createLyricsConductor, createPianoRoll, createSequencer, createSingingVoices, createVoiceRegistry, decomposeToMonophonic, drawGrid, drawHeader, drawKeyboard, drawNotes, drawSelectedNotes, drawSelectionRect, exportMIDI, extractMidiPlacements, extractMidiPlacementsByTrack, fetchSoundFontList, formatMmlMeta, generateRandomPattern, getDrawOffset, getGridCanvas, getGridContext, getGridPosition, getHeaderCanvas, getMidiBPM, getRenderConfig, getXY, icon, init, injectStyles, isChordHeavyTrack, koeUrl, mountDAW, mountMmlPlayer, normalizeLyrics, onClick, panToStereo, parseLyrics, parseMML, parseMmlMeta, setDrawOffset, setupRecorder, shiftNotes, stripLyrics, stripMmlMeta, vocalVolumeToGain };
1269
+ export { type AddNoteOptions, type ApplyChordOptions, type ChordPatternType, type ChordPlacement, type ConsumedSyllable, type CoreEventHandlers, DAW_CSS, DEFAULT_BPM, DEFAULT_GATE, DEFAULT_PAN, DEFAULT_PLAYBACK_VELOCITY, DEFAULT_STEPS_PER_BAR, DEFAULT_VELOCITY, DEFAULT_VOCAL_VOLUME, DRUM_FONT, DRUM_KEYS, DRUM_PATTERNS, type DawInstance, type DawOptions, type DawViewState, type DrumPattern, type DtmStudio, type DtmStudioEngines, type DtmStudioOptions, type ExportMidiOptions, INSTRUMENT_PRESETS, type InstrumentPreset, KOE_BASE_URL, KOE_VOICEBANKS, KOE_VOICEBANK_LABELS, KOE_VOICEBANK_TERMS, type KoeVoiceOptions, LinkedList, type LyricSyllable, type LyricTrack, type LyricsConductor, MAX_VOCAL_VOLUME, MMLCore, type MMLDisplayToken, type MMLNotePlacement, type MidiExtraction, type MidiNotePlacement, type MidiTrackAnalysis, type MmlMeta, type MmlPlayerInstance, type MmlPlayerOptions, type MountEditorOptions, type MountPlayerOptions, type Note, PITCH_MAP, PREWARM_NOTES, type ParseChordFn, type ParseChordsFn, type ParseMMLOptions, type ParseMidiFn, type ParsedMML, type PianoRollInstance, type PianoRollOptions, type PitchToken, type PlayDrumEvent, type PlayNoteEvent, type PlaybackState, type PreviewSoundCallback, type RenderConfig, type Sequencer, type SequencerOptions, type SequencerTrack, type SingingVoices, type SingingVoicesOptions, type StreamVoiceNote, type StreamVoiceTrack, TRACKS_ADVANCED, TRACKS_SIMPLE, type ToolMode, type TrackConfig, VOICE_IMAGE_KEY, type VoiceModel, type VoiceRegistry, analyzeMidiTracks, applyHarmonicFilter, applyMonophonic, buildChordPlacements, buildNameToKeyMapping, collectPitchTokens, createAudioContext, createDtmStudio, createKlattVoice, createKoeVoice, createLyricsConductor, createPianoRoll, createSequencer, createSingingVoices, createVoiceRegistry, decomposeToMonophonic, drawGrid, drawHeader, drawKeyboard, drawNotes, drawSelectedNotes, drawSelectionRect, exportMIDI, extractMidiPlacements, extractMidiPlacementsByTrack, fetchSoundFontList, formatMmlMeta, generateRandomPattern, getDrawOffset, getGridCanvas, getGridContext, getGridPosition, getHeaderCanvas, getMidiBPM, getRenderConfig, getXY, icon, init, injectStyles, isChordHeavyTrack, koeUrl, mountDAW, mountMmlPlayer, normalizeLyrics, onClick, panToStereo, parseLyrics, parseMML, parseMmlMeta, setDrawOffset, setupRecorder, shiftNotes, stripLyrics, stripMmlMeta, vocalVolumeToGain };
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ __export(index_exports, {
51
51
  buildNameToKeyMapping: () => buildNameToKeyMapping,
52
52
  collectPitchTokens: () => collectPitchTokens,
53
53
  createAudioContext: () => createAudioContext,
54
+ createDtmStudio: () => createDtmStudio,
54
55
  createKlattVoice: () => createKlattVoice,
55
56
  createKoeVoice: () => createKoeVoice,
56
57
  createLyricsConductor: () => createLyricsConductor,
@@ -6490,8 +6491,6 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6490
6491
  if (useSynth) synthPlay(e);
6491
6492
  },
6492
6493
  onPlayDrum: (e) => {
6493
- const em = emojiEls[0];
6494
- if (em) jumpEmojiAt(em, e.when);
6495
6494
  const velocity = e.velocity * (trackVolume / 100);
6496
6495
  options.onPlayDrum?.({ ...e, velocity });
6497
6496
  if (useSynth) drumSynth({ ...e, velocity });
@@ -6884,6 +6883,324 @@ var createPianoRoll = (options, handlers) => {
6884
6883
  }
6885
6884
  };
6886
6885
  };
6886
+
6887
+ // src/studio.ts
6888
+ var import_meta = {};
6889
+ var DEFAULT_CDN = {
6890
+ soundFont: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont.mjs",
6891
+ soundFontDrum: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_drum.mjs",
6892
+ soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs",
6893
+ parseChord: "https://rpgen3.github.io/piano/mjs/parseChord.mjs",
6894
+ parseChords: "https://rpgen3.github.io/piano/mjs/parseChords.mjs",
6895
+ midiParser: "https://cdn.jsdelivr.net/npm/midi-parser-js@4.0.4/+esm"
6896
+ };
6897
+ var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
6898
+ var TRACK_ROLES = ["melody", "submelody", "bass", "chord"];
6899
+ var resolveDefaultVoiceWorkerUrl = () => {
6900
+ try {
6901
+ return new URL("./voice-worker.js", import_meta.url).href;
6902
+ } catch {
6903
+ return void 0;
6904
+ }
6905
+ };
6906
+ var importFrom = async (url, name) => {
6907
+ const mod = await import(
6908
+ /* @vite-ignore */
6909
+ url
6910
+ );
6911
+ return mod[name] ?? mod.default;
6912
+ };
6913
+ var createDtmStudio = async (options = {}) => {
6914
+ const cdn = { ...DEFAULT_CDN, ...options.cdn };
6915
+ const features = {
6916
+ recorder: true,
6917
+ midi: true,
6918
+ chord: true,
6919
+ presetUI: true,
6920
+ ...options.features
6921
+ };
6922
+ const audioCtx = options.audioContext ?? new AudioContext();
6923
+ const masterGain = audioCtx.createGain();
6924
+ masterGain.gain.value = options.masterVolume ?? 1;
6925
+ masterGain.connect(audioCtx.destination);
6926
+ const drumGain = audioCtx.createGain();
6927
+ drumGain.gain.value = options.drumVolume ?? 1;
6928
+ drumGain.connect(audioCtx.destination);
6929
+ const resumeAudio = () => {
6930
+ if (audioCtx.state === "suspended") void audioCtx.resume();
6931
+ };
6932
+ const eng = options.engines ?? {};
6933
+ const [SoundFont, SoundFont_drum, SoundFont_list] = await Promise.all([
6934
+ eng.SoundFont ?? importFrom(cdn.soundFont, "SoundFont"),
6935
+ eng.SoundFont_drum ?? importFrom(cdn.soundFontDrum, "SoundFont_drum"),
6936
+ eng.SoundFont_list ?? importFrom(cdn.soundFontList, "SoundFont_list")
6937
+ ]);
6938
+ let parseChord = eng.parseChord;
6939
+ let parseChords = eng.parseChords;
6940
+ if (features.chord && (!parseChord || !parseChords)) {
6941
+ try {
6942
+ [parseChord, parseChords] = await Promise.all([
6943
+ parseChord ?? importFrom(
6944
+ cdn.parseChord,
6945
+ "parseChord"
6946
+ ),
6947
+ parseChords ?? importFrom(
6948
+ cdn.parseChords,
6949
+ "parseChords"
6950
+ )
6951
+ ]);
6952
+ } catch (e) {
6953
+ console.warn("[dtm] \u30B3\u30FC\u30C9\u89E3\u6790\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F", e);
6954
+ }
6955
+ }
6956
+ let midiParser = null;
6957
+ let parseMidi;
6958
+ if (features.midi) {
6959
+ parseMidi = eng.parseMidi;
6960
+ if (!parseMidi) {
6961
+ const midiPromise = importFrom(
6962
+ cdn.midiParser,
6963
+ "default"
6964
+ ).then((m) => {
6965
+ midiParser = m;
6966
+ }).catch((e) => console.warn("[dtm] midi-parser \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557", e));
6967
+ void midiPromise;
6968
+ parseMidi = (bytes) => {
6969
+ if (!midiParser) throw new Error("midi-parser not ready");
6970
+ return midiParser.parse(bytes);
6971
+ };
6972
+ }
6973
+ }
6974
+ const voiceWorkerUrl = options.voiceWorkerUrl === null ? void 0 : options.voiceWorkerUrl ?? resolveDefaultVoiceWorkerUrl();
6975
+ const singingVoices = createSingingVoices(audioCtx, masterGain, {
6976
+ voiceWorkerUrl
6977
+ });
6978
+ const recorder = features.recorder ? setupRecorder(audioCtx, masterGain, drumGain) : null;
6979
+ const downloadWav = () => {
6980
+ if (!recorder) return;
6981
+ const recordedData = recorder.getRecordedData();
6982
+ const ch = recordedData.length;
6983
+ const len = recordedData[0].length;
6984
+ if (len === 0) return;
6985
+ const bufSize = recordedData[0][0].length;
6986
+ const wave = new Float32Array(ch * len * bufSize);
6987
+ let idx = 0;
6988
+ for (let i = 0; i < len; i++)
6989
+ for (let j = 0; j < bufSize; j++)
6990
+ for (let k = 0; k < ch; k++) wave[idx++] = recordedData[k][i][j];
6991
+ const sampleRate = audioCtx.sampleRate;
6992
+ const channels = 2;
6993
+ const bitRate = 16;
6994
+ const step = bitRate / 8;
6995
+ const blockSize = channels * step;
6996
+ const byteLen = wave.length * step;
6997
+ const view = new DataView(new ArrayBuffer(44 + byteLen));
6998
+ const ws = (off2, s) => {
6999
+ for (let i = 0; i < s.length; i++)
7000
+ view.setUint8(off2 + i, s.charCodeAt(i));
7001
+ };
7002
+ ws(0, "RIFF");
7003
+ view.setUint32(4, 32 + byteLen, true);
7004
+ ws(8, "WAVE");
7005
+ ws(12, "fmt ");
7006
+ view.setUint32(16, 16, true);
7007
+ view.setUint16(20, 1, true);
7008
+ view.setUint16(22, channels, true);
7009
+ view.setUint32(24, sampleRate, true);
7010
+ view.setUint32(28, sampleRate * blockSize, true);
7011
+ view.setUint16(32, blockSize, true);
7012
+ view.setUint16(34, bitRate, true);
7013
+ ws(36, "data");
7014
+ view.setUint32(40, byteLen, true);
7015
+ const clamp4 = (n, a2, b) => Math.max(a2, Math.min(b, n));
7016
+ let off = 44;
7017
+ for (let i = 0; i < wave.length; i++, off += step)
7018
+ view.setInt16(
7019
+ off,
7020
+ clamp4(Math.round(wave[i] * 32768), -32768, 32767),
7021
+ true
7022
+ );
7023
+ const blob = new Blob([view], { type: "audio/wav" });
7024
+ const url = URL.createObjectURL(blob);
7025
+ const a = document.createElement("a");
7026
+ a.href = url;
7027
+ a.download = "record.wav";
7028
+ a.click();
7029
+ URL.revokeObjectURL(url);
7030
+ };
7031
+ const onToggleRecord = recorder ? () => {
7032
+ if (recorder.isRecording()) {
7033
+ recorder.stopRecording();
7034
+ downloadWav();
7035
+ } else {
7036
+ recorder.clearRecordedData();
7037
+ recorder.startRecording();
7038
+ }
7039
+ } : void 0;
7040
+ const listReady = new Promise((resolve) => {
7041
+ SoundFont_list.init();
7042
+ SoundFont_list.onload(() => resolve());
7043
+ });
7044
+ const drumReady = (async () => {
7045
+ try {
7046
+ await SoundFont_drum.load({
7047
+ ctx: audioCtx,
7048
+ font: DRUM_FONT,
7049
+ id: "0",
7050
+ keys: Object.values(DRUM_KEYS)
7051
+ });
7052
+ } catch (e) {
7053
+ console.error("[dtm] \u30C9\u30E9\u30E0\u97F3\u6E90\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557", e);
7054
+ }
7055
+ })();
7056
+ let nameToKey = {};
7057
+ const soundFonts = /* @__PURE__ */ new Map();
7058
+ const loadedKeyByTrack = /* @__PURE__ */ new Map();
7059
+ const loadSoundFont = async (instrumentKey, trackId) => {
7060
+ if (loadedKeyByTrack.get(trackId) === instrumentKey) return;
7061
+ try {
7062
+ const fullName = `${instrumentKey}_${SOUNDFONT_NAME}`;
7063
+ soundFonts.set(
7064
+ trackId,
7065
+ await SoundFont.load({
7066
+ ctx: audioCtx,
7067
+ fontName: `_tone_${fullName}`,
7068
+ url: SoundFont.toURL(fullName)
7069
+ })
7070
+ );
7071
+ loadedKeyByTrack.set(trackId, instrumentKey);
7072
+ } catch (e) {
7073
+ console.error(`[dtm] \u697D\u5668 "${instrumentKey}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557`, e);
7074
+ }
7075
+ };
7076
+ const defaultPreset = options.defaultPreset ?? "retro_game";
7077
+ const instrumentNameFor = (preset, trackId) => preset[trackId] ?? preset.melody;
7078
+ const loadPreset = async (presetKey, trackIds = [...TRACK_ROLES]) => {
7079
+ const preset = INSTRUMENT_PRESETS[presetKey];
7080
+ if (!preset) return;
7081
+ await listReady;
7082
+ await Promise.all(
7083
+ trackIds.map((trackId) => {
7084
+ const key = nameToKey[instrumentNameFor(preset, trackId)];
7085
+ return key ? loadSoundFont(key, trackId) : Promise.resolve();
7086
+ })
7087
+ );
7088
+ };
7089
+ await listReady;
7090
+ nameToKey = await buildNameToKeyMapping();
7091
+ await Promise.all([drumReady, loadPreset(defaultPreset)]);
7092
+ const playNote = (e) => {
7093
+ const sf = soundFonts.get(e.trackId);
7094
+ if (!sf) return;
7095
+ sf.play({
7096
+ ctx: audioCtx,
7097
+ destination: masterGain,
7098
+ pitch: e.pitch,
7099
+ volume: e.volume,
7100
+ when: e.when,
7101
+ duration: e.duration
7102
+ });
7103
+ };
7104
+ const playDrum = (e) => {
7105
+ if (!SoundFont_drum.font) return;
7106
+ SoundFont_drum.play({
7107
+ ctx: audioCtx,
7108
+ destination: drumGain,
7109
+ pitch: e.pitch,
7110
+ volume: e.velocity,
7111
+ when: e.when,
7112
+ duration: e.duration
7113
+ });
7114
+ };
7115
+ const sfForPlayerTrack = (trackId) => soundFonts.get(TRACK_ROLES[Number(trackId)] ?? "") ?? soundFonts.get(`t${trackId}`);
7116
+ const playPlayerNote = (e) => {
7117
+ const sf = sfForPlayerTrack(e.trackId);
7118
+ if (!sf) return;
7119
+ sf.play({
7120
+ ctx: audioCtx,
7121
+ destination: masterGain,
7122
+ pitch: e.pitch,
7123
+ volume: e.volume,
7124
+ when: e.when,
7125
+ duration: e.duration
7126
+ });
7127
+ };
7128
+ const editorPresetSelects = /* @__PURE__ */ new WeakMap();
7129
+ const mountedEditors = [];
7130
+ const mountedPlayers = [];
7131
+ const mountEditor = (target, opts = {}) => {
7132
+ const { preset, presetUI, ...dawOverrides } = opts;
7133
+ const tracks = dawOverrides.tracks ?? TRACKS_SIMPLE;
7134
+ const trackIds = tracks.map((t) => t.id);
7135
+ const base = {
7136
+ getAudioTime: () => audioCtx.currentTime,
7137
+ onResumeAudio: resumeAudio,
7138
+ onPlayNote: playNote,
7139
+ onPlayDrum: playDrum,
7140
+ singingVoices,
7141
+ parseChord,
7142
+ parseChords,
7143
+ parseMidi,
7144
+ onToggleRecord,
7145
+ ...dawOverrides
7146
+ };
7147
+ const wantPresetUI = presetUI ?? features.presetUI;
7148
+ if (wantPresetUI) {
7149
+ const select = target.ownerDocument.createElement("select");
7150
+ select.className = "dtm-studio-preset";
7151
+ for (const [key, p] of Object.entries(INSTRUMENT_PRESETS)) {
7152
+ const opt = target.ownerDocument.createElement("option");
7153
+ opt.value = key;
7154
+ opt.textContent = p.displayName;
7155
+ select.appendChild(opt);
7156
+ }
7157
+ select.value = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
7158
+ target.appendChild(select);
7159
+ editorPresetSelects.set(target, select);
7160
+ select.addEventListener("change", async () => {
7161
+ daw.setInstrument(select.value);
7162
+ await loadPreset(select.value, trackIds);
7163
+ });
7164
+ }
7165
+ const daw = mountDAW(target, base);
7166
+ mountedEditors.push(daw);
7167
+ const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
7168
+ daw.setInstrument(presetKey);
7169
+ void loadPreset(presetKey, trackIds);
7170
+ return daw;
7171
+ };
7172
+ const mountPlayer = (target, mml, opts = {}) => {
7173
+ const meta = parseMML(mml, {}).meta ?? {};
7174
+ if (meta.instrument && INSTRUMENT_PRESETS[meta.instrument]) {
7175
+ void loadPreset(meta.instrument);
7176
+ }
7177
+ const player = mountMmlPlayer(target, mml, {
7178
+ getAudioTime: () => audioCtx.currentTime,
7179
+ onResumeAudio: resumeAudio,
7180
+ onPlayNote: playPlayerNote,
7181
+ onPlayDrum: playDrum,
7182
+ singingVoices,
7183
+ ...opts
7184
+ });
7185
+ mountedPlayers.push(player);
7186
+ return player;
7187
+ };
7188
+ const dispose = () => {
7189
+ for (const p of mountedPlayers) p.destroy();
7190
+ for (const d of mountedEditors) d.destroy();
7191
+ mountedPlayers.length = 0;
7192
+ mountedEditors.length = 0;
7193
+ void audioCtx.close();
7194
+ };
7195
+ return {
7196
+ audioContext: audioCtx,
7197
+ singingVoices,
7198
+ mountEditor,
7199
+ mountPlayer,
7200
+ loadPreset,
7201
+ dispose
7202
+ };
7203
+ };
6887
7204
  // Annotate the CommonJS export names for ESM import in node:
6888
7205
  0 && (module.exports = {
6889
7206
  DAW_CSS,
@@ -6917,6 +7234,7 @@ var createPianoRoll = (options, handlers) => {
6917
7234
  buildNameToKeyMapping,
6918
7235
  collectPitchTokens,
6919
7236
  createAudioContext,
7237
+ createDtmStudio,
6920
7238
  createKlattVoice,
6921
7239
  createKoeVoice,
6922
7240
  createLyricsConductor,
package/dist/index.mjs CHANGED
@@ -6387,8 +6387,6 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6387
6387
  if (useSynth) synthPlay(e);
6388
6388
  },
6389
6389
  onPlayDrum: (e) => {
6390
- const em = emojiEls[0];
6391
- if (em) jumpEmojiAt(em, e.when);
6392
6390
  const velocity = e.velocity * (trackVolume / 100);
6393
6391
  options.onPlayDrum?.({ ...e, velocity });
6394
6392
  if (useSynth) drumSynth({ ...e, velocity });
@@ -6781,6 +6779,323 @@ var createPianoRoll = (options, handlers) => {
6781
6779
  }
6782
6780
  };
6783
6781
  };
6782
+
6783
+ // src/studio.ts
6784
+ var DEFAULT_CDN = {
6785
+ soundFont: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont.mjs",
6786
+ soundFontDrum: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_drum.mjs",
6787
+ soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs",
6788
+ parseChord: "https://rpgen3.github.io/piano/mjs/parseChord.mjs",
6789
+ parseChords: "https://rpgen3.github.io/piano/mjs/parseChords.mjs",
6790
+ midiParser: "https://cdn.jsdelivr.net/npm/midi-parser-js@4.0.4/+esm"
6791
+ };
6792
+ var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
6793
+ var TRACK_ROLES = ["melody", "submelody", "bass", "chord"];
6794
+ var resolveDefaultVoiceWorkerUrl = () => {
6795
+ try {
6796
+ return new URL("./voice-worker.js", import.meta.url).href;
6797
+ } catch {
6798
+ return void 0;
6799
+ }
6800
+ };
6801
+ var importFrom = async (url, name) => {
6802
+ const mod = await import(
6803
+ /* @vite-ignore */
6804
+ url
6805
+ );
6806
+ return mod[name] ?? mod.default;
6807
+ };
6808
+ var createDtmStudio = async (options = {}) => {
6809
+ const cdn = { ...DEFAULT_CDN, ...options.cdn };
6810
+ const features = {
6811
+ recorder: true,
6812
+ midi: true,
6813
+ chord: true,
6814
+ presetUI: true,
6815
+ ...options.features
6816
+ };
6817
+ const audioCtx = options.audioContext ?? new AudioContext();
6818
+ const masterGain = audioCtx.createGain();
6819
+ masterGain.gain.value = options.masterVolume ?? 1;
6820
+ masterGain.connect(audioCtx.destination);
6821
+ const drumGain = audioCtx.createGain();
6822
+ drumGain.gain.value = options.drumVolume ?? 1;
6823
+ drumGain.connect(audioCtx.destination);
6824
+ const resumeAudio = () => {
6825
+ if (audioCtx.state === "suspended") void audioCtx.resume();
6826
+ };
6827
+ const eng = options.engines ?? {};
6828
+ const [SoundFont, SoundFont_drum, SoundFont_list] = await Promise.all([
6829
+ eng.SoundFont ?? importFrom(cdn.soundFont, "SoundFont"),
6830
+ eng.SoundFont_drum ?? importFrom(cdn.soundFontDrum, "SoundFont_drum"),
6831
+ eng.SoundFont_list ?? importFrom(cdn.soundFontList, "SoundFont_list")
6832
+ ]);
6833
+ let parseChord = eng.parseChord;
6834
+ let parseChords = eng.parseChords;
6835
+ if (features.chord && (!parseChord || !parseChords)) {
6836
+ try {
6837
+ [parseChord, parseChords] = await Promise.all([
6838
+ parseChord ?? importFrom(
6839
+ cdn.parseChord,
6840
+ "parseChord"
6841
+ ),
6842
+ parseChords ?? importFrom(
6843
+ cdn.parseChords,
6844
+ "parseChords"
6845
+ )
6846
+ ]);
6847
+ } catch (e) {
6848
+ console.warn("[dtm] \u30B3\u30FC\u30C9\u89E3\u6790\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F", e);
6849
+ }
6850
+ }
6851
+ let midiParser = null;
6852
+ let parseMidi;
6853
+ if (features.midi) {
6854
+ parseMidi = eng.parseMidi;
6855
+ if (!parseMidi) {
6856
+ const midiPromise = importFrom(
6857
+ cdn.midiParser,
6858
+ "default"
6859
+ ).then((m) => {
6860
+ midiParser = m;
6861
+ }).catch((e) => console.warn("[dtm] midi-parser \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557", e));
6862
+ void midiPromise;
6863
+ parseMidi = (bytes) => {
6864
+ if (!midiParser) throw new Error("midi-parser not ready");
6865
+ return midiParser.parse(bytes);
6866
+ };
6867
+ }
6868
+ }
6869
+ const voiceWorkerUrl = options.voiceWorkerUrl === null ? void 0 : options.voiceWorkerUrl ?? resolveDefaultVoiceWorkerUrl();
6870
+ const singingVoices = createSingingVoices(audioCtx, masterGain, {
6871
+ voiceWorkerUrl
6872
+ });
6873
+ const recorder = features.recorder ? setupRecorder(audioCtx, masterGain, drumGain) : null;
6874
+ const downloadWav = () => {
6875
+ if (!recorder) return;
6876
+ const recordedData = recorder.getRecordedData();
6877
+ const ch = recordedData.length;
6878
+ const len = recordedData[0].length;
6879
+ if (len === 0) return;
6880
+ const bufSize = recordedData[0][0].length;
6881
+ const wave = new Float32Array(ch * len * bufSize);
6882
+ let idx = 0;
6883
+ for (let i = 0; i < len; i++)
6884
+ for (let j = 0; j < bufSize; j++)
6885
+ for (let k = 0; k < ch; k++) wave[idx++] = recordedData[k][i][j];
6886
+ const sampleRate = audioCtx.sampleRate;
6887
+ const channels = 2;
6888
+ const bitRate = 16;
6889
+ const step = bitRate / 8;
6890
+ const blockSize = channels * step;
6891
+ const byteLen = wave.length * step;
6892
+ const view = new DataView(new ArrayBuffer(44 + byteLen));
6893
+ const ws = (off2, s) => {
6894
+ for (let i = 0; i < s.length; i++)
6895
+ view.setUint8(off2 + i, s.charCodeAt(i));
6896
+ };
6897
+ ws(0, "RIFF");
6898
+ view.setUint32(4, 32 + byteLen, true);
6899
+ ws(8, "WAVE");
6900
+ ws(12, "fmt ");
6901
+ view.setUint32(16, 16, true);
6902
+ view.setUint16(20, 1, true);
6903
+ view.setUint16(22, channels, true);
6904
+ view.setUint32(24, sampleRate, true);
6905
+ view.setUint32(28, sampleRate * blockSize, true);
6906
+ view.setUint16(32, blockSize, true);
6907
+ view.setUint16(34, bitRate, true);
6908
+ ws(36, "data");
6909
+ view.setUint32(40, byteLen, true);
6910
+ const clamp4 = (n, a2, b) => Math.max(a2, Math.min(b, n));
6911
+ let off = 44;
6912
+ for (let i = 0; i < wave.length; i++, off += step)
6913
+ view.setInt16(
6914
+ off,
6915
+ clamp4(Math.round(wave[i] * 32768), -32768, 32767),
6916
+ true
6917
+ );
6918
+ const blob = new Blob([view], { type: "audio/wav" });
6919
+ const url = URL.createObjectURL(blob);
6920
+ const a = document.createElement("a");
6921
+ a.href = url;
6922
+ a.download = "record.wav";
6923
+ a.click();
6924
+ URL.revokeObjectURL(url);
6925
+ };
6926
+ const onToggleRecord = recorder ? () => {
6927
+ if (recorder.isRecording()) {
6928
+ recorder.stopRecording();
6929
+ downloadWav();
6930
+ } else {
6931
+ recorder.clearRecordedData();
6932
+ recorder.startRecording();
6933
+ }
6934
+ } : void 0;
6935
+ const listReady = new Promise((resolve) => {
6936
+ SoundFont_list.init();
6937
+ SoundFont_list.onload(() => resolve());
6938
+ });
6939
+ const drumReady = (async () => {
6940
+ try {
6941
+ await SoundFont_drum.load({
6942
+ ctx: audioCtx,
6943
+ font: DRUM_FONT,
6944
+ id: "0",
6945
+ keys: Object.values(DRUM_KEYS)
6946
+ });
6947
+ } catch (e) {
6948
+ console.error("[dtm] \u30C9\u30E9\u30E0\u97F3\u6E90\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557", e);
6949
+ }
6950
+ })();
6951
+ let nameToKey = {};
6952
+ const soundFonts = /* @__PURE__ */ new Map();
6953
+ const loadedKeyByTrack = /* @__PURE__ */ new Map();
6954
+ const loadSoundFont = async (instrumentKey, trackId) => {
6955
+ if (loadedKeyByTrack.get(trackId) === instrumentKey) return;
6956
+ try {
6957
+ const fullName = `${instrumentKey}_${SOUNDFONT_NAME}`;
6958
+ soundFonts.set(
6959
+ trackId,
6960
+ await SoundFont.load({
6961
+ ctx: audioCtx,
6962
+ fontName: `_tone_${fullName}`,
6963
+ url: SoundFont.toURL(fullName)
6964
+ })
6965
+ );
6966
+ loadedKeyByTrack.set(trackId, instrumentKey);
6967
+ } catch (e) {
6968
+ console.error(`[dtm] \u697D\u5668 "${instrumentKey}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557`, e);
6969
+ }
6970
+ };
6971
+ const defaultPreset = options.defaultPreset ?? "retro_game";
6972
+ const instrumentNameFor = (preset, trackId) => preset[trackId] ?? preset.melody;
6973
+ const loadPreset = async (presetKey, trackIds = [...TRACK_ROLES]) => {
6974
+ const preset = INSTRUMENT_PRESETS[presetKey];
6975
+ if (!preset) return;
6976
+ await listReady;
6977
+ await Promise.all(
6978
+ trackIds.map((trackId) => {
6979
+ const key = nameToKey[instrumentNameFor(preset, trackId)];
6980
+ return key ? loadSoundFont(key, trackId) : Promise.resolve();
6981
+ })
6982
+ );
6983
+ };
6984
+ await listReady;
6985
+ nameToKey = await buildNameToKeyMapping();
6986
+ await Promise.all([drumReady, loadPreset(defaultPreset)]);
6987
+ const playNote = (e) => {
6988
+ const sf = soundFonts.get(e.trackId);
6989
+ if (!sf) return;
6990
+ sf.play({
6991
+ ctx: audioCtx,
6992
+ destination: masterGain,
6993
+ pitch: e.pitch,
6994
+ volume: e.volume,
6995
+ when: e.when,
6996
+ duration: e.duration
6997
+ });
6998
+ };
6999
+ const playDrum = (e) => {
7000
+ if (!SoundFont_drum.font) return;
7001
+ SoundFont_drum.play({
7002
+ ctx: audioCtx,
7003
+ destination: drumGain,
7004
+ pitch: e.pitch,
7005
+ volume: e.velocity,
7006
+ when: e.when,
7007
+ duration: e.duration
7008
+ });
7009
+ };
7010
+ const sfForPlayerTrack = (trackId) => soundFonts.get(TRACK_ROLES[Number(trackId)] ?? "") ?? soundFonts.get(`t${trackId}`);
7011
+ const playPlayerNote = (e) => {
7012
+ const sf = sfForPlayerTrack(e.trackId);
7013
+ if (!sf) return;
7014
+ sf.play({
7015
+ ctx: audioCtx,
7016
+ destination: masterGain,
7017
+ pitch: e.pitch,
7018
+ volume: e.volume,
7019
+ when: e.when,
7020
+ duration: e.duration
7021
+ });
7022
+ };
7023
+ const editorPresetSelects = /* @__PURE__ */ new WeakMap();
7024
+ const mountedEditors = [];
7025
+ const mountedPlayers = [];
7026
+ const mountEditor = (target, opts = {}) => {
7027
+ const { preset, presetUI, ...dawOverrides } = opts;
7028
+ const tracks = dawOverrides.tracks ?? TRACKS_SIMPLE;
7029
+ const trackIds = tracks.map((t) => t.id);
7030
+ const base = {
7031
+ getAudioTime: () => audioCtx.currentTime,
7032
+ onResumeAudio: resumeAudio,
7033
+ onPlayNote: playNote,
7034
+ onPlayDrum: playDrum,
7035
+ singingVoices,
7036
+ parseChord,
7037
+ parseChords,
7038
+ parseMidi,
7039
+ onToggleRecord,
7040
+ ...dawOverrides
7041
+ };
7042
+ const wantPresetUI = presetUI ?? features.presetUI;
7043
+ if (wantPresetUI) {
7044
+ const select = target.ownerDocument.createElement("select");
7045
+ select.className = "dtm-studio-preset";
7046
+ for (const [key, p] of Object.entries(INSTRUMENT_PRESETS)) {
7047
+ const opt = target.ownerDocument.createElement("option");
7048
+ opt.value = key;
7049
+ opt.textContent = p.displayName;
7050
+ select.appendChild(opt);
7051
+ }
7052
+ select.value = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
7053
+ target.appendChild(select);
7054
+ editorPresetSelects.set(target, select);
7055
+ select.addEventListener("change", async () => {
7056
+ daw.setInstrument(select.value);
7057
+ await loadPreset(select.value, trackIds);
7058
+ });
7059
+ }
7060
+ const daw = mountDAW(target, base);
7061
+ mountedEditors.push(daw);
7062
+ const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
7063
+ daw.setInstrument(presetKey);
7064
+ void loadPreset(presetKey, trackIds);
7065
+ return daw;
7066
+ };
7067
+ const mountPlayer = (target, mml, opts = {}) => {
7068
+ const meta = parseMML(mml, {}).meta ?? {};
7069
+ if (meta.instrument && INSTRUMENT_PRESETS[meta.instrument]) {
7070
+ void loadPreset(meta.instrument);
7071
+ }
7072
+ const player = mountMmlPlayer(target, mml, {
7073
+ getAudioTime: () => audioCtx.currentTime,
7074
+ onResumeAudio: resumeAudio,
7075
+ onPlayNote: playPlayerNote,
7076
+ onPlayDrum: playDrum,
7077
+ singingVoices,
7078
+ ...opts
7079
+ });
7080
+ mountedPlayers.push(player);
7081
+ return player;
7082
+ };
7083
+ const dispose = () => {
7084
+ for (const p of mountedPlayers) p.destroy();
7085
+ for (const d of mountedEditors) d.destroy();
7086
+ mountedPlayers.length = 0;
7087
+ mountedEditors.length = 0;
7088
+ void audioCtx.close();
7089
+ };
7090
+ return {
7091
+ audioContext: audioCtx,
7092
+ singingVoices,
7093
+ mountEditor,
7094
+ mountPlayer,
7095
+ loadPreset,
7096
+ dispose
7097
+ };
7098
+ };
6784
7099
  export {
6785
7100
  DAW_CSS,
6786
7101
  DEFAULT_BPM,
@@ -6813,6 +7128,7 @@ export {
6813
7128
  buildNameToKeyMapping,
6814
7129
  collectPitchTokens,
6815
7130
  createAudioContext,
7131
+ createDtmStudio,
6816
7132
  createKlattVoice,
6817
7133
  createKoeVoice,
6818
7134
  createLyricsConductor,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "onjmin",
3
3
  "license": "MIT",
4
4
  "name": "@onjmin/dtm",
5
- "version": "0.1.4",
5
+ "version": "0.1.5",
6
6
  "description": "MMLを中間言語に用いた、モバイルファーストなDAW / ピアノロール打ち込みコンポーネント",
7
7
  "homepage": "https://onjmin.github.io/dtm",
8
8
  "repository": {