UNPKG

spessasynth_core

Version:

MIDI and SoundFont2/DLS library with no compromises

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