|
|
import { combineArrays, IndexedByteArray } from "../utils/indexed_array.js"; |
|
|
import { writeRIFFOddSize } from "../soundfont/basic_soundfont/riff_chunk.js"; |
|
|
import { getStringBytes, getStringBytesZero } from "../utils/byte_functions/string.js"; |
|
|
import { messageTypes, midiControllers, MIDIMessage } from "./midi_message.js"; |
|
|
import { getGsOn } from "./midi_editor.js"; |
|
|
import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js"; |
|
|
import { consoleColors } from "../utils/other.js"; |
|
|
import { writeLittleEndian } from "../utils/byte_functions/little_endian.js"; |
|
|
import { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js"; |
|
|
import { chooseBank, isSystemXG, parseBankSelect } from "../utils/xg_hacks.js"; |
|
|
import { isGM2On, isGMOn, isGSDrumsOn, isGSOn, isXGOn } from "../utils/sysex_detector.js"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const RMIDINFOChunks = { |
|
|
name: "INAM", |
|
|
album: "IPRD", |
|
|
album2: "IALB", |
|
|
artist: "IART", |
|
|
genre: "IGNR", |
|
|
picture: "IPIC", |
|
|
copyright: "ICOP", |
|
|
creationDate: "ICRD", |
|
|
comment: "ICMT", |
|
|
engineer: "IENG", |
|
|
software: "ISFT", |
|
|
encoding: "IENC", |
|
|
midiEncoding: "MENC", |
|
|
bankOffset: "DBNK" |
|
|
}; |
|
|
|
|
|
const FORCED_ENCODING = "utf-8"; |
|
|
const DEFAULT_COPYRIGHT = "Created using SpessaSynth"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function writeRMIDI( |
|
|
soundfontBinary, |
|
|
soundfont, |
|
|
bankOffset = 0, |
|
|
encoding = "Shift_JIS", |
|
|
metadata = {}, |
|
|
correctBankOffset = true |
|
|
) |
|
|
{ |
|
|
const mid = this; |
|
|
SpessaSynthGroup("%cWriting the RMIDI File...", consoleColors.info); |
|
|
SpessaSynthInfo( |
|
|
`%cConfiguration: Bank offset: %c${bankOffset}%c, encoding: %c${encoding}`, |
|
|
consoleColors.info, |
|
|
consoleColors.value, |
|
|
consoleColors.info, |
|
|
consoleColors.value |
|
|
); |
|
|
SpessaSynthInfo("metadata", metadata); |
|
|
SpessaSynthInfo("Initial bank offset", mid.bankOffset); |
|
|
if (correctBankOffset) |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let system = "gm"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let unwantedSystems = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const eventIndexes = Array(mid.tracks.length).fill(0); |
|
|
let remainingTracks = mid.tracks.length; |
|
|
|
|
|
function findFirstEventIndex() |
|
|
{ |
|
|
let index = 0; |
|
|
let ticks = Infinity; |
|
|
mid.tracks.forEach((track, i) => |
|
|
{ |
|
|
if (eventIndexes[i] >= track.length) |
|
|
{ |
|
|
return; |
|
|
} |
|
|
if (track[eventIndexes[i]].ticks < ticks) |
|
|
{ |
|
|
index = i; |
|
|
ticks = track[eventIndexes[i]].ticks; |
|
|
} |
|
|
}); |
|
|
return index; |
|
|
} |
|
|
|
|
|
|
|
|
const ports = Array(mid.tracks.length).fill(0); |
|
|
const channelsAmount = 16 + mid.midiPortChannelOffsets.reduce((max, cur) => cur > max ? cur : max); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const channelsInfo = []; |
|
|
for (let i = 0; i < channelsAmount; i++) |
|
|
{ |
|
|
channelsInfo.push({ |
|
|
program: 0, |
|
|
drums: i % 16 === DEFAULT_PERCUSSION, |
|
|
lastBank: undefined, |
|
|
lastBankLSB: undefined, |
|
|
hasBankSelect: false |
|
|
}); |
|
|
} |
|
|
while (remainingTracks > 0) |
|
|
{ |
|
|
let trackNum = findFirstEventIndex(); |
|
|
const track = mid.tracks[trackNum]; |
|
|
if (eventIndexes[trackNum] >= track.length) |
|
|
{ |
|
|
remainingTracks--; |
|
|
continue; |
|
|
} |
|
|
const e = track[eventIndexes[trackNum]]; |
|
|
eventIndexes[trackNum]++; |
|
|
|
|
|
let portOffset = mid.midiPortChannelOffsets[ports[trackNum]]; |
|
|
if (e.messageStatusByte === messageTypes.midiPort) |
|
|
{ |
|
|
ports[trackNum] = e.messageData[0]; |
|
|
continue; |
|
|
} |
|
|
const status = e.messageStatusByte & 0xF0; |
|
|
if ( |
|
|
status !== messageTypes.controllerChange && |
|
|
status !== messageTypes.programChange && |
|
|
status !== messageTypes.systemExclusive |
|
|
) |
|
|
{ |
|
|
continue; |
|
|
} |
|
|
|
|
|
if (status === messageTypes.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: e |
|
|
}); |
|
|
} |
|
|
else if (isGM2On(e)) |
|
|
{ |
|
|
system = "gm2"; |
|
|
} |
|
|
continue; |
|
|
} |
|
|
const sysexChannel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][e.messageData[5] & 0x0F] + portOffset; |
|
|
channelsInfo[sysexChannel].drums = !!(e.messageData[7] > 0 && e.messageData[5] >> 4); |
|
|
continue; |
|
|
} |
|
|
|
|
|
|
|
|
const chNum = (e.messageStatusByte & 0xF) + portOffset; |
|
|
|
|
|
|
|
|
|
|
|
const channel = channelsInfo[chNum]; |
|
|
if (status === messageTypes.programChange) |
|
|
{ |
|
|
const isXG = isSystemXG(system); |
|
|
|
|
|
const initialProgram = e.messageData[0]; |
|
|
if (channel.drums) |
|
|
{ |
|
|
if (soundfont.presets.findIndex(p => p.program === initialProgram && p.isDrumPreset( |
|
|
isXG, |
|
|
true |
|
|
)) === -1) |
|
|
{ |
|
|
|
|
|
e.messageData[0] = soundfont.presets.find(p => p.isDrumPreset(isXG))?.program || 0; |
|
|
SpessaSynthInfo( |
|
|
`%cNo drum preset %c${initialProgram}%c. Channel %c${chNum}%c. Changing program to ${e.messageData[0]}.`, |
|
|
consoleColors.info, |
|
|
consoleColors.unrecognized, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized, |
|
|
consoleColors.info |
|
|
); |
|
|
} |
|
|
} |
|
|
else |
|
|
{ |
|
|
if (soundfont.presets.findIndex(p => p.program === initialProgram && !p.isDrumPreset(isXG)) === -1) |
|
|
{ |
|
|
|
|
|
e.messageData[0] = soundfont.presets.find(p => !p.isDrumPreset(isXG))?.program || 0; |
|
|
SpessaSynthInfo( |
|
|
`%cNo preset %c${initialProgram}%c. Channel %c${chNum}%c. Changing program to ${e.messageData[0]}.`, |
|
|
consoleColors.info, |
|
|
consoleColors.unrecognized, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized, |
|
|
consoleColors.info |
|
|
); |
|
|
} |
|
|
} |
|
|
channel.program = e.messageData[0]; |
|
|
|
|
|
const realBank = Math.max(0, channel.lastBank?.messageData[1] - mid.bankOffset); |
|
|
const bankLSB = (channel?.lastBankLSB?.messageData[1] - mid.bankOffset) || 0; |
|
|
if (channel.lastBank === undefined) |
|
|
{ |
|
|
continue; |
|
|
} |
|
|
|
|
|
let bank = chooseBank(realBank, bankLSB, channel.drums, isXG); |
|
|
if (soundfont.presets.findIndex(p => p.bank === bank && p.program === e.messageData[0]) === -1) |
|
|
{ |
|
|
|
|
|
const targetBank = (soundfont.presets.find(p => p.program === e.messageData[0])?.bank + bankOffset) || bankOffset; |
|
|
channel.lastBank.messageData[1] = targetBank; |
|
|
if (channel?.lastBankLSB?.messageData) |
|
|
{ |
|
|
channel.lastBankLSB.messageData[1] = targetBank; |
|
|
} |
|
|
SpessaSynthInfo( |
|
|
`%cNo preset %c${bank}:${e.messageData[0]}%c. Channel %c${chNum}%c. Changing bank to ${targetBank}.`, |
|
|
consoleColors.info, |
|
|
consoleColors.unrecognized, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized, |
|
|
consoleColors.info |
|
|
); |
|
|
} |
|
|
else |
|
|
{ |
|
|
|
|
|
let drumBank = bank; |
|
|
if (isSystemXG(system) && bank === 128) |
|
|
{ |
|
|
bank = 127; |
|
|
} |
|
|
const newBank = (bank === 128 ? 128 : drumBank) + bankOffset; |
|
|
channel.lastBank.messageData[1] = newBank; |
|
|
if (channel?.lastBankLSB?.messageData && !channel.drums) |
|
|
{ |
|
|
channel.lastBankLSB.messageData[1] = channel.lastBankLSB.messageData[1] - mid.bankOffset + bankOffset; |
|
|
} |
|
|
SpessaSynthInfo( |
|
|
`%cPreset %c${bank}:${e.messageData[0]}%c exists. Channel %c${chNum}%c. Changing bank to ${newBank}.`, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized, |
|
|
consoleColors.info |
|
|
); |
|
|
} |
|
|
continue; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const isLSB = e.messageData[0] === midiControllers.lsbForControl0BankSelect; |
|
|
if (e.messageData[0] !== midiControllers.bankSelect && !isLSB) |
|
|
{ |
|
|
continue; |
|
|
} |
|
|
|
|
|
channel.hasBankSelect = true; |
|
|
const bankNumber = e.messageData[1]; |
|
|
|
|
|
const intepretation = parseBankSelect( |
|
|
channel?.lastBank?.messageData[1] || 0, |
|
|
bankNumber, |
|
|
system, |
|
|
isLSB, |
|
|
channel.drums, |
|
|
chNum |
|
|
); |
|
|
if (intepretation.drumsStatus === 2) |
|
|
{ |
|
|
channel.drums = true; |
|
|
} |
|
|
else if (intepretation.drumsStatus === 1) |
|
|
{ |
|
|
channel.drums = false; |
|
|
} |
|
|
if (isLSB) |
|
|
{ |
|
|
channel.lastBankLSB = e; |
|
|
} |
|
|
else |
|
|
{ |
|
|
channel.lastBank = e; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
channelsInfo.forEach((has, ch) => |
|
|
{ |
|
|
if (has.hasBankSelect === true) |
|
|
{ |
|
|
return; |
|
|
} |
|
|
|
|
|
const midiChannel = ch % 16; |
|
|
const status = messageTypes.programChange | midiChannel; |
|
|
|
|
|
const portOffset = Math.floor(ch / 16) * 16; |
|
|
const port = mid.midiPortChannelOffsets.indexOf(portOffset); |
|
|
const track = mid.tracks.find((t, tNum) => mid.midiPorts[tNum] === port && mid.usedChannelsOnTrack[tNum].has( |
|
|
midiChannel)); |
|
|
if (track === undefined) |
|
|
{ |
|
|
|
|
|
return; |
|
|
} |
|
|
let indexToAdd = track.findIndex(e => e.messageStatusByte === status); |
|
|
if (indexToAdd === -1) |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
const programIndex = track.findIndex(e => (e.messageStatusByte > 0x80 && e.messageStatusByte < 0xF0) && (e.messageStatusByte & 0xF) === midiChannel); |
|
|
if (programIndex === -1) |
|
|
{ |
|
|
|
|
|
return; |
|
|
} |
|
|
const programTicks = track[programIndex].ticks; |
|
|
const targetProgram = soundfont.getPreset(0, 0).program; |
|
|
track.splice(programIndex, 0, new MIDIMessage( |
|
|
programTicks, |
|
|
messageTypes.programChange | midiChannel, |
|
|
new IndexedByteArray([targetProgram]) |
|
|
)); |
|
|
indexToAdd = programIndex; |
|
|
} |
|
|
SpessaSynthInfo( |
|
|
`%cAdding bank select for %c${ch}`, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized |
|
|
); |
|
|
const ticks = track[indexToAdd].ticks; |
|
|
const targetBank = (soundfont.getPreset( |
|
|
0, |
|
|
has.program, |
|
|
isSystemXG(system) |
|
|
)?.bank + bankOffset) || bankOffset; |
|
|
track.splice(indexToAdd, 0, new MIDIMessage( |
|
|
ticks, |
|
|
messageTypes.controllerChange | midiChannel, |
|
|
new IndexedByteArray([midiControllers.bankSelect, targetBank]) |
|
|
)); |
|
|
}); |
|
|
|
|
|
|
|
|
if (system !== "gs" && !isSystemXG(system)) |
|
|
{ |
|
|
for (const m of unwantedSystems) |
|
|
{ |
|
|
mid.tracks[m.tNum].splice(mid.tracks[m.tNum].indexOf(m.e), 1); |
|
|
} |
|
|
let index = 0; |
|
|
if (mid.tracks[0][0].messageStatusByte === messageTypes.trackName) |
|
|
{ |
|
|
index++; |
|
|
} |
|
|
mid.tracks[0].splice(index, 0, getGsOn(0)); |
|
|
} |
|
|
} |
|
|
const newMid = new IndexedByteArray(mid.writeMIDI().buffer); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const infoContent = [getStringBytes("INFO")]; |
|
|
const encoder = new TextEncoder(); |
|
|
|
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.software, encoder.encode("SpessaSynth"), true) |
|
|
); |
|
|
|
|
|
if (metadata.name !== undefined) |
|
|
{ |
|
|
|
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.name, encoder.encode(metadata.name), true) |
|
|
); |
|
|
encoding = FORCED_ENCODING; |
|
|
} |
|
|
else |
|
|
{ |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.name, mid.rawMidiName, true) |
|
|
); |
|
|
} |
|
|
|
|
|
if (metadata.creationDate !== undefined) |
|
|
{ |
|
|
encoding = FORCED_ENCODING; |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.creationDate, encoder.encode(metadata.creationDate), true) |
|
|
); |
|
|
} |
|
|
else |
|
|
{ |
|
|
const today = new Date().toLocaleString(undefined, { |
|
|
weekday: "long", |
|
|
year: "numeric", |
|
|
month: "long", |
|
|
day: "numeric", |
|
|
hour: "numeric", |
|
|
minute: "numeric" |
|
|
}); |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.creationDate, getStringBytesZero(today), true) |
|
|
); |
|
|
} |
|
|
|
|
|
if (metadata.comment !== undefined) |
|
|
{ |
|
|
encoding = FORCED_ENCODING; |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.comment, encoder.encode(metadata.comment)) |
|
|
); |
|
|
} |
|
|
|
|
|
if (metadata.engineer !== undefined) |
|
|
{ |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.engineer, encoder.encode(metadata.engineer), true) |
|
|
); |
|
|
} |
|
|
|
|
|
if (metadata.album !== undefined) |
|
|
{ |
|
|
|
|
|
encoding = FORCED_ENCODING; |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.album, encoder.encode(metadata.album), true) |
|
|
); |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.album2, encoder.encode(metadata.album), true) |
|
|
); |
|
|
} |
|
|
|
|
|
if (metadata.artist !== undefined) |
|
|
{ |
|
|
encoding = FORCED_ENCODING; |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.artist, encoder.encode(metadata.artist), true) |
|
|
); |
|
|
} |
|
|
|
|
|
if (metadata.genre !== undefined) |
|
|
{ |
|
|
encoding = FORCED_ENCODING; |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.genre, encoder.encode(metadata.genre), true) |
|
|
); |
|
|
} |
|
|
|
|
|
if (metadata.picture !== undefined) |
|
|
{ |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.picture, new Uint8Array(metadata.picture)) |
|
|
); |
|
|
} |
|
|
|
|
|
if (metadata.copyright !== undefined) |
|
|
{ |
|
|
encoding = FORCED_ENCODING; |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.copyright, encoder.encode(metadata.copyright), true) |
|
|
); |
|
|
} |
|
|
else |
|
|
{ |
|
|
|
|
|
const copyright = mid.copyright.length > 0 ? mid.copyright : DEFAULT_COPYRIGHT; |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.copyright, getStringBytesZero(copyright)) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
const DBNK = new IndexedByteArray(2); |
|
|
writeLittleEndian(DBNK, bankOffset, 2); |
|
|
infoContent.push(writeRIFFOddSize(RMIDINFOChunks.bankOffset, DBNK)); |
|
|
|
|
|
if (metadata.midiEncoding !== undefined) |
|
|
{ |
|
|
infoContent.push( |
|
|
writeRIFFOddSize(RMIDINFOChunks.midiEncoding, encoder.encode(metadata.midiEncoding)) |
|
|
); |
|
|
encoding = FORCED_ENCODING; |
|
|
} |
|
|
|
|
|
infoContent.push(writeRIFFOddSize(RMIDINFOChunks.encoding, getStringBytesZero(encoding))); |
|
|
|
|
|
|
|
|
const infodata = combineArrays(infoContent); |
|
|
const rmiddata = combineArrays([ |
|
|
getStringBytes("RMID"), |
|
|
writeRIFFOddSize( |
|
|
"data", |
|
|
newMid |
|
|
), |
|
|
writeRIFFOddSize( |
|
|
"LIST", |
|
|
infodata |
|
|
), |
|
|
soundfontBinary |
|
|
]); |
|
|
SpessaSynthInfo("%cFinished!", consoleColors.info); |
|
|
SpessaSynthGroupEnd(); |
|
|
return writeRIFFOddSize( |
|
|
"RIFF", |
|
|
rmiddata |
|
|
); |
|
|
} |