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/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
- // src/webaudiofontplayer.js
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
- constructor(audioCtx, preset) {
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 baseDetune = zone.originalPitch - 100 * zone.coarseTune - zone.fineTune;
1234
- const playbackRate = Math.pow(2, (100 * pitch - baseDetune) / 1200);
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 newRate = Math.pow(2, (100 * (pitch + s.delta) - baseDetune) / 1200);
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.sample) {
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.#noZeroVolume(0), this.#audioCtx.currentTime);
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.setValueAtTime(this.#noZeroVolume(initialVol), when);
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.linearRampToValueAtTime(
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.linearRampToValueAtTime(
1442
+ envelope.gain.exponentialRampToValueAtTime(
1352
1443
  this.#noZeroVolume(volume * lastVolume),
1353
1444
  when + lastTime
1354
1445
  );
1355
1446
  }
1356
- envelope.gain.linearRampToValueAtTime(this.#noZeroVolume(0), when + duration + this.#afterTime);
1447
+ envelope.gain.exponentialRampToValueAtTime(this.#nearZero, when + duration + this.#afterTime);
1357
1448
  }
1358
- #findEnvelope() {
1359
- let envelope = this.#envelopes.find((e) => e.target === this.#audioCtx.destination && this.#audioCtx.currentTime > e.when + e.duration + 1e-3);
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 = this.#audioCtx.destination;
1373
- envelope.connect(this.#audioCtx.destination);
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/presets/defaultpreset.json
1404
- var defaultpreset_default = {
1405
- zones: [
1406
- {
1407
- midi: 81,
1408
- originalPitch: 4200,
1409
- keyRangeLow: 0,
1410
- keyRangeHigh: 43,
1411
- loopStart: 256,
1412
- loopEnd: 768,
1413
- coarseTune: 0,
1414
- fineTune: 0,
1415
- sampleRate: 48e3,
1416
- ahdsr: false,
1417
- file: "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAAGAACgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCg//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJALAAAAAAAAABgA5CeiPAAAAAAAAAAAAAAAAAAAA//vUxAAAB+gfQ1TwACTywCx/NYAJAABZOW7fg6F8L8A3AOwb5L2e7x5R5EoCADB8HwfPxACAYrB8H8oCDsHw/lAQdlwPqDHh/UCEp4fhiU8/ynv6BAGOZS1NzNUNm83W5HHJGz4Kk0TfzwzSZu8CgJ3TZ11pu3hi1hoxY8qMFcPAcBX40CM1RcwUo9EFQgiY2VPldy90c24GUwVQROZnA0ZXohQyMsyIBlAWEQK/zrsHfSBGmF4i+yiKKKNrP5FDrYJM+8XdeIRR1FaEwk0mLq5Upl0G35bBliJz8DxCcgSVtAYMpU27BVbW0tSXOzHsLU+88MTkCQ3PwO05aTrtqwF1G2YFHc7M1hamc5RYnJXbn6S9OU7L2apytcaEpk5bQVbXIszWrUzuzNatW5HKL05K+z9Jyip+vcyqG3yZTDELa1DcIazEIXV3jW1lV3jW1lV7h/M/7h/M/7h/9/+f/f/n/3/58qr2JTUtyqvYlNy3TXbFLct012xS3Kemu0lLWp6arSexu7G62Ja2Ja2Ja0QsKrWx13Wb36/a/7/ew5kMjLhQNcmAxqICUIjQsMqB4zeSzGokEALDBOxI2OrTTJJMLBUwcCQYAVMDdlQ0A6Zkis4q5jIAQzknMdGQMBLAltWVxZrJm4ma0hGaGCmrpKmfS87RrhMci3muq5p4+b4wKbPCqVlsBQHbcmCKhmxoZcGGmnZmJeZOEmiGS7nGjrWX5mn+j92HZJcMqKDIgYz0vMnJTHwUzYoMiIGdQNMuTIbj/U13dLc1aMqCjSTkzEvMpCTQjQy4qMkBjPTGR3X9pMI1N9q1aWtWpqtUyspMhBTOCwyYkMeAjNSsyUjMdATMCYyIgpZJZygGm+AqXGzWrWqtWzWrWjGgAy0lMhHwEXmVERjw4AigycfMdGwMVmSDhjQwCiSU1solVxiNamhqlpYZx3jllljvHLWWLlv/F3cfyKO+/8vfx/JY/7/y9/H8lj/v/Pv5Dk5D8bn4xGJyNxuf1llvHHWWW8cdZZbx//u0xLeAL2H/Z7nNkIFVkCR3sMAFx1llvHHWVbHGrllWAbmu1l2+wACIJmablgZJEkGiBRmRmRKSQMOakoAW1jywyxmXRDkBISlWnK1b7K0xdxcdLomjI+e1lat9latrlly7rLnrbVat8FFQgpgKeCmxTcgrgVVMQU1FMy45OS41VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
1418
- anchor: 533333e-8
1419
- },
1420
- {
1421
- midi: 81,
1422
- originalPitch: 5400,
1423
- keyRangeLow: 44,
1424
- keyRangeHigh: 55,
1425
- loopStart: 128,
1426
- loopEnd: 384,
1427
- coarseTune: 0,
1428
- fineTune: 0,
1429
- sampleRate: 48e3,
1430
- ahdsr: false,
1431
- file: "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAAEsADMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJATAAAAAAAAABLAMEvhQAAAAAAAAAAAAAAAAAAAA//vUxAAACSwNaVQwACRXSC6/NYAIAAa9txyS8qHAwMDPlz5QEIIROfLn1AhBBy3lz/BCt5c/wQz5c/RBCXPlw/EEEHLPlz9EEOt8QQQcs+XPykQVh8uH4gcIHLD6w+JgcGkGbGakSCqjColKJDEMCR4mYQ8kQZ1UAiwCAIbAc4b1maEch6LAEVzMhwxuYQGCgLBWbmQSPi+kAzkOM6i/Ei2XrGgFiMbisXednEOO7NP9KJdLKedbxd7vvezh3pl/YdmreFqxnZjEgf+NyN/I1TXaWzqnqWqTOzTyyihuX0cMUlNa3S81TYWrGd63q/RRuno4xSUUbt3YzSYU1fuH8z/uH8zp6OMUlFK6e/KKTG5zKZy+ax/+4fzP+4fzP70rp78opL1PXv0lSzVy+tjjVyyrY41e4fzf91/N/3X83/dfzf91/N/3X5/3D+b/uv5v+6/m/7r/3/6/9/+sst446yy3jVyyrY41csq2ONXLKtjjVyyrKiLXdbW5JI24SEB1CQjhFAAXAIxdxbUKO0ekXEuKphJ5DkSgYKJVFFxQVwUwFDcRcUFcFcCjsTYoK4K4FHYmxQXwrgUdibFBfCvCjsTcRXhXhT8TcRXhXhT8TcRXhXhT8TcRXhXhT8TcakxBTUUzLjk5LjWqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//tkxMWDzQgxS7zzACgAADSAAAAEqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
1432
- anchor: 266667e-8
1433
- },
1434
- {
1435
- midi: 81,
1436
- originalPitch: 6600,
1437
- keyRangeLow: 56,
1438
- keyRangeHigh: 67,
1439
- loopStart: 64,
1440
- loopEnd: 192,
1441
- coarseTune: 0,
1442
- fineTune: 0,
1443
- sampleRate: 48e3,
1444
- ahdsr: false,
1445
- file: "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAAE4ADs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJAXAAAAAAAAABODQyktQAAAAAAAAAAAAAAAAAAAA//vkxAAABywBgbQAACYVyC6/P9AwKAASUcckkt4YWD4fLn1AhBCJz5c+oEFghy5/ghy5+cghy7+GKZd/DHLn1HIYk5d/DHL+6H5d5TOdLnbLTLKrBqyq+vlkqeD4lCzTAngBgvMYACAAGg+NZZoFwogYAGAZCwCsDgBJsJhRJLyYvYUvg4BLFAA9AKpqYxQCumE3hMhEBIkBkBLuNR5oOGzUbs0Jk7jR1yTFgpzGY2TMgWV8qtT4fmaf4xDKgxlNgzTGRZScSFbPVQxarGZbWMLiNMUC1MshXMJx+MRSeaZHnuceOvbGrV2ls6MOQUMAAnMDAzMMgRMAQkMCQufiahT9zMJfim7ul5rIDBEFgJBgHgoHQuBANAwFA1WkUbqzssyns944flv09UjUcU9kjkkk9UjZfjOyzKep8Z2kxucyq9+ax9JFNJNJNZNFNFNVNJNJNbKRRvGQxjKejfJ2Udq/9bf1darY41U0U0U1U0k0k+k0U0U+U0k0k+k0e5/zD+5/zD+5/zX93/Nf3f81/d////////7/9f+//X/v/1/7/9Jop8ppJpJ9Jop6qwppJ7KxJGp6qwpHKrKxI2qqqwo3KrKxI2qqq9RuVWV8jaqqr1G5TEFNRTMuOTkuNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sUxNoDwAAB/hwAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
1446
- anchor: 133333e-8
1447
- },
1448
- {
1449
- midi: 81,
1450
- originalPitch: 7800,
1451
- keyRangeLow: 68,
1452
- keyRangeHigh: 79,
1453
- loopStart: 32,
1454
- loopEnd: 96,
1455
- coarseTune: 0,
1456
- fineTune: 0,
1457
- sampleRate: 48e3,
1458
- ahdsr: false,
1459
- file: "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADwADm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJAZAAAAAAAAAA8CsVDBMAAAAAAAAAAAAAAAAAAAA//vExAAABmQBjaAAACGQBa9497AdtbAkWdckklxRyjhQ5BBYYqOFHRAXDHKHOGOUOcMco7nOJHawxynnOU5cMcHOXOcHOl3NXGMqsiEAqoMI8QsSUcIsgeiQgJISFCg5ABAyw+VQaiK8yY18FNiuxBUorwU0FdiC4ivimgrsQXEV4KaCuxBcTfBXAv4isTfiuBf6KxN+L4r8RUU3wXoL+JsU3wXoL+JsV3gvRSpMQU1FMy45OS41qqqqqqqqqqqqqqqqqqqqqqqq6goZoAABEOiCXDs8P3G8oicJAhZjVVVBlsStNFVVUT8StNVMQU1FMy45OS41VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sUxOuDxEQnByYExigAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
1460
- anchor: 66667e-8
1461
- },
1462
- {
1463
- midi: 81,
1464
- originalPitch: 9e3,
1465
- keyRangeLow: 80,
1466
- keyRangeHigh: 91,
1467
- loopStart: 16,
1468
- loopEnd: 48,
1469
- coarseTune: 0,
1470
- fineTune: 0,
1471
- sampleRate: 48e3,
1472
- ahdsr: false,
1473
- file: "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADwADm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJAaAAAAAAAAAA8ChXkahAAAAAAAAAAAAAAAAAAAA//vExAAABkQDh+AAACGUhq9895iZZVlzMgFEYk0nBACCLQfD6gTB8/qBDBwMflC4f/gQMfqBMH/4OHP4Df+XBw5+UBN/6wJu1fapqZhlttyF2FyUYQ0W0QkFSJiRIro4TAYk8aQCAQCAKjiRIkSr+xQUFOBQUV/5BQU4KChv+CgoL4KCu/4QUN8KCn/8UFN8FFf/wgp/hQ3/8FBXYgoL/+FBX5BQ3/8FBfigopVMQU1FMy45OS41VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sUxNoDwAAB/gAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
1474
- anchor: 33333e-8
1475
- },
1476
- {
1477
- midi: 81,
1478
- originalPitch: 10200,
1479
- keyRangeLow: 92,
1480
- keyRangeHigh: 103,
1481
- loopStart: 8,
1482
- loopEnd: 56,
1483
- coarseTune: 0,
1484
- fineTune: 0,
1485
- sampleRate: 48e3,
1486
- ahdsr: false,
1487
- file: "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADwADm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJAaAAAAAAAAAA8AljJJeAAAAAAAAAAAAAAAAAAAA//vExAAABmgDh+AAACG7Cq649JmAh3unfGYwJRhSgQBBEXB8PjogBDn8EwfP6gQdicP/g4GOXB/lHfgmH905/UclwcOf/8oA/2r/qekIFM1D+AxK8OEgo9IhpLSJFdJaehzC3CbBom0GChJpEiRn/tRxIkSJT6OJJB3BUFXSwNB3BUFQVgqCoaqBoGsqd4iBo8oGgaTWCoaxEDR6VBUFTpUFQV//BoGoiBoO//rxKCv//iIGv/+WTEFNRTMuOTkuNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sUxNoDwAAB/gAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
1488
- anchor: 16667e-8
1489
- },
1490
- {
1491
- midi: 81,
1492
- originalPitch: 11400,
1493
- keyRangeLow: 104,
1494
- keyRangeHigh: 115,
1495
- loopStart: 12,
1496
- loopEnd: 52,
1497
- coarseTune: 0,
1498
- fineTune: 0,
1499
- sampleRate: 48e3,
1500
- ahdsr: false,
1501
- file: "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADwADm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJAaAAAAAAAAAA8CNr1v4AAAAAAAAAAAAAAAAAAAA//vExAAAB1SVheCEbeI4SC349ApomIy4jYiPtA6x5AACs4xjvkAAPmMY/wAXzGMf4AL/X+IgG4PvwQOfIQQOZcH//wQlwfBA5/1h/iB2sP/17/1GMvIAEJ8BiUYMEW0NSAKQjJWj5HCbB/EGHqQ5m2plUuYL1WvVqeGZmb/VVVf9mZr/1VVX9hYGwKgFgbB9TCwNgbB8BG6lKUreYxjfKUrfQxvylK3oYxjeUKAgICJYoCAgICUvoYxvylL9DGN9SlL9P+UrfMYxjOoYCAgIUb6lL////////qUpSzASTEFNRTMuOTkuNaqqqqpMQU1FMy45OS41qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//sUxNoDwAAB/gAAACAAADSAAAAEqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
1502
- anchor: 8333e-8
1503
- },
1504
- {
1505
- midi: 81,
1506
- originalPitch: 12600,
1507
- keyRangeLow: 116,
1508
- keyRangeHigh: 127,
1509
- loopStart: 6,
1510
- loopEnd: 42,
1511
- coarseTune: 0,
1512
- fineTune: 0,
1513
- sampleRate: 48e3,
1514
- ahdsr: false,
1515
- file: "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAAEIADo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJAaQAAAAAAAABCDWHfeqAAAAAAAAAAAAAAAAAAAA//vUxAAACUUrfeCE1eMLSCx88a54l3jMh4t/trUApEAAAUAGYxja+WMYx/yAAH+MY/+AYx/4AAX8xjH/7/8AF//zHyAMdoIAgAAADAYWTt4IA/Kc5/////////y4Pg/v3FZropQkAAAEiDVBBhkjdBSk5JyDlHqDmWi+kJRsJiOZ69eva6x+zAQEBCm/9VVVb/9mVVL/9mZmP/6qqqn/8ZlVV/6zMzM3/VCgICanXX+5I2JRJHaana+SSNwIQEQAwAQAQEw6j5KBCASACACAKAmBGHcdr5RNR2jtJRsed/DkTU1NTra/3Oc5znf+1rWuc7/3Na1ra/9znOc7/9rWtc7/9zWta3/4c5znO/9rTU1NXOvbW4lDtDyHkPI7TqQIQAxMQU1FMy45OS41qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpMQU1FMy45OS41qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//sUxNoDwAABpAAAACAAADSAAAAEqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
1516
- anchor: 4167e-8
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 MidiAudioPlayer = class extends index.Player {
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
- #audioPlayer = null;
1525
- #activeNotes = null;
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
- preset: defaultpreset_default,
1528
- volume: 0.012,
1529
- onEndFile: null
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((event) => this.#handleMidiPipeline(event));
1823
+ super();
1533
1824
  this.#opts = { ...this.#opts, ...opts };
1534
- this.#activeNotes = /* @__PURE__ */ new Map();
1825
+ this.#presetMapThread = this.#mapPresets();
1535
1826
  this.#audioCtx = new (window.AudioContext || window.webkitAudioContext)();
1536
- this.#audioPlayer = new WebAudioFontPlayer(this.#audioCtx, this.#opts.preset);
1537
- this.on("endOfFile", async () => {
1538
- await new Promise((resolve) => requestAnimationFrame(() => setTimeout(resolve, 1)));
1539
- await this.#endOfFile();
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 load(content) {
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.loadArrayBuffer(content);
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.#audioCtx.resume();
1550
- await super.play();
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.#audioPlayer.cancelQueue();
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
- await this.#audioPlayer.cancelQueue();
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 #endOfFile() {
1563
- if (typeof this.#opts.onEndFile == "function") await this.#opts.onEndFile();
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.#stopNotePipe(event.noteNumber);
1574
- const vol = event.velocity / 127 * this.#opts.volume;
1575
- const envelope = this.#audioPlayer.queueWaveTable(0, event.noteNumber, 2, vol);
1576
- this.#activeNotes.set(event.noteNumber, envelope);
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.#stopNotePipe(event.noteNumber);
2510
+ this.#stopNote(event.channel, event.noteNumber);
1579
2511
  }
1580
2512
  break;
1581
2513
  case "Note off":
1582
- this.#stopNotePipe(event.noteNumber);
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
- #stopNotePipe(noteNumber) {
1587
- const envelope = this.#activeNotes.get(noteNumber);
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.cancel();
1590
- this.#activeNotes.delete(noteNumber);
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
- if (this.#activeNotes) {
1595
- this.#activeNotes.forEach((envelope, note) => {
1596
- if (envelope && envelope.cancel) envelope.cancel();
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
- this.#activeNotes.clear();
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 index_default = MidiAudioPlayer;
3108
+ var index_default2 = MidiAudioPlayer;
1606
3109
  export {
1607
3110
  MidiAudioPlayer,
1608
- index_default as default
3111
+ index_default2 as default
1609
3112
  };
1610
3113
  //# sourceMappingURL=index.js.map