fix(audio): address PR #107 review — close serializer gaps, editor lifetime races, dispatch failures

All 8 CodeRabbit findings verified against the code and fixed:

- ChainOps: macOS LoadVST routes its addProcessor through chainMutationMutex
  (macOS is a first-class platform; deadlock-safe — a worker holding the
  mutex never waits on the Node/main thread there). All four single-slot
  workers (LoadVST/NAM/IR/ReplaceIR) now bump chainGeneration so the
  executor's foreign-write detection sees direct loads, not just presets.
- Rebuild barrier (beginChainRebuild/endChainRebuild): LoadPreset and
  ClearChain arm it before editor teardown; OpenPluginEditor refuses to
  open while a teardown+clear/rebuild is pending (#56 window between
  closeAllPluginEditorWindows returning and the worker taking the mutex).
- EditorWindows: all slot/processor resolution in editor lambdas runs under
  a try_lock of chainMutationMutex (try_lock, never blocking — workers
  holding the mutex block-wait on the message thread). Sandbox promotion
  bumps chainGeneration. editorWindows map is now message-thread-only
  (duplicate-window check and close-erase moved into the queued lambdas).
  Null slot->processor recheck after a faulted promotion capture.
- closeAllPluginEditorWindows returns false on refused post / 15s timeout;
  ClearChain skips the clear and LoadPreset resolves {success:false}
  instead of freeing processors under a live editor.
- AddonContext: dispatchOnMessageThread reports refused-post/timeout;
  doShutdown leaves the message thread running when teardown didn't
  complete instead of unloading mid-destruction.
- RendererBus::push rejects NaN/Inf/non-positive rates and a step that
  underflows to zero; new testRejectsUnusableRates unit case.

Verified: addon builds clean, all 78 JS tests pass (storm, contracts,
executor, N-API fuzz), all 5 engine_units native tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
OmikronApex
2026-07-14 11:56:08 +02:00
co-authored by Claude Fable 5
parent d887c68014
commit ea8c6a9ccd
9 changed files with 308 additions and 86 deletions
+27
View File
@@ -8,6 +8,7 @@
#include <cassert>
#include <cmath>
#include <cstdio>
#include <limits>
#include <vector>
using slopsmith::RendererBus;
@@ -165,9 +166,35 @@ static void testFlushOnDisable()
assert(dl[1] == 7.0f && "post-re-enable audio must be the fresh push");
}
// Rate validation (PR #107 review): non-finite rates cross the JS/IPC
// boundary; NaN passes a plain `<= 0` check, and a subnormal source rate can
// underflow step to 0 — both must be rejected before the resample loop.
// A bad sourceRate falls back to deviceRate (documented behaviour).
static void testRejectsUnusableRates()
{
RendererBus bus;
bus.setEnabled(true, 1.0f);
const auto chunk = rampChunk(128, 1.0f, 0.0f);
const double nan = std::nan("");
const double inf = std::numeric_limits<double>::infinity();
assert(!bus.push(chunk.data(), 128, 48000.0, nan));
assert(!bus.push(chunk.data(), 128, 48000.0, inf));
assert(!bus.push(chunk.data(), 128, 48000.0, -48000.0));
assert(!bus.push(chunk.data(), 128, 48000.0, 0.0));
// step underflow: denormal source over huge device rate → step == 0.
assert(!bus.push(chunk.data(), 128, 5e-324, 1e308));
assert(bus.metrics().pushedFrames == 0 && "rejected pushes must stage nothing");
// NaN/Inf/negative SOURCE rate falls back to deviceRate (step == 1).
assert(bus.push(chunk.data(), 128, nan, 48000.0));
assert(bus.push(chunk.data(), 128, inf, 48000.0));
assert(bus.push(chunk.data(), 128, -1.0, 48000.0));
assert(bus.metrics().pushedFrames > 0);
}
int main()
{
testEqualRateBitExact();
testRejectsUnusableRates();
testResampleContinuityAcrossPushes();
testPrimeGate();
testUnderflowReprimes();