From dea3594e37204f555a5b97d4b2a3ce6d19aa51fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patric=20Kanngie=C3=9Fer?= Date: Fri, 25 Dec 2020 13:03:40 +0100 Subject: [PATCH 1/7] Implemented basic midi receive --- .gitignore | 1 + Cargo.toml | 6 +++- src/data/byte/u7.rs | 1 + src/data/midi/message/message.rs | 35 ++++++++++++++++++++- src/data/midi/notes.rs | 3 +- src/data/usb/constants.rs | 2 +- src/data/usb_midi/midi_packet_reader.rs | 36 ++++++++++++++++++++++ src/data/usb_midi/mod.rs | 3 +- src/data/usb_midi/usb_midi_event_packet.rs | 35 +++++++++++++++++++++ src/midi_device.rs | 31 +++++++++++++------ 10 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 src/data/usb_midi/midi_packet_reader.rs diff --git a/.gitignore b/.gitignore index fa8d85a..eaf0cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ Cargo.lock target +.idea \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 4756664..bb5291e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,8 @@ license = "MIT" [dependencies] embedded-hal = "0.2.2" nb = "0.1.2" -usb-device = "0.2.3" \ No newline at end of file +usb-device = "0.2.3" + +[dependencies.num_enum] +version = "0.5.1" +default-features = false diff --git a/src/data/byte/u7.rs b/src/data/byte/u7.rs index f0ba796..bac83ae 100644 --- a/src/data/byte/u7.rs +++ b/src/data/byte/u7.rs @@ -2,6 +2,7 @@ use core::convert::TryFrom; use crate::data::byte::from_traits::{FromOverFlow,FromClamped}; /// A primitive value that can be from 0-0x7F +#[derive(Debug)] pub struct U7(u8); /// Error representing that this value is not a valid u7 diff --git a/src/data/midi/message/message.rs b/src/data/midi/message/message.rs index bdfd4c2..44dcc16 100644 --- a/src/data/midi/message/message.rs +++ b/src/data/midi/message/message.rs @@ -3,6 +3,9 @@ use crate::data::midi::channel::Channel; use crate::data::midi::notes::Note; use crate::data::byte::u7::U7; use crate::data::midi::message::raw::{Raw,Payload}; +use crate::data::byte::from_traits::FromClamped; +use core::convert::TryFrom; +use crate::data::usb_midi::usb_midi_event_packet::MidiPacketParsingError; type Velocity = U7; @@ -10,13 +13,14 @@ type Velocity = U7; /// Note: not current exhaustive and SysEx messages end up /// being a confusing case. So are currently note implemented /// they are sort-of unbounded +#[derive(Debug)] pub enum Message { NoteOff(Channel,Note,Velocity), NoteOn(Channel,Note,Velocity), PolyphonicAftertouch(Channel,Note,U7), ProgramChange(Channel,U7), ChannelAftertouch(Channel,U7), - PitchWheelChange(Channel,U7,U7), + PitchWheelChange(Channel,U7,U7) } const NOTE_OFF_MASK :u8 = 0b1000_0000; @@ -62,4 +66,33 @@ impl From for Raw { } } +} + +impl<'a> TryFrom<&'a [u8]> for Message { + type Error = MidiPacketParsingError; + fn try_from(data: &[u8]) -> Result { + let status_byte = data[0]; + let event_type = status_byte & 0b1111_0000; + let channel_bytes = (status_byte) & 0b0000_1111; + + let channel = Channel::try_from(channel_bytes).ok().unwrap(); + + match event_type { + NOTE_OFF_MASK => Ok(Message::NoteOff(channel, get_note(data)?, get_velocity(data))), + NOTE_ON_MASK => Ok(Message::NoteOn(channel, get_note(data)?, get_velocity(data))), + _ => Err(MidiPacketParsingError::InvalidEventType(event_type)) + } + } +} + +fn get_note(data: &[u8]) -> Result { + let note_byte = data[1]; + match Note::try_from(note_byte) { + Ok(note) => Ok(note), + Err(_) => Err(MidiPacketParsingError::InvalidNote(note_byte)) + } +} + +fn get_velocity(data: &[u8]) -> U7 { + U7::from_clamped(data[2]) } \ No newline at end of file diff --git a/src/data/midi/notes.rs b/src/data/midi/notes.rs index 225a15e..2273eea 100644 --- a/src/data/midi/notes.rs +++ b/src/data/midi/notes.rs @@ -1,10 +1,11 @@ use crate::data::byte::u7::U7; use crate::data::byte::from_traits::FromOverFlow; +use num_enum::TryFromPrimitive; /// A simple enum type that represents all the midi 'notes' /// note the flat versions are associated constants /// but can be referenced like Note::Bb3 /// C1m is the C-1 -#[derive(Debug,Copy,Clone)] +#[derive(Debug,Copy,Clone,TryFromPrimitive)] #[repr(u8)] pub enum Note { C1m, Cs1m, D1m, Ds1m, E1m, F1m, Fs1m, G1m, Gs1m, A1m, As1m, B1m, diff --git a/src/data/usb/constants.rs b/src/data/usb/constants.rs index 90f2cb7..6da6ee6 100644 --- a/src/data/usb/constants.rs +++ b/src/data/usb/constants.rs @@ -4,7 +4,7 @@ pub const USB_CLASS_NONE : u8 = 0x00; pub const USB_AUDIO_CLASS: u8 = 0x01; pub const USB_AUDIOCONTROL_SUBCLASS: u8 = 0x01; pub const USB_MIDISTREAMING_SUBCLASS: u8 =0x03; -//pub const MIDI_IN_JACK_SUBTYPE : u8 = 0x02; +pub const MIDI_IN_JACK_SUBTYPE : u8 = 0x02; pub const MIDI_OUT_JACK_SUBTYPE : u8 = 0x03; pub const EMBEDDED : u8 = 0x01; pub const CS_INTERFACE: u8 = 0x24; diff --git a/src/data/usb_midi/midi_packet_reader.rs b/src/data/usb_midi/midi_packet_reader.rs new file mode 100644 index 0000000..e3d3ebc --- /dev/null +++ b/src/data/usb_midi/midi_packet_reader.rs @@ -0,0 +1,36 @@ +use core::convert::TryFrom; +use crate::data::usb_midi::usb_midi_event_packet::{UsbMidiEventPacket, MidiPacketParsingError}; +use crate::midi_device::{MAX_PACKET_SIZE, MIDI_PACKET_SIZE}; + +pub struct MidiPacketBufferReader<'a> { + buffer: &'a [u8; MAX_PACKET_SIZE], + position: usize, + raw_bytes_received: usize +} + +impl<'a> MidiPacketBufferReader<'a> { + pub fn new(buffer: &'a [u8; MAX_PACKET_SIZE], raw_bytes_received: usize) -> Self { + MidiPacketBufferReader { + buffer, + position: 0, + raw_bytes_received + } + } +} + +impl<'a> Iterator for MidiPacketBufferReader<'a> { + type Item = Result; + + fn next(&mut self) -> Option { + if self.position <= MAX_PACKET_SIZE && self.position < self.raw_bytes_received { + let packet = match self.buffer.get(self.position .. (self.position + 4)) { + Some(packet) => Some(UsbMidiEventPacket::try_from(packet)), + None => None + }; + + self.position += MIDI_PACKET_SIZE; + return packet; + } + None + } +} \ No newline at end of file diff --git a/src/data/usb_midi/mod.rs b/src/data/usb_midi/mod.rs index eabf2cb..f3ba9b0 100644 --- a/src/data/usb_midi/mod.rs +++ b/src/data/usb_midi/mod.rs @@ -1,3 +1,4 @@ pub mod usb_midi_event_packet; pub mod cable_number; -pub mod code_index_number; \ No newline at end of file +pub mod code_index_number; +pub mod midi_packet_reader; \ No newline at end of file diff --git a/src/data/usb_midi/usb_midi_event_packet.rs b/src/data/usb_midi/usb_midi_event_packet.rs index cffdd1b..e8587fe 100644 --- a/src/data/usb_midi/usb_midi_event_packet.rs +++ b/src/data/usb_midi/usb_midi_event_packet.rs @@ -3,11 +3,18 @@ use crate::data::usb_midi::code_index_number::CodeIndexNumber; use crate::data::midi::message::Message; use crate::data::byte::u4::U4; use crate::data::midi::message::raw::{Payload,Raw}; +use crate::data::byte::u4; +use core::convert::TryFrom; +use crate::data::midi::channel::Channel::Channel1; +use crate::data::midi::notes::Note::C7; +use crate::data::byte::u7::U7; +use crate::data::byte::from_traits::FromClamped; /// A packet that communicates with the host /// Currently supported is sending the specified normal midi /// message over the supplied cable number +#[derive(Debug)] pub struct UsbMidiEventPacket { pub cable_number : CableNumber, pub message: Message @@ -37,6 +44,34 @@ impl From for [u8;4] { } } +#[derive(Debug)] +pub enum MidiPacketParsingError { + InvalidNote(u8), + InvalidCableNumber(u8), + InvalidEventType(u8), + MissingDataPacket +} + +impl TryFrom<&[u8]> for UsbMidiEventPacket { + type Error = MidiPacketParsingError; + + fn try_from(value: &[u8]) -> Result { + let raw_cable_number= value[0] >> 4; + + let cable_number = match CableNumber::try_from(u8::from(raw_cable_number)) { + Ok(val) => val, + _ => return Err(MidiPacketParsingError::InvalidCableNumber(raw_cable_number)) + }; + + let message = Message::try_from(&value[1..])?; + + Ok(UsbMidiEventPacket { + cable_number, + message + }) + } +} + impl UsbMidiEventPacket{ pub fn from_midi(cable:CableNumber, midi:Message) diff --git a/src/midi_device.rs b/src/midi_device.rs index 9693508..00dcea4 100644 --- a/src/midi_device.rs +++ b/src/midi_device.rs @@ -1,21 +1,29 @@ use usb_device::class_prelude::*; use usb_device::Result; use crate::data::usb::constants::*; -use crate::data::usb_midi::usb_midi_event_packet::UsbMidiEventPacket; +use crate::data::usb_midi::usb_midi_event_packet::{UsbMidiEventPacket, MidiPacketParsingError}; +use core::convert::TryFrom; -//const MIDI_IN_SIZE: u8 = 0x06; +const MIDI_IN_SIZE: u8 = 0x06; const MIDI_OUT_SIZE: u8 = 0x09; +pub const MIDI_PACKET_SIZE: usize = 4; +pub const MAX_PACKET_SIZE: usize = 64; + ///Note we are using MidiIn here to refer to the fact that ///The Host sees it as a midi in device ///This class allows you to send data in pub struct MidiClass<'a,B: UsbBus> { standard_ac: InterfaceNumber, standard_mc: InterfaceNumber, - //standard_bulkout: EndpointOut<'a, B>, + standard_bulkout: EndpointOut<'a, B>, standard_bulkin: EndpointIn<'a,B> } +pub enum MidiReadError { + ParsingFailed(MidiPacketParsingError), + UsbError(UsbError) +} impl MidiClass<'_, B> { /// Creates a new MidiClass with the provided UsbBus @@ -23,16 +31,19 @@ impl MidiClass<'_, B> { MidiClass { standard_ac: alloc.interface(), standard_mc: alloc.interface(), - //standard_bulkout : alloc.bulk(64), - standard_bulkin: alloc.bulk(64) + standard_bulkout : alloc.bulk(MAX_PACKET_SIZE as u16), + standard_bulkin: alloc.bulk(MAX_PACKET_SIZE as u16) } } pub fn send_message(&mut self, usb_midi:UsbMidiEventPacket) -> Result { - let bytes : [u8;4] = usb_midi.into(); + let bytes : [u8;MIDI_PACKET_SIZE] = usb_midi.into(); self.standard_bulkin.write(&bytes) } + pub fn read(&mut self, buffer: &mut [u8]) -> Result { + self.standard_bulkout.read(buffer) + } } impl UsbClass for MidiClass<'_, B> { @@ -82,7 +93,7 @@ impl UsbClass for MidiClass<'_, B> { //JACKS -/* writer.write( + writer.write( CS_INTERFACE, &[ MIDI_IN_JACK_SUBTYPE, @@ -90,7 +101,7 @@ impl UsbClass for MidiClass<'_, B> { 0x01, // id 0x00 ] - )?; */ + )?; writer.write ( CS_INTERFACE, @@ -105,7 +116,7 @@ impl UsbClass for MidiClass<'_, B> { ] )?; -/* writer.endpoint(&self.standard_bulkout)?; + writer.endpoint(&self.standard_bulkout)?; writer.write( CS_ENDPOINT, @@ -114,7 +125,7 @@ impl UsbClass for MidiClass<'_, B> { 0x01, 0x01 ] - )?; */ + )?; writer.endpoint(&self.standard_bulkin)?; From 9db505d659ef8149123a46427eb7b26449786517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patric=20Kanngie=C3=9Fer?= Date: Sat, 26 Dec 2020 16:16:32 +0100 Subject: [PATCH 2/7] Implement receive for missing event types and added ControlChange --- src/data/midi/message/message.rs | 48 ++++++++++++++++++-------- src/data/usb_midi/code_index_number.rs | 3 +- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/data/midi/message/message.rs b/src/data/midi/message/message.rs index 44dcc16..0be2441 100644 --- a/src/data/midi/message/message.rs +++ b/src/data/midi/message/message.rs @@ -8,6 +8,7 @@ use core::convert::TryFrom; use crate::data::usb_midi::usb_midi_event_packet::MidiPacketParsingError; type Velocity = U7; +type ControllerNumber = U7; /// Represents midi messages /// Note: not current exhaustive and SysEx messages end up @@ -20,15 +21,17 @@ pub enum Message { PolyphonicAftertouch(Channel,Note,U7), ProgramChange(Channel,U7), ChannelAftertouch(Channel,U7), - PitchWheelChange(Channel,U7,U7) + PitchWheelChange(Channel,U7,U7), + ControlChange(Channel, ControllerNumber, U7) } -const NOTE_OFF_MASK :u8 = 0b1000_0000; -const NOTE_ON_MASK :u8 = 0b1001_0000; -const POLYPHONIC_MASK :u8 = 0b1010_0000; -const PROGRAM_MASK :u8 = 0b1100_0000; -const CHANNEL_AFTERTOUCH_MASK :u8 = 0b1101_0000; -const PITCH_BEND_MASK :u8 = 0b1110_0000; +const NOTE_OFF_MASK :u8 = 0b1000_0000; +const NOTE_ON_MASK :u8 = 0b1001_0000; +const POLYPHONIC_MASK :u8 = 0b1010_0000; +const PROGRAM_MASK :u8 = 0b1100_0000; +const CHANNEL_AFTERTOUCH_MASK :u8 = 0b1101_0000; +const PITCH_BEND_MASK :u8 = 0b1110_0000; +const CONTROL_CHANGE_MASK :u8 = 0b1011_0000; impl From for Raw { fn from(value:Message) -> Raw { @@ -62,8 +65,12 @@ impl From for Raw { let payload = Payload::DoubleByte(lsb,msb); let status = PITCH_BEND_MASK | u8::from(chan); Raw {status , payload} + }, + Message::ControlChange(chan, controller_number, value) => { + let payload = Payload::DoubleByte(controller_number, value); + let status = CONTROL_CHANGE_MASK | u8::from(chan); + Raw {status, payload} } - } } } @@ -78,21 +85,34 @@ impl<'a> TryFrom<&'a [u8]> for Message { let channel = Channel::try_from(channel_bytes).ok().unwrap(); match event_type { - NOTE_OFF_MASK => Ok(Message::NoteOff(channel, get_note(data)?, get_velocity(data))), - NOTE_ON_MASK => Ok(Message::NoteOn(channel, get_note(data)?, get_velocity(data))), + NOTE_ON_MASK => Ok(Message::NoteOn(channel, get_note(data)?, get_u7_at(data, 2)?)), + NOTE_OFF_MASK => Ok(Message::NoteOff(channel, get_note(data)?, get_u7_at(data, 2)?)), + POLYPHONIC_MASK => Ok(Message::PolyphonicAftertouch(channel, get_note(data)?, get_u7_at(data, 2)?)), + PROGRAM_MASK => Ok(Message::ProgramChange(channel, get_u7_at(data, 1)?)), + CHANNEL_AFTERTOUCH_MASK => Ok(Message::ChannelAftertouch(channel, get_u7_at(data, 1)?)), + PITCH_BEND_MASK => Ok(Message::PitchWheelChange(channel, get_u7_at(data, 1)?, get_u7_at(data, 2)?)), + CONTROL_CHANGE_MASK => Ok(Message::ControlChange(channel, get_u7_at(data, 1)?, get_u7_at(data, 2)?)), _ => Err(MidiPacketParsingError::InvalidEventType(event_type)) } } } fn get_note(data: &[u8]) -> Result { - let note_byte = data[1]; + let note_byte = get_byte_at_position(data, 1)?; match Note::try_from(note_byte) { Ok(note) => Ok(note), Err(_) => Err(MidiPacketParsingError::InvalidNote(note_byte)) } } -fn get_velocity(data: &[u8]) -> U7 { - U7::from_clamped(data[2]) -} \ No newline at end of file +fn get_u7_at(data: &[u8], index: usize) -> Result { + let data_byte = get_byte_at_position(data, index)?; + Ok(U7::from_clamped(data_byte)) +} + +fn get_byte_at_position(data: &[u8], index: usize) -> Result { + match data.get(index) { + Some(byte) => Ok(*byte), + None => Err(MidiPacketParsingError::MissingDataPacket) + } +} diff --git a/src/data/usb_midi/code_index_number.rs b/src/data/usb_midi/code_index_number.rs index 221699a..2e144ea 100644 --- a/src/data/usb_midi/code_index_number.rs +++ b/src/data/usb_midi/code_index_number.rs @@ -71,7 +71,8 @@ impl CodeIndexNumber { Message::ChannelAftertouch(_,_) => CodeIndexNumber::CHANNEL_PRESSURE, Message::PitchWheelChange(_,_,_) => CodeIndexNumber::PITCHBEND_CHANGE, Message::PolyphonicAftertouch(_,_,_) => CodeIndexNumber::POLY_KEYPRESS, - Message::ProgramChange(_,_) => CodeIndexNumber::PROGRAM_CHANGE + Message::ProgramChange(_,_) => CodeIndexNumber::PROGRAM_CHANGE, + Message::ControlChange(_,_,_) => CodeIndexNumber::CONTROL_CHANGE } } } From 487a379633a737430215ea8d67a47b77b29a1bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patric=20Kanngie=C3=9Fer?= Date: Sun, 27 Dec 2020 19:29:47 +0100 Subject: [PATCH 3/7] Add ControlFunction struct and add constants to enable pattern matching on CC --- src/data/byte/u7.rs | 4 +- src/data/midi/message/control_function.rs | 137 ++++++++++++++++++++++ src/data/midi/message/message.rs | 10 +- src/data/midi/message/mod.rs | 3 +- src/data/usb_midi/midi_packet_reader.rs | 4 +- 5 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 src/data/midi/message/control_function.rs diff --git a/src/data/byte/u7.rs b/src/data/byte/u7.rs index bac83ae..779a038 100644 --- a/src/data/byte/u7.rs +++ b/src/data/byte/u7.rs @@ -2,8 +2,8 @@ use core::convert::TryFrom; use crate::data::byte::from_traits::{FromOverFlow,FromClamped}; /// A primitive value that can be from 0-0x7F -#[derive(Debug)] -pub struct U7(u8); +#[derive(Debug, Eq, PartialEq)] +pub struct U7(pub(crate)u8); /// Error representing that this value is not a valid u7 pub struct InvalidU7(u8); diff --git a/src/data/midi/message/control_function.rs b/src/data/midi/message/control_function.rs new file mode 100644 index 0000000..c40dde5 --- /dev/null +++ b/src/data/midi/message/control_function.rs @@ -0,0 +1,137 @@ +use crate::data::byte::u7::U7; + +#[derive(Debug, Eq, PartialEq)] +pub struct ControlFunction(pub U7); + +/// Control Functions as defined in the MIDI 1.0 Specification +/// Source: https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2 +impl ControlFunction { + pub const BANK_SELECT_0: Self = ControlFunction(U7(0)); + pub const MOD_WHEEL_1: Self = ControlFunction(U7(1)); + pub const BREATH_CONTROLLER_2: Self = ControlFunction(U7(2)); + pub const UNDEFINED_3: Self = ControlFunction(U7(3)); + pub const FOOT_CONTROLLER_4: Self = ControlFunction(U7(4)); + pub const PORTAMENTO_TIME_5: Self = ControlFunction(U7(5)); + pub const DATA_ENTRY_MSB_6: Self = ControlFunction(U7(6)); + pub const CHANNEL_VOLUME_7: Self = ControlFunction(U7(7)); + pub const BALANCE_8: Self = ControlFunction(U7(8)); + pub const UNDEFINED_9: Self = ControlFunction(U7(9)); + pub const PAN_10: Self = ControlFunction(U7(10)); + pub const EXPRESSION_CONTROLLER_11: Self = ControlFunction(U7(11)); + pub const EFFECT_CONTROL_1_12: Self = ControlFunction(U7(12)); + pub const EFFECT_CONTROL_2_13: Self = ControlFunction(U7(13)); + pub const UNDEFINED_14: Self = ControlFunction(U7(14)); + pub const UNDEFINED_15: Self = ControlFunction(U7(15)); + pub const GENERAL_PURPOSE_CONTROLLER_1_16: Self = ControlFunction(U7(16)); + pub const GENERAL_PURPOSE_CONTROLLER_2_17: Self = ControlFunction(U7(17)); + pub const GENERAL_PURPOSE_CONTROLLER_3_18: Self = ControlFunction(U7(18)); + pub const GENERAL_PURPOSE_CONTROLLER_4_19: Self = ControlFunction(U7(19)); + pub const UNDEFINED_20: Self = ControlFunction(U7(20)); + pub const UNDEFINED_21: Self = ControlFunction(U7(21)); + pub const UNDEFINED_22: Self = ControlFunction(U7(22)); + pub const UNDEFINED_23: Self = ControlFunction(U7(23)); + pub const UNDEFINED_24: Self = ControlFunction(U7(24)); + pub const UNDEFINED_25: Self = ControlFunction(U7(25)); + pub const UNDEFINED_26: Self = ControlFunction(U7(26)); + pub const UNDEFINED_27: Self = ControlFunction(U7(27)); + pub const UNDEFINED_28: Self = ControlFunction(U7(28)); + pub const UNDEFINED_29: Self = ControlFunction(U7(29)); + pub const UNDEFINED_30: Self = ControlFunction(U7(30)); + pub const UNDEFINED_31: Self = ControlFunction(U7(31)); + pub const LSB_FOR_BANK_SELECT_32: Self = ControlFunction(U7(32)); + pub const LSB_FOR_MOD_WHEEL_33: Self = ControlFunction(U7(33)); + pub const LSB_FOR_BREATH_CONTROLLER_34: Self = ControlFunction(U7(34)); + pub const LSB_FOR_UNDEFINED_35: Self = ControlFunction(U7(35)); + pub const LSB_FOR_FOOT_CONTROLLER_36: Self = ControlFunction(U7(36)); + pub const LSB_FOR_PORTAMENTO_TIME_37: Self = ControlFunction(U7(37)); + pub const LSB_FOR_DATA_ENTRY_MSB_38: Self = ControlFunction(U7(38)); + pub const LSB_FOR_CHANNEL_VOLUME_39: Self = ControlFunction(U7(39)); + pub const LSB_FOR_BALANCE_40: Self = ControlFunction(U7(40)); + pub const LSB_FOR_UNDEFINED_41: Self = ControlFunction(U7(41)); + pub const LSB_FOR_PAN_42: Self = ControlFunction(U7(42)); + pub const LSB_FOR_EXPRESSION_CONTROLLER_43: Self = ControlFunction(U7(43)); + pub const LSB_FOR_EFFECT_CONTROL_1_44: Self = ControlFunction(U7(44)); + pub const LSB_FOR_EFFECT_CONTROL_2_45: Self = ControlFunction(U7(45)); + pub const LSB_FOR_UNDEFINED_14_46: Self = ControlFunction(U7(46)); + pub const LSB_FOR_UNDEFINED_15_47: Self = ControlFunction(U7(47)); + pub const LSB_FOR_GENERAL_PURPOSE_CONTROLLER_1_48: Self = ControlFunction(U7(48)); + pub const LSB_FOR_GENERAL_PURPOSE_CONTROLLER_2_49: Self = ControlFunction(U7(49)); + pub const LSB_FOR_GENERAL_PURPOSE_CONTROLLER_3_50: Self = ControlFunction(U7(50)); + pub const LSB_FOR_GENERAL_PURPOSE_CONTROLLER_4_51: Self = ControlFunction(U7(51)); + pub const LSB_FOR_UNDEFINED_20_52: Self = ControlFunction(U7(52)); + pub const LSB_FOR_UNDEFINED_21_53: Self = ControlFunction(U7(53)); + pub const LSB_FOR_UNDEFINED_22_54: Self = ControlFunction(U7(54)); + pub const LSB_FOR_UNDEFINED_23_55: Self = ControlFunction(U7(55)); + pub const LSB_FOR_UNDEFINED_24_56: Self = ControlFunction(U7(56)); + pub const LSB_FOR_UNDEFINED_25_57: Self = ControlFunction(U7(57)); + pub const LSB_FOR_UNDEFINED_26_58: Self = ControlFunction(U7(58)); + pub const LSB_FOR_UNDEFINED_27_59: Self = ControlFunction(U7(59)); + pub const LSB_FOR_UNDEFINED_28_60: Self = ControlFunction(U7(60)); + pub const LSB_FOR_UNDEFINED_29_61: Self = ControlFunction(U7(61)); + pub const LSB_FOR_UNDEFINED_30_62: Self = ControlFunction(U7(62)); + pub const LSB_FOR_UNDEFINED_31_63: Self = ControlFunction(U7(63)); + pub const DAMPER_PEDAL_ON_OFF_64: Self = ControlFunction(U7(64)); + pub const PORTAMENTO_ON_OFF_65: Self = ControlFunction(U7(65)); + pub const SOSTENUTO_ON_OFF_66: Self = ControlFunction(U7(66)); + pub const SOFT_PEDAL_ON_OFF_67: Self = ControlFunction(U7(67)); + pub const LEGATO_FOOTSWITCH_68: Self = ControlFunction(U7(68)); + pub const HOLD_2_69: Self = ControlFunction(U7(69)); + pub const SOUND_CONTROLLER_1_70: Self = ControlFunction(U7(70)); + pub const SOUND_CONTROLLER_2_71: Self = ControlFunction(U7(71)); + pub const SOUND_CONTROLLER_3_72: Self = ControlFunction(U7(72)); + pub const SOUND_CONTROLLER_4_73: Self = ControlFunction(U7(73)); + pub const SOUND_CONTROLLER_5_74: Self = ControlFunction(U7(74)); + pub const SOUND_CONTROLLER_6_75: Self = ControlFunction(U7(75)); + pub const SOUND_CONTROLLER_7_76: Self = ControlFunction(U7(76)); + pub const SOUND_CONTROLLER_8_77: Self = ControlFunction(U7(77)); + pub const SOUND_CONTROLLER_9_78: Self = ControlFunction(U7(78)); + pub const SOUND_CONTROLLER_10_79: Self = ControlFunction(U7(79)); + pub const GENERAL_PURPOSE_CONTROLLER_5_80: Self = ControlFunction(U7(80)); + pub const GENERAL_PURPOSE_CONTROLLER_6_81: Self = ControlFunction(U7(81)); + pub const GENERAL_PURPOSE_CONTROLLER_7_82: Self = ControlFunction(U7(82)); + pub const GENERAL_PURPOSE_CONTROLLER_8_83: Self = ControlFunction(U7(83)); + pub const PORTAMENTO_CONTROL_84: Self = ControlFunction(U7(84)); + pub const UNDEFINED_85: Self = ControlFunction(U7(85)); + pub const UNDEFINED_86: Self = ControlFunction(U7(86)); + pub const UNDEFINED_87: Self = ControlFunction(U7(87)); + pub const HIGH_RESOLUTION_VELOCITY_PREFIX_88: Self = ControlFunction(U7(88)); + pub const UNDEFINED_89: Self = ControlFunction(U7(89)); + pub const UNDEFINED_90: Self = ControlFunction(U7(90)); + pub const EFFECTS_1_DEPTH_91: Self = ControlFunction(U7(91)); + pub const EFFECTS_2_DEPTH_92: Self = ControlFunction(U7(92)); + pub const EFFECTS_3_DEPTH_93: Self = ControlFunction(U7(93)); + pub const EFFECTS_4_DEPTH_94: Self = ControlFunction(U7(94)); + pub const EFFECTS_5_DEPTH_95: Self = ControlFunction(U7(95)); + pub const DATA_INCREMENT_96: Self = ControlFunction(U7(96)); + pub const DATA_DECREMENT_97: Self = ControlFunction(U7(97)); + pub const NPRN_LSB_98: Self = ControlFunction(U7(98)); + pub const NPRN_MSB_99: Self = ControlFunction(U7(99)); + pub const RPN_LSB_100: Self = ControlFunction(U7(100)); + pub const UNDEFINED_101: Self = ControlFunction(U7(101)); + pub const UNDEFINED_102: Self = ControlFunction(U7(102)); + pub const UNDEFINED_103: Self = ControlFunction(U7(103)); + pub const UNDEFINED_104: Self = ControlFunction(U7(104)); + pub const UNDEFINED_105: Self = ControlFunction(U7(105)); + pub const UNDEFINED_106: Self = ControlFunction(U7(106)); + pub const UNDEFINED_107: Self = ControlFunction(U7(107)); + pub const UNDEFINED_108: Self = ControlFunction(U7(108)); + pub const UNDEFINED_109: Self = ControlFunction(U7(109)); + pub const UNDEFINED_110: Self = ControlFunction(U7(110)); + pub const UNDEFINED_111: Self = ControlFunction(U7(111)); + pub const UNDEFINED_112: Self = ControlFunction(U7(112)); + pub const UNDEFINED_113: Self = ControlFunction(U7(113)); + pub const UNDEFINED_114: Self = ControlFunction(U7(114)); + pub const UNDEFINED_115: Self = ControlFunction(U7(115)); + pub const UNDEFINED_116: Self = ControlFunction(U7(116)); + pub const UNDEFINED_117: Self = ControlFunction(U7(117)); + pub const UNDEFINED_118: Self = ControlFunction(U7(118)); + pub const UNDEFINED_119: Self = ControlFunction(U7(119)); + pub const ALL_SOUND_OFF_120: Self = ControlFunction(U7(120)); + pub const RESET_ALL_CONTROLLERS_121: Self = ControlFunction(U7(121)); + pub const LOCAL_CONTROL_OFF_122: Self = ControlFunction(U7(122)); + pub const ALL_NOTES_OFF_123: Self = ControlFunction(U7(123)); + pub const OMNI_MODE_OFF_124: Self = ControlFunction(U7(124)); + pub const OMNI_MODE_ON_125: Self = ControlFunction(U7(125)); + pub const MONO_MODE_ON_126: Self = ControlFunction(U7(126)); + pub const POLY_MODE_ON_127: Self = ControlFunction(U7(127)); +} \ No newline at end of file diff --git a/src/data/midi/message/message.rs b/src/data/midi/message/message.rs index 0be2441..42377bd 100644 --- a/src/data/midi/message/message.rs +++ b/src/data/midi/message/message.rs @@ -6,9 +6,9 @@ use crate::data::midi::message::raw::{Raw,Payload}; use crate::data::byte::from_traits::FromClamped; use core::convert::TryFrom; use crate::data::usb_midi::usb_midi_event_packet::MidiPacketParsingError; +use crate::data::midi::message::control_function::ControlFunction; type Velocity = U7; -type ControllerNumber = U7; /// Represents midi messages /// Note: not current exhaustive and SysEx messages end up @@ -22,7 +22,7 @@ pub enum Message { ProgramChange(Channel,U7), ChannelAftertouch(Channel,U7), PitchWheelChange(Channel,U7,U7), - ControlChange(Channel, ControllerNumber, U7) + ControlChange(Channel,ControlFunction,U7) } const NOTE_OFF_MASK :u8 = 0b1000_0000; @@ -66,8 +66,8 @@ impl From for Raw { let status = PITCH_BEND_MASK | u8::from(chan); Raw {status , payload} }, - Message::ControlChange(chan, controller_number, value) => { - let payload = Payload::DoubleByte(controller_number, value); + Message::ControlChange(chan, control_function, value) => { + let payload = Payload::DoubleByte(control_function.0, value); let status = CONTROL_CHANGE_MASK | u8::from(chan); Raw {status, payload} } @@ -91,7 +91,7 @@ impl<'a> TryFrom<&'a [u8]> for Message { PROGRAM_MASK => Ok(Message::ProgramChange(channel, get_u7_at(data, 1)?)), CHANNEL_AFTERTOUCH_MASK => Ok(Message::ChannelAftertouch(channel, get_u7_at(data, 1)?)), PITCH_BEND_MASK => Ok(Message::PitchWheelChange(channel, get_u7_at(data, 1)?, get_u7_at(data, 2)?)), - CONTROL_CHANGE_MASK => Ok(Message::ControlChange(channel, get_u7_at(data, 1)?, get_u7_at(data, 2)?)), + CONTROL_CHANGE_MASK => Ok(Message::ControlChange(channel, ControlFunction(get_u7_at(data, 1)?), get_u7_at(data, 2)?)), _ => Err(MidiPacketParsingError::InvalidEventType(event_type)) } } diff --git a/src/data/midi/message/mod.rs b/src/data/midi/message/mod.rs index bb7b6df..ad4b900 100644 --- a/src/data/midi/message/mod.rs +++ b/src/data/midi/message/mod.rs @@ -1,3 +1,4 @@ pub mod message; pub mod raw; -pub use crate::data::midi::message::message::{Message}; \ No newline at end of file +pub use crate::data::midi::message::message::{Message}; +pub mod control_function; diff --git a/src/data/usb_midi/midi_packet_reader.rs b/src/data/usb_midi/midi_packet_reader.rs index e3d3ebc..b9149b1 100644 --- a/src/data/usb_midi/midi_packet_reader.rs +++ b/src/data/usb_midi/midi_packet_reader.rs @@ -22,8 +22,8 @@ impl<'a> Iterator for MidiPacketBufferReader<'a> { type Item = Result; fn next(&mut self) -> Option { - if self.position <= MAX_PACKET_SIZE && self.position < self.raw_bytes_received { - let packet = match self.buffer.get(self.position .. (self.position + 4)) { + if self.position <= MAX_PACKET_SIZE && self.position < self.raw_bytes_received { + let packet = match self.buffer.get(self.position .. (self.position + MIDI_PACKET_SIZE)) { Some(packet) => Some(UsbMidiEventPacket::try_from(packet)), None => None }; From 96f5135427fab997fc1d883072daebe907c91404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patric=20Kanngie=C3=9Fer?= Date: Tue, 29 Dec 2020 18:05:20 +0100 Subject: [PATCH 4/7] Add safety checks for parsing --- src/data/midi/message/message.rs | 6 +++++- src/data/usb_midi/usb_midi_event_packet.rs | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/data/midi/message/message.rs b/src/data/midi/message/message.rs index 42377bd..3125e8d 100644 --- a/src/data/midi/message/message.rs +++ b/src/data/midi/message/message.rs @@ -78,7 +78,11 @@ impl From for Raw { impl<'a> TryFrom<&'a [u8]> for Message { type Error = MidiPacketParsingError; fn try_from(data: &[u8]) -> Result { - let status_byte = data[0]; + let status_byte = match data.get(0) { + Some(byte) => byte, + None => return Err(MidiPacketParsingError::MissingDataPacket) + }; + let event_type = status_byte & 0b1111_0000; let channel_bytes = (status_byte) & 0b0000_1111; diff --git a/src/data/usb_midi/usb_midi_event_packet.rs b/src/data/usb_midi/usb_midi_event_packet.rs index e8587fe..815632f 100644 --- a/src/data/usb_midi/usb_midi_event_packet.rs +++ b/src/data/usb_midi/usb_midi_event_packet.rs @@ -56,14 +56,22 @@ impl TryFrom<&[u8]> for UsbMidiEventPacket { type Error = MidiPacketParsingError; fn try_from(value: &[u8]) -> Result { - let raw_cable_number= value[0] >> 4; + let raw_cable_number= match value.get(0) { + Some(byte) => *byte >> 4, + None => return Err(MidiPacketParsingError::MissingDataPacket) + }; let cable_number = match CableNumber::try_from(u8::from(raw_cable_number)) { Ok(val) => val, _ => return Err(MidiPacketParsingError::InvalidCableNumber(raw_cable_number)) }; - let message = Message::try_from(&value[1..])?; + let message_body = match value.get(1..) { + Some(bytes) => bytes, + None => return Err(MidiPacketParsingError::MissingDataPacket) + }; + + let message = Message::try_from(message_body)?; Ok(UsbMidiEventPacket { cable_number, From 0223763ed00b266ce7b622371e2ec6b996e9eba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patric=20Kanngie=C3=9Fer?= Date: Tue, 29 Dec 2020 18:10:17 +0100 Subject: [PATCH 5/7] Cleanup --- src/data/usb_midi/usb_midi_event_packet.rs | 5 ----- src/midi_device.rs | 1 - 2 files changed, 6 deletions(-) diff --git a/src/data/usb_midi/usb_midi_event_packet.rs b/src/data/usb_midi/usb_midi_event_packet.rs index 815632f..a3cd807 100644 --- a/src/data/usb_midi/usb_midi_event_packet.rs +++ b/src/data/usb_midi/usb_midi_event_packet.rs @@ -3,12 +3,7 @@ use crate::data::usb_midi::code_index_number::CodeIndexNumber; use crate::data::midi::message::Message; use crate::data::byte::u4::U4; use crate::data::midi::message::raw::{Payload,Raw}; -use crate::data::byte::u4; use core::convert::TryFrom; -use crate::data::midi::channel::Channel::Channel1; -use crate::data::midi::notes::Note::C7; -use crate::data::byte::u7::U7; -use crate::data::byte::from_traits::FromClamped; /// A packet that communicates with the host diff --git a/src/midi_device.rs b/src/midi_device.rs index 00dcea4..44d7536 100644 --- a/src/midi_device.rs +++ b/src/midi_device.rs @@ -2,7 +2,6 @@ use usb_device::class_prelude::*; use usb_device::Result; use crate::data::usb::constants::*; use crate::data::usb_midi::usb_midi_event_packet::{UsbMidiEventPacket, MidiPacketParsingError}; -use core::convert::TryFrom; const MIDI_IN_SIZE: u8 = 0x06; const MIDI_OUT_SIZE: u8 = 0x09; From 3d3ff682991d481d9cbcd49c5086198e9ea6b69d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patric=20Kanngie=C3=9Fer?= Date: Wed, 30 Dec 2020 00:39:27 +0100 Subject: [PATCH 6/7] Add tests for message decoding --- src/data/midi/channel.rs | 2 +- src/data/midi/message/message.rs | 2 +- src/data/midi/notes.rs | 2 +- src/data/usb_midi/cable_number.rs | 2 +- src/data/usb_midi/usb_midi_event_packet.rs | 58 +++++++++++++++++++++- 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/data/midi/channel.rs b/src/data/midi/channel.rs index 49ed33d..cbf7d1a 100644 --- a/src/data/midi/channel.rs +++ b/src/data/midi/channel.rs @@ -3,7 +3,7 @@ use core::convert::TryFrom; /// The Channel is a value ranging from 0x0 to 0xF /// This is a standard midi concept /// Note Channel1 = 0 on the wire -#[derive(Debug,Copy,Clone)] +#[derive(Debug,Copy,Clone, Eq, PartialEq)] #[repr(u8)] pub enum Channel { Channel1 = 0x0, Channel2 = 0x1, Channel3 = 0x2, Channel4 = 0x3, diff --git a/src/data/midi/message/message.rs b/src/data/midi/message/message.rs index 3125e8d..bd95061 100644 --- a/src/data/midi/message/message.rs +++ b/src/data/midi/message/message.rs @@ -14,7 +14,7 @@ type Velocity = U7; /// Note: not current exhaustive and SysEx messages end up /// being a confusing case. So are currently note implemented /// they are sort-of unbounded -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub enum Message { NoteOff(Channel,Note,Velocity), NoteOn(Channel,Note,Velocity), diff --git a/src/data/midi/notes.rs b/src/data/midi/notes.rs index 2273eea..cca582e 100644 --- a/src/data/midi/notes.rs +++ b/src/data/midi/notes.rs @@ -5,7 +5,7 @@ use num_enum::TryFromPrimitive; /// note the flat versions are associated constants /// but can be referenced like Note::Bb3 /// C1m is the C-1 -#[derive(Debug,Copy,Clone,TryFromPrimitive)] +#[derive(Debug,Copy,Clone,TryFromPrimitive,Eq,PartialEq)] #[repr(u8)] pub enum Note { C1m, Cs1m, D1m, Ds1m, E1m, F1m, Fs1m, G1m, Gs1m, A1m, As1m, B1m, diff --git a/src/data/usb_midi/cable_number.rs b/src/data/usb_midi/cable_number.rs index 1fff8a3..39500fa 100644 --- a/src/data/usb_midi/cable_number.rs +++ b/src/data/usb_midi/cable_number.rs @@ -4,7 +4,7 @@ use crate::data::byte::u4::U4; /// The Cable Number (CN) is a value ranging from 0x0 to 0xF /// indicating the number assignment of the Embedded MIDI Jack associated /// with the endpoint that is transferring the data -#[derive(Debug,Clone,Copy)] +#[derive(Debug,Clone,Copy,Eq, PartialEq)] #[repr(u8)] pub enum CableNumber { Cable0 = 0x0, Cable1 = 0x1, Cable2 = 0x2, Cable3 = 0x3, diff --git a/src/data/usb_midi/usb_midi_event_packet.rs b/src/data/usb_midi/usb_midi_event_packet.rs index a3cd807..5dcb14b 100644 --- a/src/data/usb_midi/usb_midi_event_packet.rs +++ b/src/data/usb_midi/usb_midi_event_packet.rs @@ -9,7 +9,7 @@ use core::convert::TryFrom; /// A packet that communicates with the host /// Currently supported is sending the specified normal midi /// message over the supplied cable number -#[derive(Debug)] +#[derive(Debug,Eq, PartialEq)] pub struct UsbMidiEventPacket { pub cable_number : CableNumber, pub message: Message @@ -84,4 +84,60 @@ impl UsbMidiEventPacket{ message : midi } } +} + +#[cfg(test)] +mod tests { + use core::convert::TryFrom; + use crate::data::usb_midi::usb_midi_event_packet::UsbMidiEventPacket; + use crate::data::midi::channel::Channel::{Channel1, Channel2}; + use crate::data::midi::notes::Note; + use crate::data::byte::u7::U7; + use crate::data::midi::message::Message; + use crate::data::usb_midi::cable_number::CableNumber::{Cable0,Cable1}; + use crate::data::midi::message::control_function::ControlFunction; + + macro_rules! decode_message_test { + ($($id:ident:$value:expr,)*) => { + $( + #[test] + fn $id() { + let (usb_midi_data_packet,expected) = $value; + let message = UsbMidiEventPacket::try_from(&usb_midi_data_packet[..]).unwrap(); + assert_eq!(expected, message); + } + )* + } + } + + decode_message_test! { + note_on: ([9, 144, 36, 127], UsbMidiEventPacket { + cable_number: Cable0, + message: Message::NoteOn(Channel1, Note::C2, U7(127)) + }), + note_off: ([8, 128, 36, 0], UsbMidiEventPacket { + cable_number: Cable0, + message: Message::NoteOff(Channel1, Note::C2, U7(0)) + }), + polyphonic_aftertouch: ([10, 160, 36, 64], UsbMidiEventPacket { + cable_number: Cable0, + message: Message::PolyphonicAftertouch(Channel1, Note::C2, U7(64)) + }), + program_change: ([28, 192, 127, 0], UsbMidiEventPacket { + cable_number: Cable1, + message: Message::ProgramChange(Channel1, U7(127)) + }), + channel_aftertouch: ([13, 208, 127, 0], UsbMidiEventPacket { + cable_number: Cable0, + message: Message::ChannelAftertouch(Channel1, U7(127)) + }), + pitch_wheel: ([14, 224, 64, 32], UsbMidiEventPacket { + cable_number: Cable0, + message: Message::PitchWheelChange(Channel1, U7(64), U7(32)) + }), + control_change: ([11, 177, 1, 32], UsbMidiEventPacket { + cable_number: Cable0, + message: Message::ControlChange(Channel2, ControlFunction::MOD_WHEEL_1, U7(32)) + }), + } } \ No newline at end of file From f7515b08f70558f2134aae0929f432ae9983e937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patric=20Kanngie=C3=9Fer?= Date: Wed, 30 Dec 2020 02:04:00 +0100 Subject: [PATCH 7/7] Add example to README --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ff5cf7e..df9a38b 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,68 @@ usbd-midi A simple usb midi device class for [usb-device](https://crates.io/crates/usb-device). Currently this aims to be a very simple implementation, that allows the micro -controller to send midi information to the PC. +controller to send MIDI information to the PC and also receive MIDI information. This crate requires the use of a hardware driver, that implements the usb-device traits. +## Example + +### Receive MIDI +Turn on the integrated LED of a STM32 BluePill board as long as C2 is pressed +```rust +fn main() -> ! { + let dp = pac::Peripherals::take().unwrap(); + + let mut rcc = dp.RCC.constrain(); + + let mut gpioa = dp.GPIOA.split(&mut rcc.apb2); + let mut gpioc = dp.GPIOC.split(&mut rcc.apb2); + + let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh); + led.set_high().unwrap(); + + let mut usb_dp = gpioa.pa12.into_push_pull_output(&mut gpioa.crh); + + let usb = Peripheral { + usb: dp.USB, + pin_dm: gpioa.pa11, + pin_dp: usb_dp.into_floating_input(&mut gpioa.crh), + }; + + let usb_bus = UsbBus::new(usb); + + let mut midi = MidiClass::new(&usb_bus); + + let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x5e4)) + .product("MIDI Test") + .device_class(USB_AUDIO_CLASS) + .device_sub_class(USB_MIDISTREAMING_SUBCLASS) + .build(); + + loop { + if !usb_dev.poll(&mut [&mut midi]) { + continue; + } + + let mut buffer = [0; 64]; + + if let Ok(size) = midi.read(&mut buffer) { + let buffer_reader = MidiPacketBufferReader::new(&buffer, size); + for packet in buffer_reader.into_iter() { + if let Ok(packet) = packet { + match packet.message { + Message::NoteOn(Channel1, Note::C2, ..) => { + led.set_low().unwrap(); + }, + Message::NoteOff(Channel1, Note::C2, ..) => { + led.set_high().unwrap(); + }, + _ => {} + } + } + } + } + } +} +``` \ No newline at end of file