first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
# `detect/detect-format`
Format detection and source-resolution helpers — the runtime-gated entry
point that turns any `Source` (string path, URL, ArrayBuffer, Uint8Array,
ReadableStream) into a header slice plus an optional detected format
name.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The package handles five concrete `Source` shapes; each needs a
different platform-specific resolution strategy. Without a single
helper, every public entry point (`decodeSource`, `encodeTo`,
`progressiveDecode`) would re-implement the same `string | URL |
ArrayBuffer | Uint8Array | ReadableStream` switch — duplicating wiring
and risking divergent behaviour for the edge cases. This module
centralises it.
The detection delegates magic-byte sniffing to
`@suaveplan/audio-metadata`'s `detectAudioFormat` — the same function
used by every other package that needs to identify audio bytes
— so the registry's `detect` and the entry-point `detectSource` always
agree.
## Features
- `Source` type union — `string | URL | ArrayBuffer | Uint8Array |
ReadableStream<Uint8Array>`.
- `DetectResult` shape — `{ format: string | undefined; bytes?:
Uint8Array; path?: string; reader?: ReadableStreamDefaultReader }`.
The optional fields tell the caller what kind of resolution happened
so they can continue draining the source efficiently.
- `detectSource(source)` — reads at most `HEADER_SIZE` (256 KiB) bytes
from the source head, sniffs the format, and returns either the
detected format name or `undefined` for unknown content. Never
throws for unknown content — only throws `INVALID_SOURCE` for a
source value that is not a recognised variant or a fetch that
returned no body.
- `resolveSource(source)` — drains the source completely into a single
`Uint8Array`. Used by `decodeSource` to materialise the full payload
before dispatch.
- `HEADER_SIZE` — exported constant set to `256 * 1024`. Reading
exactly this many bytes keeps detection cheap even for multi-GB
files while being large enough to cover every realistic header
layout (ID3v2 with embedded artwork, AIFF with large COMM-adjacent
comment chunks, etc).
## Resolution rules
- `string` starting with `http://` / `https://` / `data:` / `blob:`,
or any `URL` instance → resolved via `fetch()`. Header read via the
response stream reader, which is returned to the caller for
continued draining.
- `string` starting with `file://` → resolved via dynamic
`import('node:fs/promises')`. Returns the resolved file path so the
caller can perform random-access reads.
- Other bare `string` → on a server runtime (process.versions.node is
a string) resolves via `fs/promises.readFile`; in a browser runtime
resolves via `fetch()`.
- `ArrayBuffer` / `Uint8Array` → header is the first 256 KiB slice;
no platform call.
- `ReadableStream<Uint8Array>` → reader drains until 256 KiB observed;
the original reader is returned for continued draining.
## Usage: Basic Example
```ts
import { detectSource } from "@suaveplan/audio-codecs/detect/detect-format";
const result = await detectSource(bytes);
if (result.format === "wav") {
decodeWav(result.bytes!);
}
```
## Usage: Advanced Example
Resolve a remote URL once and reuse the detection result + stream
reader without re-fetching:
```ts
import { detectSource, resolveSource } from "@suaveplan/audio-codecs/detect/detect-format";
const result = await detectSource("https://cdn.example/audio.flac");
if (result.format === "flac") {
// We have the first 256 KiB cached on result.bytes, and the rest
// is available via result.reader.
const rest = await drainReader(result.reader!);
const full = concat(result.bytes!, rest);
return decodeFlac(full);
}
// resolveSource fully drains regardless of source shape.
const buf = await resolveSource("file:///srv/audio/in.wav");
```
## API Reference
- `type Source = string | URL | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>`
- `interface DetectResult { format: string | undefined; bytes?: Uint8Array; path?: string; reader?: ReadableStreamDefaultReader<Uint8Array> }`
- `const HEADER_SIZE: number` — `256 * 1024`
- `async function detectSource(source: Source): Promise<DetectResult>` —
throws `AUDIO_CODECS.INVALID_SOURCE` for an unrecognised source
value or when `fetch()` returns a body-less response.
- `async function resolveSource(source: Source): Promise<Uint8Array>` —
throws `AUDIO_CODECS.INVALID_SOURCE` for an unrecognised source.
## Implementation Notes
- `isServerRuntime()` is a closure over `typeof process !== "undefined"
&& typeof process.versions?.node === "string"`. The function exists
so the runtime branch is not evaluated at module-load time — the
package stays universal with no static `node:fs` import.
- The `node:fs/promises` import is **always** inside a runtime-gated
dynamic `import()` — never a top-level import. This keeps the package
importable from browser bundles without bundler config tweaks.
- The browser-only `fetch()` fallback paths (lines 154-156 in
`detectSource`, 214-216 in `resolveSource`) are unreachable under
Node tests; structural coverage of that branch requires a happy-dom
or browser-target test harness (a future integration suite).
- The `URL` instance branch and the `http://` / `https://` /
`data:` / `blob:` string branch both route through
`fetchHeaderViaFetch` — no logic is duplicated.
- `readHeaderFromStream` keeps reading until at least `HEADER_SIZE`
bytes are observed OR the stream ends. The resulting buffer may
be larger than `HEADER_SIZE` because chunks arrive whole; that's
intentional — over-read is cheap and avoids fragmenting the
caller's understanding of what bytes were consumed.