Skip to content

Commit a9035d2

Browse files
committed
check: refactored to add check-mode (info, audio). Info mode compares format structure
1 parent 6c5dc5b commit a9035d2

File tree

5 files changed

+1071
-410
lines changed

5 files changed

+1071
-410
lines changed

symphonia-check/Cargo.toml

+4-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ rust-version = "1.77"
1111
publish = false
1212

1313
[dependencies]
14-
clap = "3.1.0"
14+
clap = { version = "4.5.23", features = ["derive"] }
1515
log = { version = "0.4", features = ["release_max_level_info"] }
1616
pretty_env_logger = "0.4"
17-
symphonia = { version = "0.5.4", path = "../symphonia", features = ["all", "opt-simd"] }
17+
symphonia = { version = "0.5.4", path = "../symphonia", features = ["all", "opt-simd"] }
18+
serde = { version = "1.0", features = ["derive"] }
19+
serde_json = "1.0"

symphonia-check/src/audio.rs

+365
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
// Symphonia Check Tool
2+
// Copyright (c) 2019-2022 The Project Symphonia Developers.
3+
//
4+
// This Source Code Form is subject to the terms of the Mozilla Public
5+
// License, v. 2.0. If a copy of the MPL was not distributed with this
6+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
8+
#![warn(rust_2018_idioms)]
9+
#![forbid(unsafe_code)]
10+
// Justification: Fields on DecoderOptions and FormatOptions may change at any time, but
11+
// symphonia-check doesn't want to be updated every time those fields change, therefore always fill
12+
// in the remaining fields with default values.
13+
#![allow(clippy::needless_update)]
14+
15+
use std::fs::File;
16+
use std::path::Path;
17+
use std::process::{Command, Stdio};
18+
19+
use log::warn;
20+
use symphonia::core::audio::GenericAudioBufferRef;
21+
use symphonia::core::codecs::audio::{AudioDecoder, AudioDecoderOptions};
22+
use symphonia::core::codecs::CodecParameters;
23+
use symphonia::core::errors::{unsupported_error, Error, Result};
24+
use symphonia::core::formats::probe::Hint;
25+
use symphonia::core::formats::{FormatOptions, FormatReader, TrackType};
26+
use symphonia::core::io::{MediaSourceStream, ReadOnlySource};
27+
use symphonia::core::meta::MetadataOptions;
28+
29+
use crate::{AudioTestDecoder, AudioTestOptions, RefProcess};
30+
31+
/// The absolute maximum allowable sample delta. Around 2^-17 (-102.4dB).
32+
const ABS_MAX_ALLOWABLE_SAMPLE_DELTA: f32 = 0.00001;
33+
34+
// The absolute maximum allowable sample delta for a fully compliant MP3 decoder as specified by the
35+
// ISO. Around 2^-14 (-84.2dB).
36+
// const ABS_MAX_ALLOWABLE_SAMPLE_DELTA_MP3: f32 = 0.00006104;
37+
38+
#[derive(Default)]
39+
struct AudioTestResult {
40+
n_frames: u64,
41+
n_samples: u64,
42+
n_failed_samples: u64,
43+
n_packets: u64,
44+
n_failed_packets: u64,
45+
abs_max_delta: f32,
46+
tgt_unchecked_samples: u64,
47+
ref_unchecked_samples: u64,
48+
}
49+
50+
fn build_ffmpeg_command(path: &str, gapless: bool) -> Command {
51+
let mut cmd = Command::new("ffmpeg");
52+
53+
// Gapless argument must come before everything else.
54+
if !gapless {
55+
cmd.arg("-flags2").arg("skip_manual");
56+
}
57+
58+
cmd.arg("-nostats") // Quiet command.
59+
.arg("-hide_banner")
60+
.arg("-i") // Input path.
61+
.arg(path)
62+
.arg("-map") // Select the first audio track.
63+
.arg("0:a:0")
64+
.arg("-c:a") // Encode audio to pcm_s32le.
65+
.arg("pcm_f32le")
66+
.arg("-f") // Output in WAVE format.
67+
.arg("wav")
68+
.arg("-") // Pipe output to stdout.
69+
.stdout(Stdio::piped())
70+
.stderr(Stdio::null()); // Pipe errors to null.
71+
72+
cmd
73+
}
74+
75+
fn build_flac_command(path: &str) -> Command {
76+
let mut cmd = Command::new("flac");
77+
78+
cmd.arg("--stdout").arg("-d").arg(path).stdout(Stdio::piped()).stderr(Stdio::null());
79+
80+
cmd
81+
}
82+
83+
fn build_mpg123_command(path: &str, gapless: bool) -> Command {
84+
let mut cmd = Command::new("mpg123");
85+
86+
if !gapless {
87+
cmd.arg("--no-gapless");
88+
}
89+
90+
cmd.arg("--wav").arg("-").arg("--float").arg(path).stdout(Stdio::piped()).stderr(Stdio::null());
91+
92+
cmd
93+
}
94+
95+
fn build_oggdec_command(path: &str) -> Command {
96+
let mut cmd = Command::new("oggdec");
97+
cmd.arg(path).arg("-o").arg("-").stdout(Stdio::piped()).stderr(Stdio::null());
98+
cmd
99+
}
100+
101+
#[derive(Default)]
102+
struct FlushStats {
103+
n_packets: u64,
104+
n_samples: u64,
105+
}
106+
107+
struct DecoderInstance {
108+
format: Box<dyn FormatReader>,
109+
decoder: Box<dyn AudioDecoder>,
110+
track_id: u32,
111+
}
112+
113+
impl DecoderInstance {
114+
fn try_open(
115+
mss: MediaSourceStream<'static>,
116+
fmt_opts: FormatOptions,
117+
) -> Result<DecoderInstance> {
118+
// Use the default options for metadata and format readers, and the decoder.
119+
let meta_opts: MetadataOptions = Default::default();
120+
let dec_opts: AudioDecoderOptions = Default::default();
121+
122+
let hint = Hint::new();
123+
124+
let format = symphonia::default::get_probe().probe(&hint, mss, fmt_opts, meta_opts)?;
125+
126+
let track = format.default_track(TrackType::Audio).unwrap();
127+
128+
let codec_params = match &track.codec_params {
129+
Some(CodecParameters::Audio(params)) => params,
130+
_ => return unsupported_error("only audio tracks are supported"),
131+
};
132+
133+
let decoder =
134+
symphonia::default::get_codecs().make_audio_decoder(codec_params, &dec_opts)?;
135+
136+
let track_id = track.id;
137+
138+
Ok(DecoderInstance { format, decoder, track_id })
139+
}
140+
141+
fn samples_per_frame(&self) -> Option<u64> {
142+
self.decoder.codec_params().channels.as_ref().map(|ch| ch.count() as u64)
143+
}
144+
145+
fn next_audio_buf(&mut self, keep_going: bool) -> Result<Option<GenericAudioBufferRef<'_>>> {
146+
loop {
147+
// Get the next packet.
148+
let packet = match self.format.next_packet() {
149+
Ok(Some(packet)) => packet,
150+
Ok(None) => return Ok(None),
151+
Err(Error::IoError(err)) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
152+
// WavReader will always return an UnexpectedEof when it ends because the
153+
// reference decoder is piping the decoded audio and cannot write out the
154+
// actual length of the media. Treat UnexpectedEof as the end of the stream.
155+
return Ok(None);
156+
}
157+
Err(err) => return Err(err),
158+
};
159+
160+
// Skip packets that do not belong to the track being decoded.
161+
if packet.track_id() != self.track_id {
162+
continue;
163+
}
164+
165+
// Decode the packet, ignoring decode errors if `keep_going` is true.
166+
match self.decoder.decode(&packet) {
167+
Ok(_) => break,
168+
Err(Error::DecodeError(err)) if keep_going => warn!("{}", err),
169+
Err(err) => return Err(err),
170+
}
171+
}
172+
173+
Ok(Some(self.decoder.last_decoded()))
174+
}
175+
176+
fn flush(&mut self, keep_going: bool) -> Result<FlushStats> {
177+
let mut stats: FlushStats = Default::default();
178+
179+
while let Some(buf) = self.next_audio_buf(keep_going)? {
180+
stats.n_packets += 1;
181+
stats.n_samples += buf.samples_interleaved() as u64;
182+
}
183+
184+
Ok(stats)
185+
}
186+
}
187+
188+
fn run_check(
189+
ref_inst: &mut DecoderInstance,
190+
tgt_inst: &mut DecoderInstance,
191+
opts: &AudioTestOptions,
192+
acct: &mut AudioTestResult,
193+
) -> Result<()> {
194+
// Reference
195+
let mut ref_sample_buf: Vec<f32> = Default::default();
196+
let mut ref_sample_cnt = 0;
197+
let mut ref_sample_pos = 0;
198+
199+
// Target
200+
let mut tgt_sample_buf: Vec<f32> = Default::default();
201+
let mut tgt_sample_cnt = 0;
202+
let mut tgt_sample_pos = 0;
203+
204+
let samples_per_frame = tgt_inst.samples_per_frame().unwrap_or(1);
205+
206+
// Samples/frame must match for both decoders.
207+
if samples_per_frame != ref_inst.samples_per_frame().unwrap_or(1) {
208+
return unsupported_error("target and reference decoder samples per frame mismatch");
209+
}
210+
211+
let early_fail = 'outer: loop {
212+
// Decode the next target audio buffer and copy it to the target sample buffer.
213+
match tgt_inst.next_audio_buf(opts.keep_going)? {
214+
Some(buf) => buf.copy_to_vec_interleaved(&mut tgt_sample_buf),
215+
None => break 'outer false,
216+
};
217+
218+
tgt_sample_cnt = tgt_sample_buf.len();
219+
tgt_sample_pos = 0;
220+
221+
// The number of frames previously read & compared.
222+
let frame_num_base = acct.n_frames;
223+
224+
// The number of failed samples in the target packet.
225+
let mut n_failed_pkt_samples = 0;
226+
227+
while tgt_sample_pos < tgt_sample_cnt {
228+
// Need to read a decode a new reference buffer.
229+
if ref_sample_pos == ref_sample_cnt {
230+
// Get the next reference audio buffer and copy it to the reference sample buffer.
231+
match ref_inst.next_audio_buf(true)? {
232+
Some(buf) => buf.copy_to_vec_interleaved(&mut ref_sample_buf),
233+
None => break 'outer false,
234+
}
235+
236+
ref_sample_cnt = ref_sample_buf.len();
237+
ref_sample_pos = 0;
238+
}
239+
240+
// Get a slice of the remaining samples in the reference and target sample buffers.
241+
let ref_samples = &ref_sample_buf[ref_sample_pos..];
242+
let tgt_samples = &tgt_sample_buf[tgt_sample_pos..];
243+
244+
// The number of samples that can be compared given the current length of the reference
245+
// and target sample buffers.
246+
let n_test_samples = std::cmp::min(ref_samples.len(), tgt_samples.len());
247+
248+
// Perform the comparison.
249+
for (&t, &r) in tgt_samples[..n_test_samples].iter().zip(&ref_samples[..n_test_samples])
250+
{
251+
// Clamp the reference and target samples between [-1.0, 1.0] and find the
252+
// difference.
253+
let delta = t.clamp(-1.0, 1.0) - r.clamp(-1.0, 1.0);
254+
255+
if delta.abs() > ABS_MAX_ALLOWABLE_SAMPLE_DELTA {
256+
// Print per-sample or per-packet failure nessage based on selected options.
257+
if !opts.is_quiet && (opts.is_per_sample || n_failed_pkt_samples == 0) {
258+
println!(
259+
"[FAIL] packet={:>8}, frame={:>10} ({:>4}), plane={:>3}, dec={:+.8}, ref={:+.8} ({:+.8})",
260+
acct.n_packets,
261+
acct.n_frames,
262+
acct.n_frames - frame_num_base,
263+
acct.n_samples % samples_per_frame,
264+
t,
265+
r,
266+
r - t
267+
);
268+
}
269+
270+
n_failed_pkt_samples += 1;
271+
}
272+
273+
acct.abs_max_delta = acct.abs_max_delta.max(delta.abs());
274+
acct.n_samples += 1;
275+
276+
if acct.n_samples % samples_per_frame == 0 {
277+
acct.n_frames += 1;
278+
}
279+
}
280+
281+
// Update position in reference and target buffers.
282+
ref_sample_pos += n_test_samples;
283+
tgt_sample_pos += n_test_samples;
284+
}
285+
286+
acct.n_failed_samples += n_failed_pkt_samples;
287+
acct.n_failed_packets += u64::from(n_failed_pkt_samples > 0);
288+
acct.n_packets += 1;
289+
290+
if opts.stop_after_fail && acct.n_failed_packets > 0 {
291+
break true;
292+
}
293+
};
294+
295+
// Count how many samples were remaining for both the target and references if the loop did not
296+
// break out early due to a failed sample.
297+
if !early_fail {
298+
let tgt_stats = tgt_inst.flush(true)?;
299+
let ref_stats = ref_inst.flush(true)?;
300+
301+
acct.n_packets += tgt_stats.n_packets;
302+
acct.tgt_unchecked_samples = (tgt_sample_cnt - tgt_sample_pos) as u64 + tgt_stats.n_samples;
303+
acct.ref_unchecked_samples = (ref_sample_cnt - ref_sample_pos) as u64 + ref_stats.n_samples;
304+
}
305+
306+
Ok(())
307+
}
308+
309+
fn run_test(path: &str, opts: &AudioTestOptions, result: &mut AudioTestResult) -> Result<()> {
310+
let command = match opts.ref_decoder {
311+
AudioTestDecoder::Ffmpeg => build_ffmpeg_command(path, opts.gapless),
312+
AudioTestDecoder::Flac => build_flac_command(path),
313+
AudioTestDecoder::Mpg123 => build_mpg123_command(path, opts.gapless),
314+
AudioTestDecoder::Oggdec => build_oggdec_command(path),
315+
};
316+
317+
// 1. Start the reference decoder process.
318+
let mut ref_process = RefProcess::try_spawn(command)?;
319+
320+
// 2. Instantiate a Symphonia decoder for the reference process output.
321+
let ref_ms = Box::new(ReadOnlySource::new(ref_process.child.stdout.take().unwrap()));
322+
let ref_mss = MediaSourceStream::new(ref_ms, Default::default());
323+
324+
let mut ref_inst = DecoderInstance::try_open(ref_mss, Default::default())?;
325+
326+
// 3. Instantiate a Symphonia decoder for the test target.
327+
let tgt_ms = Box::new(File::open(Path::new(path))?);
328+
let tgt_mss = MediaSourceStream::new(tgt_ms, Default::default());
329+
330+
let tgt_fmt_opts = FormatOptions { enable_gapless: opts.gapless, ..Default::default() };
331+
332+
let mut tgt_inst = DecoderInstance::try_open(tgt_mss, tgt_fmt_opts)?;
333+
334+
// 4. Begin check.
335+
run_check(&mut ref_inst, &mut tgt_inst, opts, result)
336+
}
337+
338+
pub fn run_audio(opts: AudioTestOptions) -> Result<()> {
339+
let mut res: AudioTestResult = Default::default();
340+
341+
run_test(&opts.input, &opts, &mut res)?;
342+
343+
if !opts.is_quiet {
344+
println!();
345+
}
346+
347+
println!("Test Results");
348+
println!("=================================================");
349+
println!();
350+
println!(" Failed/Total Packets: {:>12}/{:>12}", res.n_failed_packets, res.n_packets);
351+
println!(" Failed/Total Samples: {:>12}/{:>12}", res.n_failed_samples, res.n_samples);
352+
println!();
353+
println!(" Remaining Target Samples: {:>12}", res.tgt_unchecked_samples);
354+
println!(" Remaining Reference Samples: {:>12}", res.ref_unchecked_samples);
355+
println!();
356+
println!(" Absolute Maximum Sample Delta: {:.8}", res.abs_max_delta);
357+
println!();
358+
359+
if res.n_failed_samples == 0 {
360+
Ok(())
361+
}
362+
else {
363+
unsupported_error("Some samples didn't pass validation")
364+
}
365+
}

0 commit comments

Comments
 (0)