mirror of
https://codeberg.org/armin/tcd.git
synced 2026-09-01 05:50:46 +02:00
add AGENTS.md, update README, fix frame in tcd.c
This commit is contained in:
parent
9c844122d4
commit
3695eab1c5
3 changed files with 163 additions and 26 deletions
14
AGENTS.md
Normal file
14
AGENTS.md
Normal 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 (Fletcher–Munson 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.
|
||||||
|
|
||||||
|
|
@ -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
169
tcd.c
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue