use std::sync::Arc;
use std::time::Duration;
use crossbeam_channel::Receiver as CrossbeamReceiver;
use rill_core::queues::telemetry::Telemetry;
use rill_core::queues::{MpscQueue, SetParameter};
use rill_core::NodeId;
use crate::automaton::LfoWaveform;
use crate::control::{ControlEvent, Mapping, PatchbayControl};
#[cfg(feature = "serde")]
use crate::document::PatchbayDocument;
#[cfg(feature = "serde")]
use crate::function_registry::FunctionRegistry;
use crate::sequencer::{SequencerHandle, SnapshotSequencer};
use crate::strategy::{ConflictStrategy, ControlStrategy};
pub struct PatchbayEngine {
control: PatchbayControl,
}
impl PatchbayEngine {
pub fn new(command_queue: Arc<MpscQueue<SetParameter>>) -> Self {
let _ = tokio::runtime::Handle::try_current()
.expect("PatchbayEngine requires an active tokio runtime");
Self {
control: PatchbayControl::new(command_queue),
}
}
pub fn add_automaton<A: crate::control::Automaton + 'static>(
&mut self,
id: &str,
automaton: A,
interval: Duration,
target: (NodeId, String),
range: (f64, f64),
control: ControlStrategy,
conflict: ConflictStrategy,
) {
self.control
.add_automaton_task(id, automaton, interval, target, range, control, conflict);
}
pub fn add_lfo(
&mut self,
id: &str,
frequency: f64,
amplitude: f64,
offset: f64,
waveform: LfoWaveform,
interval: Duration,
target: (NodeId, String),
range: (f64, f64),
control: ControlStrategy,
conflict: ConflictStrategy,
) {
self.control.add_lfo_task(
id, frequency, amplitude, offset, waveform, interval, target, range, control, conflict,
);
}
pub fn add_envelope(
&mut self,
id: &str,
attack: f64,
decay: f64,
sustain: f64,
release: f64,
interval: Duration,
target: (NodeId, String),
range: (f64, f64),
control: ControlStrategy,
conflict: ConflictStrategy,
) {
self.control.add_envelope_task(
id, attack, decay, sustain, release, interval, target, range, control, conflict,
);
}
pub fn add_mapping(&mut self, mapping: Mapping) {
self.control.add_mapping(mapping);
}
#[cfg(feature = "serde")]
pub fn load_document(
&mut self,
doc: &PatchbayDocument,
registry: &FunctionRegistry,
) -> Result<(), String> {
doc.apply_to_async(&mut self.control, registry)
}
pub fn handle_event(&mut self, event: ControlEvent) {
self.control.handle_event(event);
}
pub fn attach_sequencer(
&mut self,
tel_rx: CrossbeamReceiver<Telemetry>,
sequencer: SnapshotSequencer,
) -> SequencerHandle {
self.control.attach_sequencer(tel_rx, sequencer)
}
#[cfg(feature = "serde")]
pub fn load_sequencer_document(
&mut self,
tel_rx: CrossbeamReceiver<Telemetry>,
doc: crate::sequencer::SequencerDocument,
) -> SequencerHandle {
let seq = doc.into_sequencer();
self.attach_sequencer(tel_rx, seq)
}
pub fn detach_sequencer(&mut self) {
self.control.detach_sequencer();
}
pub fn sequencer_handle(&self) -> Option<&SequencerHandle> {
self.control.sequencer_handle()
}
pub fn stop(&mut self) {
self.control.stop_all();
}
pub fn control(&self) -> &PatchbayControl {
&self.control
}
pub fn control_mut(&mut self) -> &mut PatchbayControl {
&mut self.control
}
}
impl Drop for PatchbayEngine {
fn drop(&mut self) {
self.stop();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::automaton::LfoWaveform;
use crate::control::{midi_cc, ControlEvent, Transform};
use crate::strategy::ControlStrategy;
use rill_core::queues::MpscQueue;
use rill_core::NodeId;
#[tokio::test]
async fn test_engine_creation() {
let queue = Arc::new(MpscQueue::new());
let engine = PatchbayEngine::new(queue);
drop(engine);
}
#[tokio::test]
async fn test_engine_add_lfo_produces_values() {
let queue = Arc::new(MpscQueue::with_capacity(64));
let mut engine = PatchbayEngine::new(queue.clone());
engine.add_lfo(
"test_lfo",
10.0,
1.0,
0.0,
LfoWaveform::Sine,
std::time::Duration::from_millis(10),
(NodeId(1), "cutoff".into()),
(0.0, 1.0),
ControlStrategy::Absolute,
crate::strategy::ConflictStrategy::LastWriteWins,
);
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
assert!(!queue.is_empty());
}
#[tokio::test]
async fn test_engine_handle_event_direct() {
let queue = Arc::new(MpscQueue::with_capacity(64));
let mut engine = PatchbayEngine::new(queue.clone());
engine.add_mapping(midi_cc(
7,
None,
NodeId(1),
"volume",
0.0,
1.0,
Transform::Linear,
));
let event = ControlEvent::MidiControl {
channel: 0,
controller: 7,
value: 64,
normalized: 0.5,
};
engine.handle_event(event);
let cmd = queue.pop().unwrap();
assert_eq!(cmd.parameter.as_ref(), "volume");
assert!((cmd.value - 0.5).abs() < 1e-6);
}
#[tokio::test]
async fn test_engine_stop() {
let queue = Arc::new(MpscQueue::new());
let mut engine = PatchbayEngine::new(queue.clone());
engine.add_lfo(
"test_lfo",
1.0,
1.0,
0.0,
LfoWaveform::Sine,
std::time::Duration::from_millis(10),
(NodeId(1), "out".into()),
(0.0, 1.0),
ControlStrategy::Absolute,
crate::strategy::ConflictStrategy::LastWriteWins,
);
engine.stop();
}
#[tokio::test]
async fn test_engine_drop_stops_tasks() {
let queue = Arc::new(MpscQueue::new());
{
let mut engine = PatchbayEngine::new(queue.clone());
engine.add_lfo(
"test_lfo",
1.0,
1.0,
0.0,
LfoWaveform::Sine,
std::time::Duration::from_millis(10),
(NodeId(1), "out".into()),
(0.0, 1.0),
ControlStrategy::Absolute,
crate::strategy::ConflictStrategy::LastWriteWins,
);
} }
#[tokio::test]
async fn test_engine_no_runtime_panics() {
}
}