gzdoom/src/sound/music_midi_midiout.cpp
Randy Heit 3a5afd1418 - Added support for dumping from RAW/DRO/IMF files, so now anything that
can be played as OPL can also be dumped.
- Removed the opl_enable cvar, since OPL playback is now selectable as just
  another MIDI device.
- Added support for DRO playback and dual-chip RAW playback.
- Removed MUS support from OPLMUSSong, since using the OPLMIDIDevice with
  MUSSong2 works just as well. There are still lots of leftover bits in
  the class that should probably be removed at some point, too.
- Added dual-chip dumping support for the RAW format.
- Added DosBox Raw OPL (.DRO) dumping support. For whatever reason,
  in_adlib calculates the song length for this format wrong, even though
  the exact length is stored right in the header. (But in_adlib seems buggy
  in general; too bad it's the only Windows version of Adplug that seems to
  exist.)
- Rewrote the OPL dumper to work with MIDI as well as MUS.


SVN r872 (trunk)
2008-04-03 02:31:39 +00:00

795 lines
19 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;
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, bool opl)
: MIDIStreamer(opl), 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;
}
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 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;
}
}
}
//==========================================================================
//
// 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 39: // Fine channel volume
// Skip fine volume adjustment because I am lazy.
// (And it doesn't seem to be used much anyway.)
event = MIDI_META;
break;
case 110: // EMIDI Track Designation
// Instruments 4, 5, 6, and 7 are all FM synth.
// The rest are all wavetable.
if (data2 == 127)
{
track->Designation = ~0;
}
else
{
if (data2 <= 9)
{
track->Designation |= 1 << data2;
}
}
track->Designated = true;
event = MIDI_META;
break;
case 111: // EMIDI Track Exclusion
if (track->Designated)
{
track->Designation &= ~(1 << data2);
}
event = MIDI_META;
break;
case 112: // EMIDI Program Change
track->EProgramChange = true;
event = 0xC0 | (event & 0x0F);
data1 = data2;
data2 = 0;
break;
case 113: // EMIDI Volume
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 = 0xFF;
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 = 0xFF;
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 = 0xFF;
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;
}
}
if (event != MIDI_META && (!track->Designated || (track->Designation & DesignationMask)))
{
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 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 :: GetOPLDumper
//
//==========================================================================
MusInfo *MIDISong2::GetOPLDumper(const char *filename)
{
return new MIDISong2(this, filename);
}
//==========================================================================
//
// MIDISong2 OPL Dumping Constructor
//
//==========================================================================
MIDISong2::MIDISong2(const MIDISong2 *original, const char *filename)
: MIDIStreamer(filename)
{
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;
}
}