|
|
import { dataBytesAmount, getChannel, MIDIMessage } from "./midi_message.js"; |
|
|
import { IndexedByteArray } from "../utils/indexed_array.js"; |
|
|
import { consoleColors } from "../utils/other.js"; |
|
|
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js"; |
|
|
import { readRIFFChunk } from "../soundfont/basic_soundfont/riff_chunk.js"; |
|
|
import { readVariableLengthQuantity } from "../utils/byte_functions/variable_length_quantity.js"; |
|
|
import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js"; |
|
|
import { readBytesAsString } from "../utils/byte_functions/string.js"; |
|
|
import { readLittleEndian } from "../utils/byte_functions/little_endian.js"; |
|
|
import { RMIDINFOChunks } from "./rmidi_writer.js"; |
|
|
import { BasicMIDI } from "./basic_midi.js"; |
|
|
import { loadXMF } from "./xmf_loader.js"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MIDI extends BasicMIDI |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(arrayBuffer, fileName = "") |
|
|
{ |
|
|
super(); |
|
|
SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info); |
|
|
this.fileName = fileName; |
|
|
const binaryData = new IndexedByteArray(arrayBuffer); |
|
|
let fileByteArray; |
|
|
|
|
|
|
|
|
const initialString = readBytesAsString(binaryData, 4); |
|
|
binaryData.currentIndex -= 4; |
|
|
if (initialString === "RIFF") |
|
|
{ |
|
|
|
|
|
|
|
|
binaryData.currentIndex += 8; |
|
|
const rmid = readBytesAsString(binaryData, 4, undefined, false); |
|
|
if (rmid !== "RMID") |
|
|
{ |
|
|
SpessaSynthGroupEnd(); |
|
|
throw new SyntaxError(`Invalid RMIDI Header! Expected "RMID", got "${rmid}"`); |
|
|
} |
|
|
const riff = readRIFFChunk(binaryData); |
|
|
if (riff.header !== "data") |
|
|
{ |
|
|
SpessaSynthGroupEnd(); |
|
|
throw new SyntaxError(`Invalid RMIDI Chunk header! Expected "data", got "${rmid}"`); |
|
|
} |
|
|
|
|
|
fileByteArray = riff.chunkData; |
|
|
|
|
|
|
|
|
while (binaryData.currentIndex <= binaryData.length) |
|
|
{ |
|
|
const startIndex = binaryData.currentIndex; |
|
|
const currentChunk = readRIFFChunk(binaryData, true); |
|
|
if (currentChunk.header === "RIFF") |
|
|
{ |
|
|
const type = readBytesAsString(currentChunk.chunkData, 4).toLowerCase(); |
|
|
if (type === "sfbk" || type === "sfpk" || type === "dls ") |
|
|
{ |
|
|
SpessaSynthInfo("%cFound embedded soundfont!", consoleColors.recognized); |
|
|
this.embeddedSoundFont = binaryData.slice(startIndex, startIndex + currentChunk.size).buffer; |
|
|
} |
|
|
else |
|
|
{ |
|
|
SpessaSynthWarn(`Unknown RIFF chunk: "${type}"`); |
|
|
} |
|
|
if (type === "dls ") |
|
|
{ |
|
|
|
|
|
this.isDLSRMIDI = true; |
|
|
} |
|
|
} |
|
|
else if (currentChunk.header === "LIST") |
|
|
{ |
|
|
const type = readBytesAsString(currentChunk.chunkData, 4); |
|
|
if (type === "INFO") |
|
|
{ |
|
|
SpessaSynthInfo("%cFound RMIDI INFO chunk!", consoleColors.recognized); |
|
|
this.RMIDInfo = {}; |
|
|
while (currentChunk.chunkData.currentIndex <= currentChunk.size) |
|
|
{ |
|
|
const infoChunk = readRIFFChunk(currentChunk.chunkData, true); |
|
|
this.RMIDInfo[infoChunk.header] = infoChunk.chunkData; |
|
|
} |
|
|
if (this.RMIDInfo["ICOP"]) |
|
|
{ |
|
|
|
|
|
this.copyright = readBytesAsString( |
|
|
this.RMIDInfo["ICOP"], |
|
|
this.RMIDInfo["ICOP"].length, |
|
|
undefined, |
|
|
false |
|
|
).replaceAll("\n", " "); |
|
|
} |
|
|
if (this.RMIDInfo["INAM"]) |
|
|
{ |
|
|
this.rawMidiName = this.RMIDInfo[RMIDINFOChunks.name]; |
|
|
|
|
|
this.midiName = readBytesAsString( |
|
|
this.rawMidiName, |
|
|
this.rawMidiName.length, |
|
|
undefined, |
|
|
false |
|
|
).replaceAll("\n", " "); |
|
|
} |
|
|
|
|
|
if (this.RMIDInfo["IALB"] && !this.RMIDInfo["IPRD"]) |
|
|
{ |
|
|
this.RMIDInfo["IPRD"] = this.RMIDInfo["IALB"]; |
|
|
} |
|
|
if (this.RMIDInfo["IPRD"] && !this.RMIDInfo["IALB"]) |
|
|
{ |
|
|
this.RMIDInfo["IALB"] = this.RMIDInfo["IPRD"]; |
|
|
} |
|
|
this.bankOffset = 1; |
|
|
if (this.RMIDInfo[RMIDINFOChunks.bankOffset]) |
|
|
{ |
|
|
this.bankOffset = readLittleEndian(this.RMIDInfo[RMIDINFOChunks.bankOffset], 2); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (this.isDLSRMIDI) |
|
|
{ |
|
|
|
|
|
this.bankOffset = 0; |
|
|
} |
|
|
|
|
|
|
|
|
if (this.embeddedSoundFont === undefined) |
|
|
{ |
|
|
this.bankOffset = 0; |
|
|
} |
|
|
} |
|
|
else if (initialString === "XMF_") |
|
|
{ |
|
|
|
|
|
fileByteArray = loadXMF(this, binaryData); |
|
|
} |
|
|
else |
|
|
{ |
|
|
fileByteArray = binaryData; |
|
|
} |
|
|
const headerChunk = this._readMIDIChunk(fileByteArray); |
|
|
if (headerChunk.type !== "MThd") |
|
|
{ |
|
|
SpessaSynthGroupEnd(); |
|
|
throw new SyntaxError(`Invalid MIDI Header! Expected "MThd", got "${headerChunk.type}"`); |
|
|
} |
|
|
|
|
|
if (headerChunk.size !== 6) |
|
|
{ |
|
|
SpessaSynthGroupEnd(); |
|
|
throw new RangeError(`Invalid MIDI header chunk size! Expected 6, got ${headerChunk.size}`); |
|
|
} |
|
|
|
|
|
|
|
|
this.format = readBytesAsUintBigEndian(headerChunk.data, 2); |
|
|
|
|
|
this.tracksAmount = readBytesAsUintBigEndian(headerChunk.data, 2); |
|
|
|
|
|
this.timeDivision = readBytesAsUintBigEndian(headerChunk.data, 2); |
|
|
|
|
|
for (let i = 0; i < this.tracksAmount; i++) |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
const track = []; |
|
|
const trackChunk = this._readMIDIChunk(fileByteArray); |
|
|
|
|
|
if (trackChunk.type !== "MTrk") |
|
|
{ |
|
|
SpessaSynthGroupEnd(); |
|
|
throw new SyntaxError(`Invalid track header! Expected "MTrk" got "${trackChunk.type}"`); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let runningByte = undefined; |
|
|
|
|
|
let totalTicks = 0; |
|
|
|
|
|
if (this.format === 2 && i > 0) |
|
|
{ |
|
|
totalTicks += this.tracks[i - 1][this.tracks[i - 1].length - 1].ticks; |
|
|
} |
|
|
|
|
|
while (trackChunk.data.currentIndex < trackChunk.size) |
|
|
{ |
|
|
totalTicks += readVariableLengthQuantity(trackChunk.data); |
|
|
|
|
|
|
|
|
const statusByteCheck = trackChunk.data[trackChunk.data.currentIndex]; |
|
|
|
|
|
let statusByte; |
|
|
|
|
|
if (runningByte !== undefined && statusByteCheck < 0x80) |
|
|
{ |
|
|
statusByte = runningByte; |
|
|
} |
|
|
else |
|
|
{ |
|
|
if (runningByte === undefined && statusByteCheck < 0x80) |
|
|
{ |
|
|
|
|
|
SpessaSynthGroupEnd(); |
|
|
throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`); |
|
|
} |
|
|
else |
|
|
{ |
|
|
|
|
|
statusByte = trackChunk.data[trackChunk.data.currentIndex++]; |
|
|
} |
|
|
} |
|
|
const statusByteChannel = getChannel(statusByte); |
|
|
|
|
|
let eventDataLength; |
|
|
|
|
|
|
|
|
switch (statusByteChannel) |
|
|
{ |
|
|
case -1: |
|
|
|
|
|
eventDataLength = 0; |
|
|
break; |
|
|
|
|
|
case -2: |
|
|
|
|
|
statusByte = trackChunk.data[trackChunk.data.currentIndex++]; |
|
|
eventDataLength = readVariableLengthQuantity(trackChunk.data); |
|
|
break; |
|
|
|
|
|
case -3: |
|
|
|
|
|
eventDataLength = readVariableLengthQuantity(trackChunk.data); |
|
|
break; |
|
|
|
|
|
default: |
|
|
|
|
|
|
|
|
eventDataLength = dataBytesAmount[statusByte >> 4]; |
|
|
|
|
|
runningByte = statusByte; |
|
|
break; |
|
|
} |
|
|
|
|
|
|
|
|
const eventData = new IndexedByteArray(eventDataLength); |
|
|
eventData.set(trackChunk.data.slice( |
|
|
trackChunk.data.currentIndex, |
|
|
trackChunk.data.currentIndex + eventDataLength |
|
|
), 0); |
|
|
const event = new MIDIMessage(totalTicks, statusByte, eventData); |
|
|
track.push(event); |
|
|
|
|
|
trackChunk.data.currentIndex += eventDataLength; |
|
|
} |
|
|
this.tracks.push(track); |
|
|
|
|
|
SpessaSynthInfo( |
|
|
`%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`, |
|
|
consoleColors.info, |
|
|
consoleColors.value, |
|
|
consoleColors.info, |
|
|
consoleColors.value |
|
|
); |
|
|
} |
|
|
|
|
|
SpessaSynthInfo( |
|
|
`%cAll tracks parsed correctly!`, |
|
|
consoleColors.recognized |
|
|
); |
|
|
|
|
|
this._parseInternal(); |
|
|
SpessaSynthGroupEnd(); |
|
|
SpessaSynthInfo( |
|
|
`%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}%c, total seconds time: %c${this.duration}`, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_readMIDIChunk(fileByteArray) |
|
|
{ |
|
|
const chunk = {}; |
|
|
|
|
|
chunk.type = readBytesAsString(fileByteArray, 4); |
|
|
|
|
|
chunk.size = readBytesAsUintBigEndian(fileByteArray, 4); |
|
|
|
|
|
chunk.data = new IndexedByteArray(chunk.size); |
|
|
const dataSlice = fileByteArray.slice(fileByteArray.currentIndex, fileByteArray.currentIndex + chunk.size); |
|
|
chunk.data.set(dataSlice, 0); |
|
|
fileByteArray.currentIndex += chunk.size; |
|
|
return chunk; |
|
|
} |
|
|
} |
|
|
|
|
|
export { MIDI }; |