kithara stretch - zvuk/kithara GitHub Wiki
Documentation reviewed from source revision 41918f1c9. This records the documented contract at that revision; it is not a new runtime validation. API and usage · All crates.
This crate owns the time-stretch engine contract and its backend adapters,
including the explicit Identity backend. kithara-warp owns the
synchronous WarpRenderer, the speed it renders at (SpeedCurve, MIN_SPEED),
and RegionPlan, passing the play-configured PoolRegion<S> through
ElasticConfig<S>; kithara-signal owns
AudioChunk, AudioChunkInfo, AudioSpec, and the canonical sample views.
Decoder sample-rate conversion remains in the decode/audio seam. This crate must
not create a default or global pool.
The perf feature keeps hotpath load-bearing through kithara::measure_block!.
That proc macro expands to hotpath::measure_block!; cargo-machete cannot see the
expanded dependency, so the manifest records this single scanner exception.
-
ElasticEngineis the sole backend contract. Exact source/output frame counts control time;set_pitchremains independent, andprime/flush/resetdefine stream lifecycle. -
StretchKindis the compiled backend selector. Itsu8discriminants are stable regardless of which variants are compiled in:1 = Signalsmith,2 = Bungee,3 = Glide,4 = Identity. Identity is last in selector order. An unknown discriminant decodes toStretchKind::all()[0], the first compiled-in backend, which is alsoDefault. -
ElasticConfig<S>is the single fallible#[non_exhaustive]bonroot config. It owns theStretchKindselection, sample rate, channel count, maximum source/output frame spans and the practical playback-rate envelope, backend preparation geometry, and the injected pool region; the selector is not a second factory argument. - Backend geometry defaults preserve each native engine's quality preset: Signalsmith derives its block and interval from the sample rate, and Bungee uses zero synthesis-hop adjustment. Supplying one custom Signalsmith dimension requires supplying both.
-
ElasticBackendConfigPatchis what a configuration document may say about that geometry; it reaches this crate throughWarpConfig, which is the only production owner of anElasticConfig. Nothing else in this crate is document-settable: every other value onElasticConfigis measured from the stream the engine is being prepared for. -
build_engine(config)dispatches the config-owned selector toBox<dyn ElasticEngine>. -
build_varispeed_engine(config)preserves Identity when it is selected. RATE-capable selections use the zero-latency Varispeed/Glide adapter when keylock is off. - Backends with nonzero latency implement priming; callers may still render a fresh unprimed stream. A zero-latency backend refuses priming because it needs no history or warmup. Nothing above an adapter names a concrete DSP library.
ElasticEngine renders exactly request.output_frames() from exactly request.source_frames();
the frame counts are the only rate control, so the caller owns the transport and two engines fed the
same plan advance through the source identically. prepare allocates outside the render core;
capabilities() is fixed for the engine's lifetime; reset() clears history and may fail for an
engine that clears state by rebuilding itself.
Each engine reports one common ElasticRateEnvelope plus its own ElasticLatency. The configured
rate policy defaults to the practical 0.05..=4.0 range. Preparation intersects it with the selected
backend's functions and ratios representable by the fixed source/output frame limits. Identity's
envelope is unity-only; a configured policy that excludes unity fails preparation. Request shape,
buffer lengths, prepared limits and the rate window are checked once by ElasticCapabilities,
before entering backend code. A backend without RATE rejects unequal source/output spans.
For a backend with nonzero latency and a cue at source frame C, prime consumes four adjacent regions with no hidden coordinate
change: history [C-H, C), lookahead [C, C+H), warm source
[C+H, C+H+rate*O), then the next process source. H and O are the declared source and output
latencies; the warm request therefore contains exactly rate*O source frames and O output
frames. Priming clears old state and writes its O warmup frames into caller-owned discard storage,
but the first subsequent audible output starts at C, not after the warm source. A backend that
declares nonzero latency must preserve this ordering. Identity and Glide have zero latency and
refuse priming.
Engines process interleaved f32 samples. ElasticRequest names exact non-empty source and output
frame spans; their ratio is the only tempo control where RATE is available. set_pitch(scale) is
independent for KEYLOCK-capable engines (1.0 keeps pitch locked), which preserves keylock without
a second streaming API. Identity accepts only 1.0. Invalid preparation,
requests, pitch or processing return ElasticError; the outer WarpRenderer::render maps failure
to "drop this chunk + warn", never a panic.
The produce path must stay allocation-free. Callers provide fixed output slices from scratch
reserved before the checked render call, and an engine that needs planar scratch checks it out from
the PoolRegion<S> supplied in ElasticConfig<S>; no engine owns a default or global pool. Bungee keeps
that channel-major scratch in kithara_signal::PlanarBuffer instead of a backend-local buffer type.
flush(out) writes the next buffered-tail portion into non-empty caller-owned storage containing
only complete interleaved frames and returns its frame count together with whether that portion
completed the drain. The caller normally supplies one render quantum and repeats the call until
completion; a completed drain stays empty until new input.
An active drain reports completion on its final non-empty portion, so the caller can publish the
released source frontier with those samples. This streaming contract lets a
rate-dependent tail span several fixed-size chunks without loss. At a steady rate r, the complete
terminal span is ceil(H / r) + O frames, where H and O are the declared source and output
latencies. The same formula applies after a rate change has rendered O frames and reached its new
latency mapping. EOF during that bounded transition drains the real accumulated state, whose span is
history-dependent; it must not reset, replay, reverse, drop, or synthesize source audio to imitate the
settled formula. Every backend with retained terminal audio must expose this real drain behavior;
a synthetic-silence flush is not conforming. Zero-latency Identity and Glide have no terminal tail,
so their flush is empty and complete.
A RATE-capable backend's rate change between adjacent exact requests preserves history, moves monotonically, and reaches
the requested mapping within one declared output-latency window. A KEYLOCK-capable engine's pitch
changes must affect audio within the same bound. A backend must not hide already-rendered samples behind an additional software
delay. The renderer in kithara-warp does not rely on that window for a speed command
on a running engine with latency: outside a projection and a return to unity without a region plan, it primes the engine
again on the command's frame, so the new speed starts there rather than up to one output latency
later.
reset() clears buffered state after seek; source-spec and backend changes are handled by the
caller preparing a replacement outside its checked render core. The trait intentionally does not
expose kithara_signal::AudioSpec; native adapters use it only to shape canonical sample buffers.
The explicit stretch-identity feature selects StretchKind::Identity. Its capabilities have no
RATE or KEYLOCK functions, zero source/output latency and a unity-only rate envelope. It reserves
no pooled sample scratch and retains no stream history or terminal tail.
process uses the common capability validation before writing, then copies an equal-span source
into the caller-owned output without changing any sample bits. The trait supplies separate output
storage, so this adapter copies; Warp avoids that copy by returning its submitted buffer directly
when Identity is selected. A scheduled split in the render stage may supply a stage-owned prefix,
which Warp likewise returns unchanged.
set_pitch accepts exactly 1.0. prime returns a preparation error without accessing output,
reset succeeds without state changes, and flush returns zero frames with completion, leaving
output untouched. Identity does not use the resampler or Varispeed. Stretch's resampler dependency
is optional and enabled only by Signalsmith, Bungee or Glide features. Warp's shared render
feature still enables its own smoother dependency.
- Bungee uses the low-level
bungee-sysgranularStretcherAPI behind a private RAII owner. Native planar output is validated and copied immediately into pooled Rust storage; no native pointer or mutable slice escapes the call that produced it. - Bungee retains the source lookahead and output remainder required by its overlapping grains. EOF
first advances finite requests to the exact source end, clips output by native request timestamps,
then clears the four-grain pipeline with invalid requests.
flushtherefore returns real terminal audio across one or more chunks instead of dropping roughly one latency of the track. - Once one exact rate has rendered for a complete output-latency window, Bungee bounds EOF with the
integer
ElasticRequestratio and the common settled-tail formula. Before that point its real history-dependent native timestamps remain authoritative. Repeated floating-point native hops therefore cannot add a frame to a settled tail or replace a transitional tail with a formula. - Bungee preparation fails when the injected region budget cannot cover its planar scratch or native stretcher construction fails; the audio adapter warns once and marks the engine unavailable.
- Bungee priming drains old resident state, stages history, lookahead and warm source in the
injected pooled rolling source buffer, and performs native preroll without rebuilding the
stretcher. Preroll discards only native output ending at or before the cue and stops before
scheduling the cue grain. The next
processapplies its current rate and pitch when it schedules that grain; only the unconsumed remainder of one native output chunk may cross a render boundary. There is no software post-cue sample queue, and the render path allocates nothing. - The private Bungee adapter clears and drains the resident native pipeline on
resetwithout rebuilding it; its Rust-side input/output storage remains the buffers reserved from the injected pool at prepare. - Bungee reports its unity latency only after its pipeline is warm, and runtime latency moves with
the rate. Preparation measures the unity reference on the resident, shape-sized core and resets
that same core in place; no probe engine or extra pool allocation is retained. An unprimed stream converts
the larger native processing center into the measured source/output latency split while its cold
pipeline fills. Once output starts, rate changes slew that center toward the new mapping with grain
positions clamped to the configured rate envelope. Positions therefore remain strictly monotone,
phase history survives without a native reset, and the settled terminal timing reaches the final
request's
ceil(H / r) + Osplit withinOoutput frames. - Both native keylock engines accept the same pitch range,
0.25..=4.0; this is the range covered by Bungee's native sizing and prevents a backend selector from changing validation semantics. - Both native engines expose the configured practical rate policy after intersection with their prepared source/output frame limits; the conformance suite exercises its minimum, unity and maximum requests and rejects extreme in-shape ratios before backend access.
- Bungee on iOS is opt-in. Its CMake C++ build must see
IPHONEOS_DEPLOYMENT_TARGET;xtask appleexports the value from[workspace.metadata.apple] deployment-targetinto the cargo build of each Apple target. Preserve the same env for manual-F stretch-bungeeApple builds.
- Add
src/backends/<name>.rswith a concrete adapter implementingElasticEngine; expose it only to the crate-owned factory through apub(crate)re-export inbackends/mod.rsunder the same gate. - Add a feature
stretch-<name>inCargo.tomland to theany(...)guard of thecompile_error!inlib.rs(the crate requires ≥1 backend). - Gate the adapter module, the
StretchKindvariant, itsall()entry, itsFrom/u8arms, and thebuild_enginefactory arm on#[cfg(feature = "stretch-<name>")]; keep the discriminant stable. - Declare functions and latency, implement the complete
ElasticEnginelifecycle, and test the operations those capabilities permit. RATE/KEYLOCK engines join the corresponding shared facade and priming matrices intests/elastic.rs; zero-latency or no-RATE backends pin their permitted spans and lifecycle explicitly. The suite is the contract; backend-specific tests cover preparation mechanics and measured latency. - Document any target, tail-drain or priming limitation above.
kithara-stretch and kithara-warp require at least one explicitly enabled
stretch-* feature, including for Warp configuration without render. An
Identity-only build selects stretch-identity; Glide is an optional RATE
backend. Native kithara-play keeps Signalsmith as its default;
its optional stretch features forward backend selection through kithara-render
and kithara-warp.
Warp's render feature exposes the single WarpRenderer and enables its pooled
buffers and kithara-dsp smoother. Geometry and configuration remain available
without render; Warp's direct DSP dependency supplies the render smoother.
Glide also uses DSP through kithara-resampler; Identity enables neither dependency through Stretch.
With render enabled, the same renderer runs on native targets and Wasm.
Identity provides explicit unity passthrough on both targets; Glide provides
pure-Rust rate changes and projected rendering. FFI's Wasm feature keeps Glide
as its backend selection. Glide reports RATE without KEYLOCK, so its pitch
follows playback rate. Warp retains speed and keylock requests across backend
changes and applies them according to the selected backend's capabilities.
Signalsmith and Bungee remain optional native engines and cannot build for
wasm32-unknown-unknown. Feature flags select the compiled engines;
StretchKind::all() and backend capabilities describe the available operations
at runtime.