145 lines
4.6 KiB
Rust
145 lines
4.6 KiB
Rust
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<f32> {
|
|
let padding = size / 2;
|
|
|
|
YINDetector::new(size, padding)
|
|
}
|
|
|
|
/// Returns an option with (Frequency, Clarity)
|
|
pub fn pitch_detect(
|
|
detector: &mut dyn PitchDetector<f32>,
|
|
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<const LEN: usize> {
|
|
pub buffers: Buffers<LEN>,
|
|
pub sample_rate: u32,
|
|
}
|
|
|
|
pub struct DetectionOutput<const LEN: usize> {
|
|
pub buffers: Buffers<LEN>,
|
|
pub pitch_l: Option<f32>,
|
|
pub pitch_r: Option<f32>,
|
|
}
|
|
|
|
pub fn detect<const LEN: usize>(
|
|
mut inputs: Consumer<DetectionInput<LEN>>,
|
|
mut outputs: Producer<DetectionOutput<LEN>>,
|
|
) {
|
|
let mut detector_l = generate_pitch_detector(LEN);
|
|
let mut detector_r = generate_pitch_detector(LEN);
|
|
|
|
loop {
|
|
if let Some(DetectionInput::<LEN> {
|
|
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<const LEN: usize> {
|
|
/// Current recording buffer
|
|
/// Input goes here
|
|
recording_buffer: Buffers<LEN>,
|
|
|
|
/// Ringbuf producer so we can send audio chunks to the processing thread
|
|
recordings: Producer<DetectionInput<LEN>>,
|
|
/// Ringbuf consumer so we can receive processed buffers from the processing threads
|
|
processed: Consumer<DetectionOutput<LEN>>,
|
|
|
|
/// 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<Buffers<LEN>>,
|
|
}
|
|
|
|
impl<const LEN: usize> PitchDetectorThread<LEN> {
|
|
pub fn new() -> Self {
|
|
let (recordings, recording_rx) = RingBuffer::<DetectionInput<LEN>>::new(30).split();
|
|
let (processed_tx, processed) = RingBuffer::<DetectionOutput<LEN>>::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::<LEN> {
|
|
buffers: buf,
|
|
sample_rate,
|
|
});
|
|
break;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_micros(10));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn try_get_pitch(&mut self) -> Option<(Option<f32>, Option<f32>)> {
|
|
let DetectionOutput::<LEN> {
|
|
buffers,
|
|
pitch_l,
|
|
pitch_r,
|
|
} = self.processed.pop()?;
|
|
self.empty_buffers.push(buffers);
|
|
|
|
Some((pitch_l, pitch_r))
|
|
}
|
|
}
|