@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/README.md +66 -0
- package/dist/index.d.mts +204 -2
- package/dist/index.d.ts +204 -2
- package/dist/index.js +425 -144
- package/dist/index.mjs +421 -144
- package/package.json +1 -1
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
|
-
|
|
4304
|
-
const
|
|
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
|
|
4311
|
-
duration
|
|
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 (
|
|
4325
|
-
|
|
4326
|
-
|
|
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 = (
|
|
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
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
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(
|
|
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
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
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)
|
|
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)
|
|
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
|
|
6114
|
-
|
|
6115
|
-
const
|
|
6116
|
-
if (
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
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 = [
|
|
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")
|
|
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
|
|
8881
|
-
const
|
|
8882
|
-
if (
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
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
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
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
|
|
9151
|
-
|
|
9152
|
-
|
|
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,
|