qzdoom/src/sound/music_midistream.cpp

637 lines
17 KiB
C++
Raw Normal View History

/*
** music_midistream.cpp
** Implements base class for MIDI and MUS streaming.
**
**---------------------------------------------------------------------------
** 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 ------------------------------------------------------------------
#define MAX_TIME (1000000/20) // Send out 1/20 of a sec of events at a time.
// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------
// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------
// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------
// EXTERNAL DATA DECLARATIONS ----------------------------------------------
EXTERN_CVAR (Float, snd_midivolume)
extern DWORD midivolume;
extern UINT mididevice;
// PRIVATE DATA DEFINITIONS ------------------------------------------------
// PUBLIC DATA DEFINITIONS -------------------------------------------------
// CODE --------------------------------------------------------------------
//==========================================================================
//
// MIDIStreamer Constructor
//
//==========================================================================
MIDIStreamer::MIDIStreamer()
: MidiOut(0), PlayerThread(0), ExitEvent(0), BufferDoneEvent(0),
Division(0), InitialTempo(500000)
{
BufferDoneEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (BufferDoneEvent == NULL)
{
Printf(PRINT_BOLD, "Could not create buffer done event for MIDI playback\n");
}
ExitEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (ExitEvent == NULL)
{
Printf(PRINT_BOLD, "Could not create exit event for MIDI playback\n");
return;
}
}
//==========================================================================
//
// MIDIStreamer Destructor
//
//==========================================================================
MIDIStreamer::~MIDIStreamer()
{
Stop();
if (ExitEvent != NULL)
{
CloseHandle(ExitEvent);
}
if (BufferDoneEvent != NULL)
{
CloseHandle(BufferDoneEvent);
}
}
//==========================================================================
//
// MIDIStreamer :: IsMIDI
//
// You bet it is!
//
//==========================================================================
bool MIDIStreamer::IsMIDI() const
{
return true;
}
//==========================================================================
//
// MIDIStreamer :: IsValid
//
//==========================================================================
bool MIDIStreamer::IsValid() const
{
return ExitEvent != NULL && Division != 0;
}
//==========================================================================
//
// MIDIStreamer :: CheckCaps
//
// Called immediately after the device is opened in case a subclass should
// want to alter its behavior depending on which device it got.
//
//==========================================================================
void MIDIStreamer::CheckCaps(DWORD dev_id)
{
}
//==========================================================================
//
// MIDIStreamer :: Play
//
//==========================================================================
void MIDIStreamer::Play (bool looping)
{
DWORD tid;
UINT dev_id;
m_Status = STATE_Stopped;
m_Looping = looping;
EndQueued = 0;
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))
{
Printf(PRINT_BOLD, "Could not open MIDI out device\n");
return;
}
CheckCaps(dev_id);
// 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))
{
Printf(PRINT_BOLD, "Setting MIDI stream speed failed\n");
midiStreamClose(MidiOut);
MidiOut = NULL;
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);
ResetEvent(ExitEvent);
ResetEvent(BufferDoneEvent);
// Fill the initial buffers for the song.
BufferNum = 0;
do
{
int res = FillBuffer(BufferNum, MAX_EVENTS, MAX_TIME);
if (res == SONG_MORE)
{
if (MMSYSERR_NOERROR != midiStreamOut(MidiOut, &Buffer[BufferNum], sizeof(MIDIHDR)))
{
Printf ("Initial midiStreamOut failed\n");
Stop();
return;
}
BufferNum ^= 1;
}
else if (res == SONG_DONE)
{
if (looping)
{
Restarting = true;
if (SONG_MORE == FillBuffer(BufferNum, MAX_EVENTS, MAX_TIME))
{
if (MMSYSERR_NOERROR != midiStreamOut(MidiOut, &Buffer[BufferNum], sizeof(MIDIHDR)))
{
Printf ("Initial midiStreamOut failed\n");
Stop();
return;
}
BufferNum ^= 1;
}
else
{
Stop();
return;
}
}
else
{
EndQueued = true;
}
}
else
{
Stop();
return;
}
}
while (BufferNum != 0);
if (MMSYSERR_NOERROR != midiStreamRestart(MidiOut))
{
Printf ("midiStreamRestart failed\n");
Stop();
}
else
{
PlayerThread = CreateThread(NULL, 0, PlayerProc, this, 0, &tid);
if (PlayerThread == NULL)
{
Printf ("MUS CreateThread failed\n");
Stop();
}
else
{
m_Status = STATE_Playing;
}
}
}
//==========================================================================
//
// MIDIStreamer :: Pause
//
// "Pauses" the song by setting it to zero volume and filling subsequent
// buffers with NOPs until the song is unpaused.
//
//==========================================================================
void MIDIStreamer::Pause ()
{
if (m_Status == STATE_Playing)
{
m_Status = STATE_Paused;
OutputVolume(0);
}
}
//==========================================================================
//
// MIDIStreamer :: Resume
//
// "Unpauses" a song by restoring the volume and letting subsequent
// buffers store real MIDI events again.
//
//==========================================================================
void MIDIStreamer::Resume ()
{
if (m_Status == STATE_Paused)
{
OutputVolume(midivolume & 0xffff);
m_Status = STATE_Playing;
}
}
//==========================================================================
//
// MIDIStreamer :: Stop
//
// Stops playback and closes the player thread and MIDI device.
//
//==========================================================================
void MIDIStreamer::Stop ()
{
EndQueued = 2;
if (PlayerThread)
{
SetEvent(ExitEvent);
WaitForSingleObject(PlayerThread, INFINITE);
CloseHandle(PlayerThread);
PlayerThread = NULL;
}
if (MidiOut)
{
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;
}
m_Status = STATE_Stopped;
}
//==========================================================================
//
// MIDIStreamer :: IsPlaying
//
//==========================================================================
bool MIDIStreamer::IsPlaying ()
{
return m_Status != STATE_Stopped;
}
//==========================================================================
//
// MIDIStreamer :: SetVolume
//
//==========================================================================
void MIDIStreamer::SetVolume (float volume)
{
OutputVolume(midivolume & 0xffff);
}
//==========================================================================
//
// MIDIStreamer :: OutputVolume
//
// Signals the buffer filler to send volume change events on all channels.
//
//==========================================================================
void MIDIStreamer::OutputVolume (DWORD volume)
{
NewVolume = volume;
VolumeChanged = true;
}
//==========================================================================
//
// MIDIStreamer :: VolumeControllerChange
//
// Some devices don't support master volume
// (e.g. the Audigy's software MIDI synth--but not its two hardware ones),
// so assume none of them do and scale channel volumes manually.
//
//==========================================================================
int MIDIStreamer::VolumeControllerChange(int channel, int volume)
{
ChannelVolumes[channel] = volume;
return ((volume + 1) * (midivolume & 0xffff)) >> 16;
}
//==========================================================================
//
// MIDIStreamer :: Callback Static
//
// Signals the BufferDoneEvent to prepare the next buffer. The buffer is not
// prepared in the callback directly, because it's generally still in use by
// the MIDI streamer when this callback is executed.
//
//==========================================================================
void CALLBACK MIDIStreamer::Callback(HMIDIOUT hOut, UINT uMsg, DWORD_PTR dwInstance, DWORD dwParam1, DWORD dwParam2)
{
MIDIStreamer *self = (MIDIStreamer *)dwInstance;
if (self->EndQueued > 1)
{
return;
}
if (uMsg == MOM_DONE)
{
SetEvent(self->BufferDoneEvent);
}
}
//==========================================================================
//
// MIDIStreamer :: Update
//
// Called periodically to see if the player thread is still alive. If it
// isn't, stop playback now.
//
//==========================================================================
void MIDIStreamer::Update()
{
// If the PlayerThread is signalled, then it's dead.
if (PlayerThread != NULL &&
WaitForSingleObject(PlayerThread, 0) == WAIT_OBJECT_0)
{
CloseHandle(PlayerThread);
PlayerThread = NULL;
Printf ("MIDI playback failure\n");
Stop();
}
}
//==========================================================================
//
// MIDIStreamer :: PlayerProc Static
//
// Entry point for the player thread.
//
//==========================================================================
DWORD WINAPI MIDIStreamer::PlayerProc (LPVOID lpParameter)
{
return ((MIDIStreamer *)lpParameter)->PlayerLoop();
}
//==========================================================================
//
// MIDIStreamer :: PlayerLoop
//
// Services MIDI playback events.
//
//==========================================================================
DWORD MIDIStreamer::PlayerLoop()
{
HANDLE events[2] = { BufferDoneEvent, ExitEvent };
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
for (;;)
{
switch (WaitForMultipleObjects(2, events, FALSE, INFINITE))
{
case WAIT_OBJECT_0:
if (ServiceEvent())
{
return 1;
}
break;
case WAIT_OBJECT_0 + 1:
return 0;
default:
// Should not happen.
return 1;
}
}
}
//==========================================================================
//
// MIDIStreamer :: ServiceEvent
//
// Fills the buffer that just finished playing with new events and appends
// it to the MIDI stream queue. Stops the song if playback is over. Returns
// true if a problem occured and playback should stop.
//
//==========================================================================
bool MIDIStreamer::ServiceEvent()
{
if (EndQueued == 1)
{
return false;
}
if (MMSYSERR_NOERROR != midiOutUnprepareHeader((HMIDIOUT)MidiOut, &Buffer[BufferNum], sizeof(MIDIHDR)))
{
return true;
}
fill:
switch (FillBuffer(BufferNum, MAX_EVENTS, MAX_TIME))
{
case SONG_MORE:
if (MMSYSERR_NOERROR != midiStreamOut(MidiOut, &Buffer[BufferNum], sizeof(MIDIHDR)))
{
return true;
}
else
{
BufferNum ^= 1;
}
break;
case SONG_DONE:
if (m_Looping)
{
Restarting = true;
goto fill;
}
EndQueued = 1;
break;
default:
return true;
}
return false;
}
//==========================================================================
//
// MIDIStreamer :: FillBuffer
//
// Copies MIDI events from the SMF and puts them into a MIDI stream
// buffer. Filling the buffer stops when the song end is encountered, the
// buffer space is used up, or the maximum time for a buffer is hit.
//
// Can return:
// - SONG_MORE if the buffer was prepared with data.
// - SONG_DONE if the song's end was reached.
// The buffer will never have data in this case.
// - SONG_ERROR if there was a problem preparing the buffer.
//
//==========================================================================
int MIDIStreamer::FillBuffer(int buffer_num, int max_events, DWORD max_time)
{
if (!Restarting && CheckDone())
{
return SONG_DONE;
}
int i;
DWORD *events = Events[buffer_num], *max_event_p;
DWORD tot_time = 0;
DWORD time = 0;
// The final event is for a NOP to hold the delay from the last event.
max_event_p = events + (max_events - 1) * 3;
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;
}
DoInitialSetup();
}
// If the volume has changed, stick those events at the start of this buffer.
if (VolumeChanged && (m_Status != STATE_Paused || NewVolume == 0))
{
VolumeChanged = false;
for (i = 0; i < 16; ++i)
{
BYTE courseVol = (BYTE)(((ChannelVolumes[i]+1) * NewVolume) >> 16);
events[0] = 0; // dwDeltaTime
events[1] = 0; // dwStreamID
events[2] = MIDI_CTRLCHANGE | i | (7<<8) | (courseVol<<16);
events += 3;
}
}
// Play nothing while paused.
if (m_Status == STATE_Paused)
{
// Be more responsive when unpausing by only playing each buffer
// for a third of the maximum time.
events[0] = MAX<DWORD>(1, (max_time / 3) * Division / Tempo);
events[1] = 0;
events[2] = MEVT_NOP << 24;
events += 3;
}
else
{
if (Restarting)
{
Restarting = false;
// Stop all notes in case any were left hanging.
for (i = 0; i < 16; ++i)
{
events[0] = 0; // dwDeltaTime
events[1] = 0; // dwStreamID
events[2] = MIDI_NOTEOFF | i | (60 << 8) | (64<<16);
events += 3;
}
DoRestart();
}
events = MakeEvents(events, max_event_p, max_time);
}
memset(&Buffer[buffer_num], 0, sizeof(MIDIHDR));
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)))
{
return SONG_ERROR;
}
return SONG_MORE;
}
#endif