Skip to content

Clean Up a Batch of Recordings ​

A folder of rough takes lands on your drive: some clipped because the gain was too hot, some carrying 50 or 60 Hz mains hum from a bad ground, most with a second or two of dead air at both ends. You do not know, file by file, which problem which take actually has. This page measures that first, repairs only the stages the measurement justifies, and writes a record of both.

The job is not "run declip on everything". A repair stage that runs when nothing was wrong costs quality for nothing — declip resynthesizes samples that were never clipped, dehum notches out real musical content sitting at the mains frequency. The point of the repair assistant is that it declines a stage the evidence does not call for, and --explain is how you check that it actually did.

This page uses the Python CLI (pip install libsonare, command sonare). declip (Step 7) is not in the native sonare-cli; every other command here exists in both. See Which CLI Are You Using?.

What you need

  • Install: pip install libsonare (Python 3.11 or later). The Node tab needs npm install @libraz/libsonare (Node.js 22.12 or later).
  • Mono takes in raw/: WAV files, any common sample rate. The CLI repairs mono: a stereo take is folded to mono and written back as a mono file (see Step 8 for stereo).
  • For Step 4: two or three takes of the same part, take1.wav to take3.wav, at one sample rate.
  • Test material: the takes on this page are synthetic. The script below writes them, and every number printed here comes from running the steps on its output. take-raw.wav is raw/take-01.wav (copy it under that name).
A script that writes damaged test takes

Save as make_takes.py and run python make_takes.py. It needs NumPy, which pip install libsonare already installs. It writes raw/take-01.wav (clipped, with 50 Hz hum and clicks), raw/take-02.wav (hum and clicks, no clipping) and take1.wav to take3.wav for Step 4, all mono at 48 kHz.

python
"""Write damaged test takes: raw/take-01.wav, raw/take-02.wav, take1-3.wav."""
import os
import wave

import numpy as np

SR = 48000
rng = np.random.default_rng(7)


def save(path, x):
    pcm = (np.clip(x, -1.0, 1.0) * 32767).astype("<i2")
    with wave.open(path, "wb") as w:
        w.setnchannels(1)
        w.setsampwidth(2)
        w.setframerate(SR)
        w.writeframes(pcm.tobytes())


def phrase(seconds):
    t = np.arange(int(seconds * SR)) / SR
    chord = sum(np.sin(2 * np.pi * f * t) for f in (220.0, 277.2, 329.6)) / 3
    return 0.4 * chord * np.minimum(1.0, t / 0.02) * np.exp(-t / 3.0)


def damage(x, clip_gain=None):
    t = np.arange(len(x)) / SR
    x = x + 0.03 * np.sin(2 * np.pi * 50 * t) + 0.01 * np.sin(2 * np.pi * 150 * t)  # mains hum
    for i in rng.integers(SR, len(x) - SR, 6):  # sparse clicks
        x[i : i + 3] += 0.5 * np.array([1.0, -0.8, 0.4])
    if clip_gain is not None:
        x = np.clip(x * clip_gain, -1.0, 1.0)  # input gain set too hot
    pad = np.zeros(int(0.3 * SR))  # dead air at both ends
    return np.concatenate([pad, x, pad])


os.makedirs("raw", exist_ok=True)
save("raw/take-01.wav", damage(phrase(7.0), clip_gain=4.0))
save("raw/take-02.wav", damage(phrase(7.0)))

# Three takes of one part for Step 4: three phrases each, entering at slightly different times.
for take, shift in enumerate((0.0, -0.1, 0.08), start=1):
    x = np.zeros(int(8.5 * SR))
    for start in (0.6, 3.2, 6.0):
        s = int((start + shift) * SR)
        p = phrase(1.8)
        x[s : s + len(p)] += p
    save(f"take{take}.wav", x)

Running the batch on these two takes selects the same stages as below: declip, declick, decrackle and dehum for take-01, and the same minus declip for take-02.

Terms used here are defined in Repair and Input Controls. For the library workflow and individual defects, see Audio Repair, Noise and Hum Removal, Clicks, Crackle, and Clipping, and Dereverberation.

What You Will Learn ​

By the end of this page you should be able to:

  • measure exactly what is wrong with a take before touching it, and read the reason behind every repair stage the assistant chooses — or declines to run;
  • script repair, trim-silence, and declip into a per-file batch that writes both a clean take and a JSON record of what was found and what was done;
  • tell apart three ways of driving repair — measure-and-choose, a named preset, a field override — and know which situation each one is for, including which of the five restoration presets to reach for first;
  • treat a repair as the lossy trade it is, and recognize when chasing a measurement to zero costs more than it is worth.

The whole job ​

Everything below is this script. Run it against a folder of takes, then read on for what each stage is doing and why.

bash
#!/usr/bin/env bash
set -euo pipefail

mkdir -p clean reports

for raw in raw/*.wav; do
  name="$(basename "${raw%.wav}")"

  # 1. Measure the damage. Writes nothing -- safe to run on every take.
  sonare repair "$raw" --detect --json > "reports/${name}.before.json"

  # 2. Trim the dead air every take was padded with. This comes first: dehum's
  #    notch rings into the padding and a later trim would find nothing to cut.
  sonare trim-silence "$raw" -o "clean/${name}.trimmed.wav" --json \
    > "reports/${name}.trim.json"

  # 3. Let the assistant choose only the stages step 1's evidence calls for.
  sonare repair "clean/${name}.trimmed.wav" -o "clean/${name}.wav" --explain --json \
    > "reports/${name}.repair.json"

  # 4. Measure again. This is the verification step, not a separate audit.
  sonare repair "clean/${name}.wav" --detect --json > "reports/${name}.after.json"

  # 5. Fold the four records into one per-file report.
  python3 - "$name" <<'PY'
import json
import sys

name = sys.argv[1]
before = json.load(open(f"reports/{name}.before.json"))["defects"]
repair = json.load(open(f"reports/{name}.repair.json"))
trim = json.load(open(f"reports/{name}.trim.json"))
after = json.load(open(f"reports/{name}.after.json"))["defects"]

record = {
    "file": name,
    "detect_before": before,
    "stages_chosen": repair["stages"],
    "reasons": repair.get("explanation", []),
    "output_gain_db": repair["output_gain_db"],
    "trimmed_duration": trim["duration"],
    "detect_after": after,
}
with open(f"reports/{name}.json", "w") as f:
    json.dump(record, f, indent=2)
PY

  rm -f "reports/${name}.before.json" "reports/${name}.repair.json" \
        "reports/${name}.trim.json" "reports/${name}.after.json"
done

Run against two takes — one hard-clipped with mains hum, one only hummy and clicky — this prints:

text
reports/take-01.json  ->  repair.declip, repair.declick, repair.decrackle, repair.dehum
reports/take-02.json  ->  repair.declick, repair.decrackle, repair.dehum

take-02 has no clipping, and declip is absent from its stage list. That single line is the entire argument for measuring first: the assistant looked at two different takes and repaired two different things.

Batch cleanup pipeline
audioRaw takes (WAV)repair --detecttrim-silenceAssistantpicks stagesrepair --explainClean takerepair --detect(verify)Per-file record.json
The detector runs twice on every take: once to decide what needs fixing, once to prove the fix worked.

Step 1 — Measure before touching anything ​

bash
sonare repair take-raw.wav --detect --json
typescript
import { readFileSync } from 'node:fs';
import { decodeChannels, init, masteringRepairAnalyze } from '@libraz/libsonare';

await init();
// One Float32Array per channel: one for a mono take, two for a stereo one.
const { sampleRate, channels } = decodeChannels(new Uint8Array(readFileSync('take-raw.wav')));
const { defects, recommended, explanation } = masteringRepairAnalyze({ channels, sampleRate });
console.log(defects.clipSampleFraction, defects.humFundamentalProminence, recommended.map((s) => s.stage), explanation);
python
import libsonare as sonare
from libsonare import Audio

# One array per channel: one for a mono take, two for a stereo one.
path = "take-raw.wav"
channels = []
for i in range(Audio.file_channel_count(path)):
    with Audio.from_file_channel(path, i) as audio:
        channels.append(audio.data)
        sample_rate = audio.sample_rate

analysis = sonare.mastering_repair_analyze(channels, sample_rate)
print(analysis["defects"]["clipSampleFraction"], analysis["defects"]["humFundamentalProminence"])
print([s["stage"] for s in analysis["recommended"]], analysis["explanation"])

--detect measures and reports; it writes no file (the Python CLI rejects -o alongside it). That makes it free to run on every take in a folder before you decide anything. The full option set is in CLI Reference — repair.

The library call covers the measuring of Step 1 and the choosing of Step 3: recommended and explanation are the stages the assistant would run and why, and masteringRepairApply runs them. It measures every channel, so the stereo warning in Step 8 does not apply to it. The CLI reports the same measurements under snake_case names (clip_sample_fraction).

json
{
  "mode": "detect",
  "defects": {
    "click_count": 6,
    "click_rejected": 307,
    "crackle_per_second": 2.368,
    "clip_sample_fraction": 0.01259,
    "clip_run_count": 164,
    "clip_longest_run_samples": 51,
    "clip_flat_level": 0.99997,
    "noise_floor_dbfs": -31.43,
    "hum_peak_found": true,
    "hum_fundamental_hz": 50.0,
    "hum_fundamental_prominence": 23.98,
    "hum_harmonics": 2,
    "late_decay_ratio_db": -5.85
  }
}

Four numbers already tell most of the story on this take (this is the raw file; once the dead air is trimmed in Step 2, the same defects sit in a shorter file, so the fractions and rates rise a little): clip_sample_fraction says 1.26% of the samples are clipped, clip_flat_level near 1.0 says they are pinned flat rather than merely loud, a mains harmonic sits at exactly 50 Hz with a prominence of 24.0, and a nonzero crackle_per_second (2.37) says short impulsive crackle is present. hum_fundamental_prominence is a plain ratio, not decibels: the energy of the winning hum candidate divided by the median of all the candidates the search tried (the median is the middle value, so one outlier does not move it). 1.0 means no peak stood out at all; 24.0 means one frequency carries about twenty-four times the energy of a typical candidate. click_count is 6, the six clicks the script wrote; click_rejected (307) is how many impulsive-looking events the detector looked at and decided were not clicks, which is worth knowing before you conclude the detector missed something.

Step 2 — Trim the dead air ​

bash
sonare trim-silence take-raw.wav -o take-trimmed.wav --json
json
{"length": 339200, "sample_rate": 48000, "duration": 7.0667, "threshold_db": -60.0, "n_fft": 2048, "hop_length": 512, "output": "take-trimmed.wav"}

The take was 7.6 seconds with 0.3 seconds of padding at each end; the trimmed file is 7.0667 seconds, so 0.533 seconds of dead air came off (cuts land on the 512-sample hop grid). --threshold-db (default −60 dB) is the level below which a frame counts as silence; --top-db is also accepted if you would rather express the same cut relative to the file's own peak. Trimming has nothing to do with repair and runs whether or not the take needed one. Run it on the raw take, before repair, and repair the trimmed file in the next step. The repaired take is not silent in its padding any more, because dehum's notch rings into it (the padding peaks at 0.07 and 0.10 after dehum alone, and stays at 0 after declip, declick or decrackle), so the same command on a repaired take keeps all 7.6 seconds. The library has its own trimmers (trimSilence, masteringRepairTrimSilence) with their own options; their output length is not guaranteed to equal this command's, so this step is a CLI step.

Step 3 — Let the assistant choose ​

bash
sonare repair take-trimmed.wav -o take-clean.wav --explain --json
json
{
  "mode": "assistant",
  "stages": ["repair.declip", "repair.declick", "repair.decrackle", "repair.dehum"],
  "explanation": [
    "declip: runs of samples sit pinned at one level",
    "declick: impulsive runs stand out from their neighbours",
    "decrackle: samples depart from the local median",
    "dehum: a prominent harmonic series sits on a mains frequency"
  ],
  "output": "take-clean.wav",
  "output_gain_db": -2.72
}

This step repairs take-trimmed.wav, the output of the trim, which is the file the batch script repairs. Every stage names the exact evidence that triggered it, in the same vocabulary the detector used. denoise is absent because this take's noise floor sits far below the program: the integrated loudness is -11.3 LUFS and the floor about 20 dB under it, while the assistant adds denoise only when the floor comes within 12 dB. (dBFS and LUFS are both relative to full scale, so this is a rough rule, not a like-for-like measurement. The -31.43 dBFS floor in Step 1 is mostly the 50 Hz hum itself, as Step 5 shows.) dereverb is never chosen by the assistant: no reverb measurement separates a reverberant take from a dry sustained one reliably, so dereverb runs only when you ask for it through a preset or --params.

The assistant's triggers are presence tests, not severity tests:

Repair stages in run order
detectorstage, in run orderwhat it repairsshort-time defectsmasteringRepairDetectClipping1declipflat-topped peaks, a hard digital ceilingmasteringRepairDetectClicks2declickisolated discontinuities and popsmasteringRepairDetectCrackle3decrackledense, sample-scale surface defectssustained defectsmasteringRepairDetectHum4dehuma 50/60 Hz tone and its harmonicsmasteringRepairDetectNoiseFloor5denoisea steady broadband noise floormasteringRepairDetectReverb6dereverba diffuse tail after the direct soundA stage runs only when enabled. Enabling one never moves it.
Each detector enables one stage, and the stages run in this fixed order.
StageThe assistant runs it when
declipruns of samples sit pinned at one level, and repairing at that level would not touch louder unclipped audio
declickthe detector found at least one click
decracklethe detector found any crackle
dehumthe hum peak lies within 0.25 Hz of 50 or 60 Hz and its prominence (see Step 1) is above 10
denoisethe noise floor is less than 12 dB below the take's integrated loudness

That is why a take with a single click still gets declick. Gate a Delivery in CI asks a different question about a finished master, "is there enough damage to reject it?", and uses rates for that: it tolerates up to 0.2 clicks and 1.0 crackle events per second, because the detectors report a small baseline on clean program material.

output_gain_db is not a repair stage. The CLI scales the repaired file so its highest sample lands exactly at full scale, whenever the repair pushed a sample past it (declip routinely does, because it rebuilds the peaks a clipper cut off). The number is that one gain for the whole file: it tells you how far the peak went over, not how much of the waveform changed.

Every take is scaled on its own

Because the gain is fitted per file, two takes from the same session come out of repair at different levels, each with its peak at 0 dBFS. Compare and comp takes before repair, or bring the repaired takes back to a common level, before you judge one against another. A peak sitting at full scale also leaves no room for inter-sample peaks; re-stage the take to around -18 LUFS, as in Mix and Master a Song in the CLI, before it goes into a mix or a master.

--explain and --detect do not combine

--detect runs no repair stage, so there is nothing to explain; passing both is rejected as an invalid parameter. Drop --detect to see the reasoning, or drop --explain to just get the numbers.

denoise did not run on this take, so nothing above shows what it does. The demo below does: a clean sustained chord is given a layer of broadband hiss, and the repair stage removes it. Flip Compare to hear each side — the gain is untouched, so the hiss is the only thing that moves — and switch the algorithm to see how much of the raised high-frequency floor each one pulls down.

A/B PROCESS · DENOISEIDLE
Denoise repair — damaged vs repaired

Broadband hiss is added to a clean chord. Compare the noisy and denoised signals and their spectra, then choose a denoising algorithm. Both retain their processed levels without extra loudness matching. Noise reduction can also change musical transients and timbre. FLOOR shows the average high-band reduction in dB.

Compare
Algorithm

Step 4 — Cut several takes at the silence they share ​

Sometimes the problem is not one bad take but several decent ones: three attempts at the same phrase, each entering and trailing off at a slightly different point, and you want to compare them slice by slice. Running trim-silence from Step 3 on each take separately does not help here — every take gets its own cut points, so the boundaries land in a different place on each file, and the "same" slice from three takes is really three different slices.

split-silence takes more than one file at once for exactly this job. The first take is the positional argument; every other take of the same part is added with a repeatable --input:

bash
sonare split-silence take1.wav --json
json
[{"start_sample": 28160, "end_sample": 116224}, {"start_sample": 153088, "end_sample": 241152}, {"start_sample": 287232, "end_sample": 375808}]

One take alone reports three non-silent stretches, one per phrase — the same shape trim-silence finds, expressed as sample ranges rather than a single trimmed length. Add the other two takes of the same part:

bash
sonare split-silence take1.wav --input take2.wav --input take3.wav --json
json
[{"start_sample": 23040, "end_sample": 120320}, {"start_sample": 147968, "end_sample": 245248}, {"start_sample": 282624, "end_sample": 379392}]

The first interval widens from 28160–116224 to 23040–120320. That widening is the point of the feature: the reported intervals are the union of each take's own non-silent stretches, merged where they touch, so a cut lands only where every take is quiet — and mid-phrase in none of them. Cut each take to its own intervals and one take's early entrance becomes a clipped attack in another take's slice; cut all three to the union instead and every take loses exactly the silence they all share, nothing more.

split-silence never accepts -o; it only reports intervals, and passing --output exits with a usage error (exit code 2). --write-takes PREFIX is how the shared intervals become audio, one file per take per interval, named PREFIX{take:02d}_{interval:03d}.wav with both counters 1-based:

bash
sonare split-silence take1.wav --input take2.wav --input take3.wav --write-takes comp --json

Three takes and three shared intervals write nine files, comp01_001.wav through comp03_003.wav. Within one interval the files come out the same length across takes — 97280 samples for interval 1, on every take — even though the takes themselves do not line up sample-for-sample. A take that ends before the union's boundary is padded with silence for the rest of that stretch rather than sliced shorter, which is exactly what lets comp01_001.wav, comp02_001.wav, and comp03_001.wav sit side by side: same start, same length, three different performances of the same phrase.

What this does not do: it does not choose the best take, it does not crossfade between them, and it does not correct timing drift inside a phrase — a take that rushes or drags in the middle still rushes or drags after slicing. It hands you aligned slices to judge, not a finished comp.

The demo below does the choosing that this command leaves to you: three takes, one pick per segment, joined by short crossfades.

ARRANGEMENT · COMPINGIDLE
Comping — one performance from three takes

Compare the three takes, then choose one for each note-pair segment. Take B is bright but plays G#5 instead of A5 at the start of segment 3. The default B/A/A/C comp keeps its opening and uses Take A for that note. Short crossfades join the selected audio.

Segment 1
Segment 2
Segment 3
Segment 4

Every take must share a sample rate; the check runs before anything else:

bash
sonare split-silence take1.wav --input take2.wav --input t3_44.wav --json
# Error: take sample rate differs: t3_44.wav is 44100 Hz, the first take is 48000 Hz

The mismatch is rejected as an invalid parameter (exit code 3), the same class as an unrecognized --params key elsewhere on this page.

--top-db is measured from each take's own peak, not an absolute level

A noisier take finds less silence than a clean one at the same --top-db, because the threshold sits relative to that take's own peak rather than a fixed dBFS floor. On take1.wav with Gaussian noise of standard deviation 0.0001 added (seed 1), repair --detect reads a noise floor of −81.5 dBFS and the three phrases keep clear gaps between them. With a standard deviation of 0.0003 the floor reads −71.2 dBFS, and the same take collapses to a single interval, 0 to 408000, the whole file: the threshold no longer finds anywhere quiet enough to call silence. There is no fixed dB of headroom that guarantees a clean split; check noise_floor_dbfs first, and repair a noisy take before comping, for example with sonare repair --preset tapeHiss or by forcing the stage with --params repair.denoise.enabled=1 (the assistant's own rule in Step 3 would not trigger denoise at a floor of -71 dBFS), rather than lowering --top-db to compensate.

Step 5 — Verify: run --detect again ​

The detector is also the acceptance test. Re-run it on the result and compare:

bash
sonare repair take-trimmed.wav --detect --json   # the "Before" column
sonare repair take-clean.wav --detect --json     # the "After" column
FieldBeforeAfter
clip_sample_fraction0.013540.0000825
click_count60
crackle_per_second2.5470
hum_fundamental_prominence22.712.25
hum_harmonics20
noise_floor_dbfs−25.04−40.71

Clipping falls to 28 samples in three short runs, only three of them pinned flat, and clicks and crackle clear to zero. Hum prominence drops to 2.25, and although the detector still names a candidate at 60 Hz, a prominence that low is the strongest of a flat set of candidates rather than a hum. noise_floor_dbfs falls as well, because the 50 Hz hum and its harmonics were what the low band was measuring. Neither number is zero, which is what an honest repair looks like.

The residual prominence of 2.25 is below the assistant's own trigger of 10, so a second repair pass would not run dehum again, and well below the 6.0 that Gate a Delivery in CI rejects a finished master at. Listen to the low end at the level the take will be played; if a stage removed something you wanted, the field overrides below trade that against the residual.

The detector is the point

There is no separate "did it work" command. The same --detect that decided what to fix is what proves whether fixing it worked, on the same numbers, in the same units.

Step 6 — When you already know what you want ​

Two escapes exist for when you do not want the assistant deciding.

A named preset skips measurement and takes the repair stages straight from a mastering preset's own configuration. sonare mastering-presets --json lists all thirty names; five of them are restoration presets, carrying repair stages and nothing else, and each is a routing decision made from what you already know about the source rather than from what the file measures:

You are holdingReach for firstStages it runs, in chain order
An LP transfer — clicks and pops from groove wear, surface crackle, a noise floor under everythingvinyldeclick, decrackle, denoise
A 78 rpm shellac transfer — wider pops and a denser, higher surface noise than an LPshellac78the same three as vinyl, each pushed harder: longer click runs, a lower crackle threshold, deeper noise reduction
A cassette or reel transfer — steady broadband hiss and nothing elsetapeHissdenoise
A location or outdoor capture — hiss, mains hum that drifts off exactly 50 or 60 Hz, the space around itfieldRecordingdehum (adaptive tracking), denoise, dereverb
A phone or laptop voice memo — clipped against its own AGC (the phone's automatic gain control), a high mic floor, whatever room the speaker was invoiceMemodeclip, denoise, dereverb

When none of the five fits — a take with hum and clicks but no hiss, say — do not pick the nearest one: drop --preset and let the measure-and-choose path above decide from the evidence, then lean on a field override (below) if a single stage needs more. The parameters each preset sets, with their defaults, are cataloged on Mastering Processors; the reasoning for choosing one preset over another is on Choosing a Mastering Preset.

--preset and --explain do not combine either, for the same reason as --detect: a preset's stages are named directly, not chosen by measurement, so there is nothing for --explain to report.

bash
sonare repair take-trimmed.wav --preset voiceMemo -o take-voicememo.wav --json
json
{"mode": "preset", "preset": "voiceMemo", "stages": ["repair.declip", "repair.denoise", "repair.dereverb"]}

voiceMemo runs declip, denoise, and dereverb on this take — never dehum, because that is simply not a stage the preset carries, regardless of what the file measures. Re-detecting the result confirms it: hum_fundamental_prominence comes back at 22.59, essentially unchanged from the 22.71 it started at. A preset is the right tool when you already know the source (a phone voice memo, a field recording) and want the same fixed treatment every time; it is the wrong tool when you want the file's own evidence to decide, which is what --explain on the measure-and-choose path is for.

What a record-style restoration removes — and what it leaves for other stages — is audible below. The clip is a piano turnaround carrying the damage the restoration presets are routed at: mains hum, surface noise and hiss, sparse clicks and crackle. The stage applied here is the classical dereverberator alone, so what drops out is the noise bed and the smeared tails; the clicks and the hum stay, because they belong to declick and dehum.

A/B PROCESS · DEREVERBIDLE
Restoration — a worn record, before and after

The piano clip contains hum, hiss, clicks and crackle. This example applies only the classical dereverberator, which attenuates diffuse sustained energy; it does not apply a dedicated declick or dehum stage. Compare the spectra and audition the result without added loudness matching. The processor can change musical tails and timbre as well as the noise bed. FLOOR shows the high-band reduction in dB.

Compare

A field override changes one parameter of whichever path you took, via --params repair.<stage>.<field>=value,.... Pushing the assistant's own dehum stage harder illustrates the trade-off from Step 5 directly:

bash
sonare repair take-trimmed.wav -o take-clean-harm.wav --params repair.dehum.harmonics=6 --json

Raising the harmonic count the notch chases from the assistant's own 2 to 6 does not help: hum_fundamental_prominence comes back at 2.51 against 2.25 for the default run, a residual that is no better and slightly worse. output_gain_db changes too, to −2.92 dB against −2.72 dB, but that only says the repaired file peaks higher before the full-scale fit. A notch is a narrow filter that removes one frequency; it also shifts the phase of everything near it, which can move a waveform's peaks up or down; the number is not a measure of how much was changed. The cost of the extra notches is what they remove: every notch also takes out any real musical content at 100, 150, 200 Hz and so on, which on bass and kick drums is a great deal. This is the shape every "chase it to zero" instinct takes with dehum: a residual that barely moves, for content you can hear go missing. The Repair and Input Controls glossary page makes the same point about denoise — a little noise left in beats smeared drums and watery artifacts, and the same restraint applies to a notch filter eating into the fundamental it shares with real musical content.

The demo below shows that trade on a clean chord with a 50 Hz hum and three harmonics: change how the hum is removed and read the HUM score for each choice.

A/B PROCESS · DEHUMIDLE
Dehum repair — mains hum before and after

A 50 Hz hum with three harmonics is added to a clean chord, and the spectrum is zoomed to 0 to 1 kHz so the four lines are visible. Compare the damaged and repaired signals, then change how the hum is removed. HUM scores the mean level between 40 and 250 Hz: cancelling or notching all four components lowers it by about 11 dB, while cancelling only the fundamental lowers it by about 2 dB and leaves the 100, 150 and 200 Hz harmonics. No loudness matching is applied.

Compare
Removal

Step 7 — The single-purpose escape hatch: declip ​

repair's own declip stage is not independently tunable beyond --params. When it is too conservative or too aggressive for a specific take, declip is the standalone command with its own explicit controls:

bash
sonare declip take-raw.wav -o take-declip-only.wav --clip-threshold 0.95 --json
json
{"sample_rate": 48000, "samples": 364800, "clip_threshold": 0.95, "lpc_order": 36, "iterations": 2, "lpc_blend": 0.65, "output": "take-declip-only.wav"}

--clip-threshold is how close to full scale a sample must sit before it counts as clipped (the default is 0.98; this example uses 0.95, which also treats unclipped samples between 0.95 and 1.0 as clipped, so more of the waveform is rebuilt. To touch only the pinned samples, pick a threshold just under the clip_flat_level that --detect reports, 0.99997 here); --lpc-order (how many past samples the linear-predictive model, which extrapolates the waveform from the samples before the gap, looks at), --iterations, and --lpc-blend control how aggressively the reconstruction rebuilds the missing waveform versus how much of the raw (clipped) signal it blends back in. Reach for this only after --detect shows declip is the one stage worth hand-tuning — it repairs nothing else.

Step 8 — Stereo takes ​

Everything above ran on a mono take. repair --detect (and the repair chain itself) downmixes a stereo file to mono and says so on stderr:

text
warning: 2-channel input is downmixed to mono by this CLI command; use the stereo library API for channel-preserving processing

A stereo damage report describes the mono fold

It describes the mono fold only. A defect visible in that fold is useful evidence, but a clean fold does not establish clean individual channels: asymmetric clipping, opposite-polarity hum or clicks, and channel-only defects can hide. Inspect and repair the channels with the stereo entry points in the Python API rather than this CLI path, which only ever writes a mono result.

Running it in a script ​

Every command on this page takes --json, and every failure exits with a documented code: 3 for an invalid combination of flags (--detect with --explain, a missing -o) or an unrecognized parameter key, 4 for a file that does not exist. set -euo pipefail at the top of the batch script is therefore enough to stop the run on the take that actually broke, rather than silently skipping it and reporting a clean batch.

Where to go next ​