use ndarray::Array2;
use num_complex::Complex;
use numpy::PyArray2;
use pyo3::prelude::*;
use crate::{AmpScaleSpec, Spectrogram, SpectrogramParams};
use super::params::PySpectrogramParams;
pub(crate) enum PyArrayData {
F32(Py<PyArray2<f32>>),
F64(Py<PyArray2<f64>>),
}
impl PyArrayData {
pub(crate) const fn dtype(&self) -> &'static str {
match self {
Self::F32(_) => "float32",
Self::F64(_) => "float64",
}
}
pub(crate) fn bind<'py>(&self, py: Python<'py>) -> Bound<'py, PyAny> {
match self {
Self::F32(a) => a.bind(py).clone().into_any(),
Self::F64(a) => a.bind(py).clone().into_any(),
}
}
}
pub(crate) enum PyComplexArrayData {
C64(Py<PyArray2<Complex<f32>>>),
C128(Py<PyArray2<Complex<f64>>>),
}
impl PyComplexArrayData {
pub(crate) const fn dtype(&self) -> &'static str {
match self {
Self::C64(_) => "float32",
Self::C128(_) => "float64",
}
}
pub(crate) fn bind<'py>(&self, py: Python<'py>) -> Bound<'py, PyAny> {
match self {
Self::C64(a) => a.bind(py).clone().into_any(),
Self::C128(a) => a.bind(py).clone().into_any(),
}
}
}
fn validate_dlpack_args(
stream: Option<&Bound<'_, PyAny>>,
max_version: Option<(u32, u32)>,
dl_device: Option<(i32, i32)>,
copy: Option<bool>,
) -> PyResult<u64> {
use crate::python::dlpack::DLPACK_FLAG_BITMASK_IS_COPIED;
if stream.is_some() {
return Err(pyo3::exceptions::PyBufferError::new_err(
"stream must be None for CPU tensors",
));
}
if let Some((major, minor)) = max_version {
if major < 1 {
return Err(pyo3::exceptions::PyBufferError::new_err(format!(
"Unsupported DLPack version: {major}.{minor}"
)));
}
}
if let Some((dev_type, dev_id)) = dl_device {
if dev_type != 1 || dev_id != 0 {
return Err(pyo3::exceptions::PyBufferError::new_err(
"Only CPU device (1, 0) is supported",
));
}
}
let mut flags = 0u64;
if copy == Some(true) {
flags |= DLPACK_FLAG_BITMASK_IS_COPIED;
}
Ok(flags)
}
pub(crate) fn real_dlpack<'py>(
py: Python<'py>,
data: &PyArrayData,
stream: Option<&Bound<'py, PyAny>>,
max_version: Option<(u32, u32)>,
dl_device: Option<(i32, i32)>,
copy: Option<bool>,
) -> PyResult<Bound<'py, pyo3::types::PyCapsule>> {
use crate::python::dlpack::{DLDataType, DLDataTypeCode, create_dlpack_capsule};
let flags = validate_dlpack_args(stream, max_version, dl_device, copy)?;
match data {
PyArrayData::F32(a) => create_dlpack_capsule(
py,
&a.bind(py).clone(),
DLDataType {
code: DLDataTypeCode::kDLFloat as u8,
bits: 32,
lanes: 1,
},
flags,
),
PyArrayData::F64(a) => create_dlpack_capsule(
py,
&a.bind(py).clone(),
DLDataType {
code: DLDataTypeCode::kDLFloat as u8,
bits: 64,
lanes: 1,
},
flags,
),
}
}
pub(crate) fn complex_dlpack<'py>(
py: Python<'py>,
data: &PyComplexArrayData,
stream: Option<&Bound<'py, PyAny>>,
max_version: Option<(u32, u32)>,
dl_device: Option<(i32, i32)>,
copy: Option<bool>,
) -> PyResult<Bound<'py, pyo3::types::PyCapsule>> {
use crate::python::dlpack::{DLDataType, DLDataTypeCode, create_dlpack_capsule};
let flags = validate_dlpack_args(stream, max_version, dl_device, copy)?;
match data {
PyComplexArrayData::C64(a) => create_dlpack_capsule(
py,
&a.bind(py).clone(),
DLDataType {
code: DLDataTypeCode::kDLComplex as u8,
bits: 64,
lanes: 1,
},
flags,
),
PyComplexArrayData::C128(a) => create_dlpack_capsule(
py,
&a.bind(py).clone(),
DLDataType {
code: DLDataTypeCode::kDLComplex as u8,
bits: 128,
lanes: 1,
},
flags,
),
}
}
pub(crate) trait PyScalar: crate::Sample + numpy::Element {
const DTYPE: &'static str;
const COMPLEX_DTYPE: &'static str;
fn into_array_data(py: Python<'_>, a: Array2<Self>) -> PyArrayData;
fn into_complex_array_data(py: Python<'_>, a: Array2<Complex<Self>>) -> PyComplexArrayData;
}
impl PyScalar for f32 {
const DTYPE: &'static str = "float32";
const COMPLEX_DTYPE: &'static str = "complex64";
fn into_array_data(py: Python<'_>, a: Array2<Self>) -> PyArrayData {
PyArrayData::F32(PyArray2::from_owned_array(py, a).unbind())
}
fn into_complex_array_data(py: Python<'_>, a: Array2<Complex<Self>>) -> PyComplexArrayData {
PyComplexArrayData::C64(PyArray2::from_owned_array(py, a).unbind())
}
}
impl PyScalar for f64 {
const DTYPE: &'static str = "float64";
const COMPLEX_DTYPE: &'static str = "complex128";
fn into_array_data(py: Python<'_>, a: Array2<Self>) -> PyArrayData {
PyArrayData::F64(PyArray2::from_owned_array(py, a).unbind())
}
fn into_complex_array_data(py: Python<'_>, a: Array2<Complex<Self>>) -> PyComplexArrayData {
PyComplexArrayData::C128(PyArray2::from_owned_array(py, a).unbind())
}
}
#[pyclass(name = "Spectrogram", skip_from_py_object)]
pub struct PySpectrogram {
py_data: PyArrayData,
frequencies: Vec<f64>,
times: Vec<f64>,
params: SpectrogramParams,
db_range: Option<(f64, f64)>,
}
impl PySpectrogram {
pub(crate) fn from_spectrogram<FreqScale, AmpScale, T>(
py: Python<'_>,
spec: Spectrogram<FreqScale, AmpScale, T>,
) -> Self
where
FreqScale: Copy + Clone + 'static,
AmpScale: AmpScaleSpec + 'static,
T: PyScalar,
{
let frequencies = spec.frequencies().to_vec();
let times = spec.times().to_vec();
let params = spec.params().clone();
let db_range = spec.db_range();
let array = spec.into_data();
let py_data = T::into_array_data(py, array);
Self {
py_data,
frequencies,
times,
params,
db_range,
}
}
}
#[pymethods]
impl PySpectrogram {
#[getter]
fn data<'py>(&self, py: Python<'py>) -> Bound<'py, PyAny> {
self.py_data.bind(py)
}
#[getter]
const fn dtype(&self) -> &'static str {
self.py_data.dtype()
}
#[getter]
fn frequencies(&self) -> &[f64] {
&self.frequencies
}
#[getter]
fn times(&self) -> &[f64] {
&self.times
}
#[getter]
const fn n_bins(&self) -> usize {
self.frequencies.len()
}
#[getter]
const fn n_frames(&self) -> usize {
self.times.len()
}
#[getter]
const fn shape(&self) -> (usize, usize) {
(self.n_bins(), self.n_frames())
}
fn frequency_range(&self) -> (f64, f64) {
if self.frequencies.is_empty() {
(0.0, 0.0)
} else {
(
self.frequencies[0],
self.frequencies[self.frequencies.len() - 1],
)
}
}
fn duration(&self) -> f64 {
if self.times.is_empty() {
0.0
} else {
self.times[self.times.len() - 1]
}
}
const fn db_range(&self) -> Option<(f64, f64)> {
self.db_range
}
#[getter]
fn params(&self) -> PySpectrogramParams {
self.params.clone().into()
}
fn __repr__(&self) -> String {
format!(
"Spectrogram(shape=({}, {}), dtype={})",
self.n_bins(),
self.n_frames(),
self.dtype(),
)
}
fn __str__(&self) -> String {
self.__repr__()
}
const fn __len__(&self) -> usize {
self.n_frames()
}
#[getter]
#[allow(non_snake_case)]
fn T<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
self.py_data.bind(py).getattr("T")
}
#[pyo3(signature = (dtype), text_signature = "($self, dtype)")]
fn astype<'py>(
&self,
py: Python<'py>,
dtype: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PyAny>> {
self.py_data.bind(py).call_method1("astype", (dtype,))
}
#[pyo3(signature = (dtype=None), text_signature = "($self, dtype=None)")]
fn __array__<'py>(
&self,
py: Python<'py>,
dtype: Option<&Bound<'py, PyAny>>,
) -> PyResult<Bound<'py, PyAny>> {
let arr = self.py_data.bind(py);
if let Some(dt) = dtype {
arr.call_method1("astype", (dt,))
} else {
Ok(arr)
}
}
fn __getitem__<'py>(&self, py: Python<'py>, idx: &Bound<'py, PyAny>) -> PyResult<Py<PyAny>> {
let arr = self.py_data.bind(py);
let sliced: Bound<'py, PyAny> = arr.get_item(idx)?;
Ok(sliced.unbind())
}
#[staticmethod]
const fn __dlpack_device__() -> (i32, i32) {
(1, 0) }
#[pyo3(signature = (*, stream=None, max_version=None, dl_device=None, copy=None))]
fn __dlpack__<'py>(
&self,
py: Python<'py>,
stream: Option<&Bound<'py, PyAny>>,
max_version: Option<(u32, u32)>,
dl_device: Option<(i32, i32)>,
copy: Option<bool>,
) -> PyResult<Bound<'py, pyo3::types::PyCapsule>> {
real_dlpack(py, &self.py_data, stream, max_version, dl_device, copy)
}
}
pub fn register(_py: Python, parent: &Bound<'_, PyModule>) -> PyResult<()> {
parent.add_class::<PySpectrogram>()?;
Ok(())
}