UNPKG

spessasynth_core

Version:

MIDI and SoundFont2/DLS library with no compromises

256 lines (242 loc) 8.87 kB
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../../utils/loggin.js"; import { consoleColors } from "../../utils/other.js"; import { messageTypes, midiControllers } from "../midi_message.js"; import { DEFAULT_PERCUSSION } from "../../synthetizer/synth_constants.js"; import { chooseBank, isSystemXG, parseBankSelect } from "../../utils/xg_hacks.js"; import { isGSDrumsOn, isXGOn } from "../../utils/sysex_detector.js"; import { SoundFontManager } from "../../synthetizer/audio_engine/engine_components/soundfont_manager.js"; /** * Gets the used programs and keys for this MIDI file with a given sound bank * @this {BasicMIDI} * @param soundfont {SoundFontManager|BasicSoundBank} - the sound bank * @returns {Record<string, Set<string>>} Record<bank:program, Set<key-velocity>> */ export function getUsedProgramsAndKeys(soundfont) { const mid = this; SpessaSynthGroupCollapsed( "%cSearching for all used programs and keys...", consoleColors.info ); // Find every bank:program combo and every key:velocity for each. Make sure to care about ports and drums const channelsAmount = 16 + mid.midiPortChannelOffsets.reduce((max, cur) => cur > max ? cur : max); /** * @type {{program: number, bank: number, bankLSB: number, drums: boolean, string: string, actualBank: number}[]} */ const channelPresets = []; for (let i = 0; i < channelsAmount; i++) { const bank = i % 16 === DEFAULT_PERCUSSION ? 128 : 0; channelPresets.push({ program: 0, bank: bank, bankLSB: 0, actualBank: bank, drums: i % 16 === DEFAULT_PERCUSSION, // drums appear on 9 every 16 channels, string: `${bank}:0` }); } // check for xg let system = "gs"; function updateString(ch) { const bank = chooseBank(ch.bank, ch.bankLSB, ch.drums, isSystemXG(system)); // check if this exists in the soundfont let existsBank, existsProgram; if (soundfont instanceof SoundFontManager) { /** * @type {{preset: BasicPreset, bankOffset: number}} */ let exists = soundfont.getPreset(bank, ch.program, isSystemXG(system)); existsBank = exists.preset.bank + exists.bankOffset; existsProgram = exists.preset.program; } else { /** * @type {BasicPreset} */ let exists = soundfont.getPreset(bank, ch.program, isSystemXG(system)); existsBank = exists.bank; existsProgram = exists.program; } ch.actualBank = existsBank; ch.program = existsProgram; ch.string = ch.actualBank + ":" + ch.program; if (!usedProgramsAndKeys[ch.string]) { SpessaSynthInfo( `%cDetected a new preset: %c${ch.string}`, consoleColors.info, consoleColors.recognized ); usedProgramsAndKeys[ch.string] = new Set(); } } /** * find all programs used and key-velocity combos in them * bank:program each has a set of midiNote-velocity * @type {Record<string, Set<string>>} */ const usedProgramsAndKeys = {}; /** * indexes for tracks * @type {number[]} */ 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 = mid.midiPorts.slice(); // initialize channelPresets.forEach(c => { updateString(c); }); while (remainingTracks > 0) { let trackNum = findFirstEventIndex(); const track = mid.tracks[trackNum]; if (eventIndexes[trackNum] >= track.length) { remainingTracks--; continue; } const event = track[eventIndexes[trackNum]]; eventIndexes[trackNum]++; if (event.messageStatusByte === messageTypes.midiPort) { ports[trackNum] = event.messageData[0]; continue; } const status = event.messageStatusByte & 0xF0; if ( status !== messageTypes.noteOn && status !== messageTypes.controllerChange && status !== messageTypes.programChange && status !== messageTypes.systemExclusive ) { continue; } const channel = (event.messageStatusByte & 0xF) + mid.midiPortChannelOffsets[ports[trackNum]] || 0; let ch = channelPresets[channel]; switch (status) { case messageTypes.programChange: ch.program = event.messageData[0]; updateString(ch); break; case messageTypes.controllerChange: const isLSB = event.messageData[0] === midiControllers.lsbForControl0BankSelect; if (event.messageData[0] !== midiControllers.bankSelect && !isLSB) { // we only care about bank select continue; } if (system === "gs" && ch.drums) { // gs drums get changed via sysex, ignore here continue; } const bank = event.messageData[1]; if (isLSB) { ch.bankLSB = bank; } else { ch.bank = bank; } // interpret the bank const intepretation = parseBankSelect( ch.bank, bank, system, isLSB, ch.drums, channel ); switch (intepretation.drumsStatus) { case 0: // no change break; case 1: // drums changed to off // drum change is a program change ch.drums = false; updateString(ch); break; case 2: // drums changed to on // drum change is a program change ch.drums = true; updateString(ch); break; } // do not update the data, bank change doesn't change the preset break; case messageTypes.noteOn: if (event.messageData[1] === 0) { // that's a note off continue; } usedProgramsAndKeys[ch.string].add(`${event.messageData[0]}-${event.messageData[1]}`); break; case messageTypes.systemExclusive: // check for drum sysex if (!isGSDrumsOn(event)) { // check for XG if (isXGOn(event)) { system = "xg"; SpessaSynthInfo( "%cXG on detected!", consoleColors.recognized ); } continue; } const sysexChannel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][event.messageData[5] & 0x0F] + mid.midiPortChannelOffsets[ports[trackNum]]; const isDrum = !!(event.messageData[7] > 0 && event.messageData[5] >> 4); ch = channelPresets[sysexChannel]; ch.drums = isDrum; updateString(ch); break; } } for (const key of Object.keys(usedProgramsAndKeys)) { if (usedProgramsAndKeys[key].size === 0) { SpessaSynthInfo( `%cDetected change but no keys for %c${key}`, consoleColors.info, consoleColors.value ); delete usedProgramsAndKeys[key]; } } SpessaSynthGroupEnd(); return usedProgramsAndKeys; }