[utils] make a pitch detection thingy, from robotuna
This commit is contained in:
parent
2418efc63b
commit
4c3e4d42de
5 changed files with 154 additions and 135 deletions
|
@ -2,16 +2,12 @@
|
||||||
#![feature(generic_associated_types)]
|
#![feature(generic_associated_types)]
|
||||||
|
|
||||||
use baseplug::{MidiReceiver, Plugin, ProcessContext};
|
use baseplug::{MidiReceiver, Plugin, ProcessContext};
|
||||||
use ringbuf::{Consumer, Producer, RingBuffer};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use utils::buffers::*;
|
|
||||||
use utils::delay::*;
|
use utils::delay::*;
|
||||||
use utils::logs::*;
|
use utils::logs::*;
|
||||||
use utils::pitch::*;
|
use utils::pitch::*;
|
||||||
|
|
||||||
mod tuna;
|
|
||||||
|
|
||||||
const BUFFER_LEN: usize = 2 << 9;
|
const BUFFER_LEN: usize = 2 << 9;
|
||||||
const DELAY_LEN: usize = 4000;
|
const DELAY_LEN: usize = 4000;
|
||||||
|
|
||||||
|
@ -44,19 +40,7 @@ struct RoboTuna {
|
||||||
pitch_l: Option<f32>,
|
pitch_l: Option<f32>,
|
||||||
pitch_r: Option<f32>,
|
pitch_r: Option<f32>,
|
||||||
|
|
||||||
/// Current recording buffer
|
detector_thread: pitch_detection::PitchDetectorThread<BUFFER_LEN>,
|
||||||
/// 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>>,
|
|
||||||
|
|
||||||
/// Keeps delay lines for playing
|
/// Keeps delay lines for playing
|
||||||
delays: DelayLines<DELAY_LEN>,
|
delays: DelayLines<DELAY_LEN>,
|
||||||
|
@ -82,18 +66,7 @@ impl Plugin for RoboTuna {
|
||||||
fn new(_sample_rate: f32, _model: &RoboTunaModel) -> Self {
|
fn new(_sample_rate: f32, _model: &RoboTunaModel) -> Self {
|
||||||
setup_logging("robotuna.log");
|
setup_logging("robotuna.log");
|
||||||
|
|
||||||
let (recordings, recording_rx) = RingBuffer::<tuna::ProcessorInput>::new(30).split();
|
let detector_thread = pitch_detection::PitchDetectorThread::<BUFFER_LEN>::new();
|
||||||
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]);
|
|
||||||
|
|
||||||
log::info!("finished init");
|
log::info!("finished init");
|
||||||
|
|
||||||
|
@ -101,10 +74,8 @@ impl Plugin for RoboTuna {
|
||||||
note: None,
|
note: None,
|
||||||
pitch_l: None,
|
pitch_l: None,
|
||||||
pitch_r: None,
|
pitch_r: None,
|
||||||
recording_buffer: Buffers::new(),
|
detector_thread,
|
||||||
recordings,
|
|
||||||
processed,
|
|
||||||
empty_buffers,
|
|
||||||
delays: DelayLines::<DELAY_LEN>::new(),
|
delays: DelayLines::<DELAY_LEN>::new(),
|
||||||
|
|
||||||
delay_idx_l: 0.0,
|
delay_idx_l: 0.0,
|
||||||
|
@ -121,44 +92,12 @@ impl Plugin for RoboTuna {
|
||||||
let output = &mut ctx.outputs[0].buffers;
|
let output = &mut ctx.outputs[0].buffers;
|
||||||
|
|
||||||
for i in 0..ctx.nframes {
|
for i in 0..ctx.nframes {
|
||||||
// append input to main buffer
|
// pass input to pitch detector
|
||||||
let full = self
|
self.detector_thread
|
||||||
.recording_buffer
|
.write(input[0][i], input[1][i], ctx.sample_rate as u32);
|
||||||
.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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get a processed buffer from the processor thread
|
// Try to get a processed buffer from the processor thread
|
||||||
if let Some(tuna::ProcessorOutput {
|
if let Some((pitch_l, pitch_r)) = self.detector_thread.try_get_pitch() {
|
||||||
buffers,
|
|
||||||
pitch_l,
|
|
||||||
pitch_r,
|
|
||||||
}) = self.processed.pop()
|
|
||||||
{
|
|
||||||
self.empty_buffers.push(buffers);
|
|
||||||
|
|
||||||
// Update current pitch
|
// Update current pitch
|
||||||
// We use `or`, so we keep the old value if the current one is None
|
// We use `or`, so we keep the old value if the current one is None
|
||||||
self.pitch_l = pitch_l.or(self.pitch_l);
|
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"
|
log-panics = "2.0.0"
|
||||||
dirs = "3.0.2"
|
dirs = "3.0.2"
|
||||||
pvoc = { path = "../pvoc-rs" }
|
pvoc = { path = "../pvoc-rs" }
|
||||||
|
ringbuf = "0.2.5"
|
||||||
|
|
|
@ -1,30 +1,4 @@
|
||||||
use pitch_detection::detector::yin::YINDetector;
|
pub mod pitch_detection;
|
||||||
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 fn generate_vocoder(sample_rate: u32) -> PhaseVocoder {
|
pub fn generate_vocoder(sample_rate: u32) -> PhaseVocoder {
|
||||||
PhaseVocoder::new(1, sample_rate as f64, 256, 4)
|
PhaseVocoder::new(1, sample_rate as f64, 256, 4)
|
144
crates/utils/src/pitch/pitch_detection.rs
Normal file
144
crates/utils/src/pitch/pitch_detection.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue