use hang::catalog::{AudioCodec, AudioConfig};
use super::Decoded;
use super::decoder::{Config, Kind};
use crate::{Error, Layout};
mod libopus;
mod pcm;
#[cfg(feature = "aac")]
mod symphonia;
pub(crate) trait Backend: Send + std::panic::UnwindSafe + std::panic::RefUnwindSafe {
fn decode(&mut self, packet: &[u8]) -> Result<Decoded, Error>;
fn reset(&mut self) -> Result<(), Error>;
fn sample_rate(&self) -> u32;
fn layout(&self) -> Layout;
fn delay(&self) -> usize {
0
}
fn name(&self) -> &str;
}
type Open = fn(&AudioConfig) -> Result<Box<dyn Backend>, Error>;
struct Candidate {
name: &'static str,
supports: fn(&AudioCodec) -> bool,
open: Open,
}
const PLATFORM: &[Candidate] = &[];
const SOFTWARE: &[Candidate] = &[
Candidate {
name: libopus::NAME,
supports: |codec| matches!(codec, AudioCodec::Opus),
open: libopus::Libopus::open,
},
Candidate {
name: pcm::NAME,
supports: |codec| matches!(codec, AudioCodec::Pcm),
open: pcm::Pcm::open,
},
#[cfg(feature = "aac")]
Candidate {
name: symphonia::NAME,
supports: |codec| matches!(codec, AudioCodec::AAC(_)),
open: symphonia::Symphonia::open,
},
];
fn codec_name(codec: &AudioCodec) -> Option<&'static str> {
match codec {
AudioCodec::Opus => Some("opus"),
AudioCodec::Pcm => Some("pcm"),
AudioCodec::AAC(_) => Some("aac"),
_ => None,
}
}
pub(crate) fn open(catalog: &AudioConfig, config: &Config) -> Result<Box<dyn Backend>, Error> {
if let Kind::Named(requested) = &config.kind
&& codec_name(&catalog.codec) != Some(requested)
{
return Err(Error::Unsupported(format!(
"audio decoder {requested:?} is unavailable for {}",
catalog.codec
)));
}
select(catalog, candidates(&config.kind, PLATFORM, SOFTWARE))
}
fn candidates<'a>(kind: &Kind, platform: &'a [Candidate], software: &'a [Candidate]) -> Vec<&'a Candidate> {
match kind {
Kind::Software => software.iter().collect(),
Kind::Auto | Kind::Named(_) => platform.iter().chain(software).collect(),
}
}
fn select(catalog: &AudioConfig, candidates: Vec<&Candidate>) -> Result<Box<dyn Backend>, Error> {
let codec = &catalog.codec;
let mut refused = Vec::new();
for candidate in candidates {
if !(candidate.supports)(codec) {
continue;
}
match (candidate.open)(catalog) {
Ok(backend) => return Ok(backend),
Err(err) => refused.push((candidate.name, err)),
}
}
if refused.len() == 1 {
let (_, err) = refused.remove(0);
return Err(err);
}
if !refused.is_empty() {
let reasons: Vec<String> = refused.iter().map(|(name, err)| format!("{name}: {err}")).collect();
return Err(Error::Unsupported(reasons.join(", ")));
}
Err(Error::Unsupported(format!("unsupported audio codec: {codec}")))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Activity;
struct Stub(&'static str);
impl Backend for Stub {
fn decode(&mut self, _packet: &[u8]) -> Result<Decoded, Error> {
Ok(Decoded {
samples: Vec::new(),
activity: Activity::Active,
})
}
fn reset(&mut self) -> Result<(), Error> {
Ok(())
}
fn sample_rate(&self) -> u32 {
48_000
}
fn layout(&self) -> Layout {
Layout::Stereo
}
fn name(&self) -> &str {
self.0
}
}
const PLATFORM_STUB: Candidate = Candidate {
name: "platform",
supports: |codec| matches!(codec, AudioCodec::Opus),
open: |_| Ok(Box::new(Stub("platform"))),
};
const REFUSING: Candidate = Candidate {
name: "refusing",
supports: |codec| matches!(codec, AudioCodec::Opus),
open: |_| Err(Error::Unsupported("not this track".into())),
};
const SOFTWARE_STUB: Candidate = Candidate {
name: "software",
supports: |codec| matches!(codec, AudioCodec::Opus),
open: |_| Ok(Box::new(Stub("software"))),
};
const PCM_ONLY: Candidate = Candidate {
name: "pcm-only",
supports: |codec| matches!(codec, AudioCodec::Pcm),
open: |_| panic!("opened for a codec it does not advertise"),
};
fn opus() -> AudioConfig {
AudioConfig::new(AudioCodec::Opus, 48_000, 2)
}
fn pick(kind: Kind, platform: &[Candidate], software: &[Candidate]) -> Result<String, Error> {
let backend = select(&opus(), candidates(&kind, platform, software))?;
Ok(backend.name().to_owned())
}
#[test]
fn auto_prefers_platform() {
let name = pick(Kind::Auto, &[PCM_ONLY, PLATFORM_STUB], &[SOFTWARE_STUB]).unwrap();
assert_eq!(name, "platform");
}
#[test]
fn auto_falls_back_to_software() {
let name = pick(Kind::Auto, &[REFUSING], &[SOFTWARE_STUB]).unwrap();
assert_eq!(name, "software");
}
#[test]
fn software_skips_platform() {
let name = pick(Kind::Software, &[PLATFORM_STUB], &[SOFTWARE_STUB]).unwrap();
assert_eq!(name, "software");
}
#[test]
fn named_codec_picks_like_auto() {
let name = pick(Kind::Named("opus".into()), &[PLATFORM_STUB], &[SOFTWARE_STUB]).unwrap();
assert_eq!(name, "platform");
}
#[test]
fn every_refusal_is_reported() {
const ALSO_REFUSING: Candidate = Candidate {
name: "also-refusing",
..REFUSING
};
let err = pick(Kind::Auto, &[REFUSING], &[ALSO_REFUSING]).unwrap_err();
let message = err.to_string();
assert!(
message.contains("refusing: ") && message.contains("also-refusing: "),
"{message}"
);
}
fn named(name: &str) -> Config {
Config {
kind: Kind::Named(name.into()),
}
}
#[test]
fn codec_names_open_their_codec() {
let pcm = AudioConfig::new(AudioCodec::Pcm, 48_000, 2);
assert_eq!(open(&pcm, &named("pcm")).unwrap().name(), pcm::NAME);
assert_eq!(open(&opus(), &named("opus")).unwrap().name(), libopus::NAME);
}
#[test]
fn another_codecs_name_is_refused() {
assert!(matches!(open(&opus(), &named("pcm")), Err(Error::Unsupported(_))));
}
#[test]
fn backend_names_are_refused() {
let message = open(&opus(), &named(libopus::NAME))
.err()
.expect("libopus is a backend, not a codec")
.to_string();
assert!(message.contains("\"libopus\"") && message.contains("opus"), "{message}");
}
}