#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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, double *out_band_ratio) { 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); 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 "│" ANSI_RESET); printf(ANSI_BLUE); repeat_str("─", width); printf(ANSI_RESET); printf(ANSI_BLUE "│" 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++; } } printf(ANSI_BLUE "│" ANSI_RESET); 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(ANSI_BLUE "│" ANSI_RESET); printf("\n"); printf(ANSI_BLUE "│" ANSI_RESET); 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(ANSI_BLUE "│" ANSI_RESET); printf("\n"); } static void render_visual_spectrum( const Analyzer *a, double 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 - 2; 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; /* ============================ */ /* TOP BORDER */ /* ============================ */ printf(ANSI_BLUE "╭" ANSI_RESET); printf(ANSI_BLUE); repeat_str("─", chart_w + label_w); printf(ANSI_RESET); printf(ANSI_BLUE "╮" ANSI_RESET "\n"); /* --- 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 - 2; 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_BLUE "│" ANSI_RESET); printf(ANSI_BOLD ANSI_CYAN "%s" ANSI_RESET, truncated); repeat_char(' ', total_w - (int)strlen(truncated)); printf(ANSI_BLUE "│" ANSI_RESET "\n"); } else { printf(ANSI_BLUE "│" ANSI_RESET); printf(ANSI_BOLD ANSI_CYAN "%s" ANSI_RESET, title); repeat_char(' ', total_w - info_len); printf(ANSI_BLUE "│" ANSI_RESET "\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_BLUE "│" ANSI_RESET); 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(ANSI_BLUE "│" ANSI_RESET "\n"); } /* --- Cutoff arrow row --- */ printf(ANSI_BLUE "│" ANSI_RESET); repeat_char(' ', label_w); for (int c = 0; c < chart_w; c++) { if (c == cutoff_col) { printf(ANSI_YELLOW "┬" ANSI_RESET); } else { putchar(' '); } } printf(ANSI_BLUE "│" ANSI_RESET "\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; printf(ANSI_BLUE "│" ANSI_RESET); 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(ANSI_BLUE "│" ANSI_RESET "\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_BLUE "│" ANSI_RESET " " 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(ANSI_BLUE "│" ANSI_RESET "\n"); draw_hline(chart_w + label_w); printf(ANSI_BLUE "│" ANSI_RESET " " 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(ANSI_BLUE "│" ANSI_RESET "\n"); printf(ANSI_BLUE "│" ANSI_RESET " " 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(ANSI_BLUE "│" ANSI_RESET "\n"); /* ============================ */ /* DECISION / VALIDITY */ /* ============================ */ draw_hline(chart_w + label_w); double cutoff_ratio = cutoff_hz / nyquist; double confidence = compute_confidence( cutoff_ratio, steepness, roughness, band_ratio, is_native_lossy); printf(ANSI_BLUE "│" ANSI_RESET " " 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)"); } int dlen = (int)strlen(" Decision: ") + 50; repeat_char(' ', term_w - 2 - dlen); printf(ANSI_BLUE "│" ANSI_RESET "\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_BLUE "│" ANSI_RESET " " 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); int vlen = 12 + bar_w + 5 + 2 + (int)strlen(conf_label); repeat_char(' ', term_w - 2 - vlen); printf(ANSI_BLUE "│" ANSI_RESET "\n"); printf(ANSI_BLUE "│" ANSI_RESET); repeat_char(' ', term_w - 2); printf(ANSI_BLUE "│" ANSI_RESET "\n"); /* --- Factor breakdown --- */ printf(ANSI_BLUE "│" ANSI_RESET " " ANSI_BOLD "Factors:" ANSI_RESET); repeat_char(' ', term_w - 12); printf(ANSI_BLUE "│" ANSI_RESET "\n"); if (is_native_lossy) { if (upscaled) { char line[128]; snprintf(line, sizeof(line), " cutoff_ratio=%.3f < expected for bitrate (%lld kbps)", cutoff_ratio, (long long)(bitrate / 1000)); printf(ANSI_BLUE "│" ANSI_RESET " %s " ANSI_GREEN "✓" ANSI_RESET, line); int remain = term_w - 4 - (int)strlen(line) - 2; if (remain > 0) repeat_char(' ', remain); printf(ANSI_BLUE "│" ANSI_RESET "\n"); const char *src_hint = ""; if (cutoff_ratio < 0.70) src_hint = "≤ 64 kbps source"; else if (cutoff_ratio < 0.80) src_hint = "96–128 kbps source"; else if (cutoff_ratio < 0.88) src_hint = "128–192 kbps source"; printf(ANSI_BLUE "│" ANSI_RESET " → " ANSI_BOLD "Suggest %s" ANSI_RESET, src_hint); repeat_char(' ', term_w - 4 - (int)strlen(src_hint) - 14); printf(ANSI_BLUE "│" ANSI_RESET "\n"); } else { printf(ANSI_BLUE "│" ANSI_RESET " cutoff_ratio=%.3f within expected range for this bitrate " ANSI_GREEN "✓" ANSI_RESET, cutoff_ratio); repeat_char(' ', term_w - 2 - 68); printf(ANSI_BLUE "│" ANSI_RESET "\n"); } } 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) { printf(ANSI_BLUE "│" ANSI_RESET " ① cutoff_ratio=%.3f ≥ 0.99 " ANSI_RED "✗" ANSI_RESET " (full spectrum, no cutoff)", cutoff_ratio); repeat_char(' ', term_w - 2 - 62); printf(ANSI_BLUE "│" ANSI_RESET "\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(ANSI_BLUE "│" ANSI_RESET " ① cutoff_ratio=%.3f, %s " ANSI_GREEN "✓" ANSI_RESET, cutoff_ratio, bw_label); repeat_char(' ', term_w - 2 - (int)strlen(bw_label) - 24); printf(ANSI_BLUE "│" ANSI_RESET "\n"); } else { printf(ANSI_BLUE "│" ANSI_RESET " ① cutoff_ratio=%.3f, transition_bw=%.0fHz ≥ %.0fHz threshold " ANSI_RED "✗" ANSI_RESET, cutoff_ratio, steepness, max_bw_display); repeat_char(' ', term_w - 2 - 71); printf(ANSI_BLUE "│" ANSI_RESET "\n"); } } if (primary_hit) { printf(ANSI_BLUE "│" ANSI_RESET " → " ANSI_YELLOW "Transition bandwidth indicates transcode" ANSI_RESET); repeat_char(' ', term_w - 2 - 44); printf(ANSI_BLUE "│" ANSI_RESET "\n"); } if (secondary_applies) { int sec_hit = 0; if (roughness > 0.40) { printf(ANSI_BLUE "│" ANSI_RESET " ② roughness=%.3f > 0.40 " ANSI_GREEN "✓" ANSI_RESET " (high roughness → transcode)", roughness); repeat_char(' ', term_w - 2 - 60); printf(ANSI_BLUE "│" ANSI_RESET "\n"); sec_hit = 1; } else if (roughness > 0.30 && band_ratio < 0.90) { printf(ANSI_BLUE "│" ANSI_RESET " ② roughness=%.3f > 0.30 " ANSI_GREEN "✓" ANSI_RESET " band_ratio=%.3f < 0.90 " ANSI_GREEN "✓" ANSI_RESET, roughness, band_ratio); repeat_char(' ', term_w - 2 - 62); printf(ANSI_BLUE "│" ANSI_RESET "\n"); sec_hit = 1; } else if (roughness > 0.20 && band_ratio < 0.85) { printf(ANSI_BLUE "│" ANSI_RESET " ② roughness=%.3f > 0.20 " ANSI_GREEN "✓" ANSI_RESET " band_ratio=%.3f < 0.85 " ANSI_GREEN "✓" ANSI_RESET, roughness, band_ratio); repeat_char(' ', term_w - 2 - 62); printf(ANSI_BLUE "│" ANSI_RESET "\n"); sec_hit = 1; } if (sec_hit) { printf(ANSI_BLUE "│" ANSI_RESET " → " ANSI_YELLOW "Secondary criteria triggered: transcode confirmed" ANSI_RESET); repeat_char(' ', term_w - 2 - 52); printf(ANSI_BLUE "│" ANSI_RESET "\n"); } else if (!primary_hit) { printf(ANSI_BLUE "│" ANSI_RESET " ② Secondary: roughness=%.3f, band_ratio=%.3f " ANSI_RED "✗" ANSI_RESET, roughness, band_ratio); repeat_char(' ', term_w - 2 - 56); printf(ANSI_BLUE "│" ANSI_RESET "\n"); printf(ANSI_BLUE "│" ANSI_RESET " → " ANSI_GREEN "No transcode criteria met: genuine lossless" ANSI_RESET); repeat_char(' ', term_w - 2 - 46); printf(ANSI_BLUE "│" ANSI_RESET "\n"); } } else if (!primary_hit) { printf(ANSI_BLUE "│" ANSI_RESET " → " ANSI_GREEN "No transcode criteria met: genuine lossless" ANSI_RESET); repeat_char(' ', term_w - 2 - 46); printf(ANSI_BLUE "│" ANSI_RESET "\n"); } } /* ============================ */ /* BOTTOM BORDER */ /* ============================ */ printf(ANSI_BLUE "╰" ANSI_RESET); printf(ANSI_BLUE); repeat_str("─", chart_w + label_w); printf(ANSI_RESET); printf(ANSI_BLUE "╯" ANSI_RESET "\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 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) { 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] [ ...]", 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, 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; 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; int is_transcode = detect_cutoff(&analyzer, opts->threshold_db, opts->sensitivity, &cutoff_hz, &steepness, &noise_db, &roughness, &band_ratio); 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 cutoff_ratio = cutoff_hz / 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 (cutoff_ratio > 0 && cutoff_ratio < expected_min - 0.08) upscaled = 1; } render_visual_spectrum( &analyzer, cutoff_hz, 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); 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 cutoff_ratio = cutoff_hz / 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 (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; } 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; }