- 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)
This commit is contained in:
Randy Heit 2008-03-05 03:10:31 +00:00
parent 69cebb7e57
commit 68a8ea2189
11 changed files with 1778 additions and 1343 deletions

View file

@ -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 March 3, 2008
- Changed MUS playback to use MIDI streams, like it did during the early days - 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 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 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 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 - Fixed: PTR_BounceTraverse only checked for projectiles that were too
high to pass through two-sided lines, but not ones that were too low. 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 - Fixed: SBARINFO couldn't detect the extreme death damage type for the

View file

@ -1312,6 +1312,8 @@ void S_UpdateSounds (void *listener_p)
angle_t angle; angle_t angle;
int sep; int sep;
I_UpdateMusic();
if (GSnd == NULL) if (GSnd == NULL)
return; return;

View file

@ -112,7 +112,7 @@ bool MusInfo::SetPosition (int order)
return false; return false;
} }
void MusInfo::ServiceEvent () void MusInfo::Update ()
{ {
} }
@ -459,6 +459,14 @@ void *I_RegisterCDSong (int track, int id)
return info; return info;
} }
void I_UpdateMusic()
{
if (currSong != NULL)
{
currSong->Update();
}
}
// Is the song playing? // Is the song playing?
bool I_QrySongPlaying (void *handle) bool I_QrySongPlaying (void *handle)
{ {

View file

@ -44,6 +44,7 @@
void I_InitMusic (); void I_InitMusic ();
void I_ShutdownMusic (); void I_ShutdownMusic ();
void I_BuildMIDIMenuList (struct value_s **values, float *numValues); void I_BuildMIDIMenuList (struct value_s **values, float *numValues);
void I_UpdateMusic ();
// Volume. // Volume.
void I_SetMusicVolume (float volume); void I_SetMusicVolume (float volume);

View file

@ -42,7 +42,7 @@ public:
virtual bool IsMIDI () const = 0; virtual bool IsMIDI () const = 0;
virtual bool IsValid () const = 0; virtual bool IsValid () const = 0;
virtual bool SetPosition (int order); virtual bool SetPosition (int order);
virtual void ServiceEvent (); virtual void Update();
enum EState enum EState
{ {
@ -53,45 +53,41 @@ public:
bool m_Looping; bool m_Looping;
}; };
// MUS file played with MIDI output messages --------------------------------
#ifdef _WIN32 #ifdef _WIN32
struct SHORTMIDIEVENT
{
DWORD dwDeltaTime;
DWORD dwStreamID;
DWORD dwEvent;
};
struct VOLSYSEXEVENT : SHORTMIDIEVENT // Base class for streaming MUS and MIDI files ------------------------------
{
BYTE SysEx[8];
};
class MUSSong2 : public MusInfo class MIDIStreamer : public MusInfo
{ {
public: public:
MUSSong2 (FILE *file, char * musiccache, int length); MIDIStreamer();
~MUSSong2 (); ~MIDIStreamer();
void SetVolume (float volume); void SetVolume(float volume);
void Play (bool looping); void Play(bool looping);
void Pause (); void Pause();
void Resume (); void Resume();
void Stop (); void Stop();
bool IsPlaying (); bool IsPlaying();
bool IsMIDI () const; bool IsMIDI() const;
bool IsValid () const; bool IsValid() const;
void Update();
protected: protected:
static DWORD WINAPI PlayerProc (LPVOID lpParameter); 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); 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 enum
{ {
@ -106,73 +102,76 @@ protected:
}; };
HMIDISTRM MidiOut; HMIDISTRM MidiOut;
HANDLE PlayerThread;
HANDLE ExitEvent;
HANDLE BufferDoneEvent;
DWORD SavedVolume; DWORD SavedVolume;
bool VolumeWorks; bool VolumeWorks;
DWORD Events[2][MAX_EVENTS*3];
MUSHeader *MusHeader;
BYTE *MusBuffer;
BYTE LastVelocity[16];
BYTE ChannelVolumes[16];
size_t MusP, MaxMusP;
VOLSYSEXEVENT FullVolEvent;
SHORTMIDIEVENT Events[2][MAX_EVENTS];
MIDIHDR Buffer[2]; MIDIHDR Buffer[2];
int BufferNum; int BufferNum;
int EndQueued; int EndQueued;
bool VolumeChanged; bool VolumeChanged;
bool Restarting; bool Restarting;
bool InitialPlayback;
DWORD NewVolume; 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 MUSSong2 : public MIDIStreamer
class MIDISong2 : public MusInfo
{ {
public: public:
MIDISong2 (FILE *file, char * musiccache, int length); MUSSong2 (FILE *file, char *musiccache, int length);
~MIDISong2 (); ~MUSSong2 ();
void SetVolume (float volume);
void Play (bool looping);
void Pause ();
void Resume ();
void Stop ();
bool IsPlaying ();
bool IsMIDI () const;
bool IsValid () const;
protected: 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; struct TrackInfo;
static DWORD WINAPI PlayerProc (LPVOID lpParameter);
void OutputVolume (DWORD volume);
void ProcessInitialMetaEvents (); void ProcessInitialMetaEvents ();
DWORD SendCommands (); DWORD *SendCommand (DWORD *event, TrackInfo *track, DWORD delay);
void SendCommand (TrackInfo *track);
TrackInfo *FindNextDue (); TrackInfo *FindNextDue ();
void SetTempo(int new_tempo);
HMIDIOUT MidiOut;
HANDLE PlayerThread;
HANDLE PauseEvent;
HANDLE ExitEvent;
HANDLE TicEvent;
HANDLE VolumeChangeEvent;
DWORD SavedVolume;
bool VolumeWorks;
BYTE *MusHeader; BYTE *MusHeader;
BYTE ChannelVolumes[16];
TrackInfo *Tracks; TrackInfo *Tracks;
TrackInfo *TrackDue; TrackInfo *TrackDue;
int NumTracks; int NumTracks;
int Format; int Format;
int Division;
int Tempo;
WORD DesignationMask; WORD DesignationMask;
}; };
#endif
#endif /* _WIN32 */
// MOD file played with FMOD ------------------------------------------------ // MOD file played with FMOD ------------------------------------------------

View file

@ -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 #ifdef _WIN32
// HEADER FILES ------------------------------------------------------------
#include "i_musicinterns.h" #include "i_musicinterns.h"
#include "templates.h" #include "templates.h"
#include "doomdef.h" #include "doomdef.h"
#include "m_swap.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 struct MIDISong2::TrackInfo
{ {
@ -27,24 +79,50 @@ struct MIDISong2::TrackInfo
DWORD ReadVarLen (); DWORD ReadVarLen ();
}; };
// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------
// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------
// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------
// EXTERNAL DATA DECLARATIONS ----------------------------------------------
EXTERN_CVAR (Float, snd_midivolume)
extern DWORD midivolume; extern DWORD midivolume;
extern UINT mididevice; extern UINT mididevice;
// PRIVATE DATA DEFINITIONS ------------------------------------------------
static BYTE EventLengths[7] = { 2, 2, 2, 2, 1, 1, 2 }; 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 }; 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) // PUBLIC DATA DEFINITIONS -------------------------------------------------
: MidiOut (0), PlayerThread (0),
PauseEvent (0), ExitEvent (0), VolumeChangeEvent (0), // CODE --------------------------------------------------------------------
MusHeader (0)
//==========================================================================
//
// 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 p;
int i; int i;
if (ExitEvent == NULL)
{
return;
}
MusHeader = new BYTE[len]; MusHeader = new BYTE[len];
if (file != NULL) if (file != NULL)
{ {
if (fread (MusHeader, 1, len, file) != (size_t)len) if (fread(MusHeader, 1, len, file) != (size_t)len)
return; return;
} }
else else
@ -70,8 +148,12 @@ MIDISong2::MIDISong2 (FILE *file, char * musiccache, int len)
NumTracks = MusHeader[10] * 256 + MusHeader[11]; NumTracks = MusHeader[10] * 256 + MusHeader[11];
} }
// The timers only have millisecond accuracy, not microsecond. // The division is the number of pulses per quarter note (PPQN).
Division = (MusHeader[12] * 256 + MusHeader[13]) * 1000; 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]; Tracks = new TrackInfo[NumTracks];
@ -110,42 +192,16 @@ MIDISong2::MIDISong2 (FILE *file, char * musiccache, int len)
{ // No tracks, so nothing to play { // No tracks, so nothing to play
return; 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 () MIDISong2::~MIDISong2 ()
{ {
Stop ();
if (PauseEvent != NULL)
{
CloseHandle (PauseEvent);
}
if (ExitEvent != NULL)
{
CloseHandle (ExitEvent);
}
if (VolumeChangeEvent != NULL)
{
CloseHandle (VolumeChangeEvent);
}
if (Tracks != NULL) if (Tracks != NULL)
{ {
delete[] Tracks; 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 void MIDISong2::CheckCaps(DWORD dev_id)
{
return PauseEvent != 0;
}
void MIDISong2::Play (bool looping)
{ {
MIDIOUTCAPS caps; MIDIOUTCAPS caps;
DWORD tid;
m_Status = STATE_Stopped;
m_Looping = looping;
// Find out if this an FM synth or not for EMIDI
DesignationMask = 0xFF0F; 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) if (caps.wTechnology == MOD_FMSYNTH)
{ {
@ -187,222 +236,123 @@ void MIDISong2::Play (bool looping)
DesignationMask = 0x0001; 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) for (int i = 0; i < 16; ++i)
{
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)
{ {
// The ASS uses a default volume of 90, but all the other // The ASS uses a default volume of 90, but all the other
// sources I can find say it's 100. Ideally, any song that // sources I can find say it's 100. Ideally, any song that
// cares about its volume is going to initialize it to // cares about its volume is going to initialize it to
// whatever it wants and override this default. // 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; tot_time += time * Tempo / Division;
song->Tracks[i].Finished = false; for (int i = 0; i < NumTracks; ++i)
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))
{ {
case WAIT_OBJECT_0: if (!Tracks[i].Finished)
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))
{ {
song->m_Status = STATE_Stopped; Tracks[i].Delay -= time;
return 0;
}
song->OutputVolume (midivolume & 0xffff);
}
for (i = 0; i < song->NumTracks; ++i)
{
if (!song->Tracks[i].Finished)
{
song->Tracks[i].Delay -= wait;
} }
} }
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); return events;
song->m_Status = STATE_Stopped;
return 0;
} }
void MIDISong2::OutputVolume (DWORD volume) //==========================================================================
{ //
for (int i = 0; i < 16; ++i) // MIDISong2 :: SendCommand
{ //
BYTE courseVol = (BYTE)(((ChannelVolumes[i]+1) * volume) >> 16); // Places a single MIDIEVENT in the event buffer.
midiOutShortMsg (MidiOut, i | MIDI_CTRLCHANGE | (7<<8) | (courseVol<<16)); //
} //==========================================================================
}
DWORD MIDISong2::SendCommands () DWORD *MIDISong2::SendCommand (DWORD *events, TrackInfo *track, DWORD delay)
{
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 len; DWORD len;
BYTE event, data1 = 0, data2 = 0; BYTE event, data1 = 0, data2 = 0;
@ -412,7 +362,7 @@ void MIDISong2::SendCommand (TrackInfo *track)
event = track->TrackBegin[track->TrackP++]; event = track->TrackBegin[track->TrackP++];
CHECK_FINISHED CHECK_FINISHED
if (event != 0xF0 && event != 0xFF && event != 0xF7) if (event != MIDI_SYSEX && event != MIDI_META && event != MIDI_SYSEXEND)
{ {
// Normal short message // Normal short message
if ((event & 0xF0) == 0xF0) if ((event & 0xF0) == 0xF0)
@ -446,39 +396,35 @@ void MIDISong2::SendCommand (TrackInfo *track)
switch (event & 0x70) switch (event & 0x70)
{ {
case 0x40: case MIDI_PRGMCHANGE & 0x70:
if (track->EProgramChange) if (track->EProgramChange)
{ {
event = 0xFF; event = MIDI_META;
} }
break; break;
case 0x30: case MIDI_CTRLCHANGE & 0x70:
switch (data1) switch (data1)
{ {
case 7: case 7: // Channel volume
if (track->EVolume) if (track->EVolume)
{ { // Tracks that use EMIDI volume ignore normal volume changes.
event = 0xFF; event = MIDI_META;
} }
else else
{ {
// Some devices don't support master volume data2 = VolumeControllerChange(event & 15, data2);
// (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);
} }
break; break;
case 39: case 39: // Fine channel volume
// Skip fine volume adjustment because I am lazy. // Skip fine volume adjustment because I am lazy.
// (And it doesn't seem to be used much anyway.) // (And it doesn't seem to be used much anyway.)
event = 0xFF; event = MIDI_META;
break; break;
case 110: // EMIDI Track Designation 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. // The rest are all wavetable.
if (data2 == 127) if (data2 == 127)
{ {
@ -513,8 +459,7 @@ void MIDISong2::SendCommand (TrackInfo *track)
case 113: // EMIDI Volume case 113: // EMIDI Volume
track->EVolume = true; track->EVolume = true;
data1 = 7; data1 = 7;
ChannelVolumes[event & 15] = data2; data2 = VolumeControllerChange(event & 15, data2);
data2 = (BYTE)(((data2 + 1) * (midivolume & 0xffff)) >> 16);
break; break;
case 116: // EMIDI Loop Begin case 116: // EMIDI Loop Begin
@ -581,31 +526,29 @@ void MIDISong2::SendCommand (TrackInfo *track)
} }
} }
} }
event = 0xFF; event = MIDI_META;
break; 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))) events[0] = delay;
{ events[1] = 0;
track->Finished = true; events[2] = event | (data1<<8) | (data2<<16);
return; events += 3;
}
} }
} }
else else
{ {
// Skip SysEx events just because I don't want to bother with // Skip SysEx events just because I don't want to bother with them.
// preparing headers and sending them out. The old MIDI player // The old MIDI player ignored them too, so this won't break
// ignores them too, so this won't break anything that played // anything that played before.
// before. if (event == MIDI_SYSEX || event == MIDI_SYSEXEND)
if (event == 0xF0 || event == 0xF7)
{ {
len = track->ReadVarLen (); len = track->ReadVarLen ();
track->TrackP += len; track->TrackP += len;
} }
else if (event == 0xFF) else if (event == MIDI_META)
{ {
// It's a meta-event // It's a meta-event
event = track->TrackBegin[track->TrackP++]; event = track->TrackBegin[track->TrackP++];
@ -617,15 +560,19 @@ void MIDISong2::SendCommand (TrackInfo *track)
{ {
switch (event) switch (event)
{ {
case 0x2F: case MIDI_META_EOT:
track->Finished = true; track->Finished = true;
break; break;
case 0x51: case MIDI_META_TEMPO:
Tempo = Tempo =
(track->TrackBegin[track->TrackP+0]<<16) | (track->TrackBegin[track->TrackP+0]<<16) |
(track->TrackBegin[track->TrackP+1]<<8) | (track->TrackBegin[track->TrackP+1]<<8) |
(track->TrackBegin[track->TrackP+2]); (track->TrackBegin[track->TrackP+2]);
events[0] = delay;
events[1] = 0;
events[2] = (MEVT_TEMPO << 24) | Tempo;
events += 3;
break; break;
} }
track->TrackP += len; track->TrackP += len;
@ -642,11 +589,18 @@ void MIDISong2::SendCommand (TrackInfo *track)
} }
if (!track->Finished) 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 () void MIDISong2::ProcessInitialMetaEvents ()
{ {
@ -670,15 +624,16 @@ void MIDISong2::ProcessInitialMetaEvents ()
{ {
switch (event) switch (event)
{ {
case 0x2F: case MIDI_META_EOT:
track->Finished = true; track->Finished = true;
break; break;
case 0x51: case MIDI_META_TEMPO:
Tempo = SetTempo(
(track->TrackBegin[track->TrackP+0]<<16) | (track->TrackBegin[track->TrackP+0]<<16) |
(track->TrackBegin[track->TrackP+1]<<8) | (track->TrackBegin[track->TrackP+1]<<8) |
(track->TrackBegin[track->TrackP+2]); (track->TrackBegin[track->TrackP+2])
);
break; break;
} }
} }
@ -691,6 +646,14 @@ void MIDISong2::ProcessInitialMetaEvents ()
} }
} }
//==========================================================================
//
// MIDISong2 :: TrackInfo :: ReadVarLen
//
// Reads a variable-length SMF number.
//
//==========================================================================
DWORD MIDISong2::TrackInfo::ReadVarLen () DWORD MIDISong2::TrackInfo::ReadVarLen ()
{ {
DWORD time = 0, t = 0x80; DWORD time = 0, t = 0x80;
@ -703,6 +666,15 @@ DWORD MIDISong2::TrackInfo::ReadVarLen ()
return time; 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 () MIDISong2::TrackInfo *MIDISong2::FindNextDue ()
{ {
TrackInfo *track; TrackInfo *track;
@ -745,4 +717,23 @@ MIDISong2::TrackInfo *MIDISong2::FindNextDue ()
} }
return NULL; 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 #endif

View file

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

View file

@ -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 #ifdef _WIN32
// HEADER FILES ------------------------------------------------------------
#include "i_musicinterns.h" #include "i_musicinterns.h"
#include "templates.h" #include "templates.h"
#include "doomdef.h" #include "doomdef.h"
#include "m_swap.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 DWORD midivolume;
extern UINT mididevice; extern UINT mididevice;
extern HANDLE MusicEvent;
#define MAX_TIME (10)
EXTERN_CVAR (Float, snd_midivolume) EXTERN_CVAR (Float, snd_midivolume)
// PRIVATE DATA DEFINITIONS ------------------------------------------------
static const BYTE CtrlTranslate[15] = static const BYTE CtrlTranslate[15] =
{ {
0, // program change 0, // program change
@ -31,11 +80,28 @@ static const BYTE CtrlTranslate[15] =
121, // reset all controllers 121, // reset all controllers
}; };
MUSSong2::MUSSong2 (FILE *file, char *musiccache, int len) // PUBLIC DATA DEFINITIONS -------------------------------------------------
: MidiOut(0), MusHeader(0), MusBuffer(0)
{
MusHeader = (MUSHeader *)new BYTE[len];
// 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 (file != NULL)
{ {
if (fread(MusHeader, 1, len, file) != (size_t)len) if (fread(MusHeader, 1, len, file) != (size_t)len)
@ -59,322 +125,84 @@ MUSSong2::MUSSong2 (FILE *file, char *musiccache, int len)
return; 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); MusBuffer = (BYTE *)MusHeader + LittleShort(MusHeader->SongStart);
MaxMusP = MIN<int> (LittleShort(MusHeader->SongLen), len - LittleShort(MusHeader->SongStart)); MaxMusP = MIN<int> (LittleShort(MusHeader->SongLen), len - LittleShort(MusHeader->SongStart));
MusP = 0; Division = 140;
InitialTempo = 1000000;
} }
//==========================================================================
//
// MUSSong2 Destructor
//
//==========================================================================
MUSSong2::~MUSSong2 () 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) for (int i = 0; i < 16; ++i)
{ {
LastVelocity[i] = 64; LastVelocity[i] = 64;
ChannelVolumes[i] = 127; 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) MusP = 0;
{
m_Status = STATE_Paused;
OutputVolume(0);
}
} }
void MUSSong2::Resume () //==========================================================================
//
// MUSSong2 :: CheckDone
//
//==========================================================================
bool MUSSong2::CheckDone()
{ {
if (m_Status == STATE_Paused) return MusP >= MaxMusP;
{
OutputVolume(midivolume & 0xffff);
m_Status = STATE_Playing;
}
} }
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 tot_time = 0;
DWORD time = 0; DWORD time = 0;
// If the volume has changed, stick those events at the start of this buffer. max_time = max_time * Division / Tempo;
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);
}
}
// If the song is starting over, stop all notes in case any were left hanging. while (events < max_event_p && tot_time <= max_time)
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)
{ {
BYTE mid1, mid2; BYTE mid1, mid2;
BYTE channel; BYTE channel;
@ -441,14 +269,9 @@ int MUSSong2::FillBuffer(int buffer_num, int max_events, DWORD max_time)
status |= MIDI_CTRLCHANGE; status |= MIDI_CTRLCHANGE;
mid1 = CtrlTranslate[t]; mid1 = CtrlTranslate[t];
mid2 = MusBuffer[MusP++]; 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) if (mid1 == 7)
{ {
ChannelVolumes[channel] = mid2; mid2 = VolumeControllerChange(channel, mid2);
mid2 = (BYTE)(((mid2 + 1) * (midivolume & 0xffff)) >> 16);
} }
} }
break; break;
@ -459,9 +282,10 @@ int MUSSong2::FillBuffer(int buffer_num, int max_events, DWORD max_time)
goto end; goto end;
} }
events[i].dwDeltaTime = time; events[0] = time; // dwDeltaTime
events[i].dwStreamID = 0; events[1] = 0; // dwStreamID
events[i].dwEvent = MEVT_SHORTMSG | status | (mid1 << 8) | (mid2 << 16); events[2] = status | (mid1 << 8) | (mid2 << 16);
events += 3;
time = 0; time = 0;
if (event & 128) if (event & 128)
@ -478,20 +302,11 @@ int MUSSong2::FillBuffer(int buffer_num, int max_events, DWORD max_time)
end: end:
if (time != 0) if (time != 0)
{ {
events[i].dwDeltaTime = time; events[0] = time; // dwDeltaTime
events[i].dwStreamID = 0; events[1] = 0; // dwStreamID
events[i].dwEvent = MEVT_NOP; events[2] = MEVT_NOP << 24; // dwEvent
i++; events += 3;
} }
memset(&Buffer[buffer_num], 0, sizeof(MIDIHDR)); return events;
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;
} }
#endif #endif

View file

@ -95,7 +95,6 @@
#include "gameconfigfile.h" #include "gameconfigfile.h"
#include "win32iface.h" #include "win32iface.h"
#include "templates.h" #include "templates.h"
#include "i_musicinterns.h"
#define DINPUT_BUFFERSIZE 32 #define DINPUT_BUFFERSIZE 32
@ -110,7 +109,6 @@ BOOL DI_InitJoy (void);
extern HINSTANCE g_hInst; extern HINSTANCE g_hInst;
extern DWORD SessionID; extern DWORD SessionID;
extern HANDLE MusicEvent;
extern void ShowEAXEditor (); extern void ShowEAXEditor ();
extern bool SpawnEAXWindow; extern bool SpawnEAXWindow;
@ -1931,23 +1929,7 @@ void I_GetEvent ()
// Briefly enter an alertable state so that if a secondary thread // Briefly enter an alertable state so that if a secondary thread
// crashed, we will execute the APC it sent now. // crashed, we will execute the APC it sent now.
if (MusicEvent != NULL) SleepEx (0, TRUE);
{
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);
}
while (PeekMessage (&mess, NULL, 0, 0, PM_REMOVE)) while (PeekMessage (&mess, NULL, 0, 0, PM_REMOVE))
{ {

View file

@ -62,7 +62,6 @@
#include "templates.h" #include "templates.h"
#include "gameconfigfile.h" #include "gameconfigfile.h"
#include "v_font.h" #include "v_font.h"
#include "i_musicinterns.h"
#include "stats.h" #include "stats.h"
@ -86,7 +85,6 @@ UINT TimerPeriod;
UINT TimerEventID; UINT TimerEventID;
UINT MillisecondsPerTic; UINT MillisecondsPerTic;
HANDLE NewTicArrived; HANDLE NewTicArrived;
HANDLE MusicEvent;
uint32 LanguageIDs[4]; uint32 LanguageIDs[4];
void CalculateCPUSpeed (); void CalculateCPUSpeed ();
@ -175,15 +173,7 @@ int I_WaitForTicEvent (int prevtic)
{ {
while (prevtic >= tics) while (prevtic >= tics)
{ {
HANDLE handles[2] = { NewTicArrived, MusicEvent }; WaitForSingleObject(NewTicArrived, 1000/TICRATE);
switch(WaitForMultipleObjects(1 + (MusicEvent != NULL), handles, FALSE, 1000/TICRATE))
{
case WAIT_OBJECT_0 + 1:
if (currSong != NULL)
{
currSong->ServiceEvent();
}
}
} }
return tics; return tics;
@ -445,11 +435,6 @@ void I_Init (void)
I_WaitForTic = I_WaitForTicPolled; I_WaitForTic = I_WaitForTicPolled;
} }
if ((MusicEvent = CreateEvent(NULL, FALSE, FALSE, NULL)) == NULL)
{
Printf ("Creation of music event failed.");
}
atterm (I_ShutdownSound); atterm (I_ShutdownSound);
I_InitSound (); I_InitSound ();
} }
@ -515,8 +500,6 @@ void I_Quit (void)
timeKillEvent (TimerEventID); timeKillEvent (TimerEventID);
if (NewTicArrived) if (NewTicArrived)
CloseHandle (NewTicArrived); CloseHandle (NewTicArrived);
if (MusicEvent)
CloseHandle (MusicEvent);
timeEndPeriod (TimerPeriod); timeEndPeriod (TimerPeriod);

File diff suppressed because it is too large Load diff