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), + }), + } + } +}