midi-audio-player 1.1.2 → 2.0.1
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 +472 -86
- package/dist/index.js +1733 -232
- package/dist/index.js.map +4 -4
- package/dist/index.mjs +13 -13
- package/dist/index.mjs.map +4 -4
- package/dist/midi-audio-player.js +1732 -232
- package/dist/midi-audio-player.min.js +13 -14
- package/index.d.ts +905 -0
- package/index.js +0 -1
- package/package.json +21 -18
- package/src/libraries/audiocompressor.js +186 -0
- package/src/libraries/indexeddbstorage.js +107 -0
- package/src/midiaudioplayer.js +1378 -51
- package/bin/cli.js +0 -27
- package/dist/midi-audio-player.js.map +0 -7
- package/dist/midi-audio-player.min.js.map +0 -7
- package/src/downloader.js +0 -33
- package/src/presets/defaultpreset.json +0 -116
- package/src/webaudiofontplayer.js +0 -264
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
███╗ ███╗██╗██████╗ ██╗ █████╗ ██╗ ██╗██████╗ ██╗ ██████╗ ██████╗ ██╗ █████╗ ██╗ ██╗███████╗██████╗
|
|
4
|
+
████╗ ████║██║██╔══██╗██║██╔══██╗██║ ██║██╔══██╗██║██╔═══██╗██╔══██╗██║ ██╔══██╗╚██╗ ██╔╝██╔════╝██╔══██╗
|
|
5
|
+
██╔████╔██║██║██║ ██║██║███████║██║ ██║██║ ██║██║██║ ██║██████╔╝██║ ███████║ ╚████╔╝ █████╗ ██████╔╝
|
|
6
|
+
██║╚██╔╝██║██║██║ ██║██║██╔══██║██║ ██║██║ ██║██║██║ ██║██╔═══╝ ██║ ██╔══██║ ╚██╔╝ ██╔══╝ ██╔══██╗
|
|
7
|
+
██║ ╚═╝ ██║██║██████╔╝██║██║ ██║╚██████╔╝██████╔╝██║╚██████╔╝██║ ███████╗██║ ██║ ██║ ███████╗██║ ██║
|
|
8
|
+
╚═╝ ╚═╝╚═╝╚═════╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
|
|
9
|
+
|
|
10
|
+
Version: 2.0.1
|
|
11
|
+
Build: 2026-05-31 02:44:14
|
|
12
|
+
Author: Maxime Larrivée-Roy <mlarriveeroy@gmail.com>
|
|
13
|
+
Github: https://github.com/webaudiofonts/midi-audio-player/
|
|
14
|
+
Website: https://webaudiofonts.com/midiaudioplayer/
|
|
9
15
|
|
|
10
|
-
Version: 1.1.2
|
|
11
|
-
Généré: 2026-05-10 00:19:40
|
|
12
|
-
Auteur: Maxime Larrivée-Roy <mlarriveeroy@gmail.com>
|
|
13
|
-
Github: https://github.com/ZmotriN/midi-audio-player/
|
|
14
|
-
Website: https://zmotrin.github.io/midi-audio-player/
|
|
15
|
-
|
|
16
16
|
*/
|
|
17
17
|
(() => {
|
|
18
18
|
// node_modules/midi-player-js/build/index.browser.js
|
|
@@ -1212,26 +1212,94 @@
|
|
|
1212
1212
|
Constants
|
|
1213
1213
|
};
|
|
1214
1214
|
|
|
1215
|
-
//
|
|
1216
|
-
var WebAudioFontPlayer = class {
|
|
1215
|
+
// node_modules/webaudiofontplayer/dist/index.js
|
|
1216
|
+
var WebAudioFontPlayer = class _WebAudioFontPlayer {
|
|
1217
1217
|
#audioCtx = null;
|
|
1218
|
+
#compressor = null;
|
|
1218
1219
|
#preset = null;
|
|
1219
1220
|
#envelopes = [];
|
|
1220
1221
|
#afterTime = 0.05;
|
|
1221
1222
|
#nearZero = 1e-6;
|
|
1222
|
-
|
|
1223
|
+
#bendRange = 2;
|
|
1224
|
+
#mainGain = null;
|
|
1225
|
+
#volumeValue = 0.7;
|
|
1226
|
+
#expressionValue = 1;
|
|
1227
|
+
#expressionGain = null;
|
|
1228
|
+
#sustain = false;
|
|
1229
|
+
#pitchBendValue = 8192;
|
|
1230
|
+
#notesWaitingForSustain = /* @__PURE__ */ new Set();
|
|
1231
|
+
constructor(preset, audioCtx, compressor = null, callback = null) {
|
|
1223
1232
|
this.#audioCtx = audioCtx;
|
|
1233
|
+
this.#compressor = compressor;
|
|
1234
|
+
this.#mainGain = this.#audioCtx.createGain();
|
|
1235
|
+
this.#mainGain.gain.setValueAtTime(this.#volumeValue, this.#audioCtx.currentTime);
|
|
1236
|
+
this.#expressionGain = this.#audioCtx.createGain();
|
|
1237
|
+
this.#expressionGain.gain.setValueAtTime(this.#expressionValue, this.#audioCtx.currentTime);
|
|
1238
|
+
this.#mainGain.connect(this.#expressionGain);
|
|
1239
|
+
this.#expressionGain.connect(this.#compressor ? this.#compressor.input : this.#audioCtx.destination);
|
|
1240
|
+
this.setPreset(preset).then(() => {
|
|
1241
|
+
if (typeof callback === "function") callback();
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
get preset() {
|
|
1245
|
+
return this.#preset;
|
|
1246
|
+
}
|
|
1247
|
+
set preset(preset) {
|
|
1248
|
+
this.setPreset(preset);
|
|
1249
|
+
}
|
|
1250
|
+
static load(preset, audioCtx, compressor = null) {
|
|
1251
|
+
return new Promise((resolve) => {
|
|
1252
|
+
const player = new _WebAudioFontPlayer(preset, audioCtx, compressor, () => resolve(player));
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
async setPreset(preset) {
|
|
1224
1256
|
this.#preset = preset;
|
|
1225
|
-
this.#preset.zones.map((zone) => this.#adjustZone(zone));
|
|
1257
|
+
await Promise.all(this.#preset.zones.map((zone) => this.#adjustZone(zone)));
|
|
1258
|
+
}
|
|
1259
|
+
close() {
|
|
1260
|
+
const now = this.#audioCtx.currentTime;
|
|
1261
|
+
this.#envelopes.forEach((envelope) => {
|
|
1262
|
+
try {
|
|
1263
|
+
envelope.gain.cancelScheduledValues(0);
|
|
1264
|
+
if (envelope.audioBufferSourceNode) {
|
|
1265
|
+
envelope.audioBufferSourceNode.stop(now);
|
|
1266
|
+
envelope.audioBufferSourceNode.disconnect();
|
|
1267
|
+
envelope.audioBufferSourceNode = null;
|
|
1268
|
+
}
|
|
1269
|
+
} catch (e) {
|
|
1270
|
+
}
|
|
1271
|
+
try {
|
|
1272
|
+
envelope.disconnect();
|
|
1273
|
+
} catch (e) {
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
this.#envelopes = [];
|
|
1277
|
+
this.#notesWaitingForSustain.clear();
|
|
1278
|
+
try {
|
|
1279
|
+
this.#mainGain.disconnect();
|
|
1280
|
+
} catch (e) {
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
this.#expressionGain.disconnect();
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
}
|
|
1286
|
+
this.#mainGain = null;
|
|
1287
|
+
this.#expressionGain = null;
|
|
1288
|
+
this.#preset = null;
|
|
1289
|
+
this.#compressor = null;
|
|
1290
|
+
this.#audioCtx = null;
|
|
1226
1291
|
}
|
|
1227
1292
|
queueWaveTable(when, pitch, duration, volume, slides) {
|
|
1228
1293
|
if (this.#audioCtx.state === "suspended") this.#audioCtx.resume().catch(() => {
|
|
1229
1294
|
});
|
|
1230
1295
|
const vol = this.#limitVolume(volume);
|
|
1231
|
-
const zone = this.#findZone(pitch);
|
|
1296
|
+
const zone = this.#findZone(Math.round(pitch));
|
|
1232
1297
|
if (!zone?.buffer) return null;
|
|
1233
|
-
const
|
|
1234
|
-
const
|
|
1298
|
+
const baseDetuneCents = zone.originalPitch - 100 * zone.coarseTune - zone.fineTune;
|
|
1299
|
+
const originalPitchCents = pitch * 100;
|
|
1300
|
+
const currentBendCents = (this.#pitchBendValue - 8192) / 8192 * this.#bendRange * 100;
|
|
1301
|
+
const totalCents = originalPitchCents - baseDetuneCents + currentBendCents;
|
|
1302
|
+
const playbackRate = Math.pow(2, totalCents / 1200);
|
|
1235
1303
|
const startWhen = Math.max(when, this.#audioCtx.currentTime);
|
|
1236
1304
|
let waveDuration = duration + this.#afterTime;
|
|
1237
1305
|
const loop = zone.loopStart >= 1 && zone.loopStart < zone.loopEnd;
|
|
@@ -1244,7 +1312,9 @@
|
|
|
1244
1312
|
if (slides?.length > 0) {
|
|
1245
1313
|
source.playbackRate.setValueAtTime(playbackRate, startWhen);
|
|
1246
1314
|
slides.forEach((s) => {
|
|
1247
|
-
const
|
|
1315
|
+
const slidePitchCents = (pitch + s.delta) * 100;
|
|
1316
|
+
const totalSlideCents = slidePitchCents - baseDetuneCents + currentBendCents;
|
|
1317
|
+
const newRate = Math.pow(2, totalSlideCents / 1200);
|
|
1248
1318
|
source.playbackRate.linearRampToValueAtTime(newRate, startWhen + s.when);
|
|
1249
1319
|
});
|
|
1250
1320
|
}
|
|
@@ -1260,12 +1330,10 @@
|
|
|
1260
1330
|
envelope.audioBufferSourceNode = source;
|
|
1261
1331
|
envelope.when = startWhen;
|
|
1262
1332
|
envelope.duration = waveDuration;
|
|
1333
|
+
envelope.pitch = pitch;
|
|
1334
|
+
envelope.baseDetune = baseDetuneCents;
|
|
1263
1335
|
return envelope;
|
|
1264
1336
|
}
|
|
1265
|
-
queueChord(prst, w, pchs, d, v, s) {
|
|
1266
|
-
const vol = this.#limitVolume(v);
|
|
1267
|
-
return pchs.map((p, i) => this.queueWaveTable(this.#audioCtx, this.#audioCtx.destination, prst, w, p, d, vol - Math.random() * 0.01, s?.[i])).filter(Boolean);
|
|
1268
|
-
}
|
|
1269
1337
|
async cancelQueue() {
|
|
1270
1338
|
this.#envelopes.forEach((e) => {
|
|
1271
1339
|
e.gain.cancelScheduledValues(0);
|
|
@@ -1277,41 +1345,68 @@
|
|
|
1277
1345
|
}
|
|
1278
1346
|
});
|
|
1279
1347
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1348
|
+
isSustainActive() {
|
|
1349
|
+
return this.#sustain;
|
|
1350
|
+
}
|
|
1351
|
+
registerSustainNote(cancelFn) {
|
|
1352
|
+
this.#notesWaitingForSustain.add(cancelFn);
|
|
1353
|
+
}
|
|
1354
|
+
setPitchBend(value) {
|
|
1355
|
+
this.#pitchBendValue = value;
|
|
1356
|
+
const normalized = value - 8192;
|
|
1357
|
+
const semitones = normalized >= 0 ? normalized / 8191 * this.#bendRange : normalized / 8192 * this.#bendRange;
|
|
1358
|
+
const now = this.#audioCtx.currentTime;
|
|
1359
|
+
this.#envelopes.forEach((e) => {
|
|
1360
|
+
if (e.audioBufferSourceNode && e.when + e.duration > now) {
|
|
1361
|
+
const originalPitchCents = e.pitch * 100;
|
|
1362
|
+
const baseDetuneCents = e.baseDetune;
|
|
1363
|
+
const bendCents = semitones * 100;
|
|
1364
|
+
const totalCents = originalPitchCents - baseDetuneCents + bendCents;
|
|
1365
|
+
const newRate = Math.pow(2, totalCents / 1200);
|
|
1366
|
+
e.audioBufferSourceNode.playbackRate.cancelScheduledValues(now);
|
|
1367
|
+
e.audioBufferSourceNode.playbackRate.setTargetAtTime(newRate, now, 0.015);
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
setController(number, value) {
|
|
1372
|
+
const now = this.#audioCtx.currentTime;
|
|
1373
|
+
const normalizedValue = Math.max(0, Math.min(127, value)) / 127;
|
|
1374
|
+
switch (number) {
|
|
1375
|
+
case 7:
|
|
1376
|
+
this.#volumeValue = normalizedValue;
|
|
1377
|
+
this.#mainGain.gain.setTargetAtTime(this.#volumeValue, now, 0.05);
|
|
1378
|
+
break;
|
|
1379
|
+
case 11:
|
|
1380
|
+
this.#expressionValue = normalizedValue;
|
|
1381
|
+
this.#expressionGain.gain.setTargetAtTime(this.#expressionValue, now, 0.03);
|
|
1382
|
+
break;
|
|
1383
|
+
case 64:
|
|
1384
|
+
this.#sustain = value >= 64;
|
|
1385
|
+
if (!this.#sustain) {
|
|
1386
|
+
this.#notesWaitingForSustain.forEach((cancelFn) => cancelFn());
|
|
1387
|
+
this.#notesWaitingForSustain.clear();
|
|
1309
1388
|
}
|
|
1310
|
-
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
async #adjustZone(zone) {
|
|
1393
|
+
if (zone.buffer) return zone;
|
|
1394
|
+
zone.delay = 0;
|
|
1395
|
+
if (zone.file) {
|
|
1396
|
+
const binary = atob(zone.file);
|
|
1397
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
1398
|
+
try {
|
|
1399
|
+
zone.buffer = await this.#audioCtx.decodeAudioData(bytes.buffer);
|
|
1400
|
+
this.#applyZoneParameters(zone);
|
|
1401
|
+
} catch (error) {
|
|
1402
|
+
console.error("Audio decoding error:", error);
|
|
1403
|
+
console.warn(this.#preset);
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1311
1406
|
} else {
|
|
1312
1407
|
this.#applyZoneParameters(zone);
|
|
1313
|
-
return zone;
|
|
1314
1408
|
}
|
|
1409
|
+
return zone;
|
|
1315
1410
|
}
|
|
1316
1411
|
#applyZoneParameters(zone) {
|
|
1317
1412
|
zone.loopStart = this.#numValue(zone.loopStart, 0);
|
|
@@ -1319,10 +1414,9 @@
|
|
|
1319
1414
|
zone.coarseTune = this.#numValue(zone.coarseTune, 0);
|
|
1320
1415
|
zone.fineTune = this.#numValue(zone.fineTune, 0);
|
|
1321
1416
|
zone.originalPitch = this.#numValue(zone.originalPitch, 6e3);
|
|
1322
|
-
zone.sampleRate = this.#numValue(zone.sampleRate, 44100);
|
|
1323
1417
|
}
|
|
1324
1418
|
#setupEnvelope(envelope, zone, volume, when, sampleDuration, noteDuration) {
|
|
1325
|
-
envelope.gain.setValueAtTime(this.#
|
|
1419
|
+
envelope.gain.setValueAtTime(this.#nearZero, this.#audioCtx.currentTime);
|
|
1326
1420
|
const duration = Math.min(noteDuration, sampleDuration - this.#afterTime);
|
|
1327
1421
|
const ahdsr = zone.ahdsr && zone.ahdsr.length > 0 ? zone.ahdsr : [
|
|
1328
1422
|
{ duration: 0, volume: 1 },
|
|
@@ -1330,7 +1424,7 @@
|
|
|
1330
1424
|
];
|
|
1331
1425
|
envelope.gain.cancelScheduledValues(when);
|
|
1332
1426
|
const initialVol = (ahdsr[0]?.volume ?? 1) * volume;
|
|
1333
|
-
envelope.gain.
|
|
1427
|
+
envelope.gain.linearRampToValueAtTime(this.#noZeroVolume(initialVol), when + 2e-3);
|
|
1334
1428
|
let lastTime = 0;
|
|
1335
1429
|
let lastVolume = ahdsr[0]?.volume ?? 1;
|
|
1336
1430
|
for (const stage of ahdsr) {
|
|
@@ -1340,7 +1434,7 @@
|
|
|
1340
1434
|
if (stageDuration > remainingTime) {
|
|
1341
1435
|
const ratio = remainingTime / stageDuration;
|
|
1342
1436
|
const interpolatedVolume = lastVolume + ratio * (stageVolume - lastVolume);
|
|
1343
|
-
envelope.gain.
|
|
1437
|
+
envelope.gain.exponentialRampToValueAtTime(
|
|
1344
1438
|
this.#noZeroVolume(volume * interpolatedVolume),
|
|
1345
1439
|
when + duration
|
|
1346
1440
|
);
|
|
@@ -1348,15 +1442,24 @@
|
|
|
1348
1442
|
}
|
|
1349
1443
|
lastTime += stageDuration;
|
|
1350
1444
|
lastVolume = stageVolume;
|
|
1351
|
-
envelope.gain.
|
|
1445
|
+
envelope.gain.exponentialRampToValueAtTime(
|
|
1352
1446
|
this.#noZeroVolume(volume * lastVolume),
|
|
1353
1447
|
when + lastTime
|
|
1354
1448
|
);
|
|
1355
1449
|
}
|
|
1356
|
-
envelope.gain.
|
|
1450
|
+
envelope.gain.exponentialRampToValueAtTime(this.#nearZero, when + duration + this.#afterTime);
|
|
1357
1451
|
}
|
|
1358
|
-
#findEnvelope() {
|
|
1359
|
-
|
|
1452
|
+
#findEnvelope(destinationNode) {
|
|
1453
|
+
const target = destinationNode || this.#mainGain;
|
|
1454
|
+
const now = this.#audioCtx.currentTime;
|
|
1455
|
+
let envelope = this.#envelopes.find((e) => e.target === target && now > e.when + e.duration + 0.05);
|
|
1456
|
+
if (!envelope && this.#envelopes.length >= 64) {
|
|
1457
|
+
const activeEnvelopes = this.#envelopes.filter((e) => e.target === target);
|
|
1458
|
+
if (activeEnvelopes.length > 0) {
|
|
1459
|
+
activeEnvelopes.sort((a, b) => a.when - b.when);
|
|
1460
|
+
envelope = activeEnvelopes[0];
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1360
1463
|
if (envelope) {
|
|
1361
1464
|
if (envelope.audioBufferSourceNode) {
|
|
1362
1465
|
try {
|
|
@@ -1366,27 +1469,36 @@
|
|
|
1366
1469
|
}
|
|
1367
1470
|
envelope.audioBufferSourceNode = null;
|
|
1368
1471
|
}
|
|
1472
|
+
envelope.gain.cancelScheduledValues(0);
|
|
1473
|
+
envelope.gain.setValueAtTime(this.#nearZero, now);
|
|
1369
1474
|
} else {
|
|
1370
1475
|
envelope = this.#audioCtx.createGain();
|
|
1371
1476
|
envelope.gain.value = 0;
|
|
1372
|
-
envelope.target =
|
|
1373
|
-
envelope.connect(
|
|
1374
|
-
envelope.cancel = () => {
|
|
1375
|
-
if (envelope.when + envelope.duration > this.#audioCtx.currentTime) {
|
|
1376
|
-
envelope.gain.cancelScheduledValues(0);
|
|
1377
|
-
envelope.gain.setTargetAtTime(this.#nearZero, this.#audioCtx.currentTime, 0.1);
|
|
1378
|
-
envelope.when = this.#audioCtx.currentTime + 1e-5;
|
|
1379
|
-
envelope.duration = 0;
|
|
1380
|
-
}
|
|
1381
|
-
};
|
|
1477
|
+
envelope.target = target;
|
|
1478
|
+
envelope.connect(target);
|
|
1382
1479
|
this.#envelopes.push(envelope);
|
|
1383
1480
|
}
|
|
1481
|
+
envelope.cancel = (force = false) => {
|
|
1482
|
+
const currentTime = this.#audioCtx.currentTime;
|
|
1483
|
+
if (force && envelope.audioBufferSourceNode) {
|
|
1484
|
+
try {
|
|
1485
|
+
envelope.audioBufferSourceNode.stop(0);
|
|
1486
|
+
envelope.audioBufferSourceNode.disconnect();
|
|
1487
|
+
} catch (e) {
|
|
1488
|
+
}
|
|
1489
|
+
envelope.audioBufferSourceNode = null;
|
|
1490
|
+
}
|
|
1491
|
+
if (envelope.when + envelope.duration > currentTime) {
|
|
1492
|
+
envelope.gain.cancelScheduledValues(0);
|
|
1493
|
+
envelope.gain.setTargetAtTime(this.#nearZero, currentTime, force ? 5e-3 : 0.02);
|
|
1494
|
+
envelope.when = currentTime + 1e-5;
|
|
1495
|
+
envelope.duration = 0;
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1384
1498
|
return envelope;
|
|
1385
1499
|
}
|
|
1386
1500
|
#findZone(pitch) {
|
|
1387
|
-
|
|
1388
|
-
if (zone) this.#adjustZone(zone);
|
|
1389
|
-
return zone;
|
|
1501
|
+
return this.#preset.zones.findLast((z) => pitch >= z.keyRangeLow && pitch <= z.keyRangeHigh);
|
|
1390
1502
|
}
|
|
1391
1503
|
#limitVolume(v) {
|
|
1392
1504
|
const requestedVolume = v ? 1 * v : 0.5;
|
|
@@ -1399,209 +1511,1597 @@
|
|
|
1399
1511
|
return typeof a === "number" ? a : b;
|
|
1400
1512
|
}
|
|
1401
1513
|
};
|
|
1514
|
+
if (typeof window !== "undefined") window.WebAudioFontPlayer = WebAudioFontPlayer;
|
|
1515
|
+
var index_default = WebAudioFontPlayer;
|
|
1402
1516
|
|
|
1403
|
-
// src/
|
|
1404
|
-
var
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1517
|
+
// src/libraries/audiocompressor.js
|
|
1518
|
+
var AudioCompressor = class _AudioCompressor {
|
|
1519
|
+
#input = null;
|
|
1520
|
+
#output = null;
|
|
1521
|
+
#audioCtx = null;
|
|
1522
|
+
#limiter = null;
|
|
1523
|
+
#analyser = null;
|
|
1524
|
+
#reverbNode = null;
|
|
1525
|
+
#reverbWet = null;
|
|
1526
|
+
#currentReverbLevel = 0;
|
|
1527
|
+
#eqBands = /* @__PURE__ */ new Map();
|
|
1528
|
+
static #EQ_FREQUENCIES = [32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384];
|
|
1529
|
+
static #EQ_Q = /* @__PURE__ */ new Map([
|
|
1530
|
+
[32, 0.7],
|
|
1531
|
+
[64, 0.8],
|
|
1532
|
+
[128, 0.9],
|
|
1533
|
+
[256, 1],
|
|
1534
|
+
[512, 1.1],
|
|
1535
|
+
[1024, 1.2],
|
|
1536
|
+
[2048, 1.4],
|
|
1537
|
+
[4096, 1.6],
|
|
1538
|
+
[8192, 1.8],
|
|
1539
|
+
[16384, 2]
|
|
1540
|
+
]);
|
|
1541
|
+
constructor(audioCtx, volume, reverb) {
|
|
1542
|
+
this.#audioCtx = audioCtx;
|
|
1543
|
+
this.#input = this.#audioCtx.createGain();
|
|
1544
|
+
let lastNode = this.#input;
|
|
1545
|
+
const frequencies = [32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384];
|
|
1546
|
+
frequencies.forEach((freq) => {
|
|
1547
|
+
lastNode = this.#bandEqualizer(lastNode, freq);
|
|
1548
|
+
const label = freq < 1e3 ? freq : freq / 1024 + "k";
|
|
1549
|
+
this["band".concat(label)] = lastNode;
|
|
1550
|
+
});
|
|
1551
|
+
this.#currentReverbLevel = reverb;
|
|
1552
|
+
this.#reverbNode = this.#audioCtx.createConvolver();
|
|
1553
|
+
this.#reverbWet = this.#audioCtx.createGain();
|
|
1554
|
+
this.#reverbWet.gain.setValueAtTime(reverb, this.#audioCtx.currentTime);
|
|
1555
|
+
this.#generateImpulseResponse(1.5, 2);
|
|
1556
|
+
this.#limiter = this.#audioCtx.createDynamicsCompressor();
|
|
1557
|
+
this.#limiter.threshold.setValueAtTime(-10, this.#audioCtx.currentTime);
|
|
1558
|
+
this.#limiter.ratio.setValueAtTime(20, this.#audioCtx.currentTime);
|
|
1559
|
+
this.#limiter.attack.setValueAtTime(1e-3, this.#audioCtx.currentTime);
|
|
1560
|
+
this.#limiter.release.setValueAtTime(0.1, this.#audioCtx.currentTime);
|
|
1561
|
+
this.#limiter.knee.setValueAtTime(0, this.#audioCtx.currentTime);
|
|
1562
|
+
this.#analyser = this.#audioCtx.createAnalyser();
|
|
1563
|
+
this.#analyser.fftSize = 256;
|
|
1564
|
+
this.#analyser.smoothingTimeConstant = 0.6;
|
|
1565
|
+
this.#output = this.#audioCtx.createGain();
|
|
1566
|
+
this.#output.gain.setValueAtTime(volume, this.#audioCtx.currentTime);
|
|
1567
|
+
lastNode.connect(this.#output);
|
|
1568
|
+
this.#output.connect(this.#limiter);
|
|
1569
|
+
this.#output.connect(this.#reverbNode);
|
|
1570
|
+
this.#reverbNode.connect(this.#reverbWet);
|
|
1571
|
+
this.#limiter.connect(this.#analyser);
|
|
1572
|
+
this.#reverbWet.connect(this.#analyser);
|
|
1573
|
+
this.#analyser.connect(this.#audioCtx.destination);
|
|
1574
|
+
}
|
|
1575
|
+
get eqFrequencies() {
|
|
1576
|
+
return _AudioCompressor.#EQ_FREQUENCIES;
|
|
1577
|
+
}
|
|
1578
|
+
get analyser() {
|
|
1579
|
+
return this.#analyser || null;
|
|
1580
|
+
}
|
|
1581
|
+
get input() {
|
|
1582
|
+
return this.#input;
|
|
1583
|
+
}
|
|
1584
|
+
get reverb() {
|
|
1585
|
+
return this.#currentReverbLevel;
|
|
1586
|
+
}
|
|
1587
|
+
set reverb(value) {
|
|
1588
|
+
this.#currentReverbLevel = Math.max(0, Math.min(1, value));
|
|
1589
|
+
this.#reverbWet.gain.setTargetAtTime(this.#currentReverbLevel, this.#audioCtx.currentTime, 0.1);
|
|
1590
|
+
}
|
|
1591
|
+
get masterVolume() {
|
|
1592
|
+
return this.#output.gain.value;
|
|
1593
|
+
}
|
|
1594
|
+
set masterVolume(value) {
|
|
1595
|
+
const linearValue = Math.max(0, Math.min(1, value));
|
|
1596
|
+
const logVolume = Math.pow(linearValue, 2);
|
|
1597
|
+
this.#output.gain.setTargetAtTime(logVolume, this.#audioCtx.currentTime, 0.01);
|
|
1598
|
+
}
|
|
1599
|
+
killReverbTail() {
|
|
1600
|
+
const now = this.#audioCtx.currentTime;
|
|
1601
|
+
this.#reverbWet.gain.cancelScheduledValues(now);
|
|
1602
|
+
this.#reverbWet.gain.setValueAtTime(0, now);
|
|
1603
|
+
}
|
|
1604
|
+
restoreReverb() {
|
|
1605
|
+
this.reverb = this.#currentReverbLevel;
|
|
1606
|
+
}
|
|
1607
|
+
setEQ(gains, smoothTime = 0.04) {
|
|
1608
|
+
const now = this.#audioCtx.currentTime;
|
|
1609
|
+
const MAX_DB = 12;
|
|
1610
|
+
for (const [key, val] of Object.entries(gains)) {
|
|
1611
|
+
const freq = Number(key);
|
|
1612
|
+
const band = this.#eqBands.get(freq);
|
|
1613
|
+
if (!band) continue;
|
|
1614
|
+
const dbValue = Math.max(-MAX_DB, Math.min(MAX_DB, val));
|
|
1615
|
+
band.filter.gain.setTargetAtTime(dbValue, now, smoothTime);
|
|
1616
|
+
band.gain = dbValue;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
getEQ() {
|
|
1620
|
+
const result = {};
|
|
1621
|
+
for (const [freq, band] of this.#eqBands) result[freq] = band.gain;
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
resetEQ(smoothTime = 0.04) {
|
|
1625
|
+
const flat = {};
|
|
1626
|
+
for (const freq of _AudioCompressor.#EQ_FREQUENCIES) flat[freq] = 0;
|
|
1627
|
+
this.setEQ(flat, smoothTime);
|
|
1628
|
+
}
|
|
1629
|
+
setEQPreset(name) {
|
|
1630
|
+
const presets = {
|
|
1631
|
+
flat: { 32: 0, 64: 0, 128: 0, 256: 0, 512: 0, 1024: 0, 2048: 0, 4096: 0, 8192: 0, 16384: 0 },
|
|
1632
|
+
bass: { 32: 7, 64: 6, 128: 4, 256: 2, 512: 0, 1024: -1, 2048: -1, 4096: 0, 8192: 0, 16384: 0 },
|
|
1633
|
+
treble: { 32: 0, 64: 0, 128: 0, 256: 0, 512: 0, 1024: 1, 2048: 3, 4096: 5, 8192: 7, 16384: 8 },
|
|
1634
|
+
vocal: { 32: -3, 64: -2, 128: 0, 256: 2, 512: 4, 1024: 5, 2048: 4, 4096: 2, 8192: 1, 16384: 0 },
|
|
1635
|
+
loudness: { 32: 6, 64: 4, 128: 1, 256: 0, 512: -1, 1024: -1, 2048: 0, 4096: 2, 8192: 4, 16384: 5 },
|
|
1636
|
+
classical: { 32: 4, 64: 3, 128: 2, 256: 0, 512: 0, 1024: 0, 2048: 0, 4096: 2, 8192: 3, 16384: 4 },
|
|
1637
|
+
jazz: { 32: 4, 64: 3, 128: 1, 256: 0, 512: -1, 1024: -1, 2048: 0, 4096: 1, 8192: 3, 16384: 4 },
|
|
1638
|
+
electronic: { 32: 6, 64: 5, 128: 2, 256: -1, 512: -2, 1024: -1, 2048: 2, 4096: 4, 8192: 5, 16384: 6 }
|
|
1639
|
+
};
|
|
1640
|
+
const preset = presets[name];
|
|
1641
|
+
if (!preset) throw new Error('Preset EQ unkown: "'.concat(name, '". Avaiables: ').concat(Object.keys(presets).join(", ")));
|
|
1642
|
+
this.setEQ(preset);
|
|
1643
|
+
}
|
|
1644
|
+
#bandEqualizer(from, frequency) {
|
|
1645
|
+
const filter = this.#audioCtx.createBiquadFilter();
|
|
1646
|
+
filter.type = "peaking";
|
|
1647
|
+
filter.frequency.setValueAtTime(frequency, this.#audioCtx.currentTime);
|
|
1648
|
+
filter.gain.setValueAtTime(0, this.#audioCtx.currentTime);
|
|
1649
|
+
const q = _AudioCompressor.#EQ_Q.get(frequency) ?? 1;
|
|
1650
|
+
filter.Q.setValueAtTime(q, this.#audioCtx.currentTime);
|
|
1651
|
+
this.#eqBands.set(frequency, { filter, gain: 0 });
|
|
1652
|
+
from.connect(filter);
|
|
1653
|
+
return filter;
|
|
1654
|
+
}
|
|
1655
|
+
#generateImpulseResponse(duration, decay) {
|
|
1656
|
+
const sampleRate = this.#audioCtx.sampleRate;
|
|
1657
|
+
const length = sampleRate * duration;
|
|
1658
|
+
const impulse = this.#audioCtx.createBuffer(2, length, sampleRate);
|
|
1659
|
+
const preDelayTime = 0.015;
|
|
1660
|
+
const preDelaySamples = Math.floor(preDelayTime * sampleRate);
|
|
1661
|
+
for (let channel = 0; channel < impulse.numberOfChannels; channel++) {
|
|
1662
|
+
const data = impulse.getChannelData(channel);
|
|
1663
|
+
let lastValue = 0;
|
|
1664
|
+
const channelOffset = channel === 1 ? Math.floor(2e-3 * sampleRate) : 0;
|
|
1665
|
+
for (let i = 0; i < length; i++) {
|
|
1666
|
+
if (i < preDelaySamples) {
|
|
1667
|
+
data[i] = 0;
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
const t = (i - preDelaySamples) / sampleRate;
|
|
1671
|
+
const envelope = Math.exp(-t * (decay / duration));
|
|
1672
|
+
const dampingFactor = Math.max(0.01, 0.2 * Math.exp(-t * 2.5));
|
|
1673
|
+
const whiteNoise = Math.random() * 2 - 1;
|
|
1674
|
+
lastValue = whiteNoise * dampingFactor + lastValue * (1 - dampingFactor);
|
|
1675
|
+
let sampleValue = lastValue * envelope;
|
|
1676
|
+
if (t < 0.04) {
|
|
1677
|
+
if (i % 123 === 0 || i % 234 === 0) {
|
|
1678
|
+
sampleValue += (Math.random() * 2 - 1) * 0.2 * (0.04 - t) / 0.04;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (i + channelOffset < length) data[i + channelOffset] = sampleValue;
|
|
1682
|
+
else data[i] = sampleValue;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
this.#reverbNode.buffer = impulse;
|
|
1686
|
+
}
|
|
1519
1687
|
};
|
|
1520
1688
|
|
|
1689
|
+
// src/libraries/indexeddbstorage.js
|
|
1690
|
+
var DB_NAME = "MidiAudioPlayer";
|
|
1691
|
+
var STORE_NAME = "KeyValues";
|
|
1692
|
+
var DEFAULT_VERSION = 1;
|
|
1693
|
+
var dbInstance = null;
|
|
1694
|
+
var currentVersion = DEFAULT_VERSION;
|
|
1695
|
+
async function getDB(version = currentVersion) {
|
|
1696
|
+
if (dbInstance && version !== currentVersion) {
|
|
1697
|
+
dbInstance.close();
|
|
1698
|
+
dbInstance = null;
|
|
1699
|
+
currentVersion = version;
|
|
1700
|
+
}
|
|
1701
|
+
if (dbInstance) return dbInstance;
|
|
1702
|
+
return new Promise((resolve, reject) => {
|
|
1703
|
+
const request = indexedDB.open(DB_NAME, version);
|
|
1704
|
+
request.onupgradeneeded = (e) => {
|
|
1705
|
+
const db = e.target.result;
|
|
1706
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
1707
|
+
db.createObjectStore(STORE_NAME);
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
request.onsuccess = (e) => {
|
|
1711
|
+
dbInstance = e.target.result;
|
|
1712
|
+
resolve(dbInstance);
|
|
1713
|
+
};
|
|
1714
|
+
request.onerror = (e) => reject(e.target.error);
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
var indexedDbStorage = {
|
|
1718
|
+
async setVersion(version) {
|
|
1719
|
+
await getDB(version);
|
|
1720
|
+
},
|
|
1721
|
+
async setItem(key, value, compress = false) {
|
|
1722
|
+
const db = await getDB();
|
|
1723
|
+
let finalData = value;
|
|
1724
|
+
let isCompressed = false;
|
|
1725
|
+
if (compress) {
|
|
1726
|
+
const stringData = JSON.stringify(value);
|
|
1727
|
+
const stream = new Blob([stringData]).stream();
|
|
1728
|
+
const compressedStream = stream.pipeThrough(new CompressionStream("gzip"));
|
|
1729
|
+
const response = new Response(compressedStream);
|
|
1730
|
+
finalData = await response.arrayBuffer();
|
|
1731
|
+
isCompressed = true;
|
|
1732
|
+
}
|
|
1733
|
+
const record = { data: finalData, isCompressed };
|
|
1734
|
+
return new Promise((resolve, reject) => {
|
|
1735
|
+
const transaction = db.transaction(STORE_NAME, "readwrite");
|
|
1736
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1737
|
+
const request = store.put(record, key);
|
|
1738
|
+
request.onsuccess = () => resolve();
|
|
1739
|
+
request.onerror = () => reject(request.error);
|
|
1740
|
+
});
|
|
1741
|
+
},
|
|
1742
|
+
async getItem(key) {
|
|
1743
|
+
const db = await getDB();
|
|
1744
|
+
const record = await new Promise((resolve, reject) => {
|
|
1745
|
+
const transaction = db.transaction(STORE_NAME, "readonly");
|
|
1746
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1747
|
+
const request = store.get(key);
|
|
1748
|
+
request.onsuccess = () => resolve(request.result);
|
|
1749
|
+
request.onerror = () => reject(request.error);
|
|
1750
|
+
});
|
|
1751
|
+
if (!record) return null;
|
|
1752
|
+
if (record.isCompressed) {
|
|
1753
|
+
const stream = new Blob([record.data]).stream();
|
|
1754
|
+
const decompressedStream = stream.pipeThrough(new DecompressionStream("gzip"));
|
|
1755
|
+
const response = new Response(decompressedStream);
|
|
1756
|
+
const text = await response.text();
|
|
1757
|
+
return JSON.parse(text);
|
|
1758
|
+
}
|
|
1759
|
+
return record.data;
|
|
1760
|
+
},
|
|
1761
|
+
async removeItem(key) {
|
|
1762
|
+
const db = await getDB();
|
|
1763
|
+
return new Promise((resolve, reject) => {
|
|
1764
|
+
const transaction = db.transaction(STORE_NAME, "readwrite");
|
|
1765
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1766
|
+
const request = store.delete(key);
|
|
1767
|
+
request.onsuccess = () => resolve();
|
|
1768
|
+
request.onerror = () => reject(request.error);
|
|
1769
|
+
});
|
|
1770
|
+
},
|
|
1771
|
+
async clear() {
|
|
1772
|
+
const db = await getDB();
|
|
1773
|
+
return new Promise((resolve, reject) => {
|
|
1774
|
+
const transaction = db.transaction(STORE_NAME, "readwrite");
|
|
1775
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
1776
|
+
const request = store.clear();
|
|
1777
|
+
request.onsuccess = () => resolve();
|
|
1778
|
+
request.onerror = () => reject(request.error);
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
var indexeddbstorage_default = indexedDbStorage;
|
|
1783
|
+
|
|
1521
1784
|
// src/midiaudioplayer.js
|
|
1522
|
-
var
|
|
1785
|
+
var clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
|
1786
|
+
var MidiAudioPlayer = class _MidiAudioPlayer extends index.Player {
|
|
1787
|
+
static ENDPOINT = "https://webaudiofonts.com/presets/";
|
|
1788
|
+
static DEFAULT_PRESET = -1;
|
|
1789
|
+
static REFERENCE_GAIN = 0.15;
|
|
1790
|
+
static KARAOKE_CHANNEL = 0;
|
|
1791
|
+
#catalog = null;
|
|
1523
1792
|
#audioCtx = null;
|
|
1524
|
-
#
|
|
1525
|
-
#
|
|
1793
|
+
#compressor = null;
|
|
1794
|
+
#vocalChannel = null;
|
|
1795
|
+
#activeNotes = {};
|
|
1796
|
+
#channelStates = {};
|
|
1797
|
+
#instruments = {};
|
|
1798
|
+
#players = {};
|
|
1799
|
+
#channels = {};
|
|
1800
|
+
#channelVolumes = {};
|
|
1801
|
+
#presetMap = {};
|
|
1802
|
+
#bufferHash = null;
|
|
1803
|
+
#presetTimer = null;
|
|
1804
|
+
#presetMapThread = null;
|
|
1805
|
+
#lyrics = null;
|
|
1806
|
+
#haveLyrics = false;
|
|
1807
|
+
#title = "";
|
|
1526
1808
|
#opts = {
|
|
1527
|
-
|
|
1528
|
-
volume: 0.
|
|
1529
|
-
|
|
1809
|
+
endpoint: _MidiAudioPlayer.ENDPOINT,
|
|
1810
|
+
volume: 0.6,
|
|
1811
|
+
reverb: 0.3,
|
|
1812
|
+
onEndFile: null,
|
|
1813
|
+
localCache: true,
|
|
1814
|
+
presetRandom: false,
|
|
1815
|
+
karaoke: false,
|
|
1816
|
+
karaokeDelay: 0,
|
|
1817
|
+
muteExpression: false,
|
|
1818
|
+
maxCharPerLine: 48,
|
|
1819
|
+
eqPreset: "flat",
|
|
1820
|
+
preferred: [],
|
|
1821
|
+
presets: []
|
|
1530
1822
|
};
|
|
1531
1823
|
constructor(opts = {}) {
|
|
1532
|
-
super(
|
|
1824
|
+
super();
|
|
1533
1825
|
this.#opts = { ...this.#opts, ...opts };
|
|
1534
|
-
this.#
|
|
1826
|
+
this.#presetMapThread = this.#mapPresets();
|
|
1535
1827
|
this.#audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
1536
|
-
this.#
|
|
1537
|
-
this.
|
|
1538
|
-
|
|
1539
|
-
|
|
1828
|
+
this.#compressor = new AudioCompressor(this.#audioCtx, this.#opts.volume, this.#opts.reverb);
|
|
1829
|
+
this.#compressor.setEQPreset(this.#opts.eqPreset);
|
|
1830
|
+
if (this.#opts.karaoke) this.#sendKaraokeFrame("intro");
|
|
1831
|
+
}
|
|
1832
|
+
get catalog() {
|
|
1833
|
+
return this.getCatalog();
|
|
1834
|
+
}
|
|
1835
|
+
get channels() {
|
|
1836
|
+
return this.#players;
|
|
1837
|
+
}
|
|
1838
|
+
get channelStates() {
|
|
1839
|
+
return this.#channelStates;
|
|
1840
|
+
}
|
|
1841
|
+
get volume() {
|
|
1842
|
+
return this.#opts.volume;
|
|
1843
|
+
}
|
|
1844
|
+
set volume(vol) {
|
|
1845
|
+
this.#opts.volume = clamp(vol, 0, 1);
|
|
1846
|
+
this.#compressor.masterVolume = this.#opts.volume;
|
|
1847
|
+
}
|
|
1848
|
+
get volumes() {
|
|
1849
|
+
return this.#channelVolumes;
|
|
1850
|
+
}
|
|
1851
|
+
get reverb() {
|
|
1852
|
+
return this.#compressor.reverb;
|
|
1853
|
+
}
|
|
1854
|
+
set reverb(rev) {
|
|
1855
|
+
this.#compressor.reverb = rev;
|
|
1856
|
+
}
|
|
1857
|
+
get muteExpression() {
|
|
1858
|
+
return this.#opts.muteExpression;
|
|
1859
|
+
}
|
|
1860
|
+
set muteExpression(val) {
|
|
1861
|
+
this.#opts.muteExpression = Boolean(val);
|
|
1862
|
+
}
|
|
1863
|
+
get eqFrequencies() {
|
|
1864
|
+
return this.#compressor.eqFrequencies;
|
|
1865
|
+
}
|
|
1866
|
+
get eq() {
|
|
1867
|
+
return this.#compressor.getEQ();
|
|
1868
|
+
}
|
|
1869
|
+
getEQ() {
|
|
1870
|
+
return this.#compressor.getEQ();
|
|
1871
|
+
}
|
|
1872
|
+
setEQ(gains) {
|
|
1873
|
+
this.#compressor.setEQ(gains);
|
|
1874
|
+
}
|
|
1875
|
+
setEQPreset(name) {
|
|
1876
|
+
this.#compressor.setEQPreset(name);
|
|
1877
|
+
}
|
|
1878
|
+
setChannelVolume(channel, volume) {
|
|
1879
|
+
this.#channelVolumes[channel] = volume;
|
|
1880
|
+
this.#setupChange();
|
|
1881
|
+
}
|
|
1882
|
+
async #mapPresets() {
|
|
1883
|
+
await Promise.all(this.#opts.presets.map(async (p) => {
|
|
1884
|
+
const preset = await this.findPreset(p);
|
|
1885
|
+
if (preset) this.#presetMap[preset.program] = preset;
|
|
1886
|
+
}));
|
|
1887
|
+
}
|
|
1888
|
+
async findPreset(id) {
|
|
1889
|
+
let preset = null;
|
|
1890
|
+
const categories = await this.getCategories();
|
|
1891
|
+
categories.some((c) => {
|
|
1892
|
+
c.instruments.some((i) => {
|
|
1893
|
+
preset = i.presets.find((p) => p.id == id);
|
|
1894
|
+
if (preset) {
|
|
1895
|
+
preset.category = c.name;
|
|
1896
|
+
preset.instrument = i.name;
|
|
1897
|
+
preset.program = i.program;
|
|
1898
|
+
return true;
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
if (preset) return true;
|
|
1540
1902
|
});
|
|
1903
|
+
return preset;
|
|
1904
|
+
}
|
|
1905
|
+
async close() {
|
|
1906
|
+
Object.keys(this.#players).forEach((id) => this.#players[id].close());
|
|
1907
|
+
await this.#audioCtx.close();
|
|
1908
|
+
}
|
|
1909
|
+
async getCatalog() {
|
|
1910
|
+
if (this.#catalog) return this.#catalog;
|
|
1911
|
+
const cachedata = this.#opts.localCache ? await sessionStorage.getItem("waf_catalog") : null;
|
|
1912
|
+
if (cachedata) this.#catalog = JSON.parse(cachedata);
|
|
1913
|
+
else {
|
|
1914
|
+
this.#log("Downloading catalog...");
|
|
1915
|
+
const response = await fetch("".concat(this.#opts.endpoint, "catalog.json"));
|
|
1916
|
+
if (!response.ok) throw new Error("Impossible to download catalog: ".concat(response.status));
|
|
1917
|
+
this.#catalog = await response.json();
|
|
1918
|
+
if (this.#opts.localCache) await sessionStorage.setItem("waf_catalog", JSON.stringify(this.#catalog));
|
|
1919
|
+
}
|
|
1920
|
+
const catalogDate = new Date(this.#catalog.updatedAt).getTime();
|
|
1921
|
+
const catalogVersion = await indexeddbstorage_default.getItem("waf_catalog_version") || 1;
|
|
1922
|
+
if (catalogVersion < catalogDate) {
|
|
1923
|
+
await indexeddbstorage_default.clear();
|
|
1924
|
+
indexeddbstorage_default.setItem("waf_catalog_version", catalogDate);
|
|
1925
|
+
}
|
|
1926
|
+
return this.#catalog;
|
|
1927
|
+
}
|
|
1928
|
+
async getCategories() {
|
|
1929
|
+
return (await this.getCatalog()).categories;
|
|
1930
|
+
}
|
|
1931
|
+
async getProgramInstruments(program) {
|
|
1932
|
+
const categories = await this.getCategories();
|
|
1933
|
+
let instruments = [];
|
|
1934
|
+
await Promise.all(categories.map(async (category) => category.instruments.filter((elm) => elm.program == program).forEach((elm) => {
|
|
1935
|
+
elm.presets.forEach((p) => {
|
|
1936
|
+
p.instrument = category.name + " / " + elm.name;
|
|
1937
|
+
instruments.push(p);
|
|
1938
|
+
});
|
|
1939
|
+
})));
|
|
1940
|
+
return instruments;
|
|
1941
|
+
}
|
|
1942
|
+
async getPreset(id) {
|
|
1943
|
+
try {
|
|
1944
|
+
if (typeof id === "object") return id;
|
|
1945
|
+
const cacheid = "waf_preset_".concat(id);
|
|
1946
|
+
const cachedata = this.#opts.localCache ? await indexeddbstorage_default.getItem(cacheid) : null;
|
|
1947
|
+
if (cachedata) return JSON.parse(cachedata);
|
|
1948
|
+
this.#log("Downloading preset ".concat(id, "..."));
|
|
1949
|
+
const response = await fetch("".concat(_MidiAudioPlayer.ENDPOINT).concat(id, ".json"));
|
|
1950
|
+
const preset = await response.json();
|
|
1951
|
+
if (preset.zones === void 0) {
|
|
1952
|
+
console.error("Invalid preset: ".concat($id));
|
|
1953
|
+
throw new Error("Invalid preset: ".concat($id));
|
|
1954
|
+
}
|
|
1955
|
+
if (this.#opts.localCache) await indexeddbstorage_default.setItem(cacheid, JSON.stringify(preset), true);
|
|
1956
|
+
return preset;
|
|
1957
|
+
} catch (e) {
|
|
1958
|
+
console.error("Invalid preset: ".concat(id));
|
|
1959
|
+
throw new Error("Invalid preset: ".concat(id));
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
async loadPreset(presetId, channel) {
|
|
1963
|
+
const presetInfo = await this.findPreset(presetId);
|
|
1964
|
+
if (!presetInfo) throw new Error("Invalid preset: ".concat(presetId));
|
|
1965
|
+
this.#presetMap[presetInfo.program] = presetInfo;
|
|
1966
|
+
const preset = await this.getPreset(presetId);
|
|
1967
|
+
await this.#players[channel].setPreset(preset);
|
|
1968
|
+
this.#setupChange();
|
|
1541
1969
|
}
|
|
1542
|
-
async load(content) {
|
|
1970
|
+
async load(content, setup) {
|
|
1971
|
+
if (typeof content === "string") {
|
|
1972
|
+
this.#log("Downloading song...");
|
|
1973
|
+
const response = await fetch(content);
|
|
1974
|
+
content = await response.arrayBuffer();
|
|
1975
|
+
}
|
|
1976
|
+
if (typeof setup === "string") {
|
|
1977
|
+
this.#log("Downloading setup...");
|
|
1978
|
+
const response = await fetch(setup);
|
|
1979
|
+
setup = await response.json();
|
|
1980
|
+
}
|
|
1981
|
+
this.#bufferHash = await this.hashBuffer(content);
|
|
1982
|
+
await this.#presetMapThread;
|
|
1543
1983
|
if (this.isPlaying()) this.stop();
|
|
1544
1984
|
this.#clearActiveNotes();
|
|
1545
|
-
await this.
|
|
1985
|
+
await Promise.all(Object.values(this.#players).map(async (player) => player.close()));
|
|
1986
|
+
this.#players = {};
|
|
1987
|
+
this.#instruments = {};
|
|
1988
|
+
this.#activeNotes = {};
|
|
1989
|
+
this.#title = "";
|
|
1990
|
+
this.#log("Loading buffer...");
|
|
1991
|
+
try {
|
|
1992
|
+
await this.loadArrayBuffer(content);
|
|
1993
|
+
} catch (e) {
|
|
1994
|
+
await this.loadArrayBuffer(await this.#repairMidi(content));
|
|
1995
|
+
}
|
|
1996
|
+
this.#log("Loading instruments...");
|
|
1997
|
+
this.#channels = await this.#getInstruments();
|
|
1998
|
+
this.#channelStates = Object.keys(this.#channels).reduce((acc, key) => ({ ...acc, [key]: false }), {});
|
|
1999
|
+
this.#channelVolumes = Object.keys(this.#channels).reduce((acc, key) => ({ ...acc, [key]: 1 }), {});
|
|
2000
|
+
if (setup?.volumes !== void 0) {
|
|
2001
|
+
await Promise.all(Object.keys(setup.volumes).map(async (channel) => {
|
|
2002
|
+
if (this.#channelVolumes[channel] === void 0) return;
|
|
2003
|
+
this.#channelVolumes[channel] = setup.volumes[channel];
|
|
2004
|
+
}));
|
|
2005
|
+
}
|
|
2006
|
+
const setupPrograms = /* @__PURE__ */ new Set();
|
|
2007
|
+
const setupPresets = {};
|
|
2008
|
+
if (setup?.presets !== void 0) {
|
|
2009
|
+
await Promise.all(Object.keys(setup.presets).map(async (channel) => {
|
|
2010
|
+
const presetInfo = await this.findPreset(setup.presets[channel]);
|
|
2011
|
+
if (!presetInfo) return;
|
|
2012
|
+
setupPresets[channel] = await this.getPreset(presetInfo.id);
|
|
2013
|
+
setupPrograms.add(presetInfo.program);
|
|
2014
|
+
}));
|
|
2015
|
+
}
|
|
2016
|
+
const uniqueInstruments = await this.#getUniqueInstruments();
|
|
2017
|
+
if (!Object.values(this.#channels).length) this.#log("Error: no instrument found");
|
|
2018
|
+
const presets = Promise.all([...uniqueInstruments].map(async (program) => {
|
|
2019
|
+
if (setupPrograms.has(program)) return;
|
|
2020
|
+
let preset = null;
|
|
2021
|
+
if (this.#presetMap[program] !== void 0) preset = await this.getPreset(this.#presetMap[program].id);
|
|
2022
|
+
else if (this.#opts.presetRandom) preset = await this.#getRandomPreset(program);
|
|
2023
|
+
else preset = await this.#getAutoPreset(program);
|
|
2024
|
+
this.#instruments[program] = preset;
|
|
2025
|
+
}));
|
|
2026
|
+
if (this.#opts.karaoke) {
|
|
2027
|
+
this.#log("Generating karaoke frames...");
|
|
2028
|
+
this.#lyrics = null;
|
|
2029
|
+
await this.#generateKaraokeFrames();
|
|
2030
|
+
if (this.#title) this.#sendKaraokeFrame("title", this.#title);
|
|
2031
|
+
}
|
|
2032
|
+
this.#log("Trim midi events...");
|
|
2033
|
+
this.#trimMidiEvents();
|
|
2034
|
+
queueMicrotask(() => this.triggerPlayerEvent("computed"));
|
|
2035
|
+
await presets;
|
|
2036
|
+
await Promise.all(Object.keys(this.#channels).map(async (channel) => {
|
|
2037
|
+
if (this.#players[channel]) this.#players[channel].close();
|
|
2038
|
+
if (setupPresets[channel] !== void 0) this.#players[channel] = await this.#createPlayer(setupPresets[channel]);
|
|
2039
|
+
else this.#players[channel] = await this.#createPlayer(this.#instruments[this.#channels[channel]]);
|
|
2040
|
+
}));
|
|
2041
|
+
this.#log("Initializing instrument states...");
|
|
2042
|
+
await this.#initInstrumentStates();
|
|
2043
|
+
await this.triggerPlayerEvent("presetsLoaded", this.#instruments);
|
|
2044
|
+
await this.#setupChange();
|
|
2045
|
+
this.#log("Player ready");
|
|
2046
|
+
}
|
|
2047
|
+
async getSongSetup() {
|
|
2048
|
+
let setup = { hash: this.#bufferHash, presets: {}, volumes: {} };
|
|
2049
|
+
Object.keys(this.#players).map(async (channel) => setup.presets[channel] = this.#players[channel].preset.id);
|
|
2050
|
+
setup.volumes = this.#channelVolumes;
|
|
2051
|
+
return setup;
|
|
2052
|
+
}
|
|
2053
|
+
async getTrainingPresets() {
|
|
2054
|
+
return await Promise.all(Object.values(this.#presetMap).map(async (preset) => preset.id));
|
|
1546
2055
|
}
|
|
1547
2056
|
async play(content = null) {
|
|
2057
|
+
if (this.#audioCtx.state === "suspended") {
|
|
2058
|
+
try {
|
|
2059
|
+
await this.#audioCtx.resume();
|
|
2060
|
+
} catch (e) {
|
|
2061
|
+
return false;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
1548
2064
|
if (content) await this.load(content);
|
|
1549
|
-
await this.#
|
|
1550
|
-
|
|
2065
|
+
await Promise.all(Object.keys(this.#players).map(async (k) => await this.#players[k]?.cancelQueue()));
|
|
2066
|
+
this.#compressor.restoreReverb();
|
|
2067
|
+
if (!this.isPlaying()) {
|
|
2068
|
+
if (!this.startTime) this.startTime = (/* @__PURE__ */ new Date()).getTime();
|
|
2069
|
+
this.scheduledTime = Date.now();
|
|
2070
|
+
this.schedulePlayLoop(this.sampleRate);
|
|
2071
|
+
}
|
|
2072
|
+
return true;
|
|
1551
2073
|
}
|
|
1552
2074
|
async pause() {
|
|
1553
2075
|
await super.pause();
|
|
2076
|
+
this.#compressor.killReverbTail();
|
|
1554
2077
|
await this.#clearActiveNotes();
|
|
1555
|
-
await this.#
|
|
2078
|
+
await Promise.all(Object.keys(this.#players).map(async (k) => await this.#players[k]?.cancelQueue()));
|
|
1556
2079
|
}
|
|
1557
|
-
async stop() {
|
|
2080
|
+
async stop(skipKill = false) {
|
|
1558
2081
|
await super.stop();
|
|
2082
|
+
this.setTimeoutId = false;
|
|
2083
|
+
if (!skipKill) {
|
|
2084
|
+
this.#compressor.killReverbTail();
|
|
2085
|
+
await Promise.all(Object.keys(this.#players).map(async (k) => await this.#players[k]?.cancelQueue()));
|
|
2086
|
+
}
|
|
1559
2087
|
await this.#clearActiveNotes();
|
|
1560
|
-
|
|
2088
|
+
if (this.#opts.karaoke) this.#sendKaraokeFrame("intro");
|
|
2089
|
+
return this;
|
|
2090
|
+
}
|
|
2091
|
+
getRealTimeVolume() {
|
|
2092
|
+
const analyser = this.#compressor.analyser;
|
|
2093
|
+
const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
2094
|
+
analyser.getByteFrequencyData(dataArray);
|
|
2095
|
+
let values = 0;
|
|
2096
|
+
for (let i = 0; i < dataArray.length; i++) values += dataArray[i];
|
|
2097
|
+
return values / (dataArray.length * 100);
|
|
2098
|
+
}
|
|
2099
|
+
getSongTimeRemaining() {
|
|
2100
|
+
return this.ticksToSeconds(this.getCurrentTick(), this.totalTicks);
|
|
2101
|
+
}
|
|
2102
|
+
async skipToSeconds(seconds) {
|
|
2103
|
+
const songTime = this.getSongTime();
|
|
2104
|
+
if (seconds < 0 || seconds > songTime) throw seconds + " seconds not within song time of " + songTime;
|
|
2105
|
+
await this.skipToTick(this.secondsToTicks(seconds));
|
|
2106
|
+
return this;
|
|
2107
|
+
}
|
|
2108
|
+
async generateWaveformSVG(samples = 1e3) {
|
|
2109
|
+
if (!this.totalTicks || !this.events) return "";
|
|
2110
|
+
const waveform = new Array(samples).fill(0);
|
|
2111
|
+
const tickInterval = this.totalTicks / samples;
|
|
2112
|
+
const allEvents = this.events.flatMap(
|
|
2113
|
+
(track, trackIdx) => track.map((event) => ({
|
|
2114
|
+
...event,
|
|
2115
|
+
computedChannel: event.channel !== void 0 ? event.channel : trackIdx
|
|
2116
|
+
}))
|
|
2117
|
+
).filter(
|
|
2118
|
+
(event) => event.name === "Controller Change" || event.name === "Program Change" || event.name === "Note on" && event.velocity > 0
|
|
2119
|
+
).sort((a, b) => a.tick - b.tick);
|
|
2120
|
+
const channelsVolume = /* @__PURE__ */ new Map();
|
|
2121
|
+
const channelsExpression = /* @__PURE__ */ new Map();
|
|
2122
|
+
allEvents.forEach((event) => {
|
|
2123
|
+
const idx = Math.floor(event.tick / tickInterval);
|
|
2124
|
+
if (idx >= samples) return;
|
|
2125
|
+
const chan = event.computedChannel;
|
|
2126
|
+
if (!channelsVolume.has(chan)) channelsVolume.set(chan, 100);
|
|
2127
|
+
if (!channelsExpression.has(chan)) channelsExpression.set(chan, 127);
|
|
2128
|
+
if (event.name === "Controller Change") {
|
|
2129
|
+
if (event.number === 7) channelsVolume.set(chan, event.value);
|
|
2130
|
+
else if (event.number === 11) channelsExpression.set(chan, event.value);
|
|
2131
|
+
} else if (event.name === "Note on") {
|
|
2132
|
+
const volFactor = channelsVolume.get(chan) / 127;
|
|
2133
|
+
const expFactor = channelsExpression.get(chan) / 127;
|
|
2134
|
+
const modulatedVelocity = event.velocity * volFactor * expFactor;
|
|
2135
|
+
waveform[idx] += modulatedVelocity;
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
const maxAmp = waveform.reduce((max, val) => {
|
|
2139
|
+
if (isNaN(val)) return max;
|
|
2140
|
+
return val > max ? val : max;
|
|
2141
|
+
}, 0);
|
|
2142
|
+
const normalized = maxAmp > 0 ? waveform.map((v) => isNaN(v) ? 0 : v / maxAmp) : waveform.fill(0);
|
|
2143
|
+
const width = samples;
|
|
2144
|
+
const height = width / 5;
|
|
2145
|
+
const points = normalized.map((val, i) => {
|
|
2146
|
+
const x = i;
|
|
2147
|
+
const y = Math.max(0, Math.min(height, height - val * height));
|
|
2148
|
+
return "".concat(x, ",").concat(y.toFixed(2));
|
|
2149
|
+
});
|
|
2150
|
+
const d = "M 0,".concat(height, " L ").concat(points.join(" L "), " L ").concat(width, ",").concat(height);
|
|
2151
|
+
return '<svg class="midiaudioplayer-waveform" viewBox="0 0 '.concat(width, " ").concat(height, '" preserveAspectRatio="none"><path d="').concat(d, '" fill="none" stroke-linecap="round" stroke-linejoin="round" /></svg>');
|
|
2152
|
+
}
|
|
2153
|
+
// ----------------------------------------------------------------------------------------------------------------------
|
|
2154
|
+
// ----------------------------------------------------------------------------------------------------------------------
|
|
2155
|
+
// ----------------------------------------------------------------------------------------------------------------------
|
|
2156
|
+
async hashBuffer(arrayBuffer, algorithm = "SHA-256") {
|
|
2157
|
+
const hashBuffer = await crypto.subtle.digest(algorithm, arrayBuffer);
|
|
2158
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
2159
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2160
|
+
}
|
|
2161
|
+
async #setupChange() {
|
|
2162
|
+
if (this.#presetTimer) clearTimeout(this.#presetTimer);
|
|
2163
|
+
this.#presetTimer = setTimeout(async () => {
|
|
2164
|
+
const setup = await this.getSongSetup();
|
|
2165
|
+
queueMicrotask(() => this.triggerPlayerEvent("setupChange", setup));
|
|
2166
|
+
}, 1e3);
|
|
2167
|
+
}
|
|
2168
|
+
async triggerPlayerEvent(playerEvent, data) {
|
|
2169
|
+
if (playerEvent == "fileLoaded") return;
|
|
2170
|
+
else if (playerEvent == "computed") {
|
|
2171
|
+
this.#vocalChannel = await this.#detectKaraokeVocalChannel();
|
|
2172
|
+
super.triggerPlayerEvent(playerEvent, {
|
|
2173
|
+
title: this.#title,
|
|
2174
|
+
karaoke: this.#haveLyrics,
|
|
2175
|
+
vocalChannel: this.#vocalChannel,
|
|
2176
|
+
tempo: this.tempo,
|
|
2177
|
+
division: this.division,
|
|
2178
|
+
duration: this.getSongTime(),
|
|
2179
|
+
sampleRate: this.sampleRate,
|
|
2180
|
+
totalTicks: this.totalTicks,
|
|
2181
|
+
totalEvents: this.totalEvents,
|
|
2182
|
+
channels: await this.#channels
|
|
2183
|
+
});
|
|
2184
|
+
} else if (playerEvent == "endOfFile" && this.#opts.karaoke) {
|
|
2185
|
+
queueMicrotask(() => super.triggerPlayerEvent(playerEvent, data));
|
|
2186
|
+
} else super.triggerPlayerEvent(playerEvent, data);
|
|
2187
|
+
}
|
|
2188
|
+
async playLoop(dryRun) {
|
|
2189
|
+
if (this.inLoop) return;
|
|
2190
|
+
if (!dryRun && this.endOfFile() && this.tick > 0) {
|
|
2191
|
+
await this.stop(true);
|
|
2192
|
+
this.tick = 0;
|
|
2193
|
+
this.triggerPlayerEvent("endOfFile");
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
this.inLoop = true;
|
|
2197
|
+
this.tick = this.getCurrentTick();
|
|
2198
|
+
const tracksLen = this.tracks.length;
|
|
2199
|
+
for (let i = 0; i < tracksLen; i++) {
|
|
2200
|
+
const result = this.tracks[i].handleEvent(this.tick, dryRun);
|
|
2201
|
+
if (!result) continue;
|
|
2202
|
+
const isArray = result.constructor === Array;
|
|
2203
|
+
const eventsLen = isArray ? result.length : 1;
|
|
2204
|
+
for (let j = 0; j < eventsLen; j++) {
|
|
2205
|
+
const event = isArray ? result[j] : result;
|
|
2206
|
+
const { name, data, value } = event;
|
|
2207
|
+
if (name === "Set Tempo") this.setTempo(data);
|
|
2208
|
+
if (dryRun) {
|
|
2209
|
+
if (name === "Program Change" && !this.instruments.includes(value)) this.instruments.push(value);
|
|
2210
|
+
} else {
|
|
2211
|
+
this.emitEvent(event);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
if (!dryRun && this.isPlaying()) this.triggerPlayerEvent("playing", { tick: this.tick });
|
|
2216
|
+
this.inLoop = false;
|
|
2217
|
+
}
|
|
2218
|
+
schedulePlayLoop(delay) {
|
|
2219
|
+
this.setTimeoutId = setTimeout(() => {
|
|
2220
|
+
if (this.setTimeoutId === false) return;
|
|
2221
|
+
this.playLoop();
|
|
2222
|
+
const currentAudioTime = this.#audioCtx.currentTime;
|
|
2223
|
+
if (!this._lastAudioTime) this._lastAudioTime = currentAudioTime;
|
|
2224
|
+
const elapsed = currentAudioTime - this._lastAudioTime;
|
|
2225
|
+
this._lastAudioTime = currentAudioTime;
|
|
2226
|
+
const sampleRateSec = this.sampleRate / 1e3;
|
|
2227
|
+
const drift = elapsed - sampleRateSec;
|
|
2228
|
+
const nextDelay = Math.max(0, this.sampleRate - drift * 1e3);
|
|
2229
|
+
this.schedulePlayLoop(nextDelay);
|
|
2230
|
+
}, delay);
|
|
2231
|
+
}
|
|
2232
|
+
emitEvent(event) {
|
|
2233
|
+
this.#handleMidiPipeline(event);
|
|
2234
|
+
}
|
|
2235
|
+
ticksToSeconds(startTick, endTick) {
|
|
2236
|
+
if (endTick === void 0) {
|
|
2237
|
+
endTick = startTick;
|
|
2238
|
+
startTick = 0;
|
|
2239
|
+
}
|
|
2240
|
+
if (startTick >= endTick) return 0;
|
|
2241
|
+
let seconds = 0;
|
|
2242
|
+
const len = this.tempoMap.length;
|
|
2243
|
+
const timeFactor = 60 / this.division;
|
|
2244
|
+
let low = 0;
|
|
2245
|
+
let high = len - 1;
|
|
2246
|
+
let startIndex = 0;
|
|
2247
|
+
while (low <= high) {
|
|
2248
|
+
const mid = low + high >> 1;
|
|
2249
|
+
if (this.tempoMap[mid].tick <= startTick) {
|
|
2250
|
+
startIndex = mid;
|
|
2251
|
+
low = mid + 1;
|
|
2252
|
+
} else {
|
|
2253
|
+
high = mid - 1;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
let currentTick = startTick;
|
|
2257
|
+
for (let i = startIndex; i < len; i++) {
|
|
2258
|
+
const entry = this.tempoMap[i];
|
|
2259
|
+
const nextTick = i + 1 < len ? this.tempoMap[i + 1].tick : endTick;
|
|
2260
|
+
if (nextTick <= startTick) continue;
|
|
2261
|
+
const segStart = Math.max(entry.tick, startTick);
|
|
2262
|
+
const segEnd = Math.min(nextTick, endTick);
|
|
2263
|
+
if (segStart >= endTick) break;
|
|
2264
|
+
seconds += (segEnd - segStart) / entry.tempo * timeFactor;
|
|
2265
|
+
currentTick = segEnd;
|
|
2266
|
+
}
|
|
2267
|
+
if (currentTick < endTick) {
|
|
2268
|
+
const lastEntry = this.tempoMap[len - 1];
|
|
2269
|
+
seconds += (endTick - currentTick) / lastEntry.tempo * timeFactor;
|
|
2270
|
+
}
|
|
2271
|
+
return seconds;
|
|
2272
|
+
}
|
|
2273
|
+
secondsToTicks(seconds) {
|
|
2274
|
+
let remainingSeconds = seconds;
|
|
2275
|
+
const len = this.tempoMap.length;
|
|
2276
|
+
const factor = 60 / this.division;
|
|
2277
|
+
for (let i = 0; i < len; i++) {
|
|
2278
|
+
const entry = this.tempoMap[i];
|
|
2279
|
+
const nextTick = i + 1 < len ? this.tempoMap[i + 1].tick : Infinity;
|
|
2280
|
+
const segmentTicks = nextTick - entry.tick;
|
|
2281
|
+
const segmentSeconds = segmentTicks / entry.tempo * factor;
|
|
2282
|
+
if (remainingSeconds <= segmentSeconds) {
|
|
2283
|
+
return entry.tick + Math.round(remainingSeconds * entry.tempo / factor);
|
|
2284
|
+
}
|
|
2285
|
+
remainingSeconds -= segmentSeconds;
|
|
2286
|
+
}
|
|
2287
|
+
return this.totalTicks;
|
|
1561
2288
|
}
|
|
1562
|
-
|
|
1563
|
-
if (
|
|
2289
|
+
getTickBeforeSeconds(targetTick, seconds) {
|
|
2290
|
+
if (targetTick <= 0) return 0;
|
|
2291
|
+
const targetTime = this.ticksToSeconds(0, targetTick);
|
|
2292
|
+
const desiredTime = Math.max(0, targetTime - seconds);
|
|
2293
|
+
return this.secondsToTicks(desiredTime);
|
|
2294
|
+
}
|
|
2295
|
+
async skipToTick(tick) {
|
|
2296
|
+
const safeTick = Math.max(0, Math.min(tick, this.totalTicks || 0));
|
|
2297
|
+
const wasPlaying = this.isPlaying();
|
|
2298
|
+
this.#clearActiveNotes();
|
|
2299
|
+
Object.keys(this.channels).forEach((k) => this.channels[k]?.cancelQueue?.());
|
|
2300
|
+
if (wasPlaying) super.pause();
|
|
2301
|
+
this.startTick = safeTick;
|
|
2302
|
+
this.tick = safeTick;
|
|
2303
|
+
if (this.tempoMap && this.tempoMap.length > 0) {
|
|
2304
|
+
for (let i = this.tempoMap.length - 1; i >= 0; i--) {
|
|
2305
|
+
if (this.tempoMap[i].tick <= safeTick) {
|
|
2306
|
+
this.setTempo(this.tempoMap[i].tempo);
|
|
2307
|
+
break;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
try {
|
|
2312
|
+
const controllerChange = [];
|
|
2313
|
+
const programChange = [];
|
|
2314
|
+
const pitchBend = [];
|
|
2315
|
+
const karaokeEvent = [];
|
|
2316
|
+
this.#collectStateAtTick(safeTick).forEach((event) => {
|
|
2317
|
+
const channel = event.channel;
|
|
2318
|
+
if ((channel === void 0 || !this.channels[channel]) && event.name !== "Karaoke Event") return;
|
|
2319
|
+
switch (event.name) {
|
|
2320
|
+
case "Controller Change":
|
|
2321
|
+
controllerChange[event.channel] = event;
|
|
2322
|
+
break;
|
|
2323
|
+
case "Program Change":
|
|
2324
|
+
programChange[event.channel] = event;
|
|
2325
|
+
break;
|
|
2326
|
+
case "Pitch Bend":
|
|
2327
|
+
pitchBend[event.channel] = event;
|
|
2328
|
+
break;
|
|
2329
|
+
case "Karaoke Event":
|
|
2330
|
+
karaokeEvent[event.channel] = event;
|
|
2331
|
+
break;
|
|
2332
|
+
}
|
|
2333
|
+
});
|
|
2334
|
+
controllerChange.forEach((evt) => this.emitEvent(evt));
|
|
2335
|
+
programChange.forEach((evt) => this.emitEvent(evt));
|
|
2336
|
+
pitchBend.forEach((evt) => this.emitEvent(evt));
|
|
2337
|
+
karaokeEvent.forEach((evt) => this.triggerPlayerEvent("karaoke", { type: evt.type, tick: evt.tick, html: evt.text }));
|
|
2338
|
+
} catch (e) {
|
|
2339
|
+
console.warn("Chase MIDI Error:", e);
|
|
2340
|
+
this.#log("Chase MIDI Error:", e);
|
|
2341
|
+
}
|
|
2342
|
+
if (this.tracks && this.tracks.length > 0) {
|
|
2343
|
+
this.tracks.forEach((track, index2) => {
|
|
2344
|
+
const trackEvents = this.events[index2];
|
|
2345
|
+
if (trackEvents && trackEvents.length > 0) {
|
|
2346
|
+
let low = 0;
|
|
2347
|
+
let high = trackEvents.length - 1;
|
|
2348
|
+
let pointer = trackEvents.length;
|
|
2349
|
+
while (low <= high) {
|
|
2350
|
+
const mid = low + high >> 1;
|
|
2351
|
+
if (trackEvents[mid].tick >= safeTick) {
|
|
2352
|
+
pointer = mid;
|
|
2353
|
+
high = mid - 1;
|
|
2354
|
+
} else {
|
|
2355
|
+
low = mid + 1;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
track.eventIndex = pointer;
|
|
2359
|
+
} else if (typeof track.setEventIndexByTick === "function") {
|
|
2360
|
+
track.setEventIndexByTick(safeTick);
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
if (wasPlaying) this.play();
|
|
2365
|
+
else this.triggerPlayerEvent("playing", { tick: safeTick });
|
|
2366
|
+
return this;
|
|
2367
|
+
}
|
|
2368
|
+
#collectStateAtTick(tick) {
|
|
2369
|
+
const dominated = {};
|
|
2370
|
+
if (!this.events) return [];
|
|
2371
|
+
for (let t = 0; t < this.events.length; t++) {
|
|
2372
|
+
const trackEvents = this.events[t];
|
|
2373
|
+
if (!trackEvents || trackEvents.length === 0) continue;
|
|
2374
|
+
let low = 0;
|
|
2375
|
+
let high = trackEvents.length - 1;
|
|
2376
|
+
let endIdx = trackEvents.length;
|
|
2377
|
+
while (low <= high) {
|
|
2378
|
+
const mid = low + high >> 1;
|
|
2379
|
+
if (trackEvents[mid].tick >= tick) {
|
|
2380
|
+
endIdx = mid;
|
|
2381
|
+
high = mid - 1;
|
|
2382
|
+
} else {
|
|
2383
|
+
low = mid + 1;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
for (let i = 0; i < endIdx; i++) {
|
|
2387
|
+
const event = trackEvents[i];
|
|
2388
|
+
let key;
|
|
2389
|
+
if (event.name === "Program Change") {
|
|
2390
|
+
key = "pc:" + event.channel;
|
|
2391
|
+
} else if (event.name === "Controller Change") {
|
|
2392
|
+
key = "cc:" + event.channel + ":" + event.number;
|
|
2393
|
+
} else if (event.name === "Pitch Bend") {
|
|
2394
|
+
key = "pb:" + event.channel;
|
|
2395
|
+
} else if (event.name === "Karaoke Event") {
|
|
2396
|
+
key = "ke:" + event.channel;
|
|
2397
|
+
}
|
|
2398
|
+
if (key) {
|
|
2399
|
+
dominated[key] = event;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
return Object.keys(dominated).map((key) => dominated[key]);
|
|
2404
|
+
}
|
|
2405
|
+
async #initInstrumentStates() {
|
|
2406
|
+
if (this.events) {
|
|
2407
|
+
this.#collectStateAtTick(1).forEach((event) => {
|
|
2408
|
+
const channel = event.channel;
|
|
2409
|
+
if (!this.#players[channel]) return;
|
|
2410
|
+
switch (event.name) {
|
|
2411
|
+
case "Controller Change":
|
|
2412
|
+
this.#players[channel].setController(event.number, event.value);
|
|
2413
|
+
break;
|
|
2414
|
+
case "Pitch Bend":
|
|
2415
|
+
this.#players[channel].setPitchBend?.(event.value);
|
|
2416
|
+
break;
|
|
2417
|
+
case "Program Change":
|
|
2418
|
+
if (event.value >= 0 && event.value <= 127 && this.#instruments[event.value + 1] !== void 0 && event.channel != 10) {
|
|
2419
|
+
if (this.#players[channel].preset?.program !== event.value + 1) {
|
|
2420
|
+
this.#players[channel].setPreset(this.#instruments[event.value + 1]);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
break;
|
|
2424
|
+
}
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
async #getInstruments() {
|
|
2429
|
+
const instrumentMap = {};
|
|
2430
|
+
const channelUsed = /* @__PURE__ */ new Set();
|
|
2431
|
+
this.events.forEach((track) => {
|
|
2432
|
+
track.forEach((event) => {
|
|
2433
|
+
if (event.name === "Program Change" && event.value >= 0 && event.value <= 127) {
|
|
2434
|
+
if (instrumentMap[event.channel]) return;
|
|
2435
|
+
else if (event.channel == 10) instrumentMap[event.channel] = -1;
|
|
2436
|
+
else instrumentMap[event.channel] = event.value + 1;
|
|
2437
|
+
} else if (event.name === "Note on" && event.channel == 10) {
|
|
2438
|
+
instrumentMap[event.channel] = -1;
|
|
2439
|
+
channelUsed.add(10);
|
|
2440
|
+
} else if (event.name === "Note on") {
|
|
2441
|
+
channelUsed.add(event.channel);
|
|
2442
|
+
}
|
|
2443
|
+
});
|
|
2444
|
+
});
|
|
2445
|
+
Object.keys(instrumentMap).forEach((channel) => {
|
|
2446
|
+
if (!channelUsed.has(Number(channel))) delete instrumentMap[channel];
|
|
2447
|
+
});
|
|
2448
|
+
return instrumentMap;
|
|
2449
|
+
}
|
|
2450
|
+
async #getUniqueInstruments() {
|
|
2451
|
+
const instrumentMap = /* @__PURE__ */ new Set();
|
|
2452
|
+
this.events.forEach((track) => {
|
|
2453
|
+
track.forEach((event) => {
|
|
2454
|
+
if (event.name === "Program Change" && event.value >= 0 && event.value <= 127) {
|
|
2455
|
+
instrumentMap.add(event.channel == 10 ? -1 : event.value + 1);
|
|
2456
|
+
} else if (event.name === "Note on" && event.channel == 10) instrumentMap.add(-1);
|
|
2457
|
+
});
|
|
2458
|
+
});
|
|
2459
|
+
return instrumentMap;
|
|
2460
|
+
}
|
|
2461
|
+
async #getRandomPreset(program) {
|
|
2462
|
+
const instruments = await this.getProgramInstruments(program);
|
|
2463
|
+
if (!instruments.length) return null;
|
|
2464
|
+
let preset = null;
|
|
2465
|
+
this.#opts.preferred.some((bank) => {
|
|
2466
|
+
const regex = new RegExp("_".concat(bank, "$"), "i");
|
|
2467
|
+
const group = instruments.filter((i) => regex.test(i.id));
|
|
2468
|
+
if (group.length) {
|
|
2469
|
+
preset = group[Math.floor(Math.random() * group.length)];
|
|
2470
|
+
return true;
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
if (!preset) preset = instruments[Math.floor(Math.random() * instruments.length)];
|
|
2474
|
+
this.#presetMap[program] = preset;
|
|
2475
|
+
return await this.getPreset(preset.id);
|
|
2476
|
+
}
|
|
2477
|
+
async #getAutoPreset(program) {
|
|
2478
|
+
const instruments = await this.getProgramInstruments(program);
|
|
2479
|
+
if (!instruments.length) return null;
|
|
2480
|
+
let preset = null;
|
|
2481
|
+
this.#opts.preferred.some((bank) => {
|
|
2482
|
+
const regex = new RegExp("_".concat(bank, "$"), "i");
|
|
2483
|
+
preset = instruments.find((i) => regex.test(i.id));
|
|
2484
|
+
if (preset) return true;
|
|
2485
|
+
});
|
|
2486
|
+
if (!preset) preset = instruments[0];
|
|
2487
|
+
this.#presetMap[program] = preset;
|
|
2488
|
+
return await this.getPreset(preset.id);
|
|
2489
|
+
}
|
|
2490
|
+
async #createPlayer(preset) {
|
|
2491
|
+
return new index_default(preset, this.#audioCtx, this.#compressor);
|
|
1564
2492
|
}
|
|
1565
2493
|
async #handleMidiPipeline(event) {
|
|
1566
|
-
if (event.name !== "Note on" && event.name !== "Note off") return;
|
|
1567
2494
|
if (!this.isPlaying()) return;
|
|
1568
|
-
if (event.noteNumber === void 0) return;
|
|
1569
|
-
const now = this.#audioCtx.currentTime;
|
|
1570
2495
|
switch (event.name) {
|
|
1571
2496
|
case "Note on":
|
|
2497
|
+
if (event.tick < this.tick - 100) return;
|
|
2498
|
+
if (event.noteNumber === void 0) return;
|
|
2499
|
+
if (event.channel == this.#vocalChannel && this.#opts.muteExpression) return;
|
|
1572
2500
|
if (event.velocity > 0 && event.velocity <= 127) {
|
|
1573
|
-
this.#
|
|
1574
|
-
|
|
1575
|
-
const
|
|
1576
|
-
|
|
2501
|
+
this.#stopNote(event.channel, event.noteNumber);
|
|
2502
|
+
if (this.#channelVolumes[event.channel] == 0) return;
|
|
2503
|
+
const noteVelocityRatio = event.velocity / 127;
|
|
2504
|
+
const finalVol = _MidiAudioPlayer.REFERENCE_GAIN * Math.pow(noteVelocityRatio, 2) * this.#channelVolumes[event.channel];
|
|
2505
|
+
const envelope = this.#players[event.channel]?.queueWaveTable(0, event.noteNumber, 2, finalVol);
|
|
2506
|
+
if (envelope) this.#addNote(event.channel, event.noteNumber, envelope);
|
|
1577
2507
|
} else {
|
|
1578
|
-
this.#
|
|
2508
|
+
this.#stopNote(event.channel, event.noteNumber);
|
|
1579
2509
|
}
|
|
1580
2510
|
break;
|
|
1581
2511
|
case "Note off":
|
|
1582
|
-
|
|
2512
|
+
if (event.noteNumber === void 0) return;
|
|
2513
|
+
this.#stopNote(event.channel, event.noteNumber);
|
|
2514
|
+
break;
|
|
2515
|
+
case "Controller Change":
|
|
2516
|
+
this.#players[event.channel]?.setController(event.number, event.value);
|
|
2517
|
+
break;
|
|
2518
|
+
case "Pitch Bend":
|
|
2519
|
+
this.#players[event.channel]?.setPitchBend?.(event.value);
|
|
2520
|
+
break;
|
|
2521
|
+
case "Program Change":
|
|
2522
|
+
return;
|
|
2523
|
+
if (!this.#players[event.channel]) return;
|
|
2524
|
+
if (event.channel == 10 || event.value > 127 || event.value < 0) break;
|
|
2525
|
+
if (this.#players[event.channel] !== void 0 && this.#players[event.channel].preset.program != event.value + 1)
|
|
2526
|
+
this.#players[event.channel].setPreset(this.#instruments[event.value + 1]);
|
|
2527
|
+
break;
|
|
2528
|
+
case "Karaoke Event":
|
|
2529
|
+
if (event.tick < this.tick - this.secondsToTicks(10)) return;
|
|
2530
|
+
this.triggerPlayerEvent("karaoke", { type: event.type, tick: event.tick, html: event.text });
|
|
1583
2531
|
break;
|
|
1584
2532
|
}
|
|
1585
2533
|
}
|
|
1586
|
-
#
|
|
1587
|
-
|
|
2534
|
+
#addNote(channel, note, envelope) {
|
|
2535
|
+
if (!this.#activeNotes[channel]) this.#activeNotes[channel] = /* @__PURE__ */ new Map();
|
|
2536
|
+
this.#activeNotes[channel].set(note, envelope);
|
|
2537
|
+
this.#updateChannelStates();
|
|
2538
|
+
const realDurationMs = (envelope.duration || 0) * 1e3;
|
|
2539
|
+
envelope.cleanupTimer = setTimeout(() => {
|
|
2540
|
+
if (this.#activeNotes[channel]?.get(note) === envelope) {
|
|
2541
|
+
this.#activeNotes[channel].delete(note);
|
|
2542
|
+
this.#updateChannelStates();
|
|
2543
|
+
}
|
|
2544
|
+
}, realDurationMs + 50);
|
|
2545
|
+
}
|
|
2546
|
+
#stopNote(channel, noteNumber) {
|
|
2547
|
+
const player = this.#players[channel];
|
|
2548
|
+
const envelope = this.#activeNotes[channel]?.get(noteNumber);
|
|
1588
2549
|
if (envelope) {
|
|
1589
|
-
envelope.
|
|
1590
|
-
|
|
2550
|
+
if (envelope.cleanupTimer) clearTimeout(envelope.cleanupTimer);
|
|
2551
|
+
const removeNoteFromRegistry = () => {
|
|
2552
|
+
this.#activeNotes[channel]?.delete(noteNumber);
|
|
2553
|
+
this.#updateChannelStates();
|
|
2554
|
+
};
|
|
2555
|
+
if (player && player.isSustainActive()) {
|
|
2556
|
+
player.registerSustainNote(() => envelope.cancel(false));
|
|
2557
|
+
} else {
|
|
2558
|
+
envelope.cancel(false);
|
|
2559
|
+
}
|
|
2560
|
+
removeNoteFromRegistry();
|
|
1591
2561
|
}
|
|
1592
2562
|
}
|
|
1593
2563
|
#clearActiveNotes() {
|
|
1594
|
-
|
|
1595
|
-
this.#activeNotes.forEach((envelope, note) => {
|
|
1596
|
-
if (envelope
|
|
2564
|
+
Object.keys(this.#activeNotes).forEach((channel) => {
|
|
2565
|
+
this.#activeNotes[channel].forEach((envelope, note) => {
|
|
2566
|
+
if (envelope) {
|
|
2567
|
+
if (envelope.cleanupTimer) clearTimeout(envelope.cleanupTimer);
|
|
2568
|
+
if (envelope.cancel) envelope.cancel(true);
|
|
2569
|
+
}
|
|
2570
|
+
this.#activeNotes[channel]?.delete(note);
|
|
2571
|
+
});
|
|
2572
|
+
});
|
|
2573
|
+
this.#updateChannelStates();
|
|
2574
|
+
}
|
|
2575
|
+
async #updateChannelStates() {
|
|
2576
|
+
let hasChanged = false;
|
|
2577
|
+
const nextStates = {};
|
|
2578
|
+
Object.keys(this.#players).forEach((channel) => {
|
|
2579
|
+
const isActive = Boolean(this.#activeNotes[channel]?.size && this.#activeNotes[channel].size > 0);
|
|
2580
|
+
nextStates[channel] = isActive;
|
|
2581
|
+
if (this.#channelStates[channel] !== isActive) hasChanged = true;
|
|
2582
|
+
});
|
|
2583
|
+
if (hasChanged) {
|
|
2584
|
+
this.#channelStates = nextStates;
|
|
2585
|
+
this.triggerPlayerEvent("channelState", this.#channelStates);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
async #repairMidi(buffer) {
|
|
2589
|
+
const src = new Uint8Array(buffer);
|
|
2590
|
+
const view = new DataView(buffer);
|
|
2591
|
+
const magic = String.fromCharCode(...src.slice(0, 4));
|
|
2592
|
+
if (magic !== "MThd") throw new Error("Invalid MIDI file (MThd missing)");
|
|
2593
|
+
const headerLen = view.getUint32(4);
|
|
2594
|
+
const format = view.getUint16(8);
|
|
2595
|
+
const ntrks = view.getUint16(10);
|
|
2596
|
+
const division = view.getUint16(12);
|
|
2597
|
+
const EOT = [255, 47, 0];
|
|
2598
|
+
const chunks = [];
|
|
2599
|
+
let pos = 8 + headerLen;
|
|
2600
|
+
while (pos < src.length) {
|
|
2601
|
+
if (pos + 8 > src.length) {
|
|
2602
|
+
break;
|
|
2603
|
+
}
|
|
2604
|
+
const tag = String.fromCharCode(...src.slice(pos, pos + 4));
|
|
2605
|
+
const declaredLen = view.getUint32(pos + 4);
|
|
2606
|
+
const dataStart = pos + 8;
|
|
2607
|
+
const dataEnd = dataStart + declaredLen;
|
|
2608
|
+
if (tag !== "MTrk") {
|
|
2609
|
+
const end = Math.min(dataEnd, src.length);
|
|
2610
|
+
chunks.push({ tag, data: src.slice(pos, end), repaired: false });
|
|
2611
|
+
pos = dataEnd;
|
|
2612
|
+
continue;
|
|
2613
|
+
}
|
|
2614
|
+
const trackNum = chunks.filter((c) => c.tag === "MTrk").length + 1;
|
|
2615
|
+
const available = Math.min(declaredLen, src.length - dataStart);
|
|
2616
|
+
const trackData = src.slice(dataStart, dataStart + available);
|
|
2617
|
+
const last3 = trackData.slice(-3);
|
|
2618
|
+
const hasEOT = last3[0] === 255 && last3[1] === 47 && last3[2] === 0;
|
|
2619
|
+
if (hasEOT && available === declaredLen) {
|
|
2620
|
+
chunks.push({ tag, data: trackData, repaired: false });
|
|
2621
|
+
} else {
|
|
2622
|
+
let repairedData;
|
|
2623
|
+
if (available < declaredLen) {
|
|
2624
|
+
const missing = declaredLen - available;
|
|
2625
|
+
const last2 = trackData.slice(-2);
|
|
2626
|
+
if (last2[0] === 255 && last2[1] === 47) {
|
|
2627
|
+
repairedData = new Uint8Array(trackData.length + 1);
|
|
2628
|
+
repairedData.set(trackData);
|
|
2629
|
+
repairedData[trackData.length] = 0;
|
|
2630
|
+
} else {
|
|
2631
|
+
repairedData = new Uint8Array(trackData.length + 3);
|
|
2632
|
+
repairedData.set(trackData);
|
|
2633
|
+
repairedData.set(EOT, trackData.length);
|
|
2634
|
+
}
|
|
2635
|
+
} else {
|
|
2636
|
+
repairedData = new Uint8Array(trackData.length + 3);
|
|
2637
|
+
repairedData.set(trackData);
|
|
2638
|
+
repairedData.set(EOT, trackData.length);
|
|
2639
|
+
}
|
|
2640
|
+
chunks.push({ tag, data: repairedData, repaired: true });
|
|
2641
|
+
}
|
|
2642
|
+
pos = dataEnd;
|
|
2643
|
+
}
|
|
2644
|
+
const fixedNtrks = chunks.filter((c) => c.tag === "MTrk").length;
|
|
2645
|
+
const totalSize = 14 + chunks.reduce((acc, c) => acc + 8 + c.data.length, 0);
|
|
2646
|
+
const out = new Uint8Array(totalSize);
|
|
2647
|
+
const outView = new DataView(out.buffer);
|
|
2648
|
+
out.set([77, 84, 104, 100], 0);
|
|
2649
|
+
outView.setUint32(4, 6);
|
|
2650
|
+
outView.setUint16(8, format);
|
|
2651
|
+
outView.setUint16(10, fixedNtrks);
|
|
2652
|
+
outView.setUint16(12, division);
|
|
2653
|
+
let outPos = 14;
|
|
2654
|
+
for (const chunk of chunks) {
|
|
2655
|
+
const tagBytes = chunk.tag.split("").map((c) => c.charCodeAt(0));
|
|
2656
|
+
out.set(tagBytes, outPos);
|
|
2657
|
+
outView.setUint32(outPos + 4, chunk.data.length);
|
|
2658
|
+
out.set(chunk.data, outPos + 8);
|
|
2659
|
+
outPos += 8 + chunk.data.length;
|
|
2660
|
+
}
|
|
2661
|
+
const repairedCount = chunks.filter((c) => c.repaired).length;
|
|
2662
|
+
return out.buffer;
|
|
2663
|
+
}
|
|
2664
|
+
#trimMidiEvents() {
|
|
2665
|
+
if (!this.events || this.events.length === 0) return;
|
|
2666
|
+
let firstNoteTick = Infinity;
|
|
2667
|
+
let lastNoteTick = 0;
|
|
2668
|
+
this.events.forEach((track) => {
|
|
2669
|
+
track.forEach((event) => {
|
|
2670
|
+
if (event.name === "Note on" || event.name === "Note off") {
|
|
2671
|
+
if (event.tick < firstNoteTick) firstNoteTick = event.tick;
|
|
2672
|
+
if (event.tick > lastNoteTick) lastNoteTick = event.tick;
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2675
|
+
});
|
|
2676
|
+
if (firstNoteTick === Infinity) return;
|
|
2677
|
+
const allSetupEventsBeforeFirstNote = [];
|
|
2678
|
+
this.events.forEach((track, trackIdx) => {
|
|
2679
|
+
track.forEach((event) => {
|
|
2680
|
+
const isSetupEvent = event.name === "Program Change" || event.name === "Controller Change" || event.name === "Pitch Bend" || event.name === "Set Tempo";
|
|
2681
|
+
if (event.tick < firstNoteTick && isSetupEvent) {
|
|
2682
|
+
allSetupEventsBeforeFirstNote.push({ event, trackIdx });
|
|
2683
|
+
}
|
|
2684
|
+
});
|
|
2685
|
+
});
|
|
2686
|
+
const uniqueSetupByTrack = Object.fromEntries(this.events.map((_, idx) => [idx, []]));
|
|
2687
|
+
const globalUniqueKeys = /* @__PURE__ */ new Set();
|
|
2688
|
+
for (let i = allSetupEventsBeforeFirstNote.length - 1; i >= 0; i--) {
|
|
2689
|
+
const { event, trackIdx } = allSetupEventsBeforeFirstNote[i];
|
|
2690
|
+
const channel = event.channel !== void 0 ? event.channel : "track-".concat(trackIdx);
|
|
2691
|
+
let key = null;
|
|
2692
|
+
if (event.name === "Program Change") {
|
|
2693
|
+
key = "pc:".concat(channel);
|
|
2694
|
+
} else if (event.name === "Controller Change") {
|
|
2695
|
+
key = "cc:".concat(channel, ":").concat(event.number);
|
|
2696
|
+
} else if (event.name === "Pitch Bend") {
|
|
2697
|
+
key = "pb:".concat(channel);
|
|
2698
|
+
} else if (event.name === "Set Tempo") {
|
|
2699
|
+
key = "tempo";
|
|
2700
|
+
}
|
|
2701
|
+
if (key) {
|
|
2702
|
+
if (!globalUniqueKeys.has(key)) {
|
|
2703
|
+
globalUniqueKeys.add(key);
|
|
2704
|
+
const clonedEvent = { ...event, tick: 0 };
|
|
2705
|
+
uniqueSetupByTrack[trackIdx].push(clonedEvent);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
const trimmedEvents = this.events.map((track, trackIdx) => {
|
|
2710
|
+
const newTrack = [];
|
|
2711
|
+
track.forEach((event) => {
|
|
2712
|
+
const isSetupEvent = event.name === "Program Change" || event.name === "Controller Change" || event.name === "Pitch Bend" || event.name === "Set Tempo";
|
|
2713
|
+
const isTextOrKaraoke = event.name === "Text Event" || event.name === "Lyric Event" || event.name === "Track Name" || event.name === "Karaoke Event";
|
|
2714
|
+
if (event.tick < firstNoteTick) {
|
|
2715
|
+
if (!isSetupEvent && (isTextOrKaraoke || trackIdx === 0)) {
|
|
2716
|
+
event.tick = 0;
|
|
2717
|
+
newTrack.push(event);
|
|
2718
|
+
}
|
|
2719
|
+
} else {
|
|
2720
|
+
event.tick = event.tick - firstNoteTick;
|
|
2721
|
+
const maxAllowedTick = lastNoteTick - firstNoteTick;
|
|
2722
|
+
if (event.tick > maxAllowedTick) {
|
|
2723
|
+
event.tick = maxAllowedTick;
|
|
2724
|
+
}
|
|
2725
|
+
newTrack.push(event);
|
|
2726
|
+
}
|
|
1597
2727
|
});
|
|
1598
|
-
|
|
2728
|
+
const filteredTrackSetup = uniqueSetupByTrack[trackIdx] || [];
|
|
2729
|
+
return [...filteredTrackSetup, ...newTrack].sort((a, b) => a.tick - b.tick);
|
|
2730
|
+
});
|
|
2731
|
+
this.events = trimmedEvents;
|
|
2732
|
+
this.totalTicks = lastNoteTick - firstNoteTick;
|
|
2733
|
+
if (typeof this.computeTempoMap === "function") this.computeTempoMap();
|
|
2734
|
+
}
|
|
2735
|
+
async #extractLyrics() {
|
|
2736
|
+
if (this.#lyrics) return this.#lyrics;
|
|
2737
|
+
const structure = { language: "", title: "", paragraphs: [] };
|
|
2738
|
+
let bestTrack = null;
|
|
2739
|
+
let maxTextEventsCount = 0;
|
|
2740
|
+
this.events.forEach((track) => {
|
|
2741
|
+
const textEventsInTrack = track.filter(
|
|
2742
|
+
(e) => e.name === "Text Event" || e.name === "Lyric Event" || e.name === "Cue Point" || e.name === "Marker" || e.name === "Track Name"
|
|
2743
|
+
);
|
|
2744
|
+
const realLyricsCount = textEventsInTrack.filter((e) => {
|
|
2745
|
+
const textStr = e.string || e.text || "";
|
|
2746
|
+
return textStr && !textStr.startsWith("@");
|
|
2747
|
+
}).length;
|
|
2748
|
+
if (realLyricsCount > maxTextEventsCount) {
|
|
2749
|
+
maxTextEventsCount = realLyricsCount;
|
|
2750
|
+
bestTrack = textEventsInTrack;
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
if (!bestTrack || bestTrack.length === 0) return structure;
|
|
2754
|
+
const allTextEvents = bestTrack.sort((a, b) => a.tick - b.tick);
|
|
2755
|
+
let paragraphs = [];
|
|
2756
|
+
let currentParaLines = [];
|
|
2757
|
+
let currentLineBlocks = [];
|
|
2758
|
+
let lastBlockTick = 0;
|
|
2759
|
+
allTextEvents.forEach((event) => {
|
|
2760
|
+
let text = this.#decodeKaraokeString(event.string || "");
|
|
2761
|
+
if (!text) return;
|
|
2762
|
+
if (/^Track-/i.test(text.trim()) || /^Piste/i.test(text.trim()) || text.trim() === "" || event.tick === 0 && text.length > 20) {
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
if (text.startsWith("@L")) {
|
|
2766
|
+
structure.language = text.substring(2).trim();
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
if (text.startsWith("@T")) {
|
|
2770
|
+
structure.title += (structure.title ? " / " : "") + text.substring(2).trim();
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
if (text.startsWith("@") || text.startsWith("(") || text.startsWith("PART") || /^\d+\s+\d+/.test(text.trim())) {
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
if (/^(Verse|Chorus|Bridge|Break|Intro|End\.)/i.test(text.trim())) {
|
|
2777
|
+
const isExplicitCut = text.startsWith("\\") || text.startsWith("/");
|
|
2778
|
+
const isNaturalTransition = currentLineBlocks.length === 0 || event.tick - lastBlockTick > 500;
|
|
2779
|
+
if (isExplicitCut || isNaturalTransition) {
|
|
2780
|
+
if (currentLineBlocks.length > 0) {
|
|
2781
|
+
currentParaLines.push({ tick: currentLineBlocks[0].tick, blocks: currentLineBlocks });
|
|
2782
|
+
currentLineBlocks = [];
|
|
2783
|
+
}
|
|
2784
|
+
if (currentParaLines.length > 0) {
|
|
2785
|
+
while (currentParaLines.length > 4) {
|
|
2786
|
+
const linesToPush = currentParaLines.splice(0, 4);
|
|
2787
|
+
paragraphs.push({ tick: linesToPush[0].tick, lines: linesToPush });
|
|
2788
|
+
}
|
|
2789
|
+
if (currentParaLines.length > 0) {
|
|
2790
|
+
paragraphs.push({ tick: currentParaLines[0].tick, lines: currentParaLines });
|
|
2791
|
+
currentParaLines = [];
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
let forceNewLine = false;
|
|
2798
|
+
if (currentLineBlocks.length > 0) {
|
|
2799
|
+
const prevBlock = currentLineBlocks[currentLineBlocks.length - 1];
|
|
2800
|
+
const prevText = prevBlock.text;
|
|
2801
|
+
const currentTrimmed = text.trimLeft();
|
|
2802
|
+
if (currentTrimmed.length > 0) {
|
|
2803
|
+
const isCapitalized = /^[A-Z]/.test(currentTrimmed) || /^'[A-Z]/.test(currentTrimmed);
|
|
2804
|
+
const prevIsCapitalized = /^[A-Z]/.test(prevText.trim()) || /^'[A-Z]/.test(prevText.trim());
|
|
2805
|
+
if (isCapitalized && !prevText.endsWith(" ") && prevText.trim() != "o" && !prevIsCapitalized) {
|
|
2806
|
+
if (event.tick > lastBlockTick) {
|
|
2807
|
+
forceNewLine = true;
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
if (text.startsWith('"') && prevText.endsWith('"')) {
|
|
2811
|
+
forceNewLine = true;
|
|
2812
|
+
}
|
|
2813
|
+
if (text.startsWith('"') && (prevText.endsWith(")") || prevText.endsWith(')"'))) {
|
|
2814
|
+
forceNewLine = true;
|
|
2815
|
+
}
|
|
2816
|
+
if (currentTrimmed.startsWith('"') && prevText.trimRight().endsWith(")")) {
|
|
2817
|
+
forceNewLine = true;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
const isNewParagraphMarker = text.startsWith("\\");
|
|
2822
|
+
const isNewLineMarker = text.startsWith("/") || forceNewLine;
|
|
2823
|
+
if (isNewParagraphMarker || isNewLineMarker) {
|
|
2824
|
+
if (text.startsWith("\\") || text.startsWith("/")) {
|
|
2825
|
+
text = text.substring(1);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
text = text.replace(/[\r\n]/g, "");
|
|
2829
|
+
let isTimeGapTrigger = false;
|
|
2830
|
+
if (lastBlockTick > 0 && event.tick > lastBlockTick) {
|
|
2831
|
+
const secondsSilence = this.ticksToSeconds(lastBlockTick, event.tick);
|
|
2832
|
+
if (secondsSilence > 2.5) {
|
|
2833
|
+
isTimeGapTrigger = true;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
const currentLineChars = currentLineBlocks.reduce((sum, b) => sum + b.text.length, 0);
|
|
2837
|
+
const isWordLimitTrigger = currentLineChars + text.length > this.#opts.maxCharPerLine;
|
|
2838
|
+
if (isNewLineMarker || isNewParagraphMarker || isTimeGapTrigger || isWordLimitTrigger) {
|
|
2839
|
+
if (currentLineBlocks.length > 0) {
|
|
2840
|
+
currentParaLines.push({ tick: currentLineBlocks[0].tick, blocks: currentLineBlocks });
|
|
2841
|
+
currentLineBlocks = [];
|
|
2842
|
+
}
|
|
2843
|
+
if (currentParaLines.length > 0) {
|
|
2844
|
+
if (isNewParagraphMarker || isTimeGapTrigger) {
|
|
2845
|
+
while (currentParaLines.length > 4) {
|
|
2846
|
+
const linesToPush = currentParaLines.splice(0, 4);
|
|
2847
|
+
paragraphs.push({ tick: linesToPush[0].tick, lines: linesToPush });
|
|
2848
|
+
}
|
|
2849
|
+
if (currentParaLines.length > 0) {
|
|
2850
|
+
paragraphs.push({ tick: currentParaLines[0].tick, lines: currentParaLines });
|
|
2851
|
+
currentParaLines = [];
|
|
2852
|
+
}
|
|
2853
|
+
} else if (currentParaLines.length >= 6) {
|
|
2854
|
+
const linesToPush = currentParaLines.splice(0, 4);
|
|
2855
|
+
paragraphs.push({ tick: linesToPush[0].tick, lines: linesToPush });
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
if (text.length > 0) {
|
|
2860
|
+
currentLineBlocks.push({ text, tick: event.tick });
|
|
2861
|
+
lastBlockTick = event.tick;
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
if (currentLineBlocks.length > 0) {
|
|
2865
|
+
currentParaLines.push({
|
|
2866
|
+
tick: currentLineBlocks[0].tick,
|
|
2867
|
+
blocks: currentLineBlocks
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
while (currentParaLines.length > 4) {
|
|
2871
|
+
const linesToPush = currentParaLines.splice(0, 4);
|
|
2872
|
+
paragraphs.push({ tick: linesToPush[0].tick, lines: linesToPush });
|
|
2873
|
+
}
|
|
2874
|
+
if (currentParaLines.length > 0) {
|
|
2875
|
+
paragraphs.push({ tick: currentParaLines[0].tick, lines: currentParaLines });
|
|
2876
|
+
}
|
|
2877
|
+
paragraphs = paragraphs.filter((p) => {
|
|
2878
|
+
return !(p.lines.length == 1 && p.lines[0].blocks.length == 1 && ["intro", "outro", "sfx", "solo", "chorus", "verse", "bridge", "break", "end"].includes(p.lines[0].blocks[0].text.toLowerCase().trim()));
|
|
2879
|
+
});
|
|
2880
|
+
if (paragraphs.length <= 2) paragraphs = [];
|
|
2881
|
+
structure.paragraphs = paragraphs;
|
|
2882
|
+
this.#lyrics = structure;
|
|
2883
|
+
return structure;
|
|
2884
|
+
}
|
|
2885
|
+
async #generateKaraokeFrames() {
|
|
2886
|
+
const lyrics = await this.#extractLyrics();
|
|
2887
|
+
if (!lyrics.paragraphs.length) {
|
|
2888
|
+
this.#haveLyrics = false;
|
|
2889
|
+
this.events[_MidiAudioPlayer.KARAOKE_CHANNEL].push({
|
|
2890
|
+
text: '<span class="karaoke-intro"></span>',
|
|
2891
|
+
name: "Karaoke Event",
|
|
2892
|
+
type: "intro",
|
|
2893
|
+
tick: 0,
|
|
2894
|
+
channel: _MidiAudioPlayer.KARAOKE_CHANNEL
|
|
2895
|
+
});
|
|
2896
|
+
this.events[_MidiAudioPlayer.KARAOKE_CHANNEL] = this.events[_MidiAudioPlayer.KARAOKE_CHANNEL].sort((a, b) => a.tick - b.tick);
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
this.#haveLyrics = true;
|
|
2900
|
+
this.#title = lyrics.title;
|
|
2901
|
+
let lastFrameEnd = 0;
|
|
2902
|
+
const delayTicks = this.secondsToTicks(this.#opts.karaokeDelay);
|
|
2903
|
+
const threeSecondsInTicks = this.secondsToTicks(3);
|
|
2904
|
+
const fiveSecondsInTicks = this.secondsToTicks(5);
|
|
2905
|
+
const sevenSecondsInTicks = this.secondsToTicks(7);
|
|
2906
|
+
const tenSecondsInTicks = this.secondsToTicks(10);
|
|
2907
|
+
const allBlocksInSong = [];
|
|
2908
|
+
lyrics.paragraphs.forEach((p, pIdx) => {
|
|
2909
|
+
p.lines.forEach((l, lIdx) => {
|
|
2910
|
+
l.blocks.forEach((b) => {
|
|
2911
|
+
allBlocksInSong.push({
|
|
2912
|
+
block: b,
|
|
2913
|
+
lineIdx: lIdx,
|
|
2914
|
+
paraIdx: pIdx,
|
|
2915
|
+
paragraph: p,
|
|
2916
|
+
fastLinesText: p.lines.map((li) => li.blocks.map((bl) => bl.text).join(""))
|
|
2917
|
+
});
|
|
2918
|
+
});
|
|
2919
|
+
});
|
|
2920
|
+
});
|
|
2921
|
+
const paragraphDisplayTicks = [];
|
|
2922
|
+
lyrics.paragraphs.forEach((p, pIdx) => {
|
|
2923
|
+
let paragraphDisplayTick = this.getTickBeforeSeconds(p.tick, 5);
|
|
2924
|
+
if (paragraphDisplayTick < lastFrameEnd)
|
|
2925
|
+
paragraphDisplayTick = lastFrameEnd + (p.tick - lastFrameEnd) / 2;
|
|
2926
|
+
if (pIdx === 0 && paragraphDisplayTick < 20)
|
|
2927
|
+
paragraphDisplayTick = 20;
|
|
2928
|
+
paragraphDisplayTicks[pIdx] = paragraphDisplayTick;
|
|
2929
|
+
const fastLinesText = p.lines.map((li) => li.blocks.map((b) => b.text).join(""));
|
|
2930
|
+
const initialHTML = fastLinesText.map((lineText) => '<span class="karaoke-coming">'.concat(lineText, "</span>")).join("<br/>");
|
|
2931
|
+
this.events[_MidiAudioPlayer.KARAOKE_CHANNEL].push({
|
|
2932
|
+
text: initialHTML,
|
|
2933
|
+
name: "Karaoke Event",
|
|
2934
|
+
type: "lyric",
|
|
2935
|
+
tick: paragraphDisplayTick,
|
|
2936
|
+
channel: _MidiAudioPlayer.KARAOKE_CHANNEL
|
|
2937
|
+
});
|
|
2938
|
+
if (p.lines.length > 0) {
|
|
2939
|
+
const lastLine = p.lines[p.lines.length - 1];
|
|
2940
|
+
if (lastLine.blocks.length > 0)
|
|
2941
|
+
lastFrameEnd = lastLine.blocks[lastLine.blocks.length - 1].tick;
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
const firstParaDisplayTick = paragraphDisplayTicks[0] || 0;
|
|
2945
|
+
if (firstParaDisplayTick > 25) {
|
|
2946
|
+
this.events[_MidiAudioPlayer.KARAOKE_CHANNEL].push({
|
|
2947
|
+
text: '<span class="karaoke-clear"></span>',
|
|
2948
|
+
name: "Karaoke Event",
|
|
2949
|
+
type: "clear",
|
|
2950
|
+
tick: 5,
|
|
2951
|
+
channel: _MidiAudioPlayer.KARAOKE_CHANNEL
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
allBlocksInSong.forEach((current, index2) => {
|
|
2955
|
+
const currentBlock = current.block;
|
|
2956
|
+
const currentLineIdx = current.lineIdx;
|
|
2957
|
+
const currentParaIdx = current.paraIdx;
|
|
2958
|
+
const p = current.paragraph;
|
|
2959
|
+
const fastLinesText = current.fastLinesText;
|
|
2960
|
+
const generateHTML = (forceAllPlayedOnActiveLine = false) => {
|
|
2961
|
+
return p.lines.map((li, liIdx) => {
|
|
2962
|
+
if (liIdx < currentLineIdx)
|
|
2963
|
+
return '<span class="karaoke-played">'.concat(fastLinesText[liIdx], "</span>");
|
|
2964
|
+
if (liIdx > currentLineIdx)
|
|
2965
|
+
return '<span class="karaoke-coming">'.concat(fastLinesText[liIdx], "</span>");
|
|
2966
|
+
let lineHTML = "";
|
|
2967
|
+
li.blocks.forEach((block) => {
|
|
2968
|
+
let className = "coming";
|
|
2969
|
+
if (forceAllPlayedOnActiveLine || block.tick < currentBlock.tick)
|
|
2970
|
+
className = "played";
|
|
2971
|
+
else if (block.tick === currentBlock.tick)
|
|
2972
|
+
className = "playing";
|
|
2973
|
+
lineHTML += '<span class="karaoke-'.concat(className, '">').concat(block.text, "</span>");
|
|
2974
|
+
});
|
|
2975
|
+
return lineHTML;
|
|
2976
|
+
}).join("<br>");
|
|
2977
|
+
};
|
|
2978
|
+
this.events[_MidiAudioPlayer.KARAOKE_CHANNEL].push({
|
|
2979
|
+
text: generateHTML(false),
|
|
2980
|
+
name: "Karaoke Event",
|
|
2981
|
+
type: "lyric",
|
|
2982
|
+
tick: currentBlock.tick - delayTicks,
|
|
2983
|
+
channel: _MidiAudioPlayer.KARAOKE_CHANNEL
|
|
2984
|
+
});
|
|
2985
|
+
const next = allBlocksInSong[index2 + 1];
|
|
2986
|
+
if (next) {
|
|
2987
|
+
const tickDifference = next.block.tick - currentBlock.tick;
|
|
2988
|
+
if (tickDifference > threeSecondsInTicks) {
|
|
2989
|
+
let targetCleanupTick = currentBlock.tick + threeSecondsInTicks;
|
|
2990
|
+
let targetClearTick = currentBlock.tick + sevenSecondsInTicks;
|
|
2991
|
+
let shouldAddClear = tickDifference > tenSecondsInTicks && currentParaIdx > 0;
|
|
2992
|
+
if (next.paraIdx !== currentParaIdx) {
|
|
2993
|
+
const nextParaDisplayTick = paragraphDisplayTicks[next.paraIdx];
|
|
2994
|
+
if (targetCleanupTick >= nextParaDisplayTick)
|
|
2995
|
+
targetCleanupTick = nextParaDisplayTick - 1;
|
|
2996
|
+
if (shouldAddClear) {
|
|
2997
|
+
if (targetClearTick >= nextParaDisplayTick || nextParaDisplayTick - targetClearTick < threeSecondsInTicks)
|
|
2998
|
+
shouldAddClear = false;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
if (targetCleanupTick > currentBlock.tick) {
|
|
3002
|
+
this.events[_MidiAudioPlayer.KARAOKE_CHANNEL].push({
|
|
3003
|
+
text: generateHTML(true),
|
|
3004
|
+
name: "Karaoke Event",
|
|
3005
|
+
type: "lyric",
|
|
3006
|
+
tick: targetCleanupTick - delayTicks,
|
|
3007
|
+
channel: _MidiAudioPlayer.KARAOKE_CHANNEL
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
if (shouldAddClear && targetClearTick > targetCleanupTick) {
|
|
3011
|
+
this.events[_MidiAudioPlayer.KARAOKE_CHANNEL].push({
|
|
3012
|
+
text: '<span class="karaoke-clear"></span>',
|
|
3013
|
+
name: "Karaoke Event",
|
|
3014
|
+
type: "clear",
|
|
3015
|
+
tick: targetClearTick - delayTicks,
|
|
3016
|
+
channel: _MidiAudioPlayer.KARAOKE_CHANNEL
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
lastFrameEnd = currentBlock.tick;
|
|
3022
|
+
});
|
|
3023
|
+
if (this.totalTicks - lastFrameEnd > this.secondsToTicks(5)) {
|
|
3024
|
+
this.events[_MidiAudioPlayer.KARAOKE_CHANNEL].push({
|
|
3025
|
+
text: '<span class="karaoke-clear"></span>',
|
|
3026
|
+
name: "Karaoke Event",
|
|
3027
|
+
type: "clear",
|
|
3028
|
+
tick: lastFrameEnd + this.secondsToTicks(5),
|
|
3029
|
+
channel: _MidiAudioPlayer.KARAOKE_CHANNEL
|
|
3030
|
+
});
|
|
3031
|
+
} else {
|
|
3032
|
+
this.events[_MidiAudioPlayer.KARAOKE_CHANNEL].push({
|
|
3033
|
+
text: '<span class="karaoke-clear"></span>',
|
|
3034
|
+
name: "Karaoke Event",
|
|
3035
|
+
type: "clear",
|
|
3036
|
+
tick: this.totalTicks - 1,
|
|
3037
|
+
channel: _MidiAudioPlayer.KARAOKE_CHANNEL
|
|
3038
|
+
});
|
|
3039
|
+
}
|
|
3040
|
+
this.events[_MidiAudioPlayer.KARAOKE_CHANNEL] = this.events[_MidiAudioPlayer.KARAOKE_CHANNEL].sort((a, b) => a.tick - b.tick);
|
|
3041
|
+
}
|
|
3042
|
+
async #detectKaraokeVocalChannel() {
|
|
3043
|
+
const lyrics = await this.#extractLyrics();
|
|
3044
|
+
if (!lyrics?.paragraphs?.length) return null;
|
|
3045
|
+
const textTicks = lyrics.paragraphs.flatMap((p) => p.lines.flatMap((l) => l.blocks.map((b) => b.tick)));
|
|
3046
|
+
if (textTicks.length === 0) return null;
|
|
3047
|
+
const tickTolerance = this.division ? this.division / 2 : 48;
|
|
3048
|
+
const VOCAL_MIN = 48;
|
|
3049
|
+
const VOCAL_MAX = 84;
|
|
3050
|
+
const channelsToScan = Object.keys(this.#channels).map(Number).filter((chan) => chan !== 10);
|
|
3051
|
+
let bestChannel = null;
|
|
3052
|
+
let bestScore = -Infinity;
|
|
3053
|
+
for (const channel of channelsToScan) {
|
|
3054
|
+
const notes = this.events.flatMap(
|
|
3055
|
+
(track) => track.filter(
|
|
3056
|
+
(e) => e.name === "Note on" && e.velocity > 0 && e.channel === channel
|
|
3057
|
+
)
|
|
3058
|
+
);
|
|
3059
|
+
if (notes.length === 0) continue;
|
|
3060
|
+
const aligned = textTicks.filter(
|
|
3061
|
+
(t) => notes.some((n) => Math.abs(n.tick - t) <= tickTolerance)
|
|
3062
|
+
).length;
|
|
3063
|
+
const alignmentScore = aligned / textTicks.length;
|
|
3064
|
+
const notesInRange = notes.filter((n) => n.noteNumber >= VOCAL_MIN && n.noteNumber <= VOCAL_MAX);
|
|
3065
|
+
const rangeScore = notesInRange.length / notes.length;
|
|
3066
|
+
if (rangeScore < 0.3) continue;
|
|
3067
|
+
const sorted = [...notes].sort((a, b) => a.tick - b.tick);
|
|
3068
|
+
const minGap = this.division / 8 || 6;
|
|
3069
|
+
const poly = sorted.filter((n, i) => i > 0 && Math.abs(n.tick - sorted[i - 1].tick) < minGap).length;
|
|
3070
|
+
const monophonyScore = 1 - poly / Math.max(notes.length - 1, 1);
|
|
3071
|
+
const densityRatio = notes.length / Math.max(textTicks.length, 1);
|
|
3072
|
+
const densityScore = densityRatio < 0.3 ? densityRatio / 0.3 : densityRatio > 5 ? Math.max(0, 1 - (densityRatio - 5) / 10) : 1;
|
|
3073
|
+
const score = alignmentScore * 0.45 + rangeScore * 0.35 + monophonyScore * 0.15 + densityScore * 0.05;
|
|
3074
|
+
if (score > bestScore) {
|
|
3075
|
+
bestScore = score;
|
|
3076
|
+
bestChannel = channel;
|
|
3077
|
+
}
|
|
1599
3078
|
}
|
|
3079
|
+
return bestScore >= 0.4 ? bestChannel : null;
|
|
3080
|
+
}
|
|
3081
|
+
#decodeKaraokeString(str) {
|
|
3082
|
+
if (!str) return "";
|
|
3083
|
+
const bytes = new Uint8Array(str.length);
|
|
3084
|
+
for (let i = 0; i < str.length; i++) bytes[i] = str.charCodeAt(i) & 255;
|
|
3085
|
+
const decoder = new TextDecoder("windows-1252");
|
|
3086
|
+
let decoded = decoder.decode(bytes);
|
|
3087
|
+
decoded = decoded.replace(/ÿ/g, "");
|
|
3088
|
+
decoded = decoded.replace(/’/g, "'");
|
|
3089
|
+
decoded = decoded.replace(/`/g, "'");
|
|
3090
|
+
return decoded;
|
|
3091
|
+
}
|
|
3092
|
+
#sendKaraokeFrame(type = "clear", text = "") {
|
|
3093
|
+
const html = '<span class="karaoke-'.concat(type, '">').concat(text.replace(/\s\/\s/g, "<br>"), "</span>");
|
|
3094
|
+
if (this.#opts.karaoke) {
|
|
3095
|
+
if (type == "title") queueMicrotask(() => this.triggerPlayerEvent("karaoke", { type, title: text, html }));
|
|
3096
|
+
else queueMicrotask(() => this.triggerPlayerEvent("karaoke", { type, html }));
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
#log(str, err = false) {
|
|
3100
|
+
queueMicrotask(() => this.triggerPlayerEvent("logs", str));
|
|
1600
3101
|
}
|
|
1601
3102
|
};
|
|
1602
3103
|
|
|
1603
3104
|
// index.js
|
|
1604
3105
|
if (typeof window !== "undefined") window.MidiAudioPlayer = MidiAudioPlayer;
|
|
1605
|
-
var
|
|
3106
|
+
var index_default2 = MidiAudioPlayer;
|
|
1606
3107
|
})();
|
|
1607
|
-
//# sourceMappingURL=midi-audio-player.js.map
|