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
169
tcd.c
169
tcd.c
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
|
|
@ -864,26 +865,117 @@ static void render_visual_spectrum(
|
|||
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)
|
||||
{
|
||||
printf("Usage: %s [options] [<audio-file> ...]\n\n", prog);
|
||||
printf("Detect whether an MP3 or FLAC file is a transcode (lossy->lossless re-encode)\n");
|
||||
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");
|
||||
printf("Options:\n");
|
||||
printf(" -t, --threshold PCT Overall detection sensitivity (1-99). Controls all\n");
|
||||
printf(" decision thresholds: transition bandwidth, roughness,\n");
|
||||
printf(" and band ratio. Maps to -40 dB cutoff threshold (1,\n");
|
||||
printf(" least sensitive) through -80 dB (99, most sensitive).\n");
|
||||
printf(" [default: %d]. 50 is neutral; lower = fewer detections,\n", THRESHOLD_DEFAULT);
|
||||
printf(" higher = more detections. Adjust in small steps.\n");
|
||||
printf(" -f, --fft-size N FFT size (power of 2) [default: %d]\n", FFT_SIZE_DEFAULT);
|
||||
printf(" -d, --duration SEC Max seconds to analyze [default: %d]\n", MAX_ANALYSIS_SECS);
|
||||
printf(" -v, --verbose Verbose output\n");
|
||||
printf(" -s, --visual Graphical spectrum visualization (TUI)\n");
|
||||
printf(" -V Alias for -s\n");
|
||||
printf(" -a, --auto-remove Automatically remove non-native files\n");
|
||||
printf(" -h, --help Show this help\n");
|
||||
int tw = get_term_width();
|
||||
if (tw < 60) tw = 80;
|
||||
|
||||
pn(ANSI_BLUE "╭"); repeat_str("─", tw - 2); pn(ANSI_BLUE "╮" ANSI_RESET "\n");
|
||||
|
||||
pl("", tw);
|
||||
{
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), ANSI_BOLD ANSI_CYAN "tcd" ANSI_RESET
|
||||
" \xe2\x80\x94 Transcode Detector "
|
||||
"Psychoacoustic audio authenticity analysis");
|
||||
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);
|
||||
|
||||
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,
|
||||
|
|
@ -919,6 +1011,8 @@ typedef struct {
|
|||
int dump_spectrum;
|
||||
int visual;
|
||||
int auto_remove;
|
||||
int recursive;
|
||||
int full;
|
||||
} Options;
|
||||
|
||||
/* ---- Audio extension check ---- */
|
||||
|
|
@ -1027,7 +1121,7 @@ static int process_file(const char *filename, const Options *opts)
|
|||
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;
|
||||
|
||||
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;
|
||||
while ((e = readdir(dir))) {
|
||||
if (e->d_name[0] == '.') continue;
|
||||
if (!is_audio_ext(e->d_name)) continue;
|
||||
|
||||
size_t nlen = strlen(e->d_name);
|
||||
char *full = malloc(plen + 1 + nlen + 1);
|
||||
|
|
@ -1252,6 +1345,28 @@ static int process_path(const char *path, const Options *opts)
|
|||
full[plen] = '/';
|
||||
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);
|
||||
free(full);
|
||||
if (rc > overall) overall = rc;
|
||||
|
|
@ -1278,7 +1393,7 @@ int main(int argc, char **argv)
|
|||
const char *prog = argv[0];
|
||||
|
||||
Options opts = {
|
||||
.threshold_db = -(double)THRESHOLD_DEFAULT,
|
||||
.threshold_db = -(40.0 + (double)(THRESHOLD_DEFAULT - 1) * 40.0 / 98.0),
|
||||
.sensitivity = 0.5,
|
||||
.fft_size = FFT_SIZE_DEFAULT,
|
||||
.max_secs = MAX_ANALYSIS_SECS,
|
||||
|
|
@ -1286,6 +1401,8 @@ int main(int argc, char **argv)
|
|||
.dump_spectrum = 0,
|
||||
.visual = 0,
|
||||
.auto_remove = 0,
|
||||
.recursive = 0,
|
||||
.full = 0,
|
||||
};
|
||||
|
||||
static const struct option long_opts[] = {
|
||||
|
|
@ -1295,13 +1412,15 @@ int main(int argc, char **argv)
|
|||
{"verbose", no_argument, NULL, 'v'},
|
||||
{"spectrum", no_argument, NULL, 's'},
|
||||
{"visual", no_argument, NULL, 'V'},
|
||||
{"recursive", no_argument, NULL, 'r'},
|
||||
{"full", no_argument, NULL, 'F'},
|
||||
{"auto-remove", no_argument, NULL, 'a'},
|
||||
{"help", no_argument, NULL, 'h'},
|
||||
{NULL, 0, NULL, 0}
|
||||
};
|
||||
|
||||
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) {
|
||||
case 't': {
|
||||
int pct = atoi(optarg);
|
||||
|
|
@ -1314,6 +1433,8 @@ int main(int argc, char **argv)
|
|||
case 'f': opts.fft_size = atoi(optarg); break;
|
||||
case 'd': opts.max_secs = atoi(optarg); 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 'v': opts.verbose = 1; break;
|
||||
case 'V': opts.visual = 1; break;
|
||||
|
|
@ -1323,8 +1444,8 @@ int main(int argc, char **argv)
|
|||
}
|
||||
|
||||
if (optind >= argc) {
|
||||
argv[optind] = ".";
|
||||
argc = optind + 1;
|
||||
print_help(prog);
|
||||
return 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