From b2293110c22fec6d33e6cfcedc66e88215b76d55 Mon Sep 17 00:00:00 2001 From: Huck Boles Date: Sat, 1 Jul 2023 14:32:57 -0500 Subject: [PATCH] refactored: midi implementation --- src/display.rs | 93 +++++++++++++++++----- src/lib.rs | 71 +++++++++-------- src/main.rs | 2 +- src/mask.rs | 100 ++++------------------- src/midi.rs | 87 +++++++++++++------- src/music.rs | 210 +++++++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 392 insertions(+), 171 deletions(-) diff --git a/src/display.rs b/src/display.rs index 172cca2..c370398 100644 --- a/src/display.rs +++ b/src/display.rs @@ -4,7 +4,10 @@ use iced::{ Alignment, Element, Length, }; -use crate::{music::Scale, Message}; +use crate::{ + music::{Accidental, Root, RootNote, Scale}, + Message, +}; pub struct ControlMessage { probability: f32, @@ -19,6 +22,8 @@ pub struct ControlMessage { octave: u8, range: u8, scale: Scale, + root: Root, + voices: u8, } pub fn top_controls<'a>(is_playing: bool) -> Element<'a, Message> { @@ -29,29 +34,27 @@ pub fn top_controls<'a>(is_playing: bool) -> Element<'a, Message> { .on_press(Message::Quit) .style(theme::Button::Destructive),] .width(Length::Fill) - .align_items(Alignment::Center) + .align_items(Alignment::End) .spacing(10); row![play_button, other_controls] .padding(10) .spacing(40) - .align_items(Alignment::Center) .into() } pub fn bottom_controls<'a>(message: ControlMessage) -> Element<'a, Message> { - let song = row![] - .padding(10) - .spacing(10) - .align_items(Alignment::Center); - - column![map_section(&message), midi_section(&message), song] - .width(Length::Fill) - .height(Length::Fill) - .padding(10) - .spacing(40) - .align_items(Alignment::Center) - .into() + column![ + map_section(&message), + midi_section(&message), + song_section(&message) + ] + .width(Length::Fill) + .height(Length::Fill) + .padding(10) + .spacing(40) + .align_items(Alignment::Center) + .into() } fn map_section<'a>(message: &ControlMessage) -> Row<'a, Message> { @@ -165,7 +168,32 @@ fn channel_selector<'a>(channel: u8) -> Row<'a, Message> { .align_items(Alignment::Center) } -fn song_section<'a>(message: &ControlMessage) -> Column<'a, Message> {} +fn song_section<'a>(message: &ControlMessage) -> Row<'a, Message> { + row![ + column![ + loop_controls(message.is_looping, message.loop_len, message.step_num), + speed_controls(message.bpm) + ] + .padding(10) + .spacing(10) + .align_items(Alignment::Center), + column![ + voice_controls(message.voices), + octave_selector(message.octave, message.range), + scale_selector( + message.scale, + message.root.get_note(), + message.root.get_accidental() + ) + ] + .padding(10) + .spacing(10) + .align_items(Alignment::Center) + ] + .padding(10) + .spacing(10) + .align_items(Alignment::Center) +} fn octave_selector<'a>(oct: u8, range: u8) -> Row<'a, Message> { row![ @@ -218,9 +246,32 @@ fn speed_controls<'a>(bpm: usize) -> Row<'a, Message> { .spacing(10) } -fn scale_selector<'a>(scale: Scale) -> Row<'a, Message> { - row![pick_list()] - .width(Length::Fill) - .align_items(Alignment::Center) - .spacing(10) +fn voice_controls<'a>(voices: u8) -> Row<'a, Message> { + row![ + button("-") + .on_press(Message::Voices(voices.saturating_sub(1))) + .style(theme::Button::Destructive), + text(format!("voices: {voices}")), + button("+") + .on_press(Message::Voices(voices.saturating_add(1))) + .style(theme::Button::Positive), + ] + .padding(10) + .spacing(10) + .align_items(Alignment::Center) +} + +fn scale_selector<'a>(scale: Scale, note: RootNote, acc: Accidental) -> Row<'a, Message> { + row![ + pick_list(&Scale::ALL[..], Some(scale), Message::Scale), + pick_list(&RootNote::ALL[..], Some(note), move |note| { + Message::NewNote(Root::new(note, acc)) + }), + pick_list(&Accidental::ALL[..], Some(acc), move |acc| { + Message::NewNote(Root::new(note, acc)) + }) + ] + .width(Length::Fill) + .align_items(Alignment::Center) + .spacing(10) } diff --git a/src/lib.rs b/src/lib.rs index 433feef..bb93f49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,26 +61,22 @@ pub struct CellSeq { map: Map, mask: Mask, midi: MidiLink, + info: MidiInfo, is_playing: bool, bpm: usize, is_looping: bool, loop_len: usize, step_num: usize, - probability: f32, randomness: f32, - velocity_min: u8, - velocity_max: u8, - channel: u8, - octave: u8, - oct_range: u8, - scale: Scale, } #[derive(Debug, Clone)] pub enum Message { - Midi(MidiMessage), - Map(map::Message), - Mask(mask::Message), + None, + MapMessage(map::Message), + MaskMessage(mask::Message), + NewMap(CellMap), + HitCount(u8), Tick(Instant), Randomize, Reset, @@ -98,6 +94,8 @@ pub enum Message { Scale(Scale), NewOctave(u8), OctaveRange(u8), + NewNote(Root), + Voices(u8), Quit, } @@ -131,9 +129,15 @@ impl Application for CellSeq { fn update(&mut self, message: Message) -> Command { match message { - Message::Map(message) => self.map.update(message), - Message::Mask(message) => self.mask.update(message), - Message::Midi(message) => self.midi.update(message), + Message::None => {} + Message::NewMap(m) => { + self.map.update(map::Message::Ticked(m.clone())); + let hits = self.mask.tick(m); + return Command::perform(async move { hits }, Message::HitCount); + } + Message::HitCount(x) => self.midi.update(x, &self.info), + Message::MapMessage(message) => self.map.update(message), + Message::MaskMessage(message) => self.mask.update(message), Message::Tick(_) => { let map = if self.is_looping && self.step_num > self.loop_len { self.step_num = 0; @@ -143,19 +147,18 @@ impl Application for CellSeq { self.map.tick() }; - let midi = self.mask.tick(map.clone()); - - let mut commands = Vec::new(); + let channel = self.midi.channel_handle(); + let bytes = self.midi.tick(); - commands.push(Command::perform(async move { map }, |m| { - Message::Map(map::Message::Ticked(m)) - })); + let midi = tokio::spawn(async move { + for byte in bytes { + channel.send(byte).await.unwrap() + } + }); - for message in midi { - commands.push(Command::perform(async move { message }, |m| { - Message::Midi(m) - })); - } + let mut commands = Vec::new(); + commands.push(Command::perform(async move { map }, Message::NewMap)); + commands.push(Command::perform(midi, |_| Message::None)); return Command::batch(commands); } @@ -178,14 +181,16 @@ impl Application for CellSeq { } Message::LoopLength(len) => self.loop_len = len, Message::Quit => todo!(), - Message::ProbChanged(p) => self.probability = p, + Message::ProbChanged(p) => self.info.probability = p, Message::RandChanged(r) => self.randomness = r, - Message::NewVMin(v) => self.velocity_min = v, - Message::NewVMax(v) => self.velocity_max = v, - Message::ChannelChange(c) => self.channel = c, - Message::Scale(s) => self.scale = s, - Message::NewOctave(o) => self.octave = o, - Message::OctaveRange(r) => self.oct_range = r, + Message::NewVMin(v) => self.info.velocity.set_min(v), + Message::NewVMax(v) => self.info.velocity.set_max(v), + Message::ChannelChange(c) => self.info.channel = c, + Message::Scale(s) => self.info.scale = s, + Message::NewOctave(o) => self.info.octave.set_center(o), + Message::OctaveRange(r) => self.info.octave.set_range(r), + Message::NewNote(r) => self.info.root = r, + Message::Voices(v) => self.info.voices = v, } Command::none() @@ -203,8 +208,8 @@ impl Application for CellSeq { let top = top_controls(self.is_playing); let map = row![ - self.map.view().map(Message::Map), - self.mask.view().map(Message::Mask) + self.map.view().map(Message::MapMessage), + self.mask.view().map(Message::MaskMessage) ] .align_items(Alignment::Center) .width(Length::Fill) diff --git a/src/main.rs b/src/main.rs index ff826aa..3db3f9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use eyre::Result; use tokio::sync::mpsc::channel; pub fn main() -> Result<()> { - let (midi_snd, _midi_rcv) = channel::>(256); + let (midi_snd, _midi_rcv) = channel::(256); let midi = MidiLink::new(midi_snd); diff --git a/src/mask.rs b/src/mask.rs index c89920c..e6b2569 100644 --- a/src/mask.rs +++ b/src/mask.rs @@ -8,51 +8,19 @@ use iced::{ {Color, Element, Length, Point, Rectangle, Size, Theme}, }; -use crate::{Cell, CellMap, MidiMessage}; +use crate::{Cell, CellMap}; use itertools::Itertools; -use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; #[derive(Debug, Clone)] pub enum Message { Check(Cell), Uncheck(Cell), - SetNote((Cell, Note)), - Tick(CellMap), -} - -#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Hash)] -enum OnOff { - #[default] - Off, - On, -} - -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Note { - value: u8, - velocity: u8, - state: OnOff, -} - -impl Note { - pub fn is_on(&self) -> bool { - match self.state { - OnOff::On => true, - OnOff::Off => false, - } - } - - pub fn switch(&mut self) { - self.state = match self.state { - OnOff::On => OnOff::Off, - OnOff::Off => OnOff::On, - } - } } #[derive(Default, Debug)] pub struct Mask { - cells: FxHashMap, + cells: FxHashSet, mask_cache: Cache, } @@ -60,34 +28,13 @@ impl Mask { pub fn update(&mut self, message: Message) { match message { Message::Check(cell) => { - self.cells.insert(cell, Note::default()); + self.cells.insert(cell); self.mask_cache.clear() } Message::Uncheck(cell) => { self.cells.remove(&cell); self.mask_cache.clear(); } - Message::Tick(life) => { - for cell in life.iter() { - if self.cells.contains_key(cell) { - let note = self.cells.entry(*cell).or_default(); - note.switch() - } - } - - self.mask_cache.clear(); - } - Message::SetNote((cell, note)) => { - self.cells.insert(cell, note); - } - } - } - - pub fn is_on(&self, cell: &Cell) -> bool { - if let Some(note) = self.cells.get(cell) { - note.is_on() - } else { - false } } @@ -98,25 +45,14 @@ impl Mask { .into() } - pub fn tick(&mut self, life: CellMap) -> Vec { - let mut note_map: FxHashMap = FxHashMap::default(); - let mut messages = Vec::new(); - - for cell in life.iter() { - if let Some(note) = self.cells.get(cell) { - note_map.insert(note.value, note.velocity); + pub fn tick(&mut self, life: CellMap) -> u8 { + let mut hits = 0; + for cell in self.cells.iter() { + if life.contains(cell) { + hits += 1; } } - - for (note, vel) in note_map { - messages.push(MidiMessage::On { - note, - velocity: vel, - channel: 0, - }) - } - - messages + hits } } @@ -138,15 +74,13 @@ impl Program for Mask { (0..24) .cartesian_product(0..24) - .filter(|x| self.cells.contains_key(&Cell { i: x.1, j: x.0 })) + .filter(|x| self.cells.contains(&Cell { i: x.1, j: x.0 })) .for_each(|x| { - let color = if self.is_on(&Cell { i: x.0, j: x.1 }) { - Color::from_rgb8(0xF0, 0xC0, 0xC0) - } else { - Color::WHITE - }; - - frame.fill_rectangle(Point::new(x.0 as f32, x.1 as f32), Size::UNIT, color); + frame.fill_rectangle( + Point::new(x.0 as f32, x.1 as f32), + Size::UNIT, + Color::WHITE, + ); }) }); })] @@ -164,7 +98,7 @@ impl Program for Mask { let cell = Cell::at(position); return ( event::Status::Captured, - if self.cells.contains_key(&cell) { + if self.cells.contains(&cell) { Some(Message::Uncheck(cell)) } else { Some(Message::Check(cell)) diff --git a/src/midi.rs b/src/midi.rs index 4caf545..20cfaa4 100644 --- a/src/midi.rs +++ b/src/midi.rs @@ -1,47 +1,80 @@ -use std::{collections::VecDeque, fmt::Display}; +use std::fmt::Display; +use eyre::Result; +use rand::random; use thiserror::Error; use tokio::sync::mpsc::Sender; +use crate::music::{generate_note, generate_velocity, Octave, Root, Scale, Velocity}; + +#[derive(Debug, Clone, Copy, Default)] +pub struct MidiInfo { + pub channel: u8, + pub velocity: Velocity, + pub octave: Octave, + pub scale: Scale, + pub root: Root, + pub voices: u8, + pub probability: f32, +} + #[derive(Clone, Debug)] pub struct MidiLink { - buffer: VecDeque, - channel: Sender>, + buffer: Vec, + channel: Sender, } impl Default for MidiLink { fn default() -> Self { let (send, _) = tokio::sync::mpsc::channel(128); Self { - buffer: VecDeque::default(), channel: send, + buffer: Vec::default(), } } } -impl<'a> MidiLink { - pub fn new(channel: Sender>) -> Self { +impl MidiLink { + pub fn new(channel: Sender) -> Self { Self { - buffer: VecDeque::default(), channel, + ..Self::default() } } - pub fn update(&mut self, message: MidiMessage) { - let bytes = message.as_bytes().unwrap(); + pub fn channel_handle(&self) -> Sender { + self.channel.clone() + } - for byte in bytes.iter().filter_map(|x| *x) { - self.buffer.push_back(byte); + pub fn update(&mut self, hits: u8, info: &MidiInfo) { + let mut count = 0; + + for _ in 0..hits { + if count > info.voices { + break; + } else if random::() < info.probability { + continue; + } else { + count += 1; + self.buffer.push(MidiMessage::On { + note: generate_note(info), + velocity: generate_velocity(info.velocity), + channel: info.channel, + }); + } } } - pub async fn tick(&mut self) { - for byte in self.buffer.iter() { - self.channel.send(Some(*byte)).await.unwrap(); - } + pub fn tick(&mut self) -> Vec { + let vec: Vec = self + .buffer + .iter() + .flat_map(|m| m.as_bytes().unwrap()) + .flatten() + .collect(); - self.channel.send(None).await.unwrap(); self.buffer.clear(); + vec } } @@ -105,8 +138,8 @@ impl Display for MidiMessage { } } -static DATA_BIT: u8 = 0b0111_1111; -static STATUS_BIT: u8 = 0b1111_1111; +static DATA_MASK: u8 = 0b0111_1111; +static STATUS_MASK: u8 = 0b1111_1111; impl MidiMessage { pub fn as_bytes(&self) -> Result<[Option; 3], MidiError> { @@ -122,9 +155,9 @@ impl MidiMessage { } 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); + bytes[0] = Some(STATUS_MASK & (0x90 + channel)); + bytes[1] = Some(DATA_MASK & note); + bytes[2] = Some(DATA_MASK & velocity); } MidiMessage::Off { note, @@ -136,9 +169,9 @@ impl MidiMessage { } 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); + bytes[0] = Some(STATUS_MASK & (0x80 + channel)); + bytes[1] = Some(DATA_MASK & note); + bytes[2] = Some(DATA_MASK & velocity); } MidiMessage::Cc { controller, @@ -150,9 +183,9 @@ impl MidiMessage { } 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); + bytes[0] = Some(STATUS_MASK & (0xD0 + channel)); + bytes[1] = Some(DATA_MASK & controller); + bytes[2] = Some(DATA_MASK & value); } MidiMessage::TimingTick => bytes[0] = Some(0xF8), MidiMessage::StartSong => bytes[0] = Some(0xFA), diff --git a/src/music.rs b/src/music.rs index 2ef742a..4ab6df1 100644 --- a/src/music.rs +++ b/src/music.rs @@ -1,4 +1,8 @@ -use std::fmt::display; +use std::fmt::Display; + +use rand::random; + +use crate::MidiInfo; #[derive(Clone, Copy, Eq, PartialEq, Default, Debug)] pub enum Scale { @@ -18,9 +22,153 @@ pub enum Scale { WholeTone, } -impl Into<[bool; 12]> for Scale { - fn into(self) -> [bool; 12] { - match self { +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Octave { + center: u8, + range: u8, +} + +impl Octave { + pub fn set_center(&mut self, center: u8) { + self.center = center; + } + + pub fn set_range(&mut self, range: u8) { + self.range = range; + } +} + +impl Default for Octave { + fn default() -> Self { + Self { + center: 4, + range: 0, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Velocity { + min: u8, + max: u8, +} + +impl Velocity { + pub fn set_min(&mut self, min: u8) { + self.min = min; + } + + pub fn set_max(&mut self, max: u8) { + self.max = max; + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Root { + note: RootNote, + accidental: Accidental, +} + +impl Root { + pub fn new(note: RootNote, accidental: Accidental) -> Self { + Self { note, accidental } + } + + pub fn get_note(&self) -> RootNote { + self.note + } + + pub fn get_accidental(&self) -> Accidental { + self.accidental + } +} + +impl From for u8 { + fn from(val: Root) -> Self { + let n = match val.note { + RootNote::A => 21, + RootNote::B => 22, + RootNote::C => 23, + RootNote::D => 24, + RootNote::E => 25, + RootNote::F => 26, + RootNote::G => 27, + }; + + match val.accidental { + Accidental::Natural => n, + Accidental::Sharp => n + 1, + Accidental::Flat => n - 1, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum RootNote { + A, + B, + #[default] + C, + D, + E, + F, + G, +} + +impl Display for RootNote { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + RootNote::A => "A", + RootNote::B => "B", + RootNote::C => "C", + RootNote::D => "D", + RootNote::E => "E", + RootNote::F => "F", + RootNote::G => "G", + }; + + write!(f, "{str}") + } +} + +impl RootNote { + pub const ALL: [RootNote; 7] = [ + RootNote::A, + RootNote::B, + RootNote::C, + RootNote::D, + RootNote::E, + RootNote::F, + RootNote::G, + ]; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Accidental { + #[default] + Natural, + Sharp, + Flat, +} + +impl Display for Accidental { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + Accidental::Natural => "", + Accidental::Sharp => "#", + Accidental::Flat => "b", + }; + write!(f, "{str}") + } +} + +impl Accidental { + pub const ALL: [Accidental; 3] = [Accidental::Natural, Accidental::Sharp, Accidental::Flat]; +} + +impl From for [bool; 12] { + fn from(val: Scale) -> Self { + match val { Scale::Chromatic => [true; 12], Scale::Major => [ true, false, true, false, true, true, false, true, false, true, false, true, @@ -55,14 +203,32 @@ impl Into<[bool; 12]> for Scale { Scale::MinorPentatonic => [ true, false, false, true, false, true, false, true, false, false, true, false, ], - Self::WholeTone => [ + Scale::WholeTone => [ true, false, true, false, true, false, true, false, true, false, true, false, ], } } } -impl std::fmt::Display for Scale { +impl Scale { + pub const ALL: [Scale; 13] = [ + Scale::Chromatic, + Scale::Major, + Scale::Minor, + Scale::Dorian, + Scale::Phrygian, + Scale::Lydian, + Scale::Mixolydian, + Scale::Locrian, + Scale::MinorPentatonic, + Scale::MajorPentatonic, + Scale::MelodicMinor, + Scale::HarmonicMinor, + Scale::WholeTone, + ]; +} + +impl Display for Scale { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let str = match self { Scale::Chromatic => "chromatic", @@ -83,3 +249,35 @@ impl std::fmt::Display for Scale { write!(f, "{str}") } } + +pub fn generate_note(info: &MidiInfo) -> u8 { + let root: u8 = info.root.into(); + + let oct_mod = random::() % info.octave.range; + let octave = if random::() { + info.octave.center.saturating_add(oct_mod) + } else { + info.octave.center.saturating_sub(oct_mod) + }; + + let scale: [bool; 12] = info.scale.into(); + + let degree = loop { + let r = random::() % 12; + if scale[r] { + break r.try_into().unwrap(); + } else { + continue; + } + }; + + octave + .saturating_mul(12) + .saturating_add(root) + .saturating_add(degree) +} + +pub fn generate_velocity(v: Velocity) -> u8 { + let range = v.max - v.min; + v.min + (random::() % range) +} -- 2.44.2