[utils] make a pitch detection thingy, from robotuna

main
annieversary 2021-08-03 12:16:42 +02:00
parent 2418efc63b
commit 4c3e4d42de
5 changed files with 154 additions and 135 deletions

View File

@ -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);

View File

@ -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,
});
}
}
}

View File

@ -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"

View File

@ -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)

View File

@ -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))
}
}