seq continues
This commit is contained in:
175
seq/src/app.rs
175
seq/src/app.rs
@@ -1,13 +1,18 @@
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
use tui_textarea::TextArea;
|
||||
|
||||
use crate::audio::{SlotChange, MAX_SLOTS};
|
||||
use crate::file;
|
||||
use crate::link::LinkState;
|
||||
use crate::model::{Pattern, Project};
|
||||
use crate::page::Page;
|
||||
use crate::script::{ScriptEngine, StepContext};
|
||||
use crate::script::{Rng, ScriptEngine, StepContext, Variables};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Focus {
|
||||
@@ -18,11 +23,18 @@ pub enum Focus {
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum Modal {
|
||||
None,
|
||||
ConfirmQuit,
|
||||
ConfirmQuit { selected: bool },
|
||||
SaveAs(String),
|
||||
LoadFrom(String),
|
||||
PatternPicker { cursor: usize },
|
||||
BankPicker { cursor: usize },
|
||||
RenameBank { bank: usize, name: String },
|
||||
RenamePattern { bank: usize, pattern: usize, name: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum PatternsViewLevel {
|
||||
#[default]
|
||||
Banks,
|
||||
Patterns { bank: usize },
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
@@ -38,14 +50,18 @@ pub struct App {
|
||||
pub focus: Focus,
|
||||
pub page: Page,
|
||||
pub current_step: usize,
|
||||
pub playback_step: usize,
|
||||
|
||||
pub edit_bank: usize,
|
||||
pub edit_pattern: usize,
|
||||
pub playback_bank: usize,
|
||||
pub playback_pattern: usize,
|
||||
pub queued_bank: Option<usize>,
|
||||
pub queued_pattern: Option<usize>,
|
||||
|
||||
pub patterns_view_level: PatternsViewLevel,
|
||||
pub patterns_cursor: usize,
|
||||
|
||||
// Slot playback state (synced from audio thread)
|
||||
pub slot_data: [(bool, usize, usize); MAX_SLOTS], // (active, bank, pattern)
|
||||
pub slot_steps: [usize; MAX_SLOTS],
|
||||
pub queued_changes: Vec<SlotChange>,
|
||||
|
||||
pub event_count: usize,
|
||||
pub active_voices: usize,
|
||||
pub peak_voices: usize,
|
||||
@@ -54,12 +70,16 @@ pub struct App {
|
||||
pub sample_pool_mb: f32,
|
||||
pub scope: [f32; 64],
|
||||
pub script_engine: ScriptEngine,
|
||||
pub variables: Variables,
|
||||
pub rng: Rng,
|
||||
pub file_path: Option<PathBuf>,
|
||||
pub status_message: Option<String>,
|
||||
pub editor: TextArea<'static>,
|
||||
pub flash_until: Option<Instant>,
|
||||
pub modal: Modal,
|
||||
pub clipboard: Option<arboard::Clipboard>,
|
||||
pub doc_topic: usize,
|
||||
pub doc_scroll: usize,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -76,14 +96,17 @@ impl App {
|
||||
focus: Focus::Sequencer,
|
||||
page: Page::default(),
|
||||
current_step: 0,
|
||||
playback_step: 0,
|
||||
|
||||
edit_bank: 0,
|
||||
edit_pattern: 0,
|
||||
playback_bank: 0,
|
||||
playback_pattern: 0,
|
||||
queued_bank: None,
|
||||
queued_pattern: None,
|
||||
|
||||
patterns_view_level: PatternsViewLevel::default(),
|
||||
patterns_cursor: 0,
|
||||
|
||||
slot_data: [(false, 0, 0); MAX_SLOTS],
|
||||
slot_steps: [0; MAX_SLOTS],
|
||||
queued_changes: Vec::new(),
|
||||
|
||||
event_count: 0,
|
||||
active_voices: 0,
|
||||
peak_voices: 0,
|
||||
@@ -92,12 +115,16 @@ impl App {
|
||||
sample_pool_mb: 0.0,
|
||||
scope: [0.0; 64],
|
||||
script_engine: ScriptEngine::new(),
|
||||
variables: Arc::new(Mutex::new(HashMap::new())),
|
||||
rng: Arc::new(Mutex::new(StdRng::seed_from_u64(0))),
|
||||
file_path: None,
|
||||
status_message: None,
|
||||
editor: TextArea::default(),
|
||||
flash_until: None,
|
||||
modal: Modal::None,
|
||||
clipboard: arboard::Clipboard::new().ok(),
|
||||
doc_topic: 0,
|
||||
doc_scroll: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,17 +182,33 @@ impl App {
|
||||
|
||||
pub fn step_up(&mut self) {
|
||||
let len = self.current_edit_pattern().length;
|
||||
if self.current_step >= 8 {
|
||||
self.current_step -= 8;
|
||||
let num_rows = match len {
|
||||
0..=8 => 1,
|
||||
9..=16 => 2,
|
||||
17..=24 => 3,
|
||||
_ => 4,
|
||||
};
|
||||
let steps_per_row = len.div_ceil(num_rows);
|
||||
|
||||
if self.current_step >= steps_per_row {
|
||||
self.current_step -= steps_per_row;
|
||||
} else {
|
||||
self.current_step = (self.current_step + len - 8) % len;
|
||||
self.current_step = (self.current_step + len - steps_per_row) % len;
|
||||
}
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn step_down(&mut self) {
|
||||
let len = self.current_edit_pattern().length;
|
||||
self.current_step = (self.current_step + 8) % len;
|
||||
let num_rows = match len {
|
||||
0..=8 => 1,
|
||||
9..=16 => 2,
|
||||
17..=24 => 3,
|
||||
_ => 4,
|
||||
};
|
||||
let steps_per_row = len.div_ceil(num_rows);
|
||||
|
||||
self.current_step = (self.current_step + steps_per_row) % len;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
@@ -177,6 +220,39 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn length_increase(&mut self) {
|
||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
||||
let current_len = self.project.pattern_at(bank, pattern).length;
|
||||
self.project
|
||||
.pattern_at_mut(bank, pattern)
|
||||
.set_length(current_len + 1);
|
||||
}
|
||||
|
||||
pub fn length_decrease(&mut self) {
|
||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
||||
let current_len = self.project.pattern_at(bank, pattern).length;
|
||||
self.project
|
||||
.pattern_at_mut(bank, pattern)
|
||||
.set_length(current_len.saturating_sub(1));
|
||||
let new_len = self.project.pattern_at(bank, pattern).length;
|
||||
if self.current_step >= new_len {
|
||||
self.current_step = new_len - 1;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn speed_increase(&mut self) {
|
||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
||||
let pat = self.project.pattern_at_mut(bank, pattern);
|
||||
pat.speed = pat.speed.next();
|
||||
}
|
||||
|
||||
pub fn speed_decrease(&mut self) {
|
||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
||||
let pat = self.project.pattern_at_mut(bank, pattern);
|
||||
pat.speed = pat.speed.prev();
|
||||
}
|
||||
|
||||
fn load_step_to_editor(&mut self) {
|
||||
let step_idx = self.current_step;
|
||||
if let Some(step) = self.current_edit_pattern().step(step_idx) {
|
||||
@@ -223,9 +299,10 @@ impl App {
|
||||
pattern,
|
||||
tempo: self.tempo,
|
||||
phase: self.phase,
|
||||
slot: 0,
|
||||
};
|
||||
|
||||
match self.script_engine.evaluate(&script, &ctx) {
|
||||
match self.script_engine.evaluate(&script, &ctx, &self.variables, &self.rng) {
|
||||
Ok(cmd) => {
|
||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||
step.command = Some(cmd);
|
||||
@@ -268,9 +345,10 @@ impl App {
|
||||
pattern,
|
||||
tempo: self.tempo,
|
||||
phase: 0.0,
|
||||
slot: 0,
|
||||
};
|
||||
|
||||
if let Ok(cmd) = self.script_engine.evaluate(&script, &ctx) {
|
||||
if let Ok(cmd) = self.script_engine.evaluate(&script, &ctx, &self.variables, &self.rng) {
|
||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||
step.command = Some(cmd);
|
||||
}
|
||||
@@ -278,14 +356,55 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn queue_current_for_playback(&mut self) {
|
||||
self.queued_bank = Some(self.edit_bank);
|
||||
self.queued_pattern = Some(self.edit_pattern);
|
||||
self.status_message = Some(format!(
|
||||
"Queued B{:02} P{:02} (next loop)",
|
||||
self.edit_bank + 1,
|
||||
self.edit_pattern + 1
|
||||
));
|
||||
pub fn is_pattern_queued(&self, bank: usize, pattern: usize) -> Option<bool> {
|
||||
self.queued_changes.iter().find_map(|c| match c {
|
||||
SlotChange::Add { slot: _, bank: b, pattern: p } if *b == bank && *p == pattern => {
|
||||
Some(true)
|
||||
}
|
||||
SlotChange::Remove { slot } => {
|
||||
let (active, b, p) = self.slot_data[*slot];
|
||||
if active && b == bank && p == pattern {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_pattern_playback(&mut self, bank: usize, pattern: usize) {
|
||||
let playing_slot = self.slot_data.iter().enumerate().find_map(|(i, (active, b, p))| {
|
||||
if *active && *b == bank && *p == pattern {
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let pending = self.queued_changes.iter().position(|c| match c {
|
||||
SlotChange::Add { bank: b, pattern: p, .. } => *b == bank && *p == pattern,
|
||||
SlotChange::Remove { slot } => {
|
||||
let (_, b, p) = self.slot_data[*slot];
|
||||
b == bank && p == pattern
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(idx) = pending {
|
||||
self.queued_changes.remove(idx);
|
||||
self.status_message = Some(format!("B{:02}:P{:02} change cancelled", bank + 1, pattern + 1));
|
||||
} else if let Some(slot_idx) = playing_slot {
|
||||
self.queued_changes.push(SlotChange::Remove { slot: slot_idx });
|
||||
self.status_message = Some(format!("B{:02}:P{:02} queued to stop", bank + 1, pattern + 1));
|
||||
} else {
|
||||
let free_slot = (0..MAX_SLOTS).find(|&i| !self.slot_data[i].0);
|
||||
if let Some(slot_idx) = free_slot {
|
||||
self.queued_changes.push(SlotChange::Add { slot: slot_idx, bank, pattern });
|
||||
self.status_message = Some(format!("B{:02}:P{:02} queued to play", bank + 1, pattern + 1));
|
||||
} else {
|
||||
self.status_message = Some("All slots occupied".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_edit_pattern(&mut self, pattern: usize) {
|
||||
|
||||
Reference in New Issue
Block a user