wip
This commit is contained in:
@@ -1,80 +1,138 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
|
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum PatternSpeed {
|
pub struct PatternSpeed {
|
||||||
Eighth, // 1/8x
|
pub num: u8,
|
||||||
Quarter, // 1/4x
|
pub denom: u8,
|
||||||
Half, // 1/2x
|
|
||||||
#[default]
|
|
||||||
Normal, // 1x
|
|
||||||
Double, // 2x
|
|
||||||
Quad, // 4x
|
|
||||||
Octo, // 8x
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PatternSpeed {
|
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 {
|
pub fn multiplier(&self) -> f64 {
|
||||||
match self {
|
self.num as f64 / self.denom as f64
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn label(&self) -> &'static str {
|
pub fn label(&self) -> String {
|
||||||
match self {
|
if self.denom == 1 {
|
||||||
Self::Eighth => "1/8x",
|
format!("{}x", self.num)
|
||||||
Self::Quarter => "1/4x",
|
} else {
|
||||||
Self::Half => "1/2x",
|
format!("{}/{}x", self.num, self.denom)
|
||||||
Self::Normal => "1x",
|
|
||||||
Self::Double => "2x",
|
|
||||||
Self::Quad => "4x",
|
|
||||||
Self::Octo => "8x",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(&self) -> Self {
|
pub fn next(&self) -> Self {
|
||||||
match self {
|
let current = self.multiplier();
|
||||||
Self::Eighth => Self::Quarter,
|
Self::PRESETS
|
||||||
Self::Quarter => Self::Half,
|
.iter()
|
||||||
Self::Half => Self::Normal,
|
.find(|p| p.multiplier() > current + 0.0001)
|
||||||
Self::Normal => Self::Double,
|
.copied()
|
||||||
Self::Double => Self::Quad,
|
.unwrap_or(*self)
|
||||||
Self::Quad => Self::Octo,
|
|
||||||
Self::Octo => Self::Octo,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prev(&self) -> Self {
|
pub fn prev(&self) -> Self {
|
||||||
match self {
|
let current = self.multiplier();
|
||||||
Self::Eighth => Self::Eighth,
|
Self::PRESETS
|
||||||
Self::Quarter => Self::Eighth,
|
.iter()
|
||||||
Self::Half => Self::Quarter,
|
.rev()
|
||||||
Self::Normal => Self::Half,
|
.find(|p| p.multiplier() < current - 0.0001)
|
||||||
Self::Double => Self::Normal,
|
.copied()
|
||||||
Self::Quad => Self::Double,
|
.unwrap_or(*self)
|
||||||
Self::Octo => Self::Quad,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_label(s: &str) -> Option<Self> {
|
pub fn from_label(s: &str) -> Option<Self> {
|
||||||
match s.trim() {
|
let s = s.trim().trim_end_matches('x');
|
||||||
"1/8x" | "1/8" | "0.125x" => Some(Self::Eighth),
|
if let Some((num, denom)) = s.split_once('/') {
|
||||||
"1/4x" | "1/4" | "0.25x" => Some(Self::Quarter),
|
let num: u8 = num.parse().ok()?;
|
||||||
"1/2x" | "1/2" | "0.5x" => Some(Self::Half),
|
let denom: u8 = denom.parse().ok()?;
|
||||||
"1x" | "1" => Some(Self::Normal),
|
if denom == 0 {
|
||||||
"2x" | "2" => Some(Self::Double),
|
return None;
|
||||||
"4x" | "4" => Some(Self::Quad),
|
}
|
||||||
"8x" | "8" => Some(Self::Octo),
|
return Some(Self { num, denom });
|
||||||
_ => None,
|
}
|
||||||
|
if let Ok(val) = s.parse::<f64>() {
|
||||||
|
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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
(self.num, self.denom).serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for PatternSpeed {
|
||||||
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
#[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,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1095,7 +1095,7 @@ mod tests {
|
|||||||
let mut state = make_state();
|
let mut state = make_state();
|
||||||
|
|
||||||
let mut pat = simple_pattern(8);
|
let mut pat = simple_pattern(8);
|
||||||
pat.speed = crate::model::PatternSpeed::Double;
|
pat.speed = crate::model::PatternSpeed::DOUBLE;
|
||||||
|
|
||||||
state.tick(tick_with(
|
state.tick(tick_with(
|
||||||
vec![SeqCommand::PatternUpdate { bank: 0, pattern: 0, data: pat }],
|
vec![SeqCommand::PatternUpdate { bank: 0, pattern: 0, data: pat }],
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
)));
|
)));
|
||||||
} else {
|
} else {
|
||||||
ctx.dispatch(AppCommand::SetStatus(
|
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(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
|||||||
frame.render_widget(Paragraph::new(length_line), length_area);
|
frame.render_widget(Paragraph::new(length_line), length_area);
|
||||||
|
|
||||||
// Column 3: speed (only if non-default)
|
// Column 3: speed (only if non-default)
|
||||||
if speed != PatternSpeed::Normal {
|
if speed != PatternSpeed::NORMAL {
|
||||||
let speed_line = Line::from(vec![
|
let speed_line = Line::from(vec![
|
||||||
Span::styled("Speed: ", bold_style),
|
Span::styled("Speed: ", bold_style),
|
||||||
Span::styled(speed.label(), base_style),
|
Span::styled(speed.label(), base_style),
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ fn render_header(
|
|||||||
// Pattern block (name + length + speed + page + iter)
|
// Pattern block (name + length + speed + page + iter)
|
||||||
let default_pattern_name = format!("Pattern {:02}", app.editor_ctx.pattern + 1);
|
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 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())
|
format!(" · {}", pattern.speed.label())
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
@@ -295,17 +295,10 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
} else {
|
} else {
|
||||||
let bindings: Vec<(&str, &str)> = match app.page {
|
let bindings: Vec<(&str, &str)> = match app.page {
|
||||||
Page::Main => vec![
|
Page::Main => vec![
|
||||||
("←→↑↓", "Nav"),
|
|
||||||
("Shift+↑↓", "Select"),
|
|
||||||
("t", "Toggle"),
|
|
||||||
("Enter", "Edit"),
|
|
||||||
("Space", "Play"),
|
("Space", "Play"),
|
||||||
("^C", "Copy"),
|
("Enter", "Edit"),
|
||||||
("^V", "Paste"),
|
("t", "Toggle"),
|
||||||
("^B", "Link"),
|
("Tab", "Samples"),
|
||||||
("^D", "Dup"),
|
|
||||||
("Del", "Delete"),
|
|
||||||
("+-", "Tempo"),
|
|
||||||
("?", "Keys"),
|
("?", "Keys"),
|
||||||
],
|
],
|
||||||
Page::Patterns => vec![
|
Page::Patterns => vec![
|
||||||
@@ -462,7 +455,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
Modal::SetPattern { field, input } => {
|
Modal::SetPattern { field, input } => {
|
||||||
let (title, hint) = match field {
|
let (title, hint) = match field {
|
||||||
PatternField::Length => ("Set Length (1-128)", "Enter number"),
|
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)
|
TextInputModal::new(title, input)
|
||||||
.hint(hint)
|
.hint(hint)
|
||||||
@@ -727,7 +720,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
length.as_str(),
|
length.as_str(),
|
||||||
*field == PatternPropsField::Length,
|
*field == PatternPropsField::Length,
|
||||||
),
|
),
|
||||||
("Speed", speed.label(), *field == PatternPropsField::Speed),
|
("Speed", &speed.label(), *field == PatternPropsField::Speed),
|
||||||
(
|
(
|
||||||
"Quantization",
|
"Quantization",
|
||||||
quantization.label(),
|
quantization.label(),
|
||||||
|
|||||||
Reference in New Issue
Block a user