timestretch 0.4.0

Pure Rust audio time stretching library optimized for EDM
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# 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`](https://crates.io/crates/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`:

```toml
[dependencies]
timestretch = "0.4.0"
```

### One-Shot Stretching

```rust
use timestretch::{StretchParams, EdmPreset};

// Generate or load audio (f32 samples, -1.0 to 1.0)
let input: Vec<f32> = load_audio();

let params = StretchParams::new(1.5) // 1.5x longer (slower)
    .with_sample_rate(44100)
    .with_channels(1)
    .with_preset(EdmPreset::HouseLoop);

let output = timestretch::stretch(&input, &params).unwrap();
```

### DJ Beatmatching (126 BPM to 128 BPM)

```rust
use timestretch::{StretchParams, EdmPreset, bpm_ratio};

let original_bpm = 126.0_f64;
let target_bpm = 128.0_f64;
let ratio = bpm_ratio(original_bpm, target_bpm); // source / target = ~0.984

let params = StretchParams::new(ratio)
    .with_preset(EdmPreset::DjBeatmatch)
    .with_sample_rate(44100)
    .with_channels(2); // stereo

let output = timestretch::stretch(&input, &params).unwrap();
```

### Real-Time Streaming

```rust
use timestretch::{EdmPreset, QualityMode, StreamProcessor, StretchParams};

let params = StretchParams::new(1.02)
    .with_preset(EdmPreset::DjBeatmatch)
    .with_sample_rate(44100)
    .with_channels(2)
    .with_quality_mode(QualityMode::Balanced);

let mut processor = StreamProcessor::new(params);
let (_, _, _, pending_capacity) = processor.capacities();
let ratio_hint = processor
    .current_stretch_ratio()
    .max(processor.target_stretch_ratio())
    .max(1.0);
let input_samples_per_chunk = 1024 * 2; // 1024 stereo frames
let estimated_output = (input_samples_per_chunk as f64 * ratio_hint).ceil() as usize
    + pending_capacity;
let mut output_chunk = Vec::with_capacity(estimated_output);

// Feed chunks as they arrive from your audio driver
loop {
    let input_chunk = read_audio_chunk(1024);
    output_chunk.clear();
    processor.process_into(&input_chunk, &mut output_chunk).unwrap();
    play_audio(&output_chunk);
}

// Change ratio on the fly (e.g. DJ pitch fader)
processor.set_stretch_ratio(1.05).expect("valid ratio");

// Flush remaining samples when done
let mut remaining = Vec::with_capacity(pending_capacity * 2);
processor.flush_into(&mut remaining).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.

```rust
use timestretch::{EdmPreset, StreamProcessor, StretchParams};

let params = StretchParams::new(1.02)
    .with_preset(EdmPreset::DjBeatmatch)
    .with_sample_rate(44_100)
    .with_channels(2);

let mut processor = StreamProcessor::new(params);

let input_chunk = vec![0.0f32; 256 * 2];
let callback_capacity = processor
    .max_next_process_interleaved_output_samples(input_chunk.len())
    .unwrap();
let mut callback_output = vec![0.0f32; callback_capacity];

let written = processor
    .process_interleaved_into(&input_chunk, &mut callback_output)
    .unwrap();
host_submit(&callback_output[..written]);

loop {
    let written = processor
        .flush_interleaved_into(&mut callback_output)
        .unwrap();
    if written == 0 {
        break;
    }
    host_submit(&callback_output[..written]);
}
```


### Tempo-Aware Streaming (DJ)

```rust
use timestretch::StreamProcessor;

let mut processor = StreamProcessor::from_tempo(126.0, 128.0, 44100, 2);

// Move the target deck tempo during playback
processor.set_tempo(130.0);

println!(
    "Current target BPM: {:.2}, latency: {:.1} ms",
    processor.target_bpm().unwrap_or(0.0),
    processor.latency_secs() * 1000.0
);
```

### Low-Latency Tempo Constructor

```rust
use timestretch::StreamProcessor;

let mut processor = StreamProcessor::try_from_tempo_low_latency(126.0, 128.0, 44100, 2)
    .expect("valid BPM inputs");
assert!(processor.latency_secs() * 1000.0 < 40.0);
```

### Realtime Pitch Control

```rust
use timestretch::StreamProcessor;

let mut processor = StreamProcessor::from_tempo(126.0, 128.0, 44100, 2);
processor.set_pitch_scale(1.05).expect("valid pitch scale");
println!("Current pitch scale control: {:.3}", processor.pitch_scale());
```

### AudioBuffer API

```rust
use timestretch::{AudioBuffer, StretchParams};

let buffer = AudioBuffer::from_mono(samples, 44100);
let params = StretchParams::new(2.0);
let output = timestretch::stretch_buffer(&buffer, &params).unwrap();

println!("Duration: {:.2}s -> {:.2}s", buffer.duration_secs(), output.duration_secs());
```

### Pitch Shifting

```rust
use timestretch::{EnvelopePreset, StretchParams};

let params = StretchParams::new(1.0)
    .with_sample_rate(44100)
    .with_channels(1)
    .with_envelope_preset(EnvelopePreset::Vocal) // stronger formant retention
    .with_envelope_strength(1.4)
    .with_adaptive_envelope_order(true);

// Shift up one octave (2x frequency), preserving duration
let output = timestretch::pitch_shift(&input, &params, 2.0).unwrap();
assert_eq!(output.len(), input.len());
```

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

```rust
use timestretch::{StretchParams, EdmPreset};

let params = StretchParams::new(1.0) // ratio computed automatically
    .with_sample_rate(44100)
    .with_channels(2)
    .with_preset(EdmPreset::DjBeatmatch);

// Stretch a 126 BPM track to 128 BPM
let output = timestretch::stretch_to_bpm(&input, &params, 126.0, 128.0).unwrap();
```

### Offline Pre-Analysis (Optional)

```rust
use timestretch::{
    analyze_for_dj, read_preanalysis_json, stretch, write_preanalysis_json,
    StretchParams, EdmPreset,
};
use std::path::Path;

// Build a reusable analysis artifact once (offline)
let artifact = analyze_for_dj(&input, 44100);
write_preanalysis_json(Path::new("track.preanalysis.json"), &artifact).unwrap();

// Load artifact at runtime and attach it to params
let loaded = read_preanalysis_json(Path::new("track.preanalysis.json")).unwrap();
let params = StretchParams::new(126.0 / 128.0)
    .with_preset(EdmPreset::DjBeatmatch)
    .with_sample_rate(44100)
    .with_pre_analysis(loaded)
    .with_beat_snap_confidence_threshold(0.35)
    .with_beat_snap_tolerance_ms(5.0);

let output = stretch(&input, &params).unwrap();
```

### WAV File I/O

```rust
use timestretch::io::wav;

// Read a WAV file
let buffer = wav::read_wav_file("input.wav").unwrap();

// Stretch it
let params = timestretch::StretchParams::new(2.0)
    .with_preset(timestretch::EdmPreset::Halftime);
let output = timestretch::stretch_buffer(&buffer, &params).unwrap();

// Write the result (16-bit, 24-bit, or float)
wav::write_wav_file_16bit("output_16.wav", &output).unwrap();
wav::write_wav_file_24bit("output_24.wav", &output).unwrap();
wav::write_wav_file_float("output_32.wav", &output).unwrap();

// Or use the one-liner convenience API
timestretch::stretch_wav_file("input.wav", "output.wav", &params).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:

1. **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.

2. **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.

3. **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).

4. **Sub-bass treatment** — frequencies below 120 Hz always use phase-locked
   processing to prevent phase cancellation that would weaken the bass.

5. **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:

```rust
let params = StretchParams::new(1.5)
    .with_sample_rate(48000)
    .with_channels(2)
    .with_preset(EdmPreset::HouseLoop)   // apply preset first
    .with_fft_size(4096)                 // then override individual params
    .with_hop_size(1024)
    .with_transient_sensitivity(0.6)
    .with_elastic_timing(true)
    .with_crossfade_mode(timestretch::CrossfadeMode::Adaptive)
    .with_hpss(true)
    .with_multi_resolution(true)
    .with_sub_bass_cutoff(100.0)
    .with_stereo_mode(timestretch::StereoMode::MidSide)
    .with_phase_locking_mode(timestretch::PhaseLockingMode::RegionOfInfluence)
    .with_wsola_segment_size(960)
    .with_wsola_search_range(480)
    .with_beat_aware(true)
    .with_beat_snap_confidence_threshold(0.35)
    .with_beat_snap_tolerance_ms(5.0);
```

**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:

```sh
# These harnesses are excluded from default `cargo test`.

# Throughput-oriented benchmark suite (use release for realistic timing)
cargo test --features qa-harnesses --release --test benchmarks -- --nocapture

# M0 baseline command (strict corpus validation + archive)
./benchmarks/run_m0_baseline.sh

# Quality-gate benchmark subset (CI-enforced)
cargo test --features qa-harnesses --release --test quality_gates -- --nocapture

# Strict callback-budget gate (same mode used in CI quality-gates job)
TIMESTRETCH_STRICT_CALLBACK_BUDGET=1 cargo test --features qa-harnesses --release --test quality_gates -- --nocapture

# Emit quality dashboard CSV artifacts (one file per quality gate)
TIMESTRETCH_QUALITY_DASHBOARD_DIR=target/quality_dashboard cargo test --features qa-harnesses --release --test quality_gates -- --nocapture

# Reference-quality comparison (strict corpus required)
TIMESTRETCH_STRICT_REFERENCE_BENCHMARK=1 TIMESTRETCH_REFERENCE_MAX_SECONDS=30 cargo test --features qa-harnesses --test reference_quality -- --nocapture

# Ad-hoc reference-quality run (non-strict, short window)
TIMESTRETCH_REFERENCE_MAX_SECONDS=5 cargo test --features qa-harnesses --test reference_quality -- --nocapture

# 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 \
cargo test --features qa-harnesses --test rubberband_comparison -- --nocapture
```

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 like `from_tempo()`
- **`AudioBuffer`** — holds interleaved sample data with metadata (sample rate,
  channel layout)
- **`EdmPreset`** — enum of tuned parameter sets for EDM workflows
- **`EnvelopePreset`** — 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 both
  `Vec`-append (`process_into()`/`flush_into()`) and fixed-buffer interleaved
  (`process_interleaved_into()`/`flush_interleaved_into()`) deterministic APIs
- **`PreAnalysisArtifact`** — serializable offline beat/onset analysis artifact
- **`StretchError`** — error type covering invalid parameters, I/O failures,
  and input-too-short conditions

### Functions

**Time stretching:**
- `stretch(&[f32], &StretchParams)` — stretch raw sample data
- `stretch_into(&[f32], &StretchParams, &mut Vec<f32>)` — append stretched output into caller buffer
- `stretch_buffer(&AudioBuffer, &StretchParams)` — stretch an `AudioBuffer`
- `stretch_to_bpm(&[f32], &StretchParams, source_bpm, target_bpm)` — BPM-based stretch
- `stretch_to_bpm_auto(&[f32], &StretchParams, target_bpm)` — auto-detect BPM and stretch
- `stretch_bpm_buffer(&AudioBuffer, &StretchParams, source_bpm, target_bpm)` — BPM stretch for `AudioBuffer`
- `stretch_bpm_buffer_auto(&AudioBuffer, &StretchParams, target_bpm)` — auto BPM stretch for `AudioBuffer`

**Pitch shifting:**
- `pitch_shift(&[f32], &StretchParams, factor)` — shift pitch without changing duration
- `pitch_shift_buffer(&AudioBuffer, &StretchParams, factor)` — pitch shift an `AudioBuffer`

**BPM detection:**
- `detect_bpm(&[f32], sample_rate)` — detect tempo from raw samples
- `detect_bpm_buffer(&AudioBuffer)` — detect tempo from an `AudioBuffer`
- `detect_beat_grid(&[f32], sample_rate)` — detect beat grid positions
- `detect_beat_grid_buffer(&AudioBuffer)` — detect beat grid from an `AudioBuffer`
- `bpm_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 artifact
- `write_preanalysis_json(path, &PreAnalysisArtifact)` — write artifact JSON
- `read_preanalysis_json(path)` — read artifact JSON

**WAV file convenience:**
- `stretch_wav_file(input, output, &StretchParams)` — read, stretch, and write a WAV file
- `stretch_to_bpm_wav_file(input, output, &StretchParams, source_bpm, target_bpm)` — WAV BPM stretch
- `stretch_to_bpm_auto_wav_file(input, output, &StretchParams, target_bpm)` — WAV auto BPM stretch
- `pitch_shift_wav_file(input, output, &StretchParams, factor)` — read, pitch-shift, and write

See the [API documentation](https://docs.rs/timestretch) for full details.

## Examples

Run the included examples:

```sh
cargo run --example basic_stretch      # Simple time stretch
cargo run --example benchmark_quality  # Offline quality benchmark helper
cargo run --example dj_beatmatch       # 126 → 128 BPM tempo sync
cargo run --example dj_mix             # Streaming DJ transition demo
cargo run --example sample_halftime    # 2x halftime effect
cargo run --example pitch_shift        # Pitch shifting demo
cargo run --example realtime_stream    # Streaming API demo
```

## 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