Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration with midly for full MIDI message parsing / support using "Owned" message variants #36

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ name = "bevy_midi"
[dependencies]
midir = "0.9"
crossbeam-channel = "0.5.8"
midly = { version = "0.5.3", default-features = false, features = ["std", "alloc"] }

[dev-dependencies]
bevy_egui = { version = "0.23", features = ["immutable_ctx"]}
Expand Down
23 changes: 12 additions & 11 deletions examples/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,18 @@ fn show_last_message(
) {
for data in midi_data.read() {
let text_section = &mut instructions.single_mut().sections[3];
text_section.value = format!(
"Last Message: {} - {:?}",
if data.message.is_note_on() {
"NoteOn"
} else if data.message.is_note_off() {
"NoteOff"
} else {
"Other"
},
data.message.msg
);
let event_str = match &data.message {
OwnedLiveEvent::Midi { channel, message } => {
format!("Channel {channel} - {message:?}")
}
OwnedLiveEvent::Common(sc) => {
format!("{:?}", sc)
}
OwnedLiveEvent::Realtime(rt) => {
format!("{:?}", rt)
}
};
text_section.value = format!("Last Message: {:?}", event_str);
}
}

Expand Down
4 changes: 2 additions & 2 deletions examples/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ fn disconnect(input: Res<Input<KeyCode>>, output: Res<MidiOutput>) {
fn play_notes(input: Res<Input<KeyCode>>, output: Res<MidiOutput>) {
for (keycode, note) in &KEY_NOTE_MAP {
if input.just_pressed(*keycode) {
output.send([0b1001_0000, *note, 127].into()); // Note on, channel 1, max velocity
output.send(OwnedLiveEvent::note_on(0, *note, 127));
}
if input.just_released(*keycode) {
output.send([0b1000_0000, *note, 127].into()); // Note on, channel 1, max velocity
output.send(OwnedLiveEvent::note_off(0, *note, 127));
}
}
}
Expand Down
40 changes: 24 additions & 16 deletions examples/piano.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,24 +147,32 @@ fn handle_midi_input(
query: Query<(Entity, &Key)>,
) {
for data in midi_events.read() {
let [_, index, _value] = data.message.msg;
let off = index % 12;
let oct = index.overflowing_div(12).0;
let key_str = KEY_RANGE.iter().nth(off.into()).unwrap();

if data.message.is_note_on() {
for (entity, key) in query.iter() {
if key.key_val.eq(&format!("{}{}", key_str, oct).to_string()) {
commands.entity(entity).insert(PressedKey);
match data.message {
OwnedLiveEvent::Midi {
message: MidiMessage::NoteOn { key, .. },
masonium marked this conversation as resolved.
Show resolved Hide resolved
..
} => {
let index: u8 = key.into();
let off = index % 12;
let oct = index.overflowing_div(12).0;
let key_str = KEY_RANGE.iter().nth(off.into()).unwrap();

if data.is_note_on() {
for (entity, key) in query.iter() {
if key.key_val.eq(&format!("{}{}", key_str, oct).to_string()) {
commands.entity(entity).insert(PressedKey);
}
}
} else if data.is_note_off() {
for (entity, key) in query.iter() {
if key.key_val.eq(&format!("{}{}", key_str, oct).to_string()) {
commands.entity(entity).remove::<PressedKey>();
}
}
} else {
}
}
} else if data.message.is_note_off() {
for (entity, key) in query.iter() {
if key.key_val.eq(&format!("{}{}", key_str, oct).to_string()) {
commands.entity(entity).remove::<PressedKey>();
}
}
} else {
_ => {}
}
}
}
Expand Down
76 changes: 55 additions & 21 deletions src/input.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use super::{MidiMessage, KEY_RANGE};
use crate::types::OwnedLiveEvent;

use bevy::prelude::Plugin;
use bevy::{prelude::*, tasks::IoTaskPool};
use crossbeam_channel::{Receiver, Sender};
use midir::ConnectErrorKind; // XXX: do we expose this?
pub use midir::{Ignore, MidiInputPort};
use midly::stream::MidiStream;
use midly::MidiMessage;
use std::error::Error;
use std::fmt::Display;
use std::future::Future;
Expand Down Expand Up @@ -110,11 +113,35 @@
#[derive(Resource)]
pub struct MidiData {
pub stamp: u64,
pub message: MidiMessage,
pub message: OwnedLiveEvent,
}

impl bevy::prelude::Event for MidiData {}

impl MidiData {
/// Return `true` iff the underlying message represents a MIDI note on event.
pub fn is_note_on(&self) -> bool {
match self.message {

Check failure on line 124 in src/input.rs

View workflow job for this annotation

GitHub Actions / Clippy

match expression looks like `matches!` macro
OwnedLiveEvent::Midi {
message: MidiMessage::NoteOn { .. },
..
} => true,
_ => false,
}
}

/// Return `true` iff the underlying message represents a MIDI note off event.
pub fn is_note_off(&self) -> bool {
BlackPhlox marked this conversation as resolved.
Show resolved Hide resolved
match self.message {

Check failure on line 135 in src/input.rs

View workflow job for this annotation

GitHub Actions / Clippy

match expression looks like `matches!` macro
OwnedLiveEvent::Midi {
message: MidiMessage::NoteOn { .. },
..
} => true,
_ => false,
}
}
}

/// The [`Error`] type for midi input operations, accessible as an [`Event`](bevy::ecs::event::Event).
#[derive(Clone, Debug)]
pub enum MidiInputError {
Expand Down Expand Up @@ -240,14 +267,17 @@
.input
.take()
.unwrap_or_else(|| self.connection.take().unwrap().0.close().0);
let mut stream = MidiStream::new();
let conn = i.connect(
&port,
self.settings.port_name,
move |stamp, message, _| {
let _ = s.send(Reply::Midi(MidiData {
stamp,
message: [message[0], message[1], message[2]].into(),
}));
stream.feed(message, |live_event| {
let _ = s.send(Reply::Midi(MidiData {
stamp,
message: live_event.into(),
}));
});
},
(),
);
Expand Down Expand Up @@ -287,14 +317,17 @@
self.sender.send(get_available_ports(&i)).unwrap();

let s = self.sender.clone();
let mut stream = MidiStream::new();
let conn = i.connect(
&port,
self.settings.port_name,
move |stamp, message, _| {
let _ = s.send(Reply::Midi(MidiData {
stamp,
message: [message[0], message[1], message[2]].into(),
}));
stream.feed(message, |live_event| {
let _ = s.send(Reply::Midi(MidiData {
stamp,
message: live_event.into(),
}));
})
},
(),
);
Expand Down Expand Up @@ -343,16 +376,17 @@
// A system which debug prints note events
fn debug(mut midi: EventReader<MidiData>) {
for data in midi.read() {
let pitch = data.message.msg[1];
let octave = pitch / 12;
let key = KEY_RANGE[pitch as usize % 12];

if data.message.is_note_on() {
debug!("NoteOn: {}{:?} - Raw: {:?}", key, octave, data.message.msg);
} else if data.message.is_note_off() {
debug!("NoteOff: {}{:?} - Raw: {:?}", key, octave, data.message.msg);
} else {
debug!("Other: {:?}", data.message.msg);
}
debug!("{:?}", data.message);
BlackPhlox marked this conversation as resolved.
Show resolved Hide resolved
// let pitch = data.message.msg[1];
// let octave = pitch / 12;
// let key = KEY_RANGE[pitch as usize % 12];

// if data.message.is_note_on() {
// debug!("NoteOn: {}{:?} - Raw: {:?}", key, octave, data.message.msg);
// } else if data.message.is_note_off() {
// debug!("NoteOff: {}{:?} - Raw: {:?}", key, octave, data.message.msg);
// } else {
// debug!("Other: {:?}", data.message.msg);
// }
Comment on lines +380 to +390
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// let pitch = data.message.msg[1];
// let octave = pitch / 12;
// let key = KEY_RANGE[pitch as usize % 12];
// if data.message.is_note_on() {
// debug!("NoteOn: {}{:?} - Raw: {:?}", key, octave, data.message.msg);
// } else if data.message.is_note_off() {
// debug!("NoteOff: {}{:?} - Raw: {:?}", key, octave, data.message.msg);
// } else {
// debug!("Other: {:?}", data.message.msg);
// }

}
}
40 changes: 7 additions & 33 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,16 @@
/// Re-export [`midly::num`] module .
pub mod num {
pub use midly::num::{u14, u15, u24, u28, u4, u7};
}

pub mod input;
pub mod output;
pub mod types;

pub mod prelude {
pub use crate::{input::*, output::*, *};
pub use crate::{input::*, output::*, types::*, *};
}

pub const KEY_RANGE: [&str; 12] = [
"C", "C#/Db", "D", "D#/Eb", "E", "F", "F#/Gb", "G", "G#/Ab", "A", "A#/Bb", "B",
];

const NOTE_ON_STATUS: u8 = 0b1001_0000;
const NOTE_OFF_STATUS: u8 = 0b1000_0000;

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct MidiMessage {
pub msg: [u8; 3],
}

impl From<[u8; 3]> for MidiMessage {
fn from(msg: [u8; 3]) -> Self {
MidiMessage { msg }
}
}

impl MidiMessage {
#[must_use]
pub fn is_note_on(&self) -> bool {
(self.msg[0] & 0b1111_0000) == NOTE_ON_STATUS
}

#[must_use]
pub fn is_note_off(&self) -> bool {
(self.msg[0] & 0b1111_0000) == NOTE_OFF_STATUS
}

/// Get the channel of a message, assuming the message is not a system message.
#[must_use]
pub fn channel(&self) -> u8 {
self.msg[0] & 0b0000_1111
}
}
26 changes: 20 additions & 6 deletions src/output.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use super::MidiMessage;
use bevy::prelude::*;
use bevy::tasks::IoTaskPool;
use crossbeam_channel::{Receiver, Sender};
Expand All @@ -8,6 +7,8 @@ use std::fmt::Display;
use std::{error::Error, future::Future};
use MidiOutputError::{ConnectionError, PortRefreshError, SendDisconnectedError, SendError};

use crate::types::OwnedLiveEvent;

pub struct MidiOutputPlugin;

impl Plugin for MidiOutputPlugin {
Expand Down Expand Up @@ -69,7 +70,7 @@ impl MidiOutput {
}

/// Send a midi message.
pub fn send(&self, msg: MidiMessage) {
pub fn send(&self, msg: OwnedLiveEvent) {
self.sender
.send(Message::Midi(msg))
.expect("Couldn't send MIDI message");
Expand Down Expand Up @@ -103,7 +104,7 @@ impl MidiOutputConnection {
pub enum MidiOutputError {
ConnectionError(ConnectErrorKind),
SendError(midir::SendError),
SendDisconnectedError(MidiMessage),
SendDisconnectedError(OwnedLiveEvent),
PortRefreshError,
}

Expand Down Expand Up @@ -168,6 +169,9 @@ fn reply(
warn!("{}", e);
err.send(e);
}
Reply::IoError(e) => {
warn!("{}", e);
}
Reply::Connected => {
conn.connected = true;
}
Expand All @@ -182,12 +186,13 @@ enum Message {
RefreshPorts,
ConnectToPort(MidiOutputPort),
DisconnectFromPort,
Midi(MidiMessage),
Midi(OwnedLiveEvent),
}

enum Reply {
AvailablePorts(Vec<(String, MidiOutputPort)>),
Error(MidiOutputError),
IoError(std::io::Error),
Connected,
Disconnected,
}
Expand Down Expand Up @@ -279,8 +284,17 @@ impl Future for MidiOutputTask {
},
Midi(message) => {
if let Some((conn, _)) = &mut self.connection {
if let Err(e) = conn.send(&message.msg) {
self.sender.send(Reply::Error(SendError(e))).unwrap();
let mut byte_msg = Vec::with_capacity(4);
let live: midly::live::LiveEvent = (&message).into();
match live.write_std(&mut byte_msg) {
Ok(_) => {
if let Err(e) = conn.send(&byte_msg) {
self.sender.send(Reply::Error(SendError(e))).unwrap();
}
}
Err(write_err) => {
self.sender.send(Reply::IoError(write_err)).unwrap();
}
}
} else {
self.sender
Expand Down
Loading
Loading