From 2ec7d5ec1a2f1fd835b106675c73f39cf374fcfa Mon Sep 17 00:00:00 2001 From: Armin Date: Sat, 4 Jul 2026 19:59:09 +0200 Subject: [PATCH] enhance explanations in README, make tcd output more clear --- README.md | 475 ++++++++++++++++++++++++++++++------------------------ tcd.c | 26 +-- 2 files changed, 277 insertions(+), 224 deletions(-) diff --git a/README.md b/README.md index 5216d8b..41eb675 100644 --- a/README.md +++ b/README.md @@ -19,274 +19,322 @@ Careful! Dragons ahead! `tcd` *can* and absolutely *will* delete your data. Don't blindly use -a, and please read at least the --help information and *understand* what -a does. -How it works ------------- +--- -### 1. Signal acquisition +How it works (the short version) +-------------------------------- -The program opens the file with libavformat, selects the first audio stream, -decodes up to `--duration` (default 60) seconds of audio (or the entire file -when `--full` is used), and converts every sample to 32-bit float PCM. - -### 2. Windowing & FFT - -The decoded samples are fed through a sliding Hann window with **50 % overlap** -(the window hops by `fft_size / 2` samples). Each windowed block is transformed -to the frequency domain with a radix-2 FFT (Cooley–Tukey). Power spectra are -accumulated (sum of squared magnitudes) over all windows and all channels, then -averaged. The default FFT size is 4096 samples, giving 2048 frequency bins -from DC to Nyquist (22050 Hz at 44100 Hz sample rate). - -### 3. Metrics extracted from the average spectrum - -All of the following are computed from the *average magnitude spectrum* -`M[f] = sqrt(P[f] / N)` where `P[f]` is the accumulated power at bin `f` and -`N` is the number of windows summed. - -#### Cutoff frequency - -Searched from Nyquist downward. The **cutoff** is the highest frequency whose -magnitude is at least `N` dB below the spectral peak, where `N` is derived -from the threshold value (1–99). The value maps linearly to −40 dB (1, least -sensitive) through −60 dB (50, default) to −80 dB (99, most sensitive): - - threshold = peak × 10^(−N / 20) (linear) - cutoff = highest f where M[f] ≥ threshold (Hz) - -The `-t` parameter controls **all** detection thresholds - not just the cutoff -level. At lower values the transition bandwidth, roughness, and band-ratio -gates are looser (fewer detections, fewer false positives). At higher values -they are tighter (more detections, more false positives). The table below -shows how the thresholds scale with sensitivity: - -| -t | Sensitivity | max_bw multiplier | Roughness > | Band ratio < | Bypass @ | -|----|-------------|-------------------|-------------|--------------|----------| -| 1 | Least | ×2.0 | 0.70 / 0.53 / 0.35 | 0.85 / 0.80 | ≥1.00 | -| 50 | Default | ×1.0 | 0.40 / 0.30 / 0.20 | 0.90 / 0.85 | ≥0.99 | -| 99 | Most | ×0.25 | 0.10 / 0.08 / 0.05 | 0.95 / 0.90 | ≥0.98 | - -Lossy encoders place their lowpass cutoff somewhere below Nyquist. The exact -position depends on the codec, the bitrate, and the encoder implementation. - -#### Transition bandwidth (steepness) - -The **transition bandwidth** measures how abruptly the spectrum drops at the -cutoff. It is the frequency difference between the −20 dB point and the -−60 dB cutoff (the full transition band of the encoder's lowpass filter). - - high_thresh = peak × 10^(-20 / 20) (−20 dB) - low_thresh = peak × 10^(-60 / 20) (−60 dB) - bw = cutoff_freq_at_low − freq_of_highest_bin_above(high_thresh) - -A sharp, brick-wall-like filter (transition bandwidth < 500–4000 Hz, depending -on cutoff position) is characteristic of lossy encoding. Genuine lossless -recordings roll off naturally over many kilohertz due to microphone response, -analogue filters, and the inherent limits of the recording chain. Using the -full −20 dB to −60 dB span (rather than the narrower −40 dB to −60 dB range) -gives a more robust measurement that better separates lossy from lossless. - -#### Roughness - -The **roughness** quantifies how *irregular* the spectrum is in the transition -region (60 % to 95 % of the cutoff frequency). It is the coefficient of -variation of the magnitudes in that band: - - region = [0.60 × cutoff, 0.95 × cutoff] - mean = average(M[f]) over the region - var = average(((M[f] − mean) / mean)²) - roughness = sqrt(var) - -Lossy codecs introduce quantization noise that is unevenly distributed across -the spectrum, creating a "bumpy" transition band. Transcodes (double-encoded -files) show even higher roughness because the artifacts of two successive -encodes compound. - -#### Band ratio - -The **band ratio** is the ratio of the average magnitude in the 16–20 kHz band -to the average magnitude in the 12–16 kHz band: - - avg_high = average(M[f]) for f ∈ [16000, 20000) Hz - avg_low = average(M[f]) for f ∈ [12000, 16000) Hz - band_ratio = avg_high / (avg_low + ε) - -Lossy codecs aggressively discard energy above 16 kHz because the human ear is -relatively insensitive there. A low band ratio (< 0.85–0.90) is a strong -marker of lossy origins. - -#### Noise floor - -The **noise floor** is the average magnitude in the highest quarter of the -spectrum (75 % Nyquist → Nyquist), expressed in dB relative to the peak: - - noise_floor_db = 20 × log₁₀(avg(M[f]) / peak) for f ∈ [0.75·N, N) - -In a native lossless recording the noise floor is limited by the analogue -source or dither (typically −90 to −110 dBFS). Lossy decoding adds -quantisation noise that raises the floor to −60 to −80 dBFS. +tcd decodes your audio file, converts it to the frequency domain (like a +graph showing how much energy exists at each frequency), then measures several +properties of that frequency graph. Each property is a clue about whether the +audio was produced by a lossy encoder. Combined, these clues give a verdict. --- -Decision logic --------------- +What tcd displays and what each number means +--------------------------------------------- -The tool distinguishes two scenarios based on the codec of the input file. +Here is the example output you saw: -### A. Input is a lossy codec (mp3, aac, vorbis, opus, wma, ac3, …) +``` +File: ./08 You Got Me.mp3 +Format: mp3 +Bitrate: 320 kbps +Sample rate: 44100 Hz +Channels: 2 +Windows: 13550 +Peak: 47.5 dBFS +Cutoff: 20790 / 22050 Hz = 94.3% +Steepness: 20209 Hz +Roughness: 0.323 +Band ratio: 0.555 +Noise floor: -56.4 dB +Verdict: NATIVE +Verdict Info: bandwidth used=94.3% (20790/22050 Hz), expected≥90% for 320 kbps +``` -The cutoff is compared against the expected minimum for the file's *stated* +Each metric is explained below. + +--- + +### Cutoff (20790 / 22050 Hz = 94.3%) + +**What it is:** The highest frequency where the audio still has measurable +energy. Everything above this point is silence or noise. + +**The Nyquist ceiling:** Digital audio is made of snapshots (samples). For CD +quality (44100 snapshots per second), there is a hard limit: you cannot store +a frequency higher than half the snapshot rate = **22050 Hz**. This is called +the *Nyquist frequency*. It is a physical ceiling — higher frequencies simply +cannot exist. + +**How lossy encoding changes it:** MP3 and other lossy codecs deliberately cut +off high frequencies to save space. The cutoff gets lower as the bitrate drops: + +| Bitrate | Typical cutoff | Audio quality impact | +|---------|---------------|----------------------| +| 320 kbps | ≥20000 Hz (≥90%) | Keeps almost all audible high end | +| 256 kbps | ≥19000 Hz (≥86%) | Still very clean | +| 192 kbps | ~17500 Hz (~79%) | Moderate high-end roll-off | +| 128 kbps | ~16000 Hz (~73%) | Noticeable treble loss | +| 96 kbps | ~13000 Hz (~59%) | Significant high-end missing | +| 64 kbps | ~11000 Hz (~50%) | Sounds dull, heavily filtered | + +**What 94.3% means for your file:** 20790 / 22050 = 94.3%. The cutoff is very +close to the theoretical maximum. This is what we expect from a 320 kbps +encode. If this same file showed 54% (~12000 Hz), it would mean the treble +was chopped off by an aggressive low-bitrate encoder, and someone just +re-encoded it at 320 kbps — the cutoff is permanent and cannot be restored. +That would be an **UPSCALED** file. + +--- + +### Steepness + +**What it measures:** How abruptly the sound drops off *at* the cutoff point. +tcd measures this as the frequency gap between the −20 dB point (still loud) +and the −60 dB cutoff (essentially silent). A narrow gap = a sharp drop. + +**The analogy:** Imagine the frequency graph as a mountain ridge. A lossless +recording rolls off like a natural hillside — gradual, smooth, taking +thousands of Hz to go from loud to silent. A lossy encoder's lowpass filter +creates a cliff — a near-vertical drop from audible signal to nothing. + +**What the number means:** Steepness is the width (in Hz) of that drop zone. +The smaller the number, the sharper the cliff: + +| Steepness | What it looks like | Likely origin | +|-----------|-------------------|---------------| +| <500 Hz | Brick-wall drop | Lossy encoder (MP3, AAC) | +| 500–2000 Hz | Fairly sharp | Could be lossy or aggressive production filter | +| 2000–5000 Hz | Moderate | Might be natural | +| >5000 Hz | Gentle slope | Natural acoustic roll-off (lossless) | + +To understand steepness, imagine a guitar string being plucked. The sound +naturally fades across many frequencies — the harmonics near the top end of +your hearing get quieter and quieter over a broad range. This is a gentle +slope. Now imagine someone put a pair of scissors on the frequency spectrum +and cut everything above a certain note. That sharp edge — the difference +between "still audible" and "completely gone" in just a few hundred Hz — is +what lossy compression does. The steepness number tells you how sharp that +scissor cut was. + +--- + +### Roughness (0.323) + +**What it measures:** How "bumpy" or "irregular" the spectrum looks just +before the cutoff point. + +**The analogy:** Lossy encoding introduces quantization noise — tiny +rounding errors that are unevenly distributed across frequencies. In the +frequency graph, this looks like a jagged, bumpy line instead of a smooth +one. Think of it like a dirt road vs a paved highway: lossless audio is +smooth, lossy audio is bumpy. Double-encoded audio (a transcode) is even +bumpier because the errors from two encodings stack on top of each other. + +**What the number means:** + +| Roughness | What it looks like | Likely origin | +|-----------|-------------------|---------------| +| <0.15 | Very smooth | Natural/lossless | +| 0.15–0.30 | Slightly bumpy | Could be lossy single encode | +| 0.30–0.50 | Clearly bumpy | Lossy single encode, or borderline transcode | +| >0.50 | Very jagged | Almost certainly a transcode | + +--- + +### Band ratio (0.555) + +**What it measures:** How much high-frequency energy (16–20 kHz) remains +compared to mid-high energy (12–16 kHz). + +**Why it matters:** Human hearing is least sensitive above 16 kHz. Lossy +encoders exploit this by spending almost no bits on those frequencies. The +result is that the 16–20 kHz region is much quieter than the 12–16 kHz region. +In native recordings, this drop is modest; in lossy/transcoded material, it +is severe. + +**What the number means:** Band ratio = energy in 16–20 kHz band ÷ energy in +12–16 kHz band. A ratio of 1.0 means both bands are equally loud. A ratio of +0.5 means the top band is half as loud. + +| Band ratio | What it means | +|------------|---------------| +| >0.85 | Healthy high end — likely native lossless | +| 0.70–0.85 | Mild roll-off — could be lossy or natural | +| 0.50–0.70 | Significant high-end loss — likely lossy | +| <0.50 | Severe high-end loss — almost certainly lossy or transcoded | + +--- + +### Noise floor (-56.4 dB) + +**What it measures:** The average noise level in the highest quarter of the +frequency range (roughly 16500–22050 Hz). + +**The analogy:** Imagine listening in a quiet room — the background hiss is +very low. Now imagine that same room with a fan running — the background +noise rises. A lossy encoder introduces quantization noise that raises the +"background hiss" in the high frequencies. + +**What the number means:** This is measured in decibels (dB). More negative = +quieter (better). Less negative = noisier (worse): + +| Noise floor | What it means | +|-------------|---------------| +| −90 to −110 dB | Very clean — native lossless | +| −70 to −90 dB | Moderate — could be lossy or quiet lossless | +| −50 to −70 dB | Noisy — likely lossy | +| >−50 dB | Very noisy — almost certainly lossy or transcoded | + +--- + +How tcd combines these clues into a verdict +--------------------------------------------- + +tcd does not rely on any single metric. It combines them in stages, like a +detective building a case. + +### Scenario 1: The input file is lossy (MP3, AAC, etc.) + +The file already claims to be lossy. The question is: was it *originally* +encoded at the stated bitrate, or was it re-encoded from a lower bitrate? + +**The check:** tcd compares the cutoff against what is expected for that bitrate: -| Stated bitrate | Expected cutoff ratio | -|------------------|----------------------| -| < 192 kbps | ≥ 0.75 of Nyquist | -| 192–255 kbps | ≥ 0.85 of Nyquist | -| ≥ 256 kbps | ≥ 0.90 of Nyquist | +| Stated bitrate | Expected cutoff | +|----------------|-----------------| +| <192 kbps | ≥75% of Nyquist | +| 192–255 kbps | ≥85% of Nyquist | +| ≥256 kbps | ≥90% of Nyquist | -If the measured cutoff is **more than 8 percentage points below** the expected -minimum, the file is classified as **UPSCALED** (a lower-bitrate encode that -was decoded and re-encoded at a higher bitrate). Otherwise it is **NATIVE** -(a single, genuine encode at the stated bitrate). +If the actual cutoff is **more than 8 percentage points lower** than expected, +the file is **UPSCALED**. For example, a file claiming 320 kbps (expecting +≥90%) but showing a cutoff of 70% (≈15400 Hz) would be flagged as upscaled +from ~96 kbps. -### B. Input is a lossless codec (flac, pcm, alac, wavpack, …) +Otherwise it is **NATIVE** — a genuine single encode at this bitrate. -The tool applies two layers of criteria. +### Scenario 2: The input file is lossless (FLAC, WAV, ALAC, etc.) -#### Primary criteria (cutoff + transition bandwidth) +The file claims to be lossless. The question is: was it actually created by +decoding a lossy file and re-encoding to lossless? -The transition bandwidth (from −20 dB to −60 dB) is compared against a -cutoff-dependent threshold. A narrower bandwidth than the threshold indicates -a lossy encoder's brickwall filter: +tcd uses a **two-layer** check: -| Cutoff ratio range | Max transition bandwidth | Interpretation | -|-------------------|-------------------------|---------------| -| < 0.50 | 4000 Hz | Transcode | -| < 0.70 | 3000 Hz | Transcode | -| < 0.80 | 2000 Hz | Transcode | -| < 0.90 | 1200 Hz | Transcode | -| ≥ 0.90 | 500 Hz | Transcode | +**Layer 1 — Cutoff + Steepness (primary):** -This graduated approach avoids the earlier problem of rigid breakpoints that -could miss files with moderate cutoffs but wider-than-expected transition -bands, or files with cutoffs just above a hard threshold (e.g. 21 kHz / -44.1 kHz = 0.952, previously missed by a strict `< 0.95` check). +| Cutoff range | Max steepness allowed | If exceeded → | +|-------------|----------------------|---------------| +| <50% of Nyquist | 4000 Hz | TRANSCODE | +| 50–70% | 3000 Hz | TRANSCODE | +| 70–80% | 2000 Hz | TRANSCODE | +| 80–90% | 1200 Hz | TRANSCODE | +| ≥90% | 500 Hz | TRANSCODE | -The combination of a low cutoff and a sharp roll-off is the strongest -indicator. A cutoff below 50 % of Nyquist (e.g. 11 kHz at 44.1 kHz sampling) -is *impossible* for a modern lossless recording and always indicates a -transcode. +This works because lossy cutoffs are always sharp (low steepness). A lossless +recording that happens to have a low cutoff (e.g., a muddy recording with +little treble) would still have a *gradual* roll-off (high steepness) — you +need both a low cutoff **and** a sharp drop to convict. -#### Secondary criteria (roughness + band ratio) +**Layer 2 — Roughness + Band ratio (secondary):** -If the primary criteria do not match but the cutoff is above 80 % of Nyquist -(the region where lossy cutoffs can approach the lossless range), the tool -falls back to roughness and band ratio: +If Layer 1 did not trigger but the cutoff is above 80%, tcd checks roughness +and band ratio. This catches transcodes where the cutoff happens to be high +enough to pass Layer 1 but the spectrum is still bumpy and depleted in the +top band: -| Roughness | Band ratio | Interpretation | -|-----------|----------------|----------------| -| > 0.40 | any | Transcode | -| > 0.30 | < 0.90 | Transcode | -| > 0.20 | < 0.85 | Transcode | +| Roughness | Band ratio | If matched → | +|-----------|------------|--------------| +| >0.40 | any | TRANSCODE | +| >0.30 | <0.90 | TRANSCODE | +| >0.20 | <0.85 | TRANSCODE | -If no primary or secondary criterion matches, the file is classified as -**GENUINE** (native lossless). +If neither layer triggers, the file is **GENUINE** (native lossless). -### C. Confidence score +--- + +### The verdicts at a glance + +| Verdict | Input codec | What it means | +|---------|------------|---------------| +| **NATIVE** | lossy | Encoded once at the stated bitrate — genuine | +| **UPSCALED** | lossy | Originally encoded at a lower bitrate, then re-encoded higher | +| **GENUINE** | lossless | Appears to be native lossless — no evidence of lossy origin | +| **TRANSCODE** | lossless | Originated from a lossy source, decoded to lossless | +| **SILENT** | any | No detectable audio content | + +--- + +Confidence score +---------------- A continuous **confidence** (0–100 %) is computed using the same metrics with a sliding scale, providing a graded measure of how certain the tool is about its verdict. -### D. Auto-remove mode (`-a`) +--- + +Auto-remove mode (`-a`) +------------------------ When `-a` is passed, any file that is not classified as NATIVE or GENUINE is automatically deleted after analysis. This is useful for batch cleanup of -corrupt or transcoded libraries. Careful - that this will eat data. +corrupt or transcoded libraries. Careful — this will eat data. --- Why the method is (somewhat!) scientifically reliable ------------------------------------------- +------------------------------------------------------ ### 1. Lossy encoding leaves a permanent spectral fingerprint -Every lossy audio codec works by discarding information that psychoacoustic -models deem inaudible. The most universal form of this discarding is a -**lowpass filter** applied before encoding. Once the filter has been applied, -the information above the cutoff is gone forever. Decoding back to PCM and -re-encoding to lossless (FLAC, ALAC, WAV) cannot restore it. +Every lossy audio codec discards information. The most obvious form is a +**lowpass filter** — once applied, the frequencies above the cutoff are gone +forever. Decoding back to PCM and re-encoding to lossless cannot restore them. +This means a "lossless" FLAC file made from an MP3 will contain the MP3's +permanent spectral cutoff. -This means a "lossless" FLAC file that was created by decoding an MP3 and -re-compressing will contain the MP3's permanent spectral cutoff. The cutoff -and its steepness are physically embedded in the audio data and are detectable -by spectral analysis. +### 2. Steepness catches the filter shape -Multiple independent studies in the audio forensics community have confirmed -that frequency-domain analysis of cutoffs is a reliable method for identifying -lossy-sourced lossless files (see e.g. the work on "MP3Cut" and similar -tools). +Lossy encoders use sharp digital filters (brick-wall style) that drop from +audible to silent in a few hundred Hz. Natural acoustic sources (voice, +instruments, room ambience) roll off gradually over many kHz. A drop steeper +than 2 kHz at any cutoff position is extremely unlikely to occur naturally. -### 2. The steepness metric catches the filter topology +- **MP3 (ISO/IEC 11172-3):** typical roll-off of several hundred Hz to ~2 kHz +- **AAC (ISO/IEC 13818-7):** sharper, often 200–800 Hz +- **Vorbis:** variable but always steeper than natural -Lossy encoders use FIR or hybrid filterbanks with a characteristic roll-off -slope. The steepness measurement directly captures the *order* and *design* -of that filter: +### 3. Roughness detects double-encoding noise -- **MP3 (ISO/IEC 11172-3)** uses a hybrid polyphase/MDCT filterbank with a - typical roll-off of several hundred Hz to about 2 kHz, depending on the - bitrate and encoder implementation (LAME, Fraunhofer, etc.). -- **AAC (ISO/IEC 13818-7)** uses a pure MDCT with a sharper transition, - often 200–800 Hz. -- **Vorbis** uses a Bark-scale filterbank with variable steepness that is - still always measurably steeper than a natural acoustic roll-off. - -Natural acoustic sources (voice, instruments, room ambience) roll off -gradually over many kilohertz. A roll-off steeper than 2 kHz at any cutoff -position is extremely unlikely to occur naturally. - -### 3. Roughness detects compound quantization noise - -When audio is lossy-encoded, quantization noise is added in every scale-factor -band. The noise distribution is not flat; it is shaped by the psychoacoustic -model to be masked by nearby tonal components. When the audio is decoded and -re-encoded, a *second* layer of noise-shaping is applied, creating +When audio is lossy-encoded, quantization noise is shaped to be masked by the +music. Re-encoding adds a *second* layer of noise-shaping, creating irregularities in the spectrum that are statistically unlikely in a single encode. -The roughness metric measures this irregularity as the normalized standard -deviation of the magnitude in the transition band. Values above 0.20–0.40 -(calibrated on a large corpus of known-native and known-transcoded files) are -highly specific to transcodes. - ### 4. Band ratio exploits the Fletcher–Munson curves Human hearing is least sensitive above 16 kHz. Lossy encoders exploit this by -allocating very few bits to the 16–20 kHz region, resulting in a sharp drop in -energy there. The band ratio metric captures this drop. In native recordings -the 16–20 kHz region is typically only 2–6 dB quieter than the 12–16 kHz -region (band ratio 0.5–1.0). In transcoded material it is often 10–20 dB -quieter (band ratio < 0.3). +spending almost no bits there. In native recordings, the 16–20 kHz region is +typically only 2–6 dB quieter than 12–16 kHz (band ratio 0.5–1.0). In +transcoded material it is often 10–20 dB quieter (band ratio < 0.3). ### 5. Multiple independent metrics prevent false positives -No single metric is perfectly reliable on its own. A low cutoff could -theoretically occur in a genuine recording that used an aggressive lowpass -filter during production. By requiring **both** a low cutoff **and** a steep -roll-off (primary criteria), or **both** high roughness **and** a low band -ratio (secondary criteria), the tool achieves high specificity. - -The secondary criteria are activated *only* when the primary criteria fail and -the cutoff is above 85 % of Nyquist, which is the region where false positives -are most likely. This hierarchical approach ensures that borderline cases are -not misclassified. +No single metric is perfectly reliable. A low cutoff could occur in a genuine +recording that used an aggressive lowpass during mastering. By requiring +**both** a low cutoff **and** a sharp roll-off (primary), or **both** high +roughness **and** a low band ratio (secondary), the tool avoids false +positives. ### 6. The upscaling detector is conservative -For lossy files, the expected cutoff is computed from the file's *declared* -bitrate. A margin of 8 percentage points is subtracted before flagging a file -as upscaled. This margin accounts for encoder variability (different LAME -presets, AAC profiles, etc.) and prevents false positives on legitimate -high-quality encodes that simply use a conservative lowpass. +For lossy files, an 8-percentage-point margin is subtracted before flagging a +file as upscaled. This accounts for encoder variability and prevents false +positives on legitimate encodes that simply use a conservative lowpass. --- @@ -352,4 +400,3 @@ References 1999 (Fletcher–Munson equal-loudness contours). - Lerch, A. - *An Introduction to Audio Content Analysis*, Wiley, 2012 (spectral features for audio forensics). - diff --git a/tcd.c b/tcd.c index 4bc68f1..ecb27d8 100644 --- a/tcd.c +++ b/tcd.c @@ -616,19 +616,24 @@ static void render_visual_spectrum( 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 info_len = snprintf(title, sizeof(title), " %s | %s | %d Hz | %d ch | %lldk", + short_name, fmt_name, sample_rate, channels, (long long)(bitrate / 1000)); int total_w = term_w; if (info_len > total_w) { char truncated[256]; - snprintf(truncated, sizeof(truncated), " %s | %d Hz | %d ch", - short_name, sample_rate, channels); + snprintf(truncated, sizeof(truncated), " %s | %d Hz | %d ch | %lldk", + short_name, sample_rate, channels, (long long)(bitrate / 1000)); if ((int)strlen(truncated) > total_w) { - snprintf(truncated, sizeof(truncated), " %s | %d ch", short_name, channels); + snprintf(truncated, sizeof(truncated), " %s | %d ch | %lldk", + short_name, channels, (long long)(bitrate / 1000)); if ((int)strlen(truncated) > total_w) { - snprintf(truncated, sizeof(truncated), " %s", short_name); + snprintf(truncated, sizeof(truncated), " %s | %lldk", + short_name, (long long)(bitrate / 1000)); if ((int)strlen(truncated) > total_w) { - truncated[total_w] = '\0'; + snprintf(truncated, sizeof(truncated), " %s", short_name); + if ((int)strlen(truncated) > total_w) { + truncated[total_w] = '\0'; + } } } } @@ -1294,7 +1299,7 @@ static int process_file(const char *filename, const Options *opts) 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 "Cutoff:" ANSI_RESET " %.0f / %.0f Hz = %.1f%%\n", cutoff_hz, nyquist, 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); @@ -1329,8 +1334,9 @@ static int process_file(const char *filename, const Options *opts) } else { printf(ANSI_GREEN "NATIVE" ANSI_RESET " (single encode at this bitrate)\n"); } - printf(ANSI_BOLD ANSI_CYAN "Verdict Info:" ANSI_RESET " cutoff=%.1f%% Nyquist, expected≥%.0f%% for %lld kbps", - 100.0 * cutoff_ratio, 100.0 * expected_min, + printf(ANSI_BOLD ANSI_CYAN "Verdict Info:" ANSI_RESET " bandwidth used=%.1f%% (%.0f/%.0f Hz), expected≥%.0f%% for %lld kbps", + 100.0 * cutoff_ratio, effective_cutoff, nyquist, + 100.0 * expected_min, bitrate > 0 ? (long long)(bitrate / 1000) : 0); if (upscaled) { printf(" → suggests ");