@onjmin/dtm 1.0.8 → 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);
579
+ };
580
+ var add = (chord, n, half, out) => {
581
+ put(chord, out, n, half);
523
582
  };
524
- var aug = (chord) => {
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, {
@@ -8949,7 +9130,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
8949
9130
  0
8950
9131
  );
8951
9132
  const timedNotes = placements.map((p) => ({
8952
- pitch: p.pitch,
9133
+ pitch: unitsToPitchV1(p.pitchUnits),
8953
9134
  when: p.startStep * secondsPerStep,
8954
9135
  duration: p.durationSteps * secondsPerStep
8955
9136
  }));
@@ -8982,7 +9163,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
8982
9163
  id: id++,
8983
9164
  startStep: p.startStep,
8984
9165
  durationSteps: p.durationSteps,
8985
- pitch: p.pitch,
9166
+ pitchUnits: p.pitchUnits,
8986
9167
  velocity: DEFAULT_VELOCITY
8987
9168
  }));
8988
9169
  return {
@@ -9807,7 +9988,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9807
9988
  (a, b) => a.startStep - b.startStep
9808
9989
  );
9809
9990
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
9810
- const semis = (lt.octave ?? 0) * 12;
9991
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
9811
9992
  const count = Math.min(sorted.length, lt.syllables.length);
9812
9993
  const notes = [];
9813
9994
  for (let i2 = 0; i2 < count; i2++) {
@@ -9815,7 +9996,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9815
9996
  if (n.startStep < fromStep) continue;
9816
9997
  notes.push({
9817
9998
  syllable: lt.syllables[i2],
9818
- pitch: n.pitch + semis,
9999
+ pitch: n.pitchUnits + semis,
9819
10000
  startSec: (n.startStep - fromStep) * secondsPerStep,
9820
10001
  durationSec: n.durationSteps * secondsPerStep * gate
9821
10002
  });
@@ -10090,6 +10271,7 @@ var SoundFont = class _SoundFont {
10090
10271
  pitch = 60,
10091
10272
  volume = 1,
10092
10273
  velocity,
10274
+ detuneCents = 0,
10093
10275
  when = 0,
10094
10276
  duration = 1
10095
10277
  } = {}) {
@@ -10109,7 +10291,7 @@ var SoundFont = class _SoundFont {
10109
10291
  Object.assign(src, _param.src);
10110
10292
  const humanizeCents = (Math.random() * 2 - 1) * _SoundFont.humanizeDetuneCents;
10111
10293
  const humanizeGainMul = 1 + (Math.random() * 2 - 1) * _SoundFont.humanizeGainRatio;
10112
- src.detune.setValueAtTime(humanizeCents, 0);
10294
+ src.detune.setValueAtTime(humanizeCents + detuneCents, 0);
10113
10295
  const effectiveVolume = volume * humanizeGainMul;
10114
10296
  const brightness = velocity === void 0 ? 1 : Math.max(0, Math.min(1, velocity / 127));
10115
10297
  const filter = brightness >= _SoundFont.brightnessBypassAbove ? void 0 : ctx.createBiquadFilter();
@@ -11248,7 +11430,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11248
11430
  scheduleMetronomeBeats(ctx, cutGain);
11249
11431
  };
11250
11432
  const wafPlayDynamic = ({
11251
- pitch,
11433
+ pitchUnits,
11252
11434
  velocity,
11253
11435
  volume,
11254
11436
  when,
@@ -11258,10 +11440,12 @@ var mountChordPlayer = (target, chords, options = {}) => {
11258
11440
  }) => {
11259
11441
  const waf = activeGmName ? _wafCache.get(activeGmName) : null;
11260
11442
  if (waf) {
11443
+ const { midi, detuneCents } = unitsToMidiDetune(pitchUnits);
11261
11444
  waf.play({
11262
11445
  ctx,
11263
11446
  destination,
11264
- pitch,
11447
+ pitch: midi,
11448
+ detuneCents,
11265
11449
  volume: volume * 0.85,
11266
11450
  velocity,
11267
11451
  when,
@@ -11271,7 +11455,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11271
11455
  const synth = createSynth(ctx, destination);
11272
11456
  synth.playNote({
11273
11457
  trackId: "chord",
11274
- pitch,
11458
+ pitchUnits,
11275
11459
  velocity: 100,
11276
11460
  volume,
11277
11461
  when,
@@ -11300,7 +11484,8 @@ var mountChordPlayer = (target, chords, options = {}) => {
11300
11484
  trackIndex: 3,
11301
11485
  startStep: whenStep,
11302
11486
  durationSteps,
11303
- pitch: C32 + noteOffset,
11487
+ // chord-parser の構成音は12平均律の半音。内部表現の units へ写す。
11488
+ pitchUnits: pitchV1ToUnits(C32 + noteOffset),
11304
11489
  velocity: 100
11305
11490
  });
11306
11491
  }
@@ -11313,7 +11498,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11313
11498
  synth: false,
11314
11499
  onPlayNote: (e) => {
11315
11500
  wafPlayDynamic({
11316
- pitch: e.pitch,
11501
+ pitchUnits: e.pitchUnits,
11317
11502
  velocity: e.velocity,
11318
11503
  volume: e.volume,
11319
11504
  when: e.when,
@@ -11755,6 +11940,14 @@ var buildUI = (target, options) => {
11755
11940
  <summary>\u5168\u4F53\u30C8\u30E9\u30C3\u30AF\u8A2D\u5B9A</summary>
11756
11941
  <div class="dtm-panel-body">
11757
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>
11758
11951
  <div class="dtm-row">
11759
11952
  <span class="dtm-label">\u5168\u4F53\u97F3\u91CF</span>
11760
11953
  <input type="range" class="dtm-range dtm-grow" data-dtm="master-volume" value="50" min="0" max="100">
@@ -12087,6 +12280,8 @@ var buildUI = (target, options) => {
12087
12280
  copyMiniBtn: sel("copy-mini"),
12088
12281
  overlay: sel("overlay"),
12089
12282
  mmlInfoBtn: sel("mml-info"),
12283
+ edoSelect: sel("edo-select"),
12284
+ edoInfoBtn: sel("edo-info"),
12090
12285
  modalOverlay: sel("modal-overlay"),
12091
12286
  modalTitle: sel("modal-title"),
12092
12287
  modalBody: sel("modal-body"),
@@ -12207,17 +12402,21 @@ var SCALES = [
12207
12402
  [0, 2, 4, 7, 9]
12208
12403
  // Pentatonic Major
12209
12404
  ];
12405
+ var MEANTONE_STEP_BY_SEMITONE2 = [0, 3, 5, 8, 10, 13, 16, 18, 21, 23, 26, 28];
12210
12406
  var generateRandomPattern = (core, options) => {
12211
- const { stepsPerBar, startStep, pitchRangeStart } = options;
12407
+ const { stepsPerBar, startStep, pitchRangeStart, edo = 12 } = options;
12212
12408
  const numBars = 8;
12213
12409
  const noteLength = 24;
12214
- 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;
12215
12413
  const scale = SCALES[Math.floor(Math.random() * SCALES.length)];
12216
12414
  const rootOffset = Math.floor(Math.random() * 12);
12217
12415
  const availablePitches = [];
12218
12416
  for (let i2 = 0; i2 < 12; i2++) {
12219
12417
  const noteInOctave = (i2 - rootOffset + 12) % 12;
12220
- if (scale.includes(noteInOctave)) availablePitches.push(basePitch + i2);
12418
+ if (scale.includes(noteInOctave))
12419
+ availablePitches.push(basePitch + semitoneToStep(i2) * upr);
12221
12420
  }
12222
12421
  core.beginBatch();
12223
12422
  for (let bar = 0; bar < numBars; bar++) {
@@ -12252,14 +12451,17 @@ var applyHarmonicFilter = (targetCore, chordCore, options) => {
12252
12451
  const isNewBar = halfBar % 2 === 0;
12253
12452
  const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12254
12453
  if (chordHere.length > 0) {
12255
- currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
12454
+ currentClasses = new Set(
12455
+ chordHere.map((n) => n.pitchUnits % UNITS_PER_OCTAVE)
12456
+ );
12256
12457
  } else if (isNewBar) {
12257
12458
  currentClasses = /* @__PURE__ */ new Set();
12258
12459
  }
12259
12460
  if (currentClasses.size === 0) continue;
12260
12461
  const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12261
12462
  for (const n of activeHere) {
12262
- if (!currentClasses.has(n.pitch % 12)) targetCore.deleteNoteById(n.id);
12463
+ if (!currentClasses.has(n.pitchUnits % UNITS_PER_OCTAVE))
12464
+ targetCore.deleteNoteById(n.id);
12263
12465
  }
12264
12466
  }
12265
12467
  targetCore.endBatch();
@@ -12281,13 +12483,17 @@ var applyMonophonic = (targetCore, chordCore, options) => {
12281
12483
  const isNewBar = halfBar % 2 === 0;
12282
12484
  const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12283
12485
  if (chordHere.length > 0) {
12284
- currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
12486
+ currentClasses = new Set(
12487
+ chordHere.map((n) => n.pitchUnits % UNITS_PER_OCTAVE)
12488
+ );
12285
12489
  } else if (isNewBar) {
12286
12490
  currentClasses = /* @__PURE__ */ new Set();
12287
12491
  }
12288
12492
  if (currentClasses.size === 0) continue;
12289
12493
  const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12290
- 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
+ );
12291
12497
  const filteredIds = new Set(filtered.map((n) => n.id));
12292
12498
  for (const n of activeHere) {
12293
12499
  if (!filteredIds.has(n.id)) targetCore.deleteNoteById(n.id);
@@ -12299,7 +12505,7 @@ var applyMonophonic = (targetCore, chordCore, options) => {
12299
12505
  }
12300
12506
  for (const notesAtTime of timeMap.values()) {
12301
12507
  if (notesAtTime.length > 1) {
12302
- notesAtTime.sort((a, b) => b.pitch - a.pitch);
12508
+ notesAtTime.sort((a, b) => b.pitchUnits - a.pitchUnits);
12303
12509
  const [, ...others] = notesAtTime;
12304
12510
  for (const on of others) targetCore.deleteNoteById(on.id);
12305
12511
  }
@@ -12315,18 +12521,21 @@ var shiftNotes = (cores, shiftSteps) => {
12315
12521
  for (const note of notes) {
12316
12522
  const newStart = note.startStep + shiftSteps;
12317
12523
  if (newStart < 0) core.deleteNoteById(note.id);
12318
- else core.moveNote(note.id, newStart, note.pitch);
12524
+ else core.moveNote(note.id, newStart, note.pitchUnits);
12319
12525
  }
12320
12526
  core.saveHistory();
12321
12527
  }
12322
12528
  };
12323
- var transposeNotes = (cores, semitones) => {
12324
- if (semitones === 0) return;
12529
+ var transposeNotes = (cores, steps) => {
12530
+ if (steps === 0) return;
12325
12531
  for (const core of cores) {
12326
12532
  const notes = [...core.getNotes()];
12327
12533
  for (const note of notes) {
12328
- const newPitch = Math.max(0, Math.min(127, note.pitch + semitones));
12329
- 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) {
12330
12539
  core.moveNote(note.id, note.startStep, newPitch);
12331
12540
  }
12332
12541
  }
@@ -12651,11 +12860,31 @@ var exportMIDI = (options) => {
12651
12860
  const div = 480;
12652
12861
  const tickPerStep = div / STEPS_PER_BEAT3;
12653
12862
  const midiTracks = [];
12654
- 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) => {
12655
12881
  if (track.notes.length === 0) return;
12656
- const channel = ch < 9 ? ch : ch + 1 & 15;
12657
12882
  const events = [];
12658
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));
12659
12888
  const startTick = Math.round(n.startStep * tickPerStep);
12660
12889
  const endTick = Math.round(
12661
12890
  (n.startStep + (n.durationSteps || 1)) * tickPerStep
@@ -12663,12 +12892,25 @@ var exportMIDI = (options) => {
12663
12892
  const vel = Math.round(
12664
12893
  (n.velocity ?? DEFAULT_VELOCITY) * (track.volume ?? 100) / 100
12665
12894
  );
12666
- events.push({ t: startTick, m: [144 | channel, n.pitch, vel] });
12667
- 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] });
12668
12897
  }
12669
12898
  events.sort((a, b) => a.t - b.t);
12670
12899
  midiTracks.push(events);
12671
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
+ }
12672
12914
  const maxStep = Math.max(
12673
12915
  ...tracks.filter((t) => t.notes.length > 0).map(
12674
12916
  (t) => Math.max(...t.notes.map((n) => n.startStep + n.durationSteps))
@@ -12705,6 +12947,7 @@ var exportMIDI = (options) => {
12705
12947
  headerChunks(arr, midiTracks.length + 1, div);
12706
12948
  trackChunks(arr, (a) => {
12707
12949
  a.push(0, 255, 81, 3, ...to3byte(Math.round(6e7 / bpm)));
12950
+ for (const ev of bendSetup) a.push(0, ...ev.m);
12708
12951
  });
12709
12952
  for (const events of midiTracks) {
12710
12953
  trackChunks(arr, (a) => {
@@ -12951,403 +13194,8 @@ var LinkedList = class {
12951
13194
  }
12952
13195
  };
12953
13196
 
12954
- // src/renderer.ts
12955
- var g_header_canvas;
12956
- var g_key_canvas;
12957
- var g_grid_canvas;
12958
- var g_header_ctx;
12959
- var g_key_ctx;
12960
- var g_grid_ctx;
12961
- var g_config;
12962
- var KEYBOARD_WIDTH = 60;
12963
- var HEADER_HEIGHT = 20;
12964
- var getRenderConfig = () => g_config;
12965
- var g_draw_offset_x = 0;
12966
- var g_draw_offset_y = 0;
12967
- var g_bg_active = false;
12968
- var setBackgroundActive = (active) => {
12969
- g_bg_active = active;
12970
- };
12971
- var getDrawOffset = () => ({
12972
- x: g_draw_offset_x,
12973
- y: g_draw_offset_y
12974
- });
12975
- var getGridCanvas = () => g_grid_canvas;
12976
- var getGridContext = () => g_grid_ctx;
12977
- var getHeaderCanvas = () => g_header_canvas;
12978
- var init = (mountTarget, width = 800, height = 450, config) => {
12979
- g_config = config;
12980
- const headerCanvas = document.createElement("canvas");
12981
- g_header_canvas = headerCanvas;
12982
- headerCanvas.width = width - KEYBOARD_WIDTH;
12983
- headerCanvas.height = HEADER_HEIGHT;
12984
- headerCanvas.style.position = "absolute";
12985
- headerCanvas.style.left = `${KEYBOARD_WIDTH}px`;
12986
- headerCanvas.style.top = "0px";
12987
- const headerCtx = headerCanvas.getContext("2d");
12988
- if (!headerCtx)
12989
- throw new Error("Failed to get 2D rendering context for header.");
12990
- g_header_ctx = headerCtx;
12991
- const keyCanvas = document.createElement("canvas");
12992
- g_key_canvas = keyCanvas;
12993
- keyCanvas.width = KEYBOARD_WIDTH;
12994
- keyCanvas.height = height - HEADER_HEIGHT;
12995
- keyCanvas.style.position = "absolute";
12996
- keyCanvas.style.left = "0px";
12997
- keyCanvas.style.top = `${HEADER_HEIGHT}px`;
12998
- const keyCtx = keyCanvas.getContext("2d");
12999
- if (!keyCtx)
13000
- throw new Error("Failed to get 2D rendering context for keyboard.");
13001
- g_key_ctx = keyCtx;
13002
- const gridCanvas = document.createElement("canvas");
13003
- g_grid_canvas = gridCanvas;
13004
- gridCanvas.width = width - KEYBOARD_WIDTH;
13005
- gridCanvas.height = height - HEADER_HEIGHT;
13006
- gridCanvas.style.position = "absolute";
13007
- gridCanvas.style.left = `${KEYBOARD_WIDTH}px`;
13008
- gridCanvas.style.top = `${HEADER_HEIGHT}px`;
13009
- gridCanvas.style.touchAction = "none";
13010
- gridCanvas.style.userSelect = "none";
13011
- const gridCtx = gridCanvas.getContext("2d", { willReadFrequently: true });
13012
- if (!gridCtx) throw new Error("Failed to get 2D rendering context for grid.");
13013
- g_grid_ctx = gridCtx;
13014
- mountTarget.innerHTML = "";
13015
- mountTarget.style.position = "relative";
13016
- mountTarget.style.width = `${width + KEYBOARD_WIDTH}px`;
13017
- mountTarget.style.height = `${height}px`;
13018
- mountTarget.append(headerCanvas, keyCanvas, gridCanvas);
13019
- drawHeaderCorner();
13020
- };
13021
- var blackKeyPitches = /* @__PURE__ */ new Set([1, 3, 6, 8, 10]);
13022
- var KEY_NAMES = [
13023
- "C",
13024
- "C#",
13025
- "D",
13026
- "D#",
13027
- "E",
13028
- "F",
13029
- "F#",
13030
- "G",
13031
- "G#",
13032
- "A",
13033
- "A#",
13034
- "B"
13035
- ];
13036
- var drawHeaderCorner = () => {
13037
- const mountTarget = g_key_canvas.parentElement;
13038
- if (!mountTarget) return;
13039
- let cornerDiv = mountTarget.querySelector("#header-corner");
13040
- if (!cornerDiv) {
13041
- cornerDiv = document.createElement("div");
13042
- cornerDiv.id = "header-corner";
13043
- cornerDiv.style.position = "absolute";
13044
- cornerDiv.style.left = "0px";
13045
- cornerDiv.style.top = "0px";
13046
- cornerDiv.style.width = `${KEYBOARD_WIDTH}px`;
13047
- cornerDiv.style.height = `${HEADER_HEIGHT}px`;
13048
- cornerDiv.style.backgroundColor = "#0a0f1f";
13049
- cornerDiv.style.borderRight = "2px solid #29adff";
13050
- cornerDiv.style.borderBottom = "2px solid #29adff";
13051
- mountTarget.insertBefore(cornerDiv, g_header_canvas);
13052
- }
13053
- };
13054
- var drawKeyboard = () => {
13055
- g_key_ctx.clearRect(0, 0, g_key_canvas.width, g_key_canvas.height);
13056
- const { keyHeight, keyCount, pitchRangeStart } = g_config;
13057
- const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
13058
- const endY = g_draw_offset_y + g_key_canvas.height;
13059
- const WHITE_KEY = "#ccc8b4";
13060
- const BLACK_KEY = "#111111";
13061
- const BK_EDGE = "#383838";
13062
- const WW_SEP = "#807a6a";
13063
- const BK_RATIO = 0.62;
13064
- for (let y = startY; y < endY; y += keyHeight) {
13065
- const pitchIndex = keyCount - 1 - y / keyHeight;
13066
- const totalPitch = pitchIndex + pitchRangeStart;
13067
- const pitchMod12 = totalPitch % 12;
13068
- const isBlackKey = blackKeyPitches.has(pitchMod12);
13069
- const octave = Math.floor(totalPitch / 12) - 1;
13070
- const isC4Range = octave === 4;
13071
- const screenY = y - g_draw_offset_y;
13072
- const bkW = Math.floor(KEYBOARD_WIDTH * BK_RATIO);
13073
- if (isBlackKey) {
13074
- g_key_ctx.fillStyle = isC4Range ? "#d8d4be" : WHITE_KEY;
13075
- g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
13076
- g_key_ctx.fillStyle = isC4Range ? "#1a1408" : BLACK_KEY;
13077
- g_key_ctx.fillRect(0, screenY, bkW, keyHeight);
13078
- g_key_ctx.strokeStyle = BK_EDGE;
13079
- g_key_ctx.lineWidth = 1;
13080
- g_key_ctx.beginPath();
13081
- g_key_ctx.moveTo(bkW, screenY);
13082
- g_key_ctx.lineTo(bkW, screenY + keyHeight);
13083
- g_key_ctx.stroke();
13084
- } else {
13085
- g_key_ctx.fillStyle = isC4Range ? "#dedad0" : WHITE_KEY;
13086
- g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
13087
- if (pitchMod12 === 5 || pitchMod12 === 0) {
13088
- g_key_ctx.strokeStyle = WW_SEP;
13089
- g_key_ctx.lineWidth = 1;
13090
- g_key_ctx.beginPath();
13091
- g_key_ctx.moveTo(0, screenY + keyHeight - 0.5);
13092
- g_key_ctx.lineTo(KEYBOARD_WIDTH, screenY + keyHeight - 0.5);
13093
- g_key_ctx.stroke();
13094
- }
13095
- }
13096
- if (pitchMod12 === 0) {
13097
- const octave2 = Math.floor(totalPitch / 12) - 1;
13098
- g_key_ctx.fillStyle = "#555040";
13099
- g_key_ctx.font = "10px 'k8x12',monospace";
13100
- g_key_ctx.textAlign = "right";
13101
- g_key_ctx.textBaseline = "bottom";
13102
- g_key_ctx.fillText(
13103
- `${KEY_NAMES[pitchMod12]}${octave2}`,
13104
- KEYBOARD_WIDTH - 4,
13105
- screenY + keyHeight - 2
13106
- );
13107
- }
13108
- }
13109
- g_key_ctx.beginPath();
13110
- g_key_ctx.strokeStyle = "#29adff";
13111
- g_key_ctx.lineWidth = 2;
13112
- g_key_ctx.moveTo(KEYBOARD_WIDTH, 0);
13113
- g_key_ctx.lineTo(KEYBOARD_WIDTH, g_key_canvas.height);
13114
- g_key_ctx.stroke();
13115
- };
13116
- var drawHeader = () => {
13117
- g_header_ctx.clearRect(0, 0, g_header_canvas.width, g_header_canvas.height);
13118
- const { stepWidth, stepsPerBar } = g_config;
13119
- g_header_ctx.save();
13120
- g_header_ctx.translate(-g_draw_offset_x, 0);
13121
- g_header_ctx.fillStyle = g_bg_active ? "rgba(10,15,31,0.55)" : "#0a0f1f";
13122
- g_header_ctx.fillRect(
13123
- g_draw_offset_x,
13124
- 0,
13125
- g_header_canvas.width,
13126
- HEADER_HEIGHT
13127
- );
13128
- g_header_ctx.strokeStyle = "#3d405b";
13129
- g_header_ctx.lineWidth = 1;
13130
- g_header_ctx.font = "11px 'k8x12',monospace";
13131
- g_header_ctx.fillStyle = "#83769c";
13132
- const startBar = Math.floor(g_draw_offset_x / (stepsPerBar * stepWidth));
13133
- const endBar = Math.ceil(
13134
- (g_draw_offset_x + g_header_canvas.width) / (stepsPerBar * stepWidth)
13135
- );
13136
- for (let bar = startBar; bar <= endBar + 1; bar++) {
13137
- const x2 = bar * stepsPerBar * stepWidth;
13138
- const screenX = x2;
13139
- g_header_ctx.beginPath();
13140
- g_header_ctx.moveTo(screenX, 0);
13141
- g_header_ctx.lineTo(screenX, HEADER_HEIGHT);
13142
- g_header_ctx.stroke();
13143
- if (bar >= 0) {
13144
- g_header_ctx.textAlign = "left";
13145
- g_header_ctx.textBaseline = "middle";
13146
- g_header_ctx.fillText(`${bar + 1}`, screenX + 5, HEADER_HEIGHT / 2);
13147
- }
13148
- }
13149
- g_header_ctx.restore();
13150
- };
13151
- var drawGrid = (noteLengthSteps = 1) => {
13152
- drawKeyboard();
13153
- drawHeader();
13154
- g_grid_ctx.clearRect(0, 0, g_grid_canvas.width, g_grid_canvas.height);
13155
- const { keyHeight, keyCount, stepWidth, stepsPerBar, pitchRangeStart } = g_config;
13156
- const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
13157
- const endY = g_draw_offset_y + g_grid_canvas.height;
13158
- for (let y = startY; y < endY; y += keyHeight) {
13159
- const pitchIndex = keyCount - 1 - y / keyHeight;
13160
- const totalPitch = pitchIndex + pitchRangeStart;
13161
- const pitchMod12 = totalPitch % 12;
13162
- const isBlackKey = blackKeyPitches.has(pitchMod12);
13163
- const isC = pitchMod12 === 0;
13164
- const octave = Math.floor(totalPitch / 12) - 1;
13165
- const isC4Range = octave === 4;
13166
- const screenY = y - g_draw_offset_y;
13167
- g_grid_ctx.fillStyle = g_bg_active ? isBlackKey ? "rgba(8,11,22,0.55)" : "rgba(17,22,40,0.45)" : isBlackKey ? "#080b16" : "#111628";
13168
- g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13169
- if (isC4Range) {
13170
- g_grid_ctx.fillStyle = "rgba(41,173,255,0.05)";
13171
- g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13172
- }
13173
- g_grid_ctx.beginPath();
13174
- g_grid_ctx.strokeStyle = isC ? "#3d405b" : "#1a1d30";
13175
- g_grid_ctx.lineWidth = 1;
13176
- const lineY = screenY + keyHeight;
13177
- g_grid_ctx.moveTo(0, lineY);
13178
- g_grid_ctx.lineTo(g_grid_canvas.width, lineY);
13179
- g_grid_ctx.stroke();
13180
- }
13181
- const gridStep = noteLengthSteps || 48;
13182
- const startX = Math.floor(g_draw_offset_x / (stepWidth * gridStep)) * stepWidth * gridStep;
13183
- const endX = g_draw_offset_x + g_grid_canvas.width;
13184
- const lineStep = stepWidth * gridStep;
13185
- for (let x2 = startX; x2 <= endX; x2 += lineStep) {
13186
- const step = x2 / stepWidth;
13187
- const isBarLine = step % stepsPerBar === 0;
13188
- const isNoteLine = step % gridStep === 0;
13189
- const screenX = x2 - g_draw_offset_x;
13190
- g_grid_ctx.beginPath();
13191
- g_grid_ctx.strokeStyle = isBarLine ? "#3d405b" : isNoteLine ? "#242840" : "#1a1d30";
13192
- g_grid_ctx.lineWidth = isBarLine ? 2 : 1;
13193
- g_grid_ctx.moveTo(screenX, 0);
13194
- g_grid_ctx.lineTo(screenX, g_grid_canvas.height);
13195
- g_grid_ctx.stroke();
13196
- }
13197
- };
13198
- var drawNotes = (notes, color = [59, 130, 246, 1], isActive = true) => {
13199
- const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
13200
- const canvasWidth = g_grid_canvas.width;
13201
- const canvasHeight = g_grid_canvas.height;
13202
- const [r, g, b, a] = color;
13203
- for (const note of notes) {
13204
- const logicalX = note.startStep * stepWidth;
13205
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
13206
- const logicalY = yIndex * keyHeight;
13207
- const w = note.durationSteps * stepWidth;
13208
- const h = keyHeight;
13209
- const renderX = logicalX - g_draw_offset_x;
13210
- const renderY = logicalY - g_draw_offset_y;
13211
- if (renderX + w < 0 || renderX > canvasWidth) continue;
13212
- if (renderY + h < 0 || renderY > canvasHeight) continue;
13213
- if (isActive) {
13214
- const velocityOpacity = note.velocity !== void 0 ? 0.6 + note.velocity / 127 * 0.4 : 1;
13215
- const finalOpacity = a * velocityOpacity;
13216
- g_grid_ctx.fillStyle = `rgba(${r},${g},${b},${finalOpacity})`;
13217
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13218
- if (w >= 4 && h >= 4) {
13219
- g_grid_ctx.fillStyle = "rgba(255,255,255,0.4)";
13220
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, 1);
13221
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, 1, h - 2);
13222
- g_grid_ctx.fillStyle = "rgba(0,0,0,0.45)";
13223
- g_grid_ctx.fillRect(renderX + 1, renderY + h - 2, w - 2, 1);
13224
- g_grid_ctx.fillRect(renderX + w - 2, renderY + 1, 1, h - 2);
13225
- }
13226
- } else {
13227
- const velocityOpacity = note.velocity !== void 0 ? 0.7 + note.velocity / 127 * 0.3 : 1;
13228
- const finalOpacity = Math.min(0.25, a * 0.22) * velocityOpacity;
13229
- const gray = 0.299 * r + 0.587 * g + 0.114 * b;
13230
- const ghostR = Math.round(r * 0.6 + gray * 0.4);
13231
- const ghostG = Math.round(g * 0.6 + gray * 0.4);
13232
- const ghostB = Math.round(b * 0.6 + gray * 0.4);
13233
- g_grid_ctx.fillStyle = `rgba(${ghostR},${ghostG},${ghostB},${finalOpacity})`;
13234
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13235
- }
13236
- }
13237
- };
13238
- var drawNoteLyrics = (notes, syllables) => {
13239
- if (syllables.length === 0) return;
13240
- const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
13241
- if (keyHeight < 10) return;
13242
- const fontSize = Math.min(12, Math.floor(keyHeight * 0.85));
13243
- const sorted = [...notes].sort((a, b) => a.startStep - b.startStep);
13244
- const count = Math.min(sorted.length, syllables.length);
13245
- g_grid_ctx.save();
13246
- g_grid_ctx.font = `${fontSize}px 'k8x12',sans-serif`;
13247
- g_grid_ctx.textAlign = "left";
13248
- g_grid_ctx.textBaseline = "middle";
13249
- g_grid_ctx.lineWidth = 3;
13250
- g_grid_ctx.lineJoin = "round";
13251
- for (let i2 = 0; i2 < count; i2++) {
13252
- const kana = syllables[i2];
13253
- if (!kana) continue;
13254
- const note = sorted[i2];
13255
- const renderX = note.startStep * stepWidth - g_draw_offset_x;
13256
- const renderY = (keyCount - 1 - (note.pitch - pitchRangeStart)) * keyHeight - g_draw_offset_y;
13257
- const w = note.durationSteps * stepWidth;
13258
- if (w < 6) continue;
13259
- if (renderX + w < 0 || renderX > g_grid_canvas.width) continue;
13260
- if (renderY + keyHeight < 0 || renderY > g_grid_canvas.height) continue;
13261
- const textX = renderX + 2;
13262
- const textY = renderY + keyHeight / 2;
13263
- g_grid_ctx.save();
13264
- g_grid_ctx.beginPath();
13265
- g_grid_ctx.rect(renderX + 1, renderY + 1, w - 2, keyHeight - 2);
13266
- g_grid_ctx.clip();
13267
- g_grid_ctx.strokeStyle = "rgba(0,0,0,0.85)";
13268
- g_grid_ctx.strokeText(kana, textX, textY);
13269
- g_grid_ctx.fillStyle = "#fff1e8";
13270
- g_grid_ctx.fillText(kana, textX, textY);
13271
- g_grid_ctx.restore();
13272
- }
13273
- g_grid_ctx.restore();
13274
- };
13275
- var drawSelectionRect = (rect) => {
13276
- if (!rect) return;
13277
- g_grid_ctx.save();
13278
- g_grid_ctx.strokeStyle = "#ffec27";
13279
- g_grid_ctx.lineWidth = 2;
13280
- g_grid_ctx.setLineDash([4, 4]);
13281
- g_grid_ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
13282
- g_grid_ctx.fillStyle = "rgba(255,236,39,0.08)";
13283
- g_grid_ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
13284
- g_grid_ctx.restore();
13285
- };
13286
- var drawSelectedNotes = (notes, selectedIds, baseColor = [59, 130, 246, 1]) => {
13287
- const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
13288
- for (const note of notes) {
13289
- if (!selectedIds.has(note.id)) continue;
13290
- const logicalX = note.startStep * stepWidth;
13291
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
13292
- const logicalY = yIndex * keyHeight;
13293
- const w = note.durationSteps * stepWidth;
13294
- const h = keyHeight;
13295
- const renderX = logicalX - g_draw_offset_x;
13296
- const renderY = logicalY - g_draw_offset_y;
13297
- const velocityOpacity = note.velocity !== void 0 ? 0.5 + note.velocity / 127 * 0.5 : 1;
13298
- const [r, g, b, a] = baseColor;
13299
- const darkenFactor = 1.3;
13300
- const darkerR = Math.min(255, r * darkenFactor);
13301
- const darkerG = Math.min(255, g * darkenFactor);
13302
- const darkerB = Math.min(255, b * darkenFactor);
13303
- const finalOpacity = a * velocityOpacity;
13304
- g_grid_ctx.fillStyle = `rgba(${darkerR},${darkerG},${darkerB},${finalOpacity})`;
13305
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13306
- }
13307
- };
13308
- var getXY = (e) => {
13309
- const { clientX, clientY } = e;
13310
- const rect = g_grid_canvas.getBoundingClientRect();
13311
- const x2 = Math.floor(clientX - rect.left);
13312
- const y = Math.floor(clientY - rect.top);
13313
- return [x2, y, e.buttons];
13314
- };
13315
- var getGridPosition = (e) => {
13316
- const [x2, y] = getXY(e);
13317
- const { keyCount, pitchRangeStart, keyHeight, stepWidth } = g_config;
13318
- const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
13319
- const absoluteY = y + g_draw_offset_y;
13320
- const yIndex = Math.floor(absoluteY / keyHeight);
13321
- const pitch = keyCount - 1 - yIndex + pitchRangeStart;
13322
- return { step, pitch, x: x2, y };
13323
- };
13324
- var onClick = (callback) => {
13325
- g_grid_canvas.addEventListener(
13326
- "click",
13327
- (e) => {
13328
- const [x2, y] = getXY(e);
13329
- const { keyCount, pitchRangeStart, keyHeight, stepWidth } = g_config;
13330
- const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
13331
- const absoluteY = y + g_draw_offset_y;
13332
- const yIndex = Math.floor(absoluteY / keyHeight);
13333
- const pitch = keyCount - 1 - yIndex + pitchRangeStart;
13334
- if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount) {
13335
- requestAnimationFrame(() => callback(step, pitch));
13336
- }
13337
- },
13338
- { passive: true }
13339
- );
13340
- g_grid_canvas.addEventListener("contextmenu", (e) => e.preventDefault());
13341
- };
13342
- var setDrawOffset = (x2, y) => {
13343
- g_draw_offset_x = x2;
13344
- g_draw_offset_y = y;
13345
- drawKeyboard();
13346
- drawHeader();
13347
- };
13348
-
13349
13197
  // src/mml-core.ts
13350
- var PITCH_MAP2 = [
13198
+ var PITCH_MAP = [
13351
13199
  "c",
13352
13200
  "c+",
13353
13201
  "d",
@@ -13361,6 +13209,39 @@ var PITCH_MAP2 = [
13361
13209
  "a+",
13362
13210
  "b"
13363
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
+ ];
13364
13245
  var MMLCore = class _MMLCore {
13365
13246
  notes = [];
13366
13247
  nextNoteId = 0;
@@ -13374,9 +13255,12 @@ var MMLCore = class _MMLCore {
13374
13255
  lastUndoTime = 0;
13375
13256
  static UNDO_DEBOUNCE_MS = 100;
13376
13257
  toolMode = "pen";
13377
- constructor(handlers, volume = 80) {
13258
+ /** グリッド寸法の供給元(描画器ではなく呼び出し側が持つ設定を読む) */
13259
+ getConfig;
13260
+ constructor(handlers, volume = 80, getConfig) {
13378
13261
  this.handlers = handlers;
13379
13262
  this.volume = volume;
13263
+ this.getConfig = getConfig;
13380
13264
  this.lastHistorySnapshot = JSON.stringify(this.notes);
13381
13265
  this.history.add([]);
13382
13266
  this.generateAndNotify();
@@ -13463,14 +13347,14 @@ var MMLCore = class _MMLCore {
13463
13347
  */
13464
13348
  addNote(step, pitch, options) {
13465
13349
  const existingIndex = this.notes.findIndex(
13466
- (n) => n.startStep === step && n.pitch === pitch
13350
+ (n) => n.startStep === step && n.pitchUnits === pitch
13467
13351
  );
13468
13352
  if (existingIndex === -1) {
13469
13353
  const newNote = {
13470
13354
  id: this.nextNoteId++,
13471
13355
  startStep: step,
13472
13356
  durationSteps: options.noteLengthSteps,
13473
- pitch,
13357
+ pitchUnits: pitch,
13474
13358
  velocity: options.velocity ?? DEFAULT_VELOCITY
13475
13359
  };
13476
13360
  this.notes.push(newNote);
@@ -13498,9 +13382,11 @@ var MMLCore = class _MMLCore {
13498
13382
  moveNote(noteId, startStep, pitch) {
13499
13383
  const note = this.notes.find((target) => target.id === noteId);
13500
13384
  if (!note) return;
13501
- const totalSteps = this.getMaxStep() + getRenderConfig().stepsPerBar;
13502
- const pitchRangeStart = getRenderConfig().pitchRangeStart;
13503
- 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;
13504
13390
  const clampedPitch = Math.min(
13505
13391
  Math.max(pitch, pitchRangeStart),
13506
13392
  pitchRangeEnd
@@ -13510,7 +13396,7 @@ var MMLCore = class _MMLCore {
13510
13396
  totalSteps - note.durationSteps
13511
13397
  );
13512
13398
  note.startStep = clampedStart;
13513
- note.pitch = clampedPitch;
13399
+ note.pitchUnits = clampedPitch;
13514
13400
  this.notes.sort((a, b) => a.startStep - b.startStep);
13515
13401
  this.generateAndNotify();
13516
13402
  }
@@ -13555,7 +13441,7 @@ var MMLCore = class _MMLCore {
13555
13441
  * ただし、残りステップ(limit)は絶対に超えない。
13556
13442
  */
13557
13443
  stepsToMMLDuration(steps, limit) {
13558
- const config = getRenderConfig();
13444
+ const config = this.getConfig();
13559
13445
  const total = config.stepsPerBar;
13560
13446
  const candidates = [
13561
13447
  { dur: "1.", s: total * 1.5 },
@@ -13591,7 +13477,7 @@ var MMLCore = class _MMLCore {
13591
13477
  * ギャップに収まる最大の音符を探す(減算アルゴリズム用)
13592
13478
  */
13593
13479
  findBestFitDuration(gap) {
13594
- const config = getRenderConfig();
13480
+ const config = this.getConfig();
13595
13481
  const durations = [1, 2, 4, 8, 12, 16, 24, 32, 48, 64];
13596
13482
  for (const d of durations) {
13597
13483
  const stepLen = config.stepsPerBar / d;
@@ -13601,12 +13487,20 @@ var MMLCore = class _MMLCore {
13601
13487
  }
13602
13488
  return { dur: 64, steps: config.stepsPerBar / 64 };
13603
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
+ }
13604
13499
  /**
13605
13500
  * ピッチからオクターブ最適化のある音名を取得
13606
13501
  */
13607
13502
  getNoteWithOctave(pitch, lastOctave) {
13608
- const octave = Math.floor(pitch / 12) - 1;
13609
- const name = PITCH_MAP2[pitch % 12];
13503
+ const { octave, name } = this.spell(pitch);
13610
13504
  if (lastOctave === -1 || Math.abs(octave - lastOctave) >= 2) {
13611
13505
  return { text: `o${octave}${name}`, currentOctave: octave };
13612
13506
  }
@@ -13629,7 +13523,7 @@ var MMLCore = class _MMLCore {
13629
13523
  * 各音符の長さを忠実に出力する(次の音符がなければ曲末まで伸ばせる)。
13630
13524
  */
13631
13525
  generateMML = (volumeOverride) => {
13632
- const config = getRenderConfig();
13526
+ const config = this.getConfig();
13633
13527
  const vol = volumeOverride ?? this.volume;
13634
13528
  const header = `t${this.tempo} v${vol}`;
13635
13529
  const segments = [];
@@ -13670,14 +13564,13 @@ var MMLCore = class _MMLCore {
13670
13564
  const actualStepGenerated = this.getStepFromDottedMML(durStr);
13671
13565
  if (notes.length > 1) {
13672
13566
  const noteStrs = notes.map((n) => {
13673
- const oct = Math.floor(n.pitch / 12) - 1;
13674
- const name = PITCH_MAP2[n.pitch % 12];
13567
+ const { octave: oct, name } = this.spell(n.pitchUnits);
13675
13568
  return `o${oct}${name}`;
13676
13569
  });
13677
13570
  segments.push(`[${noteStrs.join("")}]${durStr}`);
13678
13571
  } else {
13679
13572
  const { text, currentOctave } = this.getNoteWithOctave(
13680
- notes[0].pitch,
13573
+ notes[0].pitchUnits,
13681
13574
  lastOctave
13682
13575
  );
13683
13576
  segments.push(`${text}${durStr}`);
@@ -13708,7 +13601,7 @@ var MMLCore = class _MMLCore {
13708
13601
  * MMLの音長文字列("4", "4.", "12"など)をステップ数に変換する
13709
13602
  */
13710
13603
  getStepFromDottedMML(durStr) {
13711
- const config = getRenderConfig();
13604
+ const config = this.getConfig();
13712
13605
  const total = config.stepsPerBar;
13713
13606
  const isDotted = durStr.endsWith(".");
13714
13607
  const baseDur = parseInt(isDotted ? durStr.slice(0, -1) : durStr, 10);
@@ -13718,7 +13611,7 @@ var MMLCore = class _MMLCore {
13718
13611
  };
13719
13612
  var decomposeToMonophonic = (notes) => {
13720
13613
  const sorted = [...notes].sort(
13721
- (a, b) => a.startStep - b.startStep || a.pitch - b.pitch
13614
+ (a, b) => a.startStep - b.startStep || a.pitchUnits - b.pitchUnits
13722
13615
  );
13723
13616
  const tracks = [];
13724
13617
  const trackEnds = [];
@@ -13753,6 +13646,493 @@ var isChordHeavyTrack = (notes, threshold = 0.6) => {
13753
13646
  return chordNotes / notes.length >= threshold;
13754
13647
  };
13755
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
+
13756
14136
  // src/daw.ts
13757
14137
  var CHORD_INFO_HTML2 = `
13758
14138
  <div class="dtm-modal-body-content">
@@ -14039,6 +14419,27 @@ var MIDI_INFO_HTML = `
14039
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>
14040
14420
  </div>
14041
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
+ `;
14042
14443
  var KOE_INFO_HTML = `
14043
14444
  <div class="dtm-modal-body-content">
14044
14445
  <h4>1. UTAU\u97F3\u6E90\u3092 .koe \u306B\u5909\u63DB\u3059\u308B</h4>
@@ -14256,7 +14657,7 @@ var AUTO_ROLE_BASE_VOLUME = {
14256
14657
  chord: 72
14257
14658
  };
14258
14659
  var computeAutoRoleStats = (notes) => {
14259
- 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;
14260
14661
  const avgDur = notes.reduce((s, n) => s + n.durationSteps, 0) / notes.length;
14261
14662
  const byStart = /* @__PURE__ */ new Map();
14262
14663
  for (const n of notes)
@@ -14270,8 +14671,9 @@ var computeAutoRoleStats = (notes) => {
14270
14671
  const notesPerBeat = notes.length / spanBeats;
14271
14672
  return { avgPitch, avgDur, avgPoly, maxPoly, notesPerBeat };
14272
14673
  };
14674
+ var AUTO_ROLE_BASS_UNITS = 48 * UNITS_PER_SEMITONE;
14273
14675
  var classifyTrackRole = (stats) => {
14274
- if (stats.avgPitch < 48) return "bass";
14676
+ if (stats.avgPitch < AUTO_ROLE_BASS_UNITS) return "bass";
14275
14677
  if (stats.maxPoly >= 2 && stats.avgDur >= AUTO_ROLE_STEPS_PER_BEAT)
14276
14678
  return "chord";
14277
14679
  if (stats.notesPerBeat < 1.2 && stats.avgDur >= AUTO_ROLE_STEPS_PER_BEAT * 1.5)
@@ -14283,7 +14685,10 @@ var computeAutoRoleVolume = (role, stats) => {
14283
14685
  if (role === "chord" && stats.avgPoly > 1) {
14284
14686
  vol /= Math.sqrt(stats.avgPoly);
14285
14687
  } else if (role === "bass") {
14286
- 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
+ );
14287
14692
  vol += Math.min(12, depthSemitones * 0.5);
14288
14693
  }
14289
14694
  return clamp5(Math.round(vol), 1, 127);
@@ -14375,17 +14780,48 @@ var mountDAW = (target, options = {}) => {
14375
14780
  refs.drumVolumeLabel.textContent = `${options.drumVolume ?? 80}%`;
14376
14781
  const renderConfig = {
14377
14782
  stepsPerBar: 192,
14378
- keyCount: 128,
14379
- pitchRangeStart: 0,
14783
+ keyCount: KEY_COUNT,
14784
+ pitchRangeStart: PITCH_RANGE_START,
14785
+ unitsPerRow: unitsPerRow(12),
14786
+ edo: 12,
14380
14787
  keyHeight: BASE_KEY_HEIGHT,
14381
14788
  stepWidth: BASE_STEP_WIDTH * 2
14382
14789
  // zoom100% 相当
14383
14790
  };
14791
+ let renderer;
14384
14792
  let zoomX = 100;
14385
14793
  let zoomY = 100;
14386
14794
  let bpm = options.defaultBpm ?? DEFAULT_BPM;
14387
14795
  let masterVolume = options.masterVolume ?? 50;
14388
- 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
+ };
14389
14825
  let reverbAmount = options.reverbAmount ?? 0;
14390
14826
  let reverbDecay = options.reverbDecay ?? DEFAULT_REVERB_DECAY_SEC;
14391
14827
  let reverbPreDelay = options.reverbPreDelay ?? DEFAULT_REVERB_PREDELAY_MS;
@@ -14443,8 +14879,8 @@ var mountDAW = (target, options = {}) => {
14443
14879
  let snapGridSteps = 12;
14444
14880
  const gridLineSteps = 48;
14445
14881
  let currentOffsetX = 0;
14446
- const _initPitch = options.initialScrollPitch ?? 48;
14447
- 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;
14448
14884
  let playStartStep = 0;
14449
14885
  let isSolo = false;
14450
14886
  let lyricTrackIndices = /* @__PURE__ */ new Set();
@@ -14539,21 +14975,28 @@ var mountDAW = (target, options = {}) => {
14539
14975
  if (!ready) return;
14540
14976
  if (!suppressPatch && options.onNotesPatch) {
14541
14977
  const prevByKey = new Map(
14542
- prevNotes.map((n) => [`${n.startStep}_${n.pitch}`, n])
14978
+ prevNotes.map((n) => [`${n.startStep}_${n.pitchUnits}`, n])
14543
14979
  );
14544
14980
  const currByKey = new Map(
14545
- notes.map((n) => [`${n.startStep}_${n.pitch}`, n])
14981
+ notes.map((n) => [`${n.startStep}_${n.pitchUnits}`, n])
14546
14982
  );
14547
14983
  const added = notes.filter((n) => {
14548
- const prev = prevByKey.get(`${n.startStep}_${n.pitch}`);
14984
+ const prev = prevByKey.get(
14985
+ `${n.startStep}_${n.pitchUnits}`
14986
+ );
14549
14987
  return !prev || prev.durationSteps !== n.durationSteps || prev.velocity !== n.velocity;
14550
14988
  }).map((n) => ({
14551
14989
  startStep: n.startStep,
14552
- pitch: n.pitch,
14990
+ pitchUnits: n.pitchUnits,
14553
14991
  durationSteps: n.durationSteps,
14554
14992
  velocity: n.velocity
14555
14993
  }));
14556
- 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
+ }));
14557
15000
  if (added.length > 0 || removed.length > 0) {
14558
15001
  options.onNotesPatch(config.id, added, removed);
14559
15002
  }
@@ -14563,7 +15006,8 @@ var mountDAW = (target, options = {}) => {
14563
15006
  updateUndoRedo();
14564
15007
  }
14565
15008
  },
14566
- config.volume
15009
+ config.volume,
15010
+ () => renderConfig
14567
15011
  ),
14568
15012
  volume: config.volume,
14569
15013
  savedChordInput: "",
@@ -14658,18 +15102,18 @@ var mountDAW = (target, options = {}) => {
14658
15102
  return (lastNoteMeasure + 2) * renderConfig.stepsPerBar;
14659
15103
  };
14660
15104
  const getMaxOffsetX = () => {
14661
- const canvas = getGridCanvas();
15105
+ const canvas = renderer.getGridCanvas();
14662
15106
  const maxNoteStep = getMaxNoteStep();
14663
15107
  const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
14664
15108
  return Math.max(0, totalContentWidth - canvas.width);
14665
15109
  };
14666
15110
  const getMaxOffsetY = () => {
14667
15111
  const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
14668
- return Math.max(0, totalHeight - getGridCanvas().height);
15112
+ return Math.max(0, totalHeight - renderer.getGridCanvas().height);
14669
15113
  };
14670
15114
  const drawStartLine = () => {
14671
- const ctx = getGridContext();
14672
- const canvas = getGridCanvas();
15115
+ const ctx = renderer.getGridContext();
15116
+ const canvas = renderer.getGridCanvas();
14673
15117
  if (!ctx) return;
14674
15118
  const x2 = playStartStep * renderConfig.stepWidth - currentOffsetX;
14675
15119
  if (x2 < -10 || x2 > canvas.width + 10) return;
@@ -14684,8 +15128,8 @@ var mountDAW = (target, options = {}) => {
14684
15128
  ctx.restore();
14685
15129
  };
14686
15130
  const drawPlayhead = () => {
14687
- const ctx = getGridContext();
14688
- const canvas = getGridCanvas();
15131
+ const ctx = renderer.getGridContext();
15132
+ const canvas = renderer.getGridCanvas();
14689
15133
  if (!ctx) return;
14690
15134
  const x2 = currentPlayStep * renderConfig.stepWidth - currentOffsetX;
14691
15135
  if (x2 < 0 || x2 > canvas.width) return;
@@ -14699,7 +15143,7 @@ var mountDAW = (target, options = {}) => {
14699
15143
  ctx.restore();
14700
15144
  };
14701
15145
  const redrawAll = () => {
14702
- drawGrid(gridLineSteps);
15146
+ renderer.drawGrid(gridLineSteps);
14703
15147
  let lyricTargetNotes = null;
14704
15148
  let activeTrackState = null;
14705
15149
  for (const t of trackStates) {
@@ -14711,16 +15155,16 @@ var mountDAW = (target, options = {}) => {
14711
15155
  }
14712
15156
  const [r, g, b] = t.config.color;
14713
15157
  const notes = t.core.getNotes();
14714
- drawNotes(notes, [r, g, b, 1], false);
15158
+ renderer.drawNotes(notes, [r, g, b, 1], false);
14715
15159
  }
14716
15160
  if (activeTrackState) {
14717
15161
  const [r, g, b] = activeTrackState.config.color;
14718
15162
  const notes = activeTrackState.core.getNotes();
14719
- drawNotes(notes, [r, g, b, 1], true);
15163
+ renderer.drawNotes(notes, [r, g, b, 1], true);
14720
15164
  lyricTargetNotes = notes;
14721
15165
  }
14722
15166
  if (activeToolMode === "select" && selectionRect) {
14723
- const ctx = getGridContext();
15167
+ const ctx = renderer.getGridContext();
14724
15168
  ctx.save();
14725
15169
  ctx.strokeStyle = "#ffec27";
14726
15170
  ctx.lineWidth = 2;
@@ -14743,19 +15187,19 @@ var mountDAW = (target, options = {}) => {
14743
15187
  if (activeToolMode === "select" && selectedNotes.length > 0) {
14744
15188
  const ids = new Set(selectedNotes.map((n) => n.id));
14745
15189
  const active = getActive();
14746
- drawSelectedNotes(active.core.getNotes(), ids, [
15190
+ renderer.drawSelectedNotes(active.core.getNotes(), ids, [
14747
15191
  ...active.config.color,
14748
15192
  1
14749
15193
  ]);
14750
15194
  }
14751
15195
  if (lyricTargetNotes)
14752
- drawNoteLyrics(lyricTargetNotes, getActiveLyricKana());
15196
+ renderer.drawNoteLyrics(lyricTargetNotes, getActiveLyricKana());
14753
15197
  drawStartLine();
14754
15198
  if (playbackState === "playing") drawPlayhead();
14755
15199
  updateScrollbars();
14756
15200
  };
14757
15201
  const updateScrollbars = () => {
14758
- const canvas = getGridCanvas();
15202
+ const canvas = renderer.getGridCanvas();
14759
15203
  const maxOffsetX = getMaxOffsetX();
14760
15204
  const sbW = refs.hScroll.clientWidth;
14761
15205
  if (maxOffsetX <= 0) {
@@ -14839,7 +15283,7 @@ var mountDAW = (target, options = {}) => {
14839
15283
  const x2 = clamp5(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
14840
15284
  const ratio = x2 / (rect.width - thumbW);
14841
15285
  currentOffsetX = clamp5(ratio * maxOffsetX, 0, maxOffsetX);
14842
- setDrawOffset(currentOffsetX, currentOffsetY);
15286
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14843
15287
  redrawAll();
14844
15288
  };
14845
15289
  const moveV = (clientY) => {
@@ -14850,7 +15294,7 @@ var mountDAW = (target, options = {}) => {
14850
15294
  const y = clamp5(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
14851
15295
  const ratio = y / (rect.height - thumbH);
14852
15296
  currentOffsetY = clamp5(ratio * maxOffset, 0, maxOffset);
14853
- setDrawOffset(currentOffsetX, currentOffsetY);
15297
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14854
15298
  redrawAll();
14855
15299
  };
14856
15300
  };
@@ -14889,8 +15333,8 @@ var mountDAW = (target, options = {}) => {
14889
15333
  const autoScrollTick = () => {
14890
15334
  autoScrollRAF = null;
14891
15335
  if (!isSelecting || !lastMoveEvent) return;
14892
- const canvas = getGridCanvas();
14893
- const { x: x2, y } = getGridPosition(lastMoveEvent);
15336
+ const canvas = renderer.getGridCanvas();
15337
+ const { x: x2, y } = renderer.getGridPosition(lastMoveEvent);
14894
15338
  const dx = computeEdgeSpeed(x2, canvas.width);
14895
15339
  const dy = computeEdgeSpeed(y, canvas.height);
14896
15340
  if (dx !== 0 || dy !== 0) {
@@ -14898,7 +15342,7 @@ var mountDAW = (target, options = {}) => {
14898
15342
  const maxOffsetY = getMaxOffsetY();
14899
15343
  currentOffsetX = clamp5(currentOffsetX + dx, 0, maxOffsetX);
14900
15344
  currentOffsetY = clamp5(currentOffsetY + dy, 0, maxOffsetY);
14901
- setDrawOffset(currentOffsetX, currentOffsetY);
15345
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14902
15346
  onPointerMove(lastMoveEvent);
14903
15347
  }
14904
15348
  if (isSelecting) {
@@ -14919,11 +15363,17 @@ var mountDAW = (target, options = {}) => {
14919
15363
  };
14920
15364
  const findActiveNoteAt = (x2, y, margin = 0) => {
14921
15365
  const active = getActive();
14922
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
14923
- 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();
14924
15374
  for (const note of active.core.getNotes()) {
14925
15375
  const logicalX = note.startStep * stepWidth;
14926
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
15376
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
14927
15377
  const logicalY = yIndex * keyHeight;
14928
15378
  const w = note.durationSteps * stepWidth;
14929
15379
  const renderX = logicalX - offset.x;
@@ -14936,7 +15386,7 @@ var mountDAW = (target, options = {}) => {
14936
15386
  const hasNoteAt = (step, pitch, excludeId) => {
14937
15387
  const active = getActive();
14938
15388
  return active.core.getNotes().some(
14939
- (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
14940
15390
  );
14941
15391
  };
14942
15392
  const snapToGrid = (duration) => Math.max(
@@ -14947,7 +15397,7 @@ var mountDAW = (target, options = {}) => {
14947
15397
  const onGridPointerDown = (event) => {
14948
15398
  event.preventDefault();
14949
15399
  options.onResumeAudio?.();
14950
- const { x: x2, y, step, pitch } = getGridPosition(event);
15400
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(event);
14951
15401
  const active = getActive();
14952
15402
  if (activeToolMode === "eraser") {
14953
15403
  if (isActiveLocked()) return;
@@ -14962,7 +15412,7 @@ var mountDAW = (target, options = {}) => {
14962
15412
  selectedOriginal = selectedNotes.map((n) => ({
14963
15413
  id: n.id,
14964
15414
  startStep: n.startStep,
14965
- pitch: n.pitch
15415
+ pitch: n.pitchUnits
14966
15416
  }));
14967
15417
  isSelecting = true;
14968
15418
  dragMode = "move";
@@ -14981,7 +15431,7 @@ var mountDAW = (target, options = {}) => {
14981
15431
  {
14982
15432
  id: clicked.id,
14983
15433
  startStep: clicked.startStep,
14984
- pitch: clicked.pitch
15434
+ pitch: clicked.pitchUnits
14985
15435
  }
14986
15436
  ];
14987
15437
  isSelecting = true;
@@ -14999,9 +15449,9 @@ var mountDAW = (target, options = {}) => {
14999
15449
  hasDragged = false;
15000
15450
  const existing = findActiveNoteAt(x2, y, TOUCH_HIT_MARGIN);
15001
15451
  if (existing) {
15002
- playPreview(existing.pitch);
15452
+ playPreview(existing.pitchUnits);
15003
15453
  const { stepWidth } = renderConfig;
15004
- const offset = getDrawOffset();
15454
+ const offset = renderer.getDrawOffset();
15005
15455
  const renderX = existing.startStep * stepWidth - offset.x;
15006
15456
  const w = existing.durationSteps * stepWidth;
15007
15457
  if (x2 >= renderX + w - resizeHandleWidth && x2 <= renderX + w) {
@@ -15012,17 +15462,17 @@ var mountDAW = (target, options = {}) => {
15012
15462
  dragOffsetPitch: 0,
15013
15463
  startStep: existing.startStep,
15014
15464
  durationSteps: existing.durationSteps,
15015
- lastPreviewPitch: existing.pitch
15465
+ lastPreviewPitch: existing.pitchUnits
15016
15466
  };
15017
15467
  } else {
15018
15468
  dragState = {
15019
15469
  noteId: existing.id,
15020
15470
  mode: "move",
15021
15471
  dragOffsetStep: step - existing.startStep,
15022
- dragOffsetPitch: pitch - existing.pitch,
15472
+ dragOffsetPitch: pitch - existing.pitchUnits,
15023
15473
  startStep: existing.startStep,
15024
15474
  durationSteps: existing.durationSteps,
15025
- lastPreviewPitch: existing.pitch
15475
+ lastPreviewPitch: existing.pitchUnits
15026
15476
  };
15027
15477
  }
15028
15478
  suppressClick = true;
@@ -15033,14 +15483,14 @@ var mountDAW = (target, options = {}) => {
15033
15483
  const newStart = snappedStep;
15034
15484
  const newEnd = newStart + currentInsertLength;
15035
15485
  const overlapping = active.core.getNotes().some(
15036
- (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
15037
15487
  );
15038
15488
  if (!overlapping) {
15039
15489
  active.core.addNote(snappedStep, pitch, {
15040
15490
  noteLengthSteps: currentInsertLength
15041
15491
  });
15042
15492
  playPreview(pitch);
15043
- 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);
15044
15494
  if (newNote) {
15045
15495
  dragState = {
15046
15496
  noteId: newNote.id,
@@ -15049,7 +15499,7 @@ var mountDAW = (target, options = {}) => {
15049
15499
  dragOffsetPitch: 0,
15050
15500
  startStep: newNote.startStep,
15051
15501
  durationSteps: newNote.durationSteps,
15052
- lastPreviewPitch: newNote.pitch
15502
+ lastPreviewPitch: newNote.pitchUnits
15053
15503
  };
15054
15504
  hasDragged = true;
15055
15505
  }
@@ -15060,7 +15510,7 @@ var mountDAW = (target, options = {}) => {
15060
15510
  const active = getActive();
15061
15511
  if (activeToolMode === "pen") {
15062
15512
  if (!dragState) return;
15063
- const { step, pitch } = getGridPosition(event);
15513
+ const { step, pitch } = renderer.getGridPosition(event);
15064
15514
  hasDragged = true;
15065
15515
  if (dragState.mode === "move") {
15066
15516
  const nextStart = step - dragState.dragOffsetStep;
@@ -15084,7 +15534,7 @@ var mountDAW = (target, options = {}) => {
15084
15534
  }
15085
15535
  if (activeToolMode === "select" && isSelecting && selectionStart) {
15086
15536
  ensureAutoScroll(event);
15087
- const { x: x2, y, step, pitch } = getGridPosition(event);
15537
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(event);
15088
15538
  if (dragMode === "rect") {
15089
15539
  const rect = {
15090
15540
  x: Math.min(x2, selectionStart.x),
@@ -15093,11 +15543,17 @@ var mountDAW = (target, options = {}) => {
15093
15543
  height: Math.abs(y - selectionStart.y)
15094
15544
  };
15095
15545
  selectionRect = rect;
15096
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
15097
- 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();
15098
15554
  selectedNotes = active.core.getNotes().filter((note) => {
15099
15555
  const logicalX = note.startStep * stepWidth;
15100
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
15556
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
15101
15557
  const logicalY = yIndex * keyHeight;
15102
15558
  const nx = logicalX - offset.x;
15103
15559
  const ny = logicalY - offset.y;
@@ -15116,7 +15572,7 @@ var mountDAW = (target, options = {}) => {
15116
15572
  const orig = selectedOriginal.find((o) => o.id === note.id);
15117
15573
  if (!orig) continue;
15118
15574
  const newPitch = orig.pitch + deltaPitch;
15119
- if (newPitch >= 0 && newPitch < 128)
15575
+ if (newPitch >= PITCH_RANGE_START && newPitch <= PITCH_RANGE_END)
15120
15576
  active.core.moveNote(
15121
15577
  note.id,
15122
15578
  orig.startStep + snappedDelta,
@@ -15264,22 +15720,23 @@ var mountDAW = (target, options = {}) => {
15264
15720
  refs.bgOpacityRow.classList.add("dtm-hidden");
15265
15721
  }
15266
15722
  refs.bgRemoveBtn.classList.toggle("dtm-hidden", !blob2);
15267
- setBackgroundActive(!!blob2);
15723
+ renderer.setBackgroundActive(!!blob2);
15268
15724
  redrawAll();
15269
15725
  };
15270
15726
  const setupCanvas = () => {
15271
15727
  const w = refs.rollContainer.clientWidth || 800;
15272
15728
  const h = refs.rollContainer.clientHeight || 450;
15273
- init(refs.wrapper, w, h, renderConfig);
15274
- const gridCanvas = getGridCanvas();
15729
+ renderer?.destroy();
15730
+ renderer = createRenderer(refs.wrapper, w, h, renderConfig);
15731
+ const gridCanvas = renderer.getGridCanvas();
15275
15732
  gridCanvas.addEventListener("pointerdown", onGridPointerDown);
15276
15733
  gridCanvas.addEventListener("dblclick", (event) => {
15277
15734
  event.preventDefault();
15278
15735
  if (isActiveLocked()) return;
15279
- const { step, pitch } = getGridPosition(event);
15736
+ const { step, pitch } = renderer.getGridPosition(event);
15280
15737
  const active = getActive();
15281
15738
  const note = active.core.getNotes().find(
15282
- (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
15283
15740
  );
15284
15741
  if (note) active.core.deleteNoteById(note.id);
15285
15742
  });
@@ -15297,7 +15754,7 @@ var mountDAW = (target, options = {}) => {
15297
15754
  0,
15298
15755
  getMaxOffsetX()
15299
15756
  );
15300
- setDrawOffset(currentOffsetX, currentOffsetY);
15757
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15301
15758
  redrawAll();
15302
15759
  },
15303
15760
  { passive: false }
@@ -15307,7 +15764,7 @@ var mountDAW = (target, options = {}) => {
15307
15764
  suppressClick = false;
15308
15765
  }
15309
15766
  });
15310
- const headerCanvas = getHeaderCanvas();
15767
+ const headerCanvas = renderer.getHeaderCanvas();
15311
15768
  headerCanvas.addEventListener("click", (event) => {
15312
15769
  if (playbackState === "playing") return;
15313
15770
  const rect = headerCanvas.getBoundingClientRect();
@@ -15323,11 +15780,11 @@ var mountDAW = (target, options = {}) => {
15323
15780
  }
15324
15781
  redrawAll();
15325
15782
  });
15326
- setDrawOffset(currentOffsetX, currentOffsetY);
15783
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15327
15784
  redrawAll();
15328
15785
  };
15329
15786
  const applyZoomX = () => {
15330
- const canvas = getGridCanvas();
15787
+ const canvas = renderer.getGridCanvas();
15331
15788
  const centerStep = (currentOffsetX + canvas.width / 2) / renderConfig.stepWidth;
15332
15789
  renderConfig.stepWidth = BASE_STEP_WIDTH * (zoomX * 2) / 100;
15333
15790
  refs.zoomXLabel.textContent = `${zoomX}%`;
@@ -15336,11 +15793,11 @@ var mountDAW = (target, options = {}) => {
15336
15793
  0,
15337
15794
  getMaxOffsetX()
15338
15795
  );
15339
- setDrawOffset(currentOffsetX, currentOffsetY);
15796
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15340
15797
  redrawAll();
15341
15798
  };
15342
15799
  const applyZoomY = () => {
15343
- const canvas = getGridCanvas();
15800
+ const canvas = renderer.getGridCanvas();
15344
15801
  const centerKey = (currentOffsetY + canvas.height / 2) / renderConfig.keyHeight;
15345
15802
  renderConfig.keyHeight = BASE_KEY_HEIGHT * zoomY / 100;
15346
15803
  refs.zoomYLabel.textContent = `${zoomY}%`;
@@ -15349,7 +15806,7 @@ var mountDAW = (target, options = {}) => {
15349
15806
  0,
15350
15807
  getMaxOffsetY()
15351
15808
  );
15352
- setDrawOffset(currentOffsetX, currentOffsetY);
15809
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15353
15810
  redrawAll();
15354
15811
  };
15355
15812
  const getViewState = () => ({
@@ -15361,7 +15818,14 @@ var mountDAW = (target, options = {}) => {
15361
15818
  const notifyViewState = () => options.onViewStateChange?.(getViewState());
15362
15819
  const dispatchNote = (trackId, pitch, trackVol, velocity, when, duration) => {
15363
15820
  const volume = trackVol / 100 * (velocity / 127) * (masterVolume / 100);
15364
- 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
+ });
15365
15829
  };
15366
15830
  const sequencer = createSequencer({
15367
15831
  getTracks: () => trackStates.map((t) => ({
@@ -15389,7 +15853,7 @@ var mountDAW = (target, options = {}) => {
15389
15853
  },
15390
15854
  onTick: (step) => {
15391
15855
  currentPlayStep = step;
15392
- const canvas = getGridCanvas();
15856
+ const canvas = renderer.getGridCanvas();
15393
15857
  const visibleSteps = canvas.width / renderConfig.stepWidth;
15394
15858
  const threshold = currentOffsetX / renderConfig.stepWidth + visibleSteps - 4;
15395
15859
  if (currentPlayStep > threshold) {
@@ -15399,7 +15863,7 @@ var mountDAW = (target, options = {}) => {
15399
15863
  0,
15400
15864
  getMaxOffsetX()
15401
15865
  );
15402
- setDrawOffset(currentOffsetX, currentOffsetY);
15866
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15403
15867
  }
15404
15868
  redrawAll();
15405
15869
  },
@@ -15430,7 +15894,7 @@ var mountDAW = (target, options = {}) => {
15430
15894
  (a, b) => a.startStep - b.startStep
15431
15895
  );
15432
15896
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
15433
- const semis = (lt.octave ?? 0) * 12;
15897
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
15434
15898
  const count = Math.min(sorted.length, lt.syllables.length);
15435
15899
  const notes = [];
15436
15900
  for (let i2 = 0; i2 < count; i2++) {
@@ -15438,7 +15902,7 @@ var mountDAW = (target, options = {}) => {
15438
15902
  if (n.startStep < fromStep) continue;
15439
15903
  notes.push({
15440
15904
  syllable: lt.syllables[i2],
15441
- pitch: n.pitch + semis,
15905
+ pitch: n.pitchUnits + semis,
15442
15906
  startSec: (n.startStep - fromStep) * secondsPerStep,
15443
15907
  durationSec: n.durationSteps * secondsPerStep * gate
15444
15908
  });
@@ -15481,13 +15945,13 @@ var mountDAW = (target, options = {}) => {
15481
15945
  }
15482
15946
  }
15483
15947
  if (playbackState !== "paused") {
15484
- const canvas = getGridCanvas();
15948
+ const canvas = renderer.getGridCanvas();
15485
15949
  currentOffsetX = clamp5(
15486
15950
  playStartStep * renderConfig.stepWidth - canvas.width * 0.5,
15487
15951
  0,
15488
15952
  getMaxOffsetX()
15489
15953
  );
15490
- setDrawOffset(currentOffsetX, currentOffsetY);
15954
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15491
15955
  }
15492
15956
  playbackState = "playing";
15493
15957
  sequencer.start(fromStep);
@@ -16474,6 +16938,7 @@ var mountDAW = (target, options = {}) => {
16474
16938
  fadeIn: Math.round(fadeInSec * 10),
16475
16939
  fadeOut: Math.round(fadeOutSec * 10),
16476
16940
  mode,
16941
+ edo: renderConfig.edo,
16477
16942
  trackInstruments: trackInstMeta,
16478
16943
  trackCompression: trackCompMeta,
16479
16944
  trackWidth: trackWidthMeta,
@@ -16502,6 +16967,7 @@ var mountDAW = (target, options = {}) => {
16502
16967
  fadeIn: Math.round(fadeInSec * 10),
16503
16968
  fadeOut: Math.round(fadeOutSec * 10),
16504
16969
  mode,
16970
+ edo: renderConfig.edo,
16505
16971
  trackInstruments: trackInstMeta,
16506
16972
  trackCompression: trackCompMeta,
16507
16973
  trackWidth: trackWidthMeta,
@@ -16641,19 +17107,19 @@ var mountDAW = (target, options = {}) => {
16641
17107
  }
16642
17108
  }
16643
17109
  if (candidateNotes.length === 0) return null;
16644
- const sum = candidateNotes.reduce((acc, note) => acc + note.pitch, 0);
17110
+ const sum = candidateNotes.reduce((acc, note) => acc + note.pitchUnits, 0);
16645
17111
  return Math.round(sum / candidateNotes.length);
16646
17112
  };
16647
17113
  const centerPitch = (pitch) => {
16648
- const canvas = getGridCanvas();
16649
- 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);
16650
17116
  const logicalY = yIndex * renderConfig.keyHeight;
16651
17117
  currentOffsetY = clamp5(
16652
17118
  logicalY - (canvas.height - renderConfig.keyHeight) / 2,
16653
17119
  0,
16654
17120
  getMaxOffsetY()
16655
17121
  );
16656
- setDrawOffset(currentOffsetX, currentOffsetY);
17122
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
16657
17123
  };
16658
17124
  const clearAll = () => {
16659
17125
  for (const t of trackStates) {
@@ -16711,10 +17177,10 @@ var mountDAW = (target, options = {}) => {
16711
17177
  if (!meta.drumFont) applyDrumPatternFont(meta.drum);
16712
17178
  }
16713
17179
  if (meta.volume !== void 0) {
16714
- masterVolume = meta.volume;
16715
- refs.masterVolume.value = String(meta.volume);
16716
- refs.masterVolumeLabel.textContent = `${meta.volume}%`;
17180
+ applyMasterVolume(meta.volume);
16717
17181
  }
17182
+ applyEdo(meta.edo ?? 12);
17183
+ refs.edoSelect.value = String(renderConfig.edo ?? 12);
16718
17184
  if (meta.drumVolume !== void 0) {
16719
17185
  drumVolume = meta.drumVolume;
16720
17186
  refs.drumVolume.value = String(meta.drumVolume);
@@ -16879,7 +17345,7 @@ var mountDAW = (target, options = {}) => {
16879
17345
  if (applyActiveOnly && p.trackIndex !== activeTrackIndex) continue;
16880
17346
  const t = trackStates[p.trackIndex];
16881
17347
  if (!t) continue;
16882
- t.core.addNote(p.startStep, p.pitch, {
17348
+ t.core.addNote(p.startStep, p.pitchUnits, {
16883
17349
  noteLengthSteps: p.durationSteps,
16884
17350
  velocity: DEFAULT_VELOCITY
16885
17351
  });
@@ -16896,7 +17362,7 @@ var mountDAW = (target, options = {}) => {
16896
17362
  if (firstPitch !== null) {
16897
17363
  centerPitch(firstPitch);
16898
17364
  } else {
16899
- centerPitch(48);
17365
+ centerPitch(pitchV1ToUnits(48));
16900
17366
  }
16901
17367
  redrawAll();
16902
17368
  updateTrackPanel();
@@ -16914,6 +17380,8 @@ var mountDAW = (target, options = {}) => {
16914
17380
  const chordTrack = trackStates.find((t) => t.config.id === "chord");
16915
17381
  if (!chordTrack) return;
16916
17382
  const placements = buildChordPlacements({
17383
+ // 和音は五度連鎖経由で音律の格子へ写す(31平均律では長3度が10度になる)
17384
+ edo: renderConfig.edo,
16917
17385
  chordStr: active.savedChordInput,
16918
17386
  patternType: active.savedChordPattern,
16919
17387
  rootShift: active.savedChordRoot,
@@ -16923,7 +17391,7 @@ var mountDAW = (target, options = {}) => {
16923
17391
  chordTrack.core.clearNotesWithoutHistory();
16924
17392
  chordTrack.core.beginBatch();
16925
17393
  for (const p of placements) {
16926
- chordTrack.core.addNote(p.startStep, p.pitch, {
17394
+ chordTrack.core.addNote(p.startStep, p.pitchUnits, {
16927
17395
  noteLengthSteps: Math.max(1, p.durationSteps),
16928
17396
  velocity: p.velocity
16929
17397
  });
@@ -16961,6 +17429,8 @@ var mountDAW = (target, options = {}) => {
16961
17429
  active.vocalOctaveUnison = "none";
16962
17430
  } else {
16963
17431
  clearAll();
17432
+ applyEdo(12);
17433
+ refs.edoSelect.value = String(renderConfig.edo ?? 12);
16964
17434
  for (const t of trackStates) t.core.setLoadMode(true);
16965
17435
  for (const t of trackStates) {
16966
17436
  t.lyrics = "";
@@ -16987,7 +17457,7 @@ var mountDAW = (target, options = {}) => {
16987
17457
  if (applyActiveOnly && p.trackId !== activeTrackId) continue;
16988
17458
  const t = trackStates.find((ts) => ts.config.id === p.trackId);
16989
17459
  if (!t) continue;
16990
- t.core.addNote(p.startStep, p.pitch, {
17460
+ t.core.addNote(p.startStep, snapToEdoGrid(pitchV1ToUnits(p.pitch)), {
16991
17461
  noteLengthSteps: p.durationSteps,
16992
17462
  velocity: p.velocity
16993
17463
  });
@@ -17004,7 +17474,7 @@ var mountDAW = (target, options = {}) => {
17004
17474
  if (firstPitch !== null) {
17005
17475
  centerPitch(firstPitch);
17006
17476
  } else {
17007
- centerPitch(48);
17477
+ centerPitch(pitchV1ToUnits(48));
17008
17478
  }
17009
17479
  redrawAll();
17010
17480
  updateTrackPanel();
@@ -17068,18 +17538,21 @@ var mountDAW = (target, options = {}) => {
17068
17538
  }
17069
17539
  }, 30);
17070
17540
  };
17071
- 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";
17072
17545
  const overlay = document.createElement("div");
17073
17546
  overlay.className = "dtm-modal-overlay";
17074
17547
  overlay.innerHTML = `
17075
17548
  <div class="dtm-modal">
17076
17549
  <div class="dtm-modal-header">
17077
- <span class="dtm-modal-title">\u30E2\u30FC\u30C9\u306E\u78BA\u8A8D</span>
17550
+ <span class="dtm-modal-title">${title}</span>
17078
17551
  </div>
17079
17552
  <div class="dtm-modal-body"><p>${message}</p></div>
17080
17553
  <div class="dtm-confirm-footer">
17081
- <button class="dtm-btn dtm-btn--ghost dtm-confirm-no">\u3044\u3044\u3048\uFF08\u3053\u306E\u307E\u307E\u8AAD\u307F\u8FBC\u3080\uFF09</button>
17082
- <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>
17083
17556
  </div>
17084
17557
  </div>`;
17085
17558
  const close = (result) => {
@@ -17187,10 +17660,36 @@ var mountDAW = (target, options = {}) => {
17187
17660
  });
17188
17661
  refs.decomposeChordToggle.addEventListener("change", notifyViewState);
17189
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
+ });
17190
17691
  refs.masterVolume.addEventListener("input", () => {
17191
- masterVolume = Number.parseInt(refs.masterVolume.value, 10) || 0;
17192
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17193
- options.singingVoices?.setVolume(masterVolume / 100);
17692
+ applyMasterVolume(Number.parseInt(refs.masterVolume.value, 10) || 0);
17194
17693
  });
17195
17694
  refs.masterComp.addEventListener("input", () => {
17196
17695
  masterCompression = Number.parseInt(refs.masterComp.value, 10) || 0;
@@ -17254,10 +17753,7 @@ var mountDAW = (target, options = {}) => {
17254
17753
  const scale = targetPeak / observedPeakMax;
17255
17754
  const suggested = clamp5(Math.round(masterVolume * scale), 10, 100);
17256
17755
  if (suggested !== masterVolume) {
17257
- masterVolume = suggested;
17258
- refs.masterVolume.value = String(masterVolume);
17259
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17260
- options.singingVoices?.setVolume(masterVolume / 100);
17756
+ applyMasterVolume(suggested);
17261
17757
  }
17262
17758
  observedPeakMax = 0;
17263
17759
  observedPlayMs = 0;
@@ -17417,7 +17913,8 @@ var mountDAW = (target, options = {}) => {
17417
17913
  generateRandomPattern(getActive().core, {
17418
17914
  stepsPerBar: renderConfig.stepsPerBar,
17419
17915
  startStep: playStartStep,
17420
- pitchRangeStart: renderConfig.pitchRangeStart
17916
+ pitchRangeStart: renderConfig.pitchRangeStart,
17917
+ edo: renderConfig.edo
17421
17918
  });
17422
17919
  redrawAll();
17423
17920
  });
@@ -17754,7 +18251,7 @@ var mountDAW = (target, options = {}) => {
17754
18251
  id: 0,
17755
18252
  startStep: p.startStep,
17756
18253
  durationSteps: p.durationSteps,
17757
- pitch: p.pitch,
18254
+ pitchUnits: snapToEdoGrid(pitchV1ToUnits(p.pitch)),
17758
18255
  velocity: p.velocity
17759
18256
  }));
17760
18257
  const mml = refCore.getMMLFromNotes(asNotes, tempo, 100).trim();
@@ -18209,10 +18706,10 @@ var mountDAW = (target, options = {}) => {
18209
18706
  const newStart = playStartStep + (note.startStep - minStart);
18210
18707
  const newEnd = newStart + note.durationSteps;
18211
18708
  const overlap = notes.some(
18212
- (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
18213
18710
  );
18214
18711
  if (!overlap)
18215
- core.addNote(newStart, note.pitch, {
18712
+ core.addNote(newStart, note.pitchUnits, {
18216
18713
  noteLengthSteps: note.durationSteps,
18217
18714
  velocity: note.velocity
18218
18715
  });
@@ -18285,13 +18782,13 @@ var mountDAW = (target, options = {}) => {
18285
18782
  pausedPlayStep = step;
18286
18783
  currentPlayStep = step;
18287
18784
  playbackState = "paused";
18288
- const canvas = getGridCanvas();
18785
+ const canvas = renderer.getGridCanvas();
18289
18786
  currentOffsetX = clamp5(
18290
18787
  step * renderConfig.stepWidth - canvas.width * 0.5,
18291
18788
  0,
18292
18789
  getMaxOffsetX()
18293
18790
  );
18294
- setDrawOffset(currentOffsetX, currentOffsetY);
18791
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
18295
18792
  updateTransport();
18296
18793
  redrawAll();
18297
18794
  };
@@ -18359,16 +18856,10 @@ var mountDAW = (target, options = {}) => {
18359
18856
  forcePauseAt,
18360
18857
  setLoading,
18361
18858
  setMasterVolume: (volume) => {
18362
- masterVolume = clamp5(volume, 0, 100);
18363
- refs.masterVolume.value = String(masterVolume);
18364
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
18365
- options.singingVoices?.setVolume(masterVolume / 100);
18859
+ applyMasterVolume(volume);
18366
18860
  },
18367
18861
  setVolume: (volume) => {
18368
- masterVolume = clamp5(volume, 0, 100);
18369
- refs.masterVolume.value = String(masterVolume);
18370
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
18371
- options.singingVoices?.setVolume(masterVolume / 100);
18862
+ applyMasterVolume(volume);
18372
18863
  },
18373
18864
  setDrumVolume: (volume) => {
18374
18865
  drumVolume = clamp5(volume, 0, 100);
@@ -18411,15 +18902,19 @@ var mountDAW = (target, options = {}) => {
18411
18902
  suppressPatch = true;
18412
18903
  track.core.beginBatch();
18413
18904
  for (const n of added) {
18414
- 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
+ );
18415
18908
  if (existing) track.core.deleteNoteById(existing.id);
18416
- track.core.addNote(n.startStep, n.pitch, {
18909
+ track.core.addNote(n.startStep, n.pitchUnits, {
18417
18910
  noteLengthSteps: n.durationSteps,
18418
18911
  velocity: n.velocity
18419
18912
  });
18420
18913
  }
18421
18914
  for (const r of removed) {
18422
- 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
+ );
18423
18918
  if (note) track.core.deleteNoteById(note.id);
18424
18919
  }
18425
18920
  track.core.endBatch();
@@ -18461,9 +18956,9 @@ var mountDAW = (target, options = {}) => {
18461
18956
  if (t.config.id === activeTrackId) updateTrackPanel();
18462
18957
  },
18463
18958
  noteToCanvas: (step, pitch) => {
18464
- const canvas = getGridCanvas();
18959
+ const canvas = renderer.getGridCanvas();
18465
18960
  const x2 = step * renderConfig.stepWidth - currentOffsetX;
18466
- 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;
18467
18962
  const onScreen = x2 >= 0 && x2 <= canvas.width && y >= 0 && y <= canvas.height;
18468
18963
  let side = null;
18469
18964
  if (!onScreen) {
@@ -18527,7 +19022,7 @@ var playSingingMML = async (mml, options = {}) => {
18527
19022
  id: id++,
18528
19023
  startStep: p.startStep,
18529
19024
  durationSteps: p.durationSteps,
18530
- pitch: p.pitch,
19025
+ pitchUnits: p.pitchUnits,
18531
19026
  velocity: p.velocity
18532
19027
  }));
18533
19028
  return {
@@ -18553,7 +19048,7 @@ var playSingingMML = async (mml, options = {}) => {
18553
19048
  (a, b) => a.startStep - b.startStep
18554
19049
  );
18555
19050
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
18556
- const semis = (lt.octave ?? 0) * 12;
19051
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
18557
19052
  const count = Math.min(sorted.length, lt.syllables.length);
18558
19053
  const notes = [];
18559
19054
  for (let i2 = 0; i2 < count; i2++) {
@@ -18561,7 +19056,7 @@ var playSingingMML = async (mml, options = {}) => {
18561
19056
  if (n.startStep < fromStep) continue;
18562
19057
  notes.push({
18563
19058
  syllable: lt.syllables[i2],
18564
- pitch: n.pitch + semis,
19059
+ pitch: n.pitchUnits + semis,
18565
19060
  startSec: (n.startStep - fromStep) * secondsPerStep,
18566
19061
  durationSec: n.durationSteps * secondsPerStep * gate
18567
19062
  });
@@ -18749,24 +19244,28 @@ var createPianoRoll = (options, handlers) => {
18749
19244
  config,
18750
19245
  noteLengthSteps = 1
18751
19246
  } = options;
18752
- init(mountTarget, width, height, config);
19247
+ const renderer = createRenderer(mountTarget, width, height, config);
18753
19248
  let currentNoteLengthSteps = noteLengthSteps;
18754
19249
  let selectionRect = null;
18755
19250
  let isSelecting = false;
18756
19251
  let selectionStart = null;
18757
19252
  let selectedNotes = [];
18758
19253
  let copiedNotes = [];
18759
- const core = new MMLCore({
18760
- onMMLGenerated: handlers.onMMLGenerated,
18761
- onNotesChanged: (notes) => {
18762
- handlers.onNotesChanged(notes);
18763
- }
18764
- });
19254
+ const core = new MMLCore(
19255
+ {
19256
+ onMMLGenerated: handlers.onMMLGenerated,
19257
+ onNotesChanged: (notes) => {
19258
+ handlers.onNotesChanged(notes);
19259
+ }
19260
+ },
19261
+ 80,
19262
+ () => config
19263
+ );
18765
19264
  const getAddNoteOptions = () => ({
18766
19265
  noteLengthSteps: currentNoteLengthSteps
18767
19266
  });
18768
19267
  let suppressClick = false;
18769
- onClick((step, pitch) => {
19268
+ renderer.onClick((step, pitch) => {
18770
19269
  if (suppressClick) {
18771
19270
  suppressClick = false;
18772
19271
  return;
@@ -18778,7 +19277,7 @@ var createPianoRoll = (options, handlers) => {
18778
19277
  } else if (mode === "eraser") {
18779
19278
  const notes = core.getNotes();
18780
19279
  const note = notes.find(
18781
- (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
18782
19281
  );
18783
19282
  if (note) {
18784
19283
  core.deleteNoteById(note.id);
@@ -18786,17 +19285,23 @@ var createPianoRoll = (options, handlers) => {
18786
19285
  }
18787
19286
  }
18788
19287
  });
18789
- const gridCanvas = getGridCanvas();
19288
+ const gridCanvas = renderer.getGridCanvas();
18790
19289
  const resizeHandleWidth = 6;
18791
19290
  let dragState = null;
18792
19291
  let hasDragged = false;
18793
19292
  let lastPreviewPitch = null;
18794
19293
  const findNoteAtPosition = (x2, y) => {
18795
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18796
- 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();
18797
19302
  for (const note of core.getNotes()) {
18798
19303
  const logicalX = note.startStep * stepWidth;
18799
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19304
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18800
19305
  const logicalY = yIndex * keyHeight;
18801
19306
  const w = note.durationSteps * stepWidth;
18802
19307
  const h = keyHeight;
@@ -18810,7 +19315,7 @@ var createPianoRoll = (options, handlers) => {
18810
19315
  };
18811
19316
  const handlePointerMove = (e) => {
18812
19317
  if (core.getToolMode() === "select" && isSelecting && selectionStart) {
18813
- const { x: x2, y } = getGridPosition(e);
19318
+ const { x: x2, y } = renderer.getGridPosition(e);
18814
19319
  const minX = Math.min(x2, selectionStart.x);
18815
19320
  const minY = Math.min(y, selectionStart.y);
18816
19321
  const width2 = Math.abs(x2 - selectionStart.x);
@@ -18822,7 +19327,7 @@ var createPianoRoll = (options, handlers) => {
18822
19327
  }
18823
19328
  if (!dragState) return;
18824
19329
  hasDragged = true;
18825
- const { step, pitch } = getGridPosition(e);
19330
+ const { step, pitch } = renderer.getGridPosition(e);
18826
19331
  if (dragState.mode === "move") {
18827
19332
  if (dragState.selectedNotes && dragState.selectedNotes.length > 0) {
18828
19333
  const noteId = dragState.noteId;
@@ -18831,10 +19336,10 @@ var createPianoRoll = (options, handlers) => {
18831
19336
  const nextStart2 = step - dragState.dragOffsetStep;
18832
19337
  const nextPitch2 = pitch - dragState.dragOffsetPitch;
18833
19338
  const stepDelta = nextStart2 - baseNote.startStep;
18834
- const pitchDelta = nextPitch2 - baseNote.pitch;
19339
+ const pitchDelta = nextPitch2 - baseNote.pitchUnits;
18835
19340
  for (const note of dragState.selectedNotes) {
18836
19341
  const newStart = note.startStep + stepDelta;
18837
- const newPitch = note.pitch + pitchDelta;
19342
+ const newPitch = note.pitchUnits + pitchDelta;
18838
19343
  core.moveNote(note.id, newStart, newPitch);
18839
19344
  }
18840
19345
  if (options.onPreviewSound && pitch !== lastPreviewPitch) {
@@ -18875,7 +19380,7 @@ var createPianoRoll = (options, handlers) => {
18875
19380
  }
18876
19381
  };
18877
19382
  gridCanvas.addEventListener("pointerdown", (e) => {
18878
- const { x: x2, y, step, pitch } = getGridPosition(e);
19383
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(e);
18879
19384
  const currentMode = core.getToolMode();
18880
19385
  if (currentMode === "select") {
18881
19386
  const clickedNote = findNoteAtPosition(x2, y);
@@ -18886,7 +19391,7 @@ var createPianoRoll = (options, handlers) => {
18886
19391
  noteId: clickedNote.id,
18887
19392
  mode: "move",
18888
19393
  dragOffsetStep: step - clickedNote.startStep,
18889
- dragOffsetPitch: pitch - clickedNote.pitch,
19394
+ dragOffsetPitch: pitch - clickedNote.pitchUnits,
18890
19395
  startStep: clickedNote.startStep,
18891
19396
  selectedNotes: notesInRect
18892
19397
  // 複数選択ノートを保存
@@ -18904,10 +19409,16 @@ var createPianoRoll = (options, handlers) => {
18904
19409
  }
18905
19410
  const note = findNoteAtPosition(x2, y);
18906
19411
  if (!note) return;
18907
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18908
- 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();
18909
19420
  const logicalX = note.startStep * stepWidth;
18910
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19421
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18911
19422
  const logicalY = yIndex * keyHeight;
18912
19423
  const renderX = logicalX - offset.x;
18913
19424
  const renderY = logicalY - offset.y;
@@ -18926,7 +19437,7 @@ var createPianoRoll = (options, handlers) => {
18926
19437
  noteId: note.id,
18927
19438
  mode: "move",
18928
19439
  dragOffsetStep: step - note.startStep,
18929
- dragOffsetPitch: pitch - note.pitch,
19440
+ dragOffsetPitch: pitch - note.pitchUnits,
18930
19441
  startStep: note.startStep
18931
19442
  };
18932
19443
  });
@@ -18937,41 +19448,47 @@ var createPianoRoll = (options, handlers) => {
18937
19448
  "wheel",
18938
19449
  (e) => {
18939
19450
  e.preventDefault();
18940
- const configValues = getRenderConfig();
19451
+ const configValues = renderer.getRenderConfig();
18941
19452
  const gridHeight = gridCanvas.height;
18942
19453
  const maxOffsetY = Math.max(
18943
19454
  0,
18944
19455
  configValues.keyCount * configValues.keyHeight - gridHeight
18945
19456
  );
18946
- const currentOffset = getDrawOffset();
19457
+ const currentOffset = renderer.getDrawOffset();
18947
19458
  const nextOffsetY = Math.min(
18948
19459
  Math.max(currentOffset.y + e.deltaY, 0),
18949
19460
  maxOffsetY
18950
19461
  );
18951
- setDrawOffset(currentOffset.x, nextOffsetY);
18952
- drawGrid();
18953
- drawNotes(core.getNotes());
19462
+ renderer.setDrawOffset(currentOffset.x, nextOffsetY);
19463
+ renderer.drawGrid();
19464
+ renderer.drawNotes(core.getNotes());
18954
19465
  },
18955
19466
  { passive: false }
18956
19467
  );
18957
19468
  const redraw = () => {
18958
- drawGrid();
18959
- drawNotes(core.getNotes());
19469
+ renderer.drawGrid();
19470
+ renderer.drawNotes(core.getNotes());
18960
19471
  if (core.getToolMode() === "select") {
18961
- drawSelectionRect(selectionRect);
19472
+ renderer.drawSelectionRect(selectionRect);
18962
19473
  if (selectedNotes.length > 0) {
18963
19474
  const selectedIds = new Set(selectedNotes.map((n) => n.id));
18964
- drawSelectedNotes(core.getNotes(), selectedIds);
19475
+ renderer.drawSelectedNotes(core.getNotes(), selectedIds);
18965
19476
  }
18966
19477
  }
18967
19478
  };
18968
19479
  const getNotesInRect = (rect) => {
18969
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18970
- 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();
18971
19488
  const notes = [];
18972
19489
  for (const note of core.getNotes()) {
18973
19490
  const logicalX = note.startStep * stepWidth;
18974
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19491
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18975
19492
  const logicalY = yIndex * keyHeight;
18976
19493
  const noteRect = {
18977
19494
  x: logicalX - offset.x,
@@ -19018,7 +19535,7 @@ var createPianoRoll = (options, handlers) => {
19018
19535
  const minStart = Math.min(...copiedNotes.map((n) => n.startStep));
19019
19536
  copiedNotes.forEach((note) => {
19020
19537
  const newStep = startStep + (note.startStep - minStart);
19021
- core.addNote(newStep, note.pitch, {
19538
+ core.addNote(newStep, note.pitchUnits, {
19022
19539
  noteLengthSteps: note.durationSteps,
19023
19540
  velocity: note.velocity
19024
19541
  });
@@ -19860,10 +20377,12 @@ var createDtmStudio = async (options = {}) => {
19860
20377
  );
19861
20378
  }
19862
20379
  if (!sfInst) return;
20380
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
19863
20381
  sfInst.play({
19864
20382
  ctx: audioCtx,
19865
20383
  destination: getChannelStrip(e.trackId).input,
19866
- pitch: e.pitch,
20384
+ pitch: midi,
20385
+ detuneCents,
19867
20386
  volume: e.volume,
19868
20387
  velocity: e.velocity,
19869
20388
  when: e.when,
@@ -20179,10 +20698,12 @@ var createDtmStudio = async (options = {}) => {
20179
20698
  );
20180
20699
  }
20181
20700
  if (!sfInst) return;
20701
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20182
20702
  sfInst.play({
20183
20703
  ctx: audioCtx,
20184
20704
  destination: getChannelStrip(e.trackId).input,
20185
- pitch: e.pitch,
20705
+ pitch: midi,
20706
+ detuneCents,
20186
20707
  volume: e.volume,
20187
20708
  velocity: e.velocity,
20188
20709
  when: e.when,
@@ -20268,10 +20789,12 @@ var createDtmStudio = async (options = {}) => {
20268
20789
  );
20269
20790
  }
20270
20791
  if (!sfInst) return;
20792
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20271
20793
  sfInst.play({
20272
20794
  ctx: audioCtx,
20273
20795
  destination: getChannelStrip(e.trackId).input,
20274
- pitch: e.pitch,
20796
+ pitch: midi,
20797
+ detuneCents,
20275
20798
  volume: e.volume,
20276
20799
  velocity: e.velocity,
20277
20800
  when: e.when,
@@ -20350,10 +20873,12 @@ var createDtmStudio = async (options = {}) => {
20350
20873
  );
20351
20874
  }
20352
20875
  if (!sfInst) return;
20876
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20353
20877
  sfInst.play({
20354
20878
  ctx: audioCtx,
20355
20879
  destination: getChannelStrip(e.trackId).input,
20356
- pitch: e.pitch,
20880
+ pitch: midi,
20881
+ detuneCents,
20357
20882
  volume: e.volume,
20358
20883
  velocity: e.velocity,
20359
20884
  when: e.when,
@@ -20390,10 +20915,12 @@ var createDtmStudio = async (options = {}) => {
20390
20915
  sfInst = resolveSoundFont(defaultPreset, role, "simple");
20391
20916
  }
20392
20917
  if (!sfInst) return;
20918
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20393
20919
  sfInst.play({
20394
20920
  ctx: audioCtx,
20395
20921
  destination: getChannelStrip(e.trackId).input,
20396
- pitch: e.pitch,
20922
+ pitch: midi,
20923
+ detuneCents,
20397
20924
  volume: e.volume,
20398
20925
  velocity: e.velocity,
20399
20926
  when: e.when,
@@ -20416,10 +20943,12 @@ var createDtmStudio = async (options = {}) => {
20416
20943
  if (audioCtx.state === "suspended") {
20417
20944
  await audioCtx.resume();
20418
20945
  }
20946
+ const { midi, detuneCents } = unitsToMidiDetune(options2.pitchUnits);
20419
20947
  sfInst.play({
20420
20948
  ctx: audioCtx,
20421
20949
  destination: masterGain,
20422
- pitch: options2.pitch,
20950
+ pitch: midi,
20951
+ detuneCents,
20423
20952
  volume: vol / 100,
20424
20953
  when: 0,
20425
20954
  duration: dur
@@ -20439,7 +20968,7 @@ var createDtmStudio = async (options = {}) => {
20439
20968
  // 伴奏トラック
20440
20969
  startStep: p.startStep,
20441
20970
  durationSteps: p.durationSteps,
20442
- pitch: p.pitch,
20971
+ pitchUnits: p.pitchUnits,
20443
20972
  velocity: p.velocity
20444
20973
  }));
20445
20974
  const playerPreset = defaultPreset;
@@ -20447,10 +20976,12 @@ var createDtmStudio = async (options = {}) => {
20447
20976
  const playPlayerNote = (e) => {
20448
20977
  const sfInst = resolveSoundFont(playerPreset, "chord");
20449
20978
  if (!sfInst) return;
20979
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20450
20980
  sfInst.play({
20451
20981
  ctx: audioCtx,
20452
20982
  destination: getChannelStrip("chord").input,
20453
- pitch: e.pitch,
20983
+ pitch: midi,
20984
+ detuneCents,
20454
20985
  volume: e.volume,
20455
20986
  velocity: e.velocity,
20456
20987
  when: e.when,
@@ -20589,8 +21120,12 @@ var createDtmStudio = async (options = {}) => {
20589
21120
  };
20590
21121
  // Annotate the CommonJS export names for ESM import in node:
20591
21122
  0 && (module.exports = {
21123
+ A4_HZ,
21124
+ A4_UNITS,
21125
+ CENTS_PER_UNIT,
20592
21126
  DAW_CSS,
20593
21127
  DEFAULT_BPM,
21128
+ DEFAULT_EDO,
20594
21129
  DEFAULT_GATE,
20595
21130
  DEFAULT_PAN,
20596
21131
  DEFAULT_PLAYBACK_VELOCITY,
@@ -20600,21 +21135,30 @@ var createDtmStudio = async (options = {}) => {
20600
21135
  DRUM_FONT,
20601
21136
  DRUM_KEYS,
20602
21137
  DRUM_PATTERNS,
21138
+ EDO31_NAMES,
20603
21139
  GM_INSTRUMENT_NAMES,
20604
21140
  INSTRUMENT_PRESETS,
21141
+ KEY_COUNT,
20605
21142
  KOE_BASE_URL,
20606
21143
  KOE_VOICEBANKS,
20607
21144
  KOE_VOICEBANK_LABELS,
20608
21145
  KOE_VOICEBANK_TERMS,
20609
21146
  LinkedList,
20610
21147
  MAX_VOCAL_VOLUME,
21148
+ MICRO_STEP,
20611
21149
  MMLCore,
20612
21150
  MML_END_MARKER,
20613
21151
  MidiSearchClient,
21152
+ PITCH_ENCODING_VERSION,
20614
21153
  PITCH_MAP,
21154
+ PITCH_RANGE_END,
21155
+ PITCH_RANGE_START,
20615
21156
  PREWARM_NOTES,
20616
21157
  TRACKS_ADVANCED,
20617
21158
  TRACKS_SIMPLE,
21159
+ UNITS_PER_EDO31_DEGREE,
21160
+ UNITS_PER_OCTAVE,
21161
+ UNITS_PER_SEMITONE,
20618
21162
  VIBRATO_MIN_SEC,
20619
21163
  VOICE_IMAGES,
20620
21164
  VOICE_IMAGE_KEY,
@@ -20624,6 +21168,7 @@ var createDtmStudio = async (options = {}) => {
20624
21168
  buildChordPlacements,
20625
21169
  buildDrumPatternJson,
20626
21170
  buildNameToKeyMapping,
21171
+ chromaticStep,
20627
21172
  collectPitchTokens,
20628
21173
  concatFloat32,
20629
21174
  createAudioContext,
@@ -20632,19 +21177,13 @@ var createDtmStudio = async (options = {}) => {
20632
21177
  createKoeVoice,
20633
21178
  createLyricsConductor,
20634
21179
  createPianoRoll,
21180
+ createRenderer,
20635
21181
  createSequencer,
20636
21182
  createSingingVoices,
20637
21183
  createSynth,
20638
21184
  createVoiceRegistry,
20639
21185
  decodeMml,
20640
21186
  decomposeToMonophonic,
20641
- drawGrid,
20642
- drawHeader,
20643
- drawKeyboard,
20644
- drawNoteLyrics,
20645
- drawNotes,
20646
- drawSelectedNotes,
20647
- drawSelectionRect,
20648
21187
  encodeMml,
20649
21188
  encodeWavPCM16,
20650
21189
  exportMIDI,
@@ -20652,36 +21191,34 @@ var createDtmStudio = async (options = {}) => {
20652
21191
  extractMidiDrumPattern,
20653
21192
  extractMidiPlacements,
20654
21193
  extractMidiPlacementsByTrack,
21194
+ fifthToStep,
21195
+ fifthToUnits,
20655
21196
  formatMmlMeta,
20656
21197
  freqFromPitch,
20657
21198
  generateRandomPattern,
20658
- getDrawOffset,
20659
21199
  getDrumPatternKeys,
20660
- getGridCanvas,
20661
- getGridContext,
20662
- getGridPosition,
20663
- getHeaderCanvas,
20664
21200
  getMidiBPM,
20665
- getRenderConfig,
20666
- getXY,
20667
21201
  icon,
20668
- init,
20669
21202
  injectStyles,
20670
21203
  isChordHeavyTrack,
21204
+ isNaturalLetter,
20671
21205
  isPlausibleMidiTranscription,
20672
21206
  isValidHttpUrl,
21207
+ keyCountFor,
20673
21208
  koeUrl,
21209
+ midiToUnits,
20674
21210
  mountChordPlayer,
20675
21211
  mountDAW,
20676
21212
  mountMmlPlayer,
21213
+ naturalStep,
20677
21214
  normalizeDrumPatterns,
20678
21215
  normalizeLyrics,
20679
- onClick,
20680
21216
  panToStereo,
20681
21217
  parseCustomVocals,
20682
21218
  parseLyrics,
20683
21219
  parseMML,
20684
21220
  parseMmlMeta,
21221
+ pitchV1ToUnits,
20685
21222
  playChords,
20686
21223
  playMML,
20687
21224
  playNote,
@@ -20689,13 +21226,18 @@ var createDtmStudio = async (options = {}) => {
20689
21226
  playSingingMML,
20690
21227
  resolveDrumPattern,
20691
21228
  resolveLoopPoint,
20692
- setBackgroundActive,
20693
- setDrawOffset,
20694
21229
  shiftNotes,
20695
21230
  showLoadingOverlay,
21231
+ spellingToUnits,
20696
21232
  stripCustomVocals,
20697
21233
  stripLyrics,
20698
21234
  stripMmlMeta,
20699
21235
  transposeNotes,
21236
+ unitsPerRow,
21237
+ unitsPerStep,
21238
+ unitsToHz,
21239
+ unitsToMidi,
21240
+ unitsToMidiDetune,
21241
+ unitsToPitchV1,
20700
21242
  vocalVolumeToGain
20701
21243
  });