@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.
package/dist/index.js ADDED
@@ -0,0 +1,4536 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DAW_CSS: () => DAW_CSS,
24
+ DRUM_FONT: () => DRUM_FONT,
25
+ DRUM_KEYS: () => DRUM_KEYS,
26
+ DRUM_PATTERNS: () => DRUM_PATTERNS,
27
+ INSTRUMENT_PRESETS: () => INSTRUMENT_PRESETS,
28
+ LinkedList: () => LinkedList,
29
+ MMLCore: () => MMLCore,
30
+ PITCH_MAP: () => PITCH_MAP,
31
+ TRACKS_ADVANCED: () => TRACKS_ADVANCED,
32
+ TRACKS_SIMPLE: () => TRACKS_SIMPLE,
33
+ analyzeMidiTracks: () => analyzeMidiTracks,
34
+ applyHarmonicFilter: () => applyHarmonicFilter,
35
+ applyMonophonic: () => applyMonophonic,
36
+ buildChordPlacements: () => buildChordPlacements,
37
+ buildNameToKeyMapping: () => buildNameToKeyMapping,
38
+ createAudioContext: () => createAudioContext,
39
+ createPianoRoll: () => createPianoRoll,
40
+ createSequencer: () => createSequencer,
41
+ decomposeToMonophonic: () => decomposeToMonophonic,
42
+ drawGrid: () => drawGrid,
43
+ drawHeader: () => drawHeader,
44
+ drawKeyboard: () => drawKeyboard,
45
+ drawNotes: () => drawNotes,
46
+ drawSelectedNotes: () => drawSelectedNotes,
47
+ drawSelectionRect: () => drawSelectionRect,
48
+ exportMIDI: () => exportMIDI,
49
+ extractMidiPlacements: () => extractMidiPlacements,
50
+ extractMidiPlacementsByTrack: () => extractMidiPlacementsByTrack,
51
+ fetchSoundFontList: () => fetchSoundFontList,
52
+ generateRandomPattern: () => generateRandomPattern,
53
+ getDrawOffset: () => getDrawOffset,
54
+ getGridCanvas: () => getGridCanvas,
55
+ getGridContext: () => getGridContext,
56
+ getGridPosition: () => getGridPosition,
57
+ getHeaderCanvas: () => getHeaderCanvas,
58
+ getMidiBPM: () => getMidiBPM,
59
+ getRenderConfig: () => getRenderConfig,
60
+ getXY: () => getXY,
61
+ icon: () => icon,
62
+ init: () => init,
63
+ injectStyles: () => injectStyles,
64
+ isChordHeavyTrack: () => isChordHeavyTrack,
65
+ mountDAW: () => mountDAW,
66
+ onClick: () => onClick,
67
+ parseMML: () => parseMML,
68
+ setDrawOffset: () => setDrawOffset,
69
+ setupRecorder: () => setupRecorder,
70
+ shiftNotes: () => shiftNotes
71
+ });
72
+ module.exports = __toCommonJS(index_exports);
73
+
74
+ // src/audio-config.ts
75
+ function createAudioContext() {
76
+ const audioCtx = new AudioContext();
77
+ const gainNode = audioCtx.createGain();
78
+ gainNode.connect(audioCtx.destination);
79
+ const drumGainNode = audioCtx.createGain();
80
+ drumGainNode.connect(audioCtx.destination);
81
+ return { audioCtx, gainNode, drumGainNode };
82
+ }
83
+ function setupRecorder(audioCtx, gainNode, drumGainNode) {
84
+ let isRecording = false;
85
+ let recordedData = [[], []];
86
+ const recorderProcessor = audioCtx.createScriptProcessor(4096, 2, 2);
87
+ recorderProcessor.onaudioprocess = (e) => {
88
+ if (!isRecording) return;
89
+ const left = e.inputBuffer.getChannelData(0);
90
+ const right = e.inputBuffer.getChannelData(1);
91
+ recordedData[0].push(left.slice());
92
+ recordedData[1].push(right.slice());
93
+ };
94
+ gainNode.connect(recorderProcessor);
95
+ drumGainNode.connect(recorderProcessor);
96
+ recorderProcessor.connect(audioCtx.destination);
97
+ return {
98
+ startRecording: () => {
99
+ isRecording = true;
100
+ },
101
+ stopRecording: () => {
102
+ isRecording = false;
103
+ },
104
+ getRecordedData: () => recordedData,
105
+ isRecording: () => isRecording,
106
+ clearRecordedData: () => {
107
+ recordedData = [[], []];
108
+ }
109
+ };
110
+ }
111
+ async function fetchSoundFontList(ttl) {
112
+ const res = await fetch(`https://rpgen3.github.io/soundfont/list/${ttl}.txt`);
113
+ const str = await res.text();
114
+ return str.trim().split("\n");
115
+ }
116
+ async function buildNameToKeyMapping() {
117
+ const nameToKey = {};
118
+ try {
119
+ const fontNames = await fetchSoundFontList("fontName_surikov");
120
+ fontNames.forEach((line) => {
121
+ const [key, ...nameParts] = line.split(" ");
122
+ const name = nameParts.join(" ");
123
+ nameToKey[name] = key;
124
+ });
125
+ } catch (e) {
126
+ console.error("Failed to build name-to-key mapping:", e);
127
+ }
128
+ return nameToKey;
129
+ }
130
+
131
+ // src/chords.ts
132
+ var C3 = 48;
133
+ var buildChordPlacements = (options) => {
134
+ const {
135
+ chordStr,
136
+ patternType,
137
+ rootShift,
138
+ bpm,
139
+ stepsPerBar,
140
+ parseChord,
141
+ parseChords
142
+ } = options;
143
+ const placements = [];
144
+ if (!chordStr.trim()) return placements;
145
+ const offset = rootShift;
146
+ const chordLength = stepsPerBar;
147
+ let chordData = [];
148
+ try {
149
+ chordData = parseChords(chordStr, bpm);
150
+ } catch {
151
+ chordData = [];
152
+ }
153
+ if (chordData.length > 0) {
154
+ const secondsPerBar = 60 / bpm * 4;
155
+ const secondsPerStep = secondsPerBar / stepsPerBar;
156
+ const chordGroups = {};
157
+ for (const chord of chordData) {
158
+ const whenStep = Math.floor(chord.when / secondsPerStep);
159
+ const durationSteps = Math.floor(chord.duration / secondsPerStep);
160
+ if (!chordGroups[whenStep]) chordGroups[whenStep] = [];
161
+ chordGroups[whenStep].push({
162
+ key: chord.key,
163
+ chord: chord.chord,
164
+ whenStep,
165
+ durationSteps
166
+ });
167
+ }
168
+ for (const group of Object.values(chordGroups)) {
169
+ for (const chord of group) {
170
+ let notes;
171
+ try {
172
+ notes = [...parseChord(`${chord.key}${chord.chord}`).value];
173
+ } catch {
174
+ continue;
175
+ }
176
+ const noteLength = chord.durationSteps;
177
+ if (patternType === "block") {
178
+ for (const noteOffset of notes) {
179
+ placements.push({
180
+ startStep: chord.whenStep,
181
+ pitch: C3 + noteOffset + offset,
182
+ durationSteps: noteLength,
183
+ velocity: 100
184
+ });
185
+ }
186
+ } else if (patternType === "arpeggio") {
187
+ const arpInterval = Math.floor(noteLength / notes.length);
188
+ notes.forEach((noteOffset, i) => {
189
+ placements.push({
190
+ startStep: chord.whenStep + i * arpInterval,
191
+ pitch: C3 + noteOffset + offset,
192
+ durationSteps: noteLength - i * arpInterval,
193
+ velocity: 100
194
+ });
195
+ });
196
+ } else if (patternType === "arpeggio-fast") {
197
+ const arpInterval = 6;
198
+ notes.forEach((noteOffset, i) => {
199
+ placements.push({
200
+ startStep: chord.whenStep + i * arpInterval,
201
+ pitch: C3 + noteOffset + offset,
202
+ durationSteps: Math.max(12, noteLength - i * arpInterval),
203
+ velocity: 100
204
+ });
205
+ });
206
+ } else if (patternType === "offbeat") {
207
+ const stepsPerQuarter = Math.floor(stepsPerBar / 4);
208
+ const halfBeat = Math.floor(stepsPerQuarter / 2);
209
+ for (let beat = 0; beat < 4; beat++) {
210
+ const syncopatedStep = chord.whenStep + beat * stepsPerQuarter + halfBeat;
211
+ if (syncopatedStep < chord.whenStep + noteLength) {
212
+ for (const noteOffset of notes) {
213
+ placements.push({
214
+ startStep: syncopatedStep,
215
+ pitch: C3 + noteOffset + offset,
216
+ durationSteps: Math.min(halfBeat, 12),
217
+ velocity: 100
218
+ });
219
+ }
220
+ }
221
+ }
222
+ } else if (patternType === "yatsume") {
223
+ const ticksPerQuarter = 480;
224
+ const stepsPerQuarter = Math.floor(stepsPerBar / 4);
225
+ const tickToStep = (tick) => Math.max(1, Math.round(tick * stepsPerQuarter / ticksPerQuarter));
226
+ const yatsumeTickOffsets = [0, 360, 960, 1320];
227
+ const yatsumeLengthSteps = tickToStep(360);
228
+ for (const tickOffset of yatsumeTickOffsets) {
229
+ const noteStart = chord.whenStep + tickToStep(tickOffset);
230
+ if (noteStart < chord.whenStep + noteLength) {
231
+ for (const noteOffset of notes) {
232
+ placements.push({
233
+ startStep: noteStart,
234
+ pitch: C3 + noteOffset + offset,
235
+ durationSteps: yatsumeLengthSteps,
236
+ velocity: 100
237
+ });
238
+ }
239
+ }
240
+ }
241
+ } else if (patternType === "alternating") {
242
+ notes.forEach((noteOffset, i) => {
243
+ const stepOffset = i * Math.floor(stepsPerBar / 4);
244
+ placements.push({
245
+ startStep: chord.whenStep + stepOffset,
246
+ pitch: C3 + noteOffset + offset,
247
+ durationSteps: Math.max(12, Math.floor(stepsPerBar / 4)),
248
+ velocity: 100
249
+ });
250
+ });
251
+ }
252
+ }
253
+ }
254
+ } else {
255
+ const chordNames = chordStr.split(/[\s,]+/).filter((c) => c);
256
+ chordNames.forEach((chordName, barIndex) => {
257
+ let notes;
258
+ try {
259
+ notes = [...parseChord(chordName).value];
260
+ } catch {
261
+ return;
262
+ }
263
+ if (notes.length === 0) return;
264
+ const startStep = barIndex * chordLength;
265
+ notes.forEach((noteOffset, i) => {
266
+ const stepOffset = i * 2;
267
+ placements.push({
268
+ startStep: startStep + stepOffset,
269
+ pitch: C3 + noteOffset + offset,
270
+ durationSteps: chordLength - stepOffset,
271
+ velocity: 100
272
+ });
273
+ });
274
+ });
275
+ }
276
+ return placements;
277
+ };
278
+
279
+ // src/icons.ts
280
+ var ICONS = {
281
+ play: { d: "M8 5v14l11-7z" },
282
+ pause: { d: "M6 5h4v14H6zm8 0h4v14h-4z" },
283
+ stop: { d: "M6 6h12v12H6z" },
284
+ record: { d: "M12 6a6 6 0 100 12 6 6 0 000-12z" },
285
+ undo: { d: "M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6", stroke: true },
286
+ redo: { d: "M21 10h-10a8 8 0 00-8 8v2M21 10l-6 6m6-6l-6-6", stroke: true },
287
+ chevronUp: { d: "M5 15l7-7 7 7", stroke: true },
288
+ chevronDown: { d: "M19 9l-7 7-7-7", stroke: true },
289
+ chevronLeft: { d: "M15 19l-7-7 7-7", stroke: true },
290
+ chevronRight: { d: "M9 5l7 7-7 7", stroke: true },
291
+ first: { d: "M18 18l-6-6 6-6M11 18l-6-6 6-6", stroke: true },
292
+ copy: {
293
+ d: "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z",
294
+ stroke: true
295
+ },
296
+ pen: {
297
+ d: "M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75 1.84-1.83zM3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z"
298
+ },
299
+ eraser: {
300
+ d: "M16.24 3.56l4.95 4.94c.78.79.78 2.05 0 2.84L12 20.53a4.008 4.008 0 01-5.66 0L2.81 17c-.78-.79-.78-2.05 0-2.84l10.6-10.6c.79-.78 2.05-.78 2.83 0zM4.22 15.58l3.54 3.53c.78.79 2.04.79 2.83 0l3.53-3.53-4.95-4.95-4.95 4.95z"
301
+ },
302
+ select: {
303
+ d: "M4 7V5a1 1 0 011-1h2M4 17v2a1 1 0 001 1h2M20 7V5a1 1 0 00-1-1h-2M20 17v2a1 1 0 01-1 1h-2M4 11v2M20 11v2M11 4h2M11 20h2",
304
+ stroke: true
305
+ },
306
+ settings: {
307
+ d: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065zM15 12a3 3 0 11-6 0 3 3 0 016 0z",
308
+ stroke: true
309
+ }
310
+ };
311
+ var icon = (name, size = 20) => {
312
+ const def = ICONS[name];
313
+ if (!def) return "";
314
+ const paint = def.stroke ? 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"' : 'fill="currentColor"';
315
+ return `<svg viewBox="0 0 24 24" width="${size}" height="${size}" ${paint} aria-hidden="true"><path d="${def.d}"/></svg>`;
316
+ };
317
+
318
+ // src/daw-ui.ts
319
+ var q = (root, sel) => root.querySelector(sel);
320
+ var buildUI = (target, options) => {
321
+ const { drumPatternNames, defaultDrumPattern, defaultBpm, showMidi } = options;
322
+ const drumOptions = [`<option value="none">\u306A\u3057</option>`].concat(
323
+ drumPatternNames.map(
324
+ (name) => `<option value="${name}" ${name === defaultDrumPattern ? "selected" : ""}>${name}</option>`
325
+ )
326
+ ).join("");
327
+ target.innerHTML = `
328
+ <div class="dtm-daw" data-dtm="root">
329
+ <div class="dtm-topbar" data-dtm="transport">
330
+ <button class="dtm-play" data-dtm="play" disabled>${icon("play")}<span>\u8A66\u8074</span></button>
331
+ <button class="dtm-iconbtn dtm-rec" data-dtm="rec" title="\u9332\u97F3">${icon("record")}</button>
332
+ <label class="dtm-toggle"><input type="checkbox" data-dtm="solo"><span>\u30BD\u30ED</span></label>
333
+ <span class="dtm-grow"></span>
334
+ <span class="dtm-label">BPM</span>
335
+ <input type="number" class="dtm-input dtm-input--num" data-dtm="bpm" value="${defaultBpm}" min="20" max="300">
336
+ </div>
337
+
338
+ <div class="dtm-tooldock">
339
+ <div class="dtm-seg">
340
+ <button class="dtm-segbtn dtm-segbtn--active" data-dtm="tool-pen" title="\u30DA\u30F3">${icon("pen")}</button>
341
+ <button class="dtm-segbtn" data-dtm="tool-select" title="\u9078\u629E">${icon("select")}</button>
342
+ <button class="dtm-segbtn" data-dtm="tool-eraser" title="\u6D88\u3057\u30B4\u30E0">${icon("eraser")}</button>
343
+ </div>
344
+ <button class="dtm-iconbtn" data-dtm="undo" title="\u5143\u306B\u623B\u3059" disabled>${icon("undo")}</button>
345
+ <button class="dtm-iconbtn" data-dtm="redo" title="\u3084\u308A\u76F4\u3057" disabled>${icon("redo")}</button>
346
+ <select class="dtm-select dtm-grow" data-dtm="note-length" title="\u97F3\u7B26\u306E\u9577\u3055">
347
+ <option value="48">4\u5206</option>
348
+ <option value="32">3\u90234</option>
349
+ <option value="24">8\u5206</option>
350
+ <option value="16">3\u90238</option>
351
+ <option value="12" selected>16\u5206</option>
352
+ <option value="8">3\u902316</option>
353
+ <option value="6">32\u5206</option>
354
+ <option value="4">3\u902332</option>
355
+ </select>
356
+ </div>
357
+
358
+ <div class="dtm-tracks" data-dtm="track-tabs"></div>
359
+
360
+ <div class="dtm-roll-wrap">
361
+ <div class="dtm-roll" data-dtm="roll"><div data-dtm="wrapper" style="position:absolute;inset:0;"></div></div>
362
+ <div class="dtm-vscroll" data-dtm="vscroll"><div class="dtm-vscroll-thumb" data-dtm="vscroll-thumb"></div></div>
363
+ </div>
364
+ <div class="dtm-hscroll" data-dtm="hscroll"><div class="dtm-hscroll-thumb" data-dtm="hscroll-thumb"></div></div>
365
+
366
+ <details class="dtm-panel" open>
367
+ <summary>\u30C8\u30E9\u30C3\u30AF\u8A2D\u5B9A</summary>
368
+ <div class="dtm-panel-body">
369
+ <div class="dtm-row">
370
+ <span class="dtm-label">\u5168\u4F53\u97F3\u91CF</span>
371
+ <input type="range" class="dtm-range dtm-grow" data-dtm="master-volume" value="50" min="0" max="100">
372
+ <span class="dtm-label" data-dtm="master-volume-label">50%</span>
373
+ </div>
374
+ <div class="dtm-track-body" data-dtm="track-body"></div>
375
+ </div>
376
+ </details>
377
+
378
+ <details class="dtm-panel">
379
+ <summary>\u8868\u793A</summary>
380
+ <div class="dtm-panel-body">
381
+ <div class="dtm-row">
382
+ <span class="dtm-label">\u6A2A\u30BA\u30FC\u30E0</span>
383
+ <button class="dtm-iconbtn" data-dtm="zoomx-out" title="\u7E2E\u5C0F">\u2212</button>
384
+ <span class="dtm-label" data-dtm="zoomx-label">100%</span>
385
+ <button class="dtm-iconbtn" data-dtm="zoomx-in" title="\u62E1\u5927">\uFF0B</button>
386
+ </div>
387
+ <div class="dtm-row">
388
+ <span class="dtm-label">\u7E26\u30BA\u30FC\u30E0</span>
389
+ <button class="dtm-iconbtn" data-dtm="zoomy-out" title="\u7E2E\u5C0F">\u2212</button>
390
+ <span class="dtm-label" data-dtm="zoomy-label">100%</span>
391
+ <button class="dtm-iconbtn" data-dtm="zoomy-in" title="\u62E1\u5927">\uFF0B</button>
392
+ </div>
393
+ </div>
394
+ </details>
395
+
396
+ <details class="dtm-panel">
397
+ <summary>\u30C9\u30E9\u30E0\u8A2D\u5B9A</summary>
398
+ <div class="dtm-panel-body">
399
+ <div class="dtm-row">
400
+ <span class="dtm-label">\u30EA\u30BA\u30E0</span>
401
+ <select class="dtm-select" data-dtm="drum-select">${drumOptions}</select>
402
+ </div>
403
+ <div class="dtm-row">
404
+ <span class="dtm-label">\u97F3\u91CF</span>
405
+ <input type="range" class="dtm-range dtm-grow" data-dtm="drum-volume" value="80" min="0" max="100">
406
+ <span class="dtm-label" data-dtm="drum-volume-label">80%</span>
407
+ </div>
408
+ </div>
409
+ </details>
410
+
411
+ <details class="dtm-panel ${showMidi ? "" : "dtm-hidden"}" data-dtm="midi-panel">
412
+ <summary>MIDI / MML \u5165\u529B</summary>
413
+ <div class="dtm-panel-body">
414
+ <div class="dtm-row">
415
+ <span class="dtm-label">MIDI</span>
416
+ <input type="file" class="dtm-input dtm-grow" accept=".mid,.midi" data-dtm="midi-input">
417
+ <button class="dtm-btn dtm-btn--success" data-dtm="midi-load">\u8AAD\u8FBC</button>
418
+ </div>
419
+ <div class="dtm-row dtm-hidden" data-dtm="midi-track-selection"></div>
420
+ <div class="dtm-row">
421
+ <span class="dtm-label">MML</span>
422
+ <textarea class="dtm-textarea" data-dtm="mml-input" placeholder="MML\u3092\u5165\u529B"></textarea>
423
+ </div>
424
+ <div class="dtm-row">
425
+ <button class="dtm-btn dtm-btn--primary" data-dtm="mml-load">MML\u8AAD\u8FBC</button>
426
+ <span class="dtm-label">\u5168\u4F53\u30B7\u30D5\u30C8</span>
427
+ <select class="dtm-select" data-dtm="shift-select">
428
+ <option value="-96">-2\u5206</option>
429
+ <option value="-48">-4\u5206</option>
430
+ <option value="-24">-8\u5206</option>
431
+ <option value="-12">-16\u5206</option>
432
+ <option value="12">+16\u5206</option>
433
+ <option value="24">+8\u5206</option>
434
+ <option value="48">+4\u5206</option>
435
+ <option value="96">+2\u5206</option>
436
+ </select>
437
+ <button class="dtm-btn dtm-btn--primary" data-dtm="shift-apply">\u9069\u7528</button>
438
+ </div>
439
+ </div>
440
+ </details>
441
+
442
+ <details class="dtm-panel">
443
+ <summary>\u30DE\u30AF\u30ED</summary>
444
+ <div class="dtm-panel-body">
445
+ <div class="dtm-row">
446
+ <button class="dtm-btn dtm-btn--danger" data-dtm="macro-clear">\u5168\u6D88\u53BB</button>
447
+ <button class="dtm-btn dtm-btn--accent" data-dtm="macro-random">\u30E9\u30F3\u30C0\u30E0\u914D\u7F6E</button>
448
+ <button class="dtm-btn dtm-btn--primary" data-dtm="macro-harmonic">\u4F34\u594F\u30D5\u30A3\u30EB\u30BF</button>
449
+ <button class="dtm-btn dtm-btn--primary" data-dtm="macro-mono">\u5358\u97F3\u5316</button>
450
+ </div>
451
+ </div>
452
+ </details>
453
+
454
+ <details class="dtm-panel">
455
+ <summary>MIDI / MML \u51FA\u529B</summary>
456
+ <div class="dtm-panel-body">
457
+ <div class="dtm-row">
458
+ <button class="dtm-btn dtm-btn--accent" data-dtm="export-midi">MIDI\u51FA\u529B</button>
459
+ <button class="dtm-btn dtm-btn--success" data-dtm="generate-mml">MML\u751F\u6210</button>
460
+ </div>
461
+ <label class="dtm-checkbox-label">
462
+ <input type="checkbox" class="dtm-checkbox" data-dtm="decompose-chord">
463
+ <span>\u548C\u97F3\u5206\u89E3\u30E2\u30FC\u30C9\uFF08\u5358\u97F3\u30C8\u30E9\u30C3\u30AF\u306B\u6700\u9069\u5206\u5272\uFF09</span>
464
+ </label>
465
+ <label class="dtm-checkbox-label dtm-checkbox-label--sub">
466
+ <input type="checkbox" class="dtm-checkbox" data-dtm="ignore-chord-heavy">
467
+ <span>\u548C\u97F3\u4F34\u594F\u30C8\u30E9\u30C3\u30AF\u3092\u7121\u8996\uFF08\u5206\u89E3\u5BFE\u8C61\u304B\u3089\u9664\u5916\uFF09</span>
468
+ </label>
469
+ <div class="dtm-row" style="margin-top:6px;align-items:center;gap:8px;">
470
+ <span class="dtm-label">\u751F\u6210\u4E0A\u9650</span>
471
+ <select class="dtm-select" data-dtm="bar-limit">
472
+ <option value="0">\u5236\u9650\u306A\u3057</option>
473
+ <option value="8">8\u5C0F\u7BC0</option>
474
+ <option value="16">16\u5C0F\u7BC0</option>
475
+ <option value="24">24\u5C0F\u7BC0</option>
476
+ <option value="32">32\u5C0F\u7BC0</option>
477
+ <option value="64">64\u5C0F\u7BC0</option>
478
+ <option value="128">128\u5C0F\u7BC0</option>
479
+ </select>
480
+ </div>
481
+ <div class="dtm-output dtm-hidden" data-dtm="output-container">
482
+ <p class="dtm-label" data-dtm="output-status"></p>
483
+ <div class="dtm-output-row">
484
+ <pre><code data-dtm="output-full"></code></pre>
485
+ <button class="dtm-btn dtm-btn--primary dtm-btn--icon" data-dtm="copy-full" title="\u30B3\u30D4\u30FC">${icon("copy")}</button>
486
+ </div>
487
+ <div class="dtm-output-row">
488
+ <pre><code data-dtm="output-mini"></code></pre>
489
+ <button class="dtm-btn dtm-btn--primary dtm-btn--icon" data-dtm="copy-mini" title="\u30B3\u30D4\u30FC">${icon("copy")}</button>
490
+ </div>
491
+ </div>
492
+ </div>
493
+ </details>
494
+
495
+ <div class="dtm-overlay" data-dtm="overlay" hidden><div class="dtm-spinner"></div></div>
496
+ </div>`;
497
+ const root = q(target, '[data-dtm="root"]');
498
+ const sel = (name) => q(root, `[data-dtm="${name}"]`);
499
+ return {
500
+ root,
501
+ playBtn: sel("play"),
502
+ recBtn: sel("rec"),
503
+ soloCheckbox: sel("solo"),
504
+ toolPen: sel("tool-pen"),
505
+ toolSelect: sel("tool-select"),
506
+ toolEraser: sel("tool-eraser"),
507
+ undoBtn: sel("undo"),
508
+ redoBtn: sel("redo"),
509
+ noteLengthSelect: sel("note-length"),
510
+ bpmInput: sel("bpm"),
511
+ zoomXLabel: sel("zoomx-label"),
512
+ zoomYLabel: sel("zoomy-label"),
513
+ zoomXIn: sel("zoomx-in"),
514
+ zoomXOut: sel("zoomx-out"),
515
+ zoomYIn: sel("zoomy-in"),
516
+ zoomYOut: sel("zoomy-out"),
517
+ rollContainer: sel("roll"),
518
+ wrapper: sel("wrapper"),
519
+ vScroll: sel("vscroll"),
520
+ vScrollThumb: sel("vscroll-thumb"),
521
+ hScroll: sel("hscroll"),
522
+ hScrollThumb: sel("hscroll-thumb"),
523
+ masterVolume: sel("master-volume"),
524
+ masterVolumeLabel: sel("master-volume-label"),
525
+ trackTabs: sel("track-tabs"),
526
+ trackBody: sel("track-body"),
527
+ drumSelect: sel("drum-select"),
528
+ drumVolume: sel("drum-volume"),
529
+ drumVolumeLabel: sel("drum-volume-label"),
530
+ midiInput: sel("midi-input"),
531
+ midiLoadBtn: sel("midi-load"),
532
+ midiTrackSelection: sel("midi-track-selection"),
533
+ midiPanel: sel("midi-panel"),
534
+ mmlInput: sel("mml-input"),
535
+ mmlLoadBtn: sel("mml-load"),
536
+ shiftSelect: sel("shift-select"),
537
+ shiftApplyBtn: sel("shift-apply"),
538
+ macroClear: sel("macro-clear"),
539
+ macroRandom: sel("macro-random"),
540
+ macroHarmonic: sel("macro-harmonic"),
541
+ macroMono: sel("macro-mono"),
542
+ exportMidiBtn: sel("export-midi"),
543
+ generateMmlBtn: sel("generate-mml"),
544
+ decomposeChordToggle: sel("decompose-chord"),
545
+ ignoreChordHeavyToggle: sel("ignore-chord-heavy"),
546
+ barLimitSelect: sel("bar-limit"),
547
+ outputContainer: sel("output-container"),
548
+ outputStatus: sel("output-status"),
549
+ outputFull: sel("output-full"),
550
+ outputMini: sel("output-mini"),
551
+ copyFullBtn: sel("copy-full"),
552
+ copyMiniBtn: sel("copy-mini"),
553
+ overlay: sel("overlay")
554
+ };
555
+ };
556
+
557
+ // src/drum-config.ts
558
+ var DRUM_FONT = "FluidR3_GM_sf2_file";
559
+ var DRUM_KEYS = {
560
+ kick: 36,
561
+ snare: 38,
562
+ clap: 39,
563
+ rimshot: 37,
564
+ hihatClosed: 42,
565
+ hihatPedal: 44,
566
+ hihatOpen: 46,
567
+ tomLow: 45,
568
+ tomMid: 47,
569
+ tomHigh: 50,
570
+ crash: 49,
571
+ ride: 51,
572
+ splash: 55,
573
+ tambourine: 54
574
+ };
575
+ var DRUM_PATTERNS = {
576
+ // 4つ打ち:より重厚に。1拍目の頭にだけ軽くオープンハイハットを混ぜるのもアリ
577
+ "4beat": [
578
+ { step: 0, pitch: DRUM_KEYS.kick, velocity: 1 },
579
+ { step: 48, pitch: DRUM_KEYS.kick, velocity: 0.9 },
580
+ { step: 96, pitch: DRUM_KEYS.kick, velocity: 1 },
581
+ { step: 144, pitch: DRUM_KEYS.kick, velocity: 0.9 }
582
+ ],
583
+ // 8ビート:クローズドハイハットに強弱をつけ、スネアにクラップを薄く重ねる
584
+ "8beat": [
585
+ { step: 0, pitch: DRUM_KEYS.kick, velocity: 1 },
586
+ { step: 0, pitch: DRUM_KEYS.hihatClosed, velocity: 0.8 },
587
+ { step: 24, pitch: DRUM_KEYS.hihatClosed, velocity: 0.5 },
588
+ { step: 48, pitch: DRUM_KEYS.snare, velocity: 1 },
589
+ { step: 48, pitch: DRUM_KEYS.clap, velocity: 0.6 },
590
+ { step: 48, pitch: DRUM_KEYS.hihatClosed, velocity: 0.8 },
591
+ { step: 72, pitch: DRUM_KEYS.hihatClosed, velocity: 0.5 },
592
+ { step: 96, pitch: DRUM_KEYS.kick, velocity: 0.9 },
593
+ { step: 96, pitch: DRUM_KEYS.hihatClosed, velocity: 0.8 },
594
+ { step: 120, pitch: DRUM_KEYS.hihatClosed, velocity: 0.5 },
595
+ { step: 144, pitch: DRUM_KEYS.snare, velocity: 1 },
596
+ { step: 144, pitch: DRUM_KEYS.hihatClosed, velocity: 0.8 },
597
+ { step: 168, pitch: DRUM_KEYS.hihatClosed, velocity: 0.5 }
598
+ ],
599
+ // 16ビート:キックのダブル(96, 108)を活かしつつ、ハイハットの強弱を細かく設定
600
+ "16beat": [
601
+ { step: 0, pitch: DRUM_KEYS.kick, velocity: 1 },
602
+ { step: 0, pitch: DRUM_KEYS.hihatClosed, velocity: 0.8 },
603
+ { step: 12, pitch: DRUM_KEYS.hihatClosed, velocity: 0.4 },
604
+ { step: 24, pitch: DRUM_KEYS.hihatClosed, velocity: 0.6 },
605
+ { step: 36, pitch: DRUM_KEYS.hihatClosed, velocity: 0.4 },
606
+ { step: 48, pitch: DRUM_KEYS.snare, velocity: 1 },
607
+ { step: 48, pitch: DRUM_KEYS.hihatClosed, velocity: 0.8 },
608
+ { step: 60, pitch: DRUM_KEYS.hihatClosed, velocity: 0.4 },
609
+ { step: 72, pitch: DRUM_KEYS.hihatClosed, velocity: 0.6 },
610
+ { step: 84, pitch: DRUM_KEYS.hihatClosed, velocity: 0.4 },
611
+ { step: 96, pitch: DRUM_KEYS.kick, velocity: 0.9 },
612
+ { step: 96, pitch: DRUM_KEYS.hihatClosed, velocity: 0.8 },
613
+ { step: 108, pitch: DRUM_KEYS.kick, velocity: 0.7 },
614
+ { step: 108, pitch: DRUM_KEYS.hihatClosed, velocity: 0.4 },
615
+ { step: 120, pitch: DRUM_KEYS.hihatClosed, velocity: 0.6 },
616
+ { step: 132, pitch: DRUM_KEYS.hihatClosed, velocity: 0.4 },
617
+ { step: 144, pitch: DRUM_KEYS.snare, velocity: 1 },
618
+ { step: 144, pitch: DRUM_KEYS.hihatClosed, velocity: 0.8 },
619
+ { step: 156, pitch: DRUM_KEYS.hihatClosed, velocity: 0.4 },
620
+ { step: 168, pitch: DRUM_KEYS.hihatClosed, velocity: 0.6 },
621
+ { step: 180, pitch: DRUM_KEYS.hihatClosed, velocity: 0.4 }
622
+ ],
623
+ // シャッフル:跳ねるタイミングのベロシティを落として、グルーヴ感を強調
624
+ shuffle: [
625
+ { step: 0, pitch: DRUM_KEYS.kick, velocity: 1 },
626
+ { step: 0, pitch: DRUM_KEYS.hihatClosed, velocity: 0.8 },
627
+ { step: 32, pitch: DRUM_KEYS.hihatClosed, velocity: 0.5 },
628
+ { step: 48, pitch: DRUM_KEYS.snare, velocity: 1 },
629
+ { step: 48, pitch: DRUM_KEYS.hihatClosed, velocity: 0.8 },
630
+ { step: 80, pitch: DRUM_KEYS.hihatClosed, velocity: 0.5 },
631
+ { step: 96, pitch: DRUM_KEYS.kick, velocity: 0.9 },
632
+ { step: 96, pitch: DRUM_KEYS.hihatClosed, velocity: 0.8 },
633
+ { step: 128, pitch: DRUM_KEYS.hihatClosed, velocity: 0.5 },
634
+ { step: 144, pitch: DRUM_KEYS.snare, velocity: 1 },
635
+ { step: 144, pitch: DRUM_KEYS.hihatClosed, velocity: 0.8 },
636
+ { step: 176, pitch: DRUM_KEYS.hihatClosed, velocity: 0.5 }
637
+ ],
638
+ // ダンス/EDM:スネアをClapに変更。キックとハイハットの対比を最大化
639
+ dance: [
640
+ { step: 0, pitch: DRUM_KEYS.kick, velocity: 1 },
641
+ { step: 24, pitch: DRUM_KEYS.hihatOpen, velocity: 0.7 },
642
+ { step: 48, pitch: DRUM_KEYS.kick, velocity: 1 },
643
+ { step: 48, pitch: DRUM_KEYS.clap, velocity: 1 },
644
+ { step: 72, pitch: DRUM_KEYS.hihatOpen, velocity: 0.7 },
645
+ { step: 96, pitch: DRUM_KEYS.kick, velocity: 1 },
646
+ { step: 120, pitch: DRUM_KEYS.hihatOpen, velocity: 0.7 },
647
+ { step: 144, pitch: DRUM_KEYS.kick, velocity: 1 },
648
+ { step: 144, pitch: DRUM_KEYS.clap, velocity: 1 },
649
+ { step: 168, pitch: DRUM_KEYS.hihatOpen, velocity: 0.7 }
650
+ ],
651
+ // ボサノバ/チル系:リムショット(37)とハイハットの組み合わせ
652
+ bossa: [
653
+ { step: 0, pitch: DRUM_KEYS.kick, velocity: 0.9 },
654
+ { step: 0, pitch: DRUM_KEYS.hihatClosed, velocity: 0.6 },
655
+ { step: 24, pitch: DRUM_KEYS.hihatClosed, velocity: 0.4 },
656
+ { step: 48, pitch: DRUM_KEYS.rimshot, velocity: 0.8 },
657
+ { step: 48, pitch: DRUM_KEYS.hihatClosed, velocity: 0.6 },
658
+ { step: 72, pitch: DRUM_KEYS.kick, velocity: 0.7 },
659
+ { step: 72, pitch: DRUM_KEYS.hihatClosed, velocity: 0.4 },
660
+ { step: 96, pitch: DRUM_KEYS.kick, velocity: 0.9 },
661
+ { step: 96, pitch: DRUM_KEYS.hihatClosed, velocity: 0.6 },
662
+ { step: 120, pitch: DRUM_KEYS.hihatClosed, velocity: 0.4 },
663
+ { step: 144, pitch: DRUM_KEYS.rimshot, velocity: 0.8 },
664
+ { step: 144, pitch: DRUM_KEYS.hihatClosed, velocity: 0.6 },
665
+ { step: 168, pitch: DRUM_KEYS.hihatClosed, velocity: 0.4 }
666
+ ],
667
+ // ファンク/ディスコ:タンバリン(54)でスピード感を出す
668
+ disco: [
669
+ { step: 0, pitch: DRUM_KEYS.kick, velocity: 1 },
670
+ { step: 0, pitch: DRUM_KEYS.hihatClosed, velocity: 0.7 },
671
+ { step: 24, pitch: DRUM_KEYS.tambourine, velocity: 0.8 },
672
+ { step: 48, pitch: DRUM_KEYS.snare, velocity: 1 },
673
+ { step: 48, pitch: DRUM_KEYS.hihatClosed, velocity: 0.7 },
674
+ { step: 72, pitch: DRUM_KEYS.tambourine, velocity: 0.8 },
675
+ { step: 96, pitch: DRUM_KEYS.kick, velocity: 1 },
676
+ { step: 96, pitch: DRUM_KEYS.hihatClosed, velocity: 0.7 },
677
+ { step: 120, pitch: DRUM_KEYS.tambourine, velocity: 0.8 },
678
+ { step: 144, pitch: DRUM_KEYS.snare, velocity: 1 },
679
+ { step: 144, pitch: DRUM_KEYS.hihatClosed, velocity: 0.7 },
680
+ { step: 168, pitch: DRUM_KEYS.tambourine, velocity: 0.8 }
681
+ ]
682
+ };
683
+
684
+ // src/macros.ts
685
+ var SCALES = [
686
+ [0, 2, 4, 5, 7, 9, 11],
687
+ // Major
688
+ [0, 2, 3, 5, 7, 8, 10],
689
+ // Minor
690
+ [0, 2, 4, 7, 9]
691
+ // Pentatonic Major
692
+ ];
693
+ var generateRandomPattern = (core, options) => {
694
+ const { stepsPerBar, startStep, pitchRangeStart } = options;
695
+ const numBars = 8;
696
+ const noteLength = 24;
697
+ const basePitch = pitchRangeStart + 60;
698
+ const scale = SCALES[Math.floor(Math.random() * SCALES.length)];
699
+ const rootOffset = Math.floor(Math.random() * 12);
700
+ const availablePitches = [];
701
+ for (let i = 0; i < 12; i++) {
702
+ const noteInOctave = (i - rootOffset + 12) % 12;
703
+ if (scale.includes(noteInOctave)) availablePitches.push(basePitch + i);
704
+ }
705
+ core.beginBatch();
706
+ for (let bar = 0; bar < numBars; bar++) {
707
+ const barStart = startStep + bar * stepsPerBar;
708
+ const numNotes = Math.floor(Math.random() * 4) + 2;
709
+ const occupied = /* @__PURE__ */ new Set();
710
+ for (let i = 0; i < numNotes; i++) {
711
+ const stepInRange = Math.floor(Math.random() * (stepsPerBar / noteLength)) * noteLength;
712
+ const step = barStart + stepInRange;
713
+ if (occupied.has(step)) continue;
714
+ occupied.add(step);
715
+ const pitch = availablePitches[Math.floor(Math.random() * availablePitches.length)];
716
+ core.addNote(step, pitch, { noteLengthSteps: noteLength });
717
+ }
718
+ }
719
+ core.endBatch();
720
+ core.saveHistory();
721
+ };
722
+ var applyHarmonicFilter = (targetCore, chordCore, options) => {
723
+ const halfStepsPerBar = options.stepsPerBar / 2;
724
+ const allNotes = targetCore.getNotes().concat(chordCore.getNotes());
725
+ if (allNotes.length === 0) return;
726
+ const maxStep = Math.max(
727
+ ...allNotes.map((n) => n.startStep + n.durationSteps)
728
+ );
729
+ const numHalfBars = Math.ceil(maxStep / halfStepsPerBar);
730
+ let currentClasses = /* @__PURE__ */ new Set();
731
+ targetCore.beginBatch();
732
+ for (let halfBar = 0; halfBar < numHalfBars; halfBar++) {
733
+ const start = halfBar * halfStepsPerBar;
734
+ const end = start + halfStepsPerBar;
735
+ const isNewBar = halfBar % 2 === 0;
736
+ const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
737
+ if (chordHere.length > 0) {
738
+ currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
739
+ } else if (isNewBar) {
740
+ currentClasses = /* @__PURE__ */ new Set();
741
+ }
742
+ if (currentClasses.size === 0) continue;
743
+ const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
744
+ for (const n of activeHere) {
745
+ if (!currentClasses.has(n.pitch % 12)) targetCore.deleteNoteById(n.id);
746
+ }
747
+ }
748
+ targetCore.endBatch();
749
+ targetCore.saveHistory();
750
+ };
751
+ var applyMonophonic = (targetCore, chordCore, options) => {
752
+ const halfStepsPerBar = options.stepsPerBar / 2;
753
+ const allNotes = targetCore.getNotes().concat(chordCore.getNotes());
754
+ if (allNotes.length === 0) return;
755
+ const maxStep = Math.max(
756
+ ...allNotes.map((n) => n.startStep + n.durationSteps)
757
+ );
758
+ const numHalfBars = Math.ceil(maxStep / halfStepsPerBar);
759
+ let currentClasses = /* @__PURE__ */ new Set();
760
+ targetCore.beginBatch();
761
+ for (let halfBar = 0; halfBar < numHalfBars; halfBar++) {
762
+ const start = halfBar * halfStepsPerBar;
763
+ const end = start + halfStepsPerBar;
764
+ const isNewBar = halfBar % 2 === 0;
765
+ const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
766
+ if (chordHere.length > 0) {
767
+ currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
768
+ } else if (isNewBar) {
769
+ currentClasses = /* @__PURE__ */ new Set();
770
+ }
771
+ if (currentClasses.size === 0) continue;
772
+ const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
773
+ const filtered = activeHere.filter((n) => currentClasses.has(n.pitch % 12));
774
+ const filteredIds = new Set(filtered.map((n) => n.id));
775
+ for (const n of activeHere) {
776
+ if (!filteredIds.has(n.id)) targetCore.deleteNoteById(n.id);
777
+ }
778
+ const timeMap = /* @__PURE__ */ new Map();
779
+ for (const n of filtered) {
780
+ if (!timeMap.has(n.startStep)) timeMap.set(n.startStep, []);
781
+ timeMap.get(n.startStep)?.push(n);
782
+ }
783
+ for (const notesAtTime of timeMap.values()) {
784
+ if (notesAtTime.length > 1) {
785
+ notesAtTime.sort((a, b) => b.pitch - a.pitch);
786
+ const [, ...others] = notesAtTime;
787
+ for (const on of others) targetCore.deleteNoteById(on.id);
788
+ }
789
+ }
790
+ }
791
+ targetCore.endBatch();
792
+ targetCore.saveHistory();
793
+ };
794
+ var shiftNotes = (cores, shiftSteps) => {
795
+ if (shiftSteps === 0) return;
796
+ for (const core of cores) {
797
+ const notes = [...core.getNotes()];
798
+ for (const note of notes) {
799
+ const newStart = note.startStep + shiftSteps;
800
+ if (newStart < 0) core.deleteNoteById(note.id);
801
+ else core.moveNote(note.id, newStart, note.pitch);
802
+ }
803
+ }
804
+ };
805
+
806
+ // src/midi-io.ts
807
+ var STEPS_PER_BEAT = 48;
808
+ var analyzeMidiTracks = (midi) => {
809
+ const { track } = midi;
810
+ const result = [];
811
+ for (let i = 0; i < track.length; i++) {
812
+ const notes = [];
813
+ let currentTime = 0;
814
+ for (const event of track[i].event) {
815
+ currentTime += event.deltaTime;
816
+ const data = event.data;
817
+ if (event.type === 9 && data && data[1]) {
818
+ notes.push({ pitch: data[0] });
819
+ } else if (event.type === 8) {
820
+ for (let k = notes.length - 1; k >= 0; k--) {
821
+ if (notes[k].pitch === data[0] && notes[k].end === void 0) {
822
+ notes[k].end = currentTime;
823
+ break;
824
+ }
825
+ }
826
+ }
827
+ }
828
+ const validNotes = notes.filter((n) => n.end !== void 0);
829
+ result.push({
830
+ index: i,
831
+ name: `Ch${i}`,
832
+ noteCount: validNotes.length,
833
+ selected: validNotes.length > 0
834
+ });
835
+ }
836
+ return result;
837
+ };
838
+ var getMidiBPM = (midi) => {
839
+ const { track } = midi;
840
+ for (const { event } of track) {
841
+ for (const { type, metaType, data } of event) {
842
+ if (type !== 255 || metaType !== 81) continue;
843
+ if (typeof data === "number") {
844
+ return 6e7 / data;
845
+ }
846
+ const [b1, b2, b3] = data;
847
+ return 6e7 / (b1 << 16 | b2 << 8 | b3);
848
+ }
849
+ }
850
+ return 120;
851
+ };
852
+ var extractMidiPlacements = (midi, selectedTrackIndices) => {
853
+ const { track, timeDivision } = midi;
854
+ const ticksPerBeat = timeDivision;
855
+ const bpm = getMidiBPM(midi);
856
+ const channelNotes = {};
857
+ for (const trackIdx of selectedTrackIndices) {
858
+ const trackData = track[trackIdx];
859
+ if (!trackData) continue;
860
+ let currentTime = 0;
861
+ for (const event of trackData.event) {
862
+ currentTime += event.deltaTime;
863
+ if (event.channel === 9) continue;
864
+ if (event.type !== 8 && event.type !== 9) continue;
865
+ const [pitch, velocity] = event.data;
866
+ const isNoteOff = event.type === 8 || !velocity;
867
+ const channel = event.channel ?? 0;
868
+ if (!channelNotes[channel]) channelNotes[channel] = [];
869
+ if (isNoteOff) {
870
+ for (let i = channelNotes[channel].length - 1; i >= 0; i--) {
871
+ const note = channelNotes[channel][i];
872
+ if (note.pitch === pitch && note.end === null) {
873
+ note.end = currentTime;
874
+ break;
875
+ }
876
+ }
877
+ } else {
878
+ channelNotes[channel].push({
879
+ pitch,
880
+ velocity,
881
+ start: currentTime,
882
+ end: null
883
+ });
884
+ }
885
+ }
886
+ }
887
+ const ticksPerBar = ticksPerBeat * 4;
888
+ const ticksPer8Bars = ticksPerBar * 8;
889
+ const channelAnalysis = {};
890
+ for (const [channelStr, notes] of Object.entries(channelNotes)) {
891
+ const channel = Number.parseInt(channelStr, 10);
892
+ const validNotes = notes.filter(
893
+ (n) => n.end !== null
894
+ );
895
+ if (validNotes.length === 0) {
896
+ channelAnalysis[channel] = {
897
+ avgPitch: 60,
898
+ maxSimultaneous: 0,
899
+ hasSubmelodyPattern: false
900
+ };
901
+ continue;
902
+ }
903
+ const avgPitch = validNotes.reduce((sum, n) => sum + n.pitch, 0) / validNotes.length;
904
+ let maxSimultaneous = 0;
905
+ const sortedNotes = [...validNotes].sort((a, b) => a.start - b.start);
906
+ for (let i = 0; i < sortedNotes.length; i++) {
907
+ let simultaneous = 1;
908
+ for (let j = i + 1; j < sortedNotes.length; j++) {
909
+ if (sortedNotes[j].start < sortedNotes[i].end) {
910
+ simultaneous++;
911
+ }
912
+ }
913
+ maxSimultaneous = Math.max(maxSimultaneous, simultaneous);
914
+ }
915
+ const isSubmelodyPattern = () => {
916
+ if (sortedNotes.length === 0) return false;
917
+ const blocks = [];
918
+ let blockStart = sortedNotes[0].start;
919
+ let blockEnd = sortedNotes[0].end;
920
+ for (let i = 1; i < sortedNotes.length; i++) {
921
+ const gap = sortedNotes[i].start - sortedNotes[i - 1].end;
922
+ if (gap >= ticksPerBar) {
923
+ blocks.push({ start: blockStart, end: blockEnd });
924
+ blockStart = sortedNotes[i].start;
925
+ blockEnd = sortedNotes[i].end;
926
+ } else {
927
+ blockEnd = sortedNotes[i].end;
928
+ }
929
+ }
930
+ blocks.push({ start: blockStart, end: blockEnd });
931
+ return blocks.every((b) => b.end - b.start < ticksPer8Bars);
932
+ };
933
+ channelAnalysis[channel] = {
934
+ avgPitch,
935
+ maxSimultaneous,
936
+ hasSubmelodyPattern: isSubmelodyPattern()
937
+ };
938
+ }
939
+ const channels = Object.keys(channelNotes).map(Number).sort((a, b) => a - b);
940
+ const sortedByPitch = [...channels].sort(
941
+ (a, b) => channelAnalysis[a].avgPitch - channelAnalysis[b].avgPitch
942
+ );
943
+ const bassThreshold = channelAnalysis[sortedByPitch[Math.floor(sortedByPitch.length / 4)]]?.avgPitch ?? 60;
944
+ const bassChannels = channels.filter(
945
+ (ch) => channelAnalysis[ch].avgPitch <= bassThreshold && channelAnalysis[ch].maxSimultaneous <= 2
946
+ );
947
+ const melodyTypeChannels = channels.filter(
948
+ (ch) => channelAnalysis[ch].maxSimultaneous <= 1 && !bassChannels.includes(ch)
949
+ );
950
+ const submelodyChannels = melodyTypeChannels.filter(
951
+ (ch) => channelAnalysis[ch].hasSubmelodyPattern
952
+ );
953
+ const melodyChannels = melodyTypeChannels.filter(
954
+ (ch) => !channelAnalysis[ch].hasSubmelodyPattern
955
+ );
956
+ const chordChannels = channels.filter(
957
+ (ch) => !bassChannels.includes(ch) && !melodyChannels.includes(ch) && !submelodyChannels.includes(ch)
958
+ );
959
+ const channelToTrack = {
960
+ melody: melodyChannels,
961
+ submelody: submelodyChannels,
962
+ bass: bassChannels,
963
+ chord: chordChannels
964
+ };
965
+ const placements = [];
966
+ const ticksPerStep = ticksPerBeat / STEPS_PER_BEAT;
967
+ for (const [channelStr, notes] of Object.entries(channelNotes)) {
968
+ const channel = Number.parseInt(channelStr, 10);
969
+ let trackId = null;
970
+ for (const [tid, chs] of Object.entries(channelToTrack)) {
971
+ if (chs.includes(channel)) {
972
+ trackId = tid;
973
+ break;
974
+ }
975
+ }
976
+ if (!trackId) continue;
977
+ for (const note of notes) {
978
+ if (note.end === null) continue;
979
+ const startStep = Math.round(note.start / ticksPerStep);
980
+ const durationSteps = Math.max(
981
+ 1,
982
+ Math.round((note.end - note.start) / ticksPerStep)
983
+ );
984
+ placements.push({
985
+ trackId,
986
+ startStep,
987
+ pitch: note.pitch,
988
+ durationSteps,
989
+ velocity: note.velocity
990
+ });
991
+ }
992
+ }
993
+ return { placements, bpm };
994
+ };
995
+ var extractMidiPlacementsByTrack = (midi, selectedIndices, trackIds) => {
996
+ const { track, timeDivision } = midi;
997
+ const ticksPerBeat = timeDivision;
998
+ const bpm = getMidiBPM(midi);
999
+ const ticksPerStep = ticksPerBeat / STEPS_PER_BEAT;
1000
+ const placements = [];
1001
+ const selectedSet = new Set(selectedIndices);
1002
+ for (let midiIdx = 0; midiIdx < track.length; midiIdx++) {
1003
+ if (!selectedSet.has(midiIdx)) continue;
1004
+ if (midiIdx >= trackIds.length) continue;
1005
+ const trackId = trackIds[midiIdx];
1006
+ const trackData = track[midiIdx];
1007
+ if (!trackData) continue;
1008
+ const active = [];
1009
+ let currentTime = 0;
1010
+ for (const event of trackData.event) {
1011
+ currentTime += event.deltaTime;
1012
+ if (event.channel === 9) continue;
1013
+ if (event.type !== 8 && event.type !== 9) continue;
1014
+ const [pitch, velocity] = event.data;
1015
+ const isOff = event.type === 8 || !velocity;
1016
+ if (isOff) {
1017
+ for (let i = active.length - 1; i >= 0; i--) {
1018
+ if (active[i].pitch === pitch && active[i].end === null) {
1019
+ active[i].end = currentTime;
1020
+ break;
1021
+ }
1022
+ }
1023
+ } else {
1024
+ active.push({ pitch, velocity, start: currentTime, end: null });
1025
+ }
1026
+ }
1027
+ for (const note of active) {
1028
+ if (note.end === null) continue;
1029
+ const startStep = Math.round(note.start / ticksPerStep);
1030
+ const durationSteps = Math.max(
1031
+ 1,
1032
+ Math.round((note.end - note.start) / ticksPerStep)
1033
+ );
1034
+ placements.push({
1035
+ trackId,
1036
+ startStep,
1037
+ pitch: note.pitch,
1038
+ durationSteps,
1039
+ velocity: note.velocity
1040
+ });
1041
+ }
1042
+ }
1043
+ return { placements, bpm };
1044
+ };
1045
+ var to2byte = (n) => [(n & 65280) >> 8, n & 255];
1046
+ var to3byte = (n) => [(n & 16711680) >> 16, ...to2byte(n)];
1047
+ var to4byte = (n) => [
1048
+ (n & 4278190080) >> 24,
1049
+ ...to3byte(n)
1050
+ ];
1051
+ var deltaTime = (n) => {
1052
+ const res = [n & 127];
1053
+ let v = n >> 7;
1054
+ while (v > 0) {
1055
+ res.push(v & 127 | 128);
1056
+ v >>= 7;
1057
+ }
1058
+ return res.reverse();
1059
+ };
1060
+ var headerChunks = (arr, trackCount, div) => {
1061
+ arr.push(77, 84, 104, 100);
1062
+ arr.push(...to4byte(6));
1063
+ arr.push(...to2byte(1));
1064
+ arr.push(...to2byte(trackCount));
1065
+ arr.push(...to2byte(div));
1066
+ };
1067
+ var trackChunks = (arr, func) => {
1068
+ arr.push(77, 84, 114, 107);
1069
+ const a = [];
1070
+ func(a);
1071
+ a.push(...deltaTime(0));
1072
+ a.push(255, 47, 0);
1073
+ arr.push(...to4byte(a.length));
1074
+ arr.push(...a);
1075
+ };
1076
+ var exportMIDI = (options) => {
1077
+ const { tracks, drumPattern, drumVolume = 80, bpm, stepsPerBar } = options;
1078
+ const div = 480;
1079
+ const tickPerStep = div / STEPS_PER_BEAT;
1080
+ const midiTracks = [];
1081
+ tracks.forEach((track, ch) => {
1082
+ if (track.notes.length === 0) return;
1083
+ const events = [];
1084
+ for (const n of track.notes) {
1085
+ const startTick = Math.round(n.startStep * tickPerStep);
1086
+ const endTick = Math.round(
1087
+ (n.startStep + (n.durationSteps || 1)) * tickPerStep
1088
+ );
1089
+ const vel = Math.round(
1090
+ (n.velocity ?? 100) * (track.volume || 100) / 100
1091
+ );
1092
+ events.push({ t: startTick, m: [144 | ch & 15, n.pitch, vel] });
1093
+ events.push({ t: endTick, m: [144 | ch & 15, n.pitch, 0] });
1094
+ }
1095
+ events.sort((a, b) => a.t - b.t);
1096
+ midiTracks.push(events);
1097
+ });
1098
+ if (drumPattern && drumPattern.length > 0) {
1099
+ const maxStep = Math.max(
1100
+ ...tracks.filter((t) => t.notes.length > 0).map(
1101
+ (t) => Math.max(...t.notes.map((n) => n.startStep + n.durationSteps))
1102
+ ),
1103
+ stepsPerBar
1104
+ );
1105
+ const drumEvents = [];
1106
+ const numBars = Math.ceil(maxStep / stepsPerBar);
1107
+ for (let bar = 0; bar < numBars; bar++) {
1108
+ const barStart = bar * stepsPerBar;
1109
+ for (const drum of drumPattern) {
1110
+ const step = barStart + drum.step;
1111
+ if (step >= maxStep) continue;
1112
+ const vel = Math.round(
1113
+ (drum.velocity ?? 1) * (drumVolume / 100) * 127
1114
+ );
1115
+ drumEvents.push({
1116
+ t: Math.round(step * tickPerStep),
1117
+ m: [153, drum.pitch, vel]
1118
+ });
1119
+ drumEvents.push({
1120
+ t: Math.round((step + 1) * tickPerStep),
1121
+ m: [153, drum.pitch, 0]
1122
+ });
1123
+ }
1124
+ }
1125
+ drumEvents.sort((a, b) => a.t - b.t);
1126
+ if (drumEvents.length > 0) midiTracks.push(drumEvents);
1127
+ }
1128
+ const arr = [];
1129
+ headerChunks(arr, midiTracks.length + 1, div);
1130
+ trackChunks(arr, (a) => {
1131
+ a.push(0, 255, 81, 3, ...to3byte(Math.round(6e7 / bpm)));
1132
+ });
1133
+ for (const events of midiTracks) {
1134
+ trackChunks(arr, (a) => {
1135
+ let lastTick = 0;
1136
+ for (const ev of events) {
1137
+ a.push(...deltaTime(ev.t - lastTick), ...ev.m);
1138
+ lastTick = ev.t;
1139
+ }
1140
+ });
1141
+ }
1142
+ return new Blob([new Uint8Array(arr).buffer], { type: "audio/midi" });
1143
+ };
1144
+
1145
+ // src/linked-list.ts
1146
+ var LinkedList = class {
1147
+ #cursor;
1148
+ constructor() {
1149
+ const node = { value: null, prev: null, next: null };
1150
+ this.#cursor = node;
1151
+ }
1152
+ /**
1153
+ * 履歴を1つ追加
1154
+ */
1155
+ add(value) {
1156
+ const node = {
1157
+ value,
1158
+ prev: this.#cursor,
1159
+ next: null
1160
+ };
1161
+ this.#cursor.next = node;
1162
+ this.#cursor = node;
1163
+ }
1164
+ /**
1165
+ * 履歴を1つ戻す
1166
+ */
1167
+ undo() {
1168
+ const { prev } = this.#cursor;
1169
+ if (prev === null || prev.value === null) return null;
1170
+ this.#cursor = prev;
1171
+ return this.#cursor.value;
1172
+ }
1173
+ /**
1174
+ * 履歴を1つ進める
1175
+ */
1176
+ redo() {
1177
+ const { next } = this.#cursor;
1178
+ if (next === null || next.value === null) return null;
1179
+ this.#cursor = next;
1180
+ return this.#cursor.value;
1181
+ }
1182
+ /**
1183
+ * Undo可能かチェック(カーソル移動なし)
1184
+ */
1185
+ canUndo() {
1186
+ return this.#cursor.prev?.value !== null;
1187
+ }
1188
+ /**
1189
+ * Redo可能かチェック(カーソル移動なし)
1190
+ */
1191
+ canRedo() {
1192
+ const { next } = this.#cursor;
1193
+ return next !== null && next.value !== null;
1194
+ }
1195
+ };
1196
+
1197
+ // src/renderer.ts
1198
+ var g_header_canvas;
1199
+ var g_key_canvas;
1200
+ var g_grid_canvas;
1201
+ var g_header_ctx;
1202
+ var g_key_ctx;
1203
+ var g_grid_ctx;
1204
+ var g_config;
1205
+ var KEYBOARD_WIDTH = 60;
1206
+ var HEADER_HEIGHT = 20;
1207
+ var getRenderConfig = () => g_config;
1208
+ var g_draw_offset_x = 0;
1209
+ var g_draw_offset_y = 0;
1210
+ var getDrawOffset = () => ({
1211
+ x: g_draw_offset_x,
1212
+ y: g_draw_offset_y
1213
+ });
1214
+ var getGridCanvas = () => g_grid_canvas;
1215
+ var getGridContext = () => g_grid_ctx;
1216
+ var getHeaderCanvas = () => g_header_canvas;
1217
+ var init = (mountTarget, width = 800, height = 450, config) => {
1218
+ g_config = config;
1219
+ const headerCanvas = document.createElement("canvas");
1220
+ g_header_canvas = headerCanvas;
1221
+ headerCanvas.width = width - KEYBOARD_WIDTH;
1222
+ headerCanvas.height = HEADER_HEIGHT;
1223
+ headerCanvas.style.position = "absolute";
1224
+ headerCanvas.style.left = `${KEYBOARD_WIDTH}px`;
1225
+ headerCanvas.style.top = "0px";
1226
+ const headerCtx = headerCanvas.getContext("2d");
1227
+ if (!headerCtx)
1228
+ throw new Error("Failed to get 2D rendering context for header.");
1229
+ g_header_ctx = headerCtx;
1230
+ const keyCanvas = document.createElement("canvas");
1231
+ g_key_canvas = keyCanvas;
1232
+ keyCanvas.width = KEYBOARD_WIDTH;
1233
+ keyCanvas.height = height - HEADER_HEIGHT;
1234
+ keyCanvas.style.position = "absolute";
1235
+ keyCanvas.style.left = "0px";
1236
+ keyCanvas.style.top = `${HEADER_HEIGHT}px`;
1237
+ const keyCtx = keyCanvas.getContext("2d");
1238
+ if (!keyCtx)
1239
+ throw new Error("Failed to get 2D rendering context for keyboard.");
1240
+ g_key_ctx = keyCtx;
1241
+ const gridCanvas = document.createElement("canvas");
1242
+ g_grid_canvas = gridCanvas;
1243
+ gridCanvas.width = width - KEYBOARD_WIDTH;
1244
+ gridCanvas.height = height - HEADER_HEIGHT;
1245
+ gridCanvas.style.position = "absolute";
1246
+ gridCanvas.style.left = `${KEYBOARD_WIDTH}px`;
1247
+ gridCanvas.style.top = `${HEADER_HEIGHT}px`;
1248
+ gridCanvas.style.touchAction = "none";
1249
+ const gridCtx = gridCanvas.getContext("2d", { willReadFrequently: true });
1250
+ if (!gridCtx) throw new Error("Failed to get 2D rendering context for grid.");
1251
+ g_grid_ctx = gridCtx;
1252
+ mountTarget.innerHTML = "";
1253
+ mountTarget.style.position = "relative";
1254
+ mountTarget.style.width = `${width + KEYBOARD_WIDTH}px`;
1255
+ mountTarget.style.height = `${height}px`;
1256
+ mountTarget.append(headerCanvas, keyCanvas, gridCanvas);
1257
+ drawHeaderCorner();
1258
+ };
1259
+ var blackKeyPitches = /* @__PURE__ */ new Set([1, 3, 6, 8, 10]);
1260
+ var KEY_NAMES = [
1261
+ "C",
1262
+ "C#",
1263
+ "D",
1264
+ "D#",
1265
+ "E",
1266
+ "F",
1267
+ "F#",
1268
+ "G",
1269
+ "G#",
1270
+ "A",
1271
+ "A#",
1272
+ "B"
1273
+ ];
1274
+ var drawHeaderCorner = () => {
1275
+ const mountTarget = g_key_canvas.parentElement;
1276
+ if (!mountTarget) return;
1277
+ let cornerDiv = mountTarget.querySelector("#header-corner");
1278
+ if (!cornerDiv) {
1279
+ cornerDiv = document.createElement("div");
1280
+ cornerDiv.id = "header-corner";
1281
+ cornerDiv.style.position = "absolute";
1282
+ cornerDiv.style.left = "0px";
1283
+ cornerDiv.style.top = "0px";
1284
+ cornerDiv.style.width = `${KEYBOARD_WIDTH}px`;
1285
+ cornerDiv.style.height = `${HEADER_HEIGHT}px`;
1286
+ cornerDiv.style.backgroundColor = "#0a0f1f";
1287
+ cornerDiv.style.borderRight = "2px solid #29adff";
1288
+ cornerDiv.style.borderBottom = "2px solid #29adff";
1289
+ mountTarget.insertBefore(cornerDiv, g_header_canvas);
1290
+ }
1291
+ };
1292
+ var drawKeyboard = () => {
1293
+ g_key_ctx.clearRect(0, 0, g_key_canvas.width, g_key_canvas.height);
1294
+ const { keyHeight, keyCount, pitchRangeStart } = g_config;
1295
+ const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
1296
+ const endY = g_draw_offset_y + g_key_canvas.height;
1297
+ const WHITE_KEY = "#ccc8b4";
1298
+ const BLACK_KEY = "#111111";
1299
+ const BK_EDGE = "#383838";
1300
+ const WW_SEP = "#807a6a";
1301
+ const BK_RATIO = 0.62;
1302
+ for (let y = startY; y < endY; y += keyHeight) {
1303
+ const pitchIndex = keyCount - 1 - y / keyHeight;
1304
+ const totalPitch = pitchIndex + pitchRangeStart;
1305
+ const pitchMod12 = totalPitch % 12;
1306
+ const isBlackKey = blackKeyPitches.has(pitchMod12);
1307
+ const screenY = y - g_draw_offset_y;
1308
+ const bkW = Math.floor(KEYBOARD_WIDTH * BK_RATIO);
1309
+ if (isBlackKey) {
1310
+ g_key_ctx.fillStyle = WHITE_KEY;
1311
+ g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
1312
+ g_key_ctx.fillStyle = BLACK_KEY;
1313
+ g_key_ctx.fillRect(0, screenY, bkW, keyHeight);
1314
+ g_key_ctx.strokeStyle = BK_EDGE;
1315
+ g_key_ctx.lineWidth = 1;
1316
+ g_key_ctx.beginPath();
1317
+ g_key_ctx.moveTo(bkW, screenY);
1318
+ g_key_ctx.lineTo(bkW, screenY + keyHeight);
1319
+ g_key_ctx.stroke();
1320
+ } else {
1321
+ g_key_ctx.fillStyle = WHITE_KEY;
1322
+ g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
1323
+ if (pitchMod12 === 5 || pitchMod12 === 0) {
1324
+ g_key_ctx.strokeStyle = WW_SEP;
1325
+ g_key_ctx.lineWidth = 1;
1326
+ g_key_ctx.beginPath();
1327
+ g_key_ctx.moveTo(0, screenY + keyHeight - 0.5);
1328
+ g_key_ctx.lineTo(KEYBOARD_WIDTH, screenY + keyHeight - 0.5);
1329
+ g_key_ctx.stroke();
1330
+ }
1331
+ }
1332
+ if (pitchMod12 === 0) {
1333
+ const octave = Math.floor(totalPitch / 12) - 1;
1334
+ g_key_ctx.fillStyle = "#555040";
1335
+ g_key_ctx.font = "10px 'k8x12',monospace";
1336
+ g_key_ctx.textAlign = "right";
1337
+ g_key_ctx.textBaseline = "bottom";
1338
+ g_key_ctx.fillText(
1339
+ `${KEY_NAMES[pitchMod12]}${octave}`,
1340
+ KEYBOARD_WIDTH - 4,
1341
+ screenY + keyHeight - 2
1342
+ );
1343
+ }
1344
+ }
1345
+ g_key_ctx.beginPath();
1346
+ g_key_ctx.strokeStyle = "#29adff";
1347
+ g_key_ctx.lineWidth = 2;
1348
+ g_key_ctx.moveTo(KEYBOARD_WIDTH, 0);
1349
+ g_key_ctx.lineTo(KEYBOARD_WIDTH, g_key_canvas.height);
1350
+ g_key_ctx.stroke();
1351
+ };
1352
+ var drawHeader = () => {
1353
+ g_header_ctx.clearRect(0, 0, g_header_canvas.width, g_header_canvas.height);
1354
+ const { stepWidth, stepsPerBar } = g_config;
1355
+ g_header_ctx.save();
1356
+ g_header_ctx.translate(-g_draw_offset_x, 0);
1357
+ g_header_ctx.fillStyle = "#0a0f1f";
1358
+ g_header_ctx.fillRect(
1359
+ g_draw_offset_x,
1360
+ 0,
1361
+ g_header_canvas.width,
1362
+ HEADER_HEIGHT
1363
+ );
1364
+ g_header_ctx.strokeStyle = "#3d405b";
1365
+ g_header_ctx.lineWidth = 1;
1366
+ g_header_ctx.font = "11px 'k8x12',monospace";
1367
+ g_header_ctx.fillStyle = "#83769c";
1368
+ const startBar = Math.floor(g_draw_offset_x / (stepsPerBar * stepWidth));
1369
+ const endBar = Math.ceil(
1370
+ (g_draw_offset_x + g_header_canvas.width) / (stepsPerBar * stepWidth)
1371
+ );
1372
+ for (let bar = startBar; bar <= endBar + 1; bar++) {
1373
+ const x = bar * stepsPerBar * stepWidth;
1374
+ const screenX = x;
1375
+ g_header_ctx.beginPath();
1376
+ g_header_ctx.moveTo(screenX, 0);
1377
+ g_header_ctx.lineTo(screenX, HEADER_HEIGHT);
1378
+ g_header_ctx.stroke();
1379
+ if (bar >= 0) {
1380
+ g_header_ctx.textAlign = "left";
1381
+ g_header_ctx.textBaseline = "middle";
1382
+ g_header_ctx.fillText(`${bar + 1}`, screenX + 5, HEADER_HEIGHT / 2);
1383
+ }
1384
+ }
1385
+ g_header_ctx.restore();
1386
+ };
1387
+ var drawGrid = (noteLengthSteps = 1) => {
1388
+ drawKeyboard();
1389
+ drawHeader();
1390
+ g_grid_ctx.clearRect(0, 0, g_grid_canvas.width, g_grid_canvas.height);
1391
+ const { keyHeight, keyCount, stepWidth, stepsPerBar } = g_config;
1392
+ const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
1393
+ const endY = g_draw_offset_y + g_grid_canvas.height;
1394
+ for (let y = startY; y < endY; y += keyHeight) {
1395
+ const pitchIndex = keyCount - 1 - y / keyHeight;
1396
+ const pitchMod12 = pitchIndex % 12;
1397
+ const isBlackKey = blackKeyPitches.has(pitchMod12);
1398
+ const isC = pitchMod12 === 0;
1399
+ const screenY = y - g_draw_offset_y;
1400
+ if (isBlackKey) {
1401
+ g_grid_ctx.fillStyle = "#0d1020";
1402
+ g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
1403
+ }
1404
+ g_grid_ctx.beginPath();
1405
+ g_grid_ctx.strokeStyle = isC ? "#3d405b" : "#1a1d30";
1406
+ g_grid_ctx.lineWidth = 1;
1407
+ const lineY = screenY + keyHeight;
1408
+ g_grid_ctx.moveTo(0, lineY);
1409
+ g_grid_ctx.lineTo(g_grid_canvas.width, lineY);
1410
+ g_grid_ctx.stroke();
1411
+ }
1412
+ const gridStep = noteLengthSteps || 48;
1413
+ const startX = Math.floor(g_draw_offset_x / (stepWidth * gridStep)) * stepWidth * gridStep;
1414
+ const endX = g_draw_offset_x + g_grid_canvas.width;
1415
+ const lineStep = stepWidth * gridStep;
1416
+ for (let x = startX; x <= endX; x += lineStep) {
1417
+ const step = x / stepWidth;
1418
+ const isBarLine = step % stepsPerBar === 0;
1419
+ const isNoteLine = step % gridStep === 0;
1420
+ const screenX = x - g_draw_offset_x;
1421
+ g_grid_ctx.beginPath();
1422
+ g_grid_ctx.strokeStyle = isBarLine ? "#3d405b" : isNoteLine ? "#242840" : "#1a1d30";
1423
+ g_grid_ctx.lineWidth = isBarLine ? 2 : 1;
1424
+ g_grid_ctx.moveTo(screenX, 0);
1425
+ g_grid_ctx.lineTo(screenX, g_grid_canvas.height);
1426
+ g_grid_ctx.stroke();
1427
+ }
1428
+ };
1429
+ var drawNotes = (notes, color = [59, 130, 246, 1]) => {
1430
+ const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
1431
+ for (const note of notes) {
1432
+ const logicalX = note.startStep * stepWidth;
1433
+ const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
1434
+ const logicalY = yIndex * keyHeight;
1435
+ const w = note.durationSteps * stepWidth;
1436
+ const h = keyHeight;
1437
+ const renderX = logicalX - g_draw_offset_x;
1438
+ const renderY = logicalY - g_draw_offset_y;
1439
+ const velocityOpacity = note.velocity !== void 0 ? 0.5 + note.velocity / 127 * 0.5 : 1;
1440
+ const [r, g, b, a] = color;
1441
+ const finalOpacity = a * velocityOpacity;
1442
+ g_grid_ctx.fillStyle = `rgba(${r},${g},${b},${finalOpacity})`;
1443
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
1444
+ }
1445
+ };
1446
+ var drawSelectionRect = (rect) => {
1447
+ if (!rect) return;
1448
+ g_grid_ctx.save();
1449
+ g_grid_ctx.strokeStyle = "#ffec27";
1450
+ g_grid_ctx.lineWidth = 2;
1451
+ g_grid_ctx.setLineDash([4, 4]);
1452
+ g_grid_ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
1453
+ g_grid_ctx.fillStyle = "rgba(255,236,39,0.08)";
1454
+ g_grid_ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
1455
+ g_grid_ctx.restore();
1456
+ };
1457
+ var drawSelectedNotes = (notes, selectedIds, baseColor = [59, 130, 246, 1]) => {
1458
+ const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
1459
+ for (const note of notes) {
1460
+ if (!selectedIds.has(note.id)) continue;
1461
+ const logicalX = note.startStep * stepWidth;
1462
+ const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
1463
+ const logicalY = yIndex * keyHeight;
1464
+ const w = note.durationSteps * stepWidth;
1465
+ const h = keyHeight;
1466
+ const renderX = logicalX - g_draw_offset_x;
1467
+ const renderY = logicalY - g_draw_offset_y;
1468
+ const velocityOpacity = note.velocity !== void 0 ? 0.5 + note.velocity / 127 * 0.5 : 1;
1469
+ const [r, g, b, a] = baseColor;
1470
+ const darkenFactor = 1.3;
1471
+ const darkerR = Math.min(255, r * darkenFactor);
1472
+ const darkerG = Math.min(255, g * darkenFactor);
1473
+ const darkerB = Math.min(255, b * darkenFactor);
1474
+ const finalOpacity = a * velocityOpacity;
1475
+ g_grid_ctx.fillStyle = `rgba(${darkerR},${darkerG},${darkerB},${finalOpacity})`;
1476
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
1477
+ }
1478
+ };
1479
+ var getXY = (e) => {
1480
+ const { clientX, clientY } = e;
1481
+ const rect = g_grid_canvas.getBoundingClientRect();
1482
+ const x = Math.floor(clientX - rect.left);
1483
+ const y = Math.floor(clientY - rect.top);
1484
+ return [x, y, e.buttons];
1485
+ };
1486
+ var getGridPosition = (e) => {
1487
+ const [x, y] = getXY(e);
1488
+ const { keyCount, pitchRangeStart, keyHeight, stepWidth } = g_config;
1489
+ const step = Math.floor((x + g_draw_offset_x) / stepWidth);
1490
+ const absoluteY = y + g_draw_offset_y;
1491
+ const yIndex = Math.floor(absoluteY / keyHeight);
1492
+ const pitch = keyCount - 1 - yIndex + pitchRangeStart;
1493
+ return { step, pitch, x, y };
1494
+ };
1495
+ var onClick = (callback) => {
1496
+ g_grid_canvas.addEventListener(
1497
+ "click",
1498
+ (e) => {
1499
+ const [x, y] = getXY(e);
1500
+ const { keyCount, pitchRangeStart, keyHeight, stepWidth } = g_config;
1501
+ const step = Math.floor((x + g_draw_offset_x) / stepWidth);
1502
+ const absoluteY = y + g_draw_offset_y;
1503
+ const yIndex = Math.floor(absoluteY / keyHeight);
1504
+ const pitch = keyCount - 1 - yIndex + pitchRangeStart;
1505
+ if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount) {
1506
+ requestAnimationFrame(() => callback(step, pitch));
1507
+ }
1508
+ },
1509
+ { passive: true }
1510
+ );
1511
+ g_grid_canvas.addEventListener("contextmenu", (e) => e.preventDefault());
1512
+ };
1513
+ var setDrawOffset = (x, y) => {
1514
+ g_draw_offset_x = x;
1515
+ g_draw_offset_y = y;
1516
+ drawKeyboard();
1517
+ drawHeader();
1518
+ };
1519
+
1520
+ // src/mml-core.ts
1521
+ var PITCH_MAP = [
1522
+ "c",
1523
+ "c+",
1524
+ "d",
1525
+ "d+",
1526
+ "e",
1527
+ "f",
1528
+ "f+",
1529
+ "g",
1530
+ "g+",
1531
+ "a",
1532
+ "a+",
1533
+ "b"
1534
+ ];
1535
+ var MMLCore = class _MMLCore {
1536
+ notes = [];
1537
+ nextNoteId = 0;
1538
+ handlers;
1539
+ volume = 80;
1540
+ tempo = 120;
1541
+ history = new LinkedList();
1542
+ isUndoRedo = false;
1543
+ isBatchOperation = false;
1544
+ lastHistorySnapshot = "[]";
1545
+ lastUndoTime = 0;
1546
+ static UNDO_DEBOUNCE_MS = 100;
1547
+ toolMode = "pen";
1548
+ constructor(handlers, volume = 80) {
1549
+ this.handlers = handlers;
1550
+ this.volume = volume;
1551
+ this.lastHistorySnapshot = JSON.stringify(this.notes);
1552
+ this.history.add([]);
1553
+ this.generateAndNotify();
1554
+ }
1555
+ beginBatch() {
1556
+ this.isBatchOperation = true;
1557
+ }
1558
+ endBatch() {
1559
+ this.isBatchOperation = false;
1560
+ this.saveHistory();
1561
+ }
1562
+ saveHistory() {
1563
+ if (this.isUndoRedo || this.isBatchOperation) {
1564
+ return;
1565
+ }
1566
+ const snapshot = JSON.stringify(this.notes);
1567
+ if (snapshot === this.lastHistorySnapshot) {
1568
+ return;
1569
+ }
1570
+ this.lastHistorySnapshot = snapshot;
1571
+ this.history.add(JSON.parse(snapshot));
1572
+ }
1573
+ restoreHistory(notes) {
1574
+ if (notes === null) return false;
1575
+ this.isUndoRedo = true;
1576
+ this.notes = JSON.parse(JSON.stringify(notes));
1577
+ this.nextNoteId = this.notes.length > 0 ? Math.max(...this.notes.map((n) => n.id)) + 1 : 0;
1578
+ this.lastHistorySnapshot = JSON.stringify(this.notes);
1579
+ this.generateAndNotify();
1580
+ this.isUndoRedo = false;
1581
+ return true;
1582
+ }
1583
+ undo() {
1584
+ const now = Date.now();
1585
+ if (now - this.lastUndoTime < _MMLCore.UNDO_DEBOUNCE_MS) {
1586
+ return false;
1587
+ }
1588
+ this.lastUndoTime = now;
1589
+ return this.restoreHistory(this.history.undo());
1590
+ }
1591
+ redo() {
1592
+ const now = Date.now();
1593
+ if (now - this.lastUndoTime < _MMLCore.UNDO_DEBOUNCE_MS) {
1594
+ return false;
1595
+ }
1596
+ this.lastUndoTime = now;
1597
+ return this.restoreHistory(this.history.redo());
1598
+ }
1599
+ canUndo() {
1600
+ return this.history.canUndo();
1601
+ }
1602
+ canRedo() {
1603
+ return this.history.canRedo();
1604
+ }
1605
+ setToolMode(mode) {
1606
+ this.toolMode = mode;
1607
+ }
1608
+ getToolMode() {
1609
+ return this.toolMode;
1610
+ }
1611
+ resetHistory() {
1612
+ this.history = new LinkedList();
1613
+ this.history.add([]);
1614
+ this.lastHistorySnapshot = JSON.stringify(this.notes);
1615
+ }
1616
+ addHistoryOnce() {
1617
+ this.lastHistorySnapshot = "[]";
1618
+ this.saveHistory();
1619
+ }
1620
+ clearNotesWithoutHistory() {
1621
+ this.notes = [];
1622
+ this.nextNoteId = 0;
1623
+ this.lastHistorySnapshot = "[]";
1624
+ }
1625
+ setLoadMode(mode) {
1626
+ this.isUndoRedo = mode;
1627
+ }
1628
+ // ============== ノート編集 (外部API) ==============
1629
+ /**
1630
+ * 指定されたグリッド位置にノートを追加する操作
1631
+ * @param step ステップ位置
1632
+ * @param pitch ピッチ番号
1633
+ * @param options ノート長などの設定
1634
+ */
1635
+ addNote(step, pitch, options) {
1636
+ const existingIndex = this.notes.findIndex(
1637
+ (n) => n.startStep === step && n.pitch === pitch
1638
+ );
1639
+ if (existingIndex === -1) {
1640
+ const newNote = {
1641
+ id: this.nextNoteId++,
1642
+ startStep: step,
1643
+ durationSteps: options.noteLengthSteps,
1644
+ pitch,
1645
+ velocity: options.velocity ?? 100
1646
+ };
1647
+ this.notes.push(newNote);
1648
+ }
1649
+ this.notes.sort((a, b) => a.startStep - b.startStep);
1650
+ this.saveHistory();
1651
+ this.generateAndNotify();
1652
+ }
1653
+ deleteNoteById(noteId) {
1654
+ const index = this.notes.findIndex((n) => n.id === noteId);
1655
+ if (index !== -1) {
1656
+ this.notes.splice(index, 1);
1657
+ this.saveHistory();
1658
+ this.generateAndNotify();
1659
+ }
1660
+ }
1661
+ getMaxStep() {
1662
+ if (this.notes.length === 0) return 0;
1663
+ const stepsPer16th = 12;
1664
+ const maxRaw = Math.max(
1665
+ ...this.notes.map((n) => n.startStep + n.durationSteps)
1666
+ );
1667
+ return Math.ceil(maxRaw / stepsPer16th) * stepsPer16th;
1668
+ }
1669
+ moveNote(noteId, startStep, pitch) {
1670
+ const note = this.notes.find((target) => target.id === noteId);
1671
+ if (!note) return;
1672
+ const totalSteps = this.getMaxStep() + getRenderConfig().stepsPerBar;
1673
+ const pitchRangeStart = getRenderConfig().pitchRangeStart;
1674
+ const pitchRangeEnd = pitchRangeStart + getRenderConfig().keyCount - 1;
1675
+ const clampedPitch = Math.min(
1676
+ Math.max(pitch, pitchRangeStart),
1677
+ pitchRangeEnd
1678
+ );
1679
+ const clampedStart = Math.min(
1680
+ Math.max(startStep, 0),
1681
+ totalSteps - note.durationSteps
1682
+ );
1683
+ note.startStep = clampedStart;
1684
+ note.pitch = clampedPitch;
1685
+ this.notes.sort((a, b) => a.startStep - b.startStep);
1686
+ this.generateAndNotify();
1687
+ }
1688
+ moveNoteEnd(_) {
1689
+ this.saveHistory();
1690
+ }
1691
+ resizeNote(noteId, durationSteps) {
1692
+ const note = this.notes.find((target) => target.id === noteId);
1693
+ if (!note) return;
1694
+ const clampedDuration = Math.max(1, durationSteps);
1695
+ note.durationSteps = clampedDuration;
1696
+ this.notes.sort((a, b) => a.startStep - b.startStep);
1697
+ this.generateAndNotify();
1698
+ }
1699
+ resizeNoteEnd(_) {
1700
+ this.saveHistory();
1701
+ }
1702
+ // ============== 状態取得 (外部API) ==============
1703
+ getNotes() {
1704
+ return this.notes;
1705
+ }
1706
+ getMML(volumeOverride) {
1707
+ return this.generateMML(volumeOverride);
1708
+ }
1709
+ // ============== 設定変更 (外部API) ==============
1710
+ setVolume(volume) {
1711
+ this.volume = volume;
1712
+ this.generateAndNotify();
1713
+ }
1714
+ setTempo(tempo) {
1715
+ this.tempo = tempo;
1716
+ this.generateAndNotify();
1717
+ }
1718
+ // ============== 内部処理 ==============
1719
+ generateAndNotify() {
1720
+ this.handlers.onNotesChanged([...this.notes]);
1721
+ const mml = this.generateMML();
1722
+ this.handlers.onMMLGenerated(mml);
1723
+ }
1724
+ /**
1725
+ * 近似値を許容して単一音符を決定する。
1726
+ * ただし、残りステップ(limit)は絶対に超えない。
1727
+ */
1728
+ stepsToMMLDuration(steps, limit) {
1729
+ const config = getRenderConfig();
1730
+ const total = config.stepsPerBar;
1731
+ const candidates = [
1732
+ { dur: "1", s: total / 1 },
1733
+ { dur: "2.", s: total / 2 * 1.5 },
1734
+ { dur: "2", s: total / 2 },
1735
+ { dur: "4.", s: total / 4 * 1.5 },
1736
+ { dur: "4", s: total / 4 },
1737
+ { dur: "8.", s: total / 8 * 1.5 },
1738
+ { dur: "8", s: total / 8 },
1739
+ { dur: "12", s: total / 12 },
1740
+ { dur: "16.", s: total / 16 * 1.5 },
1741
+ { dur: "16", s: total / 16 },
1742
+ { dur: "24", s: total / 24 },
1743
+ // 3連8分 (24step)
1744
+ { dur: "32", s: total / 32 },
1745
+ { dur: "64", s: total / 64 }
1746
+ ];
1747
+ let bestDur = "64";
1748
+ let minDiff = Infinity;
1749
+ for (const cand of candidates) {
1750
+ if (cand.s > limit) continue;
1751
+ const diff = Math.abs(steps - cand.s);
1752
+ if (diff < minDiff) {
1753
+ minDiff = diff;
1754
+ bestDur = cand.dur;
1755
+ }
1756
+ }
1757
+ return bestDur;
1758
+ }
1759
+ /**
1760
+ * ギャップに収まる最大の音符を探す(減算アルゴリズム用)
1761
+ */
1762
+ findBestFitDuration(gap) {
1763
+ const config = getRenderConfig();
1764
+ const durations = [1, 2, 4, 8, 12, 16, 24, 32, 48, 64];
1765
+ for (const d of durations) {
1766
+ const stepLen = config.stepsPerBar / d;
1767
+ if (gap >= stepLen) {
1768
+ return { dur: d, steps: stepLen };
1769
+ }
1770
+ }
1771
+ return { dur: 64, steps: config.stepsPerBar / 64 };
1772
+ }
1773
+ /**
1774
+ * ピッチからオクターブ最適化のある音名を取得
1775
+ */
1776
+ getNoteWithOctave(pitch, lastOctave) {
1777
+ const octave = Math.floor(pitch / 12) - 1;
1778
+ const name = PITCH_MAP[pitch % 12];
1779
+ if (lastOctave === -1 || Math.abs(octave - lastOctave) >= 2) {
1780
+ return { text: `o${octave}${name}`, currentOctave: octave };
1781
+ }
1782
+ if (octave === lastOctave) {
1783
+ return { text: name, currentOctave: octave };
1784
+ } else if (octave === lastOctave + 1) {
1785
+ return { text: `>${name}`, currentOctave: octave };
1786
+ } else if (octave === lastOctave - 1) {
1787
+ return { text: `<${name}`, currentOctave: octave };
1788
+ }
1789
+ return { text: `o${octave}${name}`, currentOctave: octave };
1790
+ }
1791
+ /**
1792
+ * MML生成(1/2小節パターンスキャン方式)
1793
+ */
1794
+ generateMML = (volumeOverride) => {
1795
+ const config = getRenderConfig();
1796
+ const vol = volumeOverride ?? this.volume;
1797
+ const HALF_BAR = config.stepsPerBar / 2;
1798
+ const header = `t${this.tempo} q50 v${vol}`;
1799
+ const segments = [];
1800
+ let lastOctave = -1;
1801
+ let currentCursor = 0;
1802
+ if (this.notes.length === 0) return header;
1803
+ const lastNote = this.notes[this.notes.length - 1];
1804
+ const endStep = lastNote.startStep + lastNote.durationSteps;
1805
+ const totalSteps = Math.ceil(endStep / HALF_BAR) * HALF_BAR;
1806
+ for (let windowStart = 0; windowStart < totalSteps; windowStart += HALF_BAR) {
1807
+ const windowEnd = windowStart + HALF_BAR;
1808
+ const windowNotes = this.notes.filter(
1809
+ (n) => n.startStep >= windowStart && n.startStep < windowEnd
1810
+ );
1811
+ if (windowNotes.length === 0) {
1812
+ while (currentCursor < windowEnd) {
1813
+ const gap = windowEnd - currentCursor;
1814
+ if (gap <= 2) {
1815
+ currentCursor = windowEnd;
1816
+ break;
1817
+ }
1818
+ const { dur, steps } = this.findBestFitDuration(gap);
1819
+ segments.push(`r${dur}`);
1820
+ currentCursor += steps;
1821
+ }
1822
+ continue;
1823
+ }
1824
+ const notesByStep = /* @__PURE__ */ new Map();
1825
+ windowNotes.forEach((n) => {
1826
+ const list = notesByStep.get(n.startStep) || [];
1827
+ list.push(n);
1828
+ notesByStep.set(n.startStep, list);
1829
+ });
1830
+ const sortedSteps = Array.from(notesByStep.keys()).sort((a, b) => a - b);
1831
+ for (let i = 0; i < sortedSteps.length; i++) {
1832
+ const startStep = sortedSteps[i];
1833
+ const notes = notesByStep.get(startStep);
1834
+ if (!notes) continue;
1835
+ while (currentCursor < startStep) {
1836
+ const gap = startStep - currentCursor;
1837
+ if (gap <= 2) {
1838
+ currentCursor = startStep;
1839
+ break;
1840
+ }
1841
+ const { dur, steps } = this.findBestFitDuration(gap);
1842
+ segments.push(`r${dur}`);
1843
+ currentCursor += steps;
1844
+ }
1845
+ const nextStart = sortedSteps[i + 1] ?? windowEnd;
1846
+ const physicsLimit = nextStart - currentCursor;
1847
+ const MIN_STEP = config.stepsPerBar / 64;
1848
+ if (physicsLimit < MIN_STEP) {
1849
+ currentCursor = startStep;
1850
+ continue;
1851
+ }
1852
+ const idealDuration = notes[0].durationSteps;
1853
+ const durStr = this.stepsToMMLDuration(idealDuration, physicsLimit);
1854
+ const actualStepGenerated = this.getStepFromDottedMML(durStr);
1855
+ if (notes.length > 1) {
1856
+ const noteStrs = notes.map((n) => {
1857
+ const oct = Math.floor(n.pitch / 12) - 1;
1858
+ const name = PITCH_MAP[n.pitch % 12];
1859
+ return `o${oct}${name}`;
1860
+ });
1861
+ segments.push(`[${noteStrs.join("")}]${durStr}`);
1862
+ } else {
1863
+ const { text, currentOctave } = this.getNoteWithOctave(
1864
+ notes[0].pitch,
1865
+ lastOctave
1866
+ );
1867
+ segments.push(`${text}${durStr}`);
1868
+ lastOctave = currentOctave;
1869
+ }
1870
+ currentCursor += actualStepGenerated;
1871
+ }
1872
+ while (currentCursor < windowEnd) {
1873
+ const gap = windowEnd - currentCursor;
1874
+ if (gap <= 2) {
1875
+ currentCursor = windowEnd;
1876
+ break;
1877
+ }
1878
+ const { dur, steps } = this.findBestFitDuration(gap);
1879
+ segments.push(`r${dur}`);
1880
+ currentCursor += steps;
1881
+ }
1882
+ }
1883
+ return `${header} ${segments.join(" ")}`;
1884
+ };
1885
+ /**
1886
+ * ノート配列を直接渡してMMLを生成する(一時的に内部状態を差し替えて生成後に復元)
1887
+ */
1888
+ getMMLFromNotes(notes, tempo, volume) {
1889
+ const savedNotes = this.notes;
1890
+ const savedTempo = this.tempo;
1891
+ const savedVolume = this.volume;
1892
+ this.notes = [...notes].sort((a, b) => a.startStep - b.startStep);
1893
+ if (tempo !== void 0) this.tempo = tempo;
1894
+ if (volume !== void 0) this.volume = volume;
1895
+ const result = this.generateMML();
1896
+ this.notes = savedNotes;
1897
+ this.tempo = savedTempo;
1898
+ this.volume = savedVolume;
1899
+ return result;
1900
+ }
1901
+ /**
1902
+ * MMLの音長文字列("4", "4.", "12"など)をステップ数に変換する
1903
+ */
1904
+ getStepFromDottedMML(durStr) {
1905
+ const config = getRenderConfig();
1906
+ const total = config.stepsPerBar;
1907
+ const isDotted = durStr.endsWith(".");
1908
+ const baseDur = parseInt(isDotted ? durStr.slice(0, -1) : durStr, 10);
1909
+ const baseStep = total / baseDur;
1910
+ return isDotted ? baseStep * 1.5 : baseStep;
1911
+ }
1912
+ };
1913
+ var decomposeToMonophonic = (notes) => {
1914
+ const sorted = [...notes].sort(
1915
+ (a, b) => a.startStep - b.startStep || a.pitch - b.pitch
1916
+ );
1917
+ const tracks = [];
1918
+ const trackEnds = [];
1919
+ for (const note of sorted) {
1920
+ let assigned = -1;
1921
+ let minEnd = Infinity;
1922
+ for (let i = 0; i < tracks.length; i++) {
1923
+ if (trackEnds[i] <= note.startStep && trackEnds[i] < minEnd) {
1924
+ minEnd = trackEnds[i];
1925
+ assigned = i;
1926
+ }
1927
+ }
1928
+ if (assigned === -1) {
1929
+ tracks.push([note]);
1930
+ trackEnds.push(note.startStep + note.durationSteps);
1931
+ } else {
1932
+ tracks[assigned].push(note);
1933
+ trackEnds[assigned] = note.startStep + note.durationSteps;
1934
+ }
1935
+ }
1936
+ return tracks;
1937
+ };
1938
+ var isChordHeavyTrack = (notes, threshold = 0.6) => {
1939
+ if (notes.length < 3) return false;
1940
+ const stepCounts = /* @__PURE__ */ new Map();
1941
+ for (const n of notes) {
1942
+ stepCounts.set(n.startStep, (stepCounts.get(n.startStep) ?? 0) + 1);
1943
+ }
1944
+ const chordNotes = notes.filter(
1945
+ (n) => (stepCounts.get(n.startStep) ?? 0) >= 3
1946
+ ).length;
1947
+ return chordNotes / notes.length >= threshold;
1948
+ };
1949
+
1950
+ // src/mml-parser.ts
1951
+ var PITCH_MAP2 = {
1952
+ c: 0,
1953
+ d: 2,
1954
+ e: 4,
1955
+ f: 5,
1956
+ g: 7,
1957
+ a: 9,
1958
+ b: 11
1959
+ };
1960
+ var TRACK_INDEX_COUNT = 4;
1961
+ var parseMML = (mml, options = {}) => {
1962
+ const stepsPerBar = options.stepsPerBar ?? 192;
1963
+ const placements = [];
1964
+ let bpm = null;
1965
+ if (!mml) return { placements, bpm };
1966
+ const fullMML = mml.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "").replace(/[\n\r]+/g, " ").trim();
1967
+ const parts = fullMML.split(/(@\d+)/).filter((p) => p.trim().length > 0);
1968
+ let trackIndex = 0;
1969
+ let octave = 4;
1970
+ let currentStep = 0;
1971
+ let baseLength = 16;
1972
+ for (const rawPart of parts) {
1973
+ const part = rawPart.trim();
1974
+ if (part.startsWith("@")) {
1975
+ let idx = Number.parseInt(part.substring(1), 10);
1976
+ if (idx >= TRACK_INDEX_COUNT) idx = 2;
1977
+ trackIndex = idx;
1978
+ octave = 4;
1979
+ currentStep = 0;
1980
+ baseLength = 16;
1981
+ continue;
1982
+ }
1983
+ const body = part.replace(/\s+/g, "").toLowerCase();
1984
+ let j = 0;
1985
+ const parseLength = () => {
1986
+ let numStr = "";
1987
+ while (j < body.length && /\d/.test(body[j])) {
1988
+ numStr += body[j];
1989
+ j++;
1990
+ }
1991
+ const len = numStr ? Number.parseInt(numStr, 10) : baseLength;
1992
+ let steps = Math.round(stepsPerBar / len);
1993
+ while (j < body.length && body[j] === ".") {
1994
+ steps = Math.round(steps * 1.5);
1995
+ j++;
1996
+ }
1997
+ return steps;
1998
+ };
1999
+ while (j < body.length) {
2000
+ const ch = body[j];
2001
+ if (ch === "o") {
2002
+ j++;
2003
+ let numStr = "";
2004
+ while (j < body.length && /\d/.test(body[j])) {
2005
+ numStr += body[j];
2006
+ j++;
2007
+ }
2008
+ octave = Number.parseInt(numStr, 10) || 4;
2009
+ } else if (ch === ">") {
2010
+ octave++;
2011
+ j++;
2012
+ } else if (ch === "<") {
2013
+ octave--;
2014
+ j++;
2015
+ } else if (ch === "l") {
2016
+ j++;
2017
+ let numStr = "";
2018
+ while (j < body.length && /\d/.test(body[j])) {
2019
+ numStr += body[j];
2020
+ j++;
2021
+ }
2022
+ baseLength = Number.parseInt(numStr, 10) || 16;
2023
+ } else if (ch === "r") {
2024
+ j++;
2025
+ currentStep += parseLength();
2026
+ } else if (ch === "t" || ch === "v" || ch === "q") {
2027
+ j++;
2028
+ let numStr = "";
2029
+ while (j < body.length && /\d/.test(body[j])) {
2030
+ numStr += body[j];
2031
+ j++;
2032
+ }
2033
+ if (ch === "t" && trackIndex === 0 && numStr) {
2034
+ bpm = Number.parseInt(numStr, 10);
2035
+ }
2036
+ } else if (ch === "[") {
2037
+ j++;
2038
+ const chordNotes = [];
2039
+ const savedOctave = octave;
2040
+ while (j < body.length && body[j] !== "]") {
2041
+ const c = body[j];
2042
+ if (Object.hasOwn(PITCH_MAP2, c)) {
2043
+ let pitch = PITCH_MAP2[c];
2044
+ j++;
2045
+ if (j < body.length && (body[j] === "#" || body[j] === "+")) {
2046
+ pitch++;
2047
+ j++;
2048
+ } else if (j < body.length && body[j] === "-") {
2049
+ pitch--;
2050
+ j++;
2051
+ }
2052
+ chordNotes.push((octave + 1) * 12 + pitch);
2053
+ } else if (c === ">") {
2054
+ octave++;
2055
+ j++;
2056
+ } else if (c === "<") {
2057
+ octave--;
2058
+ j++;
2059
+ } else if (c === "o") {
2060
+ j++;
2061
+ let numStr = "";
2062
+ while (j < body.length && /\d/.test(body[j])) {
2063
+ numStr += body[j];
2064
+ j++;
2065
+ }
2066
+ octave = Number.parseInt(numStr, 10) || 4;
2067
+ } else {
2068
+ j++;
2069
+ }
2070
+ }
2071
+ if (j < body.length && body[j] === "]") j++;
2072
+ const steps = parseLength();
2073
+ for (const p of chordNotes) {
2074
+ placements.push({
2075
+ trackIndex,
2076
+ startStep: currentStep,
2077
+ pitch: p,
2078
+ durationSteps: Math.max(1, steps)
2079
+ });
2080
+ }
2081
+ currentStep += steps;
2082
+ octave = savedOctave;
2083
+ } else if (Object.hasOwn(PITCH_MAP2, ch)) {
2084
+ let pitch = PITCH_MAP2[ch];
2085
+ j++;
2086
+ if (j < body.length && (body[j] === "#" || body[j] === "+")) {
2087
+ pitch++;
2088
+ j++;
2089
+ } else if (j < body.length && body[j] === "-") {
2090
+ pitch--;
2091
+ j++;
2092
+ }
2093
+ const midiPitch = (octave + 1) * 12 + pitch;
2094
+ const steps = parseLength();
2095
+ placements.push({
2096
+ trackIndex,
2097
+ startStep: currentStep,
2098
+ pitch: midiPitch,
2099
+ durationSteps: Math.max(1, steps)
2100
+ });
2101
+ currentStep += steps;
2102
+ } else {
2103
+ j++;
2104
+ }
2105
+ }
2106
+ }
2107
+ return { placements, bpm };
2108
+ };
2109
+
2110
+ // src/sequencer.ts
2111
+ var STEPS_PER_BEAT2 = 48;
2112
+ var PLAN_TIME = 0.1;
2113
+ var TICK_INTERVAL_MS = 20;
2114
+ var createSequencer = (options) => {
2115
+ let timeline = [];
2116
+ let startTime = 0;
2117
+ let nowIndex = 0;
2118
+ let intervalId = null;
2119
+ let animationId = null;
2120
+ let active = false;
2121
+ let fromStepValue = 0;
2122
+ const secondsPerStep = () => 60 / options.getBpm() / STEPS_PER_BEAT2;
2123
+ const buildTimeline = (fromStep) => {
2124
+ timeline = [];
2125
+ const sps = secondsPerStep();
2126
+ for (const track of options.getTracks()) {
2127
+ for (const note of track.notes) {
2128
+ const relativeStart = note.startStep - fromStep;
2129
+ if (relativeStart < 0) continue;
2130
+ const velocity = note.velocity ?? 127;
2131
+ timeline.push({
2132
+ trackId: track.id,
2133
+ pitch: note.pitch,
2134
+ volume: track.volume / 100,
2135
+ velocity,
2136
+ when: relativeStart * sps,
2137
+ duration: note.durationSteps * sps
2138
+ });
2139
+ }
2140
+ }
2141
+ timeline.sort((a, b) => a.when - b.when);
2142
+ };
2143
+ const scheduleTick = () => {
2144
+ const sps = secondsPerStep();
2145
+ const time = options.getAudioTime() - startTime;
2146
+ const soloId = options.getSoloTrackId();
2147
+ while (nowIndex < timeline.length) {
2148
+ const ev = timeline[nowIndex];
2149
+ if (soloId && ev.trackId !== soloId) {
2150
+ nowIndex++;
2151
+ continue;
2152
+ }
2153
+ const _when = ev.when - time;
2154
+ if (_when > PLAN_TIME) break;
2155
+ nowIndex++;
2156
+ const velocityVolume = ev.velocity / 127;
2157
+ options.onPlayNote({
2158
+ trackId: ev.trackId,
2159
+ pitch: ev.pitch,
2160
+ velocity: ev.velocity,
2161
+ volume: ev.volume * velocityVolume,
2162
+ when: Math.max(0, _when),
2163
+ duration: ev.duration
2164
+ });
2165
+ }
2166
+ const pattern = options.getDrumPattern();
2167
+ if (pattern && pattern.length > 0) {
2168
+ const { stepsPerBar } = options;
2169
+ const currentStep = (fromStepValue * sps + (options.getAudioTime() - startTime)) / sps;
2170
+ const currentStepInBar = currentStep % stepsPerBar;
2171
+ const nextStep = currentStepInBar + 4;
2172
+ const crossedBar = currentStepInBar < 4;
2173
+ for (const drum of pattern) {
2174
+ const shouldPlay = crossedBar && drum.step === 0 || drum.step >= currentStepInBar && drum.step < nextStep;
2175
+ if (!shouldPlay) continue;
2176
+ const whenSeconds = (drum.step - currentStepInBar) * sps;
2177
+ if (whenSeconds < -0.1 || whenSeconds > PLAN_TIME) continue;
2178
+ options.onPlayDrum({
2179
+ pitch: drum.pitch,
2180
+ velocity: drum.velocity ?? 1,
2181
+ when: Math.max(0, whenSeconds),
2182
+ duration: 0.1
2183
+ });
2184
+ }
2185
+ }
2186
+ const last = timeline[timeline.length - 1];
2187
+ const lastWhen = last?.when ?? 0;
2188
+ const lastDuration = last?.duration ?? 0;
2189
+ if (nowIndex >= timeline.length && time > lastWhen + lastDuration + 0.1) {
2190
+ stop();
2191
+ options.onEnd();
2192
+ }
2193
+ };
2194
+ const animate = () => {
2195
+ if (!active) return;
2196
+ const sps = secondsPerStep();
2197
+ const time = options.getAudioTime() - startTime;
2198
+ options.onTick(fromStepValue + time / sps);
2199
+ animationId = requestAnimationFrame(animate);
2200
+ };
2201
+ const stop = () => {
2202
+ if (intervalId !== null) {
2203
+ clearInterval(intervalId);
2204
+ intervalId = null;
2205
+ }
2206
+ if (animationId !== null) {
2207
+ cancelAnimationFrame(animationId);
2208
+ animationId = null;
2209
+ }
2210
+ active = false;
2211
+ };
2212
+ const start = (fromStep) => {
2213
+ stop();
2214
+ fromStepValue = fromStep ?? options.getPlayStartStep();
2215
+ buildTimeline(fromStepValue);
2216
+ if (timeline.length === 0 && !options.getDrumPattern()?.length) return;
2217
+ active = true;
2218
+ startTime = options.getAudioTime();
2219
+ nowIndex = 0;
2220
+ intervalId = setInterval(scheduleTick, TICK_INTERVAL_MS);
2221
+ animationId = requestAnimationFrame(animate);
2222
+ };
2223
+ return {
2224
+ start,
2225
+ stop,
2226
+ isActive: () => active
2227
+ };
2228
+ };
2229
+
2230
+ // src/styles.ts
2231
+ var STYLE_ID = "dtm-daw-styles";
2232
+ var DAW_CSS = `
2233
+ @font-face {
2234
+ font-family: 'k8x12';
2235
+ src: url('https://db.onlinewebfonts.com/t/777630d46640dc5a928ea833c2fcb875.woff2') format('woff2'),
2236
+ url('https://db.onlinewebfonts.com/t/777630d46640dc5a928ea833c2fcb875.woff') format('woff'),
2237
+ url('https://db.onlinewebfonts.com/t/777630d46640dc5a928ea833c2fcb875.ttf') format('truetype');
2238
+ font-weight: normal;
2239
+ font-style: normal;
2240
+ }
2241
+
2242
+ /* ====================================================
2243
+ PIXEL MUSIC STUDIO \u2014 \u30C9\u30C3\u30C8\u7D75UI\u30B7\u30B9\u30C6\u30E0
2244
+ 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
2245
+ ==================================================== */
2246
+
2247
+ .dtm-daw {
2248
+ /* PICO-8 16\u8272\u30D1\u30EC\u30C3\u30C8\u3088\u308A */
2249
+ --c-black: #000000;
2250
+ --c-navy: #1d2b53;
2251
+ --c-purple: #7e2553;
2252
+ --c-dkgreen: #008751;
2253
+ --c-brown: #ab5236;
2254
+ --c-dkgray: #5f574f;
2255
+ --c-gray: #c2c3c7;
2256
+ --c-white: #fff1e8;
2257
+ --c-red: #ff004d;
2258
+ --c-orange: #ffa300;
2259
+ --c-yellow: #ffec27;
2260
+ --c-green: #00e436;
2261
+ --c-cyan: #29adff;
2262
+ --c-lavend: #83769c;
2263
+ --c-pink: #ff77a8;
2264
+ --c-peach: #ffccaa;
2265
+
2266
+ /* \u30BB\u30DE\u30F3\u30C6\u30A3\u30C3\u30AF\u30C8\u30FC\u30AF\u30F3 */
2267
+ --dtm-bg: var(--c-black);
2268
+ --dtm-surface: var(--c-navy);
2269
+ --dtm-deep: #0a0f1f;
2270
+ --dtm-border: var(--c-cyan);
2271
+ --dtm-border2: var(--c-dkgray);
2272
+ --dtm-text: var(--c-white);
2273
+ --dtm-muted: var(--c-lavend);
2274
+ --dtm-primary: var(--c-cyan);
2275
+ --dtm-pfg: var(--c-black);
2276
+ --dtm-danger: var(--c-red);
2277
+ --dtm-success: var(--c-green);
2278
+ --dtm-accent: var(--c-pink);
2279
+ --dtm-gold: var(--c-yellow);
2280
+ --dtm-warn: var(--c-orange);
2281
+ --dtm-tap: 40px;
2282
+ --dtm-gap: 6px;
2283
+ --dtm-font: 'k8x12',ui-monospace,monospace;
2284
+
2285
+ box-sizing: border-box;
2286
+ font-family: var(--dtm-font);
2287
+ font-size: 14px;
2288
+ line-height: 1.6;
2289
+ letter-spacing: .06em;
2290
+ color: var(--dtm-text);
2291
+ background: var(--dtm-bg);
2292
+ width: 100%;
2293
+ display: flex;
2294
+ flex-direction: column;
2295
+ gap: var(--dtm-gap);
2296
+ padding: 6px;
2297
+ image-rendering: pixelated;
2298
+ -webkit-font-smoothing: none;
2299
+ -moz-osx-font-smoothing: unset;
2300
+ font-smooth: never;
2301
+ -webkit-tap-highlight-color: transparent;
2302
+ }
2303
+ .dtm-daw *,
2304
+ .dtm-daw *::before,
2305
+ .dtm-daw *::after { box-sizing: border-box; }
2306
+
2307
+ /* \u2500\u2500\u2500 \u30B2\u30FC\u30E0\u30A6\u30A3\u30F3\u30C9\u30A6\u5171\u901A\u67A0 \u2500\u2500\u2500 */
2308
+ /* \u5916\u67A0(\u9ED22px) \u2192 \u8272\u4ED8\u304D2px border \u2192 \u5185\u67A0(\u9ED2inset2px) \u306E3\u91CD\u69CB\u9020 */
2309
+ .dtm-win {
2310
+ border: 2px solid var(--c-black);
2311
+ box-shadow:
2312
+ inset 0 0 0 2px var(--c-black),
2313
+ 0 0 0 2px var(--dtm-primary),
2314
+ 4px 4px 0 var(--c-black);
2315
+ background: var(--dtm-surface);
2316
+ }
2317
+
2318
+ /* \u2500\u2500\u2500 \u5171\u901A\u30DC\u30BF\u30F3 \u2500\u2500\u2500 */
2319
+ .dtm-btn {
2320
+ display: inline-flex;
2321
+ align-items: center;
2322
+ justify-content: center;
2323
+ gap: 4px;
2324
+ min-height: var(--dtm-tap);
2325
+ min-width: var(--dtm-tap);
2326
+ padding: 0 10px;
2327
+ border: 2px solid var(--dtm-border2);
2328
+ background: var(--dtm-surface);
2329
+ color: var(--dtm-text);
2330
+ font-family: var(--dtm-font);
2331
+ font-size: 13px;
2332
+ text-transform: uppercase;
2333
+ letter-spacing: .12em;
2334
+ cursor: pointer;
2335
+ user-select: none;
2336
+ white-space: nowrap;
2337
+ box-shadow: 3px 3px 0 var(--c-black);
2338
+ transition: none;
2339
+ }
2340
+ .dtm-btn:active { transform: translate(3px,3px); box-shadow: none; }
2341
+ .dtm-btn:disabled { opacity: .3; cursor: default; box-shadow: none; }
2342
+ .dtm-btn--primary { border-color: var(--dtm-primary); background: var(--dtm-primary); color: var(--dtm-pfg); }
2343
+ .dtm-btn--success { border-color: var(--dtm-success); background: var(--dtm-success); color: var(--c-black); }
2344
+ .dtm-btn--danger { border-color: var(--dtm-danger); background: var(--dtm-danger); color: var(--c-white); }
2345
+ .dtm-btn--accent { border-color: var(--dtm-accent); background: var(--dtm-accent); color: var(--c-black); }
2346
+ .dtm-btn--ghost { background: transparent; border-color: var(--dtm-border2); }
2347
+ .dtm-btn--icon { padding: 0; }
2348
+
2349
+ /* \u2500\u2500\u2500 \u30A2\u30A4\u30B3\u30F3\u30DC\u30BF\u30F3 \u2500\u2500\u2500 */
2350
+ .dtm-iconbtn {
2351
+ display: inline-flex;
2352
+ align-items: center;
2353
+ justify-content: center;
2354
+ width: var(--dtm-tap);
2355
+ height: var(--dtm-tap);
2356
+ flex: 0 0 auto;
2357
+ border: 2px solid var(--dtm-border2);
2358
+ background: var(--dtm-surface);
2359
+ color: var(--dtm-text);
2360
+ font-size: 16px;
2361
+ cursor: pointer;
2362
+ box-shadow: 3px 3px 0 var(--c-black);
2363
+ }
2364
+ .dtm-iconbtn:active { transform: translate(3px,3px); box-shadow: none; }
2365
+ .dtm-iconbtn:disabled { opacity: .3; cursor: default; box-shadow: none; }
2366
+
2367
+ /* \u2500\u2500\u2500 \u30C8\u30E9\u30F3\u30B9\u30DD\u30FC\u30C8\u30D0\u30FC\uFF08HUD\u30B9\u30BF\u30A4\u30EB\uFF09 \u2500\u2500\u2500 */
2368
+ .dtm-topbar {
2369
+ position: sticky;
2370
+ top: 0;
2371
+ z-index: 20;
2372
+ display: flex;
2373
+ flex-wrap: wrap;
2374
+ align-items: center;
2375
+ gap: var(--dtm-gap);
2376
+ padding: 6px;
2377
+ background: var(--dtm-deep);
2378
+ border: 2px solid var(--c-black);
2379
+ box-shadow:
2380
+ inset 0 0 0 2px var(--c-black),
2381
+ 0 0 0 2px var(--dtm-success),
2382
+ 4px 4px 0 var(--c-black);
2383
+ }
2384
+
2385
+ /* PLAY\u30DC\u30BF\u30F3 \u2014 \u30B2\u30FC\u30E0\u306E\u300C\u6C7A\u5B9A\u30DC\u30BF\u30F3\u300D\u7684\u5B58\u5728\u611F */
2386
+ .dtm-play {
2387
+ display: inline-flex;
2388
+ align-items: center;
2389
+ justify-content: center;
2390
+ gap: 6px;
2391
+ min-height: 44px;
2392
+ padding: 0 20px;
2393
+ border: 2px solid var(--c-black);
2394
+ background: var(--dtm-success);
2395
+ color: var(--c-black);
2396
+ font-family: var(--dtm-font);
2397
+ font-size: 14px;
2398
+ text-transform: uppercase;
2399
+ letter-spacing: .2em;
2400
+ cursor: pointer;
2401
+ box-shadow: 0 0 0 2px var(--dtm-success), 4px 4px 0 var(--c-black);
2402
+ }
2403
+ .dtm-play:active { transform: translate(4px,4px); box-shadow: none; }
2404
+ .dtm-play:disabled { opacity: .35; cursor: default; box-shadow: none; }
2405
+ .dtm-play--stop {
2406
+ background: var(--dtm-danger);
2407
+ box-shadow: 0 0 0 2px var(--dtm-danger), 4px 4px 0 var(--c-black);
2408
+ color: var(--c-white);
2409
+ }
2410
+ .dtm-rec { color: var(--dtm-danger); }
2411
+
2412
+ /* BPM \u2014 \u30C7\u30B8\u30BF\u30EB\u30AB\u30A6\u30F3\u30BF\u30FC\u98A8 */
2413
+ .dtm-label {
2414
+ font-family: var(--dtm-font);
2415
+ font-size: 11px;
2416
+ text-transform: uppercase;
2417
+ letter-spacing: .14em;
2418
+ color: var(--dtm-muted);
2419
+ white-space: nowrap;
2420
+ }
2421
+ .dtm-checkbox-label {
2422
+ display: inline-flex;
2423
+ align-items: center;
2424
+ gap: 6px;
2425
+ font-family: var(--dtm-font);
2426
+ font-size: 11px;
2427
+ text-transform: uppercase;
2428
+ letter-spacing: .12em;
2429
+ color: var(--dtm-muted);
2430
+ cursor: pointer;
2431
+ user-select: none;
2432
+ margin-top: 4px;
2433
+ }
2434
+ .dtm-checkbox-label:hover { color: var(--dtm-text); }
2435
+ .dtm-checkbox-label--sub { margin-left: 20px; font-size: 10px; }
2436
+ .dtm-checkbox {
2437
+ width: 14px;
2438
+ height: 14px;
2439
+ accent-color: var(--dtm-success);
2440
+ cursor: pointer;
2441
+ flex-shrink: 0;
2442
+ }
2443
+
2444
+ .dtm-toggle {
2445
+ display: inline-flex;
2446
+ align-items: center;
2447
+ gap: 6px;
2448
+ font-family: var(--dtm-font);
2449
+ font-size: 12px;
2450
+ text-transform: uppercase;
2451
+ letter-spacing: .1em;
2452
+ color: var(--dtm-muted);
2453
+ cursor: pointer;
2454
+ }
2455
+ .dtm-toggle input { width: 16px; height: 16px; accent-color: var(--dtm-accent); }
2456
+
2457
+ /* \u2500\u2500\u2500 \u30C4\u30FC\u30EB\u30C9\u30C3\u30AF\uFF08\u88C5\u5099\u30B9\u30ED\u30C3\u30C8\u98A8\uFF09 \u2500\u2500\u2500 */
2458
+ .dtm-tooldock {
2459
+ display: flex;
2460
+ flex-wrap: wrap;
2461
+ align-items: center;
2462
+ gap: var(--dtm-gap);
2463
+ padding: 6px;
2464
+ background: var(--dtm-deep);
2465
+ border: 2px solid var(--c-black);
2466
+ box-shadow:
2467
+ inset 0 0 0 2px var(--c-black),
2468
+ 0 0 0 2px var(--dtm-border2),
2469
+ 4px 4px 0 var(--c-black);
2470
+ }
2471
+ .dtm-sep {
2472
+ width: 2px; align-self: stretch;
2473
+ background: var(--dtm-border2); margin: 2px;
2474
+ }
2475
+ .dtm-row .dtm-label[data-dtm] { min-width: 48px; text-align: center; }
2476
+
2477
+ /* \u2500\u2500\u2500 \u30BB\u30B0\u30E1\u30F3\u30C8\uFF08\u30A2\u30A4\u30C6\u30E0\u30B9\u30ED\u30C3\u30C8\uFF09 \u2500\u2500\u2500 */
2478
+ .dtm-seg {
2479
+ display: inline-flex;
2480
+ border: 2px solid var(--dtm-border2);
2481
+ background: var(--dtm-deep);
2482
+ box-shadow: 3px 3px 0 var(--c-black);
2483
+ }
2484
+ .dtm-segbtn {
2485
+ display: inline-flex;
2486
+ align-items: center;
2487
+ justify-content: center;
2488
+ width: var(--dtm-tap);
2489
+ height: var(--dtm-tap);
2490
+ border: none;
2491
+ border-right: 2px solid var(--dtm-border2);
2492
+ background: transparent;
2493
+ color: var(--dtm-muted);
2494
+ cursor: pointer;
2495
+ }
2496
+ .dtm-segbtn:last-child { border-right: none; }
2497
+ .dtm-segbtn--active {
2498
+ background: var(--dtm-gold);
2499
+ color: var(--c-black);
2500
+ }
2501
+ .dtm-segbtn:not(.dtm-segbtn--active):active { background: var(--dtm-border2); }
2502
+
2503
+ /* \u2500\u2500\u2500 \u30D5\u30A9\u30FC\u30E0\u8981\u7D20 \u2500\u2500\u2500 */
2504
+ .dtm-select, .dtm-input, .dtm-textarea {
2505
+ min-height: var(--dtm-tap);
2506
+ padding: 4px 8px;
2507
+ border: 2px solid var(--dtm-border2);
2508
+ background: var(--dtm-deep);
2509
+ color: var(--dtm-text);
2510
+ font-family: var(--dtm-font);
2511
+ font-size: 13px;
2512
+ letter-spacing: .06em;
2513
+ box-shadow: inset 2px 2px 0 var(--c-black);
2514
+ }
2515
+ .dtm-select:focus, .dtm-input:focus, .dtm-textarea:focus {
2516
+ outline: none;
2517
+ border-color: var(--dtm-primary);
2518
+ }
2519
+ .dtm-input--num { width: 64px; text-align: center; font-size: 16px; }
2520
+ .dtm-textarea { width: 100%; min-height: 56px; resize: vertical; line-height: 1.7; }
2521
+ .dtm-range { height: var(--dtm-tap); accent-color: var(--dtm-primary); }
2522
+
2523
+ /* \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 */
2524
+ .dtm-tracks {
2525
+ display: flex;
2526
+ flex-wrap: wrap;
2527
+ gap: var(--dtm-gap);
2528
+ }
2529
+ .dtm-pill {
2530
+ --dtm-pill-color: var(--dtm-primary);
2531
+ display: inline-flex;
2532
+ align-items: center;
2533
+ gap: 8px;
2534
+ flex: 1 1 auto;
2535
+ justify-content: center;
2536
+ min-height: 42px;
2537
+ padding: 0 12px;
2538
+ border: 2px solid var(--dtm-border2);
2539
+ background: var(--dtm-deep);
2540
+ color: var(--dtm-muted);
2541
+ font-family: var(--dtm-font);
2542
+ font-size: 13px;
2543
+ text-transform: uppercase;
2544
+ letter-spacing: .1em;
2545
+ cursor: pointer;
2546
+ box-shadow: 3px 3px 0 var(--c-black);
2547
+ }
2548
+ .dtm-pill .dtm-dot {
2549
+ width: 8px; height: 8px;
2550
+ background: var(--dtm-pill-color);
2551
+ flex: 0 0 auto;
2552
+ box-shadow: 1px 1px 0 var(--c-black);
2553
+ }
2554
+ /* \u30A2\u30AF\u30C6\u30A3\u30D6\u9078\u629E = \u91D1\u8272\u30CF\u30A4\u30E9\u30A4\u30C8 + \u30AB\u30FC\u30BD\u30EB */
2555
+ .dtm-pill--active {
2556
+ border-color: var(--dtm-gold);
2557
+ color: var(--dtm-gold);
2558
+ background: var(--dtm-surface);
2559
+ box-shadow: 0 0 0 2px var(--dtm-gold), 3px 3px 0 var(--c-black);
2560
+ }
2561
+ .dtm-pill--active::before { content: "\u25BA "; font-size: 10px; }
2562
+ .dtm-pill:not(.dtm-pill--active):active { transform: translate(3px,3px); box-shadow: none; }
2563
+
2564
+ /* \u2500\u2500\u2500 \u30D4\u30A2\u30CE\u30ED\u30FC\u30EB\uFF08\u30C8\u30E9\u30C3\u30AB\u30FC\u98A8\uFF09 \u2500\u2500\u2500 */
2565
+ .dtm-roll-wrap { display: flex; gap: var(--dtm-gap); }
2566
+ .dtm-roll {
2567
+ position: relative;
2568
+ flex: 1 1 auto;
2569
+ height: 56vh;
2570
+ min-height: 280px;
2571
+ background: var(--dtm-deep);
2572
+ border: 2px solid var(--c-black);
2573
+ box-shadow:
2574
+ inset 0 0 0 2px var(--c-black),
2575
+ 0 0 0 2px var(--dtm-border2),
2576
+ 4px 4px 0 var(--c-black);
2577
+ overflow: hidden;
2578
+ }
2579
+ .dtm-vscroll {
2580
+ position: relative;
2581
+ width: 20px;
2582
+ background: var(--dtm-deep);
2583
+ border: 2px solid var(--dtm-border2);
2584
+ cursor: pointer;
2585
+ flex: 0 0 auto;
2586
+ touch-action: none;
2587
+ }
2588
+ .dtm-vscroll-thumb, .dtm-hscroll-thumb {
2589
+ position: absolute;
2590
+ background: var(--dtm-primary);
2591
+ min-width: 20px;
2592
+ min-height: 20px;
2593
+ }
2594
+ .dtm-vscroll-thumb { left: 0; width: 100%; }
2595
+ .dtm-hscroll {
2596
+ position: relative;
2597
+ width: 100%; height: 20px;
2598
+ background: var(--dtm-deep);
2599
+ border: 2px solid var(--dtm-border2);
2600
+ cursor: pointer;
2601
+ touch-action: none;
2602
+ }
2603
+ .dtm-hscroll-thumb { top: 0; height: 100%; }
2604
+
2605
+ /* \u2500\u2500\u2500 \u30D1\u30CD\u30EB\uFF08RPG\u30C0\u30A4\u30A2\u30ED\u30B0\u30A6\u30A3\u30F3\u30C9\u30A6\uFF09 \u2500\u2500\u2500 */
2606
+ .dtm-panel {
2607
+ background: var(--dtm-surface);
2608
+ border: 2px solid var(--c-black);
2609
+ box-shadow:
2610
+ inset 0 0 0 2px var(--c-black),
2611
+ 0 0 0 2px var(--dtm-primary),
2612
+ 4px 4px 0 var(--c-black);
2613
+ overflow: hidden;
2614
+ }
2615
+ .dtm-panel > summary {
2616
+ list-style: none;
2617
+ cursor: pointer;
2618
+ padding: 0 12px;
2619
+ font-family: var(--dtm-font);
2620
+ font-size: 12px;
2621
+ text-transform: uppercase;
2622
+ letter-spacing: .14em;
2623
+ display: flex;
2624
+ align-items: center;
2625
+ min-height: var(--dtm-tap);
2626
+ background: var(--dtm-deep);
2627
+ border-bottom: 2px solid var(--dtm-border2);
2628
+ color: var(--dtm-primary);
2629
+ gap: 8px;
2630
+ }
2631
+ .dtm-panel:not([open]) > summary { border-bottom: none; }
2632
+ .dtm-panel > summary::-webkit-details-marker { display: none; }
2633
+ /* \u5DE6\u7AEF\u30E9\u30A4\u30F3\uFF08\u30B2\u30FC\u30E0UI\u306E\u30BB\u30AF\u30B7\u30E7\u30F3\u8272\u5206\u3051\uFF09 */
2634
+ .dtm-panel > summary::before {
2635
+ content: '';
2636
+ display: block;
2637
+ width: 4px;
2638
+ height: 20px;
2639
+ background: var(--dtm-accent);
2640
+ flex: 0 0 auto;
2641
+ }
2642
+ .dtm-panel[open] > summary::before { background: var(--dtm-primary); }
2643
+ /* \u6298\u308A\u305F\u305F\u307F\u77E2\u5370 */
2644
+ .dtm-panel > summary::after {
2645
+ content: "\u25B6";
2646
+ margin-left: auto;
2647
+ color: var(--dtm-muted);
2648
+ font-size: 10px;
2649
+ }
2650
+ .dtm-panel[open] > summary::after { content: "\u25BC"; }
2651
+ .dtm-panel-body { padding: 10px 12px 12px; display: flex; flex-direction: column; gap: 10px; }
2652
+ .dtm-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
2653
+ .dtm-track-body { display: flex; flex-direction: column; gap: 10px; }
2654
+
2655
+ /* \u2500\u2500\u2500 MML\u51FA\u529B\uFF08CRT\u30BF\u30FC\u30DF\u30CA\u30EB\uFF09 \u2500\u2500\u2500 */
2656
+ .dtm-output {
2657
+ background: var(--c-black);
2658
+ color: var(--dtm-success);
2659
+ border: 2px solid var(--dtm-success);
2660
+ padding: 10px;
2661
+ box-shadow: 0 0 0 2px var(--c-black), 4px 4px 0 var(--c-black);
2662
+ }
2663
+ .dtm-output::before {
2664
+ content: "C:\\> MML OUTPUT";
2665
+ display: block;
2666
+ font-size: 11px;
2667
+ color: var(--dtm-muted);
2668
+ letter-spacing: .14em;
2669
+ margin-bottom: 6px;
2670
+ padding-bottom: 6px;
2671
+ border-bottom: 1px solid var(--dtm-border2);
2672
+ }
2673
+ .dtm-output pre {
2674
+ margin: 0;
2675
+ background: transparent;
2676
+ padding: 0;
2677
+ overflow-x: auto;
2678
+ font-family: var(--dtm-font);
2679
+ font-size: 12px;
2680
+ line-height: 1.8;
2681
+ color: var(--dtm-success);
2682
+ }
2683
+ .dtm-output-row { display: flex; gap: 8px; align-items: flex-start; margin-top: 6px; }
2684
+ .dtm-output-row pre { flex: 1; }
2685
+
2686
+ /* \u2500\u2500\u2500 \u30ED\u30FC\u30C7\u30A3\u30F3\u30B0\u30AA\u30FC\u30D0\u30FC\u30EC\u30A4 \u2500\u2500\u2500 */
2687
+ .dtm-overlay {
2688
+ position: fixed; inset: 0; z-index: 1000;
2689
+ background: rgba(0,0,0,.92);
2690
+ display: flex; align-items: center; justify-content: center;
2691
+ flex-direction: column; gap: 14px;
2692
+ }
2693
+ .dtm-overlay[hidden] { display: none; }
2694
+ .dtm-overlay::before {
2695
+ content: 'NOW LOADING';
2696
+ font-family: var(--dtm-font);
2697
+ font-size: 13px;
2698
+ color: var(--dtm-primary);
2699
+ text-transform: uppercase;
2700
+ letter-spacing: .25em;
2701
+ animation: dtm-blink 1s steps(1) infinite;
2702
+ }
2703
+ /* 8\u30D6\u30ED\u30C3\u30AF\u523B\u307F\u3067\u57CB\u307E\u308B\u30D4\u30AF\u30BB\u30EB\u30D0\u30FC */
2704
+ .dtm-spinner {
2705
+ width: 96px; height: 12px;
2706
+ position: relative;
2707
+ background: var(--c-navy);
2708
+ border: 2px solid var(--dtm-primary);
2709
+ box-shadow: 0 0 0 2px var(--c-black), 4px 4px 0 var(--c-black);
2710
+ }
2711
+ .dtm-spinner::after {
2712
+ content: '';
2713
+ position: absolute;
2714
+ left: 0; top: 0; height: 100%;
2715
+ background: var(--dtm-primary);
2716
+ animation: dtm-load 1.6s steps(8) infinite;
2717
+ }
2718
+ @keyframes dtm-load { 0%{width:0} 100%{width:100%} }
2719
+
2720
+ @keyframes dtm-blink { 0%,100%{opacity:1} 50%{opacity:0} }
2721
+ .dtm-blink { animation: dtm-blink 1s steps(1) infinite; }
2722
+
2723
+ .dtm-hidden { display: none !important; }
2724
+ .dtm-grow { flex: 1 1 auto; }
2725
+
2726
+ /* \u2500\u2500\u2500 \u5E83\u5E45\u62E1\u5F35 \u2500\u2500\u2500 */
2727
+ @media (min-width: 768px) {
2728
+ .dtm-daw { gap: 8px; padding: 10px; }
2729
+ .dtm-roll { height: 420px; }
2730
+ }
2731
+ `;
2732
+ var injectStyles = (doc = document) => {
2733
+ if (doc.getElementById(STYLE_ID)) return;
2734
+ const style = doc.createElement("style");
2735
+ style.id = STYLE_ID;
2736
+ style.textContent = DAW_CSS;
2737
+ doc.head.appendChild(style);
2738
+ };
2739
+
2740
+ // src/daw.ts
2741
+ var BASE_STEP_WIDTH = 0.5;
2742
+ var BASE_KEY_HEIGHT = 15;
2743
+ var TRACKS_SIMPLE = [
2744
+ {
2745
+ id: "melody",
2746
+ name: "\u30E1\u30ED\u30C7\u30A3\u30FC",
2747
+ color: [41, 173, 255],
2748
+ instrument: 0,
2749
+ volume: 100
2750
+ },
2751
+ {
2752
+ id: "submelody",
2753
+ name: "\u30B5\u30D6\u30E1\u30ED",
2754
+ color: [255, 119, 168],
2755
+ instrument: 1,
2756
+ volume: 95
2757
+ },
2758
+ {
2759
+ id: "bass",
2760
+ name: "\u30D9\u30FC\u30B9",
2761
+ color: [0, 228, 54],
2762
+ instrument: 2,
2763
+ volume: 88
2764
+ },
2765
+ {
2766
+ id: "chord",
2767
+ name: "\u4F34\u594F",
2768
+ color: [255, 163, 0],
2769
+ instrument: 3,
2770
+ volume: 76
2771
+ }
2772
+ ];
2773
+ var TRACKS_ADVANCED = [
2774
+ {
2775
+ id: "t0",
2776
+ name: "TRACK 01",
2777
+ color: [41, 173, 255],
2778
+ instrument: 0,
2779
+ volume: 100
2780
+ },
2781
+ {
2782
+ id: "t1",
2783
+ name: "TRACK 02",
2784
+ color: [0, 228, 54],
2785
+ instrument: 1,
2786
+ volume: 100
2787
+ },
2788
+ {
2789
+ id: "t2",
2790
+ name: "TRACK 03",
2791
+ color: [255, 119, 168],
2792
+ instrument: 2,
2793
+ volume: 100
2794
+ },
2795
+ {
2796
+ id: "t3",
2797
+ name: "TRACK 04",
2798
+ color: [255, 163, 0],
2799
+ instrument: 3,
2800
+ volume: 100
2801
+ },
2802
+ {
2803
+ id: "t4",
2804
+ name: "TRACK 05",
2805
+ color: [255, 236, 39],
2806
+ instrument: 4,
2807
+ volume: 100
2808
+ },
2809
+ {
2810
+ id: "t5",
2811
+ name: "TRACK 06",
2812
+ color: [131, 118, 156],
2813
+ instrument: 5,
2814
+ volume: 100
2815
+ },
2816
+ {
2817
+ id: "t6",
2818
+ name: "TRACK 07",
2819
+ color: [255, 0, 77],
2820
+ instrument: 6,
2821
+ volume: 100
2822
+ },
2823
+ {
2824
+ id: "t7",
2825
+ name: "TRACK 08",
2826
+ color: [255, 204, 170],
2827
+ instrument: 7,
2828
+ volume: 100
2829
+ },
2830
+ {
2831
+ id: "t8",
2832
+ name: "TRACK 09",
2833
+ color: [194, 195, 199],
2834
+ instrument: 8,
2835
+ volume: 100
2836
+ },
2837
+ {
2838
+ id: "t9",
2839
+ name: "TRACK 10",
2840
+ color: [0, 135, 81],
2841
+ instrument: 9,
2842
+ volume: 100
2843
+ },
2844
+ {
2845
+ id: "t10",
2846
+ name: "TRACK 11",
2847
+ color: [171, 82, 54],
2848
+ instrument: 10,
2849
+ volume: 100
2850
+ },
2851
+ {
2852
+ id: "t11",
2853
+ name: "TRACK 12",
2854
+ color: [126, 37, 83],
2855
+ instrument: 11,
2856
+ volume: 100
2857
+ },
2858
+ {
2859
+ id: "t12",
2860
+ name: "TRACK 13",
2861
+ color: [255, 241, 232],
2862
+ instrument: 12,
2863
+ volume: 100
2864
+ },
2865
+ {
2866
+ id: "t13",
2867
+ name: "TRACK 14",
2868
+ color: [120, 200, 255],
2869
+ instrument: 13,
2870
+ volume: 100
2871
+ },
2872
+ {
2873
+ id: "t14",
2874
+ name: "TRACK 15",
2875
+ color: [100, 255, 160],
2876
+ instrument: 14,
2877
+ volume: 100
2878
+ },
2879
+ {
2880
+ id: "t15",
2881
+ name: "TRACK 16",
2882
+ color: [255, 150, 200],
2883
+ instrument: 15,
2884
+ volume: 100
2885
+ }
2886
+ ];
2887
+ var DEFAULT_TRACKS = TRACKS_SIMPLE;
2888
+ var clamp = (v, min, max) => Math.min(Math.max(v, min), max);
2889
+ var mountDAW = (target, options = {}) => {
2890
+ injectStyles();
2891
+ const getAudioTime = options.getAudioTime ?? (() => performance.now() / 1e3);
2892
+ const trackConfigs = options.tracks ?? DEFAULT_TRACKS;
2893
+ const drumPatterns = options.drumPatterns ?? DRUM_PATTERNS;
2894
+ const showMidi = !!options.parseMidi;
2895
+ const showChord = !!(options.parseChord && options.parseChords);
2896
+ const refs = buildUI(target, {
2897
+ tracks: trackConfigs,
2898
+ drumPatternNames: Object.keys(drumPatterns),
2899
+ defaultDrumPattern: drumPatterns.dance ? "dance" : Object.keys(drumPatterns)[0] ?? "none",
2900
+ defaultBpm: options.defaultBpm ?? 120,
2901
+ showMidi,
2902
+ showChord
2903
+ });
2904
+ const renderConfig = {
2905
+ stepsPerBar: 192,
2906
+ keyCount: 128,
2907
+ pitchRangeStart: 0,
2908
+ keyHeight: BASE_KEY_HEIGHT,
2909
+ stepWidth: BASE_STEP_WIDTH * 2
2910
+ // zoom100% 相当
2911
+ };
2912
+ const leftPaddingSteps = renderConfig.stepsPerBar * 16;
2913
+ let zoomX = 100;
2914
+ let zoomY = 100;
2915
+ let bpm = options.defaultBpm ?? 120;
2916
+ let masterVolume = 50;
2917
+ let drumVolume = 80;
2918
+ let currentDrumPattern = refs.drumSelect.value;
2919
+ let activeTrackId = trackConfigs[0].id;
2920
+ let activeToolMode = "pen";
2921
+ let currentInsertLength = 48;
2922
+ let snapGridSteps = 12;
2923
+ const gridLineSteps = 48;
2924
+ let currentOffsetX = 0;
2925
+ let currentOffsetY = (104 - 1 - 60) * renderConfig.keyHeight - 215;
2926
+ let playStartStep = 0;
2927
+ let isSolo = false;
2928
+ let playbackState = "stopped";
2929
+ let pausedPlayStep = 0;
2930
+ let currentPlayStep = 0;
2931
+ let ready = false;
2932
+ let selectedNotes = [];
2933
+ let selectionRect = null;
2934
+ let copiedNotes = [];
2935
+ let trackStates = [];
2936
+ const createTrackStates = () => {
2937
+ trackStates = trackConfigs.map((config) => ({
2938
+ config,
2939
+ core: new MMLCore(
2940
+ {
2941
+ onMMLGenerated: () => {
2942
+ },
2943
+ onNotesChanged: () => {
2944
+ if (!ready) return;
2945
+ redrawAll();
2946
+ updateUndoRedo();
2947
+ }
2948
+ },
2949
+ config.volume
2950
+ ),
2951
+ volume: config.volume,
2952
+ savedChordInput: "",
2953
+ savedChordPattern: "block",
2954
+ savedChordRoot: 0
2955
+ }));
2956
+ };
2957
+ const getActive = () => trackStates.find((t) => t.config.id === activeTrackId) ?? trackStates[0];
2958
+ const getMaxNoteStep = () => {
2959
+ let maxStep = renderConfig.stepsPerBar * 4;
2960
+ for (const t of trackStates) {
2961
+ for (const n of t.core.getNotes()) {
2962
+ const end = n.startStep + n.durationSteps;
2963
+ if (end > maxStep) maxStep = end;
2964
+ }
2965
+ }
2966
+ return maxStep;
2967
+ };
2968
+ const getMaxOffsetY = () => {
2969
+ const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
2970
+ return Math.max(0, totalHeight - getGridCanvas().height);
2971
+ };
2972
+ const drawStartLine = () => {
2973
+ const ctx = getGridContext();
2974
+ const canvas = getGridCanvas();
2975
+ if (!ctx) return;
2976
+ const x = playStartStep * renderConfig.stepWidth - currentOffsetX;
2977
+ if (x < -10 || x > canvas.width + 10) return;
2978
+ ctx.save();
2979
+ ctx.strokeStyle = "#ffec27";
2980
+ ctx.lineWidth = 2;
2981
+ ctx.setLineDash([4, 4]);
2982
+ ctx.beginPath();
2983
+ ctx.moveTo(x, 0);
2984
+ ctx.lineTo(x, canvas.height);
2985
+ ctx.stroke();
2986
+ ctx.restore();
2987
+ };
2988
+ const drawPlayhead = () => {
2989
+ const ctx = getGridContext();
2990
+ const canvas = getGridCanvas();
2991
+ if (!ctx) return;
2992
+ const x = currentPlayStep * renderConfig.stepWidth - currentOffsetX;
2993
+ if (x < 0 || x > canvas.width) return;
2994
+ ctx.save();
2995
+ ctx.strokeStyle = "#ff004d";
2996
+ ctx.lineWidth = 2;
2997
+ ctx.beginPath();
2998
+ ctx.moveTo(x, 0);
2999
+ ctx.lineTo(x, canvas.height);
3000
+ ctx.stroke();
3001
+ ctx.restore();
3002
+ };
3003
+ const redrawAll = () => {
3004
+ drawGrid(gridLineSteps);
3005
+ for (const t of trackStates) {
3006
+ const [r, g, b] = t.config.color;
3007
+ const a = t.config.id === activeTrackId ? 1 : 0.3;
3008
+ drawNotes(t.core.getNotes(), [r, g, b, a]);
3009
+ }
3010
+ if (activeToolMode === "select" && selectionRect) {
3011
+ const ctx = getGridContext();
3012
+ ctx.save();
3013
+ ctx.strokeStyle = "#ffec27";
3014
+ ctx.lineWidth = 2;
3015
+ ctx.setLineDash([4, 4]);
3016
+ ctx.strokeRect(
3017
+ selectionRect.x,
3018
+ selectionRect.y,
3019
+ selectionRect.width,
3020
+ selectionRect.height
3021
+ );
3022
+ ctx.fillStyle = "rgba(255,236,39,0.08)";
3023
+ ctx.fillRect(
3024
+ selectionRect.x,
3025
+ selectionRect.y,
3026
+ selectionRect.width,
3027
+ selectionRect.height
3028
+ );
3029
+ ctx.restore();
3030
+ }
3031
+ if (activeToolMode === "select" && selectedNotes.length > 0) {
3032
+ const ids = new Set(selectedNotes.map((n) => n.id));
3033
+ const active = getActive();
3034
+ drawSelectedNotes(active.core.getNotes(), ids, [
3035
+ ...active.config.color,
3036
+ 1
3037
+ ]);
3038
+ }
3039
+ drawStartLine();
3040
+ if (playbackState === "playing") drawPlayhead();
3041
+ updateScrollbars();
3042
+ };
3043
+ const updateScrollbars = () => {
3044
+ const canvas = getGridCanvas();
3045
+ const maxNoteStep = getMaxNoteStep();
3046
+ const leftPaddingWidth = leftPaddingSteps * renderConfig.stepWidth;
3047
+ const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
3048
+ const maxOffsetX = totalContentWidth - canvas.width + leftPaddingWidth;
3049
+ const sbW = refs.hScroll.clientWidth;
3050
+ if (maxOffsetX <= 0) {
3051
+ refs.hScrollThumb.style.width = "100%";
3052
+ refs.hScrollThumb.style.left = "0";
3053
+ } else {
3054
+ const thumbW = Math.max(
3055
+ 40,
3056
+ canvas.width / (totalContentWidth + leftPaddingWidth) * sbW
3057
+ );
3058
+ const ratio = currentOffsetX / maxOffsetX;
3059
+ refs.hScrollThumb.style.width = `${thumbW}px`;
3060
+ refs.hScrollThumb.style.left = `${clamp(ratio * (sbW - thumbW), 0, sbW - thumbW)}px`;
3061
+ }
3062
+ const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
3063
+ const sbH = refs.vScroll.clientHeight;
3064
+ if (totalHeight <= canvas.height) {
3065
+ refs.vScrollThumb.style.height = "100%";
3066
+ refs.vScrollThumb.style.top = "0";
3067
+ } else {
3068
+ const thumbH = Math.max(40, canvas.height / totalHeight * sbH);
3069
+ const maxOffset = getMaxOffsetY();
3070
+ const ratio = currentOffsetY / maxOffset;
3071
+ refs.vScrollThumb.style.height = `${thumbH}px`;
3072
+ refs.vScrollThumb.style.top = `${ratio * (sbH - thumbH)}px`;
3073
+ }
3074
+ };
3075
+ const initScrollbarDrag = () => {
3076
+ let draggingH = false;
3077
+ let draggingV = false;
3078
+ refs.hScroll.addEventListener("pointerdown", (e) => {
3079
+ draggingH = true;
3080
+ e.preventDefault();
3081
+ refs.hScroll.setPointerCapture(e.pointerId);
3082
+ moveH(e.clientX);
3083
+ });
3084
+ refs.vScroll.addEventListener("pointerdown", (e) => {
3085
+ draggingV = true;
3086
+ e.preventDefault();
3087
+ refs.vScroll.setPointerCapture(e.pointerId);
3088
+ moveV(e.clientY);
3089
+ });
3090
+ refs.hScroll.addEventListener("pointermove", (e) => {
3091
+ if (draggingH) moveH(e.clientX);
3092
+ });
3093
+ refs.vScroll.addEventListener("pointermove", (e) => {
3094
+ if (draggingV) moveV(e.clientY);
3095
+ });
3096
+ refs.hScroll.addEventListener("pointerup", () => {
3097
+ draggingH = false;
3098
+ });
3099
+ refs.vScroll.addEventListener("pointerup", () => {
3100
+ draggingV = false;
3101
+ });
3102
+ document.addEventListener("pointermove", (e) => {
3103
+ if (draggingH) moveH(e.clientX);
3104
+ if (draggingV) moveV(e.clientY);
3105
+ });
3106
+ document.addEventListener("pointerup", () => {
3107
+ draggingH = false;
3108
+ draggingV = false;
3109
+ });
3110
+ const moveH = (clientX) => {
3111
+ const canvas = getGridCanvas();
3112
+ const maxNoteStep = getMaxNoteStep();
3113
+ const leftPaddingWidth = leftPaddingSteps * renderConfig.stepWidth;
3114
+ const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
3115
+ const maxOffsetX = totalContentWidth - canvas.width + leftPaddingWidth;
3116
+ if (maxOffsetX <= 0) return;
3117
+ const rect = refs.hScroll.getBoundingClientRect();
3118
+ const thumbW = Number.parseFloat(refs.hScrollThumb.style.width) || 40;
3119
+ const x = clamp(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
3120
+ const ratio = x / (rect.width - thumbW);
3121
+ currentOffsetX = clamp(ratio * maxOffsetX, 0, maxOffsetX);
3122
+ setDrawOffset(currentOffsetX, currentOffsetY);
3123
+ redrawAll();
3124
+ };
3125
+ const moveV = (clientY) => {
3126
+ const maxOffset = getMaxOffsetY();
3127
+ if (maxOffset <= 0) return;
3128
+ const rect = refs.vScroll.getBoundingClientRect();
3129
+ const thumbH = Number.parseFloat(refs.vScrollThumb.style.height) || 40;
3130
+ const y = clamp(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
3131
+ const ratio = y / (rect.height - thumbH);
3132
+ currentOffsetY = clamp(ratio * maxOffset, 0, maxOffset);
3133
+ setDrawOffset(currentOffsetX, currentOffsetY);
3134
+ redrawAll();
3135
+ };
3136
+ };
3137
+ const resizeHandleWidth = 10;
3138
+ const TOUCH_HIT_MARGIN = 6;
3139
+ let suppressClick = false;
3140
+ let hasDragged = false;
3141
+ let dragState = null;
3142
+ let isSelecting = false;
3143
+ let dragMode = "rect";
3144
+ let selectionStart = null;
3145
+ let selectedOriginal = [];
3146
+ let lastMultiPreviewPitch = null;
3147
+ const playPreview = (pitch) => {
3148
+ options.onResumeAudio?.();
3149
+ const active = getActive();
3150
+ dispatchNote(active.config.id, pitch, active.volume, 100, 0, 0.1);
3151
+ };
3152
+ const findActiveNoteAt = (x, y, margin = 0) => {
3153
+ const active = getActive();
3154
+ const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
3155
+ const offset = getDrawOffset();
3156
+ for (const note of active.core.getNotes()) {
3157
+ const logicalX = note.startStep * stepWidth;
3158
+ const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
3159
+ const logicalY = yIndex * keyHeight;
3160
+ const w = note.durationSteps * stepWidth;
3161
+ const renderX = logicalX - offset.x;
3162
+ const renderY = logicalY - offset.y;
3163
+ if (x >= renderX - margin && x <= renderX + w + margin && y >= renderY - margin && y <= renderY + keyHeight + margin)
3164
+ return note;
3165
+ }
3166
+ return null;
3167
+ };
3168
+ const hasNoteAt = (step, pitch, excludeId) => {
3169
+ const active = getActive();
3170
+ return active.core.getNotes().some(
3171
+ (n) => n.id !== excludeId && n.pitch === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
3172
+ );
3173
+ };
3174
+ const snapToGrid = (duration) => Math.max(
3175
+ Math.round(duration / snapGridSteps) * snapGridSteps,
3176
+ snapGridSteps
3177
+ );
3178
+ const onGridPointerDown = (event) => {
3179
+ event.preventDefault();
3180
+ options.onResumeAudio?.();
3181
+ const { x, y, step, pitch } = getGridPosition(event);
3182
+ const active = getActive();
3183
+ if (activeToolMode === "eraser") {
3184
+ const note = findActiveNoteAt(x, y);
3185
+ if (note) active.core.deleteNoteById(note.id);
3186
+ return;
3187
+ }
3188
+ if (activeToolMode === "select") {
3189
+ if (selectedNotes.length > 0) {
3190
+ const clicked2 = findActiveNoteAt(x, y);
3191
+ if (clicked2 && selectedNotes.some((n) => n.id === clicked2.id)) {
3192
+ selectedOriginal = selectedNotes.map((n) => ({
3193
+ id: n.id,
3194
+ startStep: n.startStep,
3195
+ pitch: n.pitch
3196
+ }));
3197
+ isSelecting = true;
3198
+ dragMode = "move";
3199
+ selectionStart = { x, y, step, pitch };
3200
+ hasDragged = false;
3201
+ lastMultiPreviewPitch = null;
3202
+ return;
3203
+ }
3204
+ selectedNotes = [];
3205
+ selectionRect = null;
3206
+ }
3207
+ const clicked = findActiveNoteAt(x, y);
3208
+ if (clicked) {
3209
+ selectedNotes = [clicked];
3210
+ selectedOriginal = [
3211
+ {
3212
+ id: clicked.id,
3213
+ startStep: clicked.startStep,
3214
+ pitch: clicked.pitch
3215
+ }
3216
+ ];
3217
+ isSelecting = true;
3218
+ dragMode = "move";
3219
+ } else {
3220
+ selectedNotes = [];
3221
+ selectionRect = null;
3222
+ isSelecting = true;
3223
+ dragMode = "rect";
3224
+ }
3225
+ selectionStart = { x, y, step, pitch };
3226
+ hasDragged = false;
3227
+ return;
3228
+ }
3229
+ hasDragged = false;
3230
+ const existing = findActiveNoteAt(x, y, TOUCH_HIT_MARGIN);
3231
+ if (existing) {
3232
+ playPreview(existing.pitch);
3233
+ const { stepWidth } = renderConfig;
3234
+ const offset = getDrawOffset();
3235
+ const renderX = existing.startStep * stepWidth - offset.x;
3236
+ const w = existing.durationSteps * stepWidth;
3237
+ if (x >= renderX + w - resizeHandleWidth && x <= renderX + w) {
3238
+ dragState = {
3239
+ noteId: existing.id,
3240
+ mode: "resize",
3241
+ dragOffsetStep: 0,
3242
+ dragOffsetPitch: 0,
3243
+ startStep: existing.startStep,
3244
+ durationSteps: existing.durationSteps,
3245
+ lastPreviewPitch: existing.pitch
3246
+ };
3247
+ } else {
3248
+ dragState = {
3249
+ noteId: existing.id,
3250
+ mode: "move",
3251
+ dragOffsetStep: step - existing.startStep,
3252
+ dragOffsetPitch: pitch - existing.pitch,
3253
+ startStep: existing.startStep,
3254
+ durationSteps: existing.durationSteps,
3255
+ lastPreviewPitch: existing.pitch
3256
+ };
3257
+ }
3258
+ suppressClick = true;
3259
+ return;
3260
+ }
3261
+ const snappedStep = Math.floor(step / currentInsertLength) * currentInsertLength;
3262
+ const newStart = snappedStep;
3263
+ const newEnd = newStart + currentInsertLength;
3264
+ const overlapping = active.core.getNotes().some(
3265
+ (n) => n.pitch === pitch && newStart < n.startStep + n.durationSteps && newEnd > n.startStep
3266
+ );
3267
+ if (!overlapping) {
3268
+ active.core.addNote(snappedStep, pitch, {
3269
+ noteLengthSteps: currentInsertLength
3270
+ });
3271
+ playPreview(pitch);
3272
+ const newNote = active.core.getNotes().find((n) => n.startStep === snappedStep && n.pitch === pitch);
3273
+ if (newNote) {
3274
+ dragState = {
3275
+ noteId: newNote.id,
3276
+ mode: "move",
3277
+ dragOffsetStep: 0,
3278
+ dragOffsetPitch: 0,
3279
+ startStep: newNote.startStep,
3280
+ durationSteps: newNote.durationSteps,
3281
+ lastPreviewPitch: newNote.pitch
3282
+ };
3283
+ hasDragged = true;
3284
+ }
3285
+ suppressClick = true;
3286
+ }
3287
+ };
3288
+ const onPointerMove = (event) => {
3289
+ const active = getActive();
3290
+ if (activeToolMode === "pen") {
3291
+ if (!dragState) return;
3292
+ const { step, pitch } = getGridPosition(event);
3293
+ hasDragged = true;
3294
+ if (dragState.mode === "move") {
3295
+ const nextStart = step - dragState.dragOffsetStep;
3296
+ const snappedStart = Math.round(nextStart / snapGridSteps) * snapGridSteps;
3297
+ const nextPitch = pitch - dragState.dragOffsetPitch;
3298
+ if (hasNoteAt(snappedStart, nextPitch, dragState.noteId)) return;
3299
+ active.core.moveNote(dragState.noteId, snappedStart, nextPitch);
3300
+ if (nextPitch !== dragState.lastPreviewPitch) {
3301
+ dragState.lastPreviewPitch = nextPitch;
3302
+ playPreview(nextPitch);
3303
+ }
3304
+ return;
3305
+ }
3306
+ const rawDuration = step - dragState.startStep + 1;
3307
+ const snapped = snapToGrid(rawDuration);
3308
+ active.core.resizeNote(dragState.noteId, snapped);
3309
+ dragState.durationSteps = snapped;
3310
+ currentInsertLength = snapped;
3311
+ redrawAll();
3312
+ return;
3313
+ }
3314
+ if (activeToolMode === "select" && isSelecting && selectionStart) {
3315
+ const { x, y, step, pitch } = getGridPosition(event);
3316
+ if (dragMode === "rect") {
3317
+ const rect = {
3318
+ x: Math.min(x, selectionStart.x),
3319
+ y: Math.min(y, selectionStart.y),
3320
+ width: Math.abs(x - selectionStart.x),
3321
+ height: Math.abs(y - selectionStart.y)
3322
+ };
3323
+ selectionRect = rect;
3324
+ const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
3325
+ const offset = getDrawOffset();
3326
+ selectedNotes = active.core.getNotes().filter((note) => {
3327
+ const logicalX = note.startStep * stepWidth;
3328
+ const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
3329
+ const logicalY = yIndex * keyHeight;
3330
+ const nx = logicalX - offset.x;
3331
+ const ny = logicalY - offset.y;
3332
+ const nw = note.durationSteps * stepWidth;
3333
+ return rect.x < nx + nw && rect.x + rect.width > nx && rect.y < ny + keyHeight && rect.y + rect.height > ny;
3334
+ });
3335
+ redrawAll();
3336
+ } else {
3337
+ const rawDeltaStep = step - selectionStart.step;
3338
+ const snappedDelta = Math.round(rawDeltaStep / snapGridSteps) * snapGridSteps;
3339
+ const deltaPitch = pitch - selectionStart.pitch;
3340
+ if (snappedDelta !== 0 || deltaPitch !== 0) {
3341
+ hasDragged = true;
3342
+ if (!active.core.isBatchOperation) active.core.beginBatch();
3343
+ for (const note of selectedNotes) {
3344
+ const orig = selectedOriginal.find((o) => o.id === note.id);
3345
+ if (!orig) continue;
3346
+ const newPitch = orig.pitch + deltaPitch;
3347
+ if (newPitch >= 0 && newPitch < 128)
3348
+ active.core.moveNote(
3349
+ note.id,
3350
+ orig.startStep + snappedDelta,
3351
+ newPitch
3352
+ );
3353
+ }
3354
+ if (selectedNotes.length > 0) {
3355
+ const grab = selectedNotes[0];
3356
+ const orig = selectedOriginal.find((o) => o.id === grab.id);
3357
+ if (orig) {
3358
+ const newGrab = orig.pitch + deltaPitch;
3359
+ if (newGrab !== lastMultiPreviewPitch && newGrab >= 0 && newGrab < 128) {
3360
+ lastMultiPreviewPitch = newGrab;
3361
+ playPreview(newGrab);
3362
+ }
3363
+ }
3364
+ }
3365
+ }
3366
+ redrawAll();
3367
+ }
3368
+ }
3369
+ };
3370
+ const onPointerUp = () => {
3371
+ if (activeToolMode === "pen" && dragState) {
3372
+ if (hasDragged) {
3373
+ const active = getActive();
3374
+ if (dragState.mode === "move")
3375
+ active.core.moveNoteEnd(dragState.noteId);
3376
+ else active.core.resizeNoteEnd(dragState.noteId);
3377
+ suppressClick = true;
3378
+ }
3379
+ dragState = null;
3380
+ hasDragged = false;
3381
+ }
3382
+ if (activeToolMode === "select" && isSelecting) {
3383
+ if (hasDragged && dragMode === "move" && selectedNotes.length > 0) {
3384
+ getActive().core.endBatch();
3385
+ }
3386
+ isSelecting = false;
3387
+ selectionStart = null;
3388
+ hasDragged = false;
3389
+ lastMultiPreviewPitch = null;
3390
+ selectionRect = null;
3391
+ selectedOriginal = [];
3392
+ redrawAll();
3393
+ }
3394
+ };
3395
+ const setupCanvas = () => {
3396
+ const w = refs.rollContainer.clientWidth || 800;
3397
+ const h = refs.rollContainer.clientHeight || 450;
3398
+ init(refs.wrapper, w, h, renderConfig);
3399
+ const gridCanvas = getGridCanvas();
3400
+ gridCanvas.addEventListener("pointerdown", onGridPointerDown);
3401
+ gridCanvas.addEventListener("dblclick", (event) => {
3402
+ event.preventDefault();
3403
+ const { step, pitch } = getGridPosition(event);
3404
+ const active = getActive();
3405
+ const note = active.core.getNotes().find(
3406
+ (n) => n.pitch === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
3407
+ );
3408
+ if (note) active.core.deleteNoteById(note.id);
3409
+ });
3410
+ gridCanvas.addEventListener(
3411
+ "wheel",
3412
+ (event) => {
3413
+ event.preventDefault();
3414
+ currentOffsetY = clamp(
3415
+ currentOffsetY + event.deltaY,
3416
+ 0,
3417
+ getMaxOffsetY()
3418
+ );
3419
+ currentOffsetX = Math.max(0, currentOffsetX + event.deltaX);
3420
+ setDrawOffset(currentOffsetX, currentOffsetY);
3421
+ redrawAll();
3422
+ },
3423
+ { passive: false }
3424
+ );
3425
+ gridCanvas.addEventListener("click", () => {
3426
+ if (suppressClick) {
3427
+ suppressClick = false;
3428
+ }
3429
+ });
3430
+ const headerCanvas = getHeaderCanvas();
3431
+ headerCanvas.addEventListener("click", (event) => {
3432
+ if (playbackState === "playing") return;
3433
+ const rect = headerCanvas.getBoundingClientRect();
3434
+ const x = event.clientX - rect.left;
3435
+ const step = Math.floor((x + currentOffsetX) / renderConfig.stepWidth);
3436
+ playStartStep = Math.max(
3437
+ 0,
3438
+ Math.floor(step / snapGridSteps) * snapGridSteps
3439
+ );
3440
+ redrawAll();
3441
+ });
3442
+ setDrawOffset(currentOffsetX, currentOffsetY);
3443
+ redrawAll();
3444
+ };
3445
+ const applyZoomX = () => {
3446
+ const canvas = getGridCanvas();
3447
+ const centerStep = (currentOffsetX + canvas.width / 2) / renderConfig.stepWidth;
3448
+ renderConfig.stepWidth = BASE_STEP_WIDTH * (zoomX * 2) / 100;
3449
+ refs.zoomXLabel.textContent = `${zoomX}%`;
3450
+ currentOffsetX = Math.max(
3451
+ 0,
3452
+ centerStep * renderConfig.stepWidth - canvas.width / 2
3453
+ );
3454
+ setDrawOffset(currentOffsetX, currentOffsetY);
3455
+ redrawAll();
3456
+ };
3457
+ const applyZoomY = () => {
3458
+ const canvas = getGridCanvas();
3459
+ const centerKey = (currentOffsetY + canvas.height / 2) / renderConfig.keyHeight;
3460
+ renderConfig.keyHeight = BASE_KEY_HEIGHT * zoomY / 100;
3461
+ refs.zoomYLabel.textContent = `${zoomY}%`;
3462
+ currentOffsetY = clamp(
3463
+ centerKey * renderConfig.keyHeight - canvas.height / 2,
3464
+ 0,
3465
+ getMaxOffsetY()
3466
+ );
3467
+ setDrawOffset(currentOffsetX, currentOffsetY);
3468
+ redrawAll();
3469
+ };
3470
+ const dispatchNote = (trackId, pitch, trackVol, velocity, when, duration) => {
3471
+ const volume = trackVol / 100 * (velocity / 127) * (masterVolume / 100);
3472
+ options.onPlayNote?.({ trackId, pitch, velocity, volume, when, duration });
3473
+ };
3474
+ const sequencer = createSequencer({
3475
+ getTracks: () => trackStates.map((t) => ({
3476
+ id: t.config.id,
3477
+ volume: t.volume,
3478
+ notes: t.core.getNotes()
3479
+ })),
3480
+ getBpm: () => bpm,
3481
+ getPlayStartStep: () => playStartStep,
3482
+ getDrumPattern: () => drumPatterns[currentDrumPattern] ?? null,
3483
+ getSoloTrackId: () => isSolo ? activeTrackId : null,
3484
+ getAudioTime,
3485
+ onPlayNote: (e) => {
3486
+ const volume = e.volume * (masterVolume / 100);
3487
+ options.onPlayNote?.({ ...e, volume });
3488
+ },
3489
+ onPlayDrum: (e) => {
3490
+ const velocity = e.velocity * (drumVolume / 100) * (masterVolume / 100);
3491
+ options.onPlayDrum?.({ ...e, velocity });
3492
+ },
3493
+ onTick: (step) => {
3494
+ currentPlayStep = step;
3495
+ const canvas = getGridCanvas();
3496
+ const visibleSteps = canvas.width / renderConfig.stepWidth;
3497
+ const threshold = currentOffsetX / renderConfig.stepWidth + visibleSteps - 4;
3498
+ if (currentPlayStep > threshold) {
3499
+ const visibleBars = Math.round(visibleSteps / renderConfig.stepsPerBar);
3500
+ currentOffsetX += visibleBars * renderConfig.stepsPerBar * renderConfig.stepWidth;
3501
+ setDrawOffset(currentOffsetX, currentOffsetY);
3502
+ }
3503
+ redrawAll();
3504
+ },
3505
+ onEnd: () => {
3506
+ playbackState = "stopped";
3507
+ currentPlayStep = 0;
3508
+ updateTransport();
3509
+ redrawAll();
3510
+ },
3511
+ stepsPerBar: renderConfig.stepsPerBar
3512
+ });
3513
+ const play = () => {
3514
+ options.onResumeAudio?.();
3515
+ if (playbackState === "playing") return;
3516
+ const fromStep = playbackState === "paused" ? pausedPlayStep : playStartStep;
3517
+ if (playbackState !== "paused") {
3518
+ const canvas = getGridCanvas();
3519
+ currentOffsetX = Math.max(
3520
+ 0,
3521
+ playStartStep * renderConfig.stepWidth - canvas.width * 0.5
3522
+ );
3523
+ setDrawOffset(currentOffsetX, currentOffsetY);
3524
+ }
3525
+ playbackState = "playing";
3526
+ sequencer.start(fromStep);
3527
+ updateTransport();
3528
+ };
3529
+ const pause = () => {
3530
+ if (playbackState !== "playing") return;
3531
+ pausedPlayStep = currentPlayStep;
3532
+ sequencer.stop();
3533
+ playbackState = "paused";
3534
+ updateTransport();
3535
+ };
3536
+ const stop = () => {
3537
+ sequencer.stop();
3538
+ playbackState = "stopped";
3539
+ currentPlayStep = 0;
3540
+ updateTransport();
3541
+ redrawAll();
3542
+ };
3543
+ const togglePlay = () => {
3544
+ if (playbackState === "playing") stop();
3545
+ else play();
3546
+ };
3547
+ const updateTransport = () => {
3548
+ const playing = playbackState === "playing";
3549
+ const label = playing ? "\u505C\u6B62" : playbackState === "paused" ? "\u518D\u958B" : "\u8A66\u8074";
3550
+ refs.playBtn.innerHTML = `${icon(playing ? "stop" : "play")}<span>${label}</span>`;
3551
+ refs.playBtn.classList.toggle("dtm-play--stop", playing);
3552
+ };
3553
+ const updateUndoRedo = () => {
3554
+ const core = getActive().core;
3555
+ refs.undoBtn.disabled = !core.canUndo();
3556
+ refs.redoBtn.disabled = !core.canRedo();
3557
+ };
3558
+ const updateTrackPanel = () => {
3559
+ refs.trackTabs.innerHTML = "";
3560
+ for (const t of trackStates) {
3561
+ const [r, g, b] = t.config.color;
3562
+ const btn = document.createElement("button");
3563
+ btn.className = `dtm-pill ${t.config.id === activeTrackId ? "dtm-pill--active" : ""}`;
3564
+ btn.style.setProperty("--dtm-pill-color", `rgb(${r},${g},${b})`);
3565
+ btn.innerHTML = `<span class="dtm-dot"></span><span>${t.config.name}</span>`;
3566
+ btn.addEventListener("click", () => switchTrack(t.config.id));
3567
+ refs.trackTabs.appendChild(btn);
3568
+ }
3569
+ const active = getActive();
3570
+ refs.trackBody.innerHTML = `
3571
+ <div class="dtm-row">
3572
+ <span class="dtm-label">velocity</span>
3573
+ <input type="range" class="dtm-range dtm-grow" data-dtm="track-vol" min="0" max="127" value="${active.volume}">
3574
+ <span class="dtm-label" data-dtm="track-vol-label">${active.volume}</span>
3575
+ </div>`;
3576
+ const volInput = refs.trackBody.querySelector(
3577
+ '[data-dtm="track-vol"]'
3578
+ );
3579
+ const volLabel = refs.trackBody.querySelector(
3580
+ '[data-dtm="track-vol-label"]'
3581
+ );
3582
+ volInput.addEventListener("input", () => {
3583
+ active.volume = Number.parseInt(volInput.value, 10);
3584
+ active.core.setVolume(active.volume);
3585
+ volLabel.textContent = String(active.volume);
3586
+ });
3587
+ if (active.config.id === "chord" && showChord) {
3588
+ const div = document.createElement("div");
3589
+ div.className = "dtm-row";
3590
+ div.style.flexDirection = "column";
3591
+ div.style.alignItems = "stretch";
3592
+ const roots = [
3593
+ "C",
3594
+ "C#",
3595
+ "D",
3596
+ "D#",
3597
+ "E",
3598
+ "F",
3599
+ "F#",
3600
+ "G",
3601
+ "G#",
3602
+ "A",
3603
+ "A#",
3604
+ "B"
3605
+ ];
3606
+ div.innerHTML = `
3607
+ <div class="dtm-row">
3608
+ <span class="dtm-label">\u548C\u97F3</span>
3609
+ <select class="dtm-select" data-dtm="chord-pattern">
3610
+ <option value="block">\u30D6\u30ED\u30C3\u30AF</option>
3611
+ <option value="arpeggio">\u30A2\u30EB\u30DA\u30B8\u30AA</option>
3612
+ <option value="arpeggio-fast">\u30A2\u30EB\u30DA\u30B8\u30AA\uFF08\u30B8\u30E3\u30E9\u30FC\u30F3\uFF09</option>
3613
+ <option value="offbeat">\u88CF\u6253\u3061</option>
3614
+ <option value="yatsume">\u30E4\u30C4\u30E1\u7A74</option>
3615
+ <option value="alternating">\u4EA4\u4E92\u594F</option>
3616
+ </select>
3617
+ <select class="dtm-select" data-dtm="chord-root">
3618
+ ${roots.map((r, i) => `<option value="${i}">${r}</option>`).join("")}
3619
+ </select>
3620
+ <button class="dtm-btn dtm-btn--primary" data-dtm="chord-apply">\u9069\u7528</button>
3621
+ </div>
3622
+ <textarea class="dtm-textarea" data-dtm="chord-input" placeholder="\u4F8B: C|G|Am|Em|F|C|F|G">${active.savedChordInput}</textarea>`;
3623
+ refs.trackBody.appendChild(div);
3624
+ const patternSel = div.querySelector(
3625
+ '[data-dtm="chord-pattern"]'
3626
+ );
3627
+ const rootSel = div.querySelector(
3628
+ '[data-dtm="chord-root"]'
3629
+ );
3630
+ const input = div.querySelector(
3631
+ '[data-dtm="chord-input"]'
3632
+ );
3633
+ patternSel.value = active.savedChordPattern;
3634
+ rootSel.value = String(active.savedChordRoot);
3635
+ const save = () => {
3636
+ active.savedChordInput = input.value;
3637
+ active.savedChordPattern = patternSel.value;
3638
+ active.savedChordRoot = Number.parseInt(rootSel.value, 10);
3639
+ };
3640
+ patternSel.addEventListener("change", save);
3641
+ rootSel.addEventListener("change", save);
3642
+ input.addEventListener("input", save);
3643
+ div.querySelector('[data-dtm="chord-apply"]').addEventListener("click", () => {
3644
+ save();
3645
+ applyChord();
3646
+ });
3647
+ }
3648
+ };
3649
+ const switchTrack = (id) => {
3650
+ activeTrackId = id;
3651
+ updateTrackPanel();
3652
+ updateUndoRedo();
3653
+ redrawAll();
3654
+ };
3655
+ const setToolMode = (mode) => {
3656
+ activeToolMode = mode;
3657
+ for (const [btn, m] of [
3658
+ [refs.toolPen, "pen"],
3659
+ [refs.toolSelect, "select"],
3660
+ [refs.toolEraser, "eraser"]
3661
+ ]) {
3662
+ btn.classList.toggle("dtm-segbtn--active", m === mode);
3663
+ }
3664
+ if (mode !== "select") {
3665
+ selectionRect = null;
3666
+ selectedNotes = [];
3667
+ }
3668
+ redrawAll();
3669
+ };
3670
+ const generateMML = () => {
3671
+ const barLimitBars = Number(refs.barLimitSelect.value);
3672
+ const limitSteps = barLimitBars > 0 ? barLimitBars * renderConfig.stepsPerBar : Infinity;
3673
+ const clipNotes = (notes) => limitSteps === Infinity ? notes : notes.filter((n) => n.startStep < limitSteps);
3674
+ if (refs.decomposeChordToggle.checked) {
3675
+ const ignoreHeavy = refs.ignoreChordHeavyToggle.checked;
3676
+ const targetStates = ignoreHeavy ? trackStates.filter((t) => !isChordHeavyTrack(t.core.getNotes())) : trackStates;
3677
+ const ignoredCount = trackStates.length - targetStates.length;
3678
+ const allNotes = clipNotes(
3679
+ targetStates.flatMap((t) => t.core.getNotes())
3680
+ );
3681
+ const monoTracks = decomposeToMonophonic(allNotes);
3682
+ const refCore = trackStates[0].core;
3683
+ const full2 = monoTracks.map(
3684
+ (notes, i) => `@${i} ${refCore.getMMLFromNotes(notes, bpm, 100).trim()}`
3685
+ ).join(";\n");
3686
+ const minified2 = monoTracks.map(
3687
+ (notes, i) => `@${i}${refCore.getMMLFromNotes(notes, bpm, 100).trim().replace(/\s+/g, "")}`
3688
+ ).join(";");
3689
+ return {
3690
+ full: full2,
3691
+ minified: minified2,
3692
+ ignoredCount,
3693
+ trackCount: monoTracks.length,
3694
+ barLimit: barLimitBars
3695
+ };
3696
+ }
3697
+ const full = trackStates.map(
3698
+ (t, i) => `@${i} ${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim()}`
3699
+ ).join(";\n");
3700
+ const minified = trackStates.map(
3701
+ (t, i) => `@${i}${t.core.getMMLFromNotes(clipNotes(t.core.getNotes()), bpm, t.volume).trim().replace(/\s+/g, "")}`
3702
+ ).join(";");
3703
+ return {
3704
+ full,
3705
+ minified,
3706
+ ignoredCount: 0,
3707
+ trackCount: trackStates.length,
3708
+ barLimit: barLimitBars
3709
+ };
3710
+ };
3711
+ const showMML = () => {
3712
+ const { full, minified, ignoredCount, trackCount, barLimit } = generateMML();
3713
+ refs.outputFull.textContent = full;
3714
+ refs.outputMini.textContent = minified;
3715
+ const isDecompose = refs.decomposeChordToggle.checked;
3716
+ const modeLabel = isDecompose ? "\u548C\u97F3\u5206\u89E3" : "\u901A\u5E38";
3717
+ const ignoredLabel = ignoredCount > 0 ? ` / \u4F34\u594F${ignoredCount}\u30C8\u30E9\u30C3\u30AF\u9664\u5916` : "";
3718
+ const barLabel = barLimit > 0 ? ` / \u301C${barLimit}\u5C0F\u7BC0` : "";
3719
+ refs.outputStatus.textContent = `[${modeLabel}] (${trackCount}\u30C8\u30E9\u30C3\u30AF${ignoredLabel}${barLabel}) \u901A\u5E38: ${full.length}\u6587\u5B57 / minify: ${minified.length}\u6587\u5B57`;
3720
+ refs.outputContainer.classList.remove("dtm-hidden");
3721
+ updateUndoRedo();
3722
+ };
3723
+ const clearAll = () => {
3724
+ for (const t of trackStates) {
3725
+ t.core.resetHistory();
3726
+ t.core.clearNotesWithoutHistory();
3727
+ }
3728
+ redrawAll();
3729
+ };
3730
+ const loadMML = (mml) => {
3731
+ if (!mml) return;
3732
+ clearAll();
3733
+ for (const t of trackStates) t.core.setLoadMode(true);
3734
+ const { placements, bpm: parsedBpm } = parseMML(mml, {
3735
+ stepsPerBar: renderConfig.stepsPerBar
3736
+ });
3737
+ for (const p of placements) {
3738
+ const t = trackStates[p.trackIndex];
3739
+ if (!t) continue;
3740
+ t.core.addNote(p.startStep, p.pitch, {
3741
+ noteLengthSteps: p.durationSteps
3742
+ });
3743
+ }
3744
+ if (parsedBpm) setBpm(parsedBpm);
3745
+ for (const t of trackStates) {
3746
+ t.core.setLoadMode(false);
3747
+ t.core.addHistoryOnce();
3748
+ }
3749
+ playStartStep = 0;
3750
+ currentOffsetX = 0;
3751
+ setDrawOffset(currentOffsetX, currentOffsetY);
3752
+ redrawAll();
3753
+ updateUndoRedo();
3754
+ };
3755
+ const applyChord = () => {
3756
+ if (!options.parseChord || !options.parseChords) return;
3757
+ const active = getActive();
3758
+ const chordTrack = trackStates.find((t) => t.config.id === "chord");
3759
+ if (!chordTrack) return;
3760
+ const placements = buildChordPlacements({
3761
+ chordStr: active.savedChordInput,
3762
+ patternType: active.savedChordPattern,
3763
+ rootShift: active.savedChordRoot,
3764
+ bpm,
3765
+ stepsPerBar: renderConfig.stepsPerBar,
3766
+ parseChord: options.parseChord,
3767
+ parseChords: options.parseChords
3768
+ });
3769
+ chordTrack.core.clearNotesWithoutHistory();
3770
+ chordTrack.core.beginBatch();
3771
+ for (const p of placements) {
3772
+ chordTrack.core.addNote(p.startStep, p.pitch, {
3773
+ noteLengthSteps: Math.max(1, p.durationSteps),
3774
+ velocity: p.velocity
3775
+ });
3776
+ }
3777
+ chordTrack.core.endBatch();
3778
+ chordTrack.core.addHistoryOnce();
3779
+ redrawAll();
3780
+ };
3781
+ const loadMIDI = (bytes) => {
3782
+ if (!options.parseMidi) return;
3783
+ const midi = options.parseMidi(bytes);
3784
+ const analysis = analyzeMidiTracks(midi);
3785
+ const selected = analysis.filter((a) => a.selected).map((a) => a.index);
3786
+ applyMidiSelection(midi, selected);
3787
+ };
3788
+ const applyMidiSelection = (midi, selectedIndices) => {
3789
+ clearAll();
3790
+ for (const t of trackStates) t.core.setLoadMode(true);
3791
+ const isAdvanced = trackStates.length > TRACKS_SIMPLE.length;
3792
+ const { placements, bpm: parsedBpm } = isAdvanced ? extractMidiPlacementsByTrack(
3793
+ midi,
3794
+ selectedIndices,
3795
+ trackStates.map((t) => t.config.id)
3796
+ ) : extractMidiPlacements(midi, selectedIndices);
3797
+ for (const p of placements) {
3798
+ const t = trackStates.find((ts) => ts.config.id === p.trackId);
3799
+ if (!t) continue;
3800
+ t.core.addNote(p.startStep, p.pitch, {
3801
+ noteLengthSteps: p.durationSteps,
3802
+ velocity: p.velocity
3803
+ });
3804
+ }
3805
+ setBpm(Math.round(parsedBpm));
3806
+ for (const t of trackStates) {
3807
+ t.core.setLoadMode(false);
3808
+ t.core.addHistoryOnce();
3809
+ }
3810
+ playStartStep = 0;
3811
+ currentOffsetX = 0;
3812
+ setDrawOffset(currentOffsetX, currentOffsetY);
3813
+ redrawAll();
3814
+ updateUndoRedo();
3815
+ };
3816
+ const exportMIDI2 = () => exportMIDI({
3817
+ tracks: trackStates.map((t) => ({
3818
+ notes: t.core.getNotes(),
3819
+ volume: t.volume
3820
+ })),
3821
+ drumPattern: drumPatterns[currentDrumPattern],
3822
+ drumVolume,
3823
+ bpm,
3824
+ stepsPerBar: renderConfig.stepsPerBar
3825
+ });
3826
+ const setBpm = (value) => {
3827
+ bpm = value;
3828
+ refs.bpmInput.value = String(value);
3829
+ for (const t of trackStates) t.core.setTempo(value);
3830
+ };
3831
+ let lastUndoTime = 0;
3832
+ const undo = () => {
3833
+ const now = Date.now();
3834
+ if (now - lastUndoTime < 100) return;
3835
+ lastUndoTime = now;
3836
+ getActive().core.undo();
3837
+ redrawAll();
3838
+ updateUndoRedo();
3839
+ };
3840
+ const redo = () => {
3841
+ getActive().core.redo();
3842
+ redrawAll();
3843
+ updateUndoRedo();
3844
+ };
3845
+ const overlayDuring = (fn) => {
3846
+ refs.overlay.hidden = false;
3847
+ setTimeout(() => {
3848
+ fn();
3849
+ refs.overlay.hidden = true;
3850
+ }, 30);
3851
+ };
3852
+ const wireEvents = () => {
3853
+ refs.playBtn.addEventListener("click", togglePlay);
3854
+ refs.playBtn.disabled = false;
3855
+ refs.recBtn.addEventListener("click", () => options.onToggleRecord?.());
3856
+ refs.recBtn.style.display = options.onToggleRecord ? "" : "none";
3857
+ refs.soloCheckbox.addEventListener("change", () => {
3858
+ isSolo = refs.soloCheckbox.checked;
3859
+ });
3860
+ refs.toolPen.addEventListener("click", () => setToolMode("pen"));
3861
+ refs.toolSelect.addEventListener("click", () => setToolMode("select"));
3862
+ refs.toolEraser.addEventListener("click", () => setToolMode("eraser"));
3863
+ refs.undoBtn.addEventListener("click", undo);
3864
+ refs.redoBtn.addEventListener("click", redo);
3865
+ refs.noteLengthSelect.addEventListener("change", () => {
3866
+ snapGridSteps = Number.parseInt(refs.noteLengthSelect.value, 10);
3867
+ currentInsertLength = snapGridSteps;
3868
+ redrawAll();
3869
+ });
3870
+ refs.bpmInput.addEventListener("input", () => {
3871
+ setBpm(Number.parseInt(refs.bpmInput.value, 10) || 120);
3872
+ });
3873
+ refs.zoomXIn.addEventListener("click", () => {
3874
+ zoomX = Math.min(200, zoomX + 25);
3875
+ applyZoomX();
3876
+ });
3877
+ refs.zoomXOut.addEventListener("click", () => {
3878
+ zoomX = Math.max(25, zoomX - 25);
3879
+ applyZoomX();
3880
+ });
3881
+ refs.zoomYIn.addEventListener("click", () => {
3882
+ zoomY = Math.min(200, zoomY + 25);
3883
+ applyZoomY();
3884
+ });
3885
+ refs.zoomYOut.addEventListener("click", () => {
3886
+ zoomY = Math.max(50, zoomY - 25);
3887
+ applyZoomY();
3888
+ });
3889
+ refs.masterVolume.addEventListener("input", () => {
3890
+ masterVolume = Number.parseInt(refs.masterVolume.value, 10) || 0;
3891
+ refs.masterVolumeLabel.textContent = `${masterVolume}%`;
3892
+ });
3893
+ refs.drumSelect.addEventListener("change", () => {
3894
+ currentDrumPattern = refs.drumSelect.value;
3895
+ });
3896
+ refs.drumVolume.addEventListener("input", () => {
3897
+ drumVolume = Number.parseInt(refs.drumVolume.value, 10) || 0;
3898
+ refs.drumVolumeLabel.textContent = `${drumVolume}%`;
3899
+ });
3900
+ refs.macroClear.addEventListener("click", () => {
3901
+ const active = getActive();
3902
+ active.core.beginBatch();
3903
+ active.core.clearNotesWithoutHistory();
3904
+ active.core.endBatch();
3905
+ active.core.saveHistory();
3906
+ redrawAll();
3907
+ });
3908
+ refs.macroRandom.addEventListener("click", () => {
3909
+ generateRandomPattern(getActive().core, {
3910
+ stepsPerBar: renderConfig.stepsPerBar,
3911
+ startStep: playStartStep,
3912
+ pitchRangeStart: renderConfig.pitchRangeStart
3913
+ });
3914
+ redrawAll();
3915
+ });
3916
+ refs.macroHarmonic.addEventListener("click", () => {
3917
+ const chord = trackStates.find((t) => t.config.id === "chord");
3918
+ if (!chord || activeTrackId === "chord") return;
3919
+ applyHarmonicFilter(getActive().core, chord.core, {
3920
+ stepsPerBar: renderConfig.stepsPerBar
3921
+ });
3922
+ redrawAll();
3923
+ });
3924
+ refs.macroMono.addEventListener("click", () => {
3925
+ const chord = trackStates.find((t) => t.config.id === "chord");
3926
+ if (!chord || activeTrackId === "chord") return;
3927
+ applyMonophonic(getActive().core, chord.core, {
3928
+ stepsPerBar: renderConfig.stepsPerBar
3929
+ });
3930
+ redrawAll();
3931
+ });
3932
+ refs.generateMmlBtn.addEventListener("click", showMML);
3933
+ refs.exportMidiBtn.addEventListener("click", () => {
3934
+ const blob = exportMIDI2();
3935
+ const url = URL.createObjectURL(blob);
3936
+ const a = document.createElement("a");
3937
+ a.href = url;
3938
+ a.download = "dtm.mid";
3939
+ a.click();
3940
+ URL.revokeObjectURL(url);
3941
+ });
3942
+ const copy = (text, btn) => {
3943
+ navigator.clipboard?.writeText(text);
3944
+ btn.classList.add("dtm-btn--success");
3945
+ setTimeout(() => btn.classList.remove("dtm-btn--success"), 1200);
3946
+ };
3947
+ refs.copyFullBtn.addEventListener(
3948
+ "click",
3949
+ () => copy(refs.outputFull.textContent ?? "", refs.copyFullBtn)
3950
+ );
3951
+ refs.copyMiniBtn.addEventListener(
3952
+ "click",
3953
+ () => copy(refs.outputMini.textContent ?? "", refs.copyMiniBtn)
3954
+ );
3955
+ refs.mmlLoadBtn.addEventListener(
3956
+ "click",
3957
+ () => overlayDuring(() => loadMML(refs.mmlInput.value))
3958
+ );
3959
+ refs.shiftApplyBtn.addEventListener(
3960
+ "click",
3961
+ () => overlayDuring(() => {
3962
+ shiftNotes(
3963
+ trackStates.map((t) => t.core),
3964
+ Number.parseInt(refs.shiftSelect.value, 10) || 0
3965
+ );
3966
+ redrawAll();
3967
+ })
3968
+ );
3969
+ if (showMidi) wireMidi();
3970
+ document.addEventListener("keydown", onKeyDown);
3971
+ for (const ta of refs.root.querySelectorAll("textarea, input")) {
3972
+ ta.addEventListener("keydown", (e) => {
3973
+ const ke = e;
3974
+ if ((ke.ctrlKey || ke.metaKey) && ["KeyZ", "KeyY", "KeyV", "KeyC", "KeyX"].includes(ke.code))
3975
+ e.stopPropagation();
3976
+ });
3977
+ }
3978
+ };
3979
+ let pendingMidi = null;
3980
+ let detectedTracks = [];
3981
+ const wireMidi = () => {
3982
+ refs.midiInput.addEventListener("change", async () => {
3983
+ const file = refs.midiInput.files?.[0];
3984
+ if (!file || !options.parseMidi) return;
3985
+ refs.overlay.hidden = false;
3986
+ const buffer = new Uint8Array(await file.arrayBuffer());
3987
+ pendingMidi = options.parseMidi(buffer);
3988
+ detectedTracks = analyzeMidiTracks(pendingMidi);
3989
+ refs.midiTrackSelection.innerHTML = `<span class="dtm-label">\u30C8\u30E9\u30C3\u30AF</span>`;
3990
+ detectedTracks.forEach((t, i) => {
3991
+ const btn = document.createElement("button");
3992
+ btn.className = `dtm-btn ${t.selected ? "dtm-btn--primary" : "dtm-btn--ghost"}`;
3993
+ btn.dataset.selected = String(t.selected);
3994
+ btn.textContent = `${t.name} (${t.noteCount})`;
3995
+ btn.addEventListener("click", () => {
3996
+ const on = btn.dataset.selected !== "true";
3997
+ btn.dataset.selected = String(on);
3998
+ btn.classList.toggle("dtm-btn--primary", on);
3999
+ btn.classList.toggle("dtm-btn--ghost", !on);
4000
+ });
4001
+ refs.midiTrackSelection.appendChild(btn);
4002
+ if (i === 0) refs.midiTrackSelection.dataset.ready = "1";
4003
+ });
4004
+ refs.midiTrackSelection.classList.remove("dtm-hidden");
4005
+ refs.overlay.hidden = true;
4006
+ });
4007
+ refs.midiLoadBtn.addEventListener("click", () => {
4008
+ if (!pendingMidi) return;
4009
+ const selected = [];
4010
+ const btns = refs.midiTrackSelection.querySelectorAll("button");
4011
+ btns.forEach((b, i) => {
4012
+ if (b.dataset.selected === "true")
4013
+ selected.push(detectedTracks[i].index);
4014
+ });
4015
+ if (selected.length === 0) return;
4016
+ overlayDuring(() => applyMidiSelection(pendingMidi, selected));
4017
+ });
4018
+ };
4019
+ const onKeyDown = (e) => {
4020
+ if (!(e.ctrlKey || e.metaKey)) return;
4021
+ if (e.code === "KeyZ" && !e.shiftKey) {
4022
+ e.preventDefault();
4023
+ undo();
4024
+ } else if (e.code === "KeyZ" && e.shiftKey || e.code === "KeyY") {
4025
+ e.preventDefault();
4026
+ redo();
4027
+ } else if (e.code === "KeyC" && selectedNotes.length > 0) {
4028
+ e.preventDefault();
4029
+ copiedNotes = [...selectedNotes];
4030
+ } else if (e.code === "KeyX" && selectedNotes.length > 0) {
4031
+ e.preventDefault();
4032
+ copiedNotes = [...selectedNotes];
4033
+ const core = getActive().core;
4034
+ core.beginBatch();
4035
+ for (const n of selectedNotes) core.deleteNoteById(n.id);
4036
+ core.endBatch();
4037
+ selectedNotes = [];
4038
+ } else if (e.code === "KeyV" && copiedNotes.length > 0) {
4039
+ e.preventDefault();
4040
+ const core = getActive().core;
4041
+ const notes = core.getNotes();
4042
+ const minStart = Math.min(...copiedNotes.map((n) => n.startStep));
4043
+ core.beginBatch();
4044
+ for (const note of copiedNotes) {
4045
+ const newStart = playStartStep + (note.startStep - minStart);
4046
+ const newEnd = newStart + note.durationSteps;
4047
+ const overlap = notes.some(
4048
+ (ex) => ex.pitch === note.pitch && newStart < ex.startStep + ex.durationSteps && newEnd > ex.startStep
4049
+ );
4050
+ if (!overlap)
4051
+ core.addNote(newStart, note.pitch, {
4052
+ noteLengthSteps: note.durationSteps,
4053
+ velocity: note.velocity
4054
+ });
4055
+ }
4056
+ core.endBatch();
4057
+ redrawAll();
4058
+ }
4059
+ };
4060
+ setupCanvas();
4061
+ createTrackStates();
4062
+ ready = true;
4063
+ initScrollbarDrag();
4064
+ wireEvents();
4065
+ setBpm(bpm);
4066
+ updateTrackPanel();
4067
+ updateTransport();
4068
+ updateUndoRedo();
4069
+ redrawAll();
4070
+ if (options.initialMML) loadMML(options.initialMML);
4071
+ let resizeTimer = null;
4072
+ const resizeObserver = new ResizeObserver(() => {
4073
+ if (resizeTimer) clearTimeout(resizeTimer);
4074
+ resizeTimer = setTimeout(() => setupCanvas(), 150);
4075
+ });
4076
+ resizeObserver.observe(refs.rollContainer);
4077
+ document.addEventListener("pointermove", onPointerMove);
4078
+ document.addEventListener("pointerup", onPointerUp);
4079
+ return {
4080
+ play,
4081
+ pause,
4082
+ stop,
4083
+ getMML: generateMML,
4084
+ loadMML,
4085
+ loadMIDI,
4086
+ exportMIDI: exportMIDI2,
4087
+ setBpm,
4088
+ getPlaybackState: () => playbackState,
4089
+ destroy: () => {
4090
+ sequencer.stop();
4091
+ resizeObserver.disconnect();
4092
+ document.removeEventListener("pointermove", onPointerMove);
4093
+ document.removeEventListener("pointerup", onPointerUp);
4094
+ document.removeEventListener("keydown", onKeyDown);
4095
+ target.innerHTML = "";
4096
+ }
4097
+ };
4098
+ };
4099
+
4100
+ // src/instrument-presets.ts
4101
+ var INSTRUMENT_PRESETS = {
4102
+ // --- STANDARD: 汎用性と完成度重視 ---
4103
+ piano: {
4104
+ displayName: "\u30B0\u30E9\u30F3\u30C9\u30D4\u30A2\u30CE",
4105
+ description: "\u6700\u3082\u7834\u7DBB\u3057\u306B\u304F\u3044\u69CB\u6210\u3002\u697D\u66F2\u5236\u4F5C\u306E\u30B9\u30B1\u30C3\u30C1\u306B\u3082\u6700\u9069\u3002",
4106
+ melody: "Acoustic Grand Piano",
4107
+ submelody: "Vibraphone",
4108
+ bass: "Electric Bass (finger)",
4109
+ chord: "Pad 2 (warm)"
4110
+ },
4111
+ acoustic: {
4112
+ displayName: "\u30A2\u30B3\u30FC\u30B9\u30C6\u30A3\u30C3\u30AF",
4113
+ description: "\u751F\u697D\u5668\u306E\u6E29\u304B\u307F\u3092\u91CD\u8996\u3002\u30D5\u30A9\u30FC\u30AF\u3084\u30DD\u30C3\u30D7\u30B9\u306B\u3002",
4114
+ melody: "Acoustic Guitar (steel)",
4115
+ submelody: "Harmonica",
4116
+ bass: "Acoustic Bass",
4117
+ chord: "Acoustic Guitar (nylon)"
4118
+ },
4119
+ jazz_night: {
4120
+ displayName: "\u30B8\u30E3\u30BA\u30FB\u30CA\u30A4\u30C8",
4121
+ description: "Rhodes\u98A8\u306EEP\u3068\u30A6\u30C3\u30C9\u30D9\u30FC\u30B9\u306B\u3088\u308B\u3001\u5927\u4EBA\u3073\u305F\u30A2\u30F3\u30B5\u30F3\u30D6\u30EB\u3002",
4122
+ melody: "Electric Piano 1",
4123
+ submelody: "Flute",
4124
+ bass: "Acoustic Bass",
4125
+ chord: "Electric Guitar (jazz)"
4126
+ },
4127
+ // --- MODERN & VIBE: エッジの効いた現代的な響き ---
4128
+ synth_pop: {
4129
+ displayName: "\u30B7\u30F3\u30BB\u30DD\u30C3\u30D7",
4130
+ description: "80s\u301C\u73FE\u4EE3\u307E\u3067\u3002\u629C\u3051\u308B\u30EA\u30FC\u30C9\u3068\u592A\u3044\u30D9\u30FC\u30B9\u306E\u738B\u9053\u3002",
4131
+ melody: "Lead 2 (sawtooth)",
4132
+ submelody: "Lead 4 (chiff)",
4133
+ bass: "Synth Bass 2",
4134
+ chord: "Pad 3 (polysynth)"
4135
+ },
4136
+ cyber_punk: {
4137
+ displayName: "\u30B5\u30A4\u30D0\u30FC\u30D1\u30F3\u30AF",
4138
+ description: "\u30C7\u30B8\u30BF\u30EB\u306A\u51B7\u305F\u3055\u3068\u6B6A\u307F\u304C\u6DF7\u3056\u308A\u5408\u3046\u3001\u672A\u6765\u7684\u306A\u97FF\u304D\u3002",
4139
+ melody: "Lead 8 (bass + lead)",
4140
+ submelody: "Lead 5 (charang)",
4141
+ bass: "Synth Bass 2",
4142
+ chord: "Pad 8 (sweep)"
4143
+ },
4144
+ rock: {
4145
+ displayName: "\u30CF\u30FC\u30C9\u30ED\u30C3\u30AF",
4146
+ description: "\u6B6A\u307F\u30AE\u30BF\u30FC\u3068\u91CD\u539A\u306A\u30D9\u30FC\u30B9\u3067\u3001\u30D1\u30EF\u30FC\u3092\u524D\u9762\u306B\u3002",
4147
+ melody: "Distortion Guitar",
4148
+ submelody: "Rock Organ",
4149
+ bass: "Electric Bass (pick)",
4150
+ chord: "Overdriven Guitar"
4151
+ },
4152
+ // --- WORLD & CLASSIC: 特定のジャンル・地域 ---
4153
+ orchestra: {
4154
+ displayName: "\u30AA\u30FC\u30B1\u30B9\u30C8\u30E9",
4155
+ description: "\u58EE\u5927\u306A\u7269\u8A9E\u3092\u4E88\u611F\u3055\u305B\u308B\u3001\u7BA1\u5F26\u697D\u5668\u306E\u91CD\u539A\u306A\u97FF\u304D\u3002",
4156
+ melody: "French Horn",
4157
+ submelody: "Pizzicato Strings",
4158
+ bass: "Cello",
4159
+ chord: "Tremolo Strings"
4160
+ },
4161
+ japanese_wa: {
4162
+ displayName: "\u548C\u98A8\u30FB\u96C5",
4163
+ description: "\u7434\u3068\u4E09\u5473\u7DDA\u306E\u7E4A\u7D30\u306A\u8ABF\u3079\u306B\u3001\u5C3A\u516B\u306E\u60C5\u7DD2\u3092\u6DFB\u3048\u3066\u3002",
4164
+ melody: "Koto",
4165
+ submelody: "Shamisen",
4166
+ bass: "Taiko Drum",
4167
+ chord: "Shakuhachi"
4168
+ },
4169
+ arabic_exotic: {
4170
+ displayName: "\u30A8\u30AD\u30BE\u30C1\u30C3\u30AF",
4171
+ description: "\u30B7\u30BF\u30FC\u30EB\u3084\u30D0\u30B0\u30D1\u30A4\u30D7\u306B\u3088\u308B\u3001\u7570\u56FD\u60C5\u7DD2\u6EA2\u308C\u308B\u30B5\u30A6\u30F3\u30C9\u3002",
4172
+ melody: "Sitar",
4173
+ submelody: "Bagpipe",
4174
+ bass: "Fretless Bass",
4175
+ chord: "Kalimba"
4176
+ },
4177
+ // --- FANTASY & ATMOSPHERE: 雰囲気と余韻 ---
4178
+ fantasy_rpg: {
4179
+ displayName: "\u30D5\u30A1\u30F3\u30BF\u30B8\u30FCRPG",
4180
+ description: "\u30AA\u30AB\u30EA\u30CA\u3068\u30CF\u30FC\u30D7\u304C\u7D21\u3050\u3001\u5192\u967A\u3068\u9B54\u6CD5\u306E\u4E16\u754C\u89B3\u3002",
4181
+ melody: "Ocarina",
4182
+ submelody: "Celesta",
4183
+ bass: "Timpani",
4184
+ chord: "Orchestral Harp"
4185
+ },
4186
+ ambient_cloud: {
4187
+ displayName: "\u30A2\u30F3\u30D3\u30A8\u30F3\u30C8",
4188
+ description: "\u8F2A\u90ED\u3092\u307C\u304B\u3057\u305F\u97F3\u8272\u3067\u3001\u6DF1\u3044\u6CA1\u5165\u611F\u3068\u4F59\u97FB\u3092\u6F14\u51FA\u3002",
4189
+ melody: "Lead 6 (voice)",
4190
+ submelody: "Music Box",
4191
+ bass: "Synth Bass 1",
4192
+ chord: "Pad 7 (halo)"
4193
+ },
4194
+ retro_game: {
4195
+ displayName: "8-bit \u30EC\u30C8\u30ED",
4196
+ description: "\u77E9\u5F62\u6CE2\u3092\u60F3\u8D77\u3055\u305B\u308B\u3001\u521D\u671F\u30B2\u30FC\u30E0\u6A5F\u306E\u3088\u3046\u306A\u61D0\u304B\u3057\u3044\u97FF\u304D\u3002",
4197
+ melody: "Lead 1 (square)",
4198
+ submelody: "Lead 2 (sawtooth)",
4199
+ bass: "Synth Bass 1",
4200
+ chord: "Clavinet"
4201
+ }
4202
+ };
4203
+
4204
+ // src/piano-roll.ts
4205
+ var createPianoRoll = (options, handlers) => {
4206
+ const {
4207
+ mountTarget,
4208
+ width = 800,
4209
+ height = 450,
4210
+ config,
4211
+ noteLengthSteps = 1
4212
+ } = options;
4213
+ init(mountTarget, width, height, config);
4214
+ let currentNoteLengthSteps = noteLengthSteps;
4215
+ let selectionRect = null;
4216
+ let isSelecting = false;
4217
+ let selectionStart = null;
4218
+ let selectedNotes = [];
4219
+ let copiedNotes = [];
4220
+ const core = new MMLCore({
4221
+ onMMLGenerated: handlers.onMMLGenerated,
4222
+ onNotesChanged: (notes) => {
4223
+ handlers.onNotesChanged(notes);
4224
+ }
4225
+ });
4226
+ const getAddNoteOptions = () => ({
4227
+ noteLengthSteps: currentNoteLengthSteps
4228
+ });
4229
+ let suppressClick = false;
4230
+ onClick((step, pitch) => {
4231
+ if (suppressClick) {
4232
+ suppressClick = false;
4233
+ return;
4234
+ }
4235
+ const mode = core.getToolMode();
4236
+ if (mode === "pen") {
4237
+ core.addNote(step, pitch, getAddNoteOptions());
4238
+ handlers.onNoteClick?.(step, pitch, false);
4239
+ } else if (mode === "eraser") {
4240
+ const notes = core.getNotes();
4241
+ const note = notes.find(
4242
+ (n) => n.startStep <= step && step < n.startStep + n.durationSteps && n.pitch === pitch
4243
+ );
4244
+ if (note) {
4245
+ core.deleteNoteById(note.id);
4246
+ handlers.onNoteClick?.(step, pitch, true);
4247
+ }
4248
+ }
4249
+ });
4250
+ const gridCanvas = getGridCanvas();
4251
+ const resizeHandleWidth = 6;
4252
+ let dragState = null;
4253
+ let hasDragged = false;
4254
+ let lastPreviewPitch = null;
4255
+ const findNoteAtPosition = (x, y) => {
4256
+ const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
4257
+ const offset = getDrawOffset();
4258
+ for (const note of core.getNotes()) {
4259
+ const logicalX = note.startStep * stepWidth;
4260
+ const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
4261
+ const logicalY = yIndex * keyHeight;
4262
+ const w = note.durationSteps * stepWidth;
4263
+ const h = keyHeight;
4264
+ const renderX = logicalX - offset.x;
4265
+ const renderY = logicalY - offset.y;
4266
+ if (x >= renderX && x <= renderX + w && y >= renderY && y <= renderY + h) {
4267
+ return note;
4268
+ }
4269
+ }
4270
+ return null;
4271
+ };
4272
+ const handlePointerMove = (e) => {
4273
+ if (core.getToolMode() === "select" && isSelecting && selectionStart) {
4274
+ const { x, y } = getGridPosition(e);
4275
+ const minX = Math.min(x, selectionStart.x);
4276
+ const minY = Math.min(y, selectionStart.y);
4277
+ const width2 = Math.abs(x - selectionStart.x);
4278
+ const height2 = Math.abs(y - selectionStart.y);
4279
+ selectionRect = { x: minX, y: minY, width: width2, height: height2 };
4280
+ selectedNotes = getNotesInRect(selectionRect);
4281
+ redraw();
4282
+ return;
4283
+ }
4284
+ if (!dragState) return;
4285
+ hasDragged = true;
4286
+ const { step, pitch } = getGridPosition(e);
4287
+ if (dragState.mode === "move") {
4288
+ if (dragState.selectedNotes && dragState.selectedNotes.length > 0) {
4289
+ const noteId = dragState.noteId;
4290
+ const baseNote = dragState.selectedNotes.find((n) => n.id === noteId);
4291
+ if (!baseNote) return;
4292
+ const nextStart2 = step - dragState.dragOffsetStep;
4293
+ const nextPitch2 = pitch - dragState.dragOffsetPitch;
4294
+ const stepDelta = nextStart2 - baseNote.startStep;
4295
+ const pitchDelta = nextPitch2 - baseNote.pitch;
4296
+ for (const note of dragState.selectedNotes) {
4297
+ const newStart = note.startStep + stepDelta;
4298
+ const newPitch = note.pitch + pitchDelta;
4299
+ core.moveNote(note.id, newStart, newPitch);
4300
+ }
4301
+ if (options.onPreviewSound && pitch !== lastPreviewPitch) {
4302
+ lastPreviewPitch = pitch;
4303
+ options.onPreviewSound(pitch, step);
4304
+ }
4305
+ redraw();
4306
+ return;
4307
+ }
4308
+ const nextStart = step - dragState.dragOffsetStep;
4309
+ const nextPitch = pitch - dragState.dragOffsetPitch;
4310
+ core.moveNote(dragState.noteId, nextStart, nextPitch);
4311
+ return;
4312
+ }
4313
+ const nextDuration = step - dragState.startStep + 1;
4314
+ core.resizeNote(dragState.noteId, nextDuration);
4315
+ };
4316
+ const endDrag = () => {
4317
+ const wasSelectMode = core.getToolMode() === "select";
4318
+ if (wasSelectMode) {
4319
+ isSelecting = false;
4320
+ selectionStart = null;
4321
+ }
4322
+ if (dragState) {
4323
+ dragState = null;
4324
+ if (hasDragged) {
4325
+ suppressClick = true;
4326
+ }
4327
+ }
4328
+ hasDragged = false;
4329
+ if (wasSelectMode) {
4330
+ selectionRect = null;
4331
+ redraw();
4332
+ }
4333
+ };
4334
+ gridCanvas.addEventListener("pointerdown", (e) => {
4335
+ const { x, y, step, pitch } = getGridPosition(e);
4336
+ const currentMode = core.getToolMode();
4337
+ if (currentMode === "select") {
4338
+ const clickedNote = findNoteAtPosition(x, y);
4339
+ if (selectionRect && clickedNote) {
4340
+ const notesInRect = getNotesInRect(selectionRect);
4341
+ if (notesInRect.some((n) => n.id === clickedNote.id)) {
4342
+ dragState = {
4343
+ noteId: clickedNote.id,
4344
+ mode: "move",
4345
+ dragOffsetStep: step - clickedNote.startStep,
4346
+ dragOffsetPitch: pitch - clickedNote.pitch,
4347
+ startStep: clickedNote.startStep,
4348
+ selectedNotes: notesInRect
4349
+ // 複数選択ノートを保存
4350
+ };
4351
+ isSelecting = false;
4352
+ selectionStart = null;
4353
+ return;
4354
+ }
4355
+ }
4356
+ selectedNotes = [];
4357
+ selectionRect = null;
4358
+ isSelecting = true;
4359
+ selectionStart = { x, y, step, pitch };
4360
+ return;
4361
+ }
4362
+ const note = findNoteAtPosition(x, y);
4363
+ if (!note) return;
4364
+ const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
4365
+ const offset = getDrawOffset();
4366
+ const logicalX = note.startStep * stepWidth;
4367
+ const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
4368
+ const logicalY = yIndex * keyHeight;
4369
+ const renderX = logicalX - offset.x;
4370
+ const renderY = logicalY - offset.y;
4371
+ const w = note.durationSteps * stepWidth;
4372
+ if (x >= renderX + w - resizeHandleWidth && x <= renderX + w && y >= renderY && y <= renderY + keyHeight) {
4373
+ dragState = {
4374
+ noteId: note.id,
4375
+ mode: "resize",
4376
+ dragOffsetStep: 0,
4377
+ dragOffsetPitch: 0,
4378
+ startStep: note.startStep
4379
+ };
4380
+ return;
4381
+ }
4382
+ dragState = {
4383
+ noteId: note.id,
4384
+ mode: "move",
4385
+ dragOffsetStep: step - note.startStep,
4386
+ dragOffsetPitch: pitch - note.pitch,
4387
+ startStep: note.startStep
4388
+ };
4389
+ });
4390
+ gridCanvas.addEventListener("pointerleave", endDrag);
4391
+ document.addEventListener("pointerup", endDrag);
4392
+ document.addEventListener("pointermove", handlePointerMove);
4393
+ gridCanvas.addEventListener(
4394
+ "wheel",
4395
+ (e) => {
4396
+ e.preventDefault();
4397
+ const configValues = getRenderConfig();
4398
+ const gridHeight = gridCanvas.height;
4399
+ const maxOffsetY = Math.max(
4400
+ 0,
4401
+ configValues.keyCount * configValues.keyHeight - gridHeight
4402
+ );
4403
+ const currentOffset = getDrawOffset();
4404
+ const nextOffsetY = Math.min(
4405
+ Math.max(currentOffset.y + e.deltaY, 0),
4406
+ maxOffsetY
4407
+ );
4408
+ setDrawOffset(currentOffset.x, nextOffsetY);
4409
+ drawGrid();
4410
+ drawNotes(core.getNotes());
4411
+ },
4412
+ { passive: false }
4413
+ );
4414
+ const redraw = () => {
4415
+ drawGrid();
4416
+ drawNotes(core.getNotes());
4417
+ if (core.getToolMode() === "select") {
4418
+ drawSelectionRect(selectionRect);
4419
+ if (selectedNotes.length > 0) {
4420
+ const selectedIds = new Set(selectedNotes.map((n) => n.id));
4421
+ drawSelectedNotes(core.getNotes(), selectedIds);
4422
+ }
4423
+ }
4424
+ };
4425
+ const getNotesInRect = (rect) => {
4426
+ const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
4427
+ const offset = getDrawOffset();
4428
+ const notes = [];
4429
+ for (const note of core.getNotes()) {
4430
+ const logicalX = note.startStep * stepWidth;
4431
+ const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
4432
+ const logicalY = yIndex * keyHeight;
4433
+ const noteRect = {
4434
+ x: logicalX - offset.x,
4435
+ y: logicalY - offset.y,
4436
+ width: note.durationSteps * stepWidth,
4437
+ height: keyHeight
4438
+ };
4439
+ if (rect.x < noteRect.x + noteRect.width && rect.x + rect.width > noteRect.x && rect.y < noteRect.y + noteRect.height && rect.y + rect.height > noteRect.y) {
4440
+ notes.push(note);
4441
+ }
4442
+ }
4443
+ return notes;
4444
+ };
4445
+ redraw();
4446
+ return {
4447
+ core,
4448
+ getNotes: () => core.getNotes(),
4449
+ getMML: () => core.getMML(),
4450
+ setVolume: (volume) => core.setVolume(volume),
4451
+ setNoteLengthSteps: (steps) => {
4452
+ currentNoteLengthSteps = steps;
4453
+ },
4454
+ redraw,
4455
+ setToolMode: (mode) => {
4456
+ core.setToolMode(mode);
4457
+ if (mode !== "select") {
4458
+ selectionRect = null;
4459
+ selectedNotes = [];
4460
+ }
4461
+ },
4462
+ getToolMode: () => core.getToolMode(),
4463
+ getSelectionRect: () => selectionRect,
4464
+ getNotesInRect,
4465
+ clearSelection: () => {
4466
+ selectionRect = null;
4467
+ selectedNotes = [];
4468
+ },
4469
+ copySelection: () => {
4470
+ copiedNotes = [...selectedNotes];
4471
+ return copiedNotes;
4472
+ },
4473
+ pasteNotes: (_, startStep) => {
4474
+ if (copiedNotes.length === 0) return;
4475
+ const minStart = Math.min(...copiedNotes.map((n) => n.startStep));
4476
+ copiedNotes.forEach((note) => {
4477
+ const newStep = startStep + (note.startStep - minStart);
4478
+ core.addNote(newStep, note.pitch, {
4479
+ noteLengthSteps: note.durationSteps,
4480
+ velocity: note.velocity
4481
+ });
4482
+ });
4483
+ }
4484
+ };
4485
+ };
4486
+ // Annotate the CommonJS export names for ESM import in node:
4487
+ 0 && (module.exports = {
4488
+ DAW_CSS,
4489
+ DRUM_FONT,
4490
+ DRUM_KEYS,
4491
+ DRUM_PATTERNS,
4492
+ INSTRUMENT_PRESETS,
4493
+ LinkedList,
4494
+ MMLCore,
4495
+ PITCH_MAP,
4496
+ TRACKS_ADVANCED,
4497
+ TRACKS_SIMPLE,
4498
+ analyzeMidiTracks,
4499
+ applyHarmonicFilter,
4500
+ applyMonophonic,
4501
+ buildChordPlacements,
4502
+ buildNameToKeyMapping,
4503
+ createAudioContext,
4504
+ createPianoRoll,
4505
+ createSequencer,
4506
+ decomposeToMonophonic,
4507
+ drawGrid,
4508
+ drawHeader,
4509
+ drawKeyboard,
4510
+ drawNotes,
4511
+ drawSelectedNotes,
4512
+ drawSelectionRect,
4513
+ exportMIDI,
4514
+ extractMidiPlacements,
4515
+ extractMidiPlacementsByTrack,
4516
+ fetchSoundFontList,
4517
+ generateRandomPattern,
4518
+ getDrawOffset,
4519
+ getGridCanvas,
4520
+ getGridContext,
4521
+ getGridPosition,
4522
+ getHeaderCanvas,
4523
+ getMidiBPM,
4524
+ getRenderConfig,
4525
+ getXY,
4526
+ icon,
4527
+ init,
4528
+ injectStyles,
4529
+ isChordHeavyTrack,
4530
+ mountDAW,
4531
+ onClick,
4532
+ parseMML,
4533
+ setDrawOffset,
4534
+ setupRecorder,
4535
+ shiftNotes
4536
+ });