updated skills

This commit is contained in:
2026-06-09 21:06:27 +01:00
parent db1ce43ee1
commit 427a5bd919
1283 changed files with 24684 additions and 2313 deletions

View File

@@ -84,7 +84,7 @@ The buffer is a `Float32Array` allocated by `load`. Subsequent `load` calls repl
`pause` is intentionally a no-op when `playing === false`. Without that guard, a `pause()` before any `play()` would set `paused = true`, which would then make the next `play()` start in a paused state (because `play` only flips `playing`). This matched the behaviour of the main-branch `HTMLAudioElement`-backed implementation and is the principle of least surprise for media-player consumers.
`setPlaybackRate` validates at the setter rather than relying on the parameter's `min`/`max` clamping. The reason: the parameter declares `automatable: true`, so a parameter-curve automation could in principle write any number; the setter rejects out-of-range *direct* set calls so a UI's invalid input is caught at the boundary. Automation curves operate on a separate ramp and are clamped by the parameter range internally.
`setPlaybackRate` validates at the setter rather than relying on the parameter's `min`/`max` clamping. The reason: the parameter declares `automatable: true`, so a parameter-curve automation could in principle write any number; the setter rejects out-of-range _direct_ set calls so a UI's invalid input is caught at the boundary. Automation curves operate on a separate ramp and are clamped by the parameter range internally.
Linear-interpolated read between adjacent samples handles fractional playback rates and seeks. The interpolator is the same shape used by `SamplerPlugin` — `s0 * (1 - frac) + s1 * frac` — so pitch-preservation behaviour is consistent across the two plugins. Looping wraps with `position -= (buffer.length - 1)` rather than `position = 0` so any fractional carry survives the wrap.

View File

@@ -1,4 +1,4 @@
# @suaveplan/audio-fx-plugin-base - Foundation for the @suaveplan/audio-fx-plugin-* family. Exports the AudioFxPlugin contract, PluginChain primitive, ChainContext, AmbientState, BusRegistry, PluginRegistry, ChainSnapshot, ChainEventBus, ParameterSchedule, PresetSnapshot, and the shared AUDIO_FX.* error-code registry. Runs identically in browser, server, offline, and worklet contexts.
# @suaveplan/audio-fx-plugin-base - Foundation for the @suaveplan/audio-fx-plugin-*family. Exports the AudioFxPlugin contract, PluginChain primitive, ChainContext, AmbientState, BusRegistry, PluginRegistry, ChainSnapshot, ChainEventBus, ParameterSchedule, PresetSnapshot, and the shared AUDIO_FX.* error-code registry. Runs identically in browser, server, offline, and worklet contexts.
Foundation for the `@suaveplan/audio-fx-plugin-*` family. Universal (browser / server / offline / worklet identical). Zero `@suaveplan/audio-fx-plugin-*` deps — concrete plugins depend on this package, never the other way around.

View File

@@ -13,7 +13,7 @@ Every mode is bounded by construction: `tanh` is bounded in `[-1, 1]` for any in
## Features
- Four parameters: `mode` (enum 0..2), `drive` (dB, 0..40), `bits` (bitcrusher depth 1..16), `outputGain` (dB, -24..24).
- Drive pre-gain raises the input level *into* the transfer function — useful for waveshaper/overdrive to push the signal further into the non-linear region.
- Drive pre-gain raises the input level _into_ the transfer function — useful for waveshaper/overdrive to push the signal further into the non-linear region.
- `outputGain` compensates for level changes caused by shaping so the plugin can drop into an existing chain without raising perceived loudness.
- Mode switching is parameter-driven, so a single instance can crossfade between modes at runtime (snap by default; for click-free morphing, use the parameter automation API on `mode`).
- Preset payload carries the textual `mode` enum so saved presets are robust to future parameter-index changes.

View File

@@ -62,7 +62,7 @@ await jet.setup(
The delay line is a circular `Float32Array` per channel sized for `MAX_DELAY_MS = 20 ms` plus a 4-sample safety margin (the fractional read indexes `i0` and `i0 + 1`). Twenty milliseconds is well over `baseDelay.max + 2·depth.max = 10 + 10 = 20`, with the extra margin so the wrap path is never hit on the hottest combination of parameters.
The LFO is mapped to the delay tap via `delay = base + (sin + 1)/2 · depth`, not `delay = base + sin · depth`. Mapping to the half-positive range `[0, depth]` means the flange always *adds* delay to the base — never *subtracts* — so the line never reads near `writeIndex - 0` (which would wrap aliased noise into the output). It also matches the standard convention used by most reference flanger plugins so the perceptual sweep direction is what producers expect.
The LFO is mapped to the delay tap via `delay = base + (sin + 1)/2 · depth`, not `delay = base + sin · depth`. Mapping to the half-positive range `[0, depth]` means the flange always _adds_ delay to the base — never _subtracts_ — so the line never reads near `writeIndex - 0` (which would wrap aliased noise into the output). It also matches the standard convention used by most reference flanger plugins so the perceptual sweep direction is what producers expect.
Feedback at the comb-filter resonance is intentionally allowed to climb to 0.95 because the flanger character lives at the resonance edge. The test "max feedback over a long sine never produces non-finite samples" exercises this boundary against a one-second sine at `feedback = 0.95` and verifies every output sample is finite and amplitude-bounded. Allowing `1.0` would create a marginally stable system whose output diverges for any non-zero input; clamping at 0.95 leaves enough headroom that even a noise-floor leak stays bounded over arbitrarily long renders.

View File

@@ -72,12 +72,12 @@ game.cullDistantSources(80);
## Implementation Notes
The audio path is silent by design. Implementing the full mix of spatial sources + music layers + voice chat into a single mono output stream would require this plugin to embed three sibling-plugin engines (`SpatialAudioPlugin`, `MixerPlugin`, plus a voice-chat router) — which would either duplicate code or pull every plugin into this one's import graph. Instead, the plugin owns the *registry* of game-audio state; the browser engine reads that state via `createAudioNode()` and renders it into a Web Audio graph in a future revision. The universal block path produces silence but advances the internal sample clock so `isCrossfading` / `isBlending` work correctly under any renderer.
The audio path is silent by design. Implementing the full mix of spatial sources + music layers + voice chat into a single mono output stream would require this plugin to embed three sibling-plugin engines (`SpatialAudioPlugin`, `MixerPlugin`, plus a voice-chat router) — which would either duplicate code or pull every plugin into this one's import graph. Instead, the plugin owns the _registry_ of game-audio state; the browser engine reads that state via `createAudioNode()` and renders it into a Web Audio graph in a future revision. The universal block path produces silence but advances the internal sample clock so `isCrossfading` / `isBlending` work correctly under any renderer.
Crossfade scheduling stores the start sample (`processedSamples` at the time of the call) plus the duration in samples. `isCrossfading` is `elapsed < durationSamples`. The check is exact at the sample level — the test "isCrossfading true immediately; false after enough samples processed" exercises that by processing exactly the configured number of samples.
`cullDistantSources` walks the spatial-source map and deletes entries whose distance to the listener exceeds the threshold. The cost is `O(numSources)` per call; for the typical game-audio case (under 100 sources) this is below 1 ms even at 60 fps cull frequency. A spatial-hash variant would amortise this to `O(log n)` but adds bookkeeping that the MVP does not need.
`optimizeVoiceChatBandwidth` returns a coarse 0..3 tier rather than per-participant codec parameters because the actual codec selection lives in the transport layer (WebRTC / similar). The plugin's job is to *advise*; the transport applies the advice. Currently the heuristic is just a participant-count threshold (>8 ⇒ level 3, otherwise level 1) — a future revision can layer on signal-strength and ping data.
`optimizeVoiceChatBandwidth` returns a coarse 0..3 tier rather than per-participant codec parameters because the actual codec selection lives in the transport layer (WebRTC / similar). The plugin's job is to _advise_; the transport applies the advice. Currently the heuristic is just a participant-count threshold (>8 ⇒ level 3, otherwise level 1) — a future revision can layer on signal-strength and ping data.
The plugin maintains an internal `processedSamples` counter incremented by `output.length` each `processBlock`. This is the same clock crossfade and effect-blend countdowns reference — using `BlockContext.blockOffsetSamples` directly would not work because the chain may slice or align blocks differently, and the consumer expects clock advancement per actual rendered sample. The counter resets on `setup`/`reset`.

View File

@@ -73,4 +73,4 @@ The distortion stage is a `tanh` soft-clip with parallel dry/wet routing — sam
The low-pass is a one-pole filter (the same shape used by `MicrophoneSimulationPlugin`), and the reverb is a short feedback delay (`REVERB_DELAY_MS = 30 ms` per channel). Both are intentionally simpler than their dedicated sibling-plugin counterparts because the GLaDOS character does not need surgical control — the user wants "more or less" of each colour, not a fully parametric filter and convolution.
Preset application replaces the *entire* config in one shot via `{ ...PRESETS[preset] }`. Individual setter overrides applied after `applyGladosPreset` will stick; the next preset application replaces them. The preset payload encodes both the preset name and the live config so `applyPreset(snap)` restores any per-stage overrides made after the preset was originally applied.
Preset application replaces the _entire_ config in one shot via `{ ...PRESETS[preset] }`. Individual setter overrides applied after `applyGladosPreset` will stick; the next preset application replaces them. The preset payload encodes both the preset name and the live config so `applyPreset(snap)` restores any per-stage overrides made after the preset was originally applied.

View File

@@ -14,7 +14,7 @@ The plugin extends `AbstractAudioFxPlugin`, so it runs identically across browse
- Three parameters: `ceiling` (dB, -60..0), `release` (ms, 1..1000), and `gainReductionDb` (telemetry).
- Linked-stereo brick-wall: gain is shared across all input channels per sample, so a transient on one channel attenuates every channel identically — the stereo image stays phase-coherent.
- Zero-lookahead attack: when `|peak| > ceilingLin` at sample `i`, the gain at sample `i` is set to `ceilingLin / |peak|` *before* writing the output, so the ceiling is a hard contract verifiable by audit.
- Zero-lookahead attack: when `|peak| > ceilingLin` at sample `i`, the gain at sample `i` is set to `ceilingLin / |peak|` _before_ writing the output, so the ceiling is a hard contract verifiable by audit.
- One-pole release: `gain += releaseCoef · (1 − gain)` per sample with `releaseCoef = 1 − exp(-1/(τ · sr))`.
- `gainReductionDb` is updated once per block with the worst-case in-block reduction so meters get a useful peak reading without per-sample state.
- `getGainReductionDb()` accessor mirrors the parameter for low-friction metering.
@@ -65,6 +65,6 @@ setInterval(() => {
The limiter is intentionally a true brick-wall, not a "soft" or "transparent" limiter. We do not interpolate around the threshold or apply a knee. The reason: many use sites layer this plugin downstream of an output-stage compressor that already provides knee shaping, and rely on the limiter for guaranteed bound semantics. Adding a knee would weaken the contract that the test in `output peak never exceeds the configured ceiling` enforces.
There is no lookahead buffer. That keeps the plugin latency-free (`latencySamples: 0`, inherited default) and makes the bound testable by static inspection of the inner loop: at sample `i`, if `peak > ceilingLin` then `gain := ceilingLin / peak` *before* `output[ch][i] = input[ch][i] * gain`. The multiplication therefore can produce at most `|input[ch][i]| * (ceilingLin / |peak|) ≤ ceilingLin`. The downside is that an unusually fast transient (single-sample spike at a very low source sample rate) can produce visible distortion on its leading edge — for those cases a future `LookaheadLimiterPlugin` can sit on a separate subpath without breaking this contract.
There is no lookahead buffer. That keeps the plugin latency-free (`latencySamples: 0`, inherited default) and makes the bound testable by static inspection of the inner loop: at sample `i`, if `peak > ceilingLin` then `gain := ceilingLin / peak` _before_ `output[ch][i] = input[ch][i] * gain`. The multiplication therefore can produce at most `|input[ch][i]| * (ceilingLin / |peak|) ≤ ceilingLin`. The downside is that an unusually fast transient (single-sample spike at a very low source sample rate) can produce visible distortion on its leading edge — for those cases a future `LookaheadLimiterPlugin` can sit on a separate subpath without breaking this contract.
`gainReductionDb` is reported as a per-block peak rather than a running average. Meters typically expect peak; if a future surface wants RMS, it can compute that from the gain-reduction stream by subscribing to a parameter automation channel. The release coefficient is recomputed every block from the current `release` parameter so automation works without introducing zipper noise — the one-pole step is small per sample even when the parameter changes mid-block.

View File

@@ -70,6 +70,6 @@ The distance gain uses the Web Audio inverse-distance model rather than the line
Panning is equal-power (`cos`/`sin`) rather than linear (`1-panNorm`/`panNorm`) because equal-power preserves perceived loudness as a source moves across the stereo field. The mapping `angle = (panNorm + 1) · π / 4` puts `panNorm = -1` (hard left) at `angle = 0` (gain = `cos 0 = 1`, sin 0 = 0`) and`panNorm = 1` (hard right) at `angle = π/2` (gain = `cos π/2 = 0`, sin π/2 = 1). At`panNorm = 0` (centre), both channels get `√½` so the summed energy matches a mono source unchanged.
`orientation` is deliberately *not* a parameter. Parameters in the universal scheme are scalar-valued and automatable; a 3-vector orientation would need three separate parameters that are typically updated together. Exposing `setOrientation` / `getOrientation` keeps the API ergonomic for game-audio callers while preserving the vector across preset round-trip via `SpatialPresetData`. A future revision could add automatable `orientationX/Y/Z` parameters if a real use case emerges.
`orientation` is deliberately _not_ a parameter. Parameters in the universal scheme are scalar-valued and automatable; a 3-vector orientation would need three separate parameters that are typically updated together. Exposing `setOrientation` / `getOrientation` keeps the API ergonomic for game-audio callers while preserving the vector across preset round-trip via `SpatialPresetData`. A future revision could add automatable `orientationX/Y/Z` parameters if a real use case emerges.
Mono input is distance-attenuated only — no pan is applied because there is no L/R to mix between. This matches typical 3D-audio engine behaviour: if a sound is recorded mono, it gets routed through the spatial pipeline but emerges mono unless the engine explicitly upmixes. The `io.outputs[0].count === "preserve"` declaration carries that intent into the chain composition rules.

View File

@@ -116,4 +116,3 @@ registerSpessaSynthPlugin(registry);
## License
MIT.

View File

@@ -14,7 +14,7 @@ The loader is responsible for three things: (1) lazy-resolving the optional upst
- Dynamic `import("spessasynth_lib")` — the only static reference in the codebase, ring-fenced inside `defaultModuleLoader`.
- Injectable `moduleLoader` and `fetchImpl` for hermetic tests.
- Magic-byte sniff (`RIFF…sfbk` or `RIFF…DLS `) — rejects non-SoundFont inputs with `UNSUPPORTED_FORMAT` before invoking the parser.
- Magic-byte sniff (`RIFF…sfbk` or `RIFF…DLS`) — rejects non-SoundFont inputs with `UNSUPPORTED_FORMAT` before invoking the parser.
- `AbortSignal` propagation to the fetch.
- Returns parsed `SoundBankLike` + resolved `SpessaSynthModuleLike` + raw bytes so callers can construct the processor and (optionally) persist the bytes.
- Strict typed error surface: `SOUNDFONT_LOAD_FAILED`, `UNSUPPORTED_FORMAT`, `SOUNDFONT_PARSE_FAILED`, `MODULE_UNAVAILABLE`.

View File

@@ -1,4 +1,4 @@
# @suaveplan/audio-ir-core - Foundation contract for the @suaveplan audio-ir family: ImpulseResponse / IrManifestEntry / IrManifest Zod schemas, in-memory IrRegistry, AUDIO_IR.* error-code registry, and OpenTelemetry surface. Universal; zero @suaveplan/audio-* runtime deps.
# @suaveplan/audio-ir-core - Foundation contract for the @suaveplan audio-ir family: ImpulseResponse / IrManifestEntry / IrManifest Zod schemas, in-memory IrRegistry, AUDIO_IR.*error-code registry, and OpenTelemetry surface. Universal; zero @suaveplan/audio-* runtime deps.
Foundation contract for the `@suaveplan/audio-ir-*` family. Universal;
zero `@suaveplan/audio-*` runtime deps. Every sibling package
@@ -107,4 +107,3 @@ compose multiple content packages — or fan out to multiple resolvers
needing to agree on resolver shape. See
[`src/registrar/registrar.md`](./src/registrar/registrar.md) for the full
API reference.

View File

@@ -53,7 +53,6 @@ The pipeline + dispatch + cache decisions live at
the cross-cutting IR-family model lives at
## Quick install
```ts
@@ -76,4 +75,3 @@ const loader = createIrLoader({
});
const ir = await loader.load("club-club1");
```

View File

@@ -83,4 +83,3 @@ 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

@@ -71,4 +71,3 @@ The CDN copy lives at `https://swch.b-cdn.net/_/audio/ir/room/roompool.wav`.
License: **All Rights Reserved — Suaveplan internal media library**. Do
not redistribute the bundled WAV outside Suaveplan distributions.

View File

@@ -34,10 +34,10 @@ import { AudioDocument } from "@suaveplan/audio-core";
import { BeatDetector } from "@suaveplan/audio-analysis-beat";
const doc = new AudioDocument({
sampleRate: 48_000,
channels: 1,
sourceLength: pcm.length,
pages: [{ channelData: [pcm], startSample: 0 }],
sampleRate: 48_000,
channels: 1,
sourceLength: pcm.length,
pages: [{ channelData: [pcm], startSample: 0 }],
});
const result = await new BeatDetector().analyze(doc);
@@ -65,4 +65,3 @@ console.log(result.bpm, result.beats.length, result.confidence);
See per-module `.md` files for the deep documentation of each algorithm and
entry point.

View File

@@ -30,9 +30,9 @@ gives a confidence near 1, scattered onsets give one near 0.
import { buildBeatGrid } from "@suaveplan/audio-analysis-beat/algorithms/beat-grid";
const result = buildBeatGrid({
period: 60 / 120, // 120 BPM
duration: 10,
onsets: [0.5, 1.0, 1.5, 2.0],
period: 60 / 120, // 120 BPM
duration: 10,
onsets: [0.5, 1.0, 1.5, 2.0],
});
console.log(result.beats.length, result.phase, result.confidence);
```
@@ -46,7 +46,7 @@ import { buildBeatGrid } from "@suaveplan/audio-analysis-beat/algorithms/beat-gr
// more tightly — this is how the BeatDetector breaks ties between candidate
// tempi when an autocorrelation sweep returns two near-equal peaks.
const onsets = new Float64Array([
0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0,
0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0,
]);
const a = buildBeatGrid({ period: 60 / 120, duration: 4.2, onsets, phaseTrials: 48 });

View File

@@ -29,9 +29,9 @@ import { combTempo } from "@suaveplan/audio-analysis-beat/algorithms/comb-tempo"
// ODF sampled every 1024 samples at 48 kHz → hopSeconds = 1024/48000.
const result = combTempo(odf, {
minBpm: 60,
maxBpm: 200,
hopSeconds: 1024 / 48_000,
minBpm: 60,
maxBpm: 200,
hopSeconds: 1024 / 48_000,
});
console.log(result.bpm); // e.g. 120
```
@@ -40,8 +40,8 @@ console.log(result.bpm); // e.g. 120
```ts
import {
combScore,
combTempo,
combScore,
combTempo,
} from "@suaveplan/audio-analysis-beat/algorithms/comb-tempo";
// Draw a tempo-confidence curve over the search range manually:
@@ -50,8 +50,8 @@ const min = 60;
const max = 200;
const curve = new Float64Array(max - min + 1);
for (let bpm = min; bpm <= max; bpm++) {
const period = Math.max(1, Math.round((60 / bpm) / hopSeconds));
curve[bpm - min] = combScore(odf, period);
const period = Math.max(1, Math.round((60 / bpm) / hopSeconds));
curve[bpm - min] = combScore(odf, period);
}
// Fall back to combTempo for the canonical winner including octave handling.

View File

@@ -37,8 +37,8 @@ const odf = energyOdf(energies);
```ts
import {
blockEnergy,
energyOdfFromSamples,
blockEnergy,
energyOdfFromSamples,
} from "@suaveplan/audio-analysis-beat/algorithms/energy-odf";
const sampleRate = 48_000;
@@ -53,10 +53,10 @@ const hopSeconds = blockSize / sampleRate;
let bestIdx = 0;
let best = 0;
for (let i = 0; i < Math.floor(1 / hopSeconds); i++) {
if (odf[i] > best) {
best = odf[i];
bestIdx = i;
}
if (odf[i] > best) {
best = odf[i];
bestIdx = i;
}
}
console.log("onset at", bestIdx * hopSeconds, "seconds");
console.log("avg first-block energy", blockEnergy([left.subarray(0, blockSize)]));

View File

@@ -41,8 +41,8 @@ console.log(odf.length, "frames"); // ceil(192000 / 512) = 375
```ts
import {
magnitudeSpectrum,
spectralFlux,
magnitudeSpectrum,
spectralFlux,
} from "@suaveplan/audio-analysis-beat/algorithms/spectral-flux";
const fftSize = 2048;
@@ -55,8 +55,8 @@ const mono = new Float32Array(48_000 * 10); // 10 seconds
// thread per-frame work through other code, or thin the ODF on the fly.
const odfValues: number[] = [];
for (let start = 0; start + fftSize <= mono.length; start += hop) {
buf.set(mono.subarray(start, start + fftSize));
odfValues.push(spectralFlux(buf, prev, fftSize));
buf.set(mono.subarray(start, start + fftSize));
odfValues.push(spectralFlux(buf, prev, fftSize));
}
// Verify the magnitude spectrum has the expected shape on its own.

View File

@@ -42,10 +42,10 @@ import { AudioDocument } from "@suaveplan/audio-core";
import { BeatDetector } from "@suaveplan/audio-analysis-beat/detector/beat-detector";
const doc = new AudioDocument({
sampleRate: 48_000,
channels: 1,
sourceLength: 48_000 * 4,
pages: [{ startSample: 0, channelData: [Array.from(/* 4 s mono */)] }],
sampleRate: 48_000,
channels: 1,
sourceLength: 48_000 * 4,
pages: [{ startSample: 0, channelData: [Array.from(/* 4 s mono */)] }],
});
const detector = new BeatDetector();
@@ -60,11 +60,11 @@ import { AudioDocument } from "@suaveplan/audio-core";
import { BeatDetector } from "@suaveplan/audio-analysis-beat/detector/beat-detector";
const detector = new BeatDetector({
algorithm: "spectral-flux",
minBpm: 80,
maxBpm: 160,
fftSize: 2048,
hopSize: 1024,
algorithm: "spectral-flux",
minBpm: 80,
maxBpm: 160,
fftSize: 2048,
hopSize: 1024,
});
const controller = new AbortController();
@@ -73,10 +73,10 @@ setTimeout(() => controller.abort(), 5_000);
// Streaming refinement
let final;
for await (const partial of detector.analyzeStream(doc, {
signal: controller.signal,
signal: controller.signal,
})) {
final = partial;
console.log("BPM so far:", partial.bpm, partial.confidence);
final = partial;
console.log("BPM so far:", partial.bpm, partial.confidence);
}
// Or drive it through the audio-core AnalysisOp surface

View File

@@ -34,13 +34,13 @@ right log level without re-deriving it.
```ts
import {
AUDIO_ANALYSIS_BEAT_CODES,
AudioAnalysisBeatError,
AUDIO_ANALYSIS_BEAT_CODES,
AudioAnalysisBeatError,
} from "@suaveplan/audio-analysis-beat/error-codes";
throw new AudioAnalysisBeatError(
AUDIO_ANALYSIS_BEAT_CODES.EMPTY_DOCUMENT,
"audio-analysis-beat.empty-document",
AUDIO_ANALYSIS_BEAT_CODES.EMPTY_DOCUMENT,
"audio-analysis-beat.empty-document",
);
```
@@ -48,21 +48,21 @@ throw new AudioAnalysisBeatError(
```ts
import {
AUDIO_ANALYSIS_BEAT_CODES,
AUDIO_ANALYSIS_BEAT_ERROR_METADATA,
AudioAnalysisBeatError,
AUDIO_ANALYSIS_BEAT_CODES,
AUDIO_ANALYSIS_BEAT_ERROR_METADATA,
AudioAnalysisBeatError,
} from "@suaveplan/audio-analysis-beat/error-codes";
function logError(err: unknown): void {
if (!(err instanceof AudioAnalysisBeatError)) throw err;
const meta = AUDIO_ANALYSIS_BEAT_ERROR_METADATA[err.code];
console[meta.severity === "warning" ? "warn" : "error"](
`[${err.category}] ${err.code}: ${meta.description}`,
{ context: err.context, cause: err.cause },
);
if (err.code === AUDIO_ANALYSIS_BEAT_CODES.ABORTED) return;
if (err.code === AUDIO_ANALYSIS_BEAT_CODES.NO_BEATS_DETECTED) return;
throw err;
if (!(err instanceof AudioAnalysisBeatError)) throw err;
const meta = AUDIO_ANALYSIS_BEAT_ERROR_METADATA[err.code];
console[meta.severity === "warning" ? "warn" : "error"](
`[${err.category}] ${err.code}: ${meta.description}`,
{ context: err.context, cause: err.cause },
);
if (err.code === AUDIO_ANALYSIS_BEAT_CODES.ABORTED) return;
if (err.code === AUDIO_ANALYSIS_BEAT_CODES.NO_BEATS_DETECTED) return;
throw err;
}
```

View File

@@ -53,4 +53,3 @@ console.log({
## License
MIT

View File

@@ -60,4 +60,3 @@ All errors are `AudioAnalysisError`s carrying one of the `AUDIO_ANALYSIS_PITCH.*
## License
MIT

View File

@@ -59,4 +59,3 @@ console.log(result.centroid, result.rolloff, result.flux);
## Status
`0.1.0`. Pre-publish gate (lint + typecheck + test) and 100% coverage are required on every change.

View File

@@ -101,4 +101,3 @@ Stable mapping from `AUDIO_CLI.*` codes onto Unix exit codes:
- 100/100/100/100 coverage on every implementation file.
- Zero mocks — tests drive the real CLI against real `mkdtemp` temp files
and the real `@suaveplan/audio-engine-server` adapters.

View File

@@ -78,4 +78,3 @@ Eight codes namespaced under `AUDIO_CODEC_FFMPEG_NATIVE.*`:
`0.1.0` — first-tier landing. Subpath exports cover every implementation
module so consumers can tree-shake. See the package's OpenSpec change for
the full specification.

View File

@@ -124,4 +124,3 @@ For testing without a real worker, supply:
These are dependency-injection seams, not mocks; production callers
omit them and the real `new Worker(coreUrl)` / dynamic `import()` path
runs unchanged.

View File

@@ -69,10 +69,10 @@ modules.
## Implementation Notes
The COI check intentionally happens *before* the dynamic `import()` so
The COI check intentionally happens _before_ the dynamic `import()` so
that callers without the `@ffmpeg/core-mt` optional dep installed never
incur a module-not-found warning when the COI gate is the real failure.
The fallback path runs only when COI is true *and* the mt import rejects
The fallback path runs only when COI is true _and_ the mt import rejects
— if COI is true and mt loads successfully, no fallback occurs. The
`crossOriginIsolated` override exists so tests can drive both branches
deterministically without manipulating the global.

View File

@@ -7,7 +7,7 @@ _(no submodules)_
## Purpose
This module owns the pure logic of the encode pipeline: validating the
requested format against the supported set *before* the worker is touched,
requested format against the supported set _before_ the worker is touched,
interleaving per-channel PCM into a single `Float32Array`, projecting
that into ffmpeg's `f32le` wire format, building the format-specific
ffmpeg command for each of `wav` / `mp3` / `ogg` / `opus` / `flac` /

View File

@@ -100,7 +100,7 @@ if (fmt === "caf") {
- The format ID at offset 8 of `desc` is `lpcm` (`0x6c70636d`) for
uncompressed integer PCM. The decoder does not currently dispatch on
this field — it assumes `lpcm` and decodes accordingly. Other format
IDs (`alac`, `aac `, `lpcm` floats) would produce wrong output;
IDs (`alac`, `aac`, `lpcm` floats) would produce wrong output;
callers needing compressed CAF support should use the corresponding
WASM codec for the inner format.
- Sample byte order is determined per sample by the

View File

@@ -24,7 +24,7 @@ storage format (PCM int vs IEEE float).
- `WavCodec.canDecode(bytes)` — magic-byte sniff: returns `true` iff the
buffer is at least 12 bytes and starts with `RIFF` … `WAVE`.
- `WavCodec.decode(bytes)` — parses the RIFF header, walks the chunk list
for `fmt ` and `data`, decodes the PCM, and returns a `DecodeResult`
for `fmt` and `data`, decodes the PCM, and returns a `DecodeResult`
with `channelData`, `sampleRate`, and `format: "wav"`.
- `WavCodec.encode(channelData, opts?)` — emits a complete RIFF WAV from
PCM channels at the requested `sampleRate`, `bitDepth`, and
@@ -106,7 +106,7 @@ encoder.close();
- `canDecode(bytes: Uint8Array): boolean`
- `decode(bytes: Uint8Array, opts?: DecodeOpts): Promise<DecodeResult>` —
throws `AudioCodecsError("AUDIO_CODECS.DECODE_FAILED")` on bad magic,
missing `fmt ` chunk, missing `data` chunk, truncated `data`, fmt size
missing `fmt` chunk, missing `data` chunk, truncated `data`, fmt size
< 16, or unsupported `bitsPerSample` (`bps === 0` or `bps > 4`).
- `encode(channelData, opts?: EncodeOpts): Promise<Uint8Array>` — throws
`AudioCodecsError("AUDIO_CODECS.ENCODE_FAILED")` if `channelData` is
@@ -120,7 +120,7 @@ encoder.close();
## Implementation Notes
- The RIFF chunk walker tolerates and skips any chunk whose ID is neither
`fmt ` nor `data` — `LIST`, `bext`, `iXML`, and other metadata chunks
`fmt` nor `data` — `LIST`, `bext`, `iXML`, and other metadata chunks
are passed over silently. This matches the behaviour every real WAV
consumer expects.
- 24-bit PCM uses three signed-magnitude bytes per sample in

View File

@@ -72,4 +72,3 @@ and `bindParameter` integration with the FX plugin family.
## Status
`0.1.0` — Wave 1 migration. 100/100/100/100 coverage.

View File

@@ -98,4 +98,3 @@ for await (const block of doc.stream()) {
- [`src/stats/stats.md`](src/stats/stats.md) — `computeStats` for peak, RMS, and DC offset
- [`src/stream/stream.md`](src/stream/stream.md) — `createPlanTransformStream` for downstream transforms
- [`src/error-codes.md`](src/error-codes.md) — Error codes and `AudioError` class

View File

@@ -78,4 +78,3 @@ try {
- Resampling — surfaced as `RESAMPLE_UNSUPPORTED`; a future `@suaveplan/audio-resample` will pair with this package.
- Non-WAV formats — sibling `audio-decode-{mp3,flac,opus,…}` packages each wrap their own upstream decoder behind the same contract.
- Format dispatching — owned by the upcoming `@suaveplan/audio-decode` umbrella package.

View File

@@ -111,4 +111,3 @@ adapter.dispose();
(`audio_engine_browser_offline_render_duration_ms`).
- 100% test coverage; tests run under `happy-dom` with an injected
context shim.

View File

@@ -69,4 +69,3 @@ exporter is registered and has no import-time side effects.
See the specification for the full
requirement set.

View File

@@ -85,4 +85,3 @@ Both gates hard-fail below 100% coverage. The Python test suite is not currently
- `@suaveplan/react-audio-fingerprint` — the React 19 sibling that runs the browser-tier fingerprinter and, via this package's `python-backend` integration, consults this service for high-accuracy matching.
- `@suaveplan/audio-analysis-spectrum` — the universal FFT/spectrogram primitives the browser-tier fingerprinter is built on.

File diff suppressed because one or more lines are too long

View File

@@ -51,4 +51,3 @@ console.log(jump.length); // encoded byte length
- [`src/types/common.md`](src/types/common.md) — `AudioAdapter` interface, Zod schemas, and public types
- [`src/error-codes.md`](src/error-codes.md) — Error codes and `AudioSpriteError` class
- [`src/__benchmarks__/audio-sprite-packer.bench.md`](src/__benchmarks__/audio-sprite-packer.bench.md) — Benchmark suite notes

View File

@@ -77,4 +77,3 @@ See the co-located `.md` files inside `src/`:
- [`telemetry/telemetry.md`](./src/telemetry/telemetry.md)
- [`schemas.md`](./src/schemas.md)
- [`error-codes.md`](./src/error-codes.md)

View File

@@ -38,4 +38,3 @@ Headless DAW timeline engine with multi-track arrangements, clips, playback, loo
- [zoom](./src/zoom/zoom.md) — The `zoom.ts` module provides a discrete zoom table and navigation helpers for the timeline view. Zoom levels are expre…
- [zoom](./src/zoom/zoom.md) — The `zoom.ts` module provides a discrete zoom table and navigation helpers for the timeline view. Zoom levels are expre…
<!-- end:auto-docs -->

View File

@@ -55,4 +55,3 @@ scheduler.start();
- [Signal](src/signal/signal.md) — tempo automation curves
- [Time](src/time/time.md) — musical time parsing and conversion
- [Error Codes](src/error-codes.md) — structured error registry

View File

@@ -207,4 +207,3 @@ const traktor = {
- [Upgrade](./src/upgrade/upgrade.md)
- [Walk](./src/walk/walk.md)
- [Identity](./src/identity/identity.md)

View File

@@ -87,4 +87,3 @@ const messages = decoder.drain();
## Coverage gate
`bunx turbo test:coverage --filter=@suaveplan/midi` enforces 100/100/100/100 (statements / branches / functions / lines).

View File

@@ -8,7 +8,7 @@ _(no submodules)_
`parseSysEx` and `buildSysEx` are the canonical helpers for working with MIDI System Exclusive payloads — the manufacturer-defined byte streams that sit between an `F0` prefix and an `F7` terminator. Every vendor uses SysEx for patch dumps, parameter sysex, MMC (MIDI Machine Control), and identity-request handshakes; standardising the parse / build code in one place prevents off-by-one errors with the one-byte vs three-byte manufacturer-id forms.
`parseSysEx` expects the *body* of the frame (i.e. everything between `F0` and `F7`, exclusive of both endpoints). `buildSysEx` produces a complete `F0...F7` frame; callers feed the result straight to `MidiPortLike.send`.
`parseSysEx` expects the _body_ of the frame (i.e. everything between `F0` and `F7`, exclusive of both endpoints). `buildSysEx` produces a complete `F0...F7` frame; callers feed the result straight to `MidiPortLike.send`.
## Features

View File

@@ -6,7 +6,7 @@ _(no submodules)_
## Purpose
MIDI 2.0 introduces the Universal MIDI Packet (UMP) format — fixed-width 32, 64, 96, or 128 bit packets that carry MIDI messages alongside high-resolution per-note controllers, per-note pitch, and 32-bit velocity. This module exposes the four packet types, the message-type nibble constants, and a *lossless* bridge between MIDI 1.0 messages and their UMP equivalents.
MIDI 2.0 introduces the Universal MIDI Packet (UMP) format — fixed-width 32, 64, 96, or 128 bit packets that carry MIDI messages alongside high-resolution per-note controllers, per-note pitch, and 32-bit velocity. This module exposes the four packet types, the message-type nibble constants, and a _lossless_ bridge between MIDI 1.0 messages and their UMP equivalents.
The bridge is deliberately one-way for any MIDI 2.0 feature that exceeds MIDI 1.0's expressivity: a MIDI 2.0 channel-voice packet with 32-bit velocity cannot be downgraded to MIDI 1.0 without losing precision, so `umpToMidi1` rejects it with `MidiError("MIDI.UNSUPPORTED_UMP_VARIANT")` rather than silently truncating. Engines that want to "best-effort downgrade" wrap this bridge with their own approximation policy.

View File

@@ -14,4 +14,3 @@
- [types](./src/types/types.md) — Shared types for `@suaveplan/third-party-elevenlabs`. Defines the request / response envelope shapes consumed by the tr…
- [types](./src/types/types.md) — Shared types for `@suaveplan/third-party-elevenlabs`. Defines the request / response envelope shapes consumed by the tr…
<!-- end:auto-docs -->

View File

@@ -143,7 +143,7 @@ WebSocket:
consistently without per-call wiring.
- `MultipartFile` accepts URLs as strings; the client fetches them via
the injected `fetchImpl` before uploading. This is the only place in
the transport that issues a *secondary* network call from a single
the transport that issues a _secondary_ network call from a single
caller invocation.
- `RequestMetadata` keeps every field optional because some endpoints
do not surface `X-RateLimit-Remaining` or `X-Character-Cost`.

View File

@@ -140,4 +140,3 @@ bunx turbo test:coverage --filter=@suaveplan/voice-elevenlabs
```
Coverage gate: 100/100/100/100 statements/branches/functions/lines.

View File

@@ -85,4 +85,3 @@ const result = await synth.synthesize({
Provider-specific extensions (voice cloning, conversational AI, music,
dubbing, voice changing) ship in their own packages under separate
abstractions when those domains stabilise across two or more vendors.

View File

@@ -21,7 +21,9 @@ Designed for Three.js / game-engine font pipelines where fonts must be shipped a
- [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
- [parse-path](./src/utils/parse-path.md) — Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph o…
<!-- end:auto-docs --> - [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
- [parse-path](./src/utils/parse-path.md) — Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph o…
- [error-codes](./src/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
- [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
@@ -108,4 +110,3 @@ bunx suaveplan-font-compress ./fonts -o ./out --force
Exit codes: `0` success, `1` per-file failure(s), `2` invalid CLI args,
`3` missing input path.

View File

@@ -15,7 +15,9 @@ Converts TTF/OTF/WOFF/WOFF2 font files to the [typeface.js](https://gero3.github
- [typeface](./src/types/typeface.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [outline-to-string](./src/utils/outline-to-string.md) — Serialises an array of `PathCommand` objects to the space-separated token string stored in a `TypefaceGlyph`'s `o` fiel…
- [reverse-winding](./src/utils/reverse-winding.md) — Reverses the winding order of a typeface.js path command sequence. Three.js `FontLoader` expects counter-clockwise wind…
<!-- end:auto-docs --> - [batch-converter](./src/core/batch-converter/batch-converter.md) — <!-- begin:auto-docs --> _(no submodules)_- [font-converter](./src/core/font-converter/font-converter.md) — <!-- begin:auto-docs --> _(no submodules)_- [font-converter](./src/core/font-converter/font-converter.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/typeface.md) — <!-- begin:auto-docs --> _(no submodules)_- [typeface](./src/types/typeface.md) — <!-- begin:auto-docs --> _(no submodules)_- [outline-to-string](./src/utils/outline-to-string.md) — Serialises an array of `PathCommand` objects to the space-separated token string stored in a `TypefaceGlyph`'s `o` fiel…
- [reverse-winding](./src/utils/reverse-winding.md) — Reverses the winding order of a typeface.js path command sequence. Three.js `FontLoader` expects counter-clockwise wind…
- [batch-converter](./src/core/batch-converter/batch-converter.md) — <!-- begin:auto-docs --> _(no submodules)_- [font-converter](./src/core/font-converter/font-converter.md) — <!-- begin:auto-docs --> _(no submodules)_- [font-converter](./src/core/font-converter/font-converter.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/typeface.md) — <!-- begin:auto-docs --> _(no submodules)_- [typeface](./src/types/typeface.md) — <!-- begin:auto-docs --> _(no submodules)_- [outline-to-string](./src/utils/outline-to-string.md) — Serialises an array of `PathCommand` objects to the space-separated token string stored in a `TypefaceGlyph`'s `o` fiel…
- [reverse-winding](./src/utils/reverse-winding.md) — Reverses the winding order of a typeface.js path command sequence. Three.js `FontLoader` expects counter-clockwise wind…

View File

@@ -128,4 +128,3 @@ commit.
tests only. The runtime dependency graph is `font-decompress →
font-convert + error`; the test surface adds `font-compress` solely
to generate known-vector fixtures.

View File

@@ -15,7 +15,9 @@ Font subsetting and compression library with a codec-agnostic adapter interface.
- [common](./src/types/common.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [format-detect](./src/utils/format-detect.md) — Provides magic-byte font format detection from raw binary data. Before compressing or probing a font file, `FontCompres…
- [unicode-ranges](./src/utils/unicode-ranges.md) — Parses CSS/OpenType-style Unicode range notation into a `Set<number>` of integer codepoints. Font subsetting workflows…
<!-- end:auto-docs --> - [null](./src/adapters/null/null.md) — <!-- begin:auto-docs --> _(no submodules)_- [font-compressor](./src/core/font-compressor/font-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_- [font-compressor](./src/core/font-compressor/font-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/common.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — <!-- begin:auto-docs --> _(no submodules)_- [format-detect](./src/utils/format-detect.md) — Provides magic-byte font format detection from raw binary data. Before compressing or probing a font file, `FontCompres…
- [unicode-ranges](./src/utils/unicode-ranges.md) — Parses CSS/OpenType-style Unicode range notation into a `Set<number>` of integer codepoints. Font subsetting workflows…
- [null](./src/adapters/null/null.md) — <!-- begin:auto-docs --> _(no submodules)_- [font-compressor](./src/core/font-compressor/font-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_- [font-compressor](./src/core/font-compressor/font-compressor.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/common.md) — <!-- begin:auto-docs --> _(no submodules)_- [common](./src/types/common.md) — <!-- begin:auto-docs --> _(no submodules)_- [format-detect](./src/utils/format-detect.md) — Provides magic-byte font format detection from raw binary data. Before compressing or probing a font file, `FontCompres…
- [unicode-ranges](./src/utils/unicode-ranges.md) — Parses CSS/OpenType-style Unicode range notation into a `Set<number>` of integer codepoints. Font subsetting workflows…

View File

@@ -98,4 +98,3 @@ Universal — no DOM or Node-only APIs at the package boundary.
`sideEffects: false`. Internal deps: `@suaveplan/crypto-hash`,
`@suaveplan/error`, `@suaveplan/types`. External deps:
`@opentelemetry/api`, `tslib`, `zod`.

View File

@@ -60,8 +60,8 @@ calculateDimensions(pixel, 9999);
### `calculateDimensions(parsed: ParsedImgproxyUrl, defaultWidth: number): ImgproxyDimensions`
- **Parameters:**
- `parsed` — output of `parseImgproxyUrl`.
- `defaultWidth` — width used when the URL is in aspect-ratio mode
- `parsed` — output of `parseImgproxyUrl`.
- `defaultWidth` — width used when the URL is in aspect-ratio mode
without a `w`/`h` hint.
- **Returns:** `{ width, height }`, both non-negative integers. Either
axis may be `0` (meaning "auto / unspecified").

View File

@@ -60,9 +60,9 @@ operationToImgproxy(parsed.operation, width, height, parsed.gravity);
### `operationToImgproxy(op: ImgproxyOperation, width: number, height: number, gravity: string): string`
- **Parameters:**
- `op` — one of `""|cover|crop|fit|stretch|resize`.
- `width`, `height` — non-negative integers; either may be zero.
- `gravity` — a grammar code accepted by `gravityToImgproxy`.
- `op` — one of `""|cover|crop|fit|stretch|resize`.
- `width`, `height` — non-negative integers; either may be zero.
- `gravity` — a grammar code accepted by `gravityToImgproxy`.
- **Returns:** the imgproxy processing string, or `""` when the
operation cannot be expressed (e.g. `crop` without both dims).

View File

@@ -76,10 +76,10 @@ function tryParse(path: string): string | null {
- **Parameters:** `path` — the URL path including leading `/`.
- **Returns:** a frozen `ParsedImgproxyUrl` (see `types/types.ts`).
- **Throws:**
- `CaddyImgproxyError(INVALID_URL)` when the regex rejects the
- `CaddyImgproxyError(INVALID_URL)` when the regex rejects the
input — bad prefix, malformed dimensions, quality outside
`1..100`.
- `CaddyImgproxyError(MISSING_DIMENSIONS)` when an operation
- `CaddyImgproxyError(MISSING_DIMENSIONS)` when an operation
(`v|c|f|s|r`) is present without any dimension grammar.
## Implementation Notes

View File

@@ -68,14 +68,14 @@ const sig = signImgproxyPath({
### `signImgproxyPath(options: SignImgproxyPathOptions): string`
- **Parameters:**
- `options.key` — lower-hex HMAC key, even length, non-empty.
- `options.salt` — lower-hex salt, even length, non-empty.
- `options.path` — the imgproxy path being signed.
- `options.hash` — optional `SignerHashLike` injection seam.
- `options.key` — lower-hex HMAC key, even length, non-empty.
- `options.salt` — lower-hex salt, even length, non-empty.
- `options.path` — the imgproxy path being signed.
- `options.hash` — optional `SignerHashLike` injection seam.
- **Returns:** base64url-encoded signature, no padding.
- **Throws:**
- `CaddyImgproxyError(INVALID_KEY)` for malformed / empty / odd-length key.
- `CaddyImgproxyError(INVALID_SALT)` for malformed / empty / odd-length salt.
- `CaddyImgproxyError(INVALID_KEY)` for malformed / empty / odd-length key.
- `CaddyImgproxyError(INVALID_SALT)` for malformed / empty / odd-length salt.
### `SignerHashLike`

View File

@@ -70,10 +70,10 @@ const url = buildImgproxyUrl({ parsed, domain: "example.com", config });
### `buildImgproxyUrl(options: BuildImgproxyUrlOptions): string`
- **Parameters:**
- `options.parsed` — output of `parseImgproxyUrl`.
- `options.domain` — virtual-host segment for the S3 path.
- `options.config` — validated `ImgproxyConfig`.
- `options.hash` — optional signer hash override.
- `options.parsed` — output of `parseImgproxyUrl`.
- `options.domain` — virtual-host segment for the S3 path.
- `options.config` — validated `ImgproxyConfig`.
- `options.hash` — optional signer hash override.
- **Returns:** the full imgproxy path including the signature prefix.
### `BuildImgproxyUrlOptions`

View File

@@ -62,4 +62,3 @@ console.log(c.contrast("#ffffff")); // WCAG contrast ratio
- `@suaveplan/colours/utils` — clamping and rounding utilities
- `@suaveplan/colours/errors` — `ColourError` and factory functions
- `@suaveplan/colours/error-codes` — `COLOUR_CODES` registry

View File

@@ -131,4 +131,3 @@ Bun, Node, Deno, and modern browsers.
## License
MIT — Suaveplan Team.

View File

@@ -65,7 +65,7 @@ function pickBackgroundColour(pixels, w, h) {
The flood-fill uses an explicit stack (`pop()`) rather than recursion to
avoid blowing the call stack on large emblems. We `add` to `visited`
*before* enqueueing the neighbour, which guarantees each pixel is touched
_before_ enqueueing the neighbour, which guarantees each pixel is touched
at most twice (once when discovered, once when its packed index is popped).
The neighbour set is computed as a small inline array of `-1`-sentinel
checks for the four cardinal directions; benchmarks against a `for (const

View File

@@ -62,6 +62,6 @@ The inner `tryMerge` first tests the horizontal merge (same `y`, same
against `available` before accepting; this avoids painting through
transparent pixels. The outer loop terminates when a full pass produces
zero merges, so each call is idempotent. We deliberately do not try
*non*-same-extent merges here — that is the job of `optimiseOverlap`,
_non_-same-extent merges here — that is the job of `optimiseOverlap`,
which can produce overlapping output and is therefore a strictly stronger
(but more expensive) operation.

View File

@@ -18,7 +18,7 @@ on the layer list and is the only pass that mutates `layer.rects`.
- Composites every layer into a per-cell `target` grid first (lower-z to
higher-z), then walks rect by rect deciding whether removal would
change the composited output.
- A rect is removed iff *every* one of its required cells is either
- A rect is removed iff _every_ one of its required cells is either
covered by a sibling rect at the same z-index or overdrawn by a higher-z
rect of any colour.
- In-place mutation via `Array.prototype.splice` — the converter calls

View File

@@ -61,7 +61,7 @@ const cover = perIslandCover(info.islands, candidates, w, h);
## Implementation Notes
The centroid term is computed against the running set of *uncovered*
The centroid term is computed against the running set of _uncovered_
required pixels, not the original set — this keeps selections clustered
even as the residual shrinks. We use a precomputed `maxDist =
sqrt(w² + h²)` so the proximity term is normalised to `[0, 1]`; without

View File

@@ -59,7 +59,7 @@ const cover = enhancedGreedyCover(candidates, required, cols, rows);
The internal `histogramSweep` builds a column-by-column heights table and
then walks each row with a monotonic stack. When the stack pops, every
height ≥ the popped column's height yields a distinct maximal rect of
width `x - leftBound` and height `rectH`. We emit *every* nested height so
width `x - leftBound` and height `rectH`. We emit _every_ nested height so
the greedy cover can pick a smaller-but-better-positioned rect when the
maximal-tall one would force an awkward residual. The transposed sweep is
implemented as a fresh `boolean[][]` rather than an index permutation

View File

@@ -77,7 +77,7 @@ The strip strategy only contributes when it fully covers the required
mask. We compute coverage by intersecting each valid strip with the
`required` grid (via `pack`) and comparing the resulting count to a
single pre-tallied `totalRequired`. The adjacent-rect merge pass runs
*after* the strategies have produced their winners; running it earlier
_after_ the strategies have produced their winners; running it earlier
would force a merge on results that would have been pruned anyway. The
final reduce uses a strictly-less-than comparison (`a.length <= b.length`)
so the first result wins on ties — this preserves deterministic ordering

View File

@@ -85,4 +85,3 @@ console.log(result.finalColours); // colour count after quantisation
## License
MIT

View File

@@ -56,4 +56,3 @@ const result = proc.planTransform(meta, {
- [`src/types/image-detect/image-detect.md`](./src/types/image-detect/image-detect.md) — Format detection types and constants
- [`src/errors/errors.md`](./src/errors/errors.md) — `ImageError` hierarchy and typed factories
- [`src/error-codes/error-codes.md`](./src/error-codes/error-codes.md) — Stable `IMAGE.*` error code registry

View File

@@ -82,4 +82,3 @@ console.log(sheet.frameCount, sheet.width, sheet.height);
## License
MIT

View File

@@ -106,4 +106,3 @@ console.log(result); // { width: 1280, height: 720, format: "webm", duration: 60
## License
MIT

View File

@@ -17,7 +17,7 @@ spoofable) `Content-Type` header.
preserves the upstream sniffer byte-for-byte so consumers porting
from the legacy package see identical detection results. The supported
formats are MP4 (ISO Base Media `ftyp`), MOV (`ftyp` with `qt` brand),
Matroska / WebM (EBML `1A 45 DF A3`), RIFF AVI (`RIFF…AVI `), Adobe FLV
Matroska / WebM (EBML `1A 45 DF A3`), RIFF AVI (`RIFF…AVI`), Adobe FLV
(`FLV`), and Ogg (`OggS`). Anything else returns `null` and the caller is
expected to surface `VideoError(VIDEO.UNKNOWN_FORMAT)` or accept a
caller-supplied format hint.