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
- Persistent hybrid streaming — optional high-quality stream mode that keeps rolling state across calls instead of re-instantiating per chunk
- Stateful streaming PV core — phase state and overlap tails persist across stream chunks for smoother continuity
- Streaming API — process audio in chunks for real-time use with dynamic stretch ratio and tempo changes
- Offline pre-analysis pipeline — optional reusable artifact (BPM, phase, confidence, transient map) for safer beat/onset alignment at runtime
- 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.4.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!;
Low-Latency Tempo Constructor
use StreamProcessor;
let mut processor = try_from_tempo_low_latency
.expect;
assert!;
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 (PV-only streaming vs hybrid 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