use thiserror::Error;
pub type AudioSampleResult<T> = Result<T, AudioSampleError>;
#[derive(Error, Debug, Clone)]
#[non_exhaustive]
pub enum AudioSampleError {
#[error(transparent)]
Conversion(#[from] ConversionError),
#[error(transparent)]
Parameter(#[from] ParameterError),
#[error(transparent)]
Layout(#[from] LayoutError),
#[error(transparent)]
Processing(#[from] ProcessingError),
#[error(transparent)]
Feature(#[from] FeatureError),
#[error("Failed to parse {type_name}. Context: {context}")]
Parse {
type_name: String,
context: String,
},
#[cfg(feature = "transforms")]
#[error(transparent)]
Spectrogram(#[from] spectrograms::SpectrogramError),
#[error("Empty audio data provided where non-empty data is required")]
EmptyData,
#[error("Invalid number of samples with respect to the number of channels")]
InvalidNumberOfSamples {
total_samples: usize,
channels: u32,
},
#[error("Fmt error occurred: {0}")]
Fmt(#[from] std::fmt::Error),
#[error("Unsupported operation: {0}")]
Unsupported(String),
}
impl AudioSampleError {
#[inline]
pub fn unsupported<S>(msg: S) -> Self
where
S: ToString,
{
Self::Unsupported(msg.to_string())
}
#[inline]
#[must_use]
pub const fn invalid_number_of_samples(total_samples: usize, channels: u32) -> Self {
Self::InvalidNumberOfSamples {
total_samples,
channels,
}
}
#[inline]
pub fn parse<T, S>(msg: S) -> Self
where
S: ToString,
{
Self::Parse {
type_name: std::any::type_name::<T>().to_string(),
context: msg.to_string(),
}
}
#[inline]
pub fn layout<S>(msg: S) -> Self
where
S: ToString,
{
Self::Layout(LayoutError::ShapeError {
operation: "unknown".to_string(),
info: msg.to_string(),
})
}
}
#[derive(Error, Debug, Clone)]
#[non_exhaustive]
pub enum ConversionError {
#[error("Failed to convert sample value {value} from {source_type} to {target_type}: {reason}")]
AudioConversion {
value: String,
source_type: String,
target_type: String,
reason: String,
},
#[error("Failed to cast value {value} from {source_type} to {target_type}: {reason}")]
NumericCast {
value: String,
source_type: String,
target_type: String,
reason: String,
},
#[error("Conversion from {source_type} to {target_type} is not supported")]
UnsupportedConversion {
source_type: String,
target_type: String,
},
}
#[derive(Error, Debug, Clone)]
#[non_exhaustive]
pub enum ParameterError {
#[error(
"Parameter '{parameter}' value {value} is outside valid range [{min}, {max}]: {reason}"
)]
OutOfRange {
parameter: String,
value: String,
min: String,
max: String,
reason: String,
},
#[error("Invalid value for parameter '{parameter}': {reason}")]
InvalidValue {
parameter: String,
reason: String,
},
#[error("Required parameter '{parameter}' is missing or empty")]
Missing {
parameter: String,
},
#[error("Invalid configuration for {operation}: {reason}")]
InvalidConfiguration {
operation: String,
reason: String,
},
}
#[derive(Error, Debug, Clone)]
#[non_exhaustive]
pub enum LayoutError {
#[error(
"Array layout error: {operation} requires contiguous memory layout, but array is {layout_type}"
)]
NonContiguous {
operation: String,
layout_type: String,
},
#[error(
"Dimension mismatch: expected {expected_dims}, got {actual_dims} for operation '{operation}'"
)]
DimensionMismatch {
expected_dims: String,
actual_dims: String,
operation: String,
},
#[error("Cannot modify borrowed audio data in {operation}: {reason}")]
BorrowedDataMutation {
operation: String,
reason: String,
},
#[error("Incompatible data format for {operation}: {reason}")]
IncompatibleFormat {
operation: String,
reason: String,
},
#[error("Shape error in {operation}: {info}")]
ShapeError {
operation: String,
info: String,
},
#[error("Invalid operation on {0}:\nReason: {1}")]
InvalidOperation(String, String),
}
impl From<ndarray::ShapeError> for AudioSampleError {
#[inline]
fn from(err: ndarray::ShapeError) -> Self {
Self::Layout(LayoutError::ShapeError {
operation: "ndarray operation".to_string(),
info: err.to_string(),
})
}
}
impl LayoutError {
#[inline]
pub fn invalid_operation<S>(operation: S, reason: S) -> Self
where
S: ToString,
{
Self::InvalidOperation(operation.to_string(), reason.to_string())
}
}
#[derive(Error, Debug, Clone)]
#[non_exhaustive]
pub enum ProcessingError {
#[error("Mathematical operation '{operation}' failed: {reason}")]
MathematicalFailure {
operation: String,
reason: String,
},
#[error("Audio processing algorithm '{algorithm}' failed: {reason}")]
AlgorithmFailure {
algorithm: String,
reason: String,
},
#[error("Insufficient data for {operation}: {reason}")]
InsufficientData {
operation: String,
reason: String,
},
#[error("Algorithm '{algorithm}' failed to converge after {iterations} iterations")]
ConvergenceFailure {
algorithm: String,
iterations: u32,
},
#[error("External dependency '{dependency}' required for {operation} is unavailable: {reason}")]
ExternalDependency {
dependency: String,
operation: String,
reason: String,
},
}
#[derive(Error, Debug, Clone)]
#[non_exhaustive]
pub enum FeatureError {
#[error(
"Feature '{feature}' is required for {operation} but not enabled. Enable with: --features {feature}"
)]
NotEnabled {
feature: String,
operation: String,
},
#[error(
"Operation '{operation}' requires features [{required_features}], but only [{enabled_features}] are enabled"
)]
MultipleRequired {
operation: String,
required_features: String,
enabled_features: String,
},
#[error("Feature '{feature}' is enabled but misconfigured: {reason}")]
Misconfigured {
feature: String,
reason: String,
},
}
impl ConversionError {
#[inline]
pub fn audio_conversion<V, S, T, R>(value: V, source_type: S, target_type: T, reason: R) -> Self
where
V: ToString,
S: ToString,
T: ToString,
R: ToString,
{
Self::AudioConversion {
value: value.to_string(),
source_type: source_type.to_string(),
target_type: target_type.to_string(),
reason: reason.to_string(),
}
}
#[inline]
pub fn numeric_cast<V, S, T, R>(value: V, source_type: S, target_type: T, reason: R) -> Self
where
V: ToString,
S: ToString,
T: ToString,
R: ToString,
{
Self::NumericCast {
value: value.to_string(),
source_type: source_type.to_string(),
target_type: target_type.to_string(),
reason: reason.to_string(),
}
}
}
impl ParameterError {
#[inline]
pub fn out_of_range<P, V, Min, Max, R>(
parameter: P,
value: V,
min: Min,
max: Max,
reason: R,
) -> Self
where
P: ToString,
V: ToString,
Min: ToString,
Max: ToString,
R: ToString,
{
Self::OutOfRange {
parameter: parameter.to_string(),
value: value.to_string(),
min: min.to_string(),
max: max.to_string(),
reason: reason.to_string(),
}
}
#[inline]
pub fn invalid_value<P, R>(parameter: P, reason: R) -> Self
where
P: ToString,
R: ToString,
{
Self::InvalidValue {
parameter: parameter.to_string(),
reason: reason.to_string(),
}
}
}
impl LayoutError {
#[inline]
pub fn borrowed_mutation<O, R>(operation: O, reason: R) -> Self
where
O: ToString,
R: ToString,
{
Self::BorrowedDataMutation {
operation: operation.to_string(),
reason: reason.to_string(),
}
}
#[inline]
pub fn dimension_mismatch<E, A, O>(expected: E, actual: A, operation: O) -> Self
where
E: ToString,
A: ToString,
O: ToString,
{
Self::DimensionMismatch {
expected_dims: expected.to_string(),
actual_dims: actual.to_string(),
operation: operation.to_string(),
}
}
}
impl ProcessingError {
#[inline]
pub fn algorithm_failure<A, R>(algorithm: A, reason: R) -> Self
where
A: ToString,
R: ToString,
{
Self::AlgorithmFailure {
algorithm: algorithm.to_string(),
reason: reason.to_string(),
}
}
}
impl FeatureError {
#[inline]
pub fn not_enabled<F, O>(feature: F, operation: O) -> Self
where
F: ToString,
O: ToString,
{
Self::NotEnabled {
feature: feature.to_string(),
operation: operation.to_string(),
}
}
}
impl From<&str> for ParameterError {
#[inline]
fn from(msg: &str) -> Self {
Self::InvalidValue {
parameter: "unknown".to_string(),
reason: msg.to_string(),
}
}
}
impl From<String> for ParameterError {
#[inline]
fn from(msg: String) -> Self {
Self::InvalidValue {
parameter: "unknown".to_string(),
reason: msg,
}
}
}
impl From<&str> for ProcessingError {
#[inline]
fn from(msg: &str) -> Self {
Self::AlgorithmFailure {
algorithm: "unknown".to_string(),
reason: msg.to_string(),
}
}
}
impl From<String> for ProcessingError {
#[inline]
fn from(msg: String) -> Self {
Self::AlgorithmFailure {
algorithm: "unknown".to_string(),
reason: msg,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_hierarchy() {
let conversion_err =
ConversionError::audio_conversion("32768", "i16", "i8", "Out of range");
let audio_err = AudioSampleError::Conversion(conversion_err);
assert!(matches!(audio_err, AudioSampleError::Conversion(_)));
assert!(format!("{}", audio_err).contains("Failed to convert sample value 32768"));
}
#[test]
fn test_parameter_error() {
let param_err = ParameterError::out_of_range(
"cutoff_hz",
"25000",
"20",
"22050",
"Exceeds Nyquist limit",
);
assert!(format!("{}", param_err).contains("cutoff_hz"));
assert!(format!("{}", param_err).contains("25000"));
assert!(format!("{}", param_err).contains("22050"));
}
#[test]
fn test_layout_error() {
let layout_err = LayoutError::borrowed_mutation("slice_mut", "Data is borrowed immutably");
assert!(format!("{}", layout_err).contains("slice_mut"));
assert!(format!("{}", layout_err).contains("borrowed"));
}
#[test]
fn test_processing_error() {
let proc_err =
ProcessingError::algorithm_failure("transforms", "Input size must be power of 2");
assert!(format!("{}", proc_err).contains("transforms"));
assert!(format!("{}", proc_err).contains("power of 2"));
}
#[test]
fn test_feature_error() {
let feat_err = FeatureError::not_enabled("transforms", "spectral analysis");
assert!(format!("{}", feat_err).contains("transforms"));
assert!(format!("{}", feat_err).contains("spectral analysis"));
}
#[test]
fn test_result_type_alias() {
let ok_result: AudioSampleResult<i32> = Ok(42);
assert_eq!(ok_result.unwrap(), 42);
let err_result: AudioSampleResult<i32> = Err(AudioSampleError::Parameter(
ParameterError::invalid_value("test_param", "Invalid for testing"),
));
assert!(err_result.is_err());
}
#[test]
fn test_error_chain_display() {
let conversion_err = ConversionError::audio_conversion(
"1.5",
"f32",
"i16",
"Value out of signed 16-bit range",
);
let audio_err = AudioSampleError::Conversion(conversion_err);
let error_string = format!("{}", audio_err);
assert!(error_string.contains("Failed to convert sample value 1.5"));
assert!(error_string.contains("f32"));
assert!(error_string.contains("i16"));
}
}