API Reference
The full public surface of waxcut — everything in waxcut.__all__. All
names are importable directly from the top-level waxcut package.
load_audio_stream
def load_audio_stream(path: Path) -> AudioStream
Loads an MP3 file and parses it into an AudioStream ready
for frame-accurate splitting.
Reads the whole file into memory, scans it for MPEG Layer III frames (see
iter_frames, which also skips any leading ID3v2 tag), and
checks whether the first frame is a Xing/Info/VBRI VBR header rather than
real audio. If it is, that frame is excluded from the returned frames
list, every remaining frame's start_ms is rebased so the first real audio
frame starts at 0, and — if the header carries a LAME gapless extension —
encoder_delay_samples/encoder_padding_samples are extracted from it.
Args
path(Path) — path to an MP3 file on disk.
Returns
AudioStream
Raises
UnsupportedMp3Error— no valid MPEG Layer III frame was found anywhere in the file (propagated fromiter_frames), or the file consists of only a VBR header frame with no audio frames after it.FileNotFoundError(and other OS-level errors) — propagated from reading the file ifpathdoesn't exist or can't be opened.
AudioStream
@dataclass(frozen=True)
class AudioStream:
data: bytes
frames: list[Frame]
encoder_delay_samples: int
encoder_padding_samples: int
sample_rate: int
A parsed MP3 stream with located frames and gapless metadata. Normally
constructed via load_audio_stream rather than
directly.
Fields
data(bytes) — the complete file bytes this stream was parsed from.frames(list[Frame]) — the located frames, in file order. If the source file had a Xing/Info/VBRI VBR header frame, it has already been excluded here, and the remaining frames rebased so the first one hasstart_ms == 0.encoder_delay_samples(int) — samples of encoder padding at the start of the audio, read from a LAME gapless tag if one was present;0otherwise. Informational only: it does not affect frame boundaries or where splits can land — real players skip this many samples at the start, but split output is fresh audio starting exactly at a frame boundary with no delay semantics of its own to carry over.encoder_padding_samples(int) — samples of encoder padding at the end of the audio, from the same source;0if absent. Same caveat as above (real players stop this many samples early; split output has no padding semantics of its own).sample_rate(int) — audio sample rate in Hz (e.g.44100,48000).
Properties
duration_ms -> float— total playback duration spanned byframes(equivalent tototal_duration_ms(self.frames)).playable_duration_ms -> float— the duration a real player would report:duration_msminus the gapless delay/padding trim (converted from samples to milliseconds viasample_rate), clamped to a minimum of0.0.
Frame
@dataclass(frozen=True)
class Frame:
offset: int
length: int
start_ms: float
duration_ms: float
One located MPEG Layer III frame, as produced by
iter_frames or found in AudioStream.frames.
Fields
offset(int) — byte offset of this frame's header within the source data.length(int) — total length of this frame in bytes (header + side info + audio data) — how far to advance fromoffsetto reach the next frame.start_ms(float) — playback position of this frame's start, in milliseconds. Frames returned directly byiter_framesare timed from the very first frame found in the data; frames onAudioStream.framesare timed relative to the first real audio frame, i.e. after any VBR header frame has been excluded byload_audio_stream.duration_ms(float) — this frame's own playback duration, in milliseconds.
frame_index_at
def frame_index_at(frames: list[Frame], target_ms: float) -> int
Returns the index of the last frame in frames that starts at or before
target_ms. This is how a "split at N milliseconds" request becomes a
frame boundary for slice_bytes: a lossless split can only
land on a frame's own start, so this snaps to the nearest one at or before
the requested time.
Args
frames(list[Frame]) — frame list, e.g. fromiter_framesorAudioStream.frames.target_ms(float) — desired split point in milliseconds.
Returns
int— an index intoframes. Atarget_msbefore the first frame's start clamps to0; atarget_msat or beyond the last frame's start clamps to the last index.
On an empty frames list: raises ValueError immediately, rather than
returning a meaningless index.
slice_bytes
def slice_bytes(data: bytes, frames: list[Frame], start_idx: int, end_idx: int) -> bytes
Returns the raw bytes covering frames[start_idx:end_idx] as one
contiguous range copied directly out of data — this is a byte-copy, not a
re-parse. Frames are assumed contiguous, which holds for any list produced
by iter_frames from the same data. The result is itself a decodable,
standalone MP3 stream (no container/ID3 wrapper), byte-identical to the
corresponding span of the original file.
Args
data(bytes) — the same bytesframeswas derived from.frames(list[Frame]) — frame list fromiter_framesorAudioStream.frames.start_idx(int) — first frame index to include (inclusive).end_idx(int) — one past the last frame index to include (exclusive) — standard Python slice semantics.
Returns
bytes—b""ifstart_idx >= end_idx; otherwise the byte span fromframes[start_idx].offsetthrough the end offrames[end_idx - 1].
On an empty frames list: raises ValueError. On a negative
start_idx/end_idx: raises IndexError explicitly, rather than
silently wrapping to an unintended frame the way Python's own negative
indexing would. Positive out-of-range indices surface as a normal Python
IndexError from indexing frames[start_idx] or frames[end_idx - 1].
split_at
def split_at(stream: AudioStream, timestamps_ms: list[float]) -> list[bytes]
Convenience wrapper around frame_index_at +
slice_bytes for the common case of cutting at several
timestamps in one call, instead of looping manually.
Args
stream(AudioStream) — fromload_audio_stream.timestamps_ms(list[float]) — desired cut points, in milliseconds. Need not be sorted or in range — each is clamped byframe_index_at, so an out-of-order or duplicate timestamp simply produces an empty segment at that position rather than raising.
Returns
list[bytes]—len(timestamps_ms) + 1segments, in order. Each is a standalone, decodable MP3 stream. Concatenating all of them (seejoin_frames) reproduces the original audio exactly.
join_frames
def join_frames(segments: list[bytes]) -> bytes
Concatenates frame-aligned MP3 byte segments back into one stream. Safe because MPEG Layer III frames are self-delimited — each carries its own length in its header — so concatenation always reproduces the original bytes exactly, with no re-parsing or re-alignment needed.
Args
segments(list[bytes]) — byte segments to join, in order, as produced byslice_bytesorsplit_at.
Returns
bytes— the concatenated result.
total_duration_ms
def total_duration_ms(frames: list[Frame]) -> float
Total playback duration spanned by frames, in milliseconds — the last
frame's start_ms plus its duration_ms.
Args
frames(list[Frame]) — a non-empty list ofFrame, as returned byiter_frames.
Returns
float
Raises
IndexError—framesis empty (frames[-1]on an empty list).iter_framesitself never returns an empty list — it raisesUnsupportedMp3Errorinstead — so this only happens if you pass in an empty list you constructed or filtered yourself.
iter_frames
def iter_frames(data: bytes) -> list[Frame]
Scans data for MPEG Layer III audio frames, skipping any leading ID3v2
tag (via id3v2_size). At each position it tries to parse a
valid frame header; if the header doesn't check out (bad sync word,
unsupported layer, a reserved bitrate/sample-rate index, or a computed
frame length that would run past the end of data), the scan advances one
byte and keeps looking — this is what lets it skip past a trailing
ID3v1/APE tag or other non-frame bytes without getting stuck. Every frame
found is recorded with its own start_ms/duration_ms, timed
cumulatively from the first frame found in data.
Note that this returns every parsed frame, including a leading
Xing/Info/VBRI VBR header frame if the file has one — excluding that frame
from playback/duration is load_audio_stream's job,
not iter_frames's.
Args
data(bytes) — raw file bytes.
Returns
list[Frame]— never empty.
Raises
UnsupportedMp3Error— no valid MPEG Layer III frame was found anywhere indata. This covers both non-MP3 input and files containing only Layer I/II frames, which this parser doesn't recognize (see How It Works).
id3v2_size
def id3v2_size(data: bytes) -> int
Returns the byte length of a leading ID3v2 tag at the start of data, or
0 if data doesn't start with one. The tag's size is read from ID3v2's
syncsafe 4-byte size field and added to the fixed 10-byte header size.
Args
data(bytes) — raw file bytes.
Returns
int—0if no ID3v2 tag is present, otherwise the tag's total size in bytes, including its 10-byte header.
UnsupportedMp3Error
class UnsupportedMp3Error(ValueError)
Raised when frame parsing can't make sense of the input as an MP3. In the current implementation this covers two cases:
iter_framesfinds no valid MPEG Layer III frame anywhere in the data — this includes files that aren't MP3s at all, and files that contain only Layer I/II frames, which this parser doesn't recognize.load_audio_streamfinds a file consisting of only a VBR header frame (Xing/Info/VBRI) with no real audio frames after it.
It subclasses ValueError.