@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.mjs CHANGED
@@ -3919,7 +3919,7 @@ var PITCH_MAP2 = {
3919
3919
  b: 11
3920
3920
  };
3921
3921
  var clamp2 = (value, lo, hi) => Math.min(hi, Math.max(lo, value));
3922
- var META_DIRECTIVE = /#(inst|drum|volume)=([\w-]+)/gi;
3922
+ var META_DIRECTIVE = /#(inst|drum|volume|mode)=([\w-]+)/gi;
3923
3923
  var parseMmlMeta = (mml) => {
3924
3924
  const meta = {};
3925
3925
  for (const m of mml.matchAll(META_DIRECTIVE)) {
@@ -3929,6 +3929,10 @@ var parseMmlMeta = (mml) => {
3929
3929
  else if (key === "volume") {
3930
3930
  const v = Number.parseInt(m[2], 10);
3931
3931
  if (!Number.isNaN(v)) meta.volume = v;
3932
+ } else if (key === "mode") {
3933
+ if (m[2] === "simple" || m[2] === "advanced") {
3934
+ meta.mode = m[2];
3935
+ }
3932
3936
  }
3933
3937
  }
3934
3938
  return meta;
@@ -3939,6 +3943,7 @@ var formatMmlMeta = (meta) => {
3939
3943
  if (meta.instrument) parts.push(`#inst=${meta.instrument}`);
3940
3944
  if (meta.drum) parts.push(`#drum=${meta.drum}`);
3941
3945
  if (meta.volume !== void 0) parts.push(`#volume=${meta.volume}`);
3946
+ if (meta.mode) parts.push(`#mode=${meta.mode}`);
3942
3947
  return parts.join(" ");
3943
3948
  };
3944
3949
  var parseMML = (mml, options = {}) => {
@@ -3974,7 +3979,8 @@ var parseMML = (mml, options = {}) => {
3974
3979
  const part = rawPart.trim();
3975
3980
  if (part.startsWith("@")) {
3976
3981
  let idx = Number.parseInt(part.substring(1), 10);
3977
- if (clampTrackCount !== void 0 && idx >= clampTrackCount) idx = 2;
3982
+ if (clampTrackCount !== void 0 && idx >= clampTrackCount)
3983
+ idx = clampTrackCount - 1;
3978
3984
  trackIndex = idx;
3979
3985
  octave = 4;
3980
3986
  currentStep = 0;
@@ -4177,6 +4183,18 @@ var VOICE_IMAGES = {
4177
4183
  var STEPS_PER_BEAT2 = 48;
4178
4184
  var PLAN_TIME = 0.5;
4179
4185
  var TICK_INTERVAL_MS = 20;
4186
+ var resolveLoopPoint = (point, bpm, stepsPerBar, sps) => {
4187
+ if ("step" in point) {
4188
+ return point.step;
4189
+ }
4190
+ if ("bar" in point) {
4191
+ return Math.max(0, point.bar - 1) * stepsPerBar;
4192
+ }
4193
+ if ("seconds" in point) {
4194
+ return point.seconds / sps;
4195
+ }
4196
+ return 0;
4197
+ };
4180
4198
  var createSequencer = (options) => {
4181
4199
  let timeline = [];
4182
4200
  let startTime = 0;
@@ -4186,28 +4204,74 @@ var createSequencer = (options) => {
4186
4204
  let active = false;
4187
4205
  let fromStepValue = 0;
4188
4206
  let trackVolumeMap = /* @__PURE__ */ new Map();
4207
+ let isLooping = false;
4208
+ let loopStartStep = 0;
4209
+ let loopEndStep = 0;
4210
+ let loopStartSec = 0;
4211
+ let loopEndSec = 0;
4212
+ let loopDurationSec = 0;
4213
+ let loopStartIndex = 0;
4214
+ let loopBase = 0;
4215
+ let lastPlayStep = 0;
4189
4216
  const secondsPerStep = () => 60 / options.getBpm() / STEPS_PER_BEAT2;
4217
+ const getWrappedPlayStep = (time, sps) => {
4218
+ if (!isLooping || loopDurationSec <= 0 || time < loopEndSec) {
4219
+ return fromStepValue + time / sps;
4220
+ }
4221
+ const elapsedInLoop = (time - loopEndSec) % loopDurationSec;
4222
+ return loopStartStep + elapsedInLoop / sps;
4223
+ };
4190
4224
  const buildTimeline = (fromStep) => {
4191
4225
  timeline = [];
4192
4226
  trackVolumeMap = /* @__PURE__ */ new Map();
4193
4227
  const sps = secondsPerStep();
4228
+ const bpm = options.getBpm();
4229
+ const stepsPerBar = options.stepsPerBar;
4230
+ const loopOption = options.getLoop?.() ?? false;
4231
+ isLooping = !!loopOption;
4232
+ if (typeof loopOption === "object") {
4233
+ loopStartStep = loopOption.start ? resolveLoopPoint(loopOption.start, bpm, stepsPerBar, sps) : 0;
4234
+ const endVal = loopOption.end ? resolveLoopPoint(loopOption.end, bpm, stepsPerBar, sps) : null;
4235
+ loopEndStep = endVal !== null ? endVal : -1;
4236
+ } else {
4237
+ loopStartStep = 0;
4238
+ loopEndStep = -1;
4239
+ }
4240
+ const startLimit = isLooping ? Math.min(fromStep, loopStartStep) : fromStep;
4241
+ let maxEndStep = 0;
4194
4242
  for (const track of options.getTracks()) {
4195
4243
  trackVolumeMap.set(track.id, track.volume);
4196
4244
  for (const note of track.notes) {
4245
+ if (note.startStep < startLimit) continue;
4197
4246
  const relativeStart = note.startStep - fromStep;
4198
- if (relativeStart < 0) continue;
4199
- const velocity = note.velocity ?? DEFAULT_PLAYBACK_VELOCITY;
4247
+ const when = relativeStart * sps;
4248
+ const duration = note.durationSteps * sps;
4249
+ maxEndStep = Math.max(maxEndStep, note.startStep + note.durationSteps);
4200
4250
  timeline.push({
4201
4251
  trackId: track.id,
4202
4252
  pitch: note.pitch,
4203
4253
  volume: track.volume / 100,
4204
- velocity,
4205
- when: relativeStart * sps,
4206
- duration: note.durationSteps * sps
4254
+ velocity: note.velocity ?? DEFAULT_PLAYBACK_VELOCITY,
4255
+ when,
4256
+ duration
4207
4257
  });
4208
4258
  }
4209
4259
  }
4210
4260
  timeline.sort((a, b) => a.when - b.when);
4261
+ if (loopEndStep === -1) {
4262
+ loopEndStep = maxEndStep;
4263
+ }
4264
+ loopStartSec = (loopStartStep - fromStep) * sps;
4265
+ loopEndSec = (loopEndStep - fromStep) * sps;
4266
+ loopDurationSec = loopEndSec - loopStartSec;
4267
+ loopStartIndex = 0;
4268
+ while (loopStartIndex < timeline.length) {
4269
+ const noteStartStep = fromStep + timeline[loopStartIndex].when / sps;
4270
+ if (noteStartStep >= loopStartStep - 1e-4) {
4271
+ break;
4272
+ }
4273
+ loopStartIndex++;
4274
+ }
4211
4275
  };
4212
4276
  const scheduleTick = () => {
4213
4277
  const sps = secondsPerStep();
@@ -4216,9 +4280,16 @@ var createSequencer = (options) => {
4216
4280
  for (const track of options.getTracks()) {
4217
4281
  trackVolumeMap.set(track.id, track.volume);
4218
4282
  }
4219
- while (nowIndex < timeline.length) {
4220
- const ev = timeline[nowIndex];
4221
- const _when = ev.when - time;
4283
+ while (true) {
4284
+ let ev = timeline[nowIndex];
4285
+ if (nowIndex >= timeline.length || isLooping && ev && ev.when >= loopEndSec) {
4286
+ if (!isLooping || loopDurationSec <= 0) break;
4287
+ nowIndex = loopStartIndex;
4288
+ loopBase += loopDurationSec;
4289
+ ev = timeline[nowIndex];
4290
+ }
4291
+ if (!ev) break;
4292
+ const _when = ev.when + loopBase - time;
4222
4293
  if (_when > PLAN_TIME) break;
4223
4294
  nowIndex++;
4224
4295
  if (soloId && ev.trackId !== soloId) continue;
@@ -4236,7 +4307,7 @@ var createSequencer = (options) => {
4236
4307
  const pattern = options.getDrumPattern();
4237
4308
  if (pattern && pattern.length > 0) {
4238
4309
  const { stepsPerBar } = options;
4239
- const currentStep = (fromStepValue * sps + (options.getAudioTime() - startTime)) / sps;
4310
+ const currentStep = getWrappedPlayStep(time, sps);
4240
4311
  const currentStepInBar = currentStep % stepsPerBar;
4241
4312
  const nextStep = currentStepInBar + 4;
4242
4313
  const crossedBar = currentStepInBar < 4;
@@ -4253,19 +4324,44 @@ var createSequencer = (options) => {
4253
4324
  });
4254
4325
  }
4255
4326
  }
4256
- const last = timeline[timeline.length - 1];
4257
- const lastWhen = last?.when ?? 0;
4258
- const lastDuration = last?.duration ?? 0;
4259
- if (nowIndex >= timeline.length && time > lastWhen + lastDuration + 0.1) {
4260
- stop();
4261
- options.onEnd();
4327
+ if (time >= 0) {
4328
+ const currentStep = getWrappedPlayStep(time, sps);
4329
+ if (options.cues && options.cues.length > 0 && options.onCue) {
4330
+ const bpm = options.getBpm();
4331
+ const stepsPerBar = options.stepsPerBar;
4332
+ const isCueCrossed = (cueStep, prevStep, currStep) => {
4333
+ if (currStep >= prevStep) {
4334
+ return cueStep > prevStep && cueStep <= currStep;
4335
+ } else {
4336
+ const reachedEnd = cueStep > prevStep && cueStep <= loopEndStep;
4337
+ const startedNew = cueStep >= loopStartStep && cueStep <= currStep;
4338
+ return reachedEnd || startedNew;
4339
+ }
4340
+ };
4341
+ for (const cue of options.cues) {
4342
+ const cueStep = resolveLoopPoint(cue.time, bpm, stepsPerBar, sps);
4343
+ if (isCueCrossed(cueStep, lastPlayStep, currentStep)) {
4344
+ options.onCue(cue.id);
4345
+ }
4346
+ }
4347
+ }
4348
+ lastPlayStep = currentStep;
4349
+ }
4350
+ if (!isLooping) {
4351
+ const last = timeline[timeline.length - 1];
4352
+ const lastWhen = last?.when ?? 0;
4353
+ const lastDuration = last?.duration ?? 0;
4354
+ if (nowIndex >= timeline.length && time > lastWhen + lastDuration + 0.1) {
4355
+ stop();
4356
+ options.onEnd();
4357
+ }
4262
4358
  }
4263
4359
  };
4264
4360
  const animate = () => {
4265
4361
  if (!active) return;
4266
4362
  const sps = secondsPerStep();
4267
4363
  const time = options.getAudioTime() - startTime;
4268
- options.onTick(fromStepValue + time / sps);
4364
+ options.onTick(getWrappedPlayStep(time, sps));
4269
4365
  animationId = requestAnimationFrame(animate);
4270
4366
  };
4271
4367
  const stop = () => {
@@ -4287,7 +4383,17 @@ var createSequencer = (options) => {
4287
4383
  if (timeline.length === 0 && !options.getDrumPattern()?.length) return;
4288
4384
  active = true;
4289
4385
  startTime = options.getAudioTime() + START_DELAY;
4386
+ const sps = secondsPerStep();
4290
4387
  nowIndex = 0;
4388
+ while (nowIndex < timeline.length) {
4389
+ const noteStartStep = fromStepValue + timeline[nowIndex].when / sps;
4390
+ if (noteStartStep >= fromStepValue - 1e-4) {
4391
+ break;
4392
+ }
4393
+ nowIndex++;
4394
+ }
4395
+ loopBase = 0;
4396
+ lastPlayStep = fromStepValue - 1e-4;
4291
4397
  intervalId = setInterval(scheduleTick, TICK_INTERVAL_MS);
4292
4398
  animationId = requestAnimationFrame(animate);
4293
4399
  };
@@ -4299,6 +4405,73 @@ var createSequencer = (options) => {
4299
4405
  };
4300
4406
  };
4301
4407
 
4408
+ // src/synth.ts
4409
+ var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
4410
+ var createSynth = (ctx, destination = ctx.destination) => {
4411
+ const playNote = (e) => {
4412
+ const osc = ctx.createOscillator();
4413
+ const gain = ctx.createGain();
4414
+ osc.type = "square";
4415
+ osc.frequency.value = freqFromPitch(e.pitch);
4416
+ const t0 = ctx.currentTime + e.when;
4417
+ const peak = Math.max(1e-4, 0.06 * e.volume * 1.5);
4418
+ gain.gain.setValueAtTime(peak, t0);
4419
+ gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
4420
+ osc.connect(gain);
4421
+ if (typeof ctx.createStereoPanner === "function" && e.pan) {
4422
+ const panner = ctx.createStereoPanner();
4423
+ panner.pan.value = Math.max(-1, Math.min(1, e.pan));
4424
+ gain.connect(panner);
4425
+ panner.connect(destination);
4426
+ } else {
4427
+ gain.connect(destination);
4428
+ }
4429
+ osc.start(t0);
4430
+ osc.stop(t0 + e.duration + 0.02);
4431
+ };
4432
+ const playDrum = (e) => {
4433
+ const t0 = ctx.currentTime + e.when;
4434
+ const vol = Math.max(1e-4, Math.min(1, e.velocity));
4435
+ const isKick = e.pitch === 35 || e.pitch === 36;
4436
+ const isSnareLike = e.pitch === 38 || e.pitch === 39 || e.pitch === 40;
4437
+ if (isKick) {
4438
+ const osc = ctx.createOscillator();
4439
+ const g2 = ctx.createGain();
4440
+ osc.frequency.setValueAtTime(150, t0);
4441
+ osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
4442
+ g2.gain.setValueAtTime(vol * 0.9, t0);
4443
+ g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
4444
+ osc.connect(g2).connect(destination);
4445
+ osc.start(t0);
4446
+ osc.stop(t0 + 0.2);
4447
+ osc.onended = () => osc.disconnect();
4448
+ return;
4449
+ }
4450
+ const dur = isSnareLike ? 0.18 : 0.05;
4451
+ const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
4452
+ const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
4453
+ const data = buffer.getChannelData(0);
4454
+ for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
4455
+ const src = ctx.createBufferSource();
4456
+ src.buffer = buffer;
4457
+ const filter = ctx.createBiquadFilter();
4458
+ filter.type = isSnareLike ? "bandpass" : "highpass";
4459
+ filter.frequency.value = isSnareLike ? 2e3 : 8e3;
4460
+ const g = ctx.createGain();
4461
+ g.gain.setValueAtTime(vol * (isSnareLike ? 0.7 : 0.4), t0);
4462
+ g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
4463
+ src.connect(filter).connect(g).connect(destination);
4464
+ src.start(t0);
4465
+ src.stop(t0 + dur);
4466
+ src.onended = () => {
4467
+ src.disconnect();
4468
+ filter.disconnect();
4469
+ g.disconnect();
4470
+ };
4471
+ };
4472
+ return { playNote, playDrum };
4473
+ };
4474
+
4302
4475
  // src/styles.ts
4303
4476
  var STYLE_ID = "dtm-daw-styles";
4304
4477
  var DAW_CSS = `
@@ -4969,6 +5142,13 @@ var DAW_CSS = `
4969
5142
  font-size: 13px;
4970
5143
  line-height: 1.6;
4971
5144
  }
5145
+ .dtm-modal-body a {
5146
+ color: var(--dtm-primary);
5147
+ text-decoration: underline;
5148
+ }
5149
+ .dtm-modal-body a:hover {
5150
+ color: var(--dtm-accent);
5151
+ }
4972
5152
  .dtm-modal-body h4 {
4973
5153
  margin: 12px 0 6px 0;
4974
5154
  color: var(--dtm-primary);
@@ -5423,7 +5603,6 @@ var showBalloon = (balloonEl) => {
5423
5603
  hideActiveBalloon();
5424
5604
  }, 3e3);
5425
5605
  };
5426
- var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
5427
5606
  var mountMmlPlayer = (target, mml, options = {}) => {
5428
5607
  injectStyles(target.ownerDocument ?? document);
5429
5608
  const {
@@ -5494,68 +5673,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
5494
5673
  if (!audioCtx) audioCtx = new AudioContext();
5495
5674
  return audioCtx;
5496
5675
  };
5497
- const synthPlay = (e) => {
5498
- const ctx = ensureCtx();
5499
- const osc = ctx.createOscillator();
5500
- const gain = ctx.createGain();
5501
- osc.type = "square";
5502
- osc.frequency.value = freqFromPitch(e.pitch);
5503
- const t0 = ctx.currentTime + e.when;
5504
- const peak = Math.max(1e-4, 0.06 * e.volume * 1.5);
5505
- gain.gain.setValueAtTime(peak, t0);
5506
- gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
5507
- osc.connect(gain);
5508
- if (typeof ctx.createStereoPanner === "function" && e.pan) {
5509
- const panner = ctx.createStereoPanner();
5510
- panner.pan.value = Math.max(-1, Math.min(1, e.pan));
5511
- gain.connect(panner);
5512
- panner.connect(ctx.destination);
5513
- } else {
5514
- gain.connect(ctx.destination);
5515
- }
5516
- osc.start(t0);
5517
- osc.stop(t0 + e.duration + 0.02);
5518
- };
5519
- const drumSynth = (e) => {
5520
- const ctx = ensureCtx();
5521
- const t0 = ctx.currentTime + e.when;
5522
- const vol = Math.max(1e-4, Math.min(1, e.velocity));
5523
- const isKick = e.pitch === 35 || e.pitch === 36;
5524
- const isSnareLike = e.pitch === 38 || e.pitch === 39 || e.pitch === 40;
5525
- if (isKick) {
5526
- const osc = ctx.createOscillator();
5527
- const g2 = ctx.createGain();
5528
- osc.frequency.setValueAtTime(150, t0);
5529
- osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
5530
- g2.gain.setValueAtTime(vol * 0.9, t0);
5531
- g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
5532
- osc.connect(g2).connect(ctx.destination);
5533
- osc.start(t0);
5534
- osc.stop(t0 + 0.2);
5535
- osc.onended = () => osc.disconnect();
5536
- return;
5537
- }
5538
- const dur = isSnareLike ? 0.18 : 0.05;
5539
- const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
5540
- const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
5541
- const data = buffer.getChannelData(0);
5542
- for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
5543
- const src = ctx.createBufferSource();
5544
- src.buffer = buffer;
5545
- const filter = ctx.createBiquadFilter();
5546
- filter.type = isSnareLike ? "bandpass" : "highpass";
5547
- filter.frequency.value = isSnareLike ? 2e3 : 8e3;
5548
- const g = ctx.createGain();
5549
- g.gain.setValueAtTime(vol * (isSnareLike ? 0.7 : 0.4), t0);
5550
- g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
5551
- src.connect(filter).connect(g).connect(ctx.destination);
5552
- src.start(t0);
5553
- src.stop(t0 + dur);
5554
- src.onended = () => {
5555
- src.disconnect();
5556
- filter.disconnect();
5557
- g.disconnect();
5558
- };
5676
+ let synthInstance = null;
5677
+ const ensureSynth = () => {
5678
+ if (!synthInstance) synthInstance = createSynth(ensureCtx());
5679
+ return synthInstance;
5559
5680
  };
5560
5681
  let voices = null;
5561
5682
  const ensureVoices = () => {
@@ -5925,12 +6046,12 @@ var mountMmlPlayer = (target, mml, options = {}) => {
5925
6046
  if (em) jumpEmojiAt(em, e.when);
5926
6047
  if (lyricTracks.has(trackIdx)) return;
5927
6048
  options.onPlayNote?.(e);
5928
- if (useSynth) synthPlay(e);
6049
+ if (useSynth) ensureSynth().playNote(e);
5929
6050
  },
5930
6051
  onPlayDrum: (e) => {
5931
6052
  const velocity = e.velocity * (trackVolume / 100);
5932
6053
  options.onPlayDrum?.({ ...e, velocity });
5933
- if (useSynth) drumSynth({ ...e, velocity });
6054
+ if (useSynth) ensureSynth().playDrum({ ...e, velocity });
5934
6055
  },
5935
6056
  onTick: (step) => {
5936
6057
  renderPlayhead(step);
@@ -6005,13 +6126,19 @@ var mountMmlPlayer = (target, mml, options = {}) => {
6005
6126
  if (activePlayer && activePlayer !== instance) activePlayer.stop();
6006
6127
  activePlayer = instance;
6007
6128
  setPlayingUI(true);
6008
- void options.onResumeAudio?.();
6009
- if (useSynth) {
6010
- const ctx = ensureCtx();
6011
- if (ctx.state === "suspended") void ctx.resume();
6012
- }
6013
- if (voicesAvailable && lyricTracks.size > 0) ensureVoices().reset();
6014
- void startWhenReady();
6129
+ void (async () => {
6130
+ const resumes = [];
6131
+ const r = options.onResumeAudio?.();
6132
+ if (r) resumes.push(r);
6133
+ if (useSynth) {
6134
+ const ctx = ensureCtx();
6135
+ if (ctx.state === "suspended") resumes.push(ctx.resume());
6136
+ }
6137
+ if (resumes.length > 0) await Promise.all(resumes);
6138
+ if (!playing || activePlayer !== instance) return;
6139
+ if (voicesAvailable && lyricTracks.size > 0) ensureVoices().reset();
6140
+ await startWhenReady();
6141
+ })();
6015
6142
  };
6016
6143
  const stop = () => {
6017
6144
  if (!playing) return;
@@ -7026,8 +7153,8 @@ var mountDAW = (target, options = {}) => {
7026
7153
  stepsPerBar: renderConfig.stepsPerBar
7027
7154
  });
7028
7155
  const play = async () => {
7029
- options.onResumeAudio?.();
7030
7156
  if (playbackState === "playing") return;
7157
+ await options.onResumeAudio?.();
7031
7158
  const fromStep = playbackState === "paused" ? pausedPlayStep : playStartStep;
7032
7159
  options.singingVoices?.reset();
7033
7160
  const lyricMap = buildLyricsMap();
@@ -7381,7 +7508,8 @@ var mountDAW = (target, options = {}) => {
7381
7508
  const metaLine = formatMmlMeta({
7382
7509
  instrument: currentInstrument || void 0,
7383
7510
  drum: currentDrumPattern !== "none" ? currentDrumPattern : void 0,
7384
- volume: masterVolume
7511
+ volume: masterVolume,
7512
+ mode
7385
7513
  });
7386
7514
  if (refs.decomposeChordToggle.checked) {
7387
7515
  const ignoreHeavy = refs.ignoreChordHeavyToggle.checked;
@@ -8184,6 +8312,123 @@ var INSTRUMENT_PRESETS = {
8184
8312
  }
8185
8313
  };
8186
8314
 
8315
+ // src/headless-player.ts
8316
+ var STEPS_PER_BAR2 = 192;
8317
+ var playMML = (mml, options = {}) => {
8318
+ const { placements, bpm: parsedBpm, meta } = parseMML(mml);
8319
+ const bpm = parsedBpm ?? options.defaultBpm ?? DEFAULT_BPM;
8320
+ const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
8321
+ const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
8322
+ let masterVolume = meta.volume ?? options.volume ?? 100;
8323
+ const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
8324
+ (a, b) => a - b
8325
+ );
8326
+ const seqTracks = trackIndices.map((index) => {
8327
+ let id = 0;
8328
+ const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
8329
+ id: id++,
8330
+ startStep: p.startStep,
8331
+ durationSteps: p.durationSteps,
8332
+ pitch: p.pitch,
8333
+ velocity: 100
8334
+ }));
8335
+ return { id: String(index), volume: masterVolume, notes };
8336
+ });
8337
+ const ownsCtx = !options.audioContext;
8338
+ const ctx = options.audioContext ?? new AudioContext();
8339
+ const destination = options.destination ?? ctx.destination;
8340
+ const useSynth = options.synth ?? !options.onPlayNote;
8341
+ const synth = useSynth ? createSynth(ctx, destination) : null;
8342
+ const pauseWhenHidden = options.pauseWhenHidden ?? ownsCtx;
8343
+ let playing = false;
8344
+ const seq = createSequencer({
8345
+ getTracks: () => seqTracks,
8346
+ getBpm: () => bpm,
8347
+ getPlayStartStep: () => 0,
8348
+ getDrumPattern: () => drumPattern,
8349
+ getSoloTrackId: () => null,
8350
+ getLoop: () => options.loop ?? false,
8351
+ cues: options.cues,
8352
+ onCue: options.onCue,
8353
+ getAudioTime: () => ctx.currentTime,
8354
+ onPlayNote: (e) => {
8355
+ options.onPlayNote?.(e);
8356
+ synth?.playNote(e);
8357
+ },
8358
+ onPlayDrum: (e) => {
8359
+ const velocity = e.velocity * (masterVolume / 100);
8360
+ options.onPlayDrum?.({ ...e, velocity });
8361
+ synth?.playDrum({ ...e, velocity });
8362
+ },
8363
+ onTick: () => {
8364
+ },
8365
+ onEnd: () => finish(),
8366
+ stepsPerBar: STEPS_PER_BAR2
8367
+ });
8368
+ const finish = () => {
8369
+ if (!playing) return;
8370
+ playing = false;
8371
+ options.onStop?.();
8372
+ };
8373
+ const onVisibilityChange = () => {
8374
+ if (!playing) return;
8375
+ if (document.hidden) {
8376
+ void ctx.suspend();
8377
+ } else if (ctx.state === "suspended") {
8378
+ void ctx.resume();
8379
+ }
8380
+ };
8381
+ if (pauseWhenHidden && typeof document !== "undefined") {
8382
+ document.addEventListener("visibilitychange", onVisibilityChange);
8383
+ }
8384
+ playing = true;
8385
+ void (async () => {
8386
+ const resumes = [];
8387
+ const r = options.onResumeAudio?.();
8388
+ if (r) resumes.push(r);
8389
+ if (ctx.state === "suspended") resumes.push(ctx.resume());
8390
+ if (resumes.length > 0) await Promise.all(resumes);
8391
+ if (!playing) return;
8392
+ seq.start(0);
8393
+ })();
8394
+ const stop = () => {
8395
+ if (!playing) return;
8396
+ seq.stop();
8397
+ finish();
8398
+ };
8399
+ const setVolume = (volume) => {
8400
+ masterVolume = volume;
8401
+ for (const t of seqTracks) t.volume = volume;
8402
+ };
8403
+ const suspend = () => ctx.suspend();
8404
+ const resume = () => ctx.resume();
8405
+ const destroy = () => {
8406
+ seq.stop();
8407
+ playing = false;
8408
+ if (pauseWhenHidden && typeof document !== "undefined") {
8409
+ document.removeEventListener("visibilitychange", onVisibilityChange);
8410
+ }
8411
+ if (ownsCtx) void ctx.close();
8412
+ };
8413
+ return {
8414
+ stop,
8415
+ isPlaying: () => playing,
8416
+ setVolume,
8417
+ suspend,
8418
+ resume,
8419
+ destroy
8420
+ };
8421
+ };
8422
+
8423
+ // src/headless-singing-player.ts
8424
+ var playSingingMML = (_mml, _options = {}) => {
8425
+ return Promise.reject(
8426
+ new Error(
8427
+ "playSingingMML is not implemented yet. See implementation notes at the top of headless-singing-player.ts."
8428
+ )
8429
+ );
8430
+ };
8431
+
8187
8432
  // src/piano-roll.ts
8188
8433
  var createPianoRoll = (options, handlers) => {
8189
8434
  const {
@@ -8635,7 +8880,23 @@ var DEFAULT_CDN = {
8635
8880
  soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs"
8636
8881
  };
8637
8882
  var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
8638
- var TRACK_ROLES = ["melody", "submelody", "bass", "chord"];
8883
+ var TRACK_ROLES = [
8884
+ "melody",
8885
+ "submelody",
8886
+ "bass",
8887
+ "chord",
8888
+ "t4",
8889
+ "t5",
8890
+ "t6",
8891
+ "t7",
8892
+ "t8",
8893
+ "t9",
8894
+ "t10",
8895
+ "t11",
8896
+ "t12",
8897
+ "t13",
8898
+ "t14"
8899
+ ];
8639
8900
  var resolveDefaultVoiceWorkerUrl = () => {
8640
8901
  try {
8641
8902
  return new URL("./voice-worker.js", import.meta.url).href;
@@ -8667,7 +8928,8 @@ var createDtmStudio = async (options = {}) => {
8667
8928
  drumGain.gain.value = options.drumVolume ?? 1;
8668
8929
  drumGain.connect(audioCtx.destination);
8669
8930
  const resumeAudio = () => {
8670
- if (audioCtx.state === "suspended") void audioCtx.resume();
8931
+ if (audioCtx.state === "suspended") return audioCtx.resume();
8932
+ return Promise.resolve();
8671
8933
  };
8672
8934
  const eng = options.engines ?? {};
8673
8935
  const [SoundFont, SoundFont_drum, SoundFont_list] = await Promise.all([
@@ -8771,45 +9033,77 @@ var createDtmStudio = async (options = {}) => {
8771
9033
  })();
8772
9034
  let nameToKey = {};
8773
9035
  const soundFonts = /* @__PURE__ */ new Map();
8774
- const loadedKeyByTrack = /* @__PURE__ */ new Map();
8775
- const loadSoundFont = async (instrumentKey, trackId) => {
8776
- if (loadedKeyByTrack.get(trackId) === instrumentKey) return;
8777
- try {
8778
- const fullName = `${instrumentKey}_${SOUNDFONT_NAME}`;
8779
- soundFonts.set(
8780
- trackId,
8781
- await SoundFont.load({
8782
- ctx: audioCtx,
8783
- fontName: `_tone_${fullName}`,
8784
- url: SoundFont.toURL(fullName)
8785
- })
8786
- );
8787
- loadedKeyByTrack.set(trackId, instrumentKey);
8788
- } catch (e) {
9036
+ const loadingByKey = /* @__PURE__ */ new Map();
9037
+ const loadInstrument = (instrumentKey) => {
9038
+ if (soundFonts.has(instrumentKey)) return Promise.resolve();
9039
+ const inflight = loadingByKey.get(instrumentKey);
9040
+ if (inflight) return inflight;
9041
+ const fullName = `${instrumentKey}_${SOUNDFONT_NAME}`;
9042
+ const p = SoundFont.load({
9043
+ ctx: audioCtx,
9044
+ fontName: `_tone_${fullName}`,
9045
+ url: SoundFont.toURL(fullName)
9046
+ }).then((sf) => {
9047
+ soundFonts.set(instrumentKey, sf);
9048
+ }).catch((e) => {
8789
9049
  console.error(`[dtm] \u697D\u5668 "${instrumentKey}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557`, e);
8790
- }
9050
+ }).finally(() => {
9051
+ loadingByKey.delete(instrumentKey);
9052
+ });
9053
+ loadingByKey.set(instrumentKey, p);
9054
+ return p;
8791
9055
  };
8792
9056
  const defaultPreset = options.defaultPreset ?? "retro_game";
9057
+ const getRoleForTrackIndex = (idx, mode = "simple") => {
9058
+ if (mode === "simple") {
9059
+ if (idx === 0) return "melody";
9060
+ if (idx === 1) return "submelody";
9061
+ if (idx === 2) return "bass";
9062
+ return "chord";
9063
+ } else {
9064
+ return TRACK_ROLES[idx] ?? `t${idx}`;
9065
+ }
9066
+ };
9067
+ const getRoleFromTrackId = (trackId, mode = "simple") => {
9068
+ if (trackId === "melody" || trackId === "submelody" || trackId === "bass" || trackId === "chord") {
9069
+ return trackId;
9070
+ }
9071
+ if (trackId.startsWith("t")) {
9072
+ const idx = Number(trackId.substring(1));
9073
+ if (!isNaN(idx)) {
9074
+ return getRoleForTrackIndex(idx, mode);
9075
+ }
9076
+ }
9077
+ return trackId;
9078
+ };
8793
9079
  const instrumentNameFor = (preset, trackId) => preset[trackId] ?? preset.melody;
8794
- const loadPreset = async (presetKey, trackIds = [...TRACK_ROLES]) => {
9080
+ const resolveSoundFont = (presetKey, trackId, mode = "simple") => {
9081
+ const preset = INSTRUMENT_PRESETS[presetKey];
9082
+ if (!preset) return void 0;
9083
+ const role = getRoleFromTrackId(trackId, mode);
9084
+ const key = nameToKey[instrumentNameFor(preset, role)];
9085
+ return key ? soundFonts.get(key) : void 0;
9086
+ };
9087
+ const loadPreset = async (presetKey, trackIds = [...TRACK_ROLES], mode = "simple") => {
8795
9088
  const preset = INSTRUMENT_PRESETS[presetKey];
8796
9089
  if (!preset) return;
8797
9090
  await listReady;
8798
- await Promise.all(
8799
- trackIds.map((trackId) => {
8800
- const key = nameToKey[instrumentNameFor(preset, trackId)];
8801
- return key ? loadSoundFont(key, trackId) : Promise.resolve();
8802
- })
8803
- );
9091
+ const keys = /* @__PURE__ */ new Set();
9092
+ for (const trackId of trackIds) {
9093
+ const role = getRoleFromTrackId(trackId, mode);
9094
+ const key = nameToKey[instrumentNameFor(preset, role)];
9095
+ if (key) keys.add(key);
9096
+ }
9097
+ await Promise.all([...keys].map((key) => loadInstrument(key)));
8804
9098
  };
8805
- const applyPreset = async (daw, presetKey, trackIds, loadingTarget) => {
9099
+ const applyPreset = async (daw, presetKey, trackIds, loadingTarget, mode = "simple") => {
8806
9100
  const wasPlaying = daw.getPlaybackState() === "playing";
8807
9101
  if (wasPlaying) daw.pause();
8808
9102
  const overlay = loadingTarget ? showLoadingOverlay(loadingTarget) : null;
8809
9103
  daw.setLoading?.(true);
8810
9104
  try {
8811
9105
  daw.setInstrument(presetKey);
8812
- await loadPreset(presetKey, trackIds);
9106
+ await loadPreset(presetKey, trackIds, mode);
8813
9107
  } finally {
8814
9108
  overlay?.remove();
8815
9109
  daw.setLoading?.(false);
@@ -8844,8 +9138,10 @@ var createDtmStudio = async (options = {}) => {
8844
9138
  const key = select.value;
8845
9139
  opts.onChange?.(key);
8846
9140
  const trackIds = opts.getTrackIds?.() ?? [...TRACK_ROLES];
9141
+ const isAdvanced = trackIds.includes("t0");
9142
+ const mode = isAdvanced ? "advanced" : "simple";
8847
9143
  try {
8848
- await applyPreset(daw, key, trackIds, opts.loadingTarget);
9144
+ await applyPreset(daw, key, trackIds, opts.loadingTarget, mode);
8849
9145
  } finally {
8850
9146
  busy = false;
8851
9147
  }
@@ -8870,18 +9166,6 @@ var createDtmStudio = async (options = {}) => {
8870
9166
  await listReady;
8871
9167
  nameToKey = await buildNameToKeyMapping();
8872
9168
  await Promise.all([drumReady, loadPreset(defaultPreset)]);
8873
- const playNote = (e) => {
8874
- const sf = soundFonts.get(e.trackId);
8875
- if (!sf) return;
8876
- sf.play({
8877
- ctx: audioCtx,
8878
- destination: masterGain,
8879
- pitch: e.pitch,
8880
- volume: e.volume,
8881
- when: e.when,
8882
- duration: e.duration
8883
- });
8884
- };
8885
9169
  const playDrum = (e) => {
8886
9170
  if (!SoundFont_drum.font) return;
8887
9171
  SoundFont_drum.play({
@@ -8893,19 +9177,6 @@ var createDtmStudio = async (options = {}) => {
8893
9177
  duration: e.duration
8894
9178
  });
8895
9179
  };
8896
- const sfForPlayerTrack = (trackId) => soundFonts.get(TRACK_ROLES[Number(trackId)] ?? "") ?? soundFonts.get(`t${trackId}`);
8897
- const playPlayerNote = (e) => {
8898
- const sf = sfForPlayerTrack(e.trackId);
8899
- if (!sf) return;
8900
- sf.play({
8901
- ctx: audioCtx,
8902
- destination: masterGain,
8903
- pitch: e.pitch,
8904
- volume: e.volume,
8905
- when: e.when,
8906
- duration: e.duration
8907
- });
8908
- };
8909
9180
  const editorPresetSelects = /* @__PURE__ */ new WeakMap();
8910
9181
  const mountedEditors = [];
8911
9182
  const mountedPlayers = [];
@@ -8914,6 +9185,25 @@ var createDtmStudio = async (options = {}) => {
8914
9185
  const { preset, presetUI, ...dawOverrides } = opts;
8915
9186
  const tracks = dawOverrides.tracks ?? TRACKS_SIMPLE;
8916
9187
  const trackIds = tracks.map((t) => t.id);
9188
+ const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
9189
+ let editorPreset = presetKey;
9190
+ const isAdvancedMode = dawOverrides.mode === "advanced";
9191
+ const playNote = (e) => {
9192
+ const sf = resolveSoundFont(
9193
+ editorPreset,
9194
+ e.trackId,
9195
+ isAdvancedMode ? "advanced" : "simple"
9196
+ );
9197
+ if (!sf) return;
9198
+ sf.play({
9199
+ ctx: audioCtx,
9200
+ destination: masterGain,
9201
+ pitch: e.pitch,
9202
+ volume: e.volume,
9203
+ when: e.when,
9204
+ duration: e.duration
9205
+ });
9206
+ };
8917
9207
  const base = {
8918
9208
  getAudioTime: () => audioCtx.currentTime,
8919
9209
  onResumeAudio: resumeAudio,
@@ -8926,7 +9216,6 @@ var createDtmStudio = async (options = {}) => {
8926
9216
  };
8927
9217
  const daw = mountDAW(target, base);
8928
9218
  mountedEditors.push(daw);
8929
- const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
8930
9219
  const wantPresetUI = presetUI ?? features.presetUI;
8931
9220
  let presetSelect = null;
8932
9221
  if (wantPresetUI) {
@@ -8937,13 +9226,21 @@ var createDtmStudio = async (options = {}) => {
8937
9226
  getTrackIds: () => trackIds,
8938
9227
  value: presetKey,
8939
9228
  loadingTarget: rollEl ?? target,
8940
- position: "prepend"
9229
+ position: "prepend",
9230
+ // 楽器変更時、このエディタの発音解決が使うプリセットも追従させる。
9231
+ onChange: (key) => {
9232
+ editorPreset = key;
9233
+ }
8941
9234
  });
8942
9235
  editorPresetSelects.set(target, presetSelect);
8943
9236
  }
8944
9237
  daw.setInstrument(presetKey);
8945
9238
  daw.setLoading?.(true);
8946
- void loadPreset(presetKey, trackIds).finally(() => {
9239
+ void loadPreset(
9240
+ presetKey,
9241
+ trackIds,
9242
+ isAdvancedMode ? "advanced" : "simple"
9243
+ ).finally(() => {
8947
9244
  daw.setLoading?.(false);
8948
9245
  });
8949
9246
  const destroy = () => {
@@ -9041,10 +9338,43 @@ var createDtmStudio = async (options = {}) => {
9041
9338
  return instance;
9042
9339
  };
9043
9340
  const mountPlayer = (target, mml, opts = {}) => {
9044
- const meta = parseMML(mml, {}).meta ?? {};
9045
- if (meta.instrument && INSTRUMENT_PRESETS[meta.instrument]) {
9046
- void loadPreset(meta.instrument);
9047
- }
9341
+ const parsed = parseMML(mml, {});
9342
+ const meta = parsed.meta ?? {};
9343
+ const playerPreset = meta.instrument && INSTRUMENT_PRESETS[meta.instrument] ? meta.instrument : defaultPreset;
9344
+ const isAdvancedMode = meta.mode === "advanced";
9345
+ const trackIndices = [
9346
+ ...new Set(parsed.placements.map((p) => p.trackIndex))
9347
+ ];
9348
+ const trackIds = trackIndices.map(
9349
+ (idx) => getRoleForTrackIndex(idx, isAdvancedMode ? "advanced" : "simple")
9350
+ );
9351
+ const loadTrackIds = trackIds.length > 0 ? trackIds : [...TRACK_ROLES];
9352
+ void loadPreset(
9353
+ playerPreset,
9354
+ loadTrackIds,
9355
+ isAdvancedMode ? "advanced" : "simple"
9356
+ );
9357
+ const playPlayerNote = (e) => {
9358
+ const idx = Number(e.trackId);
9359
+ const role = getRoleForTrackIndex(
9360
+ idx,
9361
+ isAdvancedMode ? "advanced" : "simple"
9362
+ );
9363
+ const sf = resolveSoundFont(
9364
+ playerPreset,
9365
+ role,
9366
+ isAdvancedMode ? "advanced" : "simple"
9367
+ );
9368
+ if (!sf) return;
9369
+ sf.play({
9370
+ ctx: audioCtx,
9371
+ destination: masterGain,
9372
+ pitch: e.pitch,
9373
+ volume: e.volume,
9374
+ when: e.when,
9375
+ duration: e.duration
9376
+ });
9377
+ };
9048
9378
  const player = mountMmlPlayer(target, mml, {
9049
9379
  getAudioTime: () => audioCtx.currentTime,
9050
9380
  onResumeAudio: resumeAudio,
@@ -9122,6 +9452,7 @@ export {
9122
9452
  createPianoRoll,
9123
9453
  createSequencer,
9124
9454
  createSingingVoices,
9455
+ createSynth,
9125
9456
  createVoiceRegistry,
9126
9457
  decomposeToMonophonic,
9127
9458
  drawGrid,
@@ -9135,6 +9466,7 @@ export {
9135
9466
  extractMidiPlacementsByTrack,
9136
9467
  fetchSoundFontList,
9137
9468
  formatMmlMeta,
9469
+ freqFromPitch,
9138
9470
  generateRandomPattern,
9139
9471
  getDrawOffset,
9140
9472
  getGridCanvas,
@@ -9157,6 +9489,8 @@ export {
9157
9489
  parseLyrics,
9158
9490
  parseMML,
9159
9491
  parseMmlMeta,
9492
+ playMML,
9493
+ playSingingMML,
9160
9494
  setDrawOffset,
9161
9495
  setupRecorder,
9162
9496
  shiftNotes,