Skip to main content

spectrograms/python/
params.rs

1//! Python parameter wrapper classes.
2
3use std::num::NonZeroUsize;
4
5use num_complex::Complex;
6use numpy::{PyArray1, PyArray2, PyReadonlyArray1};
7use pyo3::prelude::*;
8use pyo3::types::PyType;
9
10use crate::{
11    ChromaNorm, ChromaParams, CqtParams, ErbParams, LogHzParams, LogParams, MelNorm, MelParams,
12    MfccParams, SpectrogramParams, StftParams, StftResult, WindowType,
13};
14
15/// Python wrapper for `WindowType`.
16///
17/// Represents window functions used for spectral analysis. Different windows provide
18/// different trade-offs between frequency resolution and spectral leakage.
19#[pyclass(name = "WindowType", from_py_object)]
20#[derive(Clone, Debug)]
21pub struct PyWindowType {
22    pub(crate) inner: WindowType,
23}
24
25impl PyWindowType {
26    #[must_use]
27    pub fn into_inner(self) -> WindowType {
28        self.inner
29    }
30    #[must_use]
31    pub const fn as_inner(&self) -> &WindowType {
32        &self.inner
33    }
34}
35
36#[pymethods]
37impl PyWindowType {
38    /// Create a rectangular (no) window.
39    ///
40    /// Best frequency resolution but high spectral leakage.
41    #[classattr]
42    const fn rectangular() -> Self {
43        Self {
44            inner: WindowType::Rectangular,
45        }
46    }
47
48    /// Create a Hanning window.
49    ///
50    /// Good general-purpose window with moderate leakage.
51    #[classattr]
52    const fn hanning() -> Self {
53        Self {
54            inner: WindowType::Hanning,
55        }
56    }
57
58    /// Create a Hamming window.
59    ///
60    /// Similar to Hanning but with slightly different coefficients.
61    #[classattr]
62    const fn hamming() -> Self {
63        Self {
64            inner: WindowType::Hamming,
65        }
66    }
67
68    /// Create a Blackman window.
69    ///
70    /// Low spectral leakage but wider main lobe.
71    #[classattr]
72    const fn blackman() -> Self {
73        Self {
74            inner: WindowType::Blackman,
75        }
76    }
77
78    /// Create a Kaiser window with the given beta parameter.
79    ///
80    /// Parameters
81    /// ----------
82    ///
83    /// `beta` : float
84    ///    Beta parameter controlling the trade-off between main lobe width and side lobe level
85    ///
86    /// Returns
87    /// -------
88    ///
89    /// WindowType
90    ///    Kaiser window type
91    #[classmethod]
92    #[pyo3(signature = (beta: "float"), text_signature = "(beta: float) -> WindowType")]
93    const fn kaiser(_cls: &Bound<'_, PyType>, beta: f64) -> Self {
94        Self {
95            inner: WindowType::Kaiser { beta },
96        }
97    }
98
99    /// Create a Gaussian window with the given standard deviation.
100    ///
101    /// Parameters
102    /// ----------
103    ///
104    /// `std` : float
105    ///     Standard deviation parameter controlling the window width
106    ///
107    /// Returns
108    /// -------
109    ///
110    /// WindowType
111    ///    Gaussian window type
112    #[classmethod]
113    #[pyo3(signature = (std: "float"), text_signature = "(std: float) -> WindowType")]
114    const fn gaussian(_cls: &Bound<'_, PyType>, std: f64) -> Self {
115        Self {
116            inner: WindowType::Gaussian { std },
117        }
118    }
119
120    /// Create a custom window from pre-computed coefficients.
121    ///
122    /// The coefficients will be validated (must be finite) and stored for use
123    /// in spectrogram computation. The length of the coefficients must exactly
124    /// match the FFT size (`n_fft`) that will be used in your STFT parameters.
125    ///
126    /// Parameters
127    /// ----------
128    /// coefficients : array_like
129    ///     1D array of window coefficients. Will be converted to float64.
130    ///     All values must be finite (not NaN or infinity).
131    /// normalize : str, optional
132    ///     Optional normalization mode:
133    ///     - None: No normalization (use coefficients as-is)
134    ///     - "sum": Normalize so sum equals 1.0
135    ///     - "peak" or "max": Normalize so maximum value equals 1.0
136    ///     - "energy" or "rms": Normalize so sum of squares equals 1.0
137    ///
138    /// Returns
139    /// -------
140    /// WindowType
141    ///     Custom window type
142    ///
143    /// Raises
144    /// ------
145    /// ValueError
146    ///     If coefficients array is empty, contains non-finite values,
147    ///     unknown normalization mode, or normalization would divide by zero
148    ///
149    /// Examples
150    /// --------
151    /// Create a custom window from NumPy array:
152    ///
153    /// >>> import numpy as np
154    /// >>> import spectrograms as sg
155    /// >>> # Use a pre-made window from NumPy
156    /// >>> window = sg.WindowType.custom(np.blackman(512))
157    /// >>> # Or use SciPy windows
158    /// >>> from scipy.signal.windows import tukey
159    /// >>> window = sg.WindowType.custom(tukey(512, alpha=0.5))
160    /// >>> # Use in STFT parameters
161    /// >>> stft = sg.StftParams(n_fft=512, hop_size=256, window=window)
162    /// >>> # Create with normalization
163    /// >>> window_norm = sg.WindowType.custom(np.hamming(512), normalize="sum")
164    ///
165    /// Notes
166    /// -----
167    /// The length of the custom window coefficients must exactly match the
168    /// `n_fft` parameter used in your STFT configuration. A mismatch will
169    /// cause an error at STFT parameter creation time.
170    #[classmethod]
171    #[pyo3(signature = (coefficients, normalize=None), text_signature = "(coefficients, normalize=None) -> WindowType")]
172    fn custom(
173        _cls: &Bound<'_, PyType>,
174        coefficients: PyReadonlyArray1<f64>,
175        normalize: Option<&str>,
176    ) -> PyResult<Self> {
177        let vec = coefficients.as_slice()?.to_vec();
178        let inner = WindowType::custom_with_normalization(vec, normalize)?;
179        Ok(Self { inner })
180    }
181
182    /// Create a Hanning window of length `n`.
183    ///
184    /// Parameters
185    /// ----------
186    ///
187    /// `n` : int
188    ///
189    /// Returns
190    /// -------
191    ///
192    /// numpy.ndarray
193    ///     Hanning window of length `n`
194    #[staticmethod]
195    #[pyo3(signature = (n: "int"), text_signature = "(n: int) -> numpy.ndarray")]
196    fn make_hanning(py: Python<'_>, n: NonZeroUsize) -> Bound<'_, PyArray1<f64>> {
197        let window_vec = crate::window::hanning_window(n);
198        PyArray1::from_vec(py, window_vec.into_vec())
199    }
200
201    /// Create a Hamming window of length `n`.
202    ///
203    /// Parameters
204    /// ----------
205    ///
206    /// `n` : int
207    ///
208    /// Returns
209    /// -------
210    ///
211    /// numpy.ndarray
212    ///    Hamming window of length `n`
213    #[staticmethod]
214    #[pyo3(signature = (n: "int"), text_signature = "(n: int) -> numpy.ndarray")]
215    fn make_hamming(py: Python<'_>, n: NonZeroUsize) -> Bound<'_, PyArray1<f64>> {
216        let window_vec = crate::window::hamming_window(n);
217        PyArray1::from_vec(py, window_vec.into_vec())
218    }
219
220    /// Create a Blackman window of length `n`.
221    ///
222    /// Parameters
223    /// ----------
224    ///
225    /// `n` : int
226    ///
227    /// Returns
228    /// -------
229    ///
230    /// numpy.ndarray
231    ///     Blackman window of length `n`
232    #[staticmethod]
233    #[pyo3(signature = (n: "int"), text_signature = "(n: int) -> numpy.ndarray")]
234    fn make_blackman(py: Python<'_>, n: NonZeroUsize) -> Bound<'_, PyArray1<f64>> {
235        let window_vec = crate::window::blackman_window(n);
236        PyArray1::from_vec(py, window_vec.into_vec())
237    }
238
239    /// Create a Kaiser window of length `n` with parameter `beta`.
240    ///
241    /// Parameters
242    /// ----------
243    ///
244    /// `n` : int
245    /// `beta` : float
246    ///
247    /// Returns
248    /// -------
249    ///
250    /// numpy.ndarray
251    ///     Kaiser window of length `n`
252    #[staticmethod]
253    #[pyo3(signature = (n: "int", beta: "float"), text_signature = "(n: int, beta: float) -> numpy.ndarray")]
254    fn make_kaiser(py: Python<'_>, n: NonZeroUsize, beta: f64) -> Bound<'_, PyArray1<f64>> {
255        let window_vec = crate::window::kaiser_window(n, beta);
256        PyArray1::from_vec(py, window_vec.into_vec())
257    }
258
259    /// Create a Gaussian window of length `n` with standard deviation `std`.
260    ///
261    /// Parameters
262    /// ----------
263    ///
264    /// `n` : int
265    /// `std` : float
266    ///
267    /// Returns
268    /// -------
269    ///
270    /// numpy.ndarray
271    ///     Gaussian window of length `n`
272    #[staticmethod]
273    #[pyo3(signature = (n: "int", std: "float"), text_signature = "(n: int, std: float) -> numpy.ndarray")]
274    fn make_gaussian(py: Python<'_>, n: NonZeroUsize, std: f64) -> Bound<'_, PyArray1<f64>> {
275        let window_vec = crate::window::gaussian_window(n, std);
276        PyArray1::from_vec(py, window_vec.into_vec())
277    }
278
279    fn __repr__(&self) -> String {
280        format!("{}", self.inner)
281    }
282}
283
284impl From<WindowType> for PyWindowType {
285    fn from(wt: WindowType) -> Self {
286        Self { inner: wt }
287    }
288}
289
290#[pyclass(name = "StftResult", from_py_object)]
291#[derive(Clone, Debug)]
292pub struct PyStftResult {
293    pub(crate) inner: StftResult,
294}
295
296impl From<StftResult> for PyStftResult {
297    fn from(inner: StftResult) -> Self {
298        Self { inner }
299    }
300}
301
302impl From<PyStftResult> for StftResult {
303    #[inline]
304    fn from(val: PyStftResult) -> Self {
305        val.inner
306    }
307}
308
309impl PyStftResult {
310    #[must_use]
311    pub const fn from_inner(inner: StftResult) -> Self {
312        Self { inner }
313    }
314
315    #[must_use]
316    pub fn into_inner(self) -> StftResult {
317        self.inner
318    }
319}
320
321#[pymethods]
322impl PyStftResult {
323    #[getter]
324    fn n_bins(&self) -> usize {
325        self.inner.n_bins().get()
326    }
327
328    #[getter]
329    fn n_frames(&self) -> usize {
330        self.inner.n_frames().get()
331    }
332
333    #[getter]
334    fn frequency_resolution(&self) -> f64 {
335        self.inner.frequency_resolution()
336    }
337
338    #[getter]
339    fn time_resolution(&self) -> f64 {
340        self.inner.time_resolution()
341    }
342
343    #[getter]
344    fn params(&self) -> PyStftParams {
345        PyStftParams {
346            inner: self.inner.params.clone(),
347        }
348    }
349
350    #[getter]
351    const fn sample_rate(&self) -> f64 {
352        self.inner.sample_rate
353    }
354
355    fn norm<'py>(&'py self, py: Python<'py>) -> Bound<'py, PyArray2<f64>> {
356        PyArray2::from_owned_array(py, self.inner.norm())
357    }
358
359    fn data<'py>(&'py self, py: Python<'py>) -> Bound<'py, PyArray2<Complex<f64>>> {
360        PyArray2::from_owned_array(py, self.inner.data.clone())
361    }
362}
363
364/// STFT parameters for spectrogram computation.
365#[pyclass(name = "StftParams", from_py_object)]
366#[derive(Clone, Debug)]
367pub struct PyStftParams {
368    pub inner: StftParams,
369}
370
371#[pymethods]
372impl PyStftParams {
373    /// Create new STFT parameters.
374    ///
375    /// Parameters
376    /// ----------
377    /// `n_fft` : int
378    ///     FFT size
379    /// `hop_size` : int
380    ///     Hop size between frames
381    /// window : `WindowType`
382    ///     Window function
383    /// centre : bool, default=True
384    ///     Whether to centre frames with padding
385    ///
386    /// Returns
387    /// -------
388    /// `StftParams`
389    ///     STFT parameters
390    #[new]
391    #[pyo3(signature = (
392        n_fft: "int",
393        hop_size: "int",
394        window: "WindowType",
395        centre: "bool" = true
396    ), text_signature = "(n_fft: int, hop_size: int, window: WindowType, centre: bool = True)")]
397    fn new(
398        n_fft: NonZeroUsize,
399        hop_size: NonZeroUsize,
400        window: PyWindowType,
401        centre: bool,
402    ) -> PyResult<Self> {
403        let inner = StftParams::new(n_fft, hop_size, window.inner, centre)?;
404        Ok(Self { inner })
405    }
406
407    /// FFT size.
408    #[getter]
409    const fn n_fft(&self) -> NonZeroUsize {
410        self.inner.n_fft()
411    }
412
413    /// Hop size between frames.
414    #[getter]
415    const fn hop_size(&self) -> NonZeroUsize {
416        self.inner.hop_size()
417    }
418
419    /// Window function.
420    #[getter]
421    fn window(&self) -> PyWindowType {
422        PyWindowType {
423            inner: self.inner.window(),
424        }
425    }
426
427    /// Whether to centre frames with padding.
428    #[getter]
429    const fn centre(&self) -> bool {
430        self.inner.centre()
431    }
432
433    fn __repr__(&self) -> String {
434        format!(
435            "StftParams(n_fft={}, hop_size={}, window={}, centre={})",
436            self.n_fft(),
437            self.hop_size(),
438            self.window().__repr__(),
439            self.centre()
440        )
441    }
442}
443
444impl From<PyStftParams> for StftParams {
445    #[inline]
446    fn from(val: PyStftParams) -> Self {
447        val.inner
448    }
449}
450
451impl From<StftParams> for PyStftParams {
452    #[inline]
453    fn from(inner: StftParams) -> Self {
454        Self { inner }
455    }
456}
457
458/// Decibel conversion parameters.
459
460#[pyclass(name = "LogParams", from_py_object)]
461#[derive(Debug, Copy, Clone, PartialEq)]
462pub struct PyLogParams {
463    pub(crate) inner: LogParams,
464}
465
466impl PyLogParams {
467    #[inline]
468    #[must_use]
469    pub const fn into_inner(self) -> LogParams {
470        self.inner
471    }
472
473    #[inline]
474    #[must_use]
475    pub const fn as_inner(&self) -> &LogParams {
476        &self.inner
477    }
478}
479
480#[pymethods]
481impl PyLogParams {
482    /// Parameters
483    /// ----------
484    /// `floor_db` : float
485    ///     Minimum power in decibels (values below this are clipped)
486    #[new]
487    #[pyo3(signature = (floor_db: "float"), text_signature = "(floor_db: float)")]
488    fn new(floor_db: f64) -> PyResult<Self> {
489        let inner = LogParams::new(floor_db)?;
490        Ok(Self { inner })
491    }
492
493    /// Minimum power in decibels (values below this are clipped).
494    #[getter]
495    const fn floor_db(&self) -> f64 {
496        self.inner.floor_db()
497    }
498
499    fn __repr__(&self) -> String {
500        format!("LogParams(floor_db={})", self.floor_db())
501    }
502}
503
504impl From<PyLogParams> for LogParams {
505    #[inline]
506    fn from(val: PyLogParams) -> Self {
507        val.inner
508    }
509}
510
511impl From<LogParams> for PyLogParams {
512    #[inline]
513    fn from(inner: LogParams) -> Self {
514        Self { inner }
515    }
516}
517
518/// Spectrogram computation parameters.
519#[pyclass(name = "SpectrogramParams", from_py_object)]
520#[derive(Clone, Debug)]
521pub struct PySpectrogramParams {
522    pub(crate) inner: SpectrogramParams,
523}
524
525#[pymethods]
526impl PySpectrogramParams {
527    /// Parameters
528    /// ----------
529    /// stft : `StftParams`
530    ///     STFT parameters
531    /// `sample_rate` : float
532    ///     Sample rate in Hz
533    #[new]
534    #[pyo3(signature = (
535        stft: "StftParams",
536        sample_rate: "float"
537    ), text_signature = "(stft: StftParams, sample_rate: float)")]
538    fn new(stft: &PyStftParams, sample_rate: f64) -> PyResult<Self> {
539        let inner = SpectrogramParams::new(stft.inner.clone(), sample_rate)?;
540        Ok(Self { inner })
541    }
542
543    /// STFT parameters.
544    #[getter]
545    fn stft(&self) -> PyStftParams {
546        PyStftParams {
547            inner: self.inner.stft().clone(),
548        }
549    }
550
551    /// Sample rate in Hz.
552    #[getter]
553    const fn sample_rate(&self) -> f64 {
554        self.inner.sample_rate_hz()
555    }
556
557    /// Create default parameters for speech processing.
558    ///
559    /// Uses `n_fft=512`, `hop_size=160`, Hanning window, centre=true
560    ///
561    /// Parameters
562    /// ----------
563    /// `sample_rate` : float
564    ///     Sample rate in Hz
565    ///
566    /// Returns
567    /// -------
568    /// SpectrogramParams
569    ///     `SpectrogramParams` with standard speech settings
570    #[classmethod]
571    #[pyo3(signature = (sample_rate: "float"), text_signature = "(sample_rate: float)")]
572    fn speech_default(_cls: &Bound<'_, PyType>, sample_rate: f64) -> PyResult<Self> {
573        let inner = SpectrogramParams::speech_default(sample_rate)?;
574        Ok(Self { inner })
575    }
576
577    /// Create default parameters for music processing.
578    ///
579    /// Uses `n_fft=2048`, `hop_size=512`, Hanning window, centre=true
580    ///
581    /// Parameters
582    /// ----------
583    /// `sample_rate` : float
584    ///     Sample rate in Hz
585    ///
586    /// Returns
587    /// -------
588    /// SpectrogramParams
589    ///     `SpectrogramParams` with standard music settings
590    #[classmethod]
591    #[pyo3(signature = (sample_rate: "float"), text_signature = "(sample_rate: float)")]
592    fn music_default(_cls: &Bound<'_, PyType>, sample_rate: f64) -> PyResult<Self> {
593        let inner = SpectrogramParams::music_default(sample_rate)?;
594        Ok(Self { inner })
595    }
596
597    fn __repr__(&self) -> String {
598        format!(
599            "SpectrogramParams(sample_rate={}, n_fft={}, hop_size={})",
600            self.sample_rate(),
601            self.inner.stft().n_fft(),
602            self.inner.stft().hop_size()
603        )
604    }
605}
606
607impl From<SpectrogramParams> for PySpectrogramParams {
608    fn from(inner: SpectrogramParams) -> Self {
609        Self { inner }
610    }
611}
612
613impl From<PySpectrogramParams> for SpectrogramParams {
614    #[inline]
615    fn from(py_params: PySpectrogramParams) -> Self {
616        py_params.inner
617    }
618}
619
620/// Mel filterbank normalization strategy.
621#[pyclass(name = "MelNorm", from_py_object)]
622#[derive(Clone, Copy, Debug, PartialEq, Eq)]
623pub enum PyMelNorm {
624    /// No normalization (triangular filters with peak = 1.0).
625    None,
626    /// Slaney-style area normalization (librosa default).
627    Slaney,
628    /// L1 normalization (sum of weights = 1.0).
629    L1,
630    /// L2 normalization (Euclidean norm = 1.0).
631    L2,
632}
633
634#[pymethods]
635impl PyMelNorm {
636    #[classattr]
637    const fn none() -> Self {
638        Self::None
639    }
640
641    #[classattr]
642    const fn slaney() -> Self {
643        Self::Slaney
644    }
645
646    #[classattr]
647    const fn l1() -> Self {
648        Self::L1
649    }
650
651    #[classattr]
652    const fn l2() -> Self {
653        Self::L2
654    }
655
656    fn __repr__(&self) -> String {
657        match self {
658            Self::None => "MelNorm.None".to_string(),
659            Self::Slaney => "MelNorm.Slaney".to_string(),
660            Self::L1 => "MelNorm.L1".to_string(),
661            Self::L2 => "MelNorm.L2".to_string(),
662        }
663    }
664}
665
666impl From<PyMelNorm> for MelNorm {
667    #[inline]
668    fn from(py_norm: PyMelNorm) -> Self {
669        match py_norm {
670            PyMelNorm::None => Self::None,
671            PyMelNorm::Slaney => Self::Slaney,
672            PyMelNorm::L1 => Self::L1,
673            PyMelNorm::L2 => Self::L2,
674        }
675    }
676}
677
678impl From<MelNorm> for PyMelNorm {
679    #[inline]
680    fn from(norm: MelNorm) -> Self {
681        match norm {
682            MelNorm::None => Self::None,
683            MelNorm::Slaney => Self::Slaney,
684            MelNorm::L1 => Self::L1,
685            MelNorm::L2 => Self::L2,
686        }
687    }
688}
689
690/// Mel-scale filterbank parameters.
691#[pyclass(name = "MelParams", from_py_object)]
692#[derive(Clone, Copy, Debug)]
693pub struct PyMelParams {
694    pub(crate) inner: MelParams,
695}
696
697#[pymethods]
698impl PyMelParams {
699    /// Mel-scale filterbank parameters.
700    ///
701    /// Parameters
702    /// ----------
703    /// `n_mels` : int
704    ///     Number of mel bands
705    /// `f_min` : float
706    ///     Minimum frequency in Hz
707    /// `f_max` : float
708    ///     Maximum frequency in Hz
709    /// `norm` : MelNorm or str, optional
710    ///     Filterbank normalization strategy. Can be:
711    ///     - None or "none": No normalization (default)
712    ///     - "slaney": Slaney-style area normalization (librosa default)
713    ///     - "l1": L1 normalization (sum of weights = 1.0)
714    ///     - "l2": L2 normalization (Euclidean norm = 1.0)
715    #[new]
716    #[pyo3(signature = (
717        n_mels: "int",
718        f_min: "float",
719        f_max: "float",
720        norm: "MelNorm" = None
721    ), text_signature = "(n_mels: int, f_min: float, f_max: float, norm: MelNorm | str | None = None)")]
722    fn new(
723        n_mels: NonZeroUsize,
724        f_min: f64,
725        f_max: f64,
726        norm: Option<&pyo3::Bound<'_, pyo3::PyAny>>,
727    ) -> PyResult<Self> {
728        let norm_val = if let Some(norm_arg) = norm {
729            if norm_arg.is_none() {
730                MelNorm::None
731            } else if let Ok(s) = norm_arg.extract::<String>() {
732                match s.to_lowercase().as_str() {
733                    "none" => MelNorm::None,
734                    "slaney" => MelNorm::Slaney,
735                    "l1" => MelNorm::L1,
736                    "l2" => MelNorm::L2,
737                    _ => {
738                        return Err(pyo3::exceptions::PyValueError::new_err(format!(
739                            "Invalid norm string: '{s}'. Must be one of: 'none', 'slaney', 'l1', 'l2'"
740                        )));
741                    }
742                }
743            } else if let Ok(py_norm) = norm_arg.extract::<PyMelNorm>() {
744                py_norm.into()
745            } else {
746                return Err(pyo3::exceptions::PyTypeError::new_err(
747                    "norm must be a MelNorm enum, a string, or None",
748                ));
749            }
750        } else {
751            MelNorm::None
752        };
753
754        let inner = MelParams::with_norm(n_mels, f_min, f_max, norm_val)?;
755        Ok(Self { inner })
756    }
757
758    /// Number of mel bands.
759    #[getter]
760    const fn n_mels(&self) -> NonZeroUsize {
761        self.inner.n_mels()
762    }
763
764    /// Minimum frequency in Hz.
765    #[getter]
766    const fn f_min(&self) -> f64 {
767        self.inner.f_min()
768    }
769
770    /// Maximum frequency in Hz.
771    #[getter]
772    const fn f_max(&self) -> f64 {
773        self.inner.f_max()
774    }
775
776    /// Filterbank normalization strategy.
777    #[getter]
778    fn norm(&self) -> PyMelNorm {
779        self.inner.norm().into()
780    }
781
782    fn __repr__(&self) -> String {
783        let norm_str = match self.inner.norm() {
784            MelNorm::None => "None",
785            MelNorm::Slaney => "slaney",
786            MelNorm::L1 => "l1",
787            MelNorm::L2 => "l2",
788        };
789        format!(
790            "MelParams(n_mels={}, f_min={}, f_max={}, norm='{}')",
791            self.n_mels(),
792            self.f_min(),
793            self.f_max(),
794            norm_str
795        )
796    }
797}
798
799impl From<PyMelParams> for MelParams {
800    #[inline]
801    fn from(val: PyMelParams) -> Self {
802        val.inner
803    }
804}
805
806impl From<MelParams> for PyMelParams {
807    #[inline]
808    fn from(inner: MelParams) -> Self {
809        Self { inner }
810    }
811}
812
813/// ERB-scale (Equivalent Rectangular Bandwidth) filterbank parameters.
814#[pyclass(name = "ErbParams", from_py_object)]
815#[derive(Clone, Copy, Debug)]
816pub struct PyErbParams {
817    pub(crate) inner: ErbParams,
818}
819
820impl PyErbParams {
821    #[inline]
822    #[must_use]
823    pub const fn into_inner(self) -> ErbParams {
824        self.inner
825    }
826    #[inline]
827    #[must_use]
828    pub const fn as_inner(&self) -> &ErbParams {
829        &self.inner
830    }
831}
832
833#[pymethods]
834impl PyErbParams {
835    /// ERB-scale filterbank parameters.
836    ///
837    /// Parameters
838    /// ----------
839    /// `n_filters` : int
840    ///     Number of ERB filters
841    /// `f_min` : float
842    ///     Minimum frequency in Hz
843    /// `f_max` : float
844    ///     Maximum frequency in Hz
845    #[new]
846    #[pyo3(signature = (
847        n_filters: "int",
848        f_min: "float",
849        f_max: "float"
850    ), text_signature = "(n_filters: int, f_min: float, f_max: float)")]
851    fn new(n_filters: NonZeroUsize, f_min: f64, f_max: f64) -> PyResult<Self> {
852        let inner = ErbParams::new(n_filters, f_min, f_max)?;
853        Ok(Self { inner })
854    }
855
856    /// Number of ERB filters.
857    #[getter]
858    const fn n_filters(&self) -> NonZeroUsize {
859        self.inner.n_filters()
860    }
861
862    /// Minimum frequency in Hz.
863    #[getter]
864    const fn f_min(&self) -> f64 {
865        self.inner.f_min()
866    }
867
868    /// Maximum frequency in Hz.
869    #[getter]
870    const fn f_max(&self) -> f64 {
871        self.inner.f_max()
872    }
873
874    fn __repr__(&self) -> String {
875        format!(
876            "ErbParams(n_filters={}, f_min={}, f_max={})",
877            self.n_filters(),
878            self.f_min(),
879            self.f_max()
880        )
881    }
882}
883
884impl From<ErbParams> for PyErbParams {
885    #[inline]
886    fn from(inner: ErbParams) -> Self {
887        Self { inner }
888    }
889}
890
891/// Logarithmic frequency scale parameters.
892#[pyclass(name = "LogHzParams", from_py_object)]
893#[derive(Clone, Copy, Debug)]
894pub struct PyLogHzParams {
895    pub(crate) inner: LogHzParams,
896}
897
898#[pymethods]
899impl PyLogHzParams {
900    /// Logarithmic frequency scale parameters.
901    ///
902    /// Parameters
903    /// ----------
904    /// `n_bins` : int
905    ///     Number of logarithmically-spaced frequency bins
906    /// `f_min` : float
907    ///     Minimum frequency in Hz
908    /// `f_max` : float
909    ///     Maximum frequency in Hz
910    #[new]
911    #[pyo3(signature = (
912        n_bins: "int",
913        f_min: "float",
914        f_max: "float"
915    ), text_signature = "(n_bins: int, f_min: float, f_max: float)")]
916    fn new(n_bins: NonZeroUsize, f_min: f64, f_max: f64) -> PyResult<Self> {
917        let inner = LogHzParams::new(n_bins, f_min, f_max)?;
918        Ok(Self { inner })
919    }
920
921    /// Number of frequency bins.
922    #[getter]
923    const fn n_bins(&self) -> NonZeroUsize {
924        self.inner.n_bins()
925    }
926
927    /// Minimum frequency in Hz.
928    #[getter]
929    const fn f_min(&self) -> f64 {
930        self.inner.f_min()
931    }
932
933    /// Maximum frequency in Hz.
934    #[getter]
935    const fn f_max(&self) -> f64 {
936        self.inner.f_max()
937    }
938
939    fn __repr__(&self) -> String {
940        format!(
941            "LogHzParams(n_bins={}, f_min={}, f_max={})",
942            self.n_bins(),
943            self.f_min(),
944            self.f_max()
945        )
946    }
947}
948
949/// Constant-Q Transform parameters.
950#[pyclass(name = "CqtParams", from_py_object)]
951#[derive(Clone, Debug)]
952pub struct PyCqtParams {
953    pub(crate) inner: CqtParams,
954}
955
956#[pymethods]
957impl PyCqtParams {
958    /// Constant-Q Transform parameters.
959    ///
960    /// Parameters
961    /// ----------
962    /// `bins_per_octave` : int
963    ///     Number of bins per octave (e.g., 12 for semitones)
964    /// `n_octaves` : int
965    ///     Number of octaves to span
966    /// `f_min` : float
967    ///     Minimum frequency in Hz
968    #[new]
969    #[pyo3(signature = (
970        bins_per_octave: "int",
971        n_octaves: "int",
972        f_min: "float"
973    ), text_signature = "(bins_per_octave: int, n_octaves: int, f_min: float)")]
974    fn new(bins_per_octave: NonZeroUsize, n_octaves: NonZeroUsize, f_min: f64) -> PyResult<Self> {
975        let inner = CqtParams::new(bins_per_octave, n_octaves, f_min)?;
976        Ok(Self { inner })
977    }
978
979    /// Total number of CQT bins.
980    #[getter]
981    const fn num_bins(&self) -> NonZeroUsize {
982        self.inner.num_bins()
983    }
984
985    fn __repr__(&self) -> String {
986        format!("CqtParams(num_bins={})", self.num_bins())
987    }
988}
989
990impl From<CqtParams> for PyCqtParams {
991    #[inline]
992    fn from(inner: CqtParams) -> Self {
993        Self { inner }
994    }
995}
996
997impl From<PyCqtParams> for CqtParams {
998    #[inline]
999    fn from(val: PyCqtParams) -> Self {
1000        val.inner
1001    }
1002}
1003
1004#[pyclass(name = "ChromaNorm", from_py_object)]
1005#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
1006pub struct PyChromaNorm {
1007    pub(crate) inner: ChromaNorm,
1008}
1009
1010#[pymethods]
1011impl PyChromaNorm {
1012    /// No normalization.
1013    #[classattr]
1014    const fn none() -> Self {
1015        Self {
1016            inner: ChromaNorm::None,
1017        }
1018    }
1019
1020    /// L1 normalization (sum to 1).
1021    #[classattr]
1022    const fn l1() -> Self {
1023        Self {
1024            inner: ChromaNorm::L1,
1025        }
1026    }
1027
1028    /// L2 normalization (Euclidean norm to 1).
1029    #[classattr]
1030    const fn l2() -> Self {
1031        Self {
1032            inner: ChromaNorm::L2,
1033        }
1034    }
1035
1036    /// Max normalization (max value to 1).
1037    #[classattr]
1038    const fn max() -> Self {
1039        Self {
1040            inner: ChromaNorm::Max,
1041        }
1042    }
1043
1044    fn __repr__(&self) -> String {
1045        format!("{:?}", self.inner)
1046    }
1047}
1048
1049impl From<ChromaNorm> for PyChromaNorm {
1050    #[inline]
1051    fn from(inner: ChromaNorm) -> Self {
1052        Self { inner }
1053    }
1054}
1055
1056impl From<PyChromaNorm> for ChromaNorm {
1057    #[inline]
1058    fn from(val: PyChromaNorm) -> Self {
1059        val.inner
1060    }
1061}
1062
1063/// Chromagram (pitch class profile) parameters.
1064#[pyclass(name = "ChromaParams", from_py_object)]
1065#[derive(Clone, Copy, Debug)]
1066pub struct PyChromaParams {
1067    pub(crate) inner: ChromaParams,
1068}
1069
1070#[pymethods]
1071impl PyChromaParams {
1072    /// Create new chroma parameters.
1073    ///
1074    /// Parameters
1075    /// ----------
1076    /// tuning : float, default=440.0
1077    ///     Reference tuning frequency in Hz (A4)
1078    /// `f_min` : float, default=32.7
1079    ///     Minimum frequency in Hz (C1)
1080    /// `f_max` : float, default=4186.0
1081    ///     Maximum frequency in Hz (C8)
1082    /// norm : `ChromaNorm`, optional
1083    ///     Normalization method: l1, l2, max, or None (default: l2)
1084    #[new]
1085    #[pyo3(signature = (
1086        tuning: "float" = 440.0,
1087        f_min: "float" = 32.7,
1088        f_max: "float" = 4186.0,
1089        norm: "ChromaNorm" = None
1090    ), text_signature = "(tuning: float = 440.0, f_min: float = 32.7, f_max: float = 4186.0, norm: ChromaNorm = ChromaNorm.None)")]
1091    fn new(tuning: f64, f_min: f64, f_max: f64, norm: Option<PyChromaNorm>) -> PyResult<Self> {
1092        let norm = norm.unwrap_or_default();
1093        let inner = ChromaParams::new(tuning, f_min, f_max, norm.inner)?;
1094        Ok(Self { inner })
1095    }
1096
1097    /// Create standard chroma parameters for music analysis.
1098    #[classmethod]
1099    const fn music_standard(_cls: &Bound<'_, PyType>) -> Self {
1100        let inner = ChromaParams::music_standard();
1101        Self { inner }
1102    }
1103
1104    /// Tuning frequency in Hz (typically 440.0 for A4).
1105    #[getter]
1106    const fn tuning(&self) -> f64 {
1107        self.inner.tuning()
1108    }
1109
1110    /// Minimum frequency in Hz.
1111    #[getter]
1112    const fn f_min(&self) -> f64 {
1113        self.inner.f_min()
1114    }
1115
1116    /// Maximum frequency in Hz.
1117    #[getter]
1118    const fn f_max(&self) -> f64 {
1119        self.inner.f_max()
1120    }
1121
1122    fn __repr__(&self) -> String {
1123        format!(
1124            "ChromaParams(tuning={}, f_min={}, f_max={}, norm={:?})",
1125            self.tuning(),
1126            self.f_min(),
1127            self.f_max(),
1128            self.inner
1129        )
1130    }
1131}
1132
1133impl From<ChromaParams> for PyChromaParams {
1134    #[inline]
1135    fn from(inner: ChromaParams) -> Self {
1136        Self { inner }
1137    }
1138}
1139
1140impl From<PyChromaParams> for ChromaParams {
1141    #[inline]
1142    fn from(val: PyChromaParams) -> Self {
1143        val.inner
1144    }
1145}
1146/// MFCC (Mel-Frequency Cepstral Coefficients) parameters.
1147#[pyclass(name = "MfccParams", from_py_object)]
1148#[derive(Clone, Copy, Debug)]
1149pub struct PyMfccParams {
1150    pub(crate) inner: MfccParams,
1151}
1152
1153#[pymethods]
1154impl PyMfccParams {
1155    /// Create new MFCC parameters.
1156    ///
1157    /// Parameters
1158    /// ----------
1159    /// `n_mfcc` : int, default=13
1160    ///     Number of MFCC coefficients to compute
1161    #[new]
1162    #[pyo3(signature = (n_mfcc: "int" = 13), text_signature = "(n_mfcc: int = 13)")]
1163    fn new(n_mfcc: usize) -> PyResult<Self> {
1164        let n_mfcc = NonZeroUsize::new(n_mfcc).ok_or_else(|| {
1165            pyo3::exceptions::PyValueError::new_err("n_mfcc must be a positive integer")
1166        })?;
1167        let inner = MfccParams::new(n_mfcc);
1168        Ok(Self { inner })
1169    }
1170
1171    /// Standard MFCC parameters for speech recognition (13 coefficients).
1172    #[classmethod]
1173    const fn speech_standard(_cls: &Bound<'_, PyType>) -> Self {
1174        let inner = MfccParams::speech_standard();
1175        Self { inner }
1176    }
1177
1178    /// Number of MFCC coefficients.
1179    #[getter]
1180    const fn n_mfcc(&self) -> NonZeroUsize {
1181        self.inner.n_mfcc()
1182    }
1183
1184    fn __repr__(&self) -> String {
1185        format!("MfccParams(n_mfcc={})", self.n_mfcc())
1186    }
1187}
1188
1189impl From<PyMfccParams> for MfccParams {
1190    #[inline]
1191    fn from(val: PyMfccParams) -> Self {
1192        val.inner
1193    }
1194}
1195
1196impl From<MfccParams> for PyMfccParams {
1197    #[inline]
1198    fn from(inner: MfccParams) -> Self {
1199        Self { inner }
1200    }
1201}
1202
1203/// Register all parameter classes with the Python module.
1204#[inline]
1205pub fn register(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
1206    m.add_class::<PyWindowType>()?;
1207    m.add_class::<PyStftResult>()?;
1208    m.add_class::<PyStftParams>()?;
1209    m.add_class::<PyLogParams>()?;
1210    m.add_class::<PySpectrogramParams>()?;
1211    m.add_class::<PyMelNorm>()?;
1212    m.add_class::<PyMelParams>()?;
1213    m.add_class::<PyErbParams>()?;
1214    m.add_class::<PyLogHzParams>()?;
1215    m.add_class::<PyCqtParams>()?;
1216    m.add_class::<PyChromaParams>()?;
1217    m.add_class::<PyMfccParams>()?;
1218    Ok(())
1219}