tcd/tcd.c
2026-07-04 09:44:24 +02:00

1509 lines
52 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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>
#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 120
#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,
double *out_band_ratio,
double *out_extended_cutoff)
{
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);
/* 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;
/* When the noise floor is above the detection threshold, the cutoff is
determined by noise rather than signal. Compute a noise-aware cutoff
at (noise floor + 10 dB) for the roughness computation so it operates
on signal content rather than noise-dominated bins. */
double rough_cutoff = cutoff_hz;
if (*out_noise_db > threshold_db) {
double adj_db = *out_noise_db + 10.0;
double adj_thresh = peak * pow(10.0, adj_db / 20.0);
double adj_cutoff = 0;
for (int i = n - 1; i >= 0; i--) {
if (mag[i] >= adj_thresh) { adj_cutoff = (double)i * sr / a->fft_size; break; }
}
if (adj_cutoff > 0) rough_cutoff = adj_cutoff;
}
double roughness = 0.0;
double cutoff_idx = rough_cutoff * 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;
/* Check whether the roughness region is noise-dominated. When the noise
floor is close to or above the detection threshold the cutoff is
determined by noise, and the roughness region may contain mostly noise
bins, producing spuriously high roughness values. If fewer than 30 %
of bins in the region are more than 6 dB above the noise floor, the
roughness is unreliable and we set it to a low value. */
double noise_mag = pow(10.0, *out_noise_db / 20.0) * peak;
int region_total = hi - lo + 1;
int above_noise = 0;
for (int i = lo; i <= hi; i++) {
if (mag[i] > noise_mag * 2.0) above_noise++;
}
if ((double)above_noise / region_total < 0.30) {
roughness = 0.01;
} else 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;
double nyquist = sr / 2.0;
/* When the noise floor is above the detection threshold, the cutoff
is determined by noise, not signal. Use the noise-aware cutoff
(at noise floor + 10 dB) for decision-making to avoid false positives
from an inflated cutoff ratio where noise dominates the spectrum. */
double decision_cutoff = cutoff_hz;
if (*out_noise_db > threshold_db) {
double dc_db = *out_noise_db + 10.0;
double dc_thresh = peak * pow(10.0, dc_db / 20.0);
for (int i = n - 1; i >= 0; i--) {
if (mag[i] >= dc_thresh) {
decision_cutoff = (double)i * sr / a->fft_size;
break;
}
}
if (decision_cutoff <= 0) decision_cutoff = cutoff_hz;
}
double cutoff_ratio = decision_cutoff / 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 (decision_cutoff <= 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;
free(mag);
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;
}
free(mag);
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,
double cutoff_hz, double effective_cutoff_hz,
double steepness, double noise_db,
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;
int chart_w = term_w - label_w;
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);
int total_w = term_w;
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);
printf("\n");
} else {
printf(ANSI_BOLD ANSI_CYAN "%s" ANSI_RESET, title);
printf("\n");
}
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("");
}
}
printf("\n");
}
/* --- 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(' ');
}
}
printf("\n");
/* --- 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);
printf("\n");
/* --- 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));
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",
peak_dbfs_str, analyzer_count, fft_size);
int used_metrics = 30 + 14 + 10 + 14;
repeat_char(' ', term_w - 2 - used_metrics);
printf("\n");
draw_hline(chart_w + label_w);
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",
cutoff_hz, 100.0 * cutoff_hz / nyquist, steepness, noise_db);
repeat_char(' ', term_w - 2 - 62);
printf("\n");
printf(" " ANSI_BOLD "Roughness:" ANSI_RESET " %6.3f " ANSI_BLUE "" ANSI_RESET " " ANSI_BOLD "Band ratio:" ANSI_RESET " %6.3f",
roughness, band_ratio);
repeat_char(' ', term_w - 2 - 36);
printf("\n");
/* ============================ */
/* DECISION / VALIDITY */
/* ============================ */
draw_hline(chart_w + label_w);
double cutoff_ratio = cutoff_hz / nyquist;
double eff_cutoff_ratio = effective_cutoff_hz / nyquist;
double confidence = compute_confidence(
is_native_lossy ? eff_cutoff_ratio : cutoff_ratio,
steepness, roughness, band_ratio, is_native_lossy);
/* Compute noise-aware factor ratio for consistent display with detect_cutoff().
When the noise floor is above the user's threshold the cutoff is noise-
dominated; use (noise floor + 10 dB) for the factor display instead. */
double factor_ratio = eff_cutoff_ratio;
if (!is_native_lossy && noise_db > threshold_db) {
double fc_db = noise_db + 10.0;
double fc_thresh = peak * pow(10.0, fc_db / 20.0);
int n_bins = fft_size / 2;
double fc_cut = cutoff_hz;
for (int i = n_bins - 1; i >= 0; i--) {
double m = sqrt(a->power[i] / a->count);
if (m >= fc_thresh) { fc_cut = (double)i * sample_rate / fft_size; break; }
}
if (fc_cut > 0) factor_ratio = fc_cut / nyquist;
}
printf(" " ANSI_BOLD "Decision:" ANSI_RESET " ");
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)");
}
printf("\n");
/* 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";
printf(" " ANSI_BOLD "Validity:" ANSI_RESET " ");
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);
printf("\n");
printf("\n");
/* --- Factor breakdown --- */
printf(" " ANSI_BOLD "Factors:" ANSI_RESET);
printf("\n");
if (is_native_lossy) {
if (upscaled) {
char line[128];
snprintf(line, sizeof(line),
" cutoff_ratio=%.3f < expected for bitrate (%lld kbps)",
eff_cutoff_ratio, (long long)(bitrate / 1000));
printf(" %s " ANSI_GREEN "" ANSI_RESET, line);
printf("\n");
const char *src_hint = "";
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";
printf("" ANSI_BOLD "Suggest %s" ANSI_RESET, src_hint);
printf("\n");
} else {
printf(" cutoff_ratio=%.3f within expected range for this bitrate " ANSI_GREEN "" ANSI_RESET, eff_cutoff_ratio);
printf("\n");
}
} else {
double fr = factor_ratio;
double max_bw_display;
if (fr < 0.50) {
max_bw_display = 4000.0;
} else if (fr < 0.70) {
max_bw_display = 3000.0;
} else if (fr < 0.80) {
max_bw_display = 2000.0;
} else if (fr < 0.90) {
max_bw_display = 1200.0;
} else {
max_bw_display = 500.0;
}
int primary_hit = (fr >= 0.99) ? 0 : (steepness < max_bw_display);
int secondary_applies = (fr > 0.80);
if (fr >= 0.99) {
printf(" ① cutoff_ratio=%.3f ≥ 0.99 " ANSI_RED "" ANSI_RESET " (full spectrum, no cutoff)", fr);
printf("\n");
} else {
char bw_label[64];
snprintf(bw_label, sizeof(bw_label), "transition_bw=%.0fHz < %.0fHz threshold",
steepness, max_bw_display);
if (primary_hit) {
printf(" ① cutoff_ratio=%.3f, %s " ANSI_GREEN "" ANSI_RESET,
fr, bw_label);
printf("\n");
} else {
printf(" ① cutoff_ratio=%.3f, transition_bw=%.0fHz ≥ %.0fHz threshold " ANSI_RED "" ANSI_RESET,
fr, steepness, max_bw_display);
printf("\n");
}
}
if (primary_hit) {
printf("" ANSI_YELLOW "Transition bandwidth indicates transcode" ANSI_RESET);
printf("\n");
}
if (secondary_applies) {
int sec_hit = 0;
if (roughness > 0.40) {
printf(" ② roughness=%.3f > 0.40 " ANSI_GREEN "" ANSI_RESET " (high roughness → transcode)", roughness);
printf("\n");
sec_hit = 1;
} else if (roughness > 0.30 && band_ratio < 0.90) {
printf(" ② roughness=%.3f > 0.30 " ANSI_GREEN "" ANSI_RESET " band_ratio=%.3f < 0.90 " ANSI_GREEN "" ANSI_RESET,
roughness, band_ratio);
printf("\n");
sec_hit = 1;
} else if (roughness > 0.20 && band_ratio < 0.85) {
printf(" ② roughness=%.3f > 0.20 " ANSI_GREEN "" ANSI_RESET " band_ratio=%.3f < 0.85 " ANSI_GREEN "" ANSI_RESET,
roughness, band_ratio);
printf("\n");
sec_hit = 1;
}
if (sec_hit) {
printf("" ANSI_YELLOW "Secondary criteria triggered: transcode confirmed" ANSI_RESET);
printf("\n");
} else if (!primary_hit) {
printf(" ② Secondary: roughness=%.3f, band_ratio=%.3f " ANSI_RED "" ANSI_RESET,
roughness, band_ratio);
printf("\n");
printf("" ANSI_GREEN "No transcode criteria met: genuine lossless" ANSI_RESET);
printf("\n");
}
} else if (!primary_hit) {
printf("" ANSI_GREEN "No transcode criteria met: genuine lossless" ANSI_RESET);
printf("\n");
}
}
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);
printf(" %s", s);
int pad = w - 2 - sl;
if (pad > 0) repeat_char(' ', pad);
printf("\n");
}
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);
printf(" %s", buf);
int pad = tw - 2 - v;
if (pad > 0) repeat_char(' ', pad);
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);
printf(" %s", buf);
int pad = tw - 2 - v;
if (pad > 0) repeat_char(' ', pad);
printf("\n");
}
pl("", tw);
printf(" " ANSI_BOLD "Options:" ANSI_RESET);
printf("\n");
#define OPT(fmt, desc) do { \
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);
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
}
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;
} 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;
int total_frames = 0;
if (!opts->full && fmt_ctx->duration > 0 && fmt_ctx->duration != AV_NOPTS_VALUE) {
int64_t seek_ts = fmt_ctx->duration / 2;
if (av_seek_frame(fmt_ctx, -1, seek_ts, AVSEEK_FLAG_BACKWARD) >= 0)
avcodec_flush_buffers(codec_ctx);
}
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;
double roughness = 0, band_ratio = 0, extended_cutoff = 0;
int is_transcode = detect_cutoff(&analyzer, opts->threshold_db,
opts->sensitivity,
&cutoff_hz, &steepness, &noise_db,
&roughness, &band_ratio,
&extended_cutoff);
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;
}
double effective_cutoff = cutoff_hz;
if (extended_cutoff > effective_cutoff && noise_db >= -100.0)
effective_cutoff = extended_cutoff;
double cutoff_ratio = effective_cutoff / nyquist;
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 (noise_db < -80.0) expected_min -= 0.05;
if (cutoff_ratio > 0 && cutoff_ratio < expected_min - 0.08) {
upscaled = 1;
}
}
render_visual_spectrum(
&analyzer,
cutoff_hz, effective_cutoff, steepness, noise_db,
roughness, band_ratio,
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);
if (bitrate > 0)
printf(ANSI_BOLD ANSI_CYAN "Bitrate:" ANSI_RESET " %lld kbps\n", (long long)(bitrate / 1000));
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;
}
double effective_cutoff = cutoff_hz;
if (extended_cutoff > effective_cutoff && noise_db >= -100.0)
effective_cutoff = extended_cutoff;
double cutoff_ratio = effective_cutoff / nyquist;
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 (noise_db < -80.0) expected_min -= 0.05;
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");
} else {
printf(ANSI_GREEN "NATIVE" ANSI_RESET " (single encode at this bitrate)\n");
}
printf(ANSI_BOLD ANSI_CYAN "Verdict Info:" ANSI_RESET " cutoff=%.1f%% Nyquist, expected≥%.0f%% for %lld kbps",
100.0 * cutoff_ratio, 100.0 * expected_min,
bitrate > 0 ? (long long)(bitrate / 1000) : 0);
if (upscaled) {
printf(" → suggests ");
if (cutoff_ratio < 0.70)
printf(ANSI_BOLD "≤ 64 kbps" ANSI_RESET " source");
else if (cutoff_ratio < 0.80)
printf(ANSI_BOLD "96128 kbps" ANSI_RESET " source");
else if (cutoff_ratio < 0.88)
printf(ANSI_BOLD "128192 kbps" ANSI_RESET " source");
}
printf("\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;
}
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),
.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,
};
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'},
{"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) {
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;
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;
}
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;
}