diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2be09507..f21b1dc8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -53,6 +53,7 @@ set(WOOF_SOURCES m_snapshot.c m_snapshot.h m_swap.h memio.c memio.h + midifallback.c midifallback.h midifile.c midifile.h mus2mid.c mus2mid.h net_client.c net_client.h diff --git a/src/i_winmusic.c b/src/i_winmusic.c index 9da8d034..3acb51b2 100644 --- a/src/i_winmusic.c +++ b/src/i_winmusic.c @@ -19,6 +19,7 @@ #define WIN32_LEAN_AND_MEAN #include #include +#include #include #include #include @@ -30,11 +31,21 @@ #include "memio.h" #include "mus2mid.h" #include "midifile.h" +#include "midifallback.h" -int winmm_reset_type = 2; -int winmm_reset_delay = 100; -int winmm_reverb_level = 40; -int winmm_chorus_level = 0; +int winmm_reset_type = -1; +int winmm_reset_delay = 0; +int winmm_reverb_level = -1; +int winmm_chorus_level = -1; + +enum +{ + RESET_TYPE_NONE, + RESET_TYPE_GS, + RESET_TYPE_GM, + RESET_TYPE_GM2, + RESET_TYPE_XG, +}; static byte gs_reset[] = { 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7 @@ -52,14 +63,12 @@ static byte xg_system_on[] = { 0xF0, 0x43, 0x10, 0x4C, 0x00, 0x00, 0x7E, 0x00, 0xF7 }; -static byte master_volume_msg[] = { - 0xF0, 0x7F, 0x7F, 0x04, 0x01, 0x7F, 0x7F, 0xF7 -}; +static boolean use_fallback; -#define DEFAULT_MASTER_VOLUME 16383 -static unsigned int master_volume = DEFAULT_MASTER_VOLUME; +#define DEFAULT_VOLUME 100 +static int channel_volume[MIDI_CHANNELS_PER_TRACK]; static int last_volume = -1; -static float volume_factor = 1.0f; +static float volume_factor = 0.0f; static boolean update_volume = false; static DWORD timediv; @@ -72,7 +81,7 @@ static HANDLE hBufferReturnEvent; static HANDLE hExitEvent; static HANDLE hPlayerThread; -// MS GS Wavetable Syhth id +// MS GS Wavetable Synth Device ID. static int ms_gs_synth = MIDI_MAPPER; // This is a reduced Windows MIDIEVENT structure for MEVT_F_SHORT @@ -244,12 +253,11 @@ static void SendNOPMsg(int time) WriteBuffer((byte *)&native_event, sizeof(native_event_t)); } -static void SendDelayMsg(void) +static void SendDelayMsg(int time_ms) { // Convert ms to ticks (see "Standard MIDI Files 1.0" page 14). - int ticks = (float)winmm_reset_delay * 1000 * timediv / tempo + 0.5f; - - SendNOPMsg(ticks); + int time_ticks = (float)time_ms * 1000 * timediv / tempo + 0.5f; + SendNOPMsg(time_ticks); } static void UpdateTempo(int time) @@ -261,78 +269,209 @@ static void UpdateTempo(int time) WriteBuffer((byte *)&native_event, sizeof(native_event_t)); } -static void UpdateVolume(int time) +static void SendVolumeMsg(int time, int channel) { - int volume = master_volume * volume_factor + 0.5f; - master_volume_msg[5] = volume & 0x7F; - master_volume_msg[6] = (volume >> 7) & 0x7F; - SendLongMsg(time, master_volume_msg, sizeof(master_volume_msg)); + int volume = channel_volume[channel] * volume_factor + 0.5f; + SendShortMsg(time, MIDI_EVENT_CONTROLLER, channel, + MIDI_CONTROLLER_MAIN_VOLUME, volume); } -static void ResetDevice(void) +static void UpdateVolume(void) { int i; for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) { - // midiOutReset() sends "all notes off" and "reset all controllers." - SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_ALL_SOUND_OFF, 0); + SendVolumeMsg(0, i); + } +} - // Reset pitch bend sensitivity to +/- 2 semitones and 0 cents. - SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_MSB, 0); - SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_LSB, 0); - SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_DATA_ENTRY_MSB, 2); - SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_DATA_ENTRY_LSB, 0); - SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_MSB, 127); - SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_LSB, 127); +static void ResetVolume(void) +{ + int i; - // Reset channel volume and pan. - SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_MAIN_VOLUME, 100); + for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) + { + channel_volume[i] = DEFAULT_VOLUME; + SendVolumeMsg(0, i); + } +} + +static void ResetReverb(int reset_type) +{ + int i; + int reverb = winmm_reverb_level; + + if (reverb == -1 && reset_type == RESET_TYPE_NONE) + { + // No reverb specified and no SysEx reset selected. Use GM default. + reverb = 40; + } + + if (reverb > -1) + { + for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) + { + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_REVERB, reverb); + } + } +} + +static void ResetChorus(int reset_type) +{ + int i; + int chorus = winmm_chorus_level; + + if (chorus == -1 && reset_type == RESET_TYPE_NONE) + { + // No chorus specified and no SysEx reset selected. Use GM default. + chorus = 0; + } + + if (chorus > -1) + { + for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) + { + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_CHORUS, chorus); + } + } +} + +static void ResetControllers(void) +{ + int i; + + for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) + { + // Reset commonly used controllers. + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RESET_ALL_CTRLS, 0); SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_PAN, 64); - - // Reset instrument. SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_BANK_SELECT_MSB, 0); SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_BANK_SELECT_LSB, 0); SendShortMsg(0, MIDI_EVENT_PROGRAM_CHANGE, i, 0, 0); } +} - if (MidiDevice != ms_gs_synth) - { - // Send SysEx reset message. - switch (winmm_reset_type) - { - case 1: // GS Reset - SendLongMsg(0, gs_reset, sizeof(gs_reset)); - break; - - case 2: // GM System On - SendLongMsg(0, gm_system_on, sizeof(gm_system_on)); - break; - - case 3: // GM2 System On - SendLongMsg(0, gm2_system_on, sizeof(gm2_system_on)); - break; - - case 4: // XG System On - SendLongMsg(0, xg_system_on, sizeof(xg_system_on)); - break; - - default: // None - break; - } - - // Send delay after reset. - if (winmm_reset_delay > 0) - { - SendDelayMsg(); - } - } +static void ResetPitchBendSensitivity(void) +{ + int i; for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) { - // Reset reverb and chorus. - SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_REVERB, winmm_reverb_level); - SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_CHORUS, winmm_chorus_level); + // Set RPN MSB/LSB to pitch bend sensitivity. + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_LSB, 0); + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_MSB, 0); + + // Reset pitch bend sensitivity to +/- 2 semitones and 0 cents. + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_DATA_ENTRY_MSB, 2); + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_DATA_ENTRY_LSB, 0); + + // Set RPN MSB/LSB to null value after data entry. + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_LSB, 127); + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RPN_MSB, 127); + } +} + +static void ResetDevice(void) +{ + int i; + int reset_type; + + for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) + { + // Stop sound prior to reset to prevent volume spikes. + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_ALL_NOTES_OFF, 0); + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_ALL_SOUND_OFF, 0); + } + + if (MidiDevice == ms_gs_synth) + { + // MS GS Wavetable Synth lacks instrument fallback in GS mode which can + // cause wrong or silent notes (MAYhem19.wad D_DM2TTL). It also responds + // to XG System On when it should ignore it. + switch (winmm_reset_type) + { + case RESET_TYPE_NONE: + reset_type = RESET_TYPE_NONE; + break; + + case RESET_TYPE_GS: + reset_type = RESET_TYPE_GS; + break; + + default: + reset_type = RESET_TYPE_GM; + break; + } + } + else // Unknown device + { + // Most devices support GS mode. Exceptions are some older hardware and + // a few older VSTis. Some devices lack instrument fallback in GS mode. + switch (winmm_reset_type) + { + case RESET_TYPE_NONE: + case RESET_TYPE_GM: + case RESET_TYPE_GM2: + case RESET_TYPE_XG: + reset_type = winmm_reset_type; + break; + + default: + reset_type = RESET_TYPE_GS; + break; + } + } + + // Use instrument fallback in GS mode. + MIDI_ResetFallback(); + use_fallback = (reset_type == RESET_TYPE_GS); + + switch (reset_type) + { + case RESET_TYPE_NONE: + ResetControllers(); + break; + + case RESET_TYPE_GS: + SendLongMsg(0, gs_reset, sizeof(gs_reset)); + break; + + case RESET_TYPE_GM: + SendLongMsg(0, gm_system_on, sizeof(gm_system_on)); + break; + + case RESET_TYPE_GM2: + SendLongMsg(0, gm2_system_on, sizeof(gm2_system_on)); + break; + + case RESET_TYPE_XG: + SendLongMsg(0, xg_system_on, sizeof(xg_system_on)); + break; + } + + if (reset_type == RESET_TYPE_NONE || MidiDevice == ms_gs_synth) + { + // MS GS Wavetable Synth doesn't reset pitch bend sensitivity, even + // when sending a GM/GS reset, so do it manually. + ResetPitchBendSensitivity(); + } + + ResetReverb(reset_type); + ResetChorus(reset_type); + + // Reset volume (initial playback or on shutdown if no SysEx reset). + if (initial_playback || reset_type == RESET_TYPE_NONE) + { + // Scale by slider on initial playback, max on shutdown. + volume_factor = initial_playback ? volume_factor : 1.0f; + ResetVolume(); + } + + // Send delay after reset. This is for hardware devices only (e.g. SC-55). + if (winmm_reset_delay > 0) + { + SendDelayMsg(winmm_reset_delay); } } @@ -447,56 +586,18 @@ static boolean IsSysExReset(const byte *msg, int length) return false; } -static boolean IsMasterVolume(const byte *msg, int length, unsigned int *volume) -{ - // General Midi (7F 04 01 F7) - if (length == 7 && - msg[0] == 0x7F && // Universal Real Time - msg[2] == 0x04 && // Device Control - msg[3] == 0x01) // Master Volume - { - *volume = (msg[4] & 0x7F) | ((msg[5] & 0x7F) << 7); - return true; - } - - // Roland (41 42 12 40 00 04 F7) - if (length == 10 && - msg[0] == 0x41 && // Roland - msg[2] == 0x42 && // GS - msg[3] == 0x12 && // DT1 - msg[4] == 0x40 && // Address MSB - msg[5] == 0x00 && // Address - msg[6] == 0x04) // Address LSB - { - *volume = DEFAULT_MASTER_VOLUME * (msg[7] & 0x7F) / 127; - return true; - } - - // Yamaha (43 4C 00 00 04 F7) - if (length == 8 && - msg[0] == 0x43 && // Yamaha - msg[2] == 0x4C && // XG - msg[3] == 0x00 && // Address High - msg[4] == 0x00 && // Address Mid - msg[5] == 0x04) // Address Low - { - *volume = DEFAULT_MASTER_VOLUME * (msg[6] & 0x7F) / 127; - return true; - } - - return false; -} - static void SendSysExMsg(int time, const byte *data, int length) { native_event_t native_event; + boolean is_sysex_reset; const byte event_type = MIDI_EVENT_SYSEX; - if (IsMasterVolume(data, length, &master_volume)) + is_sysex_reset = IsSysExReset(data, length); + + if (is_sysex_reset && MidiDevice == ms_gs_synth) { - // Found a master volume message in the MIDI file. Take this new - // value and scale it by the user's volume slider. - UpdateVolume(time); + // Ignore SysEx reset from MIDI file for MS GS Wavetable Synth. + SendNOPMsg(time); return; } @@ -509,12 +610,17 @@ static void SendSysExMsg(int time, const byte *data, int length) WriteBuffer(data, length); WriteBufferPad(); - if (IsSysExReset(data, length)) + if (is_sysex_reset) { - // SysEx reset also resets master volume. Take the default master - // volume and scale it by the user's volume slider. - master_volume = DEFAULT_MASTER_VOLUME; - UpdateVolume(0); + // SysEx reset also resets volume. Take the default channel volumes + // and scale them by the user's volume slider. + ResetVolume(); + + // Disable instrument fallback and give priority to MIDI file. Fallback + // assumes GS (SC-55 level) and the MIDI file could be GM, GM2, XG, or + // GS (SC-88 or higher). Preserve the composer's intent. + MIDI_ResetFallback(); + use_fallback = false; } } @@ -527,11 +633,10 @@ static void FillBuffer(void) if (initial_playback) { - initial_playback = false; - - master_volume = DEFAULT_MASTER_VOLUME; ResetDevice(); StreamOut(); + + initial_playback = false; return; } @@ -539,7 +644,7 @@ static void FillBuffer(void) { update_volume = false; - UpdateVolume(0); + UpdateVolume(); StreamOut(); return; } @@ -550,6 +655,7 @@ static void FillBuffer(void) int min_time = INT_MAX; int idx = -1; int delta_time; + midi_fallback_t fallback = {FALLBACK_NONE, 0}; // Look for an event with a minimal delta time. for (i = 0; i < MIDI_NumTracks(song.file); ++i) @@ -600,6 +706,11 @@ static void FillBuffer(void) delta_time = min_time - song.current_time; + if (use_fallback) + { + MIDI_CheckFallback(event, &fallback); + } + switch ((int)event->event_type) { case MIDI_EVENT_META: @@ -607,7 +718,8 @@ static void FillBuffer(void) { case MIDI_META_SET_TEMPO: tempo = MAKE_EVT(event->data.meta.data[2], - event->data.meta.data[1], event->data.meta.data[0], 0); + event->data.meta.data[1], + event->data.meta.data[0], 0); UpdateTempo(delta_time); break; @@ -621,22 +733,63 @@ static void FillBuffer(void) break; case MIDI_EVENT_CONTROLLER: + if (event->data.channel.param1 == MIDI_CONTROLLER_MAIN_VOLUME) + { + // Adjust channel volume. + int volume = event->data.channel.param2; + channel_volume[event->data.channel.channel] = volume; + SendVolumeMsg(delta_time, event->data.channel.channel); + break; + } + else if (fallback.type == FALLBACK_BANK_LSB && + event->data.channel.param1 == MIDI_CONTROLLER_BANK_SELECT_LSB) + { + SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER, + event->data.channel.channel, + MIDI_CONTROLLER_BANK_SELECT_LSB, + fallback.value); + break; + } + // Fall through. case MIDI_EVENT_NOTE_OFF: case MIDI_EVENT_NOTE_ON: case MIDI_EVENT_AFTERTOUCH: case MIDI_EVENT_PITCH_BEND: - SendShortMsg(delta_time, event->event_type, event->data.channel.channel, - event->data.channel.param1, event->data.channel.param2); + SendShortMsg(delta_time, event->event_type, + event->data.channel.channel, + event->data.channel.param1, + event->data.channel.param2); break; case MIDI_EVENT_PROGRAM_CHANGE: + if (fallback.type == FALLBACK_BANK_MSB) + { + SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER, + event->data.channel.channel, + MIDI_CONTROLLER_BANK_SELECT_MSB, + fallback.value); + SendShortMsg(0, MIDI_EVENT_PROGRAM_CHANGE, + event->data.channel.channel, + event->data.channel.param1, 0); + break; + } + else if (fallback.type == FALLBACK_DRUMS) + { + SendShortMsg(delta_time, MIDI_EVENT_PROGRAM_CHANGE, + event->data.channel.channel, + fallback.value, 0); + break; + } + // Fall through. case MIDI_EVENT_CHAN_AFTERTOUCH: - SendShortMsg(delta_time, event->event_type, event->data.channel.channel, - event->data.channel.param1, 0); + SendShortMsg(delta_time, event->event_type, + event->data.channel.channel, + event->data.channel.param1, 0); break; case MIDI_EVENT_SYSEX: - SendSysExMsg(delta_time, event->data.sysex.data, event->data.sysex.length); + SendSysExMsg(delta_time, event->data.sysex.data, + event->data.sysex.length); song.current_time = min_time; StreamOut(); return; @@ -698,6 +851,8 @@ static boolean I_WIN_InitMusic(int device) hBufferReturnEvent = CreateEvent(NULL, FALSE, FALSE, NULL); hExitEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + MIDI_InitFallback(); + return true; } @@ -713,32 +868,28 @@ static void I_WIN_SetMusicVolume(int volume) volume_factor = sqrtf((float)volume / 15); - update_volume = true; + update_volume = (song.file != NULL); } static void I_WIN_StopSong(void *handle) { MMRESULT mmr; - if (hPlayerThread) + if (!hPlayerThread) { - SetEvent(hExitEvent); - WaitForSingleObject(hPlayerThread, INFINITE); - - CloseHandle(hPlayerThread); - hPlayerThread = NULL; + return; } + SetEvent(hExitEvent); + WaitForSingleObject(hPlayerThread, INFINITE); + CloseHandle(hPlayerThread); + hPlayerThread = NULL; + mmr = midiStreamStop(hMidiStream); if (mmr != MMSYSERR_NOERROR) { MidiError("midiStreamStop", mmr); } - mmr = midiOutReset((HMIDIOUT)hMidiStream); - if (mmr != MMSYSERR_NOERROR) - { - MidiError("midiOutReset", mmr); - } } static void I_WIN_PlaySong(void *handle, boolean looping) @@ -753,8 +904,6 @@ static void I_WIN_PlaySong(void *handle, boolean looping) initial_playback = true; - update_volume = true; - SetEvent(hBufferReturnEvent); mmr = midiStreamRestart(hMidiStream); @@ -915,28 +1064,37 @@ static void I_WIN_ShutdownMusic(void) } WaitForSingleObject(hBufferReturnEvent, INFINITE); + mmr = midiStreamStop(hMidiStream); + if (mmr != MMSYSERR_NOERROR) + { + MidiError("midiStreamStop", mmr); + } + + if (buffer.data) + { + mmr = midiOutUnprepareHeader((HMIDIOUT)hMidiStream, &MidiStreamHdr, + sizeof(MIDIHDR)); + if (mmr != MMSYSERR_NOERROR) + { + MidiError("midiOutUnprepareHeader", mmr); + } + free(buffer.data); + buffer.data = NULL; + buffer.size = 0; + buffer.position = 0; + } + mmr = midiStreamClose(hMidiStream); if (mmr != MMSYSERR_NOERROR) { MidiError("midiStreamClose", mmr); } - hMidiStream = NULL; - if (buffer.data) - { - free(buffer.data); - buffer.data = NULL; - } - buffer.size = 0; - buffer.position = 0; - CloseHandle(hBufferReturnEvent); CloseHandle(hExitEvent); } -#include - int I_WIN_DeviceList(const char* devices[], int size) { int i; diff --git a/src/m_misc.c b/src/m_misc.c index 1f1b2504..f69c8588 100644 --- a/src/m_misc.c +++ b/src/m_misc.c @@ -2325,29 +2325,29 @@ default_t defaults[] = { { "winmm_reset_type", (config_t *) &winmm_reset_type, NULL, - {2}, {0, 4}, number, ss_none, wad_no, - "SysEx reset for native MIDI (0 = None, 1 = GS Reset, 2 = GM System On (Default), 3 = GM2 System On, 4 = XG System On" + {-1}, {-1, 4}, number, ss_none, wad_no, + "SysEx reset for native MIDI (-1 = Default, 0 = None, 1 = GS, 2 = GM, 3 = GM2, 4 = XG)" }, { "winmm_reset_delay", (config_t *) &winmm_reset_delay, NULL, - {100}, {0, 2000}, number, ss_none, wad_no, + {0}, {0, 2000}, number, ss_none, wad_no, "Delay after reset for native MIDI (milliseconds)" }, - { - "winmm_chorus_level", - (config_t *) &winmm_chorus_level, NULL, - {0}, {0, 127}, number, ss_none, wad_no, - "fine tune default chorus level for native MIDI" - }, - { "winmm_reverb_level", (config_t *) &winmm_reverb_level, NULL, - {40}, {0, 127}, number, ss_none, wad_no, - "fine tune default reverb level for native MIDI" + {-1}, {-1, 127}, number, ss_none, wad_no, + "Reverb send level for native MIDI (-1 = Default, 0 = Off, 127 = Max)" + }, + + { + "winmm_chorus_level", + (config_t *) &winmm_chorus_level, NULL, + {-1}, {-1, 127}, number, ss_none, wad_no, + "Chorus send level for native MIDI (-1 = Default, 0 = Off, 127 = Max)" }, #endif diff --git a/src/midifallback.c b/src/midifallback.c new file mode 100644 index 00000000..b2132670 --- /dev/null +++ b/src/midifallback.c @@ -0,0 +1,345 @@ +// +// Copyright(C) 2022 ceski +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// DESCRIPTION: +// MIDI instrument fallback support +// + + +#include +#include "doomtype.h" +#include "midifile.h" +#include "midifallback.h" + +static const byte drums_table[128] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x18, 0x19, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, 0x28, + 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F +}; + +static byte variation[128][128]; +static byte bank_msb[MIDI_CHANNELS_PER_TRACK]; +static byte drum_map[MIDI_CHANNELS_PER_TRACK]; +static boolean selected[MIDI_CHANNELS_PER_TRACK]; + +static void UpdateDrumMap(byte *msg, unsigned int length) +{ + byte idx; + byte checksum; + + // GS allows drums on any channel using SysEx messages. + // The message format is F0 followed by: + // + // 41 10 42 12 40 15 F7 + // + // is [11-19, 10, 1A-1F] for channels 1-16. Note the position of 10. + // is 00-02 for off (normal part), drum map 1, or drum map 2. + // is checksum. + + if (length == 10 && + msg[0] == 0x41 && // Roland + msg[1] == 0x10 && // Device ID + msg[2] == 0x42 && // GS + msg[3] == 0x12 && // DT1 + msg[4] == 0x40 && // Address MSB + msg[6] == 0x15 && // Address LSB + msg[9] == 0xF7) // SysEx EOX + { + checksum = 128 - ((int)msg[4] + msg[5] + msg[6] + msg[7]) % 128; + + if (msg[8] != checksum) + { + return; + } + + if (msg[5] == 0x10) // Channel 10 + { + idx = 9; + } + else if (msg[5] < 0x1A) // Channels 1-9 + { + idx = (msg[5] & 0x0F) - 1; + } + else // Channels 11-16 + { + idx = msg[5] & 0x0F; + } + + drum_map[idx] = msg[7]; + } +} + +void MIDI_CheckFallback(midi_event_t *event, midi_fallback_t *fallback) +{ + byte idx; + unsigned int *program; + + switch ((int)event->event_type) + { + case MIDI_EVENT_SYSEX: + UpdateDrumMap(event->data.sysex.data, event->data.sysex.length); + break; + + case MIDI_EVENT_CONTROLLER: + idx = event->data.channel.channel; + switch (event->data.channel.param1) + { + case MIDI_CONTROLLER_BANK_SELECT_MSB: + bank_msb[idx] = event->data.channel.param2; + selected[idx] = false; + break; + + case MIDI_CONTROLLER_BANK_SELECT_LSB: + selected[idx] = false; + if (event->data.channel.param2 > 0) + { + // Bank select LSB > 0 not supported. This also + // preserves user's current SC-XX map. + fallback->type = FALLBACK_BANK_LSB; + fallback->value = 0; + return; + } + break; + } + break; + + case MIDI_EVENT_PROGRAM_CHANGE: + idx = event->data.channel.channel; + program = &event->data.channel.param1; + if (drum_map[idx] == 0) // Normal channel + { + if (bank_msb[idx] == 0 || variation[bank_msb[idx]][*program]) + { + // Found a capital or variation for this bank select MSB. + selected[idx] = true; + break; + } + + fallback->type = FALLBACK_BANK_MSB; + + if (!selected[idx] || bank_msb[idx] > 63) + { + // Fall to capital when no instrument has (successfully) + // selected this variation or if the variation is above 63. + fallback->value = 0; + return; + } + + // A previous instrument used this variation but it's not + // valid for the current instrument. Fall to the next valid + // "sub-capital" (next variation that is a multiple of 8). + fallback->value = (bank_msb[idx] / 8) * 8; + while (fallback->value > 0) + { + if (variation[fallback->value][*program]) + { + break; + } + fallback->value -= 8; + } + return; + } + else // Drums channel + { + if (*program != drums_table[*program]) + { + // Use drum set from drums fallback table. + // Drums 0-63 and 127: same as original SC-55 (1.00 - 1.21). + // Drums 64-126: standard drum set (0). + fallback->type = FALLBACK_DRUMS; + fallback->value = drums_table[*program]; + selected[idx] = true; + return; + } + } + break; + } + + fallback->type = FALLBACK_NONE; + fallback->value = 0xFF; +} + +void MIDI_ResetFallback(void) +{ + int i; + + for (i = 0; i < MIDI_CHANNELS_PER_TRACK; i++) + { + bank_msb[i] = 0; + drum_map[i] = 0; + selected[i] = false; + } + + // Channel 10 (index 9) is set to drum map 1 by default. + drum_map[9] = 1; +} + +void MIDI_InitFallback(void) +{ + int program; + + MIDI_ResetFallback(); + + // Capital + for (program = 0; program < 128; program++) + { + variation[0][program] = 1; + } + + // Variation #1 + variation[1][38] = 1; + variation[1][57] = 1; + variation[1][60] = 1; + variation[1][80] = 1; + variation[1][81] = 1; + variation[1][98] = 1; + variation[1][102] = 1; + variation[1][104] = 1; + variation[1][120] = 1; + variation[1][121] = 1; + variation[1][122] = 1; + variation[1][123] = 1; + variation[1][124] = 1; + variation[1][125] = 1; + variation[1][126] = 1; + variation[1][127] = 1; + + // Variation #2 + variation[2][102] = 1; + variation[2][120] = 1; + variation[2][122] = 1; + variation[2][123] = 1; + variation[2][124] = 1; + variation[2][125] = 1; + variation[2][126] = 1; + variation[2][127] = 1; + + // Variation #3 + variation[3][122] = 1; + variation[3][123] = 1; + variation[3][124] = 1; + variation[3][125] = 1; + variation[3][126] = 1; + variation[3][127] = 1; + + // Variation #4 + variation[4][122] = 1; + variation[4][124] = 1; + variation[4][125] = 1; + variation[4][126] = 1; + + // Variation #5 + variation[5][122] = 1; + variation[5][124] = 1; + variation[5][125] = 1; + variation[5][126] = 1; + + // Variation #6 + variation[6][125] = 1; + + // Variation #7 + variation[7][125] = 1; + + // Variation #8 + variation[8][0] = 1; + variation[8][1] = 1; + variation[8][2] = 1; + variation[8][3] = 1; + variation[8][4] = 1; + variation[8][5] = 1; + variation[8][6] = 1; + variation[8][11] = 1; + variation[8][12] = 1; + variation[8][14] = 1; + variation[8][16] = 1; + variation[8][17] = 1; + variation[8][19] = 1; + variation[8][21] = 1; + variation[8][24] = 1; + variation[8][25] = 1; + variation[8][26] = 1; + variation[8][27] = 1; + variation[8][28] = 1; + variation[8][30] = 1; + variation[8][31] = 1; + variation[8][38] = 1; + variation[8][39] = 1; + variation[8][40] = 1; + variation[8][48] = 1; + variation[8][50] = 1; + variation[8][61] = 1; + variation[8][62] = 1; + variation[8][63] = 1; + variation[8][80] = 1; + variation[8][81] = 1; + variation[8][107] = 1; + variation[8][115] = 1; + variation[8][116] = 1; + variation[8][117] = 1; + variation[8][118] = 1; + variation[8][125] = 1; + + // Variation #9 + variation[9][14] = 1; + variation[9][118] = 1; + variation[9][125] = 1; + + // Variation #16 + variation[16][0] = 1; + variation[16][4] = 1; + variation[16][5] = 1; + variation[16][6] = 1; + variation[16][16] = 1; + variation[16][19] = 1; + variation[16][24] = 1; + variation[16][25] = 1; + variation[16][28] = 1; + variation[16][39] = 1; + variation[16][62] = 1; + variation[16][63] = 1; + + // Variation #24 + variation[24][4] = 1; + variation[24][6] = 1; + + // Variation #32 + variation[32][16] = 1; + variation[32][17] = 1; + variation[32][24] = 1; + variation[32][52] = 1; + + // CM-64 Map (PCM) + for (program = 0; program < 64; program++) + { + variation[126][program] = 1; + } + + // CM-64 Map (LA) + for (program = 0; program < 128; program++) + { + variation[127][program] = 1; + } +} diff --git a/src/midifallback.h b/src/midifallback.h new file mode 100644 index 00000000..dc6fc28f --- /dev/null +++ b/src/midifallback.h @@ -0,0 +1,38 @@ +// +// Copyright(C) 2022 ceski +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// DESCRIPTION: +// MIDI instrument fallback support +// + + +#include "doomtype.h" +#include "midifile.h" + +typedef enum midi_fallback_type_t +{ + FALLBACK_NONE, + FALLBACK_BANK_MSB, + FALLBACK_BANK_LSB, + FALLBACK_DRUMS, +} midi_fallback_type_t; + +typedef struct midi_fallback_t +{ + midi_fallback_type_t type; + byte value; +} midi_fallback_t; + +void MIDI_CheckFallback(midi_event_t *event, midi_fallback_t *fallback); +void MIDI_ResetFallback(void); +void MIDI_InitFallback(void);