Compare commits

...
Author SHA1 Message Date
Stefan Maier 9304daf2e8 Merge branch 'main' into test/bots-voice 2026-10-04 23:00:24 +02:00
Stefan Maier 8e8713681d Merge remote-tracking branch 'origin/fix/multiview-desktop-compile' 2026-10-04 22:59:55 +02:00
Stefan Maier 0a62ad4610 Merge branch 'main' into test/bots-voice
# Conflicts:
#	port/android/app/src/main/java/com/halo/decomp/LauncherActivity.java
#	tools/android_build.py
2026-10-04 22:05:55 +02:00
Stefan Maier 42dc1f546f Merge branch 'main' into test/bots-voice 2026-10-04 17:51:25 +02:00
Stefan Maier 79a6482c16 Merge branch 'merge/upstream-2026-10-04' into test/bots-voice 2026-10-04 13:47:58 +02:00
Stefan Maier bfd92ec17d Merge branch 'merge/upstream-2026-10-04' into test/bots-voice
# Conflicts:
#	source/interface/ui_widget.c
2026-10-04 13:47:56 +02:00
Stefan Maier 852ee322cc Merge branch 'feature/vr-hud-fixes' into test/bots-voice 2026-10-04 13:25:17 +02:00
Stefan Maier f0d4fb2608 Merge branch 'feature/bots-vehicles' into test/bots-voice 2026-10-04 13:25:17 +02:00
Stefan Maier 736c217347 Merge branch 'feature/vr-hud-fixes' into test/bots-voice 2026-10-04 12:00:43 +02:00
Stefan Maier 30edb22f36 Merge branch 'feature/vr-menu-lists' into test/bots-voice 2026-10-04 11:41:42 +02:00
Stefan Maier abc1c12f1e Merge branch 'feature/bots-vehicles' into test/bots-voice 2026-10-04 11:41:41 +02:00
Stefan Maier 3f02527c68 Merge branch 'feature/bots-vehicles' into test/bots-voice 2026-10-04 11:23:51 +02:00
Stefan Maier ceecb039e3 Merge branch 'feature/bots-vehicles' into test/bots-voice 2026-10-03 16:49:01 +02:00
Stefan Maier a34685574f Merge branch 'feature/vr-options' into test/bots-voice 2026-10-03 15:41:58 +02:00
Stefan Maier eb83e9d6d1 Merge branch 'feature/vr-options' into test/bots-voice 2026-10-03 15:38:13 +02:00
Stefan Maier 144063871a Merge branch 'main' into test/bots-voice 2026-10-03 15:38:13 +02:00
Stefan Maier 7165d40f90 Merge branch 'merge/upstream-v11' into test/bots-voice 2026-10-03 15:11:17 +02:00
Stefan Maier 65d81ad1ba Merge branch 'feature/warthog-wheel' into test/bots-voice 2026-10-03 13:50:43 +02:00
Stefan Maier d356c0abab Merge branch 'feature/warthog-wheel' into test/bots-voice 2026-10-03 13:00:27 +02:00
Stefan Maier 1536c30870 Merge branch 'feature/vr-menu-pointer' into test/bots-voice 2026-10-03 12:47:53 +02:00
Stefan Maier 458b4642b6 Merge branch 'feature/vr-menu-pointer' into test/bots-voice 2026-10-03 12:42:59 +02:00
Stefan Maier 2af2702bfa Merge branch 'feature/vr-menu-pointer' into test/bots-voice 2026-10-03 12:30:21 +02:00
Stefan Maier 7a1c5e6239 Merge branch 'feature/vr-menu-pointer' into test/bots-voice 2026-10-03 12:27:30 +02:00
Stefan Maier a9f3ce2647 Merge branch 'feature/vr-menu-pointer' into test/bots-voice 2026-10-03 12:23:29 +02:00
Stefan Maier 1cebe431d1 Merge branch 'feature/vr-menu-pointer' into test/bots-voice
# Conflicts:
#	source/interface/ui_widget.c
2026-10-03 11:56:21 +02:00
Stefan Maier ec2ab73acc Merge branch 'main' into test/bots-voice 2026-10-03 11:56:06 +02:00
Stefan Maier 2f5fa771f5 Merge branches 'feature/vr-names' and 'feature/bots-team' into test/bots-voice 2026-10-03 10:55:30 +02:00
Stefan Maier 3aa88244fc Merge branches 'feature/vr-names' and 'feature/bots-team' into test/bots-voice 2026-10-03 10:41:10 +02:00
Stefan Maier a4e1bdb80d Merge branch 'feature/bots-modes' into test/bots-voice 2026-10-03 09:34:00 +02:00
Stefan Maier ceb0260389 Merge branches 'feature/bots-modes', 'merge/upstream-2026-10-03' and 'feature/vr-scope' into test/bots-voice 2026-10-03 08:27:26 +02:00
Stefan Maier bdfdef9bd4 Merge branch 'merge/upstream-2026-10-03' into test/bots-voice
# Conflicts:
#	port/linux/game/network_distributed.h
2026-10-03 07:40:34 +02:00
Stefan Maier f02317ca25 Merge branch 'feature/bots-modes' into test/bots-voice 2026-10-02 21:22:33 +02:00
Stefan Maier d7fa443718 Merge branch 'feature/bots-items' into test/bots-voice 2026-10-02 20:41:50 +02:00
Stefan Maier 4414d7606d Merge branch 'feature/vr-scope' into test/bots-voice 2026-10-02 18:14:36 +02:00
Stefan Maier 824f79aa70 Merge branch 'feature/vr-scope' into test/bots-voice 2026-10-02 18:08:07 +02:00
Stefan Maier 25a947faac Merge bots host options into the test build 2026-10-02 18:08:07 +02:00
Stefan MaierandClaude Opus 5.5 bf545439b4 Keep the voice and bots settings out of the network table
The settings file is written in the table's order, so voice.* and bots.*
between network.* entries wrote [network] twice, and the whole file was
then refused (defaults used). They now follow the network settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:52:12 +02:00
Stefan Maier 201f69a35a Merge voice chat into the bots test build 2026-10-02 17:06:16 +02:00
Stefan MaierandClaude Opus 5.5 96a75df218 Switch the voice chat and the microphone in the online games panel
Two rows under 'Enter a join code' toggle voice.enabled and
voice.microphone with A, written to config.toml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:53:04 +02:00
Stefan MaierandClaude Opus 5.5 1a732f577f Voice chat in multiplayer
The microphone (SDL3 recording with Android's voice communication preset:
echo cancelling, noise suppression) is encoded with Opus (VOIP, 16 kbit/s,
20 ms frames) while its level is over voice.threshold, and sent every
tick in a new distributed message (_distributed_message_voice): clients
to the host, which plays it and passes it on; the host its own to every
client. Each speaker is decoded into a jitter buffer and mixed into a
stream of its own, teammates over the radio, everyone else by distance
(20 world units) and from their side of where the listener looks.
Devices open on a thread of their own so the game never waits; the
launcher asks for the microphone once and tells the game whether it may
use it. Opus v1.5.2 is fetched and built statically into the host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:43:36 +02:00
13 changed files with 1107 additions and 15 deletions

No files matched your search

@@ -19,6 +19,9 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<!-- the voice chat in multiplayer (host_voice.c) -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- installing a new version of the game (Updater) -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@@ -1,7 +1,10 @@
package com.halo.decomp;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
@@ -35,6 +38,7 @@ import java.nio.channels.FileChannel;
*/
public class LauncherActivity extends Activity {
private static final int PICK_IMAGE = 1;
private static final int ASK_MICROPHONE = 2;
private static final String SHARED_DATA_ROOT = "/home/steamos/Documents/HaloCE";
private File dataRoot;
@@ -60,12 +64,37 @@ public class LauncherActivity extends Activity {
passOnHardwareId();
passOnInvite(getIntent());
if (haveData()) {
startGame();
if (!askForMicrophone())
startGame();
return;
}
buildInterface();
}
/**
* The voice chat's microphone (port/android/host/host_voice.c), asked
* for here, once, on this flat panel, before the game starts: in the
* game, the question would stop it. Returns true if it asked (the game
* starts on the answer).
*/
private boolean askForMicrophone() {
SharedPreferences preferences = getSharedPreferences("launcher", MODE_PRIVATE);
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
|| preferences.getBoolean("asked_microphone", false))
return false;
preferences.edit().putBoolean("asked_microphone", true).apply();
requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO }, ASK_MICROPHONE);
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
super.onRequestPermissionsResult(requestCode, permissions, results);
if (requestCode == ASK_MICROPHONE)
startGame();
}
/**
* An internet play invite link the app was opened with: the game
* (port/linux/src/p2p.c) picks it up from join_link.txt, whether it is
@@ -118,6 +147,23 @@ public class LauncherActivity extends Activity {
partial.delete();
}
/**
* Whether the microphone may be used, for the game (port/linux/src/
* voice.c), which opens it only then: opening it otherwise would have
* Android ask in the middle of the game.
*/
private void passOnMicrophonePermission() {
if (dataRoot == null)
return;
boolean granted =
checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED;
try (OutputStream out = new FileOutputStream(new File(dataRoot, "microphone_permission.txt"))) {
out.write((granted ? "1" : "0").getBytes("UTF-8"));
} catch (java.io.IOException e) {
// the game then does without the microphone
}
}
/** The game data, or the marker file of the VR self-test (host_xr.c). */
private boolean haveData() {
return dataRoot != null && (new File(dataRoot, "maps/ui.map").isFile() ||
@@ -125,6 +171,7 @@ public class LauncherActivity extends Activity {
}
private void startGame() {
passOnMicrophonePermission();
startActivity(new Intent(this, HaloActivity.class));
finish();
}
+15
View File
@@ -134,6 +134,21 @@ int host_xr_copy_layer_array(int layer, unsigned int texture, int array_layer);
/* darkens the eye images from their edges, strength 0..1 (0: off) */
void host_xr_set_vignette(float strength);
/* ---------- the voice chat (host_voice.c) */
/* opens the voices' playback, and the microphone if asked (else closes
it): their volume, and the level (0..1 of full scale) the microphone's
sound must reach to be sent */
int host_voice_start(int microphone, float volume, float threshold);
void host_voice_stop(void);
/* the microphone's speech since asked: Opus packets, each after a byte of
its length; returns the bytes */
int host_voice_take_packets(void *buffer, int size);
/* the microphone's level (0..1000) */
int host_voice_level(void);
/* a speaker's packet (sequence numbered), played at these gains */
void host_voice_play(int id, int sequence, const void *data, int length, float gain_left, float gain_right);
/* ---------- Android */
/* the storage directories the port uses, copied into buffer */
+494
View File
@@ -0,0 +1,494 @@
/*
HOST_VOICE.C
The voice chat's sound (the VR port's; port/linux/game/network_voice.c
sends and receives it): the microphone, Opus, and the voices played. It
runs here, natively, where SDL's audio threads may call it.
- The microphone: SDL's default recording device, mono at 16 kHz, with
Android's voice communication preset (its echo cancelling and noise
suppression: SDL_HINT_ANDROID_AAUDIO_INPUT_PRESET). Every 20 ms of it
that is loud enough to be speech (or follows speech closely, VAD_HANGOVER)
is encoded (Opus, VOIP, 16 kbit/s) for the guest to send.
- The voices: each speaker's packets, decoded as they come (a gap in their
numbers concealed by Opus's packet loss concealment), wait in the
speaker's buffer until JITTER_START of them is there, and are mixed, each
at the gains its latest packet was given (left and right: where the
speaker is), into a stream of their own.
The devices are opened and closed on a thread of their own (voice_worker):
opening a microphone can take a long while (an emulator's took a minute),
which the game, whose clock the other machines follow, must not wait for.
*/
#include "host.h"
#include <SDL3/SDL.h>
#include <opus.h>
#include <pthread.h>
#include <string.h>
enum
{
VOICE_RATE = 16000,
FRAME_SAMPLES = VOICE_RATE / 50,
MAXIMUM_PACKET = 200,
MAXIMUM_SPEAKERS = 32,
/* a speaker's buffer: two seconds */
SPEAKER_BUFFER = VOICE_RATE * 2,
/* played from once this much is there, after silence */
JITTER_START = FRAME_SAMPLES * 3,
/* more than this behind: the oldest dropped */
JITTER_MAXIMUM = FRAME_SAMPLES * 15,
/* frames sent on after the voice drops below the threshold */
VAD_HANGOVER = 15,
BITRATE = 16000,
};
struct speaker
{
int used;
int id;
OpusDecoder *decoder;
int has_sequence;
unsigned short sequence;
short buffer[SPEAKER_BUFFER];
int read;
int count;
int playing;
float gain_left;
float gain_right;
unsigned long long heard_ticks;
};
static struct
{
SDL_Mutex *lock;
/* the devices wanted, which the worker opens and closes to match
(under worker_lock); the recording stream is read under it too */
pthread_mutex_t worker_lock;
pthread_cond_t worker_wake;
int worker_started;
int want_playback;
int want_recording;
/* the microphone */
SDL_AudioStream *recording;
OpusEncoder *encoder;
short frame[FRAME_SAMPLES];
int frame_count;
int hangover;
float threshold;
int level;
/* the voices */
SDL_AudioStream *playback;
struct speaker speakers[MAXIMUM_SPEAKERS];
float volume;
} voice;
static void voice_lock(void)
{
if (!voice.lock)
voice.lock = SDL_CreateMutex();
SDL_LockMutex(voice.lock);
}
static void voice_unlock(void)
{
SDL_UnlockMutex(voice.lock);
}
/* the voices mixed, as SDL asks for them (on its audio thread) */
static void SDLCALL voice_playback_callback(void *userdata, SDL_AudioStream *stream, int additional, int total)
{
short mixed[2 * 512];
(void)userdata;
(void)total;
while (additional > 0)
{
int frames = additional / 4;
int frame;
int index;
if (frames > 512)
frames = 512;
if (frames <= 0)
frames = 1;
memset(mixed, 0, sizeof(mixed));
voice_lock();
for (index = 0; index < MAXIMUM_SPEAKERS; index++)
{
struct speaker *speaker = &voice.speakers[index];
if (!speaker->used)
continue;
if (!speaker->playing && speaker->count >= JITTER_START)
speaker->playing = 1;
if (!speaker->playing)
continue;
for (frame = 0; frame < frames && speaker->count > 0; frame++)
{
float sample = (float)speaker->buffer[speaker->read] * voice.volume;
int left = mixed[2 * frame] + (int)(sample * speaker->gain_left);
int right = mixed[2 * frame + 1] + (int)(sample * speaker->gain_right);
mixed[2 * frame] = (short)(left > 32767 ? 32767 : left < -32768 ? -32768 : left);
mixed[2 * frame + 1] = (short)(right > 32767 ? 32767 : right < -32768 ? -32768 : right);
speaker->read = (speaker->read + 1) % SPEAKER_BUFFER;
speaker->count--;
}
/* (run dry: wait for enough again) */
if (!speaker->count)
speaker->playing = 0;
}
voice_unlock();
SDL_PutAudioStreamData(stream, mixed, frames * 4);
additional -= frames * 4;
}
}
static int voice_open_playback(void)
{
SDL_AudioSpec spec;
if (voice.playback)
return 1;
spec.format = SDL_AUDIO_S16;
spec.channels = 2;
spec.freq = VOICE_RATE;
voice.playback = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, voice_playback_callback,
NULL);
if (!voice.playback)
{
host_logf(HOST_LOG_WARN, "voice: cannot open the playback stream: %s", SDL_GetError());
return 0;
}
SDL_ResumeAudioStreamDevice(voice.playback);
return 1;
}
static SDL_AudioStream *voice_open_recording(void)
{
SDL_AudioSpec spec;
SDL_AudioStream *recording;
int error;
if (!voice.encoder)
{
voice.encoder = opus_encoder_create(VOICE_RATE, 1, OPUS_APPLICATION_VOIP, &error);
if (!voice.encoder)
{
host_logf(HOST_LOG_WARN, "voice: cannot make the encoder (%d)", error);
return NULL;
}
opus_encoder_ctl(voice.encoder, OPUS_SET_BITRATE(BITRATE));
opus_encoder_ctl(voice.encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE));
opus_encoder_ctl(voice.encoder, OPUS_SET_INBAND_FEC(1));
opus_encoder_ctl(voice.encoder, OPUS_SET_PACKET_LOSS_PERC(10));
}
/* Android's voice communication preset: echo cancelling, noise
suppression (AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION) */
SDL_SetHint(SDL_HINT_ANDROID_AAUDIO_INPUT_PRESET, "7");
spec.format = SDL_AUDIO_S16;
spec.channels = 1;
spec.freq = VOICE_RATE;
recording = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, NULL, NULL);
if (!recording)
{
host_logf(HOST_LOG_WARN, "voice: cannot open the microphone: %s", SDL_GetError());
return NULL;
}
SDL_ResumeAudioStreamDevice(recording);
host_logf(HOST_LOG_INFO, "voice: the microphone is open");
return recording;
}
static void voice_close_recording(void)
{
SDL_AudioStream *recording;
pthread_mutex_lock(&voice.worker_lock);
recording = voice.recording;
voice.recording = NULL;
voice.level = 0;
pthread_mutex_unlock(&voice.worker_lock);
if (!recording)
return;
SDL_DestroyAudioStream(recording);
host_logf(HOST_LOG_INFO, "voice: the microphone is closed");
}
static void voice_close_playback(void)
{
SDL_AudioStream *playback = voice.playback;
voice.playback = NULL;
if (playback)
SDL_DestroyAudioStream(playback);
}
/* opens and closes the devices as wanted, for as long as the app runs */
static void *voice_worker(void *unused)
{
(void)unused;
pthread_mutex_lock(&voice.worker_lock);
for (;;)
{
int want_playback = voice.want_playback;
int want_recording = voice.want_recording;
int have_playback = voice.playback != NULL;
int have_recording = voice.recording != NULL;
if (want_playback == have_playback && want_recording == have_recording)
{
pthread_cond_wait(&voice.worker_wake, &voice.worker_lock);
continue;
}
pthread_mutex_unlock(&voice.worker_lock);
if (want_recording && !have_recording)
{
/* (opened outside the lock, which the game reads it under) */
SDL_AudioStream *recording = voice_open_recording();
pthread_mutex_lock(&voice.worker_lock);
voice.recording = recording;
pthread_mutex_unlock(&voice.worker_lock);
/* (a microphone that will not open is not tried again until
wanted anew) */
if (!recording)
{
pthread_mutex_lock(&voice.worker_lock);
voice.want_recording = 0;
pthread_mutex_unlock(&voice.worker_lock);
}
}
else if (!want_recording && have_recording)
{
voice_close_recording();
}
if (want_playback && !have_playback)
{
if (!voice_open_playback())
{
pthread_mutex_lock(&voice.worker_lock);
voice.want_playback = 0;
pthread_mutex_unlock(&voice.worker_lock);
}
}
else if (!want_playback && have_playback)
{
voice_close_playback();
}
pthread_mutex_lock(&voice.worker_lock);
}
return NULL;
}
static void voice_want(int playback, int recording)
{
if (!voice.worker_started)
{
voice.worker_started = 1;
pthread_mutex_init(&voice.worker_lock, NULL);
pthread_cond_init(&voice.worker_wake, NULL);
if (host_native_thread_create(voice_worker, NULL, 256 * 1024) != 0)
{
host_logf(HOST_LOG_WARN, "voice: cannot start the device thread");
return;
}
}
pthread_mutex_lock(&voice.worker_lock);
voice.want_playback = playback;
voice.want_recording = recording;
pthread_cond_signal(&voice.worker_wake);
pthread_mutex_unlock(&voice.worker_lock);
}
/* ---------- the guest's side */
int host_voice_start(int microphone, float volume, float threshold)
{
voice_lock();
voice.volume = volume < 0.0f ? 0.0f : volume > 4.0f ? 4.0f : volume;
voice.threshold = threshold;
voice_unlock();
voice_want(1, microphone);
return 1;
}
void host_voice_stop(void)
{
int index;
voice_want(0, 0);
voice_lock();
for (index = 0; index < MAXIMUM_SPEAKERS; index++)
{
if (voice.speakers[index].decoder)
opus_decoder_destroy(voice.speakers[index].decoder);
}
memset(voice.speakers, 0, sizeof(voice.speakers));
voice_unlock();
}
int host_voice_take_packets(void *buffer, int size)
{
unsigned char *out = buffer;
int used = 0;
if (!voice.worker_started)
return 0;
pthread_mutex_lock(&voice.worker_lock);
if (!voice.recording)
{
voice.frame_count = 0;
voice.hangover = 0;
pthread_mutex_unlock(&voice.worker_lock);
return 0;
}
for (;;)
{
int wanted = (FRAME_SAMPLES - voice.frame_count) * (int)sizeof(short);
int got = SDL_GetAudioStreamData(voice.recording, voice.frame + voice.frame_count, wanted);
double energy = 0.0;
int index;
if (got <= 0)
break;
voice.frame_count += got / (int)sizeof(short);
if (voice.frame_count < FRAME_SAMPLES)
break;
voice.frame_count = 0;
for (index = 0; index < FRAME_SAMPLES; index++)
energy += (double)voice.frame[index] * voice.frame[index];
energy = SDL_sqrt(energy / FRAME_SAMPLES) / 32768.0;
voice.level = (int)(energy * 1000.0);
if (energy >= voice.threshold)
voice.hangover = VAD_HANGOVER;
else if (voice.hangover > 0)
voice.hangover--;
else
continue;
if (used + 1 + MAXIMUM_PACKET > size)
continue;
{
int length = opus_encode(voice.encoder, voice.frame, FRAME_SAMPLES, out + used + 1, MAXIMUM_PACKET);
if (length > 0 && length < 256)
{
out[used] = (unsigned char)length;
used += 1 + length;
}
}
}
pthread_mutex_unlock(&voice.worker_lock);
return used;
}
int host_voice_level(void)
{
return voice.level;
}
void host_voice_play(int id, int sequence, const void *data, int length, float gain_left, float gain_right)
{
struct speaker *speaker = NULL;
struct speaker *oldest = NULL;
short decoded[FRAME_SAMPLES * 6];
int frames[4];
int frame_count = 0;
int index;
if (length <= 0 || length > MAXIMUM_PACKET)
return;
voice_lock();
for (index = 0; index < MAXIMUM_SPEAKERS && !speaker; index++)
{
if (voice.speakers[index].used && voice.speakers[index].id == id)
speaker = &voice.speakers[index];
}
if (!speaker)
{
for (index = 0; index < MAXIMUM_SPEAKERS && !speaker; index++)
{
if (!voice.speakers[index].used)
speaker = &voice.speakers[index];
else if (!oldest || voice.speakers[index].heard_ticks < oldest->heard_ticks)
oldest = &voice.speakers[index];
}
if (!speaker)
speaker = oldest;
if (!speaker->decoder)
{
int error;
speaker->decoder = opus_decoder_create(VOICE_RATE, 1, &error);
if (!speaker->decoder)
{
voice_unlock();
return;
}
}
else
{
opus_decoder_ctl(speaker->decoder, OPUS_RESET_STATE);
}
speaker->used = 1;
speaker->id = id;
speaker->has_sequence = 0;
speaker->read = speaker->count = 0;
speaker->playing = 0;
}
speaker->heard_ticks = SDL_GetTicks();
speaker->gain_left = gain_left;
speaker->gain_right = gain_right;
{
unsigned short number = (unsigned short)sequence;
if (speaker->has_sequence)
{
short gap = (short)(number - speaker->sequence);
/* (late, or again: dropped) */
if (gap <= 0)
{
voice_unlock();
return;
}
/* (lost ones: concealed, a few) */
if (gap > 1 && gap <= 4 && speaker->count > 0)
{
int missing;
for (missing = 1; missing < gap && frame_count < 3; missing++)
{
frames[frame_count] = opus_decode(speaker->decoder, NULL, 0, decoded + frame_count * FRAME_SAMPLES,
FRAME_SAMPLES, 0);
frame_count++;
}
}
}
speaker->has_sequence = 1;
speaker->sequence = number;
}
frames[frame_count] = opus_decode(speaker->decoder, data, length, decoded + frame_count * FRAME_SAMPLES,
FRAME_SAMPLES * 2, 0);
frame_count++;
for (index = 0; index < frame_count; index++)
{
int sample;
for (sample = 0; sample < frames[index] && sample < FRAME_SAMPLES * 2; sample++)
{
if (speaker->count >= JITTER_MAXIMUM)
{
speaker->read = (speaker->read + 1) % SPEAKER_BUFFER;
speaker->count--;
}
speaker->buffer[(speaker->read + speaker->count) % SPEAKER_BUFFER] =
decoded[index * FRAME_SAMPLES + sample];
speaker->count++;
}
}
voice_unlock();
}
+5
View File
@@ -60,6 +60,11 @@ host_xr_copy_layer_region
host_xr_end_frame
host_xr_haptic
host_xr_set_vignette
host_voice_start
host_voice_stop
host_voice_take_packets
host_voice_level
host_voice_play
host_xr_copy_layer_array
host_android_path
+14
View File
@@ -3136,6 +3136,7 @@ void network_distributed_new_game(
network_damage_new_game();
network_vr_new_game();
bots_new_game();
network_voice_new_game();
}
/* after each tick (game_time.c) */
@@ -3181,6 +3182,7 @@ void network_distributed_tick(
distributed_host_send_players();
network_vr_tick();
bots_tick();
network_voice_tick();
distributed_send_pickups();
if (game_time_get() % GAME_STATE_INTERVAL_TICKS == 0)
distributed_send_game_state(NONE);
@@ -3190,6 +3192,7 @@ void network_distributed_tick(
distributed_note_own_positions();
distributed_client_send_inputs();
network_vr_tick();
network_voice_tick();
distributed_client_send_predictions();
network_objects_client_tick();
network_damage_client_tick();
@@ -3667,6 +3670,7 @@ void network_distributed_handle_message(
case _distributed_message_client_ready: entry_size = 0; break;
case _distributed_message_client_identity: entry_size = sizeof(struct distributed_client_identity); break;
case _distributed_message_vr_poses: entry_size = network_vr_entry_size(); break;
case _distributed_message_voice: entry_size = network_voice_entry_size(); break;
case _distributed_message_damage_events:
case _distributed_message_hit_reports: entry_size = network_damage_entry_size(header.type); break;
default: entry_size = network_objects_entry_size(header.type); break;
@@ -3680,6 +3684,16 @@ void network_distributed_handle_message(
/* (each kind from the host, or from a client; a VR player's hands
both ways) */
if (header.type == _distributed_message_voice)
{
if (machine_index == NONE ? game_connection() != _game_connection_network_client :
game_connection() != _game_connection_network_server)
{
return;
}
network_voice_handle(machine_index, message, size);
return;
}
if (header.type == _distributed_message_vr_poses)
{
if (machine_index == NONE ? game_connection() != _game_connection_network_client :
+10
View File
@@ -72,6 +72,10 @@ enum
/* a VR player's hands, every tick: a client's own to the host, the
host's of the others to each client (unreliable; network_vr.c) */
_distributed_message_vr_poses = 240,
/* a player's speech, every tick while they speak: a client's own to
the host, the host's and the other clients' to each client (unreliable;
network_voice.c) */
_distributed_message_voice,
NUMBER_OF_DISTRIBUTED_MESSAGES
};
@@ -252,4 +256,10 @@ void network_vr_handle_poses(long machine_index, void const *entries, short coun
void bots_new_game(void);
void bots_tick(void);
/* the voice chat (network_voice.c) */
void network_voice_new_game(void);
void network_voice_tick(void);
word network_voice_entry_size(void);
void network_voice_handle(long machine_index, void const *message, word size);
#endif // __NETWORK_DISTRIBUTED_H
+326
View File
@@ -0,0 +1,326 @@
/*
NETWORK_VOICE.C
The voice chat (the VR port's): an extension of the distributed netcode
(port/linux/NETCODE.md, network_distributed.c), as the VR players' hands
are (network_vr.c).
- Every tick, a machine sends what its microphone heard of its player's
speech (halo_voice.h: Opus packets of 20 ms, only while they speak) in
_distributed_message_voice (unreliable): a client to the host, which
plays it and passes it on to its other clients; the host its own to
every client.
- A machine plays each speaker's voice from where they are: a teammate
(in a game of teams) over the radio, as loud anywhere and only a little
to the side they are on; anyone else as near as they are (no further off
than VOICE_RANGE), from their side. A speaker with no unit (dead) is
heard over the radio by their team, and not by the rest.
A machine that does not know the message (a build without this, of the same
network version) drops it, as it drops any type past its own.
*/
#include "cseries.h"
#include "game/game.h"
#include "game/game_engine.h"
#include "game/players.h"
#include "units/units.h"
#include "network_distributed.h"
#include "halo_voice.h"
#include "halo_vr.h"
#include <math.h>
void platform_log(char const *format, ...);
/* ---------- constants */
enum
{
/* an entry: the speaker, its sequence number and the packet's length,
and the packet */
VOICE_ENTRY_HEADER = 4,
VOICE_MAXIMUM_PACKET = 200,
VOICE_MAXIMUM_TAKEN = 1024,
};
/* how far off someone not on the radio is heard (world units: 60 metres) */
#define VOICE_RANGE 20.0f
/* how far to the side a voice is put: on the radio, and near */
#define VOICE_RADIO_PAN 0.3f
#define VOICE_NEAR_PAN 0.8f
/* ---------- globals */
static struct
{
boolean active;
word sequence;
long sent;
long played;
long log_time;
} network_voice;
/* ---------- private code */
static boolean network_voice_in_network_game(
void)
{
short connection = game_connection();
return (connection == _game_connection_network_server || connection == _game_connection_network_client) &&
game_in_progress();
}
/* where a player's head is, and whether they have a unit */
static boolean network_voice_player_head(
struct player_datum const *player,
real_point3d *head)
{
long unit_index = distributed_living_unit(player);
if (unit_index == NONE)
return FALSE;
unit_get_camera_position(unit_index, head);
return TRUE;
}
/* the gains a speaker is heard at here (FALSE: not heard) */
static boolean network_voice_gains(
struct player_datum const *speaker,
real *left,
real *right)
{
long listener_index = local_player_get_player_index(0);
struct player_datum *listener = listener_index != NONE ? player_try_and_get(listener_index) : NULL;
boolean teammate = listener && game_engine_has_teams() && listener->team_index == speaker->team_index;
real_point3d speaker_head;
real_point3d listener_head;
real gain = 1.0f;
real pan = 0.0f;
if (!listener)
return FALSE;
if (!network_voice_player_head(speaker, &speaker_head))
{
/* (dead: on the radio, to the team) */
if (!teammate)
return FALSE;
*left = *right = 1.0f;
return TRUE;
}
if (network_voice_player_head(listener, &listener_head))
{
real dx = speaker_head.x - listener_head.x;
real dy = speaker_head.y - listener_head.y;
real dz = speaker_head.z - listener_head.z;
real distance = (real)sqrt(dx * dx + dy * dy + dz * dz);
real flat = (real)sqrt(dx * dx + dy * dy);
float look[3];
real forward_x;
real forward_y;
real length;
struct unit_datum *unit = unit_try_and_get(distributed_living_unit(listener));
/* (where the listener looks: the head, in VR) */
if (halo_vr_look_vector(0, look))
{
forward_x = look[0];
forward_y = look[1];
}
else
{
forward_x = unit ? unit->unit.looking_vector.i : 1.0f;
forward_y = unit ? unit->unit.looking_vector.j : 0.0f;
}
length = (real)sqrt(forward_x * forward_x + forward_y * forward_y);
if (length > 0.0001f && flat > 0.0001f)
{
/* (the right: forward turned clockwise) */
pan = (dx * forward_y - dy * forward_x) / (flat * length);
}
if (!teammate)
{
gain = 1.0f - distance / VOICE_RANGE;
if (gain <= 0.0f)
return FALSE;
gain *= gain;
}
pan *= teammate ? VOICE_RADIO_PAN : VOICE_NEAR_PAN;
}
else if (!teammate)
{
/* (the listener dead: the others as from where the body is not:
not at all) */
return FALSE;
}
*left = gain * (pan > 0.0f ? 1.0f - pan : 1.0f);
*right = gain * (pan < 0.0f ? 1.0f + pan : 1.0f);
return TRUE;
}
static void network_voice_play_entries(
byte const *entries,
byte const *end,
short count,
long machine_index)
{
short index;
for (index = 0; index < count && entries + VOICE_ENTRY_HEADER <= end; index++)
{
byte player_byte = entries[0];
word sequence = (word)(entries[1] | entries[2] << 8);
short length = entries[3];
long player_index;
struct player_datum *player;
real left;
real right;
if (length <= 0 || entries + VOICE_ENTRY_HEADER + length > end)
break;
player_index = distributed_player_from_byte(player_byte);
player = player_index != NONE ? player_try_and_get(player_index) : NULL;
/* (the host: only a client's own players') */
if (player && (machine_index == NONE || distributed_machine_has_player(machine_index, player_byte)) &&
player->local_player_index == NONE && network_voice_gains(player, &left, &right))
{
halo_voice_play(player_byte, sequence, entries + VOICE_ENTRY_HEADER, length, left, right);
network_voice.played++;
}
entries += VOICE_ENTRY_HEADER + length;
}
}
/* ---------- public code */
void network_voice_new_game(
void)
{
network_voice.sequence = 0;
}
word network_voice_entry_size(
void)
{
return VOICE_ENTRY_HEADER;
}
/* after each tick (network_distributed_tick): this machine's speech sent */
void network_voice_tick(
void)
{
struct
{
struct distributed_message_header header;
byte entries[VOICE_MAXIMUM_TAKEN + 64];
} message;
byte taken[VOICE_MAXIMUM_TAKEN];
long player_index = local_player_get_player_index(0);
long size;
long offset = 0;
long used = 0;
short count = 0;
if (!network_voice_in_network_game() || !halo_voice_enabled())
{
if (network_voice.active)
{
network_voice.active = FALSE;
halo_voice_end();
}
return;
}
if (!network_voice.active)
{
network_voice.active = TRUE;
halo_voice_begin();
}
if (game_time_get() - network_voice.log_time >= 150 && (network_voice.sent || network_voice.played))
{
platform_log("voice: sent %ld, played %ld packets (in 150 ticks)", network_voice.sent, network_voice.played);
network_voice.sent = network_voice.played = 0;
network_voice.log_time = game_time_get();
}
size = halo_voice_take(taken, sizeof(taken));
if (player_index == NONE)
return;
while (offset < size)
{
short length = taken[offset];
if (length <= 0 || offset + 1 + length > size || length > VOICE_MAXIMUM_PACKET)
break;
if (used + VOICE_ENTRY_HEADER + length > (long)sizeof(message.entries))
break;
message.entries[used] = distributed_player_to_byte(player_index);
message.entries[used + 1] = (byte)(network_voice.sequence & 0xFF);
message.entries[used + 2] = (byte)(network_voice.sequence >> 8);
message.entries[used + 3] = (byte)length;
csmemcpy(message.entries + used + VOICE_ENTRY_HEADER, taken + offset + 1, length);
used += VOICE_ENTRY_HEADER + length;
offset += 1 + length;
network_voice.sequence++;
count++;
}
if (!count)
return;
network_voice.sent += count;
distributed_send(&message, _distributed_message_voice, count, (word)(sizeof(message.header) + used),
game_connection() == _game_connection_network_server ? _distributed_to_clients : _distributed_to_host);
}
/* a machine's speech: on the host a client's, played and passed on to the
other clients; on a client the host's of any other */
void network_voice_handle(
long machine_index,
void const *message,
word size)
{
struct distributed_message_header header;
byte const *entries = (byte const *)message + sizeof(header);
byte const *end = (byte const *)message + size;
if (!halo_voice_enabled() || !network_voice.active)
return;
csmemcpy(&header, message, sizeof(header));
network_voice_play_entries(entries, end, header.count, machine_index);
if (machine_index != NONE)
{
long machine_indices[HALO_PORT_MAXIMUM_NETWORK_MACHINES];
short machine_count = distributed_client_machines(machine_indices, HALO_PORT_MAXIMUM_NETWORK_MACHINES);
short number;
struct
{
struct distributed_message_header header;
byte entries[VOICE_MAXIMUM_TAKEN + 64];
} relayed;
word entries_size = (word)(end - entries);
byte const *entry = entries;
short index;
/* (only the sender's own players' speech is passed on, whole) */
if (entries_size < VOICE_ENTRY_HEADER || entries_size > sizeof(relayed.entries))
return;
for (index = 0; index < header.count; index++)
{
if (entry + VOICE_ENTRY_HEADER > end || entry[3] == 0 || entry + VOICE_ENTRY_HEADER + entry[3] > end ||
!distributed_machine_has_player(machine_index, entry[0]))
{
return;
}
entry += VOICE_ENTRY_HEADER + entry[3];
}
if (entry != end)
return;
for (number = 0; number < machine_count; number++)
{
if (machine_indices[number] == machine_index)
continue;
csmemcpy(relayed.entries, entries, entries_size);
distributed_send_to_machine(machine_indices[number], &relayed, _distributed_message_voice, header.count,
(word)(sizeof(relayed.header) + entries_size));
}
}
}
+31
View File
@@ -0,0 +1,31 @@
/*
HALO_VOICE.H
The voice chat's sound for the game (port/linux/src/voice.c: on Android the
host's microphone, Opus and mixer, port/android/host/host_voice.c; elsewhere
nothing). port/linux/game/network_voice.c sends and receives it.
*/
#ifndef __HALO_VOICE_H
#define __HALO_VOICE_H
#ifdef __cplusplus
extern "C" {
#endif
/* whether the voice chat is on (voice.enabled) */
int halo_voice_enabled(void);
/* starts playing voices, and the microphone if voice.microphone; stops */
int halo_voice_begin(void);
void halo_voice_end(void);
/* the microphone's speech since asked: Opus packets, each after a byte of
its length; returns the bytes */
int halo_voice_take(void *buffer, int size);
/* a speaker's packet, played at these gains (left, right) */
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right);
#ifdef __cplusplus
}
#endif
#endif
+11
View File
@@ -413,6 +413,17 @@ static const struct config_setting config_settings[] =
"The Discord application internet play invites go through while the\n"
"Discord desktop client runs; empty for none." },
{ "voice.enabled", _config_boolean, "true", "HALO_VOICE_ENABLED", _environment_value, _platform_android,
"Voice chat in multiplayer: hear the other players (teammates over the\n"
"radio, the rest when near, from where they are)." },
{ "voice.microphone", _config_boolean, "true", "HALO_VOICE_MICROPHONE", _environment_value, _platform_android,
"Send your voice in multiplayer, whenever you speak (the headset's\n"
"microphone; Android asks for it once). False only listens." },
{ "voice.volume", _config_real, "1.0", "HALO_VOICE_VOLUME", _environment_value, _platform_android,
"The other players' voices' volume (1 as they come)." },
{ "voice.threshold", _config_real, "0.02", "HALO_VOICE_THRESHOLD", _environment_value, _platform_android,
"How loud the microphone must be to send (0..1 of full scale): higher\n"
"if it sends noise, lower if it cuts words off." },
{ "bots.count", _config_integer, "0", "HALO_BOTS_COUNT", _environment_value, _platform_android,
"Bots in the multiplayer games this machine hosts (0 to 15; 0: none):\n"
"players it drives itself, which every machine sees as players. Set in\n"
+87
View File
@@ -0,0 +1,87 @@
/*
VOICE.C
The voice chat's sound (halo_voice.h): on Android, the host's
(port/android/host/host_voice.c); elsewhere none.
*/
#include "halo_voice.h"
#include "platform.h"
#include "port_config.h"
#ifdef HALO_ANDROID
#include "guest_host.h"
#include <stdio.h>
static int voice_started;
/* whether the app may use the microphone, as the launcher found
(LauncherActivity.java): opened without it, Android would ask in the middle
of the game */
static int voice_microphone_allowed(void)
{
char path[512];
FILE *file;
int allowed = 0;
snprintf(path, sizeof(path), "%s/microphone_permission.txt", platform_data_root());
file = fopen(path, "rb");
if (file)
{
allowed = fgetc(file) == '1';
fclose(file);
}
return allowed;
}
int halo_voice_enabled(void)
{
return config_boolean("voice.enabled");
}
int halo_voice_begin(void)
{
int microphone;
if (voice_started)
return 1;
voice_started = 1;
microphone = config_boolean("voice.microphone") && voice_microphone_allowed();
platform_log("voice: on, %s", microphone ? "with the microphone" :
config_boolean("voice.microphone") ? "without the microphone (not allowed)" : "listening only");
return host_voice_start(microphone, (float)config_real("voice.volume"), (float)config_real("voice.threshold"));
}
void halo_voice_end(void)
{
if (!voice_started)
return;
voice_started = 0;
host_voice_stop();
platform_log("voice: off");
}
int halo_voice_take(void *buffer, int size)
{
return voice_started ? host_voice_take_packets(buffer, size) : 0;
}
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right)
{
if (voice_started)
host_voice_play(speaker, sequence, data, length, left, right);
}
#else
int halo_voice_enabled(void) { return 0; }
int halo_voice_begin(void) { return 0; }
void halo_voice_end(void) {}
int halo_voice_take(void *buffer, int size) { (void)buffer; (void)size; return 0; }
void halo_voice_play(int speaker, int sequence, const void *data, int length, float left, float right)
{
(void)speaker; (void)sequence; (void)data; (void)length; (void)left; (void)right;
}
#endif
+35 -11
View File
@@ -5990,16 +5990,18 @@ void p2p_set_public_game(int public_game);
int p2p_join_invite(char const *text);
int p2p_public_game(void);
int p2p_public_games(struct ui_public_game *games, int maximum);
int config_boolean(char const *name);
int config_write_boolean(char const *name, int value);
long config_integer(char const *name);
int config_write_integer(char const *name, long value);
enum
{
MAXIMUM_SHOWN_PUBLIC_GAMES = 32,
/* the online games panel's rows: "enter a code" and the games */
/* the online games panel's rows: "enter a code", the voice chat's
switches, and the games */
ONLINE_PANEL_ROWS = 11,
/* (the rows before the games: "enter a code") */
ONLINE_PANEL_FIXED_ROWS = 1,
ONLINE_PANEL_FIXED_ROWS = 3,
};
static struct
@@ -6073,7 +6075,7 @@ it, B closes it */
static void ui_join_code_panel_event(
struct event_record const *event)
{
short rows = (short)(ui_join_code_globals.game_count + 1);
short rows = (short)(ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS);
short move = 0;
if (event->type == _event_type_left_stick)
@@ -6099,9 +6101,20 @@ static void ui_join_code_panel_event(
break;
case _gamepad_analog_button_a:
case _gamepad_binary_button_start:
/* (the voice chat's switches, kept in config.toml) */
if (ui_join_code_globals.selection == 1)
{
config_write_boolean("voice.enabled", !config_boolean("voice.enabled"));
break;
}
if (ui_join_code_globals.selection == 2)
{
config_write_boolean("voice.microphone", !config_boolean("voice.microphone"));
break;
}
ui_join_code_globals.panel_open = FALSE;
if (ui_join_code_globals.selection == 0 ||
ui_join_code_globals.selection > ui_join_code_globals.game_count)
ui_join_code_globals.selection >= ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
{
ui_join_code_launch_keyboard();
}
@@ -6403,7 +6416,7 @@ static void ui_join_code_render_panel(
ui_join_code_globals.game_count =
(short)p2p_public_games(ui_join_code_globals.games, MAXIMUM_SHOWN_PUBLIC_GAMES);
ui_join_code_globals.selection =
(short)PIN(ui_join_code_globals.selection, 0, ui_join_code_globals.game_count);
(short)PIN(ui_join_code_globals.selection, 0, ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS - 1);
ui_join_code_draw_quad(40, 72, 600, 410, 0xF0050A18);
ui_join_code_draw_quad(40, 72, 600, 74, 0xFF3A6EA5);
ui_join_code_draw_quad(40, 408, 600, 410, 0xFF3A6EA5);
@@ -6418,7 +6431,7 @@ static void ui_join_code_render_panel(
short index = (short)(ui_join_code_globals.scroll + row);
short y = (short)(128 + row * 22);
if (index > ui_join_code_globals.game_count)
if (index >= ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
break;
if (index == ui_join_code_globals.selection)
ui_join_code_draw_quad(48, y, 592, (short)(y + 21), 0x803A6EA5);
@@ -6426,9 +6439,20 @@ static void ui_join_code_render_panel(
{
ui_join_code_draw_text(56, 584, (short)(y + 1), "Enter a join code...", FALSE, 0, &text_color);
}
else if (index == 1)
{
ui_join_code_draw_text(56, 584, (short)(y + 1),
config_boolean("voice.enabled") ? "Voice chat: On" : "Voice chat: Off", FALSE, 0, &text_color);
}
else if (index == 2)
{
ui_join_code_draw_text(56, 584, (short)(y + 1),
config_boolean("voice.microphone") ? "Microphone: On (sent while you speak)" :
"Microphone: Off (listening only)", FALSE, 0, &text_color);
}
else
{
struct ui_public_game const *game = &ui_join_code_globals.games[index - 1];
struct ui_public_game const *game = &ui_join_code_globals.games[index - ONLINE_PANEL_FIXED_ROWS];
ui_join_code_draw_text(56, 212, (short)(y + 1), game->name, FALSE, 0, &text_color);
ui_join_code_draw_text(216, 372, (short)(y + 1), ui_join_code_map_name(game->map), FALSE, 0,
@@ -6439,9 +6463,9 @@ static void ui_join_code_render_panel(
}
}
if (!ui_join_code_globals.game_count)
ui_join_code_draw_text(56, 584, 152, "No public games right now", FALSE, 0, &dim_color);
ui_join_code_draw_text(56, 584, 384, "%a-button Join %x-button Enter a code %b-button Back", TRUE, 0,
&text_color);
ui_join_code_draw_text(56, 584, 196, "No public games right now", FALSE, 0, &dim_color);
ui_join_code_draw_text(56, 584, 384, "%a-button Join or switch %x-button Enter a code %b-button Back", TRUE,
0, &text_color);
}
/* over the screens, as they are drawn */
+28 -3
View File
@@ -48,6 +48,10 @@ MUSL_URL = f"https://musl.libc.org/releases/musl-{MUSL_VERSION}.tar.gz"
SDL_TAG = "release-3.4.16"
SDL_DIR = THIRD_PARTY / "SDL3"
SDL_URL = "https://github.com/libsdl-org/SDL.git"
# Opus, the voice chat's codec (host_voice.c), built into the host library
OPUS_TAG = "v1.5.2"
OPUS_DIR = THIRD_PARTY / "opus"
OPUS_URL = "https://github.com/xiph/opus.git"
# the Khronos OpenXR loader for Android (the VR build, host_xr.c): its
# headers and library are linked from here; the app packages the same AAR
# through Gradle (port/android/app/build.gradle), which also merges the
@@ -221,6 +225,10 @@ def fetch_third_party() -> None:
print(f"Cloning SDL3 {SDL_TAG}")
subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", SDL_TAG, SDL_URL, str(SDL_DIR)],
check=True)
if not OPUS_DIR.is_dir():
print(f"Cloning Opus {OPUS_TAG}")
subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", OPUS_TAG, OPUS_URL, str(OPUS_DIR)],
check=True)
patch_sdl_java()
if not OPENXR_DIR.is_dir():
print(f"Downloading {OPENXR_URL}")
@@ -564,6 +572,23 @@ def generate_android_build(n: Writer, sln: Any) -> None:
)
n.build(outputs=libsdl, rule="android_sdl3", implicit=[SDL_DIR / "CMakeLists.txt"])
# ---------- Opus (the voice chat's codec, host_voice.c)
opus_build = BUILD / "opus-build"
libopus = opus_build / "libopus.a"
n.rule(
name="android_opus",
command=(f"cmake -S {OPUS_DIR} -B {opus_build} -G Ninja "
f"-DCMAKE_TOOLCHAIN_FILE={ndk}/build/cmake/android.toolchain.cmake "
f"-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-{ANDROID_API} -DCMAKE_BUILD_TYPE=Release "
f"-DCMAKE_POSITION_INDEPENDENT_CODE=ON -DOPUS_BUILD_SHARED_LIBRARY=OFF -DOPUS_BUILD_TESTING=OFF "
f"-DOPUS_BUILD_PROGRAMS=OFF -DOPUS_INSTALL_PKG_CONFIG_MODULE=OFF -DOPUS_INSTALL_CMAKE_CONFIG_MODULE=OFF "
f"> {BUILD}/opus-configure.log && ninja -C {opus_build} > {BUILD}/opus-build.log"),
description="ANDROID OPUS",
pool="console",
)
n.build(outputs=libopus, rule="android_opus", implicit=[OPUS_DIR / "CMakeLists.txt"])
# ---------- the host library
host_objects: List[Path] = []
@@ -578,7 +603,7 @@ def generate_android_build(n: Writer, sln: Any) -> None:
host_cflags = " ".join([
"-O2", "-g", "-fPIC", "-Wall", "-Wno-unused-function", "-D_GNU_SOURCE",
f"-I{PORT_DIR}/include", f"-I{PORT_DIR}/host", f"-I{SDL_DIR}/include", f"-I{LINUX_DIR}/src",
f"-I{TOML_DIR}", f"-I{OPENXR_DIR}/include",
f"-I{TOML_DIR}", f"-I{OPENXR_DIR}/include", f"-I{OPUS_DIR}/include",
])
host_sources = sorted((PORT_DIR / "host").glob("*.c")) + [
LINUX_DIR / "src" / "posix_files.c", LINUX_DIR / "src" / "posix_net.c",
@@ -604,12 +629,12 @@ def generate_android_build(n: Writer, sln: Any) -> None:
host_objects.append(table_obj)
n.rule(
name="android_host_link",
command=(f"$android_host_cc -shared -o $out $in -L{sdl_build} -L{OPENXR_DIR}/lib -lopenxr_loader "
command=(f"$android_host_cc -shared -o $out $in {libopus} -L{sdl_build} -L{OPENXR_DIR}/lib -lopenxr_loader "
+ " ".join(f"-l{lib}" for lib in HOST_LIBRARIES)
+ " -Wl,-z,max-page-size=16384 -Wl,--no-undefined"),
description="ANDROID HOST LINK $out",
)
n.build(outputs=libmain, rule="android_host_link", inputs=host_objects, implicit=[libsdl])
n.build(outputs=libmain, rule="android_host_link", inputs=host_objects, implicit=[libsdl, libopus])
# a do-nothing OpenXR layer under the Steam Frame's implicit layer's
# library name (port/android/xr_layer/fdm_injection_stub.c)