Skip to content

Fix pitch shifter phase wrapping#228

Merged
sudip-mondal-2002 merged 3 commits into
sudip-mondal-2002:mainfrom
donglrd:fix-pitch-shifter-phase-wrap
May 26, 2026
Merged

Fix pitch shifter phase wrapping#228
sudip-mondal-2002 merged 3 commits into
sudip-mondal-2002:mainfrom
donglrd:fix-pitch-shifter-phase-wrap

Conversation

@donglrd

@donglrd donglrd commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the per-sample phase wrapping while loops in PitchShifter::process() with a constant-time wrapper
  • reuse the same wrapper in read_linear() so phase normalization stays consistent

Fixes #218

Testing

  • git diff --check HEAD~1 HEAD
  • cmake --build build --target amplitron-tests -j10
  • ./build/amplitron-tests --gtest_filter='*pitch_shifter*' (274 passed, 0 failed)

Summary by CodeRabbit

  • Refactor
    • Improved phase normalization in the pitch shifter to use constant-time wrapping, making extreme pitch adjustments more robust and reducing risk of instability.
  • Tests
    • Added a regression test that exercises extreme pitch shifts and verifies outputs remain finite and maintain expected signal energy.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

PitchShifter's phase normalization is refactored to replace unbounded while-loop wrapping with O(1) std::fmod-based wrapping. A wrap_phase() helper is introduced and applied consistently in process() and read_linear(), eliminating real-time audio callback overhead.

Changes

Phase Wrapping Optimization

Layer / File(s) Summary
wrap_phase helper
src/audio/effects/pitch_shifter.cpp
Added wrap_phase(phase, buf_size_) using std::fmod() to normalize floating-point phase into [0, buf_size_).
read_linear consistency update
src/audio/effects/pitch_shifter.cpp
read_linear() now calls wrap_phase() to normalize the input phase before interpolation index calculation.
process() wrapping replacement
src/audio/effects/pitch_shifter.cpp
process() replaces per-sample while-loop wrapping of read_phase_a_/read_phase_b_ with wrap_phase() calls, keeping the rest of the grain read/crossfade/dry-wet logic unchanged.
Regression test: extreme shift wraps phase
tests/test_effects.cpp
Added pitch_shifter_extreme_shift_wraps_phase_without_instability which runs the effect with extreme Shift/Fine and fully-wet mix on a 4096-sample sine buffer and asserts outputs are finite and have measurable RMS.

🎯 2 (Simple) | ⏱️ ~8 minutes

Suggested labels: Audio Processing, level:advanced, type:testing

Suggested reviewers:

  • sudip-mondal-2002

🐰 I hopped through loops and found a smoother way,
No more spins that steal the audio day,
A tiny fmod keeps the phases right,
Now signals stay steady through the night,
Happy ears and buffers hum away.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix pitch shifter phase wrapping' directly and concisely describes the main change: replacing unbounded while-loop phase wrapping with O(1) modulo-based wrapping in PitchShifter.
Linked Issues check ✅ Passed The PR successfully implements the O(1) modulo-based phase wrapping fix [#218] via a new wrap_phase() helper and applies it consistently in both read_linear() and process(), with regression test coverage added as requested.
Out of Scope Changes check ✅ Passed All changes are scoped to the pitch-shifter phase wrapping fix: refactoring to use wrap_phase() helper in process() and read_linear(), adding regression test, with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Report 📊

Line Coverage: 81.3%

✅ Coverage meets threshold: 81.3% >= 60%

Full Coverage Summary
Summary coverage rate:
  lines......: 81.3% (3419 of 4204 lines)
  functions..: 91.3% (482 of 528 functions)
  branches...: no data found

@github-actions

github-actions Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

PR Preview Removed

The GitHub Pages preview for this PR has been removed because the PR was closed.

@sudip-mondal-2002

Copy link
Copy Markdown
Owner

@donglrd shouldn't we have some test coverage?

@donglrd

donglrd commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

Added a regression test for the pitch shifter phase wrapping path.

It exercises an extreme positive shift/fine-tune case, processes a full buffer, and asserts the output remains finite with signal energy.

Validated locally:

  • git diff --check
  • cmake --build build --target amplitron-tests -j10
  • ./build/amplitron-tests --gtest_filter='*pitch_shifter*' (275 passed, 0 failed)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_effects.cpp (1)

1343-1347: ⚡ Quick win

Harden the regression setup so extreme params are guaranteed active.

Set params after reset() (or assert values after reset) so this test can’t silently stop exercising the extreme-wrap path if reset behavior changes.

Proposed tweak
 TEST(pitch_shifter_extreme_shift_wraps_phase_without_instability) {
     PitchShifter ps;
     ps.set_sample_rate(48000);
+    ps.reset();
     ps.params()[0].value = 120.0f;
     ps.params()[1].value = 500.0f;
     ps.params()[2].value = 1.0f;
-    ps.reset();
+
+    ASSERT_NEAR(ps.params()[0].value, 120.0f, 1e-6f);
+    ASSERT_NEAR(ps.params()[1].value, 500.0f, 1e-6f);
+    ASSERT_NEAR(ps.params()[2].value, 1.0f, 1e-6f);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_effects.cpp` around lines 1343 - 1347, Move the parameter
assignments so they occur after calling ps.reset() (or alternatively add
assertions that ps.params()[i].value equals the intended values immediately
after reset) to ensure the extreme values are active regardless of reset
behavior; specifically modify the test to call ps.reset() first and then set
ps.params()[0].value = 120.0f, ps.params()[1].value = 500.0f,
ps.params()[2].value = 1.0f (or assert those values after ps.reset()) so the
extreme-wrap path is enforced reliably.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_effects.cpp`:
- Around line 1343-1347: Move the parameter assignments so they occur after
calling ps.reset() (or alternatively add assertions that ps.params()[i].value
equals the intended values immediately after reset) to ensure the extreme values
are active regardless of reset behavior; specifically modify the test to call
ps.reset() first and then set ps.params()[0].value = 120.0f,
ps.params()[1].value = 500.0f, ps.params()[2].value = 1.0f (or assert those
values after ps.reset()) so the extreme-wrap path is enforced reliably.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6842e96-7273-48a7-813f-a39466c4d1f0

📥 Commits

Reviewing files that changed from the base of the PR and between 4c3d4da and f8757f8.

📒 Files selected for processing (1)
  • tests/test_effects.cpp

@sudip-mondal-2002 sudip-mondal-2002 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_effects.cpp (1)

1368-1383: ⚡ Quick win

Extend this regression to include the sample-rate-change trigger path.

This test covers extreme phase stepping well, but the reported repro also includes runtime sample-rate changes. Adding a second pass after changing sample rate will lock in that path too.

Proposed test extension
 TEST(pitch_shifter_extreme_shift_wraps_phase_without_instability) {
     PitchShifter ps;
     ps.set_sample_rate(48000);
     ps.params()[0].value = 120.0f;
     ps.params()[1].value = 500.0f;
     ps.params()[2].value = 1.0f;
     ps.reset();

     std::vector<float> buf(4096);
     fill_sine(buf.data(), static_cast<int>(buf.size()), 440.0f, 48000);

     ps.process(buf.data(), static_cast<int>(buf.size()));

     ASSERT_TRUE(buffer_is_finite(buf.data(), static_cast<int>(buf.size())));
     ASSERT_GT(rms(buf.data(), static_cast<int>(buf.size())), 0.001f);
+
+    ps.set_sample_rate(96000);
+    ps.reset();
+    fill_sine(buf.data(), static_cast<int>(buf.size()), 440.0f, 96000);
+    ps.process(buf.data(), static_cast<int>(buf.size()));
+    ASSERT_TRUE(buffer_is_finite(buf.data(), static_cast<int>(buf.size())));
+    ASSERT_GT(rms(buf.data(), static_cast<int>(buf.size())), 0.001f);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_effects.cpp` around lines 1368 - 1383, The test
pitch_shifter_extreme_shift_wraps_phase_without_instability needs to exercise
the sample-rate-change trigger path too: after the first process pass, call
ps.set_sample_rate(...) with a different rate (e.g., 44100), call ps.reset() if
appropriate, then run a second ps.process(...) on the same buffer and assert
finiteness and non-zero rms again; update the TEST (and use the same
PitchShifter instance and functions set_sample_rate, reset, process,
buffer_is_finite, rms) so the regression includes the runtime sample-rate change
path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_effects.cpp`:
- Around line 1368-1383: The test
pitch_shifter_extreme_shift_wraps_phase_without_instability needs to exercise
the sample-rate-change trigger path too: after the first process pass, call
ps.set_sample_rate(...) with a different rate (e.g., 44100), call ps.reset() if
appropriate, then run a second ps.process(...) on the same buffer and assert
finiteness and non-zero rms again; update the TEST (and use the same
PitchShifter instance and functions set_sample_rate, reset, process,
buffer_is_finite, rms) so the regression includes the runtime sample-rate change
path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 56f8240d-a0d0-40a2-b048-89f4b95f1851

📥 Commits

Reviewing files that changed from the base of the PR and between f8757f8 and cfd6d41.

📒 Files selected for processing (1)
  • tests/test_effects.cpp

@sudip-mondal-2002 sudip-mondal-2002 merged commit 7a074bb into sudip-mondal-2002:main May 26, 2026
9 checks passed
github-actions Bot added a commit that referenced this pull request May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PitchShifter::process() uses unbounded while loops for phase wrapping inside the real-time audio callback — causes xruns and audio dropouts

2 participants