Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ benchmarks/audio/
.codex-plan-loop/
*.tsanalysis.json
*.tspeaks
*.tsa
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

## Unreleased

### Breaking changes

- `timestretch-cli analyze` writes the binary `.tsa` analysis container
by default (`<input>.tsa`) instead of `<input>.tsanalysis.json`; an
explicit `-o out.json` keeps the legacy JSON format. `--pre-analysis`
accepts either format.
- Desktop: tracks now converge to a single `.tsa` sidecar. Valid legacy
`.tsanalysis.json` artifacts are absorbed into the container on load
and both legacy sidecars (`.tsanalysis.json`, `.tspeaks`) are deleted
once the on-disk container supersedes them.

### Deprecated

- `read_preanalysis_json` / `write_preanalysis_json`: use the `.tsa`
analysis container (`io::tsa`) instead. The JSON pair keeps working
while downstream consumers migrate.

### Added

- `EngineProfile::WideKeylock`: wide-range Master Tempo deck profile
Expand All @@ -25,6 +42,29 @@
- Desktop: a Range selector (Standard | Wide) with seek-priced engine
rebuild that preserves the playhead, and a live pipeline-latency
readout next to it.
- `.tsa` analysis container (`io::tsa`): one content-bound file per
track holding the pre-analysis artifact and the 3-band waveform peaks
as versioned chunks (unknown chunks skip forward-compatibly; readers
reject-don't-panic on hostile input). Two API layers: bytes
(`AnalysisFile::to_bytes`/`from_bytes`/`from_bytes_validated`, for
apps that store analysis blobs in their own database keyed by
`content_hash`) and sidecar file wrappers with atomic writes
(`read_analysis_file`, `read_analysis_file_validated`,
`write_analysis_file`, `analysis_file_path` — `<audio>.tsa`).
`timestretch-cli analyze` writes both chunks, making it a complete
offline pre-analysis tool.
- `analysis::waveform`: the desktop app's 3-band waveform peaks pyramid
moved into the library (`BandPeaks`, `PeakLevel`, `NUM_BANDS`) so any
frontend gets display peaks without reimplementing the analyzer.
- `PreAnalysisArtifact::matches_identity`: `matches_source` semantics
for callers that already hold the (rate, length, hash) identity;
`matches_source` now delegates to it.

### Removed

- Desktop `.tspeaks` sidecar format (introduced on an unreleased
branch): superseded by the `.tsa` container's PEAK chunk; existing
files are deleted after migration, peaks recompute in milliseconds.

## 0.10.0

Expand Down
33 changes: 22 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,26 +190,34 @@ let output = timestretch::stretch_to_bpm(&input, &params, 126.0, 128.0).unwrap()

```rust
use timestretch::{
analyze_for_dj, read_preanalysis_json, stretch, write_preanalysis_json,
StretchParams,
analyze_for_dj, read_analysis_file, write_analysis_file, stretch,
AnalysisFile, BandPeaks, StretchParams,
};
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();
// Build the `.tsa` analysis container once (offline): the beat/onset
// artifact plus the 3-band waveform peaks a player UI needs at load.
let mut analysis = AnalysisFile::for_source(&input, 44100);
analysis.artifact = Some(analyze_for_dj(&input, 44100));
analysis.peaks = Some(BandPeaks::compute(&input, 1, 44100));
write_analysis_file(Path::new("track.wav.tsa"), &analysis).unwrap();

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

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

Apps that keep analysis in their own database rather than sidecar files
can use the bytes layer directly — `AnalysisFile::to_bytes` /
`from_bytes` / `from_bytes_validated` — and key blobs by
`AnalysisFile::content_hash`.

### WAV File I/O

```rust
Expand Down Expand Up @@ -356,10 +364,13 @@ See `benchmarks/README.md` for corpus setup and manifest/checksum requirements.
- `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:**
**Pre-analysis pipeline (`.tsa` analysis container):**
- `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
- `AnalysisFile` — one container per track: identity header + artifact + waveform peaks; `to_bytes`/`from_bytes`/`from_bytes_validated` for database-backed storage
- `write_analysis_file(path, &AnalysisFile)` / `read_analysis_file(path)` / `read_analysis_file_validated(path, rate, len, hash)` — sidecar I/O (atomic writes)
- `analysis_file_path(audio_path)` — sidecar convention: `<audio>.tsa`
- `BandPeaks::compute(&[f32], channels, sample_rate)` — 3-band waveform peaks pyramid
- `write_preanalysis_json` / `read_preanalysis_json` — deprecated: legacy JSON sidecars (use the `.tsa` container)

**WAV file convenience:**
- `stretch_wav_file(input, output, &StretchParams)` — read, stretch, and write a WAV file
Expand Down
24 changes: 12 additions & 12 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -1126,30 +1126,30 @@ Automation: auto
### Why

The RT contract is machine-verified, but the crate's *input* surface has
never been hardened: the artifact JSON loader (`src/core/preanalysis.rs`),
the WAV reader (`src/io/wav.rs`), the desktop app's binary peaks-cache
loader (`desktop/src/waveform/cache.rs`), and the public batch API have no
fuzz coverage, and there is no enforced policy that arbitrary input
produces `Err`, never a panic. For a library embedded in a shipping app — and a
never been hardened: the `.tsa` analysis-container loader
(`src/io/tsa.rs`), the deprecated artifact JSON loader
(`src/core/preanalysis.rs`), the WAV reader (`src/io/wav.rs`), and the
public batch API have no fuzz coverage, and there is no enforced policy
that arbitrary input produces `Err`, never a panic. For a library embedded in a shipping app — and a
prerequisite for any 1.0 — "does not panic on hostile or degenerate input"
must be a tested property, not an intention. This stage touches no DSP.

### Primary Files

- New: `fuzz/` (cargo-fuzz targets), a soak harness in `qa/`
- Audited in place: `src/core/preanalysis.rs` (JSON load path),
`src/io/wav.rs`, `desktop/src/waveform/cache.rs` (`.tspeaks` binary
load path), `src/lib.rs` (param validation), `src/error.rs`,
- Audited in place: `src/io/tsa.rs` (`.tsa` container load path),
`src/core/preanalysis.rs` (deprecated JSON load path),
`src/io/wav.rs`, `src/lib.rs` (param validation), `src/error.rs`,
engine constructors in `src/engine/`
- CI: `.github/workflows/ci.yml` (bounded fuzz on PRs, longer cron run)

### Work

- Fuzz targets: artifact JSON from arbitrary bytes; WAV parsing from
arbitrary bytes; the `.tspeaks` binary peaks cache from arbitrary bytes
(its reader is already written to reject-not-panic, with a unit-test
- Fuzz targets: the `.tsa` analysis container from arbitrary bytes (its
reader is already written to reject-not-panic, with a unit-test
corruption matrix — the fuzzer's job is to prove that property holds);
the batch `stretch()` API driven by arbitrary params ×
the deprecated artifact JSON from arbitrary bytes; WAV parsing from
arbitrary bytes; the batch `stretch()` API driven by arbitrary params ×
degenerate audio (NaN/Inf/denormal samples, zero-length, one sample,
extreme rates and sample rates).
- No-panic policy: every public entry point returns `Err` on invalid
Expand Down
Loading
Loading