tcd/tcd.c

1426 lines
49 KiB
C
Raw Normal View History

2026-07-03 12:15:47 +02:00
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <getopt.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>
#include <stdint.h>
#include <stdarg.h>
#include <limits.h>
2026-07-03 12:15:47 +02:00
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/log.h>
#include <libavutil/channel_layout.h>
#include <libavutil/samplefmt.h>
#include <libavutil/frame.h>
#define FFT_SIZE_DEFAULT 4096
#define THRESHOLD_DEFAULT 50
#define MAX_ANALYSIS_SECS 60
#define OVERLAP_FACTOR 2
#define ANSI_RESET "\033[0m"
#define ANSI_BOLD "\033[1m"
#define ANSI_DIM "\033[2m"
#define ANSI_RED "\033[31m"
#define ANSI_GREEN "\033[32m"
#define ANSI_YELLOW "\033[33m"
#define ANSI_BLUE "\033[34m"
#define ANSI_MAGENTA "\033[35m"
#define ANSI_CYAN "\033[36m"
#define ANSI_BRIGHT_RED "\033[91m"
#define ANSI_BG_GREY "\033[48;5;235m"
typedef struct {
double *power;
int count;
int sample_rate;
int channels;
int fft_size;
} Analyzer;
static void analyzer_init(Analyzer *a, int sample_rate, int channels, int fft_size)
{
a->power = calloc(fft_size / 2, sizeof(double));
a->count = 0;
a->sample_rate = sample_rate;
a->channels = channels;
a->fft_size = fft_size;
}
static void analyzer_free(Analyzer *a)
{
free(a->power);
a->power = NULL;
}
static void apply_hann(double *buf, int n)
{
for (int i = 0; i < n; i++)
buf[i] *= 0.5 * (1.0 - cos(2.0 * M_PI * i / (n - 1)));
}
static void fft_radix2(double *re, double *im, int n, int inv)
{
for (int i = 1, j = 0; i < n; i++) {
int bit = n >> 1;
for (; j & bit; bit >>= 1)
j ^= bit;
j ^= bit;
if (i < j) {
double tr = re[i]; re[i] = re[j]; re[j] = tr;
double ti = im[i]; im[i] = im[j]; im[j] = ti;
}
}
for (int len = 2; len <= n; len <<= 1) {
double ang = 2.0 * M_PI / len * (inv ? -1 : 1);
double wr = cos(ang), wi = sin(ang);
for (int i = 0; i < n; i += len) {
double cr = 1.0, ci = 0.0;
for (int j = 0; j < len / 2; j++) {
int a = i + j, b = a + len / 2;
double tr = cr * re[b] - ci * im[b];
double ti = cr * im[b] + ci * re[b];
re[b] = re[a] - tr; im[b] = im[a] - ti;
re[a] += tr; im[a] += ti;
double ncr = cr * wr - ci * wi;
double nci = cr * wi + ci * wr;
cr = ncr; ci = nci;
}
}
}
if (inv)
for (int i = 0; i < n; i++) { re[i] /= n; im[i] /= n; }
}
static void analyzer_add_window(Analyzer *a, const float *frame, int n)
{
if (n < a->fft_size) return;
double *buf = malloc(a->fft_size * sizeof(double));
double *re = malloc(a->fft_size * sizeof(double));
double *im = malloc(a->fft_size * sizeof(double));
if (!buf || !re || !im) { free(buf); free(re); free(im); return; }
for (int ch = 0; ch < a->channels; ch++) {
for (int pos = 0; pos + a->fft_size <= n; pos += a->fft_size / OVERLAP_FACTOR) {
for (int i = 0; i < a->fft_size; i++)
buf[i] = frame[(pos + i) * a->channels + ch];
apply_hann(buf, a->fft_size);
memcpy(re, buf, a->fft_size * sizeof(double));
memset(im, 0, a->fft_size * sizeof(double));
fft_radix2(re, im, a->fft_size, 0);
for (int i = 0; i < a->fft_size / 2; i++)
a->power[i] += re[i]*re[i] + im[i]*im[i];
a->count++;
}
}
free(buf); free(re); free(im);
}
static void frame_to_ring(AVFrame *f, int nf, int channels, int fft_size,
float *ring, int *ring_pos, Analyzer *analyzer)
{
int planar = av_sample_fmt_is_planar(f->format);
int bps = av_get_bytes_per_sample(f->format);
enum AVSampleFormat fmt = f->format;
for (int i = 0; i < nf; i++) {
for (int ch = 0; ch < channels; ch++) {
const uint8_t *src;
if (planar)
src = f->extended_data[ch] + i * bps;
else
src = f->data[0] + (i * channels + ch) * bps;
double val;
switch (fmt) {
case AV_SAMPLE_FMT_U8: case AV_SAMPLE_FMT_U8P:
val = (*src - 128) / 128.0; break;
case AV_SAMPLE_FMT_S16: case AV_SAMPLE_FMT_S16P:
val = *(const int16_t *)src / 32768.0; break;
case AV_SAMPLE_FMT_S32: case AV_SAMPLE_FMT_S32P:
val = *(const int32_t *)src / 2147483648.0; break;
case AV_SAMPLE_FMT_FLT: case AV_SAMPLE_FMT_FLTP:
val = *(const float *)src; break;
case AV_SAMPLE_FMT_DBL: case AV_SAMPLE_FMT_DBLP:
val = *(const double *)src; break;
default:
val = 0.0; break;
}
ring[*ring_pos * channels + ch] = (float)val;
}
(*ring_pos)++;
if (*ring_pos >= fft_size) {
analyzer_add_window(analyzer, ring, *ring_pos);
int slide = fft_size / 2;
memmove(ring, ring + slide * channels,
(*ring_pos - slide) * channels * sizeof(float));
*ring_pos -= slide;
}
}
}
static int detect_cutoff(const Analyzer *a, double threshold_db,
double sensitivity,
double *out_cutoff, double *out_steepness,
double *out_noise_db, double *out_roughness,
2026-07-03 23:00:42 +02:00
double *out_band_ratio,
double *out_extended_cutoff)
2026-07-03 12:15:47 +02:00
{
int n = a->fft_size / 2;
int sr = a->sample_rate;
double *mag = malloc(n * sizeof(double));
if (!mag) return -1;
double peak = 0.0;
for (int i = 0; i < n; i++) {
mag[i] = sqrt(a->power[i] / a->count);
if (mag[i] > peak) peak = mag[i];
}
if (peak < 1e-12) { free(mag); return 0; }
double threshold = peak * pow(10.0, threshold_db / 20.0);
double cutoff_hz = 0;
for (int i = n - 1; i >= 0; i--) {
if (mag[i] >= threshold) { cutoff_hz = (double)i * sr / a->fft_size; break; }
}
*out_cutoff = cutoff_hz;
/* Always compute transition bandwidth from -20 dB to -60 dB,
independent of the user's threshold. This keeps the steepness
measurement consistent regardless of sensitivity setting. */
double low_thresh_60 = peak * 0.001;
double cutoff_60_hz = 0;
int cutoff_60_bin = n - 1;
for (int i = n - 1; i >= 0; i--) {
if (mag[i] >= low_thresh_60) { cutoff_60_hz = (double)i * sr / a->fft_size; cutoff_60_bin = i; break; }
}
double high_thresh = peak * 0.1;
double cutoff_high_hz = 0;
int start = cutoff_60_bin > 0 ? cutoff_60_bin : n - 1;
for (int i = start; i >= 0; i--) {
if (mag[i] >= high_thresh) { cutoff_high_hz = (double)i * sr / a->fft_size; break; }
}
double transition_bw = (cutoff_60_hz > 0) ? cutoff_60_hz - cutoff_high_hz : 0;
if (transition_bw < 0) transition_bw = 0;
*out_steepness = transition_bw;
double noise_sum = 0; int noise_count = 0;
for (int i = n * 3 / 4; i < n; i++) {
if (mag[i] > 0) { noise_sum += mag[i]; noise_count++; }
}
double noise_floor = (noise_count > 0) ? (noise_sum / noise_count) : 1e-12;
*out_noise_db = 20.0 * log10(noise_floor / peak);
2026-07-03 23:00:42 +02:00
/* Compute extended cutoff at noise floor + 6 dB margin.
Catches high-frequency content below the user's threshold but
above the noise floor, proving the file isn't a low-bitrate
re-encode even when high frequencies are very quiet.
Limit to at most 30 dB more sensitive than user threshold. */
double ext_db = *out_noise_db + 6.0;
double min_ext_db = threshold_db - 30.0;
if (ext_db < min_ext_db) ext_db = min_ext_db;
double ext_peak = peak * pow(10.0, ext_db / 20.0);
double ext_cutoff = 0;
for (int i = n - 1; i >= 0; i--) {
if (mag[i] >= ext_peak) { ext_cutoff = (double)i * sr / a->fft_size; break; }
}
*out_extended_cutoff = ext_cutoff;
2026-07-03 12:15:47 +02:00
double roughness = 0.0;
double cutoff_idx = cutoff_hz * a->fft_size / sr;
int lo = (int)(cutoff_idx * 0.60);
int hi = (int)(cutoff_idx * 0.95);
if (hi >= n) hi = n - 1;
if (lo < 1) lo = 1;
if (hi > lo) {
double sum = 0;
for (int i = lo; i <= hi; i++) sum += mag[i];
double mean = sum / (hi - lo + 1);
if (mean > 1e-12) {
double var = 0;
for (int i = lo; i <= hi; i++) {
double dev = (mag[i] - mean) / mean;
var += dev * dev;
}
roughness = sqrt(var / (hi - lo));
}
}
if (roughness < 0.01) roughness = 0.01;
*out_roughness = roughness;
double energy_low = 0, energy_high = 0;
int el_count = 0, eh_count = 0;
for (int i = 0; i < n; i++) {
double f = (double)i * sr / a->fft_size;
if (f >= 12000 && f < 16000) { energy_low += mag[i]; el_count++; }
if (f >= 16000 && f < 20000) { energy_high += mag[i]; eh_count++; }
}
double band_ratio = (el_count > 0 && eh_count > 0)
? (energy_high / eh_count) / (energy_low / el_count + 1e-12)
: 0.5;
*out_band_ratio = band_ratio;
free(mag);
double nyquist = sr / 2.0;
double cutoff_ratio = cutoff_hz / nyquist;
/* Scale detection thresholds by sensitivity (0.0 = least, 0.5 = default, 1.0 = most) */
double bw_factor = 2.0 * (1.0 - sensitivity);
if (bw_factor < 0.25) bw_factor = 0.25;
double r1 = 0.40 * (1.0 + (0.5 - sensitivity) * 1.5);
double r2 = 0.30 * (1.0 + (0.5 - sensitivity) * 1.5);
double r3 = 0.20 * (1.0 + (0.5 - sensitivity) * 1.5);
double b1 = 0.90 - (0.5 - sensitivity) * 0.10;
double b2 = 0.85 - (0.5 - sensitivity) * 0.10;
double bypass = 0.99 - (0.5 - sensitivity) * 0.02;
int score = 0;
if (cutoff_hz <= 0 || cutoff_ratio >= bypass) {
if (roughness > r1) score = 1;
else if (roughness > r2 && band_ratio < b1) score = 1;
else if (roughness > r3 && band_ratio < b2) score = 1;
return score;
}
double max_bw;
if (cutoff_ratio < 0.50) {
max_bw = 4000.0 * bw_factor;
} else if (cutoff_ratio < 0.70) {
max_bw = 3000.0 * bw_factor;
} else if (cutoff_ratio < 0.80) {
max_bw = 2000.0 * bw_factor;
} else if (cutoff_ratio < 0.90) {
max_bw = 1200.0 * bw_factor;
} else {
max_bw = 500.0 * bw_factor;
}
if (transition_bw < max_bw) score = 1;
if (!score && cutoff_ratio > 0.80) {
if (roughness > r1) score = 1;
else if (roughness > r2 && band_ratio < b1) score = 1;
else if (roughness > r3 && band_ratio < b2) score = 1;
}
return score;
}
/* ---- Terminal utilities ---- */
static int get_term_width(void)
{
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0)
return ws.ws_col;
char *cols = getenv("COLUMNS");
if (cols) {
int n = atoi(cols);
if (n > 0) return n;
}
return 80;
}
/* ---- Confidence / validity computation ---- */
static double compute_confidence(double cutoff_ratio, double steepness,
double roughness, double band_ratio,
int is_native_lossy)
{
double conf = 0;
int count = 0;
if (is_native_lossy) {
if (cutoff_ratio < 0.70) {
double margin = (0.70 - cutoff_ratio) / 0.70;
if (margin > 1) margin = 1;
conf += 0.50 + 0.50 * margin;
count++;
} else if (cutoff_ratio < 0.80) {
double margin = (0.80 - cutoff_ratio) / 0.80;
if (margin > 1) margin = 1;
conf += 0.40 + 0.60 * margin;
count++;
} else if (cutoff_ratio < 0.85) {
double margin = (0.85 - cutoff_ratio) / 0.85;
if (margin > 1) margin = 1;
conf += 0.20 + 0.60 * margin;
count++;
} else {
conf += 0.70;
count++;
}
} else {
if (cutoff_ratio < 0.50) {
double margin = (0.50 - cutoff_ratio) / 0.50;
if (margin > 1) margin = 1;
double s_margin = (4000.0 - steepness) / 4000.0;
if (s_margin > 1) s_margin = 1;
if (s_margin < 0) s_margin = 0;
conf += 0.50 + 0.50 * (margin * 0.5 + s_margin * 0.5);
count++;
} else if (cutoff_ratio < 0.70) {
double r_margin = (0.70 - cutoff_ratio) / 0.70;
if (r_margin > 1) r_margin = 1;
double s_margin = (3000.0 - steepness) / 3000.0;
if (s_margin > 1) s_margin = 1;
if (s_margin < 0) s_margin = 0;
conf += 0.30 + 0.70 * (r_margin * 0.4 + s_margin * 0.6);
count++;
} else if (cutoff_ratio < 0.80) {
double r_margin = (0.80 - cutoff_ratio) / 0.80;
if (r_margin > 1) r_margin = 1;
double s_margin = (2000.0 - steepness) / 2000.0;
if (s_margin > 1) s_margin = 1;
if (s_margin < 0) s_margin = 0;
conf += 0.20 + 0.80 * (r_margin * 0.4 + s_margin * 0.6);
count++;
} else if (cutoff_ratio < 0.90) {
double r_margin = (0.90 - cutoff_ratio) / 0.90;
if (r_margin > 1) r_margin = 1;
double s_margin = (1200.0 - steepness) / 1200.0;
if (s_margin > 1) s_margin = 1;
if (s_margin < 0) s_margin = 0;
conf += 0.10 + 0.90 * (r_margin * 0.4 + s_margin * 0.6);
count++;
} else {
double s_margin = (500.0 - steepness) / 500.0;
if (s_margin > 1) s_margin = 1;
if (s_margin < 0) s_margin = 0;
conf += 0.20 + 0.80 * s_margin;
count++;
}
if (cutoff_ratio > 0.80) {
if (roughness > 0.40) {
double margin = (roughness - 0.40) / 0.40;
if (margin > 1) margin = 1;
conf += 0.40 + 0.60 * margin;
count++;
} else if (roughness > 0.30 && band_ratio < 0.90) {
double r_margin = (roughness - 0.30) / 0.10;
if (r_margin > 1) r_margin = 1;
double b_margin = (0.90 - band_ratio) / 0.90;
if (b_margin > 1) b_margin = 1;
conf += 0.20 + 0.80 * (r_margin * 0.5 + b_margin * 0.5);
count++;
} else if (roughness > 0.20 && band_ratio < 0.85) {
double r_margin = (roughness - 0.20) / 0.10;
if (r_margin > 1) r_margin = 1;
double b_margin = (0.85 - band_ratio) / 0.85;
if (b_margin > 1) b_margin = 1;
conf += 0.10 + 0.90 * (r_margin * 0.5 + b_margin * 0.5);
count++;
}
}
}
if (count == 0) return 0;
double result = conf / count * 100.0;
if (result < 0) result = 0;
if (result > 100) result = 100;
return result;
}
/* ---- Visual spectrum renderer ---- */
static void repeat_char(char c, int n)
{
for (int i = 0; i < n; i++) putchar(c);
}
static void repeat_str(const char *s, int n)
{
for (int i = 0; i < n; i++) printf("%s", s);
}
static void draw_hline(int width)
{
printf(ANSI_BLUE); repeat_str("", width); printf(ANSI_RESET);
printf("\n");
}
static void freq_label_row(int chart_w, double nyquist)
{
static const double freqs[] = {20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 15000, 20000};
static const char *labels[] = {"20", "50", "100", "200", "500", "1k", "2k", "5k", "10k", "15k", "20k"};
int n = sizeof(freqs) / sizeof(freqs[0]);
int pos[11], n_valid = 0;
for (int i = 0; i < n; i++) {
if (freqs[i] < nyquist) {
pos[n_valid] = (int)(freqs[i] / nyquist * chart_w);
if (pos[n_valid] >= chart_w) pos[n_valid] = chart_w - 1;
if (pos[n_valid] < 0) pos[n_valid] = 0;
n_valid++;
}
}
int min_gap = 5;
int keep[11], n_keep = 0;
int last_pos = -100;
for (int i = 0; i < n_valid; i++) {
if (pos[i] - last_pos >= min_gap) {
keep[n_keep] = i;
last_pos = pos[i];
n_keep++;
}
}
int p = 0;
for (int i = 0; i < chart_w; i++) {
if (p < n_keep && i == pos[keep[p]]) {
printf(ANSI_BLUE "" ANSI_RESET);
p++;
} else {
putchar(' ');
}
}
printf("\n");
p = 0;
for (int i = 0; i < chart_w; i++) {
if (p < n_keep && i == pos[keep[p]]) {
const char *label = labels[keep[p]];
int llen = strlen(label);
printf(ANSI_BOLD "%s" ANSI_RESET, label);
i += llen - 1;
p++;
} else {
putchar(' ');
}
}
printf("\n");
}
static void render_visual_spectrum(
const Analyzer *a,
2026-07-03 23:00:42 +02:00
double cutoff_hz, double effective_cutoff_hz,
double steepness, double noise_db,
2026-07-03 12:15:47 +02:00
double roughness, double band_ratio,
int is_transcode, int is_native_lossy, int upscaled,
double peak_db, double threshold_db,
const char *filename, const char *fmt_name,
int sample_rate, int channels, int analyzer_count,
int fft_size, int64_t bitrate)
{
(void)threshold_db;
int term_w = get_term_width();
if (term_w < 60) term_w = 60;
int n_bins = fft_size / 2;
double nyquist = sample_rate / 2.0;
int label_w = 7;
2026-07-03 20:52:11 +02:00
int chart_w = term_w - label_w;
2026-07-03 12:15:47 +02:00
if (chart_w < 20) chart_w = 20;
double *spec = malloc(n_bins * sizeof(double));
if (!spec) { fprintf(stderr, "Error: malloc failed\n"); return; }
double peak = 0;
for (int i = 0; i < n_bins; i++) {
double m = sqrt(a->power[i] / a->count);
if (m > peak) peak = m;
}
if (peak < 1e-12) peak = 1e-12;
for (int i = 0; i < n_bins; i++) {
double m = sqrt(a->power[i] / a->count);
spec[i] = 20.0 * log10(m / peak);
}
double *col_max = calloc(chart_w, sizeof(double));
if (!col_max) { free(spec); return; }
for (int c = 0; c < chart_w; c++) col_max[c] = -200.0;
for (int i = 0; i < n_bins; i++) {
int c = (i * chart_w) / n_bins;
if (c >= chart_w) c = chart_w - 1;
if (spec[i] > col_max[c]) col_max[c] = spec[i];
}
int cutoff_col = (cutoff_hz > 0 && nyquist > 0)
? (int)(cutoff_hz / nyquist * chart_w) : chart_w - 1;
if (cutoff_col < 0) cutoff_col = 0;
if (cutoff_col >= chart_w) cutoff_col = chart_w - 1;
double db_min = -100.0;
double db_max = 4.0;
int n_rows = 8;
double db_step = (db_max - db_min) / n_rows;
/* --- Title bar --- */
char title[256];
const char *short_name = strrchr(filename, '/');
short_name = short_name ? short_name + 1 : filename;
int info_len = snprintf(title, sizeof(title), " %s | %s | %d Hz | %d ch",
short_name, fmt_name, sample_rate, channels);
2026-07-03 20:52:11 +02:00
int total_w = term_w;
2026-07-03 12:15:47 +02:00
if (info_len > total_w) {
char truncated[256];
snprintf(truncated, sizeof(truncated), " %s | %d Hz | %d ch",
short_name, sample_rate, channels);
if ((int)strlen(truncated) > total_w) {
snprintf(truncated, sizeof(truncated), " %s | %d ch", short_name, channels);
if ((int)strlen(truncated) > total_w) {
snprintf(truncated, sizeof(truncated), " %s", short_name);
if ((int)strlen(truncated) > total_w) {
truncated[total_w] = '\0';
}
}
}
printf(ANSI_BOLD ANSI_CYAN "%s" ANSI_RESET, truncated);
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
} else {
printf(ANSI_BOLD ANSI_CYAN "%s" ANSI_RESET, title);
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
}
draw_hline(chart_w + label_w);
/* ============================ */
/* SPECTRUM CHART */
/* ============================ */
for (int row = 0; row < n_rows; row++) {
double db_val = db_max - row * db_step;
printf(ANSI_DIM "%6.0f " ANSI_RESET, db_val);
for (int c = 0; c < chart_w; c++) {
double v = col_max[c];
double thresh_high = db_val;
double thresh_mid = db_val - db_step;
double thresh_low = db_val - 2 * db_step;
if (v >= thresh_high) {
printf("");
} else if (v >= thresh_mid) {
printf("");
} else if (v >= thresh_low) {
printf("");
} else {
printf("");
}
}
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
}
/* --- Cutoff arrow row --- */
repeat_char(' ', label_w);
for (int c = 0; c < chart_w; c++) {
if (c == cutoff_col) {
printf(ANSI_YELLOW "" ANSI_RESET);
} else {
putchar(' ');
}
}
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
/* --- Cutoff label --- */
char cutoff_label[64];
if (cutoff_hz >= 1000)
snprintf(cutoff_label, sizeof(cutoff_label), "Cutoff %.1fkHz (%.0f%% Nyq)",
cutoff_hz / 1000.0, 100.0 * cutoff_hz / nyquist);
else
snprintf(cutoff_label, sizeof(cutoff_label), "Cutoff %.0fHz (%.0f%% Nyq)",
cutoff_hz, 100.0 * cutoff_hz / nyquist);
int clen = strlen(cutoff_label);
int label_start = cutoff_col - clen / 2;
if (label_start < 0) label_start = 0;
if (label_start + clen > chart_w) label_start = chart_w - clen;
repeat_char(' ', label_w);
repeat_char(' ', label_start);
printf(ANSI_YELLOW ANSI_BOLD "%s" ANSI_RESET, cutoff_label);
int remain = chart_w - label_start - clen;
if (remain > 0) repeat_char(' ', remain);
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
/* --- Tick marks --- */
freq_label_row(chart_w, nyquist);
/* ============================ */
/* METRICS SECTION */
/* ============================ */
draw_hline(chart_w + label_w);
char peak_dbfs_str[32];
snprintf(peak_dbfs_str, sizeof(peak_dbfs_str), "%.1f", 20.0 * log10(peak));
2026-07-03 20:52:11 +02:00
printf(" " ANSI_BOLD "Peak:" ANSI_RESET " %7s dBFS " ANSI_BLUE "" ANSI_RESET " " ANSI_BOLD "Windows:" ANSI_RESET " %d " ANSI_BLUE "" ANSI_RESET " " ANSI_BOLD "FFT:" ANSI_RESET " %d",
2026-07-03 12:15:47 +02:00
peak_dbfs_str, analyzer_count, fft_size);
int used_metrics = 30 + 14 + 10 + 14;
repeat_char(' ', term_w - 2 - used_metrics);
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
draw_hline(chart_w + label_w);
2026-07-03 20:52:11 +02:00
printf(" " ANSI_BOLD "Cutoff:" ANSI_RESET " %7.0f Hz (%5.1f%%) " ANSI_BLUE "" ANSI_RESET " " ANSI_BOLD "Steepness:" ANSI_RESET " %6.0f Hz " ANSI_BLUE "" ANSI_RESET " " ANSI_BOLD "Noise:" ANSI_RESET " %6.1f dB",
2026-07-03 12:15:47 +02:00
cutoff_hz, 100.0 * cutoff_hz / nyquist, steepness, noise_db);
repeat_char(' ', term_w - 2 - 62);
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
2026-07-03 20:52:11 +02:00
printf(" " ANSI_BOLD "Roughness:" ANSI_RESET " %6.3f " ANSI_BLUE "" ANSI_RESET " " ANSI_BOLD "Band ratio:" ANSI_RESET " %6.3f",
2026-07-03 12:15:47 +02:00
roughness, band_ratio);
repeat_char(' ', term_w - 2 - 36);
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
/* ============================ */
/* DECISION / VALIDITY */
/* ============================ */
draw_hline(chart_w + label_w);
double cutoff_ratio = cutoff_hz / nyquist;
2026-07-03 23:00:42 +02:00
double eff_cutoff_ratio = effective_cutoff_hz / nyquist;
2026-07-03 12:15:47 +02:00
double confidence = compute_confidence(
2026-07-03 23:00:42 +02:00
is_native_lossy ? eff_cutoff_ratio : cutoff_ratio,
steepness, roughness, band_ratio, is_native_lossy);
2026-07-03 12:15:47 +02:00
2026-07-03 20:52:11 +02:00
printf(" " ANSI_BOLD "Decision:" ANSI_RESET " ");
2026-07-03 12:15:47 +02:00
if (peak_db < -90.0) {
printf(ANSI_RED "SILENT" ANSI_RESET " (no detectable audio content)");
} else if (is_native_lossy) {
if (upscaled)
printf(ANSI_YELLOW "UPSCALED" ANSI_RESET " (re-encoded from lower bitrate)");
else
printf(ANSI_GREEN "NATIVE" ANSI_RESET " (single encode at this bitrate)");
} else {
if (is_transcode)
printf(ANSI_YELLOW "TRANSCODE" ANSI_RESET " (lossy \xe2\x86\x92 lossless re-encode)");
else
printf(ANSI_GREEN "GENUINE" ANSI_RESET " (likely native lossless)");
}
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
/* Validity bar */
int bar_w = 20;
int filled = (int)(confidence / 100.0 * bar_w);
if (filled < 0) filled = 0;
if (filled > bar_w) filled = bar_w;
const char *conf_label;
if (confidence >= 85) conf_label = "Very strong evidence";
else if (confidence >= 70) conf_label = "Strong evidence";
else if (confidence >= 50) conf_label = "Moderate evidence";
else if (confidence >= 30) conf_label = "Weak evidence";
else conf_label = "Borderline / inconclusive";
2026-07-03 20:52:11 +02:00
printf(" " ANSI_BOLD "Validity:" ANSI_RESET " ");
2026-07-03 12:15:47 +02:00
for (int i = 0; i < bar_w; i++) {
if (i < filled) printf(ANSI_GREEN "" ANSI_RESET);
else printf(ANSI_DIM "" ANSI_RESET);
}
printf(ANSI_BOLD " %3.0f%%" ANSI_RESET, confidence);
printf(" \xe2\x80\x94 %s", conf_label);
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
/* --- Factor breakdown --- */
2026-07-03 20:52:11 +02:00
printf(" " ANSI_BOLD "Factors:" ANSI_RESET);
printf("\n");
2026-07-03 12:15:47 +02:00
if (is_native_lossy) {
if (upscaled) {
char line[128];
snprintf(line, sizeof(line),
" cutoff_ratio=%.3f < expected for bitrate (%lld kbps)",
2026-07-03 23:00:42 +02:00
eff_cutoff_ratio, (long long)(bitrate / 1000));
2026-07-03 20:52:11 +02:00
printf(" %s " ANSI_GREEN "" ANSI_RESET, line);
printf("\n");
2026-07-03 12:15:47 +02:00
const char *src_hint = "";
2026-07-03 23:00:42 +02:00
if (eff_cutoff_ratio < 0.70) src_hint = "≤ 64 kbps source";
else if (eff_cutoff_ratio < 0.80) src_hint = "96128 kbps source";
else if (eff_cutoff_ratio < 0.88) src_hint = "128192 kbps source";
2026-07-03 20:52:11 +02:00
printf("" ANSI_BOLD "Suggest %s" ANSI_RESET, src_hint);
printf("\n");
2026-07-03 12:15:47 +02:00
} else {
2026-07-03 23:00:42 +02:00
printf(" cutoff_ratio=%.3f within expected range for this bitrate " ANSI_GREEN "" ANSI_RESET, eff_cutoff_ratio);
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
}
} else {
double max_bw_display;
if (cutoff_ratio < 0.50) {
max_bw_display = 4000.0;
} else if (cutoff_ratio < 0.70) {
max_bw_display = 3000.0;
} else if (cutoff_ratio < 0.80) {
max_bw_display = 2000.0;
} else if (cutoff_ratio < 0.90) {
max_bw_display = 1200.0;
} else {
max_bw_display = 500.0;
}
int primary_hit = (cutoff_ratio >= 0.99) ? 0 : (steepness < max_bw_display);
int secondary_applies = (cutoff_ratio > 0.80);
if (cutoff_ratio >= 0.99) {
2026-07-03 20:52:11 +02:00
printf(" ① cutoff_ratio=%.3f ≥ 0.99 " ANSI_RED "" ANSI_RESET " (full spectrum, no cutoff)", cutoff_ratio);
printf("\n");
2026-07-03 12:15:47 +02:00
} else {
char bw_label[64];
snprintf(bw_label, sizeof(bw_label), "transition_bw=%.0fHz < %.0fHz threshold",
steepness, max_bw_display);
if (primary_hit) {
2026-07-03 20:52:11 +02:00
printf(" ① cutoff_ratio=%.3f, %s " ANSI_GREEN "" ANSI_RESET,
2026-07-03 12:15:47 +02:00
cutoff_ratio, bw_label);
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
} else {
2026-07-03 20:52:11 +02:00
printf(" ① cutoff_ratio=%.3f, transition_bw=%.0fHz ≥ %.0fHz threshold " ANSI_RED "" ANSI_RESET,
2026-07-03 12:15:47 +02:00
cutoff_ratio, steepness, max_bw_display);
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
}
}
if (primary_hit) {
2026-07-03 20:52:11 +02:00
printf("" ANSI_YELLOW "Transition bandwidth indicates transcode" ANSI_RESET);
printf("\n");
2026-07-03 12:15:47 +02:00
}
if (secondary_applies) {
int sec_hit = 0;
if (roughness > 0.40) {
2026-07-03 20:52:11 +02:00
printf(" ② roughness=%.3f > 0.40 " ANSI_GREEN "" ANSI_RESET " (high roughness → transcode)", roughness);
printf("\n");
2026-07-03 12:15:47 +02:00
sec_hit = 1;
} else if (roughness > 0.30 && band_ratio < 0.90) {
2026-07-03 20:52:11 +02:00
printf(" ② roughness=%.3f > 0.30 " ANSI_GREEN "" ANSI_RESET " band_ratio=%.3f < 0.90 " ANSI_GREEN "" ANSI_RESET,
2026-07-03 12:15:47 +02:00
roughness, band_ratio);
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
sec_hit = 1;
} else if (roughness > 0.20 && band_ratio < 0.85) {
2026-07-03 20:52:11 +02:00
printf(" ② roughness=%.3f > 0.20 " ANSI_GREEN "" ANSI_RESET " band_ratio=%.3f < 0.85 " ANSI_GREEN "" ANSI_RESET,
2026-07-03 12:15:47 +02:00
roughness, band_ratio);
2026-07-03 20:52:11 +02:00
printf("\n");
2026-07-03 12:15:47 +02:00
sec_hit = 1;
}
if (sec_hit) {
2026-07-03 20:52:11 +02:00
printf("" ANSI_YELLOW "Secondary criteria triggered: transcode confirmed" ANSI_RESET);
printf("\n");
2026-07-03 12:15:47 +02:00
} else if (!primary_hit) {
2026-07-03 20:52:11 +02:00
printf(" ② Secondary: roughness=%.3f, band_ratio=%.3f " ANSI_RED "" ANSI_RESET,
2026-07-03 12:15:47 +02:00
roughness, band_ratio);
2026-07-03 20:52:11 +02:00
printf("\n");
printf("" ANSI_GREEN "No transcode criteria met: genuine lossless" ANSI_RESET);
printf("\n");
2026-07-03 12:15:47 +02:00
}
} else if (!primary_hit) {
2026-07-03 20:52:11 +02:00
printf("" ANSI_GREEN "No transcode criteria met: genuine lossless" ANSI_RESET);
printf("\n");
2026-07-03 12:15:47 +02:00
}
}
free(spec);
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 pl(const char *s, int w)
{
int sl = vis_len(s);
2026-07-03 20:52:11 +02:00
printf(" %s", s);
int pad = w - 2 - sl;
if (pad > 0) repeat_char(' ', pad);
2026-07-03 20:52:11 +02:00
printf("\n");
}
2026-07-03 12:15:47 +02:00
static void print_help(const char *prog)
{
int tw = get_term_width();
if (tw < 60) tw = 80;
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);
2026-07-03 20:52:11 +02:00
printf(" %s", buf);
int pad = tw - 2 - v;
if (pad > 0) repeat_char(' ', pad);
2026-07-03 20:52:11 +02:00
printf("\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);
2026-07-03 20:52:11 +02:00
printf(" %s", buf);
int pad = tw - 2 - v;
if (pad > 0) repeat_char(' ', pad);
2026-07-03 20:52:11 +02:00
printf("\n");
}
pl("", tw);
2026-07-03 20:52:11 +02:00
printf(" " ANSI_BOLD "Options:" ANSI_RESET);
printf("\n");
#define OPT(fmt, desc) do { \
2026-07-03 20:52:11 +02:00
printf(" " ANSI_GREEN fmt ANSI_RESET " %s", desc); \
printf("\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);
2026-07-03 20:52:11 +02:00
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);
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
2026-07-03 12:15:47 +02:00
}
static void maybe_remove(const char *filename, int peak_db_neg90,
int is_native_lossy, int upscaled, int is_transcode)
{
int should_remove = 0;
if (peak_db_neg90) {
should_remove = 1;
} else if (is_native_lossy) {
should_remove = upscaled;
} else {
should_remove = is_transcode > 0;
}
if (should_remove) {
int tw = get_term_width();
int elen = 15 + (int)strlen(filename);
printf(ANSI_YELLOW "===> " ANSI_RESET ANSI_BRIGHT_RED ANSI_BOLD "Removing:" ANSI_RESET ANSI_BRIGHT_RED " %s", filename);
if (elen < tw) repeat_char(' ', tw - elen);
printf(ANSI_RESET "\n");
if (remove(filename) != 0)
fprintf(stderr, "Error: could not remove '%s'.\n", filename);
}
}
/* ---- Options bundle ---- */
typedef struct {
double threshold_db;
double sensitivity;
int fft_size;
int max_secs;
int verbose;
int dump_spectrum;
int visual;
int auto_remove;
int recursive;
int full;
2026-07-03 12:15:47 +02:00
} Options;
/* ---- Audio extension check ---- */
static int is_audio_ext(const char *path)
{
const char *ext = strrchr(path, '.');
if (!ext) return 0;
ext++;
static const char *exts[] = {
"mp3", "flac", "wav", "aiff", "aif", "ogg", "opus",
"m4a", "wma", "ac3", "eac3", "aac", "alac", "wv",
"mp2", "mp1", "ape", "dsf", "dff", NULL
};
for (const char **p = exts; *p; p++)
if (strcasecmp(ext, *p) == 0) return 1;
return 0;
}
/* ---- Single-file processor ---- */
static int process_file(const char *filename, const Options *opts)
{
AVFormatContext *fmt_ctx = NULL;
if (avformat_open_input(&fmt_ctx, filename, NULL, NULL) < 0) {
fprintf(stderr, "Error: could not open '%s'.\n", filename);
return 1;
}
if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
fprintf(stderr, "Error: could not find stream info.\n");
avformat_close_input(&fmt_ctx);
return 1;
}
const AVCodec *decoder = NULL;
int stream_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, &decoder, 0);
if (stream_idx < 0) {
fprintf(stderr, "Error: no audio stream found.\n");
avformat_close_input(&fmt_ctx);
return 1;
}
AVStream *stream = fmt_ctx->streams[stream_idx];
AVCodecContext *codec_ctx = avcodec_alloc_context3(decoder);
if (!codec_ctx) {
fprintf(stderr, "Error: could not allocate codec context.\n");
avformat_close_input(&fmt_ctx);
return 1;
}
if (avcodec_parameters_to_context(codec_ctx, stream->codecpar) < 0) {
fprintf(stderr, "Error: could not copy codec parameters.\n");
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);
return 1;
}
if (avcodec_open2(codec_ctx, decoder, NULL) < 0) {
fprintf(stderr, "Error: could not open decoder.\n");
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);
return 1;
}
int sample_rate = codec_ctx->sample_rate;
int channels = codec_ctx->ch_layout.nb_channels;
int64_t bitrate = codec_ctx->bit_rate;
int64_t duration_av = fmt_ctx->duration;
const char *fmt_name = fmt_ctx->iformat ? fmt_ctx->iformat->name : "?";
const char *codec_name = decoder->name;
int is_native_lossy = 0;
const char *lossy_codecs[] = {
"mp3", "mp3float", "aac", "libfdk_aac", "vorbis", "opus",
"wmav1", "wmav2", "wmapro", "libvorbis", "ac3", "eac3",
"mp2", "mp1", NULL
};
for (const char **p = lossy_codecs; *p; p++) {
if (strcmp(codec_name, *p) == 0) { is_native_lossy = 1; break; }
}
if (opts->verbose) {
fprintf(stderr, "Format: %s\n", fmt_name);
fprintf(stderr, "Channels: %d, Sample rate: %d Hz\n",
channels, sample_rate);
fprintf(stderr, "Bitrate: %lld bps\n", (long long)bitrate);
fprintf(stderr, "Decoder: %s (%s)\n", decoder->name, decoder->long_name ? decoder->long_name : "");
if (duration_av != AV_NOPTS_VALUE)
fprintf(stderr, "Duration: %lld seconds\n",
(long long)(duration_av / AV_TIME_BASE));
}
Analyzer analyzer;
analyzer_init(&analyzer, sample_rate, channels, opts->fft_size);
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
if (!pkt || !frame) {
fprintf(stderr, "Error: could not allocate packet/frame.\n");
av_packet_free(&pkt);
av_frame_free(&frame);
analyzer_free(&analyzer);
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);
return 1;
}
int max_frames = opts->full ? INT_MAX : sample_rate * opts->max_secs;
2026-07-03 12:15:47 +02:00
int total_frames = 0;
float *ring = calloc(opts->fft_size * channels, sizeof(float));
int ring_pos = 0;
while (total_frames < max_frames && av_read_frame(fmt_ctx, pkt) == 0) {
if (pkt->stream_index != stream_idx) {
av_packet_unref(pkt);
continue;
}
if (avcodec_send_packet(codec_ctx, pkt) < 0) {
av_packet_unref(pkt);
continue;
}
av_packet_unref(pkt);
while (total_frames < max_frames) {
int ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN))
break;
if (ret == AVERROR_EOF)
break;
if (ret < 0)
break;
int nframes = frame->nb_samples;
if (nframes > max_frames - total_frames)
nframes = max_frames - total_frames;
if (nframes <= 0) break;
frame_to_ring(frame, nframes, channels, opts->fft_size, ring, &ring_pos, &analyzer);
total_frames += nframes;
}
}
avcodec_send_packet(codec_ctx, NULL);
while (total_frames < max_frames) {
int ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR_EOF || ret < 0)
break;
int nframes = frame->nb_samples;
if (nframes > max_frames - total_frames)
nframes = max_frames - total_frames;
if (nframes <= 0) break;
frame_to_ring(frame, nframes, channels, opts->fft_size, ring, &ring_pos, &analyzer);
total_frames += nframes;
}
if (ring_pos >= opts->fft_size)
analyzer_add_window(&analyzer, ring, ring_pos);
free(ring);
av_packet_free(&pkt);
av_frame_free(&frame);
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);
if (analyzer.count == 0) {
if (total_frames > 0)
fprintf(stderr, "Error: file too short for analysis (%d samples, need %d).\n"
" Use -f to set a smaller FFT size.\n",
total_frames, opts->fft_size);
else
fprintf(stderr, "Error: no audio data decoded from '%s'.\n", filename);
analyzer_free(&analyzer);
return 1;
}
double cutoff_hz = 0, steepness = 0, noise_db = 0;
2026-07-03 23:00:42 +02:00
double roughness = 0, band_ratio = 0, extended_cutoff = 0;
2026-07-03 12:15:47 +02:00
int is_transcode = detect_cutoff(&analyzer, opts->threshold_db,
opts->sensitivity,
&cutoff_hz, &steepness, &noise_db,
2026-07-03 23:00:42 +02:00
&roughness, &band_ratio,
&extended_cutoff);
2026-07-03 12:15:47 +02:00
double nyquist = sample_rate / 2.0;
double peak_mag = 0;
for (int i = 0; i < opts->fft_size / 2; i++) {
double m = sqrt(analyzer.power[i] / analyzer.count);
if (m > peak_mag) peak_mag = m;
}
double peak_db = (peak_mag > 1e-12) ? 20.0 * log10(peak_mag) : -200.0;
/* ---- Visual output ---- */
if (opts->visual) {
printf("\n");
if (peak_db < -90.0) {
printf(ANSI_RED "╭────────────────────────────────────────╮\n" ANSI_RESET);
printf(ANSI_RED "" ANSI_RESET " " ANSI_BOLD ANSI_RED "SILENT" ANSI_RESET " \xe2\x80\x94 no detectable audio content " ANSI_RED "\n" ANSI_RESET);
printf(ANSI_RED "╰────────────────────────────────────────╯\n" ANSI_RESET);
analyzer_free(&analyzer);
if (opts->auto_remove) maybe_remove(filename, 1, 0, 0, 0);
return 2;
}
2026-07-03 23:00:42 +02:00
double effective_cutoff = cutoff_hz;
if (extended_cutoff > effective_cutoff) effective_cutoff = extended_cutoff;
double cutoff_ratio = effective_cutoff / nyquist;
2026-07-03 12:15:47 +02:00
int upscaled = 0;
if (is_native_lossy) {
double expected_min = 0.90;
if (bitrate > 0 && bitrate < 192000) expected_min = 0.75;
else if (bitrate > 0 && bitrate < 256000) expected_min = 0.85;
if (cutoff_ratio > 0 && cutoff_ratio < expected_min - 0.08)
upscaled = 1;
}
render_visual_spectrum(
&analyzer,
2026-07-03 23:00:42 +02:00
cutoff_hz, effective_cutoff, steepness, noise_db,
roughness, band_ratio,
2026-07-03 12:15:47 +02:00
is_transcode, is_native_lossy, upscaled,
peak_db, opts->threshold_db,
filename, fmt_name,
sample_rate, channels, analyzer.count,
opts->fft_size, bitrate);
analyzer_free(&analyzer);
if (opts->auto_remove) maybe_remove(filename, peak_db < -90.0, is_native_lossy, upscaled, is_transcode);
if (is_native_lossy)
return upscaled ? 1 : 0;
else
return is_transcode > 0 ? 1 : 0;
}
/* ---- Plain text output ---- */
printf("\n");
{
int tw = get_term_width();
int flen = (int)strlen(filename);
printf(ANSI_BG_GREY ANSI_BOLD ANSI_CYAN "File:" ANSI_RESET ANSI_BG_GREY " %s", filename);
int used = 14 + flen;
if (used < tw) repeat_char(' ', tw - used);
printf(ANSI_RESET "\n");
}
printf(ANSI_BOLD ANSI_CYAN "Format:" ANSI_RESET " %s\n", fmt_name);
printf(ANSI_BOLD ANSI_CYAN "Sample rate:" ANSI_RESET " %d Hz\n", sample_rate);
printf(ANSI_BOLD ANSI_CYAN "Channels:" ANSI_RESET " %d\n", channels);
printf(ANSI_BOLD ANSI_CYAN "Windows:" ANSI_RESET " %d\n", analyzer.count);
printf(ANSI_BOLD ANSI_CYAN "Peak:" ANSI_RESET " %.1f dBFS\n", peak_db);
printf(ANSI_BOLD ANSI_CYAN "Cutoff:" ANSI_RESET " %.0f Hz (%.1f%% of Nyquist)\n", cutoff_hz, 100.0 * cutoff_hz / nyquist);
printf(ANSI_BOLD ANSI_CYAN "Steepness:" ANSI_RESET " %.0f Hz\n", steepness);
printf(ANSI_BOLD ANSI_CYAN "Roughness:" ANSI_RESET " %.3f\n", roughness);
printf(ANSI_BOLD ANSI_CYAN "Band ratio:" ANSI_RESET " %.3f\n", band_ratio);
printf(ANSI_BOLD ANSI_CYAN "Noise floor:" ANSI_RESET " %.1f dB\n", noise_db);
printf(ANSI_BOLD ANSI_CYAN "Verdict:" ANSI_RESET " ");
if (peak_db < -90.0) {
printf(ANSI_RED "SILENT" ANSI_RESET " (no detectable audio content)\n");
analyzer_free(&analyzer);
if (opts->auto_remove) maybe_remove(filename, 1, 0, 0, 0);
return 2;
}
2026-07-03 23:00:42 +02:00
double effective_cutoff = cutoff_hz;
if (extended_cutoff > effective_cutoff) effective_cutoff = extended_cutoff;
double cutoff_ratio = effective_cutoff / nyquist;
2026-07-03 12:15:47 +02:00
int upscaled = 0;
if (is_native_lossy) {
double expected_min = 0.90;
if (bitrate > 0 && bitrate < 192000) expected_min = 0.75;
else if (bitrate > 0 && bitrate < 256000) expected_min = 0.85;
if (cutoff_ratio > 0 && cutoff_ratio < expected_min - 0.08)
upscaled = 1;
if (upscaled) {
printf(ANSI_YELLOW "UPSCALED" ANSI_RESET " (re-encoded from a lower-bitrate source)\n");
printf(ANSI_BOLD ANSI_CYAN "Verdict Info:" ANSI_RESET " cut-off suggests ");
if (cutoff_ratio < 0.70)
printf(ANSI_BOLD "<= 64 kbps" ANSI_RESET " source\n");
else if (cutoff_ratio < 0.80)
printf(ANSI_BOLD "96-128 kbps" ANSI_RESET " source\n");
else if (cutoff_ratio < 0.88)
printf(ANSI_BOLD "128-192 kbps" ANSI_RESET " source\n");
} else {
printf(ANSI_GREEN "NATIVE" ANSI_RESET " (single encode at this bitrate)\n");
}
} else {
if (is_transcode)
printf(ANSI_YELLOW "TRANSCODE" ANSI_RESET " (lossy -> lossless re-encode detected)\n");
else
printf(ANSI_GREEN "GENUINE" ANSI_RESET " (likely native lossless)\n");
}
analyzer_free(&analyzer);
if (opts->auto_remove) maybe_remove(filename, peak_db < -90.0, is_native_lossy, upscaled, is_transcode);
if (is_native_lossy)
return upscaled ? 1 : 0;
else
return is_transcode > 0 ? 1 : 0;
}
/* ---- Directory walker ---- */
static int process_path(const char *path, const Options *opts)
{
struct stat st;
if (stat(path, &st) < 0) {
fprintf(stderr, "Error: cannot access '%s' (%s).\n", path, strerror(errno));
return 1;
}
if (!S_ISDIR(st.st_mode))
return process_file(path, opts);
/* Strip trailing slash so we don't double it */
size_t plen = strlen(path);
while (plen > 1 && path[plen - 1] == '/') plen--;
DIR *dir = opendir(path);
if (!dir) {
fprintf(stderr, "Error: cannot open directory '%s' (%s).\n", path, strerror(errno));
return 1;
}
int overall = 0;
struct dirent *e;
while ((e = readdir(dir))) {
if (e->d_name[0] == '.') continue;
size_t nlen = strlen(e->d_name);
char *full = malloc(plen + 1 + nlen + 1);
if (!full) continue;
memcpy(full, path, plen);
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;
}
2026-07-03 12:15:47 +02:00
int rc = process_path(full, opts);
free(full);
if (rc > overall) overall = rc;
}
closedir(dir);
return overall;
}
/* ---- FFmpeg log filter (silences non-fatal chatter) ---- */
static void quiet_log(void *avcl, int level, const char *fmt, va_list vl)
{
(void)avcl;
if (level > AV_LOG_ERROR) return;
vfprintf(stderr, fmt, vl);
}
/* ---- Main ---- */
int main(int argc, char **argv)
{
av_log_set_callback(quiet_log);
const char *prog = argv[0];
Options opts = {
.threshold_db = -(40.0 + (double)(THRESHOLD_DEFAULT - 1) * 40.0 / 98.0),
2026-07-03 12:15:47 +02:00
.sensitivity = 0.5,
.fft_size = FFT_SIZE_DEFAULT,
.max_secs = MAX_ANALYSIS_SECS,
.verbose = 0,
.dump_spectrum = 0,
.visual = 0,
.auto_remove = 0,
.recursive = 0,
.full = 0,
2026-07-03 12:15:47 +02:00
};
static const struct option long_opts[] = {
{"threshold", required_argument, NULL, 't'},
{"fft-size", required_argument, NULL, 'f'},
{"duration", required_argument, NULL, 'd'},
{"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'},
2026-07-03 12:15:47 +02:00
{"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:arFsvVh", long_opts, NULL)) != -1) {
2026-07-03 12:15:47 +02:00
switch (opt) {
case 't': {
int pct = atoi(optarg);
if (pct > 99) pct = 99;
if (pct < 1) pct = 1;
opts.threshold_db = -(40.0 + (double)(pct - 1) * 40.0 / 98.0);
opts.sensitivity = (double)(pct - 1) / 98.0;
break;
}
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;
2026-07-03 12:15:47 +02:00
case 's': opts.visual = 1; break;
case 'v': opts.verbose = 1; break;
case 'V': opts.visual = 1; break;
case 'h': print_help(prog); return 0;
default: print_help(prog); return 1;
}
}
if (optind >= argc) {
print_help(prog);
return 0;
2026-07-03 12:15:47 +02:00
}
if (opts.fft_size < 64 || opts.fft_size > 65536 || (opts.fft_size & (opts.fft_size - 1)) != 0) {
fprintf(stderr, "Error: FFT size must be a power of 2 between 64 and 65536.\n");
return 1;
}
int overall = 0;
for (int i = optind; i < argc; i++) {
int rc = process_path(argv[i], &opts);
if (rc > overall) overall = rc;
}
return overall;
}