use pitch_detection::detector::yin::YINDetector; use pitch_detection::detector::PitchDetector; use crate::buffers::Buffers; use ringbuf::{Consumer, Producer, RingBuffer}; pub fn generate_pitch_detector(size: usize) -> impl PitchDetector { let padding = size / 2; YINDetector::new(size, padding) } /// Returns an option with (Frequency, Clarity) pub fn pitch_detect( detector: &mut dyn PitchDetector, signal: &[f32], sample_rate: u32, ) -> Option<(f32, f32)> { const POWER_THRESHOLD: f32 = 0.15; const CLARITY_THRESHOLD: f32 = 0.5; let pitch = detector.get_pitch( &signal, sample_rate as usize, POWER_THRESHOLD, CLARITY_THRESHOLD, ); pitch.map(|a| (a.frequency, a.clarity)) } pub struct DetectionInput { pub buffers: Buffers, pub sample_rate: u32, } pub struct DetectionOutput { pub buffers: Buffers, pub pitch_l: Option, pub pitch_r: Option, } pub fn detect( mut inputs: Consumer>, mut outputs: Producer>, ) { let mut detector_l = generate_pitch_detector(LEN); let mut detector_r = generate_pitch_detector(LEN); loop { if let Some(DetectionInput:: { buffers, sample_rate, }) = inputs.pop() { let pitch_l = pitch_detect(&mut detector_l, &buffers.l, sample_rate).map(|a| a.0); let pitch_r = pitch_detect(&mut detector_r, &buffers.r, sample_rate).map(|a| a.0); let _ = outputs.push(DetectionOutput { buffers, pitch_l, pitch_r, }); } } } pub struct PitchDetectorThread { /// Current recording buffer /// Input goes here recording_buffer: Buffers, /// Ringbuf producer so we can send audio chunks to the processing thread recordings: Producer>, /// Ringbuf consumer so we can receive processed buffers from the processing threads processed: Consumer>, /// Contains some empty buffers so we can reuse them instead of doing allocations /// Buffers here are not actually empty, since we don't spend any time clearing them /// But since they will be overwritten, this isn't an issue empty_buffers: Vec>, } impl PitchDetectorThread { pub fn new() -> Self { let (recordings, recording_rx) = RingBuffer::>::new(30).split(); let (processed_tx, processed) = RingBuffer::>::new(30).split(); // Spawn analysis thread std::thread::spawn(move || { detect(recording_rx, processed_tx); }); // keep some empty buffer around so we can swap them let mut empty_buffers = Vec::with_capacity(80); empty_buffers.append(&mut vec![Buffers::new(); 30]); Self { recordings, processed, empty_buffers, recording_buffer: Buffers::new(), } } pub fn write(&mut self, l: f32, r: f32, sample_rate: u32) { let full = self.recording_buffer.write_advance(l, r); // If we fill the buffer, switch it with an empty one if full { // we have to loop here, cause when the daw renders audio it tries to do it faster than // real time. if we don't loop and wait, the processing thread gets stuck with all of the buffers, // and we run out of empty ones to switch to // the loop-wait ensures that we don't panic when there isn't an empty buffer loop { // get the empty buffer from unused buffer list if let Some(mut buf) = self.empty_buffers.pop() { buf.reset(); // swap it with recording buffer std::mem::swap(&mut buf, &mut self.recording_buffer); buf.reset(); // pass it to the processor thread let _ = self.recordings.push(DetectionInput:: { buffers: buf, sample_rate, }); break; } std::thread::sleep(std::time::Duration::from_micros(10)); } } } pub fn try_get_pitch(&mut self) -> Option<(Option, Option)> { let DetectionOutput:: { buffers, pitch_l, pitch_r, } = self.processed.pop()?; self.empty_buffers.push(buffers); Some((pitch_l, pitch_r)) } }