From 38337f4acb557a6d241941071ab8bf44e57b7409 Mon Sep 17 00:00:00 2001 From: izzy2lost Date: Mon, 23 Feb 2026 19:48:00 -0500 Subject: [PATCH] audio improvements --- android/app/src/main/cpp/samplerate_stub.c | 116 ++++++++++++++---- android/app/src/main/cpp/xemu_android.cpp | 4 +- .../app/src/main/cpp/xemu_settings_android.cc | 2 +- .../java/com/izzy2lost/x1box/MainActivity.kt | 1 + .../com/izzy2lost/x1box/SettingsActivity.kt | 2 +- hw/xbox/mcpx/apu/apu.c | 4 +- 6 files changed, 102 insertions(+), 27 deletions(-) diff --git a/android/app/src/main/cpp/samplerate_stub.c b/android/app/src/main/cpp/samplerate_stub.c index 9e15199ab6..3eb9b6f5ad 100644 --- a/android/app/src/main/cpp/samplerate_stub.c +++ b/android/app/src/main/cpp/samplerate_stub.c @@ -3,10 +3,30 @@ #include "samplerate.h" +/* + * Minimal libsamplerate replacement for Android. + * + * Uses linear interpolation for sample-rate conversion. Quality is lower + * than the sinc resampler used on desktop, but the ratio is correctly + * applied so voices recorded at rates other than 48 kHz (e.g. 22050 Hz + * dialogue in Halo CE) play at the right pitch instead of chipmunk-fast. + * + * The original stub did: (void)ratio; — i.e. passed samples through + * unchanged regardless of the conversion ratio, causing every sub-48 kHz + * voice to play back at 48000/source_rate times normal speed. + */ + struct SRC_STATE { src_callback_t cb; - void *cb_data; - int channels; + void *cb_data; + int channels; + + /* Current input block (pointer owned by the callback, not by us). */ + float *buf; + long buf_len; /* frames in buf */ + + /* Fractional read position within buf (advances by 1/ratio per output frame). */ + double buf_pos; }; SRC_STATE *src_callback_new(src_callback_t cb, int converter_type, int channels, @@ -23,40 +43,98 @@ SRC_STATE *src_callback_new(src_callback_t cb, int converter_type, int channels, } return NULL; } - state->cb = cb; - state->cb_data = cb_data; + state->cb = cb; + state->cb_data = cb_data; state->channels = channels; + state->buf = NULL; + state->buf_len = 0; + state->buf_pos = 0.0; return state; } +/* + * ratio = output_sample_rate / input_sample_rate + * > 1 : upsampling (e.g. 22050 -> 48000, ratio ≈ 2.177) + * < 1 : downsampling + * = 1 : pass-through (still goes through the interpolator for simplicity) + * + * step = 1/ratio = input frames consumed per output frame produced. + */ long src_callback_read(SRC_STATE *state, double ratio, long frames, float *data) { - (void)ratio; - if (!state || !state->cb || !data || frames <= 0) { + if (!state || !state->cb || !data || frames <= 0 || ratio <= 0.0) { return 0; } - float *in = NULL; - long got = state->cb(state->cb_data, &in); - if (got <= 0 || in == NULL) { - return 0; + const double step = 1.0 / ratio; + long out = 0; + + while (out < frames) { + long idx = (long)state->buf_pos; + + /* Refill the input buffer when we have consumed it. */ + if (state->buf == NULL || idx >= state->buf_len) { + /* + * Carry the fractional overshoot past the end of the old block + * into the start of the new block. This preserves continuity + * when ratio > 1 (step < 1) and we drain the buffer gradually. + */ + double carry = (state->buf_len > 0) + ? state->buf_pos - (double)state->buf_len + : 0.0; + if (carry < 0.0) { + carry = 0.0; + } + + float *new_buf = NULL; + long got = state->cb(state->cb_data, &new_buf); + if (got <= 0 || new_buf == NULL) { + break; /* source exhausted */ + } + + state->buf = new_buf; + state->buf_len = got; + state->buf_pos = carry; + idx = (long)state->buf_pos; + + if (idx >= state->buf_len) { + break; /* carry >= new block length — shouldn't happen */ + } + } + + /* Linear interpolation between sample[idx] and sample[idx+1]. */ + float alpha = (float)(state->buf_pos - (double)idx); + long next_idx = idx + 1; + + for (int ch = 0; ch < state->channels; ch++) { + float s0 = state->buf[idx * state->channels + ch]; + float s1 = (next_idx < state->buf_len) + ? state->buf[next_idx * state->channels + ch] + : s0; /* hold last sample at block boundary */ + data[out * state->channels + ch] = s0 + alpha * (s1 - s0); + } + + state->buf_pos += step; + out++; } - long to_copy = frames < got ? frames : got; - memcpy(data, in, sizeof(float) * to_copy * state->channels); - return to_copy; + return out; } int src_reset(SRC_STATE *state) { - (void)state; + if (state) { + state->buf = NULL; + state->buf_len = 0; + state->buf_pos = 0.0; + } return 0; } const char *src_strerror(int error) { (void)error; - return "libsamplerate stub"; + return "libsamplerate stub (linear)"; } void src_float_to_short_array(const float *in, short *out, int len) @@ -64,14 +142,10 @@ void src_float_to_short_array(const float *in, short *out, int len) if (!in || !out || len <= 0) { return; } - for (int i = 0; i < len; ++i) { float v = in[i]; - if (v > 1.0f) { - v = 1.0f; - } else if (v < -1.0f) { - v = -1.0f; - } + if (v > 1.0f) v = 1.0f; + else if (v < -1.0f) v = -1.0f; out[i] = (short)(v * 32767.0f); } } diff --git a/android/app/src/main/cpp/xemu_android.cpp b/android/app/src/main/cpp/xemu_android.cpp index 4c405c6e7b..7b6a47d660 100644 --- a/android/app/src/main/cpp/xemu_android.cpp +++ b/android/app/src/main/cpp/xemu_android.cpp @@ -565,7 +565,7 @@ cleanup: struct EmulatorSettings { int surface_scale = 1; // 1, 2, or 3 std::string tcg_thread = "multi"; // "single" or "multi" - bool use_dsp = true; + bool use_dsp = false; bool hrtf = true; bool cache_shaders = true; bool hard_fpu = true; @@ -776,7 +776,7 @@ static SetupFiles SyncSetupFiles() { EmulatorSettings emuSettings; emuSettings.surface_scale = GetPrefInt(env, activity, "setting_surface_scale", 1); - emuSettings.use_dsp = GetPrefBool(env, activity, "setting_use_dsp", true); + emuSettings.use_dsp = GetPrefBool(env, activity, "setting_use_dsp", false); emuSettings.hrtf = GetPrefBool(env, activity, "setting_hrtf", true); emuSettings.cache_shaders = GetPrefBool(env, activity, "setting_cache_shaders", true); emuSettings.hard_fpu = GetPrefBool(env, activity, "setting_hard_fpu", true); diff --git a/android/app/src/main/cpp/xemu_settings_android.cc b/android/app/src/main/cpp/xemu_settings_android.cc index 5fcb7c04ad..8f02156fe6 100644 --- a/android/app/src/main/cpp/xemu_settings_android.cc +++ b/android/app/src/main/cpp/xemu_settings_android.cc @@ -88,7 +88,7 @@ static void xemu_settings_apply_defaults(void) g_config.display.setup_nvidia_profile = true; g_config.audio.vp.num_workers = 0; - g_config.audio.use_dsp = true; + g_config.audio.use_dsp = false; g_config.audio.hrtf = true; g_config.audio.volume_limit = 1.0; diff --git a/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt index 8df4e6cc9d..2be871e187 100644 --- a/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/x1box/MainActivity.kt @@ -71,6 +71,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener { private fun hideSystemUI() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Android 11 (API 30) and above + @Suppress("DEPRECATION") window.setDecorFitsSystemWindows(false) window.insetsController?.let { controller -> controller.hide(WindowInsets.Type.systemBars()) diff --git a/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt b/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt index d97ae7b345..7225e2a5df 100644 --- a/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/x1box/SettingsActivity.kt @@ -44,7 +44,7 @@ class SettingsActivity : AppCompatActivity() { toggleThread.check(R.id.btn_thread_multi) } - switchDsp.isChecked = prefs.getBoolean("setting_use_dsp", true) + switchDsp.isChecked = prefs.getBoolean("setting_use_dsp", false) switchHrtf.isChecked = prefs.getBoolean("setting_hrtf", true) switchShaders.isChecked = prefs.getBoolean("setting_cache_shaders", true) switchFpu.isChecked = prefs.getBoolean("setting_hard_fpu", true) diff --git a/hw/xbox/mcpx/apu/apu.c b/hw/xbox/mcpx/apu/apu.c index 107f9fa05b..04c1eb5193 100644 --- a/hw/xbox/mcpx/apu/apu.c +++ b/hw/xbox/mcpx/apu/apu.c @@ -321,8 +321,8 @@ static void monitor_init(MCPXAPUState *d) int fifo_frames = 3; int audio_samples = 512; #ifdef __ANDROID__ - fifo_frames = 8; - audio_samples = 1024; + fifo_frames = 16; + audio_samples = 512; fifo_frames = getenv_int_clamped("XEMU_ANDROID_AUDIO_FIFO_FRAMES", 3, 32, fifo_frames); audio_samples = getenv_int_clamped("XEMU_ANDROID_AUDIO_SAMPLES", 256, 4096,