"eyre",
"iced",
"itertools",
+ "jack",
"rand",
"rustc-hash",
+ "thiserror",
"tokio",
]
"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"
[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"
-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>;
}
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);
pub struct CellSeq {
map: Map,
mask: Mask,
+ midi: MidiLink,
is_playing: bool,
bpm: usize,
is_looping: bool,
#[derive(Debug, Clone)]
pub enum Message {
+ MidiMessage(MidiMessage),
Map(map::Message),
Mask(mask::Message),
Tick(Instant),
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;
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 {
},
..Settings::default()
})
+ .map_err(|_| Ok(()));
+
+ jack.deactivate()?;
+
+ Ok(())
}
-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::*;
-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;
--- /dev/null
+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)
+ }
+}