From f396577e8f7fe5ec79f80db65959e4018e4136bf Mon Sep 17 00:00:00 2001 From: frankyfife <16177808+frankyfife@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:15:38 +0200 Subject: [PATCH] Add 5.1 surround sound The mixer outputs stereo or 5.1 (audio.channels: "auto", "stereo" or "5.1"; HALO_AUDIO_CHANNELS; Linux and Windows). On 5.1 the speaker config reports AC-3, as an Xbox set to Dolby Digital, so the game sends its 2D voices to the center and back mix bins itself; the speaker mix bins 0-5 are SDL's 5.1 channel order. 3D voices pan with equal power between adjacent speakers at the ITU-R BS.775 angles, spread out when close or above, at the stereo pan's power; stereo voices positioned in the world pan their middle the same way and keep their width on the front pair. The voices' resampling, low passes and reverb send are the stereo mix's; the reverb plays from the front and back pairs, the voice chat from the front pair, and the limiter takes all six channels. Stereo output mixes as before. Android and the web stay stereo. "auto" is the Xbox's own behaviour: the game asked the console's speaker setting and mixed for 5.1 when Dolby Digital was on. Checked on Windows with a 6-channel device: "sound: 5.1 surround" is logged and the device opens; with "stereo", and with no 6-channel device, the output is the stereo mix as before. Co-Authored-By: Claude Opus 5.5 --- port/linux/README.md | 3 +- port/linux/src/dsound_sdl.c | 319 ++++++++++++++++++++++++++++++++--- port/linux/src/port_config.c | 3 + 3 files changed, 301 insertions(+), 24 deletions(-) diff --git a/port/linux/README.md b/port/linux/README.md index 2ffc42c7da..af8e99ce6c 100644 --- a/port/linux/README.md +++ b/port/linux/README.md @@ -370,6 +370,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.channels` | `"auto"` | `HALO_AUDIO_CHANNELS` | `"stereo"`, or `"5.1"` for surround sound as with Dolby Digital on the Xbox. `"auto"`: 5.1 if the sound device has 6 or more channels. Linux and Windows only. | | `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). | | `audio.resampler` | `"sinc"` | `HALO_AUDIO_RESAMPLER` | How the sounds are converted to the output's rate (48 kHz): `"sinc"` with a band-limited low pass, which keeps out the images above the sound's own rate; `"linear"` between neighbouring samples, which lets some of them through and sounds brighter for it. Audio Setup's SOUND PROCESSING screen sets it, with `audio.reverb`. | | `audio.voice_chat` | `"push_to_talk"` | `HALO_VOICE_CHAT` | How you talk in voice chat: `"push_to_talk"` (while `controls.push_to_talk` is held; the microphone opens when you first press it), `"open_mic"` (when the microphone hears speech), or `"off"`. You hear the other players in every case. Refer to "Voice chat". | @@ -1216,7 +1217,7 @@ crafted file) is refused, and the level starts over. | High-res HUD | The HUD is drawn from high-res assets: redraws at 8x the size of the maps' bitmaps (4x for the largest), in `port/assets/hud`. They cover the meters, counters, panels and their outlines, the motion sensor, reticles, waypoints and scopes, but no bitmap with English text. `tools/hud_assets.py` makes them from the SVG redraws (the weapons' reticles from those of the PC HUD sheet that match the Xbox's exactly, the sniper rifle's 2x and 10x set in Overpass), and the build embeds them in the executable. When the game uploads one of those bitmaps, `src/hud_hires.c` gives the high-res texture in its place, if the bitmap's pixels are those of the English maps: another language's maps keep their own. The game sizes and places the HUD from its tags as before. `display.high_res_hud = false` turns this off. | | High-res text | The menus' and HUD's text is drawn with Overpass (`port/assets/fonts`, SIL Open Font License) in place of the maps' bitmap fonts, which are Interstate. `src/text_hires.c` rasterizes each glyph with stb_truetype (`port/third_party/stb`) at the resolution the game draws at, into an atlas that a placeholder bitmap of the game stands for. The game lays the text out from its font tags as before: each font is as tall as its tag's capitals, and narrowed where the tag's letters are narrower than the Xbox maps' (the fonts of a Custom Edition `loc.map`), so that its letters do not run into each other. The menus' titles (the screens' headers and the main menu's items) are pictures of text in the maps, so they are drawn as the high-res HUD is: `tools/title_assets.py` sets each one again in OpenCE, Roger White's public-domain Newtown respaced to match the maps' commercial title typeface (`tools/title_font.py`), at 4x the bitmap's size over its own plate or glow, each letter placed where the map's letter is, in `port/assets/titles`. The postgame carnage report's title is set over a hand-made SVG redraw of its panel (`port/assets/titles/svg`) instead. The A, B, X and Y button icons beside the menus' key labels and in their text ("Press A to Join") are drawn from hand-made SVG redraws at 8x the size of the maps' bitmaps (`port/assets/buttons`, made by `tools/button_assets.py`), in the same sizes and places. `display.high_res_text = false` turns it off. | | Anti-aliasing | Off unless `display.anti_aliasing` is set (`src/d3d8_gl.c`, `src/xgpu_post.c`). FXAA (written in the port) and SMAA (`port/third_party/smaa`, MIT licensed, at its HIGH preset, compiled as GLSL) are passes over the 3D view of each window, after the lens flares and before the HUD and the menus (`render/render.c`). Their programs are built when the setting is chosen. Supersampling draws the render targets the size of the screen at two times the resolution in each direction, and the display blit scales them down. Multisampling draws the back buffer and its depth buffer into multisampled renderbuffers, and with them any target that is drawn together with one of them (a mirror's view, in the secondary target with the back buffer's depth buffer), so that the attachments of a framebuffer are all multisampled or none is. A target's pixels are resolved into its texture before something reads the texture (as a texture, or at the display blit). Visibility tests count samples, divided by the samples of a pixel. An alpha-tested surface (foliage, grates) covers the samples of a pixel in proportion to its alpha past the reference (`gl_SampleMask`, not on Android). | -| Sound | Xbox DirectSound on SDL3 audio (`src/dsound_sdl.c`): PCM and Xbox ADPCM, mixed at 48 kHz, with volume, pitch, mix bins, distance, stereo pan, occlusion and obstruction. There is no Doppler effect, no cones and no reverb. | +| Sound | Xbox DirectSound on SDL3 audio (`src/dsound_sdl.c`): PCM and Xbox ADPCM, mixed at 48 kHz in stereo or 5.1, with volume, pitch, mix bins, distance, pan, occlusion and obstruction. There is no Doppler effect and no cones. On 5.1 the game gets the AC-3 speaker setting of the Xbox and sends its 2D sounds to the center and back speakers, 3D sounds pan across the five speakers, and the reverb plays from the front and back pairs. The game sends nothing to the LFE channel. | | Input | XInput on SDL3 (`src/xinput_sdl.c`): keyboard, mouse, gamepads with rumble, and the debug keyboard for the console. | | Files | The Win32 file functions and the MSVC file functions on POSIX, with the translation of Xbox paths. | | Threads | Threads, events, mutexes, critical sections, interlocked operations and alertable waits. | diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c index abf6ba659f..30c1c71060 100644 --- a/port/linux/src/dsound_sdl.c +++ b/port/linux/src/dsound_sdl.c @@ -22,6 +22,15 @@ resampling) and applies: (reverb). Doppler and cones are not modelled. A look-ahead limiter keeps the sum under full scale (limit). +The output is stereo or 5.1 (audio.channels). For 5.1 the speaker config +reports AC-3, as an Xbox set to Dolby Digital does: the game then sends its +2D voices to the center and back mix bins too (dsound_initialize_channel), +3D voices and stereo voices positioned in the world are panned around the +five full range speakers (surround_pan), and the reverb plays from the +front and back pairs. The mix bins 0-5 are in SDL's 5.1 order (FL, FR, FC, +LFE, BL, BR), so bin n is output channel n. The game sends nothing to the +LFE bin. Stereo output mixes exactly as before. + Packets the mixer has finished are completed from DirectSoundDoWork, which the game calls every frame, and from Flush, never from the audio thread: the game's completion callback is not meant to run concurrently with it. @@ -32,7 +41,8 @@ buffer, so streams still drain at their real rate. audio.volume sets the master volume (default 1.0); audio.reverb = false turns the reverb off; audio.resampler = "linear" resamples between neighbouring frames instead of with the sinc low pass; audio.enabled = false -skips opening a device (port_config.c). +skips opening a device; audio.channels chooses stereo or 5.1 +(port_config.c). */ #include "platform.h" @@ -49,7 +59,8 @@ skips opening a device (port_config.c). #include #define OUTPUT_RATE 48000 -#define OUTPUT_CHANNELS 2 +#define OUTPUT_CHANNELS 2 /* the stereo mix's, and the reverb's outputs */ +#define MAXIMUM_OUTPUT_CHANNELS 6 #define MAXIMUM_STREAM_PACKETS 64 #define MIX_CHUNK_FRAMES 1024 @@ -107,6 +118,9 @@ struct sdl_stream float stereo_distance; float stereo_distance_fade; float headroom; + /* on 5.1 (output_channels 6): the gains from each source channel to + each speaker mix bin (SetMixBins, SetMixBinVolumes) */ + float surround_bins[2][MAXIMUM_OUTPUT_CHANNELS]; /* 3D */ BOOL has_3d; @@ -141,6 +155,7 @@ struct sdl_stream clicks */ float current_left, current_right, current_room; float current_direct_lowpass, current_room_lowpass; + float current_surround[2][MAXIMUM_OUTPUT_CHANNELS]; BOOL gains_valid; /* the direct path's low pass, for each channel, and the room send's */ float direct_lowpass[2]; @@ -163,6 +178,9 @@ static struct } listener = { { 0, 0, 0 }, { 0, 0, 1 }, { 0, 1, 0 }, 1.0f, 1.0f }; static float master_volume = 1.0f; +/* 2 or 6 (audio.channels), chosen when the device first opens; the game +sets its mix bins by it, so it holds while the game runs */ +static unsigned long output_channels = 2; /* the I3DL2 listener properties (SetI3DL2Listener), which the reverb models: DirectSound's defaults until the game sets an environment, their room level @@ -457,6 +475,138 @@ static void voice_gains(const struct sdl_stream *stream, float *left, float *rig *room *= stream->volume * master_volume; } +/* the 5.1 speakers around the listener, clockwise from the center, and +their directions in radians (ITU-R BS.775: fronts at 30 degrees, backs at +110) */ +#define SURROUND_RING_SPEAKERS 5 +static const unsigned long surround_ring_channel[SURROUND_RING_SPEAKERS] = { 2, 1, 5, 4, 0 }; +static const float surround_ring_angle[SURROUND_RING_SPEAKERS + 1] = +{ + 0.0f, 0.52359878f, 1.91986218f, 4.36332313f, 5.75958653f, 6.28318531f, +}; + +/* the speaker gains of a sound from a direction, side (right) and ahead: an +equal power pan between the two speakers either side of it, blended (in +power) towards all five equally by spread, 0 to 1. Their power sums to 1, as +the stereo pan's does */ +static void surround_pan(float side, float ahead, float spread, float *gains) +{ + float angle = atan2f(side, ahead); + unsigned long segment, speaker; + + for (speaker = 0; speaker < MAXIMUM_OUTPUT_CHANNELS; speaker++) + gains[speaker] = 0.0f; + if (angle < 0.0f) + angle += 6.28318531f; + for (segment = 0; segment < SURROUND_RING_SPEAKERS - 1; segment++) + { + if (angle < surround_ring_angle[segment + 1]) + break; + } + { + float width = surround_ring_angle[segment + 1] - surround_ring_angle[segment]; + float position = (angle - surround_ring_angle[segment]) / width; + + position = position < 0.0f ? 0.0f : position > 1.0f ? 1.0f : position; + gains[surround_ring_channel[segment]] = cosf(position * 1.57079633f); + gains[surround_ring_channel[(segment + 1) % SURROUND_RING_SPEAKERS]] = sinf(position * 1.57079633f); + } + for (speaker = 0; speaker < SURROUND_RING_SPEAKERS; speaker++) + { + float *gain = &gains[surround_ring_channel[speaker]]; + + *gain = sqrtf((1.0f - spread) * *gain * *gain + spread / SURROUND_RING_SPEAKERS); + } +} + +/* on 5.1, a voice's gains from each source channel to each speaker (the +direct path's; its room send and low passes are voice_gains'): + - a 3D voice is panned around the speakers from its direction, a little + spread so that it never vanishes between two, spread out towards all + five inside its minimum distance and above or below, as the stereo pan + centres those; + - a stereo voice positioned in the world has its channels' middle (the + first row) panned towards it in the front half, and their difference + (the second) kept on the front pair as wide as the stereo mix keeps it; + - a 2D voice goes by its mix bins. */ +static void voice_surround_gains(const struct sdl_stream *stream, float gains[2][MAXIMUM_OUTPUT_CHANNELS]) +{ + float scale = stream->volume * master_volume; + unsigned long speaker; + + memset(gains, 0, 2 * sizeof(gains[0])); + if (stream->has_3d && stream->mode != DS3DMODE_DISABLE) + { + float offset[3], right_axis[3], side, ahead, distance, horizontal, attenuation, spread = 0.1f; + int axis; + + if (stream->mode == DS3DMODE_HEADRELATIVE) + { + for (axis = 0; axis < 3; axis++) + offset[axis] = stream->position[axis]; + side = offset[0]; + ahead = offset[2]; + } + else + { + for (axis = 0; axis < 3; axis++) + offset[axis] = stream->position[axis] - listener.position[axis]; + right_axis[0] = listener.top[1] * listener.front[2] - listener.top[2] * listener.front[1]; + right_axis[1] = listener.top[2] * listener.front[0] - listener.top[0] * listener.front[2]; + right_axis[2] = listener.top[0] * listener.front[1] - listener.top[1] * listener.front[0]; + side = dot3(offset, right_axis); + ahead = dot3(offset, listener.front); + } + horizontal = sqrtf(side * side + ahead * ahead); + distance = sqrtf(dot3(offset, offset)); + if (horizontal <= 1.0e-4f) + spread = 1.0f; + else if (distance > 1.0e-4f && 0.5f * (1.0f - horizontal / distance) > spread) + spread = 0.5f * (1.0f - horizontal / distance); + if (distance < stream->minimum_distance && stream->minimum_distance > 0.0f && + 1.0f - distance / stream->minimum_distance > spread) + { + spread = 1.0f - distance / stream->minimum_distance; + } + surround_pan(side, ahead, spread, gains[0]); + attenuation = distance_attenuation(stream, distance, listener.rolloff_factor) * + gain_from_millibels(stream->direct); + for (speaker = 0; speaker < MAXIMUM_OUTPUT_CHANNELS; speaker++) + { + gains[0][speaker] *= attenuation; + /* a stereo 3D voice: both channels from where it is */ + if (stream->channels == 2) + gains[1][speaker] = gains[0][speaker] *= 0.5f; + } + } + else if (stream->channels == 2 && stream->stereo_positioned) + { + float pan = stream->stereo_pan < -1.0f ? -1.0f : stream->stereo_pan > 1.0f ? 1.0f : stream->stereo_pan; + float angle = (pan + 1.0f) * 0.25f * 3.14159265f; + float direct = gain_from_millibels(stream->direct); + float far_gain = (cosf(angle) < sinf(angle) ? cosf(angle) : sinf(angle)) * 1.41421356f; + + /* the middle at the stereo pan's level (1 on each side when centred, + a power of 2) */ + surround_pan(sinf(pan * 1.57079633f), cosf(pan * 1.57079633f), 0.1f, gains[0]); + for (speaker = 0; speaker < MAXIMUM_OUTPUT_CHANNELS; speaker++) + gains[0][speaker] *= 1.41421356f * stream->mix_left * direct; + gains[1][0] = far_gain * stream->mix_left * direct; + gains[1][1] = -far_gain * stream->mix_right * direct; + } + else + { + memcpy(gains, stream->surround_bins, 2 * sizeof(gains[0])); + if (stream->channels == 1) + memset(gains[1], 0, sizeof(gains[1])); + } + for (speaker = 0; speaker < MAXIMUM_OUTPUT_CHANNELS; speaker++) + { + gains[0][speaker] *= scale; + gains[1][speaker] *= scale; + } +} + /* ---------- resampling Each voice is resampled to the output rate by band-limited interpolation (J. @@ -705,8 +855,8 @@ static BOOL voice_has_frames(struct sdl_stream *stream) /* ---------- mixing */ -/* mixes one voice into output (frames of stereo float), and into the reverb's -send (frames of mono) */ +/* mixes one voice into output (frames of output_channels floats), 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; @@ -718,6 +868,11 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns unsigned long frame; /* (a stereo voice panned towards its sound: voice_gains) */ BOOL positioned = stream->channels == 2 && stream->stereo_positioned; + /* on 5.1, the speakers' gains (voice_surround_gains) */ + BOOL surround = output_channels == 6; + float target_surround[2][MAXIMUM_OUTPUT_CHANNELS], surround_gains[2][MAXIMUM_OUTPUT_CHANNELS]; + float ramp_surround[2][MAXIMUM_OUTPUT_CHANNELS]; + unsigned long speaker; if (stream->paused || !stream->packet_count || !stream->sample_rate) return; @@ -737,8 +892,12 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns stream->gains_valid = FALSE; } voice_gains(stream, &target_left, &target_right, &target_room, &target_direct_lowpass, &target_room_lowpass); + if (surround) + voice_surround_gains(stream, target_surround); if (!stream->gains_valid) { + if (surround) + memcpy(stream->current_surround, target_surround, sizeof(target_surround)); stream->current_left = target_left; stream->current_right = target_right; stream->current_room = target_room; @@ -763,6 +922,16 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns 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; + if (surround) + { + for (speaker = 0; speaker < MAXIMUM_OUTPUT_CHANNELS; speaker++) + { + surround_gains[0][speaker] = stream->current_surround[0][speaker]; + surround_gains[1][speaker] = stream->current_surround[1][speaker]; + ramp_surround[0][speaker] = (target_surround[0][speaker] - surround_gains[0][speaker]) / (float)frames; + ramp_surround[1][speaker] = (target_surround[1][speaker] - surround_gains[1][speaker]) / (float)frames; + } + } for (frame = 0; frame < frames; frame++) { @@ -860,7 +1029,23 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns } direct_state[0] = sample_left; direct_state[1] = sample_right; - if (stream->channels == 1) + if (surround) + { + /* the first row's gains take the left (a mono voice's only) + channel, or a positioned voice's middle; the second the right, or + its difference */ + float *speakers = output + frame * MAXIMUM_OUTPUT_CHANNELS; + float first = positioned ? 0.5f * (sample_left + sample_right) : sample_left; + float second = positioned ? 0.5f * (sample_left - sample_right) : stream->channels == 2 ? sample_right : 0.0f; + + for (speaker = 0; speaker < MAXIMUM_OUTPUT_CHANNELS; speaker++) + { + speakers[speaker] += first * surround_gains[0][speaker] + second * surround_gains[1][speaker]; + surround_gains[0][speaker] += ramp_surround[0][speaker]; + surround_gains[1][speaker] += ramp_surround[1][speaker]; + } + } + else if (stream->channels == 1) { output[frame * 2] += sample_left * left; output[frame * 2 + 1] += sample_left * right; @@ -902,6 +1087,8 @@ static void mix_voice(struct sdl_stream *stream, float *output, float *send, uns stream->current_room = target_room; stream->current_direct_lowpass = target_direct_lowpass; stream->current_room_lowpass = target_room_lowpass; + if (surround) + memcpy(stream->current_surround, target_surround, sizeof(target_surround)); } /* ---------- reverb @@ -1169,8 +1356,19 @@ static void reverb_process(const float *send, float *output, unsigned long frame 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; + if (output_channels == 6) + { + /* from the front and back pairs, at the same power */ + output[frame * 6] += left * reverb.level * 0.70710678f; + output[frame * 6 + 1] += right * reverb.level * 0.70710678f; + output[frame * 6 + 4] += left * reverb.level * 0.70710678f; + output[frame * 6 + 5] += right * reverb.level * 0.70710678f; + } + else + { + 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; @@ -1215,7 +1413,7 @@ The gain comes back up over LIMITER_RELEASE_SECONDS. */ static struct { /* the frames the output is delayed by, and the gain each needs */ - float delay[LIMITER_LOOKAHEAD][OUTPUT_CHANNELS]; + float delay[LIMITER_LOOKAHEAD][MAXIMUM_OUTPUT_CHANNELS]; float needed[LIMITER_LOOKAHEAD]; /* how many of them are under 1: none while nothing is too loud, when the smallest is 1 without looking */ @@ -1247,13 +1445,13 @@ static void limit(float *output, unsigned long frames) } for (frame = 0; frame < frames; frame++) { - float *sample = output + frame * OUTPUT_CHANNELS; + float *sample = output + frame * output_channels; unsigned long position = limiter.position; unsigned long oldest = (position + 1) % LIMITER_LOOKAHEAD; float peak = 0.0f, lowest, gain; /* the new frame takes the slot of the oldest, which has played */ - for (channel = 0; channel < OUTPUT_CHANNELS; channel++) + for (channel = 0; channel < output_channels; channel++) { if (fabsf(sample[channel]) > peak) peak = fabsf(sample[channel]); @@ -1280,7 +1478,7 @@ static void limit(float *output, unsigned long frames) limiter.history[position] = limiter.held; gain = (float)(limiter.history_sum / LIMITER_LOOKAHEAD); /* the frame LIMITER_LOOKAHEAD - 1 frames old plays */ - for (channel = 0; channel < OUTPUT_CHANNELS; channel++) + for (channel = 0; channel < output_channels; channel++) sample[channel] = limiter.delay[oldest][channel] * gain; limiter.position = oldest; } @@ -1293,7 +1491,7 @@ static void mix(float *output, unsigned long frames) BOOL enabled = reverb_enabled, changed = FALSE; unsigned long serial = 0; - memset(output, 0, frames * OUTPUT_CHANNELS * sizeof(float)); + 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) @@ -1316,8 +1514,25 @@ static void mix(float *output, unsigned long frames) if ((!enabled && reverb.level <= 0.0f) || reverb.quiet > REVERB_QUIET_FRAMES) reverb_clear(); } - /* the players' voices (voice_audio.c), dry, under the limiter */ - voice_audio_mix(output, frames); + /* the players' voices (voice_audio.c), dry, under the limiter; on 5.1 + from the front pair */ + if (output_channels == 6) + { + float voices[MIX_CHUNK_FRAMES * OUTPUT_CHANNELS]; + unsigned long frame; + + memset(voices, 0, frames * OUTPUT_CHANNELS * sizeof(float)); + voice_audio_mix(voices, frames); + for (frame = 0; frame < frames; frame++) + { + output[frame * 6] += voices[frame * 2]; + output[frame * 6 + 1] += voices[frame * 2 + 1]; + } + } + else + { + voice_audio_mix(output, frames); + } limit(output, frames); } @@ -1339,28 +1554,28 @@ static const char *audio_device_setting(void) static void SDLCALL audio_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount) { - float buffer[MIX_CHUNK_FRAMES * OUTPUT_CHANNELS]; + float buffer[MIX_CHUNK_FRAMES * MAXIMUM_OUTPUT_CHANNELS]; (void)userdata; (void)total_amount; while (additional_amount > 0) { - unsigned long frames = (unsigned long)additional_amount / (OUTPUT_CHANNELS * sizeof(float)); + unsigned long frames = (unsigned long)additional_amount / (output_channels * sizeof(float)); if (frames > MIX_CHUNK_FRAMES) frames = MIX_CHUNK_FRAMES; if (!frames) frames = 1; mix(buffer, frames); - SDL_PutAudioStreamData(stream, buffer, (int)(frames * OUTPUT_CHANNELS * sizeof(float))); - additional_amount -= (int)(frames * OUTPUT_CHANNELS * sizeof(float)); + SDL_PutAudioStreamData(stream, buffer, (int)(frames * output_channels * sizeof(float))); + additional_amount -= (int)(frames * output_channels * sizeof(float)); } } /* without a device, drain voices in real time */ static void *silent_clock_thread(void *parameter) { - float buffer[480 * OUTPUT_CHANNELS]; + float buffer[480 * MAXIMUM_OUTPUT_CHANNELS]; struct timespec next; (void)parameter; @@ -1379,6 +1594,30 @@ static void *silent_clock_thread(void *parameter) return NULL; } +/* audio.channels: "stereo", "5.1", or "auto" for 5.1 when the device has +at least six channels. Android and the browser play stereo */ +static unsigned long choose_output_channels(SDL_AudioDeviceID device) +{ +#if defined(HALO_ANDROID) || defined(HALO_WEB) || defined(HALO_ARM64_GUEST) + (void)device; + return 2; +#else + const char *setting = config_string("audio.channels"); + SDL_AudioSpec format; + + if (!strcmp(setting, "stereo")) + return 2; + if (!strcmp(setting, "5.1")) + return 6; + if (strcmp(setting, "auto")) + platform_log("audio.channels \"%s\" is not \"auto\", \"stereo\" or \"5.1\"; using auto", setting); + if ((SDL_GetAudioDeviceFormat(device, &format, NULL) || + SDL_GetAudioDeviceFormat(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &format, NULL)) && format.channels >= 6) + return 6; + return 2; +#endif +} + static void audio_start(void) { SDL_AudioSpec spec; @@ -1395,8 +1634,11 @@ static void audio_start(void) if (config_boolean("audio.enabled") && platform_sdl_initialize()) { + snprintf(audio_device_name, sizeof(audio_device_name), "%s", audio_device_setting()); + output_channels = choose_output_channels(platform_audio_device(FALSE, audio_device_name)); + platform_log("sound: %s", output_channels == 6 ? "5.1 surround" : "stereo"); spec.format = SDL_AUDIO_F32; - spec.channels = OUTPUT_CHANNELS; + spec.channels = (int)output_channels; spec.freq = OUTPUT_RATE; #ifdef HALO_ARM64_GUEST /* frames per callback: on Android each callback is handed to a thread @@ -1421,6 +1663,7 @@ static void audio_start(void) return; } platform_log("cannot open an audio device (%s); sound is silent", SDL_GetError()); + output_channels = 2; } { pthread_t thread; @@ -1445,7 +1688,9 @@ void dsound_sdl_output_device_check(void) return; snprintf(audio_device_name, sizeof(audio_device_name), "%s", audio_device_setting()); spec.format = SDL_AUDIO_F32; - spec.channels = OUTPUT_CHANNELS; + /* (the game's mix bins are set for the channels the sound started with; + SDL turns them into the new device's) */ + spec.channels = (int)output_channels; spec.freq = OUTPUT_RATE; SDL_DestroyAudioStream(audio_stream); stream = SDL_OpenAudioDeviceStream(platform_audio_device(FALSE, audio_device_name), &spec, audio_callback, NULL); @@ -1751,7 +1996,7 @@ HRESULT WINAPI IDirectSound_GetCaps(LPDIRECTSOUND sound, LPDSCAPS caps) HRESULT WINAPI IDirectSound_GetSpeakerConfig(LPDIRECTSOUND sound, LPDWORD speaker_config) { (void)sound; - *speaker_config = DSSPEAKER_STEREO; + *speaker_config = output_channels == 6 ? DSSPEAKER_ENABLE_AC3 : DSSPEAKER_STEREO; return DS_OK; } @@ -1867,6 +2112,8 @@ HRESULT WINAPI IDirectSound_CreateSoundStream(LPDIRECTSOUND sound, LPCDSSTREAMDE stereo voice's channels to the front left and right */ stream->mix_left = 1.0f; stream->mix_right = 1.0f; + stream->surround_bins[0][0] = 1.0f; + stream->surround_bins[stream->channels - 1][1] = 1.0f; stream->has_3d = (description->dwFlags & DSSTREAMCAPS_CTRL3D) != 0; stream->mode = DS3DMODE_NORMAL; stream->minimum_distance = DS3D_DEFAULTMINDISTANCE; @@ -1916,11 +2163,36 @@ HRESULT WINAPI IDirectSoundStream_SetVolume(LPDIRECTSOUNDSTREAM stream, LONG vol STREAM_SETTER(record->volume = gain_from_millibels(volume)) } +/* the speaker bins of a mask, for 5.1, in the order of its set bits: a stereo +voice sends its left channel to the first, third, ... and its right to the +second, fourth, ... (the game's stereo voices on AC-3 take front left, front +right, back left, back right). The crosstalk, reverb and effect sends are not +speakers. volumes is NULL for 0 dB. */ +static void stream_set_surround_bins(struct sdl_stream *record, DWORD mix_bin_mask, const LONG *volumes, BOOL clear) +{ + unsigned long bit, index = 0; + + if (clear) + memset(record->surround_bins, 0, sizeof(record->surround_bins)); + for (bit = 0; bit < 32; bit++) + { + if (!(mix_bin_mask & (1UL << bit))) + continue; + if ((1UL << bit) & DSMIXBIN_SPEAKER_MASK) + { + record->surround_bins[record->channels == 2 ? index & 1 : 0][bit] = + volumes ? gain_from_millibels(volumes[index]) : 1.0f; + } + index++; + } +} + HRESULT WINAPI IDirectSoundStream_SetMixBins(LPDIRECTSOUNDSTREAM stream, DWORD mix_bin_mask) { STREAM_SETTER( record->mix_left = (mix_bin_mask & DSMIXBIN_FRONT_LEFT) ? 1.0f : 0.0f; - record->mix_right = (mix_bin_mask & DSMIXBIN_FRONT_RIGHT) ? 1.0f : 0.0f) + record->mix_right = (mix_bin_mask & DSMIXBIN_FRONT_RIGHT) ? 1.0f : 0.0f; + stream_set_surround_bins(record, mix_bin_mask, NULL, TRUE)) } /* volumes come in the order of the set bits of the mask */ @@ -1940,6 +2212,7 @@ HRESULT WINAPI IDirectSoundStream_SetMixBinVolumes(LPDIRECTSOUNDSTREAM stream, D record->mix_right = gain_from_millibels(volumes[index]); index++; } + stream_set_surround_bins(record, mix_bin_mask, volumes, FALSE); pthread_mutex_unlock(&mixer_lock); return DS_OK; } diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c index dfd924d7bb..8fc87ee0b9 100644 --- a/port/linux/src/port_config.c +++ b/port/linux/src/port_config.c @@ -189,6 +189,9 @@ 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.channels", _config_string, "\"auto\"", "HALO_AUDIO_CHANNELS", _environment_value, _platform_desktop, + "\"stereo\", \"5.1\" (surround, as the Xbox's Dolby Digital), or \"auto\"\n" + "for 5.1 when the sound device has six or more channels." }, { "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"