diff --git a/crates/robotuna/src/lib.rs b/crates/robotuna/src/lib.rs index 4feb099..97efecc 100644 --- a/crates/robotuna/src/lib.rs +++ b/crates/robotuna/src/lib.rs @@ -2,16 +2,12 @@ #![feature(generic_associated_types)] use baseplug::{MidiReceiver, Plugin, ProcessContext}; -use ringbuf::{Consumer, Producer, RingBuffer}; use serde::{Deserialize, Serialize}; -use utils::buffers::*; use utils::delay::*; use utils::logs::*; use utils::pitch::*; -mod tuna; - const BUFFER_LEN: usize = 2 << 9; const DELAY_LEN: usize = 4000; @@ -44,19 +40,7 @@ struct RoboTuna { pitch_l: Option, pitch_r: Option, - /// 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>, + detector_thread: pitch_detection::PitchDetectorThread, /// Keeps delay lines for playing delays: DelayLines, @@ -82,18 +66,7 @@ impl Plugin for RoboTuna { fn new(_sample_rate: f32, _model: &RoboTunaModel) -> Self { setup_logging("robotuna.log"); - let (recordings, recording_rx) = RingBuffer::::new(30).split(); - let (processed_tx, processed) = RingBuffer::::new(30).split(); - - // Spawn analysis thread - std::thread::spawn(move || { - tuna::tuna(recording_rx, processed_tx); - }); - - // keep some empty buffer around so we can swap them - let mut empty_buffers = Vec::with_capacity(80); - const BUF: Buffers = Buffers::new(); - empty_buffers.append(&mut vec![BUF; 30]); + let detector_thread = pitch_detection::PitchDetectorThread::::new(); log::info!("finished init"); @@ -101,10 +74,8 @@ impl Plugin for RoboTuna { note: None, pitch_l: None, pitch_r: None, - recording_buffer: Buffers::new(), - recordings, - processed, - empty_buffers, + detector_thread, + delays: DelayLines::::new(), delay_idx_l: 0.0, @@ -121,44 +92,12 @@ impl Plugin for RoboTuna { let output = &mut ctx.outputs[0].buffers; for i in 0..ctx.nframes { - // append input to main buffer - let full = self - .recording_buffer - .write_advance(input[0][i], input[1][i]); - // 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(tuna::ProcessorInput { - buffers: buf, - sample_rate: ctx.sample_rate as u32, - }); - break; - } - std::thread::sleep(std::time::Duration::from_micros(10)); - } - } + // pass input to pitch detector + self.detector_thread + .write(input[0][i], input[1][i], ctx.sample_rate as u32); // Try to get a processed buffer from the processor thread - if let Some(tuna::ProcessorOutput { - buffers, - pitch_l, - pitch_r, - }) = self.processed.pop() - { - self.empty_buffers.push(buffers); - + if let Some((pitch_l, pitch_r)) = self.detector_thread.try_get_pitch() { // Update current pitch // We use `or`, so we keep the old value if the current one is None self.pitch_l = pitch_l.or(self.pitch_l); diff --git a/crates/robotuna/src/tuna.rs b/crates/robotuna/src/tuna.rs deleted file mode 100644 index a4fcc42..0000000 --- a/crates/robotuna/src/tuna.rs +++ /dev/null @@ -1,39 +0,0 @@ -use ringbuf::{Consumer, Producer}; - -use utils::buffers::*; -use utils::pitch::*; - -use crate::BUFFER_LEN; - -pub struct ProcessorInput { - pub(crate) buffers: Buffers, - pub(crate) sample_rate: u32, -} - -pub struct ProcessorOutput { - pub(crate) buffers: Buffers, - pub(crate) pitch_l: Option, - pub(crate) pitch_r: Option, -} - -pub fn tuna(mut inputs: Consumer, mut outputs: Producer) { - let mut detector_l = generate_pitch_detector(BUFFER_LEN); - let mut detector_r = generate_pitch_detector(BUFFER_LEN); - - loop { - if let Some(ProcessorInput { - 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(ProcessorOutput { - buffers, - pitch_l, - pitch_r, - }); - } - } -} diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 25f5841..27b8403 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -10,3 +10,4 @@ log = "0.4.14" log-panics = "2.0.0" dirs = "3.0.2" pvoc = { path = "../pvoc-rs" } +ringbuf = "0.2.5" diff --git a/crates/utils/src/pitch.rs b/crates/utils/src/pitch/mod.rs similarity index 68% rename from crates/utils/src/pitch.rs rename to crates/utils/src/pitch/mod.rs index 779934e..fe83015 100644 --- a/crates/utils/src/pitch.rs +++ b/crates/utils/src/pitch/mod.rs @@ -1,30 +1,4 @@ -use pitch_detection::detector::yin::YINDetector; -use pitch_detection::detector::PitchDetector; - -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 mod pitch_detection; pub fn generate_vocoder(sample_rate: u32) -> PhaseVocoder { PhaseVocoder::new(1, sample_rate as f64, 256, 4) diff --git a/crates/utils/src/pitch/pitch_detection.rs b/crates/utils/src/pitch/pitch_detection.rs new file mode 100644 index 0000000..770bab0 --- /dev/null +++ b/crates/utils/src/pitch/pitch_detection.rs @@ -0,0 +1,144 @@ +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)) + } +}