cathar 0.7.3

Audio toolkit in pure Rust — denoise, de-hum, de-click, de-clip, de-reverb, normalise, and more.
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
# Cathar

![cathar — restore, enhance & level any recording, in pure Rust](https://raw.githubusercontent.com/vbasky/cathar/main/docs/banner.png)

**Name:** *cathar* is from Greek **katharós** (καθαρός), *"pure, clean"* — the
same root as **catharsis** (κάθαρσις), a cleansing. That's the whole job: take a
noisy recording and give back clean audio.

[![crates.io](https://img.shields.io/crates/v/cathar.svg?logo=rust&label=crates.io)](https://crates.io/crates/cathar)
[![docs.rs](https://img.shields.io/docsrs/cathar?logo=docs.rs&label=docs.rs)](https://docs.rs/cathar)
[![Downloads](https://img.shields.io/crates/d/cathar.svg?label=downloads)](https://crates.io/crates/cathar)
[![CI](https://img.shields.io/github/actions/workflow/status/vbasky/cathar/ci.yml?branch=main&logo=github&label=CI)](https://github.com/vbasky/cathar/actions)
[![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)
[![MSRV](https://img.shields.io/badge/MSRV-1.87-blue)](https://www.rust-lang.org)
[![Edition](https://img.shields.io/badge/edition-2024-blue?logo=rust)](https://doc.rust-lang.org/edition-guide/)
[![Stars](https://img.shields.io/github/stars/vbasky/cathar?style=social)](https://github.com/vbasky/cathar/stargazers)

**Cathar is a transparent, dependency-free audio restoration toolkit — in pure
Rust.** It works on a standalone audio file (WAV, MP3, FLAC, OGG, M4A) just as
readily as the audio track inside a video (MP4, MKV); video is never required.
Every stage is inspectable, tunable DSP — no opaque neural models, no black
boxes, so a result you don't like is a knob you can turn rather than a model you
have to re-roll. Cathar does three things and writes WAV, FLAC, or AIFF (chosen
by the output extension):

- **Restore** — denoise (phase-coherent stereo, or a learned GRU model via the
  opt-in `ml` feature), de-hum, de-wind, de-click, de-clip, de-reverb, spectral
  repair, de-plosive, de-rustle.
- **Enhance** — de-ess, breath removal, voice isolation, bandwidth extension.
- **Level** — loudness (LUFS) and peak normalisation for delivery.

No ffmpeg, no C/C++, no system libraries. Decoding is [`symphonia`], the FFT is
[`realfft`]/[`rustfft`], WAV writing is [`hound`] and FLAC is [`flacenc`] — all
pure Rust, so a single `cargo build` gives you a self-contained binary. Every
effect is also a plain function over `&[f32]`, so the same pipeline drops
straight into a Rust program or a larger media-processing pipeline.

[`symphonia`]: https://crates.io/crates/symphonia
[`realfft`]: https://crates.io/crates/realfft
[`rustfft`]: https://crates.io/crates/rustfft
[`hound`]: https://crates.io/crates/hound
[`flacenc`]: https://crates.io/crates/flacenc

## Quick start

```bash
cargo install cathar-cli                     # from crates.io — installs the `cathar` binary
```

Or from a checkout:

```bash
cargo install --path crates/cathar-cli       # install the binary from source
just setup        # one-time: enable pre-commit hook (fmt + clippy)
just build        # build the workspace
just test         # run all tests
```

```bash
# A noisy interview straight off a camera → clean dialogue:
cathar denoise interview.mp4 --out clean.wav

# Learn the room tone from a silent segment, then denoise with it:
cathar noiseprint room_tone.wav --out room.np.json
cathar denoise interview.mp4 --noiseprint room.np.json --out clean.wav

# A restoration chain, one stage at a time:
cathar dehum     recording.wav --freq 60        # kill 60 Hz mains buzz
cathar declick   recording.wav                  # interpolate impulse clicks
cathar declip    recording.wav                  # rebuild clipped peaks
cathar normalize recording.wav --target -16     # to -16 LUFS (podcast)

# Generate a synthetic noisy tone to experiment with:
cathar wave --out test.wav --duration 3 --freq 440 --noise 0.15
```

> **New to audio cleanup?** Read [**Cleaning Up Sound**]book/ — a short,
> illustrated book that explains every concept this toolkit uses from first
> principles, for readers who have never touched a DAW or DSP, with honest
> comparisons to the industry tools (iZotope RX, Audition, Audacity, SoX,
> FFmpeg). Browse it on GitHub starting from the [cover]book/, read it rendered
> at <https://vbasky.github.io/cathar/> (once Pages is enabled), or build it with
> `mdbook serve book`.

## The toolkit

Every command reads any supported format and writes WAV (32-bit float), FLAC
(24-bit lossless), or AIFF (24-bit) — the container follows the `--out`
extension (`.wav` / `.flac` / `.aif`/`.aiff`, defaulting to WAV). They are
grouped here by what they fix; run them in any order, or chain them.

### Reduce — pull noise out of the signal

| Command | What it does | Key flags |
| --- | --- | --- |
| `denoise` | Broadband denoiser — spectral subtraction (default) or Wiener filter; `--coherent` keeps the stereo image stable | `--alpha` 3.0, `--beta` 0.01, `--noiseprint <f>`, `--wiener`, `--coherent` |
| `ml-denoise` *(opt-in)* | **Learned** spectral-gain denoiser — a candle GRU predicts a per-bin suppression mask; load a trained `.safetensors` checkpoint | `--weights <f>` |
| `noiseprint` | Learn a noise profile from a silence/room-tone clip → JSON | `--out noise.np.json` |
| `dehum` | Notch out mains hum (50/60 Hz) and its harmonics; `--adaptive` tracks a drifting fundamental + per-harmonic amplitude | `--freq` 60, `--harmonics` 5, `--adaptive` |
| `dewind` | Cut low-frequency wind rumble with a 4th-order high-pass | `--cutoff` 80 |
| `dereverb` | Suppress room reverb — spectral-decay gating, or `--wpe` for Weighted Prediction Error linear-prediction dereverb | `--strength` 2.0, `--wpe`, `--taps` 15, `--delay` 3 |
| `decrackle` | Suppress dense low-level surface crackle (vinyl), distinct from de-click | `--sensitivity` 5 |
| `voiceisolate` | Keep speech, gate everything else (energy VAD + spectral gate) | `--noiseprint <f>` |
| `deesser` | Tame harsh sibilance ("sss"); `--bands >1` is multiband + adaptive | `--freq` 4000, `--threshold` -24, `--bands` 1 |
| `deplosive` | Tame plosive "p"/"b" pops (low-frequency transient bursts) | `--strength` 4 |
| `derustle` | Suppress lavalier / clothing rustle (mid-band transient bursts) | `--strength` 4 |
| `breath` | Detect and high-pass the breaths before speech onsets ||
| `riaa` | RIAA playback curve for digitized vinyl; optional elliptical mono on stereo lows | `--elliptical` 200 |
| `dequantize` | Relax quantization grain from 8/16-bit sources | `--bits` 16, `--strength` 0.7 |
| `deemphasis` | Analog playback de-emphasis (FM 50/75 µs, CD/IEC 50/15 µs) | `--curve fm50\|fm75\|cd` |

### Repair — reconstruct damaged samples

| Command | What it does | Key flags |
| --- | --- | --- |
| `declick` | Detect impulse clicks against the local RMS and rebuild the gap | `--threshold` 10.0, `--method ar\|cubic` |
| `declip` | Rebuild clipped peaks (A-SPADE by default; survey methods available) | `--threshold` 0.95, `--method spade\|cubic\|social\|omp\|nmf\|neural` |
| `repair` | Paint out isolated transient spectral artifacts (whistles, bursts, glitches) | `--strength` 4.0 |
| `inpaint` | Reconstruct dropouts/mutes by autoregressive (Janssen) interpolation — explicit span or auto zero/NaN detection | `--start-ms`, `--len-ms`, `--iterations` 3, `--max-gap-ms` 50 |

### Restore timing & pitch drift

| Command | What it does | Key flags |
| --- | --- | --- |
| `dewow` | Correct wow & flutter — track a dominant tone's instantaneous frequency and time-warp to flatten pitch ||
| `azimuth` | Correct stereo azimuth skew by aligning the right channel to the left (sub-sample cross-correlation or GCC-PHAT) | `--max-ms` 5, `--method correlation\|gcc-phat` |
| `align` | Time-align a recording to a reference track (multi-mic / reference-track workflows) | `--reference <f>`, `--max-ms` 50, `--method correlation\|gcc-phat` |
| `stereo` | Mid-side toolkit: width, mono-maker, M/S encode/decode, Haas delay, mono→stereo upmix | `--width`, `--mono-below`, `--ms`, `--from-ms`, `--upmix`, `--haas-ms` |

### Transform — time, pitch & separation

| Command | What it does | Key flags |
| --- | --- | --- |
| `tempo` | Change duration without changing pitch (WSOLA / phase vocoder) | `--factor` 1.5, `--mode wsola\|pv` |
| `pitch` | Shift pitch in semitones without changing duration | `--semitones` 2, `--mode wsola\|pv` |
| `speed` | Change speed by resampling (alters pitch *and* duration, like tape) | `--factor` 1.5 |
| `hpss` | Split into harmonic (tonal) and percussive (transient) layers (Fitzgerald median filtering) | `--harmonic <f>`, `--percussive <f>`, `--kernel` 17 |
| `sms` | Tonal purify via sinusoidal modeling — keep tracked partials, drop the stochastic residual ||

### Enhance & level

| Command | What it does | Key flags |
| --- | --- | --- |
| `enhance` | Bandwidth extension — resample up and synthesise the missing highs | `--rate` 48000, `--method replicate\|interpolate` |
| `normalize` | Loudness (LUFS, true EBU R128) or peak (dBFS) normalisation | `--target` -16, `--peak`, `--true-peak` -1 |

### Utility

| Command | What it does | Key flags |
| --- | --- | --- |
| `stats` | Peak, RMS, LUFS, true-peak, crest, noise floor, clip runs; prints a suggested `de-*` command when a check fails ||
| `resample` | Resample to a different rate (anti-aliased, any ratio) | `--rate` 48000 |
| `wave` | Generate a synthetic sine + noise test tone | `--freq` 440, `--duration` 3, `--noise` 0.1, `--sample-rate` 44100 |
| `batch` | Denoise (and optionally de-hum / normalise) a whole directory | `--indir`, `--outdir`, `--dehum <hz>`, `--normalize <lufs>`, `--exts` |
| `play` *(opt-in)* | **Winamp-style player** — plays the file with a live spectrum-analyzer visualizer | `--fft` 2048 |
| `view` *(opt-in)* | Interactive terminal **spectrogram viewer** for analysis | `--fft` 2048, `--hop` 512 |

`--target` for `normalize` is roughly: `-23` broadcast (EBU R128), `-16`
podcast, `-14` streaming.

`play` and `view` are behind the optional `tui` feature, so the default build and
its dependency set stay lean. Install with `cargo install cathar-cli --features tui`
(on Linux this needs ALSA headers: `libasound2-dev`):

- **`cathar play song.wav`** — plays the audio and animates a live, colored
  spectrum analyzer with peak-hold caps (`space` pause, `←/→` seek, `m` switches
  to an oscilloscope, `q` quits). The fun one.
- **`cathar view noisy.wav`** — a static truecolor spectrogram heatmap for
  *seeing* hiss/hum/clicks before you remove them (crosshair reads time/freq/dB,
  `+/-` zoom, `f` log frequency). The analytical one.

Both use **24-bit truecolor** where the terminal advertises it (`COLORTERM`), and
automatically fall back to the nearest **xterm-256** palette colors otherwise
(e.g. macOS Terminal.app), so the gradients render correctly everywhere.

The underlying `cathar::spectrogram` function is always available in the library.

## How denoising works

Cathar decodes to interleaved `f32` PCM, then most reduction stages run as an
**STFT (short-time Fourier transform) → modify the spectrum → inverse STFT**
loop. The denoiser uses a 2048-point FFT with a 512-sample hop (75 % overlap)
and a Hann window on both analysis and synthesis, reconstructed by overlap-add:

![cathar STFT denoise pipeline: input.mp4 → symphonia decode → f32 PCM → STFT (Hann, 2048-pt FFT, 512 hop) → magnitude + phase → spectral subtraction (phase preserved) → recombine → inverse FFT / overlap-add → clean.wav](https://raw.githubusercontent.com/vbasky/cathar/main/docs/stft-pipeline.png)

Two denoiser flavours share that frame loop:

- **Spectral subtraction** (default) — estimate the noise magnitude per bin and
  subtract `α ×` it, held above a spectral floor `β·mag` so you trade artifacts
  ("musical noise") against aggressiveness. `α` from 1→6 goes gentle→aggressive.
- **Wiener filter** (`--wiener`) — apply the statistically optimal per-bin gain
  `gain = S / (S + N)` from the estimated signal and noise power; smoother on
  stationary noise.

The noise spectrum comes either from **minimum-statistics** (the quietest ~15 %
of frames are taken as noise) or, for a cleaner result, from a **`noiseprint`**
learned off a dedicated silent segment.

### Learned denoising (`ml` feature)

The optional `ml` feature adds a third flavour that shares the same STFT frame
loop but **learns** the per-bin gain instead of deriving it from a noise
estimate. A small recurrent network — log-magnitude → linear encoder → GRU →
linear decoder → sigmoid — predicts a suppression mask in `(0, 1)` for every
time-frequency bin; the mask scales the complex spectrum (phase preserved) and
the signal is rebuilt by window-normalised overlap-add. This is the
DNS-Challenge / DeepFilterNet recipe, and unlike a noise print it can suppress
*non-stationary* noise (keystrokes, traffic, babble) that a stationary estimate
can't model.

True to Cathar's no-black-boxes rule, the architecture is open Rust
([`candle`](https://crates.io/crates/candle-core), pure Rust — no C++/ffmpeg),
weights load from an open `.safetensors` checkpoint, and inference is fully
deterministic. Build it with `--features ml`:

```bash
cargo install cathar-cli --features ml
cathar ml-denoise noisy.wav --weights denoiser.safetensors --out clean.wav
```

Without `--weights` it runs a deterministic passthrough-initialised model (every
gain ≈ 1) — a safe no-op that exercises the inference path and gives training a
known starting point. The checkpoint must match the model geometry
(`NeuralConfig`: 512-pt STFT, 256-wide GRU by default); parameter names follow
PyTorch's (`enc.weight`, `gru.weight_ih_l0`, `dec.bias`, …) so a standard
training export loads without renaming.

## Inside each tool

Every stage is classic, inspectable DSP — no black boxes.

| Tool | Technique |
| --- | --- |
| `denoise` | STFT 2048/512, Hann; spectral subtraction `max(mag−α·N, β·mag)` or Wiener `S/(S+N)`. `--coherent` derives one gain mask from the mid (L+R) signal and applies it to every channel, so the stereo image stays put |
| `ml-denoise` *(opt-in)* | STFT 512/128, Hann; a candle GRU (log-mag → encoder → GRU → decoder → sigmoid) predicts a per-bin gain mask, applied with phase preserved and window-normalised overlap-add. Weights load from `.safetensors`; pure Rust, deterministic |
| `dewind` | 4th-order Butterworth high-pass (two cascaded biquads, ~24 dB/oct) at `--cutoff` |
| `noiseprint` | Per-bin magnitude spectrum of a noise clip, serialised to JSON |
| `dehum` | Cascade of 2nd-order IIR notch biquads (Q = 30) at the base frequency and each harmonic up to Nyquist |
| `declick` | Sliding-window local RMS detects clicks; default rebuild is autoregressive Janssen interpolation (`--method ar`, same family as `inpaint`); `--method cubic` keeps the legacy Hermite fill |
| `declip` | Default **A-SPADE** (Kitić, Bertin & Gribonval) over a Hann-windowed 4×-overlap Gabor tight frame. Also: `--method social` (PEW social sparsity), `omp` (constrained DFT matching pursuit), `nmf` (STFT magnitude NMF), `neural` (deep-unfolded soft-threshold ISTA), `cubic` (fast Hermite fill) |
| `repair` | STFT 2048/512; per bin, compare magnitude to its temporal median (±4 frames) and pull transient outliers back to the median, phase preserved — sustained content is untouched, overlap-add is window-normalised to unity |
| `dereverb` | Two-pass spectral-decay gating: track each bin's envelope (8 ms attack / 50 ms release), gate bins sitting near their reverb floor |
| `voiceisolate` | Energy VAD on 20 ms frames (gap-fill < 120 ms, drop segments < 50 ms) + spectral gating of non-speech (tighter with a noiseprint) |
| `deesser` | STFT 2048/256; single-band compresses the HF region when its power ratio exceeds the threshold. `--bands >1` splits the sibilant region into sub-bands, each compressed when it rises `threshold` dB above its own EMA-tracked running level (multiband + adaptive) |
| `deplosive` / `derustle` | STFT; per frame measure energy in a band (plosive < 250 Hz, rustle 1.5–6 kHz); frames whose band energy spikes above the temporal median are scaled back toward it, phase preserved, sustained content untouched |
| `breath` | VAD-flag the frames just before a speech onset (≤ 150 ms) and high-pass them at 200 Hz, mixed 40 / 60 dry/wet |
| `resample` | Kaiser-windowed sinc (16 lobes, β = 9), arbitrary ratio; cutoff tracks the lower Nyquist so downsampling is anti-aliased and upsampling adds no imaging |
| `enhance` | Shared resampler to the target rate, then spectral band replication (4096 FFT) folds the existing top band into the empty highs with a tiled rolloff |
| `decrackle` | Second-difference (Laplacian) detector over a running EMA noise floor flags dense impulsive crackle; each micro-run is repaired by cubic-Hermite interpolation |
| `inpaint` | Autoregressive (Janssen/Godsill–Rayner) interpolation: an AR model is fit to the samples around the gap (Levinson–Durbin), the missing block solved by banded Cholesky, iterated; order scales with gap length |
| `dehum --adaptive` | Locate the precise fundamental from a spectral peak, then cancel each harmonic with an I/Q heterodyne canceller (demodulate → zero-phase low-pass → subtract) that tracks amplitude and small frequency drift |
| `deemphasis` | Exact first-order bilinear de-emphasis: FM 50/75 µs single-pole roll-off, CD/IEC 50/15 µs shelf; unity gain at DC |
| `dewow` | Track a dominant tone's instantaneous frequency by I/Q heterodyne demodulation, form a mean-normalised speed curve, then time-warp (resample at φ⁻¹, φ = ∫speed) to flatten pitch |
| `azimuth` / `align` | Sub-sample lag from normalised cross-correlation or **GCC-PHAT** (parabolic-interpolated peak) + fractional-delay shift |
| `stereo` | Exact M/S encode–decode; width = scale Side; mono-below reuses elliptical crossover; upmix = Haas delay on R; `stats` reports L/R phase correlation |
| `dereverb --wpe` | Per-frequency-bin Weighted Prediction Error: weighted (inverse-power) linear prediction of the current STFT frame from K frames past a delay, subtracted and refined over iterations (complex Hermitian solve) |
| `hpss` | Fitzgerald median filtering on the STFT magnitude — horizontal median → harmonic, vertical → percussive — with a soft Wiener mask; percussive derived by subtraction for exact reconstruction |
| `tempo` / `pitch` / `speed` | WSOLA waveform-similarity overlap-add (default) or a phase vocoder with instantaneous-frequency phase propagation; `pitch` = time-stretch ∘ resample; `speed` = resample only |
| `sms` | Per-frame spectral-peak picking (parabolic), nearest-frequency partial tracking, phase-continuous additive resynthesis; keeping only partials drops the stochastic residual |
| `normalize` | Peak: scale so the loudest sample hits the dBFS target. Loudness: ITU-R BS.1770-4 / EBU R128 integrated LUFS (K-weighting, gated) measured jointly across channels, applied as one broadband gain and held back to the `--true-peak` dBTP ceiling (4× oversampled) so it never clips |

## Library usage

The `cathar` crate is the same engine the CLI drives.

```rust
use cathar::{AudioData, Denoiser, SpectralDenoiser, dehum};

let audio = AudioData::from_file("interview.mp4")?;   // symphonia decode → f32
let sr = audio.sample_rate;

// Denoise and de-hum per channel via `map_channels`, then normalise to
// -16 LUFS (EBU R128) with a -1 dBTP true-peak ceiling. Loudness is measured
// across all channels jointly, so normalisation is a whole-signal method.
let clean = SpectralDenoiser::default()
    .denoise(&audio)?
    .map_channels(|ch| dehum(ch, sr, 60.0, 5))
    .normalize_r128(-16.0, -1.0);

clean.to_file("clean.wav")?;   // 32-bit float WAV via hound
```

Learn a noise print once and reuse it for a tighter subtraction:

```rust
use cathar::{AudioData, Denoiser, SpectralDenoiser, learn_noise_print};

let print = learn_noise_print(&AudioData::from_file("room_tone.wav")?)?;

let audio = AudioData::from_file("interview.mp4")?;
let clean = SpectralDenoiser::with_noise_print(print, /* alpha */ 3.0, /* beta */ 0.01)
    .denoise(&audio)?;
clean.to_file("clean.wav")?;
```

The public surface is small and direct:

- **`AudioData { sample_rate, channels: Vec<Vec<f32>> }`**`from_file`,
  `to_file`, `map_channels(|&[f32]| -> Vec<f32>)` for per-channel effects,
  `normalize_r128(target_lufs, true_peak_ceiling_db)` for whole-signal loudness,
  and `resample(target_rate)` for the main-path resampler.
- **`Denoiser`** trait + **`SpectralDenoiser`** (configurable `fft_size`,
  `hop_size`, `alpha`, `beta`, `noise_frame_ratio`, optional `noise_print`);
  `denoise` and `denoise_coherent` (phase-coherent stereo).
- **`NoisePrint`** + `learn_noise_print` + `wiener_denoise`.
- **`NeuralDenoiser`** + **`NeuralConfig`** *(opt-in `ml` feature)* — the learned
  spectral-gain denoiser; `new()` for the passthrough default, `from_safetensors`
  to load trained weights. Implements the same `Denoiser` trait.
- **`Stats`** + `compute_stats` — peak, RMS, LUFS, true-peak, crest, DC, noise
  floor, SNR, clip runs; `Stats::suggestions` names a restoration command per
  failing check.
- Free functions: `dehum`, `dewind`, `declick` / `declick_with_method`, `declip` / `declip_with_method`, `spectral_repair`,
  `deplosive`, `derustle`, `dereverb`, `voice_isolate`, `deesser`,
  `deess_multiband`, `breath_remove`, `bandwidth_extend`, `resample`,
  `normalize_peak`, `integrated_loudness`, `true_peak_dbtp`, `generate_wave`.

## Formats & I/O

| Stage | Detail |
| --- | --- |
| **Reads** | MP4, M4A, MKV, MP3, FLAC, WAV, OGG — any container/codec [`symphonia`] decodes (built with `features = ["all"]`) |
| **Decodes to** | 32-bit float PCM, one `Vec<f32>` per channel, at the file's native sample rate |
| **Writes** | 32-bit float WAV via [`hound`] — no inter-stage quantisation |
| **Resampling** | Only on the `enhance` path (windowed sinc); every other stage runs at the source rate |
| **Channels** | Preserved; effects run independently per channel |

## Architecture

A deliberately small two-crate workspace — a library and the binary that drives it.

```text
cathar/
├─ crates/
│  ├─ cathar/        # the engine: decode (symphonia) · DSP · encode (hound)
│  └─ cathar-cli/    # the `cathar` binary — clap subcommands over the engine
└─ docs/             # banner + assets
```

| Dependency | Role |
| --- | --- |
| `symphonia` (`all`) | Decode every supported container/codec to `f32` PCM |
| `realfft` / `rustfft` | Forward/inverse real FFT behind every STFT stage |
| `hound` | Write 32-bit float WAV |
| `clap` (derive) | CLI parsing |
| `serde` / `serde_json` | `NoisePrint` serialisation (`*.np.json`) |
| `thiserror` / `anyhow` | Library error type / CLI error reporting |
| `candle-core`, `candle-nn` | *(optional `ml` feature)* pure-Rust tensors + GRU for the learned `ml-denoise` model |

## Design

| Principle | What it means |
| --- | --- |
| **Pure Rust** | No ffmpeg, no C/C++ FFI, no `pkg-config` — one `cargo build` produces a self-contained binary |
| **Lossless float pipeline** | Decode → `f32` → process → 32-bit float WAV; nothing is quantised between stages |
| **Composable** | Every effect is a plain `fn(&[f32], …) -> Vec<f32>`; chain them in any order, in the CLI or as a library |
| **Inspectable DSP** | Classic, documented algorithms (STFT subtraction, Wiener, IIR notches, cubic interpolation) — not opaque models |
| **Deterministic** | Single-threaded and frame-synchronous: the same input always yields the same output |

## Pipeline integration

Because the whole toolbox is a library of `&[f32]` functions plus a single
static binary with no system dependencies, cathar slots cleanly into a larger
media pipeline: call it in-process through the `cathar` crate, or shell out to
`cathar <stage> …` between other steps. Inputs are read straight from the
container files, so it can sit immediately after ingest and before encoding.

## Roadmap

Cathar is `0.6.x`, restoration-first, and growing — before `1.0` — into a
general-purpose, pure-Rust audio swiss-army knife (a SoX-class tool with no
ffmpeg and no C/C++ FFI). See [`ROADMAP.md`](https://github.com/vbasky/cathar/blob/main/ROADMAP.md) for the full plan and
SoX-parity checklist. The `0.2`–`0.4` foundations are complete:

- **True EBU R128 loudness** (`normalize`) — K-weighted gated LUFS with a
  `--true-peak` dBTP ceiling.
- **Main-path resampling** — the `resample` command + `AudioData::resample`, a
  shared anti-aliased Kaiser-windowed sinc any stage can call.
- **Encode beyond WAV** — 24-bit lossless FLAC and 24-bit AIFF on the pure-Rust
  default path, selected by the output extension.

Phase 1 `0.5` (DSP depth) is complete, and `0.6` makes the `ml` feature real:
the optional `ml-denoise` command runs a candle GRU spectral-gain model (load
trained `.safetensors` weights). Next is the swiss-army expansion (Phase 2) —
see [`ROADMAP.md`](https://github.com/vbasky/cathar/blob/main/ROADMAP.md).

## Development

`just check-all` runs fmt-check, clippy (`-D warnings`), tests, and docs — the
same gate CI enforces on Linux and macOS.

| Task | Command |
| --- | --- |
| Build | `just build` / `just build-release` |
| Format | `just fmt` (`just fmt-check` to verify) |
| Lint | `just lint` |
| Test | `just test` |
| Docs | `just docs` |
| Audit | `just deny` (needs `cargo install cargo-deny`) |
| Run | `just run -- <args>` |

## License

Licensed under either of [Apache License, Version 2.0](https://github.com/vbasky/cathar/blob/main/LICENSE-APACHE) or
[MIT license](https://github.com/vbasky/cathar/blob/main/LICENSE-MIT) at your option.