use crate::{audio::AudioBuffers, error::Result, midi::MidiEvent, plugin::Plugin};
use rtrb::{Consumer, Producer, RingBuffer};
use std::{
mem::ManuallyDrop,
sync::mpsc::{sync_channel, Receiver, SyncSender, TryRecvError, TrySendError},
thread::{self, ThreadId},
};
pub(crate) fn is_normalized(value: f64) -> bool {
value.is_finite() && (0.0..=1.0).contains(&value)
}
pub(crate) fn drain_commands<T>(rx: &mut Consumer<T>, mut apply: impl FnMut(T)) -> usize {
let budget = rx.buffer().capacity();
let mut applied = 0;
for _ in 0..budget {
let Ok(command) = rx.pop() else {
break;
};
apply(command);
applied += 1;
}
applied
}
#[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 { event: MidiEvent, offset: i32 },
Param { id: u32, value: f64 },
Transport(TransportCommand),
}
pub struct RealtimePluginRunner {
plugin: Option<Plugin>,
rx: Consumer<RtCommand>,
teardown_tx: SyncSender<ManuallyDrop<Plugin>>,
}
pub struct RtControl {
tx: Producer<RtCommand>,
dropped: u64,
teardown: OwnerThreadTeardown<Plugin>,
}
struct OwnerThreadTeardown<T> {
rx: Receiver<ManuallyDrop<T>>,
owner_thread: ThreadId,
}
impl<T> OwnerThreadTeardown<T> {
fn service_one(&mut self) -> bool {
if thread::current().id() != self.owner_thread {
return false;
}
match self.rx.try_recv() {
Ok(value) => {
drop(ManuallyDrop::into_inner(value));
true
}
Err(TryRecvError::Empty | TryRecvError::Disconnected) => false,
}
}
}
impl<T> Drop for OwnerThreadTeardown<T> {
fn drop(&mut self) {
if thread::current().id() != self.owner_thread {
return;
}
while self.service_one() {}
}
}
fn teardown_handoff<T>() -> (SyncSender<ManuallyDrop<T>>, OwnerThreadTeardown<T>) {
let (tx, rx) = sync_channel(1);
(
tx,
OwnerThreadTeardown {
rx,
owner_thread: thread::current().id(),
},
)
}
fn try_handoff_teardown<T>(tx: &SyncSender<ManuallyDrop<T>>, value: T) -> bool {
match tx.try_send(ManuallyDrop::new(value)) {
Ok(()) => true,
Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => false,
}
}
impl RealtimePluginRunner {
pub fn new(plugin: Plugin, command_capacity: usize) -> (Self, RtControl) {
let (tx, rx) = RingBuffer::new(command_capacity.max(1));
let (teardown_tx, teardown) = teardown_handoff();
(
Self {
plugin: Some(plugin),
rx,
teardown_tx,
},
RtControl {
tx,
dropped: 0,
teardown,
},
)
}
pub fn start(&mut self) -> Result<()> {
self.plugin
.as_mut()
.expect("runner plugin missing")
.start_processing()
}
pub fn stop(&mut self) -> Result<()> {
self.plugin
.as_mut()
.expect("runner plugin missing")
.stop_processing()
}
pub fn process(&mut self, buffers: &mut AudioBuffers) -> Result<()> {
let plugin = self.plugin.as_mut().expect("runner plugin missing");
let rx = &mut self.rx;
drain_commands(rx, |command| match command {
RtCommand::Midi { event, offset } => {
let _ = plugin.send_midi_event_at(event, offset);
}
RtCommand::Param { id, value } => {
let _ = plugin.queue_processor_parameter_at(id, value, 0);
}
RtCommand::Transport(change) => {
change.apply(plugin);
}
});
plugin.process_audio(buffers)
}
pub fn plugin(&self) -> &Plugin {
self.plugin.as_ref().expect("runner plugin missing")
}
pub fn into_plugin(mut self) -> Plugin {
self.plugin.take().expect("runner plugin missing")
}
}
impl Drop for RealtimePluginRunner {
fn drop(&mut self) {
let Some(plugin) = self.plugin.take() else {
return;
};
let _ = try_handoff_teardown(&self.teardown_tx, plugin);
}
}
impl RtControl {
pub fn service_teardown(&mut self) -> bool {
self.teardown.service_one()
}
pub fn send_midi(&mut self, event: MidiEvent) -> bool {
self.send_midi_at(event, 0)
}
pub fn send_midi_at(&mut self, event: MidiEvent, sample_offset: i32) -> bool {
let ok = self
.tx
.push(RtCommand::Midi {
event,
offset: sample_offset.max(0),
})
.is_ok();
self.track(ok)
}
pub fn set_parameter(&mut self, id: u32, value: f64) -> bool {
if !is_normalized(value) {
return false;
}
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::*;
use crate::midi::MidiChannel;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
};
fn test_control(tx: Producer<RtCommand>) -> RtControl {
let (_teardown_tx, teardown) = teardown_handoff();
RtControl {
tx,
dropped: 0,
teardown,
}
}
#[test]
fn control_queue_reports_full_without_blocking() {
let (tx, _rx) = RingBuffer::<RtCommand>::new(2);
let mut control = test_control(tx);
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 = test_control(tx);
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 midi_offset_round_trips_through_the_ring() {
let (tx, mut rx) = RingBuffer::<RtCommand>::new(8);
let mut control = test_control(tx);
assert!(control.send_midi_at(
MidiEvent::NoteOn {
channel: MidiChannel::Ch1,
note: 60,
velocity: 100,
},
128,
));
assert!(control.send_midi(MidiEvent::NoteOff {
channel: MidiChannel::Ch1,
note: 60,
velocity: 0,
}));
match rx.pop().expect("scheduled note queued") {
RtCommand::Midi { offset, .. } => assert_eq!(offset, 128),
_ => panic!("expected a MIDI command"),
}
match rx.pop().expect("block-start note queued") {
RtCommand::Midi { offset, .. } => assert_eq!(offset, 0),
_ => panic!("expected a MIDI command"),
}
}
#[test]
fn out_of_range_parameter_values_are_rejected_not_queued() {
let (tx, mut rx) = RingBuffer::<RtCommand>::new(8);
let mut control = test_control(tx);
for bad in [
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
-0.1,
1.000_001,
7.3,
] {
assert!(!control.set_parameter(1, bad), "{bad} must be rejected");
}
assert!(rx.pop().is_err(), "no invalid value reached the ring");
assert_eq!(control.dropped_command_count(), 0);
assert!(control.set_parameter(1, 0.0));
assert!(control.set_parameter(1, 1.0));
assert_eq!(rx.slots(), 2);
}
#[test]
fn is_normalized_accepts_exactly_the_unit_interval() {
assert!(is_normalized(0.0) && is_normalized(0.5) && is_normalized(1.0));
for bad in [
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
-1e-9,
1.0 + 1e-9,
] {
assert!(!is_normalized(bad), "{bad} is not normalized");
}
}
#[test]
fn drain_commands_stops_after_one_ring_even_while_the_producer_refills() {
let (mut tx, mut rx) = RingBuffer::<u32>::new(4);
for i in 0..4 {
tx.push(i).expect("ring holds 4");
}
let mut seen = Vec::new();
let applied = drain_commands(&mut rx, |command| {
seen.push(command);
let _ = tx.push(100 + command);
});
assert_eq!(applied, 4, "exactly one ring's worth per call");
assert_eq!(seen, vec![0, 1, 2, 3]);
assert_eq!(
rx.slots(),
4,
"the commands pushed during the drain wait for the next block"
);
}
#[test]
fn drain_commands_stops_early_on_an_empty_ring() {
let (mut tx, mut rx) = RingBuffer::<u32>::new(64);
tx.push(7).expect("room");
let mut seen = Vec::new();
assert_eq!(drain_commands(&mut rx, |c| seen.push(c)), 1);
assert_eq!(seen, vec![7]);
assert_eq!(drain_commands(&mut rx, |c| seen.push(c)), 0);
}
#[test]
fn invalid_transport_values_are_rejected_not_queued() {
let (tx, _rx) = RingBuffer::<RtCommand>::new(8);
let mut control = test_control(tx);
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);
}
struct DropProbe {
drops: Arc<AtomicUsize>,
threads: Arc<Mutex<Vec<ThreadId>>>,
}
impl Drop for DropProbe {
fn drop(&mut self) {
self.drops.fetch_add(1, Ordering::SeqCst);
self.threads
.lock()
.expect("drop thread log")
.push(thread::current().id());
}
}
fn drop_probe() -> (DropProbe, Arc<AtomicUsize>, Arc<Mutex<Vec<ThreadId>>>) {
let drops = Arc::new(AtomicUsize::new(0));
let threads = Arc::new(Mutex::new(Vec::new()));
(
DropProbe {
drops: Arc::clone(&drops),
threads: Arc::clone(&threads),
},
drops,
threads,
)
}
#[test]
fn teardown_is_serviced_only_on_the_captured_owner_thread() {
let owner = thread::current().id();
let (teardown_tx, teardown) = teardown_handoff();
let (probe, drops, threads) = drop_probe();
assert!(try_handoff_teardown(&teardown_tx, probe));
let mut teardown = thread::spawn(move || {
let mut teardown = teardown;
assert!(!teardown.service_one());
teardown
})
.join()
.expect("non-owner service thread");
assert_eq!(drops.load(Ordering::SeqCst), 0);
assert!(teardown.service_one());
assert_eq!(drops.load(Ordering::SeqCst), 1);
assert_eq!(*threads.lock().expect("drop thread log"), vec![owner]);
}
#[test]
fn dropping_teardown_receiver_off_owner_leaks_queued_value() {
let (teardown_tx, teardown) = teardown_handoff();
let (probe, drops, _threads) = drop_probe();
assert!(try_handoff_teardown(&teardown_tx, probe));
thread::spawn(move || drop(teardown))
.join()
.expect("off-owner drop thread");
drop(teardown_tx);
assert_eq!(
drops.load(Ordering::SeqCst),
0,
"thread-affine value must not be destroyed off its owner thread"
);
}
#[test]
fn disconnected_or_full_handoff_leaks_instead_of_dropping_the_value() {
let (disconnected_tx, disconnected_rx) = teardown_handoff();
drop(disconnected_rx);
let (disconnected_probe, disconnected_drops, _) = drop_probe();
assert!(!try_handoff_teardown(&disconnected_tx, disconnected_probe));
assert_eq!(disconnected_drops.load(Ordering::SeqCst), 0);
let (full_tx, mut full_rx) = teardown_handoff();
let (queued_probe, queued_drops, _) = drop_probe();
let (overflow_probe, overflow_drops, _) = drop_probe();
assert!(try_handoff_teardown(&full_tx, queued_probe));
assert!(!try_handoff_teardown(&full_tx, overflow_probe));
assert_eq!(overflow_drops.load(Ordering::SeqCst), 0);
assert!(full_rx.service_one());
assert_eq!(queued_drops.load(Ordering::SeqCst), 1);
}
}