@onjmin/dtm 1.0.7 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -20,8 +20,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ A4_HZ: () => A4_HZ,
24
+ A4_UNITS: () => A4_UNITS,
25
+ CENTS_PER_UNIT: () => CENTS_PER_UNIT,
23
26
  DAW_CSS: () => DAW_CSS,
24
27
  DEFAULT_BPM: () => DEFAULT_BPM,
28
+ DEFAULT_EDO: () => DEFAULT_EDO,
25
29
  DEFAULT_GATE: () => DEFAULT_GATE,
26
30
  DEFAULT_PAN: () => DEFAULT_PAN,
27
31
  DEFAULT_PLAYBACK_VELOCITY: () => DEFAULT_PLAYBACK_VELOCITY,
@@ -31,21 +35,30 @@ __export(index_exports, {
31
35
  DRUM_FONT: () => DRUM_FONT,
32
36
  DRUM_KEYS: () => DRUM_KEYS,
33
37
  DRUM_PATTERNS: () => DRUM_PATTERNS,
38
+ EDO31_NAMES: () => EDO31_NAMES,
34
39
  GM_INSTRUMENT_NAMES: () => GM_INSTRUMENT_NAMES,
35
40
  INSTRUMENT_PRESETS: () => INSTRUMENT_PRESETS,
41
+ KEY_COUNT: () => KEY_COUNT,
36
42
  KOE_BASE_URL: () => KOE_BASE_URL,
37
43
  KOE_VOICEBANKS: () => KOE_VOICEBANKS,
38
44
  KOE_VOICEBANK_LABELS: () => KOE_VOICEBANK_LABELS,
39
45
  KOE_VOICEBANK_TERMS: () => KOE_VOICEBANK_TERMS,
40
46
  LinkedList: () => LinkedList,
41
47
  MAX_VOCAL_VOLUME: () => MAX_VOCAL_VOLUME,
48
+ MICRO_STEP: () => MICRO_STEP,
42
49
  MMLCore: () => MMLCore,
43
50
  MML_END_MARKER: () => MML_END_MARKER,
44
51
  MidiSearchClient: () => MidiSearchClient,
45
- PITCH_MAP: () => PITCH_MAP2,
52
+ PITCH_ENCODING_VERSION: () => PITCH_ENCODING_VERSION,
53
+ PITCH_MAP: () => PITCH_MAP,
54
+ PITCH_RANGE_END: () => PITCH_RANGE_END,
55
+ PITCH_RANGE_START: () => PITCH_RANGE_START,
46
56
  PREWARM_NOTES: () => PREWARM_NOTES,
47
57
  TRACKS_ADVANCED: () => TRACKS_ADVANCED,
48
58
  TRACKS_SIMPLE: () => TRACKS_SIMPLE,
59
+ UNITS_PER_EDO31_DEGREE: () => UNITS_PER_EDO31_DEGREE,
60
+ UNITS_PER_OCTAVE: () => UNITS_PER_OCTAVE,
61
+ UNITS_PER_SEMITONE: () => UNITS_PER_SEMITONE,
49
62
  VIBRATO_MIN_SEC: () => VIBRATO_MIN_SEC,
50
63
  VOICE_IMAGES: () => VOICE_IMAGES,
51
64
  VOICE_IMAGE_KEY: () => VOICE_IMAGE_KEY,
@@ -55,6 +68,7 @@ __export(index_exports, {
55
68
  buildChordPlacements: () => buildChordPlacements,
56
69
  buildDrumPatternJson: () => buildDrumPatternJson,
57
70
  buildNameToKeyMapping: () => buildNameToKeyMapping,
71
+ chromaticStep: () => chromaticStep,
58
72
  collectPitchTokens: () => collectPitchTokens,
59
73
  concatFloat32: () => concatFloat32,
60
74
  createAudioContext: () => createAudioContext,
@@ -63,19 +77,13 @@ __export(index_exports, {
63
77
  createKoeVoice: () => createKoeVoice,
64
78
  createLyricsConductor: () => createLyricsConductor,
65
79
  createPianoRoll: () => createPianoRoll,
80
+ createRenderer: () => createRenderer,
66
81
  createSequencer: () => createSequencer,
67
82
  createSingingVoices: () => createSingingVoices,
68
83
  createSynth: () => createSynth,
69
84
  createVoiceRegistry: () => createVoiceRegistry,
70
85
  decodeMml: () => decodeMml,
71
86
  decomposeToMonophonic: () => decomposeToMonophonic,
72
- drawGrid: () => drawGrid,
73
- drawHeader: () => drawHeader,
74
- drawKeyboard: () => drawKeyboard,
75
- drawNoteLyrics: () => drawNoteLyrics,
76
- drawNotes: () => drawNotes,
77
- drawSelectedNotes: () => drawSelectedNotes,
78
- drawSelectionRect: () => drawSelectionRect,
79
87
  encodeMml: () => encodeMml,
80
88
  encodeWavPCM16: () => encodeWavPCM16,
81
89
  exportMIDI: () => exportMIDI,
@@ -83,36 +91,34 @@ __export(index_exports, {
83
91
  extractMidiDrumPattern: () => extractMidiDrumPattern,
84
92
  extractMidiPlacements: () => extractMidiPlacements,
85
93
  extractMidiPlacementsByTrack: () => extractMidiPlacementsByTrack,
94
+ fifthToStep: () => fifthToStep,
95
+ fifthToUnits: () => fifthToUnits,
86
96
  formatMmlMeta: () => formatMmlMeta,
87
97
  freqFromPitch: () => freqFromPitch,
88
98
  generateRandomPattern: () => generateRandomPattern,
89
- getDrawOffset: () => getDrawOffset,
90
99
  getDrumPatternKeys: () => getDrumPatternKeys,
91
- getGridCanvas: () => getGridCanvas,
92
- getGridContext: () => getGridContext,
93
- getGridPosition: () => getGridPosition,
94
- getHeaderCanvas: () => getHeaderCanvas,
95
100
  getMidiBPM: () => getMidiBPM,
96
- getRenderConfig: () => getRenderConfig,
97
- getXY: () => getXY,
98
101
  icon: () => icon,
99
- init: () => init,
100
102
  injectStyles: () => injectStyles,
101
103
  isChordHeavyTrack: () => isChordHeavyTrack,
104
+ isNaturalLetter: () => isNaturalLetter,
102
105
  isPlausibleMidiTranscription: () => isPlausibleMidiTranscription,
103
106
  isValidHttpUrl: () => isValidHttpUrl,
107
+ keyCountFor: () => keyCountFor,
104
108
  koeUrl: () => koeUrl,
109
+ midiToUnits: () => midiToUnits,
105
110
  mountChordPlayer: () => mountChordPlayer,
106
111
  mountDAW: () => mountDAW,
107
112
  mountMmlPlayer: () => mountMmlPlayer,
113
+ naturalStep: () => naturalStep,
108
114
  normalizeDrumPatterns: () => normalizeDrumPatterns,
109
115
  normalizeLyrics: () => normalizeLyrics,
110
- onClick: () => onClick,
111
116
  panToStereo: () => panToStereo,
112
117
  parseCustomVocals: () => parseCustomVocals,
113
118
  parseLyrics: () => parseLyrics,
114
119
  parseMML: () => parseMML,
115
120
  parseMmlMeta: () => parseMmlMeta,
121
+ pitchV1ToUnits: () => pitchV1ToUnits,
116
122
  playChords: () => playChords,
117
123
  playMML: () => playMML,
118
124
  playNote: () => playNote,
@@ -120,14 +126,19 @@ __export(index_exports, {
120
126
  playSingingMML: () => playSingingMML,
121
127
  resolveDrumPattern: () => resolveDrumPattern,
122
128
  resolveLoopPoint: () => resolveLoopPoint,
123
- setBackgroundActive: () => setBackgroundActive,
124
- setDrawOffset: () => setDrawOffset,
125
129
  shiftNotes: () => shiftNotes,
126
130
  showLoadingOverlay: () => showLoadingOverlay,
131
+ spellingToUnits: () => spellingToUnits,
127
132
  stripCustomVocals: () => stripCustomVocals,
128
133
  stripLyrics: () => stripLyrics,
129
134
  stripMmlMeta: () => stripMmlMeta,
130
135
  transposeNotes: () => transposeNotes,
136
+ unitsPerRow: () => unitsPerRow,
137
+ unitsPerStep: () => unitsPerStep,
138
+ unitsToHz: () => unitsToHz,
139
+ unitsToMidi: () => unitsToMidi,
140
+ unitsToMidiDetune: () => unitsToMidiDetune,
141
+ unitsToPitchV1: () => unitsToPitchV1,
131
142
  vocalVolumeToGain: () => vocalVolumeToGain
132
143
  });
133
144
  module.exports = __toCommonJS(index_exports);
@@ -282,7 +293,7 @@ async function buildNameToKeyMapping() {
282
293
  }
283
294
  var GM_INSTRUMENT_NAMES = FONT_NAME_SURIKOV.trim().split("\n").map((line) => line.slice(line.indexOf(" ") + 1));
284
295
 
285
- // node_modules/.pnpm/@onjmin+chord-parser@1.0.4/node_modules/@onjmin/chord-parser/dist/index.mjs
296
+ // node_modules/.pnpm/@onjmin+chord-parser@1.1.1/node_modules/@onjmin/chord-parser/dist/index.mjs
286
297
  var SHARP_NAMES = [
287
298
  "C",
288
299
  "C#",
@@ -313,6 +324,10 @@ var FLAT_NAMES = [
313
324
  ];
314
325
  var toPitchClass = (n) => (n % 12 + 12) % 12;
315
326
  var noteName = (pc, flat = false) => (flat ? FLAT_NAMES : SHARP_NAMES)[toPitchClass(pc)];
327
+ var LETTER_FIFTH = [0, 2, 4, -1, 1, 3, 5];
328
+ var DEGREE_FIFTH = [0, 2, 4, -1, 1, 3, 5];
329
+ var deg2fifth = (deg) => DEGREE_FIFTH[(deg - 1) % 7];
330
+ var HALF_FIFTH = 7;
316
331
  var SyntaxErrorWithPos = class extends Error {
317
332
  constructor(input, msg) {
318
333
  super(
@@ -359,6 +374,16 @@ var Input = class _Input {
359
374
  };
360
375
  var Output = class {
361
376
  pitch = null;
377
+ /** ルート音の五度圏インデックス。`pitch` と対で持つ。 */
378
+ rootFifth = 0;
379
+ /**
380
+ * 構成音の五度圏オフセット。キーは「ルートからの相対半音 mod 12」。
381
+ *
382
+ * 五度圏インデックスはオクターブ不変なので、分数コードのオクターブシフト
383
+ * (`value` の配列を ±12 する処理)の影響を受けない。そのため構成音の集合とは
384
+ * 別にこの対応表を持っておけば、配列操作側に手を入れずに綴りを保てる。
385
+ */
386
+ relFifths = /* @__PURE__ */ new Map();
362
387
  chord = null;
363
388
  isChord = false;
364
389
  pending = null;
@@ -442,9 +467,17 @@ var parseFormula = (input, output = new Output(), nest = 0) => {
442
467
  case DIVIDE: {
443
468
  const o = parseFormula(input, new Output(), nest);
444
469
  const v = [...output.value];
470
+ const rebase = (semi, fifth) => noteFifth(
471
+ output,
472
+ semi - output.pitch,
473
+ fifth - output.rootFifth
474
+ );
445
475
  if (o.isChord) {
476
+ for (const [rel, f] of o.relFifths)
477
+ rebase(o.pitch + rel, o.rootFifth + f);
446
478
  output.value = [...o.value].concat(v);
447
479
  } else {
480
+ rebase(o.pitch, o.rootFifth);
448
481
  const a = v.sort((x2, y) => x2 - y);
449
482
  const pitch = (o.pitch + 3) % 12 - 3;
450
483
  if (a[0] < pitch) {
@@ -491,63 +524,91 @@ var parsePitch = (input, output) => {
491
524
  const pitch = pitchMatcher.parse(input);
492
525
  if (pitch === null) err(input, "Not found pitch");
493
526
  output.pitch = pitch;
527
+ output.rootFifth = LETTER_FIFTH[idx2pitch.slice(0, 7).indexOf(pitch)];
494
528
  for (let i2 = 0; i2 < 2; i2++) {
495
529
  const half = parseHalf(input, true);
496
530
  if (half === null) break;
497
531
  output.pitch += half;
532
+ output.rootFifth += half * HALF_FIFTH;
498
533
  }
499
534
  return parseBase(input, output);
500
535
  };
536
+ var noteFifth = (out, semi, fifth) => {
537
+ out.relFifths.set((semi % 12 + 12) % 12, fifth);
538
+ };
501
539
  var MAJOR = [0, 4, 7];
502
540
  var DIM = [0, 3, 6];
541
+ var BASE_FIFTHS = /* @__PURE__ */ new Map();
542
+ var MINOR = [0, 3, 7];
543
+ var AUG = [0, 4, 8];
544
+ var HALF_DIM = [0, 3, 6, 10];
545
+ BASE_FIFTHS.set(MAJOR, [0, 4, 1]);
546
+ BASE_FIFTHS.set(MINOR, [0, -3, 1]);
547
+ BASE_FIFTHS.set(DIM, [0, -3, -6]);
548
+ BASE_FIFTHS.set(AUG, [0, 4, 8]);
549
+ BASE_FIFTHS.set(HALF_DIM, [0, -3, -6, -2]);
503
550
  var baseMatcher = new Matcher();
504
- baseMatcher.set(["m", "min", "Min", "minor", "Minor", "-"], [0, 3, 7]);
551
+ baseMatcher.set(["m", "min", "Min", "minor", "Minor", "-"], MINOR);
505
552
  baseMatcher.set(["dim", "\u3007"], DIM);
506
- baseMatcher.set("+", [0, 4, 8]);
507
- baseMatcher.set(["\u03A6", "\u03C6", "\xF8"], [0, 3, 6, 10]);
553
+ baseMatcher.set("+", AUG);
554
+ baseMatcher.set(["\u03A6", "\u03C6", "\xF8"], HALF_DIM);
508
555
  var parseBase = (input, output) => {
509
556
  const isMajMarker = /^maj/i.test(input.str.slice(input.idx));
510
557
  const res = isMajMarker ? null : baseMatcher.parse(input);
511
558
  if (res !== null) output.isChord = true;
512
- output.chord = new Set(res || MAJOR);
559
+ const base = res || MAJOR;
560
+ output.chord = new Set(base);
561
+ const baseFifths = BASE_FIFTHS.get(base);
562
+ if (baseFifths)
563
+ for (const [i2, semi] of base.entries())
564
+ noteFifth(output, semi, baseFifths[i2]);
513
565
  if (res === DIM) {
514
566
  const { num } = input;
515
567
  const chord = output.chord;
516
- if (num !== null) chord.add(deg2pitch(num) - 2);
568
+ if (num !== null) {
569
+ chord.add(deg2pitch(num) - 2);
570
+ noteFifth(output, deg2pitch(num) - 2, deg2fifth(num) - 2 * HALF_FIFTH);
571
+ }
517
572
  }
518
573
  output.nest = input.nest;
519
574
  return parseTerm(input, output);
520
575
  };
521
- var add = (chord, n, half) => {
522
- chord.add(deg2pitch(n) + half);
576
+ var put = (chord, out, deg, half = 0) => {
577
+ chord.add(deg2pitch(deg) + half);
578
+ noteFifth(out, deg2pitch(deg) + half, deg2fifth(deg) + half * HALF_FIFTH);
523
579
  };
524
- var aug = (chord) => {
580
+ var add = (chord, n, half, out) => {
581
+ put(chord, out, n, half);
582
+ };
583
+ var aug = (chord, out) => {
525
584
  chord.delete(deg2pitch(5));
526
- chord.add(deg2pitch(5) + 1);
585
+ put(chord, out, 5, 1);
527
586
  };
528
- var _7th = (chord, n, _half2, isFlat = false) => {
587
+ var _7th = (chord, n, _half2, out, isFlat = false) => {
529
588
  if (n === 5) chord.delete(deg2pitch(3));
530
- else if (n === 6) chord.add(deg2pitch(6));
531
- else if (n === 69) chord.add(deg2pitch(6)).add(deg2pitch(9));
532
- else {
533
- if (n >= 7) chord.add(deg2pitch(7) + (isFlat ? -1 : 0));
534
- if (n >= 9) chord.add(deg2pitch(9));
535
- if (n >= 11) chord.add(deg2pitch(11));
536
- if (n >= 13) chord.add(deg2pitch(13));
589
+ else if (n === 6) put(chord, out, 6);
590
+ else if (n === 69) {
591
+ put(chord, out, 6);
592
+ put(chord, out, 9);
593
+ } else {
594
+ if (n >= 7) put(chord, out, 7, isFlat ? -1 : 0);
595
+ if (n >= 9) put(chord, out, 9);
596
+ if (n >= 11) put(chord, out, 11);
597
+ if (n >= 13) put(chord, out, 13);
537
598
  }
538
599
  };
539
- var _half = (chord, n, half) => {
600
+ var _half = (chord, n, half, out) => {
540
601
  chord.delete(deg2pitch(n));
541
- chord.add(deg2pitch(n) + half);
602
+ put(chord, out, n, half);
542
603
  };
543
604
  var funcMatcher = new Matcher();
544
605
  funcMatcher.set("add", add);
545
606
  funcMatcher.set(["omit", "no"], (chord, n, half) => {
546
607
  chord.delete(deg2pitch(n) + half);
547
608
  });
548
- funcMatcher.set("sus", (chord, n, half) => {
609
+ funcMatcher.set("sus", (chord, n, half, out) => {
549
610
  chord.delete(deg2pitch(3));
550
- chord.add(deg2pitch(n) + half);
611
+ put(chord, out, n, half);
551
612
  });
552
613
  funcMatcher.set(
553
614
  ["M", "maj", "Maj", "major", "Major", "\u25B3", "\u0394"],
@@ -563,17 +624,18 @@ var parseFunc = (input, output) => {
563
624
  const half = parseHalf(input);
564
625
  const { num } = input;
565
626
  if (num === null) {
566
- if (isAug) aug(chord);
627
+ if (isAug) aug(chord, output);
567
628
  else err(input, "Not found number");
568
629
  }
569
630
  if (half === null) {
570
- if (input.nest === output.nest) _7th(chord, num, 0, true);
571
- else add(chord, num, 0);
631
+ if (input.nest === output.nest)
632
+ _7th(chord, num, 0, output, true);
633
+ else add(chord, num, 0, output);
572
634
  } else {
573
- _half(chord, num, half);
635
+ _half(chord, num, half, output);
574
636
  }
575
637
  } else if (func === aug) {
576
- aug(chord);
638
+ aug(chord, output);
577
639
  } else {
578
640
  output.pending = func;
579
641
  }
@@ -587,7 +649,8 @@ var parsePending = (input, output) => {
587
649
  pending(
588
650
  chord,
589
651
  num,
590
- half === null ? 0 : half
652
+ half === null ? 0 : half,
653
+ output
591
654
  );
592
655
  output.pending = null;
593
656
  return parseTerm(input, output);
@@ -599,12 +662,29 @@ var parseChord = (symbol) => {
599
662
  const pitchClasses = [...new Set(notes.map(toPitchClass))].sort(
600
663
  (a, b) => a - b
601
664
  );
665
+ const rootPitch = output.pitch;
666
+ const rootFifth = output.rootFifth;
667
+ const noteFifths = notes.map((n) => {
668
+ const rel = ((n - rootPitch) % 12 + 12) % 12;
669
+ const f = output.relFifths.get(rel);
670
+ if (f !== void 0) return rootFifth + f;
671
+ let best = 0;
672
+ for (let k = -6; k <= 6; k++) {
673
+ if ((k * 7 % 12 + 12) % 12 === rel) {
674
+ best = k;
675
+ break;
676
+ }
677
+ }
678
+ return rootFifth + best;
679
+ });
602
680
  return {
603
681
  symbol,
604
- root: toPitchClass(output.pitch),
682
+ root: toPitchClass(rootPitch),
605
683
  notes,
606
684
  pitchClasses,
607
- intervals
685
+ intervals,
686
+ rootFifth,
687
+ noteFifths
608
688
  };
609
689
  };
610
690
  var QUALITY_SOURCE = [
@@ -1633,10 +1713,72 @@ var createChannelStrip = (ctx, destination, options = {}) => {
1633
1713
  };
1634
1714
  };
1635
1715
 
1716
+ // src/tuning.ts
1717
+ var UNITS_PER_OCTAVE = 372;
1718
+ var UNITS_PER_SEMITONE = 372 / 12;
1719
+ var UNITS_PER_EDO31_DEGREE = 372 / 31;
1720
+ var CENTS_PER_UNIT = 1200 / UNITS_PER_OCTAVE;
1721
+ var A4_UNITS = 69 * UNITS_PER_SEMITONE;
1722
+ var A4_HZ = 440;
1723
+ var DEFAULT_EDO = 12;
1724
+ var unitsPerStep = (edo) => UNITS_PER_OCTAVE / edo;
1725
+ var midiToUnits = (midi) => midi * UNITS_PER_SEMITONE;
1726
+ var unitsToMidi = (units) => units / UNITS_PER_SEMITONE;
1727
+ var unitsToHz = (units) => A4_HZ * 2 ** ((units - A4_UNITS) / UNITS_PER_OCTAVE);
1728
+ var unitsToMidiDetune = (units) => {
1729
+ const midi = Math.round(units / UNITS_PER_SEMITONE);
1730
+ return {
1731
+ midi,
1732
+ detuneCents: (units - midi * UNITS_PER_SEMITONE) * CENTS_PER_UNIT
1733
+ };
1734
+ };
1735
+ var NATURAL_STEPS = {
1736
+ 12: { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 },
1737
+ 31: { c: 0, d: 5, e: 10, f: 13, g: 18, a: 23, b: 28 }
1738
+ };
1739
+ var chromaticStep = (edo) => edo === 31 ? 2 : 1;
1740
+ var MICRO_STEP = 1;
1741
+ var isNaturalLetter = (ch) => Object.hasOwn(NATURAL_STEPS[12], ch);
1742
+ var naturalStep = (ch, edo) => NATURAL_STEPS[edo][ch] ?? null;
1743
+ var spellingToUnits = (letter, octave, chromatic, micro, edo) => {
1744
+ const step = naturalStep(letter, edo);
1745
+ if (step === null) return null;
1746
+ const perStep = unitsPerStep(edo);
1747
+ const stepsFromC = step + chromatic * chromaticStep(edo) + micro * MICRO_STEP;
1748
+ return (octave + 1) * UNITS_PER_OCTAVE + stepsFromC * perStep;
1749
+ };
1750
+ var FIFTH_STEPS = { 12: 7, 31: 18 };
1751
+ var fifthToStep = (fifthIndex, edo) => {
1752
+ const n = fifthIndex * FIFTH_STEPS[edo];
1753
+ return (n % edo + edo) % edo;
1754
+ };
1755
+ var fifthToUnits = (fifthIndex, edo) => fifthToStep(fifthIndex, edo) * unitsPerStep(edo);
1756
+ var PITCH_ENCODING_VERSION = 2;
1757
+ var pitchV1ToUnits = (pitch) => pitch * UNITS_PER_SEMITONE;
1758
+ var unitsToPitchV1 = (units) => Math.round(units / UNITS_PER_SEMITONE);
1759
+
1636
1760
  // src/chords.ts
1637
1761
  var C3 = 48;
1762
+ var MEANTONE_STEP_BY_SEMITONE = [0, 3, 5, 8, 10, 13, 16, 18, 21, 23, 26, 28];
1763
+ var chordToneToUnits = (relSemitone, relFifth, rootSemitone, rootFifth, edo) => {
1764
+ const perStep = UNITS_PER_OCTAVE / edo;
1765
+ const rootOct = Math.floor(rootSemitone / 12);
1766
+ const rootWithin = (rootSemitone % 12 + 12) % 12;
1767
+ const rootStep = edo === 31 ? (fifthToStep(rootFifth, 31) + MEANTONE_STEP_BY_SEMITONE[rootWithin] - fifthToStep(rootFifth, 31) + 31) % 31 : rootWithin;
1768
+ const relWithin12 = (relSemitone % 12 + 12) % 12;
1769
+ const relOct = Math.round((relSemitone - relWithin12) / 12);
1770
+ const relStep = (fifthToStep(relFifth, edo) - fifthToStep(rootFifth, edo) + edo) % edo;
1771
+ return (rootOct + relOct) * UNITS_PER_OCTAVE + (rootStep + relStep) * perStep;
1772
+ };
1638
1773
  var buildChordPlacements = (options) => {
1639
- const { chordStr, patternType, rootShift, bpm, stepsPerBar } = options;
1774
+ const {
1775
+ chordStr,
1776
+ patternType,
1777
+ rootShift,
1778
+ bpm,
1779
+ stepsPerBar,
1780
+ edo = 12
1781
+ } = options;
1640
1782
  const placements = [];
1641
1783
  if (!chordStr.trim()) return placements;
1642
1784
  const offset = rootShift;
@@ -1665,8 +1807,20 @@ var buildChordPlacements = (options) => {
1665
1807
  for (const group of Object.values(chordGroups)) {
1666
1808
  for (const chord of group) {
1667
1809
  let notes;
1810
+ let toUnits;
1668
1811
  try {
1669
- notes = [...parseChord(`${chord.key}${chord.chord}`).notes];
1812
+ const parsed = parseChord(`${chord.key}${chord.chord}`);
1813
+ notes = [...parsed.notes];
1814
+ const fifthOf = new Map(
1815
+ parsed.notes.map((n, i2) => [n, parsed.noteFifths[i2]])
1816
+ );
1817
+ toUnits = (rel) => chordToneToUnits(
1818
+ rel,
1819
+ fifthOf.get(rel) ?? parsed.rootFifth,
1820
+ C3 + offset,
1821
+ parsed.rootFifth,
1822
+ edo === 31 ? 31 : 12
1823
+ );
1670
1824
  } catch {
1671
1825
  continue;
1672
1826
  }
@@ -1675,7 +1829,7 @@ var buildChordPlacements = (options) => {
1675
1829
  for (const noteOffset of notes) {
1676
1830
  placements.push({
1677
1831
  startStep: chord.whenStep,
1678
- pitch: C3 + noteOffset + offset,
1832
+ pitchUnits: toUnits(noteOffset),
1679
1833
  durationSteps: noteLength,
1680
1834
  velocity: 100
1681
1835
  });
@@ -1685,7 +1839,7 @@ var buildChordPlacements = (options) => {
1685
1839
  notes.forEach((noteOffset, i2) => {
1686
1840
  placements.push({
1687
1841
  startStep: chord.whenStep + i2 * arpInterval,
1688
- pitch: C3 + noteOffset + offset,
1842
+ pitchUnits: toUnits(noteOffset),
1689
1843
  durationSteps: noteLength - i2 * arpInterval,
1690
1844
  velocity: 100
1691
1845
  });
@@ -1695,7 +1849,7 @@ var buildChordPlacements = (options) => {
1695
1849
  notes.forEach((noteOffset, i2) => {
1696
1850
  placements.push({
1697
1851
  startStep: chord.whenStep + i2 * arpInterval,
1698
- pitch: C3 + noteOffset + offset,
1852
+ pitchUnits: toUnits(noteOffset),
1699
1853
  durationSteps: Math.max(12, noteLength - i2 * arpInterval),
1700
1854
  velocity: 100
1701
1855
  });
@@ -1709,7 +1863,7 @@ var buildChordPlacements = (options) => {
1709
1863
  for (const noteOffset of notes) {
1710
1864
  placements.push({
1711
1865
  startStep: syncopatedStep,
1712
- pitch: C3 + noteOffset + offset,
1866
+ pitchUnits: toUnits(noteOffset),
1713
1867
  durationSteps: Math.min(halfBeat, 12),
1714
1868
  velocity: 100
1715
1869
  });
@@ -1728,7 +1882,7 @@ var buildChordPlacements = (options) => {
1728
1882
  for (const noteOffset of notes) {
1729
1883
  placements.push({
1730
1884
  startStep: noteStart,
1731
- pitch: C3 + noteOffset + offset,
1885
+ pitchUnits: toUnits(noteOffset),
1732
1886
  durationSteps: yatsumeLengthSteps,
1733
1887
  velocity: 100
1734
1888
  });
@@ -1740,7 +1894,7 @@ var buildChordPlacements = (options) => {
1740
1894
  const stepOffset = i2 * Math.floor(stepsPerBar / 4);
1741
1895
  placements.push({
1742
1896
  startStep: chord.whenStep + stepOffset,
1743
- pitch: C3 + noteOffset + offset,
1897
+ pitchUnits: toUnits(noteOffset),
1744
1898
  durationSteps: Math.max(12, Math.floor(stepsPerBar / 4)),
1745
1899
  velocity: 100
1746
1900
  });
@@ -1752,8 +1906,20 @@ var buildChordPlacements = (options) => {
1752
1906
  const chordNames = chordStr.split(/[\s,]+/).filter((c) => c);
1753
1907
  chordNames.forEach((chordName, barIndex) => {
1754
1908
  let notes;
1909
+ let toUnits;
1755
1910
  try {
1756
- notes = [...parseChord(chordName).notes];
1911
+ const parsed = parseChord(chordName);
1912
+ notes = [...parsed.notes];
1913
+ const fifthOf = new Map(
1914
+ parsed.notes.map((n, i2) => [n, parsed.noteFifths[i2]])
1915
+ );
1916
+ toUnits = (rel) => chordToneToUnits(
1917
+ rel,
1918
+ fifthOf.get(rel) ?? parsed.rootFifth,
1919
+ C3 + offset,
1920
+ parsed.rootFifth,
1921
+ edo === 31 ? 31 : 12
1922
+ );
1757
1923
  } catch {
1758
1924
  return;
1759
1925
  }
@@ -1763,7 +1929,7 @@ var buildChordPlacements = (options) => {
1763
1929
  const stepOffset = i2 * 3;
1764
1930
  placements.push({
1765
1931
  startStep: startStep + stepOffset,
1766
- pitch: C3 + noteOffset + offset,
1932
+ pitchUnits: toUnits(noteOffset),
1767
1933
  durationSteps: chordLength - stepOffset,
1768
1934
  velocity: 100
1769
1935
  });
@@ -2295,6 +2461,11 @@ var DEFAULT_VELOCITY = 100;
2295
2461
  var DEFAULT_PLAYBACK_VELOCITY = 127;
2296
2462
  var DEFAULT_STEPS_PER_BAR = 192;
2297
2463
  var MML_END_MARKER = "#end;";
2464
+ var PITCH_RANGE_START = 0;
2465
+ var PITCH_RANGE_END = 3937;
2466
+ var unitsPerRow = (edo) => 372 / edo;
2467
+ var keyCountFor = (edo) => Math.floor((PITCH_RANGE_END - PITCH_RANGE_START) / unitsPerRow(edo)) + 1;
2468
+ var KEY_COUNT = keyCountFor(12);
2298
2469
 
2299
2470
  // src/vibrato.ts
2300
2471
  var VIBRATO_MIN_SEC = 0.35;
@@ -2694,7 +2865,8 @@ var FORMANTS = {
2694
2865
  // 撥音(ん)は鼻音寄りの低フォルマント
2695
2866
  N: [250, 1e3]
2696
2867
  };
2697
- var midiToFreq = (m) => 440 * 2 ** ((m - 69) / 12);
2868
+ var unitsToFreq = (units) => 440 * 2 ** ((units - 2139) / 372);
2869
+ var unitsToMidiFloat = (units) => units / 31;
2698
2870
  var createKlattVoice = (ctx, destination, reverbBus, delayBus) => {
2699
2871
  const active = /* @__PURE__ */ new Set();
2700
2872
  const voice = (syllable, e) => {
@@ -2728,7 +2900,7 @@ var createKlattVoice = (ctx, destination, reverbBus, delayBus) => {
2728
2900
  }
2729
2901
  const osc = ctx.createOscillator();
2730
2902
  osc.type = "sawtooth";
2731
- osc.frequency.value = midiToFreq(e.pitch);
2903
+ osc.frequency.value = unitsToFreq(e.pitchUnits);
2732
2904
  const makeFormant = (freq, q2, gainScale) => {
2733
2905
  const filter = ctx.createBiquadFilter();
2734
2906
  filter.type = "bandpass";
@@ -2980,7 +3152,7 @@ var createLocalBackend = async (options) => {
2980
3152
  if (!consonantPcm || !vowelPcm) return null;
2981
3153
  const spliced = spliceCompositePcm(consonantPcm, vowelPcm, KOE_SAMPLE_RATE);
2982
3154
  if (!spliced) return null;
2983
- const targetHz = midiToFreq(pitch);
3155
+ const targetHz = unitsToFreq(pitch);
2984
3156
  const audio = worldline.renderNote({
2985
3157
  pcm: spliced.pcm,
2986
3158
  pitch: vibrato ? vibratoPitchCurve(targetHz, spliced.preMs) : targetHz,
@@ -3009,7 +3181,7 @@ var createLocalBackend = async (options) => {
3009
3181
  if (!pcm || pcm.length === 0) return null;
3010
3182
  const entry = bank.manifest.phonemes[alias];
3011
3183
  const lead = leadInFromEntry(entry);
3012
- const targetHz = midiToFreq(pitch);
3184
+ const targetHz = unitsToFreq(pitch);
3013
3185
  if (worldline) {
3014
3186
  const audio = worldline.renderNote({
3015
3187
  pcm,
@@ -3216,7 +3388,7 @@ var createKoeVoice = async (ctx, destination, options) => {
3216
3388
  backend.pitchTokens,
3217
3389
  syllable,
3218
3390
  prevVowel,
3219
- e.pitch
3391
+ unitsToMidiFloat(e.pitchUnits)
3220
3392
  );
3221
3393
  if (syllable.vowel && syllable.vowel !== "N") prevVowel = syllable.vowel;
3222
3394
  if (!alias) return;
@@ -3224,7 +3396,7 @@ var createKoeVoice = async (ctx, destination, options) => {
3224
3396
  const peak = Math.max(1e-4, e.volume);
3225
3397
  const pan = e.pan ?? 0;
3226
3398
  const durationMs = Math.max(60, e.duration * 1e3);
3227
- void renderInto(alias, e.pitch, durationMs).then((r) => {
3399
+ void renderInto(alias, e.pitchUnits, durationMs).then((r) => {
3228
3400
  if (r)
3229
3401
  schedule(r, t0, peak, pan, e.reverbSend, e.delaySend, e.destination);
3230
3402
  });
@@ -3236,7 +3408,10 @@ var createKoeVoice = async (ctx, destination, options) => {
3236
3408
  backend.pitchTokens,
3237
3409
  syllable,
3238
3410
  prevVowelArg,
3239
- pitch
3411
+ // 多音階バンクのピッチトークン("_G4" 等)は録音の音名なので、
3412
+ // 最寄り選択はMIDIノート番号の尺度で行う。units のまま渡すと常に
3413
+ // 最高音のトークンが選ばれてしまう。
3414
+ unitsToMidiFloat(pitch)
3240
3415
  );
3241
3416
  if (!alias) return null;
3242
3417
  const dMs = Math.max(60, durationMs);
@@ -3268,13 +3443,14 @@ var STREAM_LOOKAHEAD_SEC = 1.5;
3268
3443
  var STREAM_POLL_MS = 100;
3269
3444
  var OCTAVE_UNISON_PEAK_SCALE = 0.6;
3270
3445
  var octaveUnisonOffsets = (mode) => {
3446
+ const OCT = 372;
3271
3447
  switch (mode) {
3272
3448
  case "down":
3273
- return [-12];
3449
+ return [-OCT];
3274
3450
  case "up":
3275
- return [12];
3451
+ return [OCT];
3276
3452
  case "both":
3277
- return [-12, 12];
3453
+ return [-OCT, OCT];
3278
3454
  default:
3279
3455
  return [];
3280
3456
  }
@@ -3484,7 +3660,7 @@ var createSingingVoices = (ctx, destination, options = {}) => {
3484
3660
  const effPeak = (dest ? peak * masterVolumeScalar : peak) * peakScale;
3485
3661
  model(note.syllable, {
3486
3662
  trackId: track.id ?? "",
3487
- pitch,
3663
+ pitchUnits: pitch,
3488
3664
  velocity: 100,
3489
3665
  volume: effPeak,
3490
3666
  when,
@@ -3550,17 +3726,13 @@ var createVoiceRegistry = (models = {}, fallback = "klatt") => {
3550
3726
  };
3551
3727
 
3552
3728
  // src/mml-parser.ts
3553
- var PITCH_MAP = {
3554
- c: 0,
3555
- d: 2,
3556
- e: 4,
3557
- f: 5,
3558
- g: 7,
3559
- a: 9,
3560
- b: 11
3729
+ var NATURAL_STEPS2 = {
3730
+ 12: { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 },
3731
+ 31: { c: 0, d: 5, e: 10, f: 13, g: 18, a: 23, b: 28 }
3561
3732
  };
3733
+ var isNatural = (ch) => Object.hasOwn(NATURAL_STEPS2[12], ch);
3562
3734
  var clamp3 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
3563
- var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode)=([\w-]+)/gi;
3735
+ var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode|edo)=([\w-]+)/gi;
3564
3736
  var TRACK_INST_DIRECTIVE = /#t(\d+)inst=([^#;\r\n]+)/gi;
3565
3737
  var TRACK_COMP_DIRECTIVE = /#t(\d+)comp=(\d+)/gi;
3566
3738
  var TRACK_WIDTH_DIRECTIVE = /#t(\d+)width=(\d+)/gi;
@@ -3610,6 +3782,9 @@ var parseMmlMeta = (mml) => {
3610
3782
  if (m[2] === "simple" || m[2] === "advanced") {
3611
3783
  meta.mode = m[2];
3612
3784
  }
3785
+ } else if (key === "edo") {
3786
+ const e = Number.parseInt(m[2], 10);
3787
+ if (e === 12 || e === 31) meta.edo = e;
3613
3788
  }
3614
3789
  }
3615
3790
  for (const m of mml.matchAll(TRACK_INST_DIRECTIVE)) {
@@ -3712,6 +3887,7 @@ var formatMmlMeta = (meta, space = "") => {
3712
3887
  if (meta.fadeOut !== void 0 && meta.fadeOut !== 0)
3713
3888
  parts.push(`#fadeout=${meta.fadeOut}`);
3714
3889
  if (meta.mode) parts.push(`#mode=${meta.mode}`);
3890
+ if (meta.edo !== void 0 && meta.edo !== 12) parts.push(`#edo=${meta.edo}`);
3715
3891
  if (meta.trackInstruments) {
3716
3892
  for (const [idx, name] of Object.entries(meta.trackInstruments)) {
3717
3893
  if (name) parts.push(`#t${idx}inst=${name}`);
@@ -3786,6 +3962,10 @@ var parseMML = (mml, options = {}) => {
3786
3962
  const endMarkerBase = MML_END_MARKER.replace(/;+$/, "");
3787
3963
  const endRegex = new RegExp(`(?<![cdafgCDAFG])${endMarkerBase}\\b;?`, "gi");
3788
3964
  const fullMML = stripLyrics(noMeta).replace(endRegex, "").replace(/[\n\r]+/g, " ").trim();
3965
+ const edo = meta.edo === 31 ? 31 : 12;
3966
+ const naturals = NATURAL_STEPS2[edo];
3967
+ const unitsPerStep2 = 372 / edo;
3968
+ const chromaticStep2 = edo === 31 ? 2 : 1;
3789
3969
  const parts = fullMML.split(/(@\d+)/).filter((p) => p.trim().length > 0);
3790
3970
  let trackIndex = 0;
3791
3971
  let sourceTrackIndex = 0;
@@ -3833,6 +4013,19 @@ var parseMML = (mml, options = {}) => {
3833
4013
  type
3834
4014
  });
3835
4015
  };
4016
+ const readAccidentals = () => {
4017
+ let delta = 0;
4018
+ while (j < body.length) {
4019
+ const a = body[j];
4020
+ if (a === "#") delta += chromaticStep2;
4021
+ else if (a === "-") delta -= chromaticStep2;
4022
+ else if (a === "+") delta += edo === 31 ? 1 : chromaticStep2;
4023
+ else if (a === "_") delta -= edo === 31 ? 1 : chromaticStep2;
4024
+ else break;
4025
+ j++;
4026
+ }
4027
+ return delta;
4028
+ };
3836
4029
  const parseLength = () => {
3837
4030
  let numStr = "";
3838
4031
  while (j < body.length && /\d/.test(body[j])) {
@@ -3904,17 +4097,11 @@ var parseMML = (mml, options = {}) => {
3904
4097
  const savedOctave = octave;
3905
4098
  while (j < body.length && body[j] !== "]") {
3906
4099
  const c = body[j];
3907
- if (Object.hasOwn(PITCH_MAP, c)) {
3908
- let pitch = PITCH_MAP[c];
4100
+ if (isNatural(c)) {
4101
+ let pitchSteps = naturals[c];
3909
4102
  j++;
3910
- if (j < body.length && (body[j] === "#" || body[j] === "+")) {
3911
- pitch++;
3912
- j++;
3913
- } else if (j < body.length && body[j] === "-") {
3914
- pitch--;
3915
- j++;
3916
- }
3917
- chordNotes.push((octave + 1) * 12 + pitch);
4103
+ pitchSteps += readAccidentals();
4104
+ chordNotes.push((octave + 1) * 372 + pitchSteps * unitsPerStep2);
3918
4105
  } else if (c === ">") {
3919
4106
  octave = Math.min(8, octave + 1);
3920
4107
  j++;
@@ -3940,7 +4127,7 @@ var parseMML = (mml, options = {}) => {
3940
4127
  placements.push({
3941
4128
  trackIndex,
3942
4129
  startStep: currentStep,
3943
- pitch: p,
4130
+ pitchUnits: p,
3944
4131
  durationSteps: Math.max(1, steps),
3945
4132
  velocity
3946
4133
  });
@@ -3948,23 +4135,17 @@ var parseMML = (mml, options = {}) => {
3948
4135
  pushTok("chord", currentStep, Math.max(1, steps), tokStart);
3949
4136
  currentStep += steps;
3950
4137
  octave = savedOctave;
3951
- } else if (Object.hasOwn(PITCH_MAP, ch)) {
3952
- let pitch = PITCH_MAP[ch];
4138
+ } else if (isNatural(ch)) {
4139
+ let pitchSteps = naturals[ch];
3953
4140
  j++;
3954
- if (j < body.length && (body[j] === "#" || body[j] === "+")) {
3955
- pitch++;
3956
- j++;
3957
- } else if (j < body.length && body[j] === "-") {
3958
- pitch--;
3959
- j++;
3960
- }
3961
- const midiPitch = (octave + 1) * 12 + pitch;
4141
+ pitchSteps += readAccidentals();
4142
+ const midiPitch = (octave + 1) * 372 + pitchSteps * unitsPerStep2;
3962
4143
  const steps = parseLength();
3963
4144
  recordContributor();
3964
4145
  placements.push({
3965
4146
  trackIndex,
3966
4147
  startStep: currentStep,
3967
- pitch: midiPitch,
4148
+ pitchUnits: midiPitch,
3968
4149
  durationSteps: Math.max(1, steps),
3969
4150
  velocity
3970
4151
  });
@@ -4096,7 +4277,7 @@ var createSequencer = (options) => {
4096
4277
  maxEndStep = Math.max(maxEndStep, note.startStep + note.durationSteps);
4097
4278
  timeline.push({
4098
4279
  trackId: track.id,
4099
- pitch: note.pitch,
4280
+ pitch: note.pitchUnits,
4100
4281
  volume: track.volume / 100,
4101
4282
  velocity: note.velocity ?? DEFAULT_PLAYBACK_VELOCITY,
4102
4283
  when,
@@ -4159,7 +4340,7 @@ var createSequencer = (options) => {
4159
4340
  const currentVolume = (trackVolumeMap.get(ev.trackId) ?? ev.volume * 100) / 100;
4160
4341
  options.onPlayNote({
4161
4342
  trackId: ev.trackId,
4162
- pitch: ev.pitch,
4343
+ pitchUnits: ev.pitch,
4163
4344
  velocity: ev.velocity,
4164
4345
  volume: currentVolume * velocityVolume,
4165
4346
  when: Math.max(0, _when),
@@ -6479,7 +6660,7 @@ var SONG_DRUM_PATTERNS = {
6479
6660
  };
6480
6661
 
6481
6662
  // src/synth.ts
6482
- var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
6663
+ var freqFromPitch = (pitchUnits) => unitsToHz(pitchUnits);
6483
6664
  var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
6484
6665
  const wave = tone.wave ?? "square";
6485
6666
  const attack = tone.attack ?? 0;
@@ -6495,7 +6676,7 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
6495
6676
  const osc = ctx.createOscillator();
6496
6677
  const gain = ctx.createGain();
6497
6678
  osc.type = wave;
6498
- osc.frequency.value = freqFromPitch(e.pitch);
6679
+ osc.frequency.value = freqFromPitch(e.pitchUnits);
6499
6680
  const t0 = ctx.currentTime + e.when;
6500
6681
  const peak = Math.max(1e-4, 0.06 * e.volume * 1.5 * gainScale);
6501
6682
  if (tone.decay) {
@@ -6600,7 +6781,7 @@ var playPlacements = (placements, options) => {
6600
6781
  id: id++,
6601
6782
  startStep: p.startStep,
6602
6783
  durationSteps: p.durationSteps,
6603
- pitch: p.pitch,
6784
+ pitchUnits: p.pitchUnits,
6604
6785
  velocity: p.velocity
6605
6786
  }));
6606
6787
  return {
@@ -6771,7 +6952,7 @@ var playNote = (options) => {
6771
6952
  const dur = options.duration ?? 1;
6772
6953
  synth.playNote({
6773
6954
  trackId: "melody",
6774
- pitch: options.pitch,
6955
+ pitchUnits: options.pitchUnits,
6775
6956
  velocity: 100,
6776
6957
  volume: vol / 100,
6777
6958
  when: 0,
@@ -6792,7 +6973,7 @@ var playChords = (chordStr, options = {}) => {
6792
6973
  // 伴奏トラック
6793
6974
  startStep: p.startStep,
6794
6975
  durationSteps: p.durationSteps,
6795
- pitch: p.pitch,
6976
+ pitchUnits: p.pitchUnits,
6796
6977
  velocity: p.velocity
6797
6978
  }));
6798
6979
  return playPlacements(placements, {
@@ -7325,38 +7506,66 @@ var DAW_CSS = `
7325
7506
  flex-basis: 100%;
7326
7507
  display: flex;
7327
7508
  flex-wrap: nowrap;
7328
- gap: 3px;
7509
+ gap: 4px;
7510
+ align-items: flex-end;
7329
7511
  }
7330
7512
  .dtm-pill {
7331
7513
  --dtm-pill-color: var(--dtm-primary);
7514
+ position: relative;
7332
7515
  display: inline-flex;
7333
7516
  align-items: center;
7334
7517
  justify-content: center;
7335
7518
  flex: 1 1 0;
7336
7519
  min-width: 0;
7337
- height: 26px;
7520
+ height: 24px;
7338
7521
  padding: 0;
7339
7522
  border: 2px solid var(--c-black);
7340
- background: color-mix(in srgb, var(--dtm-pill-color) 40%, black);
7341
- color: var(--c-white);
7523
+ background: color-mix(in srgb, var(--dtm-pill-color) 22%, #0e121e);
7524
+ color: color-mix(in srgb, var(--c-white) 60%, transparent);
7342
7525
  font-family: var(--dtm-font);
7343
7526
  font-size: 11px;
7344
7527
  font-weight: bold;
7345
7528
  cursor: pointer;
7346
- box-shadow: 2px 2px 0 var(--c-black);
7347
- opacity: 0.7;
7529
+ box-shadow: 1px 1px 0 var(--c-black);
7530
+ opacity: 0.55;
7531
+ transition: opacity 120ms ease, background 120ms ease, transform 120ms ease;
7348
7532
  }
7349
- /* \u30A2\u30AF\u30C6\u30A3\u30D6\u9078\u629E = \u4E0D\u900F\u660E + \u91D1\u67A0 */
7533
+ .dtm-pill:hover:not(.dtm-pill--active) {
7534
+ opacity: 0.85;
7535
+ background: color-mix(in srgb, var(--dtm-pill-color) 45%, #0e121e);
7536
+ color: var(--c-white);
7537
+ }
7538
+ /* \u30A2\u30AF\u30C6\u30A3\u30D6\u9078\u629E = \u9BAE\u660E\u306A\u30C8\u30E9\u30C3\u30AF\u8272 + \u30B4\u30FC\u30EB\u30C9\u67A0 + \u30B0\u30ED\u30FC + \u4E0A\u6607\u7ACB\u4F53\u30BF\u30D6\u8868\u73FE */
7350
7539
  .dtm-pill--active {
7351
7540
  opacity: 1;
7541
+ height: 28px;
7542
+ background: var(--dtm-pill-color);
7543
+ color: #ffffff;
7544
+ text-shadow: 1px 1px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000;
7352
7545
  border-color: var(--dtm-gold);
7353
- box-shadow: 0 0 0 1px var(--dtm-gold), 2px 2px 0 var(--c-black);
7546
+ box-shadow:
7547
+ 0 0 0 1.5px var(--dtm-gold),
7548
+ 0 0 10px color-mix(in srgb, var(--dtm-pill-color) 70%, transparent),
7549
+ 2px 2px 0 var(--c-black);
7550
+ z-index: 1;
7551
+ transform: translateY(-2px);
7354
7552
  }
7355
- .dtm-pill:not(.dtm-pill--active):active { transform: translate(2px,2px); box-shadow: none; }
7553
+ .dtm-pill--active::before {
7554
+ content: '';
7555
+ position: absolute;
7556
+ top: -2px;
7557
+ left: 2px;
7558
+ right: 2px;
7559
+ height: 2px;
7560
+ background: #ffffff;
7561
+ border-radius: 1px;
7562
+ opacity: 0.85;
7563
+ }
7564
+ .dtm-pill:not(.dtm-pill--active):active { transform: translate(1px,1px); box-shadow: none; }
7356
7565
  /* \u518D\u751F\u4E2D\u3001\u5B9F\u969B\u306B\u767A\u97F3\u3057\u305F\u77AC\u9593\u3060\u3051\u70B9\u706F\uFF08\u3069\u306E\u30BF\u30D6\u304C\u4ECA\u9CF4\u3063\u3066\u3044\u308B\u304B\u8996\u899A\u7684\u306B\u5206\u304B\u308B\u3088\u3046\u306B\uFF09 */
7357
7566
  .dtm-pill--sounding {
7358
- background: color-mix(in srgb, var(--dtm-pill-color) 85%, white);
7359
- transition: background 30ms linear;
7567
+ filter: brightness(1.6);
7568
+ transition: filter 30ms linear;
7360
7569
  }
7361
7570
 
7362
7571
  /* \u2500\u2500\u2500 \u30D4\u30A2\u30CE\u30ED\u30FC\u30EB\uFF08\u30C8\u30E9\u30C3\u30AB\u30FC\u98A8\uFF09 \u2500\u2500\u2500 */
@@ -7462,6 +7671,49 @@ var DAW_CSS = `
7462
7671
  .dtm-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
7463
7672
  .dtm-track-body { display: flex; flex-direction: column; gap: 10px; }
7464
7673
 
7674
+ /* \u2500\u2500\u2500 \u30A2\u30AF\u30C6\u30A3\u30D6\u30C8\u30E9\u30C3\u30AF\u60C5\u5831\u30D0\u30CA\u30FC \u2500\u2500\u2500 */
7675
+ .dtm-active-track-banner {
7676
+ display: flex;
7677
+ align-items: center;
7678
+ gap: 8px;
7679
+ padding: 6px 10px;
7680
+ background: color-mix(in srgb, var(--dtm-track-color, var(--dtm-primary)) 16%, var(--dtm-deep));
7681
+ border: 2px solid var(--c-black);
7682
+ border-left: 5px solid var(--dtm-track-color, var(--dtm-primary));
7683
+ box-shadow: 2px 2px 0 var(--c-black);
7684
+ margin-bottom: 2px;
7685
+ }
7686
+ .dtm-active-track-badge {
7687
+ font-family: var(--dtm-font);
7688
+ font-size: 11px;
7689
+ font-weight: bold;
7690
+ background: var(--dtm-track-color, var(--dtm-primary));
7691
+ color: #ffffff;
7692
+ text-shadow: 1px 1px 0 #000, -1px -1px 0 #000;
7693
+ padding: 2px 6px;
7694
+ border: 1px solid var(--c-black);
7695
+ border-radius: 2px;
7696
+ letter-spacing: .06em;
7697
+ }
7698
+ .dtm-active-track-name {
7699
+ font-family: var(--dtm-font);
7700
+ font-size: 12px;
7701
+ font-weight: bold;
7702
+ color: var(--c-white);
7703
+ flex: 1 1 auto;
7704
+ }
7705
+ .dtm-active-track-pill {
7706
+ font-family: var(--dtm-font);
7707
+ font-size: 9px;
7708
+ font-weight: bold;
7709
+ letter-spacing: .08em;
7710
+ padding: 2px 5px;
7711
+ color: var(--dtm-gold);
7712
+ border: 1px solid var(--dtm-gold);
7713
+ background: rgba(0, 0, 0, 0.45);
7714
+ border-radius: 2px;
7715
+ }
7716
+
7465
7717
  /* \u2500\u2500\u2500 MML\u51FA\u529B\uFF08CRT\u30BF\u30FC\u30DF\u30CA\u30EB\uFF09 \u2500\u2500\u2500 */
7466
7718
  .dtm-output {
7467
7719
  background: var(--c-black);
@@ -8878,7 +9130,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
8878
9130
  0
8879
9131
  );
8880
9132
  const timedNotes = placements.map((p) => ({
8881
- pitch: p.pitch,
9133
+ pitch: unitsToPitchV1(p.pitchUnits),
8882
9134
  when: p.startStep * secondsPerStep,
8883
9135
  duration: p.durationSteps * secondsPerStep
8884
9136
  }));
@@ -8911,7 +9163,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
8911
9163
  id: id++,
8912
9164
  startStep: p.startStep,
8913
9165
  durationSteps: p.durationSteps,
8914
- pitch: p.pitch,
9166
+ pitchUnits: p.pitchUnits,
8915
9167
  velocity: DEFAULT_VELOCITY
8916
9168
  }));
8917
9169
  return {
@@ -9736,7 +9988,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9736
9988
  (a, b) => a.startStep - b.startStep
9737
9989
  );
9738
9990
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
9739
- const semis = (lt.octave ?? 0) * 12;
9991
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
9740
9992
  const count = Math.min(sorted.length, lt.syllables.length);
9741
9993
  const notes = [];
9742
9994
  for (let i2 = 0; i2 < count; i2++) {
@@ -9744,7 +9996,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9744
9996
  if (n.startStep < fromStep) continue;
9745
9997
  notes.push({
9746
9998
  syllable: lt.syllables[i2],
9747
- pitch: n.pitch + semis,
9999
+ pitch: n.pitchUnits + semis,
9748
10000
  startSec: (n.startStep - fromStep) * secondsPerStep,
9749
10001
  durationSec: n.durationSteps * secondsPerStep * gate
9750
10002
  });
@@ -10019,6 +10271,7 @@ var SoundFont = class _SoundFont {
10019
10271
  pitch = 60,
10020
10272
  volume = 1,
10021
10273
  velocity,
10274
+ detuneCents = 0,
10022
10275
  when = 0,
10023
10276
  duration = 1
10024
10277
  } = {}) {
@@ -10038,7 +10291,7 @@ var SoundFont = class _SoundFont {
10038
10291
  Object.assign(src, _param.src);
10039
10292
  const humanizeCents = (Math.random() * 2 - 1) * _SoundFont.humanizeDetuneCents;
10040
10293
  const humanizeGainMul = 1 + (Math.random() * 2 - 1) * _SoundFont.humanizeGainRatio;
10041
- src.detune.setValueAtTime(humanizeCents, 0);
10294
+ src.detune.setValueAtTime(humanizeCents + detuneCents, 0);
10042
10295
  const effectiveVolume = volume * humanizeGainMul;
10043
10296
  const brightness = velocity === void 0 ? 1 : Math.max(0, Math.min(1, velocity / 127));
10044
10297
  const filter = brightness >= _SoundFont.brightnessBypassAbove ? void 0 : ctx.createBiquadFilter();
@@ -11177,7 +11430,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11177
11430
  scheduleMetronomeBeats(ctx, cutGain);
11178
11431
  };
11179
11432
  const wafPlayDynamic = ({
11180
- pitch,
11433
+ pitchUnits,
11181
11434
  velocity,
11182
11435
  volume,
11183
11436
  when,
@@ -11187,10 +11440,12 @@ var mountChordPlayer = (target, chords, options = {}) => {
11187
11440
  }) => {
11188
11441
  const waf = activeGmName ? _wafCache.get(activeGmName) : null;
11189
11442
  if (waf) {
11443
+ const { midi, detuneCents } = unitsToMidiDetune(pitchUnits);
11190
11444
  waf.play({
11191
11445
  ctx,
11192
11446
  destination,
11193
- pitch,
11447
+ pitch: midi,
11448
+ detuneCents,
11194
11449
  volume: volume * 0.85,
11195
11450
  velocity,
11196
11451
  when,
@@ -11200,7 +11455,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11200
11455
  const synth = createSynth(ctx, destination);
11201
11456
  synth.playNote({
11202
11457
  trackId: "chord",
11203
- pitch,
11458
+ pitchUnits,
11204
11459
  velocity: 100,
11205
11460
  volume,
11206
11461
  when,
@@ -11229,7 +11484,8 @@ var mountChordPlayer = (target, chords, options = {}) => {
11229
11484
  trackIndex: 3,
11230
11485
  startStep: whenStep,
11231
11486
  durationSteps,
11232
- pitch: C32 + noteOffset,
11487
+ // chord-parser の構成音は12平均律の半音。内部表現の units へ写す。
11488
+ pitchUnits: pitchV1ToUnits(C32 + noteOffset),
11233
11489
  velocity: 100
11234
11490
  });
11235
11491
  }
@@ -11242,7 +11498,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11242
11498
  synth: false,
11243
11499
  onPlayNote: (e) => {
11244
11500
  wafPlayDynamic({
11245
- pitch: e.pitch,
11501
+ pitchUnits: e.pitchUnits,
11246
11502
  velocity: e.velocity,
11247
11503
  volume: e.volume,
11248
11504
  when: e.when,
@@ -11684,6 +11940,14 @@ var buildUI = (target, options) => {
11684
11940
  <summary>\u5168\u4F53\u30C8\u30E9\u30C3\u30AF\u8A2D\u5B9A</summary>
11685
11941
  <div class="dtm-panel-body">
11686
11942
  <div data-dtm="preset-select-slot"></div>
11943
+ <div class="dtm-row">
11944
+ <span class="dtm-label">\u97F3\u5F8B</span>
11945
+ <select class="dtm-select dtm-grow" data-dtm="edo-select">
11946
+ <option value="12">12\u5E73\u5747\u5F8B\uFF08\u901A\u5E38\uFF09</option>
11947
+ <option value="31">31\u5E73\u5747\u5F8B\uFF08\u5FAE\u5206\u97F3\uFF09</option>
11948
+ </select>
11949
+ <button class="dtm-infobtn" data-dtm="edo-info" title="\u97F3\u5F8B\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
11950
+ </div>
11687
11951
  <div class="dtm-row">
11688
11952
  <span class="dtm-label">\u5168\u4F53\u97F3\u91CF</span>
11689
11953
  <input type="range" class="dtm-range dtm-grow" data-dtm="master-volume" value="50" min="0" max="100">
@@ -12016,6 +12280,8 @@ var buildUI = (target, options) => {
12016
12280
  copyMiniBtn: sel("copy-mini"),
12017
12281
  overlay: sel("overlay"),
12018
12282
  mmlInfoBtn: sel("mml-info"),
12283
+ edoSelect: sel("edo-select"),
12284
+ edoInfoBtn: sel("edo-info"),
12019
12285
  modalOverlay: sel("modal-overlay"),
12020
12286
  modalTitle: sel("modal-title"),
12021
12287
  modalBody: sel("modal-body"),
@@ -12136,17 +12402,21 @@ var SCALES = [
12136
12402
  [0, 2, 4, 7, 9]
12137
12403
  // Pentatonic Major
12138
12404
  ];
12405
+ var MEANTONE_STEP_BY_SEMITONE2 = [0, 3, 5, 8, 10, 13, 16, 18, 21, 23, 26, 28];
12139
12406
  var generateRandomPattern = (core, options) => {
12140
- const { stepsPerBar, startStep, pitchRangeStart } = options;
12407
+ const { stepsPerBar, startStep, pitchRangeStart, edo = 12 } = options;
12141
12408
  const numBars = 8;
12142
12409
  const noteLength = 24;
12143
- const basePitch = pitchRangeStart + 60;
12410
+ const semitoneToStep = (semi) => edo === 31 ? MEANTONE_STEP_BY_SEMITONE2[semi] : semi;
12411
+ const upr = UNITS_PER_OCTAVE / edo;
12412
+ const basePitch = pitchRangeStart + 60 * UNITS_PER_SEMITONE;
12144
12413
  const scale = SCALES[Math.floor(Math.random() * SCALES.length)];
12145
12414
  const rootOffset = Math.floor(Math.random() * 12);
12146
12415
  const availablePitches = [];
12147
12416
  for (let i2 = 0; i2 < 12; i2++) {
12148
12417
  const noteInOctave = (i2 - rootOffset + 12) % 12;
12149
- if (scale.includes(noteInOctave)) availablePitches.push(basePitch + i2);
12418
+ if (scale.includes(noteInOctave))
12419
+ availablePitches.push(basePitch + semitoneToStep(i2) * upr);
12150
12420
  }
12151
12421
  core.beginBatch();
12152
12422
  for (let bar = 0; bar < numBars; bar++) {
@@ -12181,14 +12451,17 @@ var applyHarmonicFilter = (targetCore, chordCore, options) => {
12181
12451
  const isNewBar = halfBar % 2 === 0;
12182
12452
  const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12183
12453
  if (chordHere.length > 0) {
12184
- currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
12454
+ currentClasses = new Set(
12455
+ chordHere.map((n) => n.pitchUnits % UNITS_PER_OCTAVE)
12456
+ );
12185
12457
  } else if (isNewBar) {
12186
12458
  currentClasses = /* @__PURE__ */ new Set();
12187
12459
  }
12188
12460
  if (currentClasses.size === 0) continue;
12189
12461
  const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12190
12462
  for (const n of activeHere) {
12191
- if (!currentClasses.has(n.pitch % 12)) targetCore.deleteNoteById(n.id);
12463
+ if (!currentClasses.has(n.pitchUnits % UNITS_PER_OCTAVE))
12464
+ targetCore.deleteNoteById(n.id);
12192
12465
  }
12193
12466
  }
12194
12467
  targetCore.endBatch();
@@ -12210,13 +12483,17 @@ var applyMonophonic = (targetCore, chordCore, options) => {
12210
12483
  const isNewBar = halfBar % 2 === 0;
12211
12484
  const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12212
12485
  if (chordHere.length > 0) {
12213
- currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
12486
+ currentClasses = new Set(
12487
+ chordHere.map((n) => n.pitchUnits % UNITS_PER_OCTAVE)
12488
+ );
12214
12489
  } else if (isNewBar) {
12215
12490
  currentClasses = /* @__PURE__ */ new Set();
12216
12491
  }
12217
12492
  if (currentClasses.size === 0) continue;
12218
12493
  const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12219
- const filtered = activeHere.filter((n) => currentClasses.has(n.pitch % 12));
12494
+ const filtered = activeHere.filter(
12495
+ (n) => currentClasses.has(n.pitchUnits % UNITS_PER_OCTAVE)
12496
+ );
12220
12497
  const filteredIds = new Set(filtered.map((n) => n.id));
12221
12498
  for (const n of activeHere) {
12222
12499
  if (!filteredIds.has(n.id)) targetCore.deleteNoteById(n.id);
@@ -12228,7 +12505,7 @@ var applyMonophonic = (targetCore, chordCore, options) => {
12228
12505
  }
12229
12506
  for (const notesAtTime of timeMap.values()) {
12230
12507
  if (notesAtTime.length > 1) {
12231
- notesAtTime.sort((a, b) => b.pitch - a.pitch);
12508
+ notesAtTime.sort((a, b) => b.pitchUnits - a.pitchUnits);
12232
12509
  const [, ...others] = notesAtTime;
12233
12510
  for (const on of others) targetCore.deleteNoteById(on.id);
12234
12511
  }
@@ -12244,18 +12521,21 @@ var shiftNotes = (cores, shiftSteps) => {
12244
12521
  for (const note of notes) {
12245
12522
  const newStart = note.startStep + shiftSteps;
12246
12523
  if (newStart < 0) core.deleteNoteById(note.id);
12247
- else core.moveNote(note.id, newStart, note.pitch);
12524
+ else core.moveNote(note.id, newStart, note.pitchUnits);
12248
12525
  }
12249
12526
  core.saveHistory();
12250
12527
  }
12251
12528
  };
12252
- var transposeNotes = (cores, semitones) => {
12253
- if (semitones === 0) return;
12529
+ var transposeNotes = (cores, steps) => {
12530
+ if (steps === 0) return;
12254
12531
  for (const core of cores) {
12255
12532
  const notes = [...core.getNotes()];
12256
12533
  for (const note of notes) {
12257
- const newPitch = Math.max(0, Math.min(127, note.pitch + semitones));
12258
- if (newPitch !== note.pitch) {
12534
+ const newPitch = Math.max(
12535
+ PITCH_RANGE_START,
12536
+ Math.min(PITCH_RANGE_END, note.pitchUnits + steps)
12537
+ );
12538
+ if (newPitch !== note.pitchUnits) {
12259
12539
  core.moveNote(note.id, note.startStep, newPitch);
12260
12540
  }
12261
12541
  }
@@ -12580,11 +12860,31 @@ var exportMIDI = (options) => {
12580
12860
  const div = 480;
12581
12861
  const tickPerStep = div / STEPS_PER_BEAT3;
12582
12862
  const midiTracks = [];
12583
- tracks.forEach((track, ch) => {
12863
+ const NOTE_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15];
12864
+ const BEND_RANGE_SEMITONES = 2;
12865
+ const bendKey = (cents) => Math.round(cents * 100);
12866
+ const bendUse = /* @__PURE__ */ new Map();
12867
+ for (const track of tracks) {
12868
+ for (const n of track.notes) {
12869
+ const { detuneCents } = unitsToMidiDetune(n.pitchUnits);
12870
+ const k = bendKey(detuneCents);
12871
+ bendUse.set(k, (bendUse.get(k) ?? 0) + 1);
12872
+ }
12873
+ }
12874
+ const bendChannel = /* @__PURE__ */ new Map();
12875
+ const ordered = [...bendUse.entries()].sort((a, b) => b[1] - a[1]);
12876
+ for (const [k] of ordered) {
12877
+ if (bendChannel.size >= NOTE_CHANNELS.length) break;
12878
+ bendChannel.set(k, NOTE_CHANNELS[bendChannel.size]);
12879
+ }
12880
+ tracks.forEach((track) => {
12584
12881
  if (track.notes.length === 0) return;
12585
- const channel = ch < 9 ? ch : ch + 1 & 15;
12586
12882
  const events = [];
12587
12883
  for (const n of track.notes) {
12884
+ const { midi, detuneCents } = unitsToMidiDetune(n.pitchUnits);
12885
+ const k = bendKey(detuneCents);
12886
+ const channel = bendChannel.get(k) ?? bendChannel.get(0) ?? 0;
12887
+ const note = Math.max(0, Math.min(127, midi));
12588
12888
  const startTick = Math.round(n.startStep * tickPerStep);
12589
12889
  const endTick = Math.round(
12590
12890
  (n.startStep + (n.durationSteps || 1)) * tickPerStep
@@ -12592,12 +12892,25 @@ var exportMIDI = (options) => {
12592
12892
  const vel = Math.round(
12593
12893
  (n.velocity ?? DEFAULT_VELOCITY) * (track.volume ?? 100) / 100
12594
12894
  );
12595
- events.push({ t: startTick, m: [144 | channel, n.pitch, vel] });
12596
- events.push({ t: endTick, m: [144 | channel, n.pitch, 0] });
12895
+ events.push({ t: startTick, m: [144 | channel, note, vel] });
12896
+ events.push({ t: endTick, m: [144 | channel, note, 0] });
12597
12897
  }
12598
12898
  events.sort((a, b) => a.t - b.t);
12599
12899
  midiTracks.push(events);
12600
12900
  });
12901
+ const bendSetup = [];
12902
+ for (const [k, channel] of bendChannel) {
12903
+ const cents = k / 100;
12904
+ bendSetup.push({ t: 0, m: [176 | channel, 101, 0] });
12905
+ bendSetup.push({ t: 0, m: [176 | channel, 100, 0] });
12906
+ bendSetup.push({ t: 0, m: [176 | channel, 6, BEND_RANGE_SEMITONES] });
12907
+ bendSetup.push({ t: 0, m: [176 | channel, 38, 0] });
12908
+ bendSetup.push({ t: 0, m: [176 | channel, 101, 127] });
12909
+ bendSetup.push({ t: 0, m: [176 | channel, 100, 127] });
12910
+ const raw = 8192 + Math.round(cents / (BEND_RANGE_SEMITONES * 100) * 8192);
12911
+ const v = Math.max(0, Math.min(16383, raw));
12912
+ bendSetup.push({ t: 0, m: [224 | channel, v & 127, v >> 7 & 127] });
12913
+ }
12601
12914
  const maxStep = Math.max(
12602
12915
  ...tracks.filter((t) => t.notes.length > 0).map(
12603
12916
  (t) => Math.max(...t.notes.map((n) => n.startStep + n.durationSteps))
@@ -12634,6 +12947,7 @@ var exportMIDI = (options) => {
12634
12947
  headerChunks(arr, midiTracks.length + 1, div);
12635
12948
  trackChunks(arr, (a) => {
12636
12949
  a.push(0, 255, 81, 3, ...to3byte(Math.round(6e7 / bpm)));
12950
+ for (const ev of bendSetup) a.push(0, ...ev.m);
12637
12951
  });
12638
12952
  for (const events of midiTracks) {
12639
12953
  trackChunks(arr, (a) => {
@@ -12880,380 +13194,8 @@ var LinkedList = class {
12880
13194
  }
12881
13195
  };
12882
13196
 
12883
- // src/renderer.ts
12884
- var g_header_canvas;
12885
- var g_key_canvas;
12886
- var g_grid_canvas;
12887
- var g_header_ctx;
12888
- var g_key_ctx;
12889
- var g_grid_ctx;
12890
- var g_config;
12891
- var KEYBOARD_WIDTH = 60;
12892
- var HEADER_HEIGHT = 20;
12893
- var getRenderConfig = () => g_config;
12894
- var g_draw_offset_x = 0;
12895
- var g_draw_offset_y = 0;
12896
- var g_bg_active = false;
12897
- var setBackgroundActive = (active) => {
12898
- g_bg_active = active;
12899
- };
12900
- var getDrawOffset = () => ({
12901
- x: g_draw_offset_x,
12902
- y: g_draw_offset_y
12903
- });
12904
- var getGridCanvas = () => g_grid_canvas;
12905
- var getGridContext = () => g_grid_ctx;
12906
- var getHeaderCanvas = () => g_header_canvas;
12907
- var init = (mountTarget, width = 800, height = 450, config) => {
12908
- g_config = config;
12909
- const headerCanvas = document.createElement("canvas");
12910
- g_header_canvas = headerCanvas;
12911
- headerCanvas.width = width - KEYBOARD_WIDTH;
12912
- headerCanvas.height = HEADER_HEIGHT;
12913
- headerCanvas.style.position = "absolute";
12914
- headerCanvas.style.left = `${KEYBOARD_WIDTH}px`;
12915
- headerCanvas.style.top = "0px";
12916
- const headerCtx = headerCanvas.getContext("2d");
12917
- if (!headerCtx)
12918
- throw new Error("Failed to get 2D rendering context for header.");
12919
- g_header_ctx = headerCtx;
12920
- const keyCanvas = document.createElement("canvas");
12921
- g_key_canvas = keyCanvas;
12922
- keyCanvas.width = KEYBOARD_WIDTH;
12923
- keyCanvas.height = height - HEADER_HEIGHT;
12924
- keyCanvas.style.position = "absolute";
12925
- keyCanvas.style.left = "0px";
12926
- keyCanvas.style.top = `${HEADER_HEIGHT}px`;
12927
- const keyCtx = keyCanvas.getContext("2d");
12928
- if (!keyCtx)
12929
- throw new Error("Failed to get 2D rendering context for keyboard.");
12930
- g_key_ctx = keyCtx;
12931
- const gridCanvas = document.createElement("canvas");
12932
- g_grid_canvas = gridCanvas;
12933
- gridCanvas.width = width - KEYBOARD_WIDTH;
12934
- gridCanvas.height = height - HEADER_HEIGHT;
12935
- gridCanvas.style.position = "absolute";
12936
- gridCanvas.style.left = `${KEYBOARD_WIDTH}px`;
12937
- gridCanvas.style.top = `${HEADER_HEIGHT}px`;
12938
- gridCanvas.style.touchAction = "none";
12939
- gridCanvas.style.userSelect = "none";
12940
- const gridCtx = gridCanvas.getContext("2d", { willReadFrequently: true });
12941
- if (!gridCtx) throw new Error("Failed to get 2D rendering context for grid.");
12942
- g_grid_ctx = gridCtx;
12943
- mountTarget.innerHTML = "";
12944
- mountTarget.style.position = "relative";
12945
- mountTarget.style.width = `${width + KEYBOARD_WIDTH}px`;
12946
- mountTarget.style.height = `${height}px`;
12947
- mountTarget.append(headerCanvas, keyCanvas, gridCanvas);
12948
- drawHeaderCorner();
12949
- };
12950
- var blackKeyPitches = /* @__PURE__ */ new Set([1, 3, 6, 8, 10]);
12951
- var KEY_NAMES = [
12952
- "C",
12953
- "C#",
12954
- "D",
12955
- "D#",
12956
- "E",
12957
- "F",
12958
- "F#",
12959
- "G",
12960
- "G#",
12961
- "A",
12962
- "A#",
12963
- "B"
12964
- ];
12965
- var drawHeaderCorner = () => {
12966
- const mountTarget = g_key_canvas.parentElement;
12967
- if (!mountTarget) return;
12968
- let cornerDiv = mountTarget.querySelector("#header-corner");
12969
- if (!cornerDiv) {
12970
- cornerDiv = document.createElement("div");
12971
- cornerDiv.id = "header-corner";
12972
- cornerDiv.style.position = "absolute";
12973
- cornerDiv.style.left = "0px";
12974
- cornerDiv.style.top = "0px";
12975
- cornerDiv.style.width = `${KEYBOARD_WIDTH}px`;
12976
- cornerDiv.style.height = `${HEADER_HEIGHT}px`;
12977
- cornerDiv.style.backgroundColor = "#0a0f1f";
12978
- cornerDiv.style.borderRight = "2px solid #29adff";
12979
- cornerDiv.style.borderBottom = "2px solid #29adff";
12980
- mountTarget.insertBefore(cornerDiv, g_header_canvas);
12981
- }
12982
- };
12983
- var drawKeyboard = () => {
12984
- g_key_ctx.clearRect(0, 0, g_key_canvas.width, g_key_canvas.height);
12985
- const { keyHeight, keyCount, pitchRangeStart } = g_config;
12986
- const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
12987
- const endY = g_draw_offset_y + g_key_canvas.height;
12988
- const WHITE_KEY = "#ccc8b4";
12989
- const BLACK_KEY = "#111111";
12990
- const BK_EDGE = "#383838";
12991
- const WW_SEP = "#807a6a";
12992
- const BK_RATIO = 0.62;
12993
- for (let y = startY; y < endY; y += keyHeight) {
12994
- const pitchIndex = keyCount - 1 - y / keyHeight;
12995
- const totalPitch = pitchIndex + pitchRangeStart;
12996
- const pitchMod12 = totalPitch % 12;
12997
- const isBlackKey = blackKeyPitches.has(pitchMod12);
12998
- const octave = Math.floor(totalPitch / 12) - 1;
12999
- const isC4Range = octave === 4;
13000
- const screenY = y - g_draw_offset_y;
13001
- const bkW = Math.floor(KEYBOARD_WIDTH * BK_RATIO);
13002
- if (isBlackKey) {
13003
- g_key_ctx.fillStyle = isC4Range ? "#d8d4be" : WHITE_KEY;
13004
- g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
13005
- g_key_ctx.fillStyle = isC4Range ? "#1a1408" : BLACK_KEY;
13006
- g_key_ctx.fillRect(0, screenY, bkW, keyHeight);
13007
- g_key_ctx.strokeStyle = BK_EDGE;
13008
- g_key_ctx.lineWidth = 1;
13009
- g_key_ctx.beginPath();
13010
- g_key_ctx.moveTo(bkW, screenY);
13011
- g_key_ctx.lineTo(bkW, screenY + keyHeight);
13012
- g_key_ctx.stroke();
13013
- } else {
13014
- g_key_ctx.fillStyle = isC4Range ? "#dedad0" : WHITE_KEY;
13015
- g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
13016
- if (pitchMod12 === 5 || pitchMod12 === 0) {
13017
- g_key_ctx.strokeStyle = WW_SEP;
13018
- g_key_ctx.lineWidth = 1;
13019
- g_key_ctx.beginPath();
13020
- g_key_ctx.moveTo(0, screenY + keyHeight - 0.5);
13021
- g_key_ctx.lineTo(KEYBOARD_WIDTH, screenY + keyHeight - 0.5);
13022
- g_key_ctx.stroke();
13023
- }
13024
- }
13025
- if (pitchMod12 === 0) {
13026
- const octave2 = Math.floor(totalPitch / 12) - 1;
13027
- g_key_ctx.fillStyle = "#555040";
13028
- g_key_ctx.font = "10px 'k8x12',monospace";
13029
- g_key_ctx.textAlign = "right";
13030
- g_key_ctx.textBaseline = "bottom";
13031
- g_key_ctx.fillText(
13032
- `${KEY_NAMES[pitchMod12]}${octave2}`,
13033
- KEYBOARD_WIDTH - 4,
13034
- screenY + keyHeight - 2
13035
- );
13036
- }
13037
- }
13038
- g_key_ctx.beginPath();
13039
- g_key_ctx.strokeStyle = "#29adff";
13040
- g_key_ctx.lineWidth = 2;
13041
- g_key_ctx.moveTo(KEYBOARD_WIDTH, 0);
13042
- g_key_ctx.lineTo(KEYBOARD_WIDTH, g_key_canvas.height);
13043
- g_key_ctx.stroke();
13044
- };
13045
- var drawHeader = () => {
13046
- g_header_ctx.clearRect(0, 0, g_header_canvas.width, g_header_canvas.height);
13047
- const { stepWidth, stepsPerBar } = g_config;
13048
- g_header_ctx.save();
13049
- g_header_ctx.translate(-g_draw_offset_x, 0);
13050
- g_header_ctx.fillStyle = g_bg_active ? "rgba(10,15,31,0.55)" : "#0a0f1f";
13051
- g_header_ctx.fillRect(
13052
- g_draw_offset_x,
13053
- 0,
13054
- g_header_canvas.width,
13055
- HEADER_HEIGHT
13056
- );
13057
- g_header_ctx.strokeStyle = "#3d405b";
13058
- g_header_ctx.lineWidth = 1;
13059
- g_header_ctx.font = "11px 'k8x12',monospace";
13060
- g_header_ctx.fillStyle = "#83769c";
13061
- const startBar = Math.floor(g_draw_offset_x / (stepsPerBar * stepWidth));
13062
- const endBar = Math.ceil(
13063
- (g_draw_offset_x + g_header_canvas.width) / (stepsPerBar * stepWidth)
13064
- );
13065
- for (let bar = startBar; bar <= endBar + 1; bar++) {
13066
- const x2 = bar * stepsPerBar * stepWidth;
13067
- const screenX = x2;
13068
- g_header_ctx.beginPath();
13069
- g_header_ctx.moveTo(screenX, 0);
13070
- g_header_ctx.lineTo(screenX, HEADER_HEIGHT);
13071
- g_header_ctx.stroke();
13072
- if (bar >= 0) {
13073
- g_header_ctx.textAlign = "left";
13074
- g_header_ctx.textBaseline = "middle";
13075
- g_header_ctx.fillText(`${bar + 1}`, screenX + 5, HEADER_HEIGHT / 2);
13076
- }
13077
- }
13078
- g_header_ctx.restore();
13079
- };
13080
- var drawGrid = (noteLengthSteps = 1) => {
13081
- drawKeyboard();
13082
- drawHeader();
13083
- g_grid_ctx.clearRect(0, 0, g_grid_canvas.width, g_grid_canvas.height);
13084
- const { keyHeight, keyCount, stepWidth, stepsPerBar, pitchRangeStart } = g_config;
13085
- const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
13086
- const endY = g_draw_offset_y + g_grid_canvas.height;
13087
- for (let y = startY; y < endY; y += keyHeight) {
13088
- const pitchIndex = keyCount - 1 - y / keyHeight;
13089
- const totalPitch = pitchIndex + pitchRangeStart;
13090
- const pitchMod12 = totalPitch % 12;
13091
- const isBlackKey = blackKeyPitches.has(pitchMod12);
13092
- const isC = pitchMod12 === 0;
13093
- const octave = Math.floor(totalPitch / 12) - 1;
13094
- const isC4Range = octave === 4;
13095
- const screenY = y - g_draw_offset_y;
13096
- g_grid_ctx.fillStyle = g_bg_active ? isBlackKey ? "rgba(8,11,22,0.55)" : "rgba(17,22,40,0.45)" : isBlackKey ? "#080b16" : "#111628";
13097
- g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13098
- if (isC4Range) {
13099
- g_grid_ctx.fillStyle = "rgba(41,173,255,0.05)";
13100
- g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13101
- }
13102
- g_grid_ctx.beginPath();
13103
- g_grid_ctx.strokeStyle = isC ? "#3d405b" : "#1a1d30";
13104
- g_grid_ctx.lineWidth = 1;
13105
- const lineY = screenY + keyHeight;
13106
- g_grid_ctx.moveTo(0, lineY);
13107
- g_grid_ctx.lineTo(g_grid_canvas.width, lineY);
13108
- g_grid_ctx.stroke();
13109
- }
13110
- const gridStep = noteLengthSteps || 48;
13111
- const startX = Math.floor(g_draw_offset_x / (stepWidth * gridStep)) * stepWidth * gridStep;
13112
- const endX = g_draw_offset_x + g_grid_canvas.width;
13113
- const lineStep = stepWidth * gridStep;
13114
- for (let x2 = startX; x2 <= endX; x2 += lineStep) {
13115
- const step = x2 / stepWidth;
13116
- const isBarLine = step % stepsPerBar === 0;
13117
- const isNoteLine = step % gridStep === 0;
13118
- const screenX = x2 - g_draw_offset_x;
13119
- g_grid_ctx.beginPath();
13120
- g_grid_ctx.strokeStyle = isBarLine ? "#3d405b" : isNoteLine ? "#242840" : "#1a1d30";
13121
- g_grid_ctx.lineWidth = isBarLine ? 2 : 1;
13122
- g_grid_ctx.moveTo(screenX, 0);
13123
- g_grid_ctx.lineTo(screenX, g_grid_canvas.height);
13124
- g_grid_ctx.stroke();
13125
- }
13126
- };
13127
- var drawNotes = (notes, color = [59, 130, 246, 1]) => {
13128
- const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
13129
- for (const note of notes) {
13130
- const logicalX = note.startStep * stepWidth;
13131
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
13132
- const logicalY = yIndex * keyHeight;
13133
- const w = note.durationSteps * stepWidth;
13134
- const h = keyHeight;
13135
- const renderX = logicalX - g_draw_offset_x;
13136
- const renderY = logicalY - g_draw_offset_y;
13137
- const velocityOpacity = note.velocity !== void 0 ? 0.5 + note.velocity / 127 * 0.5 : 1;
13138
- const [r, g, b, a] = color;
13139
- const finalOpacity = a * velocityOpacity;
13140
- g_grid_ctx.fillStyle = `rgba(${r},${g},${b},${finalOpacity})`;
13141
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13142
- }
13143
- };
13144
- var drawNoteLyrics = (notes, syllables) => {
13145
- if (syllables.length === 0) return;
13146
- const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
13147
- if (keyHeight < 10) return;
13148
- const fontSize = Math.min(12, Math.floor(keyHeight * 0.85));
13149
- const sorted = [...notes].sort((a, b) => a.startStep - b.startStep);
13150
- const count = Math.min(sorted.length, syllables.length);
13151
- g_grid_ctx.save();
13152
- g_grid_ctx.font = `${fontSize}px 'k8x12',sans-serif`;
13153
- g_grid_ctx.textAlign = "left";
13154
- g_grid_ctx.textBaseline = "middle";
13155
- g_grid_ctx.lineWidth = 3;
13156
- g_grid_ctx.lineJoin = "round";
13157
- for (let i2 = 0; i2 < count; i2++) {
13158
- const kana = syllables[i2];
13159
- if (!kana) continue;
13160
- const note = sorted[i2];
13161
- const renderX = note.startStep * stepWidth - g_draw_offset_x;
13162
- const renderY = (keyCount - 1 - (note.pitch - pitchRangeStart)) * keyHeight - g_draw_offset_y;
13163
- const w = note.durationSteps * stepWidth;
13164
- if (w < 6) continue;
13165
- if (renderX + w < 0 || renderX > g_grid_canvas.width) continue;
13166
- if (renderY + keyHeight < 0 || renderY > g_grid_canvas.height) continue;
13167
- const textX = renderX + 2;
13168
- const textY = renderY + keyHeight / 2;
13169
- g_grid_ctx.save();
13170
- g_grid_ctx.beginPath();
13171
- g_grid_ctx.rect(renderX + 1, renderY + 1, w - 2, keyHeight - 2);
13172
- g_grid_ctx.clip();
13173
- g_grid_ctx.strokeStyle = "rgba(0,0,0,0.85)";
13174
- g_grid_ctx.strokeText(kana, textX, textY);
13175
- g_grid_ctx.fillStyle = "#fff1e8";
13176
- g_grid_ctx.fillText(kana, textX, textY);
13177
- g_grid_ctx.restore();
13178
- }
13179
- g_grid_ctx.restore();
13180
- };
13181
- var drawSelectionRect = (rect) => {
13182
- if (!rect) return;
13183
- g_grid_ctx.save();
13184
- g_grid_ctx.strokeStyle = "#ffec27";
13185
- g_grid_ctx.lineWidth = 2;
13186
- g_grid_ctx.setLineDash([4, 4]);
13187
- g_grid_ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
13188
- g_grid_ctx.fillStyle = "rgba(255,236,39,0.08)";
13189
- g_grid_ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
13190
- g_grid_ctx.restore();
13191
- };
13192
- var drawSelectedNotes = (notes, selectedIds, baseColor = [59, 130, 246, 1]) => {
13193
- const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
13194
- for (const note of notes) {
13195
- if (!selectedIds.has(note.id)) continue;
13196
- const logicalX = note.startStep * stepWidth;
13197
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
13198
- const logicalY = yIndex * keyHeight;
13199
- const w = note.durationSteps * stepWidth;
13200
- const h = keyHeight;
13201
- const renderX = logicalX - g_draw_offset_x;
13202
- const renderY = logicalY - g_draw_offset_y;
13203
- const velocityOpacity = note.velocity !== void 0 ? 0.5 + note.velocity / 127 * 0.5 : 1;
13204
- const [r, g, b, a] = baseColor;
13205
- const darkenFactor = 1.3;
13206
- const darkerR = Math.min(255, r * darkenFactor);
13207
- const darkerG = Math.min(255, g * darkenFactor);
13208
- const darkerB = Math.min(255, b * darkenFactor);
13209
- const finalOpacity = a * velocityOpacity;
13210
- g_grid_ctx.fillStyle = `rgba(${darkerR},${darkerG},${darkerB},${finalOpacity})`;
13211
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13212
- }
13213
- };
13214
- var getXY = (e) => {
13215
- const { clientX, clientY } = e;
13216
- const rect = g_grid_canvas.getBoundingClientRect();
13217
- const x2 = Math.floor(clientX - rect.left);
13218
- const y = Math.floor(clientY - rect.top);
13219
- return [x2, y, e.buttons];
13220
- };
13221
- var getGridPosition = (e) => {
13222
- const [x2, y] = getXY(e);
13223
- const { keyCount, pitchRangeStart, keyHeight, stepWidth } = g_config;
13224
- const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
13225
- const absoluteY = y + g_draw_offset_y;
13226
- const yIndex = Math.floor(absoluteY / keyHeight);
13227
- const pitch = keyCount - 1 - yIndex + pitchRangeStart;
13228
- return { step, pitch, x: x2, y };
13229
- };
13230
- var onClick = (callback) => {
13231
- g_grid_canvas.addEventListener(
13232
- "click",
13233
- (e) => {
13234
- const [x2, y] = getXY(e);
13235
- const { keyCount, pitchRangeStart, keyHeight, stepWidth } = g_config;
13236
- const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
13237
- const absoluteY = y + g_draw_offset_y;
13238
- const yIndex = Math.floor(absoluteY / keyHeight);
13239
- const pitch = keyCount - 1 - yIndex + pitchRangeStart;
13240
- if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount) {
13241
- requestAnimationFrame(() => callback(step, pitch));
13242
- }
13243
- },
13244
- { passive: true }
13245
- );
13246
- g_grid_canvas.addEventListener("contextmenu", (e) => e.preventDefault());
13247
- };
13248
- var setDrawOffset = (x2, y) => {
13249
- g_draw_offset_x = x2;
13250
- g_draw_offset_y = y;
13251
- drawKeyboard();
13252
- drawHeader();
13253
- };
13254
-
13255
13197
  // src/mml-core.ts
13256
- var PITCH_MAP2 = [
13198
+ var PITCH_MAP = [
13257
13199
  "c",
13258
13200
  "c+",
13259
13201
  "d",
@@ -13267,6 +13209,39 @@ var PITCH_MAP2 = [
13267
13209
  "a+",
13268
13210
  "b"
13269
13211
  ];
13212
+ var EDO31_NAMES = [
13213
+ "c",
13214
+ "c+",
13215
+ "c#",
13216
+ "d-",
13217
+ "d_",
13218
+ "d",
13219
+ "d+",
13220
+ "d#",
13221
+ "e-",
13222
+ "e_",
13223
+ "e",
13224
+ "f-",
13225
+ "e#",
13226
+ "f",
13227
+ "f+",
13228
+ "f#",
13229
+ "g-",
13230
+ "g_",
13231
+ "g",
13232
+ "g+",
13233
+ "g#",
13234
+ "a-",
13235
+ "a_",
13236
+ "a",
13237
+ "a+",
13238
+ "a#",
13239
+ "b-",
13240
+ "b_",
13241
+ "b",
13242
+ "b+",
13243
+ "b#"
13244
+ ];
13270
13245
  var MMLCore = class _MMLCore {
13271
13246
  notes = [];
13272
13247
  nextNoteId = 0;
@@ -13280,9 +13255,12 @@ var MMLCore = class _MMLCore {
13280
13255
  lastUndoTime = 0;
13281
13256
  static UNDO_DEBOUNCE_MS = 100;
13282
13257
  toolMode = "pen";
13283
- constructor(handlers, volume = 80) {
13258
+ /** グリッド寸法の供給元(描画器ではなく呼び出し側が持つ設定を読む) */
13259
+ getConfig;
13260
+ constructor(handlers, volume = 80, getConfig) {
13284
13261
  this.handlers = handlers;
13285
13262
  this.volume = volume;
13263
+ this.getConfig = getConfig;
13286
13264
  this.lastHistorySnapshot = JSON.stringify(this.notes);
13287
13265
  this.history.add([]);
13288
13266
  this.generateAndNotify();
@@ -13369,14 +13347,14 @@ var MMLCore = class _MMLCore {
13369
13347
  */
13370
13348
  addNote(step, pitch, options) {
13371
13349
  const existingIndex = this.notes.findIndex(
13372
- (n) => n.startStep === step && n.pitch === pitch
13350
+ (n) => n.startStep === step && n.pitchUnits === pitch
13373
13351
  );
13374
13352
  if (existingIndex === -1) {
13375
13353
  const newNote = {
13376
13354
  id: this.nextNoteId++,
13377
13355
  startStep: step,
13378
13356
  durationSteps: options.noteLengthSteps,
13379
- pitch,
13357
+ pitchUnits: pitch,
13380
13358
  velocity: options.velocity ?? DEFAULT_VELOCITY
13381
13359
  };
13382
13360
  this.notes.push(newNote);
@@ -13404,9 +13382,11 @@ var MMLCore = class _MMLCore {
13404
13382
  moveNote(noteId, startStep, pitch) {
13405
13383
  const note = this.notes.find((target) => target.id === noteId);
13406
13384
  if (!note) return;
13407
- const totalSteps = this.getMaxStep() + getRenderConfig().stepsPerBar;
13408
- const pitchRangeStart = getRenderConfig().pitchRangeStart;
13409
- const pitchRangeEnd = pitchRangeStart + getRenderConfig().keyCount - 1;
13385
+ const totalSteps = this.getMaxStep() + this.getConfig().stepsPerBar;
13386
+ const cfg = this.getConfig();
13387
+ const upr = cfg.unitsPerRow ?? UNITS_PER_SEMITONE;
13388
+ const pitchRangeStart = cfg.pitchRangeStart;
13389
+ const pitchRangeEnd = pitchRangeStart + (cfg.keyCount - 1) * upr;
13410
13390
  const clampedPitch = Math.min(
13411
13391
  Math.max(pitch, pitchRangeStart),
13412
13392
  pitchRangeEnd
@@ -13416,7 +13396,7 @@ var MMLCore = class _MMLCore {
13416
13396
  totalSteps - note.durationSteps
13417
13397
  );
13418
13398
  note.startStep = clampedStart;
13419
- note.pitch = clampedPitch;
13399
+ note.pitchUnits = clampedPitch;
13420
13400
  this.notes.sort((a, b) => a.startStep - b.startStep);
13421
13401
  this.generateAndNotify();
13422
13402
  }
@@ -13461,7 +13441,7 @@ var MMLCore = class _MMLCore {
13461
13441
  * ただし、残りステップ(limit)は絶対に超えない。
13462
13442
  */
13463
13443
  stepsToMMLDuration(steps, limit) {
13464
- const config = getRenderConfig();
13444
+ const config = this.getConfig();
13465
13445
  const total = config.stepsPerBar;
13466
13446
  const candidates = [
13467
13447
  { dur: "1.", s: total * 1.5 },
@@ -13497,7 +13477,7 @@ var MMLCore = class _MMLCore {
13497
13477
  * ギャップに収まる最大の音符を探す(減算アルゴリズム用)
13498
13478
  */
13499
13479
  findBestFitDuration(gap) {
13500
- const config = getRenderConfig();
13480
+ const config = this.getConfig();
13501
13481
  const durations = [1, 2, 4, 8, 12, 16, 24, 32, 48, 64];
13502
13482
  for (const d of durations) {
13503
13483
  const stepLen = config.stepsPerBar / d;
@@ -13507,12 +13487,20 @@ var MMLCore = class _MMLCore {
13507
13487
  }
13508
13488
  return { dur: 64, steps: config.stepsPerBar / 64 };
13509
13489
  }
13490
+ /** ピッチ(units) → その音律での「オクターブ番号」と「音名」。 */
13491
+ spell(pitchUnits) {
13492
+ const edo = this.getConfig().edo ?? 12;
13493
+ const names = edo === 31 ? EDO31_NAMES : PITCH_MAP;
13494
+ const octave = Math.floor(pitchUnits / 372) - 1;
13495
+ const within = (pitchUnits % 372 + 372) % 372;
13496
+ const step = Math.round(within / (372 / edo));
13497
+ return { octave, name: names[step % edo] };
13498
+ }
13510
13499
  /**
13511
13500
  * ピッチからオクターブ最適化のある音名を取得
13512
13501
  */
13513
13502
  getNoteWithOctave(pitch, lastOctave) {
13514
- const octave = Math.floor(pitch / 12) - 1;
13515
- const name = PITCH_MAP2[pitch % 12];
13503
+ const { octave, name } = this.spell(pitch);
13516
13504
  if (lastOctave === -1 || Math.abs(octave - lastOctave) >= 2) {
13517
13505
  return { text: `o${octave}${name}`, currentOctave: octave };
13518
13506
  }
@@ -13535,7 +13523,7 @@ var MMLCore = class _MMLCore {
13535
13523
  * 各音符の長さを忠実に出力する(次の音符がなければ曲末まで伸ばせる)。
13536
13524
  */
13537
13525
  generateMML = (volumeOverride) => {
13538
- const config = getRenderConfig();
13526
+ const config = this.getConfig();
13539
13527
  const vol = volumeOverride ?? this.volume;
13540
13528
  const header = `t${this.tempo} v${vol}`;
13541
13529
  const segments = [];
@@ -13576,14 +13564,13 @@ var MMLCore = class _MMLCore {
13576
13564
  const actualStepGenerated = this.getStepFromDottedMML(durStr);
13577
13565
  if (notes.length > 1) {
13578
13566
  const noteStrs = notes.map((n) => {
13579
- const oct = Math.floor(n.pitch / 12) - 1;
13580
- const name = PITCH_MAP2[n.pitch % 12];
13567
+ const { octave: oct, name } = this.spell(n.pitchUnits);
13581
13568
  return `o${oct}${name}`;
13582
13569
  });
13583
13570
  segments.push(`[${noteStrs.join("")}]${durStr}`);
13584
13571
  } else {
13585
13572
  const { text, currentOctave } = this.getNoteWithOctave(
13586
- notes[0].pitch,
13573
+ notes[0].pitchUnits,
13587
13574
  lastOctave
13588
13575
  );
13589
13576
  segments.push(`${text}${durStr}`);
@@ -13614,7 +13601,7 @@ var MMLCore = class _MMLCore {
13614
13601
  * MMLの音長文字列("4", "4.", "12"など)をステップ数に変換する
13615
13602
  */
13616
13603
  getStepFromDottedMML(durStr) {
13617
- const config = getRenderConfig();
13604
+ const config = this.getConfig();
13618
13605
  const total = config.stepsPerBar;
13619
13606
  const isDotted = durStr.endsWith(".");
13620
13607
  const baseDur = parseInt(isDotted ? durStr.slice(0, -1) : durStr, 10);
@@ -13624,7 +13611,7 @@ var MMLCore = class _MMLCore {
13624
13611
  };
13625
13612
  var decomposeToMonophonic = (notes) => {
13626
13613
  const sorted = [...notes].sort(
13627
- (a, b) => a.startStep - b.startStep || a.pitch - b.pitch
13614
+ (a, b) => a.startStep - b.startStep || a.pitchUnits - b.pitchUnits
13628
13615
  );
13629
13616
  const tracks = [];
13630
13617
  const trackEnds = [];
@@ -13659,6 +13646,493 @@ var isChordHeavyTrack = (notes, threshold = 0.6) => {
13659
13646
  return chordNotes / notes.length >= threshold;
13660
13647
  };
13661
13648
 
13649
+ // src/renderer.ts
13650
+ var KEYBOARD_WIDTH = 60;
13651
+ var HEADER_HEIGHT = 20;
13652
+ var NATURAL_STEPS3 = {
13653
+ 12: [0, 2, 4, 5, 7, 9, 11],
13654
+ 31: [0, 5, 10, 13, 18, 23, 28]
13655
+ };
13656
+ var keyTier = (step, edo) => {
13657
+ const naturals = NATURAL_STEPS3[edo] ?? NATURAL_STEPS3[12];
13658
+ let best = edo;
13659
+ for (const n of naturals) {
13660
+ const d = Math.abs(step - n);
13661
+ best = Math.min(best, d, edo - d);
13662
+ }
13663
+ if (best === 0) return 0;
13664
+ return best >= (edo === 31 ? 2 : 1) ? 2 : 1;
13665
+ };
13666
+ var KEY_NAMES = [
13667
+ "C",
13668
+ "C#",
13669
+ "D",
13670
+ "D#",
13671
+ "E",
13672
+ "F",
13673
+ "F#",
13674
+ "G",
13675
+ "G#",
13676
+ "A",
13677
+ "A#",
13678
+ "B"
13679
+ ];
13680
+ var createRenderer = (mountTarget, width = 800, height = 450, config) => {
13681
+ let g_header_canvas;
13682
+ let g_key_canvas;
13683
+ let g_grid_canvas;
13684
+ let g_header_ctx;
13685
+ let g_key_ctx;
13686
+ let g_grid_ctx;
13687
+ let g_config = config;
13688
+ const getRenderConfig = () => g_config;
13689
+ let g_draw_offset_x = 0;
13690
+ let g_draw_offset_y = 0;
13691
+ let g_bg_active = false;
13692
+ const setBackgroundActive = (active) => {
13693
+ g_bg_active = active;
13694
+ };
13695
+ const getDrawOffset = () => ({
13696
+ x: g_draw_offset_x,
13697
+ y: g_draw_offset_y
13698
+ });
13699
+ const getGridCanvas = () => g_grid_canvas;
13700
+ const getGridContext = () => g_grid_ctx;
13701
+ const getHeaderCanvas = () => g_header_canvas;
13702
+ const setup = () => {
13703
+ g_config = config;
13704
+ const headerCanvas = document.createElement("canvas");
13705
+ g_header_canvas = headerCanvas;
13706
+ headerCanvas.width = width - KEYBOARD_WIDTH;
13707
+ headerCanvas.height = HEADER_HEIGHT;
13708
+ headerCanvas.style.position = "absolute";
13709
+ headerCanvas.style.left = `${KEYBOARD_WIDTH}px`;
13710
+ headerCanvas.style.top = "0px";
13711
+ const headerCtx = headerCanvas.getContext("2d");
13712
+ if (!headerCtx)
13713
+ throw new Error("Failed to get 2D rendering context for header.");
13714
+ g_header_ctx = headerCtx;
13715
+ const keyCanvas = document.createElement("canvas");
13716
+ g_key_canvas = keyCanvas;
13717
+ keyCanvas.width = KEYBOARD_WIDTH;
13718
+ keyCanvas.height = height - HEADER_HEIGHT;
13719
+ keyCanvas.style.position = "absolute";
13720
+ keyCanvas.style.left = "0px";
13721
+ keyCanvas.style.top = `${HEADER_HEIGHT}px`;
13722
+ const keyCtx = keyCanvas.getContext("2d");
13723
+ if (!keyCtx)
13724
+ throw new Error("Failed to get 2D rendering context for keyboard.");
13725
+ g_key_ctx = keyCtx;
13726
+ const gridCanvas = document.createElement("canvas");
13727
+ g_grid_canvas = gridCanvas;
13728
+ gridCanvas.width = width - KEYBOARD_WIDTH;
13729
+ gridCanvas.height = height - HEADER_HEIGHT;
13730
+ gridCanvas.style.position = "absolute";
13731
+ gridCanvas.style.left = `${KEYBOARD_WIDTH}px`;
13732
+ gridCanvas.style.top = `${HEADER_HEIGHT}px`;
13733
+ gridCanvas.style.touchAction = "none";
13734
+ gridCanvas.style.userSelect = "none";
13735
+ const gridCtx = gridCanvas.getContext("2d", { willReadFrequently: true });
13736
+ if (!gridCtx)
13737
+ throw new Error("Failed to get 2D rendering context for grid.");
13738
+ g_grid_ctx = gridCtx;
13739
+ mountTarget.innerHTML = "";
13740
+ mountTarget.style.position = "relative";
13741
+ mountTarget.style.width = `${width + KEYBOARD_WIDTH}px`;
13742
+ mountTarget.style.height = `${height}px`;
13743
+ mountTarget.append(headerCanvas, keyCanvas, gridCanvas);
13744
+ drawHeaderCorner();
13745
+ };
13746
+ const drawHeaderCorner = () => {
13747
+ const mountTarget2 = g_key_canvas.parentElement;
13748
+ if (!mountTarget2) return;
13749
+ let cornerDiv = mountTarget2.querySelector(
13750
+ "#header-corner"
13751
+ );
13752
+ if (!cornerDiv) {
13753
+ cornerDiv = document.createElement("div");
13754
+ cornerDiv.id = "header-corner";
13755
+ cornerDiv.style.position = "absolute";
13756
+ cornerDiv.style.left = "0px";
13757
+ cornerDiv.style.top = "0px";
13758
+ cornerDiv.style.width = `${KEYBOARD_WIDTH}px`;
13759
+ cornerDiv.style.height = `${HEADER_HEIGHT}px`;
13760
+ cornerDiv.style.backgroundColor = "#0a0f1f";
13761
+ cornerDiv.style.borderRight = "2px solid #29adff";
13762
+ cornerDiv.style.borderBottom = "2px solid #29adff";
13763
+ mountTarget2.insertBefore(cornerDiv, g_header_canvas);
13764
+ }
13765
+ };
13766
+ const drawKeyboard = () => {
13767
+ g_key_ctx.clearRect(0, 0, g_key_canvas.width, g_key_canvas.height);
13768
+ const {
13769
+ keyHeight,
13770
+ keyCount,
13771
+ pitchRangeStart,
13772
+ unitsPerRow: upr = UNITS_PER_SEMITONE,
13773
+ edo = 12
13774
+ } = g_config;
13775
+ const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
13776
+ const endY = g_draw_offset_y + g_key_canvas.height;
13777
+ const WHITE_KEY = "#ccc8b4";
13778
+ const BLACK_KEY = "#111111";
13779
+ const MICRO_KEY = "#4a4a4a";
13780
+ const BK_EDGE = "#383838";
13781
+ const WW_SEP = "#807a6a";
13782
+ const BK_RATIO = 0.62;
13783
+ const MICRO_RATIO = 0.45;
13784
+ for (let y = startY; y < endY; y += keyHeight) {
13785
+ const rowIndex = keyCount - 1 - y / keyHeight;
13786
+ const units = pitchRangeStart + rowIndex * upr;
13787
+ const step = Math.round(
13788
+ (units % UNITS_PER_OCTAVE + UNITS_PER_OCTAVE) % UNITS_PER_OCTAVE / upr % edo
13789
+ );
13790
+ const tier = keyTier(step, edo);
13791
+ const octave = Math.floor(units / UNITS_PER_OCTAVE) - 1;
13792
+ const isC4Range = octave === 4;
13793
+ const screenY = y - g_draw_offset_y;
13794
+ const bkW = Math.floor(
13795
+ KEYBOARD_WIDTH * (tier === 1 ? MICRO_RATIO : BK_RATIO)
13796
+ );
13797
+ if (tier !== 0) {
13798
+ g_key_ctx.fillStyle = isC4Range ? "#d8d4be" : WHITE_KEY;
13799
+ g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
13800
+ g_key_ctx.fillStyle = tier === 1 ? MICRO_KEY : isC4Range ? "#1a1408" : BLACK_KEY;
13801
+ g_key_ctx.fillRect(0, screenY, bkW, keyHeight);
13802
+ g_key_ctx.strokeStyle = BK_EDGE;
13803
+ g_key_ctx.lineWidth = 1;
13804
+ g_key_ctx.beginPath();
13805
+ g_key_ctx.moveTo(bkW, screenY);
13806
+ g_key_ctx.lineTo(bkW, screenY + keyHeight);
13807
+ g_key_ctx.stroke();
13808
+ } else {
13809
+ g_key_ctx.fillStyle = isC4Range ? "#dedad0" : WHITE_KEY;
13810
+ g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
13811
+ if (edo === 12 && (step === 5 || step === 0)) {
13812
+ g_key_ctx.strokeStyle = WW_SEP;
13813
+ g_key_ctx.lineWidth = 1;
13814
+ g_key_ctx.beginPath();
13815
+ g_key_ctx.moveTo(0, screenY + keyHeight - 0.5);
13816
+ g_key_ctx.lineTo(KEYBOARD_WIDTH, screenY + keyHeight - 0.5);
13817
+ g_key_ctx.stroke();
13818
+ }
13819
+ }
13820
+ if (step === 0) {
13821
+ g_key_ctx.fillStyle = "#555040";
13822
+ g_key_ctx.font = "10px 'k8x12',monospace";
13823
+ g_key_ctx.textAlign = "right";
13824
+ g_key_ctx.textBaseline = "bottom";
13825
+ g_key_ctx.fillText(
13826
+ `${KEY_NAMES[0]}${octave}`,
13827
+ KEYBOARD_WIDTH - 4,
13828
+ screenY + keyHeight - 2
13829
+ );
13830
+ }
13831
+ }
13832
+ g_key_ctx.beginPath();
13833
+ g_key_ctx.strokeStyle = "#29adff";
13834
+ g_key_ctx.lineWidth = 2;
13835
+ g_key_ctx.moveTo(KEYBOARD_WIDTH, 0);
13836
+ g_key_ctx.lineTo(KEYBOARD_WIDTH, g_key_canvas.height);
13837
+ g_key_ctx.stroke();
13838
+ };
13839
+ const drawHeader = () => {
13840
+ g_header_ctx.clearRect(0, 0, g_header_canvas.width, g_header_canvas.height);
13841
+ const { stepWidth, stepsPerBar } = g_config;
13842
+ g_header_ctx.save();
13843
+ g_header_ctx.translate(-g_draw_offset_x, 0);
13844
+ g_header_ctx.fillStyle = g_bg_active ? "rgba(10,15,31,0.55)" : "#0a0f1f";
13845
+ g_header_ctx.fillRect(
13846
+ g_draw_offset_x,
13847
+ 0,
13848
+ g_header_canvas.width,
13849
+ HEADER_HEIGHT
13850
+ );
13851
+ g_header_ctx.strokeStyle = "#3d405b";
13852
+ g_header_ctx.lineWidth = 1;
13853
+ g_header_ctx.font = "11px 'k8x12',monospace";
13854
+ g_header_ctx.fillStyle = "#83769c";
13855
+ const startBar = Math.floor(g_draw_offset_x / (stepsPerBar * stepWidth));
13856
+ const endBar = Math.ceil(
13857
+ (g_draw_offset_x + g_header_canvas.width) / (stepsPerBar * stepWidth)
13858
+ );
13859
+ for (let bar = startBar; bar <= endBar + 1; bar++) {
13860
+ const x2 = bar * stepsPerBar * stepWidth;
13861
+ const screenX = x2;
13862
+ g_header_ctx.beginPath();
13863
+ g_header_ctx.moveTo(screenX, 0);
13864
+ g_header_ctx.lineTo(screenX, HEADER_HEIGHT);
13865
+ g_header_ctx.stroke();
13866
+ if (bar >= 0) {
13867
+ g_header_ctx.textAlign = "left";
13868
+ g_header_ctx.textBaseline = "middle";
13869
+ g_header_ctx.fillText(`${bar + 1}`, screenX + 5, HEADER_HEIGHT / 2);
13870
+ }
13871
+ }
13872
+ g_header_ctx.restore();
13873
+ };
13874
+ const drawGrid = (noteLengthSteps = 1) => {
13875
+ drawKeyboard();
13876
+ drawHeader();
13877
+ g_grid_ctx.clearRect(0, 0, g_grid_canvas.width, g_grid_canvas.height);
13878
+ const {
13879
+ keyHeight,
13880
+ keyCount,
13881
+ stepWidth,
13882
+ stepsPerBar,
13883
+ pitchRangeStart,
13884
+ unitsPerRow: upr = UNITS_PER_SEMITONE,
13885
+ edo = 12
13886
+ } = g_config;
13887
+ const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
13888
+ const endY = g_draw_offset_y + g_grid_canvas.height;
13889
+ for (let y = startY; y < endY; y += keyHeight) {
13890
+ const rowIndex = keyCount - 1 - y / keyHeight;
13891
+ const units = pitchRangeStart + rowIndex * upr;
13892
+ const step = Math.round(
13893
+ (units % UNITS_PER_OCTAVE + UNITS_PER_OCTAVE) % UNITS_PER_OCTAVE / upr % edo
13894
+ );
13895
+ const tier = keyTier(step, edo);
13896
+ const isC = step === 0;
13897
+ const octave = Math.floor(units / UNITS_PER_OCTAVE) - 1;
13898
+ const isC4Range = octave === 4;
13899
+ const screenY = y - g_draw_offset_y;
13900
+ g_grid_ctx.fillStyle = g_bg_active ? tier === 2 ? "rgba(8,11,22,0.55)" : tier === 1 ? "rgba(12,16,30,0.5)" : "rgba(17,22,40,0.45)" : tier === 2 ? "#080b16" : tier === 1 ? "#0c101d" : "#111628";
13901
+ g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13902
+ if (isC4Range) {
13903
+ g_grid_ctx.fillStyle = "rgba(41,173,255,0.05)";
13904
+ g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13905
+ }
13906
+ g_grid_ctx.beginPath();
13907
+ g_grid_ctx.strokeStyle = isC ? "#3d405b" : "#1a1d30";
13908
+ g_grid_ctx.lineWidth = 1;
13909
+ const lineY = screenY + keyHeight;
13910
+ g_grid_ctx.moveTo(0, lineY);
13911
+ g_grid_ctx.lineTo(g_grid_canvas.width, lineY);
13912
+ g_grid_ctx.stroke();
13913
+ }
13914
+ const gridStep = noteLengthSteps || 48;
13915
+ const startX = Math.floor(g_draw_offset_x / (stepWidth * gridStep)) * stepWidth * gridStep;
13916
+ const endX = g_draw_offset_x + g_grid_canvas.width;
13917
+ const lineStep = stepWidth * gridStep;
13918
+ for (let x2 = startX; x2 <= endX; x2 += lineStep) {
13919
+ const step = x2 / stepWidth;
13920
+ const isBarLine = step % stepsPerBar === 0;
13921
+ const isNoteLine = step % gridStep === 0;
13922
+ const screenX = x2 - g_draw_offset_x;
13923
+ g_grid_ctx.beginPath();
13924
+ g_grid_ctx.strokeStyle = isBarLine ? "#3d405b" : isNoteLine ? "#242840" : "#1a1d30";
13925
+ g_grid_ctx.lineWidth = isBarLine ? 2 : 1;
13926
+ g_grid_ctx.moveTo(screenX, 0);
13927
+ g_grid_ctx.lineTo(screenX, g_grid_canvas.height);
13928
+ g_grid_ctx.stroke();
13929
+ }
13930
+ };
13931
+ const drawNotes = (notes, color = [59, 130, 246, 1], isActive = true) => {
13932
+ const {
13933
+ keyHeight,
13934
+ stepWidth,
13935
+ keyCount,
13936
+ pitchRangeStart,
13937
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13938
+ } = g_config;
13939
+ const canvasWidth = g_grid_canvas.width;
13940
+ const canvasHeight = g_grid_canvas.height;
13941
+ const [r, g, b, a] = color;
13942
+ for (const note of notes) {
13943
+ const logicalX = note.startStep * stepWidth;
13944
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
13945
+ const logicalY = yIndex * keyHeight;
13946
+ const w = note.durationSteps * stepWidth;
13947
+ const h = keyHeight;
13948
+ const renderX = logicalX - g_draw_offset_x;
13949
+ const renderY = logicalY - g_draw_offset_y;
13950
+ if (renderX + w < 0 || renderX > canvasWidth) continue;
13951
+ if (renderY + h < 0 || renderY > canvasHeight) continue;
13952
+ if (isActive) {
13953
+ const velocityOpacity = note.velocity !== void 0 ? 0.6 + note.velocity / 127 * 0.4 : 1;
13954
+ const finalOpacity = a * velocityOpacity;
13955
+ g_grid_ctx.fillStyle = `rgba(${r},${g},${b},${finalOpacity})`;
13956
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13957
+ if (w >= 4 && h >= 4) {
13958
+ g_grid_ctx.fillStyle = "rgba(255,255,255,0.4)";
13959
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, 1);
13960
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, 1, h - 2);
13961
+ g_grid_ctx.fillStyle = "rgba(0,0,0,0.45)";
13962
+ g_grid_ctx.fillRect(renderX + 1, renderY + h - 2, w - 2, 1);
13963
+ g_grid_ctx.fillRect(renderX + w - 2, renderY + 1, 1, h - 2);
13964
+ }
13965
+ } else {
13966
+ const velocityOpacity = note.velocity !== void 0 ? 0.7 + note.velocity / 127 * 0.3 : 1;
13967
+ const finalOpacity = Math.min(0.25, a * 0.22) * velocityOpacity;
13968
+ const gray = 0.299 * r + 0.587 * g + 0.114 * b;
13969
+ const ghostR = Math.round(r * 0.6 + gray * 0.4);
13970
+ const ghostG = Math.round(g * 0.6 + gray * 0.4);
13971
+ const ghostB = Math.round(b * 0.6 + gray * 0.4);
13972
+ g_grid_ctx.fillStyle = `rgba(${ghostR},${ghostG},${ghostB},${finalOpacity})`;
13973
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13974
+ }
13975
+ }
13976
+ };
13977
+ const drawNoteLyrics = (notes, syllables) => {
13978
+ if (syllables.length === 0) return;
13979
+ const {
13980
+ keyHeight,
13981
+ stepWidth,
13982
+ keyCount,
13983
+ pitchRangeStart,
13984
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13985
+ } = g_config;
13986
+ if (keyHeight < 7) return;
13987
+ const fontSize = Math.min(12, Math.floor(keyHeight * 0.85));
13988
+ const sorted = [...notes].sort((a, b) => a.startStep - b.startStep);
13989
+ const count = Math.min(sorted.length, syllables.length);
13990
+ g_grid_ctx.save();
13991
+ g_grid_ctx.font = `${fontSize}px 'k8x12',sans-serif`;
13992
+ g_grid_ctx.textAlign = "left";
13993
+ g_grid_ctx.textBaseline = "middle";
13994
+ g_grid_ctx.lineWidth = 3;
13995
+ g_grid_ctx.lineJoin = "round";
13996
+ for (let i2 = 0; i2 < count; i2++) {
13997
+ const kana = syllables[i2];
13998
+ if (!kana) continue;
13999
+ const note = sorted[i2];
14000
+ const renderX = note.startStep * stepWidth - g_draw_offset_x;
14001
+ const renderY = (keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr) * keyHeight - g_draw_offset_y;
14002
+ const w = note.durationSteps * stepWidth;
14003
+ if (w < 6) continue;
14004
+ if (renderX + w < 0 || renderX > g_grid_canvas.width) continue;
14005
+ if (renderY + keyHeight < 0 || renderY > g_grid_canvas.height) continue;
14006
+ const textX = renderX + 2;
14007
+ const textY = renderY + keyHeight / 2;
14008
+ g_grid_ctx.save();
14009
+ g_grid_ctx.beginPath();
14010
+ g_grid_ctx.rect(renderX + 1, renderY + 1, w - 2, keyHeight - 2);
14011
+ g_grid_ctx.clip();
14012
+ g_grid_ctx.strokeStyle = "rgba(0,0,0,0.85)";
14013
+ g_grid_ctx.strokeText(kana, textX, textY);
14014
+ g_grid_ctx.fillStyle = "#fff1e8";
14015
+ g_grid_ctx.fillText(kana, textX, textY);
14016
+ g_grid_ctx.restore();
14017
+ }
14018
+ g_grid_ctx.restore();
14019
+ };
14020
+ const drawSelectionRect = (rect) => {
14021
+ if (!rect) return;
14022
+ g_grid_ctx.save();
14023
+ g_grid_ctx.strokeStyle = "#ffec27";
14024
+ g_grid_ctx.lineWidth = 2;
14025
+ g_grid_ctx.setLineDash([4, 4]);
14026
+ g_grid_ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
14027
+ g_grid_ctx.fillStyle = "rgba(255,236,39,0.08)";
14028
+ g_grid_ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
14029
+ g_grid_ctx.restore();
14030
+ };
14031
+ const drawSelectedNotes = (notes, selectedIds, baseColor = [59, 130, 246, 1]) => {
14032
+ const {
14033
+ keyHeight,
14034
+ stepWidth,
14035
+ keyCount,
14036
+ pitchRangeStart,
14037
+ unitsPerRow: upr = UNITS_PER_SEMITONE
14038
+ } = g_config;
14039
+ for (const note of notes) {
14040
+ if (!selectedIds.has(note.id)) continue;
14041
+ const logicalX = note.startStep * stepWidth;
14042
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
14043
+ const logicalY = yIndex * keyHeight;
14044
+ const w = note.durationSteps * stepWidth;
14045
+ const h = keyHeight;
14046
+ const renderX = logicalX - g_draw_offset_x;
14047
+ const renderY = logicalY - g_draw_offset_y;
14048
+ const velocityOpacity = note.velocity !== void 0 ? 0.5 + note.velocity / 127 * 0.5 : 1;
14049
+ const [r, g, b, a] = baseColor;
14050
+ const darkenFactor = 1.3;
14051
+ const darkerR = Math.min(255, r * darkenFactor);
14052
+ const darkerG = Math.min(255, g * darkenFactor);
14053
+ const darkerB = Math.min(255, b * darkenFactor);
14054
+ const finalOpacity = a * velocityOpacity;
14055
+ g_grid_ctx.fillStyle = `rgba(${darkerR},${darkerG},${darkerB},${finalOpacity})`;
14056
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
14057
+ }
14058
+ };
14059
+ const getXY = (e) => {
14060
+ const { clientX, clientY } = e;
14061
+ const rect = g_grid_canvas.getBoundingClientRect();
14062
+ const x2 = Math.floor(clientX - rect.left);
14063
+ const y = Math.floor(clientY - rect.top);
14064
+ return [x2, y, e.buttons];
14065
+ };
14066
+ const getGridPosition = (e) => {
14067
+ const [x2, y] = getXY(e);
14068
+ const {
14069
+ keyCount,
14070
+ pitchRangeStart,
14071
+ keyHeight,
14072
+ stepWidth,
14073
+ unitsPerRow: upr = UNITS_PER_SEMITONE
14074
+ } = g_config;
14075
+ const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
14076
+ const absoluteY = y + g_draw_offset_y;
14077
+ const yIndex = Math.floor(absoluteY / keyHeight);
14078
+ const pitch = pitchRangeStart + (keyCount - 1 - yIndex) * upr;
14079
+ return { step, pitch, x: x2, y };
14080
+ };
14081
+ const onClick = (callback) => {
14082
+ g_grid_canvas.addEventListener(
14083
+ "click",
14084
+ (e) => {
14085
+ const [x2, y] = getXY(e);
14086
+ const {
14087
+ keyCount,
14088
+ pitchRangeStart,
14089
+ keyHeight,
14090
+ stepWidth,
14091
+ unitsPerRow: upr = UNITS_PER_SEMITONE
14092
+ } = g_config;
14093
+ const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
14094
+ const absoluteY = y + g_draw_offset_y;
14095
+ const yIndex = Math.floor(absoluteY / keyHeight);
14096
+ const pitch = pitchRangeStart + (keyCount - 1 - yIndex) * upr;
14097
+ if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount) {
14098
+ requestAnimationFrame(() => callback(step, pitch));
14099
+ }
14100
+ },
14101
+ { passive: true }
14102
+ );
14103
+ g_grid_canvas.addEventListener("contextmenu", (e) => e.preventDefault());
14104
+ };
14105
+ const setDrawOffset = (x2, y) => {
14106
+ g_draw_offset_x = x2;
14107
+ g_draw_offset_y = y;
14108
+ drawKeyboard();
14109
+ drawHeader();
14110
+ };
14111
+ setup();
14112
+ return {
14113
+ getRenderConfig,
14114
+ setBackgroundActive,
14115
+ getDrawOffset,
14116
+ getGridCanvas,
14117
+ getGridContext,
14118
+ getHeaderCanvas,
14119
+ drawKeyboard,
14120
+ drawHeader,
14121
+ drawGrid,
14122
+ drawNotes,
14123
+ drawNoteLyrics,
14124
+ drawSelectionRect,
14125
+ drawSelectedNotes,
14126
+ getXY,
14127
+ getGridPosition,
14128
+ onClick,
14129
+ setDrawOffset,
14130
+ destroy: () => {
14131
+ mountTarget.innerHTML = "";
14132
+ }
14133
+ };
14134
+ };
14135
+
13662
14136
  // src/daw.ts
13663
14137
  var CHORD_INFO_HTML2 = `
13664
14138
  <div class="dtm-modal-body-content">
@@ -13945,6 +14419,27 @@ var MIDI_INFO_HTML = `
13945
14419
  <p style="margin-top:4px;"><small>\u6B4C\u8A5E\u306E\u62BD\u51FA: <a href="https://rpgen3.github.io/ust2txt/" target="_blank" rel="noopener">ust2txt</a></small></p>
13946
14420
  </div>
13947
14421
  `;
14422
+ var EDO_INFO_HTML = `
14423
+ <div class="dtm-modal-section">
14424
+ <p><strong>\u97F3\u5F8B</strong>\u306F1\u30AA\u30AF\u30BF\u30FC\u30D6\u3092\u4F55\u7B49\u5206\u3059\u308B\u304B\u306E\u8A2D\u5B9A\u3067\u3059\u3002\u66F2\u5168\u4F53\u306B\u52B9\u304D\u307E\u3059\uFF08\u30C8\u30E9\u30C3\u30AF\u3054\u3068\u30FB\u5C0F\u7BC0\u3054\u3068\u306B\u306F\u5909\u3048\u3089\u308C\u307E\u305B\u3093\uFF09\u3002</p>
14425
+ <p><strong>12\u5E73\u5747\u5F8B</strong>\u304C\u901A\u5E38\u306E\u30D4\u30A2\u30CE\u3068\u540C\u3058\u8ABF\u5F8B\u3067\u3059\u3002<strong>31\u5E73\u5747\u5F8B</strong>\u306F1\u30AA\u30AF\u30BF\u30FC\u30D6\u309231\u7B49\u5206\u3057\u3001\u30D4\u30A2\u30CE\u306B\u306F\u7121\u3044\u97F3\uFF08\u5FAE\u5206\u97F3\uFF09\u304C\u4F7F\u3048\u307E\u3059\u3002</p>
14426
+ <p style="margin-top:8px;"><strong>31\u5E73\u5747\u5F8B\u306E\u7279\u5FB4</strong></p>
14427
+ <ul>
14428
+ <li>\u95773\u5EA6\u304C\u7D14\u6B63\u306B\u3054\u304F\u8FD1\u304F\uFF08\u8AA4\u5DEE0.8\u30BB\u30F3\u30C8\uFF09\u3001<strong>\u548C\u97F3\u304C12\u5E73\u5747\u5F8B\u3088\u308A\u7DBA\u9E97\u306B\u97FF\u304D\u307E\u3059</strong>\u3002</li>
14429
+ <li>\u30D4\u30A2\u30CE\u306B\u7121\u3044\u97F3\u304C\u4F7F\u3048\u307E\u3059\u3002\u4E2D\u7ACB3\u5EA6\uFF08\u9577\u77ED\u3069\u3061\u3089\u3067\u3082\u306A\u30443\u5EA6\uFF09\u3001\u81EA\u71367\u5EA6\uFF08\u30D0\u30FC\u30D0\u30FC\u30B7\u30E7\u30C3\u30D7\u306E\u97FF\u304D\uFF09\u306A\u3069\u3002</li>
14430
+ <li>C\u266F \u3068 D\u266D \u304C<strong>\u5225\u306E\u97F3</strong>\u306B\u306A\u308A\u307E\u3059\uFF0812\u5E73\u5747\u5F8B\u3067\u306F\u540C\u3058\u97F3\uFF09\u3002</li>
14431
+ </ul>
14432
+ <p style="margin-top:8px;"><strong>MML\u3067\u306E\u66F8\u304D\u65B9</strong></p>
14433
+ <p><code>#edo=31</code> \u3092\u5BA3\u8A00\u3057\u307E\u3059\u3002\u81E8\u6642\u8A18\u53F7\u306F4\u3064\u4F7F\u3044\u5206\u3051\u307E\u3059\u3002</p>
14434
+ <ul>
14435
+ <li><code>#</code> \u2026 \u534A\u97F3\u4E0A\u3052\uFF082\u5EA6\uFF09\u3000<code>-</code> \u2026 \u534A\u97F3\u4E0B\u3052\uFF082\u5EA6\uFF09</li>
14436
+ <li><code>+</code> \u2026 \u5FAE\u5206\u97F31\u3064\u4E0A\u3052\uFF081\u5EA6\uFF09\u3000<code>_</code> \u2026 \u5FAE\u5206\u97F31\u3064\u4E0B\u3052\uFF081\u5EA6\uFF09</li>
14437
+ </ul>
14438
+ <p>\u4F8B: <code>#edo=31 @0 o4 c c+ c# d- d_ d;</code></p>
14439
+ <p style="margin-top:8px;"><small>12\u5E73\u5747\u5F8B\u3067\u306F4\u8A18\u53F7\u3068\u3082\u5F93\u6765\u901A\u308A\u306E\u610F\u5473\uFF08\u30B7\u30E3\u30FC\u30D72\u3064\u30FB\u30D5\u30E9\u30C3\u30C82\u3064\uFF09\u306B\u306A\u308B\u306E\u3067\u3001\u65E2\u5B58\u306E\u66F2\u306E\u89E3\u91C8\u306F\u5909\u308F\u308A\u307E\u305B\u3093\u3002</small></p>
14440
+ <p style="margin-top:8px;"><small>\u5207\u308A\u66FF\u3048\u308B\u3068\u3001\u65E2\u5B58\u306E\u97F3\u7B26\u306F\u65B0\u3057\u3044\u97F3\u5F8B\u306E\u683C\u5B50\u3078\u4E38\u3081\u3089\u308C\u307E\u3059\u300212\u219231 \u306F\u6700\u592719\u30BB\u30F3\u30C8\u306E\u79FB\u52D5\u3067\u5143\u306B\u623B\u305B\u307E\u3059\u304C\u3001<strong>31\u219212 \u306F\u6700\u592748\u30BB\u30F3\u30C8\u52D5\u304D\u300112\u5E73\u5747\u5F8B\u306B\u7121\u3044\u97F3\u306F\u5931\u308F\u308C\u307E\u3059</strong>\u3002</small></p>
14441
+ </div>
14442
+ `;
13948
14443
  var KOE_INFO_HTML = `
13949
14444
  <div class="dtm-modal-body-content">
13950
14445
  <h4>1. UTAU\u97F3\u6E90\u3092 .koe \u306B\u5909\u63DB\u3059\u308B</h4>
@@ -14162,7 +14657,7 @@ var AUTO_ROLE_BASE_VOLUME = {
14162
14657
  chord: 72
14163
14658
  };
14164
14659
  var computeAutoRoleStats = (notes) => {
14165
- const avgPitch = notes.reduce((s, n) => s + n.pitch, 0) / notes.length;
14660
+ const avgPitch = notes.reduce((s, n) => s + n.pitchUnits, 0) / notes.length;
14166
14661
  const avgDur = notes.reduce((s, n) => s + n.durationSteps, 0) / notes.length;
14167
14662
  const byStart = /* @__PURE__ */ new Map();
14168
14663
  for (const n of notes)
@@ -14176,8 +14671,9 @@ var computeAutoRoleStats = (notes) => {
14176
14671
  const notesPerBeat = notes.length / spanBeats;
14177
14672
  return { avgPitch, avgDur, avgPoly, maxPoly, notesPerBeat };
14178
14673
  };
14674
+ var AUTO_ROLE_BASS_UNITS = 48 * UNITS_PER_SEMITONE;
14179
14675
  var classifyTrackRole = (stats) => {
14180
- if (stats.avgPitch < 48) return "bass";
14676
+ if (stats.avgPitch < AUTO_ROLE_BASS_UNITS) return "bass";
14181
14677
  if (stats.maxPoly >= 2 && stats.avgDur >= AUTO_ROLE_STEPS_PER_BEAT)
14182
14678
  return "chord";
14183
14679
  if (stats.notesPerBeat < 1.2 && stats.avgDur >= AUTO_ROLE_STEPS_PER_BEAT * 1.5)
@@ -14189,7 +14685,10 @@ var computeAutoRoleVolume = (role, stats) => {
14189
14685
  if (role === "chord" && stats.avgPoly > 1) {
14190
14686
  vol /= Math.sqrt(stats.avgPoly);
14191
14687
  } else if (role === "bass") {
14192
- const depthSemitones = Math.max(0, 48 - stats.avgPitch);
14688
+ const depthSemitones = Math.max(
14689
+ 0,
14690
+ (AUTO_ROLE_BASS_UNITS - stats.avgPitch) / UNITS_PER_SEMITONE
14691
+ );
14193
14692
  vol += Math.min(12, depthSemitones * 0.5);
14194
14693
  }
14195
14694
  return clamp5(Math.round(vol), 1, 127);
@@ -14281,17 +14780,48 @@ var mountDAW = (target, options = {}) => {
14281
14780
  refs.drumVolumeLabel.textContent = `${options.drumVolume ?? 80}%`;
14282
14781
  const renderConfig = {
14283
14782
  stepsPerBar: 192,
14284
- keyCount: 128,
14285
- pitchRangeStart: 0,
14783
+ keyCount: KEY_COUNT,
14784
+ pitchRangeStart: PITCH_RANGE_START,
14785
+ unitsPerRow: unitsPerRow(12),
14786
+ edo: 12,
14286
14787
  keyHeight: BASE_KEY_HEIGHT,
14287
14788
  stepWidth: BASE_STEP_WIDTH * 2
14288
14789
  // zoom100% 相当
14289
14790
  };
14791
+ let renderer;
14290
14792
  let zoomX = 100;
14291
14793
  let zoomY = 100;
14292
14794
  let bpm = options.defaultBpm ?? DEFAULT_BPM;
14293
14795
  let masterVolume = options.masterVolume ?? 50;
14294
- options.singingVoices?.setVolume(masterVolume / 100);
14796
+ const applyMasterVolume = (volume) => {
14797
+ masterVolume = clamp5(Math.round(volume), 0, 100);
14798
+ refs.masterVolume.value = String(masterVolume);
14799
+ refs.masterVolumeLabel.textContent = `${masterVolume}%`;
14800
+ options.singingVoices?.setVolume(masterVolume / 100);
14801
+ };
14802
+ applyMasterVolume(masterVolume);
14803
+ const snapToEdoGrid = (units) => {
14804
+ const upr = renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE;
14805
+ return Math.round(units / upr) * upr;
14806
+ };
14807
+ const applyEdo = (edo, opts) => {
14808
+ const next = edo === 31 ? 31 : 12;
14809
+ if (renderConfig.edo === next) return;
14810
+ const upr = unitsPerRow(next);
14811
+ renderConfig.edo = next;
14812
+ renderConfig.unitsPerRow = upr;
14813
+ renderConfig.keyCount = keyCountFor(next);
14814
+ if (!opts?.snap) return;
14815
+ for (const t of trackStates) {
14816
+ t.core.beginBatch();
14817
+ for (const note of [...t.core.getNotes()]) {
14818
+ const snapped = Math.round(note.pitchUnits / upr) * upr;
14819
+ if (snapped !== note.pitchUnits)
14820
+ t.core.moveNote(note.id, note.startStep, snapped);
14821
+ }
14822
+ t.core.endBatch();
14823
+ }
14824
+ };
14295
14825
  let reverbAmount = options.reverbAmount ?? 0;
14296
14826
  let reverbDecay = options.reverbDecay ?? DEFAULT_REVERB_DECAY_SEC;
14297
14827
  let reverbPreDelay = options.reverbPreDelay ?? DEFAULT_REVERB_PREDELAY_MS;
@@ -14349,8 +14879,8 @@ var mountDAW = (target, options = {}) => {
14349
14879
  let snapGridSteps = 12;
14350
14880
  const gridLineSteps = 48;
14351
14881
  let currentOffsetX = 0;
14352
- const _initPitch = options.initialScrollPitch ?? 48;
14353
- let currentOffsetY = (renderConfig.keyCount - 1 - _initPitch) * renderConfig.keyHeight - 215;
14882
+ const _initPitch = pitchV1ToUnits(options.initialScrollPitch ?? 48);
14883
+ let currentOffsetY = (renderConfig.keyCount - 1 - (_initPitch - renderConfig.pitchRangeStart) / (renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE)) * renderConfig.keyHeight - 215;
14354
14884
  let playStartStep = 0;
14355
14885
  let isSolo = false;
14356
14886
  let lyricTrackIndices = /* @__PURE__ */ new Set();
@@ -14445,21 +14975,28 @@ var mountDAW = (target, options = {}) => {
14445
14975
  if (!ready) return;
14446
14976
  if (!suppressPatch && options.onNotesPatch) {
14447
14977
  const prevByKey = new Map(
14448
- prevNotes.map((n) => [`${n.startStep}_${n.pitch}`, n])
14978
+ prevNotes.map((n) => [`${n.startStep}_${n.pitchUnits}`, n])
14449
14979
  );
14450
14980
  const currByKey = new Map(
14451
- notes.map((n) => [`${n.startStep}_${n.pitch}`, n])
14981
+ notes.map((n) => [`${n.startStep}_${n.pitchUnits}`, n])
14452
14982
  );
14453
14983
  const added = notes.filter((n) => {
14454
- const prev = prevByKey.get(`${n.startStep}_${n.pitch}`);
14984
+ const prev = prevByKey.get(
14985
+ `${n.startStep}_${n.pitchUnits}`
14986
+ );
14455
14987
  return !prev || prev.durationSteps !== n.durationSteps || prev.velocity !== n.velocity;
14456
14988
  }).map((n) => ({
14457
14989
  startStep: n.startStep,
14458
- pitch: n.pitch,
14990
+ pitchUnits: n.pitchUnits,
14459
14991
  durationSteps: n.durationSteps,
14460
14992
  velocity: n.velocity
14461
14993
  }));
14462
- const removed = prevNotes.filter((n) => !currByKey.has(`${n.startStep}_${n.pitch}`)).map((n) => ({ startStep: n.startStep, pitch: n.pitch }));
14994
+ const removed = prevNotes.filter(
14995
+ (n) => !currByKey.has(`${n.startStep}_${n.pitchUnits}`)
14996
+ ).map((n) => ({
14997
+ startStep: n.startStep,
14998
+ pitchUnits: n.pitchUnits
14999
+ }));
14463
15000
  if (added.length > 0 || removed.length > 0) {
14464
15001
  options.onNotesPatch(config.id, added, removed);
14465
15002
  }
@@ -14469,7 +15006,8 @@ var mountDAW = (target, options = {}) => {
14469
15006
  updateUndoRedo();
14470
15007
  }
14471
15008
  },
14472
- config.volume
15009
+ config.volume,
15010
+ () => renderConfig
14473
15011
  ),
14474
15012
  volume: config.volume,
14475
15013
  savedChordInput: "",
@@ -14564,18 +15102,18 @@ var mountDAW = (target, options = {}) => {
14564
15102
  return (lastNoteMeasure + 2) * renderConfig.stepsPerBar;
14565
15103
  };
14566
15104
  const getMaxOffsetX = () => {
14567
- const canvas = getGridCanvas();
15105
+ const canvas = renderer.getGridCanvas();
14568
15106
  const maxNoteStep = getMaxNoteStep();
14569
15107
  const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
14570
15108
  return Math.max(0, totalContentWidth - canvas.width);
14571
15109
  };
14572
15110
  const getMaxOffsetY = () => {
14573
15111
  const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
14574
- return Math.max(0, totalHeight - getGridCanvas().height);
15112
+ return Math.max(0, totalHeight - renderer.getGridCanvas().height);
14575
15113
  };
14576
15114
  const drawStartLine = () => {
14577
- const ctx = getGridContext();
14578
- const canvas = getGridCanvas();
15115
+ const ctx = renderer.getGridContext();
15116
+ const canvas = renderer.getGridCanvas();
14579
15117
  if (!ctx) return;
14580
15118
  const x2 = playStartStep * renderConfig.stepWidth - currentOffsetX;
14581
15119
  if (x2 < -10 || x2 > canvas.width + 10) return;
@@ -14590,8 +15128,8 @@ var mountDAW = (target, options = {}) => {
14590
15128
  ctx.restore();
14591
15129
  };
14592
15130
  const drawPlayhead = () => {
14593
- const ctx = getGridContext();
14594
- const canvas = getGridCanvas();
15131
+ const ctx = renderer.getGridContext();
15132
+ const canvas = renderer.getGridCanvas();
14595
15133
  if (!ctx) return;
14596
15134
  const x2 = currentPlayStep * renderConfig.stepWidth - currentOffsetX;
14597
15135
  if (x2 < 0 || x2 > canvas.width) return;
@@ -14605,20 +15143,28 @@ var mountDAW = (target, options = {}) => {
14605
15143
  ctx.restore();
14606
15144
  };
14607
15145
  const redrawAll = () => {
14608
- drawGrid(gridLineSteps);
15146
+ renderer.drawGrid(gridLineSteps);
14609
15147
  let lyricTargetNotes = null;
15148
+ let activeTrackState = null;
14610
15149
  for (const t of trackStates) {
14611
15150
  if (hiddenTracks.has(t.config.id)) continue;
14612
15151
  if (isSolo && t.config.id !== activeTrackId) continue;
15152
+ if (t.config.id === activeTrackId) {
15153
+ activeTrackState = t;
15154
+ continue;
15155
+ }
14613
15156
  const [r, g, b] = t.config.color;
14614
- const isActive = t.config.id === activeTrackId;
14615
- const a = isActive ? 1 : 0.3;
14616
15157
  const notes = t.core.getNotes();
14617
- drawNotes(notes, [r, g, b, a]);
14618
- if (isActive) lyricTargetNotes = notes;
15158
+ renderer.drawNotes(notes, [r, g, b, 1], false);
15159
+ }
15160
+ if (activeTrackState) {
15161
+ const [r, g, b] = activeTrackState.config.color;
15162
+ const notes = activeTrackState.core.getNotes();
15163
+ renderer.drawNotes(notes, [r, g, b, 1], true);
15164
+ lyricTargetNotes = notes;
14619
15165
  }
14620
15166
  if (activeToolMode === "select" && selectionRect) {
14621
- const ctx = getGridContext();
15167
+ const ctx = renderer.getGridContext();
14622
15168
  ctx.save();
14623
15169
  ctx.strokeStyle = "#ffec27";
14624
15170
  ctx.lineWidth = 2;
@@ -14641,19 +15187,19 @@ var mountDAW = (target, options = {}) => {
14641
15187
  if (activeToolMode === "select" && selectedNotes.length > 0) {
14642
15188
  const ids = new Set(selectedNotes.map((n) => n.id));
14643
15189
  const active = getActive();
14644
- drawSelectedNotes(active.core.getNotes(), ids, [
15190
+ renderer.drawSelectedNotes(active.core.getNotes(), ids, [
14645
15191
  ...active.config.color,
14646
15192
  1
14647
15193
  ]);
14648
15194
  }
14649
15195
  if (lyricTargetNotes)
14650
- drawNoteLyrics(lyricTargetNotes, getActiveLyricKana());
15196
+ renderer.drawNoteLyrics(lyricTargetNotes, getActiveLyricKana());
14651
15197
  drawStartLine();
14652
15198
  if (playbackState === "playing") drawPlayhead();
14653
15199
  updateScrollbars();
14654
15200
  };
14655
15201
  const updateScrollbars = () => {
14656
- const canvas = getGridCanvas();
15202
+ const canvas = renderer.getGridCanvas();
14657
15203
  const maxOffsetX = getMaxOffsetX();
14658
15204
  const sbW = refs.hScroll.clientWidth;
14659
15205
  if (maxOffsetX <= 0) {
@@ -14737,7 +15283,7 @@ var mountDAW = (target, options = {}) => {
14737
15283
  const x2 = clamp5(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
14738
15284
  const ratio = x2 / (rect.width - thumbW);
14739
15285
  currentOffsetX = clamp5(ratio * maxOffsetX, 0, maxOffsetX);
14740
- setDrawOffset(currentOffsetX, currentOffsetY);
15286
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14741
15287
  redrawAll();
14742
15288
  };
14743
15289
  const moveV = (clientY) => {
@@ -14748,7 +15294,7 @@ var mountDAW = (target, options = {}) => {
14748
15294
  const y = clamp5(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
14749
15295
  const ratio = y / (rect.height - thumbH);
14750
15296
  currentOffsetY = clamp5(ratio * maxOffset, 0, maxOffset);
14751
- setDrawOffset(currentOffsetX, currentOffsetY);
15297
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14752
15298
  redrawAll();
14753
15299
  };
14754
15300
  };
@@ -14787,8 +15333,8 @@ var mountDAW = (target, options = {}) => {
14787
15333
  const autoScrollTick = () => {
14788
15334
  autoScrollRAF = null;
14789
15335
  if (!isSelecting || !lastMoveEvent) return;
14790
- const canvas = getGridCanvas();
14791
- const { x: x2, y } = getGridPosition(lastMoveEvent);
15336
+ const canvas = renderer.getGridCanvas();
15337
+ const { x: x2, y } = renderer.getGridPosition(lastMoveEvent);
14792
15338
  const dx = computeEdgeSpeed(x2, canvas.width);
14793
15339
  const dy = computeEdgeSpeed(y, canvas.height);
14794
15340
  if (dx !== 0 || dy !== 0) {
@@ -14796,7 +15342,7 @@ var mountDAW = (target, options = {}) => {
14796
15342
  const maxOffsetY = getMaxOffsetY();
14797
15343
  currentOffsetX = clamp5(currentOffsetX + dx, 0, maxOffsetX);
14798
15344
  currentOffsetY = clamp5(currentOffsetY + dy, 0, maxOffsetY);
14799
- setDrawOffset(currentOffsetX, currentOffsetY);
15345
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14800
15346
  onPointerMove(lastMoveEvent);
14801
15347
  }
14802
15348
  if (isSelecting) {
@@ -14817,11 +15363,17 @@ var mountDAW = (target, options = {}) => {
14817
15363
  };
14818
15364
  const findActiveNoteAt = (x2, y, margin = 0) => {
14819
15365
  const active = getActive();
14820
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
14821
- const offset = getDrawOffset();
15366
+ const {
15367
+ stepWidth,
15368
+ keyHeight,
15369
+ keyCount,
15370
+ pitchRangeStart,
15371
+ unitsPerRow: upr = UNITS_PER_SEMITONE
15372
+ } = renderConfig;
15373
+ const offset = renderer.getDrawOffset();
14822
15374
  for (const note of active.core.getNotes()) {
14823
15375
  const logicalX = note.startStep * stepWidth;
14824
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
15376
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
14825
15377
  const logicalY = yIndex * keyHeight;
14826
15378
  const w = note.durationSteps * stepWidth;
14827
15379
  const renderX = logicalX - offset.x;
@@ -14834,7 +15386,7 @@ var mountDAW = (target, options = {}) => {
14834
15386
  const hasNoteAt = (step, pitch, excludeId) => {
14835
15387
  const active = getActive();
14836
15388
  return active.core.getNotes().some(
14837
- (n) => n.id !== excludeId && n.pitch === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
15389
+ (n) => n.id !== excludeId && n.pitchUnits === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
14838
15390
  );
14839
15391
  };
14840
15392
  const snapToGrid = (duration) => Math.max(
@@ -14845,7 +15397,7 @@ var mountDAW = (target, options = {}) => {
14845
15397
  const onGridPointerDown = (event) => {
14846
15398
  event.preventDefault();
14847
15399
  options.onResumeAudio?.();
14848
- const { x: x2, y, step, pitch } = getGridPosition(event);
15400
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(event);
14849
15401
  const active = getActive();
14850
15402
  if (activeToolMode === "eraser") {
14851
15403
  if (isActiveLocked()) return;
@@ -14860,7 +15412,7 @@ var mountDAW = (target, options = {}) => {
14860
15412
  selectedOriginal = selectedNotes.map((n) => ({
14861
15413
  id: n.id,
14862
15414
  startStep: n.startStep,
14863
- pitch: n.pitch
15415
+ pitch: n.pitchUnits
14864
15416
  }));
14865
15417
  isSelecting = true;
14866
15418
  dragMode = "move";
@@ -14879,7 +15431,7 @@ var mountDAW = (target, options = {}) => {
14879
15431
  {
14880
15432
  id: clicked.id,
14881
15433
  startStep: clicked.startStep,
14882
- pitch: clicked.pitch
15434
+ pitch: clicked.pitchUnits
14883
15435
  }
14884
15436
  ];
14885
15437
  isSelecting = true;
@@ -14897,9 +15449,9 @@ var mountDAW = (target, options = {}) => {
14897
15449
  hasDragged = false;
14898
15450
  const existing = findActiveNoteAt(x2, y, TOUCH_HIT_MARGIN);
14899
15451
  if (existing) {
14900
- playPreview(existing.pitch);
15452
+ playPreview(existing.pitchUnits);
14901
15453
  const { stepWidth } = renderConfig;
14902
- const offset = getDrawOffset();
15454
+ const offset = renderer.getDrawOffset();
14903
15455
  const renderX = existing.startStep * stepWidth - offset.x;
14904
15456
  const w = existing.durationSteps * stepWidth;
14905
15457
  if (x2 >= renderX + w - resizeHandleWidth && x2 <= renderX + w) {
@@ -14910,17 +15462,17 @@ var mountDAW = (target, options = {}) => {
14910
15462
  dragOffsetPitch: 0,
14911
15463
  startStep: existing.startStep,
14912
15464
  durationSteps: existing.durationSteps,
14913
- lastPreviewPitch: existing.pitch
15465
+ lastPreviewPitch: existing.pitchUnits
14914
15466
  };
14915
15467
  } else {
14916
15468
  dragState = {
14917
15469
  noteId: existing.id,
14918
15470
  mode: "move",
14919
15471
  dragOffsetStep: step - existing.startStep,
14920
- dragOffsetPitch: pitch - existing.pitch,
15472
+ dragOffsetPitch: pitch - existing.pitchUnits,
14921
15473
  startStep: existing.startStep,
14922
15474
  durationSteps: existing.durationSteps,
14923
- lastPreviewPitch: existing.pitch
15475
+ lastPreviewPitch: existing.pitchUnits
14924
15476
  };
14925
15477
  }
14926
15478
  suppressClick = true;
@@ -14931,14 +15483,14 @@ var mountDAW = (target, options = {}) => {
14931
15483
  const newStart = snappedStep;
14932
15484
  const newEnd = newStart + currentInsertLength;
14933
15485
  const overlapping = active.core.getNotes().some(
14934
- (n) => n.pitch === pitch && newStart < n.startStep + n.durationSteps && newEnd > n.startStep
15486
+ (n) => n.pitchUnits === pitch && newStart < n.startStep + n.durationSteps && newEnd > n.startStep
14935
15487
  );
14936
15488
  if (!overlapping) {
14937
15489
  active.core.addNote(snappedStep, pitch, {
14938
15490
  noteLengthSteps: currentInsertLength
14939
15491
  });
14940
15492
  playPreview(pitch);
14941
- const newNote = active.core.getNotes().find((n) => n.startStep === snappedStep && n.pitch === pitch);
15493
+ const newNote = active.core.getNotes().find((n) => n.startStep === snappedStep && n.pitchUnits === pitch);
14942
15494
  if (newNote) {
14943
15495
  dragState = {
14944
15496
  noteId: newNote.id,
@@ -14947,7 +15499,7 @@ var mountDAW = (target, options = {}) => {
14947
15499
  dragOffsetPitch: 0,
14948
15500
  startStep: newNote.startStep,
14949
15501
  durationSteps: newNote.durationSteps,
14950
- lastPreviewPitch: newNote.pitch
15502
+ lastPreviewPitch: newNote.pitchUnits
14951
15503
  };
14952
15504
  hasDragged = true;
14953
15505
  }
@@ -14958,7 +15510,7 @@ var mountDAW = (target, options = {}) => {
14958
15510
  const active = getActive();
14959
15511
  if (activeToolMode === "pen") {
14960
15512
  if (!dragState) return;
14961
- const { step, pitch } = getGridPosition(event);
15513
+ const { step, pitch } = renderer.getGridPosition(event);
14962
15514
  hasDragged = true;
14963
15515
  if (dragState.mode === "move") {
14964
15516
  const nextStart = step - dragState.dragOffsetStep;
@@ -14982,7 +15534,7 @@ var mountDAW = (target, options = {}) => {
14982
15534
  }
14983
15535
  if (activeToolMode === "select" && isSelecting && selectionStart) {
14984
15536
  ensureAutoScroll(event);
14985
- const { x: x2, y, step, pitch } = getGridPosition(event);
15537
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(event);
14986
15538
  if (dragMode === "rect") {
14987
15539
  const rect = {
14988
15540
  x: Math.min(x2, selectionStart.x),
@@ -14991,11 +15543,17 @@ var mountDAW = (target, options = {}) => {
14991
15543
  height: Math.abs(y - selectionStart.y)
14992
15544
  };
14993
15545
  selectionRect = rect;
14994
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
14995
- const offset = getDrawOffset();
15546
+ const {
15547
+ stepWidth,
15548
+ keyHeight,
15549
+ keyCount,
15550
+ pitchRangeStart,
15551
+ unitsPerRow: upr = UNITS_PER_SEMITONE
15552
+ } = renderConfig;
15553
+ const offset = renderer.getDrawOffset();
14996
15554
  selectedNotes = active.core.getNotes().filter((note) => {
14997
15555
  const logicalX = note.startStep * stepWidth;
14998
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
15556
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
14999
15557
  const logicalY = yIndex * keyHeight;
15000
15558
  const nx = logicalX - offset.x;
15001
15559
  const ny = logicalY - offset.y;
@@ -15014,7 +15572,7 @@ var mountDAW = (target, options = {}) => {
15014
15572
  const orig = selectedOriginal.find((o) => o.id === note.id);
15015
15573
  if (!orig) continue;
15016
15574
  const newPitch = orig.pitch + deltaPitch;
15017
- if (newPitch >= 0 && newPitch < 128)
15575
+ if (newPitch >= PITCH_RANGE_START && newPitch <= PITCH_RANGE_END)
15018
15576
  active.core.moveNote(
15019
15577
  note.id,
15020
15578
  orig.startStep + snappedDelta,
@@ -15162,22 +15720,23 @@ var mountDAW = (target, options = {}) => {
15162
15720
  refs.bgOpacityRow.classList.add("dtm-hidden");
15163
15721
  }
15164
15722
  refs.bgRemoveBtn.classList.toggle("dtm-hidden", !blob2);
15165
- setBackgroundActive(!!blob2);
15723
+ renderer.setBackgroundActive(!!blob2);
15166
15724
  redrawAll();
15167
15725
  };
15168
15726
  const setupCanvas = () => {
15169
15727
  const w = refs.rollContainer.clientWidth || 800;
15170
15728
  const h = refs.rollContainer.clientHeight || 450;
15171
- init(refs.wrapper, w, h, renderConfig);
15172
- const gridCanvas = getGridCanvas();
15729
+ renderer?.destroy();
15730
+ renderer = createRenderer(refs.wrapper, w, h, renderConfig);
15731
+ const gridCanvas = renderer.getGridCanvas();
15173
15732
  gridCanvas.addEventListener("pointerdown", onGridPointerDown);
15174
15733
  gridCanvas.addEventListener("dblclick", (event) => {
15175
15734
  event.preventDefault();
15176
15735
  if (isActiveLocked()) return;
15177
- const { step, pitch } = getGridPosition(event);
15736
+ const { step, pitch } = renderer.getGridPosition(event);
15178
15737
  const active = getActive();
15179
15738
  const note = active.core.getNotes().find(
15180
- (n) => n.pitch === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
15739
+ (n) => n.pitchUnits === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
15181
15740
  );
15182
15741
  if (note) active.core.deleteNoteById(note.id);
15183
15742
  });
@@ -15195,7 +15754,7 @@ var mountDAW = (target, options = {}) => {
15195
15754
  0,
15196
15755
  getMaxOffsetX()
15197
15756
  );
15198
- setDrawOffset(currentOffsetX, currentOffsetY);
15757
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15199
15758
  redrawAll();
15200
15759
  },
15201
15760
  { passive: false }
@@ -15205,7 +15764,7 @@ var mountDAW = (target, options = {}) => {
15205
15764
  suppressClick = false;
15206
15765
  }
15207
15766
  });
15208
- const headerCanvas = getHeaderCanvas();
15767
+ const headerCanvas = renderer.getHeaderCanvas();
15209
15768
  headerCanvas.addEventListener("click", (event) => {
15210
15769
  if (playbackState === "playing") return;
15211
15770
  const rect = headerCanvas.getBoundingClientRect();
@@ -15221,11 +15780,11 @@ var mountDAW = (target, options = {}) => {
15221
15780
  }
15222
15781
  redrawAll();
15223
15782
  });
15224
- setDrawOffset(currentOffsetX, currentOffsetY);
15783
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15225
15784
  redrawAll();
15226
15785
  };
15227
15786
  const applyZoomX = () => {
15228
- const canvas = getGridCanvas();
15787
+ const canvas = renderer.getGridCanvas();
15229
15788
  const centerStep = (currentOffsetX + canvas.width / 2) / renderConfig.stepWidth;
15230
15789
  renderConfig.stepWidth = BASE_STEP_WIDTH * (zoomX * 2) / 100;
15231
15790
  refs.zoomXLabel.textContent = `${zoomX}%`;
@@ -15234,11 +15793,11 @@ var mountDAW = (target, options = {}) => {
15234
15793
  0,
15235
15794
  getMaxOffsetX()
15236
15795
  );
15237
- setDrawOffset(currentOffsetX, currentOffsetY);
15796
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15238
15797
  redrawAll();
15239
15798
  };
15240
15799
  const applyZoomY = () => {
15241
- const canvas = getGridCanvas();
15800
+ const canvas = renderer.getGridCanvas();
15242
15801
  const centerKey = (currentOffsetY + canvas.height / 2) / renderConfig.keyHeight;
15243
15802
  renderConfig.keyHeight = BASE_KEY_HEIGHT * zoomY / 100;
15244
15803
  refs.zoomYLabel.textContent = `${zoomY}%`;
@@ -15247,7 +15806,7 @@ var mountDAW = (target, options = {}) => {
15247
15806
  0,
15248
15807
  getMaxOffsetY()
15249
15808
  );
15250
- setDrawOffset(currentOffsetX, currentOffsetY);
15809
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15251
15810
  redrawAll();
15252
15811
  };
15253
15812
  const getViewState = () => ({
@@ -15259,7 +15818,14 @@ var mountDAW = (target, options = {}) => {
15259
15818
  const notifyViewState = () => options.onViewStateChange?.(getViewState());
15260
15819
  const dispatchNote = (trackId, pitch, trackVol, velocity, when, duration) => {
15261
15820
  const volume = trackVol / 100 * (velocity / 127) * (masterVolume / 100);
15262
- options.onPlayNote?.({ trackId, pitch, velocity, volume, when, duration });
15821
+ options.onPlayNote?.({
15822
+ trackId,
15823
+ pitchUnits: pitch,
15824
+ velocity,
15825
+ volume,
15826
+ when,
15827
+ duration
15828
+ });
15263
15829
  };
15264
15830
  const sequencer = createSequencer({
15265
15831
  getTracks: () => trackStates.map((t) => ({
@@ -15287,7 +15853,7 @@ var mountDAW = (target, options = {}) => {
15287
15853
  },
15288
15854
  onTick: (step) => {
15289
15855
  currentPlayStep = step;
15290
- const canvas = getGridCanvas();
15856
+ const canvas = renderer.getGridCanvas();
15291
15857
  const visibleSteps = canvas.width / renderConfig.stepWidth;
15292
15858
  const threshold = currentOffsetX / renderConfig.stepWidth + visibleSteps - 4;
15293
15859
  if (currentPlayStep > threshold) {
@@ -15297,7 +15863,7 @@ var mountDAW = (target, options = {}) => {
15297
15863
  0,
15298
15864
  getMaxOffsetX()
15299
15865
  );
15300
- setDrawOffset(currentOffsetX, currentOffsetY);
15866
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15301
15867
  }
15302
15868
  redrawAll();
15303
15869
  },
@@ -15328,7 +15894,7 @@ var mountDAW = (target, options = {}) => {
15328
15894
  (a, b) => a.startStep - b.startStep
15329
15895
  );
15330
15896
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
15331
- const semis = (lt.octave ?? 0) * 12;
15897
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
15332
15898
  const count = Math.min(sorted.length, lt.syllables.length);
15333
15899
  const notes = [];
15334
15900
  for (let i2 = 0; i2 < count; i2++) {
@@ -15336,7 +15902,7 @@ var mountDAW = (target, options = {}) => {
15336
15902
  if (n.startStep < fromStep) continue;
15337
15903
  notes.push({
15338
15904
  syllable: lt.syllables[i2],
15339
- pitch: n.pitch + semis,
15905
+ pitch: n.pitchUnits + semis,
15340
15906
  startSec: (n.startStep - fromStep) * secondsPerStep,
15341
15907
  durationSec: n.durationSteps * secondsPerStep * gate
15342
15908
  });
@@ -15379,13 +15945,13 @@ var mountDAW = (target, options = {}) => {
15379
15945
  }
15380
15946
  }
15381
15947
  if (playbackState !== "paused") {
15382
- const canvas = getGridCanvas();
15948
+ const canvas = renderer.getGridCanvas();
15383
15949
  currentOffsetX = clamp5(
15384
15950
  playStartStep * renderConfig.stepWidth - canvas.width * 0.5,
15385
15951
  0,
15386
15952
  getMaxOffsetX()
15387
15953
  );
15388
- setDrawOffset(currentOffsetX, currentOffsetY);
15954
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15389
15955
  }
15390
15956
  playbackState = "playing";
15391
15957
  sequencer.start(fromStep);
@@ -15471,17 +16037,38 @@ var mountDAW = (target, options = {}) => {
15471
16037
  trackPillEls.clear();
15472
16038
  for (const [i2, t] of trackStates.entries()) {
15473
16039
  const [r, g, b] = t.config.color;
16040
+ const isActive = t.config.id === activeTrackId;
15474
16041
  const btn = document.createElement("button");
15475
- btn.className = `dtm-pill ${t.config.id === activeTrackId ? "dtm-pill--active" : ""}`;
16042
+ btn.className = `dtm-pill ${isActive ? "dtm-pill--active" : ""}`;
15476
16043
  btn.style.setProperty("--dtm-pill-color", `rgb(${r},${g},${b})`);
15477
- btn.title = t.config.name;
16044
+ btn.title = `Track ${i2 + 1}: ${t.config.name}`;
16045
+ btn.setAttribute(
16046
+ "aria-label",
16047
+ `Track ${i2 + 1}: ${t.config.name}${isActive ? " (\u9078\u629E\u4E2D)" : ""}`
16048
+ );
15478
16049
  btn.textContent = String(i2 + 1);
15479
16050
  btn.addEventListener("click", () => switchTrack(t.config.id));
15480
16051
  refs.trackTabs.appendChild(btn);
15481
16052
  trackPillEls.set(t.config.id, btn);
15482
16053
  }
15483
16054
  const active = getActive();
16055
+ const activeIndex = trackStates.findIndex(
16056
+ (t) => t.config.id === activeTrackId
16057
+ );
16058
+ const [activeR, activeG, activeB] = active.config.color;
16059
+ const panelEl = refs.trackBody.closest(".dtm-panel");
16060
+ if (panelEl) {
16061
+ const summaryEl = panelEl.querySelector("summary");
16062
+ if (summaryEl) {
16063
+ summaryEl.textContent = `\u500B\u5225\u30C8\u30E9\u30C3\u30AF\u8A2D\u5B9A\uFF08Track ${activeIndex + 1}: ${active.config.name}\uFF09`;
16064
+ }
16065
+ }
15484
16066
  refs.trackBody.innerHTML = `
16067
+ <div class="dtm-active-track-banner" style="--dtm-track-color: rgb(${activeR},${activeG},${activeB})">
16068
+ <span class="dtm-active-track-badge">TRACK ${activeIndex + 1}</span>
16069
+ <span class="dtm-active-track-name">${active.config.name}</span>
16070
+ <span class="dtm-active-track-pill">ACTIVE</span>
16071
+ </div>
15485
16072
  <div class="dtm-row">
15486
16073
  <span class="dtm-label">\u30D9\u30ED\u30B7\u30C6\u30A3</span>
15487
16074
  <input type="range" class="dtm-range dtm-grow" data-dtm="track-vol" min="0" max="127" value="${active.volume}">
@@ -16351,6 +16938,7 @@ var mountDAW = (target, options = {}) => {
16351
16938
  fadeIn: Math.round(fadeInSec * 10),
16352
16939
  fadeOut: Math.round(fadeOutSec * 10),
16353
16940
  mode,
16941
+ edo: renderConfig.edo,
16354
16942
  trackInstruments: trackInstMeta,
16355
16943
  trackCompression: trackCompMeta,
16356
16944
  trackWidth: trackWidthMeta,
@@ -16379,6 +16967,7 @@ var mountDAW = (target, options = {}) => {
16379
16967
  fadeIn: Math.round(fadeInSec * 10),
16380
16968
  fadeOut: Math.round(fadeOutSec * 10),
16381
16969
  mode,
16970
+ edo: renderConfig.edo,
16382
16971
  trackInstruments: trackInstMeta,
16383
16972
  trackCompression: trackCompMeta,
16384
16973
  trackWidth: trackWidthMeta,
@@ -16518,19 +17107,19 @@ var mountDAW = (target, options = {}) => {
16518
17107
  }
16519
17108
  }
16520
17109
  if (candidateNotes.length === 0) return null;
16521
- const sum = candidateNotes.reduce((acc, note) => acc + note.pitch, 0);
17110
+ const sum = candidateNotes.reduce((acc, note) => acc + note.pitchUnits, 0);
16522
17111
  return Math.round(sum / candidateNotes.length);
16523
17112
  };
16524
17113
  const centerPitch = (pitch) => {
16525
- const canvas = getGridCanvas();
16526
- const yIndex = renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart);
17114
+ const canvas = renderer.getGridCanvas();
17115
+ const yIndex = renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart) / (renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE);
16527
17116
  const logicalY = yIndex * renderConfig.keyHeight;
16528
17117
  currentOffsetY = clamp5(
16529
17118
  logicalY - (canvas.height - renderConfig.keyHeight) / 2,
16530
17119
  0,
16531
17120
  getMaxOffsetY()
16532
17121
  );
16533
- setDrawOffset(currentOffsetX, currentOffsetY);
17122
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
16534
17123
  };
16535
17124
  const clearAll = () => {
16536
17125
  for (const t of trackStates) {
@@ -16588,10 +17177,10 @@ var mountDAW = (target, options = {}) => {
16588
17177
  if (!meta.drumFont) applyDrumPatternFont(meta.drum);
16589
17178
  }
16590
17179
  if (meta.volume !== void 0) {
16591
- masterVolume = meta.volume;
16592
- refs.masterVolume.value = String(meta.volume);
16593
- refs.masterVolumeLabel.textContent = `${meta.volume}%`;
17180
+ applyMasterVolume(meta.volume);
16594
17181
  }
17182
+ applyEdo(meta.edo ?? 12);
17183
+ refs.edoSelect.value = String(renderConfig.edo ?? 12);
16595
17184
  if (meta.drumVolume !== void 0) {
16596
17185
  drumVolume = meta.drumVolume;
16597
17186
  refs.drumVolume.value = String(meta.drumVolume);
@@ -16756,7 +17345,7 @@ var mountDAW = (target, options = {}) => {
16756
17345
  if (applyActiveOnly && p.trackIndex !== activeTrackIndex) continue;
16757
17346
  const t = trackStates[p.trackIndex];
16758
17347
  if (!t) continue;
16759
- t.core.addNote(p.startStep, p.pitch, {
17348
+ t.core.addNote(p.startStep, p.pitchUnits, {
16760
17349
  noteLengthSteps: p.durationSteps,
16761
17350
  velocity: DEFAULT_VELOCITY
16762
17351
  });
@@ -16773,7 +17362,7 @@ var mountDAW = (target, options = {}) => {
16773
17362
  if (firstPitch !== null) {
16774
17363
  centerPitch(firstPitch);
16775
17364
  } else {
16776
- centerPitch(48);
17365
+ centerPitch(pitchV1ToUnits(48));
16777
17366
  }
16778
17367
  redrawAll();
16779
17368
  updateTrackPanel();
@@ -16791,6 +17380,8 @@ var mountDAW = (target, options = {}) => {
16791
17380
  const chordTrack = trackStates.find((t) => t.config.id === "chord");
16792
17381
  if (!chordTrack) return;
16793
17382
  const placements = buildChordPlacements({
17383
+ // 和音は五度連鎖経由で音律の格子へ写す(31平均律では長3度が10度になる)
17384
+ edo: renderConfig.edo,
16794
17385
  chordStr: active.savedChordInput,
16795
17386
  patternType: active.savedChordPattern,
16796
17387
  rootShift: active.savedChordRoot,
@@ -16800,7 +17391,7 @@ var mountDAW = (target, options = {}) => {
16800
17391
  chordTrack.core.clearNotesWithoutHistory();
16801
17392
  chordTrack.core.beginBatch();
16802
17393
  for (const p of placements) {
16803
- chordTrack.core.addNote(p.startStep, p.pitch, {
17394
+ chordTrack.core.addNote(p.startStep, p.pitchUnits, {
16804
17395
  noteLengthSteps: Math.max(1, p.durationSteps),
16805
17396
  velocity: p.velocity
16806
17397
  });
@@ -16838,6 +17429,8 @@ var mountDAW = (target, options = {}) => {
16838
17429
  active.vocalOctaveUnison = "none";
16839
17430
  } else {
16840
17431
  clearAll();
17432
+ applyEdo(12);
17433
+ refs.edoSelect.value = String(renderConfig.edo ?? 12);
16841
17434
  for (const t of trackStates) t.core.setLoadMode(true);
16842
17435
  for (const t of trackStates) {
16843
17436
  t.lyrics = "";
@@ -16864,7 +17457,7 @@ var mountDAW = (target, options = {}) => {
16864
17457
  if (applyActiveOnly && p.trackId !== activeTrackId) continue;
16865
17458
  const t = trackStates.find((ts) => ts.config.id === p.trackId);
16866
17459
  if (!t) continue;
16867
- t.core.addNote(p.startStep, p.pitch, {
17460
+ t.core.addNote(p.startStep, snapToEdoGrid(pitchV1ToUnits(p.pitch)), {
16868
17461
  noteLengthSteps: p.durationSteps,
16869
17462
  velocity: p.velocity
16870
17463
  });
@@ -16881,7 +17474,7 @@ var mountDAW = (target, options = {}) => {
16881
17474
  if (firstPitch !== null) {
16882
17475
  centerPitch(firstPitch);
16883
17476
  } else {
16884
- centerPitch(48);
17477
+ centerPitch(pitchV1ToUnits(48));
16885
17478
  }
16886
17479
  redrawAll();
16887
17480
  updateTrackPanel();
@@ -16945,18 +17538,21 @@ var mountDAW = (target, options = {}) => {
16945
17538
  }
16946
17539
  }, 30);
16947
17540
  };
16948
- const showConfirmModal = (message) => new Promise((resolve) => {
17541
+ const showConfirmModal = (message, opts) => new Promise((resolve) => {
17542
+ const title = opts?.title ?? "\u30E2\u30FC\u30C9\u306E\u78BA\u8A8D";
17543
+ const yes = opts?.yes ?? "\u306F\u3044\uFF08\u4E0A\u7D1A\u8005\u30E2\u30FC\u30C9\u306B\u5207\u308A\u66FF\u3048\u308B\uFF09";
17544
+ const no = opts?.no ?? "\u3044\u3044\u3048\uFF08\u3053\u306E\u307E\u307E\u8AAD\u307F\u8FBC\u3080\uFF09";
16949
17545
  const overlay = document.createElement("div");
16950
17546
  overlay.className = "dtm-modal-overlay";
16951
17547
  overlay.innerHTML = `
16952
17548
  <div class="dtm-modal">
16953
17549
  <div class="dtm-modal-header">
16954
- <span class="dtm-modal-title">\u30E2\u30FC\u30C9\u306E\u78BA\u8A8D</span>
17550
+ <span class="dtm-modal-title">${title}</span>
16955
17551
  </div>
16956
17552
  <div class="dtm-modal-body"><p>${message}</p></div>
16957
17553
  <div class="dtm-confirm-footer">
16958
- <button class="dtm-btn dtm-btn--ghost dtm-confirm-no">\u3044\u3044\u3048\uFF08\u3053\u306E\u307E\u307E\u8AAD\u307F\u8FBC\u3080\uFF09</button>
16959
- <button class="dtm-btn dtm-btn--primary dtm-confirm-yes">\u306F\u3044\uFF08\u4E0A\u7D1A\u8005\u30E2\u30FC\u30C9\u306B\u5207\u308A\u66FF\u3048\u308B\uFF09</button>
17554
+ <button class="dtm-btn dtm-btn--ghost dtm-confirm-no">${no}</button>
17555
+ <button class="dtm-btn dtm-btn--primary dtm-confirm-yes">${yes}</button>
16960
17556
  </div>
16961
17557
  </div>`;
16962
17558
  const close = (result) => {
@@ -17064,10 +17660,36 @@ var mountDAW = (target, options = {}) => {
17064
17660
  });
17065
17661
  refs.decomposeChordToggle.addEventListener("change", notifyViewState);
17066
17662
  refs.ignoreChordHeavyToggle.addEventListener("change", notifyViewState);
17663
+ refs.edoSelect.addEventListener("change", async () => {
17664
+ const next = Number.parseInt(refs.edoSelect.value, 10) === 31 ? 31 : 12;
17665
+ const prev = renderConfig.edo ?? 12;
17666
+ if (next === prev) return;
17667
+ const hasNotes = trackStates.some((t) => t.core.getNotes().length > 0);
17668
+ if (next === 12 && prev === 31 && hasNotes) {
17669
+ const okToDrop = await showConfirmModal(
17670
+ "31\u5E73\u5747\u5F8B\u304B\u308912\u5E73\u5747\u5F8B\u3078\u623B\u3059\u3068\u3001\u97F3\u7B26\u304C\u6700\u592748\u30BB\u30F3\u30C8\uFF08\u534A\u97F3\u306E\u7D04\u534A\u5206\uFF09\u52D5\u304D\u307E\u3059\u3002<br>12\u5E73\u5747\u5F8B\u306B\u7121\u3044\u97F3\uFF08\u4E2D\u7ACB3\u5EA6\u306A\u3069\uFF09\u306F\u8FD1\u3044\u97F3\u306B\u6F70\u308C\u3001<strong>\u5143\u306B\u306F\u623B\u305B\u307E\u305B\u3093</strong>\u3002<br>\u5207\u308A\u66FF\u3048\u307E\u3059\u304B\uFF1F",
17671
+ {
17672
+ title: "\u97F3\u5F8B\u306E\u78BA\u8A8D",
17673
+ yes: "\u5207\u308A\u66FF\u3048\u308B",
17674
+ no: "\u3084\u3081\u308B"
17675
+ }
17676
+ );
17677
+ if (!okToDrop) {
17678
+ refs.edoSelect.value = String(prev);
17679
+ return;
17680
+ }
17681
+ }
17682
+ applyEdo(next, { snap: true });
17683
+ const first = getFirstDetectedPitch();
17684
+ centerPitch(first ?? pitchV1ToUnits(48));
17685
+ redrawAll();
17686
+ updateUndoRedo();
17687
+ });
17688
+ refs.edoInfoBtn.addEventListener("click", () => {
17689
+ showModal("\u97F3\u5F8B\u306E\u89E3\u8AAC", EDO_INFO_HTML);
17690
+ });
17067
17691
  refs.masterVolume.addEventListener("input", () => {
17068
- masterVolume = Number.parseInt(refs.masterVolume.value, 10) || 0;
17069
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17070
- options.singingVoices?.setVolume(masterVolume / 100);
17692
+ applyMasterVolume(Number.parseInt(refs.masterVolume.value, 10) || 0);
17071
17693
  });
17072
17694
  refs.masterComp.addEventListener("input", () => {
17073
17695
  masterCompression = Number.parseInt(refs.masterComp.value, 10) || 0;
@@ -17131,10 +17753,7 @@ var mountDAW = (target, options = {}) => {
17131
17753
  const scale = targetPeak / observedPeakMax;
17132
17754
  const suggested = clamp5(Math.round(masterVolume * scale), 10, 100);
17133
17755
  if (suggested !== masterVolume) {
17134
- masterVolume = suggested;
17135
- refs.masterVolume.value = String(masterVolume);
17136
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17137
- options.singingVoices?.setVolume(masterVolume / 100);
17756
+ applyMasterVolume(suggested);
17138
17757
  }
17139
17758
  observedPeakMax = 0;
17140
17759
  observedPlayMs = 0;
@@ -17294,7 +17913,8 @@ var mountDAW = (target, options = {}) => {
17294
17913
  generateRandomPattern(getActive().core, {
17295
17914
  stepsPerBar: renderConfig.stepsPerBar,
17296
17915
  startStep: playStartStep,
17297
- pitchRangeStart: renderConfig.pitchRangeStart
17916
+ pitchRangeStart: renderConfig.pitchRangeStart,
17917
+ edo: renderConfig.edo
17298
17918
  });
17299
17919
  redrawAll();
17300
17920
  });
@@ -17631,7 +18251,7 @@ var mountDAW = (target, options = {}) => {
17631
18251
  id: 0,
17632
18252
  startStep: p.startStep,
17633
18253
  durationSteps: p.durationSteps,
17634
- pitch: p.pitch,
18254
+ pitchUnits: snapToEdoGrid(pitchV1ToUnits(p.pitch)),
17635
18255
  velocity: p.velocity
17636
18256
  }));
17637
18257
  const mml = refCore.getMMLFromNotes(asNotes, tempo, 100).trim();
@@ -18086,10 +18706,10 @@ var mountDAW = (target, options = {}) => {
18086
18706
  const newStart = playStartStep + (note.startStep - minStart);
18087
18707
  const newEnd = newStart + note.durationSteps;
18088
18708
  const overlap = notes.some(
18089
- (ex) => ex.pitch === note.pitch && newStart < ex.startStep + ex.durationSteps && newEnd > ex.startStep
18709
+ (ex) => ex.pitchUnits === note.pitchUnits && newStart < ex.startStep + ex.durationSteps && newEnd > ex.startStep
18090
18710
  );
18091
18711
  if (!overlap)
18092
- core.addNote(newStart, note.pitch, {
18712
+ core.addNote(newStart, note.pitchUnits, {
18093
18713
  noteLengthSteps: note.durationSteps,
18094
18714
  velocity: note.velocity
18095
18715
  });
@@ -18162,13 +18782,13 @@ var mountDAW = (target, options = {}) => {
18162
18782
  pausedPlayStep = step;
18163
18783
  currentPlayStep = step;
18164
18784
  playbackState = "paused";
18165
- const canvas = getGridCanvas();
18785
+ const canvas = renderer.getGridCanvas();
18166
18786
  currentOffsetX = clamp5(
18167
18787
  step * renderConfig.stepWidth - canvas.width * 0.5,
18168
18788
  0,
18169
18789
  getMaxOffsetX()
18170
18790
  );
18171
- setDrawOffset(currentOffsetX, currentOffsetY);
18791
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
18172
18792
  updateTransport();
18173
18793
  redrawAll();
18174
18794
  };
@@ -18236,16 +18856,10 @@ var mountDAW = (target, options = {}) => {
18236
18856
  forcePauseAt,
18237
18857
  setLoading,
18238
18858
  setMasterVolume: (volume) => {
18239
- masterVolume = clamp5(volume, 0, 100);
18240
- refs.masterVolume.value = String(masterVolume);
18241
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
18242
- options.singingVoices?.setVolume(masterVolume / 100);
18859
+ applyMasterVolume(volume);
18243
18860
  },
18244
18861
  setVolume: (volume) => {
18245
- masterVolume = clamp5(volume, 0, 100);
18246
- refs.masterVolume.value = String(masterVolume);
18247
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
18248
- options.singingVoices?.setVolume(masterVolume / 100);
18862
+ applyMasterVolume(volume);
18249
18863
  },
18250
18864
  setDrumVolume: (volume) => {
18251
18865
  drumVolume = clamp5(volume, 0, 100);
@@ -18288,15 +18902,19 @@ var mountDAW = (target, options = {}) => {
18288
18902
  suppressPatch = true;
18289
18903
  track.core.beginBatch();
18290
18904
  for (const n of added) {
18291
- const existing = track.core.getNotes().find((e) => e.startStep === n.startStep && e.pitch === n.pitch);
18905
+ const existing = track.core.getNotes().find(
18906
+ (e) => e.startStep === n.startStep && e.pitchUnits === n.pitchUnits
18907
+ );
18292
18908
  if (existing) track.core.deleteNoteById(existing.id);
18293
- track.core.addNote(n.startStep, n.pitch, {
18909
+ track.core.addNote(n.startStep, n.pitchUnits, {
18294
18910
  noteLengthSteps: n.durationSteps,
18295
18911
  velocity: n.velocity
18296
18912
  });
18297
18913
  }
18298
18914
  for (const r of removed) {
18299
- const note = track.core.getNotes().find((n) => n.startStep === r.startStep && n.pitch === r.pitch);
18915
+ const note = track.core.getNotes().find(
18916
+ (n) => n.startStep === r.startStep && n.pitchUnits === r.pitchUnits
18917
+ );
18300
18918
  if (note) track.core.deleteNoteById(note.id);
18301
18919
  }
18302
18920
  track.core.endBatch();
@@ -18338,9 +18956,9 @@ var mountDAW = (target, options = {}) => {
18338
18956
  if (t.config.id === activeTrackId) updateTrackPanel();
18339
18957
  },
18340
18958
  noteToCanvas: (step, pitch) => {
18341
- const canvas = getGridCanvas();
18959
+ const canvas = renderer.getGridCanvas();
18342
18960
  const x2 = step * renderConfig.stepWidth - currentOffsetX;
18343
- const y = (renderConfig.keyCount - 1 - pitch) * renderConfig.keyHeight - currentOffsetY;
18961
+ const y = (renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart) / (renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE)) * renderConfig.keyHeight - currentOffsetY;
18344
18962
  const onScreen = x2 >= 0 && x2 <= canvas.width && y >= 0 && y <= canvas.height;
18345
18963
  let side = null;
18346
18964
  if (!onScreen) {
@@ -18404,7 +19022,7 @@ var playSingingMML = async (mml, options = {}) => {
18404
19022
  id: id++,
18405
19023
  startStep: p.startStep,
18406
19024
  durationSteps: p.durationSteps,
18407
- pitch: p.pitch,
19025
+ pitchUnits: p.pitchUnits,
18408
19026
  velocity: p.velocity
18409
19027
  }));
18410
19028
  return {
@@ -18430,7 +19048,7 @@ var playSingingMML = async (mml, options = {}) => {
18430
19048
  (a, b) => a.startStep - b.startStep
18431
19049
  );
18432
19050
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
18433
- const semis = (lt.octave ?? 0) * 12;
19051
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
18434
19052
  const count = Math.min(sorted.length, lt.syllables.length);
18435
19053
  const notes = [];
18436
19054
  for (let i2 = 0; i2 < count; i2++) {
@@ -18438,7 +19056,7 @@ var playSingingMML = async (mml, options = {}) => {
18438
19056
  if (n.startStep < fromStep) continue;
18439
19057
  notes.push({
18440
19058
  syllable: lt.syllables[i2],
18441
- pitch: n.pitch + semis,
19059
+ pitch: n.pitchUnits + semis,
18442
19060
  startSec: (n.startStep - fromStep) * secondsPerStep,
18443
19061
  durationSec: n.durationSteps * secondsPerStep * gate
18444
19062
  });
@@ -18626,24 +19244,28 @@ var createPianoRoll = (options, handlers) => {
18626
19244
  config,
18627
19245
  noteLengthSteps = 1
18628
19246
  } = options;
18629
- init(mountTarget, width, height, config);
19247
+ const renderer = createRenderer(mountTarget, width, height, config);
18630
19248
  let currentNoteLengthSteps = noteLengthSteps;
18631
19249
  let selectionRect = null;
18632
19250
  let isSelecting = false;
18633
19251
  let selectionStart = null;
18634
19252
  let selectedNotes = [];
18635
19253
  let copiedNotes = [];
18636
- const core = new MMLCore({
18637
- onMMLGenerated: handlers.onMMLGenerated,
18638
- onNotesChanged: (notes) => {
18639
- handlers.onNotesChanged(notes);
18640
- }
18641
- });
19254
+ const core = new MMLCore(
19255
+ {
19256
+ onMMLGenerated: handlers.onMMLGenerated,
19257
+ onNotesChanged: (notes) => {
19258
+ handlers.onNotesChanged(notes);
19259
+ }
19260
+ },
19261
+ 80,
19262
+ () => config
19263
+ );
18642
19264
  const getAddNoteOptions = () => ({
18643
19265
  noteLengthSteps: currentNoteLengthSteps
18644
19266
  });
18645
19267
  let suppressClick = false;
18646
- onClick((step, pitch) => {
19268
+ renderer.onClick((step, pitch) => {
18647
19269
  if (suppressClick) {
18648
19270
  suppressClick = false;
18649
19271
  return;
@@ -18655,7 +19277,7 @@ var createPianoRoll = (options, handlers) => {
18655
19277
  } else if (mode === "eraser") {
18656
19278
  const notes = core.getNotes();
18657
19279
  const note = notes.find(
18658
- (n) => n.startStep <= step && step < n.startStep + n.durationSteps && n.pitch === pitch
19280
+ (n) => n.startStep <= step && step < n.startStep + n.durationSteps && n.pitchUnits === pitch
18659
19281
  );
18660
19282
  if (note) {
18661
19283
  core.deleteNoteById(note.id);
@@ -18663,17 +19285,23 @@ var createPianoRoll = (options, handlers) => {
18663
19285
  }
18664
19286
  }
18665
19287
  });
18666
- const gridCanvas = getGridCanvas();
19288
+ const gridCanvas = renderer.getGridCanvas();
18667
19289
  const resizeHandleWidth = 6;
18668
19290
  let dragState = null;
18669
19291
  let hasDragged = false;
18670
19292
  let lastPreviewPitch = null;
18671
19293
  const findNoteAtPosition = (x2, y) => {
18672
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18673
- const offset = getDrawOffset();
19294
+ const {
19295
+ stepWidth,
19296
+ keyHeight,
19297
+ keyCount,
19298
+ pitchRangeStart,
19299
+ unitsPerRow: upr = UNITS_PER_SEMITONE
19300
+ } = renderer.getRenderConfig();
19301
+ const offset = renderer.getDrawOffset();
18674
19302
  for (const note of core.getNotes()) {
18675
19303
  const logicalX = note.startStep * stepWidth;
18676
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19304
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18677
19305
  const logicalY = yIndex * keyHeight;
18678
19306
  const w = note.durationSteps * stepWidth;
18679
19307
  const h = keyHeight;
@@ -18687,7 +19315,7 @@ var createPianoRoll = (options, handlers) => {
18687
19315
  };
18688
19316
  const handlePointerMove = (e) => {
18689
19317
  if (core.getToolMode() === "select" && isSelecting && selectionStart) {
18690
- const { x: x2, y } = getGridPosition(e);
19318
+ const { x: x2, y } = renderer.getGridPosition(e);
18691
19319
  const minX = Math.min(x2, selectionStart.x);
18692
19320
  const minY = Math.min(y, selectionStart.y);
18693
19321
  const width2 = Math.abs(x2 - selectionStart.x);
@@ -18699,7 +19327,7 @@ var createPianoRoll = (options, handlers) => {
18699
19327
  }
18700
19328
  if (!dragState) return;
18701
19329
  hasDragged = true;
18702
- const { step, pitch } = getGridPosition(e);
19330
+ const { step, pitch } = renderer.getGridPosition(e);
18703
19331
  if (dragState.mode === "move") {
18704
19332
  if (dragState.selectedNotes && dragState.selectedNotes.length > 0) {
18705
19333
  const noteId = dragState.noteId;
@@ -18708,10 +19336,10 @@ var createPianoRoll = (options, handlers) => {
18708
19336
  const nextStart2 = step - dragState.dragOffsetStep;
18709
19337
  const nextPitch2 = pitch - dragState.dragOffsetPitch;
18710
19338
  const stepDelta = nextStart2 - baseNote.startStep;
18711
- const pitchDelta = nextPitch2 - baseNote.pitch;
19339
+ const pitchDelta = nextPitch2 - baseNote.pitchUnits;
18712
19340
  for (const note of dragState.selectedNotes) {
18713
19341
  const newStart = note.startStep + stepDelta;
18714
- const newPitch = note.pitch + pitchDelta;
19342
+ const newPitch = note.pitchUnits + pitchDelta;
18715
19343
  core.moveNote(note.id, newStart, newPitch);
18716
19344
  }
18717
19345
  if (options.onPreviewSound && pitch !== lastPreviewPitch) {
@@ -18752,7 +19380,7 @@ var createPianoRoll = (options, handlers) => {
18752
19380
  }
18753
19381
  };
18754
19382
  gridCanvas.addEventListener("pointerdown", (e) => {
18755
- const { x: x2, y, step, pitch } = getGridPosition(e);
19383
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(e);
18756
19384
  const currentMode = core.getToolMode();
18757
19385
  if (currentMode === "select") {
18758
19386
  const clickedNote = findNoteAtPosition(x2, y);
@@ -18763,7 +19391,7 @@ var createPianoRoll = (options, handlers) => {
18763
19391
  noteId: clickedNote.id,
18764
19392
  mode: "move",
18765
19393
  dragOffsetStep: step - clickedNote.startStep,
18766
- dragOffsetPitch: pitch - clickedNote.pitch,
19394
+ dragOffsetPitch: pitch - clickedNote.pitchUnits,
18767
19395
  startStep: clickedNote.startStep,
18768
19396
  selectedNotes: notesInRect
18769
19397
  // 複数選択ノートを保存
@@ -18781,10 +19409,16 @@ var createPianoRoll = (options, handlers) => {
18781
19409
  }
18782
19410
  const note = findNoteAtPosition(x2, y);
18783
19411
  if (!note) return;
18784
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18785
- const offset = getDrawOffset();
19412
+ const {
19413
+ stepWidth,
19414
+ keyHeight,
19415
+ keyCount,
19416
+ pitchRangeStart,
19417
+ unitsPerRow: upr = UNITS_PER_SEMITONE
19418
+ } = renderer.getRenderConfig();
19419
+ const offset = renderer.getDrawOffset();
18786
19420
  const logicalX = note.startStep * stepWidth;
18787
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19421
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18788
19422
  const logicalY = yIndex * keyHeight;
18789
19423
  const renderX = logicalX - offset.x;
18790
19424
  const renderY = logicalY - offset.y;
@@ -18803,7 +19437,7 @@ var createPianoRoll = (options, handlers) => {
18803
19437
  noteId: note.id,
18804
19438
  mode: "move",
18805
19439
  dragOffsetStep: step - note.startStep,
18806
- dragOffsetPitch: pitch - note.pitch,
19440
+ dragOffsetPitch: pitch - note.pitchUnits,
18807
19441
  startStep: note.startStep
18808
19442
  };
18809
19443
  });
@@ -18814,41 +19448,47 @@ var createPianoRoll = (options, handlers) => {
18814
19448
  "wheel",
18815
19449
  (e) => {
18816
19450
  e.preventDefault();
18817
- const configValues = getRenderConfig();
19451
+ const configValues = renderer.getRenderConfig();
18818
19452
  const gridHeight = gridCanvas.height;
18819
19453
  const maxOffsetY = Math.max(
18820
19454
  0,
18821
19455
  configValues.keyCount * configValues.keyHeight - gridHeight
18822
19456
  );
18823
- const currentOffset = getDrawOffset();
19457
+ const currentOffset = renderer.getDrawOffset();
18824
19458
  const nextOffsetY = Math.min(
18825
19459
  Math.max(currentOffset.y + e.deltaY, 0),
18826
19460
  maxOffsetY
18827
19461
  );
18828
- setDrawOffset(currentOffset.x, nextOffsetY);
18829
- drawGrid();
18830
- drawNotes(core.getNotes());
19462
+ renderer.setDrawOffset(currentOffset.x, nextOffsetY);
19463
+ renderer.drawGrid();
19464
+ renderer.drawNotes(core.getNotes());
18831
19465
  },
18832
19466
  { passive: false }
18833
19467
  );
18834
19468
  const redraw = () => {
18835
- drawGrid();
18836
- drawNotes(core.getNotes());
19469
+ renderer.drawGrid();
19470
+ renderer.drawNotes(core.getNotes());
18837
19471
  if (core.getToolMode() === "select") {
18838
- drawSelectionRect(selectionRect);
19472
+ renderer.drawSelectionRect(selectionRect);
18839
19473
  if (selectedNotes.length > 0) {
18840
19474
  const selectedIds = new Set(selectedNotes.map((n) => n.id));
18841
- drawSelectedNotes(core.getNotes(), selectedIds);
19475
+ renderer.drawSelectedNotes(core.getNotes(), selectedIds);
18842
19476
  }
18843
19477
  }
18844
19478
  };
18845
19479
  const getNotesInRect = (rect) => {
18846
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18847
- const offset = getDrawOffset();
19480
+ const {
19481
+ stepWidth,
19482
+ keyHeight,
19483
+ keyCount,
19484
+ pitchRangeStart,
19485
+ unitsPerRow: upr = UNITS_PER_SEMITONE
19486
+ } = renderer.getRenderConfig();
19487
+ const offset = renderer.getDrawOffset();
18848
19488
  const notes = [];
18849
19489
  for (const note of core.getNotes()) {
18850
19490
  const logicalX = note.startStep * stepWidth;
18851
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19491
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18852
19492
  const logicalY = yIndex * keyHeight;
18853
19493
  const noteRect = {
18854
19494
  x: logicalX - offset.x,
@@ -18895,7 +19535,7 @@ var createPianoRoll = (options, handlers) => {
18895
19535
  const minStart = Math.min(...copiedNotes.map((n) => n.startStep));
18896
19536
  copiedNotes.forEach((note) => {
18897
19537
  const newStep = startStep + (note.startStep - minStart);
18898
- core.addNote(newStep, note.pitch, {
19538
+ core.addNote(newStep, note.pitchUnits, {
18899
19539
  noteLengthSteps: note.durationSteps,
18900
19540
  velocity: note.velocity
18901
19541
  });
@@ -19737,10 +20377,12 @@ var createDtmStudio = async (options = {}) => {
19737
20377
  );
19738
20378
  }
19739
20379
  if (!sfInst) return;
20380
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
19740
20381
  sfInst.play({
19741
20382
  ctx: audioCtx,
19742
20383
  destination: getChannelStrip(e.trackId).input,
19743
- pitch: e.pitch,
20384
+ pitch: midi,
20385
+ detuneCents,
19744
20386
  volume: e.volume,
19745
20387
  velocity: e.velocity,
19746
20388
  when: e.when,
@@ -20056,10 +20698,12 @@ var createDtmStudio = async (options = {}) => {
20056
20698
  );
20057
20699
  }
20058
20700
  if (!sfInst) return;
20701
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20059
20702
  sfInst.play({
20060
20703
  ctx: audioCtx,
20061
20704
  destination: getChannelStrip(e.trackId).input,
20062
- pitch: e.pitch,
20705
+ pitch: midi,
20706
+ detuneCents,
20063
20707
  volume: e.volume,
20064
20708
  velocity: e.velocity,
20065
20709
  when: e.when,
@@ -20145,10 +20789,12 @@ var createDtmStudio = async (options = {}) => {
20145
20789
  );
20146
20790
  }
20147
20791
  if (!sfInst) return;
20792
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20148
20793
  sfInst.play({
20149
20794
  ctx: audioCtx,
20150
20795
  destination: getChannelStrip(e.trackId).input,
20151
- pitch: e.pitch,
20796
+ pitch: midi,
20797
+ detuneCents,
20152
20798
  volume: e.volume,
20153
20799
  velocity: e.velocity,
20154
20800
  when: e.when,
@@ -20227,10 +20873,12 @@ var createDtmStudio = async (options = {}) => {
20227
20873
  );
20228
20874
  }
20229
20875
  if (!sfInst) return;
20876
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20230
20877
  sfInst.play({
20231
20878
  ctx: audioCtx,
20232
20879
  destination: getChannelStrip(e.trackId).input,
20233
- pitch: e.pitch,
20880
+ pitch: midi,
20881
+ detuneCents,
20234
20882
  volume: e.volume,
20235
20883
  velocity: e.velocity,
20236
20884
  when: e.when,
@@ -20267,10 +20915,12 @@ var createDtmStudio = async (options = {}) => {
20267
20915
  sfInst = resolveSoundFont(defaultPreset, role, "simple");
20268
20916
  }
20269
20917
  if (!sfInst) return;
20918
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20270
20919
  sfInst.play({
20271
20920
  ctx: audioCtx,
20272
20921
  destination: getChannelStrip(e.trackId).input,
20273
- pitch: e.pitch,
20922
+ pitch: midi,
20923
+ detuneCents,
20274
20924
  volume: e.volume,
20275
20925
  velocity: e.velocity,
20276
20926
  when: e.when,
@@ -20293,10 +20943,12 @@ var createDtmStudio = async (options = {}) => {
20293
20943
  if (audioCtx.state === "suspended") {
20294
20944
  await audioCtx.resume();
20295
20945
  }
20946
+ const { midi, detuneCents } = unitsToMidiDetune(options2.pitchUnits);
20296
20947
  sfInst.play({
20297
20948
  ctx: audioCtx,
20298
20949
  destination: masterGain,
20299
- pitch: options2.pitch,
20950
+ pitch: midi,
20951
+ detuneCents,
20300
20952
  volume: vol / 100,
20301
20953
  when: 0,
20302
20954
  duration: dur
@@ -20316,7 +20968,7 @@ var createDtmStudio = async (options = {}) => {
20316
20968
  // 伴奏トラック
20317
20969
  startStep: p.startStep,
20318
20970
  durationSteps: p.durationSteps,
20319
- pitch: p.pitch,
20971
+ pitchUnits: p.pitchUnits,
20320
20972
  velocity: p.velocity
20321
20973
  }));
20322
20974
  const playerPreset = defaultPreset;
@@ -20324,10 +20976,12 @@ var createDtmStudio = async (options = {}) => {
20324
20976
  const playPlayerNote = (e) => {
20325
20977
  const sfInst = resolveSoundFont(playerPreset, "chord");
20326
20978
  if (!sfInst) return;
20979
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20327
20980
  sfInst.play({
20328
20981
  ctx: audioCtx,
20329
20982
  destination: getChannelStrip("chord").input,
20330
- pitch: e.pitch,
20983
+ pitch: midi,
20984
+ detuneCents,
20331
20985
  volume: e.volume,
20332
20986
  velocity: e.velocity,
20333
20987
  when: e.when,
@@ -20466,8 +21120,12 @@ var createDtmStudio = async (options = {}) => {
20466
21120
  };
20467
21121
  // Annotate the CommonJS export names for ESM import in node:
20468
21122
  0 && (module.exports = {
21123
+ A4_HZ,
21124
+ A4_UNITS,
21125
+ CENTS_PER_UNIT,
20469
21126
  DAW_CSS,
20470
21127
  DEFAULT_BPM,
21128
+ DEFAULT_EDO,
20471
21129
  DEFAULT_GATE,
20472
21130
  DEFAULT_PAN,
20473
21131
  DEFAULT_PLAYBACK_VELOCITY,
@@ -20477,21 +21135,30 @@ var createDtmStudio = async (options = {}) => {
20477
21135
  DRUM_FONT,
20478
21136
  DRUM_KEYS,
20479
21137
  DRUM_PATTERNS,
21138
+ EDO31_NAMES,
20480
21139
  GM_INSTRUMENT_NAMES,
20481
21140
  INSTRUMENT_PRESETS,
21141
+ KEY_COUNT,
20482
21142
  KOE_BASE_URL,
20483
21143
  KOE_VOICEBANKS,
20484
21144
  KOE_VOICEBANK_LABELS,
20485
21145
  KOE_VOICEBANK_TERMS,
20486
21146
  LinkedList,
20487
21147
  MAX_VOCAL_VOLUME,
21148
+ MICRO_STEP,
20488
21149
  MMLCore,
20489
21150
  MML_END_MARKER,
20490
21151
  MidiSearchClient,
21152
+ PITCH_ENCODING_VERSION,
20491
21153
  PITCH_MAP,
21154
+ PITCH_RANGE_END,
21155
+ PITCH_RANGE_START,
20492
21156
  PREWARM_NOTES,
20493
21157
  TRACKS_ADVANCED,
20494
21158
  TRACKS_SIMPLE,
21159
+ UNITS_PER_EDO31_DEGREE,
21160
+ UNITS_PER_OCTAVE,
21161
+ UNITS_PER_SEMITONE,
20495
21162
  VIBRATO_MIN_SEC,
20496
21163
  VOICE_IMAGES,
20497
21164
  VOICE_IMAGE_KEY,
@@ -20501,6 +21168,7 @@ var createDtmStudio = async (options = {}) => {
20501
21168
  buildChordPlacements,
20502
21169
  buildDrumPatternJson,
20503
21170
  buildNameToKeyMapping,
21171
+ chromaticStep,
20504
21172
  collectPitchTokens,
20505
21173
  concatFloat32,
20506
21174
  createAudioContext,
@@ -20509,19 +21177,13 @@ var createDtmStudio = async (options = {}) => {
20509
21177
  createKoeVoice,
20510
21178
  createLyricsConductor,
20511
21179
  createPianoRoll,
21180
+ createRenderer,
20512
21181
  createSequencer,
20513
21182
  createSingingVoices,
20514
21183
  createSynth,
20515
21184
  createVoiceRegistry,
20516
21185
  decodeMml,
20517
21186
  decomposeToMonophonic,
20518
- drawGrid,
20519
- drawHeader,
20520
- drawKeyboard,
20521
- drawNoteLyrics,
20522
- drawNotes,
20523
- drawSelectedNotes,
20524
- drawSelectionRect,
20525
21187
  encodeMml,
20526
21188
  encodeWavPCM16,
20527
21189
  exportMIDI,
@@ -20529,36 +21191,34 @@ var createDtmStudio = async (options = {}) => {
20529
21191
  extractMidiDrumPattern,
20530
21192
  extractMidiPlacements,
20531
21193
  extractMidiPlacementsByTrack,
21194
+ fifthToStep,
21195
+ fifthToUnits,
20532
21196
  formatMmlMeta,
20533
21197
  freqFromPitch,
20534
21198
  generateRandomPattern,
20535
- getDrawOffset,
20536
21199
  getDrumPatternKeys,
20537
- getGridCanvas,
20538
- getGridContext,
20539
- getGridPosition,
20540
- getHeaderCanvas,
20541
21200
  getMidiBPM,
20542
- getRenderConfig,
20543
- getXY,
20544
21201
  icon,
20545
- init,
20546
21202
  injectStyles,
20547
21203
  isChordHeavyTrack,
21204
+ isNaturalLetter,
20548
21205
  isPlausibleMidiTranscription,
20549
21206
  isValidHttpUrl,
21207
+ keyCountFor,
20550
21208
  koeUrl,
21209
+ midiToUnits,
20551
21210
  mountChordPlayer,
20552
21211
  mountDAW,
20553
21212
  mountMmlPlayer,
21213
+ naturalStep,
20554
21214
  normalizeDrumPatterns,
20555
21215
  normalizeLyrics,
20556
- onClick,
20557
21216
  panToStereo,
20558
21217
  parseCustomVocals,
20559
21218
  parseLyrics,
20560
21219
  parseMML,
20561
21220
  parseMmlMeta,
21221
+ pitchV1ToUnits,
20562
21222
  playChords,
20563
21223
  playMML,
20564
21224
  playNote,
@@ -20566,13 +21226,18 @@ var createDtmStudio = async (options = {}) => {
20566
21226
  playSingingMML,
20567
21227
  resolveDrumPattern,
20568
21228
  resolveLoopPoint,
20569
- setBackgroundActive,
20570
- setDrawOffset,
20571
21229
  shiftNotes,
20572
21230
  showLoadingOverlay,
21231
+ spellingToUnits,
20573
21232
  stripCustomVocals,
20574
21233
  stripLyrics,
20575
21234
  stripMmlMeta,
20576
21235
  transposeNotes,
21236
+ unitsPerRow,
21237
+ unitsPerStep,
21238
+ unitsToHz,
21239
+ unitsToMidi,
21240
+ unitsToMidiDetune,
21241
+ unitsToPitchV1,
20577
21242
  vocalVolumeToGain
20578
21243
  });