Skip to content

Test suite, bug fixes, embedded loop tags, lossless trim and sample-level loop alignment - #1

Merged
Xehanort88 merged 4 commits into
masterfrom
add-test-suite
Sep 23, 2026
Merged

Xehanort88 merged 4 commits into
masterfrom
add-test-suite

Conversation

@Xehanort88

Copy link
Copy Markdown
Owner

Summary

Four commits, each passing its own test suite:

  1. Add test suite — pytest suite that generates its own audio (an intro followed by a repeated 8-note pattern), so the correct loop points are known in advance and detection is checked for correctness, not just for running. Covers audio loading, loop detection and scoring, zero-crossing snapping, all exports, the interactive picker, batch mode and every CLI command. Adds a CI workflow (Ubuntu and Windows, Python 3.10 and 3.13).
  2. Fix bugs found by the test suite (7):
    • Mono tracks were played and exported louder than the original (analysis-only normalization applied in place to the playback audio).
    • "Prefer longer loops" never did anything (ran before loop positions were set).
    • Loops starting in the first seconds of a track were underscored (look-behind window zero-padded on the wrong side).
    • Interactive more / all / reset discarded the choice made afterwards.
    • extend --fade-length 0 crashed.
    • export_tags() without output_dir used the file path as the directory.
    • The txt export message named loop.txt instead of loops.txt.
  3. Embedded loop tags and lossless trim command
    • Loop points already stored in a file's tags (LOOP_START/LOOP_END, LOOPSTART/LOOPLENGTH, …) are now the first choice in every command, ahead of the detected ones. New --ignore-tags option.
    • New trim command: cuts the audio --keep-after N samples after the loop end, keeping the original format, bit depth and tags. WAV and FLAC are rewritten bit-exact; Ogg Vorbis is cut at the container level without re-encoding, ending on the exact sample in both libvorbis and FFmpeg. Other formats are rejected.
  4. Sample-level loop alignment
    • Loop points were located on 512-sample STFT frames and each snapped to its own zero crossing, so loops were often a few hundred samples off (a faint flam or phase smear at the jump).
    • The loop end is now moved to where the waveforms at the two points line up (normalized cross-correlation), then both points are shifted together to where the waveforms differ the least. Falls back to zero crossings when the correlation is weak (< 0.3).
    • On game tracks with official loop tags, all top detected loops now match the official loop length (or a multiple) to the sample.
    • Adds scipy as a direct dependency (already required by librosa).

Test plan

  • uv run pytest: 83 passed
  • Each commit checked out and tested on its own (45 passed + 7 xfail → 52 → 72 → 83)
  • Lossless trim verified bit-exact on real WAV/FLAC files, and on real Ogg Vorbis files with libvorbis and FFmpeg 7.0.2 / 9.0.2
  • Alignment compared against the previous method on 181 candidates from real tracks, and against official loop tags on two game tracks

Tests generate their own audio: an intro followed by a repeated 8-note
pattern, so the correct loop points are known in advance and loop
detection can be checked for correctness, not just for running.

Covers audio loading, loop detection and scoring, zero-crossing
snapping, split/extend/tag/txt exports, the interactive picker, batch
file discovery and every CLI command.

Known bugs are marked as strict xfail, so each fix is flagged until its
marker is removed.

Also adds pytest as a dev dependency and a CI workflow running the tests
on Ubuntu and Windows with Python 3.10 and 3.13.
- Mono tracks were played and exported louder than the original: the
  analysis-only normalization was applied in place to the audio array
  that is also used for playback, since to_mono returns its input as-is
  for mono audio.
- Longer loops were never preferred among near-identical scores:
  _prioritize_duration ran before loop_start/loop_end were set, so every
  duration it compared was 0.
- Loops starting in the first seconds of a track were underscored: the
  truncated look-behind window was zero-padded on the side nearest the
  loop point, where the weights are heaviest.
- Interactive mode discarded the choice made after 'more', 'all' or
  'reset' and prompted again.
- extend with fade_length=0 crashed (x[-0:] selects the whole array).
- export_tags() without output_dir used the file path as the directory.
- The txt export message named loop.txt instead of loops.txt.
…mmand

Embedded loop tags:
- Loop points already stored in a file's metadata (LOOP_START/LOOP_END,
  LOOPSTART/LOOPLENGTH and the other known tag names) are now returned
  first by find_loop_pairs, ahead of the detected candidates. They are
  still returned if detection finds no loop, and are skipped when an
  approximate loop position is given or when they are out of range.
- LoopPair gains a from_metadata flag; interactive mode shows these
  points as "from tags" and play reports them as such.
- New --ignore-tags option to skip them.

trim command:
- Cuts the audio a given number of samples (--keep-after) after the
  loop end, keeping the original format, bit depth and tags.
- WAV and FLAC are rewritten bit-exact from their native sample format.
- Ogg Vorbis is cut at the container level without re-encoding: the
  audio packets after the one containing the end sample are dropped and
  the granule position of the final page is lowered to the exact end,
  so decoders that only trim the last packet (e.g. FFmpeg) also end on
  the exact sample. Falls back to a page-level cut if the packet
  durations cannot be determined.
- Other formats are rejected, since trimming them would require lossy
  re-encoding.

Tests cover both features; an optional test checks trimmed Ogg files
with FFmpeg as an independent decoder when it is available.
Loop points were located on STFT frames (512 samples) and each was then
moved to its own nearest zero crossing, without checking that the
waveforms at the two points line up. Loops were often off by a few
hundred samples, which can be heard as a faint flam or phase smear at
the jump.

Each candidate is now refined by:
- moving the loop end (by up to 2 frames) to where the waveform around
  it best matches the waveform around the loop start, using normalized
  cross-correlation over 75 ms on each side;
- shifting both points by the same amount (keeping the loop length) to
  where the waveforms differ the least within +/-5 ms, since the
  difference at the jump is what causes clicks.

When the correlation is below 0.3, the audio at the two points does not
match well enough for this to be reliable, and each point is moved to
its nearest zero crossing as before. The threshold was chosen by
comparing both methods on 181 candidates from real tracks.

On game tracks with official loop tags, all top detected loops now have
exactly the official loop length (or a multiple of it). The added cost
is about 0.5 ms per candidate.

Also declares scipy as a direct dependency (already required by librosa).
@Xehanort88
Xehanort88 merged commit de95fdc into master Sep 23, 2026
8 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