Skip to content

Use embedded loop tags as the first choice and add a lossless trim command - #77

Closed
Xehanort88 wants to merge 3 commits into
arkrow:masterfrom
Xehanort88:embedded-tags-and-trim
Closed

Xehanort88 wants to merge 3 commits into
arkrow:masterfrom
Xehanort88:embedded-tags-and-trim

Conversation

@Xehanort88

Copy link
Copy Markdown

Depends on #76. The first two commits come from that PR (the test suite and bug fixes); only the last commit, Use embedded loop tags as the first choice and add a lossless trim command, is new here. Once #76 is merged, this PR will show only that commit.

Summary

Two related features for tracks that already have loop metadata, which is common for game music.

Embedded loop tags as the first choice

If a file already has loop tags (LOOP_START/LOOP_END, LOOPSTART/LOOPLENGTH, or any other name recognized by the tag auto-detection from #66), those loop points now come first in every command (play, export-points, tag, split-audio, extend, trim), ahead of the detected ones.

  • Detection still runs, and its results are listed after the tagged loop, so -i still lets you pick another one. The tagged loop shows as "from tags" in the interactive table, and play reports it as such.
  • If detection finds no loop, the tagged points are still returned instead of raising LoopNotFoundError.
  • Tags are ignored when --approx-loop-position is given (the user asked for loops near a specific position) and when they fall outside the audio (with a warning).
  • New --ignore-tags option to skip them. From Python: find_loop_pairs(use_embedded_tags=False) and read_embedded_loop_pair().
  • LoopPair gains a from_metadata field (defaults to False, so existing code is unaffected).

trim command: cut after the loop end without re-encoding

pymusiclooper -i trim --path "TRACK.ogg" --keep-after 500

Writes TRACK-trimmed.ext, keeping everything up to the loop end plus --keep-after N samples (default 0), with the original format, bit depth and tags.

Format How Result
WAV (PCM 8–32-bit, float, double) Samples read and written in their native format Bit-exact
FLAC Same; the compressed bytes differ, but the audio does not Bit-exact decoded audio
Ogg Vorbis Cut at the container level, with no decoding or re-encoding (new pymusiclooper/ogg.py) Bit-exact decoded audio, ending on the exact sample
Anything else (MP3, Opus, µ-law WAV, …) Rejected with an explicit error, since trimming would require lossy re-encoding —

How the Ogg Vorbis cut works: pages before the cut are copied byte for byte. On the last kept page, the audio packets after the one containing the end sample are dropped, then its granule position is lowered to the exact end sample and the page is flagged as end of stream. The Vorbis spec (section A.2) defines this as how to end a stream on a sample that isn't on a block boundary. Dropping the extra packets keeps the trimmed samples within the final packet, the same layout encoders produce. That matters because FFmpeg only trims within the last packet. Packet durations come from the mode block flags at the end of the setup header, found by scanning it backwards the same way FFmpeg's vorbis_parser.c and liboggz do, and they're checked against the page granule positions. If they don't add up, the cut falls back to page granularity, which is still valid per the spec.

extend's existing tag-copying code is moved into a shared _copy_tags helper, which trim reuses.

Test plan

  • uv run pytest: 72 passed (20 new tests covering embedded tags, bit-exact WAV/FLAC/Ogg trims, tag preservation, the Ogg packet durations and fallback, and rejection of unsupported formats)
  • Optional tests/test_ogg_ffmpeg.py decodes trimmed Ogg files with FFmpeg as an independent decoder. It uses PML_FFMPEG, tools/ffmpeg/ or PATH, and skips when FFmpeg is missing or too old to honor Vorbis end trimming. It passes with FFmpeg 7.0.2 and 9.0.2.
  • Trimmed real-world Ogg Vorbis files (mono to 6 channels, 16–48 kHz) at many cut points. Decoded output is exact with libvorbis and FFmpeg 7.0.2/9.0.2; with FFmpeg only for cuts past the first audio page (~0.1–0.2 s), which is far earlier than any loop end.

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.
@Xehanort88 Xehanort88 mentioned this pull request Sep 23, 2026
3 tasks done
@Xehanort88 Xehanort88 closed this Sep 27, 2026
@Xehanort88
Xehanort88 deleted the embedded-tags-and-trim branch September 27, 2026 12:55
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