blob: 20f5c58f6c1d49b5a4b54fc73919989f3f067697 [file] [log] [blame]
use crate::pcm_audio::*;
use crate::timestamp_validator::*;
use fidl_fuchsia_media::*;
use fidl_fuchsia_sysmem::*;
use fuchsia_zircon as zx;
use hex::encode;
use mundane::hash::{Digest, Hasher, Sha256};
use rand::prelude::*;
use std::io::Write;
use std::rc::Rc;
use stream_processor_encoder_factory::*;
use stream_processor_test::*;
pub const TEST_PCM_FRAME_COUNT: usize = 3000;
// TODO(turnage): Generalize and promote to stream_processor_test lib.
pub struct BytesValidator {
pub output_file: Option<&'static str>,
pub expected_digest: ExpectedDigest,
}
impl BytesValidator {
fn write_and_hash(
&self,
mut file: impl Write,
oob: &[u8],
packets: &[&OutputPacket],
) -> Result<()> {
let mut hasher = Sha256::default();
hasher.update(oob);
for packet in packets {
file.write_all(&packet.data)?;
hasher.update(&packet.data);
}
let digest = hasher.finish().bytes();
if self.expected_digest.bytes != digest {
return Err(FatalError(format!(
"Expected {}; got {}",
self.expected_digest,
encode(digest)
))
.into());
}
Ok(())
}
fn output_file(&self) -> Result<impl Write> {
Ok(if let Some(file) = self.output_file {
Box::new(std::fs::File::create(file)?) as Box<dyn Write>
} else {
Box::new(std::io::sink()) as Box<dyn Write>
})
}
}
impl OutputValidator for BytesValidator {
fn validate(&self, output: &[Output]) -> Result<()> {
let packets: Vec<&OutputPacket> = output_packets(output).collect();
let oob = packets
.first()
.ok_or(FatalError(String::from("No packets in output")))?
.format
.format_details
.oob_bytes
.clone()
.unwrap_or(vec![]);
self.write_and_hash(self.output_file()?, oob.as_slice(), &packets)
}
}
pub struct AudioEncoderTestCase {
/// Encoder settings.
// This is a function because FIDL unions are not Copy or Clone.
pub settings: Rc<dyn Fn() -> EncoderSettings>,
/// The number of PCM input frames per encoded frame.
pub input_framelength: usize,
pub channel_count: usize,
pub hash_tests: Vec<AudioEncoderHashTest>,
}
/// A hash test runs audio through the encoder and checks that all that data emitted when hashed
/// sequentially results in the expected digest. Oob bytes are hashed first.
pub struct AudioEncoderHashTest {
/// If provided, the output will also be written to this file. Use this to verify new files
/// with a decoder before using their digest in tests.
pub output_file: Option<&'static str>,
pub input_format: PcmFormat,
pub output_packet_count: usize,
pub expected_digest: ExpectedDigest,
}
impl AudioEncoderTestCase {
pub async fn run(self) -> Result<()> {
self.test_termination().await?;
self.test_timestamps().await?;
self.test_hashes().await
}
async fn test_hashes(self) -> Result<()> {
let mut cases = vec![];
let easy_framelength = self.input_framelength;
for (hash_test, stream_lifetime_ordinal) in
self.hash_tests.into_iter().zip(OrdinalPattern::Odd.into_iter())
{
let settings = self.settings.clone();
let pcm_audio = PcmAudio::create_saw_wave(hash_test.input_format, TEST_PCM_FRAME_COUNT);
let stream = Rc::new(PcmAudioStream {
pcm_audio,
encoder_settings: move || (settings)(),
frames_per_packet: (0..).map(move |_| easy_framelength),
timebase: None,
});
cases.push(TestCase {
name: "Audio encoder hash test",
stream,
validators: vec![
Rc::new(TerminatesWithValidator {
expected_terminal_output: Output::Eos { stream_lifetime_ordinal },
}),
Rc::new(OutputPacketCountValidator {
expected_output_packet_count: hash_test.output_packet_count,
}),
Rc::new(BytesValidator {
output_file: hash_test.output_file,
expected_digest: hash_test.expected_digest,
}),
],
stream_options: Some(StreamOptions {
queue_format_details: false,
..StreamOptions::default()
}),
});
}
let spec = TestSpec {
cases,
relation: CaseRelation::Serial,
stream_processor_factory: Rc::new(EncoderFactory),
};
spec.run().await
}
async fn test_termination(&self) -> Result<()> {
let easy_framelength = self.input_framelength;
let stream = self.create_test_stream((0..).map(move |_| easy_framelength));
let eos_validator = Rc::new(TerminatesWithValidator {
expected_terminal_output: Output::Eos { stream_lifetime_ordinal: 1 },
});
let case = TestCase {
name: "Terminates with EOS test",
stream,
validators: vec![eos_validator],
stream_options: None,
};
let spec = TestSpec {
cases: vec![case],
relation: CaseRelation::Concurrent,
stream_processor_factory: Rc::new(EncoderFactory),
};
spec.run().await
}
async fn test_timestamps(&self) -> Result<()> {
let max_framelength = self.input_framelength * 5;
let fixed_framelength = self.input_framelength + 1;
let fixed_framelength_stream =
self.create_test_stream((0..).map(move |_| fixed_framelength));
let pcm_frame_size = fixed_framelength_stream.pcm_audio.frame_size();
let stream_options = Some(StreamOptions {
input_buffer_collection_constraints: Some(BufferCollectionConstraints {
has_buffer_memory_constraints: true,
buffer_memory_constraints: BufferMemoryConstraints {
min_size_bytes: (max_framelength * pcm_frame_size) as u32,
..BUFFER_MEMORY_CONSTRAINTS_DEFAULT
},
..BUFFER_COLLECTION_CONSTRAINTS_DEFAULT
}),
..StreamOptions::default()
});
let fixed_framelength_case = TestCase {
name: "Timestamp extrapolation test - fixed framelength",
validators: vec![Rc::new(TimestampValidator::new(
self.input_framelength,
pcm_frame_size,
fixed_framelength_stream.timestamp_generator(),
fixed_framelength_stream.as_ref(),
))],
stream: fixed_framelength_stream,
stream_options,
};
let variable_framelength_stream = self.create_test_stream((0..).map(move |i| {
let mut rng = StdRng::seed_from_u64(i as u64);
rng.gen::<usize>() % max_framelength + 1
}));
let variable_framelength_case = TestCase {
name: "Timestamp extrapolation test - variable framelength",
validators: vec![Rc::new(TimestampValidator::new(
self.input_framelength,
pcm_frame_size,
variable_framelength_stream.timestamp_generator(),
variable_framelength_stream.as_ref(),
))],
stream: variable_framelength_stream,
stream_options,
};
let spec = TestSpec {
cases: vec![fixed_framelength_case, variable_framelength_case],
relation: CaseRelation::Concurrent,
stream_processor_factory: Rc::new(EncoderFactory),
};
spec.run().await
}
fn create_test_stream(
&self,
frames_per_packet: impl Iterator<Item = usize> + Clone,
) -> Rc<PcmAudioStream<impl Iterator<Item = usize> + Clone, impl Fn() -> EncoderSettings>> {
let pcm_format = PcmFormat {
pcm_mode: AudioPcmMode::Linear,
bits_per_sample: 16,
frames_per_second: 44100,
channel_map: match self.channel_count {
1 => vec![AudioChannelId::Cf],
2 => vec![AudioChannelId::Lf, AudioChannelId::Rf],
c => panic!("{} is not a valid channel count", c),
},
};
let pcm_audio = PcmAudio::create_saw_wave(pcm_format.clone(), TEST_PCM_FRAME_COUNT);
let settings = self.settings.clone();
Rc::new(PcmAudioStream {
pcm_audio,
encoder_settings: move || (settings)(),
frames_per_packet: frames_per_packet,
timebase: Some(zx::Duration::from_seconds(1).into_nanos() as u64),
})
}
}