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>
|
2026-07-03 13:52:15 +02:00
|
|
|
|
#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,
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 13:52:15 +02:00
|
|
|
|
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");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 12:15:47 +02:00
|
|
|
|
static void print_help(const char *prog)
|
|
|
|
|
|
{
|
2026-07-03 13:52:15 +02:00
|
|
|
|
int tw = get_term_width();
|
|
|
|
|
|
if (tw < 60) tw = 80;
|
|
|
|
|
|
|
|
|
|
|
|
pn(ANSI_BLUE "╭"); repeat_str("─", tw - 2); pn(ANSI_BLUE "╮" ANSI_RESET "\n");
|
|
|
|
|
|
|
|
|
|
|
|
pl("", tw);
|
|
|
|
|
|
{
|
|
|
|
|
|
char buf[256];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), ANSI_BOLD ANSI_CYAN "tcd" ANSI_RESET
|
|
|
|
|
|
" \xe2\x80\x94 Transcode Detector "
|
|
|
|
|
|
"Psychoacoustic audio authenticity analysis");
|
|
|
|
|
|
int v = vis_len(buf);
|
|
|
|
|
|
printf(ANSI_BLUE "│" ANSI_RESET " %s", buf);
|
|
|
|
|
|
int pad = tw - 4 - v;
|
|
|
|
|
|
if (pad > 0) repeat_char(' ', pad);
|
|
|
|
|
|
printf(ANSI_BLUE "│" ANSI_RESET "\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
pl("", tw);
|
|
|
|
|
|
|
|
|
|
|
|
pl("Analyze audio files to detect transcodes (lossy \xe2\x86\x92 lossless", tw);
|
|
|
|
|
|
pl("re-encodes) by measuring spectral cutoffs and artifacts.", tw);
|
|
|
|
|
|
pl("", tw);
|
|
|
|
|
|
{
|
|
|
|
|
|
char buf[256];
|
|
|
|
|
|
snprintf(buf, sizeof(buf),
|
|
|
|
|
|
ANSI_BOLD "Usage:" ANSI_RESET " %s [options] [<audio-file> ...]", prog);
|
|
|
|
|
|
int v = vis_len(buf);
|
|
|
|
|
|
printf(ANSI_BLUE "│" ANSI_RESET " %s", buf);
|
|
|
|
|
|
int pad = tw - 4 - v;
|
|
|
|
|
|
if (pad > 0) repeat_char(' ', pad);
|
|
|
|
|
|
printf(ANSI_BLUE "│" ANSI_RESET "\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
pl("", tw);
|
|
|
|
|
|
|
|
|
|
|
|
pn(ANSI_BLUE "│" ANSI_RESET " " ANSI_BOLD "Options:" ANSI_RESET);
|
|
|
|
|
|
repeat_char(' ', tw - 12);
|
|
|
|
|
|
pn(ANSI_BLUE "│" ANSI_RESET "\n");
|
|
|
|
|
|
|
|
|
|
|
|
#define OPT(fmt, desc) do { \
|
|
|
|
|
|
printf(ANSI_BLUE "│" ANSI_RESET " " ANSI_GREEN fmt ANSI_RESET " %s", desc); \
|
|
|
|
|
|
int v = 1 + 4 + (int)strlen(fmt) + 2 + vis_len(desc); \
|
|
|
|
|
|
int pad = tw - 1 - v; \
|
|
|
|
|
|
if (pad > 0) repeat_char(' ', pad); \
|
|
|
|
|
|
printf(ANSI_BLUE "│" ANSI_RESET "\n"); \
|
|
|
|
|
|
} while (0)
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
char buf[80];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "Detection sensitivity 1-99 [" ANSI_CYAN "%d" ANSI_RESET "]", THRESHOLD_DEFAULT);
|
|
|
|
|
|
OPT("-t, --threshold PCT", buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
{
|
|
|
|
|
|
char buf[80];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), ANSI_DIM " Lower = fewer, higher = more" ANSI_RESET);
|
|
|
|
|
|
int v = 1 + 4 + vis_len(buf);
|
|
|
|
|
|
printf(ANSI_BLUE "│" ANSI_RESET " %s", buf);
|
|
|
|
|
|
int pad = tw - 1 - v;
|
|
|
|
|
|
if (pad > 0) repeat_char(' ', pad);
|
|
|
|
|
|
printf(ANSI_BLUE "│" ANSI_RESET "\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
{
|
|
|
|
|
|
char buf[80];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "FFT size, power of 2 [" ANSI_CYAN "%d" ANSI_RESET "]", FFT_SIZE_DEFAULT);
|
|
|
|
|
|
OPT("-f, --fft-size N", buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
{
|
|
|
|
|
|
char buf[80];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "Seconds to analyze [" ANSI_CYAN "%d" ANSI_RESET "]", MAX_ANALYSIS_SECS);
|
|
|
|
|
|
OPT("-d, --duration SEC", buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
OPT("-r, --recursive", "Recurse into subdirectories");
|
|
|
|
|
|
OPT("-F, --full", "Analyze entire file (no duration limit)");
|
|
|
|
|
|
OPT("-v, --verbose", "Verbose output with decision log");
|
|
|
|
|
|
OPT("-s, --visual", "Graphical spectrum TUI visualization");
|
|
|
|
|
|
OPT("-V", "Alias for " ANSI_GREEN "-s" ANSI_RESET);
|
|
|
|
|
|
OPT("-a, --auto-remove", "Automatically delete detected transcodes");
|
|
|
|
|
|
OPT("-h, --help", "Show this help screen");
|
|
|
|
|
|
|
|
|
|
|
|
#undef OPT
|
|
|
|
|
|
|
|
|
|
|
|
pn(ANSI_BLUE "╰"); repeat_str("─", tw - 2); pn(ANSI_BLUE "╯" ANSI_RESET "\n");
|
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;
|
2026-07-03 13:52:15 +02:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 13:52:15 +02:00
|
|
|
|
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;
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-07-03 13:52:15 +02:00
|
|
|
|
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 = {
|
2026-07-03 13:52:15 +02:00
|
|
|
|
.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,
|
2026-07-03 13:52:15 +02:00
|
|
|
|
.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'},
|
2026-07-03 13:52:15 +02:00
|
|
|
|
{"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;
|
2026-07-03 13:52:15 +02:00
|
|
|
|
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;
|
2026-07-03 13:52:15 +02:00
|
|
|
|
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) {
|
2026-07-03 13:52:15 +02:00
|
|
|
|
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;
|
|
|
|
|
|
}
|