updated skills
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -116,4 +116,3 @@ registerSpessaSynthPlugin(registry);
|
||||
## License
|
||||
|
||||
MIT.
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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");
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)]));
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -53,4 +53,3 @@ console.log({
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
@@ -60,4 +60,3 @@ All errors are `AudioAnalysisError`s carrying one of the `AUDIO_ANALYSIS_PITCH.*
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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` /
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -72,4 +72,3 @@ and `bindParameter` integration with the FX plugin family.
|
||||
## Status
|
||||
|
||||
`0.1.0` — Wave 1 migration. 100/100/100/100 coverage.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -69,4 +69,3 @@ exporter is registered and has no import-time side effects.
|
||||
|
||||
See the specification for the full
|
||||
requirement set.
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -207,4 +207,3 @@ const traktor = {
|
||||
- [Upgrade](./src/upgrade/upgrade.md)
|
||||
- [Walk](./src/walk/walk.md)
|
||||
- [Identity](./src/identity/identity.md)
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -140,4 +140,3 @@ bunx turbo test:coverage --filter=@suaveplan/voice-elevenlabs
|
||||
```
|
||||
|
||||
Coverage gate: 100/100/100/100 statements/branches/functions/lines.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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…
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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…
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -131,4 +131,3 @@ Bun, Node, Deno, and modern browsers.
|
||||
## License
|
||||
|
||||
MIT — Suaveplan Team.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -85,4 +85,3 @@ console.log(result.finalColours); // colour count after quantisation
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -82,4 +82,3 @@ console.log(sheet.frameCount, sheet.width, sheet.height);
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
@@ -106,4 +106,3 @@ console.log(result); // { width: 1280, height: 720, format: "webm", duration: 60
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user