pith-flac 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_flac/__init__.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 pith-hash
|
|
3
|
+
"""pith-flac SDK: FLAC decoding through ctypes.
|
|
4
|
+
|
|
5
|
+
Decodes a native FLAC stream (STREAMINFO, constant/verbatim subframes,
|
|
6
|
+
fixed predictors of orders 0-4, both Rice methods) through the Rust
|
|
7
|
+
cdylib, handing back the canonical byte stream the ``reference.json``
|
|
8
|
+
vectors are defined over: a 64-byte big-endian header followed by the
|
|
9
|
+
decoded PCM, interleaved ``i32`` little-endian.
|
|
10
|
+
|
|
11
|
+
The cdylib is located through the suite's discovery chain:
|
|
12
|
+
|
|
13
|
+
1. ``PITH_CDYLIB`` — an explicit cdylib *file* path;
|
|
14
|
+
2. ``PITH_CDYLIB_DIR`` — a *directory* scanned for the cdylib names
|
|
15
|
+
(the CD pipeline points this at ``target/release``);
|
|
16
|
+
3. the package directory itself (the built wheel ships the cdylib as
|
|
17
|
+
package data);
|
|
18
|
+
4. ``<repo root>/target/release`` — the repository working-tree layout,
|
|
19
|
+
so a source checkout runs against a local cargo build with no
|
|
20
|
+
configuration.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import ctypes
|
|
26
|
+
import os
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Canonical",
|
|
32
|
+
"FfiError",
|
|
33
|
+
"LibraryNotFoundError",
|
|
34
|
+
"find_cdylib",
|
|
35
|
+
"decode_canonical",
|
|
36
|
+
"parse_canonical",
|
|
37
|
+
"STATUS_OK",
|
|
38
|
+
"STATUS_INVALID",
|
|
39
|
+
"STATUS_REJECTED",
|
|
40
|
+
"ERR_BAD_VALUE",
|
|
41
|
+
"ERR_INVALID_MAGIC",
|
|
42
|
+
"ERR_TOO_LARGE",
|
|
43
|
+
"ERR_TRUNCATED",
|
|
44
|
+
"ERR_UNSUPPORTED",
|
|
45
|
+
"HEADER_LEN",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
#: Status: success.
|
|
49
|
+
STATUS_OK = 0
|
|
50
|
+
#: Status: a caller argument is invalid (a null pointer).
|
|
51
|
+
STATUS_INVALID = -1
|
|
52
|
+
#: Status: the core decoder refused the input (malformed FLAC).
|
|
53
|
+
STATUS_REJECTED = -2
|
|
54
|
+
|
|
55
|
+
#: Refusal kind ``"bad-value"`` (``Error::BadValue``).
|
|
56
|
+
ERR_BAD_VALUE = 1
|
|
57
|
+
#: Refusal kind ``"invalid-magic"`` (``Error::InvalidMagic``).
|
|
58
|
+
ERR_INVALID_MAGIC = 2
|
|
59
|
+
#: Refusal kind ``"too-large"`` (``Error::TooLarge``).
|
|
60
|
+
ERR_TOO_LARGE = 3
|
|
61
|
+
#: Refusal kind ``"truncated"`` (``Error::Truncated``).
|
|
62
|
+
ERR_TRUNCATED = 4
|
|
63
|
+
#: Refusal kind ``"unsupported"`` (``Error::Unsupported``).
|
|
64
|
+
ERR_UNSUPPORTED = 5
|
|
65
|
+
|
|
66
|
+
#: The canonical stream's header length in bytes.
|
|
67
|
+
HEADER_LEN = 64
|
|
68
|
+
|
|
69
|
+
#: Every cdylib file name cargo may drop into the build directory, per
|
|
70
|
+
#: platform (windows / linux / macOS).
|
|
71
|
+
CDYLIB_NAMES = ("pith_flac.dll", "libpith_flac.so", "libpith_flac.dylib")
|
|
72
|
+
|
|
73
|
+
#: Refusal-kind names by code — the same stable names the
|
|
74
|
+
#: ``error_vectors`` of ``reference.json`` record as strings.
|
|
75
|
+
ERR_KIND_NAMES = {
|
|
76
|
+
ERR_BAD_VALUE: "bad-value",
|
|
77
|
+
ERR_INVALID_MAGIC: "invalid-magic",
|
|
78
|
+
ERR_TOO_LARGE: "too-large",
|
|
79
|
+
ERR_TRUNCATED: "truncated",
|
|
80
|
+
ERR_UNSUPPORTED: "unsupported",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class Canonical:
|
|
86
|
+
"""A decoded FLAC, re-expressed from the canonical byte stream.
|
|
87
|
+
|
|
88
|
+
``pcm`` is the interleaved ``i32`` little-endian sample buffer —
|
|
89
|
+
exactly the bytes the ``pcm_i32_le_sha256`` digest covers.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
#: STREAMINFO: smallest block size (samples per channel).
|
|
93
|
+
si_min_block_size: int
|
|
94
|
+
#: STREAMINFO: largest block size (samples per channel).
|
|
95
|
+
si_max_block_size: int
|
|
96
|
+
#: STREAMINFO: sample rate in Hz (0 when the frames carry it).
|
|
97
|
+
si_sample_rate: int
|
|
98
|
+
#: STREAMINFO: channel count, 1-8.
|
|
99
|
+
si_channels: int
|
|
100
|
+
#: STREAMINFO: bits per sample, 4-32.
|
|
101
|
+
si_bits_per_sample: int
|
|
102
|
+
#: STREAMINFO: declared total samples per channel (0 = unknown).
|
|
103
|
+
si_total_samples: int
|
|
104
|
+
#: Decoded sample rate in Hz (resolved from frames when STREAMINFO
|
|
105
|
+
#: said 0).
|
|
106
|
+
sample_rate: int
|
|
107
|
+
#: Decoded channel count, 1-8.
|
|
108
|
+
channels: int
|
|
109
|
+
#: Decoded bits per sample.
|
|
110
|
+
bits_per_sample: int
|
|
111
|
+
#: Declared total samples per channel, 0 when unknown.
|
|
112
|
+
total_samples: int
|
|
113
|
+
#: Decoded sample count, interleaved across channels.
|
|
114
|
+
decoded_samples: int
|
|
115
|
+
#: The canonical byte stream the digest is computed over.
|
|
116
|
+
raw: bytes
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def pcm(self) -> bytes:
|
|
120
|
+
"""The interleaved ``i32`` little-endian PCM (everything after
|
|
121
|
+
the 64-byte header)."""
|
|
122
|
+
return self.raw[HEADER_LEN:]
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def samples_i32(self) -> list[int]:
|
|
126
|
+
"""The PCM as Python ints, in stream order."""
|
|
127
|
+
return list(
|
|
128
|
+
int.from_bytes(self.raw[HEADER_LEN + i : HEADER_LEN + i + 4], "little", signed=True)
|
|
129
|
+
for i in range(0, len(self.raw) - HEADER_LEN, 4)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class LibraryNotFoundError(OSError):
|
|
134
|
+
"""No cdylib was found through the discovery chain."""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class FfiError(Exception):
|
|
138
|
+
"""A non-zero status code came back from the cdylib."""
|
|
139
|
+
|
|
140
|
+
def __init__(self, op: str, status: int, kind: int = 0) -> None:
|
|
141
|
+
detail = {
|
|
142
|
+
STATUS_INVALID: "invalid argument",
|
|
143
|
+
STATUS_REJECTED: "input rejected",
|
|
144
|
+
}.get(status, "unknown failure")
|
|
145
|
+
if kind:
|
|
146
|
+
detail = f"{detail} ({ERR_KIND_NAMES.get(kind, f'kind {kind}')})"
|
|
147
|
+
super().__init__(f"{op} failed: {detail} (status {status})")
|
|
148
|
+
#: The raw status code the FFI returned.
|
|
149
|
+
self.status = status
|
|
150
|
+
#: The refusal kind code (0 unless the status is
|
|
151
|
+
#: :data:`STATUS_REJECTED`; one of the ``ERR_*`` constants).
|
|
152
|
+
self.kind = kind
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def find_cdylib() -> Path:
|
|
156
|
+
"""Locates the cdylib through the suite's discovery chain."""
|
|
157
|
+
explicit = os.environ.get("PITH_CDYLIB")
|
|
158
|
+
if explicit:
|
|
159
|
+
p = Path(explicit)
|
|
160
|
+
if p.is_file():
|
|
161
|
+
return p
|
|
162
|
+
env_dir = os.environ.get("PITH_CDYLIB_DIR")
|
|
163
|
+
candidates: list[Path] = []
|
|
164
|
+
if env_dir:
|
|
165
|
+
env_dir_path = Path(env_dir)
|
|
166
|
+
candidates.append(env_dir_path)
|
|
167
|
+
if not env_dir_path.is_absolute():
|
|
168
|
+
# CD and local runs invoke tools from the repository root or
|
|
169
|
+
# from sdk/<lang>; resolve the env value against both.
|
|
170
|
+
candidates.append(Path.cwd() / env_dir_path)
|
|
171
|
+
candidates.append(Path(__file__).resolve().parents[3] / env_dir_path)
|
|
172
|
+
candidates.append(Path(__file__).resolve().parent) # packaged wheel
|
|
173
|
+
candidates.append(Path(__file__).resolve().parents[3] / "target" / "release")
|
|
174
|
+
for directory in candidates:
|
|
175
|
+
for name in CDYLIB_NAMES:
|
|
176
|
+
p = directory / name
|
|
177
|
+
if p.is_file():
|
|
178
|
+
return p
|
|
179
|
+
raise LibraryNotFoundError(
|
|
180
|
+
"no pith-flac cdylib found (searched PITH_CDYLIB, PITH_CDYLIB_DIR, "
|
|
181
|
+
"the package directory and <repo>/target/release); "
|
|
182
|
+
"run `cargo build --release` first"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
_lib: ctypes.CDLL | None = None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _load() -> ctypes.CDLL:
|
|
190
|
+
global _lib
|
|
191
|
+
if _lib is None:
|
|
192
|
+
lib = ctypes.CDLL(str(find_cdylib()))
|
|
193
|
+
lib.pith_flac_decode.argtypes = [
|
|
194
|
+
ctypes.c_void_p, # data
|
|
195
|
+
ctypes.c_size_t, # len
|
|
196
|
+
ctypes.POINTER(ctypes.c_void_p), # out buffer
|
|
197
|
+
ctypes.POINTER(ctypes.c_size_t), # out length
|
|
198
|
+
ctypes.POINTER(ctypes.c_int32), # out refusal kind
|
|
199
|
+
]
|
|
200
|
+
lib.pith_flac_decode.restype = ctypes.c_int32
|
|
201
|
+
lib.pith_flac_free.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
|
202
|
+
lib.pith_flac_free.restype = None
|
|
203
|
+
_lib = lib
|
|
204
|
+
return _lib
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def decode_canonical(data: bytes) -> bytes:
|
|
208
|
+
"""Decodes a complete FLAC stream into the canonical byte stream
|
|
209
|
+
the ``reference.json`` vectors are defined over.
|
|
210
|
+
|
|
211
|
+
Raises :class:`FfiError` with ``status == STATUS_REJECTED`` for any
|
|
212
|
+
malformed input; ``err.kind`` then carries one of the ``ERR_*``
|
|
213
|
+
kind codes the reference.json error vectors pin. The decoder never
|
|
214
|
+
panics through this boundary.
|
|
215
|
+
"""
|
|
216
|
+
out = ctypes.c_void_p()
|
|
217
|
+
out_len = ctypes.c_size_t()
|
|
218
|
+
err = ctypes.c_int32()
|
|
219
|
+
status = _load().pith_flac_decode(
|
|
220
|
+
data, len(data), ctypes.byref(out), ctypes.byref(out_len), ctypes.byref(err)
|
|
221
|
+
)
|
|
222
|
+
if status != STATUS_OK:
|
|
223
|
+
raise FfiError("pith_flac_decode", status, err.value)
|
|
224
|
+
try:
|
|
225
|
+
return ctypes.string_at(out, out_len.value)
|
|
226
|
+
finally:
|
|
227
|
+
_load().pith_flac_free(out, out_len.value)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def parse_canonical(raw: bytes) -> Canonical:
|
|
231
|
+
"""Re-expresses the canonical byte stream as a :class:`Canonical`."""
|
|
232
|
+
if len(raw) < HEADER_LEN:
|
|
233
|
+
raise ValueError("canonical stream is shorter than the 64-byte header")
|
|
234
|
+
|
|
235
|
+
def u32(offset: int) -> int:
|
|
236
|
+
return int.from_bytes(raw[offset : offset + 4], "big")
|
|
237
|
+
|
|
238
|
+
def u64(offset: int) -> int:
|
|
239
|
+
return int.from_bytes(raw[offset : offset + 8], "big")
|
|
240
|
+
|
|
241
|
+
return Canonical(
|
|
242
|
+
si_min_block_size=u32(0),
|
|
243
|
+
si_max_block_size=u32(4),
|
|
244
|
+
si_sample_rate=u32(8),
|
|
245
|
+
si_channels=u32(12),
|
|
246
|
+
si_bits_per_sample=u32(16),
|
|
247
|
+
si_total_samples=u64(20),
|
|
248
|
+
sample_rate=u32(28),
|
|
249
|
+
channels=u32(32),
|
|
250
|
+
bits_per_sample=u32(36),
|
|
251
|
+
total_samples=u64(40),
|
|
252
|
+
decoded_samples=u64(48),
|
|
253
|
+
raw=raw,
|
|
254
|
+
)
|
pith_flac/pith_flac.dll
ADDED
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pith-flac
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: FLAC decoding SDK: STREAMINFO, constant/verbatim/fixed subframes and both Rice methods via the pith-flac Rust cdylib (ctypes, zero-dep)
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/pith-hash/pith-flac
|
|
7
|
+
Project-URL: Homepage, https://github.com/pith-hash/pith-flac
|
|
8
|
+
Keywords: flac,audio,decoder,pcm,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_flac/__init__.py,sha256=Wfb_-U1mF3h_Y_51tHgYNPE0mSOT2I3_gNS2bdvBom4,8614
|
|
2
|
+
pith_flac/pith_flac.dll,sha256=RBmj97bGiofgvfEpjE-T3tijX6gGvPu4J-um4titPNQ,142848
|
|
3
|
+
pith_flac-0.1.1.dist-info/METADATA,sha256=yL3-duMNAAjzbgLNv5VuEgnuzSVTrKAkRMUK-EW9FbA,560
|
|
4
|
+
pith_flac-0.1.1.dist-info/WHEEL,sha256=Zx98gwb_dQKckJK3HEOVKnxxD52EfU_PXDG_usMJ2ng,98
|
|
5
|
+
pith_flac-0.1.1.dist-info/top_level.txt,sha256=ridU2u2GOrCwadDDNceMhmXZItsJ4SJz6kOOtDA2d1w,10
|
|
6
|
+
pith_flac-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pith_flac
|