mcpx: Support 3D voice HRTF filtering

This commit is contained in:
Matt Borgerson
2025-05-31 17:16:54 -07:00
committed by mborgerson
parent 05b1f2c13d
commit 344f7da132
6 changed files with 256 additions and 4 deletions
+3
View File
@@ -215,6 +215,9 @@ display:
audio:
use_dsp: bool
hrtf:
type: bool
default: true
volume_limit:
type: number
default: 1
+82 -2
View File
@@ -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,
+19 -1
View File
@@ -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
+13 -1
View File
@@ -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 <stdint.h>
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);
+137
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef HW_XBOX_MCPX_HRTF_H
#define HW_XBOX_MCPX_HRTF_H
#include <string.h>
#include <stddef.h>
#include <math.h>
#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
+2
View File
@@ -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();
}