@onjmin/dtm 0.1.0

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.
@@ -0,0 +1,628 @@
1
+ /**
2
+ * オーディオ設定とSoundFont管理
3
+ * Audio Context初期化、SoundFont読み込み、ノート再生等
4
+ */
5
+ /**
6
+ * Audio Context初期化
7
+ * DOMに依存しない純粋な音声設定
8
+ */
9
+ declare function createAudioContext(): {
10
+ audioCtx: AudioContext;
11
+ gainNode: GainNode;
12
+ drumGainNode: GainNode;
13
+ };
14
+ /**
15
+ * オーディオレコーダーのセットアップ
16
+ */
17
+ declare function setupRecorder(audioCtx: AudioContext, gainNode: GainNode, drumGainNode: GainNode): {
18
+ startRecording: () => void;
19
+ stopRecording: () => void;
20
+ getRecordedData: () => [Float32Array<ArrayBufferLike>[], Float32Array<ArrayBufferLike>[]];
21
+ isRecording: () => boolean;
22
+ clearRecordedData: () => void;
23
+ };
24
+ /**
25
+ * SoundFontリストの取得
26
+ */
27
+ declare function fetchSoundFontList(ttl: string): Promise<string[]>;
28
+ /**
29
+ * 楽器名からキーへのマッピングを構築
30
+ */
31
+ declare function buildNameToKeyMapping(): Promise<Record<string, string>>;
32
+
33
+ /**
34
+ * ドラム音源関連の設定
35
+ * ドラムキー値とドラムパターンを定義
36
+ */
37
+ declare const DRUM_FONT = "FluidR3_GM_sf2_file";
38
+ declare const DRUM_KEYS: {
39
+ readonly kick: 36;
40
+ readonly snare: 38;
41
+ readonly clap: 39;
42
+ readonly rimshot: 37;
43
+ readonly hihatClosed: 42;
44
+ readonly hihatPedal: 44;
45
+ readonly hihatOpen: 46;
46
+ readonly tomLow: 45;
47
+ readonly tomMid: 47;
48
+ readonly tomHigh: 50;
49
+ readonly crash: 49;
50
+ readonly ride: 51;
51
+ readonly splash: 55;
52
+ readonly tambourine: 54;
53
+ };
54
+ type DrumPattern = {
55
+ step: number;
56
+ pitch: number;
57
+ velocity: number;
58
+ }[];
59
+ declare const DRUM_PATTERNS: Record<string, DrumPattern>;
60
+
61
+ type Note = {
62
+ id: number;
63
+ startStep: number;
64
+ durationSteps: number;
65
+ pitch: number;
66
+ velocity?: number;
67
+ };
68
+ type RenderConfig = {
69
+ stepsPerBar: number;
70
+ keyCount: number;
71
+ pitchRangeStart: number;
72
+ keyHeight: number;
73
+ stepWidth: number;
74
+ };
75
+ type CoreEventHandlers = {
76
+ onMMLGenerated: (mml: string) => void;
77
+ onNotesChanged: (notes: Note[]) => void;
78
+ onNoteClick?: (step: number, pitch: number, isErasing: boolean) => void;
79
+ };
80
+ type PreviewSoundCallback = (pitch: number, position: number) => void;
81
+ type AddNoteOptions = {
82
+ noteLengthSteps: number;
83
+ velocity?: number;
84
+ };
85
+ type PianoRollOptions = {
86
+ mountTarget: HTMLElement;
87
+ width?: number;
88
+ height?: number;
89
+ config: RenderConfig;
90
+ noteLengthSteps?: number;
91
+ onPreviewSound?: PreviewSoundCallback;
92
+ };
93
+ type ToolMode = "pen" | "select" | "eraser";
94
+ type TrackConfig = {
95
+ id: string;
96
+ name: string;
97
+ /** ノート描画色 [r, g, b] */
98
+ color: [number, number, number];
99
+ /** MMLチャンネル番号 (@n)。再生時のトラック識別にも使う */
100
+ instrument: number;
101
+ /** 0-127。velocity兼ボリューム(既存挙動を踏襲) */
102
+ volume: number;
103
+ };
104
+ type PlayNoteEvent = {
105
+ trackId: string;
106
+ pitch: number;
107
+ /** 元ノートのvelocity (0-127) */
108
+ velocity: number;
109
+ /** トラックvolume×velocityを反映した 0-1 程度の音量係数 */
110
+ volume: number;
111
+ /** 「今」からの相対秒。利用側は audioCtx.currentTime + when で発音する */
112
+ when: number;
113
+ /** 秒 */
114
+ duration: number;
115
+ };
116
+ type PlayDrumEvent = {
117
+ pitch: number;
118
+ /** 0-1 */
119
+ velocity: number;
120
+ /** 「今」からの相対秒 */
121
+ when: number;
122
+ /** 秒 */
123
+ duration: number;
124
+ };
125
+ type ParseChordFn = (str: string) => {
126
+ value: Iterable<number>;
127
+ };
128
+ type ParseChordsFn = (str: string, bpm: number) => Array<{
129
+ key: string;
130
+ chord: string;
131
+ when: number;
132
+ duration: number;
133
+ }>;
134
+ type ParseMidiFn = (bytes: Uint8Array) => unknown;
135
+ type DawOptions = {
136
+ /** メロディックトラックのノート発音要求 */
137
+ onPlayNote?: (e: PlayNoteEvent) => void;
138
+ /** ドラムノート発音要求 */
139
+ onPlayDrum?: (e: PlayDrumEvent) => void;
140
+ /** 初回ユーザー操作時に呼ばれる(AudioContextのresume等に使う) */
141
+ onResumeAudio?: () => void | Promise<void>;
142
+ /** 再生の基準クロック秒。既定 performance.now()/1000。利用側は audioCtx.currentTime を返す */
143
+ getAudioTime?: () => number;
144
+ /** 録音ボタン押下(利用側のオーディオグラフに依存するため任意) */
145
+ onToggleRecord?: () => void;
146
+ parseChord?: ParseChordFn;
147
+ parseChords?: ParseChordsFn;
148
+ parseMidi?: ParseMidiFn;
149
+ /** トラック構成。既定は melody/submelody/bass/chord の4本 */
150
+ tracks?: TrackConfig[];
151
+ /** ドラムパターン辞書。既定は DRUM_PATTERNS */
152
+ drumPatterns?: Record<string, DrumPattern>;
153
+ defaultBpm?: number;
154
+ initialMML?: string;
155
+ };
156
+ type PlaybackState = "stopped" | "playing" | "paused";
157
+ type DawInstance = {
158
+ play: () => void;
159
+ pause: () => void;
160
+ stop: () => void;
161
+ getMML: () => {
162
+ full: string;
163
+ minified: string;
164
+ };
165
+ loadMML: (mml: string) => void;
166
+ loadMIDI: (bytes: Uint8Array) => void;
167
+ exportMIDI: () => Blob;
168
+ setBpm: (bpm: number) => void;
169
+ getPlaybackState: () => PlaybackState;
170
+ destroy: () => void;
171
+ };
172
+
173
+ /**
174
+ * コード進行文字列を伴奏トラックのノート配置へ展開する。
175
+ *
176
+ * parseChord / parseChords は外部(rpgen3)実装を注入する想定。
177
+ * 旧 demo/index.html の applyChordProgression を移植・整理し、
178
+ * 実際のノート追加を行わず配置(placement)の配列を返す純関数にした。
179
+ */
180
+
181
+ type ChordPatternType = "block" | "arpeggio" | "arpeggio-fast" | "offbeat" | "yatsume" | "alternating";
182
+ type ChordPlacement = {
183
+ startStep: number;
184
+ pitch: number;
185
+ durationSteps: number;
186
+ velocity: number;
187
+ };
188
+ type ApplyChordOptions = {
189
+ chordStr: string;
190
+ patternType: ChordPatternType;
191
+ /** ルートのセミトーンシフト(0-11) */
192
+ rootShift: number;
193
+ bpm: number;
194
+ stepsPerBar: number;
195
+ parseChord: ParseChordFn;
196
+ parseChords: ParseChordsFn;
197
+ };
198
+ /**
199
+ * コード進行 → 伴奏ノート配置。
200
+ */
201
+ declare const buildChordPlacements: (options: ApplyChordOptions) => ChordPlacement[];
202
+
203
+ /**
204
+ * mountDAW — 1関数でマウントできるモバイルファーストDAWコンポーネント(Layer 2)。
205
+ *
206
+ * 発音は注入フック(onPlayNote / onPlayDrum)へ委譲し、ライブラリ自体は音を出さない。
207
+ * MIDI/コード解析も注入(parseMidi / parseChord / parseChords)。未注入なら該当UIを隠す。
208
+ */
209
+
210
+ /** シンプルモード(4トラック)— 役割別に自動分類してMIDIを読み込む */
211
+ declare const TRACKS_SIMPLE: TrackConfig[];
212
+ /** 上級者モード(16トラック)— MIDIトラックを1:1で扱う */
213
+ declare const TRACKS_ADVANCED: TrackConfig[];
214
+ /**
215
+ * DAWコンポーネントをマウントする。
216
+ */
217
+ declare const mountDAW: (target: HTMLElement, options?: DawOptions) => DawInstance;
218
+
219
+ /**
220
+ * インラインSVGアイコン。外部アイコンフォント(MDI等)への依存を避けるため、
221
+ * 必要最小限のパスをライブラリ内に同梱する。
222
+ */
223
+ type IconDef = {
224
+ d: string;
225
+ stroke?: boolean;
226
+ };
227
+ declare const ICONS: Record<string, IconDef>;
228
+ /**
229
+ * アイコン名からSVG文字列を生成する。
230
+ */
231
+ declare const icon: (name: keyof typeof ICONS | string, size?: number) => string;
232
+
233
+ /**
234
+ * 楽器プリセット定義
235
+ * FluidR3_GM Instrument Presets
236
+ *
237
+ * 独自の価値観に基づくソート基準:
238
+ * 1. 汎用性 (Standard): どんな曲にも合う基本セット
239
+ * 2. 時代感 (Vibe): シンセやレトロなど特定の空気感
240
+ * 3. 世界観 (World): 特定の地域やファンタジー
241
+ * 4. 感情 (Atmosphere): アンビエントやシネマティック
242
+ */
243
+ type InstrumentPreset = {
244
+ displayName: string;
245
+ description: string;
246
+ melody: string;
247
+ submelody: string;
248
+ bass: string;
249
+ chord: string;
250
+ };
251
+ declare const INSTRUMENT_PRESETS: Record<string, InstrumentPreset>;
252
+
253
+ /**
254
+ * 操作履歴を管理するための双方向連結リスト
255
+ */
256
+ declare class LinkedList<T> {
257
+ #private;
258
+ constructor();
259
+ /**
260
+ * 履歴を1つ追加
261
+ */
262
+ add(value: T): void;
263
+ /**
264
+ * 履歴を1つ戻す
265
+ */
266
+ undo(): T | null;
267
+ /**
268
+ * 履歴を1つ進める
269
+ */
270
+ redo(): T | null;
271
+ /**
272
+ * Undo可能かチェック(カーソル移動なし)
273
+ */
274
+ canUndo(): boolean;
275
+ /**
276
+ * Redo可能かチェック(カーソル移動なし)
277
+ */
278
+ canRedo(): boolean;
279
+ }
280
+
281
+ declare const PITCH_MAP: string[];
282
+ /**
283
+ * ノートデータ管理とMML生成の責務を持つDOM非依存のコアロジック
284
+ */
285
+ declare class MMLCore {
286
+ private notes;
287
+ private nextNoteId;
288
+ private handlers;
289
+ private volume;
290
+ private tempo;
291
+ private history;
292
+ private isUndoRedo;
293
+ isBatchOperation: boolean;
294
+ private lastHistorySnapshot;
295
+ private lastUndoTime;
296
+ private static readonly UNDO_DEBOUNCE_MS;
297
+ private toolMode;
298
+ constructor(handlers: CoreEventHandlers, volume?: number);
299
+ beginBatch(): void;
300
+ endBatch(): void;
301
+ saveHistory(): void;
302
+ private restoreHistory;
303
+ undo(): boolean;
304
+ redo(): boolean;
305
+ canUndo(): boolean;
306
+ canRedo(): boolean;
307
+ setToolMode(mode: "pen" | "select" | "eraser"): void;
308
+ getToolMode(): "pen" | "select" | "eraser";
309
+ resetHistory(): void;
310
+ addHistoryOnce(): void;
311
+ clearNotesWithoutHistory(): void;
312
+ setLoadMode(mode: boolean): void;
313
+ /**
314
+ * 指定されたグリッド位置にノートを追加する操作
315
+ * @param step ステップ位置
316
+ * @param pitch ピッチ番号
317
+ * @param options ノート長などの設定
318
+ */
319
+ addNote(step: number, pitch: number, options: AddNoteOptions): void;
320
+ deleteNoteById(noteId: number): void;
321
+ private getMaxStep;
322
+ moveNote(noteId: number, startStep: number, pitch: number): void;
323
+ moveNoteEnd(_: number): void;
324
+ resizeNote(noteId: number, durationSteps: number): void;
325
+ resizeNoteEnd(_: number): void;
326
+ getNotes(): Note[];
327
+ getMML(volumeOverride?: number): string;
328
+ setVolume(volume: number): void;
329
+ setTempo(tempo: number): void;
330
+ private generateAndNotify;
331
+ /**
332
+ * 近似値を許容して単一音符を決定する。
333
+ * ただし、残りステップ(limit)は絶対に超えない。
334
+ */
335
+ private stepsToMMLDuration;
336
+ /**
337
+ * ギャップに収まる最大の音符を探す(減算アルゴリズム用)
338
+ */
339
+ private findBestFitDuration;
340
+ /**
341
+ * ピッチからオクターブ最適化のある音名を取得
342
+ */
343
+ private getNoteWithOctave;
344
+ /**
345
+ * MML生成(1/2小節パターンスキャン方式)
346
+ */
347
+ private generateMML;
348
+ /**
349
+ * ノート配列を直接渡してMMLを生成する(一時的に内部状態を差し替えて生成後に復元)
350
+ */
351
+ getMMLFromNotes(notes: Note[], tempo?: number, volume?: number): string;
352
+ /**
353
+ * MMLの音長文字列("4", "4.", "12"など)をステップ数に変換する
354
+ */
355
+ private getStepFromDottedMML;
356
+ }
357
+ /**
358
+ * 和音を含むノート配列を、各トラックが単音(モノフォニック)になるよう
359
+ * 最小トラック数に分解する(区間スケジューリング・グリーディ法)。
360
+ * 最小トラック数 = 任意の瞬間の最大同時発音数。
361
+ */
362
+ declare const decomposeToMonophonic: (notes: Note[]) => Note[][];
363
+ /**
364
+ * ノート配列が「和音伴奏トラック」かどうかを判定する。
365
+ * 和音 = 同一 startStep に3音以上同時発音しているもの。
366
+ * そのような和音構成ノートの割合が threshold 以上であれば true。
367
+ * @param threshold 0〜1。デフォルト 0.6(60%以上が和音ノートならば伴奏トラック)
368
+ */
369
+ declare const isChordHeavyTrack: (notes: Note[], threshold?: number) => boolean;
370
+
371
+ /**
372
+ * 打ち込み補助マクロ群。MMLCore を直接操作する。
373
+ *
374
+ * 旧 demo/index.html の generateRandomPattern / applyHarmonicFilterMacro /
375
+ * applyMonophonicMacro / shiftAllNotes を移植・整理したもの。
376
+ */
377
+
378
+ /**
379
+ * アクティブトラックにスケールに沿ったランダムなノートを8小節分配置する。
380
+ */
381
+ declare const generateRandomPattern: (core: MMLCore, options: {
382
+ stepsPerBar: number;
383
+ startStep: number;
384
+ pitchRangeStart: number;
385
+ }) => void;
386
+ /**
387
+ * 半小節ごとに伴奏トラックの構成音(pitch class)に合致しない音を削除する。
388
+ */
389
+ declare const applyHarmonicFilter: (targetCore: MMLCore, chordCore: MMLCore, options: {
390
+ stepsPerBar: number;
391
+ }) => void;
392
+ /**
393
+ * 伴奏フィルタに加え、同タイミングの重なりを最高音優先で単音化する。
394
+ */
395
+ declare const applyMonophonic: (targetCore: MMLCore, chordCore: MMLCore, options: {
396
+ stepsPerBar: number;
397
+ }) => void;
398
+ /**
399
+ * 全トラックのノートを一括でステップシフトする(負方向で範囲外は削除)。
400
+ */
401
+ declare const shiftNotes: (cores: MMLCore[], shiftSteps: number) => void;
402
+
403
+ /**
404
+ * MIDI入出力ユーティリティ。
405
+ *
406
+ * - 入力: 注入された parseMidi(midi-parser-js 互換)の戻り値を解析し、
407
+ * チャンネルを melody/submelody/bass/chord へ自動分類してノート配置を返す。
408
+ * - 出力: トラック群とドラムパターンから .mid バイナリ(Blob)を生成する。
409
+ *
410
+ * 旧 demo/index.html の analyzeMidiTracks / getBPM / parseMidiWithSelection /
411
+ * exportMIDI を移植・整理したもの。
412
+ */
413
+
414
+ type MidiTrackAnalysis = {
415
+ index: number;
416
+ name: string;
417
+ noteCount: number;
418
+ selected: boolean;
419
+ };
420
+ type MidiNotePlacement = {
421
+ /** melody/submelody/bass/chord */
422
+ trackId: string;
423
+ startStep: number;
424
+ pitch: number;
425
+ durationSteps: number;
426
+ velocity: number;
427
+ };
428
+ type MidiExtraction = {
429
+ placements: MidiNotePlacement[];
430
+ bpm: number;
431
+ };
432
+ /**
433
+ * MIDIの各トラックを走査し、ノート数などの概要を返す(トラック選択UI用)。
434
+ */
435
+ declare const analyzeMidiTracks: (midi: unknown) => MidiTrackAnalysis[];
436
+ /** MIDIメタイベントからBPMを取得(無ければ120)。 */
437
+ declare const getMidiBPM: (midi: unknown) => number;
438
+ /**
439
+ * 選択トラックからノートを抽出し、チャンネル特性に基づいて
440
+ * melody/submelody/bass/chord へ自動分類した配置を返す。
441
+ */
442
+ declare const extractMidiPlacements: (midi: unknown, selectedTrackIndices: number[]) => MidiExtraction;
443
+ /**
444
+ * 上級者モード用:MIDIトラックインデックス N → DAW トラック trackIds[N] へ直接マッピング。
445
+ * 自動分類なし。選択された MIDI トラック K のノートは DAW トラック K に入る。
446
+ */
447
+ declare const extractMidiPlacementsByTrack: (midi: unknown, selectedIndices: number[], trackIds: string[]) => MidiExtraction;
448
+ type ExportMidiOptions = {
449
+ tracks: {
450
+ notes: Note[];
451
+ volume: number;
452
+ }[];
453
+ drumPattern?: DrumPattern;
454
+ drumVolume?: number;
455
+ bpm: number;
456
+ stepsPerBar: number;
457
+ };
458
+ /**
459
+ * トラック群とドラムパターンから .mid バイナリ(Blob)を生成する。
460
+ */
461
+ declare const exportMIDI: (options: ExportMidiOptions) => Blob;
462
+
463
+ /**
464
+ * MML文字列を解析し、トラックごとのノート配置へ復元するローダ。
465
+ *
466
+ * 旧来 demo/index.html の `loadMML` をライブラリへ移植・整理したもの。
467
+ * 実際のノート追加(MMLCore への反映)は呼び出し側で行う。
468
+ */
469
+ type MMLNotePlacement = {
470
+ /** 0:melody 1:submelody 2:bass 3:chord */
471
+ trackIndex: number;
472
+ startStep: number;
473
+ pitch: number;
474
+ durationSteps: number;
475
+ };
476
+ type ParsedMML = {
477
+ placements: MMLNotePlacement[];
478
+ /** メロディトラックの t 指定から検出したBPM(無ければ null) */
479
+ bpm: number | null;
480
+ };
481
+ type ParseMMLOptions = {
482
+ /** 1小節あたりのステップ数。既定192 */
483
+ stepsPerBar?: number;
484
+ };
485
+ /**
486
+ * MML文字列を解析してノート配置とBPMを返す。
487
+ */
488
+ declare const parseMML: (mml: string, options?: ParseMMLOptions) => ParsedMML;
489
+
490
+ type PianoRollInstance = {
491
+ core: MMLCore;
492
+ getNotes: () => Note[];
493
+ getMML: () => string;
494
+ setVolume: (volume: number) => void;
495
+ setNoteLengthSteps: (steps: number) => void;
496
+ redraw: () => void;
497
+ setToolMode: (mode: ToolMode) => void;
498
+ getToolMode: () => ToolMode;
499
+ getSelectionRect: () => {
500
+ x: number;
501
+ y: number;
502
+ width: number;
503
+ height: number;
504
+ } | null;
505
+ getNotesInRect: (rect: {
506
+ x: number;
507
+ y: number;
508
+ width: number;
509
+ height: number;
510
+ }) => Note[];
511
+ clearSelection: () => void;
512
+ copySelection: () => Note[];
513
+ pasteNotes: (notes: Note[], startStep: number) => void;
514
+ };
515
+ declare const createPianoRoll: (options: PianoRollOptions, handlers: CoreEventHandlers) => PianoRollInstance;
516
+
517
+ declare const getRenderConfig: () => RenderConfig;
518
+ declare const getDrawOffset: () => {
519
+ x: number;
520
+ y: number;
521
+ };
522
+ declare const getGridCanvas: () => HTMLCanvasElement;
523
+ declare const getGridContext: () => CanvasRenderingContext2D;
524
+ declare const getHeaderCanvas: () => HTMLCanvasElement;
525
+ /**
526
+ * Canvasを初期化し、指定されたターゲット要素にマウントします。
527
+ * ヘッダー用、鍵盤用、ノート用の3つのCanvasを作成します。
528
+ */
529
+ declare const init: (mountTarget: HTMLElement, width: number | undefined, height: number | undefined, config: RenderConfig) => void;
530
+ /**
531
+ * 鍵盤を描画します。
532
+ * - 白鍵: クリーム色、全幅
533
+ * - 黒鍵: 左 62% を黒塗り、右 38% は白鍵色(奥に伸びて見える)
534
+ * - E→F / C→B(下オクターブ) の白白境界に区切り線
535
+ */
536
+ declare const drawKeyboard: () => void;
537
+ declare const drawHeader: () => void;
538
+ /**
539
+ * グリッドと背景を描画します。(グリッドCanvasと鍵盤/ヘッダー描画の呼び出し)
540
+ * @param noteLengthSteps ノート長ステップ数(この値ごとに縦線を表示)
541
+ */
542
+ declare const drawGrid: (noteLengthSteps?: number) => void;
543
+ /**
544
+ * 指定されたノートの配列を描画します。
545
+ */
546
+ declare const drawNotes: (notes: Note[], color?: number[]) => void;
547
+ /**
548
+ * 選択範囲の四角形を描画します。
549
+ */
550
+ declare const drawSelectionRect: (rect: {
551
+ x: number;
552
+ y: number;
553
+ width: number;
554
+ height: number;
555
+ } | null) => void;
556
+ /**
557
+ * 選択されたノートをハイライト描画します。(濃い色で描画、枠なし)
558
+ */
559
+ declare const drawSelectedNotes: (notes: Note[], selectedIds: Set<number>, baseColor?: number[]) => void;
560
+ /**
561
+ * カーソルの座標取得
562
+ * (グリッドCanvasの相対座標を取得)
563
+ */
564
+ declare const getXY: (e: MouseEvent | PointerEvent) => [number, number, number];
565
+ declare const getGridPosition: (e: MouseEvent | PointerEvent) => {
566
+ step: number;
567
+ pitch: number;
568
+ x: number;
569
+ y: number;
570
+ };
571
+ /**
572
+ * ユーザーのクリックイベントを抽象化し、グリッド座標をコールバックに渡します。
573
+ * イベントリスナーをグリッドCanvasにのみ追加します。
574
+ */
575
+ declare const onClick: (callback: (step: number, pitch: number) => void) => void;
576
+ /**
577
+ * 描画オフセット(描画開始ピクセル位置)を設定します。
578
+ */
579
+ declare const setDrawOffset: (x: number, y: number) => void;
580
+
581
+ /**
582
+ * 再生シーケンサ。タイムライン計算と先読みスケジューリングを担い、
583
+ * 実際の発音は注入されたフック(onPlayNote / onPlayDrum)へ委譲する。
584
+ * 描画(プレイヘッド・オートスクロール)は onTick 経由で呼び出し側へ。
585
+ *
586
+ * 旧 demo/index.html の startPlayback / updatePlayback / stopPlayback を移植・整理。
587
+ */
588
+
589
+ type SequencerTrack = {
590
+ id: string;
591
+ volume: number;
592
+ notes: Note[];
593
+ };
594
+ type SequencerOptions = {
595
+ getTracks: () => SequencerTrack[];
596
+ getBpm: () => number;
597
+ getPlayStartStep: () => number;
598
+ getDrumPattern: () => DrumPattern | null;
599
+ /** ソロ対象トラックID(null=ソロ無効) */
600
+ getSoloTrackId: () => string | null;
601
+ getAudioTime: () => number;
602
+ onPlayNote: (e: PlayNoteEvent) => void;
603
+ onPlayDrum: (e: PlayDrumEvent) => void;
604
+ /** 毎フレーム currentPlayStep を通知(プレイヘッド/オートスクロール用) */
605
+ onTick: (currentPlayStep: number) => void;
606
+ onEnd: () => void;
607
+ stepsPerBar: number;
608
+ };
609
+ type Sequencer = {
610
+ start: (fromStep?: number) => void;
611
+ stop: () => void;
612
+ isActive: () => boolean;
613
+ };
614
+ declare const createSequencer: (options: SequencerOptions) => Sequencer;
615
+
616
+ /**
617
+ * mountDAW が注入する自己完結スタイル。
618
+ * すべてのクラスは `dtm-` プレフィックスでスコープし、ホスト側CSSと衝突しにくくする。
619
+ * ドット絵・レトロゲーム風ピクセルアートデザイン(美咲フォント・角なし・ハードシャドウ)。
620
+ * モバイルファースト(狭幅基準)で設計し、min-width メディアクエリで広幅に拡張する。
621
+ */
622
+ declare const DAW_CSS = "\n@font-face {\n font-family: 'k8x12';\n src: url('https://db.onlinewebfonts.com/t/777630d46640dc5a928ea833c2fcb875.woff2') format('woff2'),\n url('https://db.onlinewebfonts.com/t/777630d46640dc5a928ea833c2fcb875.woff') format('woff'),\n url('https://db.onlinewebfonts.com/t/777630d46640dc5a928ea833c2fcb875.ttf') format('truetype');\n font-weight: normal;\n font-style: normal;\n}\n\n/* ====================================================\n PIXEL MUSIC STUDIO \u2014 \u30C9\u30C3\u30C8\u7D75UI\u30B7\u30B9\u30C6\u30E0\n PICO-8\u30AB\u30E9\u30FC\u30D1\u30EC\u30C3\u30C8\u30FB\u7F8E\u54B2\u30D5\u30A9\u30F3\u30C8\u30FB\u30B2\u30FC\u30E0\u30A6\u30A3\u30F3\u30C9\u30A6\u67A0\n ==================================================== */\n\n.dtm-daw {\n /* PICO-8 16\u8272\u30D1\u30EC\u30C3\u30C8\u3088\u308A */\n --c-black: #000000;\n --c-navy: #1d2b53;\n --c-purple: #7e2553;\n --c-dkgreen: #008751;\n --c-brown: #ab5236;\n --c-dkgray: #5f574f;\n --c-gray: #c2c3c7;\n --c-white: #fff1e8;\n --c-red: #ff004d;\n --c-orange: #ffa300;\n --c-yellow: #ffec27;\n --c-green: #00e436;\n --c-cyan: #29adff;\n --c-lavend: #83769c;\n --c-pink: #ff77a8;\n --c-peach: #ffccaa;\n\n /* \u30BB\u30DE\u30F3\u30C6\u30A3\u30C3\u30AF\u30C8\u30FC\u30AF\u30F3 */\n --dtm-bg: var(--c-black);\n --dtm-surface: var(--c-navy);\n --dtm-deep: #0a0f1f;\n --dtm-border: var(--c-cyan);\n --dtm-border2: var(--c-dkgray);\n --dtm-text: var(--c-white);\n --dtm-muted: var(--c-lavend);\n --dtm-primary: var(--c-cyan);\n --dtm-pfg: var(--c-black);\n --dtm-danger: var(--c-red);\n --dtm-success: var(--c-green);\n --dtm-accent: var(--c-pink);\n --dtm-gold: var(--c-yellow);\n --dtm-warn: var(--c-orange);\n --dtm-tap: 40px;\n --dtm-gap: 6px;\n --dtm-font: 'k8x12',ui-monospace,monospace;\n\n box-sizing: border-box;\n font-family: var(--dtm-font);\n font-size: 14px;\n line-height: 1.6;\n letter-spacing: .06em;\n color: var(--dtm-text);\n background: var(--dtm-bg);\n width: 100%;\n display: flex;\n flex-direction: column;\n gap: var(--dtm-gap);\n padding: 6px;\n image-rendering: pixelated;\n -webkit-font-smoothing: none;\n -moz-osx-font-smoothing: unset;\n font-smooth: never;\n -webkit-tap-highlight-color: transparent;\n}\n.dtm-daw *,\n.dtm-daw *::before,\n.dtm-daw *::after { box-sizing: border-box; }\n\n/* \u2500\u2500\u2500 \u30B2\u30FC\u30E0\u30A6\u30A3\u30F3\u30C9\u30A6\u5171\u901A\u67A0 \u2500\u2500\u2500 */\n/* \u5916\u67A0(\u9ED22px) \u2192 \u8272\u4ED8\u304D2px border \u2192 \u5185\u67A0(\u9ED2inset2px) \u306E3\u91CD\u69CB\u9020 */\n.dtm-win {\n border: 2px solid var(--c-black);\n box-shadow:\n inset 0 0 0 2px var(--c-black),\n 0 0 0 2px var(--dtm-primary),\n 4px 4px 0 var(--c-black);\n background: var(--dtm-surface);\n}\n\n/* \u2500\u2500\u2500 \u5171\u901A\u30DC\u30BF\u30F3 \u2500\u2500\u2500 */\n.dtm-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 4px;\n min-height: var(--dtm-tap);\n min-width: var(--dtm-tap);\n padding: 0 10px;\n border: 2px solid var(--dtm-border2);\n background: var(--dtm-surface);\n color: var(--dtm-text);\n font-family: var(--dtm-font);\n font-size: 13px;\n text-transform: uppercase;\n letter-spacing: .12em;\n cursor: pointer;\n user-select: none;\n white-space: nowrap;\n box-shadow: 3px 3px 0 var(--c-black);\n transition: none;\n}\n.dtm-btn:active { transform: translate(3px,3px); box-shadow: none; }\n.dtm-btn:disabled { opacity: .3; cursor: default; box-shadow: none; }\n.dtm-btn--primary { border-color: var(--dtm-primary); background: var(--dtm-primary); color: var(--dtm-pfg); }\n.dtm-btn--success { border-color: var(--dtm-success); background: var(--dtm-success); color: var(--c-black); }\n.dtm-btn--danger { border-color: var(--dtm-danger); background: var(--dtm-danger); color: var(--c-white); }\n.dtm-btn--accent { border-color: var(--dtm-accent); background: var(--dtm-accent); color: var(--c-black); }\n.dtm-btn--ghost { background: transparent; border-color: var(--dtm-border2); }\n.dtm-btn--icon { padding: 0; }\n\n/* \u2500\u2500\u2500 \u30A2\u30A4\u30B3\u30F3\u30DC\u30BF\u30F3 \u2500\u2500\u2500 */\n.dtm-iconbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--dtm-tap);\n height: var(--dtm-tap);\n flex: 0 0 auto;\n border: 2px solid var(--dtm-border2);\n background: var(--dtm-surface);\n color: var(--dtm-text);\n font-size: 16px;\n cursor: pointer;\n box-shadow: 3px 3px 0 var(--c-black);\n}\n.dtm-iconbtn:active { transform: translate(3px,3px); box-shadow: none; }\n.dtm-iconbtn:disabled { opacity: .3; cursor: default; box-shadow: none; }\n\n/* \u2500\u2500\u2500 \u30C8\u30E9\u30F3\u30B9\u30DD\u30FC\u30C8\u30D0\u30FC\uFF08HUD\u30B9\u30BF\u30A4\u30EB\uFF09 \u2500\u2500\u2500 */\n.dtm-topbar {\n position: sticky;\n top: 0;\n z-index: 20;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--dtm-gap);\n padding: 6px;\n background: var(--dtm-deep);\n border: 2px solid var(--c-black);\n box-shadow:\n inset 0 0 0 2px var(--c-black),\n 0 0 0 2px var(--dtm-success),\n 4px 4px 0 var(--c-black);\n}\n\n/* PLAY\u30DC\u30BF\u30F3 \u2014 \u30B2\u30FC\u30E0\u306E\u300C\u6C7A\u5B9A\u30DC\u30BF\u30F3\u300D\u7684\u5B58\u5728\u611F */\n.dtm-play {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n min-height: 44px;\n padding: 0 20px;\n border: 2px solid var(--c-black);\n background: var(--dtm-success);\n color: var(--c-black);\n font-family: var(--dtm-font);\n font-size: 14px;\n text-transform: uppercase;\n letter-spacing: .2em;\n cursor: pointer;\n box-shadow: 0 0 0 2px var(--dtm-success), 4px 4px 0 var(--c-black);\n}\n.dtm-play:active { transform: translate(4px,4px); box-shadow: none; }\n.dtm-play:disabled { opacity: .35; cursor: default; box-shadow: none; }\n.dtm-play--stop {\n background: var(--dtm-danger);\n box-shadow: 0 0 0 2px var(--dtm-danger), 4px 4px 0 var(--c-black);\n color: var(--c-white);\n}\n.dtm-rec { color: var(--dtm-danger); }\n\n/* BPM \u2014 \u30C7\u30B8\u30BF\u30EB\u30AB\u30A6\u30F3\u30BF\u30FC\u98A8 */\n.dtm-label {\n font-family: var(--dtm-font);\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: .14em;\n color: var(--dtm-muted);\n white-space: nowrap;\n}\n.dtm-checkbox-label {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: var(--dtm-font);\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: .12em;\n color: var(--dtm-muted);\n cursor: pointer;\n user-select: none;\n margin-top: 4px;\n}\n.dtm-checkbox-label:hover { color: var(--dtm-text); }\n.dtm-checkbox-label--sub { margin-left: 20px; font-size: 10px; }\n.dtm-checkbox {\n width: 14px;\n height: 14px;\n accent-color: var(--dtm-success);\n cursor: pointer;\n flex-shrink: 0;\n}\n\n.dtm-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: var(--dtm-font);\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: .1em;\n color: var(--dtm-muted);\n cursor: pointer;\n}\n.dtm-toggle input { width: 16px; height: 16px; accent-color: var(--dtm-accent); }\n\n/* \u2500\u2500\u2500 \u30C4\u30FC\u30EB\u30C9\u30C3\u30AF\uFF08\u88C5\u5099\u30B9\u30ED\u30C3\u30C8\u98A8\uFF09 \u2500\u2500\u2500 */\n.dtm-tooldock {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--dtm-gap);\n padding: 6px;\n background: var(--dtm-deep);\n border: 2px solid var(--c-black);\n box-shadow:\n inset 0 0 0 2px var(--c-black),\n 0 0 0 2px var(--dtm-border2),\n 4px 4px 0 var(--c-black);\n}\n.dtm-sep {\n width: 2px; align-self: stretch;\n background: var(--dtm-border2); margin: 2px;\n}\n.dtm-row .dtm-label[data-dtm] { min-width: 48px; text-align: center; }\n\n/* \u2500\u2500\u2500 \u30BB\u30B0\u30E1\u30F3\u30C8\uFF08\u30A2\u30A4\u30C6\u30E0\u30B9\u30ED\u30C3\u30C8\uFF09 \u2500\u2500\u2500 */\n.dtm-seg {\n display: inline-flex;\n border: 2px solid var(--dtm-border2);\n background: var(--dtm-deep);\n box-shadow: 3px 3px 0 var(--c-black);\n}\n.dtm-segbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--dtm-tap);\n height: var(--dtm-tap);\n border: none;\n border-right: 2px solid var(--dtm-border2);\n background: transparent;\n color: var(--dtm-muted);\n cursor: pointer;\n}\n.dtm-segbtn:last-child { border-right: none; }\n.dtm-segbtn--active {\n background: var(--dtm-gold);\n color: var(--c-black);\n}\n.dtm-segbtn:not(.dtm-segbtn--active):active { background: var(--dtm-border2); }\n\n/* \u2500\u2500\u2500 \u30D5\u30A9\u30FC\u30E0\u8981\u7D20 \u2500\u2500\u2500 */\n.dtm-select, .dtm-input, .dtm-textarea {\n min-height: var(--dtm-tap);\n padding: 4px 8px;\n border: 2px solid var(--dtm-border2);\n background: var(--dtm-deep);\n color: var(--dtm-text);\n font-family: var(--dtm-font);\n font-size: 13px;\n letter-spacing: .06em;\n box-shadow: inset 2px 2px 0 var(--c-black);\n}\n.dtm-select:focus, .dtm-input:focus, .dtm-textarea:focus {\n outline: none;\n border-color: var(--dtm-primary);\n}\n.dtm-input--num { width: 64px; text-align: center; font-size: 16px; }\n.dtm-textarea { width: 100%; min-height: 56px; resize: vertical; line-height: 1.7; }\n.dtm-range { height: var(--dtm-tap); accent-color: var(--dtm-primary); }\n\n/* \u2500\u2500\u2500 \u30C8\u30E9\u30C3\u30AF\u30D4\u30EB\uFF08\u30AD\u30E3\u30E9\u30AF\u30BF\u30FC\u9078\u629E\u30DC\u30BF\u30F3\uFF09 \u2500\u2500\u2500 */\n.dtm-tracks {\n display: flex;\n flex-wrap: wrap;\n gap: var(--dtm-gap);\n}\n.dtm-pill {\n --dtm-pill-color: var(--dtm-primary);\n display: inline-flex;\n align-items: center;\n gap: 8px;\n flex: 1 1 auto;\n justify-content: center;\n min-height: 42px;\n padding: 0 12px;\n border: 2px solid var(--dtm-border2);\n background: var(--dtm-deep);\n color: var(--dtm-muted);\n font-family: var(--dtm-font);\n font-size: 13px;\n text-transform: uppercase;\n letter-spacing: .1em;\n cursor: pointer;\n box-shadow: 3px 3px 0 var(--c-black);\n}\n.dtm-pill .dtm-dot {\n width: 8px; height: 8px;\n background: var(--dtm-pill-color);\n flex: 0 0 auto;\n box-shadow: 1px 1px 0 var(--c-black);\n}\n/* \u30A2\u30AF\u30C6\u30A3\u30D6\u9078\u629E = \u91D1\u8272\u30CF\u30A4\u30E9\u30A4\u30C8 + \u30AB\u30FC\u30BD\u30EB */\n.dtm-pill--active {\n border-color: var(--dtm-gold);\n color: var(--dtm-gold);\n background: var(--dtm-surface);\n box-shadow: 0 0 0 2px var(--dtm-gold), 3px 3px 0 var(--c-black);\n}\n.dtm-pill--active::before { content: \"\u25BA \"; font-size: 10px; }\n.dtm-pill:not(.dtm-pill--active):active { transform: translate(3px,3px); box-shadow: none; }\n\n/* \u2500\u2500\u2500 \u30D4\u30A2\u30CE\u30ED\u30FC\u30EB\uFF08\u30C8\u30E9\u30C3\u30AB\u30FC\u98A8\uFF09 \u2500\u2500\u2500 */\n.dtm-roll-wrap { display: flex; gap: var(--dtm-gap); }\n.dtm-roll {\n position: relative;\n flex: 1 1 auto;\n height: 56vh;\n min-height: 280px;\n background: var(--dtm-deep);\n border: 2px solid var(--c-black);\n box-shadow:\n inset 0 0 0 2px var(--c-black),\n 0 0 0 2px var(--dtm-border2),\n 4px 4px 0 var(--c-black);\n overflow: hidden;\n}\n.dtm-vscroll {\n position: relative;\n width: 20px;\n background: var(--dtm-deep);\n border: 2px solid var(--dtm-border2);\n cursor: pointer;\n flex: 0 0 auto;\n touch-action: none;\n}\n.dtm-vscroll-thumb, .dtm-hscroll-thumb {\n position: absolute;\n background: var(--dtm-primary);\n min-width: 20px;\n min-height: 20px;\n}\n.dtm-vscroll-thumb { left: 0; width: 100%; }\n.dtm-hscroll {\n position: relative;\n width: 100%; height: 20px;\n background: var(--dtm-deep);\n border: 2px solid var(--dtm-border2);\n cursor: pointer;\n touch-action: none;\n}\n.dtm-hscroll-thumb { top: 0; height: 100%; }\n\n/* \u2500\u2500\u2500 \u30D1\u30CD\u30EB\uFF08RPG\u30C0\u30A4\u30A2\u30ED\u30B0\u30A6\u30A3\u30F3\u30C9\u30A6\uFF09 \u2500\u2500\u2500 */\n.dtm-panel {\n background: var(--dtm-surface);\n border: 2px solid var(--c-black);\n box-shadow:\n inset 0 0 0 2px var(--c-black),\n 0 0 0 2px var(--dtm-primary),\n 4px 4px 0 var(--c-black);\n overflow: hidden;\n}\n.dtm-panel > summary {\n list-style: none;\n cursor: pointer;\n padding: 0 12px;\n font-family: var(--dtm-font);\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: .14em;\n display: flex;\n align-items: center;\n min-height: var(--dtm-tap);\n background: var(--dtm-deep);\n border-bottom: 2px solid var(--dtm-border2);\n color: var(--dtm-primary);\n gap: 8px;\n}\n.dtm-panel:not([open]) > summary { border-bottom: none; }\n.dtm-panel > summary::-webkit-details-marker { display: none; }\n/* \u5DE6\u7AEF\u30E9\u30A4\u30F3\uFF08\u30B2\u30FC\u30E0UI\u306E\u30BB\u30AF\u30B7\u30E7\u30F3\u8272\u5206\u3051\uFF09 */\n.dtm-panel > summary::before {\n content: '';\n display: block;\n width: 4px;\n height: 20px;\n background: var(--dtm-accent);\n flex: 0 0 auto;\n}\n.dtm-panel[open] > summary::before { background: var(--dtm-primary); }\n/* \u6298\u308A\u305F\u305F\u307F\u77E2\u5370 */\n.dtm-panel > summary::after {\n content: \"\u25B6\";\n margin-left: auto;\n color: var(--dtm-muted);\n font-size: 10px;\n}\n.dtm-panel[open] > summary::after { content: \"\u25BC\"; }\n.dtm-panel-body { padding: 10px 12px 12px; display: flex; flex-direction: column; gap: 10px; }\n.dtm-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }\n.dtm-track-body { display: flex; flex-direction: column; gap: 10px; }\n\n/* \u2500\u2500\u2500 MML\u51FA\u529B\uFF08CRT\u30BF\u30FC\u30DF\u30CA\u30EB\uFF09 \u2500\u2500\u2500 */\n.dtm-output {\n background: var(--c-black);\n color: var(--dtm-success);\n border: 2px solid var(--dtm-success);\n padding: 10px;\n box-shadow: 0 0 0 2px var(--c-black), 4px 4px 0 var(--c-black);\n}\n.dtm-output::before {\n content: \"C:\\> MML OUTPUT\";\n display: block;\n font-size: 11px;\n color: var(--dtm-muted);\n letter-spacing: .14em;\n margin-bottom: 6px;\n padding-bottom: 6px;\n border-bottom: 1px solid var(--dtm-border2);\n}\n.dtm-output pre {\n margin: 0;\n background: transparent;\n padding: 0;\n overflow-x: auto;\n font-family: var(--dtm-font);\n font-size: 12px;\n line-height: 1.8;\n color: var(--dtm-success);\n}\n.dtm-output-row { display: flex; gap: 8px; align-items: flex-start; margin-top: 6px; }\n.dtm-output-row pre { flex: 1; }\n\n/* \u2500\u2500\u2500 \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 \u2500\u2500\u2500 */\n.dtm-overlay {\n position: fixed; inset: 0; z-index: 1000;\n background: rgba(0,0,0,.92);\n display: flex; align-items: center; justify-content: center;\n flex-direction: column; gap: 14px;\n}\n.dtm-overlay[hidden] { display: none; }\n.dtm-overlay::before {\n content: 'NOW LOADING';\n font-family: var(--dtm-font);\n font-size: 13px;\n color: var(--dtm-primary);\n text-transform: uppercase;\n letter-spacing: .25em;\n animation: dtm-blink 1s steps(1) infinite;\n}\n/* 8\u30D6\u30ED\u30C3\u30AF\u523B\u307F\u3067\u57CB\u307E\u308B\u30D4\u30AF\u30BB\u30EB\u30D0\u30FC */\n.dtm-spinner {\n width: 96px; height: 12px;\n position: relative;\n background: var(--c-navy);\n border: 2px solid var(--dtm-primary);\n box-shadow: 0 0 0 2px var(--c-black), 4px 4px 0 var(--c-black);\n}\n.dtm-spinner::after {\n content: '';\n position: absolute;\n left: 0; top: 0; height: 100%;\n background: var(--dtm-primary);\n animation: dtm-load 1.6s steps(8) infinite;\n}\n@keyframes dtm-load { 0%{width:0} 100%{width:100%} }\n\n@keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }\n.dtm-blink { animation: dtm-blink 1s steps(1) infinite; }\n\n.dtm-hidden { display: none !important; }\n.dtm-grow { flex: 1 1 auto; }\n\n/* \u2500\u2500\u2500 \u5E83\u5E45\u62E1\u5F35 \u2500\u2500\u2500 */\n@media (min-width: 768px) {\n .dtm-daw { gap: 8px; padding: 10px; }\n .dtm-roll { height: 420px; }\n}\n";
623
+ /**
624
+ * スタイルを document.head に一度だけ注入する。
625
+ */
626
+ declare const injectStyles: (doc?: Document) => void;
627
+
628
+ export { type AddNoteOptions, type ApplyChordOptions, type ChordPatternType, type ChordPlacement, type CoreEventHandlers, DAW_CSS, DRUM_FONT, DRUM_KEYS, DRUM_PATTERNS, type DawInstance, type DawOptions, type DrumPattern, type ExportMidiOptions, INSTRUMENT_PRESETS, type InstrumentPreset, LinkedList, MMLCore, type MMLNotePlacement, type MidiExtraction, type MidiNotePlacement, type MidiTrackAnalysis, type Note, PITCH_MAP, type ParseChordFn, type ParseChordsFn, type ParseMMLOptions, type ParseMidiFn, type ParsedMML, type PianoRollInstance, type PianoRollOptions, type PlayDrumEvent, type PlayNoteEvent, type PlaybackState, type PreviewSoundCallback, type RenderConfig, type Sequencer, type SequencerOptions, type SequencerTrack, TRACKS_ADVANCED, TRACKS_SIMPLE, type ToolMode, type TrackConfig, analyzeMidiTracks, applyHarmonicFilter, applyMonophonic, buildChordPlacements, buildNameToKeyMapping, createAudioContext, createPianoRoll, createSequencer, decomposeToMonophonic, drawGrid, drawHeader, drawKeyboard, drawNotes, drawSelectedNotes, drawSelectionRect, exportMIDI, extractMidiPlacements, extractMidiPlacementsByTrack, fetchSoundFontList, generateRandomPattern, getDrawOffset, getGridCanvas, getGridContext, getGridPosition, getHeaderCanvas, getMidiBPM, getRenderConfig, getXY, icon, init, injectStyles, isChordHeavyTrack, mountDAW, onClick, parseMML, setDrawOffset, setupRecorder, shiftNotes };