Skip to main content

Frame-accurate, lossless MP3 splitting and duration parsing in pure Python — no ffmpeg, no subprocess, no decode step.

Project description

waxcut

PyPI CI Fuzzing Docs OpenSSF Scorecard OpenSSF Best Practices License Python

Frame-accurate, lossless MP3 splitting and duration parsing in pure Python — 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 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

waxcut-0.2.1.tar.gz (13.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

waxcut-0.2.1-py3-none-any.whl (14.6 kB view details)

Uploaded Python 3

File details

Details for the file waxcut-0.2.1.tar.gz.

File metadata

  • Download URL: waxcut-0.2.1.tar.gz
  • Upload date:
  • Size: 13.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","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}

File hashes

Hashes for waxcut-0.2.1.tar.gz
Algorithm Hash digest
SHA256 45476a09f27ffbd88f214ddee35e53d2ceceef3813033278861e0efa8dddb5f8
MD5 28574b8c82a9744634bba294dd8ce672
BLAKE2b-256 9aae24b389cba83d65a21efff4a564a0ce06ee62ac68b2c1ad5c2e25ac923491

See more details on using hashes here.

File details

Details for the file waxcut-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: waxcut-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 14.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","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}

File hashes

Hashes for waxcut-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c76037da924b2daa6bfd1d25774e9ce40481ef1af68b9247a8d3fdf7d0af8d0f
MD5 6984203493f83852460cef337af15631
BLAKE2b-256 1733bcaf3f0949721e32e9d2a5bccd2fb46a450d5f1e318927fc1c52ed79b6c8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page