Files
doux-copy/seq/src/app.rs
2026-01-20 03:30:48 +01:00

713 lines
24 KiB
Rust

use rand::rngs::StdRng;
use rand::SeedableRng;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use crossbeam_channel::Sender;
use crate::commands::AppCommand;
use crate::config::MAX_SLOTS;
use crate::engine::{
LinkState, PatternSnapshot, SeqCommand, SequencerSnapshot, SlotChange, StepSnapshot,
};
use crate::model::{self, Pattern, Rng, ScriptEngine, StepContext, Variables};
use crate::page::Page;
use crate::services::pattern_editor;
use crate::state::{
AudioSettings, EditorContext, Focus, Metrics, Modal, PatternField, PatternsViewLevel,
PlaybackState, ProjectState, UiState,
};
use crate::views::doc_view;
pub struct App {
pub project_state: ProjectState,
pub ui: UiState,
pub playback: PlaybackState,
pub page: Page,
pub editor_ctx: EditorContext,
pub patterns_view_level: PatternsViewLevel,
pub patterns_cursor: usize,
pub metrics: Metrics,
pub sample_pool_mb: f32,
pub script_engine: ScriptEngine,
pub variables: Variables,
pub rng: Rng,
pub clipboard: Option<arboard::Clipboard>,
pub audio: AudioSettings,
}
impl App {
pub fn new() -> Self {
let variables = Arc::new(Mutex::new(HashMap::new()));
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&rng));
Self {
project_state: ProjectState::default(),
ui: UiState::default(),
playback: PlaybackState::default(),
page: Page::default(),
editor_ctx: EditorContext::default(),
patterns_view_level: PatternsViewLevel::default(),
patterns_cursor: 0,
metrics: Metrics::default(),
sample_pool_mb: 0.0,
variables,
rng,
script_engine,
clipboard: arboard::Clipboard::new().ok(),
audio: AudioSettings::default(),
}
}
fn current_bank_pattern(&self) -> (usize, usize) {
(self.editor_ctx.bank, self.editor_ctx.pattern)
}
pub fn mark_all_patterns_dirty(&mut self) {
self.project_state.mark_all_dirty();
}
pub fn toggle_playing(&mut self) {
self.playback.toggle();
}
pub fn tempo_up(&self, link: &LinkState) {
let current = link.tempo();
link.set_tempo((current + 1.0).min(300.0));
}
pub fn tempo_down(&self, link: &LinkState) {
let current = link.tempo();
link.set_tempo((current - 1.0).max(20.0));
}
pub fn toggle_focus(&mut self, link: &LinkState) {
match self.editor_ctx.focus {
Focus::Sequencer => {
self.editor_ctx.focus = Focus::Editor;
self.load_step_to_editor();
}
Focus::Editor => {
self.save_editor_to_step();
self.compile_current_step(link);
self.editor_ctx.focus = Focus::Sequencer;
}
}
}
pub fn current_edit_pattern(&self) -> &Pattern {
let (bank, pattern) = self.current_bank_pattern();
self.project_state.project.pattern_at(bank, pattern)
}
pub fn next_step(&mut self) {
let len = self.current_edit_pattern().length;
self.editor_ctx.step = (self.editor_ctx.step + 1) % len;
self.load_step_to_editor();
}
pub fn prev_step(&mut self) {
let len = self.current_edit_pattern().length;
self.editor_ctx.step = (self.editor_ctx.step + len - 1) % len;
self.load_step_to_editor();
}
pub fn step_up(&mut self) {
let len = self.current_edit_pattern().length;
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.editor_ctx.step >= steps_per_row {
self.editor_ctx.step -= steps_per_row;
} else {
self.editor_ctx.step = (self.editor_ctx.step + len - steps_per_row) % len;
}
self.load_step_to_editor();
}
pub fn step_down(&mut self) {
let len = self.current_edit_pattern().length;
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.editor_ctx.step = (self.editor_ctx.step + steps_per_row) % len;
self.load_step_to_editor();
}
pub fn toggle_step(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let change = pattern_editor::toggle_step(
&mut self.project_state.project,
bank,
pattern,
self.editor_ctx.step,
);
self.project_state.mark_dirty(change.bank, change.pattern);
}
pub fn length_increase(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let (change, _) =
pattern_editor::increase_length(&mut self.project_state.project, bank, pattern);
self.project_state.mark_dirty(change.bank, change.pattern);
}
pub fn length_decrease(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let (change, new_len) =
pattern_editor::decrease_length(&mut self.project_state.project, bank, pattern);
if self.editor_ctx.step >= new_len {
self.editor_ctx.step = new_len - 1;
self.load_step_to_editor();
}
self.project_state.mark_dirty(change.bank, change.pattern);
}
pub fn speed_increase(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let change = pattern_editor::increase_speed(&mut self.project_state.project, bank, pattern);
self.project_state.mark_dirty(change.bank, change.pattern);
}
pub fn speed_decrease(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let change = pattern_editor::decrease_speed(&mut self.project_state.project, bank, pattern);
self.project_state.mark_dirty(change.bank, change.pattern);
}
fn load_step_to_editor(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
if let Some(script) = pattern_editor::get_step_script(
&self.project_state.project,
bank,
pattern,
self.editor_ctx.step,
) {
let lines: Vec<String> = if script.is_empty() {
vec![String::new()]
} else {
script.lines().map(String::from).collect()
};
self.editor_ctx.text = tui_textarea::TextArea::new(lines);
}
}
pub fn save_editor_to_step(&mut self) {
let text = self.editor_ctx.text.lines().join("\n");
let (bank, pattern) = self.current_bank_pattern();
let change = pattern_editor::set_step_script(
&mut self.project_state.project,
bank,
pattern,
self.editor_ctx.step,
text,
);
self.project_state.mark_dirty(change.bank, change.pattern);
}
pub fn compile_current_step(&mut self, link: &LinkState) {
let step_idx = self.editor_ctx.step;
let (bank, pattern) = self.current_bank_pattern();
let script =
pattern_editor::get_step_script(&self.project_state.project, bank, pattern, step_idx)
.unwrap_or_default();
if script.trim().is_empty() {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = None;
}
return;
}
let ctx = StepContext {
step: step_idx,
beat: link.beat(),
bank,
pattern,
tempo: link.tempo(),
phase: link.phase(),
slot: 0,
};
match self.script_engine.evaluate(&script, &ctx) {
Ok(cmd) => {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = Some(cmd);
}
self.ui.flash("Script compiled", 150);
}
Err(e) => {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = None;
}
self.ui.set_status(format!("Script error: {e}"));
}
}
}
pub fn compile_all_steps(&mut self, link: &LinkState) {
let pattern_len = self.current_edit_pattern().length;
let (bank, pattern) = self.current_bank_pattern();
for step_idx in 0..pattern_len {
let script = pattern_editor::get_step_script(
&self.project_state.project,
bank,
pattern,
step_idx,
)
.unwrap_or_default();
if script.trim().is_empty() {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = None;
}
continue;
}
let ctx = StepContext {
step: step_idx,
beat: 0.0,
bank,
pattern,
tempo: link.tempo(),
phase: 0.0,
slot: 0,
};
if let Ok(cmd) = self.script_engine.evaluate(&script, &ctx) {
if let Some(step) = self
.project_state
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = Some(cmd);
}
}
}
}
pub fn is_pattern_queued(
&self,
bank: usize,
pattern: usize,
snapshot: &SequencerSnapshot,
) -> Option<bool> {
self.playback.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 s = snapshot.slot_data[slot];
if s.active && s.bank == bank && s.pattern == pattern {
Some(false)
} else {
None
}
}
_ => None,
})
}
pub fn toggle_pattern_playback(
&mut self,
bank: usize,
pattern: usize,
snapshot: &SequencerSnapshot,
) {
let playing_slot = snapshot.slot_data.iter().enumerate().find_map(|(i, s)| {
if s.active && s.bank == bank && s.pattern == pattern {
Some(i)
} else {
None
}
});
let pending = self.playback.queued_changes.iter().position(|c| match *c {
SlotChange::Add {
bank: b,
pattern: p,
..
} => b == bank && p == pattern,
SlotChange::Remove { slot } => {
let s = snapshot.slot_data[slot];
s.bank == bank && s.pattern == pattern
}
});
if let Some(idx) = pending {
self.playback.queued_changes.remove(idx);
self.ui.set_status(format!(
"B{:02}:P{:02} change cancelled",
bank + 1,
pattern + 1
));
} else if let Some(slot_idx) = playing_slot {
self.playback
.queued_changes
.push(SlotChange::Remove { slot: slot_idx });
self.ui.set_status(format!(
"B{:02}:P{:02} queued to stop",
bank + 1,
pattern + 1
));
} else {
let free_slot = (0..MAX_SLOTS).find(|&i| !snapshot.slot_data[i].active);
if let Some(slot_idx) = free_slot {
self.playback.queued_changes.push(SlotChange::Add {
slot: slot_idx,
bank,
pattern,
});
self.ui.set_status(format!(
"B{:02}:P{:02} queued to play",
bank + 1,
pattern + 1
));
} else {
self.ui.set_status("All slots occupied".to_string());
}
}
}
pub fn select_edit_pattern(&mut self, pattern: usize) {
self.editor_ctx.pattern = pattern;
self.editor_ctx.step = 0;
self.load_step_to_editor();
}
pub fn select_edit_bank(&mut self, bank: usize) {
self.editor_ctx.bank = bank;
self.editor_ctx.pattern = 0;
self.editor_ctx.step = 0;
self.load_step_to_editor();
}
pub fn save(&mut self, path: PathBuf) {
self.save_editor_to_step();
match model::save(&self.project_state.project, &path) {
Ok(()) => {
self.ui.set_status(format!("Saved: {}", path.display()));
self.project_state.file_path = Some(path);
}
Err(e) => {
self.ui.set_status(format!("Save error: {e}"));
}
}
}
pub fn load(&mut self, path: PathBuf, link: &LinkState) {
match model::load(&path) {
Ok(project) => {
self.project_state.project = project;
self.editor_ctx.step = 0;
self.load_step_to_editor();
self.compile_all_steps(link);
self.mark_all_patterns_dirty();
self.ui.set_status(format!("Loaded: {}", path.display()));
self.project_state.file_path = Some(path);
}
Err(e) => {
self.ui.set_status(format!("Load error: {e}"));
}
}
}
pub fn copy_step(&mut self) {
let (bank, pattern) = self.current_bank_pattern();
let script = pattern_editor::get_step_script(
&self.project_state.project,
bank,
pattern,
self.editor_ctx.step,
);
if let Some(script) = script {
if let Some(clip) = &mut self.clipboard {
if clip.set_text(&script).is_ok() {
self.ui.set_status("Copied".to_string());
}
}
}
}
pub fn paste_step(&mut self, link: &LinkState) {
let text = self
.clipboard
.as_mut()
.and_then(|clip| clip.get_text().ok());
if let Some(text) = text {
let (bank, pattern) = self.current_bank_pattern();
let change = pattern_editor::set_step_script(
&mut self.project_state.project,
bank,
pattern,
self.editor_ctx.step,
text,
);
self.project_state.mark_dirty(change.bank, change.pattern);
self.load_step_to_editor();
self.compile_current_step(link);
}
}
pub fn open_pattern_modal(&mut self, field: PatternField) {
let current = match field {
PatternField::Length => self.current_edit_pattern().length.to_string(),
PatternField::Speed => self.current_edit_pattern().speed.label().to_string(),
};
self.ui.modal = Modal::SetPattern {
field,
input: current,
};
}
pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) {
match cmd {
// Playback
AppCommand::TogglePlaying => self.toggle_playing(),
AppCommand::TempoUp => self.tempo_up(link),
AppCommand::TempoDown => self.tempo_down(link),
// Navigation
AppCommand::NextStep => self.next_step(),
AppCommand::PrevStep => self.prev_step(),
AppCommand::StepUp => self.step_up(),
AppCommand::StepDown => self.step_down(),
AppCommand::ToggleFocus => self.toggle_focus(link),
AppCommand::SelectEditBank(bank) => self.select_edit_bank(bank),
AppCommand::SelectEditPattern(pattern) => self.select_edit_pattern(pattern),
// Pattern editing
AppCommand::ToggleStep => self.toggle_step(),
AppCommand::LengthIncrease => self.length_increase(),
AppCommand::LengthDecrease => self.length_decrease(),
AppCommand::SpeedIncrease => self.speed_increase(),
AppCommand::SpeedDecrease => self.speed_decrease(),
AppCommand::SetLength {
bank,
pattern,
length,
} => {
let (change, new_len) = pattern_editor::set_length(
&mut self.project_state.project,
bank,
pattern,
length,
);
if self.editor_ctx.bank == bank
&& self.editor_ctx.pattern == pattern
&& self.editor_ctx.step >= new_len
{
self.editor_ctx.step = new_len - 1;
}
self.project_state.mark_dirty(change.bank, change.pattern);
}
AppCommand::SetSpeed {
bank,
pattern,
speed,
} => {
let change = pattern_editor::set_speed(
&mut self.project_state.project,
bank,
pattern,
speed,
);
self.project_state.mark_dirty(change.bank, change.pattern);
}
// Script editing
AppCommand::SaveEditorToStep => self.save_editor_to_step(),
AppCommand::CompileCurrentStep => self.compile_current_step(link),
AppCommand::CompileAllSteps => self.compile_all_steps(link),
// Clipboard
AppCommand::CopyStep => self.copy_step(),
AppCommand::PasteStep => self.paste_step(link),
// Pattern playback
AppCommand::QueueSlotChange(change) => {
self.playback.queued_changes.push(change);
}
AppCommand::TogglePatternPlayback { bank, pattern } => {
self.toggle_pattern_playback(bank, pattern, snapshot);
}
// Project
AppCommand::RenameBank { bank, name } => {
self.project_state.project.banks[bank].name = name;
}
AppCommand::RenamePattern {
bank,
pattern,
name,
} => {
self.project_state.project.banks[bank].patterns[pattern].name = name;
}
AppCommand::Save(path) => self.save(path),
AppCommand::Load(path) => self.load(path, link),
// UI
AppCommand::SetStatus(msg) => self.ui.set_status(msg),
AppCommand::ClearStatus => self.ui.clear_status(),
AppCommand::Flash {
message,
duration_ms,
} => self.ui.flash(&message, duration_ms),
AppCommand::OpenModal(modal) => self.ui.modal = modal,
AppCommand::CloseModal => self.ui.modal = Modal::None,
AppCommand::OpenPatternModal(field) => self.open_pattern_modal(field),
// Page navigation
AppCommand::PageLeft => self.page.left(),
AppCommand::PageRight => self.page.right(),
AppCommand::PageUp => self.page.up(),
AppCommand::PageDown => self.page.down(),
// Doc navigation
AppCommand::DocNextTopic => {
self.ui.doc_topic = (self.ui.doc_topic + 1) % doc_view::topic_count();
self.ui.doc_scroll = 0;
}
AppCommand::DocPrevTopic => {
let count = doc_view::topic_count();
self.ui.doc_topic = (self.ui.doc_topic + count - 1) % count;
self.ui.doc_scroll = 0;
}
AppCommand::DocScrollDown(n) => {
self.ui.doc_scroll = self.ui.doc_scroll.saturating_add(n);
}
AppCommand::DocScrollUp(n) => {
self.ui.doc_scroll = self.ui.doc_scroll.saturating_sub(n);
}
// Patterns view
AppCommand::PatternsCursorLeft => {
self.patterns_cursor = (self.patterns_cursor + 15) % 16;
}
AppCommand::PatternsCursorRight => {
self.patterns_cursor = (self.patterns_cursor + 1) % 16;
}
AppCommand::PatternsCursorUp => {
self.patterns_cursor = (self.patterns_cursor + 12) % 16;
}
AppCommand::PatternsCursorDown => {
self.patterns_cursor = (self.patterns_cursor + 4) % 16;
}
AppCommand::PatternsEnter => match self.patterns_view_level {
PatternsViewLevel::Banks => {
let bank = self.patterns_cursor;
self.patterns_view_level = PatternsViewLevel::Patterns { bank };
self.patterns_cursor = 0;
}
PatternsViewLevel::Patterns { bank } => {
let pattern = self.patterns_cursor;
self.select_edit_bank(bank);
self.select_edit_pattern(pattern);
self.patterns_view_level = PatternsViewLevel::Banks;
self.patterns_cursor = 0;
self.page.down();
}
},
AppCommand::PatternsBack => match self.patterns_view_level {
PatternsViewLevel::Banks => self.page.down(),
PatternsViewLevel::Patterns { .. } => {
self.patterns_view_level = PatternsViewLevel::Banks;
self.patterns_cursor = 0;
}
},
}
}
pub fn flush_queued_changes(&mut self, cmd_tx: &Sender<SeqCommand>) {
for change in self.playback.queued_changes.drain(..) {
match change {
SlotChange::Add {
slot,
bank,
pattern,
} => {
let _ = cmd_tx.send(SeqCommand::SlotAdd {
slot,
bank,
pattern,
});
}
SlotChange::Remove { slot } => {
let _ = cmd_tx.send(SeqCommand::SlotRemove { slot });
}
}
}
}
pub fn flush_dirty_patterns(&mut self, cmd_tx: &Sender<SeqCommand>) {
for (bank, pattern) in self.project_state.take_dirty() {
let pat = self.project_state.project.pattern_at(bank, pattern);
let snapshot = PatternSnapshot {
speed: pat.speed,
length: pat.length,
steps: pat
.steps
.iter()
.take(pat.length)
.map(|s| StepSnapshot {
active: s.active,
script: s.script.clone(),
})
.collect(),
};
let _ = cmd_tx.send(SeqCommand::PatternUpdate {
bank,
pattern,
data: snapshot,
});
}
}
}