]> git.huck.website - cellseq.git/commitdiff
added: midi messages and channels
authorHuck Boles <huck@huck.website>
Fri, 23 Jun 2023 22:59:01 +0000 (17:59 -0500)
committerHuck Boles <huck@huck.website>
Fri, 23 Jun 2023 22:59:01 +0000 (17:59 -0500)
Cargo.lock
Cargo.toml
src/lib.rs
src/main.rs
src/map.rs
src/mask.rs
src/midi.rs [new file with mode: 0644]

index d11294c14300c05749b0895c2776ce1b04f096ee..fe00ab36a032ec253f501c7c8212fae917a4bdd0 100644 (file)
@@ -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"
index 0291f17dc42fb0486a162b25e343e08f1ee44926..b74337b3c6eeca47d6789867adaea4922e0f4ede 100644 (file)
@@ -7,8 +7,10 @@ authors = ["Huck Boles <huck@huck.website>"]
 [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"
index ac87d2b70c6b7d91c6b1e74416e85f2db4495cf2..8737153fe57e2cf0f12196deda7ac64653645afa 100644 (file)
@@ -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<Cell>;
 
@@ -35,8 +40,6 @@ impl Cell {
     }
 
     fn cluster(cell: Cell) -> impl Iterator<Item = Cell> {
-        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;
index 2dc2a69c03a2a0a7c34989b977164d7364ef9ff3..7f42c61f50cc439c2ae8ada0ff2cd466e999c9bf 100644 (file)
@@ -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::<u8>(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(())
 }
index 2cc07ff3463343104a7e84adb175914576669a62..259c3ab3443f521bf1c895b35e723f556868a6b4 100644 (file)
@@ -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::*;
 
index 99ad8f1cbfe99745f3c4ebe65a2dd319f662c1b1..cc3d4ab3129a1b226c526614a99045d24e29759f 100644 (file)
@@ -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 (file)
index 0000000..3ee5fd9
--- /dev/null
@@ -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<u8>,
+    channel: Sender<u8>,
+}
+
+impl MidiLink {
+    pub fn new(channel: Sender<u8>) -> 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<u8>; 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)
+    }
+}