shithub: choc

Download patch

ref: 725c9b8ab73131f2d8a527e90ebc2938b6093948
parent: d2efe0521e570178ffabac2379a12f5dc443aa0f
author: Roman Fomin <[email protected]>
date: Wed Dec 14 18:45:28 EST 2022

win midi: Support SysEx, proper device reset and other updates (#1558)

* win midi: Support SysEx, proper device reset and other updates

* Make music_win_module, clean up i_sdlmusic.c

* Add support of SysEx messages.

* Correctly reset MIDI devices with SysEx messages (by ceski).

* Implement a "capital tone fallback" emulation (by ceski).

* Fix looping timing, various fixes (by ceski).

* Add full support of EMIDI, loop points (Final Fantasy and RPG Maker)
  (by ceski).

* Fix Linux build

* Fix Makefile

* Add new config variables

* Add missed entries to music_win_devices[] (for config compatibility?)

* Remove unused function

It was added for the initial version of i_winmusc.c

* Add missed `const` in i_winmusic.c

* Delete file immediately after use.

* Add `const` and header guards to midifallback.*

--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -65,7 +65,8 @@
     i_timer.c           i_timer.h
     i_video.c           i_video.h
     i_videohr.c         i_videohr.h
-    i_winmusic.c        i_winmusic.h
+    i_winmusic.c
+    midifallback.c      midifallback.h
     midifile.c          midifile.h
     mus2mid.c           mus2mid.h
     m_bbox.c            m_bbox.h
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -81,7 +81,8 @@
 i_timer.c            i_timer.h             \
 i_video.c            i_video.h             \
 i_videohr.c          i_videohr.h           \
-i_winmusic.c         i_winmusic.h          \
+i_winmusic.c                               \
+midifallback.c       midifallback.h        \
 midifile.c           midifile.h            \
 mus2mid.c            mus2mid.h             \
 m_bbox.c             m_bbox.h              \
--- a/src/i_oplmusic.c
+++ b/src/i_oplmusic.c
@@ -1197,7 +1197,7 @@
 
     switch (controller)
     {
-        case MIDI_CONTROLLER_MAIN_VOLUME:
+        case MIDI_CONTROLLER_VOLUME_MSB:
             SetChannelVolume(channel, param, true);
             break;
 
--- a/src/i_sdlmusic.c
+++ b/src/i_sdlmusic.c
@@ -25,8 +25,6 @@
 #include "SDL.h"
 #include "SDL_mixer.h"
 
-#include "i_winmusic.h"
-
 #include "config.h"
 #include "doomtype.h"
 #include "memio.h"
@@ -138,8 +136,6 @@
 
 static boolean sdl_was_initialized = false;
 
-static boolean win_midi_stream_opened = false;
-
 static boolean musicpaused = false;
 static int current_music_volume;
 
@@ -161,13 +157,6 @@
 {
     if (music_initialized)
     {
-#if defined(_WIN32)
-        if (win_midi_stream_opened)
-        {
-            I_WIN_ShutdownMusic();
-            win_midi_stream_opened = false;
-        }
-#endif
         Mix_HaltMusic();
         music_initialized = false;
 
@@ -284,15 +273,6 @@
         Mix_SetMusicCMD(snd_musiccmd);
     }
 
-#if defined(_WIN32)
-    // Don't enable it for GUS or Fluidsynth, since they handle their own volume
-    // just fine.
-    if (snd_musicdevice != SNDDEVICE_GUS && !fluidsynth_sf_is_set)
-    {
-        win_midi_stream_opened = I_WIN_InitMusic();
-    }
-#endif
-
     return music_initialized;
 }
 
@@ -314,9 +294,6 @@
         vol = (current_music_volume * MIX_MAX_VOLUME) / 127;
     }
 
-#if defined(_WIN32)
-    I_WIN_SetMusicVolume(vol);
-#endif
     Mix_VolumeMusic(vol);
 }
 
@@ -341,7 +318,7 @@
         return;
     }
 
-    if (handle == NULL && !win_midi_stream_opened)
+    if (handle == NULL)
     {
         return;
     }
@@ -355,16 +332,7 @@
         loops = 1;
     }
 
-#if defined(_WIN32)
-    if (win_midi_stream_opened)
-    {
-        I_WIN_PlaySong(looping);
-    }
-    else
-#endif
-    {
-        Mix_PlayMusic((Mix_Music *) handle, loops);
-    }
+    Mix_PlayMusic((Mix_Music *) handle, loops);
 }
 
 static void I_SDL_PauseSong(void)
@@ -374,18 +342,9 @@
         return;
     }
 
-#if defined(_WIN32)
-    if (win_midi_stream_opened)
-    {
-        I_WIN_PauseSong();
-    }
-    else
-#endif
-    {
-        musicpaused = true;
+    musicpaused = true;
 
-        UpdateMusicVolume();
-    }
+    UpdateMusicVolume();
 }
 
 static void I_SDL_ResumeSong(void)
@@ -395,18 +354,9 @@
         return;
     }
 
-#if defined(_WIN32)
-    if (win_midi_stream_opened)
-    {
-        I_WIN_ResumeSong();
-    }
-    else
-#endif
-    {
-        musicpaused = false;
+    musicpaused = false;
 
-        UpdateMusicVolume();
-    }
+    UpdateMusicVolume();
 }
 
 static void I_SDL_StopSong(void)
@@ -416,16 +366,7 @@
         return;
     }
 
-#if defined(_WIN32)
-    if (win_midi_stream_opened)
-    {
-        I_WIN_StopSong();
-    }
-    else
-#endif
-    {
-        Mix_HaltMusic();
-    }
+    Mix_HaltMusic();
 }
 
 static void I_SDL_UnRegisterSong(void *handle)
@@ -437,19 +378,10 @@
         return;
     }
 
-#if defined(_WIN32)
-    if (win_midi_stream_opened)
+    if (handle != NULL)
     {
-        I_WIN_UnRegisterSong();
+        Mix_FreeMusic(music);
     }
-    else
-#endif
-    {
-        if (handle != NULL)
-        {
-            Mix_FreeMusic(music);
-        }
-    }
 }
 
 // Determine whether memory block is a .mid file 
@@ -515,40 +447,21 @@
     // by now, but Mix_SetMusicCMD() only works with Mix_LoadMUS(), so
     // we have to generate a temporary file.
 
-#if defined(_WIN32)
-    // If we do not have an external music command defined, play
-    // music with the Windows native MIDI.
-    if (win_midi_stream_opened)
+    music = Mix_LoadMUS(filename);
+    if (music == NULL)
     {
-        if (I_WIN_RegisterSong(filename))
-        {
-            music = (void *) 1;
-        }
-        else
-        {
-            music = NULL;
-            fprintf(stderr, "Error loading midi: Failed to register song.\n");
-        }
+        // Failed to load
+        fprintf(stderr, "Error loading midi: %s\n", Mix_GetError());
     }
-    else
-#endif
-    {
-        music = Mix_LoadMUS(filename);
-        if (music == NULL)
-        {
-            // Failed to load
-            fprintf(stderr, "Error loading midi: %s\n", Mix_GetError());
-        }
 
-        // Remove the temporary MIDI file; however, when using an external
-        // MIDI program we can't delete the file. Otherwise, the program
-        // won't find the file to play. This means we leave a mess on
-        // disk :(
+    // Remove the temporary MIDI file; however, when using an external
+    // MIDI program we can't delete the file. Otherwise, the program
+    // won't find the file to play. This means we leave a mess on
+    // disk :(
 
-        if (strlen(snd_musiccmd) == 0)
-        {
-            M_remove(filename);
-        }
+    if (strlen(snd_musiccmd) == 0)
+    {
+        M_remove(filename);
     }
 
     free(filename);
--- a/src/i_sound.c
+++ b/src/i_sound.c
@@ -91,6 +91,9 @@
 
 static music_module_t *music_modules[] =
 {
+#ifdef _WIN32
+    &music_win_module,
+#endif
 #ifndef DISABLE_SDL2MIXER
     &music_sdl_module,
 #endif // DISABLE_SDL2MIXER
@@ -162,6 +165,18 @@
                             music_modules[i]->sound_devices,
                             music_modules[i]->num_sound_devices))
         {
+        #ifdef _WIN32
+            // Skip the native Windows MIDI module if using Timidity or
+            // FluidSynth.
+
+            if ((strcmp(timidity_cfg_path, "")
+              || strcmp(fluidsynth_sf_path, ""))
+              && music_modules[i] == &music_win_module)
+            {
+                continue;
+            }
+        #endif
+
             // Initialize the module
 
             if (music_modules[i]->Init())
@@ -492,6 +507,8 @@
     M_BindIntVariable("gus_ram_kb",              &gus_ram_kb);
 #ifdef _WIN32
     M_BindStringVariable("winmm_midi_device",    &winmm_midi_device);
+    M_BindIntVariable("winmm_reset_type",        &winmm_reset_type);
+    M_BindIntVariable("winmm_reset_delay",       &winmm_reset_delay);
     M_BindIntVariable("winmm_reverb_level",      &winmm_reverb_level);
     M_BindIntVariable("winmm_chorus_level",      &winmm_chorus_level);
 #endif
--- a/src/i_sound.h
+++ b/src/i_sound.h
@@ -258,6 +258,7 @@
 extern music_module_t music_sdl_module;
 extern music_module_t music_opl_module;
 extern music_module_t music_pack_module;
+extern music_module_t music_win_module;
 
 // For OPL module:
 
@@ -269,13 +270,11 @@
 extern char *fluidsynth_sf_path;
 extern char *timidity_cfg_path;
 #ifdef _WIN32
+extern char *winmm_midi_device;
+extern int winmm_reset_type;
+extern int winmm_reset_delay;
 extern int winmm_reverb_level;
 extern int winmm_chorus_level;
-#endif
-
-
-#ifdef _WIN32
-extern char *winmm_midi_device;
 #endif
 
 #endif
--- a/src/i_winmusic.c
+++ b/src/i_winmusic.c
@@ -1,5 +1,6 @@
 //
-// Copyright(C) 2021 Roman Fomin
+// Copyright(C) 2021-2022 Roman Fomin
+// 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
@@ -19,33 +20,78 @@
 
 #include <windows.h>
 #include <mmsystem.h>
-#include <math.h>
+#include <mmreg.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <math.h>
 
 #include "doomtype.h"
+#include "i_sound.h"
+#include "i_system.h"
 #include "m_misc.h"
+#include "memio.h"
+#include "mus2mid.h"
 #include "midifile.h"
-#include "i_sound.h"
-#include "i_winmusic.h"
+#include "midifallback.h"
 
+char *winmm_midi_device = NULL;
+int winmm_reverb_level = -1;
+int winmm_chorus_level = -1;
 
-#define BETWEEN(l,u,x) (((l)>(x))?(l):((x)>(u))?(u):(x))
+enum
+{
+    RESET_TYPE_NONE,
+    RESET_TYPE_GS,
+    RESET_TYPE_GM,
+    RESET_TYPE_GM2,
+    RESET_TYPE_XG,
+};
 
-#define REVERB_MIN 0
-#define REVERB_MAX 127
-#define CHORUS_MIN 0
-#define CHORUS_MAX 127
+int winmm_reset_type = RESET_TYPE_GS;
+int winmm_reset_delay = 0;
 
-char *winmm_midi_device = NULL;
-int winmm_reverb_level = 40;
-int winmm_chorus_level = 0;
+static const byte gs_reset[] = {
+    0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 0xF7
+};
 
+static const byte gm_system_on[] = {
+    0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7
+};
+
+static const byte gm2_system_on[] = {
+    0xF0, 0x7E, 0x7F, 0x09, 0x03, 0xF7
+};
+
+static const byte xg_system_on[] = {
+    0xF0, 0x43, 0x10, 0x4C, 0x00, 0x00, 0x7E, 0x00, 0xF7
+};
+
+static const byte ff_loopStart[] = {'l', 'o', 'o', 'p', 'S', 't', 'a', 'r', 't'};
+static const byte ff_loopEnd[] = {'l', 'o', 'o', 'p', 'E', 'n', 'd'};
+
+static boolean use_fallback;
+
+#define DEFAULT_VOLUME 100
+static int channel_volume[MIDI_CHANNELS_PER_TRACK];
+static float volume_factor = 0.0f;
+static boolean update_volume = false;
+
+static DWORD timediv;
+static DWORD tempo;
+
+static UINT MidiDevice;
 static HMIDISTRM hMidiStream;
+static MIDIHDR MidiStreamHdr;
 static HANDLE hBufferReturnEvent;
 static HANDLE hExitEvent;
 static HANDLE hPlayerThread;
 
+// MS GS Wavetable Synth Device ID.
+static int ms_gs_synth = MIDI_MAPPER;
+
+// EMIDI device for track designation.
+static int emidi_device;
+
 // This is a reduced Windows MIDIEVENT structure for MEVT_F_SHORT
 // type of events.
 
@@ -58,49 +104,56 @@
 
 typedef struct
 {
-    native_event_t *native_events;
-    int num_events;
-    int position;
+    midi_track_iter_t *iter;
+    unsigned int elapsed_time;
+    unsigned int saved_elapsed_time;
+    boolean end_of_track;
+    boolean saved_end_of_track;
+    unsigned int emidi_device_flags;
+    boolean emidi_designated;
+    boolean emidi_program;
+    boolean emidi_volume;
+    int emidi_loop_count;
+} win_midi_track_t;
+
+typedef struct
+{
+    win_midi_track_t *tracks;
+    unsigned int elapsed_time;
+    unsigned int saved_elapsed_time;
+    unsigned int num_tracks;
     boolean looping;
+    boolean ff_loop;
+    boolean ff_restart;
+    boolean rpg_loop;
 } win_midi_song_t;
 
 static win_midi_song_t song;
 
+#define BUFFER_INITIAL_SIZE 1024
+
 typedef struct
 {
-    midi_track_iter_t *iter;
-    int absolute_time;
-} win_midi_track_t;
+    byte *data;
+    unsigned int size;
+    unsigned int position;
+} buffer_t;
 
-static float volume_factor = 1.0;
+static buffer_t buffer;
 
-// Save the last volume for each MIDI channel.
-
-static int channel_volume[MIDI_CHANNELS_PER_TRACK];
-
-// Macros for use with the Windows MIDIEVENT dwEvent field.
-
-#define MIDIEVENT_CHANNEL(x)    (x & 0x0000000F)
-#define MIDIEVENT_TYPE(x)       (x & 0x000000F0)
-#define MIDIEVENT_DATA1(x)     ((x & 0x0000FF00) >> 8)
-#define MIDIEVENT_VOLUME(x)    ((x & 0x007F0000) >> 16)
-
 // Maximum of 4 events in the buffer for faster volume updates.
 
 #define STREAM_MAX_EVENTS   4
 
-typedef struct
-{
-    native_event_t events[STREAM_MAX_EVENTS];
-    int num_events;
-    MIDIHDR MidiStreamHdr;
-} buffer_t;
+#define MAKE_EVT(a, b, c, d) ((DWORD)((a) | ((b) << 8) | ((c) << 16) | ((d) << 24)))
 
-static buffer_t buffer;
+#define PADDED_SIZE(x) (((x) + sizeof(DWORD) - 1) & ~(sizeof(DWORD) - 1))
 
+static boolean initial_playback = false;
+
 // Message box for midiStream errors.
 
-static void MidiErrorMessageBox(DWORD dwError)
+static void MidiError(const char *prefix, DWORD dwError)
 {
     char szErrorBuf[MAXERRORLENGTH];
     MMRESULT mmr;
@@ -108,300 +161,991 @@
     mmr = midiOutGetErrorText(dwError, (LPSTR) szErrorBuf, MAXERRORLENGTH);
     if (mmr == MMSYSERR_NOERROR)
     {
-        MessageBox(NULL, szErrorBuf, "midiStream Error", MB_ICONEXCLAMATION);
+        char *msg = M_StringJoin(prefix, ": ", szErrorBuf, NULL);
+        MessageBox(NULL, msg, "midiStream Error", MB_ICONEXCLAMATION);
+        free(msg);
     }
     else
     {
-        fprintf(stderr, "Unknown midiStream error.\n");
+        fprintf(stderr, "%s: Unknown midiStream error.\n", prefix);
     }
 }
 
-// Fill the buffer with MIDI events, adjusting the volume as needed.
+// midiStream callback.
 
-static void FillBuffer(void)
+static void CALLBACK MidiStreamProc(HMIDIOUT hMidi, UINT uMsg,
+                                    DWORD_PTR dwInstance, DWORD_PTR dwParam1,
+                                    DWORD_PTR dwParam2)
 {
-    int i;
-
-    for (i = 0; i < STREAM_MAX_EVENTS; ++i)
+    if (uMsg == MOM_DONE)
     {
-        native_event_t *event = &buffer.events[i];
+        SetEvent(hBufferReturnEvent);
+    }
+}
 
-        if (song.position >= song.num_events)
+static void AllocateBuffer(const unsigned int size)
+{
+    MIDIHDR *hdr = &MidiStreamHdr;
+    MMRESULT mmr;
+
+    if (buffer.data)
+    {
+        mmr = midiOutUnprepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
+        if (mmr != MMSYSERR_NOERROR)
         {
-            if (song.looping)
-            {
-                song.position = 0;
-            }
-            else
-            {
-                break;
-            }
+            MidiError("midiOutUnprepareHeader", mmr);
         }
+    }
 
-        *event = song.native_events[song.position];
+    buffer.size = PADDED_SIZE(size);
+    buffer.data = I_Realloc(buffer.data, buffer.size);
 
-        if (MIDIEVENT_TYPE(event->dwEvent) == MIDI_EVENT_CONTROLLER &&
-            MIDIEVENT_DATA1(event->dwEvent) == MIDI_CONTROLLER_MAIN_VOLUME)
-        {
-            int volume = MIDIEVENT_VOLUME(event->dwEvent);
+    hdr->lpData = (LPSTR)buffer.data;
+    hdr->dwBytesRecorded = 0;
+    hdr->dwBufferLength = buffer.size;
+    mmr = midiOutPrepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
+    if (mmr != MMSYSERR_NOERROR)
+    {
+        MidiError("midiOutPrepareHeader", mmr);
+    }
+}
 
-            channel_volume[MIDIEVENT_CHANNEL(event->dwEvent)] = volume;
+static void WriteBufferPad(void)
+{
+    unsigned int padding = PADDED_SIZE(buffer.position);
+    memset(buffer.data + buffer.position, 0, padding - buffer.position);
+    buffer.position = padding;
+}
 
-            volume *= volume_factor;
+static void WriteBuffer(const byte *ptr, unsigned int size)
+{
+    if (buffer.position + size >= buffer.size)
+    {
+        AllocateBuffer(size + buffer.size * 2);
+    }
 
-            event->dwEvent = (event->dwEvent & 0xFF00FFFF) |
-                             ((volume & 0x7F) << 16);
-        }
+    memcpy(buffer.data + buffer.position, ptr, size);
+    buffer.position += size;
+}
 
-        song.position++;
+static void StreamOut(void)
+{
+    MIDIHDR *hdr = &MidiStreamHdr;
+    MMRESULT mmr;
+
+    hdr->lpData = (LPSTR)buffer.data;
+    hdr->dwBytesRecorded = buffer.position;
+
+    mmr = midiStreamOut(hMidiStream, hdr, sizeof(MIDIHDR));
+    if (mmr != MMSYSERR_NOERROR)
+    {
+        MidiError("midiStreamOut", mmr);
     }
+}
 
-    buffer.num_events = i;
+static void SendShortMsg(int time, int status, int channel, int param1, int param2)
+{
+    native_event_t native_event;
+    native_event.dwDeltaTime = time;
+    native_event.dwStreamID = 0;
+    native_event.dwEvent = MAKE_EVT(status | channel, param1, param2, MEVT_SHORTMSG);
+    WriteBuffer((byte *)&native_event, sizeof(native_event_t));
 }
 
-// Queue MIDI events.
+static void SendLongMsg(int time, const byte *ptr, int length)
+{
+    native_event_t native_event;
+    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));
+    WriteBuffer(ptr, length);
+    WriteBufferPad();
+}
 
-static void StreamOut(void)
+static void SendNOPMsg(int time)
 {
-    MIDIHDR *hdr = &buffer.MidiStreamHdr;
-    MMRESULT mmr;
+    native_event_t native_event;
+    native_event.dwDeltaTime = time;
+    native_event.dwStreamID = 0;
+    native_event.dwEvent = MAKE_EVT(0, 0, 0, MEVT_NOP);
+    WriteBuffer((byte *)&native_event, sizeof(native_event_t));
+}
 
-    int num_events = buffer.num_events;
+static void SendDelayMsg(int time_ms)
+{
+    // Convert ms to ticks (see "Standard MIDI Files 1.0" page 14).
+    int time_ticks = (float)time_ms * 1000 * timediv / tempo + 0.5f;
+    SendNOPMsg(time_ticks);
+}
 
-    if (num_events == 0)
+static void UpdateTempo(int time, midi_event_t *event)
+{
+    native_event_t native_event;
+
+    tempo = MAKE_EVT(event->data.meta.data[2], event->data.meta.data[1],
+                     event->data.meta.data[0], 0);
+
+    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 SendVolumeMsg(int time, int channel, int volume)
+{
+    int scaled_volume = volume * volume_factor + 0.5f;
+    SendShortMsg(time, MIDI_EVENT_CONTROLLER, channel,
+                 MIDI_CONTROLLER_VOLUME_MSB, scaled_volume);
+    channel_volume[channel] = volume;
+}
+
+static void UpdateVolume(void)
+{
+    int i;
+
+    for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
     {
-        return;
+        SendVolumeMsg(0, i, channel_volume[i]);
     }
+}
 
-    hdr->lpData = (LPSTR)buffer.events;
-    hdr->dwBytesRecorded = num_events * sizeof(native_event_t);
+static void ResetVolume(void)
+{
+    int i;
 
-    mmr = midiStreamOut(hMidiStream, hdr, sizeof(MIDIHDR));
-    if (mmr != MMSYSERR_NOERROR)
+    for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
     {
-        MidiErrorMessageBox(mmr);
+        SendVolumeMsg(0, i, DEFAULT_VOLUME);
     }
 }
 
-// midiStream callback.
+static void ResetReverb(int reset_type)
+{
+    int i;
+    int reverb = winmm_reverb_level;
 
-static void CALLBACK MidiStreamProc(HMIDIIN hMidi, UINT uMsg,
-                                    DWORD_PTR dwInstance, DWORD_PTR dwParam1,
-                                    DWORD_PTR dwParam2)
+    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)
 {
-    if (uMsg == MOM_DONE)
+    int i;
+    int chorus = winmm_chorus_level;
+
+    if (chorus == -1 && reset_type == RESET_TYPE_NONE)
     {
-        SetEvent(hBufferReturnEvent);
+        // 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);
+        }
+    }
 }
 
-// The Windows API documentation states: "Applications should not call any
-// multimedia functions from inside the callback function, as doing so can
-// cause a deadlock." We use thread to avoid possible deadlocks.
+static void ResetControllers(void)
+{
+    int i;
 
-static DWORD WINAPI PlayerProc(void)
+    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);
+        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);
+    }
+}
+
+static void ResetPitchBendSensitivity(void)
 {
-    HANDLE events[2] = { hBufferReturnEvent, hExitEvent };
+    int i;
 
-    while (1)
+    for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
     {
-        switch (WaitForMultipleObjects(2, events, FALSE, INFINITE))
+        // 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 WAIT_OBJECT_0:
-                FillBuffer();
-                StreamOut();
+            case RESET_TYPE_NONE:
+                reset_type = RESET_TYPE_NONE;
                 break;
 
-            case WAIT_OBJECT_0 + 1:
-                return 0;
+            case RESET_TYPE_GS:
+                reset_type = RESET_TYPE_GS;
+                break;
+
+            default:
+                reset_type = RESET_TYPE_GM;
+                break;
         }
     }
-    return 0;
+    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);
+
+    // Assign EMIDI device for track designation.
+    emidi_device = (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);
+    }
 }
 
-// Convert a multi-track MIDI file to an array of Windows MIDIEVENT structures.
+static boolean IsSysExReset(const byte *msg, int length)
+{
+    if (length < 5)
+    {
+        return false;
+    }
 
-static void MIDItoStream(midi_file_t *file)
+    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 <dev> 42 12 00 00 7F 00 01 F7 (MODE-1)
+                                // 41 <dev> 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 <dev> 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 <dev> 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 (System On)
+                        msg[5] == 0x7F) && // Address Low (All Parameter Reset)
+                        msg[6] == 0x00)    // Data
+                    {
+                        // XG System On, XG All Parameter Reset
+                        // 43 <dev> 4C 00 00 7E 00 F7
+                        // 43 <dev> 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 <dev> 09 01 F7
+                        // 7E <dev> 09 02 F7
+                        // 7E <dev> 09 03 F7
+                        return true;
+                    }
+                    break;
+            }
+            break;
+    }
+    return false;
+}
+
+static void SendSysExMsg(int time, const byte *data, int length)
 {
-    int i;
+    native_event_t native_event;
+    boolean is_sysex_reset;
+    const byte event_type = MIDI_EVENT_SYSEX;
 
-    int num_tracks =  MIDI_NumTracks(file);
-    win_midi_track_t *tracks = malloc(num_tracks * sizeof(win_midi_track_t));
+    is_sysex_reset = IsSysExReset(data, length);
 
-    int current_time = 0;
+    if (is_sysex_reset && MidiDevice == ms_gs_synth)
+    {
+        // Ignore SysEx reset from MIDI file for MS GS Wavetable Synth.
+        SendNOPMsg(time);
+        return;
+    }
 
-    for (i = 0; i < num_tracks; ++i)
+    // 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 (is_sysex_reset)
     {
-        tracks[i].iter = MIDI_IterateTrack(file, i);
-        tracks[i].absolute_time = 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;
+
+        // Use default device for EMIDI.
+        emidi_device = EMIDI_DEVICE_GENERAL_MIDI;
     }
+}
 
-    song.native_events = calloc(MIDI_NumEvents(file), sizeof(native_event_t));
+static void SendProgramMsg(int time, int channel, int program,
+                           midi_fallback_t *fallback)
+{
+    switch ((int)fallback->type)
+    {
+        case FALLBACK_BANK_MSB:
+            SendShortMsg(time, MIDI_EVENT_CONTROLLER, channel,
+                         MIDI_CONTROLLER_BANK_SELECT_MSB, fallback->value);
+            SendShortMsg(0, MIDI_EVENT_PROGRAM_CHANGE, channel, program, 0);
+            break;
 
-    while (1)
+        case FALLBACK_DRUMS:
+            SendShortMsg(time, MIDI_EVENT_PROGRAM_CHANGE, channel,
+                         fallback->value, 0);
+            break;
+
+        default:
+            SendShortMsg(time, MIDI_EVENT_PROGRAM_CHANGE, channel, program, 0);
+            break;
+    }
+}
+
+static void SetLoopPoint(void)
+{
+    unsigned int i;
+
+    for (i = 0; i < song.num_tracks; ++i)
     {
-        midi_event_t *event;
-        DWORD data = 0;
-        int min_time = INT_MAX;
-        int idx = -1;
+        MIDI_SetLoopPoint(song.tracks[i].iter);
+        song.tracks[i].saved_end_of_track = song.tracks[i].end_of_track;
+        song.tracks[i].saved_elapsed_time = song.tracks[i].elapsed_time;
+    }
+    song.saved_elapsed_time = song.elapsed_time;
+}
 
-        // Look for an event with a minimal delta time.
-        for (i = 0; i < num_tracks; ++i)
-        {
-            int time = 0;
+static void CheckFFLoop(midi_event_t *event)
+{
+    if (event->data.meta.length == sizeof(ff_loopStart) &&
+        !memcmp(event->data.meta.data, ff_loopStart, sizeof(ff_loopStart)))
+    {
+        SetLoopPoint();
+        song.ff_loop = true;
+    }
+    else if (song.ff_loop && event->data.meta.length == sizeof(ff_loopEnd) &&
+             !memcmp(event->data.meta.data, ff_loopEnd, sizeof(ff_loopEnd)))
+    {
+        song.ff_restart = true;
+    }
+}
 
-            if (tracks[i].iter == NULL)
+static boolean AddToBuffer(unsigned int delta_time, midi_event_t *event,
+                           win_midi_track_t *track)
+{
+    unsigned int i;
+    unsigned int flag;
+    int count;
+    midi_fallback_t fallback = {FALLBACK_NONE, 0};
+
+    if (use_fallback)
+    {
+        MIDI_CheckFallback(event, &fallback);
+    }
+
+    switch ((int)event->event_type)
+    {
+        case MIDI_EVENT_SYSEX:
+            SendSysExMsg(delta_time, event->data.sysex.data,
+                         event->data.sysex.length);
+            return false;
+
+        case MIDI_EVENT_META:
+            switch (event->data.meta.type)
             {
-                continue;
+                case MIDI_META_END_OF_TRACK:
+                    track->end_of_track = true;
+                    SendNOPMsg(delta_time);
+                    break;
+
+                case MIDI_META_SET_TEMPO:
+                    UpdateTempo(delta_time, event);
+                    break;
+
+                case MIDI_META_MARKER:
+                    CheckFFLoop(event);
+                    SendNOPMsg(delta_time);
+                    break;
+
+                default:
+                    SendNOPMsg(delta_time);
+                    break;
             }
+            return true;
+    }
 
-            time = tracks[i].absolute_time + MIDI_GetDeltaTime(tracks[i].iter);
+    if (track->emidi_designated && (emidi_device & ~track->emidi_device_flags))
+    {
+        // Send NOP if this device has been excluded from this track.
+        SendNOPMsg(delta_time);
+        return true;
+    }
 
-            if (time < min_time)
+    switch ((int)event->event_type)
+    {
+        case MIDI_EVENT_CONTROLLER:
+            switch (event->data.channel.param1)
             {
-                min_time = time;
-                idx = i;
-            }
-        }
+                case MIDI_CONTROLLER_VOLUME_MSB:
+                    if (track->emidi_volume)
+                    {
+                        SendNOPMsg(delta_time);
+                    }
+                    else
+                    {
+                        SendVolumeMsg(delta_time, event->data.channel.channel,
+                                      event->data.channel.param2);
+                    }
+                    break;
 
-        // No more MIDI events left, end the loop.
-        if (idx == -1)
-        {
-            break;
-        }
+                case MIDI_CONTROLLER_VOLUME_LSB:
+                    SendNOPMsg(delta_time);
+                    break;
+
+                case MIDI_CONTROLLER_BANK_SELECT_LSB:
+                    if (fallback.type == FALLBACK_BANK_LSB)
+                    {
+                        SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER,
+                                     event->data.channel.channel,
+                                     MIDI_CONTROLLER_BANK_SELECT_LSB,
+                                     fallback.value);
+                    }
+                    else
+                    {
+                        SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER,
+                                     event->data.channel.channel,
+                                     MIDI_CONTROLLER_BANK_SELECT_LSB,
+                                     event->data.channel.param2);
+                    }
+                    break;
+
+                case EMIDI_CONTROLLER_TRACK_DESIGNATION:
+                    if (track->elapsed_time < timediv)
+                    {
+                        flag = event->data.channel.param2;
+
+                        if (flag == EMIDI_DEVICE_ALL)
+                        {
+                            track->emidi_device_flags = UINT_MAX;
+                            track->emidi_designated = true;
+                        }
+                        else if (flag <= EMIDI_DEVICE_ULTRASOUND)
+                        {
+                            track->emidi_device_flags |= 1 << flag;
+                            track->emidi_designated = true;
+                        }
+                    }
+                    SendNOPMsg(delta_time);
+                    break;
 
-        tracks[idx].absolute_time = min_time;
+                case EMIDI_CONTROLLER_TRACK_EXCLUSION:
+                    if (song.rpg_loop)
+                    {
+                        SetLoopPoint();
+                    }
+                    else if (track->elapsed_time < timediv)
+                    {
+                        flag = event->data.channel.param2;
 
-        if (!MIDI_GetNextEvent(tracks[idx].iter, &event))
-        {
-            MIDI_FreeIterator(tracks[idx].iter);
-            tracks[idx].iter = NULL;
-            continue;
-        }
+                        if (!track->emidi_designated)
+                        {
+                            track->emidi_device_flags = UINT_MAX;
+                            track->emidi_designated = true;
+                        }
 
-        switch ((int)event->event_type)
-        {
-            case MIDI_EVENT_META:
-                if (event->data.meta.type == MIDI_META_SET_TEMPO)
-                {
-                    data = event->data.meta.data[2] |
-                        (event->data.meta.data[1] << 8) |
-                        (event->data.meta.data[0] << 16) |
-                        (MEVT_TEMPO << 24);
-                }
-                break;
+                        if (flag <= EMIDI_DEVICE_ULTRASOUND)
+                        {
+                            track->emidi_device_flags &= ~(1 << flag);
+                        }
+                    }
+                    SendNOPMsg(delta_time);
+                    break;
 
-            case MIDI_EVENT_NOTE_OFF:
-            case MIDI_EVENT_NOTE_ON:
-            case MIDI_EVENT_AFTERTOUCH:
-            case MIDI_EVENT_CONTROLLER:
-            case MIDI_EVENT_PITCH_BEND:
-                data = event->event_type |
-                    event->data.channel.channel |
-                    (event->data.channel.param1 << 8) |
-                    (event->data.channel.param2 << 16) |
-                    (MEVT_SHORTMSG << 24);
-                break;
+                case EMIDI_CONTROLLER_PROGRAM_CHANGE:
+                    if (track->emidi_program || track->elapsed_time < timediv)
+                    {
+                        track->emidi_program = true;
+                        SendProgramMsg(delta_time, event->data.channel.channel,
+                                       event->data.channel.param2, &fallback);
+                    }
+                    else
+                    {
+                        SendNOPMsg(delta_time);
+                    }
+                    break;
 
-            case MIDI_EVENT_PROGRAM_CHANGE:
-            case MIDI_EVENT_CHAN_AFTERTOUCH:
-                data = event->event_type |
-                    event->data.channel.channel |
-                    (event->data.channel.param1 << 8) |
-                    (0 << 16) |
-                    (MEVT_SHORTMSG << 24);
-                break;
-        }
+                case EMIDI_CONTROLLER_VOLUME:
+                    if (track->emidi_volume || track->elapsed_time < timediv)
+                    {
+                        track->emidi_volume = true;
+                        SendVolumeMsg(delta_time, event->data.channel.channel,
+                                      event->data.channel.param2);
+                    }
+                    else
+                    {
+                        SendNOPMsg(delta_time);
+                    }
+                    break;
+
+                case EMIDI_CONTROLLER_LOOP_BEGIN:
+                    count = event->data.channel.param2;
+                    count = (count == 0) ? (-1) : count;
+                    track->emidi_loop_count = count;
+                    MIDI_SetLoopPoint(track->iter);
+                    SendNOPMsg(delta_time);
+                    break;
+
+                case EMIDI_CONTROLLER_LOOP_END:
+                    if (event->data.channel.param2 == EMIDI_LOOP_FLAG)
+                    {
+                        if (track->emidi_loop_count != 0)
+                        {
+                            MIDI_RestartAtLoopPoint(track->iter);
+                        }
 
-        if (data)
-        {
-            native_event_t *native_event = &song.native_events[song.num_events];
+                        if (track->emidi_loop_count > 0)
+                        {
+                            track->emidi_loop_count--;
+                        }
+                    }
+                    SendNOPMsg(delta_time);
+                    break;
 
-            native_event->dwDeltaTime = min_time - current_time;
-            native_event->dwStreamID = 0;
-            native_event->dwEvent = data;
+                case EMIDI_CONTROLLER_GLOBAL_LOOP_BEGIN:
+                    count = event->data.channel.param2;
+                    count = (count == 0) ? (-1) : count;
+                    for (i = 0; i < song.num_tracks; ++i)
+                    {
+                        song.tracks[i].emidi_loop_count = count;
+                        MIDI_SetLoopPoint(song.tracks[i].iter);
+                    }
+                    SendNOPMsg(delta_time);
+                    break;
 
-            song.num_events++;
-            current_time = min_time;
-        }
-    }
+                case EMIDI_CONTROLLER_GLOBAL_LOOP_END:
+                    if (event->data.channel.param2 == EMIDI_LOOP_FLAG)
+                    {
+                        for (i = 0; i < song.num_tracks; ++i)
+                        {
+                            if (song.tracks[i].emidi_loop_count != 0)
+                            {
+                                MIDI_RestartAtLoopPoint(song.tracks[i].iter);
+                            }
+
+                            if (song.tracks[i].emidi_loop_count > 0)
+                            {
+                                song.tracks[i].emidi_loop_count--;
+                            }
+                        }
+                    }
+                    SendNOPMsg(delta_time);
+                    break;
 
-    if (tracks)
+                default:
+                    SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER,
+                                 event->data.channel.channel,
+                                 event->data.channel.param1,
+                                 event->data.channel.param2);
+                    break;
+            }
+            break;
+
+        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);
+            break;
+
+        case MIDI_EVENT_PROGRAM_CHANGE:
+            if (track->emidi_program)
+            {
+                SendNOPMsg(delta_time);
+            }
+            else
+            {
+                SendProgramMsg(delta_time, event->data.channel.channel,
+                               event->data.channel.param1, &fallback);
+            }
+            break;
+
+        case MIDI_EVENT_CHAN_AFTERTOUCH:
+            SendShortMsg(delta_time, MIDI_EVENT_CHAN_AFTERTOUCH,
+                         event->data.channel.channel,
+                         event->data.channel.param1, 0);
+            break;
+
+        default:
+            SendNOPMsg(delta_time);
+            break;
+    }
+
+    return true;
+}
+
+static void RestartLoop(void)
+{
+    unsigned int i;
+
+    for (i = 0; i < song.num_tracks; ++i)
     {
-        free(tracks);
+        MIDI_RestartAtLoopPoint(song.tracks[i].iter);
+        song.tracks[i].end_of_track = song.tracks[i].saved_end_of_track;
+        song.tracks[i].elapsed_time = song.tracks[i].saved_elapsed_time;
     }
+    song.elapsed_time = song.saved_elapsed_time;
 }
 
-static void UpdateVolume(void)
+static void RestartTracks(void)
 {
-    int i;
+    unsigned int i;
 
-    // Send MIDI controller events to adjust the volume.
-    for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
+    for (i = 0; i < song.num_tracks; ++i)
     {
-        DWORD msg = 0;
+        MIDI_RestartIterator(song.tracks[i].iter);
+        song.tracks[i].elapsed_time = 0;
+        song.tracks[i].end_of_track = false;
+        song.tracks[i].emidi_device_flags = 0;
+        song.tracks[i].emidi_designated = false;
+        song.tracks[i].emidi_program = false;
+        song.tracks[i].emidi_volume = false;
+        song.tracks[i].emidi_loop_count = 0;
+    }
+    song.elapsed_time = 0;
+}
 
-        int value = channel_volume[i] * volume_factor;
+static boolean IsRPGLoop(void)
+{
+    unsigned int i;
+    unsigned int num_rpg_events = 0;
+    unsigned int num_emidi_events = 0;
+    midi_event_t *event = NULL;
 
-        msg = MIDI_EVENT_CONTROLLER | i | (MIDI_CONTROLLER_MAIN_VOLUME << 8) |
-              (value << 16);
+    for (i = 0; i < song.num_tracks; ++i)
+    {
+        while (MIDI_GetNextEvent(song.tracks[i].iter, &event))
+        {
+            if (event->event_type == MIDI_EVENT_CONTROLLER)
+            {
+                switch (event->data.channel.param1)
+                {
+                    case EMIDI_CONTROLLER_TRACK_EXCLUSION:
+                        num_rpg_events++;
+                        break;
 
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+                    case EMIDI_CONTROLLER_TRACK_DESIGNATION:
+                    case EMIDI_CONTROLLER_PROGRAM_CHANGE:
+                    case EMIDI_CONTROLLER_VOLUME:
+                    case EMIDI_CONTROLLER_LOOP_BEGIN:
+                    case EMIDI_CONTROLLER_LOOP_END:
+                    case EMIDI_CONTROLLER_GLOBAL_LOOP_BEGIN:
+                    case EMIDI_CONTROLLER_GLOBAL_LOOP_END:
+                        num_emidi_events++;
+                        break;
+                }
+            }
+        }
+
+        MIDI_RestartIterator(song.tracks[i].iter);
     }
+
+    return (num_rpg_events == 1 && num_emidi_events == 0);
 }
 
-void ResetDevice(void)
+static void FillBuffer(void)
 {
-    for (int i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
+    unsigned int i;
+    int num_events;
+
+    buffer.position = 0;
+
+    if (initial_playback)
     {
-        DWORD msg = 0;
+        ResetDevice();
+        StreamOut();
+        song.rpg_loop = IsRPGLoop();
+        initial_playback = false;
+        return;
+    }
 
-        // RPN sequence to adjust pitch bend range (RPN value 0x0000)
-        msg = MIDI_EVENT_CONTROLLER | i | 0x65 << 8 | 0x00 << 16;
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
-        msg = MIDI_EVENT_CONTROLLER | i | 0x64 << 8 | 0x00 << 16;
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+    if (update_volume)
+    {
+        update_volume = false;
+        UpdateVolume();
+        StreamOut();
+        return;
+    }
 
-        // reset pitch bend range to central tuning +/- 2 semitones and 0 cents
-        msg = MIDI_EVENT_CONTROLLER | i | 0x06 << 8 | 0x02 << 16;
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
-        msg = MIDI_EVENT_CONTROLLER | i | 0x26 << 8 | 0x00 << 16;
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+    for (num_events = 0; num_events < STREAM_MAX_EVENTS; )
+    {
+        midi_event_t *event = NULL;
+        win_midi_track_t *track = NULL;
+        unsigned int min_time = UINT_MAX;
+        unsigned int delta_time;
 
-        // end of RPN sequence
-        msg = MIDI_EVENT_CONTROLLER | i | 0x64 << 8 | 0x7F << 16;
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
-        msg = MIDI_EVENT_CONTROLLER | i | 0x65 << 8 | 0x7F << 16;
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+        // Find next event across all tracks.
+        for (i = 0; i < song.num_tracks; ++i)
+        {
+            if (!song.tracks[i].end_of_track)
+            {
+                unsigned int time = song.tracks[i].elapsed_time +
+                                    MIDI_GetDeltaTime(song.tracks[i].iter);
+                if (time < min_time)
+                {
+                    min_time = time;
+                    track = &song.tracks[i];
+                }
+            }
+        }
 
-        // reset all controllers
-        msg = MIDI_EVENT_CONTROLLER | i | 0x79 << 8 | 0x00 << 16;
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+        // No more events. Restart or stop song.
+        if (track == NULL)
+        {
+            if (song.elapsed_time)
+            {
+                if (song.ff_restart || song.rpg_loop)
+                {
+                    song.ff_restart = false;
+                    RestartLoop();
+                    continue;
+                }
+                else if (song.looping)
+                {
+                    RestartTracks();
+                    continue;
+                }
+            }
+            break;
+        }
 
-        // reset pan to 64 (center)
-        msg = MIDI_EVENT_CONTROLLER | i | 0x0A << 8 | 0x40 << 16;
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+        track->elapsed_time = min_time;
+        delta_time = min_time - song.elapsed_time;
+        song.elapsed_time = min_time;
 
-        // reset reverb and other effect controllers
-        msg = MIDI_EVENT_CONTROLLER | i | 0x5B << 8 | winmm_reverb_level << 16;
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
-        msg = MIDI_EVENT_CONTROLLER | i | 0x5C << 8 | 0x00 << 16; // tremolo
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
-        msg = MIDI_EVENT_CONTROLLER | i | 0x5D << 8 | winmm_chorus_level << 16;
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
-        msg = MIDI_EVENT_CONTROLLER | i | 0x5E << 8 | 0x00 << 16; // detune
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
-        msg = MIDI_EVENT_CONTROLLER | i | 0x5F << 8 | 0x00 << 16; // phaser
-        midiOutShortMsg((HMIDIOUT)hMidiStream, msg);
+        if (!MIDI_GetNextEvent(track->iter, &event))
+        {
+            track->end_of_track = true;
+            continue;
+        }
+
+        // Restart FF loop after sending all events that share same timediv.
+        if (song.ff_restart && MIDI_GetDeltaTime(track->iter) > 0)
+        {
+            song.ff_restart = false;
+            RestartLoop();
+            continue;
+        }
+
+        if (!AddToBuffer(delta_time, event, track))
+        {
+            StreamOut();
+            return;
+        }
+
+        num_events++;
     }
+
+    if (num_events)
+    {
+        StreamOut();
+    }
 }
 
-boolean I_WIN_InitMusic(void)
+// The Windows API documentation states: "Applications should not call any
+// multimedia functions from inside the callback function, as doing so can
+// cause a deadlock." We use thread to avoid possible deadlocks.
+
+static DWORD WINAPI PlayerProc(void)
 {
-    UINT MidiDevice;
+    HANDLE events[2] = { hBufferReturnEvent, hExitEvent };
+
+    while (1)
+    {
+        switch (WaitForMultipleObjects(2, events, FALSE, INFINITE))
+        {
+            case WAIT_OBJECT_0:
+                FillBuffer();
+                break;
+
+            case WAIT_OBJECT_0 + 1:
+                return 0;
+        }
+    }
+    return 0;
+}
+
+static boolean I_WIN_InitMusic(void)
+{
     int all_devices;
     int i;
-    MIDIHDR *hdr = &buffer.MidiStreamHdr;
     MIDIOUTCAPS mcaps;
     MMRESULT mmr;
 
@@ -441,73 +1185,72 @@
         }
     }
 
+    // Is this device MS GS Synth?
+    if (mcaps.wMid == MM_MICROSOFT &&
+        mcaps.wPid == MM_MSFT_GENERIC_MIDISYNTH &&
+        mcaps.wTechnology == MOD_SWSYNTH)
+    {
+        ms_gs_synth = MidiDevice;
+    }
+
     mmr = midiStreamOpen(&hMidiStream, &MidiDevice, (DWORD)1,
                          (DWORD_PTR)MidiStreamProc, (DWORD_PTR)NULL,
                          CALLBACK_FUNCTION);
     if (mmr != MMSYSERR_NOERROR)
     {
-        MidiErrorMessageBox(mmr);
+        MidiError("midiStreamOpen", mmr);
         return false;
     }
 
-    hdr->lpData = (LPSTR)buffer.events;
-    hdr->dwBytesRecorded = 0;
-    hdr->dwBufferLength = STREAM_MAX_EVENTS * sizeof(native_event_t);
-    hdr->dwFlags = 0;
-    hdr->dwOffset = 0;
+    AllocateBuffer(BUFFER_INITIAL_SIZE);
 
-    mmr = midiOutPrepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
-    if (mmr != MMSYSERR_NOERROR)
-    {
-        MidiErrorMessageBox(mmr);
-        return false;
-    }
-
     hBufferReturnEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
     hExitEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
 
-    winmm_reverb_level = BETWEEN(REVERB_MIN, REVERB_MAX, winmm_reverb_level);
-    winmm_chorus_level = BETWEEN(CHORUS_MIN, CHORUS_MAX, winmm_chorus_level);
-    ResetDevice();
+    MIDI_InitFallback();
 
     return true;
 }
 
-void I_WIN_SetMusicVolume(int volume)
+static void I_WIN_SetMusicVolume(int volume)
 {
-    volume_factor = sqrt((float)volume / 120);
+    static int last_volume = -1;
 
-    UpdateVolume();
+    if (last_volume == volume)
+    {
+        // Ignore holding key down in volume menu.
+        return;
+    }
+
+    last_volume = volume;
+
+    volume_factor = sqrtf((float)volume / 120);
+
+    update_volume = true;
 }
 
-void I_WIN_StopSong(void)
+static void I_WIN_StopSong(void)
 {
     MMRESULT mmr;
 
-    if (hPlayerThread)
+    if (!hPlayerThread)
     {
-        SetEvent(hExitEvent);
-        WaitForSingleObject(hPlayerThread, INFINITE);
-
-        CloseHandle(hPlayerThread);
-        hPlayerThread = NULL;
+        return;
     }
 
-    ResetDevice();
+    SetEvent(hExitEvent);
+    WaitForSingleObject(hPlayerThread, INFINITE);
+    CloseHandle(hPlayerThread);
+    hPlayerThread = NULL;
 
     mmr = midiStreamStop(hMidiStream);
     if (mmr != MMSYSERR_NOERROR)
     {
-        MidiErrorMessageBox(mmr);
+        MidiError("midiStreamStop", mmr);
     }
-    mmr = midiOutReset((HMIDIOUT)hMidiStream);
-    if (mmr != MMSYSERR_NOERROR)
-    {
-        MidiErrorMessageBox(mmr);
-    }
 }
 
-void I_WIN_PlaySong(boolean looping)
+static void I_WIN_PlaySong(void *handle, boolean looping)
 {
     MMRESULT mmr;
 
@@ -517,16 +1260,18 @@
                                  0, 0, 0);
     SetThreadPriority(hPlayerThread, THREAD_PRIORITY_TIME_CRITICAL);
 
+    initial_playback = true;
+
+    SetEvent(hBufferReturnEvent);
+
     mmr = midiStreamRestart(hMidiStream);
     if (mmr != MMSYSERR_NOERROR)
     {
-        MidiErrorMessageBox(mmr);
+        MidiError("midiStreamRestart", mmr);
     }
-
-    UpdateVolume();
 }
 
-void I_WIN_PauseSong(void)
+static void I_WIN_PauseSong(void)
 {
     MMRESULT mmr;
 
@@ -533,11 +1278,11 @@
     mmr = midiStreamPause(hMidiStream);
     if (mmr != MMSYSERR_NOERROR)
     {
-        MidiErrorMessageBox(mmr);
+        MidiError("midiStreamPause", mmr);
     }
 }
 
-void I_WIN_ResumeSong(void)
+static void I_WIN_ResumeSong(void)
 {
     MMRESULT mmr;
 
@@ -544,101 +1289,224 @@
     mmr = midiStreamRestart(hMidiStream);
     if (mmr != MMSYSERR_NOERROR)
     {
-        MidiErrorMessageBox(mmr);
+        MidiError("midiStreamRestart", mmr);
     }
 }
 
-boolean I_WIN_RegisterSong(char *filename)
+// Determine whether memory block is a .mid file 
+
+static boolean IsMid(byte *mem, int len)
 {
-    int i;
+    return len > 4 && !memcmp(mem, "MThd", 4);
+}
+
+static boolean ConvertMus(byte *musdata, int len, const char *filename)
+{
+    MEMFILE *instream;
+    MEMFILE *outstream;
+    void *outbuf;
+    size_t outbuf_len;
+    int result;
+
+    instream = mem_fopen_read(musdata, len);
+    outstream = mem_fopen_write();
+
+    result = mus2mid(instream, outstream);
+
+    if (result == 0)
+    {
+        mem_get_buf(outstream, &outbuf, &outbuf_len);
+
+        M_WriteFile(filename, outbuf, outbuf_len);
+    }
+
+    mem_fclose(instream);
+    mem_fclose(outstream);
+
+    return result;
+}
+
+static void *I_WIN_RegisterSong(void *data, int len)
+{
+    unsigned int i;
+    char *filename;
     midi_file_t *file;
-    MIDIPROPTIMEDIV timediv;
-    MIDIPROPTEMPO tempo;
+
+    MIDIPROPTIMEDIV prop_timediv;
+    MIDIPROPTEMPO prop_tempo;
     MMRESULT mmr;
 
+    // MUS files begin with "MUS"
+    // Reject anything which doesnt have this signature
+
+    filename = M_TempFile("doom.mid");
+
+    if (IsMid(data, len))
+    {
+        M_WriteFile(filename, data, len);
+    }
+    else
+    {
+        // Assume a MUS file and try to convert
+
+        ConvertMus(data, len, filename);
+    }
+
     file = MIDI_LoadFile(filename);
 
+    M_remove(filename);
+    free(filename);
+
     if (file == NULL)
     {
         fprintf(stderr, "I_WIN_RegisterSong: Failed to load MID.\n");
-        return false;
+        return NULL;
     }
 
-    // Initialize channels volume.
-    for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
-    {
-        channel_volume[i] = 100;
-    }
-
-    timediv.cbStruct = sizeof(MIDIPROPTIMEDIV);
-    timediv.dwTimeDiv = MIDI_GetFileTimeDivision(file);
-    mmr = midiStreamProperty(hMidiStream, (LPBYTE)&timediv,
+    prop_timediv.cbStruct = sizeof(MIDIPROPTIMEDIV);
+    prop_timediv.dwTimeDiv = MIDI_GetFileTimeDivision(file);
+    mmr = midiStreamProperty(hMidiStream, (LPBYTE)&prop_timediv,
                              MIDIPROP_SET | MIDIPROP_TIMEDIV);
     if (mmr != MMSYSERR_NOERROR)
     {
-        MidiErrorMessageBox(mmr);
-        return false;
+        MidiError("midiStreamProperty", mmr);
+        return NULL;
     }
+    timediv = prop_timediv.dwTimeDiv;
 
     // Set initial tempo.
-    tempo.cbStruct = sizeof(MIDIPROPTIMEDIV);
-    tempo.dwTempo = 500000; // 120 bmp
-    mmr = midiStreamProperty(hMidiStream, (LPBYTE)&tempo,
+    prop_tempo.cbStruct = sizeof(MIDIPROPTIMEDIV);
+    prop_tempo.dwTempo = 500000; // 120 BPM
+    mmr = midiStreamProperty(hMidiStream, (LPBYTE)&prop_tempo,
                              MIDIPROP_SET | MIDIPROP_TEMPO);
     if (mmr != MMSYSERR_NOERROR)
     {
-        MidiErrorMessageBox(mmr);
-        return false;
+        MidiError("midiStreamProperty", mmr);
+        return NULL;
     }
+    tempo = prop_tempo.dwTempo;
 
-    MIDItoStream(file);
+    song.num_tracks = MIDI_NumTracks(file);
+    song.tracks = calloc(song.num_tracks, sizeof(win_midi_track_t));
+    for (i = 0; i < song.num_tracks; ++i)
+    {
+        song.tracks[i].iter = MIDI_IterateTrack(file, i);
+    }
 
-    MIDI_FreeFile(file);
-
     ResetEvent(hBufferReturnEvent);
     ResetEvent(hExitEvent);
 
-    FillBuffer();
-    StreamOut();
-
-    return true;
+    return file;
 }
 
-void I_WIN_UnRegisterSong(void)
+static void I_WIN_UnRegisterSong(void *handle)
 {
-    if (song.native_events)
+    if (song.tracks)
     {
-        free(song.native_events);
-        song.native_events = NULL;
+        int i;
+        for (i = 0; i < song.num_tracks; ++i)
+        {
+            MIDI_FreeIterator(song.tracks[i].iter);
+            song.tracks[i].iter = NULL;
+        }
+        free(song.tracks);
+        song.tracks = NULL;
     }
-    song.num_events = 0;
-    song.position = 0;
+    if (handle)
+    {
+        MIDI_FreeFile(handle);
+    }
+    song.elapsed_time = 0;
+    song.saved_elapsed_time = 0;
+    song.num_tracks = 0;
+    song.looping = false;
+    song.ff_loop = false;
+    song.ff_restart = false;
+    song.rpg_loop = false;
 }
 
-void I_WIN_ShutdownMusic(void)
+static void I_WIN_ShutdownMusic(void)
 {
-    MIDIHDR *hdr = &buffer.MidiStreamHdr;
     MMRESULT mmr;
 
+    if (!hMidiStream)
+    {
+        return;
+    }
+
     I_WIN_StopSong();
-    I_WIN_UnRegisterSong();
+    I_WIN_UnRegisterSong(NULL);
 
-    mmr = midiOutUnprepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
+    // Reset device at shutdown.
+    buffer.position = 0;
+    ResetDevice();
+    StreamOut();
+    mmr = midiStreamRestart(hMidiStream);
     if (mmr != MMSYSERR_NOERROR)
     {
-        MidiErrorMessageBox(mmr);
+        MidiError("midiStreamRestart", mmr);
     }
+    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)
     {
-        MidiErrorMessageBox(mmr);
+        MidiError("midiStreamClose", mmr);
     }
-
     hMidiStream = NULL;
 
     CloseHandle(hBufferReturnEvent);
     CloseHandle(hExitEvent);
 }
+
+static boolean I_WIN_MusicIsPlaying(void)
+{
+    return (song.num_tracks > 0);
+}
+
+static snddevice_t music_win_devices[] =
+{
+    SNDDEVICE_PAS,
+    SNDDEVICE_WAVEBLASTER,
+    SNDDEVICE_SOUNDCANVAS,
+    SNDDEVICE_GENMIDI,
+    SNDDEVICE_AWE32,
+};
+
+music_module_t music_win_module =
+{
+    music_win_devices,
+    arrlen(music_win_devices),
+    I_WIN_InitMusic,
+    I_WIN_ShutdownMusic,
+    I_WIN_SetMusicVolume,
+    I_WIN_PauseSong,
+    I_WIN_ResumeSong,
+    I_WIN_RegisterSong,
+    I_WIN_UnRegisterSong,
+    I_WIN_PlaySong,
+    I_WIN_StopSong,
+    I_WIN_MusicIsPlaying,
+    NULL,  // Poll
+};
 
 #endif
--- a/src/i_winmusic.h
+++ /dev/null
@@ -1,37 +1,0 @@
-//
-// Copyright(C) 2021 Roman Fomin
-//
-// 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:
-//      Windows native MIDI
-
-#ifndef __I_WINMUSIC__
-#define __I_WINMUSIC__
-
-#ifdef _WIN32
-
-#include "doomtype.h"
-
-boolean I_WIN_InitMusic(void);
-void I_WIN_PlaySong(boolean looping);
-void I_WIN_PauseSong(void);
-void I_WIN_ResumeSong(void);
-void I_WIN_StopSong(void);
-void I_WIN_SetMusicVolume(int volume);
-boolean I_WIN_RegisterSong(char* filename);
-void I_WIN_UnRegisterSong(void);
-void I_WIN_ShutdownMusic(void);
-
-
-#endif // _WIN32
-
-#endif // __I_WINMUSIC__
--- a/src/m_config.c
+++ b/src/m_config.c
@@ -973,13 +973,26 @@
     CONFIG_VARIABLE_STRING(winmm_midi_device),
 
     //!
-    // Reverb level for native Windows MIDI, default 40, range 0-127.
+    // Reset device type for native Windows MIDI, default 1. Valid values are
+    // 0 (None), 1 (GS Mode), 2 (GM Mode), 3 (GM2 Mode), 4 (XG Mode).
     //
 
+    CONFIG_VARIABLE_INT(winmm_reset_type),
+
+    //!
+    // Reset device delay for native Windows MIDI, default 0, median value 100 ms.
+    //
+
+    CONFIG_VARIABLE_INT(winmm_reset_delay),
+
+    //!
+    // Reverb level for native Windows MIDI, default -1, range 0-127.
+    //
+
     CONFIG_VARIABLE_INT(winmm_reverb_level),
 
     //!
-    // Chorus level for native Windows MIDI, default 0, range 0-127.
+    // Chorus level for native Windows MIDI, default -1, range 0-127.
     //
 
     CONFIG_VARIABLE_INT(winmm_chorus_level),
--- /dev/null
+++ b/src/midifallback.c
@@ -1,0 +1,366 @@
+//
+// 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
+//
+
+#ifdef _WIN32
+
+#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(const 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 <ch> 15 <map> <sum> F7
+    //
+    // <ch> is [11-19, 10, 1A-1F] for channels 1-16. Note the position of 10.
+    // <map> is 00-02 for off (normal part), drum map 1, or drum map 2.
+    // <sum> 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];
+    }
+}
+
+static boolean GetProgramFallback(byte idx, byte program,
+                                  midi_fallback_t *fallback)
+{
+    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;
+            return false;
+        }
+
+        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 true;
+        }
+
+        // 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 true;
+    }
+    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 true;
+        }
+    }
+
+    return false;
+}
+
+void MIDI_CheckFallback(const midi_event_t *event, midi_fallback_t *fallback)
+{
+    byte idx;
+    byte 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;
+
+                case EMIDI_CONTROLLER_PROGRAM_CHANGE:
+                    program = event->data.channel.param2;
+                    if (GetProgramFallback(idx, program, fallback))
+                    {
+                        return;
+                    }
+                    break;
+            }
+            break;
+
+        case MIDI_EVENT_PROGRAM_CHANGE:
+            idx = event->data.channel.channel;
+            program = event->data.channel.param1;
+            if (GetProgramFallback(idx, program, fallback))
+            {
+                return;
+            }
+            break;
+    }
+
+    fallback->type = FALLBACK_NONE;
+    fallback->value = 0;
+}
+
+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)
+{
+    byte 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;
+    }
+}
+
+#endif
--- /dev/null
+++ b/src/midifallback.h
@@ -1,0 +1,46 @@
+//
+// 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
+//
+
+#ifdef _WIN32
+
+#ifndef MIDIFALLBACK_H
+#define MIDIFALLBACK_H
+
+#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(const midi_event_t *event, midi_fallback_t *fallback);
+void MIDI_ResetFallback(void);
+void MIDI_InitFallback(void);
+
+#endif // MIDIFALLBACK_H
+
+#endif // _WIN32
--- a/src/midifile.c
+++ b/src/midifile.c
@@ -70,6 +70,7 @@
 {
     midi_track_t *track;
     unsigned int position;
+    unsigned int loop_point;
 };
 
 struct midi_file_s
@@ -638,21 +639,6 @@
     return file->num_tracks;
 }
 
-// Get the number of events in a MIDI file.
-
-unsigned int MIDI_NumEvents(midi_file_t *file)
-{
-    int i;
-    unsigned int num_events = 0;
-
-    for (i = 0; i < file->num_tracks; ++i)
-    {
-        num_events += file->tracks[i].num_events;
-    }
-
-    return num_events;
-}
-
 // Start iterating over the events in a track.
 
 midi_track_iter_t *MIDI_IterateTrack(midi_file_t *file, unsigned int track)
@@ -664,6 +650,7 @@
     iter = malloc(sizeof(*iter));
     iter->track = &file->tracks[track];
     iter->position = 0;
+    iter->loop_point = 0;
 
     return iter;
 }
@@ -728,6 +715,17 @@
 void MIDI_RestartIterator(midi_track_iter_t *iter)
 {
     iter->position = 0;
+    iter->loop_point = 0;
+}
+
+void MIDI_SetLoopPoint(midi_track_iter_t *iter)
+{
+    iter->loop_point = iter->position;
+}
+
+void MIDI_RestartAtLoopPoint(midi_track_iter_t *iter)
+{
+    iter->position = iter->loop_point;
 }
 
 #ifdef TEST
--- a/src/midifile.h
+++ b/src/midifile.h
@@ -27,54 +27,94 @@
 {
     MIDI_EVENT_NOTE_OFF        = 0x80,
     MIDI_EVENT_NOTE_ON         = 0x90,
-    MIDI_EVENT_AFTERTOUCH      = 0xa0,
-    MIDI_EVENT_CONTROLLER      = 0xb0,
-    MIDI_EVENT_PROGRAM_CHANGE  = 0xc0,
-    MIDI_EVENT_CHAN_AFTERTOUCH = 0xd0,
-    MIDI_EVENT_PITCH_BEND      = 0xe0,
+    MIDI_EVENT_AFTERTOUCH      = 0xA0,
+    MIDI_EVENT_CONTROLLER      = 0xB0,
+    MIDI_EVENT_PROGRAM_CHANGE  = 0xC0,
+    MIDI_EVENT_CHAN_AFTERTOUCH = 0xD0,
+    MIDI_EVENT_PITCH_BEND      = 0xE0,
 
-    MIDI_EVENT_SYSEX           = 0xf0,
-    MIDI_EVENT_SYSEX_SPLIT     = 0xf7,
-    MIDI_EVENT_META            = 0xff,
+    MIDI_EVENT_SYSEX           = 0xF0,
+    MIDI_EVENT_SYSEX_SPLIT     = 0xF7,
+    MIDI_EVENT_META            = 0xFF,
 } midi_event_type_t;
 
 typedef enum
 {
-    MIDI_CONTROLLER_BANK_SELECT     = 0x0,
-    MIDI_CONTROLLER_MODULATION      = 0x1,
-    MIDI_CONTROLLER_BREATH_CONTROL  = 0x2,
-    MIDI_CONTROLLER_FOOT_CONTROL    = 0x3,
-    MIDI_CONTROLLER_PORTAMENTO      = 0x4,
-    MIDI_CONTROLLER_DATA_ENTRY      = 0x5,
+    MIDI_CONTROLLER_BANK_SELECT_MSB = 0x00,
+    MIDI_CONTROLLER_MODULATION      = 0x01,
+    MIDI_CONTROLLER_BREATH_CONTROL  = 0x02,
+    MIDI_CONTROLLER_FOOT_CONTROL    = 0x04,
+    MIDI_CONTROLLER_PORTAMENTO      = 0x05,
+    MIDI_CONTROLLER_DATA_ENTRY_MSB  = 0x06,
+    MIDI_CONTROLLER_VOLUME_MSB      = 0x07,
+    MIDI_CONTROLLER_PAN             = 0x0A,
 
-    MIDI_CONTROLLER_MAIN_VOLUME     = 0x7,
-    MIDI_CONTROLLER_PAN             = 0xa,
+    MIDI_CONTROLLER_BANK_SELECT_LSB = 0x20,
+    MIDI_CONTROLLER_DATA_ENTRY_LSB  = 0x26,
+    MIDI_CONTROLLER_VOLUME_LSB      = 0X27,
 
-    MIDI_CONTROLLER_ALL_NOTES_OFF   = 0x7b,
+    MIDI_CONTROLLER_REVERB          = 0x5B,
+    MIDI_CONTROLLER_CHORUS          = 0x5D,
+
+    MIDI_CONTROLLER_RPN_LSB         = 0x64,
+    MIDI_CONTROLLER_RPN_MSB         = 0x65,
+
+    MIDI_CONTROLLER_ALL_SOUND_OFF   = 0x78,
+    MIDI_CONTROLLER_RESET_ALL_CTRLS = 0x79,
+    MIDI_CONTROLLER_ALL_NOTES_OFF   = 0x7B,
 } midi_controller_t;
 
 typedef enum
 {
-    MIDI_META_SEQUENCE_NUMBER       = 0x0,
+    MIDI_META_SEQUENCE_NUMBER       = 0x00,
 
-    MIDI_META_TEXT                  = 0x1,
-    MIDI_META_COPYRIGHT             = 0x2,
-    MIDI_META_TRACK_NAME            = 0x3,
-    MIDI_META_INSTR_NAME            = 0x4,
-    MIDI_META_LYRICS                = 0x5,
-    MIDI_META_MARKER                = 0x6,
-    MIDI_META_CUE_POINT             = 0x7,
+    MIDI_META_TEXT                  = 0x01,
+    MIDI_META_COPYRIGHT             = 0x02,
+    MIDI_META_TRACK_NAME            = 0x03,
+    MIDI_META_INSTR_NAME            = 0x04,
+    MIDI_META_LYRICS                = 0x05,
+    MIDI_META_MARKER                = 0x06,
+    MIDI_META_CUE_POINT             = 0x07,
 
     MIDI_META_CHANNEL_PREFIX        = 0x20,
-    MIDI_META_END_OF_TRACK          = 0x2f,
+    MIDI_META_END_OF_TRACK          = 0x2F,
 
     MIDI_META_SET_TEMPO             = 0x51,
     MIDI_META_SMPTE_OFFSET          = 0x54,
     MIDI_META_TIME_SIGNATURE        = 0x58,
     MIDI_META_KEY_SIGNATURE         = 0x59,
-    MIDI_META_SEQUENCER_SPECIFIC    = 0x7f,
+    MIDI_META_SEQUENCER_SPECIFIC    = 0x7F,
 } midi_meta_event_type_t;
 
+#define EMIDI_LOOP_FLAG 0x7F
+
+typedef enum
+{
+    EMIDI_DEVICE_GENERAL_MIDI  = 0x00,
+    EMIDI_DEVICE_SOUND_CANVAS  = 0x01,
+    EMIDI_DEVICE_AWE32         = 0x02,
+    EMIDI_DEVICE_WAVE_BLASTER  = 0x03,
+    EMIDI_DEVICE_SOUND_BLASTER = 0x04,
+    EMIDI_DEVICE_PRO_AUDIO     = 0x05,
+    EMIDI_DEVICE_SOUND_MAN_16  = 0x06,
+    EMIDI_DEVICE_ADLIB         = 0x07,
+    EMIDI_DEVICE_SOUNDSCAPE    = 0x08,
+    EMIDI_DEVICE_ULTRASOUND    = 0x09,
+    EMIDI_DEVICE_ALL           = 0x7F,
+} emidi_device_t;
+
+typedef enum
+{
+    EMIDI_CONTROLLER_TRACK_DESIGNATION = 0x6E,
+    EMIDI_CONTROLLER_TRACK_EXCLUSION   = 0x6F,
+    EMIDI_CONTROLLER_PROGRAM_CHANGE    = 0x70,
+    EMIDI_CONTROLLER_VOLUME            = 0x71,
+    EMIDI_CONTROLLER_LOOP_BEGIN        = 0x74,
+    EMIDI_CONTROLLER_LOOP_END          = 0x75,
+    EMIDI_CONTROLLER_GLOBAL_LOOP_BEGIN = 0x76,
+    EMIDI_CONTROLLER_GLOBAL_LOOP_END   = 0x77,
+} emidi_controller_t;
+
 typedef struct
 {
     // Meta event type:
@@ -145,10 +185,6 @@
 
 unsigned int MIDI_NumTracks(midi_file_t *file);
 
-// Get the number of events in a MIDI file.
-
-unsigned int MIDI_NumEvents(midi_file_t *file);
-
 // Start iterating over the events in a track.
 
 midi_track_iter_t *MIDI_IterateTrack(midi_file_t *file, unsigned int track_num);
@@ -168,6 +204,14 @@
 // Reset an iterator to the beginning of a track.
 
 void MIDI_RestartIterator(midi_track_iter_t *iter);
+
+// Set loop point to current position.
+
+void MIDI_SetLoopPoint(midi_track_iter_t *iter);
+
+// Set position to saved loop point.
+
+void MIDI_RestartAtLoopPoint(midi_track_iter_t *iter);
 
 #endif /* #ifndef MIDIFILE_H */