use std::time::{Duration, Instant};
use bevy::prelude::*;
use crossbeam_channel::{Receiver, Sender};
use crate::components::*;
use crate::events::*;
pub(crate) struct MidiSync<T>(std::sync::Mutex<T>);
impl<T> MidiSync<T> {
fn new(val: T) -> Self {
Self(std::sync::Mutex::new(val))
}
fn lock(&self) -> std::sync::MutexGuard<'_, T> {
self.0.lock().unwrap()
}
}
#[derive(Component)]
pub struct NativeInputPort(pub(crate) MidiSync<midir::MidiInputPort>);
#[derive(Component)]
pub struct NativeOutputPort(pub(crate) MidiSync<midir::MidiOutputPort>);
#[derive(Component)]
pub struct InputConnection {
_connection: MidiSync<midir::MidiInputConnection<()>>,
receiver: Receiver<MidiData>,
}
#[derive(Component)]
pub struct OutputConnection {
connection: MidiSync<midir::MidiOutputConnection>,
}
#[derive(Resource)]
pub struct PortEnumerationTimer {
last_check: Instant,
}
pub(crate) fn init(app: &mut App) {
app.insert_resource(PortEnumerationTimer {
last_check: Instant::now() - Duration::from_secs(10),
});
app.add_systems(
PreUpdate,
(
enumerate_midi_ports,
open_midi_inputs,
open_midi_outputs,
receive_midi_messages,
send_midi_messages,
)
.chain(),
);
app.add_observer(on_midi_input_removed);
app.add_observer(on_midi_output_removed);
}
pub fn enumerate_midi_ports(
mut commands: Commands,
existing_ports: Query<(Entity, &Name, &MidiPort)>,
mut timer: ResMut<PortEnumerationTimer>,
) {
let now = Instant::now();
if now.duration_since(timer.last_check) < Duration::from_secs(2) {
return;
}
timer.last_check = now;
let input_ports = match midir::MidiInput::new("nannou_midi_enum") {
Ok(midi_in) => {
let ports = midi_in.ports();
ports
.into_iter()
.filter_map(|p| midi_in.port_name(&p).ok().map(|name| (name, p)))
.collect::<Vec<_>>()
}
Err(err) => {
warn!("failed to enumerate MIDI input ports: {err}");
Vec::new()
}
};
let output_ports = match midir::MidiOutput::new("nannou_midi_enum") {
Ok(midi_out) => {
let ports = midi_out.ports();
ports
.into_iter()
.filter_map(|p| midi_out.port_name(&p).ok().map(|name| (name, p)))
.collect::<Vec<_>>()
}
Err(err) => {
warn!("failed to enumerate MIDI output ports: {err}");
Vec::new()
}
};
let mut matched_entities = Vec::new();
for (name, native_port) in input_ports {
let already_exists = existing_ports
.iter()
.any(|(_, n, p)| n.as_str() == name && p.direction == MidiPortDirection::Input);
if !already_exists {
let entity = commands
.spawn((
Name::new(name),
MidiPort {
direction: MidiPortDirection::Input,
},
NativeInputPort(MidiSync::new(native_port)),
))
.id();
commands
.entity(entity)
.trigger(|e| MidiPortAdded { entity: e });
} else {
for (e, n, p) in &existing_ports {
if n.as_str() == name && p.direction == MidiPortDirection::Input {
matched_entities.push(e);
break;
}
}
}
}
for (name, native_port) in output_ports {
let already_exists = existing_ports
.iter()
.any(|(_, n, p)| n.as_str() == name && p.direction == MidiPortDirection::Output);
if !already_exists {
let entity = commands
.spawn((
Name::new(name),
MidiPort {
direction: MidiPortDirection::Output,
},
NativeOutputPort(MidiSync::new(native_port)),
))
.id();
commands
.entity(entity)
.trigger(|e| MidiPortAdded { entity: e });
} else {
for (e, n, p) in &existing_ports {
if n.as_str() == name && p.direction == MidiPortDirection::Output {
matched_entities.push(e);
break;
}
}
}
}
for (entity, _, _) in &existing_ports {
if !matched_entities.contains(&entity) {
commands
.entity(entity)
.trigger(|e| MidiPortRemoved { entity: e });
commands.entity(entity).despawn();
}
}
}
pub fn open_midi_inputs(
mut commands: Commands,
new_inputs: Query<(Entity, &MidiInput, Option<&Name>), Added<MidiInput>>,
ports: Query<(Entity, &MidiPort, &NativeInputPort)>,
) {
for (entity, midi_input, name) in &new_inputs {
let connection_name = name
.map(|n| n.to_string())
.unwrap_or_else(|| format!("{entity}"));
let port_entity = if let Some(port) = midi_input.port {
port
} else {
match ports
.iter()
.find(|(_, p, _)| p.direction == MidiPortDirection::Input)
{
Some((e, _, _)) => e,
None => {
let msg = "no MIDI input port available";
warn!("{msg}");
commands.entity(entity).insert(MidiError {
message: msg.to_string(),
});
commands.entity(entity).trigger(|e| MidiDisconnected {
entity: e,
reason: msg.to_string(),
});
continue;
}
}
};
let Ok((_, _, native_port)) = ports.get(port_entity) else {
let msg = "referenced MIDI port entity not found";
commands.entity(entity).insert(MidiError {
message: msg.to_string(),
});
commands.entity(entity).trigger(|e| MidiDisconnected {
entity: e,
reason: msg.to_string(),
});
continue;
};
let midi_in = match midir::MidiInput::new(&connection_name) {
Ok(m) => m,
Err(err) => {
let msg = format!("failed to create MIDI input: {err}");
commands.entity(entity).insert(MidiError {
message: msg.clone(),
});
let reason = msg;
commands
.entity(entity)
.trigger(|e| MidiDisconnected { entity: e, reason });
continue;
}
};
let (sender, receiver) = crossbeam_channel::unbounded::<MidiData>();
match connect_input(midi_in, &*native_port.0.lock(), &connection_name, sender) {
Ok(connection) => {
commands.entity(entity).insert((
InputConnection {
_connection: MidiSync::new(connection),
receiver: receiver.clone(),
},
MidiInputStream::new(),
));
commands
.entity(entity)
.trigger(|e| MidiConnected { entity: e });
}
Err(msg) => {
commands.entity(entity).insert(MidiError {
message: msg.clone(),
});
let reason = msg;
commands
.entity(entity)
.trigger(|e| MidiDisconnected { entity: e, reason });
}
}
}
}
fn connect_input(
midi_in: midir::MidiInput,
port: &midir::MidiInputPort,
connection_name: &str,
sender: Sender<MidiData>,
) -> Result<midir::MidiInputConnection<()>, String> {
midi_in
.connect(
port,
connection_name,
move |stamp, message, _| {
if message.len() >= 3 {
let _ = sender.send(MidiData {
stamp,
message: MidiMessage::from([message[0], message[1], message[2]]),
});
}
},
(),
)
.map_err(|e| format!("failed to connect MIDI input: {e}"))
}
pub fn open_midi_outputs(
mut commands: Commands,
new_outputs: Query<(Entity, &MidiOutput, Option<&Name>), Added<MidiOutput>>,
ports: Query<(Entity, &MidiPort, &NativeOutputPort)>,
) {
for (entity, midi_output, name) in &new_outputs {
let connection_name = name
.map(|n| n.to_string())
.unwrap_or_else(|| format!("{entity}"));
let port_entity = if let Some(port) = midi_output.port {
port
} else {
let msg = "no MIDI output port specified";
warn!("{msg}");
commands.entity(entity).insert(MidiError {
message: msg.to_string(),
});
commands.entity(entity).trigger(|e| MidiDisconnected {
entity: e,
reason: msg.to_string(),
});
continue;
};
let Ok((_, _, native_port)) = ports.get(port_entity) else {
let msg = "referenced MIDI port entity not found";
commands.entity(entity).insert(MidiError {
message: msg.to_string(),
});
commands.entity(entity).trigger(|e| MidiDisconnected {
entity: e,
reason: msg.to_string(),
});
continue;
};
let midi_out = match midir::MidiOutput::new(&connection_name) {
Ok(m) => m,
Err(err) => {
let msg = format!("failed to create MIDI output: {err}");
commands.entity(entity).insert(MidiError {
message: msg.clone(),
});
let reason = msg;
commands
.entity(entity)
.trigger(|e| MidiDisconnected { entity: e, reason });
continue;
}
};
match midi_out.connect(&*native_port.0.lock(), &connection_name) {
Ok(connection) => {
commands.entity(entity).insert((
OutputConnection {
connection: MidiSync::new(connection),
},
MidiOutputStream::new(),
));
commands
.entity(entity)
.trigger(|e| MidiConnected { entity: e });
}
Err(err) => {
let msg = format!("failed to connect MIDI output: {err}");
commands.entity(entity).insert(MidiError {
message: msg.clone(),
});
let reason = msg;
commands
.entity(entity)
.trigger(|e| MidiDisconnected { entity: e, reason });
}
}
}
}
pub fn receive_midi_messages(mut inputs: Query<(&InputConnection, &mut MidiInputStream)>) {
for (conn, mut stream) in &mut inputs {
while let Ok(data) = conn.receiver.try_recv() {
stream.messages.push(data);
}
}
}
pub fn send_midi_messages(mut outputs: Query<(&OutputConnection, &mut MidiOutputStream)>) {
for (conn, mut stream) in &mut outputs {
let mut connection = conn.connection.lock();
for msg in stream.outbox.drain(..) {
if let Err(err) = connection.send(&msg.msg) {
warn!("failed to send MIDI message: {err}");
}
}
}
}
fn on_midi_input_removed(event: On<Remove, MidiInput>, mut commands: Commands) {
let entity = event.event_target();
commands
.entity(entity)
.remove::<(InputConnection, MidiInputStream, MidiError)>();
}
fn on_midi_output_removed(event: On<Remove, MidiOutput>, mut commands: Commands) {
let entity = event.event_target();
commands
.entity(entity)
.remove::<(OutputConnection, MidiOutputStream, MidiError)>();
}