@onjmin/dtm 1.0.8 → 2.0.3

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,30 +35,41 @@ __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,
65
+ addUnits: () => addUnits,
52
66
  analyzeMidiTracks: () => analyzeMidiTracks,
53
67
  applyHarmonicFilter: () => applyHarmonicFilter,
54
68
  applyMonophonic: () => applyMonophonic,
55
69
  buildChordPlacements: () => buildChordPlacements,
56
70
  buildDrumPatternJson: () => buildDrumPatternJson,
57
71
  buildNameToKeyMapping: () => buildNameToKeyMapping,
72
+ chromaticStep: () => chromaticStep,
58
73
  collectPitchTokens: () => collectPitchTokens,
59
74
  concatFloat32: () => concatFloat32,
60
75
  createAudioContext: () => createAudioContext,
@@ -63,19 +78,13 @@ __export(index_exports, {
63
78
  createKoeVoice: () => createKoeVoice,
64
79
  createLyricsConductor: () => createLyricsConductor,
65
80
  createPianoRoll: () => createPianoRoll,
81
+ createRenderer: () => createRenderer,
66
82
  createSequencer: () => createSequencer,
67
83
  createSingingVoices: () => createSingingVoices,
68
84
  createSynth: () => createSynth,
69
85
  createVoiceRegistry: () => createVoiceRegistry,
70
86
  decodeMml: () => decodeMml,
71
87
  decomposeToMonophonic: () => decomposeToMonophonic,
72
- drawGrid: () => drawGrid,
73
- drawHeader: () => drawHeader,
74
- drawKeyboard: () => drawKeyboard,
75
- drawNoteLyrics: () => drawNoteLyrics,
76
- drawNotes: () => drawNotes,
77
- drawSelectedNotes: () => drawSelectedNotes,
78
- drawSelectionRect: () => drawSelectionRect,
79
88
  encodeMml: () => encodeMml,
80
89
  encodeWavPCM16: () => encodeWavPCM16,
81
90
  exportMIDI: () => exportMIDI,
@@ -83,36 +92,35 @@ __export(index_exports, {
83
92
  extractMidiDrumPattern: () => extractMidiDrumPattern,
84
93
  extractMidiPlacements: () => extractMidiPlacements,
85
94
  extractMidiPlacementsByTrack: () => extractMidiPlacementsByTrack,
95
+ fifthToStep: () => fifthToStep,
96
+ fifthToUnits: () => fifthToUnits,
86
97
  formatMmlMeta: () => formatMmlMeta,
87
98
  freqFromPitch: () => freqFromPitch,
88
99
  generateRandomPattern: () => generateRandomPattern,
89
- getDrawOffset: () => getDrawOffset,
90
100
  getDrumPatternKeys: () => getDrumPatternKeys,
91
- getGridCanvas: () => getGridCanvas,
92
- getGridContext: () => getGridContext,
93
- getGridPosition: () => getGridPosition,
94
- getHeaderCanvas: () => getHeaderCanvas,
95
101
  getMidiBPM: () => getMidiBPM,
96
- getRenderConfig: () => getRenderConfig,
97
- getXY: () => getXY,
98
102
  icon: () => icon,
99
- init: () => init,
100
103
  injectStyles: () => injectStyles,
101
104
  isChordHeavyTrack: () => isChordHeavyTrack,
105
+ isNaturalLetter: () => isNaturalLetter,
102
106
  isPlausibleMidiTranscription: () => isPlausibleMidiTranscription,
103
107
  isValidHttpUrl: () => isValidHttpUrl,
108
+ keyCountFor: () => keyCountFor,
104
109
  koeUrl: () => koeUrl,
110
+ midiNote: () => midiNote,
111
+ midiToUnits: () => midiToUnits,
105
112
  mountChordPlayer: () => mountChordPlayer,
106
113
  mountDAW: () => mountDAW,
107
114
  mountMmlPlayer: () => mountMmlPlayer,
115
+ naturalStep: () => naturalStep,
108
116
  normalizeDrumPatterns: () => normalizeDrumPatterns,
109
117
  normalizeLyrics: () => normalizeLyrics,
110
- onClick: () => onClick,
111
118
  panToStereo: () => panToStereo,
112
119
  parseCustomVocals: () => parseCustomVocals,
113
120
  parseLyrics: () => parseLyrics,
114
121
  parseMML: () => parseMML,
115
122
  parseMmlMeta: () => parseMmlMeta,
123
+ pitchV1ToUnits: () => pitchV1ToUnits,
116
124
  playChords: () => playChords,
117
125
  playMML: () => playMML,
118
126
  playNote: () => playNote,
@@ -120,14 +128,20 @@ __export(index_exports, {
120
128
  playSingingMML: () => playSingingMML,
121
129
  resolveDrumPattern: () => resolveDrumPattern,
122
130
  resolveLoopPoint: () => resolveLoopPoint,
123
- setBackgroundActive: () => setBackgroundActive,
124
- setDrawOffset: () => setDrawOffset,
125
131
  shiftNotes: () => shiftNotes,
126
132
  showLoadingOverlay: () => showLoadingOverlay,
133
+ spellingToUnits: () => spellingToUnits,
127
134
  stripCustomVocals: () => stripCustomVocals,
128
135
  stripLyrics: () => stripLyrics,
129
136
  stripMmlMeta: () => stripMmlMeta,
130
137
  transposeNotes: () => transposeNotes,
138
+ units: () => units,
139
+ unitsPerRow: () => unitsPerRow,
140
+ unitsPerStep: () => unitsPerStep,
141
+ unitsToHz: () => unitsToHz,
142
+ unitsToMidi: () => unitsToMidi,
143
+ unitsToMidiDetune: () => unitsToMidiDetune,
144
+ unitsToPitchV1: () => unitsToPitchV1,
131
145
  vocalVolumeToGain: () => vocalVolumeToGain
132
146
  });
133
147
  module.exports = __toCommonJS(index_exports);
@@ -282,7 +296,7 @@ async function buildNameToKeyMapping() {
282
296
  }
283
297
  var GM_INSTRUMENT_NAMES = FONT_NAME_SURIKOV.trim().split("\n").map((line) => line.slice(line.indexOf(" ") + 1));
284
298
 
285
- // node_modules/.pnpm/@onjmin+chord-parser@1.0.4/node_modules/@onjmin/chord-parser/dist/index.mjs
299
+ // node_modules/.pnpm/@onjmin+chord-parser@1.1.1/node_modules/@onjmin/chord-parser/dist/index.mjs
286
300
  var SHARP_NAMES = [
287
301
  "C",
288
302
  "C#",
@@ -313,6 +327,10 @@ var FLAT_NAMES = [
313
327
  ];
314
328
  var toPitchClass = (n) => (n % 12 + 12) % 12;
315
329
  var noteName = (pc, flat = false) => (flat ? FLAT_NAMES : SHARP_NAMES)[toPitchClass(pc)];
330
+ var LETTER_FIFTH = [0, 2, 4, -1, 1, 3, 5];
331
+ var DEGREE_FIFTH = [0, 2, 4, -1, 1, 3, 5];
332
+ var deg2fifth = (deg) => DEGREE_FIFTH[(deg - 1) % 7];
333
+ var HALF_FIFTH = 7;
316
334
  var SyntaxErrorWithPos = class extends Error {
317
335
  constructor(input, msg) {
318
336
  super(
@@ -359,6 +377,16 @@ var Input = class _Input {
359
377
  };
360
378
  var Output = class {
361
379
  pitch = null;
380
+ /** ルート音の五度圏インデックス。`pitch` と対で持つ。 */
381
+ rootFifth = 0;
382
+ /**
383
+ * 構成音の五度圏オフセット。キーは「ルートからの相対半音 mod 12」。
384
+ *
385
+ * 五度圏インデックスはオクターブ不変なので、分数コードのオクターブシフト
386
+ * (`value` の配列を ±12 する処理)の影響を受けない。そのため構成音の集合とは
387
+ * 別にこの対応表を持っておけば、配列操作側に手を入れずに綴りを保てる。
388
+ */
389
+ relFifths = /* @__PURE__ */ new Map();
362
390
  chord = null;
363
391
  isChord = false;
364
392
  pending = null;
@@ -442,9 +470,17 @@ var parseFormula = (input, output = new Output(), nest = 0) => {
442
470
  case DIVIDE: {
443
471
  const o = parseFormula(input, new Output(), nest);
444
472
  const v = [...output.value];
473
+ const rebase = (semi, fifth) => noteFifth(
474
+ output,
475
+ semi - output.pitch,
476
+ fifth - output.rootFifth
477
+ );
445
478
  if (o.isChord) {
479
+ for (const [rel, f] of o.relFifths)
480
+ rebase(o.pitch + rel, o.rootFifth + f);
446
481
  output.value = [...o.value].concat(v);
447
482
  } else {
483
+ rebase(o.pitch, o.rootFifth);
448
484
  const a = v.sort((x2, y) => x2 - y);
449
485
  const pitch = (o.pitch + 3) % 12 - 3;
450
486
  if (a[0] < pitch) {
@@ -491,63 +527,91 @@ var parsePitch = (input, output) => {
491
527
  const pitch = pitchMatcher.parse(input);
492
528
  if (pitch === null) err(input, "Not found pitch");
493
529
  output.pitch = pitch;
530
+ output.rootFifth = LETTER_FIFTH[idx2pitch.slice(0, 7).indexOf(pitch)];
494
531
  for (let i2 = 0; i2 < 2; i2++) {
495
532
  const half = parseHalf(input, true);
496
533
  if (half === null) break;
497
534
  output.pitch += half;
535
+ output.rootFifth += half * HALF_FIFTH;
498
536
  }
499
537
  return parseBase(input, output);
500
538
  };
539
+ var noteFifth = (out, semi, fifth) => {
540
+ out.relFifths.set((semi % 12 + 12) % 12, fifth);
541
+ };
501
542
  var MAJOR = [0, 4, 7];
502
543
  var DIM = [0, 3, 6];
544
+ var BASE_FIFTHS = /* @__PURE__ */ new Map();
545
+ var MINOR = [0, 3, 7];
546
+ var AUG = [0, 4, 8];
547
+ var HALF_DIM = [0, 3, 6, 10];
548
+ BASE_FIFTHS.set(MAJOR, [0, 4, 1]);
549
+ BASE_FIFTHS.set(MINOR, [0, -3, 1]);
550
+ BASE_FIFTHS.set(DIM, [0, -3, -6]);
551
+ BASE_FIFTHS.set(AUG, [0, 4, 8]);
552
+ BASE_FIFTHS.set(HALF_DIM, [0, -3, -6, -2]);
503
553
  var baseMatcher = new Matcher();
504
- baseMatcher.set(["m", "min", "Min", "minor", "Minor", "-"], [0, 3, 7]);
554
+ baseMatcher.set(["m", "min", "Min", "minor", "Minor", "-"], MINOR);
505
555
  baseMatcher.set(["dim", "\u3007"], DIM);
506
- baseMatcher.set("+", [0, 4, 8]);
507
- baseMatcher.set(["\u03A6", "\u03C6", "\xF8"], [0, 3, 6, 10]);
556
+ baseMatcher.set("+", AUG);
557
+ baseMatcher.set(["\u03A6", "\u03C6", "\xF8"], HALF_DIM);
508
558
  var parseBase = (input, output) => {
509
559
  const isMajMarker = /^maj/i.test(input.str.slice(input.idx));
510
560
  const res = isMajMarker ? null : baseMatcher.parse(input);
511
561
  if (res !== null) output.isChord = true;
512
- output.chord = new Set(res || MAJOR);
562
+ const base = res || MAJOR;
563
+ output.chord = new Set(base);
564
+ const baseFifths = BASE_FIFTHS.get(base);
565
+ if (baseFifths)
566
+ for (const [i2, semi] of base.entries())
567
+ noteFifth(output, semi, baseFifths[i2]);
513
568
  if (res === DIM) {
514
569
  const { num } = input;
515
570
  const chord = output.chord;
516
- if (num !== null) chord.add(deg2pitch(num) - 2);
571
+ if (num !== null) {
572
+ chord.add(deg2pitch(num) - 2);
573
+ noteFifth(output, deg2pitch(num) - 2, deg2fifth(num) - 2 * HALF_FIFTH);
574
+ }
517
575
  }
518
576
  output.nest = input.nest;
519
577
  return parseTerm(input, output);
520
578
  };
521
- var add = (chord, n, half) => {
522
- chord.add(deg2pitch(n) + half);
579
+ var put = (chord, out, deg, half = 0) => {
580
+ chord.add(deg2pitch(deg) + half);
581
+ noteFifth(out, deg2pitch(deg) + half, deg2fifth(deg) + half * HALF_FIFTH);
582
+ };
583
+ var add = (chord, n, half, out) => {
584
+ put(chord, out, n, half);
523
585
  };
524
- var aug = (chord) => {
586
+ var aug = (chord, out) => {
525
587
  chord.delete(deg2pitch(5));
526
- chord.add(deg2pitch(5) + 1);
588
+ put(chord, out, 5, 1);
527
589
  };
528
- var _7th = (chord, n, _half2, isFlat = false) => {
590
+ var _7th = (chord, n, _half2, out, isFlat = false) => {
529
591
  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));
592
+ else if (n === 6) put(chord, out, 6);
593
+ else if (n === 69) {
594
+ put(chord, out, 6);
595
+ put(chord, out, 9);
596
+ } else {
597
+ if (n >= 7) put(chord, out, 7, isFlat ? -1 : 0);
598
+ if (n >= 9) put(chord, out, 9);
599
+ if (n >= 11) put(chord, out, 11);
600
+ if (n >= 13) put(chord, out, 13);
537
601
  }
538
602
  };
539
- var _half = (chord, n, half) => {
603
+ var _half = (chord, n, half, out) => {
540
604
  chord.delete(deg2pitch(n));
541
- chord.add(deg2pitch(n) + half);
605
+ put(chord, out, n, half);
542
606
  };
543
607
  var funcMatcher = new Matcher();
544
608
  funcMatcher.set("add", add);
545
609
  funcMatcher.set(["omit", "no"], (chord, n, half) => {
546
610
  chord.delete(deg2pitch(n) + half);
547
611
  });
548
- funcMatcher.set("sus", (chord, n, half) => {
612
+ funcMatcher.set("sus", (chord, n, half, out) => {
549
613
  chord.delete(deg2pitch(3));
550
- chord.add(deg2pitch(n) + half);
614
+ put(chord, out, n, half);
551
615
  });
552
616
  funcMatcher.set(
553
617
  ["M", "maj", "Maj", "major", "Major", "\u25B3", "\u0394"],
@@ -563,17 +627,18 @@ var parseFunc = (input, output) => {
563
627
  const half = parseHalf(input);
564
628
  const { num } = input;
565
629
  if (num === null) {
566
- if (isAug) aug(chord);
630
+ if (isAug) aug(chord, output);
567
631
  else err(input, "Not found number");
568
632
  }
569
633
  if (half === null) {
570
- if (input.nest === output.nest) _7th(chord, num, 0, true);
571
- else add(chord, num, 0);
634
+ if (input.nest === output.nest)
635
+ _7th(chord, num, 0, output, true);
636
+ else add(chord, num, 0, output);
572
637
  } else {
573
- _half(chord, num, half);
638
+ _half(chord, num, half, output);
574
639
  }
575
640
  } else if (func === aug) {
576
- aug(chord);
641
+ aug(chord, output);
577
642
  } else {
578
643
  output.pending = func;
579
644
  }
@@ -587,7 +652,8 @@ var parsePending = (input, output) => {
587
652
  pending(
588
653
  chord,
589
654
  num,
590
- half === null ? 0 : half
655
+ half === null ? 0 : half,
656
+ output
591
657
  );
592
658
  output.pending = null;
593
659
  return parseTerm(input, output);
@@ -599,12 +665,29 @@ var parseChord = (symbol) => {
599
665
  const pitchClasses = [...new Set(notes.map(toPitchClass))].sort(
600
666
  (a, b) => a - b
601
667
  );
668
+ const rootPitch = output.pitch;
669
+ const rootFifth = output.rootFifth;
670
+ const noteFifths = notes.map((n) => {
671
+ const rel = ((n - rootPitch) % 12 + 12) % 12;
672
+ const f = output.relFifths.get(rel);
673
+ if (f !== void 0) return rootFifth + f;
674
+ let best = 0;
675
+ for (let k = -6; k <= 6; k++) {
676
+ if ((k * 7 % 12 + 12) % 12 === rel) {
677
+ best = k;
678
+ break;
679
+ }
680
+ }
681
+ return rootFifth + best;
682
+ });
602
683
  return {
603
684
  symbol,
604
- root: toPitchClass(output.pitch),
685
+ root: toPitchClass(rootPitch),
605
686
  notes,
606
687
  pitchClasses,
607
- intervals
688
+ intervals,
689
+ rootFifth,
690
+ noteFifths
608
691
  };
609
692
  };
610
693
  var QUALITY_SOURCE = [
@@ -1633,10 +1716,77 @@ var createChannelStrip = (ctx, destination, options = {}) => {
1633
1716
  };
1634
1717
  };
1635
1718
 
1719
+ // src/tuning.ts
1720
+ var units = (n) => n;
1721
+ var midiNote = (n) => n;
1722
+ var addUnits = (a, delta) => a + delta;
1723
+ var UNITS_PER_OCTAVE = 372;
1724
+ var UNITS_PER_SEMITONE = 372 / 12;
1725
+ var UNITS_PER_EDO31_DEGREE = 372 / 31;
1726
+ var CENTS_PER_UNIT = 1200 / UNITS_PER_OCTAVE;
1727
+ var A4_UNITS = 69 * UNITS_PER_SEMITONE;
1728
+ var A4_HZ = 440;
1729
+ var DEFAULT_EDO = 12;
1730
+ var unitsPerStep = (edo) => UNITS_PER_OCTAVE / edo;
1731
+ var midiToUnits = (midi) => midi * UNITS_PER_SEMITONE;
1732
+ var unitsToMidi = (u) => u / UNITS_PER_SEMITONE;
1733
+ var unitsToHz = (u) => A4_HZ * 2 ** ((u - A4_UNITS) / UNITS_PER_OCTAVE);
1734
+ var unitsToMidiDetune = (u) => {
1735
+ const midi = Math.round(u / UNITS_PER_SEMITONE);
1736
+ return {
1737
+ midi,
1738
+ detuneCents: (u - midi * UNITS_PER_SEMITONE) * CENTS_PER_UNIT
1739
+ };
1740
+ };
1741
+ var NATURAL_STEPS = {
1742
+ 12: { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 },
1743
+ 31: { c: 0, d: 5, e: 10, f: 13, g: 18, a: 23, b: 28 }
1744
+ };
1745
+ var chromaticStep = (edo) => edo === 31 ? 2 : 1;
1746
+ var MICRO_STEP = 1;
1747
+ var isNaturalLetter = (ch) => Object.hasOwn(NATURAL_STEPS[12], ch);
1748
+ var naturalStep = (ch, edo) => NATURAL_STEPS[edo][ch] ?? null;
1749
+ var spellingToUnits = (letter, octave, chromatic, micro, edo) => {
1750
+ const step = naturalStep(letter, edo);
1751
+ if (step === null) return null;
1752
+ const perStep = unitsPerStep(edo);
1753
+ const stepsFromC = step + chromatic * chromaticStep(edo) + micro * MICRO_STEP;
1754
+ return (octave + 1) * UNITS_PER_OCTAVE + stepsFromC * perStep;
1755
+ };
1756
+ var FIFTH_STEPS = { 12: 7, 31: 18 };
1757
+ var fifthToStep = (fifthIndex, edo) => {
1758
+ const n = fifthIndex * FIFTH_STEPS[edo];
1759
+ return (n % edo + edo) % edo;
1760
+ };
1761
+ var fifthToUnits = (fifthIndex, edo) => fifthToStep(fifthIndex, edo) * unitsPerStep(edo);
1762
+ var PITCH_ENCODING_VERSION = 2;
1763
+ var pitchV1ToUnits = (pitch) => pitch * UNITS_PER_SEMITONE;
1764
+ var unitsToPitchV1 = (u) => Math.round(u / UNITS_PER_SEMITONE);
1765
+
1636
1766
  // src/chords.ts
1637
1767
  var C3 = 48;
1768
+ var MEANTONE_STEP_BY_SEMITONE = [0, 3, 5, 8, 10, 13, 16, 18, 21, 23, 26, 28];
1769
+ var chordToneToUnits = (relSemitone, relFifth, rootSemitone, rootFifth, edo) => {
1770
+ const perStep = UNITS_PER_OCTAVE / edo;
1771
+ const rootOct = Math.floor(rootSemitone / 12);
1772
+ const rootWithin = (rootSemitone % 12 + 12) % 12;
1773
+ const rootStep = edo === 31 ? (fifthToStep(rootFifth, 31) + MEANTONE_STEP_BY_SEMITONE[rootWithin] - fifthToStep(rootFifth, 31) + 31) % 31 : rootWithin;
1774
+ const relWithin12 = (relSemitone % 12 + 12) % 12;
1775
+ const relOct = Math.round((relSemitone - relWithin12) / 12);
1776
+ const relStep = (fifthToStep(relFifth, edo) - fifthToStep(rootFifth, edo) + edo) % edo;
1777
+ return units(
1778
+ (rootOct + relOct) * UNITS_PER_OCTAVE + (rootStep + relStep) * perStep
1779
+ );
1780
+ };
1638
1781
  var buildChordPlacements = (options) => {
1639
- const { chordStr, patternType, rootShift, bpm, stepsPerBar } = options;
1782
+ const {
1783
+ chordStr,
1784
+ patternType,
1785
+ rootShift,
1786
+ bpm,
1787
+ stepsPerBar,
1788
+ edo = 12
1789
+ } = options;
1640
1790
  const placements = [];
1641
1791
  if (!chordStr.trim()) return placements;
1642
1792
  const offset = rootShift;
@@ -1665,8 +1815,20 @@ var buildChordPlacements = (options) => {
1665
1815
  for (const group of Object.values(chordGroups)) {
1666
1816
  for (const chord of group) {
1667
1817
  let notes;
1818
+ let toUnits;
1668
1819
  try {
1669
- notes = [...parseChord(`${chord.key}${chord.chord}`).notes];
1820
+ const parsed = parseChord(`${chord.key}${chord.chord}`);
1821
+ notes = [...parsed.notes];
1822
+ const fifthOf = new Map(
1823
+ parsed.notes.map((n, i2) => [n, parsed.noteFifths[i2]])
1824
+ );
1825
+ toUnits = (rel) => chordToneToUnits(
1826
+ rel,
1827
+ fifthOf.get(rel) ?? parsed.rootFifth,
1828
+ C3 + offset,
1829
+ parsed.rootFifth,
1830
+ edo === 31 ? 31 : 12
1831
+ );
1670
1832
  } catch {
1671
1833
  continue;
1672
1834
  }
@@ -1675,7 +1837,7 @@ var buildChordPlacements = (options) => {
1675
1837
  for (const noteOffset of notes) {
1676
1838
  placements.push({
1677
1839
  startStep: chord.whenStep,
1678
- pitch: C3 + noteOffset + offset,
1840
+ pitchUnits: toUnits(noteOffset),
1679
1841
  durationSteps: noteLength,
1680
1842
  velocity: 100
1681
1843
  });
@@ -1685,7 +1847,7 @@ var buildChordPlacements = (options) => {
1685
1847
  notes.forEach((noteOffset, i2) => {
1686
1848
  placements.push({
1687
1849
  startStep: chord.whenStep + i2 * arpInterval,
1688
- pitch: C3 + noteOffset + offset,
1850
+ pitchUnits: toUnits(noteOffset),
1689
1851
  durationSteps: noteLength - i2 * arpInterval,
1690
1852
  velocity: 100
1691
1853
  });
@@ -1695,7 +1857,7 @@ var buildChordPlacements = (options) => {
1695
1857
  notes.forEach((noteOffset, i2) => {
1696
1858
  placements.push({
1697
1859
  startStep: chord.whenStep + i2 * arpInterval,
1698
- pitch: C3 + noteOffset + offset,
1860
+ pitchUnits: toUnits(noteOffset),
1699
1861
  durationSteps: Math.max(12, noteLength - i2 * arpInterval),
1700
1862
  velocity: 100
1701
1863
  });
@@ -1709,7 +1871,7 @@ var buildChordPlacements = (options) => {
1709
1871
  for (const noteOffset of notes) {
1710
1872
  placements.push({
1711
1873
  startStep: syncopatedStep,
1712
- pitch: C3 + noteOffset + offset,
1874
+ pitchUnits: toUnits(noteOffset),
1713
1875
  durationSteps: Math.min(halfBeat, 12),
1714
1876
  velocity: 100
1715
1877
  });
@@ -1728,7 +1890,7 @@ var buildChordPlacements = (options) => {
1728
1890
  for (const noteOffset of notes) {
1729
1891
  placements.push({
1730
1892
  startStep: noteStart,
1731
- pitch: C3 + noteOffset + offset,
1893
+ pitchUnits: toUnits(noteOffset),
1732
1894
  durationSteps: yatsumeLengthSteps,
1733
1895
  velocity: 100
1734
1896
  });
@@ -1740,7 +1902,7 @@ var buildChordPlacements = (options) => {
1740
1902
  const stepOffset = i2 * Math.floor(stepsPerBar / 4);
1741
1903
  placements.push({
1742
1904
  startStep: chord.whenStep + stepOffset,
1743
- pitch: C3 + noteOffset + offset,
1905
+ pitchUnits: toUnits(noteOffset),
1744
1906
  durationSteps: Math.max(12, Math.floor(stepsPerBar / 4)),
1745
1907
  velocity: 100
1746
1908
  });
@@ -1752,8 +1914,20 @@ var buildChordPlacements = (options) => {
1752
1914
  const chordNames = chordStr.split(/[\s,]+/).filter((c) => c);
1753
1915
  chordNames.forEach((chordName, barIndex) => {
1754
1916
  let notes;
1917
+ let toUnits;
1755
1918
  try {
1756
- notes = [...parseChord(chordName).notes];
1919
+ const parsed = parseChord(chordName);
1920
+ notes = [...parsed.notes];
1921
+ const fifthOf = new Map(
1922
+ parsed.notes.map((n, i2) => [n, parsed.noteFifths[i2]])
1923
+ );
1924
+ toUnits = (rel) => chordToneToUnits(
1925
+ rel,
1926
+ fifthOf.get(rel) ?? parsed.rootFifth,
1927
+ C3 + offset,
1928
+ parsed.rootFifth,
1929
+ edo === 31 ? 31 : 12
1930
+ );
1757
1931
  } catch {
1758
1932
  return;
1759
1933
  }
@@ -1763,7 +1937,7 @@ var buildChordPlacements = (options) => {
1763
1937
  const stepOffset = i2 * 3;
1764
1938
  placements.push({
1765
1939
  startStep: startStep + stepOffset,
1766
- pitch: C3 + noteOffset + offset,
1940
+ pitchUnits: toUnits(noteOffset),
1767
1941
  durationSteps: chordLength - stepOffset,
1768
1942
  velocity: 100
1769
1943
  });
@@ -2178,7 +2352,7 @@ var Worldline = class _Worldline {
2178
2352
  const WL = this.wasm;
2179
2353
  const FS = WORLDLINE_SAMPLE_RATE;
2180
2354
  const basePitch = sampleCurve(preMs + durationMs / 2, pitch, preMs + durationMs);
2181
- const midiNote = Math.round(69 + 12 * Math.log2(basePitch / 440));
2355
+ const midiNote2 = Math.round(69 + 12 * Math.log2(basePitch / 440));
2182
2356
  const posMs = 0;
2183
2357
  const reqLen = preMs + durationMs;
2184
2358
  const cutMs = WL_FRAME_MS * 2;
@@ -2202,7 +2376,7 @@ var Worldline = class _Worldline {
2202
2376
  sv(8, samplePtr, "*");
2203
2377
  sv(12, 0, "i32");
2204
2378
  sv(16, 0, "*");
2205
- sv(20, midiNote, "i32");
2379
+ sv(20, midiNote2, "i32");
2206
2380
  sv(24, 100, "double");
2207
2381
  sv(32, 0, "double");
2208
2382
  sv(40, reqLen, "double");
@@ -2295,6 +2469,11 @@ var DEFAULT_VELOCITY = 100;
2295
2469
  var DEFAULT_PLAYBACK_VELOCITY = 127;
2296
2470
  var DEFAULT_STEPS_PER_BAR = 192;
2297
2471
  var MML_END_MARKER = "#end;";
2472
+ var PITCH_RANGE_START = 0;
2473
+ var PITCH_RANGE_END = 3937;
2474
+ var unitsPerRow = (edo) => 372 / edo;
2475
+ var keyCountFor = (edo) => Math.floor((PITCH_RANGE_END - PITCH_RANGE_START) / unitsPerRow(edo)) + 1;
2476
+ var KEY_COUNT = keyCountFor(12);
2298
2477
 
2299
2478
  // src/vibrato.ts
2300
2479
  var VIBRATO_MIN_SEC = 0.35;
@@ -2694,7 +2873,8 @@ var FORMANTS = {
2694
2873
  // 撥音(ん)は鼻音寄りの低フォルマント
2695
2874
  N: [250, 1e3]
2696
2875
  };
2697
- var midiToFreq = (m) => 440 * 2 ** ((m - 69) / 12);
2876
+ var unitsToFreq = (units2) => 440 * 2 ** ((units2 - 2139) / 372);
2877
+ var unitsToMidiFloat = (units2) => units2 / 31;
2698
2878
  var createKlattVoice = (ctx, destination, reverbBus, delayBus) => {
2699
2879
  const active = /* @__PURE__ */ new Set();
2700
2880
  const voice = (syllable, e) => {
@@ -2728,7 +2908,7 @@ var createKlattVoice = (ctx, destination, reverbBus, delayBus) => {
2728
2908
  }
2729
2909
  const osc = ctx.createOscillator();
2730
2910
  osc.type = "sawtooth";
2731
- osc.frequency.value = midiToFreq(e.pitch);
2911
+ osc.frequency.value = unitsToFreq(e.pitchUnits);
2732
2912
  const makeFormant = (freq, q2, gainScale) => {
2733
2913
  const filter = ctx.createBiquadFilter();
2734
2914
  filter.type = "bandpass";
@@ -2980,7 +3160,7 @@ var createLocalBackend = async (options) => {
2980
3160
  if (!consonantPcm || !vowelPcm) return null;
2981
3161
  const spliced = spliceCompositePcm(consonantPcm, vowelPcm, KOE_SAMPLE_RATE);
2982
3162
  if (!spliced) return null;
2983
- const targetHz = midiToFreq(pitch);
3163
+ const targetHz = unitsToFreq(pitch);
2984
3164
  const audio = worldline.renderNote({
2985
3165
  pcm: spliced.pcm,
2986
3166
  pitch: vibrato ? vibratoPitchCurve(targetHz, spliced.preMs) : targetHz,
@@ -3009,7 +3189,7 @@ var createLocalBackend = async (options) => {
3009
3189
  if (!pcm || pcm.length === 0) return null;
3010
3190
  const entry = bank.manifest.phonemes[alias];
3011
3191
  const lead = leadInFromEntry(entry);
3012
- const targetHz = midiToFreq(pitch);
3192
+ const targetHz = unitsToFreq(pitch);
3013
3193
  if (worldline) {
3014
3194
  const audio = worldline.renderNote({
3015
3195
  pcm,
@@ -3216,7 +3396,7 @@ var createKoeVoice = async (ctx, destination, options) => {
3216
3396
  backend.pitchTokens,
3217
3397
  syllable,
3218
3398
  prevVowel,
3219
- e.pitch
3399
+ unitsToMidiFloat(e.pitchUnits)
3220
3400
  );
3221
3401
  if (syllable.vowel && syllable.vowel !== "N") prevVowel = syllable.vowel;
3222
3402
  if (!alias) return;
@@ -3224,7 +3404,7 @@ var createKoeVoice = async (ctx, destination, options) => {
3224
3404
  const peak = Math.max(1e-4, e.volume);
3225
3405
  const pan = e.pan ?? 0;
3226
3406
  const durationMs = Math.max(60, e.duration * 1e3);
3227
- void renderInto(alias, e.pitch, durationMs).then((r) => {
3407
+ void renderInto(alias, e.pitchUnits, durationMs).then((r) => {
3228
3408
  if (r)
3229
3409
  schedule(r, t0, peak, pan, e.reverbSend, e.delaySend, e.destination);
3230
3410
  });
@@ -3236,7 +3416,10 @@ var createKoeVoice = async (ctx, destination, options) => {
3236
3416
  backend.pitchTokens,
3237
3417
  syllable,
3238
3418
  prevVowelArg,
3239
- pitch
3419
+ // 多音階バンクのピッチトークン("_G4" 等)は録音の音名なので、
3420
+ // 最寄り選択はMIDIノート番号の尺度で行う。units のまま渡すと常に
3421
+ // 最高音のトークンが選ばれてしまう。
3422
+ unitsToMidiFloat(pitch)
3240
3423
  );
3241
3424
  if (!alias) return null;
3242
3425
  const dMs = Math.max(60, durationMs);
@@ -3268,13 +3451,14 @@ var STREAM_LOOKAHEAD_SEC = 1.5;
3268
3451
  var STREAM_POLL_MS = 100;
3269
3452
  var OCTAVE_UNISON_PEAK_SCALE = 0.6;
3270
3453
  var octaveUnisonOffsets = (mode) => {
3454
+ const OCT = 372;
3271
3455
  switch (mode) {
3272
3456
  case "down":
3273
- return [-12];
3457
+ return [-OCT];
3274
3458
  case "up":
3275
- return [12];
3459
+ return [OCT];
3276
3460
  case "both":
3277
- return [-12, 12];
3461
+ return [-OCT, OCT];
3278
3462
  default:
3279
3463
  return [];
3280
3464
  }
@@ -3484,7 +3668,7 @@ var createSingingVoices = (ctx, destination, options = {}) => {
3484
3668
  const effPeak = (dest ? peak * masterVolumeScalar : peak) * peakScale;
3485
3669
  model(note.syllable, {
3486
3670
  trackId: track.id ?? "",
3487
- pitch,
3671
+ pitchUnits: pitch,
3488
3672
  velocity: 100,
3489
3673
  volume: effPeak,
3490
3674
  when,
@@ -3499,7 +3683,7 @@ var createSingingVoices = (ctx, destination, options = {}) => {
3499
3683
  };
3500
3684
  dispatchNote(note.pitch, 1);
3501
3685
  for (const offset of octaveUnisonOffsets(track.octaveUnison)) {
3502
- dispatchNote(note.pitch + offset, OCTAVE_UNISON_PEAK_SCALE);
3686
+ dispatchNote(units(note.pitch + offset), OCTAVE_UNISON_PEAK_SCALE);
3503
3687
  }
3504
3688
  if (!(model.renderToCache && model.scheduleCached)) {
3505
3689
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -3550,17 +3734,13 @@ var createVoiceRegistry = (models = {}, fallback = "klatt") => {
3550
3734
  };
3551
3735
 
3552
3736
  // 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
3737
+ var NATURAL_STEPS2 = {
3738
+ 12: { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 },
3739
+ 31: { c: 0, d: 5, e: 10, f: 13, g: 18, a: 23, b: 28 }
3561
3740
  };
3741
+ var isNatural = (ch) => Object.hasOwn(NATURAL_STEPS2[12], ch);
3562
3742
  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;
3743
+ var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode|edo)=([\w-]+)/gi;
3564
3744
  var TRACK_INST_DIRECTIVE = /#t(\d+)inst=([^#;\r\n]+)/gi;
3565
3745
  var TRACK_COMP_DIRECTIVE = /#t(\d+)comp=(\d+)/gi;
3566
3746
  var TRACK_WIDTH_DIRECTIVE = /#t(\d+)width=(\d+)/gi;
@@ -3610,6 +3790,9 @@ var parseMmlMeta = (mml) => {
3610
3790
  if (m[2] === "simple" || m[2] === "advanced") {
3611
3791
  meta.mode = m[2];
3612
3792
  }
3793
+ } else if (key === "edo") {
3794
+ const e = Number.parseInt(m[2], 10);
3795
+ if (e === 12 || e === 31) meta.edo = e;
3613
3796
  }
3614
3797
  }
3615
3798
  for (const m of mml.matchAll(TRACK_INST_DIRECTIVE)) {
@@ -3712,6 +3895,7 @@ var formatMmlMeta = (meta, space = "") => {
3712
3895
  if (meta.fadeOut !== void 0 && meta.fadeOut !== 0)
3713
3896
  parts.push(`#fadeout=${meta.fadeOut}`);
3714
3897
  if (meta.mode) parts.push(`#mode=${meta.mode}`);
3898
+ if (meta.edo !== void 0 && meta.edo !== 12) parts.push(`#edo=${meta.edo}`);
3715
3899
  if (meta.trackInstruments) {
3716
3900
  for (const [idx, name] of Object.entries(meta.trackInstruments)) {
3717
3901
  if (name) parts.push(`#t${idx}inst=${name}`);
@@ -3786,6 +3970,10 @@ var parseMML = (mml, options = {}) => {
3786
3970
  const endMarkerBase = MML_END_MARKER.replace(/;+$/, "");
3787
3971
  const endRegex = new RegExp(`(?<![cdafgCDAFG])${endMarkerBase}\\b;?`, "gi");
3788
3972
  const fullMML = stripLyrics(noMeta).replace(endRegex, "").replace(/[\n\r]+/g, " ").trim();
3973
+ const edo = meta.edo === 31 ? 31 : 12;
3974
+ const naturals = NATURAL_STEPS2[edo];
3975
+ const unitsPerStep2 = 372 / edo;
3976
+ const chromaticStep2 = edo === 31 ? 2 : 1;
3789
3977
  const parts = fullMML.split(/(@\d+)/).filter((p) => p.trim().length > 0);
3790
3978
  let trackIndex = 0;
3791
3979
  let sourceTrackIndex = 0;
@@ -3833,6 +4021,19 @@ var parseMML = (mml, options = {}) => {
3833
4021
  type
3834
4022
  });
3835
4023
  };
4024
+ const readAccidentals = () => {
4025
+ let delta = 0;
4026
+ while (j < body.length) {
4027
+ const a = body[j];
4028
+ if (a === "#") delta += chromaticStep2;
4029
+ else if (a === "-") delta -= chromaticStep2;
4030
+ else if (a === "+") delta += edo === 31 ? 1 : chromaticStep2;
4031
+ else if (a === "_") delta -= edo === 31 ? 1 : chromaticStep2;
4032
+ else break;
4033
+ j++;
4034
+ }
4035
+ return delta;
4036
+ };
3836
4037
  const parseLength = () => {
3837
4038
  let numStr = "";
3838
4039
  while (j < body.length && /\d/.test(body[j])) {
@@ -3904,17 +4105,13 @@ var parseMML = (mml, options = {}) => {
3904
4105
  const savedOctave = octave;
3905
4106
  while (j < body.length && body[j] !== "]") {
3906
4107
  const c = body[j];
3907
- if (Object.hasOwn(PITCH_MAP, c)) {
3908
- let pitch = PITCH_MAP[c];
4108
+ if (isNatural(c)) {
4109
+ let pitchSteps = naturals[c];
3909
4110
  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);
4111
+ pitchSteps += readAccidentals();
4112
+ chordNotes.push(
4113
+ units((octave + 1) * 372 + pitchSteps * unitsPerStep2)
4114
+ );
3918
4115
  } else if (c === ">") {
3919
4116
  octave = Math.min(8, octave + 1);
3920
4117
  j++;
@@ -3940,7 +4137,7 @@ var parseMML = (mml, options = {}) => {
3940
4137
  placements.push({
3941
4138
  trackIndex,
3942
4139
  startStep: currentStep,
3943
- pitch: p,
4140
+ pitchUnits: p,
3944
4141
  durationSteps: Math.max(1, steps),
3945
4142
  velocity
3946
4143
  });
@@ -3948,23 +4145,17 @@ var parseMML = (mml, options = {}) => {
3948
4145
  pushTok("chord", currentStep, Math.max(1, steps), tokStart);
3949
4146
  currentStep += steps;
3950
4147
  octave = savedOctave;
3951
- } else if (Object.hasOwn(PITCH_MAP, ch)) {
3952
- let pitch = PITCH_MAP[ch];
4148
+ } else if (isNatural(ch)) {
4149
+ let pitchSteps = naturals[ch];
3953
4150
  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;
4151
+ pitchSteps += readAccidentals();
4152
+ const midiPitch = units((octave + 1) * 372 + pitchSteps * unitsPerStep2);
3962
4153
  const steps = parseLength();
3963
4154
  recordContributor();
3964
4155
  placements.push({
3965
4156
  trackIndex,
3966
4157
  startStep: currentStep,
3967
- pitch: midiPitch,
4158
+ pitchUnits: midiPitch,
3968
4159
  durationSteps: Math.max(1, steps),
3969
4160
  velocity
3970
4161
  });
@@ -4096,7 +4287,7 @@ var createSequencer = (options) => {
4096
4287
  maxEndStep = Math.max(maxEndStep, note.startStep + note.durationSteps);
4097
4288
  timeline.push({
4098
4289
  trackId: track.id,
4099
- pitch: note.pitch,
4290
+ pitch: note.pitchUnits,
4100
4291
  volume: track.volume / 100,
4101
4292
  velocity: note.velocity ?? DEFAULT_PLAYBACK_VELOCITY,
4102
4293
  when,
@@ -4159,7 +4350,7 @@ var createSequencer = (options) => {
4159
4350
  const currentVolume = (trackVolumeMap.get(ev.trackId) ?? ev.volume * 100) / 100;
4160
4351
  options.onPlayNote({
4161
4352
  trackId: ev.trackId,
4162
- pitch: ev.pitch,
4353
+ pitchUnits: ev.pitch,
4163
4354
  velocity: ev.velocity,
4164
4355
  volume: currentVolume * velocityVolume,
4165
4356
  when: Math.max(0, _when),
@@ -6479,7 +6670,7 @@ var SONG_DRUM_PATTERNS = {
6479
6670
  };
6480
6671
 
6481
6672
  // src/synth.ts
6482
- var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
6673
+ var freqFromPitch = (pitchUnits) => unitsToHz(pitchUnits);
6483
6674
  var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
6484
6675
  const wave = tone.wave ?? "square";
6485
6676
  const attack = tone.attack ?? 0;
@@ -6495,7 +6686,7 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
6495
6686
  const osc = ctx.createOscillator();
6496
6687
  const gain = ctx.createGain();
6497
6688
  osc.type = wave;
6498
- osc.frequency.value = freqFromPitch(e.pitch);
6689
+ osc.frequency.value = freqFromPitch(e.pitchUnits);
6499
6690
  const t0 = ctx.currentTime + e.when;
6500
6691
  const peak = Math.max(1e-4, 0.06 * e.volume * 1.5 * gainScale);
6501
6692
  if (tone.decay) {
@@ -6600,7 +6791,7 @@ var playPlacements = (placements, options) => {
6600
6791
  id: id++,
6601
6792
  startStep: p.startStep,
6602
6793
  durationSteps: p.durationSteps,
6603
- pitch: p.pitch,
6794
+ pitchUnits: p.pitchUnits,
6604
6795
  velocity: p.velocity
6605
6796
  }));
6606
6797
  return {
@@ -6771,7 +6962,7 @@ var playNote = (options) => {
6771
6962
  const dur = options.duration ?? 1;
6772
6963
  synth.playNote({
6773
6964
  trackId: "melody",
6774
- pitch: options.pitch,
6965
+ pitchUnits: options.pitchUnits,
6775
6966
  velocity: 100,
6776
6967
  volume: vol / 100,
6777
6968
  when: 0,
@@ -6792,7 +6983,7 @@ var playChords = (chordStr, options = {}) => {
6792
6983
  // 伴奏トラック
6793
6984
  startStep: p.startStep,
6794
6985
  durationSteps: p.durationSteps,
6795
- pitch: p.pitch,
6986
+ pitchUnits: p.pitchUnits,
6796
6987
  velocity: p.velocity
6797
6988
  }));
6798
6989
  return playPlacements(placements, {
@@ -8949,7 +9140,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
8949
9140
  0
8950
9141
  );
8951
9142
  const timedNotes = placements.map((p) => ({
8952
- pitch: p.pitch,
9143
+ pitch: unitsToPitchV1(p.pitchUnits),
8953
9144
  when: p.startStep * secondsPerStep,
8954
9145
  duration: p.durationSteps * secondsPerStep
8955
9146
  }));
@@ -8982,7 +9173,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
8982
9173
  id: id++,
8983
9174
  startStep: p.startStep,
8984
9175
  durationSteps: p.durationSteps,
8985
- pitch: p.pitch,
9176
+ pitchUnits: p.pitchUnits,
8986
9177
  velocity: DEFAULT_VELOCITY
8987
9178
  }));
8988
9179
  return {
@@ -9807,7 +9998,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9807
9998
  (a, b) => a.startStep - b.startStep
9808
9999
  );
9809
10000
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
9810
- const semis = (lt.octave ?? 0) * 12;
10001
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
9811
10002
  const count = Math.min(sorted.length, lt.syllables.length);
9812
10003
  const notes = [];
9813
10004
  for (let i2 = 0; i2 < count; i2++) {
@@ -9815,7 +10006,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9815
10006
  if (n.startStep < fromStep) continue;
9816
10007
  notes.push({
9817
10008
  syllable: lt.syllables[i2],
9818
- pitch: n.pitch + semis,
10009
+ pitch: units(n.pitchUnits + semis),
9819
10010
  startSec: (n.startStep - fromStep) * secondsPerStep,
9820
10011
  durationSec: n.durationSteps * secondsPerStep * gate
9821
10012
  });
@@ -10090,6 +10281,7 @@ var SoundFont = class _SoundFont {
10090
10281
  pitch = 60,
10091
10282
  volume = 1,
10092
10283
  velocity,
10284
+ detuneCents = 0,
10093
10285
  when = 0,
10094
10286
  duration = 1
10095
10287
  } = {}) {
@@ -10109,7 +10301,7 @@ var SoundFont = class _SoundFont {
10109
10301
  Object.assign(src, _param.src);
10110
10302
  const humanizeCents = (Math.random() * 2 - 1) * _SoundFont.humanizeDetuneCents;
10111
10303
  const humanizeGainMul = 1 + (Math.random() * 2 - 1) * _SoundFont.humanizeGainRatio;
10112
- src.detune.setValueAtTime(humanizeCents, 0);
10304
+ src.detune.setValueAtTime(humanizeCents + detuneCents, 0);
10113
10305
  const effectiveVolume = volume * humanizeGainMul;
10114
10306
  const brightness = velocity === void 0 ? 1 : Math.max(0, Math.min(1, velocity / 127));
10115
10307
  const filter = brightness >= _SoundFont.brightnessBypassAbove ? void 0 : ctx.createBiquadFilter();
@@ -11248,7 +11440,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11248
11440
  scheduleMetronomeBeats(ctx, cutGain);
11249
11441
  };
11250
11442
  const wafPlayDynamic = ({
11251
- pitch,
11443
+ pitchUnits,
11252
11444
  velocity,
11253
11445
  volume,
11254
11446
  when,
@@ -11258,10 +11450,12 @@ var mountChordPlayer = (target, chords, options = {}) => {
11258
11450
  }) => {
11259
11451
  const waf = activeGmName ? _wafCache.get(activeGmName) : null;
11260
11452
  if (waf) {
11453
+ const { midi, detuneCents } = unitsToMidiDetune(pitchUnits);
11261
11454
  waf.play({
11262
11455
  ctx,
11263
11456
  destination,
11264
- pitch,
11457
+ pitch: midi,
11458
+ detuneCents,
11265
11459
  volume: volume * 0.85,
11266
11460
  velocity,
11267
11461
  when,
@@ -11271,7 +11465,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11271
11465
  const synth = createSynth(ctx, destination);
11272
11466
  synth.playNote({
11273
11467
  trackId: "chord",
11274
- pitch,
11468
+ pitchUnits,
11275
11469
  velocity: 100,
11276
11470
  volume,
11277
11471
  when,
@@ -11300,7 +11494,8 @@ var mountChordPlayer = (target, chords, options = {}) => {
11300
11494
  trackIndex: 3,
11301
11495
  startStep: whenStep,
11302
11496
  durationSteps,
11303
- pitch: C32 + noteOffset,
11497
+ // chord-parser の構成音は12平均律の半音。内部表現の units へ写す。
11498
+ pitchUnits: pitchV1ToUnits(C32 + noteOffset),
11304
11499
  velocity: 100
11305
11500
  });
11306
11501
  }
@@ -11313,7 +11508,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11313
11508
  synth: false,
11314
11509
  onPlayNote: (e) => {
11315
11510
  wafPlayDynamic({
11316
- pitch: e.pitch,
11511
+ pitchUnits: e.pitchUnits,
11317
11512
  velocity: e.velocity,
11318
11513
  volume: e.volume,
11319
11514
  when: e.when,
@@ -11755,6 +11950,14 @@ var buildUI = (target, options) => {
11755
11950
  <summary>\u5168\u4F53\u30C8\u30E9\u30C3\u30AF\u8A2D\u5B9A</summary>
11756
11951
  <div class="dtm-panel-body">
11757
11952
  <div data-dtm="preset-select-slot"></div>
11953
+ <div class="dtm-row">
11954
+ <span class="dtm-label">\u97F3\u5F8B</span>
11955
+ <select class="dtm-select dtm-grow" data-dtm="edo-select">
11956
+ <option value="12">12\u5E73\u5747\u5F8B\uFF08\u901A\u5E38\uFF09</option>
11957
+ <option value="31">31\u5E73\u5747\u5F8B\uFF08\u5FAE\u5206\u97F3\uFF09</option>
11958
+ </select>
11959
+ <button class="dtm-infobtn" data-dtm="edo-info" title="\u97F3\u5F8B\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
11960
+ </div>
11758
11961
  <div class="dtm-row">
11759
11962
  <span class="dtm-label">\u5168\u4F53\u97F3\u91CF</span>
11760
11963
  <input type="range" class="dtm-range dtm-grow" data-dtm="master-volume" value="50" min="0" max="100">
@@ -12087,6 +12290,8 @@ var buildUI = (target, options) => {
12087
12290
  copyMiniBtn: sel("copy-mini"),
12088
12291
  overlay: sel("overlay"),
12089
12292
  mmlInfoBtn: sel("mml-info"),
12293
+ edoSelect: sel("edo-select"),
12294
+ edoInfoBtn: sel("edo-info"),
12090
12295
  modalOverlay: sel("modal-overlay"),
12091
12296
  modalTitle: sel("modal-title"),
12092
12297
  modalBody: sel("modal-body"),
@@ -12207,17 +12412,21 @@ var SCALES = [
12207
12412
  [0, 2, 4, 7, 9]
12208
12413
  // Pentatonic Major
12209
12414
  ];
12415
+ var MEANTONE_STEP_BY_SEMITONE2 = [0, 3, 5, 8, 10, 13, 16, 18, 21, 23, 26, 28];
12210
12416
  var generateRandomPattern = (core, options) => {
12211
- const { stepsPerBar, startStep, pitchRangeStart } = options;
12417
+ const { stepsPerBar, startStep, pitchRangeStart, edo = 12 } = options;
12212
12418
  const numBars = 8;
12213
12419
  const noteLength = 24;
12214
- const basePitch = pitchRangeStart + 60;
12420
+ const semitoneToStep = (semi) => edo === 31 ? MEANTONE_STEP_BY_SEMITONE2[semi] : semi;
12421
+ const upr = UNITS_PER_OCTAVE / edo;
12422
+ const basePitch = pitchRangeStart + 60 * UNITS_PER_SEMITONE;
12215
12423
  const scale = SCALES[Math.floor(Math.random() * SCALES.length)];
12216
12424
  const rootOffset = Math.floor(Math.random() * 12);
12217
12425
  const availablePitches = [];
12218
12426
  for (let i2 = 0; i2 < 12; i2++) {
12219
12427
  const noteInOctave = (i2 - rootOffset + 12) % 12;
12220
- if (scale.includes(noteInOctave)) availablePitches.push(basePitch + i2);
12428
+ if (scale.includes(noteInOctave))
12429
+ availablePitches.push(units(basePitch + semitoneToStep(i2) * upr));
12221
12430
  }
12222
12431
  core.beginBatch();
12223
12432
  for (let bar = 0; bar < numBars; bar++) {
@@ -12252,14 +12461,17 @@ var applyHarmonicFilter = (targetCore, chordCore, options) => {
12252
12461
  const isNewBar = halfBar % 2 === 0;
12253
12462
  const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12254
12463
  if (chordHere.length > 0) {
12255
- currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
12464
+ currentClasses = new Set(
12465
+ chordHere.map((n) => n.pitchUnits % UNITS_PER_OCTAVE)
12466
+ );
12256
12467
  } else if (isNewBar) {
12257
12468
  currentClasses = /* @__PURE__ */ new Set();
12258
12469
  }
12259
12470
  if (currentClasses.size === 0) continue;
12260
12471
  const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12261
12472
  for (const n of activeHere) {
12262
- if (!currentClasses.has(n.pitch % 12)) targetCore.deleteNoteById(n.id);
12473
+ if (!currentClasses.has(n.pitchUnits % UNITS_PER_OCTAVE))
12474
+ targetCore.deleteNoteById(n.id);
12263
12475
  }
12264
12476
  }
12265
12477
  targetCore.endBatch();
@@ -12281,13 +12493,17 @@ var applyMonophonic = (targetCore, chordCore, options) => {
12281
12493
  const isNewBar = halfBar % 2 === 0;
12282
12494
  const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12283
12495
  if (chordHere.length > 0) {
12284
- currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
12496
+ currentClasses = new Set(
12497
+ chordHere.map((n) => n.pitchUnits % UNITS_PER_OCTAVE)
12498
+ );
12285
12499
  } else if (isNewBar) {
12286
12500
  currentClasses = /* @__PURE__ */ new Set();
12287
12501
  }
12288
12502
  if (currentClasses.size === 0) continue;
12289
12503
  const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12290
- const filtered = activeHere.filter((n) => currentClasses.has(n.pitch % 12));
12504
+ const filtered = activeHere.filter(
12505
+ (n) => currentClasses.has(n.pitchUnits % UNITS_PER_OCTAVE)
12506
+ );
12291
12507
  const filteredIds = new Set(filtered.map((n) => n.id));
12292
12508
  for (const n of activeHere) {
12293
12509
  if (!filteredIds.has(n.id)) targetCore.deleteNoteById(n.id);
@@ -12299,7 +12515,7 @@ var applyMonophonic = (targetCore, chordCore, options) => {
12299
12515
  }
12300
12516
  for (const notesAtTime of timeMap.values()) {
12301
12517
  if (notesAtTime.length > 1) {
12302
- notesAtTime.sort((a, b) => b.pitch - a.pitch);
12518
+ notesAtTime.sort((a, b) => b.pitchUnits - a.pitchUnits);
12303
12519
  const [, ...others] = notesAtTime;
12304
12520
  for (const on of others) targetCore.deleteNoteById(on.id);
12305
12521
  }
@@ -12315,18 +12531,23 @@ var shiftNotes = (cores, shiftSteps) => {
12315
12531
  for (const note of notes) {
12316
12532
  const newStart = note.startStep + shiftSteps;
12317
12533
  if (newStart < 0) core.deleteNoteById(note.id);
12318
- else core.moveNote(note.id, newStart, note.pitch);
12534
+ else core.moveNote(note.id, newStart, note.pitchUnits);
12319
12535
  }
12320
12536
  core.saveHistory();
12321
12537
  }
12322
12538
  };
12323
- var transposeNotes = (cores, semitones) => {
12324
- if (semitones === 0) return;
12539
+ var transposeNotes = (cores, steps) => {
12540
+ if (steps === 0) return;
12325
12541
  for (const core of cores) {
12326
12542
  const notes = [...core.getNotes()];
12327
12543
  for (const note of notes) {
12328
- const newPitch = Math.max(0, Math.min(127, note.pitch + semitones));
12329
- if (newPitch !== note.pitch) {
12544
+ const newPitch = units(
12545
+ Math.max(
12546
+ PITCH_RANGE_START,
12547
+ Math.min(PITCH_RANGE_END, note.pitchUnits + steps)
12548
+ )
12549
+ );
12550
+ if (newPitch !== note.pitchUnits) {
12330
12551
  core.moveNote(note.id, note.startStep, newPitch);
12331
12552
  }
12332
12553
  }
@@ -12651,11 +12872,31 @@ var exportMIDI = (options) => {
12651
12872
  const div = 480;
12652
12873
  const tickPerStep = div / STEPS_PER_BEAT3;
12653
12874
  const midiTracks = [];
12654
- tracks.forEach((track, ch) => {
12875
+ const NOTE_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15];
12876
+ const BEND_RANGE_SEMITONES = 2;
12877
+ const bendKey = (cents) => Math.round(cents * 100);
12878
+ const bendUse = /* @__PURE__ */ new Map();
12879
+ for (const track of tracks) {
12880
+ for (const n of track.notes) {
12881
+ const { detuneCents } = unitsToMidiDetune(n.pitchUnits);
12882
+ const k = bendKey(detuneCents);
12883
+ bendUse.set(k, (bendUse.get(k) ?? 0) + 1);
12884
+ }
12885
+ }
12886
+ const bendChannel = /* @__PURE__ */ new Map();
12887
+ const ordered = [...bendUse.entries()].sort((a, b) => b[1] - a[1]);
12888
+ for (const [k] of ordered) {
12889
+ if (bendChannel.size >= NOTE_CHANNELS.length) break;
12890
+ bendChannel.set(k, NOTE_CHANNELS[bendChannel.size]);
12891
+ }
12892
+ tracks.forEach((track) => {
12655
12893
  if (track.notes.length === 0) return;
12656
- const channel = ch < 9 ? ch : ch + 1 & 15;
12657
12894
  const events = [];
12658
12895
  for (const n of track.notes) {
12896
+ const { midi, detuneCents } = unitsToMidiDetune(n.pitchUnits);
12897
+ const k = bendKey(detuneCents);
12898
+ const channel = bendChannel.get(k) ?? bendChannel.get(0) ?? 0;
12899
+ const note = Math.max(0, Math.min(127, midi));
12659
12900
  const startTick = Math.round(n.startStep * tickPerStep);
12660
12901
  const endTick = Math.round(
12661
12902
  (n.startStep + (n.durationSteps || 1)) * tickPerStep
@@ -12663,12 +12904,25 @@ var exportMIDI = (options) => {
12663
12904
  const vel = Math.round(
12664
12905
  (n.velocity ?? DEFAULT_VELOCITY) * (track.volume ?? 100) / 100
12665
12906
  );
12666
- events.push({ t: startTick, m: [144 | channel, n.pitch, vel] });
12667
- events.push({ t: endTick, m: [144 | channel, n.pitch, 0] });
12907
+ events.push({ t: startTick, m: [144 | channel, note, vel] });
12908
+ events.push({ t: endTick, m: [144 | channel, note, 0] });
12668
12909
  }
12669
12910
  events.sort((a, b) => a.t - b.t);
12670
12911
  midiTracks.push(events);
12671
12912
  });
12913
+ const bendSetup = [];
12914
+ for (const [k, channel] of bendChannel) {
12915
+ const cents = k / 100;
12916
+ bendSetup.push({ t: 0, m: [176 | channel, 101, 0] });
12917
+ bendSetup.push({ t: 0, m: [176 | channel, 100, 0] });
12918
+ bendSetup.push({ t: 0, m: [176 | channel, 6, BEND_RANGE_SEMITONES] });
12919
+ bendSetup.push({ t: 0, m: [176 | channel, 38, 0] });
12920
+ bendSetup.push({ t: 0, m: [176 | channel, 101, 127] });
12921
+ bendSetup.push({ t: 0, m: [176 | channel, 100, 127] });
12922
+ const raw = 8192 + Math.round(cents / (BEND_RANGE_SEMITONES * 100) * 8192);
12923
+ const v = Math.max(0, Math.min(16383, raw));
12924
+ bendSetup.push({ t: 0, m: [224 | channel, v & 127, v >> 7 & 127] });
12925
+ }
12672
12926
  const maxStep = Math.max(
12673
12927
  ...tracks.filter((t) => t.notes.length > 0).map(
12674
12928
  (t) => Math.max(...t.notes.map((n) => n.startStep + n.durationSteps))
@@ -12705,6 +12959,7 @@ var exportMIDI = (options) => {
12705
12959
  headerChunks(arr, midiTracks.length + 1, div);
12706
12960
  trackChunks(arr, (a) => {
12707
12961
  a.push(0, 255, 81, 3, ...to3byte(Math.round(6e7 / bpm)));
12962
+ for (const ev of bendSetup) a.push(0, ...ev.m);
12708
12963
  });
12709
12964
  for (const events of midiTracks) {
12710
12965
  trackChunks(arr, (a) => {
@@ -12951,403 +13206,8 @@ var LinkedList = class {
12951
13206
  }
12952
13207
  };
12953
13208
 
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
13209
  // src/mml-core.ts
13350
- var PITCH_MAP2 = [
13210
+ var PITCH_MAP = [
13351
13211
  "c",
13352
13212
  "c+",
13353
13213
  "d",
@@ -13361,6 +13221,39 @@ var PITCH_MAP2 = [
13361
13221
  "a+",
13362
13222
  "b"
13363
13223
  ];
13224
+ var EDO31_NAMES = [
13225
+ "c",
13226
+ "c+",
13227
+ "c#",
13228
+ "d-",
13229
+ "d_",
13230
+ "d",
13231
+ "d+",
13232
+ "d#",
13233
+ "e-",
13234
+ "e_",
13235
+ "e",
13236
+ "f-",
13237
+ "e#",
13238
+ "f",
13239
+ "f+",
13240
+ "f#",
13241
+ "g-",
13242
+ "g_",
13243
+ "g",
13244
+ "g+",
13245
+ "g#",
13246
+ "a-",
13247
+ "a_",
13248
+ "a",
13249
+ "a+",
13250
+ "a#",
13251
+ "b-",
13252
+ "b_",
13253
+ "b",
13254
+ "b+",
13255
+ "b#"
13256
+ ];
13364
13257
  var MMLCore = class _MMLCore {
13365
13258
  notes = [];
13366
13259
  nextNoteId = 0;
@@ -13374,9 +13267,12 @@ var MMLCore = class _MMLCore {
13374
13267
  lastUndoTime = 0;
13375
13268
  static UNDO_DEBOUNCE_MS = 100;
13376
13269
  toolMode = "pen";
13377
- constructor(handlers, volume = 80) {
13270
+ /** グリッド寸法の供給元(描画器ではなく呼び出し側が持つ設定を読む) */
13271
+ getConfig;
13272
+ constructor(handlers, volume = 80, getConfig) {
13378
13273
  this.handlers = handlers;
13379
13274
  this.volume = volume;
13275
+ this.getConfig = getConfig;
13380
13276
  this.lastHistorySnapshot = JSON.stringify(this.notes);
13381
13277
  this.history.add([]);
13382
13278
  this.generateAndNotify();
@@ -13463,14 +13359,14 @@ var MMLCore = class _MMLCore {
13463
13359
  */
13464
13360
  addNote(step, pitch, options) {
13465
13361
  const existingIndex = this.notes.findIndex(
13466
- (n) => n.startStep === step && n.pitch === pitch
13362
+ (n) => n.startStep === step && n.pitchUnits === pitch
13467
13363
  );
13468
13364
  if (existingIndex === -1) {
13469
13365
  const newNote = {
13470
13366
  id: this.nextNoteId++,
13471
13367
  startStep: step,
13472
13368
  durationSteps: options.noteLengthSteps,
13473
- pitch,
13369
+ pitchUnits: pitch,
13474
13370
  velocity: options.velocity ?? DEFAULT_VELOCITY
13475
13371
  };
13476
13372
  this.notes.push(newNote);
@@ -13498,19 +13394,20 @@ var MMLCore = class _MMLCore {
13498
13394
  moveNote(noteId, startStep, pitch) {
13499
13395
  const note = this.notes.find((target) => target.id === noteId);
13500
13396
  if (!note) return;
13501
- const totalSteps = this.getMaxStep() + getRenderConfig().stepsPerBar;
13502
- const pitchRangeStart = getRenderConfig().pitchRangeStart;
13503
- const pitchRangeEnd = pitchRangeStart + getRenderConfig().keyCount - 1;
13504
- const clampedPitch = Math.min(
13505
- Math.max(pitch, pitchRangeStart),
13506
- pitchRangeEnd
13397
+ const totalSteps = this.getMaxStep() + this.getConfig().stepsPerBar;
13398
+ const cfg = this.getConfig();
13399
+ const upr = cfg.unitsPerRow ?? UNITS_PER_SEMITONE;
13400
+ const pitchRangeStart = cfg.pitchRangeStart;
13401
+ const pitchRangeEnd = pitchRangeStart + (cfg.keyCount - 1) * upr;
13402
+ const clampedPitch = units(
13403
+ Math.min(Math.max(pitch, pitchRangeStart), pitchRangeEnd)
13507
13404
  );
13508
13405
  const clampedStart = Math.min(
13509
13406
  Math.max(startStep, 0),
13510
13407
  totalSteps - note.durationSteps
13511
13408
  );
13512
13409
  note.startStep = clampedStart;
13513
- note.pitch = clampedPitch;
13410
+ note.pitchUnits = clampedPitch;
13514
13411
  this.notes.sort((a, b) => a.startStep - b.startStep);
13515
13412
  this.generateAndNotify();
13516
13413
  }
@@ -13555,7 +13452,7 @@ var MMLCore = class _MMLCore {
13555
13452
  * ただし、残りステップ(limit)は絶対に超えない。
13556
13453
  */
13557
13454
  stepsToMMLDuration(steps, limit) {
13558
- const config = getRenderConfig();
13455
+ const config = this.getConfig();
13559
13456
  const total = config.stepsPerBar;
13560
13457
  const candidates = [
13561
13458
  { dur: "1.", s: total * 1.5 },
@@ -13591,7 +13488,7 @@ var MMLCore = class _MMLCore {
13591
13488
  * ギャップに収まる最大の音符を探す(減算アルゴリズム用)
13592
13489
  */
13593
13490
  findBestFitDuration(gap) {
13594
- const config = getRenderConfig();
13491
+ const config = this.getConfig();
13595
13492
  const durations = [1, 2, 4, 8, 12, 16, 24, 32, 48, 64];
13596
13493
  for (const d of durations) {
13597
13494
  const stepLen = config.stepsPerBar / d;
@@ -13601,12 +13498,20 @@ var MMLCore = class _MMLCore {
13601
13498
  }
13602
13499
  return { dur: 64, steps: config.stepsPerBar / 64 };
13603
13500
  }
13501
+ /** ピッチ(units) → その音律での「オクターブ番号」と「音名」。 */
13502
+ spell(pitchUnits) {
13503
+ const edo = this.getConfig().edo ?? 12;
13504
+ const names = edo === 31 ? EDO31_NAMES : PITCH_MAP;
13505
+ const octave = Math.floor(pitchUnits / 372) - 1;
13506
+ const within = (pitchUnits % 372 + 372) % 372;
13507
+ const step = Math.round(within / (372 / edo));
13508
+ return { octave, name: names[step % edo] };
13509
+ }
13604
13510
  /**
13605
13511
  * ピッチからオクターブ最適化のある音名を取得
13606
13512
  */
13607
13513
  getNoteWithOctave(pitch, lastOctave) {
13608
- const octave = Math.floor(pitch / 12) - 1;
13609
- const name = PITCH_MAP2[pitch % 12];
13514
+ const { octave, name } = this.spell(pitch);
13610
13515
  if (lastOctave === -1 || Math.abs(octave - lastOctave) >= 2) {
13611
13516
  return { text: `o${octave}${name}`, currentOctave: octave };
13612
13517
  }
@@ -13629,7 +13534,7 @@ var MMLCore = class _MMLCore {
13629
13534
  * 各音符の長さを忠実に出力する(次の音符がなければ曲末まで伸ばせる)。
13630
13535
  */
13631
13536
  generateMML = (volumeOverride) => {
13632
- const config = getRenderConfig();
13537
+ const config = this.getConfig();
13633
13538
  const vol = volumeOverride ?? this.volume;
13634
13539
  const header = `t${this.tempo} v${vol}`;
13635
13540
  const segments = [];
@@ -13670,14 +13575,13 @@ var MMLCore = class _MMLCore {
13670
13575
  const actualStepGenerated = this.getStepFromDottedMML(durStr);
13671
13576
  if (notes.length > 1) {
13672
13577
  const noteStrs = notes.map((n) => {
13673
- const oct = Math.floor(n.pitch / 12) - 1;
13674
- const name = PITCH_MAP2[n.pitch % 12];
13578
+ const { octave: oct, name } = this.spell(n.pitchUnits);
13675
13579
  return `o${oct}${name}`;
13676
13580
  });
13677
13581
  segments.push(`[${noteStrs.join("")}]${durStr}`);
13678
13582
  } else {
13679
13583
  const { text, currentOctave } = this.getNoteWithOctave(
13680
- notes[0].pitch,
13584
+ notes[0].pitchUnits,
13681
13585
  lastOctave
13682
13586
  );
13683
13587
  segments.push(`${text}${durStr}`);
@@ -13708,7 +13612,7 @@ var MMLCore = class _MMLCore {
13708
13612
  * MMLの音長文字列("4", "4.", "12"など)をステップ数に変換する
13709
13613
  */
13710
13614
  getStepFromDottedMML(durStr) {
13711
- const config = getRenderConfig();
13615
+ const config = this.getConfig();
13712
13616
  const total = config.stepsPerBar;
13713
13617
  const isDotted = durStr.endsWith(".");
13714
13618
  const baseDur = parseInt(isDotted ? durStr.slice(0, -1) : durStr, 10);
@@ -13718,7 +13622,7 @@ var MMLCore = class _MMLCore {
13718
13622
  };
13719
13623
  var decomposeToMonophonic = (notes) => {
13720
13624
  const sorted = [...notes].sort(
13721
- (a, b) => a.startStep - b.startStep || a.pitch - b.pitch
13625
+ (a, b) => a.startStep - b.startStep || a.pitchUnits - b.pitchUnits
13722
13626
  );
13723
13627
  const tracks = [];
13724
13628
  const trackEnds = [];
@@ -13753,6 +13657,493 @@ var isChordHeavyTrack = (notes, threshold = 0.6) => {
13753
13657
  return chordNotes / notes.length >= threshold;
13754
13658
  };
13755
13659
 
13660
+ // src/renderer.ts
13661
+ var KEYBOARD_WIDTH = 60;
13662
+ var HEADER_HEIGHT = 20;
13663
+ var NATURAL_STEPS3 = {
13664
+ 12: [0, 2, 4, 5, 7, 9, 11],
13665
+ 31: [0, 5, 10, 13, 18, 23, 28]
13666
+ };
13667
+ var keyTier = (step, edo) => {
13668
+ const naturals = NATURAL_STEPS3[edo] ?? NATURAL_STEPS3[12];
13669
+ let best = edo;
13670
+ for (const n of naturals) {
13671
+ const d = Math.abs(step - n);
13672
+ best = Math.min(best, d, edo - d);
13673
+ }
13674
+ if (best === 0) return 0;
13675
+ return best >= (edo === 31 ? 2 : 1) ? 2 : 1;
13676
+ };
13677
+ var KEY_NAMES = [
13678
+ "C",
13679
+ "C#",
13680
+ "D",
13681
+ "D#",
13682
+ "E",
13683
+ "F",
13684
+ "F#",
13685
+ "G",
13686
+ "G#",
13687
+ "A",
13688
+ "A#",
13689
+ "B"
13690
+ ];
13691
+ var createRenderer = (mountTarget, width = 800, height = 450, config) => {
13692
+ let g_header_canvas;
13693
+ let g_key_canvas;
13694
+ let g_grid_canvas;
13695
+ let g_header_ctx;
13696
+ let g_key_ctx;
13697
+ let g_grid_ctx;
13698
+ let g_config = config;
13699
+ const getRenderConfig = () => g_config;
13700
+ let g_draw_offset_x = 0;
13701
+ let g_draw_offset_y = 0;
13702
+ let g_bg_active = false;
13703
+ const setBackgroundActive = (active) => {
13704
+ g_bg_active = active;
13705
+ };
13706
+ const getDrawOffset = () => ({
13707
+ x: g_draw_offset_x,
13708
+ y: g_draw_offset_y
13709
+ });
13710
+ const getGridCanvas = () => g_grid_canvas;
13711
+ const getGridContext = () => g_grid_ctx;
13712
+ const getHeaderCanvas = () => g_header_canvas;
13713
+ const setup = () => {
13714
+ g_config = config;
13715
+ const headerCanvas = document.createElement("canvas");
13716
+ g_header_canvas = headerCanvas;
13717
+ headerCanvas.width = width - KEYBOARD_WIDTH;
13718
+ headerCanvas.height = HEADER_HEIGHT;
13719
+ headerCanvas.style.position = "absolute";
13720
+ headerCanvas.style.left = `${KEYBOARD_WIDTH}px`;
13721
+ headerCanvas.style.top = "0px";
13722
+ const headerCtx = headerCanvas.getContext("2d");
13723
+ if (!headerCtx)
13724
+ throw new Error("Failed to get 2D rendering context for header.");
13725
+ g_header_ctx = headerCtx;
13726
+ const keyCanvas = document.createElement("canvas");
13727
+ g_key_canvas = keyCanvas;
13728
+ keyCanvas.width = KEYBOARD_WIDTH;
13729
+ keyCanvas.height = height - HEADER_HEIGHT;
13730
+ keyCanvas.style.position = "absolute";
13731
+ keyCanvas.style.left = "0px";
13732
+ keyCanvas.style.top = `${HEADER_HEIGHT}px`;
13733
+ const keyCtx = keyCanvas.getContext("2d");
13734
+ if (!keyCtx)
13735
+ throw new Error("Failed to get 2D rendering context for keyboard.");
13736
+ g_key_ctx = keyCtx;
13737
+ const gridCanvas = document.createElement("canvas");
13738
+ g_grid_canvas = gridCanvas;
13739
+ gridCanvas.width = width - KEYBOARD_WIDTH;
13740
+ gridCanvas.height = height - HEADER_HEIGHT;
13741
+ gridCanvas.style.position = "absolute";
13742
+ gridCanvas.style.left = `${KEYBOARD_WIDTH}px`;
13743
+ gridCanvas.style.top = `${HEADER_HEIGHT}px`;
13744
+ gridCanvas.style.touchAction = "none";
13745
+ gridCanvas.style.userSelect = "none";
13746
+ const gridCtx = gridCanvas.getContext("2d", { willReadFrequently: true });
13747
+ if (!gridCtx)
13748
+ throw new Error("Failed to get 2D rendering context for grid.");
13749
+ g_grid_ctx = gridCtx;
13750
+ mountTarget.innerHTML = "";
13751
+ mountTarget.style.position = "relative";
13752
+ mountTarget.style.width = `${width + KEYBOARD_WIDTH}px`;
13753
+ mountTarget.style.height = `${height}px`;
13754
+ mountTarget.append(headerCanvas, keyCanvas, gridCanvas);
13755
+ drawHeaderCorner();
13756
+ };
13757
+ const drawHeaderCorner = () => {
13758
+ const mountTarget2 = g_key_canvas.parentElement;
13759
+ if (!mountTarget2) return;
13760
+ let cornerDiv = mountTarget2.querySelector(
13761
+ "#header-corner"
13762
+ );
13763
+ if (!cornerDiv) {
13764
+ cornerDiv = document.createElement("div");
13765
+ cornerDiv.id = "header-corner";
13766
+ cornerDiv.style.position = "absolute";
13767
+ cornerDiv.style.left = "0px";
13768
+ cornerDiv.style.top = "0px";
13769
+ cornerDiv.style.width = `${KEYBOARD_WIDTH}px`;
13770
+ cornerDiv.style.height = `${HEADER_HEIGHT}px`;
13771
+ cornerDiv.style.backgroundColor = "#0a0f1f";
13772
+ cornerDiv.style.borderRight = "2px solid #29adff";
13773
+ cornerDiv.style.borderBottom = "2px solid #29adff";
13774
+ mountTarget2.insertBefore(cornerDiv, g_header_canvas);
13775
+ }
13776
+ };
13777
+ const drawKeyboard = () => {
13778
+ g_key_ctx.clearRect(0, 0, g_key_canvas.width, g_key_canvas.height);
13779
+ const {
13780
+ keyHeight,
13781
+ keyCount,
13782
+ pitchRangeStart,
13783
+ unitsPerRow: upr = UNITS_PER_SEMITONE,
13784
+ edo = 12
13785
+ } = g_config;
13786
+ const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
13787
+ const endY = g_draw_offset_y + g_key_canvas.height;
13788
+ const WHITE_KEY = "#ccc8b4";
13789
+ const BLACK_KEY = "#111111";
13790
+ const MICRO_KEY = "#4a4a4a";
13791
+ const BK_EDGE = "#383838";
13792
+ const WW_SEP = "#807a6a";
13793
+ const BK_RATIO = 0.62;
13794
+ const MICRO_RATIO = 0.45;
13795
+ for (let y = startY; y < endY; y += keyHeight) {
13796
+ const rowIndex = keyCount - 1 - y / keyHeight;
13797
+ const units2 = pitchRangeStart + rowIndex * upr;
13798
+ const step = Math.round(
13799
+ (units2 % UNITS_PER_OCTAVE + UNITS_PER_OCTAVE) % UNITS_PER_OCTAVE / upr % edo
13800
+ );
13801
+ const tier = keyTier(step, edo);
13802
+ const octave = Math.floor(units2 / UNITS_PER_OCTAVE) - 1;
13803
+ const isC4Range = octave === 4;
13804
+ const screenY = y - g_draw_offset_y;
13805
+ const bkW = Math.floor(
13806
+ KEYBOARD_WIDTH * (tier === 1 ? MICRO_RATIO : BK_RATIO)
13807
+ );
13808
+ if (tier !== 0) {
13809
+ g_key_ctx.fillStyle = isC4Range ? "#d8d4be" : WHITE_KEY;
13810
+ g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
13811
+ g_key_ctx.fillStyle = tier === 1 ? MICRO_KEY : isC4Range ? "#1a1408" : BLACK_KEY;
13812
+ g_key_ctx.fillRect(0, screenY, bkW, keyHeight);
13813
+ g_key_ctx.strokeStyle = BK_EDGE;
13814
+ g_key_ctx.lineWidth = 1;
13815
+ g_key_ctx.beginPath();
13816
+ g_key_ctx.moveTo(bkW, screenY);
13817
+ g_key_ctx.lineTo(bkW, screenY + keyHeight);
13818
+ g_key_ctx.stroke();
13819
+ } else {
13820
+ g_key_ctx.fillStyle = isC4Range ? "#dedad0" : WHITE_KEY;
13821
+ g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
13822
+ if (edo === 12 && (step === 5 || step === 0)) {
13823
+ g_key_ctx.strokeStyle = WW_SEP;
13824
+ g_key_ctx.lineWidth = 1;
13825
+ g_key_ctx.beginPath();
13826
+ g_key_ctx.moveTo(0, screenY + keyHeight - 0.5);
13827
+ g_key_ctx.lineTo(KEYBOARD_WIDTH, screenY + keyHeight - 0.5);
13828
+ g_key_ctx.stroke();
13829
+ }
13830
+ }
13831
+ if (step === 0) {
13832
+ g_key_ctx.fillStyle = "#555040";
13833
+ g_key_ctx.font = "10px 'k8x12',monospace";
13834
+ g_key_ctx.textAlign = "right";
13835
+ g_key_ctx.textBaseline = "bottom";
13836
+ g_key_ctx.fillText(
13837
+ `${KEY_NAMES[0]}${octave}`,
13838
+ KEYBOARD_WIDTH - 4,
13839
+ screenY + keyHeight - 2
13840
+ );
13841
+ }
13842
+ }
13843
+ g_key_ctx.beginPath();
13844
+ g_key_ctx.strokeStyle = "#29adff";
13845
+ g_key_ctx.lineWidth = 2;
13846
+ g_key_ctx.moveTo(KEYBOARD_WIDTH, 0);
13847
+ g_key_ctx.lineTo(KEYBOARD_WIDTH, g_key_canvas.height);
13848
+ g_key_ctx.stroke();
13849
+ };
13850
+ const drawHeader = () => {
13851
+ g_header_ctx.clearRect(0, 0, g_header_canvas.width, g_header_canvas.height);
13852
+ const { stepWidth, stepsPerBar } = g_config;
13853
+ g_header_ctx.save();
13854
+ g_header_ctx.translate(-g_draw_offset_x, 0);
13855
+ g_header_ctx.fillStyle = g_bg_active ? "rgba(10,15,31,0.55)" : "#0a0f1f";
13856
+ g_header_ctx.fillRect(
13857
+ g_draw_offset_x,
13858
+ 0,
13859
+ g_header_canvas.width,
13860
+ HEADER_HEIGHT
13861
+ );
13862
+ g_header_ctx.strokeStyle = "#3d405b";
13863
+ g_header_ctx.lineWidth = 1;
13864
+ g_header_ctx.font = "11px 'k8x12',monospace";
13865
+ g_header_ctx.fillStyle = "#83769c";
13866
+ const startBar = Math.floor(g_draw_offset_x / (stepsPerBar * stepWidth));
13867
+ const endBar = Math.ceil(
13868
+ (g_draw_offset_x + g_header_canvas.width) / (stepsPerBar * stepWidth)
13869
+ );
13870
+ for (let bar = startBar; bar <= endBar + 1; bar++) {
13871
+ const x2 = bar * stepsPerBar * stepWidth;
13872
+ const screenX = x2;
13873
+ g_header_ctx.beginPath();
13874
+ g_header_ctx.moveTo(screenX, 0);
13875
+ g_header_ctx.lineTo(screenX, HEADER_HEIGHT);
13876
+ g_header_ctx.stroke();
13877
+ if (bar >= 0) {
13878
+ g_header_ctx.textAlign = "left";
13879
+ g_header_ctx.textBaseline = "middle";
13880
+ g_header_ctx.fillText(`${bar + 1}`, screenX + 5, HEADER_HEIGHT / 2);
13881
+ }
13882
+ }
13883
+ g_header_ctx.restore();
13884
+ };
13885
+ const drawGrid = (noteLengthSteps = 1) => {
13886
+ drawKeyboard();
13887
+ drawHeader();
13888
+ g_grid_ctx.clearRect(0, 0, g_grid_canvas.width, g_grid_canvas.height);
13889
+ const {
13890
+ keyHeight,
13891
+ keyCount,
13892
+ stepWidth,
13893
+ stepsPerBar,
13894
+ pitchRangeStart,
13895
+ unitsPerRow: upr = UNITS_PER_SEMITONE,
13896
+ edo = 12
13897
+ } = g_config;
13898
+ const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
13899
+ const endY = g_draw_offset_y + g_grid_canvas.height;
13900
+ for (let y = startY; y < endY; y += keyHeight) {
13901
+ const rowIndex = keyCount - 1 - y / keyHeight;
13902
+ const units2 = pitchRangeStart + rowIndex * upr;
13903
+ const step = Math.round(
13904
+ (units2 % UNITS_PER_OCTAVE + UNITS_PER_OCTAVE) % UNITS_PER_OCTAVE / upr % edo
13905
+ );
13906
+ const tier = keyTier(step, edo);
13907
+ const isC = step === 0;
13908
+ const octave = Math.floor(units2 / UNITS_PER_OCTAVE) - 1;
13909
+ const isC4Range = octave === 4;
13910
+ const screenY = y - g_draw_offset_y;
13911
+ 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";
13912
+ g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13913
+ if (isC4Range) {
13914
+ g_grid_ctx.fillStyle = "rgba(41,173,255,0.05)";
13915
+ g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13916
+ }
13917
+ g_grid_ctx.beginPath();
13918
+ g_grid_ctx.strokeStyle = isC ? "#3d405b" : "#1a1d30";
13919
+ g_grid_ctx.lineWidth = 1;
13920
+ const lineY = screenY + keyHeight;
13921
+ g_grid_ctx.moveTo(0, lineY);
13922
+ g_grid_ctx.lineTo(g_grid_canvas.width, lineY);
13923
+ g_grid_ctx.stroke();
13924
+ }
13925
+ const gridStep = noteLengthSteps || 48;
13926
+ const startX = Math.floor(g_draw_offset_x / (stepWidth * gridStep)) * stepWidth * gridStep;
13927
+ const endX = g_draw_offset_x + g_grid_canvas.width;
13928
+ const lineStep = stepWidth * gridStep;
13929
+ for (let x2 = startX; x2 <= endX; x2 += lineStep) {
13930
+ const step = x2 / stepWidth;
13931
+ const isBarLine = step % stepsPerBar === 0;
13932
+ const isNoteLine = step % gridStep === 0;
13933
+ const screenX = x2 - g_draw_offset_x;
13934
+ g_grid_ctx.beginPath();
13935
+ g_grid_ctx.strokeStyle = isBarLine ? "#3d405b" : isNoteLine ? "#242840" : "#1a1d30";
13936
+ g_grid_ctx.lineWidth = isBarLine ? 2 : 1;
13937
+ g_grid_ctx.moveTo(screenX, 0);
13938
+ g_grid_ctx.lineTo(screenX, g_grid_canvas.height);
13939
+ g_grid_ctx.stroke();
13940
+ }
13941
+ };
13942
+ const drawNotes = (notes, color = [59, 130, 246, 1], isActive = true) => {
13943
+ const {
13944
+ keyHeight,
13945
+ stepWidth,
13946
+ keyCount,
13947
+ pitchRangeStart,
13948
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13949
+ } = g_config;
13950
+ const canvasWidth = g_grid_canvas.width;
13951
+ const canvasHeight = g_grid_canvas.height;
13952
+ const [r, g, b, a] = color;
13953
+ for (const note of notes) {
13954
+ const logicalX = note.startStep * stepWidth;
13955
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
13956
+ const logicalY = yIndex * keyHeight;
13957
+ const w = note.durationSteps * stepWidth;
13958
+ const h = keyHeight;
13959
+ const renderX = logicalX - g_draw_offset_x;
13960
+ const renderY = logicalY - g_draw_offset_y;
13961
+ if (renderX + w < 0 || renderX > canvasWidth) continue;
13962
+ if (renderY + h < 0 || renderY > canvasHeight) continue;
13963
+ if (isActive) {
13964
+ const velocityOpacity = note.velocity !== void 0 ? 0.6 + note.velocity / 127 * 0.4 : 1;
13965
+ const finalOpacity = a * velocityOpacity;
13966
+ g_grid_ctx.fillStyle = `rgba(${r},${g},${b},${finalOpacity})`;
13967
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13968
+ if (w >= 4 && h >= 4) {
13969
+ g_grid_ctx.fillStyle = "rgba(255,255,255,0.4)";
13970
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, 1);
13971
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, 1, h - 2);
13972
+ g_grid_ctx.fillStyle = "rgba(0,0,0,0.45)";
13973
+ g_grid_ctx.fillRect(renderX + 1, renderY + h - 2, w - 2, 1);
13974
+ g_grid_ctx.fillRect(renderX + w - 2, renderY + 1, 1, h - 2);
13975
+ }
13976
+ } else {
13977
+ const velocityOpacity = note.velocity !== void 0 ? 0.7 + note.velocity / 127 * 0.3 : 1;
13978
+ const finalOpacity = Math.min(0.25, a * 0.22) * velocityOpacity;
13979
+ const gray = 0.299 * r + 0.587 * g + 0.114 * b;
13980
+ const ghostR = Math.round(r * 0.6 + gray * 0.4);
13981
+ const ghostG = Math.round(g * 0.6 + gray * 0.4);
13982
+ const ghostB = Math.round(b * 0.6 + gray * 0.4);
13983
+ g_grid_ctx.fillStyle = `rgba(${ghostR},${ghostG},${ghostB},${finalOpacity})`;
13984
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13985
+ }
13986
+ }
13987
+ };
13988
+ const drawNoteLyrics = (notes, syllables) => {
13989
+ if (syllables.length === 0) return;
13990
+ const {
13991
+ keyHeight,
13992
+ stepWidth,
13993
+ keyCount,
13994
+ pitchRangeStart,
13995
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13996
+ } = g_config;
13997
+ if (keyHeight < 7) return;
13998
+ const fontSize = Math.min(12, Math.floor(keyHeight * 0.85));
13999
+ const sorted = [...notes].sort((a, b) => a.startStep - b.startStep);
14000
+ const count = Math.min(sorted.length, syllables.length);
14001
+ g_grid_ctx.save();
14002
+ g_grid_ctx.font = `${fontSize}px 'k8x12',sans-serif`;
14003
+ g_grid_ctx.textAlign = "left";
14004
+ g_grid_ctx.textBaseline = "middle";
14005
+ g_grid_ctx.lineWidth = 3;
14006
+ g_grid_ctx.lineJoin = "round";
14007
+ for (let i2 = 0; i2 < count; i2++) {
14008
+ const kana = syllables[i2];
14009
+ if (!kana) continue;
14010
+ const note = sorted[i2];
14011
+ const renderX = note.startStep * stepWidth - g_draw_offset_x;
14012
+ const renderY = (keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr) * keyHeight - g_draw_offset_y;
14013
+ const w = note.durationSteps * stepWidth;
14014
+ if (w < 6) continue;
14015
+ if (renderX + w < 0 || renderX > g_grid_canvas.width) continue;
14016
+ if (renderY + keyHeight < 0 || renderY > g_grid_canvas.height) continue;
14017
+ const textX = renderX + 2;
14018
+ const textY = renderY + keyHeight / 2;
14019
+ g_grid_ctx.save();
14020
+ g_grid_ctx.beginPath();
14021
+ g_grid_ctx.rect(renderX + 1, renderY + 1, w - 2, keyHeight - 2);
14022
+ g_grid_ctx.clip();
14023
+ g_grid_ctx.strokeStyle = "rgba(0,0,0,0.85)";
14024
+ g_grid_ctx.strokeText(kana, textX, textY);
14025
+ g_grid_ctx.fillStyle = "#fff1e8";
14026
+ g_grid_ctx.fillText(kana, textX, textY);
14027
+ g_grid_ctx.restore();
14028
+ }
14029
+ g_grid_ctx.restore();
14030
+ };
14031
+ const drawSelectionRect = (rect) => {
14032
+ if (!rect) return;
14033
+ g_grid_ctx.save();
14034
+ g_grid_ctx.strokeStyle = "#ffec27";
14035
+ g_grid_ctx.lineWidth = 2;
14036
+ g_grid_ctx.setLineDash([4, 4]);
14037
+ g_grid_ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
14038
+ g_grid_ctx.fillStyle = "rgba(255,236,39,0.08)";
14039
+ g_grid_ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
14040
+ g_grid_ctx.restore();
14041
+ };
14042
+ const drawSelectedNotes = (notes, selectedIds, baseColor = [59, 130, 246, 1]) => {
14043
+ const {
14044
+ keyHeight,
14045
+ stepWidth,
14046
+ keyCount,
14047
+ pitchRangeStart,
14048
+ unitsPerRow: upr = UNITS_PER_SEMITONE
14049
+ } = g_config;
14050
+ for (const note of notes) {
14051
+ if (!selectedIds.has(note.id)) continue;
14052
+ const logicalX = note.startStep * stepWidth;
14053
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
14054
+ const logicalY = yIndex * keyHeight;
14055
+ const w = note.durationSteps * stepWidth;
14056
+ const h = keyHeight;
14057
+ const renderX = logicalX - g_draw_offset_x;
14058
+ const renderY = logicalY - g_draw_offset_y;
14059
+ const velocityOpacity = note.velocity !== void 0 ? 0.5 + note.velocity / 127 * 0.5 : 1;
14060
+ const [r, g, b, a] = baseColor;
14061
+ const darkenFactor = 1.3;
14062
+ const darkerR = Math.min(255, r * darkenFactor);
14063
+ const darkerG = Math.min(255, g * darkenFactor);
14064
+ const darkerB = Math.min(255, b * darkenFactor);
14065
+ const finalOpacity = a * velocityOpacity;
14066
+ g_grid_ctx.fillStyle = `rgba(${darkerR},${darkerG},${darkerB},${finalOpacity})`;
14067
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
14068
+ }
14069
+ };
14070
+ const getXY = (e) => {
14071
+ const { clientX, clientY } = e;
14072
+ const rect = g_grid_canvas.getBoundingClientRect();
14073
+ const x2 = Math.floor(clientX - rect.left);
14074
+ const y = Math.floor(clientY - rect.top);
14075
+ return [x2, y, e.buttons];
14076
+ };
14077
+ const getGridPosition = (e) => {
14078
+ const [x2, y] = getXY(e);
14079
+ const {
14080
+ keyCount,
14081
+ pitchRangeStart,
14082
+ keyHeight,
14083
+ stepWidth,
14084
+ unitsPerRow: upr = UNITS_PER_SEMITONE
14085
+ } = g_config;
14086
+ const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
14087
+ const absoluteY = y + g_draw_offset_y;
14088
+ const yIndex = Math.floor(absoluteY / keyHeight);
14089
+ const pitch = units(pitchRangeStart + (keyCount - 1 - yIndex) * upr);
14090
+ return { step, pitch, x: x2, y };
14091
+ };
14092
+ const onClick = (callback) => {
14093
+ g_grid_canvas.addEventListener(
14094
+ "click",
14095
+ (e) => {
14096
+ const [x2, y] = getXY(e);
14097
+ const {
14098
+ keyCount,
14099
+ pitchRangeStart,
14100
+ keyHeight,
14101
+ stepWidth,
14102
+ unitsPerRow: upr = UNITS_PER_SEMITONE
14103
+ } = g_config;
14104
+ const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
14105
+ const absoluteY = y + g_draw_offset_y;
14106
+ const yIndex = Math.floor(absoluteY / keyHeight);
14107
+ const pitch = units(pitchRangeStart + (keyCount - 1 - yIndex) * upr);
14108
+ if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount) {
14109
+ requestAnimationFrame(() => callback(step, pitch));
14110
+ }
14111
+ },
14112
+ { passive: true }
14113
+ );
14114
+ g_grid_canvas.addEventListener("contextmenu", (e) => e.preventDefault());
14115
+ };
14116
+ const setDrawOffset = (x2, y) => {
14117
+ g_draw_offset_x = x2;
14118
+ g_draw_offset_y = y;
14119
+ drawKeyboard();
14120
+ drawHeader();
14121
+ };
14122
+ setup();
14123
+ return {
14124
+ getRenderConfig,
14125
+ setBackgroundActive,
14126
+ getDrawOffset,
14127
+ getGridCanvas,
14128
+ getGridContext,
14129
+ getHeaderCanvas,
14130
+ drawKeyboard,
14131
+ drawHeader,
14132
+ drawGrid,
14133
+ drawNotes,
14134
+ drawNoteLyrics,
14135
+ drawSelectionRect,
14136
+ drawSelectedNotes,
14137
+ getXY,
14138
+ getGridPosition,
14139
+ onClick,
14140
+ setDrawOffset,
14141
+ destroy: () => {
14142
+ mountTarget.innerHTML = "";
14143
+ }
14144
+ };
14145
+ };
14146
+
13756
14147
  // src/daw.ts
13757
14148
  var CHORD_INFO_HTML2 = `
13758
14149
  <div class="dtm-modal-body-content">
@@ -14039,6 +14430,27 @@ var MIDI_INFO_HTML = `
14039
14430
  <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
14431
  </div>
14041
14432
  `;
14433
+ var EDO_INFO_HTML = `
14434
+ <div class="dtm-modal-section">
14435
+ <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>
14436
+ <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>
14437
+ <p style="margin-top:8px;"><strong>31\u5E73\u5747\u5F8B\u306E\u7279\u5FB4</strong></p>
14438
+ <ul>
14439
+ <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>
14440
+ <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>
14441
+ <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>
14442
+ </ul>
14443
+ <p style="margin-top:8px;"><strong>MML\u3067\u306E\u66F8\u304D\u65B9</strong></p>
14444
+ <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>
14445
+ <ul>
14446
+ <li><code>#</code> \u2026 \u534A\u97F3\u4E0A\u3052\uFF082\u5EA6\uFF09\u3000<code>-</code> \u2026 \u534A\u97F3\u4E0B\u3052\uFF082\u5EA6\uFF09</li>
14447
+ <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>
14448
+ </ul>
14449
+ <p>\u4F8B: <code>#edo=31 @0 o4 c c+ c# d- d_ d;</code></p>
14450
+ <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>
14451
+ <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>
14452
+ </div>
14453
+ `;
14042
14454
  var KOE_INFO_HTML = `
14043
14455
  <div class="dtm-modal-body-content">
14044
14456
  <h4>1. UTAU\u97F3\u6E90\u3092 .koe \u306B\u5909\u63DB\u3059\u308B</h4>
@@ -14256,7 +14668,7 @@ var AUTO_ROLE_BASE_VOLUME = {
14256
14668
  chord: 72
14257
14669
  };
14258
14670
  var computeAutoRoleStats = (notes) => {
14259
- const avgPitch = notes.reduce((s, n) => s + n.pitch, 0) / notes.length;
14671
+ const avgPitch = notes.reduce((s, n) => s + n.pitchUnits, 0) / notes.length;
14260
14672
  const avgDur = notes.reduce((s, n) => s + n.durationSteps, 0) / notes.length;
14261
14673
  const byStart = /* @__PURE__ */ new Map();
14262
14674
  for (const n of notes)
@@ -14270,8 +14682,9 @@ var computeAutoRoleStats = (notes) => {
14270
14682
  const notesPerBeat = notes.length / spanBeats;
14271
14683
  return { avgPitch, avgDur, avgPoly, maxPoly, notesPerBeat };
14272
14684
  };
14685
+ var AUTO_ROLE_BASS_UNITS = 48 * UNITS_PER_SEMITONE;
14273
14686
  var classifyTrackRole = (stats) => {
14274
- if (stats.avgPitch < 48) return "bass";
14687
+ if (stats.avgPitch < AUTO_ROLE_BASS_UNITS) return "bass";
14275
14688
  if (stats.maxPoly >= 2 && stats.avgDur >= AUTO_ROLE_STEPS_PER_BEAT)
14276
14689
  return "chord";
14277
14690
  if (stats.notesPerBeat < 1.2 && stats.avgDur >= AUTO_ROLE_STEPS_PER_BEAT * 1.5)
@@ -14283,7 +14696,10 @@ var computeAutoRoleVolume = (role, stats) => {
14283
14696
  if (role === "chord" && stats.avgPoly > 1) {
14284
14697
  vol /= Math.sqrt(stats.avgPoly);
14285
14698
  } else if (role === "bass") {
14286
- const depthSemitones = Math.max(0, 48 - stats.avgPitch);
14699
+ const depthSemitones = Math.max(
14700
+ 0,
14701
+ (AUTO_ROLE_BASS_UNITS - stats.avgPitch) / UNITS_PER_SEMITONE
14702
+ );
14287
14703
  vol += Math.min(12, depthSemitones * 0.5);
14288
14704
  }
14289
14705
  return clamp5(Math.round(vol), 1, 127);
@@ -14375,17 +14791,48 @@ var mountDAW = (target, options = {}) => {
14375
14791
  refs.drumVolumeLabel.textContent = `${options.drumVolume ?? 80}%`;
14376
14792
  const renderConfig = {
14377
14793
  stepsPerBar: 192,
14378
- keyCount: 128,
14379
- pitchRangeStart: 0,
14794
+ keyCount: KEY_COUNT,
14795
+ pitchRangeStart: PITCH_RANGE_START,
14796
+ unitsPerRow: unitsPerRow(12),
14797
+ edo: 12,
14380
14798
  keyHeight: BASE_KEY_HEIGHT,
14381
14799
  stepWidth: BASE_STEP_WIDTH * 2
14382
14800
  // zoom100% 相当
14383
14801
  };
14802
+ let renderer;
14384
14803
  let zoomX = 100;
14385
14804
  let zoomY = 100;
14386
14805
  let bpm = options.defaultBpm ?? DEFAULT_BPM;
14387
14806
  let masterVolume = options.masterVolume ?? 50;
14388
- options.singingVoices?.setVolume(masterVolume / 100);
14807
+ const applyMasterVolume = (volume) => {
14808
+ masterVolume = clamp5(Math.round(volume), 0, 100);
14809
+ refs.masterVolume.value = String(masterVolume);
14810
+ refs.masterVolumeLabel.textContent = `${masterVolume}%`;
14811
+ options.singingVoices?.setVolume(masterVolume / 100);
14812
+ };
14813
+ applyMasterVolume(masterVolume);
14814
+ const snapToEdoGrid = (u) => {
14815
+ const upr = renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE;
14816
+ return units(Math.round(u / upr) * upr);
14817
+ };
14818
+ const applyEdo = (edo, opts) => {
14819
+ const next = edo === 31 ? 31 : 12;
14820
+ if (renderConfig.edo === next) return;
14821
+ const upr = unitsPerRow(next);
14822
+ renderConfig.edo = next;
14823
+ renderConfig.unitsPerRow = upr;
14824
+ renderConfig.keyCount = keyCountFor(next);
14825
+ if (!opts?.snap) return;
14826
+ for (const t of trackStates) {
14827
+ t.core.beginBatch();
14828
+ for (const note of [...t.core.getNotes()]) {
14829
+ const snapped = units(Math.round(note.pitchUnits / upr) * upr);
14830
+ if (snapped !== note.pitchUnits)
14831
+ t.core.moveNote(note.id, note.startStep, snapped);
14832
+ }
14833
+ t.core.endBatch();
14834
+ }
14835
+ };
14389
14836
  let reverbAmount = options.reverbAmount ?? 0;
14390
14837
  let reverbDecay = options.reverbDecay ?? DEFAULT_REVERB_DECAY_SEC;
14391
14838
  let reverbPreDelay = options.reverbPreDelay ?? DEFAULT_REVERB_PREDELAY_MS;
@@ -14443,8 +14890,8 @@ var mountDAW = (target, options = {}) => {
14443
14890
  let snapGridSteps = 12;
14444
14891
  const gridLineSteps = 48;
14445
14892
  let currentOffsetX = 0;
14446
- const _initPitch = options.initialScrollPitch ?? 48;
14447
- let currentOffsetY = (renderConfig.keyCount - 1 - _initPitch) * renderConfig.keyHeight - 215;
14893
+ const _initPitch = pitchV1ToUnits(options.initialScrollPitch ?? 48);
14894
+ let currentOffsetY = (renderConfig.keyCount - 1 - (_initPitch - renderConfig.pitchRangeStart) / (renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE)) * renderConfig.keyHeight - 215;
14448
14895
  let playStartStep = 0;
14449
14896
  let isSolo = false;
14450
14897
  let lyricTrackIndices = /* @__PURE__ */ new Set();
@@ -14539,21 +14986,28 @@ var mountDAW = (target, options = {}) => {
14539
14986
  if (!ready) return;
14540
14987
  if (!suppressPatch && options.onNotesPatch) {
14541
14988
  const prevByKey = new Map(
14542
- prevNotes.map((n) => [`${n.startStep}_${n.pitch}`, n])
14989
+ prevNotes.map((n) => [`${n.startStep}_${n.pitchUnits}`, n])
14543
14990
  );
14544
14991
  const currByKey = new Map(
14545
- notes.map((n) => [`${n.startStep}_${n.pitch}`, n])
14992
+ notes.map((n) => [`${n.startStep}_${n.pitchUnits}`, n])
14546
14993
  );
14547
14994
  const added = notes.filter((n) => {
14548
- const prev = prevByKey.get(`${n.startStep}_${n.pitch}`);
14995
+ const prev = prevByKey.get(
14996
+ `${n.startStep}_${n.pitchUnits}`
14997
+ );
14549
14998
  return !prev || prev.durationSteps !== n.durationSteps || prev.velocity !== n.velocity;
14550
14999
  }).map((n) => ({
14551
15000
  startStep: n.startStep,
14552
- pitch: n.pitch,
15001
+ pitchUnits: n.pitchUnits,
14553
15002
  durationSteps: n.durationSteps,
14554
15003
  velocity: n.velocity
14555
15004
  }));
14556
- const removed = prevNotes.filter((n) => !currByKey.has(`${n.startStep}_${n.pitch}`)).map((n) => ({ startStep: n.startStep, pitch: n.pitch }));
15005
+ const removed = prevNotes.filter(
15006
+ (n) => !currByKey.has(`${n.startStep}_${n.pitchUnits}`)
15007
+ ).map((n) => ({
15008
+ startStep: n.startStep,
15009
+ pitchUnits: n.pitchUnits
15010
+ }));
14557
15011
  if (added.length > 0 || removed.length > 0) {
14558
15012
  options.onNotesPatch(config.id, added, removed);
14559
15013
  }
@@ -14563,7 +15017,8 @@ var mountDAW = (target, options = {}) => {
14563
15017
  updateUndoRedo();
14564
15018
  }
14565
15019
  },
14566
- config.volume
15020
+ config.volume,
15021
+ () => renderConfig
14567
15022
  ),
14568
15023
  volume: config.volume,
14569
15024
  savedChordInput: "",
@@ -14658,18 +15113,18 @@ var mountDAW = (target, options = {}) => {
14658
15113
  return (lastNoteMeasure + 2) * renderConfig.stepsPerBar;
14659
15114
  };
14660
15115
  const getMaxOffsetX = () => {
14661
- const canvas = getGridCanvas();
15116
+ const canvas = renderer.getGridCanvas();
14662
15117
  const maxNoteStep = getMaxNoteStep();
14663
15118
  const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
14664
15119
  return Math.max(0, totalContentWidth - canvas.width);
14665
15120
  };
14666
15121
  const getMaxOffsetY = () => {
14667
15122
  const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
14668
- return Math.max(0, totalHeight - getGridCanvas().height);
15123
+ return Math.max(0, totalHeight - renderer.getGridCanvas().height);
14669
15124
  };
14670
15125
  const drawStartLine = () => {
14671
- const ctx = getGridContext();
14672
- const canvas = getGridCanvas();
15126
+ const ctx = renderer.getGridContext();
15127
+ const canvas = renderer.getGridCanvas();
14673
15128
  if (!ctx) return;
14674
15129
  const x2 = playStartStep * renderConfig.stepWidth - currentOffsetX;
14675
15130
  if (x2 < -10 || x2 > canvas.width + 10) return;
@@ -14684,8 +15139,8 @@ var mountDAW = (target, options = {}) => {
14684
15139
  ctx.restore();
14685
15140
  };
14686
15141
  const drawPlayhead = () => {
14687
- const ctx = getGridContext();
14688
- const canvas = getGridCanvas();
15142
+ const ctx = renderer.getGridContext();
15143
+ const canvas = renderer.getGridCanvas();
14689
15144
  if (!ctx) return;
14690
15145
  const x2 = currentPlayStep * renderConfig.stepWidth - currentOffsetX;
14691
15146
  if (x2 < 0 || x2 > canvas.width) return;
@@ -14699,7 +15154,7 @@ var mountDAW = (target, options = {}) => {
14699
15154
  ctx.restore();
14700
15155
  };
14701
15156
  const redrawAll = () => {
14702
- drawGrid(gridLineSteps);
15157
+ renderer.drawGrid(gridLineSteps);
14703
15158
  let lyricTargetNotes = null;
14704
15159
  let activeTrackState = null;
14705
15160
  for (const t of trackStates) {
@@ -14711,16 +15166,16 @@ var mountDAW = (target, options = {}) => {
14711
15166
  }
14712
15167
  const [r, g, b] = t.config.color;
14713
15168
  const notes = t.core.getNotes();
14714
- drawNotes(notes, [r, g, b, 1], false);
15169
+ renderer.drawNotes(notes, [r, g, b, 1], false);
14715
15170
  }
14716
15171
  if (activeTrackState) {
14717
15172
  const [r, g, b] = activeTrackState.config.color;
14718
15173
  const notes = activeTrackState.core.getNotes();
14719
- drawNotes(notes, [r, g, b, 1], true);
15174
+ renderer.drawNotes(notes, [r, g, b, 1], true);
14720
15175
  lyricTargetNotes = notes;
14721
15176
  }
14722
15177
  if (activeToolMode === "select" && selectionRect) {
14723
- const ctx = getGridContext();
15178
+ const ctx = renderer.getGridContext();
14724
15179
  ctx.save();
14725
15180
  ctx.strokeStyle = "#ffec27";
14726
15181
  ctx.lineWidth = 2;
@@ -14743,19 +15198,19 @@ var mountDAW = (target, options = {}) => {
14743
15198
  if (activeToolMode === "select" && selectedNotes.length > 0) {
14744
15199
  const ids = new Set(selectedNotes.map((n) => n.id));
14745
15200
  const active = getActive();
14746
- drawSelectedNotes(active.core.getNotes(), ids, [
15201
+ renderer.drawSelectedNotes(active.core.getNotes(), ids, [
14747
15202
  ...active.config.color,
14748
15203
  1
14749
15204
  ]);
14750
15205
  }
14751
15206
  if (lyricTargetNotes)
14752
- drawNoteLyrics(lyricTargetNotes, getActiveLyricKana());
15207
+ renderer.drawNoteLyrics(lyricTargetNotes, getActiveLyricKana());
14753
15208
  drawStartLine();
14754
15209
  if (playbackState === "playing") drawPlayhead();
14755
15210
  updateScrollbars();
14756
15211
  };
14757
15212
  const updateScrollbars = () => {
14758
- const canvas = getGridCanvas();
15213
+ const canvas = renderer.getGridCanvas();
14759
15214
  const maxOffsetX = getMaxOffsetX();
14760
15215
  const sbW = refs.hScroll.clientWidth;
14761
15216
  if (maxOffsetX <= 0) {
@@ -14839,7 +15294,7 @@ var mountDAW = (target, options = {}) => {
14839
15294
  const x2 = clamp5(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
14840
15295
  const ratio = x2 / (rect.width - thumbW);
14841
15296
  currentOffsetX = clamp5(ratio * maxOffsetX, 0, maxOffsetX);
14842
- setDrawOffset(currentOffsetX, currentOffsetY);
15297
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14843
15298
  redrawAll();
14844
15299
  };
14845
15300
  const moveV = (clientY) => {
@@ -14850,7 +15305,7 @@ var mountDAW = (target, options = {}) => {
14850
15305
  const y = clamp5(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
14851
15306
  const ratio = y / (rect.height - thumbH);
14852
15307
  currentOffsetY = clamp5(ratio * maxOffset, 0, maxOffset);
14853
- setDrawOffset(currentOffsetX, currentOffsetY);
15308
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14854
15309
  redrawAll();
14855
15310
  };
14856
15311
  };
@@ -14889,8 +15344,8 @@ var mountDAW = (target, options = {}) => {
14889
15344
  const autoScrollTick = () => {
14890
15345
  autoScrollRAF = null;
14891
15346
  if (!isSelecting || !lastMoveEvent) return;
14892
- const canvas = getGridCanvas();
14893
- const { x: x2, y } = getGridPosition(lastMoveEvent);
15347
+ const canvas = renderer.getGridCanvas();
15348
+ const { x: x2, y } = renderer.getGridPosition(lastMoveEvent);
14894
15349
  const dx = computeEdgeSpeed(x2, canvas.width);
14895
15350
  const dy = computeEdgeSpeed(y, canvas.height);
14896
15351
  if (dx !== 0 || dy !== 0) {
@@ -14898,7 +15353,7 @@ var mountDAW = (target, options = {}) => {
14898
15353
  const maxOffsetY = getMaxOffsetY();
14899
15354
  currentOffsetX = clamp5(currentOffsetX + dx, 0, maxOffsetX);
14900
15355
  currentOffsetY = clamp5(currentOffsetY + dy, 0, maxOffsetY);
14901
- setDrawOffset(currentOffsetX, currentOffsetY);
15356
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14902
15357
  onPointerMove(lastMoveEvent);
14903
15358
  }
14904
15359
  if (isSelecting) {
@@ -14919,11 +15374,17 @@ var mountDAW = (target, options = {}) => {
14919
15374
  };
14920
15375
  const findActiveNoteAt = (x2, y, margin = 0) => {
14921
15376
  const active = getActive();
14922
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
14923
- const offset = getDrawOffset();
15377
+ const {
15378
+ stepWidth,
15379
+ keyHeight,
15380
+ keyCount,
15381
+ pitchRangeStart,
15382
+ unitsPerRow: upr = UNITS_PER_SEMITONE
15383
+ } = renderConfig;
15384
+ const offset = renderer.getDrawOffset();
14924
15385
  for (const note of active.core.getNotes()) {
14925
15386
  const logicalX = note.startStep * stepWidth;
14926
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
15387
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
14927
15388
  const logicalY = yIndex * keyHeight;
14928
15389
  const w = note.durationSteps * stepWidth;
14929
15390
  const renderX = logicalX - offset.x;
@@ -14936,7 +15397,7 @@ var mountDAW = (target, options = {}) => {
14936
15397
  const hasNoteAt = (step, pitch, excludeId) => {
14937
15398
  const active = getActive();
14938
15399
  return active.core.getNotes().some(
14939
- (n) => n.id !== excludeId && n.pitch === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
15400
+ (n) => n.id !== excludeId && n.pitchUnits === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
14940
15401
  );
14941
15402
  };
14942
15403
  const snapToGrid = (duration) => Math.max(
@@ -14947,7 +15408,7 @@ var mountDAW = (target, options = {}) => {
14947
15408
  const onGridPointerDown = (event) => {
14948
15409
  event.preventDefault();
14949
15410
  options.onResumeAudio?.();
14950
- const { x: x2, y, step, pitch } = getGridPosition(event);
15411
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(event);
14951
15412
  const active = getActive();
14952
15413
  if (activeToolMode === "eraser") {
14953
15414
  if (isActiveLocked()) return;
@@ -14962,7 +15423,7 @@ var mountDAW = (target, options = {}) => {
14962
15423
  selectedOriginal = selectedNotes.map((n) => ({
14963
15424
  id: n.id,
14964
15425
  startStep: n.startStep,
14965
- pitch: n.pitch
15426
+ pitch: n.pitchUnits
14966
15427
  }));
14967
15428
  isSelecting = true;
14968
15429
  dragMode = "move";
@@ -14981,7 +15442,7 @@ var mountDAW = (target, options = {}) => {
14981
15442
  {
14982
15443
  id: clicked.id,
14983
15444
  startStep: clicked.startStep,
14984
- pitch: clicked.pitch
15445
+ pitch: clicked.pitchUnits
14985
15446
  }
14986
15447
  ];
14987
15448
  isSelecting = true;
@@ -14999,9 +15460,9 @@ var mountDAW = (target, options = {}) => {
14999
15460
  hasDragged = false;
15000
15461
  const existing = findActiveNoteAt(x2, y, TOUCH_HIT_MARGIN);
15001
15462
  if (existing) {
15002
- playPreview(existing.pitch);
15463
+ playPreview(existing.pitchUnits);
15003
15464
  const { stepWidth } = renderConfig;
15004
- const offset = getDrawOffset();
15465
+ const offset = renderer.getDrawOffset();
15005
15466
  const renderX = existing.startStep * stepWidth - offset.x;
15006
15467
  const w = existing.durationSteps * stepWidth;
15007
15468
  if (x2 >= renderX + w - resizeHandleWidth && x2 <= renderX + w) {
@@ -15012,17 +15473,17 @@ var mountDAW = (target, options = {}) => {
15012
15473
  dragOffsetPitch: 0,
15013
15474
  startStep: existing.startStep,
15014
15475
  durationSteps: existing.durationSteps,
15015
- lastPreviewPitch: existing.pitch
15476
+ lastPreviewPitch: existing.pitchUnits
15016
15477
  };
15017
15478
  } else {
15018
15479
  dragState = {
15019
15480
  noteId: existing.id,
15020
15481
  mode: "move",
15021
15482
  dragOffsetStep: step - existing.startStep,
15022
- dragOffsetPitch: pitch - existing.pitch,
15483
+ dragOffsetPitch: pitch - existing.pitchUnits,
15023
15484
  startStep: existing.startStep,
15024
15485
  durationSteps: existing.durationSteps,
15025
- lastPreviewPitch: existing.pitch
15486
+ lastPreviewPitch: existing.pitchUnits
15026
15487
  };
15027
15488
  }
15028
15489
  suppressClick = true;
@@ -15033,14 +15494,14 @@ var mountDAW = (target, options = {}) => {
15033
15494
  const newStart = snappedStep;
15034
15495
  const newEnd = newStart + currentInsertLength;
15035
15496
  const overlapping = active.core.getNotes().some(
15036
- (n) => n.pitch === pitch && newStart < n.startStep + n.durationSteps && newEnd > n.startStep
15497
+ (n) => n.pitchUnits === pitch && newStart < n.startStep + n.durationSteps && newEnd > n.startStep
15037
15498
  );
15038
15499
  if (!overlapping) {
15039
15500
  active.core.addNote(snappedStep, pitch, {
15040
15501
  noteLengthSteps: currentInsertLength
15041
15502
  });
15042
15503
  playPreview(pitch);
15043
- const newNote = active.core.getNotes().find((n) => n.startStep === snappedStep && n.pitch === pitch);
15504
+ const newNote = active.core.getNotes().find((n) => n.startStep === snappedStep && n.pitchUnits === pitch);
15044
15505
  if (newNote) {
15045
15506
  dragState = {
15046
15507
  noteId: newNote.id,
@@ -15049,7 +15510,7 @@ var mountDAW = (target, options = {}) => {
15049
15510
  dragOffsetPitch: 0,
15050
15511
  startStep: newNote.startStep,
15051
15512
  durationSteps: newNote.durationSteps,
15052
- lastPreviewPitch: newNote.pitch
15513
+ lastPreviewPitch: newNote.pitchUnits
15053
15514
  };
15054
15515
  hasDragged = true;
15055
15516
  }
@@ -15060,12 +15521,12 @@ var mountDAW = (target, options = {}) => {
15060
15521
  const active = getActive();
15061
15522
  if (activeToolMode === "pen") {
15062
15523
  if (!dragState) return;
15063
- const { step, pitch } = getGridPosition(event);
15524
+ const { step, pitch } = renderer.getGridPosition(event);
15064
15525
  hasDragged = true;
15065
15526
  if (dragState.mode === "move") {
15066
15527
  const nextStart = step - dragState.dragOffsetStep;
15067
15528
  const snappedStart = Math.round(nextStart / snapGridSteps) * snapGridSteps;
15068
- const nextPitch = pitch - dragState.dragOffsetPitch;
15529
+ const nextPitch = units(pitch - dragState.dragOffsetPitch);
15069
15530
  if (hasNoteAt(snappedStart, nextPitch, dragState.noteId)) return;
15070
15531
  active.core.moveNote(dragState.noteId, snappedStart, nextPitch);
15071
15532
  if (nextPitch !== dragState.lastPreviewPitch) {
@@ -15084,7 +15545,7 @@ var mountDAW = (target, options = {}) => {
15084
15545
  }
15085
15546
  if (activeToolMode === "select" && isSelecting && selectionStart) {
15086
15547
  ensureAutoScroll(event);
15087
- const { x: x2, y, step, pitch } = getGridPosition(event);
15548
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(event);
15088
15549
  if (dragMode === "rect") {
15089
15550
  const rect = {
15090
15551
  x: Math.min(x2, selectionStart.x),
@@ -15093,11 +15554,17 @@ var mountDAW = (target, options = {}) => {
15093
15554
  height: Math.abs(y - selectionStart.y)
15094
15555
  };
15095
15556
  selectionRect = rect;
15096
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
15097
- const offset = getDrawOffset();
15557
+ const {
15558
+ stepWidth,
15559
+ keyHeight,
15560
+ keyCount,
15561
+ pitchRangeStart,
15562
+ unitsPerRow: upr = UNITS_PER_SEMITONE
15563
+ } = renderConfig;
15564
+ const offset = renderer.getDrawOffset();
15098
15565
  selectedNotes = active.core.getNotes().filter((note) => {
15099
15566
  const logicalX = note.startStep * stepWidth;
15100
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
15567
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
15101
15568
  const logicalY = yIndex * keyHeight;
15102
15569
  const nx = logicalX - offset.x;
15103
15570
  const ny = logicalY - offset.y;
@@ -15115,8 +15582,8 @@ var mountDAW = (target, options = {}) => {
15115
15582
  for (const note of selectedNotes) {
15116
15583
  const orig = selectedOriginal.find((o) => o.id === note.id);
15117
15584
  if (!orig) continue;
15118
- const newPitch = orig.pitch + deltaPitch;
15119
- if (newPitch >= 0 && newPitch < 128)
15585
+ const newPitch = units(orig.pitch + deltaPitch);
15586
+ if (newPitch >= PITCH_RANGE_START && newPitch <= PITCH_RANGE_END)
15120
15587
  active.core.moveNote(
15121
15588
  note.id,
15122
15589
  orig.startStep + snappedDelta,
@@ -15127,7 +15594,7 @@ var mountDAW = (target, options = {}) => {
15127
15594
  const grab = selectedNotes[0];
15128
15595
  const orig = selectedOriginal.find((o) => o.id === grab.id);
15129
15596
  if (orig) {
15130
- const newGrab = orig.pitch + deltaPitch;
15597
+ const newGrab = units(orig.pitch + deltaPitch);
15131
15598
  if (newGrab !== lastMultiPreviewPitch && newGrab >= 0 && newGrab < 128) {
15132
15599
  lastMultiPreviewPitch = newGrab;
15133
15600
  playPreview(newGrab);
@@ -15264,22 +15731,23 @@ var mountDAW = (target, options = {}) => {
15264
15731
  refs.bgOpacityRow.classList.add("dtm-hidden");
15265
15732
  }
15266
15733
  refs.bgRemoveBtn.classList.toggle("dtm-hidden", !blob2);
15267
- setBackgroundActive(!!blob2);
15734
+ renderer.setBackgroundActive(!!blob2);
15268
15735
  redrawAll();
15269
15736
  };
15270
15737
  const setupCanvas = () => {
15271
15738
  const w = refs.rollContainer.clientWidth || 800;
15272
15739
  const h = refs.rollContainer.clientHeight || 450;
15273
- init(refs.wrapper, w, h, renderConfig);
15274
- const gridCanvas = getGridCanvas();
15740
+ renderer?.destroy();
15741
+ renderer = createRenderer(refs.wrapper, w, h, renderConfig);
15742
+ const gridCanvas = renderer.getGridCanvas();
15275
15743
  gridCanvas.addEventListener("pointerdown", onGridPointerDown);
15276
15744
  gridCanvas.addEventListener("dblclick", (event) => {
15277
15745
  event.preventDefault();
15278
15746
  if (isActiveLocked()) return;
15279
- const { step, pitch } = getGridPosition(event);
15747
+ const { step, pitch } = renderer.getGridPosition(event);
15280
15748
  const active = getActive();
15281
15749
  const note = active.core.getNotes().find(
15282
- (n) => n.pitch === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
15750
+ (n) => n.pitchUnits === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
15283
15751
  );
15284
15752
  if (note) active.core.deleteNoteById(note.id);
15285
15753
  });
@@ -15297,7 +15765,7 @@ var mountDAW = (target, options = {}) => {
15297
15765
  0,
15298
15766
  getMaxOffsetX()
15299
15767
  );
15300
- setDrawOffset(currentOffsetX, currentOffsetY);
15768
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15301
15769
  redrawAll();
15302
15770
  },
15303
15771
  { passive: false }
@@ -15307,7 +15775,7 @@ var mountDAW = (target, options = {}) => {
15307
15775
  suppressClick = false;
15308
15776
  }
15309
15777
  });
15310
- const headerCanvas = getHeaderCanvas();
15778
+ const headerCanvas = renderer.getHeaderCanvas();
15311
15779
  headerCanvas.addEventListener("click", (event) => {
15312
15780
  if (playbackState === "playing") return;
15313
15781
  const rect = headerCanvas.getBoundingClientRect();
@@ -15323,11 +15791,11 @@ var mountDAW = (target, options = {}) => {
15323
15791
  }
15324
15792
  redrawAll();
15325
15793
  });
15326
- setDrawOffset(currentOffsetX, currentOffsetY);
15794
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15327
15795
  redrawAll();
15328
15796
  };
15329
15797
  const applyZoomX = () => {
15330
- const canvas = getGridCanvas();
15798
+ const canvas = renderer.getGridCanvas();
15331
15799
  const centerStep = (currentOffsetX + canvas.width / 2) / renderConfig.stepWidth;
15332
15800
  renderConfig.stepWidth = BASE_STEP_WIDTH * (zoomX * 2) / 100;
15333
15801
  refs.zoomXLabel.textContent = `${zoomX}%`;
@@ -15336,11 +15804,11 @@ var mountDAW = (target, options = {}) => {
15336
15804
  0,
15337
15805
  getMaxOffsetX()
15338
15806
  );
15339
- setDrawOffset(currentOffsetX, currentOffsetY);
15807
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15340
15808
  redrawAll();
15341
15809
  };
15342
15810
  const applyZoomY = () => {
15343
- const canvas = getGridCanvas();
15811
+ const canvas = renderer.getGridCanvas();
15344
15812
  const centerKey = (currentOffsetY + canvas.height / 2) / renderConfig.keyHeight;
15345
15813
  renderConfig.keyHeight = BASE_KEY_HEIGHT * zoomY / 100;
15346
15814
  refs.zoomYLabel.textContent = `${zoomY}%`;
@@ -15349,7 +15817,7 @@ var mountDAW = (target, options = {}) => {
15349
15817
  0,
15350
15818
  getMaxOffsetY()
15351
15819
  );
15352
- setDrawOffset(currentOffsetX, currentOffsetY);
15820
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15353
15821
  redrawAll();
15354
15822
  };
15355
15823
  const getViewState = () => ({
@@ -15361,7 +15829,14 @@ var mountDAW = (target, options = {}) => {
15361
15829
  const notifyViewState = () => options.onViewStateChange?.(getViewState());
15362
15830
  const dispatchNote = (trackId, pitch, trackVol, velocity, when, duration) => {
15363
15831
  const volume = trackVol / 100 * (velocity / 127) * (masterVolume / 100);
15364
- options.onPlayNote?.({ trackId, pitch, velocity, volume, when, duration });
15832
+ options.onPlayNote?.({
15833
+ trackId,
15834
+ pitchUnits: pitch,
15835
+ velocity,
15836
+ volume,
15837
+ when,
15838
+ duration
15839
+ });
15365
15840
  };
15366
15841
  const sequencer = createSequencer({
15367
15842
  getTracks: () => trackStates.map((t) => ({
@@ -15389,7 +15864,7 @@ var mountDAW = (target, options = {}) => {
15389
15864
  },
15390
15865
  onTick: (step) => {
15391
15866
  currentPlayStep = step;
15392
- const canvas = getGridCanvas();
15867
+ const canvas = renderer.getGridCanvas();
15393
15868
  const visibleSteps = canvas.width / renderConfig.stepWidth;
15394
15869
  const threshold = currentOffsetX / renderConfig.stepWidth + visibleSteps - 4;
15395
15870
  if (currentPlayStep > threshold) {
@@ -15399,7 +15874,7 @@ var mountDAW = (target, options = {}) => {
15399
15874
  0,
15400
15875
  getMaxOffsetX()
15401
15876
  );
15402
- setDrawOffset(currentOffsetX, currentOffsetY);
15877
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15403
15878
  }
15404
15879
  redrawAll();
15405
15880
  },
@@ -15430,7 +15905,7 @@ var mountDAW = (target, options = {}) => {
15430
15905
  (a, b) => a.startStep - b.startStep
15431
15906
  );
15432
15907
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
15433
- const semis = (lt.octave ?? 0) * 12;
15908
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
15434
15909
  const count = Math.min(sorted.length, lt.syllables.length);
15435
15910
  const notes = [];
15436
15911
  for (let i2 = 0; i2 < count; i2++) {
@@ -15438,7 +15913,7 @@ var mountDAW = (target, options = {}) => {
15438
15913
  if (n.startStep < fromStep) continue;
15439
15914
  notes.push({
15440
15915
  syllable: lt.syllables[i2],
15441
- pitch: n.pitch + semis,
15916
+ pitch: units(n.pitchUnits + semis),
15442
15917
  startSec: (n.startStep - fromStep) * secondsPerStep,
15443
15918
  durationSec: n.durationSteps * secondsPerStep * gate
15444
15919
  });
@@ -15481,13 +15956,13 @@ var mountDAW = (target, options = {}) => {
15481
15956
  }
15482
15957
  }
15483
15958
  if (playbackState !== "paused") {
15484
- const canvas = getGridCanvas();
15959
+ const canvas = renderer.getGridCanvas();
15485
15960
  currentOffsetX = clamp5(
15486
15961
  playStartStep * renderConfig.stepWidth - canvas.width * 0.5,
15487
15962
  0,
15488
15963
  getMaxOffsetX()
15489
15964
  );
15490
- setDrawOffset(currentOffsetX, currentOffsetY);
15965
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15491
15966
  }
15492
15967
  playbackState = "playing";
15493
15968
  sequencer.start(fromStep);
@@ -16474,6 +16949,7 @@ var mountDAW = (target, options = {}) => {
16474
16949
  fadeIn: Math.round(fadeInSec * 10),
16475
16950
  fadeOut: Math.round(fadeOutSec * 10),
16476
16951
  mode,
16952
+ edo: renderConfig.edo,
16477
16953
  trackInstruments: trackInstMeta,
16478
16954
  trackCompression: trackCompMeta,
16479
16955
  trackWidth: trackWidthMeta,
@@ -16502,6 +16978,7 @@ var mountDAW = (target, options = {}) => {
16502
16978
  fadeIn: Math.round(fadeInSec * 10),
16503
16979
  fadeOut: Math.round(fadeOutSec * 10),
16504
16980
  mode,
16981
+ edo: renderConfig.edo,
16505
16982
  trackInstruments: trackInstMeta,
16506
16983
  trackCompression: trackCompMeta,
16507
16984
  trackWidth: trackWidthMeta,
@@ -16641,19 +17118,19 @@ var mountDAW = (target, options = {}) => {
16641
17118
  }
16642
17119
  }
16643
17120
  if (candidateNotes.length === 0) return null;
16644
- const sum = candidateNotes.reduce((acc, note) => acc + note.pitch, 0);
16645
- return Math.round(sum / candidateNotes.length);
17121
+ const sum = candidateNotes.reduce((acc, note) => acc + note.pitchUnits, 0);
17122
+ return units(Math.round(sum / candidateNotes.length));
16646
17123
  };
16647
17124
  const centerPitch = (pitch) => {
16648
- const canvas = getGridCanvas();
16649
- const yIndex = renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart);
17125
+ const canvas = renderer.getGridCanvas();
17126
+ const yIndex = renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart) / (renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE);
16650
17127
  const logicalY = yIndex * renderConfig.keyHeight;
16651
17128
  currentOffsetY = clamp5(
16652
17129
  logicalY - (canvas.height - renderConfig.keyHeight) / 2,
16653
17130
  0,
16654
17131
  getMaxOffsetY()
16655
17132
  );
16656
- setDrawOffset(currentOffsetX, currentOffsetY);
17133
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
16657
17134
  };
16658
17135
  const clearAll = () => {
16659
17136
  for (const t of trackStates) {
@@ -16711,10 +17188,10 @@ var mountDAW = (target, options = {}) => {
16711
17188
  if (!meta.drumFont) applyDrumPatternFont(meta.drum);
16712
17189
  }
16713
17190
  if (meta.volume !== void 0) {
16714
- masterVolume = meta.volume;
16715
- refs.masterVolume.value = String(meta.volume);
16716
- refs.masterVolumeLabel.textContent = `${meta.volume}%`;
17191
+ applyMasterVolume(meta.volume);
16717
17192
  }
17193
+ applyEdo(meta.edo ?? 12);
17194
+ refs.edoSelect.value = String(renderConfig.edo ?? 12);
16718
17195
  if (meta.drumVolume !== void 0) {
16719
17196
  drumVolume = meta.drumVolume;
16720
17197
  refs.drumVolume.value = String(meta.drumVolume);
@@ -16879,7 +17356,7 @@ var mountDAW = (target, options = {}) => {
16879
17356
  if (applyActiveOnly && p.trackIndex !== activeTrackIndex) continue;
16880
17357
  const t = trackStates[p.trackIndex];
16881
17358
  if (!t) continue;
16882
- t.core.addNote(p.startStep, p.pitch, {
17359
+ t.core.addNote(p.startStep, p.pitchUnits, {
16883
17360
  noteLengthSteps: p.durationSteps,
16884
17361
  velocity: DEFAULT_VELOCITY
16885
17362
  });
@@ -16896,7 +17373,7 @@ var mountDAW = (target, options = {}) => {
16896
17373
  if (firstPitch !== null) {
16897
17374
  centerPitch(firstPitch);
16898
17375
  } else {
16899
- centerPitch(48);
17376
+ centerPitch(pitchV1ToUnits(48));
16900
17377
  }
16901
17378
  redrawAll();
16902
17379
  updateTrackPanel();
@@ -16914,6 +17391,8 @@ var mountDAW = (target, options = {}) => {
16914
17391
  const chordTrack = trackStates.find((t) => t.config.id === "chord");
16915
17392
  if (!chordTrack) return;
16916
17393
  const placements = buildChordPlacements({
17394
+ // 和音は五度連鎖経由で音律の格子へ写す(31平均律では長3度が10度になる)
17395
+ edo: renderConfig.edo,
16917
17396
  chordStr: active.savedChordInput,
16918
17397
  patternType: active.savedChordPattern,
16919
17398
  rootShift: active.savedChordRoot,
@@ -16923,7 +17402,7 @@ var mountDAW = (target, options = {}) => {
16923
17402
  chordTrack.core.clearNotesWithoutHistory();
16924
17403
  chordTrack.core.beginBatch();
16925
17404
  for (const p of placements) {
16926
- chordTrack.core.addNote(p.startStep, p.pitch, {
17405
+ chordTrack.core.addNote(p.startStep, p.pitchUnits, {
16927
17406
  noteLengthSteps: Math.max(1, p.durationSteps),
16928
17407
  velocity: p.velocity
16929
17408
  });
@@ -16987,7 +17466,7 @@ var mountDAW = (target, options = {}) => {
16987
17466
  if (applyActiveOnly && p.trackId !== activeTrackId) continue;
16988
17467
  const t = trackStates.find((ts) => ts.config.id === p.trackId);
16989
17468
  if (!t) continue;
16990
- t.core.addNote(p.startStep, p.pitch, {
17469
+ t.core.addNote(p.startStep, snapToEdoGrid(pitchV1ToUnits(p.pitch)), {
16991
17470
  noteLengthSteps: p.durationSteps,
16992
17471
  velocity: p.velocity
16993
17472
  });
@@ -17004,7 +17483,7 @@ var mountDAW = (target, options = {}) => {
17004
17483
  if (firstPitch !== null) {
17005
17484
  centerPitch(firstPitch);
17006
17485
  } else {
17007
- centerPitch(48);
17486
+ centerPitch(pitchV1ToUnits(48));
17008
17487
  }
17009
17488
  redrawAll();
17010
17489
  updateTrackPanel();
@@ -17068,18 +17547,21 @@ var mountDAW = (target, options = {}) => {
17068
17547
  }
17069
17548
  }, 30);
17070
17549
  };
17071
- const showConfirmModal = (message) => new Promise((resolve) => {
17550
+ const showConfirmModal = (message, opts) => new Promise((resolve) => {
17551
+ const title = opts?.title ?? "\u30E2\u30FC\u30C9\u306E\u78BA\u8A8D";
17552
+ const yes = opts?.yes ?? "\u306F\u3044\uFF08\u4E0A\u7D1A\u8005\u30E2\u30FC\u30C9\u306B\u5207\u308A\u66FF\u3048\u308B\uFF09";
17553
+ const no = opts?.no ?? "\u3044\u3044\u3048\uFF08\u3053\u306E\u307E\u307E\u8AAD\u307F\u8FBC\u3080\uFF09";
17072
17554
  const overlay = document.createElement("div");
17073
17555
  overlay.className = "dtm-modal-overlay";
17074
17556
  overlay.innerHTML = `
17075
17557
  <div class="dtm-modal">
17076
17558
  <div class="dtm-modal-header">
17077
- <span class="dtm-modal-title">\u30E2\u30FC\u30C9\u306E\u78BA\u8A8D</span>
17559
+ <span class="dtm-modal-title">${title}</span>
17078
17560
  </div>
17079
17561
  <div class="dtm-modal-body"><p>${message}</p></div>
17080
17562
  <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>
17563
+ <button class="dtm-btn dtm-btn--ghost dtm-confirm-no">${no}</button>
17564
+ <button class="dtm-btn dtm-btn--primary dtm-confirm-yes">${yes}</button>
17083
17565
  </div>
17084
17566
  </div>`;
17085
17567
  const close = (result) => {
@@ -17187,10 +17669,45 @@ var mountDAW = (target, options = {}) => {
17187
17669
  });
17188
17670
  refs.decomposeChordToggle.addEventListener("change", notifyViewState);
17189
17671
  refs.ignoreChordHeavyToggle.addEventListener("change", notifyViewState);
17672
+ refs.edoSelect.addEventListener("change", async () => {
17673
+ const next = Number.parseInt(refs.edoSelect.value, 10) === 31 ? 31 : 12;
17674
+ const prev = renderConfig.edo ?? 12;
17675
+ if (next === prev) return;
17676
+ const hasNotes = trackStates.some((t) => t.core.getNotes().length > 0);
17677
+ if (next === 12 && prev === 31 && hasNotes) {
17678
+ const okToDrop = await showConfirmModal(
17679
+ "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",
17680
+ {
17681
+ title: "\u97F3\u5F8B\u306E\u78BA\u8A8D",
17682
+ yes: "\u5207\u308A\u66FF\u3048\u308B",
17683
+ no: "\u3084\u3081\u308B"
17684
+ }
17685
+ );
17686
+ if (!okToDrop) {
17687
+ refs.edoSelect.value = String(prev);
17688
+ return;
17689
+ }
17690
+ }
17691
+ overlayDuring(() => {
17692
+ applyEdo(next, { snap: true });
17693
+ if (next === 31 && prev === 12) {
17694
+ zoomY = Math.max(50, Math.round(zoomY / 2 / 25) * 25);
17695
+ } else if (next === 12 && prev === 31) {
17696
+ zoomY = Math.min(200, Math.round(zoomY * 2 / 25) * 25);
17697
+ }
17698
+ applyZoomY();
17699
+ const first = getFirstDetectedPitch();
17700
+ centerPitch(first ?? pitchV1ToUnits(48));
17701
+ redrawAll();
17702
+ updateUndoRedo();
17703
+ notifyViewState();
17704
+ });
17705
+ });
17706
+ refs.edoInfoBtn.addEventListener("click", () => {
17707
+ showModal("\u97F3\u5F8B\u306E\u89E3\u8AAC", EDO_INFO_HTML);
17708
+ });
17190
17709
  refs.masterVolume.addEventListener("input", () => {
17191
- masterVolume = Number.parseInt(refs.masterVolume.value, 10) || 0;
17192
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17193
- options.singingVoices?.setVolume(masterVolume / 100);
17710
+ applyMasterVolume(Number.parseInt(refs.masterVolume.value, 10) || 0);
17194
17711
  });
17195
17712
  refs.masterComp.addEventListener("input", () => {
17196
17713
  masterCompression = Number.parseInt(refs.masterComp.value, 10) || 0;
@@ -17254,10 +17771,7 @@ var mountDAW = (target, options = {}) => {
17254
17771
  const scale = targetPeak / observedPeakMax;
17255
17772
  const suggested = clamp5(Math.round(masterVolume * scale), 10, 100);
17256
17773
  if (suggested !== masterVolume) {
17257
- masterVolume = suggested;
17258
- refs.masterVolume.value = String(masterVolume);
17259
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17260
- options.singingVoices?.setVolume(masterVolume / 100);
17774
+ applyMasterVolume(suggested);
17261
17775
  }
17262
17776
  observedPeakMax = 0;
17263
17777
  observedPlayMs = 0;
@@ -17417,7 +17931,8 @@ var mountDAW = (target, options = {}) => {
17417
17931
  generateRandomPattern(getActive().core, {
17418
17932
  stepsPerBar: renderConfig.stepsPerBar,
17419
17933
  startStep: playStartStep,
17420
- pitchRangeStart: renderConfig.pitchRangeStart
17934
+ pitchRangeStart: renderConfig.pitchRangeStart,
17935
+ edo: renderConfig.edo
17421
17936
  });
17422
17937
  redrawAll();
17423
17938
  });
@@ -17754,7 +18269,7 @@ var mountDAW = (target, options = {}) => {
17754
18269
  id: 0,
17755
18270
  startStep: p.startStep,
17756
18271
  durationSteps: p.durationSteps,
17757
- pitch: p.pitch,
18272
+ pitchUnits: snapToEdoGrid(pitchV1ToUnits(p.pitch)),
17758
18273
  velocity: p.velocity
17759
18274
  }));
17760
18275
  const mml = refCore.getMMLFromNotes(asNotes, tempo, 100).trim();
@@ -18209,10 +18724,10 @@ var mountDAW = (target, options = {}) => {
18209
18724
  const newStart = playStartStep + (note.startStep - minStart);
18210
18725
  const newEnd = newStart + note.durationSteps;
18211
18726
  const overlap = notes.some(
18212
- (ex) => ex.pitch === note.pitch && newStart < ex.startStep + ex.durationSteps && newEnd > ex.startStep
18727
+ (ex) => ex.pitchUnits === note.pitchUnits && newStart < ex.startStep + ex.durationSteps && newEnd > ex.startStep
18213
18728
  );
18214
18729
  if (!overlap)
18215
- core.addNote(newStart, note.pitch, {
18730
+ core.addNote(newStart, note.pitchUnits, {
18216
18731
  noteLengthSteps: note.durationSteps,
18217
18732
  velocity: note.velocity
18218
18733
  });
@@ -18285,13 +18800,13 @@ var mountDAW = (target, options = {}) => {
18285
18800
  pausedPlayStep = step;
18286
18801
  currentPlayStep = step;
18287
18802
  playbackState = "paused";
18288
- const canvas = getGridCanvas();
18803
+ const canvas = renderer.getGridCanvas();
18289
18804
  currentOffsetX = clamp5(
18290
18805
  step * renderConfig.stepWidth - canvas.width * 0.5,
18291
18806
  0,
18292
18807
  getMaxOffsetX()
18293
18808
  );
18294
- setDrawOffset(currentOffsetX, currentOffsetY);
18809
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
18295
18810
  updateTransport();
18296
18811
  redrawAll();
18297
18812
  };
@@ -18359,16 +18874,10 @@ var mountDAW = (target, options = {}) => {
18359
18874
  forcePauseAt,
18360
18875
  setLoading,
18361
18876
  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);
18877
+ applyMasterVolume(volume);
18366
18878
  },
18367
18879
  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);
18880
+ applyMasterVolume(volume);
18372
18881
  },
18373
18882
  setDrumVolume: (volume) => {
18374
18883
  drumVolume = clamp5(volume, 0, 100);
@@ -18411,15 +18920,19 @@ var mountDAW = (target, options = {}) => {
18411
18920
  suppressPatch = true;
18412
18921
  track.core.beginBatch();
18413
18922
  for (const n of added) {
18414
- const existing = track.core.getNotes().find((e) => e.startStep === n.startStep && e.pitch === n.pitch);
18923
+ const existing = track.core.getNotes().find(
18924
+ (e) => e.startStep === n.startStep && e.pitchUnits === n.pitchUnits
18925
+ );
18415
18926
  if (existing) track.core.deleteNoteById(existing.id);
18416
- track.core.addNote(n.startStep, n.pitch, {
18927
+ track.core.addNote(n.startStep, n.pitchUnits, {
18417
18928
  noteLengthSteps: n.durationSteps,
18418
18929
  velocity: n.velocity
18419
18930
  });
18420
18931
  }
18421
18932
  for (const r of removed) {
18422
- const note = track.core.getNotes().find((n) => n.startStep === r.startStep && n.pitch === r.pitch);
18933
+ const note = track.core.getNotes().find(
18934
+ (n) => n.startStep === r.startStep && n.pitchUnits === r.pitchUnits
18935
+ );
18423
18936
  if (note) track.core.deleteNoteById(note.id);
18424
18937
  }
18425
18938
  track.core.endBatch();
@@ -18461,9 +18974,9 @@ var mountDAW = (target, options = {}) => {
18461
18974
  if (t.config.id === activeTrackId) updateTrackPanel();
18462
18975
  },
18463
18976
  noteToCanvas: (step, pitch) => {
18464
- const canvas = getGridCanvas();
18977
+ const canvas = renderer.getGridCanvas();
18465
18978
  const x2 = step * renderConfig.stepWidth - currentOffsetX;
18466
- const y = (renderConfig.keyCount - 1 - pitch) * renderConfig.keyHeight - currentOffsetY;
18979
+ const y = (renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart) / (renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE)) * renderConfig.keyHeight - currentOffsetY;
18467
18980
  const onScreen = x2 >= 0 && x2 <= canvas.width && y >= 0 && y <= canvas.height;
18468
18981
  let side = null;
18469
18982
  if (!onScreen) {
@@ -18527,7 +19040,7 @@ var playSingingMML = async (mml, options = {}) => {
18527
19040
  id: id++,
18528
19041
  startStep: p.startStep,
18529
19042
  durationSteps: p.durationSteps,
18530
- pitch: p.pitch,
19043
+ pitchUnits: p.pitchUnits,
18531
19044
  velocity: p.velocity
18532
19045
  }));
18533
19046
  return {
@@ -18553,7 +19066,7 @@ var playSingingMML = async (mml, options = {}) => {
18553
19066
  (a, b) => a.startStep - b.startStep
18554
19067
  );
18555
19068
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
18556
- const semis = (lt.octave ?? 0) * 12;
19069
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
18557
19070
  const count = Math.min(sorted.length, lt.syllables.length);
18558
19071
  const notes = [];
18559
19072
  for (let i2 = 0; i2 < count; i2++) {
@@ -18561,7 +19074,7 @@ var playSingingMML = async (mml, options = {}) => {
18561
19074
  if (n.startStep < fromStep) continue;
18562
19075
  notes.push({
18563
19076
  syllable: lt.syllables[i2],
18564
- pitch: n.pitch + semis,
19077
+ pitch: units(n.pitchUnits + semis),
18565
19078
  startSec: (n.startStep - fromStep) * secondsPerStep,
18566
19079
  durationSec: n.durationSteps * secondsPerStep * gate
18567
19080
  });
@@ -18749,24 +19262,28 @@ var createPianoRoll = (options, handlers) => {
18749
19262
  config,
18750
19263
  noteLengthSteps = 1
18751
19264
  } = options;
18752
- init(mountTarget, width, height, config);
19265
+ const renderer = createRenderer(mountTarget, width, height, config);
18753
19266
  let currentNoteLengthSteps = noteLengthSteps;
18754
19267
  let selectionRect = null;
18755
19268
  let isSelecting = false;
18756
19269
  let selectionStart = null;
18757
19270
  let selectedNotes = [];
18758
19271
  let copiedNotes = [];
18759
- const core = new MMLCore({
18760
- onMMLGenerated: handlers.onMMLGenerated,
18761
- onNotesChanged: (notes) => {
18762
- handlers.onNotesChanged(notes);
18763
- }
18764
- });
19272
+ const core = new MMLCore(
19273
+ {
19274
+ onMMLGenerated: handlers.onMMLGenerated,
19275
+ onNotesChanged: (notes) => {
19276
+ handlers.onNotesChanged(notes);
19277
+ }
19278
+ },
19279
+ 80,
19280
+ () => config
19281
+ );
18765
19282
  const getAddNoteOptions = () => ({
18766
19283
  noteLengthSteps: currentNoteLengthSteps
18767
19284
  });
18768
19285
  let suppressClick = false;
18769
- onClick((step, pitch) => {
19286
+ renderer.onClick((step, pitch) => {
18770
19287
  if (suppressClick) {
18771
19288
  suppressClick = false;
18772
19289
  return;
@@ -18778,7 +19295,7 @@ var createPianoRoll = (options, handlers) => {
18778
19295
  } else if (mode === "eraser") {
18779
19296
  const notes = core.getNotes();
18780
19297
  const note = notes.find(
18781
- (n) => n.startStep <= step && step < n.startStep + n.durationSteps && n.pitch === pitch
19298
+ (n) => n.startStep <= step && step < n.startStep + n.durationSteps && n.pitchUnits === pitch
18782
19299
  );
18783
19300
  if (note) {
18784
19301
  core.deleteNoteById(note.id);
@@ -18786,17 +19303,23 @@ var createPianoRoll = (options, handlers) => {
18786
19303
  }
18787
19304
  }
18788
19305
  });
18789
- const gridCanvas = getGridCanvas();
19306
+ const gridCanvas = renderer.getGridCanvas();
18790
19307
  const resizeHandleWidth = 6;
18791
19308
  let dragState = null;
18792
19309
  let hasDragged = false;
18793
19310
  let lastPreviewPitch = null;
18794
19311
  const findNoteAtPosition = (x2, y) => {
18795
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18796
- const offset = getDrawOffset();
19312
+ const {
19313
+ stepWidth,
19314
+ keyHeight,
19315
+ keyCount,
19316
+ pitchRangeStart,
19317
+ unitsPerRow: upr = UNITS_PER_SEMITONE
19318
+ } = renderer.getRenderConfig();
19319
+ const offset = renderer.getDrawOffset();
18797
19320
  for (const note of core.getNotes()) {
18798
19321
  const logicalX = note.startStep * stepWidth;
18799
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19322
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18800
19323
  const logicalY = yIndex * keyHeight;
18801
19324
  const w = note.durationSteps * stepWidth;
18802
19325
  const h = keyHeight;
@@ -18810,7 +19333,7 @@ var createPianoRoll = (options, handlers) => {
18810
19333
  };
18811
19334
  const handlePointerMove = (e) => {
18812
19335
  if (core.getToolMode() === "select" && isSelecting && selectionStart) {
18813
- const { x: x2, y } = getGridPosition(e);
19336
+ const { x: x2, y } = renderer.getGridPosition(e);
18814
19337
  const minX = Math.min(x2, selectionStart.x);
18815
19338
  const minY = Math.min(y, selectionStart.y);
18816
19339
  const width2 = Math.abs(x2 - selectionStart.x);
@@ -18822,7 +19345,7 @@ var createPianoRoll = (options, handlers) => {
18822
19345
  }
18823
19346
  if (!dragState) return;
18824
19347
  hasDragged = true;
18825
- const { step, pitch } = getGridPosition(e);
19348
+ const { step, pitch } = renderer.getGridPosition(e);
18826
19349
  if (dragState.mode === "move") {
18827
19350
  if (dragState.selectedNotes && dragState.selectedNotes.length > 0) {
18828
19351
  const noteId = dragState.noteId;
@@ -18831,10 +19354,10 @@ var createPianoRoll = (options, handlers) => {
18831
19354
  const nextStart2 = step - dragState.dragOffsetStep;
18832
19355
  const nextPitch2 = pitch - dragState.dragOffsetPitch;
18833
19356
  const stepDelta = nextStart2 - baseNote.startStep;
18834
- const pitchDelta = nextPitch2 - baseNote.pitch;
19357
+ const pitchDelta = nextPitch2 - baseNote.pitchUnits;
18835
19358
  for (const note of dragState.selectedNotes) {
18836
19359
  const newStart = note.startStep + stepDelta;
18837
- const newPitch = note.pitch + pitchDelta;
19360
+ const newPitch = units(note.pitchUnits + pitchDelta);
18838
19361
  core.moveNote(note.id, newStart, newPitch);
18839
19362
  }
18840
19363
  if (options.onPreviewSound && pitch !== lastPreviewPitch) {
@@ -18845,7 +19368,7 @@ var createPianoRoll = (options, handlers) => {
18845
19368
  return;
18846
19369
  }
18847
19370
  const nextStart = step - dragState.dragOffsetStep;
18848
- const nextPitch = pitch - dragState.dragOffsetPitch;
19371
+ const nextPitch = units(pitch - dragState.dragOffsetPitch);
18849
19372
  core.moveNote(dragState.noteId, nextStart, nextPitch);
18850
19373
  return;
18851
19374
  }
@@ -18875,7 +19398,7 @@ var createPianoRoll = (options, handlers) => {
18875
19398
  }
18876
19399
  };
18877
19400
  gridCanvas.addEventListener("pointerdown", (e) => {
18878
- const { x: x2, y, step, pitch } = getGridPosition(e);
19401
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(e);
18879
19402
  const currentMode = core.getToolMode();
18880
19403
  if (currentMode === "select") {
18881
19404
  const clickedNote = findNoteAtPosition(x2, y);
@@ -18886,7 +19409,7 @@ var createPianoRoll = (options, handlers) => {
18886
19409
  noteId: clickedNote.id,
18887
19410
  mode: "move",
18888
19411
  dragOffsetStep: step - clickedNote.startStep,
18889
- dragOffsetPitch: pitch - clickedNote.pitch,
19412
+ dragOffsetPitch: pitch - clickedNote.pitchUnits,
18890
19413
  startStep: clickedNote.startStep,
18891
19414
  selectedNotes: notesInRect
18892
19415
  // 複数選択ノートを保存
@@ -18904,10 +19427,16 @@ var createPianoRoll = (options, handlers) => {
18904
19427
  }
18905
19428
  const note = findNoteAtPosition(x2, y);
18906
19429
  if (!note) return;
18907
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18908
- const offset = getDrawOffset();
19430
+ const {
19431
+ stepWidth,
19432
+ keyHeight,
19433
+ keyCount,
19434
+ pitchRangeStart,
19435
+ unitsPerRow: upr = UNITS_PER_SEMITONE
19436
+ } = renderer.getRenderConfig();
19437
+ const offset = renderer.getDrawOffset();
18909
19438
  const logicalX = note.startStep * stepWidth;
18910
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19439
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18911
19440
  const logicalY = yIndex * keyHeight;
18912
19441
  const renderX = logicalX - offset.x;
18913
19442
  const renderY = logicalY - offset.y;
@@ -18926,7 +19455,7 @@ var createPianoRoll = (options, handlers) => {
18926
19455
  noteId: note.id,
18927
19456
  mode: "move",
18928
19457
  dragOffsetStep: step - note.startStep,
18929
- dragOffsetPitch: pitch - note.pitch,
19458
+ dragOffsetPitch: pitch - note.pitchUnits,
18930
19459
  startStep: note.startStep
18931
19460
  };
18932
19461
  });
@@ -18937,41 +19466,47 @@ var createPianoRoll = (options, handlers) => {
18937
19466
  "wheel",
18938
19467
  (e) => {
18939
19468
  e.preventDefault();
18940
- const configValues = getRenderConfig();
19469
+ const configValues = renderer.getRenderConfig();
18941
19470
  const gridHeight = gridCanvas.height;
18942
19471
  const maxOffsetY = Math.max(
18943
19472
  0,
18944
19473
  configValues.keyCount * configValues.keyHeight - gridHeight
18945
19474
  );
18946
- const currentOffset = getDrawOffset();
19475
+ const currentOffset = renderer.getDrawOffset();
18947
19476
  const nextOffsetY = Math.min(
18948
19477
  Math.max(currentOffset.y + e.deltaY, 0),
18949
19478
  maxOffsetY
18950
19479
  );
18951
- setDrawOffset(currentOffset.x, nextOffsetY);
18952
- drawGrid();
18953
- drawNotes(core.getNotes());
19480
+ renderer.setDrawOffset(currentOffset.x, nextOffsetY);
19481
+ renderer.drawGrid();
19482
+ renderer.drawNotes(core.getNotes());
18954
19483
  },
18955
19484
  { passive: false }
18956
19485
  );
18957
19486
  const redraw = () => {
18958
- drawGrid();
18959
- drawNotes(core.getNotes());
19487
+ renderer.drawGrid();
19488
+ renderer.drawNotes(core.getNotes());
18960
19489
  if (core.getToolMode() === "select") {
18961
- drawSelectionRect(selectionRect);
19490
+ renderer.drawSelectionRect(selectionRect);
18962
19491
  if (selectedNotes.length > 0) {
18963
19492
  const selectedIds = new Set(selectedNotes.map((n) => n.id));
18964
- drawSelectedNotes(core.getNotes(), selectedIds);
19493
+ renderer.drawSelectedNotes(core.getNotes(), selectedIds);
18965
19494
  }
18966
19495
  }
18967
19496
  };
18968
19497
  const getNotesInRect = (rect) => {
18969
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18970
- const offset = getDrawOffset();
19498
+ const {
19499
+ stepWidth,
19500
+ keyHeight,
19501
+ keyCount,
19502
+ pitchRangeStart,
19503
+ unitsPerRow: upr = UNITS_PER_SEMITONE
19504
+ } = renderer.getRenderConfig();
19505
+ const offset = renderer.getDrawOffset();
18971
19506
  const notes = [];
18972
19507
  for (const note of core.getNotes()) {
18973
19508
  const logicalX = note.startStep * stepWidth;
18974
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19509
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18975
19510
  const logicalY = yIndex * keyHeight;
18976
19511
  const noteRect = {
18977
19512
  x: logicalX - offset.x,
@@ -19018,7 +19553,7 @@ var createPianoRoll = (options, handlers) => {
19018
19553
  const minStart = Math.min(...copiedNotes.map((n) => n.startStep));
19019
19554
  copiedNotes.forEach((note) => {
19020
19555
  const newStep = startStep + (note.startStep - minStart);
19021
- core.addNote(newStep, note.pitch, {
19556
+ core.addNote(newStep, note.pitchUnits, {
19022
19557
  noteLengthSteps: note.durationSteps,
19023
19558
  velocity: note.velocity
19024
19559
  });
@@ -19860,10 +20395,12 @@ var createDtmStudio = async (options = {}) => {
19860
20395
  );
19861
20396
  }
19862
20397
  if (!sfInst) return;
20398
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
19863
20399
  sfInst.play({
19864
20400
  ctx: audioCtx,
19865
20401
  destination: getChannelStrip(e.trackId).input,
19866
- pitch: e.pitch,
20402
+ pitch: midi,
20403
+ detuneCents,
19867
20404
  volume: e.volume,
19868
20405
  velocity: e.velocity,
19869
20406
  when: e.when,
@@ -20179,10 +20716,12 @@ var createDtmStudio = async (options = {}) => {
20179
20716
  );
20180
20717
  }
20181
20718
  if (!sfInst) return;
20719
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20182
20720
  sfInst.play({
20183
20721
  ctx: audioCtx,
20184
20722
  destination: getChannelStrip(e.trackId).input,
20185
- pitch: e.pitch,
20723
+ pitch: midi,
20724
+ detuneCents,
20186
20725
  volume: e.volume,
20187
20726
  velocity: e.velocity,
20188
20727
  when: e.when,
@@ -20268,10 +20807,12 @@ var createDtmStudio = async (options = {}) => {
20268
20807
  );
20269
20808
  }
20270
20809
  if (!sfInst) return;
20810
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20271
20811
  sfInst.play({
20272
20812
  ctx: audioCtx,
20273
20813
  destination: getChannelStrip(e.trackId).input,
20274
- pitch: e.pitch,
20814
+ pitch: midi,
20815
+ detuneCents,
20275
20816
  volume: e.volume,
20276
20817
  velocity: e.velocity,
20277
20818
  when: e.when,
@@ -20350,10 +20891,12 @@ var createDtmStudio = async (options = {}) => {
20350
20891
  );
20351
20892
  }
20352
20893
  if (!sfInst) return;
20894
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20353
20895
  sfInst.play({
20354
20896
  ctx: audioCtx,
20355
20897
  destination: getChannelStrip(e.trackId).input,
20356
- pitch: e.pitch,
20898
+ pitch: midi,
20899
+ detuneCents,
20357
20900
  volume: e.volume,
20358
20901
  velocity: e.velocity,
20359
20902
  when: e.when,
@@ -20390,10 +20933,12 @@ var createDtmStudio = async (options = {}) => {
20390
20933
  sfInst = resolveSoundFont(defaultPreset, role, "simple");
20391
20934
  }
20392
20935
  if (!sfInst) return;
20936
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20393
20937
  sfInst.play({
20394
20938
  ctx: audioCtx,
20395
20939
  destination: getChannelStrip(e.trackId).input,
20396
- pitch: e.pitch,
20940
+ pitch: midi,
20941
+ detuneCents,
20397
20942
  volume: e.volume,
20398
20943
  velocity: e.velocity,
20399
20944
  when: e.when,
@@ -20416,10 +20961,12 @@ var createDtmStudio = async (options = {}) => {
20416
20961
  if (audioCtx.state === "suspended") {
20417
20962
  await audioCtx.resume();
20418
20963
  }
20964
+ const { midi, detuneCents } = unitsToMidiDetune(options2.pitchUnits);
20419
20965
  sfInst.play({
20420
20966
  ctx: audioCtx,
20421
20967
  destination: masterGain,
20422
- pitch: options2.pitch,
20968
+ pitch: midi,
20969
+ detuneCents,
20423
20970
  volume: vol / 100,
20424
20971
  when: 0,
20425
20972
  duration: dur
@@ -20439,7 +20986,7 @@ var createDtmStudio = async (options = {}) => {
20439
20986
  // 伴奏トラック
20440
20987
  startStep: p.startStep,
20441
20988
  durationSteps: p.durationSteps,
20442
- pitch: p.pitch,
20989
+ pitchUnits: p.pitchUnits,
20443
20990
  velocity: p.velocity
20444
20991
  }));
20445
20992
  const playerPreset = defaultPreset;
@@ -20447,10 +20994,12 @@ var createDtmStudio = async (options = {}) => {
20447
20994
  const playPlayerNote = (e) => {
20448
20995
  const sfInst = resolveSoundFont(playerPreset, "chord");
20449
20996
  if (!sfInst) return;
20997
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20450
20998
  sfInst.play({
20451
20999
  ctx: audioCtx,
20452
21000
  destination: getChannelStrip("chord").input,
20453
- pitch: e.pitch,
21001
+ pitch: midi,
21002
+ detuneCents,
20454
21003
  volume: e.volume,
20455
21004
  velocity: e.velocity,
20456
21005
  when: e.when,
@@ -20589,8 +21138,12 @@ var createDtmStudio = async (options = {}) => {
20589
21138
  };
20590
21139
  // Annotate the CommonJS export names for ESM import in node:
20591
21140
  0 && (module.exports = {
21141
+ A4_HZ,
21142
+ A4_UNITS,
21143
+ CENTS_PER_UNIT,
20592
21144
  DAW_CSS,
20593
21145
  DEFAULT_BPM,
21146
+ DEFAULT_EDO,
20594
21147
  DEFAULT_GATE,
20595
21148
  DEFAULT_PAN,
20596
21149
  DEFAULT_PLAYBACK_VELOCITY,
@@ -20600,30 +21153,41 @@ var createDtmStudio = async (options = {}) => {
20600
21153
  DRUM_FONT,
20601
21154
  DRUM_KEYS,
20602
21155
  DRUM_PATTERNS,
21156
+ EDO31_NAMES,
20603
21157
  GM_INSTRUMENT_NAMES,
20604
21158
  INSTRUMENT_PRESETS,
21159
+ KEY_COUNT,
20605
21160
  KOE_BASE_URL,
20606
21161
  KOE_VOICEBANKS,
20607
21162
  KOE_VOICEBANK_LABELS,
20608
21163
  KOE_VOICEBANK_TERMS,
20609
21164
  LinkedList,
20610
21165
  MAX_VOCAL_VOLUME,
21166
+ MICRO_STEP,
20611
21167
  MMLCore,
20612
21168
  MML_END_MARKER,
20613
21169
  MidiSearchClient,
21170
+ PITCH_ENCODING_VERSION,
20614
21171
  PITCH_MAP,
21172
+ PITCH_RANGE_END,
21173
+ PITCH_RANGE_START,
20615
21174
  PREWARM_NOTES,
20616
21175
  TRACKS_ADVANCED,
20617
21176
  TRACKS_SIMPLE,
21177
+ UNITS_PER_EDO31_DEGREE,
21178
+ UNITS_PER_OCTAVE,
21179
+ UNITS_PER_SEMITONE,
20618
21180
  VIBRATO_MIN_SEC,
20619
21181
  VOICE_IMAGES,
20620
21182
  VOICE_IMAGE_KEY,
21183
+ addUnits,
20621
21184
  analyzeMidiTracks,
20622
21185
  applyHarmonicFilter,
20623
21186
  applyMonophonic,
20624
21187
  buildChordPlacements,
20625
21188
  buildDrumPatternJson,
20626
21189
  buildNameToKeyMapping,
21190
+ chromaticStep,
20627
21191
  collectPitchTokens,
20628
21192
  concatFloat32,
20629
21193
  createAudioContext,
@@ -20632,19 +21196,13 @@ var createDtmStudio = async (options = {}) => {
20632
21196
  createKoeVoice,
20633
21197
  createLyricsConductor,
20634
21198
  createPianoRoll,
21199
+ createRenderer,
20635
21200
  createSequencer,
20636
21201
  createSingingVoices,
20637
21202
  createSynth,
20638
21203
  createVoiceRegistry,
20639
21204
  decodeMml,
20640
21205
  decomposeToMonophonic,
20641
- drawGrid,
20642
- drawHeader,
20643
- drawKeyboard,
20644
- drawNoteLyrics,
20645
- drawNotes,
20646
- drawSelectedNotes,
20647
- drawSelectionRect,
20648
21206
  encodeMml,
20649
21207
  encodeWavPCM16,
20650
21208
  exportMIDI,
@@ -20652,36 +21210,35 @@ var createDtmStudio = async (options = {}) => {
20652
21210
  extractMidiDrumPattern,
20653
21211
  extractMidiPlacements,
20654
21212
  extractMidiPlacementsByTrack,
21213
+ fifthToStep,
21214
+ fifthToUnits,
20655
21215
  formatMmlMeta,
20656
21216
  freqFromPitch,
20657
21217
  generateRandomPattern,
20658
- getDrawOffset,
20659
21218
  getDrumPatternKeys,
20660
- getGridCanvas,
20661
- getGridContext,
20662
- getGridPosition,
20663
- getHeaderCanvas,
20664
21219
  getMidiBPM,
20665
- getRenderConfig,
20666
- getXY,
20667
21220
  icon,
20668
- init,
20669
21221
  injectStyles,
20670
21222
  isChordHeavyTrack,
21223
+ isNaturalLetter,
20671
21224
  isPlausibleMidiTranscription,
20672
21225
  isValidHttpUrl,
21226
+ keyCountFor,
20673
21227
  koeUrl,
21228
+ midiNote,
21229
+ midiToUnits,
20674
21230
  mountChordPlayer,
20675
21231
  mountDAW,
20676
21232
  mountMmlPlayer,
21233
+ naturalStep,
20677
21234
  normalizeDrumPatterns,
20678
21235
  normalizeLyrics,
20679
- onClick,
20680
21236
  panToStereo,
20681
21237
  parseCustomVocals,
20682
21238
  parseLyrics,
20683
21239
  parseMML,
20684
21240
  parseMmlMeta,
21241
+ pitchV1ToUnits,
20685
21242
  playChords,
20686
21243
  playMML,
20687
21244
  playNote,
@@ -20689,13 +21246,19 @@ var createDtmStudio = async (options = {}) => {
20689
21246
  playSingingMML,
20690
21247
  resolveDrumPattern,
20691
21248
  resolveLoopPoint,
20692
- setBackgroundActive,
20693
- setDrawOffset,
20694
21249
  shiftNotes,
20695
21250
  showLoadingOverlay,
21251
+ spellingToUnits,
20696
21252
  stripCustomVocals,
20697
21253
  stripLyrics,
20698
21254
  stripMmlMeta,
20699
21255
  transposeNotes,
21256
+ units,
21257
+ unitsPerRow,
21258
+ unitsPerStep,
21259
+ unitsToHz,
21260
+ unitsToMidi,
21261
+ unitsToMidiDetune,
21262
+ unitsToPitchV1,
20700
21263
  vocalVolumeToGain
20701
21264
  });