|
|
import { messageTypes, midiControllers, MIDIMessage } from "./midi_message.js"; |
|
|
import { IndexedByteArray } from "../utils/indexed_array.js"; |
|
|
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js"; |
|
|
import { consoleColors } from "../utils/other.js"; |
|
|
|
|
|
import { customControllers } from "../synthetizer/worklet_system/worklet_utilities/controller_tables.js"; |
|
|
import { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js"; |
|
|
import { isGM2On, isGMOn, isGSOn, isXGOn } from "../utils/sysex_detector.js"; |
|
|
import { isSystemXG, isXGDrums, XG_SFX_VOICE } from "../utils/xg_hacks.js"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getGsOn(ticks) |
|
|
{ |
|
|
return new MIDIMessage( |
|
|
ticks, |
|
|
messageTypes.systemExclusive, |
|
|
new IndexedByteArray([ |
|
|
0x41, |
|
|
0x10, |
|
|
0x42, |
|
|
0x12, |
|
|
0x40, |
|
|
0x00, |
|
|
0x7F, |
|
|
0x00, |
|
|
0x41, |
|
|
0xF7 |
|
|
]) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getControllerChange(channel, cc, value, ticks) |
|
|
{ |
|
|
return new MIDIMessage( |
|
|
ticks, |
|
|
messageTypes.controllerChange | (channel % 16), |
|
|
new IndexedByteArray([cc, value]) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getDrumChange(channel, ticks) |
|
|
{ |
|
|
const chanAddress = 0x10 | [1, 2, 3, 4, 5, 6, 7, 8, 0, 9, 10, 11, 12, 13, 14, 15][channel % 16]; |
|
|
|
|
|
const sysexData = [ |
|
|
0x41, |
|
|
0x10, |
|
|
0x42, |
|
|
0x12, |
|
|
0x40, |
|
|
chanAddress, |
|
|
0x15, |
|
|
0x01 |
|
|
]; |
|
|
|
|
|
|
|
|
const sum = 0x40 + chanAddress + 0x15 + 0x01; |
|
|
const checksum = 128 - (sum % 128); |
|
|
|
|
|
return new MIDIMessage( |
|
|
ticks, |
|
|
messageTypes.systemExclusive, |
|
|
new IndexedByteArray([ |
|
|
...sysexData, |
|
|
checksum, |
|
|
0xF7 |
|
|
]) |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function modifyMIDI( |
|
|
desiredProgramChanges = [], |
|
|
desiredControllerChanges = [], |
|
|
desiredChannelsToClear = [], |
|
|
desiredChannelsToTranspose = [] |
|
|
) |
|
|
{ |
|
|
const midi = this; |
|
|
SpessaSynthGroupCollapsed("%cApplying changes to the MIDI file...", consoleColors.info); |
|
|
|
|
|
SpessaSynthInfo("Desired program changes:", desiredProgramChanges); |
|
|
SpessaSynthInfo("Desired CC changes:", desiredControllerChanges); |
|
|
SpessaSynthInfo("Desired channels to clear:", desiredChannelsToClear); |
|
|
SpessaSynthInfo("Desired channels to transpose:", desiredChannelsToTranspose); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const channelsToChangeProgram = new Set(); |
|
|
desiredProgramChanges.forEach(c => |
|
|
{ |
|
|
channelsToChangeProgram.add(c.channel); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
let system = "gs"; |
|
|
let addedGs = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const eventIndexes = Array(midi.tracks.length).fill(0); |
|
|
let remainingTracks = midi.tracks.length; |
|
|
|
|
|
function findFirstEventIndex() |
|
|
{ |
|
|
let index = 0; |
|
|
let ticks = Infinity; |
|
|
midi.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 midiPorts = midi.midiPorts.slice(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const midiPortChannelOffsets = {}; |
|
|
let midiPortChannelOffset = 0; |
|
|
|
|
|
function assignMIDIPort(trackNum, port) |
|
|
{ |
|
|
|
|
|
if (midi.usedChannelsOnTrack[trackNum].size === 0) |
|
|
{ |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (midiPortChannelOffset === 0) |
|
|
{ |
|
|
midiPortChannelOffset += 16; |
|
|
midiPortChannelOffsets[port] = 0; |
|
|
} |
|
|
|
|
|
if (midiPortChannelOffsets[port] === undefined) |
|
|
{ |
|
|
midiPortChannelOffsets[port] = midiPortChannelOffset; |
|
|
midiPortChannelOffset += 16; |
|
|
} |
|
|
|
|
|
midiPorts[trackNum] = port; |
|
|
} |
|
|
|
|
|
|
|
|
midi.midiPorts.forEach((port, trackIndex) => |
|
|
{ |
|
|
assignMIDIPort(trackIndex, port); |
|
|
}); |
|
|
|
|
|
const channelsAmount = midiPortChannelOffset; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isFirstNoteOn = Array(channelsAmount).fill(true); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const coarseTranspose = Array(channelsAmount).fill(0); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fineTranspose = Array(channelsAmount).fill(0); |
|
|
desiredChannelsToTranspose.forEach(transpose => |
|
|
{ |
|
|
const coarse = Math.trunc(transpose.keyShift); |
|
|
const fine = transpose.keyShift - coarse; |
|
|
coarseTranspose[transpose.channel] = coarse; |
|
|
fineTranspose[transpose.channel] = fine; |
|
|
}); |
|
|
|
|
|
while (remainingTracks > 0) |
|
|
{ |
|
|
let trackNum = findFirstEventIndex(); |
|
|
const track = midi.tracks[trackNum]; |
|
|
if (eventIndexes[trackNum] >= track.length) |
|
|
{ |
|
|
remainingTracks--; |
|
|
continue; |
|
|
} |
|
|
const index = eventIndexes[trackNum]++; |
|
|
const e = track[index]; |
|
|
|
|
|
const deleteThisEvent = () => |
|
|
{ |
|
|
track.splice(index, 1); |
|
|
eventIndexes[trackNum]--; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const addEventBefore = (e, offset = 0) => |
|
|
{ |
|
|
track.splice(index + offset, 0, e); |
|
|
eventIndexes[trackNum]++; |
|
|
}; |
|
|
|
|
|
|
|
|
let portOffset = midiPortChannelOffsets[midiPorts[trackNum]] || 0; |
|
|
if (e.messageStatusByte === messageTypes.midiPort) |
|
|
{ |
|
|
assignMIDIPort(trackNum, e.messageData[0]); |
|
|
continue; |
|
|
} |
|
|
|
|
|
if (e.messageStatusByte <= messageTypes.sequenceSpecific && e.messageStatusByte >= messageTypes.sequenceNumber) |
|
|
{ |
|
|
continue; |
|
|
} |
|
|
const status = e.messageStatusByte & 0xF0; |
|
|
const midiChannel = e.messageStatusByte & 0xF; |
|
|
const channel = midiChannel + portOffset; |
|
|
|
|
|
if (desiredChannelsToClear.indexOf(channel) !== -1) |
|
|
{ |
|
|
deleteThisEvent(); |
|
|
continue; |
|
|
} |
|
|
switch (status) |
|
|
{ |
|
|
case messageTypes.noteOn: |
|
|
|
|
|
if (isFirstNoteOn[channel]) |
|
|
{ |
|
|
isFirstNoteOn[channel] = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
desiredControllerChanges.filter(c => c.channel === channel).forEach(change => |
|
|
{ |
|
|
const ccChange = getControllerChange( |
|
|
midiChannel, |
|
|
change.controllerNumber, |
|
|
change.controllerValue, |
|
|
e.ticks |
|
|
); |
|
|
addEventBefore(ccChange); |
|
|
}); |
|
|
const fineTune = fineTranspose[channel]; |
|
|
|
|
|
if (fineTune !== 0) |
|
|
{ |
|
|
|
|
|
|
|
|
const centsCoarse = (fineTune * 64) + 64; |
|
|
const rpnCoarse = getControllerChange(midiChannel, midiControllers.RPNMsb, 0, e.ticks); |
|
|
const rpnFine = getControllerChange(midiChannel, midiControllers.RPNLsb, 1, e.ticks); |
|
|
const dataEntryCoarse = getControllerChange( |
|
|
channel, |
|
|
midiControllers.dataEntryMsb, |
|
|
centsCoarse, |
|
|
e.ticks |
|
|
); |
|
|
const dataEntryFine = getControllerChange( |
|
|
midiChannel, |
|
|
midiControllers.lsbForControl6DataEntry, |
|
|
0, |
|
|
e.ticks |
|
|
); |
|
|
addEventBefore(dataEntryFine); |
|
|
addEventBefore(dataEntryCoarse); |
|
|
addEventBefore(rpnFine); |
|
|
addEventBefore(rpnCoarse); |
|
|
|
|
|
} |
|
|
|
|
|
if (channelsToChangeProgram.has(channel)) |
|
|
{ |
|
|
const change = desiredProgramChanges.find(c => c.channel === channel); |
|
|
let desiredBank = Math.max(0, Math.min(change.bank, 127)); |
|
|
const desiredProgram = change.program; |
|
|
SpessaSynthInfo( |
|
|
`%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}%c. Track num: %c${trackNum}`, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized, |
|
|
consoleColors.info, |
|
|
consoleColors.recognized |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const programChange = new MIDIMessage( |
|
|
e.ticks, |
|
|
messageTypes.programChange | midiChannel, |
|
|
new IndexedByteArray([ |
|
|
desiredProgram |
|
|
]) |
|
|
); |
|
|
addEventBefore(programChange); |
|
|
|
|
|
const addBank = (isLSB, v) => |
|
|
{ |
|
|
const bankChange = getControllerChange( |
|
|
midiChannel, |
|
|
isLSB ? midiControllers.lsbForControl0BankSelect : midiControllers.bankSelect, |
|
|
v, |
|
|
e.ticks |
|
|
); |
|
|
addEventBefore(bankChange); |
|
|
}; |
|
|
|
|
|
|
|
|
if (isSystemXG(system)) |
|
|
{ |
|
|
|
|
|
if (change.isDrum) |
|
|
{ |
|
|
SpessaSynthInfo( |
|
|
`%cAdding XG Drum change on track %c${trackNum}`, |
|
|
consoleColors.recognized, |
|
|
consoleColors.value |
|
|
); |
|
|
addBank(false, isXGDrums(desiredBank) ? desiredBank : 127); |
|
|
addBank(true, 0); |
|
|
} |
|
|
else |
|
|
{ |
|
|
|
|
|
if (desiredBank === XG_SFX_VOICE) |
|
|
{ |
|
|
addBank(false, XG_SFX_VOICE); |
|
|
addBank(true, 0); |
|
|
} |
|
|
else |
|
|
{ |
|
|
|
|
|
addBank(false, 0); |
|
|
addBank(true, desiredBank); |
|
|
} |
|
|
} |
|
|
} |
|
|
else |
|
|
{ |
|
|
|
|
|
addBank(false, desiredBank); |
|
|
|
|
|
if (change.isDrum && midiChannel !== DEFAULT_PERCUSSION) |
|
|
{ |
|
|
|
|
|
SpessaSynthInfo( |
|
|
`%cAdding GS Drum change on track %c${trackNum}`, |
|
|
consoleColors.recognized, |
|
|
consoleColors.value |
|
|
); |
|
|
addEventBefore(getDrumChange(midiChannel, e.ticks)); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
e.messageData[0] += coarseTranspose[channel]; |
|
|
break; |
|
|
|
|
|
case messageTypes.noteOff: |
|
|
e.messageData[0] += coarseTranspose[channel]; |
|
|
break; |
|
|
|
|
|
case messageTypes.programChange: |
|
|
|
|
|
if (channelsToChangeProgram.has(channel)) |
|
|
{ |
|
|
|
|
|
deleteThisEvent(); |
|
|
continue; |
|
|
} |
|
|
break; |
|
|
|
|
|
case messageTypes.controllerChange: |
|
|
const ccNum = e.messageData[0]; |
|
|
const changes = desiredControllerChanges.find(c => c.channel === channel && ccNum === c.controllerNumber); |
|
|
if (changes !== undefined) |
|
|
{ |
|
|
|
|
|
deleteThisEvent(); |
|
|
continue; |
|
|
} |
|
|
|
|
|
if (ccNum === midiControllers.bankSelect || ccNum === midiControllers.lsbForControl0BankSelect) |
|
|
{ |
|
|
if (channelsToChangeProgram.has(channel)) |
|
|
{ |
|
|
|
|
|
deleteThisEvent(); |
|
|
continue; |
|
|
} |
|
|
} |
|
|
break; |
|
|
|
|
|
case messageTypes.systemExclusive: |
|
|
|
|
|
if (isXGOn(e)) |
|
|
{ |
|
|
SpessaSynthInfo("%cXG system on detected", consoleColors.info); |
|
|
system = "xg"; |
|
|
addedGs = true; |
|
|
} |
|
|
else |
|
|
|
|
|
if ( |
|
|
e.messageData[0] === 0x43 |
|
|
&& e.messageData[2] === 0x4C |
|
|
&& e.messageData[3] === 0x08 |
|
|
&& e.messageData[5] === 0x03 |
|
|
) |
|
|
{ |
|
|
|
|
|
if (channelsToChangeProgram.has(e.messageData[4] + portOffset)) |
|
|
{ |
|
|
|
|
|
deleteThisEvent(); |
|
|
} |
|
|
} |
|
|
else |
|
|
|
|
|
if (isGSOn(e)) |
|
|
{ |
|
|
|
|
|
addedGs = true; |
|
|
SpessaSynthInfo( |
|
|
"%cGS on detected!", |
|
|
consoleColors.recognized |
|
|
); |
|
|
break; |
|
|
} |
|
|
else |
|
|
|
|
|
if (isGMOn(e) || isGM2On(e)) |
|
|
{ |
|
|
|
|
|
SpessaSynthInfo( |
|
|
"%cGM/2 on detected, removing!", |
|
|
consoleColors.info |
|
|
); |
|
|
deleteThisEvent(); |
|
|
addedGs = false; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (!addedGs && desiredProgramChanges.length > 0) |
|
|
{ |
|
|
|
|
|
let index = 0; |
|
|
if (midi.tracks[0][0].messageStatusByte === messageTypes.trackName) |
|
|
{ |
|
|
index++; |
|
|
} |
|
|
midi.tracks[0].splice(index, 0, getGsOn(0)); |
|
|
SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info); |
|
|
} |
|
|
this.flush(); |
|
|
SpessaSynthGroupEnd(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function applySnapshotToMIDI(snapshot) |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const channelsToTranspose = []; |
|
|
|
|
|
|
|
|
|
|
|
const channelsToClear = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const programChanges = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const controllerChanges = []; |
|
|
snapshot.channelSnapshots.forEach((channel, channelNumber) => |
|
|
{ |
|
|
if (channel.isMuted) |
|
|
{ |
|
|
channelsToClear.push(channelNumber); |
|
|
return; |
|
|
} |
|
|
const transposeFloat = channel.channelTransposeKeyShift + channel.customControllers[customControllers.channelTransposeFine] / 100; |
|
|
if (transposeFloat !== 0) |
|
|
{ |
|
|
channelsToTranspose.push({ |
|
|
channel: channelNumber, |
|
|
keyShift: transposeFloat |
|
|
}); |
|
|
} |
|
|
if (channel.lockPreset) |
|
|
{ |
|
|
programChanges.push({ |
|
|
channel: channelNumber, |
|
|
program: channel.program, |
|
|
bank: channel.bank, |
|
|
isDrum: channel.drumChannel |
|
|
}); |
|
|
} |
|
|
|
|
|
channel.lockedControllers.forEach((l, ccNumber) => |
|
|
{ |
|
|
if (!l || ccNumber > 127 || ccNumber === midiControllers.bankSelect) |
|
|
{ |
|
|
return; |
|
|
} |
|
|
const targetValue = channel.midiControllers[ccNumber] >> 7; |
|
|
controllerChanges.push({ |
|
|
channel: channelNumber, |
|
|
controllerNumber: ccNumber, |
|
|
controllerValue: targetValue |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
this.modifyMIDI(programChanges, controllerChanges, channelsToClear, channelsToTranspose); |
|
|
} |