# Changelog
## 0.8.0
### Breaking changes — old engine deleted (ROADMAP Stage 9)
The push-based streaming engine and the hybrid batch stretcher are gone;
the pull engine (`timestretch::engine`) is the only engine, serving both
real-time and batch.
- Removed: `StreamProcessor`, `StreamingEngine`, `StreamProfile`,
`ControlPath`, `StreamLatencyReport`, `StreamPitchQuality`,
`TransientResetStats`, `HybridStretcher`, `MultiResolutionStretcher`,
`Wsola`, `StereoMode`, and the `stream` module.
- `StretchParams`: `stereo_mode` / `with_stereo_mode` and
`with_stream_profile` removed (the engine processes channels in
lockstep natively; profiles were an old-engine latency knob).
- `timestretch-cli`: `--streaming` and `--chunk-size` removed — batch
runs on the engine graph with exact output length.
- Desktop app: the deck engine selector loses `Legacy (push)`; pull
tape/keylock remain. Legacy-only pitch-shift slider removed.
- QA: old-engine harnesses (`streaming_quality`, `profile_quality`,
`varispeed_keylock`, `quality_gates`) deleted; the engine A/B matrix
is the quality dashboard, re-anchored on absolute thresholds derived
from new-engine measurements.
- The live keylock chain drops its phase-vocoder corrector entirely
(owner listening rejected it at every threshold; SOLA carries the
whole corrected range) — chain WCET drops accordingly.
### Breaking changes — EDM presets removed
The preset system predates the engine cutover: none of the tuning knobs
`with_preset` set ever reached the pull engine, and the docs promised
behavior (HPSS, elastic timing, multi-resolution FFT, "kick punch") that
no longer existed. Batch quality is a property of the engine, not a
parameter matrix.
- Removed: `EdmPreset`, `StretchParams::with_preset`, and the CLI
`--preset` flag (plus the legacy positional preset argument).
Migration: `VocalChop`'s one live effect survives as
`with_envelope_preset(EnvelopePreset::Vocal)` — or `--envelope vocal`
on the CLI (new flag: `off`, `balanced`, `vocal`). The other presets
need no replacement; the engine ignored them.
- `StretchParams` loses the dead tuning fields and their builders:
`hop_size`, `transient_sensitivity`, `wsola_segment_size`,
`wsola_search_range`, `beat_aware`, `band_split`, `multi_resolution`,
`transient_region_secs`, `elastic_timing`, `elastic_anchor`,
`hpss_enabled`, `transient_class_adaptive_wsola`, `residual_branch`,
`residual_mix`, `crossfade_mode`, `dynamic_wsola_search`,
`adaptive_phase_locking`, and the `effective_wsola_search_*` helpers.
`CrossfadeMode` is removed with them. (`fft_size`, `window_type`, and
the envelope fields stay — the `pitch_shift` formant path reads them.)
- The analytic tonal fast path in `stretch()` is deleted (it only ever
fired with a preset set): pure-tone inputs now render through the real
engine like everything else, and
`tests/stretch_quality_regressions.rs` is rebaselined against measured
engine output instead of the analytic bypass.
- Behavior note (owner decision 2026-07-16): offline `stretch()` shares
the live keylock semantics by construction — within the corrected
range (ratios ~0.833–1.25) content below the 150 Hz crossover is not
pitch-corrected; its pitch follows tempo, offline exactly as on a
deck. Wide ratios render on the batch PV path with full-spectrum
pitch preservation. Relative level across the crossover is clean
(two-tone balance within ~1% of ideal at ratio 1.25); the wide-PV
path loses some low-band level at heavy compression (ratio 0.5),
which is quality-secondary per the product boundary.
### Added
- Keylock is a live engine parameter: `EngineController::set_keylock(bool)`
(with `keylock_target()` to read the requested state) toggles high-band
pitch correction click-free during playback via a ~12 ms per-sample
crossfade to delay-matched varispeed. Deck-style Tape/Keylock switching
no longer needs an engine rebuild; SOLA stays warm while bypassed so
re-engage is instant.
### Changed
- **MSRV raised from 1.82 to 1.85**; the crate (and the desktop app)
now use Rust edition 2024, which also enables cargo's MSRV-aware
dependency resolver (v3).
- Development toolchain pinned to Rust 1.97.0 via `rust-toolchain.toml`;
CI's stable jobs build with the pinned compiler instead of floating on
latest stable (the MSRV job still tests 1.85.0 explicitly).
### Fixed
- Detected BPM no longer carries the analysis frame-grid bias: the
representative and per-segment BPMs are computed as the median of
K-beat-baseline intervals, so position errors telescope instead of
skewing adjacent intervals (a ground-truth 125.000 BPM kick train now
reads 125.03 instead of 125.23; a real 125.0 EDM track 124.99 instead
of 125.22).
- Onset and beat positions are compensated for the analysis window's
detection latency, so markers land on the audible attack instead of
~29 ms early (measured bias drops from -31.6 ms to -2.6 ms on a
ground-truth kick train). `PREANALYSIS_VERSION` bumps to 4; older
cached sidecars regenerate automatically.
### Removed
- The `web/` WASM demo. It was built against the old engine API deleted
in 0.8.0 (`StreamProcessor`, `EdmPreset`) and had no CI coverage, so it
no longer compiled. Last present at commit `22e5117` if a web demo is
ever revived.
### Kept
- Batch API surface: `stretch`, `stretch_into`, `stretch_buffer`, BPM
helpers, `pitch_shift` (still phase-vocoder based), analysis
(`analyze_for_dj`, beat tracking), WAV I/O.
- `tests/streaming_batch_parity.rs` is superseded by
`tests/streaming_offline_determinism.rs`: streaming and offline are
sample-identical by construction.
## 0.7.0
### Breaking changes
- `BeatGrid` rebuilt around a piecewise tempo model instead of a single BPM
value:
- `beats: Vec<usize>` becomes `beats: Vec<f64>` (fractional-sample
positions, following tempo drift); the separate `beats_fractional`
field is removed.
- New fields: `downbeats: Vec<usize>`, `segments: Vec<TempoSegment>`
(piecewise-constant tempo, new type re-exported at the crate root),
`confidence: f32`, `downbeat_confidence: f32`.
- New `BeatGrid::empty(sample_rate)` and `BeatGrid::bpm_at(position)` for
querying the tempo curve at a point.
- `detect_bpm` / `detect_beat_grid`: tempo estimation moves from an
EDM-tuned 100–160 BPM inter-onset detector to a general-purpose
autocorrelation tempogram (50–220 BPM, soft log-normal octave prior, no
hard range folding) — a 90 BPM hip-hop track now reports 90 instead of
being folded into the EDM range. Detected values on existing material may
shift; use the new `detect_beat_grid_with_options` (below) to narrow the
range or hint a genre. `analyze_for_dj` keeps its 100–160 hint.
### Added
- New `engine` module (`src/engine/`): a pull-based, allocation-free,
real-time-first stage-graph engine — the first cutover milestone of the
roadmap's varispeed-first architecture. Coexists with the existing
`StreamProcessor`/`StreamingEngine` surface (frozen, unaffected) rather
than replacing it yet.
- `Engine::build(EngineConfig)` returns `EngineHandles { controller,
processor, source }`. `EngineProcessor::process(&mut [f32])` fills
exactly the requested frames — infallible and lock-free on the audio
thread. `EngineController` writes tempo changes through a lock-free,
timestamped mailbox (`set_tempo_rate`, `set_tempo_rate_at` for a
sample-accurate landing point) and drives allocation-free warm-start
priming for seeks and loop wraps (`warm_start`). `SourceProducer` feeds
the source ring from the host thread.
- `EngineProfile::Tape`: varispeed-only chain (pitch follows tempo), zero
pipeline delay.
- `EngineProfile::Keylock`: band-split at 150 Hz; the low band stays
un-keylocked (pure delay) while the high band is pitch-corrected by a
corrector chosen from transposition magnitude — beat-synchronous
time-domain SOLA across the full DJ range (rate deviation up to 9%),
a small-FFT (512/128) phase vocoder with identity phase locking beyond
it, hysteresis on the handoff, and a graceful fade to plain varispeed
past 12–22%. Constant pipeline delay of 560 frames (12.7 ms at
44.1 kHz).
- `EngineConfig.pre_analysis` attaches a `PreAnalysisArtifact` as the
engine's primary transient-control signal (splice/phase-reset
placement steers around onsets), falling back to online spectral-flux
detection when absent.
- Measured against the old streaming engine on identical fixtures: kick
transient sharpness ~70% sharper (1.21 vs 0.68 at ±4% rate), ±8% cents
ride p95 0.23 vs 1.86 (old engine 12.19), top-octave retention -0.41 vs
-0.79 dB, envelope swing under unity-crossing rides 0.65 vs 10.36 dB,
and click-free modulation torture at 1.5x/1.1x theoretical slew (tape)
and 3x/1.3x (keylock) vs the old engine's 6x/1.5x bound. Final A/B
matrix run: 9 of 9 parity rows won.
- `TempoTrackingOptions` (re-exported at the crate root): tune the tempo
search range, octave-prior center/width, or add a soft genre hint range.
- `detect_beat_grid_with_options(samples, sample_rate, &TempoTrackingOptions)`:
`detect_beat_grid` with explicit tracking options.
- `TempoSegment` (re-exported at the crate root): a piecewise-constant
tempo stretch (`start_beat`, `bpm`); serialized in the
`PreAnalysisArtifact` and used as `BeatGrid`'s tempo model.
### QA
- New engine gate suites: `qa/engine_keylock.rs`, `qa/engine_transients.rs`,
`qa/engine_wcet.rs` (per-callback WCET budget gates wired into CI),
`qa/engine_ab.rs` and `qa/engine_ab_matrix.rs` (machine-readable
old-vs-new parity dashboard), plus `tests/engine_latency.rs`,
`tests/engine_modulation_torture.rs`, and
`tests/engine_realtime_allocations.rs`.
- `tests/beat_tracking.rs`: new accuracy suite for the tempogram tracker;
`qa/bpm_accuracy.rs` gains the 5-track BPM corpus wired into the
manifest.
- RubberBand comparison anomaly explained: the historic ~-24 LUFS /
~0.15-similarity rows on chirp/sweep content were the frozen offline
hybrid driver (`src/stretch/hybrid.rs`) attenuating uniformly ~28 dB
whenever a preset was set, independent of the new engine.
`qa/rubberband_comparison.rs` gains a `TIMESTRETCH_RUBBERBAND_ENGINE=new`
mode that renders through the new keylock chain for comparison.
## 0.6.0
### Breaking changes
- Removed the legacy hybrid streaming engine from `StreamProcessor`:
- `StreamingEngine::LegacyHybridRerender` variant removed.
- `StreamProcessor::set_hybrid_mode` removed.
- `StreamProcessor::set_streaming_engine` now returns
`Result<(), StretchError>` (engine selection can fail; see below).
`StreamingEngine::Deterministic` remains the default streaming path. The
offline hybrid stretcher used by `stretch_buffer` and the batch APIs is
unaffected.
### Added
- `StreamingEngine::MultiResolution`: an opt-in streaming engine that runs
the three-band Linkwitz-Riley filterbank (`MultiResolutionStretcher`) in
the real-time path, with per-profile band FFT sizing (Club 8192 sub-bass,
Quality 16384). Selecting it with a mid FFT below 2048 (the Live profile)
returns `StretchError::InvalidFormat`. Latency reporting, warm-start
preroll, and the allocation-free steady-state guarantee cover the new
engine; `StreamingEngine` is now re-exported at the crate root.
- `MultiResolutionStretcher` streaming API: `process_streaming_into`,
`flush_streaming_into`, `reset_streaming_state`,
`streaming_frames_consumed`, `reserve_streaming_capacity`,
`last_frame_flux` (combined three-band flux), `total_spectral_bins`,
`latency_samples`, and a `with_sub_bass_fft_cap` constructor.
- `ControlPath::VarispeedFirst` (re-exported at the crate root): inverts the
keylock control architecture so the tempo fader drives a varispeed sinc
resampler at the input (sample-accurate retargets, no glide on the tempo
axis) and the phase vocoder only pitch-corrects at a delay-matched
transposition. Selected via `StreamProcessor::set_control_path` /
`control_path`. Tempo control-to-audio latency collapses to the resampler
kernel lookahead (16 samples at DJ ratios); the buffering gate becomes a
constant, host-compensated pipeline delay. Source-timeline mapping stays
exact through tempo rides (onsets, `notify_source_jump`,
`warm_start_seek`).
- `StreamLatencyReport` now splits `control_to_audio_secs` from
`pipeline_delay_secs` so hosts can compensate the constant part;
`latency_samples` keeps meaning content delay.
- `PhaseVocoder::set_smooth_ratio_updates`: disables seam-masking
continuity heuristics for smooth correction streams (the varispeed path's
torture-ride pitch wobble drops from ~100 to ~12 cents p95 on Live).
- `PhaseVocoder::reserve_streaming_capacity`: preallocates the deterministic
engine's streaming buffers, closing a latent audio-thread allocation.
### QA
- Manifest-driven BPM detection accuracy harness (`qa/bpm_accuracy.rs`):
scores the detector against `benchmarks/manifest.toml` tracks (new
`bpm_only = true` entries; wav/mp3/aiff via a `symphonia`
dev-dependency, sha256-verified) and reports exact/octave accuracy.
- Varispeed keylock pitch-stability gate (`qa/varispeed_keylock.rs`).
## 0.5.0
See the [release notes](https://github.com/robmorgan/timestretch-rs/releases)
for changes in 0.5.0 and earlier.