From 0520ef872e314ea08af6b04e8fb6aab899990804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Wed, 28 Jan 2026 13:54:29 +0100 Subject: [PATCH] wip --- crates/project/src/project.rs | 170 +++++++++++++++++++++++----------- src/engine/sequencer.rs | 2 +- src/input.rs | 2 +- src/views/patterns_view.rs | 2 +- src/views/render.rs | 19 ++-- 5 files changed, 123 insertions(+), 72 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c70d923..965b0b8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,80 +1,138 @@ use std::path::PathBuf; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS}; -#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)] -pub enum PatternSpeed { - Eighth, // 1/8x - Quarter, // 1/4x - Half, // 1/2x - #[default] - Normal, // 1x - Double, // 2x - Quad, // 4x - Octo, // 8x +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct PatternSpeed { + pub num: u8, + pub denom: u8, } impl PatternSpeed { + pub const EIGHTH: Self = Self { num: 1, denom: 8 }; + pub const FIFTH: Self = Self { num: 1, denom: 5 }; + pub const QUARTER: Self = Self { num: 1, denom: 4 }; + pub const THIRD: Self = Self { num: 1, denom: 3 }; + pub const HALF: Self = Self { num: 1, denom: 2 }; + pub const TWO_THIRDS: Self = Self { num: 2, denom: 3 }; + pub const NORMAL: Self = Self { num: 1, denom: 1 }; + pub const DOUBLE: Self = Self { num: 2, denom: 1 }; + pub const QUAD: Self = Self { num: 4, denom: 1 }; + pub const OCTO: Self = Self { num: 8, denom: 1 }; + + const PRESETS: &[Self] = &[ + Self::EIGHTH, + Self::FIFTH, + Self::QUARTER, + Self::THIRD, + Self::HALF, + Self::TWO_THIRDS, + Self::NORMAL, + Self::DOUBLE, + Self::QUAD, + Self::OCTO, + ]; + pub fn multiplier(&self) -> f64 { - match self { - Self::Eighth => 0.125, - Self::Quarter => 0.25, - Self::Half => 0.5, - Self::Normal => 1.0, - Self::Double => 2.0, - Self::Quad => 4.0, - Self::Octo => 8.0, - } + self.num as f64 / self.denom as f64 } - pub fn label(&self) -> &'static str { - match self { - Self::Eighth => "1/8x", - Self::Quarter => "1/4x", - Self::Half => "1/2x", - Self::Normal => "1x", - Self::Double => "2x", - Self::Quad => "4x", - Self::Octo => "8x", + pub fn label(&self) -> String { + if self.denom == 1 { + format!("{}x", self.num) + } else { + format!("{}/{}x", self.num, self.denom) } } pub fn next(&self) -> Self { - match self { - Self::Eighth => Self::Quarter, - Self::Quarter => Self::Half, - Self::Half => Self::Normal, - Self::Normal => Self::Double, - Self::Double => Self::Quad, - Self::Quad => Self::Octo, - Self::Octo => Self::Octo, - } + let current = self.multiplier(); + Self::PRESETS + .iter() + .find(|p| p.multiplier() > current + 0.0001) + .copied() + .unwrap_or(*self) } pub fn prev(&self) -> Self { - match self { - Self::Eighth => Self::Eighth, - Self::Quarter => Self::Eighth, - Self::Half => Self::Quarter, - Self::Normal => Self::Half, - Self::Double => Self::Normal, - Self::Quad => Self::Double, - Self::Octo => Self::Quad, - } + let current = self.multiplier(); + Self::PRESETS + .iter() + .rev() + .find(|p| p.multiplier() < current - 0.0001) + .copied() + .unwrap_or(*self) } pub fn from_label(s: &str) -> Option { - match s.trim() { - "1/8x" | "1/8" | "0.125x" => Some(Self::Eighth), - "1/4x" | "1/4" | "0.25x" => Some(Self::Quarter), - "1/2x" | "1/2" | "0.5x" => Some(Self::Half), - "1x" | "1" => Some(Self::Normal), - "2x" | "2" => Some(Self::Double), - "4x" | "4" => Some(Self::Quad), - "8x" | "8" => Some(Self::Octo), - _ => None, + let s = s.trim().trim_end_matches('x'); + if let Some((num, denom)) = s.split_once('/') { + let num: u8 = num.parse().ok()?; + let denom: u8 = denom.parse().ok()?; + if denom == 0 { + return None; + } + return Some(Self { num, denom }); + } + if let Ok(val) = s.parse::() { + if val <= 0.0 || val > 255.0 { + return None; + } + if (val - val.round()).abs() < 0.0001 { + return Some(Self { + num: val.round() as u8, + denom: 1, + }); + } + for denom in 1..=16u8 { + let num = val * denom as f64; + if (num - num.round()).abs() < 0.0001 && (1.0..=255.0).contains(&num) { + return Some(Self { + num: num.round() as u8, + denom, + }); + } + } + } + None + } +} + +impl Default for PatternSpeed { + fn default() -> Self { + Self::NORMAL + } +} + +impl Serialize for PatternSpeed { + fn serialize(&self, serializer: S) -> Result { + (self.num, self.denom).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for PatternSpeed { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + #[serde(untagged)] + enum SpeedFormat { + Tuple((u8, u8)), + Legacy(String), + } + + match SpeedFormat::deserialize(deserializer)? { + SpeedFormat::Tuple((num, denom)) => Ok(Self { num, denom }), + SpeedFormat::Legacy(s) => Ok(match s.as_str() { + "Eighth" => Self::EIGHTH, + "Quarter" => Self::QUARTER, + "Half" => Self::HALF, + "Normal" => Self::NORMAL, + "Double" => Self::DOUBLE, + "Quad" => Self::QUAD, + "Octo" => Self::OCTO, + _ => Self::NORMAL, + }), } } } diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index 4430642..dfd048d 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -1095,7 +1095,7 @@ mod tests { let mut state = make_state(); let mut pat = simple_pattern(8); - pat.speed = crate::model::PatternSpeed::Double; + pat.speed = crate::model::PatternSpeed::DOUBLE; state.tick(tick_with( vec![SeqCommand::PatternUpdate { bank: 0, pattern: 0, data: pat }], diff --git a/src/input.rs b/src/input.rs index 3bb5656..eb4aeb6 100644 --- a/src/input.rs +++ b/src/input.rs @@ -359,7 +359,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { ))); } else { ctx.dispatch(AppCommand::SetStatus( - "Invalid speed (try 1/8x, 1/4x, 1/2x, 1x, 2x, 4x, 8x)".to_string(), + "Invalid speed (try 1/3, 2/5, 1x, 2x)".to_string(), )); } } diff --git a/src/views/patterns_view.rs b/src/views/patterns_view.rs index dea8a93..0243853 100644 --- a/src/views/patterns_view.rs +++ b/src/views/patterns_view.rs @@ -287,7 +287,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a frame.render_widget(Paragraph::new(length_line), length_area); // Column 3: speed (only if non-default) - if speed != PatternSpeed::Normal { + if speed != PatternSpeed::NORMAL { let speed_line = Line::from(vec![ Span::styled("Speed: ", bold_style), Span::styled(speed.label(), base_style), diff --git a/src/views/render.rs b/src/views/render.rs index 0af749c..c84f710 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -225,7 +225,7 @@ fn render_header( // Pattern block (name + length + speed + page + iter) let default_pattern_name = format!("Pattern {:02}", app.editor_ctx.pattern + 1); let pattern_name = pattern.name.as_deref().unwrap_or(&default_pattern_name); - let speed_info = if pattern.speed != PatternSpeed::Normal { + let speed_info = if pattern.speed != PatternSpeed::NORMAL { format!(" · {}", pattern.speed.label()) } else { String::new() @@ -295,17 +295,10 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) { } else { let bindings: Vec<(&str, &str)> = match app.page { Page::Main => vec![ - ("←→↑↓", "Nav"), - ("Shift+↑↓", "Select"), - ("t", "Toggle"), - ("Enter", "Edit"), ("Space", "Play"), - ("^C", "Copy"), - ("^V", "Paste"), - ("^B", "Link"), - ("^D", "Dup"), - ("Del", "Delete"), - ("+-", "Tempo"), + ("Enter", "Edit"), + ("t", "Toggle"), + ("Tab", "Samples"), ("?", "Keys"), ], Page::Patterns => vec![ @@ -462,7 +455,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term Modal::SetPattern { field, input } => { let (title, hint) = match field { PatternField::Length => ("Set Length (1-128)", "Enter number"), - PatternField::Speed => ("Set Speed", "1/8x, 1/4x, 1/2x, 1x, 2x, 4x, 8x"), + PatternField::Speed => ("Set Speed", "e.g. 1/3, 2/5, 1x, 2x"), }; TextInputModal::new(title, input) .hint(hint) @@ -727,7 +720,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term length.as_str(), *field == PatternPropsField::Length, ), - ("Speed", speed.label(), *field == PatternPropsField::Speed), + ("Speed", &speed.label(), *field == PatternPropsField::Speed), ( "Quantization", quantization.label(),