Files
BT411/engine/MUNGA_L4/L4AUDLVL.cpp
T
arcattackandClaude Fable 5 70eea6c1a4 Boresight parallax FIXED + the #4/#5 verdict instrumentation (Gitea #16, #4, #5)
PARALLAX (#16): the pick/fire ray was anchored at mech.y+5.0 (a port
improvisation) while the sight line ran from the eyepoint (y=6.23) --
two parallel rays whose constant offset grew into the reported low-miss
as range closed (measured ry +0.072 @50u -> +1.54 @2.7u).  The decomp's
sight and pick share the eye origin (HudSimulation @4b7830 chain).  Fix:
the viewpoint mech's cockpit eye owns the aim-camera publish in BOTH
views, origin = its own eye translation; leveling + deliberate elevation
untouched; chase view now converges to cockpit ballistics (V cannot
change where shots land).  After: pick pinned to the crosshair (ry <=
5e-6) from 50u to point-blank; 26 laser + 8 missile center-mass hits at
3/4-screen.  Awaiting the reporters' approach-test.

VERDICT INSTRUMENTATION (#4 closed authentic, #5 verdict posted):
BT_RANGE_LOG per-frame pick tracing + BTGroundRayHitExact analytic
cross-check (0/8000 arena fall-throughs; cavern 6/8400 single-frame
grazes -- the 'crazy sliding' is the authentic world-pick + 500 m/s
slide over depth discontinuities); BT_AUD_TAIL StopNote/fade timing +
BT_FIRE_PULSE single-shot driver (the energy 'buzz' is the AUTHORED
charging loop: crescendo through recharge, 1.309s authored release,
measured within one frame).  CAVERN.EGG: solo cavern test egg.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 14:51:54 -05:00

511 lines
15 KiB
C++

#include <cstdlib>
#include "mungal4.h"
#pragma hdrstop
#include "l4audlvl.h"
#include "l4audres.h"
#include "..\munga\audrend.h"
#include "..\munga\objstrm.h"
#include "..\munga\namelist.h"
#include "openal/al.h"
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Release fades ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// (task #50, AUDIO_FIDELITY F13) ~20 looping presets author a releaseVolEnv of
// 1.1-3.9 s the AWE32 applied as a note-off fade while the loop continued;
// StopNote registers a dB-linear ramp here instead of cutting. Serviced once
// per frame from AudioHead::Execute; a fade finishing (or being reclaimed by a
// restart) stops + rewinds the source so the next SetupPatch sees AL_INITIAL.
//
// [aud-tail] (Gitea #5 verdict harness, additive): BT_AUD_TAIL=1 gates
// timestamped play/stop/fade tracing so the audible tail past beam-off can be
// measured against the authored releaseSec. clock() is CRT wall-clock ms and
// matches the [aud-tail] emitter state-transition logs (emitter.cpp).
#include <ctime>
static bool AudTailLog()
{
static int s_on = -1;
if (s_on < 0) s_on = (getenv("BT_AUD_TAIL") != 0) ? 1 : 0;
return s_on != 0;
}
namespace
{
struct ReleaseFade
{
ALuint source;
float gain0;
float duration;
float elapsed;
bool active;
};
const int MAX_RELEASE_FADES = 64;
ReleaseFade s_releaseFades[MAX_RELEASE_FADES];
void FinishFade(ReleaseFade &fade)
{
if (AudTailLog())
DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms fade-END src="
<< fade.source << " after " << fade.elapsed << "/" << fade.duration
<< "s\n" << std::flush;
alSourceStop(fade.source);
alSourceRewind(fade.source);
alSourcef(fade.source, AL_GAIN, fade.gain0);
fade.active = false;
}
void StartReleaseFade(ALuint source, float duration)
{
for (int i = 0; i < MAX_RELEASE_FADES; i++)
{
if (s_releaseFades[i].active && s_releaseFades[i].source == source)
{
return; // already releasing
}
}
for (int i = 0; i < MAX_RELEASE_FADES; i++)
{
if (!s_releaseFades[i].active)
{
ReleaseFade &fade = s_releaseFades[i];
fade.source = source;
alGetSourcef(source, AL_GAIN, &fade.gain0);
fade.duration = duration;
fade.elapsed = 0.0f;
fade.active = true;
if (AudTailLog())
DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms fade-START src="
<< source << " dur=" << duration << "s gain0=" << fade.gain0
<< "\n" << std::flush;
return;
}
}
// table full -- fall back to the instant cut
alSourceStop(source);
alSourceRewind(source);
}
// A restart wants this source back NOW: finalize any fade so the caller
// sees AL_INITIAL with the pre-fade gain restored.
void ReclaimFadingSource(ALuint source)
{
for (int i = 0; i < MAX_RELEASE_FADES; i++)
{
if (s_releaseFades[i].active && s_releaseFades[i].source == source)
{
FinishFade(s_releaseFades[i]);
return;
}
}
}
}
void PRESET_serviceReleaseFades(float elapsed_seconds)
{
for (int i = 0; i < MAX_RELEASE_FADES; i++)
{
ReleaseFade &fade = s_releaseFades[i];
if (!fade.active)
{
continue;
}
fade.elapsed += elapsed_seconds;
if (fade.elapsed >= fade.duration)
{
FinishFade(fade);
}
else
{
// dB-linear ramp to -96 dB over the authored release time
float attenuation = powf(10.0f, -4.8f * (fade.elapsed / fade.duration));
alSourcef(fade.source, AL_GAIN, fade.gain0 * attenuation);
}
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PatchLevelOfDetail ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#if DEBUG_LEVEL>0
TableOf<PatchLevelOfDetail*, unsigned int>
PatchLevelOfDetail::patchTableSocket(NULL, True);
#endif
//
//#############################################################################
//#############################################################################
//
PatchLevelOfDetail::PatchLevelOfDetail(PlugStream *stream):
AudioLevelOfDetail(stream)
{
MemoryStream_Read(stream, &bankID);
MemoryStream_Read(stream, &patchID);
MemoryStream_Read(stream, &maxMIDIFilterCutoff);
// (task #50) the AWE 4-voice comment applied to layers SHARING a key range;
// full-zone presets legitimately carry up to MAX_PRESET_SAMPLES zones
// (key-splits select at most a few per note).
Warn(GetVoiceCount() > MAX_PRESET_SAMPLES);
#ifdef LAB_ONLY
setupCount = 0;
#endif
//
// Keep table of created patchs to verify that duplicates
// are not created, could move this to tool time
//
#if DEBUG_LEVEL>0
unsigned int index_value = (bankID * 1000) + patchID;
if (patchTableSocket.Find(index_value) != NULL)
{
Dump((int)bankID);
Dump((int)patchID);
}
Verify(patchTableSocket.Find(index_value) == NULL);
patchTableSocket.AddValue(this, index_value);
#endif
}
//
//#############################################################################
//#############################################################################
//
void
PatchLevelOfDetail::BuildFromPage(
PlugStream *stream,
NameList *name_list,
ClassID class_ID,
ObjectID object_ID
)
{
AudioLevelOfDetail::BuildFromPage(stream, name_list, class_ID, object_ID);
//
// Store fields
//
MEM_STRM_WRITE_ENTRY(*stream, name_list, SBKBankID, bank_ID);
MEM_STRM_WRITE_ENTRY(*stream, name_list, SBKPatchID, patch_ID);
MEM_STRM_WRITE_ENTRY(*stream, name_list, MIDINRPNValue, max_filter_cutoff);
}
//
//#############################################################################
//#############################################################################
//
PatchLevelOfDetail::~PatchLevelOfDetail()
{
#ifdef LAB_ONLY
cout << "PatchLevelOfDetail::~PatchLevelOfDetail\t";
cout << (int)bankID << ":" << (int)patchID << "\t";
cout << "setupCount=\t" << setupCount << "\n";
#endif
/*if (m_numSources > 0 && m_sources != NULL)
{
for (int i=0; i < m_numSources; i++)
{
//Detach buffer
alSourcei(m_sources[i],AL_BUFFER,0);
//Stop source
ALenum state;
alGetSourcei(m_sources[i],AL_SOURCE_STATE,&state);
if (state == AL_PLAYING)
{
alSourceStop(m_sources[i]);
}
}
//Destroy all sources
alDeleteSources(m_numSources,m_sources);
}*/
}
//
//#############################################################################
//#############################################################################
//
Logical
PatchLevelOfDetail::TestInstance() const
{
AudioLevelOfDetail::TestInstance();
return True;
}
//
//#############################################################################
//#############################################################################
//
void
PatchLevelOfDetail::SetupPatch(SourceSet sourceSet, int note)
{
// Check(this);
//// Check(channel);
//
// #ifdef LAB_ONLY
// setupCount++;
// #endif
SAMPLEINFO info;
// (task #50, AUDIO_FIDELITY F1) the authored MIDI note SELECTS zones:
// attach only samples whose [keyLo,keyHi] contains the note (key-splits
// pick one band, layers attach together); detach the rest so a rewound
// source can't replay a stale buffer from a previous note.
for (int i=0; i < sourceSet.count; i++)
{
info = PRESET_getSampleInfo(bankID,patchID,i);
if (info.bufferIndex >= 0)
{
ReclaimFadingSource(sourceSet.sources[i]);
ALenum sourceState;
alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &sourceState);
bool zone_matches = (note >= info.keyLo && note <= info.keyHi);
if (getenv("BT_AUDIO_SPATIAL")) { static int s_se=0; if (s_se++<200)
DEBUG_STREAM << "[audio] SetupPatch ENTRY bank=" << (int)bankID << " patch=" << (int)patchID << " src=" << sourceSet.sources[i]
<< " file=" << (info.file?info.file:"?") << " note=" << note << (zone_matches?"":" ZONESKIP")
<< " state=" << sourceState << " (INITIAL=" << AL_INITIAL << ")" << "\n" << std::flush; }
if (sourceState == AL_INITIAL)
{
alSourcei(sourceSet.sources[i],AL_BUFFER,
zone_matches ? AL_getBuffer(info.bufferIndex) : 0);
{ static int s_a=0; if (getenv("BT_AUDIO_LOG") && s_a++<24) DEBUG_STREAM << "[audio] SetupPatch src=" << sourceSet.sources[i] << " file=" << (info.file?info.file:"?") << " loop=" << (info.loop==ForceStatic?0:1) << "\n" << std::flush; }
alSourcei(sourceSet.sources[i],AL_SOURCE_RELATIVE,AL_TRUE);
AudioRenderer *render = application->GetAudioRenderer();
AudioHead *head = render->GetAudioHead();
// (F1) authored stereo pairs: pan hard-left/right zones via a
// listener-relative offset (the distance model is AL_NONE, so
// this affects direction only, never gain).
float pan_x =
(info.chan == CHANNEL_LEFT) ? -0.5f :
(info.chan == CHANNEL_RIGHT) ? 0.5f : 0.0f;
alSource3f(sourceSet.sources[i],AL_POSITION,pan_x,0,0);
alSource3f(sourceSet.sources[i],AL_VELOCITY,0,0,0);
if (info.loop == ForceStatic)
{
alSourcei(sourceSet.sources[i],AL_LOOPING,0);
} else
{
alSourcei(sourceSet.sources[i],AL_LOOPING,1);
}
}
}
}
//
// Set the channel to the bank and program, reset patch controls
//
/* channel->SelectBank(bankID);
channel->SendProgramChange(patchID);
channel->SendController(MIDI_CONTROL_RESET, MIDI_MAX_CONTROL_VALUE);*/
//TODO: Sync up OpenAL source info appropriately
/*#if 0
Tell((int)bankID << ":" << (int)patchID << "\n");
#endif*/
}
void PatchLevelOfDetail::PlayNote(SourceSet sourceSet, int note)
{
if (sourceSet.count > 0 && sourceSet.sources != NULL)
{
alGetError();
//DEBUG_STREAM << "Playing bank " << (int)bankID << ", patch " << (int)patchID << std::endl;
for (int i = 0; i < sourceSet.count; i++)
{
// (F1) play only the zones SetupPatch attached for this note
ALint attached = 0;
alGetSourcei(sourceSet.sources[i], AL_BUFFER, &attached);
if (attached == 0)
{
continue;
}
ALenum state;
alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state);
if (state != AL_PLAYING)
{
alSourcePlay(sourceSet.sources[i]);
{ static int s_p=0; if (getenv("BT_AUDIO_LOG") && s_p++<30) DEBUG_STREAM << "[audio] PlayNote alSourcePlay src=" << sourceSet.sources[i] << " note=" << note << "\n" << std::flush; }
if (AudTailLog())
{
SAMPLEINFO zinfo = PRESET_getSampleInfo(bankID, patchID, i);
DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms PLAY src="
<< sourceSet.sources[i] << " bank=" << (int)bankID << " patch="
<< (int)patchID << " file=" << (zinfo.file ? zinfo.file : "?")
<< " loop=" << (int)(zinfo.loop == LoopAtWill)
<< " rel=" << zinfo.releaseSec << "s\n" << std::flush;
}
}
ALenum error = alGetError();
if (error != AL_NO_ERROR)
{
DEBUG_STREAM << "Playing hit an error: " << error << std::endl;
return;
}
}
}
}
void PatchLevelOfDetail::StopNote(SourceSet sourceSet)
{
if (getenv("BT_AUDIO_LOG")) { static int s_s=0; if (s_s++<400) { DEBUG_STREAM << "[audio] StopNote"; for (int _i=0;_i<sourceSet.count;_i++) DEBUG_STREAM << " src=" << sourceSet.sources[_i]; DEBUG_STREAM << "\n" << std::flush; } }
if (sourceSet.count >0 && sourceSet.sources != NULL)
{
// (task #50, AUDIO_FIDELITY F13) honor the authored releaseVolEnv:
// a playing zone with a release time fades out dB-linearly instead of
// cutting; everything else stops instantly (faithful for one-shots).
for (int i = 0; i < sourceSet.count; i++)
{
SAMPLEINFO info = PRESET_getSampleInfo(bankID,patchID,i);
ALenum state;
alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state);
if (AudTailLog() && state == AL_PLAYING)
DEBUG_STREAM << "[aud-tail] t=" << (long)clock() << "ms STOP src="
<< sourceSet.sources[i] << " bank=" << (int)bankID << " patch="
<< (int)patchID << " file=" << (info.file ? info.file : "?")
<< " rel=" << info.releaseSec << "s -> "
<< ((info.releaseSec > 0.05f) ? "fade" : "cut") << "\n" << std::flush;
if (state == AL_PLAYING && info.releaseSec > 0.05f)
{
StartReleaseFade(sourceSet.sources[i], info.releaseSec);
}
else
{
alSourceStop(sourceSet.sources[i]);
alSourceRewind(sourceSet.sources[i]);
}
}
}
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PatchResource ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
//#############################################################################
//#############################################################################
//
PatchResource::PatchResource(PlugStream *stream):
AudioResource(stream)
{
}
//
//#############################################################################
//#############################################################################
//
void
PatchResource::BuildFromPage(
PlugStream *stream,
NameList *name_list,
ClassID class_ID,
ObjectID object_ID
)
{
AudioResource::BuildFromPage(stream, name_list, class_ID, object_ID);
}
//
//#############################################################################
//#############################################################################
//
PatchResource::~PatchResource()
{
}
//
//#############################################################################
//#############################################################################
//
Logical
PatchResource::TestInstance() const
{
AudioResource::TestInstance();
return True;
}
//
//#############################################################################
//#############################################################################
//
void
PatchResource::SetupPatch(SourceSet sourceSet, int note)
{
Check(this);
// Check(channel);
PatchLevelOfDetail *patch_level_of_detail =
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
Check(patch_level_of_detail);
patch_level_of_detail->SetupPatch(sourceSet, note);
}
void PatchResource::PlayNote(SourceSet sourceSet, int note)
{
Check(this);
// Check(channel);
PatchLevelOfDetail *patch_level_of_detail =
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
Check(patch_level_of_detail);
patch_level_of_detail->PlayNote(sourceSet, note);
}
void PatchResource::StopNote(SourceSet sourceSet)
{
Check(this);
// Check(channel);
PatchLevelOfDetail *patch_level_of_detail =
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
Check(patch_level_of_detail);
patch_level_of_detail->StopNote(sourceSet);
}
//
//#############################################################################
//#############################################################################
//
int PatchResource::GetBankID()
{
PatchLevelOfDetail *patch_level_of_detail =
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
return (patch_level_of_detail != 0) ? patch_level_of_detail->GetBankID() : 0;
}
int PatchResource::GetPatchID()
{
PatchLevelOfDetail *patch_level_of_detail =
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
return (patch_level_of_detail != 0) ? patch_level_of_detail->GetPatchID() : 0;
}
//
//#############################################################################
//#############################################################################
//
MIDINRPNValue
PatchResource::GetMaxMIDIFilterCutoff()
{
Check(this);
PatchLevelOfDetail *patch_level_of_detail =
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
Check(patch_level_of_detail);
return patch_level_of_detail->GetMaxMIDIFilterCutoff();
}