From 68a8ea218927b26b526e8eea73d570c53b033411 Mon Sep 17 00:00:00 2001 From: Randy Heit Date: Wed, 5 Mar 2008 03:10:31 +0000 Subject: [PATCH] - 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 one place. - Converted the SMF MIDI playback to use the same MIDI streams as MUS playback. - Moved MUS playback back into its own thread so that it can continue uninterrupted if the main thread is too busy to service it in a timely 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 notes to become audible. SVN r784 (trunk) --- docs/rh-log.txt | 16 +- src/s_sound.cpp | 2 + src/sound/i_music.cpp | 10 +- src/sound/i_music.h | 1 + src/sound/i_musicinterns.h | 139 ++-- src/sound/music_midi_midiout.cpp | 567 +++++++------- src/sound/music_midistream.cpp | 636 +++++++++++++++ src/sound/music_mus_midiout.cpp | 453 ++++------- src/win32/i_input.cpp | 20 +- src/win32/i_system.cpp | 19 +- zdoom.vcproj | 1258 +++++++++++++++--------------- 11 files changed, 1778 insertions(+), 1343 deletions(-) create mode 100644 src/sound/music_midistream.cpp diff --git a/docs/rh-log.txt b/docs/rh-log.txt index b9c4f5d40..77e325a6c 100644 --- a/docs/rh-log.txt +++ b/docs/rh-log.txt @@ -1,9 +1,23 @@ +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 + one place. +- Converted the SMF MIDI playback to use the same MIDI streams as MUS + playback. +- Moved MUS playback back into its own thread so that it can continue + uninterrupted if the main thread is too busy to service it in a timely + 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 + notes to become audible. + March 3, 2008 - Changed MUS playback to use MIDI streams, like it did during the early days of ZDoom, except now the entire song isn't prebuffered in large chunks, so I can insert MIDI events into the playback with fairly low latency. This should offer more precise timing than the combination of low-level MIDI and - WaitForSingleObject timeouts. + WaitForMultipleObjects timeouts that it replaces. - Fixed: PTR_BounceTraverse only checked for projectiles that were too high to pass through two-sided lines, but not ones that were too low. - Fixed: SBARINFO couldn't detect the extreme death damage type for the diff --git a/src/s_sound.cpp b/src/s_sound.cpp index 2bafc3fe8..fc121843f 100644 --- a/src/s_sound.cpp +++ b/src/s_sound.cpp @@ -1312,6 +1312,8 @@ void S_UpdateSounds (void *listener_p) angle_t angle; int sep; + I_UpdateMusic(); + if (GSnd == NULL) return; diff --git a/src/sound/i_music.cpp b/src/sound/i_music.cpp index 7d7f92c6b..34328eb8e 100644 --- a/src/sound/i_music.cpp +++ b/src/sound/i_music.cpp @@ -112,7 +112,7 @@ bool MusInfo::SetPosition (int order) return false; } -void MusInfo::ServiceEvent () +void MusInfo::Update () { } @@ -459,6 +459,14 @@ void *I_RegisterCDSong (int track, int id) return info; } +void I_UpdateMusic() +{ + if (currSong != NULL) + { + currSong->Update(); + } +} + // Is the song playing? bool I_QrySongPlaying (void *handle) { diff --git a/src/sound/i_music.h b/src/sound/i_music.h index 575910110..cf808252f 100644 --- a/src/sound/i_music.h +++ b/src/sound/i_music.h @@ -44,6 +44,7 @@ void I_InitMusic (); void I_ShutdownMusic (); void I_BuildMIDIMenuList (struct value_s **values, float *numValues); +void I_UpdateMusic (); // Volume. void I_SetMusicVolume (float volume); diff --git a/src/sound/i_musicinterns.h b/src/sound/i_musicinterns.h index d2bebb508..17f506e92 100644 --- a/src/sound/i_musicinterns.h +++ b/src/sound/i_musicinterns.h @@ -42,7 +42,7 @@ public: virtual bool IsMIDI () const = 0; virtual bool IsValid () const = 0; virtual bool SetPosition (int order); - virtual void ServiceEvent (); + virtual void Update(); enum EState { @@ -53,45 +53,41 @@ public: bool m_Looping; }; -// MUS file played with MIDI output messages -------------------------------- - #ifdef _WIN32 -struct SHORTMIDIEVENT -{ - DWORD dwDeltaTime; - DWORD dwStreamID; - DWORD dwEvent; -}; -struct VOLSYSEXEVENT : SHORTMIDIEVENT -{ - BYTE SysEx[8]; -}; +// Base class for streaming MUS and MIDI files ------------------------------ -class MUSSong2 : public MusInfo +class MIDIStreamer : public MusInfo { public: - MUSSong2 (FILE *file, char * musiccache, int length); - ~MUSSong2 (); + MIDIStreamer(); + ~MIDIStreamer(); - void SetVolume (float volume); - void Play (bool looping); - void Pause (); - void Resume (); - void Stop (); - bool IsPlaying (); - bool IsMIDI () const; - bool IsValid () const; + void SetVolume(float volume); + void Play(bool looping); + void Pause(); + void Resume(); + void Stop(); + bool IsPlaying(); + bool IsMIDI() const; + bool IsValid() const; + void Update(); protected: static DWORD WINAPI PlayerProc (LPVOID lpParameter); - void OutputVolume (DWORD volume); - int SendCommand (); - bool TranslateSong(const BYTE *buffer, size_t len); - int CountEvents(const BYTE *buffer, size_t len); - int FillBuffer(int buffer_num, int max_events, DWORD max_time); - void ServiceEvent(); static void CALLBACK Callback(HMIDIOUT handle, UINT uMsg, DWORD_PTR dwInstance, DWORD dwParam1, DWORD dwParam2); + DWORD PlayerLoop(); + void OutputVolume (DWORD volume); + int FillBuffer(int buffer_num, int max_events, DWORD max_time); + bool ServiceEvent(); + int VolumeControllerChange(int channel, int volume); + + // Virtuals for subclasses to override + virtual void CheckCaps(DWORD dev_id); + virtual void DoInitialSetup() = 0; + virtual void DoRestart() = 0; + virtual bool CheckDone() = 0; + virtual DWORD *MakeEvents(DWORD *events, DWORD *max_event_p, DWORD max_time) = 0; enum { @@ -106,73 +102,76 @@ protected: }; HMIDISTRM MidiOut; + HANDLE PlayerThread; + HANDLE ExitEvent; + HANDLE BufferDoneEvent; DWORD SavedVolume; bool VolumeWorks; - - MUSHeader *MusHeader; - BYTE *MusBuffer; - BYTE LastVelocity[16]; - BYTE ChannelVolumes[16]; - size_t MusP, MaxMusP; - VOLSYSEXEVENT FullVolEvent; - SHORTMIDIEVENT Events[2][MAX_EVENTS]; + DWORD Events[2][MAX_EVENTS*3]; MIDIHDR Buffer[2]; int BufferNum; int EndQueued; bool VolumeChanged; bool Restarting; + bool InitialPlayback; DWORD NewVolume; + int Division; + int Tempo; + int InitialTempo; + BYTE ChannelVolumes[16]; }; -#endif -// MIDI file played with MIDI output messages ------------------------------- +// MUS file played with a MIDI stream --------------------------------------- -#ifdef _WIN32 -class MIDISong2 : public MusInfo +class MUSSong2 : public MIDIStreamer { public: - MIDISong2 (FILE *file, char * musiccache, int length); - ~MIDISong2 (); - - void SetVolume (float volume); - void Play (bool looping); - void Pause (); - void Resume (); - void Stop (); - bool IsPlaying (); - bool IsMIDI () const; - bool IsValid () const; + MUSSong2 (FILE *file, char *musiccache, int length); + ~MUSSong2 (); protected: + void DoInitialSetup(); + void DoRestart(); + bool CheckDone(); + DWORD *MakeEvents(DWORD *events, DWORD *max_events_p, DWORD max_time); + + MUSHeader *MusHeader; + BYTE *MusBuffer; + BYTE LastVelocity[16]; + size_t MusP, MaxMusP; +}; + +// MIDI file played with a MIDI stream -------------------------------------- + +class MIDISong2 : public MIDIStreamer +{ +public: + MIDISong2 (FILE *file, char *musiccache, int length); + ~MIDISong2 (); + +protected: + void CheckCaps(DWORD dev_id); + void DoInitialSetup(); + void DoRestart(); + bool CheckDone(); + DWORD *MakeEvents(DWORD *events, DWORD *max_events_p, DWORD max_time); + struct TrackInfo; - static DWORD WINAPI PlayerProc (LPVOID lpParameter); - void OutputVolume (DWORD volume); void ProcessInitialMetaEvents (); - DWORD SendCommands (); - void SendCommand (TrackInfo *track); + DWORD *SendCommand (DWORD *event, TrackInfo *track, DWORD delay); TrackInfo *FindNextDue (); - - HMIDIOUT MidiOut; - HANDLE PlayerThread; - HANDLE PauseEvent; - HANDLE ExitEvent; - HANDLE TicEvent; - HANDLE VolumeChangeEvent; - DWORD SavedVolume; - bool VolumeWorks; + void SetTempo(int new_tempo); BYTE *MusHeader; - BYTE ChannelVolumes[16]; TrackInfo *Tracks; TrackInfo *TrackDue; int NumTracks; int Format; - int Division; - int Tempo; WORD DesignationMask; }; -#endif + +#endif /* _WIN32 */ // MOD file played with FMOD ------------------------------------------------ diff --git a/src/sound/music_midi_midiout.cpp b/src/sound/music_midi_midiout.cpp index 16767b3d4..9f73685fe 100644 --- a/src/sound/music_midi_midiout.cpp +++ b/src/sound/music_midi_midiout.cpp @@ -1,10 +1,62 @@ +/* +** music_midi_midiout.cpp +** Code to let ZDoom play SMF MIDI music through the MIDI streaming API. +** +**--------------------------------------------------------------------------- +** Copyright 1998-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. +**--------------------------------------------------------------------------- +** +** This file also supports the Apogee Sound System's EMIDI files. That +** basically means you can play the Duke3D songs without any editing and +** have them sound right. +*/ + #ifdef _WIN32 + +// HEADER FILES ------------------------------------------------------------ + #include "i_musicinterns.h" #include "templates.h" #include "doomdef.h" #include "m_swap.h" -EXTERN_CVAR (Float, snd_midivolume) +// MACROS ------------------------------------------------------------------ + +#define MAX_TIME (1000000/20) // Send out 1/20 of a sec of events at a time. + +// Used by SendCommand to check for unexpected end-of-track conditions. +#define CHECK_FINISHED \ + if (track->TrackP >= track->MaxTrackP) \ + { \ + track->Finished = true; \ + return events; \ + } + +// TYPES ------------------------------------------------------------------- struct MIDISong2::TrackInfo { @@ -27,24 +79,50 @@ struct MIDISong2::TrackInfo DWORD ReadVarLen (); }; +// 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 ------------------------------------------------ + static BYTE EventLengths[7] = { 2, 2, 2, 2, 1, 1, 2 }; static BYTE CommonLengths[15] = { 0, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; -MIDISong2::MIDISong2 (FILE *file, char * musiccache, int len) -: MidiOut (0), PlayerThread (0), - PauseEvent (0), ExitEvent (0), VolumeChangeEvent (0), - MusHeader (0) +// PUBLIC DATA DEFINITIONS ------------------------------------------------- + +// CODE -------------------------------------------------------------------- + +//========================================================================== +// +// MIDISong2 Constructor +// +// Buffers the file and does some validation of the SMF header. +// +//========================================================================== + +MIDISong2::MIDISong2 (FILE *file, char *musiccache, int len) +: MusHeader(0), Tracks(0) { int p; int i; + if (ExitEvent == NULL) + { + return; + } MusHeader = new BYTE[len]; if (file != NULL) { - if (fread (MusHeader, 1, len, file) != (size_t)len) + if (fread(MusHeader, 1, len, file) != (size_t)len) return; } else @@ -70,8 +148,12 @@ MIDISong2::MIDISong2 (FILE *file, char * musiccache, int len) NumTracks = MusHeader[10] * 256 + MusHeader[11]; } - // The timers only have millisecond accuracy, not microsecond. - Division = (MusHeader[12] * 256 + MusHeader[13]) * 1000; + // The division is the number of pulses per quarter note (PPQN). + Division = MusHeader[12] * 256 + MusHeader[13]; + if (Division == 0) + { // PPQN is zero? Then the song cannot play because it never pulses. + return; + } Tracks = new TrackInfo[NumTracks]; @@ -110,42 +192,16 @@ MIDISong2::MIDISong2 (FILE *file, char * musiccache, int len) { // No tracks, so nothing to play return; } - - ExitEvent = CreateEvent (NULL, FALSE, FALSE, NULL); - if (ExitEvent == NULL) - { - Printf (PRINT_BOLD, "Could not create exit event for MIDI playback\n"); - return; - } - VolumeChangeEvent = CreateEvent (NULL, FALSE, FALSE, NULL); - if (VolumeChangeEvent == NULL) - { - Printf (PRINT_BOLD, "Could not create volume event for MIDI playback\n"); - return; - } - PauseEvent = CreateEvent (NULL, FALSE, FALSE, NULL); - if (PauseEvent == NULL) - { - Printf (PRINT_BOLD, "Could not create pause event for MIDI playback\n"); - } } +//========================================================================== +// +// MIDISong2 Destructor +// +//========================================================================== + MIDISong2::~MIDISong2 () { - Stop (); - - if (PauseEvent != NULL) - { - CloseHandle (PauseEvent); - } - if (ExitEvent != NULL) - { - CloseHandle (ExitEvent); - } - if (VolumeChangeEvent != NULL) - { - CloseHandle (VolumeChangeEvent); - } if (Tracks != NULL) { delete[] Tracks; @@ -156,27 +212,20 @@ MIDISong2::~MIDISong2 () } } -bool MIDISong2::IsMIDI () const -{ - return true; -} +//========================================================================== +// +// MIDISong2 :: CheckCaps +// +// Find out if this is an FM synth or not for EMIDI's benefit. +// +//========================================================================== -bool MIDISong2::IsValid () const -{ - return PauseEvent != 0; -} - -void MIDISong2::Play (bool looping) +void MIDISong2::CheckCaps(DWORD dev_id) { MIDIOUTCAPS caps; - DWORD tid; - m_Status = STATE_Stopped; - m_Looping = looping; - - // Find out if this an FM synth or not for EMIDI DesignationMask = 0xFF0F; - if (MMSYSERR_NOERROR == midiOutGetDevCaps (mididevice<0? MIDI_MAPPER:mididevice, &caps, sizeof(caps))) + if (MMSYSERR_NOERROR == midiOutGetDevCaps (dev_id, &caps, sizeof(caps))) { if (caps.wTechnology == MOD_FMSYNTH) { @@ -187,222 +236,123 @@ void MIDISong2::Play (bool looping) DesignationMask = 0x0001; } } - - if (MMSYSERR_NOERROR != midiOutOpen (&MidiOut, mididevice<0? MIDI_MAPPER:mididevice, 0, 0, CALLBACK_NULL)) - { - Printf (PRINT_BOLD, "Could not open MIDI out device\n"); - 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 (MidiOut, &SavedVolume)); - if (VolumeWorks) - { - VolumeWorks &= (MMSYSERR_NOERROR == midiOutSetVolume (MidiOut, 0xffffffff)); - } - else - { - // Send the standard SysEx message for full master volume - BYTE volmess[] = { 0xf0, 0x7f, 0x7f, 0x04, 0x01, 0x7f, 0x7f, 0xf7 }; - MIDIHDR hdr = { (LPSTR)volmess, sizeof(volmess), }; - - if (MMSYSERR_NOERROR == midiOutPrepareHeader (MidiOut, &hdr, sizeof(hdr))) - { - midiOutLongMsg (MidiOut, &hdr, sizeof(hdr)); - while (MIDIERR_STILLPLAYING == midiOutUnprepareHeader (MidiOut, &hdr, sizeof(hdr))) - { - Sleep (10); - } - } - } - - snd_midivolume.Callback(); // set volume to current music's properties - PlayerThread = CreateThread (NULL, 0, PlayerProc, this, 0, &tid); - if (PlayerThread == NULL) - { - if (VolumeWorks) - { - midiOutSetVolume (MidiOut, SavedVolume); - } - midiOutClose (MidiOut); - MidiOut = NULL; - } - - m_Status = STATE_Playing; } -void MIDISong2::Pause () + +//========================================================================== +// +// MIDISong2 :: DoInitialSetup +// +// Sets the starting channel volumes. +// +//========================================================================== + +void MIDISong2 :: DoInitialSetup() { - if (m_Status == STATE_Playing) - { - SetEvent (PauseEvent); - m_Status = STATE_Paused; - } -} - -void MIDISong2::Resume () -{ - if (m_Status == STATE_Paused) - { - SetEvent (PauseEvent); - m_Status = STATE_Playing; - } -} - -void MIDISong2::Stop () -{ - if (PlayerThread) - { - SetEvent (ExitEvent); - WaitForSingleObject (PlayerThread, INFINITE); - CloseHandle (PlayerThread); - PlayerThread = NULL; - } - if (MidiOut) - { - midiOutReset (MidiOut); - if (VolumeWorks) - { - midiOutSetVolume (MidiOut, SavedVolume); - } - midiOutClose (MidiOut); - MidiOut = NULL; - } -} - -bool MIDISong2::IsPlaying () -{ - return m_Status != STATE_Stopped; -} - -void MIDISong2::SetVolume (float volume) -{ - SetEvent (VolumeChangeEvent); -} - -DWORD WINAPI MIDISong2::PlayerProc (LPVOID lpParameter) -{ - MIDISong2 *song = (MIDISong2 *)lpParameter; - HANDLE events[2] = { song->ExitEvent, song->PauseEvent }; - bool waited = false; - int i; - DWORD wait; - - SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_TIME_CRITICAL); - - for (i = 0; i < 16; ++i) + for (int i = 0; i < 16; ++i) { // The ASS uses a default volume of 90, but all the other // sources I can find say it's 100. Ideally, any song that // cares about its volume is going to initialize it to // whatever it wants and override this default. - song->ChannelVolumes[i] = 100; + ChannelVolumes[i] = 100; } +} - song->OutputVolume (midivolume & 0xffff); - song->Tempo = 500000; +//========================================================================== +// +// MIDISong2 :: DoRestart +// +// Rewinds every track. +// +//========================================================================== - do +void MIDISong2 :: DoRestart() +{ + int i; + + // Set initial state. + for (i = 0; i < NumTracks; ++i) { - for (i = 0; i < song->NumTracks; ++i) + Tracks[i].TrackP = 0; + Tracks[i].Finished = false; + Tracks[i].RunningStatus = 0; + Tracks[i].Designated = false; + Tracks[i].Designation = 0; + Tracks[i].LoopCount = -1; + Tracks[i].EProgramChange = false; + Tracks[i].EVolume = false; + } + ProcessInitialMetaEvents (); + for (i = 0; i < NumTracks; ++i) + { + Tracks[i].Delay = Tracks[i].ReadVarLen(); + } + TrackDue = Tracks; + TrackDue = FindNextDue(); +} + +//========================================================================== +// +// MIDISong2 :: CheckDone +// +//========================================================================== + +bool MIDISong2::CheckDone() +{ + return TrackDue == NULL; +} + +//========================================================================== +// +// MIDISong2 :: MakeEvents +// +// Copies MIDI events from the SMF and puts them into a MIDI stream +// buffer. Returns the new position in the buffer. +// +//========================================================================== + +DWORD *MIDISong2::MakeEvents(DWORD *events, DWORD *max_event_p, DWORD max_time) +{ + DWORD tot_time = 0; + DWORD time = 0; + + while (TrackDue && events < max_event_p && tot_time <= max_time) + { + time = TrackDue->Delay; + // Advance time for all tracks by the amount needed for the one up next. + if (time != 0) { - song->Tracks[i].TrackP = 0; - song->Tracks[i].Finished = false; - song->Tracks[i].RunningStatus = 0; - song->Tracks[i].Designated = false; - song->Tracks[i].Designation = 0; - song->Tracks[i].LoopCount = -1; - song->Tracks[i].EProgramChange = false; - song->Tracks[i].EVolume = false; - } - - song->ProcessInitialMetaEvents (); - - for (i = 0; i < song->NumTracks; ++i) - { - song->Tracks[i].Delay = song->Tracks[i].ReadVarLen (); - } - - song->TrackDue = song->Tracks; - song->TrackDue = song->FindNextDue (); - - while (0 != (wait = song->SendCommands ())) - { - waited = true; - - // Wait for the exit or pause event or the next note - switch (WaitForMultipleObjects (2, events, FALSE, wait * song->Tempo / song->Division)) + tot_time += time * Tempo / Division; + for (int i = 0; i < NumTracks; ++i) { - case WAIT_OBJECT_0: - song->m_Status = STATE_Stopped; - return 0; - - case WAIT_OBJECT_0+1: - // Go paused - song->OutputVolume (0); - // Wait for the exit or pause event - if (WAIT_OBJECT_0 == WaitForMultipleObjects (2, events, FALSE, INFINITE)) + if (!Tracks[i].Finished) { - song->m_Status = STATE_Stopped; - return 0; - } - song->OutputVolume (midivolume & 0xffff); - } - - for (i = 0; i < song->NumTracks; ++i) - { - if (!song->Tracks[i].Finished) - { - song->Tracks[i].Delay -= wait; + Tracks[i].Delay -= time; } } - song->TrackDue = song->FindNextDue (); - - // Check if the volume needs changing - if (WAIT_OBJECT_0 == WaitForSingleObject (song->VolumeChangeEvent, 0)) - { - song->OutputVolume (midivolume & 0xffff); - } } + // Play all events for this tic. + do + { + events = SendCommand(events, TrackDue, time); + TrackDue = FindNextDue(); + time = 0; + } + while (TrackDue && TrackDue->Delay == 0 && events < max_event_p); } - while (waited && song->m_Looping); - - song->m_Status = STATE_Stopped; - return 0; + return events; } -void MIDISong2::OutputVolume (DWORD volume) -{ - for (int i = 0; i < 16; ++i) - { - BYTE courseVol = (BYTE)(((ChannelVolumes[i]+1) * volume) >> 16); - midiOutShortMsg (MidiOut, i | MIDI_CTRLCHANGE | (7<<8) | (courseVol<<16)); - } -} +//========================================================================== +// +// MIDISong2 :: SendCommand +// +// Places a single MIDIEVENT in the event buffer. +// +//========================================================================== -DWORD MIDISong2::SendCommands () -{ - while (TrackDue && TrackDue->Delay == 0) - { - SendCommand (TrackDue); - TrackDue = FindNextDue (); - } - return TrackDue ? TrackDue->Delay : 0; -} - -#define CHECK_FINISHED \ - if (track->TrackP >= track->MaxTrackP) \ - { \ - track->Finished = true; \ - return; \ - } - -void MIDISong2::SendCommand (TrackInfo *track) +DWORD *MIDISong2::SendCommand (DWORD *events, TrackInfo *track, DWORD delay) { DWORD len; BYTE event, data1 = 0, data2 = 0; @@ -412,7 +362,7 @@ void MIDISong2::SendCommand (TrackInfo *track) event = track->TrackBegin[track->TrackP++]; CHECK_FINISHED - if (event != 0xF0 && event != 0xFF && event != 0xF7) + if (event != MIDI_SYSEX && event != MIDI_META && event != MIDI_SYSEXEND) { // Normal short message if ((event & 0xF0) == 0xF0) @@ -446,39 +396,35 @@ void MIDISong2::SendCommand (TrackInfo *track) switch (event & 0x70) { - case 0x40: + case MIDI_PRGMCHANGE & 0x70: if (track->EProgramChange) { - event = 0xFF; + event = MIDI_META; } break; - case 0x30: + case MIDI_CTRLCHANGE & 0x70: switch (data1) { - case 7: + case 7: // Channel volume if (track->EVolume) - { - event = 0xFF; + { // Tracks that use EMIDI volume ignore normal volume changes. + event = MIDI_META; } else { - // 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. - ChannelVolumes[event & 15] = data2; - data2 = (BYTE)(((data2 + 1) * (midivolume & 0xffff)) >> 16); + data2 = VolumeControllerChange(event & 15, data2); } break; - case 39: + case 39: // Fine channel volume // Skip fine volume adjustment because I am lazy. // (And it doesn't seem to be used much anyway.) - event = 0xFF; + event = MIDI_META; break; case 110: // EMIDI Track Designation - // Instruments 4, 5, 6, and 7 are all FM syth. + // Instruments 4, 5, 6, and 7 are all FM synth. // The rest are all wavetable. if (data2 == 127) { @@ -513,8 +459,7 @@ void MIDISong2::SendCommand (TrackInfo *track) case 113: // EMIDI Volume track->EVolume = true; data1 = 7; - ChannelVolumes[event & 15] = data2; - data2 = (BYTE)(((data2 + 1) * (midivolume & 0xffff)) >> 16); + data2 = VolumeControllerChange(event & 15, data2); break; case 116: // EMIDI Loop Begin @@ -581,31 +526,29 @@ void MIDISong2::SendCommand (TrackInfo *track) } } } - event = 0xFF; + event = MIDI_META; break; } } - if (event != 0xFF && (!track->Designated || (track->Designation & DesignationMask))) + if (event != MIDI_META && (!track->Designated || (track->Designation & DesignationMask))) { - if (MMSYSERR_NOERROR != midiOutShortMsg (MidiOut, event | (data1<<8) | (data2<<16))) - { - track->Finished = true; - return; - } + events[0] = delay; + events[1] = 0; + events[2] = event | (data1<<8) | (data2<<16); + events += 3; } } else { - // Skip SysEx events just because I don't want to bother with - // preparing headers and sending them out. The old MIDI player - // ignores them too, so this won't break anything that played - // before. - if (event == 0xF0 || event == 0xF7) + // Skip SysEx events just because I don't want to bother with them. + // The old MIDI player ignored them too, so this won't break + // anything that played before. + if (event == MIDI_SYSEX || event == MIDI_SYSEXEND) { len = track->ReadVarLen (); track->TrackP += len; } - else if (event == 0xFF) + else if (event == MIDI_META) { // It's a meta-event event = track->TrackBegin[track->TrackP++]; @@ -617,15 +560,19 @@ void MIDISong2::SendCommand (TrackInfo *track) { switch (event) { - case 0x2F: + case MIDI_META_EOT: track->Finished = true; break; - case 0x51: + case MIDI_META_TEMPO: Tempo = (track->TrackBegin[track->TrackP+0]<<16) | (track->TrackBegin[track->TrackP+1]<<8) | (track->TrackBegin[track->TrackP+2]); + events[0] = delay; + events[1] = 0; + events[2] = (MEVT_TEMPO << 24) | Tempo; + events += 3; break; } track->TrackP += len; @@ -642,11 +589,18 @@ void MIDISong2::SendCommand (TrackInfo *track) } if (!track->Finished) { - track->Delay = track->ReadVarLen (); + track->Delay = track->ReadVarLen(); } + return events; } -#undef CHECK_FINISHED +//========================================================================== +// +// MIDISong2 :: ProcessInitialMetaEvents +// +// Handle all the meta events at the start of each track. +// +//========================================================================== void MIDISong2::ProcessInitialMetaEvents () { @@ -670,15 +624,16 @@ void MIDISong2::ProcessInitialMetaEvents () { switch (event) { - case 0x2F: + case MIDI_META_EOT: track->Finished = true; break; - case 0x51: - Tempo = + case MIDI_META_TEMPO: + SetTempo( (track->TrackBegin[track->TrackP+0]<<16) | (track->TrackBegin[track->TrackP+1]<<8) | - (track->TrackBegin[track->TrackP+2]); + (track->TrackBegin[track->TrackP+2]) + ); break; } } @@ -691,6 +646,14 @@ void MIDISong2::ProcessInitialMetaEvents () } } +//========================================================================== +// +// MIDISong2 :: TrackInfo :: ReadVarLen +// +// Reads a variable-length SMF number. +// +//========================================================================== + DWORD MIDISong2::TrackInfo::ReadVarLen () { DWORD time = 0, t = 0x80; @@ -703,6 +666,15 @@ DWORD MIDISong2::TrackInfo::ReadVarLen () return time; } +//========================================================================== +// +// MIDISong2 :: TrackInfo :: FindNextDue +// +// Scans every track for the next event to play. Returns NULL if all events +// have been consumed. +// +//========================================================================== + MIDISong2::TrackInfo *MIDISong2::FindNextDue () { TrackInfo *track; @@ -745,4 +717,23 @@ MIDISong2::TrackInfo *MIDISong2::FindNextDue () } return NULL; } + + +//========================================================================== +// +// MIDISong2 :: SetTempo +// +// Sets the tempo from a track's initial meta events. +// +//========================================================================== + +void MIDISong2::SetTempo(int new_tempo) +{ + MIDIPROPTEMPO tempo = { sizeof(MIDIPROPTEMPO), new_tempo }; + + if (MMSYSERR_NOERROR == midiStreamProperty(MidiOut, (LPBYTE)&tempo, MIDIPROP_SET | MIDIPROP_TEMPO)) + { + Tempo = new_tempo; + } +} #endif diff --git a/src/sound/music_midistream.cpp b/src/sound/music_midistream.cpp new file mode 100644 index 000000000..171a100f6 --- /dev/null +++ b/src/sound/music_midistream.cpp @@ -0,0 +1,636 @@ +/* +** 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 diff --git a/src/sound/music_mus_midiout.cpp b/src/sound/music_mus_midiout.cpp index d35c21320..c31189612 100644 --- a/src/sound/music_mus_midiout.cpp +++ b/src/sound/music_mus_midiout.cpp @@ -1,17 +1,66 @@ +/* +** music_mus_midiout.cpp +** Code to let ZDoom play MUS music through the MIDI streaming API. +** +**--------------------------------------------------------------------------- +** Copyright 1998-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 (140/20) // Each stream buffer lasts only 1/20 of a second + +// TYPES ------------------------------------------------------------------- + +// EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- + +// PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- + +// PRIVATE FUNCTION PROTOTYPES --------------------------------------------- + +// EXTERNAL DATA DECLARATIONS ---------------------------------------------- + extern DWORD midivolume; extern UINT mididevice; -extern HANDLE MusicEvent; - -#define MAX_TIME (10) EXTERN_CVAR (Float, snd_midivolume) +// PRIVATE DATA DEFINITIONS ------------------------------------------------ + static const BYTE CtrlTranslate[15] = { 0, // program change @@ -31,11 +80,28 @@ static const BYTE CtrlTranslate[15] = 121, // reset all controllers }; -MUSSong2::MUSSong2 (FILE *file, char *musiccache, int len) -: MidiOut(0), MusHeader(0), MusBuffer(0) -{ - MusHeader = (MUSHeader *)new BYTE[len]; +// PUBLIC DATA DEFINITIONS ------------------------------------------------- +// CODE -------------------------------------------------------------------- + +//========================================================================== +// +// MUSSong2 Constructor +// +// Performs some validity checks on the MUS file, buffers it, and creates +// the playback thread control events. +// +//========================================================================== + +MUSSong2::MUSSong2 (FILE *file, char *musiccache, int len) +: MusHeader(0), MusBuffer(0) +{ + if (ExitEvent == NULL) + { + return; + } + + MusHeader = (MUSHeader *)new BYTE[len]; if (file != NULL) { if (fread(MusHeader, 1, len, file) != (size_t)len) @@ -59,322 +125,84 @@ MUSSong2::MUSSong2 (FILE *file, char *musiccache, int len) return; } - FullVolEvent.dwDeltaTime = 0; - FullVolEvent.dwStreamID = 0; - FullVolEvent.dwEvent = MEVT_LONGMSG | 8; - FullVolEvent.SysEx[0] = 0xf0; - FullVolEvent.SysEx[1] = 0x7f; - FullVolEvent.SysEx[2] = 0x7f; - FullVolEvent.SysEx[3] = 0x04; - FullVolEvent.SysEx[4] = 0x01; - FullVolEvent.SysEx[5] = 0x7f; - FullVolEvent.SysEx[6] = 0x7f; - FullVolEvent.SysEx[7] = 0xf7; - MusBuffer = (BYTE *)MusHeader + LittleShort(MusHeader->SongStart); MaxMusP = MIN (LittleShort(MusHeader->SongLen), len - LittleShort(MusHeader->SongStart)); - MusP = 0; + Division = 140; + InitialTempo = 1000000; } +//========================================================================== +// +// MUSSong2 Destructor +// +//========================================================================== + MUSSong2::~MUSSong2 () { - Stop (); + if (MusHeader != NULL) + { + delete[] (BYTE *)MusHeader; + } } -bool MUSSong2::IsMIDI () const +//========================================================================== +// +// MUSSong2 :: DoInitialSetup +// +// Sets up initial velocities and channel volumes. +// +//========================================================================== + +void MUSSong2::DoInitialSetup() { - return true; -} - -bool MUSSong2::IsValid () const -{ - return MusBuffer != 0; -} - -void MUSSong2::Play (bool looping) -{ - UINT dev_id; - - m_Status = STATE_Stopped; - m_Looping = looping; - EndQueued = false; - VolumeChanged = false; - Restarting = false; - - 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; - } - - // Set time division and tempo. - MIDIPROPTIMEDIV timediv = { sizeof(MIDIPROPTIMEDIV), 140 }; - MIDIPROPTEMPO tempo = { sizeof(MIDIPROPTEMPO), 1000000 }; - - 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)); - } - if (!VolumeWorks) - { // Send the standard SysEx message for full master volume - memset(&Buffer[0], 0, sizeof(Buffer[0])); - Buffer[0].lpData = (LPSTR)&FullVolEvent; - Buffer[0].dwBufferLength = sizeof(FullVolEvent); - Buffer[0].dwBytesRecorded = sizeof(FullVolEvent); - - if (MMSYSERR_NOERROR == midiOutPrepareHeader((HMIDIOUT)MidiOut, &Buffer[0], sizeof(Buffer[0]))) - { - midiStreamOut(MidiOut, &Buffer[0], sizeof(Buffer[0])); - } - BufferNum = 1; - } - else - { - BufferNum = 0; - } - - snd_midivolume.Callback(); // set volume to current music's properties for (int i = 0; i < 16; ++i) { LastVelocity[i] = 64; ChannelVolumes[i] = 127; } - - // Fill the initial buffers for the song. - do - { - int res = FillBuffer(BufferNum, MAX_EVENTS, MAX_TIME); - if (res == SONG_MORE) - { - if (MMSYSERR_NOERROR != midiStreamOut(MidiOut, &Buffer[BufferNum], sizeof(Buffer[0]))) - { - Stop(); - return; - } - BufferNum ^= 1; - } - else if (res == SONG_DONE) - { - if (looping) - { - MusP = 0; - if (SONG_MORE == FillBuffer(BufferNum, MAX_EVENTS, MAX_TIME)) - { - if (MMSYSERR_NOERROR != midiStreamOut(MidiOut, &Buffer[BufferNum], sizeof(MIDIHDR))) - { - Stop(); - return; - } - BufferNum ^= 1; - } - else - { - Stop(); - return; - } - } - else - { - EndQueued = true; - } - } - else - { - Stop(); - return; - } - } - while (BufferNum != 0); - - if (MMSYSERR_NOERROR != midiStreamRestart(MidiOut)) - { - Stop(); - } - else - { - m_Status = STATE_Playing; - } } -void MUSSong2::Pause () +//========================================================================== +// +// MUSSong2 :: DoRestart +// +// Rewinds the song. +// +//========================================================================== + +void MUSSong2::DoRestart() { - if (m_Status == STATE_Playing) - { - m_Status = STATE_Paused; - OutputVolume(0); - } + MusP = 0; } -void MUSSong2::Resume () +//========================================================================== +// +// MUSSong2 :: CheckDone +// +//========================================================================== + +bool MUSSong2::CheckDone() { - if (m_Status == STATE_Paused) - { - OutputVolume(midivolume & 0xffff); - m_Status = STATE_Playing; - } + return MusP >= MaxMusP; } -void MUSSong2::Stop () +//========================================================================== +// +// MUSSong2 :: MakeEvents +// +// Translates MUS events into MIDI events and puts them into a MIDI stream +// buffer. Returns the new position in the buffer. +// +//========================================================================== + +DWORD *MUSSong2::MakeEvents(DWORD *events, DWORD *max_event_p, DWORD max_time) { - EndQueued = 2; - 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; -} - -bool MUSSong2::IsPlaying () -{ - return m_Status != STATE_Stopped; -} - -void MUSSong2::SetVolume (float volume) -{ - OutputVolume(midivolume & 0xffff); -} - -void MUSSong2::OutputVolume (DWORD volume) -{ - NewVolume = volume; - VolumeChanged = true; -} - -void CALLBACK MUSSong2::Callback(HMIDIOUT hOut, UINT uMsg, DWORD_PTR dwInstance, DWORD dwParam1, DWORD dwParam2) -{ - MUSSong2 *self = (MUSSong2 *)dwInstance; - - if (self->EndQueued > 1) - { - return; - } - if (uMsg == MOM_DONE) - { - SetEvent(MusicEvent); - } -} - -void MUSSong2::ServiceEvent() -{ - if (EndQueued == 1) - { - Stop(); - return; - } - if (MMSYSERR_NOERROR != midiOutUnprepareHeader((HMIDIOUT)MidiOut, &Buffer[BufferNum], sizeof(MIDIHDR))) - { - Printf ("Failed unpreparing MIDI header.\n"); - Stop(); - return; - } -fill: - switch (FillBuffer(BufferNum, MAX_EVENTS, MAX_TIME)) - { - case SONG_MORE: - if (MMSYSERR_NOERROR != midiStreamOut(MidiOut, &Buffer[BufferNum], sizeof(MIDIHDR))) - { - Printf ("Failed streaming MIDI buffer.\n"); - Stop(); - } - else - { - BufferNum ^= 1; - } - break; - - case SONG_DONE: - if (m_Looping) - { - MusP = 0; - Restarting = true; - goto fill; - } - EndQueued = 1; - break; - - default: - Stop(); - break; - } -} - -// Returns SONG_MORE if the buffer was prepared with data. -// Returns SONG_DONE if the song's end was reached. The buffer will never have data in this case. -// Returns SONG_ERROR if there was a problem preparing the buffer. -int MUSSong2::FillBuffer(int buffer_num, int max_events, DWORD max_time) -{ - if (MusP >= MaxMusP) - { - return SONG_DONE; - } - - int i = 0; - SHORTMIDIEVENT *events = Events[buffer_num]; DWORD tot_time = 0; DWORD time = 0; - // If the volume has changed, stick those events at the start of this buffer. - if (VolumeChanged) - { - VolumeChanged = false; - for (; i < 16; ++i) - { - BYTE courseVol = (BYTE)(((ChannelVolumes[i]+1) * NewVolume) >> 16); - events[i].dwDeltaTime = 0; - events[i].dwStreamID = 0; - events[i].dwEvent = MEVT_SHORTMSG | MIDI_CTRLCHANGE | i | (7<<8) | (courseVol<<16); - } - } + max_time = max_time * Division / Tempo; - // If the song is starting over, stop all notes in case any were left hanging. - if (Restarting) - { - Restarting = false; - for (int j = 0; j < 16; ++i, ++j) - { - events[i].dwDeltaTime = 0; - events[i].dwStreamID = 0; - events[i].dwEvent = MEVT_SHORTMSG | MIDI_NOTEOFF | i | (60 << 8) | (64<<16); - } - } - - // Play nothing while paused. - if (m_Status == STATE_Paused) - { - time = max_time; - goto end; - } - - // The final event is for a NOP to hold the delay from the last event. - max_events--; - - for (; i < max_events && tot_time <= max_time; ++i) + while (events < max_event_p && tot_time <= max_time) { BYTE mid1, mid2; BYTE channel; @@ -441,14 +269,9 @@ int MUSSong2::FillBuffer(int buffer_num, int max_events, DWORD max_time) status |= MIDI_CTRLCHANGE; mid1 = CtrlTranslate[t]; mid2 = MusBuffer[MusP++]; - - // 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. if (mid1 == 7) { - ChannelVolumes[channel] = mid2; - mid2 = (BYTE)(((mid2 + 1) * (midivolume & 0xffff)) >> 16); + mid2 = VolumeControllerChange(channel, mid2); } } break; @@ -459,9 +282,10 @@ int MUSSong2::FillBuffer(int buffer_num, int max_events, DWORD max_time) goto end; } - events[i].dwDeltaTime = time; - events[i].dwStreamID = 0; - events[i].dwEvent = MEVT_SHORTMSG | status | (mid1 << 8) | (mid2 << 16); + events[0] = time; // dwDeltaTime + events[1] = 0; // dwStreamID + events[2] = status | (mid1 << 8) | (mid2 << 16); + events += 3; time = 0; if (event & 128) @@ -478,20 +302,11 @@ int MUSSong2::FillBuffer(int buffer_num, int max_events, DWORD max_time) end: if (time != 0) { - events[i].dwDeltaTime = time; - events[i].dwStreamID = 0; - events[i].dwEvent = MEVT_NOP; - i++; + events[0] = time; // dwDeltaTime + events[1] = 0; // dwStreamID + events[2] = MEVT_NOP << 24; // dwEvent + events += 3; } - memset(&Buffer[buffer_num], 0, sizeof(MIDIHDR)); - Buffer[buffer_num].lpData = (LPSTR)events; - Buffer[buffer_num].dwBufferLength = sizeof(events[0]) * i; - Buffer[buffer_num].dwBytesRecorded = sizeof(events[0]) * i; - if (MMSYSERR_NOERROR != midiOutPrepareHeader((HMIDIOUT)MidiOut, &Buffer[buffer_num], sizeof(MIDIHDR))) - { - Printf ("Preparing MIDI header failed.\n"); - return SONG_ERROR; - } - return SONG_MORE; + return events; } #endif diff --git a/src/win32/i_input.cpp b/src/win32/i_input.cpp index 1b6c59e42..c8bc89eb5 100644 --- a/src/win32/i_input.cpp +++ b/src/win32/i_input.cpp @@ -95,7 +95,6 @@ #include "gameconfigfile.h" #include "win32iface.h" #include "templates.h" -#include "i_musicinterns.h" #define DINPUT_BUFFERSIZE 32 @@ -110,7 +109,6 @@ BOOL DI_InitJoy (void); extern HINSTANCE g_hInst; extern DWORD SessionID; -extern HANDLE MusicEvent; extern void ShowEAXEditor (); extern bool SpawnEAXWindow; @@ -1931,23 +1929,7 @@ void I_GetEvent () // Briefly enter an alertable state so that if a secondary thread // crashed, we will execute the APC it sent now. - if (MusicEvent != NULL) - { - DWORD res; - do - { - res = WaitForSingleObjectEx(MusicEvent, 0, TRUE); - } - while (res == WAIT_IO_COMPLETION); - if (res == WAIT_OBJECT_0 && currSong != NULL) - { - currSong->ServiceEvent(); - } - } - else - { - SleepEx (0, TRUE); - } + SleepEx (0, TRUE); while (PeekMessage (&mess, NULL, 0, 0, PM_REMOVE)) { diff --git a/src/win32/i_system.cpp b/src/win32/i_system.cpp index e6ce2f9ba..197f3903e 100644 --- a/src/win32/i_system.cpp +++ b/src/win32/i_system.cpp @@ -62,7 +62,6 @@ #include "templates.h" #include "gameconfigfile.h" #include "v_font.h" -#include "i_musicinterns.h" #include "stats.h" @@ -86,7 +85,6 @@ UINT TimerPeriod; UINT TimerEventID; UINT MillisecondsPerTic; HANDLE NewTicArrived; -HANDLE MusicEvent; uint32 LanguageIDs[4]; void CalculateCPUSpeed (); @@ -175,15 +173,7 @@ int I_WaitForTicEvent (int prevtic) { while (prevtic >= tics) { - HANDLE handles[2] = { NewTicArrived, MusicEvent }; - switch(WaitForMultipleObjects(1 + (MusicEvent != NULL), handles, FALSE, 1000/TICRATE)) - { - case WAIT_OBJECT_0 + 1: - if (currSong != NULL) - { - currSong->ServiceEvent(); - } - } + WaitForSingleObject(NewTicArrived, 1000/TICRATE); } return tics; @@ -445,11 +435,6 @@ void I_Init (void) I_WaitForTic = I_WaitForTicPolled; } - if ((MusicEvent = CreateEvent(NULL, FALSE, FALSE, NULL)) == NULL) - { - Printf ("Creation of music event failed."); - } - atterm (I_ShutdownSound); I_InitSound (); } @@ -515,8 +500,6 @@ void I_Quit (void) timeKillEvent (TimerEventID); if (NewTicArrived) CloseHandle (NewTicArrived); - if (MusicEvent) - CloseHandle (MusicEvent); timeEndPeriod (TimerPeriod); diff --git a/zdoom.vcproj b/zdoom.vcproj index be5d130cf..0e0029f73 100644 --- a/zdoom.vcproj +++ b/zdoom.vcproj @@ -1,7 +1,7 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -2721,14 +2729,6 @@ GeneratePreprocessedFile="0" /> - - - @@ -2751,7 +2751,7 @@ /> - - - @@ -3720,6 +3710,16 @@ Outputs=""src/$(InputName).h"" /> + + + @@ -3743,7 +3743,7 @@ /> - - - @@ -4806,6 +4796,16 @@ Outputs="$(IntDir)/$(InputName).obj" /> + + + @@ -4830,16 +4830,6 @@ Outputs="$(IntDir)\$(InputName).obj" /> - - - @@ -4850,6 +4840,16 @@ Outputs="$(IntDir)/$(InputName).obj" /> + + + @@ -4874,16 +4874,6 @@ Outputs="$(IntDir)\$(InputName).obj" /> - - - @@ -4894,6 +4884,16 @@ Outputs="$(IntDir)\$(InputName).obj" /> + + + @@ -4918,16 +4918,6 @@ Outputs="$(IntDir)\$(InputName).obj" /> - - - @@ -4938,6 +4928,16 @@ Outputs="$(IntDir)\$(InputName).obj" /> + + + @@ -4962,16 +4962,6 @@ Outputs="$(IntDir)\$(InputName).obj" /> - - - @@ -4982,6 +4972,16 @@ Outputs="$(IntDir)\$(InputName).obj" /> + + + @@ -5069,7 +5069,7 @@ /> + + + @@ -5408,14 +5416,6 @@ Outputs="$(IntDir)\$(InputName).obj" /> - - - - - - @@ -5650,6 +5642,14 @@ GeneratePreprocessedFile="0" /> + + + @@ -5671,7 +5671,7 @@ /> + + + @@ -9184,14 +9192,6 @@ AdditionalIncludeDirectories="src\win32;$(NoInherit)" /> - - - @@ -9266,6 +9266,10 @@ RelativePath="src\sound\music_midi_timidity.cpp" > + + @@ -9366,7 +9370,7 @@ />