mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 14:00:25 +02:00
I3DL2 reverb and muffled sounds
Based on #44 and #46 by Tyberious. 3D voices send to an I3DL2 reverb of the camera's sound environment, and obstruction and occlusion muffle their direct path above the HF reference (occlusion their send too). Both use the fixed distance; a maximum distance under the minimum is held at it. A change of environment crossfades the delays and gains over 85 ms, the filters' coefficients ramp over each mix, the diffusers, lines and filters flush denormals, and the reverb stops while it is quiet. audio.reverb (default true) is read again when the settings change.
This commit is contained in:
1 parent
b115a41263
commit
8a8e70599f
3 files changed
+508
-41
No files matched your search
@@ -243,6 +243,7 @@ the setting for one start of the game. It has priority over the file.
|
||||
| `audio.volume` | `1.0` | `HALO_VOLUME` | The master volume. |
|
||||
| `audio.music_volume` | `1.0` | `HALO_MUSIC_VOLUME` | The music's volume, of the master volume. |
|
||||
| `audio.effects_volume` | `1.0` | `HALO_EFFECTS_VOLUME` | The volume of the other sounds (effects and speech), of the master volume. |
|
||||
| `audio.reverb` | `true` | `HALO_REVERB` | `true`: the sounds of the world reverberate as the place the player is in does: the sound environments of the maps (a corridor, a cave, a large hall, outdoors) set the reverberation, as the I3DL2 reverb of the Xbox did. A sound behind a wall or a door is muffled in it too. `false`: no reverberation (sounds behind a wall are still muffled). |
|
||||
| `input.mouse_sensitivity` | `1.0` | `HALO_MOUSE_SENSITIVITY` | The multiplier for the mouse aim. |
|
||||
| `input.mouse_vertical_sensitivity` | `0.0` | `HALO_MOUSE_VERTICAL_SENSITIVITY` | The multiplier for the vertical mouse aim. `0`: the same as `input.mouse_sensitivity`. |
|
||||
| `input.invert_mouse` | `false` | `HALO_MOUSE_INVERT=1` sets `true` | `true`: the vertical mouse aim is inverted. |
|
||||
|
||||
+503
-41
@@ -15,11 +15,12 @@ resampling) and applies:
|
||||
- the front left and right mix bin volumes of 2D voices,
|
||||
- for 3D voices, DirectSound's inverse distance rolloff between the
|
||||
minimum and maximum distance, an equal power pan from the source's
|
||||
direction in listener space, and the low frequency part of the I3DL2
|
||||
direct path, obstruction and occlusion levels.
|
||||
Doppler, the high frequency filters, cones and I3DL2 reverb are not
|
||||
modelled. A look-ahead limiter keeps the sum of the voices under full scale
|
||||
(limit).
|
||||
direction in listener space, and the I3DL2 direct path, obstruction
|
||||
and occlusion levels, their high frequency levels a low pass at the
|
||||
environment's HF reference: a sound behind a wall is muffled.
|
||||
3D voices also send to an I3DL2 reverb of the environment the listener is in
|
||||
(reverb). Doppler and cones are not modelled. A look-ahead limiter keeps the
|
||||
sum under full scale (limit).
|
||||
|
||||
Packets the mixer has finished are completed from DirectSoundDoWork, which
|
||||
the game calls every frame, and from Flush, never from the audio thread:
|
||||
@@ -28,8 +29,9 @@ the game's completion callback is not meant to run concurrently with it.
|
||||
Without an audio device, a clock thread runs the same mixer into a scratch
|
||||
buffer, so streams still drain at their real rate.
|
||||
|
||||
audio.volume sets the master volume (default 1.0); audio.enabled = false
|
||||
skips opening a device (port_config.c).
|
||||
audio.volume sets the master volume (default 1.0); audio.reverb = false
|
||||
turns the reverb off; audio.enabled = false skips opening a device
|
||||
(port_config.c).
|
||||
*/
|
||||
|
||||
#include "platform.h"
|
||||
@@ -96,7 +98,13 @@ struct sdl_stream
|
||||
DWORD mode;
|
||||
float position[3];
|
||||
float minimum_distance, maximum_distance;
|
||||
float i3dl2_gain;
|
||||
/* the I3DL2 direct path's level and high frequency level, in millibels
|
||||
with obstruction and occlusion (SetI3DL2Source) */
|
||||
LONG direct, direct_hf;
|
||||
/* the I3DL2 room (reverb) send's level and high frequency level, in
|
||||
millibels with occlusion, and its rolloff factor (SetI3DL2Source) */
|
||||
LONG room, room_hf;
|
||||
float room_rolloff_factor;
|
||||
|
||||
struct voice_packet packets[MAXIMUM_STREAM_PACKETS];
|
||||
unsigned long packet_head;
|
||||
@@ -111,9 +119,14 @@ struct sdl_stream
|
||||
unsigned long center;
|
||||
double phase;
|
||||
unsigned long silence;
|
||||
/* gains the mixer is ramping from, to avoid clicks */
|
||||
float current_left, current_right;
|
||||
/* gains and low pass coefficients the mixer is ramping from, to avoid
|
||||
clicks */
|
||||
float current_left, current_right, current_room;
|
||||
float current_direct_lowpass, current_room_lowpass;
|
||||
BOOL gains_valid;
|
||||
/* the direct path's low pass, for each channel, and the room send's */
|
||||
float direct_lowpass[2];
|
||||
float room_lowpass;
|
||||
};
|
||||
|
||||
static pthread_mutex_t mixer_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
@@ -133,6 +146,17 @@ static struct
|
||||
|
||||
static float master_volume = 1.0f;
|
||||
|
||||
/* the I3DL2 listener properties (SetI3DL2Listener), which the reverb models:
|
||||
DirectSound's defaults until the game sets an environment, their room level
|
||||
leaving it silent; serial counts the changes */
|
||||
static DSI3DL2LISTENER environment =
|
||||
{
|
||||
DSBVOLUME_MIN, 0, 0.0f, 1.49f, 0.83f, -2602, 0.007f, 200, 0.011f, 100.0f, 100.0f, 5000.0f
|
||||
};
|
||||
static unsigned long environment_serial;
|
||||
/* audio.reverb */
|
||||
static BOOL reverb_enabled = TRUE;
|
||||
|
||||
static float gain_from_millibels(LONG millibels)
|
||||
{
|
||||
if (millibels <= DSBVOLUME_MIN)
|
||||
@@ -140,6 +164,40 @@ static float gain_from_millibels(LONG millibels)
|
||||
return powf(10.0f, (float)millibels / 2000.0f);
|
||||
}
|
||||
|
||||
/* value, or 0 once it is far too small to hear: the state of a filter or of
|
||||
the reverb fed silence decays into the denormals, which many CPUs work out
|
||||
slowly, and neither x86 nor ARM flushes them to zero by default */
|
||||
static float flush_denormal(float value)
|
||||
{
|
||||
return fabsf(value) < 1.0e-20f ? 0.0f : value;
|
||||
}
|
||||
|
||||
/* cos(2 pi f / OUTPUT_RATE) of a filter's frequency f, within what the
|
||||
output rate holds */
|
||||
static float frequency_cosine(float frequency)
|
||||
{
|
||||
if (frequency < 20.0f)
|
||||
frequency = 20.0f;
|
||||
if (frequency > 0.45f * OUTPUT_RATE)
|
||||
frequency = 0.45f * OUTPUT_RATE;
|
||||
return cosf(2.0f * 3.14159265f * frequency / OUTPUT_RATE);
|
||||
}
|
||||
|
||||
/* the coefficient a of the one-pole low pass y = x + a (y' - x) whose gain is
|
||||
gain (at most 1) at the frequency of cosine (frequency_cosine) */
|
||||
static float lowpass_coefficient(float gain, float cosine)
|
||||
{
|
||||
float a, b;
|
||||
|
||||
if (gain >= 1.0f)
|
||||
return 0.0f;
|
||||
if (gain < 0.001f)
|
||||
gain = 0.001f;
|
||||
a = 1.0f - gain * gain;
|
||||
b = 1.0f - gain * gain * cosine;
|
||||
return (b - sqrtf(b * b - a * a)) / a;
|
||||
}
|
||||
|
||||
/* ---------- decoding */
|
||||
|
||||
static const int ima_index_table[16] =
|
||||
@@ -249,7 +307,24 @@ static float dot3(const float *a, const float *b)
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
||||
}
|
||||
|
||||
static void spatialize(const struct sdl_stream *stream, float *left, float *right)
|
||||
/* DirectSound's inverse distance law, held beyond the maximum distance. The
|
||||
game sets a voice's maximum distance before its minimum, so the maximum can
|
||||
be under the minimum for a moment: it is held at the minimum */
|
||||
static float distance_attenuation(const struct sdl_stream *stream, float distance, float rolloff_factor)
|
||||
{
|
||||
float minimum = stream->minimum_distance;
|
||||
float maximum = stream->maximum_distance > minimum ? stream->maximum_distance : minimum;
|
||||
|
||||
if (minimum <= 0.0f || distance <= minimum)
|
||||
return 1.0f;
|
||||
if (distance > maximum)
|
||||
distance = maximum;
|
||||
return minimum / (minimum + rolloff_factor * (distance - minimum));
|
||||
}
|
||||
|
||||
/* the gains of a 3D voice to the left and right, and the room rolloff's
|
||||
attenuation of its room send */
|
||||
static void spatialize(const struct sdl_stream *stream, float *left, float *right, float *room)
|
||||
{
|
||||
float offset[3], right_axis[3], distance, attenuation, pan, side, ahead;
|
||||
int axis;
|
||||
@@ -276,16 +351,8 @@ static void spatialize(const struct sdl_stream *stream, float *left, float *righ
|
||||
distance factor turns units into meters for Doppler, and scaling by it
|
||||
here put every 3D sound 3 times as far away, up to 10 dB quieter */
|
||||
distance = sqrtf(dot3(offset, offset));
|
||||
|
||||
/* DirectSound's inverse distance law, held beyond the maximum distance */
|
||||
attenuation = 1.0f;
|
||||
if (distance > stream->minimum_distance && stream->minimum_distance > 0.0f)
|
||||
{
|
||||
float clamped = distance < stream->maximum_distance ? distance : stream->maximum_distance;
|
||||
|
||||
attenuation = stream->minimum_distance /
|
||||
(stream->minimum_distance + listener.rolloff_factor * (clamped - stream->minimum_distance));
|
||||
}
|
||||
attenuation = distance_attenuation(stream, distance, listener.rolloff_factor);
|
||||
*room = distance_attenuation(stream, distance, environment.flRoomRolloffFactor + stream->room_rolloff_factor);
|
||||
|
||||
/* equal power pan; close sources and sources straight ahead or behind
|
||||
stay centred, and neither ear drops below a quarter */
|
||||
@@ -301,15 +368,36 @@ static void spatialize(const struct sdl_stream *stream, float *left, float *righ
|
||||
*left = cosf(angle) * 1.41421356f * 0.70710678f;
|
||||
*right = sinf(angle) * 1.41421356f * 0.70710678f;
|
||||
}
|
||||
*left *= attenuation * stream->i3dl2_gain;
|
||||
*right *= attenuation * stream->i3dl2_gain;
|
||||
*left *= attenuation * gain_from_millibels(stream->direct);
|
||||
*right *= attenuation * gain_from_millibels(stream->direct);
|
||||
}
|
||||
|
||||
static void voice_gains(const struct sdl_stream *stream, float *left, float *right)
|
||||
/* the voice's gains to the left and right, and to the reverb (only 3D voices
|
||||
send to it, as their I3DL2 mix bin did on the Xbox), with the coefficients of
|
||||
the low passes of its direct path (3D voices: obstruction and occlusion
|
||||
muffle it) and of the send */
|
||||
static void voice_gains(const struct sdl_stream *stream, float *left, float *right, float *room,
|
||||
float *direct_lowpass, float *room_lowpass)
|
||||
{
|
||||
*room = 0.0f;
|
||||
*direct_lowpass = 0.0f;
|
||||
*room_lowpass = 0.0f;
|
||||
if (stream->has_3d && stream->mode != DS3DMODE_DISABLE)
|
||||
{
|
||||
spatialize(stream, left, right);
|
||||
float rolloff, cosine = frequency_cosine(environment.flHFReference);
|
||||
|
||||
spatialize(stream, left, right, &rolloff);
|
||||
if (stream->direct_hf < stream->direct)
|
||||
*direct_lowpass = lowpass_coefficient(gain_from_millibels(stream->direct_hf - stream->direct), cosine);
|
||||
if (reverb_enabled)
|
||||
{
|
||||
LONG level = environment.lRoom + stream->room;
|
||||
LONG high_level = environment.lRoom + environment.lRoomHF + stream->room_hf;
|
||||
|
||||
*room = gain_from_millibels(level) * rolloff;
|
||||
if (*room > 0.0f && high_level < level)
|
||||
*room_lowpass = lowpass_coefficient(gain_from_millibels(high_level - level), cosine);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -318,6 +406,7 @@ static void voice_gains(const struct sdl_stream *stream, float *left, float *rig
|
||||
}
|
||||
*left *= stream->volume * master_volume;
|
||||
*right *= stream->volume * master_volume;
|
||||
*room *= stream->volume * master_volume;
|
||||
}
|
||||
|
||||
/* ---------- resampling
|
||||
@@ -453,11 +542,14 @@ static BOOL take_frame(struct sdl_stream *stream, float *frame)
|
||||
|
||||
/* ---------- mixing */
|
||||
|
||||
/* mixes one voice into output (frames of stereo float) */
|
||||
static void mix_voice(struct sdl_stream *stream, float *output, unsigned long frames)
|
||||
/* mixes one voice into output (frames of stereo float), and into the reverb's
|
||||
send (frames of mono) */
|
||||
static void mix_voice(struct sdl_stream *stream, float *output, float *send, unsigned long frames)
|
||||
{
|
||||
double step;
|
||||
float target_left, target_right, left, right, ramp_left, ramp_right, scale;
|
||||
float target_left, target_right, target_room, left, right, room, ramp_left, ramp_right, ramp_room, scale;
|
||||
float target_direct_lowpass, target_room_lowpass, direct_lowpass, room_lowpass;
|
||||
float ramp_direct_lowpass, ramp_room_lowpass;
|
||||
long width;
|
||||
unsigned long frame;
|
||||
|
||||
@@ -468,17 +560,28 @@ static void mix_voice(struct sdl_stream *stream, float *output, unsigned long fr
|
||||
rate, and the frames it reaches on each side */
|
||||
scale = step > 1.0 ? (float)(1.0 / (step < RESAMPLER_MAXIMUM_STRETCH ? step : RESAMPLER_MAXIMUM_STRETCH)) : 1.0f;
|
||||
width = (long)ceilf(RESAMPLER_ZERO_CROSSINGS / scale);
|
||||
voice_gains(stream, &target_left, &target_right);
|
||||
voice_gains(stream, &target_left, &target_right, &target_room, &target_direct_lowpass, &target_room_lowpass);
|
||||
if (!stream->gains_valid)
|
||||
{
|
||||
stream->current_left = target_left;
|
||||
stream->current_right = target_right;
|
||||
stream->current_room = target_room;
|
||||
stream->current_direct_lowpass = target_direct_lowpass;
|
||||
stream->current_room_lowpass = target_room_lowpass;
|
||||
stream->direct_lowpass[0] = stream->direct_lowpass[1] = 0.0f;
|
||||
stream->room_lowpass = 0.0f;
|
||||
stream->gains_valid = TRUE;
|
||||
}
|
||||
left = stream->current_left;
|
||||
right = stream->current_right;
|
||||
room = stream->current_room;
|
||||
direct_lowpass = stream->current_direct_lowpass;
|
||||
room_lowpass = stream->current_room_lowpass;
|
||||
ramp_left = (target_left - left) / (float)frames;
|
||||
ramp_right = (target_right - right) / (float)frames;
|
||||
ramp_room = (target_room - room) / (float)frames;
|
||||
ramp_direct_lowpass = (target_direct_lowpass - direct_lowpass) / (float)frames;
|
||||
ramp_room_lowpass = (target_room_lowpass - room_lowpass) / (float)frames;
|
||||
|
||||
for (frame = 0; frame < frames; frame++)
|
||||
{
|
||||
@@ -505,10 +608,10 @@ static void mix_voice(struct sdl_stream *stream, float *output, unsigned long fr
|
||||
if (stream->silence > (unsigned long)(2 * width))
|
||||
break;
|
||||
|
||||
if (!left && !right && !ramp_left && !ramp_right)
|
||||
if (!left && !right && !room && !ramp_left && !ramp_right && !ramp_room)
|
||||
{
|
||||
/* a voice turned all the way down (out of earshot) only moves
|
||||
on */
|
||||
/* a voice turned all the way down (out of earshot), and not
|
||||
sending to the reverb, only moves on */
|
||||
}
|
||||
else if (scale == 1.0f)
|
||||
{
|
||||
@@ -538,9 +641,25 @@ static void mix_voice(struct sdl_stream *stream, float *output, unsigned long fr
|
||||
sample_right += source[1] * weight;
|
||||
}
|
||||
}
|
||||
/* the room send, with its own low pass */
|
||||
if (room || ramp_room)
|
||||
{
|
||||
float mono = stream->channels == 1 ? sample_left : 0.5f * (sample_left + sample_right);
|
||||
|
||||
stream->room_lowpass = flush_denormal(mono + room_lowpass * (stream->room_lowpass - mono));
|
||||
send[frame] += stream->room_lowpass * room;
|
||||
}
|
||||
/* the direct path, muffled, or as it is; a mono voice's mix bins or pan
|
||||
split it across the speakers */
|
||||
if (direct_lowpass || ramp_direct_lowpass)
|
||||
{
|
||||
sample_left = flush_denormal(sample_left + direct_lowpass * (stream->direct_lowpass[0] - sample_left));
|
||||
sample_right = flush_denormal(sample_right + direct_lowpass * (stream->direct_lowpass[1] - sample_right));
|
||||
}
|
||||
stream->direct_lowpass[0] = sample_left;
|
||||
stream->direct_lowpass[1] = sample_right;
|
||||
if (stream->channels == 1)
|
||||
{
|
||||
/* a mono voice's mix bins or pan split it across the speakers */
|
||||
output[frame * 2] += sample_left * left;
|
||||
output[frame * 2 + 1] += sample_left * right;
|
||||
}
|
||||
@@ -551,6 +670,9 @@ static void mix_voice(struct sdl_stream *stream, float *output, unsigned long fr
|
||||
}
|
||||
left += ramp_left;
|
||||
right += ramp_right;
|
||||
room += ramp_room;
|
||||
direct_lowpass += ramp_direct_lowpass;
|
||||
room_lowpass += ramp_room_lowpass;
|
||||
stream->phase += step;
|
||||
while (stream->phase >= 1.0)
|
||||
{
|
||||
@@ -560,6 +682,301 @@ static void mix_voice(struct sdl_stream *stream, float *output, unsigned long fr
|
||||
}
|
||||
stream->current_left = target_left;
|
||||
stream->current_right = target_right;
|
||||
stream->current_room = target_room;
|
||||
stream->current_direct_lowpass = target_direct_lowpass;
|
||||
stream->current_room_lowpass = target_room_lowpass;
|
||||
}
|
||||
|
||||
/* ---------- reverb
|
||||
|
||||
The Xbox ran an I3DL2 reverb on its audio DSP (the effects image the game
|
||||
downloads), fed by every 3D voice's I3DL2 mix bin. The game sets the listener
|
||||
properties from the sound environment the camera is in (sound_dsound_xbox.c
|
||||
dsound_set_listener_properties), and each 3D voice's room send from its
|
||||
reverb attenuation and occlusion (dsound_channel_set_I3DL2_properties). This
|
||||
is a reverb of the I3DL2 model on those properties, not the DSP's program:
|
||||
- each 3D voice sends to a mono bus at the listener's and its own room
|
||||
levels, occlusion included, their high frequency levels a low pass at the
|
||||
HF reference, attenuated with distance by the room rolloff factors
|
||||
(voice_gains);
|
||||
- the early reflections are REVERB_TAPS taps of the bus, starting the
|
||||
reflections delay later, at the reflections level;
|
||||
- the late reverberation starts the reverb delay after the reflections:
|
||||
all-pass diffusers as strong as the diffusion, then a feedback delay
|
||||
network of REVERB_LINES lines, as long as the density makes them, each
|
||||
losing what decays it by 60 dB in the decay time, and its high
|
||||
frequencies in the decay time times the HF ratio, at the reverb level.
|
||||
Both are normalized to the energy sent, so their levels are relative to the
|
||||
room level, as I3DL2 gives them. A change of environment crossfades each
|
||||
delay's read from its old length to its new one, and each gain, over
|
||||
REVERB_FADE_FRAMES: what is reverberating carries on, without the click of a
|
||||
jump or the pitch shift of a sliding delay. audio.reverb = false fades the
|
||||
reverb out over as long and stops it. Once nothing has been sent for
|
||||
REVERB_QUIET_FRAMES and the reverberation is 120 dB down, the reverb stops
|
||||
until something is sent again (outdoors, in the menus). */
|
||||
|
||||
#define REVERB_DELAY_SIZE 32768 /* the reflections and reverb delays, 0.3 + 0.1 s at most, and the taps */
|
||||
#define REVERB_TAPS 6
|
||||
#define REVERB_DIFFUSERS 4
|
||||
#define REVERB_DIFFUSER_SIZE 1024
|
||||
#define REVERB_LINES 8
|
||||
#define REVERB_LINE_SIZE 4096
|
||||
#define REVERB_FADE_FRAMES 4096 /* 85 ms */
|
||||
/* the input delays, then the diffusers, played out */
|
||||
#define REVERB_QUIET_FRAMES (REVERB_DELAY_SIZE + REVERB_DIFFUSERS * REVERB_DIFFUSER_SIZE)
|
||||
#define REVERB_QUIET_PEAK 1.0e-6f
|
||||
|
||||
/* in milliseconds: the taps after the reflections delay, the diffusers, and
|
||||
the lines at full density */
|
||||
static const float reverb_tap_times[REVERB_TAPS] = { 0.0f, 3.1f, 5.3f, 7.9f, 11.2f, 14.9f };
|
||||
static const float reverb_tap_signs[REVERB_TAPS] = { 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f };
|
||||
static const float reverb_diffuser_times[REVERB_DIFFUSERS] = { 4.7f, 3.6f, 12.7f, 9.3f };
|
||||
static const float reverb_line_times[REVERB_LINES] = { 29.7f, 37.1f, 41.1f, 43.7f, 53.3f, 59.9f, 67.1f, 73.1f };
|
||||
/* which lines go in negated, and the two outputs' signs of the lines */
|
||||
static const float reverb_input_signs[REVERB_LINES] = { 1, -1, -1, 1, 1, -1, -1, 1 };
|
||||
static const float reverb_left_signs[REVERB_LINES] = { 1, -1, 1, -1, 1, -1, 1, -1 };
|
||||
static const float reverb_right_signs[REVERB_LINES] = { 1, 1, -1, -1, 1, 1, -1, -1 };
|
||||
|
||||
/* what an environment sets: delays in samples, and gains */
|
||||
struct reverb_parameters
|
||||
{
|
||||
unsigned long taps[REVERB_TAPS];
|
||||
unsigned long late_delay;
|
||||
unsigned long lengths[REVERB_LINES];
|
||||
/* each line's gain a pass, and its low pass's coefficient */
|
||||
float feedback[REVERB_LINES];
|
||||
float damping[REVERB_LINES];
|
||||
float reflections_gain, late_gain;
|
||||
float diffusion;
|
||||
};
|
||||
|
||||
static struct
|
||||
{
|
||||
/* the bus of the current mix */
|
||||
float send[MIX_CHUNK_FRAMES];
|
||||
/* the environment serial and properties of the parameters faded to */
|
||||
unsigned long serial;
|
||||
DSI3DL2LISTENER properties;
|
||||
/* the parameters fading from and to, and the frames of the fade left
|
||||
(none: from is to) */
|
||||
struct reverb_parameters from, to;
|
||||
unsigned long fade;
|
||||
/* the output's level, 1 while audio.reverb is on, moving to 0 when it
|
||||
is turned off; whether the buffers are silent; and the frames since the
|
||||
send or the output was last over REVERB_QUIET_PEAK */
|
||||
float level;
|
||||
BOOL silent;
|
||||
unsigned long quiet;
|
||||
|
||||
/* the bus, delayed for the taps and the late reverberation */
|
||||
float delay[REVERB_DELAY_SIZE];
|
||||
unsigned long delay_position;
|
||||
|
||||
float diffusers[REVERB_DIFFUSERS][REVERB_DIFFUSER_SIZE];
|
||||
unsigned long diffuser_lengths[REVERB_DIFFUSERS];
|
||||
unsigned long diffuser_position;
|
||||
|
||||
float lines[REVERB_LINES][REVERB_LINE_SIZE];
|
||||
unsigned long line_position;
|
||||
/* each line's low pass */
|
||||
float lowpass[REVERB_LINES];
|
||||
} reverb;
|
||||
|
||||
static float clamp_real(float value, float minimum, float maximum)
|
||||
{
|
||||
return value < minimum ? minimum : value > maximum ? maximum : value;
|
||||
}
|
||||
|
||||
/* whole samples */
|
||||
static unsigned long reverb_samples(float seconds)
|
||||
{
|
||||
return (unsigned long)floorf(seconds * OUTPUT_RATE + 0.5f);
|
||||
}
|
||||
|
||||
/* the sample buffer got delay samples before position, crossfaded from the
|
||||
from delay to the to delay by fade (0 to 1) */
|
||||
static float reverb_read(const float *buffer, unsigned long mask, unsigned long position,
|
||||
unsigned long from_delay, unsigned long to_delay, float fade)
|
||||
{
|
||||
float from_sample = buffer[(position - from_delay) & mask];
|
||||
|
||||
if (from_delay == to_delay)
|
||||
return from_sample;
|
||||
return from_sample + (buffer[(position - to_delay) & mask] - from_sample) * fade;
|
||||
}
|
||||
|
||||
static float reverb_blend(float from, float to, float fade)
|
||||
{
|
||||
return from + (to - from) * fade;
|
||||
}
|
||||
|
||||
static void reverb_parameters_set(struct reverb_parameters *parameters, const DSI3DL2LISTENER *properties)
|
||||
{
|
||||
float decay = clamp_real(properties->flDecayTime, 0.1f, 20.0f);
|
||||
/* a low pass loses high frequencies; a ratio over 1 would need them to
|
||||
last longer */
|
||||
float high_decay = decay * clamp_real(properties->flDecayHFRatio, 0.1f, 1.0f);
|
||||
float scale = 0.25f + 0.75f * clamp_real(properties->flDensity / 100.0f, 0.0f, 1.0f);
|
||||
float cosine = frequency_cosine(properties->flHFReference);
|
||||
float mean_length = 0.0f, mean_feedback;
|
||||
unsigned long reflections_delay;
|
||||
int index;
|
||||
|
||||
for (index = 0; index < REVERB_LINES; index++)
|
||||
{
|
||||
float length = floorf(reverb_line_times[index] * 0.001f * OUTPUT_RATE * scale + 0.5f);
|
||||
float high_feedback;
|
||||
|
||||
parameters->lengths[index] = (unsigned long)length;
|
||||
parameters->feedback[index] = powf(10.0f, -3.0f * length / (decay * OUTPUT_RATE));
|
||||
high_feedback = powf(10.0f, -3.0f * length / (high_decay * OUTPUT_RATE));
|
||||
parameters->damping[index] = lowpass_coefficient(high_feedback / parameters->feedback[index], cosine);
|
||||
mean_length += length / REVERB_LINES;
|
||||
}
|
||||
mean_feedback = powf(10.0f, -3.0f * mean_length / (decay * OUTPUT_RATE));
|
||||
reflections_delay = reverb_samples(clamp_real(properties->flReflectionsDelay, 0.0f, 0.3f));
|
||||
for (index = 0; index < REVERB_TAPS; index++)
|
||||
parameters->taps[index] = reflections_delay + reverb_samples(reverb_tap_times[index] * 0.001f);
|
||||
parameters->late_delay = reflections_delay + reverb_samples(clamp_real(properties->flReverbDelay, 0.0f, 0.1f));
|
||||
/* the taps' energy, half of them to each side; the network's, whose
|
||||
sound circulates until it decays */
|
||||
parameters->reflections_gain = gain_from_millibels(properties->lReflections) /
|
||||
sqrtf((float)REVERB_TAPS / OUTPUT_CHANNELS);
|
||||
parameters->late_gain = gain_from_millibels(properties->lReverb) * sqrtf(1.0f - mean_feedback * mean_feedback);
|
||||
parameters->diffusion = 0.7f * clamp_real(properties->flDiffusion / 100.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
/* a new environment, faded to unless the reverb is silent. The game sets the
|
||||
same one again several times a second in some places (Derelict), which
|
||||
changes nothing */
|
||||
static void reverb_update(const DSI3DL2LISTENER *properties, unsigned long serial)
|
||||
{
|
||||
reverb.serial = serial;
|
||||
if (!reverb.silent && !memcmp(properties, &reverb.properties, sizeof(*properties)))
|
||||
return;
|
||||
reverb.properties = *properties;
|
||||
reverb_parameters_set(&reverb.to, properties);
|
||||
if (reverb.silent)
|
||||
reverb.from = reverb.to;
|
||||
reverb.fade = reverb.silent ? 0 : REVERB_FADE_FRAMES;
|
||||
}
|
||||
|
||||
static void reverb_clear(void)
|
||||
{
|
||||
memset(reverb.delay, 0, sizeof(reverb.delay));
|
||||
memset(reverb.diffusers, 0, sizeof(reverb.diffusers));
|
||||
memset(reverb.lines, 0, sizeof(reverb.lines));
|
||||
memset(reverb.lowpass, 0, sizeof(reverb.lowpass));
|
||||
reverb.from = reverb.to;
|
||||
reverb.fade = 0;
|
||||
reverb.silent = TRUE;
|
||||
reverb.quiet = 0;
|
||||
}
|
||||
|
||||
static void reverb_initialize(void)
|
||||
{
|
||||
int index;
|
||||
|
||||
for (index = 0; index < REVERB_DIFFUSERS; index++)
|
||||
reverb.diffuser_lengths[index] = reverb_samples(reverb_diffuser_times[index] * 0.001f);
|
||||
reverb.silent = TRUE;
|
||||
reverb.level = reverb_enabled ? 1.0f : 0.0f;
|
||||
reverb_update(&environment, environment_serial);
|
||||
}
|
||||
|
||||
/* adds the reverberation of send to output, its level moving to
|
||||
target_level */
|
||||
static void reverb_process(const float *send, float *output, unsigned long frames, float target_level)
|
||||
{
|
||||
const struct reverb_parameters *from = &reverb.from, *to = &reverb.to;
|
||||
float input_scale = 1.0f / sqrtf((float)REVERB_LINES), peak = 0.0f;
|
||||
unsigned long frame;
|
||||
int index;
|
||||
|
||||
reverb.silent = FALSE;
|
||||
for (frame = 0; frame < frames; frame++)
|
||||
{
|
||||
/* how far the fade from the old environment to the new is */
|
||||
float fade = 1.0f - (float)reverb.fade / REVERB_FADE_FRAMES;
|
||||
float early[OUTPUT_CHANNELS] = { 0.0f, 0.0f };
|
||||
float outputs[REVERB_LINES], late_left = 0.0f, late_right = 0.0f, feedback_sum = 0.0f, value;
|
||||
float diffusion = reverb_blend(from->diffusion, to->diffusion, fade);
|
||||
|
||||
reverb.delay[reverb.delay_position & (REVERB_DELAY_SIZE - 1)] = send[frame];
|
||||
for (index = 0; index < REVERB_TAPS; index++)
|
||||
{
|
||||
early[index % OUTPUT_CHANNELS] += reverb_tap_signs[index] * reverb_read(reverb.delay, REVERB_DELAY_SIZE - 1,
|
||||
reverb.delay_position, from->taps[index], to->taps[index], fade);
|
||||
}
|
||||
|
||||
/* diffusers: w = x + g w', y = w' - g w */
|
||||
value = reverb_read(reverb.delay, REVERB_DELAY_SIZE - 1, reverb.delay_position,
|
||||
from->late_delay, to->late_delay, fade);
|
||||
reverb.delay_position++;
|
||||
for (index = 0; index < REVERB_DIFFUSERS; index++)
|
||||
{
|
||||
float *buffer = reverb.diffusers[index];
|
||||
float delayed = buffer[(reverb.diffuser_position - reverb.diffuser_lengths[index]) & (REVERB_DIFFUSER_SIZE - 1)];
|
||||
float written = flush_denormal(value + diffusion * delayed);
|
||||
|
||||
buffer[reverb.diffuser_position & (REVERB_DIFFUSER_SIZE - 1)] = written;
|
||||
value = delayed - diffusion * written;
|
||||
}
|
||||
reverb.diffuser_position++;
|
||||
|
||||
/* the network: each line's output is damped and heard, then decayed
|
||||
and mixed back into every line by a Householder reflection */
|
||||
for (index = 0; index < REVERB_LINES; index++)
|
||||
{
|
||||
float line_output = reverb_read(reverb.lines[index], REVERB_LINE_SIZE - 1, reverb.line_position,
|
||||
from->lengths[index], to->lengths[index], fade);
|
||||
float damping = reverb_blend(from->damping[index], to->damping[index], fade);
|
||||
|
||||
reverb.lowpass[index] = flush_denormal(line_output + damping * (reverb.lowpass[index] - line_output));
|
||||
late_left += reverb_left_signs[index] * reverb.lowpass[index];
|
||||
late_right += reverb_right_signs[index] * reverb.lowpass[index];
|
||||
outputs[index] = reverb.lowpass[index] * reverb_blend(from->feedback[index], to->feedback[index], fade);
|
||||
feedback_sum += outputs[index];
|
||||
}
|
||||
feedback_sum *= 2.0f / REVERB_LINES;
|
||||
for (index = 0; index < REVERB_LINES; index++)
|
||||
{
|
||||
reverb.lines[index][reverb.line_position & (REVERB_LINE_SIZE - 1)] =
|
||||
flush_denormal(outputs[index] - feedback_sum + value * reverb_input_signs[index] * input_scale);
|
||||
}
|
||||
reverb.line_position++;
|
||||
|
||||
{
|
||||
float reflections_gain = reverb_blend(from->reflections_gain, to->reflections_gain, fade);
|
||||
float late_gain = reverb_blend(from->late_gain, to->late_gain, fade);
|
||||
float left = early[0] * reflections_gain + late_left * late_gain;
|
||||
float right = early[1] * reflections_gain + late_right * late_gain;
|
||||
|
||||
output[frame * 2] += left * reverb.level;
|
||||
output[frame * 2 + 1] += right * reverb.level;
|
||||
peak = fabsf(left) > peak ? fabsf(left) : peak;
|
||||
peak = fabsf(right) > peak ? fabsf(right) : peak;
|
||||
peak = fabsf(send[frame]) > peak ? fabsf(send[frame]) : peak;
|
||||
}
|
||||
if (reverb.fade && !--reverb.fade)
|
||||
reverb.from = reverb.to;
|
||||
reverb.level = clamp_real(target_level, reverb.level - 1.0f / REVERB_FADE_FRAMES,
|
||||
reverb.level + 1.0f / REVERB_FADE_FRAMES);
|
||||
}
|
||||
reverb.quiet = peak > REVERB_QUIET_PEAK ? 0 : reverb.quiet + frames;
|
||||
}
|
||||
|
||||
/* whether anything is sent to the reverb in send */
|
||||
static BOOL reverb_sent(const float *send, unsigned long frames)
|
||||
{
|
||||
unsigned long frame;
|
||||
|
||||
for (frame = 0; frame < frames; frame++)
|
||||
{
|
||||
if (fabsf(send[frame]) > REVERB_QUIET_PEAK)
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* ---------- limiter
|
||||
@@ -646,12 +1063,33 @@ static void limit(float *output, unsigned long frames)
|
||||
static void mix(float *output, unsigned long frames)
|
||||
{
|
||||
struct sdl_stream *stream;
|
||||
DSI3DL2LISTENER properties;
|
||||
BOOL enabled = reverb_enabled, changed = FALSE;
|
||||
unsigned long serial = 0;
|
||||
|
||||
memset(output, 0, frames * OUTPUT_CHANNELS * sizeof(float));
|
||||
memset(reverb.send, 0, frames * sizeof(float));
|
||||
pthread_mutex_lock(&mixer_lock);
|
||||
for (stream = streams; stream; stream = stream->next)
|
||||
mix_voice(stream, output, frames);
|
||||
mix_voice(stream, output, reverb.send, frames);
|
||||
/* a new environment, once the fade to the last is over */
|
||||
if (reverb.serial != environment_serial && !reverb.fade)
|
||||
{
|
||||
properties = environment;
|
||||
serial = environment_serial;
|
||||
changed = TRUE;
|
||||
}
|
||||
pthread_mutex_unlock(&mixer_lock);
|
||||
if (changed)
|
||||
reverb_update(&properties, serial);
|
||||
/* the reverb runs while something reverberates, and stops once it is
|
||||
turned off and faded out, or quiet */
|
||||
if (!reverb.silent || (enabled && reverb_sent(reverb.send, frames)))
|
||||
{
|
||||
reverb_process(reverb.send, output, frames, enabled ? 1.0f : 0.0f);
|
||||
if ((!enabled && reverb.level <= 0.0f) || reverb.quiet > REVERB_QUIET_FRAMES)
|
||||
reverb_clear();
|
||||
}
|
||||
limit(output, frames);
|
||||
}
|
||||
|
||||
@@ -710,8 +1148,10 @@ static void audio_start(void)
|
||||
return;
|
||||
audio_started = TRUE;
|
||||
master_volume = (float)config_real("audio.volume");
|
||||
reverb_enabled = config_boolean("audio.reverb");
|
||||
resampler_initialize();
|
||||
resampler_phases_initialize();
|
||||
reverb_initialize();
|
||||
|
||||
if (config_boolean("audio.enabled") && platform_sdl_initialize())
|
||||
{
|
||||
@@ -956,12 +1396,14 @@ VOID WINAPI DirectSoundDoWork(void)
|
||||
{
|
||||
static unsigned long volume_read_at = (unsigned long)-1;
|
||||
|
||||
/* (audio.volume read again when Settings changes it: on the game's
|
||||
thread, not the mixer's, whose lock the config's file I/O would hold) */
|
||||
/* (audio.volume and audio.reverb read again when the settings change: on
|
||||
the game's thread, not the mixer's, whose lock the config's file I/O
|
||||
would hold) */
|
||||
if (volume_read_at != config_changes())
|
||||
{
|
||||
volume_read_at = config_changes();
|
||||
master_volume = (float)config_real("audio.volume");
|
||||
reverb_enabled = config_boolean("audio.reverb");
|
||||
}
|
||||
streams_complete_finished();
|
||||
}
|
||||
@@ -1002,7 +1444,16 @@ HRESULT WINAPI IDirectSound_DownloadEffectsImage(LPDIRECTSOUND sound, LPCVOID im
|
||||
|
||||
HRESULT WINAPI IDirectSound_CommitDeferredSettings(LPDIRECTSOUND sound) { (void)sound; return DS_OK; }
|
||||
HRESULT WINAPI IDirectSound_SetMixBinHeadroom(LPDIRECTSOUND sound, DWORD mix_bin_mask, DWORD headroom) { (void)sound; (void)mix_bin_mask; (void)headroom; return DS_OK; }
|
||||
HRESULT WINAPI IDirectSound_SetI3DL2Listener(LPDIRECTSOUND sound, LPCDSI3DL2LISTENER listener_properties, DWORD apply) { (void)sound; (void)listener_properties; (void)apply; return DS_OK; }
|
||||
HRESULT WINAPI IDirectSound_SetI3DL2Listener(LPDIRECTSOUND sound, LPCDSI3DL2LISTENER listener_properties, DWORD apply)
|
||||
{
|
||||
(void)sound;
|
||||
(void)apply;
|
||||
pthread_mutex_lock(&mixer_lock);
|
||||
environment = *listener_properties;
|
||||
environment_serial++;
|
||||
pthread_mutex_unlock(&mixer_lock);
|
||||
return DS_OK;
|
||||
}
|
||||
|
||||
HRESULT WINAPI IDirectSound_SetDistanceFactor(LPDIRECTSOUND sound, FLOAT factor, DWORD apply)
|
||||
{
|
||||
@@ -1095,7 +1546,6 @@ HRESULT WINAPI IDirectSound_CreateSoundStream(LPDIRECTSOUND sound, LPCDSSTREAMDE
|
||||
stream->mode = DS3DMODE_NORMAL;
|
||||
stream->minimum_distance = DS3D_DEFAULTMINDISTANCE;
|
||||
stream->maximum_distance = DS3D_DEFAULTMAXDISTANCE;
|
||||
stream->i3dl2_gain = 1.0f;
|
||||
resampler_reset(stream);
|
||||
pthread_mutex_lock(&mixer_lock);
|
||||
stream->next = streams;
|
||||
@@ -1195,17 +1645,29 @@ HRESULT WINAPI IDirectSoundStream_SetMaxDistance(LPDIRECTSOUNDSTREAM stream, FLO
|
||||
|
||||
HRESULT WINAPI IDirectSoundStream_SetI3DL2Source(LPDIRECTSOUNDSTREAM stream, LPCDSI3DL2BUFFER source, DWORD apply)
|
||||
{
|
||||
LONG direct;
|
||||
LONG direct, direct_hf, room, room_hf;
|
||||
|
||||
(void)apply;
|
||||
/* the low frequency part of the direct path */
|
||||
/* the direct path: at low frequencies obstruction and occlusion take
|
||||
their LF ratio of their high frequency levels, which (with the source's
|
||||
own) muffle it above the HF reference */
|
||||
direct = source->lDirect +
|
||||
(LONG)(source->Obstruction.lHFLevel * source->Obstruction.flLFRatio) +
|
||||
(LONG)(source->Occlusion.lHFLevel * source->Occlusion.flLFRatio);
|
||||
if (direct > 0)
|
||||
direct = 0;
|
||||
direct_hf = source->lDirect + source->lDirectHF + source->Obstruction.lHFLevel + source->Occlusion.lHFLevel;
|
||||
/* the room send: occlusion muffles it as it does the direct path, and
|
||||
obstruction does not reach it */
|
||||
room = source->lRoom + (LONG)(source->Occlusion.lHFLevel * source->Occlusion.flLFRatio);
|
||||
room_hf = source->lRoom + source->lRoomHF + source->Occlusion.lHFLevel;
|
||||
{
|
||||
STREAM_SETTER(record->i3dl2_gain = gain_from_millibels(direct))
|
||||
STREAM_SETTER(
|
||||
record->direct = direct;
|
||||
record->direct_hf = direct_hf;
|
||||
record->room = room;
|
||||
record->room_hf = room_hf;
|
||||
record->room_rolloff_factor = source->flRoomRolloffFactor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,10 @@ static const struct config_setting config_settings[] =
|
||||
{ "audio.effects_volume", _config_real, "1.0", "HALO_EFFECTS_VOLUME", _environment_value, _platform_all,
|
||||
"The volume of every other sound (effects and speech), 0.0 to 1.0 (of\n"
|
||||
"audio.volume)." },
|
||||
{ "audio.reverb", _config_boolean, "true", "HALO_REVERB", _environment_value, _platform_all,
|
||||
"Reverberate the world's sounds as the place the player is in does (the\n"
|
||||
"maps' sound environments, as the Xbox's I3DL2 reverb did); false keeps\n"
|
||||
"them dry." },
|
||||
|
||||
{ "input.mouse_sensitivity", _config_real, "1.0", "HALO_MOUSE_SENSITIVITY", _environment_value, _platform_desktop,
|
||||
"How far the view turns for the mouse's movement." },
|
||||
|
||||
Reference in new issue
Block a user