mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 06:28:27 +02:00
perf(hires): analyze every channel in one audio decode pass
This commit is contained in:
1 parent
3b553b38a3
commit
5019851903
4 files changed
+672
-17
No files matched your search
@@ -14,6 +14,9 @@
|
||||
|
||||
use super::fft::Radix2Fft;
|
||||
|
||||
mod streaming;
|
||||
pub(super) use streaming::{IntegerEvidence, StftAccumulator, StftPlan};
|
||||
|
||||
pub const CONFIDENCE_CERTAIN: &str = "certain";
|
||||
pub const CONFIDENCE_LIKELY: &str = "likely";
|
||||
|
||||
@@ -83,6 +86,7 @@ pub struct StftStats {
|
||||
/// 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.
|
||||
#[cfg(test)]
|
||||
pub fn analyze_stft(
|
||||
y: &[f32],
|
||||
n_fft: usize,
|
||||
@@ -337,6 +341,7 @@ pub fn detect_imaging(
|
||||
/// every sample repeated (sample-and-hold) or the in-between samples on a
|
||||
/// straight line (linear interpolation). `samples` is one channel,
|
||||
/// right-justified; `or_bits` is the OR over the whole window.
|
||||
#[cfg(test)]
|
||||
pub fn detect_integer_upsampling(samples: &[i32], or_bits: u32, sample_rate: u32) -> &'static str {
|
||||
if or_bits == 0 || samples.len() < 4 {
|
||||
return "";
|
||||
@@ -362,6 +367,7 @@ pub fn detect_integer_upsampling(samples: &[i32], or_bits: u32, sample_rate: u32
|
||||
""
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn integer_upsampling_at_phase(
|
||||
samples: &[i32],
|
||||
unused: u32,
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
//! Exact evidence with one circular STFT frame per channel. Only scalar
|
||||
//! frame levels survive the FFT, for the existing floor/band percentiles.
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(in crate::media::hires) struct StftPlan {
|
||||
n: usize,
|
||||
window: Vec<f64>,
|
||||
window_power: f64,
|
||||
band_low: usize,
|
||||
band_high: usize,
|
||||
music_bands: Vec<(usize, usize)>,
|
||||
fft: Radix2Fft,
|
||||
re: Vec<f64>,
|
||||
im: Vec<f64>,
|
||||
band_power: Vec<f64>,
|
||||
}
|
||||
|
||||
impl StftPlan {
|
||||
pub(in crate::media::hires) fn new(n: usize, sample_rate: u32) -> Self {
|
||||
let window: Vec<f64> = (0..n)
|
||||
.map(|i| 0.5 - 0.5 * (2.0 * std::f64::consts::PI * i as f64 / n as f64).cos())
|
||||
.collect();
|
||||
let window_power = window.iter().map(|w| w * w).sum();
|
||||
let bin_hz = f64::from(sample_rate) / n as f64;
|
||||
let band_low = (FLOOR_BAND_LOW_HZ / bin_hz).ceil() as usize;
|
||||
let band_high = ((FLOOR_BAND_HIGH_HZ / bin_hz) as usize).min(n / 2);
|
||||
let nyquist = f64::from(sample_rate) / 2.0;
|
||||
let mut music_bands = Vec::new();
|
||||
if nyquist > f64::from(SOURCE_RATES[1]) / 2.0 {
|
||||
let mut lo = MUSIC_BAND_START_HZ;
|
||||
while lo + MUSIC_BAND_WIDTH_HZ <= nyquist {
|
||||
let first = (lo / bin_hz).ceil() as usize;
|
||||
let last = (((lo + MUSIC_BAND_WIDTH_HZ) / bin_hz).ceil() as usize).min(n / 2 + 1);
|
||||
if last > first {
|
||||
music_bands.push((first, last));
|
||||
}
|
||||
lo += MUSIC_BAND_WIDTH_HZ;
|
||||
}
|
||||
}
|
||||
Self {
|
||||
n,
|
||||
window,
|
||||
window_power,
|
||||
band_low,
|
||||
band_high,
|
||||
music_bands,
|
||||
fft: Radix2Fft::new(n),
|
||||
re: vec![0.0; n],
|
||||
im: vec![0.0; n],
|
||||
band_power: Vec::with_capacity(band_high.saturating_sub(band_low) + 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::media::hires) struct StftAccumulator {
|
||||
signal: Vec<f32>,
|
||||
cursor: usize,
|
||||
until_frame: usize,
|
||||
frame_count: usize,
|
||||
stats: StftStats,
|
||||
frames: Vec<(f64, f64)>,
|
||||
music_levels: Vec<Vec<f64>>,
|
||||
}
|
||||
|
||||
impl StftAccumulator {
|
||||
pub(in crate::media::hires) fn new(plan: &StftPlan) -> Self {
|
||||
Self {
|
||||
signal: vec![0.0; plan.n],
|
||||
cursor: 0,
|
||||
until_frame: plan.n,
|
||||
frame_count: 0,
|
||||
stats: StftStats {
|
||||
avg_magnitude: vec![0.0; plan.n / 2 + 1],
|
||||
quiet_floor_var: f64::NAN,
|
||||
music_band_spreads: Vec::new(),
|
||||
},
|
||||
frames: Vec::new(),
|
||||
music_levels: vec![Vec::new(); plan.music_bands.len()],
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::media::hires) fn push(
|
||||
&mut self,
|
||||
value: f32,
|
||||
plan: &mut StftPlan,
|
||||
check: &dyn Fn() -> Result<(), String>,
|
||||
) -> Result<(), String> {
|
||||
self.signal[self.cursor] = value;
|
||||
self.cursor += 1;
|
||||
if self.cursor == plan.n {
|
||||
self.cursor = 0;
|
||||
}
|
||||
self.until_frame -= 1;
|
||||
if self.until_frame != 0 {
|
||||
return Ok(());
|
||||
}
|
||||
if self.frame_count.is_multiple_of(256) {
|
||||
check()?;
|
||||
}
|
||||
self.frame_count += 1;
|
||||
self.until_frame = plan.n / 4;
|
||||
if self.signal.iter().all(|sample| *sample == 0.0) {
|
||||
return Ok(());
|
||||
}
|
||||
let ordered = self.signal[self.cursor..]
|
||||
.iter()
|
||||
.chain(&self.signal[..self.cursor]);
|
||||
for ((r, w), sample) in plan.re.iter_mut().zip(&plan.window).zip(ordered) {
|
||||
*r = f64::from(*sample) * w;
|
||||
}
|
||||
plan.im.fill(0.0);
|
||||
plan.fft.transform(&mut plan.re, &mut plan.im);
|
||||
for (k, avg) in self.stats.avg_magnitude.iter_mut().enumerate() {
|
||||
*avg += plan.re[k].hypot(plan.im[k]);
|
||||
}
|
||||
if plan.band_high <= plan.band_low {
|
||||
return Ok(());
|
||||
}
|
||||
plan.band_power.clear();
|
||||
let mut sum = 0.0;
|
||||
for k in plan.band_low..=plan.band_high {
|
||||
let power = plan.re[k] * plan.re[k] + plan.im[k] * plan.im[k];
|
||||
plan.band_power.push(power);
|
||||
sum += power;
|
||||
}
|
||||
if sum == 0.0 {
|
||||
return Ok(());
|
||||
}
|
||||
self.frames.push((
|
||||
sum / plan.band_power.len() as f64,
|
||||
median_in_place(&mut plan.band_power),
|
||||
));
|
||||
for (levels, &(first, last)) in self.music_levels.iter_mut().zip(&plan.music_bands) {
|
||||
let power = (first..last)
|
||||
.map(|k| plan.re[k] * plan.re[k] + plan.im[k] * plan.im[k])
|
||||
.sum::<f64>()
|
||||
/ (last - first) as f64;
|
||||
levels.push(10.0 * power.max(1e-30).log10());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(in crate::media::hires) fn finish(mut self, plan: &StftPlan) -> StftStats {
|
||||
if self.frame_count == 0 {
|
||||
return self.stats;
|
||||
}
|
||||
for avg in &mut self.stats.avg_magnitude {
|
||||
*avg /= self.frame_count as f64;
|
||||
}
|
||||
if !self.frames.is_empty() {
|
||||
self.frames.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let count = ((self.frames.len() as f64 * QUIET_FRAME_FRACTION) as usize).max(1);
|
||||
let mut estimates: Vec<f64> = self.frames[..count]
|
||||
.iter()
|
||||
.map(|&(_, median)| median / std::f64::consts::LN_2 / plan.window_power)
|
||||
.collect();
|
||||
self.stats.quiet_floor_var = median_in_place(&mut estimates);
|
||||
}
|
||||
for mut levels in self.music_levels {
|
||||
if levels.len() < 2 {
|
||||
break;
|
||||
}
|
||||
levels.sort_by(f64::total_cmp);
|
||||
self.stats
|
||||
.music_band_spreads
|
||||
.push(percentile_sorted(&levels, 0.95) - percentile_sorted(&levels, 0.05));
|
||||
}
|
||||
self.stats
|
||||
}
|
||||
}
|
||||
|
||||
struct Position {
|
||||
count: usize,
|
||||
moving: usize,
|
||||
// Bucket floor(log2(abs(d2) - 1)). Then abs(d2) > 2 << unused
|
||||
// exactly when its bucket is at least unused + 1. Samples are all
|
||||
// divisible by 1 << unused, once the final OR establishes that shift.
|
||||
second_derivatives: [usize; 33],
|
||||
}
|
||||
|
||||
impl Default for Position {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
count: 0,
|
||||
moving: 0,
|
||||
second_derivatives: [0; 33],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::media::hires) struct IntegerEvidence {
|
||||
pub(in crate::media::hires) or_bits: u32,
|
||||
count: usize,
|
||||
previous: [i32; 2],
|
||||
ratios: Vec<Vec<Position>>,
|
||||
}
|
||||
|
||||
impl IntegerEvidence {
|
||||
pub(in crate::media::hires) fn new(sample_rate: u32) -> Self {
|
||||
let ratios = SOURCE_RATES
|
||||
.into_iter()
|
||||
.filter_map(|rate| {
|
||||
let ratio = (sample_rate / rate) as usize;
|
||||
(sample_rate.is_multiple_of(rate) && (2..=8).contains(&ratio))
|
||||
.then(|| (0..ratio).map(|_| Position::default()).collect())
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
or_bits: 0,
|
||||
count: 0,
|
||||
previous: [0; 2],
|
||||
ratios,
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::media::hires) fn push(&mut self, value: i32) {
|
||||
self.or_bits |= value as u32;
|
||||
if self.count >= 2 {
|
||||
let d1 = i64::from(self.previous[1]) - i64::from(self.previous[0]);
|
||||
let d2 = (i64::from(self.previous[0]) - 2 * i64::from(self.previous[1])
|
||||
+ i64::from(value))
|
||||
.unsigned_abs();
|
||||
let bucket = d2.saturating_sub(1).checked_ilog2();
|
||||
for positions in &mut self.ratios {
|
||||
let position = (self.count - 1) % positions.len();
|
||||
let state = &mut positions[position];
|
||||
state.count += 1;
|
||||
state.moving += usize::from(d1 != 0);
|
||||
if let Some(bucket) = bucket {
|
||||
state.second_derivatives[bucket as usize] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.previous = [self.previous[1], value];
|
||||
self.count += 1;
|
||||
}
|
||||
|
||||
pub(in crate::media::hires) fn artifact(&self) -> &'static str {
|
||||
if self.or_bits == 0 || self.count < 4 {
|
||||
return "";
|
||||
}
|
||||
let first_bucket = self.or_bits.trailing_zeros() as usize + 1;
|
||||
for positions in &self.ratios {
|
||||
let inner_total: usize = positions.iter().map(|state| state.count).sum();
|
||||
let hold_total: usize = positions.iter().map(|state| state.moving).sum();
|
||||
let lines: Vec<usize> = positions
|
||||
.iter()
|
||||
.map(|state| state.second_derivatives[first_bucket..].iter().sum())
|
||||
.collect();
|
||||
let line_total: usize = lines.iter().sum();
|
||||
for (phase, anchor) in positions.iter().enumerate() {
|
||||
let maximum = (inner_total - anchor.count) as f64 * ARTIFACT_MAX_VIOLATION_RATE;
|
||||
if anchor.moving >= ARTIFACT_MIN_MOVING_ANCHORS
|
||||
&& (hold_total - anchor.moving) as f64 <= maximum
|
||||
{
|
||||
return ARTIFACT_SAMPLE_HOLD;
|
||||
}
|
||||
if lines[phase] >= ARTIFACT_MIN_MOVING_ANCHORS
|
||||
&& (line_total - lines[phase]) as f64 <= maximum
|
||||
{
|
||||
return ARTIFACT_INTERPOLATION;
|
||||
}
|
||||
}
|
||||
}
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
pub(in crate::media::hires) fn assert_stats_equal(actual: &StftStats, expected: &StftStats) {
|
||||
assert_eq!(actual.avg_magnitude, expected.avg_magnitude);
|
||||
assert_eq!(
|
||||
actual.quiet_floor_var.to_bits(),
|
||||
expected.quiet_floor_var.to_bits()
|
||||
);
|
||||
assert_eq!(actual.music_band_spreads, expected.music_band_spreads);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circular_stft_matches_buffered_reference_exactly() {
|
||||
for rate in [44_100, 96_000, 192_000] {
|
||||
for n in [256, 1024] {
|
||||
let mut plan = StftPlan::new(n, rate);
|
||||
for length in [n - 1, n, n + n / 4 - 1, 7 * n + 13] {
|
||||
let signals: Vec<Vec<f32>> = (0..3)
|
||||
.map(|channel| {
|
||||
(0..length)
|
||||
.map(|i| match channel {
|
||||
0 => 0.0,
|
||||
1 if i < 2 * n => 0.0,
|
||||
1 => ((i * 7919 % 65537) as f32 - 32768.0) / 65536.0,
|
||||
_ => -((i * 7919 % 65537) as f32 - 32768.0) / 65536.0,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let mut channels: Vec<_> = signals
|
||||
.iter()
|
||||
.map(|_| StftAccumulator::new(&plan))
|
||||
.collect();
|
||||
for i in 0..length {
|
||||
for (channel, signal) in channels.iter_mut().zip(&signals) {
|
||||
channel.push(signal[i], &mut plan, &|| Ok(())).unwrap();
|
||||
}
|
||||
}
|
||||
for (channel, signal) in channels.into_iter().zip(signals) {
|
||||
assert_stats_equal(
|
||||
&channel.finish(&plan),
|
||||
&analyze_stft(&signal, n, rate, &|| Ok(())).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_integer_equal(samples: &[i32], rate: u32) {
|
||||
let mut evidence = IntegerEvidence::new(rate);
|
||||
for &sample in samples {
|
||||
evidence.push(sample);
|
||||
}
|
||||
let orbit = samples.iter().fold(0, |bits, sample| bits | *sample as u32);
|
||||
assert_eq!(evidence.or_bits, orbit);
|
||||
assert_eq!(
|
||||
evidence.artifact(),
|
||||
detect_integer_upsampling(samples, orbit, rate)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer_histograms_match_every_ratio_phase_padding_and_violation_boundary() {
|
||||
for base_rate in SOURCE_RATES {
|
||||
for ratio in 2..=8 {
|
||||
for phase in 0..ratio {
|
||||
for shift in [0, 8, 16] {
|
||||
let anchors: Vec<i32> =
|
||||
(0..1300).map(|i| (i * 137 % 1021 - 510) * 16).collect();
|
||||
let hold: Vec<i32> = anchors
|
||||
.iter()
|
||||
.flat_map(|value| std::iter::repeat_n(*value << shift, ratio))
|
||||
.skip(phase)
|
||||
.collect();
|
||||
let line: Vec<i32> = anchors
|
||||
.windows(2)
|
||||
.flat_map(|pair| {
|
||||
(0..ratio).map(move |i| {
|
||||
(pair[0] + (pair[1] - pair[0]) * i as i32 / ratio as i32)
|
||||
<< shift
|
||||
})
|
||||
})
|
||||
.skip(phase)
|
||||
.collect();
|
||||
let rate = base_rate * ratio as u32;
|
||||
for signal in [hold, line] {
|
||||
assert_integer_equal(&signal, rate);
|
||||
for violations in [1, ratio, ratio * 2] {
|
||||
let mut altered = signal.clone();
|
||||
for i in 0..violations {
|
||||
altered[10 + i * 17] += 37 << shift;
|
||||
}
|
||||
assert_integer_equal(&altered, rate);
|
||||
}
|
||||
// The final value changes the unused-bit shift for all earlier derivatives.
|
||||
let mut altered = signal;
|
||||
altered.push(1);
|
||||
assert_integer_equal(&altered, rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for samples in [
|
||||
vec![],
|
||||
vec![0; 10],
|
||||
vec![i32::MIN, i32::MAX, i32::MIN, 1],
|
||||
vec![i32::MIN; 5000],
|
||||
] {
|
||||
assert_integer_equal(&samples, 96_000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,8 +161,8 @@ impl HiResCheckResult {
|
||||
}
|
||||
}
|
||||
|
||||
/// One channel's decoded segment. Channels are analyzed separately to avoid
|
||||
/// phase cancellation, keeping memory bounded to one channel at a time.
|
||||
/// Reference decoder retained for streaming parity tests.
|
||||
#[cfg(test)]
|
||||
#[derive(Default)]
|
||||
struct Window {
|
||||
signal: Vec<f32>,
|
||||
@@ -173,6 +173,36 @@ struct Window {
|
||||
samples: Vec<i32>,
|
||||
}
|
||||
|
||||
struct ChannelAnalysis {
|
||||
stft: StftAccumulator,
|
||||
integers: IntegerEvidence,
|
||||
count: usize,
|
||||
non_silent: bool,
|
||||
}
|
||||
|
||||
impl ChannelAnalysis {
|
||||
fn push(
|
||||
&mut self,
|
||||
sample: f32,
|
||||
integer: Option<i32>,
|
||||
plan: &mut StftPlan,
|
||||
check: &dyn Fn() -> Result<(), String>,
|
||||
) -> Result<(), String> {
|
||||
self.count += 1;
|
||||
self.non_silent |= f64::from(sample.abs()) > 1e-9;
|
||||
if let Some(integer) = integer {
|
||||
self.integers.push(integer);
|
||||
}
|
||||
self.stft.push(sample, plan, check)
|
||||
}
|
||||
}
|
||||
|
||||
struct WindowAnalysis {
|
||||
channels: Vec<ChannelAnalysis>,
|
||||
#[cfg(test)]
|
||||
decoded_blocks: usize,
|
||||
}
|
||||
|
||||
/// Analyses one file and returns a populated result. Only a short segment from
|
||||
/// the middle of the track is decoded, never the whole file.
|
||||
pub fn check_file(
|
||||
@@ -195,7 +225,7 @@ pub fn check_file(
|
||||
}
|
||||
check()?;
|
||||
|
||||
let source = Source::open(&mut file)?;
|
||||
let mut source = Source::open(&mut file)?;
|
||||
let sr = source.sample_rate();
|
||||
if sr == 0 {
|
||||
return Err("invalid declared sample rate 0".to_string().into());
|
||||
@@ -213,8 +243,6 @@ pub fn check_file(
|
||||
let window_frames = (analyzed_duration * f64::from(sr)) as u64;
|
||||
|
||||
let declared_bits = source.declared_bits();
|
||||
let channels = source.channel_count();
|
||||
drop(source);
|
||||
|
||||
let mut result = HiResCheckResult {
|
||||
file_path: file_path.to_string(),
|
||||
@@ -239,27 +267,26 @@ pub fn check_file(
|
||||
let mut or_bits = 0;
|
||||
let mut common_artifact: Option<&str> = None;
|
||||
let mut all_floors_at_16bit = true;
|
||||
for channel in 0..channels {
|
||||
let mut plan = StftPlan::new(n_fft, sr);
|
||||
let analysis = source
|
||||
.analyze_window(start_frame, window_frames, &mut plan, check)
|
||||
.map_err(|e| HiResCheckError::Failed(format!("could not decode audio: {e}")))?;
|
||||
for channel in analysis.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() {
|
||||
if channel.count == 0 {
|
||||
return Err("decoded audio segment is empty".to_string().into());
|
||||
}
|
||||
or_bits |= window.or_bits;
|
||||
or_bits |= channel.integers.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) {
|
||||
.min(channel.count as f64 / f64::from(sr));
|
||||
if !channel.non_silent {
|
||||
continue;
|
||||
}
|
||||
let stats = analyze_stft(&window.signal, n_fft, sr, check)?;
|
||||
let stats = channel.stft.finish(&plan);
|
||||
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);
|
||||
let artifact = channel.integers.artifact();
|
||||
// A pattern in one channel must not implicate independent,
|
||||
// full-resolution content in another channel.
|
||||
common_artifact = Some(match common_artifact {
|
||||
@@ -472,6 +499,7 @@ impl<'a> Source<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn read_window(
|
||||
&mut self,
|
||||
start_frame: u64,
|
||||
@@ -484,12 +512,85 @@ impl<'a> Source<'a> {
|
||||
Source::Wav(wav) => wav.read_window(start_frame, frames, channel, check),
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_window(
|
||||
&mut self,
|
||||
start_frame: u64,
|
||||
frames: u64,
|
||||
plan: &mut StftPlan,
|
||||
check: &dyn Fn() -> Result<(), String>,
|
||||
) -> Result<WindowAnalysis, String> {
|
||||
let sample_rate = self.sample_rate();
|
||||
let mut analysis = WindowAnalysis {
|
||||
channels: (0..self.channel_count())
|
||||
.map(|_| ChannelAnalysis {
|
||||
stft: StftAccumulator::new(plan),
|
||||
integers: IntegerEvidence::new(sample_rate),
|
||||
count: 0,
|
||||
non_silent: false,
|
||||
})
|
||||
.collect(),
|
||||
#[cfg(test)]
|
||||
decoded_blocks: 0,
|
||||
};
|
||||
match self {
|
||||
Source::Flac(reader) => {
|
||||
let scale = 1.0 / 2f64.powi(reader.streaminfo().bits_per_sample as i32 - 1);
|
||||
let end = start_frame + frames;
|
||||
let mut blocks = reader.blocks();
|
||||
let mut buffer = Vec::new();
|
||||
let mut decoded = 0_u64;
|
||||
while analysis
|
||||
.channels
|
||||
.first()
|
||||
.is_some_and(|channel| (channel.count as u64) < frames)
|
||||
{
|
||||
if decoded.is_multiple_of(64) {
|
||||
check()?;
|
||||
}
|
||||
decoded += 1;
|
||||
let block = match blocks.read_next_or_eof(buffer) {
|
||||
Ok(Some(block)) => block,
|
||||
Ok(None) => break,
|
||||
Err(_) if analysis.channels[0].count > 0 => break,
|
||||
Err(error) => return Err(error.to_string()),
|
||||
};
|
||||
#[cfg(test)]
|
||||
{
|
||||
analysis.decoded_blocks += 1;
|
||||
}
|
||||
let first = block.time();
|
||||
let n = u64::from(block.duration());
|
||||
if first + n > start_frame {
|
||||
let from = start_frame.saturating_sub(first) as usize;
|
||||
let to = n.min(end.saturating_sub(first)) as usize;
|
||||
for (index, channel) in analysis.channels.iter_mut().enumerate() {
|
||||
for &value in &block.channel(index as u32)[from..to] {
|
||||
channel.push(
|
||||
(f64::from(value) * scale) as f32,
|
||||
Some(value),
|
||||
plan,
|
||||
check,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer = block.into_buffer();
|
||||
}
|
||||
}
|
||||
Source::Wav(wav) => {
|
||||
wav.analyze_window(start_frame, frames, &mut analysis, plan, check)?
|
||||
}
|
||||
}
|
||||
Ok(analysis)
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes [start_frame, start_frame + frames). claxon hands back samples
|
||||
/// right-justified with wasted bits already shifted back in and inter-channel
|
||||
/// decorrelation undone, so each value is the stored sample. It cannot seek,
|
||||
/// so the frames before the window are decoded and skipped.
|
||||
#[cfg(test)]
|
||||
fn read_flac_window(
|
||||
reader: &mut claxon::FlacReader<BufReader<&mut File>>,
|
||||
start_frame: u64,
|
||||
@@ -637,6 +738,7 @@ impl<'a> WavSource<'a> {
|
||||
u64::from(self.channels * self.container_bits / 8)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn read_window(
|
||||
&mut self,
|
||||
start_frame: u64,
|
||||
@@ -699,4 +801,57 @@ impl<'a> WavSource<'a> {
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn analyze_window(
|
||||
&mut self,
|
||||
start_frame: u64,
|
||||
frames: u64,
|
||||
analysis: &mut WindowAnalysis,
|
||||
plan: &mut StftPlan,
|
||||
check: &dyn Fn() -> Result<(), String>,
|
||||
) -> Result<(), String> {
|
||||
let total = self.data_size / self.frame_bytes();
|
||||
let frames = frames.min(total.saturating_sub(start_frame));
|
||||
let frame_bytes = self.frame_bytes() as usize;
|
||||
self.file
|
||||
.seek(SeekFrom::Start(
|
||||
self.data_offset + start_frame * frame_bytes as u64,
|
||||
))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let bytes_per_sample = (self.container_bits / 8) as usize;
|
||||
let scale = 1.0 / 2f64.powi(self.container_bits as i32 - 1);
|
||||
let mut reader = BufReader::with_capacity(1 << 16, &mut *self.file);
|
||||
let mut frame = vec![0; frame_bytes];
|
||||
for index in 0..frames {
|
||||
if index.is_multiple_of(65_536) {
|
||||
check()?;
|
||||
}
|
||||
if reader.read_exact(&mut frame).is_err() {
|
||||
break;
|
||||
}
|
||||
for (channel, bytes) in analysis
|
||||
.channels
|
||||
.iter_mut()
|
||||
.zip(frame.chunks_exact(bytes_per_sample))
|
||||
{
|
||||
if self.is_float {
|
||||
let value = if self.container_bits == 32 {
|
||||
f64::from(f32::from_le_bytes(bytes.try_into().expect("32-bit float")))
|
||||
} else {
|
||||
f64::from_le_bytes(bytes.try_into().expect("64-bit float"))
|
||||
};
|
||||
channel.push(value as f32, None, plan, check)?;
|
||||
} else {
|
||||
let value = match self.container_bits {
|
||||
8 => i32::from(bytes[0]) - 128,
|
||||
16 => i32::from(i16::from_le_bytes(bytes.try_into().expect("16-bit PCM"))),
|
||||
24 => i32::from_le_bytes([0, bytes[0], bytes[1], bytes[2]]) >> 8,
|
||||
_ => i32::from_le_bytes(bytes.try_into().expect("32-bit PCM")),
|
||||
};
|
||||
channel.push((f64::from(value) * scale) as f32, Some(value), plan, check)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -439,6 +439,115 @@ fn write_stereo_wav(path: &Path, left: &[i32], right: &[i32], sr: u32) {
|
||||
std::fs::write(path, bytes).expect("stereo header");
|
||||
}
|
||||
|
||||
fn assert_streamed_window_matches_reference(path: &Path, start: u64, frames: u64) -> usize {
|
||||
let mut file = File::open(path).unwrap();
|
||||
let mut source = Source::open(&mut file).unwrap();
|
||||
let rate = source.sample_rate();
|
||||
let mut plan = StftPlan::new(1024, rate);
|
||||
let streamed = source
|
||||
.analyze_window(start, frames, &mut plan, &|| Ok(()))
|
||||
.unwrap();
|
||||
for (index, channel) in streamed.channels.into_iter().enumerate() {
|
||||
let mut file = File::open(path).unwrap();
|
||||
let mut reference = Source::open(&mut file).unwrap();
|
||||
let window = reference
|
||||
.read_window(start, frames, index as u32, &|| Ok(()))
|
||||
.unwrap();
|
||||
assert_eq!(channel.count, window.signal.len());
|
||||
assert_eq!(
|
||||
channel.non_silent,
|
||||
window
|
||||
.signal
|
||||
.iter()
|
||||
.any(|sample| f64::from(sample.abs()) > 1e-9)
|
||||
);
|
||||
assert_eq!(channel.integers.or_bits, window.or_bits);
|
||||
assert_eq!(
|
||||
channel.integers.artifact(),
|
||||
evidence::detect_integer_upsampling(&window.samples, window.or_bits, rate)
|
||||
);
|
||||
let expected = evidence::analyze_stft(&window.signal, 1024, rate, &|| Ok(())).unwrap();
|
||||
let actual = channel.stft.finish(&plan);
|
||||
assert_eq!(actual.avg_magnitude, expected.avg_magnitude);
|
||||
assert_eq!(
|
||||
actual.quiet_floor_var.to_bits(),
|
||||
expected.quiet_floor_var.to_bits()
|
||||
);
|
||||
assert_eq!(actual.music_band_spreads, expected.music_band_spreads);
|
||||
}
|
||||
streamed.decoded_blocks
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_flac_decode_pass_serves_all_eight_channels_with_exact_window_evidence() {
|
||||
let dir = TempDir::new("streamed-flac");
|
||||
let path = dir.file("eight-channels.flac");
|
||||
let samples = pcm_using_bits(16, 24, 4096 * 6, 29);
|
||||
write_flac(&path, &samples, 96_000, 24, 8);
|
||||
// Three prefix blocks plus the end of the sampled window: four decoded
|
||||
// blocks total, rather than four separately for every channel.
|
||||
assert_eq!(
|
||||
assert_streamed_window_matches_reference(&path, 8500, 5000),
|
||||
4
|
||||
);
|
||||
assert_eq!(assert_streamed_window_matches_reference(&path, 0, 700), 1);
|
||||
assert_eq!(
|
||||
assert_streamed_window_matches_reference(&path, 20_000, 8000),
|
||||
6
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn streaming_wav_preserves_independent_antiphase_channels_and_partial_windows() {
|
||||
let dir = TempDir::new("streamed-wav");
|
||||
let left = quantize(&full_band_noise(20_003, 23), 24);
|
||||
let right: Vec<i32> = left.iter().map(|sample| -*sample).collect();
|
||||
let path = dir.file("antiphase.wav");
|
||||
write_stereo_wav(&path, &left, &right, 96_000);
|
||||
assert_eq!(
|
||||
assert_streamed_window_matches_reference(&path, 17, 13_013),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
assert_streamed_window_matches_reference(&path, 18_000, 6000),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn streaming_wav_matches_all_integer_and_float_sample_encodings() {
|
||||
let dir = TempDir::new("streamed-encodings");
|
||||
for bits in [8, 16, 24, 32] {
|
||||
let path = dir.file(&format!("integer-{bits}.wav"));
|
||||
let samples: Vec<i32> = (0..4099).map(|i| (i * 37 % 127) - 63).collect();
|
||||
write_wav(&path, &samples, 96_000, bits);
|
||||
assert_streamed_window_matches_reference(&path, 31, 3500);
|
||||
}
|
||||
for bits in [32, 64] {
|
||||
let path = dir.file(&format!("float-{bits}.wav"));
|
||||
write_wav(&path, &[0], 96_000, 32);
|
||||
let mut bytes = std::fs::read(&path).unwrap();
|
||||
bytes.truncate(44);
|
||||
bytes[20..22].copy_from_slice(&3u16.to_le_bytes());
|
||||
bytes[28..32].copy_from_slice(&(96_000_u32 * bits / 8).to_le_bytes());
|
||||
bytes[32..34].copy_from_slice(&((bits / 8) as u16).to_le_bytes());
|
||||
bytes[34..36].copy_from_slice(&(bits as u16).to_le_bytes());
|
||||
for i in 0..4099 {
|
||||
let value = ((i * 37 % 127) as f64 - 63.0) / 128.0;
|
||||
if bits == 32 {
|
||||
bytes.extend_from_slice(&(value as f32).to_le_bytes());
|
||||
} else {
|
||||
bytes.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
}
|
||||
let data_size = (bytes.len() - 44) as u32;
|
||||
bytes[4..8].copy_from_slice(&(36 + data_size).to_le_bytes());
|
||||
bytes[40..44].copy_from_slice(&data_size.to_le_bytes());
|
||||
std::fs::write(&path, bytes).unwrap();
|
||||
assert_streamed_window_matches_reference(&path, 31, 3500);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn antiphase_channels_do_not_cancel_spectral_evidence() {
|
||||
let dir = TempDir::new("antiphase");
|
||||
|
||||
Reference in new issue
Block a user