dasp_rs/features/
manipulation.rs

1use ndarray::{Array1, Array2, Axis};
2
3/// Computes delta coefficients of arbitrary order from a 2D array.
4///
5/// # Arguments
6/// * `data` - Input 2D array (typically features × time)
7/// * `width` - Optional window width for delta computation (defaults to 9)
8/// * `order` - Optional order of delta (defaults to 1)
9/// * `axis` - Optional axis along which to compute deltas (-1 for time, 0 for features; defaults to -1)
10///
11/// # Returns
12/// Returns a 2D array of the same shape as `data` containing the delta coefficients.
13///
14/// # Examples
15/// ```
16/// use ndarray::Array2;
17/// let data = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
18/// let delta = delta(&data, None, None, None);
19/// ```
20pub fn delta(
21    data: &Array2<f32>,
22    width: Option<usize>,
23    order: Option<usize>,
24    axis: Option<isize>,
25) -> Array2<f32> {
26    let width = width.unwrap_or(9);
27    let order = order.unwrap_or(1);
28    let axis = axis.unwrap_or(-1);
29    let mut result = data.to_owned();
30    for _ in 0..order {
31        let mut delta = Array2::zeros(result.dim());
32        let half_width = width / 2;
33        let weights: Vec<f32> = (1..=half_width).map(|i| i as f32).collect();
34        let norm = weights.iter().map(|x| x.powi(2)).sum::<f32>();
35        for i in 0..result.shape()[axis.unsigned_abs()] {
36            let slice = result.index_axis(Axis(axis.unsigned_abs()), i);
37            for j in 0..slice.len() {
38                let mut sum = 0.0;
39                for w in 0..weights.len() {
40                    let left = (j as isize - w as isize - 1).max(0) as usize;
41                    let right = (j + w + 1).min(slice.len() - 1);
42                    sum += weights[w] * (slice[right] - slice[left]);
43                }
44                delta[[if axis < 0 { j } else { i }, if axis < 0 { i } else { j }]] = sum / norm;
45            }
46        }
47        result = delta;
48    }
49    result
50}
51
52/// Stacks delayed copies of a 2D array for temporal context.
53///
54/// # Arguments
55/// * `data` - Input 2D array (features × time)
56/// * `n_steps` - Optional number of delayed copies (defaults to 2)
57/// * `delay` - Optional delay between steps in frames (defaults to 1)
58///
59/// # Returns
60/// Returns a 2D array of shape `(n_features * n_steps, n_frames)` containing stacked features.
61///
62/// # Examples
63/// ```
64/// use ndarray::Array2;
65/// let data = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
66/// let stacked = stack_memory(&data, None, None);
67/// ```
68pub fn stack_memory(
69    data: &Array2<f32>,
70    n_steps: Option<usize>,
71    delay: Option<usize>,
72) -> Array2<f32> {
73    let n_steps = n_steps.unwrap_or(2);
74    let delay = delay.unwrap_or(1);
75    let n_frames = data.shape()[1];
76    let n_features = data.shape()[0];
77    let mut stacked = Array2::zeros((n_features * n_steps, n_frames));
78    for step in 0..n_steps {
79        let offset = step * delay;
80        for t in 0..n_frames {
81            let src_t = (t as isize - offset as isize).max(0) as usize;
82            for f in 0..n_features {
83                stacked[[f + step * n_features, t]] = data[[f, src_t]];
84            }
85        }
86    }
87    stacked
88}
89
90/// Computes temporal kurtosis from audio or spectrogram.
91///
92/// Kurtosis measures the "tailedness" of the distribution in each frame.
93///
94/// # Arguments
95/// * `y` - Optional audio time series
96/// * `S` - Optional spectrogram (features × time)
97/// * `frame_length` - Optional frame length for audio (defaults to 2048)
98/// * `hop_length` - Optional hop length for audio (defaults to frame_length/4)
99///
100/// # Returns
101/// Returns a 1D array containing kurtosis values for each frame.
102///
103/// # Panics
104/// Panics if neither `y` nor `S` is provided, or if both are provided.
105///
106/// # Examples
107/// ```
108/// let y = vec![0.1, 0.2, 0.3, 0.4, 0.5];
109/// let kurtosis = temporal_kurtosis(Some(&y), None, None, None);
110/// ```
111pub fn temporal_kurtosis(
112    y: Option<&[f32]>,
113    S: Option<&Array2<f32>>,
114    frame_length: Option<usize>,
115    hop_length: Option<usize>,
116) -> Array1<f32> {
117    let frame_len = frame_length.unwrap_or(2048);
118    let hop = hop_length.unwrap_or(frame_len / 4);
119    match (y, S) {
120        (Some(y), None) => {
121            let n_frames = (y.len() - frame_len) / hop + 1;
122            let mut kurtosis = Array1::zeros(n_frames);
123            for i in 0..n_frames {
124                let start = i * hop;
125                let frame = &y[start..(start + frame_len).min(y.len())];
126                let mean = frame.iter().sum::<f32>() / frame.len() as f32;
127                let m2 = frame.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / frame.len() as f32;
128                let m4 = frame.iter().map(|&x| (x - mean).powi(4)).sum::<f32>() / frame.len() as f32;
129                kurtosis[i] = if m2 > 1e-10 { m4 / m2.powi(2) - 3.0 } else { 0.0 };
130            }
131            kurtosis
132        }
133        (None, Some(S)) => S.axis_iter(Axis(1)).map(|frame| {
134            let mean = frame.mean().unwrap_or(0.0);
135            let m2 = frame.mapv(|x| (x - mean).powi(2)).mean().unwrap_or(0.0);
136            let m4 = frame.mapv(|x| (x - mean).powi(4)).mean().unwrap_or(0.0);
137            if m2 > 1e-10 { m4 / m2.powi(2) - 3.0 } else { 0.0 }
138        }).collect(),
139        _ => panic!("Must provide either y or S"),
140    }
141}
142
143/// Computes zero-crossing rate from an audio signal.
144///
145/// Measures the rate at which the signal changes sign in each frame.
146///
147/// # Arguments
148/// * `y` - Input audio time series
149/// * `frame_length` - Optional frame length (defaults to 2048)
150/// * `hop_length` - Optional hop length (defaults to frame_length/4)
151///
152/// # Returns
153/// Returns a 1D array containing zero-crossing rates for each frame.
154///
155/// # Examples
156/// ```
157/// let y = vec![1.0, -1.0, 2.0, -2.0, 1.0];
158/// let zcr = zero_crossing_rate(&y, None, None);
159/// ```
160pub fn zero_crossing_rate(
161    y: &[f32],
162    frame_length: Option<usize>,
163    hop_length: Option<usize>,
164) -> Array1<f32> {
165    let frame_len = frame_length.unwrap_or(2048);
166    let hop = hop_length.unwrap_or(frame_len / 4);
167    let n_frames = (y.len() - frame_len) / hop + 1;
168    let mut zcr = Array1::zeros(n_frames);
169    for i in 0..n_frames {
170        let start = i * hop;
171        let slice = &y[start..(start + frame_len).min(y.len())];
172        let count = slice.windows(2).filter(|w| w[0] * w[1] < 0.0).count();
173        zcr[i] = count as f32 / frame_len as f32;
174    }
175    zcr
176}