WaveLens Docs
User Guide

wavelens

A command-line Audio QA tool for objective audio testing that compares a reference WAV file against a device-under-test (DUT) copy — after a codec, a transcode, a broadcast chain, or any other processing step — and reports exactly how (and whether) they differ. Point it at two files; it hands back a self-contained HTML report with a pass/fail verdict and the specifics behind it.

wavelens v0.7.0-beta · single self-contained executable · no external runtime dependencies

Overview

wavelens answers one question, precisely: does this DUT file sound the same as this reference file, and if not, how? It runs a battery of independent checks — an exact sample comparison, a level-matching check, a distortion/noise scan, a spectral (frequency-response) comparison, a loudness comparison, and a few optional deeper checks — and folds the results into one HTML report you can open in any browser.

Every report is self-contained — a single HTML file with everything embedded (charts, data, styling). Nothing to install to view it, nothing else to send along with it.

i

wavelens is part of a small toolset. wavgen generates synthetic reference signals (tones, sweeps, noise) if you need test material; wavchan splits/packs/joins channels; wavscan answers a point-in-time frequency/level question about a single file; wavsummary rolls up a whole directory of wavelens reports into one pass/fail summary. This guide covers wavelens itself.

Input requirements

  • File format: 16-bit or 24-bit PCM WAV only. Anything else (32-bit float, 8-bit, compressed, non-PCM) is rejected at load time with a clear error — never silently converted.
  • Sample rate: 22,050 Hz to 96,000 Hz inclusive. Covers every standard rate in common use (22.05k, 24k, 32k, 44.1k, 48k, 88.2k, 96k).
  • Channels: mono or stereo. Reference and DUT must have the same channel count.
  • A valid WaveLens license — see License setup below.
i

Have 5.1/7.1/other multichannel content? Split it into stereo/mono pairs first (the companion wavchan split command does this), then run wavelens once per pair. wavelens itself only ever compares one mono or stereo pair per invocation.

License setup

wavelens requires an active license file to run. It looks for one in this order — the first mechanism that resolves wins:

  1. The --license flag — an explicit path, always wins if given.
  2. The WAVELENS_LICENSE_FILE environment variable, if set.
  3. A defaults file's general.license_path setting — see The defaults file.
  4. Zero configuration: a file named exactly wavelens.lic sitting in the same folder as the wavelens executable.
# explicit path
wavelens analyze --license /path/to/wavelens.lic --ref ref.wav --dut dut.wav --out report.html

# or, once per shell session
export WAVELENS_LICENSE_FILE=/path/to/wavelens.lic
wavelens analyze --ref ref.wav --dut dut.wav --out report.html

# or simplest: drop wavelens.lic next to the wavelens executable and pass nothing

Not sure which license file wavelens would actually pick up, or whether it's still valid? Run wavelens license info — it tells you exactly which of the four mechanisms resolved, and the license's status.

i

Don't have a license file, or need a renewal? Contact your WaveLens distributor or administrator.

Quick start

Compare a reference file against a DUT and write an HTML report:

wavelens analyze --ref reference.wav --dut device_under_test.wav --out report.html
# output:
wrote report.html (bit_exact: FAIL, level_fidelity: PASS)

Open report.html in any browser. The one-line stdout summary is just a heads-up — the report itself has the full picture, including which checks actually determine the overall pass/fail (a byte-exact mismatch alone doesn't fail a lossy-codec comparison by default — see The checks it runs).

Commands

wavelens analyzecompare a reference and a DUT file, produce a full report

The main command. Aligns the two files (so it doesn't matter if the DUT has extra silence at the start, for example), then runs every check and writes one HTML report — always. Add --json if you also want the raw numeric results as JSON, for feeding into your own tooling.

FlagRequiredDescription
--ref <ref.wav>requiredThe known-good reference file.
--dut <dut.wav>requiredThe file being checked against the reference.
--out <path>requiredWhere to write the report — see below.
--jsonoptionalAlso write the raw results as a .json file alongside the HTML.
--config <file.json>optionalOverride individual checks' tolerances for this one run — see Per-run overrides.

--out has two modes: if it names an existing directory (or ends in /), the report is written there, named after your DUT file (report_dir/ + device.wavreport_dir/device.html) — handy when scripting a batch of comparisons. Otherwise, --out is used as the exact file path you want.

By default, three short status lines print to stderr while a comparison runsAligning..., Analyzing..., Writing report... — so a long file doesn't leave you staring at a blank terminal wondering if it's still working. They're stderr-only, so they never mix into anything reading --json's stdout output or the final report path. Pass --quiet to suppress them, e.g. for a batch script where the extra lines would just clutter a log.

# exact output path
wavelens analyze --ref ref.wav --dut dut.wav --out results/my_report.html

# directory mode -- named automatically from the DUT's filename
wavelens analyze --ref ref.wav --dut dut.wav --out results/

# also write the raw numbers as JSON
wavelens analyze --ref ref.wav --dut dut.wav --out report.html --json
wavelens trimextract clean content from a sync-tone-wrapped recording

If your file was generated with a leading/trailing sync-tone marker (the companion wavgen tool can add one with its --synctone flag), trim finds both markers and writes out just the clean content between them, with a small silence guard on each side. Fails loudly — with no output file — if it can't confidently locate both markers, rather than guessing.

FlagRequiredDescription
--input <in.wav>requiredThe sync-tone-wrapped recording.
--out <out.wav>requiredWhere to write the trimmed content.
--report <report.json>optionalWrite detection diagnostics (offsets, detection confidence) as JSON instead of printing them.
wavelens trim --input raw_capture.wav --out clean.wav

# capture the detection diagnostics (offsets, PSR) as JSON instead of stdout
wavelens trim --input raw_capture.wav --out clean.wav --report offsets.json
wavelens plotvisualize a single file — no reference, no comparison

When you just want to look at one signal rather than compare it against anything — its spectrum and its full waveform over time — plot renders a self-contained, zoomable HTML page for that one file. No verdicts, since there's nothing being compared. For stereo input, every section (Spectrum, Frequency (Linear), Signal Overview, Time Energy) renders a full Left Channel view followed by a full Right Channel view — not one channel standing in for both.

FlagRequiredDescription
--input <in.wav>requiredThe file to visualize.
--out <path>requiredSame directory-vs-exact-path behavior as analyze's --out.
--jsonoptionalAlso write the file's own metadata as JSON.
wavelens plot --input mystery_file.wav --out plot.html

# also write the file's own metadata as JSON
wavelens plot --input mystery_file.wav --out plot.html --json
wavelens license infocheck what license you're actually running under

Prints the license file wavelens would use for a real run, which of the four lookup mechanisms found it, and its status. Unlike every other command, this one still runs and tells you what's wrong even when the license is invalid or expired — diagnosing that is its whole job.

wavelens license info
# example output
License File:    /opt/wavelens/wavelens.lic (next-to-executable default)
License ID:      b7b64b7e-904c-47d2-9c7c-47e00d463949
Customer:        you@example.com
Name:            Jane Doe
Product:         wavelens
License Type:    PERSONAL
Expiry:          2027-01-01
Signature:       VALID
License Status:  ACTIVE

Takes the same --license flag as every other command, if you want to check a specific file rather than whatever would resolve automatically.

Reading reports

Every report opens with an Overall Verdict — PASS or FAIL — at the top, followed by a row of navigation tabs, one per check that ran. A tab is marked when its own check failed, so you can jump straight to the section that needs attention instead of scrolling through everything.

Below that, each check gets its own section: what it measured, the numbers, and (for a failure) exactly where in the file the worst discrepancy was found. Charts are interactive — zoom and pan on the spectral and waveform-overview charts to inspect a specific region closely.

i

The full raw numeric results are embedded in every HTML report, whether or not you asked for a separate .json file with --json — the JSON option is purely a convenience for scripting against, not the only place the data lives.

The checks it runs

Five checks run on every comparison by default; two more are available if you ask for them with a flag.

CheckWhat it answersDefaultTurn off with
Bit-ExactnessAre the two files byte-for-byte identical, sample by sample?on--no-bitexact
Level FidelityDoes the DUT's level (loudness) track the reference's, sample by sample, within tolerance?onalways runs
Artifact DetectionAre there localized glitches — dropouts, clicks, spikes — that don't belong?onalways runs
Spectral FidelityDoes the DUT's frequency content (its spectrum) match the reference's?onalways runs
Distortion (THD+N)How much unwanted extra energy did the DUT add relative to the reference?on--no-distortion
LoudnessDo the two files' overall perceived loudness, loudness range, and peak level match (ITU-R BS.1770, the broadcast-standard loudness measurement)?on--no-loudness
Time DomainA deeper, whole-file per-sample percentage-difference scan.offturn on with --deepscan
Impulse TemplateA deeper, transient-focused per-sample comparison.offturn on with --impulse
i

By default, a Bit-Exactness failure alone does not fail the overall verdict. Four of the always-on checks are tolerance-based and can genuinely pass a well-functioning lossy pipeline (a codec round-trip, for example) even though the files aren't byte-identical — Bit-Exactness structurally can't pass that same case. If your workflow specifically needs lossless verification, add --bitexact to require it for a PASS.

Loudness works the same way, for the opposite reason. It still runs and its reading is always shown — badged Reference Only instead of Pass/Fail — but doesn't fail the overall verdict by default. A test signal built at or near 0dBFS genuinely trips the True Peak ceiling (below) on inter-sample overshoot alone, independent of whether the DUT matches the reference, so treating it as gating by default would fail comparisons that have nothing wrong with them. Add --loudness to make a real Loudness failure count toward the overall verdict.

Level Fidelity's p99 check has a blind spot for long files, closed by a second check. P99 is a percentile — a rare, isolated glitch (a single mutated sample, say) shouldn't be able to fail an otherwise-clean multi-hour file, and by design it doesn't. But that same design means a genuine, sustained defect can also fall below what a percentile can detect once it's a small enough fraction of a long enough file, even if it lasts a full second and is clearly audible. Level Fidelity now also tracks the longest continuous run of over-tolerance samples and fails if that run lasts at least min_sustained_run_ms (default 20ms) — independent of what percentage of the file it represents. You won't normally need to touch this; it's mentioned here so a report showing a "Sustained Run" figure alongside P99 makes sense.

Level Fidelity's own tolerance defaults to 1.02dB (level_fidelity.p99_tolerance_db, raised from an earlier 0.9dB) — comfortably above a "1dB is fine" expectation, with a little headroom left over so a genuinely flat ~1dB level difference doesn't fail on measurement noise alone. Loosen or tighten it per run with --config; see Per-run overrides.

Distortion doesn't fit every kind of content

Distortion (THD+N) is a single-tone measurement — it needs one dominant frequency to measure against. If your reference is multitone, broadband noise, or heavily clipped content with no single dominant tone, this check isn't meaningful and you should skip it with --no-distortion.

Understanding a failure

Each failed check's section shows the specific numbers that tripped its tolerance, and — where it makes sense for that check — exactly where in the file (which sample, which frame, which frequency) the worst point was found. Loudness is the one exception: it's a whole-file measurement, so there's no single "worst sample" to point to.

A quick way to triage a large batch of reports: check the nav tabs first. A report with only Bit-Exactness marked (and nothing else) is very different from one with Spectral Fidelity and Distortion both marked — the first might be an expected lossy-pipeline result, the second is a real, audible discrepancy worth investigating.

For stereo files, Signal Overview (the always-present full-waveform card at the top of the report) shows a full Left Channel envelope followed by a full Right Channel envelope — not just one channel's waveform standing in for both. Failure-position markers only ever apply to a specific channel's own data, so they only ever appear on that channel's chart, not overlaid on the other one.

A reading that's exactly at (or a hair from) a tolerance line always builds in a tiny margin for measurement noise. No real DSP pipeline (dB conversion, FFT, bit-depth quantization) produces a bit-exact number for something like a synthesized "flat 1dB shift" — the true measurement lands a few millionths of a dB to one side or the other, depending on the specific frequency and level involved, not on anything wrong with your content. Level Fidelity, Spectral Fidelity, and Loudness all absorb that noise with a small built-in margin (about one-hundred-thousandth of the configured tolerance) before deciding pass/fail, so a signal that's genuinely indistinguishable from your tolerance no longer flips PASS/FAIL depending on which exact frequency happened to be tested. This doesn't loosen what you configure — a real difference worth catching is always many orders of magnitude larger than the margin, so nothing that should fail starts passing.

The defaults file

Rather than pass the same flags on every run, drop a wavelens-defaults.cfg file next to the wavelens executable. It's picked up automatically — no flag needed — and its presence or absence never changes what a run would otherwise do if you didn't create one; it only lets you set your own defaults instead of the built-in ones.

{
  // wavelens-defaults.cfg -- // comments are supported
  "general": {
    "license_path": "/opt/wavelens/wavelens.lic"
  },
  "analysis": {
    "level_fidelity": {
      "p99_tolerance_db": 1.2
    },
    "loudness": {
      "integrated_tolerance_lu": 0.75
    }
  }
}

Want to use a defaults file from a non-default location, or check the file itself explicitly? Pass --defaults-cfg <path> on any command — available everywhere --license is.

wavelens analyze --defaults-cfg /opt/wavelens/wavelens-defaults.cfg --ref ref.wav --dut dut.wav --out report.html
!

If you point --defaults-cfg at a file that doesn't exist or won't parse, that's treated as a real error — it doesn't silently fall back to built-in defaults. That's specific to an explicit request; the automatic next-to-executable lookup, by contrast, is silently skipped if the file just isn't there.

Tester identity in the footer

Every report's footer shows the tool version, license, and timestamp. If you also want it to show who ran the test — useful for a report handed to a client, or archived for your own records — set any of test_engineer, test_product, test_company_name in your defaults file's general section, and flip tester_info_enabled to true. All four fields are off by default — a defaults file with the identity values set but tester_info_enabled left false (or omitted) doesn't show them; both have to be true for anything to appear.

{
  "general": {
    "test_engineer": "J. Rivera",
    "test_product": "SuperBass",
    "test_company_name": "Acme Audio Labs",
    "tester_info_enabled": true
  }
}

All three identity fields are optional and independent — set only the ones that apply. The footer combines them as Tested by ENGINEER · COMPANY · product PRODUCT, each part present only if you set it; an empty string ("") is treated the same as leaving the field out entirely.

test_company_name is for when the organization running the test isn't the one that built test_product — a lab testing a client's implementation, for example. It's descriptive text for the report, not tied to your license in any way — it doesn't need to match your license's own organization field.

Alignment tuning

Two fields live under a "alignment" section, for the rare case where a file's own timing characteristics trip up the default detection:

{
  "alignment": {
    "fine_align_agreement_tolerance_samples": 2,
    "sync_search_window_sec": 10.0
  }
}

fine_align_agreement_tolerance_samples (default 2) — for stereo input, the left and right channels are aligned independently, and if their computed offsets disagree by more than this many samples, analyze stops with an alignment error rather than risk comparing mis-synced audio. If you know your DUT introduces a genuine, small per-channel delay difference (some multiband processors do), raise this rather than treat the error as a bug.

sync_search_window_sec (default 10.0) — how many seconds trim/analyze scans looking for the leading/trailing sync-tone burst. Widen it if your file's own guard silence around the sync tone is longer than the built-in assumption.

A few more advanced alignment fields exist beyond these two, for harder cases — contact support if the above don't resolve an alignment issue you're hitting.

Per-run overrides

For a one-off comparison that needs different tolerances than your usual defaults, pass --config <file.json> to analyze. Anything the file doesn't mention falls back to your defaults file (or the built-in default, if you don't have one) — you only need to specify what you're actually changing.

// loose_tolerance.json -- just this one run's Level Fidelity tolerance
{
  "level_fidelity": {
    "p99_tolerance_db": 2.0
  }
}
wavelens analyze --ref ref.wav --dut dut.wav --out report.html --config loose_tolerance.json

Precedence, highest first: --config for this run > your wavelens-defaults.cfg > the built-in default. A convenience flag like --bitexact (below) always wins outright over either config file.

Turning checks on and off

FlagEffect
--deepscanTurns on the Time Domain check (off by default).
--impulseTurns on the Impulse Template check (off by default).
--no-distortionSkips Distortion entirely — no scan, no fields, no report card. Use for content THD+N can't meaningfully measure (see above).
--no-loudnessSkips Loudness entirely.
--loudnessMakes a Loudness failure fail the overall verdict (it doesn't, by default — the card is badged Reference Only instead).
--bitexactMakes a Bit-Exactness failure fail the overall verdict (it doesn't, by default).
--no-bitexactSkips Bit-Exactness entirely — for a batch where you already know every DUT won't be byte-identical and the badge is just noise.
--bitaccuracyAdds a second, tolerant Bit-Exactness reading alongside the strict one — useful when "close enough" bit-level accuracy is what you actually care about, not perfect equality.
--ampsweep / --freqsweepFor amplitude-sweep or frequency-sweep reference content specifically — tells Spectral Fidelity what kind of signal it's looking at. Mutually exclusive with each other.
--snapshots NTake N independent spectral snapshots across the file instead of one, for content whose spectrum is expected to change over time.
i

--bitexact and --no-bitexact can't both be set — they're opposites. --bitaccuracy is independent of both and can be combined with either. Same rule for --loudness and --no-loudness — one asks for strict gating, the other asks to skip the check entirely, so they can't both be set either.

Full flag reference

FlagCommandsDescription
--license <file.lic>allExplicit license file path.
--defaults-cfg <file.cfg>allExplicit defaults-file path.
--ref <ref.wav>analyzeReference file.
--dut <dut.wav>analyzeDevice-under-test file.
--input <in.wav>trim, plotThe single file to process.
--out <path>analyze, trim, plotOutput path (directory-or-exact-file, see analyze).
--jsonanalyze, plotAlso write raw results as JSON.
--report <path>trimWrite detection diagnostics as JSON.
--config <file.json>analyzePer-run tolerance overrides.
--deepscananalyzeTurn on Time Domain.
--impulseanalyzeTurn on Impulse Template.
--ampsweepanalyzeSpectral Fidelity: amplitude-sweep profile.
--freqsweepanalyzeSpectral Fidelity: frequency-sweep profile.
--snapshots NanalyzeN independent spectral snapshots.
--bitaccuracyanalyzeAdd a tolerant Bit-Exactness reading.
--bitexactanalyzeBit-Exactness failure fails the overall verdict.
--no-bitexactanalyzeSkip Bit-Exactness entirely.
--no-distortionanalyzeSkip Distortion entirely.
--no-loudnessanalyzeSkip Loudness entirely.
--loudnessanalyzeLoudness failure fails the overall verdict (reference-only by default).
--quietanalyzeSuppress the Aligning.../Analyzing.../Writing report... status lines (printed to stderr by default).

Examples

Standard codec/pipeline verification

Byte-for-byte equality isn't expected (it's a lossy pipeline), so leave Bit-Exactness non-gating — the default:

wavelens analyze --ref original.wav --dut after_codec.wav --out report.html

Strict lossless verification

Confirming a "lossless" claim actually holds — require byte-exactness to pass:

wavelens analyze --ref original.wav --dut should_be_identical.wav --out report.html --bitexact

Broadcast-safety verification

Loudness runs by default but doesn't gate the overall verdict — its reading is shown as Reference Only, since a test signal built at or near 0dBFS genuinely trips the True Peak ceiling on inter-sample overshoot alone (see above). If loudness/delivery compliance is actually what you're validating, require it to pass:

wavelens analyze --ref original.wav --dut delivery_master.wav --out report.html --loudness

Multitone or noise-based content

Skip Distortion (THD+N doesn't apply — see above):

wavelens analyze --ref multitone_ref.wav --dut multitone_dut.wav --out report.html --no-distortion

Frequency-sweep content with a tighter loudness tolerance

wavelens analyze --ref sweep_ref.wav --dut sweep_dut.wav --out report.html \
    --freqsweep --config tight_loudness.json

Amplitude-sweep content

wavelens analyze --ref ampsweep_ref.wav --dut ampsweep_dut.wav --out report.html --ampsweep

Deeper checks for content you're specifically suspicious of

Time Domain and Impulse Template are off by default because they're slower, whole-file scans — worth turning on when you already suspect a problem and want more evidence:

wavelens analyze --ref original.wav --dut suspect.wav --out report.html --deepscan --impulse

A tolerant bit-level reading alongside the strict one

Keep Bit-Exactness non-gating but also see how close the DUT gets, in dB, when it isn't byte-identical:

wavelens analyze --ref original.wav --dut dut.wav --out report.html --bitaccuracy

Content whose spectrum changes over time

Take multiple spectral snapshots across the file instead of one fixed midpoint:

wavelens analyze --ref long_program.wav --dut long_program_dut.wav --out report.html --snapshots 5

Skipping a check that's expected to fail across a whole batch

Rather than a permanent FAIL badge on every report, drop the check entirely — no scan, no field, no card. Works the same way for any always-on check:

# e.g. every DUT in this batch is a lossy re-encode, so Bit-Exactness never applies
wavelens analyze --ref ref.wav --dut dut.wav --out report.html --no-bitexact

# e.g. content with no meaningful loudness target (short effects, clicks)
wavelens analyze --ref ref.wav --dut dut.wav --out report.html --no-loudness

Errors & troubleshooting

MessageMeaning & fix
error: license INVALID -- no license found at '...'Run wavelens license info to see exactly which lookup mechanism was expected to resolve and why it didn't.
error: license EXPIRED -- license expired on ...Contact your administrator for a renewal.
error: license INVALID -- license has been revokedThis specific license has been invalidated. Contact your administrator.
error: failed to read --ref/--dut '...' (not a supported 16/24-bit PCM WAV file)Check the file is really 16/24-bit PCM — see Input requirements.
error: ... sample rate ... is outside the supported range [22050, 96000] HzThe file's sample rate is out of range — see Input requirements.
error: ... has N channel(s) -- wavelens only accepts mono or stereo inputSplit multichannel content first with wavchan split.
error: --bitexact and --no-bitexact cannot both be setPick one — they're opposites.
error: --loudness and --no-loudness cannot both be setPick one — they're opposites.
error: unrecognized argument '...'Check the flag name and that it's valid for the command you're running.

FAQ

Why did my report show Overall Verdict: PASS even though Bit-Exactness failed?

That's expected by default — see the callout in The checks it runs. Byte-exactness isn't required for an overall pass unless you pass --bitexact. The report itself always calls this out explicitly so it doesn't read as "everything succeeded" when one check structurally couldn't.

Why does my Loudness card say "Reference Only" instead of Pass or Fail?

Same idea as Bit-Exactness above, for the opposite reason: Loudness runs and its reading is always shown, but doesn't gate the overall verdict unless you pass --loudness. Its True Peak check is an absolute ceiling on the DUT alone, so a test signal built at or near 0dBFS can genuinely trip it on inter-sample overshoot, independent of whether the DUT matches the reference — see the callout in The checks it runs.

Why does True Peak fail even on a bit-exact self-comparison?

True Peak isn't a ref-vs-DUT comparison at all — it's a single absolute ceiling checked against the DUT alone (dut.true_peak_dbtp <= true_peak_ceiling_dbtp, default −1.0dBTP, the EBU R128 delivery-spec convention). It can fail even when ref and DUT are byte-identical.

The condition that trips it: content whose sample peak sits at or near 0dBFS. True Peak estimates the reconstructed analog waveform between samples (oversampled, matching what a real DAC does), not just the samples on the grid — and that reconstruction can ring slightly above the highest sample value, a well-known effect called inter-sample overshoot. A 0dBFS sine tone, a 0dBFS sweep, a limiter riding the ceiling — anything mastered right at full scale — will typically read a fraction of a dB over 0dBTP on reconstruction, which is already past a −1.0dBTP ceiling regardless of how well (or perfectly) the DUT matches the reference.

This is expected, not a comparison bug, and it's why Loudness doesn't gate the overall verdict by default (see the question above) — a test asset built at 0dBFS specifically to stress other checks would otherwise fail every run on True Peak alone. If your content is intentionally mastered at full scale, either leave --loudness off (the reading still shows, just doesn't gate) or loosen loudness.true_peak_ceiling_dbtp via --config to a ceiling your content is actually expected to clear.

Can I compare more than one DUT to a reference in one command?

Not in a single invocation — wavelens analyze always compares exactly one reference to exactly one DUT. Loop over your DUT files from a script instead; see Examples.

My files are 5.1 surround. Can wavelens compare them directly?

No — wavelens only ever takes mono or stereo input. Split wider layouts into stereo/mono pairs first with the companion wavchan split command, then run wavelens analyze once per pair.

Do I need --json to get the raw numbers?

No — every HTML report already embeds the full raw results. --json is only for when you want that same data as a separate file, e.g. for your own scripts to parse.

What's the difference between analyze and plot?

analyze compares two files and produces a verdict. plot just visualizes one file — no comparison, no pass/fail, useful when you want to look at a signal's spectrum and waveform without a reference to check it against.