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