add AGENTS.md, update README, fix frame in tcd.c

This commit is contained in:
Armin 2026-07-03 13:52:15 +02:00
commit 3695eab1c5
3 changed files with 163 additions and 26 deletions

14
AGENTS.md Normal file
View file

@ -0,0 +1,14 @@
Implements a command line utility that allows to detect the TRUE quality of your audio material in one batch. Uses psychoacoustics as defined and scientifically proven in:
- ISO/IEC 11172-3:1993 - Coding of moving pictures and associated audio for
digital storage media at up to about 1.5 Mbit/s, Part 3: Audio (MPEG-1
Audio Layer III, "MP3").
- ISO/IEC 13818-7:2006 - Generic coding of moving pictures and associated
audio information, Part 7: Advanced Audio Coding (AAC).
- Zwicker, E. & Fastl, H. - *Psychoacoustics: Facts and Models*, Springer,
1999 (FletcherMunson equal-loudness contours).
- Lerch, A. - *An Introduction to Audio Content Analysis*, Wiley, 2012
(spectral features for audio forensics).
Always implement only what is scientifically provable. As it is possible to view on a spectrum analysis of the audio to see the hard cutoff introduced by compression algorithms as MP3, it is possible to tell if we have a 96k MP3 file that just was re-encoded as 320k. This tool should allow to spot exactly that.

View file

@ -25,8 +25,8 @@ How it works
### 1. Signal acquisition ### 1. Signal acquisition
The program opens the file with libavformat, selects the first audio stream, The program opens the file with libavformat, selects the first audio stream,
decodes up to `--duration` (default 60) seconds of audio, and converts every decodes up to `--duration` (default 60) seconds of audio (or the entire file
sample to 32-bit float PCM. when `--full` is used), and converts every sample to 32-bit float PCM.
### 2. Windowing & FFT ### 2. Windowing & FFT
@ -304,6 +304,8 @@ tcd [options] <audio-file>
higher = more detections. Adjust in small steps. higher = more detections. Adjust in small steps.
-f, --fft-size N FFT size (power of 2) [default: 4096] -f, --fft-size N FFT size (power of 2) [default: 4096]
-d, --duration SEC Max seconds to analyze [default: 60] -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 -v, --verbose Verbose output
-s, --visual Graphical spectrum visualization (TUI) -s, --visual Graphical spectrum visualization (TUI)
-V Alias for -s -V Alias for -s

169
tcd.c
View file

@ -11,6 +11,7 @@
#include <stdint.h> #include <stdint.h>
#include <stdarg.h> #include <stdarg.h>
#include <limits.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
@ -864,26 +865,117 @@ static void render_visual_spectrum(
free(col_max); free(col_max);
} }
static int vis_len(const char *s)
{
int len = 0;
while (*s) {
unsigned char c = (unsigned char)*s;
if (c == '\033') {
while (*s && *s != 'm') s++;
if (*s) s++;
} else if ((c & 0xC0) == 0x80) {
s++;
} else {
len++;
s++;
}
}
return len;
}
static void pn(const char *s) { printf("%s", s); }
static void pl(const char *s, int w)
{
int sl = vis_len(s);
printf(ANSI_BLUE "" ANSI_RESET " %s", s);
int pad = w - 4 - sl;
if (pad > 0) repeat_char(' ', pad);
printf(ANSI_BLUE "" ANSI_RESET "\n");
}
static void print_help(const char *prog) static void print_help(const char *prog)
{ {
printf("Usage: %s [options] [<audio-file> ...]\n\n", prog); int tw = get_term_width();
printf("Detect whether an MP3 or FLAC file is a transcode (lossy->lossless re-encode)\n"); if (tw < 60) tw = 80;
printf("by analyzing the audio spectrum for frequency cutoffs.\n");
printf("If no file is given, processes all audio files in the current directory recursively.\n\n"); pn(ANSI_BLUE ""); repeat_str("", tw - 2); pn(ANSI_BLUE "" ANSI_RESET "\n");
printf("Options:\n");
printf(" -t, --threshold PCT Overall detection sensitivity (1-99). Controls all\n"); pl("", tw);
printf(" decision thresholds: transition bandwidth, roughness,\n"); {
printf(" and band ratio. Maps to -40 dB cutoff threshold (1,\n"); char buf[256];
printf(" least sensitive) through -80 dB (99, most sensitive).\n"); snprintf(buf, sizeof(buf), ANSI_BOLD ANSI_CYAN "tcd" ANSI_RESET
printf(" [default: %d]. 50 is neutral; lower = fewer detections,\n", THRESHOLD_DEFAULT); " \xe2\x80\x94 Transcode Detector "
printf(" higher = more detections. Adjust in small steps.\n"); "Psychoacoustic audio authenticity analysis");
printf(" -f, --fft-size N FFT size (power of 2) [default: %d]\n", FFT_SIZE_DEFAULT); int v = vis_len(buf);
printf(" -d, --duration SEC Max seconds to analyze [default: %d]\n", MAX_ANALYSIS_SECS); printf(ANSI_BLUE "" ANSI_RESET " %s", buf);
printf(" -v, --verbose Verbose output\n"); int pad = tw - 4 - v;
printf(" -s, --visual Graphical spectrum visualization (TUI)\n"); if (pad > 0) repeat_char(' ', pad);
printf(" -V Alias for -s\n"); printf(ANSI_BLUE "" ANSI_RESET "\n");
printf(" -a, --auto-remove Automatically remove non-native files\n"); }
printf(" -h, --help Show this help\n"); pl("", tw);
pl("Analyze audio files to detect transcodes (lossy \xe2\x86\x92 lossless", tw);
pl("re-encodes) by measuring spectral cutoffs and artifacts.", tw);
pl("", tw);
{
char buf[256];
snprintf(buf, sizeof(buf),
ANSI_BOLD "Usage:" ANSI_RESET " %s [options] [<audio-file> ...]", prog);
int v = vis_len(buf);
printf(ANSI_BLUE "" ANSI_RESET " %s", buf);
int pad = tw - 4 - v;
if (pad > 0) repeat_char(' ', pad);
printf(ANSI_BLUE "" ANSI_RESET "\n");
}
pl("", tw);
pn(ANSI_BLUE "" ANSI_RESET " " ANSI_BOLD "Options:" ANSI_RESET);
repeat_char(' ', tw - 12);
pn(ANSI_BLUE "" ANSI_RESET "\n");
#define OPT(fmt, desc) do { \
printf(ANSI_BLUE "" ANSI_RESET " " ANSI_GREEN fmt ANSI_RESET " %s", desc); \
int v = 1 + 4 + (int)strlen(fmt) + 2 + vis_len(desc); \
int pad = tw - 1 - v; \
if (pad > 0) repeat_char(' ', pad); \
printf(ANSI_BLUE "" ANSI_RESET "\n"); \
} while (0)
{
char buf[80];
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);
int v = 1 + 4 + vis_len(buf);
printf(ANSI_BLUE "" ANSI_RESET " %s", buf);
int pad = tw - 1 - v;
if (pad > 0) repeat_char(' ', pad);
printf(ANSI_BLUE "" ANSI_RESET "\n");
}
{
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);
OPT("-d, --duration SEC", buf);
}
OPT("-r, --recursive", "Recurse into subdirectories");
OPT("-F, --full", "Analyze entire file (no duration limit)");
OPT("-v, --verbose", "Verbose output with decision log");
OPT("-s, --visual", "Graphical spectrum TUI visualization");
OPT("-V", "Alias for " ANSI_GREEN "-s" ANSI_RESET);
OPT("-a, --auto-remove", "Automatically delete detected transcodes");
OPT("-h, --help", "Show this help screen");
#undef OPT
pn(ANSI_BLUE ""); repeat_str("", tw - 2); pn(ANSI_BLUE "" ANSI_RESET "\n");
} }
static void maybe_remove(const char *filename, int peak_db_neg90, static void maybe_remove(const char *filename, int peak_db_neg90,
@ -919,6 +1011,8 @@ typedef struct {
int dump_spectrum; int dump_spectrum;
int visual; int visual;
int auto_remove; int auto_remove;
int recursive;
int full;
} Options; } Options;
/* ---- Audio extension check ---- */ /* ---- Audio extension check ---- */
@ -1027,7 +1121,7 @@ static int process_file(const char *filename, const Options *opts)
return 1; return 1;
} }
int max_frames = sample_rate * opts->max_secs; int max_frames = opts->full ? INT_MAX : sample_rate * opts->max_secs;
int total_frames = 0; int total_frames = 0;
float *ring = calloc(opts->fft_size * channels, sizeof(float)); float *ring = calloc(opts->fft_size * channels, sizeof(float));
@ -1243,7 +1337,6 @@ static int process_path(const char *path, const Options *opts)
struct dirent *e; struct dirent *e;
while ((e = readdir(dir))) { while ((e = readdir(dir))) {
if (e->d_name[0] == '.') continue; if (e->d_name[0] == '.') continue;
if (!is_audio_ext(e->d_name)) continue;
size_t nlen = strlen(e->d_name); size_t nlen = strlen(e->d_name);
char *full = malloc(plen + 1 + nlen + 1); char *full = malloc(plen + 1 + nlen + 1);
@ -1252,6 +1345,28 @@ static int process_path(const char *path, const Options *opts)
full[plen] = '/'; full[plen] = '/';
memcpy(full + plen + 1, e->d_name, nlen + 1); memcpy(full + plen + 1, e->d_name, nlen + 1);
struct stat st;
if (stat(full, &st) < 0) {
free(full);
continue;
}
if (S_ISDIR(st.st_mode)) {
if (opts->recursive) {
int rc = process_path(full, opts);
free(full);
if (rc > overall) overall = rc;
} else {
free(full);
}
continue;
}
if (!is_audio_ext(e->d_name)) {
free(full);
continue;
}
int rc = process_path(full, opts); int rc = process_path(full, opts);
free(full); free(full);
if (rc > overall) overall = rc; if (rc > overall) overall = rc;
@ -1278,7 +1393,7 @@ int main(int argc, char **argv)
const char *prog = argv[0]; const char *prog = argv[0];
Options opts = { Options opts = {
.threshold_db = -(double)THRESHOLD_DEFAULT, .threshold_db = -(40.0 + (double)(THRESHOLD_DEFAULT - 1) * 40.0 / 98.0),
.sensitivity = 0.5, .sensitivity = 0.5,
.fft_size = FFT_SIZE_DEFAULT, .fft_size = FFT_SIZE_DEFAULT,
.max_secs = MAX_ANALYSIS_SECS, .max_secs = MAX_ANALYSIS_SECS,
@ -1286,6 +1401,8 @@ int main(int argc, char **argv)
.dump_spectrum = 0, .dump_spectrum = 0,
.visual = 0, .visual = 0,
.auto_remove = 0, .auto_remove = 0,
.recursive = 0,
.full = 0,
}; };
static const struct option long_opts[] = { static const struct option long_opts[] = {
@ -1295,13 +1412,15 @@ int main(int argc, char **argv)
{"verbose", no_argument, NULL, 'v'}, {"verbose", no_argument, NULL, 'v'},
{"spectrum", no_argument, NULL, 's'}, {"spectrum", no_argument, NULL, 's'},
{"visual", no_argument, NULL, 'V'}, {"visual", no_argument, NULL, 'V'},
{"recursive", no_argument, NULL, 'r'},
{"full", no_argument, NULL, 'F'},
{"auto-remove", no_argument, NULL, 'a'}, {"auto-remove", no_argument, NULL, 'a'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0} {NULL, 0, NULL, 0}
}; };
int opt; int opt;
while ((opt = getopt_long(argc, argv, "t:f:d:asvVh", long_opts, NULL)) != -1) { while ((opt = getopt_long(argc, argv, "t:f:d:arFsvVh", long_opts, NULL)) != -1) {
switch (opt) { switch (opt) {
case 't': { case 't': {
int pct = atoi(optarg); int pct = atoi(optarg);
@ -1314,6 +1433,8 @@ int main(int argc, char **argv)
case 'f': opts.fft_size = atoi(optarg); break; case 'f': opts.fft_size = atoi(optarg); break;
case 'd': opts.max_secs = atoi(optarg); break; case 'd': opts.max_secs = atoi(optarg); break;
case 'a': opts.auto_remove = 1; break; case 'a': opts.auto_remove = 1; break;
case 'r': opts.recursive = 1; break;
case 'F': opts.full = 1; break;
case 's': opts.visual = 1; break; case 's': opts.visual = 1; break;
case 'v': opts.verbose = 1; break; case 'v': opts.verbose = 1; break;
case 'V': opts.visual = 1; break; case 'V': opts.visual = 1; break;
@ -1323,8 +1444,8 @@ int main(int argc, char **argv)
} }
if (optind >= argc) { if (optind >= argc) {
argv[optind] = "."; print_help(prog);
argc = optind + 1; return 0;
} }
if (opts.fft_size < 64 || opts.fft_size > 65536 || (opts.fft_size & (opts.fft_size - 1)) != 0) { if (opts.fft_size < 64 || opts.fft_size > 65536 || (opts.fft_size & (opts.fft_size - 1)) != 0) {