Skip to content

Add optional octave-consistency pass (Viterbi per-note octave assignment) - #130

Merged
MrDix merged 2 commits into
mainfrom
feat/octave-consistency
Jul 7, 2026
Merged

Add optional octave-consistency pass (Viterbi per-note octave assignment)#130
MrDix merged 2 commits into
mainfrom
feat/octave-consistency

Conversation

@MrDix

@MrDix MrDix commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Problem (field report)

Passages jumping between octaves make generated charts extremely confusing to sing. A dedicated experiment against the 8 professional reference songs quantified it: the references contain 47 adjacent note jumps of >=10 semitones in total — our generated charts contain 507 (10x). The errors are not only whole passages: they are scattered single notes, short runs, and in one song a rapid octave alternation across whole sections.

Investigation highlights

  • A section-consensus approach fails when the majority of a song is off (one test song had 278/337 notes an octave low — internally consistent, so actually fine to sing, but it breaks any majority-vote center).
  • An audio-guided approach (using the ptAKF pitch frames as a second octave opinion) made things worse: the game tracker's octave opinion disagrees with the charter's chosen display octave, and scoring folds octaves anyway.
  • The decisive metric pair: adjacent >=10-semitone jumps (the confusion) and octave-sensitive pitch agreement with the time-matched reference note (the guard against folding genuine octave jumps).

Change

--octave_consistency (GUI: Post-Processing → "Octave Consistency", off by default): keeps every note's pitch class and re-chooses only its octave (-1/0/+1) via dynamic programming over the whole song. Cost = hinge smoothness (only the part of an adjacent jump beyond 8 semitones costs) + fidelity (2 per octave moved). The balance is self-limiting: folding 1-3 note scatter is cheaper than its two ~12-semitone boundary jumps, while a genuine octave passage of >= ~5 notes is cheaper to keep — validated:

metric (8 reference songs) before after professional refs
adjacent jumps >= 10 st 507 61 47
octave-sensitive match vs ref 60.1% 59.4%

Genuine wide-range songs unaffected (the widest ref, 42 st, keeps its match% and structure). The game score is unchanged (ptAKF folds octaves). The implemented pipeline function reproduces the prototype numbers exactly on all 8 songs, and pitch classes are asserted preserved.

Surfaces (kept in sync)

CLI flag + help, GUI toggle with tooltip, config default, runner arg, README section (next to the existing octave-shift/octave-snap docs, explaining the relationship: snap = single isolated spikes, consistency = the stronger scatter/short-run repair).

Tests

7 unit tests: single spike folded, short run folded, alternating scatter unified, long genuine octave passage kept, gradual wide range untouched, pitch class preserved, short-input no-op. Full suite 873 passed / 4 skipped.

Default: off (opt-in) — the default can be revisited after real singing tests.

Summary by CodeRabbit

  • New Features

    • Added an optional Octave Consistency post-processing setting in the app and CLI.
    • New toggle is available in the settings UI and is off by default.
    • Help text and documentation now describe the new option and where to find it.
  • Bug Fixes

    • Improved octave handling so scattered wrong-octave notes and short runs can be corrected while preserving legitimate octave passages.

@MrDix

MrDix commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@MrDix, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b2f4a2ff-3f9a-4df5-862f-623988e309c9

📥 Commits

Reviewing files that changed from the base of the PR and between 780e508 and 9f100fe.

📒 Files selected for processing (2)
  • pytest/modules/Midi/test_octave_consistency.py
  • src/modules/Midi/midi_creator.py
📝 Walkthrough

Walkthrough

This PR adds a new "Octave Consistency" post-processing feature that uses dynamic programming to reassign per-note octaves, correcting scattered wrong-octave notes while preserving genuine wide-range passages. It's wired into the pipeline via a new Settings field, CLI flag, GUI toggle, and documented in README, with accompanying tests.

Changes

Octave Consistency Feature

Layer / File(s) Summary
Core DP algorithm and tests
src/modules/Midi/midi_creator.py, pytest/modules/Midi/test_octave_consistency.py
Adds enforce_octave_consistency(), which converts note names to MIDI values and applies dynamic programming to choose per-note octave offsets {-1,0,+1} minimizing smoothness and fidelity penalties, then updates midi_segments. Tests cover spike folding, short-run correction, alternation collapse, long passage preservation, gradual motion, pitch-class preservation, and no-op on short input.
Settings field and pipeline wiring
src/Settings.py, src/UltraSinger.py
Adds octave_consistency: bool = False to Settings and imports/invokes enforce_octave_consistency conditionally in the MIDI post-processing pipeline before vocal-center correction.
CLI flag and help text
src/UltraSinger.py, src/modules/common_print.py
Registers --octave_consistency as a supported long option in argument parsing and adds its description to the CLI help output.
GUI toggle and config wiring
src/gui/config.py, src/gui/settings_tab.py, src/gui/ultrasinger_runner.py
Adds a default config value, a "Octave Consistency" toggle in the post-processing settings tab, includes it in collect_config(), and appends --octave_consistency to generated CLI args when enabled.
README documentation
README.md
Documents the --octave_consistency option, GUI location, behavior, scoring impact, and default-disabled state.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UltraSinger
  participant MidiCreator as enforce_octave_consistency
  participant MidiSegments

  User->>UltraSinger: run with --octave_consistency
  UltraSinger->>UltraSinger: snap_isolated_octave_spikes()
  UltraSinger->>MidiCreator: enforce_octave_consistency(midi_segments)
  MidiCreator->>MidiCreator: convert notes to MIDI, filter invalid
  MidiCreator->>MidiCreator: DP selects octave offsets per note
  MidiCreator->>MidiSegments: update note assignments
  MidiCreator-->>UltraSinger: adjusted midi_segments
  UltraSinger->>UltraSinger: correct_vocal_center() (subsequent stage)
Loading

Possibly related PRs

  • MrDix/UltraSinger#11: Both PRs modify octave-repair logic in midi_creator.py, with correct_octave_outliers() positioned adjacent to the new enforce_octave_consistency().
  • MrDix/UltraSinger#29: The new enforce_octave_consistency() call is wired directly before correct_vocal_center(), introduced in this related PR.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: an optional octave-consistency pass using Viterbi-style per-note octave assignment.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/octave-consistency

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.

@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: 1

🤖 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.

Inline comments:
In `@src/modules/Midi/midi_creator.py`:
- Around line 559-563: The note conversion fallback in MidiCreator’s segment
loop only handles ValueError and KeyError, so malformed note strings from
librosa.note_to_midi() can still escape. Update the exception handling in the
midi_segments processing block to also catch librosa.ParameterError and continue
appending None for invalid notes, keeping the behavior consistent in
midi_creator.py.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c3841785-bf6d-4dc4-8c2b-acf337656589

📥 Commits

Reviewing files that changed from the base of the PR and between 6777c3c and 780e508.

📒 Files selected for processing (9)
  • README.md
  • pytest/modules/Midi/test_octave_consistency.py
  • src/Settings.py
  • src/UltraSinger.py
  • src/gui/config.py
  • src/gui/settings_tab.py
  • src/gui/ultrasinger_runner.py
  • src/modules/Midi/midi_creator.py
  • src/modules/common_print.py

Comment thread src/modules/Midi/midi_creator.py
@MrDix

MrDix commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

Confirmed empirically in the venv: librosa.ParameterError inherits from LibrosaError -> Exception (NOT ValueError), so a malformed note string would indeed escape the fallback. Fixed — and the same pattern existed in three more pre-existing note-conversion fallbacks in this file (octave snap, outlier phases 1+2), so all four now catch (ValueError, KeyError, librosa.ParameterError) consistently. Added a regression test with a malformed note string (segment treated as unvoiced, neighbours still processed).

@MrDix

MrDix commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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 merged commit 02ee3db into main Jul 7, 2026
2 checks passed
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