#![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, /// Current recording buffer /// Input goes here recording_buffer: Buffers, /// 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, /// Current playing buffer /// Output comes from here playing_buffer: Buffers, /// 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, /// 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>, } // 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::::new(10).split(); let (processed_tx, processed) = RingBuffer::>::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 = 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) { 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");