[utils] make a pitch detection thingy, from robotuna
parent
2418efc63b
commit
4c3e4d42de
|
@ -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<f32>,
|
||||
pitch_r: Option<f32>,
|
||||
|
||||
/// Current recording buffer
|
||||
/// Input goes here
|
||||
recording_buffer: Buffers<BUFFER_LEN>,
|
||||
|
||||
/// Ringbuf producer so we can send audio chunks to the processing thread
|
||||
recordings: Producer<tuna::ProcessorInput>,
|
||||
/// Ringbuf consumer so we can receive processed buffers from the processing threads
|
||||
processed: Consumer<tuna::ProcessorOutput>,
|
||||
|
||||
/// 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<BUFFER_LEN>>,
|
||||
detector_thread: pitch_detection::PitchDetectorThread<BUFFER_LEN>,
|
||||
|
||||
/// Keeps delay lines for playing
|
||||
delays: DelayLines<DELAY_LEN>,
|
||||
|
@ -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::<tuna::ProcessorInput>::new(30).split();
|
||||
let (processed_tx, processed) = RingBuffer::<tuna::ProcessorOutput>::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<BUFFER_LEN> = Buffers::new();
|
||||
empty_buffers.append(&mut vec![BUF; 30]);
|
||||
let detector_thread = pitch_detection::PitchDetectorThread::<BUFFER_LEN>::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::<DELAY_LEN>::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);
|
||||
|
|
|
@ -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<BUFFER_LEN>,
|
||||
pub(crate) sample_rate: u32,
|
||||
}
|
||||
|
||||
pub struct ProcessorOutput {
|
||||
pub(crate) buffers: Buffers<BUFFER_LEN>,
|
||||
pub(crate) pitch_l: Option<f32>,
|
||||
pub(crate) pitch_r: Option<f32>,
|
||||
}
|
||||
|
||||
pub fn tuna(mut inputs: Consumer<ProcessorInput>, mut outputs: Producer<ProcessorOutput>) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -1,30 +1,4 @@
|
|||
use pitch_detection::detector::yin::YINDetector;
|
||||
use pitch_detection::detector::PitchDetector;
|
||||
|
||||
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 mod pitch_detection;
|
||||
|
||||
pub fn generate_vocoder(sample_rate: u32) -> PhaseVocoder {
|
||||
PhaseVocoder::new(1, sample_rate as f64, 256, 4)
|
|
@ -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<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))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue