use crate::{
music::{Accidental, Root, RootNote, Scale},
- Message,
+ Message, MidiInfo, SongInfo,
};
+#[derive(Default, Copy, Clone, Debug)]
pub struct ControlMessage {
- probability: f32,
- randomness: f32,
- velocity_min: u8,
- velocity_max: u8,
- channel: u8,
- bpm: usize,
- is_looping: bool,
- loop_len: usize,
- step_num: usize,
- octave: u8,
- range: u8,
- scale: Scale,
- root: Root,
- voices: u8,
+ pub randomness: f32,
+ pub info: MidiInfo,
+ pub song: SongInfo,
}
pub fn top_controls<'a>(is_playing: bool) -> Element<'a, Message> {
.spacing(10);
row![play_button, other_controls]
+ .width(Length::Fill)
.padding(10)
.spacing(40)
.into()
}
pub fn bottom_controls<'a>(message: ControlMessage) -> Element<'a, Message> {
- 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()
+ column![map_section(&message), midi_section(&message),]
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .padding(10)
+ .spacing(20)
+ .align_items(Alignment::Center)
+ .into()
}
fn map_section<'a>(message: &ControlMessage) -> Row<'a, Message> {
row![
map_buttons(),
column![
- probability_section(message.probability),
+ probability_section(message.info.probability),
randomize_section(message.randomness)
]
.padding(10)
.spacing(10)
.align_items(Alignment::Center)
]
- .padding(10)
.spacing(10)
- .align_items(Alignment::Center)
}
fn probability_section<'a>(p: f32) -> Row<'a, Message> {
text("probability a cell gets triggered")
.style(theme::Text::Color(iced::Color::from_rgb8(0x40, 0x40, 0x40)))
]
- .padding(10)
.spacing(10)
- .align_items(Alignment::Center)
}
fn randomize_section<'a>(r: f32) -> Row<'a, Message> {
text("percent of board to fill on randomize")
.style(theme::Text::Color(iced::Color::from_rgb8(0x40, 0x40, 0x40)))
]
- .padding(10)
.spacing(10)
- .align_items(Alignment::Center)
}
-fn map_buttons<'a>() -> Row<'a, Message> {
- row![
- button("save")
- .on_press(Message::Save)
- .style(theme::Button::Positive),
- button("clear")
- .on_press(Message::Clear)
- .style(theme::Button::Destructive),
- button("reset")
- .on_press(Message::Reset)
- .style(theme::Button::Secondary),
- button("random")
- .on_press(Message::Randomize)
- .style(theme::Button::Primary),
+fn map_buttons<'a>() -> Column<'a, Message> {
+ column![
+ row![
+ button("save")
+ .on_press(Message::Save)
+ .style(theme::Button::Positive),
+ button("clear")
+ .on_press(Message::Clear)
+ .style(theme::Button::Destructive),
+ ]
+ .spacing(10),
+ row![
+ button("reset")
+ .on_press(Message::Reset)
+ .style(theme::Button::Secondary),
+ button("random")
+ .on_press(Message::Randomize)
+ .style(theme::Button::Primary),
+ ]
+ .spacing(10)
]
- .padding(10)
.spacing(10)
- .align_items(Alignment::Center)
}
fn velocity_sliders<'a>(min: u8, max: u8) -> Column<'a, Message> {
column![
row![
- slider(0..=127, min, Message::NewVMin),
- text(format!("{min}")),
- text("minimum velocity")
+ text(format!("{max}")),
+ slider(0..=127, max, Message::NewVMax),
+ text("maximum velocity")
.style(theme::Text::Color(iced::Color::from_rgb8(0x40, 0x40, 0x40)))
]
- .padding(10)
- .spacing(10)
- .align_items(Alignment::Center),
+ .spacing(10),
row![
- slider(0..=127, max, Message::NewVMax),
- text(format!("{max}")),
- text("maximum velocity")
+ text(format!("{min}")),
+ slider(0..=127, min, Message::NewVMin),
+ text("minimum velocity")
.style(theme::Text::Color(iced::Color::from_rgb8(0x40, 0x40, 0x40)))
]
- .padding(10)
- .spacing(10)
- .align_items(Alignment::Center)
+ .spacing(10),
]
- .padding(10)
.spacing(10)
- .align_items(Alignment::Center)
}
-fn midi_section<'a>(message: &ControlMessage) -> Row<'a, Message> {
- row![
- channel_selector(message.channel),
- velocity_sliders(message.velocity_min, message.velocity_max)
- ]
- .padding(10)
- .spacing(10)
- .align_items(Alignment::Center)
-}
-
-fn channel_selector<'a>(channel: u8) -> Row<'a, Message> {
- row![
- button("-").on_press(Message::ChannelChange(channel.saturating_sub(1))),
- text(format!("channel: {channel}")),
- button("+").on_press(Message::ChannelChange(channel.saturating_add(1))),
+fn midi_section<'a>(message: &ControlMessage) -> Column<'a, Message> {
+ column![
+ song_section(message),
+ velocity_sliders(message.info.velocity.min(), message.info.velocity.max())
]
- .padding(10)
.spacing(10)
- .align_items(Alignment::Center)
}
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)
+ loop_controls(
+ message.song.is_looping,
+ message.song.loop_len,
+ message.song.step_num
+ ),
+ speed_controls(message.song.bpm),
+ division_controls(message.song.divisor),
]
.padding(10)
.spacing(10)
.align_items(Alignment::Center),
column![
- voice_controls(message.voices),
- octave_selector(message.octave, message.range),
+ voice_controls(message.info.voices, message.info.channel),
+ octave_selector(message.info.octave.center(), message.info.octave.range()),
scale_selector(
- message.scale,
- message.root.get_note(),
- message.root.get_accidental()
+ message.info.scale,
+ message.info.root.get_note(),
+ message.info.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![
- button("-")
- .on_press(Message::NewOctave(oct.saturating_sub(1)))
- .style(theme::Button::Destructive),
+ button("-").on_press(Message::NewOctave(oct.saturating_sub(1))),
text(format!("octave: {oct}")),
- button("+")
- .on_press(Message::NewOctave(oct.saturating_add(1)))
- .style(theme::Button::Positive),
- button("-")
- .on_press(Message::OctaveRange(range.saturating_sub(1)))
- .style(theme::Button::Destructive),
+ button("+").on_press(Message::NewOctave(oct.saturating_add(1))),
+ button("-").on_press(Message::OctaveRange(range.saturating_sub(1))),
text(format!("range: +/-{range}")),
- button("+")
- .on_press(Message::OctaveRange(range.saturating_add(1)))
- .style(theme::Button::Positive),
+ button("+").on_press(Message::OctaveRange(range.saturating_add(1)))
]
- .padding(10)
.spacing(10)
- .align_items(Alignment::Center)
}
fn loop_controls<'a>(looping: bool, len: usize, step: usize) -> Row<'a, Message> {
}),
button("+").on_press(Message::LoopLength(len.saturating_add(1)))
]
- .padding(10)
.spacing(10)
- .align_items(Alignment::Center)
}
fn speed_controls<'a>(bpm: usize) -> Row<'a, Message> {
button(">").on_press(Message::SpeedChanged(bpm.saturating_add(1))),
button(">>").on_press(Message::SpeedChanged(bpm.saturating_add(5))),
]
- .width(Length::Fill)
- .align_items(Alignment::Center)
.spacing(10)
}
-fn voice_controls<'a>(voices: u8) -> Row<'a, Message> {
+fn division_controls<'a>(divisor: usize) -> Row<'a, Message> {
row![
- button("-")
- .on_press(Message::Voices(voices.saturating_sub(1)))
- .style(theme::Button::Destructive),
+ button("-").on_press(if divisor > 1 {
+ Message::NewDivision(divisor.saturating_sub(1))
+ } else {
+ Message::None
+ }),
+ text(format!("note division: {divisor}")),
+ button("+").on_press(Message::NewDivision(divisor.saturating_add(1)))
+ ]
+ .spacing(10)
+}
+
+fn voice_controls<'a>(voices: u8, channel: u8) -> Row<'a, Message> {
+ row![
+ button("-").on_press(Message::ChannelChange(channel.saturating_sub(1))),
+ text(format!("channel: {channel}")),
+ button("+").on_press(Message::ChannelChange(channel.saturating_add(1))),
+ button("-").on_press(Message::Voices(voices.saturating_sub(1))),
text(format!("voices: {voices}")),
- button("+")
- .on_press(Message::Voices(voices.saturating_add(1)))
- .style(theme::Button::Positive),
+ button("+").on_press(Message::Voices(voices.saturating_add(1))),
]
- .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))
- })
+ }),
+ pick_list(&Scale::ALL[..], Some(scale), Message::Scale),
]
- .width(Length::Fill)
.align_items(Alignment::Center)
- .spacing(10)
}
map: Map,
mask: Mask,
midi: MidiLink,
+ song: SongInfo,
info: MidiInfo,
- is_playing: bool,
- bpm: usize,
- is_looping: bool,
- loop_len: usize,
- step_num: usize,
- randomness: f32,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct SongInfo {
+ pub is_playing: bool,
+ pub bpm: usize,
+ pub divisor: usize,
+ pub is_looping: bool,
+ pub loop_len: usize,
+ pub step_num: usize,
+}
+
+impl Default for SongInfo {
+ fn default() -> Self {
+ Self {
+ is_playing: false,
+ bpm: 120,
+ divisor: 1,
+ is_looping: false,
+ loop_len: 16,
+ step_num: 0,
+ }
+ }
}
#[derive(Debug, Clone)]
Save,
TogglePlayback,
SpeedChanged(usize),
+ NewDivision(usize),
ToggleLoop,
LoopLength(usize),
ProbChanged(f32),
impl CellSeq {
fn control_message(&self) -> ControlMessage {
- todo!()
+ ControlMessage {
+ randomness: self.map.randomness(),
+ info: self.info.clone(),
+ song: self.song.clone(),
+ }
}
}
fn new(flags: Self::Flags) -> (Self, Command<Message>) {
(
Self {
- bpm: 120,
- loop_len: 16,
midi: flags,
..Self::default()
},
fn update(&mut self, message: Message) -> Command<Message> {
match message {
+ Message::Quit => todo!(), // TODO: figure out how to cleanly quit
Message::None => {}
+ Message::MapMessage(message) => self.map.update(message),
+ Message::MaskMessage(message) => self.mask.update(message),
+ Message::HitCount(x) => self.midi.update(x, &self.info),
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;
+ let map = if self.song.is_looping && self.song.step_num > self.song.loop_len {
+ self.song.step_num = 0;
self.map.reset_loop()
} else {
- self.step_num += 1;
+ self.song.step_num += 1;
self.map.tick()
};
return Command::batch(commands);
}
Message::TogglePlayback => {
- self.is_playing = !self.is_playing;
- if self.is_playing {
+ self.song.is_playing = !self.song.is_playing;
+ if self.song.is_playing {
self.map.save()
}
}
- Message::SpeedChanged(bpm) => self.bpm = bpm,
- Message::Clear => self.map.clear(),
- Message::Randomize => self.map.randomize(self.randomness),
- Message::Reset => self.map.reset(),
- Message::Save => self.map.save(),
Message::ToggleLoop => {
- self.is_looping = !self.is_looping;
- if self.is_looping {
- self.step_num = 0;
+ self.song.is_looping = !self.song.is_looping;
+ if self.song.is_looping {
+ self.song.step_num = 0;
}
}
- Message::LoopLength(len) => self.loop_len = len,
- Message::Quit => todo!(),
+ Message::Clear => self.map.clear(),
+ Message::Randomize => self.map.randomize(),
+ Message::Reset => self.map.reset(),
+ Message::Save => self.map.save(),
+ Message::SpeedChanged(b) => self.song.bpm = b,
+ Message::NewDivision(d) => self.song.divisor = d,
+ Message::LoopLength(l) => self.song.loop_len = l,
Message::ProbChanged(p) => self.info.probability = p,
- Message::RandChanged(r) => self.randomness = r,
+ Message::RandChanged(r) => self.map.set_randomness(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,
}
fn subscription(&self) -> Subscription<Message> {
- if self.is_playing {
- time::every(Duration::from_millis(60000 / self.bpm as u64)).map(Message::Tick)
+ if self.song.is_playing {
+ time::every(Duration::from_millis(
+ 60000 / (self.song.bpm * self.song.divisor) as u64,
+ ))
+ .map(Message::Tick)
} else {
Subscription::none()
}
}
fn view(&self) -> Element<Message> {
- let top = top_controls(self.is_playing);
+ let top = top_controls(self.song.is_playing);
let map = row![
self.map.view().map(Message::MapMessage),