@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.mjs
CHANGED
|
@@ -4177,6 +4177,18 @@ var VOICE_IMAGES = {
|
|
|
4177
4177
|
var STEPS_PER_BEAT2 = 48;
|
|
4178
4178
|
var PLAN_TIME = 0.5;
|
|
4179
4179
|
var TICK_INTERVAL_MS = 20;
|
|
4180
|
+
var resolveLoopPoint = (point, bpm, stepsPerBar, sps) => {
|
|
4181
|
+
if ("step" in point) {
|
|
4182
|
+
return point.step;
|
|
4183
|
+
}
|
|
4184
|
+
if ("bar" in point) {
|
|
4185
|
+
return Math.max(0, point.bar - 1) * stepsPerBar;
|
|
4186
|
+
}
|
|
4187
|
+
if ("seconds" in point) {
|
|
4188
|
+
return point.seconds / sps;
|
|
4189
|
+
}
|
|
4190
|
+
return 0;
|
|
4191
|
+
};
|
|
4180
4192
|
var createSequencer = (options) => {
|
|
4181
4193
|
let timeline = [];
|
|
4182
4194
|
let startTime = 0;
|
|
@@ -4186,28 +4198,74 @@ var createSequencer = (options) => {
|
|
|
4186
4198
|
let active = false;
|
|
4187
4199
|
let fromStepValue = 0;
|
|
4188
4200
|
let trackVolumeMap = /* @__PURE__ */ new Map();
|
|
4201
|
+
let isLooping = false;
|
|
4202
|
+
let loopStartStep = 0;
|
|
4203
|
+
let loopEndStep = 0;
|
|
4204
|
+
let loopStartSec = 0;
|
|
4205
|
+
let loopEndSec = 0;
|
|
4206
|
+
let loopDurationSec = 0;
|
|
4207
|
+
let loopStartIndex = 0;
|
|
4208
|
+
let loopBase = 0;
|
|
4209
|
+
let lastPlayStep = 0;
|
|
4189
4210
|
const secondsPerStep = () => 60 / options.getBpm() / STEPS_PER_BEAT2;
|
|
4211
|
+
const getWrappedPlayStep = (time, sps) => {
|
|
4212
|
+
if (!isLooping || loopDurationSec <= 0 || time < loopEndSec) {
|
|
4213
|
+
return fromStepValue + time / sps;
|
|
4214
|
+
}
|
|
4215
|
+
const elapsedInLoop = (time - loopEndSec) % loopDurationSec;
|
|
4216
|
+
return loopStartStep + elapsedInLoop / sps;
|
|
4217
|
+
};
|
|
4190
4218
|
const buildTimeline = (fromStep) => {
|
|
4191
4219
|
timeline = [];
|
|
4192
4220
|
trackVolumeMap = /* @__PURE__ */ new Map();
|
|
4193
4221
|
const sps = secondsPerStep();
|
|
4222
|
+
const bpm = options.getBpm();
|
|
4223
|
+
const stepsPerBar = options.stepsPerBar;
|
|
4224
|
+
const loopOption = options.getLoop?.() ?? false;
|
|
4225
|
+
isLooping = !!loopOption;
|
|
4226
|
+
if (typeof loopOption === "object") {
|
|
4227
|
+
loopStartStep = loopOption.start ? resolveLoopPoint(loopOption.start, bpm, stepsPerBar, sps) : 0;
|
|
4228
|
+
const endVal = loopOption.end ? resolveLoopPoint(loopOption.end, bpm, stepsPerBar, sps) : null;
|
|
4229
|
+
loopEndStep = endVal !== null ? endVal : -1;
|
|
4230
|
+
} else {
|
|
4231
|
+
loopStartStep = 0;
|
|
4232
|
+
loopEndStep = -1;
|
|
4233
|
+
}
|
|
4234
|
+
const startLimit = isLooping ? Math.min(fromStep, loopStartStep) : fromStep;
|
|
4235
|
+
let maxEndStep = 0;
|
|
4194
4236
|
for (const track of options.getTracks()) {
|
|
4195
4237
|
trackVolumeMap.set(track.id, track.volume);
|
|
4196
4238
|
for (const note of track.notes) {
|
|
4239
|
+
if (note.startStep < startLimit) continue;
|
|
4197
4240
|
const relativeStart = note.startStep - fromStep;
|
|
4198
|
-
|
|
4199
|
-
const
|
|
4241
|
+
const when = relativeStart * sps;
|
|
4242
|
+
const duration = note.durationSteps * sps;
|
|
4243
|
+
maxEndStep = Math.max(maxEndStep, note.startStep + note.durationSteps);
|
|
4200
4244
|
timeline.push({
|
|
4201
4245
|
trackId: track.id,
|
|
4202
4246
|
pitch: note.pitch,
|
|
4203
4247
|
volume: track.volume / 100,
|
|
4204
|
-
velocity,
|
|
4205
|
-
when
|
|
4206
|
-
duration
|
|
4248
|
+
velocity: note.velocity ?? DEFAULT_PLAYBACK_VELOCITY,
|
|
4249
|
+
when,
|
|
4250
|
+
duration
|
|
4207
4251
|
});
|
|
4208
4252
|
}
|
|
4209
4253
|
}
|
|
4210
4254
|
timeline.sort((a, b) => a.when - b.when);
|
|
4255
|
+
if (loopEndStep === -1) {
|
|
4256
|
+
loopEndStep = maxEndStep;
|
|
4257
|
+
}
|
|
4258
|
+
loopStartSec = (loopStartStep - fromStep) * sps;
|
|
4259
|
+
loopEndSec = (loopEndStep - fromStep) * sps;
|
|
4260
|
+
loopDurationSec = loopEndSec - loopStartSec;
|
|
4261
|
+
loopStartIndex = 0;
|
|
4262
|
+
while (loopStartIndex < timeline.length) {
|
|
4263
|
+
const noteStartStep = fromStep + timeline[loopStartIndex].when / sps;
|
|
4264
|
+
if (noteStartStep >= loopStartStep - 1e-4) {
|
|
4265
|
+
break;
|
|
4266
|
+
}
|
|
4267
|
+
loopStartIndex++;
|
|
4268
|
+
}
|
|
4211
4269
|
};
|
|
4212
4270
|
const scheduleTick = () => {
|
|
4213
4271
|
const sps = secondsPerStep();
|
|
@@ -4216,9 +4274,16 @@ var createSequencer = (options) => {
|
|
|
4216
4274
|
for (const track of options.getTracks()) {
|
|
4217
4275
|
trackVolumeMap.set(track.id, track.volume);
|
|
4218
4276
|
}
|
|
4219
|
-
while (
|
|
4220
|
-
|
|
4221
|
-
|
|
4277
|
+
while (true) {
|
|
4278
|
+
let ev = timeline[nowIndex];
|
|
4279
|
+
if (nowIndex >= timeline.length || isLooping && ev && ev.when >= loopEndSec) {
|
|
4280
|
+
if (!isLooping || loopDurationSec <= 0) break;
|
|
4281
|
+
nowIndex = loopStartIndex;
|
|
4282
|
+
loopBase += loopDurationSec;
|
|
4283
|
+
ev = timeline[nowIndex];
|
|
4284
|
+
}
|
|
4285
|
+
if (!ev) break;
|
|
4286
|
+
const _when = ev.when + loopBase - time;
|
|
4222
4287
|
if (_when > PLAN_TIME) break;
|
|
4223
4288
|
nowIndex++;
|
|
4224
4289
|
if (soloId && ev.trackId !== soloId) continue;
|
|
@@ -4236,7 +4301,7 @@ var createSequencer = (options) => {
|
|
|
4236
4301
|
const pattern = options.getDrumPattern();
|
|
4237
4302
|
if (pattern && pattern.length > 0) {
|
|
4238
4303
|
const { stepsPerBar } = options;
|
|
4239
|
-
const currentStep = (
|
|
4304
|
+
const currentStep = getWrappedPlayStep(time, sps);
|
|
4240
4305
|
const currentStepInBar = currentStep % stepsPerBar;
|
|
4241
4306
|
const nextStep = currentStepInBar + 4;
|
|
4242
4307
|
const crossedBar = currentStepInBar < 4;
|
|
@@ -4253,19 +4318,44 @@ var createSequencer = (options) => {
|
|
|
4253
4318
|
});
|
|
4254
4319
|
}
|
|
4255
4320
|
}
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4321
|
+
if (time >= 0) {
|
|
4322
|
+
const currentStep = getWrappedPlayStep(time, sps);
|
|
4323
|
+
if (options.cues && options.cues.length > 0 && options.onCue) {
|
|
4324
|
+
const bpm = options.getBpm();
|
|
4325
|
+
const stepsPerBar = options.stepsPerBar;
|
|
4326
|
+
const isCueCrossed = (cueStep, prevStep, currStep) => {
|
|
4327
|
+
if (currStep >= prevStep) {
|
|
4328
|
+
return cueStep > prevStep && cueStep <= currStep;
|
|
4329
|
+
} else {
|
|
4330
|
+
const reachedEnd = cueStep > prevStep && cueStep <= loopEndStep;
|
|
4331
|
+
const startedNew = cueStep >= loopStartStep && cueStep <= currStep;
|
|
4332
|
+
return reachedEnd || startedNew;
|
|
4333
|
+
}
|
|
4334
|
+
};
|
|
4335
|
+
for (const cue of options.cues) {
|
|
4336
|
+
const cueStep = resolveLoopPoint(cue.time, bpm, stepsPerBar, sps);
|
|
4337
|
+
if (isCueCrossed(cueStep, lastPlayStep, currentStep)) {
|
|
4338
|
+
options.onCue(cue.id);
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
lastPlayStep = currentStep;
|
|
4343
|
+
}
|
|
4344
|
+
if (!isLooping) {
|
|
4345
|
+
const last = timeline[timeline.length - 1];
|
|
4346
|
+
const lastWhen = last?.when ?? 0;
|
|
4347
|
+
const lastDuration = last?.duration ?? 0;
|
|
4348
|
+
if (nowIndex >= timeline.length && time > lastWhen + lastDuration + 0.1) {
|
|
4349
|
+
stop();
|
|
4350
|
+
options.onEnd();
|
|
4351
|
+
}
|
|
4262
4352
|
}
|
|
4263
4353
|
};
|
|
4264
4354
|
const animate = () => {
|
|
4265
4355
|
if (!active) return;
|
|
4266
4356
|
const sps = secondsPerStep();
|
|
4267
4357
|
const time = options.getAudioTime() - startTime;
|
|
4268
|
-
options.onTick(
|
|
4358
|
+
options.onTick(getWrappedPlayStep(time, sps));
|
|
4269
4359
|
animationId = requestAnimationFrame(animate);
|
|
4270
4360
|
};
|
|
4271
4361
|
const stop = () => {
|
|
@@ -4287,7 +4377,17 @@ var createSequencer = (options) => {
|
|
|
4287
4377
|
if (timeline.length === 0 && !options.getDrumPattern()?.length) return;
|
|
4288
4378
|
active = true;
|
|
4289
4379
|
startTime = options.getAudioTime() + START_DELAY;
|
|
4380
|
+
const sps = secondsPerStep();
|
|
4290
4381
|
nowIndex = 0;
|
|
4382
|
+
while (nowIndex < timeline.length) {
|
|
4383
|
+
const noteStartStep = fromStepValue + timeline[nowIndex].when / sps;
|
|
4384
|
+
if (noteStartStep >= fromStepValue - 1e-4) {
|
|
4385
|
+
break;
|
|
4386
|
+
}
|
|
4387
|
+
nowIndex++;
|
|
4388
|
+
}
|
|
4389
|
+
loopBase = 0;
|
|
4390
|
+
lastPlayStep = fromStepValue - 1e-4;
|
|
4291
4391
|
intervalId = setInterval(scheduleTick, TICK_INTERVAL_MS);
|
|
4292
4392
|
animationId = requestAnimationFrame(animate);
|
|
4293
4393
|
};
|
|
@@ -4299,6 +4399,73 @@ var createSequencer = (options) => {
|
|
|
4299
4399
|
};
|
|
4300
4400
|
};
|
|
4301
4401
|
|
|
4402
|
+
// src/synth.ts
|
|
4403
|
+
var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
|
|
4404
|
+
var createSynth = (ctx, destination = ctx.destination) => {
|
|
4405
|
+
const playNote = (e) => {
|
|
4406
|
+
const osc = ctx.createOscillator();
|
|
4407
|
+
const gain = ctx.createGain();
|
|
4408
|
+
osc.type = "square";
|
|
4409
|
+
osc.frequency.value = freqFromPitch(e.pitch);
|
|
4410
|
+
const t0 = ctx.currentTime + e.when;
|
|
4411
|
+
const peak = Math.max(1e-4, 0.06 * e.volume * 1.5);
|
|
4412
|
+
gain.gain.setValueAtTime(peak, t0);
|
|
4413
|
+
gain.gain.exponentialRampToValueAtTime(1e-3, t0 + e.duration);
|
|
4414
|
+
osc.connect(gain);
|
|
4415
|
+
if (typeof ctx.createStereoPanner === "function" && e.pan) {
|
|
4416
|
+
const panner = ctx.createStereoPanner();
|
|
4417
|
+
panner.pan.value = Math.max(-1, Math.min(1, e.pan));
|
|
4418
|
+
gain.connect(panner);
|
|
4419
|
+
panner.connect(destination);
|
|
4420
|
+
} else {
|
|
4421
|
+
gain.connect(destination);
|
|
4422
|
+
}
|
|
4423
|
+
osc.start(t0);
|
|
4424
|
+
osc.stop(t0 + e.duration + 0.02);
|
|
4425
|
+
};
|
|
4426
|
+
const playDrum = (e) => {
|
|
4427
|
+
const t0 = ctx.currentTime + e.when;
|
|
4428
|
+
const vol = Math.max(1e-4, Math.min(1, e.velocity));
|
|
4429
|
+
const isKick = e.pitch === 35 || e.pitch === 36;
|
|
4430
|
+
const isSnareLike = e.pitch === 38 || e.pitch === 39 || e.pitch === 40;
|
|
4431
|
+
if (isKick) {
|
|
4432
|
+
const osc = ctx.createOscillator();
|
|
4433
|
+
const g2 = ctx.createGain();
|
|
4434
|
+
osc.frequency.setValueAtTime(150, t0);
|
|
4435
|
+
osc.frequency.exponentialRampToValueAtTime(50, t0 + 0.12);
|
|
4436
|
+
g2.gain.setValueAtTime(vol * 0.9, t0);
|
|
4437
|
+
g2.gain.exponentialRampToValueAtTime(1e-3, t0 + 0.18);
|
|
4438
|
+
osc.connect(g2).connect(destination);
|
|
4439
|
+
osc.start(t0);
|
|
4440
|
+
osc.stop(t0 + 0.2);
|
|
4441
|
+
osc.onended = () => osc.disconnect();
|
|
4442
|
+
return;
|
|
4443
|
+
}
|
|
4444
|
+
const dur = isSnareLike ? 0.18 : 0.05;
|
|
4445
|
+
const length = Math.max(1, Math.floor(ctx.sampleRate * dur));
|
|
4446
|
+
const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
|
|
4447
|
+
const data = buffer.getChannelData(0);
|
|
4448
|
+
for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
|
|
4449
|
+
const src = ctx.createBufferSource();
|
|
4450
|
+
src.buffer = buffer;
|
|
4451
|
+
const filter = ctx.createBiquadFilter();
|
|
4452
|
+
filter.type = isSnareLike ? "bandpass" : "highpass";
|
|
4453
|
+
filter.frequency.value = isSnareLike ? 2e3 : 8e3;
|
|
4454
|
+
const g = ctx.createGain();
|
|
4455
|
+
g.gain.setValueAtTime(vol * (isSnareLike ? 0.7 : 0.4), t0);
|
|
4456
|
+
g.gain.exponentialRampToValueAtTime(1e-3, t0 + dur);
|
|
4457
|
+
src.connect(filter).connect(g).connect(destination);
|
|
4458
|
+
src.start(t0);
|
|
4459
|
+
src.stop(t0 + dur);
|
|
4460
|
+
src.onended = () => {
|
|
4461
|
+
src.disconnect();
|
|
4462
|
+
filter.disconnect();
|
|
4463
|
+
g.disconnect();
|
|
4464
|
+
};
|
|
4465
|
+
};
|
|
4466
|
+
return { playNote, playDrum };
|
|
4467
|
+
};
|
|
4468
|
+
|
|
4302
4469
|
// src/styles.ts
|
|
4303
4470
|
var STYLE_ID = "dtm-daw-styles";
|
|
4304
4471
|
var DAW_CSS = `
|
|
@@ -4969,6 +5136,13 @@ var DAW_CSS = `
|
|
|
4969
5136
|
font-size: 13px;
|
|
4970
5137
|
line-height: 1.6;
|
|
4971
5138
|
}
|
|
5139
|
+
.dtm-modal-body a {
|
|
5140
|
+
color: var(--dtm-primary);
|
|
5141
|
+
text-decoration: underline;
|
|
5142
|
+
}
|
|
5143
|
+
.dtm-modal-body a:hover {
|
|
5144
|
+
color: var(--dtm-accent);
|
|
5145
|
+
}
|
|
4972
5146
|
.dtm-modal-body h4 {
|
|
4973
5147
|
margin: 12px 0 6px 0;
|
|
4974
5148
|
color: var(--dtm-primary);
|
|
@@ -5423,7 +5597,6 @@ var showBalloon = (balloonEl) => {
|
|
|
5423
5597
|
hideActiveBalloon();
|
|
5424
5598
|
}, 3e3);
|
|
5425
5599
|
};
|
|
5426
|
-
var freqFromPitch = (pitch) => 440 * 2 ** ((pitch - 69) / 12);
|
|
5427
5600
|
var mountMmlPlayer = (target, mml, options = {}) => {
|
|
5428
5601
|
injectStyles(target.ownerDocument ?? document);
|
|
5429
5602
|
const {
|
|
@@ -5494,68 +5667,10 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
5494
5667
|
if (!audioCtx) audioCtx = new AudioContext();
|
|
5495
5668
|
return audioCtx;
|
|
5496
5669
|
};
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
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
|
-
};
|
|
5670
|
+
let synthInstance = null;
|
|
5671
|
+
const ensureSynth = () => {
|
|
5672
|
+
if (!synthInstance) synthInstance = createSynth(ensureCtx());
|
|
5673
|
+
return synthInstance;
|
|
5559
5674
|
};
|
|
5560
5675
|
let voices = null;
|
|
5561
5676
|
const ensureVoices = () => {
|
|
@@ -5925,12 +6040,12 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
5925
6040
|
if (em) jumpEmojiAt(em, e.when);
|
|
5926
6041
|
if (lyricTracks.has(trackIdx)) return;
|
|
5927
6042
|
options.onPlayNote?.(e);
|
|
5928
|
-
if (useSynth)
|
|
6043
|
+
if (useSynth) ensureSynth().playNote(e);
|
|
5929
6044
|
},
|
|
5930
6045
|
onPlayDrum: (e) => {
|
|
5931
6046
|
const velocity = e.velocity * (trackVolume / 100);
|
|
5932
6047
|
options.onPlayDrum?.({ ...e, velocity });
|
|
5933
|
-
if (useSynth)
|
|
6048
|
+
if (useSynth) ensureSynth().playDrum({ ...e, velocity });
|
|
5934
6049
|
},
|
|
5935
6050
|
onTick: (step) => {
|
|
5936
6051
|
renderPlayhead(step);
|
|
@@ -6005,13 +6120,19 @@ var mountMmlPlayer = (target, mml, options = {}) => {
|
|
|
6005
6120
|
if (activePlayer && activePlayer !== instance) activePlayer.stop();
|
|
6006
6121
|
activePlayer = instance;
|
|
6007
6122
|
setPlayingUI(true);
|
|
6008
|
-
void
|
|
6009
|
-
|
|
6010
|
-
const
|
|
6011
|
-
if (
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6123
|
+
void (async () => {
|
|
6124
|
+
const resumes = [];
|
|
6125
|
+
const r = options.onResumeAudio?.();
|
|
6126
|
+
if (r) resumes.push(r);
|
|
6127
|
+
if (useSynth) {
|
|
6128
|
+
const ctx = ensureCtx();
|
|
6129
|
+
if (ctx.state === "suspended") resumes.push(ctx.resume());
|
|
6130
|
+
}
|
|
6131
|
+
if (resumes.length > 0) await Promise.all(resumes);
|
|
6132
|
+
if (!playing || activePlayer !== instance) return;
|
|
6133
|
+
if (voicesAvailable && lyricTracks.size > 0) ensureVoices().reset();
|
|
6134
|
+
await startWhenReady();
|
|
6135
|
+
})();
|
|
6015
6136
|
};
|
|
6016
6137
|
const stop = () => {
|
|
6017
6138
|
if (!playing) return;
|
|
@@ -7026,8 +7147,8 @@ var mountDAW = (target, options = {}) => {
|
|
|
7026
7147
|
stepsPerBar: renderConfig.stepsPerBar
|
|
7027
7148
|
});
|
|
7028
7149
|
const play = async () => {
|
|
7029
|
-
options.onResumeAudio?.();
|
|
7030
7150
|
if (playbackState === "playing") return;
|
|
7151
|
+
await options.onResumeAudio?.();
|
|
7031
7152
|
const fromStep = playbackState === "paused" ? pausedPlayStep : playStartStep;
|
|
7032
7153
|
options.singingVoices?.reset();
|
|
7033
7154
|
const lyricMap = buildLyricsMap();
|
|
@@ -8184,6 +8305,123 @@ var INSTRUMENT_PRESETS = {
|
|
|
8184
8305
|
}
|
|
8185
8306
|
};
|
|
8186
8307
|
|
|
8308
|
+
// src/headless-player.ts
|
|
8309
|
+
var STEPS_PER_BAR2 = 192;
|
|
8310
|
+
var playMML = (mml, options = {}) => {
|
|
8311
|
+
const { placements, bpm: parsedBpm, meta } = parseMML(mml);
|
|
8312
|
+
const bpm = parsedBpm ?? options.defaultBpm ?? DEFAULT_BPM;
|
|
8313
|
+
const drumPatternDict = options.drumPatterns ?? DRUM_PATTERNS;
|
|
8314
|
+
const drumPattern = meta.drum ? drumPatternDict[meta.drum] ?? null : null;
|
|
8315
|
+
let masterVolume = meta.volume ?? options.volume ?? 100;
|
|
8316
|
+
const trackIndices = [...new Set(placements.map((p) => p.trackIndex))].sort(
|
|
8317
|
+
(a, b) => a - b
|
|
8318
|
+
);
|
|
8319
|
+
const seqTracks = trackIndices.map((index) => {
|
|
8320
|
+
let id = 0;
|
|
8321
|
+
const notes = placements.filter((p) => p.trackIndex === index).map((p) => ({
|
|
8322
|
+
id: id++,
|
|
8323
|
+
startStep: p.startStep,
|
|
8324
|
+
durationSteps: p.durationSteps,
|
|
8325
|
+
pitch: p.pitch,
|
|
8326
|
+
velocity: 100
|
|
8327
|
+
}));
|
|
8328
|
+
return { id: String(index), volume: masterVolume, notes };
|
|
8329
|
+
});
|
|
8330
|
+
const ownsCtx = !options.audioContext;
|
|
8331
|
+
const ctx = options.audioContext ?? new AudioContext();
|
|
8332
|
+
const destination = options.destination ?? ctx.destination;
|
|
8333
|
+
const useSynth = options.synth ?? !options.onPlayNote;
|
|
8334
|
+
const synth = useSynth ? createSynth(ctx, destination) : null;
|
|
8335
|
+
const pauseWhenHidden = options.pauseWhenHidden ?? ownsCtx;
|
|
8336
|
+
let playing = false;
|
|
8337
|
+
const seq = createSequencer({
|
|
8338
|
+
getTracks: () => seqTracks,
|
|
8339
|
+
getBpm: () => bpm,
|
|
8340
|
+
getPlayStartStep: () => 0,
|
|
8341
|
+
getDrumPattern: () => drumPattern,
|
|
8342
|
+
getSoloTrackId: () => null,
|
|
8343
|
+
getLoop: () => options.loop ?? false,
|
|
8344
|
+
cues: options.cues,
|
|
8345
|
+
onCue: options.onCue,
|
|
8346
|
+
getAudioTime: () => ctx.currentTime,
|
|
8347
|
+
onPlayNote: (e) => {
|
|
8348
|
+
options.onPlayNote?.(e);
|
|
8349
|
+
synth?.playNote(e);
|
|
8350
|
+
},
|
|
8351
|
+
onPlayDrum: (e) => {
|
|
8352
|
+
const velocity = e.velocity * (masterVolume / 100);
|
|
8353
|
+
options.onPlayDrum?.({ ...e, velocity });
|
|
8354
|
+
synth?.playDrum({ ...e, velocity });
|
|
8355
|
+
},
|
|
8356
|
+
onTick: () => {
|
|
8357
|
+
},
|
|
8358
|
+
onEnd: () => finish(),
|
|
8359
|
+
stepsPerBar: STEPS_PER_BAR2
|
|
8360
|
+
});
|
|
8361
|
+
const finish = () => {
|
|
8362
|
+
if (!playing) return;
|
|
8363
|
+
playing = false;
|
|
8364
|
+
options.onStop?.();
|
|
8365
|
+
};
|
|
8366
|
+
const onVisibilityChange = () => {
|
|
8367
|
+
if (!playing) return;
|
|
8368
|
+
if (document.hidden) {
|
|
8369
|
+
void ctx.suspend();
|
|
8370
|
+
} else if (ctx.state === "suspended") {
|
|
8371
|
+
void ctx.resume();
|
|
8372
|
+
}
|
|
8373
|
+
};
|
|
8374
|
+
if (pauseWhenHidden && typeof document !== "undefined") {
|
|
8375
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
8376
|
+
}
|
|
8377
|
+
playing = true;
|
|
8378
|
+
void (async () => {
|
|
8379
|
+
const resumes = [];
|
|
8380
|
+
const r = options.onResumeAudio?.();
|
|
8381
|
+
if (r) resumes.push(r);
|
|
8382
|
+
if (ctx.state === "suspended") resumes.push(ctx.resume());
|
|
8383
|
+
if (resumes.length > 0) await Promise.all(resumes);
|
|
8384
|
+
if (!playing) return;
|
|
8385
|
+
seq.start(0);
|
|
8386
|
+
})();
|
|
8387
|
+
const stop = () => {
|
|
8388
|
+
if (!playing) return;
|
|
8389
|
+
seq.stop();
|
|
8390
|
+
finish();
|
|
8391
|
+
};
|
|
8392
|
+
const setVolume = (volume) => {
|
|
8393
|
+
masterVolume = volume;
|
|
8394
|
+
for (const t of seqTracks) t.volume = volume;
|
|
8395
|
+
};
|
|
8396
|
+
const suspend = () => ctx.suspend();
|
|
8397
|
+
const resume = () => ctx.resume();
|
|
8398
|
+
const destroy = () => {
|
|
8399
|
+
seq.stop();
|
|
8400
|
+
playing = false;
|
|
8401
|
+
if (pauseWhenHidden && typeof document !== "undefined") {
|
|
8402
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
8403
|
+
}
|
|
8404
|
+
if (ownsCtx) void ctx.close();
|
|
8405
|
+
};
|
|
8406
|
+
return {
|
|
8407
|
+
stop,
|
|
8408
|
+
isPlaying: () => playing,
|
|
8409
|
+
setVolume,
|
|
8410
|
+
suspend,
|
|
8411
|
+
resume,
|
|
8412
|
+
destroy
|
|
8413
|
+
};
|
|
8414
|
+
};
|
|
8415
|
+
|
|
8416
|
+
// src/headless-singing-player.ts
|
|
8417
|
+
var playSingingMML = (_mml, _options = {}) => {
|
|
8418
|
+
return Promise.reject(
|
|
8419
|
+
new Error(
|
|
8420
|
+
"playSingingMML is not implemented yet. See implementation notes at the top of headless-singing-player.ts."
|
|
8421
|
+
)
|
|
8422
|
+
);
|
|
8423
|
+
};
|
|
8424
|
+
|
|
8187
8425
|
// src/piano-roll.ts
|
|
8188
8426
|
var createPianoRoll = (options, handlers) => {
|
|
8189
8427
|
const {
|
|
@@ -8635,7 +8873,23 @@ var DEFAULT_CDN = {
|
|
|
8635
8873
|
soundFontList: "https://rpgen3.github.io/soundfont/mjs/surikov/SoundFont_list.mjs"
|
|
8636
8874
|
};
|
|
8637
8875
|
var SOUNDFONT_NAME = "FluidR3_GM_sf2_file";
|
|
8638
|
-
var TRACK_ROLES = [
|
|
8876
|
+
var TRACK_ROLES = [
|
|
8877
|
+
"melody",
|
|
8878
|
+
"submelody",
|
|
8879
|
+
"bass",
|
|
8880
|
+
"chord",
|
|
8881
|
+
"t4",
|
|
8882
|
+
"t5",
|
|
8883
|
+
"t6",
|
|
8884
|
+
"t7",
|
|
8885
|
+
"t8",
|
|
8886
|
+
"t9",
|
|
8887
|
+
"t10",
|
|
8888
|
+
"t11",
|
|
8889
|
+
"t12",
|
|
8890
|
+
"t13",
|
|
8891
|
+
"t14"
|
|
8892
|
+
];
|
|
8639
8893
|
var resolveDefaultVoiceWorkerUrl = () => {
|
|
8640
8894
|
try {
|
|
8641
8895
|
return new URL("./voice-worker.js", import.meta.url).href;
|
|
@@ -8667,7 +8921,8 @@ var createDtmStudio = async (options = {}) => {
|
|
|
8667
8921
|
drumGain.gain.value = options.drumVolume ?? 1;
|
|
8668
8922
|
drumGain.connect(audioCtx.destination);
|
|
8669
8923
|
const resumeAudio = () => {
|
|
8670
|
-
if (audioCtx.state === "suspended")
|
|
8924
|
+
if (audioCtx.state === "suspended") return audioCtx.resume();
|
|
8925
|
+
return Promise.resolve();
|
|
8671
8926
|
};
|
|
8672
8927
|
const eng = options.engines ?? {};
|
|
8673
8928
|
const [SoundFont, SoundFont_drum, SoundFont_list] = await Promise.all([
|
|
@@ -8771,36 +9026,44 @@ var createDtmStudio = async (options = {}) => {
|
|
|
8771
9026
|
})();
|
|
8772
9027
|
let nameToKey = {};
|
|
8773
9028
|
const soundFonts = /* @__PURE__ */ new Map();
|
|
8774
|
-
const
|
|
8775
|
-
const
|
|
8776
|
-
if (
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
loadedKeyByTrack.set(trackId, instrumentKey);
|
|
8788
|
-
} catch (e) {
|
|
9029
|
+
const loadingByKey = /* @__PURE__ */ new Map();
|
|
9030
|
+
const loadInstrument = (instrumentKey) => {
|
|
9031
|
+
if (soundFonts.has(instrumentKey)) return Promise.resolve();
|
|
9032
|
+
const inflight = loadingByKey.get(instrumentKey);
|
|
9033
|
+
if (inflight) return inflight;
|
|
9034
|
+
const fullName = `${instrumentKey}_${SOUNDFONT_NAME}`;
|
|
9035
|
+
const p = SoundFont.load({
|
|
9036
|
+
ctx: audioCtx,
|
|
9037
|
+
fontName: `_tone_${fullName}`,
|
|
9038
|
+
url: SoundFont.toURL(fullName)
|
|
9039
|
+
}).then((sf) => {
|
|
9040
|
+
soundFonts.set(instrumentKey, sf);
|
|
9041
|
+
}).catch((e) => {
|
|
8789
9042
|
console.error(`[dtm] \u697D\u5668 "${instrumentKey}" \u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557`, e);
|
|
8790
|
-
}
|
|
9043
|
+
}).finally(() => {
|
|
9044
|
+
loadingByKey.delete(instrumentKey);
|
|
9045
|
+
});
|
|
9046
|
+
loadingByKey.set(instrumentKey, p);
|
|
9047
|
+
return p;
|
|
8791
9048
|
};
|
|
8792
9049
|
const defaultPreset = options.defaultPreset ?? "retro_game";
|
|
8793
9050
|
const instrumentNameFor = (preset, trackId) => preset[trackId] ?? preset.melody;
|
|
9051
|
+
const resolveSoundFont = (presetKey, trackId) => {
|
|
9052
|
+
const preset = INSTRUMENT_PRESETS[presetKey];
|
|
9053
|
+
if (!preset) return void 0;
|
|
9054
|
+
const key = nameToKey[instrumentNameFor(preset, trackId)];
|
|
9055
|
+
return key ? soundFonts.get(key) : void 0;
|
|
9056
|
+
};
|
|
8794
9057
|
const loadPreset = async (presetKey, trackIds = [...TRACK_ROLES]) => {
|
|
8795
9058
|
const preset = INSTRUMENT_PRESETS[presetKey];
|
|
8796
9059
|
if (!preset) return;
|
|
8797
9060
|
await listReady;
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
);
|
|
9061
|
+
const keys = /* @__PURE__ */ new Set();
|
|
9062
|
+
for (const trackId of trackIds) {
|
|
9063
|
+
const key = nameToKey[instrumentNameFor(preset, trackId)];
|
|
9064
|
+
if (key) keys.add(key);
|
|
9065
|
+
}
|
|
9066
|
+
await Promise.all([...keys].map((key) => loadInstrument(key)));
|
|
8804
9067
|
};
|
|
8805
9068
|
const applyPreset = async (daw, presetKey, trackIds, loadingTarget) => {
|
|
8806
9069
|
const wasPlaying = daw.getPlaybackState() === "playing";
|
|
@@ -8870,18 +9133,6 @@ var createDtmStudio = async (options = {}) => {
|
|
|
8870
9133
|
await listReady;
|
|
8871
9134
|
nameToKey = await buildNameToKeyMapping();
|
|
8872
9135
|
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
9136
|
const playDrum = (e) => {
|
|
8886
9137
|
if (!SoundFont_drum.font) return;
|
|
8887
9138
|
SoundFont_drum.play({
|
|
@@ -8893,19 +9144,6 @@ var createDtmStudio = async (options = {}) => {
|
|
|
8893
9144
|
duration: e.duration
|
|
8894
9145
|
});
|
|
8895
9146
|
};
|
|
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
9147
|
const editorPresetSelects = /* @__PURE__ */ new WeakMap();
|
|
8910
9148
|
const mountedEditors = [];
|
|
8911
9149
|
const mountedPlayers = [];
|
|
@@ -8914,6 +9152,20 @@ var createDtmStudio = async (options = {}) => {
|
|
|
8914
9152
|
const { preset, presetUI, ...dawOverrides } = opts;
|
|
8915
9153
|
const tracks = dawOverrides.tracks ?? TRACKS_SIMPLE;
|
|
8916
9154
|
const trackIds = tracks.map((t) => t.id);
|
|
9155
|
+
const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
|
|
9156
|
+
let editorPreset = presetKey;
|
|
9157
|
+
const playNote = (e) => {
|
|
9158
|
+
const sf = resolveSoundFont(editorPreset, e.trackId);
|
|
9159
|
+
if (!sf) return;
|
|
9160
|
+
sf.play({
|
|
9161
|
+
ctx: audioCtx,
|
|
9162
|
+
destination: masterGain,
|
|
9163
|
+
pitch: e.pitch,
|
|
9164
|
+
volume: e.volume,
|
|
9165
|
+
when: e.when,
|
|
9166
|
+
duration: e.duration
|
|
9167
|
+
});
|
|
9168
|
+
};
|
|
8917
9169
|
const base = {
|
|
8918
9170
|
getAudioTime: () => audioCtx.currentTime,
|
|
8919
9171
|
onResumeAudio: resumeAudio,
|
|
@@ -8926,7 +9178,6 @@ var createDtmStudio = async (options = {}) => {
|
|
|
8926
9178
|
};
|
|
8927
9179
|
const daw = mountDAW(target, base);
|
|
8928
9180
|
mountedEditors.push(daw);
|
|
8929
|
-
const presetKey = preset && INSTRUMENT_PRESETS[preset] ? preset : defaultPreset;
|
|
8930
9181
|
const wantPresetUI = presetUI ?? features.presetUI;
|
|
8931
9182
|
let presetSelect = null;
|
|
8932
9183
|
if (wantPresetUI) {
|
|
@@ -8937,7 +9188,11 @@ var createDtmStudio = async (options = {}) => {
|
|
|
8937
9188
|
getTrackIds: () => trackIds,
|
|
8938
9189
|
value: presetKey,
|
|
8939
9190
|
loadingTarget: rollEl ?? target,
|
|
8940
|
-
position: "prepend"
|
|
9191
|
+
position: "prepend",
|
|
9192
|
+
// 楽器変更時、このエディタの発音解決が使うプリセットも追従させる。
|
|
9193
|
+
onChange: (key) => {
|
|
9194
|
+
editorPreset = key;
|
|
9195
|
+
}
|
|
8941
9196
|
});
|
|
8942
9197
|
editorPresetSelects.set(target, presetSelect);
|
|
8943
9198
|
}
|
|
@@ -9041,10 +9296,28 @@ var createDtmStudio = async (options = {}) => {
|
|
|
9041
9296
|
return instance;
|
|
9042
9297
|
};
|
|
9043
9298
|
const mountPlayer = (target, mml, opts = {}) => {
|
|
9044
|
-
const
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9299
|
+
const parsed = parseMML(mml, {});
|
|
9300
|
+
const meta = parsed.meta ?? {};
|
|
9301
|
+
const playerPreset = meta.instrument && INSTRUMENT_PRESETS[meta.instrument] ? meta.instrument : defaultPreset;
|
|
9302
|
+
const trackIndices = [
|
|
9303
|
+
...new Set(parsed.placements.map((p) => p.trackIndex))
|
|
9304
|
+
];
|
|
9305
|
+
const trackIds = trackIndices.map((idx) => TRACK_ROLES[idx] ?? `t${idx}`);
|
|
9306
|
+
const loadTrackIds = trackIds.length > 0 ? trackIds : [...TRACK_ROLES];
|
|
9307
|
+
void loadPreset(playerPreset, loadTrackIds);
|
|
9308
|
+
const playPlayerNote = (e) => {
|
|
9309
|
+
const role = TRACK_ROLES[Number(e.trackId)] ?? `t${e.trackId}`;
|
|
9310
|
+
const sf = resolveSoundFont(playerPreset, role);
|
|
9311
|
+
if (!sf) return;
|
|
9312
|
+
sf.play({
|
|
9313
|
+
ctx: audioCtx,
|
|
9314
|
+
destination: masterGain,
|
|
9315
|
+
pitch: e.pitch,
|
|
9316
|
+
volume: e.volume,
|
|
9317
|
+
when: e.when,
|
|
9318
|
+
duration: e.duration
|
|
9319
|
+
});
|
|
9320
|
+
};
|
|
9048
9321
|
const player = mountMmlPlayer(target, mml, {
|
|
9049
9322
|
getAudioTime: () => audioCtx.currentTime,
|
|
9050
9323
|
onResumeAudio: resumeAudio,
|
|
@@ -9122,6 +9395,7 @@ export {
|
|
|
9122
9395
|
createPianoRoll,
|
|
9123
9396
|
createSequencer,
|
|
9124
9397
|
createSingingVoices,
|
|
9398
|
+
createSynth,
|
|
9125
9399
|
createVoiceRegistry,
|
|
9126
9400
|
decomposeToMonophonic,
|
|
9127
9401
|
drawGrid,
|
|
@@ -9135,6 +9409,7 @@ export {
|
|
|
9135
9409
|
extractMidiPlacementsByTrack,
|
|
9136
9410
|
fetchSoundFontList,
|
|
9137
9411
|
formatMmlMeta,
|
|
9412
|
+
freqFromPitch,
|
|
9138
9413
|
generateRandomPattern,
|
|
9139
9414
|
getDrawOffset,
|
|
9140
9415
|
getGridCanvas,
|
|
@@ -9157,6 +9432,8 @@ export {
|
|
|
9157
9432
|
parseLyrics,
|
|
9158
9433
|
parseMML,
|
|
9159
9434
|
parseMmlMeta,
|
|
9435
|
+
playMML,
|
|
9436
|
+
playSingingMML,
|
|
9160
9437
|
setDrawOffset,
|
|
9161
9438
|
setupRecorder,
|
|
9162
9439
|
shiftNotes,
|