From 1bae8156bbedd64db3894aea5c0539297131f673 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Fri, 25 Sep 2026 19:57:44 +0700 Subject: [PATCH] fix: avoid inferring Hi-Res upsampling from bandwidth alone --- .../crates/core/src/media/hires/evidence.rs | 30 +-- .../crates/core/src/media/hires/mod.rs | 215 +++++++++++------- .../crates/core/src/media/hires/tests.rs | 131 +++++++++-- 3 files changed, 254 insertions(+), 122 deletions(-) diff --git a/rust_backend/crates/core/src/media/hires/evidence.rs b/rust_backend/crates/core/src/media/hires/evidence.rs index 6be73958..26e16f7d 100644 --- a/rust_backend/crates/core/src/media/hires/evidence.rs +++ b/rust_backend/crates/core/src/media/hires/evidence.rs @@ -5,19 +5,17 @@ //! separate LOSSLESS copy preserves the whole file. //! //! - Integer-ratio upsampling artifacts (sample-and-hold, linear -//! interpolation) and spectral imaging are exact fingerprints: "certain". +//! interpolation) give strong evidence. Spectral imaging is a heuristic. //! - A brickwall right at a CD/DAT Nyquist (22.05 / 24 kHz) means a //! 44.1/48 kHz chain. With an in-band noise floor no lower than 16-bit //! quantization noise, a CD-derived source is "likely". This remains a //! heuristic and does not authorize automatic replacement. -//! - Anything else that fails the cutoff test may be a genuine master: -//! "suspect", never replaced automatically. +//! - A limited bandwidth alone does not establish a file's provenance. use super::fft::Radix2Fft; pub const CONFIDENCE_CERTAIN: &str = "certain"; pub const CONFIDENCE_LIKELY: &str = "likely"; -pub const CONFIDENCE_SUSPECT: &str = "suspect"; pub const ARTIFACT_SAMPLE_HOLD: &str = "sample_hold"; pub const ARTIFACT_INTERPOLATION: &str = "linear_interpolation"; @@ -82,9 +80,9 @@ pub struct StftStats { pub music_band_spreads: Vec, } -/// Matches `np.abs(librosa.stft(y, n_fft)).mean(axis=1)` for the averaged -/// spectrum: periodic Hann window, hop n_fft/4, frames centred by -/// zero-padding n_fft/2 at both ends. +/// Averaged channel spectrum with a periodic Hann window and hop n_fft/4. +/// Only complete frames are used: padding a cropped segment with zeros +/// introduces an artificial discontinuity and broadband spectral leakage. pub fn analyze_stft( y: &[f32], n_fft: usize, @@ -99,11 +97,10 @@ pub fn analyze_stft( quiet_floor_var: f64::NAN, music_band_spreads: Vec::new(), }; - let padded_len = y.len() + 2 * half; - if padded_len < n_fft { + if y.len() < n_fft { return Ok(stats); } - let frame_count = 1 + (padded_len - n_fft) / hop; + let frame_count = 1 + (y.len() - n_fft) / hop; let window: Vec = (0..n_fft) .map(|i| 0.5 - 0.5 * (2.0 * std::f64::consts::PI * i as f64 / n_fft as f64).cos()) @@ -142,14 +139,9 @@ pub fn analyze_stft( check()?; } // Index into y of the frame's first sample. - let start = (f * hop) as isize - half as isize; + let start = f * hop; for (i, (r, w)) in re.iter_mut().zip(&window).enumerate() { - let j = start + i as isize; - *r = if j >= 0 && (j as usize) < y.len() { - f64::from(y[j as usize]) * w - } else { - 0.0 - }; + *r = f64::from(y[start + i]) * w; } im.fill(0.0); fft.transform(&mut re, &mut im); @@ -157,9 +149,7 @@ pub fn analyze_stft( *avg += re[k].hypot(im[k]); } - // Frames that overlap the zero padding would read as quiet for the - // wrong reason; only frames fully inside the signal are candidates. - if start < 0 || start as usize + n_fft > y.len() || band_high <= band_low { + if band_high <= band_low { continue; } band_power.clear(); diff --git a/rust_backend/crates/core/src/media/hires/mod.rs b/rust_backend/crates/core/src/media/hires/mod.rs index 3b5ab8ae..eab9bc6a 100644 --- a/rust_backend/crates/core/src/media/hires/mod.rs +++ b/rust_backend/crates/core/src/media/hires/mod.rs @@ -1,20 +1,19 @@ -//! Hi-Res authenticity check, in lockstep with SpotiFLAC-Module-Version's -//! `core/hires_check.py`: same thresholds, same verdicts. +//! Sampled Hi-Res analysis. A spectral roll-off is an observation, not proof +//! that a master was upsampled or that a replacement preserves its audio. //! //! A file can claim Hi-Res along two independent axes, and each is checked on //! its own terms: //! -//! - Sample rate: it declares e.g. 96 kHz but its spectral content stops at, -//! or just above, the ~22.05 kHz Nyquist of a 44.1 kHz source, the -//! fingerprint of upsampling. Measuring it is a heuristic. +//! - Sample rate: spectral roll-off, noise floor and interpolation patterns +//! can suggest a lower-rate source. A cutoff alone is not a fingerprint. //! - Bit depth: it declares 24-bit but only 16 of those bits ever carry data, //! the low 8 being zero in every sample: a CD master padded out. Unlike the //! spectral test this one is exact, and it is the only test that can judge //! a 24-bit/44.1 kHz file. //! //! The cutoff alone cannot tell an upsampled CD from a genuine master that -//! was low-pass filtered in mastering, so every `fake_hires` verdict is graded -//! ("certain", "likely", "suspect") by the evidence in `evidence`. These +//! was low-pass filtered in mastering; that finding is `band_limited`. +//! `fake_hires` requires additional evidence ("certain" or "likely"). These //! confidence levels describe the sampled evidence, not permission to discard //! audio. Replacement additionally requires a full, exact PCM comparison. //! @@ -36,6 +35,7 @@ use std::io::{BufReader, Read, Seek, SeekFrom}; pub const VERDICT_FAKE: &str = "fake_hires"; pub const VERDICT_STANDARD: &str = "standard_definition"; pub const VERDICT_GENUINE: &str = "genuine_hires"; +pub const VERDICT_BAND_LIMITED: &str = "band_limited"; pub const VERDICT_INCONCLUSIVE: &str = "inconclusive"; #[derive(Debug, PartialEq, Eq)] @@ -71,10 +71,8 @@ pub struct HiResCheckOptions { pub noise_floor_db: f64, /// Sample rate above which a file claims Hi-Res. pub hires_sample_rate_threshold: i64, - /// Minimum active-content cutoff a genuine Hi-Res file must reach. Sits - /// well above 22.05 kHz on purpose: a resampler upsampling from CD leaves - /// a transition-band tail a couple of kHz wide (a measured 44.1 -> 176.4 - /// kHz upsample reached ~24.7 kHz). + /// Threshold for reporting limited bandwidth, including transition-band + /// tails above CD bandwidth. This is never a requirement for authenticity. pub hires_cutoff_threshold_hz: f64, /// STFT window size; must be a power of two. Shrunk for short segments. pub n_fft: i64, @@ -108,9 +106,8 @@ pub struct HiResCheckResult { pub effective_bit_depth: u32, /// Why the file was flagged, in one clause; empty when it was not. pub reason: String, - /// How sure a fake_hires verdict is: "certain" (exact fingerprint), - /// "likely" (a resampler's cliff over a 16-bit floor) or "suspect" (may be - /// a genuine low-pass filtered master). Empty for other verdicts. + /// Strength of the sampled evidence: "certain" (padding or integer + /// pattern), "likely" (spectral evidence). Empty for other verdicts. pub confidence: String, /// Exact upsampling fingerprint found, if any: "sample_hold", /// "linear_interpolation" or "imaging". @@ -164,16 +161,16 @@ impl HiResCheckResult { } } -/// One decoded segment: the mono signal for the spectrum and the OR of every -/// raw sample for the bit-depth test, gathered in one pass. +/// One channel's decoded segment. Channels are analyzed separately to avoid +/// phase cancellation, keeping memory bounded to one channel at a time. #[derive(Default)] struct Window { - mono: Vec, + signal: Vec, /// OR of all raw integer samples, right-justified at the declared depth. or_bits: u32, - /// First channel's raw integer samples, for the upsampling-artifact + /// This channel's raw integer samples, for the upsampling-artifact /// tests; empty for float PCM. - first_channel: Vec, + samples: Vec, } /// Analyses one file and returns a populated result. Only a short segment from @@ -187,8 +184,10 @@ pub fn check_file( if options.sample_seconds <= 0 { return Err("sample_seconds must be positive".to_string().into()); } - if options.n_fft <= 0 || (options.n_fft & (options.n_fft - 1)) != 0 { - return Err("n_fft must be a positive power of two".to_string().into()); + if options.n_fft < 4 || (options.n_fft & (options.n_fft - 1)) != 0 { + return Err("n_fft must be a power of two of at least 4" + .to_string() + .into()); } let size = file.metadata().map_err(|e| e.to_string())?.len(); if size == 0 { @@ -196,7 +195,7 @@ pub fn check_file( } check()?; - let mut source = Source::open(&mut file)?; + let source = Source::open(&mut file)?; let sr = source.sample_rate(); if sr == 0 { return Err("invalid declared sample rate 0".to_string().into()); @@ -213,13 +212,9 @@ pub fn check_file( let start_frame = (offset * f64::from(sr)) as u64; let window_frames = (analyzed_duration * f64::from(sr)) as u64; - let window = source - .read_window(start_frame, window_frames, check) - .map_err(|e| HiResCheckError::Failed(format!("could not decode audio: {e}")))?; - let y = &window.mono; - if y.is_empty() { - return Err("decoded audio segment is empty".to_string().into()); - } + let declared_bits = source.declared_bits(); + let channels = source.channel_count(); + drop(source); let mut result = HiResCheckResult { file_path: file_path.to_string(), @@ -227,23 +222,71 @@ pub fn check_file( total_duration_s: total_duration, analyzed_duration_s: analyzed_duration, noise_floor_db: options.noise_floor_db, + declared_bit_depth: declared_bits, + useful_sample_rate: sr, verdict: VERDICT_INCONCLUSIVE.into(), ..HiResCheckResult::default() }; - // A silent segment makes spectral analysis meaningless rather than wrong. - if !y.iter().any(|v| f64::from(v.abs()) > 1e-9) { - return Ok(result); - } - - // Shrink n_fft for very short segments so the window does not end up - // measuring its own zero padding. + // Use complete windows only. Zero-padding a segment boundary introduces + // an artificial broadband transient and can hide a real spectral cutoff. let mut n_fft = options.n_fft as usize; - while n_fft > 256 && n_fft > y.len() * 2 { + while n_fft > 256 && n_fft as u64 > window_frames { n_fft /= 2; } - - let stats = analyze_stft(y, n_fft, sr, check)?; + let claims_by_rate = i64::from(sr) > options.hires_sample_rate_threshold; + let mut combined: Option = None; + let mut or_bits = 0; + let mut common_artifact: Option<&str> = None; + let mut all_floors_at_16bit = true; + for channel in 0..channels { + check()?; + file.seek(SeekFrom::Start(0)).map_err(|e| e.to_string())?; + let mut source = Source::open(&mut file)?; + let window = source + .read_window(start_frame, window_frames, channel, check) + .map_err(|e| HiResCheckError::Failed(format!("could not decode audio: {e}")))?; + if window.signal.is_empty() { + return Err("decoded audio segment is empty".to_string().into()); + } + or_bits |= window.or_bits; + result.analyzed_duration_s = result + .analyzed_duration_s + .min(window.signal.len() as f64 / f64::from(sr)); + if !window.signal.iter().any(|v| f64::from(v.abs()) > 1e-9) { + continue; + } + let stats = analyze_stft(&window.signal, n_fft, sr, check)?; + all_floors_at_16bit &= classify_noise_floor(stats.quiet_floor_var, 1).0 == FLOOR_AT_16BIT; + if claims_by_rate { + let artifact = detect_integer_upsampling(&window.samples, window.or_bits, sr); + // A pattern in one channel must not implicate independent, + // full-resolution content in another channel. + common_artifact = Some(match common_artifact { + None => artifact, + Some(previous) if previous == artifact => artifact, + Some(_) => "", + }); + } + if let Some(combined) = &mut combined { + for (all, channel) in combined.avg_magnitude.iter_mut().zip(&stats.avg_magnitude) { + *all = all.max(*channel); + } + for (all, channel) in combined + .music_band_spreads + .iter_mut() + .zip(&stats.music_band_spreads) + { + *all = all.max(*channel); + } + combined.quiet_floor_var = combined.quiet_floor_var.max(stats.quiet_floor_var); + } else { + combined = Some(stats); + } + } + let Some(stats) = combined else { + return Ok(result); + }; if !stats.avg_magnitude.iter().any(|&v| v > 0.0) { return Ok(result); } @@ -257,19 +300,17 @@ pub fn check_file( .map_or(0.0, |k| k as f64 * f64::from(sr) / n_fft as f64); result.cutoff_frequency_hz = cutoff; - let declared_bits = source.declared_bits(); let mut effective_bits = 0; - if declared_bits > 0 && window.or_bits != 0 { + if declared_bits > 0 && or_bits != 0 { // Digital silence carries no bits at all; leaving it at 0 keeps it // out of the padded-depth test. - effective_bits = declared_bits.saturating_sub(window.or_bits.trailing_zeros()); + effective_bits = declared_bits.saturating_sub(or_bits.trailing_zeros()); } result.declared_bit_depth = declared_bits; result.effective_bit_depth = effective_bits; // A file can claim Hi-Res by rate, by depth, or both, and each claim is // answered by the test that can actually judge it. - let claims_by_rate = i64::from(sr) > options.hires_sample_rate_threshold; let claims_by_depth = declared_bits > 16; result.useful_sample_rate = sr; if claims_by_rate { @@ -292,42 +333,46 @@ pub fn check_file( result.brickwall_hz = detect_brickwall(&spec_db, sr, n_fft, options.noise_floor_db); } let cutoff_is_low = claims_by_rate && cutoff < options.hires_cutoff_threshold_hz; - let rate_is_fake = cutoff_is_low || result.brickwall_hz > 0.0; + let limited_bandwidth = cutoff_is_low || result.brickwall_hz > 0.0; let depth_is_fake = claims_by_depth && effective_bits > 0 && effective_bits <= 16; - // Exact fingerprints of a conversion. Imaging puts content back above + // Integer fingerprints of a conversion. Imaging puts content back above // 22 kHz, so such a file can pass the cutoff test and still be a fake. - let mut artifact = ""; - if claims_by_rate { - artifact = detect_integer_upsampling(&window.first_channel, window.or_bits, sr); - if artifact.is_empty() && detect_imaging(&spec_db, sr, n_fft, options.noise_floor_db) { - artifact = ARTIFACT_IMAGING; - } + let mut artifact = common_artifact.unwrap_or(""); + if claims_by_rate + && artifact.is_empty() + && detect_imaging(&spec_db, sr, n_fft, options.noise_floor_db) + { + artifact = ARTIFACT_IMAGING; } result.upsampling_artifact = artifact.into(); - if rate_is_fake { - let (class, vs_16bit_db) = - classify_noise_floor(stats.quiet_floor_var, source.channel_count()); + if limited_bandwidth { + let (class, vs_16bit_db) = classify_noise_floor(stats.quiet_floor_var, 1); result.noise_floor_class = class.into(); result.noise_floor_vs_16bit_db = vs_16bit_db; } + // A noisier channel must not obscure a quieter channel's extra precision. + let likely_upsampled = result.brickwall_hz > 0.0 && all_floors_at_16bit; result.verdict = if !claims_by_rate && !claims_by_depth { VERDICT_STANDARD - } else if rate_is_fake || depth_is_fake || !artifact.is_empty() { + } else if likely_upsampled || depth_is_fake || !artifact.is_empty() { VERDICT_FAKE + } else if limited_bandwidth { + VERDICT_BAND_LIMITED } else { VERDICT_GENUINE } .into(); if result.verdict == VERDICT_FAKE { - result.confidence = if depth_is_fake || !artifact.is_empty() { + result.confidence = if depth_is_fake + || artifact == ARTIFACT_SAMPLE_HOLD + || artifact == ARTIFACT_INTERPOLATION + { CONFIDENCE_CERTAIN - } else if result.brickwall_hz > 0.0 && result.noise_floor_class == FLOOR_AT_16BIT { - CONFIDENCE_LIKELY } else { - CONFIDENCE_SUSPECT + CONFIDENCE_LIKELY } .into(); } @@ -349,7 +394,7 @@ pub fn check_file( findings.push(format!( "declares {sr} Hz but content stops at ~{cutoff:.0} Hz" )); - } else if rate_is_fake { + } else if limited_bandwidth { findings.push(format!( "declares {sr} Hz but the spectrum falls off a cliff at {:.0} Hz", result.brickwall_hz @@ -360,8 +405,8 @@ pub fn check_file( "declares {declared_bits}-bit but only {effective_bits} bits carry data" )); } - if result.confidence == CONFIDENCE_SUSPECT { - findings.push("may be a genuine master low-pass filtered in mastering".into()); + if result.verdict == VERDICT_BAND_LIMITED { + findings.push("bandwidth alone does not establish upsampling or master provenance".into()); } result.reason = findings.join("; "); Ok(result) @@ -431,11 +476,12 @@ impl<'a> Source<'a> { &mut self, start_frame: u64, frames: u64, + channel: u32, check: &dyn Fn() -> Result<(), String>, ) -> Result { match self { - Source::Flac(reader) => read_flac_window(reader, start_frame, frames, check), - Source::Wav(wav) => wav.read_window(start_frame, frames, check), + Source::Flac(reader) => read_flac_window(reader, start_frame, frames, channel, check), + Source::Wav(wav) => wav.read_window(start_frame, frames, channel, check), } } } @@ -448,21 +494,22 @@ fn read_flac_window( reader: &mut claxon::FlacReader>, start_frame: u64, frames: u64, + channel: u32, check: &dyn Fn() -> Result<(), String>, ) -> Result { let bits = reader.streaminfo().bits_per_sample; let scale = 1.0 / 2f64.powi(bits as i32 - 1); let capacity = usize::try_from(frames).unwrap_or(0); let mut out = Window { - mono: Vec::with_capacity(capacity), - first_channel: Vec::with_capacity(capacity), + signal: Vec::with_capacity(capacity), + samples: Vec::with_capacity(capacity), ..Window::default() }; let end = start_frame + frames; let mut blocks = reader.blocks(); let mut buffer = Vec::new(); let mut decoded = 0u64; - while (out.mono.len() as u64) < frames { + while (out.signal.len() as u64) < frames { if decoded.is_multiple_of(64) { check()?; } @@ -471,25 +518,20 @@ fn read_flac_window( Ok(Some(block)) => block, Ok(None) => break, // A truncated tail still leaves a usable window. - Err(_) if !out.mono.is_empty() => break, + Err(_) if !out.signal.is_empty() => break, Err(e) => return Err(e.to_string()), }; let first = block.time(); let n = u64::from(block.duration()); if first + n > start_frame { - let channels = block.channels(); let from = start_frame.saturating_sub(first); let to = n.min(end - first); for i in from..to { let i = i as usize; - let mut sum = 0.0; - for ch in 0..channels { - let v = block.channel(ch)[i]; - out.or_bits |= v as u32; - sum += f64::from(v); - } - out.first_channel.push(block.channel(0)[i]); - out.mono.push((sum / f64::from(channels) * scale) as f32); + let v = block.channel(channel)[i]; + out.or_bits |= v as u32; + out.samples.push(v); + out.signal.push((f64::from(v) * scale) as f32); } } buffer = block.into_buffer(); @@ -599,6 +641,7 @@ impl<'a> WavSource<'a> { &mut self, start_frame: u64, frames: u64, + channel: u32, check: &dyn Fn() -> Result<(), String>, ) -> Result { let total = self.data_size / self.frame_bytes(); @@ -614,9 +657,9 @@ impl<'a> WavSource<'a> { )) .map_err(|e| e.to_string())?; let capacity = usize::try_from(frames).unwrap_or(0); - out.mono.reserve(capacity); + out.signal.reserve(capacity); if !self.is_float { - out.first_channel.reserve(capacity); + out.samples.reserve(capacity); } let bytes_per_sample = (self.container_bits / 8) as usize; let scale = 1.0 / 2f64.powi(self.container_bits as i32 - 1); @@ -630,14 +673,17 @@ impl<'a> WavSource<'a> { if reader.read_exact(&mut frame).is_err() { break; } - let mut sum = 0.0; for (c, b) in frame.chunks_exact(bytes_per_sample).enumerate() { + if c != channel as usize { + continue; + } if self.is_float { - sum += if self.container_bits == 32 { + let v = if self.container_bits == 32 { f64::from(f32::from_le_bytes([b[0], b[1], b[2], b[3]])) } else { f64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]) }; + out.signal.push(v as f32); continue; } let v: i32 = match self.container_bits { @@ -647,12 +693,9 @@ impl<'a> WavSource<'a> { _ => i32::from_le_bytes([b[0], b[1], b[2], b[3]]), }; out.or_bits |= v as u32; - if c == 0 { - out.first_channel.push(v); - } - sum += f64::from(v) * scale; + out.samples.push(v); + out.signal.push((f64::from(v) * scale) as f32); } - out.mono.push((sum / f64::from(self.channels)) as f32); } Ok(out) } diff --git a/rust_backend/crates/core/src/media/hires/tests.rs b/rust_backend/crates/core/src/media/hires/tests.rs index f328df24..8731e670 100644 --- a/rust_backend/crates/core/src/media/hires/tests.rs +++ b/rust_backend/crates/core/src/media/hires/tests.rs @@ -397,12 +397,106 @@ fn genuine_hires_is_not_flagged() { } #[test] -fn an_upsampled_cd_signal_that_is_never_quiet_is_only_suspect() { +fn full_bandwidth_is_measured_relative_to_each_sample_rate() { + let dir = TempDir::new("rates"); + for sr in [44_100, 48_000, 88_200, 96_000, 176_400, 192_000] { + let samples = quantize(&full_band_noise(65_536, 21), 24); + let r = check(&flac(&dir, &format!("{sr}.flac"), &samples, sr, 24)); + assert_eq!(r.verdict, VERDICT_GENUINE, "{sr}: {}", r.reason); + assert!((r.cutoff_frequency_hz - f64::from(sr) / 2.0).abs() < 50.0); + assert_eq!(r.effective_bit_depth, 24); + } +} + +#[test] +fn a_96k_master_filtered_at_27k_is_not_evidence_of_upsampling() { + let dir = TempDir::new("filtered96"); + let signal = shape_spectrum(&full_band_noise(131_072, 22), 96_000, |hz| { + if hz < 27_000.0 { 1.0 } else { 0.0 } + }); + let r = check(&flac( + &dir, + "filtered.flac", + &quantize(&signal, 24), + 96_000, + 24, + )); + assert_eq!(r.verdict, VERDICT_BAND_LIMITED); + assert!((r.cutoff_frequency_hz - 27_000.0).abs() < 1000.0); + assert_eq!(r.effective_bit_depth, 24); + assert!(!r.is_suspicious()); + assert!(!r.redownload_safe()); +} + +fn write_stereo_wav(path: &Path, left: &[i32], right: &[i32], sr: u32) { + assert_eq!(left.len(), right.len()); + let interleaved: Vec = left.iter().zip(right).flat_map(|(&l, &r)| [l, r]).collect(); + write_wav(path, &interleaved, sr, 24); + let mut bytes = std::fs::read(path).expect("wav"); + bytes[22..24].copy_from_slice(&2u16.to_le_bytes()); + bytes[28..32].copy_from_slice(&(sr * 6).to_le_bytes()); + bytes[32..34].copy_from_slice(&6u16.to_le_bytes()); + std::fs::write(path, bytes).expect("stereo header"); +} + +#[test] +fn antiphase_channels_do_not_cancel_spectral_evidence() { + let dir = TempDir::new("antiphase"); + let left = quantize(&full_band_noise(65_536, 23), 24); + let right: Vec = left.iter().map(|v| -*v).collect(); + let path = dir.file("antiphase.wav"); + write_stereo_wav(&path, &left, &right, 96_000); + let r = check(&path); + assert_eq!(r.verdict, VERDICT_GENUINE); + assert!(r.cutoff_frequency_hz > 47_000.0); + assert_eq!(r.effective_bit_depth, 24); +} + +#[test] +fn a_full_resolution_channel_prevents_a_false_padding_or_pattern_verdict() { + let dir = TempDir::new("independent-channels"); + let source = pcm_using_bits(16, 24, 16_384, 24); + let left: Vec = source.iter().flat_map(|&v| [v; 4]).collect(); + let right = pcm_using_bits(24, 24, left.len(), 25); + let path = dir.file("independent.wav"); + write_stereo_wav(&path, &left, &right, HIRES_SR); + let r = check(&path); + assert_eq!(r.verdict, VERDICT_GENUINE); + assert_eq!(r.upsampling_artifact, ""); + assert_eq!(r.effective_bit_depth, 24); + assert!(r.cutoff_frequency_hz > 80_000.0); +} + +#[test] +fn cropped_segment_edges_do_not_create_ultrasonic_energy() { + let signal: Vec = (0..10_000) + .map(|i| (2.0 * std::f64::consts::PI * 3000.0 * i as f64 / 96_000.0).cos() as f32) + .collect(); + let stats = analyze_stft(&signal, 4096, 96_000, &|| Ok(())).expect("stft"); + let db = spectrum_db(&stats.avg_magnitude); + assert!(db[1024..].iter().all(|v| *v < -100.0)); +} + +#[test] +fn a_16bit_noise_floor_in_one_channel_does_not_hide_quieter_detail_in_another() { + let dir = TempDir::new("mixed-floors"); + let left = ideal_upsample(&dither_to_16_bit(&cd_source_with_quiet_half(0.0)), 4); + let right = ideal_upsample(&cd_source_with_quiet_half(1e-6), 4); + let path = dir.file("mixed.wav"); + write_stereo_wav(&path, &quantize(&left, 24), &quantize(&right, 24), HIRES_SR); + let r = check(&path); + assert_eq!(r.brickwall_hz, 22_050.0); + assert_eq!(r.verdict, VERDICT_BAND_LIMITED); + assert!(!r.redownload_safe()); +} + +#[test] +fn bandwidth_alone_cannot_identify_an_upsampled_cd() { let dir = TempDir::new("upsampled"); let samples = quantize(&upsampled_from_cd(), 24); let r = check(&flac(&dir, "fake.flac", &samples, HIRES_SR, 24)); - assert_eq!(r.verdict, VERDICT_FAKE); - assert!(r.is_suspicious()); + assert_eq!(r.verdict, VERDICT_BAND_LIMITED); + assert!(!r.is_suspicious()); assert!( r.cutoff_frequency_hz > 22_000.0 && r.cutoff_frequency_hz < 28_000.0, "{}", @@ -410,10 +504,10 @@ fn an_upsampled_cd_signal_that_is_never_quiet_is_only_suspect() { ); assert!(r.reason.contains("content stops"), "{}", r.reason); // Loud from start to end: the floor cannot be read, so a genuine master - // filtered at 22 kHz would look the same. Flagged, never replaced. + // filtered at 22 kHz would look the same. Report bandwidth, not provenance. assert_eq!(r.brickwall_hz, 22_050.0); assert_eq!(r.noise_floor_class, FLOOR_MASKED); - assert_eq!(r.confidence, CONFIDENCE_SUSPECT); + assert_eq!(r.confidence, ""); assert!(!r.redownload_safe()); } @@ -536,7 +630,7 @@ fn wav_is_checked_too() { let r = check(&fake); assert_eq!( (r.verdict.as_str(), r.effective_bit_depth), - (VERDICT_FAKE, 24) + (VERDICT_BAND_LIMITED, 24) ); } @@ -613,7 +707,7 @@ fn an_upsampled_16_bit_master_is_likely() { /// The same cliff, but the quiet half carries detail far below 16-bit noise: /// a 24-bit master made at 44.1 kHz. LOSSLESS would lose that depth. #[test] -fn a_24_bit_master_made_at_44k_is_only_suspect() { +fn a_24_bit_master_made_at_44k_has_limited_bandwidth() { let dir = TempDir::new("master24"); let signal = ideal_upsample(&cd_source_with_quiet_half(1e-6), 4); let r = check(&flac( @@ -623,22 +717,22 @@ fn a_24_bit_master_made_at_44k_is_only_suspect() { HIRES_SR, 24, )); - assert_eq!(r.verdict, VERDICT_FAKE); + assert_eq!(r.verdict, VERDICT_BAND_LIMITED); assert_eq!( r.noise_floor_class, FLOOR_BELOW_16BIT, "{} dB", r.noise_floor_vs_16bit_db ); - assert_eq!(r.confidence, CONFIDENCE_SUSPECT); + assert_eq!(r.confidence, ""); assert!(!r.redownload_safe()); - assert!(r.reason.contains("genuine master"), "{}", r.reason); + assert!(r.reason.contains("provenance"), "{}", r.reason); } /// A gradual mastering roll-off that still ends below 28 kHz: no resampler -/// cliff, so at most a suspect. Faded in and out: an abrupt start inside the +/// cliff, so no evidence of upsampling. An abrupt start inside the /// analyzed window is a step, whose broadband splatter reads as content. #[test] -fn a_gradual_mastering_roll_off_is_only_suspect() { +fn a_gradual_mastering_roll_off_is_not_flagged_as_fake() { let dir = TempDir::new("lpf"); let mut signal = shape_spectrum(&full_band_noise(HIRES_FRAMES, 0), HIRES_SR, |freq| { let gain_db = if freq > 16_000.0 { @@ -656,15 +750,15 @@ fn a_gradual_mastering_roll_off_is_only_suspect() { HIRES_SR, 24, )); - assert_eq!(r.verdict, VERDICT_FAKE); + assert_eq!(r.verdict, VERDICT_BAND_LIMITED); assert_eq!(r.brickwall_hz, 0.0); - assert_eq!(r.confidence, CONFIDENCE_SUSPECT); + assert_eq!(r.confidence, ""); } /// 24-bit source samples (so the depth test passes) taken to 176.4 kHz by the /// cheap upsamplers whose output is an exact, checkable pattern. #[test] -fn integer_upsampling_artifacts_are_certain() { +fn integer_patterns_are_certain_but_spectral_imaging_is_likely() { let source = pcm_using_bits(24, 24, CD_SOURCE_FRAMES + 1, 1); let (mut hold, mut line, mut zero_stuffed) = (Vec::new(), Vec::new(), Vec::new()); for pair in source.windows(2) { @@ -684,7 +778,12 @@ fn integer_upsampling_artifacts_are_certain() { let r = check(&flac(&dir, &format!("{name}.flac"), &samples, HIRES_SR, 24)); assert_eq!(r.upsampling_artifact, artifact, "{name}"); assert_eq!(r.verdict, VERDICT_FAKE, "{name}"); - assert_eq!(r.confidence, CONFIDENCE_CERTAIN, "{name}"); + let confidence = if artifact == ARTIFACT_IMAGING { + CONFIDENCE_LIKELY + } else { + CONFIDENCE_CERTAIN + }; + assert_eq!(r.confidence, confidence, "{name}"); } }