Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- `trim` command: losslessly cut the audio a given number of samples (`--keep-after`, 100 by default) after the loop end, keeping the original format, bit depth and metadata (WAV, FLAC and Ogg Vorbis only). WAV and Ogg Vorbis files are cut without re-encoding
- Interactive mode offers to trim WAV, FLAC and Ogg Vorbis files after the loop end once the loop is chosen (except when printing loop points to the terminal); with `tag`, a single tagged and trimmed copy is written
- Loop points already stored in a file's metadata tags (e.g. LOOP_START/LOOP_END, LOOPSTART/LOOPLENGTH), or in a WAV file's sampler (`smpl`) chunk, are used as the first loop choice by all commands; `--ignore-tags` skips them
- `--tags-only` option: use only the loop points stored in the files' metadata tags, skipping the (slower) loop analysis; files without valid loop tags are skipped
- Batch processing ends with a summary of the processed files (with loop points from their tags or detected ones), and of the skipped and failed files grouped by reason
- `tag` also writes the loop points of WAV files to their sampler (`smpl`) chunk, which many game engines and samplers read
- Test suite, run on Ubuntu and Windows with Python 3.10 and 3.13

### Changed

- Loop points are aligned at the sample level: the loop end is moved to where the waveforms at the two loop points line up, then both points are shifted to where they differ the least, avoiding the faint flam or phase smear of loops off by a few hundred samples (falls back to the nearest zero crossings when the waveforms do not match well enough)
- The chroma (pitch classes) used to compare the notes is computed with the audio's sample rate instead of librosa's default of 22050 Hz, which shifted the pitch classes of 48 kHz audio by ~1.5 semitones. This changes the best detected loop of some tracks, with no measurable effect on the loop quality overall
- The candidate loop search is ~60x faster (much faster brute force), and the analysis uses about half the memory, with unchanged results
- Skipped and non-audio files are no longer logged as errors during batch processing (they are listed in the summary; `--verbose` still logs them)

### Fixed

- Mono tracks were played and exported louder than the original
- Longer loops were never preferred among near-identical scores
- Loops starting in the first seconds of a track were underscored
- Interactive mode discarded the choice made after `more`, `all` or `reset` and prompted again
- `extend` with a fade length of 0 crashed
- `export_tags()` without an output directory used the file path as the directory
- The txt export message named `loop.txt` instead of `loops.txt`
- Ctrl+C at the interactive prompt now exits cleanly on Windows


## [3.6.0] - 2025-11-01

### Changed
Expand Down
4 changes: 4 additions & 0 deletions CLI_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@
## pymusiclooper extend

![`pymusiclooper extend --help`](img/pymusiclooper-extend.svg)

## pymusiclooper trim

![`pymusiclooper trim --help`](img/pymusiclooper-trim.svg)
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ pymusiclooper -i play --url "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
pymusiclooper play-tagged --path "TRACK_NAME.mp3" --tag-names LOOP_START LOOP_END
```

*Note: if a file already has loop metadata tags (e.g. LOOP_START/LOOP_END or LOOPSTART/LOOPLENGTH), or is a WAV file with a loop in its sampler (`smpl`) chunk, those loop points are used as the first loop choice by all commands. Use `--ignore-tags` to skip them.*
*Note: if a file already has loop metadata tags (e.g. LOOP_START/LOOP_END or LOOPSTART/LOOPLENGTH), or is a WAV file with a loop in its sampler (`smpl`) chunk, those loop points are used as the first loop choice by all commands. Use `--ignore-tags` to skip them, or `--tags-only` to use only them and skip the (slower) loop analysis; with `--tags-only`, files without valid loop tags are skipped.*

### Export

*Note: batch processing is available for all export subcommands. Simply specify a directory instead of a file as the path to be used.*
*Note: batch processing is available for all export subcommands. Simply specify a directory instead of a file as the path to be used. A summary at the end lists how many files were processed (with loop points from their tags or detected ones), and which were skipped or failed and why.*

```sh
# Split the audio track into intro, loop and outro files.
Expand Down Expand Up @@ -164,6 +164,10 @@ pymusiclooper -i tag --path "TRACK_NAME.ogg" --tag-names LOOPSTART LOOPLENGTH
# (compatible with https://github.com/libertyernie/LoopingAudioConverter/)
# Note: each line in loop.txt follows the following format: {loop-start} {loop-end} {filename}
pymusiclooper -i export-points --path "/path/to/dir/" --export-to txt

# Trim all the tracks of a directory that already have loop tags, using only their tags (no loop analysis);
# tracks without valid loop tags are skipped and listed in the summary at the end
pymusiclooper trim --path "/path/to/dir/" --tags-only --output-dir "/path/to/trimmed/"
```

### Miscellaneous
Expand Down
Loading
Loading