Files
BT412/engine/MUNGA/AUDSEQ.cpp
T
arcattackandClaude Opus 4.8 7b7d465e5e Initial commit: bt411 -- standalone Windows BattleTech (Tesla 4.10 port)
Clean, self-contained extraction of the BattleTech-specific work from the
reverse-engineering workspace -- engine + game + content + build, with nothing
from Red Planet or the raw archive dumps. Builds green (Win32) and runs the
single-player drive->animate->target->fire->damage->destroy loop out of the box.

Layout:
  engine/   MUNGA + MUNGA_L4 shared 2007 engine, carrying our BT render/loader
            work (bgfload/L4D3D/L4VIDEO: BSL bit-slice decode, LOD/ground/shadow
            models) + image codec; the minimal rp/ headers the audio HAL needs
  game/     reconstructed BT logic + surviving-original BT source + fwd shims
            + WinMain launcher
  content/  full runtime tree (BTL4.RES, VIDEO/, GAUGE/, AUDIO/, eggs, BTDPL.INI)
  docs/     format specs + reconstruction ledgers
  reference/ raw Ghidra pseudocode (recon source-of-truth) + decomp exporter
  tools/    MP console emulator + map/resource scanners

One top-level CMake builds munga_engine lib + bt410_l4 game lib + btl4.exe.
All paths relativized (186 fwd shims + ~437 CMake abs paths -> repo-relative);
DXSDK is the one external, overridable via -DDXSDK. Verified: builds to a
byte-identical 2.27MB exe and runs combat (TARGET DESTROYED, 0 crashes) against
the bundled content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:03:40 -05:00

869 lines
20 KiB
C++

#include "munga.h"
#pragma hdrstop
#include "audseq.h"
#include "objstrm.h"
#include "namelist.h"
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~ AudioControlEvent ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//#############################################################################
//#############################################################################
//
AudioControlEvent::AudioControlEvent(
AudioTick start_tick,
AudioControlID audio_control_ID,
AudioControlValue audio_control_value
):
Plug(AudioControlEventClassID)
{
startTick = start_tick;
audioControlID = audio_control_ID;
audioControlValue = audio_control_value;
}
//
//#############################################################################
//#############################################################################
//
AudioControlEvent::AudioControlEvent():
Plug(AudioControlEventClassID)
{
startTick = 0;
audioControlID = NullAudioControlID;
audioControlValue = 0.0f;
}
//
//#############################################################################
//#############################################################################
//
AudioControlEvent::~AudioControlEvent()
{
}
//
//#############################################################################
//#############################################################################
//
Logical
AudioControlEvent::TestInstance() const
{
Plug::TestInstance();
Verify(
(Enumeration)audioControlID >= (Enumeration)0 &&
(Enumeration)audioControlID < (Enumeration)AudioControlIDCount
);
return True;
}
//
//#############################################################################
//#############################################################################
//
void
AudioControlEvent::Send(AudioComponent *audio_component)
{
Check(this);
Check(audio_component);
audio_component->ReceiveControl(audioControlID, audioControlValue);
}
//
//#############################################################################
//#############################################################################
//
Logical
AudioControlEvent::Chase(AudioComponent *audio_component)
{
Check(this);
Check(audio_component);
switch (audioControlID)
{
case StartAudioControlID:
return False;
case StopAudioControlID:
Send(audio_component);
return False;
default:
break;
}
return True;
}
//
//#############################################################################
//#############################################################################
//
MemoryStream&
MemoryStream_Read(
MemoryStream *stream,
AudioControlEvent *control_event
)
{
Check(stream);
Check_Signature(control_event);
MemoryStream_Read(stream, &control_event->startTick);
MemoryStream_Read(stream, &control_event->audioControlID);
MemoryStream_Read(stream, &control_event->audioControlValue);
Check(control_event);
return *stream;
}
//
//#############################################################################
//#############################################################################
//
MemoryStream&
MemoryStream_Write(
MemoryStream *stream,
const AudioControlEvent *control_event
)
{
Check(stream);
Check(control_event);
MemoryStream_Write(stream, &control_event->startTick);
MemoryStream_Write(stream, &control_event->audioControlID);
MemoryStream_Write(stream, &control_event->audioControlValue);
return *stream;
}
//
//#############################################################################
//#############################################################################
//
std::ostream& operator << (std::ostream &strm, const AudioControlEvent &control_event)
{
Check(&control_event);
strm << "[" ;
strm << control_event.startTick << ",";
strm << control_event.audioControlID << ",";
strm << control_event.audioControlValue;
strm << "]";
return strm;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~ AudioControlSequence ~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//#############################################################################
//#############################################################################
//
AudioControlSequence::AudioControlSequence(
PlugStream *stream,
Entity *entity
):
AudioComponent(stream),
audioComponentSocket(NULL),
audioControlEventSocket(NULL)
{
Logical dump_value;
Logical is_looped;
AudioComponent *audio_component;
AudioControlEvent *audio_control_event;
CollectionSize i, number_of_control_events;
AudioDivisionsPerBeat divisions_per_beat;
AudioTempo tempo;
MemoryStream_Read(stream, &dump_value);
MemoryStream_Read(stream, &is_looped);
PlugStream_ReadObjectIDAndFindPlug(stream, &audio_component);
MemoryStream_Read(stream, &divisions_per_beat);
MemoryStream_Read(stream, &tempo);
MemoryStream_Read(stream, &number_of_control_events);
for (i = 0; i < number_of_control_events; i++)
{
audio_control_event = new AudioControlEvent;
Register_Object(audio_control_event);
MemoryStream_Read(stream, audio_control_event);
audioControlEventSocket.Add(audio_control_event);
}
AudioControlSequenceX(
audio_component,
entity,
is_looped,
divisions_per_beat,
tempo,
dump_value
);
}
//
//#############################################################################
//#############################################################################
//
#if DEBUG_LEVEL>0
void
AudioControlSequence::AudioControlSequenceX(
AudioComponent *audio_component,
Entity *entity,
Logical is_looped,
AudioDivisionsPerBeat divisions_per_beat,
AudioTempo the_tempo,
Logical dump_value
)
#else
void
AudioControlSequence::AudioControlSequenceX(
AudioComponent *audio_component,
Entity *entity,
Logical is_looped,
AudioDivisionsPerBeat divisions_per_beat,
AudioTempo the_tempo,
Logical
)
#endif
{
Check(audio_component);
audioComponentSocket.Add(audio_component);
isLooped = is_looped;
isRunning = False;
startTime = AudioTime::Null;
audioControlEventIterator = NULL;
divisionsPerBeat = divisions_per_beat;
tempo = the_tempo;
#if DEBUG_LEVEL>0
dumpValue = dump_value;
#endif
audio_component->AddWatcher(this);
Check(entity);
entity->AddAudioComponent(this);
}
//
//#############################################################################
//#############################################################################
//
AudioControlSequence::~AudioControlSequence()
{
StopSequence();
Verify(!isRunning);
Verify(audioControlEventIterator == NULL);
AudioControlEventIterator iterator(&audioControlEventSocket);
Check(&iterator);
iterator.DeletePlugs();
}
//
//#############################################################################
//#############################################################################
//
void
AudioControlSequence::BuildFromPage(
PlugStream *stream,
NameList *name_list,
ClassID class_ID,
ObjectID object_ID
)
{
AudioComponent::BuildFromPage(stream, name_list, class_ID, object_ID);
MEM_STRM_WRITE_ENTRY(*stream, name_list, Logical, dump_value);
MEM_STRM_WRITE_ENTRY(*stream, name_list, Logical, is_looped);
//
// Write audio component
//
CString audio_component_name("audio_component");
PlugStream_FindEntryAndWriteObjectID(stream, name_list, audio_component_name);
//
// Read tempo
//
AudioTempo tempo = 100;
if (name_list->FindData("tempo") != NULL)
{
Check_Pointer(name_list->FindData("tempo"));
Convert_From_Ascii((const char *)name_list->FindData("tempo"), &tempo);
}
//
// Read midi file
//
DynamicMemoryStream midi_file_stream;
{
CString midi_file_name;
Check_Pointer(name_list->FindData("midi_file"));
midi_file_name = (const char*)name_list->FindData("midi_file");
std::ifstream input_midi_file(midi_file_name, std::ios::in | std::ios::binary);
#if DEBUG_LEVEL>0
if (!input_midi_file)
{
Dump(midi_file_name);
}
#endif
MemoryStream_Write(&midi_file_stream, &input_midi_file);
midi_file_stream.Rewind();
}
//
// Parse midi stream
//
DynamicMemoryStream control_event_stream;
CreateAudioControlEventStream midi_parser;
Check(&midi_file_stream);
Check(&control_event_stream);
Check(&midi_parser);
midi_parser.Parse(&midi_file_stream, &control_event_stream, tempo);
control_event_stream.Rewind();
//
// Write the divisions per beat and tempo
//
AudioDivisionsPerBeat divisions_per_beat;
divisions_per_beat = midi_parser.GetDivisionsPerBeat();
tempo = midi_parser.GetTempo();
MemoryStream_Write(stream, &divisions_per_beat);
MemoryStream_Write(stream, &tempo);
//
// Write control event stream
//
CollectionSize number_of_control_events;
number_of_control_events = midi_parser.GetNumberOfEvents();
MemoryStream_Write(stream, &number_of_control_events);
MemoryStream_Write(stream, &control_event_stream);
}
//
//#############################################################################
//#############################################################################
//
Logical
AudioControlSequence::TestInstance() const
{
AudioComponent::TestInstance();
Check(&audioComponentSocket);
Check(&audioControlEventSocket);
if (isRunning)
{
Check(audioControlEventIterator);
}
else
{
Verify(audioControlEventIterator == NULL);
}
Verify(tempo >= 1 && tempo <= 600);
return True;
}
//
//#############################################################################
//#############################################################################
//
void
AudioControlSequence::StartSequence()
{
Check(this);
//
// If already running, stop sequence
//
if (isRunning)
{
StopSequence();
}
//
// Make an iterator for the control sequence
//
Verify(audioControlEventIterator == NULL);
audioControlEventIterator =
new AudioControlEventIterator(&audioControlEventSocket);
Register_Object(audioControlEventIterator);
startTime = AudioTime::Now();
isRunning = True;
}
//
//#############################################################################
//#############################################################################
//
void
AudioControlSequence::StopSequence()
{
Check(this);
//
// If not running, return
//
if (!isRunning)
return;
//
// Find next on or off event
//
AudioControlEvent *control_event;
Check(audioControlEventIterator);
while ((control_event = audioControlEventIterator->ReadAndNext()) != NULL)
{
Check(control_event);
if (!control_event->Chase(audioComponentSocket.GetCurrent()))
break;
}
//
// Destroy the iterator for the control sequence
//
Unregister_Object(audioControlEventIterator);
delete audioControlEventIterator;
audioControlEventIterator = NULL;
isRunning = False;
}
//
//#############################################################################
//#############################################################################
//
void
AudioControlSequence::RunSequence()
{
Check(this);
//
// If the sequence is not running, then return
//
if (!isRunning)
return;
//
// Play events which have a start time less then the
// current time
//
AudioControlEvent *control_event;
Check(audioControlEventIterator);
control_event = audioControlEventIterator->GetCurrent();
while (
control_event != NULL &&
IsEventReady(control_event)
)
{
#if DEBUG_LEVEL>0
if (dumpValue)
{
Dump(*control_event);
}
#endif
#if 0
//
// HACK - convert start control to use duration
//
Verify(control_event->audioControlID != StopAudioControlID);
if (control_event->audioControlID == StartAudioControlID)
{
//
// Convert ticks to seconds
// seconds = ticks / (divisionsPerBeat (t/b) * tempo (b/60s))
//
Scalar denominator =
(Scalar)divisionsPerBeat * (Scalar)tempo / 60.0f;
Verify(!Small_Enough(denominator));
control_event->audioControlValue =
control_event->audioControlValue / denominator;
}
#endif
//
// Send the controller to the connected audio component
//
Check(control_event);
control_event->Send(audioComponentSocket.GetCurrent());
//
// Step to next event
//
audioControlEventIterator->Next();
control_event = audioControlEventIterator->GetCurrent();
}
//
// If there are no more events to be played then stop running
//
if (control_event == NULL)
{
StopSequence();
if (isLooped)
{
StartSequence();
}
}
}
//
//#############################################################################
//#############################################################################
//
void
AudioControlSequence::ReceiveControl(
AudioControlID control_ID,
AudioControlValue control_value
)
{
Check(this);
switch(control_ID)
{
case StartAudioControlID:
StartSequence();
break;
case StopAudioControlID:
StopSequence();
break;
case IdleAudioControlID:
RunSequence();
break;
case TempoAudioControlID:
tempo = control_value;
Verify(tempo >= 1 && tempo <= 600);
break;
default:
break;
}
}
//
//#############################################################################
//#############################################################################
//
Logical
AudioControlSequence::IsEventReady(AudioControlEvent *audio_control_event)
{
Check(this);
AudioTime offset_time(startTime);
offset_time += CalculateEventSeconds(audio_control_event);
return (offset_time <= AudioTime::Now());
}
//
//#############################################################################
//#############################################################################
//
Scalar
AudioControlSequence::CalculateEventSeconds(
AudioControlEvent *audio_control_event
)
{
Check(this);
Check(audio_control_event);
//
// Convert ticks to seconds
// seconds = ticks / (divisionsPerBeat (t/b) * tempo (b/60s))
//
Scalar denominator = (Scalar)divisionsPerBeat * (Scalar)tempo / 60.0f;
Verify(!Small_Enough(denominator));
return (Scalar)audio_control_event->GetStartTick() / denominator;
}
//#############################################################################
//#################### CreateAudioControlEventStream ####################
//#############################################################################
CreateAudioControlEventStream::CreateAudioControlEventStream():
audioControlEventSocket(NULL, False)
{
inputStream = NULL;
outputStream = NULL;
divisionsPerBeat = 120;
tempo = 100;
}
//
//#############################################################################
//#############################################################################
//
CreateAudioControlEventStream::~CreateAudioControlEventStream(void)
{
AudioControlEventIterator iterator(&audioControlEventSocket);
Check(&iterator);
iterator.DeletePlugs();
}
//
//#############################################################################
//#############################################################################
//
Logical
CreateAudioControlEventStream::TestInstance() const
{
MidiParse::TestInstance();
Check(&audioControlEventSocket);
return True;
}
//
//#############################################################################
//#############################################################################
//
void
CreateAudioControlEventStream::Parse(
MemoryStream *input_stream,
MemoryStream *output_stream,
SignedLongWord tempo_argument
)
{
Check(this);
Check(input_stream);
Check(output_stream);
inputStream = input_stream;
outputStream = output_stream;
//
// Parse the stream
//
tempo = tempo_argument;
Verify(GetNumberOfEvents() == 0);
MidiParse::Parse();
//
// Write the events
//
AudioControlEventIterator iterator(&audioControlEventSocket);
AudioControlEvent *control_event;
Check(&iterator);
while ((control_event = iterator.ReadAndNext()) != NULL)
{
Check(control_event);
MemoryStream_Write(outputStream, control_event);
}
}
//
//#############################################################################
//#############################################################################
//
CollectionSize
CreateAudioControlEventStream::GetNumberOfEvents()
{
Check(this);
AudioControlEventIterator iterator(&audioControlEventSocket);
Check(&iterator);
return iterator.GetSize();
}
//
//#############################################################################
//#############################################################################
//
MidiParse::SignedWord
CreateAudioControlEventStream::Mf_getc()
{
Check(this);
unsigned char c;
MemoryStream_Read(inputStream, &c);
return c;
}
//
//#############################################################################
//#############################################################################
//
void
CreateAudioControlEventStream::Mf_error(char *str)
{
Check(this);
Check_Pointer(str);
Fail(str);
}
//
//#############################################################################
//#############################################################################
//
void
CreateAudioControlEventStream::Mf_header(
SignedWord,
SignedWord,
SignedWord division
)
{
Check(this);
divisionsPerBeat = division;
}
//
//#############################################################################
//#############################################################################
//
void
CreateAudioControlEventStream::Mf_on(
SignedWord channel,
SignedWord pitch,
SignedWord vol
)
{
Check(this);
//
// Interpret vol 0 as note off
//
if (vol == 0)
{
Mf_off(channel, pitch, vol);
return;
}
//
// Verify that last event was a stop
//
#if DEBUG_LEVEL>0
{
AudioControlEventIterator iterator(&audioControlEventSocket);
AudioControlEvent *last_control_event;
iterator.Last();
if ((last_control_event = iterator.GetCurrent()) != NULL)
{
Check(last_control_event);
Verify(last_control_event->audioControlID == StopAudioControlID);
}
}
#endif
AudioTick current_ticks = CurTime();
AudioControlEvent *audio_control_event;
//
// Add note value event
//
audio_control_event
= new AudioControlEvent(
current_ticks,
NoteAudioControlID,
(Scalar)pitch
);
Register_Object(audio_control_event);
audioControlEventSocket.AddValue(audio_control_event, current_ticks);
//
// Add velocity event
//
audio_control_event
= new AudioControlEvent(
current_ticks,
AttackVolumeAudioControlID,
(Scalar)vol / (Scalar)127
);
Register_Object(audio_control_event);
audioControlEventSocket.AddValue(audio_control_event, current_ticks);
//
// Add start event
//
audio_control_event
= new AudioControlEvent(
current_ticks,
StartAudioControlID,
0.0f
);
Register_Object(audio_control_event);
audioControlEventSocket.AddValue(audio_control_event, current_ticks);
}
//
//#############################################################################
//#############################################################################
//
void
CreateAudioControlEventStream::Mf_off(
SignedWord,
SignedWord pitch,
SignedWord
)
{
Check(this);
AudioTick current_ticks = CurTime();
AudioControlEvent *audio_control_event;
#if 0
//
// HACK - convert start control to use duration
//
AudioControlEventIterator
iterator(&audioControlEventSocket);
iterator.Last();
audio_control_event = iterator.GetCurrent();
Check(audio_control_event);
Verify(audio_control_event->audioControlID == StartAudioControlID);
audio_control_event->audioControlValue =
current_ticks - audio_control_event->startTick;
#else
//
// Add note value event
//
audio_control_event
= new AudioControlEvent(
current_ticks,
NoteAudioControlID,
(Scalar)pitch
);
Register_Object(audio_control_event);
audioControlEventSocket.AddValue(audio_control_event, current_ticks);
//
// Add stop event
//
audio_control_event
= new AudioControlEvent(
current_ticks,
StopAudioControlID,
0.0f
);
Register_Object(audio_control_event);
audioControlEventSocket.AddValue(audio_control_event, current_ticks);
#endif
}
//
//#############################################################################
//#############################################################################
//
void
CreateAudioControlEventStream::Mf_tempo(SignedLongWord)
{
Check(this);
// tempo = i1; // HACK - Cakewalk appears to produce bogus tempo
}