@onjmin/dtm 0.1.21 → 0.1.23

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
@@ -59,6 +59,7 @@ __export(index_exports, {
59
59
  createPianoRoll: () => createPianoRoll,
60
60
  createSequencer: () => createSequencer,
61
61
  createSingingVoices: () => createSingingVoices,
62
+ createSynth: () => createSynth,
62
63
  createVoiceRegistry: () => createVoiceRegistry,
63
64
  decomposeToMonophonic: () => decomposeToMonophonic,
64
65
  drawGrid: () => drawGrid,
@@ -72,6 +73,7 @@ __export(index_exports, {
72
73
  extractMidiPlacementsByTrack: () => extractMidiPlacementsByTrack,
73
74
  fetchSoundFontList: () => fetchSoundFontList,
74
75
  formatMmlMeta: () => formatMmlMeta,
76
+ freqFromPitch: () => freqFromPitch,
75
77
  generateRandomPattern: () => generateRandomPattern,
76
78
  getDrawOffset: () => getDrawOffset,
77
79
  getGridCanvas: () => getGridCanvas,
@@ -94,6 +96,8 @@ __export(index_exports, {
94
96
  parseLyrics: () => parseLyrics,
95
97
  parseMML: () => parseMML,
96
98
  parseMmlMeta: () => parseMmlMeta,
99
+ playMML: () => playMML,
100
+ playSingingMML: () => playSingingMML,
97
101
  setDrawOffset: () => setDrawOffset,
98
102
  setupRecorder: () => setupRecorder,
99
103
  shiftNotes: () => shiftNotes,
@@ -4024,7 +4028,7 @@ var PITCH_MAP2 = {
4024
4028
  b: 11
4025
4029
  };
4026
4030
  var clamp2 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
4027
- var META_DIRECTIVE = /#(inst|drum|volume)=([\w-]+)/gi;
4031
+ var META_DIRECTIVE = /#(inst|drum|volume|mode)=([\w-]+)/gi;
4028
4032
  var parseMmlMeta = (mml) => {
4029
4033
  const meta = {};
4030
4034
  for (const m of mml.matchAll(META_DIRECTIVE)) {
@@ -4034,6 +4038,10 @@ var parseMmlMeta = (mml) => {
4034
4038
  else if (key === "volume") {
4035
4039
  const v = Number.parseInt(m[2], 10);
4036
4040
  if (!Number.isNaN(v)) meta.volume = v;
4041
+ } else if (key === "mode") {
4042
+ if (m[2] === "simple" || m[2] === "advanced") {
4043
+ meta.mode = m[2];
4044
+ }
4037
4045
  }
4038
4046
  }
4039
4047
  return meta;
@@ -4044,6 +4052,7 @@ var formatMmlMeta = (meta) => {
4044
4052
  if (meta.instrument) parts.push(`#inst=${meta.instrument}`);
4045
4053
  if (meta.drum) parts.push(`#drum=${meta.drum}`);
4046
4054
  if (meta.volume !== void 0) parts.push(`#volume=${meta.volume}`);
4055
+ if (meta.mode) parts.push(`#mode=${meta.mode}`);
4047
4056
  return parts.join(" ");
4048
4057
  };
4049
4058
  var parseMML = (mml, options = {}) => {
@@ -4079,7 +4088,8 @@ var parseMML = (mml, options = {}) => {
4079
4088
  const part = rawPart.trim();
4080
4089
  if (part.startsWith("@")) {
4081
4090
  let idx = Number.parseInt(part.substring(1), 10);
4082
- if (clampTrackCount !== void 0 && idx >= clampTrackCount) idx = 2;
4091
+ if (clampTrackCount !== void 0 && idx >= clampTrackCount)
4092
+ idx = clampTrackCount - 1;
4083
4093
  trackIndex = idx;
4084
4094
  octave = 4;
4085
4095
  currentStep = 0;
@@ -4282,6 +4292,18 @@ var VOICE_IMAGES = {
4282
4292
  var STEPS_PER_BEAT2 = 48;
4283
4293
  var PLAN_TIME = 0.5;
4284
4294
  var TICK_INTERVAL_MS = 20;
4295
+ var resolveLoopPoint = (point, bpm, stepsPerBar, sps) => {
4296
+ if ("step" in point) {
4297
+ return point.step;
4298
+ }
4299
+ if ("bar" in point) {
4300
+ return Math.max(0, point.bar - 1) * stepsPerBar;
4301
+ }
4302
+ if ("seconds" in point) {
4303
+ return point.seconds / sps;
4304
+ }
4305
+ return 0;
4306
+ };
4285
4307
  var createSequencer = (options) => {
4286
4308
  let timeline = [];
4287
4309
  let startTime = 0;
@@ -4291,28 +4313,74 @@ var createSequencer = (options) => {
4291
4313
  let active = false;
4292
4314
  let fromStepValue = 0;
4293
4315
  let trackVolumeMap = /* @__PURE__ */ new Map();
4316
+ let isLooping = false;
4317
+ let loopStartStep = 0;
4318
+ let loopEndStep = 0;
4319
+ let loopStartSec = 0;
4320
+ let loopEndSec = 0;
4321
+ let loopDurationSec = 0;
4322
+ let loopStartIndex = 0;
4323
+ let loopBase = 0;
4324
+ let lastPlayStep = 0;
4294
4325
  const secondsPerStep = () => 60 / options.getBpm() / STEPS_PER_BEAT2;
4326
+ const getWrappedPlayStep = (time, sps) => {
4327
+ if (!isLooping || loopDurationSec <= 0 || time < loopEndSec) {
4328
+ return fromStepValue + time / sps;
4329
+ }
4330
+ const elapsedInLoop = (time - loopEndSec) % loopDurationSec;
4331
+ return loopStartStep + elapsedInLoop / sps;
4332
+ };
4295
4333
  const buildTimeline = (fromStep) => {
4296
4334
  timeline = [];
4297
4335
  trackVolumeMap = /* @__PURE__ */ new Map();
4298
4336
  const sps = secondsPerStep();
4337
+ const bpm = options.getBpm();
4338
+ const stepsPerBar = options.stepsPerBar;
4339
+ const loopOption = options.getLoop?.() ?? false;
4340
+ isLooping = !!loopOption;
4341
+ if (typeof loopOption === "object") {
4342
+ loopStartStep = loopOption.start ? resolveLoopPoint(loopOption.start, bpm, stepsPerBar, sps) : 0;
4343
+ const endVal = loopOption.end ? resolveLoopPoint(loopOption.end, bpm, stepsPerBar, sps) : null;
4344
+ loopEndStep = endVal !== null ? endVal : -1;
4345
+ } else {
4346
+ loopStartStep = 0;
4347
+ loopEndStep = -1;
4348
+ }
4349
+ const startLimit = isLooping ? Math.min(fromStep, loopStartStep) : fromStep;
4350
+ let maxEndStep = 0;
4299
4351
  for (const track of options.getTracks()) {
4300
4352
  trackVolumeMap.set(track.id, track.volume);
4301
4353
  for (const note of track.notes) {
4354
+ if (note.startStep < startLimit) continue;
4302
4355
  const relativeStart = note.startStep - fromStep;
4303
- if (relativeStart < 0) continue;
4304
- const velocity = note.velocity ?? DEFAULT_PLAYBACK_VELOCITY;
4356
+ const when = relativeStart * sps;
4357
+ const duration = note.durationSteps * sps;
4358
+ maxEndStep = Math.max(maxEndStep, note.startStep + note.durationSteps);
4305
4359
  timeline.push({
4306
4360
  trackId: track.id,
4307
4361
  pitch: note.pitch,
4308
4362
  volume: track.volume / 100,
4309
- velocity,
4310
- when: relativeStart * sps,
4311
- duration: note.durationSteps * sps
4363
+ velocity: note.velocity ?? DEFAULT_PLAYBACK_VELOCITY,
4364
+ when,
4365
+ duration
4312
4366
  });
4313
4367
  }
4314
4368
  }
4315
4369
  timeline.sort((a, b) => a.when - b.when);
4370
+ if (loopEndStep === -1) {
4371
+ loopEndStep = maxEndStep;
4372
+ }
4373
+ loopStartSec = (loopStartStep - fromStep) * sps;
4374
+ loopEndSec = (loopEndStep - fromStep) * sps;
4375
+ loopDurationSec = loopEndSec - loopStartSec;
4376
+ loopStartIndex = 0;
4377
+ while (loopStartIndex < timeline.length) {
4378
+ const noteStartStep = fromStep + timeline[loopStartIndex].when / sps;
4379
+ if (noteStartStep >= loopStartStep - 1e-4) {
4380
+ break;
4381
+ }
4382
+ loopStartIndex++;
4383
+ }
4316
4384
  };
4317
4385
  const scheduleTick = () => {
4318
4386
  const sps = secondsPerStep();
@@ -4321,9 +4389,16 @@ var createSequencer = (options) => {
4321
4389
  for (const track of options.getTracks()) {
4322
4390
  trackVolumeMap.set(track.id, track.volume);
4323
4391
  }
4324
- while (nowIndex < timeline.length) {
4325
- const ev = timeline[nowIndex];
4326
- const _when = ev.when - time;
4392
+ while (true) {
4393
+ let ev = timeline[nowIndex];
4394
+ if (nowIndex >= timeline.length || isLooping && ev && ev.when >= loopEndSec) {
4395
+ if (!isLooping || loopDurationSec <= 0) break;
4396
+ nowIndex = loopStartIndex;
4397
+ loopBase += loopDurationSec;
4398
+ ev = timeline[nowIndex];
4399
+ }
4400
+ if (!ev) break;
4401
+ const _when = ev.when + loopBase - time;
4327
4402
  if (_when > PLAN_TIME) break;
4328
4403
  nowIndex++;
4329
4404
  if (soloId && ev.trackId !== soloId) continue;
@@ -4341,7 +4416,7 @@ var createSequencer = (options) => {
4341
4416
  const pattern = options.getDrumPattern();
4342
4417
  if (pattern && pattern.length > 0) {
4343
4418
  const { stepsPerBar } = options;
4344
- const currentStep = (fromStepValue * sps + (options.getAudioTime() - startTime)) / sps;
4419
+ const currentStep = getWrappedPlayStep(time, sps);
4345
4420
  const currentStepInBar = currentStep % stepsPerBar;
4346
4421
  const nextStep = currentStepInBar + 4;
4347
4422
  const crossedBar = currentStepInBar < 4;
@@ -4358,19 +4433,44 @@ var createSequencer = (options) => {
4358
4433
  });
4359
4434
  }
4360
4435
  }
4361
- const last = timeline[timeline.length - 1];
4362
- const lastWhen = last?.when ?? 0;
4363
- const lastDuration = last?.duration ?? 0;
4364
- if (nowIndex >= timeline.length && time > lastWhen + lastDuration + 0.1) {
4365
- stop();
4366
- options.onEnd();
4436
+ if (time >= 0) {
4437
+ const currentStep = getWrappedPlayStep(time, sps);
4438
+ if (options.cues && options.cues.length > 0 && options.onCue) {
4439
+ const bpm = options.getBpm();
4440
+ const stepsPerBar = options.stepsPerBar;
4441
+ const isCueCrossed = (cueStep, prevStep, currStep) => {
4442
+ if (currStep >= prevStep) {
4443
+ return cueStep > prevStep && cueStep <= currStep;
4444
+ } else {
4445
+ const reachedEnd = cueStep > prevStep && cueStep <= loopEndStep;
4446
+ const startedNew = cueStep >= loopStartStep && cueStep <= currStep;
4447
+ return reachedEnd || startedNew;
4448
+ }
4449
+ };
4450
+ for (const cue of options.cues) {
4451
+ const cueStep = resolveLoopPoint(cue.time, bpm, stepsPerBar, sps);
4452
+ if (isCueCrossed(cueStep, lastPlayStep, currentStep)) {
4453
+ options.onCue(cue.id);
4454
+ }
4455
+ }
4456
+ }
4457
+ lastPlayStep = currentStep;
4458
+ }
4459
+ if (!isLooping) {
4460
+ const last = timeline[timeline.length - 1];
4461
+ const lastWhen = last?.when ?? 0;
4462
+ const lastDuration = last?.duration ?? 0;
4463
+ if (nowIndex >= timeline.length && time > lastWhen + lastDuration + 0.1) {
4464
+ stop();
4465
+ options.onEnd();
4466
+ }
4367
4467
  }
4368
4468
  };
4369
4469
  const animate = () => {
4370
4470
  if (!active) return;
4371
4471
  const sps = secondsPerStep();
4372
4472
  const time = options.getAudioTime() - startTime;
4373
- options.onTick(fromStepValue + time / sps);
4473
+ options.onTick(getWrappedPlayStep(time, sps));
4374
4474
  animationId = requestAnimationFrame(animate);
4375
4475
  };
4376
4476
  const stop = () => {
@@ -4392,7 +4492,17 @@ var createSequencer = (options) => {
4392
4492
  if (timeline.length === 0 && !options.getDrumPattern()?.length) return;
4393
4493
  active = true;
4394
4494
  startTime = options.getAudioTime() + START_DELAY;
4495
+ const sps = secondsPerStep();
4395
4496
  nowIndex = 0;
4497
+ while (nowIndex < timeline.length) {
4498
+ const noteStartStep = fromStepValue + timeline[nowIndex].when / sps;
4499
+ if (noteStartStep >= fromStepValue - 1e-4) {
4500
+ break;
4501
+ }
4502
+ nowIndex++;
4503
+ }
4504
+ loopBase = 0;
4505
+ lastPlayStep = fromStepValue - 1e-4;
4396
4506
  intervalId = setInterval(scheduleTick, TICK_INTERVAL_MS);
4397
4507
  animationId = requestAnimationFrame(animate);
4398
4508
  };
@@ -4404,6 +4514,73 @@ var createSequencer = (options) => {
4404
4514
  };
4405
4515
  };
4406
4516
 
4517
+ // src/synth.ts
4518
+ var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
4519
+ var createSynth = (ctx, destination = ctx.destination) => {
4520
+ const playNote = (e) => {
4521
+ const osc = ctx.createOscillator();
4522
+ const gain = ctx.createGain();
4523
+ osc.type = "square";
4524
+ osc.frequency.value = freqFromPitch(e.pitch);
4525
+ const t0 = ctx.currentTime + e.when;
4526
+ const peak = Math.max(1e-4, 0.06 * e.volume * 1.5);
4527
+ gain.gain.setValueAtTime(peak, t0);
4528
+ gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
4529
+ osc.connect(gain);
4530
+ if (typeof ctx.createStereoPanner === "function" && e.pan) {
4531
+ const panner = ctx.createStereoPanner();
4532
+ panner.pan.value = Math.max(-1, Math.min(1, e.pan));
4533
+ gain.connect(panner);
4534
+ panner.connect(destination);
4535
+ } else {
4536
+ gain.connect(destination);
4537
+ }
4538
+ osc.start(t0);
4539
+ osc.stop(t0 + e.duration + 0.02);
4540
+ };
4541
+ const playDrum = (e) => {
4542
+ const t0 = ctx.currentTime + e.when;
4543
+ const vol = Math.max(1e-4, Math.min(1, e.velocity));
4544
+ const isKick = e.pitch === 35 || e.pitch === 36;
4545
+ const isSnareLike = e.pitch === 38 || e.pitch === 39 || e.pitch === 40;
4546
+ if (isKick) {
4547
+ const osc = ctx.createOscillator();
4548
+ const g2 = ctx.createGain();
4549
+ osc.frequency.setValueAtTime(150, t0);
4550
+ osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
4551
+ g2.gain.setValueAtTime(vol * 0.9, t0);
4552
+ g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
4553
+ osc.connect(g2).connect(destination);
4554
+ osc.start(t0);
4555
+ osc.stop(t0 + 0.2);
4556
+ osc.onended = () => osc.disconnect();
4557
+ return;
4558
+ }
4559
+ const dur = isSnareLike ? 0.18 : 0.05;
4560
+ const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
4561
+ const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
4562
+ const data = buffer.getChannelData(0);
4563
+ for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
4564
+ const src = ctx.createBufferSource();
4565
+ src.buffer = buffer;
4566
+ const filter = ctx.createBiquadFilter();
4567
+ filter.type = isSnareLike ? "bandpass" : "highpass";
4568
+ filter.frequency.value = isSnareLike ? 2e3 : 8e3;
4569
+ const g = ctx.createGain();
4570
+ g.gain.setValueAtTime(vol * (isSnareLike ? 0.7 : 0.4), t0);
4571
+ g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
4572
+ src.connect(filter).connect(g).connect(destination);
4573
+ src.start(t0);
4574
+ src.stop(t0 + dur);
4575
+ src.onended = () => {
4576
+ src.disconnect();
4577
+ filter.disconnect();
4578
+ g.disconnect();
4579
+ };
4580
+ };
4581
+ return { playNote, playDrum };
4582
+ };
4583
+
4407
4584
  // src/styles.ts
4408
4585
  var STYLE_ID = "dtm-daw-styles";
4409
4586
  var DAW_CSS = `
@@ -5074,6 +5251,13 @@ var DAW_CSS = `
5074
5251
  font-size: 13px;
5075
5252
  line-height: 1.6;
5076
5253
  }
5254
+ .dtm-modal-body a {
5255
+ color: var(--dtm-primary);
5256
+ text-decoration: underline;
5257
+ }
5258
+ .dtm-modal-body a:hover {
5259
+ color: var(--dtm-accent);
5260
+ }
5077
5261
  .dtm-modal-body h4 {
5078
5262
  margin: 12px 0 6px 0;
5079
5263
  color: var(--dtm-primary);
@@ -5528,7 +5712,6 @@ var showBalloon = (balloonEl) => {
5528
5712
  hideActiveBalloon();
5529
5713
  }, 3e3);
5530
5714
  };
5531
- var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
5532
5715
  var mountMmlPlayer = (target, mml, options = {}) => {
5533
5716
  injectStyles(target.ownerDocument ?? document);
5534
5717
  const {
@@ -5599,68 +5782,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
5599
5782
  if (!audioCtx) audioCtx = new AudioContext();
5600
5783
  return audioCtx;
5601
5784
  };
5602
- const synthPlay = (e) => {
5603
- const ctx = ensureCtx();
5604
- const osc = ctx.createOscillator();
5605
- const gain = ctx.createGain();
5606
- osc.type = "square";
5607
- osc.frequency.value = freqFromPitch(e.pitch);
5608
- const t0 = ctx.currentTime + e.when;
5609
- const peak = Math.max(1e-4, 0.06 * e.volume * 1.5);
5610
- gain.gain.setValueAtTime(peak, t0);
5611
- gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
5612
- osc.connect(gain);
5613
- if (typeof ctx.createStereoPanner === "function" && e.pan) {
5614
- const panner = ctx.createStereoPanner();
5615
- panner.pan.value = Math.max(-1, Math.min(1, e.pan));
5616
- gain.connect(panner);
5617
- panner.connect(ctx.destination);
5618
- } else {
5619
- gain.connect(ctx.destination);
5620
- }
5621
- osc.start(t0);
5622
- osc.stop(t0 + e.duration + 0.02);
5623
- };
5624
- const drumSynth = (e) => {
5625
- const ctx = ensureCtx();
5626
- const t0 = ctx.currentTime + e.when;
5627
- const vol = Math.max(1e-4, Math.min(1, e.velocity));
5628
- const isKick = e.pitch === 35 || e.pitch === 36;
5629
- const isSnareLike = e.pitch === 38 || e.pitch === 39 || e.pitch === 40;
5630
- if (isKick) {
5631
- const osc = ctx.createOscillator();
5632
- const g2 = ctx.createGain();
5633
- osc.frequency.setValueAtTime(150, t0);
5634
- osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
5635
- g2.gain.setValueAtTime(vol * 0.9, t0);
5636
- g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
5637
- osc.connect(g2).connect(ctx.destination);
5638
- osc.start(t0);
5639
- osc.stop(t0 + 0.2);
5640
- osc.onended = () => osc.disconnect();
5641
- return;
5642
- }
5643
- const dur = isSnareLike ? 0.18 : 0.05;
5644
- const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
5645
- const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
5646
- const data = buffer.getChannelData(0);
5647
- for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
5648
- const src = ctx.createBufferSource();
5649
- src.buffer = buffer;
5650
- const filter = ctx.createBiquadFilter();
5651
- filter.type = isSnareLike ? "bandpass" : "highpass";
5652
- filter.frequency.value = isSnareLike ? 2e3 : 8e3;
5653
- const g = ctx.createGain();
5654
- g.gain.setValueAtTime(vol * (isSnareLike ? 0.7 : 0.4), t0);
5655
- g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
5656
- src.connect(filter).connect(g).connect(ctx.destination);
5657
- src.start(t0);
5658
- src.stop(t0 + dur);
5659
- src.onended = () => {
5660
- src.disconnect();
5661
- filter.disconnect();
5662
- g.disconnect();
5663
- };
5785
+ let synthInstance = null;
5786
+ const ensureSynth = () => {
5787
+ if (!synthInstance) synthInstance = createSynth(ensureCtx());
5788
+ return synthInstance;
5664
5789
  };
5665
5790
  let voices = null;
5666
5791
  const ensureVoices = () => {
@@ -6030,12 +6155,12 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6030
6155
  if (em) jumpEmojiAt(em, e.when);
6031
6156
  if (lyricTracks.has(trackIdx)) return;
6032
6157
  options.onPlayNote?.(e);
6033
- if (useSynth) synthPlay(e);
6158
+ if (useSynth) ensureSynth().playNote(e);
6034
6159
  },
6035
6160
  onPlayDrum: (e) => {
6036
6161
  const velocity = e.velocity * (trackVolume / 100);
6037
6162
  options.onPlayDrum?.({ ...e, velocity });
6038
- if (useSynth) drumSynth({ ...e, velocity });
6163
+ if (useSynth) ensureSynth().playDrum({ ...e, velocity });
6039
6164
  },
6040
6165
  onTick: (step) => {
6041
6166
  renderPlayhead(step);
@@ -6110,13 +6235,19 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6110
6235
  if (activePlayer && activePlayer !== instance) activePlayer.stop();
6111
6236
  activePlayer = instance;
6112
6237
  setPlayingUI(true);
6113
- void options.onResumeAudio?.();
6114
- if (useSynth) {
6115
- const ctx = ensureCtx();
6116
- if (ctx.state === "suspended") void ctx.resume();
6117
- }
6118
- if (voicesAvailable && lyricTracks.size > 0) ensureVoices().reset();
6119
- void startWhenReady();
6238
+ void (async () => {
6239
+ const resumes = [];
6240
+ const r = options.onResumeAudio?.();
6241
+ if (r) resumes.push(r);
6242
+ if (useSynth) {
6243
+ const ctx = ensureCtx();
6244
+ if (ctx.state === "suspended") resumes.push(ctx.resume());
6245
+ }
6246
+ if (resumes.length > 0) await Promise.all(resumes);
6247
+ if (!playing || activePlayer !== instance) return;
6248
+ if (voicesAvailable && lyricTracks.size > 0) ensureVoices().reset();
6249
+ await startWhenReady();
6250
+ })();
6120
6251
  };
6121
6252
  const stop = () => {
6122
6253
  if (!playing) return;
@@ -7131,8 +7262,8 @@ var mountDAW = (target, options = {}) => {
7131
7262
  stepsPerBar: renderConfig.stepsPerBar
7132
7263
  });
7133
7264
  const play = async () => {
7134
- options.onResumeAudio?.();
7135
7265
  if (playbackState === "playing") return;
7266
+ await options.onResumeAudio?.();
7136
7267
  const fromStep = playbackState === "paused" ? pausedPlayStep : playStartStep;
7137
7268
  options.singingVoices?.reset();
7138
7269
  const lyricMap = buildLyricsMap();
@@ -7486,7 +7617,8 @@ var mountDAW = (target, options = {}) => {
7486
7617
  const metaLine = formatMmlMeta({
7487
7618
  instrument: currentInstrument || void 0,
7488
7619
  drum: currentDrumPattern !== "none" ? currentDrumPattern : void 0,
7489
- volume: masterVolume
7620
+ volume: masterVolume,
7621
+ mode
7490
7622
  });
7491
7623
  if (refs.decomposeChordToggle.checked) {
7492
7624
  const ignoreHeavy = refs.ignoreChordHeavyToggle.checked;
@@ -8289,6 +8421,123 @@ var INSTRUMENT_PRESETS = {
8289
8421
  }
8290
8422
  };
8291
8423
 
8424
+ // src/headless-player.ts
8425
+ var STEPS_PER_BAR2 = 192;
8426
+ var playMML = (mml, options = {}) => {
8427
+ const { placements, bpm: parsedBpm, meta } = parseMML(mml);
8428
+ const bpm = parsedBpm ?? options.defaultBpm ?? DEFAULT_BPM;
8429
+ const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
8430
+ const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
8431
+ let masterVolume = meta.volume ?? options.volume ?? 100;
8432
+ const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
8433
+ (a, b) => a - b
8434
+ );
8435
+ const seqTracks = trackIndices.map((index) => {
8436
+ let id = 0;
8437
+ const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
8438
+ id: id++,
8439
+ startStep: p.startStep,
8440
+ durationSteps: p.durationSteps,
8441
+ pitch: p.pitch,
8442
+ velocity: 100
8443
+ }));
8444
+ return { id: String(index), volume: masterVolume, notes };
8445
+ });
8446
+ const ownsCtx = !options.audioContext;
8447
+ const ctx = options.audioContext ?? new AudioContext();
8448
+ const destination = options.destination ?? ctx.destination;
8449
+ const useSynth = options.synth ?? !options.onPlayNote;
8450
+ const synth = useSynth ? createSynth(ctx, destination) : null;
8451
+ const pauseWhenHidden = options.pauseWhenHidden ?? ownsCtx;
8452
+ let playing = false;
8453
+ const seq = createSequencer({
8454
+ getTracks: () => seqTracks,
8455
+ getBpm: () => bpm,
8456
+ getPlayStartStep: () => 0,
8457
+ getDrumPattern: () => drumPattern,
8458
+ getSoloTrackId: () => null,
8459
+ getLoop: () => options.loop ?? false,
8460
+ cues: options.cues,
8461
+ onCue: options.onCue,
8462
+ getAudioTime: () => ctx.currentTime,
8463
+ onPlayNote: (e) => {
8464
+ options.onPlayNote?.(e);
8465
+ synth?.playNote(e);
8466
+ },
8467
+ onPlayDrum: (e) => {
8468
+ const velocity = e.velocity * (masterVolume / 100);
8469
+ options.onPlayDrum?.({ ...e, velocity });
8470
+ synth?.playDrum({ ...e, velocity });
8471
+ },
8472
+ onTick: () => {
8473
+ },
8474
+ onEnd: () => finish(),
8475
+ stepsPerBar: STEPS_PER_BAR2
8476
+ });
8477
+ const finish = () => {
8478
+ if (!playing) return;
8479
+ playing = false;
8480
+ options.onStop?.();
8481
+ };
8482
+ const onVisibilityChange = () => {
8483
+ if (!playing) return;
8484
+ if (document.hidden) {
8485
+ void ctx.suspend();
8486
+ } else if (ctx.state === "suspended") {
8487
+ void ctx.resume();
8488
+ }
8489
+ };
8490
+ if (pauseWhenHidden && typeof document !== "undefined") {
8491
+ document.addEventListener("visibilitychange", onVisibilityChange);
8492
+ }
8493
+ playing = true;
8494
+ void (async () => {
8495
+ const resumes = [];
8496
+ const r = options.onResumeAudio?.();
8497
+ if (r) resumes.push(r);
8498
+ if (ctx.state === "suspended") resumes.push(ctx.resume());
8499
+ if (resumes.length > 0) await Promise.all(resumes);
8500
+ if (!playing) return;
8501
+ seq.start(0);
8502
+ })();
8503
+ const stop = () => {
8504
+ if (!playing) return;
8505
+ seq.stop();
8506
+ finish();
8507
+ };
8508
+ const setVolume = (volume) => {
8509
+ masterVolume = volume;
8510
+ for (const t of seqTracks) t.volume = volume;
8511
+ };
8512
+ const suspend = () => ctx.suspend();
8513
+ const resume = () => ctx.resume();
8514
+ const destroy = () => {
8515
+ seq.stop();
8516
+ playing = false;
8517
+ if (pauseWhenHidden && typeof document !== "undefined") {
8518
+ document.removeEventListener("visibilitychange", onVisibilityChange);
8519
+ }
8520
+ if (ownsCtx) void ctx.close();
8521
+ };
8522
+ return {
8523
+ stop,
8524
+ isPlaying: () => playing,
8525
+ setVolume,
8526
+ suspend,
8527
+ resume,
8528
+ destroy
8529
+ };
8530
+ };
8531
+
8532
+ // src/headless-singing-player.ts
8533
+ var playSingingMML = (_mml, _options = {}) => {
8534
+ return Promise.reject(
8535
+ new Error(
8536
+ "playSingingMML is not implemented yet. See implementation notes at the top of headless-singing-player.ts."
8537
+ )
8538
+ );
8539
+ };
8540
+
8292
8541
  // src/piano-roll.ts
8293
8542
  var createPianoRoll = (options, handlers) => {
8294
8543
  const {
@@ -8741,7 +8990,23 @@ var DEFAULT_CDN = {
8741
8990
  soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs"
8742
8991
  };
8743
8992
  var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
8744
- var TRACK_ROLES = ["melody", "submelody", "bass", "chord"];
8993
+ var TRACK_ROLES = [
8994
+ "melody",
8995
+ "submelody",
8996
+ "bass",
8997
+ "chord",
8998
+ "t4",
8999
+ "t5",
9000
+ "t6",
9001
+ "t7",
9002
+ "t8",
9003
+ "t9",
9004
+ "t10",
9005
+ "t11",
9006
+ "t12",
9007
+ "t13",
9008
+ "t14"
9009
+ ];
8745
9010
  var resolveDefaultVoiceWorkerUrl = () => {
8746
9011
  try {
8747
9012
  return new URL("./voice-worker.js", import_meta.url).href;
@@ -8773,7 +9038,8 @@ var createDtmStudio = async (options = {}) => {
8773
9038
  drumGain.gain.value = options.drumVolume ?? 1;
8774
9039
  drumGain.connect(audioCtx.destination);
8775
9040
  const resumeAudio = () => {
8776
- if (audioCtx.state === "suspended") void audioCtx.resume();
9041
+ if (audioCtx.state === "suspended") return audioCtx.resume();
9042
+ return Promise.resolve();
8777
9043
  };
8778
9044
  const eng = options.engines ?? {};
8779
9045
  const [SoundFont, SoundFont_drum, SoundFont_list] = await Promise.all([
@@ -8877,45 +9143,77 @@ var createDtmStudio = async (options = {}) => {
8877
9143
  })();
8878
9144
  let nameToKey = {};
8879
9145
  const soundFonts = /* @__PURE__ */ new Map();
8880
- const loadedKeyByTrack = /* @__PURE__ */ new Map();
8881
- const loadSoundFont = async (instrumentKey, trackId) => {
8882
- if (loadedKeyByTrack.get(trackId) === instrumentKey) return;
8883
- try {
8884
- const fullName = `${instrumentKey}_${SOUNDFONT_NAME}`;
8885
- soundFonts.set(
8886
- trackId,
8887
- await SoundFont.load({
8888
- ctx: audioCtx,
8889
- fontName: `_tone_${fullName}`,
8890
- url: SoundFont.toURL(fullName)
8891
- })
8892
- );
8893
- loadedKeyByTrack.set(trackId, instrumentKey);
8894
- } catch (e) {
9146
+ const loadingByKey = /* @__PURE__ */ new Map();
9147
+ const loadInstrument = (instrumentKey) => {
9148
+ if (soundFonts.has(instrumentKey)) return Promise.resolve();
9149
+ const inflight = loadingByKey.get(instrumentKey);
9150
+ if (inflight) return inflight;
9151
+ const fullName = `${instrumentKey}_${SOUNDFONT_NAME}`;
9152
+ const p = SoundFont.load({
9153
+ ctx: audioCtx,
9154
+ fontName: `_tone_${fullName}`,
9155
+ url: SoundFont.toURL(fullName)
9156
+ }).then((sf) => {
9157
+ soundFonts.set(instrumentKey, sf);
9158
+ }).catch((e) => {
8895
9159
  console.error(`[dtm] \u697D\u5668 "${instrumentKey}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557`, e);
8896
- }
9160
+ }).finally(() => {
9161
+ loadingByKey.delete(instrumentKey);
9162
+ });
9163
+ loadingByKey.set(instrumentKey, p);
9164
+ return p;
8897
9165
  };
8898
9166
  const defaultPreset = options.defaultPreset ?? "retro_game";
9167
+ const getRoleForTrackIndex = (idx, mode = "simple") => {
9168
+ if (mode === "simple") {
9169
+ if (idx === 0) return "melody";
9170
+ if (idx === 1) return "submelody";
9171
+ if (idx === 2) return "bass";
9172
+ return "chord";
9173
+ } else {
9174
+ return TRACK_ROLES[idx] ?? `t${idx}`;
9175
+ }
9176
+ };
9177
+ const getRoleFromTrackId = (trackId, mode = "simple") => {
9178
+ if (trackId === "melody" || trackId === "submelody" || trackId === "bass" || trackId === "chord") {
9179
+ return trackId;
9180
+ }
9181
+ if (trackId.startsWith("t")) {
9182
+ const idx = Number(trackId.substring(1));
9183
+ if (!isNaN(idx)) {
9184
+ return getRoleForTrackIndex(idx, mode);
9185
+ }
9186
+ }
9187
+ return trackId;
9188
+ };
8899
9189
  const instrumentNameFor = (preset, trackId) => preset[trackId] ?? preset.melody;
8900
- const loadPreset = async (presetKey, trackIds = [...TRACK_ROLES]) => {
9190
+ const resolveSoundFont = (presetKey, trackId, mode = "simple") => {
9191
+ const preset = INSTRUMENT_PRESETS[presetKey];
9192
+ if (!preset) return void 0;
9193
+ const role = getRoleFromTrackId(trackId, mode);
9194
+ const key = nameToKey[instrumentNameFor(preset, role)];
9195
+ return key ? soundFonts.get(key) : void 0;
9196
+ };
9197
+ const loadPreset = async (presetKey, trackIds = [...TRACK_ROLES], mode = "simple") => {
8901
9198
  const preset = INSTRUMENT_PRESETS[presetKey];
8902
9199
  if (!preset) return;
8903
9200
  await listReady;
8904
- await Promise.all(
8905
- trackIds.map((trackId) => {
8906
- const key = nameToKey[instrumentNameFor(preset, trackId)];
8907
- return key ? loadSoundFont(key, trackId) : Promise.resolve();
8908
- })
8909
- );
9201
+ const keys = /* @__PURE__ */ new Set();
9202
+ for (const trackId of trackIds) {
9203
+ const role = getRoleFromTrackId(trackId, mode);
9204
+ const key = nameToKey[instrumentNameFor(preset, role)];
9205
+ if (key) keys.add(key);
9206
+ }
9207
+ await Promise.all([...keys].map((key) => loadInstrument(key)));
8910
9208
  };
8911
- const applyPreset = async (daw, presetKey, trackIds, loadingTarget) => {
9209
+ const applyPreset = async (daw, presetKey, trackIds, loadingTarget, mode = "simple") => {
8912
9210
  const wasPlaying = daw.getPlaybackState() === "playing";
8913
9211
  if (wasPlaying) daw.pause();
8914
9212
  const overlay = loadingTarget ? showLoadingOverlay(loadingTarget) : null;
8915
9213
  daw.setLoading?.(true);
8916
9214
  try {
8917
9215
  daw.setInstrument(presetKey);
8918
- await loadPreset(presetKey, trackIds);
9216
+ await loadPreset(presetKey, trackIds, mode);
8919
9217
  } finally {
8920
9218
  overlay?.remove();
8921
9219
  daw.setLoading?.(false);
@@ -8950,8 +9248,10 @@ var createDtmStudio = async (options = {}) => {
8950
9248
  const key = select.value;
8951
9249
  opts.onChange?.(key);
8952
9250
  const trackIds = opts.getTrackIds?.() ?? [...TRACK_ROLES];
9251
+ const isAdvanced = trackIds.includes("t0");
9252
+ const mode = isAdvanced ? "advanced" : "simple";
8953
9253
  try {
8954
- await applyPreset(daw, key, trackIds, opts.loadingTarget);
9254
+ await applyPreset(daw, key, trackIds, opts.loadingTarget, mode);
8955
9255
  } finally {
8956
9256
  busy = false;
8957
9257
  }
@@ -8976,18 +9276,6 @@ var createDtmStudio = async (options = {}) => {
8976
9276
  await listReady;
8977
9277
  nameToKey = await buildNameToKeyMapping();
8978
9278
  await Promise.all([drumReady, loadPreset(defaultPreset)]);
8979
- const playNote = (e) => {
8980
- const sf = soundFonts.get(e.trackId);
8981
- if (!sf) return;
8982
- sf.play({
8983
- ctx: audioCtx,
8984
- destination: masterGain,
8985
- pitch: e.pitch,
8986
- volume: e.volume,
8987
- when: e.when,
8988
- duration: e.duration
8989
- });
8990
- };
8991
9279
  const playDrum = (e) => {
8992
9280
  if (!SoundFont_drum.font) return;
8993
9281
  SoundFont_drum.play({
@@ -8999,19 +9287,6 @@ var createDtmStudio = async (options = {}) => {
8999
9287
  duration: e.duration
9000
9288
  });
9001
9289
  };
9002
- const sfForPlayerTrack = (trackId) => soundFonts.get(TRACK_ROLES[Number(trackId)] ?? "") ?? soundFonts.get(`t${trackId}`);
9003
- const playPlayerNote = (e) => {
9004
- const sf = sfForPlayerTrack(e.trackId);
9005
- if (!sf) return;
9006
- sf.play({
9007
- ctx: audioCtx,
9008
- destination: masterGain,
9009
- pitch: e.pitch,
9010
- volume: e.volume,
9011
- when: e.when,
9012
- duration: e.duration
9013
- });
9014
- };
9015
9290
  const editorPresetSelects = /* @__PURE__ */ new WeakMap();
9016
9291
  const mountedEditors = [];
9017
9292
  const mountedPlayers = [];
@@ -9020,6 +9295,25 @@ var createDtmStudio = async (options = {}) => {
9020
9295
  const { preset, presetUI, ...dawOverrides } = opts;
9021
9296
  const tracks = dawOverrides.tracks ?? TRACKS_SIMPLE;
9022
9297
  const trackIds = tracks.map((t) => t.id);
9298
+ const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
9299
+ let editorPreset = presetKey;
9300
+ const isAdvancedMode = dawOverrides.mode === "advanced";
9301
+ const playNote = (e) => {
9302
+ const sf = resolveSoundFont(
9303
+ editorPreset,
9304
+ e.trackId,
9305
+ isAdvancedMode ? "advanced" : "simple"
9306
+ );
9307
+ if (!sf) return;
9308
+ sf.play({
9309
+ ctx: audioCtx,
9310
+ destination: masterGain,
9311
+ pitch: e.pitch,
9312
+ volume: e.volume,
9313
+ when: e.when,
9314
+ duration: e.duration
9315
+ });
9316
+ };
9023
9317
  const base = {
9024
9318
  getAudioTime: () => audioCtx.currentTime,
9025
9319
  onResumeAudio: resumeAudio,
@@ -9032,7 +9326,6 @@ var createDtmStudio = async (options = {}) => {
9032
9326
  };
9033
9327
  const daw = mountDAW(target, base);
9034
9328
  mountedEditors.push(daw);
9035
- const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
9036
9329
  const wantPresetUI = presetUI ?? features.presetUI;
9037
9330
  let presetSelect = null;
9038
9331
  if (wantPresetUI) {
@@ -9043,13 +9336,21 @@ var createDtmStudio = async (options = {}) => {
9043
9336
  getTrackIds: () => trackIds,
9044
9337
  value: presetKey,
9045
9338
  loadingTarget: rollEl ?? target,
9046
- position: "prepend"
9339
+ position: "prepend",
9340
+ // 楽器変更時、このエディタの発音解決が使うプリセットも追従させる。
9341
+ onChange: (key) => {
9342
+ editorPreset = key;
9343
+ }
9047
9344
  });
9048
9345
  editorPresetSelects.set(target, presetSelect);
9049
9346
  }
9050
9347
  daw.setInstrument(presetKey);
9051
9348
  daw.setLoading?.(true);
9052
- void loadPreset(presetKey, trackIds).finally(() => {
9349
+ void loadPreset(
9350
+ presetKey,
9351
+ trackIds,
9352
+ isAdvancedMode ? "advanced" : "simple"
9353
+ ).finally(() => {
9053
9354
  daw.setLoading?.(false);
9054
9355
  });
9055
9356
  const destroy = () => {
@@ -9147,10 +9448,43 @@ var createDtmStudio = async (options = {}) => {
9147
9448
  return instance;
9148
9449
  };
9149
9450
  const mountPlayer = (target, mml, opts = {}) => {
9150
- const meta = parseMML(mml, {}).meta ?? {};
9151
- if (meta.instrument && INSTRUMENT_PRESETS[meta.instrument]) {
9152
- void loadPreset(meta.instrument);
9153
- }
9451
+ const parsed = parseMML(mml, {});
9452
+ const meta = parsed.meta ?? {};
9453
+ const playerPreset = meta.instrument && INSTRUMENT_PRESETS[meta.instrument] ? meta.instrument : defaultPreset;
9454
+ const isAdvancedMode = meta.mode === "advanced";
9455
+ const trackIndices = [
9456
+ ...new Set(parsed.placements.map((p) => p.trackIndex))
9457
+ ];
9458
+ const trackIds = trackIndices.map(
9459
+ (idx) => getRoleForTrackIndex(idx, isAdvancedMode ? "advanced" : "simple")
9460
+ );
9461
+ const loadTrackIds = trackIds.length > 0 ? trackIds : [...TRACK_ROLES];
9462
+ void loadPreset(
9463
+ playerPreset,
9464
+ loadTrackIds,
9465
+ isAdvancedMode ? "advanced" : "simple"
9466
+ );
9467
+ const playPlayerNote = (e) => {
9468
+ const idx = Number(e.trackId);
9469
+ const role = getRoleForTrackIndex(
9470
+ idx,
9471
+ isAdvancedMode ? "advanced" : "simple"
9472
+ );
9473
+ const sf = resolveSoundFont(
9474
+ playerPreset,
9475
+ role,
9476
+ isAdvancedMode ? "advanced" : "simple"
9477
+ );
9478
+ if (!sf) return;
9479
+ sf.play({
9480
+ ctx: audioCtx,
9481
+ destination: masterGain,
9482
+ pitch: e.pitch,
9483
+ volume: e.volume,
9484
+ when: e.when,
9485
+ duration: e.duration
9486
+ });
9487
+ };
9154
9488
  const player = mountMmlPlayer(target, mml, {
9155
9489
  getAudioTime: () => audioCtx.currentTime,
9156
9490
  onResumeAudio: resumeAudio,
@@ -9229,6 +9563,7 @@ var createDtmStudio = async (options = {}) => {
9229
9563
  createPianoRoll,
9230
9564
  createSequencer,
9231
9565
  createSingingVoices,
9566
+ createSynth,
9232
9567
  createVoiceRegistry,
9233
9568
  decomposeToMonophonic,
9234
9569
  drawGrid,
@@ -9242,6 +9577,7 @@ var createDtmStudio = async (options = {}) => {
9242
9577
  extractMidiPlacementsByTrack,
9243
9578
  fetchSoundFontList,
9244
9579
  formatMmlMeta,
9580
+ freqFromPitch,
9245
9581
  generateRandomPattern,
9246
9582
  getDrawOffset,
9247
9583
  getGridCanvas,
@@ -9264,6 +9600,8 @@ var createDtmStudio = async (options = {}) => {
9264
9600
  parseLyrics,
9265
9601
  parseMML,
9266
9602
  parseMmlMeta,
9603
+ playMML,
9604
+ playSingingMML,
9267
9605
  setDrawOffset,
9268
9606
  setupRecorder,
9269
9607
  shiftNotes,