blob: e9e7fb83ab2a404bf9d4615a08d04bdaeeb1ad21 [file] [log] [blame]
// Copyright 2024 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::serde_ext;
use camino::Utf8PathBuf;
use fidl_fuchsia_audio_controller as fac;
use fuchsia_audio::{
device::{
ClockDomain, GainCapabilities, GainState, PlugDetectCapabilities, PlugState, Selector,
UniqueInstanceId,
},
format_set::{ChannelAttributes, ChannelSet, PcmFormatSet},
};
use itertools::Itertools;
use lazy_static::lazy_static;
use prettytable::{cell, format, row, Table};
use serde::Serialize;
use std::fmt::Display;
lazy_static! {
// No padding, no borders.
pub static ref TABLE_FORMAT_EMPTY: format::TableFormat = format::FormatBuilder::new().build();
// Left padding, used for the outer table.
pub static ref TABLE_FORMAT_NORMAL: format::TableFormat = format::FormatBuilder::new().padding(2, 0).build();
// Right padding, no borders, used for channel sets/attributes.
pub static ref TABLE_FORMAT_CHANNEL_SETS: format::TableFormat = format::FormatBuilder::new().padding(0, 2).build();
// With borders, used nested tables.
pub static ref TABLE_FORMAT_NESTED: format::TableFormat = format::FormatBuilder::new()
.borders('│')
.separators(&[format::LinePosition::Top], format::LineSeparator::new('─', '┬', '┌', '┐'))
.separators(&[format::LinePosition::Bottom], format::LineSeparator::new('─', '┴', '└', '┘'))
.padding(1, 1)
.build();
}
// The Zircon Rust library cannot be built on host, so we can't use `zx::Time`.
type ZxTime = i64;
/// Formatter for [GainState].
struct GainStateText<'a>(&'a GainState);
impl Display for GainStateText<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let muted = match self.0.muted {
Some(muted) => {
if muted {
"muted"
} else {
"unmuted"
}
}
None => "Muted not available",
};
let agc_enabled = match self.0.agc_enabled {
Some(agc) => {
if agc {
"AGC on"
} else {
"AGC off"
}
}
None => "AGC not available",
};
write!(f, "{} dB ({}, {})", self.0.gain_db, muted, agc_enabled)
}
}
/// Formatter for [GainCapabilities].
struct GainCapabilitiesText<'a>(&'a GainCapabilities);
impl Display for GainCapabilitiesText<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let gain_range = if self.0.min_gain_db == self.0.max_gain_db {
format!("Fixed {} dB gain", self.0.min_gain_db)
} else {
format!("Gain range [{} dB, {} dB]", self.0.min_gain_db, self.0.max_gain_db)
};
let gain_step = if self.0.gain_step_db == 0.0f32 {
"0 dB step (continuous)".to_string()
} else {
format!("{} dB step", self.0.gain_step_db)
};
let can_mute = match self.0.can_mute {
Some(can_mute) => {
if can_mute {
"can mute"
} else {
"cannot mute"
}
}
None => "Can Mute unavailable",
};
let can_agc = match self.0.can_agc {
Some(can_agc) => {
if can_agc {
"can AGC"
} else {
"cannot AGC"
}
}
None => "Can AGC unavailable",
};
write!(f, "{}; {}; {}; {}", gain_range, gain_step, can_mute, can_agc)
}
}
/// Formatter for a vector of [ChannelSet]s.
struct ChannelSetsText<'a>(&'a Vec<ChannelSet>);
impl Display for ChannelSetsText<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut table = Table::new();
table.set_format(*TABLE_FORMAT_CHANNEL_SETS);
for channel_set in self.0 {
let key = format!(
"{} {}:",
channel_set.attributes.len(),
if channel_set.attributes.len() == 1 { "channel" } else { "channels" }
);
let value = ChannelSetText(&channel_set);
table.add_row(row!(key, value));
}
table.fmt(f)
}
}
/// Formatter for [ChannelSet].
struct ChannelSetText<'a>(&'a ChannelSet);
impl Display for ChannelSetText<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut table = Table::new();
table.set_format(*TABLE_FORMAT_CHANNEL_SETS);
for (idx, attributes) in self.0.attributes.iter().enumerate() {
let key = format!("Channel {}:", idx + 1);
let value = ChannelAttributesText(&attributes);
table.add_row(row!(key, value));
}
table.fmt(f)
}
}
/// Formatter for [ChannelAttributes].
struct ChannelAttributesText<'a>(&'a ChannelAttributes);
impl Display for ChannelAttributesText<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut table = Table::new();
table.set_format(*TABLE_FORMAT_CHANNEL_SETS);
table.add_row(row!("Min frequency (Hz):", or_unknown(&self.0.min_frequency)));
table.add_row(row!("Max frequency (Hz):", or_unknown(&self.0.max_frequency)));
table.fmt(f)
}
}
/// Formatter for [PcmFormatSet].
struct PcmFormatSetText<'a>(&'a PcmFormatSet);
impl Display for PcmFormatSetText<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let sample_types = self.0.sample_types.iter().map(ToString::to_string).join(", ");
let frame_rates = self.0.frame_rates.iter().map(ToString::to_string).join(", ");
let num_channels = self.0.channel_sets.iter().map(|set| set.channels()).join(", ");
let mut table = Table::new();
table.set_format(*TABLE_FORMAT_NESTED);
table.add_row(row!(r->"Sample types:", sample_types));
table.add_row(row!(r->"Frame rates (Hz):", frame_rates));
table.add_row(row!(r->"Number of channels:", num_channels));
table.add_row(row!(r->"Channel attributes:", ChannelSetsText(&self.0.channel_sets)));
table.fmt(f)
}
}
/// Formatter for a map of ring buffer element IDs to their supported formats.
struct SupportedRingBufferFormatsText<'a>(&'a Vec<PcmFormatSet>);
impl Display for SupportedRingBufferFormatsText<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut table = Table::new();
table.set_format(*TABLE_FORMAT_EMPTY);
if self.0.is_empty() {
table.add_row(row!["No format sets available."]);
} else {
table.set_titles(row![format!(
"{} {} with format sets:",
self.0.len(),
if self.0.len() == 1 { "element" } else { "elements" }
)]);
for format_set in self.0 {
table.add_row(row![PcmFormatSetText(format_set)]);
}
}
table.fmt(f)
}
}
#[derive(Default, Debug, Serialize, PartialEq, Clone)]
pub struct InfoResult {
pub device_path: Option<Utf8PathBuf>,
#[serde(serialize_with = "serde_ext::serialize_option_tostring")]
pub unique_id: Option<UniqueInstanceId>,
pub manufacturer: Option<String>,
pub product_name: Option<String>,
#[serde(serialize_with = "serde_ext::serialize_option_gainstate")]
pub gain_state: Option<GainState>,
#[serde(serialize_with = "serde_ext::serialize_option_gaincapabilities")]
pub gain_capabilities: Option<GainCapabilities>,
#[serde(serialize_with = "serde_ext::serialize_option_tostring")]
pub plug_state: Option<PlugState>,
pub plug_time: Option<ZxTime>,
#[serde(serialize_with = "serde_ext::serialize_option_tostring")]
pub plug_detect_capabilities: Option<PlugDetectCapabilities>,
#[serde(serialize_with = "serde_ext::serialize_option_clockdomain")]
pub clock_domain: Option<ClockDomain>,
#[serde(serialize_with = "serde_ext::serialize_option_vec_pcmformatset")]
pub supported_ring_buffer_formats: Option<Vec<PcmFormatSet>>,
}
impl From<InfoResult> for Table {
fn from(value: InfoResult) -> Self {
let mut table = Table::new();
table.set_format(*TABLE_FORMAT_NORMAL);
table.add_row(row!(r->"Path:", or_unknown(&value.device_path)));
table.add_row(row!(r->"Unique ID:", or_unknown(&value.unique_id)));
table.add_row(row!(r->"Manufacturer:", or_unknown(&value.manufacturer)));
table.add_row(row!(r->"Product:", or_unknown(&value.product_name)));
table.add_row(
row!(r->"Current gain:", or_unknown(&value.gain_state.as_ref().map(GainStateText))),
);
table.add_row(
row!(r->"Gain capabilities:", or_unknown(&value.gain_capabilities.as_ref().map(GainCapabilitiesText))),
);
table.add_row(row!(r->"Plug state:", or_unknown(&value.plug_state)));
table.add_row(row!(r->"Plug time:", or_unknown(&value.plug_time)));
table.add_row(row!(r->"Plug detection:", or_unknown(&value.plug_detect_capabilities)));
table.add_row(row!(r->"Clock domain:", or_unknown(&value.clock_domain)));
table.add_row(row!(r->"Ring buffer formats:", or_unknown(&value.supported_ring_buffer_formats.as_ref().map(SupportedRingBufferFormatsText))));
table
}
}
/// Returns the [Display] representation of the option value, if it exists, or a placeholder.
fn or_unknown(value: &Option<impl ToString>) -> String {
value.as_ref().map_or("<unknown>".to_string(), |value| value.to_string())
}
impl From<(fac::DeviceInfo, Selector)> for InfoResult {
fn from(t: (fac::DeviceInfo, Selector)) -> Self {
let (device_info, selector) = t;
let device_path = match selector {
Selector::Devfs(devfs) => Some(devfs.path()),
Selector::Registry(_) => None,
};
match device_info {
fac::DeviceInfo::StreamConfig(stream_info) => {
let stream_properties = stream_info.stream_properties;
let gain_state = stream_info.gain_state;
let plug_state = stream_info.plug_state;
let pcm_supported_formats = stream_info.supported_formats;
match stream_properties {
Some(stream_properties) => Self {
device_path,
unique_id: stream_properties.unique_id.map(UniqueInstanceId::from),
manufacturer: stream_properties.manufacturer.clone(),
product_name: stream_properties.product.clone(),
gain_state: gain_state
.and_then(|gain_state| GainState::try_from(gain_state).ok()),
gain_capabilities: GainCapabilities::try_from(&stream_properties).ok(),
plug_state: plug_state
.clone()
.map(PlugState::try_from)
.transpose()
.unwrap_or_default(),
plug_time: plug_state.clone().and_then(|p| p.plug_state_time),
plug_detect_capabilities: stream_properties
.plug_detect_capabilities
.map(Into::into),
clock_domain: None,
supported_ring_buffer_formats: pcm_supported_formats.map(|formats| {
formats
.into_iter()
.map(PcmFormatSet::try_from)
.collect::<Result<Vec<_>, _>>()
.unwrap()
}),
},
None => Self { device_path, ..Default::default() },
}
}
fac::DeviceInfo::Composite(composite_info) => {
let composite_properties = composite_info.composite_properties;
match composite_properties {
Some(composite_properties) => Self {
device_path,
manufacturer: composite_properties.manufacturer,
product_name: composite_properties.product,
clock_domain: composite_properties.clock_domain.map(ClockDomain::from),
..Default::default()
},
None => Self { device_path, ..Default::default() },
}
}
_ => panic!("Unsupported device type"),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use fidl_fuchsia_audio_device as fadevice;
use fidl_fuchsia_hardware_audio as fhaudio;
use fuchsia_audio::format::SampleType;
#[test]
fn test_info_result_table() {
let info_result = InfoResult {
device_path: Some(Utf8PathBuf::from("/dev/class/audio-input/0c8301e0")),
unique_id: Some(UniqueInstanceId([
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
0x0e, 0x0f,
])),
manufacturer: Some("Test manufacturer".to_string()),
product_name: Some("Test product".to_string()),
gain_state: Some(GainState {
gain_db: -3.0_f32,
muted: Some(false),
agc_enabled: Some(false),
}),
gain_capabilities: Some(GainCapabilities {
min_gain_db: -100.0_f32,
max_gain_db: 0.0_f32,
gain_step_db: 0.0_f32,
can_mute: Some(true),
can_agc: Some(false),
}),
plug_state: Some(fadevice::PlugState::Plugged.into()),
plug_time: Some(123456789),
plug_detect_capabilities: Some(fadevice::PlugDetectCapabilities::Hardwired.into()),
clock_domain: Some(ClockDomain(fhaudio::CLOCK_DOMAIN_MONOTONIC)),
supported_ring_buffer_formats: Some(vec![
PcmFormatSet {
channel_sets: vec![
ChannelSet::try_from(vec![ChannelAttributes::default()]).unwrap()
],
sample_types: vec![SampleType::Uint8, SampleType::Int16],
frame_rates: vec![16000, 22050, 32000],
},
PcmFormatSet {
channel_sets: vec![
ChannelSet::try_from(vec![ChannelAttributes::default()]).unwrap(),
ChannelSet::try_from(vec![
ChannelAttributes::default(),
ChannelAttributes::default(),
])
.unwrap(),
],
sample_types: vec![SampleType::Float32],
frame_rates: vec![44100, 48000, 88200, 96000],
},
]),
};
let output = Table::from(info_result).to_string();
let expected = r#"
Path: /dev/class/audio-input/0c8301e0
Unique ID: 000102030405060708090a0b0c0d0e0f
Manufacturer: Test manufacturer
Product: Test product
Current gain: -3 dB (unmuted, AGC off)
Gain capabilities: Gain range [-100 dB, 0 dB]; 0 dB step (continuous); can mute; cannot AGC
Plug state: Plugged
Plug time: 123456789
Plug detection: Hardwired
Clock domain: 0 (monotonic)
Ring buffer formats: 2 elements with format sets:
┌───────────────────────────────────────────────────────────────────────────────────┐
Sample types: uint8, int16
Frame rates (Hz): 16000, 22050, 32000
Number of channels: 1
Channel attributes: 1 channel: Channel 1: Min frequency (Hz): <unknown>
Max frequency (Hz): <unknown>
└───────────────────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────────────────────┐
Sample types: float32
Frame rates (Hz): 44100, 48000, 88200, 96000
Number of channels: 1, 2
Channel attributes: 1 channel: Channel 1: Min frequency (Hz): <unknown>
Max frequency (Hz): <unknown>
2 channels: Channel 1: Min frequency (Hz): <unknown>
Max frequency (Hz): <unknown>
Channel 2: Min frequency (Hz): <unknown>
Max frequency (Hz): <unknown>
└────────────────────────────────────────────────────────────────────────────────────┘
"#
.trim_start_matches('\n');
assert_eq!(expected, output);
}
}