integrate libsamplerate and other audio fixes

This commit is contained in:
izzy2lost
2026-03-20 09:06:52 -04:00
parent 3a58ffe0c8
commit d2c6059df3
7 changed files with 191 additions and 216 deletions
+35 -1
View File
@@ -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
-151
View File
@@ -1,151 +0,0 @@
#include <stdlib.h>
#include <string.h>
#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);
}
}
+80 -14
View File
@@ -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);
}
+1
View File
@@ -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;
+73 -20
View File
@@ -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
+2
View File
@@ -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;
-30
View File
@@ -1,30 +0,0 @@
#ifndef SAMPLERATE_H
#define SAMPLERATE_H
#include <stddef.h>
#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 */