kithara warp - zvuk/kithara GitHub Wiki
kithara-warp
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 pure protocol used to align one beat map with another and to
compose maps through nested synchronization groups. It owns immutable snapshots,
coordinates, WarpMap, WarpPlan, alignment plans, cursors, and typed results.
It also owns the resident Warp<S> decorator, WarpConfig, the speed
a renderer holds (SpeedCurve, MIN_SPEED), and synchronous WarpRenderer<S>,
which applies temporal plans through the backend-neutral
kithara-stretch::ElasticEngine contract. A build selects at least one backend:
stretch-identity, stretch-glide, stretch-signalsmith, or stretch-bungee.
With the render feature, the common renderer checks backend capabilities at
runtime. Identity preserves submitted decoded PCM; RATE backends support rate
changes and projection; Signalsmith and Bungee provide KEYLOCK. The control
and configuration API is common across these builds.
Host-axis values describe an ephemeral musical clock; they do not make this crate the owner of the live Host, playback session, audio graph, or worker.
Boundaries
kithara-beatowns neural beat detection and its raw model output.kithara-analysisowns progressive analysis and the cleaned, identity-freeBeatArtifactconsumed by a future calibrated grid adapter.kithara-audioowns decoded-audio source lifecycle, decoder-side sample-rate conversion, readiness, and the prepared producer seam.kithara-stretchowns backend DSP engines and their exact-span contract.- kithara-render owns each track's render stage and its lane: it applies speed, keylock and backend commands to the renderer before the quantum that starts on each command's frame.
kithara-playownsPlayWorker,DecoderNode, final output admission, the post-Warp effect chain it hands each render stage, engine-load measurement, Players, the speed, keylock and backend its tracks start with, session state, and the audio graph.kithara-assetsis the only production persistence path.
The crate must not depend on audio, render, play, host, assets, or analyzer
runtime types. Warp<S> is generic over its source, and WarpRenderer<S> is a
synchronous stage; neither makes this crate the owner of source lifecycle,
playback scheduling, or worker threads.
Warp<S> holds the configuration its worker-side renderer is built from and
the publisher paired with that renderer's render snapshots. It shares no speed,
keylock or backend control with the renderer. A staged lane's renderer
evaluates the WarpMap of the WarpPlan it was built to enter
(WarpConfig::entering). Group acknowledgement is owned by kithara-sync.
Configuration
WarpConfig derives kithara_config::Config for its builder and read access.
It carries where a renderer built from it starts: its speed (1.0 by default),
whether it starts keylocked and on which backend, and an optional immutable RegionPlan of per-region ratio
corrections. These are construction values. Once the renderer exists it owns
them, and only its render lane changes speed, keylock and backend; no handle
shared with a deck or UI carries them. The region plan has no live path at
all. A RATE-capable renderer built with one never enters unity passthrough;
Identity refuses region correction instead of changing PCM.
starting_at(speed, keylock, backend) is the copy for one track, whose
renderer starts where the track's lane stands when it opens. It keeps every
other field of the base configuration, where a field-by-field rebuild through
the builder would silently drop a field added later.
It also carries backend preparation, source-block, smoothing, and optional
render-quantum settings expressed in frames. Backend preparation geometry
remains configurable even when the corresponding optional native engine is
absent. The geometry and configuration API is available without the render
feature; that feature exposes the renderer. Every renderer receives the caller's configured PoolRegion<S>; it
never creates a pool region. Source ownership, cancellation, worker resources,
and response budgets remain in their canonical configs and are not duplicated
here.
WarpConfigPatch is what a configuration document may say about it. Speed,
keylock, backend and the region plan are not document keys, so a document
naming one is refused: the render lane owns the first three while a track
plays. Backend preparation geometry is one, carried as backends and re-read
on every engine rebuild, so the geometry a document names survives a backend
switch. Which engine runs is the lane's, not the document's.
Fixed-ratio sample-rate conversion remains owned by kithara-decode. A selected
RATE backend uses Glide varispeed when pitch follows speed, including when the
selected kind is native; a keylock engine runs only when the selected backend
reports KEYLOCK. Identity enables no resampler through Stretch and never
selects Varispeed. supports_playback_rate() reports whether any compiled
backend provides RATE; Identity-only builds report false.
Identity has empty RATE/KEYLOCK capabilities and effective unity source advance.
Initial and live speed or keylock requests remain control state but do not
change sample bits, frame count, sample rate, source accounting, or the submitted
sample-buffer pointer. The renderer returns that buffer directly and owns no
elastic scratch, latency, history, or terminal tail for Identity. Revisions
still identify the control target associated with an emitted chunk.
Projection, entry preroll, and region correction are refused with the input
retained; the renderer does not imitate them with an identity mapping. The
shared render feature still enables Warp's smoother dependency, even though
Identity bypasses its processing.
Speed on a frame
No shared control carries a renderer's speed, keylock or backend. The renderer
owns its speed target and the backend and keylock it was last asked for, and
set_speed(SpeedCurve, revision), set_keylock and set_backend are the only
way they change; the render lane is their only production caller. Each change
takes effect from the next prepared quantum: a quantum prepared before it is
dropped and planned again under the change
(a_speed_set_after_planning_replans_the_next_quantum). The revision given
with a speed is stamped as render_revision on every chunk rendered toward
it, so output traces back to the command that set its speed.
SpeedCurve::Constant holds one requested speed, in media seconds per output
second, from the frame on. RATE backends apply it; Identity's effective rate
remains unity.
prepare_quantum takes an output cap, which the lane sets to the frames left
before its next scheduled command. A manual quantum at the backend's effective rate renders
at most that many frames, and the quantum before a scheduled command ends
exactly on its frame: the renderer asks for the fewest source frames whose
stretched output reaches the cap, forces the last engine request's output to it,
and carries the fractional rest into the next quantum. Rounding whole source
frames alone cannot end a quantum on an arbitrary output frame at a speed other
than 1.0 (a_speed_batch_lands_on_its_frame_from_any_speed in
kithara-render). Identity's source/output spans are equal, so
the cap selects the same number of source frames. A projected quantum follows
its plan's map instead and requires RATE.
MIN_SPEED is 0.05, and a renderer clamps a slower speed, constructed or set,
up to it: that already asks the backend for a 20x stretch, and slower speeds
collapse quality without giving a useful playback mode. The constant is
exported in every build, not only one with a stretch backend, so the player
refuses a slower configured speed and clamps a requested rate to it either way;
RATE backends follow the same speed floor; Identity retains the requested
control value and still renders at unity.
How a change lands depends on the engine:
- Without keylock a RATE-capable backend renders through the zero-latency varispeed
engine, whose pitch follows speed. A speed change keeps that engine and its
state; the new speed starts on the quantum at the frame and the source
position carries on unbroken
(
a_speed_change_continues_the_source_at_the_new_step). - A keylock engine (Signalsmith, Bungee) has latency, and the stretch contract
gives a running engine up to one output latency to reach a new rate. A speed
change on a running keylock engine outside a projection therefore primes it
again on the frame.
Quanta before the frame render on the old state. On the frame the renderer
drains the engine's tail into the source residency's replacement buffer,
then primes the engine at the source frame the emitted output ends on, with
history before it and lookahead and warm source after it read from the
residency; the engine then reads on from the residency until it reaches the
decoded frontier. Its new output fades in linearly over the retained tail.
The re-prime emits nothing of its own and the tail never plays ahead of the
new output, so the change leaves no gap for the device ring to run dry in.
a_keylock_speed_change_renders_on_as_an_engine_started_on_its_framepins the landing against an engine started on that frame (offset 0), and the player-levela_speed_change_keeps_every_beat_on_the_source_it_openedpins one source open and no ring underrun for both keylock backends and for varispeed. - A keylock or backend change on a running engine outside a projection retires
the old engine's tail the same way, and a rebuilt keylock engine of the
requested kind primes
on the frame (
an_engine_batch_renders_on_as_its_engine_started_on_its_framepins keylock on and a switch between Signalsmith and Bungee). When no engine runs, an engine about to engage that is not the requested one is rebuilt before it engages, so a keylock and a speed command on one frame of a lane at unity start the new engine on that frame rather than the old one for a quantum (an_engine_and_a_speed_batch_on_one_frame_engage_the_new_engine). Keylock is a request: the renderer runs a keylock engine only where the backend has one, and it keeps the request across a switch to a backend without one, so switching back plays keylocked again. - A switch from an active DSP backend to Identity emits the old backend's real terminal tail before Identity resumes original-buffer passthrough. A switch back to a RATE backend prepares it with the retained control targets. The renderer and stage retain pending decoded input across both changes.
- A change that lands while the previous change's tail still fades out does
not drop that fade. The retiring tail blends the unfinished fade in on its
way to the next replacement buffer, so the next fade starts from what sounds
on its frame and a fader drag that sends changes faster than a tail fades
does not click (
a_speed_change_within_a_tail_fade_fades_from_what_sounds). The blend needs a second tail buffer the size of the first; both are reserved from the injected pool when the engine is prepared, and a tail that does not fit fails the re-prime with a warning instead of growing a buffer. - A keylock engine returning to 1.0 without a region plan does not re-prime.
It drains its tail as output and unity passthrough resumes after it, so the
return sounds about one engine latency after its frame while the source
stays continuous (
live_unity_transition_drains_active_backend_tail). That delay is accepted rather than adding a second re-prime path into passthrough.