Merge BT411 audio-fidelity + combat work (4e72f0c..abed41e) into BT412

Brings the post-fork BT411 line forward via a local-path merge (never
touches the BT411 gitea remote): the full audio-fidelity system (engine
AUD*/L4AUD* + audiopresets.cpp + ~600 content wavs + AUDIO_FIDELITY.md),
missiles/rear-fire/HUD/gyro/gait tasks (#66-68), FOGDAY.EGG, and
refreshed context docs -- 91 commits, ~688 files clean.

Only 5 files overlapped the steamification; resolved keeping BOTH:
- L4NET.CPP: took BT411's task-#50 fix (don't close the game listener on
  console loss) over the seam's adaptation of the old buggy close; sends
  stay on NetTransport_Get().
- L4NETTRANSPORT.cpp: folded BT411's TCP_NODELAY latency fix into
  WinsockNetTransport::Connect (the seam already had retry + nonblocking).
- mechmppr.cpp: combined the device_owns_input gating with BT411's
  task-#68 look-behind, gating the lookBehind write too.
- .gitignore / CMakeLists.txt / mech4.cpp: trivial / auto-merged
  (deviceOwnsInput gating preserved).

Verified: clean build (default + implicitly the Steam TU untouched);
solo front-end mode; loopback MP through the seam (mesh completes, both
tick, replication works, no NODELAY warnings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-16 19:34:54 -05:00
co-authored by Claude Fable 5
696 changed files with 6416 additions and 422 deletions
+33
View File
@@ -1,3 +1,4 @@
#include <cstdlib>
#include "munga.h"
#pragma hdrstop
@@ -290,6 +291,11 @@ AudioControlSend::AudioControlSend(
MemoryStream_Read(stream, &control_ID);
MemoryStream_Read(stream, &control_value);
if (getenv("BT_ATTRBIND_LOG")) { static int s_sd=0; if (s_sd++<80)
DEBUG_STREAM << "[sendcfg] tgt=" << (void*)audio_component
<< " ctl=" << (int)control_ID << "/" << control_value
<< " entity=" << (void*)entity << "\n" << std::flush; }
Check(entity);
entity->AddAudioComponent(this);
@@ -472,6 +478,9 @@ void
while ((audio_component = iterator.ReadAndNext()) != NULL)
{
Check(audio_component);
if (getenv("BT_AUDIO_SPATIAL")) { static int s_sp2=0; if (s_sp2++<200)
DEBUG_STREAM << "[split] " << (void*)this << " -> tgt=" << (void*)audio_component
<< " ctl=" << (int)control_ID << "/" << control_value << "\n" << std::flush; }
audio_component->ReceiveControl(control_ID, control_value);
}
}
@@ -631,6 +640,10 @@ void
}
Check(audioComponentSocket.GetCurrent());
if (getenv("BT_AUDIO_SPATIAL")) { static int s_mix=0; if (s_mix++<80)
DEBUG_STREAM << "[mix] mixer=" << (void*)this << " -> tgt=" << (void*)audioComponentSocket.GetCurrent()
<< " outCtl=" << (int)outputControlID << " in[" << index << "]=" << control_value
<< " sum=" << mixed_value << "\n" << std::flush; }
audioComponentSocket.GetCurrent()->ReceiveControl(
outputControlID,
mixed_value
@@ -793,6 +806,9 @@ void
}
Check(audioComponentSocket.GetCurrent());
if (getenv("BT_AUDIO_SPATIAL") && mult_value <= 0.0f) { static int s_mx=0; if (s_mx++<300)
DEBUG_STREAM << "[mult0] mult=" << (void*)this << " -> tgt=" << (void*)audioComponentSocket.GetCurrent()
<< " in[" << index << "]=" << control_value << " product=" << mult_value << "\n" << std::flush; }
audioComponentSocket.GetCurrent()->ReceiveControl(
outputControlID,
mult_value
@@ -840,6 +856,11 @@ AudioControlSmoother::AudioControlSmoother(
MemoryStream_Read(stream, &initial_fill_value);
audioControlAverage.SetSize(number_of_samples, initial_fill_value);
if (getenv("BT_ATTRBIND_LOG")) { static int s_sm=0; if (s_sm++<40)
DEBUG_STREAM << "[smoothcfg] this=" << (void*)this << " ctlID=" << (int)control_ID
<< " samples=" << (int)number_of_samples << " fill=" << initial_fill_value
<< "\n" << std::flush; }
AudioControlSmootherX(
audio_component,
entity,
@@ -939,6 +960,14 @@ void
{
audioControlAverage.Add(control_value);
if (getenv("BT_AUDIO_SPATIAL")) { static int s_sa=0;
if ((controlID == 100 || controlID == 101) && s_sa++ < 3000)
DEBUG_STREAM << "[smooth] t=" << (GetTickCount() % 1000000)
<< " this=" << (void*)this << " ctl=" << (int)controlID
<< " in=" << control_value
<< " avg=" << audioControlAverage.CalculateOlympicAverage()
<< "\n" << std::flush; }
Check(audioComponentSocket.GetCurrent());
audioComponentSocket.GetCurrent()->ReceiveControl(
controlID,
@@ -1315,6 +1344,10 @@ void
nextSampleTime += sampleDuration;
Check(audioComponentSocket.GetCurrent());
if (getenv("BT_AUDIO_SPATIAL")) { static int s_sh=0; if (s_sh++<60)
DEBUG_STREAM << "[snh] this=" << (void*)this << " -> tgt=" << (void*)audioComponentSocket.GetCurrent()
<< " ctlID=" << (int)audioControlID << " val=" << currentValue
<< " range=[" << minValue << "," << maxValue << "]\n" << std::flush; }
audioComponentSocket.GetCurrent()->ReceiveControl(
audioControlID,
currentValue
+12
View File
@@ -232,6 +232,12 @@ class AudioControlMixer:
public AudioComponent
{
public:
// DIAG/game-facing: expose the authored output control so game-side control
// broadcasts (the foot-plant intensity send) can target only volume/brightness
// mix stages and never Start-outputting stages.
AudioControlID GetOutputControlID() const { return outputControlID; }
AudioComponent *GetTargetComponent() { return audioComponentSocket.GetCurrent(); }
//
//--------------------------------------------------------------------
// Construction, Destruction, Testing
@@ -307,6 +313,12 @@ class AudioControlMultiplier:
public AudioComponent
{
public:
// DIAG/game-facing: expose the authored output control so game-side control
// broadcasts (the foot-plant intensity send) can target only volume/brightness
// mix stages and never Start-outputting stages.
AudioControlID GetOutputControlID() const { return outputControlID; }
AudioComponent *GetTargetComponent() { return audioComponentSocket.GetCurrent(); }
//
//--------------------------------------------------------------------
// Construction, Destruction, Testing
+91 -3
View File
@@ -1,3 +1,4 @@
#include <cstdlib>
#include "munga.h"
#pragma hdrstop
@@ -94,8 +95,15 @@ void
}
headEntitySocket.Add(entity);
alDistanceModel(AL_LINEAR_DISTANCE);
alDopplerFactor(0.3f);
// FIDELITY (AUDIO_FIDELITY.md F3/F10): the engine computes the AUTHORED
// distance-attenuation curve (AUDIO.INI amplitude_rolloff knee/exponent ->
// AudioLocation::distanceVolumeScale) and the authored doppler-cents model.
// Disable OpenAL's own models so they can't double-apply / fight them:
// AL_LINEAR_DISTANCE made far battle audio fade to zero on a straight line
// (-6 dB vs authored at 300u), and AL doppler ran with the wrong constants
// AND a sign-inverted velocity feed (approaching sources pitched DOWN).
alDistanceModel(AL_NONE);
alDopplerFactor(0.0f);
#if 0
//
@@ -131,8 +139,39 @@ void
//
// Increment frame counter
//
audioFrameCount = Now().ticks;
// AUDIO CLOCK CALIBRATION FIX: AudioTime consumers (sequence event timing,
// compression curves) assume audioFrameCount advances at the renderer's
// calibrationRate (DefaultRendererRate = 30 frames/sec:
// Seconds_To_Frames = s * rate). The WinTesla port set raw Now().ticks here
// (~hundreds/sec), running sequences ~18x off the authored timing -- the
// AudioControlSequence events carrying the real footstep volumes never
// landed where authored. Convert ticks -> calibrated frames properly.
{
double tps = (double)SystemClock::GetTicksPerSecond();
if (tps <= 0.0) tps = 1000.0; // GetTickCount ms fallback (static not yet measured)
audioFrameCount = (AudioFrameCount)(
(double)Now().ticks
* (double)application->GetAudioRenderer()->GetCalibrationRate()
/ tps);
}
Verify(audioFrameCount < LONG_MAX);
if (getenv("BT_AUDIO_SPATIAL")) { static long s_hx=0; if ((++s_hx % 300)==0)
DEBUG_STREAM << "[audioclock] frame=" << audioFrameCount << " execs=" << s_hx << "\n" << std::flush; }
// (task #50, AUDIO_FIDELITY F13) service the authored release fades
// registered by PatchLevelOfDetail::StopNote (dB-linear note-off ramps).
{
extern void PRESET_serviceReleaseFades(float elapsed_seconds);
static long s_lastFadeTicks = 0;
long now_ticks = Now().ticks;
if (s_lastFadeTicks != 0 && now_ticks > s_lastFadeTicks)
{
double tps = (double)SystemClock::GetTicksPerSecond();
if (tps <= 0.0) tps = 1000.0;
PRESET_serviceReleaseFades((float)((now_ticks - s_lastFadeTicks) / tps));
}
s_lastFadeTicks = now_ticks;
}
//set current listener orientation
Vector3D headVelocity;
@@ -140,6 +179,32 @@ void
headVelocity.MultiplyByInverse(this->GetHeadEntity()->GetWorldLinearVelocity(), this->GetHeadEntity()->localToWorld);
alListener3f(AL_VELOCITY, -headVelocity.x, -headVelocity.y, -headVelocity.z);
// LIVE-PLAYING DUMP (BT_AUDIO_DUMP): once a second list every playing AL
// source with its sample name / gain / pitch / loop -- catches "mystery
// sounds" (wrong sample, wrong pitch, chopped) red-handed.
if (getenv("BT_AUDIO_DUMP")) {
static long s_dumpTick = 0;
if ((++s_dumpTick % 45) == 0) {
extern ALuint *g_buffers; extern int g_numBuffers; extern const char *g_bufferNames[512];
for (ALuint sid = 1; sid <= 40; ++sid) {
if (!alIsSource(sid)) continue;
ALint st = 0; alGetSourcei(sid, AL_SOURCE_STATE, &st);
if (st != AL_PLAYING) continue;
ALint buf = 0, looping = 0; ALfloat gain = 0, pitch = 0;
alGetSourcei(sid, AL_BUFFER, &buf);
alGetSourcei(sid, AL_LOOPING, &looping);
alGetSourcef(sid, AL_GAIN, &gain);
alGetSourcef(sid, AL_PITCH, &pitch);
const char *nm = "?";
for (int b = 0; b < g_numBuffers && b < 512; ++b)
if ((ALint)g_buffers[b] == buf) { nm = g_bufferNames[b] ? g_bufferNames[b] : "?"; break; }
DEBUG_STREAM << "[playing] src=" << sid << " " << nm
<< " gain=" << gain << " pitch=" << pitch
<< " loop=" << looping << "\n" << std::flush;
}
}
}
#if 0
//
// Get the entity
@@ -398,6 +463,29 @@ void
}
}
//
//#############################################################################
//#############################################################################
//
void
AudioComponent::PrimeWatchers(int passes)
{
Check(this);
for (int pass = 0; pass < passes; ++pass)
{
ChainIteratorOf<Component*> iterator(&audioWatcherSocket);
Component *component;
while ((component = iterator.ReadAndNext()) != NULL)
{
Check(component);
// recurse gate-free: an AudioComponent child (mixer/smoother/
// splitter) pumps ITS chain; an attribute watcher re-reads +
// re-sends. One pass per level keeps total work = passes x chain.
component->PrimeWatchers(1);
}
}
}
//
//#############################################################################
//#############################################################################
+8
View File
@@ -454,6 +454,14 @@ public:
void
ExecuteWatchers();
// (task #50, AUDIO_FIDELITY F19) gate-free watcher pump for the transient
// cold-start prime: ExecuteWatchers above is frame-gated
// (DefaultAudioFrameDelay), so N calls in one tick collapse to one.
// Recurses through the watcher CHAIN (source <- mixer <- smoother <-
// scale), each hop gate-free.
virtual void
PrimeWatchers(int passes);
void
Execute();
+5
View File
@@ -162,6 +162,11 @@ protected:
AudioLevelOfDetail*
GetAudioLevelOfDetail();
public:
// (task #50) public read of the active LOD -- the game-side footstep
// intensity send identifies footstep sources by their patch bank/id.
AudioLevelOfDetail* PeekAudioLevelOfDetail() { return GetAudioLevelOfDetail(); }
private:
//
//-----------------------------------------------------------------------
+45 -2
View File
@@ -174,6 +174,10 @@ void
if (audio_source->IsAudioSourceClipped(GetAudioHead()))
{
if (getenv("BT_AUDIO_SPATIAL")) { static int s_cl=0; if (s_cl++<40)
DEBUG_STREAM << "[spatial] CLIPPED src=" << (void*)audio_source
<< " headPos=(" << GetAudioHead()->GetHeadEntity()->localOrigin.linearPosition.x
<< "," << GetAudioHead()->GetHeadEntity()->localOrigin.linearPosition.z << ")\n" << std::flush; }
//
// If it is a transient source then ignore request
//
@@ -181,7 +185,7 @@ void
{
#ifdef LAB_ONLY
sourceClippedCount++;
#endif
#endif
return;
}
audio_source_priority = audio_source->GetAudioSourcePriority();
@@ -190,7 +194,25 @@ void
else
{
audio_source_priority = audio_source->GetAudioSourcePriority();
// (task #50, AUDIO_FIDELITY F19) COLD-START PRIME: an idle source's own
// watcher socket only executes at Start attempts, so its authored
// AudioControlSmoothers (footstep volume: N=30/15, fill 0) warmed ONE
// sample per attempt -- and the transient drop gate below (vol < 0.3)
// rejected the first ~25 footfalls (~10-20 s of silent steps) before
// the average could cross the gate. On a Start request, pump the
// source's watchers a full smoother window so the volume chain is
// evaluated at its true steady state; the smoother keeps its authored
// smoothing role for live variation once the source is playing.
if (message->controlID == StartAudioControlID)
{
audio_source->PrimeWatchers(30);
}
audio_source_volume_scale = audio_source->CalculateSourceVolumeScale();
if (getenv("BT_AUDIO_SPATIAL")) { static int s_vs=0; if ((s_vs++ % 120)==0)
DEBUG_STREAM << "[spatial] request src=" << (void*)audio_source
<< " vol=" << audio_source_volume_scale
<< " headPos=(" << GetAudioHead()->GetHeadEntity()->localOrigin.linearPosition.x
<< "," << GetAudioHead()->GetHeadEntity()->localOrigin.linearPosition.z << ")\n" << std::flush; }
}
//
@@ -199,17 +221,38 @@ void
// Then return
//--------------------------------------------------------------------------
//
// DIAG (BT_AUDIO_NODROP): deliver low-volume transient starts anyway (at a
// floor volume) to isolate volume-feed problems from the rest of the chain.
if (getenv("BT_AUDIO_NODROP") &&
message->controlID == StartAudioControlID &&
audio_source->GetAudioRenderType() == TransientAudioRenderType &&
audio_source_volume_scale < LowAudioVolumeThreshold)
{
DEBUG_STREAM << "[spatial] NODROP forcing start src=" << (void*)audio_source
<< " vol=" << audio_source_volume_scale << "\n" << std::flush;
audio_source->ReceiveControl(VolumeAudioControlID, 0.7f);
audio_source_volume_scale = 0.7f;
}
if (
message->controlID == StartAudioControlID &&
audio_source->GetAudioRenderType() == TransientAudioRenderType &&
audio_source_volume_scale < LowAudioVolumeThreshold
)
{
if (getenv("BT_AUDIO_SPATIAL")) { static int s_dr=0; if (s_dr++<40)
DEBUG_STREAM << "[spatial] DROP transient start t=" << (GetTickCount() % 1000000)
<< " src=" << (void*)audio_source
<< " vol=" << audio_source_volume_scale
<< " (below threshold " << LowAudioVolumeThreshold << ")\n" << std::flush; }
#ifdef LAB_ONLY
sourceClippedCount++;
#endif
#endif
return;
}
if (getenv("BT_AUDIO_SPATIAL") && message->controlID == StartAudioControlID) {
static int s_st=0; if (s_st++<40)
DEBUG_STREAM << "[spatial] START request src=" << (void*)audio_source
<< " vol=" << audio_source_volume_scale << "\n" << std::flush; }
//
//--------------------------------------------------------------------------
+17
View File
@@ -1,3 +1,4 @@
#include <cstdlib>
#include "munga.h"
#pragma hdrstop
@@ -67,6 +68,9 @@ void
{
Check(this);
Check(audio_component);
if (getenv("BT_AUDIO_SPATIAL")) { static int s_ev=0; if (s_ev++<200)
DEBUG_STREAM << "[seqev] tgt=" << (void*)audio_component
<< " ctl=" << (int)audioControlID << "/" << audioControlValue << "\n" << std::flush; }
audio_component->ReceiveControl(audioControlID, audioControlValue);
}
@@ -189,6 +193,16 @@ AudioControlSequence::AudioControlSequence(
audioControlEventSocket.Add(audio_control_event);
}
if (getenv("BT_ATTRBIND_LOG")) { static int s_sq=0; if (s_sq++<60) {
DEBUG_STREAM << "[seqcfg] seq=" << (void*)this << " tgt=" << (void*)audio_component
<< " looped=" << (int)is_looped << " div=" << (int)divisions_per_beat
<< " tempo=" << (int)tempo << " events=" << (int)number_of_control_events << " ";
{ SChainIteratorOf<AudioControlEvent*> it(&audioControlEventSocket);
AudioControlEvent *e; int n=0;
while ((e = it.ReadAndNext()) != NULL && n++ < 24)
DEBUG_STREAM << *e; }
DEBUG_STREAM << "\n" << std::flush; } }
AudioControlSequenceX(
audio_component,
entity,
@@ -378,6 +392,9 @@ Logical
void
AudioControlSequence::StartSequence()
{
if (getenv("BT_ATTRBIND_LOG")) { static int s_ss=0; if (s_ss++<40)
DEBUG_STREAM << "[seqstart] seq=" << (void*)this << "\n" << std::flush; }
Check(this);
//
+9
View File
@@ -1,3 +1,4 @@
#include <cstdlib>
#include "munga.h"
#pragma hdrstop
@@ -416,6 +417,12 @@ AudioControlValue
volume_scale;
volume_scale = volumeScale * volumeMixScale * volumeCompressionScale;
if (getenv("BT_AUDIO_SPATIAL") && volume_scale <= 0.0f) {
static int s_z=0; if (s_z++<40)
DEBUG_STREAM << "[spatial] vol=0 breakdown src=" << (void*)this
<< " volumeScale=" << volumeScale
<< " mixScale=" << volumeMixScale
<< " compression=" << volumeCompressionScale << "\n" << std::flush; }
Clamp(volume_scale, MinAudioVolume, MaxAudioVolume);
return volume_scale;
}
@@ -656,6 +663,8 @@ void
Check(this);
Clamp(control_value, MinAudioVolume, MaxAudioVolume);
volumeScale = control_value / (MaxAudioVolume - MinAudioVolume);
if (getenv("BT_AUDIO_SPATIAL")) { static int s_vh=0; if (s_vh++<300)
DEBUG_STREAM << "[volset] src=" << (void*)this << " vol=" << volumeScale << "\n" << std::flush; }
}
//
+46
View File
@@ -1,3 +1,4 @@
#include <cstdlib>
#include "munga.h"
#pragma hdrstop
@@ -134,6 +135,12 @@ AudioMotionTrigger::AudioMotionTrigger(
MemoryStream_Read(stream, &motionType);
MemoryStream_Read(stream, &motionValue);
if (getenv("BT_ATTRBIND_LOG")) { static int s_mt=0; if (s_mt++<40)
DEBUG_STREAM << "[motiontrigcfg] attrPtr=" << (void*)attributePointer
<< " motionType=" << (int)motionType << " (0=linear,1=angular)"
<< " motionValue=" << (int)motionValue << " (0=X,1=Y,2=Z,3=len)"
<< "\n" << std::flush; }
PrimeWatcher();
}
@@ -283,6 +290,12 @@ AudioMotionScale::AudioMotionScale(
MemoryStream_Read(stream, &motionType);
MemoryStream_Read(stream, &motionValue);
if (getenv("BT_ATTRBIND_LOG")) { static int s_ms=0; if (s_ms++<40)
DEBUG_STREAM << "[motionscalecfg] attrPtr=" << (void*)attributePointer
<< " motionType=" << (int)motionType << " (0=linear,1=angular)"
<< " motionValue=" << (int)motionValue << " (0=X,1=Y,2=Z,3=len)"
<< "\n" << std::flush; }
PrimeWatcher();
}
@@ -873,6 +886,31 @@ AudioStateWatcher::AudioStateWatcher(
AudioWatcherOf<StateIndicator>(stream, entity)
{
Check_Pointer(attributePointer);
// BRING-UP GUARD [T3, temporary]: an AudioStateWatcher binds to a StateIndicator
// attribute BY NAME. The Mech's own state indicators (SimulationState, Animation/
// ReplicantAnimationState, CollisionState) are real; but audio also binds state
// attrs on subsystems that are not fully reconstructed yet (GeneratorState,
// CondenserState, Torso MotionState, ...). Those resolve to an unconstructed
// object -- a null/garbage vtable at +0 or a debug-fill (0xCDCDCDCD) watcher chain
// at +0x18 -- and AddAudioWatcher would AV in SChainOf::Add. Skip the register
// (that subsystem's state audio stays silent) until the subsystem is built; this
// is SELF-CLEARING (a real StateIndicator passes). See docs: audio subsystem wave.
// Validate the AUDIO SOCKET at +0x18 (what AddAudioWatcher touches), NOT the
// object's +0 vtable: a real StateIndicator has a vtable at +0, but the binary's
// 0x54 subsystem alarm (GaugeAlarm54) is non-polymorphic there (a raw header) yet
// has a real, constructed SChainOf socket at +0x18. A skip means the socket is
// unconstructed (null / debug-fill 0xCDCDCDCD) -- the inert pad or a subsystem not
// yet reconstructed. Registering there would AV in SChainOf::Add.
{
unsigned chain = *(unsigned*)((char*)attributePointer + 0x18);
if (chain == 0 || chain == 0xCDCDCDCD)
{
if (getenv("BT_AUDIO_LOG"))
DEBUG_STREAM << "[audiostate] skip watcher on unbuilt StateIndicator "
<< attributePointer << " (chain=" << (void*)chain << ")\n" << std::flush;
return;
}
}
Cast_Object(StateIndicator*, attributePointer)->AddAudioWatcher(this);
}
@@ -936,6 +974,14 @@ AudioStateTrigger::AudioStateTrigger(
MemoryStream_Read(stream, &excludeTransition);
MemoryStream_Read(stream, &excludeState);
if (getenv("BT_ATTRBIND_LOG")) { static int s_stc=0; if (s_stc++<120)
DEBUG_STREAM << "[statecfg] attrPtr=" << (void*)attributePointer
<< " comp=" << (void*)audioComponentSocket.GetCurrent()
<< " trigState=" << triggerState << " inv=" << (int)inverseTrigger
<< " ctl=" << (int)controlID << "/" << controlValue
<< " excl=" << (int)excludeTransition << "/" << excludeState
<< "\n" << std::flush; }
PrimeWatcher();
}
+64
View File
@@ -1,4 +1,5 @@
#pragma once
#include <cstdlib>
#include "watcher.h"
#include "audio.h"
@@ -304,6 +305,14 @@ template <class T>
MemoryStream_Read(stream, &controlValueOff);
triggerOn = False;
if (getenv("BT_ATTRBIND_LOG")) { static int s_tc=0; if (s_tc++<80)
DEBUG_STREAM << "[trigcfg] attrPtr=" << (void*)attributePointer << " comp=" << (void*)audioComponentSocket.GetCurrent()
<< " thresh=" << attributeValueThreshold
<< " inverse=" << (int)inverseTrigger
<< " onID=" << (int)controlIDOn << "/" << controlValueOn
<< " offID=" << (int)controlIDOff << "/" << controlValueOff
<< "\n" << std::flush; }
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -357,6 +366,10 @@ template <class T> void
Check_Pointer(attribute_ptr);
Scalar current_value = ExtractInterestingValue(attribute_ptr);
if (getenv("BT_AUDIO_SPATIAL") && current_value > 0.0f) { static int s_tg=0; if (s_tg++<120)
DEBUG_STREAM << "[trigger] attrPtr=" << (void*)attributePointer
<< " val=" << current_value << " thresh=" << attributeValueThreshold
<< " armed=" << (int)!triggerOn << "\n" << std::flush; }
if (triggerOn)
{
if (
@@ -432,6 +445,30 @@ public:
);
~AudioScaleOf();
//
//--------------------------------------------------------------------
// Execute -- scales send EVERY poll (task #50, AUDIO_FIDELITY F19).
//
// The base watcher gate is a BITWISE compare (Motion::operator== is
// memcmp) that existed to skip truly static values. In the original,
// scale-watched attributes (velocities, accelerations, temperatures)
// were noisy physics floats that practically changed every poll, so
// scales streamed per-poll values into their authored consumers -- the
// footstep AudioControlSmoothers (N=30/15, fill 0) are SIZED for that
// cadence. Our reconstruction's math can be deterministic (the gait
// integrator lands on bit-identical derived values during smooth
// acceleration), which froze the gate and starved the smoothers (the
// 10-20 s footstep warm-up). Sending unconditionally restores the
// original's practical behavior; triggers/matchers keep the change
// gate (their semantics are edge-based).
//--------------------------------------------------------------------
//
void
Execute()
{
GrabCurrentValue();
}
//
//--------------------------------------------------------------------
// BuildFromPage
@@ -508,6 +545,13 @@ template <class T>
MemoryStream_Read(stream, &controlValueBoundary1);
MemoryStream_Read(stream, &controlValueBoundary2);
MemoryStream_Read(stream, &exponent);
if (getenv("BT_ATTRBIND_LOG")) { static int s_scf=0; if (s_scf++<160)
DEBUG_STREAM << "[scalecfg] attrPtr=" << (void*)attributePointer
<< " comp=" << (void*)audioComponentSocket.GetCurrent()
<< " ctlID=" << (int)controlID
<< " aB=[" << attributeValueBoundary1 << "," << attributeValueBoundary2 << "]"
<< " cB=[" << controlValueBoundary1 << "," << controlValueBoundary2 << "]\n" << std::flush; }
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -625,6 +669,16 @@ template <class T> void
Check(&audioComponentSocket);
Check(audioComponentSocket.GetCurrent());
if (getenv("BT_AUDIO_SPATIAL") && control_value <= 0.0f) {
static int s_sc=0; if (s_sc++<400)
DEBUG_STREAM << "[spatial] scale->0 attrPtr=" << (void*)attributePointer << " comp=" << (void*)audioComponentSocket.GetCurrent() << " raw=" << current_value << " ctlID=" << (int)controlID << " ctl=" << control_value << " aB=[" << attributeValueBoundary1 << "," << attributeValueBoundary2 << "]" << " cB=[" << controlValueBoundary1 << "," << controlValueBoundary2 << "]\n" << std::flush; }
if (getenv("BT_AUDIO_SPATIAL") && (controlID == 100 || controlID == 101)) {
static int s_fs2=0; if (s_fs2++<2000)
DEBUG_STREAM << "[fsscale] t=" << (GetTickCount() % 1000000)
<< " attrPtr=" << (void*)attributePointer
<< " comp=" << (void*)audioComponentSocket.GetCurrent()
<< " ctl" << (int)controlID << " raw=" << current_value
<< " out=" << control_value << "\n" << std::flush; }
audioComponentSocket.GetCurrent()->ReceiveControl(
controlID,
control_value
@@ -721,6 +775,12 @@ template <class T>
MemoryStream_Read(stream, &attributeMatchValue);
MemoryStream_Read(stream, &controlID);
MemoryStream_Read(stream, &controlValue);
if (getenv("BT_ATTRBIND_LOG")) { static int s_mc=0; if (s_mc++<80)
DEBUG_STREAM << "[matchcfg] attrPtr=" << (void*)attributePointer
<< " match=" << attributeMatchValue
<< " ctlID=" << (int)controlID << " ctlVal=" << controlValue
<< "\n" << std::flush; }
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -767,6 +827,10 @@ template <class T> void
if (current_value == attributeMatchValue)
{
if (getenv("BT_AUDIO_SPATIAL")) { static int s_mf=0; if (s_mf++<40)
DEBUG_STREAM << "[matchfire] attrPtr=" << (void*)attributePointer << " comp=" << (void*)audioComponentSocket.GetCurrent() << " compClass=" << (int)audioComponentSocket.GetCurrent()->GetClassID()
<< " val=" << current_value << " -> ctl " << (int)controlID
<< "/" << controlValue << "\n" << std::flush; }
Check(&audioComponentSocket);
Check(audioComponentSocket.GetCurrent());
#if 1
+11
View File
@@ -11,6 +11,17 @@ public:
virtual void Execute();
// (task #50, AUDIO_FIDELITY F19) gate-free watcher pump for the audio
// transient cold-start prime. Default: run Execute() N times (attribute
// watchers re-read + re-send each pass). AudioComponent overrides it to
// RECURSE through its watcher chain, bypassing the per-component
// audio-frame gate that otherwise collapses the passes to one.
virtual void PrimeWatchers(int passes)
{
for (int pass = 0; pass < passes; ++pass)
Execute();
}
static Derivation *GetClassDerivations();
static SharedData DefaultData;
+60 -8
View File
@@ -11,6 +11,34 @@
#include "line.h"
#include "app.h"
#include "notation.h"
#include <math.h>
//
// EXACT axis-angle rotation composition -- matches the 1995 BT binary's angular
// integrator (FUN_00409f58): build a unit rotation quaternion from the rotation
// VECTOR `rotVec` (angle = |rotVec|, axis = rotVec/angle) as { axis*sin(angle/2),
// cos(angle/2) } and Hamilton-multiply it onto `base`. The dead-reckoner previously
// did `out.Add(base, rotVec)` -- adding a scaled angular-velocity vector to the heading
// quaternion. That is only a small-angle approximation: fine per-frame (tiny angle),
// but over a long replicant dead-reckon gap it DIVERGES (the heading drifts to ~180deg
// then snaps -- the spinning-peer hesitation). This composition is exact for any angle
// and stays on the unit sphere.
//
static void ExactAngularProject(Quaternion &out, const Quaternion &base, const Vector3D &rotVec)
{
const Scalar ang = rotVec.Length();
if (ang > 1.0e-6f)
{
const Scalar h = 0.5f * (Scalar)fmodf((float)ang, 6.2831853f); // half of angle mod 2pi
const Scalar s = (Scalar)(sinf((float)h) / ang); // sin(angle/2)/angle
const Quaternion dq(rotVec.x * s, rotVec.y * s, rotVec.z * s, (Scalar)cosf((float)h));
out.Multiply(base, dq); // base (X) dq
}
else
{
out = base;
}
}
//#############################################################################
//############################### Mover #################################
@@ -395,10 +423,8 @@ Logical
//-------------------------------
//
position_delta.Multiply(updateVelocity.angularMotion, time_slice);
projectedOrigin.angularPosition.Add(
updateOrigin.angularPosition,
position_delta
);
ExactAngularProject(projectedOrigin.angularPosition, // was .Add (diverging vector-add)
updateOrigin.angularPosition, position_delta);
projectedVelocity = updateVelocity;
Check_Fpu();
@@ -460,10 +486,8 @@ Logical
updateVelocity.angularMotion,
time_slice
);
projectedOrigin.angularPosition.Add(
updateOrigin.angularPosition,
position_delta
);
ExactAngularProject(projectedOrigin.angularPosition, // was .Add (diverging vector-add)
updateOrigin.angularPosition, position_delta);
//
//-----------------------------------
@@ -1113,6 +1137,34 @@ BoxedSolid*
return solid;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// The "static world" tail of FindBoxedSolidHitBy on its own: ray-test ONLY the
// zone's static solid tree (world structures -- garages, walls, props), NOT the
// tangible movers/doors. Same tree the mech's walk collides against, so a shot
// designated through this lands on exactly the geometry that blocks the mech.
// FindBoundingBoxHitBy clips line->length to the hit distance (HitByBounded ->
// line->length = enter), so the caller reads the entry point via line->FindEnd.
//
BoxedSolid*
Mover::FindStaticSolidHitBy(
Line *line
)
{
Check(this);
Check(line);
InterestManager *interest_mgr = application->GetInterestManager();
Check(interest_mgr);
InterestZone *zone = interest_mgr->GetInterestZone(interestZoneID);
Check(zone);
BoxedSolidTree* tree = zone->GetCollisionRoot();
Check(tree);
BoxedSolid *result = (BoxedSolid*)tree->FindBoundingBoxHitBy(line);
Check_Fpu();
return result;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
BoxedSolidCollisionList*
+10
View File
@@ -300,6 +300,16 @@ public:
Line *line,
Entity *except_by
);
// STATIC-WORLD-ONLY ray query: the "test against the static world" tail of
// FindBoxedSolidHitBy factored out -- ray-tests ONLY the zone's static solid
// tree (the world structures: garages, walls, props), skipping the tangible
// movers/doors. Used by the weapon boresight pick so a shot lands on the SAME
// static geometry that already blocks the mech's walk (mechs are picked
// separately, with their damage zones/lock). Clips line->length to the entry.
BoxedSolid*
FindStaticSolidHitBy(
Line *line
);
BoxedSolidCollisionList*
CollideCenterOfMotion(
Line *line,
+11
View File
@@ -1,3 +1,4 @@
#include <cstdlib>
#include "munga.h"
#pragma hdrstop
@@ -486,6 +487,14 @@ void
//#############################################################################
// Watcher Support
//
int
Simulation::DebugAudioWatcherCount()
{
SChainIteratorOf<Component*> iterator(audioWatcherSocket);
return (int)iterator.GetSize();
}
void
Simulation::ExecuteWatchers()
{
@@ -496,6 +505,8 @@ void
// Audio
{
SChainIteratorOf<Component*> iterator(audioWatcherSocket);
if (getenv("BT_AUDIO_SPATIAL")) { static int s_ec=0; if ((s_ec++ % 600)==0)
DEBUG_STREAM << "[watchpoll] sim=" << (void*)this << " audioSocket size=" << iterator.GetSize() << "\n" << std::flush; }
while ((watcher = iterator.ReadAndNext()) != NULL)
{
watcher->Execute();
+3
View File
@@ -247,6 +247,9 @@ public:
void
ExecuteWatchers();
int // DEBUG (BT_AUDIO_LOG): how many audio watchers are registered on this sim
DebugAudioWatcherCount();
private:
SChainOf<Component*>
audioWatcherSocket;
+40 -15
View File
@@ -1,3 +1,4 @@
#include <cstdlib>
#include "munga.h"
#pragma hdrstop
@@ -95,21 +96,45 @@ AttributeWatcher::AttributeWatcher(
attributePointer = simulation->GetAttributePointer(attribute_name);
#if DEBUG_LEVEL>0
if (attributePointer == NULL)
{
Dump(attribute_name);
}
#else
if (attributePointer == NULL)
{
DEBUG_STREAM <<
"AttributeWatcher::AttributeWatcher - attribute " <<
attribute_name <<
"\n";
Fail("AttributeWatcher::AttributeWatcher - attribute not found\n");
}
#endif
if (getenv("BT_ATTRBIND_LOG"))
{
extern int g_curAudioWatcherClass;
DEBUG_STREAM << "[attrbind] class=" << g_curAudioWatcherClass
<< " subsys=[" << subsystem_name
<< "] attr=[" << attribute_name << "] -> ptr=" << attributePointer
<< " vtbl=" << (attributePointer ? *(void**)attributePointer : (void*)0)
<< "\n" << std::flush;
}
// BRING-UP GUARD [T3, temporary]: audio references attributes on subsystems that
// are not fully reconstructed yet (GeneratorState/On, CondenserState, ReportLeak,
// Torso SpeedOfTorsoHorizontal/MotionState, Reservoir/Avionics/ControlsMapper ...).
// GetAttributePointer returns NULL for those, and the original engine Fail()s
// (fatal) -- which would abort every audio-enabled run. Redirect NULL to a shared
// inert zero pad so scalar/vector watchers read 0 (that sound stays silent) instead
// of crashing; state watchers on it are skipped by the AudioStateWatcher guard.
// Self-clearing: a registered attribute resolves to its real member. Remove once
// the subsystem attribute tables are reconstructed (the audio subsystem wave).
if (attributePointer == NULL)
{
static char s_missingAttrPad[64] = {0};
// TYPED pad: ConfigureActivePress is the held-configure-button INDEX whose
// authored idle is -1 (none) -- NINE audio triggers gate the configure-mode
// ticker on it with threshold -1 across the configurable subsystems. The
// zero pad read as "button 0 held" and started the ticker at load (the
// eternal 2.7/s chirp). Redirect this attribute to a -1 pad; others keep 0.
static int s_configureIdlePad = -1;
CString configure_name("ConfigureActivePress");
if (attribute_name == configure_name)
attributePointer = &s_configureIdlePad;
else
attributePointer = s_missingAttrPad;
if (getenv("BT_AUDIO_LOG"))
DEBUG_STREAM << "[attrnull] " << subsystem_name << "." << attribute_name
<< " not reconstructed -> "
<< (attributePointer == (void*)&s_configureIdlePad ? "-1 pad" : "inert pad")
<< "\n" << std::flush;
}
Check_Pointer(attributePointer);
#endif
+30 -1
View File
@@ -1,4 +1,5 @@
#pragma once
#include <cstdlib>
#include "cmpnnt.h"
#include "slot.h"
@@ -155,14 +156,18 @@ private:
void
InitializeCurrentValue();
protected:
//
//-----------------------------------------------------------------------
// GrabCurrentValue
// GrabCurrentValue -- protected (task #50): AudioScaleOf's per-poll
// Execute override calls it directly (see AUDWTHR.h).
//-----------------------------------------------------------------------
//
void
GrabCurrentValue();
private:
//
//-----------------------------------------------------------------------
// DumpValue
@@ -262,8 +267,32 @@ template <class T> void
Check(this);
Check_Pointer(attributePointer);
if (getenv("BT_AUDIO_SPATIAL")) { // poll-rate probe: total watcher polls + change events
static long s_polls=0; if ((++s_polls % 2000)==0)
DEBUG_STREAM << "[watchpoll] total polls=" << s_polls << "\n" << std::flush;
extern void *g_btFootStepAddr; // DIAG: uncapped tracer on THE footstep watcher
if ((void*)attributePointer == g_btFootStepAddr) {
static long s_fsp=0;
if ((++s_fsp % 120)==0 || !(currentValue == *(T*)attributePointer))
DEBUG_STREAM << "[fswatch] poll#" << s_fsp << " cur=" << (int)*(int*)&currentValue
<< " mem=" << *(int*)attributePointer << "\n" << std::flush;
}
extern void *g_btAccelAddr; // DIAG: poll-vs-change split on the accel attr
if ((void*)attributePointer == g_btAccelAddr) {
static long s_ap=0, s_ac=0;
int changed = !(currentValue == *(T*)attributePointer);
if (changed) ++s_ac;
if ((++s_ap % 120)==0 || (changed && (s_ac % 30)==0))
DEBUG_STREAM << "[accwatch] t=" << (GetTickCount() % 1000000)
<< " poll#" << s_ap << " changes=" << s_ac
<< " memY=" << ((float*)attributePointer)[1]
<< " curY=" << ((float*)&currentValue)[1] << "\n" << std::flush;
}
}
if (!(currentValue == *(T*)attributePointer))
{
if (getenv("BT_AUDIO_SPATIAL")) { static int s_chg=0; if (s_chg++<60)
DEBUG_STREAM << "[watchpoll] CHANGE attrPtr=" << (void*)attributePointer << "\n" << std::flush; }
#if DEBUG_LEVEL>0
if (dumpValue)
{
+129
View File
@@ -0,0 +1,129 @@
//###########################################################################
//
// L4AUDEFX.cpp -- OpenAL EFX bridge (task #50, AUDIO_FIDELITY F9/F11).
// See L4AUDEFX.h for the fidelity rationale.
//
//###########################################################################
#include <cstdlib>
#include "mungal4.h"
#pragma hdrstop
#include "l4audefx.h"
#include "openal/alc.h"
#include "openal/efx.h"
#ifndef AL_EFFECT_EAXREVERB
#define AL_EFFECT_EAXREVERB 0x8000 // newer efx.h constant; OpenAL Soft supports it
#endif
namespace
{
bool s_available = false;
ALuint s_reverbSlot = 0;
ALuint s_reverbEffect = 0;
ALuint s_scratchFilter = 0;
LPALGENEFFECTS p_alGenEffects = 0;
LPALEFFECTI p_alEffecti = 0;
LPALEFFECTF p_alEffectf = 0;
LPALGENAUXILIARYEFFECTSLOTS p_alGenAuxiliaryEffectSlots = 0;
LPALAUXILIARYEFFECTSLOTI p_alAuxiliaryEffectSloti = 0;
LPALAUXILIARYEFFECTSLOTF p_alAuxiliaryEffectSlotf = 0;
LPALGENFILTERS p_alGenFilters = 0;
LPALFILTERI p_alFilteri = 0;
LPALFILTERF p_alFilterf = 0;
}
bool EFX_Available()
{
return s_available;
}
bool EFX_Initialize(float global_reverb_scale)
{
ALCcontext *context = alcGetCurrentContext();
if (context == 0)
{
return false;
}
ALCdevice *device = alcGetContextsDevice(context);
if (device == 0 || !alcIsExtensionPresent(device, "ALC_EXT_EFX"))
{
if (getenv("BT_AUDIO_LOG"))
DEBUG_STREAM << "[audio] EFX: ALC_EXT_EFX NOT present -- filters/reverb inert\n" << std::flush;
return false;
}
p_alGenEffects = (LPALGENEFFECTS)alGetProcAddress("alGenEffects");
p_alEffecti = (LPALEFFECTI)alGetProcAddress("alEffecti");
p_alEffectf = (LPALEFFECTF)alGetProcAddress("alEffectf");
p_alGenAuxiliaryEffectSlots = (LPALGENAUXILIARYEFFECTSLOTS)alGetProcAddress("alGenAuxiliaryEffectSlots");
p_alAuxiliaryEffectSloti = (LPALAUXILIARYEFFECTSLOTI)alGetProcAddress("alAuxiliaryEffectSloti");
p_alAuxiliaryEffectSlotf = (LPALAUXILIARYEFFECTSLOTF)alGetProcAddress("alAuxiliaryEffectSlotf");
p_alGenFilters = (LPALGENFILTERS)alGetProcAddress("alGenFilters");
p_alFilteri = (LPALFILTERI)alGetProcAddress("alFilteri");
p_alFilterf = (LPALFILTERF)alGetProcAddress("alFilterf");
if (!p_alGenEffects || !p_alEffecti || !p_alEffectf
|| !p_alGenAuxiliaryEffectSlots || !p_alAuxiliaryEffectSloti || !p_alAuxiliaryEffectSlotf
|| !p_alGenFilters || !p_alFilteri || !p_alFilterf)
{
if (getenv("BT_AUDIO_LOG"))
DEBUG_STREAM << "[audio] EFX: entry points missing -- filters/reverb inert\n" << std::flush;
return false;
}
alGetError();
p_alGenAuxiliaryEffectSlots(1, &s_reverbSlot);
p_alGenEffects(1, &s_reverbEffect);
if (alGetError() != AL_NO_ERROR)
{
return false;
}
// EAXReverb where available (OpenAL Soft: yes), plain reverb otherwise.
p_alEffecti(s_reverbEffect, AL_EFFECT_TYPE, AL_EFFECT_EAXREVERB);
if (alGetError() != AL_NO_ERROR)
{
p_alEffecti(s_reverbEffect, AL_EFFECT_TYPE, AL_EFFECT_REVERB);
}
p_alAuxiliaryEffectSloti(s_reverbSlot, AL_EFFECTSLOT_EFFECT, (ALint)s_reverbEffect);
// The authentic wet level: the original sent CC91 = global_reverb_scale
// (0.3 -> value 38) on every 3D channel; one global slot gain reproduces
// the same uniform send.
p_alAuxiliaryEffectSlotf(s_reverbSlot, AL_EFFECTSLOT_GAIN,
(global_reverb_scale < 0.0f) ? 0.0f :
(global_reverb_scale > 1.0f) ? 1.0f : global_reverb_scale);
p_alGenFilters(1, &s_scratchFilter);
p_alFilteri(s_scratchFilter, AL_FILTER_TYPE, AL_FILTER_LOWPASS);
s_available = (alGetError() == AL_NO_ERROR);
if (getenv("BT_AUDIO_LOG"))
DEBUG_STREAM << "[audio] EFX: " << (s_available ? "READY" : "FAILED")
<< " (reverb slot gain=" << global_reverb_scale << ")\n" << std::flush;
return s_available;
}
void EFX_SetSourceLowpassGainHF(ALuint source, float gainhf)
{
if (!s_available)
{
return;
}
if (gainhf < 0.001f) gainhf = 0.001f;
if (gainhf > 1.0f) gainhf = 1.0f;
// filter params are COPIED at attach, so one scratch filter serves all
p_alFilterf(s_scratchFilter, AL_LOWPASS_GAIN, 1.0f);
p_alFilterf(s_scratchFilter, AL_LOWPASS_GAINHF, gainhf);
alSourcei(source, AL_DIRECT_FILTER, (ALint)s_scratchFilter);
}
void EFX_AttachReverbSend(ALuint source)
{
if (!s_available)
{
return;
}
alSource3i(source, AL_AUXILIARY_SEND_FILTER, (ALint)s_reverbSlot, 0, AL_FILTER_NULL);
}
+44
View File
@@ -0,0 +1,44 @@
#pragma once
//###########################################################################
//
// L4AUDEFX.h -- OpenAL EFX bridge for the authored filter/reverb chains
// (task #50, AUDIO_FIDELITY F9/F11).
//
// The original drove the AWE32 initial-filter-cutoff NRPN per frame
// (brightness x distance HF rolloff) and sent CC91 reverb on the 3D
// channels (global_reverb_scale=0.3, AUDIO.INI) while keeping the cockpit
// Direct channels dry. This bridge reproduces both through OpenAL Soft's
// EFX extension: one EAXReverb aux slot + a scratch AL_FILTER_LOWPASS
// whose parameters are copied at attach time.
//
//###########################################################################
#include "openal/al.h"
// Load the EFX entry points, create the reverb slot (gain = the authentic
// global_reverb_scale) and the scratch lowpass. Call once, with the AL
// context current. Returns false (and stays inert) without ALC_EXT_EFX.
bool EFX_Initialize(float global_reverb_scale);
bool EFX_Available();
// Per-frame direct-path lowpass: gainhf is the linear HF gain at the EFX
// 5 kHz reference. Callers map the AWE cutoff (100 + scale*7900/127 Hz)
// through EFX_CutoffScaleToGainHF below.
void EFX_SetSourceLowpassGainHF(ALuint source, float gainhf);
// AWE NRPN 21 curve -> EFX gainhf: cutoff_scale in [0,1] of the 100-8000 Hz
// span; approximated as the attenuation of a 2-pole lowpass at the 5 kHz
// reference [T3 -- curve shape approximate, endpoints exact].
inline float EFX_CutoffScaleToGainHF(float cutoff_scale)
{
if (cutoff_scale < 0.0f) cutoff_scale = 0.0f;
if (cutoff_scale > 1.0f) cutoff_scale = 1.0f;
float cutoff_hz = 100.0f + cutoff_scale * 7900.0f;
float g = (cutoff_hz / 5000.0f) * (cutoff_hz / 5000.0f);
return (g > 1.0f) ? 1.0f : ((g < 0.001f) ? 0.001f : g);
}
// Wet-exterior routing: attach the source's aux send to the reverb slot
// (Dynamic3D/Static3D). Direct cockpit sources stay dry by default.
void EFX_AttachReverbSend(ALuint source);
+158 -24
View File
@@ -1,14 +1,29 @@
#include <cmath>
#include <cstdlib>
#include "mungal4.h"
#pragma hdrstop
#include "l4audio.h"
#include "l4audlvl.h"
#include "l4audefx.h"
#include "l4app.h"
#include "l4audrnd.h"
#include "..\munga\namelist.h"
#include "..\munga\player.h"
#include "..\rp\vtv.h"
// NOTE -> PITCH (task #50): the AWE32 played every patch at the requested MIDI
// note relative to the sample root (60); sequences author notes (klaxon/ambience
// rhythms are PITCHED patterns). The port captured noteValue but never applied
// any pitch to the AL sources -- everything played at root (the "chirping"
// ambience: an authored deep tick at note 51 sounded as a high click). Apply
// pitch = 2^((note-60)/12) x the control-chain cents offset at start + per frame.
static inline float BTNotePitchFactor(int note_value)
{
return (float)pow(2.0, ((double)note_value - 60.0) / 12.0);
}
//#############################################################################
//####################### L4AudioSpatialization #########################
//#############################################################################
@@ -921,7 +936,13 @@ void
patch_resource = Cast_Object(PatchResource*, GetAudioResource());
Check(patch_resource);
patch_resource->SetDistance(GetDistanceToSource());
patch_resource->SetupPatch(channelSet);
patch_resource->SetupPatch(channelSet, (int)GetCurrentNoteValue());
{ // apply the requested note's pitch to the freshly attached sources
float note_pitch = BTNotePitchFactor((int)GetCurrentNoteValue());
for (int _i = 0; _i < channelSet.count; ++_i)
alSourcef(channelSet.sources[_i], AL_PITCH, note_pitch);
}
//
// Set the channel to default control values
@@ -944,6 +965,31 @@ void
channel->SendController(MIDI_PAN_CONTROL, MIDI_RIGHT_PAN_VALUE);
break;
}*/
// (task #50, AUDIO_FIDELITY F12) the original routed each Direct cockpit
// source by its authored 6-value position enum (front/rear CARD + pan
// CC10 left/center/right). Reproduce as a listener-relative placement;
// a zone's own L/R pan (stereo-pair presets) offsets on top. The
// distance model is AL_NONE, so this affects direction only, never gain.
{
float px = 0.0f, pz = -1.0f; // Front, centered
switch (audioPosition)
{
case FrontDirectPatchPosition: px = 0.0f; pz = -1.0f; break;
case RearDirectPatchPosition: px = 0.0f; pz = 1.0f; break;
case FrontLeftDirectPatchPosition: px = -0.7f; pz = -0.7f; break;
case FrontRightDirectPatchPosition: px = 0.7f; pz = -0.7f; break;
case RearLeftDirectPatchPosition: px = -0.7f; pz = 0.7f; break;
case RearRightDirectPatchPosition: px = 0.7f; pz = 0.7f; break;
}
for (int _i = 0; _i < channelSet.count; ++_i)
{
SAMPLEINFO zinfo = PRESET_getSampleInfo(
patch_resource->GetBankID(), patch_resource->GetPatchID(), _i);
float pan_x = (zinfo.chan == CHANNEL_LEFT) ? -0.5f :
(zinfo.chan == CHANNEL_RIGHT) ? 0.5f : 0.0f;
alSource3f(channelSet.sources[_i], AL_POSITION, px + pan_x, 0.0f, pz);
}
}
//
// Set midi history to default control values
@@ -979,7 +1025,7 @@ void
//
//TODO: Start OpenAL source playing
// channel->SendNoteOn(GetCurrentNoteValue(), midi_velocity);
patch_resource->PlayNote(channelSet);
patch_resource->PlayNote(channelSet, (int)GetCurrentNoteValue());
#if 0
Tell("On " << (int)GetCurrentNoteValue() << "\n");
@@ -1033,6 +1079,11 @@ void
pitch_offset = CalculateSourcePitchOffset();
double relativePitch = pow(2.0,pitch_offset/1200.0);
Clamp(relativePitch,0.5,2.0);
{ // apply the combined control-chain + note pitch per frame
float _np = BTNotePitchFactor((int)GetCurrentNoteValue());
for (int _i = 0; _i < channelSet.count; ++_i)
alSourcef(channelSet.sources[_i], AL_PITCH, (float)relativePitch * _np);
}
//
//--------------------------------------------------------------------------
@@ -1057,6 +1108,13 @@ void
if (Abs(filter_difference) >= filter_resolution)
{
lastMIDIFilterCutoff = midi_filter_cutoff;
// (task #50, AUDIO_FIDELITY F9) the original sent this cutoff as
// AWE NRPN 21; the EFX lowpass is the modern sink (was a dead
// bookkeeping write -- ctl-5 Brightness played full-bright).
float gainhf = EFX_CutoffScaleToGainHF(
(float)lastMIDIFilterCutoff / (float)MIDI_MAX_CONTROL_VALUE);
for (int _i = 0; _i < channelSet.count; ++_i)
EFX_SetSourceLowpassGainHF(channelSet.sources[_i], gainhf);
}
}
@@ -1076,10 +1134,12 @@ void
Check(audio_renderer);
AudioHead *audio_head = audio_renderer->GetAudioHead();
Check(audio_head);
if (getenv("BT_AUDIO_SPATIAL")) { static int s_dp=0; if ((s_dp++ % 120)==0)
DEBUG_STREAM << "[spatial] direct src=" << (channelSet.count>0?channelSet.sources[0]:0)
<< " vol=" << volume_scale << "\n" << std::flush; }
for (int i=0; i < channelSet.count; i++)
{
alSourcef(channelSet.sources[i],AL_MAX_DISTANCE,audio_location->getMaxDistance(audio_head));
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale);
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale * volume_scale); // FIDELITY F4: CC7 squared law (linear was ~+6 dB at mid volumes)
}
}
@@ -1204,7 +1264,13 @@ void
patch_resource = Cast_Object(PatchResource*, GetAudioResource());
Check(patch_resource);
patch_resource->SetDistance(GetDistanceToSource());
patch_resource->SetupPatch(channelSet);
patch_resource->SetupPatch(channelSet, (int)GetCurrentNoteValue());
{ // apply the requested note's pitch to the freshly attached sources
float note_pitch = BTNotePitchFactor((int)GetCurrentNoteValue());
for (int _i = 0; _i < channelSet.count; ++_i)
alSourcef(channelSet.sources[_i], AL_PITCH, note_pitch);
}
/*patch_resource->SetDistance(GetDistanceToSource());
for (i = 0; i < AudioChannelSetSize; i++)
@@ -1227,6 +1293,12 @@ void
channel->SendController(MIDI_REVERB_CONTROL, midi_reverb_level);
channel->SendController(MIDI_CHORUS_CONTROL, MIDI_MIN_CONTROL_VALUE);
}*/
// (task #50, AUDIO_FIDELITY F11) the CC91 send above is the authentic
// wet-exterior contrast (global_reverb_scale on every 3D channel, decomp
// part_008.c:7278-7394); the EFX aux slot is the modern sink. Cockpit
// Direct sources stay dry (their CC91 was 0).
for (int _i = 0; _i < channelSet.count; ++_i)
EFX_AttachReverbSend(channelSet.sources[_i]);
//
// Set the channels to correct pan
@@ -1299,7 +1371,7 @@ void
Check(channel);
channel->SendNoteOn(GetCurrentNoteValue(), midi_velocity);
}*/
patch_resource->PlayNote(channelSet);
patch_resource->PlayNote(channelSet, (int)GetCurrentNoteValue());
}
//
@@ -1405,15 +1477,43 @@ void
pitch_offset = CalculateSourcePitchOffset();
// FIDELITY (AUDIO_FIDELITY.md F10): the AUTHORED doppler (AUDIO.INI
// doppler_range/speed_of_sound -> AudioLocation::dopplerCents, computed each
// spatial update) -- the decomp proves the original added it to the pitch
// cents on THIS dynamic path (part_008.c:7466). AL's own doppler is now
// disabled (it ran with wrong constants and a sign-inverted velocity feed).
pitch_offset += GetAudioLocation()->GetDopplerCents();
double relativePitch = pow(2.0,pitch_offset/1200.0);
Clamp(relativePitch,0.5,2.0);
{ // apply the combined control-chain + note pitch per frame
float _np = BTNotePitchFactor((int)GetCurrentNoteValue());
for (int _i = 0; _i < channelSet.count; ++_i)
alSourcef(channelSet.sources[_i], AL_PITCH, (float)relativePitch * _np);
}
if (getenv("BT_AUDIO_SPATIAL")) { static int s_sp=0; if ((s_sp++ % 120) == 0)
DEBUG_STREAM << "[spatial] dyn src=" << (channelSet.count>0?channelSet.sources[0]:0)
<< " head=(" << headPosition.x << "," << headPosition.z << ")"
<< " loc=(" << locationPosition.x << "," << locationPosition.z << ")"
<< " dist=" << posMag << " vol=" << volume_scale << "\n" << std::flush; }
// (task #50, AUDIO_FIDELITY F9) the original drove the AWE filter cutoff
// from highFreqCutoffScale x brightnessScale on this dynamic path --
// UNGATED, all channels (part_008.c:7496,7589-7604): every moving 3D
// sound gets duller with distance (AUDIO.INI knee 60 / exponent 2.0).
float dyn_gainhf = EFX_CutoffScaleToGainHF(
(float)(GetAudioLocation()->GetHighFreqCutoffScale()
* CalculateSourceBrightnessScale()
* (Scalar)patch_resource->GetMaxMIDIFilterCutoff()
/ (Scalar)MIDI_MAX_CONTROL_VALUE));
for (int i=0; i < channelSet.count; i++)
{
alSource3f(channelSet.sources[i],AL_POSITION,pos.x,pos.y,pos.z);
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale);
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale * volume_scale); // FIDELITY F4: CC7 squared law (linear was ~+6 dB at mid volumes)
alSource3f(channelSet.sources[i],AL_VELOCITY,-relative_velocity.x,-relative_velocity.y,-relative_velocity.z);
alSourcef(channelSet.sources[i],AL_MAX_DISTANCE,audio_location->getMaxDistance(audio_head));
EFX_SetSourceLowpassGainHF(channelSet.sources[i], dyn_gainhf);
}
}
@@ -1432,22 +1532,18 @@ AudioControlValue
//
Scalar
volume_scale = L4AudioSource::CalculateSourceVolumeScale();
return volume_scale;
//
// Update the spatial model that will result in the value
// for distance related volume attenuation
//
/*Check(application);
Check(application->GetAudioRenderer());
UpdateSpatialModel(application->GetAudioRenderer()->GetAudioHead());
//
// Apply distance attenuation to the volume scale
//
// FIDELITY (AUDIO_FIDELITY.md F3): apply the AUTHORED distance attenuation
// (AUDIO.INI knee/rolloff curve, computed into distanceVolumeScale on every
// spatial update). This multiply was commented out and AL_LINEAR_DISTANCE
// substituted -- far sounds faded on a straight line to zero instead of the
// authored 1/(1+(k(d-knee))^2) curve, and the volume-based cull / voice
// steal / ducking chains were distance-blind. AL's model is now AL_NONE.
//
Check(GetAudioLocation());
volume_scale *= GetAudioLocation()->GetDistanceVolumeScale();
return volume_scale;*/
return volume_scale;
}
//#############################################################################
@@ -1637,6 +1733,22 @@ void
// StartRequest(this);
}
//
//#############################################################################
//#############################################################################
//
AudioControlValue
Static3DPatchSource::CalculateSourceVolumeScale()
{
Check(this);
// FIDELITY (AUDIO_FIDELITY.md F3): same authored distance attenuation as the
// dynamic path -- the spatial model computes distanceVolumeScale per execute.
Scalar volume_scale = L4AudioSource::CalculateSourceVolumeScale();
Check(GetAudioLocation());
volume_scale *= GetAudioLocation()->GetDistanceVolumeScale();
return volume_scale;
}
//
//#############################################################################
//#############################################################################
@@ -1693,7 +1805,13 @@ void
patch_resource = Cast_Object(PatchResource*, GetAudioResource());
Check(patch_resource);
patch_resource->SetDistance(GetDistanceToSource());
patch_resource->SetupPatch(channelSet);
patch_resource->SetupPatch(channelSet, (int)GetCurrentNoteValue());
{ // apply the requested note's pitch to the freshly attached sources
float note_pitch = BTNotePitchFactor((int)GetCurrentNoteValue());
for (int _i = 0; _i < channelSet.count; ++_i)
alSourcef(channelSet.sources[_i], AL_PITCH, note_pitch);
}
/*for (i = 0; i < AudioChannelSetSize; i++)
{
if ((channel = channelSet.GetNth(i)) != NULL)
@@ -1718,6 +1836,10 @@ void
channel->SendController(MIDI_CHORUS_CONTROL, MIDI_MIN_CONTROL_VALUE);
}
}*/
// (task #50, AUDIO_FIDELITY F11) static 3D sources are wet like the
// dynamic ones (the CC91 block above); Direct cockpit stays dry.
for (int _i = 0; _i < channelSet.count; ++_i)
EFX_AttachReverbSend(channelSet.sources[_i]);
//
// Set the channels to correct pan
@@ -1850,7 +1972,7 @@ void
channel->SendNoteOn(GetCurrentNoteValue(), midi_velocity);
}
}*/
patch_resource->PlayNote(channelSet);
patch_resource->PlayNote(channelSet, (int)GetCurrentNoteValue());
}
@@ -1921,6 +2043,11 @@ void
pitch_offset = CalculateSourcePitchOffset();
double relativePitch = pow(2.0,pitch_offset/1200.0);
Clamp(relativePitch,0.5,2.0);
{ // apply the combined control-chain + note pitch per frame
float _np = BTNotePitchFactor((int)GetCurrentNoteValue());
for (int _i = 0; _i < channelSet.count; ++_i)
alSourcef(channelSet.sources[_i], AL_PITCH, (float)relativePitch * _np);
}
Vector3D relative_position;
Vector3D relative_velocity;
@@ -1933,12 +2060,19 @@ void
relative_position = audio_location->GetVectorToSource();
}
// (task #50, AUDIO_FIDELITY F9) static sources: brightness-driven cutoff
// only (part_008.c:7831-7884 -- the NRPN block commented below).
float static_gainhf = EFX_CutoffScaleToGainHF(
(float)(CalculateSourceBrightnessScale()
* (Scalar)patch_resource->GetMaxMIDIFilterCutoff()
/ (Scalar)MIDI_MAX_CONTROL_VALUE));
//Static models have their position freely available as relative positions and stand still
for (int i=0; i < channelSet.count; i++)
{
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale);
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale * volume_scale); // FIDELITY F4: CC7 squared law (linear was ~+6 dB at mid volumes)
alSource3f(channelSet.sources[i],AL_POSITION,relative_position.x,relative_position.y,relative_position.z);
alSourcef(channelSet.sources[i],AL_MAX_DISTANCE,audio_location->getMaxDistance(audio_head));
EFX_SetSourceLowpassGainHF(channelSet.sources[i], static_gainhf);
}
//
+4
View File
@@ -552,6 +552,10 @@ public:
// State implementations
//
public:
// FIDELITY (AUDIO_FIDELITY.md F3): statics get the authored distance curve too
AudioControlValue
CalculateSourceVolumeScale();
void
StartImplementation();
void
+174 -12
View File
@@ -1,3 +1,4 @@
#include <cstdlib>
#include "mungal4.h"
#pragma hdrstop
@@ -8,6 +9,100 @@
#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.
//
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)
{
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;
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
@@ -26,7 +121,10 @@ PatchLevelOfDetail::PatchLevelOfDetail(PlugStream *stream):
MemoryStream_Read(stream, &patchID);
MemoryStream_Read(stream, &maxMIDIFilterCutoff);
Warn(GetVoiceCount() > 4); // HACK - AWE appears to only play 1st 4 voices
// (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;
@@ -118,7 +216,7 @@ Logical
//#############################################################################
//
void
PatchLevelOfDetail::SetupPatch(SourceSet sourceSet)
PatchLevelOfDetail::SetupPatch(SourceSet sourceSet, int note)
{
// Check(this);
//// Check(channel);
@@ -128,24 +226,44 @@ void
// #endif
SAMPLEINFO info;
//Attach buffers
// (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,AL_getBuffer(info.bufferIndex));
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();
alSource3f(sourceSet.sources[i],AL_POSITION,0,0,0);
// (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)
@@ -173,7 +291,7 @@ void
#endif*/
}
void PatchLevelOfDetail::PlayNote(SourceSet sourceSet)
void PatchLevelOfDetail::PlayNote(SourceSet sourceSet, int note)
{
if (sourceSet.count > 0 && sourceSet.sources != NULL)
{
@@ -182,12 +300,21 @@ void PatchLevelOfDetail::PlayNote(SourceSet sourceSet)
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; }
}
ALenum error = alGetError();
@@ -202,10 +329,27 @@ void PatchLevelOfDetail::PlayNote(SourceSet sourceSet)
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)
{
alSourceStopv(sourceSet.count, sourceSet.sources);
alSourceRewindv(sourceSet.count, sourceSet.sources);
// (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 (state == AL_PLAYING && info.releaseSec > 0.05f)
{
StartReleaseFade(sourceSet.sources[i], info.releaseSec);
}
else
{
alSourceStop(sourceSet.sources[i]);
alSourceRewind(sourceSet.sources[i]);
}
}
}
}
@@ -259,7 +403,7 @@ Logical
//#############################################################################
//
void
PatchResource::SetupPatch(SourceSet sourceSet)
PatchResource::SetupPatch(SourceSet sourceSet, int note)
{
Check(this);
// Check(channel);
@@ -268,10 +412,10 @@ void
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
Check(patch_level_of_detail);
patch_level_of_detail->SetupPatch(sourceSet);
patch_level_of_detail->SetupPatch(sourceSet, note);
}
void PatchResource::PlayNote(SourceSet sourceSet)
void PatchResource::PlayNote(SourceSet sourceSet, int note)
{
Check(this);
// Check(channel);
@@ -280,7 +424,7 @@ void PatchResource::PlayNote(SourceSet sourceSet)
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
Check(patch_level_of_detail);
patch_level_of_detail->PlayNote(sourceSet);
patch_level_of_detail->PlayNote(sourceSet, note);
}
void PatchResource::StopNote(SourceSet sourceSet)
@@ -295,6 +439,24 @@ void PatchResource::StopNote(SourceSet sourceSet)
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;
}
//
//#############################################################################
//#############################################################################
+32 -6
View File
@@ -26,22 +26,38 @@ struct SAMPLEINFO
const char *file;
SampleChannel chan;
SampleLoop loop;
// (task #50, AUDIO_FIDELITY F1/F13) full-zone metadata from the SF2 banks:
// the authored MIDI note SELECTS zones by [keyLo,keyHi]; loop regions are
// sub-ranges in sample frames (AL_SOFT_loop_points); releaseSec is the
// authored releaseVolEnv fade applied on Stop instead of an instant cut.
int keyLo;
int keyHi;
int loopStart;
int loopEnd;
float releaseSec;
};
// AllExplosion (bank2 p125) authors 25 layered zones -- the largest preset.
const int MAX_PRESET_SAMPLES = 25;
struct PRESETINFO
{
int sampleNum;
SAMPLEINFO samples[5];
SAMPLEINFO samples[MAX_PRESET_SAMPLES];
bool is3d;
};
extern PRESETINFO allPresets[2][100];
extern PRESETINFO allPresets[2][128];
bool PRESET_isImplemented(int bank, int preset);
int PRESET_getNumSamples(int bank, int preset);
SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd);
void PRESET_setBufferIndex(int bank, int preset, int sampleInd, int index);
// (task #50, AUDIO_FIDELITY F13) authored release fades: StopNote registers a
// dB-linear gain ramp instead of cutting; AudioHead::Execute services them.
void PRESET_serviceReleaseFades(float elapsed_seconds);
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PatchLevelOfDetail ~~~~~~~~~~~~~~~~~~~~~~~~~~
typedef MIDIValue SBKPatchID;
@@ -59,7 +75,7 @@ public:
PatchLevelOfDetail(PlugStream *stream);
~PatchLevelOfDetail();
void PlayNote(SourceSet sourceSet);
void PlayNote(SourceSet sourceSet, int note);
void StopNote(SourceSet sourceSet);
Logical
@@ -69,6 +85,11 @@ public:
GetVoiceCount()
{return PRESET_getNumSamples(bankID,patchID);}
// (task #50) expose the authored bank/patch so the game-side footstep
// intensity send can identify footstep sources precisely.
int GetBankID() const { return (int)bankID; }
int GetPatchID() const { return (int)patchID; }
//
//-----------------------------------------------------------------------
// BuildFromPage
@@ -88,7 +109,7 @@ public:
//-----------------------------------------------------------------------
//
void
SetupPatch(SourceSet sourceSet);
SetupPatch(SourceSet sourceSet, int note);
MIDINRPNValue
GetMaxMIDIFilterCutoff()
@@ -136,7 +157,7 @@ public:
PatchResource(PlugStream *stream);
~PatchResource();
void PlayNote(SourceSet sourceSet);
void PlayNote(SourceSet sourceSet, int note);
void StopNote(SourceSet sourceSet);
Logical
@@ -161,9 +182,14 @@ public:
//-----------------------------------------------------------------------
//
void
SetupPatch(SourceSet sourceSet);
SetupPatch(SourceSet sourceSet, int note);
MIDINRPNValue
GetMaxMIDIFilterCutoff();
// (task #50) authored bank/patch pass-throughs (the LOD accessor is
// protected on AudioResource) -- zone metadata lookups at the source layer.
int GetBankID();
int GetPatchID();
};
+132 -64
View File
@@ -1,3 +1,7 @@
#include <stdio.h>
#include <direct.h>
#include <string.h>
#include <cstdlib>
#include "mungal4.h"
#pragma hdrstop
@@ -13,10 +17,66 @@
#include "..\munga\app.h"
#include "..\munga\notation.h"
#include "openal\al.h"
#include "sndfile.h"
#include "openal\alc.h"
ALuint *g_buffers;
int g_numBuffers;
const char *g_bufferNames[512]; // bufferInd -> sample file (diag: the live-playing dump)
int g_curAudioWatcherClass = 0;
//
// Minimal canonical-PCM WAV reader. The repo's libsndfile-1.dll is a STUB
// (exports 15 funcs by ordinal only, no names -- sf_open always returns NULL
// and sf_strerror(NULL) faults), so the original sf_open path loaded nothing.
// Our soundbank samples (AUDIO1/2.RES, cracked by scratchpad/sf2extract.py)
// are written as plain 16-bit mono PCM WAVs, so a tiny RIFF/WAVE fmt+data
// parser loads the real sample data directly into the AL buffer -- no
// external dependency, no stub. Handles 8/16-bit mono/stereo PCM.
//
static bool LoadWavPCM(const char *path, char **outData, int *outBytes,
int *outChannels, int *outBits, int *outRate)
{
FILE *fp = fopen(path, "rb");
if (!fp) return false;
fseek(fp, 0, SEEK_END); long flen = ftell(fp); fseek(fp, 0, SEEK_SET);
if (flen < 44) { fclose(fp); return false; }
unsigned char *buf = new unsigned char[flen];
size_t got = fread(buf, 1, flen, fp);
fclose(fp);
if ((long)got != flen) { delete [] buf; return false; }
if (memcmp(buf, "RIFF", 4) != 0 || memcmp(buf + 8, "WAVE", 4) != 0) { delete [] buf; return false; }
int channels = 0, bits = 0, rate = 0;
char *pcm = 0; int pcmBytes = 0;
bool haveFmt = false;
long p = 12;
while (p + 8 <= flen)
{
const unsigned char *id = buf + p;
unsigned int csz = buf[p+4] | (buf[p+5]<<8) | (buf[p+6]<<16) | ((unsigned)buf[p+7]<<24);
long body = p + 8;
if (memcmp(id, "fmt ", 4) == 0 && csz >= 16)
{
channels = buf[body+2] | (buf[body+3]<<8);
rate = buf[body+4] | (buf[body+5]<<8) | (buf[body+6]<<16) | ((unsigned)buf[body+7]<<24);
bits = buf[body+14] | (buf[body+15]<<8);
haveFmt = true;
}
else if (memcmp(id, "data", 4) == 0)
{
long avail = flen - body;
pcmBytes = ((long)csz <= avail) ? (int)csz : (int)avail;
if (pcmBytes < 0) pcmBytes = 0;
pcm = new char[pcmBytes > 0 ? pcmBytes : 1];
memcpy(pcm, buf + body, pcmBytes);
}
p = body + csz + (csz & 1); // RIFF chunks are word-aligned
}
delete [] buf;
if (!haveFmt || pcm == 0) { delete [] pcm; return false; }
*outData = pcm; *outBytes = pcmBytes;
*outChannels = channels; *outBits = bits; *outRate = rate;
return true;
}
//#############################################################################
//####################### AudioObjectStream #############################
@@ -82,6 +142,11 @@ RegisteredClass*
Check(this);
Verify(class_ID != RegisteredClass::NullClassID);
// DIAG (BT_ATTRBIND_LOG): record which audio-object ClassID is being built so the
// AttributeWatcher trace can print the watcher TYPE alongside the bound attribute.
extern int g_curAudioWatcherClass;
g_curAudioWatcherClass = (int)class_ID;
//
//-----------------------------------------------------------------------
// HACK - Constructors should be referenced through registration, for
@@ -542,7 +607,7 @@ void
for(int i=1; i <= 2; i++)
{
for (int j=0; j < 100; j++)
for (int j=0; j < 128; j++)
{
if (PRESET_isImplemented(i,j))
{
@@ -558,6 +623,7 @@ void
alGetError();
g_buffers = new ALuint[g_numBuffers];
alGenBuffers(g_numBuffers,g_buffers);
if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] alGenBuffers count=" << g_numBuffers << "\n" << std::flush;
ALenum error;
if ((error = alGetError()) != AL_NO_ERROR)
{
@@ -572,7 +638,7 @@ void
//Load buffers
for(int i=1; i<=2 && bufferInd < g_numBuffers; i++)
{
for(int j=0; j<100 && bufferInd < g_numBuffers; j++)
for(int j=0; j<128 && bufferInd < g_numBuffers; j++)
{
if (PRESET_isImplemented(i,j))
{
@@ -580,84 +646,86 @@ void
{
SAMPLEINFO info = PRESET_getSampleInfo(i,j,k);
//Load WAV to buffer
char *data;
int size;
//Open WAV
char fullname[50];
strcpy_s(fullname,50,"AUDIO\\");
strcpy_s(fullname,50,"AUDIO/");
strcat_s(fullname,50,info.file);
SF_INFO *sfInfo = new SF_INFO[2];
//SF_INFO is working different than expected!
sfInfo->format = 0;
SNDFILE *file = sf_open(fullname,SFM_READ,sfInfo);
if (file == NULL)
char *data = 0; int size = 0;
int wavCh = 0, wavBits = 0, wavRate = 0;
if (!LoadWavPCM(fullname, &data, &size, &wavCh, &wavBits, &wavRate))
{
DEBUG_STREAM << "Failed to open." << std::endl;
if (getenv("BT_AUDIO_LOG") && bufferInd == 0) {
char cwd[512]; cwd[0]=0; _getcwd(cwd, sizeof(cwd));
DEBUG_STREAM << "[audio] DIAG cwd=[" << cwd << "] path=[" << fullname << "] WAV load FAILED\n" << std::flush;
}
DEBUG_STREAM << "[audio] Failed to open AUDIO/" << info.file << " -- skipping\n" << std::flush;
PRESET_setBufferIndex(i,j,k,bufferInd);
bufferInd++;
continue;
}
unsigned long formatBits = 0;
unsigned long sampleRate = sfInfo->samplerate;
size = sfInfo->frames;
bool isMono = (sfInfo->channels == 1);
if (sfInfo->format & SF_FORMAT_PCM_S8)
{
formatBits = 8;
} else if (sfInfo->format & SF_FORMAT_PCM_16)
{
formatBits = 16;
} else
{
DEBUG_STREAM << "BAD FORMAT" << std::endl;
}
ALenum format;
if (formatBits == 8)
if (wavBits == 8)
format = (wavCh == 1) ? AL_FORMAT_MONO8 : AL_FORMAT_STEREO8;
else
format = (wavCh == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16;
//Feed the buffer with the real PCM sample data
alBufferData(g_buffers[bufferInd], format, data, size, wavRate);
// (task #50, AUDIO_FIDELITY F13) honor the authored loop
// REGION -- the EMU8000 looped [dwStartLoop,dwEndLoop], not
// the whole sample (MechExplosion's boom carries a ~16 ms
// sustain slice that must not replay the full boom).
#ifndef AL_LOOP_POINTS_SOFT
#define AL_LOOP_POINTS_SOFT 0x2015
#endif
if (info.loop != ForceStatic && info.loopEnd > info.loopStart)
{
if (isMono)
{
format = AL_FORMAT_MONO8;
} else
{
size *= 2;
format = AL_FORMAT_STEREO8;
}
} else if (formatBits == 16)
{
size *= 2;
if (isMono)
{
format = AL_FORMAT_MONO16;
} else
{
size *= 2;
format = AL_FORMAT_STEREO16;
}
ALint loop_points[2] = { info.loopStart, info.loopEnd };
alGetError();
alBufferiv(g_buffers[bufferInd], AL_LOOP_POINTS_SOFT, loop_points);
ALenum lpErr = alGetError();
if (lpErr != AL_NO_ERROR && getenv("BT_AUDIO_LOG"))
DEBUG_STREAM << "[audio] loop-points REJECTED buf" << bufferInd
<< " " << info.file << " [" << info.loopStart << ","
<< info.loopEnd << "] err=" << lpErr << "\n" << std::flush;
}
ALsizei alSampleRate = sampleRate;
//Load size & data
delete [] sfInfo;
data = new char[size];
sf_read_raw(file,data,size);
sf_close(file);
//Feed the buffer
alBufferData(g_buffers[bufferInd],format,data,size,alSampleRate);
if (bufferInd < 512) g_bufferNames[bufferInd] = info.file;
if (getenv("BT_AUDIO_LOG") && bufferInd < 3)
DEBUG_STREAM << "[audio] loaded buf" << bufferInd << " " << info.file
<< " ch=" << wavCh << " bits=" << wavBits << " rate=" << wavRate
<< " bytes=" << size << " alErr=" << alGetError() << "\n" << std::flush;
PRESET_setBufferIndex(i,j,k,bufferInd);
bufferInd++;
delete [] data;
delete [] data;
}
}
}
}
if (getenv("BT_AUDIO_TEST") && g_numBuffers > 0)
{
// PROOF OF LIFE: play the first loaded sample so we can confirm the
// SF2 -> WAV -> OpenAL buffer -> speakers path works end to end.
ALCcontext *ctx = alcGetCurrentContext();
alGetError();
ALuint testsrc = 0; alGenSources(1, &testsrc);
ALenum genErr = alGetError();
alSourcei(testsrc, AL_BUFFER, g_buffers[0]);
ALenum bufErr = alGetError();
alSourcei(testsrc, AL_LOOPING, AL_TRUE);
alSourcef(testsrc, AL_GAIN, 1.0f);
alSourcePlay(testsrc);
ALenum playErr = alGetError();
ALint st = 0; alGetSourcei(testsrc, AL_SOURCE_STATE, &st);
DEBUG_STREAM << "[audio] TEST-PLAY buffer0 src=" << testsrc
<< " ctx=" << (void*)ctx
<< " genErr=" << genErr << " bufErr=" << bufErr << " playErr=" << playErr
<< " state=" << st << " playing=" << (int)(st==AL_PLAYING) << "\n" << std::flush;
}
//
//----------------------------------------------------------------------
// Load the sound banks
+20
View File
@@ -1,3 +1,4 @@
#include <cstdlib>
#include "mungal4.h"
#pragma hdrstop
@@ -98,6 +99,7 @@ void
L4AudioRenderer::Initialize()
{
Check(this);
if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] L4AudioRenderer::Initialize ENTERED\n" << std::flush;
//
//----------------------------------------------------------------------
@@ -133,6 +135,7 @@ void
NotationFile notation_file(audio_file_name);
Check(&notation_file);
if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] notation file opened; about to read scalars\n" << std::flush;
//
//----------------------------------------------------------------------
@@ -379,6 +382,23 @@ void
{
ALCcontext *context = alcCreateContext(device,NULL);
alcMakeContextCurrent(context);
// Master volume: AL_GAIN on the listener scales EVERY source. Default 0.6
// (the raw samples are hot); override with BT_AUDIO_VOLUME=<0.0..1.0+>.
float masterVol = 0.6f;
if (const char *v = getenv("BT_AUDIO_VOLUME")) { float f = (float)atof(v); if (f >= 0.0f) masterVol = f; }
alListenerf(AL_GAIN, masterVol);
if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] OpenAL device OPENED + context current; master gain=" << masterVol << "\n" << std::flush;
// (task #50, AUDIO_FIDELITY F9/F11) EFX bridge: the authored lowpass
// chains + the wet-exterior/dry-cockpit reverb split, at the authentic
// AUDIO.INI global_reverb_scale read above.
{
extern bool EFX_Initialize(float global_reverb_scale);
EFX_Initialize(global_reverb_scale);
}
}
else
{
if (getenv("BT_AUDIO_LOG")) DEBUG_STREAM << "[audio] OpenAL device FAILED to open (alcOpenDevice NULL)\n" << std::flush;
}
//
+20 -2
View File
@@ -970,8 +970,17 @@ void L4NetworkManager::HostDisconnectedMessageHandler(HostDisconnectedMessage* H
//
// Post a listen for a console so it can reconnect if it wants to.
//
NetTransport_Get()->Close(gameListenerSocket);
gameListenerSocket = NULL;
// FIX (task #50, 2026-07-15): this used to close the GAME listener here
// (shutdown/closesocket(gameListenerSocket)) on a CONSOLE disconnect -- a
// naming bug: the comment above + the commented-out OpenConnection below
// intended to re-post a CONSOLE listen, which CreateConsoleHost() (below)
// already does. Closing the game listener on console loss needlessly
// bounced the game-accept path (harmless to already-established peer
// sockets -- which is why a live 2-node match keeps replicating through a
// console loss -- but wrong, and it would have blocked a NEW peer from
// joining after a console cycle). Removed; CreateConsoleHost re-listens.
// (BT412: the NetTransport seam close that briefly lived here is dropped
// with the bug it wrapped.)
//unsigned long console_socket = OpenConnection(
// NETNUB_TCP_LISTEN,
// ((L4Application *)application)->GetNetworkCommonFlatAddress(), // Local port
@@ -2138,6 +2147,12 @@ Logical L4NetworkManager::CheckBuffers(NetworkPacket *network_packet)
{
continue;
}
// TCP_NODELAY on the accepted GAME socket (not the console):
// without it Nagle coalesces the inbound per-frame update
// records into bursts -> the peer is received in lurches.
BOOL gameNoDelay = TRUE;
if (setsockopt(tempSocket, IPPROTO_TCP, TCP_NODELAY, (char*)&gameNoDelay, sizeof(gameNoDelay)))
DEBUG_STREAM << "WARN: could not set TCP_NODELAY on accepted game socket; setsockopt() failed with " << WSAGetLastError() << "\n" << std::flush;
}
remote_host->SetNetworkSocket(tempSocket);
@@ -2683,6 +2698,9 @@ SOCKET L4NetworkManager::OpenConnection(
remoteEndpoint.sin_addr.S_un.S_addr = internet_address;
remoteEndpoint.sin_port = htons(remote_port);
// BT412: the raw connect/retry/nonblocking/TCP_NODELAY sequence that
// lived here now lives in WinsockNetTransport::Connect (the seam) --
// including BT411's task-#50 TCP_NODELAY latency fix, folded in there.
return (SOCKET) NetTransport_Get()->Connect(&remoteEndpoint, local_port);
}
else if(connection_type == NETNUB_TCP_LISTEN)
+12
View File
@@ -164,6 +164,18 @@ namespace
closesocket(sock);
return InvalidConnection;
}
// TCP_NODELAY (BT411 task #50, folded into the seam): disable
// Nagle so the small per-frame update records ship immediately
// instead of being coalesced. Nagle + delayed-ACK batch the
// tiny position packets into ~40-200ms bursts, so a peer moving
// at a steady speed is RECEIVED in lurches (dead-reckoned ground
// speed swinging 3x per 0.25s window). The pod net carries
// steady real-time state where latency, not throughput, matters.
BOOL no_delay = TRUE;
if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY,
(char *) &no_delay, sizeof(no_delay)))
DEBUG_STREAM << "WARN: could not set TCP_NODELAY on active socket; setsockopt() failed with "
<< WSAGetLastError() << "\n" << std::flush;
return (Connection) sock;
}
+31
View File
@@ -8577,6 +8577,37 @@ void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::Inte
const double presentMs = (double)(_rt2.QuadPart - _rt1.QuadPart) * 1000.0 / (double)_rf.QuadPart;
static double sAcc = 0.0, sMaxD = 0.0, sMaxP = 0.0; static int sFrames = 0;
sAcc += drawMs + presentMs; ++sFrames;
// RENDER-frame heading probe (BT_RENDHDG): count render frames that redraw
// the SAME peer heading (stale between sim updates) vs frames where it moved.
// sameHeading >> moved => the peer sim updates slower than the render draws,
// so the rotation stutters regardless of how smooth each sim step is.
if (getenv("BT_RENDHDG"))
{
extern volatile float gBTReplRenderYaw;
static float sPrevY = -999.0f, sMaxStep = 0.0f, sSumStep = 0.0f;
static int sN = 0, sBack = 0; static double sHAcc = 0.0;
const float y = gBTReplRenderYaw;
if (sPrevY > -900.0f)
{
float d = y - sPrevY; // per-RENDER-frame heading change
if (d > 3.14159f) d -= 6.28319f; // unwrap
if (d < -3.14159f) d += 6.28319f;
const float ad = (d < 0.0f) ? -d : d;
sSumStep += ad; if (ad > sMaxStep) sMaxStep = ad;
if (d > 0.0f) ++sBack; // spin is -yaw here; +step == backward
++sN;
}
sPrevY = y; sHAcc += drawMs + presentMs;
if (sHAcc >= 1000.0 && sN > 0)
{
const float avg = sSumStep / sN;
DEBUG_STREAM << "[rendhdg] avgStep=" << avg << " maxStep=" << sMaxStep
<< " max/avg=" << (avg > 1e-6f ? sMaxStep / avg : 0.0f)
<< " backwardFrames=" << sBack << "/" << sN
<< " (smooth: max/avg~1, backward~0)\n" << std::flush;
sMaxStep = 0.0f; sSumStep = 0.0f; sN = 0; sBack = 0; sHAcc = 0.0;
}
}
if (drawMs > sMaxD) sMaxD = drawMs;
if (presentMs > sMaxP) sMaxP = presentMs;
if (drawMs + presentMs > 150.0)
+14 -9
View File
@@ -4,7 +4,7 @@ bool PRESET_isImplemented(int bank, int presetn)
{
PRESETINFO preset = allPresets[bank-1][presetn];
if (preset.sampleNum <= 0 || preset.sampleNum >= 5)
if (preset.sampleNum <= 0 || preset.sampleNum > MAX_PRESET_SAMPLES)
{
return false;
}
@@ -27,15 +27,20 @@ int PRESET_getNumSamples(int bank, int preset)
SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd)
{
SAMPLEINFO default;
default.chan = SampleChannel::CHANNEL_CENTER;
default.file = "";
default.implemented = false;
default.loop = SampleLoop::LoopAtWill;
if (sampleInd < 0 || sampleInd >= allPresets[bank-1][preset].sampleNum)
{
return default;
SAMPLEINFO none;
none.bufferIndex = -1;
none.implemented = false;
none.file = "";
none.chan = SampleChannel::CHANNEL_CENTER;
none.loop = SampleLoop::LoopAtWill;
none.keyLo = 0;
none.keyHi = 127;
none.loopStart = 0;
none.loopEnd = 0;
none.releaseSec = 0.0f;
return none;
}
return allPresets[bank-1][preset].samples[sampleInd];
@@ -44,4 +49,4 @@ SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd)
void PRESET_setBufferIndex(int bank, int preset, int sampleInd, int index)
{
allPresets[bank-1][preset].samples[sampleInd].bufferIndex = index;
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.