rosa 0.1.0

Audio feature extraction library matching librosa output
Documentation
/// Circular buffer with power-of-2 capacity for O(1) wrapping.
///
/// Used internally by processors to manage overlap between audio chunks.
/// The buffer maintains a write head. Samples behind the head are history.
/// Negative offsets read past samples: `get(-1)` returns the last written sample.
pub struct CircularBuffer {
    data: Vec<f64>,
    head: usize,
    mask: usize,
    written: usize,
}

impl CircularBuffer {
    /// Create with at least `min_capacity` samples of storage.
    /// Actual capacity is rounded up to next power of 2.
    pub fn new(min_capacity: usize) -> Self {
        let capacity = min_capacity.next_power_of_two();
        Self {
            data: vec![0.0; capacity],
            head: 0,
            mask: capacity - 1,
            written: 0,
        }
    }

    /// The actual capacity (always a power of 2).
    pub fn capacity(&self) -> usize {
        self.mask + 1
    }

    /// Write a chunk of samples. Advances the head.
    pub fn write(&mut self, samples: &[f64]) {
        for (i, &s) in samples.iter().enumerate() {
            self.data[(self.head + i) & self.mask] = s;
        }
        self.head = (self.head + samples.len()) & self.mask;
        self.written += samples.len();
    }

    /// Read one sample at offset relative to head.
    /// Negative = past: `get(-1)` is the last written sample.
    pub fn get(&self, offset: isize) -> f64 {
        self.data[(self.head as isize + offset) as usize & self.mask]
    }

    /// Copy `output.len()` samples starting at `offset` relative to head.
    /// Handles wrap-around transparently.
    pub fn read_into(&self, output: &mut [f64], offset: isize) {
        for (i, out) in output.iter_mut().enumerate() {
            *out = self.get(offset + i as isize);
        }
    }

    /// How many samples have been written total (monotonically increasing).
    pub fn total_written(&self) -> usize {
        self.written
    }

    /// Reset to empty state.
    pub fn clear(&mut self) {
        self.data.fill(0.0);
        self.head = 0;
        self.written = 0;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn capacity_is_power_of_two() {
        assert_eq!(CircularBuffer::new(1).capacity(), 1);
        assert_eq!(CircularBuffer::new(2).capacity(), 2);
        assert_eq!(CircularBuffer::new(3).capacity(), 4);
        assert_eq!(CircularBuffer::new(1000).capacity(), 1024);
        assert_eq!(CircularBuffer::new(2048).capacity(), 2048);
        assert_eq!(CircularBuffer::new(2049).capacity(), 4096);
    }

    #[test]
    fn write_and_read_back() {
        let mut buf = CircularBuffer::new(8);
        buf.write(&[1.0, 2.0, 3.0]);

        // head is now at 3, so offset -3 is the first written sample
        assert_eq!(buf.get(-3), 1.0);
        assert_eq!(buf.get(-2), 2.0);
        assert_eq!(buf.get(-1), 3.0);
    }

    #[test]
    fn wrap_around() {
        let mut buf = CircularBuffer::new(4); // capacity = 4
        // Write 6 samples into a 4-capacity buffer
        buf.write(&[1.0, 2.0, 3.0, 4.0]);
        buf.write(&[5.0, 6.0]);

        // Only the last 4 samples are in the buffer
        assert_eq!(buf.get(-1), 6.0);
        assert_eq!(buf.get(-2), 5.0);
        assert_eq!(buf.get(-3), 4.0);
        assert_eq!(buf.get(-4), 3.0);
    }

    #[test]
    fn read_into_basic() {
        let mut buf = CircularBuffer::new(8);
        buf.write(&[10.0, 20.0, 30.0, 40.0, 50.0]);

        let mut out = [0.0; 3];
        buf.read_into(&mut out, -3);
        assert_eq!(out, [30.0, 40.0, 50.0]);
    }

    #[test]
    fn read_into_across_wrap() {
        let mut buf = CircularBuffer::new(4); // capacity = 4
        // Fill and wrap around
        buf.write(&[1.0, 2.0, 3.0]);
        buf.write(&[4.0, 5.0]); // head wraps past index 3

        let mut out = [0.0; 4];
        buf.read_into(&mut out, -4);
        assert_eq!(out, [2.0, 3.0, 4.0, 5.0]);
    }

    #[test]
    fn total_written_monotonic() {
        let mut buf = CircularBuffer::new(4);
        assert_eq!(buf.total_written(), 0);

        buf.write(&[1.0, 2.0]);
        assert_eq!(buf.total_written(), 2);

        buf.write(&[3.0, 4.0, 5.0]);
        assert_eq!(buf.total_written(), 5);

        // Even after wrapping, total_written keeps increasing
        buf.write(&[6.0, 7.0, 8.0, 9.0, 10.0]);
        assert_eq!(buf.total_written(), 10);
    }

    #[test]
    fn clear_resets_state() {
        let mut buf = CircularBuffer::new(8);
        buf.write(&[1.0, 2.0, 3.0]);
        buf.clear();

        assert_eq!(buf.total_written(), 0);
        // After clear, all data is zero
        assert_eq!(buf.get(0), 0.0);
        assert_eq!(buf.get(-1), 0.0);
    }

    #[test]
    fn stft_scenario() {
        // Simulate STFT: n_fft=8, hop_length=4, chunk_size=16
        let n_fft = 8;
        let hop_length = 4;
        let mut buf = CircularBuffer::new(n_fft);

        // Generate test audio: 0.0, 1.0, 2.0, ...
        let chunk: Vec<f64> = (0..16).map(|i| i as f64).collect();

        // Write first chunk of hop_length samples to prime the buffer
        buf.write(&chunk[0..n_fft]);

        // Now extract frames
        let mut frames = Vec::new();
        let mut frame_buf = vec![0.0; n_fft];

        // First frame: samples 0..8
        buf.read_into(&mut frame_buf, -(n_fft as isize));
        frames.push(frame_buf.clone());
        assert_eq!(frames[0], [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);

        // Write next hop
        buf.write(&chunk[n_fft..n_fft + hop_length]);
        buf.read_into(&mut frame_buf, -(n_fft as isize));
        frames.push(frame_buf.clone());
        assert_eq!(frames[1], [4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0]);

        // Write another hop
        buf.write(&chunk[n_fft + hop_length..n_fft + 2 * hop_length]);
        buf.read_into(&mut frame_buf, -(n_fft as isize));
        frames.push(frame_buf.clone());
        assert_eq!(frames[2], [8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0]);
    }
}