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
+ )
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1 @@
1
+ pith_flac