timestretch
Audio tempo and pitch processing in mostly safe Rust, with an optional Apple Accelerate numerical provider.
There is one production processor and one host-driven execution adapter,
StreamSession. Offline rendering, the CLI and desktop playback use that same
adapter. The library owns source demand, startup, EOF, controls, seeking, loops,
output epochs and playback position. Applications own threads, wakeups, device
queues and presentation.
use ;
let input = vec!;
let audio = render?;
println!;
# Ok::
The controls use explicit units:
| Type | Meaning | Inclusive range |
|---|---|---|
TempoRate |
Source time / output time; 2 means twice as fast | 1/32–4 (−96.875% to +300% tempo) |
DurationRatio |
Output time / source time; inverse of tempo | 0.25–32 |
PitchRatio |
Frequency multiplier; 1 leaves pitch unchanged | 0.25–4 (−24 to +24 semitones) |
The combined factor pitch / tempo must be 0.25–32. For example, unchanged
pitch permits 32× duration, while +12 semitones permits at most 16× duration.
Unsupported combinations are rejected before processing. Each ratio exposes
MIN and MAX; PitchRatio::tempo_bounds() gives the valid tempo range at a
chosen pitch. Applications can impose narrower limits. Zero tempo is not a
finite stretch; use StreamSession::set_transport to pause or stop playback.
Parameters::keylock(tempo) requests unchanged pitch. For independent pitch
control, construct Parameters explicitly. Parameters::linked(tempo) links
pitch to tempo and returns Result, since linked rates must fit the pitch range:
use ;
// Sixteen times longer, one octave higher.
let parameters = Parameters ;
parameters.validate?;
// Half speed with pitch following tempo: one octave lower.
let linked = linked?;
# Ok::
Pass these parameters to SessionConfig for a new stream, or to
StreamSession::schedule for a change during playback. The default
RatioConstraint::PreservePitch keeps the selected pitch and allows duration
to follow hop quantization. PreserveDuration instead allows pitch to adjust.
AppliedParameters reports the actual quantized result, command ID and output
position where it took effect.
The processor supports mono/stereo at native 44.1/48/96 kHz. AllowConversion
accepts source/output rates from 32–192 kHz, using a 48 kHz core for non-native
sources and explicit bandlimited conversion. NativeOnly rejects conversion.
Source, native and output rates are reported separately. Multichannel and
formant preservation are outside this processor's scope.
Backend::Auto resolves to Accelerate on macOS when compiled in, otherwise
Portable. Explicit unavailable backends return an error. Pin Portable or
Accelerate for reproducible renders. Backend choice belongs to each prepared
instance; there is no thread-local provider selection or silent fallback.
| Feature | Capability |
|---|---|
accelerate (default) |
Isolated macOS FFT/vector bindings; has no platform dependency on other targets |
analysis |
Independent beat, key, loudness, transient and waveform analysis |
io |
Validated audio-buffer constructors and WAV I/O |
analysis + io |
Compatible .tsa analysis containers |
cli |
WAV rendering and analysis command |
qa |
Diagnostic access for repository qualification tools |
A minimal portable dependency uses default-features = false. Analysis is not
an input to timestretch and is not a dependency of the native DSP crate.
The desktop's optional .tsa files cache waveform and beat-grid analysis;
the library does not require them to process audio.
The library crates declare Rust 1.85; development and desktop builds use the
pinned toolchain in rust-toolchain.toml.
For streaming, prepare once, then satisfy source_request, call advance with
a work quota, collect stamped audio with read_output, and acknowledge its
actual consumption. Collection alone never advances playback position.
NeedOutput means release queued/collected output; NeedAcknowledgement means
drain control acknowledgements with next_applied. Source shortages and faults
are distinct. See the streaming example and
the session contract.
Preparation allocates plans and bounded storage. Processing performs no
allocation or locking. A work quota counts stages, not microseconds: FFTs,
startup and reset still contain indivisible operations. The desktop runs these
on its own worker. Callback deadlines and the 50% headroom gate are unchanged;
qualification records observed
failures and the scope of each result. TimingInfo separates startup, converter
lookahead, seek history, pending output and the control-boundary upper bound.
owned_memory() counts owned buffers separately from opaque FFT plans and
host-owned queues.
The desktop reference player exposes the same library limits. Keylock holds the selected pitch independently of tempo; Tape links pitch to tempo. The tempo fader does not add a pitch-changing brake at slow rates. Use Pause/Stop for zero speed. The desktop reports achieved tempo and pitch alongside its backend and timing information.
Rendering retains round(source_frames * achieved_stretch * output_rate / source_rate) frames using the session's startup and EOF accounting. It does not
normalize, align, crop to a requested nominal duration or insert user-visible
padding. --normalize explicitly adds an export gain and records it.
Checked-in component and scheduler fixtures retain their numerical tolerances. Self-contained quality, transport, allocation and generated-signal checks run without private audio or external implementations. Human listening remains a separate result.
The former Standard/Wide profiles, engine graph/stages, StretchParams,
standalone stretch/pitch implementations and tuning presets have been removed.
See migration, desktop usage and
the architecture.
Normal tests and qualification use checked-in fixtures and generated signals.
The full wall-clock matrix takes approximately 45 minutes. Physical-device tests
are opt-in with --device-seconds 15; missing device evidence is reported as
unmeasured. See QA usage.