/* ** music_xmi_midiout.cpp ** Code to let ZDoom play XMIDI music through the MIDI streaming API. ** **--------------------------------------------------------------------------- ** Copyright 2010 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. **--------------------------------------------------------------------------- ** */ // HEADER FILES ------------------------------------------------------------ #include "i_musicinterns.h" #include "templates.h" #include "doomdef.h" #include "m_swap.h" // MACROS ------------------------------------------------------------------ #define MAX_FOR_DEPTH 4 #define GET_DELAY (EventDue == EVENT_Real ? CurrSong->Delay : NoteOffs[0].Delay) // Used by SendCommand to check for unexpected end-of-track conditions. #define CHECK_FINISHED \ if (track->EventP >= track->EventLen) \ { \ track->Finished = true; \ return events; \ } // TYPES ------------------------------------------------------------------- struct LoopInfo { size_t LoopBegin; int LoopCount; bool LoopFinished; }; struct XMISong::TrackInfo { const BYTE *EventChunk; size_t EventLen; size_t EventP; const BYTE *TimbreChunk; size_t TimbreLen; DWORD Delay; DWORD PlayedTime; bool Finished; LoopInfo ForLoops[MAX_FOR_DEPTH]; int ForDepth; DWORD ReadVarLen(); DWORD ReadDelay(); }; // EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- // PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- // PRIVATE FUNCTION PROTOTYPES --------------------------------------------- // EXTERNAL DATA DECLARATIONS ---------------------------------------------- extern char MIDI_EventLengths[7]; extern char MIDI_CommonLengths[15]; // PRIVATE DATA DEFINITIONS ------------------------------------------------ // PUBLIC DATA DEFINITIONS ------------------------------------------------- // CODE -------------------------------------------------------------------- //========================================================================== // // XMISong Constructor // // Buffers the file and does some validation of the SMF header. // //========================================================================== XMISong::XMISong (FILE *file, BYTE *musiccache, int len, EMidiDevice type) : MIDIStreamer(type), MusHeader(0), Songs(0) { #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); } // Find all the songs in this file. NumSongs = FindXMIDforms(MusHeader, len, NULL); if (NumSongs == 0) { return; } // XMIDI files are played with a constant 120 Hz clock rate. While the // song may contain tempo events, these are vestigial remnants from the // original MIDI file that were not removed by the converter and should // be ignored. // // We can use any combination of Division and Tempo values that work out // to be 120 Hz. Division = 60; InitialTempo = 500000; Songs = new TrackInfo[NumSongs]; memset(Songs, 0, sizeof(*Songs) * NumSongs); FindXMIDforms(MusHeader, len, Songs); CurrSong = Songs; DPrintf("XMI song count: %d\n", NumSongs); } //========================================================================== // // XMISong Destructor // //========================================================================== XMISong::~XMISong () { if (Songs != NULL) { delete[] Songs; } if (MusHeader != NULL) { delete[] MusHeader; } } //========================================================================== // // XMISong :: FindXMIDforms // // Find all FORM XMID chunks in this chunk. // //========================================================================== int XMISong::FindXMIDforms(const BYTE *chunk, int len, TrackInfo *songs) const { int count = 0; for (int p = 0; p <= len - 12; ) { int chunktype = GetNativeInt(chunk + p); int chunklen = GetBigInt(chunk + p + 4); if (chunktype == MAKE_ID('F','O','R','M')) { if (GetNativeInt(chunk + p + 8) == MAKE_ID('X','M','I','D')) { if (songs != NULL) { FoundXMID(chunk + p + 12, chunklen - 4, songs + count); } count++; } } else if (chunktype == MAKE_ID('C','A','T',' ')) { // Recurse to handle CAT chunks. count += FindXMIDforms(chunk + p + 12, chunklen - 4, songs + count); } // IFF chunks are padded to even byte boundaries to avoid // unaligned reads on 68k processors. p += 8 + chunklen + (chunklen & 1); // Avoid crashes from corrupt chunks which indicate a negative size. if (chunklen < 0) p = len; } return count; } //========================================================================== // // XMISong :: FoundXMID // // Records information about this XMID song. // //========================================================================== void XMISong::FoundXMID(const BYTE *chunk, int len, TrackInfo *song) const { for (int p = 0; p <= len - 8; ) { int chunktype = GetNativeInt(chunk + p); int chunklen = GetBigInt(chunk + p + 4); if (chunktype == MAKE_ID('T','I','M','B')) { song->TimbreChunk = chunk + p + 8; song->TimbreLen = chunklen; } else if (chunktype == MAKE_ID('E','V','N','T')) { song->EventChunk = chunk + p + 8; song->EventLen = chunklen; // EVNT must be the final chunk in the FORM. break; } p += 8 + chunklen + (chunklen & 1); } } //========================================================================== // // XMISong :: SetMIDISubsong // // Selects which song in this file to play. // //========================================================================== bool XMISong::SetMIDISubsong(int subsong) { if ((unsigned)subsong >= (unsigned)NumSongs) { return false; } CurrSong = &Songs[subsong]; return true; } //========================================================================== // // XMISong :: DoInitialSetup // // Sets the starting channel volumes. // //========================================================================== void XMISong::DoInitialSetup() { for (int i = 0; i < 16; ++i) { ChannelVolumes[i] = 100; } } //========================================================================== // // XMISong :: DoRestart // // Rewinds the current song. // //========================================================================== void XMISong::DoRestart() { CurrSong->EventP = 0; CurrSong->Finished = false; CurrSong->PlayedTime = 0; CurrSong->ForDepth = 0; NoteOffs.Clear(); ProcessInitialMetaEvents (); CurrSong->Delay = CurrSong->ReadDelay(); EventDue = FindNextDue(); } //========================================================================== // // XMISong :: CheckDone // //========================================================================== bool XMISong::CheckDone() { return EventDue == EVENT_None; } //========================================================================== // // XMISong :: MakeEvents // // Copies MIDI events from the XMI and puts them into a MIDI stream // buffer. Returns the new position in the buffer. // //========================================================================== DWORD *XMISong::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 (EventDue != EVENT_None && 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 = GET_DELAY; time += delay; // Advance time for all tracks by the amount needed for the one up next. tot_time += delay * Tempo / Division; AdvanceSong(delay); // Play all events for this tick. do { DWORD *new_events = SendCommand(events, EventDue, time); EventDue = FindNextDue(); if (new_events != events) { time = 0; } events = new_events; } while (EventDue != EVENT_None && GET_DELAY == 0 && events < max_event_p); } while (start_events == events && EventDue != EVENT_None); time = 0; } return events; } //========================================================================== // // XMISong :: AdvanceSong // // Advances time for the current song by the specified amount. // //========================================================================== void XMISong::AdvanceSong(DWORD time) { if (time != 0) { if (!CurrSong->Finished) { CurrSong->Delay -= time; CurrSong->PlayedTime += time; } NoteOffs.AdvanceTime(time); } } //========================================================================== // // XMISong :: SendCommand // // Places a single MIDIEVENT in the event buffer. // //========================================================================== DWORD *XMISong::SendCommand (DWORD *events, EventSource due, DWORD delay) { DWORD len; BYTE event, data1 = 0, data2 = 0; if (due == EVENT_Fake) { AutoNoteOff off; NoteOffs.Pop(off); events[0] = delay; events[1] = 0; events[2] = MIDI_NOTEON | off.Channel | (off.Key << 8); return events + 3; } TrackInfo *track = CurrSong; CHECK_FINISHED event = track->EventChunk[track->EventP++]; CHECK_FINISHED if (event != MIDI_SYSEX && event != MIDI_META && event != MIDI_SYSEXEND) { // Normal short message if ((event & 0xF0) == 0xF0) { if (MIDI_CommonLengths[event & 15] > 0) { data1 = track->EventChunk[track->EventP++]; if (MIDI_CommonLengths[event & 15] > 1) { data2 = track->EventChunk[track->EventP++]; } } } else { data1 = track->EventChunk[track->EventP++]; } CHECK_FINISHED if (MIDI_EventLengths[(event&0x70)>>4] == 2) { data2 = track->EventChunk[track->EventP++]; } if ((event & 0x70) == (MIDI_CTRLCHANGE & 0x70)) { switch (data1) { case 7: // Channel volume data2 = VolumeControllerChange(event & 15, data2); break; case 110: // XMI channel lock case 111: // XMI channel lock protect case 112: // XMI voice protect case 113: // XMI timbre protect case 115: // XMI indirect controller prefix case 118: // XMI clear beat/bar count case 119: // XMI callback trigger case 120: event = MIDI_META; // none of these are relevant to us. break; case 114: // XMI patch bank select data1 = 0; // Turn this into a standard MIDI bank select controller. break; case 116: // XMI for loop controller if (track->ForDepth < MAX_FOR_DEPTH) { track->ForLoops[track->ForDepth].LoopBegin = track->EventP; track->ForLoops[track->ForDepth].LoopCount = ClampLoopCount(data2); track->ForLoops[track->ForDepth].LoopFinished = track->Finished; } track->ForDepth++; event = MIDI_META; break; case 117: // XMI next loop controller if (track->ForDepth > 0) { int depth = track->ForDepth - 1; if (depth < MAX_FOR_DEPTH) { if (data2 < 64 || (track->ForLoops[depth].LoopCount == 0 && !m_Looping)) { // throw away this loop. track->ForLoops[depth].LoopCount = 1; } // A loop count of 0 loops forever. if (track->ForLoops[depth].LoopCount == 0 || --track->ForLoops[depth].LoopCount > 0) { track->EventP = track->ForLoops[depth].LoopBegin; track->Finished = track->ForLoops[depth].LoopFinished; } else { // done with this loop track->ForDepth = depth; } } else { // ignore any loops deeper than the max depth track->ForDepth = depth; } } event = MIDI_META; break; } } events[0] = delay; events[1] = 0; if (event != MIDI_META) { events[2] = event | (data1<<8) | (data2<<16); } else { events[2] = MEVT_NOP << 24; } events += 3; if ((event & 0x70) == (MIDI_NOTEON & 0x70)) { // XMI note on events include the time until an implied note off event. NoteOffs.AddNoteOff(track->ReadVarLen(), event & 0x0F, data1); } } 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->EventP += len; } else if (event == MIDI_META) { // It's a meta-event event = track->EventChunk[track->EventP++]; CHECK_FINISHED len = track->ReadVarLen (); CHECK_FINISHED if (track->EventP + len <= track->EventLen) { if (event == MIDI_META_EOT) { track->Finished = true; } track->EventP += len; if (track->EventP == track->EventLen) { track->Finished = true; } } else { track->Finished = true; } } } if (!track->Finished) { track->Delay = track->ReadDelay(); } return events; } //========================================================================== // // XMISong :: ProcessInitialMetaEvents // // Handle all the meta events at the start of the current song. // //========================================================================== void XMISong::ProcessInitialMetaEvents () { TrackInfo *track = CurrSong; BYTE event; DWORD len; while (!track->Finished && track->EventP < track->EventLen - 3 && track->EventChunk[track->EventP] == MIDI_META) { event = track->EventChunk[track->EventP+1]; track->EventP += 2; len = track->ReadVarLen(); if (track->EventP + len <= track->EventLen && event == MIDI_META_EOT) { track->Finished = true; } track->EventP += len; } if (track->EventP >= track->EventLen - 1) { track->Finished = true; } } //========================================================================== // // XMISong :: TrackInfo :: ReadVarLen // // Reads a variable length SMF number. // //========================================================================== DWORD XMISong::TrackInfo::ReadVarLen() { DWORD time = 0, t = 0x80; while ((t & 0x80) && EventP < EventLen) { t = EventChunk[EventP++]; time = (time << 7) | (t & 127); } return time; } //========================================================================== // // XMISong :: TrackInfo :: ReadDelay // // XMI does not use variable length numbers for delays. Instead, it uses // runs of bytes with the high bit clear. // //========================================================================== DWORD XMISong::TrackInfo::ReadDelay() { DWORD time = 0, t; while (EventP < EventLen && !((t = EventChunk[EventP]) & 0x80)) { time += t; EventP++; } return time; } //========================================================================== // // XMISong :: FindNextDue // // Decides whether the next event should come from the actual stong or // from the auto note offs. // //========================================================================== XMISong::EventSource XMISong::FindNextDue() { // Are there still events available? if (CurrSong->Finished && NoteOffs.Size() == 0) { return EVENT_None; } // Which is due sooner? The current song or the note-offs? DWORD real_delay = CurrSong->Finished ? 0xFFFFFFFF : CurrSong->Delay; DWORD fake_delay = NoteOffs.Size() == 0 ? 0xFFFFFFFF : NoteOffs[0].Delay; return (fake_delay <= real_delay) ? EVENT_Fake : EVENT_Real; } //========================================================================== // // XMISong :: GetOPLDumper // //========================================================================== MusInfo *XMISong::GetOPLDumper(const char *filename) { return new XMISong(this, filename, MDEV_OPL); } //========================================================================== // // XMISong :: GetWaveDumper // //========================================================================== MusInfo *XMISong::GetWaveDumper(const char *filename, int rate) { return new XMISong(this, filename, MDEV_GUS); } //========================================================================== // // XMISong File Dumping Constructor // //========================================================================== XMISong::XMISong(const XMISong *original, const char *filename, EMidiDevice type) : MIDIStreamer(filename, type) { SongLen = original->SongLen; MusHeader = new BYTE[original->SongLen]; memcpy(MusHeader, original->MusHeader, original->SongLen); NumSongs = original->NumSongs; Tempo = InitialTempo = original->InitialTempo; Songs = new TrackInfo[NumSongs]; for (int i = 0; i < NumSongs; ++i) { TrackInfo *newtrack = &Songs[i]; const TrackInfo *oldtrack = &original->Songs[i]; newtrack->EventChunk = MusHeader + (oldtrack->EventChunk - original->MusHeader); newtrack->EventLen = oldtrack->EventLen; newtrack->EventP = 0; newtrack->TimbreChunk = MusHeader + (oldtrack->TimbreChunk - original->MusHeader); newtrack->TimbreLen = oldtrack->TimbreLen; } }