timestretch
Pure Rust audio time-stretching library optimized for electronic dance music.
Stretches audio in time without changing its pitch, using a hybrid algorithm that
combines phase vocoder (for tonal content) with WSOLA (for transients). The only
external DSP dependency is rustfft.
Features
- Hybrid algorithm — automatically switches between phase vocoder and WSOLA at transient boundaries so kicks stay punchy while pads stretch smoothly
- Exact timeline fidelity — explicit segment timeline bookkeeping and crossfade compensation keep output duration locked to target tempo
- EDM presets — tuned parameter sets for DJ beatmatching, house loops, halftime effects, ambient stretches, and vocal chops
- Stateful streaming PV core — phase state and overlap tails persist across stream chunks for smoother continuity
- Multi-resolution streaming engine — optional three-band Linkwitz-Riley filterbank with per-band phase vocoders (16384-point sub-bass FFT at the Quality profile) for tighter low-end phase coherence, at a higher buffering latency
- Streaming API — process audio in chunks for real-time use with dynamic stretch ratio and tempo changes
- Analyze-on-load pre-analysis — analyze a track once (CLI
analyzeoranalyze_for_dj), persist the artifact (BPM, beat grid, transient onsets with strengths, content hash), and every stretch path — batch and streaming — consumes it in place of online detection, withset_source_positionkeeping onsets aligned across seeks - Loudness-robust onset detection — log-compressed spectral flux with a
robust
median + k·MADthreshold and an energy-channel gate, so dense mastered material yields usable onsets/BPM (where a plateau-and-multiply detector reads zero) while sustained tones correctly report no beat - Warm-start seek/cue/loop —
warm_start_seekre-primes the streaming processor from the audio preceding a jump so playback resumes converged (no cold buffering gap, no phase-vocoder warm-up, ratio/pitch state preserved);notify_source_jumpwraps loops gaplessly - Stereo coherence hardening — shared onset/timing map and deterministic channel length agreement in mid/side mode
- Sub-bass phase locking — locks phase below 120 Hz to prevent bass smearing
- Quality gates — benchmark-style pass/fail regression checks for duration, transient alignment, timing coherence, loudness, and spectral similarity
- WAV I/O — built-in reader/writer for 16-bit, 24-bit, and 32-bit float WAV files
- Safe Rust —
#![forbid(unsafe_code)], no panics in library code
Quick Start
Add to your Cargo.toml:
[]
= "0.6.0"
One-Shot Stretching
use ;
// Generate or load audio (f32 samples, -1.0 to 1.0)
let input: = load_audio;
let params = new // 1.5x longer (slower)
.with_sample_rate
.with_channels
.with_preset;
let output = stretch.unwrap;
DJ Beatmatching (126 BPM to 128 BPM)
use ;
let original_bpm = 126.0_f64;
let target_bpm = 128.0_f64;
let ratio = bpm_ratio; // source / target = ~0.984
let params = new
.with_preset
.with_sample_rate
.with_channels; // stereo
let output = stretch.unwrap;
Real-Time Streaming
use ;
let params = new
.with_preset
.with_sample_rate
.with_channels
.with_quality_mode;
let mut processor = new;
let = processor.capacities;
let ratio_hint = processor
.current_stretch_ratio
.max
.max;
let input_samples_per_chunk = 1024 * 2; // 1024 stereo frames
let estimated_output = .ceil as usize
+ pending_capacity;
let mut output_chunk = Vecwith_capacity;
// Feed chunks as they arrive from your audio driver
loop
// Change ratio on the fly (e.g. DJ pitch fader)
processor.set_stretch_ratio.expect;
// Flush remaining samples when done
let mut remaining = Vecwith_capacity;
processor.flush_into.unwrap;
If you do not want to manage Vec capacity yourself, use
StreamProcessor::process() / StreamProcessor::flush() instead.
Fixed-Buffer Realtime Callbacks
Use these deterministic APIs when the host owns the callback buffer and you
need bounded output budgets instead of Vec append semantics.
use ;
let params = new
.with_preset
.with_sample_rate
.with_channels;
let mut processor = new;
let input_chunk = vec!;
let callback_capacity = processor
.max_next_process_interleaved_output_samples
.unwrap;
let mut callback_output = vec!;
let written = processor
.process_interleaved_into
.unwrap;
host_submit;
loop
Tempo-Aware Streaming (DJ)
use StreamProcessor;
let mut processor = from_tempo;
// Move the target deck tempo during playback
processor.set_tempo;
println!;
Streaming Profiles & Latency
Streaming latency and quality are selected together through
StreamProfile. Every profile carries the full DJ tuning bundle; they
differ only in the FFT/hop configuration that sets the buffering latency.
use ;
let mut processor =
try_from_tempo_with_profile
.expect;
assert!;
// Or via params: StretchParams::new(ratio).with_stream_profile(StreamProfile::Club)
// `try_from_tempo_low_latency` is shorthand for the Live profile.
Measured figures at 44.1 kHz (tests/streaming_latency.rs verifies that the
first output sample appears exactly at the reported gate; latency_samples()
/ latency_report() report the real current gate, not a formula):
| Profile | FFT/hop | Buffering gate (ratio in [0.9, 1.1]) |
Gate beyond ±10% | Steady-ratio similarity | Ratio-ride similarity |
|---|---|---|---|---|---|
| Live | 1024/256 | 1536 fr = 34.8 ms | 2048 fr = 46.4 ms | 0.9982 | 0.9982 |
| Club | 2048/512 | 3072 fr = 69.7 ms | 4096 fr = 92.9 ms | 0.9976 | 0.9969 |
| Quality | 4096/1024 | 6144 fr = 139.3 ms | 8192 fr = 185.8 ms | 0.9991 | 0.9932 |
Similarity = mean spectral similarity to the source on synthetic EDM material
(qa/profile_quality.rs). At steady stretch the larger windows win; under a
0.92–1.08 ratio ride the smaller windows track the modulation better — which
is exactly why Live is the DJ control profile.
Streaming Engines
StreamProcessor renders with one of two engines, selected via
set_streaming_engine (at build time, not from the audio callback —
switching starts a fresh stream):
StreamingEngine::Deterministic(default) — single persistent phase vocoder per channel with scheduled per-band transient phase resets. All figures in the profile table above are this engine.StreamingEngine::MultiResolution— three-band Linkwitz-Riley filterbank (sub-bass/mid/high at 200 Hz / 4 kHz) with a per-band phase vocoder whose FFT is tuned to its range: sub-bassmid × 4, highmid / 4. The buffering gate is set by the sub-bass FFT, so it needs the Club profile or larger — selecting it on Live returns an error.
use ;
let params = new
.with_sample_rate
.with_channels
.with_stream_profile;
let mut processor = new;
processor
.set_streaming_engine
.expect;
Measured multi-resolution figures at 44.1 kHz (tests/streaming_latency.rs
verifies the first output sample lands exactly at the reported gate):
| Profile | Band FFTs (sub/mid/high) | Buffering gate (ratio in [0.9, 1.1]) |
Steady-ratio similarity | Ratio-ride similarity |
|---|---|---|---|---|
| Club | 8192/2048/512 | 12288 fr = 278.6 ms | 0.9898 | 0.9876 |
| Quality | 16384/4096/1024 | 24576 fr = 557.3 ms | 0.9898 | 0.9928 |
Trade-off profile on the same synthetic EDM material: the multi-res engine ties the single-PV engine at the sub-bass band's similarity ceiling and wins the mid band (0.9982 vs 0.9964 at Quality), but gives some broadband similarity back in the bands containing the 200 Hz / 4 kHz crossover seams, where the independently-stretched bands overlap. Reach for it on sub-bass-critical material where low-end phase coherence matters more than control latency; keep the deterministic engine for beatmatching feel.
Control-to-audio behavior on the default path (all profiles): ratio and pitch controls glide with a ~50 ms time constant. Pitch changes reach the output almost immediately (the pitch resampler sits after the vocoder, so only the glide applies, plus 16–80 samples of sinc kernel lookahead); ratio changes take roughly the buffering gate plus the glide.
Control Paths (Varispeed-First Keylock)
StreamProcessor implements tempo through one of two control paths,
selected via set_control_path (at build time, like engine selection):
ControlPath::VocoderTempo(default) — the phase vocoder implements the tempo change, so tempo control latency is the buffering gate plus the ~50 ms glide. The right path for tape-mode/no-keylock use.ControlPath::VarispeedFirst— the tempo fader drives a varispeed sinc resampler at the input (instant, sample-accurate retargets, no glide on the tempo axis), and the vocoder only pitch-corrects at a delay-matched transposition. The buffering gate becomes a constant pipeline delay the host compensates in its timeline; tempo control-to-audio collapses to the resampler's kernel lookahead (16 samples at DJ ratios) plus the caller's callback size. Tempo ratios are bounded to[0.25, 4.0].
use ;
let params = new
.with_sample_rate
.with_channels
.with_stream_profile;
let mut processor = new;
processor
.set_control_path
.expect;
let report = processor.latency_report;
// Constant content delay to compensate in the deck timeline:
let _ = report.pipeline_delay_frames;
// Tempo fader feel — resampler lookahead only:
assert!;
latency_report() splits the two figures: pipeline_delay_frames (constant
content delay, equals latency_samples()) versus control_to_audio_frames
(tempo-change latency, excluding the caller's callback buffering). Works
behind both streaming engines.
Realtime Pitch Control
use StreamProcessor;
let mut processor = from_tempo;
processor.set_pitch_scale.expect;
println!;
AudioBuffer API
use ;
let buffer = from_mono;
let params = new;
let output = stretch_buffer.unwrap;
println!;
Pitch Shifting
use ;
let params = new
.with_sample_rate
.with_channels
.with_envelope_preset // stronger formant retention
.with_envelope_strength
.with_adaptive_envelope_order;
// Shift up one octave (2x frequency), preserving duration
let output = pitch_shift.unwrap;
assert_eq!;
Envelope control quick guide:
- Default profile is
EnvelopePreset::Balanced(envelope_strength = 1.0, adaptive order enabled). - Use
.with_envelope_preset(EnvelopePreset::Off)for classic behavior with no formant correction. - Use
.with_envelope_preset(EnvelopePreset::Vocal)for stronger vocal formant retention. - Use
.with_envelope_strength(x)to scale correction (0.0..=2.0), and.with_adaptive_envelope_order(true)for content-adaptive cepstral detail.
BPM-Based Stretching
use ;
let params = new // ratio computed automatically
.with_sample_rate
.with_channels
.with_preset;
// Stretch a 126 BPM track to 128 BPM
let output = stretch_to_bpm.unwrap;
Offline Pre-Analysis (Optional)
use ;
use Path;
// Build a reusable analysis artifact once (offline)
let artifact = analyze_for_dj;
write_preanalysis_json.unwrap;
// Load artifact at runtime and attach it to params
let loaded = read_preanalysis_json.unwrap;
let params = new
.with_preset
.with_sample_rate
.with_pre_analysis
.with_beat_snap_confidence_threshold
.with_beat_snap_tolerance_ms;
let output = stretch.unwrap;
WAV File I/O
use wav;
// Read a WAV file
let buffer = read_wav_file.unwrap;
// Stretch it
let params = new
.with_preset;
let output = stretch_buffer.unwrap;
// Write the result (16-bit, 24-bit, or float)
write_wav_file_16bit.unwrap;
write_wav_file_24bit.unwrap;
write_wav_file_float.unwrap;
// Or use the one-liner convenience API
stretch_wav_file.unwrap;
EDM Presets
| Preset | Use Case | Stretch Range | FFT | Transient Sensitivity |
|---|---|---|---|---|
DjBeatmatch |
Live mixing tempo sync | ±1–8% | 4096 | Low (0.3) |
HouseLoop |
General house/techno loops | ±5–25% | 4096 | Medium (0.5) |
Halftime |
Bass music halftime effect | 2x | 4096 | High (0.7) |
Ambient |
Ambient transitions/builds | 2x–4x | 8192 | Low (0.2) |
VocalChop |
Vocal samples & one-shots | ±10–50% | 2048 | Medium-high (0.6) |
How It Works
The library uses a hybrid segmented pipeline:
-
Transient detection — spectral flux with adaptive threshold identifies attack transients (kicks, snares, hi-hats). High-frequency bins (2–8 kHz) are weighted more heavily to catch percussive onsets.
-
Beat-aware segmentation (optional) — transient boundaries can be merged with beat-grid positions and snapped to subdivisions. If provided, an offline pre-analysis artifact is preferred when confidence is high.
-
Segment-wise stretching — the audio is split at boundaries. Transient segments are stretched with WSOLA (preserves waveform shape and attack character). Tonal segments are stretched with a phase vocoder (preserves frequency content with identity phase locking).
-
Sub-bass treatment — frequencies below 120 Hz always use phase-locked processing to prevent phase cancellation that would weaken the bass.
-
Timeline correction — explicit timeline bookkeeping compensates boundary overlap so concatenation preserves target output duration exactly.
Segment joins use fixed or adaptive raised-cosine crossfades.
Parameters
StretchParams supports a builder pattern for full control:
let params = new
.with_sample_rate
.with_channels
.with_preset // apply preset first
.with_fft_size // then override individual params
.with_hop_size
.with_transient_sensitivity
.with_elastic_timing
.with_crossfade_mode
.with_hpss
.with_multi_resolution
.with_sub_bass_cutoff
.with_stereo_mode
.with_phase_locking_mode
.with_wsola_segment_size
.with_wsola_search_range
.with_beat_aware
.with_beat_snap_confidence_threshold
.with_beat_snap_tolerance_ms;
Defaults: 44100 Hz, stereo, FFT 4096, hop 1024 (75% overlap), 120 Hz sub-bass cutoff, ~20ms WSOLA segments, ~10ms search range.
Performance
Performance depends heavily on preset, ratio, and mode (streaming vs offline batch).
Run opt-in QA harnesses:
# These harnesses are excluded from default `cargo test`.
# Throughput-oriented benchmark suite (use release for realistic timing)
# M0 baseline command (strict corpus validation + archive)
# Quality-gate benchmark subset (CI-enforced)
# Strict callback-budget gate (same mode used in CI quality-gates job)
TIMESTRETCH_STRICT_CALLBACK_BUDGET=1
# Emit quality dashboard CSV artifacts (one file per quality gate)
TIMESTRETCH_QUALITY_DASHBOARD_DIR=target/quality_dashboard
# Reference-quality comparison (strict corpus required)
TIMESTRETCH_STRICT_REFERENCE_BENCHMARK=1 TIMESTRETCH_REFERENCE_MAX_SECONDS=30
# Ad-hoc reference-quality run (non-strict, short window)
TIMESTRETCH_REFERENCE_MAX_SECONDS=5
# Single-scenario comparison against an external Rubber Band render
TIMESTRETCH_RUBBERBAND_ORIGINAL_WAV=benchmarks/audio/originals/loop.wav \
TIMESTRETCH_RUBBERBAND_REFERENCE_WAV=benchmarks/audio/references/loop_rubberband.wav \
TIMESTRETCH_RUBBERBAND_RATIO=1.113043478 \
See benchmarks/README.md for corpus setup and manifest/checksum requirements.
API Reference
Core Types
StretchParams— builder-pattern configuration: stretch ratio, sample rate, channels, FFT size, hop size, EDM preset, WSOLA parameters, beat-snap controls, optional pre-analysis artifact, and tempo helpers likefrom_tempo()AudioBuffer— holds interleaved sample data with metadata (sample rate, channel layout)EdmPreset— enum of tuned parameter sets for EDM workflowsEnvelopePreset— formant/envelope profile (Off,Balanced,Vocal)QualityMode— explicit streaming profile:LowLatency(lean path, HPSS off),Balanced,MaxQuality(HPSS + adaptive crossfade/phase-lock enabled)StreamProcessor— chunked real-time processor with on-the-fly ratio/tempo changes,from_tempo()/set_tempo(), plus bothVec-append (process_into()/flush_into()) and fixed-buffer interleaved (process_interleaved_into()/flush_interleaved_into()) deterministic APIsPreAnalysisArtifact— serializable offline beat/onset analysis artifactStretchError— error type covering invalid parameters, I/O failures, and input-too-short conditions
Functions
Time stretching:
stretch(&[f32], &StretchParams)— stretch raw sample datastretch_into(&[f32], &StretchParams, &mut Vec<f32>)— append stretched output into caller bufferstretch_buffer(&AudioBuffer, &StretchParams)— stretch anAudioBufferstretch_to_bpm(&[f32], &StretchParams, source_bpm, target_bpm)— BPM-based stretchstretch_to_bpm_auto(&[f32], &StretchParams, target_bpm)— auto-detect BPM and stretchstretch_bpm_buffer(&AudioBuffer, &StretchParams, source_bpm, target_bpm)— BPM stretch forAudioBufferstretch_bpm_buffer_auto(&AudioBuffer, &StretchParams, target_bpm)— auto BPM stretch forAudioBuffer
Pitch shifting:
pitch_shift(&[f32], &StretchParams, factor)— shift pitch without changing durationpitch_shift_buffer(&AudioBuffer, &StretchParams, factor)— pitch shift anAudioBuffer
BPM detection:
detect_bpm(&[f32], sample_rate)— detect tempo from raw samplesdetect_bpm_buffer(&AudioBuffer)— detect tempo from anAudioBufferdetect_beat_grid(&[f32], sample_rate)— detect beat grid positionsdetect_beat_grid_buffer(&AudioBuffer)— detect beat grid from anAudioBufferbpm_ratio(source_bpm, target_bpm)— compute stretch ratio for BPM change
Pre-analysis artifact pipeline:
analyze_for_dj(&[f32], sample_rate)— generate offline beat/onset artifactwrite_preanalysis_json(path, &PreAnalysisArtifact)— write artifact JSONread_preanalysis_json(path)— read artifact JSON
WAV file convenience:
stretch_wav_file(input, output, &StretchParams)— read, stretch, and write a WAV filestretch_to_bpm_wav_file(input, output, &StretchParams, source_bpm, target_bpm)— WAV BPM stretchstretch_to_bpm_auto_wav_file(input, output, &StretchParams, target_bpm)— WAV auto BPM stretchpitch_shift_wav_file(input, output, &StretchParams, factor)— read, pitch-shift, and write
See the API documentation for full details.
Examples
Run the included examples:
Audio Format
- Sample format:
f32(32-bit float, range -1.0 to 1.0) - Channel layout: mono or stereo (interleaved)
- Sample rates: any standard rate (44100, 48000, etc.)
- WAV I/O: 16-bit PCM, 24-bit PCM, and 32-bit float
License
MIT