diff --git a/Cargo.toml b/Cargo.toml
index 17910df..ed326fd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"]}
diff --git a/examples/input.rs b/examples/input.rs
index a25caf4..901dd48 100644
--- a/examples/input.rs
+++ b/examples/input.rs
@@ -103,17 +103,7 @@ 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
- );
+ text_section.value = format!("Last Message: {:?}", data.message);
}
}
diff --git a/examples/output.rs b/examples/output.rs
index 433c837..ead028e 100644
--- a/examples/output.rs
+++ b/examples/output.rs
@@ -71,10 +71,10 @@ fn disconnect(input: Res>, output: Res) {
fn play_notes(input: Res>, output: Res) {
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));
}
}
}
diff --git a/examples/piano.rs b/examples/piano.rs
index 84c8dc0..e3aef80 100644
--- a/examples/piano.rs
+++ b/examples/piano.rs
@@ -22,6 +22,7 @@ fn main() {
.init_resource::()
.add_plugins(MidiOutputPlugin)
.init_resource::()
+ .add_state::()
.add_systems(Startup, setup)
.add_systems(
Update,
@@ -31,11 +32,15 @@ fn main() {
connect_to_first_output_port,
display_press,
display_release,
+ swap_camera,
),
)
.run();
}
+#[derive(Component)]
+struct ProjectionStatus;
+
#[derive(Component, Debug)]
struct Key {
key_val: String,
@@ -59,12 +64,69 @@ fn setup(
..Default::default()
});
- //Camera
+ //Perspective Camera
cmds.spawn((
Camera3dBundle {
transform: Transform::from_xyz(8., 5., mid).looking_at(Vec3::new(0., 0., mid), Vec3::Y),
+ camera: Camera{
+ is_active: false,
+ ..Default::default()
+ },
..Default::default()
},
+ PersCamera
+ ));
+
+ // Top-down camera
+ cmds.spawn((
+ Camera3dBundle {
+ transform: Transform::from_xyz(1., 2., mid).looking_at(Vec3::new(0., 0., mid), Vec3::Y),
+ projection: Projection::Orthographic(OrthographicProjection{
+ //scaling_mode: todo!(),
+ scale: 0.011,
+ //area: todo!(),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ OrthCamera
+ ));
+
+ //UI
+ cmds.spawn((
+ TextBundle {
+ text: Text {
+ sections: vec![
+ TextSection::new(
+ "Projection:\n",
+ TextStyle {
+ font: asset_server.load("fonts/FiraSans-Bold.ttf"),
+ font_size: 30.0,
+ color: Color::WHITE,
+ },
+ ),
+ TextSection::new(
+ "Orthographic\n",
+ TextStyle {
+ font: asset_server.load("fonts/FiraSans-Bold.ttf"),
+ font_size: 30.0,
+ color: Color::AQUAMARINE,
+ },
+ ),
+ TextSection::new(
+ "Press T to switch camera",
+ TextStyle {
+ font: asset_server.load("fonts/FiraSans-Bold.ttf"),
+ font_size: 15.0,
+ color: Color::WHITE,
+ },
+ ),
+ ],
+ ..Default::default()
+ },
+ ..default()
+ },
+ ProjectionStatus,
));
let pos: Vec3 = Vec3::new(0., 0., 0.);
@@ -129,9 +191,53 @@ fn spawn_note(
));
}
-fn display_press(mut query: Query<&mut Transform, With>) {
- for mut t in &mut query {
- t.translation.y = -0.05;
+#[derive(States, Default, PartialEq, Eq, Debug, Clone, Copy, Hash)]
+enum ProjectionType {
+ #[default]
+ Orthographic,
+ Perspective,
+}
+
+#[derive(Component)]
+struct OrthCamera;
+
+#[derive(Component)]
+struct PersCamera;
+
+fn swap_camera(
+ keys: Res>,
+ proj: Res>,
+ mut proj_txt: Query<&mut Text, With>,
+ mut nxt_proj: ResMut>,
+ mut q_pers: Query<&mut Camera, (With, Without)>,
+ mut q_orth: Query<&mut Camera, (With, Without)>,
+) {
+ if keys.just_pressed(KeyCode::T) {
+ match (&mut q_pers.get_single_mut(), &mut q_orth.get_single_mut()) {
+ (Ok(pers), Ok(orth)) => {
+ let text_section = &mut proj_txt.single_mut().sections[1];
+ nxt_proj.set(if *proj == ProjectionType::Orthographic {
+ orth.is_active = false;
+ pers.is_active = true;
+ text_section.value = "Perspective\n".to_string();
+ ProjectionType::Perspective
+ } else {
+ pers.is_active = false;
+ orth.is_active = true;
+ text_section.value = "Orthographic\n".to_string();
+ ProjectionType::Orthographic
+ });
+ }
+ _ => (),
+ }
+ }
+}
+
+fn display_press(mut query: Query<(&mut Transform, &Key), With>) {
+ for (mut t, k) in &mut query {
+ if t.translation.y == k.y_reset {
+ t.translation.y += -0.05;
+ }
}
}
@@ -147,24 +253,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);
- }
- }
- } 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::();
+ match data.message {
+ OwnedLiveEvent::Midi {
+ message: MidiMessage::NoteOn { key, .. } | MidiMessage::NoteOff { key, .. },
+ ..
+ } => {
+ 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::();
+ }
+ }
+ } else {
}
}
- } else {
+ _ => {}
}
}
}
diff --git a/src/input.rs b/src/input.rs
index cb10c2b..28b6094 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -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;
@@ -110,11 +113,35 @@ impl MidiInputConnection {
#[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 {
+ matches!(
+ self.message,
+ OwnedLiveEvent::Midi {
+ message: MidiMessage::NoteOn { .. },
+ ..
+ }
+ )
+ }
+
+ /// Return `true` iff the underlying message represents a MIDI note off event.
+ pub fn is_note_off(&self) -> bool {
+ matches!(
+ self.message,
+ OwnedLiveEvent::Midi {
+ message: MidiMessage::NoteOff { .. },
+ ..
+ }
+ )
+ }
+}
+
/// The [`Error`] type for midi input operations, accessible as an [`Event`](bevy::ecs::event::Event).
#[derive(Clone, Debug)]
pub enum MidiInputError {
@@ -240,14 +267,17 @@ impl Future for MidiInputTask {
.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(),
+ }));
+ });
},
(),
);
@@ -287,14 +317,17 @@ impl Future for MidiInputTask {
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(),
+ }));
+ })
},
(),
);
@@ -343,16 +376,17 @@ fn get_available_ports(input: &midir::MidiInput) -> Reply {
// A system which debug prints note events
fn debug(mut midi: EventReader) {
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);
+ // 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);
+ // }
}
}
diff --git a/src/lib.rs b/src/lib.rs
index 16076bd..aafad51 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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
- }
-}
diff --git a/src/output.rs b/src/output.rs
index 3d49568..36b1070 100644
--- a/src/output.rs
+++ b/src/output.rs
@@ -1,4 +1,3 @@
-use super::MidiMessage;
use bevy::prelude::*;
use bevy::tasks::IoTaskPool;
use crossbeam_channel::{Receiver, Sender};
@@ -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 {
@@ -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");
@@ -103,7 +104,7 @@ impl MidiOutputConnection {
pub enum MidiOutputError {
ConnectionError(ConnectErrorKind),
SendError(midir::SendError),
- SendDisconnectedError(MidiMessage),
+ SendDisconnectedError(OwnedLiveEvent),
PortRefreshError,
}
@@ -168,6 +169,9 @@ fn reply(
warn!("{}", e);
err.send(e);
}
+ Reply::IoError(e) => {
+ warn!("{}", e);
+ }
Reply::Connected => {
conn.connected = true;
}
@@ -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,
}
@@ -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
diff --git a/src/types.rs b/src/types.rs
new file mode 100644
index 0000000..29585cd
--- /dev/null
+++ b/src/types.rs
@@ -0,0 +1,177 @@
+//! Module definined owned variants of `[midly]` structures. These owned variants allow for more
+//! ergonomic usage.
+use crate::KEY_RANGE;
+use midly::live::{LiveEvent, SystemCommon};
+use midly::num::{self, u4, u7};
+pub use midly::{
+ live::{MtcQuarterFrameMessage, SystemRealtime},
+ MidiMessage,
+};
+use std::fmt::Debug;
+/// Owned version of a [`midly::live::LiveEvent`].
+///
+/// Standard [`midly::live::LiveEvent`]s have a lifetime parameter limiting them to the scope in
+/// which they are generated to avoid any copying. However, because we are sending these messages
+/// through the bevy event system, they need to outlive this original scope.
+///
+/// Creating [`OwnedLiveEvent`]s only allocates when the message is a an [`OwnedSystemCommon`] that
+/// itself contains an allocation.
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub enum OwnedLiveEvent {
+ /// A midi message with a channel and music data.
+ Midi {
+ channel: num::u4,
+ message: midly::MidiMessage,
+ },
+
+ /// A System Common message with owned data.
+ Common(OwnedSystemCommon),
+
+ /// A one-byte System Realtime Message.
+ Realtime(SystemRealtime),
+}
+
+/// Owned version of [`midly::live::SystemCommon`].
+///
+/// [`OwnedSystemCommon`] fully owns any underlying value data, including
+/// [`OwnedSystemCommon::SysEx`] messages.
+#[derive(Clone, PartialEq, Eq, Debug, Hash)]
+pub enum OwnedSystemCommon {
+ /// A system-exclusive event.
+ ///
+ /// Only contains the data bytes; does not inclde the `0xF0` and `0xF6` begin/end marker bytes.
+ /// slice does not include either: it only includes data bytes in the `0x00..=0x7F` range.
+ SysEx(Vec),
+ /// A MIDI Time Code Quarter Frame message, carrying a tag type and a 4-bit tag value.
+ MidiTimeCodeQuarterFrame(MtcQuarterFrameMessage, num::u4),
+ /// The number of MIDI beats (6 x MIDI clocks) that have elapsed since the start of the
+ /// sequence.
+ SongPosition(num::u14),
+ /// Select a given song index.
+ SongSelect(num::u7),
+ /// Request the device to tune itself.
+ TuneRequest,
+ /// An undefined System Common message, with arbitrary data bytes.
+ Undefined(u8, Vec),
+}
+
+impl OwnedLiveEvent {
+ /// Returns a [`MidiMessage::NoteOn`] event.
+ pub fn note_on, K: Into, V: Into>(
+ channel: C,
+ key: K,
+ vel: V,
+ ) -> OwnedLiveEvent {
+ OwnedLiveEvent::Midi {
+ channel: channel.into(),
+ message: midly::MidiMessage::NoteOn {
+ key: key.into(),
+ vel: vel.into(),
+ },
+ }
+ }
+
+ /// Returns a [`MidiMessage::NoteOff`] event.
+ pub fn note_off, K: Into, V: Into>(
+ channel: C,
+ key: K,
+ vel: V,
+ ) -> OwnedLiveEvent {
+ OwnedLiveEvent::Midi {
+ channel: channel.into(),
+ message: midly::MidiMessage::NoteOff {
+ key: key.into(),
+ vel: vel.into(),
+ },
+ }
+ }
+}
+
+fn fmt_note(
+ f: &mut std::fmt::Formatter<'_>,
+ msg: &str,
+ ch: &u4,
+ key: &u7,
+ vel: &u7,
+) -> std::fmt::Result {
+ let index: u8 = key.as_int();
+ let off = index % 12;
+ let oct = index.overflowing_div(12).0;
+ let key_str = KEY_RANGE.iter().nth(off.into()).unwrap();
+
+ f.write_fmt(format_args!(
+ "Ch: {} {}: {}{:?} Vel: {}",
+ ch, msg, key_str, oct, vel
+ ))
+}
+
+impl Debug for OwnedLiveEvent {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Midi { channel, message } => {
+ {
+ let _ = match message {
+ MidiMessage::NoteOff { key, vel } => {
+ fmt_note(f, "NoteOff", channel, key, vel)
+ }
+ MidiMessage::NoteOn { key, vel } => {
+ fmt_note(f, "NoteOn", channel, key, vel)
+ }
+ MidiMessage::Aftertouch { key, vel } => {
+ fmt_note(f, "Aftertouch", channel, key, vel)
+ }
+ _ => f
+ .debug_struct("Midi")
+ .field("channel", channel)
+ .field("message", message)
+ .finish(),
+ };
+ };
+ Ok(())
+ }
+ Self::Common(arg) => f.debug_tuple("Common").field(arg).finish(),
+ Self::Realtime(arg) => f.debug_tuple("Realtime").field(arg).finish(),
+ }
+ }
+}
+
+impl<'a> From> for OwnedLiveEvent {
+ fn from(value: LiveEvent) -> Self {
+ match value {
+ LiveEvent::Midi { channel, message } => OwnedLiveEvent::Midi { channel, message },
+ LiveEvent::Realtime(rt) => OwnedLiveEvent::Realtime(rt),
+ LiveEvent::Common(sc) => OwnedLiveEvent::Common(match sc {
+ SystemCommon::MidiTimeCodeQuarterFrame(m, v) => {
+ OwnedSystemCommon::MidiTimeCodeQuarterFrame(m, v)
+ }
+ SystemCommon::SongPosition(pos) => OwnedSystemCommon::SongPosition(pos),
+ SystemCommon::SongSelect(ss) => OwnedSystemCommon::SongSelect(ss),
+ SystemCommon::TuneRequest => OwnedSystemCommon::TuneRequest,
+ SystemCommon::SysEx(b) => OwnedSystemCommon::SysEx(b.to_vec()),
+ SystemCommon::Undefined(tag, b) => OwnedSystemCommon::Undefined(tag, b.to_vec()),
+ }),
+ }
+ }
+}
+
+impl<'a, 'b: 'a> From<&'b OwnedLiveEvent> for LiveEvent<'a> {
+ fn from(value: &'b OwnedLiveEvent) -> Self {
+ match value {
+ OwnedLiveEvent::Midi { channel, message } => LiveEvent::Midi {
+ channel: *channel,
+ message: *message,
+ },
+ OwnedLiveEvent::Realtime(rt) => LiveEvent::Realtime(*rt),
+ OwnedLiveEvent::Common(sc) => LiveEvent::Common(match sc {
+ OwnedSystemCommon::MidiTimeCodeQuarterFrame(m, v) => {
+ SystemCommon::MidiTimeCodeQuarterFrame(*m, *v)
+ }
+ OwnedSystemCommon::SongPosition(pos) => SystemCommon::SongPosition(*pos),
+ OwnedSystemCommon::SongSelect(ss) => SystemCommon::SongSelect(*ss),
+ OwnedSystemCommon::TuneRequest => SystemCommon::TuneRequest,
+ OwnedSystemCommon::SysEx(b) => SystemCommon::SysEx(b),
+ OwnedSystemCommon::Undefined(tag, b) => SystemCommon::Undefined(*tag, b),
+ }),
+ }
+ }
+}