diff --git a/README.md b/README.md index bc4d585..9082fd0 100644 --- a/README.md +++ b/README.md @@ -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 + (≈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. --- @@ -268,6 +287,77 @@ If neither layer triggers, the file is **GENUINE** (native lossless). --- +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. + +--- + Confidence score ---------------- @@ -341,33 +431,49 @@ positives on legitimate encodes that simply use a conservative lowpass. Usage ----- -``` -tcd [options] + tcd [options] + +When run, tcd prints an analysis summary. Example: - -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 ``` +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) | +| Code | Meaning | +|------|-------------------------------------| +| 0 | NATIVE or GENUINE (file is clean) | +| 1 | UPSCALED or TRANSCODE detected | +| 2 | SILENT (no detectable content) | --- diff --git a/tcd.c b/tcd.c index ecb27d8..a7092e9 100644 --- a/tcd.c +++ b/tcd.c @@ -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,30 +966,29 @@ 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) { char buf[80]; - snprintf(buf, sizeof(buf), "Detection sensitivity 1-99 [" ANSI_CYAN "%d" ANSI_RESET "]", THRESHOLD_DEFAULT); + 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"); + snprintf(buf, sizeof(buf), "FFT size, power of 2 [" ANSI_CYAN "%d" ANSI_RESET "]", FFT_SIZE_DEFAULT); + OPT("-f, --fft-size N", buf); } { char buf[80]; - snprintf(buf, sizeof(buf), "FFT size, power of 2 [" ANSI_CYAN "%d" ANSI_RESET "]", FFT_SIZE_DEFAULT); - OPT("-f, --fft-size N", buf); - } - { - char buf[80]; - snprintf(buf, sizeof(buf), "Seconds to analyze [" ANSI_CYAN "%d" ANSI_RESET "]", MAX_ANALYSIS_SECS); + snprintf(buf, sizeof(buf), "Seconds to analyze [" ANSI_CYAN "%d" ANSI_RESET "]", MAX_ANALYSIS_SECS); OPT("-d, --duration SEC", buf); } OPT("-r, --recursive", "Recurse into subdirectories");