diff --git a/port/android/app/src/main/AndroidManifest.xml b/port/android/app/src/main/AndroidManifest.xml
index 08d788e9..cccdd89c 100644
--- a/port/android/app/src/main/AndroidManifest.xml
+++ b/port/android/app/src/main/AndroidManifest.xml
@@ -15,6 +15,8 @@
+
+
diff --git a/port/android/guest/runtime/guest_host.h b/port/android/guest/runtime/guest_host.h
index e77403c2..ca863654 100644
--- a/port/android/guest/runtime/guest_host.h
+++ b/port/android/guest/runtime/guest_host.h
@@ -85,6 +85,10 @@ called on the audio thread */
unsigned int host_sdl_open_audio_stream(unsigned int device, const void *spec, unsigned int callback, unsigned int userdata);
int host_sdl_put_audio_stream_data(unsigned int stream, const void *data, int length);
int host_sdl_resume_audio_stream_device(unsigned int stream);
+/* (voice chat's microphone, a stream without a callback) */
+int host_sdl_get_audio_stream_data(unsigned int stream, void *data, int length);
+int host_sdl_get_audio_stream_available(unsigned int stream);
+void host_sdl_destroy_audio_stream(unsigned int stream);
/* ---------- OpenGL ES */
diff --git a/port/android/guest/runtime/guest_sdl.c b/port/android/guest/runtime/guest_sdl.c
index 7bd89689..21197ba1 100644
--- a/port/android/guest/runtime/guest_sdl.c
+++ b/port/android/guest/runtime/guest_sdl.c
@@ -234,3 +234,18 @@ bool SDL_ResumeAudioStreamDevice(SDL_AudioStream *stream)
{
return host_sdl_resume_audio_stream_device((unsigned int)stream) != 0;
}
+
+int SDL_GetAudioStreamData(SDL_AudioStream *stream, void *data, int length)
+{
+ return host_sdl_get_audio_stream_data((unsigned int)stream, data, length);
+}
+
+int SDL_GetAudioStreamAvailable(SDL_AudioStream *stream)
+{
+ return host_sdl_get_audio_stream_available((unsigned int)stream);
+}
+
+void SDL_DestroyAudioStream(SDL_AudioStream *stream)
+{
+ host_sdl_destroy_audio_stream((unsigned int)stream);
+}
diff --git a/port/android/host/host_sdl.c b/port/android/host/host_sdl.c
index 0167e453..e994539d 100644
--- a/port/android/host/host_sdl.c
+++ b/port/android/host/host_sdl.c
@@ -339,9 +339,18 @@ static int audio_keep(struct audio_binding *binding, const void *data, int lengt
uint32_t host_sdl_open_audio_stream(uint32_t device, const void *spec, uint32_t callback, uint32_t userdata)
{
- struct audio_binding *binding = SDL_calloc(1, sizeof(*binding));
+ struct audio_binding *binding;
SDL_AudioStream *stream;
+ /* (one without a callback, the microphone's, which the guest reads:
+ nothing to bind) */
+ if (!callback)
+ {
+ stream = SDL_OpenAudioDeviceStream((SDL_AudioDeviceID)device, spec, NULL, NULL);
+ return stream ? handle_new(_handle_audio, stream) : 0;
+ }
+ binding = SDL_calloc(1, sizeof(*binding));
+
binding->callback = callback;
binding->userdata = userdata;
pthread_mutex_init(&binding->lock, NULL);
@@ -379,6 +388,36 @@ int host_sdl_resume_audio_stream_device(uint32_t stream)
return object ? SDL_ResumeAudioStreamDevice(object) : 0;
}
+/* (voice chat's microphone: port/linux/src/voice_audio.c) */
+int host_sdl_get_audio_stream_data(uint32_t stream, void *data, int length)
+{
+ SDL_AudioStream *object = handle_get(stream, _handle_audio);
+
+ return object ? SDL_GetAudioStreamData(object, data, length) : -1;
+}
+
+int host_sdl_get_audio_stream_available(uint32_t stream)
+{
+ SDL_AudioStream *object = handle_get(stream, _handle_audio);
+
+ return object ? SDL_GetAudioStreamAvailable(object) : -1;
+}
+
+/* (a stream opened without a callback only: one with a callback has a
+thread the guest's callbacks run on) */
+void host_sdl_destroy_audio_stream(uint32_t stream)
+{
+ SDL_AudioStream *object = handle_get(stream, _handle_audio);
+
+ if (!object)
+ return;
+ pthread_mutex_lock(&handle_lock);
+ handles[stream].type = _handle_free;
+ handles[stream].object = NULL;
+ pthread_mutex_unlock(&handle_lock);
+ SDL_DestroyAudioStream(object);
+}
+
/* ---------- the clipboard (internet play's invite links) */
int host_sdl_set_clipboard_text(const char *text)
diff --git a/port/android/host_imports.list b/port/android/host_imports.list
index e7a81fb1..7f2d97c6 100644
--- a/port/android/host_imports.list
+++ b/port/android/host_imports.list
@@ -43,6 +43,9 @@ host_sdl_scancode_from_name
host_sdl_open_audio_stream
host_sdl_put_audio_stream_data
host_sdl_resume_audio_stream_device
+host_sdl_get_audio_stream_data
+host_sdl_get_audio_stream_available
+host_sdl_destroy_audio_stream
host_gl_get_string
host_gl_has_extension
host_gl_read_buffer
diff --git a/port/assets/menus/UNWIRED.md b/port/assets/menus/UNWIRED.md
index 3b20f26a..a57a6a1e 100644
--- a/port/assets/menus/UNWIRED.md
+++ b/port/assets/menus/UNWIRED.md
@@ -21,7 +21,7 @@ do (save a setting, join a game) does not happen; the screens open and close as
| `gamespy update filter settings` | 2 | PC function |
| `gt edit list update` | 1 | PC function |
| `gt select list update` | 1 | PC function |
-| `mouse spinner 1wide click` | 72 | PC function |
+| `mouse spinner 1wide click` | 76 | PC function |
| `mp map list update` | 1 | PC function |
| `mp prof init teamplay options` | 1 | PC function |
| `mp prof init vehicle options` | 1 | PC function |
diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml
new file mode 100644
index 00000000..389e9277
--- /dev/null
+++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml
index 898b5c7e..08713f89 100644
--- a/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml
+++ b/port/assets/menus/ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml
@@ -48,6 +48,10 @@
+
+
+
+
diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.audio_settings.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.audio_settings.xml
index 55a92dd3..3ad3251d 100644
--- a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.audio_settings.xml
+++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.audio_settings.xml
@@ -13,11 +13,22 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -25,11 +36,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -38,7 +67,7 @@
-
+
@@ -47,7 +76,7 @@
-
+
@@ -56,7 +85,7 @@
-
+
@@ -65,11 +94,29 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -81,18 +128,26 @@
+
+
+
+
+
+
+
+
diff --git a/port/assets/menus/ce/strings.xml b/port/assets/menus/ce/strings.xml
index f267aa32..23c77ae9 100644
--- a/port/assets/menus/ce/strings.xml
+++ b/port/assets/menus/ce/strings.xml
@@ -1023,11 +1023,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1111,7 +1135,6 @@
-
diff --git a/port/assets/menus/menus.json b/port/assets/menus/menus.json
index 6986c10f..1886ef25 100644
--- a/port/assets/menus/menus.json
+++ b/port/assets/menus/menus.json
@@ -35,6 +35,7 @@
"ce/main_menu.settings_select.multiplayer_setup.playlist_edit.slayer_edit.xml",
"ce/main_menu.settings_select.multiplayer_setup.playlist_edit.xml",
"ce/main_menu.settings_select.multiplayer_setup.playlist_select.xml",
+ "ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.port.xml",
"ce/main_menu.settings_select.multiplayer_setup.teamplay_options_edit.xml",
"ce/main_menu.settings_select.multiplayer_setup.vehicle_options_edit.xml",
"ce/main_menu.settings_select.multiplayer_setup.xml",
diff --git a/port/linux/NETCODE.md b/port/linux/NETCODE.md
index ea3ef686..6f981c0c 100644
--- a/port/linux/NETCODE.md
+++ b/port/linux/NETCODE.md
@@ -249,6 +249,27 @@ for `network.votekick_ban_minutes`. The host sends each client
as it changes), for the scoreboard, whose right click frees the mouse to
pick a player; the host's own scoreboard adds Kick and Ban. Builds without
votes drop both kinds as unknown, so the network version is unchanged.
+
+Voice chat (`network_voice.c`, the sound in `port/linux/src/voice_audio.c`)
+travels as distributed messages too, but in the lobby as well as in a game:
+`network_voice_handles_message` takes them before the netcode's checks that
+a game is running (`network_distributed_handle_message`, and the host's
+stream and datagram paths in `network_server_message_handler.c`). A client
+sends its 20 ms Opus frames to the host unreliably
+(`_distributed_message_voice_up`), each with the key the host gave it over
+its stream (`_distributed_message_voice_config`, with the host's settings,
+again every ten seconds and when they change), so a datagram sent as from
+another machine is dropped; an empty one every two seconds says it hears
+voices. The host checks the key, the frame's size against its quality, a
+rate of 50 frames a second (a burst of 10), and at most 8 machines talking,
+then sends the frame to each machine that may hear it
+(`_distributed_message_voice_down`, with that machine's key) by its
+`network.voice_mode`, `network.voice_lobby` and `network.voice_proximity`,
+or plays it itself. The host never decodes a frame it only relays, and
+never sends a machine its own. A listener sets the gain and pan of a voice
+heard for being near by its own view of the speaker's unit. Mutes are each
+machine's own. Builds without voice chat never say they hear voices, so the
+host sends them none, and drop the kinds as unknown.
A speed hack of less than a tenth is let be: the host's bounds on how far
and how fast a client's player moves and fires hold it to the host's time
anyway.
diff --git a/port/linux/README.md b/port/linux/README.md
index ae7ddf3d..a25afca9 100644
--- a/port/linux/README.md
+++ b/port/linux/README.md
@@ -121,6 +121,7 @@ gamepads' only.
| zoom | Z, middle mouse button |
| show the scores (hold) | tab |
| pause menu | escape |
+| talk in voice chat (hold) | V |
Always: \` opens the developer console, F12 releases or captures the mouse,
F11 changes between fullscreen and window.
@@ -248,12 +249,15 @@ the setting for one start of the game. It has priority over the file.
| `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). |
+| `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". |
+| `audio.voice_volume` | `1.0` | `HALO_VOICE_VOLUME` | The volume of the voices of the other players, `0` to `2`. |
+| `audio.output_device`, `audio.input_device` | `"default"` | `HALO_AUDIO_OUTPUT_DEVICE`, `HALO_AUDIO_INPUT_DEVICE` | The speakers and the microphone, by the name that Settings > Audio shows, or `"default"` for the device of the system. If the device is not found, the game uses the device of the system. Not on Android. |
| `audio.loose_sounds` | `false` | `HALO_LOOSE_SOUNDS` | For those who make sounds. `true`: each sound of a map that has a sound tag file of its name in `tags/` in the data root (for example `tags/sound/sfx/weapons/assault rifle/fire.sound`) plays from that file. The files are Halo PC tag files, as the Halo Editing Kit and Invader write them. At the console, `loose_sounds_reload` reads the files again, and `loose_sounds false` plays the sounds of the map again. When a file changes, all sounds stop. |
| `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. |
| `input.mouse_aim_assist` | `false` | `HALO_MOUSE_AIM_ASSIST` | `true`: the magnetism of the controller also operates for the mouse. `false`: when the mouse moved after the right stick, the view is not slowed or dragged by a target. The autoaim of the bullets operates in both cases. |
-| `controls.` | (the table in "Controls") | `HALO_KEY_` | The keys and mouse buttons of an action, up to two, separated by a comma: `move_forward`, `move_backward`, `strafe_left`, `strafe_right`, `jump`, `crouch`, `fire`, `throw_grenade`, `melee`, `reload`, `zoom`, `switch_weapon`, `switch_grenade`, `action`, `flashlight`, `scoreboard`, `pause`. Keys by their names (`"W"`, `"Space"`, `"Left Ctrl"`, `"F1"`), and `"Mouse Left"`, `"Mouse Right"`, `"Mouse Middle"`, `"Mouse 4"`, `"Mouse 5"`, `"Wheel"` (either way), `"Wheel Up"`, `"Wheel Down"`. |
+| `controls.` | (the table in "Controls") | `HALO_KEY_` | The keys and mouse buttons of an action, up to two, separated by a comma: `move_forward`, `move_backward`, `strafe_left`, `strafe_right`, `jump`, `crouch`, `fire`, `throw_grenade`, `melee`, `reload`, `zoom`, `switch_weapon`, `switch_grenade`, `action`, `flashlight`, `scoreboard`, `pause`, `push_to_talk`. Keys by their names (`"W"`, `"Space"`, `"Left Ctrl"`, `"F1"`), and `"Mouse Left"`, `"Mouse Right"`, `"Mouse Middle"`, `"Mouse 4"`, `"Mouse 5"`, `"Wheel"` (either way), `"Wheel Up"`, `"Wheel Down"`. |
| `game.console_log` | `"important"` | `HALO_CONSOLE_LOG` | What the console shows on the screen. `"important"`: bans, players that the host drops for cheating, the reasons that the game refuses a command, and the asserts that stop the game. `"all"`: all the lines. `"none"`: only the asserts that stop the game. The output of a command always shows. `debug.txt` gets all the lines. |
| `game.language` | `""` | `HALO_LANGUAGE` | The language of the menus: `ja`, `de`, `fr`, `es` or `it`. Empty: English. |
| `paths.data` | `""` | `HALO_DATA_ROOT` | The data root. Refer to "Start the game". |
@@ -271,6 +275,10 @@ the setting for one start of the game. It has priority over the file.
| `network.coop_enemies_mode` | `"per_player"` | `HALO_NET_COOP_ENEMIES_MODE` | Online co-op's extra enemies: `"none"`; `"per_player"`, each squad of enemies that a level places grows by `network.coop_enemies` for each player past the first; or `"multiplier"`, each squad is `network.coop_enemies_multiplier` times as large, for any number of players. The extra enemies stand around the squad's places, and those that a dropship has no seats for drop out of it after its passengers. EXTRA ENEMIES in co-op's Server Setup writes its choice here. |
| `network.coop_enemies` | `50` | `HALO_NET_COOP_ENEMIES` | The extra enemies per player, a percentage from `25` to `200`: for each player past the first, each squad of enemies gets this much of itself more (`100`: as many again, so four players meet four times the squad), up to 8 times the squad however many players there are. PER PLAYER in co-op's Server Setup writes its choice here. |
| `network.coop_enemies_multiplier` | `2` | `HALO_NET_COOP_ENEMIES_MULTIPLIER` | The static multiplier of the enemies, `2` to `32`: each squad of enemies is this many times as large. MULTIPLIER in co-op's Server Setup writes its choice here. |
+| `network.voice_lobby` | `true` | `HALO_NET_VOICE_LOBBY` | When you host: `true`, all players hear all players in the lobby, before and after a game. |
+| `network.voice_mode` | `"team_global_enemy_proximity"` | `HALO_NET_VOICE_MODE` | When you host: who hears whom during a game. `"off"`; `"team_proximity"` (teammates who are near); `"team_enemy_proximity"` (all players who are near); `"team_global"` (all teammates); `"team_global_enemy_proximity"` (all teammates, and enemies who are near). Refer to "Voice chat". |
+| `network.voice_kbps` | `24` | `HALO_NET_VOICE_KBPS` | When you host: the voice quality in the lobby and in a game, in kilobits per second, `8` to `64`. |
+| `network.voice_proximity` | `15.0` | `HALO_NET_VOICE_PROXIMITY` | When you host: the distance in world units (1 unit is approximately 3 metres) at which players are near, for voice chat. `5` to `100`. |
| `network.votekick` | `true` | `HALO_NET_VOTEKICK` | When you host: `true`, the players can vote to kick a player. Refer to "Security". `false`: no votes. |
| `network.votekick_minutes` | `5` | `HALO_NET_VOTEKICK_MINUTES` | When you host: the minutes that a player must play on the server before the player can start a vote to kick (`0` to `60`). To vote, a player must play for 2 minutes, or for this time if it is less. |
| `network.votekick_ban_minutes` | `30` | `HALO_NET_VOTEKICK_BAN_MINUTES` | When you host: the minutes that a player who is kicked by a vote cannot join again (`1` to `1440`). |
@@ -289,6 +297,7 @@ the setting for one start of the game. It has priority over the file.
| `debug.gpu_skip_vertex_shaders`, `debug.gpu_debug_expression`, `debug.gpu_debug_flat`, `debug.gpu_debug_texture0` | off | `HALO_GPU_SKIP_VS`, `HALO_GPU_DEBUG_EXPR`, `HALO_GPU_DEBUG_FLAT`, `HALO_GPU_DEBUG_T0` | Tools to find problems in the graphics: skip the draws of a vertex shader, or replace the output of all pixel shaders with a GLSL expression (for example `t0.rgb`). |
| `debug.network_test`, `debug.network_test_start`, `debug.network_test_kill`, `debug.network_test_score`, `debug.network_test_shoot`, `debug.network_test_vehicle`, `debug.network_test_pickup`, `debug.network_test_pickup_weapon`, `debug.test_input` | off | `HALO_NETWORK_TEST`, `HALO_NETWORK_TEST_START`, `HALO_NETWORK_TEST_KILL`, `HALO_NETWORK_TEST_SCORE`, `HALO_NETWORK_TEST_SHOOT`, `HALO_NETWORK_TEST_VEHICLE`, `HALO_NETWORK_TEST_PICKUP`, `HALO_NETWORK_TEST_PICKUP_WEAPON`, `HALO_TEST_INPUT` | Automatic tests of system link (`game/network_test.c`). Refer to `NETCODE.md`. |
| `debug.network_latency`, `debug.network_loss`, `debug.network_corrupt`, `debug.network_corrupt_stream`, `debug.network_corrupt_after` | `0` | `HALO_NETWORK_LATENCY`, `HALO_NETWORK_LOSS`, `HALO_NETWORK_CORRUPT`, `HALO_NETWORK_CORRUPT_STREAM`, `HALO_NETWORK_CORRUPT_AFTER` | The game holds all the data that it receives for this number of milliseconds, ignores this percentage of the datagrams, and damages this percentage of the datagrams it receives, and this percentage of its reads of streams, at random (bytes changed, cut short, stretched or replaced), from this many seconds after the start. Use the first two to test the netcode as on the internet, and the others to test that nothing another machine sends can crash the game (a damaged stream is closed, so a little goes a long way; a host's messages to its own client are damaged too, so start damaging once the game has started). |
+| `debug.voice_test` | `false` | `HALO_VOICE_TEST` | Automatic tests of voice chat: a tone replaces the microphone, and each voice that the game hears is written to the log once each second. |
| `debug.telnet_console`, `debug.telnet_console_port` | `false`, `2323` | `HALO_TELNET_CONSOLE`, `HALO_TELNET_CONSOLE_PORT` | The game listens on 127.0.0.1, on this port, for a script console (connect with telnet). The console has no password, so only this computer can reach it. |
With Mesa drivers, the game sends its GL calls through the GL thread of
@@ -459,6 +468,45 @@ When the machines connect, the game of the host shows in Multiplayer,
System Link. Join the game as on a local network. System link on a local
network does not need an invite.
+### Voice chat
+
+Network games have voice chat, on the local network and on the internet.
+Hold V (`controls.push_to_talk`, Settings > Controls Setup) to talk, or set
+VOICE CHAT in Settings > Audio to OPEN MIC. VOICE VOLUME sets the volume of
+the other players. On Android, set VOICE CHAT to OPEN MIC to talk: the game
+then asks for the microphone.
+
+OUTPUT DEVICE and INPUT DEVICE in Settings > Audio select the speakers and
+the microphone (SYSTEM DEFAULT, the default, follows the system). The list
+has the devices that the computer had when the game started.
+
+The host sets voice chat for all players in Server Setup > Teamplay Options
+(`network.voice_*` in `config.toml`):
+
+- In the lobby, before and after a game, all players hear all players
+ (LOBBY VOICE CHAT).
+- During a game (VOICE CHAT): OFF; TEAM NEAR; ANYONE NEAR; TEAM; or TEAM,
+ ENEMIES NEAR (the default). Near is VOICE NEAR DISTANCE (45 metres is the
+ default), and both players must be alive. A player who is near sounds
+ quieter when further, and from their direction. In a game without teams
+ all players are enemies; in co-op, all are teammates.
+- The quality (VOICE QUALITY), 8 to 64 kilobits per second (24 is the
+ default), in the lobby and in the game.
+
+In co-op, Server Setup has no Teamplay Options: the host sets voice chat in
+`config.toml`.
+
+A speaker shows next to the name of a player who talks: in the lobby, on
+the scoreboard, and in a list at the left of the screen during a game. To
+mute a player, open the scoreboard, right-click, click the player, and
+click Mute voice. You no longer hear them, and nobody else is told.
+
+The host sends each voice on to the players who can hear it. The host
+accepts a voice only from the machine of the player (with a key that it
+gives each machine on its connection), only at the quality it set, at most
+50 frames each second from a machine, and from at most 8 players at a time.
+The voices are compressed with Opus (`port/third_party/opus`).
+
### Server browser
Server Setup in Create Game > Internet has a LISTING row:
diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c
index 180a42e0..b9c4b75e 100644
--- a/port/linux/game/menu_functions.c
+++ b/port/linux/game/menu_functions.c
@@ -78,6 +78,8 @@ their handlers open opens.
#include "halo_menus.h"
#include "custom_edition_cache.h"
#include "custom_edition_maps.h"
+#include "network_voice.h"
+#include "text/draw_string.h"
/* (internet play's server browser: the platform layer's) */
#include "../src/p2p.h"
@@ -101,6 +103,8 @@ void platform_binding_capture_begin(void);
int platform_binding_capture_poll(int *input);
void halo_input_name(int input, char *name, unsigned int size);
short pc_menu_string_index(long definition_index);
+/* cseries_windows.c's */
+unsigned long system_milliseconds(void);
/* the game's (port) */
boolean ui_widget_port_dispatch_event(struct widget_instance *widget, short event_type, short controller_index,
@@ -1875,6 +1879,7 @@ static struct
{ "controls.flashlight", L"FLASHLIGHT", 2 },
{ "controls.scoreboard", L"SHOW SCORES", 2 },
{ "controls.pause", L"PAUSE MENU", 2 },
+ { "controls.push_to_talk", L"PUSH TO TALK", 2 },
};
static struct
@@ -4222,6 +4227,58 @@ static void lobby_map_show(struct widget_instance *description, char const *map_
widget->parameters.text_box.string_list_index = level;
}
+/* the lobby's rows' texts as last updated, and their players' machines,
+for their speaker icons (menu_functions_text_box_drawn); and when */
+static struct
+{
+ struct widget_instance *texts[LOBBY_ROWS];
+ long machines[LOBBY_ROWS];
+ short count;
+ unsigned long updated_at;
+} lobby_icons;
+
+/* ui_widget.c's: a text box drawn, at the bounds its text was drawn in
+(its font and justification still set): a lobby row's player talking in
+voice chat (or muted) has a speaker just left of their name
+(port/linux/game/network_voice.c) */
+void menu_functions_text_box_drawn(struct widget_instance *widget, rectangle2d const *bounds)
+{
+ short row;
+
+ /* (as the lobby last updated them: the rows' widgets go with it) */
+ if (!lobby_icons.count || system_milliseconds() - lobby_icons.updated_at > 250)
+ return;
+ for (row = 0; row < lobby_icons.count; row++)
+ {
+ long machine_index = lobby_icons.machines[row];
+ rectangle2d icon;
+ short size;
+
+ if (lobby_icons.texts[row] != widget)
+ continue;
+ if (!network_voice_machine_speaking(machine_index) && !network_voice_machine_muted(machine_index))
+ return;
+ size = (short)MIN(14, bounds->y1 - bounds->y0);
+ icon.x1 = (short)(bounds->x1 - 4);
+ icon.y0 = (short)((bounds->y0 + bounds->y1 - size) / 2);
+ /* (the name's ink: the icon just left of it) */
+ if (widget->parameters.text_box.text && widget->parameters.text_box.text[0])
+ {
+ rectangle2d text;
+ rectangle2d cursor;
+
+ draw_unicode_string_compute_bounds(bounds, widget->parameters.text_box.text, &text, &cursor);
+ if (text.x0 - size - 6 >= bounds->x0)
+ icon.x1 = (short)(text.x0 - 6);
+ icon.y0 = (short)((text.y0 + text.y1 - size) / 2);
+ }
+ icon.x0 = (short)(icon.x1 - size);
+ icon.y1 = (short)(icon.y0 + size);
+ network_voice_draw_icon(&icon, network_voice_machine_muted(machine_index), 1.0f);
+ return;
+ }
+}
+
/* "port lobby update" */
static void lobby_update(struct widget_instance *list)
{
@@ -4242,6 +4299,21 @@ static void lobby_update(struct widget_instance *list)
if (multiplayer.lobby_first > MAX(0, lobby_player_count - LOBBY_ROWS))
multiplayer.lobby_first = (short)MAX(0, lobby_player_count - LOBBY_ROWS);
rows_update(list, (short)MIN(lobby_player_count, LOBBY_ROWS), lobby_row_text);
+ /* (the rows' players, for their speaker icons) */
+ {
+ struct widget_instance *row = list->child;
+
+ lobby_icons.count = 0;
+ for (index = 0; row && index < MIN(lobby_player_count, LOBBY_ROWS); row = row->next, index++)
+ {
+ if (strncmp(row->name, "list_item_", 10))
+ break;
+ lobby_icons.texts[index] = named(row, "list_item_text", 0);
+ lobby_icons.machines[index] = lobby_players[multiplayer.lobby_first + index]->machine_index;
+ lobby_icons.count = (short)(index + 1);
+ }
+ lobby_icons.updated_at = system_milliseconds();
+ }
lobby_join_help(list);
visible_set(named(list, "lobby_button_team", 0), game && game->variant.universal_variant.teams);
/* (the buttons' focus, off Switch Team when it is hidden) */
@@ -4476,7 +4548,8 @@ enum
_option_health, /* the variant's health, tenths */
_option_short, /* a short of the options */
_option_option_byte, /* a byte of the options */
- _option_radar /* the options' radar players, and the variant's flag */
+ _option_radar, /* the options' radar players, and the variant's flag */
+ _option_setting /* the host's own (config.toml): Server Setup's copy only */
};
struct gametype_option
@@ -4487,6 +4560,9 @@ struct gametype_option
unsigned long argument;
short count;
long values[16];
+ /* (_option_setting: the setting, and its values' text) */
+ char const *setting;
+ char const *setting_values[16];
};
#define VARIANT_FIELD(field) (short)offsetof(struct game_variant, field)
@@ -4578,11 +4654,38 @@ static struct gametype_option const gametype_options[] =
{ _friendly_fire_off, _friendly_fire_on, _friendly_fire_shields_only, _friendly_fire_explosives_only } },
{ "friendly_fire_penalty_spinner", _option_short, OPTIONS_FIELD(friendly_fire_penalty), 0, 4, { 0, 5, 10, 15 } },
{ "autobalance_spinner", _option_option_byte, OPTIONS_FIELD(auto_team_balance), 0, 2, { 0, 1 } },
+ /* (the host's voice chat, below them: port/linux/game/network_voice.c;
+ tools/port_settings.py's VOICE_ROWS) */
+ { "voice_mode_spinner", _option_setting, 0, 0, 5, { 0 }, "network.voice_mode",
+ { "off", "team_proximity", "team_enemy_proximity", "team_global", "team_global_enemy_proximity" } },
+ { "voice_lobby_spinner", _option_setting, 0, 0, 2, { 0 }, "network.voice_lobby", { "true", "false" } },
+ { "voice_kbps_spinner", _option_setting, 0, 0, 7, { 0 }, "network.voice_kbps",
+ { "8", "12", "16", "24", "32", "48", "64" } },
+ { "voice_proximity_spinner", _option_setting, 0, 0, 6, { 0 }, "network.voice_proximity",
+ { "5", "10", "15", "20", "30", "50" } },
/* vehicle options (the side's set and counts: vehicles_update) */
{ "vehicles_respawn_spinner", _option_short, OPTIONS_FIELD(vehicle_respawn_time), 0, 7,
{ 0, 30, 60, 90, 120, 180, 300 } },
};
+/* an option of the host's own setting: the value shown for it (the one
+set, else the nearest) */
+static short gametype_option_setting_index(struct gametype_option const *option)
+{
+ struct pc_menu_setting setting;
+ char text[64];
+ short index;
+
+ csmemset(&setting, 0, sizeof(setting));
+ setting.setting = option->setting;
+ setting.value_count = option->count;
+ for (index = 0; index < option->count; index++)
+ setting.values[index] = option->setting_values[index];
+ if (!setting_text(option->setting, text, sizeof(text), FALSE))
+ return 0;
+ return setting_value_index(&setting, text);
+}
+
/* (an instance's name is its definition's cut to 31 characters:
"item_options_infinite_grenades_") */
#define WIDGET_NAME_LENGTH 31
@@ -4688,6 +4791,25 @@ static void gametype_options_each(struct widget_instance *list, boolean save)
if (!option)
continue;
+ /* (the host's own settings: Server Setup's alone, their rows
+ hidden in Edit Gametypes', which edits a gametype for any game;
+ written on OK, as the gametype's are kept) */
+ if (option->kind == _option_setting)
+ {
+ short index = (short)PIN(spinner->parameters.list.selected_index, 0, option->count - 1);
+
+ visible_set(row, gametype_edit.setup);
+ if (!gametype_edit.setup)
+ continue;
+ if (!save)
+ spinner->parameters.list.selected_index = gametype_option_setting_index(option);
+ else if (index != gametype_option_setting_index(option) &&
+ !setting_write(option->setting, option->setting_values[index]))
+ {
+ platform_log("menus: could not set %s", option->setting);
+ }
+ continue;
+ }
if (save)
{
short index = (short)PIN(spinner->parameters.list.selected_index, 0, option->count - 1);
diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c
index d4548bd5..0565835e 100644
--- a/port/linux/game/menu_tags.c
+++ b/port/linux/game/menu_tags.c
@@ -46,6 +46,8 @@ void platform_log(char const *format, ...);
char const *config_string(char const *name);
int platform_display_resolutions(long *widths, long *heights, int maximum);
int platform_window_sizes(long *widths, long *heights, int maximum);
+/* (sdl_platform.c's PLATFORM_AUDIO_DEVICE_NAME_SIZE) */
+int platform_audio_devices(int recording, char (*names)[128], int maximum);
/* the game's network server (this machine hosts: network_game_globals.c) */
void *global_network_game_server_get(void);
@@ -734,13 +736,50 @@ static long split(char const *text, char const **pieces)
/* a spinner's strings, or its values, split: its own, but Video Setup's
sizes are the display's (sdl_platform.c), shown "1920 x 1080" and set
"1920x1080": its resolutions after Resolution's own (NATIVE), and the
-window sizes that fit it in place of Window Size's own */
+window sizes that fit it in place of Window Size's own; and Audio Setup's
+devices SDL finds, after their SYSTEM DEFAULT, each set by its name and
+shown by it (in capitals, as the menus' words are, and cut short) */
static long spinner_split(struct halo_menu_widget const *widget, boolean values, char const **pieces)
{
long count = split(values ? widget->values : widget->strings, pieces);
long widths[MAXIMUM_STRINGS], heights[MAXIMUM_STRINGS];
long added, index;
+ if (widget->setting && (!strcmp(widget->setting, "audio.output_device") ||
+ !strcmp(widget->setting, "audio.input_device")))
+ {
+ static char names[MAXIMUM_STRINGS][128];
+
+ added = platform_audio_devices(!strcmp(widget->setting, "audio.input_device"), names,
+ (int)(MAXIMUM_STRINGS - count));
+ for (index = 0; index < added; index++)
+ {
+ char shown[32];
+ char const *name = names[index];
+ char *piece;
+ long length;
+
+ if (!values)
+ {
+ long at;
+
+ for (at = 0; name[at] && at < (long)sizeof(shown) - 1; at++)
+ shown[at] = name[at] >= 'a' && name[at] <= 'z' ? (char)(name[at] - 'a' + 'A') : name[at];
+ shown[at] = 0;
+ /* (cut short: what the spinner shows) */
+ if (strlen(name) > 18)
+ strcpy(shown + 15, "...");
+ name = shown;
+ }
+ length = (long)strlen(name);
+ piece = allocate(length + 1);
+ if (!piece)
+ break;
+ strcpy(piece, name);
+ pieces[count++] = piece;
+ }
+ return count;
+ }
if (widget->setting && !strcmp(widget->setting, "display.resolution"))
added = platform_display_resolutions(widths, heights, (int)(MAXIMUM_STRINGS - count));
else if (widget->setting && !strcmp(widget->setting, "display.window_size"))
diff --git a/port/linux/game/network_distributed.c b/port/linux/game/network_distributed.c
index 983aed36..ce60180b 100644
--- a/port/linux/game/network_distributed.c
+++ b/port/linux/game/network_distributed.c
@@ -72,6 +72,7 @@ machine (their datum identifiers need not be).
#include "units/bipeds.h"
#include "network_coop.h"
#include "network_distributed.h"
+#include "network_voice.h"
#include
#include
@@ -3904,6 +3905,12 @@ void network_distributed_handle_message(
short index;
word entry_size;
+ /* (voice chat's, in the lobby too: network_voice.c) */
+ if (network_voice_handles_message(message, size))
+ {
+ network_voice_handle_message(machine_index, message, size);
+ return;
+ }
/* (none between games: loading, or in the menus) */
if (size < sizeof(header) || !game_in_progress())
return;
diff --git a/port/linux/game/network_distributed.h b/port/linux/game/network_distributed.h
index be3aef10..0ba55191 100644
--- a/port/linux/game/network_distributed.h
+++ b/port/linux/game/network_distributed.h
@@ -112,6 +112,12 @@ enum
/* the vote running as the host counts it, to each client, every second
and when it changes (unreliable: network_votekick.c) */
_distributed_message_votekick_status = 79,
+ /* voice chat (network_voice.c), in the lobby too: a client's frame to
+ the host, unreliably, with its key; a frame the host relays to a
+ client; the host's settings and the client's key, reliably */
+ _distributed_message_voice_up = 80,
+ _distributed_message_voice_down = 81,
+ _distributed_message_voice_config = 82,
NUMBER_OF_DISTRIBUTED_MESSAGES
};
diff --git a/port/linux/game/network_voice.c b/port/linux/game/network_voice.c
new file mode 100644
index 00000000..d3d8f11b
--- /dev/null
+++ b/port/linux/game/network_voice.c
@@ -0,0 +1,1047 @@
+/*
+NETWORK_VOICE.C
+
+Voice chat (port/linux/NETCODE.md): each machine's microphone, encoded with
+Opus (port/linux/src/voice_audio.c), goes to the host, which sends it on to
+the machines that may hear it. The host decides everything, from its own
+config.toml:
+
+- In the lobby, before and after a game (and in a game's postgame), every
+ player hears every other (network.voice_lobby).
+- In a game, network.voice_mode: off; teammates near; anyone near; all
+ teammates; or all teammates and enemies near ("near": within
+ network.voice_proximity world units of each other, both alive). A game
+ without teams has only enemies, and co-op only teammates. A voice heard
+ for being near is quieter further away, and comes from the speaker's side
+ (each listener's own reckoning of where everyone is).
+- The quality: network.voice_kbps (8 to 64), in the lobby and in a game,
+ which the host tells each machine to encode at, and holds it to.
+
+What the host takes is held to that too: a machine's frames only with the
+key the host gave it over its stream (a datagram's source address is all
+that names its machine, and anyone can send one as from another's), no
+larger than the quality allows, no more than VOICE_FRAMES_PER_SECOND (with
+a little burst), only while voice is on, and no more than
+VOICE_MAXIMUM_TALKERS machines at once. A machine is sent voices once it
+has said it hears them (a frame, or an empty one every two seconds), so a
+build without voice chat is never sent any. The host relays only: it never
+decodes, and a machine's own voice never comes back to it.
+
+Each machine decides only for itself: whether it talks (audio.voice_chat:
+push to talk, open mic, or not), how loud the others are
+(audio.voice_volume), and whom it mutes (the scoreboard's menu,
+game_engine.c), which it does not tell anyone.
+*/
+
+#include "cseries.h"
+#include "cseries/errors.h"
+#include "memory/data.h"
+#include "math/integer_math.h"
+#include "camera/observer.h"
+#include "game/game.h"
+#include "game/game_engine.h"
+#include "game/players.h"
+#include "main/console.h"
+#include "networking/network_game_globals.h"
+#include "networking/network_game_manager.h"
+#include "networking/network_client_manager.h"
+#include "objects/objects.h"
+#include "network_distributed.h"
+#include "network_voice.h"
+#include "../src/voice_audio.h"
+
+#include
+
+/* port_config.c's, network_game_globals.c's, network_server_message_handler.c's,
+xinput_sdl.c's, posix_net.c's, cseries_windows.c's */
+int config_boolean(const char *name);
+long config_integer(const char *name);
+double config_real(const char *name);
+const char *config_string(const char *name);
+unsigned long config_changes(void);
+boolean network_distributed_client_send(void *message, word size);
+boolean network_distributed_server_send_to_machine(long machine_index, void *message, word size);
+boolean network_distributed_server_send_to_machine_reliably(long machine_index, void *message, word size);
+short network_distributed_server_machines(long *machine_indices, short maximum);
+int halo_push_to_talk_held(void);
+/* cinematics.c's */
+void draw_quad(rectangle2d *rectangle, pixel32 color);
+void posix_random_bytes(void *buffer, unsigned int size);
+unsigned long system_milliseconds(void);
+
+/* ---------- constants */
+
+enum
+{
+ VOICE_MACHINES = HALO_PORT_MAXIMUM_NETWORK_MACHINES,
+ /* a frame every 20 ms, and a burst of a few (a client's frames sent
+ in a clump after a stall) */
+ VOICE_FRAMES_PER_SECOND = 50,
+ VOICE_FRAME_BURST = 10,
+ /* machines heard at once, at most */
+ VOICE_MAXIMUM_TALKERS = 8,
+ /* a machine talking: one whose frame came this lately (ms) */
+ VOICE_TALKING_MS = 500,
+ /* a machine that hears voices: one heard from this lately (ms) */
+ VOICE_LISTENING_MS = 6000,
+ /* how often a client says it hears voices, and the host repeats its
+ settings (ms) */
+ VOICE_HELLO_MS = 2000,
+ VOICE_CONFIG_MS = 10000,
+ /* a client forgets the host's settings unheard this long (ms) */
+ VOICE_CONFIG_TIMEOUT_MS = 25000,
+ /* a speaker's icon shows this long after their last frame (ms) */
+ VOICE_SPEAKING_MS = 300,
+ /* open mic: talking while the microphone's level is over this (about
+ -40 dB), and for this long after (ms) */
+ VOICE_OPEN_MIC_HOLD_MS = 400,
+ VOICE_MINIMUM_KBPS = 8,
+ VOICE_MAXIMUM_KBPS = 64,
+};
+
+#define VOICE_OPEN_MIC_LEVEL 0.01f
+/* proximity: full volume this near, quieter further (world units) */
+#define VOICE_NEAR_DISTANCE 3.0f
+#define VOICE_FARTHEST_GAIN 0.2f
+
+/* how the host's settings travel */
+enum
+{
+ _voice_config_lobby_bit,
+};
+
+/* network_client_manager.c's client state in game (its enum is its own) */
+enum
+{
+ _voice_client_state_ingame = 3,
+};
+
+/* ---------- structures */
+
+/* a client's frame to the host (its Opus packet follows; none: it hears
+voices, and says so) */
+struct distributed_voice_up
+{
+ unsigned long key;
+ word sequence;
+ byte length;
+ byte pad;
+};
+
+/* a frame relayed to a client (the packet follows) */
+struct distributed_voice_down
+{
+ unsigned long key;
+ word sequence;
+ /* the machine that spoke, and its first player (absolute; NO_PLAYER
+ in the lobby) */
+ byte machine_index;
+ byte player_index;
+ /* how it is heard (_voice_route_*) */
+ byte route;
+ byte length;
+ byte pad[2];
+};
+
+/* the host's settings, to each client over its stream, with its key */
+struct distributed_voice_config
+{
+ unsigned long key;
+ byte flags;
+ byte mode;
+ byte kbps;
+ byte pad0;
+ word proximity_tenths;
+ byte pad[2];
+};
+
+typedef char distributed_voice_up_size_assert[sizeof(struct distributed_voice_up) == 8 ? 1 : -1];
+typedef char distributed_voice_down_size_assert[sizeof(struct distributed_voice_down) == 12 ? 1 : -1];
+typedef char distributed_voice_config_size_assert[sizeof(struct distributed_voice_config) == 12 ? 1 : -1];
+
+/* the host's settings as they are used */
+struct voice_settings
+{
+ boolean lobby;
+ short mode;
+ short kbps;
+ real proximity;
+};
+
+/* ---------- globals */
+
+/* the host: each machine slot's key, whether it was told it, and when it
+was told the settings last; when it was heard from; its frames' allowance
+and when it was topped up; when its last frame was relayed */
+static struct
+{
+ unsigned long key;
+ boolean told;
+ unsigned long told_at;
+ unsigned long heard_at;
+ boolean heard;
+ real allowance;
+ unsigned long allowance_at;
+ unsigned long talked_at;
+ boolean talked;
+} voice_machines[VOICE_MACHINES];
+static struct voice_settings voice_host_settings;
+static unsigned long voice_host_settings_read_at = (unsigned long)-1;
+/* (the settings told, to tell again when they change) */
+static struct voice_settings voice_host_settings_told;
+
+/* this machine: the host's settings and its key (heard: a client), when
+they came; the sequence of its frames; whether it talked lately, and the
+microphone was asked for */
+static struct voice_settings voice_settings;
+static unsigned long voice_key;
+static boolean voice_settings_heard;
+static unsigned long voice_settings_heard_at;
+static word voice_sequence;
+static unsigned long voice_hello_at;
+static unsigned long voice_talked_at;
+static boolean voice_talked;
+static unsigned long voice_open_mic_until;
+static boolean voice_microphone_wanted;
+static boolean voice_session;
+
+/* every machine slot's last frame heard (for its icon), and whether this
+machine mutes it, with the names it had then (a new machine at the slot is
+not muted) */
+static unsigned long voice_heard_at[VOICE_MACHINES];
+static boolean voice_heard[VOICE_MACHINES];
+static boolean voice_muted[VOICE_MACHINES];
+static wchar_t voice_muted_names[VOICE_MACHINES][12];
+
+/* ---------- the route (pure: tools/harness/tests/voice_route.c) */
+
+/* how a listener hears a speaker: in the lobby (and the postgame), anyone
+if the lobby has voice; in a game, by the mode, teammates anywhere or near,
+enemies near ("near": within the range, both alive) */
+static short voice_route(
+ short mode,
+ boolean lobby,
+ boolean lobby_voice,
+ boolean teammates,
+ boolean both_alive,
+ real distance,
+ real range)
+{
+ boolean near = both_alive && distance <= range;
+
+ if (lobby)
+ return lobby_voice ? _voice_route_global : _voice_route_none;
+ switch (mode)
+ {
+ case _voice_mode_team_proximity:
+ return teammates && near ? _voice_route_proximity : _voice_route_none;
+ case _voice_mode_team_enemy_proximity:
+ return near ? _voice_route_proximity : _voice_route_none;
+ case _voice_mode_team_global:
+ return teammates ? _voice_route_global : _voice_route_none;
+ case _voice_mode_team_global_enemy_proximity:
+ if (teammates)
+ return _voice_route_global;
+ return near ? _voice_route_proximity : _voice_route_none;
+ default:
+ return _voice_route_none;
+ }
+}
+
+/* the largest packet a frame may be at the quality (kilobits a second):
+twice its share, for Opus's variable rate, and a little */
+static long voice_packet_limit(
+ short kbps)
+{
+ long limit = (long)kbps * 1000 / 8 / VOICE_FRAMES_PER_SECOND * 2 + 16;
+
+ return MIN(limit, VOICE_MAXIMUM_PACKET);
+}
+
+/* ---------- settings */
+
+static short voice_mode_from_text(
+ char const *text)
+{
+ static char const *const names[NUMBER_OF_VOICE_MODES] =
+ {
+ "off", "team_proximity", "team_enemy_proximity", "team_global", "team_global_enemy_proximity",
+ };
+ short mode;
+
+ for (mode = 0; text && mode < NUMBER_OF_VOICE_MODES; mode++)
+ {
+ if (!csstrcmp(text, names[mode]))
+ return mode;
+ }
+ return _voice_mode_team_global_enemy_proximity;
+}
+
+/* the host's, from its config.toml (read again as it changes) */
+static void voice_host_settings_read(
+ void)
+{
+ if (voice_host_settings_read_at == config_changes())
+ return;
+ voice_host_settings_read_at = config_changes();
+ voice_host_settings.lobby = config_boolean("network.voice_lobby") != 0;
+ voice_host_settings.mode = voice_mode_from_text(config_string("network.voice_mode"));
+ voice_host_settings.kbps = (short)PIN(config_integer("network.voice_kbps"), VOICE_MINIMUM_KBPS,
+ VOICE_MAXIMUM_KBPS);
+ voice_host_settings.proximity = (real)PIN(config_real("network.voice_proximity"), 5.0, 100.0);
+}
+
+static boolean voice_host(
+ void)
+{
+ return global_network_game_server_get() != NULL;
+}
+
+/* in the lobby, as voice has it: this machine's network game not in game
+(the menus' own game, the main menu's, runs meanwhile), or in a multiplayer
+game's postgame (co-op, with no game engine, has none) */
+static boolean voice_in_lobby(
+ void)
+{
+ struct network_game_client *client = global_network_game_client_get();
+ short state_data;
+
+ if (!client || network_game_client_get_state(client, &state_data) != _voice_client_state_ingame ||
+ !game_in_progress())
+ {
+ return TRUE;
+ }
+ return game_engine && game_engine_showing_postgame();
+}
+
+/* ---------- players */
+
+/* the network game this machine is in (the host's, its client's), or NULL */
+static struct network_game *voice_network_game(
+ void)
+{
+ struct network_game_client *client = global_network_game_client_get();
+
+ return client ? network_game_client_get_game(client) : NULL;
+}
+
+/* a machine's first player in the game (its datum), or NULL */
+static struct player_datum *voice_machine_player(
+ long machine_index,
+ long *player_index)
+{
+ struct data_iterator iterator;
+ struct player_datum *player;
+
+ if (!game_in_progress() || !player_data)
+ return NULL;
+ data_iterator_new(&iterator, player_data);
+ while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL)
+ {
+ if (!player->quit_out_of_game && player->network_player_data.machine_index == machine_index)
+ {
+ if (player_index)
+ *player_index = iterator.datum_index;
+ return player;
+ }
+ }
+ return NULL;
+}
+
+/* a machine's first player's name as the network game has it (empty if
+none) */
+static void voice_machine_name(
+ long machine_index,
+ wchar_t *name)
+{
+ struct network_game *game = voice_network_game();
+ short index;
+
+ csmemset(name, 0, 12 * sizeof(*name));
+ for (index = 0; game && index < HALO_PORT_MAXIMUM_NETWORK_PLAYERS; index++)
+ {
+ struct network_player *player = &game->players[index];
+
+ if (network_player_is_valid(player) && player->machine_index == machine_index)
+ {
+ csmemcpy(name, player->name, sizeof(player->name));
+ return;
+ }
+ }
+}
+
+/* a player's living unit's position */
+static boolean voice_player_position(
+ struct player_datum const *player,
+ real_point3d *position)
+{
+ long unit_index = player ? distributed_living_unit(player) : NONE;
+
+ if (unit_index == NONE)
+ return FALSE;
+ object_get_origin(unit_index, position);
+ return TRUE;
+}
+
+/* (the host) each machine's first player in the game (NULL: none), found
+once for a frame's relaying */
+static void voice_machine_players(
+ struct player_datum **players)
+{
+ struct data_iterator iterator;
+ struct player_datum *player;
+
+ csmemset(players, 0, VOICE_MACHINES * sizeof(*players));
+ if (voice_in_lobby() || !player_data)
+ return;
+ data_iterator_new(&iterator, player_data);
+ while ((player = (struct player_datum *)data_iterator_next(&iterator)) != NULL)
+ {
+ long machine_index = player->network_player_data.machine_index;
+
+ if (!player->quit_out_of_game && machine_index >= 0 && machine_index < VOICE_MACHINES &&
+ !players[machine_index])
+ {
+ players[machine_index] = player;
+ }
+ }
+}
+
+/* (the host) how a listener's machine hears a speaker's, now, by their
+first players */
+static short voice_route_between(
+ struct player_datum *speaker,
+ struct player_datum *listener)
+{
+ real_point3d speaker_position;
+ real_point3d listener_position;
+ boolean both_alive;
+ boolean teammates;
+ real distance = 0.0f;
+
+ if (voice_in_lobby())
+ return voice_route(voice_host_settings.mode, TRUE, voice_host_settings.lobby, FALSE, FALSE, 0.0f, 0.0f);
+ if (!speaker || !listener)
+ return _voice_route_none;
+ /* (co-op: one team; a game without teams: none) */
+ teammates = !game_engine || (game_engine_has_teams() && speaker->team_index == listener->team_index);
+ both_alive = voice_player_position(speaker, &speaker_position) &&
+ voice_player_position(listener, &listener_position);
+ if (both_alive)
+ distance = distance3d(&speaker_position, &listener_position);
+ return voice_route(voice_host_settings.mode, FALSE, voice_host_settings.lobby, teammates, both_alive, distance,
+ voice_host_settings.proximity);
+}
+
+/* a message's headers: the transport's (its size: the connection checks it)
+and the distributed netcode's (its time the game's, none in the lobby) */
+static void voice_fill_header(
+ struct distributed_message_header *header,
+ byte type,
+ word size)
+{
+ header->header = 0;
+ build_message_header(&header->header, size, 2, 0);
+ header->type = type;
+ header->count = 1;
+ header->game_time = game_in_progress() ? game_time_get() : 0;
+}
+
+/* ---------- the host */
+
+static boolean voice_machine_valid(
+ long machine_index)
+{
+ return machine_index >= 0 && machine_index < VOICE_MACHINES;
+}
+
+/* (network_server_manager.c) a machine joined at the slot: a new key, told
+to it before anything is taken from it */
+void network_voice_machine_joined(
+ long machine_index)
+{
+ if (!voice_machine_valid(machine_index))
+ return;
+ csmemset(&voice_machines[machine_index], 0, sizeof(voice_machines[machine_index]));
+ do
+ {
+ posix_random_bytes(&voice_machines[machine_index].key, sizeof(voice_machines[machine_index].key));
+ } while (!voice_machines[machine_index].key);
+}
+
+/* the settings and its key, to one client, over its stream */
+static void voice_host_tell(
+ long machine_index)
+{
+ struct
+ {
+ struct distributed_message_header header;
+ struct distributed_voice_config config;
+ } message;
+
+ csmemset(&message, 0, sizeof(message));
+ voice_fill_header(&message.header, _distributed_message_voice_config, (word)sizeof(message));
+ message.config.key = voice_machines[machine_index].key;
+ message.config.flags = voice_host_settings.lobby ? FLAG(_voice_config_lobby_bit) : 0;
+ message.config.mode = (byte)voice_host_settings.mode;
+ message.config.kbps = (byte)voice_host_settings.kbps;
+ message.config.proximity_tenths = (word)(voice_host_settings.proximity * 10.0f);
+ if (network_distributed_server_send_to_machine_reliably(machine_index, &message, (word)sizeof(message)))
+ {
+ voice_machines[machine_index].told = TRUE;
+ voice_machines[machine_index].told_at = system_milliseconds();
+ }
+}
+
+/* whether a machine is one this machine plays a voice of: not muted */
+static boolean voice_audible(
+ long machine_index)
+{
+ return voice_machine_valid(machine_index) && !voice_muted[machine_index];
+}
+
+/* the gain and pan of a voice heard for being near (this machine's own
+reckoning: its first player's view, and the speaker's unit) */
+static void voice_proximity_gain(
+ short player_index,
+ real *gain,
+ real *pan)
+{
+ struct player_datum *player = player_index != NONE ? distributed_player(player_index) : NULL;
+ struct observer_result const *camera;
+ real_point3d position;
+ real_vector3d direction;
+ real_vector3d right;
+ real distance;
+ real range = voice_settings.proximity > VOICE_NEAR_DISTANCE ? voice_settings.proximity : VOICE_NEAR_DISTANCE + 1.0f;
+
+ *gain = 1.0f;
+ *pan = 0.0f;
+ if (!voice_player_position(player, &position) || local_player_get_next(NONE) == NONE)
+ return;
+ camera = observer_get_camera((short)local_player_get_next(NONE));
+ if (!camera)
+ return;
+ vector_from_points3d(&camera->position, &position, &direction);
+ distance = magnitude3d(&direction);
+ if (distance > VOICE_NEAR_DISTANCE)
+ {
+ real t = PIN((distance - VOICE_NEAR_DISTANCE) / (range - VOICE_NEAR_DISTANCE), 0.0f, 1.0f);
+
+ *gain = 1.0f - (1.0f - VOICE_FARTHEST_GAIN) * t;
+ }
+ if (distance > 0.01f)
+ {
+ cross_product3d(&camera->forward, &camera->up, &right);
+ normalize3d(&right);
+ scale_vector3d(&direction, 1.0f / distance, &direction);
+ *pan = 0.8f * dot_product3d(&direction, &right);
+ }
+}
+
+/* a frame heard here (the host's own listening, or one relayed to a
+client); the speaker's first player (absolute index, NONE for none) */
+static void voice_play(
+ long machine_index,
+ short player_index,
+ short route,
+ word sequence,
+ byte const *packet,
+ long length)
+{
+ real gain = 1.0f;
+ real pan = 0.0f;
+
+ if (!voice_machine_valid(machine_index) || length <= 0)
+ return;
+ voice_heard_at[machine_index] = system_milliseconds();
+ voice_heard[machine_index] = TRUE;
+ if (!voice_audible(machine_index))
+ return;
+ if (route == _voice_route_proximity)
+ voice_proximity_gain(player_index, &gain, &pan);
+ voice_audio_play((int)machine_index, sequence, packet, (int)length, gain, pan);
+}
+
+/* how many machines talk now, but the one */
+static short voice_host_talkers(
+ long except_machine,
+ unsigned long now)
+{
+ short count = 0;
+ long index;
+
+ for (index = 0; index < VOICE_MACHINES; index++)
+ {
+ if (index != except_machine && voice_machines[index].talked &&
+ now - voice_machines[index].talked_at < VOICE_TALKING_MS)
+ {
+ count++;
+ }
+ }
+ return count;
+}
+
+/* (the host) a frame of a machine (its own, or a client's), checked, and
+sent to those that hear it */
+static void voice_host_relay(
+ long speaker_machine,
+ word sequence,
+ byte const *packet,
+ long length)
+{
+ static struct player_datum *players[VOICE_MACHINES];
+ long machine_indices[VOICE_MACHINES];
+ short count = network_distributed_server_machines(machine_indices, VOICE_MACHINES);
+ short local_machine = network_game_client_get_local_machine_index();
+ long speaker_player = NONE;
+ unsigned long now = system_milliseconds();
+ short index;
+
+ if (!voice_machine_valid(speaker_machine) || length <= 0 ||
+ length > voice_packet_limit(voice_host_settings.kbps))
+ {
+ return;
+ }
+ /* (no more than so many at once: those talking keep talking) */
+ if (!(voice_machines[speaker_machine].talked && now - voice_machines[speaker_machine].talked_at < VOICE_TALKING_MS) &&
+ voice_host_talkers(speaker_machine, now) >= VOICE_MAXIMUM_TALKERS)
+ {
+ return;
+ }
+ voice_machines[speaker_machine].talked = TRUE;
+ voice_machines[speaker_machine].talked_at = now;
+ voice_machine_players(players);
+ /* (the lobby has no players' positions: the menus' game has players of
+ its own) */
+ if (!voice_in_lobby())
+ voice_machine_player(speaker_machine, &speaker_player);
+ for (index = 0; index < count; index++)
+ {
+ long listener = machine_indices[index];
+ short route;
+
+ if (!voice_machine_valid(listener) || listener == speaker_machine)
+ continue;
+ route = voice_route_between(players[speaker_machine], players[listener]);
+ if (route == _voice_route_none)
+ continue;
+ if (listener == local_machine)
+ {
+ voice_play(speaker_machine, speaker_player != NONE ? (short)DATUM_INDEX_TO_ABSOLUTE_INDEX(speaker_player) :
+ NONE, route, sequence, packet, length);
+ continue;
+ }
+ /* (only to a machine that has said it hears voices) */
+ if (!voice_machines[listener].heard || now - voice_machines[listener].heard_at >= VOICE_LISTENING_MS ||
+ !voice_machines[listener].told)
+ {
+ continue;
+ }
+ {
+ struct
+ {
+ struct distributed_message_header header;
+ struct distributed_voice_down down;
+ byte packet[VOICE_MAXIMUM_PACKET];
+ } message;
+
+ csmemset(&message, 0, sizeof(message.header) + sizeof(message.down));
+ voice_fill_header(&message.header, _distributed_message_voice_down,
+ (word)(sizeof(message.header) + sizeof(message.down) + length));
+ message.down.key = voice_machines[listener].key;
+ message.down.sequence = sequence;
+ message.down.machine_index = (byte)speaker_machine;
+ message.down.player_index = speaker_player != NONE ?
+ (byte)DATUM_INDEX_TO_ABSOLUTE_INDEX(speaker_player) : NO_PLAYER;
+ message.down.route = (byte)route;
+ message.down.length = (byte)length;
+ csmemcpy(message.packet, packet, length);
+ network_distributed_server_send_to_machine(listener, &message,
+ (word)(sizeof(message.header) + sizeof(message.down) + length));
+ }
+ }
+}
+
+/* (the host) a client's frame: its key, its allowance, the settings' */
+static void voice_host_handle_up(
+ long machine_index,
+ struct distributed_voice_up const *up,
+ byte const *packet,
+ long length)
+{
+ unsigned long now = system_milliseconds();
+
+ if (!voice_machine_valid(machine_index) || !voice_machines[machine_index].key ||
+ up->key != voice_machines[machine_index].key || up->length != length)
+ {
+ return;
+ }
+ voice_machines[machine_index].heard = TRUE;
+ voice_machines[machine_index].heard_at = now;
+ /* (an empty frame: it hears voices) */
+ if (!length)
+ return;
+ /* (its allowance: so many frames a second, a few at once) */
+ if (!voice_machines[machine_index].allowance_at)
+ voice_machines[machine_index].allowance = VOICE_FRAME_BURST;
+ else
+ {
+ voice_machines[machine_index].allowance += (real)(now - voice_machines[machine_index].allowance_at) *
+ VOICE_FRAMES_PER_SECOND / 1000.0f;
+ }
+ voice_machines[machine_index].allowance = MIN(voice_machines[machine_index].allowance, (real)VOICE_FRAME_BURST);
+ voice_machines[machine_index].allowance_at = now;
+ if (voice_machines[machine_index].allowance < 1.0f)
+ return;
+ voice_machines[machine_index].allowance -= 1.0f;
+ voice_host_relay(machine_index, up->sequence, packet, length);
+}
+
+/* ---------- messages (network_distributed.c) */
+
+boolean network_voice_handles_message(
+ word const *message,
+ word size)
+{
+ struct distributed_message_header header;
+
+ if (size < sizeof(header))
+ return FALSE;
+ csmemcpy(&header, message, sizeof(header));
+ return header.type == _distributed_message_voice_up || header.type == _distributed_message_voice_down ||
+ header.type == _distributed_message_voice_config;
+}
+
+void network_voice_handle_message(
+ long machine_index,
+ word const *message,
+ word size)
+{
+ struct distributed_message_header header;
+ byte const *entries = (byte const *)message + sizeof(header);
+ long length = (long)size - (long)sizeof(header);
+
+ if (!network_voice_handles_message(message, size))
+ return;
+ csmemcpy(&header, message, sizeof(header));
+ switch (header.type)
+ {
+ case _distributed_message_voice_up:
+ {
+ struct distributed_voice_up up;
+
+ if (machine_index == NONE || !voice_host() || length < (long)sizeof(up))
+ return;
+ csmemcpy(&up, entries, sizeof(up));
+ voice_host_handle_up(machine_index, &up, entries + sizeof(up), length - (long)sizeof(up));
+ break;
+ }
+ case _distributed_message_voice_down:
+ {
+ struct distributed_voice_down down;
+
+ if (machine_index != NONE || voice_host() || !voice_settings_heard || length < (long)sizeof(down))
+ return;
+ csmemcpy(&down, entries, sizeof(down));
+ if (down.key != voice_key || down.length != length - (long)sizeof(down) ||
+ down.machine_index == network_game_client_get_local_machine_index())
+ {
+ return;
+ }
+ voice_play(down.machine_index, down.player_index != NO_PLAYER && !voice_in_lobby() ? down.player_index : NONE,
+ down.route, down.sequence, entries + sizeof(down), down.length);
+ break;
+ }
+ case _distributed_message_voice_config:
+ {
+ struct distributed_voice_config config;
+
+ if (machine_index != NONE || voice_host() || length < (long)sizeof(config))
+ return;
+ csmemcpy(&config, entries, sizeof(config));
+ voice_key = config.key;
+ voice_settings.lobby = TEST_FLAG(config.flags, _voice_config_lobby_bit);
+ voice_settings.mode = config.mode < NUMBER_OF_VOICE_MODES ? config.mode : _voice_mode_off;
+ voice_settings.kbps = (short)PIN(config.kbps, VOICE_MINIMUM_KBPS, VOICE_MAXIMUM_KBPS);
+ voice_settings.proximity = config.proximity_tenths / 10.0f;
+ voice_settings_heard = TRUE;
+ voice_settings_heard_at = system_milliseconds();
+ break;
+ }
+ }
+}
+
+/* ---------- every frame (main.c) */
+
+/* whether this machine may talk now, as the host's settings have it */
+static boolean voice_may_talk(
+ void)
+{
+ if (voice_in_lobby())
+ return voice_settings.lobby;
+ return voice_settings.mode != _voice_mode_off;
+}
+
+/* the host: every client told its key and the settings (again as they
+change, and now and then) */
+static void voice_host_update(
+ void)
+{
+ long machine_indices[VOICE_MACHINES];
+ short count = network_distributed_server_machines(machine_indices, VOICE_MACHINES);
+ short local_machine = network_game_client_get_local_machine_index();
+ boolean changed;
+ unsigned long now = system_milliseconds();
+ short index;
+
+ voice_host_settings_read();
+ changed = csmemcmp(&voice_host_settings, &voice_host_settings_told, sizeof(voice_host_settings)) != 0;
+ voice_host_settings_told = voice_host_settings;
+ /* (the host's own, as a client hears them) */
+ voice_settings = voice_host_settings;
+ voice_settings_heard = TRUE;
+ voice_settings_heard_at = now;
+ for (index = 0; index < count; index++)
+ {
+ long machine_index = machine_indices[index];
+
+ if (!voice_machine_valid(machine_index) || machine_index == local_machine)
+ continue;
+ if (!voice_machines[machine_index].key)
+ network_voice_machine_joined(machine_index);
+ if (!voice_machines[machine_index].told || changed ||
+ now - voice_machines[machine_index].told_at >= VOICE_CONFIG_MS)
+ {
+ voice_host_tell(machine_index);
+ }
+ }
+}
+
+/* this machine's frame: the host's own relayed, a client's sent up (none:
+it hears voices) */
+static void voice_send(
+ byte const *packet,
+ long length)
+{
+ if (voice_host())
+ {
+ if (length > 0)
+ voice_host_relay(network_game_client_get_local_machine_index(), voice_sequence, packet, length);
+ }
+ else
+ {
+ struct
+ {
+ struct distributed_message_header header;
+ struct distributed_voice_up up;
+ byte packet[VOICE_MAXIMUM_PACKET];
+ } message;
+
+ csmemset(&message, 0, sizeof(message.header) + sizeof(message.up));
+ voice_fill_header(&message.header, _distributed_message_voice_up,
+ (word)(sizeof(message.header) + sizeof(message.up) + length));
+ message.up.key = voice_key;
+ message.up.sequence = voice_sequence;
+ message.up.length = (byte)length;
+ if (length > 0)
+ csmemcpy(message.packet, packet, length);
+ network_distributed_client_send(&message, (word)(sizeof(message.header) + sizeof(message.up) + length));
+ }
+ if (length > 0)
+ voice_sequence++;
+}
+
+/* the session over (or none): the microphone closed, voices forgotten */
+static void voice_end_session(
+ void)
+{
+ if (!voice_session)
+ return;
+ voice_session = FALSE;
+ voice_microphone_wanted = FALSE;
+ voice_audio_microphone(FALSE);
+ voice_audio_forget_all();
+ voice_settings_heard = FALSE;
+ voice_talked = FALSE;
+ csmemset(voice_heard, 0, sizeof(voice_heard));
+}
+
+void network_voice_update(
+ void)
+{
+ char const *chat = config_string("audio.voice_chat");
+ boolean open_mic = chat && !csstrcmp(chat, "open_mic");
+ boolean push_to_talk = chat && !csstrcmp(chat, "push_to_talk");
+ boolean held;
+ unsigned long now = system_milliseconds();
+ float frame[VOICE_FRAME_SAMPLES];
+ short local_machine = network_game_client_get_local_machine_index();
+ long index;
+
+ voice_audio_set_volume((float)PIN(config_real("audio.voice_volume"), 0.0, 2.0));
+ /* (a network game: the host's, or one joined) */
+ if (!voice_network_game() || local_machine == NONE)
+ {
+ voice_end_session();
+ return;
+ }
+ voice_session = TRUE;
+ if (voice_host())
+ voice_host_update();
+ else if (voice_settings_heard && now - voice_settings_heard_at >= VOICE_CONFIG_TIMEOUT_MS)
+ {
+ /* (the host repeats them: one that stopped has no voice chat) */
+ voice_settings_heard = FALSE;
+ }
+ /* (mutes of a machine whose players changed are forgotten) */
+ for (index = 0; index < VOICE_MACHINES; index++)
+ {
+ wchar_t name[12];
+
+ if (!voice_muted[index])
+ continue;
+ voice_machine_name(index, name);
+ if (csmemcmp(name, voice_muted_names[index], sizeof(name)))
+ voice_muted[index] = FALSE;
+ }
+ if (!voice_settings_heard)
+ {
+ voice_audio_microphone(FALSE);
+ voice_microphone_wanted = FALSE;
+ return;
+ }
+ /* (a client says it hears voices) */
+ if (!voice_host() && now - voice_hello_at >= VOICE_HELLO_MS)
+ {
+ voice_hello_at = now;
+ voice_send(NULL, 0);
+ }
+ /* the microphone: open mic, open while voice is on; push to talk, from
+ the first press (none asked for until then) */
+ held = push_to_talk && halo_push_to_talk_held() && !console_is_active();
+ if (!voice_may_talk() || (!open_mic && !push_to_talk))
+ voice_microphone_wanted = FALSE;
+ else if (open_mic || held)
+ voice_microphone_wanted = TRUE;
+ voice_audio_microphone(voice_microphone_wanted);
+ while (voice_microphone_wanted && voice_audio_read_frame(frame))
+ {
+ boolean talk = held;
+ byte packet[VOICE_MAXIMUM_PACKET];
+ long length;
+
+ if (open_mic)
+ {
+ if (voice_audio_level(frame) >= VOICE_OPEN_MIC_LEVEL)
+ voice_open_mic_until = now + VOICE_OPEN_MIC_HOLD_MS;
+ talk = (long)(voice_open_mic_until - now) > 0;
+ }
+ if (!talk || !voice_may_talk())
+ continue;
+ length = voice_audio_encode(frame, voice_settings.kbps * 1000, packet,
+ (int)MIN(voice_packet_limit(voice_settings.kbps), VOICE_MAXIMUM_PACKET));
+ if (length <= 0)
+ continue;
+ voice_send(packet, length);
+ voice_talked = TRUE;
+ voice_talked_at = now;
+ }
+}
+
+/* ---------- the menus and the scoreboard (network_voice.h) */
+
+boolean network_voice_machine_speaking(
+ long machine_index)
+{
+ unsigned long now = system_milliseconds();
+
+ if (!voice_session || !voice_machine_valid(machine_index))
+ return FALSE;
+ if (machine_index == network_game_client_get_local_machine_index())
+ return voice_talked && now - voice_talked_at < VOICE_SPEAKING_MS;
+ return voice_heard[machine_index] && now - voice_heard_at[machine_index] < VOICE_SPEAKING_MS &&
+ !voice_muted[machine_index];
+}
+
+boolean network_voice_machine_muted(
+ long machine_index)
+{
+ return voice_machine_valid(machine_index) && voice_muted[machine_index];
+}
+
+void network_voice_mute_machine(
+ long machine_index,
+ boolean mute)
+{
+ if (!voice_machine_valid(machine_index) || machine_index == network_game_client_get_local_machine_index())
+ return;
+ voice_muted[machine_index] = mute;
+ voice_machine_name(machine_index, voice_muted_names[machine_index]);
+ if (mute)
+ voice_audio_forget((int)machine_index);
+}
+
+boolean network_voice_available(
+ void)
+{
+ return voice_session && voice_settings_heard;
+}
+
+/* a speaker drawn in the rectangle (its height the icon's size, at its left;
+muted: grey, struck through in red), fading with alpha */
+void network_voice_draw_icon(
+ rectangle2d const *bounds,
+ boolean muted,
+ real alpha)
+{
+ short size = (short)(bounds->y1 - bounds->y0);
+ short x = bounds->x0;
+ short y = bounds->y0;
+ pixel32 a = (pixel32)(PIN(alpha, 0.0f, 1.0f) * 255.0f + 0.5f) << 24;
+ pixel32 color = a | (muted ? 0x00909090 : 0x0050E050);
+ rectangle2d part;
+ short slice;
+
+ if (size < 4)
+ return;
+ /* (the body, the cone in slices widening, and two waves) */
+ part.x0 = x;
+ part.x1 = (short)(x + size * 3 / 10);
+ part.y0 = (short)(y + size * 35 / 100);
+ part.y1 = (short)(y + size * 65 / 100);
+ draw_quad(&part, color);
+ for (slice = 0; slice < 3; slice++)
+ {
+ part.x0 = (short)(x + size * (30 + slice * 8) / 100);
+ part.x1 = (short)(x + size * (38 + slice * 8) / 100);
+ part.y0 = (short)(y + size * (30 - slice * 10) / 100);
+ part.y1 = (short)(y + size * (70 + slice * 10) / 100);
+ draw_quad(&part, color);
+ }
+ if (!muted)
+ {
+ part.x0 = (short)(x + size * 66 / 100);
+ part.x1 = (short)(part.x0 + MAX(2, size / 8));
+ part.y0 = (short)(y + size * 30 / 100);
+ part.y1 = (short)(y + size * 70 / 100);
+ draw_quad(&part, color);
+ part.x0 = (short)(x + size * 84 / 100);
+ part.x1 = (short)(part.x0 + MAX(2, size / 8));
+ part.y0 = (short)(y + size * 15 / 100);
+ part.y1 = (short)(y + size * 85 / 100);
+ draw_quad(&part, color);
+ return;
+ }
+ /* (struck through: a slash of small squares) */
+ for (slice = 0; slice < 6; slice++)
+ {
+ short step = (short)(size * slice / 6);
+
+ part.x0 = (short)(x + step);
+ part.x1 = (short)(part.x0 + MAX(2, size / 6));
+ part.y0 = (short)(y + step);
+ part.y1 = (short)(part.y0 + MAX(2, size / 6));
+ draw_quad(&part, a | 0x00E03030);
+ }
+}
diff --git a/port/linux/game/network_voice.h b/port/linux/game/network_voice.h
new file mode 100644
index 00000000..b0a36a45
--- /dev/null
+++ b/port/linux/game/network_voice.h
@@ -0,0 +1,59 @@
+/*
+NETWORK_VOICE.H
+
+Voice chat (network_voice.c): what the main loop, the netcode, the host's
+server, the lobby (menu_functions.c) and the scoreboard (game_engine.c) ask
+of it.
+*/
+
+#ifndef __NETWORK_VOICE_H
+#define __NETWORK_VOICE_H
+#pragma once
+
+/* ---------- constants */
+
+/* network.voice_mode: who hears whom in a game */
+enum
+{
+ _voice_mode_off,
+ _voice_mode_team_proximity,
+ _voice_mode_team_enemy_proximity,
+ _voice_mode_team_global,
+ _voice_mode_team_global_enemy_proximity,
+ NUMBER_OF_VOICE_MODES
+};
+
+/* how a listener hears a speaker */
+enum
+{
+ _voice_route_none,
+ /* as spoken */
+ _voice_route_global,
+ /* quieter further, from the speaker's side */
+ _voice_route_proximity,
+};
+
+/* ---------- prototypes/NETWORK_VOICE.C */
+
+/* every frame (main.c): the microphone, the host's settings told */
+void network_voice_update(void);
+/* whether a message of the distributed kind is voice chat's, which it takes
+in the lobby too; and its handling (machine_index: the sender's on the
+host, NONE on a client) */
+boolean network_voice_handles_message(word const *message, word size);
+void network_voice_handle_message(long machine_index, word const *message, word size);
+/* (the host's server) a machine joined at the slot: its new key */
+void network_voice_machine_joined(long machine_index);
+/* whether this machine has voice chat now (the host's settings heard) */
+boolean network_voice_available(void);
+/* whether a machine (its index in the network game) is talking now (this
+machine's own, as it sends); muted ones never */
+boolean network_voice_machine_speaking(long machine_index);
+/* this machine's mutes of others (which it tells no one) */
+boolean network_voice_machine_muted(long machine_index);
+void network_voice_mute_machine(long machine_index, boolean mute);
+/* a speaker icon, at the rectangle's left and as tall as it (muted: struck
+through), fading with alpha */
+void network_voice_draw_icon(rectangle2d const *bounds, boolean muted, real alpha);
+
+#endif // __NETWORK_VOICE_H
diff --git a/port/linux/include/halo_keyboard.h b/port/linux/include/halo_keyboard.h
index e8a66d12..4a3791ed 100644
--- a/port/linux/include/halo_keyboard.h
+++ b/port/linux/include/halo_keyboard.h
@@ -34,6 +34,9 @@ enum halo_keyboard_action
read from it) */
HALO_KEYBOARD_SCOREBOARD,
HALO_KEYBOARD_PAUSE,
+ /* (voice chat's, which the game itself does not read:
+ halo_push_to_talk_held) */
+ HALO_KEYBOARD_PUSH_TO_TALK,
NUMBER_OF_HALO_KEYBOARD_ACTIONS
};
@@ -41,5 +44,8 @@ enum halo_keyboard_action
(a bit for each halo_keyboard_action): theirs is the first's; none while a
menu or the console is up */
unsigned long halo_keyboard_actions(short controller_index);
+/* whether voice chat's push to talk is held (controls.push_to_talk), in the
+game and in the menus alike */
+int halo_push_to_talk_held(void);
#endif
diff --git a/port/linux/src/dsound_sdl.c b/port/linux/src/dsound_sdl.c
index 3c41da36..4c3f9967 100644
--- a/port/linux/src/dsound_sdl.c
+++ b/port/linux/src/dsound_sdl.c
@@ -37,6 +37,7 @@ turns the reverb off; audio.enabled = false skips opening a device
#include "platform.h"
#include "sdl_platform.h"
#include "port_config.h"
+#include "voice_audio.h"
#include
#include
@@ -1143,6 +1144,8 @@ 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);
limit(output, frames);
}
@@ -1150,6 +1153,17 @@ static void mix(float *output, unsigned long frames)
static SDL_AudioStream *audio_stream;
static BOOL audio_started = FALSE;
+/* the device it plays on (audio.output_device), and when it was looked at */
+static char audio_device_name[PLATFORM_AUDIO_DEVICE_NAME_SIZE];
+static unsigned long audio_device_read_at = (unsigned long)-1;
+
+/* audio.output_device ("default": the system's; none on Android) */
+static const char *audio_device_setting(void)
+{
+ const char *name = config_string("audio.output_device");
+
+ return name && name[0] ? name : "default";
+}
static void SDLCALL audio_callback(void *userdata, SDL_AudioStream *stream, int additional_amount, int total_amount)
{
@@ -1220,7 +1234,12 @@ static void audio_start(void)
#else
SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES, "512");
#endif
- audio_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audio_callback, NULL);
+ snprintf(audio_device_name, sizeof(audio_device_name), "%s", audio_device_setting());
+ audio_device_read_at = config_changes();
+ audio_stream = SDL_OpenAudioDeviceStream(platform_audio_device(FALSE, audio_device_name), &spec,
+ audio_callback, NULL);
+ if (!audio_stream && strcmp(audio_device_name, "default"))
+ audio_stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audio_callback, NULL);
if (audio_stream)
{
SDL_ResumeAudioStreamDevice(audio_stream);
@@ -1236,6 +1255,44 @@ static void audio_start(void)
}
}
+/* (the event thread, each frame: sdl_platform.c) audio.output_device
+changed (Settings > Audio): the sound goes on on the new device, else the
+system's default */
+void dsound_sdl_output_device_check(void)
+{
+ SDL_AudioSpec spec;
+ SDL_AudioStream *stream;
+
+ if (!audio_stream || audio_device_read_at == config_changes())
+ return;
+ audio_device_read_at = config_changes();
+ if (!strcmp(audio_device_name, audio_device_setting()))
+ return;
+ snprintf(audio_device_name, sizeof(audio_device_name), "%s", audio_device_setting());
+ spec.format = SDL_AUDIO_F32;
+ spec.channels = OUTPUT_CHANNELS;
+ spec.freq = OUTPUT_RATE;
+ SDL_DestroyAudioStream(audio_stream);
+ stream = SDL_OpenAudioDeviceStream(platform_audio_device(FALSE, audio_device_name), &spec, audio_callback, NULL);
+ if (!stream)
+ stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audio_callback, NULL);
+ audio_stream = stream;
+ if (audio_stream)
+ {
+ SDL_ResumeAudioStreamDevice(audio_stream);
+ platform_log("audio: playing on %s", audio_device_name);
+ }
+ else
+ {
+ pthread_t thread;
+
+ /* (none at all: the voices drained in real time, as at the start) */
+ platform_log("audio: cannot open an audio device (%s); sound is silent", SDL_GetError());
+ pthread_create(&thread, NULL, silent_clock_thread, NULL);
+ pthread_detach(thread);
+ }
+}
+
/* ---------- completion */
static void packet_release(struct voice_packet *entry)
diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c
index 66be9af5..cc890700 100644
--- a/port/linux/src/port_config.c
+++ b/port/linux/src/port_config.c
@@ -167,6 +167,21 @@ static const struct config_setting config_settings[] =
"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." },
+ { "audio.voice_chat", _config_string, "\"push_to_talk\"", "HALO_VOICE_CHAT", _environment_value, _platform_all,
+ "Talking in network games' voice chat: \"push_to_talk\" (while\n"
+ "controls.push_to_talk is held; the microphone opens the first time),\n"
+ "\"open_mic\" (whenever the microphone hears speech), or \"off\". Others'\n"
+ "voices play whatever this is (audio.voice_volume 0 silences them)." },
+ { "audio.voice_volume", _config_real, "1.0", "HALO_VOICE_VOLUME", _environment_value, _platform_all,
+ "The volume of the other players' voices (0 to 2)." },
+ { "audio.output_device", _config_string, "\"default\"", "HALO_AUDIO_OUTPUT_DEVICE", _environment_value,
+ _platform_desktop,
+ "Where the game's sound and the voices play: a device's name as Settings >\n"
+ "Audio lists it, or \"default\" for the system's (also if it is not found)." },
+ { "audio.input_device", _config_string, "\"default\"", "HALO_AUDIO_INPUT_DEVICE", _environment_value,
+ _platform_desktop,
+ "The microphone voice chat listens to: a device's name as Settings >\n"
+ "Audio lists it, or \"default\" for the system's (also if it is not found)." },
{ "audio.loose_sounds", _config_boolean, "false", "HALO_LOOSE_SOUNDS", _environment_value, _platform_all,
"For those making sounds: play each of a map's sounds that has a Halo PC\n"
"sound tag file of its name under the data root's tags folder\n"
@@ -231,6 +246,8 @@ static const struct config_setting config_settings[] =
"Showing the scores (the controller's Back)." },
{ "controls.pause", _config_string, "\"Escape\"", "HALO_KEY_PAUSE", _environment_value, _platform_all,
"The pause menu (the controller's Start)." },
+ { "controls.push_to_talk", _config_string, "\"V\"", "HALO_KEY_PUSH_TO_TALK", _environment_value, _platform_all,
+ "Voice chat: talk while it is held (audio.voice_chat \"push_to_talk\")." },
{ "game.console_log", _config_string, "\"important\"", "HALO_CONSOLE_LOG", _environment_value, _platform_all,
"What the game's console shows on screen of what it logs: \"important\"\n"
@@ -295,6 +312,23 @@ static const struct config_setting config_settings[] =
"in everyone's server browser: anyone can see and join it) or, false,\n"
"PRIVATE (only players with its invite link can join). Server Setup's\n"
"LISTING changes it for each game." },
+ { "network.voice_lobby", _config_boolean, "true", "HALO_NET_VOICE_LOBBY", _environment_value, _platform_all,
+ "Hosting: voice chat in the lobby, before and after a game, where every\n"
+ "player hears every other." },
+ { "network.voice_mode", _config_string, "\"team_global_enemy_proximity\"", "HALO_NET_VOICE_MODE",
+ _environment_value, _platform_all,
+ "Hosting: voice chat in a game: \"off\"; \"team_proximity\" (teammates\n"
+ "near); \"team_enemy_proximity\" (anyone near); \"team_global\" (all\n"
+ "teammates); or \"team_global_enemy_proximity\" (all teammates, and\n"
+ "enemies near). A game without teams has only enemies; co-op only\n"
+ "teammates." },
+ { "network.voice_kbps", _config_integer, "24", "HALO_NET_VOICE_KBPS", _environment_value, _platform_all,
+ "Hosting: the voices' quality, in the lobby and in a game, in kilobits a\n"
+ "second (8 to 64)." },
+ { "network.voice_proximity", _config_real, "15.0", "HALO_NET_VOICE_PROXIMITY", _environment_value,
+ _platform_all,
+ "Hosting: how near (world units: 1 is about 3 metres) a player must be\n"
+ "to be heard by proximity voice chat (5 to 100)." },
{ "network.votekick", _config_boolean, "true", "HALO_NET_VOTEKICK", _environment_value, _platform_all,
"Hosting: let the players vote to kick a player (the scoreboard's\n"
"right-click, or the console's votekick). More than half of the players\n"
@@ -424,6 +458,9 @@ static const struct config_setting config_settings[] =
"Quit this many seconds after the window opens; 0 never." },
{ "debug.hidden_window", _config_boolean, "false", "HALO_HIDDEN_WINDOW", _environment_set_is_true, _platform_desktop,
"Keep the window hidden (and never fullscreen)." },
+ { "debug.voice_test", _config_boolean, "false", "HALO_VOICE_TEST", _environment_set_is_true, _platform_all,
+ "Voice chat's automated tests: a tone instead of the microphone, and\n"
+ "each voice heard logged once a second." },
{ "debug.null_renderer", _config_boolean, "false", "HALO_NULL_RENDERER", _environment_set_is_true, _platform_all,
"Run without a window, drawing nothing." },
{ "debug.gl_debug", _config_boolean, "false", "HALO_GL_DEBUG", _environment_set_is_true, _platform_all,
diff --git a/port/linux/src/sdl_platform.c b/port/linux/src/sdl_platform.c
index 17a68bdb..3bb05134 100644
--- a/port/linux/src/sdl_platform.c
+++ b/port/linux/src/sdl_platform.c
@@ -90,6 +90,8 @@ static struct platform_keystroke keystroke_queue[KEYSTROKE_QUEUE_SIZE];
static unsigned long keystroke_head, keystroke_count;
#ifndef HALO_ANDROID
+/* dsound_sdl.c's: the output device followed */
+void dsound_sdl_output_device_check(void);
/* updater.c's: the desktop self-updater */
void updater_start(void);
void updater_poll(SDL_Window *window);
@@ -675,6 +677,73 @@ int platform_window_sizes(long *widths, long *heights, int maximum)
}
#endif
+
+/* ---------- audio devices (Settings > Audio: audio.output_device,
+audio.input_device) */
+
+#ifndef HALO_ANDROID
+int platform_audio_devices(int recording, char (*names)[PLATFORM_AUDIO_DEVICE_NAME_SIZE], int maximum)
+{
+ SDL_AudioDeviceID *devices;
+ int device_count = 0, count = 0, index;
+
+ if (maximum < 1 || !platform_sdl_initialize())
+ return 0;
+ devices = recording ? SDL_GetAudioRecordingDevices(&device_count) : SDL_GetAudioPlaybackDevices(&device_count);
+ for (index = 0; devices && index < device_count && count < maximum; index++)
+ {
+ const char *name = SDL_GetAudioDeviceName(devices[index]);
+
+ /* (a name a setting can hold, and a menu show: no "|", which
+ separates a spinner's values) */
+ if (!name || !name[0] || strchr(name, '|') || strlen(name) >= PLATFORM_AUDIO_DEVICE_NAME_SIZE)
+ continue;
+ snprintf(names[count++], PLATFORM_AUDIO_DEVICE_NAME_SIZE, "%s", name);
+ }
+ SDL_free(devices);
+ return count;
+}
+
+SDL_AudioDeviceID platform_audio_device(int recording, const char *name)
+{
+ SDL_AudioDeviceID *devices;
+ SDL_AudioDeviceID found = recording ? SDL_AUDIO_DEVICE_DEFAULT_RECORDING : SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
+ int device_count = 0, index;
+
+ if (!name || !name[0] || !strcmp(name, "default"))
+ return found;
+ devices = recording ? SDL_GetAudioRecordingDevices(&device_count) : SDL_GetAudioPlaybackDevices(&device_count);
+ for (index = 0; devices && index < device_count; index++)
+ {
+ const char *device_name = SDL_GetAudioDeviceName(devices[index]);
+
+ if (device_name && !strcmp(device_name, name))
+ {
+ found = devices[index];
+ break;
+ }
+ }
+ SDL_free(devices);
+ if (found == (recording ? SDL_AUDIO_DEVICE_DEFAULT_RECORDING : SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK))
+ platform_log("audio: no %s device named \"%s\": the system's default", recording ? "input" : "output", name);
+ return found;
+}
+#else
+int platform_audio_devices(int recording, char (*names)[PLATFORM_AUDIO_DEVICE_NAME_SIZE], int maximum)
+{
+ (void)recording;
+ (void)names;
+ (void)maximum;
+ return 0;
+}
+
+SDL_AudioDeviceID platform_audio_device(int recording, const char *name)
+{
+ (void)name;
+ return recording ? SDL_AUDIO_DEVICE_DEFAULT_RECORDING : SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;
+}
+#endif
+
#ifndef HALO_ANDROID
/* the window's size (platform_window_size_setting), as the window was made
or last resized: platform_display_apply */
@@ -1259,6 +1328,8 @@ void platform_pump_events(void)
platform_show_pending_message();
#ifndef HALO_ANDROID
updater_poll(platform_window);
+ /* (Settings > Audio's output device, as it changes: dsound_sdl.c) */
+ dsound_sdl_output_device_check();
#endif
pthread_mutex_lock(&input_lock);
#ifndef HALO_ANDROID
diff --git a/port/linux/src/sdl_platform.h b/port/linux/src/sdl_platform.h
index 6e342994..09d8f31e 100644
--- a/port/linux/src/sdl_platform.h
+++ b/port/linux/src/sdl_platform.h
@@ -9,6 +9,7 @@ controller emulation (see sdl_platform.c).
#define __HALO_LINUX_SDL_PLATFORM_H
#include
+#include
#include
#define PLATFORM_MOUSE_BUTTON_COUNT 8
@@ -71,6 +72,14 @@ int platform_display_resolutions(long *widths, long *heights, int maximum);
that fit the desktop, and display.window_size's; how many. None on
Android. */
int platform_window_sizes(long *widths, long *heights, int maximum);
+/* Settings > Audio's devices (port/linux/game/menu_tags.c): the names of
+the output (or, recording, input) devices SDL finds; how many. None on
+Android, which plays and records with the system's. */
+#define PLATFORM_AUDIO_DEVICE_NAME_SIZE 128
+int platform_audio_devices(int recording, char (*names)[PLATFORM_AUDIO_DEVICE_NAME_SIZE], int maximum);
+/* ... the device of a name (audio.output_device, audio.input_device):
+the system's default for "default", or one not found */
+SDL_AudioDeviceID platform_audio_device(int recording, const char *name);
void platform_video_drawable_size(int *width, int *height);
/* the window's mode and size and V-Sync, from config.toml as Settings has
just written it (the main thread's) */
diff --git a/port/linux/src/voice_audio.c b/port/linux/src/voice_audio.c
new file mode 100644
index 00000000..d9ec2d86
--- /dev/null
+++ b/port/linux/src/voice_audio.c
@@ -0,0 +1,548 @@
+/*
+VOICE_AUDIO.C
+
+Voice chat's sound (port/linux/game/network_voice.c decides who hears whom):
+the microphone, read in frames of 20 ms at 48 kHz, mono, and encoded with
+Opus (port/third_party/opus); and the other players' voices, decoded into a
+buffer each and mixed into the game's output (dsound_sdl.c's mix, at its
+48 kHz, stereo) at their gain and pan.
+
+The microphone is opened on its own thread (SDL asks Android for the
+permission to record there, and waits for the answer), and only while voice
+chat may send: network_voice.c closes it otherwise. A speaker plays once
+VOICE_PREBUFFER_SAMPLES are buffered (the network's jitter), and waits again
+when its buffer runs out; a frame lost in between is made up from the next
+one's redundancy (Opus's in-band FEC), or by concealment.
+*/
+
+#include "platform.h"
+#include "sdl_platform.h"
+#include "port_config.h"
+#include "voice_audio.h"
+#include "../../third_party/opus/include/opus.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#define VOICE_RATE 48000
+/* a speaker's buffer, and how much it holds before it plays */
+#define VOICE_RING_SAMPLES (VOICE_RATE / 2)
+#define VOICE_PREBUFFER_SAMPLES (3 * VOICE_FRAME_SAMPLES)
+/* how many speakers are decoded at once, and how long one unheard keeps
+its decoder (milliseconds) */
+#define VOICE_SLOTS 16
+#define VOICE_SLOT_IDLE_MS 5000
+/* frames made up for when packets are lost, at most; more lost, it starts
+over */
+#define VOICE_MAXIMUM_CONCEALED 3
+/* the microphone's frames kept before the oldest are dropped (a game that
+stalled must not talk late) */
+#define VOICE_MAXIMUM_BACKLOG (5 * VOICE_FRAME_SAMPLES)
+/* how often a microphone that would not open is tried again (ms) */
+#define VOICE_RETRY_MS 10000
+
+struct voice_slot
+{
+ BOOL used;
+ int speaker;
+ OpusDecoder *decoder;
+ /* the sequence next expected; FALSE: none yet */
+ BOOL sequenced;
+ unsigned short next_sequence;
+ Uint64 heard_ms;
+ /* the decoded samples, from read for count */
+ float ring[VOICE_RING_SAMPLES];
+ int read, count;
+ BOOL playing;
+ /* the gain and pan wanted, and those mixed last */
+ float gain, pan;
+ float left, right;
+ /* (debug.voice_test: when it was logged) */
+ Uint64 logged_ms;
+};
+
+static pthread_mutex_t voice_lock = PTHREAD_MUTEX_INITIALIZER;
+static struct voice_slot voice_slots[VOICE_SLOTS];
+static float voice_volume = 1.0f;
+
+/* the microphone (under voice_lock: the opening thread sets it) */
+static SDL_AudioStream *microphone;
+/* the device it was opened on (audio.input_device), and the one to open */
+static char microphone_device[PLATFORM_AUDIO_DEVICE_NAME_SIZE];
+static char microphone_opening_device[PLATFORM_AUDIO_DEVICE_NAME_SIZE];
+static BOOL microphone_wanted;
+static BOOL microphone_opening;
+static Uint64 microphone_failed_ms;
+static BOOL microphone_failed;
+static float capture[VOICE_FRAME_SAMPLES];
+static int capture_count;
+static OpusEncoder *encoder;
+static int encoder_bitrate;
+/* debug.voice_test (the automated tests): a tone instead of the microphone,
+a frame each 20 ms since the first; and each speaker's level logged once a
+second */
+static int voice_test = -1;
+static Uint64 voice_test_started_ms;
+static unsigned long voice_test_frames;
+static double voice_test_phase;
+
+/* ---------- the microphone */
+
+static void *microphone_open_thread(void *parameter)
+{
+ SDL_AudioSpec spec;
+ SDL_AudioStream *stream;
+
+ (void)parameter;
+ spec.format = SDL_AUDIO_F32;
+ spec.channels = 1;
+ spec.freq = VOICE_RATE;
+ stream = SDL_OpenAudioDeviceStream(platform_audio_device(TRUE, microphone_opening_device), &spec, NULL, NULL);
+ if (!stream && strcmp(microphone_opening_device, "default"))
+ stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, NULL, NULL);
+ if (stream)
+ SDL_ResumeAudioStreamDevice(stream);
+ else
+ platform_log("voice chat: no microphone (%s)", SDL_GetError());
+ pthread_mutex_lock(&voice_lock);
+ microphone_opening = FALSE;
+ if (stream && !microphone_wanted)
+ {
+ /* (closed again meanwhile) */
+ pthread_mutex_unlock(&voice_lock);
+ SDL_DestroyAudioStream(stream);
+ return NULL;
+ }
+ microphone = stream;
+ snprintf(microphone_device, sizeof(microphone_device), "%s", microphone_opening_device);
+ microphone_failed = stream == NULL;
+ microphone_failed_ms = SDL_GetTicks();
+ pthread_mutex_unlock(&voice_lock);
+ if (stream)
+ platform_log("voice chat: microphone open");
+ return NULL;
+}
+
+static BOOL voice_testing(void)
+{
+ if (voice_test < 0)
+ voice_test = config_boolean("debug.voice_test") ? 1 : 0;
+ return voice_test != 0;
+}
+
+int voice_audio_microphone(int open)
+{
+ SDL_AudioStream *closing = NULL;
+ BOOL start = FALSE;
+ BOOL ready;
+
+ if (voice_testing())
+ {
+ if (open && !voice_test_started_ms)
+ {
+ voice_test_started_ms = SDL_GetTicks();
+ voice_test_frames = 0;
+ }
+ if (!open)
+ voice_test_started_ms = 0;
+ return open;
+ }
+ if (!platform_sdl_initialize())
+ return FALSE;
+ {
+ /* (audio.input_device: Settings > Audio's, none on Android) */
+ const char *device = config_string("audio.input_device");
+
+ device = device && device[0] ? device : "default";
+ pthread_mutex_lock(&voice_lock);
+ /* (another device chosen: the one open closed, and the new opened) */
+ if (microphone && strcmp(microphone_device, device))
+ {
+ closing = microphone;
+ microphone = NULL;
+ microphone_failed = FALSE;
+ }
+ if (!microphone_opening)
+ snprintf(microphone_opening_device, sizeof(microphone_opening_device), "%s", device);
+ pthread_mutex_unlock(&voice_lock);
+ if (closing)
+ {
+ SDL_DestroyAudioStream(closing);
+ closing = NULL;
+ capture_count = 0;
+ }
+ }
+ pthread_mutex_lock(&voice_lock);
+ microphone_wanted = open;
+ if (open && !microphone && !microphone_opening &&
+ (!microphone_failed || SDL_GetTicks() - microphone_failed_ms >= VOICE_RETRY_MS))
+ {
+ microphone_opening = TRUE;
+ start = TRUE;
+ }
+ if (!open && microphone)
+ {
+ closing = microphone;
+ microphone = NULL;
+ microphone_failed = FALSE;
+ }
+ ready = microphone != NULL;
+ pthread_mutex_unlock(&voice_lock);
+ if (closing)
+ {
+ SDL_DestroyAudioStream(closing);
+ capture_count = 0;
+ platform_log("voice chat: microphone closed");
+ }
+ if (start)
+ {
+ pthread_t thread;
+
+ if (pthread_create(&thread, NULL, microphone_open_thread, NULL) == 0)
+ pthread_detach(thread);
+ else
+ {
+ pthread_mutex_lock(&voice_lock);
+ microphone_opening = FALSE;
+ pthread_mutex_unlock(&voice_lock);
+ }
+ }
+ return ready;
+}
+
+int voice_audio_read_frame(float *frame)
+{
+ SDL_AudioStream *stream;
+
+ if (voice_testing())
+ {
+ int index;
+
+ if (!voice_test_started_ms ||
+ (SDL_GetTicks() - voice_test_started_ms) / 20 <= voice_test_frames)
+ {
+ return FALSE;
+ }
+ voice_test_frames++;
+ for (index = 0; index < VOICE_FRAME_SAMPLES; index++)
+ {
+ frame[index] = 0.3f * (float)sin(voice_test_phase);
+ voice_test_phase += 2.0 * 3.14159265358979 * 440.0 / VOICE_RATE;
+ }
+ voice_test_phase = fmod(voice_test_phase, 2.0 * 3.14159265358979);
+ return TRUE;
+ }
+
+ pthread_mutex_lock(&voice_lock);
+ stream = microphone;
+ pthread_mutex_unlock(&voice_lock);
+ if (!stream)
+ return FALSE;
+ /* (a backlog dropped but for the latest frames) */
+ {
+ int available = SDL_GetAudioStreamAvailable(stream) / (int)sizeof(float);
+
+ while (available > VOICE_MAXIMUM_BACKLOG)
+ {
+ float discard[VOICE_FRAME_SAMPLES];
+ int take = available - VOICE_MAXIMUM_BACKLOG;
+
+ if (take > VOICE_FRAME_SAMPLES)
+ take = VOICE_FRAME_SAMPLES;
+ if (SDL_GetAudioStreamData(stream, discard, take * (int)sizeof(float)) <= 0)
+ break;
+ available -= take;
+ }
+ }
+ while (capture_count < VOICE_FRAME_SAMPLES)
+ {
+ int got = SDL_GetAudioStreamData(stream, capture + capture_count,
+ (VOICE_FRAME_SAMPLES - capture_count) * (int)sizeof(float));
+
+ if (got <= 0)
+ return FALSE;
+ capture_count += got / (int)sizeof(float);
+ }
+ memcpy(frame, capture, sizeof(capture));
+ capture_count = 0;
+ return TRUE;
+}
+
+float voice_audio_level(const float *frame)
+{
+ double sum = 0.0;
+ int index;
+
+ for (index = 0; index < VOICE_FRAME_SAMPLES; index++)
+ sum += (double)frame[index] * frame[index];
+ return (float)sqrt(sum / VOICE_FRAME_SAMPLES);
+}
+
+int voice_audio_encode(const float *frame, int bitrate, unsigned char *packet, int maximum)
+{
+ int length;
+
+ if (!encoder)
+ {
+ int error = 0;
+
+ encoder = opus_encoder_create(VOICE_RATE, 1, OPUS_APPLICATION_VOIP, &error);
+ if (!encoder)
+ {
+ platform_log("voice chat: no Opus encoder (%d)", error);
+ return 0;
+ }
+ /* (a lost frame made up from the next's redundancy; modest
+ complexity: Android runs the game emulated) */
+ opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(1));
+ opus_encoder_ctl(encoder, OPUS_SET_PACKET_LOSS_PERC(10));
+ opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(5));
+ opus_encoder_ctl(encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE));
+ encoder_bitrate = 0;
+ }
+ if (bitrate != encoder_bitrate)
+ {
+ opus_encoder_ctl(encoder, OPUS_SET_BITRATE(bitrate));
+ encoder_bitrate = bitrate;
+ }
+ length = opus_encode_float(encoder, frame, VOICE_FRAME_SAMPLES, packet, maximum);
+ return length > 0 ? length : 0;
+}
+
+/* ---------- the voices */
+
+/* (under voice_lock) the slot of a speaker, or a free one (or the longest
+unheard) taken for it */
+static struct voice_slot *voice_slot_for(int speaker, BOOL create)
+{
+ Uint64 now = SDL_GetTicks();
+ struct voice_slot *oldest = NULL;
+ int index;
+
+ for (index = 0; index < VOICE_SLOTS; index++)
+ {
+ if (voice_slots[index].used && voice_slots[index].speaker == speaker)
+ return &voice_slots[index];
+ }
+ if (!create)
+ return NULL;
+ for (index = 0; index < VOICE_SLOTS; index++)
+ {
+ struct voice_slot *slot = &voice_slots[index];
+
+ if (!slot->used)
+ {
+ oldest = slot;
+ break;
+ }
+ if (now - slot->heard_ms >= VOICE_SLOT_IDLE_MS && (!oldest || slot->heard_ms < oldest->heard_ms))
+ oldest = slot;
+ }
+ if (!oldest)
+ return NULL;
+ if (!oldest->decoder)
+ {
+ int error = 0;
+
+ oldest->decoder = opus_decoder_create(VOICE_RATE, 1, &error);
+ if (!oldest->decoder)
+ return NULL;
+ }
+ else
+ {
+ opus_decoder_ctl(oldest->decoder, OPUS_RESET_STATE);
+ }
+ oldest->used = TRUE;
+ oldest->speaker = speaker;
+ oldest->sequenced = FALSE;
+ oldest->read = oldest->count = 0;
+ oldest->playing = FALSE;
+ oldest->left = oldest->right = 0.0f;
+ return oldest;
+}
+
+/* (under voice_lock) samples into a slot's buffer, the oldest dropped when
+it is full */
+static void voice_slot_put(struct voice_slot *slot, const float *samples, int count)
+{
+ int index;
+
+ for (index = 0; index < count; index++)
+ {
+ if (slot->count == VOICE_RING_SAMPLES)
+ {
+ slot->read = (slot->read + 1) % VOICE_RING_SAMPLES;
+ slot->count--;
+ }
+ slot->ring[(slot->read + slot->count) % VOICE_RING_SAMPLES] = samples[index];
+ slot->count++;
+ }
+}
+
+void voice_audio_play(int speaker, unsigned short sequence, const unsigned char *packet, int length, float gain,
+ float pan)
+{
+ float samples[(VOICE_MAXIMUM_CONCEALED + 1) * VOICE_FRAME_SAMPLES];
+ struct voice_slot *slot;
+ OpusDecoder *decoder;
+ int lost = 0;
+ int total = 0;
+ int decoded;
+
+ if (length <= 0 || length > VOICE_MAXIMUM_PACKET)
+ return;
+ pthread_mutex_lock(&voice_lock);
+ slot = voice_slot_for(speaker, TRUE);
+ if (!slot)
+ {
+ pthread_mutex_unlock(&voice_lock);
+ return;
+ }
+ slot->heard_ms = SDL_GetTicks();
+ slot->gain = gain < 0.0f ? 0.0f : gain > 2.0f ? 2.0f : gain;
+ slot->pan = pan < -1.0f ? -1.0f : pan > 1.0f ? 1.0f : pan;
+ if (slot->sequenced)
+ {
+ unsigned short ahead = (unsigned short)(sequence - slot->next_sequence);
+
+ /* (late, or repeated: dropped) */
+ if (ahead >= 0x8000)
+ {
+ pthread_mutex_unlock(&voice_lock);
+ return;
+ }
+ /* (a few lost are made up for; more, it starts over) */
+ if (ahead <= VOICE_MAXIMUM_CONCEALED)
+ lost = ahead;
+ }
+ slot->sequenced = TRUE;
+ slot->next_sequence = (unsigned short)(sequence + 1);
+ decoder = slot->decoder;
+ pthread_mutex_unlock(&voice_lock);
+ /* (decoded outside the lock, which the mixer waits on: only this
+ thread uses a slot's decoder, and only it gives slots to speakers) */
+ while (lost > 0)
+ {
+ /* (the last lost from this packet's redundancy, those before it
+ concealed) */
+ BOOL redundancy = lost == 1;
+
+ decoded = opus_decode_float(decoder, redundancy ? packet : NULL, redundancy ? length : 0,
+ samples + total, VOICE_FRAME_SAMPLES, redundancy ? 1 : 0);
+ if (decoded > 0)
+ total += decoded;
+ lost--;
+ }
+ decoded = opus_decode_float(decoder, packet, length, samples + total, VOICE_FRAME_SAMPLES, 0);
+ if (decoded > 0)
+ total += decoded;
+ /* (the tests: what each speaker sounds like, once a second) */
+ if (voice_testing() && total >= VOICE_FRAME_SAMPLES)
+ {
+ Uint64 now = SDL_GetTicks();
+
+ if (now - slot->logged_ms >= 1000)
+ {
+ slot->logged_ms = now;
+ platform_log("voice test: speaker %d level %.3f gain %.2f pan %.2f bytes %d", speaker,
+ voice_audio_level(samples + total - VOICE_FRAME_SAMPLES), gain, pan, length);
+ }
+ }
+ pthread_mutex_lock(&voice_lock);
+ if (slot->used && slot->speaker == speaker)
+ voice_slot_put(slot, samples, total);
+ pthread_mutex_unlock(&voice_lock);
+}
+
+void voice_audio_forget(int speaker)
+{
+ struct voice_slot *slot;
+
+ pthread_mutex_lock(&voice_lock);
+ slot = voice_slot_for(speaker, FALSE);
+ if (slot)
+ {
+ slot->used = FALSE;
+ slot->count = 0;
+ slot->playing = FALSE;
+ }
+ pthread_mutex_unlock(&voice_lock);
+}
+
+void voice_audio_forget_all(void)
+{
+ int index;
+
+ pthread_mutex_lock(&voice_lock);
+ for (index = 0; index < VOICE_SLOTS; index++)
+ {
+ voice_slots[index].used = FALSE;
+ voice_slots[index].count = 0;
+ voice_slots[index].playing = FALSE;
+ }
+ pthread_mutex_unlock(&voice_lock);
+}
+
+int voice_audio_speaking(int speaker)
+{
+ struct voice_slot *slot;
+ BOOL speaking = FALSE;
+
+ pthread_mutex_lock(&voice_lock);
+ slot = voice_slot_for(speaker, FALSE);
+ if (slot)
+ speaking = slot->playing || slot->count >= VOICE_FRAME_SAMPLES;
+ pthread_mutex_unlock(&voice_lock);
+ return speaking;
+}
+
+void voice_audio_set_volume(float volume)
+{
+ voice_volume = volume < 0.0f ? 0.0f : volume > 2.0f ? 2.0f : volume;
+}
+
+void voice_audio_mix(float *output, unsigned long frames)
+{
+ int index;
+
+ pthread_mutex_lock(&voice_lock);
+ for (index = 0; index < VOICE_SLOTS; index++)
+ {
+ struct voice_slot *slot = &voice_slots[index];
+ float angle, left, right, step_left, step_right;
+ unsigned long frame, count;
+
+ if (!slot->used)
+ continue;
+ if (!slot->playing && slot->count >= VOICE_PREBUFFER_SAMPLES)
+ slot->playing = TRUE;
+ if (!slot->playing)
+ continue;
+ /* (equal power: both sides at the gain when centred) */
+ angle = (slot->pan + 1.0f) * 0.25f * 3.14159265f;
+ left = slot->gain * voice_volume * cosf(angle) * 1.41421356f;
+ right = slot->gain * voice_volume * sinf(angle) * 1.41421356f;
+ count = (unsigned long)slot->count < frames ? (unsigned long)slot->count : frames;
+ /* (the gains glide to their new values over the chunk) */
+ step_left = count ? (left - slot->left) / (float)count : 0.0f;
+ step_right = count ? (right - slot->right) / (float)count : 0.0f;
+ for (frame = 0; frame < count; frame++)
+ {
+ float sample = slot->ring[slot->read];
+
+ slot->left += step_left;
+ slot->right += step_right;
+ output[frame * 2] += sample * slot->left;
+ output[frame * 2 + 1] += sample * slot->right;
+ slot->read = (slot->read + 1) % VOICE_RING_SAMPLES;
+ }
+ slot->count -= (int)count;
+ /* (run out: it waits to buffer again) */
+ if (!slot->count)
+ slot->playing = FALSE;
+ }
+ pthread_mutex_unlock(&voice_lock);
+}
diff --git a/port/linux/src/voice_audio.h b/port/linux/src/voice_audio.h
new file mode 100644
index 00000000..ecc23328
--- /dev/null
+++ b/port/linux/src/voice_audio.h
@@ -0,0 +1,42 @@
+/*
+VOICE_AUDIO.H
+
+Voice chat's sound (voice_audio.c): the microphone, Opus, and the other
+players' voices mixed into the game's output. Called by
+port/linux/game/network_voice.c (the game's side, so its flags are int) and
+dsound_sdl.c (the mix).
+*/
+
+#ifndef VOICE_AUDIO_H
+#define VOICE_AUDIO_H
+
+/* a frame: 20 ms at 48 kHz, mono */
+#define VOICE_FRAME_SAMPLES 960
+/* the longest packet sent or taken (64 kbps is 160 bytes a frame) */
+#define VOICE_MAXIMUM_PACKET 400
+
+/* opens the microphone (on another thread: not at once) or closes it;
+whether it is open */
+int voice_audio_microphone(int open);
+/* the next 20 ms the microphone heard, if it has them all; 1 if so */
+int voice_audio_read_frame(float *frame);
+/* a frame's level (its root mean square, 1 at full scale) */
+float voice_audio_level(const float *frame);
+/* a frame encoded at the bitrate (bits a second); its packet's length, 0
+for none */
+int voice_audio_encode(const float *frame, int bitrate, unsigned char *packet, int maximum);
+/* a speaker's packet (speaker: any number naming them), to play at the
+gain (1: as spoken) and pan (-1 left to 1 right) */
+void voice_audio_play(int speaker, unsigned short sequence, const unsigned char *packet, int length, float gain,
+ float pan);
+/* a speaker no longer heard: their buffer and decoder freed; or all */
+void voice_audio_forget(int speaker);
+void voice_audio_forget_all(void);
+/* whether a speaker's voice is playing now */
+int voice_audio_speaking(int speaker);
+void voice_audio_set_volume(float volume);
+/* (dsound_sdl.c, on the audio thread) the voices added to the output
+(stereo, 48 kHz) */
+void voice_audio_mix(float *output, unsigned long frames);
+
+#endif
diff --git a/port/linux/src/xinput_sdl.c b/port/linux/src/xinput_sdl.c
index a2e87267..2e42ebb7 100644
--- a/port/linux/src/xinput_sdl.c
+++ b/port/linux/src/xinput_sdl.c
@@ -331,7 +331,7 @@ static const char *const binding_settings[NUMBER_OF_HALO_KEYBOARD_ACTIONS] =
"controls.move_forward", "controls.move_backward", "controls.strafe_left", "controls.strafe_right",
"controls.jump", "controls.crouch", "controls.fire", "controls.throw_grenade", "controls.melee",
"controls.reload", "controls.zoom", "controls.switch_weapon", "controls.switch_grenade", "controls.action",
- "controls.flashlight", "controls.scoreboard", "controls.pause",
+ "controls.flashlight", "controls.scoreboard", "controls.pause", "controls.push_to_talk",
};
static const struct
@@ -488,6 +488,22 @@ unsigned long halo_keyboard_actions(short controller_index)
return controller_index == 0 ? keyboard_actions_held : 0;
}
+int halo_push_to_talk_held(void)
+{
+ struct platform_input_state input;
+ int slot;
+
+ /* (the window losing the focus lets every key go: sdl_platform.c) */
+ bindings_read();
+ platform_input_read(&input, FALSE);
+ for (slot = 0; slot < MAXIMUM_BINDINGS; slot++)
+ {
+ if (input_held(&input, bindings[HALO_KEYBOARD_PUSH_TO_TALK][slot]))
+ return 1;
+ }
+ return 0;
+}
+
/* A scroll of the wheel switches weapons once: it holds Y for WHEEL_PRESS_MS
once the wheel has turned a notch, and the scroll lasts until the wheel has
been still for WHEEL_SCROLL_GAP_MS. One notch often arrives as several events
diff --git a/port/third_party/opus/AUTHORS b/port/third_party/opus/AUTHORS
new file mode 100644
index 00000000..b3d22a20
--- /dev/null
+++ b/port/third_party/opus/AUTHORS
@@ -0,0 +1,6 @@
+Jean-Marc Valin (jmvalin@jmvalin.ca)
+Koen Vos (koenvos74@gmail.com)
+Timothy Terriberry (tterribe@xiph.org)
+Karsten Vandborg Sorensen (karsten.vandborg.sorensen@skype.net)
+Soren Skak Jensen (ssjensen@gn.com)
+Gregory Maxwell (greg@xiph.org)
diff --git a/port/third_party/opus/COPYING b/port/third_party/opus/COPYING
new file mode 100644
index 00000000..75711467
--- /dev/null
+++ b/port/third_party/opus/COPYING
@@ -0,0 +1,44 @@
+Copyright 2001-2023 Xiph.Org, Skype Limited, Octasic,
+ Jean-Marc Valin, Timothy B. Terriberry,
+ CSIRO, Gregory Maxwell, Mark Borgerding,
+ Erik de Castro Lopo, Mozilla, Amazon
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+- Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+- Neither the name of Internet Society, IETF or IETF Trust, nor the
+names of specific contributors, may be used to endorse or promote
+products derived from this software without specific prior written
+permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Opus is subject to the royalty-free patent licenses which are
+specified at:
+
+Xiph.Org Foundation:
+https://datatracker.ietf.org/ipr/1524/
+
+Microsoft Corporation:
+https://datatracker.ietf.org/ipr/1914/
+
+Broadcom Corporation:
+https://datatracker.ietf.org/ipr/1526/
diff --git a/port/third_party/opus/PORT.md b/port/third_party/opus/PORT.md
new file mode 100644
index 00000000..81847a9f
--- /dev/null
+++ b/port/third_party/opus/PORT.md
@@ -0,0 +1,11 @@
+# Opus
+
+[Opus](https://opus-codec.org) 1.5.2 (`opus-1.5.2.tar.gz`, SHA-256
+`65c1d2f78b9f2fb20082c38cbe47c951ad5839345876e46941612ee87f9a7ce1`), for
+voice chat (`port/linux/game/network_voice.c`). BSD licence: `COPYING`.
+
+Only the library is kept: `include/`, `src/`, `celt/` and `silk/` with
+`silk/float/` (the floating-point build), without the multistream and
+projection API, the demos, the tests and the SIMD and DNN code. `sources.txt`
+lists the files the builds compile (`tools/linux_build.py`), as plain C with
+`OPUS_BUILD`, `VAR_ARRAYS`, `HAVE_LRINTF` and `HAVE_LRINT`.
diff --git a/port/third_party/opus/README b/port/third_party/opus/README
new file mode 100644
index 00000000..bcf2376d
--- /dev/null
+++ b/port/third_party/opus/README
@@ -0,0 +1,189 @@
+== Opus audio codec ==
+
+Opus is a codec for interactive speech and audio transmission over the Internet.
+
+ Opus can handle a wide range of interactive audio applications, including
+Voice over IP, videoconferencing, in-game chat, and even remote live music
+performances. It can scale from low bit-rate narrowband speech to very high
+quality stereo music.
+
+ Opus, when coupled with an appropriate container format, is also suitable
+for non-realtime stored-file applications such as music distribution, game
+soundtracks, portable music players, jukeboxes, and other applications that
+have historically used high latency formats such as MP3, AAC, or Vorbis.
+
+ Opus is specified by IETF RFC 6716:
+ https://tools.ietf.org/html/rfc6716
+
+ The Opus format and this implementation of it are subject to the royalty-
+free patent and copyright licenses specified in the file COPYING.
+
+This package implements a shared library for encoding and decoding raw Opus
+bitstreams. Raw Opus bitstreams should be used over RTP according to
+ https://tools.ietf.org/html/rfc7587
+
+The package also includes a number of test tools used for testing the
+correct operation of the library. The bitstreams read/written by these
+tools should not be used for Opus file distribution: They include
+additional debugging data and cannot support seeking.
+
+Opus stored in files should use the Ogg encapsulation for Opus which is
+described at:
+ https://tools.ietf.org/html/rfc7845
+
+An opus-tools package is available which provides encoding and decoding of
+Ogg encapsulated Opus files and includes a number of useful features.
+
+Opus-tools can be found at:
+ https://gitlab.xiph.org/xiph/opus-tools.git
+or on the main Opus website:
+ https://opus-codec.org/
+
+== Deep Learning and Opus ==
+
+Lossy networks continue to be a challenge for real-time communications.
+While the original implementation of Opus provides an excellent packet loss
+concealment mechanism, the team has continued to advance the methodology used
+to improve audio quality in challenge network environments.
+
+In Opus 1.5, we added a deep learning based redundancy encoder that enhances
+audio in lossy networks by embedding one second of recovery data in the padding
+data of each packet. The underlying algorithm behind encoding and decoding the
+recovery data is called the deep redundancy (DRED) algorithm. By leveraging
+the padding data within the packet, Opus 1.5 is fully backward compatible with
+prior revisions of Opus. Please see the README under the "dnn" subdirectory to
+understand DRED.
+
+DRED was developed by a team that Amazon Web Services initially sponsored,
+who open-sourced the implementation as well as began the
+standardization process at the IETF:
+ https://datatracker.ietf.org/doc/draft-ietf-mlcodec-opus-extension/
+The license behind Opus or the intellectual property position of Opus does
+not change with Opus 1.5.
+
+== Compiling libopus ==
+
+To build from a distribution tarball, you only need to do the following:
+
+ % ./configure
+ % make
+
+To build from the git repository, the following steps are necessary:
+
+0) Set up a development environment:
+
+On an Ubuntu or Debian family Linux distribution:
+
+ % sudo apt-get install git autoconf automake libtool gcc make
+
+On a Fedora/Redhat based Linux:
+
+ % sudo dnf install git autoconf automake libtool gcc make
+
+Or for older Redhat/Centos Linux releases:
+
+ % sudo yum install git autoconf automake libtool gcc make
+
+On Apple macOS, install Xcode and brew.sh, then in the Terminal enter:
+
+ % brew install autoconf automake libtool
+
+1) Clone the repository:
+
+ % git clone https://gitlab.xiph.org/xiph/opus.git
+ % cd opus
+
+2) Compiling the source
+
+ % ./autogen.sh
+ % ./configure
+ % make
+
+On x86, it's a good idea to use a -march= option that allows the use of AVX2.
+
+3) Install the codec libraries (optional)
+
+ % sudo make install
+
+Once you have compiled the codec, there will be a opus_demo executable
+in the top directory.
+
+Usage: opus_demo [-e]
+ [options]