UNPKG

spessasynth_core

Version:

MIDI and SoundFont2/DLS library with no compromises

1,725 lines (1,724 loc) 741 kB
//#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])]; }