Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
# @suaveplan/audio-ir-room-plugin-club - Original audio-ir content package: ships the bundled `club1.wav` and `club2.wav` room impulse responses, registered through @suaveplan/storage-uri-plugin-bundle and @suaveplan/audio-ir-core. Universal; bundle-only content package.
Original audio-ir content package. Ships two bundled room impulse responses (`club1.wav`, `club2.wav`) wired through the `@suaveplan/audio-ir-*` family.
This package is the canonical example of the **bundle-only** content-package pattern in the IR family — every future `audio-ir-{family}-plugin-<name>` package that ships only local files follows this shape.
<!-- begin:auto-docs -->
## Documentation tree
- [manifest](./src/manifest/manifest.md) — The `manifest` constant is the single source of truth for which impulse responses this content package ships. It is a `…
- [manifest](./src/manifest/manifest.md) — The `manifest` constant is the single source of truth for which impulse responses this content package ships. It is a `…
- [register](./src/register/register.md) — This module is the only side-effecting entrypoint a consumer ever calls from `@suaveplan/audio-ir-room-plugin-club`. It…
- [register](./src/register/register.md) — This module is the only side-effecting entrypoint a consumer ever calls from `@suaveplan/audio-ir-room-plugin-club`. It…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/audio-ir-room-plugin-club
```
Peer-installed alongside `@suaveplan/audio-ir-core`, `@suaveplan/storage-uri`, `@suaveplan/storage-uri-plugin-bundle`.
## Quick start
```ts
import {
createIrRegistry,
createUriPluginCollector,
} from "@suaveplan/audio-ir-core";
import { register } from "@suaveplan/audio-ir-room-plugin-club";
import { createIrLoader } from "@suaveplan/audio-ir-loader";
import { createStorageUriResolver } from "@suaveplan/storage-uri";
import { nodeFileFetch } from "@suaveplan/storage-uri-plugin-bundle/node-fetch";
import { decodeWav } from "@suaveplan/audio-decode-wav";
const registry = createIrRegistry();
const collector = createUriPluginCollector();
register(registry, collector, { fetch: nodeFileFetch }); // omit `fetch` in the browser
const resolver = createStorageUriResolver(collector.plugins);
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decodeWav },
});
const club1 = await loader.load("club-club1");
const club2 = await loader.load("club-club2");
```
## Manifest
| `id` | URI | Channels | Sample rate | Gain |
|---|---|---|---|---|
| `club-club1` | `bundle://audio-ir-room-plugin-club/club1.wav` | 2 | 44 100 Hz | 0 dB |
| `club-club2` | `bundle://audio-ir-room-plugin-club/club2.wav` | 2 | 44 100 Hz | 0 dB |
Channels and sample rate are decoded at manifest-authoring time; the loader re-validates and throws `AUDIO_IR_LOADER.MANIFEST_MISMATCH` on disagreement.
## Modules
- `./manifest` — `manifest: IrManifest`, `BUNDLE_PACKAGE_SLUG`, `MANIFEST_SLUG`.
- `./register` — `register(registry, collector, options?)` (collector pattern).
The root barrel re-exports every public binding so `import { register } from "@suaveplan/audio-ir-room-plugin-club"` is equivalent to the subpath form.
## Browser vs Node
- **Browser**: omit `options.fetch` in `register` — `globalThis.fetch` reads the bundled URL directly.
- **Node**: pass `register(registry, collector, { fetch: nodeFileFetch })` from `@suaveplan/storage-uri-plugin-bundle/node-fetch` — Node's undici-backed global `fetch` refuses `file:` URLs.
## License & sources
This package is published under the MIT license. Both IR captures (`club1.wav`, `club2.wav`) originate from the operator's Mountain Duck media library at `/media/_/audio/ir/room/club{1,2}.wav` and are redistributed inside Suaveplan under the same MIT license. The IR samples themselves are room captures with no upstream proprietary attribution requirement.
The `IrManifestEntry.source` field on each entry records the Mountain Duck origin path so a future operator can reproduce the import end-to-end.
## Pre-publish gate
```bash
bunx turbo lint typecheck test --filter=@suaveplan/audio-ir-room-plugin-club
bunx turbo test:coverage --filter=@suaveplan/audio-ir-room-plugin-club # 100/100/100/100
bunx turbo build --filter=@suaveplan/audio-ir-room-plugin-club
```

View File

@@ -0,0 +1,74 @@
{
"name": "@suaveplan/audio-ir-room-plugin-club",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "Genesis-original audio-ir content package: ships the bundled `club1.wav` and `club2.wav` room impulse responses, registered through @suaveplan/storage-uri-plugin-bundle and @suaveplan/audio-ir-core. Universal; bundle-only content package.",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./manifest": {
"import": {
"types": "./dist/manifest/manifest.d.ts",
"default": "./dist/manifest/manifest.js"
}
},
"./register": {
"import": {
"types": "./dist/register/register.d.ts",
"default": "./dist/register/register.js"
}
}
},
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/ tests/",
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/ tests/",
"format": "bunx biome format --write --max-diagnostics 500 src/ tests/",
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/ tests/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@suaveplan/audio-ir-core": "workspace:^",
"@suaveplan/error": "workspace:^",
"@suaveplan/storage-uri": "workspace:^",
"@suaveplan/storage-uri-plugin-bundle": "workspace:^",
"@suaveplan/types": "workspace:^",
"tslib": "catalog:build",
"zod": "catalog:forms"
},
"devDependencies": {
"@suaveplan/audio-decode-wav": "workspace:^",
"@suaveplan/audio-ir-loader": "workspace:^",
"@suaveplan/testing": "workspace:^",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
},
"files": [
"dist",
"files",
"README.md"
]
}

View File

@@ -0,0 +1,9 @@
export {
BUNDLE_PACKAGE_SLUG,
MANIFEST_SLUG,
manifest,
} from "./manifest/manifest.js";
export {
type RegisterOptions,
register,
} from "./register/register.js";

View File

@@ -0,0 +1,65 @@
# `manifest` — frozen room-IR manifest for the club package
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `manifest` constant is the single source of truth for which impulse responses this content package ships. It is a `IrManifest` parsed once through `IrManifestSchema` at module load time, frozen by the schema's `.transform`, and exported for two consumers: (1) `register(registry, resolver)` (the sibling `src/register` module) which walks every entry into the supplied `IrRegistry`, and (2) tooling that wants to enumerate available IRs without touching the storage resolver (catalog UIs, registry warm-up loaders, build-time docs).
Every value in the manifest is a literal at authoring time. Both `channels` and `sampleRate` were captured by decoding the bundled WAVs through `@suaveplan/audio-decode-wav` and recorded as constants so the manifest can never drift from the on-disk shape. The IR loader (`@suaveplan/audio-ir-loader`) re-validates these fields at decode time and throws `AUDIO_IR_LOADER.MANIFEST_MISMATCH` if they disagree — but for a bundle-only package the on-disk file is also part of the tarball, so the only way the values can disagree is a manual edit, which the manifest-validation gate catches before publish.
## Features
- Frozen at parse time — every consumer reads the same `Object.freeze`d value.
- Schema-validated id-prefix refinement (`club-*`) and family coherence (`room` on every entry).
- Pre-computed bundle URIs that match the package's slug verbatim.
- Stable insertion order: `club-club1` before `club-club2`.
## Usage: Basic
```ts
import { manifest } from "@suaveplan/audio-ir-room-plugin-club/manifest";
console.log(manifest.entries.map((e) => e.id));
// → ["club-club1", "club-club2"]
```
## Usage: Advanced
```ts
import { manifest, BUNDLE_PACKAGE_SLUG } from "@suaveplan/audio-ir-room-plugin-club/manifest";
import { IrManifestSchema } from "@suaveplan/audio-ir-core";
import { parseUri } from "@suaveplan/storage-uri";
// Re-validate (no-op — already parsed at load time, but useful as a smoke test).
const reparsed = IrManifestSchema.parse(manifest);
console.assert(reparsed.packageSlug === "club");
// Verify every entry resolves to a well-formed bundle:// URI bound to
// THIS package's slug.
for (const entry of manifest.entries) {
const parts = parseUri(entry.uri);
console.assert(parts.scheme === "bundle");
console.assert(parts.authority === BUNDLE_PACKAGE_SLUG);
}
```
## API Reference
### `manifest: IrManifest`
The frozen, parsed manifest. Two entries, both `family: "room"`, both `gainDb: 0`, both 44.1 kHz stereo.
### `BUNDLE_PACKAGE_SLUG: "audio-ir-room-plugin-club"`
The full npm-style slug used as the `bundle://<slug>/<rel>` authority. Exported so tests and the sibling `register` module can reference it without repeating the literal.
### `MANIFEST_SLUG: "club"`
The short slug stamped onto every `entry.id` as `<MANIFEST_SLUG>-<filename-stem>`.
## Implementation Notes
The two slugs (`MANIFEST_SLUG` and `BUNDLE_PACKAGE_SLUG`) intentionally differ: the manifest slug follows the design's "short id" rule (`design.md` §2) while the bundle slug must match the npm package name so a single resolver can route multiple content packages without collision. The bundle plugin enforces the longer form on `parse`; the registry enforces the shorter form on the entry id. Re-running `IrManifestSchema.parse(manifest)` is idempotent and free of side effects — the schema's `.transform` only freezes, it does not clone.

View File

@@ -0,0 +1,46 @@
import { IrManifestSchema } from "@suaveplan/audio-ir-core";
import { parseUri } from "@suaveplan/storage-uri";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { BUNDLE_PACKAGE_SLUG, manifest } from "./manifest.js";
describe("manifest", () => {
it("manifest parses through IrManifestSchema", { timeout: 5000 }, () => {
const parsed = IrManifestSchema.parse(manifest);
expect(parsed).toEqual(manifest);
expect(parsed.packageSlug).toBe("club");
expect(parsed.family).toBe("room");
for (const entry of parsed.entries) {
expect(entry.id.startsWith("club-")).toBe(true);
expect(entry.family).toBe("room");
}
});
it("declares exactly club-club1 and club-club2", { timeout: 5000 }, () => {
const ids = manifest.entries.map((e) => e.id);
expect(ids).toEqual(["club-club1", "club-club2"]);
expect(new Set(ids).size).toBe(ids.length);
});
it("every URI parses as a well-formed bundle URI", {
timeout: 5000,
}, () => {
for (const entry of manifest.entries) {
const parts = parseUri(entry.uri);
expect(parts.scheme).toBe("bundle");
expect(parts.authority).toBe(BUNDLE_PACKAGE_SLUG);
expect(parts.path.length).toBeGreaterThan(0);
}
});
it("every entry declares positive channels, sampleRate, and finite gainDb", {
timeout: 5000,
}, () => {
for (const entry of manifest.entries) {
expect(entry.channels).toBeGreaterThanOrEqual(1);
expect(entry.sampleRate).toBeGreaterThan(0);
expect(Number.isFinite(entry.gainDb)).toBe(true);
expect(entry.license.length).toBeGreaterThan(0);
expect(entry.source.length).toBeGreaterThan(0);
}
});
});

View File

@@ -0,0 +1,75 @@
import { type IrManifest, IrManifestSchema } from "@suaveplan/audio-ir-core";
/**
* npm-name slug used as the bundle URI authority. MUST match the package
* directory name and the `bundle://<slug>/...` authority in every
* `IrManifestEntry.uri` below — the bundle plugin's `parse` rejects any
* URI whose authority differs from this string verbatim.
*/
export const BUNDLE_PACKAGE_SLUG = "audio-ir-room-plugin-club" as const;
/**
* Short slug embedded into every entry id under the
* `<manifestSlug>-<filename-stem>` rule from `audio-ir-core/design.md` §2.
* The id-prefix `IrManifestSchema` refinement requires every entry id to
* start with `<manifestSlug>-`.
*/
export const MANIFEST_SLUG = "club" as const;
/**
* License declared on every entry. Both IRs originate from the operator's
* Mountain Duck media library and are redistributed inside the genesis
* monorepo under the package's MIT license — the IR samples themselves are
* room captures with no upstream proprietary attribution requirement.
*/
const LICENSE = "MIT (Suaveplan-original room capture)" as const;
/**
* Source attribution for `club1.wav`. Identifies the file by its full
* Mountain Duck mount path so an operator can reproduce the import.
*/
const SOURCE_CLUB1 =
"Mountain Duck media library: /media/_/audio/ir/room/club1.wav" as const;
/**
* Source attribution for `club2.wav`.
*/
const SOURCE_CLUB2 =
"Mountain Duck media library: /media/_/audio/ir/room/club2.wav" as const;
/**
* Frozen, schema-validated `IrManifest` describing the two club room IRs
* shipped by this package. `channels` and `sampleRate` were measured by
* decoding the bundled WAVs at manifest-authoring time (both are 44.1 kHz
* stereo). `gainDb: 0` means the loader applies no gain adjustment.
*
* The manifest is parsed through `IrManifestSchema` exactly once at module
* load — every consumer sees the frozen, validated value. Re-parsing is
* unnecessary; mutating it is impossible.
*/
export const manifest: IrManifest = IrManifestSchema.parse({
packageSlug: MANIFEST_SLUG,
family: "room",
entries: [
{
id: "club-club1",
family: "room",
uri: `bundle://${BUNDLE_PACKAGE_SLUG}/club1.wav`,
channels: 2,
sampleRate: 44100,
gainDb: 0,
license: LICENSE,
source: SOURCE_CLUB1,
},
{
id: "club-club2",
family: "room",
uri: `bundle://${BUNDLE_PACKAGE_SLUG}/club2.wav`,
channels: 2,
sampleRate: 44100,
gainDb: 0,
license: LICENSE,
source: SOURCE_CLUB2,
},
],
});

View File

@@ -0,0 +1,130 @@
# `register` — collector-pattern wiring entrypoint
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module is the only side-effecting entrypoint a consumer ever calls from
`@suaveplan/audio-ir-room-plugin-club`. It bridges three pieces that exist in
three separate packages: the in-memory `IrRegistry` from
`@suaveplan/audio-ir-core` (which holds manifest entries), a
`MutableUriPluginRegistrar` collector — typically built via
`createUriPluginCollector()` — that gathers `UriPlugin`s before resolver
construction, and the `bundleUriPlugin` from
`@suaveplan/storage-uri-plugin-bundle` (which knows how to read this
package's bundled WAVs).
The package never auto-registers on import — every consumer calls
`register(registry, collector)` explicitly. Importing the package's barrel
without calling `register` is a no-op aside from loading the frozen
manifest constant. This matches the side-effect-free contract on all
`@suaveplan/audio-ir-*` content packages and matches the family-canon
shape established by `audio-ir-room-plugin-pool`.
## Features
- Single call wires both the bundle adapter (via `target.use(plugin)`) and
the manifest registration (via `registerManifest`).
- `RegisterOptions.fetch?: FetchLike` lets Node-tier callers pass
`nodeFileFetch` so `file://` reads work under undici's global `fetch`.
- Default `rootUrl` resolves to the in-tarball `files/` directory via
`import.meta.url` — works in both Node and any modern bundler that
preserves package URLs.
- No `createPlugin()` separate step; no `resolver.hasScheme("bundle")`
structural check; the collector pattern makes both unnecessary.
- Errors from `registerManifest` (`AUDIO_IR.DUPLICATE_ID`,
`AUDIO_IR.ID_PREFIX_MISMATCH`, …) pass through verbatim.
## Usage: Basic
```ts
import {
createIrRegistry,
createUriPluginCollector,
} from "@suaveplan/audio-ir-core";
import { register } from "@suaveplan/audio-ir-room-plugin-club";
import { createStorageUriResolver } from "@suaveplan/storage-uri";
const registry = createIrRegistry();
const collector = createUriPluginCollector();
register(registry, collector);
const resolver = createStorageUriResolver(collector.plugins);
// registry.has("club-club1") === true
// registry.has("club-club2") === true
```
## Usage: Advanced
```ts
import {
createIrRegistry,
createUriPluginCollector,
} from "@suaveplan/audio-ir-core";
import { createIrLoader } from "@suaveplan/audio-ir-loader";
import { decodeWav } from "@suaveplan/audio-decode-wav";
import { register } from "@suaveplan/audio-ir-room-plugin-club";
import { createStorageUriResolver } from "@suaveplan/storage-uri";
import { nodeFileFetch } from "@suaveplan/storage-uri-plugin-bundle/node-fetch";
// Node.js: globalThis.fetch refuses file:// — wire nodeFileFetch.
const registry = createIrRegistry();
const collector = createUriPluginCollector();
register(registry, collector, { fetch: nodeFileFetch });
const resolver = createStorageUriResolver(collector.plugins);
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decodeWav },
});
const club1 = await loader.load("club-club1");
console.log(club1.channelData[0].length, "samples @", club1.sampleRate, "Hz");
```
## API Reference
### `register(registry: IrRegistry, target: MutableUriPluginRegistrar, options?: RegisterOptions): void`
Builds the package-scoped `bundleUriPlugin`, hands it to `target.use(plugin)`,
then calls `registerManifest(registry, manifest)`.
- `registry` — an `IrRegistry` from `@suaveplan/audio-ir-core`.
- `target` — any object satisfying
`MutableUriPluginRegistrar` (`use(plugin: UriPlugin): void`). Typically
built via `createUriPluginCollector()`.
- `options.fetch?: FetchLike` — overrides the runtime fetch (pass
`nodeFileFetch` on Node so `file://` URLs resolve).
Throws `AUDIO_IR.DUPLICATE_ID` if any manifest entry is already in the
registry.
### `interface RegisterOptions`
- `readonly fetch?: FetchLike` — see above.
## Implementation Notes
`new URL("../../files/", import.meta.url)` is intentional — the
`register/register.ts` file lives at `src/register/register.ts` in the
source tree, and at `dist/register/register.js` after build. In both
cases two levels up lands on the package root, where `files/` sits at
the same level as `dist/` and `src/`. The trailing slash is required by
`bundleUriPlugin`'s `rootUrl` validator.
The `target.use(plugin)` call happens before `registerManifest(...)` so
that even when `registerManifest` rejects (for example with
`AUDIO_IR.DUPLICATE_ID` on a double-register), the bundle plugin is
already in the caller's collector. This matches the family-canon
ordering established by `audio-ir-room-plugin-pool` and keeps the
collector observable when tests inspect it after a thrown error.
The double-register test relies on `registerManifest` calling
`registry.register(entry, ctx)` for each entry in declaration order. The
registry throws `AUDIO_IR.DUPLICATE_ID` on the first colliding id — for
this package, `club-club1` — so the rethrown error is deterministic.

View File

@@ -0,0 +1,90 @@
import {
AUDIO_IR_CODES,
AudioIrError,
createIrRegistry,
createUriPluginCollector,
} from "@suaveplan/audio-ir-core";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { register } from "./register.js";
describe("register", () => {
it("register calls target.use exactly once with a bundle plugin", {
timeout: 5000,
}, () => {
const registry = createIrRegistry();
const collector = createUriPluginCollector();
register(registry, collector);
expect(collector.plugins.length).toBe(1);
const installed = collector.plugins[0];
expect(installed).toBeDefined();
if (installed === undefined) throw new Error("unreachable");
expect(installed.scheme).toBe("bundle");
// Parse smoke-tests that the plugin is bound to this package's
// slug — the bundle plugin's `parse` accepts URIs whose authority
// equals `audio-ir-room-plugin-club`.
const parsed = installed.parse(
"bundle://audio-ir-room-plugin-club/club1.wav"
);
expect(parsed.key).toBe("club1.wav");
expect(parsed.meta?.packageSlug).toBe("audio-ir-room-plugin-club");
});
it("register populates the registry with both club entries", {
timeout: 5000,
}, () => {
const registry = createIrRegistry();
const collector = createUriPluginCollector();
register(registry, collector);
expect(registry.has("club-club1")).toBe(true);
expect(registry.has("club-club2")).toBe(true);
const rooms = registry.list({ family: "room" });
const ids = rooms.map((e) => e.id);
expect(ids).toContain("club-club1");
expect(ids).toContain("club-club2");
});
it("double register against same registry surfaces DUPLICATE_ID", {
timeout: 5000,
}, () => {
const registry = createIrRegistry();
const collectorA = createUriPluginCollector();
const collectorB = createUriPluginCollector();
register(registry, collectorA);
let caught: unknown;
try {
register(registry, collectorB);
} catch (err) {
caught = err;
}
expect(caught).toBeInstanceOf(AudioIrError);
expect((caught as AudioIrError).code).toBe(AUDIO_IR_CODES.DUPLICATE_ID);
// Even though the second register threw, its `target.use(plugin)`
// ran BEFORE `registerManifest` rejected — so collectorB still
// received the bundle plugin. This matches family-canon ordering.
expect(collectorB.plugins.length).toBe(1);
expect(collectorB.plugins[0]?.scheme).toBe("bundle");
});
it("register accepts an injected fetch via options", {
timeout: 5000,
}, () => {
const registry = createIrRegistry();
const collector = createUriPluginCollector();
const fakeFetch = async () => new Response(new Uint8Array());
register(registry, collector, { fetch: fakeFetch });
expect(collector.plugins.length).toBe(1);
expect(collector.plugins[0]?.scheme).toBe("bundle");
expect(registry.has("club-club1")).toBe(true);
});
});

View File

@@ -0,0 +1,48 @@
import type {
IrRegistry,
MutableUriPluginRegistrar,
} from "@suaveplan/audio-ir-core";
import { registerManifest } from "@suaveplan/audio-ir-core";
import { bundleUriPlugin } from "@suaveplan/storage-uri-plugin-bundle";
import type { FetchLike } from "@suaveplan/types";
import { BUNDLE_PACKAGE_SLUG, manifest } from "../manifest/manifest.js";
/** Optional overrides for `register`. */
export interface RegisterOptions {
/**
* Override the `fetch` implementation passed to `bundleUriPlugin`. Useful
* under Node (which does NOT support `fetch('file://...')`) — pass
* `nodeFileFetch` from `@suaveplan/storage-uri-plugin-bundle/node-fetch`.
* Defaults to `globalThis.fetch`.
*/
readonly fetch?: FetchLike;
}
/**
* Install this content package's bundle plugin onto `target` and register
* every entry from `manifest` into `registry`.
*
* `register` does NOT install the shipped `fallback://` plugin or any
* `https://` plugin — both are resolver-composition concerns the consumer
* owns once per resolver. The collector pattern decouples plugin
* construction from resolver construction so a single
* `createStorageUriResolver(collector.plugins)` call composes every
* content package's bundle plugin into one frozen resolver.
*
* `rootUrl` is computed from `import.meta.url` so the package's
* `files/` directory is located deterministically regardless of where
* the consumer installs the published tarball.
*/
export function register(
registry: IrRegistry,
target: MutableUriPluginRegistrar,
options?: RegisterOptions
): void {
const plugin = bundleUriPlugin({
packageSlug: BUNDLE_PACKAGE_SLUG,
rootUrl: new URL("../../files/", import.meta.url),
...(options?.fetch !== undefined ? { fetch: options.fetch } : {}),
});
target.use(plugin);
registerManifest(registry, manifest);
}

View File

@@ -0,0 +1,74 @@
import { readFile, stat } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { decodeWav } from "@suaveplan/audio-decode-wav";
import {
createIrRegistry,
createUriPluginCollector,
} from "@suaveplan/audio-ir-core";
import { createIrLoader } from "@suaveplan/audio-ir-loader";
import { createStorageUriResolver } from "@suaveplan/storage-uri";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { register } from "../../src/register/register.js";
/**
* Minimal `file://` `FetchLike` for Node — undici's global `fetch` refuses
* `file:` URLs. Sufficient for the GET requests the bundle plugin issues.
*/
const nodeFileFetch: typeof globalThis.fetch = async (
input,
init
): Promise<Response> => {
const url =
input instanceof URL
? input
: input instanceof Request
? new URL(input.url)
: new URL(input);
if (url.protocol !== "file:") {
return globalThis.fetch(input, init);
}
const path = fileURLToPath(url);
const st = await stat(path);
if (!st.isFile()) return new Response(null, { status: 404 });
const buf = await readFile(path);
return new Response(buf, {
status: 200,
headers: {
"content-length": String(st.size),
"last-modified": st.mtime.toUTCString(),
},
});
};
describe("end-to-end", () => {
it("collector wiring loads both club IRs", {
timeout: 30000,
}, async () => {
const registry = createIrRegistry();
const collector = createUriPluginCollector();
register(registry, collector, { fetch: nodeFileFetch });
const resolver = createStorageUriResolver(collector.plugins);
const loader = createIrLoader({
registry,
resolver,
decoders: { wav: decodeWav },
});
for (const id of ["club-club1", "club-club2"] as const) {
const ir = await loader.load(id);
expect(Object.isFrozen(ir)).toBe(true);
expect(ir.id).toBe(id);
expect(ir.family).toBe("room");
expect(ir.channels).toBe(2);
expect(ir.sampleRate).toBe(44100);
expect(ir.channelData.length).toBe(2);
expect(ir.channelData[0]!.length).toBeGreaterThan(0);
expect(ir.channelData[1]!.length).toBe(ir.channelData[0]!.length);
expect(ir.lengthFrames).toBe(ir.channelData[0]!.length);
expect(ir.durationSec).toBeGreaterThan(0);
expect(ir.gainDb).toBe(0);
}
});
});

View File

@@ -0,0 +1,23 @@
import { readFileSync, statSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "@suaveplan/testing/runner";
const FILES_ROOT = new URL("../../files/", import.meta.url);
describe("file-presence", () => {
it("club1.wav and club2.wav exist on disk and are non-empty", {
timeout: 5000,
}, () => {
for (const name of ["club1.wav", "club2.wav"]) {
const path = fileURLToPath(new URL(name, FILES_ROOT));
const stat = statSync(path);
expect(stat.isFile()).toBe(true);
expect(stat.size).toBeGreaterThan(0);
const bytes = readFileSync(path);
expect(bytes.byteLength).toBe(stat.size);
expect(
String.fromCharCode(bytes[0]!, bytes[1]!, bytes[2]!, bytes[3]!)
).toBe("RIFF");
}
});
});

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts", "tests/**/*.ts"],
"exclude": ["dist", "node_modules"]
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,38 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir: "../../../node_modules/.vite/packages/audio-ir-room-plugin-club",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: { entry: "src/index.ts", formats: ["es"] },
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [
"@suaveplan/audio-ir-core",
"@suaveplan/error",
"@suaveplan/storage-uri",
"@suaveplan/storage-uri-plugin-bundle",
"@suaveplan/types",
"tslib",
"zod",
],
},
},
});

View File

@@ -0,0 +1,5 @@
import { createTestConfig } from "@suaveplan/testing/config";
export default createTestConfig({
testTimeout: 30000,
});