fix(audio): sanitize input/chain/output/backing gains at the engine setters

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>
This commit is contained in:
OmikronApex
2026-07-13 23:28:34 +02:00
co-authored by Claude Fable 5
parent bbb3b58db8
commit 3c8dd62ecb
6 changed files with 104 additions and 5 deletions
+1
View File
@@ -13,6 +13,7 @@ endif()
# Pure-helper tests (no JUCE / no platform deps) build everywhere.
add_subdirectory(audio_sanitize)
add_subdirectory(engine_units)
# Note: enable_testing() lives in the top-level CMakeLists.txt — calling it
# only here would register tests in build/tests/CTestTestfile.cmake but
+7
View File
@@ -0,0 +1,7 @@
# engine_units — home for the per-unit tests of the audio-engine decomposition
# (docs/audio-engine-tlc.md Part IV §4/0.c). JUCE-free targets only: units are
# extracted so they can be tested against a state struct + fake ring, without a
# real device. One executable per unit keeps failures attributable.
add_executable(gain_sanitize_test gain_sanitize_test.cpp)
target_compile_features(gain_sanitize_test PRIVATE cxx_std_17)
add_test(NAME gain_sanitize COMMAND gain_sanitize_test)
+48
View File
@@ -0,0 +1,48 @@
// Pins the Phase 0.b compat decision (docs/audio-engine-tlc.md §4): native
// gain clamp bounds are 0..32 — matching the audio-effects executor's JS-side
// clampGain so a legit high rig gain is never under-shot — with NaN/Inf
// rejected universally; stream/renderer-bus gains keep the tighter 0..8.
#include "../../src/audio/GainSanitize.h"
#include <cassert>
#include <cmath>
#include <cstdio>
#include <limits>
int main()
{
using slopsmith::sanitizeMasterGain;
using slopsmith::sanitizeStreamGain;
const float nan = std::numeric_limits<float>::quiet_NaN();
const float inf = std::numeric_limits<float>::infinity();
struct Case { float in, master, stream; };
const Case cases[] = {
{ 0.0f, 0.0f, 0.0f },
{ 1.0f, 1.0f, 1.0f },
{ 8.0f, 8.0f, 8.0f },
{ 8.5f, 8.5f, 8.0f }, // executor range beyond the stream clamp
{ 32.0f, 32.0f, 8.0f }, // upper compat bound must not under-shoot
{ 33.0f, 32.0f, 8.0f },
{ 1e9f, 32.0f, 8.0f },
{ -1.0f, 0.0f, 0.0f },
{ -0.0f, 0.0f, 0.0f },
{ nan, 0.0f, 0.0f }, // non-finite → silence, never a poisoned mix
{ inf, 0.0f, 0.0f },
{ -inf, 0.0f, 0.0f },
};
for (const auto& c : cases)
{
const float m = sanitizeMasterGain(c.in);
const float s = sanitizeStreamGain(c.in);
assert(std::isfinite(m) && std::isfinite(s));
assert(m == c.master);
assert(s == c.stream);
}
std::puts("gain_sanitize: all cases passed");
return 0;
}