@onjmin/dtm 0.1.21 → 0.1.22

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,
@@ -4282,6 +4286,18 @@ var VOICE_IMAGES = {
4282
4286
  var STEPS_PER_BEAT2 = 48;
4283
4287
  var PLAN_TIME = 0.5;
4284
4288
  var TICK_INTERVAL_MS = 20;
4289
+ var resolveLoopPoint = (point, bpm, stepsPerBar, sps) => {
4290
+ if ("step" in point) {
4291
+ return point.step;
4292
+ }
4293
+ if ("bar" in point) {
4294
+ return Math.max(0, point.bar - 1) * stepsPerBar;
4295
+ }
4296
+ if ("seconds" in point) {
4297
+ return point.seconds / sps;
4298
+ }
4299
+ return 0;
4300
+ };
4285
4301
  var createSequencer = (options) => {
4286
4302
  let timeline = [];
4287
4303
  let startTime = 0;
@@ -4291,28 +4307,74 @@ var createSequencer = (options) => {
4291
4307
  let active = false;
4292
4308
  let fromStepValue = 0;
4293
4309
  let trackVolumeMap = /* @__PURE__ */ new Map();
4310
+ let isLooping = false;
4311
+ let loopStartStep = 0;
4312
+ let loopEndStep = 0;
4313
+ let loopStartSec = 0;
4314
+ let loopEndSec = 0;
4315
+ let loopDurationSec = 0;
4316
+ let loopStartIndex = 0;
4317
+ let loopBase = 0;
4318
+ let lastPlayStep = 0;
4294
4319
  const secondsPerStep = () => 60 / options.getBpm() / STEPS_PER_BEAT2;
4320
+ const getWrappedPlayStep = (time, sps) => {
4321
+ if (!isLooping || loopDurationSec <= 0 || time < loopEndSec) {
4322
+ return fromStepValue + time / sps;
4323
+ }
4324
+ const elapsedInLoop = (time - loopEndSec) % loopDurationSec;
4325
+ return loopStartStep + elapsedInLoop / sps;
4326
+ };
4295
4327
  const buildTimeline = (fromStep) => {
4296
4328
  timeline = [];
4297
4329
  trackVolumeMap = /* @__PURE__ */ new Map();
4298
4330
  const sps = secondsPerStep();
4331
+ const bpm = options.getBpm();
4332
+ const stepsPerBar = options.stepsPerBar;
4333
+ const loopOption = options.getLoop?.() ?? false;
4334
+ isLooping = !!loopOption;
4335
+ if (typeof loopOption === "object") {
4336
+ loopStartStep = loopOption.start ? resolveLoopPoint(loopOption.start, bpm, stepsPerBar, sps) : 0;
4337
+ const endVal = loopOption.end ? resolveLoopPoint(loopOption.end, bpm, stepsPerBar, sps) : null;
4338
+ loopEndStep = endVal !== null ? endVal : -1;
4339
+ } else {
4340
+ loopStartStep = 0;
4341
+ loopEndStep = -1;
4342
+ }
4343
+ const startLimit = isLooping ? Math.min(fromStep, loopStartStep) : fromStep;
4344
+ let maxEndStep = 0;
4299
4345
  for (const track of options.getTracks()) {
4300
4346
  trackVolumeMap.set(track.id, track.volume);
4301
4347
  for (const note of track.notes) {
4348
+ if (note.startStep < startLimit) continue;
4302
4349
  const relativeStart = note.startStep - fromStep;
4303
- if (relativeStart < 0) continue;
4304
- const velocity = note.velocity ?? DEFAULT_PLAYBACK_VELOCITY;
4350
+ const when = relativeStart * sps;
4351
+ const duration = note.durationSteps * sps;
4352
+ maxEndStep = Math.max(maxEndStep, note.startStep + note.durationSteps);
4305
4353
  timeline.push({
4306
4354
  trackId: track.id,
4307
4355
  pitch: note.pitch,
4308
4356
  volume: track.volume / 100,
4309
- velocity,
4310
- when: relativeStart * sps,
4311
- duration: note.durationSteps * sps
4357
+ velocity: note.velocity ?? DEFAULT_PLAYBACK_VELOCITY,
4358
+ when,
4359
+ duration
4312
4360
  });
4313
4361
  }
4314
4362
  }
4315
4363
  timeline.sort((a, b) => a.when - b.when);
4364
+ if (loopEndStep === -1) {
4365
+ loopEndStep = maxEndStep;
4366
+ }
4367
+ loopStartSec = (loopStartStep - fromStep) * sps;
4368
+ loopEndSec = (loopEndStep - fromStep) * sps;
4369
+ loopDurationSec = loopEndSec - loopStartSec;
4370
+ loopStartIndex = 0;
4371
+ while (loopStartIndex < timeline.length) {
4372
+ const noteStartStep = fromStep + timeline[loopStartIndex].when / sps;
4373
+ if (noteStartStep >= loopStartStep - 1e-4) {
4374
+ break;
4375
+ }
4376
+ loopStartIndex++;
4377
+ }
4316
4378
  };
4317
4379
  const scheduleTick = () => {
4318
4380
  const sps = secondsPerStep();
@@ -4321,9 +4383,16 @@ var createSequencer = (options) => {
4321
4383
  for (const track of options.getTracks()) {
4322
4384
  trackVolumeMap.set(track.id, track.volume);
4323
4385
  }
4324
- while (nowIndex < timeline.length) {
4325
- const ev = timeline[nowIndex];
4326
- const _when = ev.when - time;
4386
+ while (true) {
4387
+ let ev = timeline[nowIndex];
4388
+ if (nowIndex >= timeline.length || isLooping && ev && ev.when >= loopEndSec) {
4389
+ if (!isLooping || loopDurationSec <= 0) break;
4390
+ nowIndex = loopStartIndex;
4391
+ loopBase += loopDurationSec;
4392
+ ev = timeline[nowIndex];
4393
+ }
4394
+ if (!ev) break;
4395
+ const _when = ev.when + loopBase - time;
4327
4396
  if (_when > PLAN_TIME) break;
4328
4397
  nowIndex++;
4329
4398
  if (soloId && ev.trackId !== soloId) continue;
@@ -4341,7 +4410,7 @@ var createSequencer = (options) => {
4341
4410
  const pattern = options.getDrumPattern();
4342
4411
  if (pattern && pattern.length > 0) {
4343
4412
  const { stepsPerBar } = options;
4344
- const currentStep = (fromStepValue * sps + (options.getAudioTime() - startTime)) / sps;
4413
+ const currentStep = getWrappedPlayStep(time, sps);
4345
4414
  const currentStepInBar = currentStep % stepsPerBar;
4346
4415
  const nextStep = currentStepInBar + 4;
4347
4416
  const crossedBar = currentStepInBar < 4;
@@ -4358,19 +4427,44 @@ var createSequencer = (options) => {
4358
4427
  });
4359
4428
  }
4360
4429
  }
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();
4430
+ if (time >= 0) {
4431
+ const currentStep = getWrappedPlayStep(time, sps);
4432
+ if (options.cues && options.cues.length > 0 && options.onCue) {
4433
+ const bpm = options.getBpm();
4434
+ const stepsPerBar = options.stepsPerBar;
4435
+ const isCueCrossed = (cueStep, prevStep, currStep) => {
4436
+ if (currStep >= prevStep) {
4437
+ return cueStep > prevStep && cueStep <= currStep;
4438
+ } else {
4439
+ const reachedEnd = cueStep > prevStep && cueStep <= loopEndStep;
4440
+ const startedNew = cueStep >= loopStartStep && cueStep <= currStep;
4441
+ return reachedEnd || startedNew;
4442
+ }
4443
+ };
4444
+ for (const cue of options.cues) {
4445
+ const cueStep = resolveLoopPoint(cue.time, bpm, stepsPerBar, sps);
4446
+ if (isCueCrossed(cueStep, lastPlayStep, currentStep)) {
4447
+ options.onCue(cue.id);
4448
+ }
4449
+ }
4450
+ }
4451
+ lastPlayStep = currentStep;
4452
+ }
4453
+ if (!isLooping) {
4454
+ const last = timeline[timeline.length - 1];
4455
+ const lastWhen = last?.when ?? 0;
4456
+ const lastDuration = last?.duration ?? 0;
4457
+ if (nowIndex >= timeline.length && time > lastWhen + lastDuration + 0.1) {
4458
+ stop();
4459
+ options.onEnd();
4460
+ }
4367
4461
  }
4368
4462
  };
4369
4463
  const animate = () => {
4370
4464
  if (!active) return;
4371
4465
  const sps = secondsPerStep();
4372
4466
  const time = options.getAudioTime() - startTime;
4373
- options.onTick(fromStepValue + time / sps);
4467
+ options.onTick(getWrappedPlayStep(time, sps));
4374
4468
  animationId = requestAnimationFrame(animate);
4375
4469
  };
4376
4470
  const stop = () => {
@@ -4392,7 +4486,17 @@ var createSequencer = (options) => {
4392
4486
  if (timeline.length === 0 && !options.getDrumPattern()?.length) return;
4393
4487
  active = true;
4394
4488
  startTime = options.getAudioTime() + START_DELAY;
4489
+ const sps = secondsPerStep();
4395
4490
  nowIndex = 0;
4491
+ while (nowIndex < timeline.length) {
4492
+ const noteStartStep = fromStepValue + timeline[nowIndex].when / sps;
4493
+ if (noteStartStep >= fromStepValue - 1e-4) {
4494
+ break;
4495
+ }
4496
+ nowIndex++;
4497
+ }
4498
+ loopBase = 0;
4499
+ lastPlayStep = fromStepValue - 1e-4;
4396
4500
  intervalId = setInterval(scheduleTick, TICK_INTERVAL_MS);
4397
4501
  animationId = requestAnimationFrame(animate);
4398
4502
  };
@@ -4404,6 +4508,73 @@ var createSequencer = (options) => {
4404
4508
  };
4405
4509
  };
4406
4510
 
4511
+ // src/synth.ts
4512
+ var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
4513
+ var createSynth = (ctx, destination = ctx.destination) => {
4514
+ const playNote = (e) => {
4515
+ const osc = ctx.createOscillator();
4516
+ const gain = ctx.createGain();
4517
+ osc.type = "square";
4518
+ osc.frequency.value = freqFromPitch(e.pitch);
4519
+ const t0 = ctx.currentTime + e.when;
4520
+ const peak = Math.max(1e-4, 0.06 * e.volume * 1.5);
4521
+ gain.gain.setValueAtTime(peak, t0);
4522
+ gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
4523
+ osc.connect(gain);
4524
+ if (typeof ctx.createStereoPanner === "function" && e.pan) {
4525
+ const panner = ctx.createStereoPanner();
4526
+ panner.pan.value = Math.max(-1, Math.min(1, e.pan));
4527
+ gain.connect(panner);
4528
+ panner.connect(destination);
4529
+ } else {
4530
+ gain.connect(destination);
4531
+ }
4532
+ osc.start(t0);
4533
+ osc.stop(t0 + e.duration + 0.02);
4534
+ };
4535
+ const playDrum = (e) => {
4536
+ const t0 = ctx.currentTime + e.when;
4537
+ const vol = Math.max(1e-4, Math.min(1, e.velocity));
4538
+ const isKick = e.pitch === 35 || e.pitch === 36;
4539
+ const isSnareLike = e.pitch === 38 || e.pitch === 39 || e.pitch === 40;
4540
+ if (isKick) {
4541
+ const osc = ctx.createOscillator();
4542
+ const g2 = ctx.createGain();
4543
+ osc.frequency.setValueAtTime(150, t0);
4544
+ osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
4545
+ g2.gain.setValueAtTime(vol * 0.9, t0);
4546
+ g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
4547
+ osc.connect(g2).connect(destination);
4548
+ osc.start(t0);
4549
+ osc.stop(t0 + 0.2);
4550
+ osc.onended = () => osc.disconnect();
4551
+ return;
4552
+ }
4553
+ const dur = isSnareLike ? 0.18 : 0.05;
4554
+ const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
4555
+ const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
4556
+ const data = buffer.getChannelData(0);
4557
+ for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
4558
+ const src = ctx.createBufferSource();
4559
+ src.buffer = buffer;
4560
+ const filter = ctx.createBiquadFilter();
4561
+ filter.type = isSnareLike ? "bandpass" : "highpass";
4562
+ filter.frequency.value = isSnareLike ? 2e3 : 8e3;
4563
+ const g = ctx.createGain();
4564
+ g.gain.setValueAtTime(vol * (isSnareLike ? 0.7 : 0.4), t0);
4565
+ g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
4566
+ src.connect(filter).connect(g).connect(destination);
4567
+ src.start(t0);
4568
+ src.stop(t0 + dur);
4569
+ src.onended = () => {
4570
+ src.disconnect();
4571
+ filter.disconnect();
4572
+ g.disconnect();
4573
+ };
4574
+ };
4575
+ return { playNote, playDrum };
4576
+ };
4577
+
4407
4578
  // src/styles.ts
4408
4579
  var STYLE_ID = "dtm-daw-styles";
4409
4580
  var DAW_CSS = `
@@ -5074,6 +5245,13 @@ var DAW_CSS = `
5074
5245
  font-size: 13px;
5075
5246
  line-height: 1.6;
5076
5247
  }
5248
+ .dtm-modal-body a {
5249
+ color: var(--dtm-primary);
5250
+ text-decoration: underline;
5251
+ }
5252
+ .dtm-modal-body a:hover {
5253
+ color: var(--dtm-accent);
5254
+ }
5077
5255
  .dtm-modal-body h4 {
5078
5256
  margin: 12px 0 6px 0;
5079
5257
  color: var(--dtm-primary);
@@ -5528,7 +5706,6 @@ var showBalloon = (balloonEl) => {
5528
5706
  hideActiveBalloon();
5529
5707
  }, 3e3);
5530
5708
  };
5531
- var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
5532
5709
  var mountMmlPlayer = (target, mml, options = {}) => {
5533
5710
  injectStyles(target.ownerDocument ?? document);
5534
5711
  const {
@@ -5599,68 +5776,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
5599
5776
  if (!audioCtx) audioCtx = new AudioContext();
5600
5777
  return audioCtx;
5601
5778
  };
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
- };
5779
+ let synthInstance = null;
5780
+ const ensureSynth = () => {
5781
+ if (!synthInstance) synthInstance = createSynth(ensureCtx());
5782
+ return synthInstance;
5664
5783
  };
5665
5784
  let voices = null;
5666
5785
  const ensureVoices = () => {
@@ -6030,12 +6149,12 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6030
6149
  if (em) jumpEmojiAt(em, e.when);
6031
6150
  if (lyricTracks.has(trackIdx)) return;
6032
6151
  options.onPlayNote?.(e);
6033
- if (useSynth) synthPlay(e);
6152
+ if (useSynth) ensureSynth().playNote(e);
6034
6153
  },
6035
6154
  onPlayDrum: (e) => {
6036
6155
  const velocity = e.velocity * (trackVolume / 100);
6037
6156
  options.onPlayDrum?.({ ...e, velocity });
6038
- if (useSynth) drumSynth({ ...e, velocity });
6157
+ if (useSynth) ensureSynth().playDrum({ ...e, velocity });
6039
6158
  },
6040
6159
  onTick: (step) => {
6041
6160
  renderPlayhead(step);
@@ -6110,13 +6229,19 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6110
6229
  if (activePlayer && activePlayer !== instance) activePlayer.stop();
6111
6230
  activePlayer = instance;
6112
6231
  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();
6232
+ void (async () => {
6233
+ const resumes = [];
6234
+ const r = options.onResumeAudio?.();
6235
+ if (r) resumes.push(r);
6236
+ if (useSynth) {
6237
+ const ctx = ensureCtx();
6238
+ if (ctx.state === "suspended") resumes.push(ctx.resume());
6239
+ }
6240
+ if (resumes.length > 0) await Promise.all(resumes);
6241
+ if (!playing || activePlayer !== instance) return;
6242
+ if (voicesAvailable && lyricTracks.size > 0) ensureVoices().reset();
6243
+ await startWhenReady();
6244
+ })();
6120
6245
  };
6121
6246
  const stop = () => {
6122
6247
  if (!playing) return;
@@ -7131,8 +7256,8 @@ var mountDAW = (target, options = {}) => {
7131
7256
  stepsPerBar: renderConfig.stepsPerBar
7132
7257
  });
7133
7258
  const play = async () => {
7134
- options.onResumeAudio?.();
7135
7259
  if (playbackState === "playing") return;
7260
+ await options.onResumeAudio?.();
7136
7261
  const fromStep = playbackState === "paused" ? pausedPlayStep : playStartStep;
7137
7262
  options.singingVoices?.reset();
7138
7263
  const lyricMap = buildLyricsMap();
@@ -8289,6 +8414,123 @@ var INSTRUMENT_PRESETS = {
8289
8414
  }
8290
8415
  };
8291
8416
 
8417
+ // src/headless-player.ts
8418
+ var STEPS_PER_BAR2 = 192;
8419
+ var playMML = (mml, options = {}) => {
8420
+ const { placements, bpm: parsedBpm, meta } = parseMML(mml);
8421
+ const bpm = parsedBpm ?? options.defaultBpm ?? DEFAULT_BPM;
8422
+ const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
8423
+ const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
8424
+ let masterVolume = meta.volume ?? options.volume ?? 100;
8425
+ const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
8426
+ (a, b) => a - b
8427
+ );
8428
+ const seqTracks = trackIndices.map((index) => {
8429
+ let id = 0;
8430
+ const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
8431
+ id: id++,
8432
+ startStep: p.startStep,
8433
+ durationSteps: p.durationSteps,
8434
+ pitch: p.pitch,
8435
+ velocity: 100
8436
+ }));
8437
+ return { id: String(index), volume: masterVolume, notes };
8438
+ });
8439
+ const ownsCtx = !options.audioContext;
8440
+ const ctx = options.audioContext ?? new AudioContext();
8441
+ const destination = options.destination ?? ctx.destination;
8442
+ const useSynth = options.synth ?? !options.onPlayNote;
8443
+ const synth = useSynth ? createSynth(ctx, destination) : null;
8444
+ const pauseWhenHidden = options.pauseWhenHidden ?? ownsCtx;
8445
+ let playing = false;
8446
+ const seq = createSequencer({
8447
+ getTracks: () => seqTracks,
8448
+ getBpm: () => bpm,
8449
+ getPlayStartStep: () => 0,
8450
+ getDrumPattern: () => drumPattern,
8451
+ getSoloTrackId: () => null,
8452
+ getLoop: () => options.loop ?? false,
8453
+ cues: options.cues,
8454
+ onCue: options.onCue,
8455
+ getAudioTime: () => ctx.currentTime,
8456
+ onPlayNote: (e) => {
8457
+ options.onPlayNote?.(e);
8458
+ synth?.playNote(e);
8459
+ },
8460
+ onPlayDrum: (e) => {
8461
+ const velocity = e.velocity * (masterVolume / 100);
8462
+ options.onPlayDrum?.({ ...e, velocity });
8463
+ synth?.playDrum({ ...e, velocity });
8464
+ },
8465
+ onTick: () => {
8466
+ },
8467
+ onEnd: () => finish(),
8468
+ stepsPerBar: STEPS_PER_BAR2
8469
+ });
8470
+ const finish = () => {
8471
+ if (!playing) return;
8472
+ playing = false;
8473
+ options.onStop?.();
8474
+ };
8475
+ const onVisibilityChange = () => {
8476
+ if (!playing) return;
8477
+ if (document.hidden) {
8478
+ void ctx.suspend();
8479
+ } else if (ctx.state === "suspended") {
8480
+ void ctx.resume();
8481
+ }
8482
+ };
8483
+ if (pauseWhenHidden && typeof document !== "undefined") {
8484
+ document.addEventListener("visibilitychange", onVisibilityChange);
8485
+ }
8486
+ playing = true;
8487
+ void (async () => {
8488
+ const resumes = [];
8489
+ const r = options.onResumeAudio?.();
8490
+ if (r) resumes.push(r);
8491
+ if (ctx.state === "suspended") resumes.push(ctx.resume());
8492
+ if (resumes.length > 0) await Promise.all(resumes);
8493
+ if (!playing) return;
8494
+ seq.start(0);
8495
+ })();
8496
+ const stop = () => {
8497
+ if (!playing) return;
8498
+ seq.stop();
8499
+ finish();
8500
+ };
8501
+ const setVolume = (volume) => {
8502
+ masterVolume = volume;
8503
+ for (const t of seqTracks) t.volume = volume;
8504
+ };
8505
+ const suspend = () => ctx.suspend();
8506
+ const resume = () => ctx.resume();
8507
+ const destroy = () => {
8508
+ seq.stop();
8509
+ playing = false;
8510
+ if (pauseWhenHidden && typeof document !== "undefined") {
8511
+ document.removeEventListener("visibilitychange", onVisibilityChange);
8512
+ }
8513
+ if (ownsCtx) void ctx.close();
8514
+ };
8515
+ return {
8516
+ stop,
8517
+ isPlaying: () => playing,
8518
+ setVolume,
8519
+ suspend,
8520
+ resume,
8521
+ destroy
8522
+ };
8523
+ };
8524
+
8525
+ // src/headless-singing-player.ts
8526
+ var playSingingMML = (_mml, _options = {}) => {
8527
+ return Promise.reject(
8528
+ new Error(
8529
+ "playSingingMML is not implemented yet. See implementation notes at the top of headless-singing-player.ts."
8530
+ )
8531
+ );
8532
+ };
8533
+
8292
8534
  // src/piano-roll.ts
8293
8535
  var createPianoRoll = (options, handlers) => {
8294
8536
  const {
@@ -8741,7 +8983,23 @@ var DEFAULT_CDN = {
8741
8983
  soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs"
8742
8984
  };
8743
8985
  var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
8744
- var TRACK_ROLES = ["melody", "submelody", "bass", "chord"];
8986
+ var TRACK_ROLES = [
8987
+ "melody",
8988
+ "submelody",
8989
+ "bass",
8990
+ "chord",
8991
+ "t4",
8992
+ "t5",
8993
+ "t6",
8994
+ "t7",
8995
+ "t8",
8996
+ "t9",
8997
+ "t10",
8998
+ "t11",
8999
+ "t12",
9000
+ "t13",
9001
+ "t14"
9002
+ ];
8745
9003
  var resolveDefaultVoiceWorkerUrl = () => {
8746
9004
  try {
8747
9005
  return new URL("./voice-worker.js", import_meta.url).href;
@@ -8773,7 +9031,8 @@ var createDtmStudio = async (options = {}) => {
8773
9031
  drumGain.gain.value = options.drumVolume ?? 1;
8774
9032
  drumGain.connect(audioCtx.destination);
8775
9033
  const resumeAudio = () => {
8776
- if (audioCtx.state === "suspended") void audioCtx.resume();
9034
+ if (audioCtx.state === "suspended") return audioCtx.resume();
9035
+ return Promise.resolve();
8777
9036
  };
8778
9037
  const eng = options.engines ?? {};
8779
9038
  const [SoundFont, SoundFont_drum, SoundFont_list] = await Promise.all([
@@ -8877,36 +9136,44 @@ var createDtmStudio = async (options = {}) => {
8877
9136
  })();
8878
9137
  let nameToKey = {};
8879
9138
  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) {
9139
+ const loadingByKey = /* @__PURE__ */ new Map();
9140
+ const loadInstrument = (instrumentKey) => {
9141
+ if (soundFonts.has(instrumentKey)) return Promise.resolve();
9142
+ const inflight = loadingByKey.get(instrumentKey);
9143
+ if (inflight) return inflight;
9144
+ const fullName = `${instrumentKey}_${SOUNDFONT_NAME}`;
9145
+ const p = SoundFont.load({
9146
+ ctx: audioCtx,
9147
+ fontName: `_tone_${fullName}`,
9148
+ url: SoundFont.toURL(fullName)
9149
+ }).then((sf) => {
9150
+ soundFonts.set(instrumentKey, sf);
9151
+ }).catch((e) => {
8895
9152
  console.error(`[dtm] \u697D\u5668 "${instrumentKey}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557`, e);
8896
- }
9153
+ }).finally(() => {
9154
+ loadingByKey.delete(instrumentKey);
9155
+ });
9156
+ loadingByKey.set(instrumentKey, p);
9157
+ return p;
8897
9158
  };
8898
9159
  const defaultPreset = options.defaultPreset ?? "retro_game";
8899
9160
  const instrumentNameFor = (preset, trackId) => preset[trackId] ?? preset.melody;
9161
+ const resolveSoundFont = (presetKey, trackId) => {
9162
+ const preset = INSTRUMENT_PRESETS[presetKey];
9163
+ if (!preset) return void 0;
9164
+ const key = nameToKey[instrumentNameFor(preset, trackId)];
9165
+ return key ? soundFonts.get(key) : void 0;
9166
+ };
8900
9167
  const loadPreset = async (presetKey, trackIds = [...TRACK_ROLES]) => {
8901
9168
  const preset = INSTRUMENT_PRESETS[presetKey];
8902
9169
  if (!preset) return;
8903
9170
  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
- );
9171
+ const keys = /* @__PURE__ */ new Set();
9172
+ for (const trackId of trackIds) {
9173
+ const key = nameToKey[instrumentNameFor(preset, trackId)];
9174
+ if (key) keys.add(key);
9175
+ }
9176
+ await Promise.all([...keys].map((key) => loadInstrument(key)));
8910
9177
  };
8911
9178
  const applyPreset = async (daw, presetKey, trackIds, loadingTarget) => {
8912
9179
  const wasPlaying = daw.getPlaybackState() === "playing";
@@ -8976,18 +9243,6 @@ var createDtmStudio = async (options = {}) => {
8976
9243
  await listReady;
8977
9244
  nameToKey = await buildNameToKeyMapping();
8978
9245
  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
9246
  const playDrum = (e) => {
8992
9247
  if (!SoundFont_drum.font) return;
8993
9248
  SoundFont_drum.play({
@@ -8999,19 +9254,6 @@ var createDtmStudio = async (options = {}) => {
8999
9254
  duration: e.duration
9000
9255
  });
9001
9256
  };
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
9257
  const editorPresetSelects = /* @__PURE__ */ new WeakMap();
9016
9258
  const mountedEditors = [];
9017
9259
  const mountedPlayers = [];
@@ -9020,6 +9262,20 @@ var createDtmStudio = async (options = {}) => {
9020
9262
  const { preset, presetUI, ...dawOverrides } = opts;
9021
9263
  const tracks = dawOverrides.tracks ?? TRACKS_SIMPLE;
9022
9264
  const trackIds = tracks.map((t) => t.id);
9265
+ const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
9266
+ let editorPreset = presetKey;
9267
+ const playNote = (e) => {
9268
+ const sf = resolveSoundFont(editorPreset, e.trackId);
9269
+ if (!sf) return;
9270
+ sf.play({
9271
+ ctx: audioCtx,
9272
+ destination: masterGain,
9273
+ pitch: e.pitch,
9274
+ volume: e.volume,
9275
+ when: e.when,
9276
+ duration: e.duration
9277
+ });
9278
+ };
9023
9279
  const base = {
9024
9280
  getAudioTime: () => audioCtx.currentTime,
9025
9281
  onResumeAudio: resumeAudio,
@@ -9032,7 +9288,6 @@ var createDtmStudio = async (options = {}) => {
9032
9288
  };
9033
9289
  const daw = mountDAW(target, base);
9034
9290
  mountedEditors.push(daw);
9035
- const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
9036
9291
  const wantPresetUI = presetUI ?? features.presetUI;
9037
9292
  let presetSelect = null;
9038
9293
  if (wantPresetUI) {
@@ -9043,7 +9298,11 @@ var createDtmStudio = async (options = {}) => {
9043
9298
  getTrackIds: () => trackIds,
9044
9299
  value: presetKey,
9045
9300
  loadingTarget: rollEl ?? target,
9046
- position: "prepend"
9301
+ position: "prepend",
9302
+ // 楽器変更時、このエディタの発音解決が使うプリセットも追従させる。
9303
+ onChange: (key) => {
9304
+ editorPreset = key;
9305
+ }
9047
9306
  });
9048
9307
  editorPresetSelects.set(target, presetSelect);
9049
9308
  }
@@ -9147,10 +9406,28 @@ var createDtmStudio = async (options = {}) => {
9147
9406
  return instance;
9148
9407
  };
9149
9408
  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
- }
9409
+ const parsed = parseMML(mml, {});
9410
+ const meta = parsed.meta ?? {};
9411
+ const playerPreset = meta.instrument && INSTRUMENT_PRESETS[meta.instrument] ? meta.instrument : defaultPreset;
9412
+ const trackIndices = [
9413
+ ...new Set(parsed.placements.map((p) => p.trackIndex))
9414
+ ];
9415
+ const trackIds = trackIndices.map((idx) => TRACK_ROLES[idx] ?? `t${idx}`);
9416
+ const loadTrackIds = trackIds.length > 0 ? trackIds : [...TRACK_ROLES];
9417
+ void loadPreset(playerPreset, loadTrackIds);
9418
+ const playPlayerNote = (e) => {
9419
+ const role = TRACK_ROLES[Number(e.trackId)] ?? `t${e.trackId}`;
9420
+ const sf = resolveSoundFont(playerPreset, role);
9421
+ if (!sf) return;
9422
+ sf.play({
9423
+ ctx: audioCtx,
9424
+ destination: masterGain,
9425
+ pitch: e.pitch,
9426
+ volume: e.volume,
9427
+ when: e.when,
9428
+ duration: e.duration
9429
+ });
9430
+ };
9154
9431
  const player = mountMmlPlayer(target, mml, {
9155
9432
  getAudioTime: () => audioCtx.currentTime,
9156
9433
  onResumeAudio: resumeAudio,
@@ -9229,6 +9506,7 @@ var createDtmStudio = async (options = {}) => {
9229
9506
  createPianoRoll,
9230
9507
  createSequencer,
9231
9508
  createSingingVoices,
9509
+ createSynth,
9232
9510
  createVoiceRegistry,
9233
9511
  decomposeToMonophonic,
9234
9512
  drawGrid,
@@ -9242,6 +9520,7 @@ var createDtmStudio = async (options = {}) => {
9242
9520
  extractMidiPlacementsByTrack,
9243
9521
  fetchSoundFontList,
9244
9522
  formatMmlMeta,
9523
+ freqFromPitch,
9245
9524
  generateRandomPattern,
9246
9525
  getDrawOffset,
9247
9526
  getGridCanvas,
@@ -9264,6 +9543,8 @@ var createDtmStudio = async (options = {}) => {
9264
9543
  parseLyrics,
9265
9544
  parseMML,
9266
9545
  parseMmlMeta,
9546
+ playMML,
9547
+ playSingingMML,
9267
9548
  setDrawOffset,
9268
9549
  setupRecorder,
9269
9550
  shiftNotes,