Skip to content

Add UltraStar song compatibility fixer tool - #315

Closed
MrDix wants to merge 31 commits into
rakuri255:mainfrom
MrDix:feature/audio-format-converter-tool
Closed

MrDix wants to merge 31 commits into
rakuri255:mainfrom
MrDix:feature/audio-format-converter-tool

Conversation

@MrDix

@MrDix MrDix commented Mar 13, 2026 •

Copy link
Copy Markdown

Summary

Standalone tool (tools/fix_ultrastar_compatibility.py) that scans UltraStar song directories and fixes two categories of compatibility issues:

  • Audio format conversion: Converts incompatible audio formats (AAC, FLAC, OPUS, M4A, WMA, etc.) to OGG Vorbis via FFmpeg. Updates all audio tags (#MP3:, #AUDIO:, #VOCALS:, #INSTRUMENTAL:). Original audio files are preserved.
  • Encoding normalization (--normalize-encoding): Converts legacy encodings (CP1252, Latin-1) to UTF-8 for universal compatibility across USDX, UltraStar Play, Performous, and Vocaluxe. Removes deprecated #ENCODING: tags.

Features

  • --dry-run mode for safe preview of all changes
  • Multi-encoding detection with fallback chain (UTF-8-sig → CP1252 → Latin-1)
  • Detailed statistics and per-song change lists
  • Handles edge cases: missing audio files, existing OGG targets, multiple audio tags per song, BOM handling

Usage

# Audio conversion only
uv run python tools/fix_ultrastar_compatibility.py "D:\UltraStar\Songs"

# Audio + encoding normalization
uv run python tools/fix_ultrastar_compatibility.py "D:\UltraStar\Songs" --normalize-encoding

# Preview changes without modifying anything
uv run python tools/fix_ultrastar_compatibility.py "D:\UltraStar\Songs" --normalize-encoding --dry-run

Test plan

  • --dry-run on 18,287 songs → 60 audio conversions, 5,397 encoding fixes detected
  • Backwards compatibility: without --normalize-encoding no encoding info shown
  • Byte-level verification of encoding conversion (CP1252 0xFC → UTF-8 0xC3 0xBC)
  • Idempotency: already-UTF-8 files not touched (0/20 SingStar Abba songs)
  • Multi-encoding fallback: German (ü/ö/ä), Finnish (ä), Polish (ó) characters all handled

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Manual BPM override for custom tempo settings
    • Configurable denoise parameters: noise reduction, noise floor, and adaptive noise tracking
    • Advanced octave correction for improved MIDI accuracy
    • UltraStar file comparison tool with detailed quality metrics
    • Auto-conversion of incompatible audio formats to OGG for UltraStar compatibility
  • Bug Fixes

    • Improved pitch-to-note conversion using confidence-weighted algorithms
    • Enhanced silence detection for more accurate line break placement
    • Separated audio processing paths for better transcription and pitch detection
  • Documentation

    • Added BPM override usage guide
  • Tests

    • Added comprehensive test coverage for BPM correction, MIDI octave adjustments, and UltraStar file comparison
  • Chores

    • Enhanced CPU/CUDA PyTorch dependency management

Anthony and others added 30 commits March 10, 2026 04:34
UltraStar Play only supports mp3, ogg, and wav audio formats, but
YouTube downloads typically produce AAC audio. This adds automatic
conversion to OGG Vorbis (high quality VBR) when the extracted audio
is in an incompatible format.

Changes:
- Add convert_to_ultrastar_format() in ffmpeg_helper.py
- Integrate conversion into separate_audio_video() for YouTube downloads
- Add conversion for local audio/video file inputs in UltraSinger.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes case-sensitivity issue where uppercase extensions (.MP3, .WAV, .OGG)
would bypass the compatibility check, causing unnecessary conversion and
deletion of the original file.

Reported by CodeRabbit in PR #1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The FFmpeg afftdn noise reduction was hard-coded to 70dB (nr=70), far
above the default of 12dB. This strips consonants, sibilants and
transients from vocals, degrading both speech recognition accuracy and
pitch detection quality.

Changes:
- Reduce default noise reduction from 70dB to 20dB
- Make denoise parameters configurable via Settings and CLI args
  (--denoise_nr, --denoise_nf, --disable_denoise_track_noise)
- Include denoise config in cache filename for proper cache invalidation
- Add help text for new denoise options

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Normalize denoise_config cache key with consistent float formatting
  (:.1f) so that int defaults (20) and CLI floats (20.0) produce the
  same filename
- Add range validation for --denoise_nr (0.01-97) and --denoise_nf
  (-80 to -20) with clear error messages

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

feat: auto-convert incompatible audio formats to OGG for UltraStar Play
Fix aggressive denoising (nr=70 -> nr=20)
The mute_no_singing_parts() function zeroes silent audio sections,
which helps pitch detection (SwiftF0) focus on singing parts. However,
these hard zero→audio transitions cause WhisperX's wav2vec2 forced
alignment to lose sync, resulting in sudden timestamp jumps after
non-singing sections.

Split CreateProcessAudio() into two separate paths:
- whisper_audio_path (_mono.wav): denoised mono without muting, used
  for Whisper transcription, BPM detection, key detection, and
  silence-based transcription cleanup
- processing_audio_path (_mute.wav): muted audio, used only for
  pitch detection (SwiftF0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove dead code: processing_audio_path assignment in
  CreateProcessAudio() was immediately overwritten in run()
- Rename misleading parameter processing_audio_path → audio_path
  in transcribe_audio() to match new design
- Add '_unmuted' suffix to transcription cache key so old caches
  (from muted audio) are automatically invalidated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Separate audio paths for Whisper and pitch detection
* Fix beat rounding cascade and get_multiplier() off-by-one

Three fixes to reduce timing drift in generated UltraStar files:

1. get_multiplier() off-by-one: The loop returned a multiplier that gave
   bpm*mult < 400 instead of >= 400 (e.g. 37.5*10=375 instead of 37.5*11=412.5).
   This reduced beat resolution and amplified rounding errors.

2. Beat rounding: Changed round() to floor() for start beats and ceil() for
   durations. round() caused ±0.5 beat errors that accumulated because the
   overlap guard (max) only shifts notes later, never earlier. floor() for
   starts prevents notes from being placed too late, ceil() ensures minimum
   1-beat duration.

3. Redundant get_multiplier() call: multiplication and ultrastar_bpm were
   computed with two separate get_multiplier() calls instead of reusing the
   result.

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

* Fix negative BPM guard and linebreak rounding inconsistency

- get_multiplier(): reject negative BPM (would cause infinite loop)
- Linebreak position: use math.floor() consistent with note starts

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>
Read fresh session cookies directly from a browser (chrome, firefox,
edge, etc.) instead of requiring a manually exported cookies.txt file.
This avoids the common issue of cookie files going stale after one or
two YouTube downloads.

- Add _cookie_opts() helper in youtube.py to centralise cookie
  option building; cookies_from_browser takes precedence over
  cookiefile.
- Thread the new parameter through all yt-dlp call sites.
- Add --cookies_from_browser CLI argument, help text, and
  interactive-mode prompt.
- Update README with detailed authentication documentation.

Co-authored-by: Anthony <anthony@baseattack.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…)" (#7)

This reverts commit 4add153.

Co-authored-by: Anthony <anthony@baseattack.com>
Audio chunks exported via --create_audio_chunks were extracted from
the muted audio (_mute.wav), which contains hard zero transitions
where silence was removed. This made the chunks useless for
verifying what was actually sung. Switch to whisper_audio_path
(_mono.wav) so chunks contain the original vocal audio.

Co-authored-by: Anthony <anthony@baseattack.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add --bpm CLI option to override auto-detected BPM

librosa.feature.tempo() can misdetect BPM (e.g. 378 instead of 340
for "Bring Me To Life"), shifting all beat positions in the output.
This adds a --bpm flag that lets users provide the correct BPM
manually, skipping auto-detection entirely.

Changes:
- Settings.py: add bpm_override field
- UltraSinger.py: parse --bpm arg, skip get_bpm_from_file() when set
- common_print.py: add --bpm to help text
- init_interactive_mode.py: add BPM input to interactive mode

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

* Fix CodeRabbit findings for --bpm override

- Wrap float(arg) in try/except for non-numeric --bpm values
- Apply BPM override unconditionally (works with .txt input too)
- Preserve CLI --bpm in interactive mode (skip prompt if already set)

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>
* Replace mode-based pitch selection with confidence-weighted median

The previous approach converted frequencies to note names first, then
picked the most common note (mode via Counter). This caused wrong pitch
selection when pitch jitter spread frames across note boundaries —
the mode could pick a rare outlier, and notes ended up 3-4 octaves off.

Changes:
- pitched_data_helper: return confidence weights alongside filtered
  frequencies; fall back to top 25% by confidence instead of ALL
  frequencies when none pass the threshold (reduces noise notes)
- midi_creator: replace most_frequent()/convert_frequencies_to_notes()
  with confidence_weighted_median_note() that computes a weighted median
  in continuous Hz space before converting to a note name

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

* Add empty-input guards for confidence_weighted_median_note

Address CodeRabbit findings: validate that frequencies/weights are
non-empty and length-matched before computing the weighted median.
Guard the call site in create_midi_note_from_pitched_data to fall
back to C4 when no valid frequencies are available.

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>
Pitch detectors occasionally jump notes into wrong octaves (e.g. C2
instead of C4). This adds a post-processing step that compares each
note's MIDI value to the median of its neighbours (window=5) and shifts
it by octaves until it is within 11 semitones of the local median.

The correction runs after MIDI segment creation and before syllable
merging, so both the transcription and re-pitch pipelines benefit.

Co-authored-by: Anthony <anthony@baseattack.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Improve BPM detection with half/double-tempo correction

librosa.feature.tempo() sometimes reports double or half the actual
tempo.  This change fetches multiple tempo candidates (aggregate=None)
and applies half/double-tempo correction to prefer a value in the
typical song range (60-200 BPM).

For example, if librosa reports 378 BPM, the corrector will also
consider 189 BPM (half) which falls in the normal range and is more
likely correct.

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

* Fix incorrect librosa tempo API usage per CodeRabbit review

aggregate=None returns per-frame estimates, not ranked candidates.
Simplified to use default aggregation (single primary BPM) and apply
half/double correction on that single value. Also simplified
_pick_best_tempo to accept a single float instead of an array.

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>
* Replace mean-based linebreak detection with percentile threshold

The old deviation() function used a mean of silence durations (excluding
the longest gap) to decide where to insert linebreaks. This produced
poor results: evenly distributed pauses created no linebreaks at all,
while a single long pause could skew the average and suppress linebreaks
elsewhere.

Replace with silence_threshold() using numpy's 75th percentile: only
silences in the top 25% of duration trigger a linebreak. This is more
robust against both uniform distributions and single-pause outliers.

Also refactored calculate_silent_beat_length() into a thin wrapper
around the new silence_threshold() function for better testability.

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

* Address CodeRabbit review: None sentinel, clamp negative gaps, add tests

- Change silence_threshold() sentinel from 0 to None for "too few gaps"
  to prevent false linebreaks when any positive intra-word pause exists
- Guard the linebreak condition with `is not None` check so the entire
  condition is skipped when there are fewer than 5 gaps
- Clamp overlapping segment gaps to 0 with max() before percentile
  calculation to avoid negative silences lowering the threshold
- Add tests for short-input branch, few-segments wrapper, and negative
  gap clamping

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>
* Add global octave correction for sub-harmonic pitch detection

Pitch detectors (e.g. SwiftF0) sometimes lock onto a sub-harmonic,
pushing all notes into an implausibly low register (e.g. MIDI 14-24
instead of 62-74). The existing correct_octave_outliers() only
compares each note to its local neighbours and cannot detect such a
global shift because all notes are consistently wrong.

This adds correct_global_octave() which computes the median MIDI
value of the entire song and shifts all notes by whole octaves if
the median falls outside the expected vocal range (C3-C6 / MIDI
48-84). It runs before the local outlier correction in the pipeline.

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

* Address CodeRabbit review: narrow exceptions, clean up imports

- Narrow bare `except Exception` to `except (ValueError, TypeError)`
  in correct_global_octave() for safer error handling
- Remove unused `correct_octave_outliers` import from tests
- Move `numpy` import to file-level in test module
- Add `strict=True` to zip() for defensive length checking
- Remove copyrighted song name references from test docstrings

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

* Replace Unicode EN DASH with ASCII hyphen in docstrings

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>
* Expand BPM detection with additional tempo-ratio candidates

The previous half/double correction missed songs in triple or
compound metre where librosa reports 3x or 4x the actual tempo.
Expand _pick_best_tempo() to consider 10 common musical ratios
(identity, half, double, third, triple, quarter, quadruple,
two-thirds, dotted-half, three-quarters) with tie-breaking that
favours simpler ratios.  The function signature now accepts
custom range and target parameters for flexibility.

Add 21 unit tests covering edge cases, backward compatibility
with the old half/double behaviour, and the new ratio candidates.

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

* Guard in-range tempos from ratio rescaling (CodeRabbit review)

When the detected BPM already lies inside the acceptable range
(default 60-200), return it unchanged.  Ratio correction now only
applies to out-of-range values.  This prevents legitimate fast songs
(e.g. 180 BPM) from being silently rescaled, which would break all
downstream beat-length calculations.

Also import _TEMPO_RATIOS in tests instead of hard-coding the list,
and add explicit tests for in-range preservation.

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>
The 75th-percentile default produced ~46% more linebreaks than
manually-authored UltraStar files.  Raising the threshold to the
85th percentile keeps only the longest 15% of inter-note gaps as
linebreaks, which better matches human editing patterns.

Existing regression tests now pass an explicit percentile=75 so
they remain unaffected; two new tests verify the stricter default.

Co-authored-by: Anthony <anthony@baseattack.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add --bpm override option to README

Document the --bpm CLI parameter in both the command-line reference
block and as a dedicated section with usage example. This parameter
was added in a previous PR but not yet documented.

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

* docs: clarify --bpm accepts positive numbers and decimals

Address CodeRabbit review finding: document that --bpm must be a
positive number and that decimal values are supported.

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>
Previously, CUDA PyTorch was replaced by CPU-only on every `uv sync`
because no package index was configured. Install scripts now maintain
a named PyTorch index in pyproject.toml (CPU default) and modify the
URL in-place when switching between CPU and CUDA variants.

Key changes:
- pyproject.toml: Add [[tool.uv.index]] + [tool.uv.sources] for torch
- Install scripts: sed/powershell to toggle CPU/CUDA index URL, then
  regenerate lockfile with `uv lock` before `uv sync`
- Install scripts: Always remove old .venv for clean CPU↔CUDA switch
- .gitignore: Add uv.toml (reserved for future local overrides)

Co-authored-by: Anthony <anthony@baseattack.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Widen BPM range to [50, 500] and remove target bias

UltraStar uses BPM as a timing-resolution parameter, not as musical
tempo -- higher values give a finer beat grid and more precise note
placement.  The previous range [60, 200] with target=120 aggressively
downscaled detected tempos (e.g. 409 BPM -> 136 BPM), breaking timing
alignment.

Changes:
- Widen acceptable range from [60, 200] to [50, 500]
- Default target=None (use detected BPM as reference instead of 120)
- Fallback for invalid input (<=0) always returns 120.0
- Values inside range pass through unchanged (no correction)
- Out-of-range correction picks candidate closest to detected value

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

* Remove duplicate test for out-of-range fallback

test_nothing_in_range_returns_primary duplicated
test_very_low_bpm_prefers_closest_to_original (both tested
_pick_best_tempo(10.0) == 10.0).

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

* Exercise actual tie-break in test_simpler_ratio_wins_tie

Use primary=600 with target=350 so half (300) and two-thirds (400)
are equidistant from target — verifying that the simpler ratio
(earlier in _TEMPO_RATIOS) wins the tie.

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>
* Add multi-pass octave outlier correction with global consensus

Enhances correct_octave_outliers() with two improvements:

Phase 1 - Multi-pass local correction:
- Runs multiple passes (default 2) instead of one
- Each pass rebuilds MIDI values from corrected notes
- When multiple octave shifts are valid, prefers the one closest
  to the global median (tie-breaker)
- Stops early when a pass makes no corrections

Phase 2 - Global consensus correction:
- After local passes, fixes clusters of wrong-octave notes that
  local correction missed due to contaminated local medians
- Only activates when >50% of notes agree on an octave region
- Shifts minority notes by whole octaves toward global median
- Handles sub-harmonic detection clusters where 6+ consecutive
  notes are all one octave too low

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

* Address CodeRabbit review feedback for octave outlier correction

Phase 1: Buffer corrections and apply after the scan instead of
mutating mid-pass, which made results order-dependent.

Phase 2: Raise the consensus skip threshold from 6 to 12 semitones
so legitimate melody intervals (e.g. G4, a perfect fifth above C4)
are never shifted.  Only notes a full octave or more from the global
median are candidates for consensus correction.

Add two regression tests: one verifying G4 is preserved among C4
neighbours, and one exercising Phase 1 + Phase 2 cooperation on a
mixed outlier/cluster scenario.

Fix Ruff B007 (unused loop variable) and BLE001 (blind Exception).

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>
Fix two issues that caused CUDA users to silently revert to CPU:

1. Windows PowerShell BOM bug: Set-Content -Encoding UTF8 writes a
   UTF-8 BOM (EF BB BF) that breaks TOML parsing. Switch to .NET
   File.WriteAllText which writes BOM-free UTF-8 by default.

2. Git operations reset: Branch switches, pulls, and checkouts would
   overwrite the local pyproject.toml CUDA index URL with the
   committed CPU default. CUDA install scripts now set git
   skip-worktree on pyproject.toml and uv.lock to protect local
   config. CPU install scripts clear the flag since CPU matches
   the committed default.

Also fix: sed temp-file pattern replaced with in-place -i flag,
and incorrect 'py' command replaced with 'python' on Linux/macOS.

Co-authored-by: Anthony <anthony@baseattack.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add format-aware UltraStar comparison tool

Developer tool for evaluating generated UltraStar files against
reference files. Key capabilities:

- Auto-detects file format version (#VERSION header) and normalizes
  pitch values to MIDI (v1.2.0: pitch+48, legacy: raw MIDI)
- Fuzzy word alignment using SequenceMatcher with tilde note grouping
- Comprehensive metrics: pitch (median/exact/within-2ST), intervals,
  timing (excluding first word for GAP offset), lyrics precision/recall
- CLI with --json and --detailed output modes

43 unit tests covering format detection, pitch conversion, beat-to-ms
conversion, word alignment, tilde grouping, timing exclusion, interval
format independence, JSON output structure, and edge cases.

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

* docs: add docstrings to reach 80%+ coverage threshold

Add missing docstrings to all public functions, classes, and test
methods in compare_ultrastar.py and test_compare_ultrastar.py to
satisfy the CodeRabbit docstring coverage requirement (was 58%, now
above 80%).

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

* fix: address CodeRabbit nitpicks for robustness

- Wrap int() conversions in note-line parsing with try/except to
  gracefully skip malformed lines instead of aborting
- Use NFKD Unicode normalisation in word matching so accented
  characters (é→e, ü→u) are handled correctly for i18n songs

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>
Address CodeRabbit feedback from PR #22:

- Remove f-prefix from constant strings without placeholders (F541)
- Harden _version_ge() for shorthand ("1.2"), suffixed ("1.2.0-beta"),
  and single-component ("2") version strings with zero-padding
- Merge tilde continuation text into parent word so split words like
  "Hel" + "~lo" produce the group word "Hello"
- Use same non-empty word filter for lyrics precision/recall
  denominators as align_words() uses for matching

Tests: 48 passed (5 new, 2 updated)

Co-authored-by: Anthony <anthony@baseattack.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Standalone tool that scans song directories for UltraStar TXT files and:
- Converts incompatible audio formats (AAC, FLAC, OPUS, M4A, WMA, etc.)
  to OGG Vorbis via FFmpeg for UltraStar Play compatibility
- Normalizes text encoding from legacy codepages (CP1252, Latin-1) to
  UTF-8 for universal compatibility across all UltraStar derivatives
- Removes deprecated #ENCODING: tags during normalization
- Supports --dry-run for safe preview of changes
- Handles multi-encoding detection (UTF-8-sig, CP1252, Latin-1 fallback)
- Updates all audio tags (#MP3:, #AUDIO:, #VOCALS:, #INSTRUMENTAL:)
- Preserves original audio files (never deleted)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The tool now covers both audio format conversion and encoding
normalization — the old name only reflected the audio aspect.

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

MrDix commented Mar 13, 2026

Copy link
Copy Markdown
Author

Opened against wrong repo by accident. Moving to fork.

@MrDix MrDix closed this Mar 13, 2026
@coderabbitai

coderabbitai Bot commented Mar 13, 2026 •

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac514bce-86c0-45a7-a355-2692ad04e775

📥 Commits

Reviewing files that changed from the base of the PR and between 4155efd and b57b5db.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (26)
  • .gitignore
  • README.md
  • install/CPU/linux_cpu.sh
  • install/CPU/macos_cpu.sh
  • install/CPU/windows_cpu.bat
  • install/CUDA/linux_cuda_gpu.sh
  • install/CUDA/windows_cuda_gpu.bat
  • pyproject.toml
  • pytest/modules/Audio/test_bpm.py
  • pytest/modules/Midi/test_midi_creator.py
  • pytest/modules/UltraStar/test_ultrastar_writer.py
  • pytest/tools/test_compare_ultrastar.py
  • src/Settings.py
  • src/UltraSinger.py
  • src/modules/Audio/bpm.py
  • src/modules/Audio/denoise.py
  • src/modules/Audio/vocal_chunks.py
  • src/modules/Midi/midi_creator.py
  • src/modules/Pitcher/pitched_data_helper.py
  • src/modules/ProcessData.py
  • src/modules/Ultrastar/ultrastar_writer.py
  • src/modules/common_print.py
  • src/modules/ffmpeg_helper.py
  • src/modules/init_interactive_mode.py
  • tools/compare_ultrastar.py
  • tools/fix_ultrastar_compatibility.py

📝 Walkthrough

Walkthrough

This PR introduces BPM override functionality and denoising configuration options, refactors audio processing into separate whisper and pitch detection paths, replaces mode-based MIDI note detection with confidence-weighted median calculation, adds octave correction algorithms for MIDI consistency, and includes new UltraStar compatibility tools and comprehensive test coverage.

Changes

Cohort / File(s) Summary
Configuration & Dependency Management
.gitignore, pyproject.toml, install/CPU/linux_cpu.sh, install/CPU/macos_cpu.sh, install/CPU/windows_cpu.bat, install/CUDA/linux_cuda_gpu.sh, install/CUDA/windows_cuda_gpu.bat
Added uv.toml to .gitignore; updated pyproject.toml with PyTorch CPU index configuration; refactored installation scripts to enforce CPU or CUDA-specific PyTorch indices, regenerate lockfiles, reset git skip-worktree flags, remove existing .venv for clean state, and use uv sync for dependency resolution across Linux, macOS, and Windows platforms.
Settings & CLI Configuration
src/Settings.py, src/common_print.py, src/modules/init_interactive_mode.py
Added public Settings attributes for bpm_override (float), denoise_noise_reduction, denoise_noise_floor, and denoise_track_noise (all with defaults); updated help text with new CLI options --bpm, --denoise_nr, --denoise_nf, --disable_denoise_track_noise; added interactive prompt for manual BPM override in configure_additional_options.
Audio Path Separation & Processing
src/modules/ProcessData.py, src/modules/Audio/vocal_chunks.py, src/modules/Audio/denoise.py
Introduced whisper_audio_path field to ProcessDataPaths for unmuted audio used by Whisper transcription; clarified processing_audio_path for pitch detection; updated denoise_vocal_audio and \_\_ffmpeg_reduce_noise signatures to accept configurable noise_reduction, noise_floor, and track_noise parameters; refactored vocal_chunks to use whisper_audio_path instead of processing_audio_path.
BPM Detection & Tempo Correction
src/modules/Audio/bpm.py
Added private \_pick_best_tempo helper function with tempo ratio candidates and range enforcement (default 50–500 BPM); added \_TEMPO_RATIOS constant; refactored get_bpm_from_data to apply tempo correction and return corrected BPM; preserved get_bpm_from_file signature.
MIDI Note Generation & Octave Correction
src/modules/Midi/midi_creator.py, src/modules/Pitcher/pitched_data_helper.py
Replaced frequency-to-note mode-based conversion with confidence_weighted_median_note using weighted median on raw frequencies; added correct_global_octave to shift median MIDI into vocal range (default 48–84); added correct_octave_outliers with two-phase local/global octave alignment; updated get_frequencies_with_high_confidence signature to return tuple of (frequencies, weights) with fallback for low-confidence frames; removed Counter-based mode logic.
Ultrastar Output & Format Handling
src/modules/Ultrastar/ultrastar_writer.py, src/modules/ffmpeg_helper.py
Added silence_threshold (percentile-based) and calculate_silent_beat_length utilities; removed deviation function; updated BPM multiplier logic and linebreak timing with floor/ceil for precision; added convert_to_ultrastar_format function to convert non-compatible audio to OGG; updated separate_audio_video to invoke format conversion and return updated audio path/extension.
Main Application Orchestration
src/UltraSinger.py
Updated CreateProcessAudio to return tuple (whisper_audio_path, pitch_audio_path); refactored transcribe_audio to use whisper_audio_path and renamed parameter; added imports for correct_global_octave, correct_octave_outliers, convert_to_ultrastar_format; applied octave correction immediately after MIDI generation; added CLI parsing for --bpm, --denoise_nr, --denoise_nf, --disable_denoise_track_noise; displayed BPM override in options output; updated infos_from_audio_video_input_file to apply format conversion.
New Comparison & Compatibility Tools
tools/compare_ultrastar.py, tools/fix_ultrastar_compatibility.py
Added compare_ultrastar.py utility to parse UltraStar TXT files, align words via fuzzy matching, calculate pitch/timing/interval differences, and output JSON or human-readable comparison reports. Added fix_ultrastar_compatibility.py to scan directories, convert incompatible audio formats to OGG via FFmpeg, optionally normalize TXT encodings, and provide detailed conversion summaries with dry-run support.
Comprehensive Test Coverage
pytest/modules/Audio/test_bpm.py, pytest/modules/Midi/test_midi_creator.py, pytest/modules/UltraStar/test_ultrastar_writer.py, pytest/tools/test_compare_ultrastar.py
Added test_bpm.py validating \_pick_best_tempo with edge cases, in-range pass-through, out-of-range mapping, and tie-breaking. Added test_midi_creator.py covering correct_global_octave, correct_octave_outliers, and confidence_weighted_median_note across normal, boundary, and multi-pass scenarios. Added test_ultrastar_writer.py for new silence_threshold and calculate_silent_beat_length APIs. Added comprehensive test_compare_ultrastar.py covering parsing, format detection, pitch/MIDI conversion, word alignment, and full integration scenarios.
Documentation
README.md
Added BPM Override section documenting --bpm option usage, decimal support, and reference guidance; updated Table of Contents with new navigation entry.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UltraSinger as UltraSinger.py
    participant AudioProc as Audio Processing
    participant MidiDetect as MIDI Detection
    participant OctaveCorr as Octave Correction
    participant USWriter as Ultrastar Writer

    User->>UltraSinger: Run with --bpm override & denoise opts
    UltraSinger->>AudioProc: CreateProcessAudio()<br/>(returns whisper_audio_path<br/>+ pitch_audio_path)
    AudioProc-->>UltraSinger: Two audio paths
    
    UltraSinger->>AudioProc: TranscribeAudio(whisper_audio_path)
    AudioProc-->>UltraSinger: Transcription + silence removal
    
    UltraSinger->>AudioProc: get_bpm_from_file(whisper_audio_path)
    Note over AudioProc: Apply _pick_best_tempo<br/>correction with ratios
    AudioProc-->>UltraSinger: Corrected BPM
    
    UltraSinger->>MidiDetect: create_midi_from_pitch(pitch_audio_path)
    Note over MidiDetect: Use confidence_weighted_median_note<br/>instead of mode
    MidiDetect-->>UltraSinger: midi_segments
    
    UltraSinger->>OctaveCorr: correct_global_octave(midi_segments)
    Note over OctaveCorr: Shift median into vocal range
    OctaveCorr->>OctaveCorr: correct_octave_outliers()<br/>(two-phase: local + global)
    OctaveCorr-->>UltraSinger: Corrected midi_segments
    
    UltraSinger->>USWriter: create_ultrastar_txt()<br/>(with silence_threshold)
    Note over USWriter: Calculate linebreaks<br/>using percentile-based threshold
    USWriter-->>UltraSinger: UltraStar file
    
    UltraSinger-->>User: Karaoke file generated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Fix installer with uv and swift f0 pitcher #299: Introduces complementary installer script updates using uv, pyproject/CI configuration changes, and modifications to pitcher/ffmpeg_helper modules for audio format handling, affecting overlapping files and dependency management patterns.

Poem

🐰 A rabbit's ode to harmonious pitches and tempo

With whispers and pitches now split on the road,
And octaves corrected to lighten the load,
The BPM dances to rhythm's refrain—
From chaos of ratios, melody's reign! 🎵🥁

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 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.

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