kithara stretch - zvuk/kithara GitHub Wiki

kithara-stretch

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.

Ownership

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.

  • ElasticEngine is the sole backend contract. Exact source/output frame counts control time; set_pitch remains independent, and prime / flush / reset define stream lifecycle.
  • StretchKind is the compiled backend selector. Its u8 discriminants 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 to StretchKind::all()[0], the first compiled-in backend, which is also Default.
  • ElasticConfig<S> is the single fallible #[non_exhaustive] bon root config. It owns the StretchKind selection, 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.
  • ElasticBackendConfigPatch is what a configuration document may say about that geometry; it reaches this crate through WarpConfig, which is the only production owner of an ElasticConfig. Nothing else in this crate is document-settable: every other value on ElasticConfig is measured from the stream the engine is being prepared for.
  • build_engine(config) dispatches the config-owned selector to Box<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.

Exact-span contract

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.

Engine contract

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.

Identity

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.

Backend limitations

  • Bungee uses the low-level bungee-sys granular Stretcher API 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. flush therefore 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 ElasticRequest ratio 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 process applies 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 reset without 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) + O split within O output 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 apple exports the value from [workspace.metadata.apple] deployment-target into the cargo build of each Apple target. Preserve the same env for manual -F stretch-bungee Apple builds.

Adding a backend

  1. Add src/backends/<name>.rs with a concrete adapter implementing ElasticEngine; expose it only to the crate-owned factory through a pub(crate) re-export in backends/mod.rs under the same gate.
  2. Add a feature stretch-<name> in Cargo.toml and to the any(...) guard of the compile_error! in lib.rs (the crate requires ≥1 backend).
  3. Gate the adapter module, the StretchKind variant, its all() entry, its From/u8 arms, and the build_engine factory arm on #[cfg(feature = "stretch-<name>")]; keep the discriminant stable.
  4. Declare functions and latency, implement the complete ElasticEngine lifecycle, and test the operations those capabilities permit. RATE/KEYLOCK engines join the corresponding shared facade and priming matrices in tests/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.
  5. Document any target, tail-drain or priming limitation above.

Backend selection and wasm builds

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.

⚠️ **GitHub.com Fallback** ⚠️