re-format usage information output, update README accordingly

This commit is contained in:
Armin 2026-07-04 20:55:45 +02:00
commit 545ada8bc3
2 changed files with 146 additions and 42 deletions

152
README.md
View file

@ -21,13 +21,32 @@ please read at least the --help information and *understand* what -a does.
---
How it works (the short version)
--------------------------------
How it works (precise description)
-----------------------------------
tcd decodes your audio file, converts it to the frequency domain (like a
graph showing how much energy exists at each frequency), then measures several
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.
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
(≈1650022050 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 1620 kHz to mean magnitude in
1216 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.
---
@ -268,6 +287,77 @@ If neither layer triggers, the file is **GENUINE** (native lossless).
---
Threshold sensitivity (`-t`)
-----------------------------
The `-t` parameter (199, 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 (140):** reduce false positives on already-filtered material
(e.g. deliberate lowpass during mastering). Rarely needed.
- **Higher values (6099):** 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.
---
Confidence score
----------------
@ -341,30 +431,46 @@ positives on legitimate encodes that simply use a conservative lowpass.
Usage
-----
```
tcd [options] <audio-file>
-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
When run, tcd prints an analysis summary. Example:
```
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
Exit codes:
| Code | Meaning |
|------|-----------------------------------|
|------|-------------------------------------|
| 0 | NATIVE or GENUINE (file is clean) |
| 1 | UPSCALED or TRANSCODE detected |
| 2 | SILENT (no detectable content) |

16
tcd.c
View file

@ -939,8 +939,7 @@ static void print_help(const char *prog)
{
char buf[256];
snprintf(buf, sizeof(buf), ANSI_BOLD ANSI_CYAN "tcd" ANSI_RESET
" \xe2\x80\x94 Transcode Detector "
"Psychoacoustic audio authenticity analysis");
" (Transcode Detector / Psychoacoustic audio authenticity analysis tool)");
int v = vis_len(buf);
printf(" %s", buf);
int pad = tw - 2 - v;
@ -967,8 +966,13 @@ static void print_help(const char *prog)
printf(" " ANSI_BOLD "Options:" ANSI_RESET);
printf("\n");
#define OPT_COL 28
#define OPT(fmt, desc) do { \
printf(" " ANSI_GREEN fmt ANSI_RESET " %s", desc); \
int flen = vis_len(fmt); \
int pad = OPT_COL - flen; \
if (pad < 1) pad = 1; \
printf(" " ANSI_GREEN "%s" ANSI_RESET "%*s%s", fmt, pad, "", desc); \
printf("\n"); \
} while (0)
@ -977,12 +981,6 @@ static void print_help(const char *prog)
snprintf(buf, sizeof(buf), "Detection sensitivity 1-99 [" ANSI_CYAN "%d" ANSI_RESET "]", THRESHOLD_DEFAULT);
OPT("-t, --threshold PCT", buf);
}
{
char buf[80];
snprintf(buf, sizeof(buf), ANSI_DIM " Lower = fewer, higher = more" ANSI_RESET);
printf(" %s", buf);
printf("\n");
}
{
char buf[80];
snprintf(buf, sizeof(buf), "FFT size, power of 2 [" ANSI_CYAN "%d" ANSI_RESET "]", FFT_SIZE_DEFAULT);