diff --git a/port/linux/README.md b/port/linux/README.md index 4819c9db..7a60db9a 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -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. | diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index 08a3fe6f..713248a0 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -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) } } diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index c7554eac..0b433835 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -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." },