#![allow(unused_imports)]
#[cfg(feature = "processing")]
use crate::traits::StandardSample;
use crate::{AudioSampleError, AudioSampleResult, ParameterError};
use core::fmt::Display;
use std::num::NonZeroUsize;
use std::str::FromStr;
#[cfg(feature = "editing")]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PadSide {
Left,
#[default]
Right,
}
#[cfg(feature = "editing")]
impl FromStr for PadSide {
type Err = crate::AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"left" => Ok(Self::Left),
"right" => Ok(Self::Right),
_ => Err(AudioSampleError::Parameter(ParameterError::InvalidValue {
parameter: s.to_string(),
reason: "Expected 'left' or 'right'".to_string(),
})),
}
}
}
#[cfg(any(feature = "processing", feature = "peak-picking"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum NormalizationMethod {
#[default]
MinMax,
Peak,
Mean,
Median,
ZScore,
}
#[cfg(feature = "processing")]
impl FromStr for NormalizationMethod {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"min_max" | "min-max" | "minmax" => Ok(Self::MinMax),
"peak" => Ok(Self::Peak),
"mean" => Ok(Self::Mean),
"median" => Ok(Self::Median),
"zscore" | "z_score" | "z-score" => Ok(Self::ZScore),
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {}. Got {}, must be one of {:?}",
std::any::type_name::<Self>(),
s,
[
"min_max", "min-max", "minmax", "peak", "mean", "median", "zscore", "z_score",
"z-score",
]
))),
}
}
}
#[cfg(feature = "processing")]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct NormalizationConfig<T>
where
T: StandardSample,
{
pub method: NormalizationMethod,
pub min: Option<T>,
pub max: Option<T>,
pub target: Option<T>,
}
#[cfg(feature = "processing")]
impl<T> NormalizationConfig<T>
where
T: StandardSample,
{
#[inline]
pub const fn peak(target: T) -> Self {
Self {
method: NormalizationMethod::Peak,
min: None,
max: None,
target: Some(target),
}
}
#[inline]
pub const fn min_max(min: T, max: T) -> Self {
Self {
method: NormalizationMethod::MinMax,
min: Some(min),
max: Some(max),
target: None,
}
}
#[inline]
#[must_use]
pub const fn mean() -> Self {
Self {
method: NormalizationMethod::Mean,
min: None,
max: None,
target: None,
}
}
#[inline]
#[must_use]
pub const fn median() -> Self {
Self {
method: NormalizationMethod::Median,
min: None,
max: None,
target: None,
}
}
#[inline]
#[must_use]
pub const fn zscore() -> Self {
Self {
method: NormalizationMethod::ZScore,
min: None,
max: None,
target: None,
}
}
}
#[cfg(feature = "processing")]
impl NormalizationConfig<f64> {
#[inline]
#[must_use]
pub const fn peak_normalized() -> Self {
Self::peak(1.0)
}
#[inline]
#[must_use]
pub const fn range_normalized() -> Self {
Self::min_max(-1.0, 1.0)
}
}
#[cfg(feature = "editing")]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FadeCurve {
#[default]
Linear,
Exponential,
Logarithmic,
SmoothStep,
}
#[cfg(feature = "editing")]
impl FromStr for FadeCurve {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(AudioSampleError::parse::<Self, _>(
"Input must not be empty. Must be one of ['linear', 'exp', 'exponential', 'log', 'logarithmic', 'smooth', 'smoothstep']",
));
}
let normalised = s.trim();
match normalised.to_lowercase().as_str() {
"linear" => Ok(Self::Linear),
"exp" | "exponential" => Ok(Self::Exponential),
"log" | "logarithmic" => Ok(Self::Logarithmic),
"smooth" | "smoothstep" => Ok(Self::SmoothStep),
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {normalised}. Must be one of ['linear', 'exp', 'exponential', 'log', 'logarithmic', 'smooth', 'smoothstep']"
))),
}
}
}
#[cfg(feature = "channels")]
#[derive(Default, Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum MonoConversionMethod {
#[default]
Average,
Left,
Right,
Weighted(Vec<f64>),
Center,
}
#[cfg(feature = "channels")]
#[derive(Default, Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum StereoConversionMethod {
#[default]
Duplicate,
Pan(f64),
Left,
Right,
}
#[cfg(feature = "vad")]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum VadMethod {
#[default]
Energy,
ZeroCrossing,
Combined,
Spectral,
}
#[cfg(feature = "vad")]
impl FromStr for VadMethod {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"energy" | "rms" => Ok(Self::Energy),
"zero_crossing" | "zero-crossing" | "zcr" => Ok(Self::ZeroCrossing),
"combined" | "energy_zcr" | "energy-zcr" => Ok(Self::Combined),
"spectral" | "spectrum" => Ok(Self::Spectral),
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {}. Got {}, must be one of {:?}",
std::any::type_name::<Self>(),
s,
[
"energy",
"rms",
"zero_crossing",
"zero-crossing",
"zcr",
"combined",
"energy_zcr",
"energy-zcr",
"spectral",
"spectrum",
]
))),
}
}
}
#[cfg(feature = "vad")]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum VadChannelPolicy {
#[default]
AverageToMono,
AnyChannel,
AllChannels,
Channel(usize),
}
#[cfg(feature = "vad")]
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct VadConfig {
pub method: VadMethod,
pub frame_size: NonZeroUsize,
pub hop_size: NonZeroUsize,
pub pad_end: bool,
pub channel_policy: VadChannelPolicy,
pub energy_threshold_db: f64,
pub zcr_min: f64,
pub zcr_max: f64,
pub min_speech_frames: usize,
pub min_silence_frames: usize,
pub hangover_frames: NonZeroUsize,
pub smooth_frames: NonZeroUsize,
pub speech_band_low_hz: f64,
pub speech_band_high_hz: f64,
pub spectral_ratio_threshold: f64,
}
#[cfg(feature = "vad")]
impl VadConfig {
#[inline]
#[must_use]
pub const fn new(
method: VadMethod,
frame_size: NonZeroUsize,
hop_size: NonZeroUsize,
pad_end: bool,
channel_policy: VadChannelPolicy,
energy_threshold_db: f64,
zcr_min: f64,
zcr_max: f64,
min_speech_frames: usize,
min_silence_frames: usize,
hangover_frames: NonZeroUsize,
smooth_frames: NonZeroUsize,
speech_band_low_hz: f64,
speech_band_high_hz: f64,
spectral_ratio_threshold: f64,
) -> Self {
Self {
method,
frame_size,
hop_size,
pad_end,
channel_policy,
energy_threshold_db,
zcr_min,
zcr_max,
min_speech_frames,
min_silence_frames,
hangover_frames,
smooth_frames,
speech_band_low_hz,
speech_band_high_hz,
spectral_ratio_threshold,
}
}
#[inline]
#[must_use]
pub fn energy_only() -> Self {
Self {
method: VadMethod::Energy,
..Default::default()
}
}
#[inline]
pub fn validate(self) -> AudioSampleResult<Self> {
if self.hop_size > self.frame_size {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"hop_size",
"must be <= frame_size",
)));
}
if self.zcr_min < 0.0 || self.zcr_max > 1.0 || self.zcr_min > self.zcr_max {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"zcr_*",
"expected 0 <= zcr_min <= zcr_max <= 1",
)));
}
if self.speech_band_low_hz <= 0.0
|| self.speech_band_high_hz <= 0.0
|| self.speech_band_low_hz >= self.speech_band_high_hz
{
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"speech_band_*",
"expected 0 < low_hz < high_hz",
)));
}
if self.spectral_ratio_threshold < 0.0 || self.spectral_ratio_threshold > 1.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"spectral_ratio_threshold",
"expected 0 <= threshold <= 1",
)));
}
Ok(self)
}
#[inline]
#[must_use]
pub const fn with_method(self, method: VadMethod) -> Self {
Self { method, ..self }
}
#[inline]
pub fn with_frame_size(self, frame_size: NonZeroUsize) -> AudioSampleResult<Self> {
let updated = Self { frame_size, ..self };
if updated.hop_size > updated.frame_size {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"frame_size",
"must be >= hop_size",
)));
}
Ok(updated)
}
#[inline]
pub fn with_hop_size(self, hop_size: NonZeroUsize) -> AudioSampleResult<Self> {
let updated = Self { hop_size, ..self };
if updated.hop_size > updated.frame_size {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"hop_size",
"must be <= frame_size",
)));
}
Ok(updated)
}
#[inline]
#[must_use]
pub const fn with_pad_end(self, pad_end: bool) -> Self {
Self { pad_end, ..self }
}
#[inline]
#[must_use]
pub const fn with_channel_policy(self, channel_policy: VadChannelPolicy) -> Self {
Self {
channel_policy,
..self
}
}
#[inline]
#[must_use]
pub const fn with_energy_threshold_db(self, energy_threshold_db: f64) -> Self {
Self {
energy_threshold_db,
..self
}
}
#[inline]
pub fn with_zcr_min(self, zcr_min: f64) -> AudioSampleResult<Self> {
let updated = Self { zcr_min, ..self };
if updated.zcr_min < 0.0 || updated.zcr_min > 1.0 || updated.zcr_min > updated.zcr_max {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"zcr_min",
"expected 0 <= zcr_min <= zcr_max <= 1",
)));
}
Ok(updated)
}
#[inline]
pub fn with_zcr_max(self, zcr_max: f64) -> AudioSampleResult<Self> {
let updated = Self { zcr_max, ..self };
if updated.zcr_max < 0.0 || updated.zcr_max > 1.0 || updated.zcr_min > updated.zcr_max {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"zcr_max",
"expected 0 <= zcr_min <= zcr_max <= 1",
)));
}
Ok(updated)
}
#[inline]
#[must_use]
pub const fn with_min_speech_frames(self, min_speech_frames: usize) -> Self {
Self {
min_speech_frames,
..self
}
}
#[inline]
#[must_use]
pub const fn with_min_silence_frames(self, min_silence_frames: usize) -> Self {
Self {
min_silence_frames,
..self
}
}
#[inline]
#[must_use]
pub const fn with_hangover_frames(self, hangover_frames: NonZeroUsize) -> Self {
Self {
hangover_frames,
..self
}
}
#[inline]
#[must_use]
pub const fn with_smooth_frames(self, smooth_frames: NonZeroUsize) -> Self {
Self {
smooth_frames,
..self
}
}
#[inline]
pub fn with_speech_band_low_hz(self, speech_band_low_hz: f64) -> AudioSampleResult<Self> {
let updated = Self {
speech_band_low_hz,
..self
};
if updated.speech_band_low_hz <= 0.0
|| updated.speech_band_low_hz >= updated.speech_band_high_hz
{
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"speech_band_low_hz",
"expected 0 < low_hz < high_hz",
)));
}
Ok(updated)
}
#[inline]
pub fn with_speech_band_high_hz(self, speech_band_high_hz: f64) -> AudioSampleResult<Self> {
let updated = Self {
speech_band_high_hz,
..self
};
if updated.speech_band_high_hz <= 0.0
|| updated.speech_band_low_hz >= updated.speech_band_high_hz
{
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"speech_band_high_hz",
"expected 0 < low_hz < high_hz",
)));
}
Ok(updated)
}
#[inline]
pub fn with_spectral_ratio_threshold(
self,
spectral_ratio_threshold: f64,
) -> AudioSampleResult<Self> {
let updated = Self {
spectral_ratio_threshold,
..self
};
if updated.spectral_ratio_threshold < 0.0 || updated.spectral_ratio_threshold > 1.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"spectral_ratio_threshold",
"expected 0 <= threshold <= 1",
)));
}
Ok(updated)
}
}
#[cfg(feature = "vad")]
impl Default for VadConfig {
fn default() -> Self {
Self::new(
VadMethod::Energy,
crate::nzu!(1024),
crate::nzu!(512),
true,
VadChannelPolicy::AverageToMono,
-40.0, 0.02, 0.3, 3, 5, crate::nzu!(2), crate::nzu!(5), 300.0, 3400.0, 0.6, )
}
}
#[cfg(feature = "resampling")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[non_exhaustive]
pub enum ResamplingQuality {
#[default]
Fast,
Medium,
High,
}
#[cfg(feature = "resampling")]
impl Display for ResamplingQuality {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
Self::Fast => "fast",
Self::Medium => "medium",
Self::High => "high",
};
f.write_str(s)
}
}
#[cfg(feature = "resampling")]
impl FromStr for ResamplingQuality {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"fast" | "low" => Ok(Self::Fast),
"medium" | "med" | "balanced" => Ok(Self::Medium),
"high" | "best" => Ok(Self::High),
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {}. Got {}, must be one of {:?}",
std::any::type_name::<Self>(),
s,
["fast", "low", "medium", "med", "balanced", "high", "best"]
))),
}
}
}
#[cfg(feature = "resampling")]
impl TryFrom<&str> for ResamplingQuality {
type Error = AudioSampleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(feature = "transforms")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum SpectrogramScale {
#[default]
Linear,
Log,
Mel,
}
#[cfg(feature = "transforms")]
impl Display for SpectrogramScale {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
Self::Linear => "linear",
Self::Log => "log",
Self::Mel => "mel",
};
f.write_str(s)
}
}
#[cfg(feature = "transforms")]
impl FromStr for SpectrogramScale {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"linear" | "lin" => Ok(Self::Linear),
"log" | "logarithmic" | "db" | "decibel" => Ok(Self::Log),
"mel" | "mel-scale" | "melscale" => Ok(Self::Mel),
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {}. Got {}, must be one of {:?}",
std::any::type_name::<Self>(),
s,
["linear", "lin", "log", "db", "decibel", "mel", "mel-scale",]
))),
}
}
}
#[cfg(feature = "transforms")]
impl TryFrom<&str> for SpectrogramScale {
type Error = AudioSampleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(feature = "pitch-analysis")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum PitchDetectionMethod {
#[default]
Yin,
Autocorrelation,
Cepstrum,
HarmonicProduct,
}
#[cfg(feature = "pitch-analysis")]
impl Display for PitchDetectionMethod {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
Self::Yin => "yin",
Self::Autocorrelation => "autocorrelation",
Self::Cepstrum => "cepstrum",
Self::HarmonicProduct => "harmonic_product",
};
f.write_str(s)
}
}
#[cfg(feature = "pitch-analysis")]
impl FromStr for PitchDetectionMethod {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"yin" => Ok(Self::Yin),
"autocorrelation" | "auto" | "acf" => Ok(Self::Autocorrelation),
"cepstrum" | "cep" => Ok(Self::Cepstrum),
"harmonic_product" | "harmonic-product" | "hps" | "harmonic" => {
Ok(Self::HarmonicProduct)
}
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {}. Got {}, must be one of {:?}",
std::any::type_name::<Self>(),
s,
[
"yin",
"autocorrelation",
"acf",
"cepstrum",
"cep",
"harmonic_product",
"hps",
]
))),
}
}
}
#[cfg(feature = "pitch-analysis")]
impl TryFrom<&str> for PitchDetectionMethod {
type Error = AudioSampleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(feature = "iir-filtering")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum IirFilterType {
#[default]
Butterworth,
ChebyshevI,
ChebyshevII,
Elliptic,
}
#[cfg(feature = "iir-filtering")]
impl Display for IirFilterType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
Self::Butterworth => "butterworth",
Self::ChebyshevI => "chebyshev1",
Self::ChebyshevII => "chebyshev2",
Self::Elliptic => "elliptic",
};
f.write_str(s)
}
}
#[cfg(feature = "iir-filtering")]
impl FromStr for IirFilterType {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"butterworth" | "butter" | "bw" => Ok(Self::Butterworth),
"chebyshev1" | "cheby1" | "chebyshev_i" | "chebyshev-i" => Ok(Self::ChebyshevI),
"chebyshev2" | "cheby2" | "chebyshev_ii" | "chebyshev-ii" => Ok(Self::ChebyshevII),
"elliptic" | "cauer" | "ellip" => Ok(Self::Elliptic),
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {}. Got {}, must be one of {:?}",
std::any::type_name::<Self>(),
s,
[
"butterworth",
"chebyshev1",
"chebyshev2",
"elliptic",
"cauer",
]
))),
}
}
}
#[cfg(feature = "iir-filtering")]
impl TryFrom<&str> for IirFilterType {
type Error = AudioSampleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(feature = "iir-filtering")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum FilterResponse {
#[default]
LowPass,
HighPass,
BandPass,
BandStop,
}
#[cfg(feature = "iir-filtering")]
impl Display for FilterResponse {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
Self::LowPass => "lowpass",
Self::HighPass => "highpass",
Self::BandPass => "bandpass",
Self::BandStop => "bandstop",
};
f.write_str(s)
}
}
#[cfg(feature = "iir-filtering")]
impl FromStr for FilterResponse {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"lowpass" | "low-pass" | "lp" => Ok(Self::LowPass),
"highpass" | "high-pass" | "hp" => Ok(Self::HighPass),
"bandpass" | "band-pass" | "bp" => Ok(Self::BandPass),
"bandstop" | "band-stop" | "bs" | "notch" => Ok(Self::BandStop),
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {}. Got {}, must be one of {:?}",
std::any::type_name::<Self>(),
s,
[
"lowpass", "highpass", "bandpass", "bandstop", "lp", "hp", "bp", "bs",
]
))),
}
}
}
#[cfg(feature = "iir-filtering")]
impl TryFrom<&str> for FilterResponse {
type Error = AudioSampleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(feature = "iir-filtering")]
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct IirFilterDesign {
pub filter_type: IirFilterType,
pub response: FilterResponse,
pub order: NonZeroUsize,
pub cutoff_frequency: Option<f64>,
pub low_frequency: Option<f64>,
pub high_frequency: Option<f64>,
pub passband_ripple: Option<f64>,
pub stopband_attenuation: Option<f64>,
}
#[cfg(feature = "iir-filtering")]
impl IirFilterDesign {
#[inline]
#[must_use]
pub const fn butterworth_lowpass(order: NonZeroUsize, cutoff_frequency: f64) -> Self {
Self {
filter_type: IirFilterType::Butterworth,
response: FilterResponse::LowPass,
order,
cutoff_frequency: Some(cutoff_frequency),
low_frequency: None,
high_frequency: None,
passband_ripple: None,
stopband_attenuation: None,
}
}
#[inline]
#[must_use]
pub const fn butterworth_highpass(order: NonZeroUsize, cutoff_frequency: f64) -> Self {
Self {
filter_type: IirFilterType::Butterworth,
response: FilterResponse::HighPass,
order,
cutoff_frequency: Some(cutoff_frequency),
low_frequency: None,
high_frequency: None,
passband_ripple: None,
stopband_attenuation: None,
}
}
#[inline]
#[must_use]
pub const fn butterworth_bandpass(
order: NonZeroUsize,
low_frequency: f64,
high_frequency: f64,
) -> Self {
Self {
filter_type: IirFilterType::Butterworth,
response: FilterResponse::BandPass,
order,
cutoff_frequency: None,
low_frequency: Some(low_frequency),
high_frequency: Some(high_frequency),
passband_ripple: None,
stopband_attenuation: None,
}
}
#[inline]
#[must_use]
pub const fn chebyshev_i(
response: FilterResponse,
order: NonZeroUsize,
cutoff_frequency: f64,
passband_ripple: f64,
) -> Self {
Self {
filter_type: IirFilterType::ChebyshevI,
response,
order,
cutoff_frequency: Some(cutoff_frequency),
low_frequency: None,
high_frequency: None,
passband_ripple: Some(passband_ripple),
stopband_attenuation: None,
}
}
}
#[cfg(feature = "parametric-eq")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum EqBandType {
#[default]
Peak,
LowShelf,
HighShelf,
LowPass,
HighPass,
BandPass,
BandStop,
}
#[cfg(feature = "parametric-eq")]
impl Display for EqBandType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
Self::Peak => "peak",
Self::LowShelf => "low_shelf",
Self::HighShelf => "high_shelf",
Self::LowPass => "lowpass",
Self::HighPass => "highpass",
Self::BandPass => "bandpass",
Self::BandStop => "bandstop",
};
f.write_str(s)
}
}
#[cfg(feature = "parametric-eq")]
impl FromStr for EqBandType {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"peak" | "bell" | "notch" => Ok(Self::Peak),
"low_shelf" | "low-shelf" | "lowshelf" | "ls" => Ok(Self::LowShelf),
"high_shelf" | "high-shelf" | "highshelf" | "hs" => Ok(Self::HighShelf),
"lowpass" | "low-pass" | "lp" => Ok(Self::LowPass),
"highpass" | "high-pass" | "hp" => Ok(Self::HighPass),
"bandpass" | "band-pass" | "bp" => Ok(Self::BandPass),
"bandstop" | "band-stop" | "bs" => Ok(Self::BandStop),
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {}. Got {}, must be one of {:?}",
std::any::type_name::<Self>(),
s,
[
"peak",
"bell",
"low_shelf",
"high_shelf",
"lowpass",
"highpass",
"bandpass",
"bandstop",
]
))),
}
}
}
#[cfg(feature = "parametric-eq")]
impl TryFrom<&str> for EqBandType {
type Error = AudioSampleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(feature = "parametric-eq")]
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct EqBand {
pub band_type: EqBandType,
pub frequency: f64,
pub gain_db: f64,
pub q_factor: f64,
pub enabled: bool,
}
#[cfg(feature = "parametric-eq")]
impl EqBand {
#[inline]
#[must_use]
pub const fn peak(frequency: f64, gain_db: f64, q_factor: f64) -> Self {
Self {
band_type: EqBandType::Peak,
frequency,
gain_db,
q_factor,
enabled: true,
}
}
#[inline]
#[must_use]
pub const fn low_shelf(frequency: f64, gain_db: f64, q_factor: f64) -> Self {
Self {
band_type: EqBandType::LowShelf,
frequency,
gain_db,
q_factor,
enabled: true,
}
}
#[inline]
#[must_use]
pub const fn high_shelf(frequency: f64, gain_db: f64, q_factor: f64) -> Self {
Self {
band_type: EqBandType::HighShelf,
frequency,
gain_db,
q_factor,
enabled: true,
}
}
#[inline]
#[must_use]
pub const fn low_pass(frequency: f64, q_factor: f64) -> Self {
Self {
band_type: EqBandType::LowPass,
frequency,
gain_db: 0.0,
q_factor,
enabled: true,
}
}
#[inline]
#[must_use]
pub const fn high_pass(frequency: f64, q_factor: f64) -> Self {
Self {
band_type: EqBandType::HighPass,
frequency,
gain_db: 0.0,
q_factor,
enabled: true,
}
}
#[inline]
pub const fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
#[inline]
#[must_use]
pub const fn is_enabled(&self) -> bool {
self.enabled
}
#[inline]
pub fn validate(self, sample_rate: f64) -> AudioSampleResult<Self> {
let nyquist = sample_rate / 2.0;
if self.frequency <= 0.0 || self.frequency >= nyquist {
return Err(AudioSampleError::Parameter(ParameterError::out_of_range(
"frequency",
format!("{} Hz", self.frequency),
"0",
format!("{nyquist}"),
"Frequency must be between 0 and Nyquist frequency",
)));
}
if self.q_factor <= 0.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"q_factor",
"Q factor must be positive",
)));
}
if self.gain_db.abs() > 40.0 {
return Err(AudioSampleError::Parameter(ParameterError::out_of_range(
"gain_db",
format!("{} dB", self.gain_db),
"-40",
"40",
"Gain must be within reasonable range",
)));
}
Ok(self)
}
}
#[cfg(feature = "parametric-eq")]
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ParametricEq {
pub bands: Vec<EqBand>,
pub output_gain_db: f64,
pub bypassed: bool,
}
#[cfg(feature = "parametric-eq")]
impl ParametricEq {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
bands: Vec::new(),
output_gain_db: 0.0,
bypassed: false,
}
}
#[inline]
pub fn add_band(&mut self, band: EqBand) {
self.bands.push(band);
}
#[inline]
pub fn remove_band(&mut self, index: usize) -> Option<EqBand> {
if index < self.bands.len() {
Some(self.bands.remove(index))
} else {
None
}
}
#[inline]
#[must_use]
pub fn get_band(&self, index: usize) -> Option<&EqBand> {
self.bands.get(index)
}
#[inline]
pub fn get_band_mut(&mut self, index: usize) -> Option<&mut EqBand> {
self.bands.get_mut(index)
}
#[inline]
#[must_use]
pub const fn band_count(&self) -> usize {
self.bands.len()
}
#[inline]
pub const fn set_output_gain(&mut self, gain_db: f64) {
self.output_gain_db = gain_db;
}
#[inline]
pub const fn set_bypassed(&mut self, bypassed: bool) {
self.bypassed = bypassed;
}
#[inline]
#[must_use]
pub const fn is_bypassed(&self) -> bool {
self.bypassed
}
#[inline]
pub fn validate(self, sample_rate: f64) -> AudioSampleResult<Self> {
for (i, band) in self.bands.iter().enumerate() {
match band.validate(sample_rate) {
Ok(_) => {}
Err(er) => {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"band",
format!("Band {}/{} validation error: {}", i, self.bands.len(), er),
)));
}
}
}
Ok(self)
}
#[inline]
#[must_use]
pub fn three_band(
low_freq: f64,
low_gain: f64,
mid_freq: f64,
mid_gain: f64,
mid_q: f64,
high_freq: f64,
high_gain: f64,
) -> Self {
let mut eq = Self::new();
eq.add_band(EqBand::low_shelf(low_freq, low_gain, 0.707));
eq.add_band(EqBand::peak(mid_freq, mid_gain, mid_q));
eq.add_band(EqBand::high_shelf(high_freq, high_gain, 0.707));
eq
}
#[inline]
#[must_use]
pub fn five_band() -> Self {
let mut eq = Self::new();
eq.add_band(EqBand::low_shelf(100.0, 0.0, 0.707));
eq.add_band(EqBand::peak(300.0, 0.0, 1.0));
eq.add_band(EqBand::peak(1000.0, 0.0, 1.0));
eq.add_band(EqBand::peak(3000.0, 0.0, 1.0));
eq.add_band(EqBand::high_shelf(8000.0, 0.0, 0.707));
eq
}
}
#[cfg(feature = "parametric-eq")]
impl Default for ParametricEq {
fn default() -> Self {
Self::new()
}
}
#[cfg(any(feature = "parametric-eq", feature = "dynamic-range"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum KneeType {
Hard,
#[default]
Soft,
}
#[cfg(any(feature = "parametric-eq", feature = "dynamic-range"))]
impl Display for KneeType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
Self::Hard => "hard",
Self::Soft => "soft",
};
f.write_str(s)
}
}
#[cfg(any(feature = "parametric-eq", feature = "dynamic-range"))]
impl FromStr for KneeType {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"hard" | "hard-knee" | "hardknee" => Ok(Self::Hard),
"soft" | "soft-knee" | "softknee" => Ok(Self::Soft),
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {}. Got {}, must be one of {:?}",
std::any::type_name::<Self>(),
s,
[
"hard",
"hard-knee",
"hardknee",
"soft",
"soft-knee",
"softknee"
]
))),
}
}
}
#[cfg(any(feature = "parametric-eq", feature = "dynamic-range"))]
impl TryFrom<&str> for KneeType {
type Error = AudioSampleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(feature = "dynamic-range")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum DynamicRangeMethod {
#[default]
Rms,
Peak,
Hybrid,
}
#[cfg(feature = "dynamic-range")]
impl Display for DynamicRangeMethod {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
Self::Rms => "rms",
Self::Peak => "peak",
Self::Hybrid => "hybrid",
};
f.write_str(s)
}
}
#[cfg(feature = "dynamic-range")]
impl FromStr for DynamicRangeMethod {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"rms" | "average" | "avg" => Ok(Self::Rms),
"peak" | "pk" => Ok(Self::Peak),
"hybrid" | "mixed" | "combo" => Ok(Self::Hybrid),
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {}. Got {}, must be one of {:?}",
std::any::type_name::<Self>(),
s,
[
"rms", "average", "avg", "peak", "pk", "hybrid", "mixed", "combo"
]
))),
}
}
}
#[cfg(feature = "dynamic-range")]
impl TryFrom<&str> for DynamicRangeMethod {
type Error = AudioSampleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(feature = "dynamic-range")]
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct SideChainConfig {
pub enabled: bool,
pub high_pass_freq: Option<f64>,
pub low_pass_freq: Option<f64>,
pub pre_emphasis_db: f64,
pub external_mix: f64,
}
#[cfg(feature = "dynamic-range")]
impl SideChainConfig {
#[inline]
#[must_use]
pub const fn disabled() -> Self {
Self {
enabled: false,
high_pass_freq: None,
low_pass_freq: None,
pre_emphasis_db: 0.0,
external_mix: 0.0,
}
}
#[inline]
#[must_use]
pub const fn enabled() -> Self {
Self {
enabled: true,
high_pass_freq: Some(100.0),
low_pass_freq: None,
pre_emphasis_db: 0.0,
external_mix: 1.0,
}
}
#[inline]
pub const fn enable(&mut self) {
self.enabled = true;
}
#[inline]
pub const fn disable(&mut self) {
self.enabled = false;
}
#[inline]
pub const fn set_high_pass(&mut self, freq: f64) {
self.high_pass_freq = Some(freq);
}
#[inline]
pub const fn set_low_pass(&mut self, freq: f64) {
self.low_pass_freq = Some(freq);
}
#[inline]
pub fn validate(self, sample_rate: f64) -> AudioSampleResult<Self> {
if let Some(hp_freq) = self.high_pass_freq
&& (hp_freq <= 0.0 || hp_freq >= sample_rate / 2.0)
{
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"high_pass_freq",
"High-pass frequency must be between 0 and Nyquist frequency",
)));
}
if let Some(lp_freq) = self.low_pass_freq
&& (lp_freq <= 0.0 || lp_freq >= sample_rate / 2.0)
{
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"low_pass_freq",
"Low-pass frequency must be between 0 and Nyquist frequency",
)));
}
if let (Some(hp), Some(lp)) = (self.high_pass_freq, self.low_pass_freq)
&& (hp >= lp)
{
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"high_pass_freq",
"High-pass frequency must be less than low-pass frequency",
)));
}
if self.external_mix < 0.0 || self.external_mix > 1.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"external_mix",
"External mix must be between0.0 and1.0",
)));
}
Ok(self)
}
}
#[cfg(feature = "dynamic-range")]
impl Default for SideChainConfig {
fn default() -> Self {
Self::disabled()
}
}
#[cfg(feature = "dynamic-range")]
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct CompressorConfig {
pub threshold_db: f64,
pub ratio: f64,
pub attack_ms: f64,
pub release_ms: f64,
pub makeup_gain_db: f64,
pub knee_type: KneeType,
pub knee_width_db: f64,
pub detection_method: DynamicRangeMethod,
pub side_chain: SideChainConfig,
pub lookahead_ms: f64,
}
#[cfg(feature = "dynamic-range")]
impl CompressorConfig {
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[inline]
#[must_use]
pub const fn vocal() -> Self {
Self {
threshold_db: -18.0,
ratio: 3.0,
attack_ms: 2.0,
release_ms: 100.0,
makeup_gain_db: 3.0,
knee_type: KneeType::Soft,
knee_width_db: 4.0,
detection_method: DynamicRangeMethod::Rms,
side_chain: SideChainConfig::disabled(),
lookahead_ms: 0.0,
}
}
#[inline]
#[must_use]
pub const fn drum() -> Self {
Self {
threshold_db: -8.0,
ratio: 6.0,
attack_ms: 0.1,
release_ms: 20.0,
makeup_gain_db: 2.0,
knee_type: KneeType::Hard,
knee_width_db: 0.5,
detection_method: DynamicRangeMethod::Peak,
side_chain: SideChainConfig::disabled(),
lookahead_ms: 0.0,
}
}
#[inline]
#[must_use]
pub const fn bus() -> Self {
Self {
threshold_db: -20.0,
ratio: 2.0,
attack_ms: 10.0,
release_ms: 200.0,
makeup_gain_db: 1.0,
knee_type: KneeType::Soft,
knee_width_db: 6.0,
detection_method: DynamicRangeMethod::Rms,
side_chain: SideChainConfig::disabled(),
lookahead_ms: 0.0,
}
}
#[inline]
pub fn validate(self, sample_rate: f64) -> AudioSampleResult<Self> {
if self.threshold_db > 0.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"threshold_db",
"Threshold should be negative (below 0 dB)",
)));
}
if self.ratio < 1.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"ratio",
"Ratio must be1.0 or greater",
)));
}
if self.attack_ms < 0.01 || self.attack_ms > 1000.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Attack time must be between 0.01 and 1000 ms",
)));
}
if self.release_ms < 1.0 || self.release_ms > 10000.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Release time must be between1.0 and 10000 ms",
)));
}
if self.makeup_gain_db.abs() > 40.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Makeup gain must be between -40.0 and +40.0 dB",
)));
}
if self.knee_width_db < 0.0 || self.knee_width_db > 20.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Knee width must be between0.0 and 20.0 dB",
)));
}
if self.lookahead_ms < 0.0 || self.lookahead_ms > 20.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Lookahead time must be between0.0 and 20.0 ms",
)));
}
self.side_chain.validate(sample_rate)?;
Ok(self)
}
}
#[cfg(feature = "dynamic-range")]
impl Default for CompressorConfig {
fn default() -> Self {
Self {
threshold_db: -12.0,
ratio: 4.0,
attack_ms: 5.0,
release_ms: 50.0,
makeup_gain_db: 0.0,
knee_type: KneeType::Soft,
knee_width_db: 2.0,
detection_method: DynamicRangeMethod::Rms,
side_chain: SideChainConfig::disabled(),
lookahead_ms: 0.0,
}
}
}
#[cfg(feature = "dynamic-range")]
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct LimiterConfig {
pub ceiling_db: f64,
pub attack_ms: f64,
pub release_ms: f64,
pub knee_type: KneeType,
pub knee_width_db: f64,
pub detection_method: DynamicRangeMethod,
pub side_chain: SideChainConfig,
pub lookahead_ms: f64,
pub isp_limiting: bool,
}
#[cfg(feature = "dynamic-range")]
impl LimiterConfig {
#[inline]
#[must_use]
pub const fn new(
ceiling_db: f64,
attack_ms: f64,
release_ms: f64,
knee_type: KneeType,
knee_width_db: f64,
detection_method: DynamicRangeMethod,
lookahead_ms: f64,
isp_filtering: bool,
) -> Self {
Self {
ceiling_db,
attack_ms,
release_ms,
knee_type,
knee_width_db,
detection_method,
side_chain: SideChainConfig::disabled(),
lookahead_ms,
isp_limiting: isp_filtering,
}
}
#[inline]
#[must_use]
pub const fn transparent() -> Self {
Self {
ceiling_db: -0.1,
attack_ms: 0.1,
release_ms: 100.0,
knee_type: KneeType::Soft,
knee_width_db: 2.0,
detection_method: DynamicRangeMethod::Peak,
side_chain: SideChainConfig::disabled(),
lookahead_ms: 5.0,
isp_limiting: true,
}
}
#[inline]
#[must_use]
pub const fn mastering() -> Self {
Self {
ceiling_db: -0.3,
attack_ms: 1.0,
release_ms: 200.0,
knee_type: KneeType::Soft,
knee_width_db: 3.0,
detection_method: DynamicRangeMethod::Hybrid,
side_chain: SideChainConfig::disabled(),
lookahead_ms: 10.0,
isp_limiting: true,
}
}
#[inline]
#[must_use]
pub const fn broadcast() -> Self {
Self {
ceiling_db: -1.0,
attack_ms: 0.5,
release_ms: 50.0,
knee_type: KneeType::Hard,
knee_width_db: 0.5,
detection_method: DynamicRangeMethod::Peak,
side_chain: SideChainConfig::disabled(),
lookahead_ms: 2.0,
isp_limiting: true,
}
}
#[inline]
pub fn validate(self, sample_rate: f64) -> AudioSampleResult<Self> {
if self.ceiling_db > 0.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Ceiling should be negative (below 0 dB)",
)));
}
if self.attack_ms < 0.001 || self.attack_ms > 100.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Attack time must be between 0.001 and 100 ms",
)));
}
if self.release_ms < 1.0 || self.release_ms > 10000.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Release time must be between1.0 and 10000 ms",
)));
}
if self.knee_width_db < 0.0 || self.knee_width_db > 10.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Knee width must be between0.0 and 10.0 dB",
)));
}
if self.lookahead_ms < 0.0 || self.lookahead_ms > 20.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Lookahead time must be between0.0 and 20.0 ms",
)));
}
self.side_chain.validate(sample_rate)?;
Ok(self)
}
}
#[cfg(feature = "dynamic-range")]
impl Default for LimiterConfig {
fn default() -> Self {
Self {
ceiling_db: -0.1,
attack_ms: 0.5,
release_ms: 50.0,
knee_type: KneeType::Soft,
knee_width_db: 1.0,
detection_method: DynamicRangeMethod::Peak,
side_chain: SideChainConfig::disabled(),
lookahead_ms: 2.0,
isp_limiting: true,
}
}
}
#[cfg(feature = "peak-picking")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum AdaptiveThresholdMethod {
Delta,
#[default]
Percentile,
Combined,
}
#[cfg(feature = "peak-picking")]
impl Display for AdaptiveThresholdMethod {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
Self::Delta => "delta",
Self::Percentile => "percentile",
Self::Combined => "combined",
};
f.write_str(s)
}
}
#[cfg(feature = "peak-picking")]
impl FromStr for AdaptiveThresholdMethod {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"delta" | "offset" => Ok(Self::Delta),
"percentile" | "quantile" | "pct" => Ok(Self::Percentile),
"combined" | "hybrid" | "mixed" => Ok(Self::Combined),
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {}. Got {}, must be one of {:?}",
std::any::type_name::<Self>(),
s,
[
"delta",
"offset",
"percentile",
"quantile",
"pct",
"combined",
"hybrid",
"mixed"
]
))),
}
}
}
#[cfg(feature = "peak-picking")]
impl TryFrom<&str> for AdaptiveThresholdMethod {
type Error = AudioSampleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg(feature = "peak-picking")]
#[non_exhaustive]
pub struct AdaptiveThresholdConfig {
pub method: AdaptiveThresholdMethod,
pub delta: f64,
pub percentile: f64,
pub window_size: usize,
pub min_threshold: f64,
pub max_threshold: f64,
}
#[cfg(feature = "peak-picking")]
impl AdaptiveThresholdConfig {
#[inline]
#[must_use]
pub const fn new(
method: AdaptiveThresholdMethod,
delta: f64,
percentile: f64,
window_size: usize,
min_threshold: f64,
max_threshold: f64,
) -> Self {
Self {
method,
delta,
percentile,
window_size,
min_threshold,
max_threshold,
}
}
#[inline]
#[must_use]
pub const fn delta(delta: f64, window_size: usize) -> Self {
Self {
method: AdaptiveThresholdMethod::Delta,
delta,
percentile: 0.9,
window_size,
min_threshold: 0.01,
max_threshold: 1.0,
}
}
#[inline]
#[must_use]
pub const fn percentile(percentile: f64, window_size: usize) -> Self {
Self {
method: AdaptiveThresholdMethod::Percentile,
delta: 0.05,
percentile,
window_size,
min_threshold: 0.01,
max_threshold: 1.0,
}
}
#[inline]
#[must_use]
pub const fn combined(delta: f64, percentile: f64, window_size: usize) -> Self {
Self {
method: AdaptiveThresholdMethod::Combined,
delta,
percentile,
window_size,
min_threshold: 0.01,
max_threshold: 1.0,
}
}
#[inline]
pub const fn set_min_threshold(&mut self, min_threshold: f64) {
self.min_threshold = min_threshold;
}
#[inline]
pub const fn set_max_threshold(&mut self, max_threshold: f64) {
self.max_threshold = max_threshold;
}
#[inline]
pub fn validate(self) -> AudioSampleResult<Self> {
if self.delta < 0.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Delta must be non-negative",
)));
}
if self.percentile < 0.0 || self.percentile > 1.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Percentile must be between0.0 and1.0",
)));
}
if self.window_size == 0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Window size must be greater than 0",
)));
}
if self.min_threshold < 0.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Minimum threshold must be non-negative",
)));
}
if self.max_threshold <= self.min_threshold {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Maximum threshold must be greater than minimum threshold",
)));
}
Ok(self)
}
}
#[cfg(feature = "peak-picking")]
impl Default for AdaptiveThresholdConfig {
fn default() -> Self {
Self {
method: AdaptiveThresholdMethod::Delta,
delta: 0.05,
percentile: 0.9,
window_size: 1024,
min_threshold: 0.01,
max_threshold: 1.0,
}
}
}
#[cfg(feature = "peak-picking")]
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct PeakPickingConfig {
pub adaptive_threshold: AdaptiveThresholdConfig,
pub min_peak_separation: NonZeroUsize,
pub pre_emphasis: bool,
pub pre_emphasis_coeff: f64,
pub median_filter: bool,
pub median_filter_length: NonZeroUsize,
pub normalize_onset_strength: bool,
pub normalization_method: NormalizationMethod,
}
#[cfg(feature = "peak-picking")]
impl PeakPickingConfig {
#[inline]
#[must_use]
pub const fn new(
adaptive_threshold: AdaptiveThresholdConfig,
min_peak_separation: NonZeroUsize,
pre_emphasis: bool,
pre_emphasis_coeff: f64,
median_filter: bool,
median_filter_length: NonZeroUsize,
normalize_onset_strength: bool,
normalization_method: NormalizationMethod,
) -> Self {
Self {
adaptive_threshold,
min_peak_separation,
pre_emphasis,
pre_emphasis_coeff,
median_filter,
median_filter_length,
normalize_onset_strength,
normalization_method,
}
}
#[inline]
#[must_use]
pub const fn music() -> Self {
Self {
adaptive_threshold: AdaptiveThresholdConfig::combined(0.03, 0.85, 2048),
min_peak_separation: NonZeroUsize::new(1024).expect("Must be non-zero"),
pre_emphasis: true,
pre_emphasis_coeff: 0.95,
median_filter: true,
median_filter_length: NonZeroUsize::new(5).expect("Must be non-zero"),
normalize_onset_strength: true,
normalization_method: NormalizationMethod::Peak,
}
}
#[inline]
#[must_use]
pub const fn speech() -> Self {
Self {
adaptive_threshold: AdaptiveThresholdConfig::delta(0.07, 1024),
min_peak_separation: NonZeroUsize::new(256).expect("Must be non-zero"),
pre_emphasis: true,
pre_emphasis_coeff: 0.98,
median_filter: true,
median_filter_length: NonZeroUsize::new(3).expect("Must be non-zero"),
normalize_onset_strength: true,
normalization_method: NormalizationMethod::Peak,
}
}
#[inline]
#[must_use]
pub const fn drums() -> Self {
Self {
adaptive_threshold: AdaptiveThresholdConfig::percentile(0.95, 512),
min_peak_separation: NonZeroUsize::new(128).expect("Must be non-zero"),
pre_emphasis: true,
pre_emphasis_coeff: 0.93,
median_filter: false,
median_filter_length: NonZeroUsize::new(3).expect("Must be non-zero"),
normalize_onset_strength: true,
normalization_method: NormalizationMethod::Peak,
}
}
#[inline]
pub const fn set_min_peak_separation(&mut self, samples: NonZeroUsize) {
self.min_peak_separation = samples;
}
#[inline]
pub fn set_min_peak_separation_ms(&mut self, ms: f64, sample_rate: f64) {
self.min_peak_separation =
NonZeroUsize::new((ms * sample_rate / 1000.0) as usize).expect("Must be non-zero");
}
#[inline]
pub const fn set_pre_emphasis(&mut self, enabled: bool, coeff: f64) {
self.pre_emphasis = enabled;
self.pre_emphasis_coeff = coeff;
}
#[inline]
pub const fn set_median_filter(&mut self, enabled: bool, length: NonZeroUsize) {
self.median_filter = enabled;
self.median_filter_length = length;
}
#[inline]
pub fn validate(self) -> AudioSampleResult<Self> {
self.adaptive_threshold.validate()?;
if self.pre_emphasis_coeff < 0.0 || self.pre_emphasis_coeff > 1.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Pre-emphasis coefficient must be between0.0 and1.0",
)));
}
if self.median_filter_length.get().is_multiple_of(2) {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Median filter length must be a positive odd integer",
)));
}
Ok(self)
}
}
#[cfg(feature = "peak-picking")]
impl Default for PeakPickingConfig {
fn default() -> Self {
Self {
adaptive_threshold: AdaptiveThresholdConfig::default(),
min_peak_separation: NonZeroUsize::new(512).expect("Must be non-zero"),
pre_emphasis: true,
pre_emphasis_coeff: 0.97,
median_filter: true,
median_filter_length: NonZeroUsize::new(3).expect("Must be non-zero"),
normalize_onset_strength: true,
normalization_method: NormalizationMethod::Peak,
}
}
}
#[cfg(feature = "editing")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum NoiseColor {
#[default]
White,
Pink,
Brown,
}
#[cfg(feature = "editing")]
impl Display for NoiseColor {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
Self::White => "white",
Self::Pink => "pink",
Self::Brown => "brown",
};
f.write_str(s)
}
}
#[cfg(feature = "editing")]
impl FromStr for NoiseColor {
type Err = AudioSampleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalised = s.trim().to_ascii_lowercase();
match normalised.as_str() {
"white" | "white_noise" | "whitenoise" => Ok(Self::White),
"pink" | "pink_noise" | "pinknoise" => Ok(Self::Pink),
"brown" | "brown_noise" | "brownian" | "red" | "red_noise" => Ok(Self::Brown),
_ => Err(AudioSampleError::parse::<Self, _>(format!(
"Failed to parse {}. Got {}, must be one of {:?}",
std::any::type_name::<Self>(),
s,
[
"white",
"white_noise",
"whitenoise",
"pink",
"pink_noise",
"pinknoise",
"brown",
"brownian",
"red",
"red_noise"
]
))),
}
}
}
#[cfg(feature = "editing")]
impl TryFrom<&str> for NoiseColor {
type Error = AudioSampleError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(feature = "editing")]
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum PerturbationMethod {
GaussianNoise {
target_snr_db: f64,
noise_color: NoiseColor,
},
RandomGain {
min_gain_db: f64,
max_gain_db: f64,
},
HighPassFilter {
cutoff_hz: f64,
slope_db_per_octave: Option<f64>,
},
LowPassFilter {
cutoff_hz: f64,
slope_db_per_octave: Option<f64>,
},
PitchShift {
semitones: f64,
preserve_formants: bool,
},
}
#[cfg(feature = "editing")]
impl PerturbationMethod {
#[inline]
#[must_use]
pub const fn gaussian_noise(target_snr_db: f64, noise_color: NoiseColor) -> Self {
Self::GaussianNoise {
target_snr_db,
noise_color,
}
}
#[inline]
#[must_use]
pub const fn random_gain(min_gain_db: f64, max_gain_db: f64) -> Self {
Self::RandomGain {
min_gain_db,
max_gain_db,
}
}
#[inline]
#[must_use]
pub const fn high_pass_filter(cutoff_hz: f64) -> Self {
Self::HighPassFilter {
cutoff_hz,
slope_db_per_octave: None,
}
}
#[inline]
#[must_use]
pub const fn high_pass_filter_with_slope(cutoff_hz: f64, slope_db_per_octave: f64) -> Self {
Self::HighPassFilter {
cutoff_hz,
slope_db_per_octave: Some(slope_db_per_octave),
}
}
#[inline]
#[must_use]
pub const fn low_pass_filter(cutoff_hz: f64, slope_db_per_octave: Option<f64>) -> Self {
Self::LowPassFilter {
cutoff_hz,
slope_db_per_octave,
}
}
#[inline]
#[must_use]
pub const fn pitch_shift(semitones: f64, preserve_formants: bool) -> Self {
Self::PitchShift {
semitones,
preserve_formants,
}
}
#[inline]
pub fn validate(self, sample_rate: f64) -> AudioSampleResult<Self> {
match self {
Self::GaussianNoise { target_snr_db, .. } => {
if !(-60.0..=60.0).contains(&target_snr_db) {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Target SNR should be between -60 and 60 dB",
)));
}
}
Self::RandomGain {
min_gain_db,
max_gain_db,
} => {
if min_gain_db >= max_gain_db {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Minimum gain must be less than maximum gain",
)));
}
if min_gain_db < -40.0 || max_gain_db > 20.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Gain values should be between -40 dB and +20 dB",
)));
}
}
Self::HighPassFilter {
cutoff_hz,
slope_db_per_octave,
}
| Self::LowPassFilter {
cutoff_hz,
slope_db_per_octave,
} => {
let nyquist = sample_rate / 2.0;
if cutoff_hz <= 0.0 || cutoff_hz >= nyquist {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
format!("Cutoff frequency must be between 0 and Nyquist ({nyquist:.1} Hz)"),
)));
}
if let Some(slope) = slope_db_per_octave
&& !(0.0..=48.0).contains(&slope)
{
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Slope must be between 0 and 48 dB/octave",
)));
}
}
Self::PitchShift { semitones, .. } => {
if semitones.abs() > 12.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Pitch shift should be between -12 and +12 semitones",
)));
}
}
}
Ok(self)
}
}
#[cfg(feature = "editing")]
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct PerturbationConfig {
pub method: PerturbationMethod,
pub seed: Option<u64>,
}
#[cfg(feature = "editing")]
impl PerturbationConfig {
#[inline]
#[must_use]
pub const fn new(method: PerturbationMethod) -> Self {
Self { method, seed: None }
}
#[inline]
#[must_use]
pub const fn with_seed(method: PerturbationMethod, seed: u64) -> Self {
Self {
method,
seed: Some(seed),
}
}
#[inline]
pub fn validate(self, sample_rate: f64) -> AudioSampleResult<Self> {
self.method.validate(sample_rate)?;
Ok(self)
}
}
#[cfg(feature = "editing")]
impl Default for PerturbationConfig {
fn default() -> Self {
Self::new(PerturbationMethod::GaussianNoise {
target_snr_db: 20.0,
noise_color: NoiseColor::White,
})
}
}