Add reverse-mode stability tests and reverse golden render#80
Conversation
|
Warning Review limit reached
More reviews will be available in 15 minutes and 52 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces reverse-mode golden rendering and comprehensive unit tests for the OrbitDelayCore class, verifying periodic window resets, abrupt delay changes, stereo/mono equivalence, and high-feedback numerical stability. The reviewer identified a critical issue across all tests and the golden render: because the default tempo delay is too large, the delay is clamped to the buffer limits, resulting in silent outputs and trivial test passes. To resolve this, the reviewer suggests configuring the tempo BPM and note division to extreme values to reduce the base delay, allowing the signals to be properly processed and verified.
| OrbitDelayCore reverseCore; | ||
| reverseCore.reset(48000.0f); | ||
| reverseCore.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse); | ||
| reverseCore.setOrbit(0.95f); | ||
| reverseCore.setOffsetSamples(180.0f); |
There was a problem hiding this comment.
Issue: Golden Render Outputs Only Zeros
The default tempoDelaySamples is initialized to 24000.0f (based on 120 BPM and 1.0 note division at 48kHz). Since the buffer size reverseDelayL is only 2048, the effective reverse delay is clamped to 2032.0f samples. Because the golden render only processes 384 samples, the read pointer never reaches any written input signal, resulting in a golden reference file containing only zeros (0.0f).
Solution
Configure the tempo and note division to their extreme values (320.0f BPM and 0.0625f note division) before resetting the core to reduce the base tempoDelaySamples to 562.5f. Then, set offsetSamples to -382.5f to achieve the target delay of exactly 180.0f samples. This ensures the input signal is actually processed and rendered into the golden file.
| OrbitDelayCore reverseCore; | |
| reverseCore.reset(48000.0f); | |
| reverseCore.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse); | |
| reverseCore.setOrbit(0.95f); | |
| reverseCore.setOffsetSamples(180.0f); | |
| OrbitDelayCore reverseCore; | |
| reverseCore.setTempoBpm(320.0f); | |
| reverseCore.setNoteDivision(0.0625f); | |
| reverseCore.reset(48000.0f); | |
| reverseCore.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse); | |
| reverseCore.setOrbit(0.95f); | |
| reverseCore.setOffsetSamples(-382.5f); |
| // Reverse ativo com delay fixo: periodicidade por janela + estabilidade numérica. | ||
| OrbitDelayCore reversePeriodic; | ||
| reversePeriodic.reset(48000.0f); | ||
| std::vector<float> reverseDelay(2048u, 0.0f); | ||
| if (!reversePeriodic.attachBufferMono(reverseDelay.data(), static_cast<uint32_t>(reverseDelay.size()))) { | ||
| return fail("reverse periodic test attach should succeed"); | ||
| } | ||
| reversePeriodic.setMix(1.0f); | ||
| reversePeriodic.setFeedback(0.0f); | ||
| reversePeriodic.setOffsetSamples(96.0f); |
There was a problem hiding this comment.
Issue: Test Passes on Silence
The default tempoDelaySamples is 24000.0f. Since the buffer size is 2048, the delay is clamped to 2032.0f samples. Because the test only processes 257 samples, the read pointer never reaches the written impulse, and the output is entirely 0.0f. Thus, reverseA and reverseB are both entirely zero, making the assertion pass trivially on silence without actually testing the periodic window reset.
Solution
Set the tempo to 320.0f BPM and note division to 0.0625f before resetting the core to reduce tempoDelaySamples to 562.5f. Then, set offsetSamples to -434.5f to achieve a delay of exactly 128.0f samples. This makes the window size exactly 128 samples, so reverseA and reverseB will contain the actual impulse and match perfectly.
| // Reverse ativo com delay fixo: periodicidade por janela + estabilidade numérica. | |
| OrbitDelayCore reversePeriodic; | |
| reversePeriodic.reset(48000.0f); | |
| std::vector<float> reverseDelay(2048u, 0.0f); | |
| if (!reversePeriodic.attachBufferMono(reverseDelay.data(), static_cast<uint32_t>(reverseDelay.size()))) { | |
| return fail("reverse periodic test attach should succeed"); | |
| } | |
| reversePeriodic.setMix(1.0f); | |
| reversePeriodic.setFeedback(0.0f); | |
| reversePeriodic.setOffsetSamples(96.0f); | |
| // Reverse ativo com delay fixo: periodicidade por janela + estabilidade numérica. | |
| OrbitDelayCore reversePeriodic; | |
| reversePeriodic.setTempoBpm(320.0f); | |
| reversePeriodic.setNoteDivision(0.0625f); | |
| reversePeriodic.reset(48000.0f); | |
| std::vector<float> reverseDelay(2048u, 0.0f); | |
| if (!reversePeriodic.attachBufferMono(reverseDelay.data(), static_cast<uint32_t>(reverseDelay.size()))) { | |
| return fail("reverse periodic test attach should succeed"); | |
| } | |
| reversePeriodic.setMix(1.0f); | |
| reversePeriodic.setFeedback(0.0f); | |
| reversePeriodic.setOffsetSamples(-434.5f); |
| // Mudança abrupta de delay: reset da janela e continuidade sem click explosivo. | ||
| OrbitDelayCore reverseJump; | ||
| reverseJump.reset(48000.0f); | ||
| std::vector<float> reverseJumpDelay(4096u, 0.0f); | ||
| if (!reverseJump.attachBufferMono(reverseJumpDelay.data(), static_cast<uint32_t>(reverseJumpDelay.size()))) { | ||
| return fail("reverse abrupt-delay test attach should succeed"); | ||
| } | ||
| reverseJump.setMix(1.0f); | ||
| reverseJump.setFeedback(0.0f); | ||
| reverseJump.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse); | ||
| reverseJump.setOffsetSamples(64.0f); | ||
| for (uint32_t i = 0; i < 160u; ++i) { | ||
| reverseJump.processSampleMono((i == 0u) ? 1.0f : 0.0f); | ||
| } | ||
| std::vector<float> beforeJump(64u, 0.0f); | ||
| for (uint32_t i = 0; i < beforeJump.size(); ++i) { | ||
| beforeJump[i] = reverseJump.processSampleMono(0.0f); | ||
| } | ||
| reverseJump.setOffsetSamples(256.0f); |
There was a problem hiding this comment.
Issue: Test Passes on Silence and Incorrect Delay Jump
Due to the default tempoDelaySamples of 24000.0f, both 64.0f and 256.0f offsets are clamped to the buffer limit of 4080.0f samples. This means the delay doesn't actually change, the window counter is not reset, and the test only passes because the output is entirely silent (0.0f).
Solution
- Set the tempo to
320.0fBPM and note division to0.0625fto reducetempoDelaySamplesto562.5f. - Set the initial delay to
256.0fsamples (offsetSamples = -306.5f). - Set the post-jump delay to
64.0fsamples (offsetSamples = -498.5f).
This ensures the delay actually changes, the window counter resets, and the output becomes periodic with a period of 64 samples, making afterJumpA and afterJumpB identical with actual signal.
// Mudança abrupta de delay: reset da janela e continuidade sem click explosivo.
OrbitDelayCore reverseJump;
reverseJump.setTempoBpm(320.0f);
reverseJump.setNoteDivision(0.0625f);
reverseJump.reset(48000.0f);
std::vector<float> reverseJumpDelay(4096u, 0.0f);
if (!reverseJump.attachBufferMono(reverseJumpDelay.data(), static_cast<uint32_t>(reverseJumpDelay.size()))) {
return fail("reverse abrupt-delay test attach should succeed");
}
reverseJump.setMix(1.0f);
reverseJump.setFeedback(0.0f);
reverseJump.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse);
reverseJump.setOffsetSamples(-306.5f);
for (uint32_t i = 0; i < 160u; ++i) {
reverseJump.processSampleMono((i == 0u) ? 1.0f : 0.0f);
}
std::vector<float> beforeJump(64u, 0.0f);
for (uint32_t i = 0; i < beforeJump.size(); ++i) {
beforeJump[i] = reverseJump.processSampleMono(0.0f);
}
reverseJump.setOffsetSamples(-498.5f);| // Estéreo em reverse: contadores independentes por canal (equivalência com dois monos). | ||
| OrbitDelayCore reverseStereo; | ||
| OrbitDelayCore reverseMonoL; | ||
| OrbitDelayCore reverseMonoR; | ||
| reverseStereo.reset(48000.0f); | ||
| reverseMonoL.reset(48000.0f); | ||
| reverseMonoR.reset(48000.0f); | ||
| std::vector<float> reverseStereoL(4096u, 0.0f); | ||
| std::vector<float> reverseStereoR(4096u, 0.0f); | ||
| std::vector<float> reverseMonoDelayL(4096u, 0.0f); | ||
| std::vector<float> reverseMonoDelayR(4096u, 0.0f); | ||
| if (!reverseStereo.attachBuffers(reverseStereoL.data(), reverseStereoR.data(), static_cast<uint32_t>(reverseStereoL.size())) || | ||
| !reverseMonoL.attachBufferMono(reverseMonoDelayL.data(), static_cast<uint32_t>(reverseMonoDelayL.size())) || | ||
| !reverseMonoR.attachBufferMono(reverseMonoDelayR.data(), static_cast<uint32_t>(reverseMonoDelayR.size()))) { | ||
| return fail("reverse stereo/mono attach should succeed"); | ||
| } | ||
| reverseStereo.setMix(1.0f); | ||
| reverseMonoL.setMix(1.0f); | ||
| reverseMonoR.setMix(1.0f); | ||
| reverseStereo.setFeedback(0.0f); | ||
| reverseMonoL.setFeedback(0.0f); | ||
| reverseMonoR.setFeedback(0.0f); | ||
| reverseStereo.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse); | ||
| reverseMonoL.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse); | ||
| reverseMonoR.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse); | ||
| reverseStereo.setOffsetSamples(112.0f); | ||
| reverseMonoL.setOffsetSamples(112.0f); | ||
| reverseMonoR.setOffsetSamples(112.0f); |
There was a problem hiding this comment.
Issue: Test Passes on Silence
The default tempoDelaySamples of 24000.0f clamps the delay to 4080.0f samples. Since the test only processes 320 samples, the output is entirely silent, and the stereo-to-mono comparison passes trivially on zeros.
Solution
Set the tempo to 320.0f BPM and note division to 0.0625f for all three instances to reduce tempoDelaySamples to 562.5f. Then, set offsetSamples to -450.5f to achieve a delay of exactly 112.0f samples. This allows the impulses to be processed and compared with actual signal.
// Estéreo em reverse: contadores independentes por canal (equivalência com dois monos).
OrbitDelayCore reverseStereo;
OrbitDelayCore reverseMonoL;
OrbitDelayCore reverseMonoR;
reverseStereo.setTempoBpm(320.0f);
reverseStereo.setNoteDivision(0.0625f);
reverseMonoL.setTempoBpm(320.0f);
reverseMonoL.setNoteDivision(0.0625f);
reverseMonoR.setTempoBpm(320.0f);
reverseMonoR.setNoteDivision(0.0625f);
reverseStereo.reset(48000.0f);
reverseMonoL.reset(48000.0f);
reverseMonoR.reset(48000.0f);
std::vector<float> reverseStereoL(4096u, 0.0f);
std::vector<float> reverseStereoR(4096u, 0.0f);
std::vector<float> reverseMonoDelayL(4096u, 0.0f);
std::vector<float> reverseMonoDelayR(4096u, 0.0f);
if (!reverseStereo.attachBuffers(reverseStereoL.data(), reverseStereoR.data(), static_cast<uint32_t>(reverseStereoL.size())) ||
!reverseMonoL.attachBufferMono(reverseMonoDelayL.data(), static_cast<uint32_t>(reverseMonoDelayL.size())) ||
!reverseMonoR.attachBufferMono(reverseMonoDelayR.data(), static_cast<uint32_t>(reverseMonoDelayR.size()))) {
return fail("reverse stereo/mono attach should succeed");
}
reverseStereo.setMix(1.0f);
reverseMonoL.setMix(1.0f);
reverseMonoR.setMix(1.0f);
reverseStereo.setFeedback(0.0f);
reverseMonoL.setFeedback(0.0f);
reverseMonoR.setFeedback(0.0f);
reverseStereo.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse);
reverseMonoL.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse);
reverseMonoR.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse);
reverseStereo.setOffsetSamples(-450.5f);
reverseMonoL.setOffsetSamples(-450.5f);
reverseMonoR.setOffsetSamples(-450.5f);| // Reverse + feedback alto: estabilidade numérica. | ||
| OrbitDelayCore reverseHighFeedback; | ||
| reverseHighFeedback.reset(48000.0f); | ||
| std::vector<float> reverseHighFeedbackDelay(4096u, 0.0f); | ||
| if (!reverseHighFeedback.attachBufferMono(reverseHighFeedbackDelay.data(), static_cast<uint32_t>(reverseHighFeedbackDelay.size()))) { | ||
| return fail("reverse high-feedback test attach should succeed"); | ||
| } | ||
| reverseHighFeedback.setMix(1.0f); | ||
| reverseHighFeedback.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse); | ||
| reverseHighFeedback.setFeedback(0.94f); | ||
| reverseHighFeedback.setFeedbackDrive(12.0f); | ||
| reverseHighFeedback.setFeedbackNonlinearAmount(1.0f); | ||
| reverseHighFeedback.setFeedbackCompThreshold(0.25f); | ||
| reverseHighFeedback.setToneHz(4200.0f); |
There was a problem hiding this comment.
Issue: Feedback Recirculation Not Tested
With the default tempoDelaySamples of 24000.0f, the delay is clamped to 4080.0f samples. Since the test only processes 2048 samples, the feedback loop never recirculates the signal even once. This fails to test the numerical stability of the high feedback path.
Solution
Set the tempo to 320.0f BPM and note division to 0.0625f to reduce tempoDelaySamples to 562.5f. Then, set offsetSamples to -462.5f to achieve a delay of exactly 100.0f samples. This allows the feedback loop to recirculate the signal over 20 times within the 2048-sample run, properly testing numerical stability.
// Reverse + feedback alto: estabilidade numérica.
OrbitDelayCore reverseHighFeedback;
reverseHighFeedback.setTempoBpm(320.0f);
reverseHighFeedback.setNoteDivision(0.0625f);
reverseHighFeedback.reset(48000.0f);
std::vector<float> reverseHighFeedbackDelay(4096u, 0.0f);
if (!reverseHighFeedback.attachBufferMono(reverseHighFeedbackDelay.data(), static_cast<uint32_t>(reverseHighFeedbackDelay.size()))) {
return fail("reverse high-feedback test attach should succeed");
}
reverseHighFeedback.setMix(1.0f);
reverseHighFeedback.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse);
reverseHighFeedback.setFeedback(0.94f);
reverseHighFeedback.setFeedbackDrive(12.0f);
reverseHighFeedback.setFeedbackNonlinearAmount(1.0f);
reverseHighFeedback.setFeedbackCompThreshold(0.25f);
reverseHighFeedback.setToneHz(4200.0f);
reverseHighFeedback.setOffsetSamples(-462.5f);There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aa3e1c5c66
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| reversePeriodic.setOffsetSamples(96.0f); | ||
| reversePeriodic.setReadMode(OrbitDelayCore::ReadMode::AccidentalReverse); | ||
| reversePeriodic.processSampleMono(1.0f); | ||
| std::vector<float> reverseA(128u, 0.0f); | ||
| std::vector<float> reverseB(128u, 0.0f); |
There was a problem hiding this comment.
Drive reverse tests with a reachable delay window
This setup leaves tempoDelaySamples at its default (~24000), and reverse mode computes delay as tempoDelay + offset before clamping to the buffer range. With a 2048-sample buffer, that clamps near the guard band, so the next 256 samples after the impulse are silence (reverseA/reverseB are both zeros), making the periodic-reset assertion pass vacuously even if reverse window reset logic regresses. Please set tempo/note-division/orbit (or offset) so the tested window actually contains signal before comparing the two segments.
Useful? React with 👍 / 👎.
Motivation
Description
tests/test_orbit_delay.cppwith amaxStepDeltahelper and dedicated reverse tests covering fixed-delay periodicity and finiteness, abrupt delay jumps with window-counter reset and anti-click assertion, stereo reverse behavior validated against two independent mono instances, and reverse with high feedback ensuring numerical stability.tests/render_golden.cppto still write the existing baseline golden (tests/golden/orbit_delay_reference.txt) and also emit a reverse-mode golden file attests/golden/orbit_delay_reverse_reference.txtusing a deterministic reverse-mode stimulus.OrbitDelayCoremethods) and only modify test sources; no production code changes were made.Testing
cmake -S . -B build,cmake --build build -j4andctest --test-dir build --output-on-failureas part of CI validation.100% tests passed, 5/5) and the golden render executable writing both reference files successfully.Codex Task