@onjmin/dtm 2.1.10 → 2.1.11

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
@@ -1763,11 +1763,19 @@ var createChannelStrip = (ctx, destination, options = {}) => {
1763
1763
  const duckGain = ctx.createGain();
1764
1764
  duckGain.gain.value = 1;
1765
1765
  const DUCK_ATTACK_SEC = 8e-3;
1766
+ let duckReleaseEndAt = 0;
1766
1767
  const duck = (atTime, depth = 0.3, releaseSec = 0.18) => {
1767
1768
  const t = Math.max(atTime, ctx.currentTime);
1768
1769
  const floor = clamp(1 - depth, 0, 1);
1769
- duckGain.gain.linearRampToValueAtTime(floor, t + DUCK_ATTACK_SEC);
1770
- duckGain.gain.linearRampToValueAtTime(1, t + DUCK_ATTACK_SEC + releaseSec);
1770
+ const param = duckGain.gain;
1771
+ if (t >= duckReleaseEndAt) {
1772
+ param.setValueAtTime(1, t);
1773
+ } else if (typeof param.cancelAndHoldAtTime === "function") {
1774
+ param.cancelAndHoldAtTime(t);
1775
+ }
1776
+ param.linearRampToValueAtTime(floor, t + DUCK_ATTACK_SEC);
1777
+ param.linearRampToValueAtTime(1, t + DUCK_ATTACK_SEC + releaseSec);
1778
+ duckReleaseEndAt = t + DUCK_ATTACK_SEC + releaseSec;
1771
1779
  };
1772
1780
  eqHigh.connect(compressor);
1773
1781
  compressor.connect(duckGain);
@@ -2064,12 +2072,15 @@ var buildChordPlacements = (options) => {
2064
2072
  }
2065
2073
  }
2066
2074
  } else if (patternType === "alternating") {
2075
+ const quarterSteps = Math.floor(stepsPerBar / 4);
2067
2076
  notes.forEach((noteOffset, i2) => {
2068
- const stepOffset = i2 * Math.floor(stepsPerBar / 4);
2077
+ const stepOffset = i2 * quarterSteps;
2078
+ const isLast = i2 === notes.length - 1;
2079
+ const durationSteps = isLast ? Math.max(12, noteLength - stepOffset) : Math.max(12, quarterSteps);
2069
2080
  placements.push({
2070
2081
  startStep: chord.whenStep + stepOffset,
2071
2082
  pitchUnits: toUnits(noteOffset),
2072
- durationSteps: Math.max(12, Math.floor(stepsPerBar / 4)),
2083
+ durationSteps,
2073
2084
  velocity: 100
2074
2085
  });
2075
2086
  });
@@ -4697,6 +4708,45 @@ var createReverbImpulse = (ctx, decaySec = DEFAULT_REVERB_DECAY_SEC) => {
4697
4708
  };
4698
4709
  var reverbAmountToGain = (amount) => Math.max(0, Math.min(100, amount)) / 100;
4699
4710
 
4711
+ // src/safety-limiter.ts
4712
+ var THRESHOLD_DB = -1;
4713
+ var KNEE_DB = 0;
4714
+ var RATIO = 20;
4715
+ var ATTACK_SEC = 1e-3;
4716
+ var RELEASE_SEC = 0.1;
4717
+ var CURVE_SAMPLES2 = 2048;
4718
+ var SOFT_KNEE = 0.85;
4719
+ var CEILING = 0.98;
4720
+ var SHAPER_RANGE = 4;
4721
+ var OVERSAMPLE2 = "none";
4722
+ var createSoftClipCurve = () => {
4723
+ const curve = new Float32Array(CURVE_SAMPLES2);
4724
+ const span = CEILING - SOFT_KNEE;
4725
+ for (let i2 = 0; i2 < CURVE_SAMPLES2; i2++) {
4726
+ const x2 = (i2 / (CURVE_SAMPLES2 - 1) * 2 - 1) * SHAPER_RANGE;
4727
+ const a = Math.abs(x2);
4728
+ curve[i2] = Math.sign(x2) * (a <= SOFT_KNEE ? a : SOFT_KNEE + span * Math.tanh((a - SOFT_KNEE) / span));
4729
+ }
4730
+ return curve;
4731
+ };
4732
+ var createSafetyLimiter = (ctx, destination) => {
4733
+ const limiter = ctx.createDynamicsCompressor();
4734
+ limiter.threshold.value = THRESHOLD_DB;
4735
+ limiter.knee.value = KNEE_DB;
4736
+ limiter.ratio.value = RATIO;
4737
+ limiter.attack.value = ATTACK_SEC;
4738
+ limiter.release.value = RELEASE_SEC;
4739
+ const preScale = ctx.createGain();
4740
+ preScale.gain.value = 1 / SHAPER_RANGE;
4741
+ const softClip = ctx.createWaveShaper();
4742
+ softClip.curve = createSoftClipCurve();
4743
+ softClip.oversample = OVERSAMPLE2;
4744
+ limiter.connect(preScale);
4745
+ preScale.connect(softClip);
4746
+ softClip.connect(destination);
4747
+ return limiter;
4748
+ };
4749
+
4700
4750
  // src/sequencer.ts
4701
4751
  var STEPS_PER_BEAT = 48;
4702
4752
  var PLAN_TIME = 0.5;
@@ -7160,6 +7210,9 @@ var SONG_DRUM_PATTERNS = {
7160
7210
 
7161
7211
  // src/synth.ts
7162
7212
  var freqFromPitch = (pitchUnits) => unitsToHz(pitchUnits);
7213
+ var MIN_ATTACK_SEC = 15e-4;
7214
+ var MIN_RELEASE_SEC = 4e-3;
7215
+ var TAIL_RATIO = 1e-3;
7163
7216
  var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
7164
7217
  const wave = tone.wave ?? "square";
7165
7218
  const attack = tone.attack ?? 0;
@@ -7178,25 +7231,25 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
7178
7231
  osc.frequency.value = freqFromPitch(e.pitchUnits);
7179
7232
  const t0 = ctx.currentTime + e.when;
7180
7233
  const peak = Math.max(1e-4, 0.06 * e.volume * 1.5 * gainScale);
7234
+ const tail = peak * TAIL_RATIO;
7235
+ const stopAt = t0 + e.duration + MIN_RELEASE_SEC;
7181
7236
  if (tone.decay) {
7182
7237
  gain.gain.setValueAtTime(1e-4, t0);
7183
7238
  gain.gain.linearRampToValueAtTime(peak, t0 + Math.max(3e-3, attack));
7184
- gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
7239
+ gain.gain.exponentialRampToValueAtTime(tail, t0 + e.duration);
7185
7240
  } else {
7186
7241
  const releaseTime = Math.min(0.02, e.duration * 0.1);
7187
7242
  const sustainDuration = e.duration - releaseTime;
7188
- if (attack > 0) {
7189
- gain.gain.setValueAtTime(1e-4, t0);
7190
- gain.gain.linearRampToValueAtTime(
7191
- peak,
7192
- t0 + Math.min(attack, sustainDuration)
7193
- );
7194
- } else {
7195
- gain.gain.setValueAtTime(peak, t0);
7196
- }
7243
+ const attackTime = Math.min(
7244
+ Math.max(attack, MIN_ATTACK_SEC),
7245
+ sustainDuration
7246
+ );
7247
+ gain.gain.setValueAtTime(1e-4, t0);
7248
+ gain.gain.linearRampToValueAtTime(peak, t0 + attackTime);
7197
7249
  gain.gain.setValueAtTime(peak, t0 + sustainDuration);
7198
- gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
7250
+ gain.gain.exponentialRampToValueAtTime(tail, t0 + e.duration);
7199
7251
  }
7252
+ gain.gain.linearRampToValueAtTime(0, stopAt);
7200
7253
  osc.connect(gain);
7201
7254
  let panner = null;
7202
7255
  if (typeof ctx.createStereoPanner === "function" && e.pan) {
@@ -7208,7 +7261,7 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
7208
7261
  gain.connect(compressor);
7209
7262
  }
7210
7263
  osc.start(t0);
7211
- osc.stop(t0 + e.duration + 0.02);
7264
+ osc.stop(stopAt);
7212
7265
  osc.onended = () => {
7213
7266
  osc.disconnect();
7214
7267
  gain.disconnect();
@@ -7223,14 +7276,21 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
7223
7276
  if (isKick) {
7224
7277
  const osc = ctx.createOscillator();
7225
7278
  const g2 = ctx.createGain();
7279
+ const kickPeak = vol * 0.135;
7280
+ const kickDecayEnd = t0 + 0.18;
7281
+ const kickEnd = kickDecayEnd + MIN_RELEASE_SEC;
7226
7282
  osc.frequency.setValueAtTime(150, t0);
7227
7283
  osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
7228
- g2.gain.setValueAtTime(vol * 0.135, t0);
7229
- g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
7284
+ g2.gain.setValueAtTime(kickPeak, t0);
7285
+ g2.gain.exponentialRampToValueAtTime(kickPeak * TAIL_RATIO, kickDecayEnd);
7286
+ g2.gain.linearRampToValueAtTime(0, kickEnd);
7230
7287
  osc.connect(g2).connect(compressor);
7231
7288
  osc.start(t0);
7232
- osc.stop(t0 + 0.2);
7233
- osc.onended = () => osc.disconnect();
7289
+ osc.stop(kickEnd);
7290
+ osc.onended = () => {
7291
+ osc.disconnect();
7292
+ g2.disconnect();
7293
+ };
7234
7294
  return;
7235
7295
  }
7236
7296
  const dur = isSnareLike ? 0.18 : 0.05;
@@ -7244,8 +7304,13 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
7244
7304
  filter.type = isSnareLike ? "bandpass" : "highpass";
7245
7305
  filter.frequency.value = isSnareLike ? 2e3 : 8e3;
7246
7306
  const g = ctx.createGain();
7247
- g.gain.setValueAtTime(vol * (isSnareLike ? 0.105 : 0.06), t0);
7248
- g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
7307
+ const noisePeak = vol * (isSnareLike ? 0.105 : 0.06);
7308
+ g.gain.setValueAtTime(noisePeak, t0);
7309
+ g.gain.exponentialRampToValueAtTime(
7310
+ noisePeak * TAIL_RATIO,
7311
+ t0 + dur - MIN_RELEASE_SEC
7312
+ );
7313
+ g.gain.linearRampToValueAtTime(0, t0 + dur);
7249
7314
  src.connect(filter).connect(g).connect(compressor);
7250
7315
  src.start(t0);
7251
7316
  src.stop(t0 + dur);
@@ -7307,7 +7372,7 @@ var playPlacements = (placements, options) => {
7307
7372
  reverbPreDelay.connect(reverbConvolver);
7308
7373
  reverbConvolver.connect(reverbWetGain);
7309
7374
  reverbWetGain.connect(finalMix);
7310
- finalMix.connect(rawDestination);
7375
+ finalMix.connect(createSafetyLimiter(ctx, rawDestination));
7311
7376
  const channelStrips = /* @__PURE__ */ new Map();
7312
7377
  const getChannelStrip = (index) => {
7313
7378
  let strip = channelStrips.get(index);
@@ -9762,17 +9827,24 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9762
9827
  if (!audioCtx) audioCtx = new AudioContext();
9763
9828
  return audioCtx;
9764
9829
  };
9830
+ let limiterNode = null;
9831
+ const ensureOutput = () => {
9832
+ const ctx = ensureCtx();
9833
+ if (!limiterNode) limiterNode = createSafetyLimiter(ctx, ctx.destination);
9834
+ return limiterNode;
9835
+ };
9765
9836
  let synthInstance = null;
9766
9837
  const ensureSynth = () => {
9767
- if (!synthInstance) synthInstance = createSynth(ensureCtx());
9838
+ if (!synthInstance) {
9839
+ synthInstance = createSynth(ensureCtx(), ensureOutput());
9840
+ }
9768
9841
  return synthInstance;
9769
9842
  };
9770
9843
  let voices = null;
9771
9844
  const ensureVoices = () => {
9772
9845
  if (options.singingVoices) return options.singingVoices;
9773
9846
  if (!voices) {
9774
- const ctx = ensureCtx();
9775
- voices = createSingingVoices(ctx, ctx.destination);
9847
+ voices = createSingingVoices(ensureCtx(), ensureOutput());
9776
9848
  voices.setVolume(trackVolume / 100 * (masterVolume / 100));
9777
9849
  }
9778
9850
  return voices;
@@ -10834,6 +10906,12 @@ var SoundFont = class _SoundFont {
10834
10906
  // ファミリごとに変える。
10835
10907
  /** アタック(無音からピークまで)秒。クリック防止の最小限。全楽器共通。 */
10836
10908
  static attackSec = 5e-3;
10909
+ /**
10910
+ * 消え際に最低限確保する秒数。アタックと対になるクリック防止で、**サンプルが
10911
+ * 終わるまでに**必ずここまでに 0 へ落とし切る。振幅が残ったままバッファが尽きたり
10912
+ * `stop()` が来たりすると、その瞬間の値がそのまま段差になりプチノイズが出る。
10913
+ */
10914
+ static minReleaseSec = 4e-3;
10837
10915
  /**
10838
10916
  * 減衰の型ごとのエンベロープ。
10839
10917
  *
@@ -11020,14 +11098,23 @@ var SoundFont = class _SoundFont {
11020
11098
  const startGainTime = Math.max(ctx.currentTime, _when);
11021
11099
  g.gain.setValueAtTime(0, startGainTime);
11022
11100
  const env = _SoundFont.envelopes[this.style.env] ?? _SoundFont.envelopes.sustain;
11023
- const limit = src.loop ? Number.POSITIVE_INFINITY : _when + _param.max - startOffsetSec;
11101
+ const playRate = _param.playbackRate * 2 ** ((humanizeCents + detuneCents) / 1200);
11102
+ const sampleEnd = _when + (buffer.duration - startOffsetSec) / playRate;
11103
+ const limit = src.loop ? Number.POSITIVE_INFINITY : sampleEnd;
11024
11104
  const attackEnd = Math.min(startGainTime + _SoundFont.attackSec, limit);
11025
11105
  const decayEnd = Math.min(
11026
11106
  Math.max(attackEnd, attackEnd + env.decaySec),
11027
11107
  limit
11028
11108
  );
11029
- const noteOff = Math.min(Math.max(decayEnd, _when + duration), limit);
11030
- const end = isDrum ? _when + buffer.duration - startOffsetSec : Math.max(Math.min(noteOff + env.releaseSec, limit), noteOff + 1e-3);
11109
+ const tailRoom = Math.min(
11110
+ _SoundFont.minReleaseSec,
11111
+ Math.max(0, limit - decayEnd)
11112
+ );
11113
+ const noteOff = Math.min(
11114
+ Math.max(decayEnd, _when + duration),
11115
+ limit - tailRoom
11116
+ );
11117
+ const end = isDrum ? sampleEnd : Math.max(Math.min(noteOff + env.releaseSec, limit), noteOff + tailRoom);
11031
11118
  if (!isDrum) {
11032
11119
  const sustainVolume = effectiveVolume * env.sustain;
11033
11120
  g.gain.linearRampToValueAtTime(effectiveVolume, attackEnd);
@@ -11036,6 +11123,11 @@ var SoundFont = class _SoundFont {
11036
11123
  g.gain.linearRampToValueAtTime(0, end);
11037
11124
  } else {
11038
11125
  g.gain.linearRampToValueAtTime(effectiveVolume, attackEnd);
11126
+ const releaseStart = Math.max(attackEnd, end - _SoundFont.minReleaseSec);
11127
+ if (releaseStart > attackEnd) {
11128
+ g.gain.setValueAtTime(effectiveVolume, releaseStart);
11129
+ }
11130
+ g.gain.linearRampToValueAtTime(0, end);
11039
11131
  }
11040
11132
  if (filter) src.connect(filter).connect(g);
11041
11133
  else src.connect(g);
@@ -11245,12 +11337,10 @@ var addParam = (zone, pitch) => {
11245
11337
  coarseTune,
11246
11338
  fineTune,
11247
11339
  sampleRate,
11248
- delay,
11249
- buffer
11340
+ delay
11250
11341
  } = zone;
11251
11342
  const baseDetune = originalPitch - 100 * coarseTune - fineTune;
11252
11343
  const playbackRate = 2 ** ((100 * pitch - baseDetune) / 1200);
11253
- const max = (buffer?.duration ?? 0) / playbackRate;
11254
11344
  const src = {
11255
11345
  loop: loopStart >= 1 && loopStart < loopEnd
11256
11346
  };
@@ -11258,7 +11348,7 @@ var addParam = (zone, pitch) => {
11258
11348
  [src.loopStart, src.loopEnd] = [loopStart, loopEnd].map(
11259
11349
  (v) => v / sampleRate + delay
11260
11350
  );
11261
- zone._param = { playbackRate, max, src };
11351
+ zone._param = { playbackRate, src };
11262
11352
  };
11263
11353
 
11264
11354
  // src/chord-player.ts
@@ -12556,29 +12646,30 @@ var CORPUS_BANDS = {
12556
12646
  climaxPeaks: [1, 2, 13.5, 26],
12557
12647
  complementarity: [0, 0.026, 0.179, 0.405]
12558
12648
  };
12559
- var CORPUS_MEDIANS = {
12560
- entropy: 0.907,
12561
- valueKinds: 4,
12562
- restRatio: 0.149,
12563
- leapRatio: 0.339,
12564
- stepRatio: 0.538,
12565
- chromaticRatio: 0.052,
12566
- maxLeap: 12,
12567
- melodyRange: 20,
12568
- notesPerBar: 5.511,
12569
- shortNoteRatio: 0.816,
12570
- barDensityCv: 0.279,
12571
- densityCliff: 0.026,
12572
- sim1: 0.518,
12573
- sim2: 0.552,
12574
- sim4: 0.616,
12575
- sim8: 0.676,
12576
- phraseBreath: 0.227,
12577
- turnRatio: 0.519,
12578
- climaxPosition: 0.273,
12579
- climaxPeaks: 6,
12580
- complementarity: 0.075
12581
- };
12649
+ var CORPUS_PROFILE_KEYS = [
12650
+ "entropy",
12651
+ "valueKinds",
12652
+ "restRatio",
12653
+ "leapRatio",
12654
+ "stepRatio",
12655
+ "chromaticRatio",
12656
+ "maxLeap",
12657
+ "melodyRange",
12658
+ "notesPerBar",
12659
+ "shortNoteRatio",
12660
+ "barDensityCv",
12661
+ "densityCliff",
12662
+ "sim1",
12663
+ "sim2",
12664
+ "sim4",
12665
+ "sim8",
12666
+ "phraseBreath",
12667
+ "turnRatio",
12668
+ "climaxPosition",
12669
+ "climaxPeaks",
12670
+ "complementarity"
12671
+ ];
12672
+ var CORPUS_DEVIATION_BUDGET = 4;
12582
12673
  var CORPUS_CELL_WEIGHTS = {
12583
12674
  "0,2,4,6,8,10,12,14": 584,
12584
12675
  "0,2,4,6,8,12,14": 512,
@@ -13443,16 +13534,15 @@ var tensionFeatures = (barTension, opts) => {
13443
13534
  return { rise, resolve };
13444
13535
  };
13445
13536
  var band = (v, lo, idealLo, idealHi, hi) => {
13446
- if (v <= lo || v >= hi) return 0;
13447
13537
  if (v >= idealLo && v <= idealHi) return 1;
13538
+ if (v <= lo || v >= hi) return 0;
13448
13539
  if (v < idealLo) return idealLo === lo ? 0 : (v - lo) / (idealLo - lo);
13449
13540
  return hi === idealHi ? 0 : (hi - v) / (hi - idealHi);
13450
13541
  };
13451
- var centeredBand = (v, b, median) => {
13452
- const score = band(v, b[0], b[1], b[2], b[3]);
13453
- if (score < 1) return score;
13454
- const spread = Math.max(median - b[1], b[2] - median, 1e-9);
13455
- return 1 - 0.15 * Math.min(1, Math.abs(v - median) / spread);
13542
+ var plausibleBand = (v, b) => {
13543
+ const span = b[3] - b[0];
13544
+ const margin = span > 0 ? span * 0.5 : 1;
13545
+ return band(v, b[0] - margin, b[0], b[3], b[3] + margin);
13456
13546
  };
13457
13547
  var featureVector = (f) => [
13458
13548
  f.entropy / 3,
@@ -14355,6 +14445,7 @@ var sectionAt = (plan, bar) => {
14355
14445
  // src/compose.ts
14356
14446
  var STEP_SEMITONES = 2;
14357
14447
  var MAX_LEAP_SEMITONES = 10;
14448
+ var LEAP_CEILINGS = [7, 8, 9, 10, 10, 12, 14, 16];
14358
14449
  var MAX_BAR_LEAP_SEMITONES = 10;
14359
14450
  var HARD = {
14360
14451
  /** メロディが1音も無い、音域が半音未満(同じ音を並べただけ)。 */
@@ -14413,13 +14504,16 @@ var WEIGHTS = {
14413
14504
  // --- 直近に作った曲と違うか ---
14414
14505
  novelty: 1.2
14415
14506
  };
14507
+ var DEVIATION_BUDGET = CORPUS_DEVIATION_BUDGET;
14508
+ var BUDGETED_KEYS = new Set(CORPUS_PROFILE_KEYS);
14416
14509
  var HAND_BANDS = {
14417
14510
  /** サブメロの音数/小節。少なすぎると「置いただけ」、多すぎるとメロディを食う。 */
14418
14511
  subDensity: [0.5, 1.8, 4.5, 8],
14419
14512
  complementarity: [0.05, 0.25, 0.7, 0.95],
14420
14513
  climaxPeaks: [0, 1, 2, 5]
14421
14514
  };
14422
- var DRAW_COUNT = 40;
14515
+ var DRAW_COUNT = 12;
14516
+ var SELECT_TEMPERATURE = 0.05;
14423
14517
  var BASE_STEPS_PER_BAR = 192;
14424
14518
  var WHOLE = 192;
14425
14519
  var DOT_HALF = 144;
@@ -15135,6 +15229,14 @@ for (const c of [...MOTIF_CELLS, ...RHYTHM_CELLS]) {
15135
15229
  const key = onsetKeyOf(c.value);
15136
15230
  cellEntryCount.set(key, (cellEntryCount.get(key) ?? 0) + 1);
15137
15231
  }
15232
+ var resolveMelodyForm = (choice, rnd) => {
15233
+ const c = (choice ?? "").trim() || "auto";
15234
+ if (c === "motif" || c === "ostinato" || c === "through") return c;
15235
+ const r = rnd();
15236
+ if (r < 0.25) return "ostinato";
15237
+ if (r < 0.4) return "through";
15238
+ return "motif";
15239
+ };
15138
15240
  var groovyCells = (cells, groove, rnd) => {
15139
15241
  const hasSixteenth = (c) => c.value.some((v) => Math.abs(v) <= SIXTEENTH);
15140
15242
  if (groove === "eighth") {
@@ -15310,7 +15412,7 @@ var clampSemi = (semi, low, high) => {
15310
15412
  while (s > high) s -= 12;
15311
15413
  return s;
15312
15414
  };
15313
- var leapTarget = (from, tones, rnd) => {
15415
+ var leapTarget = (from, tones, rnd, maxLeap = MAX_LEAP_SEMITONES) => {
15314
15416
  const candidates = [];
15315
15417
  for (const tone of tones) {
15316
15418
  const base = pitchClass(tone.semi);
@@ -15318,7 +15420,7 @@ var leapTarget = (from, tones, rnd) => {
15318
15420
  const semi = base + oct * 12;
15319
15421
  if (semi < MELODY_LOW || semi > MELODY_HIGH) continue;
15320
15422
  const gap = Math.abs(semi - from);
15321
- if (gap >= 3 && gap <= MAX_LEAP_SEMITONES) candidates.push(semi);
15423
+ if (gap >= 3 && gap <= maxLeap) candidates.push(semi);
15322
15424
  }
15323
15425
  }
15324
15426
  if (candidates.length === 0) return null;
@@ -15475,7 +15577,7 @@ var barDegrees = (role, slots, tones, style, scale, motifContour, startDegree, c
15475
15577
  if (slots[i2].value >= quarterSteps) {
15476
15578
  if (rnd() < style.leapAffinity) {
15477
15579
  const from = degreeToPitch(scale, out[i2 - 1]).semi;
15478
- const target = leapTarget(from, tones, rnd);
15580
+ const target = leapTarget(from, tones, rnd, style.maxLeap);
15479
15581
  if (target !== null) out[i2] = semitoneToDegree(scale, target);
15480
15582
  }
15481
15583
  continue;
@@ -15486,13 +15588,13 @@ var barDegrees = (role, slots, tones, style, scale, motifContour, startDegree, c
15486
15588
  }
15487
15589
  return out;
15488
15590
  };
15489
- var fitMotif = (degrees, slots, tones, prevSemi, quarterSteps, scale, pentatonic, preferShift) => {
15591
+ var fitMotif = (degrees, slots, tones, prevSemi, quarterSteps, scale, pentatonic, preferShift, maxShift = 3) => {
15490
15592
  let best = degrees;
15491
15593
  let bestShift = 0;
15492
15594
  let bestScore = Number.NEGATIVE_INFINITY;
15493
15595
  let preferScore = Number.NEGATIVE_INFINITY;
15494
15596
  let preferMoved = null;
15495
- for (let shift = -3; shift <= 3; shift++) {
15597
+ for (let shift = -maxShift; shift <= maxShift; shift++) {
15496
15598
  const moved = degrees.map(
15497
15599
  (d) => pentatonic ? coreToDegree(scale, degreeToCore(scale, d) + shift) : d + shift
15498
15600
  );
@@ -15549,7 +15651,7 @@ var shapeBar = (degrees, slots, tones, prevSemi, opts) => {
15549
15651
  MELODY_LOW,
15550
15652
  MELODY_HIGH
15551
15653
  );
15552
- const limit = i2 === 0 ? MAX_BAR_LEAP_SEMITONES : MAX_LEAP_SEMITONES;
15654
+ const limit = i2 === 0 ? MAX_BAR_LEAP_SEMITONES : opts.maxLeap;
15553
15655
  if (!opts.allowLeap && Math.abs(semi - prev) > limit) {
15554
15656
  semi = clampSemi(
15555
15657
  walk(opts.scale, prev, Math.sign(semi - prev) * 3),
@@ -15942,6 +16044,7 @@ var draw = (options, resolvedKey, scale, rnd) => {
15942
16044
  landing = (landing + pick([2, 4], rnd)) % size;
15943
16045
  return landing;
15944
16046
  };
16047
+ const form = resolveMelodyForm(options.form, rnd);
15945
16048
  const units2 = [];
15946
16049
  for (const section of sectionPlan) {
15947
16050
  const unitCount = Math.max(1, Math.round(section.bars / 2));
@@ -15962,16 +16065,31 @@ var draw = (options, resolvedKey, scale, rnd) => {
15962
16065
  const isLast = u === unitCount - 1;
15963
16066
  if (u % 2 === 0) {
15964
16067
  units2.push({
15965
- role: section.kind === "bridge" ? "step" : section.kind === "chorus" ? "climax" : src === "a2" || u > 0 ? "sequence" : "motif",
15966
- source: src,
16068
+ // リフ主体の曲は**変形しない**。セクエンツもオクターブ上げも入れず、
16069
+ // 同じ型を回し続ける。セクションの対比は編曲側(ドラム・楽器・レイヤ)
16070
+ // が担う——ヤツメ穴型の曲がまさにその作りで、120小節を通して
16071
+ // 5半音のセルが変わらない。
16072
+ role: form === "ostinato" ? "motif" : form === "through" ? (
16073
+ // 通し作曲は素材を戻さない。`step` の書法(アーチ・谷・波)で
16074
+ // 独立した線を書き、たまに走句と山を挟んで単調さを避ける。
16075
+ u % 3 === 2 ? "run" : u % 3 === 1 ? "climax" : "step"
16076
+ ) : section.kind === "bridge" ? "step" : section.kind === "chorus" ? "climax" : src === "a2" || u > 0 ? "sequence" : "motif",
16077
+ // リフ型は素材も1つに揃える(`sourceOf` でセクションごとに
16078
+ // 変えると、そこだけ別の型が始まってオスティナートにならない)。
16079
+ source: form === "ostinato" ? "a" : src,
15967
16080
  landing: null,
15968
16081
  section
15969
16082
  });
15970
16083
  } else {
15971
16084
  const landing = landingOf(section);
16085
+ const riff = form === "ostinato" && !(isLast && landing === 0);
15972
16086
  units2.push({
15973
- role: isLast && landing === 0 ? "cadence" : "answer",
15974
- source: "answer",
16087
+ role: isLast && landing === 0 ? "cadence" : riff ? "motif" : (
16088
+ // 通し作曲は「問いと答え」で閉じない。answer は問いのリズムを
16089
+ // 受けて着地音だけ変える形なので、そのままだと反復が戻る。
16090
+ form === "through" ? "step" : "answer"
16091
+ ),
16092
+ source: riff ? "a" : "answer",
15975
16093
  landing: isLast ? landing : null,
15976
16094
  section
15977
16095
  });
@@ -15989,6 +16107,7 @@ var draw = (options, resolvedKey, scale, rnd) => {
15989
16107
  const restatementOf = (bar) => {
15990
16108
  const curSec = sectionAt(sectionPlan, bar);
15991
16109
  if (!curSec.spec.melody) return null;
16110
+ if (form === "through") return null;
15992
16111
  if (curSec.restatement) {
15993
16112
  const firstSec = sectionPlan.find(
15994
16113
  (s) => s.kind === curSec.kind && !s.restatement
@@ -16010,13 +16129,15 @@ var draw = (options, resolvedKey, scale, rnd) => {
16010
16129
  }
16011
16130
  return null;
16012
16131
  };
16132
+ const registerSpread = form === "ostinato" ? rnd() ** 2 * 0.5 : 0.25 + rnd() * 0.75;
16013
16133
  const style = {
16014
16134
  groove: pick(["eighth", "sixteenth"], rnd),
16015
16135
  arcPeriod: pick([4, 8, 8, 16], rnd),
16016
16136
  arcPhase: pick([0, 1, 2], rnd),
16017
- arcAmp: 2 + rnd() * 3,
16137
+ arcAmp: 5 * registerSpread,
16018
16138
  // オクターブ跳躍は参考曲では音程の1.0%しかない。上げすぎると音域が広がる。
16019
- octaveAffinity: 0.06 + rnd() * 0.12,
16139
+ octaveAffinity: 0.18 * registerSpread,
16140
+ maxLeap: pick(LEAP_CEILINGS, rnd),
16020
16141
  // **音階を厳しく締める曲は必ず中核音の歩数で組む。** ダイアトニックの度数で輪郭を
16021
16142
  // 作ると、琉球音階なのにレやラが輪郭の中に入り込む。ファ・シを自由に使う
16022
16143
  // 陽・民謡だけが、曲ごとに掛けたり掛けなかったりする({@link ComposeScale.strict})。
@@ -16069,8 +16190,11 @@ var draw = (options, resolvedKey, scale, rnd) => {
16069
16190
  subInterval: pick([3, 4, 8, 9], rnd)
16070
16191
  };
16071
16192
  const motifPool = groovyCells(MOTIF_CELLS, style.groove, rnd);
16072
- const targetNotesPerBar = 4.5 + rnd() * 4.5;
16073
- const targetRestRatio = 0.02 + rnd() * 0.1;
16193
+ const targetRestRatio = 0.02 + rnd() ** 2 * 0.42;
16194
+ const targetNotesPerBar = Math.max(
16195
+ 2.8,
16196
+ Math.min(9.5, 7.4 - 6 * targetRestRatio + (rnd() * 4 - 2))
16197
+ );
16074
16198
  const cellNotes = (c) => c.value.filter((v) => v > 0).length;
16075
16199
  const cellRest = (c) => {
16076
16200
  let rest = 0;
@@ -16093,9 +16217,13 @@ var draw = (options, resolvedKey, scale, rnd) => {
16093
16217
  return pool[pool.length - 1];
16094
16218
  };
16095
16219
  const pickCell = (pool, densityMul = 1) => {
16096
- let best = weightedPick(pool);
16220
+ const want = targetRestRatio / Math.max(0.5, densityMul);
16221
+ const tol = Math.max(0.08, want * 0.6);
16222
+ const near = pool.filter((c) => Math.abs(cellRest(c) - want) <= tol);
16223
+ const from = near.length > 0 ? near : pool;
16224
+ let best = weightedPick(from);
16097
16225
  for (let i2 = 0; i2 < 2; i2++) {
16098
- const c = weightedPick(pool);
16226
+ const c = weightedPick(from);
16099
16227
  if (cellDistance(c, densityMul) < cellDistance(best, densityMul))
16100
16228
  best = c;
16101
16229
  }
@@ -16351,13 +16479,16 @@ var draw = (options, resolvedKey, scale, rnd) => {
16351
16479
  quarterSteps,
16352
16480
  scale,
16353
16481
  style.pentatonicMotif,
16354
- motifShiftMemo.get(shiftKey) ?? null
16482
+ motifShiftMemo.get(shiftKey) ?? null,
16483
+ // リフ型は和音へ寄せない。同じセルを回し続けるのが役目。
16484
+ form === "ostinato" ? 0 : 3
16355
16485
  );
16356
16486
  fitted = r.degrees;
16357
16487
  motifShiftMemo.set(shiftKey, r.shift);
16358
16488
  }
16359
16489
  const pitches = shapeBar(fitted, slots, tones, prevSemi, {
16360
16490
  scale,
16491
+ maxLeap: style.maxLeap,
16361
16492
  allowLeap: role === "climax",
16362
16493
  allowArpeggio: role === "climax" || role === "run" && style.runShape === "broken" || role === "cadence" && style.cadenceShape !== "descend",
16363
16494
  quarterSteps,
@@ -16803,6 +16934,7 @@ var draw = (options, resolvedKey, scale, rnd) => {
16803
16934
  (n) => n.durationSteps >= quarterSteps || rnd() < octaveCoverage
16804
16935
  ).map((n) => ({ ...n, velocity: Math.max(40, n.velocity - 26) })) : [];
16805
16936
  return {
16937
+ form,
16806
16938
  chordProgression,
16807
16939
  chordPattern,
16808
16940
  rootShift,
@@ -16889,7 +17021,7 @@ var evaluate = (d, recent) => {
16889
17021
  Math.min(...recent.map((r) => featureDistance(fingerprint, r))) / 1
16890
17022
  );
16891
17023
  const at = (b, v) => band(v, b[0], b[1], b[2], b[3]);
16892
- const atc = (key, v) => centeredBand(v, CORPUS_BANDS[key], CORPUS_MEDIANS[key]);
17024
+ const atc = (key, v) => plausibleBand(v, CORPUS_BANDS[key]);
16893
17025
  const peakBand = d.bars > 24 ? [0, 1, Math.round(d.bars / 16), Math.round(d.bars / 8) + 2] : HAND_BANDS.climaxPeaks;
16894
17026
  const scoreBreakdown = {
16895
17027
  entropy: atc("entropy", entropy),
@@ -16925,9 +17057,16 @@ var evaluate = (d, recent) => {
16925
17057
  tensionResolve: d.tonal.floating ? 1 : tension.resolve,
16926
17058
  novelty
16927
17059
  };
17060
+ const forgiven = new Set(
17061
+ Object.entries(WEIGHTS).filter(([key]) => BUDGETED_KEYS.has(key)).map(([key, weight]) => ({
17062
+ key,
17063
+ deficit: weight * (1 - (scoreBreakdown[key] ?? 0))
17064
+ })).sort((a, b) => b.deficit - a.deficit).slice(0, DEVIATION_BUDGET).filter((e) => e.deficit > 0).map((e) => e.key)
17065
+ );
16928
17066
  let weighted = 0;
16929
17067
  let weightSum = 0;
16930
17068
  for (const [key, weight] of Object.entries(WEIGHTS)) {
17069
+ if (forgiven.has(key)) continue;
16931
17070
  weighted += (scoreBreakdown[key] ?? 0) * weight;
16932
17071
  weightSum += weight;
16933
17072
  }
@@ -16964,49 +17103,64 @@ var composeSong = (options) => {
16964
17103
  resolvedKey.mode === "minor",
16965
17104
  rnd
16966
17105
  );
16967
- let best = null;
16968
- let bestScore = Number.NEGATIVE_INFINITY;
16969
- let bestIsValid = false;
17106
+ const valid = [];
17107
+ const invalid = [];
16970
17108
  let rejected = 0;
16971
17109
  for (let attempt = 1; attempt <= count; attempt++) {
16972
- const d = draw(options, resolvedKey, scale, rnd);
16973
- const { stats, ok } = evaluate(d, recent);
16974
- if (!ok) rejected++;
16975
- const better = ok === bestIsValid ? stats.score > bestScore : ok;
16976
- if (!better) continue;
16977
- bestScore = stats.score;
16978
- bestIsValid = ok;
16979
- best = {
16980
- // ドラム・楽器・編曲プランは勝った候補にだけ後から付ける(メロディに
16981
- // 依存しないので候補ごとに引いても採点は動かず、40本ぶん無駄になる)。
16982
- drum: "",
16983
- instrument: "",
16984
- arrange: EMPTY_ARRANGE,
16985
- chordProgression: d.chordProgression,
16986
- chordPattern: d.chordPattern,
16987
- rootShift: d.rootShift,
16988
- keyName: d.keyName,
16989
- keyLabel: d.keyLabel,
16990
- scaleId: scale.id,
16991
- scaleLabel: scale.label,
16992
- moodLabel: d.moodLabel,
16993
- bpm: d.bpm,
16994
- sections: d.sections,
16995
- bars: d.bars,
16996
- vocal: d.vocal,
16997
- tonal: d.tonal,
16998
- melody: d.melody,
16999
- submelody: d.submelody,
17000
- bass: d.bass,
17001
- harmony: d.harmony,
17002
- harmony2: d.harmony2,
17003
- octave: d.octave,
17004
- pad: d.pad,
17005
- solo: d.solo,
17006
- stats: { ...stats, attempts: attempt, rejected }
17007
- };
17110
+ const d2 = draw(options, resolvedKey, scale, rnd);
17111
+ const { stats, ok } = evaluate(d2, recent);
17112
+ if (ok) valid.push({ d: d2, stats });
17113
+ else {
17114
+ rejected++;
17115
+ invalid.push({ d: d2, stats });
17116
+ }
17117
+ }
17118
+ const pool = valid.length > 0 ? valid : invalid;
17119
+ const top = Math.max(...pool.map((c) => c.stats.score));
17120
+ const weights = pool.map(
17121
+ (c) => Math.exp((c.stats.score - top) / SELECT_TEMPERATURE)
17122
+ );
17123
+ const total = weights.reduce((a, b) => a + b, 0);
17124
+ let ticket = rnd() * total;
17125
+ let chosen = pool[pool.length - 1];
17126
+ for (let i2 = 0; i2 < pool.length; i2++) {
17127
+ ticket -= weights[i2];
17128
+ if (ticket <= 0) {
17129
+ chosen = pool[i2];
17130
+ break;
17131
+ }
17008
17132
  }
17009
- const result = best;
17133
+ const d = chosen.d;
17134
+ const result = {
17135
+ // ドラム・楽器・編曲プランは勝った候補にだけ後から付ける(メロディに
17136
+ // 依存しないので候補ごとに引いても採点は動かず、候補数ぶん無駄になる)。
17137
+ drum: "",
17138
+ instrument: "",
17139
+ arrange: EMPTY_ARRANGE,
17140
+ chordProgression: d.chordProgression,
17141
+ chordPattern: d.chordPattern,
17142
+ rootShift: d.rootShift,
17143
+ keyName: d.keyName,
17144
+ keyLabel: d.keyLabel,
17145
+ scaleId: scale.id,
17146
+ scaleLabel: scale.label,
17147
+ form: d.form,
17148
+ moodLabel: d.moodLabel,
17149
+ bpm: d.bpm,
17150
+ sections: d.sections,
17151
+ bars: d.bars,
17152
+ vocal: d.vocal,
17153
+ tonal: d.tonal,
17154
+ melody: d.melody,
17155
+ submelody: d.submelody,
17156
+ bass: d.bass,
17157
+ harmony: d.harmony,
17158
+ harmony2: d.harmony2,
17159
+ octave: d.octave,
17160
+ pad: d.pad,
17161
+ solo: d.solo,
17162
+ stats: { ...chosen.stats, attempts: count, rejected }
17163
+ };
17010
17164
  result.stats.attempts = count;
17011
17165
  result.stats.rejected = rejected;
17012
17166
  result.drum = pickBuiltinDrum(result, rnd);
@@ -17207,157 +17361,643 @@ var composeLyrics = (melody, options) => {
17207
17361
  return out.join("");
17208
17362
  };
17209
17363
 
17210
- // src/delay.ts
17211
- var DELAY_DIVISIONS = [
17212
- { value: "4", label: "4\u5206", beats: 1 },
17213
- { value: "8", label: "8\u5206", beats: 0.5 },
17214
- { value: "8d", label: "\u4ED8\u70B98\u5206", beats: 0.75 },
17215
- { value: "16", label: "16\u5206", beats: 0.25 }
17216
- ];
17217
- var clamp4 = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
17218
- var divisionToSeconds = (division, bpm) => {
17219
- const beats = DELAY_DIVISIONS.find((d) => d.value === division)?.beats ?? 0.5;
17220
- const safeBpm = bpm > 0 ? bpm : 120;
17221
- return 60 / safeBpm * beats;
17222
- };
17223
- var DELAY_MAX_WET = 0.45;
17224
- var delayAmountToGain = (amount) => clamp4(amount, 0, 100) / 100 * DELAY_MAX_WET;
17225
- var FEEDBACK_GAIN = 0.3;
17226
- var MAX_DELAY_SEC = 2;
17227
- var createDelayBus = (ctx, destination, options = {}) => {
17228
- const input = ctx.createGain();
17229
- const delayNode = ctx.createDelay(MAX_DELAY_SEC);
17230
- const feedback = ctx.createGain();
17231
- feedback.gain.value = FEEDBACK_GAIN;
17232
- const wetGain = ctx.createGain();
17233
- wetGain.gain.value = delayAmountToGain(options.amount ?? 0);
17234
- input.connect(delayNode);
17235
- delayNode.connect(feedback);
17236
- feedback.connect(delayNode);
17237
- delayNode.connect(wetGain);
17238
- wetGain.connect(destination);
17239
- let bpm = options.bpm ?? 120;
17240
- let division = options.division ?? "8";
17241
- const applyTime = () => {
17242
- delayNode.delayTime.setTargetAtTime(
17243
- divisionToSeconds(division, bpm),
17244
- ctx.currentTime,
17245
- 0.05
17246
- );
17247
- };
17248
- applyTime();
17249
- return {
17250
- input,
17251
- setAmount: (amount) => {
17252
- wetGain.gain.setTargetAtTime(
17253
- delayAmountToGain(amount),
17254
- ctx.currentTime,
17255
- 0.02
17256
- );
17257
- },
17258
- setDivision: (d) => {
17259
- division = d;
17260
- applyTime();
17261
- },
17262
- setBpm: (b) => {
17263
- bpm = b;
17264
- applyTime();
17265
- },
17266
- dispose: () => {
17267
- input.disconnect();
17268
- delayNode.disconnect();
17269
- feedback.disconnect();
17270
- wetGain.disconnect();
17271
- }
17272
- };
17273
- };
17274
-
17275
- // src/panel-state.ts
17276
- var STORAGE_PREFIX = "dtm-panel-open:";
17277
- var readPanelOpen = (key) => {
17278
- try {
17279
- if (typeof localStorage === "undefined" || !localStorage) return null;
17280
- const raw = localStorage.getItem(STORAGE_PREFIX + key);
17281
- if (raw === "1") return true;
17282
- if (raw === "0") return false;
17283
- } catch (_) {
17284
- }
17285
- return null;
17286
- };
17287
- var writePanelOpen = (key, open) => {
17288
- try {
17289
- if (typeof localStorage === "undefined" || !localStorage) return;
17290
- localStorage.setItem(STORAGE_PREFIX + key, open ? "1" : "0");
17291
- } catch (_) {
17292
- }
17293
- };
17294
- var persistPanels = (root) => {
17295
- const panels = root.querySelectorAll(
17296
- "details[data-dtm-acc]"
17297
- );
17298
- for (const panel of panels) {
17299
- const key = panel.dataset.dtmAcc;
17300
- if (!key) continue;
17301
- const stored = readPanelOpen(key);
17302
- if (stored !== null) panel.open = stored;
17303
- panel.addEventListener("toggle", () => {
17304
- writePanelOpen(key, panel.open);
17305
- });
17364
+ // src/instrument-presets.ts
17365
+ var INSTRUMENT_PRESETS = {
17366
+ // --- STANDARD: 汎用性と完成度重視 ---
17367
+ piano: {
17368
+ displayName: "\u30B0\u30E9\u30F3\u30C9\u30D4\u30A2\u30CE",
17369
+ description: "\u6700\u3082\u7834\u7DBB\u3057\u306B\u304F\u3044\u69CB\u6210\u3002\u697D\u66F2\u5236\u4F5C\u306E\u30B9\u30B1\u30C3\u30C1\u306B\u3082\u6700\u9069\u3002",
17370
+ melody: "Acoustic Grand Piano",
17371
+ submelody: "Vibraphone",
17372
+ bass: "Electric Bass (finger)",
17373
+ chord: "Pad 2 (warm)",
17374
+ solo: "Electric Guitar (clean)",
17375
+ // **Glockenspiel は使わない。音源側のサンプルが音程を外している**(実聴で確認)。
17376
+ // 音域の問題(G5〜の高音楽器)なら {@link GM_BRIGHT_CEILING} で下げれば済むが、
17377
+ // ピッチそのものがずれているものは置き場所を変えても直らない。同じ役割
17378
+ // (サビの上に乗る明るい音板)で、素直に鳴る Celesta へ差し替えてある。
17379
+ // {@link GM_INSTRUMENT_RANGE} / {@link GM_BRIGHT_CEILING} の Glockenspiel の
17380
+ // 項は残す。手で選んだときの置き場所の判断はそのまま要るため。
17381
+ chorusLead: "Celesta",
17382
+ chordAlt: "Electric Piano 1",
17383
+ sparkle: "Music Box",
17384
+ bassAlt: "Acoustic Bass",
17385
+ harmonyAlt: "Choir Aahs"
17386
+ },
17387
+ acoustic: {
17388
+ displayName: "\u30A2\u30B3\u30FC\u30B9\u30C6\u30A3\u30C3\u30AF",
17389
+ description: "\u751F\u697D\u5668\u306E\u6E29\u304B\u307F\u3092\u91CD\u8996\u3002\u30D5\u30A9\u30FC\u30AF\u3084\u30DD\u30C3\u30D7\u30B9\u306B\u3002",
17390
+ melody: "Acoustic Guitar (steel)",
17391
+ submelody: "Harmonica",
17392
+ bass: "Acoustic Bass",
17393
+ chord: "Acoustic Guitar (nylon)",
17394
+ solo: "Overdriven Guitar",
17395
+ chorusLead: "String Ensemble 1",
17396
+ chordAlt: "Acoustic Grand Piano",
17397
+ sparkle: "Celesta",
17398
+ bassAlt: "Electric Bass (finger)",
17399
+ harmonyAlt: "Choir Aahs"
17400
+ },
17401
+ jazz_night: {
17402
+ displayName: "\u30B8\u30E3\u30BA\u30FB\u30CA\u30A4\u30C8",
17403
+ description: "Rhodes\u98A8\u306EEP\u3068\u30A6\u30C3\u30C9\u30D9\u30FC\u30B9\u306B\u3088\u308B\u3001\u5927\u4EBA\u3073\u305F\u30A2\u30F3\u30B5\u30F3\u30D6\u30EB\u3002",
17404
+ melody: "Electric Piano 1",
17405
+ submelody: "Flute",
17406
+ bass: "Acoustic Bass",
17407
+ chord: "Electric Guitar (jazz)",
17408
+ solo: "Tenor Sax",
17409
+ chorusLead: "Muted Trumpet",
17410
+ chordAlt: "Vibraphone",
17411
+ sparkle: "Celesta",
17412
+ bassAlt: "Electric Bass (finger)",
17413
+ harmonyAlt: "Choir Aahs"
17414
+ },
17415
+ // --- MODERN & VIBE: エッジの効いた現代的な響き ---
17416
+ synth_pop: {
17417
+ displayName: "\u30B7\u30F3\u30BB\u30DD\u30C3\u30D7",
17418
+ description: "80s\u301C\u73FE\u4EE3\u307E\u3067\u3002\u629C\u3051\u308B\u30EA\u30FC\u30C9\u3068\u592A\u3044\u30D9\u30FC\u30B9\u306E\u738B\u9053\u3002",
17419
+ melody: "Lead 2 (sawtooth)",
17420
+ submelody: "Lead 4 (chiff)",
17421
+ bass: "Synth Bass 2",
17422
+ chord: "Pad 3 (polysynth)",
17423
+ solo: "Distortion Guitar",
17424
+ chorusLead: "Synth Brass 1",
17425
+ chordAlt: "Electric Piano 2",
17426
+ sparkle: "FX 3 (crystal)",
17427
+ bassAlt: "Synth Bass 1",
17428
+ harmonyAlt: "Synth Choir"
17429
+ },
17430
+ cyber_punk: {
17431
+ displayName: "\u30B5\u30A4\u30D0\u30FC\u30D1\u30F3\u30AF",
17432
+ description: "\u30C7\u30B8\u30BF\u30EB\u306A\u51B7\u305F\u3055\u3068\u6B6A\u307F\u304C\u6DF7\u3056\u308A\u5408\u3046\u3001\u672A\u6765\u7684\u306A\u97FF\u304D\u3002",
17433
+ melody: "Lead 8 (bass + lead)",
17434
+ submelody: "Lead 5 (charang)",
17435
+ bass: "Synth Bass 2",
17436
+ chord: "Pad 8 (sweep)",
17437
+ solo: "Distortion Guitar",
17438
+ chorusLead: "Lead 7 (fifths)",
17439
+ chordAlt: "Pad 4 (choir)",
17440
+ sparkle: "FX 3 (crystal)",
17441
+ bassAlt: "Synth Bass 1",
17442
+ harmonyAlt: "Synth Choir"
17443
+ },
17444
+ rock: {
17445
+ displayName: "\u30CF\u30FC\u30C9\u30ED\u30C3\u30AF",
17446
+ description: "\u6B6A\u307F\u30AE\u30BF\u30FC\u3068\u91CD\u539A\u306A\u30D9\u30FC\u30B9\u3067\u3001\u30D1\u30EF\u30FC\u3092\u524D\u9762\u306B\u3002",
17447
+ melody: "Distortion Guitar",
17448
+ submelody: "Rock Organ",
17449
+ bass: "Electric Bass (pick)",
17450
+ chord: "Overdriven Guitar",
17451
+ solo: "Distortion Guitar",
17452
+ chorusLead: "Brass Section",
17453
+ chordAlt: "Electric Guitar (clean)",
17454
+ sparkle: "Electric Guitar (muted)",
17455
+ bassAlt: "Electric Bass (finger)",
17456
+ harmonyAlt: "Choir Aahs"
17457
+ },
17458
+ // --- WORLD & CLASSIC: 特定のジャンル・地域 ---
17459
+ orchestra: {
17460
+ displayName: "\u30AA\u30FC\u30B1\u30B9\u30C8\u30E9",
17461
+ description: "\u58EE\u5927\u306A\u7269\u8A9E\u3092\u4E88\u611F\u3055\u305B\u308B\u3001\u7BA1\u5F26\u697D\u5668\u306E\u91CD\u539A\u306A\u97FF\u304D\u3002",
17462
+ melody: "French Horn",
17463
+ submelody: "Pizzicato Strings",
17464
+ bass: "Cello",
17465
+ chord: "Tremolo Strings",
17466
+ solo: "Violin",
17467
+ chorusLead: "Trumpet",
17468
+ chordAlt: "String Ensemble 1",
17469
+ sparkle: "Orchestral Harp",
17470
+ bassAlt: "Contrabass",
17471
+ harmonyAlt: "Choir Aahs"
17472
+ },
17473
+ japanese_wa: {
17474
+ displayName: "\u548C\u98A8\u30FB\u96C5",
17475
+ description: "\u7434\u3068\u4E09\u5473\u7DDA\u306E\u7E4A\u7D30\u306A\u8ABF\u3079\u306B\u3001\u5C3A\u516B\u306E\u60C5\u7DD2\u3092\u6DFB\u3048\u3066\u3002",
17476
+ melody: "Koto",
17477
+ submelody: "Shamisen",
17478
+ bass: "Taiko Drum",
17479
+ chord: "Shakuhachi",
17480
+ solo: "Shakuhachi",
17481
+ // piano プリセットと同じ理由で Glockenspiel を避ける(音源のピッチずれ)。
17482
+ chorusLead: "Celesta",
17483
+ chordAlt: "Kalimba",
17484
+ sparkle: "Music Box",
17485
+ bassAlt: "Acoustic Bass",
17486
+ harmonyAlt: "Choir Aahs"
17487
+ },
17488
+ arabic_exotic: {
17489
+ displayName: "\u30A8\u30AD\u30BE\u30C1\u30C3\u30AF",
17490
+ description: "\u30B7\u30BF\u30FC\u30EB\u3084\u30D0\u30B0\u30D1\u30A4\u30D7\u306B\u3088\u308B\u3001\u7570\u56FD\u60C5\u7DD2\u6EA2\u308C\u308B\u30B5\u30A6\u30F3\u30C9\u3002",
17491
+ melody: "Sitar",
17492
+ submelody: "Bagpipe",
17493
+ bass: "Fretless Bass",
17494
+ chord: "Kalimba",
17495
+ solo: "Shanai",
17496
+ chorusLead: "Steel Drums",
17497
+ chordAlt: "Orchestral Harp",
17498
+ sparkle: "Tinkle Bell",
17499
+ bassAlt: "Acoustic Bass",
17500
+ harmonyAlt: "Choir Aahs"
17501
+ },
17502
+ // --- FANTASY & ATMOSPHERE: 雰囲気と余韻 ---
17503
+ fantasy_rpg: {
17504
+ displayName: "\u30D5\u30A1\u30F3\u30BF\u30B8\u30FCRPG",
17505
+ description: "\u30AA\u30AB\u30EA\u30CA\u3068\u30CF\u30FC\u30D7\u304C\u7D21\u3050\u3001\u5192\u967A\u3068\u9B54\u6CD5\u306E\u4E16\u754C\u89B3\u3002",
17506
+ melody: "Ocarina",
17507
+ submelody: "Celesta",
17508
+ bass: "Timpani",
17509
+ chord: "Orchestral Harp",
17510
+ solo: "Pan Flute",
17511
+ chorusLead: "Choir Aahs",
17512
+ chordAlt: "String Ensemble 2",
17513
+ sparkle: "Tinkle Bell",
17514
+ bassAlt: "Contrabass",
17515
+ harmonyAlt: "Choir Aahs"
17516
+ },
17517
+ ambient_cloud: {
17518
+ displayName: "\u30A2\u30F3\u30D3\u30A8\u30F3\u30C8",
17519
+ description: "\u8F2A\u90ED\u3092\u307C\u304B\u3057\u305F\u97F3\u8272\u3067\u3001\u6DF1\u3044\u6CA1\u5165\u611F\u3068\u4F59\u97FB\u3092\u6F14\u51FA\u3002",
17520
+ melody: "Lead 6 (voice)",
17521
+ submelody: "Music Box",
17522
+ bass: "Synth Bass 1",
17523
+ chord: "Pad 7 (halo)",
17524
+ solo: "Lead 3 (calliope)",
17525
+ chorusLead: "Synth Choir",
17526
+ chordAlt: "Pad 5 (bowed)",
17527
+ sparkle: "FX 3 (crystal)",
17528
+ bassAlt: "Synth Bass 2",
17529
+ harmonyAlt: "Synth Choir"
17530
+ },
17531
+ retro_game: {
17532
+ displayName: "8-bit \u30EC\u30C8\u30ED",
17533
+ description: "\u77E9\u5F62\u6CE2\u3092\u60F3\u8D77\u3055\u305B\u308B\u3001\u521D\u671F\u30B2\u30FC\u30E0\u6A5F\u306E\u3088\u3046\u306A\u61D0\u304B\u3057\u3044\u97FF\u304D\u3002",
17534
+ melody: "Lead 1 (square)",
17535
+ submelody: "Lead 2 (sawtooth)",
17536
+ bass: "Synth Bass 1",
17537
+ chord: "Clavinet",
17538
+ solo: "Lead 8 (bass + lead)",
17539
+ chorusLead: "Lead 4 (chiff)",
17540
+ chordAlt: "Lead 5 (charang)",
17541
+ sparkle: "Xylophone",
17542
+ bassAlt: "Synth Bass 2",
17543
+ harmonyAlt: "Lead 6 (voice)"
17306
17544
  }
17307
17545
  };
17546
+ var GM_INSTRUMENT_RANGE = {
17547
+ // 鍵盤・音板
17548
+ "Acoustic Grand Piano": [21, 108],
17549
+ "Electric Piano 1": [28, 103],
17550
+ Clavinet: [36, 96],
17551
+ Celesta: [60, 108],
17552
+ Glockenspiel: [79, 108],
17553
+ "Music Box": [72, 108],
17554
+ Vibraphone: [53, 89],
17555
+ Kalimba: [60, 84],
17556
+ "Orchestral Harp": [23, 104],
17557
+ // 弦・撥弦
17558
+ "Acoustic Guitar (steel)": [40, 83],
17559
+ "Acoustic Guitar (nylon)": [40, 83],
17560
+ "Electric Guitar (clean)": [40, 86],
17561
+ "Electric Guitar (jazz)": [40, 86],
17562
+ "Overdriven Guitar": [40, 88],
17563
+ "Distortion Guitar": [40, 88],
17564
+ Violin: [55, 103],
17565
+ Cello: [36, 76],
17566
+ "String Ensemble 1": [28, 100],
17567
+ "Tremolo Strings": [28, 100],
17568
+ "Pizzicato Strings": [28, 96],
17569
+ Sitar: [48, 79],
17570
+ Shamisen: [48, 84],
17571
+ Koto: [41, 77],
17572
+ // ベース
17573
+ "Acoustic Bass": [28, 60],
17574
+ "Electric Bass (finger)": [28, 67],
17575
+ "Electric Bass (pick)": [28, 67],
17576
+ "Fretless Bass": [28, 67],
17577
+ "Synth Bass 1": [24, 72],
17578
+ "Synth Bass 2": [24, 72],
17579
+ // 管
17580
+ Flute: [60, 96],
17581
+ "Pan Flute": [60, 91],
17582
+ Shakuhachi: [62, 86],
17583
+ Ocarina: [60, 84],
17584
+ Harmonica: [60, 96],
17585
+ Bagpipe: [62, 86],
17586
+ Shanai: [60, 86],
17587
+ "Tenor Sax": [44, 75],
17588
+ Trumpet: [55, 82],
17589
+ "Muted Trumpet": [55, 82],
17590
+ "French Horn": [41, 77],
17591
+ "Brass Section": [41, 84],
17592
+ // 声・打・オルガン
17593
+ "Choir Aahs": [43, 84],
17594
+ "Synth Choir": [43, 84],
17595
+ "Steel Drums": [55, 86],
17596
+ "Taiko Drum": [30, 60],
17597
+ Timpani: [36, 57],
17598
+ "Rock Organ": [36, 96],
17599
+ // シンセ(実物が無いので一般的な使用域)
17600
+ "Synth Brass 1": [36, 96],
17601
+ "Lead 1 (square)": [36, 96],
17602
+ "Lead 2 (sawtooth)": [36, 96],
17603
+ "Lead 3 (calliope)": [48, 96],
17604
+ "Lead 4 (chiff)": [48, 96],
17605
+ "Lead 5 (charang)": [40, 96],
17606
+ "Lead 6 (voice)": [43, 91],
17607
+ "Lead 7 (fifths)": [36, 84],
17608
+ "Lead 8 (bass + lead)": [28, 91],
17609
+ "Pad 2 (warm)": [24, 96],
17610
+ "Pad 3 (polysynth)": [24, 96],
17611
+ "Pad 7 (halo)": [24, 96],
17612
+ "Pad 8 (sweep)": [24, 96]
17613
+ };
17614
+ var GM_BRIGHT_CEILING = {
17615
+ Glockenspiel: 72,
17616
+ "Tinkle Bell": 72,
17617
+ "Music Box": 79,
17618
+ Celesta: 84,
17619
+ Kalimba: 79,
17620
+ "Steel Drums": 84,
17621
+ "FX 3 (crystal)": 79
17622
+ };
17623
+ var OCTAVE_FIT_TOLERANCE = 3;
17624
+ var fitInstrumentOctave = (semitoneRange, instrument, wanted) => {
17625
+ const range = GM_INSTRUMENT_RANGE[instrument];
17626
+ if (!range || !semitoneRange) return wanted;
17627
+ const hi = Math.min(range[1], GM_BRIGHT_CEILING[instrument] ?? range[1]);
17628
+ let octave = wanted;
17629
+ while (octave > wanted - 2) {
17630
+ if (semitoneRange[1] + octave * 12 <= hi + OCTAVE_FIT_TOLERANCE) break;
17631
+ octave--;
17632
+ }
17633
+ return octave;
17634
+ };
17308
17635
 
17309
- // src/daw-ui.ts
17310
- var q = (root, sel) => root.querySelector(sel);
17311
- var buildUI = (target, options) => {
17312
- const {
17313
- drumPatterns,
17314
- defaultDrumPattern,
17315
- defaultBpm,
17316
- showMidi,
17317
- showMidiSearch,
17318
- showCompose
17319
- } = options;
17320
- const drumOptions = [`<option value="none">\u306A\u3057</option>`].concat(
17321
- drumPatterns.map(
17322
- (p) => `<option value="${p.value}" ${p.value === defaultDrumPattern ? "selected" : ""}>${p.label}</option>`
17323
- )
17324
- ).join("");
17325
- target.innerHTML = `
17326
- <div class="dtm-daw" data-dtm="root">
17327
- <div class="dtm-topbar" data-dtm="transport">
17328
- <div class="dtm-topbar-row1">
17329
- <button class="dtm-iconbtn" data-dtm="prev-bar" title="1\u5C0F\u7BC0\u524D">${icon("chevronLeft")}</button>
17330
- <button class="dtm-play" data-dtm="play" disabled>${icon("play")}</button>
17331
- <button class="dtm-iconbtn" data-dtm="next-bar" title="1\u5C0F\u7BC0\u5F8C">${icon("chevronRight")}</button>
17332
- <label class="dtm-toggle"><input type="checkbox" data-dtm="solo"><span>\u30BD\u30ED</span></label>
17333
- <span class="dtm-topbar-loading dtm-blink" data-dtm="topbar-loading">... LOADING ...</span>
17334
- <button class="dtm-clip-badge dtm-hidden" data-dtm="clip-badge" title="\u97F3\u5272\u308C\u691C\u77E5\uFF08\u30AF\u30EA\u30C3\u30AF\u3067\u6D88\u3059\uFF09">CLIP</button>
17335
- <span class="dtm-grow"></span>
17336
- <span class="dtm-label">BPM</span>
17337
- <input type="number" class="dtm-input dtm-input--num" data-dtm="bpm" value="${defaultBpm}" min="20" max="300">
17338
- </div>
17339
- <div class="dtm-tracks" data-dtm="track-tabs"></div>
17340
- </div>
17341
-
17342
- <div class="dtm-tooldock">
17343
- <div class="dtm-seg">
17344
- <button class="dtm-segbtn dtm-segbtn--active" data-dtm="tool-pen" title="\u30DA\u30F3">${icon("pen")}</button>
17345
- <button class="dtm-segbtn" data-dtm="tool-select" title="\u9078\u629E">${icon("select")}</button>
17346
- <button class="dtm-segbtn" data-dtm="tool-eraser" title="\u6D88\u3057\u30B4\u30E0">${icon("eraser")}</button>
17347
- </div>
17348
- <button class="dtm-iconbtn" data-dtm="undo" title="\u5143\u306B\u623B\u3059" disabled>${icon("undo")}</button>
17349
- <button class="dtm-iconbtn" data-dtm="redo" title="\u3084\u308A\u76F4\u3057" disabled>${icon("redo")}</button>
17350
- <select class="dtm-select dtm-grow" data-dtm="note-length" title="\u97F3\u7B26\u306E\u9577\u3055">
17351
- <option value="48">4\u5206</option>
17352
- <option value="32">3\u90234</option>
17353
- <option value="24">8\u5206</option>
17354
- <option value="16">3\u90238</option>
17355
- <option value="12" selected>16\u5206</option>
17356
- <option value="8">3\u902316</option>
17357
- <option value="6">32\u5206</option>
17358
- <option value="4">3\u902332</option>
17359
- </select>
17360
- </div>
17636
+ // src/advanced-layers.ts
17637
+ var buildAdvancedLayers = (song, config) => {
17638
+ const { edo, stepsPerBar, preset } = config;
17639
+ const semitoneRange = (notes) => {
17640
+ if (notes.length === 0) return null;
17641
+ let lo = Number.POSITIVE_INFINITY;
17642
+ let hi = Number.NEGATIVE_INFINITY;
17643
+ for (const n of notes) {
17644
+ const semi = n.pitchUnits / UNITS_PER_SEMITONE;
17645
+ if (semi < lo) lo = semi;
17646
+ if (semi > hi) hi = semi;
17647
+ }
17648
+ return [Math.round(lo), Math.round(hi)];
17649
+ };
17650
+ const fit = (notes, slot, wanted) => fitInstrumentOctave(semitoneRange(notes), preset[slot], wanted);
17651
+ const kindAtBar = (bar) => song.sections.find(
17652
+ (sec) => bar >= sec.startBar && bar < sec.startBar + sec.bars
17653
+ )?.kind ?? null;
17654
+ const onlyIn = (notes, kinds) => {
17655
+ if (!kinds) return notes;
17656
+ const want = new Set(kinds);
17657
+ return notes.filter((n) => {
17658
+ const kind = kindAtBar(Math.floor(n.startStep / stepsPerBar));
17659
+ return kind !== null && want.has(kind);
17660
+ });
17661
+ };
17662
+ const chordNotes = (pattern, velocityShift = 0) => buildChordPlacements({
17663
+ edo,
17664
+ chordStr: song.chordProgression,
17665
+ patternType: pattern,
17666
+ rootShift: song.rootShift,
17667
+ bpm: song.bpm,
17668
+ stepsPerBar
17669
+ }).map((p) => ({
17670
+ startStep: p.startStep,
17671
+ pitchUnits: p.pitchUnits,
17672
+ durationSteps: p.durationSteps,
17673
+ velocity: Math.max(30, p.velocity + velocityShift)
17674
+ }));
17675
+ const plan = song.arrange;
17676
+ const kindsPresent = [...new Set(song.sections.map((sec) => sec.kind))];
17677
+ const LOUD = ["chorus", "drop_chorus"];
17678
+ const loudKinds = kindsPresent.filter((k) => LOUD.includes(k));
17679
+ const quietKinds = kindsPresent.filter((k) => !LOUD.includes(k));
17680
+ const splittable = loudKinds.length > 0 && quietKinds.length > 0;
17681
+ const leadNotes = plan.lead ? onlyIn(song.melody, plan.lead.sections) : [];
17682
+ const leadOctave = fit(leadNotes, "chorusLead", plan.lead?.octave ?? 0);
17683
+ const leadLayer = {
17684
+ index: 1,
17685
+ notes: leadNotes,
17686
+ octave: leadOctave,
17687
+ // ユニゾンで重ねるときは、オクターブ上より前に出やすいので少し引く。
17688
+ volume: leadOctave === 0 ? 54 : 62,
17689
+ slot: "chorusLead"
17690
+ };
17691
+ const bassNotes = plan.bassLayer ? onlyIn(song.bass, plan.bassLayer.sections) : [];
17692
+ const bassOctave = fit(bassNotes, "bass", plan.bassLayer?.octave ?? 0);
17693
+ const wantBassLayer = plan.bassLayer !== null && bassOctave !== 0;
17694
+ const bassLayer = wantBassLayer ? {
17695
+ index: 5,
17696
+ notes: bassNotes,
17697
+ octave: bassOctave,
17698
+ volume: 58,
17699
+ slot: "bass"
17700
+ } : {
17701
+ index: 5,
17702
+ notes: splittable ? onlyIn(song.bass, loudKinds) : [],
17703
+ octave: 0,
17704
+ volume: 92,
17705
+ slot: "bassAlt"
17706
+ };
17707
+ const padNotes = onlyIn(song.pad, plan.padSections);
17708
+ const layers = [
17709
+ { index: 0, notes: song.melody, octave: 0, volume: 104, slot: "melody" },
17710
+ leadLayer,
17711
+ // t2 ハモリ。**セクションで分割しない。**
17712
+ // 一度 t12 と loud/quiet で分けてみたが、ハモリは音の87%が盛り上がる側にあり、
17713
+ // 71%の曲は静かな側に1音も無い——分割すると t2 が空になって中身が t12 へ移る。
17714
+ // 「トラック番号と役割の対応は固定」(ピアノロールのどこに何があるかが曲ごとに
17715
+ // 動くとユーザーが編集できない)という原則にも反するので、ここは丸ごと持つ。
17716
+ { index: 2, notes: song.harmony, octave: 0, volume: 82 },
17717
+ // t3 サブメロ。t14 が間奏ソロを持たないとき、そちらへ盛り上がる側を渡して
17718
+ // こちらは静かな側だけ持つ。対旋律・合いの手がセクションで音色替えする。
17719
+ {
17720
+ index: 3,
17721
+ notes: song.solo.length === 0 && splittable ? onlyIn(song.submelody, quietKinds) : song.submelody,
17722
+ octave: 0,
17723
+ volume: 86,
17724
+ slot: "submelody"
17725
+ },
17726
+ // t4。t5 が `bassAlt` で盛り上がる側を持つときは、こちらは静かな側だけ持つ
17727
+ // (分割であって間引きではない——2本合わせて元のベースが漏れなく鳴る)。
17728
+ {
17729
+ index: 4,
17730
+ notes: !wantBassLayer && splittable ? onlyIn(song.bass, quietKinds) : song.bass,
17731
+ octave: 0,
17732
+ volume: 92,
17733
+ slot: "bass"
17734
+ },
17735
+ bassLayer,
17736
+ // **パッドは伴奏用の楽器で、伴奏より1.5オクターブ高いところを鳴らす。**
17737
+ // そのまま置くとナイロンギターで10半音、カリンバで9半音、尺八で7半音ぶん
17738
+ // 音域を突き抜ける。楽器に合わせて下げる。
17739
+ {
17740
+ index: 6,
17741
+ notes: padNotes,
17742
+ octave: fit(padNotes, "chord", 0),
17743
+ volume: 64,
17744
+ slot: "chord"
17745
+ }
17746
+ ];
17747
+ const backingVolumes = [62, 54, 50];
17748
+ for (let i2 = 0; i2 < 3; i2++) {
17749
+ const layer = plan.backing[i2];
17750
+ layers.push({
17751
+ index: 7 + i2,
17752
+ notes: layer ? onlyIn(chordNotes(layer.pattern), layer.sections) : [],
17753
+ octave: layer?.octave ?? 0,
17754
+ volume: backingVolumes[i2],
17755
+ slot: i2 % 2 === 0 ? "chordAlt" : "chord"
17756
+ });
17757
+ }
17758
+ const sparkleNotes = plan.sparkle ? onlyIn(chordNotes(plan.sparkle.pattern, -14), plan.sparkle.sections) : [];
17759
+ layers.push(
17760
+ {
17761
+ index: 10,
17762
+ notes: sparkleNotes,
17763
+ // 装飾は伴奏と別の音色にする(以前はここも `chord` の使い回しだった)。
17764
+ octave: fit(sparkleNotes, "sparkle", plan.sparkle?.octave ?? 0),
17765
+ volume: 56,
17766
+ slot: "sparkle"
17767
+ },
17768
+ // 掛け合い(デュエット)の相手。**歌入り作曲のときだけ**中身が入る。
17769
+ { index: 11, notes: [], octave: 0, volume: 104, slot: "melody" },
17770
+ // 2声目のハモリ(主旋律を上下から挟む3声)と、主旋律のオクターブ下の重ね。
17771
+ // どちらも曲ごとに出るかどうかが決まる(`song.vocal`)。
17772
+ // t12。3声目のハモリは曲ごとに出るかが決まり、実測28%。
17773
+ //
17774
+ // 出ない曲では**パッド(t6)が鳴っていないセクション**を、別の音色
17775
+ // (`harmonyAlt` =声もの・柔らかい持続音)で受け持つ。パッドはセクション種別の
17776
+ // 39%しか覆っておらず補集合は常に存在するので、ここは**いま何も鳴っていない
17777
+ // 場所に持続音を足す**ことになる——重ねでも写しでもなく、純粋な追加。
17778
+ song.harmony2.length > 0 ? { index: 12, notes: song.harmony2, octave: 0, volume: 74 } : (() => {
17779
+ const rest = plan.padSections ? kindsPresent.filter((k) => !plan.padSections?.includes(k)) : [];
17780
+ const notes = rest.length > 0 ? onlyIn(song.pad, rest) : [];
17781
+ return {
17782
+ index: 12,
17783
+ notes,
17784
+ octave: fit(notes, "harmonyAlt", 0),
17785
+ volume: 60,
17786
+ slot: "harmonyAlt"
17787
+ };
17788
+ })(),
17789
+ // t13。既定は**主旋律のオクターブ下の重ね**で、歌入り作曲ではここに歌詞が付く
17790
+ // (オクターブ下でハモる歌手。単なる写しではない)。
17791
+ //
17792
+ // ただしこの層は曲ごとに出るかどうかが決まり、実測で**18%しか鳴らない**。
17793
+ // 残り82%はトラックが1本まるごと遊ぶ。オクターブ等価の写しに常設の価値は
17794
+ // 無いが、**空けておく価値はもっと無い**ので、層が無い曲では
17795
+ // 「t1(サビ重ね)が担当しないセクションの主旋律を、別の楽器でなぞる」層に回す。
17796
+ // t1 は CHORUS/LOUD 側を持つので、こちらは静かな側を持つ——結果として
17797
+ // **セクションの境目で主旋律に付く音色が入れ替わる**。1トラック1楽器の制約下で
17798
+ // 「パートごとに楽器が変わる」を作れるのは、この書き分けだけ。
17799
+ song.octave.length > 0 ? {
17800
+ index: 13,
17801
+ notes: song.octave,
17802
+ octave: -1,
17803
+ volume: 56,
17804
+ slot: "melody"
17805
+ } : (() => {
17806
+ const leadKinds = new Set(plan.lead?.sections ?? []);
17807
+ const quiet = song.sections.filter((sec) => sec.spec.melody && !leadKinds.has(sec.kind)).map((sec) => sec.kind);
17808
+ const notes = quiet.length > 0 ? onlyIn(song.melody, quiet) : [];
17809
+ const slot = ["solo", "chorusLead", "submelody", "chordAlt"].find(
17810
+ (k) => preset[k] !== preset.melody && preset[k] !== preset.chord
17811
+ ) ?? "submelody";
17812
+ return {
17813
+ index: 13,
17814
+ notes,
17815
+ // ユニゾンで置く。**音色だけを変えるのが狙い**なので、
17816
+ // オクターブを動かすと「オクターブ写し」に戻ってしまう。
17817
+ octave: fit(notes, slot, 0),
17818
+ volume: 58,
17819
+ slot
17820
+ };
17821
+ })(),
17822
+ // **間奏のソロ。** 音が入るのは間奏の小節だけなので、この1本だけを
17823
+ // 別の楽器にしても他のセクションの鳴りは変わらない。歌の音域をそのまま
17824
+ // 渡すと管楽器が上へ抜ける(テナーサックスで10半音)ので、ここも合わせる。
17825
+ // t14。**間奏のソロ。** ただし間奏は既定の構成(`DEFAULT_SECTIONS`)に無く、
17826
+ // テンプレートも `jpop_standard` しか含まないので、実測で**0%**——設計上の
17827
+ // 出番が既定では一度も来ないトラックだった。
17828
+ // 間奏が無い曲では、サブメロの盛り上がる側を `solo` の音色で受け持つ
17829
+ // (t3 は静かな側。分割なので音数は増えない)。
17830
+ song.solo.length > 0 ? {
17831
+ index: 14,
17832
+ notes: song.solo,
17833
+ octave: fit(song.solo, "solo", 0),
17834
+ volume: 100,
17835
+ slot: "solo"
17836
+ } : (() => {
17837
+ const notes = splittable ? onlyIn(song.submelody, loudKinds) : [];
17838
+ return {
17839
+ index: 14,
17840
+ notes,
17841
+ octave: fit(notes, "solo", 0),
17842
+ volume: 86,
17843
+ slot: "solo"
17844
+ };
17845
+ })()
17846
+ );
17847
+ return layers;
17848
+ };
17849
+
17850
+ // src/delay.ts
17851
+ var DELAY_DIVISIONS = [
17852
+ { value: "4", label: "4\u5206", beats: 1 },
17853
+ { value: "8", label: "8\u5206", beats: 0.5 },
17854
+ { value: "8d", label: "\u4ED8\u70B98\u5206", beats: 0.75 },
17855
+ { value: "16", label: "16\u5206", beats: 0.25 }
17856
+ ];
17857
+ var clamp4 = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
17858
+ var divisionToSeconds = (division, bpm) => {
17859
+ const beats = DELAY_DIVISIONS.find((d) => d.value === division)?.beats ?? 0.5;
17860
+ const safeBpm = bpm > 0 ? bpm : 120;
17861
+ return 60 / safeBpm * beats;
17862
+ };
17863
+ var DELAY_MAX_WET = 0.45;
17864
+ var delayAmountToGain = (amount) => clamp4(amount, 0, 100) / 100 * DELAY_MAX_WET;
17865
+ var FEEDBACK_GAIN = 0.3;
17866
+ var MAX_DELAY_SEC = 2;
17867
+ var createDelayBus = (ctx, destination, options = {}) => {
17868
+ const input = ctx.createGain();
17869
+ const delayNode = ctx.createDelay(MAX_DELAY_SEC);
17870
+ const feedback = ctx.createGain();
17871
+ feedback.gain.value = FEEDBACK_GAIN;
17872
+ const wetGain = ctx.createGain();
17873
+ wetGain.gain.value = delayAmountToGain(options.amount ?? 0);
17874
+ input.connect(delayNode);
17875
+ delayNode.connect(feedback);
17876
+ feedback.connect(delayNode);
17877
+ delayNode.connect(wetGain);
17878
+ wetGain.connect(destination);
17879
+ let bpm = options.bpm ?? 120;
17880
+ let division = options.division ?? "8";
17881
+ const applyTime = () => {
17882
+ delayNode.delayTime.setTargetAtTime(
17883
+ divisionToSeconds(division, bpm),
17884
+ ctx.currentTime,
17885
+ 0.05
17886
+ );
17887
+ };
17888
+ applyTime();
17889
+ return {
17890
+ input,
17891
+ setAmount: (amount) => {
17892
+ wetGain.gain.setTargetAtTime(
17893
+ delayAmountToGain(amount),
17894
+ ctx.currentTime,
17895
+ 0.02
17896
+ );
17897
+ },
17898
+ setDivision: (d) => {
17899
+ division = d;
17900
+ applyTime();
17901
+ },
17902
+ setBpm: (b) => {
17903
+ bpm = b;
17904
+ applyTime();
17905
+ },
17906
+ dispose: () => {
17907
+ input.disconnect();
17908
+ delayNode.disconnect();
17909
+ feedback.disconnect();
17910
+ wetGain.disconnect();
17911
+ }
17912
+ };
17913
+ };
17914
+
17915
+ // src/panel-state.ts
17916
+ var STORAGE_PREFIX = "dtm-panel-open:";
17917
+ var readPanelOpen = (key) => {
17918
+ try {
17919
+ if (typeof localStorage === "undefined" || !localStorage) return null;
17920
+ const raw = localStorage.getItem(STORAGE_PREFIX + key);
17921
+ if (raw === "1") return true;
17922
+ if (raw === "0") return false;
17923
+ } catch (_) {
17924
+ }
17925
+ return null;
17926
+ };
17927
+ var writePanelOpen = (key, open) => {
17928
+ try {
17929
+ if (typeof localStorage === "undefined" || !localStorage) return;
17930
+ localStorage.setItem(STORAGE_PREFIX + key, open ? "1" : "0");
17931
+ } catch (_) {
17932
+ }
17933
+ };
17934
+ var persistPanels = (root) => {
17935
+ const panels = root.querySelectorAll(
17936
+ "details[data-dtm-acc]"
17937
+ );
17938
+ for (const panel of panels) {
17939
+ const key = panel.dataset.dtmAcc;
17940
+ if (!key) continue;
17941
+ const stored = readPanelOpen(key);
17942
+ if (stored !== null) panel.open = stored;
17943
+ panel.addEventListener("toggle", () => {
17944
+ writePanelOpen(key, panel.open);
17945
+ });
17946
+ }
17947
+ };
17948
+
17949
+ // src/daw-ui.ts
17950
+ var q = (root, sel) => root.querySelector(sel);
17951
+ var buildUI = (target, options) => {
17952
+ const {
17953
+ drumPatterns,
17954
+ defaultDrumPattern,
17955
+ defaultBpm,
17956
+ showMidi,
17957
+ showMidiSearch,
17958
+ showCompose
17959
+ } = options;
17960
+ const drumOptions = [`<option value="none">\u306A\u3057</option>`].concat(
17961
+ drumPatterns.map(
17962
+ (p) => `<option value="${p.value}" ${p.value === defaultDrumPattern ? "selected" : ""}>${p.label}</option>`
17963
+ )
17964
+ ).join("");
17965
+ target.innerHTML = `
17966
+ <div class="dtm-daw" data-dtm="root">
17967
+ <div class="dtm-topbar" data-dtm="transport">
17968
+ <div class="dtm-topbar-row1">
17969
+ <button class="dtm-iconbtn" data-dtm="prev-bar" title="1\u5C0F\u7BC0\u524D">${icon("chevronLeft")}</button>
17970
+ <button class="dtm-play" data-dtm="play" disabled>${icon("play")}</button>
17971
+ <button class="dtm-iconbtn" data-dtm="next-bar" title="1\u5C0F\u7BC0\u5F8C">${icon("chevronRight")}</button>
17972
+ <label class="dtm-toggle"><input type="checkbox" data-dtm="solo"><span>\u30BD\u30ED</span></label>
17973
+ <span class="dtm-topbar-loading dtm-blink" data-dtm="topbar-loading">... LOADING ...</span>
17974
+ <button class="dtm-clip-badge dtm-hidden" data-dtm="clip-badge" title="\u97F3\u5272\u308C\u691C\u77E5\uFF08\u30AF\u30EA\u30C3\u30AF\u3067\u6D88\u3059\uFF09">CLIP</button>
17975
+ <span class="dtm-grow"></span>
17976
+ <span class="dtm-label">BPM</span>
17977
+ <input type="number" class="dtm-input dtm-input--num" data-dtm="bpm" value="${defaultBpm}" min="20" max="300">
17978
+ </div>
17979
+ <div class="dtm-tracks" data-dtm="track-tabs"></div>
17980
+ </div>
17981
+
17982
+ <div class="dtm-tooldock">
17983
+ <div class="dtm-seg">
17984
+ <button class="dtm-segbtn dtm-segbtn--active" data-dtm="tool-pen" title="\u30DA\u30F3">${icon("pen")}</button>
17985
+ <button class="dtm-segbtn" data-dtm="tool-select" title="\u9078\u629E">${icon("select")}</button>
17986
+ <button class="dtm-segbtn" data-dtm="tool-eraser" title="\u6D88\u3057\u30B4\u30E0">${icon("eraser")}</button>
17987
+ </div>
17988
+ <button class="dtm-iconbtn" data-dtm="undo" title="\u5143\u306B\u623B\u3059" disabled>${icon("undo")}</button>
17989
+ <button class="dtm-iconbtn" data-dtm="redo" title="\u3084\u308A\u76F4\u3057" disabled>${icon("redo")}</button>
17990
+ <select class="dtm-select dtm-grow" data-dtm="note-length" title="\u97F3\u7B26\u306E\u9577\u3055">
17991
+ <option value="48">4\u5206</option>
17992
+ <option value="32">3\u90234</option>
17993
+ <option value="24">8\u5206</option>
17994
+ <option value="16">3\u90238</option>
17995
+ <option value="12" selected>16\u5206</option>
17996
+ <option value="8">3\u902316</option>
17997
+ <option value="6">32\u5206</option>
17998
+ <option value="4">3\u902332</option>
17999
+ </select>
18000
+ </div>
17361
18001
 
17362
18002
  <div class="dtm-roll-wrap">
17363
18003
  <div class="dtm-roll" data-dtm="roll">
@@ -17762,355 +18402,135 @@ var buildUI = (target, options) => {
17762
18402
  <div class="dtm-modal-header">
17763
18403
  <span class="dtm-modal-title" data-dtm="modal-title"></span>
17764
18404
  <button class="dtm-modal-close" data-dtm="modal-close">&times;</button>
17765
- </div>
17766
- <div class="dtm-modal-body" data-dtm="modal-body"></div>
17767
- </div>
17768
- </div>
17769
-
17770
- </div>`;
17771
- const root = q(target, '[data-dtm="root"]');
17772
- persistPanels(root);
17773
- const sel = (name) => q(root, `[data-dtm="${name}"]`);
17774
- return {
17775
- root,
17776
- topbar: sel("transport"),
17777
- topbarLoading: sel("topbar-loading"),
17778
- playBtn: sel("play"),
17779
- prevBarBtn: sel("prev-bar"),
17780
- nextBarBtn: sel("next-bar"),
17781
- soloCheckbox: sel("solo"),
17782
- clipBadge: sel("clip-badge"),
17783
- toolPen: sel("tool-pen"),
17784
- toolSelect: sel("tool-select"),
17785
- toolEraser: sel("tool-eraser"),
17786
- undoBtn: sel("undo"),
17787
- redoBtn: sel("redo"),
17788
- noteLengthSelect: sel("note-length"),
17789
- bpmInput: sel("bpm"),
17790
- zoomXLabel: sel("zoomx-label"),
17791
- zoomYLabel: sel("zoomy-label"),
17792
- zoomXIn: sel("zoomx-in"),
17793
- zoomXOut: sel("zoomx-out"),
17794
- zoomYIn: sel("zoomy-in"),
17795
- zoomYOut: sel("zoomy-out"),
17796
- bgFileInput: sel("bg-file-input"),
17797
- bgUploadBtn: sel("bg-upload"),
17798
- bgRemoveBtn: sel("bg-remove"),
17799
- bgOpacityInput: sel("bg-opacity"),
17800
- bgOpacityRow: sel("bg-opacity-row"),
17801
- rollContainer: sel("roll"),
17802
- wrapper: sel("wrapper"),
17803
- vScroll: sel("vscroll"),
17804
- vScrollThumb: sel("vscroll-thumb"),
17805
- hScroll: sel("hscroll"),
17806
- hScrollThumb: sel("hscroll-thumb"),
17807
- loopToggle: sel("loop-toggle"),
17808
- loopToggleLabel: sel("loop-toggle-label"),
17809
- loopInfoBtn: sel("loop-info"),
17810
- masterVolume: sel("master-volume"),
17811
- masterVolumeLabel: sel("master-volume-label"),
17812
- masterComp: sel("master-comp"),
17813
- masterCompLabel: sel("master-comp-label"),
17814
- masterCompInfoBtn: sel("master-comp-info"),
17815
- reverbAmount: sel("reverb-amount"),
17816
- reverbAmountLabel: sel("reverb-amount-label"),
17817
- reverbAmountInfoBtn: sel("reverb-amount-info"),
17818
- reverbDecay: sel("reverb-decay"),
17819
- reverbDecayLabel: sel("reverb-decay-label"),
17820
- reverbPreDelay: sel("reverb-predelay"),
17821
- reverbPreDelayLabel: sel("reverb-predelay-label"),
17822
- delayAmount: sel("delay-amount"),
17823
- delayAmountLabel: sel("delay-amount-label"),
17824
- delayAmountInfoBtn: sel("delay-amount-info"),
17825
- delayDivision: sel("delay-division"),
17826
- fadeIn: sel("fade-in"),
17827
- fadeInLabel: sel("fade-in-label"),
17828
- fadeOut: sel("fade-out"),
17829
- fadeOutLabel: sel("fade-out-label"),
17830
- fadeInfoBtn: sel("fade-info"),
17831
- autoMasterBtn: sel("auto-master"),
17832
- autoMasterInfoBtn: sel("auto-master-info"),
17833
- trackTabs: sel("track-tabs"),
17834
- trackBody: sel("track-body"),
17835
- drumSelect: sel("drum-select"),
17836
- drumFontSelect: sel("drum-font-select"),
17837
- drumVolume: sel("drum-volume"),
17838
- drumVolumeLabel: sel("drum-volume-label"),
17839
- midiInput: sel("midi-input"),
17840
- midiLoadBtn: sel("midi-load"),
17841
- midiInfoBtn: sel("midi-info"),
17842
- midiTrackSelection: sel("midi-track-selection"),
17843
- midiPanel: sel("midi-panel"),
17844
- midiSearchOpenBtn: sel("midi-search-open"),
17845
- mmlInput: sel("mml-input"),
17846
- mmlLoadBtn: sel("mml-load"),
17847
- mmlLoadNote: sel("mml-load-note"),
17848
- applyActiveOnly: sel("apply-active-only"),
17849
- shiftSelect: sel("shift-select"),
17850
- shiftApplyBtn: sel("shift-apply"),
17851
- transposeSelect: sel("transpose-select"),
17852
- transposeApplyBtn: sel("transpose-apply"),
17853
- transposeInfoBtn: sel("transpose-info"),
17854
- macroCompose: sel("macro-compose"),
17855
- composeTemplate: sel("compose-template"),
17856
- composeSections: sel("compose-sections"),
17857
- composeSectionsLen: sel("compose-sections-len"),
17858
- composeKey: sel("compose-key"),
17859
- composeKeyHint: sel("compose-key-hint"),
17860
- composeScale: sel("compose-scale"),
17861
- composeScaleHint: sel("compose-scale-hint"),
17862
- macroComposeVocal: sel("macro-compose-vocal"),
17863
- macroComposeInfo: sel("macro-compose-info"),
17864
- macroClear: sel("macro-clear"),
17865
- macroRandom: sel("macro-random"),
17866
- macroHarmonic: sel("macro-harmonic"),
17867
- macroMono: sel("macro-mono"),
17868
- exportMidiBtn: sel("export-midi"),
17869
- exportWavBtn: sel("export-wav"),
17870
- drumJsonExportBtn: sel("drum-json-export"),
17871
- drumJsonOutput: sel("drum-json-output"),
17872
- drumJsonStatus: sel("drum-json-status"),
17873
- drumJsonText: sel("drum-json-text"),
17874
- drumJsonCopyBtn: sel("drum-json-copy"),
17875
- generateMmlBtn: sel("generate-mml"),
17876
- decomposeChordToggle: sel("decompose-chord"),
17877
- ignoreChordHeavyToggle: sel("ignore-chord-heavy"),
17878
- barLimitSelect: sel("bar-limit"),
17879
- outputContainer: sel("output-container"),
17880
- outputStatus: sel("output-status"),
17881
- outputFull: sel("output-full"),
17882
- outputMini: sel("output-mini"),
17883
- copyFullBtn: sel("copy-full"),
17884
- copyMiniBtn: sel("copy-mini"),
17885
- overlay: sel("overlay"),
17886
- mmlInfoBtn: sel("mml-info"),
17887
- edoSelect: sel("edo-select"),
17888
- edoInfoBtn: sel("edo-info"),
17889
- modalOverlay: sel("modal-overlay"),
17890
- modalTitle: sel("modal-title"),
17891
- modalBody: sel("modal-body"),
17892
- modalClose: sel("modal-close")
17893
- };
17894
- };
17895
-
17896
- // src/instrument-presets.ts
17897
- var INSTRUMENT_PRESETS = {
17898
- // --- STANDARD: 汎用性と完成度重視 ---
17899
- piano: {
17900
- displayName: "\u30B0\u30E9\u30F3\u30C9\u30D4\u30A2\u30CE",
17901
- description: "\u6700\u3082\u7834\u7DBB\u3057\u306B\u304F\u3044\u69CB\u6210\u3002\u697D\u66F2\u5236\u4F5C\u306E\u30B9\u30B1\u30C3\u30C1\u306B\u3082\u6700\u9069\u3002",
17902
- melody: "Acoustic Grand Piano",
17903
- submelody: "Vibraphone",
17904
- bass: "Electric Bass (finger)",
17905
- chord: "Pad 2 (warm)",
17906
- solo: "Electric Guitar (clean)",
17907
- // グロッケンは実物の音域が G5(79)〜 の高音楽器で、旋律の音域へそのまま置くと
17908
- // 金切り音になる。**外すのではなくオクターブで下げて使う**
17909
- // ({@link GM_BRIGHT_CEILING} / {@link fitInstrumentOctave})。
17910
- chorusLead: "Glockenspiel"
17911
- },
17912
- acoustic: {
17913
- displayName: "\u30A2\u30B3\u30FC\u30B9\u30C6\u30A3\u30C3\u30AF",
17914
- description: "\u751F\u697D\u5668\u306E\u6E29\u304B\u307F\u3092\u91CD\u8996\u3002\u30D5\u30A9\u30FC\u30AF\u3084\u30DD\u30C3\u30D7\u30B9\u306B\u3002",
17915
- melody: "Acoustic Guitar (steel)",
17916
- submelody: "Harmonica",
17917
- bass: "Acoustic Bass",
17918
- chord: "Acoustic Guitar (nylon)",
17919
- solo: "Overdriven Guitar",
17920
- chorusLead: "String Ensemble 1"
17921
- },
17922
- jazz_night: {
17923
- displayName: "\u30B8\u30E3\u30BA\u30FB\u30CA\u30A4\u30C8",
17924
- description: "Rhodes\u98A8\u306EEP\u3068\u30A6\u30C3\u30C9\u30D9\u30FC\u30B9\u306B\u3088\u308B\u3001\u5927\u4EBA\u3073\u305F\u30A2\u30F3\u30B5\u30F3\u30D6\u30EB\u3002",
17925
- melody: "Electric Piano 1",
17926
- submelody: "Flute",
17927
- bass: "Acoustic Bass",
17928
- chord: "Electric Guitar (jazz)",
17929
- solo: "Tenor Sax",
17930
- chorusLead: "Muted Trumpet"
17931
- },
17932
- // --- MODERN & VIBE: エッジの効いた現代的な響き ---
17933
- synth_pop: {
17934
- displayName: "\u30B7\u30F3\u30BB\u30DD\u30C3\u30D7",
17935
- description: "80s\u301C\u73FE\u4EE3\u307E\u3067\u3002\u629C\u3051\u308B\u30EA\u30FC\u30C9\u3068\u592A\u3044\u30D9\u30FC\u30B9\u306E\u738B\u9053\u3002",
17936
- melody: "Lead 2 (sawtooth)",
17937
- submelody: "Lead 4 (chiff)",
17938
- bass: "Synth Bass 2",
17939
- chord: "Pad 3 (polysynth)",
17940
- solo: "Distortion Guitar",
17941
- chorusLead: "Synth Brass 1"
17942
- },
17943
- cyber_punk: {
17944
- displayName: "\u30B5\u30A4\u30D0\u30FC\u30D1\u30F3\u30AF",
17945
- description: "\u30C7\u30B8\u30BF\u30EB\u306A\u51B7\u305F\u3055\u3068\u6B6A\u307F\u304C\u6DF7\u3056\u308A\u5408\u3046\u3001\u672A\u6765\u7684\u306A\u97FF\u304D\u3002",
17946
- melody: "Lead 8 (bass + lead)",
17947
- submelody: "Lead 5 (charang)",
17948
- bass: "Synth Bass 2",
17949
- chord: "Pad 8 (sweep)",
17950
- solo: "Distortion Guitar",
17951
- chorusLead: "Lead 7 (fifths)"
17952
- },
17953
- rock: {
17954
- displayName: "\u30CF\u30FC\u30C9\u30ED\u30C3\u30AF",
17955
- description: "\u6B6A\u307F\u30AE\u30BF\u30FC\u3068\u91CD\u539A\u306A\u30D9\u30FC\u30B9\u3067\u3001\u30D1\u30EF\u30FC\u3092\u524D\u9762\u306B\u3002",
17956
- melody: "Distortion Guitar",
17957
- submelody: "Rock Organ",
17958
- bass: "Electric Bass (pick)",
17959
- chord: "Overdriven Guitar",
17960
- solo: "Distortion Guitar",
17961
- chorusLead: "Brass Section"
17962
- },
17963
- // --- WORLD & CLASSIC: 特定のジャンル・地域 ---
17964
- orchestra: {
17965
- displayName: "\u30AA\u30FC\u30B1\u30B9\u30C8\u30E9",
17966
- description: "\u58EE\u5927\u306A\u7269\u8A9E\u3092\u4E88\u611F\u3055\u305B\u308B\u3001\u7BA1\u5F26\u697D\u5668\u306E\u91CD\u539A\u306A\u97FF\u304D\u3002",
17967
- melody: "French Horn",
17968
- submelody: "Pizzicato Strings",
17969
- bass: "Cello",
17970
- chord: "Tremolo Strings",
17971
- solo: "Violin",
17972
- chorusLead: "Trumpet"
17973
- },
17974
- japanese_wa: {
17975
- displayName: "\u548C\u98A8\u30FB\u96C5",
17976
- description: "\u7434\u3068\u4E09\u5473\u7DDA\u306E\u7E4A\u7D30\u306A\u8ABF\u3079\u306B\u3001\u5C3A\u516B\u306E\u60C5\u7DD2\u3092\u6DFB\u3048\u3066\u3002",
17977
- melody: "Koto",
17978
- submelody: "Shamisen",
17979
- bass: "Taiko Drum",
17980
- chord: "Shakuhachi",
17981
- solo: "Shakuhachi",
17982
- chorusLead: "Glockenspiel"
17983
- },
17984
- arabic_exotic: {
17985
- displayName: "\u30A8\u30AD\u30BE\u30C1\u30C3\u30AF",
17986
- description: "\u30B7\u30BF\u30FC\u30EB\u3084\u30D0\u30B0\u30D1\u30A4\u30D7\u306B\u3088\u308B\u3001\u7570\u56FD\u60C5\u7DD2\u6EA2\u308C\u308B\u30B5\u30A6\u30F3\u30C9\u3002",
17987
- melody: "Sitar",
17988
- submelody: "Bagpipe",
17989
- bass: "Fretless Bass",
17990
- chord: "Kalimba",
17991
- solo: "Shanai",
17992
- chorusLead: "Steel Drums"
17993
- },
17994
- // --- FANTASY & ATMOSPHERE: 雰囲気と余韻 ---
17995
- fantasy_rpg: {
17996
- displayName: "\u30D5\u30A1\u30F3\u30BF\u30B8\u30FCRPG",
17997
- description: "\u30AA\u30AB\u30EA\u30CA\u3068\u30CF\u30FC\u30D7\u304C\u7D21\u3050\u3001\u5192\u967A\u3068\u9B54\u6CD5\u306E\u4E16\u754C\u89B3\u3002",
17998
- melody: "Ocarina",
17999
- submelody: "Celesta",
18000
- bass: "Timpani",
18001
- chord: "Orchestral Harp",
18002
- solo: "Pan Flute",
18003
- chorusLead: "Choir Aahs"
18004
- },
18005
- ambient_cloud: {
18006
- displayName: "\u30A2\u30F3\u30D3\u30A8\u30F3\u30C8",
18007
- description: "\u8F2A\u90ED\u3092\u307C\u304B\u3057\u305F\u97F3\u8272\u3067\u3001\u6DF1\u3044\u6CA1\u5165\u611F\u3068\u4F59\u97FB\u3092\u6F14\u51FA\u3002",
18008
- melody: "Lead 6 (voice)",
18009
- submelody: "Music Box",
18010
- bass: "Synth Bass 1",
18011
- chord: "Pad 7 (halo)",
18012
- solo: "Lead 3 (calliope)",
18013
- chorusLead: "Synth Choir"
18014
- },
18015
- retro_game: {
18016
- displayName: "8-bit \u30EC\u30C8\u30ED",
18017
- description: "\u77E9\u5F62\u6CE2\u3092\u60F3\u8D77\u3055\u305B\u308B\u3001\u521D\u671F\u30B2\u30FC\u30E0\u6A5F\u306E\u3088\u3046\u306A\u61D0\u304B\u3057\u3044\u97FF\u304D\u3002",
18018
- melody: "Lead 1 (square)",
18019
- submelody: "Lead 2 (sawtooth)",
18020
- bass: "Synth Bass 1",
18021
- chord: "Clavinet",
18022
- solo: "Lead 8 (bass + lead)",
18023
- chorusLead: "Lead 4 (chiff)"
18024
- }
18025
- };
18026
- var GM_INSTRUMENT_RANGE = {
18027
- // 鍵盤・音板
18028
- "Acoustic Grand Piano": [21, 108],
18029
- "Electric Piano 1": [28, 103],
18030
- Clavinet: [36, 96],
18031
- Celesta: [60, 108],
18032
- Glockenspiel: [79, 108],
18033
- "Music Box": [72, 108],
18034
- Vibraphone: [53, 89],
18035
- Kalimba: [60, 84],
18036
- "Orchestral Harp": [23, 104],
18037
- // 弦・撥弦
18038
- "Acoustic Guitar (steel)": [40, 83],
18039
- "Acoustic Guitar (nylon)": [40, 83],
18040
- "Electric Guitar (clean)": [40, 86],
18041
- "Electric Guitar (jazz)": [40, 86],
18042
- "Overdriven Guitar": [40, 88],
18043
- "Distortion Guitar": [40, 88],
18044
- Violin: [55, 103],
18045
- Cello: [36, 76],
18046
- "String Ensemble 1": [28, 100],
18047
- "Tremolo Strings": [28, 100],
18048
- "Pizzicato Strings": [28, 96],
18049
- Sitar: [48, 79],
18050
- Shamisen: [48, 84],
18051
- Koto: [41, 77],
18052
- // ベース
18053
- "Acoustic Bass": [28, 60],
18054
- "Electric Bass (finger)": [28, 67],
18055
- "Electric Bass (pick)": [28, 67],
18056
- "Fretless Bass": [28, 67],
18057
- "Synth Bass 1": [24, 72],
18058
- "Synth Bass 2": [24, 72],
18059
- // 管
18060
- Flute: [60, 96],
18061
- "Pan Flute": [60, 91],
18062
- Shakuhachi: [62, 86],
18063
- Ocarina: [60, 84],
18064
- Harmonica: [60, 96],
18065
- Bagpipe: [62, 86],
18066
- Shanai: [60, 86],
18067
- "Tenor Sax": [44, 75],
18068
- Trumpet: [55, 82],
18069
- "Muted Trumpet": [55, 82],
18070
- "French Horn": [41, 77],
18071
- "Brass Section": [41, 84],
18072
- // 声・打・オルガン
18073
- "Choir Aahs": [43, 84],
18074
- "Synth Choir": [43, 84],
18075
- "Steel Drums": [55, 86],
18076
- "Taiko Drum": [30, 60],
18077
- Timpani: [36, 57],
18078
- "Rock Organ": [36, 96],
18079
- // シンセ(実物が無いので一般的な使用域)
18080
- "Synth Brass 1": [36, 96],
18081
- "Lead 1 (square)": [36, 96],
18082
- "Lead 2 (sawtooth)": [36, 96],
18083
- "Lead 3 (calliope)": [48, 96],
18084
- "Lead 4 (chiff)": [48, 96],
18085
- "Lead 5 (charang)": [40, 96],
18086
- "Lead 6 (voice)": [43, 91],
18087
- "Lead 7 (fifths)": [36, 84],
18088
- "Lead 8 (bass + lead)": [28, 91],
18089
- "Pad 2 (warm)": [24, 96],
18090
- "Pad 3 (polysynth)": [24, 96],
18091
- "Pad 7 (halo)": [24, 96],
18092
- "Pad 8 (sweep)": [24, 96]
18093
- };
18094
- var GM_BRIGHT_CEILING = {
18095
- Glockenspiel: 72,
18096
- "Tinkle Bell": 72,
18097
- "Music Box": 79,
18098
- Celesta: 84,
18099
- Kalimba: 79,
18100
- "Steel Drums": 84,
18101
- "FX 3 (crystal)": 79
18102
- };
18103
- var OCTAVE_FIT_TOLERANCE = 3;
18104
- var fitInstrumentOctave = (semitoneRange, instrument, wanted) => {
18105
- const range = GM_INSTRUMENT_RANGE[instrument];
18106
- if (!range || !semitoneRange) return wanted;
18107
- const hi = Math.min(range[1], GM_BRIGHT_CEILING[instrument] ?? range[1]);
18108
- let octave = wanted;
18109
- while (octave > wanted - 2) {
18110
- if (semitoneRange[1] + octave * 12 <= hi + OCTAVE_FIT_TOLERANCE) break;
18111
- octave--;
18112
- }
18113
- return octave;
18405
+ </div>
18406
+ <div class="dtm-modal-body" data-dtm="modal-body"></div>
18407
+ </div>
18408
+ </div>
18409
+
18410
+ </div>`;
18411
+ const root = q(target, '[data-dtm="root"]');
18412
+ persistPanels(root);
18413
+ const sel = (name) => q(root, `[data-dtm="${name}"]`);
18414
+ return {
18415
+ root,
18416
+ topbar: sel("transport"),
18417
+ topbarLoading: sel("topbar-loading"),
18418
+ playBtn: sel("play"),
18419
+ prevBarBtn: sel("prev-bar"),
18420
+ nextBarBtn: sel("next-bar"),
18421
+ soloCheckbox: sel("solo"),
18422
+ clipBadge: sel("clip-badge"),
18423
+ toolPen: sel("tool-pen"),
18424
+ toolSelect: sel("tool-select"),
18425
+ toolEraser: sel("tool-eraser"),
18426
+ undoBtn: sel("undo"),
18427
+ redoBtn: sel("redo"),
18428
+ noteLengthSelect: sel("note-length"),
18429
+ bpmInput: sel("bpm"),
18430
+ zoomXLabel: sel("zoomx-label"),
18431
+ zoomYLabel: sel("zoomy-label"),
18432
+ zoomXIn: sel("zoomx-in"),
18433
+ zoomXOut: sel("zoomx-out"),
18434
+ zoomYIn: sel("zoomy-in"),
18435
+ zoomYOut: sel("zoomy-out"),
18436
+ bgFileInput: sel("bg-file-input"),
18437
+ bgUploadBtn: sel("bg-upload"),
18438
+ bgRemoveBtn: sel("bg-remove"),
18439
+ bgOpacityInput: sel("bg-opacity"),
18440
+ bgOpacityRow: sel("bg-opacity-row"),
18441
+ rollContainer: sel("roll"),
18442
+ wrapper: sel("wrapper"),
18443
+ vScroll: sel("vscroll"),
18444
+ vScrollThumb: sel("vscroll-thumb"),
18445
+ hScroll: sel("hscroll"),
18446
+ hScrollThumb: sel("hscroll-thumb"),
18447
+ loopToggle: sel("loop-toggle"),
18448
+ loopToggleLabel: sel("loop-toggle-label"),
18449
+ loopInfoBtn: sel("loop-info"),
18450
+ masterVolume: sel("master-volume"),
18451
+ masterVolumeLabel: sel("master-volume-label"),
18452
+ masterComp: sel("master-comp"),
18453
+ masterCompLabel: sel("master-comp-label"),
18454
+ masterCompInfoBtn: sel("master-comp-info"),
18455
+ reverbAmount: sel("reverb-amount"),
18456
+ reverbAmountLabel: sel("reverb-amount-label"),
18457
+ reverbAmountInfoBtn: sel("reverb-amount-info"),
18458
+ reverbDecay: sel("reverb-decay"),
18459
+ reverbDecayLabel: sel("reverb-decay-label"),
18460
+ reverbPreDelay: sel("reverb-predelay"),
18461
+ reverbPreDelayLabel: sel("reverb-predelay-label"),
18462
+ delayAmount: sel("delay-amount"),
18463
+ delayAmountLabel: sel("delay-amount-label"),
18464
+ delayAmountInfoBtn: sel("delay-amount-info"),
18465
+ delayDivision: sel("delay-division"),
18466
+ fadeIn: sel("fade-in"),
18467
+ fadeInLabel: sel("fade-in-label"),
18468
+ fadeOut: sel("fade-out"),
18469
+ fadeOutLabel: sel("fade-out-label"),
18470
+ fadeInfoBtn: sel("fade-info"),
18471
+ autoMasterBtn: sel("auto-master"),
18472
+ autoMasterInfoBtn: sel("auto-master-info"),
18473
+ trackTabs: sel("track-tabs"),
18474
+ trackBody: sel("track-body"),
18475
+ drumSelect: sel("drum-select"),
18476
+ drumFontSelect: sel("drum-font-select"),
18477
+ drumVolume: sel("drum-volume"),
18478
+ drumVolumeLabel: sel("drum-volume-label"),
18479
+ midiInput: sel("midi-input"),
18480
+ midiLoadBtn: sel("midi-load"),
18481
+ midiInfoBtn: sel("midi-info"),
18482
+ midiTrackSelection: sel("midi-track-selection"),
18483
+ midiPanel: sel("midi-panel"),
18484
+ midiSearchOpenBtn: sel("midi-search-open"),
18485
+ mmlInput: sel("mml-input"),
18486
+ mmlLoadBtn: sel("mml-load"),
18487
+ mmlLoadNote: sel("mml-load-note"),
18488
+ applyActiveOnly: sel("apply-active-only"),
18489
+ shiftSelect: sel("shift-select"),
18490
+ shiftApplyBtn: sel("shift-apply"),
18491
+ transposeSelect: sel("transpose-select"),
18492
+ transposeApplyBtn: sel("transpose-apply"),
18493
+ transposeInfoBtn: sel("transpose-info"),
18494
+ macroCompose: sel("macro-compose"),
18495
+ composeTemplate: sel("compose-template"),
18496
+ composeSections: sel("compose-sections"),
18497
+ composeSectionsLen: sel("compose-sections-len"),
18498
+ composeKey: sel("compose-key"),
18499
+ composeKeyHint: sel("compose-key-hint"),
18500
+ composeScale: sel("compose-scale"),
18501
+ composeScaleHint: sel("compose-scale-hint"),
18502
+ macroComposeVocal: sel("macro-compose-vocal"),
18503
+ macroComposeInfo: sel("macro-compose-info"),
18504
+ macroClear: sel("macro-clear"),
18505
+ macroRandom: sel("macro-random"),
18506
+ macroHarmonic: sel("macro-harmonic"),
18507
+ macroMono: sel("macro-mono"),
18508
+ exportMidiBtn: sel("export-midi"),
18509
+ exportWavBtn: sel("export-wav"),
18510
+ drumJsonExportBtn: sel("drum-json-export"),
18511
+ drumJsonOutput: sel("drum-json-output"),
18512
+ drumJsonStatus: sel("drum-json-status"),
18513
+ drumJsonText: sel("drum-json-text"),
18514
+ drumJsonCopyBtn: sel("drum-json-copy"),
18515
+ generateMmlBtn: sel("generate-mml"),
18516
+ decomposeChordToggle: sel("decompose-chord"),
18517
+ ignoreChordHeavyToggle: sel("ignore-chord-heavy"),
18518
+ barLimitSelect: sel("bar-limit"),
18519
+ outputContainer: sel("output-container"),
18520
+ outputStatus: sel("output-status"),
18521
+ outputFull: sel("output-full"),
18522
+ outputMini: sel("output-mini"),
18523
+ copyFullBtn: sel("copy-full"),
18524
+ copyMiniBtn: sel("copy-mini"),
18525
+ overlay: sel("overlay"),
18526
+ mmlInfoBtn: sel("mml-info"),
18527
+ edoSelect: sel("edo-select"),
18528
+ edoInfoBtn: sel("edo-info"),
18529
+ modalOverlay: sel("modal-overlay"),
18530
+ modalTitle: sel("modal-title"),
18531
+ modalBody: sel("modal-body"),
18532
+ modalClose: sel("modal-close")
18533
+ };
18114
18534
  };
18115
18535
 
18116
18536
  // src/macro-state.ts
@@ -20575,128 +20995,6 @@ var pickComposeVocal = (exclude) => {
20575
20995
  const list = pool.length > 0 ? pool : COMPOSE_VOCAL_POOL;
20576
20996
  return list[Math.floor(Math.random() * list.length)] ?? "klatt";
20577
20997
  };
20578
- var buildAdvancedLayers = (song, config) => {
20579
- const { edo, stepsPerBar, preset } = config;
20580
- const semitoneRange = (notes) => {
20581
- if (notes.length === 0) return null;
20582
- let lo = Number.POSITIVE_INFINITY;
20583
- let hi = Number.NEGATIVE_INFINITY;
20584
- for (const n of notes) {
20585
- const semi = n.pitchUnits / UNITS_PER_SEMITONE;
20586
- if (semi < lo) lo = semi;
20587
- if (semi > hi) hi = semi;
20588
- }
20589
- return [Math.round(lo), Math.round(hi)];
20590
- };
20591
- const fit = (notes, slot, wanted) => fitInstrumentOctave(semitoneRange(notes), preset[slot], wanted);
20592
- const kindAtBar = (bar) => song.sections.find(
20593
- (sec) => bar >= sec.startBar && bar < sec.startBar + sec.bars
20594
- )?.kind ?? null;
20595
- const onlyIn = (notes, kinds) => {
20596
- if (!kinds) return notes;
20597
- const want = new Set(kinds);
20598
- return notes.filter((n) => {
20599
- const kind = kindAtBar(Math.floor(n.startStep / stepsPerBar));
20600
- return kind !== null && want.has(kind);
20601
- });
20602
- };
20603
- const chordNotes = (pattern, velocityShift = 0) => buildChordPlacements({
20604
- edo,
20605
- chordStr: song.chordProgression,
20606
- patternType: pattern,
20607
- rootShift: song.rootShift,
20608
- bpm: song.bpm,
20609
- stepsPerBar
20610
- }).map((p) => ({
20611
- startStep: p.startStep,
20612
- pitchUnits: p.pitchUnits,
20613
- durationSteps: p.durationSteps,
20614
- velocity: Math.max(30, p.velocity + velocityShift)
20615
- }));
20616
- const plan = song.arrange;
20617
- const leadNotes = plan.lead ? onlyIn(song.melody, plan.lead.sections) : [];
20618
- const leadOctave = fit(leadNotes, "chorusLead", plan.lead?.octave ?? 0);
20619
- const leadLayer = {
20620
- index: 1,
20621
- notes: leadNotes,
20622
- octave: leadOctave,
20623
- // ユニゾンで重ねるときは、オクターブ上より前に出やすいので少し引く。
20624
- volume: leadOctave === 0 ? 54 : 62,
20625
- slot: "chorusLead"
20626
- };
20627
- const bassNotes = plan.bassLayer ? onlyIn(song.bass, plan.bassLayer.sections) : [];
20628
- const bassOctave = fit(bassNotes, "bass", plan.bassLayer?.octave ?? 0);
20629
- const bassLayer = {
20630
- index: 5,
20631
- notes: bassOctave === 0 ? [] : bassNotes,
20632
- octave: bassOctave,
20633
- volume: 58,
20634
- slot: "bass"
20635
- };
20636
- const padNotes = onlyIn(song.pad, plan.padSections);
20637
- const layers = [
20638
- { index: 0, notes: song.melody, octave: 0, volume: 104, slot: "melody" },
20639
- leadLayer,
20640
- { index: 2, notes: song.harmony, octave: 0, volume: 82 },
20641
- {
20642
- index: 3,
20643
- notes: song.submelody,
20644
- octave: 0,
20645
- volume: 86,
20646
- slot: "submelody"
20647
- },
20648
- { index: 4, notes: song.bass, octave: 0, volume: 92, slot: "bass" },
20649
- bassLayer,
20650
- // **パッドは伴奏用の楽器で、伴奏より1.5オクターブ高いところを鳴らす。**
20651
- // そのまま置くとナイロンギターで10半音、カリンバで9半音、尺八で7半音ぶん
20652
- // 音域を突き抜ける。楽器に合わせて下げる。
20653
- {
20654
- index: 6,
20655
- notes: padNotes,
20656
- octave: fit(padNotes, "chord", 0),
20657
- volume: 64,
20658
- slot: "chord"
20659
- }
20660
- ];
20661
- const backingVolumes = [62, 54, 50];
20662
- for (let i2 = 0; i2 < 3; i2++) {
20663
- const layer = plan.backing[i2];
20664
- layers.push({
20665
- index: 7 + i2,
20666
- notes: layer ? onlyIn(chordNotes(layer.pattern), layer.sections) : [],
20667
- octave: layer?.octave ?? 0,
20668
- volume: backingVolumes[i2],
20669
- slot: "chord"
20670
- });
20671
- }
20672
- const sparkleNotes = plan.sparkle ? onlyIn(chordNotes(plan.sparkle.pattern, -14), plan.sparkle.sections) : [];
20673
- layers.push(
20674
- {
20675
- index: 10,
20676
- notes: sparkleNotes,
20677
- octave: fit(sparkleNotes, "chord", plan.sparkle?.octave ?? 0),
20678
- volume: 56,
20679
- slot: "chord"
20680
- },
20681
- // 掛け合い(デュエット)の相手。**歌入り作曲のときだけ**中身が入る。
20682
- { index: 11, notes: [], octave: 0, volume: 104, slot: "melody" },
20683
- // 2声目のハモリ(主旋律を上下から挟む3声)と、主旋律のオクターブ下の重ね。
20684
- // どちらも曲ごとに出るかどうかが決まる(`song.vocal`)。
20685
- { index: 12, notes: song.harmony2, octave: 0, volume: 74 },
20686
- { index: 13, notes: song.octave, octave: -1, volume: 56, slot: "melody" },
20687
- // **間奏のソロ。** 音が入るのは間奏の小節だけなので、この1本だけを
20688
- // 別の楽器にしても他のセクションの鳴りは変わらない。歌の音域をそのまま
20689
- // 渡すと管楽器が上へ抜ける(テナーサックスで10半音)ので、ここも合わせる。
20690
- {
20691
- index: 14,
20692
- notes: song.solo,
20693
- octave: fit(song.solo, "solo", 0),
20694
- volume: 100,
20695
- slot: "solo"
20696
- }
20697
- );
20698
- return layers;
20699
- };
20700
20998
  var LYRIC_MODEL_CATEGORIES = [
20701
20999
  {
20702
21000
  label: "kusa\u30D7\u30EA\u30BB\u30C3\u30C8",
@@ -25845,8 +26143,9 @@ var playSingingMML = async (mml, options = {}) => {
25845
26143
  });
25846
26144
  const ownsCtx = !options.audioContext;
25847
26145
  const ctx = options.audioContext ?? new AudioContext();
25848
- const destination = options.destination ?? ctx.destination;
26146
+ const rawDestination = options.destination ?? ctx.destination;
25849
26147
  const useSynth = options.synth ?? !options.onPlayNote;
26148
+ const destination = createSafetyLimiter(ctx, rawDestination);
25850
26149
  const synth = useSynth ? createSynth(ctx, destination) : null;
25851
26150
  const pauseWhenHidden = options.pauseWhenHidden ?? ownsCtx;
25852
26151
  let playing = false;
@@ -26851,21 +27150,29 @@ var createDtmStudio = async (options = {}) => {
26851
27150
  };
26852
27151
  applyGlueCompression(options.masterCompression ?? 0);
26853
27152
  const setMasterCompression = (amount) => applyGlueCompression(amount);
26854
- const safetyLimiter = audioCtx.createDynamicsCompressor();
26855
- safetyLimiter.threshold.value = -1;
26856
- safetyLimiter.knee.value = 0;
26857
- safetyLimiter.ratio.value = 20;
26858
- safetyLimiter.attack.value = 1e-3;
26859
- safetyLimiter.release.value = 0.1;
26860
- glueMakeup.connect(safetyLimiter);
26861
27153
  const fadeGain = audioCtx.createGain();
26862
27154
  fadeGain.gain.value = 1;
26863
- safetyLimiter.connect(fadeGain);
26864
27155
  fadeGain.connect(options.destination ?? audioCtx.destination);
27156
+ const safetyLimiter = createSafetyLimiter(audioCtx, fadeGain);
27157
+ glueMakeup.connect(safetyLimiter);
27158
+ const FADE_RESTORE_SEC = 0.02;
27159
+ const rampFadeGainToUnity = (from, at) => {
27160
+ fadeGain.gain.setValueAtTime(from, at);
27161
+ fadeGain.gain.linearRampToValueAtTime(1, at + FADE_RESTORE_SEC);
27162
+ };
27163
+ const restoreFadeGainIfMuted = () => {
27164
+ const current = fadeGain.gain.value;
27165
+ if (current >= 1) return;
27166
+ const now = audioCtx.currentTime;
27167
+ fadeGain.gain.cancelScheduledValues(now);
27168
+ rampFadeGainToUnity(current, now);
27169
+ };
26865
27170
  const scheduleFade = (params) => {
26866
- fadeGain.gain.cancelScheduledValues(audioCtx.currentTime);
27171
+ const now = audioCtx.currentTime;
27172
+ const current = fadeGain.gain.value;
27173
+ fadeGain.gain.cancelScheduledValues(now);
26867
27174
  if (!params) {
26868
- fadeGain.gain.setValueAtTime(1, audioCtx.currentTime);
27175
+ rampFadeGainToUnity(current, now);
26869
27176
  return;
26870
27177
  }
26871
27178
  const { fadeInStartAt, fadeInEndAt, fadeOutStartAt, fadeOutEndAt } = params;
@@ -26873,13 +27180,12 @@ var createDtmStudio = async (options = {}) => {
26873
27180
  fadeGain.gain.setValueAtTime(0, fadeInStartAt);
26874
27181
  fadeGain.gain.linearRampToValueAtTime(1, fadeInEndAt);
26875
27182
  } else {
26876
- fadeGain.gain.setValueAtTime(1, audioCtx.currentTime);
27183
+ rampFadeGainToUnity(current, now);
26877
27184
  }
26878
27185
  if (fadeOutStartAt !== void 0 && fadeOutEndAt !== void 0) {
26879
27186
  fadeGain.gain.setValueAtTime(1, fadeOutStartAt);
26880
27187
  fadeGain.gain.linearRampToValueAtTime(0, fadeOutEndAt);
26881
- fadeGain.gain.setValueAtTime(0, fadeOutEndAt + 2);
26882
- fadeGain.gain.setValueAtTime(1, fadeOutEndAt + 2);
27188
+ rampFadeGainToUnity(0, fadeOutEndAt + 2);
26883
27189
  }
26884
27190
  };
26885
27191
  const clipMeter = createClipMeter(audioCtx, glueMakeup);
@@ -26939,8 +27245,7 @@ var createDtmStudio = async (options = {}) => {
26939
27245
  };
26940
27246
  };
26941
27247
  const resumeAudio = () => {
26942
- fadeGain.gain.cancelScheduledValues(audioCtx.currentTime);
26943
- fadeGain.gain.setValueAtTime(1, audioCtx.currentTime);
27248
+ restoreFadeGainIfMuted();
26944
27249
  if (audioCtx.state === "closed") return Promise.resolve();
26945
27250
  return audioCtx.resume();
26946
27251
  };
@@ -27151,12 +27456,6 @@ var createDtmStudio = async (options = {}) => {
27151
27456
  await listReady;
27152
27457
  nameToKey = await buildNameToKeyMapping();
27153
27458
  await Promise.all([drumReady, loadPreset(defaultPreset)]);
27154
- const restoreFadeGainIfMuted = () => {
27155
- if (fadeGain.gain.value < 1) {
27156
- fadeGain.gain.cancelScheduledValues(audioCtx.currentTime);
27157
- fadeGain.gain.setValueAtTime(1, audioCtx.currentTime);
27158
- }
27159
- };
27160
27459
  const playDrum = (e) => {
27161
27460
  if (!sfDrum.font) return;
27162
27461
  if (e.when === 0) restoreFadeGainIfMuted();
@@ -27907,7 +28206,7 @@ var createDtmStudio = async (options = {}) => {
27907
28206
  };
27908
28207
  const setMasterVolume = (volume) => {
27909
28208
  const g = Math.max(0, Math.min(100, volume)) / 100;
27910
- masterGain.gain.setValueAtTime(g, audioCtx.currentTime);
28209
+ masterGain.gain.setTargetAtTime(g, audioCtx.currentTime, 0.02);
27911
28210
  };
27912
28211
  let recordDestNode = null;
27913
28212
  const createMediaStreamDestination = () => {