gzdoom-gles/src/sound/music_midi_midiout.cpp
Randy Heit 0bb92662d3 - Fixed: Any MIDI ticks that contain only events that are interpreted by
the MIDI parser and not passed on to the MIDI device would mess up timing
  for future events.


SVN r945 (trunk)
2008-04-26 17:12:37 +00:00

957 lines
24 KiB
C++

/*
** 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.
*/
// HEADER FILES ------------------------------------------------------------
#include "i_musicinterns.h"
#include "templates.h"
#include "doomdef.h"
#include "m_swap.h"
// MACROS ------------------------------------------------------------------
// 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
{
const BYTE *TrackBegin;
size_t TrackP;
size_t MaxTrackP;
DWORD Delay;
DWORD PlayedTime;
bool Finished;
BYTE RunningStatus;
SBYTE LoopCount;
bool Designated;
bool EProgramChange;
bool EVolume;
WORD Designation;
size_t LoopBegin;
DWORD LoopDelay;
bool LoopFinished;
DWORD ReadVarLen ();
};
// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------
// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------
// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------
// EXTERNAL DATA DECLARATIONS ----------------------------------------------
// 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 };
// 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, EMIDIDevice type)
: MIDIStreamer(type), MusHeader(0), Tracks(0)
{
int p;
int i;
#ifdef _WIN32
if (ExitEvent == NULL)
{
return;
}
#endif
MusHeader = new BYTE[len];
SongLen = len;
if (file != NULL)
{
if (fread(MusHeader, 1, len, file) != (size_t)len)
return;
}
else
{
memcpy(MusHeader, musiccache, len);
}
// Do some validation of the MIDI file
if (MusHeader[4] != 0 || MusHeader[5] != 0 || MusHeader[6] != 0 || MusHeader[7] != 6)
return;
if (MusHeader[8] != 0 || MusHeader[9] > 2)
return;
Format = MusHeader[9];
if (Format == 0)
{
NumTracks = 1;
}
else
{
NumTracks = MusHeader[10] * 256 + MusHeader[11];
}
// 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];
// Gather information about each track
for (i = 0, p = 14; i < NumTracks && p < len + 8; ++i)
{
DWORD chunkLen =
(MusHeader[p+4]<<24) |
(MusHeader[p+5]<<16) |
(MusHeader[p+6]<<8) |
(MusHeader[p+7]);
if (chunkLen + p + 8 > (DWORD)len)
{ // Track too long, so truncate it
chunkLen = len - p - 8;
}
if (MusHeader[p+0] == 'M' &&
MusHeader[p+1] == 'T' &&
MusHeader[p+2] == 'r' &&
MusHeader[p+3] == 'k')
{
Tracks[i].TrackBegin = MusHeader + p + 8;
Tracks[i].TrackP = 0;
Tracks[i].MaxTrackP = chunkLen;
}
p += chunkLen + 8;
}
// In case there were fewer actual chunks in the file than the
// header specified, update NumTracks with the current value of i
NumTracks = i;
if (NumTracks == 0)
{ // No tracks, so nothing to play
return;
}
}
//==========================================================================
//
// MIDISong2 Destructor
//
//==========================================================================
MIDISong2::~MIDISong2 ()
{
if (Tracks != NULL)
{
delete[] Tracks;
}
if (MusHeader != NULL)
{
delete[] MusHeader;
}
}
//==========================================================================
//
// MIDISong2 :: CheckCaps
//
// Find out if this is an FM synth or not for EMIDI's benefit.
// (Do any released EMIDIs use track designations?)
//
//==========================================================================
void MIDISong2::CheckCaps()
{
int tech = MIDI->GetTechnology();
DesignationMask = 0xFF0F;
if (tech == MOD_FMSYNTH)
{
DesignationMask = 0x00F0;
}
else if (tech == MOD_MIDIPORT)
{
DesignationMask = 0x0001;
}
}
//==========================================================================
//
// MIDISong2 :: DoInitialSetup
//
// Sets the starting channel volumes.
//
//==========================================================================
void MIDISong2 :: DoInitialSetup()
{
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.
ChannelVolumes[i] = 100;
}
}
//==========================================================================
//
// MIDISong2 :: DoRestart
//
// Rewinds every track.
//
//==========================================================================
void MIDISong2 :: DoRestart()
{
int i;
// Set initial state.
for (i = 0; i < 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;
Tracks[i].PlayedTime = 0;
}
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 *start_events;
DWORD tot_time = 0;
DWORD time = 0;
DWORD delay;
start_events = events;
while (TrackDue && events < max_event_p && tot_time <= max_time)
{
// It's possible that this tick may be nothing but meta-events and
// not generate any real events. Repeat this until we actually
// get some output so we don't send an empty buffer to the MIDI
// device.
do
{
delay = TrackDue->Delay;
time += delay;
// Advance time for all tracks by the amount needed for the one up next.
tot_time += delay * Tempo / Division;
AdvanceTracks(delay);
// Play all events for this tick.
do
{
DWORD *new_events = SendCommand(events, TrackDue, time);
TrackDue = FindNextDue();
if (new_events != events)
{
time = 0;
}
events = new_events;
}
while (TrackDue && TrackDue->Delay == 0 && events < max_event_p);
}
while (start_events == events && TrackDue);
time = 0;
}
return events;
}
//==========================================================================
//
// MIDISong2 :: AdvanceTracks
//
// Advaces time for all tracks by the specified amount.
//
//==========================================================================
void MIDISong2::AdvanceTracks(DWORD time)
{
for (int i = 0; i < NumTracks; ++i)
{
if (!Tracks[i].Finished)
{
Tracks[i].Delay -= time;
Tracks[i].PlayedTime += time;
}
}
}
//==========================================================================
//
// MIDISong2 :: SendCommand
//
// Places a single MIDIEVENT in the event buffer.
//
//==========================================================================
DWORD *MIDISong2::SendCommand (DWORD *events, TrackInfo *track, DWORD delay)
{
DWORD len;
BYTE event, data1 = 0, data2 = 0;
int i;
CHECK_FINISHED
event = track->TrackBegin[track->TrackP++];
CHECK_FINISHED
if (event != MIDI_SYSEX && event != MIDI_META && event != MIDI_SYSEXEND)
{
// Normal short message
if ((event & 0xF0) == 0xF0)
{
if (CommonLengths[event & 15] > 0)
{
data1 = track->TrackBegin[track->TrackP++];
if (CommonLengths[event & 15] > 1)
{
data2 = track->TrackBegin[track->TrackP++];
}
}
}
else if ((event & 0x80) == 0)
{
data1 = event;
event = track->RunningStatus;
}
else
{
track->RunningStatus = event;
data1 = track->TrackBegin[track->TrackP++];
}
CHECK_FINISHED
if (EventLengths[(event&0x70)>>4] == 2)
{
data2 = track->TrackBegin[track->TrackP++];
}
switch (event & 0x70)
{
case MIDI_PRGMCHANGE & 0x70:
if (track->EProgramChange)
{
event = MIDI_META;
}
break;
case MIDI_CTRLCHANGE & 0x70:
switch (data1)
{
case 7: // Channel volume
if (track->EVolume)
{ // Tracks that use EMIDI volume ignore normal volume changes.
event = MIDI_META;
}
else
{
data2 = VolumeControllerChange(event & 15, data2);
}
break;
case 7+32: // Channel volume (LSB)
if (track->EVolume)
{
event = MIDI_META;
}
// It should be safe to pass this straight through to the
// MIDI device, since it's a very fine amount.
break;
case 110: // EMIDI Track Designation - InitBeat only
// Instruments 4, 5, 6, and 7 are all FM synth.
// The rest are all wavetable.
if (track->PlayedTime < (DWORD)Division)
{
if (data2 == 127)
{
track->Designation = ~0;
track->Designated = true;
}
else if (data2 <= 9)
{
track->Designation |= 1 << data2;
track->Designated = true;
}
event = MIDI_META;
}
break;
case 111: // EMIDI Track Exclusion - InitBeat only
if (track->PlayedTime < (DWORD)Division)
{
if (track->Designated && data2 <= 9)
{
track->Designation &= ~(1 << data2);
}
event = MIDI_META;
}
break;
case 112: // EMIDI Program Change
// Ignored unless it also appears in the InitBeat
if (track->PlayedTime < (DWORD)Division || track->EProgramChange)
{
track->EProgramChange = true;
event = 0xC0 | (event & 0x0F);
data1 = data2;
data2 = 0;
}
break;
case 113: // EMIDI Volume
// Ignored unless it also appears in the InitBeat
if (track->PlayedTime < (DWORD)Division || track->EVolume)
{
track->EVolume = true;
data1 = 7;
data2 = VolumeControllerChange(event & 15, data2);
}
break;
case 116: // EMIDI Loop Begin
track->LoopBegin = track->TrackP;
track->LoopDelay = 0;
track->LoopCount = data2;
track->LoopFinished = track->Finished;
event = MIDI_META;
break;
case 117: // EMIDI Loop End
if (track->LoopCount >= 0 && data2 == 127)
{
if (track->LoopCount == 0 && !m_Looping)
{
track->Finished = true;
}
else
{
if (track->LoopCount > 0 && --track->LoopCount == 0)
{
track->LoopCount = -1;
}
track->TrackP = track->LoopBegin;
track->Delay = track->LoopDelay;
track->Finished = track->LoopFinished;
}
}
event = MIDI_META;
break;
case 118: // EMIDI Global Loop Begin
for (i = 0; i < NumTracks; ++i)
{
Tracks[i].LoopBegin = Tracks[i].TrackP;
Tracks[i].LoopDelay = Tracks[i].Delay;
Tracks[i].LoopCount = data2;
Tracks[i].LoopFinished = Tracks[i].Finished;
}
event = MIDI_META;
break;
case 119: // EMIDI Global Loop End
if (data2 == 127)
{
for (i = 0; i < NumTracks; ++i)
{
if (Tracks[i].LoopCount >= 0)
{
if (Tracks[i].LoopCount == 0 && !m_Looping)
{
Tracks[i].Finished = true;
}
else
{
if (Tracks[i].LoopCount > 0 && --Tracks[i].LoopCount == 0)
{
Tracks[i].LoopCount = -1;
}
Tracks[i].TrackP = Tracks[i].LoopBegin;
Tracks[i].Delay = Tracks[i].LoopDelay;
Tracks[i].Finished = Tracks[i].LoopFinished;
}
}
}
}
event = MIDI_META;
break;
}
}
events[0] = delay;
events[1] = 0;
if (event != MIDI_META && (!track->Designated || (track->Designation & DesignationMask)))
{
events[2] = event | (data1<<8) | (data2<<16);
}
else
{
events[2] = MEVT_NOP;
}
events += 3;
}
else
{
// 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 == MIDI_META)
{
// It's a meta-event
event = track->TrackBegin[track->TrackP++];
CHECK_FINISHED
len = track->ReadVarLen ();
CHECK_FINISHED
if (track->TrackP + len <= track->MaxTrackP)
{
switch (event)
{
case MIDI_META_EOT:
track->Finished = true;
break;
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;
if (track->TrackP == track->MaxTrackP)
{
track->Finished = true;
}
}
else
{
track->Finished = true;
}
}
}
if (!track->Finished)
{
track->Delay = track->ReadVarLen();
}
return events;
}
//==========================================================================
//
// MIDISong2 :: ProcessInitialMetaEvents
//
// Handle all the meta events at the start of each track.
//
//==========================================================================
void MIDISong2::ProcessInitialMetaEvents ()
{
TrackInfo *track;
int i;
BYTE event;
DWORD len;
for (i = 0; i < NumTracks; ++i)
{
track = &Tracks[i];
while (!track->Finished &&
track->TrackP < track->MaxTrackP - 4 &&
track->TrackBegin[track->TrackP] == 0 &&
track->TrackBegin[track->TrackP+1] == 0xFF)
{
event = track->TrackBegin[track->TrackP+2];
track->TrackP += 3;
len = track->ReadVarLen ();
if (track->TrackP + len <= track->MaxTrackP)
{
switch (event)
{
case MIDI_META_EOT:
track->Finished = true;
break;
case MIDI_META_TEMPO:
SetTempo(
(track->TrackBegin[track->TrackP+0]<<16) |
(track->TrackBegin[track->TrackP+1]<<8) |
(track->TrackBegin[track->TrackP+2])
);
break;
}
}
track->TrackP += len;
}
if (track->TrackP >= track->MaxTrackP - 4)
{
track->Finished = true;
}
}
}
//==========================================================================
//
// MIDISong2 :: TrackInfo :: ReadVarLen
//
// Reads a variable-length SMF number.
//
//==========================================================================
DWORD MIDISong2::TrackInfo::ReadVarLen ()
{
DWORD time = 0, t = 0x80;
while ((t & 0x80) && TrackP < MaxTrackP)
{
t = TrackBegin[TrackP++];
time = (time << 7) | (t & 127);
}
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;
DWORD best;
int i;
if (!TrackDue->Finished && TrackDue->Delay == 0)
{
return TrackDue;
}
switch (Format)
{
case 0:
return Tracks[0].Finished ? NULL : Tracks;
case 1:
track = NULL;
best = 0xFFFFFFFF;
for (i = 0; i < NumTracks; ++i)
{
if (!Tracks[i].Finished)
{
if (Tracks[i].Delay < best)
{
best = Tracks[i].Delay;
track = &Tracks[i];
}
}
}
return track;
case 2:
track = TrackDue;
if (track->Finished)
{
track++;
}
return track < &Tracks[NumTracks] ? track : NULL;
}
return NULL;
}
//==========================================================================
//
// MIDISong2 :: SetTempo
//
// Sets the tempo from a track's initial meta events.
//
//==========================================================================
void MIDISong2::SetTempo(int new_tempo)
{
if (0 == MIDI->SetTempo(new_tempo))
{
Tempo = new_tempo;
}
}
//==========================================================================
//
// MIDISong2 :: Precache
//
// Scans each track for program change events on normal channels and note on
// events on channel 10. Does not care about bank selects, since they're
// unlikely to appear in a song aimed at Doom.
//
//==========================================================================
void MIDISong2::Precache()
{
// This array keeps track of instruments that are used. The first 128
// entries are for melodic instruments. The second 128 are for
// percussion.
BYTE found_instruments[256] = { 0, };
BYTE found_banks[256] = { 0, };
bool multiple_banks = false;
int i, j;
DoRestart();
found_banks[0] = true; // Bank 0 is always used.
found_banks[128] = true;
for (i = 0; i < NumTracks; ++i)
{
TrackInfo *track = &Tracks[i];
BYTE running_status = 0;
BYTE ev, data1, data2, command, channel;
int len;
data2 = 0; // Silence, GCC
while (track->TrackP < track->MaxTrackP)
{
ev = track->TrackBegin[track->TrackP++];
command = ev & 0xF0;
if (ev == MIDI_META)
{
track->TrackP++;
len = track->ReadVarLen();
track->TrackP += len;
}
else if (ev == MIDI_SYSEX || ev == MIDI_SYSEXEND)
{
len = track->ReadVarLen();
track->TrackP += len;
}
else if (command == 0xF0)
{
track->TrackP += CommonLengths[ev & 0x0F];
}
else
{
if ((ev & 0x80) == 0)
{ // Use running status.
data1 = ev;
ev = running_status;
}
else
{ // Store new running status.
running_status = ev;
data1 = track->TrackBegin[track->TrackP++];
}
command = ev & 0x70;
channel = ev & 0x0F;
if (EventLengths[command >> 4] == 2)
{
data2 = track->TrackBegin[track->TrackP++];
}
if (channel != 9 && command == (MIDI_PRGMCHANGE & 0x70))
{
found_instruments[data1 & 127] = true;
}
else if (channel == 9 && command == (MIDI_PRGMCHANGE & 0x70) && data1 != 0)
{ // On a percussion channel, program change also serves as bank select.
multiple_banks = true;
found_banks[data1 | 128] = true;
}
else if (channel == 9 && command == (MIDI_NOTEON & 0x70) && data2 != 0)
{
found_instruments[data1 | 128] = true;
}
else if (command == (MIDI_CTRLCHANGE & 0x70) && data1 == 0 && data2 != 0)
{
multiple_banks = true;
if (channel == 9)
{
found_banks[data2 | 128] = true;
}
else
{
found_banks[data2 & 127] = true;
}
}
}
track->ReadVarLen(); // Skip delay.
}
}
DoRestart();
// Now pack everything into a contiguous region for the PrecacheInstruments call().
TArray<WORD> packed;
for (i = 0; i < 256; ++i)
{
if (found_instruments[i])
{
WORD packnum = (i & 127) | ((i & 128) << 7);
if (!multiple_banks)
{
packed.Push(packnum);
}
else
{ // In order to avoid having to multiplex tracks in a type 1 file,
// precache every used instrument in every used bank, even if not
// all combinations are actually used.
for (j = 0; j < 128; ++j)
{
if (found_banks[j + (i & 128)])
{
packed.Push(packnum | (j << 7));
}
}
}
}
}
MIDI->PrecacheInstruments(&packed[0], packed.Size());
}
//==========================================================================
//
// MIDISong2 :: GetOPLDumper
//
//==========================================================================
MusInfo *MIDISong2::GetOPLDumper(const char *filename)
{
return new MIDISong2(this, filename, MIDI_OPL);
}
//==========================================================================
//
// MIDISong2 :: GetWaveDumper
//
//==========================================================================
MusInfo *MIDISong2::GetWaveDumper(const char *filename, int rate)
{
return new MIDISong2(this, filename, MIDI_Timidity);
}
//==========================================================================
//
// MIDISong2 File Dumping Constructor
//
//==========================================================================
MIDISong2::MIDISong2(const MIDISong2 *original, const char *filename, EMIDIDevice type)
: MIDIStreamer(filename, type)
{
SongLen = original->SongLen;
MusHeader = new BYTE[original->SongLen];
memcpy(MusHeader, original->MusHeader, original->SongLen);
Format = original->Format;
NumTracks = original->NumTracks;
DesignationMask = 0;
Division = original->Division;
Tempo = InitialTempo = original->InitialTempo;
Tracks = new TrackInfo[NumTracks];
for (int i = 0; i < NumTracks; ++i)
{
TrackInfo *newtrack = &Tracks[i];
const TrackInfo *oldtrack = &original->Tracks[i];
newtrack->TrackBegin = MusHeader + (oldtrack->TrackBegin - original->MusHeader);
newtrack->TrackP = 0;
newtrack->MaxTrackP = oldtrack->MaxTrackP;
}
}