@onjmin/dtm 1.0.7 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -148,7 +148,7 @@ async function buildNameToKeyMapping() {
148
148
  }
149
149
  var GM_INSTRUMENT_NAMES = FONT_NAME_SURIKOV.trim().split("\n").map((line) => line.slice(line.indexOf(" ") + 1));
150
150
 
151
- // node_modules/.pnpm/@onjmin+chord-parser@1.0.4/node_modules/@onjmin/chord-parser/dist/index.mjs
151
+ // node_modules/.pnpm/@onjmin+chord-parser@1.1.1/node_modules/@onjmin/chord-parser/dist/index.mjs
152
152
  var SHARP_NAMES = [
153
153
  "C",
154
154
  "C#",
@@ -179,6 +179,10 @@ var FLAT_NAMES = [
179
179
  ];
180
180
  var toPitchClass = (n) => (n % 12 + 12) % 12;
181
181
  var noteName = (pc, flat = false) => (flat ? FLAT_NAMES : SHARP_NAMES)[toPitchClass(pc)];
182
+ var LETTER_FIFTH = [0, 2, 4, -1, 1, 3, 5];
183
+ var DEGREE_FIFTH = [0, 2, 4, -1, 1, 3, 5];
184
+ var deg2fifth = (deg) => DEGREE_FIFTH[(deg - 1) % 7];
185
+ var HALF_FIFTH = 7;
182
186
  var SyntaxErrorWithPos = class extends Error {
183
187
  constructor(input, msg) {
184
188
  super(
@@ -225,6 +229,16 @@ var Input = class _Input {
225
229
  };
226
230
  var Output = class {
227
231
  pitch = null;
232
+ /** ルート音の五度圏インデックス。`pitch` と対で持つ。 */
233
+ rootFifth = 0;
234
+ /**
235
+ * 構成音の五度圏オフセット。キーは「ルートからの相対半音 mod 12」。
236
+ *
237
+ * 五度圏インデックスはオクターブ不変なので、分数コードのオクターブシフト
238
+ * (`value` の配列を ±12 する処理)の影響を受けない。そのため構成音の集合とは
239
+ * 別にこの対応表を持っておけば、配列操作側に手を入れずに綴りを保てる。
240
+ */
241
+ relFifths = /* @__PURE__ */ new Map();
228
242
  chord = null;
229
243
  isChord = false;
230
244
  pending = null;
@@ -308,9 +322,17 @@ var parseFormula = (input, output = new Output(), nest = 0) => {
308
322
  case DIVIDE: {
309
323
  const o = parseFormula(input, new Output(), nest);
310
324
  const v = [...output.value];
325
+ const rebase = (semi, fifth) => noteFifth(
326
+ output,
327
+ semi - output.pitch,
328
+ fifth - output.rootFifth
329
+ );
311
330
  if (o.isChord) {
331
+ for (const [rel, f] of o.relFifths)
332
+ rebase(o.pitch + rel, o.rootFifth + f);
312
333
  output.value = [...o.value].concat(v);
313
334
  } else {
335
+ rebase(o.pitch, o.rootFifth);
314
336
  const a = v.sort((x2, y) => x2 - y);
315
337
  const pitch = (o.pitch + 3) % 12 - 3;
316
338
  if (a[0] < pitch) {
@@ -357,63 +379,91 @@ var parsePitch = (input, output) => {
357
379
  const pitch = pitchMatcher.parse(input);
358
380
  if (pitch === null) err(input, "Not found pitch");
359
381
  output.pitch = pitch;
382
+ output.rootFifth = LETTER_FIFTH[idx2pitch.slice(0, 7).indexOf(pitch)];
360
383
  for (let i2 = 0; i2 < 2; i2++) {
361
384
  const half = parseHalf(input, true);
362
385
  if (half === null) break;
363
386
  output.pitch += half;
387
+ output.rootFifth += half * HALF_FIFTH;
364
388
  }
365
389
  return parseBase(input, output);
366
390
  };
391
+ var noteFifth = (out, semi, fifth) => {
392
+ out.relFifths.set((semi % 12 + 12) % 12, fifth);
393
+ };
367
394
  var MAJOR = [0, 4, 7];
368
395
  var DIM = [0, 3, 6];
396
+ var BASE_FIFTHS = /* @__PURE__ */ new Map();
397
+ var MINOR = [0, 3, 7];
398
+ var AUG = [0, 4, 8];
399
+ var HALF_DIM = [0, 3, 6, 10];
400
+ BASE_FIFTHS.set(MAJOR, [0, 4, 1]);
401
+ BASE_FIFTHS.set(MINOR, [0, -3, 1]);
402
+ BASE_FIFTHS.set(DIM, [0, -3, -6]);
403
+ BASE_FIFTHS.set(AUG, [0, 4, 8]);
404
+ BASE_FIFTHS.set(HALF_DIM, [0, -3, -6, -2]);
369
405
  var baseMatcher = new Matcher();
370
- baseMatcher.set(["m", "min", "Min", "minor", "Minor", "-"], [0, 3, 7]);
406
+ baseMatcher.set(["m", "min", "Min", "minor", "Minor", "-"], MINOR);
371
407
  baseMatcher.set(["dim", "\u3007"], DIM);
372
- baseMatcher.set("+", [0, 4, 8]);
373
- baseMatcher.set(["\u03A6", "\u03C6", "\xF8"], [0, 3, 6, 10]);
408
+ baseMatcher.set("+", AUG);
409
+ baseMatcher.set(["\u03A6", "\u03C6", "\xF8"], HALF_DIM);
374
410
  var parseBase = (input, output) => {
375
411
  const isMajMarker = /^maj/i.test(input.str.slice(input.idx));
376
412
  const res = isMajMarker ? null : baseMatcher.parse(input);
377
413
  if (res !== null) output.isChord = true;
378
- output.chord = new Set(res || MAJOR);
414
+ const base = res || MAJOR;
415
+ output.chord = new Set(base);
416
+ const baseFifths = BASE_FIFTHS.get(base);
417
+ if (baseFifths)
418
+ for (const [i2, semi] of base.entries())
419
+ noteFifth(output, semi, baseFifths[i2]);
379
420
  if (res === DIM) {
380
421
  const { num } = input;
381
422
  const chord = output.chord;
382
- if (num !== null) chord.add(deg2pitch(num) - 2);
423
+ if (num !== null) {
424
+ chord.add(deg2pitch(num) - 2);
425
+ noteFifth(output, deg2pitch(num) - 2, deg2fifth(num) - 2 * HALF_FIFTH);
426
+ }
383
427
  }
384
428
  output.nest = input.nest;
385
429
  return parseTerm(input, output);
386
430
  };
387
- var add = (chord, n, half) => {
388
- chord.add(deg2pitch(n) + half);
431
+ var put = (chord, out, deg, half = 0) => {
432
+ chord.add(deg2pitch(deg) + half);
433
+ noteFifth(out, deg2pitch(deg) + half, deg2fifth(deg) + half * HALF_FIFTH);
389
434
  };
390
- var aug = (chord) => {
435
+ var add = (chord, n, half, out) => {
436
+ put(chord, out, n, half);
437
+ };
438
+ var aug = (chord, out) => {
391
439
  chord.delete(deg2pitch(5));
392
- chord.add(deg2pitch(5) + 1);
440
+ put(chord, out, 5, 1);
393
441
  };
394
- var _7th = (chord, n, _half2, isFlat = false) => {
442
+ var _7th = (chord, n, _half2, out, isFlat = false) => {
395
443
  if (n === 5) chord.delete(deg2pitch(3));
396
- else if (n === 6) chord.add(deg2pitch(6));
397
- else if (n === 69) chord.add(deg2pitch(6)).add(deg2pitch(9));
398
- else {
399
- if (n >= 7) chord.add(deg2pitch(7) + (isFlat ? -1 : 0));
400
- if (n >= 9) chord.add(deg2pitch(9));
401
- if (n >= 11) chord.add(deg2pitch(11));
402
- if (n >= 13) chord.add(deg2pitch(13));
444
+ else if (n === 6) put(chord, out, 6);
445
+ else if (n === 69) {
446
+ put(chord, out, 6);
447
+ put(chord, out, 9);
448
+ } else {
449
+ if (n >= 7) put(chord, out, 7, isFlat ? -1 : 0);
450
+ if (n >= 9) put(chord, out, 9);
451
+ if (n >= 11) put(chord, out, 11);
452
+ if (n >= 13) put(chord, out, 13);
403
453
  }
404
454
  };
405
- var _half = (chord, n, half) => {
455
+ var _half = (chord, n, half, out) => {
406
456
  chord.delete(deg2pitch(n));
407
- chord.add(deg2pitch(n) + half);
457
+ put(chord, out, n, half);
408
458
  };
409
459
  var funcMatcher = new Matcher();
410
460
  funcMatcher.set("add", add);
411
461
  funcMatcher.set(["omit", "no"], (chord, n, half) => {
412
462
  chord.delete(deg2pitch(n) + half);
413
463
  });
414
- funcMatcher.set("sus", (chord, n, half) => {
464
+ funcMatcher.set("sus", (chord, n, half, out) => {
415
465
  chord.delete(deg2pitch(3));
416
- chord.add(deg2pitch(n) + half);
466
+ put(chord, out, n, half);
417
467
  });
418
468
  funcMatcher.set(
419
469
  ["M", "maj", "Maj", "major", "Major", "\u25B3", "\u0394"],
@@ -429,17 +479,18 @@ var parseFunc = (input, output) => {
429
479
  const half = parseHalf(input);
430
480
  const { num } = input;
431
481
  if (num === null) {
432
- if (isAug) aug(chord);
482
+ if (isAug) aug(chord, output);
433
483
  else err(input, "Not found number");
434
484
  }
435
485
  if (half === null) {
436
- if (input.nest === output.nest) _7th(chord, num, 0, true);
437
- else add(chord, num, 0);
486
+ if (input.nest === output.nest)
487
+ _7th(chord, num, 0, output, true);
488
+ else add(chord, num, 0, output);
438
489
  } else {
439
- _half(chord, num, half);
490
+ _half(chord, num, half, output);
440
491
  }
441
492
  } else if (func === aug) {
442
- aug(chord);
493
+ aug(chord, output);
443
494
  } else {
444
495
  output.pending = func;
445
496
  }
@@ -453,7 +504,8 @@ var parsePending = (input, output) => {
453
504
  pending(
454
505
  chord,
455
506
  num,
456
- half === null ? 0 : half
507
+ half === null ? 0 : half,
508
+ output
457
509
  );
458
510
  output.pending = null;
459
511
  return parseTerm(input, output);
@@ -465,12 +517,29 @@ var parseChord = (symbol) => {
465
517
  const pitchClasses = [...new Set(notes.map(toPitchClass))].sort(
466
518
  (a, b) => a - b
467
519
  );
520
+ const rootPitch = output.pitch;
521
+ const rootFifth = output.rootFifth;
522
+ const noteFifths = notes.map((n) => {
523
+ const rel = ((n - rootPitch) % 12 + 12) % 12;
524
+ const f = output.relFifths.get(rel);
525
+ if (f !== void 0) return rootFifth + f;
526
+ let best = 0;
527
+ for (let k = -6; k <= 6; k++) {
528
+ if ((k * 7 % 12 + 12) % 12 === rel) {
529
+ best = k;
530
+ break;
531
+ }
532
+ }
533
+ return rootFifth + best;
534
+ });
468
535
  return {
469
536
  symbol,
470
- root: toPitchClass(output.pitch),
537
+ root: toPitchClass(rootPitch),
471
538
  notes,
472
539
  pitchClasses,
473
- intervals
540
+ intervals,
541
+ rootFifth,
542
+ noteFifths
474
543
  };
475
544
  };
476
545
  var QUALITY_SOURCE = [
@@ -1499,10 +1568,72 @@ var createChannelStrip = (ctx, destination, options = {}) => {
1499
1568
  };
1500
1569
  };
1501
1570
 
1571
+ // src/tuning.ts
1572
+ var UNITS_PER_OCTAVE = 372;
1573
+ var UNITS_PER_SEMITONE = 372 / 12;
1574
+ var UNITS_PER_EDO31_DEGREE = 372 / 31;
1575
+ var CENTS_PER_UNIT = 1200 / UNITS_PER_OCTAVE;
1576
+ var A4_UNITS = 69 * UNITS_PER_SEMITONE;
1577
+ var A4_HZ = 440;
1578
+ var DEFAULT_EDO = 12;
1579
+ var unitsPerStep = (edo) => UNITS_PER_OCTAVE / edo;
1580
+ var midiToUnits = (midi) => midi * UNITS_PER_SEMITONE;
1581
+ var unitsToMidi = (units) => units / UNITS_PER_SEMITONE;
1582
+ var unitsToHz = (units) => A4_HZ * 2 ** ((units - A4_UNITS) / UNITS_PER_OCTAVE);
1583
+ var unitsToMidiDetune = (units) => {
1584
+ const midi = Math.round(units / UNITS_PER_SEMITONE);
1585
+ return {
1586
+ midi,
1587
+ detuneCents: (units - midi * UNITS_PER_SEMITONE) * CENTS_PER_UNIT
1588
+ };
1589
+ };
1590
+ var NATURAL_STEPS = {
1591
+ 12: { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 },
1592
+ 31: { c: 0, d: 5, e: 10, f: 13, g: 18, a: 23, b: 28 }
1593
+ };
1594
+ var chromaticStep = (edo) => edo === 31 ? 2 : 1;
1595
+ var MICRO_STEP = 1;
1596
+ var isNaturalLetter = (ch) => Object.hasOwn(NATURAL_STEPS[12], ch);
1597
+ var naturalStep = (ch, edo) => NATURAL_STEPS[edo][ch] ?? null;
1598
+ var spellingToUnits = (letter, octave, chromatic, micro, edo) => {
1599
+ const step = naturalStep(letter, edo);
1600
+ if (step === null) return null;
1601
+ const perStep = unitsPerStep(edo);
1602
+ const stepsFromC = step + chromatic * chromaticStep(edo) + micro * MICRO_STEP;
1603
+ return (octave + 1) * UNITS_PER_OCTAVE + stepsFromC * perStep;
1604
+ };
1605
+ var FIFTH_STEPS = { 12: 7, 31: 18 };
1606
+ var fifthToStep = (fifthIndex, edo) => {
1607
+ const n = fifthIndex * FIFTH_STEPS[edo];
1608
+ return (n % edo + edo) % edo;
1609
+ };
1610
+ var fifthToUnits = (fifthIndex, edo) => fifthToStep(fifthIndex, edo) * unitsPerStep(edo);
1611
+ var PITCH_ENCODING_VERSION = 2;
1612
+ var pitchV1ToUnits = (pitch) => pitch * UNITS_PER_SEMITONE;
1613
+ var unitsToPitchV1 = (units) => Math.round(units / UNITS_PER_SEMITONE);
1614
+
1502
1615
  // src/chords.ts
1503
1616
  var C3 = 48;
1617
+ var MEANTONE_STEP_BY_SEMITONE = [0, 3, 5, 8, 10, 13, 16, 18, 21, 23, 26, 28];
1618
+ var chordToneToUnits = (relSemitone, relFifth, rootSemitone, rootFifth, edo) => {
1619
+ const perStep = UNITS_PER_OCTAVE / edo;
1620
+ const rootOct = Math.floor(rootSemitone / 12);
1621
+ const rootWithin = (rootSemitone % 12 + 12) % 12;
1622
+ const rootStep = edo === 31 ? (fifthToStep(rootFifth, 31) + MEANTONE_STEP_BY_SEMITONE[rootWithin] - fifthToStep(rootFifth, 31) + 31) % 31 : rootWithin;
1623
+ const relWithin12 = (relSemitone % 12 + 12) % 12;
1624
+ const relOct = Math.round((relSemitone - relWithin12) / 12);
1625
+ const relStep = (fifthToStep(relFifth, edo) - fifthToStep(rootFifth, edo) + edo) % edo;
1626
+ return (rootOct + relOct) * UNITS_PER_OCTAVE + (rootStep + relStep) * perStep;
1627
+ };
1504
1628
  var buildChordPlacements = (options) => {
1505
- const { chordStr, patternType, rootShift, bpm, stepsPerBar } = options;
1629
+ const {
1630
+ chordStr,
1631
+ patternType,
1632
+ rootShift,
1633
+ bpm,
1634
+ stepsPerBar,
1635
+ edo = 12
1636
+ } = options;
1506
1637
  const placements = [];
1507
1638
  if (!chordStr.trim()) return placements;
1508
1639
  const offset = rootShift;
@@ -1531,8 +1662,20 @@ var buildChordPlacements = (options) => {
1531
1662
  for (const group of Object.values(chordGroups)) {
1532
1663
  for (const chord of group) {
1533
1664
  let notes;
1665
+ let toUnits;
1534
1666
  try {
1535
- notes = [...parseChord(`${chord.key}${chord.chord}`).notes];
1667
+ const parsed = parseChord(`${chord.key}${chord.chord}`);
1668
+ notes = [...parsed.notes];
1669
+ const fifthOf = new Map(
1670
+ parsed.notes.map((n, i2) => [n, parsed.noteFifths[i2]])
1671
+ );
1672
+ toUnits = (rel) => chordToneToUnits(
1673
+ rel,
1674
+ fifthOf.get(rel) ?? parsed.rootFifth,
1675
+ C3 + offset,
1676
+ parsed.rootFifth,
1677
+ edo === 31 ? 31 : 12
1678
+ );
1536
1679
  } catch {
1537
1680
  continue;
1538
1681
  }
@@ -1541,7 +1684,7 @@ var buildChordPlacements = (options) => {
1541
1684
  for (const noteOffset of notes) {
1542
1685
  placements.push({
1543
1686
  startStep: chord.whenStep,
1544
- pitch: C3 + noteOffset + offset,
1687
+ pitchUnits: toUnits(noteOffset),
1545
1688
  durationSteps: noteLength,
1546
1689
  velocity: 100
1547
1690
  });
@@ -1551,7 +1694,7 @@ var buildChordPlacements = (options) => {
1551
1694
  notes.forEach((noteOffset, i2) => {
1552
1695
  placements.push({
1553
1696
  startStep: chord.whenStep + i2 * arpInterval,
1554
- pitch: C3 + noteOffset + offset,
1697
+ pitchUnits: toUnits(noteOffset),
1555
1698
  durationSteps: noteLength - i2 * arpInterval,
1556
1699
  velocity: 100
1557
1700
  });
@@ -1561,7 +1704,7 @@ var buildChordPlacements = (options) => {
1561
1704
  notes.forEach((noteOffset, i2) => {
1562
1705
  placements.push({
1563
1706
  startStep: chord.whenStep + i2 * arpInterval,
1564
- pitch: C3 + noteOffset + offset,
1707
+ pitchUnits: toUnits(noteOffset),
1565
1708
  durationSteps: Math.max(12, noteLength - i2 * arpInterval),
1566
1709
  velocity: 100
1567
1710
  });
@@ -1575,7 +1718,7 @@ var buildChordPlacements = (options) => {
1575
1718
  for (const noteOffset of notes) {
1576
1719
  placements.push({
1577
1720
  startStep: syncopatedStep,
1578
- pitch: C3 + noteOffset + offset,
1721
+ pitchUnits: toUnits(noteOffset),
1579
1722
  durationSteps: Math.min(halfBeat, 12),
1580
1723
  velocity: 100
1581
1724
  });
@@ -1594,7 +1737,7 @@ var buildChordPlacements = (options) => {
1594
1737
  for (const noteOffset of notes) {
1595
1738
  placements.push({
1596
1739
  startStep: noteStart,
1597
- pitch: C3 + noteOffset + offset,
1740
+ pitchUnits: toUnits(noteOffset),
1598
1741
  durationSteps: yatsumeLengthSteps,
1599
1742
  velocity: 100
1600
1743
  });
@@ -1606,7 +1749,7 @@ var buildChordPlacements = (options) => {
1606
1749
  const stepOffset = i2 * Math.floor(stepsPerBar / 4);
1607
1750
  placements.push({
1608
1751
  startStep: chord.whenStep + stepOffset,
1609
- pitch: C3 + noteOffset + offset,
1752
+ pitchUnits: toUnits(noteOffset),
1610
1753
  durationSteps: Math.max(12, Math.floor(stepsPerBar / 4)),
1611
1754
  velocity: 100
1612
1755
  });
@@ -1618,8 +1761,20 @@ var buildChordPlacements = (options) => {
1618
1761
  const chordNames = chordStr.split(/[\s,]+/).filter((c) => c);
1619
1762
  chordNames.forEach((chordName, barIndex) => {
1620
1763
  let notes;
1764
+ let toUnits;
1621
1765
  try {
1622
- notes = [...parseChord(chordName).notes];
1766
+ const parsed = parseChord(chordName);
1767
+ notes = [...parsed.notes];
1768
+ const fifthOf = new Map(
1769
+ parsed.notes.map((n, i2) => [n, parsed.noteFifths[i2]])
1770
+ );
1771
+ toUnits = (rel) => chordToneToUnits(
1772
+ rel,
1773
+ fifthOf.get(rel) ?? parsed.rootFifth,
1774
+ C3 + offset,
1775
+ parsed.rootFifth,
1776
+ edo === 31 ? 31 : 12
1777
+ );
1623
1778
  } catch {
1624
1779
  return;
1625
1780
  }
@@ -1629,7 +1784,7 @@ var buildChordPlacements = (options) => {
1629
1784
  const stepOffset = i2 * 3;
1630
1785
  placements.push({
1631
1786
  startStep: startStep + stepOffset,
1632
- pitch: C3 + noteOffset + offset,
1787
+ pitchUnits: toUnits(noteOffset),
1633
1788
  durationSteps: chordLength - stepOffset,
1634
1789
  velocity: 100
1635
1790
  });
@@ -2161,6 +2316,11 @@ var DEFAULT_VELOCITY = 100;
2161
2316
  var DEFAULT_PLAYBACK_VELOCITY = 127;
2162
2317
  var DEFAULT_STEPS_PER_BAR = 192;
2163
2318
  var MML_END_MARKER = "#end;";
2319
+ var PITCH_RANGE_START = 0;
2320
+ var PITCH_RANGE_END = 3937;
2321
+ var unitsPerRow = (edo) => 372 / edo;
2322
+ var keyCountFor = (edo) => Math.floor((PITCH_RANGE_END - PITCH_RANGE_START) / unitsPerRow(edo)) + 1;
2323
+ var KEY_COUNT = keyCountFor(12);
2164
2324
 
2165
2325
  // src/vibrato.ts
2166
2326
  var VIBRATO_MIN_SEC = 0.35;
@@ -2560,7 +2720,8 @@ var FORMANTS = {
2560
2720
  // 撥音(ん)は鼻音寄りの低フォルマント
2561
2721
  N: [250, 1e3]
2562
2722
  };
2563
- var midiToFreq = (m) => 440 * 2 ** ((m - 69) / 12);
2723
+ var unitsToFreq = (units) => 440 * 2 ** ((units - 2139) / 372);
2724
+ var unitsToMidiFloat = (units) => units / 31;
2564
2725
  var createKlattVoice = (ctx, destination, reverbBus, delayBus) => {
2565
2726
  const active = /* @__PURE__ */ new Set();
2566
2727
  const voice = (syllable, e) => {
@@ -2594,7 +2755,7 @@ var createKlattVoice = (ctx, destination, reverbBus, delayBus) => {
2594
2755
  }
2595
2756
  const osc = ctx.createOscillator();
2596
2757
  osc.type = "sawtooth";
2597
- osc.frequency.value = midiToFreq(e.pitch);
2758
+ osc.frequency.value = unitsToFreq(e.pitchUnits);
2598
2759
  const makeFormant = (freq, q2, gainScale) => {
2599
2760
  const filter = ctx.createBiquadFilter();
2600
2761
  filter.type = "bandpass";
@@ -2846,7 +3007,7 @@ var createLocalBackend = async (options) => {
2846
3007
  if (!consonantPcm || !vowelPcm) return null;
2847
3008
  const spliced = spliceCompositePcm(consonantPcm, vowelPcm, KOE_SAMPLE_RATE);
2848
3009
  if (!spliced) return null;
2849
- const targetHz = midiToFreq(pitch);
3010
+ const targetHz = unitsToFreq(pitch);
2850
3011
  const audio = worldline.renderNote({
2851
3012
  pcm: spliced.pcm,
2852
3013
  pitch: vibrato ? vibratoPitchCurve(targetHz, spliced.preMs) : targetHz,
@@ -2875,7 +3036,7 @@ var createLocalBackend = async (options) => {
2875
3036
  if (!pcm || pcm.length === 0) return null;
2876
3037
  const entry = bank.manifest.phonemes[alias];
2877
3038
  const lead = leadInFromEntry(entry);
2878
- const targetHz = midiToFreq(pitch);
3039
+ const targetHz = unitsToFreq(pitch);
2879
3040
  if (worldline) {
2880
3041
  const audio = worldline.renderNote({
2881
3042
  pcm,
@@ -3082,7 +3243,7 @@ var createKoeVoice = async (ctx, destination, options) => {
3082
3243
  backend.pitchTokens,
3083
3244
  syllable,
3084
3245
  prevVowel,
3085
- e.pitch
3246
+ unitsToMidiFloat(e.pitchUnits)
3086
3247
  );
3087
3248
  if (syllable.vowel && syllable.vowel !== "N") prevVowel = syllable.vowel;
3088
3249
  if (!alias) return;
@@ -3090,7 +3251,7 @@ var createKoeVoice = async (ctx, destination, options) => {
3090
3251
  const peak = Math.max(1e-4, e.volume);
3091
3252
  const pan = e.pan ?? 0;
3092
3253
  const durationMs = Math.max(60, e.duration * 1e3);
3093
- void renderInto(alias, e.pitch, durationMs).then((r) => {
3254
+ void renderInto(alias, e.pitchUnits, durationMs).then((r) => {
3094
3255
  if (r)
3095
3256
  schedule(r, t0, peak, pan, e.reverbSend, e.delaySend, e.destination);
3096
3257
  });
@@ -3102,7 +3263,10 @@ var createKoeVoice = async (ctx, destination, options) => {
3102
3263
  backend.pitchTokens,
3103
3264
  syllable,
3104
3265
  prevVowelArg,
3105
- pitch
3266
+ // 多音階バンクのピッチトークン("_G4" 等)は録音の音名なので、
3267
+ // 最寄り選択はMIDIノート番号の尺度で行う。units のまま渡すと常に
3268
+ // 最高音のトークンが選ばれてしまう。
3269
+ unitsToMidiFloat(pitch)
3106
3270
  );
3107
3271
  if (!alias) return null;
3108
3272
  const dMs = Math.max(60, durationMs);
@@ -3134,13 +3298,14 @@ var STREAM_LOOKAHEAD_SEC = 1.5;
3134
3298
  var STREAM_POLL_MS = 100;
3135
3299
  var OCTAVE_UNISON_PEAK_SCALE = 0.6;
3136
3300
  var octaveUnisonOffsets = (mode) => {
3301
+ const OCT = 372;
3137
3302
  switch (mode) {
3138
3303
  case "down":
3139
- return [-12];
3304
+ return [-OCT];
3140
3305
  case "up":
3141
- return [12];
3306
+ return [OCT];
3142
3307
  case "both":
3143
- return [-12, 12];
3308
+ return [-OCT, OCT];
3144
3309
  default:
3145
3310
  return [];
3146
3311
  }
@@ -3350,7 +3515,7 @@ var createSingingVoices = (ctx, destination, options = {}) => {
3350
3515
  const effPeak = (dest ? peak * masterVolumeScalar : peak) * peakScale;
3351
3516
  model(note.syllable, {
3352
3517
  trackId: track.id ?? "",
3353
- pitch,
3518
+ pitchUnits: pitch,
3354
3519
  velocity: 100,
3355
3520
  volume: effPeak,
3356
3521
  when,
@@ -3416,17 +3581,13 @@ var createVoiceRegistry = (models = {}, fallback = "klatt") => {
3416
3581
  };
3417
3582
 
3418
3583
  // src/mml-parser.ts
3419
- var PITCH_MAP = {
3420
- c: 0,
3421
- d: 2,
3422
- e: 4,
3423
- f: 5,
3424
- g: 7,
3425
- a: 9,
3426
- b: 11
3584
+ var NATURAL_STEPS2 = {
3585
+ 12: { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 },
3586
+ 31: { c: 0, d: 5, e: 10, f: 13, g: 18, a: 23, b: 28 }
3427
3587
  };
3588
+ var isNatural = (ch) => Object.hasOwn(NATURAL_STEPS2[12], ch);
3428
3589
  var clamp3 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
3429
- var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode)=([\w-]+)/gi;
3590
+ var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode|edo)=([\w-]+)/gi;
3430
3591
  var TRACK_INST_DIRECTIVE = /#t(\d+)inst=([^#;\r\n]+)/gi;
3431
3592
  var TRACK_COMP_DIRECTIVE = /#t(\d+)comp=(\d+)/gi;
3432
3593
  var TRACK_WIDTH_DIRECTIVE = /#t(\d+)width=(\d+)/gi;
@@ -3476,6 +3637,9 @@ var parseMmlMeta = (mml) => {
3476
3637
  if (m[2] === "simple" || m[2] === "advanced") {
3477
3638
  meta.mode = m[2];
3478
3639
  }
3640
+ } else if (key === "edo") {
3641
+ const e = Number.parseInt(m[2], 10);
3642
+ if (e === 12 || e === 31) meta.edo = e;
3479
3643
  }
3480
3644
  }
3481
3645
  for (const m of mml.matchAll(TRACK_INST_DIRECTIVE)) {
@@ -3578,6 +3742,7 @@ var formatMmlMeta = (meta, space = "") => {
3578
3742
  if (meta.fadeOut !== void 0 && meta.fadeOut !== 0)
3579
3743
  parts.push(`#fadeout=${meta.fadeOut}`);
3580
3744
  if (meta.mode) parts.push(`#mode=${meta.mode}`);
3745
+ if (meta.edo !== void 0 && meta.edo !== 12) parts.push(`#edo=${meta.edo}`);
3581
3746
  if (meta.trackInstruments) {
3582
3747
  for (const [idx, name] of Object.entries(meta.trackInstruments)) {
3583
3748
  if (name) parts.push(`#t${idx}inst=${name}`);
@@ -3652,6 +3817,10 @@ var parseMML = (mml, options = {}) => {
3652
3817
  const endMarkerBase = MML_END_MARKER.replace(/;+$/, "");
3653
3818
  const endRegex = new RegExp(`(?<![cdafgCDAFG])${endMarkerBase}\\b;?`, "gi");
3654
3819
  const fullMML = stripLyrics(noMeta).replace(endRegex, "").replace(/[\n\r]+/g, " ").trim();
3820
+ const edo = meta.edo === 31 ? 31 : 12;
3821
+ const naturals = NATURAL_STEPS2[edo];
3822
+ const unitsPerStep2 = 372 / edo;
3823
+ const chromaticStep2 = edo === 31 ? 2 : 1;
3655
3824
  const parts = fullMML.split(/(@\d+)/).filter((p) => p.trim().length > 0);
3656
3825
  let trackIndex = 0;
3657
3826
  let sourceTrackIndex = 0;
@@ -3699,6 +3868,19 @@ var parseMML = (mml, options = {}) => {
3699
3868
  type
3700
3869
  });
3701
3870
  };
3871
+ const readAccidentals = () => {
3872
+ let delta = 0;
3873
+ while (j < body.length) {
3874
+ const a = body[j];
3875
+ if (a === "#") delta += chromaticStep2;
3876
+ else if (a === "-") delta -= chromaticStep2;
3877
+ else if (a === "+") delta += edo === 31 ? 1 : chromaticStep2;
3878
+ else if (a === "_") delta -= edo === 31 ? 1 : chromaticStep2;
3879
+ else break;
3880
+ j++;
3881
+ }
3882
+ return delta;
3883
+ };
3702
3884
  const parseLength = () => {
3703
3885
  let numStr = "";
3704
3886
  while (j < body.length && /\d/.test(body[j])) {
@@ -3770,17 +3952,11 @@ var parseMML = (mml, options = {}) => {
3770
3952
  const savedOctave = octave;
3771
3953
  while (j < body.length && body[j] !== "]") {
3772
3954
  const c = body[j];
3773
- if (Object.hasOwn(PITCH_MAP, c)) {
3774
- let pitch = PITCH_MAP[c];
3955
+ if (isNatural(c)) {
3956
+ let pitchSteps = naturals[c];
3775
3957
  j++;
3776
- if (j < body.length && (body[j] === "#" || body[j] === "+")) {
3777
- pitch++;
3778
- j++;
3779
- } else if (j < body.length && body[j] === "-") {
3780
- pitch--;
3781
- j++;
3782
- }
3783
- chordNotes.push((octave + 1) * 12 + pitch);
3958
+ pitchSteps += readAccidentals();
3959
+ chordNotes.push((octave + 1) * 372 + pitchSteps * unitsPerStep2);
3784
3960
  } else if (c === ">") {
3785
3961
  octave = Math.min(8, octave + 1);
3786
3962
  j++;
@@ -3806,7 +3982,7 @@ var parseMML = (mml, options = {}) => {
3806
3982
  placements.push({
3807
3983
  trackIndex,
3808
3984
  startStep: currentStep,
3809
- pitch: p,
3985
+ pitchUnits: p,
3810
3986
  durationSteps: Math.max(1, steps),
3811
3987
  velocity
3812
3988
  });
@@ -3814,23 +3990,17 @@ var parseMML = (mml, options = {}) => {
3814
3990
  pushTok("chord", currentStep, Math.max(1, steps), tokStart);
3815
3991
  currentStep += steps;
3816
3992
  octave = savedOctave;
3817
- } else if (Object.hasOwn(PITCH_MAP, ch)) {
3818
- let pitch = PITCH_MAP[ch];
3993
+ } else if (isNatural(ch)) {
3994
+ let pitchSteps = naturals[ch];
3819
3995
  j++;
3820
- if (j < body.length && (body[j] === "#" || body[j] === "+")) {
3821
- pitch++;
3822
- j++;
3823
- } else if (j < body.length && body[j] === "-") {
3824
- pitch--;
3825
- j++;
3826
- }
3827
- const midiPitch = (octave + 1) * 12 + pitch;
3996
+ pitchSteps += readAccidentals();
3997
+ const midiPitch = (octave + 1) * 372 + pitchSteps * unitsPerStep2;
3828
3998
  const steps = parseLength();
3829
3999
  recordContributor();
3830
4000
  placements.push({
3831
4001
  trackIndex,
3832
4002
  startStep: currentStep,
3833
- pitch: midiPitch,
4003
+ pitchUnits: midiPitch,
3834
4004
  durationSteps: Math.max(1, steps),
3835
4005
  velocity
3836
4006
  });
@@ -3962,7 +4132,7 @@ var createSequencer = (options) => {
3962
4132
  maxEndStep = Math.max(maxEndStep, note.startStep + note.durationSteps);
3963
4133
  timeline.push({
3964
4134
  trackId: track.id,
3965
- pitch: note.pitch,
4135
+ pitch: note.pitchUnits,
3966
4136
  volume: track.volume / 100,
3967
4137
  velocity: note.velocity ?? DEFAULT_PLAYBACK_VELOCITY,
3968
4138
  when,
@@ -4025,7 +4195,7 @@ var createSequencer = (options) => {
4025
4195
  const currentVolume = (trackVolumeMap.get(ev.trackId) ?? ev.volume * 100) / 100;
4026
4196
  options.onPlayNote({
4027
4197
  trackId: ev.trackId,
4028
- pitch: ev.pitch,
4198
+ pitchUnits: ev.pitch,
4029
4199
  velocity: ev.velocity,
4030
4200
  volume: currentVolume * velocityVolume,
4031
4201
  when: Math.max(0, _when),
@@ -6345,7 +6515,7 @@ var SONG_DRUM_PATTERNS = {
6345
6515
  };
6346
6516
 
6347
6517
  // src/synth.ts
6348
- var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
6518
+ var freqFromPitch = (pitchUnits) => unitsToHz(pitchUnits);
6349
6519
  var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
6350
6520
  const wave = tone.wave ?? "square";
6351
6521
  const attack = tone.attack ?? 0;
@@ -6361,7 +6531,7 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
6361
6531
  const osc = ctx.createOscillator();
6362
6532
  const gain = ctx.createGain();
6363
6533
  osc.type = wave;
6364
- osc.frequency.value = freqFromPitch(e.pitch);
6534
+ osc.frequency.value = freqFromPitch(e.pitchUnits);
6365
6535
  const t0 = ctx.currentTime + e.when;
6366
6536
  const peak = Math.max(1e-4, 0.06 * e.volume * 1.5 * gainScale);
6367
6537
  if (tone.decay) {
@@ -6466,7 +6636,7 @@ var playPlacements = (placements, options) => {
6466
6636
  id: id++,
6467
6637
  startStep: p.startStep,
6468
6638
  durationSteps: p.durationSteps,
6469
- pitch: p.pitch,
6639
+ pitchUnits: p.pitchUnits,
6470
6640
  velocity: p.velocity
6471
6641
  }));
6472
6642
  return {
@@ -6637,7 +6807,7 @@ var playNote = (options) => {
6637
6807
  const dur = options.duration ?? 1;
6638
6808
  synth.playNote({
6639
6809
  trackId: "melody",
6640
- pitch: options.pitch,
6810
+ pitchUnits: options.pitchUnits,
6641
6811
  velocity: 100,
6642
6812
  volume: vol / 100,
6643
6813
  when: 0,
@@ -6658,7 +6828,7 @@ var playChords = (chordStr, options = {}) => {
6658
6828
  // 伴奏トラック
6659
6829
  startStep: p.startStep,
6660
6830
  durationSteps: p.durationSteps,
6661
- pitch: p.pitch,
6831
+ pitchUnits: p.pitchUnits,
6662
6832
  velocity: p.velocity
6663
6833
  }));
6664
6834
  return playPlacements(placements, {
@@ -7191,38 +7361,66 @@ var DAW_CSS = `
7191
7361
  flex-basis: 100%;
7192
7362
  display: flex;
7193
7363
  flex-wrap: nowrap;
7194
- gap: 3px;
7364
+ gap: 4px;
7365
+ align-items: flex-end;
7195
7366
  }
7196
7367
  .dtm-pill {
7197
7368
  --dtm-pill-color: var(--dtm-primary);
7369
+ position: relative;
7198
7370
  display: inline-flex;
7199
7371
  align-items: center;
7200
7372
  justify-content: center;
7201
7373
  flex: 1 1 0;
7202
7374
  min-width: 0;
7203
- height: 26px;
7375
+ height: 24px;
7204
7376
  padding: 0;
7205
7377
  border: 2px solid var(--c-black);
7206
- background: color-mix(in srgb, var(--dtm-pill-color) 40%, black);
7207
- color: var(--c-white);
7378
+ background: color-mix(in srgb, var(--dtm-pill-color) 22%, #0e121e);
7379
+ color: color-mix(in srgb, var(--c-white) 60%, transparent);
7208
7380
  font-family: var(--dtm-font);
7209
7381
  font-size: 11px;
7210
7382
  font-weight: bold;
7211
7383
  cursor: pointer;
7212
- box-shadow: 2px 2px 0 var(--c-black);
7213
- opacity: 0.7;
7384
+ box-shadow: 1px 1px 0 var(--c-black);
7385
+ opacity: 0.55;
7386
+ transition: opacity 120ms ease, background 120ms ease, transform 120ms ease;
7214
7387
  }
7215
- /* \u30A2\u30AF\u30C6\u30A3\u30D6\u9078\u629E = \u4E0D\u900F\u660E + \u91D1\u67A0 */
7388
+ .dtm-pill:hover:not(.dtm-pill--active) {
7389
+ opacity: 0.85;
7390
+ background: color-mix(in srgb, var(--dtm-pill-color) 45%, #0e121e);
7391
+ color: var(--c-white);
7392
+ }
7393
+ /* \u30A2\u30AF\u30C6\u30A3\u30D6\u9078\u629E = \u9BAE\u660E\u306A\u30C8\u30E9\u30C3\u30AF\u8272 + \u30B4\u30FC\u30EB\u30C9\u67A0 + \u30B0\u30ED\u30FC + \u4E0A\u6607\u7ACB\u4F53\u30BF\u30D6\u8868\u73FE */
7216
7394
  .dtm-pill--active {
7217
7395
  opacity: 1;
7396
+ height: 28px;
7397
+ background: var(--dtm-pill-color);
7398
+ color: #ffffff;
7399
+ text-shadow: 1px 1px 0 #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000;
7218
7400
  border-color: var(--dtm-gold);
7219
- box-shadow: 0 0 0 1px var(--dtm-gold), 2px 2px 0 var(--c-black);
7401
+ box-shadow:
7402
+ 0 0 0 1.5px var(--dtm-gold),
7403
+ 0 0 10px color-mix(in srgb, var(--dtm-pill-color) 70%, transparent),
7404
+ 2px 2px 0 var(--c-black);
7405
+ z-index: 1;
7406
+ transform: translateY(-2px);
7220
7407
  }
7221
- .dtm-pill:not(.dtm-pill--active):active { transform: translate(2px,2px); box-shadow: none; }
7408
+ .dtm-pill--active::before {
7409
+ content: '';
7410
+ position: absolute;
7411
+ top: -2px;
7412
+ left: 2px;
7413
+ right: 2px;
7414
+ height: 2px;
7415
+ background: #ffffff;
7416
+ border-radius: 1px;
7417
+ opacity: 0.85;
7418
+ }
7419
+ .dtm-pill:not(.dtm-pill--active):active { transform: translate(1px,1px); box-shadow: none; }
7222
7420
  /* \u518D\u751F\u4E2D\u3001\u5B9F\u969B\u306B\u767A\u97F3\u3057\u305F\u77AC\u9593\u3060\u3051\u70B9\u706F\uFF08\u3069\u306E\u30BF\u30D6\u304C\u4ECA\u9CF4\u3063\u3066\u3044\u308B\u304B\u8996\u899A\u7684\u306B\u5206\u304B\u308B\u3088\u3046\u306B\uFF09 */
7223
7421
  .dtm-pill--sounding {
7224
- background: color-mix(in srgb, var(--dtm-pill-color) 85%, white);
7225
- transition: background 30ms linear;
7422
+ filter: brightness(1.6);
7423
+ transition: filter 30ms linear;
7226
7424
  }
7227
7425
 
7228
7426
  /* \u2500\u2500\u2500 \u30D4\u30A2\u30CE\u30ED\u30FC\u30EB\uFF08\u30C8\u30E9\u30C3\u30AB\u30FC\u98A8\uFF09 \u2500\u2500\u2500 */
@@ -7328,6 +7526,49 @@ var DAW_CSS = `
7328
7526
  .dtm-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
7329
7527
  .dtm-track-body { display: flex; flex-direction: column; gap: 10px; }
7330
7528
 
7529
+ /* \u2500\u2500\u2500 \u30A2\u30AF\u30C6\u30A3\u30D6\u30C8\u30E9\u30C3\u30AF\u60C5\u5831\u30D0\u30CA\u30FC \u2500\u2500\u2500 */
7530
+ .dtm-active-track-banner {
7531
+ display: flex;
7532
+ align-items: center;
7533
+ gap: 8px;
7534
+ padding: 6px 10px;
7535
+ background: color-mix(in srgb, var(--dtm-track-color, var(--dtm-primary)) 16%, var(--dtm-deep));
7536
+ border: 2px solid var(--c-black);
7537
+ border-left: 5px solid var(--dtm-track-color, var(--dtm-primary));
7538
+ box-shadow: 2px 2px 0 var(--c-black);
7539
+ margin-bottom: 2px;
7540
+ }
7541
+ .dtm-active-track-badge {
7542
+ font-family: var(--dtm-font);
7543
+ font-size: 11px;
7544
+ font-weight: bold;
7545
+ background: var(--dtm-track-color, var(--dtm-primary));
7546
+ color: #ffffff;
7547
+ text-shadow: 1px 1px 0 #000, -1px -1px 0 #000;
7548
+ padding: 2px 6px;
7549
+ border: 1px solid var(--c-black);
7550
+ border-radius: 2px;
7551
+ letter-spacing: .06em;
7552
+ }
7553
+ .dtm-active-track-name {
7554
+ font-family: var(--dtm-font);
7555
+ font-size: 12px;
7556
+ font-weight: bold;
7557
+ color: var(--c-white);
7558
+ flex: 1 1 auto;
7559
+ }
7560
+ .dtm-active-track-pill {
7561
+ font-family: var(--dtm-font);
7562
+ font-size: 9px;
7563
+ font-weight: bold;
7564
+ letter-spacing: .08em;
7565
+ padding: 2px 5px;
7566
+ color: var(--dtm-gold);
7567
+ border: 1px solid var(--dtm-gold);
7568
+ background: rgba(0, 0, 0, 0.45);
7569
+ border-radius: 2px;
7570
+ }
7571
+
7331
7572
  /* \u2500\u2500\u2500 MML\u51FA\u529B\uFF08CRT\u30BF\u30FC\u30DF\u30CA\u30EB\uFF09 \u2500\u2500\u2500 */
7332
7573
  .dtm-output {
7333
7574
  background: var(--c-black);
@@ -8744,7 +8985,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
8744
8985
  0
8745
8986
  );
8746
8987
  const timedNotes = placements.map((p) => ({
8747
- pitch: p.pitch,
8988
+ pitch: unitsToPitchV1(p.pitchUnits),
8748
8989
  when: p.startStep * secondsPerStep,
8749
8990
  duration: p.durationSteps * secondsPerStep
8750
8991
  }));
@@ -8777,7 +9018,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
8777
9018
  id: id++,
8778
9019
  startStep: p.startStep,
8779
9020
  durationSteps: p.durationSteps,
8780
- pitch: p.pitch,
9021
+ pitchUnits: p.pitchUnits,
8781
9022
  velocity: DEFAULT_VELOCITY
8782
9023
  }));
8783
9024
  return {
@@ -9602,7 +9843,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9602
9843
  (a, b) => a.startStep - b.startStep
9603
9844
  );
9604
9845
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
9605
- const semis = (lt.octave ?? 0) * 12;
9846
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
9606
9847
  const count = Math.min(sorted.length, lt.syllables.length);
9607
9848
  const notes = [];
9608
9849
  for (let i2 = 0; i2 < count; i2++) {
@@ -9610,7 +9851,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9610
9851
  if (n.startStep < fromStep) continue;
9611
9852
  notes.push({
9612
9853
  syllable: lt.syllables[i2],
9613
- pitch: n.pitch + semis,
9854
+ pitch: n.pitchUnits + semis,
9614
9855
  startSec: (n.startStep - fromStep) * secondsPerStep,
9615
9856
  durationSec: n.durationSteps * secondsPerStep * gate
9616
9857
  });
@@ -9885,6 +10126,7 @@ var SoundFont = class _SoundFont {
9885
10126
  pitch = 60,
9886
10127
  volume = 1,
9887
10128
  velocity,
10129
+ detuneCents = 0,
9888
10130
  when = 0,
9889
10131
  duration = 1
9890
10132
  } = {}) {
@@ -9904,7 +10146,7 @@ var SoundFont = class _SoundFont {
9904
10146
  Object.assign(src, _param.src);
9905
10147
  const humanizeCents = (Math.random() * 2 - 1) * _SoundFont.humanizeDetuneCents;
9906
10148
  const humanizeGainMul = 1 + (Math.random() * 2 - 1) * _SoundFont.humanizeGainRatio;
9907
- src.detune.setValueAtTime(humanizeCents, 0);
10149
+ src.detune.setValueAtTime(humanizeCents + detuneCents, 0);
9908
10150
  const effectiveVolume = volume * humanizeGainMul;
9909
10151
  const brightness = velocity === void 0 ? 1 : Math.max(0, Math.min(1, velocity / 127));
9910
10152
  const filter = brightness >= _SoundFont.brightnessBypassAbove ? void 0 : ctx.createBiquadFilter();
@@ -11043,7 +11285,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11043
11285
  scheduleMetronomeBeats(ctx, cutGain);
11044
11286
  };
11045
11287
  const wafPlayDynamic = ({
11046
- pitch,
11288
+ pitchUnits,
11047
11289
  velocity,
11048
11290
  volume,
11049
11291
  when,
@@ -11053,10 +11295,12 @@ var mountChordPlayer = (target, chords, options = {}) => {
11053
11295
  }) => {
11054
11296
  const waf = activeGmName ? _wafCache.get(activeGmName) : null;
11055
11297
  if (waf) {
11298
+ const { midi, detuneCents } = unitsToMidiDetune(pitchUnits);
11056
11299
  waf.play({
11057
11300
  ctx,
11058
11301
  destination,
11059
- pitch,
11302
+ pitch: midi,
11303
+ detuneCents,
11060
11304
  volume: volume * 0.85,
11061
11305
  velocity,
11062
11306
  when,
@@ -11066,7 +11310,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11066
11310
  const synth = createSynth(ctx, destination);
11067
11311
  synth.playNote({
11068
11312
  trackId: "chord",
11069
- pitch,
11313
+ pitchUnits,
11070
11314
  velocity: 100,
11071
11315
  volume,
11072
11316
  when,
@@ -11095,7 +11339,8 @@ var mountChordPlayer = (target, chords, options = {}) => {
11095
11339
  trackIndex: 3,
11096
11340
  startStep: whenStep,
11097
11341
  durationSteps,
11098
- pitch: C32 + noteOffset,
11342
+ // chord-parser の構成音は12平均律の半音。内部表現の units へ写す。
11343
+ pitchUnits: pitchV1ToUnits(C32 + noteOffset),
11099
11344
  velocity: 100
11100
11345
  });
11101
11346
  }
@@ -11108,7 +11353,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11108
11353
  synth: false,
11109
11354
  onPlayNote: (e) => {
11110
11355
  wafPlayDynamic({
11111
- pitch: e.pitch,
11356
+ pitchUnits: e.pitchUnits,
11112
11357
  velocity: e.velocity,
11113
11358
  volume: e.volume,
11114
11359
  when: e.when,
@@ -11550,6 +11795,14 @@ var buildUI = (target, options) => {
11550
11795
  <summary>\u5168\u4F53\u30C8\u30E9\u30C3\u30AF\u8A2D\u5B9A</summary>
11551
11796
  <div class="dtm-panel-body">
11552
11797
  <div data-dtm="preset-select-slot"></div>
11798
+ <div class="dtm-row">
11799
+ <span class="dtm-label">\u97F3\u5F8B</span>
11800
+ <select class="dtm-select dtm-grow" data-dtm="edo-select">
11801
+ <option value="12">12\u5E73\u5747\u5F8B\uFF08\u901A\u5E38\uFF09</option>
11802
+ <option value="31">31\u5E73\u5747\u5F8B\uFF08\u5FAE\u5206\u97F3\uFF09</option>
11803
+ </select>
11804
+ <button class="dtm-infobtn" data-dtm="edo-info" title="\u97F3\u5F8B\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
11805
+ </div>
11553
11806
  <div class="dtm-row">
11554
11807
  <span class="dtm-label">\u5168\u4F53\u97F3\u91CF</span>
11555
11808
  <input type="range" class="dtm-range dtm-grow" data-dtm="master-volume" value="50" min="0" max="100">
@@ -11882,6 +12135,8 @@ var buildUI = (target, options) => {
11882
12135
  copyMiniBtn: sel("copy-mini"),
11883
12136
  overlay: sel("overlay"),
11884
12137
  mmlInfoBtn: sel("mml-info"),
12138
+ edoSelect: sel("edo-select"),
12139
+ edoInfoBtn: sel("edo-info"),
11885
12140
  modalOverlay: sel("modal-overlay"),
11886
12141
  modalTitle: sel("modal-title"),
11887
12142
  modalBody: sel("modal-body"),
@@ -12002,17 +12257,21 @@ var SCALES = [
12002
12257
  [0, 2, 4, 7, 9]
12003
12258
  // Pentatonic Major
12004
12259
  ];
12260
+ var MEANTONE_STEP_BY_SEMITONE2 = [0, 3, 5, 8, 10, 13, 16, 18, 21, 23, 26, 28];
12005
12261
  var generateRandomPattern = (core, options) => {
12006
- const { stepsPerBar, startStep, pitchRangeStart } = options;
12262
+ const { stepsPerBar, startStep, pitchRangeStart, edo = 12 } = options;
12007
12263
  const numBars = 8;
12008
12264
  const noteLength = 24;
12009
- const basePitch = pitchRangeStart + 60;
12265
+ const semitoneToStep = (semi) => edo === 31 ? MEANTONE_STEP_BY_SEMITONE2[semi] : semi;
12266
+ const upr = UNITS_PER_OCTAVE / edo;
12267
+ const basePitch = pitchRangeStart + 60 * UNITS_PER_SEMITONE;
12010
12268
  const scale = SCALES[Math.floor(Math.random() * SCALES.length)];
12011
12269
  const rootOffset = Math.floor(Math.random() * 12);
12012
12270
  const availablePitches = [];
12013
12271
  for (let i2 = 0; i2 < 12; i2++) {
12014
12272
  const noteInOctave = (i2 - rootOffset + 12) % 12;
12015
- if (scale.includes(noteInOctave)) availablePitches.push(basePitch + i2);
12273
+ if (scale.includes(noteInOctave))
12274
+ availablePitches.push(basePitch + semitoneToStep(i2) * upr);
12016
12275
  }
12017
12276
  core.beginBatch();
12018
12277
  for (let bar = 0; bar < numBars; bar++) {
@@ -12047,14 +12306,17 @@ var applyHarmonicFilter = (targetCore, chordCore, options) => {
12047
12306
  const isNewBar = halfBar % 2 === 0;
12048
12307
  const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12049
12308
  if (chordHere.length > 0) {
12050
- currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
12309
+ currentClasses = new Set(
12310
+ chordHere.map((n) => n.pitchUnits % UNITS_PER_OCTAVE)
12311
+ );
12051
12312
  } else if (isNewBar) {
12052
12313
  currentClasses = /* @__PURE__ */ new Set();
12053
12314
  }
12054
12315
  if (currentClasses.size === 0) continue;
12055
12316
  const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12056
12317
  for (const n of activeHere) {
12057
- if (!currentClasses.has(n.pitch % 12)) targetCore.deleteNoteById(n.id);
12318
+ if (!currentClasses.has(n.pitchUnits % UNITS_PER_OCTAVE))
12319
+ targetCore.deleteNoteById(n.id);
12058
12320
  }
12059
12321
  }
12060
12322
  targetCore.endBatch();
@@ -12076,13 +12338,17 @@ var applyMonophonic = (targetCore, chordCore, options) => {
12076
12338
  const isNewBar = halfBar % 2 === 0;
12077
12339
  const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12078
12340
  if (chordHere.length > 0) {
12079
- currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
12341
+ currentClasses = new Set(
12342
+ chordHere.map((n) => n.pitchUnits % UNITS_PER_OCTAVE)
12343
+ );
12080
12344
  } else if (isNewBar) {
12081
12345
  currentClasses = /* @__PURE__ */ new Set();
12082
12346
  }
12083
12347
  if (currentClasses.size === 0) continue;
12084
12348
  const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12085
- const filtered = activeHere.filter((n) => currentClasses.has(n.pitch % 12));
12349
+ const filtered = activeHere.filter(
12350
+ (n) => currentClasses.has(n.pitchUnits % UNITS_PER_OCTAVE)
12351
+ );
12086
12352
  const filteredIds = new Set(filtered.map((n) => n.id));
12087
12353
  for (const n of activeHere) {
12088
12354
  if (!filteredIds.has(n.id)) targetCore.deleteNoteById(n.id);
@@ -12094,7 +12360,7 @@ var applyMonophonic = (targetCore, chordCore, options) => {
12094
12360
  }
12095
12361
  for (const notesAtTime of timeMap.values()) {
12096
12362
  if (notesAtTime.length > 1) {
12097
- notesAtTime.sort((a, b) => b.pitch - a.pitch);
12363
+ notesAtTime.sort((a, b) => b.pitchUnits - a.pitchUnits);
12098
12364
  const [, ...others] = notesAtTime;
12099
12365
  for (const on of others) targetCore.deleteNoteById(on.id);
12100
12366
  }
@@ -12110,18 +12376,21 @@ var shiftNotes = (cores, shiftSteps) => {
12110
12376
  for (const note of notes) {
12111
12377
  const newStart = note.startStep + shiftSteps;
12112
12378
  if (newStart < 0) core.deleteNoteById(note.id);
12113
- else core.moveNote(note.id, newStart, note.pitch);
12379
+ else core.moveNote(note.id, newStart, note.pitchUnits);
12114
12380
  }
12115
12381
  core.saveHistory();
12116
12382
  }
12117
12383
  };
12118
- var transposeNotes = (cores, semitones) => {
12119
- if (semitones === 0) return;
12384
+ var transposeNotes = (cores, steps) => {
12385
+ if (steps === 0) return;
12120
12386
  for (const core of cores) {
12121
12387
  const notes = [...core.getNotes()];
12122
12388
  for (const note of notes) {
12123
- const newPitch = Math.max(0, Math.min(127, note.pitch + semitones));
12124
- if (newPitch !== note.pitch) {
12389
+ const newPitch = Math.max(
12390
+ PITCH_RANGE_START,
12391
+ Math.min(PITCH_RANGE_END, note.pitchUnits + steps)
12392
+ );
12393
+ if (newPitch !== note.pitchUnits) {
12125
12394
  core.moveNote(note.id, note.startStep, newPitch);
12126
12395
  }
12127
12396
  }
@@ -12446,11 +12715,31 @@ var exportMIDI = (options) => {
12446
12715
  const div = 480;
12447
12716
  const tickPerStep = div / STEPS_PER_BEAT3;
12448
12717
  const midiTracks = [];
12449
- tracks.forEach((track, ch) => {
12718
+ const NOTE_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15];
12719
+ const BEND_RANGE_SEMITONES = 2;
12720
+ const bendKey = (cents) => Math.round(cents * 100);
12721
+ const bendUse = /* @__PURE__ */ new Map();
12722
+ for (const track of tracks) {
12723
+ for (const n of track.notes) {
12724
+ const { detuneCents } = unitsToMidiDetune(n.pitchUnits);
12725
+ const k = bendKey(detuneCents);
12726
+ bendUse.set(k, (bendUse.get(k) ?? 0) + 1);
12727
+ }
12728
+ }
12729
+ const bendChannel = /* @__PURE__ */ new Map();
12730
+ const ordered = [...bendUse.entries()].sort((a, b) => b[1] - a[1]);
12731
+ for (const [k] of ordered) {
12732
+ if (bendChannel.size >= NOTE_CHANNELS.length) break;
12733
+ bendChannel.set(k, NOTE_CHANNELS[bendChannel.size]);
12734
+ }
12735
+ tracks.forEach((track) => {
12450
12736
  if (track.notes.length === 0) return;
12451
- const channel = ch < 9 ? ch : ch + 1 & 15;
12452
12737
  const events = [];
12453
12738
  for (const n of track.notes) {
12739
+ const { midi, detuneCents } = unitsToMidiDetune(n.pitchUnits);
12740
+ const k = bendKey(detuneCents);
12741
+ const channel = bendChannel.get(k) ?? bendChannel.get(0) ?? 0;
12742
+ const note = Math.max(0, Math.min(127, midi));
12454
12743
  const startTick = Math.round(n.startStep * tickPerStep);
12455
12744
  const endTick = Math.round(
12456
12745
  (n.startStep + (n.durationSteps || 1)) * tickPerStep
@@ -12458,12 +12747,25 @@ var exportMIDI = (options) => {
12458
12747
  const vel = Math.round(
12459
12748
  (n.velocity ?? DEFAULT_VELOCITY) * (track.volume ?? 100) / 100
12460
12749
  );
12461
- events.push({ t: startTick, m: [144 | channel, n.pitch, vel] });
12462
- events.push({ t: endTick, m: [144 | channel, n.pitch, 0] });
12750
+ events.push({ t: startTick, m: [144 | channel, note, vel] });
12751
+ events.push({ t: endTick, m: [144 | channel, note, 0] });
12463
12752
  }
12464
12753
  events.sort((a, b) => a.t - b.t);
12465
12754
  midiTracks.push(events);
12466
12755
  });
12756
+ const bendSetup = [];
12757
+ for (const [k, channel] of bendChannel) {
12758
+ const cents = k / 100;
12759
+ bendSetup.push({ t: 0, m: [176 | channel, 101, 0] });
12760
+ bendSetup.push({ t: 0, m: [176 | channel, 100, 0] });
12761
+ bendSetup.push({ t: 0, m: [176 | channel, 6, BEND_RANGE_SEMITONES] });
12762
+ bendSetup.push({ t: 0, m: [176 | channel, 38, 0] });
12763
+ bendSetup.push({ t: 0, m: [176 | channel, 101, 127] });
12764
+ bendSetup.push({ t: 0, m: [176 | channel, 100, 127] });
12765
+ const raw = 8192 + Math.round(cents / (BEND_RANGE_SEMITONES * 100) * 8192);
12766
+ const v = Math.max(0, Math.min(16383, raw));
12767
+ bendSetup.push({ t: 0, m: [224 | channel, v & 127, v >> 7 & 127] });
12768
+ }
12467
12769
  const maxStep = Math.max(
12468
12770
  ...tracks.filter((t) => t.notes.length > 0).map(
12469
12771
  (t) => Math.max(...t.notes.map((n) => n.startStep + n.durationSteps))
@@ -12500,6 +12802,7 @@ var exportMIDI = (options) => {
12500
12802
  headerChunks(arr, midiTracks.length + 1, div);
12501
12803
  trackChunks(arr, (a) => {
12502
12804
  a.push(0, 255, 81, 3, ...to3byte(Math.round(6e7 / bpm)));
12805
+ for (const ev of bendSetup) a.push(0, ...ev.m);
12503
12806
  });
12504
12807
  for (const events of midiTracks) {
12505
12808
  trackChunks(arr, (a) => {
@@ -12746,380 +13049,8 @@ var LinkedList = class {
12746
13049
  }
12747
13050
  };
12748
13051
 
12749
- // src/renderer.ts
12750
- var g_header_canvas;
12751
- var g_key_canvas;
12752
- var g_grid_canvas;
12753
- var g_header_ctx;
12754
- var g_key_ctx;
12755
- var g_grid_ctx;
12756
- var g_config;
12757
- var KEYBOARD_WIDTH = 60;
12758
- var HEADER_HEIGHT = 20;
12759
- var getRenderConfig = () => g_config;
12760
- var g_draw_offset_x = 0;
12761
- var g_draw_offset_y = 0;
12762
- var g_bg_active = false;
12763
- var setBackgroundActive = (active) => {
12764
- g_bg_active = active;
12765
- };
12766
- var getDrawOffset = () => ({
12767
- x: g_draw_offset_x,
12768
- y: g_draw_offset_y
12769
- });
12770
- var getGridCanvas = () => g_grid_canvas;
12771
- var getGridContext = () => g_grid_ctx;
12772
- var getHeaderCanvas = () => g_header_canvas;
12773
- var init = (mountTarget, width = 800, height = 450, config) => {
12774
- g_config = config;
12775
- const headerCanvas = document.createElement("canvas");
12776
- g_header_canvas = headerCanvas;
12777
- headerCanvas.width = width - KEYBOARD_WIDTH;
12778
- headerCanvas.height = HEADER_HEIGHT;
12779
- headerCanvas.style.position = "absolute";
12780
- headerCanvas.style.left = `${KEYBOARD_WIDTH}px`;
12781
- headerCanvas.style.top = "0px";
12782
- const headerCtx = headerCanvas.getContext("2d");
12783
- if (!headerCtx)
12784
- throw new Error("Failed to get 2D rendering context for header.");
12785
- g_header_ctx = headerCtx;
12786
- const keyCanvas = document.createElement("canvas");
12787
- g_key_canvas = keyCanvas;
12788
- keyCanvas.width = KEYBOARD_WIDTH;
12789
- keyCanvas.height = height - HEADER_HEIGHT;
12790
- keyCanvas.style.position = "absolute";
12791
- keyCanvas.style.left = "0px";
12792
- keyCanvas.style.top = `${HEADER_HEIGHT}px`;
12793
- const keyCtx = keyCanvas.getContext("2d");
12794
- if (!keyCtx)
12795
- throw new Error("Failed to get 2D rendering context for keyboard.");
12796
- g_key_ctx = keyCtx;
12797
- const gridCanvas = document.createElement("canvas");
12798
- g_grid_canvas = gridCanvas;
12799
- gridCanvas.width = width - KEYBOARD_WIDTH;
12800
- gridCanvas.height = height - HEADER_HEIGHT;
12801
- gridCanvas.style.position = "absolute";
12802
- gridCanvas.style.left = `${KEYBOARD_WIDTH}px`;
12803
- gridCanvas.style.top = `${HEADER_HEIGHT}px`;
12804
- gridCanvas.style.touchAction = "none";
12805
- gridCanvas.style.userSelect = "none";
12806
- const gridCtx = gridCanvas.getContext("2d", { willReadFrequently: true });
12807
- if (!gridCtx) throw new Error("Failed to get 2D rendering context for grid.");
12808
- g_grid_ctx = gridCtx;
12809
- mountTarget.innerHTML = "";
12810
- mountTarget.style.position = "relative";
12811
- mountTarget.style.width = `${width + KEYBOARD_WIDTH}px`;
12812
- mountTarget.style.height = `${height}px`;
12813
- mountTarget.append(headerCanvas, keyCanvas, gridCanvas);
12814
- drawHeaderCorner();
12815
- };
12816
- var blackKeyPitches = /* @__PURE__ */ new Set([1, 3, 6, 8, 10]);
12817
- var KEY_NAMES = [
12818
- "C",
12819
- "C#",
12820
- "D",
12821
- "D#",
12822
- "E",
12823
- "F",
12824
- "F#",
12825
- "G",
12826
- "G#",
12827
- "A",
12828
- "A#",
12829
- "B"
12830
- ];
12831
- var drawHeaderCorner = () => {
12832
- const mountTarget = g_key_canvas.parentElement;
12833
- if (!mountTarget) return;
12834
- let cornerDiv = mountTarget.querySelector("#header-corner");
12835
- if (!cornerDiv) {
12836
- cornerDiv = document.createElement("div");
12837
- cornerDiv.id = "header-corner";
12838
- cornerDiv.style.position = "absolute";
12839
- cornerDiv.style.left = "0px";
12840
- cornerDiv.style.top = "0px";
12841
- cornerDiv.style.width = `${KEYBOARD_WIDTH}px`;
12842
- cornerDiv.style.height = `${HEADER_HEIGHT}px`;
12843
- cornerDiv.style.backgroundColor = "#0a0f1f";
12844
- cornerDiv.style.borderRight = "2px solid #29adff";
12845
- cornerDiv.style.borderBottom = "2px solid #29adff";
12846
- mountTarget.insertBefore(cornerDiv, g_header_canvas);
12847
- }
12848
- };
12849
- var drawKeyboard = () => {
12850
- g_key_ctx.clearRect(0, 0, g_key_canvas.width, g_key_canvas.height);
12851
- const { keyHeight, keyCount, pitchRangeStart } = g_config;
12852
- const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
12853
- const endY = g_draw_offset_y + g_key_canvas.height;
12854
- const WHITE_KEY = "#ccc8b4";
12855
- const BLACK_KEY = "#111111";
12856
- const BK_EDGE = "#383838";
12857
- const WW_SEP = "#807a6a";
12858
- const BK_RATIO = 0.62;
12859
- for (let y = startY; y < endY; y += keyHeight) {
12860
- const pitchIndex = keyCount - 1 - y / keyHeight;
12861
- const totalPitch = pitchIndex + pitchRangeStart;
12862
- const pitchMod12 = totalPitch % 12;
12863
- const isBlackKey = blackKeyPitches.has(pitchMod12);
12864
- const octave = Math.floor(totalPitch / 12) - 1;
12865
- const isC4Range = octave === 4;
12866
- const screenY = y - g_draw_offset_y;
12867
- const bkW = Math.floor(KEYBOARD_WIDTH * BK_RATIO);
12868
- if (isBlackKey) {
12869
- g_key_ctx.fillStyle = isC4Range ? "#d8d4be" : WHITE_KEY;
12870
- g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
12871
- g_key_ctx.fillStyle = isC4Range ? "#1a1408" : BLACK_KEY;
12872
- g_key_ctx.fillRect(0, screenY, bkW, keyHeight);
12873
- g_key_ctx.strokeStyle = BK_EDGE;
12874
- g_key_ctx.lineWidth = 1;
12875
- g_key_ctx.beginPath();
12876
- g_key_ctx.moveTo(bkW, screenY);
12877
- g_key_ctx.lineTo(bkW, screenY + keyHeight);
12878
- g_key_ctx.stroke();
12879
- } else {
12880
- g_key_ctx.fillStyle = isC4Range ? "#dedad0" : WHITE_KEY;
12881
- g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
12882
- if (pitchMod12 === 5 || pitchMod12 === 0) {
12883
- g_key_ctx.strokeStyle = WW_SEP;
12884
- g_key_ctx.lineWidth = 1;
12885
- g_key_ctx.beginPath();
12886
- g_key_ctx.moveTo(0, screenY + keyHeight - 0.5);
12887
- g_key_ctx.lineTo(KEYBOARD_WIDTH, screenY + keyHeight - 0.5);
12888
- g_key_ctx.stroke();
12889
- }
12890
- }
12891
- if (pitchMod12 === 0) {
12892
- const octave2 = Math.floor(totalPitch / 12) - 1;
12893
- g_key_ctx.fillStyle = "#555040";
12894
- g_key_ctx.font = "10px 'k8x12',monospace";
12895
- g_key_ctx.textAlign = "right";
12896
- g_key_ctx.textBaseline = "bottom";
12897
- g_key_ctx.fillText(
12898
- `${KEY_NAMES[pitchMod12]}${octave2}`,
12899
- KEYBOARD_WIDTH - 4,
12900
- screenY + keyHeight - 2
12901
- );
12902
- }
12903
- }
12904
- g_key_ctx.beginPath();
12905
- g_key_ctx.strokeStyle = "#29adff";
12906
- g_key_ctx.lineWidth = 2;
12907
- g_key_ctx.moveTo(KEYBOARD_WIDTH, 0);
12908
- g_key_ctx.lineTo(KEYBOARD_WIDTH, g_key_canvas.height);
12909
- g_key_ctx.stroke();
12910
- };
12911
- var drawHeader = () => {
12912
- g_header_ctx.clearRect(0, 0, g_header_canvas.width, g_header_canvas.height);
12913
- const { stepWidth, stepsPerBar } = g_config;
12914
- g_header_ctx.save();
12915
- g_header_ctx.translate(-g_draw_offset_x, 0);
12916
- g_header_ctx.fillStyle = g_bg_active ? "rgba(10,15,31,0.55)" : "#0a0f1f";
12917
- g_header_ctx.fillRect(
12918
- g_draw_offset_x,
12919
- 0,
12920
- g_header_canvas.width,
12921
- HEADER_HEIGHT
12922
- );
12923
- g_header_ctx.strokeStyle = "#3d405b";
12924
- g_header_ctx.lineWidth = 1;
12925
- g_header_ctx.font = "11px 'k8x12',monospace";
12926
- g_header_ctx.fillStyle = "#83769c";
12927
- const startBar = Math.floor(g_draw_offset_x / (stepsPerBar * stepWidth));
12928
- const endBar = Math.ceil(
12929
- (g_draw_offset_x + g_header_canvas.width) / (stepsPerBar * stepWidth)
12930
- );
12931
- for (let bar = startBar; bar <= endBar + 1; bar++) {
12932
- const x2 = bar * stepsPerBar * stepWidth;
12933
- const screenX = x2;
12934
- g_header_ctx.beginPath();
12935
- g_header_ctx.moveTo(screenX, 0);
12936
- g_header_ctx.lineTo(screenX, HEADER_HEIGHT);
12937
- g_header_ctx.stroke();
12938
- if (bar >= 0) {
12939
- g_header_ctx.textAlign = "left";
12940
- g_header_ctx.textBaseline = "middle";
12941
- g_header_ctx.fillText(`${bar + 1}`, screenX + 5, HEADER_HEIGHT / 2);
12942
- }
12943
- }
12944
- g_header_ctx.restore();
12945
- };
12946
- var drawGrid = (noteLengthSteps = 1) => {
12947
- drawKeyboard();
12948
- drawHeader();
12949
- g_grid_ctx.clearRect(0, 0, g_grid_canvas.width, g_grid_canvas.height);
12950
- const { keyHeight, keyCount, stepWidth, stepsPerBar, pitchRangeStart } = g_config;
12951
- const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
12952
- const endY = g_draw_offset_y + g_grid_canvas.height;
12953
- for (let y = startY; y < endY; y += keyHeight) {
12954
- const pitchIndex = keyCount - 1 - y / keyHeight;
12955
- const totalPitch = pitchIndex + pitchRangeStart;
12956
- const pitchMod12 = totalPitch % 12;
12957
- const isBlackKey = blackKeyPitches.has(pitchMod12);
12958
- const isC = pitchMod12 === 0;
12959
- const octave = Math.floor(totalPitch / 12) - 1;
12960
- const isC4Range = octave === 4;
12961
- const screenY = y - g_draw_offset_y;
12962
- g_grid_ctx.fillStyle = g_bg_active ? isBlackKey ? "rgba(8,11,22,0.55)" : "rgba(17,22,40,0.45)" : isBlackKey ? "#080b16" : "#111628";
12963
- g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
12964
- if (isC4Range) {
12965
- g_grid_ctx.fillStyle = "rgba(41,173,255,0.05)";
12966
- g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
12967
- }
12968
- g_grid_ctx.beginPath();
12969
- g_grid_ctx.strokeStyle = isC ? "#3d405b" : "#1a1d30";
12970
- g_grid_ctx.lineWidth = 1;
12971
- const lineY = screenY + keyHeight;
12972
- g_grid_ctx.moveTo(0, lineY);
12973
- g_grid_ctx.lineTo(g_grid_canvas.width, lineY);
12974
- g_grid_ctx.stroke();
12975
- }
12976
- const gridStep = noteLengthSteps || 48;
12977
- const startX = Math.floor(g_draw_offset_x / (stepWidth * gridStep)) * stepWidth * gridStep;
12978
- const endX = g_draw_offset_x + g_grid_canvas.width;
12979
- const lineStep = stepWidth * gridStep;
12980
- for (let x2 = startX; x2 <= endX; x2 += lineStep) {
12981
- const step = x2 / stepWidth;
12982
- const isBarLine = step % stepsPerBar === 0;
12983
- const isNoteLine = step % gridStep === 0;
12984
- const screenX = x2 - g_draw_offset_x;
12985
- g_grid_ctx.beginPath();
12986
- g_grid_ctx.strokeStyle = isBarLine ? "#3d405b" : isNoteLine ? "#242840" : "#1a1d30";
12987
- g_grid_ctx.lineWidth = isBarLine ? 2 : 1;
12988
- g_grid_ctx.moveTo(screenX, 0);
12989
- g_grid_ctx.lineTo(screenX, g_grid_canvas.height);
12990
- g_grid_ctx.stroke();
12991
- }
12992
- };
12993
- var drawNotes = (notes, color = [59, 130, 246, 1]) => {
12994
- const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
12995
- for (const note of notes) {
12996
- const logicalX = note.startStep * stepWidth;
12997
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
12998
- const logicalY = yIndex * keyHeight;
12999
- const w = note.durationSteps * stepWidth;
13000
- const h = keyHeight;
13001
- const renderX = logicalX - g_draw_offset_x;
13002
- const renderY = logicalY - g_draw_offset_y;
13003
- const velocityOpacity = note.velocity !== void 0 ? 0.5 + note.velocity / 127 * 0.5 : 1;
13004
- const [r, g, b, a] = color;
13005
- const finalOpacity = a * velocityOpacity;
13006
- g_grid_ctx.fillStyle = `rgba(${r},${g},${b},${finalOpacity})`;
13007
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13008
- }
13009
- };
13010
- var drawNoteLyrics = (notes, syllables) => {
13011
- if (syllables.length === 0) return;
13012
- const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
13013
- if (keyHeight < 10) return;
13014
- const fontSize = Math.min(12, Math.floor(keyHeight * 0.85));
13015
- const sorted = [...notes].sort((a, b) => a.startStep - b.startStep);
13016
- const count = Math.min(sorted.length, syllables.length);
13017
- g_grid_ctx.save();
13018
- g_grid_ctx.font = `${fontSize}px 'k8x12',sans-serif`;
13019
- g_grid_ctx.textAlign = "left";
13020
- g_grid_ctx.textBaseline = "middle";
13021
- g_grid_ctx.lineWidth = 3;
13022
- g_grid_ctx.lineJoin = "round";
13023
- for (let i2 = 0; i2 < count; i2++) {
13024
- const kana = syllables[i2];
13025
- if (!kana) continue;
13026
- const note = sorted[i2];
13027
- const renderX = note.startStep * stepWidth - g_draw_offset_x;
13028
- const renderY = (keyCount - 1 - (note.pitch - pitchRangeStart)) * keyHeight - g_draw_offset_y;
13029
- const w = note.durationSteps * stepWidth;
13030
- if (w < 6) continue;
13031
- if (renderX + w < 0 || renderX > g_grid_canvas.width) continue;
13032
- if (renderY + keyHeight < 0 || renderY > g_grid_canvas.height) continue;
13033
- const textX = renderX + 2;
13034
- const textY = renderY + keyHeight / 2;
13035
- g_grid_ctx.save();
13036
- g_grid_ctx.beginPath();
13037
- g_grid_ctx.rect(renderX + 1, renderY + 1, w - 2, keyHeight - 2);
13038
- g_grid_ctx.clip();
13039
- g_grid_ctx.strokeStyle = "rgba(0,0,0,0.85)";
13040
- g_grid_ctx.strokeText(kana, textX, textY);
13041
- g_grid_ctx.fillStyle = "#fff1e8";
13042
- g_grid_ctx.fillText(kana, textX, textY);
13043
- g_grid_ctx.restore();
13044
- }
13045
- g_grid_ctx.restore();
13046
- };
13047
- var drawSelectionRect = (rect) => {
13048
- if (!rect) return;
13049
- g_grid_ctx.save();
13050
- g_grid_ctx.strokeStyle = "#ffec27";
13051
- g_grid_ctx.lineWidth = 2;
13052
- g_grid_ctx.setLineDash([4, 4]);
13053
- g_grid_ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
13054
- g_grid_ctx.fillStyle = "rgba(255,236,39,0.08)";
13055
- g_grid_ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
13056
- g_grid_ctx.restore();
13057
- };
13058
- var drawSelectedNotes = (notes, selectedIds, baseColor = [59, 130, 246, 1]) => {
13059
- const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
13060
- for (const note of notes) {
13061
- if (!selectedIds.has(note.id)) continue;
13062
- const logicalX = note.startStep * stepWidth;
13063
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
13064
- const logicalY = yIndex * keyHeight;
13065
- const w = note.durationSteps * stepWidth;
13066
- const h = keyHeight;
13067
- const renderX = logicalX - g_draw_offset_x;
13068
- const renderY = logicalY - g_draw_offset_y;
13069
- const velocityOpacity = note.velocity !== void 0 ? 0.5 + note.velocity / 127 * 0.5 : 1;
13070
- const [r, g, b, a] = baseColor;
13071
- const darkenFactor = 1.3;
13072
- const darkerR = Math.min(255, r * darkenFactor);
13073
- const darkerG = Math.min(255, g * darkenFactor);
13074
- const darkerB = Math.min(255, b * darkenFactor);
13075
- const finalOpacity = a * velocityOpacity;
13076
- g_grid_ctx.fillStyle = `rgba(${darkerR},${darkerG},${darkerB},${finalOpacity})`;
13077
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13078
- }
13079
- };
13080
- var getXY = (e) => {
13081
- const { clientX, clientY } = e;
13082
- const rect = g_grid_canvas.getBoundingClientRect();
13083
- const x2 = Math.floor(clientX - rect.left);
13084
- const y = Math.floor(clientY - rect.top);
13085
- return [x2, y, e.buttons];
13086
- };
13087
- var getGridPosition = (e) => {
13088
- const [x2, y] = getXY(e);
13089
- const { keyCount, pitchRangeStart, keyHeight, stepWidth } = g_config;
13090
- const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
13091
- const absoluteY = y + g_draw_offset_y;
13092
- const yIndex = Math.floor(absoluteY / keyHeight);
13093
- const pitch = keyCount - 1 - yIndex + pitchRangeStart;
13094
- return { step, pitch, x: x2, y };
13095
- };
13096
- var onClick = (callback) => {
13097
- g_grid_canvas.addEventListener(
13098
- "click",
13099
- (e) => {
13100
- const [x2, y] = getXY(e);
13101
- const { keyCount, pitchRangeStart, keyHeight, stepWidth } = g_config;
13102
- const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
13103
- const absoluteY = y + g_draw_offset_y;
13104
- const yIndex = Math.floor(absoluteY / keyHeight);
13105
- const pitch = keyCount - 1 - yIndex + pitchRangeStart;
13106
- if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount) {
13107
- requestAnimationFrame(() => callback(step, pitch));
13108
- }
13109
- },
13110
- { passive: true }
13111
- );
13112
- g_grid_canvas.addEventListener("contextmenu", (e) => e.preventDefault());
13113
- };
13114
- var setDrawOffset = (x2, y) => {
13115
- g_draw_offset_x = x2;
13116
- g_draw_offset_y = y;
13117
- drawKeyboard();
13118
- drawHeader();
13119
- };
13120
-
13121
13052
  // src/mml-core.ts
13122
- var PITCH_MAP2 = [
13053
+ var PITCH_MAP = [
13123
13054
  "c",
13124
13055
  "c+",
13125
13056
  "d",
@@ -13133,6 +13064,39 @@ var PITCH_MAP2 = [
13133
13064
  "a+",
13134
13065
  "b"
13135
13066
  ];
13067
+ var EDO31_NAMES = [
13068
+ "c",
13069
+ "c+",
13070
+ "c#",
13071
+ "d-",
13072
+ "d_",
13073
+ "d",
13074
+ "d+",
13075
+ "d#",
13076
+ "e-",
13077
+ "e_",
13078
+ "e",
13079
+ "f-",
13080
+ "e#",
13081
+ "f",
13082
+ "f+",
13083
+ "f#",
13084
+ "g-",
13085
+ "g_",
13086
+ "g",
13087
+ "g+",
13088
+ "g#",
13089
+ "a-",
13090
+ "a_",
13091
+ "a",
13092
+ "a+",
13093
+ "a#",
13094
+ "b-",
13095
+ "b_",
13096
+ "b",
13097
+ "b+",
13098
+ "b#"
13099
+ ];
13136
13100
  var MMLCore = class _MMLCore {
13137
13101
  notes = [];
13138
13102
  nextNoteId = 0;
@@ -13146,9 +13110,12 @@ var MMLCore = class _MMLCore {
13146
13110
  lastUndoTime = 0;
13147
13111
  static UNDO_DEBOUNCE_MS = 100;
13148
13112
  toolMode = "pen";
13149
- constructor(handlers, volume = 80) {
13113
+ /** グリッド寸法の供給元(描画器ではなく呼び出し側が持つ設定を読む) */
13114
+ getConfig;
13115
+ constructor(handlers, volume = 80, getConfig) {
13150
13116
  this.handlers = handlers;
13151
13117
  this.volume = volume;
13118
+ this.getConfig = getConfig;
13152
13119
  this.lastHistorySnapshot = JSON.stringify(this.notes);
13153
13120
  this.history.add([]);
13154
13121
  this.generateAndNotify();
@@ -13235,14 +13202,14 @@ var MMLCore = class _MMLCore {
13235
13202
  */
13236
13203
  addNote(step, pitch, options) {
13237
13204
  const existingIndex = this.notes.findIndex(
13238
- (n) => n.startStep === step && n.pitch === pitch
13205
+ (n) => n.startStep === step && n.pitchUnits === pitch
13239
13206
  );
13240
13207
  if (existingIndex === -1) {
13241
13208
  const newNote = {
13242
13209
  id: this.nextNoteId++,
13243
13210
  startStep: step,
13244
13211
  durationSteps: options.noteLengthSteps,
13245
- pitch,
13212
+ pitchUnits: pitch,
13246
13213
  velocity: options.velocity ?? DEFAULT_VELOCITY
13247
13214
  };
13248
13215
  this.notes.push(newNote);
@@ -13270,9 +13237,11 @@ var MMLCore = class _MMLCore {
13270
13237
  moveNote(noteId, startStep, pitch) {
13271
13238
  const note = this.notes.find((target) => target.id === noteId);
13272
13239
  if (!note) return;
13273
- const totalSteps = this.getMaxStep() + getRenderConfig().stepsPerBar;
13274
- const pitchRangeStart = getRenderConfig().pitchRangeStart;
13275
- const pitchRangeEnd = pitchRangeStart + getRenderConfig().keyCount - 1;
13240
+ const totalSteps = this.getMaxStep() + this.getConfig().stepsPerBar;
13241
+ const cfg = this.getConfig();
13242
+ const upr = cfg.unitsPerRow ?? UNITS_PER_SEMITONE;
13243
+ const pitchRangeStart = cfg.pitchRangeStart;
13244
+ const pitchRangeEnd = pitchRangeStart + (cfg.keyCount - 1) * upr;
13276
13245
  const clampedPitch = Math.min(
13277
13246
  Math.max(pitch, pitchRangeStart),
13278
13247
  pitchRangeEnd
@@ -13282,7 +13251,7 @@ var MMLCore = class _MMLCore {
13282
13251
  totalSteps - note.durationSteps
13283
13252
  );
13284
13253
  note.startStep = clampedStart;
13285
- note.pitch = clampedPitch;
13254
+ note.pitchUnits = clampedPitch;
13286
13255
  this.notes.sort((a, b) => a.startStep - b.startStep);
13287
13256
  this.generateAndNotify();
13288
13257
  }
@@ -13327,7 +13296,7 @@ var MMLCore = class _MMLCore {
13327
13296
  * ただし、残りステップ(limit)は絶対に超えない。
13328
13297
  */
13329
13298
  stepsToMMLDuration(steps, limit) {
13330
- const config = getRenderConfig();
13299
+ const config = this.getConfig();
13331
13300
  const total = config.stepsPerBar;
13332
13301
  const candidates = [
13333
13302
  { dur: "1.", s: total * 1.5 },
@@ -13363,7 +13332,7 @@ var MMLCore = class _MMLCore {
13363
13332
  * ギャップに収まる最大の音符を探す(減算アルゴリズム用)
13364
13333
  */
13365
13334
  findBestFitDuration(gap) {
13366
- const config = getRenderConfig();
13335
+ const config = this.getConfig();
13367
13336
  const durations = [1, 2, 4, 8, 12, 16, 24, 32, 48, 64];
13368
13337
  for (const d of durations) {
13369
13338
  const stepLen = config.stepsPerBar / d;
@@ -13373,12 +13342,20 @@ var MMLCore = class _MMLCore {
13373
13342
  }
13374
13343
  return { dur: 64, steps: config.stepsPerBar / 64 };
13375
13344
  }
13345
+ /** ピッチ(units) → その音律での「オクターブ番号」と「音名」。 */
13346
+ spell(pitchUnits) {
13347
+ const edo = this.getConfig().edo ?? 12;
13348
+ const names = edo === 31 ? EDO31_NAMES : PITCH_MAP;
13349
+ const octave = Math.floor(pitchUnits / 372) - 1;
13350
+ const within = (pitchUnits % 372 + 372) % 372;
13351
+ const step = Math.round(within / (372 / edo));
13352
+ return { octave, name: names[step % edo] };
13353
+ }
13376
13354
  /**
13377
13355
  * ピッチからオクターブ最適化のある音名を取得
13378
13356
  */
13379
13357
  getNoteWithOctave(pitch, lastOctave) {
13380
- const octave = Math.floor(pitch / 12) - 1;
13381
- const name = PITCH_MAP2[pitch % 12];
13358
+ const { octave, name } = this.spell(pitch);
13382
13359
  if (lastOctave === -1 || Math.abs(octave - lastOctave) >= 2) {
13383
13360
  return { text: `o${octave}${name}`, currentOctave: octave };
13384
13361
  }
@@ -13401,7 +13378,7 @@ var MMLCore = class _MMLCore {
13401
13378
  * 各音符の長さを忠実に出力する(次の音符がなければ曲末まで伸ばせる)。
13402
13379
  */
13403
13380
  generateMML = (volumeOverride) => {
13404
- const config = getRenderConfig();
13381
+ const config = this.getConfig();
13405
13382
  const vol = volumeOverride ?? this.volume;
13406
13383
  const header = `t${this.tempo} v${vol}`;
13407
13384
  const segments = [];
@@ -13442,14 +13419,13 @@ var MMLCore = class _MMLCore {
13442
13419
  const actualStepGenerated = this.getStepFromDottedMML(durStr);
13443
13420
  if (notes.length > 1) {
13444
13421
  const noteStrs = notes.map((n) => {
13445
- const oct = Math.floor(n.pitch / 12) - 1;
13446
- const name = PITCH_MAP2[n.pitch % 12];
13422
+ const { octave: oct, name } = this.spell(n.pitchUnits);
13447
13423
  return `o${oct}${name}`;
13448
13424
  });
13449
13425
  segments.push(`[${noteStrs.join("")}]${durStr}`);
13450
13426
  } else {
13451
13427
  const { text, currentOctave } = this.getNoteWithOctave(
13452
- notes[0].pitch,
13428
+ notes[0].pitchUnits,
13453
13429
  lastOctave
13454
13430
  );
13455
13431
  segments.push(`${text}${durStr}`);
@@ -13480,7 +13456,7 @@ var MMLCore = class _MMLCore {
13480
13456
  * MMLの音長文字列("4", "4.", "12"など)をステップ数に変換する
13481
13457
  */
13482
13458
  getStepFromDottedMML(durStr) {
13483
- const config = getRenderConfig();
13459
+ const config = this.getConfig();
13484
13460
  const total = config.stepsPerBar;
13485
13461
  const isDotted = durStr.endsWith(".");
13486
13462
  const baseDur = parseInt(isDotted ? durStr.slice(0, -1) : durStr, 10);
@@ -13490,7 +13466,7 @@ var MMLCore = class _MMLCore {
13490
13466
  };
13491
13467
  var decomposeToMonophonic = (notes) => {
13492
13468
  const sorted = [...notes].sort(
13493
- (a, b) => a.startStep - b.startStep || a.pitch - b.pitch
13469
+ (a, b) => a.startStep - b.startStep || a.pitchUnits - b.pitchUnits
13494
13470
  );
13495
13471
  const tracks = [];
13496
13472
  const trackEnds = [];
@@ -13525,6 +13501,493 @@ var isChordHeavyTrack = (notes, threshold = 0.6) => {
13525
13501
  return chordNotes / notes.length >= threshold;
13526
13502
  };
13527
13503
 
13504
+ // src/renderer.ts
13505
+ var KEYBOARD_WIDTH = 60;
13506
+ var HEADER_HEIGHT = 20;
13507
+ var NATURAL_STEPS3 = {
13508
+ 12: [0, 2, 4, 5, 7, 9, 11],
13509
+ 31: [0, 5, 10, 13, 18, 23, 28]
13510
+ };
13511
+ var keyTier = (step, edo) => {
13512
+ const naturals = NATURAL_STEPS3[edo] ?? NATURAL_STEPS3[12];
13513
+ let best = edo;
13514
+ for (const n of naturals) {
13515
+ const d = Math.abs(step - n);
13516
+ best = Math.min(best, d, edo - d);
13517
+ }
13518
+ if (best === 0) return 0;
13519
+ return best >= (edo === 31 ? 2 : 1) ? 2 : 1;
13520
+ };
13521
+ var KEY_NAMES = [
13522
+ "C",
13523
+ "C#",
13524
+ "D",
13525
+ "D#",
13526
+ "E",
13527
+ "F",
13528
+ "F#",
13529
+ "G",
13530
+ "G#",
13531
+ "A",
13532
+ "A#",
13533
+ "B"
13534
+ ];
13535
+ var createRenderer = (mountTarget, width = 800, height = 450, config) => {
13536
+ let g_header_canvas;
13537
+ let g_key_canvas;
13538
+ let g_grid_canvas;
13539
+ let g_header_ctx;
13540
+ let g_key_ctx;
13541
+ let g_grid_ctx;
13542
+ let g_config = config;
13543
+ const getRenderConfig = () => g_config;
13544
+ let g_draw_offset_x = 0;
13545
+ let g_draw_offset_y = 0;
13546
+ let g_bg_active = false;
13547
+ const setBackgroundActive = (active) => {
13548
+ g_bg_active = active;
13549
+ };
13550
+ const getDrawOffset = () => ({
13551
+ x: g_draw_offset_x,
13552
+ y: g_draw_offset_y
13553
+ });
13554
+ const getGridCanvas = () => g_grid_canvas;
13555
+ const getGridContext = () => g_grid_ctx;
13556
+ const getHeaderCanvas = () => g_header_canvas;
13557
+ const setup = () => {
13558
+ g_config = config;
13559
+ const headerCanvas = document.createElement("canvas");
13560
+ g_header_canvas = headerCanvas;
13561
+ headerCanvas.width = width - KEYBOARD_WIDTH;
13562
+ headerCanvas.height = HEADER_HEIGHT;
13563
+ headerCanvas.style.position = "absolute";
13564
+ headerCanvas.style.left = `${KEYBOARD_WIDTH}px`;
13565
+ headerCanvas.style.top = "0px";
13566
+ const headerCtx = headerCanvas.getContext("2d");
13567
+ if (!headerCtx)
13568
+ throw new Error("Failed to get 2D rendering context for header.");
13569
+ g_header_ctx = headerCtx;
13570
+ const keyCanvas = document.createElement("canvas");
13571
+ g_key_canvas = keyCanvas;
13572
+ keyCanvas.width = KEYBOARD_WIDTH;
13573
+ keyCanvas.height = height - HEADER_HEIGHT;
13574
+ keyCanvas.style.position = "absolute";
13575
+ keyCanvas.style.left = "0px";
13576
+ keyCanvas.style.top = `${HEADER_HEIGHT}px`;
13577
+ const keyCtx = keyCanvas.getContext("2d");
13578
+ if (!keyCtx)
13579
+ throw new Error("Failed to get 2D rendering context for keyboard.");
13580
+ g_key_ctx = keyCtx;
13581
+ const gridCanvas = document.createElement("canvas");
13582
+ g_grid_canvas = gridCanvas;
13583
+ gridCanvas.width = width - KEYBOARD_WIDTH;
13584
+ gridCanvas.height = height - HEADER_HEIGHT;
13585
+ gridCanvas.style.position = "absolute";
13586
+ gridCanvas.style.left = `${KEYBOARD_WIDTH}px`;
13587
+ gridCanvas.style.top = `${HEADER_HEIGHT}px`;
13588
+ gridCanvas.style.touchAction = "none";
13589
+ gridCanvas.style.userSelect = "none";
13590
+ const gridCtx = gridCanvas.getContext("2d", { willReadFrequently: true });
13591
+ if (!gridCtx)
13592
+ throw new Error("Failed to get 2D rendering context for grid.");
13593
+ g_grid_ctx = gridCtx;
13594
+ mountTarget.innerHTML = "";
13595
+ mountTarget.style.position = "relative";
13596
+ mountTarget.style.width = `${width + KEYBOARD_WIDTH}px`;
13597
+ mountTarget.style.height = `${height}px`;
13598
+ mountTarget.append(headerCanvas, keyCanvas, gridCanvas);
13599
+ drawHeaderCorner();
13600
+ };
13601
+ const drawHeaderCorner = () => {
13602
+ const mountTarget2 = g_key_canvas.parentElement;
13603
+ if (!mountTarget2) return;
13604
+ let cornerDiv = mountTarget2.querySelector(
13605
+ "#header-corner"
13606
+ );
13607
+ if (!cornerDiv) {
13608
+ cornerDiv = document.createElement("div");
13609
+ cornerDiv.id = "header-corner";
13610
+ cornerDiv.style.position = "absolute";
13611
+ cornerDiv.style.left = "0px";
13612
+ cornerDiv.style.top = "0px";
13613
+ cornerDiv.style.width = `${KEYBOARD_WIDTH}px`;
13614
+ cornerDiv.style.height = `${HEADER_HEIGHT}px`;
13615
+ cornerDiv.style.backgroundColor = "#0a0f1f";
13616
+ cornerDiv.style.borderRight = "2px solid #29adff";
13617
+ cornerDiv.style.borderBottom = "2px solid #29adff";
13618
+ mountTarget2.insertBefore(cornerDiv, g_header_canvas);
13619
+ }
13620
+ };
13621
+ const drawKeyboard = () => {
13622
+ g_key_ctx.clearRect(0, 0, g_key_canvas.width, g_key_canvas.height);
13623
+ const {
13624
+ keyHeight,
13625
+ keyCount,
13626
+ pitchRangeStart,
13627
+ unitsPerRow: upr = UNITS_PER_SEMITONE,
13628
+ edo = 12
13629
+ } = g_config;
13630
+ const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
13631
+ const endY = g_draw_offset_y + g_key_canvas.height;
13632
+ const WHITE_KEY = "#ccc8b4";
13633
+ const BLACK_KEY = "#111111";
13634
+ const MICRO_KEY = "#4a4a4a";
13635
+ const BK_EDGE = "#383838";
13636
+ const WW_SEP = "#807a6a";
13637
+ const BK_RATIO = 0.62;
13638
+ const MICRO_RATIO = 0.45;
13639
+ for (let y = startY; y < endY; y += keyHeight) {
13640
+ const rowIndex = keyCount - 1 - y / keyHeight;
13641
+ const units = pitchRangeStart + rowIndex * upr;
13642
+ const step = Math.round(
13643
+ (units % UNITS_PER_OCTAVE + UNITS_PER_OCTAVE) % UNITS_PER_OCTAVE / upr % edo
13644
+ );
13645
+ const tier = keyTier(step, edo);
13646
+ const octave = Math.floor(units / UNITS_PER_OCTAVE) - 1;
13647
+ const isC4Range = octave === 4;
13648
+ const screenY = y - g_draw_offset_y;
13649
+ const bkW = Math.floor(
13650
+ KEYBOARD_WIDTH * (tier === 1 ? MICRO_RATIO : BK_RATIO)
13651
+ );
13652
+ if (tier !== 0) {
13653
+ g_key_ctx.fillStyle = isC4Range ? "#d8d4be" : WHITE_KEY;
13654
+ g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
13655
+ g_key_ctx.fillStyle = tier === 1 ? MICRO_KEY : isC4Range ? "#1a1408" : BLACK_KEY;
13656
+ g_key_ctx.fillRect(0, screenY, bkW, keyHeight);
13657
+ g_key_ctx.strokeStyle = BK_EDGE;
13658
+ g_key_ctx.lineWidth = 1;
13659
+ g_key_ctx.beginPath();
13660
+ g_key_ctx.moveTo(bkW, screenY);
13661
+ g_key_ctx.lineTo(bkW, screenY + keyHeight);
13662
+ g_key_ctx.stroke();
13663
+ } else {
13664
+ g_key_ctx.fillStyle = isC4Range ? "#dedad0" : WHITE_KEY;
13665
+ g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
13666
+ if (edo === 12 && (step === 5 || step === 0)) {
13667
+ g_key_ctx.strokeStyle = WW_SEP;
13668
+ g_key_ctx.lineWidth = 1;
13669
+ g_key_ctx.beginPath();
13670
+ g_key_ctx.moveTo(0, screenY + keyHeight - 0.5);
13671
+ g_key_ctx.lineTo(KEYBOARD_WIDTH, screenY + keyHeight - 0.5);
13672
+ g_key_ctx.stroke();
13673
+ }
13674
+ }
13675
+ if (step === 0) {
13676
+ g_key_ctx.fillStyle = "#555040";
13677
+ g_key_ctx.font = "10px 'k8x12',monospace";
13678
+ g_key_ctx.textAlign = "right";
13679
+ g_key_ctx.textBaseline = "bottom";
13680
+ g_key_ctx.fillText(
13681
+ `${KEY_NAMES[0]}${octave}`,
13682
+ KEYBOARD_WIDTH - 4,
13683
+ screenY + keyHeight - 2
13684
+ );
13685
+ }
13686
+ }
13687
+ g_key_ctx.beginPath();
13688
+ g_key_ctx.strokeStyle = "#29adff";
13689
+ g_key_ctx.lineWidth = 2;
13690
+ g_key_ctx.moveTo(KEYBOARD_WIDTH, 0);
13691
+ g_key_ctx.lineTo(KEYBOARD_WIDTH, g_key_canvas.height);
13692
+ g_key_ctx.stroke();
13693
+ };
13694
+ const drawHeader = () => {
13695
+ g_header_ctx.clearRect(0, 0, g_header_canvas.width, g_header_canvas.height);
13696
+ const { stepWidth, stepsPerBar } = g_config;
13697
+ g_header_ctx.save();
13698
+ g_header_ctx.translate(-g_draw_offset_x, 0);
13699
+ g_header_ctx.fillStyle = g_bg_active ? "rgba(10,15,31,0.55)" : "#0a0f1f";
13700
+ g_header_ctx.fillRect(
13701
+ g_draw_offset_x,
13702
+ 0,
13703
+ g_header_canvas.width,
13704
+ HEADER_HEIGHT
13705
+ );
13706
+ g_header_ctx.strokeStyle = "#3d405b";
13707
+ g_header_ctx.lineWidth = 1;
13708
+ g_header_ctx.font = "11px 'k8x12',monospace";
13709
+ g_header_ctx.fillStyle = "#83769c";
13710
+ const startBar = Math.floor(g_draw_offset_x / (stepsPerBar * stepWidth));
13711
+ const endBar = Math.ceil(
13712
+ (g_draw_offset_x + g_header_canvas.width) / (stepsPerBar * stepWidth)
13713
+ );
13714
+ for (let bar = startBar; bar <= endBar + 1; bar++) {
13715
+ const x2 = bar * stepsPerBar * stepWidth;
13716
+ const screenX = x2;
13717
+ g_header_ctx.beginPath();
13718
+ g_header_ctx.moveTo(screenX, 0);
13719
+ g_header_ctx.lineTo(screenX, HEADER_HEIGHT);
13720
+ g_header_ctx.stroke();
13721
+ if (bar >= 0) {
13722
+ g_header_ctx.textAlign = "left";
13723
+ g_header_ctx.textBaseline = "middle";
13724
+ g_header_ctx.fillText(`${bar + 1}`, screenX + 5, HEADER_HEIGHT / 2);
13725
+ }
13726
+ }
13727
+ g_header_ctx.restore();
13728
+ };
13729
+ const drawGrid = (noteLengthSteps = 1) => {
13730
+ drawKeyboard();
13731
+ drawHeader();
13732
+ g_grid_ctx.clearRect(0, 0, g_grid_canvas.width, g_grid_canvas.height);
13733
+ const {
13734
+ keyHeight,
13735
+ keyCount,
13736
+ stepWidth,
13737
+ stepsPerBar,
13738
+ pitchRangeStart,
13739
+ unitsPerRow: upr = UNITS_PER_SEMITONE,
13740
+ edo = 12
13741
+ } = g_config;
13742
+ const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
13743
+ const endY = g_draw_offset_y + g_grid_canvas.height;
13744
+ for (let y = startY; y < endY; y += keyHeight) {
13745
+ const rowIndex = keyCount - 1 - y / keyHeight;
13746
+ const units = pitchRangeStart + rowIndex * upr;
13747
+ const step = Math.round(
13748
+ (units % UNITS_PER_OCTAVE + UNITS_PER_OCTAVE) % UNITS_PER_OCTAVE / upr % edo
13749
+ );
13750
+ const tier = keyTier(step, edo);
13751
+ const isC = step === 0;
13752
+ const octave = Math.floor(units / UNITS_PER_OCTAVE) - 1;
13753
+ const isC4Range = octave === 4;
13754
+ const screenY = y - g_draw_offset_y;
13755
+ 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";
13756
+ g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13757
+ if (isC4Range) {
13758
+ g_grid_ctx.fillStyle = "rgba(41,173,255,0.05)";
13759
+ g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13760
+ }
13761
+ g_grid_ctx.beginPath();
13762
+ g_grid_ctx.strokeStyle = isC ? "#3d405b" : "#1a1d30";
13763
+ g_grid_ctx.lineWidth = 1;
13764
+ const lineY = screenY + keyHeight;
13765
+ g_grid_ctx.moveTo(0, lineY);
13766
+ g_grid_ctx.lineTo(g_grid_canvas.width, lineY);
13767
+ g_grid_ctx.stroke();
13768
+ }
13769
+ const gridStep = noteLengthSteps || 48;
13770
+ const startX = Math.floor(g_draw_offset_x / (stepWidth * gridStep)) * stepWidth * gridStep;
13771
+ const endX = g_draw_offset_x + g_grid_canvas.width;
13772
+ const lineStep = stepWidth * gridStep;
13773
+ for (let x2 = startX; x2 <= endX; x2 += lineStep) {
13774
+ const step = x2 / stepWidth;
13775
+ const isBarLine = step % stepsPerBar === 0;
13776
+ const isNoteLine = step % gridStep === 0;
13777
+ const screenX = x2 - g_draw_offset_x;
13778
+ g_grid_ctx.beginPath();
13779
+ g_grid_ctx.strokeStyle = isBarLine ? "#3d405b" : isNoteLine ? "#242840" : "#1a1d30";
13780
+ g_grid_ctx.lineWidth = isBarLine ? 2 : 1;
13781
+ g_grid_ctx.moveTo(screenX, 0);
13782
+ g_grid_ctx.lineTo(screenX, g_grid_canvas.height);
13783
+ g_grid_ctx.stroke();
13784
+ }
13785
+ };
13786
+ const drawNotes = (notes, color = [59, 130, 246, 1], isActive = true) => {
13787
+ const {
13788
+ keyHeight,
13789
+ stepWidth,
13790
+ keyCount,
13791
+ pitchRangeStart,
13792
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13793
+ } = g_config;
13794
+ const canvasWidth = g_grid_canvas.width;
13795
+ const canvasHeight = g_grid_canvas.height;
13796
+ const [r, g, b, a] = color;
13797
+ for (const note of notes) {
13798
+ const logicalX = note.startStep * stepWidth;
13799
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
13800
+ const logicalY = yIndex * keyHeight;
13801
+ const w = note.durationSteps * stepWidth;
13802
+ const h = keyHeight;
13803
+ const renderX = logicalX - g_draw_offset_x;
13804
+ const renderY = logicalY - g_draw_offset_y;
13805
+ if (renderX + w < 0 || renderX > canvasWidth) continue;
13806
+ if (renderY + h < 0 || renderY > canvasHeight) continue;
13807
+ if (isActive) {
13808
+ const velocityOpacity = note.velocity !== void 0 ? 0.6 + note.velocity / 127 * 0.4 : 1;
13809
+ const finalOpacity = a * velocityOpacity;
13810
+ g_grid_ctx.fillStyle = `rgba(${r},${g},${b},${finalOpacity})`;
13811
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13812
+ if (w >= 4 && h >= 4) {
13813
+ g_grid_ctx.fillStyle = "rgba(255,255,255,0.4)";
13814
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, 1);
13815
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, 1, h - 2);
13816
+ g_grid_ctx.fillStyle = "rgba(0,0,0,0.45)";
13817
+ g_grid_ctx.fillRect(renderX + 1, renderY + h - 2, w - 2, 1);
13818
+ g_grid_ctx.fillRect(renderX + w - 2, renderY + 1, 1, h - 2);
13819
+ }
13820
+ } else {
13821
+ const velocityOpacity = note.velocity !== void 0 ? 0.7 + note.velocity / 127 * 0.3 : 1;
13822
+ const finalOpacity = Math.min(0.25, a * 0.22) * velocityOpacity;
13823
+ const gray = 0.299 * r + 0.587 * g + 0.114 * b;
13824
+ const ghostR = Math.round(r * 0.6 + gray * 0.4);
13825
+ const ghostG = Math.round(g * 0.6 + gray * 0.4);
13826
+ const ghostB = Math.round(b * 0.6 + gray * 0.4);
13827
+ g_grid_ctx.fillStyle = `rgba(${ghostR},${ghostG},${ghostB},${finalOpacity})`;
13828
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13829
+ }
13830
+ }
13831
+ };
13832
+ const drawNoteLyrics = (notes, syllables) => {
13833
+ if (syllables.length === 0) return;
13834
+ const {
13835
+ keyHeight,
13836
+ stepWidth,
13837
+ keyCount,
13838
+ pitchRangeStart,
13839
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13840
+ } = g_config;
13841
+ if (keyHeight < 7) return;
13842
+ const fontSize = Math.min(12, Math.floor(keyHeight * 0.85));
13843
+ const sorted = [...notes].sort((a, b) => a.startStep - b.startStep);
13844
+ const count = Math.min(sorted.length, syllables.length);
13845
+ g_grid_ctx.save();
13846
+ g_grid_ctx.font = `${fontSize}px 'k8x12',sans-serif`;
13847
+ g_grid_ctx.textAlign = "left";
13848
+ g_grid_ctx.textBaseline = "middle";
13849
+ g_grid_ctx.lineWidth = 3;
13850
+ g_grid_ctx.lineJoin = "round";
13851
+ for (let i2 = 0; i2 < count; i2++) {
13852
+ const kana = syllables[i2];
13853
+ if (!kana) continue;
13854
+ const note = sorted[i2];
13855
+ const renderX = note.startStep * stepWidth - g_draw_offset_x;
13856
+ const renderY = (keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr) * keyHeight - g_draw_offset_y;
13857
+ const w = note.durationSteps * stepWidth;
13858
+ if (w < 6) continue;
13859
+ if (renderX + w < 0 || renderX > g_grid_canvas.width) continue;
13860
+ if (renderY + keyHeight < 0 || renderY > g_grid_canvas.height) continue;
13861
+ const textX = renderX + 2;
13862
+ const textY = renderY + keyHeight / 2;
13863
+ g_grid_ctx.save();
13864
+ g_grid_ctx.beginPath();
13865
+ g_grid_ctx.rect(renderX + 1, renderY + 1, w - 2, keyHeight - 2);
13866
+ g_grid_ctx.clip();
13867
+ g_grid_ctx.strokeStyle = "rgba(0,0,0,0.85)";
13868
+ g_grid_ctx.strokeText(kana, textX, textY);
13869
+ g_grid_ctx.fillStyle = "#fff1e8";
13870
+ g_grid_ctx.fillText(kana, textX, textY);
13871
+ g_grid_ctx.restore();
13872
+ }
13873
+ g_grid_ctx.restore();
13874
+ };
13875
+ const drawSelectionRect = (rect) => {
13876
+ if (!rect) return;
13877
+ g_grid_ctx.save();
13878
+ g_grid_ctx.strokeStyle = "#ffec27";
13879
+ g_grid_ctx.lineWidth = 2;
13880
+ g_grid_ctx.setLineDash([4, 4]);
13881
+ g_grid_ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
13882
+ g_grid_ctx.fillStyle = "rgba(255,236,39,0.08)";
13883
+ g_grid_ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
13884
+ g_grid_ctx.restore();
13885
+ };
13886
+ const drawSelectedNotes = (notes, selectedIds, baseColor = [59, 130, 246, 1]) => {
13887
+ const {
13888
+ keyHeight,
13889
+ stepWidth,
13890
+ keyCount,
13891
+ pitchRangeStart,
13892
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13893
+ } = g_config;
13894
+ for (const note of notes) {
13895
+ if (!selectedIds.has(note.id)) continue;
13896
+ const logicalX = note.startStep * stepWidth;
13897
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
13898
+ const logicalY = yIndex * keyHeight;
13899
+ const w = note.durationSteps * stepWidth;
13900
+ const h = keyHeight;
13901
+ const renderX = logicalX - g_draw_offset_x;
13902
+ const renderY = logicalY - g_draw_offset_y;
13903
+ const velocityOpacity = note.velocity !== void 0 ? 0.5 + note.velocity / 127 * 0.5 : 1;
13904
+ const [r, g, b, a] = baseColor;
13905
+ const darkenFactor = 1.3;
13906
+ const darkerR = Math.min(255, r * darkenFactor);
13907
+ const darkerG = Math.min(255, g * darkenFactor);
13908
+ const darkerB = Math.min(255, b * darkenFactor);
13909
+ const finalOpacity = a * velocityOpacity;
13910
+ g_grid_ctx.fillStyle = `rgba(${darkerR},${darkerG},${darkerB},${finalOpacity})`;
13911
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13912
+ }
13913
+ };
13914
+ const getXY = (e) => {
13915
+ const { clientX, clientY } = e;
13916
+ const rect = g_grid_canvas.getBoundingClientRect();
13917
+ const x2 = Math.floor(clientX - rect.left);
13918
+ const y = Math.floor(clientY - rect.top);
13919
+ return [x2, y, e.buttons];
13920
+ };
13921
+ const getGridPosition = (e) => {
13922
+ const [x2, y] = getXY(e);
13923
+ const {
13924
+ keyCount,
13925
+ pitchRangeStart,
13926
+ keyHeight,
13927
+ stepWidth,
13928
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13929
+ } = g_config;
13930
+ const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
13931
+ const absoluteY = y + g_draw_offset_y;
13932
+ const yIndex = Math.floor(absoluteY / keyHeight);
13933
+ const pitch = pitchRangeStart + (keyCount - 1 - yIndex) * upr;
13934
+ return { step, pitch, x: x2, y };
13935
+ };
13936
+ const onClick = (callback) => {
13937
+ g_grid_canvas.addEventListener(
13938
+ "click",
13939
+ (e) => {
13940
+ const [x2, y] = getXY(e);
13941
+ const {
13942
+ keyCount,
13943
+ pitchRangeStart,
13944
+ keyHeight,
13945
+ stepWidth,
13946
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13947
+ } = g_config;
13948
+ const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
13949
+ const absoluteY = y + g_draw_offset_y;
13950
+ const yIndex = Math.floor(absoluteY / keyHeight);
13951
+ const pitch = pitchRangeStart + (keyCount - 1 - yIndex) * upr;
13952
+ if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount) {
13953
+ requestAnimationFrame(() => callback(step, pitch));
13954
+ }
13955
+ },
13956
+ { passive: true }
13957
+ );
13958
+ g_grid_canvas.addEventListener("contextmenu", (e) => e.preventDefault());
13959
+ };
13960
+ const setDrawOffset = (x2, y) => {
13961
+ g_draw_offset_x = x2;
13962
+ g_draw_offset_y = y;
13963
+ drawKeyboard();
13964
+ drawHeader();
13965
+ };
13966
+ setup();
13967
+ return {
13968
+ getRenderConfig,
13969
+ setBackgroundActive,
13970
+ getDrawOffset,
13971
+ getGridCanvas,
13972
+ getGridContext,
13973
+ getHeaderCanvas,
13974
+ drawKeyboard,
13975
+ drawHeader,
13976
+ drawGrid,
13977
+ drawNotes,
13978
+ drawNoteLyrics,
13979
+ drawSelectionRect,
13980
+ drawSelectedNotes,
13981
+ getXY,
13982
+ getGridPosition,
13983
+ onClick,
13984
+ setDrawOffset,
13985
+ destroy: () => {
13986
+ mountTarget.innerHTML = "";
13987
+ }
13988
+ };
13989
+ };
13990
+
13528
13991
  // src/daw.ts
13529
13992
  var CHORD_INFO_HTML2 = `
13530
13993
  <div class="dtm-modal-body-content">
@@ -13811,6 +14274,27 @@ var MIDI_INFO_HTML = `
13811
14274
  <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>
13812
14275
  </div>
13813
14276
  `;
14277
+ var EDO_INFO_HTML = `
14278
+ <div class="dtm-modal-section">
14279
+ <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>
14280
+ <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>
14281
+ <p style="margin-top:8px;"><strong>31\u5E73\u5747\u5F8B\u306E\u7279\u5FB4</strong></p>
14282
+ <ul>
14283
+ <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>
14284
+ <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>
14285
+ <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>
14286
+ </ul>
14287
+ <p style="margin-top:8px;"><strong>MML\u3067\u306E\u66F8\u304D\u65B9</strong></p>
14288
+ <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>
14289
+ <ul>
14290
+ <li><code>#</code> \u2026 \u534A\u97F3\u4E0A\u3052\uFF082\u5EA6\uFF09\u3000<code>-</code> \u2026 \u534A\u97F3\u4E0B\u3052\uFF082\u5EA6\uFF09</li>
14291
+ <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>
14292
+ </ul>
14293
+ <p>\u4F8B: <code>#edo=31 @0 o4 c c+ c# d- d_ d;</code></p>
14294
+ <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>
14295
+ <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>
14296
+ </div>
14297
+ `;
13814
14298
  var KOE_INFO_HTML = `
13815
14299
  <div class="dtm-modal-body-content">
13816
14300
  <h4>1. UTAU\u97F3\u6E90\u3092 .koe \u306B\u5909\u63DB\u3059\u308B</h4>
@@ -14028,7 +14512,7 @@ var AUTO_ROLE_BASE_VOLUME = {
14028
14512
  chord: 72
14029
14513
  };
14030
14514
  var computeAutoRoleStats = (notes) => {
14031
- const avgPitch = notes.reduce((s, n) => s + n.pitch, 0) / notes.length;
14515
+ const avgPitch = notes.reduce((s, n) => s + n.pitchUnits, 0) / notes.length;
14032
14516
  const avgDur = notes.reduce((s, n) => s + n.durationSteps, 0) / notes.length;
14033
14517
  const byStart = /* @__PURE__ */ new Map();
14034
14518
  for (const n of notes)
@@ -14042,8 +14526,9 @@ var computeAutoRoleStats = (notes) => {
14042
14526
  const notesPerBeat = notes.length / spanBeats;
14043
14527
  return { avgPitch, avgDur, avgPoly, maxPoly, notesPerBeat };
14044
14528
  };
14529
+ var AUTO_ROLE_BASS_UNITS = 48 * UNITS_PER_SEMITONE;
14045
14530
  var classifyTrackRole = (stats) => {
14046
- if (stats.avgPitch < 48) return "bass";
14531
+ if (stats.avgPitch < AUTO_ROLE_BASS_UNITS) return "bass";
14047
14532
  if (stats.maxPoly >= 2 && stats.avgDur >= AUTO_ROLE_STEPS_PER_BEAT)
14048
14533
  return "chord";
14049
14534
  if (stats.notesPerBeat < 1.2 && stats.avgDur >= AUTO_ROLE_STEPS_PER_BEAT * 1.5)
@@ -14055,7 +14540,10 @@ var computeAutoRoleVolume = (role, stats) => {
14055
14540
  if (role === "chord" && stats.avgPoly > 1) {
14056
14541
  vol /= Math.sqrt(stats.avgPoly);
14057
14542
  } else if (role === "bass") {
14058
- const depthSemitones = Math.max(0, 48 - stats.avgPitch);
14543
+ const depthSemitones = Math.max(
14544
+ 0,
14545
+ (AUTO_ROLE_BASS_UNITS - stats.avgPitch) / UNITS_PER_SEMITONE
14546
+ );
14059
14547
  vol += Math.min(12, depthSemitones * 0.5);
14060
14548
  }
14061
14549
  return clamp5(Math.round(vol), 1, 127);
@@ -14147,17 +14635,48 @@ var mountDAW = (target, options = {}) => {
14147
14635
  refs.drumVolumeLabel.textContent = `${options.drumVolume ?? 80}%`;
14148
14636
  const renderConfig = {
14149
14637
  stepsPerBar: 192,
14150
- keyCount: 128,
14151
- pitchRangeStart: 0,
14638
+ keyCount: KEY_COUNT,
14639
+ pitchRangeStart: PITCH_RANGE_START,
14640
+ unitsPerRow: unitsPerRow(12),
14641
+ edo: 12,
14152
14642
  keyHeight: BASE_KEY_HEIGHT,
14153
14643
  stepWidth: BASE_STEP_WIDTH * 2
14154
14644
  // zoom100% 相当
14155
14645
  };
14646
+ let renderer;
14156
14647
  let zoomX = 100;
14157
14648
  let zoomY = 100;
14158
14649
  let bpm = options.defaultBpm ?? DEFAULT_BPM;
14159
14650
  let masterVolume = options.masterVolume ?? 50;
14160
- options.singingVoices?.setVolume(masterVolume / 100);
14651
+ const applyMasterVolume = (volume) => {
14652
+ masterVolume = clamp5(Math.round(volume), 0, 100);
14653
+ refs.masterVolume.value = String(masterVolume);
14654
+ refs.masterVolumeLabel.textContent = `${masterVolume}%`;
14655
+ options.singingVoices?.setVolume(masterVolume / 100);
14656
+ };
14657
+ applyMasterVolume(masterVolume);
14658
+ const snapToEdoGrid = (units) => {
14659
+ const upr = renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE;
14660
+ return Math.round(units / upr) * upr;
14661
+ };
14662
+ const applyEdo = (edo, opts) => {
14663
+ const next = edo === 31 ? 31 : 12;
14664
+ if (renderConfig.edo === next) return;
14665
+ const upr = unitsPerRow(next);
14666
+ renderConfig.edo = next;
14667
+ renderConfig.unitsPerRow = upr;
14668
+ renderConfig.keyCount = keyCountFor(next);
14669
+ if (!opts?.snap) return;
14670
+ for (const t of trackStates) {
14671
+ t.core.beginBatch();
14672
+ for (const note of [...t.core.getNotes()]) {
14673
+ const snapped = Math.round(note.pitchUnits / upr) * upr;
14674
+ if (snapped !== note.pitchUnits)
14675
+ t.core.moveNote(note.id, note.startStep, snapped);
14676
+ }
14677
+ t.core.endBatch();
14678
+ }
14679
+ };
14161
14680
  let reverbAmount = options.reverbAmount ?? 0;
14162
14681
  let reverbDecay = options.reverbDecay ?? DEFAULT_REVERB_DECAY_SEC;
14163
14682
  let reverbPreDelay = options.reverbPreDelay ?? DEFAULT_REVERB_PREDELAY_MS;
@@ -14215,8 +14734,8 @@ var mountDAW = (target, options = {}) => {
14215
14734
  let snapGridSteps = 12;
14216
14735
  const gridLineSteps = 48;
14217
14736
  let currentOffsetX = 0;
14218
- const _initPitch = options.initialScrollPitch ?? 48;
14219
- let currentOffsetY = (renderConfig.keyCount - 1 - _initPitch) * renderConfig.keyHeight - 215;
14737
+ const _initPitch = pitchV1ToUnits(options.initialScrollPitch ?? 48);
14738
+ let currentOffsetY = (renderConfig.keyCount - 1 - (_initPitch - renderConfig.pitchRangeStart) / (renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE)) * renderConfig.keyHeight - 215;
14220
14739
  let playStartStep = 0;
14221
14740
  let isSolo = false;
14222
14741
  let lyricTrackIndices = /* @__PURE__ */ new Set();
@@ -14311,21 +14830,28 @@ var mountDAW = (target, options = {}) => {
14311
14830
  if (!ready) return;
14312
14831
  if (!suppressPatch && options.onNotesPatch) {
14313
14832
  const prevByKey = new Map(
14314
- prevNotes.map((n) => [`${n.startStep}_${n.pitch}`, n])
14833
+ prevNotes.map((n) => [`${n.startStep}_${n.pitchUnits}`, n])
14315
14834
  );
14316
14835
  const currByKey = new Map(
14317
- notes.map((n) => [`${n.startStep}_${n.pitch}`, n])
14836
+ notes.map((n) => [`${n.startStep}_${n.pitchUnits}`, n])
14318
14837
  );
14319
14838
  const added = notes.filter((n) => {
14320
- const prev = prevByKey.get(`${n.startStep}_${n.pitch}`);
14839
+ const prev = prevByKey.get(
14840
+ `${n.startStep}_${n.pitchUnits}`
14841
+ );
14321
14842
  return !prev || prev.durationSteps !== n.durationSteps || prev.velocity !== n.velocity;
14322
14843
  }).map((n) => ({
14323
14844
  startStep: n.startStep,
14324
- pitch: n.pitch,
14845
+ pitchUnits: n.pitchUnits,
14325
14846
  durationSteps: n.durationSteps,
14326
14847
  velocity: n.velocity
14327
14848
  }));
14328
- const removed = prevNotes.filter((n) => !currByKey.has(`${n.startStep}_${n.pitch}`)).map((n) => ({ startStep: n.startStep, pitch: n.pitch }));
14849
+ const removed = prevNotes.filter(
14850
+ (n) => !currByKey.has(`${n.startStep}_${n.pitchUnits}`)
14851
+ ).map((n) => ({
14852
+ startStep: n.startStep,
14853
+ pitchUnits: n.pitchUnits
14854
+ }));
14329
14855
  if (added.length > 0 || removed.length > 0) {
14330
14856
  options.onNotesPatch(config.id, added, removed);
14331
14857
  }
@@ -14335,7 +14861,8 @@ var mountDAW = (target, options = {}) => {
14335
14861
  updateUndoRedo();
14336
14862
  }
14337
14863
  },
14338
- config.volume
14864
+ config.volume,
14865
+ () => renderConfig
14339
14866
  ),
14340
14867
  volume: config.volume,
14341
14868
  savedChordInput: "",
@@ -14430,18 +14957,18 @@ var mountDAW = (target, options = {}) => {
14430
14957
  return (lastNoteMeasure + 2) * renderConfig.stepsPerBar;
14431
14958
  };
14432
14959
  const getMaxOffsetX = () => {
14433
- const canvas = getGridCanvas();
14960
+ const canvas = renderer.getGridCanvas();
14434
14961
  const maxNoteStep = getMaxNoteStep();
14435
14962
  const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
14436
14963
  return Math.max(0, totalContentWidth - canvas.width);
14437
14964
  };
14438
14965
  const getMaxOffsetY = () => {
14439
14966
  const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
14440
- return Math.max(0, totalHeight - getGridCanvas().height);
14967
+ return Math.max(0, totalHeight - renderer.getGridCanvas().height);
14441
14968
  };
14442
14969
  const drawStartLine = () => {
14443
- const ctx = getGridContext();
14444
- const canvas = getGridCanvas();
14970
+ const ctx = renderer.getGridContext();
14971
+ const canvas = renderer.getGridCanvas();
14445
14972
  if (!ctx) return;
14446
14973
  const x2 = playStartStep * renderConfig.stepWidth - currentOffsetX;
14447
14974
  if (x2 < -10 || x2 > canvas.width + 10) return;
@@ -14456,8 +14983,8 @@ var mountDAW = (target, options = {}) => {
14456
14983
  ctx.restore();
14457
14984
  };
14458
14985
  const drawPlayhead = () => {
14459
- const ctx = getGridContext();
14460
- const canvas = getGridCanvas();
14986
+ const ctx = renderer.getGridContext();
14987
+ const canvas = renderer.getGridCanvas();
14461
14988
  if (!ctx) return;
14462
14989
  const x2 = currentPlayStep * renderConfig.stepWidth - currentOffsetX;
14463
14990
  if (x2 < 0 || x2 > canvas.width) return;
@@ -14471,20 +14998,28 @@ var mountDAW = (target, options = {}) => {
14471
14998
  ctx.restore();
14472
14999
  };
14473
15000
  const redrawAll = () => {
14474
- drawGrid(gridLineSteps);
15001
+ renderer.drawGrid(gridLineSteps);
14475
15002
  let lyricTargetNotes = null;
15003
+ let activeTrackState = null;
14476
15004
  for (const t of trackStates) {
14477
15005
  if (hiddenTracks.has(t.config.id)) continue;
14478
15006
  if (isSolo && t.config.id !== activeTrackId) continue;
15007
+ if (t.config.id === activeTrackId) {
15008
+ activeTrackState = t;
15009
+ continue;
15010
+ }
14479
15011
  const [r, g, b] = t.config.color;
14480
- const isActive = t.config.id === activeTrackId;
14481
- const a = isActive ? 1 : 0.3;
14482
15012
  const notes = t.core.getNotes();
14483
- drawNotes(notes, [r, g, b, a]);
14484
- if (isActive) lyricTargetNotes = notes;
15013
+ renderer.drawNotes(notes, [r, g, b, 1], false);
15014
+ }
15015
+ if (activeTrackState) {
15016
+ const [r, g, b] = activeTrackState.config.color;
15017
+ const notes = activeTrackState.core.getNotes();
15018
+ renderer.drawNotes(notes, [r, g, b, 1], true);
15019
+ lyricTargetNotes = notes;
14485
15020
  }
14486
15021
  if (activeToolMode === "select" && selectionRect) {
14487
- const ctx = getGridContext();
15022
+ const ctx = renderer.getGridContext();
14488
15023
  ctx.save();
14489
15024
  ctx.strokeStyle = "#ffec27";
14490
15025
  ctx.lineWidth = 2;
@@ -14507,19 +15042,19 @@ var mountDAW = (target, options = {}) => {
14507
15042
  if (activeToolMode === "select" && selectedNotes.length > 0) {
14508
15043
  const ids = new Set(selectedNotes.map((n) => n.id));
14509
15044
  const active = getActive();
14510
- drawSelectedNotes(active.core.getNotes(), ids, [
15045
+ renderer.drawSelectedNotes(active.core.getNotes(), ids, [
14511
15046
  ...active.config.color,
14512
15047
  1
14513
15048
  ]);
14514
15049
  }
14515
15050
  if (lyricTargetNotes)
14516
- drawNoteLyrics(lyricTargetNotes, getActiveLyricKana());
15051
+ renderer.drawNoteLyrics(lyricTargetNotes, getActiveLyricKana());
14517
15052
  drawStartLine();
14518
15053
  if (playbackState === "playing") drawPlayhead();
14519
15054
  updateScrollbars();
14520
15055
  };
14521
15056
  const updateScrollbars = () => {
14522
- const canvas = getGridCanvas();
15057
+ const canvas = renderer.getGridCanvas();
14523
15058
  const maxOffsetX = getMaxOffsetX();
14524
15059
  const sbW = refs.hScroll.clientWidth;
14525
15060
  if (maxOffsetX <= 0) {
@@ -14603,7 +15138,7 @@ var mountDAW = (target, options = {}) => {
14603
15138
  const x2 = clamp5(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
14604
15139
  const ratio = x2 / (rect.width - thumbW);
14605
15140
  currentOffsetX = clamp5(ratio * maxOffsetX, 0, maxOffsetX);
14606
- setDrawOffset(currentOffsetX, currentOffsetY);
15141
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14607
15142
  redrawAll();
14608
15143
  };
14609
15144
  const moveV = (clientY) => {
@@ -14614,7 +15149,7 @@ var mountDAW = (target, options = {}) => {
14614
15149
  const y = clamp5(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
14615
15150
  const ratio = y / (rect.height - thumbH);
14616
15151
  currentOffsetY = clamp5(ratio * maxOffset, 0, maxOffset);
14617
- setDrawOffset(currentOffsetX, currentOffsetY);
15152
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14618
15153
  redrawAll();
14619
15154
  };
14620
15155
  };
@@ -14653,8 +15188,8 @@ var mountDAW = (target, options = {}) => {
14653
15188
  const autoScrollTick = () => {
14654
15189
  autoScrollRAF = null;
14655
15190
  if (!isSelecting || !lastMoveEvent) return;
14656
- const canvas = getGridCanvas();
14657
- const { x: x2, y } = getGridPosition(lastMoveEvent);
15191
+ const canvas = renderer.getGridCanvas();
15192
+ const { x: x2, y } = renderer.getGridPosition(lastMoveEvent);
14658
15193
  const dx = computeEdgeSpeed(x2, canvas.width);
14659
15194
  const dy = computeEdgeSpeed(y, canvas.height);
14660
15195
  if (dx !== 0 || dy !== 0) {
@@ -14662,7 +15197,7 @@ var mountDAW = (target, options = {}) => {
14662
15197
  const maxOffsetY = getMaxOffsetY();
14663
15198
  currentOffsetX = clamp5(currentOffsetX + dx, 0, maxOffsetX);
14664
15199
  currentOffsetY = clamp5(currentOffsetY + dy, 0, maxOffsetY);
14665
- setDrawOffset(currentOffsetX, currentOffsetY);
15200
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14666
15201
  onPointerMove(lastMoveEvent);
14667
15202
  }
14668
15203
  if (isSelecting) {
@@ -14683,11 +15218,17 @@ var mountDAW = (target, options = {}) => {
14683
15218
  };
14684
15219
  const findActiveNoteAt = (x2, y, margin = 0) => {
14685
15220
  const active = getActive();
14686
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
14687
- const offset = getDrawOffset();
15221
+ const {
15222
+ stepWidth,
15223
+ keyHeight,
15224
+ keyCount,
15225
+ pitchRangeStart,
15226
+ unitsPerRow: upr = UNITS_PER_SEMITONE
15227
+ } = renderConfig;
15228
+ const offset = renderer.getDrawOffset();
14688
15229
  for (const note of active.core.getNotes()) {
14689
15230
  const logicalX = note.startStep * stepWidth;
14690
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
15231
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
14691
15232
  const logicalY = yIndex * keyHeight;
14692
15233
  const w = note.durationSteps * stepWidth;
14693
15234
  const renderX = logicalX - offset.x;
@@ -14700,7 +15241,7 @@ var mountDAW = (target, options = {}) => {
14700
15241
  const hasNoteAt = (step, pitch, excludeId) => {
14701
15242
  const active = getActive();
14702
15243
  return active.core.getNotes().some(
14703
- (n) => n.id !== excludeId && n.pitch === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
15244
+ (n) => n.id !== excludeId && n.pitchUnits === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
14704
15245
  );
14705
15246
  };
14706
15247
  const snapToGrid = (duration) => Math.max(
@@ -14711,7 +15252,7 @@ var mountDAW = (target, options = {}) => {
14711
15252
  const onGridPointerDown = (event) => {
14712
15253
  event.preventDefault();
14713
15254
  options.onResumeAudio?.();
14714
- const { x: x2, y, step, pitch } = getGridPosition(event);
15255
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(event);
14715
15256
  const active = getActive();
14716
15257
  if (activeToolMode === "eraser") {
14717
15258
  if (isActiveLocked()) return;
@@ -14726,7 +15267,7 @@ var mountDAW = (target, options = {}) => {
14726
15267
  selectedOriginal = selectedNotes.map((n) => ({
14727
15268
  id: n.id,
14728
15269
  startStep: n.startStep,
14729
- pitch: n.pitch
15270
+ pitch: n.pitchUnits
14730
15271
  }));
14731
15272
  isSelecting = true;
14732
15273
  dragMode = "move";
@@ -14745,7 +15286,7 @@ var mountDAW = (target, options = {}) => {
14745
15286
  {
14746
15287
  id: clicked.id,
14747
15288
  startStep: clicked.startStep,
14748
- pitch: clicked.pitch
15289
+ pitch: clicked.pitchUnits
14749
15290
  }
14750
15291
  ];
14751
15292
  isSelecting = true;
@@ -14763,9 +15304,9 @@ var mountDAW = (target, options = {}) => {
14763
15304
  hasDragged = false;
14764
15305
  const existing = findActiveNoteAt(x2, y, TOUCH_HIT_MARGIN);
14765
15306
  if (existing) {
14766
- playPreview(existing.pitch);
15307
+ playPreview(existing.pitchUnits);
14767
15308
  const { stepWidth } = renderConfig;
14768
- const offset = getDrawOffset();
15309
+ const offset = renderer.getDrawOffset();
14769
15310
  const renderX = existing.startStep * stepWidth - offset.x;
14770
15311
  const w = existing.durationSteps * stepWidth;
14771
15312
  if (x2 >= renderX + w - resizeHandleWidth && x2 <= renderX + w) {
@@ -14776,17 +15317,17 @@ var mountDAW = (target, options = {}) => {
14776
15317
  dragOffsetPitch: 0,
14777
15318
  startStep: existing.startStep,
14778
15319
  durationSteps: existing.durationSteps,
14779
- lastPreviewPitch: existing.pitch
15320
+ lastPreviewPitch: existing.pitchUnits
14780
15321
  };
14781
15322
  } else {
14782
15323
  dragState = {
14783
15324
  noteId: existing.id,
14784
15325
  mode: "move",
14785
15326
  dragOffsetStep: step - existing.startStep,
14786
- dragOffsetPitch: pitch - existing.pitch,
15327
+ dragOffsetPitch: pitch - existing.pitchUnits,
14787
15328
  startStep: existing.startStep,
14788
15329
  durationSteps: existing.durationSteps,
14789
- lastPreviewPitch: existing.pitch
15330
+ lastPreviewPitch: existing.pitchUnits
14790
15331
  };
14791
15332
  }
14792
15333
  suppressClick = true;
@@ -14797,14 +15338,14 @@ var mountDAW = (target, options = {}) => {
14797
15338
  const newStart = snappedStep;
14798
15339
  const newEnd = newStart + currentInsertLength;
14799
15340
  const overlapping = active.core.getNotes().some(
14800
- (n) => n.pitch === pitch && newStart < n.startStep + n.durationSteps && newEnd > n.startStep
15341
+ (n) => n.pitchUnits === pitch && newStart < n.startStep + n.durationSteps && newEnd > n.startStep
14801
15342
  );
14802
15343
  if (!overlapping) {
14803
15344
  active.core.addNote(snappedStep, pitch, {
14804
15345
  noteLengthSteps: currentInsertLength
14805
15346
  });
14806
15347
  playPreview(pitch);
14807
- const newNote = active.core.getNotes().find((n) => n.startStep === snappedStep && n.pitch === pitch);
15348
+ const newNote = active.core.getNotes().find((n) => n.startStep === snappedStep && n.pitchUnits === pitch);
14808
15349
  if (newNote) {
14809
15350
  dragState = {
14810
15351
  noteId: newNote.id,
@@ -14813,7 +15354,7 @@ var mountDAW = (target, options = {}) => {
14813
15354
  dragOffsetPitch: 0,
14814
15355
  startStep: newNote.startStep,
14815
15356
  durationSteps: newNote.durationSteps,
14816
- lastPreviewPitch: newNote.pitch
15357
+ lastPreviewPitch: newNote.pitchUnits
14817
15358
  };
14818
15359
  hasDragged = true;
14819
15360
  }
@@ -14824,7 +15365,7 @@ var mountDAW = (target, options = {}) => {
14824
15365
  const active = getActive();
14825
15366
  if (activeToolMode === "pen") {
14826
15367
  if (!dragState) return;
14827
- const { step, pitch } = getGridPosition(event);
15368
+ const { step, pitch } = renderer.getGridPosition(event);
14828
15369
  hasDragged = true;
14829
15370
  if (dragState.mode === "move") {
14830
15371
  const nextStart = step - dragState.dragOffsetStep;
@@ -14848,7 +15389,7 @@ var mountDAW = (target, options = {}) => {
14848
15389
  }
14849
15390
  if (activeToolMode === "select" && isSelecting && selectionStart) {
14850
15391
  ensureAutoScroll(event);
14851
- const { x: x2, y, step, pitch } = getGridPosition(event);
15392
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(event);
14852
15393
  if (dragMode === "rect") {
14853
15394
  const rect = {
14854
15395
  x: Math.min(x2, selectionStart.x),
@@ -14857,11 +15398,17 @@ var mountDAW = (target, options = {}) => {
14857
15398
  height: Math.abs(y - selectionStart.y)
14858
15399
  };
14859
15400
  selectionRect = rect;
14860
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
14861
- const offset = getDrawOffset();
15401
+ const {
15402
+ stepWidth,
15403
+ keyHeight,
15404
+ keyCount,
15405
+ pitchRangeStart,
15406
+ unitsPerRow: upr = UNITS_PER_SEMITONE
15407
+ } = renderConfig;
15408
+ const offset = renderer.getDrawOffset();
14862
15409
  selectedNotes = active.core.getNotes().filter((note) => {
14863
15410
  const logicalX = note.startStep * stepWidth;
14864
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
15411
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
14865
15412
  const logicalY = yIndex * keyHeight;
14866
15413
  const nx = logicalX - offset.x;
14867
15414
  const ny = logicalY - offset.y;
@@ -14880,7 +15427,7 @@ var mountDAW = (target, options = {}) => {
14880
15427
  const orig = selectedOriginal.find((o) => o.id === note.id);
14881
15428
  if (!orig) continue;
14882
15429
  const newPitch = orig.pitch + deltaPitch;
14883
- if (newPitch >= 0 && newPitch < 128)
15430
+ if (newPitch >= PITCH_RANGE_START && newPitch <= PITCH_RANGE_END)
14884
15431
  active.core.moveNote(
14885
15432
  note.id,
14886
15433
  orig.startStep + snappedDelta,
@@ -15028,22 +15575,23 @@ var mountDAW = (target, options = {}) => {
15028
15575
  refs.bgOpacityRow.classList.add("dtm-hidden");
15029
15576
  }
15030
15577
  refs.bgRemoveBtn.classList.toggle("dtm-hidden", !blob2);
15031
- setBackgroundActive(!!blob2);
15578
+ renderer.setBackgroundActive(!!blob2);
15032
15579
  redrawAll();
15033
15580
  };
15034
15581
  const setupCanvas = () => {
15035
15582
  const w = refs.rollContainer.clientWidth || 800;
15036
15583
  const h = refs.rollContainer.clientHeight || 450;
15037
- init(refs.wrapper, w, h, renderConfig);
15038
- const gridCanvas = getGridCanvas();
15584
+ renderer?.destroy();
15585
+ renderer = createRenderer(refs.wrapper, w, h, renderConfig);
15586
+ const gridCanvas = renderer.getGridCanvas();
15039
15587
  gridCanvas.addEventListener("pointerdown", onGridPointerDown);
15040
15588
  gridCanvas.addEventListener("dblclick", (event) => {
15041
15589
  event.preventDefault();
15042
15590
  if (isActiveLocked()) return;
15043
- const { step, pitch } = getGridPosition(event);
15591
+ const { step, pitch } = renderer.getGridPosition(event);
15044
15592
  const active = getActive();
15045
15593
  const note = active.core.getNotes().find(
15046
- (n) => n.pitch === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
15594
+ (n) => n.pitchUnits === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
15047
15595
  );
15048
15596
  if (note) active.core.deleteNoteById(note.id);
15049
15597
  });
@@ -15061,7 +15609,7 @@ var mountDAW = (target, options = {}) => {
15061
15609
  0,
15062
15610
  getMaxOffsetX()
15063
15611
  );
15064
- setDrawOffset(currentOffsetX, currentOffsetY);
15612
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15065
15613
  redrawAll();
15066
15614
  },
15067
15615
  { passive: false }
@@ -15071,7 +15619,7 @@ var mountDAW = (target, options = {}) => {
15071
15619
  suppressClick = false;
15072
15620
  }
15073
15621
  });
15074
- const headerCanvas = getHeaderCanvas();
15622
+ const headerCanvas = renderer.getHeaderCanvas();
15075
15623
  headerCanvas.addEventListener("click", (event) => {
15076
15624
  if (playbackState === "playing") return;
15077
15625
  const rect = headerCanvas.getBoundingClientRect();
@@ -15087,11 +15635,11 @@ var mountDAW = (target, options = {}) => {
15087
15635
  }
15088
15636
  redrawAll();
15089
15637
  });
15090
- setDrawOffset(currentOffsetX, currentOffsetY);
15638
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15091
15639
  redrawAll();
15092
15640
  };
15093
15641
  const applyZoomX = () => {
15094
- const canvas = getGridCanvas();
15642
+ const canvas = renderer.getGridCanvas();
15095
15643
  const centerStep = (currentOffsetX + canvas.width / 2) / renderConfig.stepWidth;
15096
15644
  renderConfig.stepWidth = BASE_STEP_WIDTH * (zoomX * 2) / 100;
15097
15645
  refs.zoomXLabel.textContent = `${zoomX}%`;
@@ -15100,11 +15648,11 @@ var mountDAW = (target, options = {}) => {
15100
15648
  0,
15101
15649
  getMaxOffsetX()
15102
15650
  );
15103
- setDrawOffset(currentOffsetX, currentOffsetY);
15651
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15104
15652
  redrawAll();
15105
15653
  };
15106
15654
  const applyZoomY = () => {
15107
- const canvas = getGridCanvas();
15655
+ const canvas = renderer.getGridCanvas();
15108
15656
  const centerKey = (currentOffsetY + canvas.height / 2) / renderConfig.keyHeight;
15109
15657
  renderConfig.keyHeight = BASE_KEY_HEIGHT * zoomY / 100;
15110
15658
  refs.zoomYLabel.textContent = `${zoomY}%`;
@@ -15113,7 +15661,7 @@ var mountDAW = (target, options = {}) => {
15113
15661
  0,
15114
15662
  getMaxOffsetY()
15115
15663
  );
15116
- setDrawOffset(currentOffsetX, currentOffsetY);
15664
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15117
15665
  redrawAll();
15118
15666
  };
15119
15667
  const getViewState = () => ({
@@ -15125,7 +15673,14 @@ var mountDAW = (target, options = {}) => {
15125
15673
  const notifyViewState = () => options.onViewStateChange?.(getViewState());
15126
15674
  const dispatchNote = (trackId, pitch, trackVol, velocity, when, duration) => {
15127
15675
  const volume = trackVol / 100 * (velocity / 127) * (masterVolume / 100);
15128
- options.onPlayNote?.({ trackId, pitch, velocity, volume, when, duration });
15676
+ options.onPlayNote?.({
15677
+ trackId,
15678
+ pitchUnits: pitch,
15679
+ velocity,
15680
+ volume,
15681
+ when,
15682
+ duration
15683
+ });
15129
15684
  };
15130
15685
  const sequencer = createSequencer({
15131
15686
  getTracks: () => trackStates.map((t) => ({
@@ -15153,7 +15708,7 @@ var mountDAW = (target, options = {}) => {
15153
15708
  },
15154
15709
  onTick: (step) => {
15155
15710
  currentPlayStep = step;
15156
- const canvas = getGridCanvas();
15711
+ const canvas = renderer.getGridCanvas();
15157
15712
  const visibleSteps = canvas.width / renderConfig.stepWidth;
15158
15713
  const threshold = currentOffsetX / renderConfig.stepWidth + visibleSteps - 4;
15159
15714
  if (currentPlayStep > threshold) {
@@ -15163,7 +15718,7 @@ var mountDAW = (target, options = {}) => {
15163
15718
  0,
15164
15719
  getMaxOffsetX()
15165
15720
  );
15166
- setDrawOffset(currentOffsetX, currentOffsetY);
15721
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15167
15722
  }
15168
15723
  redrawAll();
15169
15724
  },
@@ -15194,7 +15749,7 @@ var mountDAW = (target, options = {}) => {
15194
15749
  (a, b) => a.startStep - b.startStep
15195
15750
  );
15196
15751
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
15197
- const semis = (lt.octave ?? 0) * 12;
15752
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
15198
15753
  const count = Math.min(sorted.length, lt.syllables.length);
15199
15754
  const notes = [];
15200
15755
  for (let i2 = 0; i2 < count; i2++) {
@@ -15202,7 +15757,7 @@ var mountDAW = (target, options = {}) => {
15202
15757
  if (n.startStep < fromStep) continue;
15203
15758
  notes.push({
15204
15759
  syllable: lt.syllables[i2],
15205
- pitch: n.pitch + semis,
15760
+ pitch: n.pitchUnits + semis,
15206
15761
  startSec: (n.startStep - fromStep) * secondsPerStep,
15207
15762
  durationSec: n.durationSteps * secondsPerStep * gate
15208
15763
  });
@@ -15245,13 +15800,13 @@ var mountDAW = (target, options = {}) => {
15245
15800
  }
15246
15801
  }
15247
15802
  if (playbackState !== "paused") {
15248
- const canvas = getGridCanvas();
15803
+ const canvas = renderer.getGridCanvas();
15249
15804
  currentOffsetX = clamp5(
15250
15805
  playStartStep * renderConfig.stepWidth - canvas.width * 0.5,
15251
15806
  0,
15252
15807
  getMaxOffsetX()
15253
15808
  );
15254
- setDrawOffset(currentOffsetX, currentOffsetY);
15809
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15255
15810
  }
15256
15811
  playbackState = "playing";
15257
15812
  sequencer.start(fromStep);
@@ -15337,17 +15892,38 @@ var mountDAW = (target, options = {}) => {
15337
15892
  trackPillEls.clear();
15338
15893
  for (const [i2, t] of trackStates.entries()) {
15339
15894
  const [r, g, b] = t.config.color;
15895
+ const isActive = t.config.id === activeTrackId;
15340
15896
  const btn = document.createElement("button");
15341
- btn.className = `dtm-pill ${t.config.id === activeTrackId ? "dtm-pill--active" : ""}`;
15897
+ btn.className = `dtm-pill ${isActive ? "dtm-pill--active" : ""}`;
15342
15898
  btn.style.setProperty("--dtm-pill-color", `rgb(${r},${g},${b})`);
15343
- btn.title = t.config.name;
15899
+ btn.title = `Track ${i2 + 1}: ${t.config.name}`;
15900
+ btn.setAttribute(
15901
+ "aria-label",
15902
+ `Track ${i2 + 1}: ${t.config.name}${isActive ? " (\u9078\u629E\u4E2D)" : ""}`
15903
+ );
15344
15904
  btn.textContent = String(i2 + 1);
15345
15905
  btn.addEventListener("click", () => switchTrack(t.config.id));
15346
15906
  refs.trackTabs.appendChild(btn);
15347
15907
  trackPillEls.set(t.config.id, btn);
15348
15908
  }
15349
15909
  const active = getActive();
15910
+ const activeIndex = trackStates.findIndex(
15911
+ (t) => t.config.id === activeTrackId
15912
+ );
15913
+ const [activeR, activeG, activeB] = active.config.color;
15914
+ const panelEl = refs.trackBody.closest(".dtm-panel");
15915
+ if (panelEl) {
15916
+ const summaryEl = panelEl.querySelector("summary");
15917
+ if (summaryEl) {
15918
+ summaryEl.textContent = `\u500B\u5225\u30C8\u30E9\u30C3\u30AF\u8A2D\u5B9A\uFF08Track ${activeIndex + 1}: ${active.config.name}\uFF09`;
15919
+ }
15920
+ }
15350
15921
  refs.trackBody.innerHTML = `
15922
+ <div class="dtm-active-track-banner" style="--dtm-track-color: rgb(${activeR},${activeG},${activeB})">
15923
+ <span class="dtm-active-track-badge">TRACK ${activeIndex + 1}</span>
15924
+ <span class="dtm-active-track-name">${active.config.name}</span>
15925
+ <span class="dtm-active-track-pill">ACTIVE</span>
15926
+ </div>
15351
15927
  <div class="dtm-row">
15352
15928
  <span class="dtm-label">\u30D9\u30ED\u30B7\u30C6\u30A3</span>
15353
15929
  <input type="range" class="dtm-range dtm-grow" data-dtm="track-vol" min="0" max="127" value="${active.volume}">
@@ -16217,6 +16793,7 @@ var mountDAW = (target, options = {}) => {
16217
16793
  fadeIn: Math.round(fadeInSec * 10),
16218
16794
  fadeOut: Math.round(fadeOutSec * 10),
16219
16795
  mode,
16796
+ edo: renderConfig.edo,
16220
16797
  trackInstruments: trackInstMeta,
16221
16798
  trackCompression: trackCompMeta,
16222
16799
  trackWidth: trackWidthMeta,
@@ -16245,6 +16822,7 @@ var mountDAW = (target, options = {}) => {
16245
16822
  fadeIn: Math.round(fadeInSec * 10),
16246
16823
  fadeOut: Math.round(fadeOutSec * 10),
16247
16824
  mode,
16825
+ edo: renderConfig.edo,
16248
16826
  trackInstruments: trackInstMeta,
16249
16827
  trackCompression: trackCompMeta,
16250
16828
  trackWidth: trackWidthMeta,
@@ -16384,19 +16962,19 @@ var mountDAW = (target, options = {}) => {
16384
16962
  }
16385
16963
  }
16386
16964
  if (candidateNotes.length === 0) return null;
16387
- const sum = candidateNotes.reduce((acc, note) => acc + note.pitch, 0);
16965
+ const sum = candidateNotes.reduce((acc, note) => acc + note.pitchUnits, 0);
16388
16966
  return Math.round(sum / candidateNotes.length);
16389
16967
  };
16390
16968
  const centerPitch = (pitch) => {
16391
- const canvas = getGridCanvas();
16392
- const yIndex = renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart);
16969
+ const canvas = renderer.getGridCanvas();
16970
+ const yIndex = renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart) / (renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE);
16393
16971
  const logicalY = yIndex * renderConfig.keyHeight;
16394
16972
  currentOffsetY = clamp5(
16395
16973
  logicalY - (canvas.height - renderConfig.keyHeight) / 2,
16396
16974
  0,
16397
16975
  getMaxOffsetY()
16398
16976
  );
16399
- setDrawOffset(currentOffsetX, currentOffsetY);
16977
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
16400
16978
  };
16401
16979
  const clearAll = () => {
16402
16980
  for (const t of trackStates) {
@@ -16454,10 +17032,10 @@ var mountDAW = (target, options = {}) => {
16454
17032
  if (!meta.drumFont) applyDrumPatternFont(meta.drum);
16455
17033
  }
16456
17034
  if (meta.volume !== void 0) {
16457
- masterVolume = meta.volume;
16458
- refs.masterVolume.value = String(meta.volume);
16459
- refs.masterVolumeLabel.textContent = `${meta.volume}%`;
17035
+ applyMasterVolume(meta.volume);
16460
17036
  }
17037
+ applyEdo(meta.edo ?? 12);
17038
+ refs.edoSelect.value = String(renderConfig.edo ?? 12);
16461
17039
  if (meta.drumVolume !== void 0) {
16462
17040
  drumVolume = meta.drumVolume;
16463
17041
  refs.drumVolume.value = String(meta.drumVolume);
@@ -16622,7 +17200,7 @@ var mountDAW = (target, options = {}) => {
16622
17200
  if (applyActiveOnly && p.trackIndex !== activeTrackIndex) continue;
16623
17201
  const t = trackStates[p.trackIndex];
16624
17202
  if (!t) continue;
16625
- t.core.addNote(p.startStep, p.pitch, {
17203
+ t.core.addNote(p.startStep, p.pitchUnits, {
16626
17204
  noteLengthSteps: p.durationSteps,
16627
17205
  velocity: DEFAULT_VELOCITY
16628
17206
  });
@@ -16639,7 +17217,7 @@ var mountDAW = (target, options = {}) => {
16639
17217
  if (firstPitch !== null) {
16640
17218
  centerPitch(firstPitch);
16641
17219
  } else {
16642
- centerPitch(48);
17220
+ centerPitch(pitchV1ToUnits(48));
16643
17221
  }
16644
17222
  redrawAll();
16645
17223
  updateTrackPanel();
@@ -16657,6 +17235,8 @@ var mountDAW = (target, options = {}) => {
16657
17235
  const chordTrack = trackStates.find((t) => t.config.id === "chord");
16658
17236
  if (!chordTrack) return;
16659
17237
  const placements = buildChordPlacements({
17238
+ // 和音は五度連鎖経由で音律の格子へ写す(31平均律では長3度が10度になる)
17239
+ edo: renderConfig.edo,
16660
17240
  chordStr: active.savedChordInput,
16661
17241
  patternType: active.savedChordPattern,
16662
17242
  rootShift: active.savedChordRoot,
@@ -16666,7 +17246,7 @@ var mountDAW = (target, options = {}) => {
16666
17246
  chordTrack.core.clearNotesWithoutHistory();
16667
17247
  chordTrack.core.beginBatch();
16668
17248
  for (const p of placements) {
16669
- chordTrack.core.addNote(p.startStep, p.pitch, {
17249
+ chordTrack.core.addNote(p.startStep, p.pitchUnits, {
16670
17250
  noteLengthSteps: Math.max(1, p.durationSteps),
16671
17251
  velocity: p.velocity
16672
17252
  });
@@ -16704,6 +17284,8 @@ var mountDAW = (target, options = {}) => {
16704
17284
  active.vocalOctaveUnison = "none";
16705
17285
  } else {
16706
17286
  clearAll();
17287
+ applyEdo(12);
17288
+ refs.edoSelect.value = String(renderConfig.edo ?? 12);
16707
17289
  for (const t of trackStates) t.core.setLoadMode(true);
16708
17290
  for (const t of trackStates) {
16709
17291
  t.lyrics = "";
@@ -16730,7 +17312,7 @@ var mountDAW = (target, options = {}) => {
16730
17312
  if (applyActiveOnly && p.trackId !== activeTrackId) continue;
16731
17313
  const t = trackStates.find((ts) => ts.config.id === p.trackId);
16732
17314
  if (!t) continue;
16733
- t.core.addNote(p.startStep, p.pitch, {
17315
+ t.core.addNote(p.startStep, snapToEdoGrid(pitchV1ToUnits(p.pitch)), {
16734
17316
  noteLengthSteps: p.durationSteps,
16735
17317
  velocity: p.velocity
16736
17318
  });
@@ -16747,7 +17329,7 @@ var mountDAW = (target, options = {}) => {
16747
17329
  if (firstPitch !== null) {
16748
17330
  centerPitch(firstPitch);
16749
17331
  } else {
16750
- centerPitch(48);
17332
+ centerPitch(pitchV1ToUnits(48));
16751
17333
  }
16752
17334
  redrawAll();
16753
17335
  updateTrackPanel();
@@ -16811,18 +17393,21 @@ var mountDAW = (target, options = {}) => {
16811
17393
  }
16812
17394
  }, 30);
16813
17395
  };
16814
- const showConfirmModal = (message) => new Promise((resolve) => {
17396
+ const showConfirmModal = (message, opts) => new Promise((resolve) => {
17397
+ const title = opts?.title ?? "\u30E2\u30FC\u30C9\u306E\u78BA\u8A8D";
17398
+ const yes = opts?.yes ?? "\u306F\u3044\uFF08\u4E0A\u7D1A\u8005\u30E2\u30FC\u30C9\u306B\u5207\u308A\u66FF\u3048\u308B\uFF09";
17399
+ const no = opts?.no ?? "\u3044\u3044\u3048\uFF08\u3053\u306E\u307E\u307E\u8AAD\u307F\u8FBC\u3080\uFF09";
16815
17400
  const overlay = document.createElement("div");
16816
17401
  overlay.className = "dtm-modal-overlay";
16817
17402
  overlay.innerHTML = `
16818
17403
  <div class="dtm-modal">
16819
17404
  <div class="dtm-modal-header">
16820
- <span class="dtm-modal-title">\u30E2\u30FC\u30C9\u306E\u78BA\u8A8D</span>
17405
+ <span class="dtm-modal-title">${title}</span>
16821
17406
  </div>
16822
17407
  <div class="dtm-modal-body"><p>${message}</p></div>
16823
17408
  <div class="dtm-confirm-footer">
16824
- <button class="dtm-btn dtm-btn--ghost dtm-confirm-no">\u3044\u3044\u3048\uFF08\u3053\u306E\u307E\u307E\u8AAD\u307F\u8FBC\u3080\uFF09</button>
16825
- <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>
17409
+ <button class="dtm-btn dtm-btn--ghost dtm-confirm-no">${no}</button>
17410
+ <button class="dtm-btn dtm-btn--primary dtm-confirm-yes">${yes}</button>
16826
17411
  </div>
16827
17412
  </div>`;
16828
17413
  const close = (result) => {
@@ -16930,10 +17515,36 @@ var mountDAW = (target, options = {}) => {
16930
17515
  });
16931
17516
  refs.decomposeChordToggle.addEventListener("change", notifyViewState);
16932
17517
  refs.ignoreChordHeavyToggle.addEventListener("change", notifyViewState);
17518
+ refs.edoSelect.addEventListener("change", async () => {
17519
+ const next = Number.parseInt(refs.edoSelect.value, 10) === 31 ? 31 : 12;
17520
+ const prev = renderConfig.edo ?? 12;
17521
+ if (next === prev) return;
17522
+ const hasNotes = trackStates.some((t) => t.core.getNotes().length > 0);
17523
+ if (next === 12 && prev === 31 && hasNotes) {
17524
+ const okToDrop = await showConfirmModal(
17525
+ "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",
17526
+ {
17527
+ title: "\u97F3\u5F8B\u306E\u78BA\u8A8D",
17528
+ yes: "\u5207\u308A\u66FF\u3048\u308B",
17529
+ no: "\u3084\u3081\u308B"
17530
+ }
17531
+ );
17532
+ if (!okToDrop) {
17533
+ refs.edoSelect.value = String(prev);
17534
+ return;
17535
+ }
17536
+ }
17537
+ applyEdo(next, { snap: true });
17538
+ const first = getFirstDetectedPitch();
17539
+ centerPitch(first ?? pitchV1ToUnits(48));
17540
+ redrawAll();
17541
+ updateUndoRedo();
17542
+ });
17543
+ refs.edoInfoBtn.addEventListener("click", () => {
17544
+ showModal("\u97F3\u5F8B\u306E\u89E3\u8AAC", EDO_INFO_HTML);
17545
+ });
16933
17546
  refs.masterVolume.addEventListener("input", () => {
16934
- masterVolume = Number.parseInt(refs.masterVolume.value, 10) || 0;
16935
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
16936
- options.singingVoices?.setVolume(masterVolume / 100);
17547
+ applyMasterVolume(Number.parseInt(refs.masterVolume.value, 10) || 0);
16937
17548
  });
16938
17549
  refs.masterComp.addEventListener("input", () => {
16939
17550
  masterCompression = Number.parseInt(refs.masterComp.value, 10) || 0;
@@ -16997,10 +17608,7 @@ var mountDAW = (target, options = {}) => {
16997
17608
  const scale = targetPeak / observedPeakMax;
16998
17609
  const suggested = clamp5(Math.round(masterVolume * scale), 10, 100);
16999
17610
  if (suggested !== masterVolume) {
17000
- masterVolume = suggested;
17001
- refs.masterVolume.value = String(masterVolume);
17002
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17003
- options.singingVoices?.setVolume(masterVolume / 100);
17611
+ applyMasterVolume(suggested);
17004
17612
  }
17005
17613
  observedPeakMax = 0;
17006
17614
  observedPlayMs = 0;
@@ -17160,7 +17768,8 @@ var mountDAW = (target, options = {}) => {
17160
17768
  generateRandomPattern(getActive().core, {
17161
17769
  stepsPerBar: renderConfig.stepsPerBar,
17162
17770
  startStep: playStartStep,
17163
- pitchRangeStart: renderConfig.pitchRangeStart
17771
+ pitchRangeStart: renderConfig.pitchRangeStart,
17772
+ edo: renderConfig.edo
17164
17773
  });
17165
17774
  redrawAll();
17166
17775
  });
@@ -17497,7 +18106,7 @@ var mountDAW = (target, options = {}) => {
17497
18106
  id: 0,
17498
18107
  startStep: p.startStep,
17499
18108
  durationSteps: p.durationSteps,
17500
- pitch: p.pitch,
18109
+ pitchUnits: snapToEdoGrid(pitchV1ToUnits(p.pitch)),
17501
18110
  velocity: p.velocity
17502
18111
  }));
17503
18112
  const mml = refCore.getMMLFromNotes(asNotes, tempo, 100).trim();
@@ -17952,10 +18561,10 @@ var mountDAW = (target, options = {}) => {
17952
18561
  const newStart = playStartStep + (note.startStep - minStart);
17953
18562
  const newEnd = newStart + note.durationSteps;
17954
18563
  const overlap = notes.some(
17955
- (ex) => ex.pitch === note.pitch && newStart < ex.startStep + ex.durationSteps && newEnd > ex.startStep
18564
+ (ex) => ex.pitchUnits === note.pitchUnits && newStart < ex.startStep + ex.durationSteps && newEnd > ex.startStep
17956
18565
  );
17957
18566
  if (!overlap)
17958
- core.addNote(newStart, note.pitch, {
18567
+ core.addNote(newStart, note.pitchUnits, {
17959
18568
  noteLengthSteps: note.durationSteps,
17960
18569
  velocity: note.velocity
17961
18570
  });
@@ -18028,13 +18637,13 @@ var mountDAW = (target, options = {}) => {
18028
18637
  pausedPlayStep = step;
18029
18638
  currentPlayStep = step;
18030
18639
  playbackState = "paused";
18031
- const canvas = getGridCanvas();
18640
+ const canvas = renderer.getGridCanvas();
18032
18641
  currentOffsetX = clamp5(
18033
18642
  step * renderConfig.stepWidth - canvas.width * 0.5,
18034
18643
  0,
18035
18644
  getMaxOffsetX()
18036
18645
  );
18037
- setDrawOffset(currentOffsetX, currentOffsetY);
18646
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
18038
18647
  updateTransport();
18039
18648
  redrawAll();
18040
18649
  };
@@ -18102,16 +18711,10 @@ var mountDAW = (target, options = {}) => {
18102
18711
  forcePauseAt,
18103
18712
  setLoading,
18104
18713
  setMasterVolume: (volume) => {
18105
- masterVolume = clamp5(volume, 0, 100);
18106
- refs.masterVolume.value = String(masterVolume);
18107
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
18108
- options.singingVoices?.setVolume(masterVolume / 100);
18714
+ applyMasterVolume(volume);
18109
18715
  },
18110
18716
  setVolume: (volume) => {
18111
- masterVolume = clamp5(volume, 0, 100);
18112
- refs.masterVolume.value = String(masterVolume);
18113
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
18114
- options.singingVoices?.setVolume(masterVolume / 100);
18717
+ applyMasterVolume(volume);
18115
18718
  },
18116
18719
  setDrumVolume: (volume) => {
18117
18720
  drumVolume = clamp5(volume, 0, 100);
@@ -18154,15 +18757,19 @@ var mountDAW = (target, options = {}) => {
18154
18757
  suppressPatch = true;
18155
18758
  track.core.beginBatch();
18156
18759
  for (const n of added) {
18157
- const existing = track.core.getNotes().find((e) => e.startStep === n.startStep && e.pitch === n.pitch);
18760
+ const existing = track.core.getNotes().find(
18761
+ (e) => e.startStep === n.startStep && e.pitchUnits === n.pitchUnits
18762
+ );
18158
18763
  if (existing) track.core.deleteNoteById(existing.id);
18159
- track.core.addNote(n.startStep, n.pitch, {
18764
+ track.core.addNote(n.startStep, n.pitchUnits, {
18160
18765
  noteLengthSteps: n.durationSteps,
18161
18766
  velocity: n.velocity
18162
18767
  });
18163
18768
  }
18164
18769
  for (const r of removed) {
18165
- const note = track.core.getNotes().find((n) => n.startStep === r.startStep && n.pitch === r.pitch);
18770
+ const note = track.core.getNotes().find(
18771
+ (n) => n.startStep === r.startStep && n.pitchUnits === r.pitchUnits
18772
+ );
18166
18773
  if (note) track.core.deleteNoteById(note.id);
18167
18774
  }
18168
18775
  track.core.endBatch();
@@ -18204,9 +18811,9 @@ var mountDAW = (target, options = {}) => {
18204
18811
  if (t.config.id === activeTrackId) updateTrackPanel();
18205
18812
  },
18206
18813
  noteToCanvas: (step, pitch) => {
18207
- const canvas = getGridCanvas();
18814
+ const canvas = renderer.getGridCanvas();
18208
18815
  const x2 = step * renderConfig.stepWidth - currentOffsetX;
18209
- const y = (renderConfig.keyCount - 1 - pitch) * renderConfig.keyHeight - currentOffsetY;
18816
+ const y = (renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart) / (renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE)) * renderConfig.keyHeight - currentOffsetY;
18210
18817
  const onScreen = x2 >= 0 && x2 <= canvas.width && y >= 0 && y <= canvas.height;
18211
18818
  let side = null;
18212
18819
  if (!onScreen) {
@@ -18270,7 +18877,7 @@ var playSingingMML = async (mml, options = {}) => {
18270
18877
  id: id++,
18271
18878
  startStep: p.startStep,
18272
18879
  durationSteps: p.durationSteps,
18273
- pitch: p.pitch,
18880
+ pitchUnits: p.pitchUnits,
18274
18881
  velocity: p.velocity
18275
18882
  }));
18276
18883
  return {
@@ -18296,7 +18903,7 @@ var playSingingMML = async (mml, options = {}) => {
18296
18903
  (a, b) => a.startStep - b.startStep
18297
18904
  );
18298
18905
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
18299
- const semis = (lt.octave ?? 0) * 12;
18906
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
18300
18907
  const count = Math.min(sorted.length, lt.syllables.length);
18301
18908
  const notes = [];
18302
18909
  for (let i2 = 0; i2 < count; i2++) {
@@ -18304,7 +18911,7 @@ var playSingingMML = async (mml, options = {}) => {
18304
18911
  if (n.startStep < fromStep) continue;
18305
18912
  notes.push({
18306
18913
  syllable: lt.syllables[i2],
18307
- pitch: n.pitch + semis,
18914
+ pitch: n.pitchUnits + semis,
18308
18915
  startSec: (n.startStep - fromStep) * secondsPerStep,
18309
18916
  durationSec: n.durationSteps * secondsPerStep * gate
18310
18917
  });
@@ -18492,24 +19099,28 @@ var createPianoRoll = (options, handlers) => {
18492
19099
  config,
18493
19100
  noteLengthSteps = 1
18494
19101
  } = options;
18495
- init(mountTarget, width, height, config);
19102
+ const renderer = createRenderer(mountTarget, width, height, config);
18496
19103
  let currentNoteLengthSteps = noteLengthSteps;
18497
19104
  let selectionRect = null;
18498
19105
  let isSelecting = false;
18499
19106
  let selectionStart = null;
18500
19107
  let selectedNotes = [];
18501
19108
  let copiedNotes = [];
18502
- const core = new MMLCore({
18503
- onMMLGenerated: handlers.onMMLGenerated,
18504
- onNotesChanged: (notes) => {
18505
- handlers.onNotesChanged(notes);
18506
- }
18507
- });
19109
+ const core = new MMLCore(
19110
+ {
19111
+ onMMLGenerated: handlers.onMMLGenerated,
19112
+ onNotesChanged: (notes) => {
19113
+ handlers.onNotesChanged(notes);
19114
+ }
19115
+ },
19116
+ 80,
19117
+ () => config
19118
+ );
18508
19119
  const getAddNoteOptions = () => ({
18509
19120
  noteLengthSteps: currentNoteLengthSteps
18510
19121
  });
18511
19122
  let suppressClick = false;
18512
- onClick((step, pitch) => {
19123
+ renderer.onClick((step, pitch) => {
18513
19124
  if (suppressClick) {
18514
19125
  suppressClick = false;
18515
19126
  return;
@@ -18521,7 +19132,7 @@ var createPianoRoll = (options, handlers) => {
18521
19132
  } else if (mode === "eraser") {
18522
19133
  const notes = core.getNotes();
18523
19134
  const note = notes.find(
18524
- (n) => n.startStep <= step && step < n.startStep + n.durationSteps && n.pitch === pitch
19135
+ (n) => n.startStep <= step && step < n.startStep + n.durationSteps && n.pitchUnits === pitch
18525
19136
  );
18526
19137
  if (note) {
18527
19138
  core.deleteNoteById(note.id);
@@ -18529,17 +19140,23 @@ var createPianoRoll = (options, handlers) => {
18529
19140
  }
18530
19141
  }
18531
19142
  });
18532
- const gridCanvas = getGridCanvas();
19143
+ const gridCanvas = renderer.getGridCanvas();
18533
19144
  const resizeHandleWidth = 6;
18534
19145
  let dragState = null;
18535
19146
  let hasDragged = false;
18536
19147
  let lastPreviewPitch = null;
18537
19148
  const findNoteAtPosition = (x2, y) => {
18538
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18539
- const offset = getDrawOffset();
19149
+ const {
19150
+ stepWidth,
19151
+ keyHeight,
19152
+ keyCount,
19153
+ pitchRangeStart,
19154
+ unitsPerRow: upr = UNITS_PER_SEMITONE
19155
+ } = renderer.getRenderConfig();
19156
+ const offset = renderer.getDrawOffset();
18540
19157
  for (const note of core.getNotes()) {
18541
19158
  const logicalX = note.startStep * stepWidth;
18542
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19159
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18543
19160
  const logicalY = yIndex * keyHeight;
18544
19161
  const w = note.durationSteps * stepWidth;
18545
19162
  const h = keyHeight;
@@ -18553,7 +19170,7 @@ var createPianoRoll = (options, handlers) => {
18553
19170
  };
18554
19171
  const handlePointerMove = (e) => {
18555
19172
  if (core.getToolMode() === "select" && isSelecting && selectionStart) {
18556
- const { x: x2, y } = getGridPosition(e);
19173
+ const { x: x2, y } = renderer.getGridPosition(e);
18557
19174
  const minX = Math.min(x2, selectionStart.x);
18558
19175
  const minY = Math.min(y, selectionStart.y);
18559
19176
  const width2 = Math.abs(x2 - selectionStart.x);
@@ -18565,7 +19182,7 @@ var createPianoRoll = (options, handlers) => {
18565
19182
  }
18566
19183
  if (!dragState) return;
18567
19184
  hasDragged = true;
18568
- const { step, pitch } = getGridPosition(e);
19185
+ const { step, pitch } = renderer.getGridPosition(e);
18569
19186
  if (dragState.mode === "move") {
18570
19187
  if (dragState.selectedNotes && dragState.selectedNotes.length > 0) {
18571
19188
  const noteId = dragState.noteId;
@@ -18574,10 +19191,10 @@ var createPianoRoll = (options, handlers) => {
18574
19191
  const nextStart2 = step - dragState.dragOffsetStep;
18575
19192
  const nextPitch2 = pitch - dragState.dragOffsetPitch;
18576
19193
  const stepDelta = nextStart2 - baseNote.startStep;
18577
- const pitchDelta = nextPitch2 - baseNote.pitch;
19194
+ const pitchDelta = nextPitch2 - baseNote.pitchUnits;
18578
19195
  for (const note of dragState.selectedNotes) {
18579
19196
  const newStart = note.startStep + stepDelta;
18580
- const newPitch = note.pitch + pitchDelta;
19197
+ const newPitch = note.pitchUnits + pitchDelta;
18581
19198
  core.moveNote(note.id, newStart, newPitch);
18582
19199
  }
18583
19200
  if (options.onPreviewSound && pitch !== lastPreviewPitch) {
@@ -18618,7 +19235,7 @@ var createPianoRoll = (options, handlers) => {
18618
19235
  }
18619
19236
  };
18620
19237
  gridCanvas.addEventListener("pointerdown", (e) => {
18621
- const { x: x2, y, step, pitch } = getGridPosition(e);
19238
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(e);
18622
19239
  const currentMode = core.getToolMode();
18623
19240
  if (currentMode === "select") {
18624
19241
  const clickedNote = findNoteAtPosition(x2, y);
@@ -18629,7 +19246,7 @@ var createPianoRoll = (options, handlers) => {
18629
19246
  noteId: clickedNote.id,
18630
19247
  mode: "move",
18631
19248
  dragOffsetStep: step - clickedNote.startStep,
18632
- dragOffsetPitch: pitch - clickedNote.pitch,
19249
+ dragOffsetPitch: pitch - clickedNote.pitchUnits,
18633
19250
  startStep: clickedNote.startStep,
18634
19251
  selectedNotes: notesInRect
18635
19252
  // 複数選択ノートを保存
@@ -18647,10 +19264,16 @@ var createPianoRoll = (options, handlers) => {
18647
19264
  }
18648
19265
  const note = findNoteAtPosition(x2, y);
18649
19266
  if (!note) return;
18650
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18651
- const offset = getDrawOffset();
19267
+ const {
19268
+ stepWidth,
19269
+ keyHeight,
19270
+ keyCount,
19271
+ pitchRangeStart,
19272
+ unitsPerRow: upr = UNITS_PER_SEMITONE
19273
+ } = renderer.getRenderConfig();
19274
+ const offset = renderer.getDrawOffset();
18652
19275
  const logicalX = note.startStep * stepWidth;
18653
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19276
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18654
19277
  const logicalY = yIndex * keyHeight;
18655
19278
  const renderX = logicalX - offset.x;
18656
19279
  const renderY = logicalY - offset.y;
@@ -18669,7 +19292,7 @@ var createPianoRoll = (options, handlers) => {
18669
19292
  noteId: note.id,
18670
19293
  mode: "move",
18671
19294
  dragOffsetStep: step - note.startStep,
18672
- dragOffsetPitch: pitch - note.pitch,
19295
+ dragOffsetPitch: pitch - note.pitchUnits,
18673
19296
  startStep: note.startStep
18674
19297
  };
18675
19298
  });
@@ -18680,41 +19303,47 @@ var createPianoRoll = (options, handlers) => {
18680
19303
  "wheel",
18681
19304
  (e) => {
18682
19305
  e.preventDefault();
18683
- const configValues = getRenderConfig();
19306
+ const configValues = renderer.getRenderConfig();
18684
19307
  const gridHeight = gridCanvas.height;
18685
19308
  const maxOffsetY = Math.max(
18686
19309
  0,
18687
19310
  configValues.keyCount * configValues.keyHeight - gridHeight
18688
19311
  );
18689
- const currentOffset = getDrawOffset();
19312
+ const currentOffset = renderer.getDrawOffset();
18690
19313
  const nextOffsetY = Math.min(
18691
19314
  Math.max(currentOffset.y + e.deltaY, 0),
18692
19315
  maxOffsetY
18693
19316
  );
18694
- setDrawOffset(currentOffset.x, nextOffsetY);
18695
- drawGrid();
18696
- drawNotes(core.getNotes());
19317
+ renderer.setDrawOffset(currentOffset.x, nextOffsetY);
19318
+ renderer.drawGrid();
19319
+ renderer.drawNotes(core.getNotes());
18697
19320
  },
18698
19321
  { passive: false }
18699
19322
  );
18700
19323
  const redraw = () => {
18701
- drawGrid();
18702
- drawNotes(core.getNotes());
19324
+ renderer.drawGrid();
19325
+ renderer.drawNotes(core.getNotes());
18703
19326
  if (core.getToolMode() === "select") {
18704
- drawSelectionRect(selectionRect);
19327
+ renderer.drawSelectionRect(selectionRect);
18705
19328
  if (selectedNotes.length > 0) {
18706
19329
  const selectedIds = new Set(selectedNotes.map((n) => n.id));
18707
- drawSelectedNotes(core.getNotes(), selectedIds);
19330
+ renderer.drawSelectedNotes(core.getNotes(), selectedIds);
18708
19331
  }
18709
19332
  }
18710
19333
  };
18711
19334
  const getNotesInRect = (rect) => {
18712
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18713
- const offset = getDrawOffset();
19335
+ const {
19336
+ stepWidth,
19337
+ keyHeight,
19338
+ keyCount,
19339
+ pitchRangeStart,
19340
+ unitsPerRow: upr = UNITS_PER_SEMITONE
19341
+ } = renderer.getRenderConfig();
19342
+ const offset = renderer.getDrawOffset();
18714
19343
  const notes = [];
18715
19344
  for (const note of core.getNotes()) {
18716
19345
  const logicalX = note.startStep * stepWidth;
18717
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19346
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18718
19347
  const logicalY = yIndex * keyHeight;
18719
19348
  const noteRect = {
18720
19349
  x: logicalX - offset.x,
@@ -18761,7 +19390,7 @@ var createPianoRoll = (options, handlers) => {
18761
19390
  const minStart = Math.min(...copiedNotes.map((n) => n.startStep));
18762
19391
  copiedNotes.forEach((note) => {
18763
19392
  const newStep = startStep + (note.startStep - minStart);
18764
- core.addNote(newStep, note.pitch, {
19393
+ core.addNote(newStep, note.pitchUnits, {
18765
19394
  noteLengthSteps: note.durationSteps,
18766
19395
  velocity: note.velocity
18767
19396
  });
@@ -19602,10 +20231,12 @@ var createDtmStudio = async (options = {}) => {
19602
20231
  );
19603
20232
  }
19604
20233
  if (!sfInst) return;
20234
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
19605
20235
  sfInst.play({
19606
20236
  ctx: audioCtx,
19607
20237
  destination: getChannelStrip(e.trackId).input,
19608
- pitch: e.pitch,
20238
+ pitch: midi,
20239
+ detuneCents,
19609
20240
  volume: e.volume,
19610
20241
  velocity: e.velocity,
19611
20242
  when: e.when,
@@ -19921,10 +20552,12 @@ var createDtmStudio = async (options = {}) => {
19921
20552
  );
19922
20553
  }
19923
20554
  if (!sfInst) return;
20555
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
19924
20556
  sfInst.play({
19925
20557
  ctx: audioCtx,
19926
20558
  destination: getChannelStrip(e.trackId).input,
19927
- pitch: e.pitch,
20559
+ pitch: midi,
20560
+ detuneCents,
19928
20561
  volume: e.volume,
19929
20562
  velocity: e.velocity,
19930
20563
  when: e.when,
@@ -20010,10 +20643,12 @@ var createDtmStudio = async (options = {}) => {
20010
20643
  );
20011
20644
  }
20012
20645
  if (!sfInst) return;
20646
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20013
20647
  sfInst.play({
20014
20648
  ctx: audioCtx,
20015
20649
  destination: getChannelStrip(e.trackId).input,
20016
- pitch: e.pitch,
20650
+ pitch: midi,
20651
+ detuneCents,
20017
20652
  volume: e.volume,
20018
20653
  velocity: e.velocity,
20019
20654
  when: e.when,
@@ -20092,10 +20727,12 @@ var createDtmStudio = async (options = {}) => {
20092
20727
  );
20093
20728
  }
20094
20729
  if (!sfInst) return;
20730
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20095
20731
  sfInst.play({
20096
20732
  ctx: audioCtx,
20097
20733
  destination: getChannelStrip(e.trackId).input,
20098
- pitch: e.pitch,
20734
+ pitch: midi,
20735
+ detuneCents,
20099
20736
  volume: e.volume,
20100
20737
  velocity: e.velocity,
20101
20738
  when: e.when,
@@ -20132,10 +20769,12 @@ var createDtmStudio = async (options = {}) => {
20132
20769
  sfInst = resolveSoundFont(defaultPreset, role, "simple");
20133
20770
  }
20134
20771
  if (!sfInst) return;
20772
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20135
20773
  sfInst.play({
20136
20774
  ctx: audioCtx,
20137
20775
  destination: getChannelStrip(e.trackId).input,
20138
- pitch: e.pitch,
20776
+ pitch: midi,
20777
+ detuneCents,
20139
20778
  volume: e.volume,
20140
20779
  velocity: e.velocity,
20141
20780
  when: e.when,
@@ -20158,10 +20797,12 @@ var createDtmStudio = async (options = {}) => {
20158
20797
  if (audioCtx.state === "suspended") {
20159
20798
  await audioCtx.resume();
20160
20799
  }
20800
+ const { midi, detuneCents } = unitsToMidiDetune(options2.pitchUnits);
20161
20801
  sfInst.play({
20162
20802
  ctx: audioCtx,
20163
20803
  destination: masterGain,
20164
- pitch: options2.pitch,
20804
+ pitch: midi,
20805
+ detuneCents,
20165
20806
  volume: vol / 100,
20166
20807
  when: 0,
20167
20808
  duration: dur
@@ -20181,7 +20822,7 @@ var createDtmStudio = async (options = {}) => {
20181
20822
  // 伴奏トラック
20182
20823
  startStep: p.startStep,
20183
20824
  durationSteps: p.durationSteps,
20184
- pitch: p.pitch,
20825
+ pitchUnits: p.pitchUnits,
20185
20826
  velocity: p.velocity
20186
20827
  }));
20187
20828
  const playerPreset = defaultPreset;
@@ -20189,10 +20830,12 @@ var createDtmStudio = async (options = {}) => {
20189
20830
  const playPlayerNote = (e) => {
20190
20831
  const sfInst = resolveSoundFont(playerPreset, "chord");
20191
20832
  if (!sfInst) return;
20833
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20192
20834
  sfInst.play({
20193
20835
  ctx: audioCtx,
20194
20836
  destination: getChannelStrip("chord").input,
20195
- pitch: e.pitch,
20837
+ pitch: midi,
20838
+ detuneCents,
20196
20839
  volume: e.volume,
20197
20840
  velocity: e.velocity,
20198
20841
  when: e.when,
@@ -20330,8 +20973,12 @@ var createDtmStudio = async (options = {}) => {
20330
20973
  };
20331
20974
  };
20332
20975
  export {
20976
+ A4_HZ,
20977
+ A4_UNITS,
20978
+ CENTS_PER_UNIT,
20333
20979
  DAW_CSS,
20334
20980
  DEFAULT_BPM,
20981
+ DEFAULT_EDO,
20335
20982
  DEFAULT_GATE,
20336
20983
  DEFAULT_PAN,
20337
20984
  DEFAULT_PLAYBACK_VELOCITY,
@@ -20341,21 +20988,30 @@ export {
20341
20988
  DRUM_FONT,
20342
20989
  DRUM_KEYS,
20343
20990
  DRUM_PATTERNS,
20991
+ EDO31_NAMES,
20344
20992
  GM_INSTRUMENT_NAMES,
20345
20993
  INSTRUMENT_PRESETS,
20994
+ KEY_COUNT,
20346
20995
  KOE_BASE_URL,
20347
20996
  KOE_VOICEBANKS,
20348
20997
  KOE_VOICEBANK_LABELS,
20349
20998
  KOE_VOICEBANK_TERMS,
20350
20999
  LinkedList,
20351
21000
  MAX_VOCAL_VOLUME,
21001
+ MICRO_STEP,
20352
21002
  MMLCore,
20353
21003
  MML_END_MARKER,
20354
21004
  MidiSearchClient,
20355
- PITCH_MAP2 as PITCH_MAP,
21005
+ PITCH_ENCODING_VERSION,
21006
+ PITCH_MAP,
21007
+ PITCH_RANGE_END,
21008
+ PITCH_RANGE_START,
20356
21009
  PREWARM_NOTES,
20357
21010
  TRACKS_ADVANCED,
20358
21011
  TRACKS_SIMPLE,
21012
+ UNITS_PER_EDO31_DEGREE,
21013
+ UNITS_PER_OCTAVE,
21014
+ UNITS_PER_SEMITONE,
20359
21015
  VIBRATO_MIN_SEC,
20360
21016
  VOICE_IMAGES,
20361
21017
  VOICE_IMAGE_KEY,
@@ -20365,6 +21021,7 @@ export {
20365
21021
  buildChordPlacements,
20366
21022
  buildDrumPatternJson,
20367
21023
  buildNameToKeyMapping,
21024
+ chromaticStep,
20368
21025
  collectPitchTokens,
20369
21026
  concatFloat32,
20370
21027
  createAudioContext,
@@ -20373,19 +21030,13 @@ export {
20373
21030
  createKoeVoice,
20374
21031
  createLyricsConductor,
20375
21032
  createPianoRoll,
21033
+ createRenderer,
20376
21034
  createSequencer,
20377
21035
  createSingingVoices,
20378
21036
  createSynth,
20379
21037
  createVoiceRegistry,
20380
21038
  decodeMml,
20381
21039
  decomposeToMonophonic,
20382
- drawGrid,
20383
- drawHeader,
20384
- drawKeyboard,
20385
- drawNoteLyrics,
20386
- drawNotes,
20387
- drawSelectedNotes,
20388
- drawSelectionRect,
20389
21040
  encodeMml,
20390
21041
  encodeWavPCM16,
20391
21042
  exportMIDI,
@@ -20393,36 +21044,34 @@ export {
20393
21044
  extractMidiDrumPattern,
20394
21045
  extractMidiPlacements,
20395
21046
  extractMidiPlacementsByTrack,
21047
+ fifthToStep,
21048
+ fifthToUnits,
20396
21049
  formatMmlMeta,
20397
21050
  freqFromPitch,
20398
21051
  generateRandomPattern,
20399
- getDrawOffset,
20400
21052
  getDrumPatternKeys,
20401
- getGridCanvas,
20402
- getGridContext,
20403
- getGridPosition,
20404
- getHeaderCanvas,
20405
21053
  getMidiBPM,
20406
- getRenderConfig,
20407
- getXY,
20408
21054
  icon,
20409
- init,
20410
21055
  injectStyles,
20411
21056
  isChordHeavyTrack,
21057
+ isNaturalLetter,
20412
21058
  isPlausibleMidiTranscription,
20413
21059
  isValidHttpUrl,
21060
+ keyCountFor,
20414
21061
  koeUrl,
21062
+ midiToUnits,
20415
21063
  mountChordPlayer,
20416
21064
  mountDAW,
20417
21065
  mountMmlPlayer,
21066
+ naturalStep,
20418
21067
  normalizeDrumPatterns,
20419
21068
  normalizeLyrics,
20420
- onClick,
20421
21069
  panToStereo,
20422
21070
  parseCustomVocals,
20423
21071
  parseLyrics,
20424
21072
  parseMML,
20425
21073
  parseMmlMeta,
21074
+ pitchV1ToUnits,
20426
21075
  playChords,
20427
21076
  playMML,
20428
21077
  playNote,
@@ -20430,13 +21079,18 @@ export {
20430
21079
  playSingingMML,
20431
21080
  resolveDrumPattern,
20432
21081
  resolveLoopPoint,
20433
- setBackgroundActive,
20434
- setDrawOffset,
20435
21082
  shiftNotes,
20436
21083
  showLoadingOverlay,
21084
+ spellingToUnits,
20437
21085
  stripCustomVocals,
20438
21086
  stripLyrics,
20439
21087
  stripMmlMeta,
20440
21088
  transposeNotes,
21089
+ unitsPerRow,
21090
+ unitsPerStep,
21091
+ unitsToHz,
21092
+ unitsToMidi,
21093
+ unitsToMidiDetune,
21094
+ unitsToPitchV1,
20441
21095
  vocalVolumeToGain
20442
21096
  };