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-beat owns neural beat detection and its raw model output.
  • kithara-analysis owns progressive analysis and the cleaned, identity-free BeatArtifact consumed by a future calibrated grid adapter.
  • kithara-audio owns decoded-audio source lifecycle, decoder-side sample-rate conversion, readiness, and the prepared producer seam.
  • kithara-stretch owns 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-play owns PlayWorker, 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-assets is 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_frame pins the landing against an engine started on that frame (offset 0), and the player-level a_speed_change_keeps_every_beat_on_the_source_it_opened pins 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_frame pins 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.