refactor(audio): extract RendererBus (phase 2)

Moves the WebAudio→engine bus — ring, producer-side linear resampler,
prefill gate, fill clamp, metrics — verbatim into
src/audio/engine/RendererBus.h. AudioEngine keeps thin facades
(setRendererBus/pushRendererAudio/pullRendererBus/getRendererBusMetrics) so
the NodeAddon surface is unchanged. JUCE-free: pull() takes raw channel
pointers, which is what lets tests/engine_units drive the resampler
continuity, prime/underflow/clamp, and metrics cases without a device.

The control-thread readIndex write on disable (deep-read §4) is preserved
verbatim and marked; its flush-flag fix lands as the phase-8 commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
OmikronApex
2026-07-13 23:46:55 +02:00
co-authored by Claude Fable 5
parent eb40b87dea
commit 70f3316094
5 changed files with 380 additions and 152 deletions
+4 -43
View File
@@ -3,6 +3,7 @@
#include "GainSanitize.h"
#include "engine/PackedStereoRing.h"
#include "engine/EngineState.h"
#include "engine/RendererBus.h"
#include "BackingLeveler.h"
#include "signalsmith-stretch.h"
#include <juce_audio_devices/juce_audio_devices.h>
@@ -270,20 +271,7 @@ public:
// mixer path is silenced. SPSC: producer is the main-process IPC thread,
// consumer is whichever output callback is live (duplex or split). Default
// off → zero behaviour change.
void setRendererBus(bool enabled, float gain)
{
rendererBusGain.store(sanitizeStreamGain(gain), std::memory_order_relaxed);
const bool was = rendererBusEnabled.exchange(enabled, std::memory_order_acq_rel);
if (was && !enabled)
{
// Drop buffered audio on disable so a later re-enable starts fresh
// instead of playing a stale tail. Consumer tolerates the jump.
rendererBusRing.readIndex.store(
rendererBusRing.writeIndex.load(std::memory_order_acquire),
std::memory_order_release);
rendererBusPrimed.store(false, std::memory_order_relaxed);
}
}
void setRendererBus(bool enabled, float gain) { rendererBus.setEnabled(enabled, gain); }
// Interleaved stereo frames at `sourceRate`; linear-resampled to the device
// rate on the producer thread (fractional position + previous frame carried
// across calls). Returns false when the bus is disabled or the engine is
@@ -563,35 +551,8 @@ private:
static constexpr int kOutputRingFrames = 4096;
slopsmith::PackedStereoRing<kOutputRingFrames> outputRing;
// ── Renderer-audio bus ring (see setRendererBus/pushRendererAudio) ───────
// Same packed-LR SPSC design as outputRing. Sized generously
// (~1.5 s @ 48 kHz — vs outputRing's 85 ms) because the producer is
// an IPC thread with scheduling jitter, not another audio callback; the
// consumer trims steady-state fill via the drift clamp in the mix step.
static constexpr int kRendererBusFrames = 65536;
static_assert((kRendererBusFrames & (kRendererBusFrames - 1)) == 0,
"kRendererBusFrames must be a power of two for mask wraparound");
// Prefill gate: consume nothing until the producer has built this cushion
// (~10.7 ms @ 48 kHz); re-armed after every underflow so stall recovery is
// one clean gap. Fill clamp: fill beyond this (~85 ms) means a renderer
// stall dumped a backlog — trim to the prime target, don't play the tail.
static constexpr int kRendererBusPrimeFrames = 512;
static constexpr int kRendererBusMaxFillFrames = 4096;
slopsmith::PackedStereoRing<kRendererBusFrames> rendererBusRing;
std::atomic<uint64_t> rendererBusPushedFrames{0};
std::atomic<uint64_t> rendererBusConsumedFrames{0};
std::atomic<uint64_t> rendererBusUnderflowCount{0};
std::atomic<uint64_t> rendererBusOverflowCount{0};
std::atomic<bool> rendererBusEnabled{false};
std::atomic<float> rendererBusGain{1.0f};
// Consumer-side prefill-gate state. Only the live output callback touches
// it, but duplex/split hand-offs cross threads — atomic keeps that safe.
std::atomic<bool> rendererBusPrimed{false};
// Producer-thread-only linear-resampler state (fractional read position
// into the incoming chunk + the previous chunk's last frame for
// interpolation continuity across pushes).
double rendererBusSrcPos = 0.0;
float rendererBusPrevL = 0.0f, rendererBusPrevR = 0.0f;
// ── Renderer-audio bus (see engine/RendererBus.h — moved in TLC phase 2)
slopsmith::RendererBus rendererBus;
// Shared consumer step for the duplex and split output paths: drain one
// block from the renderer-bus ring into `dest` (stereo, bus gain applied,
// dest cleared first). Returns numSamples on success, 0 when gated