// Adapted from MacNativeGames games/totk/runtime/audio_renderer.c @ b75be8c (MIT, Copyright (c) 2026 // MacNativeGames contributors). Its voice lifetime (play state, wave buffer release, flush) is ported from // Ryujinx.Audio VoiceInfo / DataSourceHelper (MIT, Copyright (c) Ryujinx Team and Contributors). // audren:u. The SDK's nn::audio client builds RequestUpdate input unchanged; this // service validates every section against the header, keeps server-side voice state, advances it in real time (one // frame of sample_count / sample_rate seconds per render tick, signalling the renderer's event) and writes the output // status the client expects. The mixer behind it is audio_mixer.c. #include #include #include #include #include "audren.h" #include "host.h" #include "ipc.h" #include "platform/platform.h" static volatile int32_t g_live; long audren_renderers_live(void) { return plat_cas32(&g_live, 0, 0); } static int revision_number(uint32_t rev) { return (int)(rev >> 24) - '0'; } static void need(int ok, const char *what) { if (ok) return; ipc_backtrace(); rt_fatal("audren: %s", what); } static AudioRenderer *renderer_new(const AudrenParameter *p) { need(p->sample_rate == 48000 || p->sample_rate == 32000, "unsupported output rate"); need(p->sample_count > 0 && p->sample_count <= 4800, "sample count"); need(p->voice_count > 0 && p->voice_count <= 1024 && p->sink_count > 0 && p->sink_count <= 256 && p->effect_count >= 0 && p->effect_count <= 4096 && p->submix_count >= 0 && p->submix_count < 256, "renderer counts"); AudioRenderer *r = calloc(1, sizeof *r); need(r != NULL, "renderer allocation"); r->param = *p; r->revision = p->revision; r->voices = calloc((size_t)p->voice_count, sizeof *r->voices); r->pools = calloc((size_t)p->effect_count + 4u * (size_t)p->voice_count, sizeof *r->pools); r->mixes = calloc((size_t)p->submix_count + 1, sizeof *r->mixes); r->sinks = calloc((size_t)p->sink_count, sizeof *r->sinks); r->effects = calloc((size_t)(p->effect_count ? p->effect_count : 1), sizeof *r->effects); r->channels = calloc((size_t)p->voice_count * 6, sizeof *r->channels); need(r->voices && r->pools && r->mixes && r->sinks && r->effects && r->channels, "renderer tables"); plat_mutex_init(&r->lock); r->event = kern_event_new(); r->state = 1; // stopped return r; } static void renderer_free(AudioRenderer *r) { free(r->voices); free(r->pools); free(r->mixes); free(r->sinks); free(r->effects); free(r->channels); free(r); } // ---- RequestUpdate typedef struct { const uint8_t *p, *end; } Reader; static const uint8_t *take(Reader *in, uint32_t n, const char *what) { if ((size_t)(in->end - in->p) < n) { rt_log("audren: input section %s (%u bytes) overruns the request", what, n); need(0, "update input section size"); } const uint8_t *p = in->p; in->p += n; return p; } static void voice_reset(ServerVoice *v) { memset(v->valid, 0, sizeof v->valid); memset(v->pending, 0, sizeof v->pending); v->state = SV_STOPPED; v->flush = v->flush_from = 0; v->index = 0; v->offset = 0; v->fraction = 0; v->played = 0; v->consumed = 0; v->loops_done = 0; memset(&v->adpcm, 0, sizeof v->adpcm); memset(v->biquad_state, 0, sizeof v->biquad_state); } static void update_voice(AudioRenderer *r, const AudrenVoiceIn *in) { need(in->id < (uint32_t)r->param.voice_count, "voice id out of range"); ServerVoice *v = &r->voices[in->id]; if (in->is_new || !in->in_use) voice_reset(v); v->in = *in; v->use_float_biquads = 0; if (!in->in_use) return; need(in->wave_buffer_index < 4, "wave buffer index"); // The play state as the DSP keeps it (Ryujinx VoiceInfo.UpdatePlayState): Stop on a voice that is not stopped // becomes Stopping, which the next render frame turns into Stopped after giving back every queued buffer. if (in->play_state == PLAY_STARTED) v->state = SV_STARTED; else if (in->play_state == PLAY_PAUSED) v->state = SV_PAUSED; else if (v->state != SV_STOPPED) v->state = SV_STOPPING; for (unsigned i = 0; i < 4; i++) { // a new buffer waits until a render frame finds the voice playing const AudrenWaveBuffer *w = &in->wave_buffers[i]; if (!w->sent) { v->buffers[i] = *w; v->pending[i] = 1; } } v->flush += in->flush_wave_buffer_count; v->flush_from = in->wave_buffer_index; if (in->clear_voice_drop) v->drops = 0; } // No samples (no memory, no size or an empty range): the DSP counts the buffer as played when it reaches it, so a // stream's empty last buffer does not hold up the buffers queued behind it. int audio_buffer_empty(const AudrenWaveBuffer *b) { return !b->address || !b->size || b->end <= b->start; } static void skip_empty(AudioRenderer *r, ServerVoice *v) { while (v->valid[v->index] && audio_buffer_empty(&v->buffers[v->index])) { const AudrenWaveBuffer *b = &v->buffers[v->index]; if (b->loop && b->loop_count < 0) break; // an endless empty loop starves the voice, as on the DSP v->valid[v->index] = 0; v->consumed++; v->offset = 0; v->loops_done = 0; v->index = (v->index + 1) & 3; r->empty_played++; if (b->end_of_stream) { v->played = 0; break; } } } // Each render frame, before mixing (Ryujinx VoiceInfo.UpdateParametersForCommandGeneration): flush, stop, start. static void voices_prepare(AudioRenderer *r) { for (int k = 0; k < r->param.voice_count; k++) { ServerVoice *v = &r->voices[k]; if (!v->in.in_use) continue; if (v->flush) { // FlushWaveBuffers: from the client's current buffer, the queued ones count as played unsigned i = v->flush_from & 3; for (uint32_t n = v->flush; n; n--, i = (i + 1) & 3) { v->pending[i] = 0; if (v->valid[i]) { v->valid[i] = 0; v->consumed++; v->index = (v->index + 1) & 3; v->offset = 0; v->loops_done = 0; r->flushed++; } } v->flush = 0; } if (v->state == SV_STOPPING) { // every queued buffer counts as played; unsent ones are dropped for (int i = 0; i < 4; i++) { if (v->valid[i]) { v->consumed++; v->index = (v->index + 1) & 3; r->stop_released++; } v->valid[i] = 0; v->pending[i] = 0; } v->offset = 0; v->played = 0; v->fraction = 0; v->loops_done = 0; v->state = SV_STOPPED; continue; } if (v->state == SV_STARTED) { for (int i = 0; i < 4; i++) if (v->pending[i]) { v->pending[i] = 0; v->valid[i] = 1; if ((unsigned)i == v->index) { v->offset = 0; v->loops_done = 0; } } skip_empty(r, v); } } } static uint32_t effect_out_bytes(const AudioRenderer *r) { return revision_number(r->revision) >= 9 ? 0x90u : 0x10u; } static void request_update(AudioRenderer *r, const uint8_t *input, uint64_t in_size, uint8_t *output, uint64_t out_size, uint64_t perf_size) { need(in_size >= sizeof(AudrenHeader), "update input too small"); AudrenHeader h; memcpy(&h, input, sizeof h); need(h.total <= in_size, "update total size"); Reader in = {input + sizeof h, input + h.total}; plat_mutex_lock(&r->lock); r->revision = h.revision; const int vc = r->param.voice_count, ec = r->param.effect_count, sc = r->param.sink_count, mc = r->param.submix_count + 1; const uint32_t npools = (uint32_t)ec + 4u * (uint32_t)vc; if (!r->logged) { r->logged = 1; rt_log("audren: update REV%d sizes behavior=%u pools=%u voices=%u channels=%u effects=%u mixes=%u sinks=%u " "perf=%u splitters=%u render=%u total=%u out=%llu perf_out=%llu", revision_number(h.revision), h.behavior, h.memory_pools, h.voices, h.voice_resources, h.effects, h.mixes, h.sinks, h.performance, h.splitters, h.render_info, h.total, (unsigned long long)out_size, (unsigned long long)perf_size); } need(h.behavior == 0x10, "behavior section size"); const uint8_t *behavior = take(&in, h.behavior, "behavior"); memcpy(&r->behavior_flags, behavior + 8, 8); need(h.memory_pools == npools * sizeof(AudrenPoolIn), "memory pool section size"); const AudrenPoolIn *pools = (const AudrenPoolIn *)take(&in, h.memory_pools, "memory pools"); need(h.voice_resources % sizeof(AudrenChannelIn) == 0 && h.voice_resources / sizeof(AudrenChannelIn) <= (uint32_t)vc * 6, "voice channel section size"); const AudrenChannelIn *chans = (const AudrenChannelIn *)take(&in, h.voice_resources, "voice channels"); const int float_voices = revision_number(h.revision) >= 15; const size_t voice_stride = float_voices ? sizeof(AudrenVoiceInV2) : sizeof(AudrenVoiceIn); need(h.voices == (uint32_t)vc * voice_stride, "voice section size"); const uint8_t *voices = take(&in, h.voices, "voices"); need(h.effects == (uint32_t)ec * sizeof(AudrenEffectIn), "effect section size"); const AudrenEffectIn *effects = (const AudrenEffectIn *)take(&in, h.effects, "effects"); const uint8_t *splitters = take(&in, h.splitters, "splitters"); const uint8_t *mixes = take(&in, h.mixes, "mixes"); need(h.sinks == (uint32_t)sc * sizeof(AudrenSinkIn), "sink section size"); const AudrenSinkIn *sinks = (const AudrenSinkIn *)take(&in, h.sinks, "sinks"); take(&in, h.performance, "performance"); take(&in, h.render_info, "render info"); need(in.p == in.end, "unconsumed update input"); for (uint32_t i = 0; i < h.voice_resources / sizeof(AudrenChannelIn); i++) { need(chans[i].id < (uint32_t)vc * 6, "channel resource id"); r->channels[chans[i].id] = chans[i]; } for (int i = 0; i < vc; i++) { AudrenVoiceIn v; AudrenVoiceInV2 v2; if (float_voices) { memcpy(&v2, voices + i * voice_stride, sizeof v2); memset(&v, 0, sizeof v); memcpy(&v, v2.prefix, sizeof v2.prefix); memcpy((uint8_t *)&v + 0x3c, v2.tail, sizeof v2.tail); for (unsigned f = 0; f < 2; f++) { v.biquads[f].enable = v2.biquads[f].enable; if (!v.biquads[f].enable) continue; for (unsigned c = 0; c < 3; c++) need(isfinite(v2.biquads[f].numerator[c]), "voice biquad numerator"); for (unsigned c = 0; c < 2; c++) need(isfinite(v2.biquads[f].denominator[c]), "voice biquad denominator"); } } else memcpy(&v, voices + i * voice_stride, sizeof v); update_voice(r, &v); ServerVoice *server = &r->voices[v.id]; server->use_float_biquads = (uint8_t)float_voices; if (float_voices) memcpy(server->float_biquads, v2.biquads, sizeof v2.biquads); } for (int i = 0; i < ec; i++) r->effects[i] = effects[i]; audio_mixer_update_effects(r); audio_mixer_update_routing(r, splitters, h.splitters, mixes, h.mixes, mc); for (int i = 0; i < sc; i++) r->sinks[i] = sinks[i]; // ---- output const uint32_t eo = effect_out_bytes(r); AudrenHeader o = {0}; o.revision = AUDREN_REV(revision_number(h.revision) >= 1 ? revision_number(h.revision) : 1); o.memory_pools = npools * sizeof(AudrenPoolOut); o.voices = (uint32_t)vc * sizeof(AudrenVoiceOut); o.effects = (uint32_t)ec * eo; o.sinks = (uint32_t)sc * sizeof(AudrenSinkOut); o.performance = 0x10; o.behavior = AUDREN_BEHAVIOR_OUT; o.render_info = AUDREN_RENDER_INFO_OUT; o.total = (uint32_t)sizeof o + o.memory_pools + o.voices + o.effects + o.sinks + o.performance + o.behavior + o.render_info; if (o.total > out_size) { rt_log("audren: output needs %u bytes, the client gave %llu (effect out %u)", o.total, (unsigned long long)out_size, eo); need(0, "update output size"); } memset(output, 0, out_size); uint8_t *w = output; memcpy(w, &o, sizeof o); w += sizeof o; for (uint32_t i = 0; i < npools; i++) { uint32_t s = pools[i].state; if (s == POOL_REQUEST_ATTACH) s = POOL_ATTACHED; else if (s == POOL_REQUEST_DETACH) s = POOL_DETACHED; r->pools[i] = pools[i]; r->pools[i].state = s; AudrenPoolOut po = {s, {0}}; memcpy(w, &po, sizeof po); w += sizeof po; } for (int i = 0; i < vc; i++) { AudrenVoiceOut vo = {r->voices[i].played, r->voices[i].consumed, r->voices[i].drops}; memcpy(w, &vo, sizeof vo); w += sizeof vo; } for (int i = 0; i < ec; i++) { // EffectStatus Enabled 3 / Disabled 4 w[0] = effects[i].enabled ? 3 : 4; if (eo > 16) audio_mixer_effect_result(r, i, w + 16, eo - 16); w += eo; } for (int i = 0; i < sc; i++) { AudrenSinkOut so = {0}; if (r->sinks[i].type == SINK_CIRCULAR) so.last_written_offset = audio_mixer_sink_written(r, i); memcpy(w, &so, sizeof so); w += sizeof so; } w += 0x10; // performance: nothing written w += AUDREN_BEHAVIOR_OUT; // behavior: no errors memcpy(w, &r->elapsed_frames, 8); plat_mutex_unlock(&r->lock); } // ---- render tick: one frame each time the output device has taken the ring below its target (audio_mixer_pace_ns), // or every sample_count / sample_rate seconds of the host clock without a device, while stopped or while it stalls static void advance_voice(AudioRenderer *r, ServerVoice *v) { if (!v->in.in_use || v->state != SV_STARTED || !v->in.sample_rate) return; uint64_t samples = audio_voice_consumed(r, v, (int)r->param.sample_count); while (samples && v->valid[v->index]) { AudrenWaveBuffer *b = &v->buffers[v->index]; uint64_t length = (uint64_t)(b->end - b->start), left = length - v->offset; uint64_t step = samples < left ? samples : left; v->offset += step; v->played += step; samples -= step; if (v->offset < length) break; if (b->loop && (b->loop_count < 0 || v->loops_done < b->loop_count)) { v->loops_done++; v->offset = b->loop_start > b->start && b->loop_start < b->end ? (uint64_t)(b->loop_start - b->start) : 0; continue; } v->valid[v->index] = 0; v->consumed++; v->offset = 0; v->loops_done = 0; v->index = (v->index + 1) & 3; if (b->end_of_stream) { v->played = 0; break; } skip_empty(r, v); } } static void render_thread(void *p) { AudioRenderer *r = p; plat_thread_priority(PLAT_PRIO_HIGHEST); const uint64_t frame_ns = (uint64_t)r->param.sample_count * 1000000000ull / (uint64_t)r->param.sample_rate; const double frame = (double)r->param.sample_count / r->param.sample_rate; // When the device pulls a period (1024 samples on PipeWire, 4-5 frames) the ring drops below its target // and the frames would otherwise render back to back. On hardware the DSP renders one frame every 5 ms and // the game's audio thread works in between; AUX effects need that time to fill the return ring, or mix_aux // reads stale samples and the frame clicks at its seams. Device-paced frames stay at least 4/5 of a frame // apart, which still refills a period well before the device drains it. Linux only: on Windows SDL pulls // 480-sample periods, and a sink dump (TOTK_AUDIO_DUMP) of the bursts there showed no more seam spikes than // chance, so a wait on this thread buys nothing. #ifdef __linux__ const uint64_t gap_ns = frame_ns * 4 / 5; #else const uint64_t gap_ns = 0; #endif uint64_t next = kern_now_ns(), last = 0; while (plat_cas32(&r->running, 0, 0)) { uint64_t now = kern_now_ns(); int64_t pace = audio_mixer_pace_ns(r, now); if (pace > 0) { plat_wait_ns((uint64_t)pace); continue; } if (pace == 0 && last && now - last < gap_ns) { plat_wait_ns(gap_ns - (now - last)); continue; } if (pace == 0) { next = now + frame_ns; // where the timer goes on if the device stops pacing } else { if (next > now) plat_wait_ns(next - now); else if (now - next > 100000000ull) next = now; // fell far behind (debugger, sleep): resynchronise next += frame_ns; } last = kern_now_ns(); plat_mutex_lock(&r->lock); if (r->state == 0) { voices_prepare(r); audio_mixer_render(r, frame); for (int i = 0; i < r->param.voice_count; i++) advance_voice(r, &r->voices[i]); r->elapsed_frames++; uint64_t given = r->stop_released + r->empty_played + r->flushed; if ((rt_trace & RT_TRACE_IPC) && given != r->logged_released && r->elapsed_frames % 1000 == 0) { r->logged_released = given; rt_log("audren: wave buffers given back: %llu by stopped voices, %llu without samples, %llu flushed", (unsigned long long)r->stop_released, (unsigned long long)r->empty_played, (unsigned long long)r->flushed); } } plat_mutex_unlock(&r->lock); kern_event_signal(r->event); } } // ---- IAudioRenderer static IpcBuffer pick(const IpcBuffer *a, unsigned na, const IpcBuffer *b, unsigned nb, unsigned i) { IpcBuffer x = i < na ? a[i] : (IpcBuffer){0}, y = i < nb ? b[i] : (IpcBuffer){0}; return x.size ? x : y; } static void renderer_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) { AudioRenderer *r = s->state; switch (rq->cmd) { case 0: { int32_t v = r->param.sample_rate; IPC_OUT(rs, v); return; } // GetSampleRate case 1: { int32_t v = r->param.sample_count; IPC_OUT(rs, v); return; } // GetSampleCount case 2: { int32_t v = r->param.mix_buffer_count; IPC_OUT(rs, v); return; } // GetMixBufferCount case 3: { uint32_t v = r->state; IPC_OUT(rs, v); return; } // GetState case 4: case 10: { // RequestUpdate(Auto): out(output), out(performance), in(input) IpcBuffer in = pick(rq->a, rq->na, rq->x, rq->nx, 0); IpcBuffer out = pick(rq->b, rq->nb, rq->c, rq->nc, 0), perf = pick(rq->b, rq->nb, rq->c, rq->nc, 1); if (!r->logged) rt_log("audren: RequestUpdate cmd %u in=%#llx+%#llx out=%#llx+%#llx perf=%#llx+%#llx (a%u b%u x%u c%u)", rq->cmd, (unsigned long long)in.addr, (unsigned long long)in.size, (unsigned long long)out.addr, (unsigned long long)out.size, (unsigned long long)perf.addr, (unsigned long long)perf.size, rq->na, rq->nb, rq->nx, rq->nc); need(in.size && out.size, "RequestUpdate buffers"); request_update(r, ipc_guest(in.addr, in.size), in.size, ipc_guest(out.addr, out.size), out.size, perf.size); for (unsigned i = 0; i < rq->nc && rq->cmd == 10; i++) if (rq->c[i].size && (i >= rq->nb || !rq->b[i].size)) rs->xout[rs->nxout++] = (IpcBuffer){rq->c[i].addr, rq->c[i].size, i}; return; } case 5: // Start plat_mutex_lock(&r->lock); r->state = 0; audio_mixer_set_running(r, 1); plat_mutex_unlock(&r->lock); return; case 6: // Stop plat_mutex_lock(&r->lock); r->state = 1; audio_mixer_set_running(r, 0); plat_mutex_unlock(&r->lock); return; case 7: rs->copy[rs->ncopy++] = kern_event_readable(r->event); return; // QuerySystemEvent case 8: r->time_limit = IPC_ARG(rq, uint32_t, 0); return; // SetRenderingTimeLimit case 9: { uint32_t v = r->time_limit ? r->time_limit : 100; IPC_OUT(rs, v); return; } // GetRenderingTimeLimit default: ipc_unimplemented(s, rq); } } static void renderer_close(void *p) { AudioRenderer *r = p; plat_xchg32(&r->running, 0); int away = kern_core_away(); plat_thread_join(r->thread); kern_core_back(&away); plat_dec32(&g_live); audio_mixer_close(r); kern_handle_close(r->event); rt_log("audren: renderer closed after %llu frames", (unsigned long long)r->elapsed_frames); renderer_free(r); } // ---- IAudioRendererManager void audren_manager_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) { switch (rq->cmd) { case 0: { // OpenAudioRenderer(param, pad, work size, aruid; copy tmem, process; pid) AudrenParameter p = IPC_ARG(rq, AudrenParameter, 0); uint64_t work = IPC_ARG(rq, uint64_t, 0x38); need(rq->ncopy >= 1, "OpenAudioRenderer without a transfer memory handle"); IpcTransferMemory *tm = kern_handle_get(rq->copy[0], KERN_TMEM); need(tm && tm->size >= work, "OpenAudioRenderer work buffer is not a transfer memory of the stated size"); AudioRenderer *r = renderer_new(&p); r->running = 1; audio_mixer_open(r); r->thread = plat_thread_new(render_thread, r, "audren"); if (!r->thread) rt_fatal("audren: cannot start the render thread"); plat_inc32(&g_live); rt_log("audren: renderer open %d Hz x%d, mix buffers %d, voices %d, effects %d, submixes %d, sinks %d, " "splitters %d/%d, REV%d, work buffer %#llx+%#llx", p.sample_rate, p.sample_count, p.mix_buffer_count, p.voice_count, p.effect_count, p.submix_count, p.sink_count, p.splitter_count, p.splitter_destination_count, revision_number(p.revision), (unsigned long long)tm->addr, (unsigned long long)work); rs->move[rs->nmove++] = ipc_session_new("IAudioRenderer", renderer_handler, r, renderer_close); return; } case 1: { // GetWorkBufferSize(param) -> u64 AudrenParameter p = IPC_ARG(rq, AudrenParameter, 0); need(p.sample_rate == 48000 || p.sample_rate == 32000, "GetWorkBufferSize: unsupported output rate"); uint64_t v = AUDREN_WORK_BUFFER_BYTES; IPC_OUT(rs, v); return; } case 2: case 4: // GetAudioDeviceService(aruid), GetAudioDeviceServiceWithRevisionInfo(revision, aruid) rs->move[rs->nmove++] = ipc_session_new("IAudioDevice", audio_device_handler, NULL, NULL); return; default: ipc_unimplemented(s, rq); } }