From 68f726e4229ce1f24dec250aa1adf1a1b7b7289f Mon Sep 17 00:00:00 2001
From: Randy Heit <rheit@zdoom.fake>
Date: Fri, 7 Mar 2008 00:43:05 +0000
Subject: [PATCH] - The full master volume SysEx is now always sent to the MIDI
 device, even if   it seems to have a working volume control. - Renamed
 music_midi_stream.cpp to music_midi_base.cpp. - Moved the WinMM MIDI code
 into a new container class.

SVN r787 (trunk)
---
 docs/rh-log.txt                               |  10 +-
 src/sound/i_musicinterns.h                    |  64 ++++-
 ...ic_midi_stream.cpp => music_midi_base.cpp} |   0
 src/sound/music_midi_midiout.cpp              |  28 +-
 src/sound/music_midistream.cpp                | 112 ++++----
 src/sound/music_win_mididevice.cpp            | 254 ++++++++++++++++++
 zdoom.vcproj                                  |   8 +-
 7 files changed, 394 insertions(+), 82 deletions(-)
 rename src/sound/{music_midi_stream.cpp => music_midi_base.cpp} (100%)
 create mode 100644 src/sound/music_win_mididevice.cpp

diff --git a/docs/rh-log.txt b/docs/rh-log.txt
index 77e325a6c..804d301d3 100644
--- a/docs/rh-log.txt
+++ b/docs/rh-log.txt
@@ -1,6 +1,12 @@
+March 6, 2008
+- The full master volume SysEx is now always sent to the MIDI device, even if
+  it seems to have a working volume control.
+- Renamed music_midi_stream.cpp to music_midi_base.cpp.
+- Moved the WinMM MIDI code into a new container class.
+
 March 4, 2008
 - Moved the identical code between the MUS and MIDI streamers into a new base
-  class so they all the low-level details of MIDI streaming are kept in
+  class so that all the low-level details of MIDI streaming are kept in
   one place.
 - Converted the SMF MIDI playback to use the same MIDI streams as MUS
   playback.
@@ -9,7 +15,7 @@ March 4, 2008
   manner.
 - Fixed: The MEVT_* values are not defined shifted into their spot for a
   MIDIEVENT, so I need to do it myself.
-- Fixed: Pausing a MUS and the changing snd_midivolume caused the paused
+- Fixed: Pausing a MUS and then changing snd_midivolume caused the paused
   notes to become audible.
 
 March 3, 2008
diff --git a/src/sound/i_musicinterns.h b/src/sound/i_musicinterns.h
index 17f506e92..928a6c265 100644
--- a/src/sound/i_musicinterns.h
+++ b/src/sound/i_musicinterns.h
@@ -55,8 +55,61 @@ public:
 
 #ifdef _WIN32
 
+// A device that provides a WinMM-like MIDI streaming interface -------------
+
+class MIDIDevice
+{
+public:
+	MIDIDevice();
+	virtual ~MIDIDevice();
+
+	virtual int Open(void (*callback)(unsigned int, void *, DWORD, DWORD), void *userdata) = 0;
+	virtual void Close() = 0;
+	virtual bool IsOpen() const = 0;
+	virtual int GetTechnology() const = 0;
+	virtual int SetTempo(int tempo) = 0;
+	virtual int SetTimeDiv(int timediv) = 0;
+	virtual int StreamOut(MIDIHDR *data) = 0;
+	virtual int Resume() = 0;
+	virtual void Stop() = 0;
+	virtual int PrepareHeader(MIDIHDR *data) = 0;
+	virtual int UnprepareHeader(MIDIHDR *data) = 0;
+};
+
+// WinMM implementation of a MIDI output device -----------------------------
+
+class WinMIDIDevice : public MIDIDevice
+{
+public:
+	WinMIDIDevice(int dev_id);
+	~WinMIDIDevice();
+	int Open(void (*callback)(unsigned int, void *, DWORD, DWORD), void *userdata);
+	void Close();
+	bool IsOpen() const;
+	int GetTechnology() const;
+	int SetTempo(int tempo);
+	int SetTimeDiv(int timediv);
+	int StreamOut(MIDIHDR *data);
+	int Resume();
+	void Stop();
+	int PrepareHeader(MIDIHDR *data);
+	int UnprepareHeader(MIDIHDR *data);
+
+protected:
+	static void CALLBACK CallbackFunc(HMIDIOUT, UINT, DWORD_PTR, DWORD, DWORD);
+
+	HMIDISTRM MidiOut;
+	UINT DeviceID;
+	DWORD SavedVolume;
+	bool VolumeWorks;
+
+	void (*Callback)(unsigned int, void *, DWORD, DWORD);
+	void *CallbackData;
+};
+
 // Base class for streaming MUS and MIDI files ------------------------------
 
+
 class MIDIStreamer : public MusInfo
 {
 public:
@@ -75,7 +128,7 @@ public:
 
 protected:
 	static DWORD WINAPI PlayerProc (LPVOID lpParameter);
-	static void CALLBACK Callback(HMIDIOUT handle, UINT uMsg, DWORD_PTR dwInstance, DWORD dwParam1, DWORD dwParam2);
+	static void Callback(UINT uMsg, void *userdata, DWORD dwParam1, DWORD dwParam2);
 	DWORD PlayerLoop();
 	void OutputVolume (DWORD volume);
 	int FillBuffer(int buffer_num, int max_events, DWORD max_time);
@@ -83,7 +136,7 @@ protected:
 	int VolumeControllerChange(int channel, int volume);
 
 	// Virtuals for subclasses to override
-	virtual void CheckCaps(DWORD dev_id);
+	virtual void CheckCaps();
 	virtual void DoInitialSetup() = 0;
 	virtual void DoRestart() = 0;
 	virtual bool CheckDone() = 0;
@@ -101,12 +154,11 @@ protected:
 		SONG_ERROR
 	};
 
-	HMIDISTRM MidiOut;
+	MIDIDevice *MIDI;
 	HANDLE PlayerThread;
 	HANDLE ExitEvent;
 	HANDLE BufferDoneEvent;
-	DWORD SavedVolume;
-	bool VolumeWorks;
+
 	DWORD Events[2][MAX_EVENTS*3];
 	MIDIHDR Buffer[2];
 	int BufferNum;
@@ -150,7 +202,7 @@ public:
 	~MIDISong2 ();
 
 protected:
-	void CheckCaps(DWORD dev_id);
+	void CheckCaps();
 	void DoInitialSetup();
 	void DoRestart();
 	bool CheckDone();
diff --git a/src/sound/music_midi_stream.cpp b/src/sound/music_midi_base.cpp
similarity index 100%
rename from src/sound/music_midi_stream.cpp
rename to src/sound/music_midi_base.cpp
diff --git a/src/sound/music_midi_midiout.cpp b/src/sound/music_midi_midiout.cpp
index 9f73685fe..b089b7707 100644
--- a/src/sound/music_midi_midiout.cpp
+++ b/src/sound/music_midi_midiout.cpp
@@ -217,24 +217,22 @@ MIDISong2::~MIDISong2 ()
 // MIDISong2 :: CheckCaps
 //
 // Find out if this is an FM synth or not for EMIDI's benefit.
+// (Do any released EMIDIs use track designations?)
 //
 //==========================================================================
 
-void MIDISong2::CheckCaps(DWORD dev_id)
+void MIDISong2::CheckCaps()
 {
-	MIDIOUTCAPS caps;
+	int tech = MIDI->GetTechnology();
 
 	DesignationMask = 0xFF0F;
-	if (MMSYSERR_NOERROR == midiOutGetDevCaps (dev_id, &caps, sizeof(caps)))
+	if (tech == MOD_FMSYNTH)
 	{
-		if (caps.wTechnology == MOD_FMSYNTH)
-		{
-			DesignationMask = 0x00F0;
-		}
-		else if (caps.wTechnology == MOD_MIDIPORT)
-		{
-			DesignationMask = 0x0001;
-		}
+		DesignationMask = 0x00F0;
+	}
+	else if (tech == MOD_MIDIPORT)
+	{
+		DesignationMask = 0x0001;
 	}
 }
 
@@ -438,7 +436,7 @@ DWORD *MIDISong2::SendCommand (DWORD *events, TrackInfo *track, DWORD delay)
 					}
 				}
 				track->Designated = true;
-				event = 0xFF;
+				event = MIDI_META;
 				break;
 
 			case 111:	// EMIDI Track Exclusion
@@ -446,7 +444,7 @@ DWORD *MIDISong2::SendCommand (DWORD *events, TrackInfo *track, DWORD delay)
 				{
 					track->Designation &= ~(1 << data2);
 				}
-				event = 0xFF;
+				event = MIDI_META;
 				break;
 
 			case 112:	// EMIDI Program Change
@@ -729,9 +727,7 @@ MIDISong2::TrackInfo *MIDISong2::FindNextDue ()
 
 void MIDISong2::SetTempo(int new_tempo)
 {
-	MIDIPROPTEMPO tempo = { sizeof(MIDIPROPTEMPO), new_tempo };
-
-	if (MMSYSERR_NOERROR == midiStreamProperty(MidiOut, (LPBYTE)&tempo, MIDIPROP_SET | MIDIPROP_TEMPO))
+	if (MMSYSERR_NOERROR == MIDI->SetTempo(new_tempo))
 	{
 		Tempo = new_tempo;
 	}
diff --git a/src/sound/music_midistream.cpp b/src/sound/music_midistream.cpp
index 171a100f6..264bde4a4 100644
--- a/src/sound/music_midistream.cpp
+++ b/src/sound/music_midistream.cpp
@@ -71,7 +71,7 @@ extern UINT mididevice;
 //==========================================================================
 
 MIDIStreamer::MIDIStreamer()
-: MidiOut(0), PlayerThread(0), ExitEvent(0), BufferDoneEvent(0),
+: MIDI(0), PlayerThread(0), ExitEvent(0), BufferDoneEvent(0),
   Division(0), InitialTempo(500000)
 {
 	BufferDoneEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
@@ -104,6 +104,10 @@ MIDIStreamer::~MIDIStreamer()
 	{
 		CloseHandle(BufferDoneEvent);
 	}
+	if (MIDI != NULL)
+	{
+		delete MIDI;
+	}
 }
 
 //==========================================================================
@@ -139,7 +143,7 @@ bool MIDIStreamer::IsValid() const
 //
 //==========================================================================
 
-void MIDIStreamer::CheckCaps(DWORD dev_id)
+void MIDIStreamer::CheckCaps()
 {
 }
 
@@ -152,7 +156,6 @@ void MIDIStreamer::CheckCaps(DWORD dev_id)
 void MIDIStreamer::Play (bool looping)
 {
 	DWORD tid;
-	UINT dev_id;
 
 	m_Status = STATE_Stopped;
 	m_Looping = looping;
@@ -160,41 +163,27 @@ void MIDIStreamer::Play (bool looping)
 	VolumeChanged = false;
 	Restarting = true;
 	InitialPlayback = true;
-	dev_id = MAX(mididevice, 0u);
 
-	if (MMSYSERR_NOERROR != midiStreamOpen(&MidiOut, &dev_id, 1, (DWORD_PTR)Callback, (DWORD_PTR)this, CALLBACK_FUNCTION))
+	assert(MIDI == NULL);
+	MIDI = new WinMIDIDevice(mididevice);
+
+	if (0 != MIDI->Open(Callback, this))
 	{
 		Printf(PRINT_BOLD, "Could not open MIDI out device\n");
 		return;
 	}
 
-	CheckCaps(dev_id);
+	CheckCaps();
 
 	// Set time division and tempo.
-	MIDIPROPTIMEDIV timediv = { sizeof(MIDIPROPTIMEDIV), Division };
-	MIDIPROPTEMPO tempo = { sizeof(MIDIPROPTEMPO), Tempo = InitialTempo };
-
-	if (MMSYSERR_NOERROR != midiStreamProperty(MidiOut, (LPBYTE)&timediv, MIDIPROP_SET | MIDIPROP_TIMEDIV) ||
-		MMSYSERR_NOERROR != midiStreamProperty(MidiOut, (LPBYTE)&tempo, MIDIPROP_SET | MIDIPROP_TEMPO))
+	if (0 != MIDI->SetTimeDiv(Division) ||
+		0 != MIDI->SetTempo(Tempo = InitialTempo))
 	{
 		Printf(PRINT_BOLD, "Setting MIDI stream speed failed\n");
-		midiStreamClose(MidiOut);
-		MidiOut = NULL;
+		MIDI->Close();
 		return;
 	}
 
-	// Try two different methods for setting the stream to full volume.
-	// Unfortunately, this isn't as reliable as it once was, which is a pity.
-	// The real volume selection is done by setting the volume controller for
-	// each channel. Because every General MIDI-compliant device must support
-	// this controller, it is the most reliable means of setting the volume.
-
-	VolumeWorks = (MMSYSERR_NOERROR == midiOutGetVolume((HMIDIOUT)MidiOut, &SavedVolume));
-	if (VolumeWorks)
-	{
-		VolumeWorks &= (MMSYSERR_NOERROR == midiOutSetVolume((HMIDIOUT)MidiOut, 0xffffffff));
-	}
-
 	snd_midivolume.Callback();	// set volume to current music's properties
 	OutputVolume (midivolume & 0xffff);
 
@@ -208,7 +197,7 @@ void MIDIStreamer::Play (bool looping)
 		int res = FillBuffer(BufferNum, MAX_EVENTS, MAX_TIME);
 		if (res == SONG_MORE)
 		{
-			if (MMSYSERR_NOERROR != midiStreamOut(MidiOut, &Buffer[BufferNum], sizeof(MIDIHDR)))
+			if (0 != MIDI->StreamOut(&Buffer[BufferNum]))
 			{
 				Printf ("Initial midiStreamOut failed\n");
 				Stop();
@@ -223,7 +212,7 @@ void MIDIStreamer::Play (bool looping)
 				Restarting = true;
 				if (SONG_MORE == FillBuffer(BufferNum, MAX_EVENTS, MAX_TIME))
 				{
-					if (MMSYSERR_NOERROR != midiStreamOut(MidiOut, &Buffer[BufferNum], sizeof(MIDIHDR)))
+					if (0 != MIDI->StreamOut(&Buffer[BufferNum]))
 					{
 						Printf ("Initial midiStreamOut failed\n");
 						Stop();
@@ -250,9 +239,9 @@ void MIDIStreamer::Play (bool looping)
 	}
 	while (BufferNum != 0);
 
-	if (MMSYSERR_NOERROR != midiStreamRestart(MidiOut))
+	if (0 != MIDI->Resume())
 	{
-		Printf ("midiStreamRestart failed\n");
+		Printf ("Starting MIDI playback failed\n");
 		Stop();
 	}
 	else
@@ -260,7 +249,7 @@ void MIDIStreamer::Play (bool looping)
 		PlayerThread = CreateThread(NULL, 0, PlayerProc, this, 0, &tid);
 		if (PlayerThread == NULL)
 		{
-			Printf ("MUS CreateThread failed\n");
+			Printf ("Creating MIDI thread failed\n");
 			Stop();
 		}
 		else
@@ -324,18 +313,17 @@ void MIDIStreamer::Stop ()
 		CloseHandle(PlayerThread);
 		PlayerThread = NULL;
 	}
-	if (MidiOut)
+	if (MIDI != NULL && MIDI->IsOpen())
 	{
-		midiStreamStop(MidiOut);
-		midiOutReset((HMIDIOUT)MidiOut);
-		if (VolumeWorks)
-		{
-			midiOutSetVolume((HMIDIOUT)MidiOut, SavedVolume);
-		}
-		midiOutUnprepareHeader((HMIDIOUT)MidiOut, &Buffer[0], sizeof(MIDIHDR));
-		midiOutUnprepareHeader((HMIDIOUT)MidiOut, &Buffer[1], sizeof(MIDIHDR));
-		midiStreamClose(MidiOut);
-		MidiOut = NULL;
+		MIDI->Stop();
+		MIDI->UnprepareHeader(&Buffer[0]);
+		MIDI->UnprepareHeader(&Buffer[1]);
+		MIDI->Close();
+	}
+	if (MIDI != NULL)
+	{
+		delete MIDI;
+		MIDI = NULL;
 	}
 	m_Status = STATE_Stopped;
 }
@@ -402,9 +390,9 @@ int MIDIStreamer::VolumeControllerChange(int channel, int volume)
 //
 //==========================================================================
 
-void CALLBACK MIDIStreamer::Callback(HMIDIOUT hOut, UINT uMsg, DWORD_PTR dwInstance, DWORD dwParam1, DWORD dwParam2)
+void MIDIStreamer::Callback(UINT uMsg, void *userdata, DWORD dwParam1, DWORD dwParam2)
 {
-	MIDIStreamer *self = (MIDIStreamer *)dwInstance;
+	MIDIStreamer *self = (MIDIStreamer *)userdata;
 
 	if (self->EndQueued > 1)
 	{
@@ -502,7 +490,7 @@ bool MIDIStreamer::ServiceEvent()
 	{
 		return false;
 	}
-	if (MMSYSERR_NOERROR != midiOutUnprepareHeader((HMIDIOUT)MidiOut, &Buffer[BufferNum], sizeof(MIDIHDR)))
+	if (0 != MIDI->UnprepareHeader(&Buffer[BufferNum]))
 	{
 		return true;
 	}
@@ -510,7 +498,7 @@ fill:
 	switch (FillBuffer(BufferNum, MAX_EVENTS, MAX_TIME))
 	{
 	case SONG_MORE:
-		if (MMSYSERR_NOERROR != midiStreamOut(MidiOut, &Buffer[BufferNum], sizeof(MIDIHDR)))
+		if (0 != MIDI->StreamOut(&Buffer[BufferNum]))
 		{
 			return true;
 		}
@@ -569,16 +557,13 @@ int MIDIStreamer::FillBuffer(int buffer_num, int max_events, DWORD max_time)
 	if (InitialPlayback)
 	{
 		InitialPlayback = false;
-		if (!VolumeWorks)
-		{
-			// Send the full master volume SysEx message.
-			events[0] = 0;							// dwDeltaTime
-			events[1] = 0;							// dwStreamID
-			events[2] = (MEVT_LONGMSG << 24) | 8;	// dwEvent
-			events[3] = 0x047f7ff0;					// dwParms[0]
-			events[4] = 0xf77f7f01;					// dwParms[1]
-			events += 5;
-		}
+		// Send the full master volume SysEx message.
+		events[0] = 0;							// dwDeltaTime
+		events[1] = 0;							// dwStreamID
+		events[2] = (MEVT_LONGMSG << 24) | 8;	// dwEvent
+		events[3] = 0x047f7ff0;					// dwParms[0]
+		events[4] = 0xf77f7f01;					// dwParms[1]
+		events += 5;
 		DoInitialSetup();
 	}
 
@@ -627,10 +612,25 @@ int MIDIStreamer::FillBuffer(int buffer_num, int max_events, DWORD max_time)
 	Buffer[buffer_num].lpData = (LPSTR)Events[buffer_num];
 	Buffer[buffer_num].dwBufferLength = DWORD((LPSTR)events - Buffer[buffer_num].lpData);
 	Buffer[buffer_num].dwBytesRecorded = Buffer[buffer_num].dwBufferLength;
-	if (MMSYSERR_NOERROR != midiOutPrepareHeader((HMIDIOUT)MidiOut, &Buffer[buffer_num], sizeof(MIDIHDR)))
+	if (0 != MIDI->PrepareHeader(&Buffer[buffer_num]))
 	{
 		return SONG_ERROR;
 	}
 	return SONG_MORE;
 }
+
+//==========================================================================
+//
+// MIDIDevice constructor and desctructor stubs.
+//
+//==========================================================================
+
+MIDIDevice::MIDIDevice()
+{
+}
+
+MIDIDevice::~MIDIDevice()
+{
+}
+
 #endif
diff --git a/src/sound/music_win_mididevice.cpp b/src/sound/music_win_mididevice.cpp
new file mode 100644
index 000000000..69f3cca97
--- /dev/null
+++ b/src/sound/music_win_mididevice.cpp
@@ -0,0 +1,254 @@
+/*
+** music_mididevice.cpp
+** Provides a WinMM implementation of a generic MIDI output device.
+**
+**---------------------------------------------------------------------------
+** Copyright 2008 Randy Heit
+** All rights reserved.
+**
+** Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions
+** are met:
+**
+** 1. Redistributions of source code must retain the above copyright
+**    notice, this list of conditions and the following disclaimer.
+** 2. Redistributions in binary form must reproduce the above copyright
+**    notice, this list of conditions and the following disclaimer in the
+**    documentation and/or other materials provided with the distribution.
+** 3. The name of the author may not be used to endorse or promote products
+**    derived from this software without specific prior written permission.
+**
+** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**---------------------------------------------------------------------------
+**
+*/
+
+#ifdef _WIN32
+
+// HEADER FILES ------------------------------------------------------------
+
+#include "i_musicinterns.h"
+#include "templates.h"
+#include "doomdef.h"
+#include "m_swap.h"
+
+// MACROS ------------------------------------------------------------------
+
+// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------
+
+// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------
+
+// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------
+
+// EXTERNAL DATA DECLARATIONS ----------------------------------------------
+
+// PRIVATE DATA DEFINITIONS ------------------------------------------------
+
+// PUBLIC DATA DEFINITIONS -------------------------------------------------
+
+// CODE --------------------------------------------------------------------
+
+//==========================================================================
+//
+// WinMIDIDevice Contructor
+//
+//==========================================================================
+
+WinMIDIDevice::WinMIDIDevice(int dev_id)
+{
+	DeviceID = MAX<DWORD>(dev_id, 0);
+	MidiOut = 0;
+}
+
+//==========================================================================
+//
+// WinMIDIDevice Destructor
+//
+//==========================================================================
+
+WinMIDIDevice::~WinMIDIDevice()
+{
+	Close();
+}
+
+//==========================================================================
+//
+// WinMIDIDevice :: Open
+//
+//==========================================================================
+
+int WinMIDIDevice::Open(void (*callback)(UINT, void *, DWORD, DWORD), void *userdata)
+{
+	MMRESULT err;
+
+	Callback = callback;
+	CallbackData = userdata;
+	if (MidiOut == NULL)
+	{
+		err = midiStreamOpen(&MidiOut, &DeviceID, 1, (DWORD_PTR)CallbackFunc, (DWORD_PTR)this, CALLBACK_FUNCTION);
+
+		if (err == MMSYSERR_NOERROR)
+		{
+			// Set master volume to full, if the device allows it on this interface.
+			VolumeWorks = (MMSYSERR_NOERROR == midiOutGetVolume((HMIDIOUT)MidiOut, &SavedVolume));
+			if (VolumeWorks)
+			{
+				VolumeWorks &= (MMSYSERR_NOERROR == midiOutSetVolume((HMIDIOUT)MidiOut, 0xffffffff));
+			}
+		}
+	}
+	return 0;
+}
+
+//==========================================================================
+//
+// WinMIDIDevice :: Close
+//
+//==========================================================================
+
+void WinMIDIDevice::Close()
+{
+	if (MidiOut != NULL)
+	{
+		midiStreamClose(MidiOut);
+		MidiOut = NULL;
+	}
+}
+
+//==========================================================================
+//
+// WinMIDIDevice :: IsOpen
+//
+//==========================================================================
+
+bool WinMIDIDevice::IsOpen() const
+{
+	return MidiOut != NULL;
+}
+
+//==========================================================================
+//
+// WinMIDIDevice :: GetTechnology
+//
+//==========================================================================
+
+int WinMIDIDevice::GetTechnology() const
+{
+	MIDIOUTCAPS caps;
+
+	if (MMSYSERR_NOERROR == midiOutGetDevCaps(DeviceID, &caps, sizeof(caps)))
+	{
+		return caps.wTechnology;
+	}
+	return -1;
+}
+
+//==========================================================================
+//
+// WinMIDIDevice :: SetTempo
+//
+//==========================================================================
+
+int WinMIDIDevice::SetTempo(int tempo)
+{
+	MIDIPROPTEMPO data = { sizeof(MIDIPROPTEMPO), tempo };
+	return midiStreamProperty(MidiOut, (LPBYTE)&data, MIDIPROP_SET | MIDIPROP_TEMPO);
+}
+
+//==========================================================================
+//
+// WinMIDIDevice :: SetTimeDiv
+//
+//==========================================================================
+
+int WinMIDIDevice::SetTimeDiv(int timediv)
+{
+	MIDIPROPTIMEDIV data = { sizeof(MIDIPROPTIMEDIV), timediv };
+	return midiStreamProperty(MidiOut, (LPBYTE)&data, MIDIPROP_SET | MIDIPROP_TIMEDIV);
+}
+
+//==========================================================================
+//
+// WinMIDIDevice :: Resume
+//
+//==========================================================================
+
+int WinMIDIDevice::Resume()
+{
+	return midiStreamRestart(MidiOut);
+}
+
+//==========================================================================
+//
+// WinMIDIDevice :: Stop
+//
+//==========================================================================
+
+void WinMIDIDevice::Stop()
+{
+	midiStreamStop(MidiOut);
+	midiOutReset((HMIDIOUT)MidiOut);
+	if (VolumeWorks)
+	{
+		midiOutSetVolume((HMIDIOUT)MidiOut, SavedVolume);
+	}
+}
+
+//==========================================================================
+//
+// WinMIDIDevice :: StreamOut
+//
+//==========================================================================
+
+int WinMIDIDevice::StreamOut(MIDIHDR *header)
+{
+	return midiStreamOut(MidiOut, header, sizeof(MIDIHDR));
+}
+
+//==========================================================================
+//
+// WinMIDIDevice :: PrepareHeader
+//
+//==========================================================================
+
+int WinMIDIDevice::PrepareHeader(MIDIHDR *header)
+{
+	return midiOutPrepareHeader((HMIDIOUT)MidiOut, header, sizeof(MIDIHDR));
+}
+
+//==========================================================================
+//
+// WinMIDIDevice :: UnprepareHeader
+//
+//==========================================================================
+
+int WinMIDIDevice::UnprepareHeader(MIDIHDR *header)
+{
+	return midiOutUnprepareHeader((HMIDIOUT)MidiOut, header, sizeof(MIDIHDR));
+}
+
+//==========================================================================
+//
+// WinMIDIDevice :: CallbackFunc									static
+//
+//==========================================================================
+
+void CALLBACK WinMIDIDevice::CallbackFunc(HMIDIOUT hOut, UINT uMsg, DWORD_PTR dwInstance, DWORD dwParam1, DWORD dwParam2)
+{
+	WinMIDIDevice *self = (WinMIDIDevice *)dwInstance;
+	if (self->Callback != NULL)
+	{
+		self->Callback(uMsg, self->CallbackData, dwParam1, dwParam2);
+	}
+}
+
+#endif
diff --git a/zdoom.vcproj b/zdoom.vcproj
index 0e0029f73..c63377ff6 100644
--- a/zdoom.vcproj
+++ b/zdoom.vcproj
@@ -9255,11 +9255,11 @@
 				>
 			</File>
 			<File
-				RelativePath="src\sound\music_midi_midiout.cpp"
+				RelativePath=".\src\sound\music_midi_base.cpp"
 				>
 			</File>
 			<File
-				RelativePath="src\sound\music_midi_stream.cpp"
+				RelativePath="src\sound\music_midi_midiout.cpp"
 				>
 			</File>
 			<File
@@ -9290,6 +9290,10 @@
 				RelativePath="src\sound\music_stream.cpp"
 				>
 			</File>
+			<File
+				RelativePath=".\src\sound\music_win_mididevice.cpp"
+				>
+			</File>
 			<File
 				RelativePath="src\sound\sample_flac.cpp"
 				>