Files
NX2navi/tests/audio_mixer_check.c
chrg6 7e7fed639e Refuse or leave out the audio values the game sends that the renderer cannot use, instead of ending the game on them.
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>
2026-11-13 20:13:02 -05:00

442 lines
23 KiB
C

/* The mixer's volume ramps (runtime/audio_mixer.c accumulate_ramp, audio_mixer_render), what it owes the device
after a stall (ring_pull, audio_mixer_owed) and the device change events (change_event_of, audio_channels_live),
with the rest of the runtime stubbed. No game data and no audio device:
NX2NAVI_AUDIO=none mixes without opening one, and the device's requests are made by hand. 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; }
/* kernel events: numbered from 1; which were closed, signalled, and how many readable ends were handed out */
static int events_made, readables, closed[64], signalled[64], signal_closed;
static volatile uint32_t *race_kind; /* set: the next kern_event_new lets "another thread" win that kind first */
uint32_t kern_event_new(void) {
if (race_kind) {
volatile uint32_t *k = race_kind;
race_kind = NULL;
uint32_t other = kern_event_new();
change_event_list(other);
*k = other;
}
return (uint32_t)++events_made;
}
uint32_t kern_event_readable(uint32_t writable) { readables++; return writable; }
void kern_event_signal(uint32_t writable) {
if (writable < 64 && closed[writable]) signal_closed++;
if (writable < 64) signalled[writable]++;
}
void kern_handle_close(uint32_t h) { if (h < 64) closed[h] = 1; }
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; }
static const char *audio_mode = "none", *channels_setting = "stereo";
const char *cfg_get(const char *name) { return !strcmp(name, "NX2NAVI_AUDIO") ? audio_mode : NULL; }
const char *cfg_value(const char *name) { return !strcmp(name, "NX2NAVI_AUDIO_CHANNELS") ? channels_setting : "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, and no value the mixer leaves out (audren.c audio_bad: counted, none expected) */
static int bad_calls;
void audio_bad(AudioRenderer *r, int kind, const char *fmt, ...) { (void)r, (void)kind, (void)fmt; bad_calls++; }
const char *audio_effect_invalid(unsigned type, const void *param) { (void)type, (void)param; return NULL; }
const char *audio_reverb_invalid(const Reverb3dParam *p) { (void)p; return NULL; }
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; `drop`: a catch-up frame. The voices are not
advanced: every sample is the same. */
static void render(AudioRenderer *r, int drop) {
audio_mixer_render(r, (double)FRAME / 48000, drop);
r->elapsed_frames++;
}
static void pass(AudioRenderer *r) { render(r, 0); }
/* 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;
}
/* What the device paces: the device's requests (ring_pull, the SDL callback's part) and the render thread's question
(audio_mixer_owed) by hand, on a mixer that takes itself to have a device. */
static void catch_up(void) {
AudioRenderer *r = renderer_open();
Mixer *m = r->mixer;
enum { PERIOD = 2 * FRAME };
static float out[2 * (PERIOD + FRAME)];
const int prefill = m->prefill; /* render frames the output waits for; even on every platform */
m->device_ok = 1, m->period = PERIOD, m->target = prefill * FRAME;
r->channels[0].mix[0] = 1, r->channels[0].mix[1] = 1;
ServerVoice *v = voice_new(r, 0, 1, 0, SPLITTER_UNUSED);
ring_pull(m, out, PERIOD);
ring_pull(m, out, PERIOD);
CHECK(out[0] == 0 && audio_mixer_owed(r) == 0, "the silence before the first prefill is not owed");
for (int i = 0; i < prefill; i++) pass(r);
uint64_t queued = atomic_load(&m->wr) - atomic_load(&m->rd);
CHECK(queued == (uint64_t)prefill * FRAME, "the frames are queued for the device (%llu samples)", (unsigned long long)queued);
for (int i = 0; i < prefill; i += 2) {
ring_pull(m, out, PERIOD);
CHECK(out[0] == 0.5f && out[2 * PERIOD - 1] == 0.5f, "the device plays what was queued");
}
CHECK(audio_mixer_owed(r) == 0, "nothing is owed while the ring keeps up");
/* the render thread is held up for three device periods: an underrun, then silence until the prefill is back */
for (int i = 0; i < 3; i++) ring_pull(m, out, PERIOD);
CHECK(out[0] == 0 && atomic_load(&m->underruns) == 1, "the device plays silence");
v->in.volume = 0.5f; /* the game's update that came meanwhile */
for (int i = 0; i < 6; i++) {
uint64_t wr = atomic_load(&m->wr), frames = r->elapsed_frames;
CHECK(audio_mixer_owed(r) == 1, "three periods of silence: render frame %d of 6 is owed", i + 1);
render(r, 1);
CHECK(atomic_load(&m->wr) == wr && r->elapsed_frames == frames + 1, "a catch-up frame is rendered and not queued");
if (i == 0) CHECK(ramped(buf(r, 0), 0.5f, 0.25f), "it is mixed in full: the volume the game set ramps in it");
}
CHECK(audio_mixer_owed(r) == 0, "after six frames nothing more is owed");
for (int i = 0; i < prefill; i++) pass(r);
queued = atomic_load(&m->wr) - atomic_load(&m->rd);
CHECK(queued == (uint64_t)prefill * FRAME, "the ring is filled to the same target afterwards");
ring_pull(m, out, PERIOD);
CHECK(out[0] == 0.25f && out[2 * PERIOD - 1] == 0.25f, "the device goes on with the frames after the dropped ones");
CHECK(audio_mixer_owed(r) == 0, "the device's requests while the ring keeps up add nothing");
/* less than a render frame of silence waits for the next gap */
for (int i = 2; i < prefill; i += 2) ring_pull(m, out, PERIOD); /* the ring is empty again, */
for (int i = 0; i < prefill; i++) pass(r); /* and filled */
for (int i = 2; i < prefill; i += 2) ring_pull(m, out, PERIOD); /* down to one period */
ring_pull(m, out, PERIOD + 100); /* a request for 100 samples more than that */
CHECK(atomic_load(&m->underruns) == 2 && audio_mixer_owed(r) == 0, "100 samples of silence are not a frame yet");
ring_pull(m, out, 200); /* still waiting for the prefill: 300 in all */
CHECK(audio_mixer_owed(r) == 1 && audio_mixer_owed(r) == 0, "300 samples of silence: one frame is owed, 60 samples stay");
/* a stop forgets what was owed, and the wait for the prefill after the start is not owed */
for (int i = 0; i < 3; i++) ring_pull(m, out, PERIOD);
audio_mixer_set_running(r, 0);
ring_pull(m, out, PERIOD);
audio_mixer_set_running(r, 1);
ring_pull(m, out, PERIOD);
ring_pull(m, out, PERIOD);
CHECK(audio_mixer_owed(r) == 0, "nothing is owed across a stop");
/* more than AUDIO_CATCHUP_MAX_MS of silence is not caught up */
for (int i = 0; i < prefill; i++) pass(r);
for (int i = 0; i < prefill / 2 + 1 + 48000 / PERIOD * (AUDIO_CATCHUP_MAX_MS + 100) / 1000; i++) ring_pull(m, out, PERIOD);
CHECK(audio_mixer_owed(r) == -1, "over %d ms of silence is given up", AUDIO_CATCHUP_MAX_MS);
CHECK(audio_mixer_owed(r) == 0, "and nothing of it is owed afterwards");
m->device_ok = 0;
renderer_close(r);
}
/* The device's, the two notifications' and the virtual device manager's change events: one event per kind however
often the game asks, a readable end per request, and a channel change signals each once. Two first requests at once:
the loser's event is closed and never signalled. */
static void change_events_once(void) {
int made0 = events_made, read0 = readables;
static const unsigned device_cmds[] = {4, 11, 12, 15, 17};
for (int round = 0; round < 10; round++) {
for (unsigned i = 0; i < sizeof device_cmds / sizeof *device_cmds; i++) {
IpcRequest rq = {.cmd = device_cmds[i]};
IpcResponse rs = {0};
audio_device_handler(NULL, &rq, &rs);
CHECK(rs.ncopy == 1 && rs.copy[0], "command %u hands out a readable event", device_cmds[i]);
}
IpcRequest rq = {.cmd = 500};
IpcResponse rs = {0};
virtual_device_manager_handler(NULL, &rq, &rs);
CHECK(rs.ncopy == 1 && rs.copy[0], "the virtual device manager hands out a readable event");
}
CHECK(events_made - made0 == 4, "one event per kind: %d made for 60 requests", events_made - made0);
CHECK(readables - read0 == 60, "a readable end per request (%d)", readables - read0);
CHECK(atomic_load(&n_change_events) == 4, "four events listed (%d)", atomic_load(&n_change_events));
/* a lost race: the loser closes its own event, which is not listed */
int listed = atomic_load(&n_change_events);
static volatile uint32_t raced;
race_kind = &raced;
uint32_t got = change_event_of(&raced);
CHECK(got == raced && !closed[got], "the loser takes the winner's event");
CHECK(closed[events_made], "the loser closes its own event");
CHECK(atomic_load(&n_change_events) == listed + 1, "only the winner's event is listed");
/* a channel change signals every listed event once, never a closed one */
channels_setting = "stereo";
audio_channels_live();
CHECK(output_channels() == 2, "stereo decided");
audio_mode = "sdl", channels_setting = "5.1";
audio_channels_live();
int n = atomic_load(&n_change_events), once = 0;
for (int i = 0; i < n && i < 16; i++) once += signalled[change_events[i]] == 1;
CHECK(once == n, "a channel change signals each listed event once (%d of %d)", once, n);
CHECK(!signal_closed, "no closed event is signalled");
/* a slot counted whose handle is not stored yet (a kind's first request on another thread): skipped, never
signalled as handle 0, which the kernel answers with a fatal error */
atomic_fetch_add(&n_change_events, 1);
channels_setting = "stereo";
audio_channels_live();
CHECK(output_channels() == 2, "back to stereo");
CHECK(!signalled[0], "a slot not stored yet is skipped (handle 0 signalled %d times)", signalled[0]);
CHECK(signalled[change_events[0]] == 2, "the listed events are signalled again (%d)", signalled[change_events[0]]);
audio_mode = "none", channels_setting = "stereo";
}
int main(void) {
voice_into_mix();
voice_into_splitter();
mix_into_splitter();
ramps_off();
catch_up();
change_events_once();
CHECK(!bad_calls, "the mixer left %d values of these renderers out", bad_calls);
printf("audio_mixer_check: %s\n", fails ? "FAIL" : "ok");
return fails ? 1 : 0;
}