#![doc = r#"
A plugin and types for handling MIDI output
"#]
use midix::MidiMessageBytes;
mod connection;
use connection::*;
mod error;
pub use error::*;
mod plugin;
pub use plugin::*;
use bevy::prelude::*;
use midir::{MidiOutputPort, SendError};
use crate::MidiSettings;
enum MidiOutputState {
Listening(midir::MidiOutput),
Active(MidiOutputConnection),
}
unsafe impl Sync for MidiOutputState {}
#[derive(Resource)]
pub struct MidiOutput {
settings: MidiSettings,
state: Option<MidiOutputState>,
ports: Vec<MidiOutputPort>,
}
impl MidiOutput {
pub fn new(settings: MidiSettings) -> Self {
let listener = match midir::MidiOutput::new(settings.client_name) {
Ok(output) => output,
Err(e) => {
panic!("Error initializing midi output! {e:?}");
}
};
let ports = listener.ports();
Self {
state: Some(MidiOutputState::Listening(listener)),
settings,
ports,
}
}
pub fn ports(&self) -> &[MidiOutputPort] {
&self.ports
}
pub fn connect_to_index(&mut self, index: usize) -> Result<(), MidiOutputError> {
if self
.state
.as_ref()
.is_none_or(|s| matches!(s, MidiOutputState::Active(_)))
{
return Err(MidiOutputError::invalid(
"Cannot connect: not currently active!",
));
}
let Some(port) = self.ports.get(index) else {
return Err(MidiOutputError::port_not_found(
"Port was not found at {index}!",
));
};
let MidiOutputState::Listening(listener) = self.state.take().unwrap() else {
unreachable!()
};
self.state = Some(MidiOutputState::Active(
MidiOutputConnection::new(listener, port, self.settings.port_name).unwrap(),
));
Ok(())
}
pub fn reset(&mut self) {
let listener = match midir::MidiOutput::new(self.settings.client_name) {
Ok(output) => output,
Err(e) => {
error!("Failed to reset listening state! {e:?}");
return;
}
};
self.state = Some(MidiOutputState::Listening(listener));
}
pub fn connect_to_port(&mut self, port: &MidiOutputPort) -> Result<(), MidiOutputError> {
if self
.state
.as_ref()
.is_none_or(|s| matches!(s, MidiOutputState::Active(_)))
{
return Err(MidiOutputError::invalid(
"Cannot connect: not currently active!",
));
}
let MidiOutputState::Listening(listener) = self.state.take().unwrap() else {
unreachable!()
};
self.state = Some(MidiOutputState::Active(
MidiOutputConnection::new(listener, port, self.settings.port_name).unwrap(),
));
Ok(())
}
pub fn connect_to_id(&mut self, id: String) -> Result<(), MidiOutputError> {
if self
.state
.as_ref()
.is_none_or(|s| matches!(s, MidiOutputState::Active(_)))
{
return Err(MidiOutputError::invalid(
"Cannot connect: not currently active!",
));
}
let MidiOutputState::Listening(listener) = self.state.take().unwrap() else {
unreachable!()
};
let Some(port) = listener.find_port_by_id(id.clone()) else {
return Err(MidiOutputError::port_not_found(id));
};
self.state = Some(MidiOutputState::Active(
MidiOutputConnection::new(listener, &port, self.settings.port_name).unwrap(),
));
Ok(())
}
pub fn is_active(&self) -> bool {
self.state
.as_ref()
.is_some_and(|s| matches!(s, MidiOutputState::Active(_)))
}
pub fn is_listening(&self) -> bool {
self.state
.as_ref()
.is_some_and(|s| matches!(s, MidiOutputState::Listening(_)))
}
pub fn refresh_ports(&mut self) {
let Some(MidiOutputState::Listening(listener)) = &self.state else {
return;
};
self.ports = listener.ports();
}
pub fn disconnect(&mut self) {
if self
.state
.as_ref()
.is_none_or(|s| matches!(s, MidiOutputState::Listening(_)))
{
return;
}
let MidiOutputState::Active(conn) = self.state.take().unwrap() else {
unreachable!()
};
let listener = conn.close();
self.state = Some(MidiOutputState::Listening(listener));
}
pub fn send(&mut self, message: impl Into<MidiMessageBytes>) -> Result<(), SendError> {
let Some(MidiOutputState::Active(conn)) = &mut self.state else {
return Err(SendError::Other("Disconnected."));
};
conn.send(message)
}
}