diff --git a/CMakeLists.txt b/CMakeLists.txt index d302a07..68ecb9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,6 +179,7 @@ add_library(munga_engine STATIC "engine/MUNGA/WRHOUS.cpp" "engine/MUNGA_L4/DXUtils.cpp" "engine/MUNGA_L4/L4APP.cpp" + "engine/MUNGA_L4/L4AUDEFX.cpp" "engine/MUNGA_L4/L4AUDHDW.cpp" "engine/MUNGA_L4/L4AUDIO.cpp" "engine/MUNGA_L4/L4AUDLVL.cpp" diff --git a/engine/MUNGA_L4/L4AUDEFX.cpp b/engine/MUNGA_L4/L4AUDEFX.cpp new file mode 100644 index 0000000..d5019ab --- /dev/null +++ b/engine/MUNGA_L4/L4AUDEFX.cpp @@ -0,0 +1,129 @@ +//########################################################################### +// +// L4AUDEFX.cpp -- OpenAL EFX bridge (task #50, AUDIO_FIDELITY F9/F11). +// See L4AUDEFX.h for the fidelity rationale. +// +//########################################################################### +#include +#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); +} diff --git a/engine/MUNGA_L4/L4AUDEFX.h b/engine/MUNGA_L4/L4AUDEFX.h new file mode 100644 index 0000000..423c85c --- /dev/null +++ b/engine/MUNGA_L4/L4AUDEFX.h @@ -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); diff --git a/engine/MUNGA_L4/L4AUDIO.cpp b/engine/MUNGA_L4/L4AUDIO.cpp index e9755af..942dc41 100644 --- a/engine/MUNGA_L4/L4AUDIO.cpp +++ b/engine/MUNGA_L4/L4AUDIO.cpp @@ -5,6 +5,7 @@ #include "l4audio.h" #include "l4audlvl.h" +#include "l4audefx.h" #include "l4app.h" #include "l4audrnd.h" #include "..\munga\namelist.h" @@ -964,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 @@ -1082,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); } } @@ -1260,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 @@ -1459,11 +1498,22 @@ void << " 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 * 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); + EFX_SetSourceLowpassGainHF(channelSet.sources[i], dyn_gainhf); } } @@ -1786,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 @@ -2006,11 +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 * 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); + EFX_SetSourceLowpassGainHF(channelSet.sources[i], static_gainhf); } // diff --git a/engine/MUNGA_L4/L4AUDLVL.cpp b/engine/MUNGA_L4/L4AUDLVL.cpp index 7e1a79c..73285e1 100644 --- a/engine/MUNGA_L4/L4AUDLVL.cpp +++ b/engine/MUNGA_L4/L4AUDLVL.cpp @@ -439,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; +} + // //############################################################################# //############################################################################# diff --git a/engine/MUNGA_L4/L4AUDLVL.h b/engine/MUNGA_L4/L4AUDLVL.h index 9af5940..934aea8 100644 --- a/engine/MUNGA_L4/L4AUDLVL.h +++ b/engine/MUNGA_L4/L4AUDLVL.h @@ -186,5 +186,10 @@ public: 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(); }; diff --git a/engine/MUNGA_L4/L4AUDRND.cpp b/engine/MUNGA_L4/L4AUDRND.cpp index af8c8e2..8077135 100644 --- a/engine/MUNGA_L4/L4AUDRND.cpp +++ b/engine/MUNGA_L4/L4AUDRND.cpp @@ -388,6 +388,13 @@ void 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 {