layers, baked tuning, loop regions, release fades Extractor rewrite (tools/sf2extract.py): every sample-bearing instrument zone becomes a SAMPLEINFO slot -- 603 zones / 241 presets (68/115 + 94/126 multi- zone, matching the audit exactly). Per zone: keyRange(43), sampleModes(54), SBK samplePitch(55) + rootKey(58) + coarse/fineTune(51/52), attenuation(48, INVERTED SBK scale, 0.375 dB/step [T3], baked into the PCM), releaseVolEnv(38), pan(17), shdr loop region. F2 pitch (algebraically exact): WAV rate = round(44100 * 2^(((6000 - rootCents) + tune)/1200)) -- EMU8000 v1 base 44100. Cross-checks: FootFall 17300 Hz (the audit's +4.2 st), MissileLoaded low zone 88200 (-24 st), Warnings01 8-way klaxon split w/ 3 looped zones, Death01 162 Hz extremes. F1 zone selection: SetupPatch/PlayNote take the authored note; attach/play only zones whose [keyLo,keyHi] contains it (detach the rest so a rewound source can't replay a stale buffer). Live-verified: LaserLoaded/ MissileLoaded note 36 -> low clunk zone, note 84 -> high blip zone (pitch 4); LaserCFire plays all 3 authored layers incl the looping sustain. Stereo-pair zones pan via listener-relative AL_POSITION (distance model is AL_NONE). MAX_PRESET_SAMPLES=25 (AllExplosion); fixed PRESET_isImplemented's >=5 bound. F13 loops + releases: authored [loopStart,loopEnd] applied via AL_SOFT_loop_points at buffer load (0 rejections; kills the latent boom-loop on MechExplosion's 1.5% sustain slice + the ~2.4 Hz LaserBSustain wrap tick); StopNote now honors the authored releaseVolEnv (1.1-3.9 s on ~20 looping presets) with a dB-linear fade serviced from AudioHead::Execute; restarts reclaim fading sources. One-shots keep the instant stop (faithful). Regression (40s drive+fire): stable, loop points accepted, key-splits + layers verified in the delivery trace, chirp still dead, footfalls fire. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
540 lines
14 KiB
C++
540 lines
14 KiB
C++
#include <cstdlib>
|
|
#include "munga.h"
|
|
#pragma hdrstop
|
|
|
|
#include "audio.h"
|
|
#include "audrend.h"
|
|
#include "app.h"
|
|
#include "..\munga_l4\openal\al.h"
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Audio Constants ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
const Scalar AudioDoubleTriggerCutoff = 0.2f;
|
|
|
|
const AudioControlValue MaxAudioVolume = 1.0f;
|
|
const AudioControlValue MinAudioVolume = 0.0f;
|
|
const AudioControlValue LowAudioVolumeThreshold = 0.3f;
|
|
|
|
const AudioControlValue MaxAudioBrightness = 1.0f;
|
|
const AudioControlValue MinAudioBrightness = 0.0f;
|
|
|
|
const AudioControlValue MaxAudioAttack = 1.0f;
|
|
const AudioControlValue MinAudioAttack = 0.0f;
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ AudioHead ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
AudioHead::AudioHead():
|
|
headEntitySocket(NULL)
|
|
{
|
|
//
|
|
// Start frame counter at 0, other counts should be at
|
|
// NullAudioFrameCount
|
|
//
|
|
audioFrameCount = 0;
|
|
|
|
//
|
|
// Other audio parameters
|
|
//
|
|
clippingRadius = 100.0f;
|
|
distanceBetweenEars = 1.0f;
|
|
amplitudeRollOffExponent = 1.0f;
|
|
amplitudeRollOffKnee = 10.0f,
|
|
amplitudeRollOffDistanceScale = 0.05f;
|
|
highFrequencyRollOffExponent = 0.5f;
|
|
highFrequencyRollOffKnee = 10.0f;
|
|
highFrequencyRollOffDistanceScale = 0.05f;
|
|
reverbToDryRatio = 0.1f;
|
|
audioSoundSpeed = 345.0f;
|
|
audioDopplerConstant = 20.0f;
|
|
sourceCompressionExponent = 0.5f;
|
|
sourceCompressionScale = 0.5f;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
AudioHead::~AudioHead()
|
|
{
|
|
Check(this);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
Logical
|
|
AudioHead::TestInstance() const
|
|
{
|
|
Check(&headEntitySocket);
|
|
return True;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioHead::LinkToEntity(Entity *entity)
|
|
{
|
|
Check(this);
|
|
Check(entity);
|
|
|
|
//
|
|
// Remove existing entity, add new one
|
|
//
|
|
if (headEntitySocket.GetCurrent() != NULL)
|
|
{
|
|
headEntitySocket.Remove();
|
|
}
|
|
headEntitySocket.Add(entity);
|
|
|
|
// 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
|
|
//
|
|
// Make the new clipping sphere
|
|
//
|
|
clippingSphere.SetCurrentOrigin(entity->localOrigin.linearPosition);
|
|
#endif
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
LinearMatrix
|
|
AudioHead::GetEarToWorld()
|
|
{
|
|
Check(this);
|
|
|
|
Entity *entity = headEntitySocket.GetCurrent();
|
|
Check(entity);
|
|
return entity->localToWorld;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioHead::Execute()
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
// Increment frame counter
|
|
//
|
|
// 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;
|
|
|
|
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
|
|
//
|
|
Entity *entity = headEntitySocket.GetCurrent();
|
|
Check(entity);
|
|
|
|
//
|
|
// Make the new clipping sphere
|
|
//
|
|
clippingSphere.SetCurrentOrigin(entity->localOrigin.linearPosition);
|
|
#endif
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioHead::DefineClippingSphere(Scalar radius)
|
|
{
|
|
Check(this);
|
|
|
|
clippingRadius = radius;
|
|
|
|
#if 0
|
|
//
|
|
// Get entity position
|
|
//
|
|
Entity *entity;
|
|
Point3D position(0.0f, 0.0f, 0.0f);
|
|
|
|
if ((entity = headEntitySocket.GetCurrent()) != NULL)
|
|
{
|
|
Check(entity);
|
|
position = entity->localOrigin.linearPosition;
|
|
}
|
|
|
|
//
|
|
// Make the new clipping sphere
|
|
//
|
|
clippingSphere.SetCurrentOrigin(position);
|
|
clippingSphere.SetReferenceRadius(radius);
|
|
#endif
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
Logical
|
|
AudioHead::IsPointClipped(const Point3D &point)
|
|
{
|
|
Check(this);
|
|
|
|
Entity
|
|
*entity;
|
|
Vector3D
|
|
difference;
|
|
|
|
entity = headEntitySocket.GetCurrent();
|
|
Check(entity);
|
|
difference.Subtract(entity->localOrigin.linearPosition, point);
|
|
return difference.LengthSquared() > clippingRadius*clippingRadius;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioHead::ControlDopplerEffect(
|
|
Scalar doppler_constant,
|
|
Scalar sound_speed
|
|
)
|
|
{
|
|
Check(this);
|
|
audioDopplerConstant = doppler_constant;
|
|
audioSoundSpeed = sound_speed;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioHead::ControlAmplitudeRollOff(
|
|
Scalar exponent,
|
|
Scalar amplitude_rolloff_knee,
|
|
Scalar distance_scale
|
|
)
|
|
{
|
|
Check(this);
|
|
amplitudeRollOffExponent = exponent;
|
|
amplitudeRollOffKnee = amplitude_rolloff_knee;
|
|
amplitudeRollOffDistanceScale = distance_scale;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioHead::ControlHighFrequencyRollOff(
|
|
Scalar exponent,
|
|
Scalar high_frequency_rolloff_knee,
|
|
Scalar distance_scale
|
|
)
|
|
{
|
|
Check(this);
|
|
highFrequencyRollOffExponent = exponent;
|
|
highFrequencyRollOffKnee = high_frequency_rolloff_knee;
|
|
highFrequencyRollOffDistanceScale = distance_scale;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioHead::ControlSourceCompression(
|
|
Scalar exponent,
|
|
Scalar scale
|
|
)
|
|
{
|
|
Check(this);
|
|
sourceCompressionExponent = exponent;
|
|
sourceCompressionScale = scale;
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioHead::SetPositionalCulling(Logical)
|
|
{
|
|
Check(this);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ AudioComponent ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
const Receiver::HandlerEntry
|
|
AudioComponent::MessageHandlerEntries[]=
|
|
{
|
|
{
|
|
AudioComponent::ReceiveControlMessageID,
|
|
"ReceiveControl",
|
|
(AudioComponent::Handler)&AudioComponent::ReceiveControlMessageHandler
|
|
}
|
|
};
|
|
|
|
AudioComponent::MessageHandlerSet& AudioComponent::GetMessageHandlers()
|
|
{
|
|
static AudioComponent::MessageHandlerSet messageHandlers(ELEMENTS(AudioComponent::MessageHandlerEntries), AudioComponent::MessageHandlerEntries, Component::GetMessageHandlers());
|
|
return messageHandlers;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
Derivation* AudioComponent::GetClassDerivations()
|
|
{
|
|
static Derivation classDerivations(Component::GetClassDerivations(), "AudioComponent");
|
|
return &classDerivations;
|
|
}
|
|
|
|
AudioComponent::SharedData
|
|
AudioComponent::DefaultData(
|
|
AudioComponent::GetClassDerivations(),
|
|
AudioComponent::GetMessageHandlers()
|
|
);
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
AudioComponent::AudioComponent(
|
|
PlugStream *stream,
|
|
SharedData &shared_data
|
|
):
|
|
Component(stream, shared_data),
|
|
audioWatcherSocket(NULL)
|
|
{
|
|
nextExecuteWatcherFrame = NullAudioFrameCount;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
AudioComponent::~AudioComponent()
|
|
{
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
Logical
|
|
AudioComponent::TestInstance() const
|
|
{
|
|
if (!IsDerivedFrom(*GetClassDerivations()))
|
|
{
|
|
return False;
|
|
}
|
|
Check(&audioWatcherSocket);
|
|
return True;
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioComponent::ExecuteWatchers()
|
|
{
|
|
Check(this);
|
|
|
|
//
|
|
// Execute watchers if this is the next watcher frame
|
|
//
|
|
AudioFrameCount
|
|
audio_frame_count;
|
|
|
|
Check(application);
|
|
Check(application->GetAudioRenderer());
|
|
audio_frame_count = application->GetAudioRenderer()->GetAudioFrameCount();
|
|
if (nextExecuteWatcherFrame <= audio_frame_count)
|
|
{
|
|
nextExecuteWatcherFrame = audio_frame_count + DefaultAudioFrameDelay;
|
|
|
|
//
|
|
// Execute watchers
|
|
//
|
|
ChainIteratorOf<Component*>
|
|
iterator(&audioWatcherSocket);
|
|
Component
|
|
*component;
|
|
|
|
while ((component = iterator.ReadAndNext()) != NULL)
|
|
{
|
|
Check(component);
|
|
component->Execute();
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioComponent::Execute()
|
|
{
|
|
Check(this);
|
|
ExecuteWatchers();
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioComponent::ReceiveControl(
|
|
AudioControlID,
|
|
AudioControlValue
|
|
)
|
|
{
|
|
Fail("AudioComponent::ReceiveControl - Should never reach here");
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioComponent::PostReceiveControl(
|
|
AudioControlID control_ID,
|
|
AudioControlValue control_value
|
|
)
|
|
{
|
|
Check(this);
|
|
|
|
ReceiveControlMessage
|
|
message(control_ID, control_value);
|
|
|
|
Check(application);
|
|
// ECH 1/26/96 - application->Post(HighEventPriority, this, &message);
|
|
application->Post(DefaultEventPriority, this, &message);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//
|
|
//#############################################################################
|
|
//#############################################################################
|
|
//
|
|
void
|
|
AudioComponent::ReceiveControlMessageHandler(ReceiveControlMessage *message)
|
|
{
|
|
Check(this);
|
|
Check(message);
|
|
ReceiveControl(message->controlID, message->controlValue);
|
|
Check_Fpu();
|
|
}
|
|
|
|
//~~~~~~~~~~~~~~~~~~ AudioComponent__ReceiveControlMessage ~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
AudioComponent__ReceiveControlMessage::AudioComponent__ReceiveControlMessage(
|
|
AudioControlID control_ID,
|
|
AudioControlValue control_value
|
|
):
|
|
Receiver::Message(
|
|
AudioComponent::ReceiveControlMessageID,
|
|
sizeof(AudioComponent__ReceiveControlMessage)
|
|
)
|
|
{
|
|
controlID = control_ID;
|
|
controlValue = control_value;
|
|
}
|