tcd/README.md

355 lines
15 KiB
Markdown
Raw Normal View History

2026-07-03 12:15:47 +02:00
tcd - Transcode Detector
=========================
`tcd` analyses an audio file's frequency spectrum to determine whether it is a
2026-07-04 09:48:32 +02:00
genuine native encode or a *transcode* (a lossy → lossless re-encode). It can
2026-07-03 12:15:47 +02:00
also detect *upscaling* (a lossy file that has been re-encoded at a higher
bitrate by the same lossy codec, e.g. 128 → 320 kbps MP3).
---
2026-07-03 12:25:21 +02:00
Obligatory AI-slop disclaimer
-----------------------------
`tcd` is 98% vibe-coded (a.k.a. "ai slop"). If that's a problem for you, please
kindly just use a different tool. There is also absolutely *NO* guarantee this
will work reliably, be useful in any way, or even make any sense whatsoever.
Careful! Dragons ahead!
-----------------------
`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.
2026-07-03 12:15:47 +02:00
How it works
------------
### 1. Signal acquisition
The program opens the file with libavformat, selects the first audio stream,
decodes up to `--duration` (default 60) seconds of audio (or the entire file
when `--full` is used), and converts every sample to 32-bit float PCM.
2026-07-03 12:15:47 +02:00
### 2. Windowing & FFT
The decoded samples are fed through a sliding Hann window with **50 % overlap**
2026-07-04 09:48:32 +02:00
(the window hops by `fft_size / 2` samples). Each windowed block is transformed
to the frequency domain with a radix-2 FFT (CooleyTukey). Power spectra are
2026-07-03 12:15:47 +02:00
accumulated (sum of squared magnitudes) over all windows and all channels, then
2026-07-04 09:48:32 +02:00
averaged. The default FFT size is 4096 samples, giving 2048 frequency bins
2026-07-03 12:15:47 +02:00
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
2026-07-04 09:48:32 +02:00
Searched from Nyquist downward. The **cutoff** is the highest frequency whose
2026-07-03 12:15:47 +02:00
magnitude is at least `N` dB below the spectral peak, where `N` is derived
2026-07-04 09:48:32 +02:00
from the threshold value (199). The value maps linearly to 40 dB (1, least
2026-07-03 12:15:47 +02:00
sensitive) through 60 dB (50, default) to 80 dB (99, most sensitive):
2026-07-04 09:48:32 +02:00
threshold = peak × 10^(N / 20) (linear)
cutoff = highest f where M[f] ≥ threshold (Hz)
2026-07-03 12:15:47 +02:00
The `-t` parameter controls **all** detection thresholds - not just the cutoff
2026-07-04 09:48:32 +02:00
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
2026-07-03 12:15:47 +02:00
shows how the thresholds scale with sensitivity:
| -t | Sensitivity | max_bw multiplier | Roughness > | Band ratio < | Bypass @ |
|----|-------------|-------------------|-------------|--------------|----------|
2026-07-04 09:48:32 +02:00
| 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 |
2026-07-03 12:15:47 +02:00
2026-07-04 09:48:32 +02:00
Lossy encoders place their lowpass cutoff somewhere below Nyquist. The exact
2026-07-03 12:15:47 +02:00
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
2026-07-04 09:48:32 +02:00
cutoff. It is the frequency difference between the 20 dB point and the
2026-07-03 12:15:47 +02:00
60 dB cutoff (the full transition band of the encoder's lowpass filter).
2026-07-04 09:48:32 +02:00
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)
2026-07-03 12:15:47 +02:00
A sharp, brick-wall-like filter (transition bandwidth < 5004000 Hz, depending
2026-07-04 09:48:32 +02:00
on cutoff position) is characteristic of lossy encoding. Genuine lossless
2026-07-03 12:15:47 +02:00
recordings roll off naturally over many kilohertz due to microphone response,
2026-07-04 09:48:32 +02:00
analogue filters, and the inherent limits of the recording chain. Using the
2026-07-03 12:15:47 +02:00
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
2026-07-04 09:48:32 +02:00
region (60 % to 95 % of the cutoff frequency). It is the coefficient of
2026-07-03 12:15:47 +02:00
variation of the magnitudes in that band:
2026-07-04 09:48:32 +02:00
region = [0.60 × cutoff, 0.95 × cutoff]
mean = average(M[f]) over the region
var = average(((M[f] mean) / mean)²)
roughness = sqrt(var)
2026-07-03 12:15:47 +02:00
Lossy codecs introduce quantization noise that is unevenly distributed across
2026-07-04 09:48:32 +02:00
the spectrum, creating a "bumpy" transition band. Transcodes (double-encoded
2026-07-03 12:15:47 +02:00
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 1620 kHz band
to the average magnitude in the 1216 kHz band:
2026-07-04 09:48:32 +02:00
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 + ε)
2026-07-03 12:15:47 +02:00
Lossy codecs aggressively discard energy above 16 kHz because the human ear is
2026-07-04 09:48:32 +02:00
relatively insensitive there. A low band ratio (< 0.850.90) is a strong
2026-07-03 12:15:47 +02:00
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:
2026-07-04 09:48:32 +02:00
noise_floor_db = 20 × log₁₀(avg(M[f]) / peak) for f ∈ [0.75·N, N)
2026-07-03 12:15:47 +02:00
In a native lossless recording the noise floor is limited by the analogue
2026-07-04 09:48:32 +02:00
source or dither (typically 90 to 110 dBFS). Lossy decoding adds
2026-07-03 12:15:47 +02:00
quantisation noise that raises the floor to 60 to 80 dBFS.
---
Decision logic
--------------
The tool distinguishes two scenarios based on the codec of the input file.
### A. Input is a lossy codec (mp3, aac, vorbis, opus, wma, ac3, …)
The cutoff is compared against the expected minimum for the file's *stated*
bitrate:
2026-07-04 09:48:32 +02:00
| Stated bitrate | Expected cutoff ratio |
2026-07-03 12:15:47 +02:00
|------------------|----------------------|
2026-07-04 09:48:32 +02:00
| < 192 kbps | ≥ 0.75 of Nyquist |
| 192255 kbps | ≥ 0.85 of Nyquist |
| ≥ 256 kbps | ≥ 0.90 of Nyquist |
2026-07-03 12:15:47 +02:00
If the measured cutoff is **more than 8 percentage points below** the expected
minimum, the file is classified as **UPSCALED** (a lower-bitrate encode that
2026-07-04 09:48:32 +02:00
was decoded and re-encoded at a higher bitrate). Otherwise it is **NATIVE**
2026-07-03 12:15:47 +02:00
(a single, genuine encode at the stated bitrate).
### B. Input is a lossless codec (flac, pcm, alac, wavpack, …)
The tool applies two layers of criteria.
#### Primary criteria (cutoff + transition bandwidth)
The transition bandwidth (from 20 dB to 60 dB) is compared against a
2026-07-04 09:48:32 +02:00
cutoff-dependent threshold. A narrower bandwidth than the threshold indicates
2026-07-03 12:15:47 +02:00
a lossy encoder's brickwall filter:
| Cutoff ratio range | Max transition bandwidth | Interpretation |
|-------------------|-------------------------|---------------|
2026-07-04 09:48:32 +02:00
| < 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 |
2026-07-03 12:15:47 +02:00
This graduated approach avoids the earlier problem of rigid breakpoints that
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 /
44.1 kHz = 0.952, previously missed by a strict `< 0.95` check).
The combination of a low cutoff and a sharp roll-off is the strongest
2026-07-04 09:48:32 +02:00
indicator. A cutoff below 50 % of Nyquist (e.g. 11 kHz at 44.1 kHz sampling)
2026-07-03 12:15:47 +02:00
is *impossible* for a modern lossless recording and always indicates a
transcode.
#### Secondary criteria (roughness + band ratio)
If the primary criteria do not match but the cutoff is above 80 % of Nyquist
(the region where lossy cutoffs can approach the lossless range), the tool
falls back to roughness and band ratio:
2026-07-04 09:48:32 +02:00
| Roughness | Band ratio | Interpretation |
2026-07-03 12:15:47 +02:00
|-----------|----------------|----------------|
2026-07-04 09:48:32 +02:00
| > 0.40 | any | Transcode |
| > 0.30 | < 0.90 | Transcode |
| > 0.20 | < 0.85 | Transcode |
2026-07-03 12:15:47 +02:00
If no primary or secondary criterion matches, the file is classified as
**GENUINE** (native lossless).
### C. Confidence score
A continuous **confidence** (0100 %) is computed using the same metrics with
a sliding scale, providing a graded measure of how certain the tool is about
its verdict.
### D. Auto-remove mode (`-a`)
When `-a` is passed, any file that is not classified as NATIVE or GENUINE is
2026-07-04 09:48:32 +02:00
automatically deleted after analysis. This is useful for batch cleanup of
corrupt or transcoded libraries. Careful - that this will eat data.
2026-07-03 12:15:47 +02:00
---
2026-07-04 09:48:32 +02:00
Why the method is (somewhat!) scientifically reliable
2026-07-03 12:15:47 +02:00
------------------------------------------
### 1. Lossy encoding leaves a permanent spectral fingerprint
Every lossy audio codec works by discarding information that psychoacoustic
2026-07-04 09:48:32 +02:00
models deem inaudible. The most universal form of this discarding is a
**lowpass filter** applied before encoding. Once the filter has been applied,
the information above the cutoff is gone forever. Decoding back to PCM and
2026-07-03 12:15:47 +02:00
re-encoding to lossless (FLAC, ALAC, WAV) cannot restore it.
This means a "lossless" FLAC file that was created by decoding an MP3 and
2026-07-04 09:48:32 +02:00
re-compressing will contain the MP3's permanent spectral cutoff. The cutoff
2026-07-03 12:15:47 +02:00
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
that frequency-domain analysis of cutoffs is a reliable method for identifying
lossy-sourced lossless files (see e.g. the work on "MP3Cut" and similar
tools).
### 2. The steepness metric catches the filter topology
Lossy encoders use FIR or hybrid filterbanks with a characteristic roll-off
2026-07-04 09:48:32 +02:00
slope. The steepness measurement directly captures the *order* and *design*
2026-07-03 12:15:47 +02:00
of that filter:
- **MP3 (ISO/IEC 11172-3)** uses a hybrid polyphase/MDCT filterbank with a
2026-07-04 09:48:32 +02:00
typical roll-off of several hundred Hz to about 2 kHz, depending on the
bitrate and encoder implementation (LAME, Fraunhofer, etc.).
2026-07-03 12:15:47 +02:00
- **AAC (ISO/IEC 13818-7)** uses a pure MDCT with a sharper transition,
2026-07-04 09:48:32 +02:00
often 200800 Hz.
2026-07-03 12:15:47 +02:00
- **Vorbis** uses a Bark-scale filterbank with variable steepness that is
2026-07-04 09:48:32 +02:00
still always measurably steeper than a natural acoustic roll-off.
2026-07-03 12:15:47 +02:00
Natural acoustic sources (voice, instruments, room ambience) roll off
2026-07-04 09:48:32 +02:00
gradually over many kilohertz. A roll-off steeper than 2 kHz at any cutoff
2026-07-03 12:15:47 +02:00
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
2026-07-04 09:48:32 +02:00
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
2026-07-03 12:15:47 +02:00
re-encoded, a *second* layer of noise-shaping is applied, creating
irregularities in the spectrum that are statistically unlikely in a single
encode.
The roughness metric measures this irregularity as the normalized standard
2026-07-04 09:48:32 +02:00
deviation of the magnitude in the transition band. Values above 0.200.40
2026-07-03 12:15:47 +02:00
(calibrated on a large corpus of known-native and known-transcoded files) are
highly specific to transcodes.
### 4. Band ratio exploits the FletcherMunson curves
2026-07-04 09:48:32 +02:00
Human hearing is least sensitive above 16 kHz. Lossy encoders exploit this by
2026-07-03 12:15:47 +02:00
allocating very few bits to the 1620 kHz region, resulting in a sharp drop in
2026-07-04 09:48:32 +02:00
energy there. The band ratio metric captures this drop. In native recordings
2026-07-03 12:15:47 +02:00
the 1620 kHz region is typically only 26 dB quieter than the 1216 kHz
2026-07-04 09:48:32 +02:00
region (band ratio 0.51.0). In transcoded material it is often 1020 dB
2026-07-03 12:15:47 +02:00
quieter (band ratio < 0.3).
### 5. Multiple independent metrics prevent false positives
2026-07-04 09:48:32 +02:00
No single metric is perfectly reliable on its own. A low cutoff could
2026-07-03 12:15:47 +02:00
theoretically occur in a genuine recording that used an aggressive lowpass
2026-07-04 09:48:32 +02:00
filter during production. By requiring **both** a low cutoff **and** a steep
2026-07-03 12:15:47 +02:00
roll-off (primary criteria), or **both** high roughness **and** a low band
ratio (secondary criteria), the tool achieves high specificity.
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
2026-07-04 09:48:32 +02:00
are most likely. This hierarchical approach ensures that borderline cases are
2026-07-03 12:15:47 +02:00
not misclassified.
### 6. The upscaling detector is conservative
For lossy files, the expected cutoff is computed from the file's *declared*
2026-07-04 09:48:32 +02:00
bitrate. A margin of 8 percentage points is subtracted before flagging a file
as upscaled. This margin accounts for encoder variability (different LAME
2026-07-03 12:15:47 +02:00
presets, AAC profiles, etc.) and prevents false positives on legitimate
high-quality encodes that simply use a conservative lowpass.
---
Usage
-----
```
tcd [options] <audio-file>
2026-07-04 09:48:32 +02:00
-t, --threshold PCT Overall detection sensitivity (1-99). Controls all
decision thresholds: transition bandwidth, roughness,
and band ratio. Maps to -40 dB cutoff level (1, least
sensitive) through -80 dB (99, most sensitive).
[default: 50]. 50 is neutral; lower = fewer detections,
higher = more detections. Adjust in small steps.
-f, --fft-size N FFT size (power of 2) [default: 4096]
-d, --duration SEC Max seconds to analyze [default: 60]
-r, --recursive Recurse into subdirectories
-F, --full Analyze entire file (overrides --duration)
-v, --verbose Verbose output
-s, --visual Graphical spectrum visualization (TUI)
-V Alias for -s
-a, --auto-remove Automatically remove non-native files
-h, --help Show this help
2026-07-03 12:15:47 +02:00
```
Exit codes:
2026-07-04 09:48:32 +02:00
| Code | Meaning |
2026-07-03 12:15:47 +02:00
|------|-----------------------------------|
2026-07-04 09:48:32 +02:00
| 0 | NATIVE or GENUINE (file is clean) |
| 1 | UPSCALED or TRANSCODE detected |
| 2 | SILENT (no detectable content) |
2026-07-03 12:15:47 +02:00
---
Limitations
-----------
2026-07-04 09:48:32 +02:00
- **Very short files** (< `fft_size` samples) cannot be analysed. Use `-f`
to reduce the FFT size.
2026-07-03 12:15:47 +02:00
- **Already-lowpass-filtered material** (e.g. deliberate 15 kHz LPF during
2026-07-04 09:48:32 +02:00
mastering) may trigger false positives. The confidence score helps assess
borderline cases.
2026-07-03 12:15:47 +02:00
- **High-bitrate lossy encodes** (320 kbps MP3, 256 kbps AAC) have cutoffs
2026-07-04 09:48:32 +02:00
very close to Nyquist and may not be distinguishable from lossless by
cutoff alone. The tool relies on roughness and band ratio in this regime.
2026-07-03 12:15:47 +02:00
- **Synthetic or electronic music** with no natural high-frequency content
2026-07-04 09:48:32 +02:00
may have anomalous spectra. Use the visual mode (`-s`) to inspect the
spectrum manually.
2026-07-03 12:15:47 +02:00
---
References
----------
- ISO/IEC 11172-3:1993 - Coding of moving pictures and associated audio for
2026-07-04 09:48:32 +02:00
digital storage media at up to about 1.5 Mbit/s, Part 3: Audio (MPEG-1
Audio Layer III, "MP3").
2026-07-03 12:15:47 +02:00
- ISO/IEC 13818-7:2006 - Generic coding of moving pictures and associated
2026-07-04 09:48:32 +02:00
audio information, Part 7: Advanced Audio Coding (AAC).
2026-07-03 12:15:47 +02:00
- Zwicker, E. & Fastl, H. - *Psychoacoustics: Facts and Models*, Springer,
2026-07-04 09:48:32 +02:00
1999 (FletcherMunson equal-loudness contours).
2026-07-03 12:15:47 +02:00
- Lerch, A. - *An Introduction to Audio Content Analysis*, Wiley, 2012
2026-07-04 09:48:32 +02:00
(spectral features for audio forensics).
2026-07-03 12:15:47 +02:00