@onjmin/dtm 1.0.8 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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);
434
+ };
435
+ var add = (chord, n, half, out) => {
436
+ put(chord, out, n, half);
389
437
  };
390
- var aug = (chord) => {
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, {
@@ -8815,7 +8985,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
8815
8985
  0
8816
8986
  );
8817
8987
  const timedNotes = placements.map((p) => ({
8818
- pitch: p.pitch,
8988
+ pitch: unitsToPitchV1(p.pitchUnits),
8819
8989
  when: p.startStep * secondsPerStep,
8820
8990
  duration: p.durationSteps * secondsPerStep
8821
8991
  }));
@@ -8848,7 +9018,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
8848
9018
  id: id++,
8849
9019
  startStep: p.startStep,
8850
9020
  durationSteps: p.durationSteps,
8851
- pitch: p.pitch,
9021
+ pitchUnits: p.pitchUnits,
8852
9022
  velocity: DEFAULT_VELOCITY
8853
9023
  }));
8854
9024
  return {
@@ -9673,7 +9843,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9673
9843
  (a, b) => a.startStep - b.startStep
9674
9844
  );
9675
9845
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
9676
- const semis = (lt.octave ?? 0) * 12;
9846
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
9677
9847
  const count = Math.min(sorted.length, lt.syllables.length);
9678
9848
  const notes = [];
9679
9849
  for (let i2 = 0; i2 < count; i2++) {
@@ -9681,7 +9851,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9681
9851
  if (n.startStep < fromStep) continue;
9682
9852
  notes.push({
9683
9853
  syllable: lt.syllables[i2],
9684
- pitch: n.pitch + semis,
9854
+ pitch: n.pitchUnits + semis,
9685
9855
  startSec: (n.startStep - fromStep) * secondsPerStep,
9686
9856
  durationSec: n.durationSteps * secondsPerStep * gate
9687
9857
  });
@@ -9956,6 +10126,7 @@ var SoundFont = class _SoundFont {
9956
10126
  pitch = 60,
9957
10127
  volume = 1,
9958
10128
  velocity,
10129
+ detuneCents = 0,
9959
10130
  when = 0,
9960
10131
  duration = 1
9961
10132
  } = {}) {
@@ -9975,7 +10146,7 @@ var SoundFont = class _SoundFont {
9975
10146
  Object.assign(src, _param.src);
9976
10147
  const humanizeCents = (Math.random() * 2 - 1) * _SoundFont.humanizeDetuneCents;
9977
10148
  const humanizeGainMul = 1 + (Math.random() * 2 - 1) * _SoundFont.humanizeGainRatio;
9978
- src.detune.setValueAtTime(humanizeCents, 0);
10149
+ src.detune.setValueAtTime(humanizeCents + detuneCents, 0);
9979
10150
  const effectiveVolume = volume * humanizeGainMul;
9980
10151
  const brightness = velocity === void 0 ? 1 : Math.max(0, Math.min(1, velocity / 127));
9981
10152
  const filter = brightness >= _SoundFont.brightnessBypassAbove ? void 0 : ctx.createBiquadFilter();
@@ -11114,7 +11285,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11114
11285
  scheduleMetronomeBeats(ctx, cutGain);
11115
11286
  };
11116
11287
  const wafPlayDynamic = ({
11117
- pitch,
11288
+ pitchUnits,
11118
11289
  velocity,
11119
11290
  volume,
11120
11291
  when,
@@ -11124,10 +11295,12 @@ var mountChordPlayer = (target, chords, options = {}) => {
11124
11295
  }) => {
11125
11296
  const waf = activeGmName ? _wafCache.get(activeGmName) : null;
11126
11297
  if (waf) {
11298
+ const { midi, detuneCents } = unitsToMidiDetune(pitchUnits);
11127
11299
  waf.play({
11128
11300
  ctx,
11129
11301
  destination,
11130
- pitch,
11302
+ pitch: midi,
11303
+ detuneCents,
11131
11304
  volume: volume * 0.85,
11132
11305
  velocity,
11133
11306
  when,
@@ -11137,7 +11310,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11137
11310
  const synth = createSynth(ctx, destination);
11138
11311
  synth.playNote({
11139
11312
  trackId: "chord",
11140
- pitch,
11313
+ pitchUnits,
11141
11314
  velocity: 100,
11142
11315
  volume,
11143
11316
  when,
@@ -11166,7 +11339,8 @@ var mountChordPlayer = (target, chords, options = {}) => {
11166
11339
  trackIndex: 3,
11167
11340
  startStep: whenStep,
11168
11341
  durationSteps,
11169
- pitch: C32 + noteOffset,
11342
+ // chord-parser の構成音は12平均律の半音。内部表現の units へ写す。
11343
+ pitchUnits: pitchV1ToUnits(C32 + noteOffset),
11170
11344
  velocity: 100
11171
11345
  });
11172
11346
  }
@@ -11179,7 +11353,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11179
11353
  synth: false,
11180
11354
  onPlayNote: (e) => {
11181
11355
  wafPlayDynamic({
11182
- pitch: e.pitch,
11356
+ pitchUnits: e.pitchUnits,
11183
11357
  velocity: e.velocity,
11184
11358
  volume: e.volume,
11185
11359
  when: e.when,
@@ -11621,6 +11795,14 @@ var buildUI = (target, options) => {
11621
11795
  <summary>\u5168\u4F53\u30C8\u30E9\u30C3\u30AF\u8A2D\u5B9A</summary>
11622
11796
  <div class="dtm-panel-body">
11623
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>
11624
11806
  <div class="dtm-row">
11625
11807
  <span class="dtm-label">\u5168\u4F53\u97F3\u91CF</span>
11626
11808
  <input type="range" class="dtm-range dtm-grow" data-dtm="master-volume" value="50" min="0" max="100">
@@ -11953,6 +12135,8 @@ var buildUI = (target, options) => {
11953
12135
  copyMiniBtn: sel("copy-mini"),
11954
12136
  overlay: sel("overlay"),
11955
12137
  mmlInfoBtn: sel("mml-info"),
12138
+ edoSelect: sel("edo-select"),
12139
+ edoInfoBtn: sel("edo-info"),
11956
12140
  modalOverlay: sel("modal-overlay"),
11957
12141
  modalTitle: sel("modal-title"),
11958
12142
  modalBody: sel("modal-body"),
@@ -12073,17 +12257,21 @@ var SCALES = [
12073
12257
  [0, 2, 4, 7, 9]
12074
12258
  // Pentatonic Major
12075
12259
  ];
12260
+ var MEANTONE_STEP_BY_SEMITONE2 = [0, 3, 5, 8, 10, 13, 16, 18, 21, 23, 26, 28];
12076
12261
  var generateRandomPattern = (core, options) => {
12077
- const { stepsPerBar, startStep, pitchRangeStart } = options;
12262
+ const { stepsPerBar, startStep, pitchRangeStart, edo = 12 } = options;
12078
12263
  const numBars = 8;
12079
12264
  const noteLength = 24;
12080
- 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;
12081
12268
  const scale = SCALES[Math.floor(Math.random() * SCALES.length)];
12082
12269
  const rootOffset = Math.floor(Math.random() * 12);
12083
12270
  const availablePitches = [];
12084
12271
  for (let i2 = 0; i2 < 12; i2++) {
12085
12272
  const noteInOctave = (i2 - rootOffset + 12) % 12;
12086
- if (scale.includes(noteInOctave)) availablePitches.push(basePitch + i2);
12273
+ if (scale.includes(noteInOctave))
12274
+ availablePitches.push(basePitch + semitoneToStep(i2) * upr);
12087
12275
  }
12088
12276
  core.beginBatch();
12089
12277
  for (let bar = 0; bar < numBars; bar++) {
@@ -12118,14 +12306,17 @@ var applyHarmonicFilter = (targetCore, chordCore, options) => {
12118
12306
  const isNewBar = halfBar % 2 === 0;
12119
12307
  const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12120
12308
  if (chordHere.length > 0) {
12121
- currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
12309
+ currentClasses = new Set(
12310
+ chordHere.map((n) => n.pitchUnits % UNITS_PER_OCTAVE)
12311
+ );
12122
12312
  } else if (isNewBar) {
12123
12313
  currentClasses = /* @__PURE__ */ new Set();
12124
12314
  }
12125
12315
  if (currentClasses.size === 0) continue;
12126
12316
  const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12127
12317
  for (const n of activeHere) {
12128
- if (!currentClasses.has(n.pitch % 12)) targetCore.deleteNoteById(n.id);
12318
+ if (!currentClasses.has(n.pitchUnits % UNITS_PER_OCTAVE))
12319
+ targetCore.deleteNoteById(n.id);
12129
12320
  }
12130
12321
  }
12131
12322
  targetCore.endBatch();
@@ -12147,13 +12338,17 @@ var applyMonophonic = (targetCore, chordCore, options) => {
12147
12338
  const isNewBar = halfBar % 2 === 0;
12148
12339
  const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12149
12340
  if (chordHere.length > 0) {
12150
- currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
12341
+ currentClasses = new Set(
12342
+ chordHere.map((n) => n.pitchUnits % UNITS_PER_OCTAVE)
12343
+ );
12151
12344
  } else if (isNewBar) {
12152
12345
  currentClasses = /* @__PURE__ */ new Set();
12153
12346
  }
12154
12347
  if (currentClasses.size === 0) continue;
12155
12348
  const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12156
- 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
+ );
12157
12352
  const filteredIds = new Set(filtered.map((n) => n.id));
12158
12353
  for (const n of activeHere) {
12159
12354
  if (!filteredIds.has(n.id)) targetCore.deleteNoteById(n.id);
@@ -12165,7 +12360,7 @@ var applyMonophonic = (targetCore, chordCore, options) => {
12165
12360
  }
12166
12361
  for (const notesAtTime of timeMap.values()) {
12167
12362
  if (notesAtTime.length > 1) {
12168
- notesAtTime.sort((a, b) => b.pitch - a.pitch);
12363
+ notesAtTime.sort((a, b) => b.pitchUnits - a.pitchUnits);
12169
12364
  const [, ...others] = notesAtTime;
12170
12365
  for (const on of others) targetCore.deleteNoteById(on.id);
12171
12366
  }
@@ -12181,18 +12376,21 @@ var shiftNotes = (cores, shiftSteps) => {
12181
12376
  for (const note of notes) {
12182
12377
  const newStart = note.startStep + shiftSteps;
12183
12378
  if (newStart < 0) core.deleteNoteById(note.id);
12184
- else core.moveNote(note.id, newStart, note.pitch);
12379
+ else core.moveNote(note.id, newStart, note.pitchUnits);
12185
12380
  }
12186
12381
  core.saveHistory();
12187
12382
  }
12188
12383
  };
12189
- var transposeNotes = (cores, semitones) => {
12190
- if (semitones === 0) return;
12384
+ var transposeNotes = (cores, steps) => {
12385
+ if (steps === 0) return;
12191
12386
  for (const core of cores) {
12192
12387
  const notes = [...core.getNotes()];
12193
12388
  for (const note of notes) {
12194
- const newPitch = Math.max(0, Math.min(127, note.pitch + semitones));
12195
- 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) {
12196
12394
  core.moveNote(note.id, note.startStep, newPitch);
12197
12395
  }
12198
12396
  }
@@ -12517,11 +12715,31 @@ var exportMIDI = (options) => {
12517
12715
  const div = 480;
12518
12716
  const tickPerStep = div / STEPS_PER_BEAT3;
12519
12717
  const midiTracks = [];
12520
- 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) => {
12521
12736
  if (track.notes.length === 0) return;
12522
- const channel = ch < 9 ? ch : ch + 1 & 15;
12523
12737
  const events = [];
12524
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));
12525
12743
  const startTick = Math.round(n.startStep * tickPerStep);
12526
12744
  const endTick = Math.round(
12527
12745
  (n.startStep + (n.durationSteps || 1)) * tickPerStep
@@ -12529,12 +12747,25 @@ var exportMIDI = (options) => {
12529
12747
  const vel = Math.round(
12530
12748
  (n.velocity ?? DEFAULT_VELOCITY) * (track.volume ?? 100) / 100
12531
12749
  );
12532
- events.push({ t: startTick, m: [144 | channel, n.pitch, vel] });
12533
- 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] });
12534
12752
  }
12535
12753
  events.sort((a, b) => a.t - b.t);
12536
12754
  midiTracks.push(events);
12537
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
+ }
12538
12769
  const maxStep = Math.max(
12539
12770
  ...tracks.filter((t) => t.notes.length > 0).map(
12540
12771
  (t) => Math.max(...t.notes.map((n) => n.startStep + n.durationSteps))
@@ -12571,6 +12802,7 @@ var exportMIDI = (options) => {
12571
12802
  headerChunks(arr, midiTracks.length + 1, div);
12572
12803
  trackChunks(arr, (a) => {
12573
12804
  a.push(0, 255, 81, 3, ...to3byte(Math.round(6e7 / bpm)));
12805
+ for (const ev of bendSetup) a.push(0, ...ev.m);
12574
12806
  });
12575
12807
  for (const events of midiTracks) {
12576
12808
  trackChunks(arr, (a) => {
@@ -12817,403 +13049,8 @@ var LinkedList = class {
12817
13049
  }
12818
13050
  };
12819
13051
 
12820
- // src/renderer.ts
12821
- var g_header_canvas;
12822
- var g_key_canvas;
12823
- var g_grid_canvas;
12824
- var g_header_ctx;
12825
- var g_key_ctx;
12826
- var g_grid_ctx;
12827
- var g_config;
12828
- var KEYBOARD_WIDTH = 60;
12829
- var HEADER_HEIGHT = 20;
12830
- var getRenderConfig = () => g_config;
12831
- var g_draw_offset_x = 0;
12832
- var g_draw_offset_y = 0;
12833
- var g_bg_active = false;
12834
- var setBackgroundActive = (active) => {
12835
- g_bg_active = active;
12836
- };
12837
- var getDrawOffset = () => ({
12838
- x: g_draw_offset_x,
12839
- y: g_draw_offset_y
12840
- });
12841
- var getGridCanvas = () => g_grid_canvas;
12842
- var getGridContext = () => g_grid_ctx;
12843
- var getHeaderCanvas = () => g_header_canvas;
12844
- var init = (mountTarget, width = 800, height = 450, config) => {
12845
- g_config = config;
12846
- const headerCanvas = document.createElement("canvas");
12847
- g_header_canvas = headerCanvas;
12848
- headerCanvas.width = width - KEYBOARD_WIDTH;
12849
- headerCanvas.height = HEADER_HEIGHT;
12850
- headerCanvas.style.position = "absolute";
12851
- headerCanvas.style.left = `${KEYBOARD_WIDTH}px`;
12852
- headerCanvas.style.top = "0px";
12853
- const headerCtx = headerCanvas.getContext("2d");
12854
- if (!headerCtx)
12855
- throw new Error("Failed to get 2D rendering context for header.");
12856
- g_header_ctx = headerCtx;
12857
- const keyCanvas = document.createElement("canvas");
12858
- g_key_canvas = keyCanvas;
12859
- keyCanvas.width = KEYBOARD_WIDTH;
12860
- keyCanvas.height = height - HEADER_HEIGHT;
12861
- keyCanvas.style.position = "absolute";
12862
- keyCanvas.style.left = "0px";
12863
- keyCanvas.style.top = `${HEADER_HEIGHT}px`;
12864
- const keyCtx = keyCanvas.getContext("2d");
12865
- if (!keyCtx)
12866
- throw new Error("Failed to get 2D rendering context for keyboard.");
12867
- g_key_ctx = keyCtx;
12868
- const gridCanvas = document.createElement("canvas");
12869
- g_grid_canvas = gridCanvas;
12870
- gridCanvas.width = width - KEYBOARD_WIDTH;
12871
- gridCanvas.height = height - HEADER_HEIGHT;
12872
- gridCanvas.style.position = "absolute";
12873
- gridCanvas.style.left = `${KEYBOARD_WIDTH}px`;
12874
- gridCanvas.style.top = `${HEADER_HEIGHT}px`;
12875
- gridCanvas.style.touchAction = "none";
12876
- gridCanvas.style.userSelect = "none";
12877
- const gridCtx = gridCanvas.getContext("2d", { willReadFrequently: true });
12878
- if (!gridCtx) throw new Error("Failed to get 2D rendering context for grid.");
12879
- g_grid_ctx = gridCtx;
12880
- mountTarget.innerHTML = "";
12881
- mountTarget.style.position = "relative";
12882
- mountTarget.style.width = `${width + KEYBOARD_WIDTH}px`;
12883
- mountTarget.style.height = `${height}px`;
12884
- mountTarget.append(headerCanvas, keyCanvas, gridCanvas);
12885
- drawHeaderCorner();
12886
- };
12887
- var blackKeyPitches = /* @__PURE__ */ new Set([1, 3, 6, 8, 10]);
12888
- var KEY_NAMES = [
12889
- "C",
12890
- "C#",
12891
- "D",
12892
- "D#",
12893
- "E",
12894
- "F",
12895
- "F#",
12896
- "G",
12897
- "G#",
12898
- "A",
12899
- "A#",
12900
- "B"
12901
- ];
12902
- var drawHeaderCorner = () => {
12903
- const mountTarget = g_key_canvas.parentElement;
12904
- if (!mountTarget) return;
12905
- let cornerDiv = mountTarget.querySelector("#header-corner");
12906
- if (!cornerDiv) {
12907
- cornerDiv = document.createElement("div");
12908
- cornerDiv.id = "header-corner";
12909
- cornerDiv.style.position = "absolute";
12910
- cornerDiv.style.left = "0px";
12911
- cornerDiv.style.top = "0px";
12912
- cornerDiv.style.width = `${KEYBOARD_WIDTH}px`;
12913
- cornerDiv.style.height = `${HEADER_HEIGHT}px`;
12914
- cornerDiv.style.backgroundColor = "#0a0f1f";
12915
- cornerDiv.style.borderRight = "2px solid #29adff";
12916
- cornerDiv.style.borderBottom = "2px solid #29adff";
12917
- mountTarget.insertBefore(cornerDiv, g_header_canvas);
12918
- }
12919
- };
12920
- var drawKeyboard = () => {
12921
- g_key_ctx.clearRect(0, 0, g_key_canvas.width, g_key_canvas.height);
12922
- const { keyHeight, keyCount, pitchRangeStart } = g_config;
12923
- const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
12924
- const endY = g_draw_offset_y + g_key_canvas.height;
12925
- const WHITE_KEY = "#ccc8b4";
12926
- const BLACK_KEY = "#111111";
12927
- const BK_EDGE = "#383838";
12928
- const WW_SEP = "#807a6a";
12929
- const BK_RATIO = 0.62;
12930
- for (let y = startY; y < endY; y += keyHeight) {
12931
- const pitchIndex = keyCount - 1 - y / keyHeight;
12932
- const totalPitch = pitchIndex + pitchRangeStart;
12933
- const pitchMod12 = totalPitch % 12;
12934
- const isBlackKey = blackKeyPitches.has(pitchMod12);
12935
- const octave = Math.floor(totalPitch / 12) - 1;
12936
- const isC4Range = octave === 4;
12937
- const screenY = y - g_draw_offset_y;
12938
- const bkW = Math.floor(KEYBOARD_WIDTH * BK_RATIO);
12939
- if (isBlackKey) {
12940
- g_key_ctx.fillStyle = isC4Range ? "#d8d4be" : WHITE_KEY;
12941
- g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
12942
- g_key_ctx.fillStyle = isC4Range ? "#1a1408" : BLACK_KEY;
12943
- g_key_ctx.fillRect(0, screenY, bkW, keyHeight);
12944
- g_key_ctx.strokeStyle = BK_EDGE;
12945
- g_key_ctx.lineWidth = 1;
12946
- g_key_ctx.beginPath();
12947
- g_key_ctx.moveTo(bkW, screenY);
12948
- g_key_ctx.lineTo(bkW, screenY + keyHeight);
12949
- g_key_ctx.stroke();
12950
- } else {
12951
- g_key_ctx.fillStyle = isC4Range ? "#dedad0" : WHITE_KEY;
12952
- g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
12953
- if (pitchMod12 === 5 || pitchMod12 === 0) {
12954
- g_key_ctx.strokeStyle = WW_SEP;
12955
- g_key_ctx.lineWidth = 1;
12956
- g_key_ctx.beginPath();
12957
- g_key_ctx.moveTo(0, screenY + keyHeight - 0.5);
12958
- g_key_ctx.lineTo(KEYBOARD_WIDTH, screenY + keyHeight - 0.5);
12959
- g_key_ctx.stroke();
12960
- }
12961
- }
12962
- if (pitchMod12 === 0) {
12963
- const octave2 = Math.floor(totalPitch / 12) - 1;
12964
- g_key_ctx.fillStyle = "#555040";
12965
- g_key_ctx.font = "10px 'k8x12',monospace";
12966
- g_key_ctx.textAlign = "right";
12967
- g_key_ctx.textBaseline = "bottom";
12968
- g_key_ctx.fillText(
12969
- `${KEY_NAMES[pitchMod12]}${octave2}`,
12970
- KEYBOARD_WIDTH - 4,
12971
- screenY + keyHeight - 2
12972
- );
12973
- }
12974
- }
12975
- g_key_ctx.beginPath();
12976
- g_key_ctx.strokeStyle = "#29adff";
12977
- g_key_ctx.lineWidth = 2;
12978
- g_key_ctx.moveTo(KEYBOARD_WIDTH, 0);
12979
- g_key_ctx.lineTo(KEYBOARD_WIDTH, g_key_canvas.height);
12980
- g_key_ctx.stroke();
12981
- };
12982
- var drawHeader = () => {
12983
- g_header_ctx.clearRect(0, 0, g_header_canvas.width, g_header_canvas.height);
12984
- const { stepWidth, stepsPerBar } = g_config;
12985
- g_header_ctx.save();
12986
- g_header_ctx.translate(-g_draw_offset_x, 0);
12987
- g_header_ctx.fillStyle = g_bg_active ? "rgba(10,15,31,0.55)" : "#0a0f1f";
12988
- g_header_ctx.fillRect(
12989
- g_draw_offset_x,
12990
- 0,
12991
- g_header_canvas.width,
12992
- HEADER_HEIGHT
12993
- );
12994
- g_header_ctx.strokeStyle = "#3d405b";
12995
- g_header_ctx.lineWidth = 1;
12996
- g_header_ctx.font = "11px 'k8x12',monospace";
12997
- g_header_ctx.fillStyle = "#83769c";
12998
- const startBar = Math.floor(g_draw_offset_x / (stepsPerBar * stepWidth));
12999
- const endBar = Math.ceil(
13000
- (g_draw_offset_x + g_header_canvas.width) / (stepsPerBar * stepWidth)
13001
- );
13002
- for (let bar = startBar; bar <= endBar + 1; bar++) {
13003
- const x2 = bar * stepsPerBar * stepWidth;
13004
- const screenX = x2;
13005
- g_header_ctx.beginPath();
13006
- g_header_ctx.moveTo(screenX, 0);
13007
- g_header_ctx.lineTo(screenX, HEADER_HEIGHT);
13008
- g_header_ctx.stroke();
13009
- if (bar >= 0) {
13010
- g_header_ctx.textAlign = "left";
13011
- g_header_ctx.textBaseline = "middle";
13012
- g_header_ctx.fillText(`${bar + 1}`, screenX + 5, HEADER_HEIGHT / 2);
13013
- }
13014
- }
13015
- g_header_ctx.restore();
13016
- };
13017
- var drawGrid = (noteLengthSteps = 1) => {
13018
- drawKeyboard();
13019
- drawHeader();
13020
- g_grid_ctx.clearRect(0, 0, g_grid_canvas.width, g_grid_canvas.height);
13021
- const { keyHeight, keyCount, stepWidth, stepsPerBar, pitchRangeStart } = g_config;
13022
- const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
13023
- const endY = g_draw_offset_y + g_grid_canvas.height;
13024
- for (let y = startY; y < endY; y += keyHeight) {
13025
- const pitchIndex = keyCount - 1 - y / keyHeight;
13026
- const totalPitch = pitchIndex + pitchRangeStart;
13027
- const pitchMod12 = totalPitch % 12;
13028
- const isBlackKey = blackKeyPitches.has(pitchMod12);
13029
- const isC = pitchMod12 === 0;
13030
- const octave = Math.floor(totalPitch / 12) - 1;
13031
- const isC4Range = octave === 4;
13032
- const screenY = y - g_draw_offset_y;
13033
- g_grid_ctx.fillStyle = g_bg_active ? isBlackKey ? "rgba(8,11,22,0.55)" : "rgba(17,22,40,0.45)" : isBlackKey ? "#080b16" : "#111628";
13034
- g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13035
- if (isC4Range) {
13036
- g_grid_ctx.fillStyle = "rgba(41,173,255,0.05)";
13037
- g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13038
- }
13039
- g_grid_ctx.beginPath();
13040
- g_grid_ctx.strokeStyle = isC ? "#3d405b" : "#1a1d30";
13041
- g_grid_ctx.lineWidth = 1;
13042
- const lineY = screenY + keyHeight;
13043
- g_grid_ctx.moveTo(0, lineY);
13044
- g_grid_ctx.lineTo(g_grid_canvas.width, lineY);
13045
- g_grid_ctx.stroke();
13046
- }
13047
- const gridStep = noteLengthSteps || 48;
13048
- const startX = Math.floor(g_draw_offset_x / (stepWidth * gridStep)) * stepWidth * gridStep;
13049
- const endX = g_draw_offset_x + g_grid_canvas.width;
13050
- const lineStep = stepWidth * gridStep;
13051
- for (let x2 = startX; x2 <= endX; x2 += lineStep) {
13052
- const step = x2 / stepWidth;
13053
- const isBarLine = step % stepsPerBar === 0;
13054
- const isNoteLine = step % gridStep === 0;
13055
- const screenX = x2 - g_draw_offset_x;
13056
- g_grid_ctx.beginPath();
13057
- g_grid_ctx.strokeStyle = isBarLine ? "#3d405b" : isNoteLine ? "#242840" : "#1a1d30";
13058
- g_grid_ctx.lineWidth = isBarLine ? 2 : 1;
13059
- g_grid_ctx.moveTo(screenX, 0);
13060
- g_grid_ctx.lineTo(screenX, g_grid_canvas.height);
13061
- g_grid_ctx.stroke();
13062
- }
13063
- };
13064
- var drawNotes = (notes, color = [59, 130, 246, 1], isActive = true) => {
13065
- const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
13066
- const canvasWidth = g_grid_canvas.width;
13067
- const canvasHeight = g_grid_canvas.height;
13068
- const [r, g, b, a] = color;
13069
- for (const note of notes) {
13070
- const logicalX = note.startStep * stepWidth;
13071
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
13072
- const logicalY = yIndex * keyHeight;
13073
- const w = note.durationSteps * stepWidth;
13074
- const h = keyHeight;
13075
- const renderX = logicalX - g_draw_offset_x;
13076
- const renderY = logicalY - g_draw_offset_y;
13077
- if (renderX + w < 0 || renderX > canvasWidth) continue;
13078
- if (renderY + h < 0 || renderY > canvasHeight) continue;
13079
- if (isActive) {
13080
- const velocityOpacity = note.velocity !== void 0 ? 0.6 + note.velocity / 127 * 0.4 : 1;
13081
- const finalOpacity = a * velocityOpacity;
13082
- g_grid_ctx.fillStyle = `rgba(${r},${g},${b},${finalOpacity})`;
13083
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13084
- if (w >= 4 && h >= 4) {
13085
- g_grid_ctx.fillStyle = "rgba(255,255,255,0.4)";
13086
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, 1);
13087
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, 1, h - 2);
13088
- g_grid_ctx.fillStyle = "rgba(0,0,0,0.45)";
13089
- g_grid_ctx.fillRect(renderX + 1, renderY + h - 2, w - 2, 1);
13090
- g_grid_ctx.fillRect(renderX + w - 2, renderY + 1, 1, h - 2);
13091
- }
13092
- } else {
13093
- const velocityOpacity = note.velocity !== void 0 ? 0.7 + note.velocity / 127 * 0.3 : 1;
13094
- const finalOpacity = Math.min(0.25, a * 0.22) * velocityOpacity;
13095
- const gray = 0.299 * r + 0.587 * g + 0.114 * b;
13096
- const ghostR = Math.round(r * 0.6 + gray * 0.4);
13097
- const ghostG = Math.round(g * 0.6 + gray * 0.4);
13098
- const ghostB = Math.round(b * 0.6 + gray * 0.4);
13099
- g_grid_ctx.fillStyle = `rgba(${ghostR},${ghostG},${ghostB},${finalOpacity})`;
13100
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13101
- }
13102
- }
13103
- };
13104
- var drawNoteLyrics = (notes, syllables) => {
13105
- if (syllables.length === 0) return;
13106
- const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
13107
- if (keyHeight < 10) return;
13108
- const fontSize = Math.min(12, Math.floor(keyHeight * 0.85));
13109
- const sorted = [...notes].sort((a, b) => a.startStep - b.startStep);
13110
- const count = Math.min(sorted.length, syllables.length);
13111
- g_grid_ctx.save();
13112
- g_grid_ctx.font = `${fontSize}px 'k8x12',sans-serif`;
13113
- g_grid_ctx.textAlign = "left";
13114
- g_grid_ctx.textBaseline = "middle";
13115
- g_grid_ctx.lineWidth = 3;
13116
- g_grid_ctx.lineJoin = "round";
13117
- for (let i2 = 0; i2 < count; i2++) {
13118
- const kana = syllables[i2];
13119
- if (!kana) continue;
13120
- const note = sorted[i2];
13121
- const renderX = note.startStep * stepWidth - g_draw_offset_x;
13122
- const renderY = (keyCount - 1 - (note.pitch - pitchRangeStart)) * keyHeight - g_draw_offset_y;
13123
- const w = note.durationSteps * stepWidth;
13124
- if (w < 6) continue;
13125
- if (renderX + w < 0 || renderX > g_grid_canvas.width) continue;
13126
- if (renderY + keyHeight < 0 || renderY > g_grid_canvas.height) continue;
13127
- const textX = renderX + 2;
13128
- const textY = renderY + keyHeight / 2;
13129
- g_grid_ctx.save();
13130
- g_grid_ctx.beginPath();
13131
- g_grid_ctx.rect(renderX + 1, renderY + 1, w - 2, keyHeight - 2);
13132
- g_grid_ctx.clip();
13133
- g_grid_ctx.strokeStyle = "rgba(0,0,0,0.85)";
13134
- g_grid_ctx.strokeText(kana, textX, textY);
13135
- g_grid_ctx.fillStyle = "#fff1e8";
13136
- g_grid_ctx.fillText(kana, textX, textY);
13137
- g_grid_ctx.restore();
13138
- }
13139
- g_grid_ctx.restore();
13140
- };
13141
- var drawSelectionRect = (rect) => {
13142
- if (!rect) return;
13143
- g_grid_ctx.save();
13144
- g_grid_ctx.strokeStyle = "#ffec27";
13145
- g_grid_ctx.lineWidth = 2;
13146
- g_grid_ctx.setLineDash([4, 4]);
13147
- g_grid_ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
13148
- g_grid_ctx.fillStyle = "rgba(255,236,39,0.08)";
13149
- g_grid_ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
13150
- g_grid_ctx.restore();
13151
- };
13152
- var drawSelectedNotes = (notes, selectedIds, baseColor = [59, 130, 246, 1]) => {
13153
- const { keyHeight, stepWidth, keyCount, pitchRangeStart } = g_config;
13154
- for (const note of notes) {
13155
- if (!selectedIds.has(note.id)) continue;
13156
- const logicalX = note.startStep * stepWidth;
13157
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
13158
- const logicalY = yIndex * keyHeight;
13159
- const w = note.durationSteps * stepWidth;
13160
- const h = keyHeight;
13161
- const renderX = logicalX - g_draw_offset_x;
13162
- const renderY = logicalY - g_draw_offset_y;
13163
- const velocityOpacity = note.velocity !== void 0 ? 0.5 + note.velocity / 127 * 0.5 : 1;
13164
- const [r, g, b, a] = baseColor;
13165
- const darkenFactor = 1.3;
13166
- const darkerR = Math.min(255, r * darkenFactor);
13167
- const darkerG = Math.min(255, g * darkenFactor);
13168
- const darkerB = Math.min(255, b * darkenFactor);
13169
- const finalOpacity = a * velocityOpacity;
13170
- g_grid_ctx.fillStyle = `rgba(${darkerR},${darkerG},${darkerB},${finalOpacity})`;
13171
- g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13172
- }
13173
- };
13174
- var getXY = (e) => {
13175
- const { clientX, clientY } = e;
13176
- const rect = g_grid_canvas.getBoundingClientRect();
13177
- const x2 = Math.floor(clientX - rect.left);
13178
- const y = Math.floor(clientY - rect.top);
13179
- return [x2, y, e.buttons];
13180
- };
13181
- var getGridPosition = (e) => {
13182
- const [x2, y] = getXY(e);
13183
- const { keyCount, pitchRangeStart, keyHeight, stepWidth } = g_config;
13184
- const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
13185
- const absoluteY = y + g_draw_offset_y;
13186
- const yIndex = Math.floor(absoluteY / keyHeight);
13187
- const pitch = keyCount - 1 - yIndex + pitchRangeStart;
13188
- return { step, pitch, x: x2, y };
13189
- };
13190
- var onClick = (callback) => {
13191
- g_grid_canvas.addEventListener(
13192
- "click",
13193
- (e) => {
13194
- const [x2, y] = getXY(e);
13195
- const { keyCount, pitchRangeStart, keyHeight, stepWidth } = g_config;
13196
- const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
13197
- const absoluteY = y + g_draw_offset_y;
13198
- const yIndex = Math.floor(absoluteY / keyHeight);
13199
- const pitch = keyCount - 1 - yIndex + pitchRangeStart;
13200
- if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount) {
13201
- requestAnimationFrame(() => callback(step, pitch));
13202
- }
13203
- },
13204
- { passive: true }
13205
- );
13206
- g_grid_canvas.addEventListener("contextmenu", (e) => e.preventDefault());
13207
- };
13208
- var setDrawOffset = (x2, y) => {
13209
- g_draw_offset_x = x2;
13210
- g_draw_offset_y = y;
13211
- drawKeyboard();
13212
- drawHeader();
13213
- };
13214
-
13215
13052
  // src/mml-core.ts
13216
- var PITCH_MAP2 = [
13053
+ var PITCH_MAP = [
13217
13054
  "c",
13218
13055
  "c+",
13219
13056
  "d",
@@ -13227,6 +13064,39 @@ var PITCH_MAP2 = [
13227
13064
  "a+",
13228
13065
  "b"
13229
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
+ ];
13230
13100
  var MMLCore = class _MMLCore {
13231
13101
  notes = [];
13232
13102
  nextNoteId = 0;
@@ -13240,9 +13110,12 @@ var MMLCore = class _MMLCore {
13240
13110
  lastUndoTime = 0;
13241
13111
  static UNDO_DEBOUNCE_MS = 100;
13242
13112
  toolMode = "pen";
13243
- constructor(handlers, volume = 80) {
13113
+ /** グリッド寸法の供給元(描画器ではなく呼び出し側が持つ設定を読む) */
13114
+ getConfig;
13115
+ constructor(handlers, volume = 80, getConfig) {
13244
13116
  this.handlers = handlers;
13245
13117
  this.volume = volume;
13118
+ this.getConfig = getConfig;
13246
13119
  this.lastHistorySnapshot = JSON.stringify(this.notes);
13247
13120
  this.history.add([]);
13248
13121
  this.generateAndNotify();
@@ -13329,14 +13202,14 @@ var MMLCore = class _MMLCore {
13329
13202
  */
13330
13203
  addNote(step, pitch, options) {
13331
13204
  const existingIndex = this.notes.findIndex(
13332
- (n) => n.startStep === step && n.pitch === pitch
13205
+ (n) => n.startStep === step && n.pitchUnits === pitch
13333
13206
  );
13334
13207
  if (existingIndex === -1) {
13335
13208
  const newNote = {
13336
13209
  id: this.nextNoteId++,
13337
13210
  startStep: step,
13338
13211
  durationSteps: options.noteLengthSteps,
13339
- pitch,
13212
+ pitchUnits: pitch,
13340
13213
  velocity: options.velocity ?? DEFAULT_VELOCITY
13341
13214
  };
13342
13215
  this.notes.push(newNote);
@@ -13364,9 +13237,11 @@ var MMLCore = class _MMLCore {
13364
13237
  moveNote(noteId, startStep, pitch) {
13365
13238
  const note = this.notes.find((target) => target.id === noteId);
13366
13239
  if (!note) return;
13367
- const totalSteps = this.getMaxStep() + getRenderConfig().stepsPerBar;
13368
- const pitchRangeStart = getRenderConfig().pitchRangeStart;
13369
- 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;
13370
13245
  const clampedPitch = Math.min(
13371
13246
  Math.max(pitch, pitchRangeStart),
13372
13247
  pitchRangeEnd
@@ -13376,7 +13251,7 @@ var MMLCore = class _MMLCore {
13376
13251
  totalSteps - note.durationSteps
13377
13252
  );
13378
13253
  note.startStep = clampedStart;
13379
- note.pitch = clampedPitch;
13254
+ note.pitchUnits = clampedPitch;
13380
13255
  this.notes.sort((a, b) => a.startStep - b.startStep);
13381
13256
  this.generateAndNotify();
13382
13257
  }
@@ -13421,7 +13296,7 @@ var MMLCore = class _MMLCore {
13421
13296
  * ただし、残りステップ(limit)は絶対に超えない。
13422
13297
  */
13423
13298
  stepsToMMLDuration(steps, limit) {
13424
- const config = getRenderConfig();
13299
+ const config = this.getConfig();
13425
13300
  const total = config.stepsPerBar;
13426
13301
  const candidates = [
13427
13302
  { dur: "1.", s: total * 1.5 },
@@ -13457,7 +13332,7 @@ var MMLCore = class _MMLCore {
13457
13332
  * ギャップに収まる最大の音符を探す(減算アルゴリズム用)
13458
13333
  */
13459
13334
  findBestFitDuration(gap) {
13460
- const config = getRenderConfig();
13335
+ const config = this.getConfig();
13461
13336
  const durations = [1, 2, 4, 8, 12, 16, 24, 32, 48, 64];
13462
13337
  for (const d of durations) {
13463
13338
  const stepLen = config.stepsPerBar / d;
@@ -13467,12 +13342,20 @@ var MMLCore = class _MMLCore {
13467
13342
  }
13468
13343
  return { dur: 64, steps: config.stepsPerBar / 64 };
13469
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
+ }
13470
13354
  /**
13471
13355
  * ピッチからオクターブ最適化のある音名を取得
13472
13356
  */
13473
13357
  getNoteWithOctave(pitch, lastOctave) {
13474
- const octave = Math.floor(pitch / 12) - 1;
13475
- const name = PITCH_MAP2[pitch % 12];
13358
+ const { octave, name } = this.spell(pitch);
13476
13359
  if (lastOctave === -1 || Math.abs(octave - lastOctave) >= 2) {
13477
13360
  return { text: `o${octave}${name}`, currentOctave: octave };
13478
13361
  }
@@ -13495,7 +13378,7 @@ var MMLCore = class _MMLCore {
13495
13378
  * 各音符の長さを忠実に出力する(次の音符がなければ曲末まで伸ばせる)。
13496
13379
  */
13497
13380
  generateMML = (volumeOverride) => {
13498
- const config = getRenderConfig();
13381
+ const config = this.getConfig();
13499
13382
  const vol = volumeOverride ?? this.volume;
13500
13383
  const header = `t${this.tempo} v${vol}`;
13501
13384
  const segments = [];
@@ -13536,14 +13419,13 @@ var MMLCore = class _MMLCore {
13536
13419
  const actualStepGenerated = this.getStepFromDottedMML(durStr);
13537
13420
  if (notes.length > 1) {
13538
13421
  const noteStrs = notes.map((n) => {
13539
- const oct = Math.floor(n.pitch / 12) - 1;
13540
- const name = PITCH_MAP2[n.pitch % 12];
13422
+ const { octave: oct, name } = this.spell(n.pitchUnits);
13541
13423
  return `o${oct}${name}`;
13542
13424
  });
13543
13425
  segments.push(`[${noteStrs.join("")}]${durStr}`);
13544
13426
  } else {
13545
13427
  const { text, currentOctave } = this.getNoteWithOctave(
13546
- notes[0].pitch,
13428
+ notes[0].pitchUnits,
13547
13429
  lastOctave
13548
13430
  );
13549
13431
  segments.push(`${text}${durStr}`);
@@ -13574,7 +13456,7 @@ var MMLCore = class _MMLCore {
13574
13456
  * MMLの音長文字列("4", "4.", "12"など)をステップ数に変換する
13575
13457
  */
13576
13458
  getStepFromDottedMML(durStr) {
13577
- const config = getRenderConfig();
13459
+ const config = this.getConfig();
13578
13460
  const total = config.stepsPerBar;
13579
13461
  const isDotted = durStr.endsWith(".");
13580
13462
  const baseDur = parseInt(isDotted ? durStr.slice(0, -1) : durStr, 10);
@@ -13584,7 +13466,7 @@ var MMLCore = class _MMLCore {
13584
13466
  };
13585
13467
  var decomposeToMonophonic = (notes) => {
13586
13468
  const sorted = [...notes].sort(
13587
- (a, b) => a.startStep - b.startStep || a.pitch - b.pitch
13469
+ (a, b) => a.startStep - b.startStep || a.pitchUnits - b.pitchUnits
13588
13470
  );
13589
13471
  const tracks = [];
13590
13472
  const trackEnds = [];
@@ -13619,6 +13501,493 @@ var isChordHeavyTrack = (notes, threshold = 0.6) => {
13619
13501
  return chordNotes / notes.length >= threshold;
13620
13502
  };
13621
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
+
13622
13991
  // src/daw.ts
13623
13992
  var CHORD_INFO_HTML2 = `
13624
13993
  <div class="dtm-modal-body-content">
@@ -13905,6 +14274,27 @@ var MIDI_INFO_HTML = `
13905
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>
13906
14275
  </div>
13907
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
+ `;
13908
14298
  var KOE_INFO_HTML = `
13909
14299
  <div class="dtm-modal-body-content">
13910
14300
  <h4>1. UTAU\u97F3\u6E90\u3092 .koe \u306B\u5909\u63DB\u3059\u308B</h4>
@@ -14122,7 +14512,7 @@ var AUTO_ROLE_BASE_VOLUME = {
14122
14512
  chord: 72
14123
14513
  };
14124
14514
  var computeAutoRoleStats = (notes) => {
14125
- 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;
14126
14516
  const avgDur = notes.reduce((s, n) => s + n.durationSteps, 0) / notes.length;
14127
14517
  const byStart = /* @__PURE__ */ new Map();
14128
14518
  for (const n of notes)
@@ -14136,8 +14526,9 @@ var computeAutoRoleStats = (notes) => {
14136
14526
  const notesPerBeat = notes.length / spanBeats;
14137
14527
  return { avgPitch, avgDur, avgPoly, maxPoly, notesPerBeat };
14138
14528
  };
14529
+ var AUTO_ROLE_BASS_UNITS = 48 * UNITS_PER_SEMITONE;
14139
14530
  var classifyTrackRole = (stats) => {
14140
- if (stats.avgPitch < 48) return "bass";
14531
+ if (stats.avgPitch < AUTO_ROLE_BASS_UNITS) return "bass";
14141
14532
  if (stats.maxPoly >= 2 && stats.avgDur >= AUTO_ROLE_STEPS_PER_BEAT)
14142
14533
  return "chord";
14143
14534
  if (stats.notesPerBeat < 1.2 && stats.avgDur >= AUTO_ROLE_STEPS_PER_BEAT * 1.5)
@@ -14149,7 +14540,10 @@ var computeAutoRoleVolume = (role, stats) => {
14149
14540
  if (role === "chord" && stats.avgPoly > 1) {
14150
14541
  vol /= Math.sqrt(stats.avgPoly);
14151
14542
  } else if (role === "bass") {
14152
- 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
+ );
14153
14547
  vol += Math.min(12, depthSemitones * 0.5);
14154
14548
  }
14155
14549
  return clamp5(Math.round(vol), 1, 127);
@@ -14241,17 +14635,48 @@ var mountDAW = (target, options = {}) => {
14241
14635
  refs.drumVolumeLabel.textContent = `${options.drumVolume ?? 80}%`;
14242
14636
  const renderConfig = {
14243
14637
  stepsPerBar: 192,
14244
- keyCount: 128,
14245
- pitchRangeStart: 0,
14638
+ keyCount: KEY_COUNT,
14639
+ pitchRangeStart: PITCH_RANGE_START,
14640
+ unitsPerRow: unitsPerRow(12),
14641
+ edo: 12,
14246
14642
  keyHeight: BASE_KEY_HEIGHT,
14247
14643
  stepWidth: BASE_STEP_WIDTH * 2
14248
14644
  // zoom100% 相当
14249
14645
  };
14646
+ let renderer;
14250
14647
  let zoomX = 100;
14251
14648
  let zoomY = 100;
14252
14649
  let bpm = options.defaultBpm ?? DEFAULT_BPM;
14253
14650
  let masterVolume = options.masterVolume ?? 50;
14254
- 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
+ };
14255
14680
  let reverbAmount = options.reverbAmount ?? 0;
14256
14681
  let reverbDecay = options.reverbDecay ?? DEFAULT_REVERB_DECAY_SEC;
14257
14682
  let reverbPreDelay = options.reverbPreDelay ?? DEFAULT_REVERB_PREDELAY_MS;
@@ -14309,8 +14734,8 @@ var mountDAW = (target, options = {}) => {
14309
14734
  let snapGridSteps = 12;
14310
14735
  const gridLineSteps = 48;
14311
14736
  let currentOffsetX = 0;
14312
- const _initPitch = options.initialScrollPitch ?? 48;
14313
- 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;
14314
14739
  let playStartStep = 0;
14315
14740
  let isSolo = false;
14316
14741
  let lyricTrackIndices = /* @__PURE__ */ new Set();
@@ -14405,21 +14830,28 @@ var mountDAW = (target, options = {}) => {
14405
14830
  if (!ready) return;
14406
14831
  if (!suppressPatch && options.onNotesPatch) {
14407
14832
  const prevByKey = new Map(
14408
- prevNotes.map((n) => [`${n.startStep}_${n.pitch}`, n])
14833
+ prevNotes.map((n) => [`${n.startStep}_${n.pitchUnits}`, n])
14409
14834
  );
14410
14835
  const currByKey = new Map(
14411
- notes.map((n) => [`${n.startStep}_${n.pitch}`, n])
14836
+ notes.map((n) => [`${n.startStep}_${n.pitchUnits}`, n])
14412
14837
  );
14413
14838
  const added = notes.filter((n) => {
14414
- const prev = prevByKey.get(`${n.startStep}_${n.pitch}`);
14839
+ const prev = prevByKey.get(
14840
+ `${n.startStep}_${n.pitchUnits}`
14841
+ );
14415
14842
  return !prev || prev.durationSteps !== n.durationSteps || prev.velocity !== n.velocity;
14416
14843
  }).map((n) => ({
14417
14844
  startStep: n.startStep,
14418
- pitch: n.pitch,
14845
+ pitchUnits: n.pitchUnits,
14419
14846
  durationSteps: n.durationSteps,
14420
14847
  velocity: n.velocity
14421
14848
  }));
14422
- 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
+ }));
14423
14855
  if (added.length > 0 || removed.length > 0) {
14424
14856
  options.onNotesPatch(config.id, added, removed);
14425
14857
  }
@@ -14429,7 +14861,8 @@ var mountDAW = (target, options = {}) => {
14429
14861
  updateUndoRedo();
14430
14862
  }
14431
14863
  },
14432
- config.volume
14864
+ config.volume,
14865
+ () => renderConfig
14433
14866
  ),
14434
14867
  volume: config.volume,
14435
14868
  savedChordInput: "",
@@ -14524,18 +14957,18 @@ var mountDAW = (target, options = {}) => {
14524
14957
  return (lastNoteMeasure + 2) * renderConfig.stepsPerBar;
14525
14958
  };
14526
14959
  const getMaxOffsetX = () => {
14527
- const canvas = getGridCanvas();
14960
+ const canvas = renderer.getGridCanvas();
14528
14961
  const maxNoteStep = getMaxNoteStep();
14529
14962
  const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
14530
14963
  return Math.max(0, totalContentWidth - canvas.width);
14531
14964
  };
14532
14965
  const getMaxOffsetY = () => {
14533
14966
  const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
14534
- return Math.max(0, totalHeight - getGridCanvas().height);
14967
+ return Math.max(0, totalHeight - renderer.getGridCanvas().height);
14535
14968
  };
14536
14969
  const drawStartLine = () => {
14537
- const ctx = getGridContext();
14538
- const canvas = getGridCanvas();
14970
+ const ctx = renderer.getGridContext();
14971
+ const canvas = renderer.getGridCanvas();
14539
14972
  if (!ctx) return;
14540
14973
  const x2 = playStartStep * renderConfig.stepWidth - currentOffsetX;
14541
14974
  if (x2 < -10 || x2 > canvas.width + 10) return;
@@ -14550,8 +14983,8 @@ var mountDAW = (target, options = {}) => {
14550
14983
  ctx.restore();
14551
14984
  };
14552
14985
  const drawPlayhead = () => {
14553
- const ctx = getGridContext();
14554
- const canvas = getGridCanvas();
14986
+ const ctx = renderer.getGridContext();
14987
+ const canvas = renderer.getGridCanvas();
14555
14988
  if (!ctx) return;
14556
14989
  const x2 = currentPlayStep * renderConfig.stepWidth - currentOffsetX;
14557
14990
  if (x2 < 0 || x2 > canvas.width) return;
@@ -14565,7 +14998,7 @@ var mountDAW = (target, options = {}) => {
14565
14998
  ctx.restore();
14566
14999
  };
14567
15000
  const redrawAll = () => {
14568
- drawGrid(gridLineSteps);
15001
+ renderer.drawGrid(gridLineSteps);
14569
15002
  let lyricTargetNotes = null;
14570
15003
  let activeTrackState = null;
14571
15004
  for (const t of trackStates) {
@@ -14577,16 +15010,16 @@ var mountDAW = (target, options = {}) => {
14577
15010
  }
14578
15011
  const [r, g, b] = t.config.color;
14579
15012
  const notes = t.core.getNotes();
14580
- drawNotes(notes, [r, g, b, 1], false);
15013
+ renderer.drawNotes(notes, [r, g, b, 1], false);
14581
15014
  }
14582
15015
  if (activeTrackState) {
14583
15016
  const [r, g, b] = activeTrackState.config.color;
14584
15017
  const notes = activeTrackState.core.getNotes();
14585
- drawNotes(notes, [r, g, b, 1], true);
15018
+ renderer.drawNotes(notes, [r, g, b, 1], true);
14586
15019
  lyricTargetNotes = notes;
14587
15020
  }
14588
15021
  if (activeToolMode === "select" && selectionRect) {
14589
- const ctx = getGridContext();
15022
+ const ctx = renderer.getGridContext();
14590
15023
  ctx.save();
14591
15024
  ctx.strokeStyle = "#ffec27";
14592
15025
  ctx.lineWidth = 2;
@@ -14609,19 +15042,19 @@ var mountDAW = (target, options = {}) => {
14609
15042
  if (activeToolMode === "select" && selectedNotes.length > 0) {
14610
15043
  const ids = new Set(selectedNotes.map((n) => n.id));
14611
15044
  const active = getActive();
14612
- drawSelectedNotes(active.core.getNotes(), ids, [
15045
+ renderer.drawSelectedNotes(active.core.getNotes(), ids, [
14613
15046
  ...active.config.color,
14614
15047
  1
14615
15048
  ]);
14616
15049
  }
14617
15050
  if (lyricTargetNotes)
14618
- drawNoteLyrics(lyricTargetNotes, getActiveLyricKana());
15051
+ renderer.drawNoteLyrics(lyricTargetNotes, getActiveLyricKana());
14619
15052
  drawStartLine();
14620
15053
  if (playbackState === "playing") drawPlayhead();
14621
15054
  updateScrollbars();
14622
15055
  };
14623
15056
  const updateScrollbars = () => {
14624
- const canvas = getGridCanvas();
15057
+ const canvas = renderer.getGridCanvas();
14625
15058
  const maxOffsetX = getMaxOffsetX();
14626
15059
  const sbW = refs.hScroll.clientWidth;
14627
15060
  if (maxOffsetX <= 0) {
@@ -14705,7 +15138,7 @@ var mountDAW = (target, options = {}) => {
14705
15138
  const x2 = clamp5(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
14706
15139
  const ratio = x2 / (rect.width - thumbW);
14707
15140
  currentOffsetX = clamp5(ratio * maxOffsetX, 0, maxOffsetX);
14708
- setDrawOffset(currentOffsetX, currentOffsetY);
15141
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14709
15142
  redrawAll();
14710
15143
  };
14711
15144
  const moveV = (clientY) => {
@@ -14716,7 +15149,7 @@ var mountDAW = (target, options = {}) => {
14716
15149
  const y = clamp5(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
14717
15150
  const ratio = y / (rect.height - thumbH);
14718
15151
  currentOffsetY = clamp5(ratio * maxOffset, 0, maxOffset);
14719
- setDrawOffset(currentOffsetX, currentOffsetY);
15152
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14720
15153
  redrawAll();
14721
15154
  };
14722
15155
  };
@@ -14755,8 +15188,8 @@ var mountDAW = (target, options = {}) => {
14755
15188
  const autoScrollTick = () => {
14756
15189
  autoScrollRAF = null;
14757
15190
  if (!isSelecting || !lastMoveEvent) return;
14758
- const canvas = getGridCanvas();
14759
- const { x: x2, y } = getGridPosition(lastMoveEvent);
15191
+ const canvas = renderer.getGridCanvas();
15192
+ const { x: x2, y } = renderer.getGridPosition(lastMoveEvent);
14760
15193
  const dx = computeEdgeSpeed(x2, canvas.width);
14761
15194
  const dy = computeEdgeSpeed(y, canvas.height);
14762
15195
  if (dx !== 0 || dy !== 0) {
@@ -14764,7 +15197,7 @@ var mountDAW = (target, options = {}) => {
14764
15197
  const maxOffsetY = getMaxOffsetY();
14765
15198
  currentOffsetX = clamp5(currentOffsetX + dx, 0, maxOffsetX);
14766
15199
  currentOffsetY = clamp5(currentOffsetY + dy, 0, maxOffsetY);
14767
- setDrawOffset(currentOffsetX, currentOffsetY);
15200
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14768
15201
  onPointerMove(lastMoveEvent);
14769
15202
  }
14770
15203
  if (isSelecting) {
@@ -14785,11 +15218,17 @@ var mountDAW = (target, options = {}) => {
14785
15218
  };
14786
15219
  const findActiveNoteAt = (x2, y, margin = 0) => {
14787
15220
  const active = getActive();
14788
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
14789
- 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();
14790
15229
  for (const note of active.core.getNotes()) {
14791
15230
  const logicalX = note.startStep * stepWidth;
14792
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
15231
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
14793
15232
  const logicalY = yIndex * keyHeight;
14794
15233
  const w = note.durationSteps * stepWidth;
14795
15234
  const renderX = logicalX - offset.x;
@@ -14802,7 +15241,7 @@ var mountDAW = (target, options = {}) => {
14802
15241
  const hasNoteAt = (step, pitch, excludeId) => {
14803
15242
  const active = getActive();
14804
15243
  return active.core.getNotes().some(
14805
- (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
14806
15245
  );
14807
15246
  };
14808
15247
  const snapToGrid = (duration) => Math.max(
@@ -14813,7 +15252,7 @@ var mountDAW = (target, options = {}) => {
14813
15252
  const onGridPointerDown = (event) => {
14814
15253
  event.preventDefault();
14815
15254
  options.onResumeAudio?.();
14816
- const { x: x2, y, step, pitch } = getGridPosition(event);
15255
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(event);
14817
15256
  const active = getActive();
14818
15257
  if (activeToolMode === "eraser") {
14819
15258
  if (isActiveLocked()) return;
@@ -14828,7 +15267,7 @@ var mountDAW = (target, options = {}) => {
14828
15267
  selectedOriginal = selectedNotes.map((n) => ({
14829
15268
  id: n.id,
14830
15269
  startStep: n.startStep,
14831
- pitch: n.pitch
15270
+ pitch: n.pitchUnits
14832
15271
  }));
14833
15272
  isSelecting = true;
14834
15273
  dragMode = "move";
@@ -14847,7 +15286,7 @@ var mountDAW = (target, options = {}) => {
14847
15286
  {
14848
15287
  id: clicked.id,
14849
15288
  startStep: clicked.startStep,
14850
- pitch: clicked.pitch
15289
+ pitch: clicked.pitchUnits
14851
15290
  }
14852
15291
  ];
14853
15292
  isSelecting = true;
@@ -14865,9 +15304,9 @@ var mountDAW = (target, options = {}) => {
14865
15304
  hasDragged = false;
14866
15305
  const existing = findActiveNoteAt(x2, y, TOUCH_HIT_MARGIN);
14867
15306
  if (existing) {
14868
- playPreview(existing.pitch);
15307
+ playPreview(existing.pitchUnits);
14869
15308
  const { stepWidth } = renderConfig;
14870
- const offset = getDrawOffset();
15309
+ const offset = renderer.getDrawOffset();
14871
15310
  const renderX = existing.startStep * stepWidth - offset.x;
14872
15311
  const w = existing.durationSteps * stepWidth;
14873
15312
  if (x2 >= renderX + w - resizeHandleWidth && x2 <= renderX + w) {
@@ -14878,17 +15317,17 @@ var mountDAW = (target, options = {}) => {
14878
15317
  dragOffsetPitch: 0,
14879
15318
  startStep: existing.startStep,
14880
15319
  durationSteps: existing.durationSteps,
14881
- lastPreviewPitch: existing.pitch
15320
+ lastPreviewPitch: existing.pitchUnits
14882
15321
  };
14883
15322
  } else {
14884
15323
  dragState = {
14885
15324
  noteId: existing.id,
14886
15325
  mode: "move",
14887
15326
  dragOffsetStep: step - existing.startStep,
14888
- dragOffsetPitch: pitch - existing.pitch,
15327
+ dragOffsetPitch: pitch - existing.pitchUnits,
14889
15328
  startStep: existing.startStep,
14890
15329
  durationSteps: existing.durationSteps,
14891
- lastPreviewPitch: existing.pitch
15330
+ lastPreviewPitch: existing.pitchUnits
14892
15331
  };
14893
15332
  }
14894
15333
  suppressClick = true;
@@ -14899,14 +15338,14 @@ var mountDAW = (target, options = {}) => {
14899
15338
  const newStart = snappedStep;
14900
15339
  const newEnd = newStart + currentInsertLength;
14901
15340
  const overlapping = active.core.getNotes().some(
14902
- (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
14903
15342
  );
14904
15343
  if (!overlapping) {
14905
15344
  active.core.addNote(snappedStep, pitch, {
14906
15345
  noteLengthSteps: currentInsertLength
14907
15346
  });
14908
15347
  playPreview(pitch);
14909
- 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);
14910
15349
  if (newNote) {
14911
15350
  dragState = {
14912
15351
  noteId: newNote.id,
@@ -14915,7 +15354,7 @@ var mountDAW = (target, options = {}) => {
14915
15354
  dragOffsetPitch: 0,
14916
15355
  startStep: newNote.startStep,
14917
15356
  durationSteps: newNote.durationSteps,
14918
- lastPreviewPitch: newNote.pitch
15357
+ lastPreviewPitch: newNote.pitchUnits
14919
15358
  };
14920
15359
  hasDragged = true;
14921
15360
  }
@@ -14926,7 +15365,7 @@ var mountDAW = (target, options = {}) => {
14926
15365
  const active = getActive();
14927
15366
  if (activeToolMode === "pen") {
14928
15367
  if (!dragState) return;
14929
- const { step, pitch } = getGridPosition(event);
15368
+ const { step, pitch } = renderer.getGridPosition(event);
14930
15369
  hasDragged = true;
14931
15370
  if (dragState.mode === "move") {
14932
15371
  const nextStart = step - dragState.dragOffsetStep;
@@ -14950,7 +15389,7 @@ var mountDAW = (target, options = {}) => {
14950
15389
  }
14951
15390
  if (activeToolMode === "select" && isSelecting && selectionStart) {
14952
15391
  ensureAutoScroll(event);
14953
- const { x: x2, y, step, pitch } = getGridPosition(event);
15392
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(event);
14954
15393
  if (dragMode === "rect") {
14955
15394
  const rect = {
14956
15395
  x: Math.min(x2, selectionStart.x),
@@ -14959,11 +15398,17 @@ var mountDAW = (target, options = {}) => {
14959
15398
  height: Math.abs(y - selectionStart.y)
14960
15399
  };
14961
15400
  selectionRect = rect;
14962
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
14963
- 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();
14964
15409
  selectedNotes = active.core.getNotes().filter((note) => {
14965
15410
  const logicalX = note.startStep * stepWidth;
14966
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
15411
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
14967
15412
  const logicalY = yIndex * keyHeight;
14968
15413
  const nx = logicalX - offset.x;
14969
15414
  const ny = logicalY - offset.y;
@@ -14982,7 +15427,7 @@ var mountDAW = (target, options = {}) => {
14982
15427
  const orig = selectedOriginal.find((o) => o.id === note.id);
14983
15428
  if (!orig) continue;
14984
15429
  const newPitch = orig.pitch + deltaPitch;
14985
- if (newPitch >= 0 && newPitch < 128)
15430
+ if (newPitch >= PITCH_RANGE_START && newPitch <= PITCH_RANGE_END)
14986
15431
  active.core.moveNote(
14987
15432
  note.id,
14988
15433
  orig.startStep + snappedDelta,
@@ -15130,22 +15575,23 @@ var mountDAW = (target, options = {}) => {
15130
15575
  refs.bgOpacityRow.classList.add("dtm-hidden");
15131
15576
  }
15132
15577
  refs.bgRemoveBtn.classList.toggle("dtm-hidden", !blob2);
15133
- setBackgroundActive(!!blob2);
15578
+ renderer.setBackgroundActive(!!blob2);
15134
15579
  redrawAll();
15135
15580
  };
15136
15581
  const setupCanvas = () => {
15137
15582
  const w = refs.rollContainer.clientWidth || 800;
15138
15583
  const h = refs.rollContainer.clientHeight || 450;
15139
- init(refs.wrapper, w, h, renderConfig);
15140
- const gridCanvas = getGridCanvas();
15584
+ renderer?.destroy();
15585
+ renderer = createRenderer(refs.wrapper, w, h, renderConfig);
15586
+ const gridCanvas = renderer.getGridCanvas();
15141
15587
  gridCanvas.addEventListener("pointerdown", onGridPointerDown);
15142
15588
  gridCanvas.addEventListener("dblclick", (event) => {
15143
15589
  event.preventDefault();
15144
15590
  if (isActiveLocked()) return;
15145
- const { step, pitch } = getGridPosition(event);
15591
+ const { step, pitch } = renderer.getGridPosition(event);
15146
15592
  const active = getActive();
15147
15593
  const note = active.core.getNotes().find(
15148
- (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
15149
15595
  );
15150
15596
  if (note) active.core.deleteNoteById(note.id);
15151
15597
  });
@@ -15163,7 +15609,7 @@ var mountDAW = (target, options = {}) => {
15163
15609
  0,
15164
15610
  getMaxOffsetX()
15165
15611
  );
15166
- setDrawOffset(currentOffsetX, currentOffsetY);
15612
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15167
15613
  redrawAll();
15168
15614
  },
15169
15615
  { passive: false }
@@ -15173,7 +15619,7 @@ var mountDAW = (target, options = {}) => {
15173
15619
  suppressClick = false;
15174
15620
  }
15175
15621
  });
15176
- const headerCanvas = getHeaderCanvas();
15622
+ const headerCanvas = renderer.getHeaderCanvas();
15177
15623
  headerCanvas.addEventListener("click", (event) => {
15178
15624
  if (playbackState === "playing") return;
15179
15625
  const rect = headerCanvas.getBoundingClientRect();
@@ -15189,11 +15635,11 @@ var mountDAW = (target, options = {}) => {
15189
15635
  }
15190
15636
  redrawAll();
15191
15637
  });
15192
- setDrawOffset(currentOffsetX, currentOffsetY);
15638
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15193
15639
  redrawAll();
15194
15640
  };
15195
15641
  const applyZoomX = () => {
15196
- const canvas = getGridCanvas();
15642
+ const canvas = renderer.getGridCanvas();
15197
15643
  const centerStep = (currentOffsetX + canvas.width / 2) / renderConfig.stepWidth;
15198
15644
  renderConfig.stepWidth = BASE_STEP_WIDTH * (zoomX * 2) / 100;
15199
15645
  refs.zoomXLabel.textContent = `${zoomX}%`;
@@ -15202,11 +15648,11 @@ var mountDAW = (target, options = {}) => {
15202
15648
  0,
15203
15649
  getMaxOffsetX()
15204
15650
  );
15205
- setDrawOffset(currentOffsetX, currentOffsetY);
15651
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15206
15652
  redrawAll();
15207
15653
  };
15208
15654
  const applyZoomY = () => {
15209
- const canvas = getGridCanvas();
15655
+ const canvas = renderer.getGridCanvas();
15210
15656
  const centerKey = (currentOffsetY + canvas.height / 2) / renderConfig.keyHeight;
15211
15657
  renderConfig.keyHeight = BASE_KEY_HEIGHT * zoomY / 100;
15212
15658
  refs.zoomYLabel.textContent = `${zoomY}%`;
@@ -15215,7 +15661,7 @@ var mountDAW = (target, options = {}) => {
15215
15661
  0,
15216
15662
  getMaxOffsetY()
15217
15663
  );
15218
- setDrawOffset(currentOffsetX, currentOffsetY);
15664
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15219
15665
  redrawAll();
15220
15666
  };
15221
15667
  const getViewState = () => ({
@@ -15227,7 +15673,14 @@ var mountDAW = (target, options = {}) => {
15227
15673
  const notifyViewState = () => options.onViewStateChange?.(getViewState());
15228
15674
  const dispatchNote = (trackId, pitch, trackVol, velocity, when, duration) => {
15229
15675
  const volume = trackVol / 100 * (velocity / 127) * (masterVolume / 100);
15230
- 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
+ });
15231
15684
  };
15232
15685
  const sequencer = createSequencer({
15233
15686
  getTracks: () => trackStates.map((t) => ({
@@ -15255,7 +15708,7 @@ var mountDAW = (target, options = {}) => {
15255
15708
  },
15256
15709
  onTick: (step) => {
15257
15710
  currentPlayStep = step;
15258
- const canvas = getGridCanvas();
15711
+ const canvas = renderer.getGridCanvas();
15259
15712
  const visibleSteps = canvas.width / renderConfig.stepWidth;
15260
15713
  const threshold = currentOffsetX / renderConfig.stepWidth + visibleSteps - 4;
15261
15714
  if (currentPlayStep > threshold) {
@@ -15265,7 +15718,7 @@ var mountDAW = (target, options = {}) => {
15265
15718
  0,
15266
15719
  getMaxOffsetX()
15267
15720
  );
15268
- setDrawOffset(currentOffsetX, currentOffsetY);
15721
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15269
15722
  }
15270
15723
  redrawAll();
15271
15724
  },
@@ -15296,7 +15749,7 @@ var mountDAW = (target, options = {}) => {
15296
15749
  (a, b) => a.startStep - b.startStep
15297
15750
  );
15298
15751
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
15299
- const semis = (lt.octave ?? 0) * 12;
15752
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
15300
15753
  const count = Math.min(sorted.length, lt.syllables.length);
15301
15754
  const notes = [];
15302
15755
  for (let i2 = 0; i2 < count; i2++) {
@@ -15304,7 +15757,7 @@ var mountDAW = (target, options = {}) => {
15304
15757
  if (n.startStep < fromStep) continue;
15305
15758
  notes.push({
15306
15759
  syllable: lt.syllables[i2],
15307
- pitch: n.pitch + semis,
15760
+ pitch: n.pitchUnits + semis,
15308
15761
  startSec: (n.startStep - fromStep) * secondsPerStep,
15309
15762
  durationSec: n.durationSteps * secondsPerStep * gate
15310
15763
  });
@@ -15347,13 +15800,13 @@ var mountDAW = (target, options = {}) => {
15347
15800
  }
15348
15801
  }
15349
15802
  if (playbackState !== "paused") {
15350
- const canvas = getGridCanvas();
15803
+ const canvas = renderer.getGridCanvas();
15351
15804
  currentOffsetX = clamp5(
15352
15805
  playStartStep * renderConfig.stepWidth - canvas.width * 0.5,
15353
15806
  0,
15354
15807
  getMaxOffsetX()
15355
15808
  );
15356
- setDrawOffset(currentOffsetX, currentOffsetY);
15809
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15357
15810
  }
15358
15811
  playbackState = "playing";
15359
15812
  sequencer.start(fromStep);
@@ -16340,6 +16793,7 @@ var mountDAW = (target, options = {}) => {
16340
16793
  fadeIn: Math.round(fadeInSec * 10),
16341
16794
  fadeOut: Math.round(fadeOutSec * 10),
16342
16795
  mode,
16796
+ edo: renderConfig.edo,
16343
16797
  trackInstruments: trackInstMeta,
16344
16798
  trackCompression: trackCompMeta,
16345
16799
  trackWidth: trackWidthMeta,
@@ -16368,6 +16822,7 @@ var mountDAW = (target, options = {}) => {
16368
16822
  fadeIn: Math.round(fadeInSec * 10),
16369
16823
  fadeOut: Math.round(fadeOutSec * 10),
16370
16824
  mode,
16825
+ edo: renderConfig.edo,
16371
16826
  trackInstruments: trackInstMeta,
16372
16827
  trackCompression: trackCompMeta,
16373
16828
  trackWidth: trackWidthMeta,
@@ -16507,19 +16962,19 @@ var mountDAW = (target, options = {}) => {
16507
16962
  }
16508
16963
  }
16509
16964
  if (candidateNotes.length === 0) return null;
16510
- const sum = candidateNotes.reduce((acc, note) => acc + note.pitch, 0);
16965
+ const sum = candidateNotes.reduce((acc, note) => acc + note.pitchUnits, 0);
16511
16966
  return Math.round(sum / candidateNotes.length);
16512
16967
  };
16513
16968
  const centerPitch = (pitch) => {
16514
- const canvas = getGridCanvas();
16515
- 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);
16516
16971
  const logicalY = yIndex * renderConfig.keyHeight;
16517
16972
  currentOffsetY = clamp5(
16518
16973
  logicalY - (canvas.height - renderConfig.keyHeight) / 2,
16519
16974
  0,
16520
16975
  getMaxOffsetY()
16521
16976
  );
16522
- setDrawOffset(currentOffsetX, currentOffsetY);
16977
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
16523
16978
  };
16524
16979
  const clearAll = () => {
16525
16980
  for (const t of trackStates) {
@@ -16577,10 +17032,10 @@ var mountDAW = (target, options = {}) => {
16577
17032
  if (!meta.drumFont) applyDrumPatternFont(meta.drum);
16578
17033
  }
16579
17034
  if (meta.volume !== void 0) {
16580
- masterVolume = meta.volume;
16581
- refs.masterVolume.value = String(meta.volume);
16582
- refs.masterVolumeLabel.textContent = `${meta.volume}%`;
17035
+ applyMasterVolume(meta.volume);
16583
17036
  }
17037
+ applyEdo(meta.edo ?? 12);
17038
+ refs.edoSelect.value = String(renderConfig.edo ?? 12);
16584
17039
  if (meta.drumVolume !== void 0) {
16585
17040
  drumVolume = meta.drumVolume;
16586
17041
  refs.drumVolume.value = String(meta.drumVolume);
@@ -16745,7 +17200,7 @@ var mountDAW = (target, options = {}) => {
16745
17200
  if (applyActiveOnly && p.trackIndex !== activeTrackIndex) continue;
16746
17201
  const t = trackStates[p.trackIndex];
16747
17202
  if (!t) continue;
16748
- t.core.addNote(p.startStep, p.pitch, {
17203
+ t.core.addNote(p.startStep, p.pitchUnits, {
16749
17204
  noteLengthSteps: p.durationSteps,
16750
17205
  velocity: DEFAULT_VELOCITY
16751
17206
  });
@@ -16762,7 +17217,7 @@ var mountDAW = (target, options = {}) => {
16762
17217
  if (firstPitch !== null) {
16763
17218
  centerPitch(firstPitch);
16764
17219
  } else {
16765
- centerPitch(48);
17220
+ centerPitch(pitchV1ToUnits(48));
16766
17221
  }
16767
17222
  redrawAll();
16768
17223
  updateTrackPanel();
@@ -16780,6 +17235,8 @@ var mountDAW = (target, options = {}) => {
16780
17235
  const chordTrack = trackStates.find((t) => t.config.id === "chord");
16781
17236
  if (!chordTrack) return;
16782
17237
  const placements = buildChordPlacements({
17238
+ // 和音は五度連鎖経由で音律の格子へ写す(31平均律では長3度が10度になる)
17239
+ edo: renderConfig.edo,
16783
17240
  chordStr: active.savedChordInput,
16784
17241
  patternType: active.savedChordPattern,
16785
17242
  rootShift: active.savedChordRoot,
@@ -16789,7 +17246,7 @@ var mountDAW = (target, options = {}) => {
16789
17246
  chordTrack.core.clearNotesWithoutHistory();
16790
17247
  chordTrack.core.beginBatch();
16791
17248
  for (const p of placements) {
16792
- chordTrack.core.addNote(p.startStep, p.pitch, {
17249
+ chordTrack.core.addNote(p.startStep, p.pitchUnits, {
16793
17250
  noteLengthSteps: Math.max(1, p.durationSteps),
16794
17251
  velocity: p.velocity
16795
17252
  });
@@ -16827,6 +17284,8 @@ var mountDAW = (target, options = {}) => {
16827
17284
  active.vocalOctaveUnison = "none";
16828
17285
  } else {
16829
17286
  clearAll();
17287
+ applyEdo(12);
17288
+ refs.edoSelect.value = String(renderConfig.edo ?? 12);
16830
17289
  for (const t of trackStates) t.core.setLoadMode(true);
16831
17290
  for (const t of trackStates) {
16832
17291
  t.lyrics = "";
@@ -16853,7 +17312,7 @@ var mountDAW = (target, options = {}) => {
16853
17312
  if (applyActiveOnly && p.trackId !== activeTrackId) continue;
16854
17313
  const t = trackStates.find((ts) => ts.config.id === p.trackId);
16855
17314
  if (!t) continue;
16856
- t.core.addNote(p.startStep, p.pitch, {
17315
+ t.core.addNote(p.startStep, snapToEdoGrid(pitchV1ToUnits(p.pitch)), {
16857
17316
  noteLengthSteps: p.durationSteps,
16858
17317
  velocity: p.velocity
16859
17318
  });
@@ -16870,7 +17329,7 @@ var mountDAW = (target, options = {}) => {
16870
17329
  if (firstPitch !== null) {
16871
17330
  centerPitch(firstPitch);
16872
17331
  } else {
16873
- centerPitch(48);
17332
+ centerPitch(pitchV1ToUnits(48));
16874
17333
  }
16875
17334
  redrawAll();
16876
17335
  updateTrackPanel();
@@ -16934,18 +17393,21 @@ var mountDAW = (target, options = {}) => {
16934
17393
  }
16935
17394
  }, 30);
16936
17395
  };
16937
- 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";
16938
17400
  const overlay = document.createElement("div");
16939
17401
  overlay.className = "dtm-modal-overlay";
16940
17402
  overlay.innerHTML = `
16941
17403
  <div class="dtm-modal">
16942
17404
  <div class="dtm-modal-header">
16943
- <span class="dtm-modal-title">\u30E2\u30FC\u30C9\u306E\u78BA\u8A8D</span>
17405
+ <span class="dtm-modal-title">${title}</span>
16944
17406
  </div>
16945
17407
  <div class="dtm-modal-body"><p>${message}</p></div>
16946
17408
  <div class="dtm-confirm-footer">
16947
- <button class="dtm-btn dtm-btn--ghost dtm-confirm-no">\u3044\u3044\u3048\uFF08\u3053\u306E\u307E\u307E\u8AAD\u307F\u8FBC\u3080\uFF09</button>
16948
- <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>
16949
17411
  </div>
16950
17412
  </div>`;
16951
17413
  const close = (result) => {
@@ -17053,10 +17515,36 @@ var mountDAW = (target, options = {}) => {
17053
17515
  });
17054
17516
  refs.decomposeChordToggle.addEventListener("change", notifyViewState);
17055
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
+ });
17056
17546
  refs.masterVolume.addEventListener("input", () => {
17057
- masterVolume = Number.parseInt(refs.masterVolume.value, 10) || 0;
17058
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17059
- options.singingVoices?.setVolume(masterVolume / 100);
17547
+ applyMasterVolume(Number.parseInt(refs.masterVolume.value, 10) || 0);
17060
17548
  });
17061
17549
  refs.masterComp.addEventListener("input", () => {
17062
17550
  masterCompression = Number.parseInt(refs.masterComp.value, 10) || 0;
@@ -17120,10 +17608,7 @@ var mountDAW = (target, options = {}) => {
17120
17608
  const scale = targetPeak / observedPeakMax;
17121
17609
  const suggested = clamp5(Math.round(masterVolume * scale), 10, 100);
17122
17610
  if (suggested !== masterVolume) {
17123
- masterVolume = suggested;
17124
- refs.masterVolume.value = String(masterVolume);
17125
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17126
- options.singingVoices?.setVolume(masterVolume / 100);
17611
+ applyMasterVolume(suggested);
17127
17612
  }
17128
17613
  observedPeakMax = 0;
17129
17614
  observedPlayMs = 0;
@@ -17283,7 +17768,8 @@ var mountDAW = (target, options = {}) => {
17283
17768
  generateRandomPattern(getActive().core, {
17284
17769
  stepsPerBar: renderConfig.stepsPerBar,
17285
17770
  startStep: playStartStep,
17286
- pitchRangeStart: renderConfig.pitchRangeStart
17771
+ pitchRangeStart: renderConfig.pitchRangeStart,
17772
+ edo: renderConfig.edo
17287
17773
  });
17288
17774
  redrawAll();
17289
17775
  });
@@ -17620,7 +18106,7 @@ var mountDAW = (target, options = {}) => {
17620
18106
  id: 0,
17621
18107
  startStep: p.startStep,
17622
18108
  durationSteps: p.durationSteps,
17623
- pitch: p.pitch,
18109
+ pitchUnits: snapToEdoGrid(pitchV1ToUnits(p.pitch)),
17624
18110
  velocity: p.velocity
17625
18111
  }));
17626
18112
  const mml = refCore.getMMLFromNotes(asNotes, tempo, 100).trim();
@@ -18075,10 +18561,10 @@ var mountDAW = (target, options = {}) => {
18075
18561
  const newStart = playStartStep + (note.startStep - minStart);
18076
18562
  const newEnd = newStart + note.durationSteps;
18077
18563
  const overlap = notes.some(
18078
- (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
18079
18565
  );
18080
18566
  if (!overlap)
18081
- core.addNote(newStart, note.pitch, {
18567
+ core.addNote(newStart, note.pitchUnits, {
18082
18568
  noteLengthSteps: note.durationSteps,
18083
18569
  velocity: note.velocity
18084
18570
  });
@@ -18151,13 +18637,13 @@ var mountDAW = (target, options = {}) => {
18151
18637
  pausedPlayStep = step;
18152
18638
  currentPlayStep = step;
18153
18639
  playbackState = "paused";
18154
- const canvas = getGridCanvas();
18640
+ const canvas = renderer.getGridCanvas();
18155
18641
  currentOffsetX = clamp5(
18156
18642
  step * renderConfig.stepWidth - canvas.width * 0.5,
18157
18643
  0,
18158
18644
  getMaxOffsetX()
18159
18645
  );
18160
- setDrawOffset(currentOffsetX, currentOffsetY);
18646
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
18161
18647
  updateTransport();
18162
18648
  redrawAll();
18163
18649
  };
@@ -18225,16 +18711,10 @@ var mountDAW = (target, options = {}) => {
18225
18711
  forcePauseAt,
18226
18712
  setLoading,
18227
18713
  setMasterVolume: (volume) => {
18228
- masterVolume = clamp5(volume, 0, 100);
18229
- refs.masterVolume.value = String(masterVolume);
18230
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
18231
- options.singingVoices?.setVolume(masterVolume / 100);
18714
+ applyMasterVolume(volume);
18232
18715
  },
18233
18716
  setVolume: (volume) => {
18234
- masterVolume = clamp5(volume, 0, 100);
18235
- refs.masterVolume.value = String(masterVolume);
18236
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
18237
- options.singingVoices?.setVolume(masterVolume / 100);
18717
+ applyMasterVolume(volume);
18238
18718
  },
18239
18719
  setDrumVolume: (volume) => {
18240
18720
  drumVolume = clamp5(volume, 0, 100);
@@ -18277,15 +18757,19 @@ var mountDAW = (target, options = {}) => {
18277
18757
  suppressPatch = true;
18278
18758
  track.core.beginBatch();
18279
18759
  for (const n of added) {
18280
- 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
+ );
18281
18763
  if (existing) track.core.deleteNoteById(existing.id);
18282
- track.core.addNote(n.startStep, n.pitch, {
18764
+ track.core.addNote(n.startStep, n.pitchUnits, {
18283
18765
  noteLengthSteps: n.durationSteps,
18284
18766
  velocity: n.velocity
18285
18767
  });
18286
18768
  }
18287
18769
  for (const r of removed) {
18288
- 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
+ );
18289
18773
  if (note) track.core.deleteNoteById(note.id);
18290
18774
  }
18291
18775
  track.core.endBatch();
@@ -18327,9 +18811,9 @@ var mountDAW = (target, options = {}) => {
18327
18811
  if (t.config.id === activeTrackId) updateTrackPanel();
18328
18812
  },
18329
18813
  noteToCanvas: (step, pitch) => {
18330
- const canvas = getGridCanvas();
18814
+ const canvas = renderer.getGridCanvas();
18331
18815
  const x2 = step * renderConfig.stepWidth - currentOffsetX;
18332
- 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;
18333
18817
  const onScreen = x2 >= 0 && x2 <= canvas.width && y >= 0 && y <= canvas.height;
18334
18818
  let side = null;
18335
18819
  if (!onScreen) {
@@ -18393,7 +18877,7 @@ var playSingingMML = async (mml, options = {}) => {
18393
18877
  id: id++,
18394
18878
  startStep: p.startStep,
18395
18879
  durationSteps: p.durationSteps,
18396
- pitch: p.pitch,
18880
+ pitchUnits: p.pitchUnits,
18397
18881
  velocity: p.velocity
18398
18882
  }));
18399
18883
  return {
@@ -18419,7 +18903,7 @@ var playSingingMML = async (mml, options = {}) => {
18419
18903
  (a, b) => a.startStep - b.startStep
18420
18904
  );
18421
18905
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
18422
- const semis = (lt.octave ?? 0) * 12;
18906
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
18423
18907
  const count = Math.min(sorted.length, lt.syllables.length);
18424
18908
  const notes = [];
18425
18909
  for (let i2 = 0; i2 < count; i2++) {
@@ -18427,7 +18911,7 @@ var playSingingMML = async (mml, options = {}) => {
18427
18911
  if (n.startStep < fromStep) continue;
18428
18912
  notes.push({
18429
18913
  syllable: lt.syllables[i2],
18430
- pitch: n.pitch + semis,
18914
+ pitch: n.pitchUnits + semis,
18431
18915
  startSec: (n.startStep - fromStep) * secondsPerStep,
18432
18916
  durationSec: n.durationSteps * secondsPerStep * gate
18433
18917
  });
@@ -18615,24 +19099,28 @@ var createPianoRoll = (options, handlers) => {
18615
19099
  config,
18616
19100
  noteLengthSteps = 1
18617
19101
  } = options;
18618
- init(mountTarget, width, height, config);
19102
+ const renderer = createRenderer(mountTarget, width, height, config);
18619
19103
  let currentNoteLengthSteps = noteLengthSteps;
18620
19104
  let selectionRect = null;
18621
19105
  let isSelecting = false;
18622
19106
  let selectionStart = null;
18623
19107
  let selectedNotes = [];
18624
19108
  let copiedNotes = [];
18625
- const core = new MMLCore({
18626
- onMMLGenerated: handlers.onMMLGenerated,
18627
- onNotesChanged: (notes) => {
18628
- handlers.onNotesChanged(notes);
18629
- }
18630
- });
19109
+ const core = new MMLCore(
19110
+ {
19111
+ onMMLGenerated: handlers.onMMLGenerated,
19112
+ onNotesChanged: (notes) => {
19113
+ handlers.onNotesChanged(notes);
19114
+ }
19115
+ },
19116
+ 80,
19117
+ () => config
19118
+ );
18631
19119
  const getAddNoteOptions = () => ({
18632
19120
  noteLengthSteps: currentNoteLengthSteps
18633
19121
  });
18634
19122
  let suppressClick = false;
18635
- onClick((step, pitch) => {
19123
+ renderer.onClick((step, pitch) => {
18636
19124
  if (suppressClick) {
18637
19125
  suppressClick = false;
18638
19126
  return;
@@ -18644,7 +19132,7 @@ var createPianoRoll = (options, handlers) => {
18644
19132
  } else if (mode === "eraser") {
18645
19133
  const notes = core.getNotes();
18646
19134
  const note = notes.find(
18647
- (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
18648
19136
  );
18649
19137
  if (note) {
18650
19138
  core.deleteNoteById(note.id);
@@ -18652,17 +19140,23 @@ var createPianoRoll = (options, handlers) => {
18652
19140
  }
18653
19141
  }
18654
19142
  });
18655
- const gridCanvas = getGridCanvas();
19143
+ const gridCanvas = renderer.getGridCanvas();
18656
19144
  const resizeHandleWidth = 6;
18657
19145
  let dragState = null;
18658
19146
  let hasDragged = false;
18659
19147
  let lastPreviewPitch = null;
18660
19148
  const findNoteAtPosition = (x2, y) => {
18661
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18662
- 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();
18663
19157
  for (const note of core.getNotes()) {
18664
19158
  const logicalX = note.startStep * stepWidth;
18665
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19159
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18666
19160
  const logicalY = yIndex * keyHeight;
18667
19161
  const w = note.durationSteps * stepWidth;
18668
19162
  const h = keyHeight;
@@ -18676,7 +19170,7 @@ var createPianoRoll = (options, handlers) => {
18676
19170
  };
18677
19171
  const handlePointerMove = (e) => {
18678
19172
  if (core.getToolMode() === "select" && isSelecting && selectionStart) {
18679
- const { x: x2, y } = getGridPosition(e);
19173
+ const { x: x2, y } = renderer.getGridPosition(e);
18680
19174
  const minX = Math.min(x2, selectionStart.x);
18681
19175
  const minY = Math.min(y, selectionStart.y);
18682
19176
  const width2 = Math.abs(x2 - selectionStart.x);
@@ -18688,7 +19182,7 @@ var createPianoRoll = (options, handlers) => {
18688
19182
  }
18689
19183
  if (!dragState) return;
18690
19184
  hasDragged = true;
18691
- const { step, pitch } = getGridPosition(e);
19185
+ const { step, pitch } = renderer.getGridPosition(e);
18692
19186
  if (dragState.mode === "move") {
18693
19187
  if (dragState.selectedNotes && dragState.selectedNotes.length > 0) {
18694
19188
  const noteId = dragState.noteId;
@@ -18697,10 +19191,10 @@ var createPianoRoll = (options, handlers) => {
18697
19191
  const nextStart2 = step - dragState.dragOffsetStep;
18698
19192
  const nextPitch2 = pitch - dragState.dragOffsetPitch;
18699
19193
  const stepDelta = nextStart2 - baseNote.startStep;
18700
- const pitchDelta = nextPitch2 - baseNote.pitch;
19194
+ const pitchDelta = nextPitch2 - baseNote.pitchUnits;
18701
19195
  for (const note of dragState.selectedNotes) {
18702
19196
  const newStart = note.startStep + stepDelta;
18703
- const newPitch = note.pitch + pitchDelta;
19197
+ const newPitch = note.pitchUnits + pitchDelta;
18704
19198
  core.moveNote(note.id, newStart, newPitch);
18705
19199
  }
18706
19200
  if (options.onPreviewSound && pitch !== lastPreviewPitch) {
@@ -18741,7 +19235,7 @@ var createPianoRoll = (options, handlers) => {
18741
19235
  }
18742
19236
  };
18743
19237
  gridCanvas.addEventListener("pointerdown", (e) => {
18744
- const { x: x2, y, step, pitch } = getGridPosition(e);
19238
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(e);
18745
19239
  const currentMode = core.getToolMode();
18746
19240
  if (currentMode === "select") {
18747
19241
  const clickedNote = findNoteAtPosition(x2, y);
@@ -18752,7 +19246,7 @@ var createPianoRoll = (options, handlers) => {
18752
19246
  noteId: clickedNote.id,
18753
19247
  mode: "move",
18754
19248
  dragOffsetStep: step - clickedNote.startStep,
18755
- dragOffsetPitch: pitch - clickedNote.pitch,
19249
+ dragOffsetPitch: pitch - clickedNote.pitchUnits,
18756
19250
  startStep: clickedNote.startStep,
18757
19251
  selectedNotes: notesInRect
18758
19252
  // 複数選択ノートを保存
@@ -18770,10 +19264,16 @@ var createPianoRoll = (options, handlers) => {
18770
19264
  }
18771
19265
  const note = findNoteAtPosition(x2, y);
18772
19266
  if (!note) return;
18773
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18774
- 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();
18775
19275
  const logicalX = note.startStep * stepWidth;
18776
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19276
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18777
19277
  const logicalY = yIndex * keyHeight;
18778
19278
  const renderX = logicalX - offset.x;
18779
19279
  const renderY = logicalY - offset.y;
@@ -18792,7 +19292,7 @@ var createPianoRoll = (options, handlers) => {
18792
19292
  noteId: note.id,
18793
19293
  mode: "move",
18794
19294
  dragOffsetStep: step - note.startStep,
18795
- dragOffsetPitch: pitch - note.pitch,
19295
+ dragOffsetPitch: pitch - note.pitchUnits,
18796
19296
  startStep: note.startStep
18797
19297
  };
18798
19298
  });
@@ -18803,41 +19303,47 @@ var createPianoRoll = (options, handlers) => {
18803
19303
  "wheel",
18804
19304
  (e) => {
18805
19305
  e.preventDefault();
18806
- const configValues = getRenderConfig();
19306
+ const configValues = renderer.getRenderConfig();
18807
19307
  const gridHeight = gridCanvas.height;
18808
19308
  const maxOffsetY = Math.max(
18809
19309
  0,
18810
19310
  configValues.keyCount * configValues.keyHeight - gridHeight
18811
19311
  );
18812
- const currentOffset = getDrawOffset();
19312
+ const currentOffset = renderer.getDrawOffset();
18813
19313
  const nextOffsetY = Math.min(
18814
19314
  Math.max(currentOffset.y + e.deltaY, 0),
18815
19315
  maxOffsetY
18816
19316
  );
18817
- setDrawOffset(currentOffset.x, nextOffsetY);
18818
- drawGrid();
18819
- drawNotes(core.getNotes());
19317
+ renderer.setDrawOffset(currentOffset.x, nextOffsetY);
19318
+ renderer.drawGrid();
19319
+ renderer.drawNotes(core.getNotes());
18820
19320
  },
18821
19321
  { passive: false }
18822
19322
  );
18823
19323
  const redraw = () => {
18824
- drawGrid();
18825
- drawNotes(core.getNotes());
19324
+ renderer.drawGrid();
19325
+ renderer.drawNotes(core.getNotes());
18826
19326
  if (core.getToolMode() === "select") {
18827
- drawSelectionRect(selectionRect);
19327
+ renderer.drawSelectionRect(selectionRect);
18828
19328
  if (selectedNotes.length > 0) {
18829
19329
  const selectedIds = new Set(selectedNotes.map((n) => n.id));
18830
- drawSelectedNotes(core.getNotes(), selectedIds);
19330
+ renderer.drawSelectedNotes(core.getNotes(), selectedIds);
18831
19331
  }
18832
19332
  }
18833
19333
  };
18834
19334
  const getNotesInRect = (rect) => {
18835
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18836
- 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();
18837
19343
  const notes = [];
18838
19344
  for (const note of core.getNotes()) {
18839
19345
  const logicalX = note.startStep * stepWidth;
18840
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19346
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18841
19347
  const logicalY = yIndex * keyHeight;
18842
19348
  const noteRect = {
18843
19349
  x: logicalX - offset.x,
@@ -18884,7 +19390,7 @@ var createPianoRoll = (options, handlers) => {
18884
19390
  const minStart = Math.min(...copiedNotes.map((n) => n.startStep));
18885
19391
  copiedNotes.forEach((note) => {
18886
19392
  const newStep = startStep + (note.startStep - minStart);
18887
- core.addNote(newStep, note.pitch, {
19393
+ core.addNote(newStep, note.pitchUnits, {
18888
19394
  noteLengthSteps: note.durationSteps,
18889
19395
  velocity: note.velocity
18890
19396
  });
@@ -19725,10 +20231,12 @@ var createDtmStudio = async (options = {}) => {
19725
20231
  );
19726
20232
  }
19727
20233
  if (!sfInst) return;
20234
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
19728
20235
  sfInst.play({
19729
20236
  ctx: audioCtx,
19730
20237
  destination: getChannelStrip(e.trackId).input,
19731
- pitch: e.pitch,
20238
+ pitch: midi,
20239
+ detuneCents,
19732
20240
  volume: e.volume,
19733
20241
  velocity: e.velocity,
19734
20242
  when: e.when,
@@ -20044,10 +20552,12 @@ var createDtmStudio = async (options = {}) => {
20044
20552
  );
20045
20553
  }
20046
20554
  if (!sfInst) return;
20555
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20047
20556
  sfInst.play({
20048
20557
  ctx: audioCtx,
20049
20558
  destination: getChannelStrip(e.trackId).input,
20050
- pitch: e.pitch,
20559
+ pitch: midi,
20560
+ detuneCents,
20051
20561
  volume: e.volume,
20052
20562
  velocity: e.velocity,
20053
20563
  when: e.when,
@@ -20133,10 +20643,12 @@ var createDtmStudio = async (options = {}) => {
20133
20643
  );
20134
20644
  }
20135
20645
  if (!sfInst) return;
20646
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20136
20647
  sfInst.play({
20137
20648
  ctx: audioCtx,
20138
20649
  destination: getChannelStrip(e.trackId).input,
20139
- pitch: e.pitch,
20650
+ pitch: midi,
20651
+ detuneCents,
20140
20652
  volume: e.volume,
20141
20653
  velocity: e.velocity,
20142
20654
  when: e.when,
@@ -20215,10 +20727,12 @@ var createDtmStudio = async (options = {}) => {
20215
20727
  );
20216
20728
  }
20217
20729
  if (!sfInst) return;
20730
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20218
20731
  sfInst.play({
20219
20732
  ctx: audioCtx,
20220
20733
  destination: getChannelStrip(e.trackId).input,
20221
- pitch: e.pitch,
20734
+ pitch: midi,
20735
+ detuneCents,
20222
20736
  volume: e.volume,
20223
20737
  velocity: e.velocity,
20224
20738
  when: e.when,
@@ -20255,10 +20769,12 @@ var createDtmStudio = async (options = {}) => {
20255
20769
  sfInst = resolveSoundFont(defaultPreset, role, "simple");
20256
20770
  }
20257
20771
  if (!sfInst) return;
20772
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20258
20773
  sfInst.play({
20259
20774
  ctx: audioCtx,
20260
20775
  destination: getChannelStrip(e.trackId).input,
20261
- pitch: e.pitch,
20776
+ pitch: midi,
20777
+ detuneCents,
20262
20778
  volume: e.volume,
20263
20779
  velocity: e.velocity,
20264
20780
  when: e.when,
@@ -20281,10 +20797,12 @@ var createDtmStudio = async (options = {}) => {
20281
20797
  if (audioCtx.state === "suspended") {
20282
20798
  await audioCtx.resume();
20283
20799
  }
20800
+ const { midi, detuneCents } = unitsToMidiDetune(options2.pitchUnits);
20284
20801
  sfInst.play({
20285
20802
  ctx: audioCtx,
20286
20803
  destination: masterGain,
20287
- pitch: options2.pitch,
20804
+ pitch: midi,
20805
+ detuneCents,
20288
20806
  volume: vol / 100,
20289
20807
  when: 0,
20290
20808
  duration: dur
@@ -20304,7 +20822,7 @@ var createDtmStudio = async (options = {}) => {
20304
20822
  // 伴奏トラック
20305
20823
  startStep: p.startStep,
20306
20824
  durationSteps: p.durationSteps,
20307
- pitch: p.pitch,
20825
+ pitchUnits: p.pitchUnits,
20308
20826
  velocity: p.velocity
20309
20827
  }));
20310
20828
  const playerPreset = defaultPreset;
@@ -20312,10 +20830,12 @@ var createDtmStudio = async (options = {}) => {
20312
20830
  const playPlayerNote = (e) => {
20313
20831
  const sfInst = resolveSoundFont(playerPreset, "chord");
20314
20832
  if (!sfInst) return;
20833
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20315
20834
  sfInst.play({
20316
20835
  ctx: audioCtx,
20317
20836
  destination: getChannelStrip("chord").input,
20318
- pitch: e.pitch,
20837
+ pitch: midi,
20838
+ detuneCents,
20319
20839
  volume: e.volume,
20320
20840
  velocity: e.velocity,
20321
20841
  when: e.when,
@@ -20453,8 +20973,12 @@ var createDtmStudio = async (options = {}) => {
20453
20973
  };
20454
20974
  };
20455
20975
  export {
20976
+ A4_HZ,
20977
+ A4_UNITS,
20978
+ CENTS_PER_UNIT,
20456
20979
  DAW_CSS,
20457
20980
  DEFAULT_BPM,
20981
+ DEFAULT_EDO,
20458
20982
  DEFAULT_GATE,
20459
20983
  DEFAULT_PAN,
20460
20984
  DEFAULT_PLAYBACK_VELOCITY,
@@ -20464,21 +20988,30 @@ export {
20464
20988
  DRUM_FONT,
20465
20989
  DRUM_KEYS,
20466
20990
  DRUM_PATTERNS,
20991
+ EDO31_NAMES,
20467
20992
  GM_INSTRUMENT_NAMES,
20468
20993
  INSTRUMENT_PRESETS,
20994
+ KEY_COUNT,
20469
20995
  KOE_BASE_URL,
20470
20996
  KOE_VOICEBANKS,
20471
20997
  KOE_VOICEBANK_LABELS,
20472
20998
  KOE_VOICEBANK_TERMS,
20473
20999
  LinkedList,
20474
21000
  MAX_VOCAL_VOLUME,
21001
+ MICRO_STEP,
20475
21002
  MMLCore,
20476
21003
  MML_END_MARKER,
20477
21004
  MidiSearchClient,
20478
- PITCH_MAP2 as PITCH_MAP,
21005
+ PITCH_ENCODING_VERSION,
21006
+ PITCH_MAP,
21007
+ PITCH_RANGE_END,
21008
+ PITCH_RANGE_START,
20479
21009
  PREWARM_NOTES,
20480
21010
  TRACKS_ADVANCED,
20481
21011
  TRACKS_SIMPLE,
21012
+ UNITS_PER_EDO31_DEGREE,
21013
+ UNITS_PER_OCTAVE,
21014
+ UNITS_PER_SEMITONE,
20482
21015
  VIBRATO_MIN_SEC,
20483
21016
  VOICE_IMAGES,
20484
21017
  VOICE_IMAGE_KEY,
@@ -20488,6 +21021,7 @@ export {
20488
21021
  buildChordPlacements,
20489
21022
  buildDrumPatternJson,
20490
21023
  buildNameToKeyMapping,
21024
+ chromaticStep,
20491
21025
  collectPitchTokens,
20492
21026
  concatFloat32,
20493
21027
  createAudioContext,
@@ -20496,19 +21030,13 @@ export {
20496
21030
  createKoeVoice,
20497
21031
  createLyricsConductor,
20498
21032
  createPianoRoll,
21033
+ createRenderer,
20499
21034
  createSequencer,
20500
21035
  createSingingVoices,
20501
21036
  createSynth,
20502
21037
  createVoiceRegistry,
20503
21038
  decodeMml,
20504
21039
  decomposeToMonophonic,
20505
- drawGrid,
20506
- drawHeader,
20507
- drawKeyboard,
20508
- drawNoteLyrics,
20509
- drawNotes,
20510
- drawSelectedNotes,
20511
- drawSelectionRect,
20512
21040
  encodeMml,
20513
21041
  encodeWavPCM16,
20514
21042
  exportMIDI,
@@ -20516,36 +21044,34 @@ export {
20516
21044
  extractMidiDrumPattern,
20517
21045
  extractMidiPlacements,
20518
21046
  extractMidiPlacementsByTrack,
21047
+ fifthToStep,
21048
+ fifthToUnits,
20519
21049
  formatMmlMeta,
20520
21050
  freqFromPitch,
20521
21051
  generateRandomPattern,
20522
- getDrawOffset,
20523
21052
  getDrumPatternKeys,
20524
- getGridCanvas,
20525
- getGridContext,
20526
- getGridPosition,
20527
- getHeaderCanvas,
20528
21053
  getMidiBPM,
20529
- getRenderConfig,
20530
- getXY,
20531
21054
  icon,
20532
- init,
20533
21055
  injectStyles,
20534
21056
  isChordHeavyTrack,
21057
+ isNaturalLetter,
20535
21058
  isPlausibleMidiTranscription,
20536
21059
  isValidHttpUrl,
21060
+ keyCountFor,
20537
21061
  koeUrl,
21062
+ midiToUnits,
20538
21063
  mountChordPlayer,
20539
21064
  mountDAW,
20540
21065
  mountMmlPlayer,
21066
+ naturalStep,
20541
21067
  normalizeDrumPatterns,
20542
21068
  normalizeLyrics,
20543
- onClick,
20544
21069
  panToStereo,
20545
21070
  parseCustomVocals,
20546
21071
  parseLyrics,
20547
21072
  parseMML,
20548
21073
  parseMmlMeta,
21074
+ pitchV1ToUnits,
20549
21075
  playChords,
20550
21076
  playMML,
20551
21077
  playNote,
@@ -20553,13 +21079,18 @@ export {
20553
21079
  playSingingMML,
20554
21080
  resolveDrumPattern,
20555
21081
  resolveLoopPoint,
20556
- setBackgroundActive,
20557
- setDrawOffset,
20558
21082
  shiftNotes,
20559
21083
  showLoadingOverlay,
21084
+ spellingToUnits,
20560
21085
  stripCustomVocals,
20561
21086
  stripLyrics,
20562
21087
  stripMmlMeta,
20563
21088
  transposeNotes,
21089
+ unitsPerRow,
21090
+ unitsPerStep,
21091
+ unitsToHz,
21092
+ unitsToMidi,
21093
+ unitsToMidiDetune,
21094
+ unitsToPitchV1,
20564
21095
  vocalVolumeToGain
20565
21096
  };