use mousiki::c_style_api::opus_decoder::{opus_decode, opus_decoder_create};
use mousiki::c_style_api::opus_encoder::{
OpusEncoderCtlRequest, opus_encode, opus_encoder_create, opus_encoder_ctl,
};
use std::env;
use std::fmt;
use std::fs;
use std::io::{Cursor, Read, Write};
use std::path::{Path, PathBuf};
use std::time::Instant;
const MAGIC: [u8; 8] = *b"OPUSBEN1";
const MAX_PACKET_SIZE: usize = 3 * 1276;
#[cfg(feature = "dhat_alloc")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
fn main() {
#[cfg(feature = "dhat_alloc")]
let _profiler = dhat::Profiler::new_heap();
if let Err(err) = run() {
eprintln!("{err}");
std::process::exit(1);
}
}
fn run() -> Result<(), BenchError> {
let mut args = env::args();
let _program = args.next();
let command = args.next().ok_or(BenchError::Usage(usage()))?;
let rest: Vec<String> = args.collect();
match command.as_str() {
"packets" => {
let config = PacketCorpusArgs::parse(&rest)?;
write_packet_corpus(&config)?;
}
"encode" => {
let config = EncodeBenchArgs::parse(&rest)?;
let stats = benchmark_encode(&config)?;
print_result(&stats, config.output);
}
"decode" => {
let config = DecodeBenchArgs::parse(&rest)?;
let stats = benchmark_decode(&config)?;
print_result(&stats, config.output);
}
_ => return Err(BenchError::Usage(usage())),
}
Ok(())
}
fn usage() -> String {
[
"usage:",
" codec_bench packets --input INPUT.pcm --output OUTPUT.opusbench \\",
" --sample-rate 48000 --channels 2 --frame-size 960 \\",
" --application audio --bitrate 64000 [--complexity 10] [--bitrate-mode cvbr] [--max-frames N]",
" codec_bench encode --input INPUT.pcm --sample-rate 48000 --channels 2 --frame-size 960 \\",
" --application audio --bitrate 64000 [--complexity 10] [--bitrate-mode cvbr] \\",
" [--warmup 3] [--measure 10] [--max-frames N] [--format text|csv] [--no-header]",
" codec_bench decode --packets INPUT.opusbench [--warmup 3] [--measure 10] [--max-frames N] \\",
" [--format text|csv] [--no-header]",
]
.join("\n")
}
#[derive(Clone, Copy)]
enum OutputFormat {
Text,
Csv,
}
#[derive(Clone, Copy)]
struct OutputOptions {
format: OutputFormat,
header: bool,
}
impl Default for OutputOptions {
fn default() -> Self {
Self {
format: OutputFormat::Text,
header: true,
}
}
}
#[derive(Clone, Copy)]
enum BitrateMode {
Vbr,
Cvbr,
Cbr,
}
impl BitrateMode {
fn parse(value: &str) -> Result<Self, BenchError> {
match value {
"vbr" => Ok(Self::Vbr),
"cvbr" => Ok(Self::Cvbr),
"cbr" => Ok(Self::Cbr),
_ => Err(BenchError::Argument(format!(
"invalid --bitrate-mode '{value}', expected vbr|cvbr|cbr"
))),
}
}
const fn as_str(self) -> &'static str {
match self {
Self::Vbr => "vbr",
Self::Cvbr => "cvbr",
Self::Cbr => "cbr",
}
}
}
#[derive(Clone, Copy)]
enum ApplicationArg {
Voip,
Audio,
RestrictedLowDelay,
}
impl ApplicationArg {
fn parse(value: &str) -> Result<Self, BenchError> {
match value {
"voip" => Ok(Self::Voip),
"audio" => Ok(Self::Audio),
"restricted-lowdelay" | "restricted_lowdelay" | "lowdelay" => {
Ok(Self::RestrictedLowDelay)
}
_ => Err(BenchError::Argument(format!(
"invalid --application '{value}', expected voip|audio|restricted-lowdelay"
))),
}
}
const fn as_str(self) -> &'static str {
match self {
Self::Voip => "voip",
Self::Audio => "audio",
Self::RestrictedLowDelay => "restricted-lowdelay",
}
}
const fn to_opus_int(self) -> i32 {
match self {
Self::Voip => 2048,
Self::Audio => 2049,
Self::RestrictedLowDelay => 2051,
}
}
}
#[derive(Clone, Copy)]
struct EncodeConfig {
sample_rate: i32,
channels: i32,
frame_size: usize,
application: ApplicationArg,
bitrate: i32,
complexity: i32,
bitrate_mode: BitrateMode,
}
impl EncodeConfig {
const fn max_frame_size(self) -> usize {
self.frame_size * 6
}
}
struct PacketCorpusArgs {
input: PathBuf,
output: PathBuf,
encode: EncodeConfig,
max_frames: Option<usize>,
}
impl PacketCorpusArgs {
fn parse(args: &[String]) -> Result<Self, BenchError> {
let mut input = None;
let mut output = None;
let mut config = EncodeDefaults::default();
let mut max_frames = None;
let mut idx = 0;
while idx < args.len() {
match args[idx].as_str() {
"--input" => input = Some(PathBuf::from(next_value(args, &mut idx, "--input")?)),
"--output" => output = Some(PathBuf::from(next_value(args, &mut idx, "--output")?)),
"--sample-rate" => config.sample_rate = parse_i32(args, &mut idx, "--sample-rate")?,
"--channels" => config.channels = parse_i32(args, &mut idx, "--channels")?,
"--frame-size" => config.frame_size = parse_usize(args, &mut idx, "--frame-size")?,
"--application" => {
config.application =
ApplicationArg::parse(next_value(args, &mut idx, "--application")?)?
}
"--bitrate" => config.bitrate = parse_i32(args, &mut idx, "--bitrate")?,
"--complexity" => config.complexity = parse_i32(args, &mut idx, "--complexity")?,
"--bitrate-mode" => {
config.bitrate_mode =
BitrateMode::parse(next_value(args, &mut idx, "--bitrate-mode")?)?
}
"--max-frames" => max_frames = Some(parse_usize(args, &mut idx, "--max-frames")?),
other => return Err(BenchError::Argument(format!("unknown argument '{other}'"))),
}
idx += 1;
}
Ok(Self {
input: input.ok_or_else(|| BenchError::Argument("missing --input".into()))?,
output: output.ok_or_else(|| BenchError::Argument("missing --output".into()))?,
encode: config.finish(),
max_frames,
})
}
}
struct EncodeBenchArgs {
input: PathBuf,
encode: EncodeConfig,
warmup: usize,
measure: usize,
max_frames: Option<usize>,
output: OutputOptions,
}
impl EncodeBenchArgs {
fn parse(args: &[String]) -> Result<Self, BenchError> {
let mut input = None;
let mut config = EncodeDefaults::default();
let mut warmup = 3;
let mut measure = 10;
let mut max_frames = None;
let mut output = OutputOptions::default();
let mut idx = 0;
while idx < args.len() {
match args[idx].as_str() {
"--input" => input = Some(PathBuf::from(next_value(args, &mut idx, "--input")?)),
"--sample-rate" => config.sample_rate = parse_i32(args, &mut idx, "--sample-rate")?,
"--channels" => config.channels = parse_i32(args, &mut idx, "--channels")?,
"--frame-size" => config.frame_size = parse_usize(args, &mut idx, "--frame-size")?,
"--application" => {
config.application =
ApplicationArg::parse(next_value(args, &mut idx, "--application")?)?
}
"--bitrate" => config.bitrate = parse_i32(args, &mut idx, "--bitrate")?,
"--complexity" => config.complexity = parse_i32(args, &mut idx, "--complexity")?,
"--bitrate-mode" => {
config.bitrate_mode =
BitrateMode::parse(next_value(args, &mut idx, "--bitrate-mode")?)?
}
"--warmup" => warmup = parse_usize(args, &mut idx, "--warmup")?,
"--measure" => measure = parse_usize(args, &mut idx, "--measure")?,
"--max-frames" => max_frames = Some(parse_usize(args, &mut idx, "--max-frames")?),
"--format" => {
output.format = parse_output_format(next_value(args, &mut idx, "--format")?)?
}
"--no-header" => output.header = false,
other => return Err(BenchError::Argument(format!("unknown argument '{other}'"))),
}
idx += 1;
}
Ok(Self {
input: input.ok_or_else(|| BenchError::Argument("missing --input".into()))?,
encode: config.finish(),
warmup,
measure,
max_frames,
output,
})
}
}
struct DecodeBenchArgs {
packets: PathBuf,
warmup: usize,
measure: usize,
max_frames: Option<usize>,
output: OutputOptions,
}
impl DecodeBenchArgs {
fn parse(args: &[String]) -> Result<Self, BenchError> {
let mut packets = None;
let mut warmup = 3;
let mut measure = 10;
let mut max_frames = None;
let mut output = OutputOptions::default();
let mut idx = 0;
while idx < args.len() {
match args[idx].as_str() {
"--packets" => {
packets = Some(PathBuf::from(next_value(args, &mut idx, "--packets")?))
}
"--warmup" => warmup = parse_usize(args, &mut idx, "--warmup")?,
"--measure" => measure = parse_usize(args, &mut idx, "--measure")?,
"--max-frames" => max_frames = Some(parse_usize(args, &mut idx, "--max-frames")?),
"--format" => {
output.format = parse_output_format(next_value(args, &mut idx, "--format")?)?
}
"--no-header" => output.header = false,
other => return Err(BenchError::Argument(format!("unknown argument '{other}'"))),
}
idx += 1;
}
Ok(Self {
packets: packets.ok_or_else(|| BenchError::Argument("missing --packets".into()))?,
warmup,
measure,
max_frames,
output,
})
}
}
struct EncodeDefaults {
sample_rate: i32,
channels: i32,
frame_size: usize,
application: ApplicationArg,
bitrate: i32,
complexity: i32,
bitrate_mode: BitrateMode,
}
impl Default for EncodeDefaults {
fn default() -> Self {
Self {
sample_rate: 48_000,
channels: 2,
frame_size: 960,
application: ApplicationArg::Audio,
bitrate: 64_000,
complexity: 10,
bitrate_mode: BitrateMode::Cvbr,
}
}
}
impl EncodeDefaults {
fn finish(self) -> EncodeConfig {
EncodeConfig {
sample_rate: self.sample_rate,
channels: self.channels,
frame_size: self.frame_size,
application: self.application,
bitrate: self.bitrate,
complexity: self.complexity,
bitrate_mode: self.bitrate_mode,
}
}
}
fn parse_output_format(value: &str) -> Result<OutputFormat, BenchError> {
match value {
"text" => Ok(OutputFormat::Text),
"csv" => Ok(OutputFormat::Csv),
_ => Err(BenchError::Argument(format!(
"invalid --format '{value}', expected text|csv"
))),
}
}
fn next_value<'a>(args: &'a [String], idx: &mut usize, flag: &str) -> Result<&'a str, BenchError> {
*idx += 1;
args.get(*idx)
.map(String::as_str)
.ok_or_else(|| BenchError::Argument(format!("missing value for {flag}")))
}
fn parse_i32(args: &[String], idx: &mut usize, flag: &str) -> Result<i32, BenchError> {
next_value(args, idx, flag)?
.parse::<i32>()
.map_err(|_| BenchError::Argument(format!("invalid integer for {flag}")))
}
fn parse_usize(args: &[String], idx: &mut usize, flag: &str) -> Result<usize, BenchError> {
next_value(args, idx, flag)?
.parse::<usize>()
.map_err(|_| BenchError::Argument(format!("invalid integer for {flag}")))
}
struct PacketCorpus {
header: PacketHeader,
packets: Vec<Vec<u8>>,
}
#[derive(Clone, Copy)]
struct PacketHeader {
sample_rate: u32,
channels: u16,
frame_size: u16,
application: u32,
bitrate: i32,
complexity: u8,
bitrate_mode: BitrateMode,
}
impl PacketHeader {
fn write(self, mut writer: impl Write) -> Result<(), BenchError> {
writer.write_all(&MAGIC)?;
writer.write_all(&self.sample_rate.to_le_bytes())?;
writer.write_all(&self.channels.to_le_bytes())?;
writer.write_all(&self.frame_size.to_le_bytes())?;
writer.write_all(&self.application.to_le_bytes())?;
writer.write_all(&self.bitrate.to_le_bytes())?;
writer.write_all(&[self.complexity])?;
writer.write_all(&[match self.bitrate_mode {
BitrateMode::Vbr => 0,
BitrateMode::Cvbr => 1,
BitrateMode::Cbr => 2,
}])?;
writer.write_all(&[0, 0])?;
Ok(())
}
fn read(mut reader: impl Read) -> Result<Self, BenchError> {
let mut magic = [0u8; 8];
reader.read_exact(&mut magic)?;
if magic != MAGIC {
return Err(BenchError::Corpus("invalid packet corpus magic".into()));
}
let sample_rate = read_u32(&mut reader)?;
let channels = read_u16(&mut reader)?;
let frame_size = read_u16(&mut reader)?;
let application = read_u32(&mut reader)?;
let bitrate = read_i32(&mut reader)?;
let complexity = read_u8(&mut reader)?;
let bitrate_mode = match read_u8(&mut reader)? {
0 => BitrateMode::Vbr,
1 => BitrateMode::Cvbr,
2 => BitrateMode::Cbr,
value => {
return Err(BenchError::Corpus(format!(
"invalid packet corpus bitrate mode {value}"
)));
}
};
let _reserved = read_u16(&mut reader)?;
Ok(Self {
sample_rate,
channels,
frame_size,
application,
bitrate,
complexity,
bitrate_mode,
})
}
}
fn read_u8(reader: &mut impl Read) -> Result<u8, BenchError> {
let mut buf = [0u8; 1];
reader.read_exact(&mut buf)?;
Ok(buf[0])
}
fn read_u16(reader: &mut impl Read) -> Result<u16, BenchError> {
let mut buf = [0u8; 2];
reader.read_exact(&mut buf)?;
Ok(u16::from_le_bytes(buf))
}
fn read_u32(reader: &mut impl Read) -> Result<u32, BenchError> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
fn read_i32(reader: &mut impl Read) -> Result<i32, BenchError> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
Ok(i32::from_le_bytes(buf))
}
fn write_packet_corpus(config: &PacketCorpusArgs) -> Result<(), BenchError> {
let pcm = load_pcm_samples(&config.input)?;
let frames = frame_count(pcm.len(), config.encode)?;
let frame_limit = config.max_frames.unwrap_or(frames).min(frames);
if frame_limit == 0 {
return Err(BenchError::Argument(
"packet corpus would contain zero frames".into(),
));
}
let corpus = encode_corpus(&pcm, config.encode, frame_limit)?;
let output = &config.output;
let mut file = fs::File::create(output)?;
corpus.header.write(&mut file)?;
for packet in &corpus.packets {
let len = u16::try_from(packet.len())
.map_err(|_| BenchError::Corpus(format!("packet too large: {}", packet.len())))?;
file.write_all(&len.to_le_bytes())?;
file.write_all(packet)?;
}
println!(
"wrote {} packets to {}",
corpus.packets.len(),
output.display()
);
Ok(())
}
fn encode_corpus(
pcm: &[i16],
config: EncodeConfig,
frame_limit: usize,
) -> Result<PacketCorpus, BenchError> {
let mut encoder = create_encoder(config)?;
let channels = usize::try_from(config.channels)
.map_err(|_| BenchError::Argument("channels out of range".into()))?;
let frame_samples = config.frame_size * channels;
let mut packet_buf = vec![0u8; MAX_PACKET_SIZE];
let mut packets = Vec::with_capacity(frame_limit);
for frame in 0..frame_limit {
let start = frame * frame_samples;
let end = start + frame_samples;
let len = opus_encode(
&mut encoder,
&pcm[start..end],
config.frame_size,
&mut packet_buf,
)
.map_err(BenchError::Encode)?;
packets.push(packet_buf[..len].to_vec());
}
Ok(PacketCorpus {
header: PacketHeader {
sample_rate: u32::try_from(config.sample_rate)
.map_err(|_| BenchError::Argument("sample rate out of range".into()))?,
channels: u16::try_from(config.channels)
.map_err(|_| BenchError::Argument("channels out of range".into()))?,
frame_size: u16::try_from(config.frame_size)
.map_err(|_| BenchError::Argument("frame size out of range".into()))?,
application: u32::try_from(config.application.to_opus_int())
.map_err(|_| BenchError::Argument("application out of range".into()))?,
bitrate: config.bitrate,
complexity: u8::try_from(config.complexity)
.map_err(|_| BenchError::Argument("complexity out of range".into()))?,
bitrate_mode: config.bitrate_mode,
},
packets,
})
}
fn benchmark_encode(config: &EncodeBenchArgs) -> Result<BenchResult, BenchError> {
let pcm = load_pcm_samples(&config.input)?;
let total_frames = frame_count(pcm.len(), config.encode)?;
let frame_limit = config.max_frames.unwrap_or(total_frames).min(total_frames);
if frame_limit == 0 {
return Err(BenchError::Argument(
"encode benchmark would process zero frames".into(),
));
}
let channels = usize::try_from(config.encode.channels)
.map_err(|_| BenchError::Argument("channels out of range".into()))?;
let frame_samples = config.encode.frame_size * channels;
let mut measurements = Vec::with_capacity(config.measure);
for _ in 0..config.warmup {
run_encode_iteration(&pcm, config.encode, frame_limit, frame_samples)?;
}
for _ in 0..config.measure {
measurements.push(run_encode_iteration(
&pcm,
config.encode,
frame_limit,
frame_samples,
)?);
}
Ok(BenchResult::from_measurements(
"rust",
"encode",
config.encode,
frame_limit,
config.warmup,
config.measure,
measurements,
))
}
fn run_encode_iteration(
pcm: &[i16],
config: EncodeConfig,
frame_limit: usize,
frame_samples: usize,
) -> Result<u128, BenchError> {
let mut encoder = create_encoder(config)?;
let mut packet_buf = vec![0u8; MAX_PACKET_SIZE];
let start = Instant::now();
for frame in 0..frame_limit {
let offset = frame * frame_samples;
let end = offset + frame_samples;
let _ = opus_encode(
&mut encoder,
&pcm[offset..end],
config.frame_size,
&mut packet_buf,
)
.map_err(BenchError::Encode)?;
}
Ok(start.elapsed().as_nanos())
}
fn benchmark_decode(config: &DecodeBenchArgs) -> Result<BenchResult, BenchError> {
let corpus = load_packet_corpus(&config.packets)?;
let total_frames = corpus.packets.len();
let frame_limit = config.max_frames.unwrap_or(total_frames).min(total_frames);
if frame_limit == 0 {
return Err(BenchError::Argument(
"decode benchmark would process zero frames".into(),
));
}
let encode = EncodeConfig {
sample_rate: i32::try_from(corpus.header.sample_rate)
.map_err(|_| BenchError::Corpus("sample rate out of range".into()))?,
channels: i32::from(corpus.header.channels),
frame_size: usize::from(corpus.header.frame_size),
application: match corpus.header.application {
2048 => ApplicationArg::Voip,
2049 => ApplicationArg::Audio,
2051 => ApplicationArg::RestrictedLowDelay,
value => {
return Err(BenchError::Corpus(format!(
"invalid application code {value}"
)));
}
},
bitrate: corpus.header.bitrate,
complexity: i32::from(corpus.header.complexity),
bitrate_mode: corpus.header.bitrate_mode,
};
let max_frame_size = encode.max_frame_size();
let channels = usize::try_from(encode.channels)
.map_err(|_| BenchError::Corpus("channels out of range".into()))?;
let mut measurements = Vec::with_capacity(config.measure);
for _ in 0..config.warmup {
run_decode_iteration(&corpus, encode, frame_limit, max_frame_size, channels)?;
}
for _ in 0..config.measure {
measurements.push(run_decode_iteration(
&corpus,
encode,
frame_limit,
max_frame_size,
channels,
)?);
}
Ok(BenchResult::from_measurements(
"rust",
"decode",
encode,
frame_limit,
config.warmup,
config.measure,
measurements,
))
}
fn run_decode_iteration(
corpus: &PacketCorpus,
config: EncodeConfig,
frame_limit: usize,
max_frame_size: usize,
channels: usize,
) -> Result<u128, BenchError> {
let mut decoder =
opus_decoder_create(config.sample_rate, config.channels).map_err(BenchError::DecodeInit)?;
let mut pcm = vec![0i16; max_frame_size * channels];
let start = Instant::now();
for packet in corpus.packets.iter().take(frame_limit) {
let _ = opus_decode(
&mut decoder,
Some(packet),
packet.len(),
&mut pcm,
max_frame_size,
false,
)
.map_err(BenchError::Decode)?;
}
Ok(start.elapsed().as_nanos())
}
fn create_encoder(
config: EncodeConfig,
) -> Result<mousiki::c_style_api::opus_encoder::OpusEncoder<'static>, BenchError> {
let mut encoder = opus_encoder_create(
config.sample_rate,
config.channels,
config.application.to_opus_int(),
)
.map_err(BenchError::EncodeInit)?;
opus_encoder_ctl(
&mut encoder,
OpusEncoderCtlRequest::SetBitrate(config.bitrate),
)
.map_err(BenchError::EncodeCtl)?;
opus_encoder_ctl(
&mut encoder,
OpusEncoderCtlRequest::SetComplexity(config.complexity),
)
.map_err(BenchError::EncodeCtl)?;
match config.bitrate_mode {
BitrateMode::Vbr => {
opus_encoder_ctl(&mut encoder, OpusEncoderCtlRequest::SetVbr(true))
.map_err(BenchError::EncodeCtl)?;
opus_encoder_ctl(&mut encoder, OpusEncoderCtlRequest::SetVbrConstraint(false))
.map_err(BenchError::EncodeCtl)?;
}
BitrateMode::Cvbr => {
opus_encoder_ctl(&mut encoder, OpusEncoderCtlRequest::SetVbr(true))
.map_err(BenchError::EncodeCtl)?;
opus_encoder_ctl(&mut encoder, OpusEncoderCtlRequest::SetVbrConstraint(true))
.map_err(BenchError::EncodeCtl)?;
}
BitrateMode::Cbr => {
opus_encoder_ctl(&mut encoder, OpusEncoderCtlRequest::SetVbr(false))
.map_err(BenchError::EncodeCtl)?;
opus_encoder_ctl(&mut encoder, OpusEncoderCtlRequest::SetVbrConstraint(true))
.map_err(BenchError::EncodeCtl)?;
}
}
Ok(encoder)
}
fn load_pcm_samples(path: &Path) -> Result<Vec<i16>, BenchError> {
let bytes = fs::read(path)?;
if !bytes.len().is_multiple_of(2) {
return Err(BenchError::Input(format!(
"pcm byte length must be even, got {}",
bytes.len()
)));
}
let mut samples = Vec::with_capacity(bytes.len() / 2);
for chunk in bytes.chunks_exact(2) {
samples.push(i16::from_le_bytes([chunk[0], chunk[1]]));
}
Ok(samples)
}
fn frame_count(sample_count: usize, config: EncodeConfig) -> Result<usize, BenchError> {
let channels = usize::try_from(config.channels)
.map_err(|_| BenchError::Argument("channels out of range".into()))?;
let frame_samples = config.frame_size * channels;
if frame_samples == 0 {
return Err(BenchError::Argument("frame size must be non-zero".into()));
}
Ok(sample_count / frame_samples)
}
fn load_packet_corpus(path: &Path) -> Result<PacketCorpus, BenchError> {
let bytes = fs::read(path)?;
let mut cursor = Cursor::new(bytes);
let header = PacketHeader::read(&mut cursor)?;
let mut packets = Vec::new();
loop {
let position = usize::try_from(cursor.position())
.map_err(|_| BenchError::Corpus("cursor position overflow".into()))?;
if position == cursor.get_ref().len() {
break;
}
if position + 2 > cursor.get_ref().len() {
return Err(BenchError::Corpus("truncated packet length".into()));
}
let len = usize::from(read_u16(&mut cursor)?);
let position = usize::try_from(cursor.position())
.map_err(|_| BenchError::Corpus("cursor position overflow".into()))?;
let end = position
.checked_add(len)
.ok_or_else(|| BenchError::Corpus("packet length overflow".into()))?;
if end > cursor.get_ref().len() {
return Err(BenchError::Corpus("truncated packet data".into()));
}
packets.push(cursor.get_ref()[position..end].to_vec());
cursor.set_position(
u64::try_from(end)
.map_err(|_| BenchError::Corpus("cursor position overflow".into()))?,
);
}
Ok(PacketCorpus { header, packets })
}
struct BenchResult {
implementation: &'static str,
operation: &'static str,
config: EncodeConfig,
frames: usize,
warmup: usize,
measure: usize,
median_ns_per_frame: f64,
p95_ns_per_frame: f64,
median_packets_per_sec: f64,
median_realtime_x: f64,
}
impl BenchResult {
fn from_measurements(
implementation: &'static str,
operation: &'static str,
config: EncodeConfig,
frames: usize,
warmup: usize,
measure: usize,
measurements: Vec<u128>,
) -> Self {
let mut sorted = measurements;
sorted.sort_unstable();
let median_elapsed = percentile(&sorted, 50);
let p95_elapsed = percentile(&sorted, 95);
let median_ns_per_frame = median_elapsed as f64 / frames as f64;
let p95_ns_per_frame = p95_elapsed as f64 / frames as f64;
let frames_per_sec = 1_000_000_000.0 / median_ns_per_frame;
let realtime_x = frames_per_sec * config.frame_size as f64 / config.sample_rate as f64;
Self {
implementation,
operation,
config,
frames,
warmup,
measure,
median_ns_per_frame,
p95_ns_per_frame,
median_packets_per_sec: frames_per_sec,
median_realtime_x: realtime_x,
}
}
}
fn percentile(values: &[u128], percentile: usize) -> u128 {
debug_assert!(!values.is_empty());
if values.len() == 1 {
return values[0];
}
let idx = (values.len() - 1) * percentile / 100;
values[idx]
}
fn print_result(result: &BenchResult, output: OutputOptions) {
match output.format {
OutputFormat::Text => print_text(result),
OutputFormat::Csv => print_csv(result, output.header),
}
}
fn print_text(result: &BenchResult) {
println!("implementation={}", result.implementation);
println!("operation={}", result.operation);
println!("sample_rate={}", result.config.sample_rate);
println!("channels={}", result.config.channels);
println!("frame_size={}", result.config.frame_size);
println!("application={}", result.config.application.as_str());
println!("bitrate={}", result.config.bitrate);
println!("complexity={}", result.config.complexity);
println!("bitrate_mode={}", result.config.bitrate_mode.as_str());
println!("frames={}", result.frames);
println!("warmup_iters={}", result.warmup);
println!("measure_iters={}", result.measure);
println!("median_ns_per_frame={:.3}", result.median_ns_per_frame);
println!("p95_ns_per_frame={:.3}", result.p95_ns_per_frame);
println!(
"median_packets_per_sec={:.3}",
result.median_packets_per_sec
);
println!("median_realtime_x={:.3}", result.median_realtime_x);
}
fn print_csv(result: &BenchResult, header: bool) {
if header {
println!(
"implementation,operation,sample_rate,channels,frame_size,application,bitrate,complexity,bitrate_mode,frames,warmup_iters,measure_iters,median_ns_per_frame,p95_ns_per_frame,median_packets_per_sec,median_realtime_x"
);
}
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{:.3},{:.3},{:.3},{:.3}",
result.implementation,
result.operation,
result.config.sample_rate,
result.config.channels,
result.config.frame_size,
result.config.application.as_str(),
result.config.bitrate,
result.config.complexity,
result.config.bitrate_mode.as_str(),
result.frames,
result.warmup,
result.measure,
result.median_ns_per_frame,
result.p95_ns_per_frame,
result.median_packets_per_sec,
result.median_realtime_x,
);
}
enum BenchError {
Usage(String),
Argument(String),
Input(String),
Corpus(String),
Io(std::io::Error),
EncodeInit(mousiki::c_style_api::opus_encoder::OpusEncoderInitError),
EncodeCtl(mousiki::c_style_api::opus_encoder::OpusEncoderCtlError),
Encode(mousiki::c_style_api::opus_encoder::OpusEncodeError),
DecodeInit(mousiki::c_style_api::opus_decoder::OpusDecoderInitError),
Decode(mousiki::c_style_api::opus_decoder::OpusDecodeError),
}
impl From<std::io::Error> for BenchError {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
impl fmt::Display for BenchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Usage(message) => f.write_str(message),
Self::Argument(message) | Self::Input(message) | Self::Corpus(message) => {
f.write_str(message)
}
Self::Io(err) => write!(f, "io error: {err}"),
Self::EncodeInit(err) => write!(f, "encoder init failed: {err:?}"),
Self::EncodeCtl(err) => write!(f, "encoder ctl failed: {err:?}"),
Self::Encode(err) => write!(f, "encode failed: {err:?}"),
Self::DecodeInit(err) => write!(f, "decoder init failed: {err:?}"),
Self::Decode(err) => write!(f, "decode failed: {err:?}"),
}
}
}