waxcut
Frame-accurate, lossless MP3 splitting and duration parsing in pure Python with no ffmpeg, no subprocess, no decode step.
Cuts are made by parsing the file's own MPEG frame headers and byte-copying whole frames: output is bit-identical to the source, just shorter.
Install
pip install waxcut
# or
uv add waxcut
Usage
Quick duration check from the shell, no script needed — replace song.mp3
at the end with the path to your own file and run it as-is:
python -c "from pathlib import Path; from waxcut import load_audio_stream as l; import sys; print(f'{l(Path(sys.argv[1])).playable_duration_ms / 1000:.1f}s')" song.mp3
For actually splitting a file, here's the full pattern — load it once, then cut at whatever timestamp you want:
from pathlib import Path
from waxcut import load_audio_stream, frame_index_at, slice_bytes
stream = load_audio_stream(Path("song.mp3"))
print(f"{stream.playable_duration_ms / 1000:.1f}s")
# Split at the 90-second mark
cut_at = frame_index_at(stream.frames, target_ms=90_000)
first_half = slice_bytes(stream.data, stream.frames, 0, cut_at)
second_half = slice_bytes(stream.data, stream.frames, cut_at, len(stream.frames))
Path("part1.mp3").write_bytes(first_half)
Path("part2.mp3").write_bytes(second_half)
Splitting into more than two parts — split_at/join_frames collapse the
loop above into one call:
from waxcut import load_audio_stream, split_at, join_frames, slice_bytes
stream = load_audio_stream(Path("mixtape.mp3"))
parts = split_at(stream, timestamps_ms=[90_000, 180_000, 270_000])
for i, part in enumerate(parts):
Path(f"part{i}.mp3").write_bytes(part)
# join_frames is the inverse: reassembling parts reproduces the original
assert join_frames(parts) == slice_bytes(stream.data, stream.frames, 0, len(stream.frames))
Why not ffmpeg or a decode/re-encode library?
MP3 frames are self-describing, so their boundaries can be found directly from the byte stream — no decode step, no re-encode step, no external binary to shell out to.
waxcut also handles the parts that make naive frame-splitting subtly wrong:
- Skips leading
ID3v2tags when scanning for the first frame. - Excludes the
Xing/Info/VBRIVBR header frame — encoder metadata, not audio, and including it corrupts both output and duration. - Parses LAME's gapless delay/padding extension, so reported duration matches what a real player shows, not just the raw frame count.
Duration parsing is cross-validated against mutagen's independent implementation to the millisecond (see Testing).
Scope
Parses MPEG-1/2/2.5 Audio Layer III — what "MP3" actually means. Layer
I/II frames raise UnsupportedMp3Error rather than being silently
mishandled, since virtually no real-world "MP3" file uses them.
Testing
uv sync
uv run pytest tests/ -v
Validated against mutagen's independent parser (duration must match
exactly, including LAME gapless delay/padding) across CBR/VBR, mono/stereo,
and multiple encoder tags. Where ffmpeg/ffprobe are available, every
split output is independently decoded to confirm it's valid. Fuzzed
continuously with ClusterFuzzLite.
Contributing
Bug reports and pull requests are welcome — see CONTRIBUTING.md for the dev setup and PR process. Report security vulnerabilities per SECURITY.md rather than as public issues.
License
Apache-2.0 — see LICENSE.
Metadata
Release files for waxcut 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| waxcut-0.3.0.tar.gz | 22.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| waxcut-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 46.8 kB
Release files / waxcut-0.3.0.tar.gz
| Download URL | waxcut-0.3.0.tar.gz |
|---|---|
| Size | 22.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
34ea2f56c5a86522e4c9fd849a7e7fcc6c214d76593a8955bbd134463214a29f
|
|
BLAKE2b-256 checksum How to use checksums |
1ea61670232ff6003d4bdf9e64998c8cc8edb0d02a27a55916ea331629d47d5d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / waxcut-0.3.0-py3-none-any.whl
| Download URL | waxcut-0.3.0-py3-none-any.whl |
|---|---|
| Size | 24.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8a97b4db37a06b73bcaf9a8ff69f7b7385d6d19d86adfc89e38406429b76c03d
|
|
BLAKE2b-256 checksum How to use checksums |
77ada9bd0d9874d4aa6ac5daf2041d647650f0805271e454260f71abeb3ea41e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|