@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.js CHANGED
@@ -1970,11 +1970,19 @@ var createChannelStrip = (ctx, destination, options = {}) => {
1970
1970
  const duckGain = ctx.createGain();
1971
1971
  duckGain.gain.value = 1;
1972
1972
  const DUCK_ATTACK_SEC = 8e-3;
1973
+ let duckReleaseEndAt = 0;
1973
1974
  const duck = (atTime, depth = 0.3, releaseSec = 0.18) => {
1974
1975
  const t = Math.max(atTime, ctx.currentTime);
1975
1976
  const floor = clamp(1 - depth, 0, 1);
1976
- duckGain.gain.linearRampToValueAtTime(floor, t + DUCK_ATTACK_SEC);
1977
- duckGain.gain.linearRampToValueAtTime(1, t + DUCK_ATTACK_SEC + releaseSec);
1977
+ const param = duckGain.gain;
1978
+ if (t >= duckReleaseEndAt) {
1979
+ param.setValueAtTime(1, t);
1980
+ } else if (typeof param.cancelAndHoldAtTime === "function") {
1981
+ param.cancelAndHoldAtTime(t);
1982
+ }
1983
+ param.linearRampToValueAtTime(floor, t + DUCK_ATTACK_SEC);
1984
+ param.linearRampToValueAtTime(1, t + DUCK_ATTACK_SEC + releaseSec);
1985
+ duckReleaseEndAt = t + DUCK_ATTACK_SEC + releaseSec;
1978
1986
  };
1979
1987
  eqHigh.connect(compressor);
1980
1988
  compressor.connect(duckGain);
@@ -2271,12 +2279,15 @@ var buildChordPlacements = (options) => {
2271
2279
  }
2272
2280
  }
2273
2281
  } else if (patternType === "alternating") {
2282
+ const quarterSteps = Math.floor(stepsPerBar / 4);
2274
2283
  notes.forEach((noteOffset, i2) => {
2275
- const stepOffset = i2 * Math.floor(stepsPerBar / 4);
2284
+ const stepOffset = i2 * quarterSteps;
2285
+ const isLast = i2 === notes.length - 1;
2286
+ const durationSteps = isLast ? Math.max(12, noteLength - stepOffset) : Math.max(12, quarterSteps);
2276
2287
  placements.push({
2277
2288
  startStep: chord.whenStep + stepOffset,
2278
2289
  pitchUnits: toUnits(noteOffset),
2279
- durationSteps: Math.max(12, Math.floor(stepsPerBar / 4)),
2290
+ durationSteps,
2280
2291
  velocity: 100
2281
2292
  });
2282
2293
  });
@@ -4904,6 +4915,45 @@ var createReverbImpulse = (ctx, decaySec = DEFAULT_REVERB_DECAY_SEC) => {
4904
4915
  };
4905
4916
  var reverbAmountToGain = (amount) => Math.max(0, Math.min(100, amount)) / 100;
4906
4917
 
4918
+ // src/safety-limiter.ts
4919
+ var THRESHOLD_DB = -1;
4920
+ var KNEE_DB = 0;
4921
+ var RATIO = 20;
4922
+ var ATTACK_SEC = 1e-3;
4923
+ var RELEASE_SEC = 0.1;
4924
+ var CURVE_SAMPLES2 = 2048;
4925
+ var SOFT_KNEE = 0.85;
4926
+ var CEILING = 0.98;
4927
+ var SHAPER_RANGE = 4;
4928
+ var OVERSAMPLE2 = "none";
4929
+ var createSoftClipCurve = () => {
4930
+ const curve = new Float32Array(CURVE_SAMPLES2);
4931
+ const span = CEILING - SOFT_KNEE;
4932
+ for (let i2 = 0; i2 < CURVE_SAMPLES2; i2++) {
4933
+ const x2 = (i2 / (CURVE_SAMPLES2 - 1) * 2 - 1) * SHAPER_RANGE;
4934
+ const a = Math.abs(x2);
4935
+ curve[i2] = Math.sign(x2) * (a <= SOFT_KNEE ? a : SOFT_KNEE + span * Math.tanh((a - SOFT_KNEE) / span));
4936
+ }
4937
+ return curve;
4938
+ };
4939
+ var createSafetyLimiter = (ctx, destination) => {
4940
+ const limiter = ctx.createDynamicsCompressor();
4941
+ limiter.threshold.value = THRESHOLD_DB;
4942
+ limiter.knee.value = KNEE_DB;
4943
+ limiter.ratio.value = RATIO;
4944
+ limiter.attack.value = ATTACK_SEC;
4945
+ limiter.release.value = RELEASE_SEC;
4946
+ const preScale = ctx.createGain();
4947
+ preScale.gain.value = 1 / SHAPER_RANGE;
4948
+ const softClip = ctx.createWaveShaper();
4949
+ softClip.curve = createSoftClipCurve();
4950
+ softClip.oversample = OVERSAMPLE2;
4951
+ limiter.connect(preScale);
4952
+ preScale.connect(softClip);
4953
+ softClip.connect(destination);
4954
+ return limiter;
4955
+ };
4956
+
4907
4957
  // src/sequencer.ts
4908
4958
  var STEPS_PER_BEAT = 48;
4909
4959
  var PLAN_TIME = 0.5;
@@ -7367,6 +7417,9 @@ var SONG_DRUM_PATTERNS = {
7367
7417
 
7368
7418
  // src/synth.ts
7369
7419
  var freqFromPitch = (pitchUnits) => unitsToHz(pitchUnits);
7420
+ var MIN_ATTACK_SEC = 15e-4;
7421
+ var MIN_RELEASE_SEC = 4e-3;
7422
+ var TAIL_RATIO = 1e-3;
7370
7423
  var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
7371
7424
  const wave = tone.wave ?? "square";
7372
7425
  const attack = tone.attack ?? 0;
@@ -7385,25 +7438,25 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
7385
7438
  osc.frequency.value = freqFromPitch(e.pitchUnits);
7386
7439
  const t0 = ctx.currentTime + e.when;
7387
7440
  const peak = Math.max(1e-4, 0.06 * e.volume * 1.5 * gainScale);
7441
+ const tail = peak * TAIL_RATIO;
7442
+ const stopAt = t0 + e.duration + MIN_RELEASE_SEC;
7388
7443
  if (tone.decay) {
7389
7444
  gain.gain.setValueAtTime(1e-4, t0);
7390
7445
  gain.gain.linearRampToValueAtTime(peak, t0 + Math.max(3e-3, attack));
7391
- gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
7446
+ gain.gain.exponentialRampToValueAtTime(tail, t0 + e.duration);
7392
7447
  } else {
7393
7448
  const releaseTime = Math.min(0.02, e.duration * 0.1);
7394
7449
  const sustainDuration = e.duration - releaseTime;
7395
- if (attack > 0) {
7396
- gain.gain.setValueAtTime(1e-4, t0);
7397
- gain.gain.linearRampToValueAtTime(
7398
- peak,
7399
- t0 + Math.min(attack, sustainDuration)
7400
- );
7401
- } else {
7402
- gain.gain.setValueAtTime(peak, t0);
7403
- }
7450
+ const attackTime = Math.min(
7451
+ Math.max(attack, MIN_ATTACK_SEC),
7452
+ sustainDuration
7453
+ );
7454
+ gain.gain.setValueAtTime(1e-4, t0);
7455
+ gain.gain.linearRampToValueAtTime(peak, t0 + attackTime);
7404
7456
  gain.gain.setValueAtTime(peak, t0 + sustainDuration);
7405
- gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
7457
+ gain.gain.exponentialRampToValueAtTime(tail, t0 + e.duration);
7406
7458
  }
7459
+ gain.gain.linearRampToValueAtTime(0, stopAt);
7407
7460
  osc.connect(gain);
7408
7461
  let panner = null;
7409
7462
  if (typeof ctx.createStereoPanner === "function" && e.pan) {
@@ -7415,7 +7468,7 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
7415
7468
  gain.connect(compressor);
7416
7469
  }
7417
7470
  osc.start(t0);
7418
- osc.stop(t0 + e.duration + 0.02);
7471
+ osc.stop(stopAt);
7419
7472
  osc.onended = () => {
7420
7473
  osc.disconnect();
7421
7474
  gain.disconnect();
@@ -7430,14 +7483,21 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
7430
7483
  if (isKick) {
7431
7484
  const osc = ctx.createOscillator();
7432
7485
  const g2 = ctx.createGain();
7486
+ const kickPeak = vol * 0.135;
7487
+ const kickDecayEnd = t0 + 0.18;
7488
+ const kickEnd = kickDecayEnd + MIN_RELEASE_SEC;
7433
7489
  osc.frequency.setValueAtTime(150, t0);
7434
7490
  osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
7435
- g2.gain.setValueAtTime(vol * 0.135, t0);
7436
- g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
7491
+ g2.gain.setValueAtTime(kickPeak, t0);
7492
+ g2.gain.exponentialRampToValueAtTime(kickPeak * TAIL_RATIO, kickDecayEnd);
7493
+ g2.gain.linearRampToValueAtTime(0, kickEnd);
7437
7494
  osc.connect(g2).connect(compressor);
7438
7495
  osc.start(t0);
7439
- osc.stop(t0 + 0.2);
7440
- osc.onended = () => osc.disconnect();
7496
+ osc.stop(kickEnd);
7497
+ osc.onended = () => {
7498
+ osc.disconnect();
7499
+ g2.disconnect();
7500
+ };
7441
7501
  return;
7442
7502
  }
7443
7503
  const dur = isSnareLike ? 0.18 : 0.05;
@@ -7451,8 +7511,13 @@ var createSynth = (ctx, destination = ctx.destination, tone = {}) => {
7451
7511
  filter.type = isSnareLike ? "bandpass" : "highpass";
7452
7512
  filter.frequency.value = isSnareLike ? 2e3 : 8e3;
7453
7513
  const g = ctx.createGain();
7454
- g.gain.setValueAtTime(vol * (isSnareLike ? 0.105 : 0.06), t0);
7455
- g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
7514
+ const noisePeak = vol * (isSnareLike ? 0.105 : 0.06);
7515
+ g.gain.setValueAtTime(noisePeak, t0);
7516
+ g.gain.exponentialRampToValueAtTime(
7517
+ noisePeak * TAIL_RATIO,
7518
+ t0 + dur - MIN_RELEASE_SEC
7519
+ );
7520
+ g.gain.linearRampToValueAtTime(0, t0 + dur);
7456
7521
  src.connect(filter).connect(g).connect(compressor);
7457
7522
  src.start(t0);
7458
7523
  src.stop(t0 + dur);
@@ -7514,7 +7579,7 @@ var playPlacements = (placements, options) => {
7514
7579
  reverbPreDelay.connect(reverbConvolver);
7515
7580
  reverbConvolver.connect(reverbWetGain);
7516
7581
  reverbWetGain.connect(finalMix);
7517
- finalMix.connect(rawDestination);
7582
+ finalMix.connect(createSafetyLimiter(ctx, rawDestination));
7518
7583
  const channelStrips = /* @__PURE__ */ new Map();
7519
7584
  const getChannelStrip = (index) => {
7520
7585
  let strip = channelStrips.get(index);
@@ -9969,17 +10034,24 @@ var mountMmlPlayer = (target, mml, options = {}) => {
9969
10034
  if (!audioCtx) audioCtx = new AudioContext();
9970
10035
  return audioCtx;
9971
10036
  };
10037
+ let limiterNode = null;
10038
+ const ensureOutput = () => {
10039
+ const ctx = ensureCtx();
10040
+ if (!limiterNode) limiterNode = createSafetyLimiter(ctx, ctx.destination);
10041
+ return limiterNode;
10042
+ };
9972
10043
  let synthInstance = null;
9973
10044
  const ensureSynth = () => {
9974
- if (!synthInstance) synthInstance = createSynth(ensureCtx());
10045
+ if (!synthInstance) {
10046
+ synthInstance = createSynth(ensureCtx(), ensureOutput());
10047
+ }
9975
10048
  return synthInstance;
9976
10049
  };
9977
10050
  let voices = null;
9978
10051
  const ensureVoices = () => {
9979
10052
  if (options.singingVoices) return options.singingVoices;
9980
10053
  if (!voices) {
9981
- const ctx = ensureCtx();
9982
- voices = createSingingVoices(ctx, ctx.destination);
10054
+ voices = createSingingVoices(ensureCtx(), ensureOutput());
9983
10055
  voices.setVolume(trackVolume / 100 * (masterVolume / 100));
9984
10056
  }
9985
10057
  return voices;
@@ -11041,6 +11113,12 @@ var SoundFont = class _SoundFont {
11041
11113
  // ファミリごとに変える。
11042
11114
  /** アタック(無音からピークまで)秒。クリック防止の最小限。全楽器共通。 */
11043
11115
  static attackSec = 5e-3;
11116
+ /**
11117
+ * 消え際に最低限確保する秒数。アタックと対になるクリック防止で、**サンプルが
11118
+ * 終わるまでに**必ずここまでに 0 へ落とし切る。振幅が残ったままバッファが尽きたり
11119
+ * `stop()` が来たりすると、その瞬間の値がそのまま段差になりプチノイズが出る。
11120
+ */
11121
+ static minReleaseSec = 4e-3;
11044
11122
  /**
11045
11123
  * 減衰の型ごとのエンベロープ。
11046
11124
  *
@@ -11227,14 +11305,23 @@ var SoundFont = class _SoundFont {
11227
11305
  const startGainTime = Math.max(ctx.currentTime, _when);
11228
11306
  g.gain.setValueAtTime(0, startGainTime);
11229
11307
  const env = _SoundFont.envelopes[this.style.env] ?? _SoundFont.envelopes.sustain;
11230
- const limit = src.loop ? Number.POSITIVE_INFINITY : _when + _param.max - startOffsetSec;
11308
+ const playRate = _param.playbackRate * 2 ** ((humanizeCents + detuneCents) / 1200);
11309
+ const sampleEnd = _when + (buffer.duration - startOffsetSec) / playRate;
11310
+ const limit = src.loop ? Number.POSITIVE_INFINITY : sampleEnd;
11231
11311
  const attackEnd = Math.min(startGainTime + _SoundFont.attackSec, limit);
11232
11312
  const decayEnd = Math.min(
11233
11313
  Math.max(attackEnd, attackEnd + env.decaySec),
11234
11314
  limit
11235
11315
  );
11236
- const noteOff = Math.min(Math.max(decayEnd, _when + duration), limit);
11237
- const end = isDrum ? _when + buffer.duration - startOffsetSec : Math.max(Math.min(noteOff + env.releaseSec, limit), noteOff + 1e-3);
11316
+ const tailRoom = Math.min(
11317
+ _SoundFont.minReleaseSec,
11318
+ Math.max(0, limit - decayEnd)
11319
+ );
11320
+ const noteOff = Math.min(
11321
+ Math.max(decayEnd, _when + duration),
11322
+ limit - tailRoom
11323
+ );
11324
+ const end = isDrum ? sampleEnd : Math.max(Math.min(noteOff + env.releaseSec, limit), noteOff + tailRoom);
11238
11325
  if (!isDrum) {
11239
11326
  const sustainVolume = effectiveVolume * env.sustain;
11240
11327
  g.gain.linearRampToValueAtTime(effectiveVolume, attackEnd);
@@ -11243,6 +11330,11 @@ var SoundFont = class _SoundFont {
11243
11330
  g.gain.linearRampToValueAtTime(0, end);
11244
11331
  } else {
11245
11332
  g.gain.linearRampToValueAtTime(effectiveVolume, attackEnd);
11333
+ const releaseStart = Math.max(attackEnd, end - _SoundFont.minReleaseSec);
11334
+ if (releaseStart > attackEnd) {
11335
+ g.gain.setValueAtTime(effectiveVolume, releaseStart);
11336
+ }
11337
+ g.gain.linearRampToValueAtTime(0, end);
11246
11338
  }
11247
11339
  if (filter) src.connect(filter).connect(g);
11248
11340
  else src.connect(g);
@@ -11452,12 +11544,10 @@ var addParam = (zone, pitch) => {
11452
11544
  coarseTune,
11453
11545
  fineTune,
11454
11546
  sampleRate,
11455
- delay,
11456
- buffer
11547
+ delay
11457
11548
  } = zone;
11458
11549
  const baseDetune = originalPitch - 100 * coarseTune - fineTune;
11459
11550
  const playbackRate = 2 ** ((100 * pitch - baseDetune) / 1200);
11460
- const max = (buffer?.duration ?? 0) / playbackRate;
11461
11551
  const src = {
11462
11552
  loop: loopStart >= 1 && loopStart < loopEnd
11463
11553
  };
@@ -11465,7 +11555,7 @@ var addParam = (zone, pitch) => {
11465
11555
  [src.loopStart, src.loopEnd] = [loopStart, loopEnd].map(
11466
11556
  (v) => v / sampleRate + delay
11467
11557
  );
11468
- zone._param = { playbackRate, max, src };
11558
+ zone._param = { playbackRate, src };
11469
11559
  };
11470
11560
 
11471
11561
  // src/chord-player.ts
@@ -12763,29 +12853,30 @@ var CORPUS_BANDS = {
12763
12853
  climaxPeaks: [1, 2, 13.5, 26],
12764
12854
  complementarity: [0, 0.026, 0.179, 0.405]
12765
12855
  };
12766
- var CORPUS_MEDIANS = {
12767
- entropy: 0.907,
12768
- valueKinds: 4,
12769
- restRatio: 0.149,
12770
- leapRatio: 0.339,
12771
- stepRatio: 0.538,
12772
- chromaticRatio: 0.052,
12773
- maxLeap: 12,
12774
- melodyRange: 20,
12775
- notesPerBar: 5.511,
12776
- shortNoteRatio: 0.816,
12777
- barDensityCv: 0.279,
12778
- densityCliff: 0.026,
12779
- sim1: 0.518,
12780
- sim2: 0.552,
12781
- sim4: 0.616,
12782
- sim8: 0.676,
12783
- phraseBreath: 0.227,
12784
- turnRatio: 0.519,
12785
- climaxPosition: 0.273,
12786
- climaxPeaks: 6,
12787
- complementarity: 0.075
12788
- };
12856
+ var CORPUS_PROFILE_KEYS = [
12857
+ "entropy",
12858
+ "valueKinds",
12859
+ "restRatio",
12860
+ "leapRatio",
12861
+ "stepRatio",
12862
+ "chromaticRatio",
12863
+ "maxLeap",
12864
+ "melodyRange",
12865
+ "notesPerBar",
12866
+ "shortNoteRatio",
12867
+ "barDensityCv",
12868
+ "densityCliff",
12869
+ "sim1",
12870
+ "sim2",
12871
+ "sim4",
12872
+ "sim8",
12873
+ "phraseBreath",
12874
+ "turnRatio",
12875
+ "climaxPosition",
12876
+ "climaxPeaks",
12877
+ "complementarity"
12878
+ ];
12879
+ var CORPUS_DEVIATION_BUDGET = 4;
12789
12880
  var CORPUS_CELL_WEIGHTS = {
12790
12881
  "0,2,4,6,8,10,12,14": 584,
12791
12882
  "0,2,4,6,8,12,14": 512,
@@ -13650,16 +13741,15 @@ var tensionFeatures = (barTension, opts) => {
13650
13741
  return { rise, resolve };
13651
13742
  };
13652
13743
  var band = (v, lo, idealLo, idealHi, hi) => {
13653
- if (v <= lo || v >= hi) return 0;
13654
13744
  if (v >= idealLo && v <= idealHi) return 1;
13745
+ if (v <= lo || v >= hi) return 0;
13655
13746
  if (v < idealLo) return idealLo === lo ? 0 : (v - lo) / (idealLo - lo);
13656
13747
  return hi === idealHi ? 0 : (hi - v) / (hi - idealHi);
13657
13748
  };
13658
- var centeredBand = (v, b, median) => {
13659
- const score = band(v, b[0], b[1], b[2], b[3]);
13660
- if (score < 1) return score;
13661
- const spread = Math.max(median - b[1], b[2] - median, 1e-9);
13662
- return 1 - 0.15 * Math.min(1, Math.abs(v - median) / spread);
13749
+ var plausibleBand = (v, b) => {
13750
+ const span = b[3] - b[0];
13751
+ const margin = span > 0 ? span * 0.5 : 1;
13752
+ return band(v, b[0] - margin, b[0], b[3], b[3] + margin);
13663
13753
  };
13664
13754
  var featureVector = (f) => [
13665
13755
  f.entropy / 3,
@@ -14562,6 +14652,7 @@ var sectionAt = (plan, bar) => {
14562
14652
  // src/compose.ts
14563
14653
  var STEP_SEMITONES = 2;
14564
14654
  var MAX_LEAP_SEMITONES = 10;
14655
+ var LEAP_CEILINGS = [7, 8, 9, 10, 10, 12, 14, 16];
14565
14656
  var MAX_BAR_LEAP_SEMITONES = 10;
14566
14657
  var HARD = {
14567
14658
  /** メロディが1音も無い、音域が半音未満(同じ音を並べただけ)。 */
@@ -14620,13 +14711,16 @@ var WEIGHTS = {
14620
14711
  // --- 直近に作った曲と違うか ---
14621
14712
  novelty: 1.2
14622
14713
  };
14714
+ var DEVIATION_BUDGET = CORPUS_DEVIATION_BUDGET;
14715
+ var BUDGETED_KEYS = new Set(CORPUS_PROFILE_KEYS);
14623
14716
  var HAND_BANDS = {
14624
14717
  /** サブメロの音数/小節。少なすぎると「置いただけ」、多すぎるとメロディを食う。 */
14625
14718
  subDensity: [0.5, 1.8, 4.5, 8],
14626
14719
  complementarity: [0.05, 0.25, 0.7, 0.95],
14627
14720
  climaxPeaks: [0, 1, 2, 5]
14628
14721
  };
14629
- var DRAW_COUNT = 40;
14722
+ var DRAW_COUNT = 12;
14723
+ var SELECT_TEMPERATURE = 0.05;
14630
14724
  var BASE_STEPS_PER_BAR = 192;
14631
14725
  var WHOLE = 192;
14632
14726
  var DOT_HALF = 144;
@@ -15342,6 +15436,14 @@ for (const c of [...MOTIF_CELLS, ...RHYTHM_CELLS]) {
15342
15436
  const key = onsetKeyOf(c.value);
15343
15437
  cellEntryCount.set(key, (cellEntryCount.get(key) ?? 0) + 1);
15344
15438
  }
15439
+ var resolveMelodyForm = (choice, rnd) => {
15440
+ const c = (choice ?? "").trim() || "auto";
15441
+ if (c === "motif" || c === "ostinato" || c === "through") return c;
15442
+ const r = rnd();
15443
+ if (r < 0.25) return "ostinato";
15444
+ if (r < 0.4) return "through";
15445
+ return "motif";
15446
+ };
15345
15447
  var groovyCells = (cells, groove, rnd) => {
15346
15448
  const hasSixteenth = (c) => c.value.some((v) => Math.abs(v) <= SIXTEENTH);
15347
15449
  if (groove === "eighth") {
@@ -15517,7 +15619,7 @@ var clampSemi = (semi, low, high) => {
15517
15619
  while (s > high) s -= 12;
15518
15620
  return s;
15519
15621
  };
15520
- var leapTarget = (from, tones, rnd) => {
15622
+ var leapTarget = (from, tones, rnd, maxLeap = MAX_LEAP_SEMITONES) => {
15521
15623
  const candidates = [];
15522
15624
  for (const tone of tones) {
15523
15625
  const base = pitchClass(tone.semi);
@@ -15525,7 +15627,7 @@ var leapTarget = (from, tones, rnd) => {
15525
15627
  const semi = base + oct * 12;
15526
15628
  if (semi < MELODY_LOW || semi > MELODY_HIGH) continue;
15527
15629
  const gap = Math.abs(semi - from);
15528
- if (gap >= 3 && gap <= MAX_LEAP_SEMITONES) candidates.push(semi);
15630
+ if (gap >= 3 && gap <= maxLeap) candidates.push(semi);
15529
15631
  }
15530
15632
  }
15531
15633
  if (candidates.length === 0) return null;
@@ -15682,7 +15784,7 @@ var barDegrees = (role, slots, tones, style, scale, motifContour, startDegree, c
15682
15784
  if (slots[i2].value >= quarterSteps) {
15683
15785
  if (rnd() < style.leapAffinity) {
15684
15786
  const from = degreeToPitch(scale, out[i2 - 1]).semi;
15685
- const target = leapTarget(from, tones, rnd);
15787
+ const target = leapTarget(from, tones, rnd, style.maxLeap);
15686
15788
  if (target !== null) out[i2] = semitoneToDegree(scale, target);
15687
15789
  }
15688
15790
  continue;
@@ -15693,13 +15795,13 @@ var barDegrees = (role, slots, tones, style, scale, motifContour, startDegree, c
15693
15795
  }
15694
15796
  return out;
15695
15797
  };
15696
- var fitMotif = (degrees, slots, tones, prevSemi, quarterSteps, scale, pentatonic, preferShift) => {
15798
+ var fitMotif = (degrees, slots, tones, prevSemi, quarterSteps, scale, pentatonic, preferShift, maxShift = 3) => {
15697
15799
  let best = degrees;
15698
15800
  let bestShift = 0;
15699
15801
  let bestScore = Number.NEGATIVE_INFINITY;
15700
15802
  let preferScore = Number.NEGATIVE_INFINITY;
15701
15803
  let preferMoved = null;
15702
- for (let shift = -3; shift <= 3; shift++) {
15804
+ for (let shift = -maxShift; shift <= maxShift; shift++) {
15703
15805
  const moved = degrees.map(
15704
15806
  (d) => pentatonic ? coreToDegree(scale, degreeToCore(scale, d) + shift) : d + shift
15705
15807
  );
@@ -15756,7 +15858,7 @@ var shapeBar = (degrees, slots, tones, prevSemi, opts) => {
15756
15858
  MELODY_LOW,
15757
15859
  MELODY_HIGH
15758
15860
  );
15759
- const limit = i2 === 0 ? MAX_BAR_LEAP_SEMITONES : MAX_LEAP_SEMITONES;
15861
+ const limit = i2 === 0 ? MAX_BAR_LEAP_SEMITONES : opts.maxLeap;
15760
15862
  if (!opts.allowLeap && Math.abs(semi - prev) > limit) {
15761
15863
  semi = clampSemi(
15762
15864
  walk(opts.scale, prev, Math.sign(semi - prev) * 3),
@@ -16149,6 +16251,7 @@ var draw = (options, resolvedKey, scale, rnd) => {
16149
16251
  landing = (landing + pick([2, 4], rnd)) % size;
16150
16252
  return landing;
16151
16253
  };
16254
+ const form = resolveMelodyForm(options.form, rnd);
16152
16255
  const units2 = [];
16153
16256
  for (const section of sectionPlan) {
16154
16257
  const unitCount = Math.max(1, Math.round(section.bars / 2));
@@ -16169,16 +16272,31 @@ var draw = (options, resolvedKey, scale, rnd) => {
16169
16272
  const isLast = u === unitCount - 1;
16170
16273
  if (u % 2 === 0) {
16171
16274
  units2.push({
16172
- role: section.kind === "bridge" ? "step" : section.kind === "chorus" ? "climax" : src === "a2" || u > 0 ? "sequence" : "motif",
16173
- source: src,
16275
+ // リフ主体の曲は**変形しない**。セクエンツもオクターブ上げも入れず、
16276
+ // 同じ型を回し続ける。セクションの対比は編曲側(ドラム・楽器・レイヤ)
16277
+ // が担う——ヤツメ穴型の曲がまさにその作りで、120小節を通して
16278
+ // 5半音のセルが変わらない。
16279
+ role: form === "ostinato" ? "motif" : form === "through" ? (
16280
+ // 通し作曲は素材を戻さない。`step` の書法(アーチ・谷・波)で
16281
+ // 独立した線を書き、たまに走句と山を挟んで単調さを避ける。
16282
+ u % 3 === 2 ? "run" : u % 3 === 1 ? "climax" : "step"
16283
+ ) : section.kind === "bridge" ? "step" : section.kind === "chorus" ? "climax" : src === "a2" || u > 0 ? "sequence" : "motif",
16284
+ // リフ型は素材も1つに揃える(`sourceOf` でセクションごとに
16285
+ // 変えると、そこだけ別の型が始まってオスティナートにならない)。
16286
+ source: form === "ostinato" ? "a" : src,
16174
16287
  landing: null,
16175
16288
  section
16176
16289
  });
16177
16290
  } else {
16178
16291
  const landing = landingOf(section);
16292
+ const riff = form === "ostinato" && !(isLast && landing === 0);
16179
16293
  units2.push({
16180
- role: isLast && landing === 0 ? "cadence" : "answer",
16181
- source: "answer",
16294
+ role: isLast && landing === 0 ? "cadence" : riff ? "motif" : (
16295
+ // 通し作曲は「問いと答え」で閉じない。answer は問いのリズムを
16296
+ // 受けて着地音だけ変える形なので、そのままだと反復が戻る。
16297
+ form === "through" ? "step" : "answer"
16298
+ ),
16299
+ source: riff ? "a" : "answer",
16182
16300
  landing: isLast ? landing : null,
16183
16301
  section
16184
16302
  });
@@ -16196,6 +16314,7 @@ var draw = (options, resolvedKey, scale, rnd) => {
16196
16314
  const restatementOf = (bar) => {
16197
16315
  const curSec = sectionAt(sectionPlan, bar);
16198
16316
  if (!curSec.spec.melody) return null;
16317
+ if (form === "through") return null;
16199
16318
  if (curSec.restatement) {
16200
16319
  const firstSec = sectionPlan.find(
16201
16320
  (s) => s.kind === curSec.kind && !s.restatement
@@ -16217,13 +16336,15 @@ var draw = (options, resolvedKey, scale, rnd) => {
16217
16336
  }
16218
16337
  return null;
16219
16338
  };
16339
+ const registerSpread = form === "ostinato" ? rnd() ** 2 * 0.5 : 0.25 + rnd() * 0.75;
16220
16340
  const style = {
16221
16341
  groove: pick(["eighth", "sixteenth"], rnd),
16222
16342
  arcPeriod: pick([4, 8, 8, 16], rnd),
16223
16343
  arcPhase: pick([0, 1, 2], rnd),
16224
- arcAmp: 2 + rnd() * 3,
16344
+ arcAmp: 5 * registerSpread,
16225
16345
  // オクターブ跳躍は参考曲では音程の1.0%しかない。上げすぎると音域が広がる。
16226
- octaveAffinity: 0.06 + rnd() * 0.12,
16346
+ octaveAffinity: 0.18 * registerSpread,
16347
+ maxLeap: pick(LEAP_CEILINGS, rnd),
16227
16348
  // **音階を厳しく締める曲は必ず中核音の歩数で組む。** ダイアトニックの度数で輪郭を
16228
16349
  // 作ると、琉球音階なのにレやラが輪郭の中に入り込む。ファ・シを自由に使う
16229
16350
  // 陽・民謡だけが、曲ごとに掛けたり掛けなかったりする({@link ComposeScale.strict})。
@@ -16276,8 +16397,11 @@ var draw = (options, resolvedKey, scale, rnd) => {
16276
16397
  subInterval: pick([3, 4, 8, 9], rnd)
16277
16398
  };
16278
16399
  const motifPool = groovyCells(MOTIF_CELLS, style.groove, rnd);
16279
- const targetNotesPerBar = 4.5 + rnd() * 4.5;
16280
- const targetRestRatio = 0.02 + rnd() * 0.1;
16400
+ const targetRestRatio = 0.02 + rnd() ** 2 * 0.42;
16401
+ const targetNotesPerBar = Math.max(
16402
+ 2.8,
16403
+ Math.min(9.5, 7.4 - 6 * targetRestRatio + (rnd() * 4 - 2))
16404
+ );
16281
16405
  const cellNotes = (c) => c.value.filter((v) => v > 0).length;
16282
16406
  const cellRest = (c) => {
16283
16407
  let rest = 0;
@@ -16300,9 +16424,13 @@ var draw = (options, resolvedKey, scale, rnd) => {
16300
16424
  return pool[pool.length - 1];
16301
16425
  };
16302
16426
  const pickCell = (pool, densityMul = 1) => {
16303
- let best = weightedPick(pool);
16427
+ const want = targetRestRatio / Math.max(0.5, densityMul);
16428
+ const tol = Math.max(0.08, want * 0.6);
16429
+ const near = pool.filter((c) => Math.abs(cellRest(c) - want) <= tol);
16430
+ const from = near.length > 0 ? near : pool;
16431
+ let best = weightedPick(from);
16304
16432
  for (let i2 = 0; i2 < 2; i2++) {
16305
- const c = weightedPick(pool);
16433
+ const c = weightedPick(from);
16306
16434
  if (cellDistance(c, densityMul) < cellDistance(best, densityMul))
16307
16435
  best = c;
16308
16436
  }
@@ -16558,13 +16686,16 @@ var draw = (options, resolvedKey, scale, rnd) => {
16558
16686
  quarterSteps,
16559
16687
  scale,
16560
16688
  style.pentatonicMotif,
16561
- motifShiftMemo.get(shiftKey) ?? null
16689
+ motifShiftMemo.get(shiftKey) ?? null,
16690
+ // リフ型は和音へ寄せない。同じセルを回し続けるのが役目。
16691
+ form === "ostinato" ? 0 : 3
16562
16692
  );
16563
16693
  fitted = r.degrees;
16564
16694
  motifShiftMemo.set(shiftKey, r.shift);
16565
16695
  }
16566
16696
  const pitches = shapeBar(fitted, slots, tones, prevSemi, {
16567
16697
  scale,
16698
+ maxLeap: style.maxLeap,
16568
16699
  allowLeap: role === "climax",
16569
16700
  allowArpeggio: role === "climax" || role === "run" && style.runShape === "broken" || role === "cadence" && style.cadenceShape !== "descend",
16570
16701
  quarterSteps,
@@ -17010,6 +17141,7 @@ var draw = (options, resolvedKey, scale, rnd) => {
17010
17141
  (n) => n.durationSteps >= quarterSteps || rnd() < octaveCoverage
17011
17142
  ).map((n) => ({ ...n, velocity: Math.max(40, n.velocity - 26) })) : [];
17012
17143
  return {
17144
+ form,
17013
17145
  chordProgression,
17014
17146
  chordPattern,
17015
17147
  rootShift,
@@ -17096,7 +17228,7 @@ var evaluate = (d, recent) => {
17096
17228
  Math.min(...recent.map((r) => featureDistance(fingerprint, r))) / 1
17097
17229
  );
17098
17230
  const at = (b, v) => band(v, b[0], b[1], b[2], b[3]);
17099
- const atc = (key, v) => centeredBand(v, CORPUS_BANDS[key], CORPUS_MEDIANS[key]);
17231
+ const atc = (key, v) => plausibleBand(v, CORPUS_BANDS[key]);
17100
17232
  const peakBand = d.bars > 24 ? [0, 1, Math.round(d.bars / 16), Math.round(d.bars / 8) + 2] : HAND_BANDS.climaxPeaks;
17101
17233
  const scoreBreakdown = {
17102
17234
  entropy: atc("entropy", entropy),
@@ -17132,9 +17264,16 @@ var evaluate = (d, recent) => {
17132
17264
  tensionResolve: d.tonal.floating ? 1 : tension.resolve,
17133
17265
  novelty
17134
17266
  };
17267
+ const forgiven = new Set(
17268
+ Object.entries(WEIGHTS).filter(([key]) => BUDGETED_KEYS.has(key)).map(([key, weight]) => ({
17269
+ key,
17270
+ deficit: weight * (1 - (scoreBreakdown[key] ?? 0))
17271
+ })).sort((a, b) => b.deficit - a.deficit).slice(0, DEVIATION_BUDGET).filter((e) => e.deficit > 0).map((e) => e.key)
17272
+ );
17135
17273
  let weighted = 0;
17136
17274
  let weightSum = 0;
17137
17275
  for (const [key, weight] of Object.entries(WEIGHTS)) {
17276
+ if (forgiven.has(key)) continue;
17138
17277
  weighted += (scoreBreakdown[key] ?? 0) * weight;
17139
17278
  weightSum += weight;
17140
17279
  }
@@ -17171,49 +17310,64 @@ var composeSong = (options) => {
17171
17310
  resolvedKey.mode === "minor",
17172
17311
  rnd
17173
17312
  );
17174
- let best = null;
17175
- let bestScore = Number.NEGATIVE_INFINITY;
17176
- let bestIsValid = false;
17313
+ const valid = [];
17314
+ const invalid = [];
17177
17315
  let rejected = 0;
17178
17316
  for (let attempt = 1; attempt <= count; attempt++) {
17179
- const d = draw(options, resolvedKey, scale, rnd);
17180
- const { stats, ok } = evaluate(d, recent);
17181
- if (!ok) rejected++;
17182
- const better = ok === bestIsValid ? stats.score > bestScore : ok;
17183
- if (!better) continue;
17184
- bestScore = stats.score;
17185
- bestIsValid = ok;
17186
- best = {
17187
- // ドラム・楽器・編曲プランは勝った候補にだけ後から付ける(メロディに
17188
- // 依存しないので候補ごとに引いても採点は動かず、40本ぶん無駄になる)。
17189
- drum: "",
17190
- instrument: "",
17191
- arrange: EMPTY_ARRANGE,
17192
- chordProgression: d.chordProgression,
17193
- chordPattern: d.chordPattern,
17194
- rootShift: d.rootShift,
17195
- keyName: d.keyName,
17196
- keyLabel: d.keyLabel,
17197
- scaleId: scale.id,
17198
- scaleLabel: scale.label,
17199
- moodLabel: d.moodLabel,
17200
- bpm: d.bpm,
17201
- sections: d.sections,
17202
- bars: d.bars,
17203
- vocal: d.vocal,
17204
- tonal: d.tonal,
17205
- melody: d.melody,
17206
- submelody: d.submelody,
17207
- bass: d.bass,
17208
- harmony: d.harmony,
17209
- harmony2: d.harmony2,
17210
- octave: d.octave,
17211
- pad: d.pad,
17212
- solo: d.solo,
17213
- stats: { ...stats, attempts: attempt, rejected }
17214
- };
17317
+ const d2 = draw(options, resolvedKey, scale, rnd);
17318
+ const { stats, ok } = evaluate(d2, recent);
17319
+ if (ok) valid.push({ d: d2, stats });
17320
+ else {
17321
+ rejected++;
17322
+ invalid.push({ d: d2, stats });
17323
+ }
17324
+ }
17325
+ const pool = valid.length > 0 ? valid : invalid;
17326
+ const top = Math.max(...pool.map((c) => c.stats.score));
17327
+ const weights = pool.map(
17328
+ (c) => Math.exp((c.stats.score - top) / SELECT_TEMPERATURE)
17329
+ );
17330
+ const total = weights.reduce((a, b) => a + b, 0);
17331
+ let ticket = rnd() * total;
17332
+ let chosen = pool[pool.length - 1];
17333
+ for (let i2 = 0; i2 < pool.length; i2++) {
17334
+ ticket -= weights[i2];
17335
+ if (ticket <= 0) {
17336
+ chosen = pool[i2];
17337
+ break;
17338
+ }
17215
17339
  }
17216
- const result = best;
17340
+ const d = chosen.d;
17341
+ const result = {
17342
+ // ドラム・楽器・編曲プランは勝った候補にだけ後から付ける(メロディに
17343
+ // 依存しないので候補ごとに引いても採点は動かず、候補数ぶん無駄になる)。
17344
+ drum: "",
17345
+ instrument: "",
17346
+ arrange: EMPTY_ARRANGE,
17347
+ chordProgression: d.chordProgression,
17348
+ chordPattern: d.chordPattern,
17349
+ rootShift: d.rootShift,
17350
+ keyName: d.keyName,
17351
+ keyLabel: d.keyLabel,
17352
+ scaleId: scale.id,
17353
+ scaleLabel: scale.label,
17354
+ form: d.form,
17355
+ moodLabel: d.moodLabel,
17356
+ bpm: d.bpm,
17357
+ sections: d.sections,
17358
+ bars: d.bars,
17359
+ vocal: d.vocal,
17360
+ tonal: d.tonal,
17361
+ melody: d.melody,
17362
+ submelody: d.submelody,
17363
+ bass: d.bass,
17364
+ harmony: d.harmony,
17365
+ harmony2: d.harmony2,
17366
+ octave: d.octave,
17367
+ pad: d.pad,
17368
+ solo: d.solo,
17369
+ stats: { ...chosen.stats, attempts: count, rejected }
17370
+ };
17217
17371
  result.stats.attempts = count;
17218
17372
  result.stats.rejected = rejected;
17219
17373
  result.drum = pickBuiltinDrum(result, rnd);
@@ -17414,157 +17568,643 @@ var composeLyrics = (melody, options) => {
17414
17568
  return out.join("");
17415
17569
  };
17416
17570
 
17417
- // src/delay.ts
17418
- var DELAY_DIVISIONS = [
17419
- { value: "4", label: "4\u5206", beats: 1 },
17420
- { value: "8", label: "8\u5206", beats: 0.5 },
17421
- { value: "8d", label: "\u4ED8\u70B98\u5206", beats: 0.75 },
17422
- { value: "16", label: "16\u5206", beats: 0.25 }
17423
- ];
17424
- var clamp4 = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
17425
- var divisionToSeconds = (division, bpm) => {
17426
- const beats = DELAY_DIVISIONS.find((d) => d.value === division)?.beats ?? 0.5;
17427
- const safeBpm = bpm > 0 ? bpm : 120;
17428
- return 60 / safeBpm * beats;
17429
- };
17430
- var DELAY_MAX_WET = 0.45;
17431
- var delayAmountToGain = (amount) => clamp4(amount, 0, 100) / 100 * DELAY_MAX_WET;
17432
- var FEEDBACK_GAIN = 0.3;
17433
- var MAX_DELAY_SEC = 2;
17434
- var createDelayBus = (ctx, destination, options = {}) => {
17435
- const input = ctx.createGain();
17436
- const delayNode = ctx.createDelay(MAX_DELAY_SEC);
17437
- const feedback = ctx.createGain();
17438
- feedback.gain.value = FEEDBACK_GAIN;
17439
- const wetGain = ctx.createGain();
17440
- wetGain.gain.value = delayAmountToGain(options.amount ?? 0);
17441
- input.connect(delayNode);
17442
- delayNode.connect(feedback);
17443
- feedback.connect(delayNode);
17444
- delayNode.connect(wetGain);
17445
- wetGain.connect(destination);
17446
- let bpm = options.bpm ?? 120;
17447
- let division = options.division ?? "8";
17448
- const applyTime = () => {
17449
- delayNode.delayTime.setTargetAtTime(
17450
- divisionToSeconds(division, bpm),
17451
- ctx.currentTime,
17452
- 0.05
17453
- );
17454
- };
17455
- applyTime();
17456
- return {
17457
- input,
17458
- setAmount: (amount) => {
17459
- wetGain.gain.setTargetAtTime(
17460
- delayAmountToGain(amount),
17461
- ctx.currentTime,
17462
- 0.02
17463
- );
17464
- },
17465
- setDivision: (d) => {
17466
- division = d;
17467
- applyTime();
17468
- },
17469
- setBpm: (b) => {
17470
- bpm = b;
17471
- applyTime();
17472
- },
17473
- dispose: () => {
17474
- input.disconnect();
17475
- delayNode.disconnect();
17476
- feedback.disconnect();
17477
- wetGain.disconnect();
17478
- }
17479
- };
17480
- };
17481
-
17482
- // src/panel-state.ts
17483
- var STORAGE_PREFIX = "dtm-panel-open:";
17484
- var readPanelOpen = (key) => {
17485
- try {
17486
- if (typeof localStorage === "undefined" || !localStorage) return null;
17487
- const raw = localStorage.getItem(STORAGE_PREFIX + key);
17488
- if (raw === "1") return true;
17489
- if (raw === "0") return false;
17490
- } catch (_) {
17491
- }
17492
- return null;
17493
- };
17494
- var writePanelOpen = (key, open) => {
17495
- try {
17496
- if (typeof localStorage === "undefined" || !localStorage) return;
17497
- localStorage.setItem(STORAGE_PREFIX + key, open ? "1" : "0");
17498
- } catch (_) {
17499
- }
17500
- };
17501
- var persistPanels = (root) => {
17502
- const panels = root.querySelectorAll(
17503
- "details[data-dtm-acc]"
17504
- );
17505
- for (const panel of panels) {
17506
- const key = panel.dataset.dtmAcc;
17507
- if (!key) continue;
17508
- const stored = readPanelOpen(key);
17509
- if (stored !== null) panel.open = stored;
17510
- panel.addEventListener("toggle", () => {
17511
- writePanelOpen(key, panel.open);
17512
- });
17571
+ // src/instrument-presets.ts
17572
+ var INSTRUMENT_PRESETS = {
17573
+ // --- STANDARD: 汎用性と完成度重視 ---
17574
+ piano: {
17575
+ displayName: "\u30B0\u30E9\u30F3\u30C9\u30D4\u30A2\u30CE",
17576
+ 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",
17577
+ melody: "Acoustic Grand Piano",
17578
+ submelody: "Vibraphone",
17579
+ bass: "Electric Bass (finger)",
17580
+ chord: "Pad 2 (warm)",
17581
+ solo: "Electric Guitar (clean)",
17582
+ // **Glockenspiel は使わない。音源側のサンプルが音程を外している**(実聴で確認)。
17583
+ // 音域の問題(G5〜の高音楽器)なら {@link GM_BRIGHT_CEILING} で下げれば済むが、
17584
+ // ピッチそのものがずれているものは置き場所を変えても直らない。同じ役割
17585
+ // (サビの上に乗る明るい音板)で、素直に鳴る Celesta へ差し替えてある。
17586
+ // {@link GM_INSTRUMENT_RANGE} / {@link GM_BRIGHT_CEILING} の Glockenspiel の
17587
+ // 項は残す。手で選んだときの置き場所の判断はそのまま要るため。
17588
+ chorusLead: "Celesta",
17589
+ chordAlt: "Electric Piano 1",
17590
+ sparkle: "Music Box",
17591
+ bassAlt: "Acoustic Bass",
17592
+ harmonyAlt: "Choir Aahs"
17593
+ },
17594
+ acoustic: {
17595
+ displayName: "\u30A2\u30B3\u30FC\u30B9\u30C6\u30A3\u30C3\u30AF",
17596
+ description: "\u751F\u697D\u5668\u306E\u6E29\u304B\u307F\u3092\u91CD\u8996\u3002\u30D5\u30A9\u30FC\u30AF\u3084\u30DD\u30C3\u30D7\u30B9\u306B\u3002",
17597
+ melody: "Acoustic Guitar (steel)",
17598
+ submelody: "Harmonica",
17599
+ bass: "Acoustic Bass",
17600
+ chord: "Acoustic Guitar (nylon)",
17601
+ solo: "Overdriven Guitar",
17602
+ chorusLead: "String Ensemble 1",
17603
+ chordAlt: "Acoustic Grand Piano",
17604
+ sparkle: "Celesta",
17605
+ bassAlt: "Electric Bass (finger)",
17606
+ harmonyAlt: "Choir Aahs"
17607
+ },
17608
+ jazz_night: {
17609
+ displayName: "\u30B8\u30E3\u30BA\u30FB\u30CA\u30A4\u30C8",
17610
+ 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",
17611
+ melody: "Electric Piano 1",
17612
+ submelody: "Flute",
17613
+ bass: "Acoustic Bass",
17614
+ chord: "Electric Guitar (jazz)",
17615
+ solo: "Tenor Sax",
17616
+ chorusLead: "Muted Trumpet",
17617
+ chordAlt: "Vibraphone",
17618
+ sparkle: "Celesta",
17619
+ bassAlt: "Electric Bass (finger)",
17620
+ harmonyAlt: "Choir Aahs"
17621
+ },
17622
+ // --- MODERN & VIBE: エッジの効いた現代的な響き ---
17623
+ synth_pop: {
17624
+ displayName: "\u30B7\u30F3\u30BB\u30DD\u30C3\u30D7",
17625
+ description: "80s\u301C\u73FE\u4EE3\u307E\u3067\u3002\u629C\u3051\u308B\u30EA\u30FC\u30C9\u3068\u592A\u3044\u30D9\u30FC\u30B9\u306E\u738B\u9053\u3002",
17626
+ melody: "Lead 2 (sawtooth)",
17627
+ submelody: "Lead 4 (chiff)",
17628
+ bass: "Synth Bass 2",
17629
+ chord: "Pad 3 (polysynth)",
17630
+ solo: "Distortion Guitar",
17631
+ chorusLead: "Synth Brass 1",
17632
+ chordAlt: "Electric Piano 2",
17633
+ sparkle: "FX 3 (crystal)",
17634
+ bassAlt: "Synth Bass 1",
17635
+ harmonyAlt: "Synth Choir"
17636
+ },
17637
+ cyber_punk: {
17638
+ displayName: "\u30B5\u30A4\u30D0\u30FC\u30D1\u30F3\u30AF",
17639
+ 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",
17640
+ melody: "Lead 8 (bass + lead)",
17641
+ submelody: "Lead 5 (charang)",
17642
+ bass: "Synth Bass 2",
17643
+ chord: "Pad 8 (sweep)",
17644
+ solo: "Distortion Guitar",
17645
+ chorusLead: "Lead 7 (fifths)",
17646
+ chordAlt: "Pad 4 (choir)",
17647
+ sparkle: "FX 3 (crystal)",
17648
+ bassAlt: "Synth Bass 1",
17649
+ harmonyAlt: "Synth Choir"
17650
+ },
17651
+ rock: {
17652
+ displayName: "\u30CF\u30FC\u30C9\u30ED\u30C3\u30AF",
17653
+ description: "\u6B6A\u307F\u30AE\u30BF\u30FC\u3068\u91CD\u539A\u306A\u30D9\u30FC\u30B9\u3067\u3001\u30D1\u30EF\u30FC\u3092\u524D\u9762\u306B\u3002",
17654
+ melody: "Distortion Guitar",
17655
+ submelody: "Rock Organ",
17656
+ bass: "Electric Bass (pick)",
17657
+ chord: "Overdriven Guitar",
17658
+ solo: "Distortion Guitar",
17659
+ chorusLead: "Brass Section",
17660
+ chordAlt: "Electric Guitar (clean)",
17661
+ sparkle: "Electric Guitar (muted)",
17662
+ bassAlt: "Electric Bass (finger)",
17663
+ harmonyAlt: "Choir Aahs"
17664
+ },
17665
+ // --- WORLD & CLASSIC: 特定のジャンル・地域 ---
17666
+ orchestra: {
17667
+ displayName: "\u30AA\u30FC\u30B1\u30B9\u30C8\u30E9",
17668
+ description: "\u58EE\u5927\u306A\u7269\u8A9E\u3092\u4E88\u611F\u3055\u305B\u308B\u3001\u7BA1\u5F26\u697D\u5668\u306E\u91CD\u539A\u306A\u97FF\u304D\u3002",
17669
+ melody: "French Horn",
17670
+ submelody: "Pizzicato Strings",
17671
+ bass: "Cello",
17672
+ chord: "Tremolo Strings",
17673
+ solo: "Violin",
17674
+ chorusLead: "Trumpet",
17675
+ chordAlt: "String Ensemble 1",
17676
+ sparkle: "Orchestral Harp",
17677
+ bassAlt: "Contrabass",
17678
+ harmonyAlt: "Choir Aahs"
17679
+ },
17680
+ japanese_wa: {
17681
+ displayName: "\u548C\u98A8\u30FB\u96C5",
17682
+ description: "\u7434\u3068\u4E09\u5473\u7DDA\u306E\u7E4A\u7D30\u306A\u8ABF\u3079\u306B\u3001\u5C3A\u516B\u306E\u60C5\u7DD2\u3092\u6DFB\u3048\u3066\u3002",
17683
+ melody: "Koto",
17684
+ submelody: "Shamisen",
17685
+ bass: "Taiko Drum",
17686
+ chord: "Shakuhachi",
17687
+ solo: "Shakuhachi",
17688
+ // piano プリセットと同じ理由で Glockenspiel を避ける(音源のピッチずれ)。
17689
+ chorusLead: "Celesta",
17690
+ chordAlt: "Kalimba",
17691
+ sparkle: "Music Box",
17692
+ bassAlt: "Acoustic Bass",
17693
+ harmonyAlt: "Choir Aahs"
17694
+ },
17695
+ arabic_exotic: {
17696
+ displayName: "\u30A8\u30AD\u30BE\u30C1\u30C3\u30AF",
17697
+ 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",
17698
+ melody: "Sitar",
17699
+ submelody: "Bagpipe",
17700
+ bass: "Fretless Bass",
17701
+ chord: "Kalimba",
17702
+ solo: "Shanai",
17703
+ chorusLead: "Steel Drums",
17704
+ chordAlt: "Orchestral Harp",
17705
+ sparkle: "Tinkle Bell",
17706
+ bassAlt: "Acoustic Bass",
17707
+ harmonyAlt: "Choir Aahs"
17708
+ },
17709
+ // --- FANTASY & ATMOSPHERE: 雰囲気と余韻 ---
17710
+ fantasy_rpg: {
17711
+ displayName: "\u30D5\u30A1\u30F3\u30BF\u30B8\u30FCRPG",
17712
+ description: "\u30AA\u30AB\u30EA\u30CA\u3068\u30CF\u30FC\u30D7\u304C\u7D21\u3050\u3001\u5192\u967A\u3068\u9B54\u6CD5\u306E\u4E16\u754C\u89B3\u3002",
17713
+ melody: "Ocarina",
17714
+ submelody: "Celesta",
17715
+ bass: "Timpani",
17716
+ chord: "Orchestral Harp",
17717
+ solo: "Pan Flute",
17718
+ chorusLead: "Choir Aahs",
17719
+ chordAlt: "String Ensemble 2",
17720
+ sparkle: "Tinkle Bell",
17721
+ bassAlt: "Contrabass",
17722
+ harmonyAlt: "Choir Aahs"
17723
+ },
17724
+ ambient_cloud: {
17725
+ displayName: "\u30A2\u30F3\u30D3\u30A8\u30F3\u30C8",
17726
+ description: "\u8F2A\u90ED\u3092\u307C\u304B\u3057\u305F\u97F3\u8272\u3067\u3001\u6DF1\u3044\u6CA1\u5165\u611F\u3068\u4F59\u97FB\u3092\u6F14\u51FA\u3002",
17727
+ melody: "Lead 6 (voice)",
17728
+ submelody: "Music Box",
17729
+ bass: "Synth Bass 1",
17730
+ chord: "Pad 7 (halo)",
17731
+ solo: "Lead 3 (calliope)",
17732
+ chorusLead: "Synth Choir",
17733
+ chordAlt: "Pad 5 (bowed)",
17734
+ sparkle: "FX 3 (crystal)",
17735
+ bassAlt: "Synth Bass 2",
17736
+ harmonyAlt: "Synth Choir"
17737
+ },
17738
+ retro_game: {
17739
+ displayName: "8-bit \u30EC\u30C8\u30ED",
17740
+ 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",
17741
+ melody: "Lead 1 (square)",
17742
+ submelody: "Lead 2 (sawtooth)",
17743
+ bass: "Synth Bass 1",
17744
+ chord: "Clavinet",
17745
+ solo: "Lead 8 (bass + lead)",
17746
+ chorusLead: "Lead 4 (chiff)",
17747
+ chordAlt: "Lead 5 (charang)",
17748
+ sparkle: "Xylophone",
17749
+ bassAlt: "Synth Bass 2",
17750
+ harmonyAlt: "Lead 6 (voice)"
17513
17751
  }
17514
17752
  };
17753
+ var GM_INSTRUMENT_RANGE = {
17754
+ // 鍵盤・音板
17755
+ "Acoustic Grand Piano": [21, 108],
17756
+ "Electric Piano 1": [28, 103],
17757
+ Clavinet: [36, 96],
17758
+ Celesta: [60, 108],
17759
+ Glockenspiel: [79, 108],
17760
+ "Music Box": [72, 108],
17761
+ Vibraphone: [53, 89],
17762
+ Kalimba: [60, 84],
17763
+ "Orchestral Harp": [23, 104],
17764
+ // 弦・撥弦
17765
+ "Acoustic Guitar (steel)": [40, 83],
17766
+ "Acoustic Guitar (nylon)": [40, 83],
17767
+ "Electric Guitar (clean)": [40, 86],
17768
+ "Electric Guitar (jazz)": [40, 86],
17769
+ "Overdriven Guitar": [40, 88],
17770
+ "Distortion Guitar": [40, 88],
17771
+ Violin: [55, 103],
17772
+ Cello: [36, 76],
17773
+ "String Ensemble 1": [28, 100],
17774
+ "Tremolo Strings": [28, 100],
17775
+ "Pizzicato Strings": [28, 96],
17776
+ Sitar: [48, 79],
17777
+ Shamisen: [48, 84],
17778
+ Koto: [41, 77],
17779
+ // ベース
17780
+ "Acoustic Bass": [28, 60],
17781
+ "Electric Bass (finger)": [28, 67],
17782
+ "Electric Bass (pick)": [28, 67],
17783
+ "Fretless Bass": [28, 67],
17784
+ "Synth Bass 1": [24, 72],
17785
+ "Synth Bass 2": [24, 72],
17786
+ // 管
17787
+ Flute: [60, 96],
17788
+ "Pan Flute": [60, 91],
17789
+ Shakuhachi: [62, 86],
17790
+ Ocarina: [60, 84],
17791
+ Harmonica: [60, 96],
17792
+ Bagpipe: [62, 86],
17793
+ Shanai: [60, 86],
17794
+ "Tenor Sax": [44, 75],
17795
+ Trumpet: [55, 82],
17796
+ "Muted Trumpet": [55, 82],
17797
+ "French Horn": [41, 77],
17798
+ "Brass Section": [41, 84],
17799
+ // 声・打・オルガン
17800
+ "Choir Aahs": [43, 84],
17801
+ "Synth Choir": [43, 84],
17802
+ "Steel Drums": [55, 86],
17803
+ "Taiko Drum": [30, 60],
17804
+ Timpani: [36, 57],
17805
+ "Rock Organ": [36, 96],
17806
+ // シンセ(実物が無いので一般的な使用域)
17807
+ "Synth Brass 1": [36, 96],
17808
+ "Lead 1 (square)": [36, 96],
17809
+ "Lead 2 (sawtooth)": [36, 96],
17810
+ "Lead 3 (calliope)": [48, 96],
17811
+ "Lead 4 (chiff)": [48, 96],
17812
+ "Lead 5 (charang)": [40, 96],
17813
+ "Lead 6 (voice)": [43, 91],
17814
+ "Lead 7 (fifths)": [36, 84],
17815
+ "Lead 8 (bass + lead)": [28, 91],
17816
+ "Pad 2 (warm)": [24, 96],
17817
+ "Pad 3 (polysynth)": [24, 96],
17818
+ "Pad 7 (halo)": [24, 96],
17819
+ "Pad 8 (sweep)": [24, 96]
17820
+ };
17821
+ var GM_BRIGHT_CEILING = {
17822
+ Glockenspiel: 72,
17823
+ "Tinkle Bell": 72,
17824
+ "Music Box": 79,
17825
+ Celesta: 84,
17826
+ Kalimba: 79,
17827
+ "Steel Drums": 84,
17828
+ "FX 3 (crystal)": 79
17829
+ };
17830
+ var OCTAVE_FIT_TOLERANCE = 3;
17831
+ var fitInstrumentOctave = (semitoneRange, instrument, wanted) => {
17832
+ const range = GM_INSTRUMENT_RANGE[instrument];
17833
+ if (!range || !semitoneRange) return wanted;
17834
+ const hi = Math.min(range[1], GM_BRIGHT_CEILING[instrument] ?? range[1]);
17835
+ let octave = wanted;
17836
+ while (octave > wanted - 2) {
17837
+ if (semitoneRange[1] + octave * 12 <= hi + OCTAVE_FIT_TOLERANCE) break;
17838
+ octave--;
17839
+ }
17840
+ return octave;
17841
+ };
17515
17842
 
17516
- // src/daw-ui.ts
17517
- var q = (root, sel) => root.querySelector(sel);
17518
- var buildUI = (target, options) => {
17519
- const {
17520
- drumPatterns,
17521
- defaultDrumPattern,
17522
- defaultBpm,
17523
- showMidi,
17524
- showMidiSearch,
17525
- showCompose
17526
- } = options;
17527
- const drumOptions = [`<option value="none">\u306A\u3057</option>`].concat(
17528
- drumPatterns.map(
17529
- (p) => `<option value="${p.value}" ${p.value === defaultDrumPattern ? "selected" : ""}>${p.label}</option>`
17530
- )
17531
- ).join("");
17532
- target.innerHTML = `
17533
- <div class="dtm-daw" data-dtm="root">
17534
- <div class="dtm-topbar" data-dtm="transport">
17535
- <div class="dtm-topbar-row1">
17536
- <button class="dtm-iconbtn" data-dtm="prev-bar" title="1\u5C0F\u7BC0\u524D">${icon("chevronLeft")}</button>
17537
- <button class="dtm-play" data-dtm="play" disabled>${icon("play")}</button>
17538
- <button class="dtm-iconbtn" data-dtm="next-bar" title="1\u5C0F\u7BC0\u5F8C">${icon("chevronRight")}</button>
17539
- <label class="dtm-toggle"><input type="checkbox" data-dtm="solo"><span>\u30BD\u30ED</span></label>
17540
- <span class="dtm-topbar-loading dtm-blink" data-dtm="topbar-loading">... LOADING ...</span>
17541
- <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>
17542
- <span class="dtm-grow"></span>
17543
- <span class="dtm-label">BPM</span>
17544
- <input type="number" class="dtm-input dtm-input--num" data-dtm="bpm" value="${defaultBpm}" min="20" max="300">
17545
- </div>
17546
- <div class="dtm-tracks" data-dtm="track-tabs"></div>
17547
- </div>
17548
-
17549
- <div class="dtm-tooldock">
17550
- <div class="dtm-seg">
17551
- <button class="dtm-segbtn dtm-segbtn--active" data-dtm="tool-pen" title="\u30DA\u30F3">${icon("pen")}</button>
17552
- <button class="dtm-segbtn" data-dtm="tool-select" title="\u9078\u629E">${icon("select")}</button>
17553
- <button class="dtm-segbtn" data-dtm="tool-eraser" title="\u6D88\u3057\u30B4\u30E0">${icon("eraser")}</button>
17554
- </div>
17555
- <button class="dtm-iconbtn" data-dtm="undo" title="\u5143\u306B\u623B\u3059" disabled>${icon("undo")}</button>
17556
- <button class="dtm-iconbtn" data-dtm="redo" title="\u3084\u308A\u76F4\u3057" disabled>${icon("redo")}</button>
17557
- <select class="dtm-select dtm-grow" data-dtm="note-length" title="\u97F3\u7B26\u306E\u9577\u3055">
17558
- <option value="48">4\u5206</option>
17559
- <option value="32">3\u90234</option>
17560
- <option value="24">8\u5206</option>
17561
- <option value="16">3\u90238</option>
17562
- <option value="12" selected>16\u5206</option>
17563
- <option value="8">3\u902316</option>
17564
- <option value="6">32\u5206</option>
17565
- <option value="4">3\u902332</option>
17566
- </select>
17567
- </div>
17843
+ // src/advanced-layers.ts
17844
+ var buildAdvancedLayers = (song, config) => {
17845
+ const { edo, stepsPerBar, preset } = config;
17846
+ const semitoneRange = (notes) => {
17847
+ if (notes.length === 0) return null;
17848
+ let lo = Number.POSITIVE_INFINITY;
17849
+ let hi = Number.NEGATIVE_INFINITY;
17850
+ for (const n of notes) {
17851
+ const semi = n.pitchUnits / UNITS_PER_SEMITONE;
17852
+ if (semi < lo) lo = semi;
17853
+ if (semi > hi) hi = semi;
17854
+ }
17855
+ return [Math.round(lo), Math.round(hi)];
17856
+ };
17857
+ const fit = (notes, slot, wanted) => fitInstrumentOctave(semitoneRange(notes), preset[slot], wanted);
17858
+ const kindAtBar = (bar) => song.sections.find(
17859
+ (sec) => bar >= sec.startBar && bar < sec.startBar + sec.bars
17860
+ )?.kind ?? null;
17861
+ const onlyIn = (notes, kinds) => {
17862
+ if (!kinds) return notes;
17863
+ const want = new Set(kinds);
17864
+ return notes.filter((n) => {
17865
+ const kind = kindAtBar(Math.floor(n.startStep / stepsPerBar));
17866
+ return kind !== null && want.has(kind);
17867
+ });
17868
+ };
17869
+ const chordNotes = (pattern, velocityShift = 0) => buildChordPlacements({
17870
+ edo,
17871
+ chordStr: song.chordProgression,
17872
+ patternType: pattern,
17873
+ rootShift: song.rootShift,
17874
+ bpm: song.bpm,
17875
+ stepsPerBar
17876
+ }).map((p) => ({
17877
+ startStep: p.startStep,
17878
+ pitchUnits: p.pitchUnits,
17879
+ durationSteps: p.durationSteps,
17880
+ velocity: Math.max(30, p.velocity + velocityShift)
17881
+ }));
17882
+ const plan = song.arrange;
17883
+ const kindsPresent = [...new Set(song.sections.map((sec) => sec.kind))];
17884
+ const LOUD = ["chorus", "drop_chorus"];
17885
+ const loudKinds = kindsPresent.filter((k) => LOUD.includes(k));
17886
+ const quietKinds = kindsPresent.filter((k) => !LOUD.includes(k));
17887
+ const splittable = loudKinds.length > 0 && quietKinds.length > 0;
17888
+ const leadNotes = plan.lead ? onlyIn(song.melody, plan.lead.sections) : [];
17889
+ const leadOctave = fit(leadNotes, "chorusLead", plan.lead?.octave ?? 0);
17890
+ const leadLayer = {
17891
+ index: 1,
17892
+ notes: leadNotes,
17893
+ octave: leadOctave,
17894
+ // ユニゾンで重ねるときは、オクターブ上より前に出やすいので少し引く。
17895
+ volume: leadOctave === 0 ? 54 : 62,
17896
+ slot: "chorusLead"
17897
+ };
17898
+ const bassNotes = plan.bassLayer ? onlyIn(song.bass, plan.bassLayer.sections) : [];
17899
+ const bassOctave = fit(bassNotes, "bass", plan.bassLayer?.octave ?? 0);
17900
+ const wantBassLayer = plan.bassLayer !== null && bassOctave !== 0;
17901
+ const bassLayer = wantBassLayer ? {
17902
+ index: 5,
17903
+ notes: bassNotes,
17904
+ octave: bassOctave,
17905
+ volume: 58,
17906
+ slot: "bass"
17907
+ } : {
17908
+ index: 5,
17909
+ notes: splittable ? onlyIn(song.bass, loudKinds) : [],
17910
+ octave: 0,
17911
+ volume: 92,
17912
+ slot: "bassAlt"
17913
+ };
17914
+ const padNotes = onlyIn(song.pad, plan.padSections);
17915
+ const layers = [
17916
+ { index: 0, notes: song.melody, octave: 0, volume: 104, slot: "melody" },
17917
+ leadLayer,
17918
+ // t2 ハモリ。**セクションで分割しない。**
17919
+ // 一度 t12 と loud/quiet で分けてみたが、ハモリは音の87%が盛り上がる側にあり、
17920
+ // 71%の曲は静かな側に1音も無い——分割すると t2 が空になって中身が t12 へ移る。
17921
+ // 「トラック番号と役割の対応は固定」(ピアノロールのどこに何があるかが曲ごとに
17922
+ // 動くとユーザーが編集できない)という原則にも反するので、ここは丸ごと持つ。
17923
+ { index: 2, notes: song.harmony, octave: 0, volume: 82 },
17924
+ // t3 サブメロ。t14 が間奏ソロを持たないとき、そちらへ盛り上がる側を渡して
17925
+ // こちらは静かな側だけ持つ。対旋律・合いの手がセクションで音色替えする。
17926
+ {
17927
+ index: 3,
17928
+ notes: song.solo.length === 0 && splittable ? onlyIn(song.submelody, quietKinds) : song.submelody,
17929
+ octave: 0,
17930
+ volume: 86,
17931
+ slot: "submelody"
17932
+ },
17933
+ // t4。t5 が `bassAlt` で盛り上がる側を持つときは、こちらは静かな側だけ持つ
17934
+ // (分割であって間引きではない——2本合わせて元のベースが漏れなく鳴る)。
17935
+ {
17936
+ index: 4,
17937
+ notes: !wantBassLayer && splittable ? onlyIn(song.bass, quietKinds) : song.bass,
17938
+ octave: 0,
17939
+ volume: 92,
17940
+ slot: "bass"
17941
+ },
17942
+ bassLayer,
17943
+ // **パッドは伴奏用の楽器で、伴奏より1.5オクターブ高いところを鳴らす。**
17944
+ // そのまま置くとナイロンギターで10半音、カリンバで9半音、尺八で7半音ぶん
17945
+ // 音域を突き抜ける。楽器に合わせて下げる。
17946
+ {
17947
+ index: 6,
17948
+ notes: padNotes,
17949
+ octave: fit(padNotes, "chord", 0),
17950
+ volume: 64,
17951
+ slot: "chord"
17952
+ }
17953
+ ];
17954
+ const backingVolumes = [62, 54, 50];
17955
+ for (let i2 = 0; i2 < 3; i2++) {
17956
+ const layer = plan.backing[i2];
17957
+ layers.push({
17958
+ index: 7 + i2,
17959
+ notes: layer ? onlyIn(chordNotes(layer.pattern), layer.sections) : [],
17960
+ octave: layer?.octave ?? 0,
17961
+ volume: backingVolumes[i2],
17962
+ slot: i2 % 2 === 0 ? "chordAlt" : "chord"
17963
+ });
17964
+ }
17965
+ const sparkleNotes = plan.sparkle ? onlyIn(chordNotes(plan.sparkle.pattern, -14), plan.sparkle.sections) : [];
17966
+ layers.push(
17967
+ {
17968
+ index: 10,
17969
+ notes: sparkleNotes,
17970
+ // 装飾は伴奏と別の音色にする(以前はここも `chord` の使い回しだった)。
17971
+ octave: fit(sparkleNotes, "sparkle", plan.sparkle?.octave ?? 0),
17972
+ volume: 56,
17973
+ slot: "sparkle"
17974
+ },
17975
+ // 掛け合い(デュエット)の相手。**歌入り作曲のときだけ**中身が入る。
17976
+ { index: 11, notes: [], octave: 0, volume: 104, slot: "melody" },
17977
+ // 2声目のハモリ(主旋律を上下から挟む3声)と、主旋律のオクターブ下の重ね。
17978
+ // どちらも曲ごとに出るかどうかが決まる(`song.vocal`)。
17979
+ // t12。3声目のハモリは曲ごとに出るかが決まり、実測28%。
17980
+ //
17981
+ // 出ない曲では**パッド(t6)が鳴っていないセクション**を、別の音色
17982
+ // (`harmonyAlt` =声もの・柔らかい持続音)で受け持つ。パッドはセクション種別の
17983
+ // 39%しか覆っておらず補集合は常に存在するので、ここは**いま何も鳴っていない
17984
+ // 場所に持続音を足す**ことになる——重ねでも写しでもなく、純粋な追加。
17985
+ song.harmony2.length > 0 ? { index: 12, notes: song.harmony2, octave: 0, volume: 74 } : (() => {
17986
+ const rest = plan.padSections ? kindsPresent.filter((k) => !plan.padSections?.includes(k)) : [];
17987
+ const notes = rest.length > 0 ? onlyIn(song.pad, rest) : [];
17988
+ return {
17989
+ index: 12,
17990
+ notes,
17991
+ octave: fit(notes, "harmonyAlt", 0),
17992
+ volume: 60,
17993
+ slot: "harmonyAlt"
17994
+ };
17995
+ })(),
17996
+ // t13。既定は**主旋律のオクターブ下の重ね**で、歌入り作曲ではここに歌詞が付く
17997
+ // (オクターブ下でハモる歌手。単なる写しではない)。
17998
+ //
17999
+ // ただしこの層は曲ごとに出るかどうかが決まり、実測で**18%しか鳴らない**。
18000
+ // 残り82%はトラックが1本まるごと遊ぶ。オクターブ等価の写しに常設の価値は
18001
+ // 無いが、**空けておく価値はもっと無い**ので、層が無い曲では
18002
+ // 「t1(サビ重ね)が担当しないセクションの主旋律を、別の楽器でなぞる」層に回す。
18003
+ // t1 は CHORUS/LOUD 側を持つので、こちらは静かな側を持つ——結果として
18004
+ // **セクションの境目で主旋律に付く音色が入れ替わる**。1トラック1楽器の制約下で
18005
+ // 「パートごとに楽器が変わる」を作れるのは、この書き分けだけ。
18006
+ song.octave.length > 0 ? {
18007
+ index: 13,
18008
+ notes: song.octave,
18009
+ octave: -1,
18010
+ volume: 56,
18011
+ slot: "melody"
18012
+ } : (() => {
18013
+ const leadKinds = new Set(plan.lead?.sections ?? []);
18014
+ const quiet = song.sections.filter((sec) => sec.spec.melody && !leadKinds.has(sec.kind)).map((sec) => sec.kind);
18015
+ const notes = quiet.length > 0 ? onlyIn(song.melody, quiet) : [];
18016
+ const slot = ["solo", "chorusLead", "submelody", "chordAlt"].find(
18017
+ (k) => preset[k] !== preset.melody && preset[k] !== preset.chord
18018
+ ) ?? "submelody";
18019
+ return {
18020
+ index: 13,
18021
+ notes,
18022
+ // ユニゾンで置く。**音色だけを変えるのが狙い**なので、
18023
+ // オクターブを動かすと「オクターブ写し」に戻ってしまう。
18024
+ octave: fit(notes, slot, 0),
18025
+ volume: 58,
18026
+ slot
18027
+ };
18028
+ })(),
18029
+ // **間奏のソロ。** 音が入るのは間奏の小節だけなので、この1本だけを
18030
+ // 別の楽器にしても他のセクションの鳴りは変わらない。歌の音域をそのまま
18031
+ // 渡すと管楽器が上へ抜ける(テナーサックスで10半音)ので、ここも合わせる。
18032
+ // t14。**間奏のソロ。** ただし間奏は既定の構成(`DEFAULT_SECTIONS`)に無く、
18033
+ // テンプレートも `jpop_standard` しか含まないので、実測で**0%**——設計上の
18034
+ // 出番が既定では一度も来ないトラックだった。
18035
+ // 間奏が無い曲では、サブメロの盛り上がる側を `solo` の音色で受け持つ
18036
+ // (t3 は静かな側。分割なので音数は増えない)。
18037
+ song.solo.length > 0 ? {
18038
+ index: 14,
18039
+ notes: song.solo,
18040
+ octave: fit(song.solo, "solo", 0),
18041
+ volume: 100,
18042
+ slot: "solo"
18043
+ } : (() => {
18044
+ const notes = splittable ? onlyIn(song.submelody, loudKinds) : [];
18045
+ return {
18046
+ index: 14,
18047
+ notes,
18048
+ octave: fit(notes, "solo", 0),
18049
+ volume: 86,
18050
+ slot: "solo"
18051
+ };
18052
+ })()
18053
+ );
18054
+ return layers;
18055
+ };
18056
+
18057
+ // src/delay.ts
18058
+ var DELAY_DIVISIONS = [
18059
+ { value: "4", label: "4\u5206", beats: 1 },
18060
+ { value: "8", label: "8\u5206", beats: 0.5 },
18061
+ { value: "8d", label: "\u4ED8\u70B98\u5206", beats: 0.75 },
18062
+ { value: "16", label: "16\u5206", beats: 0.25 }
18063
+ ];
18064
+ var clamp4 = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
18065
+ var divisionToSeconds = (division, bpm) => {
18066
+ const beats = DELAY_DIVISIONS.find((d) => d.value === division)?.beats ?? 0.5;
18067
+ const safeBpm = bpm > 0 ? bpm : 120;
18068
+ return 60 / safeBpm * beats;
18069
+ };
18070
+ var DELAY_MAX_WET = 0.45;
18071
+ var delayAmountToGain = (amount) => clamp4(amount, 0, 100) / 100 * DELAY_MAX_WET;
18072
+ var FEEDBACK_GAIN = 0.3;
18073
+ var MAX_DELAY_SEC = 2;
18074
+ var createDelayBus = (ctx, destination, options = {}) => {
18075
+ const input = ctx.createGain();
18076
+ const delayNode = ctx.createDelay(MAX_DELAY_SEC);
18077
+ const feedback = ctx.createGain();
18078
+ feedback.gain.value = FEEDBACK_GAIN;
18079
+ const wetGain = ctx.createGain();
18080
+ wetGain.gain.value = delayAmountToGain(options.amount ?? 0);
18081
+ input.connect(delayNode);
18082
+ delayNode.connect(feedback);
18083
+ feedback.connect(delayNode);
18084
+ delayNode.connect(wetGain);
18085
+ wetGain.connect(destination);
18086
+ let bpm = options.bpm ?? 120;
18087
+ let division = options.division ?? "8";
18088
+ const applyTime = () => {
18089
+ delayNode.delayTime.setTargetAtTime(
18090
+ divisionToSeconds(division, bpm),
18091
+ ctx.currentTime,
18092
+ 0.05
18093
+ );
18094
+ };
18095
+ applyTime();
18096
+ return {
18097
+ input,
18098
+ setAmount: (amount) => {
18099
+ wetGain.gain.setTargetAtTime(
18100
+ delayAmountToGain(amount),
18101
+ ctx.currentTime,
18102
+ 0.02
18103
+ );
18104
+ },
18105
+ setDivision: (d) => {
18106
+ division = d;
18107
+ applyTime();
18108
+ },
18109
+ setBpm: (b) => {
18110
+ bpm = b;
18111
+ applyTime();
18112
+ },
18113
+ dispose: () => {
18114
+ input.disconnect();
18115
+ delayNode.disconnect();
18116
+ feedback.disconnect();
18117
+ wetGain.disconnect();
18118
+ }
18119
+ };
18120
+ };
18121
+
18122
+ // src/panel-state.ts
18123
+ var STORAGE_PREFIX = "dtm-panel-open:";
18124
+ var readPanelOpen = (key) => {
18125
+ try {
18126
+ if (typeof localStorage === "undefined" || !localStorage) return null;
18127
+ const raw = localStorage.getItem(STORAGE_PREFIX + key);
18128
+ if (raw === "1") return true;
18129
+ if (raw === "0") return false;
18130
+ } catch (_) {
18131
+ }
18132
+ return null;
18133
+ };
18134
+ var writePanelOpen = (key, open) => {
18135
+ try {
18136
+ if (typeof localStorage === "undefined" || !localStorage) return;
18137
+ localStorage.setItem(STORAGE_PREFIX + key, open ? "1" : "0");
18138
+ } catch (_) {
18139
+ }
18140
+ };
18141
+ var persistPanels = (root) => {
18142
+ const panels = root.querySelectorAll(
18143
+ "details[data-dtm-acc]"
18144
+ );
18145
+ for (const panel of panels) {
18146
+ const key = panel.dataset.dtmAcc;
18147
+ if (!key) continue;
18148
+ const stored = readPanelOpen(key);
18149
+ if (stored !== null) panel.open = stored;
18150
+ panel.addEventListener("toggle", () => {
18151
+ writePanelOpen(key, panel.open);
18152
+ });
18153
+ }
18154
+ };
18155
+
18156
+ // src/daw-ui.ts
18157
+ var q = (root, sel) => root.querySelector(sel);
18158
+ var buildUI = (target, options) => {
18159
+ const {
18160
+ drumPatterns,
18161
+ defaultDrumPattern,
18162
+ defaultBpm,
18163
+ showMidi,
18164
+ showMidiSearch,
18165
+ showCompose
18166
+ } = options;
18167
+ const drumOptions = [`<option value="none">\u306A\u3057</option>`].concat(
18168
+ drumPatterns.map(
18169
+ (p) => `<option value="${p.value}" ${p.value === defaultDrumPattern ? "selected" : ""}>${p.label}</option>`
18170
+ )
18171
+ ).join("");
18172
+ target.innerHTML = `
18173
+ <div class="dtm-daw" data-dtm="root">
18174
+ <div class="dtm-topbar" data-dtm="transport">
18175
+ <div class="dtm-topbar-row1">
18176
+ <button class="dtm-iconbtn" data-dtm="prev-bar" title="1\u5C0F\u7BC0\u524D">${icon("chevronLeft")}</button>
18177
+ <button class="dtm-play" data-dtm="play" disabled>${icon("play")}</button>
18178
+ <button class="dtm-iconbtn" data-dtm="next-bar" title="1\u5C0F\u7BC0\u5F8C">${icon("chevronRight")}</button>
18179
+ <label class="dtm-toggle"><input type="checkbox" data-dtm="solo"><span>\u30BD\u30ED</span></label>
18180
+ <span class="dtm-topbar-loading dtm-blink" data-dtm="topbar-loading">... LOADING ...</span>
18181
+ <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>
18182
+ <span class="dtm-grow"></span>
18183
+ <span class="dtm-label">BPM</span>
18184
+ <input type="number" class="dtm-input dtm-input--num" data-dtm="bpm" value="${defaultBpm}" min="20" max="300">
18185
+ </div>
18186
+ <div class="dtm-tracks" data-dtm="track-tabs"></div>
18187
+ </div>
18188
+
18189
+ <div class="dtm-tooldock">
18190
+ <div class="dtm-seg">
18191
+ <button class="dtm-segbtn dtm-segbtn--active" data-dtm="tool-pen" title="\u30DA\u30F3">${icon("pen")}</button>
18192
+ <button class="dtm-segbtn" data-dtm="tool-select" title="\u9078\u629E">${icon("select")}</button>
18193
+ <button class="dtm-segbtn" data-dtm="tool-eraser" title="\u6D88\u3057\u30B4\u30E0">${icon("eraser")}</button>
18194
+ </div>
18195
+ <button class="dtm-iconbtn" data-dtm="undo" title="\u5143\u306B\u623B\u3059" disabled>${icon("undo")}</button>
18196
+ <button class="dtm-iconbtn" data-dtm="redo" title="\u3084\u308A\u76F4\u3057" disabled>${icon("redo")}</button>
18197
+ <select class="dtm-select dtm-grow" data-dtm="note-length" title="\u97F3\u7B26\u306E\u9577\u3055">
18198
+ <option value="48">4\u5206</option>
18199
+ <option value="32">3\u90234</option>
18200
+ <option value="24">8\u5206</option>
18201
+ <option value="16">3\u90238</option>
18202
+ <option value="12" selected>16\u5206</option>
18203
+ <option value="8">3\u902316</option>
18204
+ <option value="6">32\u5206</option>
18205
+ <option value="4">3\u902332</option>
18206
+ </select>
18207
+ </div>
17568
18208
 
17569
18209
  <div class="dtm-roll-wrap">
17570
18210
  <div class="dtm-roll" data-dtm="roll">
@@ -17969,355 +18609,135 @@ var buildUI = (target, options) => {
17969
18609
  <div class="dtm-modal-header">
17970
18610
  <span class="dtm-modal-title" data-dtm="modal-title"></span>
17971
18611
  <button class="dtm-modal-close" data-dtm="modal-close">&times;</button>
17972
- </div>
17973
- <div class="dtm-modal-body" data-dtm="modal-body"></div>
17974
- </div>
17975
- </div>
17976
-
17977
- </div>`;
17978
- const root = q(target, '[data-dtm="root"]');
17979
- persistPanels(root);
17980
- const sel = (name) => q(root, `[data-dtm="${name}"]`);
17981
- return {
17982
- root,
17983
- topbar: sel("transport"),
17984
- topbarLoading: sel("topbar-loading"),
17985
- playBtn: sel("play"),
17986
- prevBarBtn: sel("prev-bar"),
17987
- nextBarBtn: sel("next-bar"),
17988
- soloCheckbox: sel("solo"),
17989
- clipBadge: sel("clip-badge"),
17990
- toolPen: sel("tool-pen"),
17991
- toolSelect: sel("tool-select"),
17992
- toolEraser: sel("tool-eraser"),
17993
- undoBtn: sel("undo"),
17994
- redoBtn: sel("redo"),
17995
- noteLengthSelect: sel("note-length"),
17996
- bpmInput: sel("bpm"),
17997
- zoomXLabel: sel("zoomx-label"),
17998
- zoomYLabel: sel("zoomy-label"),
17999
- zoomXIn: sel("zoomx-in"),
18000
- zoomXOut: sel("zoomx-out"),
18001
- zoomYIn: sel("zoomy-in"),
18002
- zoomYOut: sel("zoomy-out"),
18003
- bgFileInput: sel("bg-file-input"),
18004
- bgUploadBtn: sel("bg-upload"),
18005
- bgRemoveBtn: sel("bg-remove"),
18006
- bgOpacityInput: sel("bg-opacity"),
18007
- bgOpacityRow: sel("bg-opacity-row"),
18008
- rollContainer: sel("roll"),
18009
- wrapper: sel("wrapper"),
18010
- vScroll: sel("vscroll"),
18011
- vScrollThumb: sel("vscroll-thumb"),
18012
- hScroll: sel("hscroll"),
18013
- hScrollThumb: sel("hscroll-thumb"),
18014
- loopToggle: sel("loop-toggle"),
18015
- loopToggleLabel: sel("loop-toggle-label"),
18016
- loopInfoBtn: sel("loop-info"),
18017
- masterVolume: sel("master-volume"),
18018
- masterVolumeLabel: sel("master-volume-label"),
18019
- masterComp: sel("master-comp"),
18020
- masterCompLabel: sel("master-comp-label"),
18021
- masterCompInfoBtn: sel("master-comp-info"),
18022
- reverbAmount: sel("reverb-amount"),
18023
- reverbAmountLabel: sel("reverb-amount-label"),
18024
- reverbAmountInfoBtn: sel("reverb-amount-info"),
18025
- reverbDecay: sel("reverb-decay"),
18026
- reverbDecayLabel: sel("reverb-decay-label"),
18027
- reverbPreDelay: sel("reverb-predelay"),
18028
- reverbPreDelayLabel: sel("reverb-predelay-label"),
18029
- delayAmount: sel("delay-amount"),
18030
- delayAmountLabel: sel("delay-amount-label"),
18031
- delayAmountInfoBtn: sel("delay-amount-info"),
18032
- delayDivision: sel("delay-division"),
18033
- fadeIn: sel("fade-in"),
18034
- fadeInLabel: sel("fade-in-label"),
18035
- fadeOut: sel("fade-out"),
18036
- fadeOutLabel: sel("fade-out-label"),
18037
- fadeInfoBtn: sel("fade-info"),
18038
- autoMasterBtn: sel("auto-master"),
18039
- autoMasterInfoBtn: sel("auto-master-info"),
18040
- trackTabs: sel("track-tabs"),
18041
- trackBody: sel("track-body"),
18042
- drumSelect: sel("drum-select"),
18043
- drumFontSelect: sel("drum-font-select"),
18044
- drumVolume: sel("drum-volume"),
18045
- drumVolumeLabel: sel("drum-volume-label"),
18046
- midiInput: sel("midi-input"),
18047
- midiLoadBtn: sel("midi-load"),
18048
- midiInfoBtn: sel("midi-info"),
18049
- midiTrackSelection: sel("midi-track-selection"),
18050
- midiPanel: sel("midi-panel"),
18051
- midiSearchOpenBtn: sel("midi-search-open"),
18052
- mmlInput: sel("mml-input"),
18053
- mmlLoadBtn: sel("mml-load"),
18054
- mmlLoadNote: sel("mml-load-note"),
18055
- applyActiveOnly: sel("apply-active-only"),
18056
- shiftSelect: sel("shift-select"),
18057
- shiftApplyBtn: sel("shift-apply"),
18058
- transposeSelect: sel("transpose-select"),
18059
- transposeApplyBtn: sel("transpose-apply"),
18060
- transposeInfoBtn: sel("transpose-info"),
18061
- macroCompose: sel("macro-compose"),
18062
- composeTemplate: sel("compose-template"),
18063
- composeSections: sel("compose-sections"),
18064
- composeSectionsLen: sel("compose-sections-len"),
18065
- composeKey: sel("compose-key"),
18066
- composeKeyHint: sel("compose-key-hint"),
18067
- composeScale: sel("compose-scale"),
18068
- composeScaleHint: sel("compose-scale-hint"),
18069
- macroComposeVocal: sel("macro-compose-vocal"),
18070
- macroComposeInfo: sel("macro-compose-info"),
18071
- macroClear: sel("macro-clear"),
18072
- macroRandom: sel("macro-random"),
18073
- macroHarmonic: sel("macro-harmonic"),
18074
- macroMono: sel("macro-mono"),
18075
- exportMidiBtn: sel("export-midi"),
18076
- exportWavBtn: sel("export-wav"),
18077
- drumJsonExportBtn: sel("drum-json-export"),
18078
- drumJsonOutput: sel("drum-json-output"),
18079
- drumJsonStatus: sel("drum-json-status"),
18080
- drumJsonText: sel("drum-json-text"),
18081
- drumJsonCopyBtn: sel("drum-json-copy"),
18082
- generateMmlBtn: sel("generate-mml"),
18083
- decomposeChordToggle: sel("decompose-chord"),
18084
- ignoreChordHeavyToggle: sel("ignore-chord-heavy"),
18085
- barLimitSelect: sel("bar-limit"),
18086
- outputContainer: sel("output-container"),
18087
- outputStatus: sel("output-status"),
18088
- outputFull: sel("output-full"),
18089
- outputMini: sel("output-mini"),
18090
- copyFullBtn: sel("copy-full"),
18091
- copyMiniBtn: sel("copy-mini"),
18092
- overlay: sel("overlay"),
18093
- mmlInfoBtn: sel("mml-info"),
18094
- edoSelect: sel("edo-select"),
18095
- edoInfoBtn: sel("edo-info"),
18096
- modalOverlay: sel("modal-overlay"),
18097
- modalTitle: sel("modal-title"),
18098
- modalBody: sel("modal-body"),
18099
- modalClose: sel("modal-close")
18100
- };
18101
- };
18102
-
18103
- // src/instrument-presets.ts
18104
- var INSTRUMENT_PRESETS = {
18105
- // --- STANDARD: 汎用性と完成度重視 ---
18106
- piano: {
18107
- displayName: "\u30B0\u30E9\u30F3\u30C9\u30D4\u30A2\u30CE",
18108
- 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",
18109
- melody: "Acoustic Grand Piano",
18110
- submelody: "Vibraphone",
18111
- bass: "Electric Bass (finger)",
18112
- chord: "Pad 2 (warm)",
18113
- solo: "Electric Guitar (clean)",
18114
- // グロッケンは実物の音域が G5(79)〜 の高音楽器で、旋律の音域へそのまま置くと
18115
- // 金切り音になる。**外すのではなくオクターブで下げて使う**
18116
- // ({@link GM_BRIGHT_CEILING} / {@link fitInstrumentOctave})。
18117
- chorusLead: "Glockenspiel"
18118
- },
18119
- acoustic: {
18120
- displayName: "\u30A2\u30B3\u30FC\u30B9\u30C6\u30A3\u30C3\u30AF",
18121
- description: "\u751F\u697D\u5668\u306E\u6E29\u304B\u307F\u3092\u91CD\u8996\u3002\u30D5\u30A9\u30FC\u30AF\u3084\u30DD\u30C3\u30D7\u30B9\u306B\u3002",
18122
- melody: "Acoustic Guitar (steel)",
18123
- submelody: "Harmonica",
18124
- bass: "Acoustic Bass",
18125
- chord: "Acoustic Guitar (nylon)",
18126
- solo: "Overdriven Guitar",
18127
- chorusLead: "String Ensemble 1"
18128
- },
18129
- jazz_night: {
18130
- displayName: "\u30B8\u30E3\u30BA\u30FB\u30CA\u30A4\u30C8",
18131
- 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",
18132
- melody: "Electric Piano 1",
18133
- submelody: "Flute",
18134
- bass: "Acoustic Bass",
18135
- chord: "Electric Guitar (jazz)",
18136
- solo: "Tenor Sax",
18137
- chorusLead: "Muted Trumpet"
18138
- },
18139
- // --- MODERN & VIBE: エッジの効いた現代的な響き ---
18140
- synth_pop: {
18141
- displayName: "\u30B7\u30F3\u30BB\u30DD\u30C3\u30D7",
18142
- description: "80s\u301C\u73FE\u4EE3\u307E\u3067\u3002\u629C\u3051\u308B\u30EA\u30FC\u30C9\u3068\u592A\u3044\u30D9\u30FC\u30B9\u306E\u738B\u9053\u3002",
18143
- melody: "Lead 2 (sawtooth)",
18144
- submelody: "Lead 4 (chiff)",
18145
- bass: "Synth Bass 2",
18146
- chord: "Pad 3 (polysynth)",
18147
- solo: "Distortion Guitar",
18148
- chorusLead: "Synth Brass 1"
18149
- },
18150
- cyber_punk: {
18151
- displayName: "\u30B5\u30A4\u30D0\u30FC\u30D1\u30F3\u30AF",
18152
- 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",
18153
- melody: "Lead 8 (bass + lead)",
18154
- submelody: "Lead 5 (charang)",
18155
- bass: "Synth Bass 2",
18156
- chord: "Pad 8 (sweep)",
18157
- solo: "Distortion Guitar",
18158
- chorusLead: "Lead 7 (fifths)"
18159
- },
18160
- rock: {
18161
- displayName: "\u30CF\u30FC\u30C9\u30ED\u30C3\u30AF",
18162
- description: "\u6B6A\u307F\u30AE\u30BF\u30FC\u3068\u91CD\u539A\u306A\u30D9\u30FC\u30B9\u3067\u3001\u30D1\u30EF\u30FC\u3092\u524D\u9762\u306B\u3002",
18163
- melody: "Distortion Guitar",
18164
- submelody: "Rock Organ",
18165
- bass: "Electric Bass (pick)",
18166
- chord: "Overdriven Guitar",
18167
- solo: "Distortion Guitar",
18168
- chorusLead: "Brass Section"
18169
- },
18170
- // --- WORLD & CLASSIC: 特定のジャンル・地域 ---
18171
- orchestra: {
18172
- displayName: "\u30AA\u30FC\u30B1\u30B9\u30C8\u30E9",
18173
- description: "\u58EE\u5927\u306A\u7269\u8A9E\u3092\u4E88\u611F\u3055\u305B\u308B\u3001\u7BA1\u5F26\u697D\u5668\u306E\u91CD\u539A\u306A\u97FF\u304D\u3002",
18174
- melody: "French Horn",
18175
- submelody: "Pizzicato Strings",
18176
- bass: "Cello",
18177
- chord: "Tremolo Strings",
18178
- solo: "Violin",
18179
- chorusLead: "Trumpet"
18180
- },
18181
- japanese_wa: {
18182
- displayName: "\u548C\u98A8\u30FB\u96C5",
18183
- description: "\u7434\u3068\u4E09\u5473\u7DDA\u306E\u7E4A\u7D30\u306A\u8ABF\u3079\u306B\u3001\u5C3A\u516B\u306E\u60C5\u7DD2\u3092\u6DFB\u3048\u3066\u3002",
18184
- melody: "Koto",
18185
- submelody: "Shamisen",
18186
- bass: "Taiko Drum",
18187
- chord: "Shakuhachi",
18188
- solo: "Shakuhachi",
18189
- chorusLead: "Glockenspiel"
18190
- },
18191
- arabic_exotic: {
18192
- displayName: "\u30A8\u30AD\u30BE\u30C1\u30C3\u30AF",
18193
- 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",
18194
- melody: "Sitar",
18195
- submelody: "Bagpipe",
18196
- bass: "Fretless Bass",
18197
- chord: "Kalimba",
18198
- solo: "Shanai",
18199
- chorusLead: "Steel Drums"
18200
- },
18201
- // --- FANTASY & ATMOSPHERE: 雰囲気と余韻 ---
18202
- fantasy_rpg: {
18203
- displayName: "\u30D5\u30A1\u30F3\u30BF\u30B8\u30FCRPG",
18204
- description: "\u30AA\u30AB\u30EA\u30CA\u3068\u30CF\u30FC\u30D7\u304C\u7D21\u3050\u3001\u5192\u967A\u3068\u9B54\u6CD5\u306E\u4E16\u754C\u89B3\u3002",
18205
- melody: "Ocarina",
18206
- submelody: "Celesta",
18207
- bass: "Timpani",
18208
- chord: "Orchestral Harp",
18209
- solo: "Pan Flute",
18210
- chorusLead: "Choir Aahs"
18211
- },
18212
- ambient_cloud: {
18213
- displayName: "\u30A2\u30F3\u30D3\u30A8\u30F3\u30C8",
18214
- description: "\u8F2A\u90ED\u3092\u307C\u304B\u3057\u305F\u97F3\u8272\u3067\u3001\u6DF1\u3044\u6CA1\u5165\u611F\u3068\u4F59\u97FB\u3092\u6F14\u51FA\u3002",
18215
- melody: "Lead 6 (voice)",
18216
- submelody: "Music Box",
18217
- bass: "Synth Bass 1",
18218
- chord: "Pad 7 (halo)",
18219
- solo: "Lead 3 (calliope)",
18220
- chorusLead: "Synth Choir"
18221
- },
18222
- retro_game: {
18223
- displayName: "8-bit \u30EC\u30C8\u30ED",
18224
- 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",
18225
- melody: "Lead 1 (square)",
18226
- submelody: "Lead 2 (sawtooth)",
18227
- bass: "Synth Bass 1",
18228
- chord: "Clavinet",
18229
- solo: "Lead 8 (bass + lead)",
18230
- chorusLead: "Lead 4 (chiff)"
18231
- }
18232
- };
18233
- var GM_INSTRUMENT_RANGE = {
18234
- // 鍵盤・音板
18235
- "Acoustic Grand Piano": [21, 108],
18236
- "Electric Piano 1": [28, 103],
18237
- Clavinet: [36, 96],
18238
- Celesta: [60, 108],
18239
- Glockenspiel: [79, 108],
18240
- "Music Box": [72, 108],
18241
- Vibraphone: [53, 89],
18242
- Kalimba: [60, 84],
18243
- "Orchestral Harp": [23, 104],
18244
- // 弦・撥弦
18245
- "Acoustic Guitar (steel)": [40, 83],
18246
- "Acoustic Guitar (nylon)": [40, 83],
18247
- "Electric Guitar (clean)": [40, 86],
18248
- "Electric Guitar (jazz)": [40, 86],
18249
- "Overdriven Guitar": [40, 88],
18250
- "Distortion Guitar": [40, 88],
18251
- Violin: [55, 103],
18252
- Cello: [36, 76],
18253
- "String Ensemble 1": [28, 100],
18254
- "Tremolo Strings": [28, 100],
18255
- "Pizzicato Strings": [28, 96],
18256
- Sitar: [48, 79],
18257
- Shamisen: [48, 84],
18258
- Koto: [41, 77],
18259
- // ベース
18260
- "Acoustic Bass": [28, 60],
18261
- "Electric Bass (finger)": [28, 67],
18262
- "Electric Bass (pick)": [28, 67],
18263
- "Fretless Bass": [28, 67],
18264
- "Synth Bass 1": [24, 72],
18265
- "Synth Bass 2": [24, 72],
18266
- // 管
18267
- Flute: [60, 96],
18268
- "Pan Flute": [60, 91],
18269
- Shakuhachi: [62, 86],
18270
- Ocarina: [60, 84],
18271
- Harmonica: [60, 96],
18272
- Bagpipe: [62, 86],
18273
- Shanai: [60, 86],
18274
- "Tenor Sax": [44, 75],
18275
- Trumpet: [55, 82],
18276
- "Muted Trumpet": [55, 82],
18277
- "French Horn": [41, 77],
18278
- "Brass Section": [41, 84],
18279
- // 声・打・オルガン
18280
- "Choir Aahs": [43, 84],
18281
- "Synth Choir": [43, 84],
18282
- "Steel Drums": [55, 86],
18283
- "Taiko Drum": [30, 60],
18284
- Timpani: [36, 57],
18285
- "Rock Organ": [36, 96],
18286
- // シンセ(実物が無いので一般的な使用域)
18287
- "Synth Brass 1": [36, 96],
18288
- "Lead 1 (square)": [36, 96],
18289
- "Lead 2 (sawtooth)": [36, 96],
18290
- "Lead 3 (calliope)": [48, 96],
18291
- "Lead 4 (chiff)": [48, 96],
18292
- "Lead 5 (charang)": [40, 96],
18293
- "Lead 6 (voice)": [43, 91],
18294
- "Lead 7 (fifths)": [36, 84],
18295
- "Lead 8 (bass + lead)": [28, 91],
18296
- "Pad 2 (warm)": [24, 96],
18297
- "Pad 3 (polysynth)": [24, 96],
18298
- "Pad 7 (halo)": [24, 96],
18299
- "Pad 8 (sweep)": [24, 96]
18300
- };
18301
- var GM_BRIGHT_CEILING = {
18302
- Glockenspiel: 72,
18303
- "Tinkle Bell": 72,
18304
- "Music Box": 79,
18305
- Celesta: 84,
18306
- Kalimba: 79,
18307
- "Steel Drums": 84,
18308
- "FX 3 (crystal)": 79
18309
- };
18310
- var OCTAVE_FIT_TOLERANCE = 3;
18311
- var fitInstrumentOctave = (semitoneRange, instrument, wanted) => {
18312
- const range = GM_INSTRUMENT_RANGE[instrument];
18313
- if (!range || !semitoneRange) return wanted;
18314
- const hi = Math.min(range[1], GM_BRIGHT_CEILING[instrument] ?? range[1]);
18315
- let octave = wanted;
18316
- while (octave > wanted - 2) {
18317
- if (semitoneRange[1] + octave * 12 <= hi + OCTAVE_FIT_TOLERANCE) break;
18318
- octave--;
18319
- }
18320
- return octave;
18612
+ </div>
18613
+ <div class="dtm-modal-body" data-dtm="modal-body"></div>
18614
+ </div>
18615
+ </div>
18616
+
18617
+ </div>`;
18618
+ const root = q(target, '[data-dtm="root"]');
18619
+ persistPanels(root);
18620
+ const sel = (name) => q(root, `[data-dtm="${name}"]`);
18621
+ return {
18622
+ root,
18623
+ topbar: sel("transport"),
18624
+ topbarLoading: sel("topbar-loading"),
18625
+ playBtn: sel("play"),
18626
+ prevBarBtn: sel("prev-bar"),
18627
+ nextBarBtn: sel("next-bar"),
18628
+ soloCheckbox: sel("solo"),
18629
+ clipBadge: sel("clip-badge"),
18630
+ toolPen: sel("tool-pen"),
18631
+ toolSelect: sel("tool-select"),
18632
+ toolEraser: sel("tool-eraser"),
18633
+ undoBtn: sel("undo"),
18634
+ redoBtn: sel("redo"),
18635
+ noteLengthSelect: sel("note-length"),
18636
+ bpmInput: sel("bpm"),
18637
+ zoomXLabel: sel("zoomx-label"),
18638
+ zoomYLabel: sel("zoomy-label"),
18639
+ zoomXIn: sel("zoomx-in"),
18640
+ zoomXOut: sel("zoomx-out"),
18641
+ zoomYIn: sel("zoomy-in"),
18642
+ zoomYOut: sel("zoomy-out"),
18643
+ bgFileInput: sel("bg-file-input"),
18644
+ bgUploadBtn: sel("bg-upload"),
18645
+ bgRemoveBtn: sel("bg-remove"),
18646
+ bgOpacityInput: sel("bg-opacity"),
18647
+ bgOpacityRow: sel("bg-opacity-row"),
18648
+ rollContainer: sel("roll"),
18649
+ wrapper: sel("wrapper"),
18650
+ vScroll: sel("vscroll"),
18651
+ vScrollThumb: sel("vscroll-thumb"),
18652
+ hScroll: sel("hscroll"),
18653
+ hScrollThumb: sel("hscroll-thumb"),
18654
+ loopToggle: sel("loop-toggle"),
18655
+ loopToggleLabel: sel("loop-toggle-label"),
18656
+ loopInfoBtn: sel("loop-info"),
18657
+ masterVolume: sel("master-volume"),
18658
+ masterVolumeLabel: sel("master-volume-label"),
18659
+ masterComp: sel("master-comp"),
18660
+ masterCompLabel: sel("master-comp-label"),
18661
+ masterCompInfoBtn: sel("master-comp-info"),
18662
+ reverbAmount: sel("reverb-amount"),
18663
+ reverbAmountLabel: sel("reverb-amount-label"),
18664
+ reverbAmountInfoBtn: sel("reverb-amount-info"),
18665
+ reverbDecay: sel("reverb-decay"),
18666
+ reverbDecayLabel: sel("reverb-decay-label"),
18667
+ reverbPreDelay: sel("reverb-predelay"),
18668
+ reverbPreDelayLabel: sel("reverb-predelay-label"),
18669
+ delayAmount: sel("delay-amount"),
18670
+ delayAmountLabel: sel("delay-amount-label"),
18671
+ delayAmountInfoBtn: sel("delay-amount-info"),
18672
+ delayDivision: sel("delay-division"),
18673
+ fadeIn: sel("fade-in"),
18674
+ fadeInLabel: sel("fade-in-label"),
18675
+ fadeOut: sel("fade-out"),
18676
+ fadeOutLabel: sel("fade-out-label"),
18677
+ fadeInfoBtn: sel("fade-info"),
18678
+ autoMasterBtn: sel("auto-master"),
18679
+ autoMasterInfoBtn: sel("auto-master-info"),
18680
+ trackTabs: sel("track-tabs"),
18681
+ trackBody: sel("track-body"),
18682
+ drumSelect: sel("drum-select"),
18683
+ drumFontSelect: sel("drum-font-select"),
18684
+ drumVolume: sel("drum-volume"),
18685
+ drumVolumeLabel: sel("drum-volume-label"),
18686
+ midiInput: sel("midi-input"),
18687
+ midiLoadBtn: sel("midi-load"),
18688
+ midiInfoBtn: sel("midi-info"),
18689
+ midiTrackSelection: sel("midi-track-selection"),
18690
+ midiPanel: sel("midi-panel"),
18691
+ midiSearchOpenBtn: sel("midi-search-open"),
18692
+ mmlInput: sel("mml-input"),
18693
+ mmlLoadBtn: sel("mml-load"),
18694
+ mmlLoadNote: sel("mml-load-note"),
18695
+ applyActiveOnly: sel("apply-active-only"),
18696
+ shiftSelect: sel("shift-select"),
18697
+ shiftApplyBtn: sel("shift-apply"),
18698
+ transposeSelect: sel("transpose-select"),
18699
+ transposeApplyBtn: sel("transpose-apply"),
18700
+ transposeInfoBtn: sel("transpose-info"),
18701
+ macroCompose: sel("macro-compose"),
18702
+ composeTemplate: sel("compose-template"),
18703
+ composeSections: sel("compose-sections"),
18704
+ composeSectionsLen: sel("compose-sections-len"),
18705
+ composeKey: sel("compose-key"),
18706
+ composeKeyHint: sel("compose-key-hint"),
18707
+ composeScale: sel("compose-scale"),
18708
+ composeScaleHint: sel("compose-scale-hint"),
18709
+ macroComposeVocal: sel("macro-compose-vocal"),
18710
+ macroComposeInfo: sel("macro-compose-info"),
18711
+ macroClear: sel("macro-clear"),
18712
+ macroRandom: sel("macro-random"),
18713
+ macroHarmonic: sel("macro-harmonic"),
18714
+ macroMono: sel("macro-mono"),
18715
+ exportMidiBtn: sel("export-midi"),
18716
+ exportWavBtn: sel("export-wav"),
18717
+ drumJsonExportBtn: sel("drum-json-export"),
18718
+ drumJsonOutput: sel("drum-json-output"),
18719
+ drumJsonStatus: sel("drum-json-status"),
18720
+ drumJsonText: sel("drum-json-text"),
18721
+ drumJsonCopyBtn: sel("drum-json-copy"),
18722
+ generateMmlBtn: sel("generate-mml"),
18723
+ decomposeChordToggle: sel("decompose-chord"),
18724
+ ignoreChordHeavyToggle: sel("ignore-chord-heavy"),
18725
+ barLimitSelect: sel("bar-limit"),
18726
+ outputContainer: sel("output-container"),
18727
+ outputStatus: sel("output-status"),
18728
+ outputFull: sel("output-full"),
18729
+ outputMini: sel("output-mini"),
18730
+ copyFullBtn: sel("copy-full"),
18731
+ copyMiniBtn: sel("copy-mini"),
18732
+ overlay: sel("overlay"),
18733
+ mmlInfoBtn: sel("mml-info"),
18734
+ edoSelect: sel("edo-select"),
18735
+ edoInfoBtn: sel("edo-info"),
18736
+ modalOverlay: sel("modal-overlay"),
18737
+ modalTitle: sel("modal-title"),
18738
+ modalBody: sel("modal-body"),
18739
+ modalClose: sel("modal-close")
18740
+ };
18321
18741
  };
18322
18742
 
18323
18743
  // src/macro-state.ts
@@ -20782,128 +21202,6 @@ var pickComposeVocal = (exclude) => {
20782
21202
  const list = pool.length > 0 ? pool : COMPOSE_VOCAL_POOL;
20783
21203
  return list[Math.floor(Math.random() * list.length)] ?? "klatt";
20784
21204
  };
20785
- var buildAdvancedLayers = (song, config) => {
20786
- const { edo, stepsPerBar, preset } = config;
20787
- const semitoneRange = (notes) => {
20788
- if (notes.length === 0) return null;
20789
- let lo = Number.POSITIVE_INFINITY;
20790
- let hi = Number.NEGATIVE_INFINITY;
20791
- for (const n of notes) {
20792
- const semi = n.pitchUnits / UNITS_PER_SEMITONE;
20793
- if (semi < lo) lo = semi;
20794
- if (semi > hi) hi = semi;
20795
- }
20796
- return [Math.round(lo), Math.round(hi)];
20797
- };
20798
- const fit = (notes, slot, wanted) => fitInstrumentOctave(semitoneRange(notes), preset[slot], wanted);
20799
- const kindAtBar = (bar) => song.sections.find(
20800
- (sec) => bar >= sec.startBar && bar < sec.startBar + sec.bars
20801
- )?.kind ?? null;
20802
- const onlyIn = (notes, kinds) => {
20803
- if (!kinds) return notes;
20804
- const want = new Set(kinds);
20805
- return notes.filter((n) => {
20806
- const kind = kindAtBar(Math.floor(n.startStep / stepsPerBar));
20807
- return kind !== null && want.has(kind);
20808
- });
20809
- };
20810
- const chordNotes = (pattern, velocityShift = 0) => buildChordPlacements({
20811
- edo,
20812
- chordStr: song.chordProgression,
20813
- patternType: pattern,
20814
- rootShift: song.rootShift,
20815
- bpm: song.bpm,
20816
- stepsPerBar
20817
- }).map((p) => ({
20818
- startStep: p.startStep,
20819
- pitchUnits: p.pitchUnits,
20820
- durationSteps: p.durationSteps,
20821
- velocity: Math.max(30, p.velocity + velocityShift)
20822
- }));
20823
- const plan = song.arrange;
20824
- const leadNotes = plan.lead ? onlyIn(song.melody, plan.lead.sections) : [];
20825
- const leadOctave = fit(leadNotes, "chorusLead", plan.lead?.octave ?? 0);
20826
- const leadLayer = {
20827
- index: 1,
20828
- notes: leadNotes,
20829
- octave: leadOctave,
20830
- // ユニゾンで重ねるときは、オクターブ上より前に出やすいので少し引く。
20831
- volume: leadOctave === 0 ? 54 : 62,
20832
- slot: "chorusLead"
20833
- };
20834
- const bassNotes = plan.bassLayer ? onlyIn(song.bass, plan.bassLayer.sections) : [];
20835
- const bassOctave = fit(bassNotes, "bass", plan.bassLayer?.octave ?? 0);
20836
- const bassLayer = {
20837
- index: 5,
20838
- notes: bassOctave === 0 ? [] : bassNotes,
20839
- octave: bassOctave,
20840
- volume: 58,
20841
- slot: "bass"
20842
- };
20843
- const padNotes = onlyIn(song.pad, plan.padSections);
20844
- const layers = [
20845
- { index: 0, notes: song.melody, octave: 0, volume: 104, slot: "melody" },
20846
- leadLayer,
20847
- { index: 2, notes: song.harmony, octave: 0, volume: 82 },
20848
- {
20849
- index: 3,
20850
- notes: song.submelody,
20851
- octave: 0,
20852
- volume: 86,
20853
- slot: "submelody"
20854
- },
20855
- { index: 4, notes: song.bass, octave: 0, volume: 92, slot: "bass" },
20856
- bassLayer,
20857
- // **パッドは伴奏用の楽器で、伴奏より1.5オクターブ高いところを鳴らす。**
20858
- // そのまま置くとナイロンギターで10半音、カリンバで9半音、尺八で7半音ぶん
20859
- // 音域を突き抜ける。楽器に合わせて下げる。
20860
- {
20861
- index: 6,
20862
- notes: padNotes,
20863
- octave: fit(padNotes, "chord", 0),
20864
- volume: 64,
20865
- slot: "chord"
20866
- }
20867
- ];
20868
- const backingVolumes = [62, 54, 50];
20869
- for (let i2 = 0; i2 < 3; i2++) {
20870
- const layer = plan.backing[i2];
20871
- layers.push({
20872
- index: 7 + i2,
20873
- notes: layer ? onlyIn(chordNotes(layer.pattern), layer.sections) : [],
20874
- octave: layer?.octave ?? 0,
20875
- volume: backingVolumes[i2],
20876
- slot: "chord"
20877
- });
20878
- }
20879
- const sparkleNotes = plan.sparkle ? onlyIn(chordNotes(plan.sparkle.pattern, -14), plan.sparkle.sections) : [];
20880
- layers.push(
20881
- {
20882
- index: 10,
20883
- notes: sparkleNotes,
20884
- octave: fit(sparkleNotes, "chord", plan.sparkle?.octave ?? 0),
20885
- volume: 56,
20886
- slot: "chord"
20887
- },
20888
- // 掛け合い(デュエット)の相手。**歌入り作曲のときだけ**中身が入る。
20889
- { index: 11, notes: [], octave: 0, volume: 104, slot: "melody" },
20890
- // 2声目のハモリ(主旋律を上下から挟む3声)と、主旋律のオクターブ下の重ね。
20891
- // どちらも曲ごとに出るかどうかが決まる(`song.vocal`)。
20892
- { index: 12, notes: song.harmony2, octave: 0, volume: 74 },
20893
- { index: 13, notes: song.octave, octave: -1, volume: 56, slot: "melody" },
20894
- // **間奏のソロ。** 音が入るのは間奏の小節だけなので、この1本だけを
20895
- // 別の楽器にしても他のセクションの鳴りは変わらない。歌の音域をそのまま
20896
- // 渡すと管楽器が上へ抜ける(テナーサックスで10半音)ので、ここも合わせる。
20897
- {
20898
- index: 14,
20899
- notes: song.solo,
20900
- octave: fit(song.solo, "solo", 0),
20901
- volume: 100,
20902
- slot: "solo"
20903
- }
20904
- );
20905
- return layers;
20906
- };
20907
21205
  var LYRIC_MODEL_CATEGORIES = [
20908
21206
  {
20909
21207
  label: "kusa\u30D7\u30EA\u30BB\u30C3\u30C8",
@@ -26052,8 +26350,9 @@ var playSingingMML = async (mml, options = {}) => {
26052
26350
  });
26053
26351
  const ownsCtx = !options.audioContext;
26054
26352
  const ctx = options.audioContext ?? new AudioContext();
26055
- const destination = options.destination ?? ctx.destination;
26353
+ const rawDestination = options.destination ?? ctx.destination;
26056
26354
  const useSynth = options.synth ?? !options.onPlayNote;
26355
+ const destination = createSafetyLimiter(ctx, rawDestination);
26057
26356
  const synth = useSynth ? createSynth(ctx, destination) : null;
26058
26357
  const pauseWhenHidden = options.pauseWhenHidden ?? ownsCtx;
26059
26358
  let playing = false;
@@ -27059,21 +27358,29 @@ var createDtmStudio = async (options = {}) => {
27059
27358
  };
27060
27359
  applyGlueCompression(options.masterCompression ?? 0);
27061
27360
  const setMasterCompression = (amount) => applyGlueCompression(amount);
27062
- const safetyLimiter = audioCtx.createDynamicsCompressor();
27063
- safetyLimiter.threshold.value = -1;
27064
- safetyLimiter.knee.value = 0;
27065
- safetyLimiter.ratio.value = 20;
27066
- safetyLimiter.attack.value = 1e-3;
27067
- safetyLimiter.release.value = 0.1;
27068
- glueMakeup.connect(safetyLimiter);
27069
27361
  const fadeGain = audioCtx.createGain();
27070
27362
  fadeGain.gain.value = 1;
27071
- safetyLimiter.connect(fadeGain);
27072
27363
  fadeGain.connect(options.destination ?? audioCtx.destination);
27364
+ const safetyLimiter = createSafetyLimiter(audioCtx, fadeGain);
27365
+ glueMakeup.connect(safetyLimiter);
27366
+ const FADE_RESTORE_SEC = 0.02;
27367
+ const rampFadeGainToUnity = (from, at) => {
27368
+ fadeGain.gain.setValueAtTime(from, at);
27369
+ fadeGain.gain.linearRampToValueAtTime(1, at + FADE_RESTORE_SEC);
27370
+ };
27371
+ const restoreFadeGainIfMuted = () => {
27372
+ const current = fadeGain.gain.value;
27373
+ if (current >= 1) return;
27374
+ const now = audioCtx.currentTime;
27375
+ fadeGain.gain.cancelScheduledValues(now);
27376
+ rampFadeGainToUnity(current, now);
27377
+ };
27073
27378
  const scheduleFade = (params) => {
27074
- fadeGain.gain.cancelScheduledValues(audioCtx.currentTime);
27379
+ const now = audioCtx.currentTime;
27380
+ const current = fadeGain.gain.value;
27381
+ fadeGain.gain.cancelScheduledValues(now);
27075
27382
  if (!params) {
27076
- fadeGain.gain.setValueAtTime(1, audioCtx.currentTime);
27383
+ rampFadeGainToUnity(current, now);
27077
27384
  return;
27078
27385
  }
27079
27386
  const { fadeInStartAt, fadeInEndAt, fadeOutStartAt, fadeOutEndAt } = params;
@@ -27081,13 +27388,12 @@ var createDtmStudio = async (options = {}) => {
27081
27388
  fadeGain.gain.setValueAtTime(0, fadeInStartAt);
27082
27389
  fadeGain.gain.linearRampToValueAtTime(1, fadeInEndAt);
27083
27390
  } else {
27084
- fadeGain.gain.setValueAtTime(1, audioCtx.currentTime);
27391
+ rampFadeGainToUnity(current, now);
27085
27392
  }
27086
27393
  if (fadeOutStartAt !== void 0 && fadeOutEndAt !== void 0) {
27087
27394
  fadeGain.gain.setValueAtTime(1, fadeOutStartAt);
27088
27395
  fadeGain.gain.linearRampToValueAtTime(0, fadeOutEndAt);
27089
- fadeGain.gain.setValueAtTime(0, fadeOutEndAt + 2);
27090
- fadeGain.gain.setValueAtTime(1, fadeOutEndAt + 2);
27396
+ rampFadeGainToUnity(0, fadeOutEndAt + 2);
27091
27397
  }
27092
27398
  };
27093
27399
  const clipMeter = createClipMeter(audioCtx, glueMakeup);
@@ -27147,8 +27453,7 @@ var createDtmStudio = async (options = {}) => {
27147
27453
  };
27148
27454
  };
27149
27455
  const resumeAudio = () => {
27150
- fadeGain.gain.cancelScheduledValues(audioCtx.currentTime);
27151
- fadeGain.gain.setValueAtTime(1, audioCtx.currentTime);
27456
+ restoreFadeGainIfMuted();
27152
27457
  if (audioCtx.state === "closed") return Promise.resolve();
27153
27458
  return audioCtx.resume();
27154
27459
  };
@@ -27359,12 +27664,6 @@ var createDtmStudio = async (options = {}) => {
27359
27664
  await listReady;
27360
27665
  nameToKey = await buildNameToKeyMapping();
27361
27666
  await Promise.all([drumReady, loadPreset(defaultPreset)]);
27362
- const restoreFadeGainIfMuted = () => {
27363
- if (fadeGain.gain.value < 1) {
27364
- fadeGain.gain.cancelScheduledValues(audioCtx.currentTime);
27365
- fadeGain.gain.setValueAtTime(1, audioCtx.currentTime);
27366
- }
27367
- };
27368
27667
  const playDrum = (e) => {
27369
27668
  if (!sfDrum.font) return;
27370
27669
  if (e.when === 0) restoreFadeGainIfMuted();
@@ -28115,7 +28414,7 @@ var createDtmStudio = async (options = {}) => {
28115
28414
  };
28116
28415
  const setMasterVolume = (volume) => {
28117
28416
  const g = Math.max(0, Math.min(100, volume)) / 100;
28118
- masterGain.gain.setValueAtTime(g, audioCtx.currentTime);
28417
+ masterGain.gain.setTargetAtTime(g, audioCtx.currentTime, 0.02);
28119
28418
  };
28120
28419
  let recordDestNode = null;
28121
28420
  const createMediaStreamDestination = () => {