Make the high-pass feedback tap a fixed 1/48000 s interval so resonance stops scaling with sample rate; 48k and 44.1k bit-identical
This commit is contained in:
+306
-48
@@ -238,17 +238,19 @@ static void testHighQPeaksAtCutoffInBothModes() {
|
||||
|
||||
// Drive real sines through VoiceFilter and measure steady-state RMS. Unlike the analytic
|
||||
// check above this also exercises the high-pass input-feedback path, which is outside the
|
||||
// coefficient transfer function.
|
||||
// coefficient transfer function. The settle and measure windows are wall-clock, not sample
|
||||
// counts, so every rate integrates the same amount of signal.
|
||||
static double measuredRms(FilterMode mode, float cutoffNorm, float resNorm, double freqHz,
|
||||
double sr) {
|
||||
double sr, double amp = 1.0) {
|
||||
VoiceFilter f;
|
||||
f.prepare({mode, cutoffNorm, resNorm}, sr);
|
||||
f.reset();
|
||||
|
||||
const int settle = 24000, measure = 24000;
|
||||
const int settle = static_cast<int>(sr * 0.15);
|
||||
const int measure = static_cast<int>(sr * 0.10);
|
||||
double sumSq = 0.0;
|
||||
for (int i = 0; i < settle + measure; ++i) {
|
||||
const float x = static_cast<float>(std::sin(2.0 * kPi * freqHz * i / sr));
|
||||
const float x = static_cast<float>(amp * std::sin(2.0 * kPi * freqHz * i / sr));
|
||||
const float y = f.process(0, x);
|
||||
if (i >= settle) sumSq += static_cast<double>(y) * y;
|
||||
}
|
||||
@@ -280,6 +282,233 @@ static void testMeasuredResponsePeaksAtCutoffInBothModes() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sample-rate invariance
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// The rates the invariance claim is made over. 88.2k is deliberately included: it is the rate
|
||||
// whose calibrated feedback delay lands between two whole taps, so it is the one the
|
||||
// interpolating read has to earn.
|
||||
static const double kRates[] = {44100.0, 48000.0, 88200.0, 96000.0, 192000.0};
|
||||
static constexpr int kRateCount = 5;
|
||||
static constexpr int kRef48k = 1; // index of the reference rate within kRates
|
||||
|
||||
// Resonant emphasis: level at the cutoff over the passband level. Measured at the requested
|
||||
// cutoff rather than at the scanned peak so no frequency-grid quantization leaks into the
|
||||
// comparison. The passband reference is the same frequency at every rate, or the ratio would
|
||||
// compare a different measurement at each rate -- and it must stay well clear of the LOWEST
|
||||
// Nyquist tested, since a high-pass reference near 44.1k's band edge measures the bilinear
|
||||
// warping rather than the resonance.
|
||||
static double emphasisAtCutoff(FilterMode mode, double fcHz, float resNorm, double sr) {
|
||||
const float cn = filterNormFromCutoffHz(static_cast<float>(fcHz));
|
||||
const double refHz = (mode == FilterMode::LowPass) ? fcHz / 8.0 : fcHz * 8.0;
|
||||
return measuredRms(mode, cn, resNorm, fcHz, sr, 0.25) /
|
||||
measuredRms(mode, cn, resNorm, refHz, sr, 0.25);
|
||||
}
|
||||
|
||||
// The feedback loop's contribution alone: the measured closed-loop level at a frequency over the
|
||||
// level the bare coefficients predict there. Dividing the coefficient response out removes the
|
||||
// bilinear discretization difference between rates -- which is real, correct, and not something
|
||||
// a feedback fix can or should touch -- leaving exactly the loop under audit. In low-pass mode
|
||||
// there is no loop, so this is identically 1 at every rate.
|
||||
static double feedbackContribution(FilterMode mode, double fcHz, float resNorm, double sr) {
|
||||
const float cn = filterNormFromCutoffHz(static_cast<float>(fcHz));
|
||||
VoiceFilter f;
|
||||
f.prepare({mode, cn, resNorm}, sr);
|
||||
const double openLoopRms = magnitudeAt(f.coeffs(), fcHz, sr) * 0.25 / std::sqrt(2.0);
|
||||
return measuredRms(mode, cn, resNorm, fcHz, sr, 0.25) / openLoopRms;
|
||||
}
|
||||
|
||||
// Where the response actually peaks, as a multiple of the requested cutoff.
|
||||
static double peakOverCutoff(FilterMode mode, double fcHz, float resNorm, double sr) {
|
||||
const float cn = filterNormFromCutoffHz(static_cast<float>(fcHz));
|
||||
double peak = 0.0, peakF = 0.0;
|
||||
for (int i = 0; i <= 12; ++i) {
|
||||
const double f = fcHz * std::pow(2.0, -0.5 + i / 12.0);
|
||||
const double r = measuredRms(mode, cn, resNorm, f, sr, 0.25);
|
||||
if (r > peak) { peak = r; peakF = f; }
|
||||
}
|
||||
return peakF / fcHz;
|
||||
}
|
||||
|
||||
// The defect these pin: the high-pass feedback loop closes once per sample, so while its tap was
|
||||
// the immediately previous output the loop's phase at the cutoff -- and with it the resonant
|
||||
// emphasis -- scaled with the sample rate. Against that one-sample tap, emphasisAtCutoff for
|
||||
// fc=1 kHz, res=1.0 measured 5.46 at 48k rising monotonically to 6.04 at 192k (10.5%), and
|
||||
// feedbackContribution for fc=4 kHz, res=1.0 ran 0.443 at 48k against 0.506 at 192k (14.4%).
|
||||
// Both now sit inside the bounds below.
|
||||
//
|
||||
// The two tolerances split on the reference rate, and the split is load-bearing rather than
|
||||
// convenient. At or above 48k the calibrated interval is at least one sample, so the tap
|
||||
// reproduces it and only the bilinear discretization difference remains. Below it -- 44.1k --
|
||||
// one sample is ALREADY longer than the interval, so the delay cannot be shortened to match
|
||||
// without a sub-sample delay the loop cannot contain; 44.1k is left exactly where it has always
|
||||
// been, which is up to 6% off 48k at the top of the cutoff range.
|
||||
static constexpr double kAtOrAboveReferenceTolerance = 0.02;
|
||||
static constexpr double kBelowReferenceTolerance = 0.08;
|
||||
|
||||
static void checkInvariant(const char* what, FilterMode mode, double fcHz, float resNorm,
|
||||
double (*measure)(FilterMode, double, float, double)) {
|
||||
const double reference = measure(mode, fcHz, resNorm, kRates[kRef48k]);
|
||||
for (int r = 0; r < kRateCount; ++r) {
|
||||
const double v = measure(mode, fcHz, resNorm, kRates[r]);
|
||||
const double deviation = std::fabs(v - reference) / reference;
|
||||
const double tolerance = kRates[r] >= kRates[kRef48k] ? kAtOrAboveReferenceTolerance
|
||||
: kBelowReferenceTolerance;
|
||||
if (!(deviation <= tolerance)) {
|
||||
std::printf("FAIL line %d: %s %s fc=%.0f res=%.2f at %.0f Hz: %.5f vs 48k %.5f "
|
||||
"(%.2f%% > %.2f%%)\n",
|
||||
__LINE__, what, mode == FilterMode::LowPass ? "LP" : "HP", fcHz, resNorm,
|
||||
kRates[r], v, reference, deviation * 100.0, tolerance * 100.0);
|
||||
++g_fail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End-to-end: the emphasis a listener hears, coefficients and feedback together. Held to cutoffs
|
||||
// whose passband reference (8x the cutoff) stays well below 44.1k's band edge -- higher cutoffs
|
||||
// are covered by the isolated test below, which does not need a passband reference at all.
|
||||
static void testHighPassResonanceIsRateInvariant() {
|
||||
for (float res : {0.2f, 0.5f, 1.0f}) {
|
||||
checkInvariant("emphasis", FilterMode::HighPass, 250.0, res, emphasisAtCutoff);
|
||||
checkInvariant("emphasis", FilterMode::HighPass, 1000.0, res, emphasisAtCutoff);
|
||||
}
|
||||
}
|
||||
|
||||
// The low-pass has no feedback path, so it was already invariant. Pinning it is the control: it
|
||||
// proves the measurement detects what it claims to, and it keeps a future feedback path on the
|
||||
// low-pass from acquiring the same defect unnoticed.
|
||||
static void testLowPassResonanceIsRateInvariant() {
|
||||
for (float res : {0.2f, 0.5f, 1.0f}) {
|
||||
checkInvariant("emphasis", FilterMode::LowPass, 250.0, res, emphasisAtCutoff);
|
||||
checkInvariant("emphasis", FilterMode::LowPass, 1000.0, res, emphasisAtCutoff);
|
||||
checkInvariant("emphasis", FilterMode::LowPass, 4000.0, res, emphasisAtCutoff);
|
||||
}
|
||||
}
|
||||
|
||||
// The precise form of the same claim, with the discretization difference divided out, so it also
|
||||
// holds at the top of the cutoff range where a passband reference cannot sit clear of 44.1k's
|
||||
// band edge.
|
||||
static void testFeedbackLoopContributionIsRateInvariant() {
|
||||
for (float res : {0.2f, 0.5f, 1.0f}) {
|
||||
for (double fc : {250.0, 1000.0, 4000.0}) {
|
||||
checkInvariant("loop", FilterMode::HighPass, fc, res, feedbackContribution);
|
||||
checkInvariant("loop", FilterMode::LowPass, fc, res, feedbackContribution);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void testResonantPeakTracksCutoffAtEveryRate() {
|
||||
for (FilterMode mode : {FilterMode::LowPass, FilterMode::HighPass}) {
|
||||
for (double fc : {250.0, 1000.0, 4000.0}) {
|
||||
for (int r = 0; r < kRateCount; ++r) {
|
||||
// At full resonance there is a real peak to find; a quarter octave either side
|
||||
// of the requested cutoff is the same window the 48k-only test uses.
|
||||
const double ratio = peakOverCutoff(mode, fc, 1.0f, kRates[r]);
|
||||
if (!(ratio > 1.0 / 1.19 && ratio < 1.19)) {
|
||||
std::printf("FAIL line %d: %s peak at %.3f x fc (fc=%.0f, sr=%.0f)\n",
|
||||
__LINE__, mode == FilterMode::LowPass ? "LP" : "HP", ratio, fc,
|
||||
kRates[r]);
|
||||
++g_fail;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 48k is the rate the feedback constants were voiced at, and the rate Daniel's ear judgments
|
||||
// were made against, so making the other rates match it must not move it. These literals were
|
||||
// captured from the build BEFORE the fixed-time feedback tap landed; the tap resolves to
|
||||
// exactly one sample at 48k, so they must reproduce bit-for-bit rather than merely closely.
|
||||
static void testFortyEightKilohertzBehaviorIsUnchanged() {
|
||||
struct Pin {
|
||||
FilterMode mode;
|
||||
double y1, y7, y31, y127, energy, sineRms;
|
||||
};
|
||||
const Pin pins[2] = {
|
||||
{FilterMode::LowPass, 0.016871979, 0.098936319, -0.084338546, -0.044524558, 0.652648822,
|
||||
1.767755710},
|
||||
{FilterMode::HighPass, -0.184770823, -0.082661532, 0.057580549, -0.008336116, 1.427662234,
|
||||
0.895141269},
|
||||
};
|
||||
|
||||
for (const Pin& p : pins) {
|
||||
VoiceFilter f;
|
||||
f.prepare({p.mode, filterNormFromCutoffHz(1000.0f), 1.0f}, 48000.0);
|
||||
f.reset();
|
||||
double energy = 0.0;
|
||||
for (int i = 0; i < 4096; ++i) {
|
||||
const float y = f.process(0, i == 0 ? 1.0f : 0.0f);
|
||||
energy += static_cast<double>(y) * y;
|
||||
if (i == 1) CHECK_NEAR(y, p.y1, 1e-7);
|
||||
if (i == 7) CHECK_NEAR(y, p.y7, 1e-7);
|
||||
if (i == 31) CHECK_NEAR(y, p.y31, 1e-7);
|
||||
if (i == 127) CHECK_NEAR(y, p.y127, 1e-7);
|
||||
}
|
||||
CHECK_NEAR(energy, p.energy, 1e-7);
|
||||
|
||||
VoiceFilter g;
|
||||
g.prepare({p.mode, filterNormFromCutoffHz(1000.0f), 1.0f}, 48000.0);
|
||||
g.reset();
|
||||
double sumSq = 0.0;
|
||||
for (int i = 0; i < 28800; ++i) {
|
||||
const float x = static_cast<float>(0.25 * std::sin(2.0 * kPi * 1000.0 * i / 48000.0));
|
||||
const float y = g.process(0, x);
|
||||
if (i >= 14400) sumSq += static_cast<double>(y) * y;
|
||||
}
|
||||
CHECK_NEAR(std::sqrt(sumSq / 14400.0), p.sineRms, 1e-7);
|
||||
}
|
||||
}
|
||||
|
||||
// The tap is a fixed INTERVAL, so the sample offset it resolves to scales with the rate. Read
|
||||
// out of the filter's behavior, not its internals: run an impulse through the high-pass and
|
||||
// alongside it the bare difference equation on the SAME coefficients with no feedback at all.
|
||||
// The tap reads y[n-D], and every earlier history slot is zero, so the first sample at which the
|
||||
// two can possibly diverge is exactly D. Against the pre-fix one-sample tap this reports 1 at
|
||||
// every rate; it must now report 1, 1, 1, 2, 4.
|
||||
static void testFeedbackTapOffsetScalesWithSampleRate() {
|
||||
const int expected[kRateCount] = {1, 1, 1, 2, 4};
|
||||
for (int r = 0; r < kRateCount; ++r) {
|
||||
VoiceFilter f;
|
||||
f.prepare({FilterMode::HighPass, filterNormFromCutoffHz(1000.0f), 1.0f}, kRates[r]);
|
||||
f.reset();
|
||||
const BiquadCoeffs c = f.coeffs();
|
||||
|
||||
float x1 = 0.0f, x2 = 0.0f, y1 = 0.0f, y2 = 0.0f;
|
||||
int firstDivergence = -1;
|
||||
for (int i = 0; i < 64 && firstDivergence < 0; ++i) {
|
||||
const float x = (i == 0) ? 1.0f : 0.0f;
|
||||
const float actual = f.process(0, x);
|
||||
const float noFeedback = c.b0 * x + c.b1 * x1 + c.b2 * x2 - c.a1 * y1 - c.a2 * y2;
|
||||
x2 = x1;
|
||||
x1 = x;
|
||||
y2 = y1;
|
||||
y1 = noFeedback;
|
||||
if (actual != noFeedback) firstDivergence = i;
|
||||
}
|
||||
if (firstDivergence != expected[r]) {
|
||||
std::printf("FAIL line %d: sr=%.0f feedback first reaches the output at sample %d, "
|
||||
"expected %d\n",
|
||||
__LINE__, kRates[r], firstDivergence, expected[r]);
|
||||
++g_fail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The floor is load-bearing, not defensive: below 48k one sample is ALREADY longer than the
|
||||
// calibrated interval, so the offset cannot shrink to match without a sub-sample delay the loop
|
||||
// cannot contain -- it would be algebraic and uncomputable. A rate at or below the reference
|
||||
// therefore keeps the firmware's single tap, and a non-positive rate lands on the same floor
|
||||
// rather than on an invented rate.
|
||||
static void testFeedbackTapNeverFallsBelowOneSample() {
|
||||
for (double sr : {-48000.0, 0.0, 1000.0, 22050.0, 44100.0, 48000.0}) {
|
||||
VoiceFilter f;
|
||||
f.prepare({FilterMode::HighPass, filterNormFromCutoffHz(1000.0f), 1.0f}, sr);
|
||||
f.reset();
|
||||
for (int i = 0; i < 512; ++i) CHECK(std::isfinite(f.process(0, i == 0 ? 1.0f : 0.0f)));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stability
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -292,13 +521,16 @@ static void testFullRangeCutoffSweepAtAudioRateStaysBounded() {
|
||||
return static_cast<float>(static_cast<int>(rng >> 9) - (1 << 22)) / static_cast<float>(1 << 22);
|
||||
};
|
||||
|
||||
for (double sr : {44100.0, 48000.0, 96000.0}) {
|
||||
for (int r = 0; r < kRateCount; ++r) {
|
||||
const double sr = kRates[r];
|
||||
for (FilterMode mode : {FilterMode::LowPass, FilterMode::HighPass}) {
|
||||
for (float res : {0.0f, 0.5f, 1.0f}) {
|
||||
for (int direction = 0; direction < 2; ++direction) {
|
||||
VoiceFilter f;
|
||||
f.reset();
|
||||
const int n = 48000;
|
||||
// A fixed WALL-CLOCK sweep: the same cutoff travel per second at every rate,
|
||||
// so the per-sample coefficient step gets no gentler as the rate rises.
|
||||
const int n = static_cast<int>(sr * 0.25);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const float t = static_cast<float>(i) / static_cast<float>(n - 1);
|
||||
// Per-sample coefficient update across the whole cutoff travel.
|
||||
@@ -314,37 +546,50 @@ static void testFullRangeCutoffSweepAtAudioRateStaysBounded() {
|
||||
}
|
||||
}
|
||||
|
||||
// The decay to the floor is a fixed WALL-CLOCK time (~0.21 s at these settings), not a fixed
|
||||
// sample count -- so the budget has to scale with the rate. A fixed 20000-sample budget is itself
|
||||
// a rate assumption: it is ample at 48k and expires mid-decay at 96k and above.
|
||||
static void testStateFlushesToZeroWithoutStallingInDenormals() {
|
||||
const double sr = 48000.0;
|
||||
for (FilterMode mode : {FilterMode::LowPass, FilterMode::HighPass}) {
|
||||
VoiceFilter f;
|
||||
f.prepare({mode, filterNormFromCutoffHz(1000.0f), 1.0f}, sr);
|
||||
f.reset();
|
||||
for (int r = 0; r < kRateCount; ++r) {
|
||||
const double sr = kRates[r];
|
||||
const int budget = static_cast<int>(sr * 0.5);
|
||||
for (FilterMode mode : {FilterMode::LowPass, FilterMode::HighPass}) {
|
||||
VoiceFilter f;
|
||||
f.prepare({mode, filterNormFromCutoffHz(1000.0f), 1.0f}, sr);
|
||||
f.reset();
|
||||
|
||||
// Excite, then hard-cut to silence the way a released voice does.
|
||||
for (int i = 0; i < 480; ++i) {
|
||||
f.process(0, 0.5f * static_cast<float>(std::sin(2.0 * kPi * 1000.0 * i / sr)));
|
||||
}
|
||||
|
||||
int subnormalSamples = 0;
|
||||
int silentAt = -1;
|
||||
for (int i = 0; i < 20000; ++i) {
|
||||
f.process(0, 0.0f);
|
||||
const VoiceFilter::State& s = f.state(0);
|
||||
const float vals[5] = {s.x1, s.x2, s.y1, s.y2, s.fb};
|
||||
for (float v : vals) {
|
||||
if (v != 0.0f && std::fabs(v) < FLT_MIN) { ++subnormalSamples; break; }
|
||||
// Excite, then hard-cut to silence the way a released voice does.
|
||||
const int excite = static_cast<int>(sr * 0.01);
|
||||
for (int i = 0; i < excite; ++i) {
|
||||
f.process(0, 0.5f * static_cast<float>(std::sin(2.0 * kPi * 1000.0 * i / sr)));
|
||||
}
|
||||
if (silentAt < 0 && f.isSilent()) silentAt = i;
|
||||
|
||||
int subnormalSamples = 0;
|
||||
int silentAt = -1;
|
||||
for (int i = 0; i < budget; ++i) {
|
||||
f.process(0, 0.0f);
|
||||
const VoiceFilter::State& s = f.state(0);
|
||||
bool subnormal = false;
|
||||
for (float v : {s.x1, s.x2, s.y1, s.y2}) {
|
||||
if (v != 0.0f && std::fabs(v) < FLT_MIN) subnormal = true;
|
||||
}
|
||||
for (float v : s.fb) {
|
||||
if (v != 0.0f && std::fabs(v) < FLT_MIN) subnormal = true;
|
||||
}
|
||||
if (subnormal) ++subnormalSamples;
|
||||
if (silentAt < 0 && f.isSilent()) silentAt = i;
|
||||
}
|
||||
// Without the flush the state grinds down through the subnormal range for thousands
|
||||
// of samples; a stray sample or two at a zero crossing is not a stall. The feedback
|
||||
// tap line holds copies of the flushed y, so it drains behind it rather than feeding
|
||||
// subnormals back into the loop.
|
||||
CHECK(subnormalSamples <= 2);
|
||||
CHECK(silentAt >= 0);
|
||||
CHECK(silentAt < budget);
|
||||
// And it stays silent — a flush that perturbs the feedback loop would re-excite it.
|
||||
for (int i = 0; i < 1000; ++i) CHECK(f.process(0, 0.0f) == 0.0f);
|
||||
CHECK(f.isSilent());
|
||||
}
|
||||
// Without the flush the state grinds down through the subnormal range for thousands
|
||||
// of samples; a stray sample or two at a zero crossing is not a stall.
|
||||
CHECK(subnormalSamples <= 2);
|
||||
CHECK(silentAt >= 0);
|
||||
CHECK(silentAt < 20000);
|
||||
// And it stays silent — a flush that perturbs the feedback loop would re-excite it.
|
||||
for (int i = 0; i < 1000; ++i) CHECK(f.process(0, 0.0f) == 0.0f);
|
||||
CHECK(f.isSilent());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,25 +599,31 @@ static void testStateFlushesToZeroWithoutStallingInDenormals() {
|
||||
// with y1/y2 discards that pinned history; the next sample then recomputes a full-amplitude
|
||||
// step from b0*in alone, which re-rings and repeats forever (a click train). This must fail
|
||||
// against a flush that also clears x1/x2.
|
||||
// Run at full resonance as well as none: at res=0 the feedback share is zero and the tap line is
|
||||
// inert, so that case alone would never notice the tap line failing to drain behind a flush.
|
||||
static void testHighPassSustainedDCDoesNotReRing() {
|
||||
const double sr = 48000.0;
|
||||
VoiceFilter f;
|
||||
f.prepare({FilterMode::HighPass, filterNormFromCutoffHz(1000.0f), 0.0f}, sr);
|
||||
f.reset();
|
||||
for (int r = 0; r < kRateCount; ++r) {
|
||||
for (float res : {0.0f, 1.0f}) {
|
||||
const double sr = kRates[r];
|
||||
VoiceFilter f;
|
||||
f.prepare({FilterMode::HighPass, filterNormFromCutoffHz(1000.0f), res}, sr);
|
||||
f.reset();
|
||||
|
||||
const int settle = 1000;
|
||||
float worstAfterSettle = 0.0f;
|
||||
for (int i = 0; i < 20000; ++i) {
|
||||
const float y = f.process(0, 1.0f);
|
||||
if (i >= settle) {
|
||||
const float a = std::fabs(y);
|
||||
if (a > worstAfterSettle) worstAfterSettle = a;
|
||||
const int settle = static_cast<int>(sr * 0.05);
|
||||
float worstAfterSettle = 0.0f;
|
||||
for (int i = 0; i < static_cast<int>(sr * 0.5); ++i) {
|
||||
const float y = f.process(0, 1.0f);
|
||||
if (i >= settle) {
|
||||
const float a = std::fabs(y);
|
||||
if (a > worstAfterSettle) worstAfterSettle = a;
|
||||
}
|
||||
}
|
||||
// A correct flush leaves the settled output pinned near zero. The click train this
|
||||
// regresses against recurs every ~4760 samples at 48k at a magnitude around 0.6 --
|
||||
// nowhere near this tolerance.
|
||||
CHECK(worstAfterSettle < 1e-3f);
|
||||
}
|
||||
}
|
||||
// A correct flush leaves the settled output pinned near zero. The click train this
|
||||
// regresses against recurs every ~4760 samples at a magnitude around 0.6 -- nowhere near
|
||||
// this tolerance.
|
||||
CHECK(worstAfterSettle < 1e-3f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -473,6 +724,13 @@ int main() {
|
||||
testPassbandGainIsUnity();
|
||||
testHighQPeaksAtCutoffInBothModes();
|
||||
testMeasuredResponsePeaksAtCutoffInBothModes();
|
||||
testHighPassResonanceIsRateInvariant();
|
||||
testLowPassResonanceIsRateInvariant();
|
||||
testFeedbackLoopContributionIsRateInvariant();
|
||||
testResonantPeakTracksCutoffAtEveryRate();
|
||||
testFortyEightKilohertzBehaviorIsUnchanged();
|
||||
testFeedbackTapOffsetScalesWithSampleRate();
|
||||
testFeedbackTapNeverFallsBelowOneSample();
|
||||
testFullRangeCutoffSweepAtAudioRateStaysBounded();
|
||||
testStateFlushesToZeroWithoutStallingInDenormals();
|
||||
testHighPassSustainedDCDoesNotReRing();
|
||||
|
||||
Reference in New Issue
Block a user