2026-07-04 21:04:24 +02:00
|
|
|
|

|
|
|
|
|
|
|
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-08-03 00:21:41 +02:00
|
|
|
|
Obligatory AI notice
|
2026-07-03 12:25:21 +02:00
|
|
|
|
-----------------------------
|
2026-08-03 00:21:41 +02:00
|
|
|
|
`tcd` was coded with the help of AI. If that's a problem for you, please
|
|
|
|
|
|
kindly just use a different tool.
|
2026-07-03 12:25:21 +02:00
|
|
|
|
|
|
|
|
|
|
Careful! Dragons ahead!
|
|
|
|
|
|
-----------------------
|
2026-08-03 00:21:41 +02:00
|
|
|
|
`tcd` *can* and absolutely *will* delete your data - don't blindly use -a (!!!), and
|
2026-07-03 12:25:21 +02:00
|
|
|
|
please read at least the --help information and *understand* what -a does.
|
|
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
---
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 20:55:45 +02:00
|
|
|
|
How it works (precise description)
|
|
|
|
|
|
-----------------------------------
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 20:55:45 +02:00
|
|
|
|
tcd decodes the audio file to PCM via FFmpeg, applies a Hann window, and
|
|
|
|
|
|
computes the FFT (default 4096-point) with 50% overlap for each windowed
|
|
|
|
|
|
frame. Magnitude spectra from all frames are accumulated and averaged into a
|
|
|
|
|
|
single power spectrum. From this averaged spectrum, five metrics are computed:
|
|
|
|
|
|
|
|
|
|
|
|
- **Cutoff**: highest frequency bin whose magnitude ≥ peak × 10^(threshold_db/20),
|
|
|
|
|
|
controlled by the sensitivity setting (see [Threshold sensitivity](#threshold-sensitivity)).
|
|
|
|
|
|
- **Steepness**: transition bandwidth between the −20 dB and −60 dB points
|
|
|
|
|
|
(relative to peak), measuring how sharply the spectrum rolls off.
|
|
|
|
|
|
- **Noise floor**: average magnitude in the top quartile of the spectrum
|
|
|
|
|
|
(≈16500–22050 Hz at 44.1 kHz sample rate), expressed in dB relative to peak.
|
|
|
|
|
|
- **Roughness**: coefficient of variation (standard deviation / mean) of
|
|
|
|
|
|
magnitude values in the 60%–95% band below cutoff. This quantifies spectral
|
|
|
|
|
|
irregularity introduced by quantization noise.
|
|
|
|
|
|
- **Band ratio**: ratio of mean magnitude in 16–20 kHz to mean magnitude in
|
|
|
|
|
|
12–16 kHz. Lossy codecs under-allocate bits above 16 kHz, producing a
|
|
|
|
|
|
characteristic dip in this region.
|
|
|
|
|
|
|
|
|
|
|
|
The verdict is determined by evaluating these metrics against empirically
|
|
|
|
|
|
derived thresholds (which are scaled by the [sensitivity setting](#threshold-sensitivity)),
|
|
|
|
|
|
first using cutoff + steepness, then secondarily using roughness + band ratio
|
|
|
|
|
|
for high-cutoff cases. For lossy input formats, cutoff is compared against
|
|
|
|
|
|
bitrate-specific expectations to detect upscaling.
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
What tcd displays and what each number means
|
|
|
|
|
|
---------------------------------------------
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 20:01:30 +02:00
|
|
|
|
Here is example output from an MP3 file:
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
```
|
|
|
|
|
|
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
|
|
|
|
|
|
```
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
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
|
2026-07-04 20:02:19 +02:00
|
|
|
|
the *Nyquist frequency*. It is a physical ceiling - higher frequencies simply
|
2026-07-04 19:59:09 +02:00
|
|
|
|
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 |
|
|
|
|
|
|
|
2026-07-04 20:01:30 +02:00
|
|
|
|
**What 94.3% means for the example file:** 20790 / 22050 = 94.3%. The cutoff is very
|
2026-07-04 19:59:09 +02:00
|
|
|
|
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
|
2026-07-04 20:02:19 +02:00
|
|
|
|
re-encoded it at 320 kbps - the cutoff is permanent and cannot be restored.
|
2026-07-04 19:59:09 +02:00
|
|
|
|
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
|
2026-07-04 20:02:19 +02:00
|
|
|
|
recording rolls off like a natural hillside - gradual, smooth, taking
|
2026-07-04 19:59:09 +02:00
|
|
|
|
thousands of Hz to go from loud to silent. A lossy encoder's lowpass filter
|
2026-07-04 20:02:19 +02:00
|
|
|
|
creates a cliff - a near-vertical drop from audible signal to nothing.
|
2026-07-04 19:59:09 +02:00
|
|
|
|
|
|
|
|
|
|
**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
|
2026-07-04 20:02:19 +02:00
|
|
|
|
naturally fades across many frequencies - the harmonics near the top end of
|
2026-07-04 19:59:09 +02:00
|
|
|
|
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
|
2026-07-04 20:02:19 +02:00
|
|
|
|
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
|
2026-07-04 19:59:09 +02:00
|
|
|
|
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.
|
|
|
|
|
|
|
2026-07-04 20:02:19 +02:00
|
|
|
|
**The analogy:** Lossy encoding introduces quantization noise - tiny
|
2026-07-04 19:59:09 +02:00
|
|
|
|
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 |
|
|
|
|
|
|
|------------|---------------|
|
2026-07-04 20:02:19 +02:00
|
|
|
|
| >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 |
|
2026-07-04 19:59:09 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### Noise floor (-56.4 dB)
|
|
|
|
|
|
|
|
|
|
|
|
**What it measures:** The average noise level in the highest quarter of the
|
|
|
|
|
|
frequency range (roughly 16500–22050 Hz).
|
|
|
|
|
|
|
2026-07-04 20:02:19 +02:00
|
|
|
|
**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
|
2026-07-04 19:59:09 +02:00
|
|
|
|
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 |
|
|
|
|
|
|
|-------------|---------------|
|
2026-07-04 20:02:19 +02:00
|
|
|
|
| −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 |
|
2026-07-04 19:59:09 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-03 12:15:47 +02:00
|
|
|
|
bitrate:
|
|
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
| Stated bitrate | Expected cutoff |
|
|
|
|
|
|
|----------------|-----------------|
|
|
|
|
|
|
| <192 kbps | ≥75% of Nyquist |
|
|
|
|
|
|
| 192–255 kbps | ≥85% of Nyquist |
|
|
|
|
|
|
| ≥256 kbps | ≥90% of Nyquist |
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
If the actual cutoff is **more than 8 percentage points lower** than expected,
|
|
|
|
|
|
the file is **UPSCALED**. For example, a file claiming 320 kbps (expecting
|
|
|
|
|
|
≥90%) but showing a cutoff of 70% (≈15400 Hz) would be flagged as upscaled
|
|
|
|
|
|
from ~96 kbps.
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 20:02:19 +02:00
|
|
|
|
Otherwise it is **NATIVE** - a genuine single encode at this bitrate.
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
### Scenario 2: The input file is lossless (FLAC, WAV, ALAC, etc.)
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
The file claims to be lossless. The question is: was it actually created by
|
|
|
|
|
|
decoding a lossy file and re-encoding to lossless?
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
tcd uses a **two-layer** check:
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 20:02:19 +02:00
|
|
|
|
**Layer 1 - Cutoff + Steepness (primary):**
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
| Cutoff range | Max steepness allowed | If exceeded → |
|
|
|
|
|
|
|-------------|----------------------|---------------|
|
|
|
|
|
|
| <50% of Nyquist | 4000 Hz | TRANSCODE |
|
|
|
|
|
|
| 50–70% | 3000 Hz | TRANSCODE |
|
|
|
|
|
|
| 70–80% | 2000 Hz | TRANSCODE |
|
|
|
|
|
|
| 80–90% | 1200 Hz | TRANSCODE |
|
|
|
|
|
|
| ≥90% | 500 Hz | TRANSCODE |
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
This works because lossy cutoffs are always sharp (low steepness). A lossless
|
|
|
|
|
|
recording that happens to have a low cutoff (e.g., a muddy recording with
|
2026-07-04 20:02:19 +02:00
|
|
|
|
little treble) would still have a *gradual* roll-off (high steepness) - you
|
2026-07-04 19:59:09 +02:00
|
|
|
|
need both a low cutoff **and** a sharp drop to convict.
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 20:02:19 +02:00
|
|
|
|
**Layer 2 - Roughness + Band ratio (secondary):**
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
If Layer 1 did not trigger but the cutoff is above 80%, tcd checks roughness
|
|
|
|
|
|
and band ratio. This catches transcodes where the cutoff happens to be high
|
|
|
|
|
|
enough to pass Layer 1 but the spectrum is still bumpy and depleted in the
|
|
|
|
|
|
top band:
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
| Roughness | Band ratio | If matched → |
|
|
|
|
|
|
|-----------|------------|--------------|
|
|
|
|
|
|
| >0.40 | any | TRANSCODE |
|
|
|
|
|
|
| >0.30 | <0.90 | TRANSCODE |
|
|
|
|
|
|
| >0.20 | <0.85 | TRANSCODE |
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
If neither layer triggers, the file is **GENUINE** (native lossless).
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
### The verdicts at a glance
|
|
|
|
|
|
|
|
|
|
|
|
| Verdict | Input codec | What it means |
|
|
|
|
|
|
|---------|------------|---------------|
|
2026-07-04 20:02:19 +02:00
|
|
|
|
| **NATIVE** | lossy | Encoded once at the stated bitrate - genuine |
|
2026-07-04 19:59:09 +02:00
|
|
|
|
| **UPSCALED** | lossy | Originally encoded at a lower bitrate, then re-encoded higher |
|
2026-07-04 20:02:19 +02:00
|
|
|
|
| **GENUINE** | lossless | Appears to be native lossless - no evidence of lossy origin |
|
2026-07-04 19:59:09 +02:00
|
|
|
|
| **TRANSCODE** | lossless | Originated from a lossy source, decoded to lossless |
|
|
|
|
|
|
| **SILENT** | any | No detectable audio content |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-04 20:55:45 +02:00
|
|
|
|
Threshold sensitivity (`-t`)
|
|
|
|
|
|
-----------------------------
|
|
|
|
|
|
|
|
|
|
|
|
The `-t` parameter (1–99, default 50) controls how aggressively tcd detects
|
|
|
|
|
|
transcodes and upscaled files. It works at two levels.
|
|
|
|
|
|
|
|
|
|
|
|
### 1. Cutoff detection threshold
|
|
|
|
|
|
|
|
|
|
|
|
The cutoff frequency is defined as the highest bin whose magnitude is at
|
|
|
|
|
|
least `peak × 10^(threshold_db / 20)`. The percentage is mapped linearly
|
|
|
|
|
|
to dB:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
threshold_db = −(40 + (pct − 1) × 40 / 98)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
| `-t` value | threshold_db | Magnitude threshold | Behaviour |
|
|
|
|
|
|
|-----------|-------------|--------------------|-----------|
|
|
|
|
|
|
| 1 | −40.0 dB | 1.0 % of peak | Least sensitive – only the strongest signal counts |
|
|
|
|
|
|
| 50 | −60.0 dB | 0.1 % of peak | Default – balances sensitivity and specificity |
|
|
|
|
|
|
| 99 | −80.0 dB | 0.01 % of peak | Most sensitive – detects cutoff deep in the noise floor |
|
|
|
|
|
|
|
|
|
|
|
|
A higher `-t` (more negative dB) detects the cutoff at a *higher* frequency
|
|
|
|
|
|
because it can follow the spectrum deeper into the noise floor. This makes
|
|
|
|
|
|
the tool stricter: small spectral remnants above a true lossy cutoff are
|
|
|
|
|
|
still recognised as "signal."
|
|
|
|
|
|
|
|
|
|
|
|
### 2. Verdict-threshold scaling
|
|
|
|
|
|
|
|
|
|
|
|
The same percentage also produces a continuous `sensitivity` factor:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
sensitivity = (pct − 1) / 98.0
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
| `-t` value | sensitivity | Effect on verdict thresholds |
|
|
|
|
|
|
|-----------|-------------|------------------------------|
|
|
|
|
|
|
| 1 | 0.00 | Doubles the allowed steepness – very permissive, few false positives |
|
|
|
|
|
|
| 50 | 0.50 | Base thresholds used as published above |
|
|
|
|
|
|
| 99 | 1.00 | Steepness limits multiplied by 0.25; roughness thresholds lowered by up to 4× – very strict, catches borderline cases |
|
|
|
|
|
|
|
|
|
|
|
|
The table of steepness thresholds used in the transcode verdict (see
|
|
|
|
|
|
[How tcd combines these clues](#how-tcd-combines-these-clues-into-a-verdict))
|
|
|
|
|
|
is scaled by:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
bw_factor = max(2.0 × (1.0 − sensitivity), 0.25)
|
|
|
|
|
|
allowed_steepness = base_max_bw × bw_factor
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
The roughness thresholds (r1 = 0.40, r2 = 0.30, r3 = 0.20) are likewise
|
|
|
|
|
|
scaled:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
r1 = 0.40 × (1.0 + (0.5 − sensitivity) × 1.5)
|
|
|
|
|
|
r2 = 0.30 × (1.0 + (0.5 − sensitivity) × 1.5)
|
|
|
|
|
|
r3 = 0.20 × (1.0 + (0.5 − sensitivity) × 1.5)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. Practical guidance
|
|
|
|
|
|
|
|
|
|
|
|
- **Default (50):** well-tuned for most material. Use this unless you have a
|
|
|
|
|
|
specific reason to change it.
|
|
|
|
|
|
- **Lower values (1–40):** reduce false positives on already-filtered material
|
|
|
|
|
|
(e.g. deliberate lowpass during mastering). Rarely needed.
|
|
|
|
|
|
- **Higher values (60–99):** catch transcodes that leave very little spectral
|
|
|
|
|
|
evidence. Useful for batch cleaning of a library, but may increase false
|
|
|
|
|
|
positives on quiet or synthetic content.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
Confidence score
|
|
|
|
|
|
----------------
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
its verdict.
|
|
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
Auto-remove mode (`-a`)
|
|
|
|
|
|
------------------------
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-04 20:02:19 +02:00
|
|
|
|
corrupt or transcoded libraries. Careful - 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-04 19:59:09 +02:00
|
|
|
|
------------------------------------------------------
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
|
|
|
|
|
### 1. Lossy encoding leaves a permanent spectral fingerprint
|
|
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
Every lossy audio codec discards information. The most obvious form is a
|
2026-07-04 20:02:19 +02:00
|
|
|
|
**lowpass filter** - once applied, the frequencies above the cutoff are gone
|
2026-07-04 19:59:09 +02:00
|
|
|
|
forever. Decoding back to PCM and re-encoding to lossless cannot restore them.
|
|
|
|
|
|
This means a "lossless" FLAC file made from an MP3 will contain the MP3's
|
|
|
|
|
|
permanent spectral cutoff.
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
### 2. Steepness catches the filter shape
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
Lossy encoders use sharp digital filters (brick-wall style) that drop from
|
|
|
|
|
|
audible to silent in a few hundred Hz. Natural acoustic sources (voice,
|
|
|
|
|
|
instruments, room ambience) roll off gradually over many kHz. A drop steeper
|
|
|
|
|
|
than 2 kHz at any cutoff position is extremely unlikely to occur naturally.
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
- **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
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
### 3. Roughness detects double-encoding noise
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
When audio is lossy-encoded, quantization noise is shaped to be masked by the
|
|
|
|
|
|
music. Re-encoding adds a *second* layer of noise-shaping, creating
|
2026-07-03 12:15:47 +02:00
|
|
|
|
irregularities in the spectrum that are statistically unlikely in a single
|
|
|
|
|
|
encode.
|
|
|
|
|
|
|
|
|
|
|
|
### 4. Band ratio exploits the Fletcher–Munson curves
|
|
|
|
|
|
|
2026-07-04 09:48:32 +02:00
|
|
|
|
Human hearing is least sensitive above 16 kHz. Lossy encoders exploit this by
|
2026-07-04 19:59:09 +02:00
|
|
|
|
spending almost no bits there. In native recordings, the 16–20 kHz region is
|
|
|
|
|
|
typically only 2–6 dB quieter than 12–16 kHz (band ratio 0.5–1.0). In
|
|
|
|
|
|
transcoded material it is often 10–20 dB quieter (band ratio < 0.3).
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
|
|
|
|
|
### 5. Multiple independent metrics prevent false positives
|
|
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
No single metric is perfectly reliable. A low cutoff could occur in a genuine
|
|
|
|
|
|
recording that used an aggressive lowpass during mastering. By requiring
|
|
|
|
|
|
**both** a low cutoff **and** a sharp roll-off (primary), or **both** high
|
|
|
|
|
|
roughness **and** a low band ratio (secondary), the tool avoids false
|
|
|
|
|
|
positives.
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
|
|
|
|
|
### 6. The upscaling detector is conservative
|
|
|
|
|
|
|
2026-07-04 19:59:09 +02:00
|
|
|
|
For lossy files, an 8-percentage-point margin is subtracted before flagging a
|
|
|
|
|
|
file as upscaled. This accounts for encoder variability and prevents false
|
|
|
|
|
|
positives on legitimate encodes that simply use a conservative lowpass.
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
Usage
|
|
|
|
|
|
-----
|
|
|
|
|
|
|
2026-07-04 20:55:45 +02:00
|
|
|
|
tcd [options] <audio-file>
|
|
|
|
|
|
|
|
|
|
|
|
When run, tcd prints an analysis summary. Example:
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
|
|
|
|
|
```
|
2026-07-04 20:55:45 +02:00
|
|
|
|
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
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Each field is explained in the [What each number means](#what-tcd-displays-and-what-each-number-means) section.
|
|
|
|
|
|
|
|
|
|
|
|
Options:
|
|
|
|
|
|
|
|
|
|
|
|
-t, --threshold PCT Detection sensitivity 1-99 [50]
|
|
|
|
|
|
-f, --fft-size N FFT size, power of 2 [4096]
|
|
|
|
|
|
-d, --duration SEC Seconds to analyze [120]
|
|
|
|
|
|
-r, --recursive Recurse into subdirectories
|
|
|
|
|
|
-F, --full Analyze entire file (no duration limit)
|
|
|
|
|
|
-v, --verbose Verbose output with decision log
|
|
|
|
|
|
-s, --visual Graphical spectrum TUI visualization
|
|
|
|
|
|
-V Alias for -s
|
|
|
|
|
|
-a, --auto-remove Automatically delete detected transcodes
|
|
|
|
|
|
-h, --help Show this help screen
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
|
|
|
|
|
Exit codes:
|
|
|
|
|
|
|
2026-07-04 20:55:45 +02:00
|
|
|
|
| Code | Meaning |
|
|
|
|
|
|
|------|-------------------------------------|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-07-04 20:10:31 +02:00
|
|
|
|
References (freely available)
|
|
|
|
|
|
------------------------------
|
2026-07-03 12:15:47 +02:00
|
|
|
|
|
2026-07-04 20:11:32 +02:00
|
|
|
|
- Nyquist–Shannon sampling theorem - Wikipedia
|
2026-07-04 20:10:31 +02:00
|
|
|
|
https://en.wikipedia.org/wiki/Nyquist–Shannon_sampling_theorem
|
2026-07-04 20:11:32 +02:00
|
|
|
|
- Equal-loudness contour (Fletcher–Munson curves) - Wikipedia
|
2026-07-04 20:10:31 +02:00
|
|
|
|
https://en.wikipedia.org/wiki/Equal-loudness_contour
|
2026-07-04 20:11:32 +02:00
|
|
|
|
- LAME MP3 encoder psychoacoustic model - Hydrogenaudio Knowledge Base
|
2026-07-04 20:10:31 +02:00
|
|
|
|
https://wiki.hydrogenaudio.org/index.php?title=LAME
|
2026-07-04 20:11:32 +02:00
|
|
|
|
- MP3 lowpass filter cutoffs by bitrate - Hydrogenaudio forum discussion
|
2026-07-04 20:10:31 +02:00
|
|
|
|
https://hydrogenaud.io/index.php/topic,71398.0.html
|
2026-07-04 20:11:32 +02:00
|
|
|
|
- GPSYCHO - open-source psychoacoustic model (used by LAME)
|
2026-07-04 20:10:31 +02:00
|
|
|
|
https://lame.sourceforge.io/gpsycho.php
|
2026-07-04 20:11:32 +02:00
|
|
|
|
- LAME source code (lowpass filter implementation) - GitHub
|
2026-07-04 20:10:31 +02:00
|
|
|
|
https://github.com/lameproject/lame
|
2026-07-04 20:11:32 +02:00
|
|
|
|
- "Audio Authentication Using Spectral Analysis" - University of Michigan
|
2026-07-04 20:10:31 +02:00
|
|
|
|
open-access thesis, 2025
|
|
|
|
|
|
https://doi.org/10.7302/28306
|
2026-08-03 00:21:41 +02:00
|
|
|
|
|