use alloc::string::String;
use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AfpError {
#[error("audio too short: needed at least {needed} samples, got {got}")]
AudioTooShort {
needed: usize,
got: usize,
},
#[error("unsupported sample rate: {0} Hz")]
UnsupportedSampleRate(u32),
#[error("unsupported channel count: {0}")]
UnsupportedChannels(u16),
#[error("model not found at {0}")]
ModelNotFound(String),
#[error("model load failed: {0}")]
ModelLoad(String),
#[error("inference failed: {0}")]
Inference(String),
#[error(
"input too large: {provided} exceeds maximum {limit}; \
raise the limit or set it to None to disable"
)]
InputTooLarge {
limit: usize,
provided: usize,
},
#[error("buffer overrun: dropped {dropped} samples")]
BufferOverrun {
dropped: usize,
},
#[error("audio contains non-finite sample (NaN or Inf) at index {index}")]
NonFiniteSample {
index: usize,
},
#[error("deserialize: {0}")]
Deserialize(String),
#[cfg(feature = "std")]
#[error("decode timeout: elapsed {elapsed_ms} ms exceeds limit of {limit_ms} ms")]
Timeout {
elapsed_ms: u64,
limit_ms: u64,
},
#[error("invalid configuration: {0}")]
Config(String),
#[cfg(feature = "std")]
#[error("{0}")]
Io(IoError),
#[cfg(not(feature = "std"))]
#[error("io: {0}")]
Io(String),
}
#[cfg(feature = "std")]
#[derive(Debug)]
pub struct IoError {
pub path: Option<std::path::PathBuf>,
pub kind: std::io::ErrorKind,
pub source: std::io::Error,
}
#[cfg(feature = "std")]
impl IoError {
pub fn new(path: impl Into<std::path::PathBuf>, source: std::io::Error) -> Self {
let kind = source.kind();
Self {
path: Some(path.into()),
kind,
source,
}
}
pub fn without_path(source: std::io::Error) -> Self {
let kind = source.kind();
Self {
path: None,
kind,
source,
}
}
}
#[cfg(feature = "std")]
impl core::fmt::Display for IoError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match &self.path {
Some(p) => write!(f, "io error at {}: {}", p.display(), self.source),
None => write!(f, "io error: {}", self.source),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for IoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
#[cfg(feature = "std")]
impl From<std::io::Error> for AfpError {
fn from(e: std::io::Error) -> Self {
AfpError::Io(IoError::without_path(e))
}
}
#[cfg(feature = "std")]
impl AfpError {
pub fn io_with_path(path: impl Into<std::path::PathBuf>, source: std::io::Error) -> Self {
AfpError::Io(IoError::new(path, source))
}
}
#[cfg(any(feature = "neural", feature = "watermark"))]
pub(crate) fn map_model_open_io(path: &str, e: std::io::Error) -> AfpError {
use alloc::string::ToString;
if e.kind() == std::io::ErrorKind::NotFound {
AfpError::ModelNotFound(path.to_string())
} else {
AfpError::ModelLoad(alloc::format!("open: {e}"))
}
}
#[cfg(any(feature = "neural", feature = "watermark"))]
pub(crate) fn map_model_load_err(e: impl core::fmt::Display) -> AfpError {
AfpError::ModelLoad(alloc::format!("load: {e}"))
}
pub type Result<T> = core::result::Result<T, AfpError>;
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn audio_too_short_displays_both_numbers() {
let e = AfpError::AudioTooShort {
needed: 16_000,
got: 8_000,
};
let s = e.to_string();
assert!(s.contains("16000"), "got: {s}");
assert!(s.contains("8000"), "got: {s}");
}
#[test]
fn unsupported_sample_rate_displays_both_rates() {
let s = AfpError::UnsupportedSampleRate(7_000).to_string();
assert!(s.contains("7000"), "must contain the offending rate: {s}");
assert!(
!s.contains("(supported"),
"must not advertise a hardcoded supported list: {s}",
);
}
#[test]
fn non_finite_sample_displays_index() {
let s = AfpError::NonFiniteSample { index: 42 }.to_string();
assert!(s.contains("42"));
assert!(s.contains("non-finite"));
}
#[test]
fn buffer_overrun_reports_drop_count() {
let s = AfpError::BufferOverrun { dropped: 1024 }.to_string();
assert!(s.contains("1024"));
}
#[test]
fn input_too_large_displays_both_limit_and_provided() {
let err = AfpError::InputTooLarge {
limit: 1_000_000,
provided: 5_000_000,
};
let s = err.to_string();
assert!(s.contains("1000000"), "got: {s}");
assert!(s.contains("5000000"), "got: {s}");
assert!(s.contains("exceeds maximum"), "got: {s}");
}
#[test]
fn result_ok_path() {
let f = |x: u32| -> Result<u32> { Ok(x * 2) };
assert_eq!(f(21).unwrap(), 42);
}
#[test]
fn unsupported_channels_displays_the_count() {
let err = AfpError::UnsupportedChannels(7);
assert_eq!(err.to_string(), "unsupported channel count: 7");
}
#[test]
#[cfg(feature = "std")]
fn io_displays_path_and_source() {
let source = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let err = AfpError::io_with_path("/some/path.wav", source);
let s = err.to_string();
assert!(s.contains("/some/path.wav"), "got: {s}");
assert!(s.contains("file missing"), "got: {s}");
}
#[test]
#[cfg(feature = "std")]
fn io_without_path_displays_source() {
let source = std::io::Error::other("disk full");
let err = AfpError::from(source);
let s = err.to_string();
assert!(s.contains("disk full"), "got: {s}");
}
#[test]
#[cfg(not(feature = "std"))]
fn io_displays_the_inner_message() {
let err = AfpError::Io("disk full".to_string());
assert_eq!(err.to_string(), "io: disk full");
}
}