rockbox-ffi 0.1.0
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/README.md +243 -0
- package/dist/api.d.ts +64 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/bun.d.ts +52 -0
- package/dist/bun.d.ts.map +1 -0
- package/dist/bun.js +96 -0
- package/dist/bun.js.map +10 -0
- package/dist/chunk-ewj9g332.js +99 -0
- package/dist/chunk-ewj9g332.js.map +10 -0
- package/dist/chunk-raaa0pkf.js +215 -0
- package/dist/chunk-raaa0pkf.js.map +11 -0
- package/dist/deno.d.ts +52 -0
- package/dist/deno.d.ts.map +1 -0
- package/dist/deno.js +90 -0
- package/dist/deno.js.map +10 -0
- package/dist/enums.d.ts +35 -0
- package/dist/enums.d.ts.map +1 -0
- package/dist/ffi.d.ts +37 -0
- package/dist/ffi.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +10 -0
- package/dist/node.d.ts +52 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +88 -0
- package/dist/node.js.map +10 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +50 -0
- package/src/api.ts +221 -0
- package/src/bun.ts +95 -0
- package/src/deno.ts +83 -0
- package/src/enums.ts +44 -0
- package/src/ffi.ts +144 -0
- package/src/index.ts +37 -0
- package/src/node.ts +87 -0
- package/src/types.ts +80 -0
package/dist/node.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SPEC,
|
|
3
|
+
resolveLibPath
|
|
4
|
+
} from "./chunk-ewj9g332.js";
|
|
5
|
+
import {
|
|
6
|
+
ChannelConfig,
|
|
7
|
+
CrossfadeMode,
|
|
8
|
+
DspReplayGainMode,
|
|
9
|
+
MixMode,
|
|
10
|
+
ReplayGainMode,
|
|
11
|
+
makeApi,
|
|
12
|
+
sineStereo
|
|
13
|
+
} from "./chunk-raaa0pkf.js";
|
|
14
|
+
|
|
15
|
+
// src/node.ts
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { createRequire } from "node:module";
|
|
20
|
+
var require2 = createRequire(import.meta.url);
|
|
21
|
+
var koffi = require2("koffi");
|
|
22
|
+
var TOK = {
|
|
23
|
+
void: "void",
|
|
24
|
+
ptr: "void *",
|
|
25
|
+
u32: "uint32",
|
|
26
|
+
i32: "int32",
|
|
27
|
+
u64: "uint64",
|
|
28
|
+
usize: "size_t",
|
|
29
|
+
f32: "float",
|
|
30
|
+
bool: "bool",
|
|
31
|
+
strbuf: "str",
|
|
32
|
+
i16in: "void *",
|
|
33
|
+
outsize: "void *"
|
|
34
|
+
};
|
|
35
|
+
function makeRaw() {
|
|
36
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const libPath = resolveLibPath(here, (k) => process.env[k], existsSync, join, dirname);
|
|
38
|
+
const lib = koffi.load(libPath);
|
|
39
|
+
const sym = {};
|
|
40
|
+
for (const [name, spec] of Object.entries(SPEC)) {
|
|
41
|
+
sym[name] = lib.func(name, TOK[spec.ret], spec.args.map((t) => TOK[t]));
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
sym,
|
|
45
|
+
cstr(s) {
|
|
46
|
+
return s;
|
|
47
|
+
},
|
|
48
|
+
i16in(a) {
|
|
49
|
+
return Buffer.from(a.buffer, a.byteOffset, a.byteLength);
|
|
50
|
+
},
|
|
51
|
+
sizeOut() {
|
|
52
|
+
const buf = Buffer.alloc(8);
|
|
53
|
+
return { arg: buf, value: () => Number(buf.readBigUInt64LE(0)) };
|
|
54
|
+
},
|
|
55
|
+
takeString(p) {
|
|
56
|
+
if (p === null || p === undefined)
|
|
57
|
+
return null;
|
|
58
|
+
const str = koffi.decode(p, "char", -1);
|
|
59
|
+
sym.rb_string_free(p);
|
|
60
|
+
return str;
|
|
61
|
+
},
|
|
62
|
+
takeI16(p, len) {
|
|
63
|
+
const arr = koffi.decode(p, koffi.array("int16_t", len));
|
|
64
|
+
const copy = Int16Array.from(arr);
|
|
65
|
+
sym.rb_buffer_free(p, len);
|
|
66
|
+
return copy;
|
|
67
|
+
},
|
|
68
|
+
isNull(p) {
|
|
69
|
+
return p === null || p === undefined;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
var api = makeApi(makeRaw());
|
|
74
|
+
var { abiVersion, metadata, Dsp, Player } = api;
|
|
75
|
+
export {
|
|
76
|
+
sineStereo,
|
|
77
|
+
metadata,
|
|
78
|
+
abiVersion,
|
|
79
|
+
ReplayGainMode,
|
|
80
|
+
Player,
|
|
81
|
+
MixMode,
|
|
82
|
+
DspReplayGainMode,
|
|
83
|
+
Dsp,
|
|
84
|
+
CrossfadeMode,
|
|
85
|
+
ChannelConfig
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
//# debugId=C34E81133977033C64756E2164756E21
|
package/dist/node.js.map
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/node.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"// Node.js backend + full API, using koffi for FFI. Import under Node:\n// import { Dsp, Player, metadata } from \"rockbox-ffi/node\";\n//\n// Node has no built-in FFI, so this backend depends on `koffi`\n// (https://koffi.dev) — install it with `npm install koffi`.\n\nimport { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createRequire } from \"node:module\";\n\nimport { makeApi, sineStereo } from \"./api.ts\";\nimport { resolveLibPath, SPEC, type Raw, type Tok } from \"./ffi.ts\";\n\n// koffi ships as CommonJS; load it via require to keep ESM interop simple.\nconst require = createRequire(import.meta.url);\n// deno-lint-ignore no-explicit-any\nconst koffi: any = require(\"koffi\");\n\nconst TOK: Record<Tok, string> = {\n void: \"void\",\n ptr: \"void *\",\n u32: \"uint32\",\n i32: \"int32\",\n u64: \"uint64\",\n usize: \"size_t\",\n f32: \"float\",\n bool: \"bool\",\n strbuf: \"str\", // koffi marshals a JS string to a NUL-terminated char*\n i16in: \"void *\", // we pass a Buffer view of the Int16Array\n outsize: \"void *\", // we pass a Buffer.alloc(8) and read it back\n};\n\nfunction makeRaw(): Raw {\n const here = dirname(fileURLToPath(import.meta.url));\n const libPath = resolveLibPath(\n here,\n (k) => process.env[k],\n existsSync,\n join,\n dirname,\n );\n\n const lib = koffi.load(libPath);\n const sym: Record<string, (...a: any[]) => any> = {};\n for (const [name, spec] of Object.entries(SPEC)) {\n sym[name] = lib.func(name, TOK[spec.ret], spec.args.map((t) => TOK[t]));\n }\n\n return {\n sym,\n cstr(s: string) {\n // The `strbuf` params are typed \"str\"; koffi marshals the JS string.\n return s;\n },\n i16in(a: Int16Array) {\n return Buffer.from(a.buffer, a.byteOffset, a.byteLength);\n },\n sizeOut() {\n const buf = Buffer.alloc(8);\n return { arg: buf, value: () => Number(buf.readBigUInt64LE(0)) };\n },\n takeString(p: unknown) {\n if (p === null || p === undefined) return null;\n // Read a NUL-terminated char* of unknown length. (koffi's \"str\" type\n // is unreliable for longer strings in 2.16 — this form is robust.)\n const str = koffi.decode(p, \"char\", -1) as string;\n sym.rb_string_free(p);\n return str;\n },\n takeI16(p: unknown, len: number) {\n const arr = koffi.decode(p, koffi.array(\"int16_t\", len)) as number[];\n const copy = Int16Array.from(arr);\n sym.rb_buffer_free(p, len);\n return copy;\n },\n isNull(p: unknown) {\n return p === null || p === undefined;\n },\n };\n}\n\nconst api = makeApi(makeRaw());\nexport const { abiVersion, metadata, Dsp, Player } = api;\nexport { sineStereo };\nexport * from \"./enums.ts\";\nexport type { Metadata, PlayerConfig, PlayerStatus } from \"./types.ts\";\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;AAMA;AACA;AACA;AACA;AAMA,IAAM,WAAU,cAAc,YAAY,GAAG;AAE7C,IAAM,QAAa,SAAQ,OAAO;AAElC,IAAM,MAA2B;AAAA,EAC/B,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,OAAO;AAAA,EACP,KAAK;AAAA,EACL,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,SAAS;AACX;AAEA,SAAS,OAAO,GAAQ;AAAA,EACtB,MAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AAAA,EACnD,MAAM,UAAU,eACd,MACA,CAAC,MAAM,QAAQ,IAAI,IACnB,YACA,MACA,OACF;AAAA,EAEA,MAAM,MAAM,MAAM,KAAK,OAAO;AAAA,EAC9B,MAAM,MAA4C,CAAC;AAAA,EACnD,YAAY,MAAM,SAAS,OAAO,QAAQ,IAAI,GAAG;AAAA,IAC/C,IAAI,QAAQ,IAAI,KAAK,MAAM,IAAI,KAAK,MAAM,KAAK,KAAK,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;AAAA,EACxE;AAAA,EAEA,OAAO;AAAA,IACL;AAAA,IACA,IAAI,CAAC,GAAW;AAAA,MAEd,OAAO;AAAA;AAAA,IAET,KAAK,CAAC,GAAe;AAAA,MACnB,OAAO,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU;AAAA;AAAA,IAEzD,OAAO,GAAG;AAAA,MACR,MAAM,MAAM,OAAO,MAAM,CAAC;AAAA,MAC1B,OAAO,EAAE,KAAK,KAAK,OAAO,MAAM,OAAO,IAAI,gBAAgB,CAAC,CAAC,EAAE;AAAA;AAAA,IAEjE,UAAU,CAAC,GAAY;AAAA,MACrB,IAAI,MAAM,QAAQ,MAAM;AAAA,QAAW,OAAO;AAAA,MAG1C,MAAM,MAAM,MAAM,OAAO,GAAG,QAAQ,EAAE;AAAA,MACtC,IAAI,eAAe,CAAC;AAAA,MACpB,OAAO;AAAA;AAAA,IAET,OAAO,CAAC,GAAY,KAAa;AAAA,MAC/B,MAAM,MAAM,MAAM,OAAO,GAAG,MAAM,MAAM,WAAW,GAAG,CAAC;AAAA,MACvD,MAAM,OAAO,WAAW,KAAK,GAAG;AAAA,MAChC,IAAI,eAAe,GAAG,GAAG;AAAA,MACzB,OAAO;AAAA;AAAA,IAET,MAAM,CAAC,GAAY;AAAA,MACjB,OAAO,MAAM,QAAQ,MAAM;AAAA;AAAA,EAE/B;AAAA;AAGF,IAAM,MAAM,QAAQ,QAAQ,CAAC;AACtB,MAAQ,YAAY,UAAU,KAAK,WAAW;",
|
|
8
|
+
"debugId": "C34E81133977033C64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface ReplayGain {
|
|
2
|
+
track_gain_db: number | null;
|
|
3
|
+
album_gain_db: number | null;
|
|
4
|
+
track_peak: number | null;
|
|
5
|
+
album_peak: number | null;
|
|
6
|
+
raw_track_gain: number;
|
|
7
|
+
raw_album_gain: number;
|
|
8
|
+
raw_track_peak: number;
|
|
9
|
+
raw_album_peak: number;
|
|
10
|
+
}
|
|
11
|
+
export interface AlbumArt {
|
|
12
|
+
kind: "bmp" | "png" | "jpeg" | "unknown";
|
|
13
|
+
offset: number;
|
|
14
|
+
size: number;
|
|
15
|
+
id3_unsync: boolean;
|
|
16
|
+
vorbis_base64: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface Cuesheet {
|
|
19
|
+
offset: number;
|
|
20
|
+
size: number;
|
|
21
|
+
encoding: "latin1" | "utf8" | "utf16le" | "utf16be" | "unknown";
|
|
22
|
+
}
|
|
23
|
+
export interface Metadata {
|
|
24
|
+
codec: string;
|
|
25
|
+
codec_id: number;
|
|
26
|
+
title: string;
|
|
27
|
+
artist: string;
|
|
28
|
+
album: string;
|
|
29
|
+
albumartist: string;
|
|
30
|
+
composer: string;
|
|
31
|
+
grouping: string;
|
|
32
|
+
comment: string;
|
|
33
|
+
genre: string;
|
|
34
|
+
year_string: string;
|
|
35
|
+
track_string: string;
|
|
36
|
+
disc_string: string;
|
|
37
|
+
mb_track_id: string;
|
|
38
|
+
track_number: number | null;
|
|
39
|
+
disc_number: number | null;
|
|
40
|
+
year: number | null;
|
|
41
|
+
duration_ms: number;
|
|
42
|
+
bitrate: number;
|
|
43
|
+
sample_rate: number;
|
|
44
|
+
filesize: number;
|
|
45
|
+
samples: number;
|
|
46
|
+
vbr: boolean;
|
|
47
|
+
first_frame_offset: number;
|
|
48
|
+
replaygain: ReplayGain;
|
|
49
|
+
album_art: AlbumArt | null;
|
|
50
|
+
cuesheet: Cuesheet | null;
|
|
51
|
+
}
|
|
52
|
+
export interface PlayerStatus {
|
|
53
|
+
state: "stopped" | "playing" | "paused";
|
|
54
|
+
index: number | null;
|
|
55
|
+
position_ms: number;
|
|
56
|
+
duration_ms: number;
|
|
57
|
+
queue_len: number;
|
|
58
|
+
metadata: Metadata | null;
|
|
59
|
+
}
|
|
60
|
+
export interface PlayerConfig {
|
|
61
|
+
sampleRate?: number;
|
|
62
|
+
bufferSeconds?: number;
|
|
63
|
+
volume?: number;
|
|
64
|
+
replaygainMode?: number;
|
|
65
|
+
replaygainPreampDb?: number;
|
|
66
|
+
replaygainPreventClipping?: boolean;
|
|
67
|
+
crossfadeMode?: number;
|
|
68
|
+
fadeOutDelayMs?: number;
|
|
69
|
+
fadeOutDurationMs?: number;
|
|
70
|
+
fadeInDelayMs?: number;
|
|
71
|
+
fadeInDurationMs?: number;
|
|
72
|
+
mixMode?: number;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rockbox-ffi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript bindings (Bun + Deno + Node.js FFI) for the Rockbox DSP / metadata / playback engine",
|
|
5
|
+
"license": "GPL-2.0-or-later",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./bun": {
|
|
21
|
+
"types": "./dist/bun.d.ts",
|
|
22
|
+
"import": "./dist/bun.js"
|
|
23
|
+
},
|
|
24
|
+
"./deno": {
|
|
25
|
+
"types": "./dist/deno.d.ts",
|
|
26
|
+
"import": "./dist/deno.js"
|
|
27
|
+
},
|
|
28
|
+
"./node": {
|
|
29
|
+
"types": "./dist/node.d.ts",
|
|
30
|
+
"import": "./dist/node.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "bun run scripts/build.ts",
|
|
35
|
+
"prepublishOnly": "bun run build",
|
|
36
|
+
"smoke:bun": "bun run examples/smoke.bun.ts",
|
|
37
|
+
"smoke:deno": "deno run --allow-ffi --allow-read --allow-env examples/smoke.deno.ts",
|
|
38
|
+
"smoke:node": "tsx examples/smoke.node.ts",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"koffi": "^2.9.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/bun": "latest",
|
|
46
|
+
"@types/node": "^22.0.0",
|
|
47
|
+
"tsx": "^4.19.0",
|
|
48
|
+
"typescript": "^5.7.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Runtime-agnostic high-level API, written once against the Raw backend.
|
|
2
|
+
|
|
3
|
+
import type { Raw } from "./ffi.ts";
|
|
4
|
+
import { CrossfadeMode, MixMode, ReplayGainMode } from "./enums.ts";
|
|
5
|
+
import type { Metadata, PlayerConfig, PlayerStatus } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
const opt = (v: number | null | undefined): number =>
|
|
8
|
+
v === null || v === undefined ? Number.NaN : v;
|
|
9
|
+
|
|
10
|
+
export function makeApi(raw: Raw) {
|
|
11
|
+
const s = raw.sym;
|
|
12
|
+
|
|
13
|
+
const abiVersion = (): number => Number(s.rb_ffi_abi_version());
|
|
14
|
+
|
|
15
|
+
const metadata = {
|
|
16
|
+
/** Parse the metadata of the audio file at `path`. */
|
|
17
|
+
read(path: string): Metadata {
|
|
18
|
+
const json = raw.takeString(s.rb_meta_read_json(raw.cstr(path)));
|
|
19
|
+
if (json === null) throw new Error(`could not read metadata from ${path}`);
|
|
20
|
+
return JSON.parse(json) as Metadata;
|
|
21
|
+
},
|
|
22
|
+
/** Guess the codec label from a filename's extension (null if unknown). */
|
|
23
|
+
probe(filename: string): string | null {
|
|
24
|
+
return raw.takeString(s.rb_meta_probe(raw.cstr(filename)));
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Interleaved-S16LE-stereo DSP instance. Single instance per process. */
|
|
29
|
+
class Dsp {
|
|
30
|
+
#h: unknown;
|
|
31
|
+
constructor(sampleRate: number) {
|
|
32
|
+
this.#h = s.rb_dsp_new(sampleRate);
|
|
33
|
+
if (raw.isNull(this.#h)) throw new Error("rb_dsp_new returned NULL");
|
|
34
|
+
}
|
|
35
|
+
close(): void {
|
|
36
|
+
if (!raw.isNull(this.#h)) {
|
|
37
|
+
s.rb_dsp_free(this.#h);
|
|
38
|
+
this.#h = null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
[Symbol.dispose](): void {
|
|
42
|
+
this.close();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setInputFrequency(hz: number): void {
|
|
46
|
+
s.rb_dsp_set_input_frequency(this.#h, hz);
|
|
47
|
+
}
|
|
48
|
+
flush(): void {
|
|
49
|
+
s.rb_dsp_flush(this.#h);
|
|
50
|
+
}
|
|
51
|
+
eqEnable(enable: boolean): void {
|
|
52
|
+
s.rb_dsp_eq_enable(this.#h, enable);
|
|
53
|
+
}
|
|
54
|
+
setEqBand(band: number, cutoffHz: number, q: number, gainDb: number): void {
|
|
55
|
+
s.rb_dsp_set_eq_band(this.#h, band, cutoffHz, q, gainDb);
|
|
56
|
+
}
|
|
57
|
+
setEqPrecut(db: number): void {
|
|
58
|
+
s.rb_dsp_set_eq_precut(this.#h, db);
|
|
59
|
+
}
|
|
60
|
+
setTone(bassDb: number, trebleDb: number): void {
|
|
61
|
+
s.rb_dsp_set_tone(this.#h, bassDb, trebleDb);
|
|
62
|
+
}
|
|
63
|
+
setToneCutoffs(bassHz: number, trebleHz: number): void {
|
|
64
|
+
s.rb_dsp_set_tone_cutoffs(this.#h, bassHz, trebleHz);
|
|
65
|
+
}
|
|
66
|
+
setSurround(delayMs: number, balance: number, fx1: number, fx2: number): void {
|
|
67
|
+
s.rb_dsp_set_surround(this.#h, delayMs, balance, fx1, fx2);
|
|
68
|
+
}
|
|
69
|
+
setChannelConfig(mode: number): void {
|
|
70
|
+
s.rb_dsp_set_channel_config(this.#h, mode);
|
|
71
|
+
}
|
|
72
|
+
setStereoWidth(percent: number): void {
|
|
73
|
+
s.rb_dsp_set_stereo_width(this.#h, percent);
|
|
74
|
+
}
|
|
75
|
+
setCompressor(
|
|
76
|
+
threshold: number, makeupGain: number, ratio: number, knee: number,
|
|
77
|
+
releaseTime: number, attackTime: number,
|
|
78
|
+
): void {
|
|
79
|
+
s.rb_dsp_set_compressor(this.#h, threshold, makeupGain, ratio, knee, releaseTime, attackTime);
|
|
80
|
+
}
|
|
81
|
+
/** mode: DspReplayGainMode (TRACK=0, ALBUM=1, SHUFFLE=2, OFF=3). */
|
|
82
|
+
setReplaygain(mode: number, noclip: boolean, preampDb: number): void {
|
|
83
|
+
s.rb_dsp_set_replaygain(this.#h, mode, noclip, preampDb);
|
|
84
|
+
}
|
|
85
|
+
/** Per-track gains in plain dB, peaks as linear amplitude. undefined => absent. */
|
|
86
|
+
setReplaygainGains(
|
|
87
|
+
trackGainDb?: number | null, albumGainDb?: number | null,
|
|
88
|
+
trackPeak?: number | null, albumPeak?: number | null,
|
|
89
|
+
): void {
|
|
90
|
+
s.rb_dsp_set_replaygain_gains(
|
|
91
|
+
this.#h, opt(trackGainDb), opt(albumGainDb), opt(trackPeak), opt(albumPeak),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
/** Native Q7.24 factors (the `raw_*` metadata fields); use BigInt. */
|
|
95
|
+
setReplaygainGainsRaw(
|
|
96
|
+
trackGain: bigint, albumGain: bigint, trackPeak: bigint, albumPeak: bigint,
|
|
97
|
+
): void {
|
|
98
|
+
s.rb_dsp_set_replaygain_gains_raw(this.#h, trackGain, albumGain, trackPeak, albumPeak);
|
|
99
|
+
}
|
|
100
|
+
/** Run interleaved stereo S16 samples through the pipeline. */
|
|
101
|
+
process(samples: Int16Array): Int16Array {
|
|
102
|
+
if (samples.length % 2 !== 0) {
|
|
103
|
+
throw new Error("input must be interleaved stereo (even length)");
|
|
104
|
+
}
|
|
105
|
+
const out = raw.sizeOut();
|
|
106
|
+
const ptr = s.rb_dsp_process(this.#h, raw.i16in(samples), samples.length, out.arg);
|
|
107
|
+
const n = out.value();
|
|
108
|
+
if (raw.isNull(ptr) || n === 0) return new Int16Array(0);
|
|
109
|
+
return raw.takeI16(ptr, n);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Queue-based player. Owns a live output device + engine thread. */
|
|
114
|
+
class Player {
|
|
115
|
+
#h: unknown;
|
|
116
|
+
constructor(config?: PlayerConfig) {
|
|
117
|
+
if (config === undefined) {
|
|
118
|
+
this.#h = s.rb_player_new();
|
|
119
|
+
} else {
|
|
120
|
+
this.#h = s.rb_player_new_with_config(
|
|
121
|
+
config.sampleRate ?? 0,
|
|
122
|
+
config.bufferSeconds ?? 4.0,
|
|
123
|
+
config.volume ?? 1.0,
|
|
124
|
+
config.replaygainMode ?? ReplayGainMode.OFF,
|
|
125
|
+
config.replaygainPreampDb ?? 0.0,
|
|
126
|
+
config.replaygainPreventClipping ?? true,
|
|
127
|
+
config.crossfadeMode ?? CrossfadeMode.OFF,
|
|
128
|
+
config.fadeOutDelayMs ?? 0,
|
|
129
|
+
config.fadeOutDurationMs ?? 2000,
|
|
130
|
+
config.fadeInDelayMs ?? 0,
|
|
131
|
+
config.fadeInDurationMs ?? 2000,
|
|
132
|
+
config.mixMode ?? MixMode.CROSSFADE,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (raw.isNull(this.#h)) {
|
|
136
|
+
throw new Error("failed to create Player (no output device?)");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
close(): void {
|
|
140
|
+
if (!raw.isNull(this.#h)) {
|
|
141
|
+
s.rb_player_free(this.#h);
|
|
142
|
+
this.#h = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
[Symbol.dispose](): void {
|
|
146
|
+
this.close();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setQueue(paths: string[]): void {
|
|
150
|
+
s.rb_player_set_queue_json(this.#h, raw.cstr(JSON.stringify(paths)));
|
|
151
|
+
}
|
|
152
|
+
enqueue(path: string): void {
|
|
153
|
+
s.rb_player_enqueue(this.#h, raw.cstr(path));
|
|
154
|
+
}
|
|
155
|
+
play(): void {
|
|
156
|
+
s.rb_player_play(this.#h);
|
|
157
|
+
}
|
|
158
|
+
pause(): void {
|
|
159
|
+
s.rb_player_pause(this.#h);
|
|
160
|
+
}
|
|
161
|
+
toggle(): void {
|
|
162
|
+
s.rb_player_toggle(this.#h);
|
|
163
|
+
}
|
|
164
|
+
stop(): void {
|
|
165
|
+
s.rb_player_stop(this.#h);
|
|
166
|
+
}
|
|
167
|
+
next(): void {
|
|
168
|
+
s.rb_player_next(this.#h);
|
|
169
|
+
}
|
|
170
|
+
previous(): void {
|
|
171
|
+
s.rb_player_previous(this.#h);
|
|
172
|
+
}
|
|
173
|
+
skipTo(index: number): void {
|
|
174
|
+
s.rb_player_skip_to(this.#h, index);
|
|
175
|
+
}
|
|
176
|
+
seekMs(ms: number | bigint): void {
|
|
177
|
+
s.rb_player_seek_ms(this.#h, BigInt(ms));
|
|
178
|
+
}
|
|
179
|
+
setVolume(vol: number): void {
|
|
180
|
+
s.rb_player_set_volume(this.#h, vol);
|
|
181
|
+
}
|
|
182
|
+
volume(): number {
|
|
183
|
+
return Number(s.rb_player_volume(this.#h));
|
|
184
|
+
}
|
|
185
|
+
sampleRate(): number {
|
|
186
|
+
return Number(s.rb_player_sample_rate(this.#h));
|
|
187
|
+
}
|
|
188
|
+
setCrossfade(
|
|
189
|
+
mode: number, foDelayMs = 0, foDurMs = 2000, fiDelayMs = 0, fiDurMs = 2000,
|
|
190
|
+
mixMode = MixMode.CROSSFADE,
|
|
191
|
+
): void {
|
|
192
|
+
s.rb_player_set_crossfade(this.#h, mode, foDelayMs, foDurMs, fiDelayMs, fiDurMs, mixMode);
|
|
193
|
+
}
|
|
194
|
+
/** mode: ReplayGainMode (OFF=0, TRACK=1, ALBUM=2). */
|
|
195
|
+
setReplaygain(mode: number, preampDb: number, preventClipping: boolean): void {
|
|
196
|
+
s.rb_player_set_replaygain(this.#h, mode, preampDb, preventClipping);
|
|
197
|
+
}
|
|
198
|
+
status(): PlayerStatus {
|
|
199
|
+
const json = raw.takeString(s.rb_player_status_json(this.#h));
|
|
200
|
+
if (json === null) throw new Error("rb_player_status_json returned NULL");
|
|
201
|
+
return JSON.parse(json) as PlayerStatus;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { abiVersion, metadata, Dsp, Player };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Generate `seconds` of a sine as interleaved stereo Int16. */
|
|
209
|
+
export function sineStereo(
|
|
210
|
+
freqHz: number, seconds: number, rate: number, amplitude = 16000,
|
|
211
|
+
): Int16Array {
|
|
212
|
+
const n = Math.floor(seconds * rate);
|
|
213
|
+
const buf = new Int16Array(n * 2);
|
|
214
|
+
for (let i = 0; i < n; i++) {
|
|
215
|
+
let v = Math.round(Math.sin((i * 2 * Math.PI * freqHz) / rate) * amplitude);
|
|
216
|
+
v = Math.max(-32768, Math.min(32767, v));
|
|
217
|
+
buf[i * 2] = v;
|
|
218
|
+
buf[i * 2 + 1] = v;
|
|
219
|
+
}
|
|
220
|
+
return buf;
|
|
221
|
+
}
|
package/src/bun.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Bun backend + full API. Import from here under Bun:
|
|
2
|
+
// import { Dsp, Player, metadata } from "rockbox-ffi/bun";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CString,
|
|
6
|
+
dlopen,
|
|
7
|
+
FFIType,
|
|
8
|
+
type Pointer,
|
|
9
|
+
ptr,
|
|
10
|
+
toArrayBuffer,
|
|
11
|
+
} from "bun:ffi";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
import { makeApi, sineStereo } from "./api.ts";
|
|
17
|
+
import { resolveLibPath, SPEC, type Raw, type Tok } from "./ffi.ts";
|
|
18
|
+
|
|
19
|
+
const TOK: Record<Tok, FFIType> = {
|
|
20
|
+
void: FFIType.void,
|
|
21
|
+
ptr: FFIType.ptr,
|
|
22
|
+
u32: FFIType.u32,
|
|
23
|
+
i32: FFIType.i32,
|
|
24
|
+
u64: FFIType.u64,
|
|
25
|
+
usize: FFIType.u64,
|
|
26
|
+
f32: FFIType.f32,
|
|
27
|
+
bool: FFIType.bool,
|
|
28
|
+
strbuf: FFIType.ptr,
|
|
29
|
+
i16in: FFIType.ptr,
|
|
30
|
+
outsize: FFIType.ptr,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function makeRaw(): Raw {
|
|
34
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const libPath = resolveLibPath(
|
|
36
|
+
here,
|
|
37
|
+
(k) => Bun.env[k],
|
|
38
|
+
existsSync,
|
|
39
|
+
join,
|
|
40
|
+
dirname,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const symbols: Record<string, { args: FFIType[]; returns: FFIType }> = {};
|
|
44
|
+
for (const [name, spec] of Object.entries(SPEC)) {
|
|
45
|
+
symbols[name] = { args: spec.args.map((t) => TOK[t]), returns: TOK[spec.ret] };
|
|
46
|
+
}
|
|
47
|
+
const lib = dlopen(libPath, symbols);
|
|
48
|
+
const sym = lib.symbols as Record<string, (...a: any[]) => any>;
|
|
49
|
+
|
|
50
|
+
// Keep marshalled input buffers alive across the (synchronous) call by
|
|
51
|
+
// holding a reference on the caller's stack — Bun's `ptr()` does not copy.
|
|
52
|
+
const enc = new TextEncoder();
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
sym,
|
|
56
|
+
cstr(s: string) {
|
|
57
|
+
const buf = enc.encode(s + "\0");
|
|
58
|
+
// Attach to a WeakRef-free holder: return the pointer but stash the
|
|
59
|
+
// buffer on the returned Number via closure is impossible, so we rely
|
|
60
|
+
// on synchronous FFI — the buffer is created fresh here and the call
|
|
61
|
+
// happens immediately after in the same tick.
|
|
62
|
+
return ptr(buf);
|
|
63
|
+
},
|
|
64
|
+
i16in(a: Int16Array) {
|
|
65
|
+
return ptr(a);
|
|
66
|
+
},
|
|
67
|
+
sizeOut() {
|
|
68
|
+
const arr = new BigUint64Array(1);
|
|
69
|
+
return { arg: ptr(arr), value: () => Number(arr[0]) };
|
|
70
|
+
},
|
|
71
|
+
takeString(p: unknown) {
|
|
72
|
+
if (p === null || p === 0) return null;
|
|
73
|
+
// Bun pointers are numbers at runtime; the typed API wants `Pointer`.
|
|
74
|
+
const str = new CString(p as Pointer).toString();
|
|
75
|
+
sym.rb_string_free(p);
|
|
76
|
+
return str;
|
|
77
|
+
},
|
|
78
|
+
takeI16(p: unknown, len: number) {
|
|
79
|
+
const ab = toArrayBuffer(p as Pointer, 0, len * 2);
|
|
80
|
+
const copy = new Int16Array(len);
|
|
81
|
+
copy.set(new Int16Array(ab));
|
|
82
|
+
sym.rb_buffer_free(p, len);
|
|
83
|
+
return copy;
|
|
84
|
+
},
|
|
85
|
+
isNull(p: unknown) {
|
|
86
|
+
return p === null || p === 0 || p === undefined;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const api = makeApi(makeRaw());
|
|
92
|
+
export const { abiVersion, metadata, Dsp, Player } = api;
|
|
93
|
+
export { sineStereo };
|
|
94
|
+
export * from "./enums.ts";
|
|
95
|
+
export type { Metadata, PlayerStatus, PlayerConfig } from "./types.ts";
|
package/src/deno.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Deno backend + full API. Import from here under Deno:
|
|
2
|
+
// import { Dsp, Player, metadata } from "./src/deno.ts";
|
|
3
|
+
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { makeApi, sineStereo } from "./api.ts";
|
|
9
|
+
import { resolveLibPath, SPEC, type Raw, type Tok } from "./ffi.ts";
|
|
10
|
+
|
|
11
|
+
// deno-lint-ignore no-explicit-any
|
|
12
|
+
const D = (globalThis as any).Deno;
|
|
13
|
+
|
|
14
|
+
const TOK: Record<Tok, string> = {
|
|
15
|
+
void: "void",
|
|
16
|
+
ptr: "pointer",
|
|
17
|
+
u32: "u32",
|
|
18
|
+
i32: "i32",
|
|
19
|
+
u64: "u64",
|
|
20
|
+
usize: "usize",
|
|
21
|
+
f32: "f32",
|
|
22
|
+
bool: "bool",
|
|
23
|
+
strbuf: "buffer",
|
|
24
|
+
i16in: "buffer",
|
|
25
|
+
outsize: "buffer",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function makeRaw(): Raw {
|
|
29
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const libPath = resolveLibPath(
|
|
31
|
+
here,
|
|
32
|
+
(k) => D.env.get(k),
|
|
33
|
+
existsSync,
|
|
34
|
+
join,
|
|
35
|
+
dirname,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const defs: Record<string, { parameters: string[]; result: string }> = {};
|
|
39
|
+
for (const [name, spec] of Object.entries(SPEC)) {
|
|
40
|
+
defs[name] = { parameters: spec.args.map((t) => TOK[t]), result: TOK[spec.ret] };
|
|
41
|
+
}
|
|
42
|
+
const lib = D.dlopen(libPath, defs);
|
|
43
|
+
const sym = lib.symbols as Record<string, (...a: any[]) => any>;
|
|
44
|
+
|
|
45
|
+
const enc = new TextEncoder();
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
sym,
|
|
49
|
+
cstr(s: string) {
|
|
50
|
+
return enc.encode(s + "\0");
|
|
51
|
+
},
|
|
52
|
+
i16in(a: Int16Array) {
|
|
53
|
+
return a;
|
|
54
|
+
},
|
|
55
|
+
sizeOut() {
|
|
56
|
+
const buf = new Uint8Array(8);
|
|
57
|
+
const view = new DataView(buf.buffer);
|
|
58
|
+
return { arg: buf, value: () => Number(view.getBigUint64(0, true)) };
|
|
59
|
+
},
|
|
60
|
+
takeString(p: unknown) {
|
|
61
|
+
if (p === null) return null;
|
|
62
|
+
const str = new D.UnsafePointerView(p).getCString();
|
|
63
|
+
sym.rb_string_free(p);
|
|
64
|
+
return str;
|
|
65
|
+
},
|
|
66
|
+
takeI16(p: unknown, len: number) {
|
|
67
|
+
const ab = new D.UnsafePointerView(p).getArrayBuffer(len * 2);
|
|
68
|
+
const copy = new Int16Array(len);
|
|
69
|
+
copy.set(new Int16Array(ab));
|
|
70
|
+
sym.rb_buffer_free(p, len);
|
|
71
|
+
return copy;
|
|
72
|
+
},
|
|
73
|
+
isNull(p: unknown) {
|
|
74
|
+
return p === null || p === undefined;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const api = makeApi(makeRaw());
|
|
80
|
+
export const { abiVersion, metadata, Dsp, Player } = api;
|
|
81
|
+
export { sineStereo };
|
|
82
|
+
export * from "./enums.ts";
|
|
83
|
+
export type { Metadata, PlayerConfig, PlayerStatus } from "./types.ts";
|
package/src/enums.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Enum constants for the Rockbox FFI.
|
|
2
|
+
//
|
|
3
|
+
// NOTE: the DSP and player use *different* ReplayGain mode integers (a quirk
|
|
4
|
+
// of the underlying C ABI). Use DspReplayGainMode with `Dsp.setReplaygain`
|
|
5
|
+
// and ReplayGainMode with `Player.setReplaygain`.
|
|
6
|
+
|
|
7
|
+
/** For `Dsp.setReplaygain` (native Rockbox values). */
|
|
8
|
+
export const DspReplayGainMode = {
|
|
9
|
+
TRACK: 0,
|
|
10
|
+
ALBUM: 1,
|
|
11
|
+
SHUFFLE: 2,
|
|
12
|
+
OFF: 3,
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
/** For `Player.setReplaygain`. */
|
|
16
|
+
export const ReplayGainMode = {
|
|
17
|
+
OFF: 0,
|
|
18
|
+
TRACK: 1,
|
|
19
|
+
ALBUM: 2,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export const CrossfadeMode = {
|
|
23
|
+
OFF: 0,
|
|
24
|
+
AUTO_SKIP: 1,
|
|
25
|
+
MANUAL_SKIP: 2,
|
|
26
|
+
SHUFFLE: 3,
|
|
27
|
+
SHUFFLE_OR_MANUAL: 4,
|
|
28
|
+
ALWAYS: 5,
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export const MixMode = {
|
|
32
|
+
CROSSFADE: 0,
|
|
33
|
+
MIX: 1,
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export const ChannelConfig = {
|
|
37
|
+
STEREO: 0,
|
|
38
|
+
MONO: 1,
|
|
39
|
+
CUSTOM: 2,
|
|
40
|
+
MONO_LEFT: 3,
|
|
41
|
+
MONO_RIGHT: 4,
|
|
42
|
+
KARAOKE: 5,
|
|
43
|
+
SWAP: 6,
|
|
44
|
+
} as const;
|