225 lines
7.5 KiB
Rust
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");
|