mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
189 lines
12 KiB
C
189 lines
12 KiB
C
// Adapted from MacNativeGames games/totk/runtime/audio_effects.c @ b75be8c (MIT, Copyright (c) 2026 MacNativeGames contributors).
|
|
// Algorithms from Ryujinx.Audio (MIT, Copyright (c) Ryujinx Team and Contributors).
|
|
/* Delay, standard reverb and compressor DSP, ported from Ryujinx.Audio.
|
|
Copyright (c) Ryujinx Team and Contributors. MIT license:
|
|
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. These are Ryujinx.Audio's five
|
|
tables; its own version reads 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};
|
|
static void validate_delay(const AudrenDelayParam *p) {
|
|
unsigned rate=(p->sample_rate>>14)/1000;
|
|
if(!channel_ok(p->channels) || p->channel_max<p->channels || rate<1 || rate>192 || p->delay_max>20000 || p->delay>p->delay_max)
|
|
rt_fatal("audio delay parameters");
|
|
}
|
|
static void validate_reverb(const AudrenReverbParam *p) {
|
|
if(!channel_ok(p->channels) || p->channel_max<p->channels || p->sample_rate<16384 || p->sample_rate>192*16384 ||
|
|
p->early_mode<0 || p->early_mode>4 || p->late_mode<0 || p->late_mode>3 || p->decay<=0 || p->hf_decay<=0 || p->pre_delay<0)
|
|
rt_fatal("audio standard reverb parameters");
|
|
}
|
|
static void validate_compressor(const AudrenCompressorParam *p) {
|
|
if(!channel_ok(p->channels) || p->channel_max<p->channels || !isfinite(p->threshold) || !(p->ratio>0) || !isfinite(p->ratio) ||
|
|
!(p->input_gain>=0 && p->input_gain<=1) || !(p->attack>=0 && p->attack<=1) || !(p->release>=0 && p->release<=1) || !isfinite(p->output_gain))
|
|
rt_fatal("audio compressor parameters");
|
|
}
|
|
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);} }
|
|
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);}
|
|
}
|