//
// 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
// 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

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <mmsystem.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 "midifallback.h"

enum
{
    COMP_VANILLA,
    COMP_STANDARD,
    COMP_FULL,
};

enum
{
    RESET_TYPE_NONE,
    RESET_TYPE_GM,
    RESET_TYPE_GS,
    RESET_TYPE_XG,
};

char *winmm_midi_device = NULL;
int winmm_complevel = COMP_VANILLA;
int winmm_reset_type = RESET_TYPE_GM;
int winmm_reset_delay = 0;

static const byte gm_system_on[] = {
    0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7
};

static const byte gs_reset[] = {
    0xF0, 0x41, 0x10, 0x42, 0x12, 0x40, 0x00, 0x7F, 0x00, 0x41, 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 byte channel_volume[MIDI_CHANNELS_PER_TRACK];
static float volume_factor = 0.0f;
static boolean update_volume = false;

typedef enum
{
    STATE_STARTUP,
    STATE_SHUTDOWN,
    STATE_EXIT,
    STATE_STOPPING,
    STATE_STOPPED,
    STATE_PLAYING,
    STATE_PAUSED
} win_midi_state_t;

static win_midi_state_t win_midi_state;

static DWORD timediv;
static DWORD tempo;

static UINT MidiDevice;
static HMIDISTRM hMidiStream;
static MIDIHDR MidiStreamHdr;
static HANDLE hBufferReturnEvent;
static HANDLE hStoppedEvent;
static HANDLE hPlayerThread;
static CRITICAL_SECTION CriticalSection;

#define EMIDI_DEVICE (1U << EMIDI_DEVICE_GENERAL_MIDI)

// This is a reduced Windows MIDIEVENT structure for MEVT_F_SHORT
// type of events.

typedef struct
{
    DWORD dwDeltaTime;
    DWORD dwStreamID; // always 0
    DWORD dwEvent;
} native_event_t;

typedef struct
{
    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 registered;
    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 8192

#define PLAYER_THREAD_WAIT_TIME 3000

typedef struct
{
    byte *data;
    unsigned int size;
    unsigned int position;
    boolean prepared;
} buffer_t;

static buffer_t buffer;

// Maximum of 4 events in the buffer for faster volume updates.

#define STREAM_MAX_EVENTS   4

#define MAKE_EVT(a, b, c, d)                                                   \
    ((DWORD)(a) | ((DWORD)(b) << 8) | ((DWORD)(c) << 16) | ((DWORD)(d) << 24))

#define PADDED_SIZE(x) (((x) + sizeof(DWORD) - 1) & ~(sizeof(DWORD) - 1))

// Check for midiStream errors.

static void MidiError(const char *prefix, DWORD dwError)
{
    wchar_t werror[MAXERRORLENGTH];
    MMRESULT mmr;

    mmr = midiOutGetErrorTextW(dwError, (LPWSTR) werror, MAXERRORLENGTH);
    if (mmr == MMSYSERR_NOERROR)
    {
        char *error = M_ConvertWideToUtf8(werror);
        fprintf(stderr, "%s: %s.\n", prefix, error);
        free(error);
    }
    else
    {
        fprintf(stderr, "%s: Unknown midiStream error.\n", prefix);
    }
}

// midiStream callback.

static void CALLBACK MidiStreamProc(HMIDIOUT hMidi, UINT uMsg,
                                    DWORD_PTR dwInstance, DWORD_PTR dwParam1,
                                    DWORD_PTR dwParam2)
{
    if (uMsg == MOM_DONE)
    {
        SetEvent(hBufferReturnEvent);
    }
}

// Unprepare MIDI header. The calling thread must have exclusive access to the
// shared resources in this function.

static void UnprepareHeader(void)
{
    // Avoid ASan detection. Commentary by Microsoft: "It looks like
    // midiOutPrepareHeader() allocates with HeapAlloc(), and then
    // midiOutUnprepareHeader() deallocates with GlobalFree(GlobalHandle
    // (...)). By design, this kind of allocator mismatch is an issue that ASan
    // is designed to catch. It is theoretically possible for us to support
    // this kind of code, but it’s not very high priority since it is undefined
    // behavior, though it happens to work right now outside of ASan."
    // https://developercommunity.visualstudio.com/t/1597288

#ifndef __SANITIZE_ADDRESS__
    MIDIHDR *hdr = &MidiStreamHdr;
    MMRESULT mmr;

    mmr = midiOutUnprepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
    if (mmr != MMSYSERR_NOERROR)
    {
        MidiError("midiOutUnprepareHeader", mmr);
    }
#endif
}

// Allocate buffer. The calling thread must have exclusive access to the shared
// resources in this function.

static void AllocateBuffer(const unsigned int size)
{
    buffer.size = PADDED_SIZE(size);
    buffer.data = I_Realloc(buffer.data, buffer.size);
}

// Pads the buffer with zeros so that an integral number of DWORDs are stored.
// Required for long messages (SysEx). Call this function from the MIDI thread
// only, with exclusive access to shared resources.

static void WriteBufferPad(void)
{
    unsigned int padding = PADDED_SIZE(buffer.position);
    memset(buffer.data + buffer.position, 0, padding - buffer.position);
    buffer.position = padding;
}

// Writes message data to buffer. Call this function from the MIDI thread only,
// with exclusive access to shared resources.

static void WriteBuffer(const byte *ptr, unsigned int size)
{
    if (buffer.prepared)
    {
        UnprepareHeader();
        buffer.prepared = false;
    }

    if (buffer.position + size >= buffer.size)
    {
        AllocateBuffer(size + buffer.size * 2);
    }

    memcpy(buffer.data + buffer.position, ptr, size);
    buffer.position += size;
}

// Streams out the current buffer. Call this function from the MIDI thread only,
// with exclusive access to shared resources.

static void StreamOut(void)
{
    MIDIHDR *hdr = &MidiStreamHdr;
    MMRESULT mmr;

    memset(hdr, 0, sizeof(*hdr));
    hdr->lpData = (LPSTR)buffer.data;
    hdr->dwBytesRecorded = buffer.position;
    hdr->dwBufferLength = buffer.size;

    // Reset buffer position even if midiStreamOut fails.
    buffer.position = 0;

    mmr = midiOutPrepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
    if (mmr != MMSYSERR_NOERROR)
    {
        MidiError("midiOutPrepareHeader", mmr);
        return;
    }

    buffer.prepared = true;

    mmr = midiStreamOut(hMidiStream, hdr, sizeof(MIDIHDR));
    if (mmr != MMSYSERR_NOERROR)
    {
        MidiError("midiStreamOut", mmr);
    }
}

// Writes a short MIDI message. Call this function from the MIDI thread only,
// with exclusive access to shared resources.

static void SendShortMsg(unsigned int delta_time, byte status, byte channel,
                         byte param1, byte param2)
{
    native_event_t native_event;
    native_event.dwDeltaTime = delta_time;
    native_event.dwStreamID = 0;
    native_event.dwEvent = MAKE_EVT(status | channel, param1, param2, MEVT_SHORTMSG);
    WriteBuffer((byte *)&native_event, sizeof(native_event_t));
}

// Writes a short MIDI message (from an event). Call this function from the MIDI
// thread only, with exclusive access to shared resources.

static void SendChannelMsg(unsigned int delta_time, const midi_event_t *event,
                           boolean use_param2)
{
    SendShortMsg(delta_time, event->event_type, event->data.channel.channel,
                 event->data.channel.param1,
                 use_param2 ? event->data.channel.param2 : 0);
}

// Writes a long MIDI message (SysEx). Call this function from the MIDI thread
// only, with exclusive access to shared resources.

static void SendLongMsg(unsigned int delta_time, const byte *ptr,
                        unsigned int length)
{
    native_event_t native_event;
    native_event.dwDeltaTime = delta_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();
}

// Writes an RPN message set to NULL (0x7F). Prevents accidental data entry.
// Call this function from the MIDI thread only, with exclusive access to shared
// resources.

static void SendNullRPN(unsigned int delta_time, const midi_event_t *event)
{
    const byte channel = event->data.channel.channel;
    SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER, channel,
                 MIDI_CONTROLLER_RPN_LSB, MIDI_RPN_NULL);
    SendShortMsg(0, MIDI_EVENT_CONTROLLER, channel,
                 MIDI_CONTROLLER_RPN_MSB, MIDI_RPN_NULL);
}

// Writes a NOP message (ticks). Call this function from the MIDI thread only,
// with exclusive access to shared resources.

static void SendNOPMsg(unsigned int delta_time)
{
    native_event_t native_event;
    native_event.dwDeltaTime = delta_time;
    native_event.dwStreamID = 0;
    native_event.dwEvent = MAKE_EVT(0, 0, 0, MEVT_NOP);
    WriteBuffer((byte *)&native_event, sizeof(native_event_t));
}

// Writes a NOP message (milliseconds). Call this function from the MIDI thread
// only, with exclusive access to shared resources.

static void SendDelayMsg(unsigned int time_ms)
{
    // Convert ms to ticks (see "Standard MIDI Files 1.0" page 14).
    const unsigned int ticks = (float) time_ms * 1000 * timediv / tempo + 0.5f;
    SendNOPMsg(ticks);
}

// Writes a tempo MIDI meta message. Call this function from the MIDI thread
// only, with exclusive access to shared resources.

static void UpdateTempo(unsigned int delta_time, const 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 = delta_time;
    native_event.dwStreamID = 0;
    native_event.dwEvent = MAKE_EVT(tempo, 0, 0, MEVT_TEMPO);
    WriteBuffer((byte *)&native_event, sizeof(native_event_t));
}

// Writes a MIDI volume message. The value is scaled by the volume slider. Call
// this function from the MIDI thread only, with exclusive access to shared
// resources.

static void SendManualVolumeMsg(unsigned int delta_time, byte channel,
                                byte volume)
{
    unsigned int scaled_volume;

    scaled_volume = volume * volume_factor + 0.5f;

    if (scaled_volume > 127)
    {
        scaled_volume = 127;
    }

    SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER, channel,
                 MIDI_CONTROLLER_VOLUME_MSB, scaled_volume);

    channel_volume[channel] = volume;
}

// Writes a MIDI volume message (from an event). The value is scaled by the
// volume slider. Call this function from the MIDI thread only, with exclusive
// access to shared resources.

static void SendVolumeMsg(unsigned int delta_time, const midi_event_t *event)
{
    SendManualVolumeMsg(delta_time, event->data.channel.channel,
                        event->data.channel.param2);
}

// Sets each channel to its saved volume level, scaled by the volume slider.
// Call this function from the MIDI thread only, with exclusive access to shared
// resources.

static void UpdateVolume(void)
{
    int i;

    for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
    {
        SendManualVolumeMsg(0, i, channel_volume[i]);
    }
}

// Sets each channel to the default volume level, scaled by the volume slider.
// Call this function from the MIDI thread only, with exclusive access to shared
// resources.

static void ResetVolume(void)
{
    int i;

    for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
    {
        SendManualVolumeMsg(0, i, DEFAULT_VOLUME);
    }
}

// Writes "notes off" and "sound off" messages for each channel. Some devices
// may support only one or the other. Held notes (sustained, etc.) are released
// to prevent hanging notes. Call this function from the MIDI thread only, with
// exclusive access to shared resources.

static void SendNotesSoundOff(void)
{
    int i;

    for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
    {
        SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_ALL_NOTES_OFF, 0);
        SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_ALL_SOUND_OFF, 0);
    }
}

// Resets commonly used controllers. This is only for a reset type of "none" for
// devices that don't support SysEx resets. Call this function from the MIDI
// thread only, with exclusive access to shared resources.

static void ResetControllers(void)
{
    int i;

    for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
    {
        // Reset commonly used controllers.
        SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RESET_ALL_CTRLS, 0);
        SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_PAN, 64);
        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);
        SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_REVERB, 40);
        SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_CHORUS, 0);
    }
}

// Resets the pitch bend sensitivity for each channel. This must be sent during
// a reset due to an MS GS Wavetable Synth bug. Call this function from the MIDI
// thread only, with exclusive access to shared resources.

static void ResetPitchBendSensitivity(void)
{
    int i;

    for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
    {
        // 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);
    }
}

// Resets the MIDI device. Call this function before each song starts and once
// at shut down. Call this function from the MIDI thread only, with exclusive
// access to shared resources.

static void ResetDevice(void)
{
    MIDI_ResetFallback();
    use_fallback = false;

    switch (winmm_reset_type)
    {
        case RESET_TYPE_NONE:
            ResetControllers();
            break;

        case RESET_TYPE_GS:
            SendLongMsg(0, gs_reset, sizeof(gs_reset));
            use_fallback = (winmm_complevel != COMP_VANILLA);
            break;

        case RESET_TYPE_XG:
            SendLongMsg(0, xg_system_on, sizeof(xg_system_on));
            break;

        default:
            SendLongMsg(0, gm_system_on, sizeof(gm_system_on));
            break;
    }

    // MS GS Wavetable Synth doesn't reset pitch bend sensitivity.
    ResetPitchBendSensitivity();

    // Reset volume (initial playback or on shutdown if no SysEx reset).
    // Scale by slider on initial playback, max on shutdown.
    if (win_midi_state == STATE_STARTUP)
    {
        ResetVolume();
    }
    else if (winmm_reset_type == RESET_TYPE_NONE)
    {
        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);
    }
}

// Normally, volume is controlled by channel volume messages. Roland defined a
// special SysEx message called "part level" that is equivalent to this. MS GS
// Wavetable Synth ignores these messages, but other MIDI devices support them.
// Returns true if there is a match. Call this function from the MIDI thread
// only, with exclusive access to shared resources.

static boolean IsPartLevel(const byte *msg, unsigned int length)
{
    if (length == 10 &&
        msg[0] == 0x41 && // Roland
        msg[2] == 0x42 && // GS
        msg[3] == 0x12 && // DT1
        msg[4] == 0x40 && // Address MSB
        msg[5] >= 0x10 && // Address
        msg[5] <= 0x1F && // Address
        msg[6] == 0x19 && // Address LSB
        msg[9] == 0xF7)   // SysEx EOX
    {
        const byte checksum =
            128 - ((int) msg[4] + msg[5] + msg[6] + msg[7]) % 128;

        if (msg[8] == checksum)
        {
            // GS Part Level (aka Channel Volume)
            // 41 <dev> 42 12 40 <ch> 19 <vol> <sum> F7
            return true;
        }
    }

    return false;
}

// Checks if the current SysEx message matches any known SysEx reset message.
// Returns true if there is a match. Call this function from the MIDI thread
// only, with exclusive access to shared resources.

static boolean IsSysExReset(const byte *msg, unsigned 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 <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;
}

// Writes a MIDI SysEx message. Call this function from the MIDI thread only,
// with exclusive access to shared resources.

static void SendSysExMsg(unsigned int delta_time, const midi_event_t *event)
{
    native_event_t native_event;
    const byte event_type = MIDI_EVENT_SYSEX;
    const byte *data = event->data.sysex.data;
    const unsigned int length = event->data.sysex.length;

    if (IsPartLevel(data, length))
    {
        byte channel;

        // Convert "block number" to a channel number.
        if (data[5] == 0x10) // Channel 10
        {
            channel = 9;
        }
        else if (data[5] < 0x1A) // Channels 1-9
        {
            channel = (data[5] & 0x0F) - 1;
        }
        else // Channels 11-16
        {
            channel = data[5] & 0x0F;
        }

        // Replace SysEx part level message with channel volume message.
        SendManualVolumeMsg(delta_time, channel, data[7]);
        return;
    }

    // Send the SysEx message.
    native_event.dwDeltaTime = delta_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 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.
        if (use_fallback)
        {
            MIDI_ResetFallback();
            use_fallback = false;
        }
    }
}

// Writes a MIDI program change message. If applicable, emulates capital tone
// fallback to fix invalid instruments. Call this function from the MIDI thread
// only, with exclusive access to shared resources.

static void SendProgramMsg(unsigned int delta_time, byte channel, byte program,
                           const midi_fallback_t *fallback)
{
    switch ((int)fallback->type)
    {
        case FALLBACK_BANK_MSB:
            SendShortMsg(delta_time, MIDI_EVENT_CONTROLLER, channel,
                         MIDI_CONTROLLER_BANK_SELECT_MSB, fallback->value);
            SendShortMsg(0, MIDI_EVENT_PROGRAM_CHANGE, channel, program, 0);
            break;

        case FALLBACK_DRUMS:
            SendShortMsg(delta_time, MIDI_EVENT_PROGRAM_CHANGE, channel,
                         fallback->value, 0);
            break;

        default:
            SendShortMsg(delta_time, MIDI_EVENT_PROGRAM_CHANGE, channel,
                         program, 0);
            break;
    }
}

// Sets a Final Fantasy or RPG Maker loop point. Call this function from the
// MIDI thread only, with exclusive access to shared resources.

static void SetLoopPoint(void)
{
    unsigned int i;

    for (i = 0; i < song.num_tracks; ++i)
    {
        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;
}

// Checks if the MIDI meta message contains a Final Fantasy loop marker. Call
// this function from the MIDI thread only, with exclusive access to shared
// resources.

static void CheckFFLoop(const 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;
    }
}

// Writes an EMIDI message. Call this function from the MIDI thread only, with
// exclusive access to shared resources.

static void SendEMIDI(unsigned int delta_time, const midi_event_t *event,
                      win_midi_track_t *track, const midi_fallback_t *fallback)
{
    unsigned int i;
    unsigned int flag;
    int count;

    switch (event->data.channel.param1)
    {
        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 |= 1U << flag;
                    track->emidi_designated = true;
                }
            }
            SendNOPMsg(delta_time);
            break;

        case EMIDI_CONTROLLER_TRACK_EXCLUSION:
            if (song.rpg_loop)
            {
                SetLoopPoint();
            }
            else if (track->elapsed_time < timediv)
            {
                flag = event->data.channel.param2;

                if (!track->emidi_designated)
                {
                    track->emidi_device_flags = UINT_MAX;
                    track->emidi_designated = true;
                }

                if (flag <= EMIDI_DEVICE_ULTRASOUND)
                {
                    track->emidi_device_flags &= ~(1U << flag);
                }
            }
            SendNOPMsg(delta_time);
            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 EMIDI_CONTROLLER_VOLUME:
            if (track->emidi_volume || track->elapsed_time < timediv)
            {
                track->emidi_volume = true;
                SendVolumeMsg(delta_time, event);
            }
            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 (track->emidi_loop_count > 0)
                {
                    track->emidi_loop_count--;
                }
            }
            SendNOPMsg(delta_time);
            break;

        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);
                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;
            SendNOPMsg(delta_time);
            break;

        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);
                        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;
                    }

                    if (song.tracks[i].emidi_loop_count > 0)
                    {
                        song.tracks[i].emidi_loop_count--;
                    }
                }
            }
            SendNOPMsg(delta_time);
            break;
    }
}

// Writes a MIDI meta message. Call this function from the MIDI thread only,
// with exclusive access to shared resources.

static void SendMetaMsg(unsigned int delta_time, const midi_event_t *event,
                        win_midi_track_t *track)
{
    switch (event->data.meta.type)
    {
        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:
            if (winmm_complevel != COMP_VANILLA)
            {
                CheckFFLoop(event);
            }
            SendNOPMsg(delta_time);
            break;

        default:
            SendNOPMsg(delta_time);
            break;
    }
}

// AddToBuffer function for vanilla (DMX MPU-401) compatibility level. Do not
// call this function directly. See the AddToBuffer function pointer.

static boolean AddToBuffer_Vanilla(unsigned int delta_time,
                                   const midi_event_t *event,
                                   win_midi_track_t *track)
{
    switch ((int) event->event_type)
    {
        case MIDI_EVENT_SYSEX:
            SendNOPMsg(delta_time);
            return false;

        case MIDI_EVENT_META:
            SendMetaMsg(delta_time, event, track);
            break;

        case MIDI_EVENT_CONTROLLER:
            switch (event->data.channel.param1)
            {
                case MIDI_CONTROLLER_BANK_SELECT_MSB:
                case MIDI_CONTROLLER_BANK_SELECT_LSB:
                    // DMX has broken bank select support and runs in GM mode.
                    SendChannelMsg(delta_time, event, false);
                    break;

                case MIDI_CONTROLLER_MODULATION:
                case MIDI_CONTROLLER_PAN:
                case MIDI_CONTROLLER_EXPRESSION:
                case MIDI_CONTROLLER_HOLD1_PEDAL:
                case MIDI_CONTROLLER_SOFT_PEDAL:
                case MIDI_CONTROLLER_REVERB:
                case MIDI_CONTROLLER_CHORUS:
                case MIDI_CONTROLLER_ALL_SOUND_OFF:
                case MIDI_CONTROLLER_ALL_NOTES_OFF:
                    SendChannelMsg(delta_time, event, true);
                    break;

                case MIDI_CONTROLLER_VOLUME_MSB:
                    SendVolumeMsg(delta_time, event);
                    break;

                case MIDI_CONTROLLER_RESET_ALL_CTRLS:
                    // MS GS Wavetable Synth resets volume if param2 isn't zero.
                    SendChannelMsg(delta_time, event, false);
                    break;

                default:
                    SendNOPMsg(delta_time);
                    break;
            }
            break;

        case MIDI_EVENT_NOTE_OFF:
        case MIDI_EVENT_NOTE_ON:
        case MIDI_EVENT_PITCH_BEND:
            SendChannelMsg(delta_time, event, true);
            break;

        case MIDI_EVENT_PROGRAM_CHANGE:
            SendChannelMsg(delta_time, event, false);
            break;

        default:
            SendNOPMsg(delta_time);
            break;
    }

    return true;
}

// AddToBuffer function for standard and full MIDI compatibility levels. Do not
// call this function directly. See the AddToBuffer function pointer.

static boolean AddToBuffer_Standard(unsigned int delta_time,
                                    const midi_event_t *event,
                                    win_midi_track_t *track)
{
    midi_fallback_t fallback = {FALLBACK_NONE, 0};

    if (use_fallback)
    {
        MIDI_CheckFallback(event, &fallback, winmm_complevel == COMP_FULL);
    }

    switch ((int) event->event_type)
    {
        case MIDI_EVENT_SYSEX:
            if (winmm_complevel == COMP_FULL)
            {
                SendSysExMsg(delta_time, event);
            }
            else
            {
                SendNOPMsg(delta_time);
            }
            return false;

        case MIDI_EVENT_META:
            SendMetaMsg(delta_time, event, track);
            return true;
    }

    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;
    }

    switch ((int) event->event_type)
    {
        case MIDI_EVENT_CONTROLLER:
            switch (event->data.channel.param1)
            {
                case MIDI_CONTROLLER_BANK_SELECT_MSB:
                case MIDI_CONTROLLER_MODULATION:
                case MIDI_CONTROLLER_DATA_ENTRY_MSB:
                case MIDI_CONTROLLER_PAN:
                case MIDI_CONTROLLER_EXPRESSION:
                case MIDI_CONTROLLER_DATA_ENTRY_LSB:
                case MIDI_CONTROLLER_HOLD1_PEDAL:
                case MIDI_CONTROLLER_SOFT_PEDAL:
                case MIDI_CONTROLLER_REVERB:
                case MIDI_CONTROLLER_CHORUS:
                case MIDI_CONTROLLER_ALL_SOUND_OFF:
                case MIDI_CONTROLLER_ALL_NOTES_OFF:
                case MIDI_CONTROLLER_POLY_MODE_OFF:
                case MIDI_CONTROLLER_POLY_MODE_ON:
                    SendChannelMsg(delta_time, event, true);
                    break;

                case MIDI_CONTROLLER_VOLUME_MSB:
                    if (track->emidi_volume)
                    {
                        SendNOPMsg(delta_time);
                    }
                    else
                    {
                        SendVolumeMsg(delta_time, event);
                    }
                    break;

                case MIDI_CONTROLLER_VOLUME_LSB:
                    SendNOPMsg(delta_time);
                    break;

                case MIDI_CONTROLLER_BANK_SELECT_LSB:
                    SendChannelMsg(delta_time, event,
                                   fallback.type != FALLBACK_BANK_LSB);
                    break;

                case MIDI_CONTROLLER_NRPN_LSB:
                case MIDI_CONTROLLER_NRPN_MSB:
                    if (winmm_complevel == COMP_FULL)
                    {
                        SendChannelMsg(delta_time, event, true);
                    }
                    else
                    {
                        // MS GS Wavetable Synth nulls RPN for any NRPN.
                        SendNullRPN(delta_time, event);
                    }
                    break;

                case MIDI_CONTROLLER_RPN_LSB:
                    switch (event->data.channel.param2)
                    {
                        case MIDI_RPN_PITCH_BEND_SENS_LSB:
                        case MIDI_RPN_FINE_TUNING_LSB:
                        case MIDI_RPN_COARSE_TUNING_LSB:
                        case MIDI_RPN_NULL:
                            SendChannelMsg(delta_time, event, true);
                            break;

                        default:
                            if (winmm_complevel == COMP_FULL)
                            {
                                SendChannelMsg(delta_time, event, true);
                            }
                            else
                            {
                                // MS GS Wavetable Synth ignores other RPNs.
                                SendNullRPN(delta_time, event);
                            }
                            break;
                    }
                    break;

                case MIDI_CONTROLLER_RPN_MSB:
                    switch (event->data.channel.param2)
                    {
                        case MIDI_RPN_MSB:
                        case MIDI_RPN_NULL:
                            SendChannelMsg(delta_time, event, true);
                            break;

                        default:
                            if (winmm_complevel == COMP_FULL)
                            {
                                SendChannelMsg(delta_time, event, true);
                            }
                            else
                            {
                                // MS GS Wavetable Synth ignores other RPNs.
                                SendNullRPN(delta_time, event);
                            }
                            break;
                    }
                    break;

                case EMIDI_CONTROLLER_TRACK_DESIGNATION:
                case EMIDI_CONTROLLER_TRACK_EXCLUSION:
                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:
                    SendEMIDI(delta_time, event, track, &fallback);
                    break;

                case MIDI_CONTROLLER_RESET_ALL_CTRLS:
                    // MS GS Wavetable Synth resets volume if param2 isn't zero.
                    SendChannelMsg(delta_time, event, false);
                    break;

                default:
                    if (winmm_complevel == COMP_FULL)
                    {
                        SendChannelMsg(delta_time, event, true);
                    }
                    else
                    {
                        SendNOPMsg(delta_time);
                    }
                    break;
            }
            break;

        case MIDI_EVENT_NOTE_OFF:
        case MIDI_EVENT_NOTE_ON:
        case MIDI_EVENT_PITCH_BEND:
            SendChannelMsg(delta_time, event, true);
            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_AFTERTOUCH:
            if (winmm_complevel == COMP_FULL)
            {
                SendChannelMsg(delta_time, event, true);
            }
            else
            {
                SendNOPMsg(delta_time);
            }
            break;

        case MIDI_EVENT_CHAN_AFTERTOUCH:
            if (winmm_complevel == COMP_FULL)
            {
                SendChannelMsg(delta_time, event, false);
            }
            else
            {
                SendNOPMsg(delta_time);
            }
            break;

        default:
            SendNOPMsg(delta_time);
            break;
    }

    return true;
}

// Function pointer determined by the desired MIDI compatibility level. Set
// during initialization by the main thread, then called from the MIDI thread
// only. The calling thread must have exclusive access to the shared resources
// in this function.

static boolean (*AddToBuffer)(unsigned int delta_time,
                              const midi_event_t *event,
                              win_midi_track_t *track) = AddToBuffer_Vanilla;

// Restarts a song that uses a Final Fantasy or RPG Maker loop point. Call this
// function from the MIDI thread only, with exclusive access to shared
// resources.

static void RestartLoop(void)
{
    unsigned int i;

    for (i = 0; i < song.num_tracks; ++i)
    {
        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;
}

// Restarts a song that uses standard looping. Call this function from the MIDI
// thread only, with exclusive access to shared resources.

static void RestartTracks(void)
{
    unsigned int i;

    for (i = 0; i < song.num_tracks; ++i)
    {
        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;
}

// The controllers "EMIDI track exclusion" and "RPG Maker loop point" share the
// same number (CC#111) and are not compatible with each other. As a workaround,
// allow an RPG Maker loop point only if no other EMIDI events are present. Call
// this function from the MIDI thread only, before the song starts, with
// exclusive access to shared resources.

static boolean IsRPGLoop(void)
{
    unsigned int i;
    unsigned int num_rpg_events = 0;
    unsigned int num_emidi_events = 0;
    midi_event_t *event = NULL;

    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;

                    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);
}

// Fills the output buffer with events from the current song and then streams it
// out. Call this function from the MIDI thread only, with exclusive access to
// shared resources.

static void FillBuffer(void)
{
    unsigned int i;
    int num_events;

    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;

        // 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];
                }
            }
        }

        // 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)
                {
                    for (i = 0; i < MIDI_CHANNELS_PER_TRACK; ++i)
                    {
                        SendShortMsg(0, MIDI_EVENT_CONTROLLER, i, MIDI_CONTROLLER_RESET_ALL_CTRLS, 0);
                    }
                    RestartTracks();
                    continue;
                }
            }
            break;
        }

        track->elapsed_time = min_time;
        delta_time = min_time - song.elapsed_time;
        song.elapsed_time = min_time;

        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();
    }
}

// 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 a thread to avoid possible deadlocks.

static DWORD WINAPI PlayerProc(void)
{
    boolean keep_going = true;

    while (keep_going)
    {
        if (WaitForSingleObject(hBufferReturnEvent, INFINITE) != WAIT_OBJECT_0)
        {
            continue;
        }

        // The MIDI thread must have exclusive access to shared resources until
        // the end of the current loop iteration or when the thread exits.
        EnterCriticalSection(&CriticalSection);

        switch (win_midi_state)
        {
            case STATE_STARTUP:
                ResetDevice();
                StreamOut();
                song.rpg_loop = IsRPGLoop();
                win_midi_state = STATE_PLAYING;
                break;

            case STATE_SHUTDOWN:
                // Send notes/sound off prior to reset to prevent volume spikes.
                SendNotesSoundOff();
                ResetDevice();
                StreamOut();
                win_midi_state = STATE_EXIT;
                break;

            case STATE_EXIT:
                keep_going = false;
                break;

            case STATE_PLAYING:
                if (update_volume)
                {
                    UpdateVolume();
                    StreamOut();
                    update_volume = false;
                    break;
                }
                FillBuffer();
                break;

            case STATE_STOPPING:
                // Send notes/sound off to prevent hanging notes.
                SendNotesSoundOff();
                StreamOut();
                win_midi_state = STATE_STOPPED;
                break;

            case STATE_STOPPED:
                SetEvent(hStoppedEvent);
                break;

            case STATE_PAUSED:
                break;
        }

        LeaveCriticalSection(&CriticalSection);
    }

    return 0;
}

// Restarts the MIDI stream. Call this function from the main thread only, with
// exclusive access to shared resources.

static void StreamStart(void)
{
    MMRESULT mmr;

    SetEvent(hBufferReturnEvent);

    mmr = midiStreamRestart(hMidiStream);
    if (mmr != MMSYSERR_NOERROR)
    {
        MidiError("midiStreamRestart", mmr);
    }
}

// Turns off notes but does not release all held ones (use SendNotesSoundOff()
// to prevent hanging notes). The output buffer is returned to the callback
// function and flagged as MHDR_DONE. Call this function from the main thread
// only, with exclusive access to shared resources.

static void StreamStop(void)
{
    MMRESULT mmr;

    mmr = midiStreamStop(hMidiStream);
    if (mmr != MMSYSERR_NOERROR)
    {
        MidiError("midiStreamStop", mmr);
    }

    ResetEvent(hBufferReturnEvent);
    ResetEvent(hStoppedEvent);
}

static boolean I_WIN_InitMusic(void)
{
    const int all_devices = midiOutGetNumDevs();
    int i;
    MMRESULT mmr;

    if (winmm_midi_device == NULL)
    {
        winmm_midi_device = M_StringDuplicate("");
    }

    for (i = 0; i < all_devices; i++)
    {
        MIDIOUTCAPS caps;

        if (midiOutGetDevCaps(i, &caps, sizeof(caps)) == MMSYSERR_NOERROR &&
            !strncasecmp(winmm_midi_device, caps.szPname, MAXPNAMELEN))
        {
            MidiDevice = i;
            break;
        }
    }

    if (i == all_devices)
    {
        free(winmm_midi_device);
        winmm_midi_device = M_StringDuplicate("Microsoft MIDI Mapper");
        MidiDevice = MIDI_MAPPER;
    }

    mmr = midiStreamOpen(&hMidiStream, &MidiDevice, (DWORD)1,
                         (DWORD_PTR)MidiStreamProc, (DWORD_PTR)NULL,
                         CALLBACK_FUNCTION);
    if (mmr != MMSYSERR_NOERROR)
    {
        MidiError("midiStreamOpen", mmr);
        hMidiStream = NULL;
        return false;
    }

    AllocateBuffer(BUFFER_INITIAL_SIZE);

    hBufferReturnEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    hStoppedEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    hPlayerThread =
        CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) PlayerProc, 0, 0, 0);
    SetThreadPriority(hPlayerThread, THREAD_PRIORITY_TIME_CRITICAL);
    InitializeCriticalSectionAndSpinCount(&CriticalSection, 1024);

    AddToBuffer = (winmm_complevel == COMP_VANILLA) ? AddToBuffer_Vanilla
                                                    : AddToBuffer_Standard;
    MIDI_InitFallback();

    return true;
}

static void I_WIN_SetMusicVolume(int volume)
{
    static int last_volume = -1;

    if (last_volume == volume)
    {
        // Ignore holding key down in volume menu.
        return;
    }
    last_volume = volume;

    EnterCriticalSection(&CriticalSection);
    volume_factor = sqrtf((float)volume / 120);
    update_volume = song.registered;
    LeaveCriticalSection(&CriticalSection);
}

static void I_WIN_StopSong(void)
{
    if (!hMidiStream)
    {
        return;
    }

    EnterCriticalSection(&CriticalSection);
    StreamStop();
    win_midi_state = STATE_STOPPING;
    StreamStart();
    LeaveCriticalSection(&CriticalSection);

    WaitForSingleObject(hStoppedEvent, PLAYER_THREAD_WAIT_TIME);
    StreamStop();
}

static void I_WIN_PlaySong(void *handle, boolean looping)
{
    if (!hMidiStream)
    {
        return;
    }

    EnterCriticalSection(&CriticalSection);
    song.looping = looping;
    win_midi_state = STATE_STARTUP;
    StreamStart();
    LeaveCriticalSection(&CriticalSection);
}

static void I_WIN_PauseSong(void)
{
    if (!hMidiStream)
    {
        return;
    }

    I_WIN_StopSong();

    EnterCriticalSection(&CriticalSection);
    win_midi_state = STATE_PAUSED;
    LeaveCriticalSection(&CriticalSection);
}

static void I_WIN_ResumeSong(void)
{
    if (!hMidiStream)
    {
        return;
    }

    EnterCriticalSection(&CriticalSection);
    if (win_midi_state == STATE_PAUSED)
    {
        win_midi_state = STATE_PLAYING;
        StreamStart();
    }
    LeaveCriticalSection(&CriticalSection);
}

// Determine whether memory block is a .mid file 

static boolean IsMid(byte *mem, int len)
{
    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 prop_timediv;
    MIDIPROPTEMPO prop_tempo;
    MMRESULT mmr;

    if (!hMidiStream)
    {
        return NULL;
    }

    // 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 NULL;
    }

    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)
    {
        MidiError("midiStreamProperty", mmr);
        return NULL;
    }
    timediv = prop_timediv.dwTimeDiv;

    // Set initial 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)
    {
        MidiError("midiStreamProperty", mmr);
        return NULL;
    }
    tempo = prop_tempo.dwTempo;

    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);
    }
    song.registered = true;

    return file;
}

static void I_WIN_UnRegisterSong(void *handle)
{
    if (!hMidiStream)
    {
        return;
    }

    if (song.tracks)
    {
        unsigned 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;
    }
    if (handle)
    {
        MIDI_FreeFile(handle);
    }
    song.elapsed_time = 0;
    song.saved_elapsed_time = 0;
    song.num_tracks = 0;
    song.registered = false;
    song.looping = false;
    song.ff_loop = false;
    song.ff_restart = false;
    song.rpg_loop = false;
}

static void I_WIN_ShutdownMusic(void)
{
    MMRESULT mmr;

    if (!hMidiStream)
    {
        return;
    }

    EnterCriticalSection(&CriticalSection);
    StreamStop();
    I_WIN_UnRegisterSong(NULL);
    win_midi_state = STATE_SHUTDOWN;
    StreamStart();
    LeaveCriticalSection(&CriticalSection);

    if (WaitForSingleObject(hPlayerThread, PLAYER_THREAD_WAIT_TIME) ==
        WAIT_OBJECT_0)
    {
        CloseHandle(hPlayerThread);
        hPlayerThread = NULL;
    }
    StreamStop();

    if (buffer.prepared)
    {
        UnprepareHeader();
        buffer.prepared = false;
    }

    mmr = midiStreamClose(hMidiStream);
    if (mmr != MMSYSERR_NOERROR)
    {
        MidiError("midiStreamClose", mmr);
    }
    hMidiStream = NULL;

    free(buffer.data);
    buffer.data = NULL;
    buffer.size = 0;
    buffer.position = 0;

    CloseHandle(hBufferReturnEvent);
    CloseHandle(hStoppedEvent);
    DeleteCriticalSection(&CriticalSection);
}

static boolean I_WIN_MusicIsPlaying(void)
{
    return (song.num_tracks > 0);
}

static const snddevice_t music_win_devices[] =
{
    SNDDEVICE_PAS,
    SNDDEVICE_WAVEBLASTER,
    SNDDEVICE_SOUNDCANVAS,
    SNDDEVICE_GENMIDI,
    SNDDEVICE_AWE32,
};

const 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
