Fix total-audio-silence-on-any-EQ-change: negate ADAU1701 feedback coeffs
designPeakingEq() mapped the RBJ cookbook's a1/a2 straight into the ADAU1701 Param EQ cell's A0/A1 registers. The cookbook's difference equation subtracts the feedback terms; the ADAU1701 cell adds them. Any nonzero band gain therefore applied positive instead of negative feedback at the target frequency, so the biquad's state diverged and railed to a constant (inaudible DC) value -- the "any EQ/enhancement change goes completely silent, even the beep test tone" bug. Confirmed live: SigmaStudio's own direct safeload writes to the same registers (correctly signed by its own tool) only distorted, never silenced, which pointed at this driver's own coefficient math rather than the DSP chain or the safeload mechanism itself. Adds a host test asserting Jury stability for the ADD-convention denominator across the actual bass/stereo-enhance gain values and the GainDb range extremes, so a regression trips ctest instead of requiring a live listening test to notice. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,12 +82,20 @@ BiquadCoefficients designPeakingEq(FrequencyHz center, GainDb gain,
|
||||
const float a1 = -2.0F * cosOmega;
|
||||
const float a2 = 1.0F - alpha / a;
|
||||
|
||||
// RBJ's cookbook form subtracts the feedback terms
|
||||
// (y = ... - (a1/a0) y[n-1] - (a2/a0) y[n-2]); the ADAU1701 Param EQ
|
||||
// cell's A0/A1 registers add them instead (y = ... + A0 y[n-1] +
|
||||
// A1 y[n-2]), so the normalized RBJ a1/a2 must be negated when they
|
||||
// land in A0/A1. Without this, any nonzero gain applies positive
|
||||
// instead of negative feedback at the band's pole, so the biquad's
|
||||
// state diverges and rails to a constant (inaudible DC) value --
|
||||
// root cause of the 2026-08-25 total-silence-on-any-EQ-change bug.
|
||||
return BiquadCoefficients{
|
||||
.b0 = b0 / a0,
|
||||
.b1 = b1 / a0,
|
||||
.b2 = b2 / a0,
|
||||
.a0 = a1 / a0,
|
||||
.a1 = a2 / a0,
|
||||
.a0 = -(a1 / a0),
|
||||
.a1 = -(a2 / a0),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,55 @@ namespace {
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
* The ADAU1701 Param EQ cell's A0/A1 registers ADD the feedback terms
|
||||
* (y = ... + A0 y[n-1] + A1 y[n-2]), unlike the RBJ cookbook's native
|
||||
* subtractive form. Denominator 1 - A0 z^-1 - A1 z^-2 = 0 has poles at the
|
||||
* roots of z^2 - A0 z - A1 = 0; by the Jury test for a monic real quadratic
|
||||
* z^2 + c1 z + c0 (c1 = -A0, c0 = -A1), both poles lie strictly inside the
|
||||
* unit circle iff |A1| < 1, A0 + A1 < 1, and A0 - A1 > -1. A missing
|
||||
* negation when mapping the RBJ a1/a2 into A0/A1 (2026-08-25 total-silence
|
||||
* regression) fails this for real gain/Q combinations, so this guards
|
||||
* against that class of bug rather than just checking magnitudes.
|
||||
*/
|
||||
[[nodiscard]] bool isAdau1701Stable(const core::BiquadCoefficients &c) noexcept
|
||||
{
|
||||
return std::fabs(c.a1) < 1.0F && (c.a0 + c.a1) < 1.0F
|
||||
&& (c.a0 - c.a1) > -1.0F;
|
||||
}
|
||||
|
||||
[[nodiscard]] int runPeakingStabilityTest()
|
||||
{
|
||||
const struct
|
||||
{
|
||||
std::uint32_t hz;
|
||||
float dbGain;
|
||||
float q;
|
||||
} cases[] = {
|
||||
{100U, 9.0F, 0.9F}, // bass-enhance band 1 at max level
|
||||
{400U, 3.0F, 1.0F}, // bass-enhance band 2 at max level
|
||||
{1000U, -1.5F, 1.0F}, // stereo-enhance band 3 at max level
|
||||
{3000U, 2.0F, 1.0F}, // stereo-enhance band 4 at max level
|
||||
{8000U, 4.0F, 1.0F}, // stereo-enhance band 5 at max level
|
||||
{1000U, 12.0F, 10.0F}, // GainDb::kMaxDb at high Q
|
||||
{1000U, -96.0F, 0.2F}, // near GainDb::kMinDb at low Q
|
||||
};
|
||||
|
||||
for (const auto &tc : cases) {
|
||||
const auto center = core::FrequencyHz::tryFromHz(tc.hz);
|
||||
const auto gain = core::GainDb::tryFromDb(tc.dbGain);
|
||||
const core::BiquadCoefficients peaking =
|
||||
core::designPeakingEq(*center, *gain, tc.q);
|
||||
if (!isAdau1701Stable(peaking)) {
|
||||
std::cerr << "unstable ADAU1701 biquad for " << tc.hz << " Hz, "
|
||||
<< tc.dbGain << " dB, Q=" << tc.q << ": a0=" << peaking.a0
|
||||
<< " a1=" << peaking.a1 << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -64,5 +113,8 @@ int main()
|
||||
if (runFlatBiquadTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (runPeakingStabilityTest() != EXIT_SUCCESS) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user