mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
The audio renderer checked the game's RequestUpdate with about 80 rt_fatal and need() sites in audren.c, audio_mixer.c, audio_effects.c and audio_reverb.c, so a section size, a splitter or mix record or an effect setting out of range (a reverb3d whose delays add up past 0.4 s, a limiter attack outside 0 to 1, a mix buffer index past its mix, a filter coefficient that is not finite) ended the process. The tested route never reached one; dungeons and bosses were never run. Now an update whose sections do not match the renderer is refused whole with 0x5299 before anything of it is applied: the SDK's RequestUpdateAudioRenderer hands that result back to the game, which ignores it and sends its next update, where any other result makes the SDK abort. A record with an id the renderer does not have is left out of an update that is otherwise applied, a splitter destination id it does not have is stored as -1, which both render loops skip, and an effect or sink whose values cannot be used is passed through or skipped in the render pass, with every index checked before its first write. audio_bad logs the first of each kind per renderer with the game frame and counts the rest. rt_fatal stays for host allocations, OpenAudioRenderer's parameters and the checks the new ones turn into invariants. Two inputs that did not end the process before change too: a mix in use with a negative buffer count is refused (it laid the mixes after it out before buffer 0), and a limiter attack or release that is not a number passes the sound through instead of turning the mix into NaN. NX2NAVI_AUDIO_STRICT=1 (audio_strict, default 0) makes audio_bad end the process as before, for bug reports. Not reproduced before the change in the game; crafted updates reproduced the fatals. The new tests/audio_params_check sends 27 refused updates, 10 records and 38 effects and sinks the renderer cannot use through renderer_handler and runs the strict switch; 15 deliberate breakages of the change each made it fail. audio_mixer_check, cfg_rows_check, check_content, a compile of every runtime file with no new -Wall -Wextra warnings in the changed ones and the stability pass's test matrix (the two baseline environment failures only) passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
225 lines
14 KiB
C
225 lines
14 KiB
C
// Contains code Copyright (c) 2026 MacNativeGames contributors (MIT; see THIRD_PARTY_NOTICES.md).
|
|
/* Delay, standard reverb and compressor DSP. Contains code Copyright (c) Ryujinx Team and Contributors (MIT; see
|
|
third_party/licenses/Ryujinx-Audio.txt). Runtime samples are normalized floats. */
|
|
#include "audio_effects.h"
|
|
#include "host.h"
|
|
#include <math.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
typedef struct { float *data; unsigned max, count, at; } DelayLine;
|
|
struct AudioEffectDsp {
|
|
unsigned type, revision, channels;
|
|
union { AudrenDelayParam delay; AudrenReverbParam reverb; AudrenCompressorParam compressor; } p;
|
|
DelayLine line[10];
|
|
float history[6], direct[4], previous[4], decay_rate;
|
|
unsigned early_time[10], pre_time;
|
|
float early_gain[10];
|
|
float input_mean, gain_mean, last_target, last_alpha, output_gain, makeup;
|
|
float stats_min, stats_mean, stats_last[6];
|
|
};
|
|
static int channel_ok(unsigned n) { return n==1 || n==2 || n==4 || n==6; }
|
|
static unsigned sample_count(unsigned rate, float ms) { return (unsigned)nearbyintf(rate*ms); }
|
|
/* The ring has `count` slots. The reference DelayLine wraps its cursor at
|
|
CurrentSampleCount-1 while its taps index modulo CurrentSampleCount, so once
|
|
per turn every tap reads a slot that is never written (a one-sample dropout
|
|
per tap, audible as grain). The cursor and the taps share one ring length here;
|
|
each delay is one sample longer than the reference. */
|
|
static void line_set(DelayLine *d, unsigned n) { d->count=n<d->max?n:d->max; d->at=0; }
|
|
static void line_init(DelayLine *d, unsigned rate, float ms) {
|
|
d->max=sample_count(rate,ms); d->data=calloc((size_t)d->max+1,sizeof(float));
|
|
if(!d->data)rt_fatal("audio effect delay allocation"); line_set(d,d->max);
|
|
}
|
|
static float line_read(const DelayLine *d) { return d->data[d->at]; }
|
|
static float line_update(DelayLine *d,float v) {
|
|
float old=line_read(d);d->data[d->at++]=v;
|
|
if(d->at >= d->count)d->at=0;
|
|
return old;
|
|
}
|
|
static float line_tap(const DelayLine *d,unsigned sample,int offset) {
|
|
int at=(int)d->at-(int)sample-offset;
|
|
if(d->count){at%=(int)d->count;if(at<0)at+=(int)d->count;}
|
|
if(at<0 || (unsigned)at>d->max)rt_fatal("audio effect delay tap bounds");
|
|
return d->data[at];
|
|
}
|
|
static float gain10(float x) { return x>=0?1:x<=-5.3f?0:powf(10,x); }
|
|
static const float fdn_time[4][4]={{53.953247f,79.192566f,116.238770f,130.615295f},{53.953247f,79.192566f,116.238770f,170.615295f},{5,10,5,10},{47.03f,71,103,170}};
|
|
static const float decay_time[4][4]={{7,9,13,17},{7,9,13,17},{1,1,1,1},{7,7,13,9}};
|
|
/* Early reflections are indexed by EarlyMode: five tables, one per mode, rather
|
|
than the first row for every mode. */
|
|
static const float early_time[5][10]={
|
|
{0,3.5f,2.8f,3.9f,2.7f,13.4f,7.9f,8.4f,9.9f,12},
|
|
{0,11.8f,5.5f,11.2f,10.4f,38.1f,22.2f,29.6f,21.2f,24.8f},
|
|
{0,41.5f,20.5f,41.3f,0,29.5f,33.8f,45.2f,46.8f,0},
|
|
{33.1f,43.3f,22.8f,37.9f,14.9f,35.3f,17.9f,34.2f,0,43.3f},
|
|
{0}
|
|
};
|
|
static const float early_gain[5][10]={
|
|
{.70f,.68f,.70f,.68f,.70f,.68f,.70f,.68f,.68f,.68f},
|
|
{.70f,.68f,.70f,.68f,.70f,.68f,.68f,.68f,.68f,.68f},
|
|
{.50f,.70f,.70f,.68f,.50f,.68f,.68f,.70f,.68f,0},
|
|
{.93f,.92f,.87f,.86f,.94f,.81f,.80f,.77f,.76f,.65f},
|
|
{0}
|
|
};
|
|
static const float pre_time[5]={12.5f,40,50,50,0};
|
|
/* What makes a delay, reverb or compressor block unusable for the DSP below, or NULL. The game sends these blocks, so
|
|
audio_mixer.c audio_mixer_update_effects asks audio_effect_invalid before it creates or updates a DSP and passes
|
|
the effect's sound through instead; the validate_* checks in create and update are then invariants. */
|
|
static const char *delay_invalid(const AudrenDelayParam *p) {
|
|
unsigned rate=(p->sample_rate>>14)/1000;
|
|
if(!channel_ok(p->channels))return "channel count not 1, 2, 4 or 6";
|
|
if(p->channel_max<p->channels)return "more channels than its channel_max";
|
|
if(rate<1 || rate>192)return "sample rate outside 1 to 192 kHz";
|
|
if(p->delay_max>20000)return "longest delay above 20000";
|
|
if(p->delay>p->delay_max)return "delay above its longest delay";
|
|
return NULL;
|
|
}
|
|
static const char *reverb_invalid(const AudrenReverbParam *p) {
|
|
if(!channel_ok(p->channels))return "channel count not 1, 2, 4 or 6";
|
|
if(p->channel_max<p->channels)return "more channels than its channel_max";
|
|
if(p->sample_rate<16384 || p->sample_rate>192*16384)return "sample rate outside 1 to 192 kHz";
|
|
if(p->early_mode<0 || p->early_mode>4)return "early mode outside 0 to 4";
|
|
if(p->late_mode<0 || p->late_mode>3)return "late mode outside 0 to 3";
|
|
if(p->decay<=0 || p->hf_decay<=0)return "decay or high-frequency decay not above 0";
|
|
if(p->pre_delay<0)return "pre-delay below 0";
|
|
return NULL;
|
|
}
|
|
static const char *compressor_invalid(const AudrenCompressorParam *p) {
|
|
if(!channel_ok(p->channels))return "channel count not 1, 2, 4 or 6";
|
|
if(p->channel_max<p->channels)return "more channels than its channel_max";
|
|
if(!isfinite(p->threshold))return "threshold not finite";
|
|
if(!(p->ratio>0) || !isfinite(p->ratio))return "ratio not above 0 or not finite";
|
|
if(!(p->input_gain>=0 && p->input_gain<=1) || !(p->attack>=0 && p->attack<=1) || !(p->release>=0 && p->release<=1))
|
|
return "input gain, attack or release outside 0 to 1";
|
|
if(!isfinite(p->output_gain))return "output gain not finite";
|
|
return NULL;
|
|
}
|
|
const char *audio_effect_invalid(unsigned type,const void *param) {
|
|
if(type==3){AudrenDelayParam p;memcpy(&p,param,sizeof p);return delay_invalid(&p);}
|
|
if(type==4){AudrenReverbParam p;memcpy(&p,param,sizeof p);return reverb_invalid(&p);}
|
|
if(type==9){AudrenCompressorParam p;memcpy(&p,param,sizeof p);return compressor_invalid(&p);}
|
|
return NULL;
|
|
}
|
|
static void validate_delay(const AudrenDelayParam *p) {
|
|
const char *why=delay_invalid(p);
|
|
if(why)rt_fatal("audio delay parameters: %s",why);
|
|
}
|
|
static void validate_reverb(const AudrenReverbParam *p) {
|
|
const char *why=reverb_invalid(p);
|
|
if(why)rt_fatal("audio standard reverb parameters: %s",why);
|
|
}
|
|
static void validate_compressor(const AudrenCompressorParam *p) {
|
|
const char *why=compressor_invalid(p);
|
|
if(why)rt_fatal("audio compressor parameters: %s",why);
|
|
}
|
|
AudioEffectDsp *audio_effect_create(unsigned type,const void *param,unsigned revision) {
|
|
AudioEffectDsp *s=calloc(1,sizeof *s);if(!s)rt_fatal("audio effect allocation");
|
|
s->type=type;s->revision=revision;s->gain_mean=s->last_target=s->stats_min=1;
|
|
if(type==3) {
|
|
AudrenDelayParam p;memcpy(&p,param,sizeof p);validate_delay(&p);s->channels=p.channels;
|
|
for(unsigned c=0;c<s->channels;c++){line_init(&s->line[c],(p.sample_rate>>14)/1000,p.delay_max);line_set(&s->line[c],sample_count((p.sample_rate>>14)/1000,p.delay));}
|
|
p.status=0;audio_effect_update(s,&p);
|
|
} else if(type==4) {
|
|
AudrenReverbParam p;memcpy(&p,param,sizeof p);validate_reverb(&p);s->channels=p.channels;unsigned khz=(unsigned)p.sample_rate>>14;
|
|
for(int i=0;i<4;i++){line_init(&s->line[i],khz,fdn_time[1][i]);line_init(&s->line[i+4],khz,decay_time[1][i]);}
|
|
line_init(&s->line[8],khz,revision>=3?350:150);line_init(&s->line[9],khz,5);
|
|
p.status=0;audio_effect_update(s,&p);
|
|
} else if(type==9) {
|
|
AudrenCompressorParam p;memcpy(&p,param,sizeof p);validate_compressor(&p);s->channels=p.channels;
|
|
p.status=0;audio_effect_update(s,&p);
|
|
} else rt_fatal("audio effect DSP type");
|
|
return s;
|
|
}
|
|
void audio_effect_free(AudioEffectDsp *s) { if(s){for(int i=0;i<10;i++)free(s->line[i].data);free(s);} }
|
|
/* The "requires reset" checks are invariants too: audio_mixer_update_effects frees the DSP whenever the channels, the
|
|
rate or the longest delay differ from the block of the update before, which is the one this DSP was made or last
|
|
updated from. */
|
|
void audio_effect_update(AudioEffectDsp *s,const void *param) {
|
|
if(s->type==3){
|
|
AudrenDelayParam p;memcpy(&p,param,sizeof p);validate_delay(&p);
|
|
if(p.channels!=s->channels || (s->p.delay.sample_rate && (p.sample_rate!=s->p.delay.sample_rate || p.delay_max!=s->p.delay.delay_max)))rt_fatal("audio delay requires reset");
|
|
s->p.delay=p;
|
|
} else if(s->type==4){
|
|
AudrenReverbParam p;memcpy(&p,param,sizeof p);validate_reverb(&p);if(p.channels!=s->channels)rt_fatal("audio reverb requires reset");s->p.reverb=p;
|
|
if(p.status>=2)return;
|
|
unsigned rate=(unsigned)p.sample_rate>>14;
|
|
for(int i=0;i<10;i++){
|
|
unsigned n=sample_count(rate,early_time[p.early_mode][i]+p.pre_delay/16384.f);s->early_time[i]=(n<s->line[8].max?n:s->line[8].max)+1;
|
|
s->early_gain[i]=early_gain[p.early_mode][i]*(p.early_gain/16384.f);
|
|
}
|
|
if(p.channels==2){s->early_gain[4]*=.5f;s->early_gain[5]*=.5f;}
|
|
unsigned n=sample_count(rate,pre_time[p.early_mode]+p.pre_delay/16384.f);s->pre_time=n<s->line[8].max?n:s->line[8].max;
|
|
float decay=p.decay/16384.f,hf=p.hf_decay/16384.f,co=cosf((1280.f/rate)*(3.14159265358979323846f/180));
|
|
s->decay_rate=.6f*(1-p.coloration/16384.f);
|
|
for(int i=0;i<4;i++){
|
|
line_set(&s->line[i],sample_count(rate,fdn_time[p.late_mode][i]));line_set(&s->line[4+i],sample_count(rate,decay_time[p.late_mode][i]));
|
|
float a=-3.f*(s->line[i].count+s->line[i+4].count),b=a/(decay*rate),c=0;
|
|
if(hf<.995f){float e=gain10(((1/hf-1)*2)/100*(b/10)),f=1-e,g=2-e*2*co;c=(g-sqrtf(g*g-f*f*4))/(f*2);}
|
|
s->direct[i]=gain10(b/1000)*(1-c)*.7071f;s->previous[i]=c;s->history[i]=0;
|
|
}
|
|
} else {
|
|
AudrenCompressorParam p;memcpy(&p,param,sizeof p);validate_compressor(&p);if(p.channels!=s->channels)rt_fatal("audio compressor requires reset");s->p.compressor=p;
|
|
if(p.status<2){s->last_alpha=p.attack;s->makeup=p.makeup?p.threshold*.5f*(1/p.ratio-1)-3:0;s->output_gain=powf(10,(p.output_gain+s->makeup)/20);}
|
|
}
|
|
}
|
|
static void delay_process(AudioEffectDsp *s,float *const in[6],float *const out[6],int frames) {
|
|
const AudrenDelayParam *p=&s->p.delay;unsigned n=s->channels;
|
|
float fb=p->feedback/16384.f,spread=p->spread/16384.f,base=(1-spread)*fb*.98f,cross=spread*fb*.98f/(n>=4?2:1),lp=.95f*(p->lowpass/16384.f);
|
|
static const int quad[4][2]={{1,2},{0,3},{0,3},{1,2}};
|
|
static const int surround[6][2]={{2,4},{2,5},{0,1},{3,3},{0,5},{1,4}};
|
|
for(int k=0;k<frames;k++){
|
|
float x[6],d[6],v[6];for(unsigned c=0;c<n;c++){x[c]=in[c][k]*64;d[c]=line_read(&s->line[c]);}
|
|
for(unsigned c=0;c<n;c++){
|
|
float feedback=n==1 || (n==6 && c==3)?d[c]*fb:d[c]*base;
|
|
if(n==2)feedback+=d[1-c]*cross;
|
|
else if(n>=4 && !(n==6 && c==3)){const int *q=n==4?quad[c]:surround[c];feedback+=(d[q[0]]+d[q[1]])*cross;}
|
|
v[c]=x[c]*(p->in_gain/16384.f)+feedback;
|
|
float z=lp*s->history[c]+v[c]*(1-lp);s->history[c]=z;line_update(&s->line[c],z);
|
|
out[c][k]=(x[c]*(p->dry_gain/16384.f)+d[c]*(p->out_gain/16384.f))/64;
|
|
}
|
|
}
|
|
}
|
|
static void reverb_process(AudioEffectDsp *s,float *const in[6],float *const out[6],int frames) {
|
|
const AudrenReverbParam *p=&s->p.reverb;unsigned n=s->channels;
|
|
static const int mono[10]={0,0,0,0,0,0,0,0,0,0},stereo[10]={0,0,1,1,0,1,0,0,1,1},quad[10]={0,0,1,1,0,1,2,2,3,3},surround[10]={0,0,1,1,4,4,2,2,3,3};
|
|
static const int feedback_stereo[4]={2,0,3,1},output_stereo[4]={0,0,1,1};
|
|
const int *map=n==1?mono:n==2?stereo:n==4?quad:surround;
|
|
for(int k=0;k<frames;k++){
|
|
float y[6]={0},x[6],fb[4],v[4],sum=0;
|
|
for(int i=0;i<10;i++){float tap=line_tap(&s->line[8],s->early_time[i],0)*s->early_gain[i];y[map[i]]+=tap;if(n==6)y[5]+=tap;}
|
|
if(n==6)y[5]*=.2f;
|
|
for(unsigned c=0;c<n;c++){x[c]=in[c][k]*64;sum+=x[c]*(p->reverb_gain/16384.f);}line_update(&s->line[8],sum);
|
|
unsigned at=s->pre_time;if(at>=s->line[8].count)at=s->line[8].count-1;
|
|
float late=line_tap(&s->line[8],at,-1)*(p->late_gain/16384.f);
|
|
for(int i=0;i<4;i++){v[i]=line_read(&s->line[i])*s->direct[i]+s->history[i]*s->previous[i];s->history[i]=v[i];}
|
|
fb[0]=v[2]+v[1];fb[1]=-v[0]-v[3];fb[2]=v[0]-v[3];fb[3]=v[1]-v[2];
|
|
for(int i=0;i<4;i++){float d=line_read(&s->line[4+i]),z=fb[i]+late-s->decay_rate*d;v[i]=line_update(&s->line[4+i],z)+z*s->decay_rate;line_update(&s->line[i],v[i]);}
|
|
if(n==1)for(int i=0;i<4;i++)y[0]+=v[i];
|
|
else if(n==2)for(int i=0;i<4;i++)y[output_stereo[i]]+=v[feedback_stereo[i]];
|
|
else {for(int i=0;i<4;i++)y[i]+=v[i];if(n==6){y[5]+=v[3];y[4]+=line_update(&s->line[9],(v[2]-v[3])*.5f);}}
|
|
for(unsigned c=0;c<n;c++)out[c][k]=(y[c]*(p->out_gain/16384.f)+x[c]*(p->dry_gain/16384.f))/64;
|
|
}
|
|
}
|
|
static void compressor_process(AudioEffectDsp *s,float *const in[6],float *const out[6],int frames) {
|
|
const AudrenCompressorParam *p=&s->p.compressor;
|
|
if(p->reset_statistics){s->stats_min=1;s->stats_mean=0;memset(s->stats_last,0,sizeof s->stats_last);}
|
|
for(int k=0;k<frames;k++){
|
|
float x[6],mean=0;for(unsigned c=0;c<s->channels;c++){x[c]=in[c][k];mean+=x[c]*x[c];}mean/=s->channels;
|
|
s->input_mean+=p->input_gain*(mean-s->input_mean);float db=10*log10f(fmaxf(s->input_mean,1e-10f)),z=1;
|
|
if(db>=p->threshold-1.5f){float g=db>=p->threshold+1.5f?(1/p->ratio-1)*(db-p->threshold):(db-p->threshold+1.5f)*(db-p->threshold+1.5f)*(-(1-1/p->ratio)/6);z=powf(10,g/20);}
|
|
float alpha,target=z,diff=s->last_target-z;
|
|
if(diff>.08f)alpha=p->attack;
|
|
else if(diff<-.08f)alpha=p->release;
|
|
else {if(fabsf(s->gain_mean-z)>=.001f)target=s->last_target;alpha=s->last_alpha;}
|
|
s->gain_mean+=alpha*(z-s->gain_mean);s->last_target=target;s->last_alpha=alpha;
|
|
for(unsigned c=0;c<s->channels;c++){out[c][k]=x[c]*s->gain_mean*s->output_gain;s->stats_last[c]=fabsf(x[c]);}
|
|
s->stats_min=fminf(s->stats_min,s->gain_mean*s->output_gain);s->stats_mean=fmaxf(s->stats_mean,mean);
|
|
}
|
|
}
|
|
void audio_effect_process(AudioEffectDsp *s,float *const input[6],float *const output[6],int frames) {
|
|
if(s->type==3)delay_process(s,input,output,frames);else if(s->type==4)reverb_process(s,input,output,frames);else compressor_process(s,input,output,frames);
|
|
}
|
|
void audio_effect_statistics(AudioEffectDsp *s,void *output,unsigned bytes) {
|
|
if(s && s->type==9 && bytes>=32){float stats[8]={s->stats_mean,s->stats_min};memcpy(stats+2,s->stats_last,24);memcpy(output,stats,32);}
|
|
}
|