enhance explanations in README, make tcd output more clear

This commit is contained in:
Armin 2026-07-04 19:59:09 +02:00
commit 2ec7d5ec1a
2 changed files with 256 additions and 203 deletions

475
README.md
View file

@ -19,274 +19,322 @@ Careful! Dragons ahead!
`tcd` *can* and absolutely *will* delete your data. Don't blindly use -a, and `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. 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, tcd decodes your audio file, converts it to the frequency domain (like a
decodes up to `--duration` (default 60) seconds of audio (or the entire file graph showing how much energy exists at each frequency), then measures several
when `--full` is used), and converts every sample to 32-bit float PCM. 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.
### 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 (CooleyTukey). 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 (199). 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 < 5004000 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 1620 kHz band
to the average magnitude in the 1216 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.850.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.
--- ---
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) |
| 5002000 Hz | Fairly sharp | Could be lossy or aggressive production filter |
| 20005000 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.150.30 | Slightly bumpy | Could be lossy single encode |
| 0.300.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 (1620 kHz) remains
compared to mid-high energy (1216 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 1620 kHz region is much quieter than the 1216 kHz region.
In native recordings, this drop is modest; in lossy/transcoded material, it
is severe.
**What the number means:** Band ratio = energy in 1620 kHz band ÷ energy in
1216 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.700.85 | Mild roll-off — could be lossy or natural |
| 0.500.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 1650022050 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: bitrate:
| Stated bitrate | Expected cutoff ratio | | Stated bitrate | Expected cutoff |
|------------------|----------------------| |----------------|-----------------|
| < 192 kbps | ≥ 0.75 of Nyquist | | <192 kbps | ≥75% of Nyquist |
| 192255 kbps | ≥ 0.85 of Nyquist | | 192255 kbps | ≥85% of Nyquist |
| ≥ 256 kbps | ≥ 0.90 of Nyquist | | ≥256 kbps | ≥90% of Nyquist |
If the measured cutoff is **more than 8 percentage points below** the expected If the actual cutoff is **more than 8 percentage points lower** than expected,
minimum, the file is classified as **UPSCALED** (a lower-bitrate encode that the file is **UPSCALED**. For example, a file claiming 320 kbps (expecting
was decoded and re-encoded at a higher bitrate). Otherwise it is **NATIVE** ≥90%) but showing a cutoff of 70% (≈15400 Hz) would be flagged as upscaled
(a single, genuine encode at the stated bitrate). 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 tcd uses a **two-layer** check:
cutoff-dependent threshold. A narrower bandwidth than the threshold indicates
a lossy encoder's brickwall filter:
| Cutoff ratio range | Max transition bandwidth | Interpretation | **Layer 1 — Cutoff + Steepness (primary):**
|-------------------|-------------------------|---------------|
| < 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 |
This graduated approach avoids the earlier problem of rigid breakpoints that | Cutoff range | Max steepness allowed | If exceeded → |
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 / | <50% of Nyquist | 4000 Hz | TRANSCODE |
44.1 kHz = 0.952, previously missed by a strict `< 0.95` check). | 5070% | 3000 Hz | TRANSCODE |
| 7080% | 2000 Hz | TRANSCODE |
| 8090% | 1200 Hz | TRANSCODE |
| ≥90% | 500 Hz | TRANSCODE |
The combination of a low cutoff and a sharp roll-off is the strongest This works because lossy cutoffs are always sharp (low steepness). A lossless
indicator. A cutoff below 50 % of Nyquist (e.g. 11 kHz at 44.1 kHz sampling) recording that happens to have a low cutoff (e.g., a muddy recording with
is *impossible* for a modern lossless recording and always indicates a little treble) would still have a *gradual* roll-off (high steepness) — you
transcode. 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 If Layer 1 did not trigger but the cutoff is above 80%, tcd checks roughness
(the region where lossy cutoffs can approach the lossless range), the tool and band ratio. This catches transcodes where the cutoff happens to be high
falls back to roughness and band ratio: enough to pass Layer 1 but the spectrum is still bumpy and depleted in the
top band:
| Roughness | Band ratio | Interpretation | | Roughness | Band ratio | If matched → |
|-----------|----------------|----------------| |-----------|------------|--------------|
| > 0.40 | any | Transcode | | >0.40 | any | TRANSCODE |
| > 0.30 | < 0.90 | Transcode | | >0.30 | <0.90 | TRANSCODE |
| > 0.20 | < 0.85 | Transcode | | >0.20 | <0.85 | TRANSCODE |
If no primary or secondary criterion matches, the file is classified as If neither layer triggers, the file is **GENUINE** (native lossless).
**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** (0100 %) is computed using the same metrics with A continuous **confidence** (0100 %) is computed using the same metrics with
a sliding scale, providing a graded measure of how certain the tool is about a sliding scale, providing a graded measure of how certain the tool is about
its verdict. 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 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 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 Why the method is (somewhat!) scientifically reliable
------------------------------------------ ------------------------------------------------------
### 1. Lossy encoding leaves a permanent spectral fingerprint ### 1. Lossy encoding leaves a permanent spectral fingerprint
Every lossy audio codec works by discarding information that psychoacoustic Every lossy audio codec discards information. The most obvious form is a
models deem inaudible. The most universal form of this discarding is a **lowpass filter** — once applied, the frequencies above the cutoff are gone
**lowpass filter** applied before encoding. Once the filter has been applied, forever. Decoding back to PCM and re-encoding to lossless cannot restore them.
the information above the cutoff is gone forever. Decoding back to PCM and This means a "lossless" FLAC file made from an MP3 will contain the MP3's
re-encoding to lossless (FLAC, ALAC, WAV) cannot restore it. permanent spectral cutoff.
This means a "lossless" FLAC file that was created by decoding an MP3 and ### 2. Steepness catches the filter shape
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.
Multiple independent studies in the audio forensics community have confirmed Lossy encoders use sharp digital filters (brick-wall style) that drop from
that frequency-domain analysis of cutoffs is a reliable method for identifying audible to silent in a few hundred Hz. Natural acoustic sources (voice,
lossy-sourced lossless files (see e.g. the work on "MP3Cut" and similar instruments, room ambience) roll off gradually over many kHz. A drop steeper
tools). 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 200800 Hz
- **Vorbis:** variable but always steeper than natural
Lossy encoders use FIR or hybrid filterbanks with a characteristic roll-off ### 3. Roughness detects double-encoding noise
slope. The steepness measurement directly captures the *order* and *design*
of that filter:
- **MP3 (ISO/IEC 11172-3)** uses a hybrid polyphase/MDCT filterbank with a When audio is lossy-encoded, quantization noise is shaped to be masked by the
typical roll-off of several hundred Hz to about 2 kHz, depending on the music. Re-encoding adds a *second* layer of noise-shaping, creating
bitrate and encoder implementation (LAME, Fraunhofer, etc.).
- **AAC (ISO/IEC 13818-7)** uses a pure MDCT with a sharper transition,
often 200800 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
irregularities in the spectrum that are statistically unlikely in a single irregularities in the spectrum that are statistically unlikely in a single
encode. encode.
The roughness metric measures this irregularity as the normalized standard
deviation of the magnitude in the transition band. Values above 0.200.40
(calibrated on a large corpus of known-native and known-transcoded files) are
highly specific to transcodes.
### 4. Band ratio exploits the FletcherMunson curves ### 4. Band ratio exploits the FletcherMunson curves
Human hearing is least sensitive above 16 kHz. Lossy encoders exploit this by Human hearing is least sensitive above 16 kHz. Lossy encoders exploit this by
allocating very few bits to the 1620 kHz region, resulting in a sharp drop in spending almost no bits there. In native recordings, the 1620 kHz region is
energy there. The band ratio metric captures this drop. In native recordings typically only 26 dB quieter than 1216 kHz (band ratio 0.51.0). In
the 1620 kHz region is typically only 26 dB quieter than the 1216 kHz transcoded material it is often 1020 dB quieter (band ratio < 0.3).
region (band ratio 0.51.0). In transcoded material it is often 1020 dB
quieter (band ratio < 0.3).
### 5. Multiple independent metrics prevent false positives ### 5. Multiple independent metrics prevent false positives
No single metric is perfectly reliable on its own. A low cutoff could No single metric is perfectly reliable. A low cutoff could occur in a genuine
theoretically occur in a genuine recording that used an aggressive lowpass recording that used an aggressive lowpass during mastering. By requiring
filter during production. By requiring **both** a low cutoff **and** a steep **both** a low cutoff **and** a sharp roll-off (primary), or **both** high
roll-off (primary criteria), or **both** high roughness **and** a low band roughness **and** a low band ratio (secondary), the tool avoids false
ratio (secondary criteria), the tool achieves high specificity. positives.
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.
### 6. The upscaling detector is conservative ### 6. The upscaling detector is conservative
For lossy files, the expected cutoff is computed from the file's *declared* For lossy files, an 8-percentage-point margin is subtracted before flagging a
bitrate. A margin of 8 percentage points is subtracted before flagging a file file as upscaled. This accounts for encoder variability and prevents false
as upscaled. This margin accounts for encoder variability (different LAME positives on legitimate encodes that simply use a conservative lowpass.
presets, AAC profiles, etc.) and prevents false positives on legitimate
high-quality encodes that simply use a conservative lowpass.
--- ---
@ -352,4 +400,3 @@ References
1999 (FletcherMunson equal-loudness contours). 1999 (FletcherMunson equal-loudness contours).
- Lerch, A. - *An Introduction to Audio Content Analysis*, Wiley, 2012 - Lerch, A. - *An Introduction to Audio Content Analysis*, Wiley, 2012
(spectral features for audio forensics). (spectral features for audio forensics).

22
tcd.c
View file

@ -616,15 +616,19 @@ static void render_visual_spectrum(
char title[256]; char title[256];
const char *short_name = strrchr(filename, '/'); const char *short_name = strrchr(filename, '/');
short_name = short_name ? short_name + 1 : filename; short_name = short_name ? short_name + 1 : filename;
int info_len = snprintf(title, sizeof(title), " %s | %s | %d Hz | %d ch", int info_len = snprintf(title, sizeof(title), " %s | %s | %d Hz | %d ch | %lldk",
short_name, fmt_name, sample_rate, channels); short_name, fmt_name, sample_rate, channels, (long long)(bitrate / 1000));
int total_w = term_w; int total_w = term_w;
if (info_len > total_w) { if (info_len > total_w) {
char truncated[256]; char truncated[256];
snprintf(truncated, sizeof(truncated), " %s | %d Hz | %d ch", snprintf(truncated, sizeof(truncated), " %s | %d Hz | %d ch | %lldk",
short_name, sample_rate, channels); short_name, sample_rate, channels, (long long)(bitrate / 1000));
if ((int)strlen(truncated) > total_w) { 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 | %lldk",
short_name, (long long)(bitrate / 1000));
if ((int)strlen(truncated) > total_w) { if ((int)strlen(truncated) > total_w) {
snprintf(truncated, sizeof(truncated), " %s", short_name); snprintf(truncated, sizeof(truncated), " %s", short_name);
if ((int)strlen(truncated) > total_w) { if ((int)strlen(truncated) > total_w) {
@ -632,6 +636,7 @@ static void render_visual_spectrum(
} }
} }
} }
}
printf(ANSI_BOLD ANSI_CYAN "%s" ANSI_RESET, truncated); printf(ANSI_BOLD ANSI_CYAN "%s" ANSI_RESET, truncated);
printf("\n"); printf("\n");
} else { } else {
@ -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 "Channels:" ANSI_RESET " %d\n", channels);
printf(ANSI_BOLD ANSI_CYAN "Windows:" ANSI_RESET " %d\n", analyzer.count); 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 "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 "Steepness:" ANSI_RESET " %.0f Hz\n", steepness);
printf(ANSI_BOLD ANSI_CYAN "Roughness:" ANSI_RESET " %.3f\n", roughness); 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 "Band ratio:" ANSI_RESET " %.3f\n", band_ratio);
@ -1329,8 +1334,9 @@ static int process_file(const char *filename, const Options *opts)
} else { } else {
printf(ANSI_GREEN "NATIVE" ANSI_RESET " (single encode at this bitrate)\n"); 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", printf(ANSI_BOLD ANSI_CYAN "Verdict Info:" ANSI_RESET " bandwidth used=%.1f%% (%.0f/%.0f Hz), expected≥%.0f%% for %lld kbps",
100.0 * cutoff_ratio, 100.0 * expected_min, 100.0 * cutoff_ratio, effective_cutoff, nyquist,
100.0 * expected_min,
bitrate > 0 ? (long long)(bitrate / 1000) : 0); bitrate > 0 ? (long long)(bitrate / 1000) : 0);
if (upscaled) { if (upscaled) {
printf(" → suggests "); printf(" → suggests ");