Skip to content

Add vocal-centre safety-net for consistent wrong-octave detection - #29

Merged
MrDix merged 3 commits into
mainfrom
feat/vocal-center-octave
Mar 14, 2026
Merged

Add vocal-centre safety-net for consistent wrong-octave detection#29
MrDix merged 3 commits into
mainfrom
feat/vocal-center-octave

Conversation

@MrDix

@MrDix MrDix commented Mar 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds correct_vocal_center() as a safety-net octave correction for cases where ALL detected pitches are consistently in the wrong octave
  • Uses a tighter expected centre band (MIDI 55-79, ~G3-G5) with an 80% concentration threshold
  • Runs after both existing corrections as a final safety net before manual --octave override

Problem

When the pitch detector consistently locks onto the wrong octave (e.g. all notes -12 HT):

  • correct_global_octave(low=48, high=84) doesn't catch it because the median (e.g. MIDI 54) still falls inside [48,84]
  • correct_octave_outliers() can't help because there are no outliers — 100% of notes are consistent

This was observed in the Phase 5 benchmark where one song scored 0% Pitch ±2 ST despite having 90.8% Interval accuracy (intervals were correct, just the wrong octave).

Solution

New pipeline step correct_vocal_center():

  1. Compute median of all MIDI notes
  2. If median is in [55, 79] → do nothing (normal centre range)
  3. If median < 55 AND >80% of notes below 55 → shift all notes +12 ST
  4. If median > 79 AND >80% of notes above 79 → shift all notes -12 ST

Pipeline order: global_octave → octave_outliers → vocal_center → manual_shift

Test plan

  • 13 new unit tests covering: edge cases, boundary conditions, concentration thresholds, interval preservation, custom parameters
  • Full test suite: 154 passed, 3 skipped
  • A/B test with known wrong-octave song (simple_fast benchmark)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added vocal center correction to automatically adjust notes when they concentrate outside the optimal vocal range; enabled by default.
    • Added a new CLI option to disable vocal center correction at runtime.
  • Tests

    • Added comprehensive tests validating vocal center correction across edge cases, boundary conditions, concentration thresholds, interval and metadata preservation.

When ALL detected pitches are consistently in the wrong octave (e.g. -12 HT),
correct_global_octave() may not catch it because the median still falls inside
the broad [48,84] range, and correct_octave_outliers() can't help because
there are no outliers to compare against (100% consistency = "correct").

The new correct_vocal_center() function runs AFTER both existing corrections
as a final safety net. It uses a tighter expected centre band (MIDI 55-79,
roughly G3-G5) and checks if >80% of notes cluster below/above the threshold.
If so, it shifts all notes by ±12 semitones.

Pipeline order: global_octave → octave_outliers → vocal_center → manual_shift

Includes 13 unit tests covering edge cases, boundary conditions, concentration
thresholds, interval preservation, and custom parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@MrDix has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 50 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cff62e98-7db5-4220-93bc-d98c95bf0d49

📥 Commits

Reviewing files that changed from the base of the PR and between df57a61 and 04a8a7f.

📒 Files selected for processing (1)
  • pytest/modules/Midi/test_midi_creator.py
📝 Walkthrough

Walkthrough

A new correct_vocal_center function was added to the MIDI module to shift note collections by octaves when concentrated outside vocal thresholds. It's wired into UltraSinger's processing pipeline behind a settings flag and covered by extensive new tests.

Changes

Cohort / File(s) Summary
Core Implementation
src/modules/Midi/midi_creator.py
Added correct_vocal_center(midi_segments, low_threshold=55, high_threshold=79, concentration_pct=0.80) that computes median note, checks concentration vs thresholds, and shifts all notes by ±12 semitones when criteria met. Handles empty/invalid input and returns modified segments.
Pipeline Integration
src/UltraSinger.py, src/Settings.py, src/modules/common_print.py
Imported and invoked correct_vocal_center in the MIDI processing pipeline guarded by new Settings.vocal_center_correction (default True). Added CLI option --disable_vocal_center to toggle behavior.
Tests
pytest/modules/Midi/test_midi_creator.py
Added a comprehensive TestCorrectVocalCenter test class covering empty input, no-op cases, boundary medians, octave shifts (up/down by 12), concentration threshold behavior (including exact 80%), interval and metadata preservation, and custom threshold handling. (Diff shows duplicated class definitions — review for unintended duplication.)

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLI
  participant Settings as Settings
  participant Runner as UltraSinger.run
  participant MidiMod as modules.Midi.midi_creator
  participant Segs as MidiSegments

  CLI->>Settings: parse args (--disable_vocal_center)
  Settings->>Runner: provide config (vocal_center_correction)
  Runner->>MidiMod: process_data.midi_segments (after octave-outlier correction)
  alt vocal_center_correction == true
    MidiMod->>Segs: correct_vocal_center(segs, thresholds)
    Segs-->>MidiMod: possibly shifted segments
  end
  Runner->>Segs: continue pipeline with final segments
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through notes both high and low,

When clusters stray I give a nudge—just so.
Twelve steps up, or twelve steps down,
I tuck them gently to the vocal crown. 🎵

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main change: adding a vocal-centre safety-net for octave detection, which is the core feature across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 80.95% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/vocal-center-octave
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
pytest/modules/Midi/test_midi_creator.py (1)

524-617: Add a no-shift regression for valid low/high-centre input.

These tests only prove the “shift” path. A correct passage like ["F#3"] * 5 is still inside the broader 48-84 vocal range, but this heuristic would currently treat it as wrong-octave, and the suite has no case here to catch that false positive.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pytest/modules/Midi/test_midi_creator.py` around lines 524 - 617, Add a
regression test that ensures correct_vocal_center does NOT shift notes that are
already in a valid mid-range (e.g., five identical "F#3" notes); create a new
test (e.g., test_no_shift_valid_center) that builds segs with
self._make_segs(["F#3"] * 5), calls correct_vocal_center(segs), extracts midis
with self._get_midis(result) and asserts the midis are unchanged (expected =
original MIDI values for F#3), so the heuristic won't falsely octave-shift valid
input.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/Midi/midi_creator.py`:
- Around line 249-252: The concentration check currently uses a >= comparator
which causes an exact-threshold case to trigger an octave shift contrary to the
docstring; change the three comparisons that reference concentration_pct (the
expressions counting notes below low_threshold or above high_threshold and
comparing to concentration_pct) to use > instead of >= so the shift only occurs
when more than concentration_pct of notes are outside the centre band (update
all occurrences that reference concentration_pct, low_threshold, and
high_threshold).

In `@src/UltraSinger.py`:
- Around line 239-241: The unconditional call to correct_vocal_center on
process_data.midi_segments inside run() can transpose already-correct tracks;
change it to only run when a configurable flag or stronger trigger is set (e.g.,
add a boolean option like enable_vocal_center_correction or a validation
function that checks extreme out-of-range concentration before calling
correct_vocal_center), wire that flag into the run() flow so
correct_vocal_center(process_data.midi_segments) is executed only when the
flag/trigger is true, and ensure the new option is documented and defaults to
false to avoid unexpected full-track transpositions.

---

Nitpick comments:
In `@pytest/modules/Midi/test_midi_creator.py`:
- Around line 524-617: Add a regression test that ensures correct_vocal_center
does NOT shift notes that are already in a valid mid-range (e.g., five identical
"F#3" notes); create a new test (e.g., test_no_shift_valid_center) that builds
segs with self._make_segs(["F#3"] * 5), calls correct_vocal_center(segs),
extracts midis with self._get_midis(result) and asserts the midis are unchanged
(expected = original MIDI values for F#3), so the heuristic won't falsely
octave-shift valid input.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ad89430-c843-4566-bfae-98bdee381c92

📥 Commits

Reviewing files that changed from the base of the PR and between 4969308 and 058db34.

📒 Files selected for processing (3)
  • pytest/modules/Midi/test_midi_creator.py
  • src/UltraSinger.py
  • src/modules/Midi/midi_creator.py

Comment thread src/modules/Midi/midi_creator.py
Comment thread src/UltraSinger.py Outdated
1. Change concentration_pct comparison from >= to > to align with
   docstring ("more than concentration_pct"). Exactly 80% no longer
   triggers the shift — only values strictly above 80%.

2. Guard correct_vocal_center() behind settings.vocal_center_correction
   flag (default: True, disable with --disable_vocal_center). This
   prevents unexpected transpositions for songs with legitimately low
   vocal ranges.

3. Update tests: split exact-80% test into two cases (exactly 80%
   does NOT trigger, 90% DOES trigger).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@MrDix

MrDix commented Mar 14, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai check

@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown

@MrDix Sure! I'll review the changes in this PR now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@MrDix

MrDix commented Mar 14, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

CodeRabbit noted the test suite only covered the shift path.  Add two
tests confirming that notes already inside the expected centre band
(MIDI 55-79) are never shifted — one near the low end and one near the
high end.

Note: The other two review comments were false positives:
- The concentration check already uses > (not >=)
- The call is already behind settings.vocal_center_correction flag

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@MrDix
MrDix merged commit 10d7c14 into main Mar 14, 2026
1 check passed
@MrDix
MrDix deleted the feat/vocal-center-octave branch March 14, 2026 05:05
MrDix pushed a commit that referenced this pull request Mar 14, 2026
Resolve conflicts with feat/vocal-center-octave (PR #29):
- Settings.py: keep both onset_correction and vocal_center_correction
- UltraSinger.py: keep both --disable_vocal_center and --disable_onset_correction CLI options

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MrDix pushed a commit that referenced this pull request Mar 14, 2026
Resolve conflicts: keep onset_correction setting and CLI option
alongside vocal_center_correction from PR #29.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MrDix added a commit that referenced this pull request Mar 14, 2026
* Add onset-based timing correction for note start times

After WhisperX forced alignment, note start times can be offset from
actual vocal onsets by up to ~100ms. This adds a post-processing step
that detects audio onsets using librosa and snaps nearby note starts
to improve timing accuracy.

- New module: onset_correction.py with detect_vocal_onsets() and
  snap_to_onsets() (max 80ms snap distance, preserves min 10ms duration)
- Enabled by default, disable with --disable_onset_correction
- Runs after transcription, before syllable splitting
- Uses whisper_audio_path (un-muted vocals) for onset detection

Includes 13 unit tests for edge cases, boundary conditions, multi-note
independence, word/timing preservation, and custom parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address CodeRabbit review: overlap prevention + fail-open

- Prevent snap_to_onsets() from moving a note before the previous note's
  end time by clamping the candidate to max(nearest, prev_end).
- Wrap onset correction in UltraSinger.run() in try/except so I/O or
  decoder errors degrade gracefully instead of aborting the pipeline.
- Add two overlap-prevention regression tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Merge main into feat/onset-timing-correction

Resolve conflicts with feat/vocal-center-octave (PR #29):
- Settings.py: keep both onset_correction and vocal_center_correction
- UltraSinger.py: keep both --disable_vocal_center and --disable_onset_correction CLI options

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Anthony <anthony@baseattack.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
MrDix added a commit that referenced this pull request Jul 4, 2026
* Add vocal-centre safety-net for consistent wrong-octave detection

When ALL detected pitches are consistently in the wrong octave (e.g. -12 HT),
correct_global_octave() may not catch it because the median still falls inside
the broad [48,84] range, and correct_octave_outliers() can't help because
there are no outliers to compare against (100% consistency = "correct").

The new correct_vocal_center() function runs AFTER both existing corrections
as a final safety net. It uses a tighter expected centre band (MIDI 55-79,
roughly G3-G5) and checks if >80% of notes cluster below/above the threshold.
If so, it shifts all notes by ±12 semitones.

Pipeline order: global_octave → octave_outliers → vocal_center → manual_shift

Includes 13 unit tests covering edge cases, boundary conditions, concentration
thresholds, interval preservation, and custom parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CodeRabbit findings: strict > comparator and configurable flag

1. Change concentration_pct comparison from >= to > to align with
   docstring ("more than concentration_pct"). Exactly 80% no longer
   triggers the shift — only values strictly above 80%.

2. Guard correct_vocal_center() behind settings.vocal_center_correction
   flag (default: True, disable with --disable_vocal_center). This
   prevents unexpected transpositions for songs with legitimately low
   vocal ranges.

3. Update tests: split exact-80% test into two cases (exactly 80%
   does NOT trigger, 90% DOES trigger).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add no-shift regression tests for valid centre-band input

CodeRabbit noted the test suite only covered the shift path.  Add two
tests confirming that notes already inside the expected centre band
(MIDI 55-79) are never shifted — one near the low end and one near the
high end.

Note: The other two review comments were false positives:
- The concentration check already uses > (not >=)
- The call is already behind settings.vocal_center_correction flag

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Anthony <anthony@baseattack.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
MrDix added a commit that referenced this pull request Jul 4, 2026
* Add onset-based timing correction for note start times

After WhisperX forced alignment, note start times can be offset from
actual vocal onsets by up to ~100ms. This adds a post-processing step
that detects audio onsets using librosa and snaps nearby note starts
to improve timing accuracy.

- New module: onset_correction.py with detect_vocal_onsets() and
  snap_to_onsets() (max 80ms snap distance, preserves min 10ms duration)
- Enabled by default, disable with --disable_onset_correction
- Runs after transcription, before syllable splitting
- Uses whisper_audio_path (un-muted vocals) for onset detection

Includes 13 unit tests for edge cases, boundary conditions, multi-note
independence, word/timing preservation, and custom parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address CodeRabbit review: overlap prevention + fail-open

- Prevent snap_to_onsets() from moving a note before the previous note's
  end time by clamping the candidate to max(nearest, prev_end).
- Wrap onset correction in UltraSinger.run() in try/except so I/O or
  decoder errors degrade gracefully instead of aborting the pipeline.
- Add two overlap-prevention regression tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Merge main into feat/onset-timing-correction

Resolve conflicts with feat/vocal-center-octave (PR #29):
- Settings.py: keep both onset_correction and vocal_center_correction
- UltraSinger.py: keep both --disable_vocal_center and --disable_onset_correction CLI options

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Anthony <anthony@baseattack.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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.

1 participant