spessasynth_core
Version:
MIDI and SoundFont2/DLS library with no compromises
1,725 lines (1,724 loc) • 741 kB
JavaScript
//#region src/utils/byte_functions/big_endian.ts
/**
* Reads number as Big endian.
* @param dataArray the array to read from.
* @param bytesAmount the number of bytes to read.
* @param offset the offset to start reading from.
* @returns the number.
*/
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;
}
/**
* Reads number as Big endian from an IndexedByteArray.
* @param dataArray the array to read from.
* @param bytesAmount the number of bytes to read.
* @returns the number.
*/
function readBigEndianIndexed(dataArray, bytesAmount) {
const res = readBigEndian(dataArray, bytesAmount, dataArray.currentIndex);
dataArray.currentIndex += bytesAmount;
return res;
}
/**
* Writes a number as Big endian.
* @param number the number to write.
* @param bytesAmount the amount of bytes to use. Excess bytes will be set to zero.
* @returns the Big endian representation of the number.
*/
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;
}
//#endregion
//#region src/utils/byte_functions/little_endian.ts
/**
* Reads the number as little endian from an IndexedByteArray. Uses BigInt to go above 32 bytes.
* @param dataArray the array to read from.
* @param bytesAmount the number of bytes to read.
* @returns the number.
*/
function readLE64Indexed(dataArray, bytesAmount) {
const res = readLE64(dataArray, bytesAmount, dataArray.currentIndex);
dataArray.currentIndex += bytesAmount;
return res;
}
/**
* Reads the number as little endian. Uses BigInt to go above 32 bytes.
* @param dataArray the array to read from.
* @param bytesAmount the number of bytes to read.
* @param offset the offset to start reading at.
* @returns the number.
*/
function readLE64(dataArray, bytesAmount, offset = 0) {
let out = 0n;
for (let i = 0; i < bytesAmount; i++) out |= BigInt(dataArray[offset + i]) << BigInt(i * 8);
return Number(out);
}
/**
* Reads the number as little endian from an IndexedByteArray.
* @param dataArray the array to read from.
* @param bytesAmount the number of bytes to read.
* @returns the number.
*/
function readLittleEndianIndexed(dataArray, bytesAmount) {
const res = readLittleEndian(dataArray, bytesAmount, dataArray.currentIndex);
dataArray.currentIndex += bytesAmount;
return res;
}
/**
* Reads the number as little endian.
* @param dataArray the array to read from.
* @param bytesAmount the number of bytes to read.
* @param offset the offset to start reading at.
* @returns the number.
*/
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;
}
/**
* Writes a number as little endian seems to also work for negative numbers so yay?
* @param dataArray the IndexedByteArray to write to.
* @param number the number to write.
* @param byteTarget the amount of bytes to use. Excess bytes will be set to zero.
* @returns the Big endian representation of the number.
*/
function writeLittleEndianIndexed(dataArray, number, byteTarget) {
for (let i = 0; i < byteTarget; i++) dataArray[dataArray.currentIndex++] = number >> i * 8 & 255;
}
/**
* Writes a WORD (SHORT)
* 16 bits.
*/
function writeWord(dataArray, word) {
dataArray[dataArray.currentIndex++] = word & 255;
dataArray[dataArray.currentIndex++] = word >> 8;
}
/**
* Writes a DWORD (INT)
* 32 bits.
*/
function writeDword(dataArray, dword) {
writeLittleEndianIndexed(dataArray, dword, 4);
}
/**
* Writes a QWORD (LONG)
* 64 bits.
*/
function writeQword(dataArray, qword) {
const qb = BigInt(qword);
for (let i = 0n; i < 8n; i++) dataArray[dataArray.currentIndex++] = Number(qb >> i * 8n & 255n);
}
/**
* Reads two bytes as a signed short.
*/
function signedInt16(byte1, byte2) {
const val = byte2 << 8 | byte1;
if (val > 32767) return val - 65536;
return val;
}
/**
* Reads a byte as a signed char.
*/
function signedInt8(byte) {
if (byte > 127) return byte - 256;
return byte;
}
//#endregion
//#region src/utils/indexed_array.ts
/**
* Indexed_array.ts
* purpose: extends Uint8Array with a currentIndex property.
*/
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;
}
};
//#endregion
//#region src/utils/byte_functions/string.ts
/**
* Reads bytes as an ASCII string. This version works with any numeric array.
* @param dataArray the array to read from.
* @param bytes the amount of bytes to read.
* @param offset the offset in the array to start reading from.
* @returns the string.
*/
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;
}
/**
* Reads bytes as an ASCII string from an IndexedByteArray.
* @param dataArray the IndexedByteArray to read from.
* @param bytes the amount of bytes to read.
* @returns the string.
*/
function readBinaryStringIndexed(dataArray, bytes) {
const startIndex = dataArray.currentIndex;
dataArray.currentIndex += bytes;
return readBinaryString(dataArray, bytes, startIndex);
}
/**
* Gets ASCII bytes from string.
* @param string the string.
* @param addZero adds a zero terminator at the end.
* @param ensureEven ensures even byte count.
* @returns the binary data.
*/
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;
}
/**
* Writes ASCII bytes into a specified array.
* @param string the string.
* @param outArray the target array
* @param padLength pad with zeros if the string is shorter
* @returns modified _in-place_
*/
function writeBinaryStringIndexed(outArray, string, padLength = 0) {
if (padLength > 0 && 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;
}
//#endregion
//#region src/utils/byte_functions/variable_length_quantity.ts
/**
* Reads VLQ from a MIDI byte array.
* @param midiByteArray the array to read from.
* @returns the number.
*/
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;
}
/**
* Writes a VLQ from a number to a byte array.
* @param number the number to write.
* @returns the VLQ representation of the number.
*/
function writeVariableLengthQuantity(number) {
const bytes = [number & 127];
number >>= 7;
while (number > 0) {
bytes.unshift(number & 127 | 128);
number >>= 7;
}
return bytes;
}
//#endregion
//#region src/utils/other.ts
/**
* Other.ts
* purpose: contains some useful functions that don't belong in any specific category
*/
/**
* Formats the given seconds to nice readable time
* @param totalSeconds time in seconds
*/
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")}`
};
}
/**
* Does what it says
*/
function arrayToHexString(arr) {
let hexString = "";
for (let i = 0; i < arr.length; i++) {
const hex = arr[i].toString(16).padStart(2, "0").toUpperCase();
hexString += hex;
if (i < arr.length - 1) hexString += " ";
}
return hexString;
}
const ConsoleColors = {
warn: "color: orange;",
unrecognized: "color: red;",
info: "color: aqua;",
recognized: "color: lime",
value: "color: yellow; background-color: black;"
};
//#endregion
//#region src/externals/fflate/fflate.min.js
let 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;
j.r;
var _ = 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();
try {
Er.decode(Tr, { stream: !0 });
} catch {}
tr = mr;
})();
//#endregion
//#region src/externals/fflate/fflate_wrapper.ts
const inf = tr;
//#endregion
//#region src/utils/riff_chunk.ts
/**
* Riff_chunk.ts
* reads a riff chunk and stores it as a class
*/
var RIFFChunk = class RIFFChunk {
/**
* 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;
/**
* The size of the chunk's header in bytes.
* This varies for 32-bit and 64-bit RIFF chunks.
*/
headerSize;
/**
* Creates a new RIFF chunk.
*/
constructor(header, size, data, headerSize = 8) {
this.header = header;
this.size = size;
this.data = data;
this.headerSize = headerSize;
}
/**
* Reads a RIFF chunk from an array.
* @param dataArray the array to read from.
* @param rf64 if the chunk uses a 64-bit size.
* @param readData if the data should be read as well.
*/
static read(dataArray, rf64 = false, readData = true) {
const header = readBinaryStringIndexed(dataArray, 4);
let size = rf64 ? readLE64Indexed(dataArray, 8) : readLittleEndianIndexed(dataArray, 4);
if (header === "") size = 0;
const chunkData = readData ? dataArray.slice(dataArray.currentIndex, dataArray.currentIndex + size) : new IndexedByteArray(0);
if (readData) {
dataArray.currentIndex += size;
if (size % 2 !== 0) dataArray.currentIndex++;
}
return new RIFFChunk(header, size, chunkData, rf64 ? 12 : 8);
}
/**
* Writes a RIFF chunk correctly.
* @param header the fourCC code of the header.
* @param data the binary chunk data.
* @param isList if a "LIST" should be set as the chunk type and the actual type should be written at the start of the data.
* @param rf64 if the chunk uses a 64-bit size.
* @returns the binary data.
*/
static write(header, data, rf64 = false, isList = false) {
if (header.length !== 4) throw new Error(`Invalid header length: ${header}`);
let dataStartOffset = rf64 ? 12 : 8;
let headerWritten = header;
const dataLength = data.length;
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);
if (rf64) writeQword(outArray, writtenSize);
else writeDword(outArray, writtenSize);
if (isList) writeBinaryStringIndexed(outArray, header);
outArray.set(data, dataStartOffset);
return outArray;
}
/**
* "Writes" a RIFF chunk as a list of binary blobs,
* which can be appended to a list without using more memory,
* then finally allocated at the end with `writeParts`.
* This allows avoiding large array allocations and only one writeParts call at the end.
* @param header the fourCC code of the header.
* @param chunks binary chunk data parts, will be combined in order.
* @param isList if a "LIST" should be set as the chunk type and the actual type should be written at the start of the data.
* @param rf64 if the chunk uses a 64-bit size.
* @returns the chunk as binary blobs.
*/
static getParts(header, chunks, rf64 = false, isList = false) {
let headerWritten = header;
let totalSize = chunks.reduce((len, c) => c.length + len, 0);
if (isList) {
totalSize += 4;
headerWritten = "LIST";
}
let sizeBytes;
if (rf64) {
sizeBytes = new IndexedByteArray(8);
writeQword(sizeBytes, totalSize);
} else {
sizeBytes = new IndexedByteArray(4);
writeDword(sizeBytes, totalSize);
}
const parts = [getStringBytes(headerWritten), sizeBytes];
if (isList) parts.push(getStringBytes(header));
parts.push(...chunks);
if (totalSize % 2 !== 0) parts.push(new Uint8Array(1));
return parts;
}
/**
* Writes RIFF chunk given binary blobs.
* It merges them together into data and allocates one large array.
* @param header the fourCC code of the header.
* @param chunks binary chunk data parts, will be combined in order.
* @param isList if a "LIST" should be set as the chunk type and the actual type should be written at the start of the data.
* @param rf64 if the chunk uses a 64-bit size.
* @returns the binary data.
*/
static writeParts(header, chunks, rf64 = false, isList = false) {
let dataOffset = rf64 ? 12 : 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);
if (rf64) writeQword(outArray, writtenSize);
else writeDword(outArray, writtenSize);
if (isList) writeBinaryStringIndexed(outArray, header);
for (const c of chunks) {
outArray.set(c, dataOffset);
dataOffset += c.length;
}
return outArray;
}
/**
* Finds a given type in a list.
* @remarks
* Also skips the current index to after the list FourCC.
*/
static findListType(collection, type) {
return collection.find((c) => {
if (c.header !== "LIST") return false;
c.data.currentIndex = 4;
return readBinaryString(c.data, 4) === type;
});
}
};
//#endregion
//#region src/utils/fill_with_defaults.ts
/**
* Fills the object with default values.
* @param obj object to fill.
* @param defObj object to fill with.
*/
function fillWithDefaults(obj, defObj) {
return {
...defObj,
...obj
};
}
//#endregion
//#region src/utils/write_wav.ts
const DEFAULT_WAV_WRITE_OPTIONS = {
normalizeAudio: true,
loop: void 0,
metadata: {}
};
/**
* Writes an audio into a valid WAV file.
* @param audioData the audio data channels.
* @param sampleRate the sample rate, in Hertz.
* @param options Additional options for writing the file.
* @returns the binary file.
*/
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 = [RIFFChunk.writeParts("ICMT", [encoder.encode("Created with SpessaSynth"), [0]])];
if (metadata.artist) infoChunks.push(RIFFChunk.writeParts("IART", [encoder.encode(metadata.artist), [0]]));
if (metadata.album) infoChunks.push(RIFFChunk.writeParts("IPRD", [encoder.encode(metadata.album), [0]]));
if (metadata.genre) infoChunks.push(RIFFChunk.writeParts("IGNR", [encoder.encode(metadata.genre), [0]]));
if (metadata.title) infoChunks.push(RIFFChunk.writeParts("INAM", [encoder.encode(metadata.title), [0]]));
infoChunk = RIFFChunk.writeParts("INFO", infoChunks, false, 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 = RIFFChunk.writeParts("cue ", [
new IndexedByteArray([
2,
0,
0,
0
]),
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++) for (const d of audioData) {
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;
}
//#endregion
//#region src/utils/loggin.ts
/**
* Manage the log level of `spessasynth_core`.
*/
var SpessaLog = class SpessaLog {
/**
* The most verbose log level, prints out a lot of small details.
*/
static infoEnabled = false;
/**
* The default log level, prints out warnings for unexpected and erroneous behavior.
*/
static warnEnabled = true;
/**
* If grouping of the log messages is allowed. Recommended for the `info` verbosity level.
*/
static groupEnabled = false;
/**
* Enables or disables logging.
* @param enableInfo enables info.
* @param enableWarn enables warning.
* @param enableGroup enables groups.
*/
static setLogLevel(enableInfo, enableWarn, enableGroup) {
this.infoEnabled = enableInfo;
this.warnEnabled = enableWarn;
this.groupEnabled = enableGroup;
}
static info(...message) {
if (this.infoEnabled) console.info(...message);
}
static warn(...message) {
if (this.warnEnabled) console.warn(...message);
}
static group(...message) {
if (this.groupEnabled) console.group(...message);
}
static groupCollapsed(...message) {
if (this.groupEnabled) console.groupCollapsed(...message);
}
static groupEnd() {
if (this.groupEnabled) console.groupEnd();
}
/**
* @internal
*/
static unsupported(what, syx, reason = "") {
if (this.infoEnabled) this.info(`%cUnsupported %c${what}%c message: %c${arrayToHexString(syx)}%c. ${reason}`, ConsoleColors.warn, ConsoleColors.recognized, ConsoleColors.warn, ConsoleColors.unrecognized, ConsoleColors.warn);
}
/**
* @internal
*/
static gmInfo(what, value, unit = "") {
if (this.infoEnabled) this.coolInfo(`General MIDI ${what}`, value, unit);
}
/**
* @internal
*/
static gmFail(what, syx) {
if (this.infoEnabled) this.unsupported(`General MIDI ${what}`, syx);
}
/**
* @internal
*/
static gsInfo(what, value, unit = "") {
if (this.infoEnabled) this.coolInfo(`Roland GS ${what}`, value, unit);
}
/**
* @internal
*/
static gsFail(what, syx, reason = "") {
if (this.infoEnabled) this.unsupported(`Roland GS ${what}`, syx, reason);
}
/**
* @internal
*/
static xgInfo(what, value, unit = "") {
if (this.infoEnabled) this.coolInfo(`Yamaha XG ${what}`, value, unit);
}
/**
* @internal
*/
static xgFail(what, syx, reason = "") {
if (this.infoEnabled) this.unsupported(`Yamaha XG ${what}`, syx, reason);
}
/**
* @internal
*/
static coolInfo(what, value, unit = "") {
if (!this.infoEnabled) return;
if (unit) SpessaLog.info(`%c${what}%c is now set to %c${value}%c ${unit}.`, ConsoleColors.recognized, ConsoleColors.info, ConsoleColors.value, ConsoleColors.info);
else SpessaLog.info(`%c${what}%c is now set to %c${value}%c.`, ConsoleColors.recognized, ConsoleColors.info, ConsoleColors.value, ConsoleColors.info);
}
};
//#endregion
//#region src/utils/exports.ts
const SpessaSynthCoreUtils = {
ConsoleColors,
readBigEndian,
readLittleEndian,
readLittleEndianIndexed,
readBinaryString,
readBinaryStringIndexed,
readVariableLengthQuantity,
inflateSync: inf
};
//#endregion
//#region src/midi/enums.ts
const 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
};
const MIDIControllers = {
bankSelect: 0,
modulationWheel: 1,
breathController: 2,
undefinedCC3: 3,
footController: 4,
portamentoTime: 5,
dataEntryMSB: 6,
mainVolume: 7,
balance: 8,
undefinedCC9: 9,
pan: 10,
expression: 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,
expressionLSB: 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,
variationDepth: 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
};
const RegisteredParameterTypes = {
pitchWheelRange: 0,
fineTuning: 1,
coarseTuning: 2,
modulationDepth: 5,
resetParameters: 16383
};
const NonRegisteredMSB = {
partParameter: 1,
drumPitch: 24,
drumPitchFine: 25,
drumLevel: 26,
drumPan: 28,
drumReverb: 29,
drumChorus: 30,
drumDelay: 31,
awe32: 127,
SF2: 120
};
/**
* https://cdn.roland.com/assets/media/pdf/SC-8850_OM.pdf
* http://hummer.stanford.edu/sig/doc/classes/MidiOutput/rpn.html
* These also seem to match XG
*/
const NonRegisteredLSB = {
vibratoRate: 8,
vibratoDepth: 9,
vibratoDelay: 10,
tvfCutoffFrequency: 32,
tvfResonance: 33,
envelopeAttackTime: 99,
envelopeDecayTime: 100,
envelopeReleaseTime: 102
};
//#endregion
//#region src/midi/midi_message.ts
/**
* Midi_message.ts
* purpose: contains enums for midi events and controllers and functions to parse them
*/
var MIDIMessage = class MIDIMessage {
/**
* 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;
}
/**
* Returns a new MIDI Pitch Wheel message.
* @param ticks time of this message in absolute MIDI ticks.
* @param channel the channel number of this message.
* @param value the new value, between 0 and 16383, where 8192 is the center (no pitch change).
*/
static pitchWheel(ticks, channel, value) {
return new MIDIMessage(ticks, MIDIMessageTypes.pitchWheel | channel % 16, new Uint8Array([value & 127, value >> 7 & 127]));
}
/**
* Returns a new MIDI Channel Pressure message.
* @param ticks time of this message in absolute MIDI ticks.
* @param channel the channel number of this message.
* @param value the new value, between 0 and 127.
*/
static channelPressure(ticks, channel, value) {
return new MIDIMessage(ticks, MIDIMessageTypes.channelPressure | channel % 16, new Uint8Array([value]));
}
/**
* Returns a new MIDI Program Change message.
* @param ticks time of this message in absolute MIDI ticks.
* @param channel the channel number of this message.
* @param program the new MIDI program number, between 0 and 127.
*/
static programChange(ticks, channel, program) {
return new MIDIMessage(ticks, MIDIMessageTypes.programChange | channel % 16, new Uint8Array([program]));
}
/**
* Returns a new MIDI Controller Change message.
* @param ticks time of this message in absolute MIDI ticks.
* @param channel the channel number of this message.
* @param controller the MIDI controller.
* @param value the new value.
*/
static controllerChange(ticks, channel, controller, value) {
return new MIDIMessage(ticks, MIDIMessageTypes.controllerChange | channel % 16, new Uint8Array([controller, value]));
}
/**
* Returns a new MIDI System Exclusive message.
* @param ticks time of this message in absolute MIDI ticks.
* @param data the data of the system exclusive message,
* excluding the starting 0xF0 byte.
*/
static systemExclusive(ticks, data) {
return new MIDIMessage(ticks, MIDIMessageTypes.systemExclusive, new Uint8Array(data));
}
/**
* Returns a new MIDI Registered Parameter message. Sends both data MSB and LSB.
* @param ticks time of this message in absolute MIDI ticks.
* @param channel the channel number of this message.
* @param parameter the 14-bit MIDI registered parameter number.
* @param value the 14-bit new value.
*/
static registeredParameter(ticks, channel, parameter, value) {
if (parameter > 16383 || parameter < 0 || value > 16383 || value < 0) throw new Error("Parameter and value must be between 0 and 16383.");
return [
MIDIMessage.controllerChange(ticks, channel, MIDIControllers.registeredParameterMSB, parameter >> 7),
MIDIMessage.controllerChange(ticks, channel, MIDIControllers.registeredParameterLSB, parameter & 127),
MIDIMessage.controllerChange(ticks, channel, MIDIControllers.dataEntryMSB, value >> 7),
MIDIMessage.controllerChange(ticks, channel, MIDIControllers.dataEntryLSB, value & 127)
];
}
};
//#endregion
//#region src/midi/write/midi.ts
const writeText = (text, arr) => {
for (let i = 0; i < text.length; i++) arr.push(text.charCodeAt(i));
};
/**
* Exports the midi as a standard MIDI file
* @param midi the MIDI to write
*/
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), ...messageData);
currentTick += deltaTicks;
}
binaryTrack.push(0, 255, MIDIMessageTypes.endOfTrack, 0);
binaryTrackData.push(binaryTrack);
}
let binaryData = [];
writeText("MThd", binaryData);
binaryData.push(...writeBigEndian(6, 4), 0, midi.format, ...writeBigEndian(midi.tracks.length, 2), ...writeBigEndian(midi.timeDivision, 2));
for (const track of binaryTrackData) {
writeText("MTrk", binaryData);
binaryData = binaryData.concat(writeBigEndian(track.length, 4), track);
}
return new Uint8Array(binaryData).buffer;
}
//#endregion
//#region src/synthesizer/audio_engine/synth_constants.ts
/**
* Synthesizer's default voice cap.
*/
const VOICE_CAP = 350;
/**
* Default MIDI drum channel.
*/
const DEFAULT_PERCUSSION = 9;
/**
* Default bank select and SysEx mode.
*/
const DEFAULT_SYNTH_MODE = "gs";
/**
* Used globally to identify the embedded sound bank
* This is used to prevent the embedded bank from being deleted.
*/
const EMBEDDED_SOUND_BANK_ID = `SPESSASYNTH_EMBEDDED_BANK_${Math.random()}_DO_NOT_DELETE`;
const GENERATOR_OVERRIDE_NO_CHANGE_VALUE = 32767;
const DEFAULT_SYNTH_METHOD_OPTIONS = { time: 0 };
/**
* If the note is released faster than that, it forced to last that long
* This is used mostly for drum channels, where a lot of midis like to send instant note off after a note on
*/
const MIN_NOTE_LENGTH = .03;
/**
* This sounds way nicer for an instant hi-hat cutoff
*/
const MIN_EXCLUSIVE_LENGTH = .07;
/**
* This panning factor ensures that spessasynth doesn't stay too loud.
* You can set te `gain` system parameter to an inverse of it to negate the effect.
*/
const SPESSASYNTH_GAIN_FACTOR = .6;
/**
* The default buffer size for the synthesizer.
*/
const SPESSA_BUFSIZE = 128;
/**
* This is needed because effects (regular ones) are send straight from the mono signal, whereas
* insertion effects receive the panned audio (twice), which reduces gain by a factor of cos(pi/4) * cos(pi/4) (master pan + voice pan).
* This reverses it.
*/
const EFX_SENDS_GAIN_CORRECTION = 1 / Math.cos(Math.PI / 4) ** 2;
/**
* The amount of MIDI controllers (127)
*/
const CONTROLLER_TABLE_SIZE = 128;
const GM2_DEFAULT_BANK = 121;
/**
* A class for handling various ways of selecting patches (GS, XG, GM2)
*/
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 isXGDrum(bankMSB) {
return bankMSB === 120 || bankMSB === 127;
}
/**
* Checks if this MSB is a valid XG MSB
*/
static isValidXGMSB(bankMSB) {
return this.isXGDrum(bankMSB) || bankMSB === 64 || bankMSB === GM2_DEFAULT_BANK;
}
static isSystemXG(system) {
return system === "gm2" || system === "xg";
}
static addBankOffset(bankMSB, bankOffset, isXG) {
if (this.isXGDrum(bankMSB) && isXG) return bankMSB;
return Math.min(bankMSB + bankOffset, 127);
}
static subtractBankOffset(bankMSB, bankOffset, isXG) {
if (this.isXGDrum(bankMSB) && isXG) return bankMSB;
return Math.max(0, bankMSB - bankOffset);
}
};
//#endregion
//#region src/soundbank/basic_soundbank/midi_patch.ts
var MIDIPatchTools = class MIDIPatchTools {
/**
* Converts a given `MIDIPatch` to a string.
* The format is:
* - `DRUM:program` for `GMGSDrum` set to `true`.
* - `bankLSB:bankMSB:program` for `GMGSDrum` set to `false`.
*/
static toMIDIString(patch) {
if (patch.isGMGSDrum) return `DRUM:${patch.program}`;
return `${patch.bankLSB}:${patch.bankMSB}:${patch.program}`;
}
/**
* Gets `MIDIPatch` from a given string.
*/
static fromMIDIString(string) {
const parts = string.split(":");
if (parts.length > 3 || parts.length < 2) throw new Error(`Invalid MIDI string: ${string}`);
return string.startsWith("DRUM") ? {
bankMSB: 0,
bankLSB: 0,
program: Number.parseInt(parts[1]),
isGMGSDrum: true
} : {
bankLSB: Number.parseInt(parts[0]),
bankMSB: Number.parseInt(parts[1]),
program: Number.parseInt(parts[2]),
isGMGSDrum: false
};
}
/**
* Converts a given `MIDIPatchFull`to string.
* The format is:
* - `<MIDIPatch string> D <name>` for `isDrum` set to `true`.
* - `<MIDIPatch string> M <name>` for `isDrum` set to `true`.
*/
static toFullMIDIString(patch) {
return `${this.toMIDIString(patch)} ${patch.isDrum ? "D" : "M"} ${patch.name}`;
}
/**
* Gets `MIDIPatchFull` from a given string.
*/
static fromFullMIDIString(string) {
const firstSpace = string.indexOf(" ");
const secondSpace = string.indexOf(" ", firstSpace + 1);
if (firstSpace === -1 || secondSpace === -1) throw new Error(`Invalid named MIDI string: ${string}`);
const midiPart = string.slice(0, Math.max(0, firstSpace));
const drumMode = string.slice(firstSpace + 1, secondSpace);
const name = string.slice(Math.max(0, secondSpace + 1));
return {
...MIDIPatchTools.fromMIDIString(midiPart),
isDrum: drumMode === "D",
name
};
}
/**
* Checks if two MIDI patches represent the same one.
*/
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;
}
/**
* A comparison function for `.sort()` or `.toSorted()`,
* ordering the patches in ascending order.
*/
static compare(a, b) {
if (a.isGMGSDrum && !b.isGMGSDrum) return 1;
if (!a.isGMGSDrum && b.isGMGSDrum) return -1;
if (a.program !== b.program) return a.program - b.program;
if (a.bankMSB !== b.bankMSB) return a.bankMSB - b.bankMSB;
return a.bankLSB - b.bankLSB;
}
/**
* Checks if the given `MIDIPatchFull` is an XG/GM2 drum patch.
*/
static isXGDrum(p) {
return p.isDrum && !p.isGMGSDrum;
}
/**
* A sophisticated patch selection system based on the MIDI Patch system.
* This is the algorithm that the synthesizer uses for selecting presets.
* @param patches The `MIDIPatchFull` array to select from.
* @param patch The `MIDIPatch` to select.
* @param system The MIDI system to select for.
* @returns The selected patch.
*/
static selectPatch(patches, patch, system) {
if (patches.length === 0) throw new Error("No presets!");
if (patch.isGMGSDrum && BankSelectHacks.isSystemXG(system)) patch = {
...patch,
isGMGSDrum: false,
bankLSB: 0,
bankMSB: BankSelectHacks.getDrumBank(system)
};
const { isGMGSDrum, bankLSB, bankMSB, program } = patch;
const isXG = BankSelectHacks.isSystemXG(system);
const xgDrums = BankSelectHacks.isXGDrum(bankMSB) && isXG;
let p = patches.find((p) => this.matches(p, patch));
if (p && (!xgDrums || xgDrums && this.isXGDrum(p))) return p;
const returnReplacement = (pres) => {
SpessaLog.info(`%cPreset %c${MIDIPatchTools.toMIDIString(patch)}%c not found. (${system}) Replaced with %c${this.toFullMIDIString(pres)}`, ConsoleColors.warn, ConsoleColors.unrecognized, ConsoleColors.warn, ConsoleColors.value);
};
if (isGMGSDrum) {
let p = patches.find((p) => p.isGMGSDrum && p.program === program);
if (p) {
returnReplacement(p);
return p;
}
p = patches.find((p) => p.isDrum && p.program === program);
if (p) {
returnReplacement(p);
return p;
}
p = this.getAnyDrums(patches, false);
returnReplacement(p);
return p;
}
if (xgDrums) {
let p = patches.find((p) => p.program === program && p.isDrum && !p.isGMGSDrum);
if (p) {
returnReplacement(p);
return p;
}
p = patches.find((p) => p.isDrum && p.program === program);
if (p && p.program < 49) {
returnReplacement(p);
return p;
}
p = this.getAnyDrums(patches, true);
returnReplacement(p);
return p;
}
const matchingPrograms = patches.filter((p) => p.program === program && !p.isDrum);
if (matchingPrograms.length === 0) {
returnReplacement(patches[0]);
return patches[0];
}
p = isXG ? matchingPrograms.find((p) => p.bankLSB === bankLSB) : matchingPrograms.find((p) => p.bankMSB === bankMSB);
if (p) {
returnReplacement(p);
return p;
}
if (bankLSB !== 64 || !isXG) {
const bank = Math.max(bankMSB, bankLSB);
p = matchingPrograms.find((p) => p.bankLSB === bank || p.bankMSB === bank);
if (p) {
returnReplacement(p);
return p;
}
}
returnReplacement(matchingPrograms[0]);
return matchingPrograms[0];
}
static getAnyDrums(presets, preferXG) {
const p = preferXG ? presets.find((p) => this.isXGDrum(p)) : presets.find((p) => p.isGMGSDrum);
if (p) return p;
return presets.find((p) => p.isDrum) ?? presets[0];
}
};
//#endregion
//#region src/midi/midi_tools/midi_utils.ts
const OTHER = Object.freeze({ type: "Other" });
/**
* A general purpose class for handling MIDI messages.
*/
var MIDIUtils = class MIDIUtils {
/**
* Analyzes a MIDI System Exclusive message
* and returns an identification and data for it.
* @param syx the System Exclusive message, WITHOUT the first 0xF0 System Exclusive byte!
*/
static analyzeSysEx(syx) {
if (syx.length < 3) return OTHER;
switch (syx[0]) {
default: return OTHER;
case 126:
case 127: return this.analyzeGM(syx);
case 65: return this.analyzeGS(syx);
case 67: return this.analyzeXG(syx);
}
}
/**
* Analyzes a MIDI Registered Parameter Number
* and returns an identification and data for it.
* @param channel The MIDI channel number.
* @param rpn The 14-bit RPN number.
* @param value The 14-bit value for that number.
*/
static analyzeRPN(channel, rpn, value) {
switch (rpn) {
default: return OTHER;
case RegisteredParameterTypes.pitchWheelRange: return {
type: "Channel MIDI Param",
channel,
parameter: "pitchWheelRange",
value: value / 128
};
case RegisteredParameterTypes.fineTuning: return {
type: "Channel MIDI Param",
channel,
parameter: "fineTune",
value: (value - 8192) / 81.92
};
case RegisteredParameterTypes.coarseTuning: return {
type: "Channel MIDI Param",
channel,
parameter: "keyShift",
value: (value >> 7) - 64
};
case RegisteredParameterTypes.modulationDepth: return {
type: "Channel MIDI Param",
channel,
parameter: "modulationDepth",
value: value / 1.28
};
}
}
/**
* Analyzes a MIDI Non-Registered Parameter Number
* and returns an identification and data for it.
* @param channel The MIDI channel number.
* @param nrpn The 14-bit NRPN number.
* @param value The 14-bit value for that number.
*/
static analyzeNRPN(channel, nrpn, value) {
const msb = nrpn >> 7;
const lsb = nrpn & 127;
switch (msb) {
default: return OTHER;
case NonRegisteredMSB.partParameter: switch (lsb) {
default: return OTHER;
case NonRegisteredLSB.vibratoRate: return {
type: "Controller Change",
channel,
controller: MIDIControllers.vibratoRate,
value: value >> 7
};
case NonRegisteredLSB.vibratoDepth: return {
type: "Controller Change",
channel,
controller: MIDIControllers.vibratoDepth,
value: value >> 7
};
case NonRegisteredLSB.vibratoDelay: return {
type: "Controller Change",
channel,
controller: MIDIControllers.vibratoDelay,
value: value >> 7
};
case NonRegisteredLSB.tvfCutoffFrequency: return {
type: "Controller Change",
channel,
controller: MIDIControllers.brightness,
value: value >> 7
};
case NonRegisteredLSB.tvfResonance: return {
type: "Controller Change",
channel,
controller: MIDIControllers.filterResonance,
value: value >> 7
};
case NonRegisteredLSB.envelopeAttackTime: return {
type: "Controller Change",
channel,
controller: MIDIControllers.attackTime,
value: value >> 7
};
case NonRegisteredLSB.envelopeDecayTime: return {
type: "Controller Change",
channel,
controller: MIDIControllers.decayTime,
value: value >> 7
};
case NonRegisteredLSB.envelopeReleaseTime: return {
type: "Controller Change",
channel,
controller: MIDIControllers.releaseTime,
value: value >> 7
};
}
case NonRegisteredMSB.drumPitch:
case NonRegisteredMSB.drumPitchFine:
case NonRegisteredMSB.drumLevel:
case NonRegisteredMSB.drumPan:
case NonRegisteredMSB.drumReverb:
case NonRegisteredMSB.drumChorus:
case NonRegisteredMSB.drumDelay: return { type: "Drum Setup" };
}
}
/**
* Returns a list of MIDI events needed to set the given parameter.
* @param ticks The ticks for all events.
* @param system If the message has multiple ways of setting it,
* this selects the preferred way. Otherwise, it prefers Universal (GM).
* @param parameter The parameter to set.
* @param value The value to set it to.
*/
static setGlobalMIDIParameter(ticks, system, parameter, value) {
switch (parameter) {
case "system": return [MIDIUtils.reset(ticks, value)];
case "keyShift": switch (system) {
default: return [MIDIUtils.deviceControlMessage(ticks, 4, [0, value + 64])];
case "xg": return [MIDIUtils.xgMessage(ticks, 0, 0, 6, [value + 64])];
case "gs": return [MIDIUtils.gsMessage(ticks, 64, 0, 5, [value + 64])];
}