@onjmin/dtm 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -4213,7 +4213,7 @@ var NATURAL_STEPS2 = {
4213
4213
  };
4214
4214
  var isNatural = (ch) => Object.hasOwn(NATURAL_STEPS2[12], ch);
4215
4215
  var clamp3 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
4216
- var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode|edo)=([\w-]+)/gi;
4216
+ var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode|edo)=([\w:-]+)/gi;
4217
4217
  var TRACK_INST_DIRECTIVE = /#t(\d+)inst=([^#;\r\n]+)/gi;
4218
4218
  var TRACK_COMP_DIRECTIVE = /#t(\d+)comp=(\d+)/gi;
4219
4219
  var TRACK_WIDTH_DIRECTIVE = /#t(\d+)width=(\d+)/gi;
@@ -8734,6 +8734,27 @@ var DAW_CSS = `
8734
8734
  .dtm-player-play:active { transform: translate(2px, 2px); box-shadow: none; }
8735
8735
  .dtm-player-play--stop { background: var(--dtm-danger); }
8736
8736
  .dtm-player-play:disabled { opacity: 0.4; cursor: default; }
8737
+ .dtm-player-loop {
8738
+ display: inline-flex;
8739
+ align-items: center;
8740
+ justify-content: center;
8741
+ width: 28px;
8742
+ height: 28px;
8743
+ padding: 0;
8744
+ border: 2px solid var(--dtm-border2);
8745
+ background: var(--dtm-bg);
8746
+ color: var(--dtm-muted);
8747
+ cursor: pointer;
8748
+ flex: 0 0 auto;
8749
+ }
8750
+ .dtm-player-loop:active { transform: translate(1px, 1px); }
8751
+ .dtm-player-loop--on {
8752
+ border-color: var(--c-black);
8753
+ background: var(--dtm-gold);
8754
+ color: var(--c-black);
8755
+ box-shadow: 2px 2px 0 var(--c-black);
8756
+ }
8757
+ .dtm-player-loop:disabled { opacity: 0.4; cursor: default; }
8737
8758
  .dtm-player-beat-row {
8738
8759
  display: flex;
8739
8760
  align-items: center;
@@ -9392,6 +9413,12 @@ var FALLBACK_VOCAL_ICON = Chip_default;
9392
9413
  var STEPS_PER_BEAT2 = 48;
9393
9414
  var STEPS_PER_BAR2 = 192;
9394
9415
  var DEFAULT_TRACK_COLORS = ["#00e436", "#29adff", "#ff77a8", "#ffec27"];
9416
+ var parseLoopMeta = (mml) => {
9417
+ const m = mml.match(/^#\s*loop\s*=\s*(on|off|1|0|true|false)\s*$/im);
9418
+ if (!m) return null;
9419
+ const v = m[1].toLowerCase();
9420
+ return v === "on" || v === "1" || v === "true";
9421
+ };
9395
9422
  var activePlayer = null;
9396
9423
  var agreedModelsInSession = /* @__PURE__ */ new Set();
9397
9424
  var LYRIC_MODEL_LABELS = {
@@ -9631,6 +9658,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9631
9658
  const colors = options.trackColors ?? DEFAULT_TRACK_COLORS;
9632
9659
  const useSynth = options.synth ?? !options.onPlayNote;
9633
9660
  const secondsPerStep = 60 / bpm / STEPS_PER_BEAT2;
9661
+ let loopEnabled = options.loop ?? parseLoopMeta(mml) ?? false;
9634
9662
  const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
9635
9663
  (a, b) => a - b
9636
9664
  );
@@ -9719,6 +9747,13 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9719
9747
  playBtn.className = "dtm-player-play";
9720
9748
  playBtn.innerHTML = icon("play", 12);
9721
9749
  playBtn.disabled = trackIndices.length === 0;
9750
+ const loopBtn = doc.createElement("button");
9751
+ loopBtn.type = "button";
9752
+ loopBtn.className = "dtm-player-loop";
9753
+ loopBtn.innerHTML = icon("loop", 12);
9754
+ loopBtn.title = loopEnabled ? "LOOP ON" : "LOOP OFF";
9755
+ loopBtn.classList.toggle("dtm-player-loop--on", loopEnabled);
9756
+ loopBtn.disabled = trackIndices.length === 0;
9722
9757
  const mutedTracks = /* @__PURE__ */ new Set();
9723
9758
  const labelByTrack = /* @__PURE__ */ new Map();
9724
9759
  const emojiByTrack = /* @__PURE__ */ new Map();
@@ -10151,7 +10186,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
10151
10186
  chordEl.className = "dtm-player-chord";
10152
10187
  chordEl.textContent = "";
10153
10188
  beatRow.appendChild(chordEl);
10154
- head.append(playBtn, beatRow, dots, mmlHeader);
10189
+ head.append(playBtn, loopBtn, beatRow, dots, mmlHeader);
10155
10190
  root.appendChild(head);
10156
10191
  const seekRow = doc.createElement("div");
10157
10192
  seekRow.className = "dtm-player-seek-row";
@@ -10451,6 +10486,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
10451
10486
  getPlayStartStep: () => 0,
10452
10487
  getDrumPattern: (currentBar) => resolveDrumPattern(drumPatternName, drumPatternDict, currentBar),
10453
10488
  getSoloTrackId: () => null,
10489
+ getLoop: () => loopEnabled,
10454
10490
  getAudioTime,
10455
10491
  onPlayNote: (e) => {
10456
10492
  const trackIdx = Number(e.trackId);
@@ -10567,8 +10603,16 @@ var mountMmlPlayer = (target, mml, options = {}) => {
10567
10603
  if (streaming && !skipSinging) {
10568
10604
  const v = ensureVoices();
10569
10605
  v.setVolume(trackVolume / 100 * (masterVolume / 100));
10606
+ let loopLengthSec;
10607
+ let loopStartSec;
10608
+ if (loopEnabled) {
10609
+ loopStartSec = 0;
10610
+ loopLengthSec = maxStep * secondsPerStep;
10611
+ }
10570
10612
  v.startStream(tracks, seq.getStartTime(), {
10571
10613
  isAudible: (t) => !mutedTracks.has(Number(t.id)),
10614
+ loopLengthSec,
10615
+ loopStartSec,
10572
10616
  onLateSkip: () => {
10573
10617
  showPlayerMessage(
10574
10618
  "\u97F3\u58F0\u5408\u6210\u304C\u9593\u306B\u5408\u308F\u306A\u3044\u305F\u3081\u3001\u4E00\u90E8\u306E\u767A\u97F3\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u307E\u3057\u305F"
@@ -10640,6 +10684,14 @@ var mountMmlPlayer = (target, mml, options = {}) => {
10640
10684
  if (playing) pause();
10641
10685
  else play(Number(seekInput.value));
10642
10686
  });
10687
+ loopBtn.addEventListener("click", () => {
10688
+ loopEnabled = !loopEnabled;
10689
+ loopBtn.classList.toggle("dtm-player-loop--on", loopEnabled);
10690
+ loopBtn.title = loopEnabled ? "LOOP ON" : "LOOP OFF";
10691
+ if (playing) {
10692
+ seekTo(playStep);
10693
+ }
10694
+ });
10643
10695
  const destroy = () => {
10644
10696
  doc.removeEventListener("click", handleOutsideClick);
10645
10697
  seq.stop();
@@ -11179,7 +11231,7 @@ var parseBpmMeta = (chords) => {
11179
11231
  if (Number.isNaN(val)) return null;
11180
11232
  return Math.max(40, Math.min(240, val));
11181
11233
  };
11182
- var parseLoopMeta = (chords) => {
11234
+ var parseLoopMeta2 = (chords) => {
11183
11235
  const m = chords.match(/^#\s*loop\s*=\s*(on|off|1|0|true|false)\s*$/im);
11184
11236
  if (!m) return null;
11185
11237
  const v = m[1].toLowerCase();
@@ -11356,7 +11408,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11356
11408
  let masterVolume = options.volume ?? 80;
11357
11409
  let isLoading = false;
11358
11410
  let loadAbortId = 0;
11359
- let loopEnabled = options.loop ?? parseLoopMeta(chords) ?? false;
11411
+ let loopEnabled = options.loop ?? parseLoopMeta2(chords) ?? false;
11360
11412
  let metronomeEnabled = parseMetronomeMeta(chords);
11361
11413
  let playOffsetSec = 0;
11362
11414
  let currentPlaySec = 0;
@@ -12422,292 +12474,356 @@ var CORPUS_MEDIANS = {
12422
12474
  complementarity: 0.045
12423
12475
  };
12424
12476
 
12425
- // src/compose-drums.ts
12426
- var DRUM_STYLE_LABELS = {
12427
- eight: "8\u30D3\u30FC\u30C8",
12428
- sixteen: "16\u30D3\u30FC\u30C8",
12429
- four: "4\u3064\u6253\u3061",
12430
- shuffle: "\u30B7\u30E3\u30C3\u30D5\u30EB",
12431
- ballad: "\u30D0\u30E9\u30FC\u30C9",
12432
- rock: "\u30ED\u30C3\u30AF"
12433
- };
12434
- var KICKS = {
12435
- // [抑えめ, 標準, 最大]
12436
- eight: [
12437
- [0, 8],
12438
- [0, 6, 8],
12439
- [0, 6, 8, 14]
12440
- ],
12441
- sixteen: [
12442
- [0, 3, 8],
12443
- [0, 3, 8, 11],
12444
- [0, 3, 6, 8, 11, 14]
12445
- ],
12446
- four: [
12447
- [0, 4, 8, 12],
12448
- [0, 4, 8, 12],
12449
- [0, 4, 8, 12, 14]
12450
- ],
12451
- shuffle: [
12452
- [0, 8],
12453
- [0, 8, 11],
12454
- [0, 5, 8, 11]
12455
- ],
12456
- ballad: [[0], [0, 8], [0, 8, 12]],
12457
- rock: [
12458
- [0, 8],
12459
- [0, 3, 8],
12460
- [0, 3, 8, 10, 14]
12461
- ]
12462
- };
12463
- var SNARES = {
12464
- eight: [
12465
- [4, 12],
12466
- [4, 12],
12467
- [4, 12]
12468
- ],
12469
- sixteen: [
12470
- [4, 12],
12471
- [4, 12],
12472
- [4, 12]
12473
- ],
12474
- four: [
12475
- [4, 12],
12476
- [4, 12],
12477
- [4, 12]
12478
- ],
12479
- shuffle: [
12480
- [4, 12],
12481
- [4, 12],
12482
- [4, 12]
12483
- ],
12484
- // バラードはサイドスティック(リムショット)から入って、盛り上がりでスネアへ移る。
12485
- ballad: [[12], [4, 12], [4, 12]],
12486
- rock: [
12487
- [4, 12],
12488
- [4, 12],
12489
- [4, 12]
12490
- ]
12491
- };
12492
- var GHOSTS = {
12493
- eight: [7],
12494
- sixteen: [7, 11, 15],
12495
- four: [],
12496
- shuffle: [11],
12497
- ballad: [],
12498
- rock: [7, 15]
12499
- };
12500
- var HIHATS = {
12501
- eight: [
12502
- [0, 4, 8, 12],
12503
- [0, 2, 4, 6, 8, 10, 12, 14],
12504
- [0, 2, 4, 6, 8, 10, 12, 14]
12505
- ],
12506
- sixteen: [
12507
- [0, 2, 4, 6, 8, 10, 12, 14],
12508
- [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
12509
- [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
12510
- ],
12511
- four: [
12512
- [2, 6, 10, 14],
12513
- [0, 2, 4, 6, 8, 10, 12, 14],
12514
- [0, 2, 4, 6, 8, 10, 12, 14]
12515
- ],
12516
- shuffle: [[], [], []],
12517
- // 三連なので格子では書けない。{@link shuffleHats} が作る
12518
- ballad: [
12519
- [0, 8],
12520
- [0, 4, 8, 12],
12521
- [0, 2, 4, 6, 8, 10, 12, 14]
12522
- ],
12523
- rock: [
12524
- [0, 4, 8, 12],
12525
- [0, 2, 4, 6, 8, 10, 12, 14],
12526
- [0, 2, 4, 6, 8, 10, 12, 14]
12527
- ]
12528
- };
12529
- var FILLS = [
12530
- // スネアの16分の詰め
12477
+ // src/compose-keys.ts
12478
+ var COMPOSE_KEYS = {
12479
+ // --- 長調 (12調) ---
12480
+ key_C: {
12481
+ id: "key_C",
12482
+ name: "C",
12483
+ label: "\u30CF\u9577\u8ABF (C)",
12484
+ mode: "major",
12485
+ rootShift: 0,
12486
+ moodId: "mood_happy",
12487
+ description: "\u7121\u57A2\u306B\u559C\u3070\u3057\u3044\u3001\u7D14\u7C8B\u3001\u7D20\u6734\u3001\u51FA\u767A"
12488
+ },
12489
+ key_Db: {
12490
+ id: "key_Db",
12491
+ name: "D\u266D",
12492
+ label: "\u5909\u30CB\u9577\u8ABF (D\u266D)",
12493
+ mode: "major",
12494
+ rootShift: 1,
12495
+ moodId: "mood_melancholy",
12496
+ description: "\u60B2\u3057\u307F\u3001\u6182\u9B31\u306A\u3001\u7518\u7F8E\u306A\u611F\u50B7\uFF08\u5B30\u30CF\u9577\u8ABF\u3068\u540C\u97F3\uFF09"
12497
+ },
12498
+ key_D: {
12499
+ id: "key_D",
12500
+ name: "D",
12501
+ label: "\u30CB\u9577\u8ABF (D)",
12502
+ mode: "major",
12503
+ rootShift: 2,
12504
+ moodId: "mood_triumphant",
12505
+ description: "\u610F\u6C17\u63DA\u3005\u3068\u3057\u305F\u3001\u52DD\u5229\u306E\u3001\u558A\u58F0\u3001\u83EF\u3084\u304B"
12506
+ },
12507
+ key_Eb: {
12508
+ id: "key_Eb",
12509
+ name: "E\u266D",
12510
+ label: "\u5909\u30DB\u9577\u8ABF (E\u266D)",
12511
+ mode: "major",
12512
+ rootShift: 3,
12513
+ moodId: "mood_dark",
12514
+ description: "\u53B3\u3057\u3044\u3001\u304D\u3064\u3044\u3001\u305D\u308C\u3067\u3044\u3066\u611B\u306B\u6E80\u3061\u305F\u3001\u8358\u91CD"
12515
+ },
12516
+ key_E: {
12517
+ id: "key_E",
12518
+ name: "E",
12519
+ label: "\u30DB\u9577\u8ABF (E)",
12520
+ mode: "major",
12521
+ rootShift: 4,
12522
+ moodId: "mood_fierce",
12523
+ description: "\u3051\u3093\u304B\u3063\u65E9\u3044\u3001\u8352\u3005\u3057\u3044\u3001\u8F1D\u304B\u3057\u3044\u60C5\u71B1"
12524
+ },
12525
+ key_F: {
12526
+ id: "key_F",
12527
+ name: "F",
12528
+ label: "\u30D8\u9577\u8ABF (F)",
12529
+ mode: "major",
12530
+ rootShift: 5,
12531
+ moodId: "mood_fierce",
12532
+ description: "\u6012\u308A\u72C2\u3063\u305F\u3001\u6C17\u6027\u306E\u8352\u3044\u3001\u4E00\u6642\u7684\u306A\u60B2\u5606\u3001\u6FC0\u52D5"
12533
+ },
12534
+ key_Gb: {
12535
+ id: "key_Gb",
12536
+ name: "G\u266D",
12537
+ label: "\u5909\u30C8\u9577\u8ABF (G\u266D)",
12538
+ mode: "major",
12539
+ rootShift: 6,
12540
+ moodId: "mood_triumphant",
12541
+ description: "\u56F0\u96E3\u306E\u6253\u7834\u3001\u5B89\u5835\u306E\u305F\u3081\u606F\u3001\u51F1\u65CB\uFF08\u5B30\u30D8\u9577\u8ABF\u3068\u540C\u97F3\uFF09"
12542
+ },
12543
+ key_G: {
12544
+ id: "key_G",
12545
+ name: "G",
12546
+ label: "\u30C8\u9577\u8ABF (G)",
12547
+ mode: "major",
12548
+ rootShift: -5,
12549
+ moodId: "mood_solemn",
12550
+ description: "\u53B3\u7C9B\u306A\u3001\u5D07\u9AD8\u306A\u3001\u5E7B\u60F3\u3001\u8AA0\u5B9F\u3001\u5E83\u304C\u308A"
12551
+ },
12552
+ key_Ab: {
12553
+ id: "key_Ab",
12554
+ name: "A\u266D",
12555
+ label: "\u5909\u30A4\u9577\u8ABF (A\u266D)",
12556
+ mode: "major",
12557
+ rootShift: -4,
12558
+ moodId: "mood_dark",
12559
+ description: "\u6B7B\u3001\u6C38\u9060\u3001\u88C1\u304D\u3001\u6DF1\u9060\u306A\u7791\u60F3"
12560
+ },
12561
+ key_A: {
12562
+ id: "key_A",
12563
+ name: "A",
12564
+ label: "\u30A4\u9577\u8ABF (A)",
12565
+ mode: "major",
12566
+ rootShift: -3,
12567
+ moodId: "mood_happy",
12568
+ description: "\u3046\u308C\u3057\u3044\u3001\u7267\u6B4C\u7684\u306A\u3001\u611B\u306E\u544A\u767D\u3001\u304D\u3089\u3073\u3084\u304B"
12569
+ },
12570
+ key_Bb: {
12571
+ id: "key_Bb",
12572
+ name: "B\u266D",
12573
+ label: "\u5909\u30ED\u9577\u8ABF (B\u266D)",
12574
+ mode: "major",
12575
+ rootShift: -2,
12576
+ moodId: "mood_happy",
12577
+ description: "\u559C\u3070\u3057\u3044\u3001\u98A8\u5909\u308F\u308A\u306A\u3001\u967D\u6C17\u306A\u3001\u8EFD\u5FEB"
12578
+ },
12579
+ key_B: {
12580
+ id: "key_B",
12581
+ name: "B",
12582
+ label: "\u30ED\u9577\u8ABF (B)",
12583
+ mode: "major",
12584
+ rootShift: -1,
12585
+ moodId: "mood_fierce",
12586
+ description: "\u3069\u304E\u3064\u3044\u3001\u5F37\u70C8\u306A\u3001\u8352\u3063\u307D\u3044\u3001\u731B\u70C8"
12587
+ },
12588
+ // --- 短調 (12調) ---
12589
+ key_Am: {
12590
+ id: "key_Am",
12591
+ name: "Am",
12592
+ label: "\u30A4\u77ED\u8ABF (Am)",
12593
+ mode: "minor",
12594
+ rootShift: 0,
12595
+ moodId: "mood_solemn",
12596
+ description: "\u67D4\u3089\u304B\u306A\u3001\u7269\u60B2\u3057\u3044\u3001\u656C\u8654\u306A\u3001\u7D20\u6734\u306A\u54C0\u6101"
12597
+ },
12598
+ key_Bbm: {
12599
+ id: "key_Bbm",
12600
+ name: "B\u266Dm",
12601
+ label: "\u5909\u30ED\u77ED\u8ABF (B\u266Dm)",
12602
+ mode: "minor",
12603
+ rootShift: 1,
12604
+ moodId: "mood_dark",
12605
+ description: "\u6050\u308D\u3057\u3044\u3001\u6697\u95C7\u3001\u5632\u308B\u3088\u3046\u306A\u3001\u4E0D\u6C17\u5473\uFF08\u5B30\u30A4\u77ED\u8ABF\u3068\u540C\u97F3\uFF09"
12606
+ },
12607
+ key_Bm: {
12608
+ id: "key_Bm",
12609
+ name: "Bm",
12610
+ label: "\u30ED\u77ED\u8ABF (Bm)",
12611
+ mode: "minor",
12612
+ rootShift: 2,
12613
+ moodId: "mood_melancholy",
12614
+ description: "\u5B64\u72EC\u306A\u3001\u6182\u9B31\u306A\u3001\u5FCD\u8010\u3001\u9759\u304B\u306A\u8AE6\u5FF5"
12615
+ },
12616
+ key_Cm: {
12617
+ id: "key_Cm",
12618
+ name: "Cm",
12619
+ label: "\u30CF\u77ED\u8ABF (Cm)",
12620
+ mode: "minor",
12621
+ rootShift: 3,
12622
+ moodId: "mood_plaintive",
12623
+ description: "\u7D14\u7C8B\u306B\u60B2\u3057\u3052\u306A\u3001\u604B\u308F\u305A\u3089\u3044\u306E\u3001\u60B2\u5287\u7684"
12624
+ },
12625
+ key_Csm: {
12626
+ id: "key_Csm",
12627
+ name: "C\u266Fm",
12628
+ label: "\u5B30\u30CF\u77ED\u8ABF (C\u266Fm)",
12629
+ mode: "minor",
12630
+ rootShift: 4,
12631
+ moodId: "mood_melancholy",
12632
+ description: "\u843D\u80C6\u3001\u6CE3\u304D\u53EB\u3093\u3060\u3001\u60B2\u6D99\u306E\u3001\u6DF1\u3044\u5606\u304D"
12633
+ },
12634
+ key_Dm: {
12635
+ id: "key_Dm",
12636
+ name: "Dm",
12637
+ label: "\u30CB\u77ED\u8ABF (Dm)",
12638
+ mode: "minor",
12639
+ rootShift: 5,
12640
+ moodId: "mood_solemn",
12641
+ description: "\u53B3\u7C9B\u306A\u3001\u656C\u8654\u306A\u3001\u601D\u7D22\u7684\u306A\u3001\u91CD\u539A\u306A\u7948\u308A"
12642
+ },
12643
+ key_Ebm: {
12644
+ id: "key_Ebm",
12645
+ name: "E\u266Dm",
12646
+ label: "\u5909\u30DB\u77ED\u8ABF (E\u266Dm)",
12647
+ mode: "minor",
12648
+ rootShift: 6,
12649
+ moodId: "mood_anxious",
12650
+ description: "\u6DF1\u3044\u82E6\u60A9\u3001\u5B9F\u5B58\u7684\u306A\u4E0D\u5B89\u3001\u6226\u6144\uFF08\u5B30\u30CB\u77ED\u8ABF\u3068\u540C\u97F3\uFF09"
12651
+ },
12652
+ key_Em: {
12653
+ id: "key_Em",
12654
+ name: "Em",
12655
+ label: "\u30DB\u77ED\u8ABF (Em)",
12656
+ mode: "minor",
12657
+ rootShift: -5,
12658
+ moodId: "mood_plaintive",
12659
+ description: "\u5F31\u3005\u3057\u3044\u3001\u306A\u307E\u3081\u304B\u3057\u3044\u3001\u843D\u3061\u7740\u304D\u306E\u306A\u3044\u3001\u7E4A\u7D30"
12660
+ },
12661
+ key_Fm: {
12662
+ id: "key_Fm",
12663
+ name: "Fm",
12664
+ label: "\u30D8\u77ED\u8ABF (Fm)",
12665
+ mode: "minor",
12666
+ rootShift: -4,
12667
+ moodId: "mood_plaintive",
12668
+ description: "\u307C\u3093\u3084\u308A\u3057\u305F\u3001\u7269\u60B2\u3057\u3044\u3001\u3057\u3081\u3084\u304B\u306A\u3001\u846C\u9001"
12669
+ },
12670
+ key_Fsm: {
12671
+ id: "key_Fsm",
12672
+ name: "F\u266Fm",
12673
+ label: "\u5B30\u30D8\u77ED\u8ABF (F\u266Fm)",
12674
+ mode: "minor",
12675
+ rootShift: -3,
12676
+ moodId: "mood_anxious",
12677
+ description: "\u9670\u6C17\u306A\u3001\u6FC0\u3057\u3044\u61A4\u308A\u3001\u6697\u3044\u60C5\u5FF5"
12678
+ },
12679
+ key_Gm: {
12680
+ id: "key_Gm",
12681
+ name: "Gm",
12682
+ label: "\u30C8\u77ED\u8ABF (Gm)",
12683
+ mode: "minor",
12684
+ rootShift: -2,
12685
+ moodId: "mood_anxious",
12686
+ description: "\u4E0D\u6E80\u3001\u4E0D\u5B89\u3001\u3084\u308B\u305B\u306A\u3055\u3001\u60B2\u75DB\u306A\u53EB\u3073"
12687
+ },
12688
+ key_Abm: {
12689
+ id: "key_Abm",
12690
+ name: "A\u266Dm",
12691
+ label: "\u5909\u30A4\u77ED\u8ABF (A\u266Dm)",
12692
+ mode: "minor",
12693
+ rootShift: -1,
12694
+ moodId: "mood_anxious",
12695
+ description: "\u4E0D\u670D\u306A\u3001\u5606\u304D\u306E\u3001\u6CE3\u304D\u53EB\u3093\u3060\uFF08\u5B30\u30C8\u77ED\u8ABF\u3068\u540C\u97F3\uFF09"
12696
+ }
12697
+ };
12698
+ var COMPOSE_MOOD_GROUPS = [
12531
12699
  {
12532
- grid: [12, 13, 14, 15],
12533
- drums: [
12534
- DRUM_KEYS.acousticSnare,
12535
- DRUM_KEYS.acousticSnare,
12536
- DRUM_KEYS.acousticSnare,
12537
- DRUM_KEYS.acousticSnare
12538
- ]
12700
+ id: "mood_happy",
12701
+ label: "\u559C\u3070\u3057\u3044\u30FB\u967D\u6C17\u306A\u66F2",
12702
+ description: "\u7121\u57A2\u306B\u559C\u3070\u3057\u3044\u3001\u7267\u6B4C\u7684\u3001\u611B\u306E\u544A\u767D\u3001\u98A8\u5909\u308F\u308A\u3067\u967D\u6C17",
12703
+ keyIds: ["key_C", "key_A", "key_Bb"]
12539
12704
  },
12540
- // タムで降りる
12541
12705
  {
12542
- grid: [8, 10, 12, 14],
12543
- drums: [
12544
- DRUM_KEYS.highTom,
12545
- DRUM_KEYS.highTom,
12546
- DRUM_KEYS.lowMidTom,
12547
- DRUM_KEYS.lowTom
12548
- ]
12706
+ id: "mood_triumphant",
12707
+ label: "\u52DD\u5229\u30FB\u529B\u5F37\u3044\u66F2",
12708
+ description: "\u610F\u6C17\u63DA\u3005\u3001\u52DD\u5229\u306E\u558A\u58F0\u3001\u56F0\u96E3\u306E\u6253\u7834\u3001\u5B89\u5835\u306E\u305F\u3081\u606F",
12709
+ keyIds: ["key_D", "key_Gb"]
12549
12710
  },
12550
- // スネア → タム
12551
12711
  {
12552
- grid: [8, 9, 10, 11, 12, 14],
12553
- drums: [
12554
- DRUM_KEYS.acousticSnare,
12555
- DRUM_KEYS.acousticSnare,
12556
- DRUM_KEYS.highTom,
12557
- DRUM_KEYS.highTom,
12558
- DRUM_KEYS.lowMidTom,
12559
- DRUM_KEYS.lowTom
12560
- ]
12712
+ id: "mood_fierce",
12713
+ label: "\u6FC0\u3057\u3044\u30FB\u8352\u3005\u3057\u3044\u66F2",
12714
+ description: "\u3051\u3093\u304B\u3063\u65E9\u3044\u3001\u6012\u308A\u72C2\u3063\u305F\u6C17\u6027\u306E\u8352\u3055\u3001\u3069\u304E\u3064\u304F\u731B\u70C8",
12715
+ keyIds: ["key_E", "key_F", "key_B"]
12561
12716
  },
12562
- // 短く締める
12563
12717
  {
12564
- grid: [14, 15],
12565
- drums: [DRUM_KEYS.acousticSnare, DRUM_KEYS.acousticSnare]
12718
+ id: "mood_solemn",
12719
+ label: "\u53B3\u7C9B\u30FB\u5E7B\u60F3\u7684\u306A\u66F2",
12720
+ description: "\u53B3\u7C9B\u3001\u5D07\u9AD8\u3001\u5E7B\u60F3\u3001\u656C\u8654\u3001\u601D\u7D22\u7684\u3001\u67D4\u3089\u304B\u306A\u7269\u60B2\u3057\u3055",
12721
+ keyIds: ["key_G", "key_Dm", "key_Am"]
12566
12722
  },
12567
- // 16分の連打から解放
12568
12723
  {
12569
- grid: [8, 9, 10, 11, 12, 13, 14, 15],
12570
- drums: [
12571
- DRUM_KEYS.acousticSnare,
12572
- DRUM_KEYS.acousticSnare,
12573
- DRUM_KEYS.acousticSnare,
12574
- DRUM_KEYS.acousticSnare,
12575
- DRUM_KEYS.highTom,
12576
- DRUM_KEYS.highTom,
12577
- DRUM_KEYS.lowMidTom,
12578
- DRUM_KEYS.lowTom
12579
- ]
12724
+ id: "mood_plaintive",
12725
+ label: "\u7269\u60B2\u3057\u3044\u30FB\u54C0\u6101\u306E\u66F2",
12726
+ description: "\u7D14\u7C8B\u306B\u60B2\u3057\u3052\u3001\u604B\u308F\u305A\u3089\u3044\u3001\u843D\u3061\u7740\u304D\u306E\u306A\u3044\u3001\u3057\u3081\u3084\u304B\u306A\u60B2\u54C0",
12727
+ keyIds: ["key_Cm", "key_Em", "key_Fm"]
12728
+ },
12729
+ {
12730
+ id: "mood_melancholy",
12731
+ label: "\u6182\u9B31\u30FB\u5B64\u72EC\u306A\u66F2",
12732
+ description: "\u60B2\u3057\u307F\u3001\u6182\u9B31\u3001\u5B64\u72EC\u3001\u5FCD\u8010\u3001\u843D\u80C6\u3001\u60B2\u6D99\u306E\u5606\u304D",
12733
+ keyIds: ["key_Db", "key_Bm", "key_Csm"]
12734
+ },
12735
+ {
12736
+ id: "mood_anxious",
12737
+ label: "\u4E0D\u5B89\u30FB\u82E6\u60A9\u306A\u66F2",
12738
+ description: "\u4E0D\u6E80\u3001\u4E0D\u5B89\u3001\u6DF1\u3044\u82E6\u60A9\u3001\u5B9F\u5B58\u7684\u4E0D\u5B89\u3001\u9670\u6C17\u306A\u61A4\u308A\u3001\u5606\u304D",
12739
+ keyIds: ["key_Gm", "key_Ebm", "key_Fsm", "key_Abm"]
12580
12740
  },
12581
- // 3連のタム回し(16分格子の外)
12582
12741
  {
12583
- grid: [12, 13.33, 14.67],
12584
- drums: [DRUM_KEYS.highTom, DRUM_KEYS.lowMidTom, DRUM_KEYS.lowTom]
12742
+ id: "mood_dark",
12743
+ label: "\u6697\u95C7\u30FB\u91CD\u539A\u306A\u66F2",
12744
+ description: "\u53B3\u3057\u3044\u611B\u3001\u6B7B\u3001\u6C38\u9060\u3001\u88C1\u304D\u3001\u6050\u308D\u3057\u3044\u6697\u95C7\u3001\u5632\u308A",
12745
+ keyIds: ["key_Eb", "key_Ab", "key_Bbm"]
12585
12746
  }
12586
12747
  ];
12587
- var pick = (items, rnd) => items[Math.floor(rnd() * items.length)];
12588
- var shuffleHats = (stepsPerBar, level) => {
12589
- const beat = stepsPerBar / 4;
12590
- const triplet = beat / 3;
12591
- const out = [];
12592
- for (let b = 0; b < 4; b++) {
12593
- out.push(b * beat);
12594
- out.push(b * beat + triplet * 2);
12595
- if (level === 2) out.push(b * beat + triplet);
12748
+ var MAJOR_KEY_IDS = Object.values(COMPOSE_KEYS).filter((k) => k.mode === "major").map((k) => k.id);
12749
+ var MINOR_KEY_IDS = Object.values(COMPOSE_KEYS).filter((k) => k.mode === "minor").map((k) => k.id);
12750
+ var pickItem = (list, rnd) => list[Math.floor(rnd() * list.length)];
12751
+ var resolveComposeKey = (choice, rnd = Math.random) => {
12752
+ const c = (choice ?? "").trim() || "any";
12753
+ if (COMPOSE_KEYS[c]) {
12754
+ const target2 = COMPOSE_KEYS[c];
12755
+ const mood2 = COMPOSE_MOOD_GROUPS.find((m) => m.id === target2.moodId);
12756
+ return {
12757
+ mode: target2.mode,
12758
+ rootShift: target2.rootShift,
12759
+ keyName: target2.name,
12760
+ keyLabel: target2.label,
12761
+ moodLabel: mood2?.label,
12762
+ description: target2.description
12763
+ };
12596
12764
  }
12597
- return out;
12598
- };
12599
- var composeDrumPattern = (options) => {
12600
- const { bars, stepsPerBar, rnd } = options;
12601
- const style = options.style ?? pick(
12602
- ["eight", "sixteen", "four", "shuffle", "ballad", "rock"],
12603
- rnd
12604
- );
12605
- const toStep = (g) => Math.round(g * stepsPerBar / 16) % stepsPerBar;
12606
- const fillA = pick(FILLS, rnd);
12607
- const fillB = pick(
12608
- FILLS.filter((f) => f !== fillA),
12609
- rnd
12610
- );
12611
- const openTail = rnd() < 0.6;
12612
- const useClap = style === "four" && rnd() < 0.5;
12613
- const levels = options.levels;
12614
- const crashSet = new Set(options.crashBars ?? [1, 9, 13]);
12615
- const fillSet = new Set(
12616
- options.fillBars ?? [4, 8, 12, bars - 1].filter((b) => b > 0)
12617
- );
12618
- const sectionLevel = (bar) => {
12619
- if (levels) return levels[bar - 1] ?? 1;
12620
- if (bar <= 4) return 0;
12621
- if (bar <= 8) return 1;
12622
- if (bar <= 12) return 2;
12623
- return 1;
12624
- };
12625
- const barPatterns = [];
12626
- for (let bar = 1; bar <= bars; bar++) {
12627
- const level = sectionLevel(bar);
12628
- const isLast = bar === bars;
12629
- const isFill = !isLast && fillSet.has(bar);
12630
- const hits = [];
12631
- if (crashSet.has(bar) || isLast)
12632
- hits.push({
12633
- step: 0,
12634
- pitch: DRUM_KEYS.crashCymbal1,
12635
- velocity: bar === 9 || isLast ? 1 : 0.85
12636
- });
12637
- if (isLast) {
12638
- hits.push({ step: 0, pitch: DRUM_KEYS.bassDrum1, velocity: 1 });
12639
- hits.sort((a, b) => a.step - b.step || a.pitch - b.pitch);
12640
- barPatterns.push(hits);
12641
- continue;
12642
- }
12643
- for (const g of KICKS[style][level])
12644
- hits.push({
12645
- step: toStep(g),
12646
- pitch: DRUM_KEYS.bassDrum1,
12647
- velocity: g === 0 ? 1 : 0.88
12648
- });
12649
- const snarePitch = style === "ballad" && level === 0 ? DRUM_KEYS.sideStick : DRUM_KEYS.acousticSnare;
12650
- for (const g of SNARES[style][level]) {
12651
- hits.push({ step: toStep(g), pitch: snarePitch, velocity: 0.95 });
12652
- if (useClap)
12653
- hits.push({
12654
- step: toStep(g),
12655
- pitch: DRUM_KEYS.handClap,
12656
- velocity: 0.7
12657
- });
12658
- }
12659
- if (level >= 1)
12660
- for (const g of GHOSTS[style])
12661
- hits.push({
12662
- step: toStep(g),
12663
- pitch: DRUM_KEYS.acousticSnare,
12664
- velocity: 0.3
12665
- });
12666
- const hatSteps = style === "shuffle" ? shuffleHats(stepsPerBar, level) : HIHATS[style][level].map(toStep);
12667
- const hatPitch = level === 2 && style !== "four" ? DRUM_KEYS.rideCymbal1 : DRUM_KEYS.closedHihat;
12668
- for (const step of hatSteps) {
12669
- if (isFill && step >= stepsPerBar / 2) continue;
12670
- const isDownbeat = step % (stepsPerBar / 4) === 0;
12671
- const isTail = openTail && step === toStep(14) && !isFill;
12672
- hits.push({
12673
- step,
12674
- pitch: isTail ? DRUM_KEYS.openHihat : hatPitch,
12675
- velocity: isTail ? 0.8 : isDownbeat ? 0.75 : 0.5
12676
- });
12677
- }
12678
- if (isFill) {
12679
- const fill = bar % 8 === 0 ? fillB : fillA;
12680
- fill.grid.forEach((g, i2) => {
12681
- hits.push({
12682
- step: toStep(g),
12683
- pitch: fill.drums[i2] ?? DRUM_KEYS.acousticSnare,
12684
- // 追い込みで少しずつ強くする
12685
- velocity: 0.7 + 0.28 * i2 / Math.max(1, fill.grid.length - 1)
12686
- });
12687
- });
12688
- }
12689
- hits.sort((a, b) => a.step - b.step || a.pitch - b.pitch);
12690
- barPatterns.push(hits);
12691
- }
12692
- const pattern = [];
12693
- let runStart = 1;
12694
- for (let bar = 1; bar <= bars; bar++) {
12695
- const same = bar < bars && JSON.stringify(barPatterns[bar]) === JSON.stringify(barPatterns[bar - 1]);
12696
- if (same) continue;
12697
- const instruction = {
12698
- ranges: [[runStart, bar]],
12699
- patternBars: 1,
12700
- pattern: barPatterns[runStart - 1]
12765
+ const mood = COMPOSE_MOOD_GROUPS.find((m) => m.id === c);
12766
+ if (mood) {
12767
+ const pickedKeyId2 = pickItem(mood.keyIds, rnd);
12768
+ const target2 = COMPOSE_KEYS[pickedKeyId2];
12769
+ return {
12770
+ mode: target2.mode,
12771
+ rootShift: target2.rootShift,
12772
+ keyName: target2.name,
12773
+ keyLabel: target2.label,
12774
+ moodLabel: mood.label,
12775
+ description: target2.description
12701
12776
  };
12702
- pattern.push(instruction);
12703
- runStart = bar + 1;
12704
12777
  }
12705
- const variety = new Set(barPatterns.map((p) => JSON.stringify(p))).size;
12778
+ if (c === "major") {
12779
+ const pickedKeyId2 = pickItem(MAJOR_KEY_IDS, rnd);
12780
+ const target2 = COMPOSE_KEYS[pickedKeyId2];
12781
+ const targetMood2 = COMPOSE_MOOD_GROUPS.find((m) => m.id === target2.moodId);
12782
+ return {
12783
+ mode: "major",
12784
+ rootShift: target2.rootShift,
12785
+ keyName: target2.name,
12786
+ keyLabel: target2.label,
12787
+ moodLabel: targetMood2?.label,
12788
+ description: target2.description
12789
+ };
12790
+ }
12791
+ if (c === "minor") {
12792
+ const pickedKeyId2 = pickItem(MINOR_KEY_IDS, rnd);
12793
+ const target2 = COMPOSE_KEYS[pickedKeyId2];
12794
+ const targetMood2 = COMPOSE_MOOD_GROUPS.find((m) => m.id === target2.moodId);
12795
+ return {
12796
+ mode: "minor",
12797
+ rootShift: target2.rootShift,
12798
+ keyName: target2.name,
12799
+ keyLabel: target2.label,
12800
+ moodLabel: targetMood2?.label,
12801
+ description: target2.description
12802
+ };
12803
+ }
12804
+ const allKeyIds = Object.keys(COMPOSE_KEYS);
12805
+ const pickedKeyId = pickItem(allKeyIds, rnd);
12806
+ const target = COMPOSE_KEYS[pickedKeyId];
12807
+ const targetMood = COMPOSE_MOOD_GROUPS.find((m) => m.id === target.moodId);
12706
12808
  return {
12707
- style,
12708
- variety,
12709
- def: { label: `\u4F5C\u66F2\uFF08${DRUM_STYLE_LABELS[style]}\uFF09`, pattern }
12710
- };
12809
+ mode: void 0,
12810
+ // コード進行の長短制約は掛けず従来通りランダム
12811
+ rootShift: target.rootShift,
12812
+ keyName: target.name,
12813
+ keyLabel: target.label,
12814
+ moodLabel: targetMood?.label,
12815
+ description: target.description
12816
+ };
12817
+ };
12818
+ var getComposeKeyDescription = (choice) => {
12819
+ if (!choice || choice === "any") return "\u516824\u8ABF\u304B\u3089\u30E9\u30F3\u30C0\u30E0\u306B\u6C7A\u5B9A\u3057\u307E\u3059";
12820
+ if (choice === "major") return "12\u306E\u9577\u8ABF\u306E\u4E2D\u304B\u3089\u30E9\u30F3\u30C0\u30E0\u306B\u62BD\u9078\u3057\u307E\u3059";
12821
+ if (choice === "minor") return "12\u306E\u77ED\u8ABF\u306E\u4E2D\u304B\u3089\u30E9\u30F3\u30C0\u30E0\u306B\u62BD\u9078\u3057\u307E\u3059";
12822
+ const mood = COMPOSE_MOOD_GROUPS.find((m) => m.id === choice);
12823
+ if (mood) return `${mood.label}\uFF1A${mood.description}`;
12824
+ const target = COMPOSE_KEYS[choice];
12825
+ if (target) return `${target.label}\uFF1A${target.description}`;
12826
+ return "";
12711
12827
  };
12712
12828
 
12713
12829
  // src/compose-metrics.ts
@@ -13139,7 +13255,6 @@ var transposeChordName = (chord, shift) => {
13139
13255
  return names[((pc + normShift) % 12 + 12) % 12];
13140
13256
  });
13141
13257
  };
13142
- var ROOT_SHIFTS = [0, 1, 2, 3, 4, 5, 6, -1, -2, -3, -4, -5];
13143
13258
  var BPM_CHOICES = [
13144
13259
  112,
13145
13260
  120,
@@ -13761,7 +13876,7 @@ var MOTIF_ARCHETYPES = [
13761
13876
  [0, 5, 4, 2, 0]
13762
13877
  // 6度跳躍からの大きな下降
13763
13878
  ];
13764
- var pick2 = (items, rnd) => items[Math.floor(rnd() * items.length)];
13879
+ var pick = (items, rnd) => items[Math.floor(rnd() * items.length)];
13765
13880
  var durationEntropy = (durations) => {
13766
13881
  if (durations.length === 0) return 0;
13767
13882
  const counts = /* @__PURE__ */ new Map();
@@ -13829,7 +13944,7 @@ var leapTarget = (from, tones, rnd) => {
13829
13944
  ...octaves,
13830
13945
  ...near.slice(0, Math.max(2, Math.ceil(near.length / 2)))
13831
13946
  ];
13832
- return pick2(weighted.length > 0 ? weighted : candidates, rnd);
13947
+ return pick(weighted.length > 0 ? weighted : candidates, rnd);
13833
13948
  };
13834
13949
  var landOn = (degrees, scaleIndex) => {
13835
13950
  if (degrees.length === 0) return;
@@ -13858,7 +13973,7 @@ var barDegrees = (role, slots, tones, style, motifContour, startDegree, contourO
13858
13973
  const noteCount = slots.length;
13859
13974
  const out = [];
13860
13975
  if (role === "motif" || role === "sequence" || role === "climax" || role === "answer") {
13861
- const shift = (role === "sequence" ? pick2([-2, -1, 1, 2], rnd) : role === "climax" ? 5 : 0) + repeatShift;
13976
+ const shift = (role === "sequence" ? pick([-2, -1, 1, 2], rnd) : role === "climax" ? 5 : 0) + repeatShift;
13862
13977
  if (style.pentatonicMotif) {
13863
13978
  const startPenta = degreeToPenta(startDegree);
13864
13979
  for (let i2 = 0; i2 < noteCount; i2++)
@@ -13924,7 +14039,7 @@ var barDegrees = (role, slots, tones, style, motifContour, startDegree, contourO
13924
14039
  }
13925
14040
  return out;
13926
14041
  }
13927
- const span = pick2([2, 3, 4], rnd);
14042
+ const span = pick([2, 3, 4], rnd);
13928
14043
  const dir = style.stepShape === "descend" ? -1 : style.stepShape === "ascend" ? 1 : rnd() < 0.5 ? 1 : -1;
13929
14044
  for (let i2 = 0; i2 < noteCount; i2++) {
13930
14045
  const t = noteCount === 1 ? 0 : i2 / (noteCount - 1);
@@ -14152,7 +14267,7 @@ var applyChromatic = (pitches, fifths, slots, tones, opts) => {
14152
14267
  lastAltered = i2;
14153
14268
  }
14154
14269
  };
14155
- var draw = (options, rnd) => {
14270
+ var draw = (options, resolvedKey, rnd) => {
14156
14271
  const stepsPerBar = options.stepsPerBar;
14157
14272
  const edo = options.edo === 31 ? 31 : 12;
14158
14273
  const scaleStep = (v) => Math.max(1, Math.round(Math.abs(v) * stepsPerBar / BASE_STEPS_PER_BAR)) * Math.sign(v);
@@ -14176,12 +14291,12 @@ var draw = (options, rnd) => {
14176
14291
  const sectionPlan = buildSectionPlan(options.sections ?? DEFAULT_SECTIONS);
14177
14292
  const totalBars = sectionPlan.reduce((sum, s) => sum + s.bars, 0);
14178
14293
  if (rnd() < 0.35) {
14179
- const modType = pick2(
14294
+ const modType = pick(
14180
14295
  ["chorus_up", "chorus_up", "prechorus_down"],
14181
14296
  rnd
14182
14297
  );
14183
14298
  if (modType === "chorus_up") {
14184
- const shift = pick2([1, 1, 2], rnd);
14299
+ const shift = pick([1, 1, 2], rnd);
14185
14300
  let lastChorusIdx = -1;
14186
14301
  for (let i2 = 0; i2 < sectionPlan.length; i2++) {
14187
14302
  if (sectionPlan[i2].kind === "chorus") lastChorusIdx = i2;
@@ -14192,7 +14307,7 @@ var draw = (options, rnd) => {
14192
14307
  }
14193
14308
  }
14194
14309
  } else {
14195
- const shift = pick2([-2, 3], rnd);
14310
+ const shift = pick([-2, 3], rnd);
14196
14311
  for (const s of sectionPlan) {
14197
14312
  if (s.kind === "prechorus") s.keyShift = shift;
14198
14313
  }
@@ -14204,14 +14319,15 @@ var draw = (options, rnd) => {
14204
14319
  barKeyShift[b] = s.keyShift;
14205
14320
  }
14206
14321
  }
14207
- const progA = pick2(SECTION_A_PROGRESSIONS, rnd);
14322
+ const progAPool = resolvedKey.mode === "major" ? SECTION_A_PROGRESSIONS.filter((p) => !p[0].startsWith("Am")) : resolvedKey.mode === "minor" ? SECTION_A_PROGRESSIONS.filter((p) => p[0].startsWith("Am")) : SECTION_A_PROGRESSIONS;
14323
+ const progA = pick(progAPool, rnd);
14208
14324
  const progBPool = SECTION_B_PROGRESSIONS.filter(
14209
14325
  (p) => p.join("|") !== progA.join("|")
14210
14326
  );
14211
- const progB = pick2(progBPool, rnd);
14327
+ const progB = pick(progBPool, rnd);
14212
14328
  const tonic = progA[0].startsWith("Am") ? "Am" : "C";
14213
- const progHalf = pick2(SECTION_A2_DERIVATIONS, rnd)(progA);
14214
- const progFull = pick2(SECTION_A3_DERIVATIONS, rnd)(progA, tonic);
14329
+ const progHalf = pick(SECTION_A2_DERIVATIONS, rnd)(progA);
14330
+ const progFull = pick(SECTION_A3_DERIVATIONS, rnd)(progA, tonic);
14215
14331
  const progression = [];
14216
14332
  for (const section of sectionPlan) {
14217
14333
  const base = section.spec.progression === "b" ? progB : progA;
@@ -14229,9 +14345,9 @@ var draw = (options, rnd) => {
14229
14345
  }
14230
14346
  progression.length = totalBars;
14231
14347
  const chordProgression = progression.map((chord, bar) => transposeChordName(chord, barKeyShift[bar])).join("|");
14232
- const chordPattern = pick2(CHORD_PATTERNS, rnd);
14233
- const rootShift = pick2(ROOT_SHIFTS, rnd);
14234
- const bpm = pick2(BPM_CHOICES, rnd);
14348
+ const chordPattern = pick(CHORD_PATTERNS, rnd);
14349
+ const rootShift = resolvedKey.rootShift;
14350
+ const bpm = pick(BPM_CHOICES, rnd);
14235
14351
  const sourceOf = (kind) => kind === "chorus" || kind === "interlude" ? "b" : kind === "prechorus" ? "a2" : "a";
14236
14352
  const units2 = [];
14237
14353
  for (const section of sectionPlan) {
@@ -14280,19 +14396,19 @@ var draw = (options, rnd) => {
14280
14396
  return null;
14281
14397
  };
14282
14398
  const style = {
14283
- groove: pick2(["eighth", "sixteenth"], rnd),
14284
- arcPeriod: pick2([4, 8, 8, 16], rnd),
14285
- arcPhase: pick2([0, 1, 2], rnd),
14399
+ groove: pick(["eighth", "sixteenth"], rnd),
14400
+ arcPeriod: pick([4, 8, 8, 16], rnd),
14401
+ arcPhase: pick([0, 1, 2], rnd),
14286
14402
  arcAmp: 3 + rnd() * 4,
14287
14403
  octaveAffinity: 0.14 + rnd() * 0.2,
14288
14404
  pentatonicMotif: rnd() < 0.55,
14289
- runShape: pick2(["scale", "turn", "broken", "zigzag"], rnd),
14290
- stepShape: pick2(
14405
+ runShape: pick(["scale", "turn", "broken", "zigzag"], rnd),
14406
+ stepShape: pick(
14291
14407
  ["arch", "valley", "ascend", "descend", "wave", "pivot"],
14292
14408
  rnd
14293
14409
  ),
14294
- holdShape: pick2(["long", "third", "neighbor"], rnd),
14295
- cadenceShape: pick2(
14410
+ holdShape: pick(["long", "third", "neighbor"], rnd),
14411
+ cadenceShape: pick(
14296
14412
  ["descend", "five-three-one", "leap-up", "hold-tonic"],
14297
14413
  rnd
14298
14414
  ),
@@ -14302,7 +14418,7 @@ var draw = (options, rnd) => {
14302
14418
  // 全曲に掛けると「どの曲も同じ味付け」になる)。
14303
14419
  chromaticAffinity: rnd() < 0.25 ? 0 : 0.15 + rnd() * 0.45,
14304
14420
  barHeadWeight: rnd() < 0.65 ? 3 : 2,
14305
- bassStyle: pick2(
14421
+ bassStyle: pick(
14306
14422
  [
14307
14423
  "quarter",
14308
14424
  "alternate",
@@ -14323,8 +14439,8 @@ var draw = (options, rnd) => {
14323
14439
  // パッド・保続音・付点2分は「置いただけの音」になりやすく、実際
14324
14440
  // サブメロの音の38%が全音符1つ(1.5音/小節)まで薄くなっていた。
14325
14441
  // それらは終止の小節でだけ使う逃げ道に降格する。
14326
- subStyle: pick2(["harmony", "harmony", "counter"], rnd),
14327
- subInterval: pick2([3, 4, 8, 9], rnd)
14442
+ subStyle: pick(["harmony", "harmony", "counter"], rnd),
14443
+ subInterval: pick([3, 4, 8, 9], rnd)
14328
14444
  };
14329
14445
  const motifPool = groovyCells(MOTIF_CELLS, style.groove, rnd);
14330
14446
  const targetNotesPerBar = 5.5 + rnd() * 2.5;
@@ -14341,9 +14457,9 @@ var draw = (options, rnd) => {
14341
14457
  };
14342
14458
  const cellDistance = (c) => Math.abs(cellNotes(c) - targetNotesPerBar) + Math.abs(cellRest(c) - targetRestRatio) * 8;
14343
14459
  const pickCell = (pool) => {
14344
- let best = pick2(pool, rnd);
14460
+ let best = pick(pool, rnd);
14345
14461
  for (let i2 = 0; i2 < 2; i2++) {
14346
- const c = pick2(pool, rnd);
14462
+ const c = pick(pool, rnd);
14347
14463
  if (cellDistance(c) < cellDistance(best)) best = c;
14348
14464
  }
14349
14465
  return best;
@@ -14366,7 +14482,7 @@ var draw = (options, rnd) => {
14366
14482
  const motifBVar = pickCell(
14367
14483
  motifPool.filter((c) => c !== motifB && c !== motifB2)
14368
14484
  );
14369
- const buildUpCell = pick2(
14485
+ const buildUpCell = pick(
14370
14486
  [
14371
14487
  {
14372
14488
  value: [EIGHTH, EIGHTH, EIGHTH, EIGHTH, QUARTER, -QUARTER],
@@ -14387,7 +14503,7 @@ var draw = (options, rnd) => {
14387
14503
  ],
14388
14504
  rnd
14389
14505
  );
14390
- const counterRhythm = pick2(
14506
+ const counterRhythm = pick(
14391
14507
  groovyCells(
14392
14508
  RHYTHM_CELLS.filter((c) => c.density === "medium"),
14393
14509
  style.groove,
@@ -14395,7 +14511,7 @@ var draw = (options, rnd) => {
14395
14511
  ),
14396
14512
  rnd
14397
14513
  ).value;
14398
- const breathCell = pick2(
14514
+ const breathCell = pick(
14399
14515
  RHYTHM_CELLS.filter(
14400
14516
  (c) => c.density === "sparse" && c.value.some((v) => v < 0)
14401
14517
  ),
@@ -14432,14 +14548,14 @@ var draw = (options, rnd) => {
14432
14548
  barRhythms.push(scaleCell(cell.value));
14433
14549
  }
14434
14550
  const motifNoteCount = motifCell.value.filter((v) => v > 0).length + motifCell2.value.filter((v) => v > 0).length;
14435
- const archetype = pick2(MOTIF_ARCHETYPES, rnd);
14551
+ const archetype = pick(MOTIF_ARCHETYPES, rnd);
14436
14552
  const motifContour = [];
14437
14553
  let baseDegree = 0;
14438
14554
  for (let i2 = 0; i2 < motifNoteCount; i2++) {
14439
14555
  const idx = i2 % archetype.length;
14440
14556
  if (i2 > 0 && idx === 0) {
14441
14557
  const lastVal = motifContour[i2 - 1];
14442
- const connectShift = pick2([-1, 0, 1], rnd);
14558
+ const connectShift = pick([-1, 0, 1], rnd);
14443
14559
  baseDegree = lastVal + connectShift - archetype[0];
14444
14560
  }
14445
14561
  motifContour.push(baseDegree + archetype[idx]);
@@ -14457,12 +14573,12 @@ var draw = (options, rnd) => {
14457
14573
  let intervals = 0;
14458
14574
  let chromaticNotes = 0;
14459
14575
  let sungBars = 0;
14460
- let prevSemi = pick2([60, 64, 65, 67, 69, 72, 74, 76], rnd);
14576
+ let prevSemi = pick([60, 64, 65, 67, 69, 72, 74, 76], rnd);
14461
14577
  const pickFigure = (gapSteps, scale) => {
14462
14578
  const fits = ANSWER_FIGURES.filter(
14463
14579
  (f) => f.reduce((sum, v) => sum + scale(Math.abs(v)), 0) <= gapSteps
14464
14580
  );
14465
- return fits.length === 0 ? null : pick2(fits, rnd);
14581
+ return fits.length === 0 ? null : pick(fits, rnd);
14466
14582
  };
14467
14583
  const melodyGaps = (slots) => {
14468
14584
  const gaps = [];
@@ -14503,7 +14619,7 @@ var draw = (options, rnd) => {
14503
14619
  ).semi;
14504
14620
  const contourOffset = barInUnit(bar) === 0 ? 0 : barRhythms[bar - 1].filter((v) => v > 0).length;
14505
14621
  const prevRole = bar > 0 ? barRoles[bar - 1] : null;
14506
- const repeatShift = role === "motif" && (prevRole === "motif" || prevRole === "sequence" || prevRole === "climax") ? pick2([-2, -1, 1, 2], rnd) : 0;
14622
+ const repeatShift = role === "motif" && (prevRole === "motif" || prevRole === "sequence" || prevRole === "climax") ? pick([-2, -1, 1, 2], rnd) : 0;
14507
14623
  const degrees = barDegrees(
14508
14624
  role,
14509
14625
  slots,
@@ -14799,6 +14915,9 @@ var draw = (options, rnd) => {
14799
14915
  chordProgression,
14800
14916
  chordPattern,
14801
14917
  rootShift,
14918
+ keyName: resolvedKey.keyName,
14919
+ keyLabel: resolvedKey.keyLabel,
14920
+ moodLabel: resolvedKey.moodLabel,
14802
14921
  bpm,
14803
14922
  sections: sectionPlan,
14804
14923
  bars: totalBars,
@@ -14913,12 +15032,13 @@ var composeSong = (options) => {
14913
15032
  const rnd = options.random ?? Math.random;
14914
15033
  const recent = options.recent ?? [];
14915
15034
  const count = Math.max(1, options.drawCount ?? DRAW_COUNT);
15035
+ const resolvedKey = resolveComposeKey(options.baseKey, rnd);
14916
15036
  let best = null;
14917
15037
  let bestScore = Number.NEGATIVE_INFINITY;
14918
15038
  let bestIsValid = false;
14919
15039
  let rejected = 0;
14920
15040
  for (let attempt = 1; attempt <= count; attempt++) {
14921
- const d = draw(options, rnd);
15041
+ const d = draw(options, resolvedKey, rnd);
14922
15042
  const { stats, ok } = evaluate(d, recent);
14923
15043
  if (!ok) rejected++;
14924
15044
  const better = ok === bestIsValid ? stats.score > bestScore : ok;
@@ -14928,10 +15048,13 @@ var composeSong = (options) => {
14928
15048
  best = {
14929
15049
  // ドラムは勝った候補にだけ後から付ける(メロディに依存しないので
14930
15050
  // 候補ごとに引いても採点は動かず、40本ぶん無駄になる)。
14931
- drums: null,
15051
+ drum: "",
14932
15052
  chordProgression: d.chordProgression,
14933
15053
  chordPattern: d.chordPattern,
14934
15054
  rootShift: d.rootShift,
15055
+ keyName: d.keyName,
15056
+ keyLabel: d.keyLabel,
15057
+ moodLabel: d.moodLabel,
14935
15058
  bpm: d.bpm,
14936
15059
  sections: d.sections,
14937
15060
  bars: d.bars,
@@ -14944,32 +15067,14 @@ var composeSong = (options) => {
14944
15067
  const result = best;
14945
15068
  result.stats.attempts = count;
14946
15069
  result.stats.rejected = rejected;
14947
- const levels = new Array(result.bars).fill(1);
14948
- const crashBars = [];
14949
- const fillBars = [];
14950
- for (const section of result.sections) {
14951
- for (let i2 = 0; i2 < section.bars; i2++)
14952
- levels[section.startBar + i2] = section.spec.drumLevel;
14953
- crashBars.push(section.startBar + 1);
14954
- const fill = section.startBar + section.bars - 1;
14955
- if (fill > 0) fillBars.push(fill);
14956
- }
14957
- result.drums = composeDrumPattern({
14958
- bars: result.bars,
14959
- stepsPerBar: options.stepsPerBar,
14960
- rnd,
14961
- style: pickDrumStyle(result, rnd),
14962
- levels,
14963
- crashBars,
14964
- fillBars
14965
- });
15070
+ result.drum = pickBuiltinDrum(result, rnd);
14966
15071
  return result;
14967
15072
  };
14968
- var pickDrumStyle = (song, rnd) => {
15073
+ var pickBuiltinDrum = (song, rnd) => {
14969
15074
  const eighth = BASE_STEPS_PER_BAR / 8;
14970
15075
  const short = song.melody.filter((n) => n.durationSteps <= eighth).length / Math.max(1, song.melody.length);
14971
- const pool = song.bpm >= 150 ? ["four", "rock", "sixteen"] : short >= 0.6 ? ["sixteen", "four", "rock"] : song.bpm <= 124 && short < 0.4 ? ["ballad", "eight", "ballad"] : ["eight", "rock", "four", "sixteen"];
14972
- return pick2(pool, rnd);
15076
+ const pool = song.bpm >= 150 ? ["4beat", "dance", "16beat", "disco"] : short >= 0.6 ? ["16beat", "dance", "4beat", "disco"] : song.bpm <= 110 && short < 0.4 ? ["bossa", "8beat"] : ["8beat", "4beat", "16beat"];
15077
+ return pick(pool, rnd);
14973
15078
  };
14974
15079
  var LYRIC_WORDS = [
14975
15080
  "\u3042\u3055",
@@ -15017,7 +15122,7 @@ var composeLyrics = (melody, options) => {
15017
15122
  const quarter = stepsPerBar / 4;
15018
15123
  let buffer = [];
15019
15124
  const nextKana = () => {
15020
- if (buffer.length === 0) buffer = [...pick2(LYRIC_WORDS, rnd)];
15125
+ if (buffer.length === 0) buffer = [...pick(LYRIC_WORDS, rnd)];
15021
15126
  return buffer.shift();
15022
15127
  };
15023
15128
  const out = [];
@@ -15382,6 +15487,56 @@ var buildUI = (target, options) => {
15382
15487
  <span class="dtm-grow"></span>
15383
15488
  <span class="dtm-hint" data-dtm="compose-sections-len"></span>
15384
15489
  </div>
15490
+ <div class="dtm-row ${showCompose ? "" : "dtm-hidden"}" data-dtm="compose-key-row">
15491
+ <span class="dtm-label">\u30D9\u30FC\u30B9\u8ABF</span>
15492
+ <select class="dtm-select" data-dtm="compose-key" title="\u81EA\u52D5\u4F5C\u66F2\u306E\u30D9\u30FC\u30B9\u3068\u306A\u308B\u8ABF\u3084\u96F0\u56F2\u6C17\u3092\u9078\u3073\u307E\u3059">
15493
+ <option value="any" title="\u516824\u8ABF\u304B\u3089\u30E9\u30F3\u30C0\u30E0\u306B\u6C7A\u5B9A\u3057\u307E\u3059">\u5E0C\u671B\u306A\u3057</option>
15494
+ <optgroup label="\u57FA\u672C">
15495
+ <option value="major" title="12\u306E\u9577\u8ABF\u306E\u4E2D\u304B\u3089\u30E9\u30F3\u30C0\u30E0\u306B\u62BD\u9078\u3057\u307E\u3059">\u9577\u8ABF</option>
15496
+ <option value="minor" title="12\u306E\u77ED\u8ABF\u306E\u4E2D\u304B\u3089\u30E9\u30F3\u30C0\u30E0\u306B\u62BD\u9078\u3057\u307E\u3059">\u77ED\u8ABF</option>
15497
+ </optgroup>
15498
+ <optgroup label="\u96F0\u56F2\u6C17\u304B\u3089\u9078\u3076\uFF08\u62BD\u9078\uFF09">
15499
+ <option value="mood_happy" title="\u30CF\u9577\u8ABF\u30FB\u30A4\u9577\u8ABF\u30FB\u5909\u30ED\u9577\u8ABF\u304B\u3089\u62BD\u9078\uFF08\u7121\u57A2\u306B\u559C\u3070\u3057\u3044\u3001\u7267\u6B4C\u7684\u3001\u967D\u6C17\uFF09">\u559C\u3070\u3057\u3044\u30FB\u967D\u6C17\u306A\u66F2</option>
15500
+ <option value="mood_triumphant" title="\u30CB\u9577\u8ABF\u30FB\u5909\u30C8\u9577\u8ABF\u304B\u3089\u62BD\u9078\uFF08\u610F\u6C17\u63DA\u3005\u3001\u52DD\u5229\u306E\u558A\u58F0\u3001\u56F0\u96E3\u6253\u7834\u3001\u5B89\u5835\uFF09">\u52DD\u5229\u30FB\u529B\u5F37\u3044\u66F2</option>
15501
+ <option value="mood_fierce" title="\u30DB\u9577\u8ABF\u30FB\u30D8\u9577\u8ABF\u30FB\u30ED\u9577\u8ABF\u304B\u3089\u62BD\u9078\uFF08\u3051\u3093\u304B\u3063\u65E9\u3044\u3001\u6012\u308A\u72C2\u3063\u305F\u8352\u3005\u3057\u3055\u3001\u3069\u304E\u3064\u304F\u731B\u70C8\uFF09">\u6FC0\u3057\u3044\u30FB\u8352\u3005\u3057\u3044\u66F2</option>
15502
+ <option value="mood_solemn" title="\u30C8\u9577\u8ABF\u30FB\u30CB\u77ED\u8ABF\u30FB\u30A4\u77ED\u8ABF\u304B\u3089\u62BD\u9078\uFF08\u53B3\u7C9B\u3001\u5D07\u9AD8\u3001\u5E7B\u60F3\u3001\u656C\u8654\u3001\u601D\u7D22\u7684\uFF09">\u53B3\u7C9B\u30FB\u5E7B\u60F3\u7684\u306A\u66F2</option>
15503
+ <option value="mood_plaintive" title="\u30CF\u77ED\u8ABF\u30FB\u30DB\u77ED\u8ABF\u30FB\u30D8\u77ED\u8ABF\u304B\u3089\u62BD\u9078\uFF08\u7D14\u7C8B\u306B\u60B2\u3057\u3052\u3001\u604B\u308F\u305A\u3089\u3044\u3001\u843D\u3061\u7740\u304D\u306E\u306A\u3044\u3001\u7269\u60B2\u3057\u3044\u54C0\u6101\uFF09">\u7269\u60B2\u3057\u3044\u30FB\u54C0\u6101\u306E\u66F2</option>
15504
+ <option value="mood_melancholy" title="\u5909\u30CB\u9577\u8ABF\u30FB\u30ED\u77ED\u8ABF\u30FB\u5B30\u30CF\u77ED\u8ABF\u304B\u3089\u62BD\u9078\uFF08\u60B2\u3057\u307F\u3001\u6182\u9B31\u3001\u5B64\u72EC\u3001\u5FCD\u8010\u3001\u843D\u80C6\u3001\u60B2\u6D99\uFF09">\u6182\u9B31\u30FB\u5B64\u72EC\u306A\u66F2</option>
15505
+ <option value="mood_anxious" title="\u30C8\u77ED\u8ABF\u30FB\u5909\u30DB\u77ED\u8ABF\u30FB\u5B30\u30D8\u77ED\u8ABF\u30FB\u5909\u30A4\u77ED\u8ABF\u304B\u3089\u62BD\u9078\uFF08\u4E0D\u6E80\u3001\u4E0D\u5B89\u3001\u6DF1\u3044\u82E6\u60A9\u3001\u9670\u6C17\u306A\u61A4\u308A\uFF09">\u4E0D\u5B89\u30FB\u82E6\u60A9\u306A\u66F2</option>
15506
+ <option value="mood_dark" title="\u5909\u30DB\u9577\u8ABF\u30FB\u5909\u30A4\u9577\u8ABF\u30FB\u5909\u30ED\u77ED\u8ABF\u304B\u3089\u62BD\u9078\uFF08\u53B3\u3057\u3044\u611B\u3001\u6B7B\u3001\u6C38\u9060\u3001\u88C1\u304D\u3001\u6697\u95C7\u3001\u6050\u308D\u3057\u3044\u5632\u308A\uFF09">\u6697\u95C7\u30FB\u91CD\u539A\u306A\u66F2</option>
15507
+ </optgroup>
15508
+ <optgroup label="\u9577\u8ABF\uFF08\u500B\u5225\u6307\u5B9A\uFF09">
15509
+ <option value="key_C" title="\u7121\u57A2\u306B\u559C\u3070\u3057\u3044\u3001\u7D14\u7C8B\u3001\u7D20\u6734\u3001\u51FA\u767A">\u30CF\u9577\u8ABF (C)</option>
15510
+ <option value="key_Db" title="\u60B2\u3057\u307F\u3001\u6182\u9B31\u306A\u3001\u7518\u7F8E\u306A\u611F\u50B7">\u5909\u30CB\u9577\u8ABF (D\u266D)</option>
15511
+ <option value="key_D" title="\u610F\u6C17\u63DA\u3005\u3068\u3057\u305F\u3001\u52DD\u5229\u306E\u3001\u558A\u58F0">\u30CB\u9577\u8ABF (D)</option>
15512
+ <option value="key_Eb" title="\u53B3\u3057\u3044\u3001\u304D\u3064\u3044\u3001\u305D\u308C\u3067\u3044\u3066\u611B\u306B\u6E80\u3061\u305F">\u5909\u30DB\u9577\u8ABF (E\u266D)</option>
15513
+ <option value="key_E" title="\u3051\u3093\u304B\u3063\u65E9\u3044\u3001\u8352\u3005\u3057\u3044\u3001\u8F1D\u304B\u3057\u3044\u60C5\u71B1">\u30DB\u9577\u8ABF (E)</option>
15514
+ <option value="key_F" title="\u6012\u308A\u72C2\u3063\u305F\u3001\u6C17\u6027\u306E\u8352\u3044\u3001\u4E00\u6642\u7684\u306A\u60B2\u5606">\u30D8\u9577\u8ABF (F)</option>
15515
+ <option value="key_Gb" title="\u56F0\u96E3\u306E\u6253\u7834\u3001\u5B89\u5835\u306E\u305F\u3081\u606F\u3001\u51F1\u65CB">\u5909\u30C8\u9577\u8ABF (G\u266D)</option>
15516
+ <option value="key_G" title="\u53B3\u7C9B\u306A\u3001\u5D07\u9AD8\u306A\u3001\u5E7B\u60F3\u3001\u8AA0\u5B9F">\u30C8\u9577\u8ABF (G)</option>
15517
+ <option value="key_Ab" title="\u6B7B\u3001\u6C38\u9060\u3001\u88C1\u304D\u3001\u6DF1\u9060\u306A\u7791\u60F3">\u5909\u30A4\u9577\u8ABF (A\u266D)</option>
15518
+ <option value="key_A" title="\u3046\u308C\u3057\u3044\u3001\u7267\u6B4C\u7684\u306A\u3001\u611B\u306E\u544A\u767D">\u30A4\u9577\u8ABF (A)</option>
15519
+ <option value="key_Bb" title="\u559C\u3070\u3057\u3044\u3001\u98A8\u5909\u308F\u308A\u306A\u3001\u967D\u6C17\u306A\u3001\u8EFD\u5FEB">\u5909\u30ED\u9577\u8ABF (B\u266D)</option>
15520
+ <option value="key_B" title="\u3069\u304E\u3064\u3044\u3001\u5F37\u70C8\u306A\u3001\u8352\u3063\u307D\u3044\u3001\u731B\u70C8">\u30ED\u9577\u8ABF (B)</option>
15521
+ </optgroup>
15522
+ <optgroup label="\u77ED\u8ABF\uFF08\u500B\u5225\u6307\u5B9A\uFF09">
15523
+ <option value="key_Am" title="\u67D4\u3089\u304B\u306A\u3001\u7269\u60B2\u3057\u3044\u3001\u656C\u8654\u306A\u3001\u7D20\u6734\u306A\u54C0\u6101">\u30A4\u77ED\u8ABF (Am)</option>
15524
+ <option value="key_Bbm" title="\u6050\u308D\u3057\u3044\u3001\u6697\u95C7\u3001\u5632\u308B\u3088\u3046\u306A\u3001\u4E0D\u6C17\u5473">\u5909\u30ED\u77ED\u8ABF (B\u266Dm)</option>
15525
+ <option value="key_Bm" title="\u5B64\u72EC\u306A\u3001\u6182\u9B31\u306A\u3001\u5FCD\u8010\u3001\u9759\u304B\u306A\u8AE6\u5FF5">\u30ED\u77ED\u8ABF (Bm)</option>
15526
+ <option value="key_Cm" title="\u7D14\u7C8B\u306B\u60B2\u3057\u3052\u306A\u3001\u604B\u308F\u305A\u3089\u3044\u306E\u3001\u60B2\u5287\u7684">\u30CF\u77ED\u8ABF (Cm)</option>
15527
+ <option value="key_Csm" title="\u843D\u80C6\u3001\u6CE3\u304D\u53EB\u3093\u3060\u3001\u60B2\u6D99\u306E\u3001\u6DF1\u3044\u5606\u304D">\u5B30\u30CF\u77ED\u8ABF (C\u266Fm)</option>
15528
+ <option value="key_Dm" title="\u53B3\u7C9B\u306A\u3001\u656C\u8654\u306A\u3001\u601D\u7D22\u7684\u306A\u3001\u91CD\u539A\u306A\u7948\u308A">\u30CB\u77ED\u8ABF (Dm)</option>
15529
+ <option value="key_Ebm" title="\u6DF1\u3044\u82E6\u60A9\u3001\u5B9F\u5B58\u7684\u306A\u4E0D\u5B89\u3001\u6226\u6144">\u5909\u30DB\u77ED\u8ABF (E\u266Dm)</option>
15530
+ <option value="key_Em" title="\u5F31\u3005\u3057\u3044\u3001\u306A\u307E\u3081\u304B\u3057\u3044\u3001\u843D\u3061\u7740\u304D\u306E\u306A\u3044">\u30DB\u77ED\u8ABF (Em)</option>
15531
+ <option value="key_Fm" title="\u307C\u3093\u3084\u308A\u3057\u305F\u3001\u7269\u60B2\u3057\u3044\u3001\u3057\u3081\u3084\u304B\u306A\u3001\u846C\u9001">\u30D8\u77ED\u8ABF (Fm)</option>
15532
+ <option value="key_Fsm" title="\u9670\u6C17\u306A\u3001\u6FC0\u3057\u3044\u61A4\u308A\u3001\u6697\u3044\u60C5\u5FF5">\u5B30\u30D8\u77ED\u8ABF (F\u266Fm)</option>
15533
+ <option value="key_Gm" title="\u4E0D\u6E80\u3001\u4E0D\u5B89\u3001\u3084\u308B\u305B\u306A\u3055\u3001\u60B2\u75DB\u306A\u53EB\u3073">\u30C8\u77ED\u8ABF (Gm)</option>
15534
+ <option value="key_Abm" title="\u4E0D\u670D\u306A\u3001\u5606\u304D\u306E\u3001\u6CE3\u304D\u53EB\u3093\u3060">\u5909\u30A4\u77ED\u8ABF (A\u266Dm)</option>
15535
+ </optgroup>
15536
+ </select>
15537
+ <span class="dtm-grow"></span>
15538
+ <span class="dtm-hint" data-dtm="compose-key-hint"></span>
15539
+ </div>
15385
15540
  <div class="dtm-row">
15386
15541
  <span class="dtm-label">\u5168\u4F53\u30B7\u30D5\u30C8</span>
15387
15542
  <select class="dtm-select" data-dtm="shift-select">
@@ -15577,6 +15732,8 @@ var buildUI = (target, options) => {
15577
15732
  macroCompose: sel("macro-compose"),
15578
15733
  composeSections: sel("compose-sections"),
15579
15734
  composeSectionsLen: sel("compose-sections-len"),
15735
+ composeKey: sel("compose-key"),
15736
+ composeKeyHint: sel("compose-key-hint"),
15580
15737
  macroComposeVocal: sel("macro-compose-vocal"),
15581
15738
  macroComposeInfo: sel("macro-compose-info"),
15582
15739
  macroClear: sel("macro-clear"),
@@ -19348,6 +19505,7 @@ var mountDAW = (target, options = {}) => {
19348
19505
  redrawAll();
19349
19506
  },
19350
19507
  onEnd: (interrupted) => {
19508
+ options.onScheduleFade?.(null);
19351
19509
  if (interrupted) {
19352
19510
  playbackState = "paused";
19353
19511
  pausedPlayStep = currentPlayStep;
@@ -19439,15 +19597,20 @@ var mountDAW = (target, options = {}) => {
19439
19597
  }
19440
19598
  if (fadeOutSec > 0) {
19441
19599
  const endStep = getSongEndStepExact();
19442
- const totalDurationSec = (endStep - fromStep) * secondsPerStepFade;
19443
- if (totalDurationSec > 0) {
19444
- const fadeOutEndAt = anchor + totalDurationSec;
19445
- const earliestStart = params.fadeInEndAt ?? anchor;
19446
- params.fadeOutEndAt = fadeOutEndAt;
19447
- params.fadeOutStartAt = Math.max(
19448
- fadeOutEndAt - fadeOutSec,
19449
- earliestStart
19450
- );
19600
+ if (endStep > fromStep) {
19601
+ const totalDurationSec = (endStep - fromStep) * secondsPerStepFade;
19602
+ if (totalDurationSec > 0.1) {
19603
+ const fadeOutEndAt = anchor + totalDurationSec;
19604
+ const earliestStart = params.fadeInEndAt ?? anchor;
19605
+ const fadeOutStartAt = Math.max(
19606
+ fadeOutEndAt - fadeOutSec,
19607
+ earliestStart
19608
+ );
19609
+ if (fadeOutStartAt < fadeOutEndAt) {
19610
+ params.fadeOutStartAt = fadeOutStartAt;
19611
+ params.fadeOutEndAt = fadeOutEndAt;
19612
+ }
19613
+ }
19451
19614
  }
19452
19615
  }
19453
19616
  options.onScheduleFade?.(params);
@@ -21472,6 +21635,16 @@ var mountDAW = (target, options = {}) => {
21472
21635
  };
21473
21636
  refs.composeSections.addEventListener("change", updateComposeSectionsLen);
21474
21637
  updateComposeSectionsLen();
21638
+ const updateComposeKeyHint = () => {
21639
+ if (!refs.composeKey || !refs.composeKeyHint) return;
21640
+ const desc = getComposeKeyDescription(refs.composeKey.value);
21641
+ refs.composeKeyHint.textContent = desc;
21642
+ refs.composeKeyHint.title = desc;
21643
+ };
21644
+ if (refs.composeKey) {
21645
+ refs.composeKey.addEventListener("change", updateComposeKeyHint);
21646
+ updateComposeKeyHint();
21647
+ }
21475
21648
  const runCompose = (withVocal) => {
21476
21649
  stop();
21477
21650
  overlayDuring(() => {
@@ -21479,6 +21652,7 @@ var mountDAW = (target, options = {}) => {
21479
21652
  stepsPerBar: renderConfig.stepsPerBar,
21480
21653
  edo: renderConfig.edo,
21481
21654
  sections: selectedComposeSections(),
21655
+ baseKey: refs.composeKey?.value ?? "any",
21482
21656
  // 直近に作った曲の特徴を渡すと、それらから離れた候補に加点される。
21483
21657
  // 「作曲」を続けて押したときに似た曲が並ぶのを防ぐ。
21484
21658
  recent: recentComposeFingerprints
@@ -21551,7 +21725,10 @@ var mountDAW = (target, options = {}) => {
21551
21725
  }
21552
21726
  }
21553
21727
  setBpm(song.bpm);
21554
- applyComposedDrumPattern(song.drums.def);
21728
+ currentDrumPattern = song.drum;
21729
+ refs.drumSelect.value = song.drum;
21730
+ options.onDrumChange?.(song.drum);
21731
+ applyDrumPatternFont(song.drum);
21555
21732
  for (const t of bassTracksForCompose()) {
21556
21733
  t.trackCompression = AUTO_ROLE_MIX.bass.compression;
21557
21734
  options.onTrackCompressionChange?.(t.config.id, t.trackCompression);
@@ -21570,6 +21747,10 @@ var mountDAW = (target, options = {}) => {
21570
21747
  fireLyricsChange(melodyTrack);
21571
21748
  }
21572
21749
  }
21750
+ if (refs.composeKeyHint) {
21751
+ refs.composeKeyHint.textContent = `${song.keyLabel} \u3067\u4F5C\u6210`;
21752
+ refs.composeKeyHint.title = `${song.keyLabel}${song.moodLabel ? `\uFF08${song.moodLabel}\uFF09` : ""}`;
21753
+ }
21573
21754
  playStartStep = 0;
21574
21755
  redrawAll();
21575
21756
  updateTrackPanel();
@@ -21920,9 +22101,6 @@ var mountDAW = (target, options = {}) => {
21920
22101
  extractedMidiDrum = patternDef.pattern;
21921
22102
  installDrumPattern("_extracted_midi", patternDef);
21922
22103
  };
21923
- const applyComposedDrumPattern = (patternDef) => {
21924
- installDrumPattern("_composed", patternDef);
21925
- };
21926
22104
  const wireMidi = () => {
21927
22105
  refs.midiInput.addEventListener("change", async () => {
21928
22106
  const file = refs.midiInput.files?.[0];
@@ -23880,6 +24058,8 @@ var createDtmStudio = async (options = {}) => {
23880
24058
  };
23881
24059
  };
23882
24060
  const resumeAudio = () => {
24061
+ fadeGain.gain.cancelScheduledValues(audioCtx.currentTime);
24062
+ fadeGain.gain.setValueAtTime(1, audioCtx.currentTime);
23883
24063
  if (audioCtx.state === "closed") return Promise.resolve();
23884
24064
  return audioCtx.resume();
23885
24065
  };
@@ -24441,7 +24621,7 @@ var createDtmStudio = async (options = {}) => {
24441
24621
  await loadInstrument(key);
24442
24622
  }
24443
24623
  };
24444
- void Promise.all([
24624
+ const presetPromise = Promise.all([
24445
24625
  loadPreset(
24446
24626
  playerPreset,
24447
24627
  loadTrackIds,
@@ -24481,25 +24661,29 @@ var createDtmStudio = async (options = {}) => {
24481
24661
  duration: e.duration
24482
24662
  });
24483
24663
  };
24664
+ const { onResumeAudio: userOnResumeAudio, ...playerOpts } = opts;
24484
24665
  const player = mountMmlPlayer(target, mml, {
24666
+ ...playerOpts,
24485
24667
  getAudioTime: () => audioCtx.currentTime,
24486
24668
  onResumeAudio: async () => {
24487
24669
  await resumeAudio();
24670
+ await presetPromise;
24488
24671
  if (meta.drum) {
24489
24672
  await loadRequiredDrums(
24490
24673
  getDrumPatternKeys(
24491
24674
  meta.drum,
24492
- resolveDrumPatterns(opts.drumPatterns ?? options.drumPatterns)
24675
+ resolveDrumPatterns(
24676
+ playerOpts.drumPatterns ?? options.drumPatterns
24677
+ )
24493
24678
  ),
24494
24679
  meta.drumFont || "FluidR3_GM_sf2_file:0"
24495
24680
  );
24496
24681
  }
24497
- await opts.onResumeAudio?.();
24682
+ await userOnResumeAudio?.();
24498
24683
  },
24499
24684
  onPlayNote: playPlayerNote,
24500
24685
  onPlayDrum: playDrum,
24501
- singingVoices,
24502
- ...opts
24686
+ singingVoices
24503
24687
  });
24504
24688
  mountedPlayers.push(player);
24505
24689
  const destroy = () => {
@@ -24532,7 +24716,7 @@ var createDtmStudio = async (options = {}) => {
24532
24716
  await loadInstrument(key);
24533
24717
  }
24534
24718
  };
24535
- void Promise.all([
24719
+ const presetPromise = Promise.all([
24536
24720
  loadPreset(
24537
24721
  playerPreset,
24538
24722
  loadTrackIds,
@@ -24572,8 +24756,9 @@ var createDtmStudio = async (options = {}) => {
24572
24756
  duration: e.duration
24573
24757
  });
24574
24758
  };
24759
+ const { onResumeAudio: userOnResumeAudio, ...playOpts } = opts;
24575
24760
  return playMML(mml, {
24576
- ...opts,
24761
+ ...playOpts,
24577
24762
  audioContext: audioCtx,
24578
24763
  destination: masterGain,
24579
24764
  synth: false,
@@ -24581,15 +24766,18 @@ var createDtmStudio = async (options = {}) => {
24581
24766
  onPlayDrum: playDrum,
24582
24767
  onResumeAudio: async () => {
24583
24768
  await resumeAudio();
24769
+ await presetPromise;
24584
24770
  if (meta.drum) {
24585
24771
  await loadRequiredDrums(
24586
24772
  getDrumPatternKeys(
24587
24773
  meta.drum,
24588
- resolveDrumPatterns(opts.drumPatterns ?? options.drumPatterns)
24774
+ resolveDrumPatterns(
24775
+ playOpts.drumPatterns ?? options.drumPatterns
24776
+ )
24589
24777
  )
24590
24778
  );
24591
24779
  }
24592
- await opts.onResumeAudio?.();
24780
+ await userOnResumeAudio?.();
24593
24781
  }
24594
24782
  });
24595
24783
  };
@@ -24616,7 +24804,7 @@ var createDtmStudio = async (options = {}) => {
24616
24804
  await loadInstrument(key);
24617
24805
  }
24618
24806
  };
24619
- void Promise.all([
24807
+ const presetPromise = Promise.all([
24620
24808
  loadPreset(
24621
24809
  playerPreset,
24622
24810
  loadTrackIds,
@@ -24656,8 +24844,9 @@ var createDtmStudio = async (options = {}) => {
24656
24844
  duration: e.duration
24657
24845
  });
24658
24846
  };
24847
+ const { onResumeAudio: userOnResumeAudio, ...playOpts } = opts;
24659
24848
  return playSingingMML(mml, {
24660
- ...opts,
24849
+ ...playOpts,
24661
24850
  audioContext: audioCtx,
24662
24851
  destination: masterGain,
24663
24852
  synth: false,
@@ -24666,15 +24855,18 @@ var createDtmStudio = async (options = {}) => {
24666
24855
  onPlayDrum: playDrum,
24667
24856
  onResumeAudio: async () => {
24668
24857
  await resumeAudio();
24858
+ await presetPromise;
24669
24859
  if (meta.drum) {
24670
24860
  await loadRequiredDrums(
24671
24861
  getDrumPatternKeys(
24672
24862
  meta.drum,
24673
- resolveDrumPatterns(opts.drumPatterns ?? options.drumPatterns)
24863
+ resolveDrumPatterns(
24864
+ playOpts.drumPatterns ?? options.drumPatterns
24865
+ )
24674
24866
  )
24675
24867
  );
24676
24868
  }
24677
- await opts.onResumeAudio?.();
24869
+ await userOnResumeAudio?.();
24678
24870
  }
24679
24871
  });
24680
24872
  };
@@ -24711,9 +24903,7 @@ var createDtmStudio = async (options = {}) => {
24711
24903
  if (!sfInst) return;
24712
24904
  const vol = options2.volume ?? 80;
24713
24905
  const dur = options2.duration ?? 1;
24714
- if (audioCtx.state === "suspended") {
24715
- await audioCtx.resume();
24716
- }
24906
+ await resumeAudio();
24717
24907
  const { midi, detuneCents } = unitsToMidiDetune(options2.pitchUnits);
24718
24908
  sfInst.play({
24719
24909
  ctx: audioCtx,
@@ -24743,7 +24933,7 @@ var createDtmStudio = async (options = {}) => {
24743
24933
  velocity: p.velocity
24744
24934
  }));
24745
24935
  const playerPreset = defaultPreset;
24746
- void loadPreset(playerPreset, ["chord"]);
24936
+ const presetPromise = loadPreset(playerPreset, ["chord"]);
24747
24937
  const playPlayerNote = (e) => {
24748
24938
  const sfInst = resolveSoundFont(playerPreset, "chord");
24749
24939
  if (!sfInst) return;
@@ -24759,16 +24949,21 @@ var createDtmStudio = async (options = {}) => {
24759
24949
  duration: e.duration
24760
24950
  });
24761
24951
  };
24952
+ const { onResumeAudio: userOnResumeAudio, ...chordOpts } = opts;
24762
24953
  return playPlacements(placements, {
24763
- ...opts,
24954
+ ...chordOpts,
24764
24955
  audioContext: audioCtx,
24765
24956
  destination: masterGain,
24766
24957
  synth: false,
24767
24958
  onPlayNote: playPlayerNote,
24768
24959
  onPlayDrum: playDrum,
24769
- onResumeAudio: resumeAudio,
24960
+ onResumeAudio: async () => {
24961
+ await resumeAudio();
24962
+ await presetPromise;
24963
+ await userOnResumeAudio?.();
24964
+ },
24770
24965
  bpm,
24771
- metaVolume: opts.volume ?? 100
24966
+ metaVolume: chordOpts.volume ?? 100
24772
24967
  });
24773
24968
  };
24774
24969
  const mountChordPlayerInstance = (target, chords, options2 = {}) => {
@@ -24894,6 +25089,8 @@ export {
24894
25089
  A4_UNITS,
24895
25090
  BREATH_MARK,
24896
25091
  CENTS_PER_UNIT,
25092
+ COMPOSE_KEYS,
25093
+ COMPOSE_MOOD_GROUPS,
24897
25094
  CORPUS_BANDS,
24898
25095
  CORPUS_SIZE,
24899
25096
  DAW_CSS,
@@ -24920,8 +25117,10 @@ export {
24920
25117
  KOE_VOICEBANK_LABELS,
24921
25118
  KOE_VOICEBANK_TERMS,
24922
25119
  LinkedList,
25120
+ MAJOR_KEY_IDS,
24923
25121
  MAX_VOCAL_VOLUME,
24924
25122
  MICRO_STEP,
25123
+ MINOR_KEY_IDS,
24925
25124
  MMLCore,
24926
25125
  MML_END_MARKER,
24927
25126
  MidiSearchClient,
@@ -24990,6 +25189,7 @@ export {
24990
25189
  formatMmlMeta,
24991
25190
  freqFromPitch,
24992
25191
  generateRandomPattern,
25192
+ getComposeKeyDescription,
24993
25193
  getDrumPatternKeys,
24994
25194
  getMidiBPM,
24995
25195
  icon,
@@ -25019,6 +25219,7 @@ export {
25019
25219
  playNote,
25020
25220
  playPlacements,
25021
25221
  playSingingMML,
25222
+ resolveComposeKey,
25022
25223
  resolveDrumPattern,
25023
25224
  resolveLoopPoint,
25024
25225
  semitonesToUnits,