@onjmin/dtm 0.1.88 → 0.1.90

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
@@ -1466,6 +1466,149 @@ var getDrumPatternKeys = (name, dict) => {
1466
1466
  return Array.from(keys);
1467
1467
  };
1468
1468
 
1469
+ // src/channel-strip.ts
1470
+ var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
1471
+ var compressionParams = (amount) => {
1472
+ const t = clamp(amount, 0, 100) / 100;
1473
+ return {
1474
+ threshold: 0 + (-24 - 0) * t,
1475
+ ratio: 1 + (12 - 1) * t,
1476
+ knee: 0 + (6 - 0) * t,
1477
+ attack: 0.02 + (3e-3 - 0.02) * t,
1478
+ release: 0.25 + (0.15 - 0.25) * t
1479
+ };
1480
+ };
1481
+ var EQ_LOW_FREQ = 200;
1482
+ var EQ_MID_FREQ = 1e3;
1483
+ var EQ_HIGH_FREQ = 5e3;
1484
+ var EQ_MID_Q = 1;
1485
+ var EQ_MAX_DB = 12;
1486
+ var createChannelStrip = (ctx, destination, options = {}) => {
1487
+ const input = ctx.createGain();
1488
+ const eqLow = ctx.createBiquadFilter();
1489
+ eqLow.type = "lowshelf";
1490
+ eqLow.frequency.value = EQ_LOW_FREQ;
1491
+ eqLow.gain.value = clamp(options.eqLow ?? 0, -EQ_MAX_DB, EQ_MAX_DB);
1492
+ const eqMid = ctx.createBiquadFilter();
1493
+ eqMid.type = "peaking";
1494
+ eqMid.frequency.value = EQ_MID_FREQ;
1495
+ eqMid.Q.value = EQ_MID_Q;
1496
+ eqMid.gain.value = clamp(options.eqMid ?? 0, -EQ_MAX_DB, EQ_MAX_DB);
1497
+ const eqHigh = ctx.createBiquadFilter();
1498
+ eqHigh.type = "highshelf";
1499
+ eqHigh.frequency.value = EQ_HIGH_FREQ;
1500
+ eqHigh.gain.value = clamp(options.eqHigh ?? 0, -EQ_MAX_DB, EQ_MAX_DB);
1501
+ input.connect(eqLow);
1502
+ eqLow.connect(eqMid);
1503
+ eqMid.connect(eqHigh);
1504
+ const compressor = ctx.createDynamicsCompressor();
1505
+ const applyCompression = (amount) => {
1506
+ const p = compressionParams(amount);
1507
+ const now = ctx.currentTime;
1508
+ compressor.threshold.setValueAtTime(p.threshold, now);
1509
+ compressor.ratio.setValueAtTime(p.ratio, now);
1510
+ compressor.knee.setValueAtTime(p.knee, now);
1511
+ compressor.attack.setValueAtTime(p.attack, now);
1512
+ compressor.release.setValueAtTime(p.release, now);
1513
+ };
1514
+ applyCompression(options.compression ?? 0);
1515
+ const splitter = ctx.createChannelSplitter(2);
1516
+ const mid = ctx.createGain();
1517
+ mid.gain.value = 0.5;
1518
+ splitter.connect(mid, 0);
1519
+ splitter.connect(mid, 1);
1520
+ const sideSum = ctx.createGain();
1521
+ sideSum.gain.value = 1;
1522
+ const sideL = ctx.createGain();
1523
+ sideL.gain.value = 0.5;
1524
+ const sideR = ctx.createGain();
1525
+ sideR.gain.value = -0.5;
1526
+ splitter.connect(sideL, 0);
1527
+ splitter.connect(sideR, 1);
1528
+ sideL.connect(sideSum);
1529
+ sideR.connect(sideSum);
1530
+ const widthGain = ctx.createGain();
1531
+ sideSum.connect(widthGain);
1532
+ const widthInv = ctx.createGain();
1533
+ widthInv.gain.value = -1;
1534
+ widthGain.connect(widthInv);
1535
+ const outL = ctx.createGain();
1536
+ mid.connect(outL);
1537
+ widthGain.connect(outL);
1538
+ const outR = ctx.createGain();
1539
+ mid.connect(outR);
1540
+ widthInv.connect(outR);
1541
+ const merger = ctx.createChannelMerger(2);
1542
+ outL.connect(merger, 0, 0);
1543
+ outR.connect(merger, 0, 1);
1544
+ const setWidth = (width) => {
1545
+ widthGain.gain.setTargetAtTime(
1546
+ clamp(width, 0, 200) / 100,
1547
+ ctx.currentTime,
1548
+ 0.02
1549
+ );
1550
+ };
1551
+ setWidth(options.width ?? 100);
1552
+ eqHigh.connect(compressor);
1553
+ compressor.connect(splitter);
1554
+ merger.connect(destination);
1555
+ const reverbSendGain = ctx.createGain();
1556
+ reverbSendGain.gain.value = clamp(options.reverbSend ?? 0, 0, 100) / 100;
1557
+ merger.connect(reverbSendGain);
1558
+ if (options.reverbBus) reverbSendGain.connect(options.reverbBus);
1559
+ return {
1560
+ input,
1561
+ setEqLow: (db) => {
1562
+ eqLow.gain.setTargetAtTime(
1563
+ clamp(db, -EQ_MAX_DB, EQ_MAX_DB),
1564
+ ctx.currentTime,
1565
+ 0.02
1566
+ );
1567
+ },
1568
+ setEqMid: (db) => {
1569
+ eqMid.gain.setTargetAtTime(
1570
+ clamp(db, -EQ_MAX_DB, EQ_MAX_DB),
1571
+ ctx.currentTime,
1572
+ 0.02
1573
+ );
1574
+ },
1575
+ setEqHigh: (db) => {
1576
+ eqHigh.gain.setTargetAtTime(
1577
+ clamp(db, -EQ_MAX_DB, EQ_MAX_DB),
1578
+ ctx.currentTime,
1579
+ 0.02
1580
+ );
1581
+ },
1582
+ setCompression: applyCompression,
1583
+ setWidth,
1584
+ setReverbSend: (amount) => {
1585
+ reverbSendGain.gain.setTargetAtTime(
1586
+ clamp(amount, 0, 100) / 100,
1587
+ ctx.currentTime,
1588
+ 0.02
1589
+ );
1590
+ },
1591
+ dispose: () => {
1592
+ input.disconnect();
1593
+ eqLow.disconnect();
1594
+ eqMid.disconnect();
1595
+ eqHigh.disconnect();
1596
+ compressor.disconnect();
1597
+ splitter.disconnect();
1598
+ mid.disconnect();
1599
+ sideSum.disconnect();
1600
+ sideL.disconnect();
1601
+ sideR.disconnect();
1602
+ widthGain.disconnect();
1603
+ widthInv.disconnect();
1604
+ outL.disconnect();
1605
+ outR.disconnect();
1606
+ merger.disconnect();
1607
+ reverbSendGain.disconnect();
1608
+ }
1609
+ };
1610
+ };
1611
+
1469
1612
  // src/chords.ts
1470
1613
  var C3 = 48;
1471
1614
  var buildChordPlacements = (options) => {
@@ -2295,7 +2438,7 @@ var normalizeLyricLines = (lines) => {
2295
2438
  var LYRIC_LINE = /^@@(\d+)\s*(.*)$/;
2296
2439
  var isLyricContinuation = (seg) => !/^[@#]/.test(seg);
2297
2440
  var splitSegments = (mml) => mml.split(/[;\n\r]+/).map((s) => s.trim()).filter((s) => s.length > 0);
2298
- var clamp = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
2441
+ var clamp2 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
2299
2442
  var MAX_VOCAL_VOLUME = 400;
2300
2443
  var VOCAL_BOOST_DB_PER_PERCENT = 0.08;
2301
2444
  var vocalVolumeToGain = (v) => {
@@ -2329,7 +2472,7 @@ var parseLyrics = (mml) => {
2329
2472
  if (modelMatch) {
2330
2473
  model = modelMatch[1].toLowerCase();
2331
2474
  if (modelMatch[2]) {
2332
- volume = clamp(Number.parseInt(modelMatch[2], 10), 0, MAX_VOCAL_VOLUME);
2475
+ volume = clamp2(Number.parseInt(modelMatch[2], 10), 0, MAX_VOCAL_VOLUME);
2333
2476
  }
2334
2477
  metaTokens.push(modelMatch[0]);
2335
2478
  rest = rest.substring(modelMatch[0].length).trim();
@@ -2337,28 +2480,28 @@ var parseLyrics = (mml) => {
2337
2480
  while (true) {
2338
2481
  const vMatch = rest.match(/^v(\d+)/i);
2339
2482
  if (vMatch) {
2340
- volume = clamp(Number.parseInt(vMatch[1], 10), 0, MAX_VOCAL_VOLUME);
2483
+ volume = clamp2(Number.parseInt(vMatch[1], 10), 0, MAX_VOCAL_VOLUME);
2341
2484
  metaTokens.push(vMatch[0]);
2342
2485
  rest = rest.substring(vMatch[0].length).trim();
2343
2486
  continue;
2344
2487
  }
2345
2488
  const qMatch = rest.match(/^q(\d+)/i);
2346
2489
  if (qMatch) {
2347
- gate = clamp(Number.parseInt(qMatch[1], 10), 0, 100);
2490
+ gate = clamp2(Number.parseInt(qMatch[1], 10), 0, 100);
2348
2491
  metaTokens.push(qMatch[0]);
2349
2492
  rest = rest.substring(qMatch[0].length).trim();
2350
2493
  continue;
2351
2494
  }
2352
2495
  const pMatch = rest.match(/^p(\d+)/i);
2353
2496
  if (pMatch) {
2354
- pan = clamp(Number.parseInt(pMatch[1], 10), 0, 127);
2497
+ pan = clamp2(Number.parseInt(pMatch[1], 10), 0, 127);
2355
2498
  metaTokens.push(pMatch[0]);
2356
2499
  rest = rest.substring(pMatch[0].length).trim();
2357
2500
  continue;
2358
2501
  }
2359
2502
  const oMatch = rest.match(/^o(-?\d+)/i);
2360
2503
  if (oMatch) {
2361
- octave = clamp(Number.parseInt(oMatch[1], 10), -2, 2);
2504
+ octave = clamp2(Number.parseInt(oMatch[1], 10), -2, 2);
2362
2505
  metaTokens.push(oMatch[0]);
2363
2506
  rest = rest.substring(oMatch[0].length).trim();
2364
2507
  continue;
@@ -2372,28 +2515,28 @@ var parseLyrics = (mml) => {
2372
2515
  }
2373
2516
  const rMatch = rest.match(/^r(\d+)/i);
2374
2517
  if (rMatch) {
2375
- reverb = clamp(Number.parseInt(rMatch[1], 10), 0, 100);
2518
+ reverb = clamp2(Number.parseInt(rMatch[1], 10), 0, 100);
2376
2519
  metaTokens.push(rMatch[0]);
2377
2520
  rest = rest.substring(rMatch[0].length).trim();
2378
2521
  continue;
2379
2522
  }
2380
2523
  const gMatch = rest.match(/^g(\d+)/i);
2381
2524
  if (gMatch) {
2382
- gender = clamp(Number.parseInt(gMatch[1], 10), 0, 100);
2525
+ gender = clamp2(Number.parseInt(gMatch[1], 10), 0, 100);
2383
2526
  metaTokens.push(gMatch[0]);
2384
2527
  rest = rest.substring(gMatch[0].length).trim();
2385
2528
  continue;
2386
2529
  }
2387
2530
  const hMatch = rest.match(/^h(\d+)/i);
2388
2531
  if (hMatch) {
2389
- breathiness = clamp(Number.parseInt(hMatch[1], 10), 0, 100);
2532
+ breathiness = clamp2(Number.parseInt(hMatch[1], 10), 0, 100);
2390
2533
  metaTokens.push(hMatch[0]);
2391
2534
  rest = rest.substring(hMatch[0].length).trim();
2392
2535
  continue;
2393
2536
  }
2394
2537
  const eMatch = rest.match(/^e(\d+)/i);
2395
2538
  if (eMatch) {
2396
- delay = clamp(Number.parseInt(eMatch[1], 10), 0, 100);
2539
+ delay = clamp2(Number.parseInt(eMatch[1], 10), 0, 100);
2397
2540
  metaTokens.push(eMatch[0]);
2398
2541
  rest = rest.substring(eMatch[0].length).trim();
2399
2542
  continue;
@@ -3309,7 +3452,7 @@ var PITCH_MAP = {
3309
3452
  a: 9,
3310
3453
  b: 11
3311
3454
  };
3312
- var clamp2 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
3455
+ var clamp3 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
3313
3456
  var META_DIRECTIVE = /#(inst|drum|drumfont|volume|drumvolume|reverb|reverbdecay|reverbpredelay|delay|delaydiv|fadein|fadeout|mode)=([\w-]+)/gi;
3314
3457
  var TRACK_INST_DIRECTIVE = /#t(\d+)inst=([^#;\r\n]+)/gi;
3315
3458
  var TRACK_COMP_DIRECTIVE = /#t(\d+)comp=(\d+)/gi;
@@ -3333,24 +3476,24 @@ var parseMmlMeta = (mml) => {
3333
3476
  if (!Number.isNaN(dv)) meta.drumVolume = dv;
3334
3477
  } else if (key === "reverb") {
3335
3478
  const rv = Number.parseInt(m[2], 10);
3336
- if (!Number.isNaN(rv)) meta.reverb = clamp2(rv, 0, 100);
3479
+ if (!Number.isNaN(rv)) meta.reverb = clamp3(rv, 0, 100);
3337
3480
  } else if (key === "reverbdecay") {
3338
3481
  const rd = Number.parseInt(m[2], 10);
3339
- if (!Number.isNaN(rd)) meta.reverbDecay = clamp2(rd, 3, 40);
3482
+ if (!Number.isNaN(rd)) meta.reverbDecay = clamp3(rd, 3, 40);
3340
3483
  } else if (key === "reverbpredelay") {
3341
3484
  const rp = Number.parseInt(m[2], 10);
3342
- if (!Number.isNaN(rp)) meta.reverbPreDelay = clamp2(rp, 0, 150);
3485
+ if (!Number.isNaN(rp)) meta.reverbPreDelay = clamp3(rp, 0, 150);
3343
3486
  } else if (key === "delay") {
3344
3487
  const dv = Number.parseInt(m[2], 10);
3345
- if (!Number.isNaN(dv)) meta.delay = clamp2(dv, 0, 100);
3488
+ if (!Number.isNaN(dv)) meta.delay = clamp3(dv, 0, 100);
3346
3489
  } else if (key === "delaydiv") {
3347
3490
  if (["4", "8", "8d", "16"].includes(m[2])) meta.delayDivision = m[2];
3348
3491
  } else if (key === "fadein") {
3349
3492
  const fi = Number.parseInt(m[2], 10);
3350
- if (!Number.isNaN(fi)) meta.fadeIn = clamp2(fi, 0, 100);
3493
+ if (!Number.isNaN(fi)) meta.fadeIn = clamp3(fi, 0, 100);
3351
3494
  } else if (key === "fadeout") {
3352
3495
  const fo = Number.parseInt(m[2], 10);
3353
- if (!Number.isNaN(fo)) meta.fadeOut = clamp2(fo, 0, 100);
3496
+ if (!Number.isNaN(fo)) meta.fadeOut = clamp3(fo, 0, 100);
3354
3497
  } else if (key === "mode") {
3355
3498
  if (m[2] === "simple" || m[2] === "advanced") {
3356
3499
  meta.mode = m[2];
@@ -3367,7 +3510,7 @@ var parseMmlMeta = (mml) => {
3367
3510
  }
3368
3511
  for (const m of mml.matchAll(TRACK_COMP_DIRECTIVE)) {
3369
3512
  const idx = Number.parseInt(m[1], 10);
3370
- const val = clamp2(Number.parseInt(m[2], 10), 0, 100);
3513
+ const val = clamp3(Number.parseInt(m[2], 10), 0, 100);
3371
3514
  if (!Number.isNaN(idx) && !Number.isNaN(val)) {
3372
3515
  meta.trackCompression ??= {};
3373
3516
  meta.trackCompression[idx] = val;
@@ -3375,7 +3518,7 @@ var parseMmlMeta = (mml) => {
3375
3518
  }
3376
3519
  for (const m of mml.matchAll(TRACK_WIDTH_DIRECTIVE)) {
3377
3520
  const idx = Number.parseInt(m[1], 10);
3378
- const val = clamp2(Number.parseInt(m[2], 10), 0, 200);
3521
+ const val = clamp3(Number.parseInt(m[2], 10), 0, 200);
3379
3522
  if (!Number.isNaN(idx) && !Number.isNaN(val)) {
3380
3523
  meta.trackWidth ??= {};
3381
3524
  meta.trackWidth[idx] = val;
@@ -3383,7 +3526,7 @@ var parseMmlMeta = (mml) => {
3383
3526
  }
3384
3527
  for (const m of mml.matchAll(TRACK_REVERBSEND_DIRECTIVE)) {
3385
3528
  const idx = Number.parseInt(m[1], 10);
3386
- const val = clamp2(Number.parseInt(m[2], 10), 0, 100);
3529
+ const val = clamp3(Number.parseInt(m[2], 10), 0, 100);
3387
3530
  if (!Number.isNaN(idx) && !Number.isNaN(val)) {
3388
3531
  meta.trackReverbSend ??= {};
3389
3532
  meta.trackReverbSend[idx] = val;
@@ -3391,7 +3534,7 @@ var parseMmlMeta = (mml) => {
3391
3534
  }
3392
3535
  for (const m of mml.matchAll(TRACK_EQLOW_DIRECTIVE)) {
3393
3536
  const idx = Number.parseInt(m[1], 10);
3394
- const val = clamp2(Number.parseInt(m[2], 10), -12, 12);
3537
+ const val = clamp3(Number.parseInt(m[2], 10), -12, 12);
3395
3538
  if (!Number.isNaN(idx) && !Number.isNaN(val)) {
3396
3539
  meta.trackEqLow ??= {};
3397
3540
  meta.trackEqLow[idx] = val;
@@ -3399,7 +3542,7 @@ var parseMmlMeta = (mml) => {
3399
3542
  }
3400
3543
  for (const m of mml.matchAll(TRACK_EQMID_DIRECTIVE)) {
3401
3544
  const idx = Number.parseInt(m[1], 10);
3402
- const val = clamp2(Number.parseInt(m[2], 10), -12, 12);
3545
+ const val = clamp3(Number.parseInt(m[2], 10), -12, 12);
3403
3546
  if (!Number.isNaN(idx) && !Number.isNaN(val)) {
3404
3547
  meta.trackEqMid ??= {};
3405
3548
  meta.trackEqMid[idx] = val;
@@ -3407,7 +3550,7 @@ var parseMmlMeta = (mml) => {
3407
3550
  }
3408
3551
  for (const m of mml.matchAll(TRACK_EQHIGH_DIRECTIVE)) {
3409
3552
  const idx = Number.parseInt(m[1], 10);
3410
- const val = clamp2(Number.parseInt(m[2], 10), -12, 12);
3553
+ const val = clamp3(Number.parseInt(m[2], 10), -12, 12);
3411
3554
  if (!Number.isNaN(idx) && !Number.isNaN(val)) {
3412
3555
  meta.trackEqHigh ??= {};
3413
3556
  meta.trackEqHigh[idx] = val;
@@ -3556,7 +3699,7 @@ var parseMML = (mml, options = {}) => {
3556
3699
  numStr += body[j];
3557
3700
  j++;
3558
3701
  }
3559
- const len = numStr ? clamp2(Number.parseInt(numStr, 10), 1, 64) : baseLength;
3702
+ const len = numStr ? clamp3(Number.parseInt(numStr, 10), 1, 64) : baseLength;
3560
3703
  let steps = Math.round(stepsPerBar / len);
3561
3704
  while (j < body.length && body[j] === ".") {
3562
3705
  steps = Math.round(steps * 1.5);
@@ -3574,7 +3717,7 @@ var parseMML = (mml, options = {}) => {
3574
3717
  numStr += body[j];
3575
3718
  j++;
3576
3719
  }
3577
- octave = numStr ? clamp2(Number.parseInt(numStr, 10), 0, 8) : 4;
3720
+ octave = numStr ? clamp3(Number.parseInt(numStr, 10), 0, 8) : 4;
3578
3721
  pushTok("octave", currentStep, 0, tokStart);
3579
3722
  } else if (ch === ">") {
3580
3723
  octave = Math.min(8, octave + 1);
@@ -3591,7 +3734,7 @@ var parseMML = (mml, options = {}) => {
3591
3734
  numStr += body[j];
3592
3735
  j++;
3593
3736
  }
3594
- baseLength = clamp2(Number.parseInt(numStr, 10) || 16, 1, 64);
3737
+ baseLength = clamp3(Number.parseInt(numStr, 10) || 16, 1, 64);
3595
3738
  pushTok("length", currentStep, 0, tokStart);
3596
3739
  } else if (ch === "r") {
3597
3740
  j++;
@@ -3608,10 +3751,10 @@ var parseMML = (mml, options = {}) => {
3608
3751
  }
3609
3752
  if (ch === "t" && numStr) {
3610
3753
  if (bpm === null) {
3611
- bpm = clamp2(Number.parseInt(numStr, 10), 1, 255);
3754
+ bpm = clamp3(Number.parseInt(numStr, 10), 1, 255);
3612
3755
  }
3613
3756
  } else if (ch === "v" && numStr) {
3614
- velocity = clamp2(Number.parseInt(numStr, 10), 0, 127);
3757
+ velocity = clamp3(Number.parseInt(numStr, 10), 0, 127);
3615
3758
  trackVelocity.set(trackIndex, velocity);
3616
3759
  }
3617
3760
  pushTok("ctrl", currentStep, 0, tokStart);
@@ -3645,7 +3788,7 @@ var parseMML = (mml, options = {}) => {
3645
3788
  numStr += body[j];
3646
3789
  j++;
3647
3790
  }
3648
- octave = numStr ? clamp2(Number.parseInt(numStr, 10), 0, 8) : 4;
3791
+ octave = numStr ? clamp3(Number.parseInt(numStr, 10), 0, 8) : 4;
3649
3792
  } else {
3650
3793
  j++;
3651
3794
  }
@@ -3707,6 +3850,37 @@ var parseMML = (mml, options = {}) => {
3707
3850
  };
3708
3851
  };
3709
3852
 
3853
+ // src/reverb.ts
3854
+ var DEFAULT_REVERB_DECAY_SEC = 2.2;
3855
+ var MIN_REVERB_DECAY_SEC = 0.3;
3856
+ var MAX_REVERB_DECAY_SEC = 4;
3857
+ var IMPULSE_DECAY_CURVE = 2.5;
3858
+ var DEFAULT_REVERB_PREDELAY_MS = 0;
3859
+ var MIN_REVERB_PREDELAY_MS = 0;
3860
+ var MAX_REVERB_PREDELAY_MS = 150;
3861
+ var createReverbImpulse = (ctx, decaySec = DEFAULT_REVERB_DECAY_SEC) => {
3862
+ const rate = ctx.sampleRate;
3863
+ const length = Math.max(
3864
+ 1,
3865
+ Math.floor(
3866
+ rate * Math.max(
3867
+ MIN_REVERB_DECAY_SEC,
3868
+ Math.min(MAX_REVERB_DECAY_SEC, decaySec)
3869
+ )
3870
+ )
3871
+ );
3872
+ const impulse = ctx.createBuffer(2, length, rate);
3873
+ for (let ch = 0; ch < impulse.numberOfChannels; ch++) {
3874
+ const data = impulse.getChannelData(ch);
3875
+ for (let i2 = 0; i2 < length; i2++) {
3876
+ const envelope = (1 - i2 / length) ** IMPULSE_DECAY_CURVE;
3877
+ data[i2] = (Math.random() * 2 - 1) * envelope;
3878
+ }
3879
+ }
3880
+ return impulse;
3881
+ };
3882
+ var reverbAmountToGain = (amount) => Math.max(0, Math.min(100, amount)) / 100;
3883
+
3710
3884
  // src/sequencer.ts
3711
3885
  var STEPS_PER_BEAT = 48;
3712
3886
  var PLAN_TIME = 0.5;
@@ -6284,9 +6458,54 @@ var playPlacements = (placements, options) => {
6284
6458
  });
6285
6459
  const ownsCtx = !options.audioContext;
6286
6460
  const ctx = options.audioContext ?? new AudioContext();
6287
- const destination = options.destination ?? ctx.destination;
6461
+ const rawDestination = options.destination ?? ctx.destination;
6288
6462
  const useSynth = options.synth ?? !options.onPlayNote;
6289
- const synth = useSynth ? createSynth(ctx, destination) : null;
6463
+ const finalMix = ctx.createGain();
6464
+ const masterGain = ctx.createGain();
6465
+ masterGain.connect(finalMix);
6466
+ const reverbPreDelay = ctx.createDelay(MAX_REVERB_PREDELAY_MS / 1e3);
6467
+ reverbPreDelay.delayTime.value = (options.metaReverbPreDelay ?? DEFAULT_REVERB_PREDELAY_MS) / 1e3;
6468
+ const reverbConvolver = ctx.createConvolver();
6469
+ const reverbDecaySec = (options.metaReverbDecay ?? 22) / 10 || DEFAULT_REVERB_DECAY_SEC;
6470
+ reverbConvolver.buffer = createReverbImpulse(ctx, reverbDecaySec);
6471
+ reverbConvolver.normalize = true;
6472
+ const reverbWetGain = ctx.createGain();
6473
+ reverbWetGain.gain.value = reverbAmountToGain(options.metaReverb ?? 0);
6474
+ reverbPreDelay.connect(reverbConvolver);
6475
+ reverbConvolver.connect(reverbWetGain);
6476
+ reverbWetGain.connect(finalMix);
6477
+ finalMix.connect(rawDestination);
6478
+ const channelStrips = /* @__PURE__ */ new Map();
6479
+ const getChannelStrip = (index) => {
6480
+ let strip = channelStrips.get(index);
6481
+ if (!strip) {
6482
+ strip = createChannelStrip(ctx, masterGain, {
6483
+ compression: options.trackCompression?.[index] ?? 0,
6484
+ width: options.trackWidth?.[index] ?? 100,
6485
+ eqLow: options.trackEqLow?.[index] ?? 0,
6486
+ eqMid: options.trackEqMid?.[index] ?? 0,
6487
+ eqHigh: options.trackEqHigh?.[index] ?? 0,
6488
+ reverbSend: options.trackReverbSend?.[index] ?? 0,
6489
+ reverbBus: reverbPreDelay
6490
+ });
6491
+ channelStrips.set(index, strip);
6492
+ }
6493
+ return strip;
6494
+ };
6495
+ const synths = /* @__PURE__ */ new Map();
6496
+ const getSynth = (index) => {
6497
+ let s = synths.get(index);
6498
+ if (!s) {
6499
+ s = createSynth(ctx, getChannelStrip(index).input);
6500
+ synths.set(index, s);
6501
+ }
6502
+ return s;
6503
+ };
6504
+ const drumSynth = useSynth ? createSynth(ctx, masterGain) : null;
6505
+ const trackIndexById = /* @__PURE__ */ new Map();
6506
+ trackIndices.forEach((idx) => {
6507
+ trackIndexById.set(TRACK_ID_BY_INDEX[idx] ?? `t${idx}`, idx);
6508
+ });
6290
6509
  const pauseWhenHidden = options.pauseWhenHidden ?? ownsCtx;
6291
6510
  let playing = false;
6292
6511
  const seq = createSequencer({
@@ -6301,12 +6520,14 @@ var playPlacements = (placements, options) => {
6301
6520
  getAudioTime: () => ctx.currentTime,
6302
6521
  onPlayNote: (e) => {
6303
6522
  options.onPlayNote?.(e);
6304
- synth?.playNote(e);
6523
+ if (!useSynth) return;
6524
+ const index = trackIndexById.get(e.trackId);
6525
+ (index === void 0 ? drumSynth : getSynth(index))?.playNote(e);
6305
6526
  },
6306
6527
  onPlayDrum: (e) => {
6307
6528
  const velocity = e.velocity * (drumVolume / 100) * (masterVolume / 100);
6308
6529
  options.onPlayDrum?.({ ...e, velocity });
6309
- synth?.playDrum({ ...e, velocity });
6530
+ drumSynth?.playDrum({ ...e, velocity });
6310
6531
  },
6311
6532
  onTick: (step) => {
6312
6533
  options.onTick?.(step);
@@ -6357,6 +6578,7 @@ var playPlacements = (placements, options) => {
6357
6578
  if (pauseWhenHidden && typeof document !== "undefined") {
6358
6579
  document.removeEventListener("visibilitychange", onVisibilityChange);
6359
6580
  }
6581
+ for (const strip of channelStrips.values()) strip.dispose();
6360
6582
  if (ownsCtx) void ctx.close();
6361
6583
  };
6362
6584
  return {
@@ -6376,7 +6598,16 @@ var playMML = (mml, options = {}) => {
6376
6598
  bpm,
6377
6599
  metaVolume: meta.volume,
6378
6600
  metaDrum: meta.drum,
6379
- metaDrumVolume: meta.drumVolume
6601
+ metaDrumVolume: meta.drumVolume,
6602
+ metaReverb: meta.reverb,
6603
+ metaReverbDecay: meta.reverbDecay,
6604
+ metaReverbPreDelay: meta.reverbPreDelay,
6605
+ trackCompression: meta.trackCompression,
6606
+ trackWidth: meta.trackWidth,
6607
+ trackReverbSend: meta.trackReverbSend,
6608
+ trackEqLow: meta.trackEqLow,
6609
+ trackEqMid: meta.trackEqMid,
6610
+ trackEqHigh: meta.trackEqHigh
6380
6611
  });
6381
6612
  };
6382
6613
  var playNote = (options) => {
@@ -9682,20 +9913,23 @@ var adjustZone = async (ctx, fontName, zone) => {
9682
9913
  const newLength = attackLength + loopLengthFrame * repeatCount + releaseLength;
9683
9914
  let totalPeak = 0;
9684
9915
  let loopPeak = 0;
9685
- if (oldBuf.numberOfChannels > 0) {
9686
- const ch0 = oldBuf.getChannelData(0);
9687
- for (let i2 = 0; i2 < ch0.length; i2++) {
9688
- const abs = Math.abs(ch0[i2]);
9916
+ for (let ch = 0; ch < oldBuf.numberOfChannels; ch++) {
9917
+ const chData = oldBuf.getChannelData(ch);
9918
+ for (let i2 = 0; i2 < chData.length; i2++) {
9919
+ const abs = Math.abs(chData[i2]);
9689
9920
  if (abs > totalPeak) totalPeak = abs;
9690
9921
  if (i2 >= loopStartFrame && i2 < loopEndFrame) {
9691
9922
  if (abs > loopPeak) loopPeak = abs;
9692
9923
  }
9693
9924
  }
9694
9925
  }
9926
+ const decay = isDecayInstrument(fontName);
9927
+ const targetRatio = decay ? 0.4 : 0.75;
9928
+ const maxMultiplier = decay ? 6 : 20;
9695
9929
  let gainMultiplier = 1;
9696
- if (!isDecayInstrument(fontName) && loopPeak > 0 && totalPeak > 0 && loopPeak < totalPeak * 0.8) {
9697
- gainMultiplier = totalPeak * 0.75 / loopPeak;
9698
- if (gainMultiplier > 20) gainMultiplier = 20;
9930
+ if (loopPeak > 0 && totalPeak > 0 && loopPeak < totalPeak * 0.8) {
9931
+ gainMultiplier = totalPeak * targetRatio / loopPeak;
9932
+ if (gainMultiplier > maxMultiplier) gainMultiplier = maxMultiplier;
9699
9933
  }
9700
9934
  try {
9701
9935
  const newBuf = ctx.createBuffer(
@@ -11027,14 +11261,14 @@ var DELAY_DIVISIONS = [
11027
11261
  { value: "8d", label: "\u4ED8\u70B98\u5206", beats: 0.75 },
11028
11262
  { value: "16", label: "16\u5206", beats: 0.25 }
11029
11263
  ];
11030
- var clamp3 = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
11264
+ var clamp4 = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
11031
11265
  var divisionToSeconds = (division, bpm) => {
11032
11266
  const beats = DELAY_DIVISIONS.find((d) => d.value === division)?.beats ?? 0.5;
11033
11267
  const safeBpm = bpm > 0 ? bpm : 120;
11034
11268
  return 60 / safeBpm * beats;
11035
11269
  };
11036
11270
  var DELAY_MAX_WET = 0.45;
11037
- var delayAmountToGain = (amount) => clamp3(amount, 0, 100) / 100 * DELAY_MAX_WET;
11271
+ var delayAmountToGain = (amount) => clamp4(amount, 0, 100) / 100 * DELAY_MAX_WET;
11038
11272
  var FEEDBACK_GAIN = 0.3;
11039
11273
  var MAX_DELAY_SEC = 2;
11040
11274
  var createDelayBus = (ctx, destination, options = {}) => {
@@ -13109,38 +13343,6 @@ var isChordHeavyTrack = (notes, threshold = 0.6) => {
13109
13343
  return chordNotes / notes.length >= threshold;
13110
13344
  };
13111
13345
 
13112
- // src/reverb.ts
13113
- var DEFAULT_REVERB_DECAY_SEC = 2.2;
13114
- var MIN_REVERB_DECAY_SEC = 0.3;
13115
- var MAX_REVERB_DECAY_SEC = 4;
13116
- var IMPULSE_DECAY_CURVE = 2.5;
13117
- var DEFAULT_REVERB_PREDELAY_MS = 0;
13118
- var MIN_REVERB_PREDELAY_MS = 0;
13119
- var MAX_REVERB_PREDELAY_MS = 150;
13120
- var createReverbImpulse = (ctx, decaySec = DEFAULT_REVERB_DECAY_SEC) => {
13121
- const rate = ctx.sampleRate;
13122
- const length = Math.max(
13123
- 1,
13124
- Math.floor(
13125
- rate * Math.max(
13126
- MIN_REVERB_DECAY_SEC,
13127
- Math.min(MAX_REVERB_DECAY_SEC, decaySec)
13128
- )
13129
- )
13130
- );
13131
- const impulse = ctx.createBuffer(2, length, rate);
13132
- for (let ch = 0; ch < impulse.numberOfChannels; ch++) {
13133
- const data = impulse.getChannelData(ch);
13134
- for (let i2 = 0; i2 < length; i2++) {
13135
- const envelope = (1 - i2 / length) ** IMPULSE_DECAY_CURVE;
13136
- data[i2] = (Math.random() * 2 - 1) * envelope;
13137
- }
13138
- }
13139
- return impulse;
13140
- };
13141
- var REVERB_MAX_WET = 0.6;
13142
- var reverbAmountToGain = (amount) => Math.max(0, Math.min(100, amount)) / 100 * REVERB_MAX_WET;
13143
-
13144
13346
  // src/daw.ts
13145
13347
  var CHORD_INFO_HTML2 = `
13146
13348
  <div class="dtm-modal-body-content">
@@ -13576,7 +13778,7 @@ var deriveCustomVocalKeyFromUrl = (url2) => {
13576
13778
  if (name && /^[0-9]/.test(name)) name = `_${name}`;
13577
13779
  return CUSTOM_VOCAL_KEY_RE.test(name) ? name : "";
13578
13780
  };
13579
- var clamp4 = (v, min, max) => Math.min(Math.max(v, min), max);
13781
+ var clamp5 = (v, min, max) => Math.min(Math.max(v, min), max);
13580
13782
  var normalizeInstrumentName = (name) => {
13581
13783
  if (!name) return "";
13582
13784
  const stripped = name.replace(/\s+/g, "").toLowerCase();
@@ -13665,6 +13867,8 @@ var mountDAW = (target, options = {}) => {
13665
13867
  refs.drumFontSelect.value = currentDrumFont;
13666
13868
  let currentInstrument = "";
13667
13869
  let activeTrackId = options.initialActiveTrack ?? trackConfigs[0].id;
13870
+ let trackFxAdvancedOpen = false;
13871
+ let lyricAdvancedOpen = false;
13668
13872
  let activeToolMode = "pen";
13669
13873
  let currentInsertLength = 48;
13670
13874
  let snapGridSteps = 12;
@@ -13961,7 +14165,7 @@ var mountDAW = (target, options = {}) => {
13961
14165
  const thumbW = Math.max(40, canvas.width / totalContentWidth * sbW);
13962
14166
  const ratio = currentOffsetX / maxOffsetX;
13963
14167
  refs.hScrollThumb.style.width = `${thumbW}px`;
13964
- refs.hScrollThumb.style.left = `${clamp4(ratio * (sbW - thumbW), 0, sbW - thumbW)}px`;
14168
+ refs.hScrollThumb.style.left = `${clamp5(ratio * (sbW - thumbW), 0, sbW - thumbW)}px`;
13965
14169
  }
13966
14170
  const totalHeight = renderConfig.keyCount * renderConfig.keyHeight;
13967
14171
  const sbH = refs.vScroll.clientHeight;
@@ -14031,9 +14235,9 @@ var mountDAW = (target, options = {}) => {
14031
14235
  if (maxOffsetX <= 0) return;
14032
14236
  const rect = refs.hScroll.getBoundingClientRect();
14033
14237
  const thumbW = Number.parseFloat(refs.hScrollThumb.style.width) || 40;
14034
- const x2 = clamp4(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
14238
+ const x2 = clamp5(clientX - rect.left - thumbW / 2, 0, rect.width - thumbW);
14035
14239
  const ratio = x2 / (rect.width - thumbW);
14036
- currentOffsetX = clamp4(ratio * maxOffsetX, 0, maxOffsetX);
14240
+ currentOffsetX = clamp5(ratio * maxOffsetX, 0, maxOffsetX);
14037
14241
  setDrawOffset(currentOffsetX, currentOffsetY);
14038
14242
  redrawAll();
14039
14243
  };
@@ -14042,9 +14246,9 @@ var mountDAW = (target, options = {}) => {
14042
14246
  if (maxOffset <= 0) return;
14043
14247
  const rect = refs.vScroll.getBoundingClientRect();
14044
14248
  const thumbH = Number.parseFloat(refs.vScrollThumb.style.height) || 40;
14045
- const y = clamp4(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
14249
+ const y = clamp5(clientY - rect.top - thumbH / 2, 0, rect.height - thumbH);
14046
14250
  const ratio = y / (rect.height - thumbH);
14047
- currentOffsetY = clamp4(ratio * maxOffset, 0, maxOffset);
14251
+ currentOffsetY = clamp5(ratio * maxOffset, 0, maxOffset);
14048
14252
  setDrawOffset(currentOffsetX, currentOffsetY);
14049
14253
  redrawAll();
14050
14254
  };
@@ -14091,8 +14295,8 @@ var mountDAW = (target, options = {}) => {
14091
14295
  if (dx !== 0 || dy !== 0) {
14092
14296
  const maxOffsetX = getMaxOffsetX();
14093
14297
  const maxOffsetY = getMaxOffsetY();
14094
- currentOffsetX = clamp4(currentOffsetX + dx, 0, maxOffsetX);
14095
- currentOffsetY = clamp4(currentOffsetY + dy, 0, maxOffsetY);
14298
+ currentOffsetX = clamp5(currentOffsetX + dx, 0, maxOffsetX);
14299
+ currentOffsetY = clamp5(currentOffsetY + dy, 0, maxOffsetY);
14096
14300
  setDrawOffset(currentOffsetX, currentOffsetY);
14097
14301
  onPointerMove(lastMoveEvent);
14098
14302
  }
@@ -14482,12 +14686,12 @@ var mountDAW = (target, options = {}) => {
14482
14686
  "wheel",
14483
14687
  (event) => {
14484
14688
  event.preventDefault();
14485
- currentOffsetY = clamp4(
14689
+ currentOffsetY = clamp5(
14486
14690
  currentOffsetY + event.deltaY,
14487
14691
  0,
14488
14692
  getMaxOffsetY()
14489
14693
  );
14490
- currentOffsetX = clamp4(
14694
+ currentOffsetX = clamp5(
14491
14695
  currentOffsetX + event.deltaX,
14492
14696
  0,
14493
14697
  getMaxOffsetX()
@@ -14526,7 +14730,7 @@ var mountDAW = (target, options = {}) => {
14526
14730
  const centerStep = (currentOffsetX + canvas.width / 2) / renderConfig.stepWidth;
14527
14731
  renderConfig.stepWidth = BASE_STEP_WIDTH * (zoomX * 2) / 100;
14528
14732
  refs.zoomXLabel.textContent = `${zoomX}%`;
14529
- currentOffsetX = clamp4(
14733
+ currentOffsetX = clamp5(
14530
14734
  centerStep * renderConfig.stepWidth - canvas.width / 2,
14531
14735
  0,
14532
14736
  getMaxOffsetX()
@@ -14539,7 +14743,7 @@ var mountDAW = (target, options = {}) => {
14539
14743
  const centerKey = (currentOffsetY + canvas.height / 2) / renderConfig.keyHeight;
14540
14744
  renderConfig.keyHeight = BASE_KEY_HEIGHT * zoomY / 100;
14541
14745
  refs.zoomYLabel.textContent = `${zoomY}%`;
14542
- currentOffsetY = clamp4(
14746
+ currentOffsetY = clamp5(
14543
14747
  centerKey * renderConfig.keyHeight - canvas.height / 2,
14544
14748
  0,
14545
14749
  getMaxOffsetY()
@@ -14589,7 +14793,7 @@ var mountDAW = (target, options = {}) => {
14589
14793
  const threshold = currentOffsetX / renderConfig.stepWidth + visibleSteps - 4;
14590
14794
  if (currentPlayStep > threshold) {
14591
14795
  const visibleBars = Math.round(visibleSteps / renderConfig.stepsPerBar);
14592
- currentOffsetX = clamp4(
14796
+ currentOffsetX = clamp5(
14593
14797
  currentOffsetX + visibleBars * renderConfig.stepsPerBar * renderConfig.stepWidth,
14594
14798
  0,
14595
14799
  getMaxOffsetX()
@@ -14676,7 +14880,7 @@ var mountDAW = (target, options = {}) => {
14676
14880
  }
14677
14881
  if (playbackState !== "paused") {
14678
14882
  const canvas = getGridCanvas();
14679
- currentOffsetX = clamp4(
14883
+ currentOffsetX = clamp5(
14680
14884
  playStartStep * renderConfig.stepWidth - canvas.width * 0.5,
14681
14885
  0,
14682
14886
  getMaxOffsetX()
@@ -14780,7 +14984,7 @@ var mountDAW = (target, options = {}) => {
14780
14984
  <input type="range" class="dtm-range dtm-grow" data-dtm="track-vol" min="0" max="127" value="${active.volume}">
14781
14985
  <span class="dtm-label" data-dtm="track-vol-label">${active.volume}</span>
14782
14986
  </div>
14783
- <details class="dtm-advanced" data-dtm="track-fx-advanced">
14987
+ <details class="dtm-advanced" data-dtm="track-fx-advanced" ${trackFxAdvancedOpen ? "open" : ""}>
14784
14988
  <summary>\u8A73\u7D30\u8A2D\u5B9A\uFF08EQ\u30FB\u97F3\u5727\u30FB\u30B9\u30C6\u30EC\u30AA\u5E45\uFF09</summary>
14785
14989
  <div class="dtm-row">
14786
14990
  <span class="dtm-label">EQ\u4F4E\u57DF</span>
@@ -14817,6 +15021,11 @@ var mountDAW = (target, options = {}) => {
14817
15021
  <button class="dtm-infobtn" data-dtm="track-reverb-send-info" title="\u30EA\u30D0\u30FC\u30D6\u9001\u308A\u306E\u89E3\u8AAC">${icon("info", 12)}</button>
14818
15022
  </div>
14819
15023
  </details>`;
15024
+ refs.trackBody.querySelector(
15025
+ '[data-dtm="track-fx-advanced"]'
15026
+ ).addEventListener("toggle", (e) => {
15027
+ trackFxAdvancedOpen = e.target.open;
15028
+ });
14820
15029
  const volInput = refs.trackBody.querySelector(
14821
15030
  '[data-dtm="track-vol"]'
14822
15031
  );
@@ -15030,7 +15239,7 @@ var mountDAW = (target, options = {}) => {
15030
15239
  <input type="range" class="dtm-range dtm-grow" data-dtm="lyric-vol" min="0" max="${MAX_VOCAL_VOLUME}" aria-label="\u6B4C\u5531\u306E\u58F0\u91CF\uFF08100=\u7B49\u500D\u3001100\u8D85\u3067\u30D6\u30FC\u30B9\u30C8\u3001\u65E2\u5B9A200\uFF09">
15031
15240
  <span class="dtm-label" data-dtm="lyric-vol-label"></span>
15032
15241
  </div>
15033
- <details class="dtm-advanced" data-dtm="lyric-advanced">
15242
+ <details class="dtm-advanced" data-dtm="lyric-advanced" ${lyricAdvancedOpen ? "open" : ""}>
15034
15243
  <summary>\u8A73\u7D30\u8A2D\u5B9A</summary>
15035
15244
  <div class="dtm-row">
15036
15245
  <span class="dtm-label">\u30AA\u30AF\u30BF\u30FC\u30D6</span>
@@ -15091,6 +15300,11 @@ var mountDAW = (target, options = {}) => {
15091
15300
  <textarea class="dtm-textarea" data-dtm="lyric-input" rows="2" placeholder="\u3072\u3089\u304C\u306A\u30FB\u30AB\u30BF\u30AB\u30CA\u3067\u6B4C\u8A5E\uFF08\u4F8B: \u3069\u308C\u307F\u3075\u3041\u305D\u3089\u3057\u3069\uFF09"></textarea>
15092
15301
  </div>`;
15093
15302
  refs.trackBody.appendChild(lyricDiv);
15303
+ lyricDiv.querySelector(
15304
+ '[data-dtm="lyric-advanced"]'
15305
+ ).addEventListener("toggle", (e) => {
15306
+ lyricAdvancedOpen = e.target.open;
15307
+ });
15094
15308
  const lyricModelSel = lyricDiv.querySelector(
15095
15309
  '[data-dtm="lyric-model"]'
15096
15310
  );
@@ -15713,7 +15927,7 @@ var mountDAW = (target, options = {}) => {
15713
15927
  const canvas = getGridCanvas();
15714
15928
  const yIndex = renderConfig.keyCount - 1 - (pitch - renderConfig.pitchRangeStart);
15715
15929
  const logicalY = yIndex * renderConfig.keyHeight;
15716
- currentOffsetY = clamp4(
15930
+ currentOffsetY = clamp5(
15717
15931
  logicalY - (canvas.height - renderConfig.keyHeight) / 2,
15718
15932
  0,
15719
15933
  getMaxOffsetY()
@@ -16288,8 +16502,8 @@ var mountDAW = (target, options = {}) => {
16288
16502
  refs.reverbAmount.value = "20";
16289
16503
  refs.reverbAmountLabel.textContent = "20%";
16290
16504
  options.onReverbChange?.(20);
16291
- const bpmT = clamp4((bpm - 60) / (180 - 60), 0, 1);
16292
- reverbDecay = clamp4(
16505
+ const bpmT = clamp5((bpm - 60) / (180 - 60), 0, 1);
16506
+ reverbDecay = clamp5(
16293
16507
  3 - bpmT * (3 - 0.9),
16294
16508
  MIN_REVERB_DECAY_SEC,
16295
16509
  MAX_REVERB_DECAY_SEC
@@ -16324,7 +16538,7 @@ var mountDAW = (target, options = {}) => {
16324
16538
  }
16325
16539
  if (t.lyricModel) {
16326
16540
  t.vocalVibrato = true;
16327
- t.vocalReverb = isMainVocal ? 15 : 30;
16541
+ t.vocalReverb = isMainVocal ? 25 : 45;
16328
16542
  fireLyricsChange(t);
16329
16543
  }
16330
16544
  }
@@ -17220,7 +17434,7 @@ var mountDAW = (target, options = {}) => {
17220
17434
  currentPlayStep = step;
17221
17435
  playbackState = "paused";
17222
17436
  const canvas = getGridCanvas();
17223
- currentOffsetX = clamp4(
17437
+ currentOffsetX = clamp5(
17224
17438
  step * renderConfig.stepWidth - canvas.width * 0.5,
17225
17439
  0,
17226
17440
  getMaxOffsetX()
@@ -17266,11 +17480,11 @@ var mountDAW = (target, options = {}) => {
17266
17480
  getViewState,
17267
17481
  setViewState: (state) => {
17268
17482
  if (typeof state.zoomX === "number") {
17269
- zoomX = clamp4(state.zoomX, 25, 200);
17483
+ zoomX = clamp5(state.zoomX, 25, 200);
17270
17484
  applyZoomX();
17271
17485
  }
17272
17486
  if (typeof state.zoomY === "number") {
17273
- zoomY = clamp4(state.zoomY, 50, 200);
17487
+ zoomY = clamp5(state.zoomY, 50, 200);
17274
17488
  applyZoomY();
17275
17489
  }
17276
17490
  if (typeof state.decomposeChord === "boolean") {
@@ -17292,24 +17506,24 @@ var mountDAW = (target, options = {}) => {
17292
17506
  forcePauseAt,
17293
17507
  setLoading,
17294
17508
  setMasterVolume: (volume) => {
17295
- masterVolume = clamp4(volume, 0, 100);
17509
+ masterVolume = clamp5(volume, 0, 100);
17296
17510
  refs.masterVolume.value = String(masterVolume);
17297
17511
  refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17298
17512
  options.singingVoices?.setVolume(masterVolume / 100);
17299
17513
  },
17300
17514
  setVolume: (volume) => {
17301
- masterVolume = clamp4(volume, 0, 100);
17515
+ masterVolume = clamp5(volume, 0, 100);
17302
17516
  refs.masterVolume.value = String(masterVolume);
17303
17517
  refs.masterVolumeLabel.textContent = `${masterVolume}%`;
17304
17518
  options.singingVoices?.setVolume(masterVolume / 100);
17305
17519
  },
17306
17520
  setDrumVolume: (volume) => {
17307
- drumVolume = clamp4(volume, 0, 100);
17521
+ drumVolume = clamp5(volume, 0, 100);
17308
17522
  refs.drumVolume.value = String(drumVolume);
17309
17523
  refs.drumVolumeLabel.textContent = `${drumVolume}%`;
17310
17524
  },
17311
17525
  setReverbAmount: (amount) => {
17312
- reverbAmount = clamp4(amount, 0, 100);
17526
+ reverbAmount = clamp5(amount, 0, 100);
17313
17527
  refs.reverbAmount.value = String(reverbAmount);
17314
17528
  refs.reverbAmountLabel.textContent = `${reverbAmount}%`;
17315
17529
  options.onReverbChange?.(reverbAmount);
@@ -17333,7 +17547,7 @@ var mountDAW = (target, options = {}) => {
17333
17547
  options.onReverbPreDelayChange?.(reverbPreDelay);
17334
17548
  },
17335
17549
  setDelayAmount: (amount) => {
17336
- delayAmount = clamp4(amount, 0, 100);
17550
+ delayAmount = clamp5(amount, 0, 100);
17337
17551
  refs.delayAmount.value = String(delayAmount);
17338
17552
  refs.delayAmountLabel.textContent = `${delayAmount}%`;
17339
17553
  options.onDelayChange?.(delayAmount);
@@ -18112,149 +18326,6 @@ var isSupported = midiJsonParser.isSupported;
18112
18326
  var parseArrayBuffer = midiJsonParser.parseArrayBuffer;
18113
18327
  URL.revokeObjectURL(url);
18114
18328
 
18115
- // src/channel-strip.ts
18116
- var clamp5 = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
18117
- var compressionParams = (amount) => {
18118
- const t = clamp5(amount, 0, 100) / 100;
18119
- return {
18120
- threshold: 0 + (-24 - 0) * t,
18121
- ratio: 1 + (12 - 1) * t,
18122
- knee: 0 + (6 - 0) * t,
18123
- attack: 0.02 + (3e-3 - 0.02) * t,
18124
- release: 0.25 + (0.15 - 0.25) * t
18125
- };
18126
- };
18127
- var EQ_LOW_FREQ = 200;
18128
- var EQ_MID_FREQ = 1e3;
18129
- var EQ_HIGH_FREQ = 5e3;
18130
- var EQ_MID_Q = 1;
18131
- var EQ_MAX_DB = 12;
18132
- var createChannelStrip = (ctx, destination, options = {}) => {
18133
- const input = ctx.createGain();
18134
- const eqLow = ctx.createBiquadFilter();
18135
- eqLow.type = "lowshelf";
18136
- eqLow.frequency.value = EQ_LOW_FREQ;
18137
- eqLow.gain.value = clamp5(options.eqLow ?? 0, -EQ_MAX_DB, EQ_MAX_DB);
18138
- const eqMid = ctx.createBiquadFilter();
18139
- eqMid.type = "peaking";
18140
- eqMid.frequency.value = EQ_MID_FREQ;
18141
- eqMid.Q.value = EQ_MID_Q;
18142
- eqMid.gain.value = clamp5(options.eqMid ?? 0, -EQ_MAX_DB, EQ_MAX_DB);
18143
- const eqHigh = ctx.createBiquadFilter();
18144
- eqHigh.type = "highshelf";
18145
- eqHigh.frequency.value = EQ_HIGH_FREQ;
18146
- eqHigh.gain.value = clamp5(options.eqHigh ?? 0, -EQ_MAX_DB, EQ_MAX_DB);
18147
- input.connect(eqLow);
18148
- eqLow.connect(eqMid);
18149
- eqMid.connect(eqHigh);
18150
- const compressor = ctx.createDynamicsCompressor();
18151
- const applyCompression = (amount) => {
18152
- const p = compressionParams(amount);
18153
- const now = ctx.currentTime;
18154
- compressor.threshold.setValueAtTime(p.threshold, now);
18155
- compressor.ratio.setValueAtTime(p.ratio, now);
18156
- compressor.knee.setValueAtTime(p.knee, now);
18157
- compressor.attack.setValueAtTime(p.attack, now);
18158
- compressor.release.setValueAtTime(p.release, now);
18159
- };
18160
- applyCompression(options.compression ?? 0);
18161
- const splitter = ctx.createChannelSplitter(2);
18162
- const mid = ctx.createGain();
18163
- mid.gain.value = 0.5;
18164
- splitter.connect(mid, 0);
18165
- splitter.connect(mid, 1);
18166
- const sideSum = ctx.createGain();
18167
- sideSum.gain.value = 1;
18168
- const sideL = ctx.createGain();
18169
- sideL.gain.value = 0.5;
18170
- const sideR = ctx.createGain();
18171
- sideR.gain.value = -0.5;
18172
- splitter.connect(sideL, 0);
18173
- splitter.connect(sideR, 1);
18174
- sideL.connect(sideSum);
18175
- sideR.connect(sideSum);
18176
- const widthGain = ctx.createGain();
18177
- sideSum.connect(widthGain);
18178
- const widthInv = ctx.createGain();
18179
- widthInv.gain.value = -1;
18180
- widthGain.connect(widthInv);
18181
- const outL = ctx.createGain();
18182
- mid.connect(outL);
18183
- widthGain.connect(outL);
18184
- const outR = ctx.createGain();
18185
- mid.connect(outR);
18186
- widthInv.connect(outR);
18187
- const merger = ctx.createChannelMerger(2);
18188
- outL.connect(merger, 0, 0);
18189
- outR.connect(merger, 0, 1);
18190
- const setWidth = (width) => {
18191
- widthGain.gain.setTargetAtTime(
18192
- clamp5(width, 0, 200) / 100,
18193
- ctx.currentTime,
18194
- 0.02
18195
- );
18196
- };
18197
- setWidth(options.width ?? 100);
18198
- eqHigh.connect(compressor);
18199
- compressor.connect(splitter);
18200
- merger.connect(destination);
18201
- const reverbSendGain = ctx.createGain();
18202
- reverbSendGain.gain.value = clamp5(options.reverbSend ?? 0, 0, 100) / 100;
18203
- merger.connect(reverbSendGain);
18204
- if (options.reverbBus) reverbSendGain.connect(options.reverbBus);
18205
- return {
18206
- input,
18207
- setEqLow: (db) => {
18208
- eqLow.gain.setTargetAtTime(
18209
- clamp5(db, -EQ_MAX_DB, EQ_MAX_DB),
18210
- ctx.currentTime,
18211
- 0.02
18212
- );
18213
- },
18214
- setEqMid: (db) => {
18215
- eqMid.gain.setTargetAtTime(
18216
- clamp5(db, -EQ_MAX_DB, EQ_MAX_DB),
18217
- ctx.currentTime,
18218
- 0.02
18219
- );
18220
- },
18221
- setEqHigh: (db) => {
18222
- eqHigh.gain.setTargetAtTime(
18223
- clamp5(db, -EQ_MAX_DB, EQ_MAX_DB),
18224
- ctx.currentTime,
18225
- 0.02
18226
- );
18227
- },
18228
- setCompression: applyCompression,
18229
- setWidth,
18230
- setReverbSend: (amount) => {
18231
- reverbSendGain.gain.setTargetAtTime(
18232
- clamp5(amount, 0, 100) / 100,
18233
- ctx.currentTime,
18234
- 0.02
18235
- );
18236
- },
18237
- dispose: () => {
18238
- input.disconnect();
18239
- eqLow.disconnect();
18240
- eqMid.disconnect();
18241
- eqHigh.disconnect();
18242
- compressor.disconnect();
18243
- splitter.disconnect();
18244
- mid.disconnect();
18245
- sideSum.disconnect();
18246
- sideL.disconnect();
18247
- sideR.disconnect();
18248
- widthGain.disconnect();
18249
- widthInv.disconnect();
18250
- outL.disconnect();
18251
- outR.disconnect();
18252
- merger.disconnect();
18253
- reverbSendGain.disconnect();
18254
- }
18255
- };
18256
- };
18257
-
18258
18329
  // src/clip-meter.ts
18259
18330
  var createClipMeter = (ctx, source, options = {}) => {
18260
18331
  const threshold = options.threshold ?? 0.98;
@@ -18459,6 +18530,11 @@ var createDtmStudio = async (options = {}) => {
18459
18530
  presetUI: true,
18460
18531
  ...options.features
18461
18532
  };
18533
+ const resolveDrumPatterns = (custom) => ({
18534
+ ...DRUM_PATTERNS,
18535
+ ...SONG_DRUM_PATTERNS,
18536
+ ...normalizeDrumPatterns(custom ?? {})
18537
+ });
18462
18538
  const audioCtx = options.audioContext ?? new AudioContext({ sampleRate: 44100 });
18463
18539
  const masterGain = audioCtx.createGain();
18464
18540
  masterGain.gain.value = options.masterVolume ?? 1;
@@ -19120,7 +19196,7 @@ var createDtmStudio = async (options = {}) => {
19120
19196
  await loadRequiredDrums(
19121
19197
  getDrumPatternKeys(
19122
19198
  meta.drum,
19123
- opts.drumPatterns ?? options.drumPatterns ?? DRUM_PATTERNS
19199
+ resolveDrumPatterns(opts.drumPatterns ?? options.drumPatterns)
19124
19200
  ),
19125
19201
  meta.drumFont || "FluidR3_GM_sf2_file:0"
19126
19202
  );
@@ -19211,7 +19287,7 @@ var createDtmStudio = async (options = {}) => {
19211
19287
  await loadRequiredDrums(
19212
19288
  getDrumPatternKeys(
19213
19289
  meta.drum,
19214
- opts.drumPatterns ?? options.drumPatterns ?? DRUM_PATTERNS
19290
+ resolveDrumPatterns(opts.drumPatterns ?? options.drumPatterns)
19215
19291
  )
19216
19292
  );
19217
19293
  }
@@ -19291,7 +19367,7 @@ var createDtmStudio = async (options = {}) => {
19291
19367
  await loadRequiredDrums(
19292
19368
  getDrumPatternKeys(
19293
19369
  meta.drum,
19294
- opts.drumPatterns ?? options.drumPatterns ?? DRUM_PATTERNS
19370
+ resolveDrumPatterns(opts.drumPatterns ?? options.drumPatterns)
19295
19371
  )
19296
19372
  );
19297
19373
  }