Two deep-read fixes now homed in their phase-1/2 units:
RendererBus (§4): setEnabled(false) no longer writes readIndex from the
control thread — the ring's designated consumer-side writer is pull(). The
drop-on-disable is now a flushRequested atomic the consumer honors at its
next pull, closing the last SPSC-discipline hole (a concurrent pull
mid-drain could overwrite the control thread's store and replay a stale
tail after re-enable). New unit test pins flush-then-fresh-audio.
setAudioDevices (§3): the restart decision reads state.userWantsAudio
(intent, written only by start/stopAudio) instead of the racy device-state
flag that transient audioDeviceStopped() fires clear — a reconfigure landing
inside a transient-stop window no longer leaves the engine configured but
stopped ('no audio until Start/Apply is pressed again').
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moves probeDeviceOptionsDual, applyDuplexSetup, applySplitSetup, and
teardownSplitMode verbatim into src/audio/engine/DeviceSetup.{h,cpp}. The
component holds references to the two device managers + EngineState and owns
no lifetime; engine-owned collaborators (monitor chain, split output ring +
counters, output callback registration) are passed by reference per call.
setAudioDevices stays on the facade as the orchestrator. The public
DeviceOptions/DeviceConfig/DeviceConfigResult shapes move to the slopsmith
namespace with using-aliases on AudioEngine, so the NodeAddon spelling is
unchanged.
Lands the deep-read §7 dedupe structurally: the <=0.5 rate tolerance,
midpoint-rounding fail-closed candidate, and empty-name→first-enumerated
resolution now exist once (RateMatch.h — JUCE-free + unit-tested boundary
cases — and DeviceSetup::resolveDeviceName/rateSupportedBy) instead of three
hand-synced copies.
Full device-matrix validation (WASAPI shared/exclusive, ASIO, dual-type
split) rides the next tester build per the plan's phase-4 gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Moves the shared run-state atomics (currentSampleRate, block sizes,
duplexMode, run flags) into slopsmith::EngineState (src/audio/engine/) so
later extracted units take EngineState& and stay unit-testable without JUCE
devices. AudioEngine binds the members back by reference under their
historical names — zero call-site churn, behavior-identical.
The old audioRunning conflated user intent with device state (deep-read
§3/§6); it is now state.deviceRunning (same semantics, isAudioRunning compat
pinned) plus a new state.userWantsAudio written only by startAudio/stopAudio.
Nothing reads the intent flag yet — phase 8 flips setAudioDevices' restart
decision onto it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the four hand-maintained copies of the packed-LR SPSC design
(split-mode output ring, per-InputDeviceSlot rings, stream-sink ring,
renderer-bus ring) with slopsmith::PackedStereoRing<NFrames>
(src/audio/engine/PackedStereoRing.h). The template owns the storage,
power-of-two/lock-free asserts, pack/unpack, producer publish, reset, the
w<r resync, and the lapped catch-up; per-site consumer policy (pull-vs-
consume skew, renderer prime/fill-clamp) stays verbatim at the call sites.
Pure code move per the TLC plan — no behavior change; the renderer bus's
control-thread readIndex write on disable (deep-read §4) is deliberately
preserved and gets its flush-flag fix in phase 2.
Unit-tested in tests/engine_units/packed_stereo_ring_test.cpp: threaded
tear-freedom under lapping (2M frames), drop-oldest catch-up, index-reset
resync, pull-vs-consume skew.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NaN/Inf from any JS caller (audio:setGain does no validation) previously
reached the gain atomics raw; a NaN master gain multiplies the whole device
output to NaN and poisons the peak meters (TLC deep-read §2). Clamp at the
four setters — the single choke point covering the legacy facade, the
source-indexed API, and the audio-effects executor.
Bounds 0..32 match the executor's clampGain (Phase 0.b compat pin); stream/
renderer-bus keep their historical 0..8 via the same JUCE-free helper, now
testable in the new tests/engine_units target (Phase 0.c harness).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>