mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-09-12 10:08:12 +00:00
fix(audio): close the PR #107 review findings
Seven fixes on top of the audio-engine TLC branch, each with the gate that catches its regression. Blocking: - Monitor-mute suppression leaked its refcount. setMonitorMuteSuppressed() became a refcounted acquire/release, but screen.js's callers are deliberately unpaired: resolveChainRebuildGuard() leaves the suppression on when a rebuild yields an empty chain, and returns early without releasing while a provider route is still resolving. Harmless against the old latched bool, a permanent +1 each against a refcount — after a failed tone rebuild the count never returned to zero and monitor mute was silently dead for the rest of the session. The renderer now holds at most one suppression. - Slot ids are monotonic HANDLES (nextSlotId, never reset by clear()), not bounded indices, so argSlotId's 4096 ceiling meant that once a session created its 4096th processor EVERY guarded binding — setBypass, setParameter, remove/moveProcessor, open/closePluginEditor — silently no-opped for the rest of the run. Ceiling removed (same for SetMultiBypass's hardcoded 4096); unknown ids are still rejected by SignalChain::findSlotIndex. - clearChain / removeProcessor / moveProcessor took chainMutationMutex with a blocking lock_guard on the N-API thread — Electron's main thread, and on macOS also the JUCE message thread. LoadPreset/LoadVST hold that mutex across an unbounded plugin init (done->wait() has no timeout by design), so a slow plugin froze the whole main process, every IPC channel with it. They now queue on a libuv worker via queueChainMutation() and resolve a promise; the bridge awaits them so callers still observe the mutation applied. Also: - getChainState() dereferenced raw ProcessorSlot* returned by getAllSlots() after the lock was dropped — a concurrent clear() frees them under the reader. Replaced with SignalChain::getSlotSummaries(), which copies under the lock. getAllSlots() is gone (it had one caller). - The device-settings migration removed the localStorage copy even when the file-store save failed or was unavailable, losing the user's settings. - SetSlotState and GetParameters kept the raw Int32Value() path: IsNumber() is true for NaN, so setSlotState(NaN) wrote onto slot 0 — the same coercion class the rest of the branch fixed. - RendererBus flushed to the LIVE writeIndex, so a disable→re-enable with no pull in between discarded the freshly pushed audio along with the stale tail. It now snapshots the flush target at disable time. - LoadPreset's rebuild barrier is now released by a scope guard, so a throw between arming it and Queue() can't block editor opens forever. Gates: new renderer-bus case (fails on the old flush), new slot-id-handle case (fails on the old ceiling). ctest 9/9, npm test 79 pass / 0 fail, chain-mutation storm green, addon export contract unchanged.
This commit is contained in:
@@ -166,6 +166,35 @@ static void testFlushOnDisable()
|
||||
assert(dl[1] == 7.0f && "post-re-enable audio must be the fresh push");
|
||||
}
|
||||
|
||||
// A pending flush must drop the STALE tail only. If no output callback runs
|
||||
// between the disable and a re-enable (stopped device, device swap), the
|
||||
// flush is still pending when fresh audio arrives — flushing to the live
|
||||
// writeIndex at that point would discard the re-enabled bus's first frames
|
||||
// too, silencing it until it re-primed. The flush target is snapshotted at
|
||||
// disable time instead.
|
||||
static void testFlushSparesPostReEnableAudio()
|
||||
{
|
||||
RendererBus bus;
|
||||
bus.setEnabled(true, 1.0f);
|
||||
const auto stale = rampChunk(RendererBus::kPrimeFrames * 2, 5.0f, 0.0f);
|
||||
bus.push(stale.data(), RendererBus::kPrimeFrames * 2, 48000.0, 48000.0);
|
||||
|
||||
// Disable + re-enable with NO pull in between: the flush is still pending.
|
||||
bus.setEnabled(false, 1.0f);
|
||||
bus.setEnabled(true, 1.0f);
|
||||
|
||||
// Fresh audio pushed while the flush is still pending must survive it.
|
||||
const auto fresh = rampChunk(RendererBus::kPrimeFrames + 65, 7.0f, 0.0f);
|
||||
bus.push(fresh.data(), RendererBus::kPrimeFrames + 65, 48000.0, 48000.0);
|
||||
|
||||
std::vector<float> dl(64), dr(64);
|
||||
assert(bus.pull(dl.data(), dr.data(), 64) == 64
|
||||
&& "fresh post-re-enable audio must not be flushed away with the stale tail");
|
||||
// Frame 0 is the resampler's one-frame interpolation carry (by design);
|
||||
// everything after must be the fresh push, never the flushed 5.0 tail.
|
||||
assert(dl[1] == 7.0f && "flush must drop only the pre-disable tail");
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -202,6 +231,7 @@ int main()
|
||||
testDisabledIsInert();
|
||||
testGainApplied();
|
||||
testFlushOnDisable();
|
||||
testFlushSparesPostReEnableAudio();
|
||||
std::puts("renderer_bus: all cases passed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user