@pith-hash/pith-flac 0.0.0 → 0.1.1-beta.1

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.
package/index.js CHANGED
@@ -1 +1,219 @@
1
- module.exports = {};
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 pith-hash
3
+ "use strict";
4
+
5
+ /**
6
+ * pith-flac SDK: FLAC decoding through koffi.
7
+ *
8
+ * Decodes a native FLAC stream (STREAMINFO, constant/verbatim
9
+ * subframes, fixed predictors of orders 0-4, both Rice methods) into
10
+ * the canonical byte stream the `reference.json` vectors are defined
11
+ * over: a 64-byte big-endian header followed by the decoded PCM,
12
+ * interleaved `i32` little-endian.
13
+ *
14
+ * The cdylib is located through the suite's discovery chain:
15
+ *
16
+ * 1. `PITH_CDYLIB` — an explicit cdylib *file* path;
17
+ * 2. `PITH_CDYLIB_DIR` — a *directory* scanned for the cdylib names
18
+ * (the CD pipeline points this at `target/release`);
19
+ * 3. `prebuilds/` — the packaged npm layout the CD publish job
20
+ * assembles, flat and per `<os-arch>` (e.g. `linux-x64`);
21
+ * 4. `<repo root>/target/release` — the repository working-tree
22
+ * layout, so a source checkout runs against a local cargo build
23
+ * with no configuration.
24
+ *
25
+ * The FFI surface is one decode operation plus one free:
26
+ * `pith_flac_decode` decodes a whole FLAC stream into the canonical
27
+ * byte stream, and `pith_flac_free` releases the handed-out buffer.
28
+ */
29
+
30
+ const koffi = require("koffi");
31
+ const fs = require("node:fs");
32
+ const path = require("node:path");
33
+
34
+ const STATUS_OK = 0;
35
+ const STATUS_INVALID = -1;
36
+ const STATUS_REJECTED = -2;
37
+
38
+ /** Refusal kind "bad-value" (Error::BadValue). */
39
+ const ERR_BAD_VALUE = 1;
40
+ /** Refusal kind "invalid-magic" (Error::InvalidMagic). */
41
+ const ERR_INVALID_MAGIC = 2;
42
+ /** Refusal kind "too-large" (Error::TooLarge). */
43
+ const ERR_TOO_LARGE = 3;
44
+ /** Refusal kind "truncated" (Error::Truncated). */
45
+ const ERR_TRUNCATED = 4;
46
+ /** Refusal kind "unsupported" (Error::Unsupported). */
47
+ const ERR_UNSUPPORTED = 5;
48
+
49
+ /** The canonical stream's header length in bytes. */
50
+ const HEADER_LEN = 64;
51
+
52
+ /** Refusal-kind names by code — the stable names reference.json
53
+ * `error_vectors` record as strings. */
54
+ const ERR_KIND_NAMES = Object.freeze({
55
+ [ERR_BAD_VALUE]: "bad-value",
56
+ [ERR_INVALID_MAGIC]: "invalid-magic",
57
+ [ERR_TOO_LARGE]: "too-large",
58
+ [ERR_TRUNCATED]: "truncated",
59
+ [ERR_UNSUPPORTED]: "unsupported",
60
+ });
61
+
62
+ /** Every cdylib file name cargo may drop into the build directory, per platform. */
63
+ const CDYLIB_NAMES = ["pith_flac.dll", "libpith_flac.so", "libpith_flac.dylib"];
64
+
65
+ const PKG_ROOT = path.join(__dirname);
66
+ const REPO_ROOT = path.resolve(__dirname, "..", "..");
67
+
68
+ /** FfiError: a non-zero status code came back from the cdylib. */
69
+ class FfiError extends Error {
70
+ /**
71
+ * @param {string} op the FFI operation name
72
+ * @param {number} status the raw status code
73
+ * @param {number} kind the refusal kind code (0 unless rejected)
74
+ */
75
+ constructor(op, status, kind = 0) {
76
+ let kinded = { [STATUS_INVALID]: "invalid argument", [STATUS_REJECTED]: "input rejected" }[status] ?? "unknown failure";
77
+ if (kind) kinded += ` (${ERR_KIND_NAMES[kind] ?? `kind ${kind}`})`;
78
+ super(`${op} failed: ${kinded} (status ${status})`);
79
+ this.name = "FfiError";
80
+ /** The raw status code the FFI returned. */
81
+ this.status = status;
82
+ /** The refusal kind code (0 unless status is STATUS_REJECTED). */
83
+ this.kind = kind;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Locates the cdylib through the suite's discovery chain.
89
+ * @returns {string} an absolute path to the cdylib file
90
+ * @throws {Error} when nothing is found
91
+ */
92
+ function findCdylib() {
93
+ const explicit = process.env.PITH_CDYLIB;
94
+ if (explicit && fs.statSync(explicit, { throwIfNoEntry: false })?.isFile()) {
95
+ return path.resolve(explicit);
96
+ }
97
+ /** @type {string[]} */
98
+ const dirs = [];
99
+ const envDir = process.env.PITH_CDYLIB_DIR;
100
+ if (envDir) {
101
+ dirs.push(envDir);
102
+ if (!path.isAbsolute(envDir)) {
103
+ dirs.push(path.join(REPO_ROOT, envDir));
104
+ }
105
+ }
106
+ const osArch = `${process.platform}-${process.arch}`;
107
+ dirs.push(path.join(PKG_ROOT, "prebuilds", osArch));
108
+ dirs.push(path.join(PKG_ROOT, "prebuilds"));
109
+ dirs.push(path.join(REPO_ROOT, "target", "release"));
110
+ for (const dir of dirs) {
111
+ for (const name of CDYLIB_NAMES) {
112
+ const p = path.join(dir, name);
113
+ if (fs.statSync(p, { throwIfNoEntry: false })?.isFile()) return p;
114
+ }
115
+ }
116
+ throw new Error(
117
+ "no pith-flac cdylib found (searched PITH_CDYLIB, PITH_CDYLIB_DIR, prebuilds/ and <repo>/target/release); " +
118
+ "run `cargo build --release` first",
119
+ );
120
+ }
121
+
122
+ let cached = undefined;
123
+
124
+ /**
125
+ * Loads the cdylib and binds the exported symbols (lazily, once).
126
+ * @returns {{decode: Function, free: Function}}
127
+ */
128
+ function loadLibrary() {
129
+ if (cached) return cached;
130
+ const lib = koffi.load(findCdylib());
131
+ const decode = lib.func("pith_flac_decode", "int32_t", [
132
+ "const uint8_t *",
133
+ "size_t",
134
+ koffi.out(koffi.pointer("void *")),
135
+ koffi.out(koffi.pointer("size_t")),
136
+ koffi.out(koffi.pointer("int32_t")),
137
+ ]);
138
+ const free = lib.func("void pith_flac_free(void *ptr, size_t len)");
139
+ cached = { decode, free };
140
+ return cached;
141
+ }
142
+
143
+ /**
144
+ * Decodes a complete FLAC stream into the canonical byte stream the
145
+ * `reference.json` vectors are defined over. The handed-out cdylib
146
+ * buffer is copied into a JS Buffer and released before returning.
147
+ *
148
+ * @param {Buffer} data the complete FLAC file bytes
149
+ * @returns {Buffer} the canonical stream (64-byte header + PCM)
150
+ * @throws {FfiError} with `status === -2` for any malformed input
151
+ */
152
+ function decodeCanonical(data) {
153
+ if (!Buffer.isBuffer(data)) {
154
+ throw new TypeError("data must be a Buffer");
155
+ }
156
+ const { decode, free } = loadLibrary();
157
+ const out = [null];
158
+ const outLen = [0];
159
+ const err = [0];
160
+ const status = decode(data, data.length, out, outLen, err);
161
+ if (status !== STATUS_OK) {
162
+ throw new FfiError("pith_flac_decode", status, err[0]);
163
+ }
164
+ try {
165
+ // koffi.decode hands back a Uint8Array view over the external
166
+ // buffer; copy it into a Buffer before the cdylib buffer is freed.
167
+ return Buffer.from(koffi.decode(out[0], "uint8_t", Number(outLen[0])));
168
+ } finally {
169
+ free(out[0], Number(outLen[0]));
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Re-expresses the canonical byte stream as a plain object.
175
+ *
176
+ * @param {Buffer} raw the canonical stream
177
+ * @returns {{siMinBlockSize: number, siMaxBlockSize: number, siSampleRate: number,
178
+ * siChannels: number, siBitsPerSample: number, siTotalSamples: number,
179
+ * sampleRate: number, channels: number, bitsPerSample: number,
180
+ * totalSamples: number, decodedSamples: number, pcm: Buffer, raw: Buffer}}
181
+ */
182
+ function parseCanonical(raw) {
183
+ if (!Buffer.isBuffer(raw) || raw.length < HEADER_LEN) {
184
+ throw new TypeError("canonical stream is shorter than the 64-byte header");
185
+ }
186
+ return {
187
+ siMinBlockSize: raw.readUInt32BE(0),
188
+ siMaxBlockSize: raw.readUInt32BE(4),
189
+ siSampleRate: raw.readUInt32BE(8),
190
+ siChannels: raw.readUInt32BE(12),
191
+ siBitsPerSample: raw.readUInt32BE(16),
192
+ siTotalSamples: Number(raw.readBigUInt64BE(20)),
193
+ sampleRate: raw.readUInt32BE(28),
194
+ channels: raw.readUInt32BE(32),
195
+ bitsPerSample: raw.readUInt32BE(36),
196
+ totalSamples: Number(raw.readBigUInt64BE(40)),
197
+ decodedSamples: Number(raw.readBigUInt64BE(48)),
198
+ pcm: raw.subarray(HEADER_LEN),
199
+ raw,
200
+ };
201
+ }
202
+
203
+ module.exports = {
204
+ STATUS_OK,
205
+ STATUS_INVALID,
206
+ STATUS_REJECTED,
207
+ ERR_BAD_VALUE,
208
+ ERR_INVALID_MAGIC,
209
+ ERR_TOO_LARGE,
210
+ ERR_TRUNCATED,
211
+ ERR_UNSUPPORTED,
212
+ HEADER_LEN,
213
+ ERR_KIND_NAMES,
214
+ CDYLIB_NAMES,
215
+ FfiError,
216
+ findCdylib,
217
+ decodeCanonical,
218
+ parseCanonical,
219
+ };
package/package.json CHANGED
@@ -1,14 +1,33 @@
1
- {
2
- "name": "@pith-hash/pith-flac",
3
- "version": "0.0.0",
4
- "description": "pith SDK placeholder - real artifacts published by CI",
5
- "license": "MIT",
6
- "private": false,
7
- "publishConfig": {
8
- "access": "public"
9
- },
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://github.com/pith-hash/pith-flac.git"
13
- }
14
- }
1
+ {
2
+ "name": "@pith-hash/pith-flac",
3
+ "version": "0.1.1-beta.1",
4
+ "description": "FLAC decoding SDK: STREAMINFO, constant/verbatim/fixed subframes and both Rice methods via the pith-flac Rust cdylib (koffi, prebuilt)",
5
+ "license": "MIT",
6
+ "main": "index.js",
7
+ "files": [
8
+ "index.js",
9
+ "prebuilds/",
10
+ "reference.json"
11
+ ],
12
+ "scripts": {
13
+ "test": "node --test"
14
+ },
15
+ "dependencies": {
16
+ "koffi": "^2.12.0"
17
+ },
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/pith-hash/pith-flac.git",
24
+ "directory": "sdk/node"
25
+ },
26
+ "keywords": [
27
+ "flac",
28
+ "audio",
29
+ "decoder",
30
+ "pcm",
31
+ "hashing"
32
+ ]
33
+ }
package/reference.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "suite": "pith",
3
+ "crate": "pith-flac",
4
+ "format_version": 1,
5
+ "generator": "cargo run --bin gen-reference -- gen",
6
+ "vectors": [
7
+ {
8
+ "name": "fixture-tone",
9
+ "input_kind": "fixture-file",
10
+ "input_path": "tests/fixtures/tone.flac",
11
+ "input_sha256": "f954b800b5476f06529b365ed429da96fbf9961067073e955c1966d5fa098d9e",
12
+ "streaminfo_min_block_size": 16,
13
+ "streaminfo_max_block_size": 65535,
14
+ "streaminfo_sample_rate": 44100,
15
+ "streaminfo_channels": 1,
16
+ "streaminfo_bits_per_sample": 16,
17
+ "streaminfo_total_samples": 44100,
18
+ "sample_rate": 44100,
19
+ "channels": 1,
20
+ "bits_per_sample": 16,
21
+ "total_samples": 44100,
22
+ "decoded_samples": 44100,
23
+ "pcm_i32_le_sha256": "2514eff527051953533d999b57bc3745386432e2a37dcc32ccab77fb67faee6f",
24
+ "pcm_i32_le_fnv1a64": "3a1160e77c61f2b9"
25
+ },
26
+ {
27
+ "name": "constant-mono-16bit",
28
+ "input_kind": "inline-hex",
29
+ "input_hex": "664c61438000002200c012000000000000000ac440f00000000800000000000000000000000000000000fff860000007ff001234fd6f",
30
+ "sample_rate": 44100,
31
+ "channels": 1,
32
+ "bits_per_sample": 16,
33
+ "total_samples": 8,
34
+ "decoded_samples": 8,
35
+ "samples_i32": [4660, 4660, 4660, 4660, 4660, 4660, 4660, 4660],
36
+ "pcm_i32_le_sha256": "8949c8b20154a8ad94f90be09da09819eb3f6e4327b25df4ff0b28b0d8e9295c",
37
+ "pcm_i32_le_fnv1a64": "9e2315414d536a25"
38
+ },
39
+ {
40
+ "name": "verbatim-mono-signs",
41
+ "input_kind": "inline-hex",
42
+ "input_hex": "664c61438000002200c012000000000000000ac440f00000000800000000000000000000000000000000fff860000007ff020001fffe0003fffc0005fffa0007fff8af0b",
43
+ "sample_rate": 44100,
44
+ "channels": 1,
45
+ "bits_per_sample": 16,
46
+ "total_samples": 8,
47
+ "decoded_samples": 8,
48
+ "samples_i32": [1, -2, 3, -4, 5, -6, 7, -8],
49
+ "pcm_i32_le_sha256": "b646e9e2e7d9d9cdb8963241f30ef5b14db614b51f24cc9bbfbb8d6773ae4e60",
50
+ "pcm_i32_le_fnv1a64": "209cbf97ac330015"
51
+ },
52
+ {
53
+ "name": "fixed1-rice4-partitioned",
54
+ "input_kind": "inline-hex",
55
+ "input_hex": "664c61438000002200c012000000000000000ac440f00000000800000000000000000000000000000000fff860000007ff1201f40500000000000000000000000000000003e00000000000000000000000010300000000000100000000008000000000400000000020cd7a",
56
+ "sample_rate": 44100,
57
+ "channels": 1,
58
+ "bits_per_sample": 16,
59
+ "total_samples": 8,
60
+ "decoded_samples": 8,
61
+ "samples_i32": [500, -500, 300, 320, 340, 360, 380, 400],
62
+ "pcm_i32_le_sha256": "4224637b975f1bc8ecc4673122c35fa5c6b01976ac175627c1e1b37a0e68ea39",
63
+ "pcm_i32_le_fnv1a64": "57959265f7dfb14a"
64
+ },
65
+ {
66
+ "name": "fixed0-rice-escape",
67
+ "input_kind": "inline-hex",
68
+ "input_hex": "664c61438000002200c012000000000000000ac440f00000000800000000000000000000000000000000fff860000003e31007cb17848023fd",
69
+ "sample_rate": 44100,
70
+ "channels": 1,
71
+ "bits_per_sample": 16,
72
+ "total_samples": 8,
73
+ "decoded_samples": 4,
74
+ "samples_i32": [-15, 15, 0, 1],
75
+ "pcm_i32_le_sha256": "edc39f894f69e2678320dfe2884634e7d59ebee9324ad4d5a3ad4889f14c80a2",
76
+ "pcm_i32_le_fnv1a64": "8f8d061ed1f929c9"
77
+ },
78
+ {
79
+ "name": "stereo-left-side",
80
+ "input_kind": "inline-hex",
81
+ "input_hex": "664c61438000002200c012000000000000000ac442f00000000400000000000000000000000000000000fff860800003e8020064ff9c0064ff9c02001e7fdd00119ffc4038c7",
82
+ "sample_rate": 44100,
83
+ "channels": 2,
84
+ "bits_per_sample": 16,
85
+ "total_samples": 4,
86
+ "decoded_samples": 8,
87
+ "samples_i32": [100, 40, -100, 40, 100, -40, -100, -40],
88
+ "pcm_i32_le_sha256": "ad4f6b88c82175e563cf7b84e4b7ba62e0d3ab97bd8447011c2ea4817c085d9f",
89
+ "pcm_i32_le_fnv1a64": "ff5258122066d21d"
90
+ }
91
+ ],
92
+ "error_vectors": [
93
+ {
94
+ "name": "not-a-flac-stream",
95
+ "input_kind": "inline-hex",
96
+ "input_hex": "6e6f7065",
97
+ "error": "invalid-magic"
98
+ },
99
+ {
100
+ "name": "headerless-stream",
101
+ "input_kind": "inline-hex",
102
+ "input_hex": "664c6143",
103
+ "error": "truncated"
104
+ }
105
+ ]
106
+ }
package/README.md DELETED
@@ -1,2 +0,0 @@
1
- # @pith-hash/pith-flac
2
- Placeholder - real SDK publishes via CI trusted publishing.