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