unnieversal/crates/robotuna/src/lib.rs

225 lines
7.5 KiB
Rust

#![allow(incomplete_features)]
#![feature(generic_associated_types)]
use std::time::Duration;
use baseplug::{MidiReceiver, Plugin, ProcessContext};
use ringbuf::{Consumer, Producer, RingBuffer};
use serde::{Deserialize, Serialize};
use utils::buffers::*;
use utils::logs::*;
mod tuna;
const BUFFER_LEN: usize = 2 << 10;
const OVERLAP: usize = BUFFER_LEN / 3;
baseplug::model! {
#[derive(Debug, Serialize, Deserialize)]
struct RoboTunaModel {
#[model(min = 0.0, max = 1.0)]
#[parameter(name = "manual/snap")]
manual: f32,
#[model(min = 0.1, max = 2.0)]
#[parameter(name = "frequency gain")]
freq_gain: f32,
}
}
impl Default for RoboTunaModel {
fn default() -> Self {
Self {
manual: 1.0,
freq_gain: 1.0,
}
}
}
struct RoboTuna {
/// Current midi note
note: Option<u8>,
/// Current recording buffer
/// Input goes here
recording_buffer: Buffers<BUFFER_LEN>,
/// The next recording buffer we'll use. It gets a bit of the end of the `recording_buffer`
/// so we can do overlap
next_recording_buffer: Buffers<BUFFER_LEN>,
/// Current playing buffer
/// Output comes from here
playing_buffer: Buffers<BUFFER_LEN>,
/// Next playing buffer we'll use
/// We start using it at the end of the previous buffer so we can overlap
next_playing_buffer: Buffers<BUFFER_LEN>,
/// Ringbuf producer so we can send audio chunks to the processing thread
recordings: Producer<tuna::ProcessChunk>,
/// Ringbuf consumer so we can receive processed buffers from the processing threads
processed: Consumer<Buffers<BUFFER_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<BUFFER_LEN>>,
}
// LMAO let's go, i think this works
impl Plugin for RoboTuna {
const NAME: &'static str = "robotuna";
const PRODUCT: &'static str = "robotuna";
const VENDOR: &'static str = "unnieversal";
const INPUT_CHANNELS: usize = 2;
const OUTPUT_CHANNELS: usize = 2;
type Model = RoboTunaModel;
#[inline]
fn new(_sample_rate: f32, _model: &RoboTunaModel) -> Self {
setup_logging("robotuna.log");
let (recordings, recording_rx) = RingBuffer::<tuna::ProcessChunk>::new(10).split();
let (processed_tx, processed) = RingBuffer::<Buffers<BUFFER_LEN>>::new(10).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(50);
const BUF: Buffers<BUFFER_LEN> = Buffers::new();
empty_buffers.append(&mut vec![BUF; 30]);
log::info!("finished init");
Self {
note: None,
recording_buffer: Buffers::new(),
next_recording_buffer: Buffers::new(),
playing_buffer: Buffers::new(),
next_playing_buffer: Buffers::new(),
recordings,
processed,
empty_buffers,
}
}
#[inline]
fn process(&mut self, model: &RoboTunaModelProcess, ctx: &mut ProcessContext<Self>) {
let input = &ctx.inputs[0].buffers;
let output = &mut ctx.outputs[0].buffers;
for i in 0..ctx.nframes {
// Record
// Add to main buffer
let full = self
.recording_buffer
.write_advance(input[0][i], input[1][i]);
// If we're in overlap section, also add to next buffer
if self.recording_buffer.idx > BUFFER_LEN - OVERLAP {
self.next_recording_buffer
.write_advance(input[0][i], input[1][i]);
}
// If we finish the buffer, switch them
if full {
// get the empty buffer from unused buffer list
let mut buf = self
.empty_buffers
.pop()
.expect("should have an empty buffer");
buf.reset();
std::mem::swap(&mut buf, &mut self.recording_buffer);
buf.reset();
std::mem::swap(&mut self.next_recording_buffer, &mut self.recording_buffer);
let _ = self.recordings.push(tuna::ProcessChunk {
buffers: buf,
sample_rate: ctx.sample_rate as u32,
note: self.note,
manual: model.manual[i] <= 0.5,
freq_gain: model.freq_gain[i],
});
}
// Play
// Get values from main buffer
let (mut l, mut r, full) = self.playing_buffer.read_advance();
// If we're in overlap section, also play from next buffer
if self.playing_buffer.idx > BUFFER_LEN - OVERLAP {
let (l1, r1, _) = self.next_playing_buffer.read_advance();
// How much into overlap we are, from 0 to 1
let overlap =
(OVERLAP - (BUFFER_LEN - self.playing_buffer.idx)) as f32 / OVERLAP as f32;
// Linearly crossfade
// lineal crossfade works well for two waves that are highly correlated
l *= 1. - overlap;
r *= 1. - overlap;
l += overlap * l1;
r += overlap * r1;
}
// If we finish the buffer, switch them
if full {
// We try to switch like with the recording buffer, but since there might not be a processed
// buffer yet, we do it in a loop and retry every 1 millisecond
// After 10 iterations we give up. Since we didn't swap the buffer, we're gonna play the last one
// again. This isn't ideal, but it's better than silence ig (it might not be, idk)
// The 10 iterations is arbitrary, as is the 1 millisecond wait time
for _ in 0..10 {
if let Some(mut buf) = self.processed.pop() {
buf.reset();
std::mem::swap(&mut buf, &mut self.playing_buffer);
buf.reset();
std::mem::swap(&mut self.next_playing_buffer, &mut self.playing_buffer);
// Stick buf in unused buffer list
self.empty_buffers.push(buf);
// Exit loop
break;
} else {
log::info!("didn't have a processed buffer to swap to, retrying");
}
std::thread::sleep(Duration::from_millis(1));
}
}
output[0][i] = l;
output[1][i] = r;
}
}
}
impl MidiReceiver for RoboTuna {
fn midi_input(&mut self, _model: &RoboTunaModelProcess, data: [u8; 3]) {
match data[0] {
// note on
0x90 => {
self.note = Some(data[1]);
}
// note off
0x80 => {
// only set note to None if it's the same one we currently have
if let Some(n) = self.note {
if n == data[1] {
self.note = None;
}
}
}
_ => (),
}
}
}
baseplug::vst2!(RoboTuna, b"tuna");