pith-mp3 0.1.1__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pith_mp3/__init__.py +209 -0
- pith_mp3/pith_mp3.dll +0 -0
- pith_mp3-0.1.1.dist-info/METADATA +12 -0
- pith_mp3-0.1.1.dist-info/RECORD +6 -0
- pith_mp3-0.1.1.dist-info/WHEEL +5 -0
- pith_mp3-0.1.1.dist-info/top_level.txt +1 -0
pith_mp3/__init__.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 pith-hash
|
|
3
|
+
"""pith-mp3 SDK: MPEG audio decoding through ctypes.
|
|
4
|
+
|
|
5
|
+
Decodes layers I/II/III (including the bit reservoir) through the Rust
|
|
6
|
+
cdylib, handing back the canonical byte stream the ``reference.json``
|
|
7
|
+
vectors are defined over: a 48-byte big-endian header followed by the
|
|
8
|
+
decoded PCM, interleaved ``i32`` little-endian.
|
|
9
|
+
|
|
10
|
+
The cdylib is located through the suite's discovery chain:
|
|
11
|
+
|
|
12
|
+
1. ``PITH_CDYLIB`` — an explicit cdylib *file* path;
|
|
13
|
+
2. ``PITH_CDYLIB_DIR`` — a *directory* scanned for the cdylib names
|
|
14
|
+
(the CD pipeline points this at ``target/release``);
|
|
15
|
+
3. the package directory itself (the built wheel ships the cdylib as
|
|
16
|
+
package data);
|
|
17
|
+
4. ``<repo root>/target/release`` — the repository working-tree layout,
|
|
18
|
+
so a source checkout runs against a local cargo build with no
|
|
19
|
+
configuration.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import ctypes
|
|
25
|
+
import os
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Canonical",
|
|
31
|
+
"FfiError",
|
|
32
|
+
"LibraryNotFoundError",
|
|
33
|
+
"find_cdylib",
|
|
34
|
+
"decode_canonical",
|
|
35
|
+
"parse_canonical",
|
|
36
|
+
"STATUS_OK",
|
|
37
|
+
"STATUS_INVALID",
|
|
38
|
+
"STATUS_REJECTED",
|
|
39
|
+
"HEADER_LEN",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
#: Status: success.
|
|
43
|
+
STATUS_OK = 0
|
|
44
|
+
#: Status: a caller argument is invalid (a null pointer).
|
|
45
|
+
STATUS_INVALID = -1
|
|
46
|
+
#: Status: the core decoder refused the input (malformed MPEG audio).
|
|
47
|
+
STATUS_REJECTED = -2
|
|
48
|
+
|
|
49
|
+
#: The canonical stream's header length in bytes.
|
|
50
|
+
HEADER_LEN = 48
|
|
51
|
+
|
|
52
|
+
#: Every cdylib file name cargo may drop into the build directory, per
|
|
53
|
+
#: platform (windows / linux / macOS).
|
|
54
|
+
CDYLIB_NAMES = ("pith_mp3.dll", "libpith_mp3.so", "libpith_mp3.dylib")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class Canonical:
|
|
59
|
+
"""A decoded MPEG audio stream, re-expressed from the canonical
|
|
60
|
+
byte stream.
|
|
61
|
+
|
|
62
|
+
``pcm`` is the interleaved ``i32`` little-endian sample buffer —
|
|
63
|
+
exactly the bytes the ``pcm_sha256`` digest covers.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
#: Layer of the first frame: 1, 2 or 3.
|
|
67
|
+
layer: int
|
|
68
|
+
#: Bitrate of the first frame in kbit/s (VBR streams vary after).
|
|
69
|
+
bitrate_kbps: int
|
|
70
|
+
#: Sample rate declared in the first frame header.
|
|
71
|
+
header_sample_rate: int
|
|
72
|
+
#: Channel count declared in the first frame header.
|
|
73
|
+
header_channels: int
|
|
74
|
+
#: Decoded channel count, 1 or 2.
|
|
75
|
+
channels: int
|
|
76
|
+
#: Decoded sample rate per channel.
|
|
77
|
+
sample_rate: int
|
|
78
|
+
#: Frames actually decoded.
|
|
79
|
+
frames: int
|
|
80
|
+
#: Decoded sample count, interleaved across channels.
|
|
81
|
+
samples: int
|
|
82
|
+
#: True when a Xing/Info VBR header frame was seen.
|
|
83
|
+
vbr: bool
|
|
84
|
+
#: The canonical byte stream the digest is computed over.
|
|
85
|
+
raw: bytes
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def pcm(self) -> bytes:
|
|
89
|
+
"""The interleaved ``i32`` little-endian PCM (everything after
|
|
90
|
+
the 48-byte header)."""
|
|
91
|
+
return self.raw[HEADER_LEN:]
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def pcm_head_hex(self) -> str:
|
|
95
|
+
"""The first 64 PCM bytes, lowercase hex — the same string the
|
|
96
|
+
vectors' ``pcm_head_hex`` records."""
|
|
97
|
+
return self.raw[HEADER_LEN : HEADER_LEN + 64].hex()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class LibraryNotFoundError(OSError):
|
|
101
|
+
"""No cdylib was found through the discovery chain."""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class FfiError(Exception):
|
|
105
|
+
"""A non-zero status code came back from the cdylib."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, op: str, status: int) -> None:
|
|
108
|
+
detail = {
|
|
109
|
+
STATUS_INVALID: "invalid argument",
|
|
110
|
+
STATUS_REJECTED: "input rejected",
|
|
111
|
+
}.get(status, "unknown failure")
|
|
112
|
+
super().__init__(f"{op} failed: {detail} (status {status})")
|
|
113
|
+
#: The raw status code the FFI returned.
|
|
114
|
+
self.status = status
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def find_cdylib() -> Path:
|
|
118
|
+
"""Locates the cdylib through the suite's discovery chain."""
|
|
119
|
+
explicit = os.environ.get("PITH_CDYLIB")
|
|
120
|
+
if explicit:
|
|
121
|
+
p = Path(explicit)
|
|
122
|
+
if p.is_file():
|
|
123
|
+
return p
|
|
124
|
+
env_dir = os.environ.get("PITH_CDYLIB_DIR")
|
|
125
|
+
candidates: list[Path] = []
|
|
126
|
+
if env_dir:
|
|
127
|
+
env_dir_path = Path(env_dir)
|
|
128
|
+
candidates.append(env_dir_path)
|
|
129
|
+
if not env_dir_path.is_absolute():
|
|
130
|
+
# CD and local runs invoke tools from the repository root or
|
|
131
|
+
# from sdk/<lang>; resolve the env value against both.
|
|
132
|
+
candidates.append(Path.cwd() / env_dir_path)
|
|
133
|
+
candidates.append(Path(__file__).resolve().parents[3] / env_dir_path)
|
|
134
|
+
candidates.append(Path(__file__).resolve().parent) # packaged wheel
|
|
135
|
+
candidates.append(Path(__file__).resolve().parents[3] / "target" / "release")
|
|
136
|
+
for directory in candidates:
|
|
137
|
+
for name in CDYLIB_NAMES:
|
|
138
|
+
p = directory / name
|
|
139
|
+
if p.is_file():
|
|
140
|
+
return p
|
|
141
|
+
raise LibraryNotFoundError(
|
|
142
|
+
"no pith-mp3 cdylib found (searched PITH_CDYLIB, PITH_CDYLIB_DIR, "
|
|
143
|
+
"the package directory and <repo>/target/release); "
|
|
144
|
+
"run `cargo build --release` first"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
_lib: ctypes.CDLL | None = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _load() -> ctypes.CDLL:
|
|
152
|
+
global _lib
|
|
153
|
+
if _lib is None:
|
|
154
|
+
lib = ctypes.CDLL(str(find_cdylib()))
|
|
155
|
+
lib.pith_mp3_decode.argtypes = [
|
|
156
|
+
ctypes.c_void_p, # data
|
|
157
|
+
ctypes.c_size_t, # len
|
|
158
|
+
ctypes.POINTER(ctypes.c_void_p), # out buffer
|
|
159
|
+
ctypes.POINTER(ctypes.c_size_t), # out length
|
|
160
|
+
]
|
|
161
|
+
lib.pith_mp3_decode.restype = ctypes.c_int32
|
|
162
|
+
lib.pith_mp3_free.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
|
163
|
+
lib.pith_mp3_free.restype = None
|
|
164
|
+
_lib = lib
|
|
165
|
+
return _lib
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def decode_canonical(data: bytes) -> bytes:
|
|
169
|
+
"""Decodes a complete MPEG audio stream into the canonical byte
|
|
170
|
+
stream the ``reference.json`` vectors are defined over.
|
|
171
|
+
|
|
172
|
+
Raises :class:`FfiError` with ``status == STATUS_REJECTED`` for any
|
|
173
|
+
malformed input — no sync word, bad header fields, CRC mismatch or
|
|
174
|
+
a truncated stream; the decoder never panics through this boundary.
|
|
175
|
+
"""
|
|
176
|
+
out = ctypes.c_void_p()
|
|
177
|
+
out_len = ctypes.c_size_t()
|
|
178
|
+
status = _load().pith_mp3_decode(data, len(data), ctypes.byref(out), ctypes.byref(out_len))
|
|
179
|
+
if status != STATUS_OK:
|
|
180
|
+
raise FfiError("pith_mp3_decode", status)
|
|
181
|
+
try:
|
|
182
|
+
return ctypes.string_at(out, out_len.value)
|
|
183
|
+
finally:
|
|
184
|
+
_load().pith_mp3_free(out, out_len.value)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def parse_canonical(raw: bytes) -> Canonical:
|
|
188
|
+
"""Re-expresses the canonical byte stream as a :class:`Canonical`."""
|
|
189
|
+
if len(raw) < HEADER_LEN:
|
|
190
|
+
raise ValueError("canonical stream is shorter than the 48-byte header")
|
|
191
|
+
|
|
192
|
+
def u32(offset: int) -> int:
|
|
193
|
+
return int.from_bytes(raw[offset : offset + 4], "big")
|
|
194
|
+
|
|
195
|
+
def u64(offset: int) -> int:
|
|
196
|
+
return int.from_bytes(raw[offset : offset + 8], "big")
|
|
197
|
+
|
|
198
|
+
return Canonical(
|
|
199
|
+
layer=u32(0),
|
|
200
|
+
bitrate_kbps=u32(4),
|
|
201
|
+
header_sample_rate=u32(8),
|
|
202
|
+
header_channels=u32(12),
|
|
203
|
+
channels=u32(16),
|
|
204
|
+
sample_rate=u32(20),
|
|
205
|
+
frames=u32(24),
|
|
206
|
+
samples=u64(28),
|
|
207
|
+
vbr=u32(36) == 1,
|
|
208
|
+
raw=raw,
|
|
209
|
+
)
|
pith_mp3/pith_mp3.dll
ADDED
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pith-mp3
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: MP3 decoding SDK: layers I/II/III including the bit reservoir via the pith-mp3 Rust cdylib (ctypes, zero-dep)
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/pith-hash/pith-mp3
|
|
7
|
+
Project-URL: Homepage, https://github.com/pith-hash/pith-mp3
|
|
8
|
+
Keywords: mp3,audio,decoder,mpeg,hashing
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Rust
|
|
11
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
12
|
+
Requires-Python: >=3.9
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pith_mp3/__init__.py,sha256=9pJd6cGfZgAHCoHmgaVHwqNNcwXh9-Q5zmjov3LiQ_Q,6904
|
|
2
|
+
pith_mp3/pith_mp3.dll,sha256=tzeqZIS59O67mm9cATSR-yYBWM2Tpg02LrWAjRvaFpI,221184
|
|
3
|
+
pith_mp3-0.1.1.dist-info/METADATA,sha256=X3IERL3JaWWKaZMBSAyCgr7Slie6UJdm4lqhZMNmxSE,531
|
|
4
|
+
pith_mp3-0.1.1.dist-info/WHEEL,sha256=Zx98gwb_dQKckJK3HEOVKnxxD52EfU_PXDG_usMJ2ng,98
|
|
5
|
+
pith_mp3-0.1.1.dist-info/top_level.txt,sha256=pMC_7NkpJ4WZY4t2MQta96jQcvKqEV64DmY0B2qUpMA,9
|
|
6
|
+
pith_mp3-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pith_mp3
|