use crate::{audio::AudioBuffers, error::Result, midi::MidiEvent, plugin::Plugin};
use rtrb::{Consumer, Producer, RingBuffer};
#[derive(Clone, Copy)]
pub(crate) enum TransportCommand {
Tempo(f64),
TimeSignature(i32, i32),
Playing(bool),
}
impl TransportCommand {
pub(crate) fn apply(self, plugin: &mut Plugin) {
match self {
TransportCommand::Tempo(bpm) => {
let _ = plugin.set_tempo(bpm);
}
TransportCommand::TimeSignature(num, den) => {
let _ = plugin.set_time_signature(num, den);
}
TransportCommand::Playing(playing) => {
let _ = plugin.set_playing(playing);
}
}
}
}
enum RtCommand {
Midi(MidiEvent),
Param { id: u32, value: f64 },
Transport(TransportCommand),
}
pub struct RealtimePluginRunner {
plugin: Plugin,
rx: Consumer<RtCommand>,
}
pub struct RtControl {
tx: Producer<RtCommand>,
dropped: u64,
}
impl RealtimePluginRunner {
pub fn new(plugin: Plugin, command_capacity: usize) -> (Self, RtControl) {
let (tx, rx) = RingBuffer::new(command_capacity.max(1));
(Self { plugin, rx }, RtControl { tx, dropped: 0 })
}
pub fn start(&mut self) -> Result<()> {
self.plugin.start_processing()
}
pub fn stop(&mut self) -> Result<()> {
self.plugin.stop_processing()
}
pub fn process(&mut self, buffers: &mut AudioBuffers) -> Result<()> {
while let Ok(cmd) = self.rx.pop() {
match cmd {
RtCommand::Midi(event) => {
let _ = self.plugin.send_midi_event(event);
}
RtCommand::Param { id, value } => {
let _ = self.plugin.set_parameter(id, value);
}
RtCommand::Transport(change) => {
change.apply(&mut self.plugin);
}
}
}
self.plugin.process_audio(buffers)
}
pub fn plugin(&self) -> &Plugin {
&self.plugin
}
pub fn into_plugin(self) -> Plugin {
self.plugin
}
}
impl RtControl {
pub fn send_midi(&mut self, event: MidiEvent) -> bool {
let ok = self.tx.push(RtCommand::Midi(event)).is_ok();
self.track(ok)
}
pub fn set_parameter(&mut self, id: u32, value: f64) -> bool {
let ok = self.tx.push(RtCommand::Param { id, value }).is_ok();
self.track(ok)
}
pub fn set_tempo(&mut self, bpm: f64) -> bool {
if !(bpm.is_finite() && bpm > 0.0) {
return false;
}
let ok = self
.tx
.push(RtCommand::Transport(TransportCommand::Tempo(bpm)))
.is_ok();
self.track(ok)
}
pub fn set_time_signature(&mut self, numerator: i32, denominator: i32) -> bool {
if numerator <= 0 || !matches!(denominator, 1 | 2 | 4 | 8 | 16) {
return false;
}
let ok = self
.tx
.push(RtCommand::Transport(TransportCommand::TimeSignature(
numerator,
denominator,
)))
.is_ok();
self.track(ok)
}
pub fn set_playing(&mut self, playing: bool) -> bool {
let ok = self
.tx
.push(RtCommand::Transport(TransportCommand::Playing(playing)))
.is_ok();
self.track(ok)
}
pub fn dropped_command_count(&self) -> u64 {
self.dropped
}
fn track(&mut self, ok: bool) -> bool {
if !ok {
self.dropped += 1;
}
ok
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn control_queue_reports_full_without_blocking() {
let (tx, _rx) = RingBuffer::<RtCommand>::new(2);
let mut control = RtControl { tx, dropped: 0 };
assert!(control.set_parameter(1, 0.5));
assert!(control.set_parameter(1, 0.6));
assert!(!control.set_parameter(1, 0.7));
assert!(!control.send_midi(crate::midi::MidiEvent::NoteOn {
channel: crate::midi::MidiChannel::Ch1,
note: 60,
velocity: 100
}));
assert_eq!(control.dropped_command_count(), 2);
}
#[test]
fn transport_commands_round_trip_through_the_ring() {
let (tx, mut rx) = RingBuffer::<RtCommand>::new(8);
let mut control = RtControl { tx, dropped: 0 };
assert!(control.set_tempo(140.0));
assert!(control.set_time_signature(7, 8));
assert!(control.set_playing(false));
match rx.pop().expect("tempo queued") {
RtCommand::Transport(TransportCommand::Tempo(bpm)) => assert_eq!(bpm, 140.0),
_ => panic!("expected tempo transport command"),
}
match rx.pop().expect("time sig queued") {
RtCommand::Transport(TransportCommand::TimeSignature(n, d)) => {
assert_eq!((n, d), (7, 8))
}
_ => panic!("expected time-signature transport command"),
}
match rx.pop().expect("playing queued") {
RtCommand::Transport(TransportCommand::Playing(p)) => assert!(!p),
_ => panic!("expected playing transport command"),
}
}
#[test]
fn invalid_transport_values_are_rejected_not_queued() {
let (tx, _rx) = RingBuffer::<RtCommand>::new(8);
let mut control = RtControl { tx, dropped: 0 };
assert!(!control.set_tempo(0.0));
assert!(!control.set_tempo(f64::NAN));
assert!(!control.set_time_signature(0, 4));
assert!(!control.set_time_signature(4, 3));
assert_eq!(control.dropped_command_count(), 0);
}
}