mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The review found six texts that claimed more than the code does. NX2NAVI_AUDIO_STRICT=1 does not bring 0.10.1 back
exactly: it also ends the process on a limiter attack or release that is not a number and on a mix in use with a
negative buffer count, which 0.10.1 let through, so the cfg.c comment, nx2navi.ini.example, the audren.h audio_bad
comment and the test's comment now say it does so for most of these values. Its FATAL line described the non-strict
outcome ("its sound passes through unchanged", "the renderer goes on") and then ended the process; it now ends with
"NX2NAVI_AUDIO_STRICT=1 ends the process here instead". The audren.c header and docs/architecture.md said nothing the
game sends ends the process, while OpenAudioRenderer's parameters and the request's buffer descriptors still do; both
now say a value in the game's RequestUpdate does not, and name the two that still do. The first log line of a kind
gave "N such values so far", but bad_count counts every update and every render pass that meets any kind, so it now
reads "N times so far for all kinds together"; the renderer_close line and the bad_count comment say the same.
docs/building.md and the test's comment called 0x5299 the one result the SDK hands back; the SDK hands back 0x5299,
0x5499 and 0x5699 and aborts on any other. Texts only: no behaviour changes.
The first commit's message says any other result than 0x5299 makes the SDK abort; it should read any result but
0x5299, 0x5499 and 0x5699. The branch's history is kept, so this message carries the correction.
audio_params_check (the strict runs still end with "FATAL: audio:" and the switch's name), cfg_rows_check (0
problems) and check_content passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
278 lines
16 KiB
C
278 lines
16 KiB
C
// Contains code Copyright (c) 2026 MacNativeGames contributors (MIT; see THIRD_PARTY_NOTICES.md).
|
|
// Wire layouts follow the public audren:u wire format.
|
|
// audren:u, the audio renderer service: RequestUpdate wire formats, the server-side
|
|
// renderer state, and the mixer interface (audio_mixer.c: the real mixer with SDL3 output).
|
|
#pragma once
|
|
#include <stdint.h>
|
|
|
|
#include "ipc.h"
|
|
#include "platform/platform.h"
|
|
|
|
typedef struct {
|
|
int32_t sample_rate, sample_count, mix_buffer_count, submix_count, voice_count, sink_count, effect_count,
|
|
performance_frame_count;
|
|
uint8_t voice_drop_enabled, unknown, rendering_device, execution_mode;
|
|
int32_t splitter_count, splitter_destination_count;
|
|
uint32_t external_context_size, revision;
|
|
} AudrenParameter;
|
|
_Static_assert(sizeof(AudrenParameter) == 0x34, "AudioRendererParameterInternal");
|
|
|
|
typedef struct {
|
|
uint32_t revision, behavior, memory_pools, voices, voice_resources, effects, mixes, sinks, performance,
|
|
splitters, render_info, reserved[4], total;
|
|
} AudrenHeader;
|
|
_Static_assert(sizeof(AudrenHeader) == 0x40, "UpdateDataHeader");
|
|
|
|
typedef struct { uint64_t address, size; uint32_t state, pad[3]; } AudrenPoolIn;
|
|
typedef struct { uint32_t state, pad[3]; } AudrenPoolOut;
|
|
enum { POOL_INVALID, POOL_NEW, POOL_REQUEST_DETACH, POOL_DETACHED, POOL_REQUEST_ATTACH, POOL_ATTACHED, POOL_RELEASED };
|
|
|
|
typedef struct { uint32_t id; float mix[24]; uint8_t in_use, pad[11]; } AudrenChannelIn;
|
|
_Static_assert(sizeof(AudrenChannelIn) == 0x70, "voice channel resource");
|
|
|
|
typedef struct { uint8_t enable, pad; int16_t numerator[3], denominator[2]; } AudrenBiquad;
|
|
typedef struct { uint8_t enable, pad[3]; float numerator[3], denominator[2]; } AudrenBiquadFloat;
|
|
_Static_assert(sizeof(AudrenBiquadFloat) == 0x18, "REV15 float biquad");
|
|
|
|
typedef struct {
|
|
uint64_t address, size;
|
|
int32_t start, end;
|
|
uint8_t loop, end_of_stream, sent, pad;
|
|
int32_t loop_count;
|
|
uint64_t context_address, context_size;
|
|
int32_t loop_start, loop_end;
|
|
} AudrenWaveBuffer;
|
|
_Static_assert(sizeof(AudrenWaveBuffer) == 0x38, "wave buffer");
|
|
|
|
typedef struct {
|
|
uint32_t id, node_id;
|
|
uint8_t is_new, in_use, play_state, sample_format;
|
|
uint32_t sample_rate;
|
|
int32_t priority, sort_order;
|
|
uint32_t channel_count;
|
|
float pitch, volume;
|
|
AudrenBiquad biquads[2];
|
|
uint32_t wave_buffer_count;
|
|
uint16_t wave_buffer_index, pad0;
|
|
uint32_t pad1;
|
|
uint64_t extra_address, extra_size;
|
|
int32_t mix_id;
|
|
uint32_t splitter_id;
|
|
AudrenWaveBuffer wave_buffers[4];
|
|
int32_t channel_ids[6];
|
|
uint8_t clear_voice_drop, flush_wave_buffer_count, pad2[2], flags, pad3, src_quality, pad4[17];
|
|
} AudrenVoiceIn;
|
|
_Static_assert(sizeof(AudrenVoiceIn) == 0x170, "voice in parameter");
|
|
// REV15 widens only the two filter records (Q14 -> float); the fields after them move by 24 bytes.
|
|
typedef struct { uint8_t prefix[0x24]; AudrenBiquadFloat biquads[2]; uint8_t tail[0x134]; } AudrenVoiceInV2;
|
|
_Static_assert(sizeof(AudrenVoiceInV2) == 0x188, "REV15 voice in parameter");
|
|
typedef struct { uint64_t played_samples; uint32_t wave_buffers_consumed, voice_drops; } AudrenVoiceOut;
|
|
enum { PLAY_STARTED, PLAY_STOPPED, PLAY_PAUSED };
|
|
enum { FMT_INVALID, FMT_PCM8, FMT_PCM16, FMT_PCM24, FMT_PCM32, FMT_PCMF32, FMT_ADPCM };
|
|
|
|
typedef struct {
|
|
float volume;
|
|
uint32_t sample_rate;
|
|
int32_t buffer_count;
|
|
uint8_t in_use, dirty, pad0[2];
|
|
int32_t mix_id;
|
|
uint32_t effect_count, node_id, pad1[2];
|
|
float mix[24][24];
|
|
int32_t dest_mix_id;
|
|
uint32_t dest_splitter_id, pad2;
|
|
} AudrenMixIn;
|
|
_Static_assert(sizeof(AudrenMixIn) == 0x930, "mix in parameter");
|
|
|
|
typedef struct {
|
|
uint8_t type, in_use, pad0[2];
|
|
uint32_t node_id;
|
|
uint64_t pad1[3];
|
|
char name[0x100];
|
|
uint32_t input_count;
|
|
int8_t inputs[6];
|
|
uint8_t pad2, downmix_enabled;
|
|
float downmix[4];
|
|
} AudrenSinkIn;
|
|
_Static_assert(sizeof(AudrenSinkIn) == 0x140, "sink in parameter");
|
|
typedef struct { uint32_t last_written_offset, pad0; uint64_t pad1[3]; } AudrenSinkOut;
|
|
enum { SINK_INVALID, SINK_DEVICE, SINK_CIRCULAR };
|
|
|
|
typedef struct {
|
|
uint8_t type, is_new, enabled, pad0;
|
|
int32_t mix_id;
|
|
uint64_t buffer_address, buffer_size;
|
|
int32_t order;
|
|
uint32_t pad1;
|
|
uint8_t specific[0xa0];
|
|
} AudrenEffectIn;
|
|
_Static_assert(sizeof(AudrenEffectIn) == 0xc0, "effect in parameter");
|
|
|
|
typedef struct {
|
|
uint8_t input[6], output[6]; uint16_t channel_max, channels;
|
|
int32_t sample_rate, lookahead_max, attack_time, release_time, lookahead;
|
|
float attack, release, threshold, input_gain, output_gain;
|
|
int32_t delay_min, delay_max; uint8_t status, statistics_enabled, statistics_reset, reserved;
|
|
} AudrenLimiterParam;
|
|
_Static_assert(sizeof(AudrenLimiterParam) == 68, "limiter effect wire layout");
|
|
|
|
typedef struct {
|
|
uint8_t input[24], output[24]; uint32_t channels, sample_rate, capacity, channel_max;
|
|
uint64_t send_info, send_storage, return_info, return_storage;
|
|
uint32_t sample_size, total_samples, frame_samples;
|
|
} AudrenAuxParam;
|
|
_Static_assert(__builtin_offsetof(AudrenAuxParam, send_info) == 64, "auxiliary effect wire layout");
|
|
|
|
#define AUDREN_REV(n) (0x30564552u + ((uint32_t)(n) << 24)) // "REVn"
|
|
#define AUDREN_WORK_BUFFER_BYTES 0x10000u // the host keeps its state in host memory
|
|
#define AUDREN_BEHAVIOR_OUT 0xb0u
|
|
#define AUDREN_RENDER_INFO_OUT 0x10u
|
|
// Catch-up after a stall (NX2NAVI_AUDIO_CATCHUP; audren.c render_thread, audio_mixer.c audio_mixer_owed): a gap longer
|
|
// than this is not caught up, the sound goes on from now.
|
|
#define AUDIO_CATCHUP_MAX_MS 3000
|
|
|
|
// Server-side voice: the DSP's view of one client voice.
|
|
typedef struct {
|
|
AudrenVoiceIn in;
|
|
AudrenBiquadFloat float_biquads[2];
|
|
uint8_t use_float_biquads;
|
|
AudrenWaveBuffer buffers[4];
|
|
uint8_t valid[4];
|
|
uint8_t pending[4]; // sent while the voice was not playing: valid once it plays
|
|
uint8_t state; // SV_*: the client's Start / Stop / Pause, plus Stopping until the next frame
|
|
uint32_t flush, flush_from; // FlushWaveBuffers since the last frame, from the client's current buffer
|
|
unsigned index;
|
|
uint64_t offset;
|
|
double fraction;
|
|
int32_t loops_done;
|
|
uint64_t played;
|
|
uint32_t consumed, drops;
|
|
struct { int16_t history[2]; int32_t buffer; uint64_t at; } adpcm; // DSP-ADPCM decode cursor (buffer slot, next sample) and history
|
|
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 };
|
|
|
|
// A RequestUpdate this host refuses (audren.c update_check, audio_mixer.c audio_mixer_routing_check and
|
|
// audio_mixer_mix_layout_check): audio module 153, description 41, the update's sections do not match the renderer.
|
|
// The SDK's RequestUpdateAudioRenderer hands it back to its caller (as it does descriptions 42 and 43) and aborts on
|
|
// any other result; the game ignores it and sends its next update. Nothing of a refused update is applied.
|
|
#define AUDREN_R_INVALID_UPDATE 0x5299u
|
|
|
|
// Kinds of game value this host cannot use (audren.c audio_bad), one per site; each is logged once per renderer.
|
|
enum {
|
|
// the RequestUpdate is refused
|
|
AB_UPD_BUFFERS, AB_UPD_SMALL, AB_UPD_TOTAL, AB_UPD_BEHAVIOR, AB_UPD_POOLS, AB_UPD_CHANNELS, AB_UPD_VOICES,
|
|
AB_UPD_EFFECTS, AB_UPD_SINKS, AB_UPD_OVERRUN, AB_UPD_UNCONSUMED, AB_UPD_OUTPUT,
|
|
AB_SPL_HEADER, AB_SPL_MAGIC, AB_SPL_INFO_OVERRUN, AB_SPL_INFO, AB_SPL_PADDING, AB_DEST_OVERRUN, AB_DEST_MAGIC,
|
|
AB_SPL_SIZE, AB_MIX_DIRTY, AB_MIX_SIZE, AB_MIX_LAYOUT,
|
|
// one record of the update is left out, the rest is applied
|
|
AB_CHANNEL_ID, AB_VOICE_ID, AB_WAVE_INDEX, AB_VOICE_FILTER, AB_SPL_ID, AB_SPL_DEST_REF, AB_DEST_ID,
|
|
AB_DEST_FILTER, AB_MIX_ID,
|
|
// one effect or sink is passed through or skipped
|
|
AB_LIMITER_SIZE, AB_LIMITER_CHANNELS, AB_LIMITER_PARAMS, AB_LIMITER_INDEX, AB_DELAY_PARAMS, AB_REVERB_PARAMS,
|
|
AB_COMPRESSOR_PARAMS, AB_DSP_CHANNELS, AB_DSP_INDEX, AB_R3D_PARAMS, AB_R3D_CHANNELS, AB_R3D_INDEX,
|
|
AB_AUX_SIZE, AB_AUX_ADDRESS, AB_AUX_INDEX, AB_CAPTURE_SIZE, AB_CAPTURE_ADDRESS, AB_CAPTURE_INDEX,
|
|
AB_BUFMIX_COUNT, AB_BUFMIX_INDEX, AB_SINK_PARAMS, AB_SINK_ADDRESS, AB_SINK_INDEX,
|
|
AB_KINDS
|
|
};
|
|
|
|
typedef struct AudioRenderer {
|
|
AudrenParameter param;
|
|
PlatMutex lock;
|
|
PlatThread *thread;
|
|
volatile int32_t running;
|
|
uint32_t state, revision, time_limit, event; // state 0 started, 1 stopped; event: writable handle
|
|
int logged;
|
|
uint64_t behavior_flags, elapsed_frames;
|
|
uint64_t stop_released, empty_played, flushed, logged_released;
|
|
struct AudrenStats *stats; // NX2NAVI_AUDIO_STATS (audren.c), NULL when off
|
|
ServerVoice *voices;
|
|
AudrenPoolIn *pools;
|
|
AudrenMixIn *mixes;
|
|
AudrenSinkIn *sinks;
|
|
AudrenEffectIn *effects;
|
|
AudrenChannelIn *channels;
|
|
void *mixer;
|
|
uint8_t strict; // NX2NAVI_AUDIO_STRICT (OpenAudioRenderer): audio_bad ends the process
|
|
uint8_t bad_seen[AB_KINDS]; // audio_bad: the kinds logged so far (atomic exchange; any thread)
|
|
uint64_t bad_count; // audio_bad: times a value was left out (per update or per render pass) or an
|
|
// update refused (atomic)
|
|
uint64_t refused, refused_run; // updates refused, and refused in a row (the thread that sends RequestUpdate)
|
|
} AudioRenderer;
|
|
|
|
// A value the game sent that this host cannot use: the caller refuses the update or leaves the one record, effect or
|
|
// sink out. Logs the first of each kind per renderer, with the game frame, and counts every one; with
|
|
// NX2NAVI_AUDIO_STRICT=1 it ends the process instead, as most of these values did before 0.10.2. Any thread; takes no
|
|
// lock but the log's.
|
|
void audio_bad(AudioRenderer *r, int kind, const char *fmt, ...) __attribute__((format(printf, 3, 4)));
|
|
// audio_bad for a refused RequestUpdate, as an expression that gives AUDREN_R_INVALID_UPDATE.
|
|
#define AUDIO_REFUSE(r, kind, fmt, ...) \
|
|
(audio_bad((r), (kind), \
|
|
"RequestUpdate refused with %#x: " fmt "; the renderer goes on with the last update it took", \
|
|
AUDREN_R_INVALID_UPDATE, ##__VA_ARGS__), \
|
|
AUDREN_R_INVALID_UPDATE)
|
|
|
|
// IAudioRendererManager, served under "audren:u" (ipc.c's service table).
|
|
void audren_manager_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs);
|
|
long audren_renderers_live(void); // renderers whose render tick runs
|
|
|
|
// Mixer interface (audio_mixer.c). Every call runs with r->lock held except open, close and set_running, the render
|
|
// thread's audio_mixer_pace_ns, audio_mixer_owed and audio_mixer_out_stats (audren.c render_thread and stats_report
|
|
// call them without it), and audio_mixer_routing_check. The render thread's three touch only atomics and state that
|
|
// only the render thread changes: device_check and channels_switch, which close and open the output stream, run on
|
|
// that thread too, and audio_mixer_close runs after renderer_close has joined it. audio_mixer_routing_check runs on the
|
|
// game's update thread before it takes the lock and reads only the renderer's fixed counts and the game's input.
|
|
void audio_mixer_open(AudioRenderer *r);
|
|
void audio_mixer_set_running(AudioRenderer *r, int running);
|
|
void audio_mixer_close(AudioRenderer *r);
|
|
// Before an update is applied, without the renderer lock: the framing of the splitter and mix sections in the layout
|
|
// of `revision`. 0, or AUDREN_R_INVALID_UPDATE (logged through audio_bad).
|
|
uint32_t audio_mixer_routing_check(AudioRenderer *r, uint32_t revision, const uint8_t *splitters,
|
|
uint32_t splitter_bytes, const uint8_t *mixes, uint32_t mix_bytes, int mix_count);
|
|
// Renderer lock held, before the update is applied: the mixes in use, as the update leaves them, fit the renderer's
|
|
// mix buffers (audio_mixer_render lays them out one after another). 0, or AUDREN_R_INVALID_UPDATE.
|
|
uint32_t audio_mixer_mix_layout_check(AudioRenderer *r, const uint8_t *mixes, uint32_t mix_bytes, int mix_count);
|
|
// After both checks passed. A record whose ids the renderer does not have is left out (audio_bad).
|
|
void audio_mixer_update_routing(AudioRenderer *r, const uint8_t *splitters, uint32_t splitter_bytes,
|
|
const uint8_t *mixes, uint32_t mix_bytes, int mix_count);
|
|
// An enabled effect whose parameters the DSP cannot use is passed through by audio_mixer_render (audio_bad).
|
|
void audio_mixer_update_effects(AudioRenderer *r);
|
|
void audio_mixer_effect_result(AudioRenderer *r, int id, void *output, unsigned size);
|
|
uint32_t audio_mixer_sink_written(AudioRenderer *r, int id);
|
|
// After audio_voices_prepare, before voices advance. `drop`: a catch-up frame (audren.c render_thread). It is mixed in
|
|
// full, so the voices' filters, the effects and the sinks the game reads back advance as for any frame, and nothing
|
|
// of it is queued for the device.
|
|
void audio_mixer_render(AudioRenderer *r, double seconds, int drop);
|
|
// Device pacing for the render thread at kern_now_ns `now`: 0 renders the next frame now, a positive value waits that
|
|
// many ns (the ring is at its target), -1 leaves the pace to the render thread's timer (no device, renderer stopped,
|
|
// device stalled).
|
|
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now);
|
|
// Catch-up for the frames the device paces (render thread, lock not held). 1: the device has played at least one render
|
|
// frame of silence for want of rendered sound that is not made up for yet; that frame is taken off what is owed and
|
|
// the caller renders one frame with `drop` set. 0: nothing is owed. -1: more than AUDIO_CATCHUP_MAX_MS was owed and is
|
|
// forgotten.
|
|
int audio_mixer_owed(AudioRenderer *r);
|
|
// NX2NAVI_AUDIO_STATS: the output's counters since it opened, and the longest gap between two device requests since
|
|
// 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
|
|
void audio_channels_live(void); // after a settings change: another NX2NAVI_AUDIO_CHANNELS reaches the game and the mixer
|
|
void audio_dmg_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs); // auddmg:u (Switch 2)
|
|
void audio_sysmix_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs); // audsmx:u (Switch 2)
|