From 9536e46d87153fc2fe5139a9d722141ceaf3a38f Mon Sep 17 00:00:00 2001 From: Huck Boles Date: Fri, 23 Jun 2023 17:59:01 -0500 Subject: [PATCH] added: midi messages and channels --- Cargo.lock | 29 ++++++++++ Cargo.toml | 2 + src/lib.rs | 22 +++++--- src/main.rs | 26 ++++++++- src/map.rs | 9 ++-- src/mask.rs | 13 ++--- src/midi.rs | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 235 insertions(+), 19 deletions(-) create mode 100644 src/midi.rs diff --git a/Cargo.lock b/Cargo.lock index d11294c..fe00ab3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,8 +209,10 @@ dependencies = [ "eyre", "iced", "itertools", + "jack", "rand", "rustc-hash", + "thiserror", "tokio", ] @@ -1168,6 +1170,33 @@ dependencies = [ "either", ] +[[package]] +name = "jack" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e5a18a3c2aefb354fb77111ade228b20267bdc779de84e7a4ccf7ea96b9a6cd" +dependencies = [ + "bitflags", + "jack-sys", + "lazy_static", + "libc", + "log", +] + +[[package]] +name = "jack-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6013b7619b95a22b576dfb43296faa4ecbe40abbdb97dfd22ead520775fc86ab" +dependencies = [ + "bitflags", + "lazy_static", + "libc", + "libloading 0.7.4", + "log", + "pkg-config", +] + [[package]] name = "jni-sys" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 0291f17..b74337b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,10 @@ authors = ["Huck Boles "] [dependencies] rand = "0.8" eyre = "0.6" +thiserror = "1.0" tokio = { version = "1", features = ["full"] } array2d = "0.3.0" iced = { version = "0.9", features = ["canvas", "tokio", "debug"] } itertools = "0.10" rustc-hash = "1.1" +jack = "0.11" diff --git a/src/lib.rs b/src/lib.rs index ac87d2b..8737153 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,22 @@ -use iced::executor; -use iced::theme::{self, Theme}; -use iced::time; -use iced::widget::{button, column, container, row, text}; -use iced::{Alignment, Application, Command, Element, Length, Point, Subscription}; - +use iced::{ + executor, + theme::{self, Theme}, + time, + widget::{button, column, container, row, text}, + {Alignment, Application, Command, Element, Length, Point, Subscription}, +}; + +use itertools::Itertools; use rustc_hash::FxHashSet; use std::time::{Duration, Instant}; mod map; mod mask; +mod midi; use map::*; use mask::*; +use midi::*; pub type CellMap = FxHashSet; @@ -35,8 +40,6 @@ impl Cell { } fn cluster(cell: Cell) -> impl Iterator { - use itertools::Itertools; - let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1); let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1); @@ -52,6 +55,7 @@ impl Cell { pub struct CellSeq { map: Map, mask: Mask, + midi: MidiLink, is_playing: bool, bpm: usize, is_looping: bool, @@ -61,6 +65,7 @@ pub struct CellSeq { #[derive(Debug, Clone)] pub enum Message { + MidiMessage(MidiMessage), Map(map::Message), Mask(mask::Message), Tick(Instant), @@ -99,6 +104,7 @@ impl Application for CellSeq { match message { Message::Map(message) => self.map.update(message), Message::Mask(message) => self.mask.update(message), + Message::MidiMessage(message) => {} Message::Tick(_) => { let life = if self.step_num == self.loop_len && self.is_looping { self.step_num = 0; diff --git a/src/main.rs b/src/main.rs index 2dc2a69..7f42c61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,26 @@ use cellseq::*; use iced::{window, Application, Settings}; -pub fn main() -> iced::Result { +use eyre::Result; +use jack::{Client, ClientOptions, ClosureProcessHandler, Control, MidiOut, ProcessScope, RawMidi}; +use tokio::sync::mpsc::{channel, Receiver, Sender}; + +pub fn main() -> Result<()> { + let (midi_snd, midi_rcv) = channel::(256); + + // setting up jack client + let (jack_client, jack_status) = Client::new("cellseq", ClientOptions::empty())?; + let mut midi_port = jack_client.register_port("cellseq_midi", MidiOut::default())?; + + let process_handler = ClosureProcessHandler::new(move |_: &Client, scope: &ProcessScope| { + let writer = midi_port.writer(scope); + + Control::Continue + }); + + let jack = jack_client.activate_async((), process_handler)?; + + // running the graphics window CellSeq::run(Settings { antialiasing: true, window: window::Settings { @@ -11,4 +30,9 @@ pub fn main() -> iced::Result { }, ..Settings::default() }) + .map_err(|_| Ok(())); + + jack.deactivate()?; + + Ok(()) } diff --git a/src/map.rs b/src/map.rs index 2cc07ff..259c3ab 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,10 +1,11 @@ -use iced::widget::canvas::event::{self, Event}; -use iced::widget::canvas::{Cache, Canvas, Cursor, Geometry, Path}; use iced::{ mouse::{self, Button::Left, Event::ButtonPressed}, - widget::canvas::Program, + widget::canvas::{ + event::{self, Event}, + Cache, Canvas, Cursor, Geometry, Path, Program, + }, + {Color, Element, Length, Point, Rectangle, Size, Theme}, }; -use iced::{Color, Element, Length, Point, Rectangle, Size, Theme}; use super::*; diff --git a/src/mask.rs b/src/mask.rs index 99ad8f1..cc3d4ab 100644 --- a/src/mask.rs +++ b/src/mask.rs @@ -1,13 +1,14 @@ -use iced::mouse::Interaction; -use iced::widget::canvas::event::{self, Event}; -use iced::widget::canvas::{Cache, Canvas, Cursor, Geometry, Path}; use iced::{ + mouse::Interaction, mouse::{Button::Left, Event::ButtonPressed}, - widget::canvas::Program, + widget::canvas::{ + event::{self, Event}, + Cache, Canvas, Cursor, Geometry, Path, Program, + }, + {Color, Element, Length, Point, Rectangle, Size, Theme}, }; -use iced::{Color, Element, Length, Point, Rectangle, Size, Theme}; -use crate::{Cell, CellMap}; +use crate::{Cell, CellMap, MidiMessage}; use itertools::Itertools; use rustc_hash::FxHashMap; diff --git a/src/midi.rs b/src/midi.rs new file mode 100644 index 0000000..3ee5fd9 --- /dev/null +++ b/src/midi.rs @@ -0,0 +1,153 @@ +use std::{collections::VecDeque, fmt::Display}; + +use thiserror::Error; +use tokio::sync::mpsc::Sender; + +#[derive(Clone, Debug)] +pub struct MidiLink { + buffer: VecDeque, + channel: Sender, +} + +impl MidiLink { + pub fn new(channel: Sender) -> Self { + Self { + buffer: VecDeque::default(), + channel, + } + } + + pub fn update(&mut self, message: MidiMessage) { + let bytes = message.as_bytes().unwrap(); + + for byte in bytes.iter().filter_map(|x| *x) { + self.buffer.push_back(byte); + } + } + + pub async fn tick(&mut self) { + for byte in self.buffer.iter() { + self.channel.send(*byte).await.unwrap(); + } + + self.buffer.clear(); + } +} + +#[derive(Clone, Copy, Debug, Error)] +pub enum MidiError { + #[error("value greater than 127: {message}")] + ValueOverflow { message: MidiMessage }, + #[error("channel not within (0-15): {message}")] + ChannelOverflow { message: MidiMessage }, +} + +#[derive(Debug, Default, Clone, Copy)] +pub enum MidiMessage { + On { + note: u8, + velocity: u8, + channel: u8, + }, + Off { + note: u8, + velocity: u8, + channel: u8, + }, + Cc { + controller: u8, + value: u8, + channel: u8, + }, + #[default] + TimingTick, + StartSong, + ContinueSong, + StopSong, +} + +impl Display for MidiMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + MidiMessage::On { + note, + velocity, + channel, + } => format!("note on\n\tchannel: {channel}\n\tpitch: {note}\n\tvelocity: {velocity}"), + MidiMessage::Off { + note, + velocity, + channel, + } => format!("note off\n\tchannel: {channel}\n\tpitch: {note}\n\tvelocity: {velocity}"), + MidiMessage::Cc { + controller, + value, + channel, + } => format!("control change\n\tchannel: {channel}\n\tcontroller: {controller}\n\tvalue: {value}"), + MidiMessage::TimingTick => String::from("timing tick"), + MidiMessage::StartSong => String::from("start song"), + MidiMessage::ContinueSong => String::from("continue song"), + MidiMessage::StopSong => String::from("stop song"), + }; + + write!(f, "{str}") + } +} + +static DATA_BIT: u8 = 0b0111_1111; +static STATUS_BIT: u8 = 0b1111_1111; + +impl MidiMessage { + pub fn as_bytes(&self) -> Result<[Option; 3], MidiError> { + let mut bytes = [None; 3]; + match self { + MidiMessage::On { + note, + velocity, + channel, + } => { + if *note > 127 || *velocity > 127 { + return Err(MidiError::ValueOverflow { message: *self }); + } else if *channel > 15 { + return Err(MidiError::ChannelOverflow { message: *self }); + } + bytes[0] = Some(STATUS_BIT & (0x90 + channel)); + bytes[1] = Some(DATA_BIT & note); + bytes[2] = Some(DATA_BIT & velocity); + } + MidiMessage::Off { + note, + velocity, + channel, + } => { + if *note > 127 || *velocity > 127 { + return Err(MidiError::ValueOverflow { message: *self }); + } else if *channel > 15 { + return Err(MidiError::ChannelOverflow { message: *self }); + } + bytes[0] = Some(STATUS_BIT & (0x80 + channel)); + bytes[1] = Some(DATA_BIT & note); + bytes[2] = Some(DATA_BIT & velocity); + } + MidiMessage::Cc { + controller, + value, + channel, + } => { + if *controller > 127 || *value > 127 { + return Err(MidiError::ValueOverflow { message: *self }); + } else if *channel > 15 { + return Err(MidiError::ChannelOverflow { message: *self }); + } + bytes[0] = Some(STATUS_BIT & (0xD0 + channel)); + bytes[1] = Some(DATA_BIT & controller); + bytes[2] = Some(DATA_BIT & value); + } + MidiMessage::TimingTick => bytes[0] = Some(0xF8), + MidiMessage::StartSong => bytes[0] = Some(0xFA), + MidiMessage::ContinueSong => bytes[0] = Some(0xFB), + MidiMessage::StopSong => bytes[0] = Some(0xFC), + } + Ok(bytes) + } +} -- 2.44.2