Skip to main content

waxcut

PyPI CI Fuzzing Docs OpenSSF Scorecard OpenSSF Best Practices License Python

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' below (keep the quotes) 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; print(round(l(Path('song.mp3')).playable_duration_ms / 1000, 1), 's')"

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 ID3v2 tags when scanning for the first frame.
  • Excludes the Xing/Info/VBRI VBR 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.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for waxcut 0.3.1
File Size Uploaded
waxcut-0.3.1.tar.gz 22.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for waxcut 0.3.1
File Interpreter ABI Platform
waxcut-0.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 47.2 kB

Release files / waxcut-0.3.1.tar.gz

Download URL waxcut-0.3.1.tar.gz
Size 22.9 kB
Tags Source
SHA-256 checksum
How to use checksums
ad8664c38d36b4b9c7312e1c96af6aef6fe32e1390866d2f09f2a50a7f0db7e1
BLAKE2b-256 checksum
How to use checksums
8d2dbf8d84cabf4f2f6d3b4f0e10f73692b3f5bf12fdccf71076069302e395cd
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.1-py3-none-any.whl

Download URL waxcut-0.3.1-py3-none-any.whl
Size 24.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f2eb3fb4d6377ded3f543c3f7cc42c9cddda47cf2530d189a36b434253fcc8e6
BLAKE2b-256 checksum
How to use checksums
305e415e864c082f7ad1983bbb4afd9a3fe5a41c484f95241b0560947b438860
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 history Release notifications | RSS feed

This release

0.3.1 This release

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page