mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Ramp a changed volume across the audio frame instead of applying it at once.
The mixer applied each voice's mix volume and each splitter destination's volume to a whole 5 ms frame as one constant, so the level jumped at the frame's first sample whenever the game moved a volume, which is heard as a click. It now keeps what each voice and each splitter destination was mixed with in the frame before and goes from there to the new value in equal steps across the frame's samples, as the console's renderer does. An unchanged volume stays on the constant path. A voice that has just started, one that was silent in the frame before or was moved to another mix or splitter, a destination that is new or was moved, and the first frame after the renderer starts take the new volume at once. NX2NAVI_AUDIO_RAMP=0 turns the ramps off. The audio statistics line counts the render passes that ramped a volume. tests/audio_mixer_check.py builds the mixer with the rest of the runtime stubbed and checks the ramps and the cases that must not ramp. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -358,6 +358,14 @@ frame, uneven frame times one steady step, and game time follows the clock. It c
|
||||
sites do: at 144 fps each of 64 water objects gets its ripple steps and the day its seconds, and a step of one frame
|
||||
gives what the game computes. No game files.
|
||||
|
||||
A change to how the mixer applies volumes (`runtime/audio_mixer.c` `accumulate_ramp`, the voice and splitter paths
|
||||
of `audio_mixer_render`) passes `python tests\audio_mixer_check.py`, which builds `audio_mixer.c` with the rest of
|
||||
the runtime stubbed and mixes voices of one constant sample, so a mix buffer shows the gain sample by sample: a
|
||||
volume stepped from 0 to 1 rises in equal steps across the next 5 ms frame and holds after it, for a voice into a mix,
|
||||
a voice into a splitter destination and a submix into one; a voice that starts, was silent in the frame before or was
|
||||
moved to another mix, a destination that is new or was moved, and the first frame after the renderer starts take the
|
||||
new volume at once; `NX2NAVI_AUDIO_RAMP=0` applies every volume at once. No game files and no audio device.
|
||||
|
||||
A change to the mouse camera (`runtime/mouse_direct.c`, the mouse path of `runtime/input.c`, the set `mouse_direct` of
|
||||
`recomp/patches-v111.txt` or `patches-v143.txt`) passes `python tests\mouse_direct_check.py --cc <clang> --deps
|
||||
<ucrt64>\include`, which builds `mouse_direct.c`, and `input.c` with `--deps`, with the rest of the runtime stubbed and
|
||||
|
||||
@@ -713,6 +713,10 @@
|
||||
# Host priority of the game's audio thread: 0 leaves it alone, 1 raises it to the highest while its audio update is
|
||||
# read (the audio render thread waits for that), 2 also keeps it above normal. Takes effect at the next start.
|
||||
# audio_game_prio = 0
|
||||
# When the game changes a sound's volume, go from the old volume to the new one across the 5 ms of sound that follow,
|
||||
# as the console does (1, the default). 0 applies the new volume at once, which clicks when a volume moves quickly (a
|
||||
# fade, a sound passing by). Takes effect at the next start.
|
||||
# audio_ramp = 1
|
||||
# Volume in percent (0 to 100), on top of the game's own. Changes while the game runs take effect at once.
|
||||
# volume = 100
|
||||
# Speakers: stereo, 5.1 or auto (5.1 when the Windows speaker setup has six or more channels, stereo otherwise). The
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
//
|
||||
// Per 5 ms frame the render thread (audren.c, renderer lock held) decodes every started voice (PCM8/16/24/32, float,
|
||||
// DSP-ADPCM) along the sample path the voice clock consumes, resamples it linearly, runs the voice biquads, scales it
|
||||
// into its destination mix, folds the submixes, runs the mix effects and writes the device sink's mix (stereo, or 5.1
|
||||
// into its destination mix (a volume the game changed is reached in equal steps across the frame's samples,
|
||||
// accumulate_ramp), folds the submixes, runs the mix effects and writes the device sink's mix (stereo, or 5.1
|
||||
// with NX2NAVI_AUDIO_CHANNELS, output_channels) into a single-producer single-consumer ring. The SDL audio thread drains
|
||||
// that ring without taking any lock: an empty ring
|
||||
// plays silence and never blocks the render thread or the guest. NX2NAVI_AUDIO=none (or NX2NAVI_AUDIO_OFF) mixes without a
|
||||
@@ -41,8 +42,15 @@
|
||||
#define SPLITTER_UNUSED 0xffffffffu
|
||||
#define dm_guest_write_prepare(p, n) ((void)(p), (void)(n)) // guest memory is plain host memory on Windows
|
||||
|
||||
typedef struct { float vol[24]; int32_t dest; uint8_t used; AudrenBiquadFloat biquads[2]; float state[2][2]; } SplitDest;
|
||||
typedef struct {
|
||||
float vol[24]; int32_t dest; uint8_t used; AudrenBiquadFloat biquads[2]; float state[2][2];
|
||||
// volume ramps (audio_mixer_render): the volumes it had in the render pass before, the mix it went to and how many
|
||||
// of that mix's buffers; prev_ok 0 when that pass did not use it
|
||||
float prev[24]; int32_t prev_dest, prev_buffers; uint8_t prev_ok;
|
||||
} SplitDest;
|
||||
typedef struct { int32_t ndest; int32_t *ids; } SplitInfo;
|
||||
// volume ramps: a mix in the render pass before: its volume, the splitter it went to, its buffer count, whether in use
|
||||
typedef struct { float volume; uint32_t splitter; int32_t buffers; uint8_t ok; } MixPrev;
|
||||
typedef struct {
|
||||
Reverb3d *reverb; Reverb3dParam param; int pending;
|
||||
float *delay, detector[6], compression[6], peak[6], minimum[6]; int position[6], delay_max, channels;
|
||||
@@ -73,6 +81,10 @@ typedef struct Mixer {
|
||||
double level_sum, level_peak; uint64_t level_count, level_frames; int first_sound_logged;
|
||||
double input_sum[6]; // NX2NAVI_TRACE=audio: the device sink's inputs, squared and summed
|
||||
int prefill; // render blocks
|
||||
int ramp; // NX2NAVI_AUDIO_RAMP: volumes ramp across a frame (accumulate_ramp)
|
||||
int snap; // the next pass has no pass before it to ramp from (open, start)
|
||||
MixPrev *mix_prev; // each mix in the pass before
|
||||
uint64_t ramps, ramp_passes; // ramps made; render passes with at least one (audio_mixer_ramp_stats)
|
||||
uint64_t trace_pass, trace_seen[1024]; // NX2NAVI_TRACE=audio: render passes; the pass each voice was last mixed in
|
||||
uint64_t trace_buf[1024]; // and the wave buffer address it read in that pass
|
||||
uint64_t trace_pair[1024]; // the last shared-buffer pair logged for each voice (other voice, address)
|
||||
@@ -571,7 +583,8 @@ void audio_mixer_open(AudioRenderer *r) {
|
||||
m->ndest = r->param.splitter_destination_count;
|
||||
m->split = calloc((size_t)(m->nsplit ? m->nsplit : 1), sizeof *m->split);
|
||||
m->dest = calloc((size_t)(m->ndest ? m->ndest : 1), sizeof *m->dest);
|
||||
if (!m->buf || !m->offset || !m->split || !m->dest) rt_fatal("audio mixer tables");
|
||||
m->mix_prev = calloc((size_t)r->param.submix_count + 1, sizeof *m->mix_prev);
|
||||
if (!m->buf || !m->offset || !m->split || !m->dest || !m->mix_prev) rt_fatal("audio mixer tables");
|
||||
m->effect = calloc((size_t)(r->param.effect_count ? r->param.effect_count : 1), sizeof *m->effect);
|
||||
m->effect_order = calloc((size_t)(r->param.effect_count ? r->param.effect_count : 1), sizeof *m->effect_order);
|
||||
if (!m->effect || !m->effect_order) rt_fatal("audio effect tables");
|
||||
@@ -590,6 +603,10 @@ void audio_mixer_open(AudioRenderer *r) {
|
||||
m->prefill = PREFILL_DEFAULT_MS / 5;
|
||||
}
|
||||
m->channels = output_channels();
|
||||
m->ramp = cfg_bool("NX2NAVI_AUDIO_RAMP");
|
||||
m->snap = 1;
|
||||
rt_log("audio: NX2NAVI_AUDIO_RAMP=%d: a changed volume %s", m->ramp,
|
||||
m->ramp ? "is reached in equal steps across the frame" : "applies to the whole frame at once");
|
||||
r->mixer = m;
|
||||
atomic_store(&m->playing, r->state == 0);
|
||||
output_open(m, r->param.sample_rate);
|
||||
@@ -601,6 +618,8 @@ void audio_mixer_set_running(AudioRenderer *r, int running) {
|
||||
if (!running) {
|
||||
atomic_store_explicit(&m->flush_until, atomic_load_explicit(&m->wr, memory_order_acquire), memory_order_release);
|
||||
atomic_fetch_add_explicit(&m->stop_epoch, 1, memory_order_release);
|
||||
} else {
|
||||
m->snap = 1; // what was mixed before the stop is not the sample before the next pass's first
|
||||
}
|
||||
atomic_store_explicit(&m->playing, running, memory_order_release);
|
||||
}
|
||||
@@ -627,6 +646,7 @@ void audio_mixer_close(AudioRenderer *r) {
|
||||
free(m->effect_order);
|
||||
free(m->buf);
|
||||
free(m->offset);
|
||||
free(m->mix_prev);
|
||||
free(m->split);
|
||||
free(m->dest);
|
||||
free(m);
|
||||
@@ -844,6 +864,29 @@ static void accumulate(Mixer *m, int buffer, const float *src, float vol, int fr
|
||||
if (vol == 0 || buffer < 0 || buffer >= m->nbuf) return;
|
||||
float *d = mixbuf(m, buffer); for (int k = 0; k < frames; k++) d[k] += src[k] * vol;
|
||||
}
|
||||
// Volume ramps (NX2NAVI_AUDIO_RAMP, default 1). The game sets volumes once per audio update, and a volume applied to a
|
||||
// whole frame as one constant makes the level jump at the frame's first sample whenever the game moves it (a fade, a
|
||||
// pan, a sound going past the camera): a step in the waveform, heard as a click. The console's renderer goes from the
|
||||
// volume of the frame before to the new one across the frame's samples, and so does this: `from` is the gain the same
|
||||
// source went into this buffer with in the render pass before, sample k gets from + k (to - from) / frames, and the
|
||||
// next pass, which starts at `to`, continues the line. Equal gains take accumulate's constant path, and so does a gain
|
||||
// that is not a finite number (NaN also marks "no gain in the pass before").
|
||||
static void accumulate_ramp(Mixer *m, int buffer, const float *src, float from, float to, int frames) {
|
||||
if (from == to || !isfinite(from) || !isfinite(to)) { accumulate(m, buffer, src, to, frames); return; }
|
||||
if (buffer < 0 || buffer >= m->nbuf) return;
|
||||
const float step = (to - from) / (float)frames;
|
||||
float *d = mixbuf(m, buffer), gain = from;
|
||||
for (int k = 0; k < frames; k++) { d[k] += src[k] * gain; gain += step; }
|
||||
m->ramps++;
|
||||
}
|
||||
// A splitter destination ramps when the pass before used it and sent it to the same mix: one that is new or was moved
|
||||
// starts at its new volumes.
|
||||
static int dest_ramps(const SplitDest *d) { return d->prev_ok && d->prev_dest == d->dest; }
|
||||
void audio_mixer_ramp_stats(const AudioRenderer *r, uint64_t *passes, uint64_t *ramps) {
|
||||
const Mixer *m = r->mixer;
|
||||
*passes = m ? m->ramp_passes : 0;
|
||||
*ramps = m ? m->ramps : 0;
|
||||
}
|
||||
static int mix_valid(AudioRenderer *r, int32_t id) { return id >= 0 && id <= r->param.submix_count && r->mixes[id].in_use; }
|
||||
static int mix_depth(AudioRenderer *r, int id) {
|
||||
int d = 0;
|
||||
@@ -1046,6 +1089,10 @@ void audio_mixer_render(AudioRenderer *r, double seconds) {
|
||||
|
||||
float (*vs)[MAX_FRAME]=m->voice_scratch,*filtered=m->filter_scratch;
|
||||
m->trace_pass++;
|
||||
// A pass ramps from the pass before it. The first one after the mixer opened or the renderer started has none.
|
||||
const int ramp = m->ramp && !m->snap;
|
||||
const uint64_t ramps_before = m->ramps;
|
||||
m->snap = 0;
|
||||
for (int i = 0; i < r->param.voice_count; i++) {
|
||||
ServerVoice *v = &r->voices[i];
|
||||
if (!v->in.in_use || v->state != SV_STARTED || !v->in.sample_rate || !v->valid[v->index]) continue;
|
||||
@@ -1089,21 +1136,50 @@ void audio_mixer_render(AudioRenderer *r, double seconds) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The voice ramps when the pass before mixed it, with as many channels and into the same mix or splitter. One
|
||||
// that has just started (voice_reset in audren.c; or stopped, paused or out of wave buffers in that pass) and
|
||||
// one the game moved to another destination start at their new volumes.
|
||||
const int vramp = ramp && v->ramp_pass == r->elapsed_frames && v->ramp_channels == (uint32_t)cc &&
|
||||
v->ramp_mix == v->in.mix_id && v->ramp_splitter == v->in.splitter_id;
|
||||
int routed = 0;
|
||||
if (v->in.splitter_id != SPLITTER_UNUSED && v->in.splitter_id < (uint32_t)m->nsplit) {
|
||||
const SplitInfo *s = &m->split[v->in.splitter_id];
|
||||
for (int c = 0; c < cc; c++) for (int k = c; k < s->ndest; k += cc) {
|
||||
SplitDest *d = &m->dest[s->ids[k]];
|
||||
if (!d->used || !mix_valid(r, d->dest)) continue;
|
||||
const float *source=splitter_filter(d,vs[c],filtered,frames);
|
||||
for (int b = 0; b < r->mixes[d->dest].buffer_count && b < 24; b++) accumulate(m, m->offset[d->dest] + b, source, d->vol[b] * v->in.volume, frames);
|
||||
// from the destination's volumes and the voice's volume of the pass before, whose product is the gain
|
||||
// that pass mixed with (the destination's are kept once per pass: other voices may feed it too)
|
||||
const int from_prev = vramp && dest_ramps(d);
|
||||
for (int b = 0; b < r->mixes[d->dest].buffer_count && b < 24; b++) {
|
||||
const float to = d->vol[b] * v->in.volume;
|
||||
accumulate_ramp(m, m->offset[d->dest] + b, source, from_prev && b < d->prev_buffers ? d->prev[b] * v->ramp_volume : to, to, frames);
|
||||
}
|
||||
}
|
||||
routed = 1;
|
||||
} else if (mix_valid(r, v->in.mix_id)) {
|
||||
const int nb = r->mixes[v->in.mix_id].buffer_count < 24 ? r->mixes[v->in.mix_id].buffer_count : 24;
|
||||
for (int c = 0; c < cc; c++) {
|
||||
int32_t id = v->in.channel_ids[c]; if (id < 0 || id >= r->param.voice_count * 6) continue;
|
||||
int32_t id = v->in.channel_ids[c];
|
||||
if (id < 0 || id >= r->param.voice_count * 6) { // nothing goes in: no gain for the next pass to ramp from
|
||||
for (int b = 0; b < nb; b++) v->ramp_gain[c][b] = NAN;
|
||||
continue;
|
||||
}
|
||||
const AudrenChannelIn *ch = &r->channels[id];
|
||||
for (int b = 0; b < r->mixes[v->in.mix_id].buffer_count && b < 24; b++) accumulate(m, m->offset[v->in.mix_id] + b, vs[c], ch->mix[b] * v->in.volume, frames);
|
||||
for (int b = 0; b < nb; b++) {
|
||||
const float to = ch->mix[b] * v->in.volume;
|
||||
accumulate_ramp(m, m->offset[v->in.mix_id] + b, vs[c], vramp && b < v->ramp_buffers ? v->ramp_gain[c][b] : to, to, frames);
|
||||
v->ramp_gain[c][b] = to;
|
||||
}
|
||||
}
|
||||
v->ramp_buffers = nb;
|
||||
routed = 1;
|
||||
}
|
||||
v->ramp_pass = routed ? r->elapsed_frames + 1 : 0; // audren.c render_thread counts the pass after this render
|
||||
v->ramp_channels = (uint32_t)cc;
|
||||
v->ramp_mix = v->in.mix_id;
|
||||
v->ramp_splitter = v->in.splitter_id;
|
||||
v->ramp_volume = v->in.volume;
|
||||
}
|
||||
if (rt_trace & RT_TRACE_AUDIO) {
|
||||
// Voices mixed in the last pass and not in this one, and why; then voices that read the same wave buffer memory
|
||||
@@ -1139,17 +1215,38 @@ void audio_mixer_render(AudioRenderer *r, double seconds) {
|
||||
const AudrenMixIn *x = &r->mixes[order[a]]; int src = m->offset[order[a]];
|
||||
if (x->dest_splitter_id != SPLITTER_UNUSED && x->dest_splitter_id < (uint32_t)m->nsplit) {
|
||||
const SplitInfo *s = &m->split[x->dest_splitter_id];
|
||||
// a mix into a splitter ramps like a voice into one: from the destination's volumes and the mix's volume
|
||||
// of the pass before, when that pass had the mix with as many buffers on the same splitter
|
||||
const MixPrev *xp = &m->mix_prev[order[a]];
|
||||
const int xramp = ramp && xp->ok && xp->splitter == x->dest_splitter_id && xp->buffers == x->buffer_count;
|
||||
for (int c = 0; c < x->buffer_count && c < 24; c++) for (int k = c; k < s->ndest; k += x->buffer_count) {
|
||||
SplitDest *d = &m->dest[s->ids[k]];
|
||||
if (!d->used || !mix_valid(r, d->dest)) continue;
|
||||
const float *source=splitter_filter(d,mixbuf(m,src+c),filtered,frames);
|
||||
for (int b = 0; b < r->mixes[d->dest].buffer_count && b < 24; b++) accumulate(m, m->offset[d->dest] + b, source, d->vol[b] * x->volume, frames);
|
||||
const int from_prev = xramp && dest_ramps(d);
|
||||
for (int b = 0; b < r->mixes[d->dest].buffer_count && b < 24; b++) {
|
||||
const float to = d->vol[b] * x->volume;
|
||||
accumulate_ramp(m, m->offset[d->dest] + b, source, from_prev && b < d->prev_buffers ? d->prev[b] * xp->volume : to, to, frames);
|
||||
}
|
||||
}
|
||||
} else if (mix_valid(r, x->dest_mix_id)) {
|
||||
for (int c = 0; c < x->buffer_count && c < 24; c++)
|
||||
for (int b = 0; b < r->mixes[x->dest_mix_id].buffer_count && b < 24; b++) accumulate(m, m->offset[x->dest_mix_id] + b, mixbuf(m, src + c), x->mix[c][b] * x->volume, frames);
|
||||
}
|
||||
}
|
||||
// What every splitter destination and every mix went in with, for the next pass's ramps: kept here, once per pass,
|
||||
// after every voice and mix that feeds a destination has read the values of the pass before.
|
||||
for (int i = 0; i < m->ndest; i++) {
|
||||
SplitDest *d = &m->dest[i];
|
||||
d->prev_ok = d->used && mix_valid(r, d->dest);
|
||||
if (!d->prev_ok) continue;
|
||||
memcpy(d->prev, d->vol, sizeof d->prev);
|
||||
d->prev_dest = d->dest;
|
||||
d->prev_buffers = r->mixes[d->dest].buffer_count;
|
||||
}
|
||||
for (int i = 1; i < mc; i++)
|
||||
m->mix_prev[i] = (MixPrev){r->mixes[i].volume, r->mixes[i].dest_splitter_id, r->mixes[i].buffer_count, r->mixes[i].in_use != 0};
|
||||
if (m->ramps != ramps_before) m->ramp_passes++;
|
||||
if (r->mixes[0].in_use) mix_effects(r, 0);
|
||||
/* device sink */
|
||||
const AudrenSinkIn *sink = NULL;int circular=0;
|
||||
|
||||
@@ -47,6 +47,7 @@ typedef struct AudrenStats {
|
||||
upd_hold_max_ns;
|
||||
// voices (under the lock)
|
||||
uint64_t dry, late[3], late_max; // next buffer late by 1-2 passes, 3-8, 9-AS_LATE_PASSES
|
||||
uint64_t ramp_passes, ramps; // the mixer's volume ramp counts at the window's start (audio_mixer_ramp_stats)
|
||||
int events;
|
||||
uint64_t voices_ns; // the game's update (under the lock): the last voice census line
|
||||
int voices_most; // the most voices in use a census found
|
||||
@@ -132,6 +133,7 @@ static void voice_reset(ServerVoice *v) {
|
||||
memset(&v->adpcm, 0, sizeof v->adpcm);
|
||||
memset(v->biquad_state, 0, sizeof v->biquad_state);
|
||||
v->dry_pass = 0;
|
||||
v->ramp_pass = 0; // a new voice starts at its volumes: no ramp from what the slot's last voice was mixed with
|
||||
}
|
||||
|
||||
static void update_voice(AudioRenderer *r, const AudrenVoiceIn *in) {
|
||||
@@ -471,7 +473,9 @@ static void stats_report(AudioRenderer *r, uint64_t now, uint64_t lock_ns, uint6
|
||||
int dev = audio_mixer_out_stats(r, &o);
|
||||
char event[320] = "";
|
||||
int window = 0;
|
||||
uint64_t ramp_passes, ramps;
|
||||
plat_mutex_lock(&r->lock);
|
||||
audio_mixer_ramp_stats(r, &ramp_passes, &ramps);
|
||||
if (dev && o.pull_gap_max_ns > st->pull_gap_max_ns) st->pull_gap_max_ns = o.pull_gap_max_ns;
|
||||
if (dev && o.underruns > st->seen_underruns) {
|
||||
if (st->events < AS_EVENTS) {
|
||||
@@ -490,6 +494,7 @@ static void stats_report(AudioRenderer *r, uint64_t now, uint64_t lock_ns, uint6
|
||||
st->window_ns = now;
|
||||
st->out_underruns = o.underruns, st->out_missing = o.missing_frames, st->out_skipped = o.skipped_frames;
|
||||
st->out_overruns = o.overruns;
|
||||
st->ramp_passes = ramp_passes, st->ramps = ramps;
|
||||
}
|
||||
if (now - st->window_ns >= AS_WINDOW_NS) {
|
||||
snap = *st;
|
||||
@@ -500,6 +505,7 @@ static void stats_report(AudioRenderer *r, uint64_t now, uint64_t lock_ns, uint6
|
||||
st->queue_min = UINT64_MAX;
|
||||
st->out_underruns = o.underruns, st->out_missing = o.missing_frames, st->out_skipped = o.skipped_frames;
|
||||
st->out_overruns = o.overruns;
|
||||
st->ramp_passes = ramp_passes, st->ramps = ramps;
|
||||
st->upd_n = 0; // upd_last_ns and upd_pass_last stay: the next gap is measured from the last update
|
||||
st->upd_gap_max_ns = st->upd_gap_frame = st->upd_gaps_over = st->upd_passes_max = st->upd_hold_max_ns = 0;
|
||||
st->dry = st->late[0] = st->late[1] = st->late[2] = st->late_max = 0;
|
||||
@@ -516,7 +522,8 @@ static void stats_report(AudioRenderer *r, uint64_t now, uint64_t lock_ns, uint6
|
||||
"frames %llu, ring at least %.1f ms, longest device request gap %.1f ms; render passes %llu, gaps over %llu "
|
||||
"ms %llu (longest %.1f ms), lock waits over %llu ms %llu (longest %.2f ms); game updates %llu, gaps over "
|
||||
"%llu ms %llu (longest %.1f ms at game frame %llu, %llu passes), longest held the lock %.2f ms; voices ran "
|
||||
"dry %llu, next buffer late by 1-2 passes %llu, 3-8 %llu, 9-%d %llu (most %llu)",
|
||||
"dry %llu, next buffer late by 1-2 passes %llu, 3-8 %llu, 9-%d %llu (most %llu); render passes that ramped a "
|
||||
"volume %llu (%llu ramps)",
|
||||
(now - snap.window_ns) / 1e9, (unsigned long long)game_frame(), (unsigned long long)under,
|
||||
(o.missing_frames - snap.out_missing) * 1000.0 / rate, (o.skipped_frames - snap.out_skipped) * 1000.0 / rate,
|
||||
(unsigned long long)(o.overruns - snap.out_overruns),
|
||||
@@ -527,7 +534,8 @@ static void stats_report(AudioRenderer *r, uint64_t now, uint64_t lock_ns, uint6
|
||||
(unsigned long long)snap.upd_gaps_over, snap.upd_gap_max_ns / 1e6, (unsigned long long)snap.upd_gap_frame,
|
||||
(unsigned long long)snap.upd_passes_max, snap.upd_hold_max_ns / 1e6, (unsigned long long)snap.dry,
|
||||
(unsigned long long)snap.late[0], (unsigned long long)snap.late[1], AS_LATE_PASSES,
|
||||
(unsigned long long)snap.late[2], (unsigned long long)snap.late_max);
|
||||
(unsigned long long)snap.late[2], (unsigned long long)snap.late_max,
|
||||
(unsigned long long)(ramp_passes - snap.ramp_passes), (unsigned long long)(ramps - snap.ramps));
|
||||
}
|
||||
|
||||
static void render_thread(void *p) {
|
||||
|
||||
@@ -147,6 +147,15 @@ typedef struct {
|
||||
float biquad_state[2][6][2]; // filter, channel: transposed direct-form II delays
|
||||
uint64_t dry_pass; // NX2NAVI_AUDIO_STATS: 1 + the render pass in which the voice ran out of queued
|
||||
// buffers before its end of stream (audren.c advance_voice), 0 otherwise
|
||||
// Volume ramps (audio_mixer.c audio_mixer_render): what the voice was mixed with in its last render pass. The pass
|
||||
// after it starts every gain there and reaches the new volumes at its end; voice_reset (audren.c) forgets it.
|
||||
uint64_t ramp_pass; // 1 + the render pass the voice was last mixed in, 0 when it has none to ramp from
|
||||
int32_t ramp_mix; // the mix and
|
||||
uint32_t ramp_splitter; // the splitter it went to then, and
|
||||
uint32_t ramp_channels; // its channel count: a pass with another of these starts at the new volumes
|
||||
int32_t ramp_buffers; // into a mix: how many of the mix's buffers ramp_gain holds
|
||||
float ramp_volume; // into a splitter: the voice's volume then (the destinations keep their own)
|
||||
float ramp_gain[6][24]; // into a mix: the gain channel c went into buffer b with (NaN: nothing went in)
|
||||
} ServerVoice;
|
||||
enum { SV_STOPPED, SV_STARTED, SV_PAUSED, SV_STOPPING };
|
||||
|
||||
@@ -191,6 +200,9 @@ int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now);
|
||||
// the last call (which starts that measure again). False without a device.
|
||||
typedef struct { uint64_t underruns, missing_frames, skipped_frames, overruns, queued_frames, pull_gap_max_ns; } AudioOutStats;
|
||||
int audio_mixer_out_stats(AudioRenderer *r, AudioOutStats *o);
|
||||
// NX2NAVI_AUDIO_STATS: the render passes in which at least one volume was ramped (NX2NAVI_AUDIO_RAMP), and the ramps (one
|
||||
// source channel into one mix buffer), since the mixer opened.
|
||||
void audio_mixer_ramp_stats(const AudioRenderer *r, uint64_t *passes, uint64_t *ramps);
|
||||
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames); // source samples over one frame; advances v->fraction
|
||||
int audio_buffer_empty(const AudrenWaveBuffer *b); // no samples to decode: played as soon as it is reached
|
||||
void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs); // IAudioDevice
|
||||
|
||||
@@ -35,7 +35,7 @@ static int save_ok = 1; // the last cfg_save_to wrote (o
|
||||
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
|
||||
static const char *const known[] = {
|
||||
"CORES", "TM_DISPATCH", "NVN2_CS_TABLE", "SKIP_SHADERS", "MOVIE_DEBAND", "MOVIE_FPS", "MOVIE_SKIP", "MOVIE_OVERLAY", "MOVIES", "NPLN", "NVN2_PROGRAMS", "NVN_TRAP", "OCARINA_REALTIME", "ONESHOT_BUDGET", "ONESHOT_WAIT_MS", "OCARINA_STEP", "PHYSICS_STEPS", "PICKUP_COUNT", "STEP_STEADY", "VI_HZ", "VI_VRR", "TIME_DUMP", "SHADER_DUMP", "SHOT_AFTER", "SHOT_DIR", "SHOT_EVERY", // Switch 2 port: movie skip (romfs.c), NVN2 proc traps (nvn_procs.c)
|
||||
"AA", "AA_LOG", "AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CHANNELS", "AUDIO_DEVICE", "AUDIO_GAME_PRIO", "AUDIO_OFF", "AUDIO_PREFILL_MS", "AUDIO_STATS",
|
||||
"AA", "AA_LOG", "AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CHANNELS", "AUDIO_DEVICE", "AUDIO_GAME_PRIO", "AUDIO_OFF", "AUDIO_PREFILL_MS", "AUDIO_RAMP", "AUDIO_STATS",
|
||||
"BARRIER_GAME", "MESH_AHEAD", "MESH_FALLBACK", "MESH_FALLBACK_MB", "MESH_FALLBACK_WORKERS", "PROBE_MESHFB_DIR",
|
||||
"BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT",
|
||||
"BIND_LSTICK_RIGHT", "BIND_LSTICK_UP", "BIND_MINUS",
|
||||
@@ -121,6 +121,7 @@ static const struct { const char *key, *section; } sections[] = {
|
||||
{"MODS_DIR", "mods"}, {"MODS_ORDER", "mods"}, {"MODS_DISABLED", "mods"}, {"MODS_REBUILD", "mods"},
|
||||
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
|
||||
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"}, {"AUDIO_DEVICE", "audio"}, {"MICROPHONE", "audio"}, {"MENU_SOUND_VOLUME", "audio"},
|
||||
{"AUDIO_RAMP", "audio"},
|
||||
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"}, {"DS3", "input"},
|
||||
{"PAD_LAYOUT", "input"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
|
||||
{"STICK_SENS", "input"}, {"DEBUG_MENU", "input"},
|
||||
@@ -231,6 +232,7 @@ static const CfgMeta metas[] = {
|
||||
{"VOLUME", "100", "0..100", LIVE}, {"MENU_SOUND_VOLUME", "80", "0..100", LIVE}, {"AUDIO_CHANNELS", "stereo", "auto|stereo|5.1", LIVE}, // audio_mixer.c audio_channels_live
|
||||
{"AUDIO_DEVICE", "default", NULL, RESTART}, // default|<1-based index>|<part of a device name> (audio_mixer.c resolve_audio_device)
|
||||
{"MICROPHONE", "1", "0|1", RESTART}, // ov_audio_input.c mic_on: 0 refuses the game's input streams
|
||||
{"AUDIO_RAMP", "1", "0|1", RESTART}, // audio_mixer.c accumulate_ramp: a changed volume is reached across the frame
|
||||
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
|
||||
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "nintendo", "auto|nintendo|xbox|playstation", LIVE},
|
||||
{"DS3", "0", "0|1", LIVE}, // input_ds3_win.c ds3_read: read a DualShock 3 directly; off: no HID scan
|
||||
|
||||
293
tests/audio_mixer_check.c
Normal file
293
tests/audio_mixer_check.c
Normal file
@@ -0,0 +1,293 @@
|
||||
/* The mixer's volume ramps (runtime/audio_mixer.c accumulate_ramp, audio_mixer_render), with the rest of the runtime
|
||||
stubbed. No game data and no audio device: NX2NAVI_AUDIO=none mixes without opening one. Each voice plays a wave
|
||||
buffer of one constant sample, so a mix buffer holds the gain the voice went in with, sample by sample. */
|
||||
#include "../runtime/audio_mixer.c"
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
static int fails;
|
||||
#define CHECK(ok, ...) do { if (!(ok)) { fails++; printf("FAIL %s:%d: ", __func__, __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
/* ---- the runtime around the mixer ---- */
|
||||
static int ramp_setting = 1;
|
||||
unsigned rt_trace;
|
||||
void rt_log(const char *fmt, ...) { (void)fmt; }
|
||||
void rt_fatal(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vprintf(fmt, ap);
|
||||
va_end(ap);
|
||||
printf("\n");
|
||||
exit(2);
|
||||
}
|
||||
uint64_t kern_now_ns(void) { return 1; }
|
||||
uint32_t kern_event_new(void) { return 1; }
|
||||
uint32_t kern_event_readable(uint32_t writable) { return writable; }
|
||||
void kern_event_signal(uint32_t writable) { (void)writable; }
|
||||
void *ipc_guest(uint64_t addr, uint64_t size) { (void)size; return (void *)(uintptr_t)addr; }
|
||||
uint32_t ipc_session_new(const char *name, IpcHandler handler, void *state, void (*close)(void *state)) {
|
||||
(void)name, (void)handler, (void)state, (void)close;
|
||||
return 1;
|
||||
}
|
||||
void ipc_unimplemented(const IpcSession *s, const IpcRequest *rq) { (void)s, (void)rq; exit(2); }
|
||||
void ipc_out(IpcResponse *rs, const void *p, uint32_t n) { (void)rs, (void)p, (void)n; }
|
||||
const void *ipc_arg(const IpcRequest *rq, uint32_t off, uint32_t n) { (void)rq, (void)off, (void)n; exit(2); }
|
||||
void menu_sound_mix(float *out, uint32_t frames, int channels, int rate) { (void)out, (void)frames, (void)channels, (void)rate; }
|
||||
const char *cfg_get(const char *name) { return !strcmp(name, "NX2NAVI_AUDIO") ? "none" : NULL; }
|
||||
const char *cfg_value(const char *name) { return !strcmp(name, "NX2NAVI_AUDIO_CHANNELS") ? "stereo" : "default"; }
|
||||
int cfg_bool(const char *name) {
|
||||
if (!strcmp(name, "NX2NAVI_AUDIO_RAMP")) return ramp_setting;
|
||||
printf("unexpected setting %s\n", name);
|
||||
exit(2);
|
||||
}
|
||||
double cfg_double(const char *name) { (void)name; return 100; }
|
||||
unsigned cfg_version(void) { return 1; }
|
||||
int audio_buffer_empty(const AudrenWaveBuffer *b) { return !b->address || !b->size || b->end <= b->start; }
|
||||
/* no effects in these renderers */
|
||||
AudioEffectDsp *audio_effect_create(unsigned type, const void *param, unsigned revision) { (void)type, (void)param, (void)revision; return NULL; }
|
||||
void audio_effect_free(AudioEffectDsp *s) { (void)s; }
|
||||
void audio_effect_update(AudioEffectDsp *s, const void *param) { (void)s, (void)param; }
|
||||
void audio_effect_process(AudioEffectDsp *s, float *const input[6], float *const output[6], int frames) { (void)s, (void)input, (void)output, (void)frames; }
|
||||
void audio_effect_statistics(AudioEffectDsp *s, void *output, unsigned bytes) { (void)s, (void)output, (void)bytes; }
|
||||
Reverb3d *audio_reverb_create(const Reverb3dParam *p) { (void)p; return NULL; }
|
||||
void audio_reverb_free(Reverb3d *s) { (void)s; }
|
||||
void audio_reverb_update(Reverb3d *s, const Reverb3dParam *p) { (void)s, (void)p; }
|
||||
void audio_reverb_process(Reverb3d *s, float *const input[6], float *const output[6], int frames) { (void)s, (void)input, (void)output, (void)frames; }
|
||||
|
||||
/* ---- a renderer: the final mix (4 buffers, the mixer's buffers 0-3), one submix (2 buffers, 4-5) that goes
|
||||
nowhere, three voices, one splitter with one destination ---- */
|
||||
#define FRAME 240
|
||||
#define SUBMIX 4 /* the submix's first buffer */
|
||||
static int16_t pcm[4 * FRAME]; /* every sample 16384: 0.5 */
|
||||
|
||||
static AudioRenderer *renderer_open(void) {
|
||||
AudioRenderer *r = calloc(1, sizeof *r);
|
||||
r->param = (AudrenParameter){.sample_rate = 48000, .sample_count = FRAME, .mix_buffer_count = 8, .submix_count = 1,
|
||||
.voice_count = 3, .sink_count = 1, .effect_count = 0, .splitter_count = 1,
|
||||
.splitter_destination_count = 1, .revision = AUDREN_REV(11)};
|
||||
r->revision = r->param.revision;
|
||||
r->voices = calloc(3, sizeof *r->voices);
|
||||
r->mixes = calloc(2, sizeof *r->mixes);
|
||||
r->sinks = calloc(1, sizeof *r->sinks);
|
||||
r->effects = calloc(1, sizeof *r->effects);
|
||||
r->channels = calloc(3 * 6, sizeof *r->channels);
|
||||
r->mixes[0] = (AudrenMixIn){.volume = 1, .sample_rate = 48000, .buffer_count = 4, .in_use = 1, .mix_id = 0,
|
||||
.dest_mix_id = -1, .dest_splitter_id = SPLITTER_UNUSED};
|
||||
r->mixes[1] = (AudrenMixIn){.volume = 1, .sample_rate = 48000, .buffer_count = 2, .in_use = 1, .mix_id = 1,
|
||||
.dest_mix_id = 0, .dest_splitter_id = SPLITTER_UNUSED};
|
||||
r->sinks[0] = (AudrenSinkIn){.type = SINK_DEVICE, .in_use = 1, .input_count = 2, .inputs = {0, 1}};
|
||||
for (size_t i = 0; i < sizeof pcm / sizeof *pcm; i++) pcm[i] = 16384;
|
||||
audio_mixer_open(r);
|
||||
audio_mixer_set_running(r, 1);
|
||||
return r;
|
||||
}
|
||||
|
||||
static void renderer_close(AudioRenderer *r) {
|
||||
audio_mixer_close(r);
|
||||
free(r->voices), free(r->mixes), free(r->sinks), free(r->effects), free(r->channels), free(r);
|
||||
}
|
||||
|
||||
/* One render pass, counted as audren.c render_thread counts it. The voices are not advanced: every sample is the same. */
|
||||
static void pass(AudioRenderer *r) {
|
||||
audio_mixer_render(r, (double)FRAME / 48000);
|
||||
r->elapsed_frames++;
|
||||
}
|
||||
|
||||
/* A voice as the game's update leaves a new one (audren.c update_voice with is_new: voice_reset, then started). Of
|
||||
what the slot's last voice was mixed with, voice_reset clears only ramp_pass, so the rest is kept here too. */
|
||||
static ServerVoice *voice_new(AudioRenderer *r, int i, float volume, int32_t mix, uint32_t splitter) {
|
||||
ServerVoice *v = &r->voices[i], was = *v;
|
||||
memset(v, 0, sizeof *v);
|
||||
v->ramp_mix = was.ramp_mix, v->ramp_splitter = was.ramp_splitter, v->ramp_channels = was.ramp_channels;
|
||||
v->ramp_buffers = was.ramp_buffers, v->ramp_volume = was.ramp_volume;
|
||||
memcpy(v->ramp_gain, was.ramp_gain, sizeof v->ramp_gain);
|
||||
v->in.id = (uint32_t)i, v->in.in_use = 1, v->in.play_state = PLAY_STARTED, v->in.sample_format = FMT_PCM16;
|
||||
v->in.sample_rate = 48000, v->in.channel_count = 1, v->in.pitch = 1, v->in.volume = volume;
|
||||
v->in.mix_id = mix, v->in.splitter_id = splitter;
|
||||
v->in.channel_ids[0] = i * 6;
|
||||
v->state = SV_STARTED;
|
||||
v->buffers[0] = (AudrenWaveBuffer){.address = (uintptr_t)pcm, .size = sizeof pcm, .start = 0, .end = 4 * FRAME};
|
||||
v->valid[0] = 1;
|
||||
return v;
|
||||
}
|
||||
|
||||
/* The splitter section of an update (REV11 layout): splitter 0 with destination 0, which goes to `mix` with `volume`
|
||||
on that mix's buffer `buffer`. */
|
||||
static void splitter_set(AudioRenderer *r, int used, int32_t mix, int buffer, float volume) {
|
||||
uint8_t wire[0x20 + 16 + 4 + 12 + 0x70 + 12] = {0};
|
||||
uint32_t header[3] = {0x48444e53u, 1, 1}, info[4] = {0x49444e53u, 0, 0, 1}, dest[2] = {0x44444e53u, 0};
|
||||
int32_t id = 0;
|
||||
float volumes[24] = {0};
|
||||
volumes[buffer] = volume;
|
||||
memcpy(wire, header, sizeof header);
|
||||
memcpy(wire + 0x20, info, sizeof info);
|
||||
memcpy(wire + 0x30, &id, 4);
|
||||
uint8_t *d = wire + 0x40;
|
||||
memcpy(d, dest, sizeof dest);
|
||||
memcpy(d + 8, volumes, sizeof volumes);
|
||||
memcpy(d + 0x68, &mix, 4);
|
||||
d[0x6c] = (uint8_t)used;
|
||||
AudrenMixIn mixes[2] = {r->mixes[0], r->mixes[1]};
|
||||
audio_mixer_update_routing(r, wire, sizeof wire, (const uint8_t *)mixes, sizeof mixes, 2);
|
||||
}
|
||||
|
||||
static const float *buf(AudioRenderer *r, int b) { return mixbuf(r->mixer, b); }
|
||||
|
||||
/* Every sample of the pass went in with the same gain. */
|
||||
static int flat(const float *d, float level) {
|
||||
for (int k = 0; k < FRAME; k++)
|
||||
if (fabsf(d[k] - level) > 1e-6f) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* The pass went from `from` to `to` in equal steps: sample k on the line from + k (to - from) / FRAME, so the first
|
||||
sample is the level before and the pass after continues at `to`. */
|
||||
static int ramped(const float *d, float from, float to) {
|
||||
for (int k = 0; k < FRAME; k++) {
|
||||
if (fabsf(d[k] - (from + (to - from) * (float)k / FRAME)) > 2e-5f) return 0;
|
||||
if (k && to > from ? d[k] <= d[k - 1] : k && d[k] >= d[k - 1]) return 0;
|
||||
}
|
||||
return d[0] == from;
|
||||
}
|
||||
|
||||
static void voice_into_mix(void) {
|
||||
AudioRenderer *r = renderer_open();
|
||||
uint64_t passes, ramps;
|
||||
r->channels[0].mix[0] = 1, r->channels[0].mix[1] = 0.5f;
|
||||
ServerVoice *v = voice_new(r, 0, 0, 0, SPLITTER_UNUSED);
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, 0), 0) && flat(buf(r, 1), 0), "a voice at volume 0 is silent");
|
||||
v->in.volume = 1;
|
||||
pass(r);
|
||||
CHECK(ramped(buf(r, 0), 0, 0.5f), "volume 0 to 1: the pass rises in equal steps (first %g, middle %g, last %g)",
|
||||
buf(r, 0)[0], buf(r, 0)[FRAME / 2], buf(r, 0)[FRAME - 1]);
|
||||
CHECK(ramped(buf(r, 1), 0, 0.25f), "each buffer of the mix ramps to its own gain");
|
||||
CHECK(flat(buf(r, 2), 0) && flat(buf(r, 3), 0), "buffers the voice does not go into stay silent");
|
||||
audio_mixer_ramp_stats(r, &passes, &ramps);
|
||||
CHECK(passes == 1 && ramps == 2, "one pass ramped, two buffers (%llu, %llu)", (unsigned long long)passes, (unsigned long long)ramps);
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, 0), 0.5f) && flat(buf(r, 1), 0.25f), "the pass after the step holds the new volume");
|
||||
audio_mixer_ramp_stats(r, &passes, &ramps);
|
||||
CHECK(passes == 1 && ramps == 2, "an unchanged volume is not counted as a ramp");
|
||||
r->channels[0].mix[0] = 0.5f; /* the channel's mix volume moves, the voice's does not */
|
||||
pass(r);
|
||||
CHECK(ramped(buf(r, 0), 0.5f, 0.25f) && flat(buf(r, 1), 0.25f), "a changed mix volume ramps, the other buffer does not");
|
||||
v->in.volume = 0;
|
||||
pass(r);
|
||||
CHECK(ramped(buf(r, 0), 0.25f, 0) && ramped(buf(r, 1), 0.25f, 0), "a volume set to 0 fades across the pass");
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, 0), 0), "and is silent after it");
|
||||
|
||||
/* a voice that starts is not ramped, whatever the slot held before */
|
||||
r->channels[6].mix[2] = 1;
|
||||
voice_new(r, 1, 1, 0, SPLITTER_UNUSED);
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, 2), 0.5f), "a new voice starts at its volume (first sample %g)", buf(r, 2)[0]);
|
||||
r->voices[1].in.volume = 0.5f;
|
||||
pass(r);
|
||||
CHECK(ramped(buf(r, 2), 0.5f, 0.25f), "and ramps from its second pass on");
|
||||
voice_new(r, 1, 1, 0, SPLITTER_UNUSED);
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, 2), 0.5f), "a voice started again in the same slot starts at its volume");
|
||||
|
||||
/* not mixed in the pass before: paused, then played at another volume */
|
||||
r->voices[1].state = SV_PAUSED;
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, 2), 0), "a paused voice is silent");
|
||||
r->voices[1].state = SV_STARTED, r->voices[1].in.volume = 0.25f;
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, 2), 0.125f), "a voice that was silent in the pass before starts at its volume");
|
||||
|
||||
/* another destination: the submix */
|
||||
r->voices[1].in.mix_id = 1, r->voices[1].in.volume = 1;
|
||||
r->channels[6].mix[0] = 1;
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, SUBMIX), 0.5f) && flat(buf(r, 2), 0), "a voice moved to another mix starts at its volume there");
|
||||
r->voices[1].in.volume = 0.5f;
|
||||
pass(r);
|
||||
CHECK(ramped(buf(r, SUBMIX), 0.5f, 0.25f), "and ramps there afterwards");
|
||||
|
||||
/* the renderer stopped and started: the pass before is not the sound before */
|
||||
audio_mixer_set_running(r, 0);
|
||||
audio_mixer_set_running(r, 1);
|
||||
r->voices[1].in.volume = 1;
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, SUBMIX), 0.5f), "the first pass after a start takes every volume as it is");
|
||||
renderer_close(r);
|
||||
}
|
||||
|
||||
static void voice_into_splitter(void) {
|
||||
AudioRenderer *r = renderer_open();
|
||||
splitter_set(r, 1, 0, 3, 0);
|
||||
ServerVoice *v = voice_new(r, 2, 1, 0, 0);
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, 3), 0), "a destination at volume 0 is silent");
|
||||
splitter_set(r, 1, 0, 3, 1);
|
||||
pass(r);
|
||||
CHECK(ramped(buf(r, 3), 0, 0.5f), "destination volume 0 to 1: the pass rises in equal steps (first %g, last %g)",
|
||||
buf(r, 3)[0], buf(r, 3)[FRAME - 1]);
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, 3), 0.5f), "the pass after the step holds the new volume");
|
||||
v->in.volume = 0.5f;
|
||||
pass(r);
|
||||
CHECK(ramped(buf(r, 3), 0.5f, 0.25f), "the voice's own volume ramps through the destination");
|
||||
v->in.volume = 1;
|
||||
splitter_set(r, 1, 0, 3, 0.5f); /* both move: from the product before to the product now, here the same gain */
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, 3), 0.25f), "voice volume doubled and destination volume halved: the gain does not move");
|
||||
|
||||
splitter_set(r, 1, 1, 0, 1); /* the destination goes to the submix */
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, SUBMIX), 0.5f) && flat(buf(r, 3), 0), "a destination moved to another mix starts at its volume there");
|
||||
splitter_set(r, 0, 1, 0, 1);
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, SUBMIX), 0), "an unused destination is silent");
|
||||
splitter_set(r, 1, 1, 0, 0.5f);
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, SUBMIX), 0.25f), "a destination used again starts at its volume");
|
||||
splitter_set(r, 1, 1, 0, 1);
|
||||
voice_new(r, 2, 1, 0, 0);
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, SUBMIX), 0.5f), "a new voice into a destination whose volume moved starts at the new gain");
|
||||
renderer_close(r);
|
||||
}
|
||||
|
||||
/* A submix into a splitter ramps the destination's volumes like a voice does. */
|
||||
static void mix_into_splitter(void) {
|
||||
AudioRenderer *r = renderer_open();
|
||||
r->channels[0].mix[0] = 1;
|
||||
r->mixes[1].dest_splitter_id = 0, r->mixes[1].buffer_count = 1;
|
||||
splitter_set(r, 1, 0, 3, 0.5f);
|
||||
voice_new(r, 0, 1, 1, SPLITTER_UNUSED);
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, SUBMIX), 0.5f) && flat(buf(r, 3), 0.25f), "the submix reaches the final mix through the destination");
|
||||
splitter_set(r, 1, 0, 3, 1);
|
||||
pass(r);
|
||||
CHECK(ramped(buf(r, 3), 0.25f, 0.5f), "the destination's volume ramps for a mix");
|
||||
r->mixes[1].volume = 0.5f;
|
||||
pass(r);
|
||||
CHECK(ramped(buf(r, 3), 0.5f, 0.25f), "and so does the mix's own volume");
|
||||
renderer_close(r);
|
||||
}
|
||||
|
||||
static void ramps_off(void) {
|
||||
ramp_setting = 0;
|
||||
AudioRenderer *r = renderer_open();
|
||||
r->channels[0].mix[0] = 1;
|
||||
ServerVoice *v = voice_new(r, 0, 0, 0, SPLITTER_UNUSED);
|
||||
pass(r);
|
||||
v->in.volume = 1;
|
||||
pass(r);
|
||||
CHECK(flat(buf(r, 0), 0.5f), "NX2NAVI_AUDIO_RAMP=0: the new volume applies to the whole pass");
|
||||
renderer_close(r);
|
||||
ramp_setting = 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
voice_into_mix();
|
||||
voice_into_splitter();
|
||||
mix_into_splitter();
|
||||
ramps_off();
|
||||
printf("audio_mixer_check: %s\n", fails ? "FAIL" : "ok");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
42
tests/audio_mixer_check.py
Normal file
42
tests/audio_mixer_check.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Build and run tests/audio_mixer_check.c: runtime/audio_mixer.c with the rest of the runtime stubbed. No game files
|
||||
and no audio device (the mixer runs as with NX2NAVI_AUDIO=none); SDL3 is only linked.
|
||||
|
||||
Windows: python tests\\audio_mixer_check.py [--toolchain C:/msys64/ucrt64]
|
||||
Linux: python tests/audio_mixer_check.py (clang and SDL3 development files via pkg-config)
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MSYS = Path(r'C:\msys64\ucrt64')
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument('--toolchain', type=Path, default=MSYS if os.name == 'nt' and MSYS.exists() else None)
|
||||
a = ap.parse_args()
|
||||
work = ROOT / 'build' / 'test' / 'audio_mixer'
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
env = dict(os.environ)
|
||||
if a.toolchain:
|
||||
env['PATH'] = str(a.toolchain / 'bin') + os.pathsep + env.get('PATH', '')
|
||||
cc = os.environ.get('CC') or str(a.toolchain / 'bin' / 'clang.exe')
|
||||
sdl = ['-L', str(a.toolchain / 'lib'), '-lSDL3']
|
||||
else:
|
||||
cc = os.environ.get('CC') or shutil.which('clang') or 'cc'
|
||||
sdl = shlex.split(subprocess.check_output(['pkg-config', '--cflags', '--libs', 'sdl3'], text=True))
|
||||
exe = work / ('audio_mixer_check.exe' if os.name == 'nt' else 'audio_mixer_check')
|
||||
# the default build's target (x86-64-v3: AVX2 and FMA), so the mixing loops are compiled as they ship
|
||||
subprocess.check_call([cc, '-std=gnu11', '-O2', '-fno-strict-aliasing', '-march=x86-64-v3', '-Wall',
|
||||
'-Wno-unused-function', '-I', str(ROOT / 'runtime'),
|
||||
str(ROOT / 'tests' / 'audio_mixer_check.c'), '-o', str(exe), *sdl, '-lm'], env=env)
|
||||
return subprocess.run([str(exe)], env=env, timeout=60).returncode
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user