635 lines
18 KiB
Rust
635 lines
18 KiB
Rust
//! Project, Bank, Pattern, and Step structs with serialization.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|
|
|
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
|
|
|
|
/// Speed multiplier for a pattern, expressed as a rational fraction.
|
|
#[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,
|
|
];
|
|
|
|
/// Return the speed as a floating-point multiplier.
|
|
pub fn multiplier(&self) -> f64 {
|
|
self.num as f64 / self.denom as f64
|
|
}
|
|
|
|
/// Format as a human-readable label (e.g. "2x", "1/4x").
|
|
pub fn label(&self) -> String {
|
|
if self.denom == 1 {
|
|
format!("{}x", self.num)
|
|
} else {
|
|
format!("{}/{}x", self.num, self.denom)
|
|
}
|
|
}
|
|
|
|
/// Return the next faster preset, or self if already at maximum.
|
|
pub fn next(&self) -> Self {
|
|
let current = self.multiplier();
|
|
Self::PRESETS
|
|
.iter()
|
|
.find(|p| p.multiplier() > current + 0.0001)
|
|
.copied()
|
|
.unwrap_or(*self)
|
|
}
|
|
|
|
/// Return the next slower preset, or self if already at minimum.
|
|
pub fn prev(&self) -> Self {
|
|
let current = self.multiplier();
|
|
Self::PRESETS
|
|
.iter()
|
|
.rev()
|
|
.find(|p| p.multiplier() < current - 0.0001)
|
|
.copied()
|
|
.unwrap_or(*self)
|
|
}
|
|
|
|
/// Parse a speed label like "2x" or "1/4x" into a `PatternSpeed`.
|
|
pub fn from_label(s: &str) -> Option<Self> {
|
|
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::<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,
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Quantization grid for launching patterns.
|
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
|
pub enum LaunchQuantization {
|
|
Immediate,
|
|
Beat,
|
|
#[default]
|
|
Bar,
|
|
Bars2,
|
|
Bars4,
|
|
Bars8,
|
|
}
|
|
|
|
impl LaunchQuantization {
|
|
/// Human-readable label for display.
|
|
pub fn label(&self) -> &'static str {
|
|
match self {
|
|
Self::Immediate => "Immediate",
|
|
Self::Beat => "Beat",
|
|
Self::Bar => "1 Bar",
|
|
Self::Bars2 => "2 Bars",
|
|
Self::Bars4 => "4 Bars",
|
|
Self::Bars8 => "8 Bars",
|
|
}
|
|
}
|
|
|
|
pub fn short_label(&self) -> &'static str {
|
|
match self {
|
|
Self::Immediate => "Imm",
|
|
Self::Beat => "Bt",
|
|
Self::Bar => "1B",
|
|
Self::Bars2 => "2B",
|
|
Self::Bars4 => "4B",
|
|
Self::Bars8 => "8B",
|
|
}
|
|
}
|
|
|
|
/// Cycle to the next longer quantization, clamped at `Bars8`.
|
|
pub fn next(&self) -> Self {
|
|
match self {
|
|
Self::Immediate => Self::Beat,
|
|
Self::Beat => Self::Bar,
|
|
Self::Bar => Self::Bars2,
|
|
Self::Bars2 => Self::Bars4,
|
|
Self::Bars4 => Self::Bars8,
|
|
Self::Bars8 => Self::Bars8,
|
|
}
|
|
}
|
|
|
|
/// Cycle to the next shorter quantization, clamped at `Immediate`.
|
|
pub fn prev(&self) -> Self {
|
|
match self {
|
|
Self::Immediate => Self::Immediate,
|
|
Self::Beat => Self::Immediate,
|
|
Self::Bar => Self::Beat,
|
|
Self::Bars2 => Self::Bar,
|
|
Self::Bars4 => Self::Bars2,
|
|
Self::Bars8 => Self::Bars4,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// How a pattern synchronizes when launched: restart or phase-lock.
|
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
|
pub enum SyncMode {
|
|
#[default]
|
|
Reset,
|
|
PhaseLock,
|
|
}
|
|
|
|
impl SyncMode {
|
|
/// Human-readable label for display.
|
|
pub fn label(&self) -> &'static str {
|
|
match self {
|
|
Self::Reset => "Reset",
|
|
Self::PhaseLock => "Phase-Lock",
|
|
}
|
|
}
|
|
|
|
pub fn short_label(&self) -> &'static str {
|
|
match self {
|
|
Self::Reset => "Rst",
|
|
Self::PhaseLock => "Plk",
|
|
}
|
|
}
|
|
|
|
/// Toggle between Reset and PhaseLock.
|
|
pub fn toggle(&self) -> Self {
|
|
match self {
|
|
Self::Reset => Self::PhaseLock,
|
|
Self::PhaseLock => Self::Reset,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What happens when a pattern finishes: loop, stop, or chain to another.
|
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
|
pub enum FollowUp {
|
|
#[default]
|
|
Loop,
|
|
Stop,
|
|
Chain { bank: usize, pattern: usize },
|
|
}
|
|
|
|
impl FollowUp {
|
|
/// Human-readable label for display.
|
|
pub fn label(&self) -> &'static str {
|
|
match self {
|
|
Self::Loop => "Loop",
|
|
Self::Stop => "Stop",
|
|
Self::Chain { .. } => "Chain",
|
|
}
|
|
}
|
|
|
|
/// Cycle forward through follow-up modes.
|
|
pub fn next_mode(&self) -> Self {
|
|
match self {
|
|
Self::Loop => Self::Stop,
|
|
Self::Stop => Self::Chain { bank: 0, pattern: 0 },
|
|
Self::Chain { .. } => Self::Loop,
|
|
}
|
|
}
|
|
|
|
/// Cycle backward through follow-up modes.
|
|
pub fn prev_mode(&self) -> Self {
|
|
match self {
|
|
Self::Loop => Self::Chain { bank: 0, pattern: 0 },
|
|
Self::Stop => Self::Loop,
|
|
Self::Chain { .. } => Self::Stop,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_default_follow_up(f: &FollowUp) -> bool {
|
|
*f == FollowUp::default()
|
|
}
|
|
|
|
/// Single step in a pattern, holding a Forth script and optional metadata.
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct Step {
|
|
pub active: bool,
|
|
pub script: String,
|
|
#[serde(default)]
|
|
pub source: Option<u8>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub name: Option<String>,
|
|
}
|
|
|
|
impl Step {
|
|
/// True if all fields are at their default values.
|
|
pub fn is_default(&self) -> bool {
|
|
self.active && self.script.is_empty() && self.source.is_none() && self.name.is_none()
|
|
}
|
|
|
|
/// True if the script is non-empty.
|
|
pub fn has_content(&self) -> bool {
|
|
!self.script.is_empty()
|
|
}
|
|
}
|
|
|
|
impl Default for Step {
|
|
fn default() -> Self {
|
|
Self {
|
|
active: true,
|
|
script: String::new(),
|
|
source: None,
|
|
name: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sequence of steps with playback settings (speed, quantization, sync, follow-up).
|
|
#[derive(Clone)]
|
|
pub struct Pattern {
|
|
pub steps: Vec<Step>,
|
|
pub length: usize,
|
|
pub speed: PatternSpeed,
|
|
pub name: Option<String>,
|
|
pub description: Option<String>,
|
|
pub quantization: LaunchQuantization,
|
|
pub sync_mode: SyncMode,
|
|
pub follow_up: FollowUp,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct SparseStep {
|
|
i: usize,
|
|
#[serde(default = "default_active", skip_serializing_if = "is_true")]
|
|
active: bool,
|
|
#[serde(default, skip_serializing_if = "String::is_empty")]
|
|
script: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
source: Option<u8>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
name: Option<String>,
|
|
}
|
|
|
|
fn default_active() -> bool {
|
|
true
|
|
}
|
|
|
|
fn is_true(v: &bool) -> bool {
|
|
*v
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
struct SparsePattern {
|
|
steps: Vec<SparseStep>,
|
|
length: usize,
|
|
#[serde(default)]
|
|
speed: PatternSpeed,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
name: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
description: Option<String>,
|
|
#[serde(default, skip_serializing_if = "is_default_quantization")]
|
|
quantization: LaunchQuantization,
|
|
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
|
|
sync_mode: SyncMode,
|
|
#[serde(default, skip_serializing_if = "is_default_follow_up")]
|
|
follow_up: FollowUp,
|
|
}
|
|
|
|
fn is_default_quantization(q: &LaunchQuantization) -> bool {
|
|
*q == LaunchQuantization::default()
|
|
}
|
|
|
|
fn is_default_sync_mode(s: &SyncMode) -> bool {
|
|
*s == SyncMode::default()
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct LegacyPattern {
|
|
steps: Vec<Step>,
|
|
length: usize,
|
|
#[serde(default)]
|
|
speed: PatternSpeed,
|
|
#[serde(default)]
|
|
name: Option<String>,
|
|
#[serde(default)]
|
|
description: Option<String>,
|
|
#[serde(default)]
|
|
quantization: LaunchQuantization,
|
|
#[serde(default)]
|
|
sync_mode: SyncMode,
|
|
#[serde(default)]
|
|
follow_up: FollowUp,
|
|
}
|
|
|
|
impl Serialize for Pattern {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
let sparse_steps: Vec<SparseStep> = self
|
|
.steps
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, step)| !step.is_default())
|
|
.map(|(i, step)| SparseStep {
|
|
i,
|
|
active: step.active,
|
|
script: step.script.clone(),
|
|
source: step.source,
|
|
name: step.name.clone(),
|
|
})
|
|
.collect();
|
|
|
|
let sparse = SparsePattern {
|
|
steps: sparse_steps,
|
|
length: self.length,
|
|
speed: self.speed,
|
|
name: self.name.clone(),
|
|
description: self.description.clone(),
|
|
quantization: self.quantization,
|
|
sync_mode: self.sync_mode,
|
|
follow_up: self.follow_up,
|
|
};
|
|
sparse.serialize(serializer)
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for Pattern {
|
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
|
#[derive(Deserialize)]
|
|
#[serde(untagged)]
|
|
enum PatternFormat {
|
|
Sparse(SparsePattern),
|
|
Legacy(LegacyPattern),
|
|
}
|
|
|
|
match PatternFormat::deserialize(deserializer)? {
|
|
PatternFormat::Sparse(sparse) => {
|
|
let mut steps: Vec<Step> = (0..MAX_STEPS).map(|_| Step::default()).collect();
|
|
for ss in sparse.steps {
|
|
if ss.i < MAX_STEPS {
|
|
steps[ss.i] = Step {
|
|
active: ss.active,
|
|
script: ss.script,
|
|
source: ss.source,
|
|
name: ss.name,
|
|
};
|
|
}
|
|
}
|
|
Ok(Pattern {
|
|
steps,
|
|
length: sparse.length,
|
|
speed: sparse.speed,
|
|
name: sparse.name,
|
|
description: sparse.description,
|
|
quantization: sparse.quantization,
|
|
sync_mode: sparse.sync_mode,
|
|
follow_up: sparse.follow_up,
|
|
})
|
|
}
|
|
PatternFormat::Legacy(legacy) => Ok(Pattern {
|
|
steps: legacy.steps,
|
|
length: legacy.length,
|
|
speed: legacy.speed,
|
|
name: legacy.name,
|
|
description: legacy.description,
|
|
quantization: legacy.quantization,
|
|
sync_mode: legacy.sync_mode,
|
|
follow_up: legacy.follow_up,
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Pattern {
|
|
fn default() -> Self {
|
|
Self {
|
|
steps: (0..MAX_STEPS).map(|_| Step::default()).collect(),
|
|
length: DEFAULT_LENGTH,
|
|
speed: PatternSpeed::default(),
|
|
name: None,
|
|
description: None,
|
|
quantization: LaunchQuantization::default(),
|
|
sync_mode: SyncMode::default(),
|
|
follow_up: FollowUp::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Pattern {
|
|
/// Borrow a step by index.
|
|
pub fn step(&self, index: usize) -> Option<&Step> {
|
|
self.steps.get(index)
|
|
}
|
|
|
|
/// Mutably borrow a step by index.
|
|
pub fn step_mut(&mut self, index: usize) -> Option<&mut Step> {
|
|
self.steps.get_mut(index)
|
|
}
|
|
|
|
/// Set the active length, clamped to `[1, MAX_STEPS]`.
|
|
pub fn set_length(&mut self, length: usize) {
|
|
let length = length.clamp(1, MAX_STEPS);
|
|
while self.steps.len() < length {
|
|
self.steps.push(Step::default());
|
|
}
|
|
self.length = length;
|
|
}
|
|
|
|
/// Follow the source chain from `index` to find the originating step.
|
|
pub fn resolve_source(&self, index: usize) -> usize {
|
|
let mut current = index;
|
|
for _ in 0..self.steps.len() {
|
|
if let Some(step) = self.steps.get(current) {
|
|
if let Some(source) = step.source {
|
|
current = source as usize;
|
|
} else {
|
|
return current;
|
|
}
|
|
} else {
|
|
return index;
|
|
}
|
|
}
|
|
index
|
|
}
|
|
|
|
/// Return the script at the resolved source of `index`.
|
|
pub fn resolve_script(&self, index: usize) -> Option<&str> {
|
|
let source_idx = self.resolve_source(index);
|
|
self.steps.get(source_idx).map(|s| s.script.as_str())
|
|
}
|
|
|
|
/// Count active-length steps that have a script or a source reference.
|
|
pub fn content_step_count(&self) -> usize {
|
|
self.steps[..self.length]
|
|
.iter()
|
|
.filter(|s| s.has_content() || s.source.is_some())
|
|
.count()
|
|
}
|
|
}
|
|
|
|
/// Collection of patterns forming a bank.
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct Bank {
|
|
pub patterns: Vec<Pattern>,
|
|
#[serde(default)]
|
|
pub name: Option<String>,
|
|
#[serde(default)]
|
|
pub prelude: String,
|
|
}
|
|
|
|
impl Bank {
|
|
/// Count patterns that contain at least one non-empty step.
|
|
pub fn content_pattern_count(&self) -> usize {
|
|
self.patterns
|
|
.iter()
|
|
.filter(|p| p.content_step_count() > 0)
|
|
.count()
|
|
}
|
|
}
|
|
|
|
impl Default for Bank {
|
|
fn default() -> Self {
|
|
Self {
|
|
patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(),
|
|
name: None,
|
|
prelude: String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Top-level project: banks, tempo, sample paths, and prelude script.
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct Project {
|
|
pub banks: Vec<Bank>,
|
|
#[serde(default)]
|
|
pub sample_paths: Vec<PathBuf>,
|
|
#[serde(default = "default_tempo")]
|
|
pub tempo: f64,
|
|
#[serde(default)]
|
|
pub playing_patterns: Vec<(usize, usize)>,
|
|
#[serde(default)]
|
|
pub prelude: String,
|
|
#[serde(default)]
|
|
pub script: String,
|
|
#[serde(default)]
|
|
pub script_speed: PatternSpeed,
|
|
#[serde(default = "default_script_length")]
|
|
pub script_length: usize,
|
|
}
|
|
|
|
fn default_tempo() -> f64 {
|
|
120.0
|
|
}
|
|
|
|
fn default_script_length() -> usize {
|
|
16
|
|
}
|
|
|
|
impl Default for Project {
|
|
fn default() -> Self {
|
|
Self {
|
|
banks: (0..MAX_BANKS).map(|_| Bank::default()).collect(),
|
|
sample_paths: Vec::new(),
|
|
tempo: default_tempo(),
|
|
playing_patterns: Vec::new(),
|
|
prelude: String::new(),
|
|
script: String::new(),
|
|
script_speed: PatternSpeed::default(),
|
|
script_length: default_script_length(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Project {
|
|
/// Borrow a pattern by bank and pattern index.
|
|
pub fn pattern_at(&self, bank: usize, pattern: usize) -> &Pattern {
|
|
&self.banks[bank].patterns[pattern]
|
|
}
|
|
|
|
/// Mutably borrow a pattern by bank and pattern index.
|
|
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
|
|
&mut self.banks[bank].patterns[pattern]
|
|
}
|
|
|
|
/// Pad banks, patterns, and steps to their maximum sizes after deserialization.
|
|
pub fn normalize(&mut self) {
|
|
self.banks.resize_with(MAX_BANKS, Bank::default);
|
|
for bank in &mut self.banks {
|
|
bank.patterns.resize_with(MAX_PATTERNS, Pattern::default);
|
|
for pattern in &mut bank.patterns {
|
|
pattern.steps.resize_with(MAX_STEPS, Step::default);
|
|
}
|
|
}
|
|
}
|
|
}
|