mirror of
https://github.com/ZDoom/qzdoom.git
synced 2024-11-15 08:52:04 +00:00
9457a1b3f8
should ignore any tempo events in XMIDI songs that were left over from the original MIDI files, since the converter didn't remove them. SVN r3384 (trunk)
711 lines
19 KiB
C++
711 lines
19 KiB
C++
/*
|
|
** 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;
|
|
}
|
|
}
|