lib/
audio.rs

1use crate::tuner::{identify_frequency, Resonators};
2use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
3use std::sync::mpsc::Sender;
4use std::{
5    sync::{Arc, Mutex},
6    thread,
7};
8
9/// A fixed-size circular buffer
10/// - `buffer`: The underlying storage for the circular buffer.
11/// - `max_size`: The maximum capacity of the buffer.
12/// - `write_index`: The position in the buffer where the next chunk will be written.
13pub struct CircularBuffer {
14    buffer: Vec<f32>,
15    max_size: usize,
16    write_index: usize,
17}
18
19impl CircularBuffer {
20    /// Creates a new circular buffer with a specified maximum size.
21    ///
22    /// # Arguments
23    /// * `max_size` - The capacity of the buffer (number of elements it can hold).
24    ///
25    /// # Returns
26    /// A `CircularBuffer` instance initialized with zeros.
27    pub fn new(max_size: usize) -> Self {
28        Self {
29            buffer: vec![0.0; max_size],
30            max_size,
31            write_index: 0,
32        }
33    }
34    /// Adds a chunk of data to the circular buffer.
35    ///
36    /// If the chunk is larger than the buffer size, only the last `max_size` elements are stored.
37    /// If the chunk wraps around the end of the buffer, it is split and written in two parts.
38    ///
39    /// # Arguments
40    /// * `chunk` - A slice of `f32` values to add to the buffer.
41    pub fn add_chunk(&mut self, chunk: &[f32]) {
42        if chunk.len() < self.max_size {
43            // Case 1: Chunk can fit into the buffer without overflow
44            let remaining = self.max_size - self.write_index;
45
46            if remaining >= chunk.len() {
47                self.buffer[self.write_index..(self.write_index + chunk.len())]
48                    .copy_from_slice(chunk);
49                self.write_index += chunk.len();
50            } else {
51                // The chunk wraps around the buffer
52                // 1. Copy the first part to the end of the buffer
53                self.buffer[self.write_index..].copy_from_slice(&chunk[..remaining]);
54
55                // 2. Copy the remainder to the beginning of the buffer
56                let wrapped_length = chunk.len() - remaining;
57                self.buffer[..wrapped_length].copy_from_slice(&chunk[remaining..]);
58
59                // Update the write index to the new position
60                self.write_index = wrapped_length;
61            }
62        } else {
63            // Case 2: Chunk is larger than the buffer size
64            // Only the last `max_size` elements are retained
65            let start = chunk.len() - self.max_size;
66            self.write_index = 0;
67            self.buffer.clone_from_slice(&chunk[start..]);
68        }
69    }
70
71    /// Copies the current state of the circular buffer into an output slice.
72    ///
73    /// The data is copied in order, starting from the oldest data to the newest.
74    ///
75    /// # Arguments
76    /// * `output` - A mutable slice where the buffer contents will be copied.
77    ///
78    /// # Panics
79    /// Panics if `output` is not the same length as the circular buffer (`max_size`).
80    pub fn copy_to_buffer(&self, output: &mut [f32]) {
81        assert_eq!(output.len(), self.max_size, "Output slice size mismatch");
82
83        let start_len = self.max_size - self.write_index;
84        output[..start_len].copy_from_slice(&self.buffer[self.write_index..]);
85        output[start_len..].copy_from_slice(&self.buffer[..self.write_index]);
86    }
87
88    /// Returns a copy of the current state of the buffer as a vector.
89    ///
90    /// The data is ordered, starting from the oldest to the newest values.
91    ///
92    /// # Returns
93    /// A `Vec<f32>` containing the buffer contents.
94    pub fn get_buffer(&self) -> Vec<f32> {
95        let mut result = Vec::with_capacity(self.max_size);
96        result.extend_from_slice(&self.buffer[self.write_index..]);
97        result.extend_from_slice(&self.buffer[..self.write_index]);
98        result
99    }
100}
101
102/// Start the audio processing loop
103pub fn start_audio_processing(device_name: String, pitch_tx: Sender<Option<f32>>) {
104    thread::spawn(move || {
105        let host = cpal::default_host();
106        let device = host
107            .input_devices()
108            .unwrap()
109            .find(|d| d.name().unwrap_or_default() == device_name)
110            .expect("Device not found");
111
112        let config = device.default_input_config().unwrap();
113        let sample_rate = config.sample_rate().0;
114        let channels = config.channels() as usize;
115
116        let max_window = sample_rate / 10; // 2 seconds
117
118        let candidate_frequencies: Vec<f32> = (1..=24)
119            .into_iter()
120            .map(|n| 36.70810 * 2.0f32.powf(n as f32 / 12.0))
121            .collect();
122
123        let resonators = Arc::new(Mutex::new(Resonators::new(
124            &candidate_frequencies,
125            sample_rate as i32,
126            10.0,
127            sample_rate as usize / 20,
128        )));
129
130        let circular_buffer = Arc::new(Mutex::new(CircularBuffer::new(max_window as usize))); // Share buffer between threads
131        let process_buffer = Arc::clone(&circular_buffer);
132        let resonators_read = Arc::clone(&resonators);
133
134        // Thread to process pitch detection
135        thread::spawn(move || {
136            let mut work_buffer = vec![0.0; max_window as usize];
137            loop {
138                // Safely access and retrieve a linearized copy of the buffer
139                let f_candidate = {
140                    let buf = process_buffer.lock().unwrap();
141                    buf.copy_to_buffer(&mut work_buffer);
142                    let r = resonators_read.lock().unwrap();
143                    let (f_candidate, _) = r.current_peak();
144                    f_candidate
145                };
146
147                let mut pitch = identify_frequency(
148                    &work_buffer,
149                    sample_rate as f32,
150                    f_candidate - 5.0,
151                    f_candidate + 5.0,
152                    true,
153                );
154
155                // Sometimes the 2nd harmonic is more energetic than the fundamental frequency
156                // but it's not enough to trigger detection via autocorrelation.  So we will
157                // also check the half-frequency to see if it's a match
158                if pitch.is_none() {
159                    pitch = identify_frequency(
160                        &work_buffer,
161                        sample_rate as f32,
162                        0.5 * f_candidate - 5.0,
163                        0.5 * f_candidate + 5.0,
164                        true,
165                    );
166                }
167
168                pitch_tx.send(pitch).ok();
169                thread::sleep(std::time::Duration::from_millis(150)); // Control processing frequency
170            }
171        });
172
173        let input_buffer = Arc::clone(&circular_buffer);
174        let stream = device
175            .build_input_stream(
176                &config.into(),
177                move |data: &[f32], _: &_| {
178                    let mono_chunk: Vec<f32> = data
179                        .chunks(channels)
180                        .map(|frame| frame.iter().sum::<f32>() / channels as f32)
181                        .collect();
182                    let mut buf = input_buffer.lock().unwrap();
183                    buf.add_chunk(&mono_chunk);
184                    let mut r = resonators.lock().unwrap();
185                    r.process_new_samples(&mono_chunk);
186                },
187                |err| eprintln!("Stream error: {}", err),
188                None,
189            )
190            .unwrap();
191
192        stream.play().unwrap();
193    });
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_new() {
202        let buffer = CircularBuffer::new(5);
203        assert_eq!(buffer.get_buffer(), vec![0.0; 5]);
204    }
205
206    #[test]
207    fn test_add_chunk_fit_exactly() {
208        let mut buffer = CircularBuffer::new(5);
209        buffer.add_chunk(&[1.0, 2.0, 3.0, 4.0, 5.0]);
210        assert_eq!(buffer.get_buffer(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
211    }
212
213    #[test]
214    fn test_add_chunk_smaller_than_buffer() {
215        let mut buffer = CircularBuffer::new(5);
216        buffer.add_chunk(&[1.0, 2.0]);
217        assert_eq!(buffer.get_buffer(), vec![0.0, 0.0, 0.0, 1.0, 2.0]);
218    }
219
220    #[test]
221    fn test_add_chunk_wrap_around() {
222        let mut buffer = CircularBuffer::new(5);
223        buffer.add_chunk(&[1.0, 2.0, 3.0]);
224        buffer.add_chunk(&[4.0, 5.0, 6.0]);
225        // Expected: Last 5 values, wrapped around
226        assert_eq!(buffer.get_buffer(), vec![2.0, 3.0, 4.0, 5.0, 6.0]);
227    }
228
229    #[test]
230    fn test_add_chunk_larger_than_buffer() {
231        let mut buffer = CircularBuffer::new(5);
232        buffer.add_chunk(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
233        // Expected: Only the last 5 values are retained
234        assert_eq!(buffer.get_buffer(), vec![3.0, 4.0, 5.0, 6.0, 7.0]);
235    }
236
237    #[test]
238    fn test_copy_to_buffer_exact_size() {
239        let mut buffer = CircularBuffer::new(5);
240        buffer.add_chunk(&[1.0, 2.0, 3.0, 4.0, 5.0]);
241
242        let mut output = vec![0.0; 5];
243        buffer.copy_to_buffer(&mut output);
244        assert_eq!(output, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
245    }
246
247    #[test]
248    fn test_copy_to_buffer_wrap_around() {
249        let mut buffer = CircularBuffer::new(5);
250        buffer.add_chunk(&[1.0, 2.0, 3.0]);
251        buffer.add_chunk(&[4.0, 5.0, 6.0]);
252
253        let mut output = vec![0.0; 5];
254        buffer.copy_to_buffer(&mut output);
255        assert_eq!(output, vec![2.0, 3.0, 4.0, 5.0, 6.0]);
256    }
257
258    #[test]
259    #[should_panic(expected = "Output slice size mismatch")]
260    fn test_copy_to_buffer_invalid_output_size() {
261        let mut buffer = CircularBuffer::new(5);
262        buffer.add_chunk(&[1.0, 2.0, 3.0]);
263
264        let mut output = vec![0.0; 4]; // Incorrect size
265        buffer.copy_to_buffer(&mut output);
266    }
267}