From 344f7da1323ef22d9d131181b21762b149e345d3 Mon Sep 17 00:00:00 2001 From: Matt Borgerson Date: Thu, 29 May 2025 14:33:54 -0700 Subject: [PATCH] mcpx: Support 3D voice HRTF filtering --- config_spec.yml | 3 + hw/xbox/mcpx/apu.c | 84 +++++++++++++++++++++++- hw/xbox/mcpx/apu_regs.h | 20 +++++- hw/xbox/mcpx/fpconv.h | 14 +++- hw/xbox/mcpx/hrtf.h | 137 ++++++++++++++++++++++++++++++++++++++++ ui/xui/debug.cc | 2 + 6 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 hw/xbox/mcpx/hrtf.h diff --git a/config_spec.yml b/config_spec.yml index e95e6bddf7..5b86484934 100644 --- a/config_spec.yml +++ b/config_spec.yml @@ -215,6 +215,9 @@ display: audio: use_dsp: bool + hrtf: + type: bool + default: true volume_limit: type: number default: 1 diff --git a/hw/xbox/mcpx/apu.c b/hw/xbox/mcpx/apu.c index 137f0f7479..db368f4ce9 100644 --- a/hw/xbox/mcpx/apu.c +++ b/hw/xbox/mcpx/apu.c @@ -45,6 +45,7 @@ #include "adpcm.h" #include "svf.h" #include "fpconv.h" +#include "hrtf.h" #define GET_MASK(v, mask) (((v) & (mask)) >> ctz32(mask)) @@ -85,6 +86,7 @@ typedef struct MCPXAPUVoiceFilter { float resample_buf[NUM_SAMPLES_PER_FRAME * 2]; SRC_STATE *resampler; sv_filter svf[2]; + HrtfFilter hrtf; } MCPXAPUVoiceFilter; typedef struct MCPXAPUState { @@ -123,6 +125,15 @@ typedef struct MCPXAPUState { float sample_buf[NUM_SAMPLES_PER_FRAME][2]; uint64_t voice_locked[4]; QemuSpin voice_spinlocks[MCPX_HW_MAX_VOICES]; + + struct { + int current_entry; + // FIXME: Stored in RAM + struct { + float hrir[2][HRTF_NUM_TAPS]; + float itd; + } entries[HRTF_ENTRY_COUNT]; + } hrtf; } vp; /* Global Processor */ @@ -496,6 +507,13 @@ static bool is_voice_locked(MCPXAPUState *d, uint16_t v) return (qatomic_read(&d->vp.voice_locked[v / 64]) & mask) != 0; } +static void set_hrir_coeff_tar(MCPXAPUState *d, int channel, int coeff_idx, + int8_t value) +{ + int entry = d->vp.hrtf.current_entry; + d->vp.hrtf.entries[entry].hrir[channel][coeff_idx] = int8_to_float(value); +} + static void fe_method(MCPXAPUState *d, uint32_t method, uint32_t argument) { unsigned int slot; @@ -648,6 +666,11 @@ static void fe_method(MCPXAPUState *d, uint32_t method, uint32_t argument) NV_PAVS_VOICE_PAR_STATE, NV_PAVS_VOICE_PAR_STATE_PAUSED, (argument & NV1BA0_PIO_VOICE_PAUSE_ACTION) != 0); break; + case NV1BA0_PIO_SET_CURRENT_HRTF_ENTRY: { + int handle = GET_MASK(argument, NV1BA0_PIO_SET_CURRENT_HRTF_ENTRY_HANDLE); + d->vp.hrtf.current_entry = handle; + break; + } case NV1BA0_PIO_SET_CURRENT_VOICE: d->regs[NV_PAPU_FECV] = argument; break; @@ -679,6 +702,23 @@ static void fe_method(MCPXAPUState *d, uint32_t method, uint32_t argument) voice_set_mask(d, d->regs[NV_PAPU_FECV], NV_PAVS_VOICE_CFG_MISC, 0xFFFFFFFF, argument); break; + case NV1BA0_PIO_SET_VOICE_TAR_HRTF: { + int handle = GET_MASK(argument, NV1BA0_PIO_SET_VOICE_TAR_HRTF_HANDLE); + int current_voice = d->regs[NV_PAPU_FECV]; + voice_set_mask(d, current_voice, NV_PAVS_VOICE_CFG_HRTF_TARGET, + NV_PAVS_VOICE_CFG_HRTF_TARGET_HANDLE, handle); + if (current_voice < MCPX_HW_MAX_3D_VOICES && + handle != HRTF_NULL_HANDLE) { + // FIXME: Xbox software seems to reliably set voice HRTF handles + // after updating filter parameters, however it may be possible to + // update parameter targets for an active voice. + assert(handle < HRTF_ENTRY_COUNT); + hrtf_filter_set_target_params(&d->vp.filters[current_voice].hrtf, + d->vp.hrtf.entries[handle].hrir, + d->vp.hrtf.entries[handle].itd); + } + break; + } case NV1BA0_PIO_SET_VOICE_TAR_VOLA: voice_set_mask(d, d->regs[NV_PAPU_FECV], NV_PAVS_VOICE_TAR_VOLA, 0xFFFFFFFF, argument); @@ -724,6 +764,31 @@ static void fe_method(MCPXAPUState *d, uint32_t method, uint32_t argument) voice_set_mask(d, d->regs[NV_PAPU_FECV], NV_PAVS_VOICE_PAR_NEXT, NV_PAVS_VOICE_PAR_NEXT_EBO, argument); break; + case NV1BA0_PIO_SET_HRIR ... NV1BA0_PIO_SET_HRIR_X - 1: { + assert(d->vp.hrtf.current_entry < HRTF_ENTRY_COUNT); + slot = (method - NV1BA0_PIO_SET_HRIR) / 4; + int8_t left0 = GET_MASK(argument, NV1BA0_PIO_SET_HRIR_LEFT0); + int8_t right0 = GET_MASK(argument, NV1BA0_PIO_SET_HRIR_RIGHT0); + int8_t left1 = GET_MASK(argument, NV1BA0_PIO_SET_HRIR_LEFT1); + int8_t right1 = GET_MASK(argument, NV1BA0_PIO_SET_HRIR_RIGHT1); + int coeff_idx = slot * 2; + set_hrir_coeff_tar(d, 0, coeff_idx, left0); + set_hrir_coeff_tar(d, 1, coeff_idx, right0); + coeff_idx += 1; + set_hrir_coeff_tar(d, 0, coeff_idx, left1); + set_hrir_coeff_tar(d, 1, coeff_idx, right1); + break; + } + case NV1BA0_PIO_SET_HRIR_X: { + assert(d->vp.hrtf.current_entry < HRTF_ENTRY_COUNT); + int8_t left30 = GET_MASK(argument, NV1BA0_PIO_SET_HRIR_X_LEFT30); + int8_t right30 = GET_MASK(argument, NV1BA0_PIO_SET_HRIR_X_RIGHT30); + int16_t itd = GET_MASK(argument, NV1BA0_PIO_SET_HRIR_X_ITD); + set_hrir_coeff_tar(d, 0, 30, left30); + set_hrir_coeff_tar(d, 1, 30, right30); + d->vp.hrtf.entries[d->vp.hrtf.current_entry].itd = s6p9_to_float(itd); + break; + } case NV1BA0_PIO_SET_CURRENT_INBUF_SGE: d->inbuf_sge_handle = argument & NV1BA0_PIO_SET_CURRENT_INBUF_SGE_HANDLE; break; @@ -893,6 +958,7 @@ static void vp_write(void *opaque, hwaddr addr, uint64_t val, unsigned int size) case NV1BA0_PIO_VOICE_RELEASE: case NV1BA0_PIO_VOICE_OFF: case NV1BA0_PIO_VOICE_PAUSE: + case NV1BA0_PIO_SET_CURRENT_HRTF_ENTRY: case NV1BA0_PIO_SET_CURRENT_VOICE: case NV1BA0_PIO_SET_VOICE_CFG_VBIN: case NV1BA0_PIO_SET_VOICE_CFG_FMT: @@ -901,6 +967,7 @@ static void vp_write(void *opaque, hwaddr addr, uint64_t val, unsigned int size) case NV1BA0_PIO_SET_VOICE_CFG_ENV1: case NV1BA0_PIO_SET_VOICE_CFG_ENVF: case NV1BA0_PIO_SET_VOICE_CFG_MISC: + case NV1BA0_PIO_SET_VOICE_TAR_HRTF: case NV1BA0_PIO_SET_VOICE_TAR_VOLA: case NV1BA0_PIO_SET_VOICE_TAR_VOLB: case NV1BA0_PIO_SET_VOICE_TAR_VOLC: @@ -912,6 +979,8 @@ static void vp_write(void *opaque, hwaddr addr, uint64_t val, unsigned int size) case NV1BA0_PIO_SET_VOICE_CFG_BUF_LBO: case NV1BA0_PIO_SET_VOICE_BUF_CBO: case NV1BA0_PIO_SET_VOICE_CFG_BUF_EBO: + case NV1BA0_PIO_SET_HRIR ... NV1BA0_PIO_SET_HRIR_X - 1: + case NV1BA0_PIO_SET_HRIR_X: case NV1BA0_PIO_SET_CURRENT_INBUF_SGE: case NV1BA0_PIO_SET_CURRENT_INBUF_SGE_OFFSET: CASE_4(NV1BA0_PIO_SET_OUTBUF_BA, 8): // 8 byte pitch, 4 entries @@ -1873,6 +1942,15 @@ static void voice_process(MCPXAPUState *d, } } + if (v < MCPX_HW_MAX_3D_VOICES && g_config.audio.hrtf) { + uint16_t hrtf_handle = + voice_get_mask(d, v, NV_PAVS_VOICE_CFG_HRTF_TARGET, + NV_PAVS_VOICE_CFG_HRTF_TARGET_HANDLE); + if (hrtf_handle != HRTF_NULL_HANDLE) { + hrtf_filter_process(&d->vp.filters[v].hrtf, samples, samples); + } + } + // FIXME: ParaEQ for (int b = 0; b < 8; b++) { @@ -1880,8 +1958,7 @@ static void voice_process(MCPXAPUState *d, float hr; if ((v < MCPX_HW_MAX_3D_VOICES) && (b < 4)) { // FIXME: Not sure if submix/voice headroom factor in for HRTF - // Note: Attenuate extra 6dB to simulate HRTF - hr = 1 << (d->vp.hrtf_headroom + 1); + hr = 1 << d->vp.hrtf_headroom; } else { hr = 1 << d->vp.submix_headroom[bin[b]]; } @@ -2483,6 +2560,9 @@ static void mcpx_apu_reset(MCPXAPUState *d) memset(d->vp.hrtf_submix, 0, sizeof(d->vp.hrtf_submix)); memset(d->vp.submix_headroom, 0, sizeof(d->vp.submix_headroom)); memset(d->vp.voice_locked, 0, sizeof(d->vp.voice_locked)); + for (int v = 0; v < ARRAY_SIZE(d->vp.filters); v++) { + hrtf_filter_init(&d->vp.filters[v].hrtf); + } // FIXME: Reset DSP state memset(d->gp.dsp->core.pram_opcache, 0, diff --git a/hw/xbox/mcpx/apu_regs.h b/hw/xbox/mcpx/apu_regs.h index 84a881e88f..a1e25582d2 100644 --- a/hw/xbox/mcpx/apu_regs.h +++ b/hw/xbox/mcpx/apu_regs.h @@ -145,6 +145,8 @@ #define NV1BA0_PIO_VOICE_PAUSE 0x00000140 # define NV1BA0_PIO_VOICE_PAUSE_HANDLE 0x0000FFFF # define NV1BA0_PIO_VOICE_PAUSE_ACTION (1 << 18) +#define NV1BA0_PIO_SET_CURRENT_HRTF_ENTRY 0x00000160 +# define NV1BA0_PIO_SET_CURRENT_HRTF_ENTRY_HANDLE 0x0000FFFF #define NV1BA0_PIO_SET_CONTEXT_DMA_NOTIFY 0x00000180 #define NV1BA0_PIO_SET_CURRENT_SSL_CONTEXT_DMA 0x0000018C #define NV1BA0_PIO_SET_CURRENT_SSL 0x00000190 @@ -153,7 +155,7 @@ #define NV1BA0_PIO_SET_SSL_SEGMENT_LENGTH 0x00000604 #define NV1BA0_PIO_SET_SUBMIX_HEADROOM 0x00000200 # define NV1BA0_PIO_SET_SUBMIX_HEADROOM_AMOUNT 0x7 -#define NV1BA0_PIO_SET_HRTF_HEADROOM 0x00000280 +#define NV1BA0_PIO_SET_HRTF_HEADROOM 0x00000280 # define NV1BA0_PIO_SET_HRTF_HEADROOM_AMOUNT 0x7 #define NV1BA0_PIO_SET_HRTF_SUBMIXES 0x000002C0 #define NV1BA0_PIO_SET_CURRENT_VOICE 0x000002F8 @@ -165,6 +167,8 @@ #define NV1BA0_PIO_SET_VOICE_CFG_ENV1 0x00000310 #define NV1BA0_PIO_SET_VOICE_CFG_ENVF 0x00000314 #define NV1BA0_PIO_SET_VOICE_CFG_MISC 0x00000318 +#define NV1BA0_PIO_SET_VOICE_TAR_HRTF 0x0000031C +# define NV1BA0_PIO_SET_VOICE_TAR_HRTF_HANDLE 0x0000FFFF #define NV1BA0_PIO_SET_VOICE_SSL_A 0x00000320 # define NV1BA0_PIO_SET_VOICE_SSL_A_COUNT 0x000000FF # define NV1BA0_PIO_SET_VOICE_SSL_A_BASE 0xFFFFFF00 @@ -185,6 +189,15 @@ # define NV1BA0_PIO_SET_VOICE_BUF_CBO_OFFSET 0x00FFFFFF #define NV1BA0_PIO_SET_VOICE_CFG_BUF_EBO 0x000003DC # define NV1BA0_PIO_SET_VOICE_CFG_BUF_EBO_OFFSET 0x00FFFFFF +#define NV1BA0_PIO_SET_HRIR 0x00000400 +# define NV1BA0_PIO_SET_HRIR_LEFT0 0x000000FF +# define NV1BA0_PIO_SET_HRIR_RIGHT0 0x0000FF00 +# define NV1BA0_PIO_SET_HRIR_LEFT1 0x00FF0000 +# define NV1BA0_PIO_SET_HRIR_RIGHT1 0xFF000000 +#define NV1BA0_PIO_SET_HRIR_X 0x0000043C +# define NV1BA0_PIO_SET_HRIR_X_LEFT30 0x000000FF +# define NV1BA0_PIO_SET_HRIR_X_RIGHT30 0x0000FF00 +# define NV1BA0_PIO_SET_HRIR_X_ITD 0xFFFF0000 #define NV1BA0_PIO_SET_SSL_SEGMENT_OFFSET 0x00000600 #define NV1BA0_PIO_SET_SSL_SEGMENT_LENGTH 0x00000604 #define NV1BA0_PIO_SET_CURRENT_INBUF_SGE 0x00000804 @@ -249,6 +262,8 @@ #define NV_PAVS_VOICE_CFG_MISC 0x00000018 # define NV_PAVS_VOICE_CFG_MISC_EF_RELEASERATE (0xFFF << 0) # define NV_PAVS_VOICE_CFG_MISC_FMODE (3 << 16) +#define NV_PAVS_VOICE_CFG_HRTF_TARGET 0x0000001C +# define NV_PAVS_VOICE_CFG_HRTF_TARGET_HANDLE 0x0000FFFF #define NV_PAVS_VOICE_CUR_PSL_START 0x00000020 # define NV_PAVS_VOICE_CUR_PSL_START_BA 0x00FFFFFF #define NV_PAVS_VOICE_CUR_PSH_SAMPLE 0x00000024 @@ -337,6 +352,9 @@ enum MCPX_HW_NOTIFIER { #define NV1BA0_NOTIFICATION_STATUS_DONE_SUCCESS 0x01 #define NV1BA0_NOTIFICATION_STATUS_IN_PROGRESS 0x80 +#define HRTF_NULL_HANDLE 0xFFFF +#define HRTF_ENTRY_COUNT 128 + // clang-format on #endif diff --git a/hw/xbox/mcpx/fpconv.h b/hw/xbox/mcpx/fpconv.h index 3995d150a7..fd6b9442f0 100644 --- a/hw/xbox/mcpx/fpconv.h +++ b/hw/xbox/mcpx/fpconv.h @@ -1,7 +1,7 @@ /* * Helper FP conversions * - * Copyright (c) 2020-2021 Matt Borgerson + * Copyright (c) 2020-2025 Matt Borgerson * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,6 +21,13 @@ #ifndef FLOATCONV_H #define FLOATCONV_H +#include + +static float int8_to_float(int8_t x) +{ + return x / 128.0f; +} + static float uint8_to_float(uint8_t value) { return ((int)value - 0x80) / (1.0 * 0x80); @@ -31,6 +38,11 @@ static float int16_to_float(int16_t value) return value / (1.0 * 0x8000); } +static float s6p9_to_float(int16_t value) +{ + return value / 512.0f; +} + static float int32_to_float(int32_t value) { return value / (1.0 * 0x80000000); diff --git a/hw/xbox/mcpx/hrtf.h b/hw/xbox/mcpx/hrtf.h new file mode 100644 index 0000000000..f7036bc409 --- /dev/null +++ b/hw/xbox/mcpx/hrtf.h @@ -0,0 +1,137 @@ +/* + * HRTF Filter + * + * Copyright (c) 2025 Matt Borgerson + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#ifndef HW_XBOX_MCPX_HRTF_H +#define HW_XBOX_MCPX_HRTF_H + +#include +#include +#include + +#include "apu_regs.h" + +#define HRTF_SAMPLES_PER_FRAME NUM_SAMPLES_PER_FRAME +#define HRTF_NUM_TAPS 31 +#define HRTF_MAX_DELAY_SAMPLES 42 +#define HRTF_BUFLEN (HRTF_NUM_TAPS + HRTF_MAX_DELAY_SAMPLES) +#define HRTF_PARAM_SMOOTH_ALPHA 0.01f + +typedef struct { + int buf_pos; + struct { + float buf[HRTF_BUFLEN]; + float hrir_coeff_cur[HRTF_NUM_TAPS]; + float hrir_coeff_tar[HRTF_NUM_TAPS]; + } ch[2]; + float itd_cur; + float itd_tar; +} HrtfFilter; + +static inline void hrtf_filter_init(HrtfFilter *f) +{ + memset(f, 0, sizeof(*f)); +} + +static inline void +hrtf_filter_set_target_params(HrtfFilter *f, float hrir_coeff[2][HRTF_NUM_TAPS], + float itd) +{ + f->itd_tar = + fmaxf(-HRTF_MAX_DELAY_SAMPLES, fminf(itd, HRTF_MAX_DELAY_SAMPLES)); + + for (int ch = 0; ch < 2; ch++) { + float *coeff = f->ch[ch].hrir_coeff_tar; + memcpy(coeff, hrir_coeff[ch], sizeof(f->ch[ch].hrir_coeff_tar)); + + // Normalize coefficients for unity filter gain + float s = 0.0f; + for (int k = 0; k < HRTF_NUM_TAPS; k++) { + s += fabsf(coeff[k]); + } + if (s == 0.0f || s == 1.0f) { + break; + } + for (int k = 0; k < HRTF_NUM_TAPS; k++) { + coeff[k] /= s; + } + } +} + +static inline float hrtf_filter_smooth_param(float cur, float tar) +{ + // FIXME: Match hardware parameter transition + return cur + HRTF_PARAM_SMOOTH_ALPHA * (tar - cur); +} + +static inline void hrtf_filter_step_parameters(HrtfFilter *f) +{ + for (int ch = 0; ch < 2; ch++) { + float *coeff_cur = f->ch[ch].hrir_coeff_cur; + float *coeff_tar = f->ch[ch].hrir_coeff_tar; + for (int k = 0; k < HRTF_NUM_TAPS; k++) { + coeff_cur[k] = hrtf_filter_smooth_param(coeff_cur[k], coeff_tar[k]); + } + } + f->itd_cur = hrtf_filter_smooth_param(f->itd_cur, f->itd_tar); +} + +static inline void hrtf_filter_process(HrtfFilter *f, + float in[HRTF_SAMPLES_PER_FRAME][2], + float out[HRTF_SAMPLES_PER_FRAME][2]) +{ + for (int n = 0; n < HRTF_SAMPLES_PER_FRAME; n++) { + hrtf_filter_step_parameters(f); + + for (int ch = 0; ch < 2; ch++) { + float *buf = f->ch[ch].buf; + float *coeff = f->ch[ch].hrir_coeff_cur; + + // Push new sample + buf[f->buf_pos] = in[n][ch]; + + // Interaural time difference (channel delay) + float d = f->itd_cur * (ch == 0 ? +1.0f : -1.0f); + if (d < 0.0f) { + d = 0.0f; + } + int di = d; + float dfrac = d - di; + + // HRIR Convolution + float acc = 0.0f; + for (int k = 0; k < HRTF_NUM_TAPS; k++) { + int idx1 = (f->buf_pos - di - k + HRTF_BUFLEN) % HRTF_BUFLEN; + float s = buf[idx1]; + + // Linear interpolation for fractional part + if (dfrac > 0.0f) { + int idx2 = (idx1 - 1 + HRTF_BUFLEN) % HRTF_BUFLEN; + s = s * (1 - dfrac) + buf[idx2] * dfrac; + } + acc += coeff[k] * s; + } + + out[n][ch] = acc; + } + + f->buf_pos = (f->buf_pos + 1) % HRTF_BUFLEN; + } +} + +#endif diff --git a/ui/xui/debug.cc b/ui/xui/debug.cc index fcc7a99cff..bc6de7a2ae 100644 --- a/ui/xui/debug.cc +++ b/ui/xui/debug.cc @@ -228,6 +228,8 @@ void DebugApuWindow::Draw() mcpx_apu_debug_set_ep_realtime_enabled(ep_realtime); } + ImGui::Checkbox("HRTF Filtering\n", &g_config.audio.hrtf); + ImGui::Columns(1); ImGui::End(); }