<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://wavelens.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://wavelens.dev/" rel="alternate" type="text/html" /><updated>2026-08-29T23:29:42+00:00</updated><id>https://wavelens.dev/feed.xml</id><title type="html">WaveLens</title><subtitle>WaveLens is an objective audio testing and audio QA tool -- compare a reference file against a processed copy and see exactly how, and whether, they still sound the same.</subtitle><author><name>WaveLens</name></author><entry><title type="html">Reading a WaveLens report: what each check actually tells you</title><link href="https://wavelens.dev/blog/2026/08/18/reading-a-wavelens-report/" rel="alternate" type="text/html" title="Reading a WaveLens report: what each check actually tells you" /><published>2026-08-18T00:00:00+00:00</published><updated>2026-08-18T00:00:00+00:00</updated><id>https://wavelens.dev/blog/2026/08/18/reading-a-wavelens-report</id><content type="html" xml:base="https://wavelens.dev/blog/2026/08/18/reading-a-wavelens-report/"><![CDATA[<p class="lead">A WaveLens report never just says “pass” or “fail.” Underneath the overall verdict are six independent checks, each looking at a different way two audio files can stop being the same file. Here’s what each one is actually measuring, and what it means when one of them turns red.</p>

<p>Every run starts the same way: <code class="language-plaintext highlighter-rouge">wavelens analyze</code> takes a reference file and a device-under-test (DUT) copy, and runs both through the same six-check battery.</p>

<div class="code-block">
  <div class="bar">
    <div class="dots"><span></span><span></span><span></span></div>
    <button type="button" class="copy-btn">
      <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke-width="1.6"><rect x="8" y="8" width="11" height="11" rx="1.5" /><path d="M5.5 14H5A1.5 1.5 0 0 1 3.5 12.5v-8A1.5 1.5 0 0 1 5 3h8a1.5 1.5 0 0 1 1.5 1.5V5" /></svg>
      <span class="copy-label">Copy</span>
    </button>
  </div>
  <pre><span class="c">$</span> wavelens <span class="flag">analyze</span> --ref <span class="str">reference.wav</span> --dut <span class="str">device_under_test.wav</span> --out <span class="str">report.html</span>

<span class="out">wrote report.html (bit_exact: FAIL, level_fidelity: PASS, loudness: PASS)</span></pre>
</div>

<p>The one-line stdout summary is a preview. The full report breaks the verdict down check by check, and that’s where the useful information actually lives.</p>

<h2 id="signal-alignment">Signal Alignment</h2>

<p>Before any comparison can mean anything, both files need to be looking at the same moment in time. Signal Alignment finds the time offset between reference and DUT — a few samples of latency from a codec, a resampler, or a processing chain — and corrects for it before every other check runs. If this check fails, every other result on the report should be treated with suspicion: the tool may be comparing the wrong samples to each other.</p>

<h2 id="spectral-fidelity">Spectral Fidelity</h2>

<p>This is the frequency-domain comparison — does the DUT have the same frequency response as the reference, or has something rolled off the highs, boosted the low end, or introduced energy that wasn’t there before? A Spectral Fidelity failure usually points at filtering, EQ, or a lossy codec’s psychoacoustic model doing something audible.</p>

<h2 id="bit-exactness">Bit-Exactness</h2>

<p>The strictest check on the report. Bit-Exactness asks a literal question: is every sample in the DUT identical to the corresponding sample in the reference? Not “close,” not “inaudibly different” — identical. A single flipped bit anywhere in the file fails this check.</p>

<div class="check-box">
  <span class="badge fail">FAIL</span>
  <p>Bit-Exactness &middot; sample 48,204 &mdash; the report names the exact sample where the files first diverge, so you're not hunting for it by ear.</p>
</div>

<p>This is also the check most likely to fail for reasons that don’t matter. Dither, a different but valid resampling implementation, or an intentional processing step will all break bit-exactness while leaving the audio perceptually identical. That’s exactly why it’s one check among six, not the whole verdict — see <a href="/blog/2026/08/12/bit-exact-vs-close-enough/">Bit-Exact vs. “close enough”</a> for when this one actually matters.</p>

<h2 id="level-fidelity">Level Fidelity</h2>

<p>Checks that overall amplitude/gain wasn’t quietly changed somewhere in the chain — a mismatched gain stage, an unintended normalize, a broadcast chain applying makeup gain. Level Fidelity catches shifts that a spectral or loudness check alone might not flag clearly.</p>

<h2 id="distortion">Distortion</h2>

<p>Compares the harmonic and noise floor content between files — has the DUT introduced clipping, quantization noise, or harmonic artifacts that weren’t present in the reference? This is where a bad gain-staging decision or an overloaded encoder tends to show up first.</p>

<h2 id="loudness-bs1770">Loudness (BS.1770)</h2>

<p>Added in v0.7.0. Measures integrated loudness per the BS.1770 standard broadcasters actually use for delivery specs. A file can pass every other check and still fail loudness if a normalization step or a broadcast chain pushed it out of spec — see <a href="/blog/2026/07/29/bs1770-loudness-explained/">BS.1770 loudness, explained</a> for what this measurement does and doesn’t catch.</p>

<h2 id="reading-the-overall-verdict">Reading the overall verdict</h2>

<p>The Overall Verdict is a rollup of all six — if any check fails, the run fails. That’s deliberately strict by default, and deliberately not the end of the story. A report showing <strong>Bit-Exactness: FAIL</strong> next to five passes is telling you something very different from one showing <strong>Spectral Fidelity: FAIL</strong> and <strong>Loudness: FAIL</strong> together. The point of six checks instead of one PASS/FAIL light is that you get to make that call yourself, with the actual evidence in front of you.</p>]]></content><author><name>WaveLens</name></author><category term="guide" /><summary type="html"><![CDATA[Six checks, one verdict — a walkthrough of what Signal Alignment, Spectral Fidelity, Bit-Exactness, Level Fidelity, Distortion, and Loudness are actually measuring, and what a FAIL on each one really means for your file.]]></summary></entry><entry><title type="html">Bit-Exact vs. “close enough”: what audio QA actually means</title><link href="https://wavelens.dev/blog/2026/08/12/bit-exact-vs-close-enough/" rel="alternate" type="text/html" title="Bit-Exact vs. “close enough”: what audio QA actually means" /><published>2026-08-12T00:00:00+00:00</published><updated>2026-08-12T00:00:00+00:00</updated><id>https://wavelens.dev/blog/2026/08/12/bit-exact-vs-close-enough</id><content type="html" xml:base="https://wavelens.dev/blog/2026/08/12/bit-exact-vs-close-enough/"><![CDATA[<p class="lead">“Sounds the same” and “is the same” are different claims, and a lot of audio QA confusion comes from treating them as one. A listening test — even a careful, trained one — is checking the first. Bit-Exactness, one of the six checks on a WaveLens report, is checking the second, literally: is every sample identical, byte for byte.</p>

<h2 id="a-report-that-looks-contradictory-until-it-isnt">A report that looks contradictory until it isn’t</h2>

<p>Here’s a report from a real codec round-trip — encode, decode, done:</p>

<div class="code-block">
  <div class="bar">
    <div class="dots"><span></span><span></span><span></span></div>
  </div>
  <pre>Signal Alignment&nbsp;&nbsp;&nbsp;&nbsp;<span class="out">PASS</span>
Spectral Fidelity&nbsp;&nbsp;&nbsp;&nbsp;<span class="out">PASS</span>
Bit-Exactness&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="fail-line">FAIL &middot; sample 12</span>
Level Fidelity&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="out">PASS</span>
Distortion&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="out">PASS</span>
Loudness&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="out">PASS</span>

Overall Verdict&nbsp;&nbsp;&nbsp;<span class="out">PASS</span></pre>
</div>

<p>Five checks pass, Bit-Exactness fails at sample 12 — the very start of the file, which is the tell: that’s almost always a dither pattern applied during the encode, not a real defect. The Overall Verdict is still PASS, because Bit-Exactness is non-gating by default. That’s not the tool shrugging off a failure; it’s the tool correctly recognizing that “not byte-identical” and “not the same audio” aren’t the same claim.</p>

<h2 id="where-the-strict-claim-actually-matters">Where the strict claim actually matters</h2>

<p>The cases where you genuinely need bit-exact equality are narrower than they first seem:</p>

<ul>
  <li><strong>Confirming a “lossless” archival copy really is lossless.</strong> If a vendor or a pipeline step claims a transfer is lossless, Bit-Exactness is the only check that can actually verify that claim — any difference at all is the finding.</li>
  <li><strong>Verifying a file survived storage or transfer untouched.</strong> A backup, a CDN cache, a database blob column — anywhere a file is supposed to come back exactly as it went in.</li>
  <li><strong>Checking a build pipeline’s “no-op” path.</strong> If a step in your pipeline is supposed to pass a file through unchanged under certain conditions, Bit-Exactness confirms it actually did.</li>
</ul>

<p>For any of these, add <code class="language-plaintext highlighter-rouge">--bitexact</code> to make a Bit-Exactness failure fail the overall verdict — otherwise it’s informational only, exactly as in the report above.</p>

<h2 id="where-it-doesnt">Where it doesn’t</h2>

<p>Everything else — did processing change the <em>sound</em>, not just the bytes — is what Spectral Fidelity, Level Fidelity, Distortion, and Loudness are built to answer, with real tolerances instead of a strict yes/no. A codec round-trip, a valid resampling implementation, or intentional dithering will all break Bit-Exactness while leaving those four checks clean, which is exactly what the report above shows. See <a href="/blog/2026/07/01/bit-exactness-vs-bit-accuracy/">Bit-Exactness vs. Bit Accuracy</a> for the tolerant middle ground when you want a <em>measured</em> answer instead of a strict pass/fail, and <a href="/blog/2026/08/18/reading-a-wavelens-report/">Reading a WaveLens report</a> for how all six checks combine into one verdict.</p>]]></content><author><name>WaveLens</name></author><category term="eng" /><summary type="html"><![CDATA[Two files can sound identical and still not be the same file. Why bit-exactness is a stricter, more literal claim than "passes a listening test" — and when you actually need it.]]></summary></entry><entry><title type="html">Catching a codec regression before it ships</title><link href="https://wavelens.dev/blog/2026/08/05/catching-a-codec-regression/" rel="alternate" type="text/html" title="Catching a codec regression before it ships" /><published>2026-08-05T00:00:00+00:00</published><updated>2026-08-05T00:00:00+00:00</updated><id>https://wavelens.dev/blog/2026/08/05/catching-a-codec-regression</id><content type="html" xml:base="https://wavelens.dev/blog/2026/08/05/catching-a-codec-regression/"><![CDATA[<p class="lead">A codec library upgrade landed as a routine dependency bump — the kind of change that usually gets a quick listen and a shrug. It shouldn’t have been notable at all.</p>

<h2 id="the-change-that-looked-routine">The change that looked routine</h2>

<p>The upgrade was three minor versions ahead of what the pipeline had been running, pulled in as part of a broader dependency refresh. Nothing in the changelog mentioned audio behavior. The listening pass — a quick A/B on a handful of representative files — came back clean; nothing sounded obviously wrong. It would have shipped on that basis alone if the nightly comparison hadn’t run.</p>

<h2 id="what-the-report-showed">What the report showed</h2>

<p>The nightly <code class="language-plaintext highlighter-rouge">wavelens</code> comparison against the previous build’s reference output came back FAIL, with exactly one check marked:</p>

<div class="check-box">
  <span class="badge fail">FAIL</span>
  <p>Level Fidelity &middot; consistent +1.4 dB offset across the full file duration &mdash; Spectral Fidelity, Distortion, and Loudness all reported PASS.</p>
</div>

<p>That pattern narrowed the search immediately. Not a quality regression — Spectral Fidelity and Distortion were both clean, so nothing had introduced new artifacts or reshaped the frequency response. Just amplitude, and consistently across the whole file rather than in one section, which pointed at something structural in the encode path rather than a one-off glitch.</p>

<h2 id="finding-the-cause">Finding the cause</h2>

<p>A few minutes into the codec library’s changelog turned up the actual cause: a default had changed for an internal gain-normalization option, three versions back, buried in a “behavior changes” section rather than called out as a breaking change. The previous codec version hadn’t applied it; the new one did, silently, unless explicitly disabled.</p>

<p>+1.4 dB applied consistently across an entire file is genuinely hard to catch by ear in a quick listening pass — especially without an A/B reference playing at the exact same moment, which a quick spot-check pass usually doesn’t set up. It’s exactly the class of change a level-fidelity check is built to catch and a human review pass is built to miss.</p>

<h2 id="what-changed-after">What changed after</h2>

<p>The fix was a one-line configuration change to pin the old gain-normalization behavior. The more lasting change was to the release checklist: the nightly <code class="language-plaintext highlighter-rouge">wavelens analyze</code> comparison against the previous release’s output, which had been an informational step someone occasionally glanced at, became a required gate with <code class="language-plaintext highlighter-rouge">wavsummary</code> blocking the build on any failure.</p>

<div class="code-block">
  <div class="bar">
    <div class="dots"><span></span><span></span><span></span></div>
  </div>
  <pre><span class="c">$</span> wavelens <span class="flag">analyze</span> --ref <span class="str">previous_release.wav</span> --dut <span class="str">candidate_build.wav</span> --out <span class="str">reports/nightly.html</span> --json
<span class="c">$</span> wavsummary ./reports --json || exit 1</pre>
</div>

<p>Not a replacement for listening — a catch for the class of regression a listening pass reliably won’t flag on its own.</p>]]></content><author><name>WaveLens</name></author><category term="case" /><summary type="html"><![CDATA[A nightly build comparison caught a level-fidelity drift that a listening pass missed entirely. What the report looked like, and how it changed the release checklist.]]></summary></entry><entry><title type="html">BS.1770 loudness, explained — and why it fails silently</title><link href="https://wavelens.dev/blog/2026/07/29/bs1770-loudness-explained/" rel="alternate" type="text/html" title="BS.1770 loudness, explained — and why it fails silently" /><published>2026-07-29T00:00:00+00:00</published><updated>2026-07-29T00:00:00+00:00</updated><id>https://wavelens.dev/blog/2026/07/29/bs1770-loudness-explained</id><content type="html" xml:base="https://wavelens.dev/blog/2026/07/29/bs1770-loudness-explained/"><![CDATA[<p class="lead">Loudness is the check most likely to surprise you, because it’s the only one on a WaveLens report where “passes every other check” and “sounds right” can genuinely disagree.</p>

<h2 id="what-bs1770-actually-measures">What BS.1770 actually measures</h2>

<p>BS.1770 isn’t a peak meter and it isn’t RMS — it’s the ITU-R standard broadcasters actually use for delivery specs, and it works by applying a K-weighting filter (which de-emphasizes very low frequencies, roughly matching how the ear perceives loudness) before integrating the result over time. The output is a single number in LUFS (loudness units relative to full scale) that’s meant to correlate with <em>perceived</em> loudness far better than a raw amplitude measurement does.</p>

<p><code class="language-plaintext highlighter-rouge">wavelens</code> runs this on both the reference and the DUT and compares the two integrated values against a tolerance.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wavelens analyze --ref reference.wav --dut device_under_test.wav --out report.html

wrote report.html (loudness: FAIL — integrated -14.2 LUFS vs -18.6 LUFS reference)
</code></pre></div></div>

<h2 id="why-a-normalization-step-is-the-usual-suspect">Why a normalization step is the usual suspect</h2>

<p>The most common cause of a Loudness failure isn’t distortion or a bad encode — it’s a delivery-chain step that adjusted overall level on purpose. A loudness normalization pass, a broadcast chain applying makeup gain, or a platform-specific loudness target (streaming services each have their own) can all push a file’s integrated loudness outside your reference’s tolerance while leaving every other check clean.</p>

<p>That’s not necessarily a bug in your pipeline — sometimes it’s the pipeline doing exactly what it was told to do. The report just tells you it happened; deciding whether it <em>should</em> have happened is still a human call.</p>

<h2 id="where-the-check-has-blind-spots">Where the check has blind spots</h2>

<p>Loudness is a whole-file, integrated measurement — there’s no single “worst sample” the way there is for Bit-Exactness or Distortion, because the whole point is that it’s averaged over time. That has two consequences worth knowing:</p>

<ul>
  <li><strong>A file can pass Loudness and still have a problem in one section.</strong> A three-minute program with one badly clipped ten-second passage can still land close enough to the reference’s <em>integrated</em> loudness for the check to pass — the clipping would show up in Distortion instead, not here.</li>
  <li><strong>A file can fail Loudness while sounding fine.</strong> If your reference and DUT differ by a deliberate, known gain offset — say, a broadcast delivery spec that intentionally targets a different LUFS than your internal reference — that’s a real, correct difference, and the fix is adjusting your comparison, not chasing a phantom bug.</li>
</ul>

<h2 id="reading-it-alongside-level-fidelity">Reading it alongside Level Fidelity</h2>

<p>Loudness and Level Fidelity look related but answer different questions. Level Fidelity tracks amplitude/gain sample-by-sample against tolerance — it’s built to catch a mismatched gain stage or an unintended normalize as a <em>local</em> event. Loudness reports the integrated, perceptual result over the whole file. A file can fail one without the other: a brief gain glitch might trip Level Fidelity without moving the integrated LUFS enough to fail Loudness, and a deliberate whole-file loudness target change can fail Loudness while Level Fidelity — tracking relative shifts, not absolute targets — still passes.</p>

<p>If you’re triaging a report and only Loudness is marked, start by asking whether something in the chain was <em>supposed</em> to change overall level before you assume it’s a regression.</p>]]></content><author><name>WaveLens</name></author><category term="eng" /><summary type="html"><![CDATA[Loudness can be technically "in spec" and still sound wrong, or vice versa. What the BS.1770 measurement actually integrates, and where the check has blind spots.]]></summary></entry><entry><title type="html">Wiring wavsummary into a CI pipeline</title><link href="https://wavelens.dev/blog/2026/07/22/wiring-wavsummary-into-ci/" rel="alternate" type="text/html" title="Wiring wavsummary into a CI pipeline" /><published>2026-07-22T00:00:00+00:00</published><updated>2026-07-22T00:00:00+00:00</updated><id>https://wavelens.dev/blog/2026/07/22/wiring-wavsummary-into-ci</id><content type="html" xml:base="https://wavelens.dev/blog/2026/07/22/wiring-wavsummary-into-ci/"><![CDATA[<p class="lead">A single <code class="language-plaintext highlighter-rouge">wavelens analyze</code> run answers “did this one file change?” <code class="language-plaintext highlighter-rouge">wavsummary</code> answers the question a CI pipeline actually needs: “did <em>anything</em> in this batch change?” — and it answers it with an exit code, not a report you have to open by hand.</p>

<h2 id="what-it-actually-does">What it actually does</h2>

<p><code class="language-plaintext highlighter-rouge">wavsummary</code> scans a directory of <code class="language-plaintext highlighter-rouge">wavelens</code> reports (the JSON output, written with <code class="language-plaintext highlighter-rouge">--json</code> alongside each report’s HTML) and rolls every one of them up into a single pass/fail summary. It doesn’t re-run any checks — it’s purely a rollup of results <code class="language-plaintext highlighter-rouge">wavelens</code> already produced.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wavsummary ./reports --json

142 reports · 3 failed
exit 1
</code></pre></div></div>

<p>Exit code 0 means every report in the directory passed. Anything else means at least one didn’t, and the summary output tells you which ones.</p>

<h2 id="the-minimal-ci-step">The minimal CI step</h2>

<p>The whole point is that this fits into a build system as one command with a meaningful exit code:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wavelens analyze --ref reference.wav --dut build/output.wav --out reports/output.html --json
$ wavsummary ./reports --json || exit 1
</code></pre></div></div>

<p>In a real pipeline you’re usually generating many reports first — one per test file, one per platform build, one per content type — and then running <code class="language-plaintext highlighter-rouge">wavsummary</code> once at the end as a single gate:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ for f in build/*.wav; do
    wavelens analyze --ref "refs/$(basename $f)" --dut "$f" --out "reports/$(basename $f .wav).html" --json
  done
$ wavsummary ./reports --json
</code></pre></div></div>

<p>A failed unit fails the build. Nobody has to remember to open a report by hand for it to count.</p>

<h2 id="what-to-gate-on-and-what-not-to">What to gate on, and what not to</h2>

<p>It’s worth deciding upfront which failures should actually block a merge versus which should just be visible. A codec round-trip that reliably fails Bit-Exactness but passes everything else isn’t a regression — it’s expected behavior for a lossy pipeline (see <a href="/blog/2026/08/12/bit-exact-vs-close-enough/">Bit-Exact vs. “close enough”</a>). If that’s your situation, run <code class="language-plaintext highlighter-rouge">wavelens analyze</code> without <code class="language-plaintext highlighter-rouge">--bitexact</code> so Bit-Exactness stays non-gating, rather than filtering <code class="language-plaintext highlighter-rouge">wavsummary</code>’s output after the fact — the exit code stays meaningful, and nobody has to remember why a “failure” is actually fine.</p>

<h2 id="archiving-the-evidence">Archiving the evidence</h2>

<p><code class="language-plaintext highlighter-rouge">wavsummary</code>’s exit code is what gates the build, but the individual HTML reports are what someone actually looks at when a build fails. Most teams archive the <code class="language-plaintext highlighter-rouge">reports/</code> directory as a build artifact alongside the <code class="language-plaintext highlighter-rouge">wavsummary</code> output — that way a red build comes with the exact report that explains why, not just a number.</p>]]></content><author><name>WaveLens</name></author><category term="guide" /><summary type="html"><![CDATA[wavsummary scans a directory of reports and exits non-zero on any failure. Gating a build on it, from a single CI step to a full release checklist.]]></summary></entry><entry><title type="html">5.1 to stereo: what actually happens during a downmix</title><link href="https://wavelens.dev/blog/2026/07/15/51-to-stereo-downmix/" rel="alternate" type="text/html" title="5.1 to stereo: what actually happens during a downmix" /><published>2026-07-15T00:00:00+00:00</published><updated>2026-07-15T00:00:00+00:00</updated><id>https://wavelens.dev/blog/2026/07/15/51-to-stereo-downmix</id><content type="html" xml:base="https://wavelens.dev/blog/2026/07/15/51-to-stereo-downmix/"><![CDATA[<p class="lead"><code class="language-plaintext highlighter-rouge">wavelens</code> only ever compares a mono or stereo pair — so multichannel content has to go through <code class="language-plaintext highlighter-rouge">wavchan</code> first. That step isn’t just a format conversion; it’s a real signal-processing operation with its own tolerances to think about before you compare.</p>

<h2 id="why-you-cant-just-add-the-channels-together">Why you can’t just “add the channels together”</h2>

<p>A naive 5.1-to-stereo downmix — summing left and right surrounds straight into the stereo pair, full level — clips easily and throws away the spatial information a real downmix coefficient set is designed to preserve. <code class="language-plaintext highlighter-rouge">wavchan</code>’s downmix follows the standard ITU coefficients: front left/right pass through at unity, center is split evenly between the stereo pair at a reduced coefficient, and the surrounds are folded in at their own reduced level rather than full strength.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wavchan downmix --input program_5.1.wav --out program_stereo.wav --preset itu
</code></pre></div></div>

<p>That means a correct downmix is <em>not</em> bit-exact with any simple sum you’d compute by hand — the coefficients are doing real attenuation, not just channel routing.</p>

<h2 id="what-to-actually-compare">What to actually compare</h2>

<p>Once you have a stereo file on each side of a pipeline change, the comparison itself is ordinary <code class="language-plaintext highlighter-rouge">wavelens analyze</code> — but the tolerances you’d use for a straight stereo-to-stereo comparison are often too tight for a downmix comparison, because small differences in how two downmix implementations round or dither can add up to real, non-bit-exact differences that aren’t a regression.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wavchan downmix --input program_5.1.wav --out ref_stereo.wav --preset itu
$ wavchan downmix --input program_5.1_reencoded.wav --out dut_stereo.wav --preset itu
$ wavelens analyze --ref ref_stereo.wav --dut dut_stereo.wav --out report.html
</code></pre></div></div>

<p>Bit-Exactness will very often fail here even between two correct downmixes — that’s expected, not a bug, the same way it’s expected after a codec round-trip. Spectral Fidelity and Level Fidelity are the checks that actually tell you whether the downmix changed in a way that matters.</p>

<h2 id="splitting-instead-of-downmixing">Splitting instead of downmixing</h2>

<p>Not every multichannel workflow wants a mixed-down stereo pair — sometimes you want the individual channels split out and compared separately, channel by channel, rather than folded together. <code class="language-plaintext highlighter-rouge">wavchan split</code> does that instead:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wavchan split --input program_5.1.wav --out-dir channels/
</code></pre></div></div>

<p>That gives you one mono file per channel (<code class="language-plaintext highlighter-rouge">channels/FL.wav</code>, <code class="language-plaintext highlighter-rouge">channels/FR.wav</code>, <code class="language-plaintext highlighter-rouge">channels/C.wav</code>, and so on), each of which <code class="language-plaintext highlighter-rouge">wavelens</code> can compare directly against its own reference — useful when you specifically need to know whether, say, the center channel alone changed, rather than whether the downmixed result changed.</p>

<h2 id="packing-back-together">Packing back together</h2>

<p>The inverse operation — <code class="language-plaintext highlighter-rouge">wavchan pack</code> — takes separate mono/stereo files and interleaves them back into one multichannel file, for when a pipeline step needs multichannel input rather than the split-apart pieces.</p>]]></content><author><name>WaveLens</name></author><category term="eng" /><summary type="html"><![CDATA[Folding six channels into two isn't just adding them together. What wavchan does during a downmix, and why comparing before/after needs its own tolerance.]]></summary></entry><entry><title type="html">WaveLens v0.7.0: Loudness (BS.1770) and the Analysis Mode tile</title><link href="https://wavelens.dev/blog/2026/07/08/wavelens-v070-release-notes/" rel="alternate" type="text/html" title="WaveLens v0.7.0: Loudness (BS.1770) and the Analysis Mode tile" /><published>2026-07-08T00:00:00+00:00</published><updated>2026-07-08T00:00:00+00:00</updated><id>https://wavelens.dev/blog/2026/07/08/wavelens-v070-release-notes</id><content type="html" xml:base="https://wavelens.dev/blog/2026/07/08/wavelens-v070-release-notes/"><![CDATA[<h2 id="loudness-bs1770">Loudness (BS.1770)</h2>

<p>The headline addition: a full integrated-loudness check per the ITU-R BS.1770 standard, running as a sixth check alongside Signal Alignment, Spectral Fidelity, Bit-Exactness, Level Fidelity, and Distortion. See <a href="/blog/2026/07/29/bs1770-loudness-explained/">BS.1770 loudness, explained</a> for what it measures and where it has blind spots.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wavelens analyze --ref reference.wav --dut device_under_test.wav --out report.html

wrote report.html (loudness: FAIL — integrated -14.2 LUFS vs -18.6 LUFS reference)
</code></pre></div></div>

<h2 id="analysis-mode-tile">Analysis Mode tile</h2>

<p>Every report now shows an Analysis Mode tile indicating whether that run’s comparison used time-domain or frequency-domain alignment internally — useful when triaging a Signal Alignment result, since the two modes handle certain kinds of latency differently.</p>

<h2 id="everything-else">Everything else</h2>

<ul>
  <li>Minor performance improvements to the Spectral Fidelity check on files longer than 10 minutes.</li>
  <li>Fixed a rare false-positive on Distortion for multitone reference material with closely spaced frequencies.</li>
  <li><code class="language-plaintext highlighter-rouge">wavsummary</code> output now includes a per-check failure breakdown, not just the overall pass/fail count.</li>
</ul>

<p>Upgrade notes: existing <code class="language-plaintext highlighter-rouge">wavelens-defaults.cfg</code> files don’t need any changes — Loudness runs with sensible defaults out of the box, and <code class="language-plaintext highlighter-rouge">--no-loudness</code> is available if you need to skip it for content without a meaningful loudness target.</p>]]></content><author><name>WaveLens</name></author><category term="release" /><summary type="html"><![CDATA[This release adds a full BS.1770 loudness check and a new Analysis Mode tile on every report, showing whether a comparison ran in time-domain or frequency-domain mode.]]></summary></entry><entry><title type="html">Bit-Exactness vs. Bit Accuracy — which do I want?</title><link href="https://wavelens.dev/blog/2026/07/01/bit-exactness-vs-bit-accuracy/" rel="alternate" type="text/html" title="Bit-Exactness vs. Bit Accuracy — which do I want?" /><published>2026-07-01T00:00:00+00:00</published><updated>2026-07-01T00:00:00+00:00</updated><id>https://wavelens.dev/blog/2026/07/01/bit-exactness-vs-bit-accuracy</id><content type="html" xml:base="https://wavelens.dev/blog/2026/07/01/bit-exactness-vs-bit-accuracy/"><![CDATA[<p class="lead">Bit-Exactness and bit accuracy sound like the same idea stated two ways. On a WaveLens report they’re two different measurements, answering two different questions, and mixing them up leads to the wrong conclusion about a failure.</p>

<h2 id="bit-exactness-a-yesno-question">Bit-Exactness: a yes/no question</h2>

<p>Bit-Exactness is binary. Every sample in the DUT either matches the reference exactly, or it doesn’t — there’s no partial credit, no tolerance, no “close.” One flipped bit anywhere in the file is a fail. It’s the strictest check WaveLens runs, and by default it’s non-gating: a Bit-Exactness fail alone doesn’t fail the Overall Verdict, because plenty of correct, expected processing (a codec round-trip, a valid resampling implementation, intentional dither) breaks it without changing anything anyone can hear.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wavelens analyze --ref ref.wav --dut dut.wav --out report.html --bitexact
</code></pre></div></div>

<p>Add <code class="language-plaintext highlighter-rouge">--bitexact</code> when you specifically need to <em>require</em> byte-for-byte equality for a PASS — confirming a “lossless” claim actually holds is the usual reason.</p>

<h2 id="bit-accuracy-a-how-close-question">Bit Accuracy: a “how close” question</h2>

<p><code class="language-plaintext highlighter-rouge">--bitaccuracy</code> adds a second, separate reading alongside Bit-Exactness — a tolerant, measured comparison expressed in dB, rather than a strict pass/fail. Where Bit-Exactness can only tell you “identical” or “not identical,” bit accuracy tells you <em>how far off</em>, which is the more useful number when you already expect some non-zero difference and want to know whether it’s within an acceptable range.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wavelens analyze --ref ref.wav --dut dut.wav --out report.html --bitaccuracy
</code></pre></div></div>

<p>The two flags are independent of each other — <code class="language-plaintext highlighter-rouge">--bitaccuracy</code> doesn’t change whether Bit-Exactness itself gates the verdict, it just adds the additional measurement to the report.</p>

<h2 id="picking-the-right-one-for-the-job">Picking the right one for the job</h2>

<ul>
  <li><strong>Confirming a claimed lossless step actually is lossless?</strong> Use <code class="language-plaintext highlighter-rouge">--bitexact</code>. You want a hard yes/no, and any non-zero difference is a real finding.</li>
  <li><strong>Comparing something you already know won’t be byte-identical, but need to know how close it got?</strong> Use <code class="language-plaintext highlighter-rouge">--bitaccuracy</code>. A codec round-trip or a valid alternate resampling implementation will always show <em>some</em> numeric difference — the question is whether it’s small enough to not matter, and Bit-Exactness alone can’t answer that.</li>
  <li><strong>Not sure yet?</strong> Run both. <code class="language-plaintext highlighter-rouge">--bitexact</code> and <code class="language-plaintext highlighter-rouge">--no-bitexact</code> can’t be combined (they’re opposites), but <code class="language-plaintext highlighter-rouge">--bitaccuracy</code> can be added alongside either — you get the strict verdict and the measured distance in the same report.</li>
</ul>

<p>The report itself calls out explicitly when Bit-Exactness didn’t gate the verdict, so a PASS next to a Bit-Exactness FAIL never silently reads as “everything matched.”</p>]]></content><author><name>WaveLens</name></author><category term="eng" /><summary type="html"><![CDATA[The two terms get used interchangeably and shouldn't be. A precise breakdown of what each one checks, and which one your QA process actually needs.]]></summary></entry><entry><title type="html">wavgen: generating repeatable test signals for regression suites</title><link href="https://wavelens.dev/blog/2026/06/24/wavgen-repeatable-test-signals/" rel="alternate" type="text/html" title="wavgen: generating repeatable test signals for regression suites" /><published>2026-06-24T00:00:00+00:00</published><updated>2026-06-24T00:00:00+00:00</updated><id>https://wavelens.dev/blog/2026/06/24/wavgen-repeatable-test-signals</id><content type="html" xml:base="https://wavelens.dev/blog/2026/06/24/wavgen-repeatable-test-signals/"><![CDATA[<p class="lead">A regression suite is only as trustworthy as its test material is stable. If your reference signal is even slightly different every time you generate it, a <code class="language-plaintext highlighter-rouge">wavelens</code> comparison against yesterday’s build and today’s build is comparing against two different things — and any diff you see might just be signal drift, not a real regression.</p>

<h2 id="deterministic-by-default">Deterministic by default</h2>

<p><code class="language-plaintext highlighter-rouge">wavgen</code> seeds every generator deterministically. Given the same parameters, it produces bit-identical output every time — the same tone, the same sweep, the same multitone burst, run to run, machine to machine. That’s what makes it safe to check a generated reference file into a repo (or regenerate it on demand in CI) without worrying that a “fresh” reference will silently differ from the one a previous build was compared against.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wavgen tone --freq 1000 --duration 5 --out tone_1khz.wav
$ wavgen tone --freq 1000 --duration 5 --out tone_1khz_again.wav
$ wavelens analyze --ref tone_1khz.wav --dut tone_1khz_again.wav --out report.html --bitexact

wrote report.html (bit_exact: PASS)
</code></pre></div></div>

<h2 id="what-it-generates">What it generates</h2>

<ul>
  <li><strong>Tones</strong> — single-frequency sine waves at a specified frequency, level, and duration. The baseline for a distortion/noise-floor check.</li>
  <li><strong>Sweeps</strong> — linear or logarithmic frequency sweeps, useful for exercising a full frequency range in one file rather than testing discrete tones one at a time.</li>
  <li><strong>Multitone</strong> — several simultaneous frequencies in one signal, closer to how a Spectral Fidelity check would want to exercise a full frequency response at once.</li>
</ul>

<h2 id="wrapping-output-for-later-trimming">Wrapping output for later trimming</h2>

<p>Real pipelines often need to capture audio through a system rather than compare a file directly — recording off a physical output, for instance — and a raw capture usually has extra silence or noise at the start and end. <code class="language-plaintext highlighter-rouge">wavgen</code> can wrap its output with a leading and trailing sync-tone marker for exactly this case:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wavgen sweep --start 20 --end 20000 --duration 10 --synctone --out sweep_wrapped.wav
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">wavelens trim</code> (part of the core <code class="language-plaintext highlighter-rouge">wavelens</code> tool) finds both markers in a captured recording and extracts just the clean content between them, with a small silence guard on each side — and it fails loudly, with no output file, if it can’t confidently locate both markers, rather than guessing at where the real content starts.</p>

<h2 id="building-a-regression-suite-around-it">Building a regression suite around it</h2>

<p>The pattern most teams land on: generate a fixed library of <code class="language-plaintext highlighter-rouge">wavgen</code> reference material once (tones at standard frequencies, a representative sweep, a multitone burst), commit it or cache it in CI, and run every pipeline change against that same fixed set with <code class="language-plaintext highlighter-rouge">wavelens analyze</code>. Because the generation is deterministic, the reference library itself never needs regenerating unless you deliberately change it — which means every comparison in the suite is actually testing your pipeline, not testing whether your test material stayed the same.</p>]]></content><author><name>WaveLens</name></author><category term="eng" /><summary type="html"><![CDATA[Tones, sweeps, and multitone material need to be exactly reproducible run to run. How wavgen seeds its generators, and why that matters for a regression suite.]]></summary></entry><entry><title type="html">Peak vs RMS: what wavscan actually measures</title><link href="https://wavelens.dev/blog/2026/06/17/peak-vs-rms-wavscan/" rel="alternate" type="text/html" title="Peak vs RMS: what wavscan actually measures" /><published>2026-06-17T00:00:00+00:00</published><updated>2026-06-17T00:00:00+00:00</updated><id>https://wavelens.dev/blog/2026/06/17/peak-vs-rms-wavscan</id><content type="html" xml:base="https://wavelens.dev/blog/2026/06/17/peak-vs-rms-wavscan/"><![CDATA[<p class="lead"><code class="language-plaintext highlighter-rouge">wavscan</code> doesn’t compare two files the way <code class="language-plaintext highlighter-rouge">wavelens</code> does — it inspects one file on its own, no reference needed. That makes it the right tool for a quick sanity check before committing to a full comparison, but only if you’re reading the right number for what you’re actually trying to catch.</p>

<h2 id="running-it">Running it</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ wavscan --input mystery_file.wav

Peak:        -0.8 dBFS
RMS:         -18.3 dBFS
Crest factor: 17.5 dB
Sample rate:  48000 Hz
Channels:     2
</code></pre></div></div>

<p>No <code class="language-plaintext highlighter-rouge">--ref</code>, no verdict, no pass/fail — just a level and format snapshot of the file as it is.</p>

<h2 id="peak-the-instantaneous-ceiling">Peak: the instantaneous ceiling</h2>

<p>Peak level is the loudest single sample in the file, full stop. It’s the number that matters for headroom and clipping — if peak is sitting at or above 0 dBFS, you’re clipping (or right at the edge of it), regardless of how loud the file sounds on average. Peak tells you nothing about perceived loudness; a file can have a high peak from one short transient while sounding quiet overall.</p>

<h2 id="rms-the-average-level">RMS: the average level</h2>

<p>RMS (root mean square) is a windowed average of the signal’s energy — closer to “how loud does this sound,” though still not a perceptual loudness measurement the way BS.1770 is (see <a href="/blog/2026/07/29/bs1770-loudness-explained/">BS.1770 loudness, explained</a> for that). RMS is useful for a quick relative comparison — “is this file noticeably quieter than that one” — without needing a full <code class="language-plaintext highlighter-rouge">wavelens</code> loudness check.</p>

<h2 id="crest-factor-the-gap-between-them">Crest factor: the gap between them</h2>

<p>The difference between peak and RMS — crest factor — tells you something neither number alone does: how much of the file’s dynamic range is being used. A low crest factor (peak and RMS close together) usually means heavy compression or limiting; a high crest factor means the file has real dynamic range, with occasional loud moments well above the average level.</p>

<h2 id="when-wavscan-is-enough-and-when-it-isnt">When wavscan is enough, and when it isn’t</h2>

<p><code class="language-plaintext highlighter-rouge">wavscan</code> is the right call when you just need to eyeball one file — checking that a freshly generated <code class="language-plaintext highlighter-rouge">wavgen</code> reference isn’t clipping, confirming a captured file actually has signal in it before running a full comparison, or spot-checking level on a batch of files without a reference for any of them. It’s not a substitute for <code class="language-plaintext highlighter-rouge">wavelens analyze</code> once you actually have a reference to compare against — peak and RMS on a single file can’t tell you whether <em>this</em> file still matches <em>that</em> one, which is the question a real comparison answers.</p>]]></content><author><name>WaveLens</name></author><category term="guide" /><summary type="html"><![CDATA[A quick peak/level scan can mean two different things depending on which number you're reading. What wavscan reports, and when each measurement is the right one.]]></summary></entry></feed>