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