mirror of
https://codeberg.org/armin/tcd.git
synced 2026-09-01 05:50:46 +02:00
enhance explanations in README, make tcd output more clear
This commit is contained in:
parent
9b205be6a0
commit
2ec7d5ec1a
2 changed files with 256 additions and 203 deletions
475
README.md
475
README.md
|
|
@ -19,274 +19,322 @@ Careful! Dragons ahead!
|
||||||
`tcd` *can* and absolutely *will* delete your data. Don't blindly use -a, and
|
`tcd` *can* and absolutely *will* delete your data. Don't blindly use -a, and
|
||||||
please read at least the --help information and *understand* what -a does.
|
please read at least the --help information and *understand* what -a does.
|
||||||
|
|
||||||
How it works
|
---
|
||||||
------------
|
|
||||||
|
|
||||||
### 1. Signal acquisition
|
How it works (the short version)
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
The program opens the file with libavformat, selects the first audio stream,
|
tcd decodes your audio file, converts it to the frequency domain (like a
|
||||||
decodes up to `--duration` (default 60) seconds of audio (or the entire file
|
graph showing how much energy exists at each frequency), then measures several
|
||||||
when `--full` is used), and converts every sample to 32-bit float PCM.
|
properties of that frequency graph. Each property is a clue about whether the
|
||||||
|
audio was produced by a lossy encoder. Combined, these clues give a verdict.
|
||||||
### 2. Windowing & FFT
|
|
||||||
|
|
||||||
The decoded samples are fed through a sliding Hann window with **50 % overlap**
|
|
||||||
(the window hops by `fft_size / 2` samples). Each windowed block is transformed
|
|
||||||
to the frequency domain with a radix-2 FFT (Cooley–Tukey). Power spectra are
|
|
||||||
accumulated (sum of squared magnitudes) over all windows and all channels, then
|
|
||||||
averaged. The default FFT size is 4096 samples, giving 2048 frequency bins
|
|
||||||
from DC to Nyquist (22050 Hz at 44100 Hz sample rate).
|
|
||||||
|
|
||||||
### 3. Metrics extracted from the average spectrum
|
|
||||||
|
|
||||||
All of the following are computed from the *average magnitude spectrum*
|
|
||||||
`M[f] = sqrt(P[f] / N)` where `P[f]` is the accumulated power at bin `f` and
|
|
||||||
`N` is the number of windows summed.
|
|
||||||
|
|
||||||
#### Cutoff frequency
|
|
||||||
|
|
||||||
Searched from Nyquist downward. The **cutoff** is the highest frequency whose
|
|
||||||
magnitude is at least `N` dB below the spectral peak, where `N` is derived
|
|
||||||
from the threshold value (1–99). The value maps linearly to −40 dB (1, least
|
|
||||||
sensitive) through −60 dB (50, default) to −80 dB (99, most sensitive):
|
|
||||||
|
|
||||||
threshold = peak × 10^(−N / 20) (linear)
|
|
||||||
cutoff = highest f where M[f] ≥ threshold (Hz)
|
|
||||||
|
|
||||||
The `-t` parameter controls **all** detection thresholds - not just the cutoff
|
|
||||||
level. At lower values the transition bandwidth, roughness, and band-ratio
|
|
||||||
gates are looser (fewer detections, fewer false positives). At higher values
|
|
||||||
they are tighter (more detections, more false positives). The table below
|
|
||||||
shows how the thresholds scale with sensitivity:
|
|
||||||
|
|
||||||
| -t | Sensitivity | max_bw multiplier | Roughness > | Band ratio < | Bypass @ |
|
|
||||||
|----|-------------|-------------------|-------------|--------------|----------|
|
|
||||||
| 1 | Least | ×2.0 | 0.70 / 0.53 / 0.35 | 0.85 / 0.80 | ≥1.00 |
|
|
||||||
| 50 | Default | ×1.0 | 0.40 / 0.30 / 0.20 | 0.90 / 0.85 | ≥0.99 |
|
|
||||||
| 99 | Most | ×0.25 | 0.10 / 0.08 / 0.05 | 0.95 / 0.90 | ≥0.98 |
|
|
||||||
|
|
||||||
Lossy encoders place their lowpass cutoff somewhere below Nyquist. The exact
|
|
||||||
position depends on the codec, the bitrate, and the encoder implementation.
|
|
||||||
|
|
||||||
#### Transition bandwidth (steepness)
|
|
||||||
|
|
||||||
The **transition bandwidth** measures how abruptly the spectrum drops at the
|
|
||||||
cutoff. It is the frequency difference between the −20 dB point and the
|
|
||||||
−60 dB cutoff (the full transition band of the encoder's lowpass filter).
|
|
||||||
|
|
||||||
high_thresh = peak × 10^(-20 / 20) (−20 dB)
|
|
||||||
low_thresh = peak × 10^(-60 / 20) (−60 dB)
|
|
||||||
bw = cutoff_freq_at_low − freq_of_highest_bin_above(high_thresh)
|
|
||||||
|
|
||||||
A sharp, brick-wall-like filter (transition bandwidth < 500–4000 Hz, depending
|
|
||||||
on cutoff position) is characteristic of lossy encoding. Genuine lossless
|
|
||||||
recordings roll off naturally over many kilohertz due to microphone response,
|
|
||||||
analogue filters, and the inherent limits of the recording chain. Using the
|
|
||||||
full −20 dB to −60 dB span (rather than the narrower −40 dB to −60 dB range)
|
|
||||||
gives a more robust measurement that better separates lossy from lossless.
|
|
||||||
|
|
||||||
#### Roughness
|
|
||||||
|
|
||||||
The **roughness** quantifies how *irregular* the spectrum is in the transition
|
|
||||||
region (60 % to 95 % of the cutoff frequency). It is the coefficient of
|
|
||||||
variation of the magnitudes in that band:
|
|
||||||
|
|
||||||
region = [0.60 × cutoff, 0.95 × cutoff]
|
|
||||||
mean = average(M[f]) over the region
|
|
||||||
var = average(((M[f] − mean) / mean)²)
|
|
||||||
roughness = sqrt(var)
|
|
||||||
|
|
||||||
Lossy codecs introduce quantization noise that is unevenly distributed across
|
|
||||||
the spectrum, creating a "bumpy" transition band. Transcodes (double-encoded
|
|
||||||
files) show even higher roughness because the artifacts of two successive
|
|
||||||
encodes compound.
|
|
||||||
|
|
||||||
#### Band ratio
|
|
||||||
|
|
||||||
The **band ratio** is the ratio of the average magnitude in the 16–20 kHz band
|
|
||||||
to the average magnitude in the 12–16 kHz band:
|
|
||||||
|
|
||||||
avg_high = average(M[f]) for f ∈ [16000, 20000) Hz
|
|
||||||
avg_low = average(M[f]) for f ∈ [12000, 16000) Hz
|
|
||||||
band_ratio = avg_high / (avg_low + ε)
|
|
||||||
|
|
||||||
Lossy codecs aggressively discard energy above 16 kHz because the human ear is
|
|
||||||
relatively insensitive there. A low band ratio (< 0.85–0.90) is a strong
|
|
||||||
marker of lossy origins.
|
|
||||||
|
|
||||||
#### Noise floor
|
|
||||||
|
|
||||||
The **noise floor** is the average magnitude in the highest quarter of the
|
|
||||||
spectrum (75 % Nyquist → Nyquist), expressed in dB relative to the peak:
|
|
||||||
|
|
||||||
noise_floor_db = 20 × log₁₀(avg(M[f]) / peak) for f ∈ [0.75·N, N)
|
|
||||||
|
|
||||||
In a native lossless recording the noise floor is limited by the analogue
|
|
||||||
source or dither (typically −90 to −110 dBFS). Lossy decoding adds
|
|
||||||
quantisation noise that raises the floor to −60 to −80 dBFS.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Decision logic
|
What tcd displays and what each number means
|
||||||
--------------
|
---------------------------------------------
|
||||||
|
|
||||||
The tool distinguishes two scenarios based on the codec of the input file.
|
Here is the example output you saw:
|
||||||
|
|
||||||
### A. Input is a lossy codec (mp3, aac, vorbis, opus, wma, ac3, …)
|
```
|
||||||
|
File: ./08 You Got Me.mp3
|
||||||
|
Format: mp3
|
||||||
|
Bitrate: 320 kbps
|
||||||
|
Sample rate: 44100 Hz
|
||||||
|
Channels: 2
|
||||||
|
Windows: 13550
|
||||||
|
Peak: 47.5 dBFS
|
||||||
|
Cutoff: 20790 / 22050 Hz = 94.3%
|
||||||
|
Steepness: 20209 Hz
|
||||||
|
Roughness: 0.323
|
||||||
|
Band ratio: 0.555
|
||||||
|
Noise floor: -56.4 dB
|
||||||
|
Verdict: NATIVE
|
||||||
|
Verdict Info: bandwidth used=94.3% (20790/22050 Hz), expected≥90% for 320 kbps
|
||||||
|
```
|
||||||
|
|
||||||
The cutoff is compared against the expected minimum for the file's *stated*
|
Each metric is explained below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Cutoff (20790 / 22050 Hz = 94.3%)
|
||||||
|
|
||||||
|
**What it is:** The highest frequency where the audio still has measurable
|
||||||
|
energy. Everything above this point is silence or noise.
|
||||||
|
|
||||||
|
**The Nyquist ceiling:** Digital audio is made of snapshots (samples). For CD
|
||||||
|
quality (44100 snapshots per second), there is a hard limit: you cannot store
|
||||||
|
a frequency higher than half the snapshot rate = **22050 Hz**. This is called
|
||||||
|
the *Nyquist frequency*. It is a physical ceiling — higher frequencies simply
|
||||||
|
cannot exist.
|
||||||
|
|
||||||
|
**How lossy encoding changes it:** MP3 and other lossy codecs deliberately cut
|
||||||
|
off high frequencies to save space. The cutoff gets lower as the bitrate drops:
|
||||||
|
|
||||||
|
| Bitrate | Typical cutoff | Audio quality impact |
|
||||||
|
|---------|---------------|----------------------|
|
||||||
|
| 320 kbps | ≥20000 Hz (≥90%) | Keeps almost all audible high end |
|
||||||
|
| 256 kbps | ≥19000 Hz (≥86%) | Still very clean |
|
||||||
|
| 192 kbps | ~17500 Hz (~79%) | Moderate high-end roll-off |
|
||||||
|
| 128 kbps | ~16000 Hz (~73%) | Noticeable treble loss |
|
||||||
|
| 96 kbps | ~13000 Hz (~59%) | Significant high-end missing |
|
||||||
|
| 64 kbps | ~11000 Hz (~50%) | Sounds dull, heavily filtered |
|
||||||
|
|
||||||
|
**What 94.3% means for your file:** 20790 / 22050 = 94.3%. The cutoff is very
|
||||||
|
close to the theoretical maximum. This is what we expect from a 320 kbps
|
||||||
|
encode. If this same file showed 54% (~12000 Hz), it would mean the treble
|
||||||
|
was chopped off by an aggressive low-bitrate encoder, and someone just
|
||||||
|
re-encoded it at 320 kbps — the cutoff is permanent and cannot be restored.
|
||||||
|
That would be an **UPSCALED** file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Steepness
|
||||||
|
|
||||||
|
**What it measures:** How abruptly the sound drops off *at* the cutoff point.
|
||||||
|
tcd measures this as the frequency gap between the −20 dB point (still loud)
|
||||||
|
and the −60 dB cutoff (essentially silent). A narrow gap = a sharp drop.
|
||||||
|
|
||||||
|
**The analogy:** Imagine the frequency graph as a mountain ridge. A lossless
|
||||||
|
recording rolls off like a natural hillside — gradual, smooth, taking
|
||||||
|
thousands of Hz to go from loud to silent. A lossy encoder's lowpass filter
|
||||||
|
creates a cliff — a near-vertical drop from audible signal to nothing.
|
||||||
|
|
||||||
|
**What the number means:** Steepness is the width (in Hz) of that drop zone.
|
||||||
|
The smaller the number, the sharper the cliff:
|
||||||
|
|
||||||
|
| Steepness | What it looks like | Likely origin |
|
||||||
|
|-----------|-------------------|---------------|
|
||||||
|
| <500 Hz | Brick-wall drop | Lossy encoder (MP3, AAC) |
|
||||||
|
| 500–2000 Hz | Fairly sharp | Could be lossy or aggressive production filter |
|
||||||
|
| 2000–5000 Hz | Moderate | Might be natural |
|
||||||
|
| >5000 Hz | Gentle slope | Natural acoustic roll-off (lossless) |
|
||||||
|
|
||||||
|
To understand steepness, imagine a guitar string being plucked. The sound
|
||||||
|
naturally fades across many frequencies — the harmonics near the top end of
|
||||||
|
your hearing get quieter and quieter over a broad range. This is a gentle
|
||||||
|
slope. Now imagine someone put a pair of scissors on the frequency spectrum
|
||||||
|
and cut everything above a certain note. That sharp edge — the difference
|
||||||
|
between "still audible" and "completely gone" in just a few hundred Hz — is
|
||||||
|
what lossy compression does. The steepness number tells you how sharp that
|
||||||
|
scissor cut was.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Roughness (0.323)
|
||||||
|
|
||||||
|
**What it measures:** How "bumpy" or "irregular" the spectrum looks just
|
||||||
|
before the cutoff point.
|
||||||
|
|
||||||
|
**The analogy:** Lossy encoding introduces quantization noise — tiny
|
||||||
|
rounding errors that are unevenly distributed across frequencies. In the
|
||||||
|
frequency graph, this looks like a jagged, bumpy line instead of a smooth
|
||||||
|
one. Think of it like a dirt road vs a paved highway: lossless audio is
|
||||||
|
smooth, lossy audio is bumpy. Double-encoded audio (a transcode) is even
|
||||||
|
bumpier because the errors from two encodings stack on top of each other.
|
||||||
|
|
||||||
|
**What the number means:**
|
||||||
|
|
||||||
|
| Roughness | What it looks like | Likely origin |
|
||||||
|
|-----------|-------------------|---------------|
|
||||||
|
| <0.15 | Very smooth | Natural/lossless |
|
||||||
|
| 0.15–0.30 | Slightly bumpy | Could be lossy single encode |
|
||||||
|
| 0.30–0.50 | Clearly bumpy | Lossy single encode, or borderline transcode |
|
||||||
|
| >0.50 | Very jagged | Almost certainly a transcode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Band ratio (0.555)
|
||||||
|
|
||||||
|
**What it measures:** How much high-frequency energy (16–20 kHz) remains
|
||||||
|
compared to mid-high energy (12–16 kHz).
|
||||||
|
|
||||||
|
**Why it matters:** Human hearing is least sensitive above 16 kHz. Lossy
|
||||||
|
encoders exploit this by spending almost no bits on those frequencies. The
|
||||||
|
result is that the 16–20 kHz region is much quieter than the 12–16 kHz region.
|
||||||
|
In native recordings, this drop is modest; in lossy/transcoded material, it
|
||||||
|
is severe.
|
||||||
|
|
||||||
|
**What the number means:** Band ratio = energy in 16–20 kHz band ÷ energy in
|
||||||
|
12–16 kHz band. A ratio of 1.0 means both bands are equally loud. A ratio of
|
||||||
|
0.5 means the top band is half as loud.
|
||||||
|
|
||||||
|
| Band ratio | What it means |
|
||||||
|
|------------|---------------|
|
||||||
|
| >0.85 | Healthy high end — likely native lossless |
|
||||||
|
| 0.70–0.85 | Mild roll-off — could be lossy or natural |
|
||||||
|
| 0.50–0.70 | Significant high-end loss — likely lossy |
|
||||||
|
| <0.50 | Severe high-end loss — almost certainly lossy or transcoded |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Noise floor (-56.4 dB)
|
||||||
|
|
||||||
|
**What it measures:** The average noise level in the highest quarter of the
|
||||||
|
frequency range (roughly 16500–22050 Hz).
|
||||||
|
|
||||||
|
**The analogy:** Imagine listening in a quiet room — the background hiss is
|
||||||
|
very low. Now imagine that same room with a fan running — the background
|
||||||
|
noise rises. A lossy encoder introduces quantization noise that raises the
|
||||||
|
"background hiss" in the high frequencies.
|
||||||
|
|
||||||
|
**What the number means:** This is measured in decibels (dB). More negative =
|
||||||
|
quieter (better). Less negative = noisier (worse):
|
||||||
|
|
||||||
|
| Noise floor | What it means |
|
||||||
|
|-------------|---------------|
|
||||||
|
| −90 to −110 dB | Very clean — native lossless |
|
||||||
|
| −70 to −90 dB | Moderate — could be lossy or quiet lossless |
|
||||||
|
| −50 to −70 dB | Noisy — likely lossy |
|
||||||
|
| >−50 dB | Very noisy — almost certainly lossy or transcoded |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
How tcd combines these clues into a verdict
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
tcd does not rely on any single metric. It combines them in stages, like a
|
||||||
|
detective building a case.
|
||||||
|
|
||||||
|
### Scenario 1: The input file is lossy (MP3, AAC, etc.)
|
||||||
|
|
||||||
|
The file already claims to be lossy. The question is: was it *originally*
|
||||||
|
encoded at the stated bitrate, or was it re-encoded from a lower bitrate?
|
||||||
|
|
||||||
|
**The check:** tcd compares the cutoff against what is expected for that
|
||||||
bitrate:
|
bitrate:
|
||||||
|
|
||||||
| Stated bitrate | Expected cutoff ratio |
|
| Stated bitrate | Expected cutoff |
|
||||||
|------------------|----------------------|
|
|----------------|-----------------|
|
||||||
| < 192 kbps | ≥ 0.75 of Nyquist |
|
| <192 kbps | ≥75% of Nyquist |
|
||||||
| 192–255 kbps | ≥ 0.85 of Nyquist |
|
| 192–255 kbps | ≥85% of Nyquist |
|
||||||
| ≥ 256 kbps | ≥ 0.90 of Nyquist |
|
| ≥256 kbps | ≥90% of Nyquist |
|
||||||
|
|
||||||
If the measured cutoff is **more than 8 percentage points below** the expected
|
If the actual cutoff is **more than 8 percentage points lower** than expected,
|
||||||
minimum, the file is classified as **UPSCALED** (a lower-bitrate encode that
|
the file is **UPSCALED**. For example, a file claiming 320 kbps (expecting
|
||||||
was decoded and re-encoded at a higher bitrate). Otherwise it is **NATIVE**
|
≥90%) but showing a cutoff of 70% (≈15400 Hz) would be flagged as upscaled
|
||||||
(a single, genuine encode at the stated bitrate).
|
from ~96 kbps.
|
||||||
|
|
||||||
### B. Input is a lossless codec (flac, pcm, alac, wavpack, …)
|
Otherwise it is **NATIVE** — a genuine single encode at this bitrate.
|
||||||
|
|
||||||
The tool applies two layers of criteria.
|
### Scenario 2: The input file is lossless (FLAC, WAV, ALAC, etc.)
|
||||||
|
|
||||||
#### Primary criteria (cutoff + transition bandwidth)
|
The file claims to be lossless. The question is: was it actually created by
|
||||||
|
decoding a lossy file and re-encoding to lossless?
|
||||||
|
|
||||||
The transition bandwidth (from −20 dB to −60 dB) is compared against a
|
tcd uses a **two-layer** check:
|
||||||
cutoff-dependent threshold. A narrower bandwidth than the threshold indicates
|
|
||||||
a lossy encoder's brickwall filter:
|
|
||||||
|
|
||||||
| Cutoff ratio range | Max transition bandwidth | Interpretation |
|
**Layer 1 — Cutoff + Steepness (primary):**
|
||||||
|-------------------|-------------------------|---------------|
|
|
||||||
| < 0.50 | 4000 Hz | Transcode |
|
|
||||||
| < 0.70 | 3000 Hz | Transcode |
|
|
||||||
| < 0.80 | 2000 Hz | Transcode |
|
|
||||||
| < 0.90 | 1200 Hz | Transcode |
|
|
||||||
| ≥ 0.90 | 500 Hz | Transcode |
|
|
||||||
|
|
||||||
This graduated approach avoids the earlier problem of rigid breakpoints that
|
| Cutoff range | Max steepness allowed | If exceeded → |
|
||||||
could miss files with moderate cutoffs but wider-than-expected transition
|
|-------------|----------------------|---------------|
|
||||||
bands, or files with cutoffs just above a hard threshold (e.g. 21 kHz /
|
| <50% of Nyquist | 4000 Hz | TRANSCODE |
|
||||||
44.1 kHz = 0.952, previously missed by a strict `< 0.95` check).
|
| 50–70% | 3000 Hz | TRANSCODE |
|
||||||
|
| 70–80% | 2000 Hz | TRANSCODE |
|
||||||
|
| 80–90% | 1200 Hz | TRANSCODE |
|
||||||
|
| ≥90% | 500 Hz | TRANSCODE |
|
||||||
|
|
||||||
The combination of a low cutoff and a sharp roll-off is the strongest
|
This works because lossy cutoffs are always sharp (low steepness). A lossless
|
||||||
indicator. A cutoff below 50 % of Nyquist (e.g. 11 kHz at 44.1 kHz sampling)
|
recording that happens to have a low cutoff (e.g., a muddy recording with
|
||||||
is *impossible* for a modern lossless recording and always indicates a
|
little treble) would still have a *gradual* roll-off (high steepness) — you
|
||||||
transcode.
|
need both a low cutoff **and** a sharp drop to convict.
|
||||||
|
|
||||||
#### Secondary criteria (roughness + band ratio)
|
**Layer 2 — Roughness + Band ratio (secondary):**
|
||||||
|
|
||||||
If the primary criteria do not match but the cutoff is above 80 % of Nyquist
|
If Layer 1 did not trigger but the cutoff is above 80%, tcd checks roughness
|
||||||
(the region where lossy cutoffs can approach the lossless range), the tool
|
and band ratio. This catches transcodes where the cutoff happens to be high
|
||||||
falls back to roughness and band ratio:
|
enough to pass Layer 1 but the spectrum is still bumpy and depleted in the
|
||||||
|
top band:
|
||||||
|
|
||||||
| Roughness | Band ratio | Interpretation |
|
| Roughness | Band ratio | If matched → |
|
||||||
|-----------|----------------|----------------|
|
|-----------|------------|--------------|
|
||||||
| > 0.40 | any | Transcode |
|
| >0.40 | any | TRANSCODE |
|
||||||
| > 0.30 | < 0.90 | Transcode |
|
| >0.30 | <0.90 | TRANSCODE |
|
||||||
| > 0.20 | < 0.85 | Transcode |
|
| >0.20 | <0.85 | TRANSCODE |
|
||||||
|
|
||||||
If no primary or secondary criterion matches, the file is classified as
|
If neither layer triggers, the file is **GENUINE** (native lossless).
|
||||||
**GENUINE** (native lossless).
|
|
||||||
|
|
||||||
### C. Confidence score
|
---
|
||||||
|
|
||||||
|
### The verdicts at a glance
|
||||||
|
|
||||||
|
| Verdict | Input codec | What it means |
|
||||||
|
|---------|------------|---------------|
|
||||||
|
| **NATIVE** | lossy | Encoded once at the stated bitrate — genuine |
|
||||||
|
| **UPSCALED** | lossy | Originally encoded at a lower bitrate, then re-encoded higher |
|
||||||
|
| **GENUINE** | lossless | Appears to be native lossless — no evidence of lossy origin |
|
||||||
|
| **TRANSCODE** | lossless | Originated from a lossy source, decoded to lossless |
|
||||||
|
| **SILENT** | any | No detectable audio content |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Confidence score
|
||||||
|
----------------
|
||||||
|
|
||||||
A continuous **confidence** (0–100 %) is computed using the same metrics with
|
A continuous **confidence** (0–100 %) is computed using the same metrics with
|
||||||
a sliding scale, providing a graded measure of how certain the tool is about
|
a sliding scale, providing a graded measure of how certain the tool is about
|
||||||
its verdict.
|
its verdict.
|
||||||
|
|
||||||
### D. Auto-remove mode (`-a`)
|
---
|
||||||
|
|
||||||
|
Auto-remove mode (`-a`)
|
||||||
|
------------------------
|
||||||
|
|
||||||
When `-a` is passed, any file that is not classified as NATIVE or GENUINE is
|
When `-a` is passed, any file that is not classified as NATIVE or GENUINE is
|
||||||
automatically deleted after analysis. This is useful for batch cleanup of
|
automatically deleted after analysis. This is useful for batch cleanup of
|
||||||
corrupt or transcoded libraries. Careful - that this will eat data.
|
corrupt or transcoded libraries. Careful — this will eat data.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Why the method is (somewhat!) scientifically reliable
|
Why the method is (somewhat!) scientifically reliable
|
||||||
------------------------------------------
|
------------------------------------------------------
|
||||||
|
|
||||||
### 1. Lossy encoding leaves a permanent spectral fingerprint
|
### 1. Lossy encoding leaves a permanent spectral fingerprint
|
||||||
|
|
||||||
Every lossy audio codec works by discarding information that psychoacoustic
|
Every lossy audio codec discards information. The most obvious form is a
|
||||||
models deem inaudible. The most universal form of this discarding is a
|
**lowpass filter** — once applied, the frequencies above the cutoff are gone
|
||||||
**lowpass filter** applied before encoding. Once the filter has been applied,
|
forever. Decoding back to PCM and re-encoding to lossless cannot restore them.
|
||||||
the information above the cutoff is gone forever. Decoding back to PCM and
|
This means a "lossless" FLAC file made from an MP3 will contain the MP3's
|
||||||
re-encoding to lossless (FLAC, ALAC, WAV) cannot restore it.
|
permanent spectral cutoff.
|
||||||
|
|
||||||
This means a "lossless" FLAC file that was created by decoding an MP3 and
|
### 2. Steepness catches the filter shape
|
||||||
re-compressing will contain the MP3's permanent spectral cutoff. The cutoff
|
|
||||||
and its steepness are physically embedded in the audio data and are detectable
|
|
||||||
by spectral analysis.
|
|
||||||
|
|
||||||
Multiple independent studies in the audio forensics community have confirmed
|
Lossy encoders use sharp digital filters (brick-wall style) that drop from
|
||||||
that frequency-domain analysis of cutoffs is a reliable method for identifying
|
audible to silent in a few hundred Hz. Natural acoustic sources (voice,
|
||||||
lossy-sourced lossless files (see e.g. the work on "MP3Cut" and similar
|
instruments, room ambience) roll off gradually over many kHz. A drop steeper
|
||||||
tools).
|
than 2 kHz at any cutoff position is extremely unlikely to occur naturally.
|
||||||
|
|
||||||
### 2. The steepness metric catches the filter topology
|
- **MP3 (ISO/IEC 11172-3):** typical roll-off of several hundred Hz to ~2 kHz
|
||||||
|
- **AAC (ISO/IEC 13818-7):** sharper, often 200–800 Hz
|
||||||
|
- **Vorbis:** variable but always steeper than natural
|
||||||
|
|
||||||
Lossy encoders use FIR or hybrid filterbanks with a characteristic roll-off
|
### 3. Roughness detects double-encoding noise
|
||||||
slope. The steepness measurement directly captures the *order* and *design*
|
|
||||||
of that filter:
|
|
||||||
|
|
||||||
- **MP3 (ISO/IEC 11172-3)** uses a hybrid polyphase/MDCT filterbank with a
|
When audio is lossy-encoded, quantization noise is shaped to be masked by the
|
||||||
typical roll-off of several hundred Hz to about 2 kHz, depending on the
|
music. Re-encoding adds a *second* layer of noise-shaping, creating
|
||||||
bitrate and encoder implementation (LAME, Fraunhofer, etc.).
|
|
||||||
- **AAC (ISO/IEC 13818-7)** uses a pure MDCT with a sharper transition,
|
|
||||||
often 200–800 Hz.
|
|
||||||
- **Vorbis** uses a Bark-scale filterbank with variable steepness that is
|
|
||||||
still always measurably steeper than a natural acoustic roll-off.
|
|
||||||
|
|
||||||
Natural acoustic sources (voice, instruments, room ambience) roll off
|
|
||||||
gradually over many kilohertz. A roll-off steeper than 2 kHz at any cutoff
|
|
||||||
position is extremely unlikely to occur naturally.
|
|
||||||
|
|
||||||
### 3. Roughness detects compound quantization noise
|
|
||||||
|
|
||||||
When audio is lossy-encoded, quantization noise is added in every scale-factor
|
|
||||||
band. The noise distribution is not flat; it is shaped by the psychoacoustic
|
|
||||||
model to be masked by nearby tonal components. When the audio is decoded and
|
|
||||||
re-encoded, a *second* layer of noise-shaping is applied, creating
|
|
||||||
irregularities in the spectrum that are statistically unlikely in a single
|
irregularities in the spectrum that are statistically unlikely in a single
|
||||||
encode.
|
encode.
|
||||||
|
|
||||||
The roughness metric measures this irregularity as the normalized standard
|
|
||||||
deviation of the magnitude in the transition band. Values above 0.20–0.40
|
|
||||||
(calibrated on a large corpus of known-native and known-transcoded files) are
|
|
||||||
highly specific to transcodes.
|
|
||||||
|
|
||||||
### 4. Band ratio exploits the Fletcher–Munson curves
|
### 4. Band ratio exploits the Fletcher–Munson curves
|
||||||
|
|
||||||
Human hearing is least sensitive above 16 kHz. Lossy encoders exploit this by
|
Human hearing is least sensitive above 16 kHz. Lossy encoders exploit this by
|
||||||
allocating very few bits to the 16–20 kHz region, resulting in a sharp drop in
|
spending almost no bits there. In native recordings, the 16–20 kHz region is
|
||||||
energy there. The band ratio metric captures this drop. In native recordings
|
typically only 2–6 dB quieter than 12–16 kHz (band ratio 0.5–1.0). In
|
||||||
the 16–20 kHz region is typically only 2–6 dB quieter than the 12–16 kHz
|
transcoded material it is often 10–20 dB quieter (band ratio < 0.3).
|
||||||
region (band ratio 0.5–1.0). In transcoded material it is often 10–20 dB
|
|
||||||
quieter (band ratio < 0.3).
|
|
||||||
|
|
||||||
### 5. Multiple independent metrics prevent false positives
|
### 5. Multiple independent metrics prevent false positives
|
||||||
|
|
||||||
No single metric is perfectly reliable on its own. A low cutoff could
|
No single metric is perfectly reliable. A low cutoff could occur in a genuine
|
||||||
theoretically occur in a genuine recording that used an aggressive lowpass
|
recording that used an aggressive lowpass during mastering. By requiring
|
||||||
filter during production. By requiring **both** a low cutoff **and** a steep
|
**both** a low cutoff **and** a sharp roll-off (primary), or **both** high
|
||||||
roll-off (primary criteria), or **both** high roughness **and** a low band
|
roughness **and** a low band ratio (secondary), the tool avoids false
|
||||||
ratio (secondary criteria), the tool achieves high specificity.
|
positives.
|
||||||
|
|
||||||
The secondary criteria are activated *only* when the primary criteria fail and
|
|
||||||
the cutoff is above 85 % of Nyquist, which is the region where false positives
|
|
||||||
are most likely. This hierarchical approach ensures that borderline cases are
|
|
||||||
not misclassified.
|
|
||||||
|
|
||||||
### 6. The upscaling detector is conservative
|
### 6. The upscaling detector is conservative
|
||||||
|
|
||||||
For lossy files, the expected cutoff is computed from the file's *declared*
|
For lossy files, an 8-percentage-point margin is subtracted before flagging a
|
||||||
bitrate. A margin of 8 percentage points is subtracted before flagging a file
|
file as upscaled. This accounts for encoder variability and prevents false
|
||||||
as upscaled. This margin accounts for encoder variability (different LAME
|
positives on legitimate encodes that simply use a conservative lowpass.
|
||||||
presets, AAC profiles, etc.) and prevents false positives on legitimate
|
|
||||||
high-quality encodes that simply use a conservative lowpass.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -352,4 +400,3 @@ References
|
||||||
1999 (Fletcher–Munson equal-loudness contours).
|
1999 (Fletcher–Munson equal-loudness contours).
|
||||||
- Lerch, A. - *An Introduction to Audio Content Analysis*, Wiley, 2012
|
- Lerch, A. - *An Introduction to Audio Content Analysis*, Wiley, 2012
|
||||||
(spectral features for audio forensics).
|
(spectral features for audio forensics).
|
||||||
|
|
||||||
|
|
|
||||||
22
tcd.c
22
tcd.c
|
|
@ -616,15 +616,19 @@ static void render_visual_spectrum(
|
||||||
char title[256];
|
char title[256];
|
||||||
const char *short_name = strrchr(filename, '/');
|
const char *short_name = strrchr(filename, '/');
|
||||||
short_name = short_name ? short_name + 1 : filename;
|
short_name = short_name ? short_name + 1 : filename;
|
||||||
int info_len = snprintf(title, sizeof(title), " %s | %s | %d Hz | %d ch",
|
int info_len = snprintf(title, sizeof(title), " %s | %s | %d Hz | %d ch | %lldk",
|
||||||
short_name, fmt_name, sample_rate, channels);
|
short_name, fmt_name, sample_rate, channels, (long long)(bitrate / 1000));
|
||||||
int total_w = term_w;
|
int total_w = term_w;
|
||||||
if (info_len > total_w) {
|
if (info_len > total_w) {
|
||||||
char truncated[256];
|
char truncated[256];
|
||||||
snprintf(truncated, sizeof(truncated), " %s | %d Hz | %d ch",
|
snprintf(truncated, sizeof(truncated), " %s | %d Hz | %d ch | %lldk",
|
||||||
short_name, sample_rate, channels);
|
short_name, sample_rate, channels, (long long)(bitrate / 1000));
|
||||||
if ((int)strlen(truncated) > total_w) {
|
if ((int)strlen(truncated) > total_w) {
|
||||||
snprintf(truncated, sizeof(truncated), " %s | %d ch", short_name, channels);
|
snprintf(truncated, sizeof(truncated), " %s | %d ch | %lldk",
|
||||||
|
short_name, channels, (long long)(bitrate / 1000));
|
||||||
|
if ((int)strlen(truncated) > total_w) {
|
||||||
|
snprintf(truncated, sizeof(truncated), " %s | %lldk",
|
||||||
|
short_name, (long long)(bitrate / 1000));
|
||||||
if ((int)strlen(truncated) > total_w) {
|
if ((int)strlen(truncated) > total_w) {
|
||||||
snprintf(truncated, sizeof(truncated), " %s", short_name);
|
snprintf(truncated, sizeof(truncated), " %s", short_name);
|
||||||
if ((int)strlen(truncated) > total_w) {
|
if ((int)strlen(truncated) > total_w) {
|
||||||
|
|
@ -632,6 +636,7 @@ static void render_visual_spectrum(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
printf(ANSI_BOLD ANSI_CYAN "%s" ANSI_RESET, truncated);
|
printf(ANSI_BOLD ANSI_CYAN "%s" ANSI_RESET, truncated);
|
||||||
printf("\n");
|
printf("\n");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1294,7 +1299,7 @@ static int process_file(const char *filename, const Options *opts)
|
||||||
printf(ANSI_BOLD ANSI_CYAN "Channels:" ANSI_RESET " %d\n", channels);
|
printf(ANSI_BOLD ANSI_CYAN "Channels:" ANSI_RESET " %d\n", channels);
|
||||||
printf(ANSI_BOLD ANSI_CYAN "Windows:" ANSI_RESET " %d\n", analyzer.count);
|
printf(ANSI_BOLD ANSI_CYAN "Windows:" ANSI_RESET " %d\n", analyzer.count);
|
||||||
printf(ANSI_BOLD ANSI_CYAN "Peak:" ANSI_RESET " %.1f dBFS\n", peak_db);
|
printf(ANSI_BOLD ANSI_CYAN "Peak:" ANSI_RESET " %.1f dBFS\n", peak_db);
|
||||||
printf(ANSI_BOLD ANSI_CYAN "Cutoff:" ANSI_RESET " %.0f Hz (%.1f%% of Nyquist)\n", cutoff_hz, 100.0 * cutoff_hz / nyquist);
|
printf(ANSI_BOLD ANSI_CYAN "Cutoff:" ANSI_RESET " %.0f / %.0f Hz = %.1f%%\n", cutoff_hz, nyquist, 100.0 * cutoff_hz / nyquist);
|
||||||
printf(ANSI_BOLD ANSI_CYAN "Steepness:" ANSI_RESET " %.0f Hz\n", steepness);
|
printf(ANSI_BOLD ANSI_CYAN "Steepness:" ANSI_RESET " %.0f Hz\n", steepness);
|
||||||
printf(ANSI_BOLD ANSI_CYAN "Roughness:" ANSI_RESET " %.3f\n", roughness);
|
printf(ANSI_BOLD ANSI_CYAN "Roughness:" ANSI_RESET " %.3f\n", roughness);
|
||||||
printf(ANSI_BOLD ANSI_CYAN "Band ratio:" ANSI_RESET " %.3f\n", band_ratio);
|
printf(ANSI_BOLD ANSI_CYAN "Band ratio:" ANSI_RESET " %.3f\n", band_ratio);
|
||||||
|
|
@ -1329,8 +1334,9 @@ static int process_file(const char *filename, const Options *opts)
|
||||||
} else {
|
} else {
|
||||||
printf(ANSI_GREEN "NATIVE" ANSI_RESET " (single encode at this bitrate)\n");
|
printf(ANSI_GREEN "NATIVE" ANSI_RESET " (single encode at this bitrate)\n");
|
||||||
}
|
}
|
||||||
printf(ANSI_BOLD ANSI_CYAN "Verdict Info:" ANSI_RESET " cutoff=%.1f%% Nyquist, expected≥%.0f%% for %lld kbps",
|
printf(ANSI_BOLD ANSI_CYAN "Verdict Info:" ANSI_RESET " bandwidth used=%.1f%% (%.0f/%.0f Hz), expected≥%.0f%% for %lld kbps",
|
||||||
100.0 * cutoff_ratio, 100.0 * expected_min,
|
100.0 * cutoff_ratio, effective_cutoff, nyquist,
|
||||||
|
100.0 * expected_min,
|
||||||
bitrate > 0 ? (long long)(bitrate / 1000) : 0);
|
bitrate > 0 ? (long long)(bitrate / 1000) : 0);
|
||||||
if (upscaled) {
|
if (upscaled) {
|
||||||
printf(" → suggests ");
|
printf(" → suggests ");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue