@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.js CHANGED
@@ -24,6 +24,8 @@ __export(index_exports, {
24
24
  A4_UNITS: () => A4_UNITS,
25
25
  BREATH_MARK: () => BREATH_MARK,
26
26
  CENTS_PER_UNIT: () => CENTS_PER_UNIT,
27
+ COMPOSE_KEYS: () => COMPOSE_KEYS,
28
+ COMPOSE_MOOD_GROUPS: () => COMPOSE_MOOD_GROUPS,
27
29
  CORPUS_BANDS: () => CORPUS_BANDS,
28
30
  CORPUS_SIZE: () => CORPUS_SIZE,
29
31
  DAW_CSS: () => DAW_CSS,
@@ -50,8 +52,10 @@ __export(index_exports, {
50
52
  KOE_VOICEBANK_LABELS: () => KOE_VOICEBANK_LABELS,
51
53
  KOE_VOICEBANK_TERMS: () => KOE_VOICEBANK_TERMS,
52
54
  LinkedList: () => LinkedList,
55
+ MAJOR_KEY_IDS: () => MAJOR_KEY_IDS,
53
56
  MAX_VOCAL_VOLUME: () => MAX_VOCAL_VOLUME,
54
57
  MICRO_STEP: () => MICRO_STEP,
58
+ MINOR_KEY_IDS: () => MINOR_KEY_IDS,
55
59
  MMLCore: () => MMLCore,
56
60
  MML_END_MARKER: () => MML_END_MARKER,
57
61
  MidiSearchClient: () => MidiSearchClient,
@@ -120,6 +124,7 @@ __export(index_exports, {
120
124
  formatMmlMeta: () => formatMmlMeta,
121
125
  freqFromPitch: () => freqFromPitch,
122
126
  generateRandomPattern: () => generateRandomPattern,
127
+ getComposeKeyDescription: () => getComposeKeyDescription,
123
128
  getDrumPatternKeys: () => getDrumPatternKeys,
124
129
  getMidiBPM: () => getMidiBPM,
125
130
  icon: () => icon,
@@ -149,6 +154,7 @@ __export(index_exports, {
149
154
  playNote: () => playNote,
150
155
  playPlacements: () => playPlacements,
151
156
  playSingingMML: () => playSingingMML,
157
+ resolveComposeKey: () => resolveComposeKey,
152
158
  resolveDrumPattern: () => resolveDrumPattern,
153
159
  resolveLoopPoint: () => resolveLoopPoint,
154
160
  semitonesToUnits: () => semitonesToUnits,
@@ -4389,7 +4395,7 @@ var NATURAL_STEPS2 = {
4389
4395
  };
4390
4396
  var isNatural = (ch) => Object.hasOwn(NATURAL_STEPS2[12], ch);
4391
4397
  var clamp3 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
4392
- var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode|edo)=([\w-]+)/gi;
4398
+ var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|mastercomp|fadein|fadeout|mode|edo)=([\w:-]+)/gi;
4393
4399
  var TRACK_INST_DIRECTIVE = /#t(\d+)inst=([^#;\r\n]+)/gi;
4394
4400
  var TRACK_COMP_DIRECTIVE = /#t(\d+)comp=(\d+)/gi;
4395
4401
  var TRACK_WIDTH_DIRECTIVE = /#t(\d+)width=(\d+)/gi;
@@ -8910,6 +8916,27 @@ var DAW_CSS = `
8910
8916
  .dtm-player-play:active { transform: translate(2px, 2px); box-shadow: none; }
8911
8917
  .dtm-player-play--stop { background: var(--dtm-danger); }
8912
8918
  .dtm-player-play:disabled { opacity: 0.4; cursor: default; }
8919
+ .dtm-player-loop {
8920
+ display: inline-flex;
8921
+ align-items: center;
8922
+ justify-content: center;
8923
+ width: 28px;
8924
+ height: 28px;
8925
+ padding: 0;
8926
+ border: 2px solid var(--dtm-border2);
8927
+ background: var(--dtm-bg);
8928
+ color: var(--dtm-muted);
8929
+ cursor: pointer;
8930
+ flex: 0 0 auto;
8931
+ }
8932
+ .dtm-player-loop:active { transform: translate(1px, 1px); }
8933
+ .dtm-player-loop--on {
8934
+ border-color: var(--c-black);
8935
+ background: var(--dtm-gold);
8936
+ color: var(--c-black);
8937
+ box-shadow: 2px 2px 0 var(--c-black);
8938
+ }
8939
+ .dtm-player-loop:disabled { opacity: 0.4; cursor: default; }
8913
8940
  .dtm-player-beat-row {
8914
8941
  display: flex;
8915
8942
  align-items: center;
@@ -9568,6 +9595,12 @@ var FALLBACK_VOCAL_ICON = Chip_default;
9568
9595
  var STEPS_PER_BEAT2 = 48;
9569
9596
  var STEPS_PER_BAR2 = 192;
9570
9597
  var DEFAULT_TRACK_COLORS = ["#00e436", "#29adff", "#ff77a8", "#ffec27"];
9598
+ var parseLoopMeta = (mml) => {
9599
+ const m = mml.match(/^#\s*loop\s*=\s*(on|off|1|0|true|false)\s*$/im);
9600
+ if (!m) return null;
9601
+ const v = m[1].toLowerCase();
9602
+ return v === "on" || v === "1" || v === "true";
9603
+ };
9571
9604
  var activePlayer = null;
9572
9605
  var agreedModelsInSession = /* @__PURE__ */ new Set();
9573
9606
  var LYRIC_MODEL_LABELS = {
@@ -9807,6 +9840,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9807
9840
  const colors = options.trackColors ?? DEFAULT_TRACK_COLORS;
9808
9841
  const useSynth = options.synth ?? !options.onPlayNote;
9809
9842
  const secondsPerStep = 60 / bpm / STEPS_PER_BEAT2;
9843
+ let loopEnabled = options.loop ?? parseLoopMeta(mml) ?? false;
9810
9844
  const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
9811
9845
  (a, b) => a - b
9812
9846
  );
@@ -9895,6 +9929,13 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9895
9929
  playBtn.className = "dtm-player-play";
9896
9930
  playBtn.innerHTML = icon("play", 12);
9897
9931
  playBtn.disabled = trackIndices.length === 0;
9932
+ const loopBtn = doc.createElement("button");
9933
+ loopBtn.type = "button";
9934
+ loopBtn.className = "dtm-player-loop";
9935
+ loopBtn.innerHTML = icon("loop", 12);
9936
+ loopBtn.title = loopEnabled ? "LOOP ON" : "LOOP OFF";
9937
+ loopBtn.classList.toggle("dtm-player-loop--on", loopEnabled);
9938
+ loopBtn.disabled = trackIndices.length === 0;
9898
9939
  const mutedTracks = /* @__PURE__ */ new Set();
9899
9940
  const labelByTrack = /* @__PURE__ */ new Map();
9900
9941
  const emojiByTrack = /* @__PURE__ */ new Map();
@@ -10327,7 +10368,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
10327
10368
  chordEl.className = "dtm-player-chord";
10328
10369
  chordEl.textContent = "";
10329
10370
  beatRow.appendChild(chordEl);
10330
- head.append(playBtn, beatRow, dots, mmlHeader);
10371
+ head.append(playBtn, loopBtn, beatRow, dots, mmlHeader);
10331
10372
  root.appendChild(head);
10332
10373
  const seekRow = doc.createElement("div");
10333
10374
  seekRow.className = "dtm-player-seek-row";
@@ -10627,6 +10668,7 @@ var mountMmlPlayer = (target, mml, options = {}) => {
10627
10668
  getPlayStartStep: () => 0,
10628
10669
  getDrumPattern: (currentBar) => resolveDrumPattern(drumPatternName, drumPatternDict, currentBar),
10629
10670
  getSoloTrackId: () => null,
10671
+ getLoop: () => loopEnabled,
10630
10672
  getAudioTime,
10631
10673
  onPlayNote: (e) => {
10632
10674
  const trackIdx = Number(e.trackId);
@@ -10743,8 +10785,16 @@ var mountMmlPlayer = (target, mml, options = {}) => {
10743
10785
  if (streaming && !skipSinging) {
10744
10786
  const v = ensureVoices();
10745
10787
  v.setVolume(trackVolume / 100 * (masterVolume / 100));
10788
+ let loopLengthSec;
10789
+ let loopStartSec;
10790
+ if (loopEnabled) {
10791
+ loopStartSec = 0;
10792
+ loopLengthSec = maxStep * secondsPerStep;
10793
+ }
10746
10794
  v.startStream(tracks, seq.getStartTime(), {
10747
10795
  isAudible: (t) => !mutedTracks.has(Number(t.id)),
10796
+ loopLengthSec,
10797
+ loopStartSec,
10748
10798
  onLateSkip: () => {
10749
10799
  showPlayerMessage(
10750
10800
  "\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"
@@ -10816,6 +10866,14 @@ var mountMmlPlayer = (target, mml, options = {}) => {
10816
10866
  if (playing) pause();
10817
10867
  else play(Number(seekInput.value));
10818
10868
  });
10869
+ loopBtn.addEventListener("click", () => {
10870
+ loopEnabled = !loopEnabled;
10871
+ loopBtn.classList.toggle("dtm-player-loop--on", loopEnabled);
10872
+ loopBtn.title = loopEnabled ? "LOOP ON" : "LOOP OFF";
10873
+ if (playing) {
10874
+ seekTo(playStep);
10875
+ }
10876
+ });
10819
10877
  const destroy = () => {
10820
10878
  doc.removeEventListener("click", handleOutsideClick);
10821
10879
  seq.stop();
@@ -11355,7 +11413,7 @@ var parseBpmMeta = (chords) => {
11355
11413
  if (Number.isNaN(val)) return null;
11356
11414
  return Math.max(40, Math.min(240, val));
11357
11415
  };
11358
- var parseLoopMeta = (chords) => {
11416
+ var parseLoopMeta2 = (chords) => {
11359
11417
  const m = chords.match(/^#\s*loop\s*=\s*(on|off|1|0|true|false)\s*$/im);
11360
11418
  if (!m) return null;
11361
11419
  const v = m[1].toLowerCase();
@@ -11532,7 +11590,7 @@ var mountChordPlayer = (target, chords, options = {}) => {
11532
11590
  let masterVolume = options.volume ?? 80;
11533
11591
  let isLoading = false;
11534
11592
  let loadAbortId = 0;
11535
- let loopEnabled = options.loop ?? parseLoopMeta(chords) ?? false;
11593
+ let loopEnabled = options.loop ?? parseLoopMeta2(chords) ?? false;
11536
11594
  let metronomeEnabled = parseMetronomeMeta(chords);
11537
11595
  let playOffsetSec = 0;
11538
11596
  let currentPlaySec = 0;
@@ -12598,292 +12656,356 @@ var CORPUS_MEDIANS = {
12598
12656
  complementarity: 0.045
12599
12657
  };
12600
12658
 
12601
- // src/compose-drums.ts
12602
- var DRUM_STYLE_LABELS = {
12603
- eight: "8\u30D3\u30FC\u30C8",
12604
- sixteen: "16\u30D3\u30FC\u30C8",
12605
- four: "4\u3064\u6253\u3061",
12606
- shuffle: "\u30B7\u30E3\u30C3\u30D5\u30EB",
12607
- ballad: "\u30D0\u30E9\u30FC\u30C9",
12608
- rock: "\u30ED\u30C3\u30AF"
12609
- };
12610
- var KICKS = {
12611
- // [抑えめ, 標準, 最大]
12612
- eight: [
12613
- [0, 8],
12614
- [0, 6, 8],
12615
- [0, 6, 8, 14]
12616
- ],
12617
- sixteen: [
12618
- [0, 3, 8],
12619
- [0, 3, 8, 11],
12620
- [0, 3, 6, 8, 11, 14]
12621
- ],
12622
- four: [
12623
- [0, 4, 8, 12],
12624
- [0, 4, 8, 12],
12625
- [0, 4, 8, 12, 14]
12626
- ],
12627
- shuffle: [
12628
- [0, 8],
12629
- [0, 8, 11],
12630
- [0, 5, 8, 11]
12631
- ],
12632
- ballad: [[0], [0, 8], [0, 8, 12]],
12633
- rock: [
12634
- [0, 8],
12635
- [0, 3, 8],
12636
- [0, 3, 8, 10, 14]
12637
- ]
12638
- };
12639
- var SNARES = {
12640
- eight: [
12641
- [4, 12],
12642
- [4, 12],
12643
- [4, 12]
12644
- ],
12645
- sixteen: [
12646
- [4, 12],
12647
- [4, 12],
12648
- [4, 12]
12649
- ],
12650
- four: [
12651
- [4, 12],
12652
- [4, 12],
12653
- [4, 12]
12654
- ],
12655
- shuffle: [
12656
- [4, 12],
12657
- [4, 12],
12658
- [4, 12]
12659
- ],
12660
- // バラードはサイドスティック(リムショット)から入って、盛り上がりでスネアへ移る。
12661
- ballad: [[12], [4, 12], [4, 12]],
12662
- rock: [
12663
- [4, 12],
12664
- [4, 12],
12665
- [4, 12]
12666
- ]
12667
- };
12668
- var GHOSTS = {
12669
- eight: [7],
12670
- sixteen: [7, 11, 15],
12671
- four: [],
12672
- shuffle: [11],
12673
- ballad: [],
12674
- rock: [7, 15]
12675
- };
12676
- var HIHATS = {
12677
- eight: [
12678
- [0, 4, 8, 12],
12679
- [0, 2, 4, 6, 8, 10, 12, 14],
12680
- [0, 2, 4, 6, 8, 10, 12, 14]
12681
- ],
12682
- sixteen: [
12683
- [0, 2, 4, 6, 8, 10, 12, 14],
12684
- [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
12685
- [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
12686
- ],
12687
- four: [
12688
- [2, 6, 10, 14],
12689
- [0, 2, 4, 6, 8, 10, 12, 14],
12690
- [0, 2, 4, 6, 8, 10, 12, 14]
12691
- ],
12692
- shuffle: [[], [], []],
12693
- // 三連なので格子では書けない。{@link shuffleHats} が作る
12694
- ballad: [
12695
- [0, 8],
12696
- [0, 4, 8, 12],
12697
- [0, 2, 4, 6, 8, 10, 12, 14]
12698
- ],
12699
- rock: [
12700
- [0, 4, 8, 12],
12701
- [0, 2, 4, 6, 8, 10, 12, 14],
12702
- [0, 2, 4, 6, 8, 10, 12, 14]
12703
- ]
12704
- };
12705
- var FILLS = [
12706
- // スネアの16分の詰め
12659
+ // src/compose-keys.ts
12660
+ var COMPOSE_KEYS = {
12661
+ // --- 長調 (12調) ---
12662
+ key_C: {
12663
+ id: "key_C",
12664
+ name: "C",
12665
+ label: "\u30CF\u9577\u8ABF (C)",
12666
+ mode: "major",
12667
+ rootShift: 0,
12668
+ moodId: "mood_happy",
12669
+ description: "\u7121\u57A2\u306B\u559C\u3070\u3057\u3044\u3001\u7D14\u7C8B\u3001\u7D20\u6734\u3001\u51FA\u767A"
12670
+ },
12671
+ key_Db: {
12672
+ id: "key_Db",
12673
+ name: "D\u266D",
12674
+ label: "\u5909\u30CB\u9577\u8ABF (D\u266D)",
12675
+ mode: "major",
12676
+ rootShift: 1,
12677
+ moodId: "mood_melancholy",
12678
+ description: "\u60B2\u3057\u307F\u3001\u6182\u9B31\u306A\u3001\u7518\u7F8E\u306A\u611F\u50B7\uFF08\u5B30\u30CF\u9577\u8ABF\u3068\u540C\u97F3\uFF09"
12679
+ },
12680
+ key_D: {
12681
+ id: "key_D",
12682
+ name: "D",
12683
+ label: "\u30CB\u9577\u8ABF (D)",
12684
+ mode: "major",
12685
+ rootShift: 2,
12686
+ moodId: "mood_triumphant",
12687
+ description: "\u610F\u6C17\u63DA\u3005\u3068\u3057\u305F\u3001\u52DD\u5229\u306E\u3001\u558A\u58F0\u3001\u83EF\u3084\u304B"
12688
+ },
12689
+ key_Eb: {
12690
+ id: "key_Eb",
12691
+ name: "E\u266D",
12692
+ label: "\u5909\u30DB\u9577\u8ABF (E\u266D)",
12693
+ mode: "major",
12694
+ rootShift: 3,
12695
+ moodId: "mood_dark",
12696
+ description: "\u53B3\u3057\u3044\u3001\u304D\u3064\u3044\u3001\u305D\u308C\u3067\u3044\u3066\u611B\u306B\u6E80\u3061\u305F\u3001\u8358\u91CD"
12697
+ },
12698
+ key_E: {
12699
+ id: "key_E",
12700
+ name: "E",
12701
+ label: "\u30DB\u9577\u8ABF (E)",
12702
+ mode: "major",
12703
+ rootShift: 4,
12704
+ moodId: "mood_fierce",
12705
+ description: "\u3051\u3093\u304B\u3063\u65E9\u3044\u3001\u8352\u3005\u3057\u3044\u3001\u8F1D\u304B\u3057\u3044\u60C5\u71B1"
12706
+ },
12707
+ key_F: {
12708
+ id: "key_F",
12709
+ name: "F",
12710
+ label: "\u30D8\u9577\u8ABF (F)",
12711
+ mode: "major",
12712
+ rootShift: 5,
12713
+ moodId: "mood_fierce",
12714
+ description: "\u6012\u308A\u72C2\u3063\u305F\u3001\u6C17\u6027\u306E\u8352\u3044\u3001\u4E00\u6642\u7684\u306A\u60B2\u5606\u3001\u6FC0\u52D5"
12715
+ },
12716
+ key_Gb: {
12717
+ id: "key_Gb",
12718
+ name: "G\u266D",
12719
+ label: "\u5909\u30C8\u9577\u8ABF (G\u266D)",
12720
+ mode: "major",
12721
+ rootShift: 6,
12722
+ moodId: "mood_triumphant",
12723
+ 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"
12724
+ },
12725
+ key_G: {
12726
+ id: "key_G",
12727
+ name: "G",
12728
+ label: "\u30C8\u9577\u8ABF (G)",
12729
+ mode: "major",
12730
+ rootShift: -5,
12731
+ moodId: "mood_solemn",
12732
+ description: "\u53B3\u7C9B\u306A\u3001\u5D07\u9AD8\u306A\u3001\u5E7B\u60F3\u3001\u8AA0\u5B9F\u3001\u5E83\u304C\u308A"
12733
+ },
12734
+ key_Ab: {
12735
+ id: "key_Ab",
12736
+ name: "A\u266D",
12737
+ label: "\u5909\u30A4\u9577\u8ABF (A\u266D)",
12738
+ mode: "major",
12739
+ rootShift: -4,
12740
+ moodId: "mood_dark",
12741
+ description: "\u6B7B\u3001\u6C38\u9060\u3001\u88C1\u304D\u3001\u6DF1\u9060\u306A\u7791\u60F3"
12742
+ },
12743
+ key_A: {
12744
+ id: "key_A",
12745
+ name: "A",
12746
+ label: "\u30A4\u9577\u8ABF (A)",
12747
+ mode: "major",
12748
+ rootShift: -3,
12749
+ moodId: "mood_happy",
12750
+ description: "\u3046\u308C\u3057\u3044\u3001\u7267\u6B4C\u7684\u306A\u3001\u611B\u306E\u544A\u767D\u3001\u304D\u3089\u3073\u3084\u304B"
12751
+ },
12752
+ key_Bb: {
12753
+ id: "key_Bb",
12754
+ name: "B\u266D",
12755
+ label: "\u5909\u30ED\u9577\u8ABF (B\u266D)",
12756
+ mode: "major",
12757
+ rootShift: -2,
12758
+ moodId: "mood_happy",
12759
+ description: "\u559C\u3070\u3057\u3044\u3001\u98A8\u5909\u308F\u308A\u306A\u3001\u967D\u6C17\u306A\u3001\u8EFD\u5FEB"
12760
+ },
12761
+ key_B: {
12762
+ id: "key_B",
12763
+ name: "B",
12764
+ label: "\u30ED\u9577\u8ABF (B)",
12765
+ mode: "major",
12766
+ rootShift: -1,
12767
+ moodId: "mood_fierce",
12768
+ description: "\u3069\u304E\u3064\u3044\u3001\u5F37\u70C8\u306A\u3001\u8352\u3063\u307D\u3044\u3001\u731B\u70C8"
12769
+ },
12770
+ // --- 短調 (12調) ---
12771
+ key_Am: {
12772
+ id: "key_Am",
12773
+ name: "Am",
12774
+ label: "\u30A4\u77ED\u8ABF (Am)",
12775
+ mode: "minor",
12776
+ rootShift: 0,
12777
+ moodId: "mood_solemn",
12778
+ description: "\u67D4\u3089\u304B\u306A\u3001\u7269\u60B2\u3057\u3044\u3001\u656C\u8654\u306A\u3001\u7D20\u6734\u306A\u54C0\u6101"
12779
+ },
12780
+ key_Bbm: {
12781
+ id: "key_Bbm",
12782
+ name: "B\u266Dm",
12783
+ label: "\u5909\u30ED\u77ED\u8ABF (B\u266Dm)",
12784
+ mode: "minor",
12785
+ rootShift: 1,
12786
+ moodId: "mood_dark",
12787
+ 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"
12788
+ },
12789
+ key_Bm: {
12790
+ id: "key_Bm",
12791
+ name: "Bm",
12792
+ label: "\u30ED\u77ED\u8ABF (Bm)",
12793
+ mode: "minor",
12794
+ rootShift: 2,
12795
+ moodId: "mood_melancholy",
12796
+ description: "\u5B64\u72EC\u306A\u3001\u6182\u9B31\u306A\u3001\u5FCD\u8010\u3001\u9759\u304B\u306A\u8AE6\u5FF5"
12797
+ },
12798
+ key_Cm: {
12799
+ id: "key_Cm",
12800
+ name: "Cm",
12801
+ label: "\u30CF\u77ED\u8ABF (Cm)",
12802
+ mode: "minor",
12803
+ rootShift: 3,
12804
+ moodId: "mood_plaintive",
12805
+ description: "\u7D14\u7C8B\u306B\u60B2\u3057\u3052\u306A\u3001\u604B\u308F\u305A\u3089\u3044\u306E\u3001\u60B2\u5287\u7684"
12806
+ },
12807
+ key_Csm: {
12808
+ id: "key_Csm",
12809
+ name: "C\u266Fm",
12810
+ label: "\u5B30\u30CF\u77ED\u8ABF (C\u266Fm)",
12811
+ mode: "minor",
12812
+ rootShift: 4,
12813
+ moodId: "mood_melancholy",
12814
+ description: "\u843D\u80C6\u3001\u6CE3\u304D\u53EB\u3093\u3060\u3001\u60B2\u6D99\u306E\u3001\u6DF1\u3044\u5606\u304D"
12815
+ },
12816
+ key_Dm: {
12817
+ id: "key_Dm",
12818
+ name: "Dm",
12819
+ label: "\u30CB\u77ED\u8ABF (Dm)",
12820
+ mode: "minor",
12821
+ rootShift: 5,
12822
+ moodId: "mood_solemn",
12823
+ description: "\u53B3\u7C9B\u306A\u3001\u656C\u8654\u306A\u3001\u601D\u7D22\u7684\u306A\u3001\u91CD\u539A\u306A\u7948\u308A"
12824
+ },
12825
+ key_Ebm: {
12826
+ id: "key_Ebm",
12827
+ name: "E\u266Dm",
12828
+ label: "\u5909\u30DB\u77ED\u8ABF (E\u266Dm)",
12829
+ mode: "minor",
12830
+ rootShift: 6,
12831
+ moodId: "mood_anxious",
12832
+ description: "\u6DF1\u3044\u82E6\u60A9\u3001\u5B9F\u5B58\u7684\u306A\u4E0D\u5B89\u3001\u6226\u6144\uFF08\u5B30\u30CB\u77ED\u8ABF\u3068\u540C\u97F3\uFF09"
12833
+ },
12834
+ key_Em: {
12835
+ id: "key_Em",
12836
+ name: "Em",
12837
+ label: "\u30DB\u77ED\u8ABF (Em)",
12838
+ mode: "minor",
12839
+ rootShift: -5,
12840
+ moodId: "mood_plaintive",
12841
+ description: "\u5F31\u3005\u3057\u3044\u3001\u306A\u307E\u3081\u304B\u3057\u3044\u3001\u843D\u3061\u7740\u304D\u306E\u306A\u3044\u3001\u7E4A\u7D30"
12842
+ },
12843
+ key_Fm: {
12844
+ id: "key_Fm",
12845
+ name: "Fm",
12846
+ label: "\u30D8\u77ED\u8ABF (Fm)",
12847
+ mode: "minor",
12848
+ rootShift: -4,
12849
+ moodId: "mood_plaintive",
12850
+ description: "\u307C\u3093\u3084\u308A\u3057\u305F\u3001\u7269\u60B2\u3057\u3044\u3001\u3057\u3081\u3084\u304B\u306A\u3001\u846C\u9001"
12851
+ },
12852
+ key_Fsm: {
12853
+ id: "key_Fsm",
12854
+ name: "F\u266Fm",
12855
+ label: "\u5B30\u30D8\u77ED\u8ABF (F\u266Fm)",
12856
+ mode: "minor",
12857
+ rootShift: -3,
12858
+ moodId: "mood_anxious",
12859
+ description: "\u9670\u6C17\u306A\u3001\u6FC0\u3057\u3044\u61A4\u308A\u3001\u6697\u3044\u60C5\u5FF5"
12860
+ },
12861
+ key_Gm: {
12862
+ id: "key_Gm",
12863
+ name: "Gm",
12864
+ label: "\u30C8\u77ED\u8ABF (Gm)",
12865
+ mode: "minor",
12866
+ rootShift: -2,
12867
+ moodId: "mood_anxious",
12868
+ description: "\u4E0D\u6E80\u3001\u4E0D\u5B89\u3001\u3084\u308B\u305B\u306A\u3055\u3001\u60B2\u75DB\u306A\u53EB\u3073"
12869
+ },
12870
+ key_Abm: {
12871
+ id: "key_Abm",
12872
+ name: "A\u266Dm",
12873
+ label: "\u5909\u30A4\u77ED\u8ABF (A\u266Dm)",
12874
+ mode: "minor",
12875
+ rootShift: -1,
12876
+ moodId: "mood_anxious",
12877
+ description: "\u4E0D\u670D\u306A\u3001\u5606\u304D\u306E\u3001\u6CE3\u304D\u53EB\u3093\u3060\uFF08\u5B30\u30C8\u77ED\u8ABF\u3068\u540C\u97F3\uFF09"
12878
+ }
12879
+ };
12880
+ var COMPOSE_MOOD_GROUPS = [
12707
12881
  {
12708
- grid: [12, 13, 14, 15],
12709
- drums: [
12710
- DRUM_KEYS.acousticSnare,
12711
- DRUM_KEYS.acousticSnare,
12712
- DRUM_KEYS.acousticSnare,
12713
- DRUM_KEYS.acousticSnare
12714
- ]
12882
+ id: "mood_happy",
12883
+ label: "\u559C\u3070\u3057\u3044\u30FB\u967D\u6C17\u306A\u66F2",
12884
+ 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",
12885
+ keyIds: ["key_C", "key_A", "key_Bb"]
12715
12886
  },
12716
- // タムで降りる
12717
12887
  {
12718
- grid: [8, 10, 12, 14],
12719
- drums: [
12720
- DRUM_KEYS.highTom,
12721
- DRUM_KEYS.highTom,
12722
- DRUM_KEYS.lowMidTom,
12723
- DRUM_KEYS.lowTom
12724
- ]
12888
+ id: "mood_triumphant",
12889
+ label: "\u52DD\u5229\u30FB\u529B\u5F37\u3044\u66F2",
12890
+ description: "\u610F\u6C17\u63DA\u3005\u3001\u52DD\u5229\u306E\u558A\u58F0\u3001\u56F0\u96E3\u306E\u6253\u7834\u3001\u5B89\u5835\u306E\u305F\u3081\u606F",
12891
+ keyIds: ["key_D", "key_Gb"]
12725
12892
  },
12726
- // スネア → タム
12727
12893
  {
12728
- grid: [8, 9, 10, 11, 12, 14],
12729
- drums: [
12730
- DRUM_KEYS.acousticSnare,
12731
- DRUM_KEYS.acousticSnare,
12732
- DRUM_KEYS.highTom,
12733
- DRUM_KEYS.highTom,
12734
- DRUM_KEYS.lowMidTom,
12735
- DRUM_KEYS.lowTom
12736
- ]
12894
+ id: "mood_fierce",
12895
+ label: "\u6FC0\u3057\u3044\u30FB\u8352\u3005\u3057\u3044\u66F2",
12896
+ 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",
12897
+ keyIds: ["key_E", "key_F", "key_B"]
12737
12898
  },
12738
- // 短く締める
12739
12899
  {
12740
- grid: [14, 15],
12741
- drums: [DRUM_KEYS.acousticSnare, DRUM_KEYS.acousticSnare]
12900
+ id: "mood_solemn",
12901
+ label: "\u53B3\u7C9B\u30FB\u5E7B\u60F3\u7684\u306A\u66F2",
12902
+ 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",
12903
+ keyIds: ["key_G", "key_Dm", "key_Am"]
12742
12904
  },
12743
- // 16分の連打から解放
12744
12905
  {
12745
- grid: [8, 9, 10, 11, 12, 13, 14, 15],
12746
- drums: [
12747
- DRUM_KEYS.acousticSnare,
12748
- DRUM_KEYS.acousticSnare,
12749
- DRUM_KEYS.acousticSnare,
12750
- DRUM_KEYS.acousticSnare,
12751
- DRUM_KEYS.highTom,
12752
- DRUM_KEYS.highTom,
12753
- DRUM_KEYS.lowMidTom,
12754
- DRUM_KEYS.lowTom
12755
- ]
12906
+ id: "mood_plaintive",
12907
+ label: "\u7269\u60B2\u3057\u3044\u30FB\u54C0\u6101\u306E\u66F2",
12908
+ 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",
12909
+ keyIds: ["key_Cm", "key_Em", "key_Fm"]
12910
+ },
12911
+ {
12912
+ id: "mood_melancholy",
12913
+ label: "\u6182\u9B31\u30FB\u5B64\u72EC\u306A\u66F2",
12914
+ description: "\u60B2\u3057\u307F\u3001\u6182\u9B31\u3001\u5B64\u72EC\u3001\u5FCD\u8010\u3001\u843D\u80C6\u3001\u60B2\u6D99\u306E\u5606\u304D",
12915
+ keyIds: ["key_Db", "key_Bm", "key_Csm"]
12916
+ },
12917
+ {
12918
+ id: "mood_anxious",
12919
+ label: "\u4E0D\u5B89\u30FB\u82E6\u60A9\u306A\u66F2",
12920
+ 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",
12921
+ keyIds: ["key_Gm", "key_Ebm", "key_Fsm", "key_Abm"]
12756
12922
  },
12757
- // 3連のタム回し(16分格子の外)
12758
12923
  {
12759
- grid: [12, 13.33, 14.67],
12760
- drums: [DRUM_KEYS.highTom, DRUM_KEYS.lowMidTom, DRUM_KEYS.lowTom]
12924
+ id: "mood_dark",
12925
+ label: "\u6697\u95C7\u30FB\u91CD\u539A\u306A\u66F2",
12926
+ description: "\u53B3\u3057\u3044\u611B\u3001\u6B7B\u3001\u6C38\u9060\u3001\u88C1\u304D\u3001\u6050\u308D\u3057\u3044\u6697\u95C7\u3001\u5632\u308A",
12927
+ keyIds: ["key_Eb", "key_Ab", "key_Bbm"]
12761
12928
  }
12762
12929
  ];
12763
- var pick = (items, rnd) => items[Math.floor(rnd() * items.length)];
12764
- var shuffleHats = (stepsPerBar, level) => {
12765
- const beat = stepsPerBar / 4;
12766
- const triplet = beat / 3;
12767
- const out = [];
12768
- for (let b = 0; b < 4; b++) {
12769
- out.push(b * beat);
12770
- out.push(b * beat + triplet * 2);
12771
- if (level === 2) out.push(b * beat + triplet);
12930
+ var MAJOR_KEY_IDS = Object.values(COMPOSE_KEYS).filter((k) => k.mode === "major").map((k) => k.id);
12931
+ var MINOR_KEY_IDS = Object.values(COMPOSE_KEYS).filter((k) => k.mode === "minor").map((k) => k.id);
12932
+ var pickItem = (list, rnd) => list[Math.floor(rnd() * list.length)];
12933
+ var resolveComposeKey = (choice, rnd = Math.random) => {
12934
+ const c = (choice ?? "").trim() || "any";
12935
+ if (COMPOSE_KEYS[c]) {
12936
+ const target2 = COMPOSE_KEYS[c];
12937
+ const mood2 = COMPOSE_MOOD_GROUPS.find((m) => m.id === target2.moodId);
12938
+ return {
12939
+ mode: target2.mode,
12940
+ rootShift: target2.rootShift,
12941
+ keyName: target2.name,
12942
+ keyLabel: target2.label,
12943
+ moodLabel: mood2?.label,
12944
+ description: target2.description
12945
+ };
12772
12946
  }
12773
- return out;
12774
- };
12775
- var composeDrumPattern = (options) => {
12776
- const { bars, stepsPerBar, rnd } = options;
12777
- const style = options.style ?? pick(
12778
- ["eight", "sixteen", "four", "shuffle", "ballad", "rock"],
12779
- rnd
12780
- );
12781
- const toStep = (g) => Math.round(g * stepsPerBar / 16) % stepsPerBar;
12782
- const fillA = pick(FILLS, rnd);
12783
- const fillB = pick(
12784
- FILLS.filter((f) => f !== fillA),
12785
- rnd
12786
- );
12787
- const openTail = rnd() < 0.6;
12788
- const useClap = style === "four" && rnd() < 0.5;
12789
- const levels = options.levels;
12790
- const crashSet = new Set(options.crashBars ?? [1, 9, 13]);
12791
- const fillSet = new Set(
12792
- options.fillBars ?? [4, 8, 12, bars - 1].filter((b) => b > 0)
12793
- );
12794
- const sectionLevel = (bar) => {
12795
- if (levels) return levels[bar - 1] ?? 1;
12796
- if (bar <= 4) return 0;
12797
- if (bar <= 8) return 1;
12798
- if (bar <= 12) return 2;
12799
- return 1;
12800
- };
12801
- const barPatterns = [];
12802
- for (let bar = 1; bar <= bars; bar++) {
12803
- const level = sectionLevel(bar);
12804
- const isLast = bar === bars;
12805
- const isFill = !isLast && fillSet.has(bar);
12806
- const hits = [];
12807
- if (crashSet.has(bar) || isLast)
12808
- hits.push({
12809
- step: 0,
12810
- pitch: DRUM_KEYS.crashCymbal1,
12811
- velocity: bar === 9 || isLast ? 1 : 0.85
12812
- });
12813
- if (isLast) {
12814
- hits.push({ step: 0, pitch: DRUM_KEYS.bassDrum1, velocity: 1 });
12815
- hits.sort((a, b) => a.step - b.step || a.pitch - b.pitch);
12816
- barPatterns.push(hits);
12817
- continue;
12818
- }
12819
- for (const g of KICKS[style][level])
12820
- hits.push({
12821
- step: toStep(g),
12822
- pitch: DRUM_KEYS.bassDrum1,
12823
- velocity: g === 0 ? 1 : 0.88
12824
- });
12825
- const snarePitch = style === "ballad" && level === 0 ? DRUM_KEYS.sideStick : DRUM_KEYS.acousticSnare;
12826
- for (const g of SNARES[style][level]) {
12827
- hits.push({ step: toStep(g), pitch: snarePitch, velocity: 0.95 });
12828
- if (useClap)
12829
- hits.push({
12830
- step: toStep(g),
12831
- pitch: DRUM_KEYS.handClap,
12832
- velocity: 0.7
12833
- });
12834
- }
12835
- if (level >= 1)
12836
- for (const g of GHOSTS[style])
12837
- hits.push({
12838
- step: toStep(g),
12839
- pitch: DRUM_KEYS.acousticSnare,
12840
- velocity: 0.3
12841
- });
12842
- const hatSteps = style === "shuffle" ? shuffleHats(stepsPerBar, level) : HIHATS[style][level].map(toStep);
12843
- const hatPitch = level === 2 && style !== "four" ? DRUM_KEYS.rideCymbal1 : DRUM_KEYS.closedHihat;
12844
- for (const step of hatSteps) {
12845
- if (isFill && step >= stepsPerBar / 2) continue;
12846
- const isDownbeat = step % (stepsPerBar / 4) === 0;
12847
- const isTail = openTail && step === toStep(14) && !isFill;
12848
- hits.push({
12849
- step,
12850
- pitch: isTail ? DRUM_KEYS.openHihat : hatPitch,
12851
- velocity: isTail ? 0.8 : isDownbeat ? 0.75 : 0.5
12852
- });
12853
- }
12854
- if (isFill) {
12855
- const fill = bar % 8 === 0 ? fillB : fillA;
12856
- fill.grid.forEach((g, i2) => {
12857
- hits.push({
12858
- step: toStep(g),
12859
- pitch: fill.drums[i2] ?? DRUM_KEYS.acousticSnare,
12860
- // 追い込みで少しずつ強くする
12861
- velocity: 0.7 + 0.28 * i2 / Math.max(1, fill.grid.length - 1)
12862
- });
12863
- });
12864
- }
12865
- hits.sort((a, b) => a.step - b.step || a.pitch - b.pitch);
12866
- barPatterns.push(hits);
12867
- }
12868
- const pattern = [];
12869
- let runStart = 1;
12870
- for (let bar = 1; bar <= bars; bar++) {
12871
- const same = bar < bars && JSON.stringify(barPatterns[bar]) === JSON.stringify(barPatterns[bar - 1]);
12872
- if (same) continue;
12873
- const instruction = {
12874
- ranges: [[runStart, bar]],
12875
- patternBars: 1,
12876
- pattern: barPatterns[runStart - 1]
12947
+ const mood = COMPOSE_MOOD_GROUPS.find((m) => m.id === c);
12948
+ if (mood) {
12949
+ const pickedKeyId2 = pickItem(mood.keyIds, rnd);
12950
+ const target2 = COMPOSE_KEYS[pickedKeyId2];
12951
+ return {
12952
+ mode: target2.mode,
12953
+ rootShift: target2.rootShift,
12954
+ keyName: target2.name,
12955
+ keyLabel: target2.label,
12956
+ moodLabel: mood.label,
12957
+ description: target2.description
12877
12958
  };
12878
- pattern.push(instruction);
12879
- runStart = bar + 1;
12880
12959
  }
12881
- const variety = new Set(barPatterns.map((p) => JSON.stringify(p))).size;
12960
+ if (c === "major") {
12961
+ const pickedKeyId2 = pickItem(MAJOR_KEY_IDS, rnd);
12962
+ const target2 = COMPOSE_KEYS[pickedKeyId2];
12963
+ const targetMood2 = COMPOSE_MOOD_GROUPS.find((m) => m.id === target2.moodId);
12964
+ return {
12965
+ mode: "major",
12966
+ rootShift: target2.rootShift,
12967
+ keyName: target2.name,
12968
+ keyLabel: target2.label,
12969
+ moodLabel: targetMood2?.label,
12970
+ description: target2.description
12971
+ };
12972
+ }
12973
+ if (c === "minor") {
12974
+ const pickedKeyId2 = pickItem(MINOR_KEY_IDS, rnd);
12975
+ const target2 = COMPOSE_KEYS[pickedKeyId2];
12976
+ const targetMood2 = COMPOSE_MOOD_GROUPS.find((m) => m.id === target2.moodId);
12977
+ return {
12978
+ mode: "minor",
12979
+ rootShift: target2.rootShift,
12980
+ keyName: target2.name,
12981
+ keyLabel: target2.label,
12982
+ moodLabel: targetMood2?.label,
12983
+ description: target2.description
12984
+ };
12985
+ }
12986
+ const allKeyIds = Object.keys(COMPOSE_KEYS);
12987
+ const pickedKeyId = pickItem(allKeyIds, rnd);
12988
+ const target = COMPOSE_KEYS[pickedKeyId];
12989
+ const targetMood = COMPOSE_MOOD_GROUPS.find((m) => m.id === target.moodId);
12882
12990
  return {
12883
- style,
12884
- variety,
12885
- def: { label: `\u4F5C\u66F2\uFF08${DRUM_STYLE_LABELS[style]}\uFF09`, pattern }
12886
- };
12991
+ mode: void 0,
12992
+ // コード進行の長短制約は掛けず従来通りランダム
12993
+ rootShift: target.rootShift,
12994
+ keyName: target.name,
12995
+ keyLabel: target.label,
12996
+ moodLabel: targetMood?.label,
12997
+ description: target.description
12998
+ };
12999
+ };
13000
+ var getComposeKeyDescription = (choice) => {
13001
+ if (!choice || choice === "any") return "\u516824\u8ABF\u304B\u3089\u30E9\u30F3\u30C0\u30E0\u306B\u6C7A\u5B9A\u3057\u307E\u3059";
13002
+ if (choice === "major") return "12\u306E\u9577\u8ABF\u306E\u4E2D\u304B\u3089\u30E9\u30F3\u30C0\u30E0\u306B\u62BD\u9078\u3057\u307E\u3059";
13003
+ if (choice === "minor") return "12\u306E\u77ED\u8ABF\u306E\u4E2D\u304B\u3089\u30E9\u30F3\u30C0\u30E0\u306B\u62BD\u9078\u3057\u307E\u3059";
13004
+ const mood = COMPOSE_MOOD_GROUPS.find((m) => m.id === choice);
13005
+ if (mood) return `${mood.label}\uFF1A${mood.description}`;
13006
+ const target = COMPOSE_KEYS[choice];
13007
+ if (target) return `${target.label}\uFF1A${target.description}`;
13008
+ return "";
12887
13009
  };
12888
13010
 
12889
13011
  // src/compose-metrics.ts
@@ -13315,7 +13437,6 @@ var transposeChordName = (chord, shift) => {
13315
13437
  return names[((pc + normShift) % 12 + 12) % 12];
13316
13438
  });
13317
13439
  };
13318
- var ROOT_SHIFTS = [0, 1, 2, 3, 4, 5, 6, -1, -2, -3, -4, -5];
13319
13440
  var BPM_CHOICES = [
13320
13441
  112,
13321
13442
  120,
@@ -13937,7 +14058,7 @@ var MOTIF_ARCHETYPES = [
13937
14058
  [0, 5, 4, 2, 0]
13938
14059
  // 6度跳躍からの大きな下降
13939
14060
  ];
13940
- var pick2 = (items, rnd) => items[Math.floor(rnd() * items.length)];
14061
+ var pick = (items, rnd) => items[Math.floor(rnd() * items.length)];
13941
14062
  var durationEntropy = (durations) => {
13942
14063
  if (durations.length === 0) return 0;
13943
14064
  const counts = /* @__PURE__ */ new Map();
@@ -14005,7 +14126,7 @@ var leapTarget = (from, tones, rnd) => {
14005
14126
  ...octaves,
14006
14127
  ...near.slice(0, Math.max(2, Math.ceil(near.length / 2)))
14007
14128
  ];
14008
- return pick2(weighted.length > 0 ? weighted : candidates, rnd);
14129
+ return pick(weighted.length > 0 ? weighted : candidates, rnd);
14009
14130
  };
14010
14131
  var landOn = (degrees, scaleIndex) => {
14011
14132
  if (degrees.length === 0) return;
@@ -14034,7 +14155,7 @@ var barDegrees = (role, slots, tones, style, motifContour, startDegree, contourO
14034
14155
  const noteCount = slots.length;
14035
14156
  const out = [];
14036
14157
  if (role === "motif" || role === "sequence" || role === "climax" || role === "answer") {
14037
- const shift = (role === "sequence" ? pick2([-2, -1, 1, 2], rnd) : role === "climax" ? 5 : 0) + repeatShift;
14158
+ const shift = (role === "sequence" ? pick([-2, -1, 1, 2], rnd) : role === "climax" ? 5 : 0) + repeatShift;
14038
14159
  if (style.pentatonicMotif) {
14039
14160
  const startPenta = degreeToPenta(startDegree);
14040
14161
  for (let i2 = 0; i2 < noteCount; i2++)
@@ -14100,7 +14221,7 @@ var barDegrees = (role, slots, tones, style, motifContour, startDegree, contourO
14100
14221
  }
14101
14222
  return out;
14102
14223
  }
14103
- const span = pick2([2, 3, 4], rnd);
14224
+ const span = pick([2, 3, 4], rnd);
14104
14225
  const dir = style.stepShape === "descend" ? -1 : style.stepShape === "ascend" ? 1 : rnd() < 0.5 ? 1 : -1;
14105
14226
  for (let i2 = 0; i2 < noteCount; i2++) {
14106
14227
  const t = noteCount === 1 ? 0 : i2 / (noteCount - 1);
@@ -14328,7 +14449,7 @@ var applyChromatic = (pitches, fifths, slots, tones, opts) => {
14328
14449
  lastAltered = i2;
14329
14450
  }
14330
14451
  };
14331
- var draw = (options, rnd) => {
14452
+ var draw = (options, resolvedKey, rnd) => {
14332
14453
  const stepsPerBar = options.stepsPerBar;
14333
14454
  const edo = options.edo === 31 ? 31 : 12;
14334
14455
  const scaleStep = (v) => Math.max(1, Math.round(Math.abs(v) * stepsPerBar / BASE_STEPS_PER_BAR)) * Math.sign(v);
@@ -14352,12 +14473,12 @@ var draw = (options, rnd) => {
14352
14473
  const sectionPlan = buildSectionPlan(options.sections ?? DEFAULT_SECTIONS);
14353
14474
  const totalBars = sectionPlan.reduce((sum, s) => sum + s.bars, 0);
14354
14475
  if (rnd() < 0.35) {
14355
- const modType = pick2(
14476
+ const modType = pick(
14356
14477
  ["chorus_up", "chorus_up", "prechorus_down"],
14357
14478
  rnd
14358
14479
  );
14359
14480
  if (modType === "chorus_up") {
14360
- const shift = pick2([1, 1, 2], rnd);
14481
+ const shift = pick([1, 1, 2], rnd);
14361
14482
  let lastChorusIdx = -1;
14362
14483
  for (let i2 = 0; i2 < sectionPlan.length; i2++) {
14363
14484
  if (sectionPlan[i2].kind === "chorus") lastChorusIdx = i2;
@@ -14368,7 +14489,7 @@ var draw = (options, rnd) => {
14368
14489
  }
14369
14490
  }
14370
14491
  } else {
14371
- const shift = pick2([-2, 3], rnd);
14492
+ const shift = pick([-2, 3], rnd);
14372
14493
  for (const s of sectionPlan) {
14373
14494
  if (s.kind === "prechorus") s.keyShift = shift;
14374
14495
  }
@@ -14380,14 +14501,15 @@ var draw = (options, rnd) => {
14380
14501
  barKeyShift[b] = s.keyShift;
14381
14502
  }
14382
14503
  }
14383
- const progA = pick2(SECTION_A_PROGRESSIONS, rnd);
14504
+ 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;
14505
+ const progA = pick(progAPool, rnd);
14384
14506
  const progBPool = SECTION_B_PROGRESSIONS.filter(
14385
14507
  (p) => p.join("|") !== progA.join("|")
14386
14508
  );
14387
- const progB = pick2(progBPool, rnd);
14509
+ const progB = pick(progBPool, rnd);
14388
14510
  const tonic = progA[0].startsWith("Am") ? "Am" : "C";
14389
- const progHalf = pick2(SECTION_A2_DERIVATIONS, rnd)(progA);
14390
- const progFull = pick2(SECTION_A3_DERIVATIONS, rnd)(progA, tonic);
14511
+ const progHalf = pick(SECTION_A2_DERIVATIONS, rnd)(progA);
14512
+ const progFull = pick(SECTION_A3_DERIVATIONS, rnd)(progA, tonic);
14391
14513
  const progression = [];
14392
14514
  for (const section of sectionPlan) {
14393
14515
  const base = section.spec.progression === "b" ? progB : progA;
@@ -14405,9 +14527,9 @@ var draw = (options, rnd) => {
14405
14527
  }
14406
14528
  progression.length = totalBars;
14407
14529
  const chordProgression = progression.map((chord, bar) => transposeChordName(chord, barKeyShift[bar])).join("|");
14408
- const chordPattern = pick2(CHORD_PATTERNS, rnd);
14409
- const rootShift = pick2(ROOT_SHIFTS, rnd);
14410
- const bpm = pick2(BPM_CHOICES, rnd);
14530
+ const chordPattern = pick(CHORD_PATTERNS, rnd);
14531
+ const rootShift = resolvedKey.rootShift;
14532
+ const bpm = pick(BPM_CHOICES, rnd);
14411
14533
  const sourceOf = (kind) => kind === "chorus" || kind === "interlude" ? "b" : kind === "prechorus" ? "a2" : "a";
14412
14534
  const units2 = [];
14413
14535
  for (const section of sectionPlan) {
@@ -14456,19 +14578,19 @@ var draw = (options, rnd) => {
14456
14578
  return null;
14457
14579
  };
14458
14580
  const style = {
14459
- groove: pick2(["eighth", "sixteenth"], rnd),
14460
- arcPeriod: pick2([4, 8, 8, 16], rnd),
14461
- arcPhase: pick2([0, 1, 2], rnd),
14581
+ groove: pick(["eighth", "sixteenth"], rnd),
14582
+ arcPeriod: pick([4, 8, 8, 16], rnd),
14583
+ arcPhase: pick([0, 1, 2], rnd),
14462
14584
  arcAmp: 3 + rnd() * 4,
14463
14585
  octaveAffinity: 0.14 + rnd() * 0.2,
14464
14586
  pentatonicMotif: rnd() < 0.55,
14465
- runShape: pick2(["scale", "turn", "broken", "zigzag"], rnd),
14466
- stepShape: pick2(
14587
+ runShape: pick(["scale", "turn", "broken", "zigzag"], rnd),
14588
+ stepShape: pick(
14467
14589
  ["arch", "valley", "ascend", "descend", "wave", "pivot"],
14468
14590
  rnd
14469
14591
  ),
14470
- holdShape: pick2(["long", "third", "neighbor"], rnd),
14471
- cadenceShape: pick2(
14592
+ holdShape: pick(["long", "third", "neighbor"], rnd),
14593
+ cadenceShape: pick(
14472
14594
  ["descend", "five-three-one", "leap-up", "hold-tonic"],
14473
14595
  rnd
14474
14596
  ),
@@ -14478,7 +14600,7 @@ var draw = (options, rnd) => {
14478
14600
  // 全曲に掛けると「どの曲も同じ味付け」になる)。
14479
14601
  chromaticAffinity: rnd() < 0.25 ? 0 : 0.15 + rnd() * 0.45,
14480
14602
  barHeadWeight: rnd() < 0.65 ? 3 : 2,
14481
- bassStyle: pick2(
14603
+ bassStyle: pick(
14482
14604
  [
14483
14605
  "quarter",
14484
14606
  "alternate",
@@ -14499,8 +14621,8 @@ var draw = (options, rnd) => {
14499
14621
  // パッド・保続音・付点2分は「置いただけの音」になりやすく、実際
14500
14622
  // サブメロの音の38%が全音符1つ(1.5音/小節)まで薄くなっていた。
14501
14623
  // それらは終止の小節でだけ使う逃げ道に降格する。
14502
- subStyle: pick2(["harmony", "harmony", "counter"], rnd),
14503
- subInterval: pick2([3, 4, 8, 9], rnd)
14624
+ subStyle: pick(["harmony", "harmony", "counter"], rnd),
14625
+ subInterval: pick([3, 4, 8, 9], rnd)
14504
14626
  };
14505
14627
  const motifPool = groovyCells(MOTIF_CELLS, style.groove, rnd);
14506
14628
  const targetNotesPerBar = 5.5 + rnd() * 2.5;
@@ -14517,9 +14639,9 @@ var draw = (options, rnd) => {
14517
14639
  };
14518
14640
  const cellDistance = (c) => Math.abs(cellNotes(c) - targetNotesPerBar) + Math.abs(cellRest(c) - targetRestRatio) * 8;
14519
14641
  const pickCell = (pool) => {
14520
- let best = pick2(pool, rnd);
14642
+ let best = pick(pool, rnd);
14521
14643
  for (let i2 = 0; i2 < 2; i2++) {
14522
- const c = pick2(pool, rnd);
14644
+ const c = pick(pool, rnd);
14523
14645
  if (cellDistance(c) < cellDistance(best)) best = c;
14524
14646
  }
14525
14647
  return best;
@@ -14542,7 +14664,7 @@ var draw = (options, rnd) => {
14542
14664
  const motifBVar = pickCell(
14543
14665
  motifPool.filter((c) => c !== motifB && c !== motifB2)
14544
14666
  );
14545
- const buildUpCell = pick2(
14667
+ const buildUpCell = pick(
14546
14668
  [
14547
14669
  {
14548
14670
  value: [EIGHTH, EIGHTH, EIGHTH, EIGHTH, QUARTER, -QUARTER],
@@ -14563,7 +14685,7 @@ var draw = (options, rnd) => {
14563
14685
  ],
14564
14686
  rnd
14565
14687
  );
14566
- const counterRhythm = pick2(
14688
+ const counterRhythm = pick(
14567
14689
  groovyCells(
14568
14690
  RHYTHM_CELLS.filter((c) => c.density === "medium"),
14569
14691
  style.groove,
@@ -14571,7 +14693,7 @@ var draw = (options, rnd) => {
14571
14693
  ),
14572
14694
  rnd
14573
14695
  ).value;
14574
- const breathCell = pick2(
14696
+ const breathCell = pick(
14575
14697
  RHYTHM_CELLS.filter(
14576
14698
  (c) => c.density === "sparse" && c.value.some((v) => v < 0)
14577
14699
  ),
@@ -14608,14 +14730,14 @@ var draw = (options, rnd) => {
14608
14730
  barRhythms.push(scaleCell(cell.value));
14609
14731
  }
14610
14732
  const motifNoteCount = motifCell.value.filter((v) => v > 0).length + motifCell2.value.filter((v) => v > 0).length;
14611
- const archetype = pick2(MOTIF_ARCHETYPES, rnd);
14733
+ const archetype = pick(MOTIF_ARCHETYPES, rnd);
14612
14734
  const motifContour = [];
14613
14735
  let baseDegree = 0;
14614
14736
  for (let i2 = 0; i2 < motifNoteCount; i2++) {
14615
14737
  const idx = i2 % archetype.length;
14616
14738
  if (i2 > 0 && idx === 0) {
14617
14739
  const lastVal = motifContour[i2 - 1];
14618
- const connectShift = pick2([-1, 0, 1], rnd);
14740
+ const connectShift = pick([-1, 0, 1], rnd);
14619
14741
  baseDegree = lastVal + connectShift - archetype[0];
14620
14742
  }
14621
14743
  motifContour.push(baseDegree + archetype[idx]);
@@ -14633,12 +14755,12 @@ var draw = (options, rnd) => {
14633
14755
  let intervals = 0;
14634
14756
  let chromaticNotes = 0;
14635
14757
  let sungBars = 0;
14636
- let prevSemi = pick2([60, 64, 65, 67, 69, 72, 74, 76], rnd);
14758
+ let prevSemi = pick([60, 64, 65, 67, 69, 72, 74, 76], rnd);
14637
14759
  const pickFigure = (gapSteps, scale) => {
14638
14760
  const fits = ANSWER_FIGURES.filter(
14639
14761
  (f) => f.reduce((sum, v) => sum + scale(Math.abs(v)), 0) <= gapSteps
14640
14762
  );
14641
- return fits.length === 0 ? null : pick2(fits, rnd);
14763
+ return fits.length === 0 ? null : pick(fits, rnd);
14642
14764
  };
14643
14765
  const melodyGaps = (slots) => {
14644
14766
  const gaps = [];
@@ -14679,7 +14801,7 @@ var draw = (options, rnd) => {
14679
14801
  ).semi;
14680
14802
  const contourOffset = barInUnit(bar) === 0 ? 0 : barRhythms[bar - 1].filter((v) => v > 0).length;
14681
14803
  const prevRole = bar > 0 ? barRoles[bar - 1] : null;
14682
- const repeatShift = role === "motif" && (prevRole === "motif" || prevRole === "sequence" || prevRole === "climax") ? pick2([-2, -1, 1, 2], rnd) : 0;
14804
+ const repeatShift = role === "motif" && (prevRole === "motif" || prevRole === "sequence" || prevRole === "climax") ? pick([-2, -1, 1, 2], rnd) : 0;
14683
14805
  const degrees = barDegrees(
14684
14806
  role,
14685
14807
  slots,
@@ -14975,6 +15097,9 @@ var draw = (options, rnd) => {
14975
15097
  chordProgression,
14976
15098
  chordPattern,
14977
15099
  rootShift,
15100
+ keyName: resolvedKey.keyName,
15101
+ keyLabel: resolvedKey.keyLabel,
15102
+ moodLabel: resolvedKey.moodLabel,
14978
15103
  bpm,
14979
15104
  sections: sectionPlan,
14980
15105
  bars: totalBars,
@@ -15089,12 +15214,13 @@ var composeSong = (options) => {
15089
15214
  const rnd = options.random ?? Math.random;
15090
15215
  const recent = options.recent ?? [];
15091
15216
  const count = Math.max(1, options.drawCount ?? DRAW_COUNT);
15217
+ const resolvedKey = resolveComposeKey(options.baseKey, rnd);
15092
15218
  let best = null;
15093
15219
  let bestScore = Number.NEGATIVE_INFINITY;
15094
15220
  let bestIsValid = false;
15095
15221
  let rejected = 0;
15096
15222
  for (let attempt = 1; attempt <= count; attempt++) {
15097
- const d = draw(options, rnd);
15223
+ const d = draw(options, resolvedKey, rnd);
15098
15224
  const { stats, ok } = evaluate(d, recent);
15099
15225
  if (!ok) rejected++;
15100
15226
  const better = ok === bestIsValid ? stats.score > bestScore : ok;
@@ -15104,10 +15230,13 @@ var composeSong = (options) => {
15104
15230
  best = {
15105
15231
  // ドラムは勝った候補にだけ後から付ける(メロディに依存しないので
15106
15232
  // 候補ごとに引いても採点は動かず、40本ぶん無駄になる)。
15107
- drums: null,
15233
+ drum: "",
15108
15234
  chordProgression: d.chordProgression,
15109
15235
  chordPattern: d.chordPattern,
15110
15236
  rootShift: d.rootShift,
15237
+ keyName: d.keyName,
15238
+ keyLabel: d.keyLabel,
15239
+ moodLabel: d.moodLabel,
15111
15240
  bpm: d.bpm,
15112
15241
  sections: d.sections,
15113
15242
  bars: d.bars,
@@ -15120,32 +15249,14 @@ var composeSong = (options) => {
15120
15249
  const result = best;
15121
15250
  result.stats.attempts = count;
15122
15251
  result.stats.rejected = rejected;
15123
- const levels = new Array(result.bars).fill(1);
15124
- const crashBars = [];
15125
- const fillBars = [];
15126
- for (const section of result.sections) {
15127
- for (let i2 = 0; i2 < section.bars; i2++)
15128
- levels[section.startBar + i2] = section.spec.drumLevel;
15129
- crashBars.push(section.startBar + 1);
15130
- const fill = section.startBar + section.bars - 1;
15131
- if (fill > 0) fillBars.push(fill);
15132
- }
15133
- result.drums = composeDrumPattern({
15134
- bars: result.bars,
15135
- stepsPerBar: options.stepsPerBar,
15136
- rnd,
15137
- style: pickDrumStyle(result, rnd),
15138
- levels,
15139
- crashBars,
15140
- fillBars
15141
- });
15252
+ result.drum = pickBuiltinDrum(result, rnd);
15142
15253
  return result;
15143
15254
  };
15144
- var pickDrumStyle = (song, rnd) => {
15255
+ var pickBuiltinDrum = (song, rnd) => {
15145
15256
  const eighth = BASE_STEPS_PER_BAR / 8;
15146
15257
  const short = song.melody.filter((n) => n.durationSteps <= eighth).length / Math.max(1, song.melody.length);
15147
- 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"];
15148
- return pick2(pool, rnd);
15258
+ 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"];
15259
+ return pick(pool, rnd);
15149
15260
  };
15150
15261
  var LYRIC_WORDS = [
15151
15262
  "\u3042\u3055",
@@ -15193,7 +15304,7 @@ var composeLyrics = (melody, options) => {
15193
15304
  const quarter = stepsPerBar / 4;
15194
15305
  let buffer = [];
15195
15306
  const nextKana = () => {
15196
- if (buffer.length === 0) buffer = [...pick2(LYRIC_WORDS, rnd)];
15307
+ if (buffer.length === 0) buffer = [...pick(LYRIC_WORDS, rnd)];
15197
15308
  return buffer.shift();
15198
15309
  };
15199
15310
  const out = [];
@@ -15558,6 +15669,56 @@ var buildUI = (target, options) => {
15558
15669
  <span class="dtm-grow"></span>
15559
15670
  <span class="dtm-hint" data-dtm="compose-sections-len"></span>
15560
15671
  </div>
15672
+ <div class="dtm-row ${showCompose ? "" : "dtm-hidden"}" data-dtm="compose-key-row">
15673
+ <span class="dtm-label">\u30D9\u30FC\u30B9\u8ABF</span>
15674
+ <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">
15675
+ <option value="any" title="\u516824\u8ABF\u304B\u3089\u30E9\u30F3\u30C0\u30E0\u306B\u6C7A\u5B9A\u3057\u307E\u3059">\u5E0C\u671B\u306A\u3057</option>
15676
+ <optgroup label="\u57FA\u672C">
15677
+ <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>
15678
+ <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>
15679
+ </optgroup>
15680
+ <optgroup label="\u96F0\u56F2\u6C17\u304B\u3089\u9078\u3076\uFF08\u62BD\u9078\uFF09">
15681
+ <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>
15682
+ <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>
15683
+ <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>
15684
+ <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>
15685
+ <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>
15686
+ <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>
15687
+ <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>
15688
+ <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>
15689
+ </optgroup>
15690
+ <optgroup label="\u9577\u8ABF\uFF08\u500B\u5225\u6307\u5B9A\uFF09">
15691
+ <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>
15692
+ <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>
15693
+ <option value="key_D" title="\u610F\u6C17\u63DA\u3005\u3068\u3057\u305F\u3001\u52DD\u5229\u306E\u3001\u558A\u58F0">\u30CB\u9577\u8ABF (D)</option>
15694
+ <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>
15695
+ <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>
15696
+ <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>
15697
+ <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>
15698
+ <option value="key_G" title="\u53B3\u7C9B\u306A\u3001\u5D07\u9AD8\u306A\u3001\u5E7B\u60F3\u3001\u8AA0\u5B9F">\u30C8\u9577\u8ABF (G)</option>
15699
+ <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>
15700
+ <option value="key_A" title="\u3046\u308C\u3057\u3044\u3001\u7267\u6B4C\u7684\u306A\u3001\u611B\u306E\u544A\u767D">\u30A4\u9577\u8ABF (A)</option>
15701
+ <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>
15702
+ <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>
15703
+ </optgroup>
15704
+ <optgroup label="\u77ED\u8ABF\uFF08\u500B\u5225\u6307\u5B9A\uFF09">
15705
+ <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>
15706
+ <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>
15707
+ <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>
15708
+ <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>
15709
+ <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>
15710
+ <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>
15711
+ <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>
15712
+ <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>
15713
+ <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>
15714
+ <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>
15715
+ <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>
15716
+ <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>
15717
+ </optgroup>
15718
+ </select>
15719
+ <span class="dtm-grow"></span>
15720
+ <span class="dtm-hint" data-dtm="compose-key-hint"></span>
15721
+ </div>
15561
15722
  <div class="dtm-row">
15562
15723
  <span class="dtm-label">\u5168\u4F53\u30B7\u30D5\u30C8</span>
15563
15724
  <select class="dtm-select" data-dtm="shift-select">
@@ -15753,6 +15914,8 @@ var buildUI = (target, options) => {
15753
15914
  macroCompose: sel("macro-compose"),
15754
15915
  composeSections: sel("compose-sections"),
15755
15916
  composeSectionsLen: sel("compose-sections-len"),
15917
+ composeKey: sel("compose-key"),
15918
+ composeKeyHint: sel("compose-key-hint"),
15756
15919
  macroComposeVocal: sel("macro-compose-vocal"),
15757
15920
  macroComposeInfo: sel("macro-compose-info"),
15758
15921
  macroClear: sel("macro-clear"),
@@ -19524,6 +19687,7 @@ var mountDAW = (target, options = {}) => {
19524
19687
  redrawAll();
19525
19688
  },
19526
19689
  onEnd: (interrupted) => {
19690
+ options.onScheduleFade?.(null);
19527
19691
  if (interrupted) {
19528
19692
  playbackState = "paused";
19529
19693
  pausedPlayStep = currentPlayStep;
@@ -19615,15 +19779,20 @@ var mountDAW = (target, options = {}) => {
19615
19779
  }
19616
19780
  if (fadeOutSec > 0) {
19617
19781
  const endStep = getSongEndStepExact();
19618
- const totalDurationSec = (endStep - fromStep) * secondsPerStepFade;
19619
- if (totalDurationSec > 0) {
19620
- const fadeOutEndAt = anchor + totalDurationSec;
19621
- const earliestStart = params.fadeInEndAt ?? anchor;
19622
- params.fadeOutEndAt = fadeOutEndAt;
19623
- params.fadeOutStartAt = Math.max(
19624
- fadeOutEndAt - fadeOutSec,
19625
- earliestStart
19626
- );
19782
+ if (endStep > fromStep) {
19783
+ const totalDurationSec = (endStep - fromStep) * secondsPerStepFade;
19784
+ if (totalDurationSec > 0.1) {
19785
+ const fadeOutEndAt = anchor + totalDurationSec;
19786
+ const earliestStart = params.fadeInEndAt ?? anchor;
19787
+ const fadeOutStartAt = Math.max(
19788
+ fadeOutEndAt - fadeOutSec,
19789
+ earliestStart
19790
+ );
19791
+ if (fadeOutStartAt < fadeOutEndAt) {
19792
+ params.fadeOutStartAt = fadeOutStartAt;
19793
+ params.fadeOutEndAt = fadeOutEndAt;
19794
+ }
19795
+ }
19627
19796
  }
19628
19797
  }
19629
19798
  options.onScheduleFade?.(params);
@@ -21648,6 +21817,16 @@ var mountDAW = (target, options = {}) => {
21648
21817
  };
21649
21818
  refs.composeSections.addEventListener("change", updateComposeSectionsLen);
21650
21819
  updateComposeSectionsLen();
21820
+ const updateComposeKeyHint = () => {
21821
+ if (!refs.composeKey || !refs.composeKeyHint) return;
21822
+ const desc = getComposeKeyDescription(refs.composeKey.value);
21823
+ refs.composeKeyHint.textContent = desc;
21824
+ refs.composeKeyHint.title = desc;
21825
+ };
21826
+ if (refs.composeKey) {
21827
+ refs.composeKey.addEventListener("change", updateComposeKeyHint);
21828
+ updateComposeKeyHint();
21829
+ }
21651
21830
  const runCompose = (withVocal) => {
21652
21831
  stop();
21653
21832
  overlayDuring(() => {
@@ -21655,6 +21834,7 @@ var mountDAW = (target, options = {}) => {
21655
21834
  stepsPerBar: renderConfig.stepsPerBar,
21656
21835
  edo: renderConfig.edo,
21657
21836
  sections: selectedComposeSections(),
21837
+ baseKey: refs.composeKey?.value ?? "any",
21658
21838
  // 直近に作った曲の特徴を渡すと、それらから離れた候補に加点される。
21659
21839
  // 「作曲」を続けて押したときに似た曲が並ぶのを防ぐ。
21660
21840
  recent: recentComposeFingerprints
@@ -21727,7 +21907,10 @@ var mountDAW = (target, options = {}) => {
21727
21907
  }
21728
21908
  }
21729
21909
  setBpm(song.bpm);
21730
- applyComposedDrumPattern(song.drums.def);
21910
+ currentDrumPattern = song.drum;
21911
+ refs.drumSelect.value = song.drum;
21912
+ options.onDrumChange?.(song.drum);
21913
+ applyDrumPatternFont(song.drum);
21731
21914
  for (const t of bassTracksForCompose()) {
21732
21915
  t.trackCompression = AUTO_ROLE_MIX.bass.compression;
21733
21916
  options.onTrackCompressionChange?.(t.config.id, t.trackCompression);
@@ -21746,6 +21929,10 @@ var mountDAW = (target, options = {}) => {
21746
21929
  fireLyricsChange(melodyTrack);
21747
21930
  }
21748
21931
  }
21932
+ if (refs.composeKeyHint) {
21933
+ refs.composeKeyHint.textContent = `${song.keyLabel} \u3067\u4F5C\u6210`;
21934
+ refs.composeKeyHint.title = `${song.keyLabel}${song.moodLabel ? `\uFF08${song.moodLabel}\uFF09` : ""}`;
21935
+ }
21749
21936
  playStartStep = 0;
21750
21937
  redrawAll();
21751
21938
  updateTrackPanel();
@@ -22096,9 +22283,6 @@ var mountDAW = (target, options = {}) => {
22096
22283
  extractedMidiDrum = patternDef.pattern;
22097
22284
  installDrumPattern("_extracted_midi", patternDef);
22098
22285
  };
22099
- const applyComposedDrumPattern = (patternDef) => {
22100
- installDrumPattern("_composed", patternDef);
22101
- };
22102
22286
  const wireMidi = () => {
22103
22287
  refs.midiInput.addEventListener("change", async () => {
22104
22288
  const file = refs.midiInput.files?.[0];
@@ -24057,6 +24241,8 @@ var createDtmStudio = async (options = {}) => {
24057
24241
  };
24058
24242
  };
24059
24243
  const resumeAudio = () => {
24244
+ fadeGain.gain.cancelScheduledValues(audioCtx.currentTime);
24245
+ fadeGain.gain.setValueAtTime(1, audioCtx.currentTime);
24060
24246
  if (audioCtx.state === "closed") return Promise.resolve();
24061
24247
  return audioCtx.resume();
24062
24248
  };
@@ -24618,7 +24804,7 @@ var createDtmStudio = async (options = {}) => {
24618
24804
  await loadInstrument(key);
24619
24805
  }
24620
24806
  };
24621
- void Promise.all([
24807
+ const presetPromise = Promise.all([
24622
24808
  loadPreset(
24623
24809
  playerPreset,
24624
24810
  loadTrackIds,
@@ -24658,25 +24844,29 @@ var createDtmStudio = async (options = {}) => {
24658
24844
  duration: e.duration
24659
24845
  });
24660
24846
  };
24847
+ const { onResumeAudio: userOnResumeAudio, ...playerOpts } = opts;
24661
24848
  const player = mountMmlPlayer(target, mml, {
24849
+ ...playerOpts,
24662
24850
  getAudioTime: () => audioCtx.currentTime,
24663
24851
  onResumeAudio: async () => {
24664
24852
  await resumeAudio();
24853
+ await presetPromise;
24665
24854
  if (meta.drum) {
24666
24855
  await loadRequiredDrums(
24667
24856
  getDrumPatternKeys(
24668
24857
  meta.drum,
24669
- resolveDrumPatterns(opts.drumPatterns ?? options.drumPatterns)
24858
+ resolveDrumPatterns(
24859
+ playerOpts.drumPatterns ?? options.drumPatterns
24860
+ )
24670
24861
  ),
24671
24862
  meta.drumFont || "FluidR3_GM_sf2_file:0"
24672
24863
  );
24673
24864
  }
24674
- await opts.onResumeAudio?.();
24865
+ await userOnResumeAudio?.();
24675
24866
  },
24676
24867
  onPlayNote: playPlayerNote,
24677
24868
  onPlayDrum: playDrum,
24678
- singingVoices,
24679
- ...opts
24869
+ singingVoices
24680
24870
  });
24681
24871
  mountedPlayers.push(player);
24682
24872
  const destroy = () => {
@@ -24709,7 +24899,7 @@ var createDtmStudio = async (options = {}) => {
24709
24899
  await loadInstrument(key);
24710
24900
  }
24711
24901
  };
24712
- void Promise.all([
24902
+ const presetPromise = Promise.all([
24713
24903
  loadPreset(
24714
24904
  playerPreset,
24715
24905
  loadTrackIds,
@@ -24749,8 +24939,9 @@ var createDtmStudio = async (options = {}) => {
24749
24939
  duration: e.duration
24750
24940
  });
24751
24941
  };
24942
+ const { onResumeAudio: userOnResumeAudio, ...playOpts } = opts;
24752
24943
  return playMML(mml, {
24753
- ...opts,
24944
+ ...playOpts,
24754
24945
  audioContext: audioCtx,
24755
24946
  destination: masterGain,
24756
24947
  synth: false,
@@ -24758,15 +24949,18 @@ var createDtmStudio = async (options = {}) => {
24758
24949
  onPlayDrum: playDrum,
24759
24950
  onResumeAudio: async () => {
24760
24951
  await resumeAudio();
24952
+ await presetPromise;
24761
24953
  if (meta.drum) {
24762
24954
  await loadRequiredDrums(
24763
24955
  getDrumPatternKeys(
24764
24956
  meta.drum,
24765
- resolveDrumPatterns(opts.drumPatterns ?? options.drumPatterns)
24957
+ resolveDrumPatterns(
24958
+ playOpts.drumPatterns ?? options.drumPatterns
24959
+ )
24766
24960
  )
24767
24961
  );
24768
24962
  }
24769
- await opts.onResumeAudio?.();
24963
+ await userOnResumeAudio?.();
24770
24964
  }
24771
24965
  });
24772
24966
  };
@@ -24793,7 +24987,7 @@ var createDtmStudio = async (options = {}) => {
24793
24987
  await loadInstrument(key);
24794
24988
  }
24795
24989
  };
24796
- void Promise.all([
24990
+ const presetPromise = Promise.all([
24797
24991
  loadPreset(
24798
24992
  playerPreset,
24799
24993
  loadTrackIds,
@@ -24833,8 +25027,9 @@ var createDtmStudio = async (options = {}) => {
24833
25027
  duration: e.duration
24834
25028
  });
24835
25029
  };
25030
+ const { onResumeAudio: userOnResumeAudio, ...playOpts } = opts;
24836
25031
  return playSingingMML(mml, {
24837
- ...opts,
25032
+ ...playOpts,
24838
25033
  audioContext: audioCtx,
24839
25034
  destination: masterGain,
24840
25035
  synth: false,
@@ -24843,15 +25038,18 @@ var createDtmStudio = async (options = {}) => {
24843
25038
  onPlayDrum: playDrum,
24844
25039
  onResumeAudio: async () => {
24845
25040
  await resumeAudio();
25041
+ await presetPromise;
24846
25042
  if (meta.drum) {
24847
25043
  await loadRequiredDrums(
24848
25044
  getDrumPatternKeys(
24849
25045
  meta.drum,
24850
- resolveDrumPatterns(opts.drumPatterns ?? options.drumPatterns)
25046
+ resolveDrumPatterns(
25047
+ playOpts.drumPatterns ?? options.drumPatterns
25048
+ )
24851
25049
  )
24852
25050
  );
24853
25051
  }
24854
- await opts.onResumeAudio?.();
25052
+ await userOnResumeAudio?.();
24855
25053
  }
24856
25054
  });
24857
25055
  };
@@ -24888,9 +25086,7 @@ var createDtmStudio = async (options = {}) => {
24888
25086
  if (!sfInst) return;
24889
25087
  const vol = options2.volume ?? 80;
24890
25088
  const dur = options2.duration ?? 1;
24891
- if (audioCtx.state === "suspended") {
24892
- await audioCtx.resume();
24893
- }
25089
+ await resumeAudio();
24894
25090
  const { midi, detuneCents } = unitsToMidiDetune(options2.pitchUnits);
24895
25091
  sfInst.play({
24896
25092
  ctx: audioCtx,
@@ -24920,7 +25116,7 @@ var createDtmStudio = async (options = {}) => {
24920
25116
  velocity: p.velocity
24921
25117
  }));
24922
25118
  const playerPreset = defaultPreset;
24923
- void loadPreset(playerPreset, ["chord"]);
25119
+ const presetPromise = loadPreset(playerPreset, ["chord"]);
24924
25120
  const playPlayerNote = (e) => {
24925
25121
  const sfInst = resolveSoundFont(playerPreset, "chord");
24926
25122
  if (!sfInst) return;
@@ -24936,16 +25132,21 @@ var createDtmStudio = async (options = {}) => {
24936
25132
  duration: e.duration
24937
25133
  });
24938
25134
  };
25135
+ const { onResumeAudio: userOnResumeAudio, ...chordOpts } = opts;
24939
25136
  return playPlacements(placements, {
24940
- ...opts,
25137
+ ...chordOpts,
24941
25138
  audioContext: audioCtx,
24942
25139
  destination: masterGain,
24943
25140
  synth: false,
24944
25141
  onPlayNote: playPlayerNote,
24945
25142
  onPlayDrum: playDrum,
24946
- onResumeAudio: resumeAudio,
25143
+ onResumeAudio: async () => {
25144
+ await resumeAudio();
25145
+ await presetPromise;
25146
+ await userOnResumeAudio?.();
25147
+ },
24947
25148
  bpm,
24948
- metaVolume: opts.volume ?? 100
25149
+ metaVolume: chordOpts.volume ?? 100
24949
25150
  });
24950
25151
  };
24951
25152
  const mountChordPlayerInstance = (target, chords, options2 = {}) => {
@@ -25072,6 +25273,8 @@ var createDtmStudio = async (options = {}) => {
25072
25273
  A4_UNITS,
25073
25274
  BREATH_MARK,
25074
25275
  CENTS_PER_UNIT,
25276
+ COMPOSE_KEYS,
25277
+ COMPOSE_MOOD_GROUPS,
25075
25278
  CORPUS_BANDS,
25076
25279
  CORPUS_SIZE,
25077
25280
  DAW_CSS,
@@ -25098,8 +25301,10 @@ var createDtmStudio = async (options = {}) => {
25098
25301
  KOE_VOICEBANK_LABELS,
25099
25302
  KOE_VOICEBANK_TERMS,
25100
25303
  LinkedList,
25304
+ MAJOR_KEY_IDS,
25101
25305
  MAX_VOCAL_VOLUME,
25102
25306
  MICRO_STEP,
25307
+ MINOR_KEY_IDS,
25103
25308
  MMLCore,
25104
25309
  MML_END_MARKER,
25105
25310
  MidiSearchClient,
@@ -25168,6 +25373,7 @@ var createDtmStudio = async (options = {}) => {
25168
25373
  formatMmlMeta,
25169
25374
  freqFromPitch,
25170
25375
  generateRandomPattern,
25376
+ getComposeKeyDescription,
25171
25377
  getDrumPatternKeys,
25172
25378
  getMidiBPM,
25173
25379
  icon,
@@ -25197,6 +25403,7 @@ var createDtmStudio = async (options = {}) => {
25197
25403
  playNote,
25198
25404
  playPlacements,
25199
25405
  playSingingMML,
25406
+ resolveComposeKey,
25200
25407
  resolveDrumPattern,
25201
25408
  resolveLoopPoint,
25202
25409
  semitonesToUnits,