kithara resampler - zvuk/kithara GitHub Wiki
Documentation reviewed from source revision 4ba0bb5bc. This records the documented contract at that revision; it is not a new runtime validation. API and usage · All crates.
Detailed contracts and invariants for the kithara-resampler crate; the README is the overview.
This crate owns the platform-neutral standalone sample-rate resampling
contract: Resampler (processing), ResamplerBackend (factory trait),
ResamplerCapabilities, ResamplerMode, the construction configs, and the
error types. Platform backend contracts such as Apple AudioConverter live
here; shared Apple ABI and safe AudioToolbox wrappers come from kithara-apple.
Backend choice is encoded by the caller's B: ResamplerBackend type parameter.
Playback, analysis, and decoder configs thread that type through their own
builders; this crate exposes no cfg-selected default backend handle.
NoResamplerBackend is the explicit "no backend" type — it advertises empty
capabilities and fails at build, it is not a fallback.
Decoder placement is not owned here: kithara-decode decides whether a decoder
uses a codec-embedded converter or wraps decoded audio in a decoder adapter. Codec
lifecycle, media input, and gapless translation stay there. Playback graph
routing is not owned here either: kithara-audio passes device-rate config into
decode, while kithara-analysis owns analysis-side use; neither crate contains
backend-specific resampler policy.
Backends advertising REALTIME_SAFE must not allocate through ordinary Vec
growth. Backends use one of these storage owners:
- caller-owned input and output slices passed to
Resampler::process_into_buffer; - scratch buffers taken from the
PoolRegion<S>insideResamplerSettings<S>; - backend-owned scratch acquired during construction.
Library code must not create a hidden pool region. The facade is injected by the
surface that owns memory sizing, matching the kithara-bufpool contract.
Steady-state processing reuses already-owned buffers.
Rubato rebuilds only mutable channel metadata on each call. That metadata is
stack-backed through eight channels; wider layouts allocate one call-scoped
Vec sized to the configured channel count. Rubato therefore does not advertise
REALTIME_SAFE.
Any backend adapter bridging a third-party API shape (Rubato's planar adapter surface, the Apple converter's pull callback) must hide that bridge behind owned scratch and keep the public trait on borrowed planar slices.
Resampler handles standalone decoded audio only: it accepts borrowed planar f32
slices and writes into caller-owned planar output slices. The returned
ResamplerProcess reports accepted input frames and produced output frames.
The mutable channel view is call-scoped and must be rebuilt before each process
call; the underlying output buffers remain caller-owned and reusable.
Callers size output using the backend's frame-capacity methods
(output_frames_max, output_frames_next, output_frames_for_input).
flush_into_buffer processes the final caller-supplied block (default: plain
process_into_buffer) and drain_into_buffer drains backend-owned tail
(default: none), so a backend with no tail needs neither.
Variable ratio and glide are advertised through ResamplerCapabilities
(FIXED_RATIO, VARIABLE_RATIO, RATIO_GLIDE, REALTIME_SAFE,
REPORTS_LATENCY, STANDALONE). Callers holding a concrete resampler reach the
optional ResamplerControl surface through Resampler::control_mut();
fixed-ratio backends return None. Fixed-ratio analysis config never asks for
DJ pitch/glide modes.
validate_settings is the single gate: it rejects a non-positive
chunk_size, a non-finite/negative passthrough_tolerance, a non-finite or
non-positive max_ratio_adjustment, a mode the backend's capabilities do not
carry, and a non-finite or non-positive ratio. create_resampler validates and
then builds exactly the configured backend, or returns
ResamplerBuildError. There is no runtime fallback chain and no portable default
backend in this crate.
ResamplerQuality adjusts implementation quality inside a selected backend; it
never selects a backend. Rubato-specific algorithm choices, including FFT, are
rubato::RubatoConfig fields, not separate backend families or cargo features.
External backends may ignore quality or map it into their own config.
Everything is constructed through bon builders. ResamplerSettings<S> (channels,
pools, mode, options, quality) is what a backend builds from;
ResamplerConfig<B, S> pairs it with a backend value and is what create_resampler
consumes. ResamplerOptions holds the numeric tunables — max_ratio_adjustment
8.0, passthrough_tolerance 0.0001, chunk_size 4096 — and its Default goes
through the builder, so overriding one keeps the other defaults.
On the decode side, kithara-decode's DecoderResamplerConfig<B> carries the
backend value, target_sample_rate, options and quality, keeping the backend in
the type at the call site. MonoStream<B> / MonoStreamConfig<B, S> are the
pooled single-channel streaming adapter used by beat analysis in
kithara-analysis.
-
Rubato(resample-rubato): fixed-ratio.rubato::RubatoConfigselects async poly/sinc or FFT viaRubatoAlgorithm. -
Glide(resample-glide): cursor-based glide renderer advertisingFIXED_RATIO | VARIABLE_RATIO | RATIO_GLIDE | REALTIME_SAFE | STANDALONE;GlideBackend::with_configselects the interpolation (kithara_dsp::interp::Interpolation: linear, quadratic, Hermite, Watte) and anti-aliasing throughGlideConfig. Filtering, interpolation and read positions run onkithara-dsp, which picks Accelerate on Apple andfearless_simdelsewhere; Glide has no platform feature of its own.- Positions follow
interp::RateRampin closed form: a glide lands on its target ratio exactly and the cursor carries between blocks inf64. - The anti-alias low-pass (RBJ from the
biquadcrate, Q 0.7071, cutoff0.9·sr / (2·max(1, ratio))) runs on every block except passthrough,anti_alias = falseand a near-unity exact span. Every input frame advances the filter state once, so the output does not depend on how the stream is chunked; entering the filter settles it on the history frame. - Hermite and Watte read one frame further ahead, so a streaming block stops one input frame earlier than with linear or quadratic.
- Positions follow
apple::AppleAudioConverterBackend is a standalone CoreAudio AudioConverter
backend compiled on macOS/iOS without a cargo feature. It advertises
FIXED_RATIO | REPORTS_LATENCY | STANDALONE, has no configuration of its own,
and builds apple::AppleResampler through kithara-apple wrappers rather than
local AudioToolbox FFI. The crate root denies unsafe code, so unavoidable Apple
unsafe lives in kithara-apple.
Codec-embedded Apple decode remains in kithara-decode but uses the same shared
Apple ABI crate.
No Android backend exists until there is a real Android resampling backend. Future backend families get cargo features only when their implementation module lands; empty placeholder features are not part of the public contract.
kithara-decode imports this crate and supports one of two placements:
- codec-embedded: the decoder emits target-rate audio internally (Apple decoder
plus Apple
AudioConverter); - decoder-adapter: any decoder emits source-rate audio and is wrapped by a
standalone backend from this crate — built-in or any external type implementing
ResamplerBackend.
Explicit invalid pairs return typed configuration errors. The planner must not try another backend when the requested pair is unsupported.
Gapless frame metadata belongs to the decoder-output domain. Scaling from source rate to output rate happens once in the decode plan, independent of whether the resampler is codec-embedded or adapter-based.