spessasynth_core
Version:
MIDI and SoundFont2/DLS library with no compromises
1,774 lines (1,748 loc) • 640 kB
JavaScript
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// src/utils/indexed_array.ts
var IndexedByteArray = class extends Uint8Array {
/**
* The current index of the array.
*/
currentIndex = 0;
/**
* Returns a section of an array.
* @param start The beginning of the specified portion of the array.
* @param end The end of the specified portion of the array. This is exclusive of the element at the index 'end'.
*/
slice(start, end) {
const a = super.slice(start, end);
a.currentIndex = 0;
return a;
}
};
// src/utils/byte_functions/string.ts
function readBinaryString(dataArray, bytes = dataArray.length, offset = 0) {
let string = "";
for (let i = 0; i < bytes; i++) {
const byte = dataArray[offset + i];
if (byte === 0) {
return string;
}
string += String.fromCharCode(byte);
}
return string;
}
function readBinaryStringIndexed(dataArray, bytes) {
const startIndex = dataArray.currentIndex;
dataArray.currentIndex += bytes;
return readBinaryString(dataArray, bytes, startIndex);
}
function getStringBytes(string, addZero = false, ensureEven = false) {
let len = string.length;
if (addZero) {
len++;
}
if (ensureEven && len % 2 !== 0) {
len++;
}
const arr = new IndexedByteArray(len);
writeBinaryStringIndexed(arr, string);
return arr;
}
function writeBinaryStringIndexed(outArray, string, padLength = 0) {
if (padLength > 0) {
if (string.length > padLength) {
string = string.slice(0, padLength);
}
}
for (let i = 0; i < string.length; i++) {
outArray[outArray.currentIndex++] = string.charCodeAt(i);
}
if (padLength > string.length) {
for (let i = 0; i < padLength - string.length; i++) {
outArray[outArray.currentIndex++] = 0;
}
}
return outArray;
}
// src/utils/byte_functions/little_endian.ts
function readLittleEndianIndexed(dataArray, bytesAmount) {
const res = readLittleEndian(
dataArray,
bytesAmount,
dataArray.currentIndex
);
dataArray.currentIndex += bytesAmount;
return res;
}
function readLittleEndian(dataArray, bytesAmount, offset = 0) {
let out = 0;
for (let i = 0; i < bytesAmount; i++) {
out |= dataArray[offset + i] << i * 8;
}
return out >>> 0;
}
function writeLittleEndianIndexed(dataArray, number, byteTarget) {
for (let i = 0; i < byteTarget; i++) {
dataArray[dataArray.currentIndex++] = number >> i * 8 & 255;
}
}
function writeWord(dataArray, word) {
dataArray[dataArray.currentIndex++] = word & 255;
dataArray[dataArray.currentIndex++] = word >> 8;
}
function writeDword(dataArray, dword) {
writeLittleEndianIndexed(dataArray, dword, 4);
}
function signedInt16(byte1, byte2) {
const val = byte2 << 8 | byte1;
if (val > 32767) {
return val - 65536;
}
return val;
}
function signedInt8(byte) {
if (byte > 127) {
return byte - 256;
}
return byte;
}
// src/utils/riff_chunk.ts
var RIFFChunk = class {
/**
* The chunks FourCC code.
*/
header;
/**
* Chunk's size, in bytes.
*/
size;
/**
* Chunk's binary data. Note that this will have a length of 0 if "readData" was set to false.
*/
data;
/**
* Creates a new RIFF chunk.
*/
constructor(header, size, data) {
this.header = header;
this.size = size;
this.data = data;
}
};
function readRIFFChunk(dataArray, readData = true, forceShift = false) {
const header = readBinaryStringIndexed(dataArray, 4);
let size = readLittleEndianIndexed(dataArray, 4);
if (header === "") {
size = 0;
}
let chunkData;
if (readData) {
chunkData = dataArray.slice(
dataArray.currentIndex,
dataArray.currentIndex + size
);
} else {
chunkData = new IndexedByteArray(0);
}
if (readData || forceShift) {
dataArray.currentIndex += size;
if (size % 2 !== 0) {
dataArray.currentIndex++;
}
}
return new RIFFChunk(header, size, chunkData);
}
function writeRIFFChunkRaw(header, data, addZeroByte = false, isList = false) {
if (header.length !== 4) {
throw new Error(`Invalid header length: ${header}`);
}
let dataStartOffset = 8;
let headerWritten = header;
let dataLength = data.length;
if (addZeroByte) {
dataLength++;
}
let writtenSize = dataLength;
if (isList) {
dataStartOffset += 4;
writtenSize += 4;
headerWritten = "LIST";
}
let finalSize = dataStartOffset + dataLength;
if (finalSize % 2 !== 0) {
finalSize++;
}
const outArray = new IndexedByteArray(finalSize);
writeBinaryStringIndexed(outArray, headerWritten);
writeDword(outArray, writtenSize);
if (isList) {
writeBinaryStringIndexed(outArray, header);
}
outArray.set(data, dataStartOffset);
return outArray;
}
function writeRIFFChunkParts(header, chunks, isList = false) {
let dataOffset = 8;
let headerWritten = header;
const dataLength = chunks.reduce((len, c) => c.length + len, 0);
let writtenSize = dataLength;
if (isList) {
dataOffset += 4;
writtenSize += 4;
headerWritten = "LIST";
}
let finalSize = dataOffset + dataLength;
if (finalSize % 2 !== 0) {
finalSize++;
}
const outArray = new IndexedByteArray(finalSize);
writeBinaryStringIndexed(outArray, headerWritten);
writeDword(outArray, writtenSize);
if (isList) {
writeBinaryStringIndexed(outArray, header);
}
chunks.forEach((c) => {
outArray.set(c, dataOffset);
dataOffset += c.length;
});
return outArray;
}
function findRIFFListType(collection, type) {
return collection.find((c) => {
if (c.header !== "LIST") {
return false;
}
c.data.currentIndex = 4;
return readBinaryString(c.data, 4) === type;
});
}
// src/utils/fill_with_defaults.ts
function fillWithDefaults(obj, defObj) {
return {
...defObj,
...obj ?? {}
};
}
// src/utils/write_wav.ts
function audioToWav(audioData, sampleRate, options = DEFAULT_WAV_WRITE_OPTIONS) {
const length = audioData[0].length;
const numChannels = audioData.length;
const bytesPerSample = 2;
const fullOptions = fillWithDefaults(options, DEFAULT_WAV_WRITE_OPTIONS);
const loop = fullOptions.loop;
const metadata = fullOptions.metadata;
let infoChunk = new IndexedByteArray(0);
const infoOn = Object.keys(metadata).length > 0;
if (infoOn) {
const encoder = new TextEncoder();
const infoChunks = [
writeRIFFChunkRaw(
"ICMT",
encoder.encode("Created with SpessaSynth"),
true
)
];
if (metadata.artist) {
infoChunks.push(
writeRIFFChunkRaw("IART", encoder.encode(metadata.artist), true)
);
}
if (metadata.album) {
infoChunks.push(
writeRIFFChunkRaw("IPRD", encoder.encode(metadata.album), true)
);
}
if (metadata.genre) {
infoChunks.push(
writeRIFFChunkRaw("IGNR", encoder.encode(metadata.genre), true)
);
}
if (metadata.title) {
infoChunks.push(
writeRIFFChunkRaw("INAM", encoder.encode(metadata.title), true)
);
}
infoChunk = writeRIFFChunkParts("INFO", infoChunks, true);
}
let cueChunk = new IndexedByteArray(0);
const cueOn = loop?.end !== void 0 && loop?.start !== void 0;
if (cueOn) {
const loopStartSamples = Math.floor(loop.start * sampleRate);
const loopEndSamples = Math.floor(loop.end * sampleRate);
const cueStart = new IndexedByteArray(24);
writeLittleEndianIndexed(cueStart, 0, 4);
writeLittleEndianIndexed(cueStart, 0, 4);
writeBinaryStringIndexed(cueStart, "data");
writeLittleEndianIndexed(cueStart, 0, 4);
writeLittleEndianIndexed(cueStart, 0, 4);
writeLittleEndianIndexed(cueStart, loopStartSamples, 4);
const cueEnd = new IndexedByteArray(24);
writeLittleEndianIndexed(cueEnd, 1, 4);
writeLittleEndianIndexed(cueEnd, 0, 4);
writeBinaryStringIndexed(cueEnd, "data");
writeLittleEndianIndexed(cueEnd, 0, 4);
writeLittleEndianIndexed(cueEnd, 0, 4);
writeLittleEndianIndexed(cueEnd, loopEndSamples, 4);
cueChunk = writeRIFFChunkParts("cue ", [
new IndexedByteArray([2, 0, 0, 0]),
// Cue points count
cueStart,
cueEnd
]);
}
const headerSize = 44;
const dataSize = length * numChannels * bytesPerSample;
const fileSize = headerSize + dataSize + infoChunk.length + cueChunk.length - 8;
const header = new Uint8Array(headerSize);
header.set([82, 73, 70, 70], 0);
header.set(
new Uint8Array([
fileSize & 255,
fileSize >> 8 & 255,
fileSize >> 16 & 255,
fileSize >> 24 & 255
]),
4
);
header.set([87, 65, 86, 69], 8);
header.set([102, 109, 116, 32], 12);
header.set([16, 0, 0, 0], 16);
header.set([1, 0], 20);
header.set([numChannels & 255, numChannels >> 8], 22);
header.set(
new Uint8Array([
sampleRate & 255,
sampleRate >> 8 & 255,
sampleRate >> 16 & 255,
sampleRate >> 24 & 255
]),
24
);
const byteRate = sampleRate * numChannels * bytesPerSample;
header.set(
new Uint8Array([
byteRate & 255,
byteRate >> 8 & 255,
byteRate >> 16 & 255,
byteRate >> 24 & 255
]),
28
);
header.set([numChannels * bytesPerSample, 0], 32);
header.set([16, 0], 34);
header.set([100, 97, 116, 97], 36);
header.set(
new Uint8Array([
dataSize & 255,
dataSize >> 8 & 255,
dataSize >> 16 & 255,
dataSize >> 24 & 255
]),
40
);
const wavData = new Uint8Array(fileSize + 8);
let offset = headerSize;
wavData.set(header, 0);
let multiplier = 32767;
if (fullOptions.normalizeAudio) {
const numSamples = audioData[0].length;
let maxAbsValue = 0;
for (let ch = 0; ch < numChannels; ch++) {
const data = audioData[ch];
for (let i = 0; i < numSamples; i++) {
const sample = Math.abs(data[i]);
if (sample > maxAbsValue) {
maxAbsValue = sample;
}
}
}
multiplier = maxAbsValue > 0 ? 32767 / maxAbsValue : 1;
}
for (let i = 0; i < length; i++) {
audioData.forEach((d) => {
const sample = Math.min(32767, Math.max(-32768, d[i] * multiplier));
wavData[offset++] = sample & 255;
wavData[offset++] = sample >> 8 & 255;
});
}
if (infoOn) {
wavData.set(infoChunk, offset);
offset += infoChunk.length;
}
if (cueOn) {
wavData.set(cueChunk, offset);
}
return wavData.buffer;
}
// src/utils/byte_functions/big_endian.ts
function readBigEndian(dataArray, bytesAmount, offset = 0) {
let out = 0;
for (let i = 0; i < bytesAmount; i++) {
out = out << 8 | dataArray[offset + i];
}
return out >>> 0;
}
function readBigEndianIndexed(dataArray, bytesAmount) {
const res = readBigEndian(dataArray, bytesAmount, dataArray.currentIndex);
dataArray.currentIndex += bytesAmount;
return res;
}
function writeBigEndian(number, bytesAmount) {
const bytes = new Array(bytesAmount).fill(0);
for (let i = bytesAmount - 1; i >= 0; i--) {
bytes[i] = number & 255;
number >>= 8;
}
return bytes;
}
// src/utils/byte_functions/variable_length_quantity.ts
function readVariableLengthQuantity(MIDIbyteArray) {
let out = 0;
while (MIDIbyteArray) {
const byte = MIDIbyteArray[MIDIbyteArray.currentIndex++];
out = out << 7 | byte & 127;
if (byte >> 7 !== 1) {
break;
}
}
return out;
}
function writeVariableLengthQuantity(number) {
const bytes = [number & 127];
number >>= 7;
while (number > 0) {
bytes.unshift(number & 127 | 128);
number >>= 7;
}
return bytes;
}
// src/utils/other.ts
function formatTime(totalSeconds) {
totalSeconds = Math.floor(totalSeconds);
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.round(totalSeconds - minutes * 60);
return {
minutes,
seconds,
time: `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`
};
}
function arrayToHexString(arr) {
let hexString = "";
for (const i of arr) {
const hex = i.toString(16).padStart(2, "0").toUpperCase();
hexString += hex;
hexString += " ";
}
return hexString;
}
var consoleColors = {
warn: "color: orange;",
unrecognized: "color: red;",
info: "color: aqua;",
recognized: "color: lime",
value: "color: yellow; background-color: black;"
};
// src/externals/fflate/fflate.min.js
var tr;
(() => {
var l = Uint8Array, T = Uint16Array, ur = Int32Array, W = new l([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0]), X = new l([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0]), wr = new l([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]), Y = function(r, a) {
for (var e = new T(31), f = 0; f < 31; ++f) e[f] = a += 1 << r[f - 1];
for (var v = new ur(e[30]), f = 1; f < 30; ++f) for (var g = e[f]; g < e[f + 1]; ++g) v[g] = g - e[f] << 5 | f;
return { b: e, r: v };
}, Z = Y(W, 2), $ = Z.b, cr = Z.r;
$[28] = 258, cr[258] = 28;
var j = Y(X, 0), hr = j.b, Fr = j.r, _ = new T(32768);
for (i = 0; i < 32768; ++i) c = (i & 43690) >> 1 | (i & 21845) << 1, c = (c & 52428) >> 2 | (c & 13107) << 2, c = (c & 61680) >> 4 | (c & 3855) << 4, _[i] = ((c & 65280) >> 8 | (c & 255) << 8) >> 1;
var c, i, A = function(r, a, e) {
for (var f = r.length, v = 0, g = new T(a); v < f; ++v) r[v] && ++g[r[v] - 1];
var k = new T(a);
for (v = 1; v < a; ++v) k[v] = k[v - 1] + g[v - 1] << 1;
var b;
if (e) {
b = new T(1 << a);
var m = 15 - a;
for (v = 0; v < f; ++v) if (r[v]) for (var U = v << 4 | r[v], x = a - r[v], n = k[r[v] - 1]++ << x, o = n | (1 << x) - 1; n <= o; ++n) b[_[n] >> m] = U;
} else for (b = new T(f), v = 0; v < f; ++v) r[v] && (b[v] = _[k[r[v] - 1]++] >> 15 - r[v]);
return b;
}, M = new l(288);
for (i = 0; i < 144; ++i) M[i] = 8;
var i;
for (i = 144; i < 256; ++i) M[i] = 9;
var i;
for (i = 256; i < 280; ++i) M[i] = 7;
var i;
for (i = 280; i < 288; ++i) M[i] = 8;
var i, L = new l(32);
for (i = 0; i < 32; ++i) L[i] = 5;
var i, gr = A(M, 9, 1), br = A(L, 5, 1), q = function(r) {
for (var a = r[0], e = 1; e < r.length; ++e) r[e] > a && (a = r[e]);
return a;
}, u = function(r, a, e) {
var f = a / 8 | 0;
return (r[f] | r[f + 1] << 8) >> (a & 7) & e;
}, C = function(r, a) {
var e = a / 8 | 0;
return (r[e] | r[e + 1] << 8 | r[e + 2] << 16) >> (a & 7);
}, kr = function(r) {
return (r + 7) / 8 | 0;
}, xr = function(r, a, e) {
return (a == null || a < 0) && (a = 0), (e == null || e > r.length) && (e = r.length), new l(r.subarray(a, e));
}, yr = ["unexpected EOF", "invalid block type", "invalid length/literal", "invalid distance", "stream finished", "no stream handler", , "no callback", "invalid UTF-8 data", "extra field too long", "date not in range 1980-2099", "filename too long", "stream finishing", "invalid zip data"], h = function(r, a, e) {
var f = new Error(a || yr[r]);
if (f.code = r, Error.captureStackTrace && Error.captureStackTrace(f, h), !e) throw f;
return f;
}, Sr = function(r, a, e, f) {
var v = r.length, g = f ? f.length : 0;
if (!v || a.f && !a.l) return e || new l(0);
var k = !e, b = k || a.i != 2, m = a.i;
k && (e = new l(v * 3));
var U = function(fr) {
var or = e.length;
if (fr > or) {
var lr = new l(Math.max(or * 2, fr));
lr.set(e), e = lr;
}
}, x = a.f || 0, n = a.p || 0, o = a.b || 0, S = a.l, I = a.d, z = a.m, D = a.n, G = v * 8;
do {
if (!S) {
x = u(r, n, 1);
var H = u(r, n + 1, 3);
if (n += 3, H) if (H == 1) S = gr, I = br, z = 9, D = 5;
else if (H == 2) {
var N = u(r, n, 31) + 257, s = u(r, n + 10, 15) + 4, d = N + u(r, n + 5, 31) + 1;
n += 14;
for (var F = new l(d), P = new l(19), t = 0; t < s; ++t) P[wr[t]] = u(r, n + t * 3, 7);
n += s * 3;
for (var rr = q(P), Ar = (1 << rr) - 1, Mr = A(P, rr, 1), t = 0; t < d; ) {
var ar = Mr[u(r, n, Ar)];
n += ar & 15;
var w = ar >> 4;
if (w < 16) F[t++] = w;
else {
var E = 0, O = 0;
for (w == 16 ? (O = 3 + u(r, n, 3), n += 2, E = F[t - 1]) : w == 17 ? (O = 3 + u(r, n, 7), n += 3) : w == 18 && (O = 11 + u(r, n, 127), n += 7); O--; ) F[t++] = E;
}
}
var er = F.subarray(0, N), y = F.subarray(N);
z = q(er), D = q(y), S = A(er, z, 1), I = A(y, D, 1);
} else h(1);
else {
var w = kr(n) + 4, J = r[w - 4] | r[w - 3] << 8, K = w + J;
if (K > v) {
m && h(0);
break;
}
b && U(o + J), e.set(r.subarray(w, K), o), a.b = o += J, a.p = n = K * 8, a.f = x;
continue;
}
if (n > G) {
m && h(0);
break;
}
}
b && U(o + 131072);
for (var Ur = (1 << z) - 1, zr = (1 << D) - 1, Q = n; ; Q = n) {
var E = S[C(r, n) & Ur], p = E >> 4;
if (n += E & 15, n > G) {
m && h(0);
break;
}
if (E || h(2), p < 256) e[o++] = p;
else if (p == 256) {
Q = n, S = null;
break;
} else {
var nr = p - 254;
if (p > 264) {
var t = p - 257, B = W[t];
nr = u(r, n, (1 << B) - 1) + $[t], n += B;
}
var R = I[C(r, n) & zr], V = R >> 4;
R || h(3), n += R & 15;
var y = hr[V];
if (V > 3) {
var B = X[V];
y += C(r, n) & (1 << B) - 1, n += B;
}
if (n > G) {
m && h(0);
break;
}
b && U(o + 131072);
var vr = o + nr;
if (o < y) {
var ir = g - y, Dr = Math.min(y, vr);
for (ir + o < 0 && h(3); o < Dr; ++o) e[o] = f[ir + o];
}
for (; o < vr; ++o) e[o] = e[o - y];
}
}
a.l = S, a.p = Q, a.b = o, a.f = x, S && (x = 1, a.m = z, a.d = I, a.n = D);
} while (!x);
return o != e.length && k ? xr(e, 0, o) : e.subarray(0, o);
}, Tr = new l(0);
function mr(r, a) {
return Sr(r, { i: 2 }, a && a.out, a && a.dictionary);
}
var Er = typeof TextDecoder < "u" && new TextDecoder(), pr = 0;
try {
Er.decode(Tr, { stream: true }), pr = 1;
} catch {
}
tr = mr;
})();
// src/externals/fflate/fflate_wrapper.ts
var inf = tr;
// src/utils/loggin.ts
var ENABLE_INFO = false;
var ENABLE_WARN = true;
var ENABLE_GROUP = false;
function SpessaSynthLogging(enableInfo, enableWarn, enableGroup) {
ENABLE_INFO = enableInfo;
ENABLE_WARN = enableWarn;
ENABLE_GROUP = enableGroup;
}
function SpessaSynthInfo(...message) {
if (ENABLE_INFO) {
console.info(...message);
}
}
function SpessaSynthWarn(...message) {
if (ENABLE_WARN) {
console.warn(...message);
}
}
function SpessaSynthGroup(...message) {
if (ENABLE_GROUP) {
console.group(...message);
}
}
function SpessaSynthGroupCollapsed(...message) {
if (ENABLE_GROUP) {
console.groupCollapsed(...message);
}
}
function SpessaSynthGroupEnd() {
if (ENABLE_GROUP) {
console.groupEnd();
}
}
// src/utils/exports.ts
var SpessaSynthCoreUtils = {
consoleColors,
SpessaSynthInfo,
SpessaSynthWarn,
SpessaSynthGroupCollapsed,
// noinspection JSUnusedGlobalSymbols
SpessaSynthGroup,
SpessaSynthGroupEnd,
// noinspection JSUnusedGlobalSymbols
readBytesAsUintBigEndian: readBigEndian,
readLittleEndian: readLittleEndianIndexed,
readBytesAsString: readBinaryStringIndexed,
// noinspection JSUnusedGlobalSymbols
readVariableLengthQuantity,
inflateSync: inf
};
var DEFAULT_WAV_WRITE_OPTIONS = {
normalizeAudio: true,
loop: void 0,
metadata: {}
};
// src/midi/midi_message.ts
var MIDIMessage = class {
/**
* Absolute number of MIDI ticks from the start of the track.
*/
ticks;
/**
* The MIDI message status byte. Note that for meta events, it is the second byte. (not 0xFF)
*/
statusByte;
/**
* Message's binary data
*/
data;
/**
* Creates a new MIDI message
* @param ticks time of this message in absolute MIDI ticks
* @param byte the message status byte
* @param data the message's binary data
*/
constructor(ticks, byte, data) {
this.ticks = ticks;
this.statusByte = byte;
this.data = data;
}
};
function getChannel(statusByte) {
const eventType = statusByte & 240;
const channel = statusByte & 15;
let resultChannel = channel;
switch (eventType) {
// Midi (and meta and sysex headers)
case 128:
case 144:
case 160:
case 176:
case 192:
case 208:
case 224:
break;
case 240:
switch (channel) {
case 0:
resultChannel = -3;
break;
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
case 10:
case 11:
case 12:
case 13:
case 14:
resultChannel = -1;
break;
case 15:
resultChannel = -2;
break;
}
break;
default:
resultChannel = -1;
}
return resultChannel;
}
function getEvent(statusByte) {
const status = statusByte & 240;
const channel = statusByte & 15;
let eventChannel = -1;
let eventStatus = statusByte;
if (status >= 128 && status <= 224) {
eventChannel = channel;
eventStatus = status;
}
return {
status: eventStatus,
channel: eventChannel
};
}
var dataBytesAmount = {
8: 2,
// Note off
9: 2,
// Note on
10: 2,
// Note at
11: 2,
// Cc change
12: 1,
// Pg change
13: 1,
// Channel after touch
14: 2
// Pitch wheel
};
// src/midi/enums.ts
var midiMessageTypes = {
noteOff: 128,
noteOn: 144,
polyPressure: 160,
controllerChange: 176,
programChange: 192,
channelPressure: 208,
pitchWheel: 224,
systemExclusive: 240,
timecode: 241,
songPosition: 242,
songSelect: 243,
tuneRequest: 246,
clock: 248,
start: 250,
continue: 251,
stop: 252,
activeSensing: 254,
reset: 255,
sequenceNumber: 0,
text: 1,
copyright: 2,
trackName: 3,
instrumentName: 4,
lyric: 5,
marker: 6,
cuePoint: 7,
programName: 8,
midiChannelPrefix: 32,
midiPort: 33,
endOfTrack: 47,
setTempo: 81,
smpteOffset: 84,
timeSignature: 88,
keySignature: 89,
sequenceSpecific: 127
};
var midiControllers = {
bankSelect: 0,
modulationWheel: 1,
breathController: 2,
undefinedCC3: 3,
footController: 4,
portamentoTime: 5,
dataEntryMSB: 6,
mainVolume: 7,
balance: 8,
undefinedCC9: 9,
pan: 10,
expressionController: 11,
effectControl1: 12,
effectControl2: 13,
undefinedCC14: 14,
undefinedCC15: 15,
generalPurposeController1: 16,
generalPurposeController2: 17,
generalPurposeController3: 18,
generalPurposeController4: 19,
undefinedCC20: 20,
undefinedCC21: 21,
undefinedCC22: 22,
undefinedCC23: 23,
undefinedCC24: 24,
undefinedCC25: 25,
undefinedCC26: 26,
undefinedCC27: 27,
undefinedCC28: 28,
undefinedCC29: 29,
undefinedCC30: 30,
undefinedCC31: 31,
bankSelectLSB: 32,
modulationWheelLSB: 33,
breathControllerLSB: 34,
undefinedCC3LSB: 35,
footControllerLSB: 36,
portamentoTimeLSB: 37,
dataEntryLSB: 38,
mainVolumeLSB: 39,
balanceLSB: 40,
undefinedCC9LSB: 41,
panLSB: 42,
expressionControllerLSB: 43,
effectControl1LSB: 44,
effectControl2LSB: 45,
undefinedCC14LSB: 46,
undefinedCC15LSB: 47,
undefinedCC16LSB: 48,
undefinedCC17LSB: 49,
undefinedCC18LSB: 50,
undefinedCC19LSB: 51,
undefinedCC20LSB: 52,
undefinedCC21LSB: 53,
undefinedCC22LSB: 54,
undefinedCC23LSB: 55,
undefinedCC24LSB: 56,
undefinedCC25LSB: 57,
undefinedCC26LSB: 58,
undefinedCC27LSB: 59,
undefinedCC28LSB: 60,
undefinedCC29LSB: 61,
undefinedCC30LSB: 62,
undefinedCC31LSB: 63,
sustainPedal: 64,
portamentoOnOff: 65,
sostenutoPedal: 66,
softPedal: 67,
legatoFootswitch: 68,
hold2Pedal: 69,
soundVariation: 70,
filterResonance: 71,
releaseTime: 72,
attackTime: 73,
brightness: 74,
decayTime: 75,
vibratoRate: 76,
vibratoDepth: 77,
vibratoDelay: 78,
soundController10: 79,
generalPurposeController5: 80,
generalPurposeController6: 81,
generalPurposeController7: 82,
generalPurposeController8: 83,
portamentoControl: 84,
undefinedCC85: 85,
undefinedCC86: 86,
undefinedCC87: 87,
undefinedCC88: 88,
undefinedCC89: 89,
undefinedCC90: 90,
reverbDepth: 91,
tremoloDepth: 92,
chorusDepth: 93,
detuneDepth: 94,
phaserDepth: 95,
dataIncrement: 96,
dataDecrement: 97,
nonRegisteredParameterLSB: 98,
nonRegisteredParameterMSB: 99,
registeredParameterLSB: 100,
registeredParameterMSB: 101,
undefinedCC102LSB: 102,
undefinedCC103LSB: 103,
undefinedCC104LSB: 104,
undefinedCC105LSB: 105,
undefinedCC106LSB: 106,
undefinedCC107LSB: 107,
undefinedCC108LSB: 108,
undefinedCC109LSB: 109,
undefinedCC110LSB: 110,
undefinedCC111LSB: 111,
undefinedCC112LSB: 112,
undefinedCC113LSB: 113,
undefinedCC114LSB: 114,
undefinedCC115LSB: 115,
undefinedCC116LSB: 116,
undefinedCC117LSB: 117,
undefinedCC118LSB: 118,
undefinedCC119LSB: 119,
allSoundOff: 120,
resetAllControllers: 121,
localControlOnOff: 122,
allNotesOff: 123,
omniModeOff: 124,
omniModeOn: 125,
monoModeOn: 126,
polyModeOn: 127
};
// src/midi/midi_tools/midi_writer.ts
function writeMIDIInternal(midi) {
if (!midi.tracks) {
throw new Error("MIDI has no tracks!");
}
const binaryTrackData = [];
for (const track of midi.tracks) {
const binaryTrack = [];
let currentTick = 0;
let runningByte = void 0;
for (const event of track.events) {
const deltaTicks = Math.max(0, event.ticks - currentTick);
if (event.statusByte === midiMessageTypes.endOfTrack) {
currentTick += deltaTicks;
continue;
}
let messageData;
if (event.statusByte <= midiMessageTypes.sequenceSpecific) {
messageData = [
255,
event.statusByte,
...writeVariableLengthQuantity(event.data.length),
...event.data
];
runningByte = void 0;
} else if (event.statusByte === midiMessageTypes.systemExclusive) {
messageData = [
240,
...writeVariableLengthQuantity(event.data.length),
...event.data
];
runningByte = void 0;
} else {
messageData = [];
if (runningByte !== event.statusByte) {
runningByte = event.statusByte;
messageData.push(event.statusByte);
}
messageData.push(...event.data);
}
binaryTrack.push(...writeVariableLengthQuantity(deltaTicks));
binaryTrack.push(...messageData);
currentTick += deltaTicks;
}
binaryTrack.push(0);
binaryTrack.push(255);
binaryTrack.push(midiMessageTypes.endOfTrack);
binaryTrack.push(0);
binaryTrackData.push(new Uint8Array(binaryTrack));
}
const writeText = (text, arr) => {
for (let i = 0; i < text.length; i++) {
arr.push(text.charCodeAt(i));
}
};
const binaryData = [];
writeText("MThd", binaryData);
binaryData.push(...writeBigEndian(6, 4));
binaryData.push(0, midi.format);
binaryData.push(...writeBigEndian(midi.tracks.length, 2));
binaryData.push(...writeBigEndian(midi.timeDivision, 2));
for (const track of binaryTrackData) {
writeText("MTrk", binaryData);
binaryData.push(...writeBigEndian(track.length, 4));
binaryData.push(...track);
}
return new Uint8Array(binaryData).buffer;
}
// src/synthesizer/audio_engine/engine_components/synth_constants.ts
var VOICE_CAP = 350;
var DEFAULT_PERCUSSION = 9;
var MIDI_CHANNEL_COUNT = 16;
var DEFAULT_SYNTH_MODE = "gs";
var ALL_CHANNELS_OR_DIFFERENT_ACTION = -1;
var EMBEDDED_SOUND_BANK_ID = `SPESSASYNTH_EMBEDDED_BANK_${Math.random()}_DO_NOT_DELETE`;
var GENERATOR_OVERRIDE_NO_CHANGE_VALUE = 32767;
var DEFAULT_SYNTH_METHOD_OPTIONS = {
time: 0
};
var MIN_NOTE_LENGTH = 0.03;
var MIN_EXCLUSIVE_LENGTH = 0.07;
var SYNTHESIZER_GAIN = 1;
// src/utils/midi_hacks.ts
var XG_SFX_VOICE = 64;
var GM2_DEFAULT_BANK = 121;
var BankSelectHacks = class {
/**
* GM2 has a different default bank number
*/
static getDefaultBank(sys) {
return sys === "gm2" ? GM2_DEFAULT_BANK : 0;
}
static getDrumBank(sys) {
switch (sys) {
default:
throw new Error(`${sys} doesn't have a bank MSB for drums.`);
case "gm2":
return 120;
case "xg":
return 127;
}
}
/**
* Checks if this bank number is XG drums.
*/
static isXGDrums(bankMSB) {
return bankMSB === 120 || bankMSB === 127;
}
/**
* Checks if this MSB is a valid XG MSB
*/
static isValidXGMSB(bankMSB) {
return this.isXGDrums(bankMSB) || bankMSB === XG_SFX_VOICE || bankMSB === GM2_DEFAULT_BANK;
}
static isSystemXG(system) {
return system === "gm2" || system === "xg";
}
static addBankOffset(bankMSB, bankOffset, xgDrums = true) {
if (this.isXGDrums(bankMSB) && xgDrums) {
return bankMSB;
}
return Math.min(bankMSB + bankOffset, 127);
}
static subtrackBankOffset(bankMSB, bankOffset, xgDrums = true) {
if (this.isXGDrums(bankMSB) && xgDrums) {
return bankMSB;
}
return Math.max(0, bankMSB - bankOffset);
}
};
// src/utils/sysex_detector.ts
function isXGOn(e) {
return e.data[0] === 67 && // Yamaha
e.data[2] === 76 && // XG ON
e.data[5] === 126 && e.data[6] === 0;
}
function isGSDrumsOn(e) {
return e.data[0] === 65 && // Roland
e.data[2] === 66 && // GS
e.data[3] === 18 && // GS
e.data[4] === 64 && // System parameter
(e.data[5] & 16) !== 0 && // Part parameter
e.data[6] === 21;
}
function isGSOn(e) {
return e.data[0] === 65 && // Roland
e.data[2] === 66 && // GS
e.data[6] === 127;
}
function isGMOn(e) {
return e.data[0] === 126 && // Non realtime
e.data[2] === 9 && // Gm system
e.data[3] === 1;
}
function isGM2On(e) {
return e.data[0] === 126 && // Non realtime
e.data[2] === 9 && // Gm system
e.data[3] === 3;
}
// src/midi/midi_tools/get_gs_on.ts
function getGsOn(ticks) {
return new MIDIMessage(
ticks,
midiMessageTypes.systemExclusive,
new IndexedByteArray([
65,
// Roland
16,
// Device ID (defaults to 16 on roland)
66,
// GS
18,
// Command ID (DT1) (whatever that means...)
64,
// System parameter - Address
0,
// Global parameter - Address
127,
// GS Change - Address
0,
// Turn on - Data
65,
// Checksum
247
// End of exclusive
])
);
}
// src/soundbank/basic_soundbank/midi_patch.ts
var MIDIPatchTools = class _MIDIPatchTools {
/**
* Converts a MIDI patch to a string.
*/
static toMIDIString(patch) {
if (patch.isGMGSDrum) {
return `DRUM:${patch.program}`;
}
return `${patch.bankLSB}:${patch.bankMSB}:${patch.program}`;
}
// noinspection JSUnusedGlobalSymbols
/**
* Gets a MIDI patch from a string.
* @param string
*/
static fromMIDIString(string) {
const parts = string.split(":");
if (parts.length > 3 || parts.length < 2) {
throw new Error("Invalid MIDI string:");
}
if (string.startsWith("DRUM")) {
return {
bankMSB: 0,
bankLSB: 0,
program: parseInt(parts[1]),
isGMGSDrum: true
};
} else {
return {
bankLSB: parseInt(parts[0]),
bankMSB: parseInt(parts[1]),
program: parseInt(parts[2]),
isGMGSDrum: false
};
}
}
/**
* Converts a named MIDI patch to string.
* @param patch
*/
static toNamedMIDIString(patch) {
return `${_MIDIPatchTools.toMIDIString(patch)} ${patch.name}`;
}
/**
* Checks if two MIDI patches match.
* @param patch1
* @param patch2
*/
static matches(patch1, patch2) {
if (patch1.isGMGSDrum || patch2.isGMGSDrum) {
return patch1.isGMGSDrum === patch2.isGMGSDrum && patch1.program === patch2.program;
}
return patch1.program === patch2.program && patch1.bankLSB === patch2.bankLSB && patch1.bankMSB === patch2.bankMSB;
}
// noinspection JSUnusedGlobalSymbols
/**
* Gets a named MIDI patch from a string.
* @param string
*/
static fromNamedMIDIString(string) {
const firstSpace = string.indexOf(" ");
if (firstSpace < 0) {
throw new Error(`Invalid named MIDI string: ${string}`);
}
const patch = this.fromMIDIString(string.substring(0, firstSpace));
const name = string.substring(firstSpace + 1);
return {
...patch,
name
};
}
static sorter(a, b) {
if (a.program !== b.program) {
return a.program - b.program;
}
if (a.isGMGSDrum && !b.isGMGSDrum) return 1;
if (!a.isGMGSDrum && b.isGMGSDrum) return -1;
if (a.bankMSB !== b.bankMSB) {
return a.bankMSB - b.bankMSB;
}
return a.bankLSB - b.bankLSB;
}
};
// src/midi/midi_tools/rmidi_writer.ts
var DEFAULT_COPYRIGHT = "Created using SpessaSynth";
function correctBankOffsetInternal(mid, bankOffset, soundBank) {
let system = "gm";
const unwantedSystems = [];
const ports = Array(mid.tracks.length).fill(0);
const channelsAmount = 16 + Math.max(...mid.portChannelOffsetMap);
const channelsInfo = [];
for (let i = 0; i < channelsAmount; i++) {
channelsInfo.push({
program: 0,
drums: i % 16 === DEFAULT_PERCUSSION,
// Drums appear on 9 every 16 channels,
lastBank: void 0,
lastBankLSB: void 0,
hasBankSelect: false
});
}
mid.iterate((e, trackNum) => {
const portOffset = mid.portChannelOffsetMap[ports[trackNum]];
if (e.statusByte === midiMessageTypes.midiPort) {
ports[trackNum] = e.data[0];
return;
}
const status = e.statusByte & 240;
if (status !== midiMessageTypes.controllerChange && status !== midiMessageTypes.programChange && status !== midiMessageTypes.systemExclusive) {
return;
}
if (status === midiMessageTypes.systemExclusive) {
if (!isGSDrumsOn(e)) {
if (isXGOn(e)) {
system = "xg";
} else if (isGSOn(e)) {
system = "gs";
} else if (isGMOn(e)) {
system = "gm";
unwantedSystems.push({
tNum: trackNum,
e
});
} else if (isGM2On(e)) {
system = "gm2";
}
return;
}
const sysexChannel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][e.data[5] & 15] + portOffset;
channelsInfo[sysexChannel].drums = !!(e.data[7] > 0 && e.data[5] >> 4);
return;
}
const chNum = (e.statusByte & 15) + portOffset;
const channel = channelsInfo[chNum];
if (status === midiMessageTypes.programChange) {
const sentProgram = e.data[0];
const patch = {
program: sentProgram,
bankLSB: channel.lastBankLSB?.data?.[1] ?? 0,
// Make sure to take bank offset into account
bankMSB: BankSelectHacks.subtrackBankOffset(
channel.lastBank?.data?.[1] ?? 0,
mid.bankOffset
),
isGMGSDrum: channel.drums
};
const targetPreset = soundBank.getPreset(patch, system);
SpessaSynthInfo(
`%cInput patch: %c${MIDIPatchTools.toMIDIString(patch)}%c. Channel %c${chNum}%c. Changing patch to ${targetPreset.toString()}.`,
consoleColors.info,
consoleColors.unrecognized,
consoleColors.info,
consoleColors.recognized,
consoleColors.info
);
e.data[0] = targetPreset.program;
if (targetPreset.isGMGSDrum && BankSelectHacks.isSystemXG(system)) {
return;
}
if (channel.lastBank === void 0) {
return;
}
channel.lastBank.data[1] = BankSelectHacks.addBankOffset(
targetPreset.bankMSB,
bankOffset,
targetPreset.isXGDrums
);
if (channel.lastBankLSB === void 0) {
return;
}
channel.lastBankLSB.data[1] = targetPreset.bankLSB;
return;
}
const isLSB = e.data[0] === midiControllers.bankSelectLSB;
if (e.data[0] !== midiControllers.bankSelect && !isLSB) {
return;
}
channel.hasBankSelect = true;
if (isLSB) {
channel.lastBankLSB = e;
} else {
channel.lastBank = e;
}
});
channelsInfo.forEach((has, ch) => {
if (has.hasBankSelect) {
return;
}
const midiChannel = ch % 16;
const status = midiMessageTypes.programChange | midiChannel;
const portOffset = Math.floor(ch / 16) * 16;
const port = mid.portChannelOffsetMap.indexOf(portOffset);
const track = mid.tracks.find(
(t) => t.port === port && t.channels.has(midiChannel)
);
if (track === void 0) {
return;
}
let indexToAdd = track.events.findIndex((e) => e.statusByte === status);
if (indexToAdd === -1) {
const programIndex = track.events.findIndex(
(e) => e.statusByte > 128 && e.statusByte < 240 && (e.statusByte & 15) === midiChannel
);
if (programIndex === -1) {
return;
}
const programTicks = track.events[programIndex].ticks;
const targetProgram = soundBank.getPreset(
{
bankMSB: 0,
bankLSB: 0,
program: 0,
isGMGSDrum: false
},
system
).program;
track.addEvent(
new MIDIMessage(
programTicks,
midiMessageTypes.programChange | midiChannel,
new IndexedByteArray([targetProgram])
),
programIndex
);
indexToAdd = programIndex;
}
SpessaSynthInfo(
`%cAdding bank select for %c${ch}`,
consoleColors.info,
consoleColors.recognized
);
const ticks = track.events[indexToAdd].ticks;
const targetPreset = soundBank.getPreset(
{
bankLSB: 0,
bankMSB: 0,
program: has.program,
isGMGSDrum: has.drums
},
system
);
const targetBank = BankSelectHacks.addBankOffset(
targetPreset.bankMSB,
bankOffset,
targetPreset.isXGDrums
);
track.addEvent(
new MIDIMessage(
ticks,
midiMessageTypes.controllerChange | midiChannel,
new IndexedByteArray([midiControllers.bankSelect, targetBank])
),
indexToAdd
);
});
if (system === "gm" && !BankSelectHacks.isSystemXG(system)) {
for (const m of unwantedSystems) {
const track = mid.tracks[m.tNum];
track.deleteEvent(track.events.indexOf(m.e));
}
let index = 0;
if (mid.tracks[0].events[0].statusByte === midiMessageTypes.trackName) {
index++;
}
mid.tracks[0].addEvent(getGsOn(0), index);
}
}
var DEFAULT_RMIDI_WRITE_OPTIONS = {
bankOffset: 0,
metadata: {},
correctBankOffset: true,
soundBank: void 0
};
function writeRMIDIInternal(mid, soundBankBinary, options) {
const metadata = options.metadata;
SpessaSynthGroup("%cWriting the RMIDI File...", consoleColors.info);
SpessaSynthInfo("metadata", metadata);
SpessaSynthInfo("Initial bank offset", mid.bankOffset);
if (options.correctBankOffset) {
if (!options.soundBank) {
throw new Error(
"Sound bank must be provided if correcting bank offset."
);
}
correctBankOffsetInternal(mid, options.bankOffset, options.soundBank);
}
const newMid = new IndexedByteArray(mid.writeMIDI());
metadata.name ??= mid.getName();
metadata.creationDate ??= /* @__PURE__ */ new Date();
metadata.copyright ??= DEFAULT_COPYRIGHT;
metadata.software ??= "SpessaSynth";
Object.entries(metadata).forEach(
(v) => {
const val = v;
if (val[1]) {
mid.setRMIDInfo(val[0], val[1]);
}
}
);
const infoContent = [];
Object.entries(mid.rmidiInfo).forEach((v) => {
const type = v[0];
const data = v[1];
const writeInfo = (type2) => {
infoContent.push(writeRIFFChunkRaw(type2, data));
};
switch (type) {
case "album":
writeInfo("IALB");
writeInfo("IPRD");
break;
case "software":
writeInfo("ISFT");
break;
case "infoEncoding":
writeInfo("IENC");
break;
case "creationDate":
writeInfo("ICRD");
break;
case "picture":
writeInfo("IPIC");
break;
case "name":
writeInfo("INAM");
break;
case "artist":
writeInfo("IART");
break;
case "genre":
writeInfo("IGNR");
break;
case "copyright":
writeInfo("ICOP");
break;
case "comment":
writeInfo("ICMT");
break;
case "engineer":
writeInfo("IENG");
break;
case "subject":
writeInfo("ISBJ");
break;
case "midiEncoding":
writeInfo("MENC");
break;
}
});
const DBNK = new IndexedByteArray(2);
writeLittleEndianIndexed(DBNK, options.bankOffset, 2);
infoContent.push(writeRIFFChunkRaw("DBNK", DBNK));
SpessaSynthInfo("%cFinished!", consoleColors.info);
SpessaSynthGroupEnd();
return writeRIFFChunkParts("RIFF", [
getStringBytes("RMID"),
writeRIFFChunkRaw("data", newMid),
writeRIFFChunkParts("INFO", infoContent, true),
new IndexedByteArray(soundBankBinary)
]).buffer;
}
// src/midi/midi_tools/used_keys_loaded.ts
function getUsedProgramsAndKeys(mid, soundBank) {
SpessaSynthGroupCollapsed(
"%cSearching for all used programs and keys...",
consoleColors.info
);
const channelsAmount = 16 + Math.max(...mid.portChannelOffsetMap);
const channelPresets = [];
let system = "gs";
for (let i = 0; i < channelsAmount; i++) {
const isDrum = i % 16 === DEFAULT_PERCUSSION;
channelPresets.push({
preset: soundBank.getPreset(
{
bankLSB: 0,
bankMSB: 0,
isGMGSDrum: isDrum,
program: 0
},
system
),
bankMSB: 0,
bankLSB: 0,
isDrum
});
}
const usedProgramsAndKeys = /* @__PURE__ */ new Map();
const ports = mid.tracks.map((t) => t.port);
mid.iterate((event, trackNum) => {
if (event.statusByte === midiMessageTypes.midiPort) {
ports[trackNum] = event.data[0];
return;
}
const status = event.statusByte & 240;
if (status !== midiMessageTypes.noteOn && status !== midiMessageTypes.controllerChange && status !== midiMessageTypes.programChange && status !== midiMessageTypes.systemExclusive) {
return;
}
const channel = (event.statusByte & 15) + mid.portChannelOffsetMap[ports[trackNum]] || 0;
let ch = channelPresets[channel];
switch (status) {
case midiMessageTypes.programChange:
ch.preset = soundBank.getPreset(
{
bankMSB: ch.bankMSB,
bankLSB: ch.bankLSB,
program: event.data[0],
isGMGSDrum: ch.isDrum
},
system
);
break;
case midiMessageTypes.controllerChange:
{
switch (event.data[0]) {
default:
return;
case midiControllers.bankSelectLSB:
ch.bankLSB = event.data[1];
break;
case midiControllers.bankSelect:
ch.bankMSB = event.data[1];
}
}
break;
case midiMessageTypes.noteOn:
if (event.data[1] === 0) {
return;
}
let combos = usedProgramsAndKeys.get(ch.preset);
if (!combos) {
combos = /* @__PURE__ */ new Set();
usedProgramsAndKeys.set(ch.preset, combos);
}
combos.add(`${event.data[0]}-${event.data[1]}`);
break;
case midiMessageTypes.systemExclusive:
{
if (!isGSDrumsOn(event)) {
if (isXGOn(event)) {
system = "xg";
SpessaSynthInfo(
"%cXG on detected!",
consoleColors.recognized
);
} else if (isGM2On(event)) {
system = "gm2";
SpessaSynthInfo(
"%cGM2 on detected!",
consoleColors.recognized
);
} else if (isGMOn(event)) {
system = "gm";
SpessaSynthInfo(
"%cGM on detected!",
consoleColors.recognized
);
} else if (isGSOn(event)) {
system = "gs";
SpessaSynthInfo(
"%cGS on detected!",
consoleColors.recognized
);
}
return;
}
const sysexChannel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][event.data[5] & 15] + mid.portChannelOffsetMap[ports[trackNum]];
const isDrum = !!(event.data[7] > 0 && event.data[5] >> 4);
ch = channelPresets[sysexChannel];
ch.isDrum = isDrum;
}
break;
}
});
usedProgramsAndKeys.forEach((combos, preset) => {
if (combos.size === 0) {
SpessaSynthInfo(
`%cDetected change but no keys for %c${preset.name}`,
consoleColors.info,
consoleColors.value
);
usedProgramsAndKeys.delete(preset);
}
});
SpessaSynthGroupEnd();
return usedProgramsAndKeys;
}
// src/midi/midi_tools/get_note_times.ts
function getNoteTimesInternal(midi, minDrumLength = 0) {
const getTempo = (event) => {
event.data = new IndexedByteArray(event.data.buffer);
return 6e7 / readBigEndian(event.data, 3);
};
const noteTimes = [];
const trackData = midi.tracks.map((t) => t.events);
const events = trackData.flat();
events.sort((e1, e2) => e1.ticks - e2.ticks);
for (let i = 0; i < 16; i++) {
noteTimes.push([]);
}
let elapsedTime = 0;
let oneTickToSeconds = 60 / (120 * midi.timeDivision);
let eventIndex = 0;
let unfinished = 0;
const unfinishedNotes = [];
for (let i = 0; i < 16; i++) {
unfinishedNotes.push([]);
}
const noteOff2 = (midiNote, channel) => {
const noteIndex = unfinishedNotes[channel].findIndex(
(n) => n.midiNote === midiNote
);
const note = unfinishedNotes[channel][noteIndex];
if (note) {
const time = elapsedTime - note.start;
note.length = time;
if (channel === DEFAULT_PERCUSSION) {
note.length = time < minDrumLength ? minDrumLength : time;
}
unfinishedNotes[channel].splice(noteIndex, 1);
}
unfinished--;
};
while (eventIndex < events.length) {
const event = events[eventIndex];
const status = event.statusByte >> 4;
const channel = event.statusByte & 15;
if (status === 8) {
noteOff2(event.data[0], channel);
} else if (status === 9) {
if (event.data[1] === 0) {
noteOff2(event.data[0], channel);
} else {
noteOff2(event.data[0], channel);
const noteTime = {
midiNote: event.data[0],
start: elapsedTime,
length: -1,
velocity: event.data[1] / 127
};
noteTimes[channel].push(noteTime);
unfinishedNotes[channel].push(noteTime);
unfinished++;
}
} else if (event.statusByte === 81) {
oneTickToSeconds = 60 / (getTempo(event) * midi.timeDivision);
}
if (++eventIndex >= events.length) {
break;
}
elapsedTime += oneTickToSeconds * (events[eventIndex].ticks - event.ticks);
}
if (unfinished > 0) {
unfinishedNotes.forEach((channelNotes, channel) => {
channelNotes.forEach((note) => {
const time = elapsedTime - note.start;
note.length = time;
if (channel === DEFAULT_PERCUSSION) {
note.length = time < minDrumLength ? minDrumLength : time;
}
});
});
}
return noteTimes;
}
// src/synthesizer/enums.ts
var interpolationTypes = {
linear: 0,
nearestNeighbor: 1,
hermite: 2
};
var dataEntryStates = {
Idle: 0,
RPCoarse: 1,
RPFine: 2,
NRPCoarse: 3,
NRPFine: 4,
DataCoarse: 5,
DataFine: 6
};
var customControllers = {
channelTuning: 0,
// Cents, RPN for fine tuning
channelTransposeFine: 1,
// Cents, only the decimal tuning, (e.g., transpose is 4.5,
// Then shift by 4 keys + tune by 50 cents)
modulationMultiplier: 2,
// Cents, set by modulation depth RPN
masterTuning: 3,
// Cents, set by system exclusive
channelTuningSemitones: 4,
// Semitones, for RPN coarse tuning
channelKeyShift: 5,
// Key shift: for system exclusive
sf2NPRNGeneratorLSB: 6
// Sf2 NPRN LSB for selecting a generator value
};
// src/midi/midi_tools/midi_editor.ts
function getControllerChange(channel, cc, value, ticks) {
return new MIDIMessage(
ticks,
midiMessageTypes.controllerChange | channel % 16,
new IndexedByteArray([cc, value])
);
}
function getDrumChange(channel, ticks) {
const chanAddress = 16 | [1, 2, 3, 4, 5, 6, 7, 8, 0, 9, 10, 11, 12, 13, 14, 15][channel % 16];
const sysexData = [
65,
// Roland
16,
// Device ID (defaults to 16 on roland)
66,
// GS
18,
// Command ID (DT1) (whatever that means...)
64,
// System parameter }
chanAddress,
// Channel parameter } Address
21,
// Drum change }
1
// Is Drums } Data
];
const sum = 64 + chanAddress + 21 + 1;
const checksum = 128 - sum % 128;
return new MIDIMessage(
ticks,
midiMessageTypes.systemExclusive,
new IndexedByteArray([...sysexData, checksum, 247])
);
}
function modifyMIDIInternal(midi, desiredProgramChanges = [], desiredControllerChanges = [], desiredChannelsToClear = [], desiredChannelsToTranspose = []) {
SpessaSynthGroupCollapsed(
"%cApplyi