wavscan
A command-line tool that answers one question: at this exact moment in this WAV file, what frequencies are present, and how loud is each one? Point it at a timestamp or a sample index and it reports the dominant frequency plus the next loudest distinct peaks nearby — a measurement, not a pass/fail verdict.
Overview
wavscan windows a signal around one instant, computes its magnitude spectrum, and reports what's actually there — closer in spirit to ffprobe than to wavelens or wavsummary. There's no reference file, no tolerance, no --config, and no exit-code-as-pass/fail semantics: a query either succeeds (prints what it found, exit 0) or fails on a genuine usage error (bad file, out-of-range query point, exit 1). Finding zero peaks above the noise floor is a valid, non-error result.
It's the tool to reach for when you already know something is wrong at a specific point in a file (from a wavelens analyze report's worst-sample or worst-bin fields, or just from listening) and want to see the actual frequency content there, or when you're spot-checking a generated fixture's content without running a full comparison.
Requirements
- A valid, active WaveLens license file (see License below)
- A 16-bit or 24-bit PCM WAV file — mono, stereo, or multichannel
License
wavscan requires an active WaveLens license to run, checked before the file is read. It ships as part of the WaveLens toolset, not as a separately licensed product — the same license file that activates wavelens activates wavscan. There are three ways to point it at your license file, checked in this order — the first one that's set wins.
1. Explicit path with --license
# explicit --license overrides both the env var and the next-to-executable default wavscan --license /path/to/wavelens.lic --input take.wav --at 1.5
2. WAVELENS_LICENSE_FILE environment variable
# bash / zsh export WAVELENS_LICENSE_FILE=/path/to/wavelens.lic wavscan --input take.wav --at 1.5
3. Zero configuration — no flag, no env var
Place a file named exactly wavelens.lic in the same folder as the wavscan executable.
Don't have a license file, or need a renewal? Contact your WaveLens distributor or administrator.
Quick start
Query a WAV file at a specific timestamp:
wavscan --input take.wav --at 2.5
# output:
query time 2.500s (sample 120,000 @ 48000Hz)
window 4096 samples (85.3ms, resolution 11.7Hz/bin)
channel 0:
dominant peak 1002.0 Hz @ -6.6 dB
top 5 peaks:
1. 1002.0 Hz -6.6 dB
2. 2004.0 Hz -24.1 dB
3. 3006.0 Hz -31.8 dB
Every channel in the file is analyzed and reported by default. Add --channel N to restrict the query to one channel, or --json/--csv for machine-readable output — see Output formats.
Command syntax
wavscan --input <in.wav> (--at <SECONDS> | --at-sample <N>) [--license L] [--window-samples 1024|2048|4096|8192|16384|32768] [--top N] [--min-separation-hz HZ] [--noise-floor-db DB] [--channel N] [--json] [--csv] [--html FILE]
| Option | Default | Description |
|---|---|---|
| --input FILE | required | WAV file to query (16/24-bit PCM only). |
| --at SECONDS | one of | Query point as a timestamp. Mutually exclusive with --at-sample — exactly one of the two is required. |
| --at-sample N | one of | Query point as an exact sample index. |
| --window-samples N | 4096 | Must be one of 1024, 2048, 4096, 8192, 16384, 32768. |
| --top N | 5 | May return fewer if fewer peaks survive the separation/noise-floor gates. |
| --min-separation-hz HZ | 4 × bin width | Bin width is sample_rate / window_samples, so the default is resolved per query. |
| --noise-floor-db DB | -80.0 | Candidates below this level are never picked. |
| --channel N | all channels | Restrict to one channel, 0-indexed. Omitted means every channel in the file is analyzed and reported. |
| --json | off | Print JSON instead of text. Mutually exclusive with --csv. |
| --csv | off | Print flat CSV instead of text. Mutually exclusive with --json. |
| --html FILE | off | Also write a self-contained HTML report — see --html report. Independent of --json/--csv. |
Query point: --at vs --at-sample
--at SECONDS is a timestamp, rounded to the nearest sample against the file's own sample rate. --at-sample N is an exact sample index, useful when you already have one from another tool's output — e.g. a wavelens analyze report's worst_sample or worst_event_start_frame field. Exactly one of the two is required; passing both, or neither, is a usage error.
The query point must land strictly inside the file — at or past the file's own duration is rejected, not clamped, since a silently-clamped query would report content from a different point than the one asked for.
How peaks are picked
A window of --window-samples samples is centered on the query point (zero-padded at either edge if the query sits near the start or end of the file), Hann-windowed, and converted to a magnitude-dB spectrum — the same window function and dB scaling wavelens's own Spectral Fidelity primitive uses, so numbers from the two tools agree on the same content.
From that spectrum, up to --top peaks are kept, subject to two independent gates:
- Noise floor — a candidate below
--noise-floor-dbis never picked, regardless of rank. - Minimum separation — two candidates closer together than
--min-separation-hzare treated as one peak (the louder one wins), so a single tone's spectral leakage across adjacent bins doesn't get reported as several distinct peaks.
Peaks are local maxima only, ranked by level, most prominent first. A query may legitimately return fewer than --top peaks — a quiet or narrowband signal simply doesn't have that many distinct peaks above the floor. Returning 0 peaks is a valid result, not an error.
Output formats
Exactly one of text (default), --json, or --csv is printed to stdout — never mixed together, so a script piping the output doesn't have to filter out anything extra.
Text (default)
query time 0.500s (sample 24,000 @ 48000Hz)
window 4096 samples (85.3ms, resolution 11.7Hz/bin)
channel 0:
dominant peak 1002.0 Hz @ -6.6 dB
top 1 peak:
1. 1002.0 Hz -6.6 dB
--json
{
"input_file": "take.wav",
"sample_rate": 48000,
"channels": 2,
"query_time_s": 0.500000,
"query_sample": 24000,
"window_samples": 4096,
"window_duration_ms": 85.333333,
"resolution_hz": 11.718750,
"min_separation_hz": 46.875000,
"noise_floor_db": -80.000000,
"results": [
{
"channel": 0,
"dominant_peak": { "frequency_hz": 1001.9531, "level_db": -6.6288 },
"top_peaks": [
{ "rank": 1, "frequency_hz": 1001.9531, "level_db": -6.6288 }
]
}
]
}
The window/resolution fields (window_samples through noise_floor_db) are top-level, once per query — they depend only on the query parameters, not on any one channel's content. dominant_peak is null, and top_peaks an empty array, when nothing survived the gates for that channel.
--csv
channel,rank,frequency_hz,level_db 0,1,1001.9531,-6.6288 1,1,1001.9531,-6.6288
One row per peak, flat across all channels — no header block, no window/resolution metadata. Use --json instead when that metadata matters.
--html report
--html FILE writes a self-contained HTML page alongside whatever stdout format was chosen — one spectral chart per channel, with the picked peaks marked and labeled directly on the curve. It's independent of --json/--csv; combine --html report.html --json to get both a page to look at and data to script against from the same query.
wavscan --input take.wav --at 2.5 --html scan.html --json > scan.json
The confirmation line (wrote scan.html) is printed to stderr, not stdout — so a pipeline reading stdout for --json/--csv never has to filter out a trailing non-data line.
Exit codes
| Code | Meaning |
|---|---|
| 0 | The query ran successfully — including a query that found 0 peaks above the noise floor, which is a valid result, not a failure. |
| 1 | A usage error — bad arguments, a missing/unreadable input file, an out-of-range query point or channel, or an invalid license. |
wavscan has no pass/fail concept, so its exit code is purely "did the query run," never "did the content look right." That question is wavelens analyze's job.
Common recipes
Inspect the exact point a wavelens report flagged
# worst_sample from a level_fidelity_channel_0_worst_sample field, say wavscan --input dut.wav --at-sample 96044
Check one channel only, with a tighter noise floor
wavscan --input take.wav --at 3.2 --channel 0 --noise-floor-db -100
Higher-resolution query on a low-frequency signal
# bigger window = finer resolution_hz, at the cost of time precision wavscan --input take.wav --at 1.0 --window-samples 16384
Script against the numbers directly
wavscan --input take.wav --at 2.5 --json \ | python3 -c "import json,sys; d = json.load(sys.stdin); print(d['results'][0]['dominant_peak'])"
Errors & troubleshooting
| Message | Meaning & fix |
|---|---|
| error: license INVALID -- no license found at '...' | Check whichever of the three lookup mechanisms actually resolved — see License. |
| error: license EXPIRED -- license expired on ... | Contact your administrator for a renewal. |
| error: --input is required | --input is mandatory on every invocation. |
| error: exactly one of --at or --at-sample is required | Pass one, not both, not neither. |
| error: failed to read '...' | The file doesn't exist, isn't valid WAV, or isn't 16/24-bit PCM. |
| error: --channel N is out of range ('...' has M channels) | Check the file's actual channel count — N is 0-indexed. |
| error: query point (sample N) is at or past '...'s own duration | The timestamp/sample index falls at or beyond the end of the file. Not clamped — pick a point strictly inside it. |
| error: --window-samples must be one of 1024, 2048, 4096, 8192, 16384, 32768 | Only those six discrete sizes are accepted. |
| error: --json and --csv are mutually exclusive | Pick one. |
| error: unrecognized argument '...' | Check the flag name against Command syntax. |
FAQ
Does wavscan compare two files, like wavelens does?
No. wavscan takes exactly one input file and reports what's in it at one instant — there's no ref/DUT concept, no tolerance, and no pass/fail. Use wavelens analyze for comparisons.
Why did my query return fewer peaks than --top asked for?
Not an error — a window simply doesn't always have that many distinct peaks above --noise-floor-db once the minimum-separation gate merges nearby candidates. See How peaks are picked.
Do the frequency numbers match what wavelens analyze reports for the same content?
Yes — wavscan reimplements the identical Hann-window magnitude-dB spectrum wavelens's Spectral Fidelity primitive uses, so a peak found here at a given point should agree with what a wavelens analyze report shows for that same window.
Can I query multiple points in one invocation?
No — one query point per run. Script a loop over --at/--at-sample values if you need several, ideally with --json so each result is easy to collect.