From d2c6059df3114d2748d91322d7b1499d39b7ef87 Mon Sep 17 00:00:00 2001 From: izzy2lost Date: Fri, 20 Mar 2026 09:06:52 -0400 Subject: [PATCH] integrate libsamplerate and other audio fixes --- android/app/src/main/cpp/CMakeLists.txt | 36 ++++- android/app/src/main/cpp/samplerate_stub.c | 151 --------------------- hw/xbox/mcpx/apu/apu.c | 94 +++++++++++-- hw/xbox/mcpx/apu/apu_int.h | 1 + hw/xbox/mcpx/apu/vp/vp.c | 93 ++++++++++--- hw/xbox/mcpx/apu/vp/vp.h | 2 + include/samplerate.h | 30 ---- 7 files changed, 191 insertions(+), 216 deletions(-) delete mode 100644 android/app/src/main/cpp/samplerate_stub.c delete mode 100644 include/samplerate.h diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt index d7e918c9f8..4c16bb4194 100644 --- a/android/app/src/main/cpp/CMakeLists.txt +++ b/android/app/src/main/cpp/CMakeLists.txt @@ -78,6 +78,40 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(tomlplusplus) +# --- libsamplerate --- +set(LIBSAMPLERATE_GIT_REV "c96f5e3de9c4488f4e6c97f59f5245f22fda22f7") +set(_xemu_saved_build_shared_libs "${BUILD_SHARED_LIBS}") +set(_xemu_had_build_shared_libs FALSE) +if(DEFINED BUILD_SHARED_LIBS) + set(_xemu_had_build_shared_libs TRUE) +endif() +set(_xemu_saved_build_testing "${BUILD_TESTING}") +set(_xemu_had_build_testing FALSE) +if(DEFINED BUILD_TESTING) + set(_xemu_had_build_testing TRUE) +endif() +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(BUILD_TESTING OFF CACHE BOOL "" FORCE) +set(LIBSAMPLERATE_EXAMPLES OFF CACHE BOOL "" FORCE) +set(LIBSAMPLERATE_INSTALL OFF CACHE BOOL "" FORCE) +FetchContent_Declare( + libsamplerate + GIT_REPOSITORY "https://github.com/libsndfile/libsamplerate.git" + GIT_TAG "${LIBSAMPLERATE_GIT_REV}" + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(libsamplerate) +if(_xemu_had_build_shared_libs) + set(BUILD_SHARED_LIBS "${_xemu_saved_build_shared_libs}" CACHE BOOL "" FORCE) +else() + unset(BUILD_SHARED_LIBS CACHE) +endif() +if(_xemu_had_build_testing) + set(BUILD_TESTING "${_xemu_saved_build_testing}" CACHE BOOL "" FORCE) +else() + unset(BUILD_TESTING CACHE) +endif() + # --- nv2a_vsh_cpu --- set(NV2A_VSH_CPU_GIT_REV "1115255708c10c4841b65dcd2223262e7a316598") set(nv2a_vsh_cpu_UNIT_TEST OFF CACHE BOOL "" FORCE) @@ -727,7 +761,6 @@ list(APPEND XEMU_CORE_SOURCES "${REPO_ROOT}/ui/vnc-stubs.c" "${REPO_ROOT}/target/i386/kvm/hyperv-stub.c" "${CMAKE_CURRENT_LIST_DIR}/kvmclock_stub.c" - "${CMAKE_CURRENT_LIST_DIR}/samplerate_stub.c" "${CMAKE_CURRENT_LIST_DIR}/fast_hash_stub.c" "${CMAKE_CURRENT_LIST_DIR}/libintl_stub.c" "${CMAKE_CURRENT_LIST_DIR}/qmp_stub.c" @@ -1006,6 +1039,7 @@ target_include_directories(xemu_core PRIVATE add_dependencies(xemu_core glib_ep) target_link_libraries(xemu_core PRIVATE + SampleRate::samplerate nv2a_vsh_emulator nv2a_vsh_cpu nv2a_vsh_disassembler diff --git a/android/app/src/main/cpp/samplerate_stub.c b/android/app/src/main/cpp/samplerate_stub.c deleted file mode 100644 index 3eb9b6f5ad..0000000000 --- a/android/app/src/main/cpp/samplerate_stub.c +++ /dev/null @@ -1,151 +0,0 @@ -#include -#include - -#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; - - /* 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, - int *error, void *cb_data) -{ - (void)converter_type; - if (error) { - *error = 0; - } - SRC_STATE *state = (SRC_STATE *)calloc(1, sizeof(*state)); - if (!state) { - if (error) { - *error = -1; - } - return NULL; - } - 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) -{ - if (!state || !state->cb || !data || frames <= 0 || ratio <= 0.0) { - 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++; - } - - return out; -} - -int src_reset(SRC_STATE *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 (linear)"; -} - -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; - out[i] = (short)(v * 32767.0f); - } -} diff --git a/hw/xbox/mcpx/apu/apu.c b/hw/xbox/mcpx/apu/apu.c index f61ec7d96b..cca374c882 100644 --- a/hw/xbox/mcpx/apu/apu.c +++ b/hw/xbox/mcpx/apu/apu.c @@ -134,6 +134,16 @@ static void throttle(MCPXAPUState *d) queued_bytes = monitor_num_used_bytes(d); } +#ifdef __ANDROID__ + /* Android scheduler granularity is often too coarse for the extra + * low-watermark pacing below and can make speech sound dragged out. + * Keep FIFO backpressure, but let the output callback set the pace. + */ + d->next_frame_time_us = 0; + d->sleep_acc_us += qemu_clock_get_us(QEMU_CLOCK_REALTIME) - start_us; + return; +#endif + if (queued_bytes > d->monitor.queued_bytes_low) { int64_t now_us = qemu_clock_get_us(QEMU_CLOCK_REALTIME); if (d->next_frame_time_us == 0 || @@ -264,14 +274,49 @@ static int getenv_int_clamped(const char *name, int min_value, int max_value, return (int)parsed; } -static void monitor_hold_last_sample(MCPXAPUState *s, uint8_t *stream, int len) +static void monitor_apply_fade_in(uint8_t *stream, int len) { - int frame_bytes = sizeof(s->monitor.last_output_sample); + int frame_bytes = sizeof(int16_t[2]); + int frames = len / frame_bytes; + + if (frames <= 1) { + return; + } + + int fade_frames = MIN(frames, 64); + int16_t *samples = (int16_t *)stream; + for (int i = 0; i < fade_frames; i++) { + int gain_num = i; + int gain_den = fade_frames - 1; + samples[i * 2 + 0] = (int16_t)((samples[i * 2 + 0] * gain_num) / gain_den); + samples[i * 2 + 1] = (int16_t)((samples[i * 2 + 1] * gain_num) / gain_den); + } +} + +static void monitor_fill_underrun(const int16_t start_sample[2], uint8_t *stream, + int len) +{ + int frame_bytes = sizeof(int16_t[2]); int frames = len / frame_bytes; int16_t *out = (int16_t *)stream; - for (int i = 0; i < frames; i++) { - out[i * 2 + 0] = s->monitor.last_output_sample[0]; - out[i * 2 + 1] = s->monitor.last_output_sample[1]; + + if (frames == 1) { + out[0] = 0; + out[1] = 0; + } else if (frames > 1) { + int fade_frames = MIN(frames, 64); + for (int i = 0; i < fade_frames; i++) { + int gain_num = fade_frames - 1 - i; + int gain_den = fade_frames - 1; + out[i * 2 + 0] = + (int16_t)((start_sample[0] * gain_num) / gain_den); + out[i * 2 + 1] = + (int16_t)((start_sample[1] * gain_num) / gain_den); + } + if (fade_frames < frames) { + memset(stream + (fade_frames * frame_bytes), 0, + (frames - fade_frames) * frame_bytes); + } } int tail_bytes = len - (frames * frame_bytes); @@ -290,11 +335,7 @@ static void monitor_sink_cb(void *opaque, uint8_t *stream, int free_b) } int avail = 0; -#ifdef __ANDROID__ - int wait_attempts = 24; -#else int wait_attempts = 10; -#endif for (int i = 0; i < wait_attempts; i++) { qemu_spin_lock(&s->monitor.fifo_lock); avail = fifo8_num_used(&s->monitor.fifo); @@ -324,8 +365,22 @@ static void monitor_sink_cb(void *opaque, uint8_t *stream, int free_b) copied += chunk_len; } + if (copied > 0 && s->monitor.resume_fade_pending) { + monitor_apply_fade_in(stream, copied); + s->monitor.resume_fade_pending = false; + } + if (copied < free_b) { - monitor_hold_last_sample(s, stream + copied, free_b - copied); + int16_t fill_from[2] = { + s->monitor.last_output_sample[0], + s->monitor.last_output_sample[1], + }; + if (copied >= sizeof(fill_from)) { + memcpy(fill_from, stream + copied - sizeof(fill_from), + sizeof(fill_from)); + } + monitor_fill_underrun(fill_from, stream + copied, free_b - copied); + s->monitor.resume_fade_pending = true; } if (free_b >= sizeof(s->monitor.last_output_sample)) { @@ -347,15 +402,17 @@ static void monitor_init(MCPXAPUState *d) d->monitor.queued_bytes_high = 0; d->monitor.last_output_sample[0] = 0; d->monitor.last_output_sample[1] = 0; + d->monitor.resume_fade_pending = false; int fifo_frames = 3; int audio_samples = 512; #ifdef __ANDROID__ - /* Give Android a little more audio headroom to ride out short stalls - * without increasing the device callback size again. + /* Keep Android closer to the desktop callback size now that voice + * resampling is handled by libsamplerate instead of the old stub. The + * FIFO still provides extra headroom for short scheduling stalls. */ - fifo_frames = 24; - audio_samples = 2048; + 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, @@ -409,8 +466,17 @@ static void monitor_init(MCPXAPUState *d) int max_high = MAX(fifo_capacity_bytes - frame_bytes, frame_bytes); d->monitor.fifo_capacity_bytes = fifo_capacity_bytes; d->monitor.device_buffer_bytes = device_buffer_bytes; +#ifdef __ANDROID__ + /* Keep the Android queue short enough to avoid noticeable drift/latency, + * but still allow about two callback drains of headroom for scheduler + * jitter before backpressuring the APU. + */ + d->monitor.queued_bytes_high = MIN(2 * drain_bytes, max_high); + d->monitor.queued_bytes_low = 0; +#else d->monitor.queued_bytes_high = MIN(3 * drain_bytes, max_high); d->monitor.queued_bytes_low = MIN(drain_bytes, d->monitor.queued_bytes_high); +#endif SDL_PauseAudioDevice(sdl_audio_dev, 0); } diff --git a/hw/xbox/mcpx/apu/apu_int.h b/hw/xbox/mcpx/apu/apu_int.h index 0ffc6640b9..739f84c21b 100644 --- a/hw/xbox/mcpx/apu/apu_int.h +++ b/hw/xbox/mcpx/apu/apu_int.h @@ -103,6 +103,7 @@ typedef struct MCPXAPUState { McpxApuDebugMonitorPoint point; int16_t frame_buf[256][2]; // 1 EP frame (0x400 bytes), 8 buffered int16_t last_output_sample[2]; + bool resume_fade_pending; QemuSpin fifo_lock; Fifo8 fifo; int fifo_capacity_bytes; diff --git a/hw/xbox/mcpx/apu/vp/vp.c b/hw/xbox/mcpx/apu/vp/vp.c index 3d776969d3..1236305cfd 100644 --- a/hw/xbox/mcpx/apu/vp/vp.c +++ b/hw/xbox/mcpx/apu/vp/vp.c @@ -1140,6 +1140,7 @@ static long voice_resample_callback(void *cb_data, float **data) uint16_t v = filter->voice; assert(v < MCPX_HW_MAX_VOICES); MCPXAPUState *d = container_of(filter, MCPXAPUState, vp.filters[v]); + int channels = filter->resampler_channels ?: 2; int sample_count = 0; while (sample_count < NUM_SAMPLES_PER_FRAME) { @@ -1148,9 +1149,20 @@ static long voice_resample_callback(void *cb_data, float **data) if (!active) { break; } - int count = voice_get_samples( - d, v, (float(*)[2]) & filter->resample_buf[2 * sample_count], - NUM_SAMPLES_PER_FRAME - sample_count); + int count; + if (channels == 1) { + count = voice_get_samples( + d, v, (float(*)[2]) filter->resample_buf, + NUM_SAMPLES_PER_FRAME - sample_count); + for (int i = 0; i < count; i++) { + filter->mono_resample_buf[sample_count + i] = + filter->resample_buf[i * 2]; + } + } else { + count = voice_get_samples( + d, v, (float(*)[2]) &filter->resample_buf[2 * sample_count], + NUM_SAMPLES_PER_FRAME - sample_count); + } if (count < 0) { break; } @@ -1159,41 +1171,76 @@ static long voice_resample_callback(void *cb_data, float **data) if (sample_count < NUM_SAMPLES_PER_FRAME) { /* Starvation causes SRC hang on repeated calls. Provide silence. */ - memset(&filter->resample_buf[2*sample_count], 0, - 2*(NUM_SAMPLES_PER_FRAME-sample_count)*sizeof(float)); + if (channels == 1) { + memset(&filter->mono_resample_buf[sample_count], 0, + (NUM_SAMPLES_PER_FRAME - sample_count) * sizeof(float)); + } else { + memset(&filter->resample_buf[2 * sample_count], 0, + 2 * (NUM_SAMPLES_PER_FRAME - sample_count) * sizeof(float)); + } sample_count = NUM_SAMPLES_PER_FRAME; } - *data = filter->resample_buf; + *data = channels == 1 ? filter->mono_resample_buf : filter->resample_buf; return sample_count; } +static int voice_resampler_converter_type(void) +{ +#ifdef __ANDROID__ + /* The desktop sinc converter is expensive enough to drag mobile + * frametimes, which in turn makes voice playback sound slow/raspy. + * Use libsamplerate's linear converter on Android to keep the ratio + * correction from the real library without the full sinc cost. + */ + return SRC_LINEAR; +#else + return SRC_SINC_FASTEST; +#endif +} + static int voice_resample(MCPXAPUState *d, uint16_t v, float samples[][2], - int requested_num, float rate) + int requested_num, float rate, int channels) { assert(v < MCPX_HW_MAX_VOICES); MCPXAPUVoiceFilter *filter = &d->vp.filters[v]; + if (filter->resampler && filter->resampler_channels != channels) { + src_delete(filter->resampler); + filter->resampler = NULL; + } + if (filter->resampler == NULL) { filter->voice = v; + filter->resampler_channels = channels; int err; - /* Note: Using a sinc based resampler for quality. Unsure about - * hardware's actual interpolation method; it could just be linear, in - * which case using this resampler is overkill, but quality is good - * so use it for now. + /* Unsure about the hardware's exact interpolation method. Desktop uses + * libsamplerate's faster sinc mode for quality; Android uses the + * lighter linear converter to keep mobile frametimes stable. */ - // FIXME: Don't do 2ch resampling if this is a mono voice filter->resampler = src_callback_new(&voice_resample_callback, - SRC_SINC_FASTEST, 2, &err, filter); + voice_resampler_converter_type(), + channels, &err, filter); if (filter->resampler == NULL) { fprintf(stderr, "src error: %s\n", src_strerror(err)); assert(0); } } - int count = src_callback_read(filter->resampler, rate, requested_num, + int count; + if (channels == 1) { + float mono_samples[NUM_SAMPLES_PER_FRAME]; + count = src_callback_read(filter->resampler, rate, requested_num, + mono_samples); + for (int i = 0; i < count; i++) { + samples[i][0] = mono_samples[i]; + samples[i][1] = mono_samples[i]; + } + } else { + count = src_callback_read(filter->resampler, rate, requested_num, (float *)samples); + } if (count == -1) { DPRINTF("resample error\n"); } @@ -1359,7 +1406,8 @@ static void voice_process(MCPXAPUState *d, } int count = voice_resample(d, v, &samples[sample_count], - NUM_SAMPLES_PER_FRAME - sample_count, rate); + NUM_SAMPLES_PER_FRAME - sample_count, rate, + channels); if (count < 0) { break; } @@ -1819,9 +1867,8 @@ static int mcpx_apu_default_vp_worker_count(void) #ifdef __ANDROID__ /* * Mobile SoCs are often oversubscribed already (TCG + render + I/O). - * Keep VP worker defaults very conservative to reduce thread contention and - * leave more CPU time for the emulation threads that gate frametime. - * Allow explicit override via env. + * Stay under the desktop default, but give the VP path enough parallelism + * for libsamplerate-backed voice mixing. Allow explicit override via env. */ const char *value = getenv("XEMU_ANDROID_VP_WORKERS"); if (value && value[0] != '\0') { @@ -1832,10 +1879,16 @@ static int mcpx_apu_default_vp_worker_count(void) } } - if (cpu_count <= 6) { + if (cpu_count <= 2) { return 1; } - return 2; + if (cpu_count <= 4) { + return 2; + } + if (cpu_count <= 6) { + return 3; + } + return 4; #else return cpu_count; #endif diff --git a/hw/xbox/mcpx/apu/vp/vp.h b/hw/xbox/mcpx/apu/vp/vp.h index 1f166fc4de..93c33a08c7 100644 --- a/hw/xbox/mcpx/apu/vp/vp.h +++ b/hw/xbox/mcpx/apu/vp/vp.h @@ -43,7 +43,9 @@ typedef struct MCPXAPUVPSSLData { typedef struct MCPXAPUVoiceFilter { uint16_t voice; float resample_buf[NUM_SAMPLES_PER_FRAME * 2]; + float mono_resample_buf[NUM_SAMPLES_PER_FRAME]; SRC_STATE *resampler; + int resampler_channels; sv_filter svf[2]; HrtfFilter hrtf; } MCPXAPUVoiceFilter; diff --git a/include/samplerate.h b/include/samplerate.h deleted file mode 100644 index 94e79a9a8c..0000000000 --- a/include/samplerate.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef SAMPLERATE_H -#define SAMPLERATE_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct SRC_STATE SRC_STATE; - -typedef long (*src_callback_t)(void *cb_data, float **data); - -enum { - SRC_SINC_FASTEST = 0, -}; - -SRC_STATE *src_callback_new(src_callback_t cb, int converter_type, int channels, - int *error, void *cb_data); -long src_callback_read(SRC_STATE *state, double ratio, long frames, float *data); -int src_reset(SRC_STATE *state); -const char *src_strerror(int error); - -void src_float_to_short_array(const float *in, short *out, int len); - -#ifdef __cplusplus -} -#endif - -#endif /* SAMPLERATE_H */