From ca5d39b6f5a8b8fe3cfddae2e24df08f0b4812c0 Mon Sep 17 00:00:00 2001 From: maxjivi05 Date: Tue, 6 Oct 2026 12:58:02 -0400 Subject: [PATCH] DirectAudio relay: prime before playing, one consumer, safe buffer floors - The output stream started before the client had its ring, so every connection opened with a run of short callbacks: six clicks per stream on a RedMagic, even idle. The callback now plays silence until the ring holds its target, and only a real underrun afterwards counts. - An underrun ends the block with a short fade and waits for the ring to refill to target before resuming with a fade in, instead of zero-padding every short callback. Each callback spent starved grows the target, so one stall sizes it for the next. With a producer freezing 60 ms every 2 s: 14 underruns before, 1 after. - During a reopen the outgoing and incoming streams could both drain the ring. Only the promoted stream reads it now; any other plays silence. - The ring target starts at 40 ms instead of two bursts (8 ms) and keeps what it learned across reopens. - The device buffer is at least two bursts; 12 ms was a single burst on devices that grant a 20 ms legacy stream. --- tools/directaudio-relay/directaudio-relay.c | 101 +++++++++++++++----- 1 file changed, 77 insertions(+), 24 deletions(-) diff --git a/tools/directaudio-relay/directaudio-relay.c b/tools/directaudio-relay/directaudio-relay.c index f7463f6b..753b9d86 100644 --- a/tools/directaudio-relay/directaudio-relay.c +++ b/tools/directaudio-relay/directaudio-relay.c @@ -100,6 +100,9 @@ static void futex_wake_all(volatile uint32_t *word) #define DA_DECAY_QUIET_NS 10000000000ull #define DA_DECAY_PUNISH_NS 5000000000ull #define DA_DECAY_MAX_BACKOFF 32 +#define DA_MIN_BUFFER_BURSTS 2 +#define DA_RING_MIN_TARGET_MS 40 +#define DA_FADE_FRAMES 64 /* ---- live runtime config (mailbox) ---------------------------------------- * The path arrives in the first client's hello (every client of one host names @@ -159,6 +162,9 @@ struct client unsigned int decay_backoff, cb_count, wd_last_cb; int reopen_running, reopen_redo; int32_t ring_max_target; /* ceiling for the ring's own adaptive step */ + int consuming; + int primed; + int starved; /* capture: ring + the one INPUT stream */ int in_fd; @@ -212,6 +218,18 @@ static void ring_destroy(struct da_ring *r, int fd) /* ---- OUTPUT stream ------------------------------------------------------- */ static void request_reopen(struct client *c, const char *why); +static void fade(float *p, uint32_t frames, int in) +{ + uint32_t i, ch; + + for (i = 0; i < frames; i++) + { + float g = (float)(in ? i + 1 : frames - i) / (float)(frames + 1); + + for (ch = 0; ch < DA_RING_CHANNELS; ch++) p[(size_t)i * DA_RING_CHANNELS + ch] *= g; + } +} + /* The AAudio data callback: pull one block from the render ring. This is the * in-process mixer_cb with the mixing replaced by a ring read; the bookkeeping * after the copy (liveness, adaptive growth, decay) is ported as-is. */ @@ -221,30 +239,59 @@ static aaudio_data_callback_result_t out_cb(AAudioStream *aq, void *user, struct client *c = user; struct da_ring *r = c->out; float *out = audioData; + AAudioStream *live = __atomic_load_n(&c->aq, __ATOMIC_SEQ_CST); uint32_t cap = r->cap_frames; - uint32_t ridx = __atomic_load_n(&r->ridx, __ATOMIC_ACQUIRE); - uint32_t avail = __atomic_load_n(&r->widx, __ATOMIC_ACQUIRE) - ridx; - uint32_t n = (uint32_t)numFrames < avail ? (uint32_t)numFrames : avail; - uint32_t i, first, slot = ridx % cap; + uint32_t frames = (uint32_t)numFrames; + uint32_t ridx, avail, n = 0, i; + int fade_in = 0, short_read = 0, starving = 0; uint64_t now; - /* Copy out, at most two memcpy's around the wrap. The soft-knee limiter ran - * in the game before the samples were queued, so this is a straight copy. */ - first = cap - slot < n ? cap - slot : n; - memcpy(out, &r->data[(size_t)slot * DA_RING_CHANNELS], (size_t)first * DA_RING_CHANNELS * sizeof(float)); - if (n > first) - memcpy(out + (size_t)first * DA_RING_CHANNELS, r->data, (size_t)(n - first) * DA_RING_CHANNELS * sizeof(float)); - for (i = n * DA_RING_CHANNELS; i < (uint32_t)numFrames * DA_RING_CHANNELS; i++) out[i] = 0.0f; + if ((live && aq != live) || __atomic_exchange_n(&c->consuming, 1, __ATOMIC_ACQUIRE)) + { + memset(out, 0, (size_t)frames * DA_RING_CHANNELS * sizeof(float)); + return AAUDIO_CALLBACK_RESULT_CONTINUE; + } + + ridx = __atomic_load_n(&r->ridx, __ATOMIC_ACQUIRE); + avail = __atomic_load_n(&r->widx, __ATOMIC_ACQUIRE) - ridx; + if (!c->primed) + { + uint32_t start = (uint32_t)__atomic_load_n(&r->target_frames, __ATOMIC_ACQUIRE); + + if (start < frames) start = frames; + if (start > cap) start = cap; + c->primed = fade_in = avail >= start; + if (c->primed) c->starved = 0; + else starving = c->starved; + } + if (c->primed) + { + uint32_t slot = ridx % cap, first; + + n = frames < avail ? frames : avail; + first = cap - slot < n ? cap - slot : n; + memcpy(out, &r->data[(size_t)slot * DA_RING_CHANNELS], (size_t)first * DA_RING_CHANNELS * sizeof(float)); + if (n > first) + memcpy(out + (size_t)first * DA_RING_CHANNELS, r->data, (size_t)(n - first) * DA_RING_CHANNELS * sizeof(float)); + if (fade_in) + fade(out, n < DA_FADE_FRAMES ? n : DA_FADE_FRAMES, 1); + if (n < frames) + { + uint32_t tail = n < DA_FADE_FRAMES ? n : DA_FADE_FRAMES; + + fade(out + (size_t)(n - tail) * DA_RING_CHANNELS, tail, 0); + c->primed = 0; + c->starved = 1; + short_read = 1; + } + __atomic_store_n(&r->ridx, ridx + n, __ATOMIC_RELEASE); + } + for (i = n * DA_RING_CHANNELS; i < frames * DA_RING_CHANNELS; i++) out[i] = 0.0f; + __atomic_store_n(&c->consuming, 0, __ATOMIC_RELEASE); - __atomic_store_n(&r->ridx, ridx + n, __ATOMIC_RELEASE); __atomic_add_fetch(&r->wake, 1, __ATOMIC_RELEASE); futex_wake_all(&r->wake); - /* Only the live, promoted stream owns the state below (same guard as the - * in-process build: an outgoing stream still gets a callback or two). */ - if (aq != __atomic_load_n(&c->aq, __ATOMIC_SEQ_CST) && __atomic_load_n(&c->aq, __ATOMIC_SEQ_CST)) - return AAUDIO_CALLBACK_RESULT_CONTINUE; - now = now_ns(); { uint64_t prev = __atomic_exchange_n(&c->last_cb_ns, now, __ATOMIC_SEQ_CST); @@ -262,9 +309,10 @@ static aaudio_data_callback_result_t out_cb(AAudioStream *aq, void *user, * earlier - so ask it to keep one more burst queued. Only grows; the AAudio * buffer below is where decay lives, and the ring stays a small fixed cost * on top of it. */ - if (n < (uint32_t)numFrames) - { + if (short_read) __atomic_add_fetch(&r->underruns, 1, __ATOMIC_RELAXED); + if (short_read || starving) + { if (c->adaptive) { int32_t burst = AAudioStream_getFramesPerBurst(aq); @@ -276,7 +324,7 @@ static aaudio_data_callback_result_t out_cb(AAudioStream *aq, void *user, { __atomic_store_n(&r->target_frames, want, __ATOMIC_RELEASE); RLOG("[%d] ring grow: target %d -> %d frames (short by %u)", c->pid, cur, want, - (uint32_t)numFrames - n); + frames - n); } } } @@ -411,16 +459,21 @@ static aaudio_result_t open_output(struct client *c, AAudioStream **out) want = (nb < 1 ? 1 : nb) * burst; } } + if (burst > 0 && want < DA_MIN_BUFFER_BURSTS * burst) want = DA_MIN_BUFFER_BURSTS * burst; if (want > capf) want = capf; if (want > 0) AAudioStream_setBufferSizeInFrames(aq, want); - /* The ring's initial target: DA_RING_TARGET_BURSTS bursts of the real - * burst size, ceiling DA_RING_MAX_TARGET_MS. Set on every (re)open so a - * recovered stream starts low again, like the buffer does. */ if (burst <= 0) burst = 192; c->ring_max_target = DA_RING_MAX_TARGET_MS * DA_RING_RATE / 1000; if (c->ring_max_target > (int32_t)c->out->cap_frames) c->ring_max_target = c->out->cap_frames; - __atomic_store_n(&c->out->target_frames, DA_RING_TARGET_BURSTS * burst, __ATOMIC_RELEASE); + { + int32_t floor = DA_RING_TARGET_BURSTS * burst; + + if (floor < DA_RING_MIN_TARGET_MS * DA_RING_RATE / 1000) floor = DA_RING_MIN_TARGET_MS * DA_RING_RATE / 1000; + if (floor > c->ring_max_target) floor = c->ring_max_target; + if (__atomic_load_n(&c->out->target_frames, __ATOMIC_ACQUIRE) < floor) + __atomic_store_n(&c->out->target_frames, floor, __ATOMIC_RELEASE); + } __atomic_store_n(&c->out->hw_burst, burst, __ATOMIC_RELAXED); } c->last_xrun = AAudioStream_getXRunCount(aq);