From e88086684c85f4a5fac0d28e61aa034b3db5353b Mon Sep 17 00:00:00 2001 From: ceski <56656010+ceski-1@users.noreply.github.com> Date: Sat, 3 Dec 2022 01:43:11 -0800 Subject: [PATCH] winmidi: Enable SysEx messages from MIDI files (#822) * winmidi: Enable SysEx messages from MIDI files * Remove unused variable * Discard incomplete SysEx messages when a song loops * Write all 0xF0 SysEx messages, ignore all 0xF7 SysEx messages * Remove extra delay after reset * Remove `winmm_allow_sysex` * Don't use compound literal --- src/i_winmusic.c | 384 +++++++++++++++++++++++++++++++++-------------- src/m_misc.c | 8 - 2 files changed, 273 insertions(+), 119 deletions(-) diff --git a/src/i_winmusic.c b/src/i_winmusic.c index 7be37d3d..c1c8727a 100644 --- a/src/i_winmusic.c +++ b/src/i_winmusic.c @@ -34,7 +34,6 @@ int winmm_reset_type = 2; int winmm_reset_delay = 100; int winmm_reverb_level = 40; int winmm_chorus_level = 0; -int winmm_allow_sysex = 0; static byte gs_reset[] = { 0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7 @@ -52,8 +51,18 @@ static byte xg_system_on[] = { 0xF0, 0x43, 0x10, 0x4C, 0x00, 0x00, 0x7E, 0x00, 0xF7 }; -static int timediv; -static int tempo; +static byte master_volume_msg[] = { + 0xF0, 0x7F, 0x7F, 0x04, 0x01, 0x7F, 0x7F, 0xF7 +}; + +#define DEFAULT_MASTER_VOLUME 16383 +static unsigned int master_volume = DEFAULT_MASTER_VOLUME; +static int last_volume = -1; +static float volume_factor = 1.0f; +static boolean update_volume = false; + +static DWORD timediv; +static DWORD tempo; static HMIDISTRM hMidiStream; static MIDIHDR MidiStreamHdr; @@ -88,14 +97,6 @@ typedef struct static win_midi_song_t song; -static float volume_factor = 1.0f; - -static boolean update_volume = false; - -// Save the last volume for each MIDI channel. - -static int channel_volume[MIDI_CHANNELS_PER_TRACK]; - #define BUFFER_INITIAL_SIZE 1024 typedef struct @@ -111,7 +112,7 @@ static buffer_t buffer; #define STREAM_MAX_EVENTS 4 -#define MAKE_EVT(a, b, c, d) ((uint32_t)((a) | ((b) << 8) | ((c) << 16) | ((d) << 24))) +#define MAKE_EVT(a, b, c, d) ((DWORD)((a) | ((b) << 8) | ((c) << 16) | ((d) << 24))) #define PADDED_SIZE(x) (((x) + sizeof(DWORD) - 1) & ~(sizeof(DWORD) - 1)) @@ -209,19 +210,19 @@ static void StreamOut(void) } } -static void SendShortMsg(int type, int param1, int param2) +static void SendShortMsg(int time, int status, int channel, int param1, int param2) { native_event_t native_event; - native_event.dwDeltaTime = 0; + native_event.dwDeltaTime = time; native_event.dwStreamID = 0; - native_event.dwEvent = MAKE_EVT(type, param1, param2, MEVT_SHORTMSG); + native_event.dwEvent = MAKE_EVT(status | channel, param1, param2, MEVT_SHORTMSG); WriteBuffer((byte *)&native_event, sizeof(native_event_t)); } -static void SendLongMsg(const byte *ptr, int length) +static void SendLongMsg(int time, const byte *ptr, int length) { native_event_t native_event; - native_event.dwDeltaTime = 0; + native_event.dwDeltaTime = time; native_event.dwStreamID = 0; native_event.dwEvent = MAKE_EVT(length, 0, 0, MEVT_LONGMSG); WriteBuffer((byte *)&native_event, sizeof(native_event_t)); @@ -229,8 +230,10 @@ static void SendLongMsg(const byte *ptr, int length) WriteBufferPad(); } -static void SendDelayMsg(int ticks) +static void SendDelayMsg(void) { + // Convert ms to ticks (see "Standard MIDI Files 1.0" page 14). + int ticks = (float)winmm_reset_delay * 1000 * timediv / tempo + 0.5f; native_event_t native_event; native_event.dwDeltaTime = ticks; native_event.dwStreamID = 0; @@ -238,6 +241,23 @@ static void SendDelayMsg(int ticks) WriteBuffer((byte *)&native_event, sizeof(native_event_t)); } +static void UpdateTempo(int time) +{ + native_event_t native_event; + native_event.dwDeltaTime = time; + native_event.dwStreamID = 0; + native_event.dwEvent = MAKE_EVT(tempo, 0, 0, MEVT_TEMPO); + WriteBuffer((byte *)&native_event, sizeof(native_event_t)); +} + +static void UpdateVolume(int time) +{ + 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)); +} + static void ResetDevice(void) { int i; @@ -245,43 +265,43 @@ static void ResetDevice(void) for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) { // midiOutReset() sends "all notes off" and "reset all controllers." - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_ALL_SOUND_OFF, 0); + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_ALL_SOUND_OFF, 0); // Reset pitch bend sensitivity to +/- 2 semitones and 0 cents. - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_RPN_MSB, 0); - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_RPN_LSB, 0); - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_DATA_ENTRY_MSB, 2); - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_DATA_ENTRY_LSB, 0); - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_RPN_MSB, 127); - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_RPN_LSB, 127); + 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); // Reset channel volume and pan. - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_MAIN_VOLUME, 100); - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_PAN, 64); + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_MAIN_VOLUME, 100); + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_PAN, 64); // Reset instrument. - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_BANK_SELECT_MSB, 0); - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_BANK_SELECT_LSB, 0); - SendShortMsg(MIDI_EVENT_PROGRAM_CHANGE | i, 0, 0); + 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); } // Send SysEx reset message. switch (winmm_reset_type) { case 1: // GS Reset - SendLongMsg(gs_reset, sizeof(gs_reset)); + SendLongMsg(0, gs_reset, sizeof(gs_reset)); break; case 2: // GM System On - SendLongMsg(gm_system_on, sizeof(gm_system_on)); + SendLongMsg(0, gm_system_on, sizeof(gm_system_on)); break; case 3: // GM2 System On - SendLongMsg(gm2_system_on, sizeof(gm2_system_on)); + SendLongMsg(0, gm2_system_on, sizeof(gm2_system_on)); break; case 4: // XG System On - SendLongMsg(xg_system_on, sizeof(xg_system_on)); + SendLongMsg(0, xg_system_on, sizeof(xg_system_on)); break; default: // None @@ -291,17 +311,196 @@ static void ResetDevice(void) // Send delay after reset. if (winmm_reset_delay > 0) { - // Convert ms to ticks (see "Standard MIDI Files 1.0" page 14). - int ticks = (float)winmm_reset_delay * 1000 * timediv / tempo + 0.5f; - - SendDelayMsg(ticks); + SendDelayMsg(); } for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) { // Reset reverb and chorus. - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_REVERB, winmm_reverb_level); - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_CHORUS, winmm_chorus_level); + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_REVERB, winmm_reverb_level); + SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_CHORUS, winmm_chorus_level); + } +} + +static boolean IsSysExReset(const byte *msg, int length) +{ + if (length < 5) + { + return false; + } + + switch (msg[0]) + { + case 0x41: // Roland + switch (msg[2]) + { + case 0x42: // GS + switch (msg[3]) + { + case 0x12: // DT1 + if (length == 10 && + msg[4] == 0x00 && // Address MSB + msg[5] == 0x00 && // Address + msg[6] == 0x7F && // Address LSB + ((msg[7] == 0x00 && // Data (MODE-1) + msg[8] == 0x01) || // Checksum (MODE-1) + (msg[7] == 0x01 && // Data (MODE-2) + msg[8] == 0x00))) // Checksum (MODE-2) + { + // SC-88 System Mode Set + // 41 42 12 00 00 7F 00 01 F7 (MODE-1) + // 41 42 12 00 00 7F 01 00 F7 (MODE-2) + return true; + } + else if (length == 10 && + msg[4] == 0x40 && // Address MSB + msg[5] == 0x00 && // Address + msg[6] == 0x7F && // Address LSB + msg[7] == 0x00 && // Data (GS Reset) + msg[8] == 0x41) // Checksum + { + // GS Reset + // 41 42 12 40 00 7F 00 41 F7 + return true; + } + break; + } + break; + } + break; + + case 0x43: // Yamaha + switch (msg[2]) + { + case 0x2B: // TG300 + if (length == 9 && + msg[3] == 0x00 && // Start Address b20 - b14 + msg[4] == 0x00 && // Start Address b13 - b7 + msg[5] == 0x7F && // Start Address b6 - b0 + msg[6] == 0x00 && // Data + msg[7] == 0x01) // Checksum + { + // TG300 All Parameter Reset + // 43 2B 00 00 7F 00 01 F7 + return true; + } + break; + + case 0x4C: // XG + if (length == 8 && + msg[3] == 0x00 && // Address High + msg[4] == 0x00 && // Address Mid + msg[5] == 0x7E && // Address Low + msg[6] == 0x00) // Data + { + // XG System On + // 43 4C 00 00 7E 00 F7 + return true; + } + else if (length == 8 && + msg[3] == 0x00 && // Address High + msg[4] == 0x00 && // Address Mid + msg[5] == 0x7F && // Address Low + msg[6] == 0x00) // Data + { + // XG All Parameter Reset + // 43 4C 00 00 7F 00 F7 + return true; + } + break; + } + break; + + case 0x7E: // Universal Non-Real Time + switch (msg[2]) + { + case 0x09: // General Midi + if (length == 5 && + (msg[3] == 0x01 || // GM System On + msg[3] == 0x02 || // GM System Off + msg[3] == 0x03)) // GM2 System On + { + // GM System On/Off, GM2 System On + // 7E 09 01 F7 + // 7E 09 02 F7 + // 7E 09 03 F7 + return true; + } + break; + } + break; + } + 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; + const byte event_type = MIDI_EVENT_SYSEX; + + if (IsMasterVolume(data, length, &master_volume)) + { + // Found a master volume message in the MIDI file. Take this new + // value and scale it by the user's volume slider. + UpdateVolume(time); + return; + } + + // Send the SysEx message. + native_event.dwDeltaTime = time; + native_event.dwStreamID = 0; + native_event.dwEvent = MAKE_EVT(length + sizeof(byte), 0, 0, MEVT_LONGMSG); + WriteBuffer((byte *)&native_event, sizeof(native_event_t)); + WriteBuffer(&event_type, sizeof(byte)); + WriteBuffer(data, length); + WriteBufferPad(); + + if (IsSysExReset(data, length)) + { + // 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); } } @@ -316,29 +515,27 @@ static void FillBuffer(void) { initial_playback = false; + master_volume = DEFAULT_MASTER_VOLUME; ResetDevice(); + StreamOut(); + return; } - // If the volume has changed, stick those events at the start of this buffer. if (update_volume) { update_volume = false; - for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) - { - int volume = (int)((float)channel_volume[i] * volume_factor + 0.5f); - - SendShortMsg(MIDI_EVENT_CONTROLLER | i, MIDI_CONTROLLER_MAIN_VOLUME, - volume); - } + UpdateVolume(0); + StreamOut(); + return; } for (num_events = 0; num_events < STREAM_MAX_EVENTS; ) { midi_event_t *event; - DWORD data = 0; int min_time = INT_MAX; int idx = -1; + int delta_time; // Look for an event with a minimal delta time. for (i = 0; i < MIDI_NumTracks(song.file); ++i) @@ -385,6 +582,8 @@ static void FillBuffer(void) continue; } + delta_time = min_time - song.current_time; + switch ((int)event->event_type) { case MIDI_EVENT_META: @@ -392,77 +591,38 @@ static void FillBuffer(void) { tempo = MAKE_EVT(event->data.meta.data[2], event->data.meta.data[1], event->data.meta.data[0], 0); - data = MAKE_EVT(tempo, 0, 0, MEVT_TEMPO); - } - break; - - case MIDI_EVENT_CONTROLLER: - // Adjust volume as needed. - if (event->data.channel.param1 == MIDI_CONTROLLER_MAIN_VOLUME) - { - int volume = event->data.channel.param2; - - channel_volume[event->data.channel.channel] = volume; - - volume = (int)((float)volume * volume_factor + 0.5f); - - data = MAKE_EVT(event->event_type | event->data.channel.channel, - event->data.channel.param1, (volume & 0x7F), - MEVT_SHORTMSG); - + UpdateTempo(delta_time); break; } - // Fall through. + continue; + + case MIDI_EVENT_CONTROLLER: case MIDI_EVENT_NOTE_OFF: case MIDI_EVENT_NOTE_ON: case MIDI_EVENT_AFTERTOUCH: case MIDI_EVENT_PITCH_BEND: - data = MAKE_EVT(event->event_type | event->data.channel.channel, - event->data.channel.param1, event->data.channel.param2, - MEVT_SHORTMSG); + SendShortMsg(delta_time, event->event_type, event->data.channel.channel, + event->data.channel.param1, event->data.channel.param2); break; case MIDI_EVENT_PROGRAM_CHANGE: case MIDI_EVENT_CHAN_AFTERTOUCH: - data = MAKE_EVT(event->event_type | event->data.channel.channel, - event->data.channel.param1, 0, MEVT_SHORTMSG); + SendShortMsg(delta_time, event->event_type, event->data.channel.channel, + event->data.channel.param1, 0); break; case MIDI_EVENT_SYSEX: - case MIDI_EVENT_SYSEX_SPLIT: - if (winmm_allow_sysex) - { - uint32_t length = event->data.sysex.length + sizeof(byte); - data = MAKE_EVT(length, 0, 0, MEVT_LONGMSG); - } - else - { - // Preserve timing with a NOP. - data = MAKE_EVT(0, 0, 0, MEVT_NOP); - } - break; + SendSysExMsg(delta_time, event->data.sysex.data, event->data.sysex.length); + song.current_time = min_time; + StreamOut(); + return; + + default: + continue; } - if (data) - { - native_event_t native_event; - - native_event.dwDeltaTime = min_time - song.current_time; - native_event.dwStreamID = 0; - native_event.dwEvent = data; - WriteBuffer((byte *)&native_event, sizeof(native_event_t)); - - if (winmm_allow_sysex && (event->event_type == MIDI_EVENT_SYSEX || - event->event_type == MIDI_EVENT_SYSEX_SPLIT)) - { - WriteBuffer((byte *)&event->event_type, sizeof(byte)); - WriteBuffer(event->data.sysex.data, event->data.sysex.length); - WriteBufferPad(); - } - - num_events++; - song.current_time = min_time; - } + num_events++; + song.current_time = min_time; } if (num_events) @@ -518,6 +678,14 @@ static boolean I_WIN_InitMusic(void) static void I_WIN_SetMusicVolume(int volume) { + if (last_volume == volume) + { + // Ignore holding key down in volume menu. + return; + } + + last_volume = volume; + volume_factor = sqrtf((float)volume / 15); update_volume = true; @@ -637,12 +805,6 @@ static void *I_WIN_RegisterSong(void *data, int len) return NULL; } - // Initialize channels volume. - for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i) - { - channel_volume[i] = 100; - } - prop_timediv.cbStruct = sizeof(MIDIPROPTIMEDIV); prop_timediv.dwTimeDiv = MIDI_GetFileTimeDivision(song.file); mmr = midiStreamProperty(hMidiStream, (LPBYTE)&prop_timediv, diff --git a/src/m_misc.c b/src/m_misc.c index 489be78e..0acf7240 100644 --- a/src/m_misc.c +++ b/src/m_misc.c @@ -99,7 +99,6 @@ extern int winmm_reset_type; extern int winmm_reset_delay; extern int winmm_reverb_level; extern int winmm_chorus_level; -extern int winmm_allow_sysex; #endif extern boolean demobar; extern boolean smoothlight; @@ -2349,13 +2348,6 @@ default_t defaults[] = { {40}, {0, 127}, number, ss_none, wad_no, "fine tune default reverb level for native MIDI" }, - - { - "winmm_allow_sysex", - (config_t *) &winmm_allow_sysex, NULL, - {0}, {0, 1}, number, ss_none, wad_no, - "Allow SysEx messages from MIDI files for native MIDI" - }, #endif // [FG] uncapped rendering frame rate