@onjmin/dtm 1.0.8 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,77 @@ var createChannelStrip = (ctx, destination, options = {}) => {
1499
1568
  };
1500
1569
  };
1501
1570
 
1571
+ // src/tuning.ts
1572
+ var units = (n) => n;
1573
+ var midiNote = (n) => n;
1574
+ var addUnits = (a, delta) => a + delta;
1575
+ var UNITS_PER_OCTAVE = 372;
1576
+ var UNITS_PER_SEMITONE = 372 / 12;
1577
+ var UNITS_PER_EDO31_DEGREE = 372 / 31;
1578
+ var CENTS_PER_UNIT = 1200 / UNITS_PER_OCTAVE;
1579
+ var A4_UNITS = 69 * UNITS_PER_SEMITONE;
1580
+ var A4_HZ = 440;
1581
+ var DEFAULT_EDO = 12;
1582
+ var unitsPerStep = (edo) => UNITS_PER_OCTAVE / edo;
1583
+ var midiToUnits = (midi) => midi * UNITS_PER_SEMITONE;
1584
+ var unitsToMidi = (u) => u / UNITS_PER_SEMITONE;
1585
+ var unitsToHz = (u) => A4_HZ * 2 ** ((u - A4_UNITS) / UNITS_PER_OCTAVE);
1586
+ var unitsToMidiDetune = (u) => {
1587
+ const midi = Math.round(u / UNITS_PER_SEMITONE);
1588
+ return {
1589
+ midi,
1590
+ detuneCents: (u - midi * UNITS_PER_SEMITONE) * CENTS_PER_UNIT
1591
+ };
1592
+ };
1593
+ var NATURAL_STEPS = {
1594
+ 12: { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 },
1595
+ 31: { c: 0, d: 5, e: 10, f: 13, g: 18, a: 23, b: 28 }
1596
+ };
1597
+ var chromaticStep = (edo) => edo === 31 ? 2 : 1;
1598
+ var MICRO_STEP = 1;
1599
+ var isNaturalLetter = (ch) => Object.hasOwn(NATURAL_STEPS[12], ch);
1600
+ var naturalStep = (ch, edo) => NATURAL_STEPS[edo][ch] ?? null;
1601
+ var spellingToUnits = (letter, octave, chromatic, micro, edo) => {
1602
+ const step = naturalStep(letter, edo);
1603
+ if (step === null) return null;
1604
+ const perStep = unitsPerStep(edo);
1605
+ const stepsFromC = step + chromatic * chromaticStep(edo) + micro * MICRO_STEP;
1606
+ return (octave + 1) * UNITS_PER_OCTAVE + stepsFromC * perStep;
1607
+ };
1608
+ var FIFTH_STEPS = { 12: 7, 31: 18 };
1609
+ var fifthToStep = (fifthIndex, edo) => {
1610
+ const n = fifthIndex * FIFTH_STEPS[edo];
1611
+ return (n % edo + edo) % edo;
1612
+ };
1613
+ var fifthToUnits = (fifthIndex, edo) => fifthToStep(fifthIndex, edo) * unitsPerStep(edo);
1614
+ var PITCH_ENCODING_VERSION = 2;
1615
+ var pitchV1ToUnits = (pitch) => pitch * UNITS_PER_SEMITONE;
1616
+ var unitsToPitchV1 = (u) => Math.round(u / UNITS_PER_SEMITONE);
1617
+
1502
1618
  // src/chords.ts
1503
1619
  var C3 = 48;
1620
+ var MEANTONE_STEP_BY_SEMITONE = [0, 3, 5, 8, 10, 13, 16, 18, 21, 23, 26, 28];
1621
+ var chordToneToUnits = (relSemitone, relFifth, rootSemitone, rootFifth, edo) => {
1622
+ const perStep = UNITS_PER_OCTAVE / edo;
1623
+ const rootOct = Math.floor(rootSemitone / 12);
1624
+ const rootWithin = (rootSemitone % 12 + 12) % 12;
1625
+ const rootStep = edo === 31 ? (fifthToStep(rootFifth, 31) + MEANTONE_STEP_BY_SEMITONE[rootWithin] - fifthToStep(rootFifth, 31) + 31) % 31 : rootWithin;
1626
+ const relWithin12 = (relSemitone % 12 + 12) % 12;
1627
+ const relOct = Math.round((relSemitone - relWithin12) / 12);
1628
+ const relStep = (fifthToStep(relFifth, edo) - fifthToStep(rootFifth, edo) + edo) % edo;
1629
+ return units(
1630
+ (rootOct + relOct) * UNITS_PER_OCTAVE + (rootStep + relStep) * perStep
1631
+ );
1632
+ };
1504
1633
  var buildChordPlacements = (options) => {
1505
- const { chordStr, patternType, rootShift, bpm, stepsPerBar } = options;
1634
+ const {
1635
+ chordStr,
1636
+ patternType,
1637
+ rootShift,
1638
+ bpm,
1639
+ stepsPerBar,
1640
+ edo = 12
1641
+ } = options;
1506
1642
  const placements = [];
1507
1643
  if (!chordStr.trim()) return placements;
1508
1644
  const offset = rootShift;
@@ -1531,8 +1667,20 @@ var buildChordPlacements = (options) => {
1531
1667
  for (const group of Object.values(chordGroups)) {
1532
1668
  for (const chord of group) {
1533
1669
  let notes;
1670
+ let toUnits;
1534
1671
  try {
1535
- notes = [...parseChord(`${chord.key}${chord.chord}`).notes];
1672
+ const parsed = parseChord(`${chord.key}${chord.chord}`);
1673
+ notes = [...parsed.notes];
1674
+ const fifthOf = new Map(
1675
+ parsed.notes.map((n, i2) => [n, parsed.noteFifths[i2]])
1676
+ );
1677
+ toUnits = (rel) => chordToneToUnits(
1678
+ rel,
1679
+ fifthOf.get(rel) ?? parsed.rootFifth,
1680
+ C3 + offset,
1681
+ parsed.rootFifth,
1682
+ edo === 31 ? 31 : 12
1683
+ );
1536
1684
  } catch {
1537
1685
  continue;
1538
1686
  }
@@ -1541,7 +1689,7 @@ var buildChordPlacements = (options) => {
1541
1689
  for (const noteOffset of notes) {
1542
1690
  placements.push({
1543
1691
  startStep: chord.whenStep,
1544
- pitch: C3 + noteOffset + offset,
1692
+ pitchUnits: toUnits(noteOffset),
1545
1693
  durationSteps: noteLength,
1546
1694
  velocity: 100
1547
1695
  });
@@ -1551,7 +1699,7 @@ var buildChordPlacements = (options) => {
1551
1699
  notes.forEach((noteOffset, i2) => {
1552
1700
  placements.push({
1553
1701
  startStep: chord.whenStep + i2 * arpInterval,
1554
- pitch: C3 + noteOffset + offset,
1702
+ pitchUnits: toUnits(noteOffset),
1555
1703
  durationSteps: noteLength - i2 * arpInterval,
1556
1704
  velocity: 100
1557
1705
  });
@@ -1561,7 +1709,7 @@ var buildChordPlacements = (options) => {
1561
1709
  notes.forEach((noteOffset, i2) => {
1562
1710
  placements.push({
1563
1711
  startStep: chord.whenStep + i2 * arpInterval,
1564
- pitch: C3 + noteOffset + offset,
1712
+ pitchUnits: toUnits(noteOffset),
1565
1713
  durationSteps: Math.max(12, noteLength - i2 * arpInterval),
1566
1714
  velocity: 100
1567
1715
  });
@@ -1575,7 +1723,7 @@ var buildChordPlacements = (options) => {
1575
1723
  for (const noteOffset of notes) {
1576
1724
  placements.push({
1577
1725
  startStep: syncopatedStep,
1578
- pitch: C3 + noteOffset + offset,
1726
+ pitchUnits: toUnits(noteOffset),
1579
1727
  durationSteps: Math.min(halfBeat, 12),
1580
1728
  velocity: 100
1581
1729
  });
@@ -1594,7 +1742,7 @@ var buildChordPlacements = (options) => {
1594
1742
  for (const noteOffset of notes) {
1595
1743
  placements.push({
1596
1744
  startStep: noteStart,
1597
- pitch: C3 + noteOffset + offset,
1745
+ pitchUnits: toUnits(noteOffset),
1598
1746
  durationSteps: yatsumeLengthSteps,
1599
1747
  velocity: 100
1600
1748
  });
@@ -1606,7 +1754,7 @@ var buildChordPlacements = (options) => {
1606
1754
  const stepOffset = i2 * Math.floor(stepsPerBar / 4);
1607
1755
  placements.push({
1608
1756
  startStep: chord.whenStep + stepOffset,
1609
- pitch: C3 + noteOffset + offset,
1757
+ pitchUnits: toUnits(noteOffset),
1610
1758
  durationSteps: Math.max(12, Math.floor(stepsPerBar / 4)),
1611
1759
  velocity: 100
1612
1760
  });
@@ -1618,8 +1766,20 @@ var buildChordPlacements = (options) => {
1618
1766
  const chordNames = chordStr.split(/[\s,]+/).filter((c) => c);
1619
1767
  chordNames.forEach((chordName, barIndex) => {
1620
1768
  let notes;
1769
+ let toUnits;
1621
1770
  try {
1622
- notes = [...parseChord(chordName).notes];
1771
+ const parsed = parseChord(chordName);
1772
+ notes = [...parsed.notes];
1773
+ const fifthOf = new Map(
1774
+ parsed.notes.map((n, i2) => [n, parsed.noteFifths[i2]])
1775
+ );
1776
+ toUnits = (rel) => chordToneToUnits(
1777
+ rel,
1778
+ fifthOf.get(rel) ?? parsed.rootFifth,
1779
+ C3 + offset,
1780
+ parsed.rootFifth,
1781
+ edo === 31 ? 31 : 12
1782
+ );
1623
1783
  } catch {
1624
1784
  return;
1625
1785
  }
@@ -1629,7 +1789,7 @@ var buildChordPlacements = (options) => {
1629
1789
  const stepOffset = i2 * 3;
1630
1790
  placements.push({
1631
1791
  startStep: startStep + stepOffset,
1632
- pitch: C3 + noteOffset + offset,
1792
+ pitchUnits: toUnits(noteOffset),
1633
1793
  durationSteps: chordLength - stepOffset,
1634
1794
  velocity: 100
1635
1795
  });
@@ -2044,7 +2204,7 @@ var Worldline = class _Worldline {
2044
2204
  const WL = this.wasm;
2045
2205
  const FS = WORLDLINE_SAMPLE_RATE;
2046
2206
  const basePitch = sampleCurve(preMs + durationMs / 2, pitch, preMs + durationMs);
2047
- const midiNote = Math.round(69 + 12 * Math.log2(basePitch / 440));
2207
+ const midiNote2 = Math.round(69 + 12 * Math.log2(basePitch / 440));
2048
2208
  const posMs = 0;
2049
2209
  const reqLen = preMs + durationMs;
2050
2210
  const cutMs = WL_FRAME_MS * 2;
@@ -2068,7 +2228,7 @@ var Worldline = class _Worldline {
2068
2228
  sv(8, samplePtr, "*");
2069
2229
  sv(12, 0, "i32");
2070
2230
  sv(16, 0, "*");
2071
- sv(20, midiNote, "i32");
2231
+ sv(20, midiNote2, "i32");
2072
2232
  sv(24, 100, "double");
2073
2233
  sv(32, 0, "double");
2074
2234
  sv(40, reqLen, "double");
@@ -2161,6 +2321,11 @@ var DEFAULT_VELOCITY = 100;
2161
2321
  var DEFAULT_PLAYBACK_VELOCITY = 127;
2162
2322
  var DEFAULT_STEPS_PER_BAR = 192;
2163
2323
  var MML_END_MARKER = "#end;";
2324
+ var PITCH_RANGE_START = 0;
2325
+ var PITCH_RANGE_END = 3937;
2326
+ var unitsPerRow = (edo) => 372 / edo;
2327
+ var keyCountFor = (edo) => Math.floor((PITCH_RANGE_END - PITCH_RANGE_START) / unitsPerRow(edo)) + 1;
2328
+ var KEY_COUNT = keyCountFor(12);
2164
2329
 
2165
2330
  // src/vibrato.ts
2166
2331
  var VIBRATO_MIN_SEC = 0.35;
@@ -2560,7 +2725,8 @@ var FORMANTS = {
2560
2725
  // 撥音(ん)は鼻音寄りの低フォルマント
2561
2726
  N: [250, 1e3]
2562
2727
  };
2563
- var midiToFreq = (m) => 440 * 2 ** ((m - 69) / 12);
2728
+ var unitsToFreq = (units2) => 440 * 2 ** ((units2 - 2139) / 372);
2729
+ var unitsToMidiFloat = (units2) => units2 / 31;
2564
2730
  var createKlattVoice = (ctx, destination, reverbBus, delayBus) => {
2565
2731
  const active = /* @__PURE__ */ new Set();
2566
2732
  const voice = (syllable, e) => {
@@ -2594,7 +2760,7 @@ var createKlattVoice = (ctx, destination, reverbBus, delayBus) => {
2594
2760
  }
2595
2761
  const osc = ctx.createOscillator();
2596
2762
  osc.type = "sawtooth";
2597
- osc.frequency.value = midiToFreq(e.pitch);
2763
+ osc.frequency.value = unitsToFreq(e.pitchUnits);
2598
2764
  const makeFormant = (freq, q2, gainScale) => {
2599
2765
  const filter = ctx.createBiquadFilter();
2600
2766
  filter.type = "bandpass";
@@ -2846,7 +3012,7 @@ var createLocalBackend = async (options) => {
2846
3012
  if (!consonantPcm || !vowelPcm) return null;
2847
3013
  const spliced = spliceCompositePcm(consonantPcm, vowelPcm, KOE_SAMPLE_RATE);
2848
3014
  if (!spliced) return null;
2849
- const targetHz = midiToFreq(pitch);
3015
+ const targetHz = unitsToFreq(pitch);
2850
3016
  const audio = worldline.renderNote({
2851
3017
  pcm: spliced.pcm,
2852
3018
  pitch: vibrato ? vibratoPitchCurve(targetHz, spliced.preMs) : targetHz,
@@ -2875,7 +3041,7 @@ var createLocalBackend = async (options) => {
2875
3041
  if (!pcm || pcm.length === 0) return null;
2876
3042
  const entry = bank.manifest.phonemes[alias];
2877
3043
  const lead = leadInFromEntry(entry);
2878
- const targetHz = midiToFreq(pitch);
3044
+ const targetHz = unitsToFreq(pitch);
2879
3045
  if (worldline) {
2880
3046
  const audio = worldline.renderNote({
2881
3047
  pcm,
@@ -3082,7 +3248,7 @@ var createKoeVoice = async (ctx, destination, options) => {
3082
3248
  backend.pitchTokens,
3083
3249
  syllable,
3084
3250
  prevVowel,
3085
- e.pitch
3251
+ unitsToMidiFloat(e.pitchUnits)
3086
3252
  );
3087
3253
  if (syllable.vowel && syllable.vowel !== "N") prevVowel = syllable.vowel;
3088
3254
  if (!alias) return;
@@ -3090,7 +3256,7 @@ var createKoeVoice = async (ctx, destination, options) => {
3090
3256
  const peak = Math.max(1e-4, e.volume);
3091
3257
  const pan = e.pan ?? 0;
3092
3258
  const durationMs = Math.max(60, e.duration * 1e3);
3093
- void renderInto(alias, e.pitch, durationMs).then((r) => {
3259
+ void renderInto(alias, e.pitchUnits, durationMs).then((r) => {
3094
3260
  if (r)
3095
3261
  schedule(r, t0, peak, pan, e.reverbSend, e.delaySend, e.destination);
3096
3262
  });
@@ -3102,7 +3268,10 @@ var createKoeVoice = async (ctx, destination, options) => {
3102
3268
  backend.pitchTokens,
3103
3269
  syllable,
3104
3270
  prevVowelArg,
3105
- pitch
3271
+ // 多音階バンクのピッチトークン("_G4" 等)は録音の音名なので、
3272
+ // 最寄り選択はMIDIノート番号の尺度で行う。units のまま渡すと常に
3273
+ // 最高音のトークンが選ばれてしまう。
3274
+ unitsToMidiFloat(pitch)
3106
3275
  );
3107
3276
  if (!alias) return null;
3108
3277
  const dMs = Math.max(60, durationMs);
@@ -3134,13 +3303,14 @@ var STREAM_LOOKAHEAD_SEC = 1.5;
3134
3303
  var STREAM_POLL_MS = 100;
3135
3304
  var OCTAVE_UNISON_PEAK_SCALE = 0.6;
3136
3305
  var octaveUnisonOffsets = (mode) => {
3306
+ const OCT = 372;
3137
3307
  switch (mode) {
3138
3308
  case "down":
3139
- return [-12];
3309
+ return [-OCT];
3140
3310
  case "up":
3141
- return [12];
3311
+ return [OCT];
3142
3312
  case "both":
3143
- return [-12, 12];
3313
+ return [-OCT, OCT];
3144
3314
  default:
3145
3315
  return [];
3146
3316
  }
@@ -3350,7 +3520,7 @@ var createSingingVoices = (ctx, destination, options = {}) => {
3350
3520
  const effPeak = (dest ? peak * masterVolumeScalar : peak) * peakScale;
3351
3521
  model(note.syllable, {
3352
3522
  trackId: track.id ?? "",
3353
- pitch,
3523
+ pitchUnits: pitch,
3354
3524
  velocity: 100,
3355
3525
  volume: effPeak,
3356
3526
  when,
@@ -3365,7 +3535,7 @@ var createSingingVoices = (ctx, destination, options = {}) => {
3365
3535
  };
3366
3536
  dispatchNote(note.pitch, 1);
3367
3537
  for (const offset of octaveUnisonOffsets(track.octaveUnison)) {
3368
- dispatchNote(note.pitch + offset, OCTAVE_UNISON_PEAK_SCALE);
3538
+ dispatchNote(units(note.pitch + offset), OCTAVE_UNISON_PEAK_SCALE);
3369
3539
  }
3370
3540
  if (!(model.renderToCache && model.scheduleCached)) {
3371
3541
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -3416,17 +3586,13 @@ var createVoiceRegistry = (models = {}, fallback = "klatt") => {
3416
3586
  };
3417
3587
 
3418
3588
  // 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
3589
+ var NATURAL_STEPS2 = {
3590
+ 12: { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 },
3591
+ 31: { c: 0, d: 5, e: 10, f: 13, g: 18, a: 23, b: 28 }
3427
3592
  };
3593
+ var isNatural = (ch) => Object.hasOwn(NATURAL_STEPS2[12], ch);
3428
3594
  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;
3595
+ var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode|edo)=([\w-]+)/gi;
3430
3596
  var TRACK_INST_DIRECTIVE = /#t(\d+)inst=([^#;\r\n]+)/gi;
3431
3597
  var TRACK_COMP_DIRECTIVE = /#t(\d+)comp=(\d+)/gi;
3432
3598
  var TRACK_WIDTH_DIRECTIVE = /#t(\d+)width=(\d+)/gi;
@@ -3476,6 +3642,9 @@ var parseMmlMeta = (mml) => {
3476
3642
  if (m[2] === "simple" || m[2] === "advanced") {
3477
3643
  meta.mode = m[2];
3478
3644
  }
3645
+ } else if (key === "edo") {
3646
+ const e = Number.parseInt(m[2], 10);
3647
+ if (e === 12 || e === 31) meta.edo = e;
3479
3648
  }
3480
3649
  }
3481
3650
  for (const m of mml.matchAll(TRACK_INST_DIRECTIVE)) {
@@ -3578,6 +3747,7 @@ var formatMmlMeta = (meta, space = "") => {
3578
3747
  if (meta.fadeOut !== void 0 && meta.fadeOut !== 0)
3579
3748
  parts.push(`#fadeout=${meta.fadeOut}`);
3580
3749
  if (meta.mode) parts.push(`#mode=${meta.mode}`);
3750
+ if (meta.edo !== void 0 && meta.edo !== 12) parts.push(`#edo=${meta.edo}`);
3581
3751
  if (meta.trackInstruments) {
3582
3752
  for (const [idx, name] of Object.entries(meta.trackInstruments)) {
3583
3753
  if (name) parts.push(`#t${idx}inst=${name}`);
@@ -3652,6 +3822,10 @@ var parseMML = (mml, options = {}) => {
3652
3822
  const endMarkerBase = MML_END_MARKER.replace(/;+$/, "");
3653
3823
  const endRegex = new RegExp(`(?<![cdafgCDAFG])${endMarkerBase}\\b;?`, "gi");
3654
3824
  const fullMML = stripLyrics(noMeta).replace(endRegex, "").replace(/[\n\r]+/g, " ").trim();
3825
+ const edo = meta.edo === 31 ? 31 : 12;
3826
+ const naturals = NATURAL_STEPS2[edo];
3827
+ const unitsPerStep2 = 372 / edo;
3828
+ const chromaticStep2 = edo === 31 ? 2 : 1;
3655
3829
  const parts = fullMML.split(/(@\d+)/).filter((p) => p.trim().length > 0);
3656
3830
  let trackIndex = 0;
3657
3831
  let sourceTrackIndex = 0;
@@ -3699,6 +3873,19 @@ var parseMML = (mml, options = {}) => {
3699
3873
  type
3700
3874
  });
3701
3875
  };
3876
+ const readAccidentals = () => {
3877
+ let delta = 0;
3878
+ while (j < body.length) {
3879
+ const a = body[j];
3880
+ if (a === "#") delta += chromaticStep2;
3881
+ else if (a === "-") delta -= chromaticStep2;
3882
+ else if (a === "+") delta += edo === 31 ? 1 : chromaticStep2;
3883
+ else if (a === "_") delta -= edo === 31 ? 1 : chromaticStep2;
3884
+ else break;
3885
+ j++;
3886
+ }
3887
+ return delta;
3888
+ };
3702
3889
  const parseLength = () => {
3703
3890
  let numStr = "";
3704
3891
  while (j < body.length && /\d/.test(body[j])) {
@@ -3770,17 +3957,13 @@ var parseMML = (mml, options = {}) => {
3770
3957
  const savedOctave = octave;
3771
3958
  while (j < body.length && body[j] !== "]") {
3772
3959
  const c = body[j];
3773
- if (Object.hasOwn(PITCH_MAP, c)) {
3774
- let pitch = PITCH_MAP[c];
3960
+ if (isNatural(c)) {
3961
+ let pitchSteps = naturals[c];
3775
3962
  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);
3963
+ pitchSteps += readAccidentals();
3964
+ chordNotes.push(
3965
+ units((octave + 1) * 372 + pitchSteps * unitsPerStep2)
3966
+ );
3784
3967
  } else if (c === ">") {
3785
3968
  octave = Math.min(8, octave + 1);
3786
3969
  j++;
@@ -3806,7 +3989,7 @@ var parseMML = (mml, options = {}) => {
3806
3989
  placements.push({
3807
3990
  trackIndex,
3808
3991
  startStep: currentStep,
3809
- pitch: p,
3992
+ pitchUnits: p,
3810
3993
  durationSteps: Math.max(1, steps),
3811
3994
  velocity
3812
3995
  });
@@ -3814,23 +3997,17 @@ var parseMML = (mml, options = {}) => {
3814
3997
  pushTok("chord", currentStep, Math.max(1, steps), tokStart);
3815
3998
  currentStep += steps;
3816
3999
  octave = savedOctave;
3817
- } else if (Object.hasOwn(PITCH_MAP, ch)) {
3818
- let pitch = PITCH_MAP[ch];
4000
+ } else if (isNatural(ch)) {
4001
+ let pitchSteps = naturals[ch];
3819
4002
  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;
4003
+ pitchSteps += readAccidentals();
4004
+ const midiPitch = units((octave + 1) * 372 + pitchSteps * unitsPerStep2);
3828
4005
  const steps = parseLength();
3829
4006
  recordContributor();
3830
4007
  placements.push({
3831
4008
  trackIndex,
3832
4009
  startStep: currentStep,
3833
- pitch: midiPitch,
4010
+ pitchUnits: midiPitch,
3834
4011
  durationSteps: Math.max(1, steps),
3835
4012
  velocity
3836
4013
  });
@@ -3962,7 +4139,7 @@ var createSequencer = (options) => {
3962
4139
  maxEndStep = Math.max(maxEndStep, note.startStep + note.durationSteps);
3963
4140
  timeline.push({
3964
4141
  trackId: track.id,
3965
- pitch: note.pitch,
4142
+ pitch: note.pitchUnits,
3966
4143
  volume: track.volume / 100,
3967
4144
  velocity: note.velocity ?? DEFAULT_PLAYBACK_VELOCITY,
3968
4145
  when,
@@ -4025,7 +4202,7 @@ var createSequencer = (options) => {
4025
4202
  const currentVolume = (trackVolumeMap.get(ev.trackId) ?? ev.volume * 100) / 100;
4026
4203
  options.onPlayNote({
4027
4204
  trackId: ev.trackId,
4028
- pitch: ev.pitch,
4205
+ pitchUnits: ev.pitch,
4029
4206
  velocity: ev.velocity,
4030
4207
  volume: currentVolume * velocityVolume,
4031
4208
  when: Math.max(0, _when),
@@ -6345,7 +6522,7 @@ var SONG_DRUM_PATTERNS = {
6345
6522
  };
6346
6523
 
6347
6524
  // src/synth.ts
6348
- var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
6525
+ var freqFromPitch = (pitchUnits) => unitsToHz(pitchUnits);
6349
6526
  var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
6350
6527
  const wave = tone.wave ?? "square";
6351
6528
  const attack = tone.attack ?? 0;
@@ -6361,7 +6538,7 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
6361
6538
  const osc = ctx.createOscillator();
6362
6539
  const gain = ctx.createGain();
6363
6540
  osc.type = wave;
6364
- osc.frequency.value = freqFromPitch(e.pitch);
6541
+ osc.frequency.value = freqFromPitch(e.pitchUnits);
6365
6542
  const t0 = ctx.currentTime + e.when;
6366
6543
  const peak = Math.max(1e-4, 0.06 * e.volume * 1.5 * gainScale);
6367
6544
  if (tone.decay) {
@@ -6466,7 +6643,7 @@ var playPlacements = (placements, options) => {
6466
6643
  id: id++,
6467
6644
  startStep: p.startStep,
6468
6645
  durationSteps: p.durationSteps,
6469
- pitch: p.pitch,
6646
+ pitchUnits: p.pitchUnits,
6470
6647
  velocity: p.velocity
6471
6648
  }));
6472
6649
  return {
@@ -6637,7 +6814,7 @@ var playNote = (options) => {
6637
6814
  const dur = options.duration ?? 1;
6638
6815
  synth.playNote({
6639
6816
  trackId: "melody",
6640
- pitch: options.pitch,
6817
+ pitchUnits: options.pitchUnits,
6641
6818
  velocity: 100,
6642
6819
  volume: vol / 100,
6643
6820
  when: 0,
@@ -6658,7 +6835,7 @@ var playChords = (chordStr, options = {}) => {
6658
6835
  // 伴奏トラック
6659
6836
  startStep: p.startStep,
6660
6837
  durationSteps: p.durationSteps,
6661
- pitch: p.pitch,
6838
+ pitchUnits: p.pitchUnits,
6662
6839
  velocity: p.velocity
6663
6840
  }));
6664
6841
  return playPlacements(placements, {
@@ -8815,7 +8992,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
8815
8992
  0
8816
8993
  );
8817
8994
  const timedNotes = placements.map((p) => ({
8818
- pitch: p.pitch,
8995
+ pitch: unitsToPitchV1(p.pitchUnits),
8819
8996
  when: p.startStep * secondsPerStep,
8820
8997
  duration: p.durationSteps * secondsPerStep
8821
8998
  }));
@@ -8848,7 +9025,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
8848
9025
  id: id++,
8849
9026
  startStep: p.startStep,
8850
9027
  durationSteps: p.durationSteps,
8851
- pitch: p.pitch,
9028
+ pitchUnits: p.pitchUnits,
8852
9029
  velocity: DEFAULT_VELOCITY
8853
9030
  }));
8854
9031
  return {
@@ -9673,7 +9850,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9673
9850
  (a, b) => a.startStep - b.startStep
9674
9851
  );
9675
9852
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
9676
- const semis = (lt.octave ?? 0) * 12;
9853
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
9677
9854
  const count = Math.min(sorted.length, lt.syllables.length);
9678
9855
  const notes = [];
9679
9856
  for (let i2 = 0; i2 < count; i2++) {
@@ -9681,7 +9858,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9681
9858
  if (n.startStep < fromStep) continue;
9682
9859
  notes.push({
9683
9860
  syllable: lt.syllables[i2],
9684
- pitch: n.pitch + semis,
9861
+ pitch: units(n.pitchUnits + semis),
9685
9862
  startSec: (n.startStep - fromStep) * secondsPerStep,
9686
9863
  durationSec: n.durationSteps * secondsPerStep * gate
9687
9864
  });
@@ -9956,6 +10133,7 @@ var SoundFont = class _SoundFont {
9956
10133
  pitch = 60,
9957
10134
  volume = 1,
9958
10135
  velocity,
10136
+ detuneCents = 0,
9959
10137
  when = 0,
9960
10138
  duration = 1
9961
10139
  } = {}) {
@@ -9975,7 +10153,7 @@ var SoundFont = class _SoundFont {
9975
10153
  Object.assign(src, _param.src);
9976
10154
  const humanizeCents = (Math.random() * 2 - 1) * _SoundFont.humanizeDetuneCents;
9977
10155
  const humanizeGainMul = 1 + (Math.random() * 2 - 1) * _SoundFont.humanizeGainRatio;
9978
- src.detune.setValueAtTime(humanizeCents, 0);
10156
+ src.detune.setValueAtTime(humanizeCents + detuneCents, 0);
9979
10157
  const effectiveVolume = volume * humanizeGainMul;
9980
10158
  const brightness = velocity === void 0 ? 1 : Math.max(0, Math.min(1, velocity / 127));
9981
10159
  const filter = brightness >= _SoundFont.brightnessBypassAbove ? void 0 : ctx.createBiquadFilter();
@@ -11114,7 +11292,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11114
11292
  scheduleMetronomeBeats(ctx, cutGain);
11115
11293
  };
11116
11294
  const wafPlayDynamic = ({
11117
- pitch,
11295
+ pitchUnits,
11118
11296
  velocity,
11119
11297
  volume,
11120
11298
  when,
@@ -11124,10 +11302,12 @@ var mountChordPlayer = (target, chords, options = {}) => {
11124
11302
  }) => {
11125
11303
  const waf = activeGmName ? _wafCache.get(activeGmName) : null;
11126
11304
  if (waf) {
11305
+ const { midi, detuneCents } = unitsToMidiDetune(pitchUnits);
11127
11306
  waf.play({
11128
11307
  ctx,
11129
11308
  destination,
11130
- pitch,
11309
+ pitch: midi,
11310
+ detuneCents,
11131
11311
  volume: volume * 0.85,
11132
11312
  velocity,
11133
11313
  when,
@@ -11137,7 +11317,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11137
11317
  const synth = createSynth(ctx, destination);
11138
11318
  synth.playNote({
11139
11319
  trackId: "chord",
11140
- pitch,
11320
+ pitchUnits,
11141
11321
  velocity: 100,
11142
11322
  volume,
11143
11323
  when,
@@ -11166,7 +11346,8 @@ var mountChordPlayer = (target, chords, options = {}) => {
11166
11346
  trackIndex: 3,
11167
11347
  startStep: whenStep,
11168
11348
  durationSteps,
11169
- pitch: C32 + noteOffset,
11349
+ // chord-parser の構成音は12平均律の半音。内部表現の units へ写す。
11350
+ pitchUnits: pitchV1ToUnits(C32 + noteOffset),
11170
11351
  velocity: 100
11171
11352
  });
11172
11353
  }
@@ -11179,7 +11360,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11179
11360
  synth: false,
11180
11361
  onPlayNote: (e) => {
11181
11362
  wafPlayDynamic({
11182
- pitch: e.pitch,
11363
+ pitchUnits: e.pitchUnits,
11183
11364
  velocity: e.velocity,
11184
11365
  volume: e.volume,
11185
11366
  when: e.when,
@@ -11621,6 +11802,14 @@ var buildUI = (target, options) => {
11621
11802
  <summary>\u5168\u4F53\u30C8\u30E9\u30C3\u30AF\u8A2D\u5B9A</summary>
11622
11803
  <div class="dtm-panel-body">
11623
11804
  <div data-dtm="preset-select-slot"></div>
11805
+ <div class="dtm-row">
11806
+ <span class="dtm-label">\u97F3\u5F8B</span>
11807
+ <select class="dtm-select dtm-grow" data-dtm="edo-select">
11808
+ <option value="12">12\u5E73\u5747\u5F8B\uFF08\u901A\u5E38\uFF09</option>
11809
+ <option value="31">31\u5E73\u5747\u5F8B\uFF08\u5FAE\u5206\u97F3\uFF09</option>
11810
+ </select>
11811
+ <button class="dtm-infobtn" data-dtm="edo-info" title="\u97F3\u5F8B\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
11812
+ </div>
11624
11813
  <div class="dtm-row">
11625
11814
  <span class="dtm-label">\u5168\u4F53\u97F3\u91CF</span>
11626
11815
  <input type="range" class="dtm-range dtm-grow" data-dtm="master-volume" value="50" min="0" max="100">
@@ -11953,6 +12142,8 @@ var buildUI = (target, options) => {
11953
12142
  copyMiniBtn: sel("copy-mini"),
11954
12143
  overlay: sel("overlay"),
11955
12144
  mmlInfoBtn: sel("mml-info"),
12145
+ edoSelect: sel("edo-select"),
12146
+ edoInfoBtn: sel("edo-info"),
11956
12147
  modalOverlay: sel("modal-overlay"),
11957
12148
  modalTitle: sel("modal-title"),
11958
12149
  modalBody: sel("modal-body"),
@@ -12073,17 +12264,21 @@ var SCALES = [
12073
12264
  [0, 2, 4, 7, 9]
12074
12265
  // Pentatonic Major
12075
12266
  ];
12267
+ var MEANTONE_STEP_BY_SEMITONE2 = [0, 3, 5, 8, 10, 13, 16, 18, 21, 23, 26, 28];
12076
12268
  var generateRandomPattern = (core, options) => {
12077
- const { stepsPerBar, startStep, pitchRangeStart } = options;
12269
+ const { stepsPerBar, startStep, pitchRangeStart, edo = 12 } = options;
12078
12270
  const numBars = 8;
12079
12271
  const noteLength = 24;
12080
- const basePitch = pitchRangeStart + 60;
12272
+ const semitoneToStep = (semi) => edo === 31 ? MEANTONE_STEP_BY_SEMITONE2[semi] : semi;
12273
+ const upr = UNITS_PER_OCTAVE / edo;
12274
+ const basePitch = pitchRangeStart + 60 * UNITS_PER_SEMITONE;
12081
12275
  const scale = SCALES[Math.floor(Math.random() * SCALES.length)];
12082
12276
  const rootOffset = Math.floor(Math.random() * 12);
12083
12277
  const availablePitches = [];
12084
12278
  for (let i2 = 0; i2 < 12; i2++) {
12085
12279
  const noteInOctave = (i2 - rootOffset + 12) % 12;
12086
- if (scale.includes(noteInOctave)) availablePitches.push(basePitch + i2);
12280
+ if (scale.includes(noteInOctave))
12281
+ availablePitches.push(units(basePitch + semitoneToStep(i2) * upr));
12087
12282
  }
12088
12283
  core.beginBatch();
12089
12284
  for (let bar = 0; bar < numBars; bar++) {
@@ -12118,14 +12313,17 @@ var applyHarmonicFilter = (targetCore, chordCore, options) => {
12118
12313
  const isNewBar = halfBar % 2 === 0;
12119
12314
  const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12120
12315
  if (chordHere.length > 0) {
12121
- currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
12316
+ currentClasses = new Set(
12317
+ chordHere.map((n) => n.pitchUnits % UNITS_PER_OCTAVE)
12318
+ );
12122
12319
  } else if (isNewBar) {
12123
12320
  currentClasses = /* @__PURE__ */ new Set();
12124
12321
  }
12125
12322
  if (currentClasses.size === 0) continue;
12126
12323
  const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12127
12324
  for (const n of activeHere) {
12128
- if (!currentClasses.has(n.pitch % 12)) targetCore.deleteNoteById(n.id);
12325
+ if (!currentClasses.has(n.pitchUnits % UNITS_PER_OCTAVE))
12326
+ targetCore.deleteNoteById(n.id);
12129
12327
  }
12130
12328
  }
12131
12329
  targetCore.endBatch();
@@ -12147,13 +12345,17 @@ var applyMonophonic = (targetCore, chordCore, options) => {
12147
12345
  const isNewBar = halfBar % 2 === 0;
12148
12346
  const chordHere = chordCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12149
12347
  if (chordHere.length > 0) {
12150
- currentClasses = new Set(chordHere.map((n) => n.pitch % 12));
12348
+ currentClasses = new Set(
12349
+ chordHere.map((n) => n.pitchUnits % UNITS_PER_OCTAVE)
12350
+ );
12151
12351
  } else if (isNewBar) {
12152
12352
  currentClasses = /* @__PURE__ */ new Set();
12153
12353
  }
12154
12354
  if (currentClasses.size === 0) continue;
12155
12355
  const activeHere = targetCore.getNotes().filter((n) => n.startStep >= start && n.startStep < end);
12156
- const filtered = activeHere.filter((n) => currentClasses.has(n.pitch % 12));
12356
+ const filtered = activeHere.filter(
12357
+ (n) => currentClasses.has(n.pitchUnits % UNITS_PER_OCTAVE)
12358
+ );
12157
12359
  const filteredIds = new Set(filtered.map((n) => n.id));
12158
12360
  for (const n of activeHere) {
12159
12361
  if (!filteredIds.has(n.id)) targetCore.deleteNoteById(n.id);
@@ -12165,7 +12367,7 @@ var applyMonophonic = (targetCore, chordCore, options) => {
12165
12367
  }
12166
12368
  for (const notesAtTime of timeMap.values()) {
12167
12369
  if (notesAtTime.length > 1) {
12168
- notesAtTime.sort((a, b) => b.pitch - a.pitch);
12370
+ notesAtTime.sort((a, b) => b.pitchUnits - a.pitchUnits);
12169
12371
  const [, ...others] = notesAtTime;
12170
12372
  for (const on of others) targetCore.deleteNoteById(on.id);
12171
12373
  }
@@ -12181,18 +12383,23 @@ var shiftNotes = (cores, shiftSteps) => {
12181
12383
  for (const note of notes) {
12182
12384
  const newStart = note.startStep + shiftSteps;
12183
12385
  if (newStart < 0) core.deleteNoteById(note.id);
12184
- else core.moveNote(note.id, newStart, note.pitch);
12386
+ else core.moveNote(note.id, newStart, note.pitchUnits);
12185
12387
  }
12186
12388
  core.saveHistory();
12187
12389
  }
12188
12390
  };
12189
- var transposeNotes = (cores, semitones) => {
12190
- if (semitones === 0) return;
12391
+ var transposeNotes = (cores, steps) => {
12392
+ if (steps === 0) return;
12191
12393
  for (const core of cores) {
12192
12394
  const notes = [...core.getNotes()];
12193
12395
  for (const note of notes) {
12194
- const newPitch = Math.max(0, Math.min(127, note.pitch + semitones));
12195
- if (newPitch !== note.pitch) {
12396
+ const newPitch = units(
12397
+ Math.max(
12398
+ PITCH_RANGE_START,
12399
+ Math.min(PITCH_RANGE_END, note.pitchUnits + steps)
12400
+ )
12401
+ );
12402
+ if (newPitch !== note.pitchUnits) {
12196
12403
  core.moveNote(note.id, note.startStep, newPitch);
12197
12404
  }
12198
12405
  }
@@ -12517,11 +12724,31 @@ var exportMIDI = (options) => {
12517
12724
  const div = 480;
12518
12725
  const tickPerStep = div / STEPS_PER_BEAT3;
12519
12726
  const midiTracks = [];
12520
- tracks.forEach((track, ch) => {
12727
+ const NOTE_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15];
12728
+ const BEND_RANGE_SEMITONES = 2;
12729
+ const bendKey = (cents) => Math.round(cents * 100);
12730
+ const bendUse = /* @__PURE__ */ new Map();
12731
+ for (const track of tracks) {
12732
+ for (const n of track.notes) {
12733
+ const { detuneCents } = unitsToMidiDetune(n.pitchUnits);
12734
+ const k = bendKey(detuneCents);
12735
+ bendUse.set(k, (bendUse.get(k) ?? 0) + 1);
12736
+ }
12737
+ }
12738
+ const bendChannel = /* @__PURE__ */ new Map();
12739
+ const ordered = [...bendUse.entries()].sort((a, b) => b[1] - a[1]);
12740
+ for (const [k] of ordered) {
12741
+ if (bendChannel.size >= NOTE_CHANNELS.length) break;
12742
+ bendChannel.set(k, NOTE_CHANNELS[bendChannel.size]);
12743
+ }
12744
+ tracks.forEach((track) => {
12521
12745
  if (track.notes.length === 0) return;
12522
- const channel = ch < 9 ? ch : ch + 1 & 15;
12523
12746
  const events = [];
12524
12747
  for (const n of track.notes) {
12748
+ const { midi, detuneCents } = unitsToMidiDetune(n.pitchUnits);
12749
+ const k = bendKey(detuneCents);
12750
+ const channel = bendChannel.get(k) ?? bendChannel.get(0) ?? 0;
12751
+ const note = Math.max(0, Math.min(127, midi));
12525
12752
  const startTick = Math.round(n.startStep * tickPerStep);
12526
12753
  const endTick = Math.round(
12527
12754
  (n.startStep + (n.durationSteps || 1)) * tickPerStep
@@ -12529,12 +12756,25 @@ var exportMIDI = (options) => {
12529
12756
  const vel = Math.round(
12530
12757
  (n.velocity ?? DEFAULT_VELOCITY) * (track.volume ?? 100) / 100
12531
12758
  );
12532
- events.push({ t: startTick, m: [144 | channel, n.pitch, vel] });
12533
- events.push({ t: endTick, m: [144 | channel, n.pitch, 0] });
12759
+ events.push({ t: startTick, m: [144 | channel, note, vel] });
12760
+ events.push({ t: endTick, m: [144 | channel, note, 0] });
12534
12761
  }
12535
12762
  events.sort((a, b) => a.t - b.t);
12536
12763
  midiTracks.push(events);
12537
12764
  });
12765
+ const bendSetup = [];
12766
+ for (const [k, channel] of bendChannel) {
12767
+ const cents = k / 100;
12768
+ bendSetup.push({ t: 0, m: [176 | channel, 101, 0] });
12769
+ bendSetup.push({ t: 0, m: [176 | channel, 100, 0] });
12770
+ bendSetup.push({ t: 0, m: [176 | channel, 6, BEND_RANGE_SEMITONES] });
12771
+ bendSetup.push({ t: 0, m: [176 | channel, 38, 0] });
12772
+ bendSetup.push({ t: 0, m: [176 | channel, 101, 127] });
12773
+ bendSetup.push({ t: 0, m: [176 | channel, 100, 127] });
12774
+ const raw = 8192 + Math.round(cents / (BEND_RANGE_SEMITONES * 100) * 8192);
12775
+ const v = Math.max(0, Math.min(16383, raw));
12776
+ bendSetup.push({ t: 0, m: [224 | channel, v & 127, v >> 7 & 127] });
12777
+ }
12538
12778
  const maxStep = Math.max(
12539
12779
  ...tracks.filter((t) => t.notes.length > 0).map(
12540
12780
  (t) => Math.max(...t.notes.map((n) => n.startStep + n.durationSteps))
@@ -12571,6 +12811,7 @@ var exportMIDI = (options) => {
12571
12811
  headerChunks(arr, midiTracks.length + 1, div);
12572
12812
  trackChunks(arr, (a) => {
12573
12813
  a.push(0, 255, 81, 3, ...to3byte(Math.round(6e7 / bpm)));
12814
+ for (const ev of bendSetup) a.push(0, ...ev.m);
12574
12815
  });
12575
12816
  for (const events of midiTracks) {
12576
12817
  trackChunks(arr, (a) => {
@@ -12817,403 +13058,8 @@ var LinkedList = class {
12817
13058
  }
12818
13059
  };
12819
13060
 
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
13061
  // src/mml-core.ts
13216
- var PITCH_MAP2 = [
13062
+ var PITCH_MAP = [
13217
13063
  "c",
13218
13064
  "c+",
13219
13065
  "d",
@@ -13227,6 +13073,39 @@ var PITCH_MAP2 = [
13227
13073
  "a+",
13228
13074
  "b"
13229
13075
  ];
13076
+ var EDO31_NAMES = [
13077
+ "c",
13078
+ "c+",
13079
+ "c#",
13080
+ "d-",
13081
+ "d_",
13082
+ "d",
13083
+ "d+",
13084
+ "d#",
13085
+ "e-",
13086
+ "e_",
13087
+ "e",
13088
+ "f-",
13089
+ "e#",
13090
+ "f",
13091
+ "f+",
13092
+ "f#",
13093
+ "g-",
13094
+ "g_",
13095
+ "g",
13096
+ "g+",
13097
+ "g#",
13098
+ "a-",
13099
+ "a_",
13100
+ "a",
13101
+ "a+",
13102
+ "a#",
13103
+ "b-",
13104
+ "b_",
13105
+ "b",
13106
+ "b+",
13107
+ "b#"
13108
+ ];
13230
13109
  var MMLCore = class _MMLCore {
13231
13110
  notes = [];
13232
13111
  nextNoteId = 0;
@@ -13240,9 +13119,12 @@ var MMLCore = class _MMLCore {
13240
13119
  lastUndoTime = 0;
13241
13120
  static UNDO_DEBOUNCE_MS = 100;
13242
13121
  toolMode = "pen";
13243
- constructor(handlers, volume = 80) {
13122
+ /** グリッド寸法の供給元(描画器ではなく呼び出し側が持つ設定を読む) */
13123
+ getConfig;
13124
+ constructor(handlers, volume = 80, getConfig) {
13244
13125
  this.handlers = handlers;
13245
13126
  this.volume = volume;
13127
+ this.getConfig = getConfig;
13246
13128
  this.lastHistorySnapshot = JSON.stringify(this.notes);
13247
13129
  this.history.add([]);
13248
13130
  this.generateAndNotify();
@@ -13329,14 +13211,14 @@ var MMLCore = class _MMLCore {
13329
13211
  */
13330
13212
  addNote(step, pitch, options) {
13331
13213
  const existingIndex = this.notes.findIndex(
13332
- (n) => n.startStep === step && n.pitch === pitch
13214
+ (n) => n.startStep === step && n.pitchUnits === pitch
13333
13215
  );
13334
13216
  if (existingIndex === -1) {
13335
13217
  const newNote = {
13336
13218
  id: this.nextNoteId++,
13337
13219
  startStep: step,
13338
13220
  durationSteps: options.noteLengthSteps,
13339
- pitch,
13221
+ pitchUnits: pitch,
13340
13222
  velocity: options.velocity ?? DEFAULT_VELOCITY
13341
13223
  };
13342
13224
  this.notes.push(newNote);
@@ -13364,19 +13246,20 @@ var MMLCore = class _MMLCore {
13364
13246
  moveNote(noteId, startStep, pitch) {
13365
13247
  const note = this.notes.find((target) => target.id === noteId);
13366
13248
  if (!note) return;
13367
- const totalSteps = this.getMaxStep() + getRenderConfig().stepsPerBar;
13368
- const pitchRangeStart = getRenderConfig().pitchRangeStart;
13369
- const pitchRangeEnd = pitchRangeStart + getRenderConfig().keyCount - 1;
13370
- const clampedPitch = Math.min(
13371
- Math.max(pitch, pitchRangeStart),
13372
- pitchRangeEnd
13249
+ const totalSteps = this.getMaxStep() + this.getConfig().stepsPerBar;
13250
+ const cfg = this.getConfig();
13251
+ const upr = cfg.unitsPerRow ?? UNITS_PER_SEMITONE;
13252
+ const pitchRangeStart = cfg.pitchRangeStart;
13253
+ const pitchRangeEnd = pitchRangeStart + (cfg.keyCount - 1) * upr;
13254
+ const clampedPitch = units(
13255
+ Math.min(Math.max(pitch, pitchRangeStart), pitchRangeEnd)
13373
13256
  );
13374
13257
  const clampedStart = Math.min(
13375
13258
  Math.max(startStep, 0),
13376
13259
  totalSteps - note.durationSteps
13377
13260
  );
13378
13261
  note.startStep = clampedStart;
13379
- note.pitch = clampedPitch;
13262
+ note.pitchUnits = clampedPitch;
13380
13263
  this.notes.sort((a, b) => a.startStep - b.startStep);
13381
13264
  this.generateAndNotify();
13382
13265
  }
@@ -13421,7 +13304,7 @@ var MMLCore = class _MMLCore {
13421
13304
  * ただし、残りステップ(limit)は絶対に超えない。
13422
13305
  */
13423
13306
  stepsToMMLDuration(steps, limit) {
13424
- const config = getRenderConfig();
13307
+ const config = this.getConfig();
13425
13308
  const total = config.stepsPerBar;
13426
13309
  const candidates = [
13427
13310
  { dur: "1.", s: total * 1.5 },
@@ -13457,7 +13340,7 @@ var MMLCore = class _MMLCore {
13457
13340
  * ギャップに収まる最大の音符を探す(減算アルゴリズム用)
13458
13341
  */
13459
13342
  findBestFitDuration(gap) {
13460
- const config = getRenderConfig();
13343
+ const config = this.getConfig();
13461
13344
  const durations = [1, 2, 4, 8, 12, 16, 24, 32, 48, 64];
13462
13345
  for (const d of durations) {
13463
13346
  const stepLen = config.stepsPerBar / d;
@@ -13467,12 +13350,20 @@ var MMLCore = class _MMLCore {
13467
13350
  }
13468
13351
  return { dur: 64, steps: config.stepsPerBar / 64 };
13469
13352
  }
13353
+ /** ピッチ(units) → その音律での「オクターブ番号」と「音名」。 */
13354
+ spell(pitchUnits) {
13355
+ const edo = this.getConfig().edo ?? 12;
13356
+ const names = edo === 31 ? EDO31_NAMES : PITCH_MAP;
13357
+ const octave = Math.floor(pitchUnits / 372) - 1;
13358
+ const within = (pitchUnits % 372 + 372) % 372;
13359
+ const step = Math.round(within / (372 / edo));
13360
+ return { octave, name: names[step % edo] };
13361
+ }
13470
13362
  /**
13471
13363
  * ピッチからオクターブ最適化のある音名を取得
13472
13364
  */
13473
13365
  getNoteWithOctave(pitch, lastOctave) {
13474
- const octave = Math.floor(pitch / 12) - 1;
13475
- const name = PITCH_MAP2[pitch % 12];
13366
+ const { octave, name } = this.spell(pitch);
13476
13367
  if (lastOctave === -1 || Math.abs(octave - lastOctave) >= 2) {
13477
13368
  return { text: `o${octave}${name}`, currentOctave: octave };
13478
13369
  }
@@ -13495,7 +13386,7 @@ var MMLCore = class _MMLCore {
13495
13386
  * 各音符の長さを忠実に出力する(次の音符がなければ曲末まで伸ばせる)。
13496
13387
  */
13497
13388
  generateMML = (volumeOverride) => {
13498
- const config = getRenderConfig();
13389
+ const config = this.getConfig();
13499
13390
  const vol = volumeOverride ?? this.volume;
13500
13391
  const header = `t${this.tempo} v${vol}`;
13501
13392
  const segments = [];
@@ -13536,14 +13427,13 @@ var MMLCore = class _MMLCore {
13536
13427
  const actualStepGenerated = this.getStepFromDottedMML(durStr);
13537
13428
  if (notes.length > 1) {
13538
13429
  const noteStrs = notes.map((n) => {
13539
- const oct = Math.floor(n.pitch / 12) - 1;
13540
- const name = PITCH_MAP2[n.pitch % 12];
13430
+ const { octave: oct, name } = this.spell(n.pitchUnits);
13541
13431
  return `o${oct}${name}`;
13542
13432
  });
13543
13433
  segments.push(`[${noteStrs.join("")}]${durStr}`);
13544
13434
  } else {
13545
13435
  const { text, currentOctave } = this.getNoteWithOctave(
13546
- notes[0].pitch,
13436
+ notes[0].pitchUnits,
13547
13437
  lastOctave
13548
13438
  );
13549
13439
  segments.push(`${text}${durStr}`);
@@ -13574,7 +13464,7 @@ var MMLCore = class _MMLCore {
13574
13464
  * MMLの音長文字列("4", "4.", "12"など)をステップ数に変換する
13575
13465
  */
13576
13466
  getStepFromDottedMML(durStr) {
13577
- const config = getRenderConfig();
13467
+ const config = this.getConfig();
13578
13468
  const total = config.stepsPerBar;
13579
13469
  const isDotted = durStr.endsWith(".");
13580
13470
  const baseDur = parseInt(isDotted ? durStr.slice(0, -1) : durStr, 10);
@@ -13584,7 +13474,7 @@ var MMLCore = class _MMLCore {
13584
13474
  };
13585
13475
  var decomposeToMonophonic = (notes) => {
13586
13476
  const sorted = [...notes].sort(
13587
- (a, b) => a.startStep - b.startStep || a.pitch - b.pitch
13477
+ (a, b) => a.startStep - b.startStep || a.pitchUnits - b.pitchUnits
13588
13478
  );
13589
13479
  const tracks = [];
13590
13480
  const trackEnds = [];
@@ -13619,6 +13509,493 @@ var isChordHeavyTrack = (notes, threshold = 0.6) => {
13619
13509
  return chordNotes / notes.length >= threshold;
13620
13510
  };
13621
13511
 
13512
+ // src/renderer.ts
13513
+ var KEYBOARD_WIDTH = 60;
13514
+ var HEADER_HEIGHT = 20;
13515
+ var NATURAL_STEPS3 = {
13516
+ 12: [0, 2, 4, 5, 7, 9, 11],
13517
+ 31: [0, 5, 10, 13, 18, 23, 28]
13518
+ };
13519
+ var keyTier = (step, edo) => {
13520
+ const naturals = NATURAL_STEPS3[edo] ?? NATURAL_STEPS3[12];
13521
+ let best = edo;
13522
+ for (const n of naturals) {
13523
+ const d = Math.abs(step - n);
13524
+ best = Math.min(best, d, edo - d);
13525
+ }
13526
+ if (best === 0) return 0;
13527
+ return best >= (edo === 31 ? 2 : 1) ? 2 : 1;
13528
+ };
13529
+ var KEY_NAMES = [
13530
+ "C",
13531
+ "C#",
13532
+ "D",
13533
+ "D#",
13534
+ "E",
13535
+ "F",
13536
+ "F#",
13537
+ "G",
13538
+ "G#",
13539
+ "A",
13540
+ "A#",
13541
+ "B"
13542
+ ];
13543
+ var createRenderer = (mountTarget, width = 800, height = 450, config) => {
13544
+ let g_header_canvas;
13545
+ let g_key_canvas;
13546
+ let g_grid_canvas;
13547
+ let g_header_ctx;
13548
+ let g_key_ctx;
13549
+ let g_grid_ctx;
13550
+ let g_config = config;
13551
+ const getRenderConfig = () => g_config;
13552
+ let g_draw_offset_x = 0;
13553
+ let g_draw_offset_y = 0;
13554
+ let g_bg_active = false;
13555
+ const setBackgroundActive = (active) => {
13556
+ g_bg_active = active;
13557
+ };
13558
+ const getDrawOffset = () => ({
13559
+ x: g_draw_offset_x,
13560
+ y: g_draw_offset_y
13561
+ });
13562
+ const getGridCanvas = () => g_grid_canvas;
13563
+ const getGridContext = () => g_grid_ctx;
13564
+ const getHeaderCanvas = () => g_header_canvas;
13565
+ const setup = () => {
13566
+ g_config = config;
13567
+ const headerCanvas = document.createElement("canvas");
13568
+ g_header_canvas = headerCanvas;
13569
+ headerCanvas.width = width - KEYBOARD_WIDTH;
13570
+ headerCanvas.height = HEADER_HEIGHT;
13571
+ headerCanvas.style.position = "absolute";
13572
+ headerCanvas.style.left = `${KEYBOARD_WIDTH}px`;
13573
+ headerCanvas.style.top = "0px";
13574
+ const headerCtx = headerCanvas.getContext("2d");
13575
+ if (!headerCtx)
13576
+ throw new Error("Failed to get 2D rendering context for header.");
13577
+ g_header_ctx = headerCtx;
13578
+ const keyCanvas = document.createElement("canvas");
13579
+ g_key_canvas = keyCanvas;
13580
+ keyCanvas.width = KEYBOARD_WIDTH;
13581
+ keyCanvas.height = height - HEADER_HEIGHT;
13582
+ keyCanvas.style.position = "absolute";
13583
+ keyCanvas.style.left = "0px";
13584
+ keyCanvas.style.top = `${HEADER_HEIGHT}px`;
13585
+ const keyCtx = keyCanvas.getContext("2d");
13586
+ if (!keyCtx)
13587
+ throw new Error("Failed to get 2D rendering context for keyboard.");
13588
+ g_key_ctx = keyCtx;
13589
+ const gridCanvas = document.createElement("canvas");
13590
+ g_grid_canvas = gridCanvas;
13591
+ gridCanvas.width = width - KEYBOARD_WIDTH;
13592
+ gridCanvas.height = height - HEADER_HEIGHT;
13593
+ gridCanvas.style.position = "absolute";
13594
+ gridCanvas.style.left = `${KEYBOARD_WIDTH}px`;
13595
+ gridCanvas.style.top = `${HEADER_HEIGHT}px`;
13596
+ gridCanvas.style.touchAction = "none";
13597
+ gridCanvas.style.userSelect = "none";
13598
+ const gridCtx = gridCanvas.getContext("2d", { willReadFrequently: true });
13599
+ if (!gridCtx)
13600
+ throw new Error("Failed to get 2D rendering context for grid.");
13601
+ g_grid_ctx = gridCtx;
13602
+ mountTarget.innerHTML = "";
13603
+ mountTarget.style.position = "relative";
13604
+ mountTarget.style.width = `${width + KEYBOARD_WIDTH}px`;
13605
+ mountTarget.style.height = `${height}px`;
13606
+ mountTarget.append(headerCanvas, keyCanvas, gridCanvas);
13607
+ drawHeaderCorner();
13608
+ };
13609
+ const drawHeaderCorner = () => {
13610
+ const mountTarget2 = g_key_canvas.parentElement;
13611
+ if (!mountTarget2) return;
13612
+ let cornerDiv = mountTarget2.querySelector(
13613
+ "#header-corner"
13614
+ );
13615
+ if (!cornerDiv) {
13616
+ cornerDiv = document.createElement("div");
13617
+ cornerDiv.id = "header-corner";
13618
+ cornerDiv.style.position = "absolute";
13619
+ cornerDiv.style.left = "0px";
13620
+ cornerDiv.style.top = "0px";
13621
+ cornerDiv.style.width = `${KEYBOARD_WIDTH}px`;
13622
+ cornerDiv.style.height = `${HEADER_HEIGHT}px`;
13623
+ cornerDiv.style.backgroundColor = "#0a0f1f";
13624
+ cornerDiv.style.borderRight = "2px solid #29adff";
13625
+ cornerDiv.style.borderBottom = "2px solid #29adff";
13626
+ mountTarget2.insertBefore(cornerDiv, g_header_canvas);
13627
+ }
13628
+ };
13629
+ const drawKeyboard = () => {
13630
+ g_key_ctx.clearRect(0, 0, g_key_canvas.width, g_key_canvas.height);
13631
+ const {
13632
+ keyHeight,
13633
+ keyCount,
13634
+ pitchRangeStart,
13635
+ unitsPerRow: upr = UNITS_PER_SEMITONE,
13636
+ edo = 12
13637
+ } = g_config;
13638
+ const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
13639
+ const endY = g_draw_offset_y + g_key_canvas.height;
13640
+ const WHITE_KEY = "#ccc8b4";
13641
+ const BLACK_KEY = "#111111";
13642
+ const MICRO_KEY = "#4a4a4a";
13643
+ const BK_EDGE = "#383838";
13644
+ const WW_SEP = "#807a6a";
13645
+ const BK_RATIO = 0.62;
13646
+ const MICRO_RATIO = 0.45;
13647
+ for (let y = startY; y < endY; y += keyHeight) {
13648
+ const rowIndex = keyCount - 1 - y / keyHeight;
13649
+ const units2 = pitchRangeStart + rowIndex * upr;
13650
+ const step = Math.round(
13651
+ (units2 % UNITS_PER_OCTAVE + UNITS_PER_OCTAVE) % UNITS_PER_OCTAVE / upr % edo
13652
+ );
13653
+ const tier = keyTier(step, edo);
13654
+ const octave = Math.floor(units2 / UNITS_PER_OCTAVE) - 1;
13655
+ const isC4Range = octave === 4;
13656
+ const screenY = y - g_draw_offset_y;
13657
+ const bkW = Math.floor(
13658
+ KEYBOARD_WIDTH * (tier === 1 ? MICRO_RATIO : BK_RATIO)
13659
+ );
13660
+ if (tier !== 0) {
13661
+ g_key_ctx.fillStyle = isC4Range ? "#d8d4be" : WHITE_KEY;
13662
+ g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
13663
+ g_key_ctx.fillStyle = tier === 1 ? MICRO_KEY : isC4Range ? "#1a1408" : BLACK_KEY;
13664
+ g_key_ctx.fillRect(0, screenY, bkW, keyHeight);
13665
+ g_key_ctx.strokeStyle = BK_EDGE;
13666
+ g_key_ctx.lineWidth = 1;
13667
+ g_key_ctx.beginPath();
13668
+ g_key_ctx.moveTo(bkW, screenY);
13669
+ g_key_ctx.lineTo(bkW, screenY + keyHeight);
13670
+ g_key_ctx.stroke();
13671
+ } else {
13672
+ g_key_ctx.fillStyle = isC4Range ? "#dedad0" : WHITE_KEY;
13673
+ g_key_ctx.fillRect(0, screenY, KEYBOARD_WIDTH, keyHeight);
13674
+ if (edo === 12 && (step === 5 || step === 0)) {
13675
+ g_key_ctx.strokeStyle = WW_SEP;
13676
+ g_key_ctx.lineWidth = 1;
13677
+ g_key_ctx.beginPath();
13678
+ g_key_ctx.moveTo(0, screenY + keyHeight - 0.5);
13679
+ g_key_ctx.lineTo(KEYBOARD_WIDTH, screenY + keyHeight - 0.5);
13680
+ g_key_ctx.stroke();
13681
+ }
13682
+ }
13683
+ if (step === 0) {
13684
+ g_key_ctx.fillStyle = "#555040";
13685
+ g_key_ctx.font = "10px 'k8x12',monospace";
13686
+ g_key_ctx.textAlign = "right";
13687
+ g_key_ctx.textBaseline = "bottom";
13688
+ g_key_ctx.fillText(
13689
+ `${KEY_NAMES[0]}${octave}`,
13690
+ KEYBOARD_WIDTH - 4,
13691
+ screenY + keyHeight - 2
13692
+ );
13693
+ }
13694
+ }
13695
+ g_key_ctx.beginPath();
13696
+ g_key_ctx.strokeStyle = "#29adff";
13697
+ g_key_ctx.lineWidth = 2;
13698
+ g_key_ctx.moveTo(KEYBOARD_WIDTH, 0);
13699
+ g_key_ctx.lineTo(KEYBOARD_WIDTH, g_key_canvas.height);
13700
+ g_key_ctx.stroke();
13701
+ };
13702
+ const drawHeader = () => {
13703
+ g_header_ctx.clearRect(0, 0, g_header_canvas.width, g_header_canvas.height);
13704
+ const { stepWidth, stepsPerBar } = g_config;
13705
+ g_header_ctx.save();
13706
+ g_header_ctx.translate(-g_draw_offset_x, 0);
13707
+ g_header_ctx.fillStyle = g_bg_active ? "rgba(10,15,31,0.55)" : "#0a0f1f";
13708
+ g_header_ctx.fillRect(
13709
+ g_draw_offset_x,
13710
+ 0,
13711
+ g_header_canvas.width,
13712
+ HEADER_HEIGHT
13713
+ );
13714
+ g_header_ctx.strokeStyle = "#3d405b";
13715
+ g_header_ctx.lineWidth = 1;
13716
+ g_header_ctx.font = "11px 'k8x12',monospace";
13717
+ g_header_ctx.fillStyle = "#83769c";
13718
+ const startBar = Math.floor(g_draw_offset_x / (stepsPerBar * stepWidth));
13719
+ const endBar = Math.ceil(
13720
+ (g_draw_offset_x + g_header_canvas.width) / (stepsPerBar * stepWidth)
13721
+ );
13722
+ for (let bar = startBar; bar <= endBar + 1; bar++) {
13723
+ const x2 = bar * stepsPerBar * stepWidth;
13724
+ const screenX = x2;
13725
+ g_header_ctx.beginPath();
13726
+ g_header_ctx.moveTo(screenX, 0);
13727
+ g_header_ctx.lineTo(screenX, HEADER_HEIGHT);
13728
+ g_header_ctx.stroke();
13729
+ if (bar >= 0) {
13730
+ g_header_ctx.textAlign = "left";
13731
+ g_header_ctx.textBaseline = "middle";
13732
+ g_header_ctx.fillText(`${bar + 1}`, screenX + 5, HEADER_HEIGHT / 2);
13733
+ }
13734
+ }
13735
+ g_header_ctx.restore();
13736
+ };
13737
+ const drawGrid = (noteLengthSteps = 1) => {
13738
+ drawKeyboard();
13739
+ drawHeader();
13740
+ g_grid_ctx.clearRect(0, 0, g_grid_canvas.width, g_grid_canvas.height);
13741
+ const {
13742
+ keyHeight,
13743
+ keyCount,
13744
+ stepWidth,
13745
+ stepsPerBar,
13746
+ pitchRangeStart,
13747
+ unitsPerRow: upr = UNITS_PER_SEMITONE,
13748
+ edo = 12
13749
+ } = g_config;
13750
+ const startY = Math.floor(g_draw_offset_y / keyHeight) * keyHeight;
13751
+ const endY = g_draw_offset_y + g_grid_canvas.height;
13752
+ for (let y = startY; y < endY; y += keyHeight) {
13753
+ const rowIndex = keyCount - 1 - y / keyHeight;
13754
+ const units2 = pitchRangeStart + rowIndex * upr;
13755
+ const step = Math.round(
13756
+ (units2 % UNITS_PER_OCTAVE + UNITS_PER_OCTAVE) % UNITS_PER_OCTAVE / upr % edo
13757
+ );
13758
+ const tier = keyTier(step, edo);
13759
+ const isC = step === 0;
13760
+ const octave = Math.floor(units2 / UNITS_PER_OCTAVE) - 1;
13761
+ const isC4Range = octave === 4;
13762
+ const screenY = y - g_draw_offset_y;
13763
+ 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";
13764
+ g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13765
+ if (isC4Range) {
13766
+ g_grid_ctx.fillStyle = "rgba(41,173,255,0.05)";
13767
+ g_grid_ctx.fillRect(0, screenY, g_grid_canvas.width, keyHeight);
13768
+ }
13769
+ g_grid_ctx.beginPath();
13770
+ g_grid_ctx.strokeStyle = isC ? "#3d405b" : "#1a1d30";
13771
+ g_grid_ctx.lineWidth = 1;
13772
+ const lineY = screenY + keyHeight;
13773
+ g_grid_ctx.moveTo(0, lineY);
13774
+ g_grid_ctx.lineTo(g_grid_canvas.width, lineY);
13775
+ g_grid_ctx.stroke();
13776
+ }
13777
+ const gridStep = noteLengthSteps || 48;
13778
+ const startX = Math.floor(g_draw_offset_x / (stepWidth * gridStep)) * stepWidth * gridStep;
13779
+ const endX = g_draw_offset_x + g_grid_canvas.width;
13780
+ const lineStep = stepWidth * gridStep;
13781
+ for (let x2 = startX; x2 <= endX; x2 += lineStep) {
13782
+ const step = x2 / stepWidth;
13783
+ const isBarLine = step % stepsPerBar === 0;
13784
+ const isNoteLine = step % gridStep === 0;
13785
+ const screenX = x2 - g_draw_offset_x;
13786
+ g_grid_ctx.beginPath();
13787
+ g_grid_ctx.strokeStyle = isBarLine ? "#3d405b" : isNoteLine ? "#242840" : "#1a1d30";
13788
+ g_grid_ctx.lineWidth = isBarLine ? 2 : 1;
13789
+ g_grid_ctx.moveTo(screenX, 0);
13790
+ g_grid_ctx.lineTo(screenX, g_grid_canvas.height);
13791
+ g_grid_ctx.stroke();
13792
+ }
13793
+ };
13794
+ const drawNotes = (notes, color = [59, 130, 246, 1], isActive = true) => {
13795
+ const {
13796
+ keyHeight,
13797
+ stepWidth,
13798
+ keyCount,
13799
+ pitchRangeStart,
13800
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13801
+ } = g_config;
13802
+ const canvasWidth = g_grid_canvas.width;
13803
+ const canvasHeight = g_grid_canvas.height;
13804
+ const [r, g, b, a] = color;
13805
+ for (const note of notes) {
13806
+ const logicalX = note.startStep * stepWidth;
13807
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
13808
+ const logicalY = yIndex * keyHeight;
13809
+ const w = note.durationSteps * stepWidth;
13810
+ const h = keyHeight;
13811
+ const renderX = logicalX - g_draw_offset_x;
13812
+ const renderY = logicalY - g_draw_offset_y;
13813
+ if (renderX + w < 0 || renderX > canvasWidth) continue;
13814
+ if (renderY + h < 0 || renderY > canvasHeight) continue;
13815
+ if (isActive) {
13816
+ const velocityOpacity = note.velocity !== void 0 ? 0.6 + note.velocity / 127 * 0.4 : 1;
13817
+ const finalOpacity = a * velocityOpacity;
13818
+ g_grid_ctx.fillStyle = `rgba(${r},${g},${b},${finalOpacity})`;
13819
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13820
+ if (w >= 4 && h >= 4) {
13821
+ g_grid_ctx.fillStyle = "rgba(255,255,255,0.4)";
13822
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, 1);
13823
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, 1, h - 2);
13824
+ g_grid_ctx.fillStyle = "rgba(0,0,0,0.45)";
13825
+ g_grid_ctx.fillRect(renderX + 1, renderY + h - 2, w - 2, 1);
13826
+ g_grid_ctx.fillRect(renderX + w - 2, renderY + 1, 1, h - 2);
13827
+ }
13828
+ } else {
13829
+ const velocityOpacity = note.velocity !== void 0 ? 0.7 + note.velocity / 127 * 0.3 : 1;
13830
+ const finalOpacity = Math.min(0.25, a * 0.22) * velocityOpacity;
13831
+ const gray = 0.299 * r + 0.587 * g + 0.114 * b;
13832
+ const ghostR = Math.round(r * 0.6 + gray * 0.4);
13833
+ const ghostG = Math.round(g * 0.6 + gray * 0.4);
13834
+ const ghostB = Math.round(b * 0.6 + gray * 0.4);
13835
+ g_grid_ctx.fillStyle = `rgba(${ghostR},${ghostG},${ghostB},${finalOpacity})`;
13836
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13837
+ }
13838
+ }
13839
+ };
13840
+ const drawNoteLyrics = (notes, syllables) => {
13841
+ if (syllables.length === 0) return;
13842
+ const {
13843
+ keyHeight,
13844
+ stepWidth,
13845
+ keyCount,
13846
+ pitchRangeStart,
13847
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13848
+ } = g_config;
13849
+ if (keyHeight < 7) return;
13850
+ const fontSize = Math.min(12, Math.floor(keyHeight * 0.85));
13851
+ const sorted = [...notes].sort((a, b) => a.startStep - b.startStep);
13852
+ const count = Math.min(sorted.length, syllables.length);
13853
+ g_grid_ctx.save();
13854
+ g_grid_ctx.font = `${fontSize}px 'k8x12',sans-serif`;
13855
+ g_grid_ctx.textAlign = "left";
13856
+ g_grid_ctx.textBaseline = "middle";
13857
+ g_grid_ctx.lineWidth = 3;
13858
+ g_grid_ctx.lineJoin = "round";
13859
+ for (let i2 = 0; i2 < count; i2++) {
13860
+ const kana = syllables[i2];
13861
+ if (!kana) continue;
13862
+ const note = sorted[i2];
13863
+ const renderX = note.startStep * stepWidth - g_draw_offset_x;
13864
+ const renderY = (keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr) * keyHeight - g_draw_offset_y;
13865
+ const w = note.durationSteps * stepWidth;
13866
+ if (w < 6) continue;
13867
+ if (renderX + w < 0 || renderX > g_grid_canvas.width) continue;
13868
+ if (renderY + keyHeight < 0 || renderY > g_grid_canvas.height) continue;
13869
+ const textX = renderX + 2;
13870
+ const textY = renderY + keyHeight / 2;
13871
+ g_grid_ctx.save();
13872
+ g_grid_ctx.beginPath();
13873
+ g_grid_ctx.rect(renderX + 1, renderY + 1, w - 2, keyHeight - 2);
13874
+ g_grid_ctx.clip();
13875
+ g_grid_ctx.strokeStyle = "rgba(0,0,0,0.85)";
13876
+ g_grid_ctx.strokeText(kana, textX, textY);
13877
+ g_grid_ctx.fillStyle = "#fff1e8";
13878
+ g_grid_ctx.fillText(kana, textX, textY);
13879
+ g_grid_ctx.restore();
13880
+ }
13881
+ g_grid_ctx.restore();
13882
+ };
13883
+ const drawSelectionRect = (rect) => {
13884
+ if (!rect) return;
13885
+ g_grid_ctx.save();
13886
+ g_grid_ctx.strokeStyle = "#ffec27";
13887
+ g_grid_ctx.lineWidth = 2;
13888
+ g_grid_ctx.setLineDash([4, 4]);
13889
+ g_grid_ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
13890
+ g_grid_ctx.fillStyle = "rgba(255,236,39,0.08)";
13891
+ g_grid_ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
13892
+ g_grid_ctx.restore();
13893
+ };
13894
+ const drawSelectedNotes = (notes, selectedIds, baseColor = [59, 130, 246, 1]) => {
13895
+ const {
13896
+ keyHeight,
13897
+ stepWidth,
13898
+ keyCount,
13899
+ pitchRangeStart,
13900
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13901
+ } = g_config;
13902
+ for (const note of notes) {
13903
+ if (!selectedIds.has(note.id)) continue;
13904
+ const logicalX = note.startStep * stepWidth;
13905
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
13906
+ const logicalY = yIndex * keyHeight;
13907
+ const w = note.durationSteps * stepWidth;
13908
+ const h = keyHeight;
13909
+ const renderX = logicalX - g_draw_offset_x;
13910
+ const renderY = logicalY - g_draw_offset_y;
13911
+ const velocityOpacity = note.velocity !== void 0 ? 0.5 + note.velocity / 127 * 0.5 : 1;
13912
+ const [r, g, b, a] = baseColor;
13913
+ const darkenFactor = 1.3;
13914
+ const darkerR = Math.min(255, r * darkenFactor);
13915
+ const darkerG = Math.min(255, g * darkenFactor);
13916
+ const darkerB = Math.min(255, b * darkenFactor);
13917
+ const finalOpacity = a * velocityOpacity;
13918
+ g_grid_ctx.fillStyle = `rgba(${darkerR},${darkerG},${darkerB},${finalOpacity})`;
13919
+ g_grid_ctx.fillRect(renderX + 1, renderY + 1, w - 2, h - 2);
13920
+ }
13921
+ };
13922
+ const getXY = (e) => {
13923
+ const { clientX, clientY } = e;
13924
+ const rect = g_grid_canvas.getBoundingClientRect();
13925
+ const x2 = Math.floor(clientX - rect.left);
13926
+ const y = Math.floor(clientY - rect.top);
13927
+ return [x2, y, e.buttons];
13928
+ };
13929
+ const getGridPosition = (e) => {
13930
+ const [x2, y] = getXY(e);
13931
+ const {
13932
+ keyCount,
13933
+ pitchRangeStart,
13934
+ keyHeight,
13935
+ stepWidth,
13936
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13937
+ } = g_config;
13938
+ const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
13939
+ const absoluteY = y + g_draw_offset_y;
13940
+ const yIndex = Math.floor(absoluteY / keyHeight);
13941
+ const pitch = units(pitchRangeStart + (keyCount - 1 - yIndex) * upr);
13942
+ return { step, pitch, x: x2, y };
13943
+ };
13944
+ const onClick = (callback) => {
13945
+ g_grid_canvas.addEventListener(
13946
+ "click",
13947
+ (e) => {
13948
+ const [x2, y] = getXY(e);
13949
+ const {
13950
+ keyCount,
13951
+ pitchRangeStart,
13952
+ keyHeight,
13953
+ stepWidth,
13954
+ unitsPerRow: upr = UNITS_PER_SEMITONE
13955
+ } = g_config;
13956
+ const step = Math.floor((x2 + g_draw_offset_x) / stepWidth);
13957
+ const absoluteY = y + g_draw_offset_y;
13958
+ const yIndex = Math.floor(absoluteY / keyHeight);
13959
+ const pitch = units(pitchRangeStart + (keyCount - 1 - yIndex) * upr);
13960
+ if (pitch >= pitchRangeStart && pitch < pitchRangeStart + keyCount) {
13961
+ requestAnimationFrame(() => callback(step, pitch));
13962
+ }
13963
+ },
13964
+ { passive: true }
13965
+ );
13966
+ g_grid_canvas.addEventListener("contextmenu", (e) => e.preventDefault());
13967
+ };
13968
+ const setDrawOffset = (x2, y) => {
13969
+ g_draw_offset_x = x2;
13970
+ g_draw_offset_y = y;
13971
+ drawKeyboard();
13972
+ drawHeader();
13973
+ };
13974
+ setup();
13975
+ return {
13976
+ getRenderConfig,
13977
+ setBackgroundActive,
13978
+ getDrawOffset,
13979
+ getGridCanvas,
13980
+ getGridContext,
13981
+ getHeaderCanvas,
13982
+ drawKeyboard,
13983
+ drawHeader,
13984
+ drawGrid,
13985
+ drawNotes,
13986
+ drawNoteLyrics,
13987
+ drawSelectionRect,
13988
+ drawSelectedNotes,
13989
+ getXY,
13990
+ getGridPosition,
13991
+ onClick,
13992
+ setDrawOffset,
13993
+ destroy: () => {
13994
+ mountTarget.innerHTML = "";
13995
+ }
13996
+ };
13997
+ };
13998
+
13622
13999
  // src/daw.ts
13623
14000
  var CHORD_INFO_HTML2 = `
13624
14001
  <div class="dtm-modal-body-content">
@@ -13905,6 +14282,27 @@ var MIDI_INFO_HTML = `
13905
14282
  <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
14283
  </div>
13907
14284
  `;
14285
+ var EDO_INFO_HTML = `
14286
+ <div class="dtm-modal-section">
14287
+ <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>
14288
+ <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>
14289
+ <p style="margin-top:8px;"><strong>31\u5E73\u5747\u5F8B\u306E\u7279\u5FB4</strong></p>
14290
+ <ul>
14291
+ <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>
14292
+ <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>
14293
+ <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>
14294
+ </ul>
14295
+ <p style="margin-top:8px;"><strong>MML\u3067\u306E\u66F8\u304D\u65B9</strong></p>
14296
+ <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>
14297
+ <ul>
14298
+ <li><code>#</code> \u2026 \u534A\u97F3\u4E0A\u3052\uFF082\u5EA6\uFF09\u3000<code>-</code> \u2026 \u534A\u97F3\u4E0B\u3052\uFF082\u5EA6\uFF09</li>
14299
+ <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>
14300
+ </ul>
14301
+ <p>\u4F8B: <code>#edo=31 @0 o4 c c+ c# d- d_ d;</code></p>
14302
+ <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>
14303
+ <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>
14304
+ </div>
14305
+ `;
13908
14306
  var KOE_INFO_HTML = `
13909
14307
  <div class="dtm-modal-body-content">
13910
14308
  <h4>1. UTAU\u97F3\u6E90\u3092 .koe \u306B\u5909\u63DB\u3059\u308B</h4>
@@ -14122,7 +14520,7 @@ var AUTO_ROLE_BASE_VOLUME = {
14122
14520
  chord: 72
14123
14521
  };
14124
14522
  var computeAutoRoleStats = (notes) => {
14125
- const avgPitch = notes.reduce((s, n) => s + n.pitch, 0) / notes.length;
14523
+ const avgPitch = notes.reduce((s, n) => s + n.pitchUnits, 0) / notes.length;
14126
14524
  const avgDur = notes.reduce((s, n) => s + n.durationSteps, 0) / notes.length;
14127
14525
  const byStart = /* @__PURE__ */ new Map();
14128
14526
  for (const n of notes)
@@ -14136,8 +14534,9 @@ var computeAutoRoleStats = (notes) => {
14136
14534
  const notesPerBeat = notes.length / spanBeats;
14137
14535
  return { avgPitch, avgDur, avgPoly, maxPoly, notesPerBeat };
14138
14536
  };
14537
+ var AUTO_ROLE_BASS_UNITS = 48 * UNITS_PER_SEMITONE;
14139
14538
  var classifyTrackRole = (stats) => {
14140
- if (stats.avgPitch < 48) return "bass";
14539
+ if (stats.avgPitch < AUTO_ROLE_BASS_UNITS) return "bass";
14141
14540
  if (stats.maxPoly >= 2 && stats.avgDur >= AUTO_ROLE_STEPS_PER_BEAT)
14142
14541
  return "chord";
14143
14542
  if (stats.notesPerBeat < 1.2 && stats.avgDur >= AUTO_ROLE_STEPS_PER_BEAT * 1.5)
@@ -14149,7 +14548,10 @@ var computeAutoRoleVolume = (role, stats) => {
14149
14548
  if (role === "chord" && stats.avgPoly > 1) {
14150
14549
  vol /= Math.sqrt(stats.avgPoly);
14151
14550
  } else if (role === "bass") {
14152
- const depthSemitones = Math.max(0, 48 - stats.avgPitch);
14551
+ const depthSemitones = Math.max(
14552
+ 0,
14553
+ (AUTO_ROLE_BASS_UNITS - stats.avgPitch) / UNITS_PER_SEMITONE
14554
+ );
14153
14555
  vol += Math.min(12, depthSemitones * 0.5);
14154
14556
  }
14155
14557
  return clamp5(Math.round(vol), 1, 127);
@@ -14241,17 +14643,48 @@ var mountDAW = (target, options = {}) => {
14241
14643
  refs.drumVolumeLabel.textContent = `${options.drumVolume ?? 80}%`;
14242
14644
  const renderConfig = {
14243
14645
  stepsPerBar: 192,
14244
- keyCount: 128,
14245
- pitchRangeStart: 0,
14646
+ keyCount: KEY_COUNT,
14647
+ pitchRangeStart: PITCH_RANGE_START,
14648
+ unitsPerRow: unitsPerRow(12),
14649
+ edo: 12,
14246
14650
  keyHeight: BASE_KEY_HEIGHT,
14247
14651
  stepWidth: BASE_STEP_WIDTH * 2
14248
14652
  // zoom100% 相当
14249
14653
  };
14654
+ let renderer;
14250
14655
  let zoomX = 100;
14251
14656
  let zoomY = 100;
14252
14657
  let bpm = options.defaultBpm ?? DEFAULT_BPM;
14253
14658
  let masterVolume = options.masterVolume ?? 50;
14254
- options.singingVoices?.setVolume(masterVolume / 100);
14659
+ const applyMasterVolume = (volume) => {
14660
+ masterVolume = clamp5(Math.round(volume), 0, 100);
14661
+ refs.masterVolume.value = String(masterVolume);
14662
+ refs.masterVolumeLabel.textContent = `${masterVolume}%`;
14663
+ options.singingVoices?.setVolume(masterVolume / 100);
14664
+ };
14665
+ applyMasterVolume(masterVolume);
14666
+ const snapToEdoGrid = (u) => {
14667
+ const upr = renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE;
14668
+ return units(Math.round(u / upr) * upr);
14669
+ };
14670
+ const applyEdo = (edo, opts) => {
14671
+ const next = edo === 31 ? 31 : 12;
14672
+ if (renderConfig.edo === next) return;
14673
+ const upr = unitsPerRow(next);
14674
+ renderConfig.edo = next;
14675
+ renderConfig.unitsPerRow = upr;
14676
+ renderConfig.keyCount = keyCountFor(next);
14677
+ if (!opts?.snap) return;
14678
+ for (const t of trackStates) {
14679
+ t.core.beginBatch();
14680
+ for (const note of [...t.core.getNotes()]) {
14681
+ const snapped = units(Math.round(note.pitchUnits / upr) * upr);
14682
+ if (snapped !== note.pitchUnits)
14683
+ t.core.moveNote(note.id, note.startStep, snapped);
14684
+ }
14685
+ t.core.endBatch();
14686
+ }
14687
+ };
14255
14688
  let reverbAmount = options.reverbAmount ?? 0;
14256
14689
  let reverbDecay = options.reverbDecay ?? DEFAULT_REVERB_DECAY_SEC;
14257
14690
  let reverbPreDelay = options.reverbPreDelay ?? DEFAULT_REVERB_PREDELAY_MS;
@@ -14309,8 +14742,8 @@ var mountDAW = (target, options = {}) => {
14309
14742
  let snapGridSteps = 12;
14310
14743
  const gridLineSteps = 48;
14311
14744
  let currentOffsetX = 0;
14312
- const _initPitch = options.initialScrollPitch ?? 48;
14313
- let currentOffsetY = (renderConfig.keyCount - 1 - _initPitch) * renderConfig.keyHeight - 215;
14745
+ const _initPitch = pitchV1ToUnits(options.initialScrollPitch ?? 48);
14746
+ let currentOffsetY = (renderConfig.keyCount - 1 - (_initPitch - renderConfig.pitchRangeStart) / (renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE)) * renderConfig.keyHeight - 215;
14314
14747
  let playStartStep = 0;
14315
14748
  let isSolo = false;
14316
14749
  let lyricTrackIndices = /* @__PURE__ */ new Set();
@@ -14405,21 +14838,28 @@ var mountDAW = (target, options = {}) => {
14405
14838
  if (!ready) return;
14406
14839
  if (!suppressPatch && options.onNotesPatch) {
14407
14840
  const prevByKey = new Map(
14408
- prevNotes.map((n) => [`${n.startStep}_${n.pitch}`, n])
14841
+ prevNotes.map((n) => [`${n.startStep}_${n.pitchUnits}`, n])
14409
14842
  );
14410
14843
  const currByKey = new Map(
14411
- notes.map((n) => [`${n.startStep}_${n.pitch}`, n])
14844
+ notes.map((n) => [`${n.startStep}_${n.pitchUnits}`, n])
14412
14845
  );
14413
14846
  const added = notes.filter((n) => {
14414
- const prev = prevByKey.get(`${n.startStep}_${n.pitch}`);
14847
+ const prev = prevByKey.get(
14848
+ `${n.startStep}_${n.pitchUnits}`
14849
+ );
14415
14850
  return !prev || prev.durationSteps !== n.durationSteps || prev.velocity !== n.velocity;
14416
14851
  }).map((n) => ({
14417
14852
  startStep: n.startStep,
14418
- pitch: n.pitch,
14853
+ pitchUnits: n.pitchUnits,
14419
14854
  durationSteps: n.durationSteps,
14420
14855
  velocity: n.velocity
14421
14856
  }));
14422
- const removed = prevNotes.filter((n) => !currByKey.has(`${n.startStep}_${n.pitch}`)).map((n) => ({ startStep: n.startStep, pitch: n.pitch }));
14857
+ const removed = prevNotes.filter(
14858
+ (n) => !currByKey.has(`${n.startStep}_${n.pitchUnits}`)
14859
+ ).map((n) => ({
14860
+ startStep: n.startStep,
14861
+ pitchUnits: n.pitchUnits
14862
+ }));
14423
14863
  if (added.length > 0 || removed.length > 0) {
14424
14864
  options.onNotesPatch(config.id, added, removed);
14425
14865
  }
@@ -14429,7 +14869,8 @@ var mountDAW = (target, options = {}) => {
14429
14869
  updateUndoRedo();
14430
14870
  }
14431
14871
  },
14432
- config.volume
14872
+ config.volume,
14873
+ () => renderConfig
14433
14874
  ),
14434
14875
  volume: config.volume,
14435
14876
  savedChordInput: "",
@@ -14524,18 +14965,18 @@ var mountDAW = (target, options = {}) => {
14524
14965
  return (lastNoteMeasure + 2) * renderConfig.stepsPerBar;
14525
14966
  };
14526
14967
  const getMaxOffsetX = () => {
14527
- const canvas = getGridCanvas();
14968
+ const canvas = renderer.getGridCanvas();
14528
14969
  const maxNoteStep = getMaxNoteStep();
14529
14970
  const totalContentWidth = maxNoteStep * renderConfig.stepWidth;
14530
14971
  return Math.max(0, totalContentWidth - canvas.width);
14531
14972
  };
14532
14973
  const getMaxOffsetY = () => {
14533
14974
  const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
14534
- return Math.max(0, totalHeight - getGridCanvas().height);
14975
+ return Math.max(0, totalHeight - renderer.getGridCanvas().height);
14535
14976
  };
14536
14977
  const drawStartLine = () => {
14537
- const ctx = getGridContext();
14538
- const canvas = getGridCanvas();
14978
+ const ctx = renderer.getGridContext();
14979
+ const canvas = renderer.getGridCanvas();
14539
14980
  if (!ctx) return;
14540
14981
  const x2 = playStartStep * renderConfig.stepWidth - currentOffsetX;
14541
14982
  if (x2 < -10 || x2 > canvas.width + 10) return;
@@ -14550,8 +14991,8 @@ var mountDAW = (target, options = {}) => {
14550
14991
  ctx.restore();
14551
14992
  };
14552
14993
  const drawPlayhead = () => {
14553
- const ctx = getGridContext();
14554
- const canvas = getGridCanvas();
14994
+ const ctx = renderer.getGridContext();
14995
+ const canvas = renderer.getGridCanvas();
14555
14996
  if (!ctx) return;
14556
14997
  const x2 = currentPlayStep * renderConfig.stepWidth - currentOffsetX;
14557
14998
  if (x2 < 0 || x2 > canvas.width) return;
@@ -14565,7 +15006,7 @@ var mountDAW = (target, options = {}) => {
14565
15006
  ctx.restore();
14566
15007
  };
14567
15008
  const redrawAll = () => {
14568
- drawGrid(gridLineSteps);
15009
+ renderer.drawGrid(gridLineSteps);
14569
15010
  let lyricTargetNotes = null;
14570
15011
  let activeTrackState = null;
14571
15012
  for (const t of trackStates) {
@@ -14577,16 +15018,16 @@ var mountDAW = (target, options = {}) => {
14577
15018
  }
14578
15019
  const [r, g, b] = t.config.color;
14579
15020
  const notes = t.core.getNotes();
14580
- drawNotes(notes, [r, g, b, 1], false);
15021
+ renderer.drawNotes(notes, [r, g, b, 1], false);
14581
15022
  }
14582
15023
  if (activeTrackState) {
14583
15024
  const [r, g, b] = activeTrackState.config.color;
14584
15025
  const notes = activeTrackState.core.getNotes();
14585
- drawNotes(notes, [r, g, b, 1], true);
15026
+ renderer.drawNotes(notes, [r, g, b, 1], true);
14586
15027
  lyricTargetNotes = notes;
14587
15028
  }
14588
15029
  if (activeToolMode === "select" && selectionRect) {
14589
- const ctx = getGridContext();
15030
+ const ctx = renderer.getGridContext();
14590
15031
  ctx.save();
14591
15032
  ctx.strokeStyle = "#ffec27";
14592
15033
  ctx.lineWidth = 2;
@@ -14609,19 +15050,19 @@ var mountDAW = (target, options = {}) => {
14609
15050
  if (activeToolMode === "select" && selectedNotes.length > 0) {
14610
15051
  const ids = new Set(selectedNotes.map((n) => n.id));
14611
15052
  const active = getActive();
14612
- drawSelectedNotes(active.core.getNotes(), ids, [
15053
+ renderer.drawSelectedNotes(active.core.getNotes(), ids, [
14613
15054
  ...active.config.color,
14614
15055
  1
14615
15056
  ]);
14616
15057
  }
14617
15058
  if (lyricTargetNotes)
14618
- drawNoteLyrics(lyricTargetNotes, getActiveLyricKana());
15059
+ renderer.drawNoteLyrics(lyricTargetNotes, getActiveLyricKana());
14619
15060
  drawStartLine();
14620
15061
  if (playbackState === "playing") drawPlayhead();
14621
15062
  updateScrollbars();
14622
15063
  };
14623
15064
  const updateScrollbars = () => {
14624
- const canvas = getGridCanvas();
15065
+ const canvas = renderer.getGridCanvas();
14625
15066
  const maxOffsetX = getMaxOffsetX();
14626
15067
  const sbW = refs.hScroll.clientWidth;
14627
15068
  if (maxOffsetX <= 0) {
@@ -14705,7 +15146,7 @@ var mountDAW = (target, options = {}) => {
14705
15146
  const x2 = clamp5(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
14706
15147
  const ratio = x2 / (rect.width - thumbW);
14707
15148
  currentOffsetX = clamp5(ratio * maxOffsetX, 0, maxOffsetX);
14708
- setDrawOffset(currentOffsetX, currentOffsetY);
15149
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14709
15150
  redrawAll();
14710
15151
  };
14711
15152
  const moveV = (clientY) => {
@@ -14716,7 +15157,7 @@ var mountDAW = (target, options = {}) => {
14716
15157
  const y = clamp5(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
14717
15158
  const ratio = y / (rect.height - thumbH);
14718
15159
  currentOffsetY = clamp5(ratio * maxOffset, 0, maxOffset);
14719
- setDrawOffset(currentOffsetX, currentOffsetY);
15160
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14720
15161
  redrawAll();
14721
15162
  };
14722
15163
  };
@@ -14755,8 +15196,8 @@ var mountDAW = (target, options = {}) => {
14755
15196
  const autoScrollTick = () => {
14756
15197
  autoScrollRAF = null;
14757
15198
  if (!isSelecting || !lastMoveEvent) return;
14758
- const canvas = getGridCanvas();
14759
- const { x: x2, y } = getGridPosition(lastMoveEvent);
15199
+ const canvas = renderer.getGridCanvas();
15200
+ const { x: x2, y } = renderer.getGridPosition(lastMoveEvent);
14760
15201
  const dx = computeEdgeSpeed(x2, canvas.width);
14761
15202
  const dy = computeEdgeSpeed(y, canvas.height);
14762
15203
  if (dx !== 0 || dy !== 0) {
@@ -14764,7 +15205,7 @@ var mountDAW = (target, options = {}) => {
14764
15205
  const maxOffsetY = getMaxOffsetY();
14765
15206
  currentOffsetX = clamp5(currentOffsetX + dx, 0, maxOffsetX);
14766
15207
  currentOffsetY = clamp5(currentOffsetY + dy, 0, maxOffsetY);
14767
- setDrawOffset(currentOffsetX, currentOffsetY);
15208
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
14768
15209
  onPointerMove(lastMoveEvent);
14769
15210
  }
14770
15211
  if (isSelecting) {
@@ -14785,11 +15226,17 @@ var mountDAW = (target, options = {}) => {
14785
15226
  };
14786
15227
  const findActiveNoteAt = (x2, y, margin = 0) => {
14787
15228
  const active = getActive();
14788
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
14789
- const offset = getDrawOffset();
15229
+ const {
15230
+ stepWidth,
15231
+ keyHeight,
15232
+ keyCount,
15233
+ pitchRangeStart,
15234
+ unitsPerRow: upr = UNITS_PER_SEMITONE
15235
+ } = renderConfig;
15236
+ const offset = renderer.getDrawOffset();
14790
15237
  for (const note of active.core.getNotes()) {
14791
15238
  const logicalX = note.startStep * stepWidth;
14792
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
15239
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
14793
15240
  const logicalY = yIndex * keyHeight;
14794
15241
  const w = note.durationSteps * stepWidth;
14795
15242
  const renderX = logicalX - offset.x;
@@ -14802,7 +15249,7 @@ var mountDAW = (target, options = {}) => {
14802
15249
  const hasNoteAt = (step, pitch, excludeId) => {
14803
15250
  const active = getActive();
14804
15251
  return active.core.getNotes().some(
14805
- (n) => n.id !== excludeId && n.pitch === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
15252
+ (n) => n.id !== excludeId && n.pitchUnits === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
14806
15253
  );
14807
15254
  };
14808
15255
  const snapToGrid = (duration) => Math.max(
@@ -14813,7 +15260,7 @@ var mountDAW = (target, options = {}) => {
14813
15260
  const onGridPointerDown = (event) => {
14814
15261
  event.preventDefault();
14815
15262
  options.onResumeAudio?.();
14816
- const { x: x2, y, step, pitch } = getGridPosition(event);
15263
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(event);
14817
15264
  const active = getActive();
14818
15265
  if (activeToolMode === "eraser") {
14819
15266
  if (isActiveLocked()) return;
@@ -14828,7 +15275,7 @@ var mountDAW = (target, options = {}) => {
14828
15275
  selectedOriginal = selectedNotes.map((n) => ({
14829
15276
  id: n.id,
14830
15277
  startStep: n.startStep,
14831
- pitch: n.pitch
15278
+ pitch: n.pitchUnits
14832
15279
  }));
14833
15280
  isSelecting = true;
14834
15281
  dragMode = "move";
@@ -14847,7 +15294,7 @@ var mountDAW = (target, options = {}) => {
14847
15294
  {
14848
15295
  id: clicked.id,
14849
15296
  startStep: clicked.startStep,
14850
- pitch: clicked.pitch
15297
+ pitch: clicked.pitchUnits
14851
15298
  }
14852
15299
  ];
14853
15300
  isSelecting = true;
@@ -14865,9 +15312,9 @@ var mountDAW = (target, options = {}) => {
14865
15312
  hasDragged = false;
14866
15313
  const existing = findActiveNoteAt(x2, y, TOUCH_HIT_MARGIN);
14867
15314
  if (existing) {
14868
- playPreview(existing.pitch);
15315
+ playPreview(existing.pitchUnits);
14869
15316
  const { stepWidth } = renderConfig;
14870
- const offset = getDrawOffset();
15317
+ const offset = renderer.getDrawOffset();
14871
15318
  const renderX = existing.startStep * stepWidth - offset.x;
14872
15319
  const w = existing.durationSteps * stepWidth;
14873
15320
  if (x2 >= renderX + w - resizeHandleWidth && x2 <= renderX + w) {
@@ -14878,17 +15325,17 @@ var mountDAW = (target, options = {}) => {
14878
15325
  dragOffsetPitch: 0,
14879
15326
  startStep: existing.startStep,
14880
15327
  durationSteps: existing.durationSteps,
14881
- lastPreviewPitch: existing.pitch
15328
+ lastPreviewPitch: existing.pitchUnits
14882
15329
  };
14883
15330
  } else {
14884
15331
  dragState = {
14885
15332
  noteId: existing.id,
14886
15333
  mode: "move",
14887
15334
  dragOffsetStep: step - existing.startStep,
14888
- dragOffsetPitch: pitch - existing.pitch,
15335
+ dragOffsetPitch: pitch - existing.pitchUnits,
14889
15336
  startStep: existing.startStep,
14890
15337
  durationSteps: existing.durationSteps,
14891
- lastPreviewPitch: existing.pitch
15338
+ lastPreviewPitch: existing.pitchUnits
14892
15339
  };
14893
15340
  }
14894
15341
  suppressClick = true;
@@ -14899,14 +15346,14 @@ var mountDAW = (target, options = {}) => {
14899
15346
  const newStart = snappedStep;
14900
15347
  const newEnd = newStart + currentInsertLength;
14901
15348
  const overlapping = active.core.getNotes().some(
14902
- (n) => n.pitch === pitch && newStart < n.startStep + n.durationSteps && newEnd > n.startStep
15349
+ (n) => n.pitchUnits === pitch && newStart < n.startStep + n.durationSteps && newEnd > n.startStep
14903
15350
  );
14904
15351
  if (!overlapping) {
14905
15352
  active.core.addNote(snappedStep, pitch, {
14906
15353
  noteLengthSteps: currentInsertLength
14907
15354
  });
14908
15355
  playPreview(pitch);
14909
- const newNote = active.core.getNotes().find((n) => n.startStep === snappedStep && n.pitch === pitch);
15356
+ const newNote = active.core.getNotes().find((n) => n.startStep === snappedStep && n.pitchUnits === pitch);
14910
15357
  if (newNote) {
14911
15358
  dragState = {
14912
15359
  noteId: newNote.id,
@@ -14915,7 +15362,7 @@ var mountDAW = (target, options = {}) => {
14915
15362
  dragOffsetPitch: 0,
14916
15363
  startStep: newNote.startStep,
14917
15364
  durationSteps: newNote.durationSteps,
14918
- lastPreviewPitch: newNote.pitch
15365
+ lastPreviewPitch: newNote.pitchUnits
14919
15366
  };
14920
15367
  hasDragged = true;
14921
15368
  }
@@ -14926,12 +15373,12 @@ var mountDAW = (target, options = {}) => {
14926
15373
  const active = getActive();
14927
15374
  if (activeToolMode === "pen") {
14928
15375
  if (!dragState) return;
14929
- const { step, pitch } = getGridPosition(event);
15376
+ const { step, pitch } = renderer.getGridPosition(event);
14930
15377
  hasDragged = true;
14931
15378
  if (dragState.mode === "move") {
14932
15379
  const nextStart = step - dragState.dragOffsetStep;
14933
15380
  const snappedStart = Math.round(nextStart / snapGridSteps) * snapGridSteps;
14934
- const nextPitch = pitch - dragState.dragOffsetPitch;
15381
+ const nextPitch = units(pitch - dragState.dragOffsetPitch);
14935
15382
  if (hasNoteAt(snappedStart, nextPitch, dragState.noteId)) return;
14936
15383
  active.core.moveNote(dragState.noteId, snappedStart, nextPitch);
14937
15384
  if (nextPitch !== dragState.lastPreviewPitch) {
@@ -14950,7 +15397,7 @@ var mountDAW = (target, options = {}) => {
14950
15397
  }
14951
15398
  if (activeToolMode === "select" && isSelecting && selectionStart) {
14952
15399
  ensureAutoScroll(event);
14953
- const { x: x2, y, step, pitch } = getGridPosition(event);
15400
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(event);
14954
15401
  if (dragMode === "rect") {
14955
15402
  const rect = {
14956
15403
  x: Math.min(x2, selectionStart.x),
@@ -14959,11 +15406,17 @@ var mountDAW = (target, options = {}) => {
14959
15406
  height: Math.abs(y - selectionStart.y)
14960
15407
  };
14961
15408
  selectionRect = rect;
14962
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = renderConfig;
14963
- const offset = getDrawOffset();
15409
+ const {
15410
+ stepWidth,
15411
+ keyHeight,
15412
+ keyCount,
15413
+ pitchRangeStart,
15414
+ unitsPerRow: upr = UNITS_PER_SEMITONE
15415
+ } = renderConfig;
15416
+ const offset = renderer.getDrawOffset();
14964
15417
  selectedNotes = active.core.getNotes().filter((note) => {
14965
15418
  const logicalX = note.startStep * stepWidth;
14966
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
15419
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
14967
15420
  const logicalY = yIndex * keyHeight;
14968
15421
  const nx = logicalX - offset.x;
14969
15422
  const ny = logicalY - offset.y;
@@ -14981,8 +15434,8 @@ var mountDAW = (target, options = {}) => {
14981
15434
  for (const note of selectedNotes) {
14982
15435
  const orig = selectedOriginal.find((o) => o.id === note.id);
14983
15436
  if (!orig) continue;
14984
- const newPitch = orig.pitch + deltaPitch;
14985
- if (newPitch >= 0 && newPitch < 128)
15437
+ const newPitch = units(orig.pitch + deltaPitch);
15438
+ if (newPitch >= PITCH_RANGE_START && newPitch <= PITCH_RANGE_END)
14986
15439
  active.core.moveNote(
14987
15440
  note.id,
14988
15441
  orig.startStep + snappedDelta,
@@ -14993,7 +15446,7 @@ var mountDAW = (target, options = {}) => {
14993
15446
  const grab = selectedNotes[0];
14994
15447
  const orig = selectedOriginal.find((o) => o.id === grab.id);
14995
15448
  if (orig) {
14996
- const newGrab = orig.pitch + deltaPitch;
15449
+ const newGrab = units(orig.pitch + deltaPitch);
14997
15450
  if (newGrab !== lastMultiPreviewPitch && newGrab >= 0 && newGrab < 128) {
14998
15451
  lastMultiPreviewPitch = newGrab;
14999
15452
  playPreview(newGrab);
@@ -15130,22 +15583,23 @@ var mountDAW = (target, options = {}) => {
15130
15583
  refs.bgOpacityRow.classList.add("dtm-hidden");
15131
15584
  }
15132
15585
  refs.bgRemoveBtn.classList.toggle("dtm-hidden", !blob2);
15133
- setBackgroundActive(!!blob2);
15586
+ renderer.setBackgroundActive(!!blob2);
15134
15587
  redrawAll();
15135
15588
  };
15136
15589
  const setupCanvas = () => {
15137
15590
  const w = refs.rollContainer.clientWidth || 800;
15138
15591
  const h = refs.rollContainer.clientHeight || 450;
15139
- init(refs.wrapper, w, h, renderConfig);
15140
- const gridCanvas = getGridCanvas();
15592
+ renderer?.destroy();
15593
+ renderer = createRenderer(refs.wrapper, w, h, renderConfig);
15594
+ const gridCanvas = renderer.getGridCanvas();
15141
15595
  gridCanvas.addEventListener("pointerdown", onGridPointerDown);
15142
15596
  gridCanvas.addEventListener("dblclick", (event) => {
15143
15597
  event.preventDefault();
15144
15598
  if (isActiveLocked()) return;
15145
- const { step, pitch } = getGridPosition(event);
15599
+ const { step, pitch } = renderer.getGridPosition(event);
15146
15600
  const active = getActive();
15147
15601
  const note = active.core.getNotes().find(
15148
- (n) => n.pitch === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
15602
+ (n) => n.pitchUnits === pitch && step >= n.startStep && step < n.startStep + n.durationSteps
15149
15603
  );
15150
15604
  if (note) active.core.deleteNoteById(note.id);
15151
15605
  });
@@ -15163,7 +15617,7 @@ var mountDAW = (target, options = {}) => {
15163
15617
  0,
15164
15618
  getMaxOffsetX()
15165
15619
  );
15166
- setDrawOffset(currentOffsetX, currentOffsetY);
15620
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15167
15621
  redrawAll();
15168
15622
  },
15169
15623
  { passive: false }
@@ -15173,7 +15627,7 @@ var mountDAW = (target, options = {}) => {
15173
15627
  suppressClick = false;
15174
15628
  }
15175
15629
  });
15176
- const headerCanvas = getHeaderCanvas();
15630
+ const headerCanvas = renderer.getHeaderCanvas();
15177
15631
  headerCanvas.addEventListener("click", (event) => {
15178
15632
  if (playbackState === "playing") return;
15179
15633
  const rect = headerCanvas.getBoundingClientRect();
@@ -15189,11 +15643,11 @@ var mountDAW = (target, options = {}) => {
15189
15643
  }
15190
15644
  redrawAll();
15191
15645
  });
15192
- setDrawOffset(currentOffsetX, currentOffsetY);
15646
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15193
15647
  redrawAll();
15194
15648
  };
15195
15649
  const applyZoomX = () => {
15196
- const canvas = getGridCanvas();
15650
+ const canvas = renderer.getGridCanvas();
15197
15651
  const centerStep = (currentOffsetX + canvas.width / 2) / renderConfig.stepWidth;
15198
15652
  renderConfig.stepWidth = BASE_STEP_WIDTH * (zoomX * 2) / 100;
15199
15653
  refs.zoomXLabel.textContent = `${zoomX}%`;
@@ -15202,11 +15656,11 @@ var mountDAW = (target, options = {}) => {
15202
15656
  0,
15203
15657
  getMaxOffsetX()
15204
15658
  );
15205
- setDrawOffset(currentOffsetX, currentOffsetY);
15659
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15206
15660
  redrawAll();
15207
15661
  };
15208
15662
  const applyZoomY = () => {
15209
- const canvas = getGridCanvas();
15663
+ const canvas = renderer.getGridCanvas();
15210
15664
  const centerKey = (currentOffsetY + canvas.height / 2) / renderConfig.keyHeight;
15211
15665
  renderConfig.keyHeight = BASE_KEY_HEIGHT * zoomY / 100;
15212
15666
  refs.zoomYLabel.textContent = `${zoomY}%`;
@@ -15215,7 +15669,7 @@ var mountDAW = (target, options = {}) => {
15215
15669
  0,
15216
15670
  getMaxOffsetY()
15217
15671
  );
15218
- setDrawOffset(currentOffsetX, currentOffsetY);
15672
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15219
15673
  redrawAll();
15220
15674
  };
15221
15675
  const getViewState = () => ({
@@ -15227,7 +15681,14 @@ var mountDAW = (target, options = {}) => {
15227
15681
  const notifyViewState = () => options.onViewStateChange?.(getViewState());
15228
15682
  const dispatchNote = (trackId, pitch, trackVol, velocity, when, duration) => {
15229
15683
  const volume = trackVol / 100 * (velocity / 127) * (masterVolume / 100);
15230
- options.onPlayNote?.({ trackId, pitch, velocity, volume, when, duration });
15684
+ options.onPlayNote?.({
15685
+ trackId,
15686
+ pitchUnits: pitch,
15687
+ velocity,
15688
+ volume,
15689
+ when,
15690
+ duration
15691
+ });
15231
15692
  };
15232
15693
  const sequencer = createSequencer({
15233
15694
  getTracks: () => trackStates.map((t) => ({
@@ -15255,7 +15716,7 @@ var mountDAW = (target, options = {}) => {
15255
15716
  },
15256
15717
  onTick: (step) => {
15257
15718
  currentPlayStep = step;
15258
- const canvas = getGridCanvas();
15719
+ const canvas = renderer.getGridCanvas();
15259
15720
  const visibleSteps = canvas.width / renderConfig.stepWidth;
15260
15721
  const threshold = currentOffsetX / renderConfig.stepWidth + visibleSteps - 4;
15261
15722
  if (currentPlayStep > threshold) {
@@ -15265,7 +15726,7 @@ var mountDAW = (target, options = {}) => {
15265
15726
  0,
15266
15727
  getMaxOffsetX()
15267
15728
  );
15268
- setDrawOffset(currentOffsetX, currentOffsetY);
15729
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15269
15730
  }
15270
15731
  redrawAll();
15271
15732
  },
@@ -15296,7 +15757,7 @@ var mountDAW = (target, options = {}) => {
15296
15757
  (a, b) => a.startStep - b.startStep
15297
15758
  );
15298
15759
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
15299
- const semis = (lt.octave ?? 0) * 12;
15760
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
15300
15761
  const count = Math.min(sorted.length, lt.syllables.length);
15301
15762
  const notes = [];
15302
15763
  for (let i2 = 0; i2 < count; i2++) {
@@ -15304,7 +15765,7 @@ var mountDAW = (target, options = {}) => {
15304
15765
  if (n.startStep < fromStep) continue;
15305
15766
  notes.push({
15306
15767
  syllable: lt.syllables[i2],
15307
- pitch: n.pitch + semis,
15768
+ pitch: units(n.pitchUnits + semis),
15308
15769
  startSec: (n.startStep - fromStep) * secondsPerStep,
15309
15770
  durationSec: n.durationSteps * secondsPerStep * gate
15310
15771
  });
@@ -15347,13 +15808,13 @@ var mountDAW = (target, options = {}) => {
15347
15808
  }
15348
15809
  }
15349
15810
  if (playbackState !== "paused") {
15350
- const canvas = getGridCanvas();
15811
+ const canvas = renderer.getGridCanvas();
15351
15812
  currentOffsetX = clamp5(
15352
15813
  playStartStep * renderConfig.stepWidth - canvas.width * 0.5,
15353
15814
  0,
15354
15815
  getMaxOffsetX()
15355
15816
  );
15356
- setDrawOffset(currentOffsetX, currentOffsetY);
15817
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
15357
15818
  }
15358
15819
  playbackState = "playing";
15359
15820
  sequencer.start(fromStep);
@@ -16340,6 +16801,7 @@ var mountDAW = (target, options = {}) => {
16340
16801
  fadeIn: Math.round(fadeInSec * 10),
16341
16802
  fadeOut: Math.round(fadeOutSec * 10),
16342
16803
  mode,
16804
+ edo: renderConfig.edo,
16343
16805
  trackInstruments: trackInstMeta,
16344
16806
  trackCompression: trackCompMeta,
16345
16807
  trackWidth: trackWidthMeta,
@@ -16368,6 +16830,7 @@ var mountDAW = (target, options = {}) => {
16368
16830
  fadeIn: Math.round(fadeInSec * 10),
16369
16831
  fadeOut: Math.round(fadeOutSec * 10),
16370
16832
  mode,
16833
+ edo: renderConfig.edo,
16371
16834
  trackInstruments: trackInstMeta,
16372
16835
  trackCompression: trackCompMeta,
16373
16836
  trackWidth: trackWidthMeta,
@@ -16507,19 +16970,19 @@ var mountDAW = (target, options = {}) => {
16507
16970
  }
16508
16971
  }
16509
16972
  if (candidateNotes.length === 0) return null;
16510
- const sum = candidateNotes.reduce((acc, note) => acc + note.pitch, 0);
16511
- return Math.round(sum / candidateNotes.length);
16973
+ const sum = candidateNotes.reduce((acc, note) => acc + note.pitchUnits, 0);
16974
+ return units(Math.round(sum / candidateNotes.length));
16512
16975
  };
16513
16976
  const centerPitch = (pitch) => {
16514
- const canvas = getGridCanvas();
16515
- const yIndex = renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart);
16977
+ const canvas = renderer.getGridCanvas();
16978
+ const yIndex = renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart) / (renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE);
16516
16979
  const logicalY = yIndex * renderConfig.keyHeight;
16517
16980
  currentOffsetY = clamp5(
16518
16981
  logicalY - (canvas.height - renderConfig.keyHeight) / 2,
16519
16982
  0,
16520
16983
  getMaxOffsetY()
16521
16984
  );
16522
- setDrawOffset(currentOffsetX, currentOffsetY);
16985
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
16523
16986
  };
16524
16987
  const clearAll = () => {
16525
16988
  for (const t of trackStates) {
@@ -16577,10 +17040,10 @@ var mountDAW = (target, options = {}) => {
16577
17040
  if (!meta.drumFont) applyDrumPatternFont(meta.drum);
16578
17041
  }
16579
17042
  if (meta.volume !== void 0) {
16580
- masterVolume = meta.volume;
16581
- refs.masterVolume.value = String(meta.volume);
16582
- refs.masterVolumeLabel.textContent = `${meta.volume}%`;
17043
+ applyMasterVolume(meta.volume);
16583
17044
  }
17045
+ applyEdo(meta.edo ?? 12);
17046
+ refs.edoSelect.value = String(renderConfig.edo ?? 12);
16584
17047
  if (meta.drumVolume !== void 0) {
16585
17048
  drumVolume = meta.drumVolume;
16586
17049
  refs.drumVolume.value = String(meta.drumVolume);
@@ -16745,7 +17208,7 @@ var mountDAW = (target, options = {}) => {
16745
17208
  if (applyActiveOnly && p.trackIndex !== activeTrackIndex) continue;
16746
17209
  const t = trackStates[p.trackIndex];
16747
17210
  if (!t) continue;
16748
- t.core.addNote(p.startStep, p.pitch, {
17211
+ t.core.addNote(p.startStep, p.pitchUnits, {
16749
17212
  noteLengthSteps: p.durationSteps,
16750
17213
  velocity: DEFAULT_VELOCITY
16751
17214
  });
@@ -16762,7 +17225,7 @@ var mountDAW = (target, options = {}) => {
16762
17225
  if (firstPitch !== null) {
16763
17226
  centerPitch(firstPitch);
16764
17227
  } else {
16765
- centerPitch(48);
17228
+ centerPitch(pitchV1ToUnits(48));
16766
17229
  }
16767
17230
  redrawAll();
16768
17231
  updateTrackPanel();
@@ -16780,6 +17243,8 @@ var mountDAW = (target, options = {}) => {
16780
17243
  const chordTrack = trackStates.find((t) => t.config.id === "chord");
16781
17244
  if (!chordTrack) return;
16782
17245
  const placements = buildChordPlacements({
17246
+ // 和音は五度連鎖経由で音律の格子へ写す(31平均律では長3度が10度になる)
17247
+ edo: renderConfig.edo,
16783
17248
  chordStr: active.savedChordInput,
16784
17249
  patternType: active.savedChordPattern,
16785
17250
  rootShift: active.savedChordRoot,
@@ -16789,7 +17254,7 @@ var mountDAW = (target, options = {}) => {
16789
17254
  chordTrack.core.clearNotesWithoutHistory();
16790
17255
  chordTrack.core.beginBatch();
16791
17256
  for (const p of placements) {
16792
- chordTrack.core.addNote(p.startStep, p.pitch, {
17257
+ chordTrack.core.addNote(p.startStep, p.pitchUnits, {
16793
17258
  noteLengthSteps: Math.max(1, p.durationSteps),
16794
17259
  velocity: p.velocity
16795
17260
  });
@@ -16853,7 +17318,7 @@ var mountDAW = (target, options = {}) => {
16853
17318
  if (applyActiveOnly && p.trackId !== activeTrackId) continue;
16854
17319
  const t = trackStates.find((ts) => ts.config.id === p.trackId);
16855
17320
  if (!t) continue;
16856
- t.core.addNote(p.startStep, p.pitch, {
17321
+ t.core.addNote(p.startStep, snapToEdoGrid(pitchV1ToUnits(p.pitch)), {
16857
17322
  noteLengthSteps: p.durationSteps,
16858
17323
  velocity: p.velocity
16859
17324
  });
@@ -16870,7 +17335,7 @@ var mountDAW = (target, options = {}) => {
16870
17335
  if (firstPitch !== null) {
16871
17336
  centerPitch(firstPitch);
16872
17337
  } else {
16873
- centerPitch(48);
17338
+ centerPitch(pitchV1ToUnits(48));
16874
17339
  }
16875
17340
  redrawAll();
16876
17341
  updateTrackPanel();
@@ -16934,18 +17399,21 @@ var mountDAW = (target, options = {}) => {
16934
17399
  }
16935
17400
  }, 30);
16936
17401
  };
16937
- const showConfirmModal = (message) => new Promise((resolve) => {
17402
+ const showConfirmModal = (message, opts) => new Promise((resolve) => {
17403
+ const title = opts?.title ?? "\u30E2\u30FC\u30C9\u306E\u78BA\u8A8D";
17404
+ const yes = opts?.yes ?? "\u306F\u3044\uFF08\u4E0A\u7D1A\u8005\u30E2\u30FC\u30C9\u306B\u5207\u308A\u66FF\u3048\u308B\uFF09";
17405
+ const no = opts?.no ?? "\u3044\u3044\u3048\uFF08\u3053\u306E\u307E\u307E\u8AAD\u307F\u8FBC\u3080\uFF09";
16938
17406
  const overlay = document.createElement("div");
16939
17407
  overlay.className = "dtm-modal-overlay";
16940
17408
  overlay.innerHTML = `
16941
17409
  <div class="dtm-modal">
16942
17410
  <div class="dtm-modal-header">
16943
- <span class="dtm-modal-title">\u30E2\u30FC\u30C9\u306E\u78BA\u8A8D</span>
17411
+ <span class="dtm-modal-title">${title}</span>
16944
17412
  </div>
16945
17413
  <div class="dtm-modal-body"><p>${message}</p></div>
16946
17414
  <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>
17415
+ <button class="dtm-btn dtm-btn--ghost dtm-confirm-no">${no}</button>
17416
+ <button class="dtm-btn dtm-btn--primary dtm-confirm-yes">${yes}</button>
16949
17417
  </div>
16950
17418
  </div>`;
16951
17419
  const close = (result) => {
@@ -17053,10 +17521,45 @@ var mountDAW = (target, options = {}) => {
17053
17521
  });
17054
17522
  refs.decomposeChordToggle.addEventListener("change", notifyViewState);
17055
17523
  refs.ignoreChordHeavyToggle.addEventListener("change", notifyViewState);
17524
+ refs.edoSelect.addEventListener("change", async () => {
17525
+ const next = Number.parseInt(refs.edoSelect.value, 10) === 31 ? 31 : 12;
17526
+ const prev = renderConfig.edo ?? 12;
17527
+ if (next === prev) return;
17528
+ const hasNotes = trackStates.some((t) => t.core.getNotes().length > 0);
17529
+ if (next === 12 && prev === 31 && hasNotes) {
17530
+ const okToDrop = await showConfirmModal(
17531
+ "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",
17532
+ {
17533
+ title: "\u97F3\u5F8B\u306E\u78BA\u8A8D",
17534
+ yes: "\u5207\u308A\u66FF\u3048\u308B",
17535
+ no: "\u3084\u3081\u308B"
17536
+ }
17537
+ );
17538
+ if (!okToDrop) {
17539
+ refs.edoSelect.value = String(prev);
17540
+ return;
17541
+ }
17542
+ }
17543
+ overlayDuring(() => {
17544
+ applyEdo(next, { snap: true });
17545
+ if (next === 31 && prev === 12) {
17546
+ zoomY = Math.max(50, Math.round(zoomY / 2 / 25) * 25);
17547
+ } else if (next === 12 && prev === 31) {
17548
+ zoomY = Math.min(200, Math.round(zoomY * 2 / 25) * 25);
17549
+ }
17550
+ applyZoomY();
17551
+ const first = getFirstDetectedPitch();
17552
+ centerPitch(first ?? pitchV1ToUnits(48));
17553
+ redrawAll();
17554
+ updateUndoRedo();
17555
+ notifyViewState();
17556
+ });
17557
+ });
17558
+ refs.edoInfoBtn.addEventListener("click", () => {
17559
+ showModal("\u97F3\u5F8B\u306E\u89E3\u8AAC", EDO_INFO_HTML);
17560
+ });
17056
17561
  refs.masterVolume.addEventListener("input", () => {
17057
- masterVolume = Number.parseInt(refs.masterVolume.value, 10) || 0;
17058
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17059
- options.singingVoices?.setVolume(masterVolume / 100);
17562
+ applyMasterVolume(Number.parseInt(refs.masterVolume.value, 10) || 0);
17060
17563
  });
17061
17564
  refs.masterComp.addEventListener("input", () => {
17062
17565
  masterCompression = Number.parseInt(refs.masterComp.value, 10) || 0;
@@ -17120,10 +17623,7 @@ var mountDAW = (target, options = {}) => {
17120
17623
  const scale = targetPeak / observedPeakMax;
17121
17624
  const suggested = clamp5(Math.round(masterVolume * scale), 10, 100);
17122
17625
  if (suggested !== masterVolume) {
17123
- masterVolume = suggested;
17124
- refs.masterVolume.value = String(masterVolume);
17125
- refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17126
- options.singingVoices?.setVolume(masterVolume / 100);
17626
+ applyMasterVolume(suggested);
17127
17627
  }
17128
17628
  observedPeakMax = 0;
17129
17629
  observedPlayMs = 0;
@@ -17283,7 +17783,8 @@ var mountDAW = (target, options = {}) => {
17283
17783
  generateRandomPattern(getActive().core, {
17284
17784
  stepsPerBar: renderConfig.stepsPerBar,
17285
17785
  startStep: playStartStep,
17286
- pitchRangeStart: renderConfig.pitchRangeStart
17786
+ pitchRangeStart: renderConfig.pitchRangeStart,
17787
+ edo: renderConfig.edo
17287
17788
  });
17288
17789
  redrawAll();
17289
17790
  });
@@ -17620,7 +18121,7 @@ var mountDAW = (target, options = {}) => {
17620
18121
  id: 0,
17621
18122
  startStep: p.startStep,
17622
18123
  durationSteps: p.durationSteps,
17623
- pitch: p.pitch,
18124
+ pitchUnits: snapToEdoGrid(pitchV1ToUnits(p.pitch)),
17624
18125
  velocity: p.velocity
17625
18126
  }));
17626
18127
  const mml = refCore.getMMLFromNotes(asNotes, tempo, 100).trim();
@@ -18075,10 +18576,10 @@ var mountDAW = (target, options = {}) => {
18075
18576
  const newStart = playStartStep + (note.startStep - minStart);
18076
18577
  const newEnd = newStart + note.durationSteps;
18077
18578
  const overlap = notes.some(
18078
- (ex) => ex.pitch === note.pitch && newStart < ex.startStep + ex.durationSteps && newEnd > ex.startStep
18579
+ (ex) => ex.pitchUnits === note.pitchUnits && newStart < ex.startStep + ex.durationSteps && newEnd > ex.startStep
18079
18580
  );
18080
18581
  if (!overlap)
18081
- core.addNote(newStart, note.pitch, {
18582
+ core.addNote(newStart, note.pitchUnits, {
18082
18583
  noteLengthSteps: note.durationSteps,
18083
18584
  velocity: note.velocity
18084
18585
  });
@@ -18151,13 +18652,13 @@ var mountDAW = (target, options = {}) => {
18151
18652
  pausedPlayStep = step;
18152
18653
  currentPlayStep = step;
18153
18654
  playbackState = "paused";
18154
- const canvas = getGridCanvas();
18655
+ const canvas = renderer.getGridCanvas();
18155
18656
  currentOffsetX = clamp5(
18156
18657
  step * renderConfig.stepWidth - canvas.width * 0.5,
18157
18658
  0,
18158
18659
  getMaxOffsetX()
18159
18660
  );
18160
- setDrawOffset(currentOffsetX, currentOffsetY);
18661
+ renderer.setDrawOffset(currentOffsetX, currentOffsetY);
18161
18662
  updateTransport();
18162
18663
  redrawAll();
18163
18664
  };
@@ -18225,16 +18726,10 @@ var mountDAW = (target, options = {}) => {
18225
18726
  forcePauseAt,
18226
18727
  setLoading,
18227
18728
  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);
18729
+ applyMasterVolume(volume);
18232
18730
  },
18233
18731
  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);
18732
+ applyMasterVolume(volume);
18238
18733
  },
18239
18734
  setDrumVolume: (volume) => {
18240
18735
  drumVolume = clamp5(volume, 0, 100);
@@ -18277,15 +18772,19 @@ var mountDAW = (target, options = {}) => {
18277
18772
  suppressPatch = true;
18278
18773
  track.core.beginBatch();
18279
18774
  for (const n of added) {
18280
- const existing = track.core.getNotes().find((e) => e.startStep === n.startStep && e.pitch === n.pitch);
18775
+ const existing = track.core.getNotes().find(
18776
+ (e) => e.startStep === n.startStep && e.pitchUnits === n.pitchUnits
18777
+ );
18281
18778
  if (existing) track.core.deleteNoteById(existing.id);
18282
- track.core.addNote(n.startStep, n.pitch, {
18779
+ track.core.addNote(n.startStep, n.pitchUnits, {
18283
18780
  noteLengthSteps: n.durationSteps,
18284
18781
  velocity: n.velocity
18285
18782
  });
18286
18783
  }
18287
18784
  for (const r of removed) {
18288
- const note = track.core.getNotes().find((n) => n.startStep === r.startStep && n.pitch === r.pitch);
18785
+ const note = track.core.getNotes().find(
18786
+ (n) => n.startStep === r.startStep && n.pitchUnits === r.pitchUnits
18787
+ );
18289
18788
  if (note) track.core.deleteNoteById(note.id);
18290
18789
  }
18291
18790
  track.core.endBatch();
@@ -18327,9 +18826,9 @@ var mountDAW = (target, options = {}) => {
18327
18826
  if (t.config.id === activeTrackId) updateTrackPanel();
18328
18827
  },
18329
18828
  noteToCanvas: (step, pitch) => {
18330
- const canvas = getGridCanvas();
18829
+ const canvas = renderer.getGridCanvas();
18331
18830
  const x2 = step * renderConfig.stepWidth - currentOffsetX;
18332
- const y = (renderConfig.keyCount - 1 - pitch) * renderConfig.keyHeight - currentOffsetY;
18831
+ const y = (renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart) / (renderConfig.unitsPerRow ?? UNITS_PER_SEMITONE)) * renderConfig.keyHeight - currentOffsetY;
18333
18832
  const onScreen = x2 >= 0 && x2 <= canvas.width && y >= 0 && y <= canvas.height;
18334
18833
  let side = null;
18335
18834
  if (!onScreen) {
@@ -18393,7 +18892,7 @@ var playSingingMML = async (mml, options = {}) => {
18393
18892
  id: id++,
18394
18893
  startStep: p.startStep,
18395
18894
  durationSteps: p.durationSteps,
18396
- pitch: p.pitch,
18895
+ pitchUnits: p.pitchUnits,
18397
18896
  velocity: p.velocity
18398
18897
  }));
18399
18898
  return {
@@ -18419,7 +18918,7 @@ var playSingingMML = async (mml, options = {}) => {
18419
18918
  (a, b) => a.startStep - b.startStep
18420
18919
  );
18421
18920
  const gate = (lt.gate ?? DEFAULT_GATE) / 100;
18422
- const semis = (lt.octave ?? 0) * 12;
18921
+ const semis = (lt.octave ?? 0) * UNITS_PER_OCTAVE;
18423
18922
  const count = Math.min(sorted.length, lt.syllables.length);
18424
18923
  const notes = [];
18425
18924
  for (let i2 = 0; i2 < count; i2++) {
@@ -18427,7 +18926,7 @@ var playSingingMML = async (mml, options = {}) => {
18427
18926
  if (n.startStep < fromStep) continue;
18428
18927
  notes.push({
18429
18928
  syllable: lt.syllables[i2],
18430
- pitch: n.pitch + semis,
18929
+ pitch: units(n.pitchUnits + semis),
18431
18930
  startSec: (n.startStep - fromStep) * secondsPerStep,
18432
18931
  durationSec: n.durationSteps * secondsPerStep * gate
18433
18932
  });
@@ -18615,24 +19114,28 @@ var createPianoRoll = (options, handlers) => {
18615
19114
  config,
18616
19115
  noteLengthSteps = 1
18617
19116
  } = options;
18618
- init(mountTarget, width, height, config);
19117
+ const renderer = createRenderer(mountTarget, width, height, config);
18619
19118
  let currentNoteLengthSteps = noteLengthSteps;
18620
19119
  let selectionRect = null;
18621
19120
  let isSelecting = false;
18622
19121
  let selectionStart = null;
18623
19122
  let selectedNotes = [];
18624
19123
  let copiedNotes = [];
18625
- const core = new MMLCore({
18626
- onMMLGenerated: handlers.onMMLGenerated,
18627
- onNotesChanged: (notes) => {
18628
- handlers.onNotesChanged(notes);
18629
- }
18630
- });
19124
+ const core = new MMLCore(
19125
+ {
19126
+ onMMLGenerated: handlers.onMMLGenerated,
19127
+ onNotesChanged: (notes) => {
19128
+ handlers.onNotesChanged(notes);
19129
+ }
19130
+ },
19131
+ 80,
19132
+ () => config
19133
+ );
18631
19134
  const getAddNoteOptions = () => ({
18632
19135
  noteLengthSteps: currentNoteLengthSteps
18633
19136
  });
18634
19137
  let suppressClick = false;
18635
- onClick((step, pitch) => {
19138
+ renderer.onClick((step, pitch) => {
18636
19139
  if (suppressClick) {
18637
19140
  suppressClick = false;
18638
19141
  return;
@@ -18644,7 +19147,7 @@ var createPianoRoll = (options, handlers) => {
18644
19147
  } else if (mode === "eraser") {
18645
19148
  const notes = core.getNotes();
18646
19149
  const note = notes.find(
18647
- (n) => n.startStep <= step && step < n.startStep + n.durationSteps && n.pitch === pitch
19150
+ (n) => n.startStep <= step && step < n.startStep + n.durationSteps && n.pitchUnits === pitch
18648
19151
  );
18649
19152
  if (note) {
18650
19153
  core.deleteNoteById(note.id);
@@ -18652,17 +19155,23 @@ var createPianoRoll = (options, handlers) => {
18652
19155
  }
18653
19156
  }
18654
19157
  });
18655
- const gridCanvas = getGridCanvas();
19158
+ const gridCanvas = renderer.getGridCanvas();
18656
19159
  const resizeHandleWidth = 6;
18657
19160
  let dragState = null;
18658
19161
  let hasDragged = false;
18659
19162
  let lastPreviewPitch = null;
18660
19163
  const findNoteAtPosition = (x2, y) => {
18661
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18662
- const offset = getDrawOffset();
19164
+ const {
19165
+ stepWidth,
19166
+ keyHeight,
19167
+ keyCount,
19168
+ pitchRangeStart,
19169
+ unitsPerRow: upr = UNITS_PER_SEMITONE
19170
+ } = renderer.getRenderConfig();
19171
+ const offset = renderer.getDrawOffset();
18663
19172
  for (const note of core.getNotes()) {
18664
19173
  const logicalX = note.startStep * stepWidth;
18665
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19174
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18666
19175
  const logicalY = yIndex * keyHeight;
18667
19176
  const w = note.durationSteps * stepWidth;
18668
19177
  const h = keyHeight;
@@ -18676,7 +19185,7 @@ var createPianoRoll = (options, handlers) => {
18676
19185
  };
18677
19186
  const handlePointerMove = (e) => {
18678
19187
  if (core.getToolMode() === "select" && isSelecting && selectionStart) {
18679
- const { x: x2, y } = getGridPosition(e);
19188
+ const { x: x2, y } = renderer.getGridPosition(e);
18680
19189
  const minX = Math.min(x2, selectionStart.x);
18681
19190
  const minY = Math.min(y, selectionStart.y);
18682
19191
  const width2 = Math.abs(x2 - selectionStart.x);
@@ -18688,7 +19197,7 @@ var createPianoRoll = (options, handlers) => {
18688
19197
  }
18689
19198
  if (!dragState) return;
18690
19199
  hasDragged = true;
18691
- const { step, pitch } = getGridPosition(e);
19200
+ const { step, pitch } = renderer.getGridPosition(e);
18692
19201
  if (dragState.mode === "move") {
18693
19202
  if (dragState.selectedNotes && dragState.selectedNotes.length > 0) {
18694
19203
  const noteId = dragState.noteId;
@@ -18697,10 +19206,10 @@ var createPianoRoll = (options, handlers) => {
18697
19206
  const nextStart2 = step - dragState.dragOffsetStep;
18698
19207
  const nextPitch2 = pitch - dragState.dragOffsetPitch;
18699
19208
  const stepDelta = nextStart2 - baseNote.startStep;
18700
- const pitchDelta = nextPitch2 - baseNote.pitch;
19209
+ const pitchDelta = nextPitch2 - baseNote.pitchUnits;
18701
19210
  for (const note of dragState.selectedNotes) {
18702
19211
  const newStart = note.startStep + stepDelta;
18703
- const newPitch = note.pitch + pitchDelta;
19212
+ const newPitch = units(note.pitchUnits + pitchDelta);
18704
19213
  core.moveNote(note.id, newStart, newPitch);
18705
19214
  }
18706
19215
  if (options.onPreviewSound && pitch !== lastPreviewPitch) {
@@ -18711,7 +19220,7 @@ var createPianoRoll = (options, handlers) => {
18711
19220
  return;
18712
19221
  }
18713
19222
  const nextStart = step - dragState.dragOffsetStep;
18714
- const nextPitch = pitch - dragState.dragOffsetPitch;
19223
+ const nextPitch = units(pitch - dragState.dragOffsetPitch);
18715
19224
  core.moveNote(dragState.noteId, nextStart, nextPitch);
18716
19225
  return;
18717
19226
  }
@@ -18741,7 +19250,7 @@ var createPianoRoll = (options, handlers) => {
18741
19250
  }
18742
19251
  };
18743
19252
  gridCanvas.addEventListener("pointerdown", (e) => {
18744
- const { x: x2, y, step, pitch } = getGridPosition(e);
19253
+ const { x: x2, y, step, pitch } = renderer.getGridPosition(e);
18745
19254
  const currentMode = core.getToolMode();
18746
19255
  if (currentMode === "select") {
18747
19256
  const clickedNote = findNoteAtPosition(x2, y);
@@ -18752,7 +19261,7 @@ var createPianoRoll = (options, handlers) => {
18752
19261
  noteId: clickedNote.id,
18753
19262
  mode: "move",
18754
19263
  dragOffsetStep: step - clickedNote.startStep,
18755
- dragOffsetPitch: pitch - clickedNote.pitch,
19264
+ dragOffsetPitch: pitch - clickedNote.pitchUnits,
18756
19265
  startStep: clickedNote.startStep,
18757
19266
  selectedNotes: notesInRect
18758
19267
  // 複数選択ノートを保存
@@ -18770,10 +19279,16 @@ var createPianoRoll = (options, handlers) => {
18770
19279
  }
18771
19280
  const note = findNoteAtPosition(x2, y);
18772
19281
  if (!note) return;
18773
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18774
- const offset = getDrawOffset();
19282
+ const {
19283
+ stepWidth,
19284
+ keyHeight,
19285
+ keyCount,
19286
+ pitchRangeStart,
19287
+ unitsPerRow: upr = UNITS_PER_SEMITONE
19288
+ } = renderer.getRenderConfig();
19289
+ const offset = renderer.getDrawOffset();
18775
19290
  const logicalX = note.startStep * stepWidth;
18776
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19291
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18777
19292
  const logicalY = yIndex * keyHeight;
18778
19293
  const renderX = logicalX - offset.x;
18779
19294
  const renderY = logicalY - offset.y;
@@ -18792,7 +19307,7 @@ var createPianoRoll = (options, handlers) => {
18792
19307
  noteId: note.id,
18793
19308
  mode: "move",
18794
19309
  dragOffsetStep: step - note.startStep,
18795
- dragOffsetPitch: pitch - note.pitch,
19310
+ dragOffsetPitch: pitch - note.pitchUnits,
18796
19311
  startStep: note.startStep
18797
19312
  };
18798
19313
  });
@@ -18803,41 +19318,47 @@ var createPianoRoll = (options, handlers) => {
18803
19318
  "wheel",
18804
19319
  (e) => {
18805
19320
  e.preventDefault();
18806
- const configValues = getRenderConfig();
19321
+ const configValues = renderer.getRenderConfig();
18807
19322
  const gridHeight = gridCanvas.height;
18808
19323
  const maxOffsetY = Math.max(
18809
19324
  0,
18810
19325
  configValues.keyCount * configValues.keyHeight - gridHeight
18811
19326
  );
18812
- const currentOffset = getDrawOffset();
19327
+ const currentOffset = renderer.getDrawOffset();
18813
19328
  const nextOffsetY = Math.min(
18814
19329
  Math.max(currentOffset.y + e.deltaY, 0),
18815
19330
  maxOffsetY
18816
19331
  );
18817
- setDrawOffset(currentOffset.x, nextOffsetY);
18818
- drawGrid();
18819
- drawNotes(core.getNotes());
19332
+ renderer.setDrawOffset(currentOffset.x, nextOffsetY);
19333
+ renderer.drawGrid();
19334
+ renderer.drawNotes(core.getNotes());
18820
19335
  },
18821
19336
  { passive: false }
18822
19337
  );
18823
19338
  const redraw = () => {
18824
- drawGrid();
18825
- drawNotes(core.getNotes());
19339
+ renderer.drawGrid();
19340
+ renderer.drawNotes(core.getNotes());
18826
19341
  if (core.getToolMode() === "select") {
18827
- drawSelectionRect(selectionRect);
19342
+ renderer.drawSelectionRect(selectionRect);
18828
19343
  if (selectedNotes.length > 0) {
18829
19344
  const selectedIds = new Set(selectedNotes.map((n) => n.id));
18830
- drawSelectedNotes(core.getNotes(), selectedIds);
19345
+ renderer.drawSelectedNotes(core.getNotes(), selectedIds);
18831
19346
  }
18832
19347
  }
18833
19348
  };
18834
19349
  const getNotesInRect = (rect) => {
18835
- const { stepWidth, keyHeight, keyCount, pitchRangeStart } = getRenderConfig();
18836
- const offset = getDrawOffset();
19350
+ const {
19351
+ stepWidth,
19352
+ keyHeight,
19353
+ keyCount,
19354
+ pitchRangeStart,
19355
+ unitsPerRow: upr = UNITS_PER_SEMITONE
19356
+ } = renderer.getRenderConfig();
19357
+ const offset = renderer.getDrawOffset();
18837
19358
  const notes = [];
18838
19359
  for (const note of core.getNotes()) {
18839
19360
  const logicalX = note.startStep * stepWidth;
18840
- const yIndex = keyCount - 1 - (note.pitch - pitchRangeStart);
19361
+ const yIndex = keyCount - 1 - (note.pitchUnits - pitchRangeStart) / upr;
18841
19362
  const logicalY = yIndex * keyHeight;
18842
19363
  const noteRect = {
18843
19364
  x: logicalX - offset.x,
@@ -18884,7 +19405,7 @@ var createPianoRoll = (options, handlers) => {
18884
19405
  const minStart = Math.min(...copiedNotes.map((n) => n.startStep));
18885
19406
  copiedNotes.forEach((note) => {
18886
19407
  const newStep = startStep + (note.startStep - minStart);
18887
- core.addNote(newStep, note.pitch, {
19408
+ core.addNote(newStep, note.pitchUnits, {
18888
19409
  noteLengthSteps: note.durationSteps,
18889
19410
  velocity: note.velocity
18890
19411
  });
@@ -19725,10 +20246,12 @@ var createDtmStudio = async (options = {}) => {
19725
20246
  );
19726
20247
  }
19727
20248
  if (!sfInst) return;
20249
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
19728
20250
  sfInst.play({
19729
20251
  ctx: audioCtx,
19730
20252
  destination: getChannelStrip(e.trackId).input,
19731
- pitch: e.pitch,
20253
+ pitch: midi,
20254
+ detuneCents,
19732
20255
  volume: e.volume,
19733
20256
  velocity: e.velocity,
19734
20257
  when: e.when,
@@ -20044,10 +20567,12 @@ var createDtmStudio = async (options = {}) => {
20044
20567
  );
20045
20568
  }
20046
20569
  if (!sfInst) return;
20570
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20047
20571
  sfInst.play({
20048
20572
  ctx: audioCtx,
20049
20573
  destination: getChannelStrip(e.trackId).input,
20050
- pitch: e.pitch,
20574
+ pitch: midi,
20575
+ detuneCents,
20051
20576
  volume: e.volume,
20052
20577
  velocity: e.velocity,
20053
20578
  when: e.when,
@@ -20133,10 +20658,12 @@ var createDtmStudio = async (options = {}) => {
20133
20658
  );
20134
20659
  }
20135
20660
  if (!sfInst) return;
20661
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20136
20662
  sfInst.play({
20137
20663
  ctx: audioCtx,
20138
20664
  destination: getChannelStrip(e.trackId).input,
20139
- pitch: e.pitch,
20665
+ pitch: midi,
20666
+ detuneCents,
20140
20667
  volume: e.volume,
20141
20668
  velocity: e.velocity,
20142
20669
  when: e.when,
@@ -20215,10 +20742,12 @@ var createDtmStudio = async (options = {}) => {
20215
20742
  );
20216
20743
  }
20217
20744
  if (!sfInst) return;
20745
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20218
20746
  sfInst.play({
20219
20747
  ctx: audioCtx,
20220
20748
  destination: getChannelStrip(e.trackId).input,
20221
- pitch: e.pitch,
20749
+ pitch: midi,
20750
+ detuneCents,
20222
20751
  volume: e.volume,
20223
20752
  velocity: e.velocity,
20224
20753
  when: e.when,
@@ -20255,10 +20784,12 @@ var createDtmStudio = async (options = {}) => {
20255
20784
  sfInst = resolveSoundFont(defaultPreset, role, "simple");
20256
20785
  }
20257
20786
  if (!sfInst) return;
20787
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20258
20788
  sfInst.play({
20259
20789
  ctx: audioCtx,
20260
20790
  destination: getChannelStrip(e.trackId).input,
20261
- pitch: e.pitch,
20791
+ pitch: midi,
20792
+ detuneCents,
20262
20793
  volume: e.volume,
20263
20794
  velocity: e.velocity,
20264
20795
  when: e.when,
@@ -20281,10 +20812,12 @@ var createDtmStudio = async (options = {}) => {
20281
20812
  if (audioCtx.state === "suspended") {
20282
20813
  await audioCtx.resume();
20283
20814
  }
20815
+ const { midi, detuneCents } = unitsToMidiDetune(options2.pitchUnits);
20284
20816
  sfInst.play({
20285
20817
  ctx: audioCtx,
20286
20818
  destination: masterGain,
20287
- pitch: options2.pitch,
20819
+ pitch: midi,
20820
+ detuneCents,
20288
20821
  volume: vol / 100,
20289
20822
  when: 0,
20290
20823
  duration: dur
@@ -20304,7 +20837,7 @@ var createDtmStudio = async (options = {}) => {
20304
20837
  // 伴奏トラック
20305
20838
  startStep: p.startStep,
20306
20839
  durationSteps: p.durationSteps,
20307
- pitch: p.pitch,
20840
+ pitchUnits: p.pitchUnits,
20308
20841
  velocity: p.velocity
20309
20842
  }));
20310
20843
  const playerPreset = defaultPreset;
@@ -20312,10 +20845,12 @@ var createDtmStudio = async (options = {}) => {
20312
20845
  const playPlayerNote = (e) => {
20313
20846
  const sfInst = resolveSoundFont(playerPreset, "chord");
20314
20847
  if (!sfInst) return;
20848
+ const { midi, detuneCents } = unitsToMidiDetune(e.pitchUnits);
20315
20849
  sfInst.play({
20316
20850
  ctx: audioCtx,
20317
20851
  destination: getChannelStrip("chord").input,
20318
- pitch: e.pitch,
20852
+ pitch: midi,
20853
+ detuneCents,
20319
20854
  volume: e.volume,
20320
20855
  velocity: e.velocity,
20321
20856
  when: e.when,
@@ -20453,8 +20988,12 @@ var createDtmStudio = async (options = {}) => {
20453
20988
  };
20454
20989
  };
20455
20990
  export {
20991
+ A4_HZ,
20992
+ A4_UNITS,
20993
+ CENTS_PER_UNIT,
20456
20994
  DAW_CSS,
20457
20995
  DEFAULT_BPM,
20996
+ DEFAULT_EDO,
20458
20997
  DEFAULT_GATE,
20459
20998
  DEFAULT_PAN,
20460
20999
  DEFAULT_PLAYBACK_VELOCITY,
@@ -20464,30 +21003,41 @@ export {
20464
21003
  DRUM_FONT,
20465
21004
  DRUM_KEYS,
20466
21005
  DRUM_PATTERNS,
21006
+ EDO31_NAMES,
20467
21007
  GM_INSTRUMENT_NAMES,
20468
21008
  INSTRUMENT_PRESETS,
21009
+ KEY_COUNT,
20469
21010
  KOE_BASE_URL,
20470
21011
  KOE_VOICEBANKS,
20471
21012
  KOE_VOICEBANK_LABELS,
20472
21013
  KOE_VOICEBANK_TERMS,
20473
21014
  LinkedList,
20474
21015
  MAX_VOCAL_VOLUME,
21016
+ MICRO_STEP,
20475
21017
  MMLCore,
20476
21018
  MML_END_MARKER,
20477
21019
  MidiSearchClient,
20478
- PITCH_MAP2 as PITCH_MAP,
21020
+ PITCH_ENCODING_VERSION,
21021
+ PITCH_MAP,
21022
+ PITCH_RANGE_END,
21023
+ PITCH_RANGE_START,
20479
21024
  PREWARM_NOTES,
20480
21025
  TRACKS_ADVANCED,
20481
21026
  TRACKS_SIMPLE,
21027
+ UNITS_PER_EDO31_DEGREE,
21028
+ UNITS_PER_OCTAVE,
21029
+ UNITS_PER_SEMITONE,
20482
21030
  VIBRATO_MIN_SEC,
20483
21031
  VOICE_IMAGES,
20484
21032
  VOICE_IMAGE_KEY,
21033
+ addUnits,
20485
21034
  analyzeMidiTracks,
20486
21035
  applyHarmonicFilter,
20487
21036
  applyMonophonic,
20488
21037
  buildChordPlacements,
20489
21038
  buildDrumPatternJson,
20490
21039
  buildNameToKeyMapping,
21040
+ chromaticStep,
20491
21041
  collectPitchTokens,
20492
21042
  concatFloat32,
20493
21043
  createAudioContext,
@@ -20496,19 +21046,13 @@ export {
20496
21046
  createKoeVoice,
20497
21047
  createLyricsConductor,
20498
21048
  createPianoRoll,
21049
+ createRenderer,
20499
21050
  createSequencer,
20500
21051
  createSingingVoices,
20501
21052
  createSynth,
20502
21053
  createVoiceRegistry,
20503
21054
  decodeMml,
20504
21055
  decomposeToMonophonic,
20505
- drawGrid,
20506
- drawHeader,
20507
- drawKeyboard,
20508
- drawNoteLyrics,
20509
- drawNotes,
20510
- drawSelectedNotes,
20511
- drawSelectionRect,
20512
21056
  encodeMml,
20513
21057
  encodeWavPCM16,
20514
21058
  exportMIDI,
@@ -20516,36 +21060,35 @@ export {
20516
21060
  extractMidiDrumPattern,
20517
21061
  extractMidiPlacements,
20518
21062
  extractMidiPlacementsByTrack,
21063
+ fifthToStep,
21064
+ fifthToUnits,
20519
21065
  formatMmlMeta,
20520
21066
  freqFromPitch,
20521
21067
  generateRandomPattern,
20522
- getDrawOffset,
20523
21068
  getDrumPatternKeys,
20524
- getGridCanvas,
20525
- getGridContext,
20526
- getGridPosition,
20527
- getHeaderCanvas,
20528
21069
  getMidiBPM,
20529
- getRenderConfig,
20530
- getXY,
20531
21070
  icon,
20532
- init,
20533
21071
  injectStyles,
20534
21072
  isChordHeavyTrack,
21073
+ isNaturalLetter,
20535
21074
  isPlausibleMidiTranscription,
20536
21075
  isValidHttpUrl,
21076
+ keyCountFor,
20537
21077
  koeUrl,
21078
+ midiNote,
21079
+ midiToUnits,
20538
21080
  mountChordPlayer,
20539
21081
  mountDAW,
20540
21082
  mountMmlPlayer,
21083
+ naturalStep,
20541
21084
  normalizeDrumPatterns,
20542
21085
  normalizeLyrics,
20543
- onClick,
20544
21086
  panToStereo,
20545
21087
  parseCustomVocals,
20546
21088
  parseLyrics,
20547
21089
  parseMML,
20548
21090
  parseMmlMeta,
21091
+ pitchV1ToUnits,
20549
21092
  playChords,
20550
21093
  playMML,
20551
21094
  playNote,
@@ -20553,13 +21096,19 @@ export {
20553
21096
  playSingingMML,
20554
21097
  resolveDrumPattern,
20555
21098
  resolveLoopPoint,
20556
- setBackgroundActive,
20557
- setDrawOffset,
20558
21099
  shiftNotes,
20559
21100
  showLoadingOverlay,
21101
+ spellingToUnits,
20560
21102
  stripCustomVocals,
20561
21103
  stripLyrics,
20562
21104
  stripMmlMeta,
20563
21105
  transposeNotes,
21106
+ units,
21107
+ unitsPerRow,
21108
+ unitsPerStep,
21109
+ unitsToHz,
21110
+ unitsToMidi,
21111
+ unitsToMidiDetune,
21112
+ unitsToPitchV1,
20564
21113
  vocalVolumeToGain
20565
21114
  };