use std::{any::Any, error::Error, fmt, sync::Arc};
use midly::live::LiveEvent;
use tokio::sync::mpsc::Sender;
use crate::{config, dmx::engine::Engine, playsync::PlaybackSync, songs::Song};
pub(crate) mod beat_clock;
pub(crate) mod midir;
pub(crate) mod mock;
pub mod morningstar;
pub(crate) mod playback;
mod transform;
#[derive(Debug, thiserror::Error)]
pub enum MidiError {
#[error("MIDI device not found: {0}")]
DeviceNotFound(String),
#[error("MIDI port error: {0}")]
Port(String),
#[error("MIDI playback error: {0}")]
Playback(String),
#[error(transparent)]
Other(Box<dyn Error + Send + Sync>),
}
impl From<Box<dyn Error + Send + Sync>> for MidiError {
fn from(e: Box<dyn Error + Send + Sync>) -> Self {
MidiError::Other(e)
}
}
pub trait Device: Any + fmt::Display + std::marker::Send + std::marker::Sync {
fn watch_events(&self, sender: Sender<Vec<u8>>) -> Result<(), Box<dyn Error>>;
fn stop_watch_events(&self);
fn play_from(&self, song: Arc<Song>, sync: PlaybackSync) -> Result<(), Box<dyn Error>>;
fn emit(&self, midi_event: Option<LiveEvent<'static>>) -> Result<(), Box<dyn Error>>;
fn emit_sysex(&self, bytes: &[u8]) -> Result<(), Box<dyn Error>>;
#[cfg(test)]
fn to_mock(&self) -> Result<Arc<mock::Device>, Box<dyn Error>>;
}
pub use midir::MidiDeviceInfo;
pub fn list_device_info() -> Result<Vec<MidiDeviceInfo>, Box<dyn Error>> {
midir::list_device_info()
}
pub fn list_devices() -> Result<Vec<Box<dyn Device>>, Box<dyn Error>> {
midir::list()
}
pub fn get_device(
config: Option<config::Midi>,
dmx_engine: Option<Arc<Engine>>,
) -> Result<Option<Arc<dyn Device>>, Box<dyn Error>> {
let config = match config {
Some(config) => config,
None => return Ok(None),
};
let device = config.device();
if device.starts_with("mock") {
return Ok(Some(Arc::new(mock::Device::get(device))));
};
Ok(Some(Arc::new(midir::get(&config, dmx_engine)?)))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn list_devices_does_not_panic() {
let _result = list_devices();
}
#[test]
fn get_device_none_config_returns_none() {
let result = get_device(None, None).unwrap();
assert!(result.is_none());
}
#[test]
fn get_device_mock_returns_some() {
let config = config::Midi::new("mock-midi", None);
let result = get_device(Some(config), None).unwrap();
assert!(result.is_some());
assert!(format!("{}", result.unwrap()).contains("mock-midi"));
}
#[test]
fn mock_device_display() {
let device = mock::Device::get("mock-test");
assert!(format!("{}", device).contains("mock-test"));
assert!(format!("{}", device).contains("Mock"));
}
#[test]
fn mock_device_emit_none_is_ok() {
let device = mock::Device::get("mock-test");
let midi_device: &dyn Device = &device;
assert!(midi_device.emit(None).is_ok());
}
#[test]
fn mock_device_emit_some_stores_event() {
let device = mock::Device::get("mock-test");
let event = LiveEvent::Midi {
channel: 0.into(),
message: midly::MidiMessage::NoteOn {
key: midly::num::u7::new(60),
vel: midly::num::u7::new(100),
},
};
let midi_device: &dyn Device = &device;
assert!(midi_device.emit(Some(event)).is_ok());
let emitted = device.get_emitted_event();
assert!(emitted.is_some());
let bytes = emitted.unwrap();
assert_eq!(bytes[0], 0x90); assert_eq!(bytes[1], 60);
assert_eq!(bytes[2], 100);
}
#[test]
fn mock_device_reset_emitted_event() {
let device = mock::Device::get("mock-test");
let event = LiveEvent::Midi {
channel: 0.into(),
message: midly::MidiMessage::NoteOn {
key: midly::num::u7::new(60),
vel: midly::num::u7::new(100),
},
};
let midi_device: &dyn Device = &device;
midi_device.emit(Some(event)).unwrap();
assert!(device.get_emitted_event().is_some());
device.reset_emitted_event();
assert!(device.get_emitted_event().is_none());
}
#[test]
fn mock_device_to_mock() {
let device = mock::Device::get("mock-test");
let midi_device: &dyn Device = &device;
let mock = midi_device.to_mock();
assert!(mock.is_ok());
}
#[test]
fn mock_device_to_string_contains_mock() {
let device = mock::Device::get("my-device");
let s = format!("{}", device);
assert!(s.contains("my-device"));
assert!(s.contains("Mock"));
}
#[tokio::test]
async fn mock_device_watch_and_stop() {
let device = mock::Device::get("mock-test");
let (tx, mut rx) = tokio::sync::mpsc::channel(10);
let midi_device: &dyn Device = &device;
midi_device.watch_events(tx).unwrap();
device.mock_event(&[0x90, 60, 100]);
let received = rx.recv().await.unwrap();
assert_eq!(received, vec![0x90, 60, 100]);
midi_device.stop_watch_events();
}
#[tokio::test]
async fn mock_device_watch_events_already_watching() {
let device = mock::Device::get("mock-test");
let (tx1, _rx1) = tokio::sync::mpsc::channel(10);
let (tx2, _rx2) = tokio::sync::mpsc::channel(10);
let midi_device: &dyn Device = &device;
midi_device.watch_events(tx1).unwrap();
let result = midi_device.watch_events(tx2);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Already watching"));
midi_device.stop_watch_events();
}
}