Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf59ac9f4b | ||
|
|
05f993b9aa | ||
|
|
f83f56e14d | ||
|
|
f44cdbacca | ||
|
|
303758cb52 | ||
|
|
12f9ebefab | ||
|
|
a417175da8 | ||
|
|
943a15cef4 | ||
|
|
cdccb16251 | ||
|
|
a5faa6cf9b | ||
|
|
25e25260b1 | ||
|
|
4e8392fcfb | ||
|
|
523f713a30 | ||
|
|
e133d4c993 | ||
|
|
d361a0b8be | ||
|
|
ce1b0ab9c3 | ||
|
|
6ce729bab5 | ||
|
|
f4fef29428 |
@@ -84,3 +84,10 @@ assets/**/last.spl
|
|||||||
|
|
||||||
# packaged releases (attached to Gitea releases, not tracked)
|
# packaged releases (attached to Gitea releases, not tracked)
|
||||||
RedPlanet-*.zip
|
RedPlanet-*.zip
|
||||||
|
|
||||||
|
# Crash dumps sent in by testers. Read them with cdb against the matching
|
||||||
|
# Release\rpl4opt.pdb - the PE timestamp in the dump says which build, and
|
||||||
|
# the symbols only mean anything if it matches. They are not ours to keep
|
||||||
|
# in the history: a minidump carries process memory and the sender's own
|
||||||
|
# file paths.
|
||||||
|
/Crashdmp/
|
||||||
|
|||||||
+10
-2
@@ -94,8 +94,16 @@ void
|
|||||||
}
|
}
|
||||||
headEntitySocket.Add(entity);
|
headEntitySocket.Add(entity);
|
||||||
|
|
||||||
alDistanceModel(AL_LINEAR_DISTANCE);
|
// FIDELITY (docs/SOUND.md F3/F10): the engine computes the AUTHORED distance
|
||||||
alDopplerFactor(0.3f);
|
// attenuation curve (AUDIO.INI amplitude_rolloff knee/exponent ->
|
||||||
|
// AudioLocation::distanceVolumeScale) and the AUTHORED doppler-cents model
|
||||||
|
// (doppler_range=600 / speed_of_sound=250) on every spatial update. Disable
|
||||||
|
// OpenAL's own models so they cannot double-apply or fight them:
|
||||||
|
// AL_LINEAR_DISTANCE faded distant audio to zero on a straight line where the
|
||||||
|
// authored curve still sits near 44% at the clip edge, and AL doppler ran at
|
||||||
|
// the wrong constants with a sign-inverted velocity feed.
|
||||||
|
alDistanceModel(AL_NONE);
|
||||||
|
alDopplerFactor(0.0f);
|
||||||
|
|
||||||
#if 0
|
#if 0
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -21,6 +21,31 @@
|
|||||||
BitTrace Gauge_Renderer("Gauge Renderer");
|
BitTrace Gauge_Renderer("Gauge Renderer");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
//
|
||||||
|
// How long a single background pass may spend drawing gauges, in
|
||||||
|
// milliseconds. RP412GAUGESLICE tunes it; 0 restores the original
|
||||||
|
// behaviour of exactly one gauge per pass.
|
||||||
|
//
|
||||||
|
static long
|
||||||
|
GaugeSliceMs()
|
||||||
|
{
|
||||||
|
static long
|
||||||
|
slice = -1L;
|
||||||
|
|
||||||
|
if (slice < 0L)
|
||||||
|
{
|
||||||
|
const char
|
||||||
|
*setting = getenv("RP412GAUGESLICE");
|
||||||
|
|
||||||
|
slice = (setting != NULL) ? atol(setting) : 2L;
|
||||||
|
if (slice < 0L)
|
||||||
|
{
|
||||||
|
slice = 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slice;
|
||||||
|
}
|
||||||
|
|
||||||
//#######################################################################
|
//#######################################################################
|
||||||
// Miscellaneous utilities
|
// Miscellaneous utilities
|
||||||
//#######################################################################
|
//#######################################################################
|
||||||
@@ -3682,8 +3707,28 @@ Logical
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case background:
|
case background:
|
||||||
|
{
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Draw gauges until the slice is spent, rather than exactly
|
||||||
|
// one per pass.
|
||||||
|
//
|
||||||
|
// The background loop is only guaranteed a single pass per
|
||||||
|
// frame; it gets more only while time remains before the
|
||||||
|
// frame is due. On a busy map the 3D foreground eats the
|
||||||
|
// whole budget, so a cycle of ninety-odd gauges takes
|
||||||
|
// ninety-odd frames to come round and the displays sit
|
||||||
|
// frozen for seconds. Working to a slice makes the refresh
|
||||||
|
// rate depend on elapsed time instead of on how much spare
|
||||||
|
// frame there happened to be.
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
Time slice_end = Now();
|
||||||
|
slice_end += GaugeSliceMs();
|
||||||
|
|
||||||
|
do
|
||||||
{
|
{
|
||||||
result = ProcessOneActiveGauge();
|
result = ProcessOneActiveGauge();
|
||||||
|
}
|
||||||
|
while (result && taskMode == background && Now() < slice_end);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
//###########################################################################
|
||||||
|
//
|
||||||
|
// L4AUDEFX.cpp -- OpenAL EFX bridge (docs/SOUND.md, findings F9 and F11).
|
||||||
|
// See L4AUDEFX.h for the fidelity rationale.
|
||||||
|
//
|
||||||
|
//###########################################################################
|
||||||
|
#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"))
|
||||||
|
{
|
||||||
|
Tell("L4AUDEFX: ALC_EXT_EFX not present - filters and reverb inert\n");
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Tell("L4AUDEFX: EFX entry points missing - filters and reverb inert\n");
|
||||||
|
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 on
|
||||||
|
// every 3D channel, so one global slot gain reproduces the same uniform
|
||||||
|
// send. RP authors 0.35 (AUDIO.INI); BT used 0.3.
|
||||||
|
//
|
||||||
|
p_alAuxiliaryEffectSlotf(s_reverbSlot, AL_EFFECTSLOT_GAIN,
|
||||||
|
(global_reverb_scale < 0.0f) ? 0.0f :
|
||||||
|
(global_reverb_scale > 1.0f) ? 1.0f : global_reverb_scale);
|
||||||
|
|
||||||
|
//
|
||||||
|
// LOWPASS only, deliberately. A bandpass would have been convenient -- one
|
||||||
|
// direct filter carrying both the authored brightness model and a bass trim
|
||||||
|
// -- but the OpenAL this game ships (Creative's, via oalinst.exe; renderer
|
||||||
|
// reports "Generic Software") implements ONLY AL_FILTER_LOWPASS. It rejects
|
||||||
|
// both HIGHPASS and BANDPASS, verified on the build machine. Asking for one
|
||||||
|
// leaves an error pending, which the check below would read as total EFX
|
||||||
|
// failure and silently take the reverb down with it.
|
||||||
|
//
|
||||||
|
p_alGenFilters(1, &s_scratchFilter);
|
||||||
|
p_alFilteri(s_scratchFilter, AL_FILTER_TYPE, AL_FILTER_LOWPASS);
|
||||||
|
|
||||||
|
if (alGetError() != AL_NO_ERROR)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// No usable direct filter. The reverb slot above is independent of it,
|
||||||
|
// so keep the bridge alive and just make the filter path a no-op rather
|
||||||
|
// than losing F11 as well.
|
||||||
|
//
|
||||||
|
s_scratchFilter = 0;
|
||||||
|
Tell("L4AUDEFX: no lowpass filter available - brightness path inert\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
s_available = (alGetError() == AL_NO_ERROR);
|
||||||
|
Tell("L4AUDEFX: " << (s_available ? "ready" : "failed")
|
||||||
|
<< " (reverb slot gain " << global_reverb_scale << ")\n");
|
||||||
|
return s_available;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EFX_SetSourceLowpassGainHF(ALuint source, float gainhf)
|
||||||
|
{
|
||||||
|
if (!s_available || s_scratchFilter == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (gainhf < 0.001f) gainhf = 0.001f;
|
||||||
|
if (gainhf > 1.0f) gainhf = 1.0f;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Nothing to do at unity -- detach rather than attach a filter that would
|
||||||
|
// only cost mixing work to achieve nothing.
|
||||||
|
//
|
||||||
|
if (gainhf >= 0.999f)
|
||||||
|
{
|
||||||
|
alSourcei(source, AL_DIRECT_FILTER, AL_FILTER_NULL);
|
||||||
|
alGetError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Filter parameters are COPIED at attach time, so one scratch filter object
|
||||||
|
// serves every source -- no per-source filter allocation is needed.
|
||||||
|
//
|
||||||
|
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);
|
||||||
|
alGetError();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EFX_AttachReverbSend(ALuint source)
|
||||||
|
{
|
||||||
|
if (!s_available)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alSource3i(source, AL_AUXILIARY_SEND_FILTER, (ALint)s_reverbSlot, 0, AL_FILTER_NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EFX_ClearSourceEffects(ALuint source)
|
||||||
|
{
|
||||||
|
if (!s_available)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alSourcei(source, AL_DIRECT_FILTER, AL_FILTER_NULL);
|
||||||
|
alSource3i(source, AL_AUXILIARY_SEND_FILTER, AL_EFFECTSLOT_NULL, 0, AL_FILTER_NULL);
|
||||||
|
alGetError(); // swallow any property complaint
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#pragma once
|
||||||
|
//###########################################################################
|
||||||
|
//
|
||||||
|
// L4AUDEFX.h -- OpenAL EFX bridge for the authored filter/reverb chains
|
||||||
|
// (docs/SOUND.md, findings F9 and F11).
|
||||||
|
//
|
||||||
|
// The original drove the AWE32's initial-filter-cutoff NRPN (21) every frame
|
||||||
|
// -- brightness x the distance high-frequency rolloff -- and sent CC91 reverb
|
||||||
|
// on the 3D channels (global_reverb_scale=0.35 in RP's AUDIO.INI) while
|
||||||
|
// keeping the cockpit DirectPatch channels dry. The OpenAL port computed
|
||||||
|
// both and applied neither: GetHighFreqCutoffScale() had no callers at all
|
||||||
|
// and every CC91 send site sat inside a comment block, so RP played
|
||||||
|
// spectrally full-bright at every distance and bone-dry everywhere.
|
||||||
|
//
|
||||||
|
// This bridge reproduces both through OpenAL Soft's EFX extension: one
|
||||||
|
// EAXReverb auxiliary slot plus a scratch AL_FILTER_LOWPASS whose parameters
|
||||||
|
// are copied at attach time. Without ALC_EXT_EFX it stays inert and every
|
||||||
|
// entry point below is a no-op, so the game still runs on a bare OpenAL.
|
||||||
|
//
|
||||||
|
//###########################################################################
|
||||||
|
|
||||||
|
#include "openal/al.h"
|
||||||
|
|
||||||
|
//
|
||||||
|
// Load the EFX entry points, create the reverb slot (gain = the authored
|
||||||
|
// 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 filter: gainhf is the linear high-frequency gain at the
|
||||||
|
// EFX 5 kHz reference, carrying the authored brightness x distance model.
|
||||||
|
// Callers map the AWE cutoff through EFX_CutoffScaleToGainHF below.
|
||||||
|
//
|
||||||
|
// At unity the filter is detached rather than attached at no-op settings.
|
||||||
|
//
|
||||||
|
// NOTE: this is a LOWPASS and can only ever be one. The OpenAL this game ships
|
||||||
|
// (Creative's) implements no other filter type -- see L4AUDEFX.cpp -- so the
|
||||||
|
// bass trim could not ride here as a bandpass GAINLF and lives in the resource
|
||||||
|
// loader instead (RPApplyBassTrim, L4AUDRES.cpp).
|
||||||
|
//
|
||||||
|
void EFX_SetSourceLowpassGainHF(ALuint source, float gainhf);
|
||||||
|
|
||||||
|
//
|
||||||
|
// AWE NRPN 21 curve -> EFX gainhf. cutoff_scale is [0,1] of the 100-8000 Hz
|
||||||
|
// span; approximated as the attenuation of a 2-pole lowpass at the 5 kHz
|
||||||
|
// reference. Curve shape is 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 auxiliary send to the reverb slot
|
||||||
|
// (Dynamic3D / Static3D). Direct cockpit sources stay dry.
|
||||||
|
//
|
||||||
|
void EFX_AttachReverbSend(ALuint source);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Drop both the direct-path filter and the reverb send. Required when a source
|
||||||
|
// is recycled through the pool: without it a dry cockpit sound can inherit the
|
||||||
|
// wet send of the 3D source that used the name before it, and a full-bright
|
||||||
|
// source can inherit a distant source's lowpass.
|
||||||
|
//
|
||||||
|
void EFX_ClearSourceEffects(ALuint source);
|
||||||
+266
-29
@@ -2,6 +2,7 @@
|
|||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
#include "l4audio.h"
|
#include "l4audio.h"
|
||||||
|
#include "l4audefx.h"
|
||||||
#include "l4audlvl.h"
|
#include "l4audlvl.h"
|
||||||
#include "l4app.h"
|
#include "l4app.h"
|
||||||
#include "l4audrnd.h"
|
#include "l4audrnd.h"
|
||||||
@@ -9,6 +10,49 @@
|
|||||||
#include "..\munga\player.h"
|
#include "..\munga\player.h"
|
||||||
#include "..\rp\vtv.h"
|
#include "..\rp\vtv.h"
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md): the AWE32 played each patch at the requested MIDI
|
||||||
|
// note relative to the sample root (60). RP's authored 4.10 content predates
|
||||||
|
// NoteAudioControlID -- its AudioControlID enum stops at AttackTimeAudioControlID
|
||||||
|
// -- so every source runs at DEFAULT_NOTE and this factor is 1.0 today. It is
|
||||||
|
// applied anyway so the pitch path is complete if authored notes ever appear,
|
||||||
|
// and to keep the shared MUNGA engine in step with the BT tree.
|
||||||
|
//
|
||||||
|
static inline float RPNotePitchFactor(int note_value)
|
||||||
|
{
|
||||||
|
return (float)pow(2.0, ((double)note_value - 60.0) / 12.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md F12): the authored DirectPatchSource `position=`
|
||||||
|
// enum picked a SOUND CARD (front pair for Front/FrontLeft/FrontRight, rear
|
||||||
|
// pair for Rear/RearLeft/RearRight) and a MIDI pan (CC10 centre/left/right).
|
||||||
|
// The port read audioPosition from the stream and then discarded it -- every
|
||||||
|
// cockpit sound played dead centre because SetupPatch pins each source
|
||||||
|
// AL_SOURCE_RELATIVE at the origin.
|
||||||
|
//
|
||||||
|
// Sources are listener-relative and no AL_ORIENTATION is ever set, so OpenAL's
|
||||||
|
// default listener frame applies: facing -Z with +Y up. Front is therefore
|
||||||
|
// -Z, rear +Z, left -X, right +X; the corner values combine both at equal
|
||||||
|
// weight. RP's own content only ever authors Front (28 sites) and Rear (13),
|
||||||
|
// but the corners are mapped for completeness since the enum allows them.
|
||||||
|
//
|
||||||
|
static void RPGetDirectPatchPosition(DirectPatchPosition p, float *x, float *z)
|
||||||
|
{
|
||||||
|
const float diag = 0.7071068f; // unit vector split across both axes
|
||||||
|
|
||||||
|
switch (p)
|
||||||
|
{
|
||||||
|
case FrontDirectPatchPosition: *x = 0.0f; *z = -1.0f; break;
|
||||||
|
case RearDirectPatchPosition: *x = 0.0f; *z = 1.0f; break;
|
||||||
|
case FrontLeftDirectPatchPosition: *x = -diag; *z = -diag; break;
|
||||||
|
case FrontRightDirectPatchPosition: *x = diag; *z = -diag; break;
|
||||||
|
case RearLeftDirectPatchPosition: *x = -diag; *z = diag; break;
|
||||||
|
case RearRightDirectPatchPosition: *x = diag; *z = diag; break;
|
||||||
|
default: *x = 0.0f; *z = 0.0f; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//####################### L4AudioSpatialization #########################
|
//####################### L4AudioSpatialization #########################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -658,6 +702,19 @@ L4AudioSource::L4AudioSource(
|
|||||||
AudioSource(stream, entity)
|
AudioSource(stream, entity)
|
||||||
{
|
{
|
||||||
channelSet.count = GetAudioVoiceCount();
|
channelSet.count = GetAudioVoiceCount();
|
||||||
|
|
||||||
|
//
|
||||||
|
// sources[] was left uninitialized here, and RequestAudioChannels decides
|
||||||
|
// whether a slot already holds a source by asking alIsSource about it.
|
||||||
|
// Garbage that happened to match a live name meant silently sharing another
|
||||||
|
// source -- a real hazard now that the pool recycles small integer names.
|
||||||
|
// 0 is never a valid AL name.
|
||||||
|
//
|
||||||
|
for (int i = 0; i < (int)(sizeof(channelSet.sources) / sizeof(channelSet.sources[0])); i++)
|
||||||
|
{
|
||||||
|
channelSet.sources[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
L4AudioSourceX();
|
L4AudioSourceX();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -923,6 +980,22 @@ void
|
|||||||
patch_resource->SetDistance(GetDistanceToSource());
|
patch_resource->SetDistance(GetDistanceToSource());
|
||||||
patch_resource->SetupPatch(channelSet);
|
patch_resource->SetupPatch(channelSet);
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md F12): place the source per the authored position
|
||||||
|
// enum. SetupPatch has just pinned it AL_SOURCE_RELATIVE at the origin, so
|
||||||
|
// this must run after it. With AL_NONE as the distance model the unit
|
||||||
|
// radius costs no attenuation -- it only supplies direction.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
float pos_x, pos_z;
|
||||||
|
|
||||||
|
RPGetDirectPatchPosition(audioPosition, &pos_x, &pos_z);
|
||||||
|
for (int i = 0; i < channelSet.count; i++)
|
||||||
|
{
|
||||||
|
alSource3f(channelSet.sources[i], AL_POSITION, pos_x, 0.0f, pos_z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Set the channel to default control values
|
// Set the channel to default control values
|
||||||
//
|
//
|
||||||
@@ -1039,6 +1112,8 @@ void
|
|||||||
// Apply filter scale
|
// Apply filter scale
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
|
float direct_gainhf = 1.0f;
|
||||||
|
|
||||||
if (UseSourceBrightnessScale())
|
if (UseSourceBrightnessScale())
|
||||||
{
|
{
|
||||||
const MIDINRPNValue filter_resolution = 2;// HACK - should come from audio.ini
|
const MIDINRPNValue filter_resolution = 2;// HACK - should come from audio.ini
|
||||||
@@ -1058,6 +1133,32 @@ void
|
|||||||
{
|
{
|
||||||
lastMIDIFilterCutoff = midi_filter_cutoff;
|
lastMIDIFilterCutoff = midi_filter_cutoff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md F9): this block previously computed the AWE
|
||||||
|
// initial-filter-cutoff (NRPN 21) and then only updated its own
|
||||||
|
// bookkeeping member -- the cutoff was never applied to anything, so
|
||||||
|
// authored brightness (ctl 5) was inert. Route it through EFX instead.
|
||||||
|
// Direct sources take brightness alone; the distance rolloff belongs to
|
||||||
|
// the 3D paths.
|
||||||
|
//
|
||||||
|
direct_gainhf = EFX_CutoffScaleToGainHF(
|
||||||
|
(float)midi_filter_cutoff / (float)MIDI_MAX_CONTROL_VALUE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Applied OUTSIDE the brightness gate: a source that does not use brightness
|
||||||
|
// still has to be told, because the same call carries the player's bass trim.
|
||||||
|
// At unity on both axes it detaches the filter, so this costs nothing in the
|
||||||
|
// default configuration.
|
||||||
|
//
|
||||||
|
if (EFX_Available())
|
||||||
|
{
|
||||||
|
for (int i = 0; i < channelSet.count; i++)
|
||||||
|
{
|
||||||
|
EFX_SetSourceLowpassGainHF(channelSet.sources[i], direct_gainhf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -1069,17 +1170,24 @@ void
|
|||||||
const MIDIValue volume_resolution = 2; // HACK - should come from audio.ini
|
const MIDIValue volume_resolution = 2; // HACK - should come from audio.ini
|
||||||
|
|
||||||
volume_scale = CalculateSourceVolumeScale();
|
volume_scale = CalculateSourceVolumeScale();
|
||||||
L4AudioLocation *audio_location = Cast_Object(L4AudioLocation*, GetAudioLocation());
|
|
||||||
Check(application);
|
//
|
||||||
L4AudioRenderer *audio_renderer =
|
// FIDELITY (docs/SOUND.md F4): the original ended its volume path in MIDI
|
||||||
Cast_Object(L4AudioRenderer*, application->GetAudioRenderer());
|
// CC7, whose GM/SoundFont curve is concave -- amplitude ~ (v/127)^2. Writing
|
||||||
Check(audio_renderer);
|
// volume_scale linearly to AL_GAIN played every intermediate level about
|
||||||
AudioHead *audio_head = audio_renderer->GetAudioHead();
|
// +6 dB hot at mid-scale and compressed the authored dynamic range.
|
||||||
Check(audio_head);
|
//
|
||||||
|
// AL_MAX_DISTANCE is no longer written here: the distance model is AL_NONE
|
||||||
|
// (see MUNGA/AUDIO.cpp) so it has no effect, and DirectPatch is the
|
||||||
|
// non-positional cockpit path which never took distance attenuation anyway.
|
||||||
|
//
|
||||||
|
const float direct_note_pitch = RPNotePitchFactor((int)GetCurrentNoteValue());
|
||||||
|
PatchResource *direct_patch = Cast_Object(PatchResource*, GetAudioResource());
|
||||||
for (int i=0; i < channelSet.count; i++)
|
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,
|
||||||
alSourcef(channelSet.sources[i], AL_GAIN, volume_scale);
|
volume_scale * volume_scale * direct_patch->GetZoneBassGain(i));
|
||||||
|
alSourcef(channelSet.sources[i], AL_PITCH, (float)relativePitch * direct_note_pitch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1206,6 +1314,17 @@ void
|
|||||||
patch_resource->SetDistance(GetDistanceToSource());
|
patch_resource->SetDistance(GetDistanceToSource());
|
||||||
patch_resource->SetupPatch(channelSet);
|
patch_resource->SetupPatch(channelSet);
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md F11): wet exterior. The original sent CC91 =
|
||||||
|
// global_reverb_scale on all four channels of a 3D source and CC91 = 0 on
|
||||||
|
// the cockpit DirectPatch channels -- a deliberate outside/inside contrast
|
||||||
|
// that the port lost when every send site was commented out.
|
||||||
|
//
|
||||||
|
for (int i = 0; i < channelSet.count; i++)
|
||||||
|
{
|
||||||
|
EFX_AttachReverbSend(channelSet.sources[i]);
|
||||||
|
}
|
||||||
|
|
||||||
/*patch_resource->SetDistance(GetDistanceToSource());
|
/*patch_resource->SetDistance(GetDistanceToSource());
|
||||||
for (i = 0; i < AudioChannelSetSize; i++)
|
for (i = 0; i < AudioChannelSetSize; i++)
|
||||||
{
|
{
|
||||||
@@ -1405,15 +1524,71 @@ void
|
|||||||
|
|
||||||
pitch_offset = CalculateSourcePitchOffset();
|
pitch_offset = CalculateSourcePitchOffset();
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md F10): add the AUTHORED doppler. AUDIO.INI's
|
||||||
|
// doppler_range=600 / speed_of_sound=250 are computed into
|
||||||
|
// AudioLocation::dopplerCents on every spatial update, and the original
|
||||||
|
// applied it on this dynamic path only -- static and direct sources stayed
|
||||||
|
// doppler-free. GetDopplerCents() previously had no callers at all.
|
||||||
|
//
|
||||||
|
pitch_offset += GetAudioLocation()->GetDopplerCents();
|
||||||
|
|
||||||
double relativePitch = pow(2.0,pitch_offset/1200.0);
|
double relativePitch = pow(2.0,pitch_offset/1200.0);
|
||||||
Clamp(relativePitch,0.5,2.0);
|
Clamp(relativePitch,0.5,2.0);
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md): relativePitch was computed here and never
|
||||||
|
// applied -- there was no AL_PITCH call anywhere in the tree, so the whole
|
||||||
|
// authored pitch chain (pitch_mix_offset / PitchAudioControlID, authored 97
|
||||||
|
// times across RP's sequences) was inert along with doppler.
|
||||||
|
//
|
||||||
|
// AL_VELOCITY is still written for bookkeeping but is now inert: doppler
|
||||||
|
// factor is 0 (see MUNGA/AUDIO.cpp) because this feed is sign-inverted
|
||||||
|
// relative to the AL_POSITION frame and never subtracted head velocity.
|
||||||
|
// AL_MAX_DISTANCE is dropped -- the distance model is AL_NONE and the
|
||||||
|
// authored curve is applied in CalculateSourceVolumeScale instead.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md F9): the AUTHORED high-frequency rolloff. The
|
||||||
|
// original drove the AWE filter cutoff on this path from
|
||||||
|
// highFreqCutoffScale x brightnessScale, ungated, on all four quadrant
|
||||||
|
// channels -- every moving 3D sound got duller with distance. AUDIO.INI
|
||||||
|
// still computes highFreqCutoffScale each frame (rolloff 2.0, knee 60,
|
||||||
|
// scale 0.005) and GetHighFreqCutoffScale() previously had zero callers.
|
||||||
|
//
|
||||||
|
float dynamic_gainhf = 1.0f;
|
||||||
|
|
||||||
|
if (EFX_Available())
|
||||||
|
{
|
||||||
|
PatchResource *filter_patch =
|
||||||
|
Cast_Object(PatchResource*, GetAudioResource());
|
||||||
|
Check(filter_patch);
|
||||||
|
|
||||||
|
Scalar filter_scale =
|
||||||
|
GetAudioLocation()->GetHighFreqCutoffScale() *
|
||||||
|
CalculateSourceBrightnessScale();
|
||||||
|
Scalar max_cutoff = (Scalar)filter_patch->GetMaxMIDIFilterCutoff();
|
||||||
|
Scalar midi_cutoff = filter_scale * max_cutoff;
|
||||||
|
|
||||||
|
dynamic_gainhf = EFX_CutoffScaleToGainHF(
|
||||||
|
(float)(midi_cutoff / (Scalar)MIDI_MAX_CONTROL_VALUE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float dynamic_note_pitch = RPNotePitchFactor((int)GetCurrentNoteValue());
|
||||||
|
PatchResource *dynamic_patch = Cast_Object(PatchResource*, GetAudioResource());
|
||||||
for (int i=0; i < channelSet.count; i++)
|
for (int i=0; i < channelSet.count; i++)
|
||||||
{
|
{
|
||||||
alSource3f(channelSet.sources[i],AL_POSITION,pos.x,pos.y,pos.z);
|
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 * dynamic_patch->GetZoneBassGain(i));
|
||||||
|
alSourcef(channelSet.sources[i], AL_PITCH, (float)relativePitch * dynamic_note_pitch);
|
||||||
alSource3f(channelSet.sources[i],AL_VELOCITY,-relative_velocity.x,-relative_velocity.y,-relative_velocity.z);
|
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));
|
|
||||||
|
if (EFX_Available())
|
||||||
|
{
|
||||||
|
EFX_SetSourceLowpassGainHF(channelSet.sources[i], dynamic_gainhf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1432,22 +1607,25 @@ AudioControlValue
|
|||||||
//
|
//
|
||||||
Scalar
|
Scalar
|
||||||
volume_scale = L4AudioSource::CalculateSourceVolumeScale();
|
volume_scale = L4AudioSource::CalculateSourceVolumeScale();
|
||||||
return volume_scale;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Update the spatial model that will result in the value
|
// FIDELITY (docs/SOUND.md F3): apply the AUTHORED distance attenuation.
|
||||||
// for distance related volume attenuation
|
// AUDIO.INI's knee/rolloff curve (amplitude_rolloff=2.0, knee=60,
|
||||||
|
// distance_scale=0.003, clipping_radius=550) is computed into
|
||||||
|
// distanceVolumeScale on every spatial update; this multiply was commented
|
||||||
|
// out behind an early return and AL_LINEAR_DISTANCE substituted, which faded
|
||||||
|
// distant audio on a straight line to zero instead of the authored
|
||||||
|
// 1/(1+(k(d-knee))^2). Restoring it also un-blinds the volume-based
|
||||||
|
// transient cull, the AudioWeighting voice-steal, and the CalculateMix
|
||||||
|
// ducking chain, all of which key off this value and were treating far
|
||||||
|
// sources as full-presence.
|
||||||
//
|
//
|
||||||
/*Check(application);
|
// The spatial model is already refreshed each Execute, so the
|
||||||
Check(application->GetAudioRenderer());
|
// UpdateSpatialModel call the original comment carried is not needed here.
|
||||||
UpdateSpatialModel(application->GetAudioRenderer()->GetAudioHead());
|
|
||||||
|
|
||||||
//
|
|
||||||
// Apply distance attenuation to the volume scale
|
|
||||||
//
|
//
|
||||||
Check(GetAudioLocation());
|
Check(GetAudioLocation());
|
||||||
volume_scale *= GetAudioLocation()->GetDistanceVolumeScale();
|
volume_scale *= GetAudioLocation()->GetDistanceVolumeScale();
|
||||||
return volume_scale;*/
|
return volume_scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -1468,6 +1646,26 @@ Static3DPatchSource::Static3DPatchSource(
|
|||||||
MemoryStream_Read(stream, &useInternalSpatialization);
|
MemoryStream_Read(stream, &useInternalSpatialization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
AudioControlValue
|
||||||
|
Static3DPatchSource::CalculateSourceVolumeScale()
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md F3): same authored distance attenuation as the
|
||||||
|
// dynamic path. The spatial model computes distanceVolumeScale on every
|
||||||
|
// execute; without this multiply statics were left to AL_LINEAR_DISTANCE.
|
||||||
|
//
|
||||||
|
Scalar volume_scale = L4AudioSource::CalculateSourceVolumeScale();
|
||||||
|
Check(GetAudioLocation());
|
||||||
|
volume_scale *= GetAudioLocation()->GetDistanceVolumeScale();
|
||||||
|
return volume_scale;
|
||||||
|
}
|
||||||
|
|
||||||
Logical Static3DPatchSource::IsAudioSourceClipped(AudioHead *audio_head)
|
Logical Static3DPatchSource::IsAudioSourceClipped(AudioHead *audio_head)
|
||||||
{
|
{
|
||||||
if (AudioSource::IsAudioSourceClipped(audio_head) || l4_application->GetMissionPlayer()->GetPlayerVehicle()->GetSimulationState() == VTV::BurningState)
|
if (AudioSource::IsAudioSourceClipped(audio_head) || l4_application->GetMissionPlayer()->GetPlayerVehicle()->GetSimulationState() == VTV::BurningState)
|
||||||
@@ -1694,6 +1892,16 @@ void
|
|||||||
Check(patch_resource);
|
Check(patch_resource);
|
||||||
patch_resource->SetDistance(GetDistanceToSource());
|
patch_resource->SetDistance(GetDistanceToSource());
|
||||||
patch_resource->SetupPatch(channelSet);
|
patch_resource->SetupPatch(channelSet);
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md F11): statics are exterior sources too, so they
|
||||||
|
// take the same wet send as the dynamic path.
|
||||||
|
//
|
||||||
|
for (int i = 0; i < channelSet.count; i++)
|
||||||
|
{
|
||||||
|
EFX_AttachReverbSend(channelSet.sources[i]);
|
||||||
|
}
|
||||||
|
|
||||||
/*for (i = 0; i < AudioChannelSetSize; i++)
|
/*for (i = 0; i < AudioChannelSetSize; i++)
|
||||||
{
|
{
|
||||||
if ((channel = channelSet.GetNth(i)) != NULL)
|
if ((channel = channelSet.GetNth(i)) != NULL)
|
||||||
@@ -1909,12 +2117,6 @@ void
|
|||||||
|
|
||||||
Scalar volume_scale = CalculateSourceVolumeScale();
|
Scalar volume_scale = CalculateSourceVolumeScale();
|
||||||
L4AudioLocation *audio_location = Cast_Object(L4AudioLocation*, GetAudioLocation());
|
L4AudioLocation *audio_location = Cast_Object(L4AudioLocation*, GetAudioLocation());
|
||||||
Check(application);
|
|
||||||
L4AudioRenderer *audio_renderer =
|
|
||||||
Cast_Object(L4AudioRenderer*, application->GetAudioRenderer());
|
|
||||||
Check(audio_renderer);
|
|
||||||
AudioHead *audio_head = audio_renderer->GetAudioHead();
|
|
||||||
Check(audio_head);
|
|
||||||
|
|
||||||
Scalar pitch_offset;
|
Scalar pitch_offset;
|
||||||
|
|
||||||
@@ -1933,12 +2135,47 @@ void
|
|||||||
relative_position = audio_location->GetVectorToSource();
|
relative_position = audio_location->GetVectorToSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md F4 + pitch): squared CC7 volume law, and the
|
||||||
|
// authored pitch chain applied -- see the DirectPatch/Dynamic3D paths. The
|
||||||
|
// original left static sources doppler-free, so no doppler term here.
|
||||||
|
// AL_MAX_DISTANCE dropped with the AL_NONE distance model; the authored
|
||||||
|
// curve is applied in CalculateSourceVolumeScale.
|
||||||
|
//
|
||||||
//Static models have their position freely available as relative positions and stand still
|
//Static models have their position freely available as relative positions and stand still
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md F9): statics took brightness alone in the
|
||||||
|
// original -- no distance term on this path.
|
||||||
|
//
|
||||||
|
float static_gainhf = 1.0f;
|
||||||
|
|
||||||
|
if (EFX_Available() && UseSourceBrightnessScale())
|
||||||
|
{
|
||||||
|
PatchResource *filter_patch =
|
||||||
|
Cast_Object(PatchResource*, GetAudioResource());
|
||||||
|
Check(filter_patch);
|
||||||
|
|
||||||
|
Scalar midi_cutoff =
|
||||||
|
CalculateSourceBrightnessScale() *
|
||||||
|
(Scalar)filter_patch->GetMaxMIDIFilterCutoff();
|
||||||
|
|
||||||
|
static_gainhf = EFX_CutoffScaleToGainHF(
|
||||||
|
(float)(midi_cutoff / (Scalar)MIDI_MAX_CONTROL_VALUE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float static_note_pitch = RPNotePitchFactor((int)GetCurrentNoteValue());
|
||||||
for (int i=0; i < channelSet.count; i++)
|
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 * patch_resource->GetZoneBassGain(i));
|
||||||
|
alSourcef(channelSet.sources[i], AL_PITCH, (float)relativePitch * static_note_pitch);
|
||||||
alSource3f(channelSet.sources[i],AL_POSITION,relative_position.x,relative_position.y,relative_position.z);
|
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));
|
|
||||||
|
if (EFX_Available())
|
||||||
|
{
|
||||||
|
EFX_SetSourceLowpassGainHF(channelSet.sources[i], static_gainhf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -541,6 +541,13 @@ public:
|
|||||||
|
|
||||||
virtual Logical IsAudioSourceClipped(AudioHead *audio_head);
|
virtual Logical IsAudioSourceClipped(AudioHead *audio_head);
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// Mix levels
|
||||||
|
//
|
||||||
|
public:
|
||||||
|
AudioControlValue
|
||||||
|
CalculateSourceVolumeScale();
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// SetPosition
|
// SetPosition
|
||||||
//
|
//
|
||||||
|
|||||||
+34
-1
@@ -128,8 +128,25 @@ void
|
|||||||
// #endif
|
// #endif
|
||||||
SAMPLEINFO info;
|
SAMPLEINFO info;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Ask this patch for no more zones than it has.
|
||||||
|
//
|
||||||
|
// sourceSet.count was fixed when the audio source was built, from
|
||||||
|
// whichever level of detail was selected at the time. SetDistance
|
||||||
|
// re-picks the level of detail by distance immediately before this
|
||||||
|
// runs (see Static3DPatchSource::StartImplementation), and a
|
||||||
|
// further-away patch can have fewer zones than the one the source was
|
||||||
|
// sized for - so the count outruns this patch's zone list, and the
|
||||||
|
// zones past the end come back as "no such zone".
|
||||||
|
//
|
||||||
|
int zone_count = PRESET_getNumSamples(bankID,patchID);
|
||||||
|
if (zone_count > sourceSet.count)
|
||||||
|
{
|
||||||
|
zone_count = sourceSet.count;
|
||||||
|
}
|
||||||
|
|
||||||
//Attach buffers
|
//Attach buffers
|
||||||
for (int i=0; i < sourceSet.count; i++)
|
for (int i=0; i < zone_count; i++)
|
||||||
{
|
{
|
||||||
info = PRESET_getSampleInfo(bankID,patchID,i);
|
info = PRESET_getSampleInfo(bankID,patchID,i);
|
||||||
if (info.bufferIndex >= 0)
|
if (info.bufferIndex >= 0)
|
||||||
@@ -310,3 +327,19 @@ MIDINRPNValue
|
|||||||
Check(patch_level_of_detail);
|
Check(patch_level_of_detail);
|
||||||
return patch_level_of_detail->GetMaxMIDIFilterCutoff();
|
return patch_level_of_detail->GetMaxMIDIFilterCutoff();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//#############################################################################
|
||||||
|
//#############################################################################
|
||||||
|
//
|
||||||
|
float
|
||||||
|
PatchResource::GetZoneBassGain(int zone_index)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
PatchLevelOfDetail *patch_level_of_detail =
|
||||||
|
Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail());
|
||||||
|
|
||||||
|
Check(patch_level_of_detail);
|
||||||
|
return patch_level_of_detail->GetZoneBassGain(zone_index);
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ struct PRESETINFO
|
|||||||
|
|
||||||
extern PRESETINFO allPresets[2][100];
|
extern PRESETINFO allPresets[2][100];
|
||||||
|
|
||||||
|
//
|
||||||
|
// Defined in L4AUDRES.cpp; declared here rather than including that header so
|
||||||
|
// the level-of-detail and resource headers stay independent of each other.
|
||||||
|
//
|
||||||
|
float RPBufferBassGain(int buffer_index);
|
||||||
|
|
||||||
bool PRESET_isImplemented(int bank, int preset);
|
bool PRESET_isImplemented(int bank, int preset);
|
||||||
int PRESET_getNumSamples(int bank, int preset);
|
int PRESET_getNumSamples(int bank, int preset);
|
||||||
SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd);
|
SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd);
|
||||||
@@ -69,6 +75,14 @@ public:
|
|||||||
GetVoiceCount()
|
GetVoiceCount()
|
||||||
{return PRESET_getNumSamples(bankID,patchID);}
|
{return PRESET_getNumSamples(bankID,patchID);}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Gain this zone takes from the Home/End bass trim, 1.0 when untouched.
|
||||||
|
//
|
||||||
|
float
|
||||||
|
GetZoneBassGain(int zone_index)
|
||||||
|
{return RPBufferBassGain(
|
||||||
|
PRESET_getSampleInfo(bankID,patchID,zone_index).bufferIndex);}
|
||||||
|
|
||||||
//
|
//
|
||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
// BuildFromPage
|
// BuildFromPage
|
||||||
@@ -163,6 +177,12 @@ public:
|
|||||||
void
|
void
|
||||||
SetupPatch(SourceSet sourceSet);
|
SetupPatch(SourceSet sourceSet);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Gain this zone takes from the Home/End bass trim, 1.0 when untouched.
|
||||||
|
//
|
||||||
|
float
|
||||||
|
GetZoneBassGain(int zone_index);
|
||||||
|
|
||||||
MIDINRPNValue
|
MIDINRPNValue
|
||||||
GetMaxMIDIFilterCutoff();
|
GetMaxMIDIFilterCutoff();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,130 @@
|
|||||||
ALuint *g_buffers;
|
ALuint *g_buffers;
|
||||||
int g_numBuffers;
|
int g_numBuffers;
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Bass trim ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// RP412AUDIOBASS, 0.0..1.0, default 1.0 (the mix exactly as authored), stepped
|
||||||
|
// live by the Home/End keys.
|
||||||
|
//
|
||||||
|
// The arcade pod ran the game at unity and did its volume and tone shaping in
|
||||||
|
// hardware -- an external amplifier and a 3-way crossover. A desktop player has
|
||||||
|
// neither, so the low band needs a control in software. This is the crossover's
|
||||||
|
// low trim; the master volume (L4AUDRND.cpp) is the amplifier's.
|
||||||
|
//
|
||||||
|
// It cannot be an EFX filter: the OpenAL this game ships implements only
|
||||||
|
// AL_FILTER_LOWPASS, so there is no low shelf or bandpass to lean on, and the
|
||||||
|
// one direct filter a source gets is already carrying the authored brightness
|
||||||
|
// model. So the trim is a GAIN, applied per zone in the mix.
|
||||||
|
//
|
||||||
|
// That works because of HOW the low end is built. RP's soundbanks carry their
|
||||||
|
// weight in discrete deep layer zones whose per-zone tuning bakes out to a very
|
||||||
|
// low playback rate -- 13 zones sit below 8 kHz, between 3.4 and 5.2 octaves
|
||||||
|
// below their recorded pitch, against 81% of the set at 22 kHz and up. A zone's
|
||||||
|
// baked rate is therefore a reliable proxy for which band it occupies, so
|
||||||
|
// attenuating the low-rate zones is a genuine low-band trim rather than a blunt
|
||||||
|
// overall cut.
|
||||||
|
//
|
||||||
|
// Ramp: untouched at or above 22050 Hz, full trim at or below 5512 Hz, log
|
||||||
|
// interpolated between, so nothing steps abruptly at a threshold. Each buffer's
|
||||||
|
// DEPTH is fixed at load; the trim itself is read at mix time, which is what
|
||||||
|
// lets the keys move it while sounds are playing.
|
||||||
|
//
|
||||||
|
static const ALsizei kBassTrimFullRate = 5512; // at/below: full trim
|
||||||
|
static const ALsizei kBassTrimNoneRate = 22050; // at/above: untouched
|
||||||
|
static const char kBassTrimFile[] = "bass.cfg";
|
||||||
|
static const float kBassTrimStep = 0.05f;
|
||||||
|
|
||||||
|
static float *g_bufferBassDepth = NULL; // one per loaded buffer
|
||||||
|
static float g_bassTrim = 1.0f;
|
||||||
|
|
||||||
|
//
|
||||||
|
// How much of the trim a buffer at this rate takes: 0 = untouched, 1 = fully.
|
||||||
|
//
|
||||||
|
static float
|
||||||
|
RPBassDepthForRate(ALsizei rate)
|
||||||
|
{
|
||||||
|
if (rate >= kBassTrimNoneRate) return 0.0f;
|
||||||
|
if (rate <= kBassTrimFullRate) return 1.0f;
|
||||||
|
|
||||||
|
const float span = (float)log((double)kBassTrimNoneRate / (double)kBassTrimFullRate);
|
||||||
|
|
||||||
|
return (float)log((double)kBassTrimNoneRate / (double)rate) / span;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RPBassTrimInitialize()
|
||||||
|
{
|
||||||
|
g_bassTrim = 1.0f;
|
||||||
|
|
||||||
|
if (const char *setting = getenv("RP412AUDIOBASS"))
|
||||||
|
{
|
||||||
|
float value = (float)atof(setting);
|
||||||
|
|
||||||
|
if (value >= 0.0f && value <= 1.0f)
|
||||||
|
{
|
||||||
|
g_bassTrim = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Whatever the player last set with the keys wins, exactly as the master
|
||||||
|
// volume behaves -- environ.ini only decides where an untouched machine
|
||||||
|
// starts out.
|
||||||
|
//
|
||||||
|
if (FILE *cfg = fopen(kBassTrimFile, "rt"))
|
||||||
|
{
|
||||||
|
float value = -1.0f;
|
||||||
|
|
||||||
|
if (fscanf(cfg, "%f", &value) == 1 && value >= 0.0f && value <= 1.0f)
|
||||||
|
{
|
||||||
|
g_bassTrim = value;
|
||||||
|
}
|
||||||
|
fclose(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tell("Audio bass trim " << (int)(g_bassTrim * 100.0f + 0.5f) << "%\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RPBassTrimStep(int direction)
|
||||||
|
{
|
||||||
|
g_bassTrim += (direction > 0) ? kBassTrimStep : -kBassTrimStep;
|
||||||
|
|
||||||
|
if (g_bassTrim < 0.0f) g_bassTrim = 0.0f;
|
||||||
|
if (g_bassTrim > 1.0f) g_bassTrim = 1.0f;
|
||||||
|
|
||||||
|
g_bassTrim = (float)((int)(g_bassTrim / kBassTrimStep + 0.5f)) * kBassTrimStep;
|
||||||
|
|
||||||
|
if (FILE *cfg = fopen(kBassTrimFile, "wt"))
|
||||||
|
{
|
||||||
|
fprintf(cfg, "%.2f\n", g_bassTrim);
|
||||||
|
fclose(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tell("Audio bass trim " << (int)(g_bassTrim * 100.0f + 0.5f) << "%\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
float
|
||||||
|
RPBassTrim()
|
||||||
|
{
|
||||||
|
return g_bassTrim;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The gain a zone takes at the current trim. 1.0 whenever the player has not
|
||||||
|
// touched it, so the default costs one multiply by one.
|
||||||
|
//
|
||||||
|
float
|
||||||
|
RPBufferBassGain(int buffer_index)
|
||||||
|
{
|
||||||
|
if (g_bassTrim >= 0.999f || g_bufferBassDepth == NULL
|
||||||
|
|| buffer_index < 0 || buffer_index >= g_numBuffers)
|
||||||
|
{
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
return 1.0f - (1.0f - g_bassTrim) * g_bufferBassDepth[buffer_index];
|
||||||
|
}
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//####################### AudioObjectStream #############################
|
//####################### AudioObjectStream #############################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -566,6 +690,18 @@ void
|
|||||||
g_buffers = NULL;
|
g_buffers = NULL;
|
||||||
g_numBuffers = 0;
|
g_numBuffers = 0;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Parallel to g_buffers: how much of the bass trim each zone takes.
|
||||||
|
//
|
||||||
|
RPBassTrimInitialize();
|
||||||
|
g_bufferBassDepth = new float[g_numBuffers];
|
||||||
|
for (int b = 0; b < g_numBuffers; b++)
|
||||||
|
{
|
||||||
|
g_bufferBassDepth[b] = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int bufferInd = 0;
|
int bufferInd = 0;
|
||||||
@@ -647,6 +783,15 @@ void
|
|||||||
sf_read_raw(file,data,size);
|
sf_read_raw(file,data,size);
|
||||||
sf_close(file);
|
sf_close(file);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Record which band this zone sits in, for the Home/End bass
|
||||||
|
// trim. Fixed per buffer; the trim itself is read at mix time.
|
||||||
|
//
|
||||||
|
if (g_bufferBassDepth != NULL)
|
||||||
|
{
|
||||||
|
g_bufferBassDepth[bufferInd] = RPBassDepthForRate(alSampleRate);
|
||||||
|
}
|
||||||
|
|
||||||
//Feed the buffer
|
//Feed the buffer
|
||||||
alBufferData(g_buffers[bufferInd],format,data,size,alSampleRate);
|
alBufferData(g_buffers[bufferInd],format,data,size,alSampleRate);
|
||||||
PRESET_setBufferIndex(i,j,k,bufferInd);
|
PRESET_setBufferIndex(i,j,k,bufferInd);
|
||||||
@@ -726,6 +871,16 @@ void
|
|||||||
|
|
||||||
ALuint AL_getBuffer(int index)
|
ALuint AL_getBuffer(int index)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// 0 is AL_NONE - "no buffer" - which alSourcei accepts and which detaches
|
||||||
|
// the source rather than crashing. An index that is out of range means a
|
||||||
|
// zone that does not exist, and the only thing an unchecked lookup here
|
||||||
|
// can do about it is read whatever lies past the array.
|
||||||
|
//
|
||||||
|
if (g_buffers == NULL || index < 0 || index >= g_numBuffers)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
return g_buffers[index];
|
return g_buffers[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ ALuint AL_getBuffer(int index);
|
|||||||
extern ALuint *g_buffers;
|
extern ALuint *g_buffers;
|
||||||
extern int g_numBuffers;
|
extern int g_numBuffers;
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412AUDIOBASS low-band trim, stepped live by the Home/End keys. Applied as
|
||||||
|
// a per-zone gain in the mix; see the comment block in L4AUDRES.cpp for why it
|
||||||
|
// lives here and not in EFX.
|
||||||
|
//
|
||||||
|
void RPBassTrimInitialize();
|
||||||
|
void RPBassTrimStep(int direction);
|
||||||
|
float RPBassTrim();
|
||||||
|
float RPBufferBassGain(int buffer_index);
|
||||||
|
|
||||||
|
|
||||||
//class AudioHardware;
|
//class AudioHardware;
|
||||||
|
|
||||||
|
|||||||
+308
-25
@@ -2,9 +2,20 @@
|
|||||||
#pragma hdrstop
|
#pragma hdrstop
|
||||||
|
|
||||||
#include "l4audrnd.h"
|
#include "l4audrnd.h"
|
||||||
|
#include "l4audefx.h"
|
||||||
#include "..\munga\notation.h"
|
#include "..\munga\notation.h"
|
||||||
#include "openal/alc.h"
|
#include "openal/alc.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
//
|
||||||
|
// Master volume limits, shared by the startup load and the PgUp/PgDn step.
|
||||||
|
// The file sits beside the exe with the other runtime state.
|
||||||
|
//
|
||||||
|
static const char kAudioVolumeFile[] = "volume.cfg";
|
||||||
|
static const float kAudioVolumeStep = 0.05f;
|
||||||
|
static const float kAudioVolumeMax = 2.0f;
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// L4AudioRenderer
|
// L4AudioRenderer
|
||||||
@@ -379,6 +390,66 @@ void
|
|||||||
{
|
{
|
||||||
ALCcontext *context = alcCreateContext(device,NULL);
|
ALCcontext *context = alcCreateContext(device,NULL);
|
||||||
alcMakeContextCurrent(context);
|
alcMakeContextCurrent(context);
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIDELITY (docs/SOUND.md F9/F11): bring up the EFX bridge that carries
|
||||||
|
// the authored brightness/distance lowpass and the wet-exterior reverb
|
||||||
|
// send. Needs the context current, and the reverb gain has already been
|
||||||
|
// read from AUDIO.INI into the head above. Inert without ALC_EXT_EFX.
|
||||||
|
//
|
||||||
|
EFX_Initialize(audio_head->GetGlobalReverbScale());
|
||||||
|
|
||||||
|
//
|
||||||
|
// Master volume. There was no listener gain at all before -- the mix
|
||||||
|
// always ran at unity -- so restoring the authored dynamics gave players
|
||||||
|
// no way to pull the whole thing down. This lives in environ.ini rather
|
||||||
|
// than AUDIO.INI deliberately: AUDIO.INI is byte-identical to the file
|
||||||
|
// that shipped in 1995 and is worth keeping that way.
|
||||||
|
//
|
||||||
|
// Default is 1.0, i.e. exactly the previous behaviour -- the knob only
|
||||||
|
// does something when someone asks for it.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
float master_volume = 1.0f;
|
||||||
|
|
||||||
|
if (const char *setting = getenv("RP412AUDIOVOLUME"))
|
||||||
|
{
|
||||||
|
float value = (float)atof(setting);
|
||||||
|
|
||||||
|
if (value >= 0.0f && value <= kAudioVolumeMax)
|
||||||
|
{
|
||||||
|
master_volume = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Whatever the player last set with the volume keys wins over the
|
||||||
|
// environ.ini figure: the keys are the amplifier knob, and a knob
|
||||||
|
// stays where it was left. environ.ini sets where it starts on a
|
||||||
|
// machine that has never been touched.
|
||||||
|
//
|
||||||
|
if (FILE *cfg = fopen(kAudioVolumeFile, "rt"))
|
||||||
|
{
|
||||||
|
float value = -1.0f;
|
||||||
|
|
||||||
|
if (fscanf(cfg, "%f", &value) == 1
|
||||||
|
&& value >= 0.0f && value <= kAudioVolumeMax)
|
||||||
|
{
|
||||||
|
master_volume = value;
|
||||||
|
}
|
||||||
|
fclose(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
gRPMasterVolume = master_volume;
|
||||||
|
alListenerf(AL_GAIN, master_volume);
|
||||||
|
Tell("Audio master volume " << (int)(master_volume * 100.0f + 0.5f) << "%\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The bass trim is not set here: it is a per-zone gain owned by the
|
||||||
|
// resource manager (L4AUDRES.cpp), which needs the buffers to exist
|
||||||
|
// first. PreloadResources initialises it below.
|
||||||
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -1257,6 +1328,190 @@ Logical
|
|||||||
return resources_available;
|
return resources_available;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Master volume ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// The pod ran at unity and left volume to an external amplifier, so the game
|
||||||
|
// never had a level control. Standing in for that amplifier means the player
|
||||||
|
// needs to reach it while playing, not only through environ.ini -- hence the
|
||||||
|
// PgUp/PgDn binding in L4Application::KeyCommandMessageHandler.
|
||||||
|
//
|
||||||
|
// Page keys specifically: they produce no typed character, so they cannot
|
||||||
|
// collide with any of the engine's character-keyed commands the way '+'/'-'
|
||||||
|
// would, they are bound to nothing in any RP layout, and they exist on
|
||||||
|
// tenkeyless keyboards.
|
||||||
|
//
|
||||||
|
float gRPMasterVolume = 1.0f;
|
||||||
|
|
||||||
|
void
|
||||||
|
RPAudioMasterVolumeStep(int direction)
|
||||||
|
{
|
||||||
|
gRPMasterVolume += (direction > 0) ? kAudioVolumeStep : -kAudioVolumeStep;
|
||||||
|
|
||||||
|
if (gRPMasterVolume < 0.0f) gRPMasterVolume = 0.0f;
|
||||||
|
if (gRPMasterVolume > kAudioVolumeMax) gRPMasterVolume = kAudioVolumeMax;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Snap to the step grid so repeated presses cannot drift on float error and
|
||||||
|
// land somewhere that never reads back as a round number.
|
||||||
|
//
|
||||||
|
gRPMasterVolume =
|
||||||
|
(float)((int)(gRPMasterVolume / kAudioVolumeStep + 0.5f)) * kAudioVolumeStep;
|
||||||
|
|
||||||
|
alListenerf(AL_GAIN, gRPMasterVolume);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Persist immediately. A pod operator setting the level expects it to still
|
||||||
|
// be there after the cabinet is power-cycled, and there is no settings UI to
|
||||||
|
// hang it off.
|
||||||
|
//
|
||||||
|
if (FILE *cfg = fopen(kAudioVolumeFile, "wt"))
|
||||||
|
{
|
||||||
|
fprintf(cfg, "%.2f\n", gRPMasterVolume);
|
||||||
|
fclose(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tell("Audio master volume " << (int)(gRPMasterVolume * 100.0f + 0.5f) << "%\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OpenAL source pool ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
// Sources are expensive to create and destroy and are a HARD per-context
|
||||||
|
// resource (this driver grants 256 mono). Generating one per sound event and
|
||||||
|
// deleting it on release burns through that ceiling during busy play even
|
||||||
|
// though steady-state demand is modest, which shows up as sounds silently
|
||||||
|
// failing to start. Generate once, recycle forever.
|
||||||
|
//
|
||||||
|
// The cap sits below the driver grant with a reserve, so growth stops on our
|
||||||
|
// terms rather than on an alGenSources failure. Growth also stops by itself if
|
||||||
|
// a driver offers fewer sources than the cap -- a failed generate simply ends
|
||||||
|
// growth and the pool recycles what it already has.
|
||||||
|
//
|
||||||
|
static const int kAudioPoolMax = 512; // free-list array size
|
||||||
|
static const int kAudioPoolCap = 240; // grow no further than this
|
||||||
|
|
||||||
|
static ALuint gAudioPoolFree[kAudioPoolMax];
|
||||||
|
static int gAudioPoolFreeCount = 0; // entries parked in gAudioPoolFree
|
||||||
|
static int gAudioPoolTotal = 0; // sources ever generated (<= cap)
|
||||||
|
static long gAudioPoolReuses = 0; // diagnostics
|
||||||
|
|
||||||
|
int RPAudioPoolSize() { return gAudioPoolTotal; }
|
||||||
|
int RPAudioPoolFree() { return gAudioPoolFreeCount; }
|
||||||
|
long RPAudioPoolReuses() { return gAudioPoolReuses; }
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reset a source to a neutral state so nothing carries across owners.
|
||||||
|
//
|
||||||
|
static void
|
||||||
|
RPAudioScrubSource(ALuint src)
|
||||||
|
{
|
||||||
|
ALint state = AL_STOPPED;
|
||||||
|
|
||||||
|
alGetSourcei(src, AL_SOURCE_STATE, &state);
|
||||||
|
if (state == AL_PLAYING || state == AL_PAUSED)
|
||||||
|
{
|
||||||
|
alSourceStop(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
alSourcei(src, AL_BUFFER, 0); // detach (nothing is queued here)
|
||||||
|
alSourcei(src, AL_LOOPING, AL_FALSE); // or the next owner inherits a loop
|
||||||
|
alSourcef(src, AL_GAIN, 1.0f);
|
||||||
|
alSourcef(src, AL_PITCH, 1.0f);
|
||||||
|
alSourcei(src, AL_SOURCE_RELATIVE, AL_FALSE);
|
||||||
|
alSource3f(src, AL_POSITION, 0.0f, 0.0f, 0.0f);
|
||||||
|
alSource3f(src, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Drop the EFX state too. Without this a recycled name can carry a 3D
|
||||||
|
// source's reverb send into a dry cockpit sound, or a distant source's
|
||||||
|
// lowpass into a close one.
|
||||||
|
//
|
||||||
|
EFX_ClearSourceEffects(src);
|
||||||
|
|
||||||
|
alGetError(); // swallow any property complaint
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Hand out a source: recycle first, generate only while under the cap.
|
||||||
|
// False means genuinely out, and the caller retries after the steal loop runs.
|
||||||
|
//
|
||||||
|
Logical
|
||||||
|
RPAudioPoolAcquire(ALuint *out)
|
||||||
|
{
|
||||||
|
Check_Pointer(out);
|
||||||
|
|
||||||
|
while (gAudioPoolFreeCount > 0)
|
||||||
|
{
|
||||||
|
ALuint src = gAudioPoolFree[--gAudioPoolFreeCount];
|
||||||
|
|
||||||
|
if (alIsSource(src)) // a context reset invalidates names
|
||||||
|
{
|
||||||
|
++gAudioPoolReuses;
|
||||||
|
*out = src;
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
--gAudioPoolTotal; // stale name: forget it
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gAudioPoolTotal >= kAudioPoolCap)
|
||||||
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
|
||||||
|
ALuint src = 0;
|
||||||
|
|
||||||
|
alGetError();
|
||||||
|
alGenSources(1, &src);
|
||||||
|
if (alGetError() != AL_NO_ERROR || !alIsSource(src))
|
||||||
|
{
|
||||||
|
return False; // driver said no before our cap
|
||||||
|
}
|
||||||
|
|
||||||
|
++gAudioPoolTotal;
|
||||||
|
|
||||||
|
#if DEBUG_LEVEL>0
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// One line per high-water band, so a log shows how close real play gets
|
||||||
|
// to the ceiling without spamming.
|
||||||
|
//
|
||||||
|
static int s_notified = 0;
|
||||||
|
|
||||||
|
if (gAudioPoolTotal >= s_notified + 25)
|
||||||
|
{
|
||||||
|
s_notified = gAudioPoolTotal;
|
||||||
|
Tell("Audio source pool high-water: " << gAudioPoolTotal
|
||||||
|
<< " of " << kAudioPoolCap << "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
*out = src;
|
||||||
|
return True;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Take a source back. Scrubbed and parked, never deleted.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
RPAudioPoolRelease(ALuint src)
|
||||||
|
{
|
||||||
|
if (!alIsSource(src))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RPAudioScrubSource(src);
|
||||||
|
|
||||||
|
if (gAudioPoolFreeCount < kAudioPoolMax)
|
||||||
|
{
|
||||||
|
gAudioPoolFree[gAudioPoolFreeCount++] = src;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alDeleteSources(1, &src); // unreachable: cap < array size
|
||||||
|
--gAudioPoolTotal;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// RequestAudioChannels
|
// RequestAudioChannels
|
||||||
@@ -1271,32 +1526,56 @@ Logical
|
|||||||
Check(this);
|
Check(this);
|
||||||
Check(source_request);
|
Check(source_request);
|
||||||
|
|
||||||
//Do we have enough?
|
//
|
||||||
|
// SOURCE POOLING (docs/SOUND.md). This used to alGenSources per sound
|
||||||
|
// event, with ReleaseSourceSet alDeleteSources'ing on release -- so play
|
||||||
|
// activity CHURNED through OpenAL's per-context source limit (the driver
|
||||||
|
// grants 256 mono here). Recovering the soundbanks took the voice count
|
||||||
|
// per sound from about 1.1 zones to about 2.6, roughly doubling that churn.
|
||||||
|
//
|
||||||
|
// The BT tree measured this exact problem: raising the budget was NOT the
|
||||||
|
// fix, recycling was, and it was a net CPU win besides. Sources are now
|
||||||
|
// generated once and handed back to a free list, so steady-state play costs
|
||||||
|
// no allocation at all.
|
||||||
|
//
|
||||||
int requested = source_request->count;
|
int requested = source_request->count;
|
||||||
|
|
||||||
bool failed = true;
|
if (requested > (int)(sizeof(source_request->sources) / sizeof(source_request->sources[0])))
|
||||||
|
{
|
||||||
alGetError();
|
requested = (int)(sizeof(source_request->sources) / sizeof(source_request->sources[0]));
|
||||||
|
source_request->count = requested;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < requested; i++)
|
for (int i = 0; i < requested; i++)
|
||||||
{
|
{
|
||||||
if (!alIsSource(source_request->sources[i]))
|
if (source_request->sources[i] != 0 && alIsSource(source_request->sources[i]))
|
||||||
{
|
{
|
||||||
alGenSources(1, source_request->sources + i);
|
continue; // slot already holds a live source
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ALenum error = alGetError();
|
ALuint src = 0;
|
||||||
if (error == AL_NO_ERROR)
|
|
||||||
{
|
|
||||||
failed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed)
|
if (!RPAudioPoolAcquire(&src))
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// Out of sources. Hand back everything acquired on THIS attempt so a
|
||||||
|
// failed request cannot strand voices -- the renderer's steal loop
|
||||||
|
// will free some and retry.
|
||||||
|
//
|
||||||
|
for (int j = 0; j < i; j++)
|
||||||
|
{
|
||||||
|
if (source_request->sources[j] != 0)
|
||||||
|
{
|
||||||
|
RPAudioPoolRelease(source_request->sources[j]);
|
||||||
|
source_request->sources[j] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
return False;
|
return False;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
source_request->sources[i] = src;
|
||||||
|
}
|
||||||
|
|
||||||
return True;
|
return True;
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -1375,23 +1654,27 @@ Logical
|
|||||||
|
|
||||||
void L4AudioRenderer::ReleaseSourceSet(SourceSet &sourceSet)
|
void L4AudioRenderer::ReleaseSourceSet(SourceSet &sourceSet)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// SOURCE POOLING (docs/SOUND.md): park each source on the free list rather
|
||||||
|
// than destroying it. RPAudioPoolRelease stops it, detaches its buffer and
|
||||||
|
// scrubs the state -- including the EFX filter and reverb send -- so the
|
||||||
|
// next owner starts clean.
|
||||||
|
//
|
||||||
|
// The bulk alDeleteSources(count, sources) this replaces was also a leak
|
||||||
|
// waiting to happen: per the AL spec it is ATOMIC, so ONE invalid name in
|
||||||
|
// the array (an empty slot of a partial set, or the old -1 sentinel on a
|
||||||
|
// double release) meant NOTHING was deleted and the whole set leaked.
|
||||||
|
// Slots are parked at 0, which is never a valid AL name -- unlike -1, which
|
||||||
|
// alIsSource would be asked about as 0xFFFFFFFF.
|
||||||
|
//
|
||||||
for (int i = 0; i < sourceSet.count; i++)
|
for (int i = 0; i < sourceSet.count; i++)
|
||||||
{
|
{
|
||||||
ALenum state;
|
if (sourceSet.sources[i] != 0)
|
||||||
alGetSourcei(sourceSet.sources[i], AL_SOURCE_STATE, &state);
|
|
||||||
|
|
||||||
if (state == AL_PLAYING)
|
|
||||||
{
|
{
|
||||||
alSourceStop(sourceSet.sources[i]);
|
RPAudioPoolRelease(sourceSet.sources[i]);
|
||||||
|
sourceSet.sources[i] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
alDeleteSources(sourceSet.count, sourceSet.sources);
|
|
||||||
|
|
||||||
for (int i = 0; i < sourceSet.count; i++)
|
|
||||||
{
|
|
||||||
sourceSet.sources[i] = -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer profile bits ~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer profile bits ~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -6,6 +6,14 @@
|
|||||||
#include "l4audres.h"
|
#include "l4audres.h"
|
||||||
#include "openal/al.h"
|
#include "openal/al.h"
|
||||||
|
|
||||||
|
//
|
||||||
|
// Master volume, standing in for the amplifier the cabinets had. Stepped by
|
||||||
|
// PgUp/PgDn (L4APP.cpp) and persisted to volume.cfg; see L4AUDRND.cpp.
|
||||||
|
//
|
||||||
|
extern float gRPMasterVolume;
|
||||||
|
|
||||||
|
void RPAudioMasterVolumeStep(int direction);
|
||||||
|
|
||||||
|
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include "l4ctrl.h"
|
#include "l4ctrl.h"
|
||||||
#include "l4keybd.h"
|
#include "l4keybd.h"
|
||||||
#include "l4app.h"
|
#include "l4app.h"
|
||||||
|
#include "l4audrnd.h" // RPAudioMasterVolumeStep, for the PgUp/PgDn keys
|
||||||
|
#include "l4audres.h" // RPBassTrimStep, for the Home/End keys
|
||||||
#include "l4dinput.h"
|
#include "l4dinput.h"
|
||||||
#include "..\munga\appmgr.h"
|
#include "..\munga\appmgr.h"
|
||||||
#include "dxutils.h"
|
#include "dxutils.h"
|
||||||
@@ -1513,6 +1515,78 @@ void
|
|||||||
// Update the PC keyboard mapping group
|
// Update the PC keyboard mapping group
|
||||||
//-------------------------------------------------------------------------
|
//-------------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
|
//
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// Master volume, PgUp louder / PgDn quieter.
|
||||||
|
//
|
||||||
|
// The cabinets ran the game at unity and left level to an external
|
||||||
|
// amplifier and crossover; without that hardware the player has to be able
|
||||||
|
// to reach the volume while playing.
|
||||||
|
//
|
||||||
|
// POLLED, not taken off the key message below, and that is deliberate. The
|
||||||
|
// pump below only ever consumes WM_KEYUP / WM_SYSKEYUP / WM_CHAR from the
|
||||||
|
// front of the queue, and the front-end runs message loops of its own, so
|
||||||
|
// key messages are raced for and routinely lost -- measured here at roughly
|
||||||
|
// two of every six presses arriving. That is survivable for a one-shot like
|
||||||
|
// the abort chord; it is not survivable for a control you tap repeatedly to
|
||||||
|
// find a level. Reading the key state directly costs nothing and cannot be
|
||||||
|
// dropped.
|
||||||
|
//
|
||||||
|
// Page keys because they produce no typed character, so they cannot collide
|
||||||
|
// with the character-keyed commands the pump feeds, nothing else in RP binds
|
||||||
|
// them, and they exist on tenkeyless keyboards.
|
||||||
|
//
|
||||||
|
// The foreground check keeps an alt-tabbed game from eating the volume keys
|
||||||
|
// of whatever the player switched to.
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Home/End do the same for the bass trim - the crossover's low band to
|
||||||
|
// PgUp/PgDn's amplifier.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
static int volume_up_held = 0;
|
||||||
|
static int volume_down_held = 0;
|
||||||
|
static int bass_up_held = 0;
|
||||||
|
static int bass_down_held = 0;
|
||||||
|
|
||||||
|
int focused = 0;
|
||||||
|
|
||||||
|
if (HWND foreground = GetForegroundWindow())
|
||||||
|
{
|
||||||
|
DWORD foreground_process = 0;
|
||||||
|
|
||||||
|
GetWindowThreadProcessId(foreground, &foreground_process);
|
||||||
|
focused = (foreground_process == GetCurrentProcessId());
|
||||||
|
}
|
||||||
|
|
||||||
|
const int up = focused && (GetAsyncKeyState(VK_PRIOR) & 0x8000) != 0;
|
||||||
|
const int down = focused && (GetAsyncKeyState(VK_NEXT) & 0x8000) != 0;
|
||||||
|
const int bass_up = focused && (GetAsyncKeyState(VK_HOME) & 0x8000) != 0;
|
||||||
|
const int bass_down = focused && (GetAsyncKeyState(VK_END) & 0x8000) != 0;
|
||||||
|
|
||||||
|
if (up && !volume_up_held)
|
||||||
|
{
|
||||||
|
RPAudioMasterVolumeStep(+1);
|
||||||
|
}
|
||||||
|
if (down && !volume_down_held)
|
||||||
|
{
|
||||||
|
RPAudioMasterVolumeStep(-1);
|
||||||
|
}
|
||||||
|
if (bass_up && !bass_up_held)
|
||||||
|
{
|
||||||
|
RPBassTrimStep(+1);
|
||||||
|
}
|
||||||
|
if (bass_down && !bass_down_held)
|
||||||
|
{
|
||||||
|
RPBassTrimStep(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_up_held = up;
|
||||||
|
volume_down_held = down;
|
||||||
|
bass_up_held = bass_up;
|
||||||
|
bass_down_held = bass_down;
|
||||||
|
}
|
||||||
|
|
||||||
if (flags.keyboardExists)
|
if (flags.keyboardExists)
|
||||||
{
|
{
|
||||||
//RB 1/20/07
|
//RB 1/20/07
|
||||||
|
|||||||
+142
-5
@@ -3828,6 +3828,13 @@ static LRESULT CALLBACK
|
|||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
static WNDPROC gCockpitBaseProc = NULL;
|
static WNDPROC gCockpitBaseProc = NULL;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Which window we subclassed, so the destructor can put its own proc
|
||||||
|
// back. The shell is the GAME window: it outlives the cockpit and
|
||||||
|
// carries the console screen from one race to the next.
|
||||||
|
//
|
||||||
|
static HWND gCockpitShellWindow = NULL;
|
||||||
|
|
||||||
static LRESULT CALLBACK
|
static LRESULT CALLBACK
|
||||||
CockpitShellProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
CockpitShellProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||||
{
|
{
|
||||||
@@ -4358,6 +4365,15 @@ SVGA16::SVGA16(
|
|||||||
// Split-view mode: decide before BuildWindows so the packed gauge
|
// Split-view mode: decide before BuildWindows so the packed gauge
|
||||||
// windows can stay hidden.
|
// windows can stay hidden.
|
||||||
//------------------------------------------------------------------
|
//------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Before anything else: the constructor calls Update() below, and
|
||||||
|
// both of these are read in there. mDisplayToUpdate was only being
|
||||||
|
// set at the END of the constructor, so that first pass indexed the
|
||||||
|
// display arrays with whatever was on the stack.
|
||||||
|
//
|
||||||
|
mDisplayToUpdate = 0;
|
||||||
|
mDisplaysCopiedThisPass = 0;
|
||||||
|
|
||||||
splitViews = False;
|
splitViews = False;
|
||||||
cockpitViewscreen = NULL;
|
cockpitViewscreen = NULL;
|
||||||
Logical explodedViews = False;
|
Logical explodedViews = False;
|
||||||
@@ -4679,9 +4695,25 @@ SVGA16::SVGA16(
|
|||||||
GetClientRect(cockpit, &inner);
|
GetClientRect(cockpit, &inner);
|
||||||
LayoutCockpit(inner.right, inner.bottom);
|
LayoutCockpit(inner.right, inner.bottom);
|
||||||
|
|
||||||
// catch maximise / restore / drag-resize and re-fit
|
//
|
||||||
|
// Catch maximise / restore / drag-resize and re-fit - ONCE.
|
||||||
|
//
|
||||||
|
// The destructor puts the original proc back, so ordinarily
|
||||||
|
// this window is unsubclassed by the time a second race
|
||||||
|
// builds a new cockpit. The guard is for the case where it
|
||||||
|
// was not: subclassing an already-subclassed window makes
|
||||||
|
// SetWindowLongPtr hand back CockpitShellProc itself as the
|
||||||
|
// "original", and the proc below then chains to itself on
|
||||||
|
// every single message until the stack runs out. That is a
|
||||||
|
// stack overflow a few frames into the second race, with no
|
||||||
|
// hint of a cause in the log.
|
||||||
|
//
|
||||||
|
if (gCockpitShellWindow != cockpit)
|
||||||
|
{
|
||||||
gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA(
|
gCockpitBaseProc = (WNDPROC) SetWindowLongPtrA(
|
||||||
cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc);
|
cockpit, GWLP_WNDPROC, (LONG_PTR) CockpitShellProc);
|
||||||
|
gCockpitShellWindow = cockpit;
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Sticky placement for the shell. Position AND size: nothing
|
// Sticky placement for the shell. Position AND size: nothing
|
||||||
@@ -4917,6 +4949,30 @@ SVGA16::~SVGA16()
|
|||||||
cockpitViewscreen = NULL;
|
cockpitViewscreen = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Give the game window its own proc back, and stop answering for a
|
||||||
|
// cockpit that is about to stop existing.
|
||||||
|
//
|
||||||
|
// The window survives us - it is the one that shows the console
|
||||||
|
// screen between races - so both of these outlived their subject.
|
||||||
|
// The subclass was the worse of the two: the next race re-subclassed
|
||||||
|
// the same window and CockpitShellProc ended up chained to itself.
|
||||||
|
// activeCockpit was the quieter one, left pointing at this object
|
||||||
|
// after it was freed, ready for the next WM_SIZE to lay out a
|
||||||
|
// cockpit that had already gone.
|
||||||
|
//
|
||||||
|
if (gCockpitShellWindow != NULL)
|
||||||
|
{
|
||||||
|
SetWindowLongPtrA(
|
||||||
|
gCockpitShellWindow, GWLP_WNDPROC, (LONG_PTR) gCockpitBaseProc);
|
||||||
|
gCockpitShellWindow = NULL;
|
||||||
|
gCockpitBaseProc = NULL;
|
||||||
|
}
|
||||||
|
if (activeCockpit == this)
|
||||||
|
{
|
||||||
|
activeCockpit = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4997,12 +5053,16 @@ Logical SVGA16::Update(Logical forceAll)
|
|||||||
GaugeRenderer *renderer = application->GetGaugeRenderer();
|
GaugeRenderer *renderer = application->GetGaugeRenderer();
|
||||||
if (!valid || renderer == NULL)
|
if (!valid || renderer == NULL)
|
||||||
{
|
{
|
||||||
|
mDisplaysCopiedThisPass = 0;
|
||||||
CLEAR_SCREEN_COPY();
|
CLEAR_SCREEN_COPY();
|
||||||
return False; // Do no more!
|
return False; // Do no more!
|
||||||
}
|
}
|
||||||
|
|
||||||
if (++mDisplayToUpdate >= NUMGAUGEWINDOWS)
|
// one display per call; the rotation steps at the end of the function
|
||||||
|
if (mDisplayToUpdate >= NUMGAUGEWINDOWS)
|
||||||
|
{
|
||||||
mDisplayToUpdate = 0;
|
mDisplayToUpdate = 0;
|
||||||
|
}
|
||||||
|
|
||||||
//Top MFD's
|
//Top MFD's
|
||||||
L4GraphicsPort *UL = static_cast<L4GraphicsPort*>(renderer->GetGraphicsPort("auxUL2"));
|
L4GraphicsPort *UL = static_cast<L4GraphicsPort*>(renderer->GetGraphicsPort("auxUL2"));
|
||||||
@@ -5045,7 +5105,10 @@ Logical SVGA16::Update(Logical forceAll)
|
|||||||
lrMask |= (lrMask << 16);
|
lrMask |= (lrMask << 16);
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
//No MFDs to draw, break out early
|
//No MFDs to draw, break out early. The sweep counter resets
|
||||||
|
//too: leaving it part-used would keep the renderer in its
|
||||||
|
//copy phase, and it never draws another gauge while there.
|
||||||
|
mDisplaysCopiedThisPass = 0;
|
||||||
return False;
|
return False;
|
||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
@@ -5057,7 +5120,8 @@ Logical SVGA16::Update(Logical forceAll)
|
|||||||
secPalette = &((SVGA16 *) secPort->graphicsDisplay)->palette[secPort->paletteID];
|
secPalette = &((SVGA16 *) secPort->graphicsDisplay)->palette[secPort->paletteID];
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
//No secondary, skip
|
//No secondary, skip - and end the sweep, as above.
|
||||||
|
mDisplaysCopiedThisPass = 0;
|
||||||
return False;
|
return False;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5227,7 +5291,80 @@ Logical SVGA16::Update(Logical forceAll)
|
|||||||
// if (end.ticks - start.ticks > 100)
|
// if (end.ticks - start.ticks > 100)
|
||||||
// end = start;
|
// end = start;
|
||||||
|
|
||||||
return False; // True == 'more to do'
|
//
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// Step the rotation, and say "more to do" until every display has
|
||||||
|
// had its turn.
|
||||||
|
//
|
||||||
|
// This used to return False unconditionally, which told the gauge
|
||||||
|
// renderer its copy phase was over after a SINGLE display. A full
|
||||||
|
// gauge sweep - one gauge per background pass, so as many passes as
|
||||||
|
// there are active gauges - therefore refreshed one display, and the
|
||||||
|
// map, one of three, came round only every third sweep.
|
||||||
|
//
|
||||||
|
// That is invisible with frame time to spare, because the background
|
||||||
|
// loop keeps running until the frame budget is used up and gets
|
||||||
|
// through several sweeps. On a big map the 3D foreground eats the
|
||||||
|
// whole budget, the loop drops to the one pass per frame it is
|
||||||
|
// guaranteed, and the map goes seconds between refreshes - which is
|
||||||
|
// what the field reports describe, on exactly those maps. A death
|
||||||
|
// makes the renderer skip every static object, the budget frees up,
|
||||||
|
// and the backlog drains at once: the display appears to come back
|
||||||
|
// to life, which is the tell that led here.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
mDisplayToUpdate++;
|
||||||
|
if (mDisplayToUpdate >= NUMGAUGEWINDOWS)
|
||||||
|
{
|
||||||
|
mDisplayToUpdate = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++mDisplaysCopiedThisPass < NUMGAUGEWINDOWS)
|
||||||
|
{
|
||||||
|
return True; // call again - there are displays waiting
|
||||||
|
}
|
||||||
|
mDisplaysCopiedThisPass = 0;
|
||||||
|
|
||||||
|
//
|
||||||
|
// RP412GAUGEDIAG=1 reports how often the displays are actually being
|
||||||
|
// refreshed. Pixel-watching from outside cannot tell a display that
|
||||||
|
// is not refreshing from one whose picture simply is not changing,
|
||||||
|
// and that ambiguity is exactly what makes "my map froze" hard to
|
||||||
|
// pin down. This counts the real thing.
|
||||||
|
//
|
||||||
|
{
|
||||||
|
static int diagnostics = -1;
|
||||||
|
if (diagnostics < 0)
|
||||||
|
{
|
||||||
|
const char *setting = getenv("RP412GAUGEDIAG");
|
||||||
|
diagnostics = (setting != NULL && atoi(setting) != 0) ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (diagnostics)
|
||||||
|
{
|
||||||
|
static unsigned long window_start = 0;
|
||||||
|
static int sweeps = 0;
|
||||||
|
unsigned long now = GetTickCount();
|
||||||
|
++sweeps;
|
||||||
|
if (window_start == 0)
|
||||||
|
{
|
||||||
|
window_start = now;
|
||||||
|
}
|
||||||
|
else if (now - window_start >= 2000)
|
||||||
|
{
|
||||||
|
// tenths, by hand: whole sweeps per second rounds the
|
||||||
|
// interesting cases - a starved pipeline managing two
|
||||||
|
// thirds of a sweep a second reads as a flat "0/s".
|
||||||
|
int tenths = sweeps * 10000 / (int)(now - window_start);
|
||||||
|
DEBUG_STREAM << "GaugeDiag: " << sweeps << " display sweep(s) in "
|
||||||
|
<< (now - window_start) << " ms ("
|
||||||
|
<< (tenths / 10) << '.' << (tenths % 10) << "/s, "
|
||||||
|
<< NUMGAUGEWINDOWS << " displays each)\n" << std::flush;
|
||||||
|
window_start = now;
|
||||||
|
sweeps = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return False; // the sweep is complete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -293,6 +293,17 @@ private:
|
|||||||
|
|
||||||
int mDisplayToUpdate;
|
int mDisplayToUpdate;
|
||||||
|
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
// How many displays this copy pass has refreshed.
|
||||||
|
//
|
||||||
|
// The gauge renderer's copy phase ends the moment Update() reports it
|
||||||
|
// has finished, and Update() reported that after ONE display - so a
|
||||||
|
// whole gauge sweep refreshed a single display, and the map, one of
|
||||||
|
// three, came round only every third sweep. Counting them out means
|
||||||
|
// one sweep refreshes all of them.
|
||||||
|
//------------------------------------------------------------------
|
||||||
|
int mDisplaysCopiedThisPass;
|
||||||
|
|
||||||
//------------------------------------------------------------------
|
//------------------------------------------------------------------
|
||||||
// Split-view mode (L4MFDSPLIT=1): the five channel-packed MFDs and
|
// Split-view mode (L4MFDSPLIT=1): the five channel-packed MFDs and
|
||||||
// the rotated map render as their own desktop windows; the packed
|
// the rotated map render as their own desktop windows; the packed
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
#include "..\munga\nttmgr.h"
|
#include "..\munga\nttmgr.h"
|
||||||
#include "..\munga\app.h"
|
#include "..\munga\app.h"
|
||||||
#include "l4particles.h"
|
#include "l4particles.h"
|
||||||
|
#include "l4padrio.h" // PadRIO::IsActive, for the per-frame lamp sweep
|
||||||
|
#include "..\munga\gaugrend.h"
|
||||||
|
#include "..\munga\lamp.h"
|
||||||
|
#include "..\munga\mode.h"
|
||||||
#include "DXUtils.h"
|
#include "DXUtils.h"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
@@ -5983,11 +5987,84 @@ void
|
|||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Execute Method, performs the rendering of one frame
|
// Execute Method, performs the rendering of one frame
|
||||||
//
|
//
|
||||||
|
//
|
||||||
|
//===========================================================================
|
||||||
|
// RPSweepCockpitLamps
|
||||||
|
//
|
||||||
|
// Push the cockpit lamp STATE once per frame, instead of once per gauge
|
||||||
|
// cycle.
|
||||||
|
//
|
||||||
|
// The on-screen vRIO buttons light themselves from PadRIO::GetLampState,
|
||||||
|
// and they redraw with their MFD strip. What FILLS that store is
|
||||||
|
// LampManager::Update -> AssertNewLampValue -> SetLamp, and that rides
|
||||||
|
// the gauge renderer's FOREGROUND turn - which comes round only once per
|
||||||
|
// full gauge cycle. On a busy map the cycle takes the best part of a
|
||||||
|
// second, so the lit buttons froze and any flash stalled while the 3D
|
||||||
|
// view, a separate per-frame render, stayed perfectly smooth. BT411 saw
|
||||||
|
// the same thing on its glass surround and fixed it the same way.
|
||||||
|
//
|
||||||
|
// It is cheap: a sweep over the lamps, no raster, and AssertNewLampValue
|
||||||
|
// already drops anything that has not changed - so this pushes no extra
|
||||||
|
// traffic, it only stops changes arriving late.
|
||||||
|
//
|
||||||
|
// Only when a PadRIO is active, i.e. cockpit-less play. With real serial
|
||||||
|
// hardware selected the pod keeps its authentic bandwidth-paced cadence,
|
||||||
|
// untouched. RP412LAMPSWEEP=0 restores the once-per-cycle behaviour.
|
||||||
|
//===========================================================================
|
||||||
|
//
|
||||||
|
static void
|
||||||
|
RPSweepCockpitLamps()
|
||||||
|
{
|
||||||
|
static int
|
||||||
|
enabled = -1;
|
||||||
|
|
||||||
|
if (enabled < 0)
|
||||||
|
{
|
||||||
|
const char
|
||||||
|
*setting = getenv("RP412LAMPSWEEP");
|
||||||
|
|
||||||
|
enabled = (setting != NULL && setting[0] == '0') ? 0 : 1;
|
||||||
|
}
|
||||||
|
if (!enabled || !PadRIO::IsActive() || application == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Only while a mission is actually running. This is called from the
|
||||||
|
// top of the frame, ahead of the state switch below, so it would
|
||||||
|
// otherwise fire while the mission is still being built and the
|
||||||
|
// gauges do not exist yet.
|
||||||
|
//
|
||||||
|
if (application->GetApplicationState() != Application::RunningMission)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GaugeRenderer
|
||||||
|
*renderer = application->GetGaugeRenderer();
|
||||||
|
ModeManager
|
||||||
|
*modes = application->GetModeManager();
|
||||||
|
|
||||||
|
if (renderer != NULL && modes != NULL)
|
||||||
|
{
|
||||||
|
LampManager
|
||||||
|
*lamps = renderer->GetLampManager();
|
||||||
|
|
||||||
|
if (lamps != NULL)
|
||||||
|
{
|
||||||
|
lamps->Update(modes->GetModeMask());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::InterestingEntityIterator* all_iterator)
|
void DPLRenderer::ExecuteImplementation(RendererComplexity, RendererOrigin::InterestingEntityIterator* all_iterator)
|
||||||
{
|
{
|
||||||
Component *component;
|
Component *component;
|
||||||
HRESULT hr;
|
HRESULT hr;
|
||||||
|
|
||||||
|
RPSweepCockpitLamps(); // keep the lit buttons tracking the sim (see above)
|
||||||
|
|
||||||
// timing variables
|
// timing variables
|
||||||
__int64 ticks = HiResNowTicks();
|
__int64 ticks = HiResNowTicks();
|
||||||
#ifdef LOGFRAMERATE
|
#ifdef LOGFRAMERATE
|
||||||
|
|||||||
@@ -227,6 +227,7 @@
|
|||||||
<ClCompile Include="..\MUNGA\WRHOUS.cpp" />
|
<ClCompile Include="..\MUNGA\WRHOUS.cpp" />
|
||||||
<ClCompile Include=".\DXUtils.cpp" />
|
<ClCompile Include=".\DXUtils.cpp" />
|
||||||
<ClCompile Include=".\L4APP.cpp" />
|
<ClCompile Include=".\L4APP.cpp" />
|
||||||
|
<ClCompile Include=".\L4AUDEFX.cpp" />
|
||||||
<ClCompile Include=".\L4AUDHDW.cpp" />
|
<ClCompile Include=".\L4AUDHDW.cpp" />
|
||||||
<ClCompile Include=".\L4AUDIO.cpp" />
|
<ClCompile Include=".\L4AUDIO.cpp" />
|
||||||
<ClCompile Include=".\L4AUDLVL.cpp" />
|
<ClCompile Include=".\L4AUDLVL.cpp" />
|
||||||
@@ -432,6 +433,7 @@
|
|||||||
<ClInclude Include="..\MUNGA\WRHOUS.h" />
|
<ClInclude Include="..\MUNGA\WRHOUS.h" />
|
||||||
<ClInclude Include=".\DXUtils.h" />
|
<ClInclude Include=".\DXUtils.h" />
|
||||||
<ClInclude Include=".\L4APP.H" />
|
<ClInclude Include=".\L4APP.H" />
|
||||||
|
<ClInclude Include=".\L4AUDEFX.h" />
|
||||||
<ClInclude Include=".\L4AUDHDW.h" />
|
<ClInclude Include=".\L4AUDHDW.h" />
|
||||||
<ClInclude Include=".\L4AUDIO.h" />
|
<ClInclude Include=".\L4AUDIO.h" />
|
||||||
<ClInclude Include=".\L4AUDLVL.h" />
|
<ClInclude Include=".\L4AUDLVL.h" />
|
||||||
|
|||||||
@@ -486,6 +486,9 @@
|
|||||||
<ClCompile Include=".\L4AUDHDW.cpp">
|
<ClCompile Include=".\L4AUDHDW.cpp">
|
||||||
<Filter>Source Files\MUNGA_L4</Filter>
|
<Filter>Source Files\MUNGA_L4</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include=".\L4AUDEFX.cpp">
|
||||||
|
<Filter>Source Files\MUNGA_L4</Filter>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include=".\L4AUDIO.cpp">
|
<ClCompile Include=".\L4AUDIO.cpp">
|
||||||
<Filter>Source Files\MUNGA_L4</Filter>
|
<Filter>Source Files\MUNGA_L4</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
@@ -1058,6 +1061,9 @@
|
|||||||
<ClInclude Include=".\L4APP.H">
|
<ClInclude Include=".\L4APP.H">
|
||||||
<Filter>Header Files\MUNGA_L4</Filter>
|
<Filter>Header Files\MUNGA_L4</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include=".\L4AUDEFX.h">
|
||||||
|
<Filter>Header Files\MUNGA_L4</Filter>
|
||||||
|
</ClInclude>
|
||||||
<ClInclude Include=".\L4AUDHDW.h">
|
<ClInclude Include=".\L4AUDHDW.h">
|
||||||
<Filter>Header Files\MUNGA_L4</Filter>
|
<Filter>Header Files\MUNGA_L4</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd)
|
|||||||
default.file = "";
|
default.file = "";
|
||||||
default.implemented = false;
|
default.implemented = false;
|
||||||
default.loop = SampleLoop::LoopAtWill;
|
default.loop = SampleLoop::LoopAtWill;
|
||||||
|
//
|
||||||
|
// -1 = no buffer. Every caller tests bufferIndex >= 0 before using it as
|
||||||
|
// an index, and this one field was being left as whatever was on the
|
||||||
|
// stack - so "this zone does not exist" read as a real buffer whenever
|
||||||
|
// the garbage happened to be positive, and indexed g_buffers with it.
|
||||||
|
//
|
||||||
|
default.bufferIndex = -1;
|
||||||
|
|
||||||
if (sampleInd < 0 || sampleInd >= allPresets[bank-1][preset].sampleNum)
|
if (sampleInd < 0 || sampleInd >= allPresets[bank-1][preset].sampleNum)
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-2
@@ -229,8 +229,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
|||||||
" Expired %s\n\n"
|
" Expired %s\n\n"
|
||||||
"Test builds are good for a fortnight so that nobody spends an "
|
"Test builds are good for a fortnight so that nobody spends an "
|
||||||
"afternoon chasing something that was fixed a week ago.\n\n"
|
"afternoon chasing something that was fixed a week ago.\n\n"
|
||||||
"Grab the current one:\n"
|
"Ask for the current one.",
|
||||||
"https://gitea.mysticmachines.com/VWE/RP412/releases",
|
|
||||||
RP412_VERSION_LONG, RP412_EXPIRY_TEXT);
|
RP412_VERSION_LONG, RP412_EXPIRY_TEXT);
|
||||||
MessageBoxA(NULL, notice, "Red Planet - test build expired",
|
MessageBoxA(NULL, notice, "Red Planet - test build expired",
|
||||||
MB_OK | MB_ICONWARNING | MB_SETFOREGROUND);
|
MB_OK | MB_ICONWARNING | MB_SETFOREGROUND);
|
||||||
|
|||||||
@@ -185,6 +185,30 @@ namespace
|
|||||||
"# default is 60; the arcade pods shipped at 25.\n"
|
"# default is 60; the arcade pods shipped at 25.\n"
|
||||||
"TARGETFPS=60\n"
|
"TARGETFPS=60\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
"# How long one background pass may spend drawing cockpit gauges, in\n"
|
||||||
|
"# milliseconds. The gauges and the MFD/map displays are redrawn in the\n"
|
||||||
|
"# time left over after the 3D view; on a big, busy map there is none\n"
|
||||||
|
"# left, and at the original one-gauge-per-pass the map and the countdown\n"
|
||||||
|
"# clock could sit frozen for seconds at a time - until something (a\n"
|
||||||
|
"# death, say) lightened the 3D view enough for the backlog to drain.\n"
|
||||||
|
"# Working to a slice ties the refresh rate to elapsed time instead. Set\n"
|
||||||
|
"# 0 for the old behaviour; raise it to favour the displays over frame\n"
|
||||||
|
"# rate.\n"
|
||||||
|
"RP412GAUGESLICE=2\n"
|
||||||
|
"\n"
|
||||||
|
"# 1 = log how many times a second every cockpit display is actually\n"
|
||||||
|
"# refreshed, to rpl4.log. Watching the screen cannot tell a display that\n"
|
||||||
|
"# has stopped refreshing from one whose picture simply is not changing.\n"
|
||||||
|
"#RP412GAUGEDIAG=1\n"
|
||||||
|
"\n"
|
||||||
|
"# 0 = light the on-screen cockpit buttons on the same slow cadence the\n"
|
||||||
|
"# arcade pod's serial hardware used. The lamp state is filled once per\n"
|
||||||
|
"# gauge cycle, so under the load described above the lit buttons froze\n"
|
||||||
|
"# and flashing ones stalled while the 3D view stayed perfectly smooth.\n"
|
||||||
|
"# On by default: the buttons are refreshed every frame instead. Ignored\n"
|
||||||
|
"# when real RIO hardware is selected - the pod keeps its own cadence.\n"
|
||||||
|
"#RP412LAMPSWEEP=0\n"
|
||||||
|
"\n"
|
||||||
"# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client\n"
|
"# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client\n"
|
||||||
"# running and steam_appid.txt beside the exe; without them the game\n"
|
"# running and steam_appid.txt beside the exe; without them the game\n"
|
||||||
"# logs the reason and falls back to plain TCP. 0 = TCP only.\n"
|
"# logs the reason and falls back to plain TCP. 0 = TCP only.\n"
|
||||||
@@ -205,6 +229,35 @@ namespace
|
|||||||
"# Unset or nonzero = on (the default); 0 = off.\n"
|
"# Unset or nonzero = on (the default); 0 = off.\n"
|
||||||
"#RP412KEYLIGHT=0\n"
|
"#RP412KEYLIGHT=0\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
"# The cabinets ran the game at unity and did all their volume and tone\n"
|
||||||
|
"# shaping outside it, in an amplifier and a 3-way crossover. You almost\n"
|
||||||
|
"# certainly have neither, so these two stand in for them. Both default\n"
|
||||||
|
"# to leaving the mix exactly as the pod played it.\n"
|
||||||
|
"\n"
|
||||||
|
"# Master volume, 0.0 to 2.0, the amplifier's knob. 1.0 is unity. The\n"
|
||||||
|
"# sound effects now carry the pitch, layering and dynamics the original\n"
|
||||||
|
"# AWE32 soundbanks ask for, which is a good deal livelier than earlier\n"
|
||||||
|
"# 4.12 builds - lower this if the whole thing sits too hot.\n"
|
||||||
|
"#\n"
|
||||||
|
"# PageUp and PageDown change it while you play, in steps of 0.05, and\n"
|
||||||
|
"# whatever you leave it on is written to volume.cfg beside the exe and\n"
|
||||||
|
"# used from then on - so this line only decides where a machine that has\n"
|
||||||
|
"# never been touched starts out. Delete volume.cfg to come back here.\n"
|
||||||
|
"#RP412AUDIOVOLUME=0.8\n"
|
||||||
|
"\n"
|
||||||
|
"# Bass trim, 0.0 to 1.0, the crossover's low band. 1.0 is the low end\n"
|
||||||
|
"# exactly as authored. The soundbanks put real weight under collisions,\n"
|
||||||
|
"# engines and explosions - deep layers earlier builds played at the\n"
|
||||||
|
"# wrong rate, so they barely sounded at all. Lower this to pull that\n"
|
||||||
|
"# back; it eases in below 22kHz of playback rate and reaches full cut\n"
|
||||||
|
"# on the deepest layers, leaving the mid and top alone.\n"
|
||||||
|
"#\n"
|
||||||
|
"# Home and End change it while you play, in steps of 0.05, and what you\n"
|
||||||
|
"# leave it on is written to bass.cfg beside the exe and used from then\n"
|
||||||
|
"# on - so this line only decides where an untouched machine starts.\n"
|
||||||
|
"# Delete bass.cfg to come back here.\n"
|
||||||
|
"#RP412AUDIOBASS=0.7\n"
|
||||||
|
"\n"
|
||||||
"# Invert the stick on top of whatever bindings.txt produces:\n"
|
"# Invert the stick on top of whatever bindings.txt produces:\n"
|
||||||
"# X = invert X only, Y = invert Y only, XY = both (case-insensitive).\n"
|
"# X = invert X only, Y = invert Y only, XY = both (case-insensitive).\n"
|
||||||
"#L4PADFLIP=XY\n"
|
"#L4PADFLIP=XY\n"
|
||||||
|
|||||||
+84
-43
@@ -725,6 +725,13 @@ void
|
|||||||
mode_manager->AddModeMask(previousPresetModeMask);
|
mode_manager->AddModeMask(previousPresetModeMask);
|
||||||
}
|
}
|
||||||
//-----------------------------------
|
//-----------------------------------
|
||||||
|
// Move the lamps with the mappings.
|
||||||
|
// Doing it here rather than in the
|
||||||
|
// switch handler keeps the keyboard
|
||||||
|
// presets (1-6) in step as well.
|
||||||
|
//-----------------------------------
|
||||||
|
NotifyOfPresetChange(previousPresetNumber, preset_number);
|
||||||
|
//-----------------------------------
|
||||||
// Save the new preset number
|
// Save the new preset number
|
||||||
//-----------------------------------
|
//-----------------------------------
|
||||||
previousPresetNumber = preset_number;
|
previousPresetNumber = preset_number;
|
||||||
@@ -733,6 +740,19 @@ void
|
|||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
//
|
||||||
|
void
|
||||||
|
L4VTVControlsMapper::NotifyOfPresetChange(
|
||||||
|
int /*old_preset*/,
|
||||||
|
int /*new_preset*/
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
// The base mapper has no preset lamps to move.
|
||||||
|
Check_Fpu();
|
||||||
|
}
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
//########################### ThrustmasterMapper ##############################
|
//########################### ThrustmasterMapper ##############################
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
@@ -1400,45 +1420,13 @@ void
|
|||||||
if (message->dataContents > 0)
|
if (message->dataContents > 0)
|
||||||
{
|
{
|
||||||
//-----------------------------------
|
//-----------------------------------
|
||||||
// Choose a new preset
|
// Choose a new preset. PresetEnable
|
||||||
|
// ignores a repeat of the lit switch
|
||||||
|
// and moves the lamps itself.
|
||||||
//-----------------------------------
|
//-----------------------------------
|
||||||
int
|
PresetEnable(
|
||||||
current_preset_number = (message->dataContents - 1)
|
(message->dataContents - 1) - LBE4ControlsManager::ButtonSecondary7
|
||||||
- LBE4ControlsManager::ButtonSecondary7;
|
);
|
||||||
|
|
||||||
if (previousPresetNumber != current_preset_number)
|
|
||||||
{
|
|
||||||
//-----------------------------------
|
|
||||||
// Set the old preset lamp to 'dim'
|
|
||||||
//-----------------------------------
|
|
||||||
if (previousPresetNumber >= 0)
|
|
||||||
{
|
|
||||||
Verify(previousPresetNumber < presetCount);
|
|
||||||
|
|
||||||
if (modeLamp[previousPresetNumber] != NULL)
|
|
||||||
{
|
|
||||||
Check(modeLamp[previousPresetNumber]);
|
|
||||||
modeLamp[previousPresetNumber]->SetState(L4Lamp::LampStateDim);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//-----------------------------------
|
|
||||||
// Set the new preset lamp to 'on'
|
|
||||||
//-----------------------------------
|
|
||||||
if (current_preset_number >= 0)
|
|
||||||
{
|
|
||||||
Verify(current_preset_number < presetCount);
|
|
||||||
|
|
||||||
if (modeLamp[current_preset_number] != NULL)
|
|
||||||
{
|
|
||||||
Check(modeLamp[current_preset_number]);
|
|
||||||
modeLamp[current_preset_number]->SetState(L4Lamp::LampStateOn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//-----------------------------------
|
|
||||||
// Change presets
|
|
||||||
//-----------------------------------
|
|
||||||
PresetEnable(current_preset_number);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
@@ -1655,6 +1643,44 @@ void
|
|||||||
Check_Fpu();
|
Check_Fpu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
// The six amber switches down the map's right flank. Called by PresetEnable,
|
||||||
|
// so the lamps follow the mappings no matter what asked for the change.
|
||||||
|
//
|
||||||
|
void
|
||||||
|
VTVRIOMapper::NotifyOfPresetChange(int old_preset, int new_preset)
|
||||||
|
{
|
||||||
|
Check(this);
|
||||||
|
|
||||||
|
//----------------------------------
|
||||||
|
// Set the old preset lamp to 'dim'
|
||||||
|
//----------------------------------
|
||||||
|
if (old_preset >= 0)
|
||||||
|
{
|
||||||
|
Verify(old_preset < presetCount);
|
||||||
|
|
||||||
|
if (presetLamp[old_preset] != NULL)
|
||||||
|
{
|
||||||
|
Check(presetLamp[old_preset]);
|
||||||
|
presetLamp[old_preset]->SetState(L4Lamp::LampStateDim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//----------------------------------
|
||||||
|
// Set the new preset lamp to 'on'
|
||||||
|
//----------------------------------
|
||||||
|
if (new_preset >= 0)
|
||||||
|
{
|
||||||
|
Verify(new_preset < presetCount);
|
||||||
|
|
||||||
|
if (presetLamp[new_preset] != NULL)
|
||||||
|
{
|
||||||
|
Check(presetLamp[new_preset]);
|
||||||
|
presetLamp[new_preset]->SetState(L4Lamp::LampStateOn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Check_Fpu();
|
||||||
|
}
|
||||||
|
|
||||||
//#############################################################################
|
//#############################################################################
|
||||||
// Construction and Destruction Support
|
// Construction and Destruction Support
|
||||||
//
|
//
|
||||||
@@ -1680,6 +1706,20 @@ VTVRIOMapper::VTVRIOMapper(
|
|||||||
leftPedal = 0.0f;
|
leftPedal = 0.0f;
|
||||||
rightPedal = 0.0f;
|
rightPedal = 0.0f;
|
||||||
|
|
||||||
|
//------------------------------------------------
|
||||||
|
// There are no lamps until the mapping blocks
|
||||||
|
// below make them - and under NOMODES they never
|
||||||
|
// do, so the notify methods must see NULLs.
|
||||||
|
//------------------------------------------------
|
||||||
|
{
|
||||||
|
int
|
||||||
|
i;
|
||||||
|
|
||||||
|
for(i=0; i<configLampCount; ++i) configLamp[i] = NULL;
|
||||||
|
for(i=0; i<modeLampCount; ++i) modeLamp[i] = NULL;
|
||||||
|
for(i=0; i<presetCount; ++i) presetLamp[i] = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
Check(application);
|
Check(application);
|
||||||
LBE4ControlsManager
|
LBE4ControlsManager
|
||||||
*controls = Cast_Object(
|
*controls = Cast_Object(
|
||||||
@@ -1915,13 +1955,14 @@ VTVRIOMapper::VTVRIOMapper(
|
|||||||
this
|
this
|
||||||
);
|
);
|
||||||
|
|
||||||
// These lamps are explicitly controlled by SelectPresetMessageHandler
|
// These lamps are explicitly controlled by NotifyOfPresetChange.
|
||||||
modeLamp[i] = CreateControlledLamp(button_number[i]);
|
// They are six, and they are NOT the four mode lamps above.
|
||||||
|
presetLamp[i] = CreateControlledLamp(button_number[i]);
|
||||||
|
|
||||||
if (modeLamp[i] != NULL)
|
if (presetLamp[i] != NULL)
|
||||||
{
|
{
|
||||||
Check(modeLamp[i]);
|
Check(presetLamp[i]);
|
||||||
modeLamp[i]->SetState(
|
presetLamp[i]->SetState(
|
||||||
(i==0)? L4Lamp::LampStateOn : L4Lamp::LampStateDim
|
(i==0)? L4Lamp::LampStateOn : L4Lamp::LampStateDim
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,12 @@ ModeMask
|
|||||||
//
|
//
|
||||||
void
|
void
|
||||||
PresetEnable(int preset_number);
|
PresetEnable(int preset_number);
|
||||||
|
|
||||||
|
// Announced by PresetEnable for EVERY preset change, whichever way it was
|
||||||
|
// triggered - map-flank switch or keyboard. Platforms carrying preset
|
||||||
|
// lamps move them here; the base mapper has none.
|
||||||
|
virtual void
|
||||||
|
NotifyOfPresetChange(int old_preset, int new_preset);
|
||||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
// Protected data
|
// Protected data
|
||||||
//
|
//
|
||||||
@@ -250,6 +256,9 @@ public:
|
|||||||
void
|
void
|
||||||
NotifyOfConfigurationModeChange(Logical new_state);
|
NotifyOfConfigurationModeChange(Logical new_state);
|
||||||
|
|
||||||
|
void
|
||||||
|
NotifyOfPresetChange(int old_preset, int new_preset);
|
||||||
|
|
||||||
void
|
void
|
||||||
SetPerformance(Performance performance)
|
SetPerformance(Performance performance)
|
||||||
{
|
{
|
||||||
|
|||||||
+348
-381
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user