seq
This commit is contained in:
375
seq/src/app.rs
Normal file
375
seq/src/app.rs
Normal file
@@ -0,0 +1,375 @@
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
use tui_textarea::TextArea;
|
||||
|
||||
use crate::file;
|
||||
use crate::link::LinkState;
|
||||
use crate::model::{Pattern, Project};
|
||||
use crate::page::Page;
|
||||
use crate::script::{ScriptEngine, StepContext};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Focus {
|
||||
Sequencer,
|
||||
Editor,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum Modal {
|
||||
None,
|
||||
ConfirmQuit,
|
||||
SaveAs(String),
|
||||
LoadFrom(String),
|
||||
PatternPicker { cursor: usize },
|
||||
BankPicker { cursor: usize },
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub tempo: f64,
|
||||
pub beat: f64,
|
||||
pub phase: f64,
|
||||
pub peers: u64,
|
||||
pub playing: bool,
|
||||
#[allow(dead_code)]
|
||||
pub quantum: f64,
|
||||
|
||||
pub project: Project,
|
||||
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 event_count: usize,
|
||||
pub active_voices: usize,
|
||||
pub peak_voices: usize,
|
||||
pub cpu_load: f32,
|
||||
pub schedule_depth: usize,
|
||||
pub sample_pool_mb: f32,
|
||||
pub scope: [f32; 64],
|
||||
pub script_engine: ScriptEngine,
|
||||
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>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(tempo: f64, quantum: f64) -> Self {
|
||||
Self {
|
||||
tempo,
|
||||
beat: 0.0,
|
||||
phase: 0.0,
|
||||
peers: 0,
|
||||
playing: true,
|
||||
quantum,
|
||||
|
||||
project: Project::default(),
|
||||
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,
|
||||
event_count: 0,
|
||||
active_voices: 0,
|
||||
peak_voices: 0,
|
||||
cpu_load: 0.0,
|
||||
schedule_depth: 0,
|
||||
sample_pool_mb: 0.0,
|
||||
scope: [0.0; 64],
|
||||
script_engine: ScriptEngine::new(),
|
||||
file_path: None,
|
||||
status_message: None,
|
||||
editor: TextArea::default(),
|
||||
flash_until: None,
|
||||
modal: Modal::None,
|
||||
clipboard: arboard::Clipboard::new().ok(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_from_link(&mut self, link: &LinkState) {
|
||||
let (tempo, beat, phase, peers) = link.query();
|
||||
self.tempo = tempo;
|
||||
self.beat = beat;
|
||||
self.phase = phase;
|
||||
self.peers = peers;
|
||||
}
|
||||
|
||||
pub fn toggle_playing(&mut self) {
|
||||
self.playing = !self.playing;
|
||||
}
|
||||
|
||||
pub fn tempo_up(&mut self, link: &LinkState) {
|
||||
self.tempo = (self.tempo + 1.0).min(300.0);
|
||||
link.set_tempo(self.tempo);
|
||||
}
|
||||
|
||||
pub fn tempo_down(&mut self, link: &LinkState) {
|
||||
self.tempo = (self.tempo - 1.0).max(20.0);
|
||||
link.set_tempo(self.tempo);
|
||||
}
|
||||
|
||||
pub fn toggle_focus(&mut self) {
|
||||
match self.focus {
|
||||
Focus::Sequencer => {
|
||||
self.focus = Focus::Editor;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
Focus::Editor => {
|
||||
self.save_editor_to_step();
|
||||
self.compile_current_step();
|
||||
self.focus = Focus::Sequencer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_edit_pattern(&self) -> &Pattern {
|
||||
self.project.pattern_at(self.edit_bank, self.edit_pattern)
|
||||
}
|
||||
|
||||
pub fn next_step(&mut self) {
|
||||
let len = self.current_edit_pattern().length;
|
||||
self.current_step = (self.current_step + 1) % len;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn prev_step(&mut self) {
|
||||
let len = self.current_edit_pattern().length;
|
||||
self.current_step = (self.current_step + len - 1) % len;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn step_up(&mut self) {
|
||||
let len = self.current_edit_pattern().length;
|
||||
if self.current_step >= 8 {
|
||||
self.current_step -= 8;
|
||||
} else {
|
||||
self.current_step = (self.current_step + len - 8) % 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;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn toggle_step(&mut self) {
|
||||
let step_idx = self.current_step;
|
||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||
step.active = !step.active;
|
||||
}
|
||||
}
|
||||
|
||||
fn load_step_to_editor(&mut self) {
|
||||
let step_idx = self.current_step;
|
||||
if let Some(step) = self.current_edit_pattern().step(step_idx) {
|
||||
let lines: Vec<String> = if step.script.is_empty() {
|
||||
vec![String::new()]
|
||||
} else {
|
||||
step.script.lines().map(String::from).collect()
|
||||
};
|
||||
self.editor = TextArea::new(lines);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_editor_to_step(&mut self) {
|
||||
let text = self.editor.lines().join("\n");
|
||||
let step_idx = self.current_step;
|
||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||
step.script = text;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compile_current_step(&mut self) {
|
||||
let step_idx = self.current_step;
|
||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
||||
|
||||
let script = self
|
||||
.project
|
||||
.pattern_at(bank, pattern)
|
||||
.step(step_idx)
|
||||
.map(|s| s.script.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
if script.trim().is_empty() {
|
||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||
step.command = None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let ctx = StepContext {
|
||||
step: step_idx,
|
||||
beat: self.beat,
|
||||
bank,
|
||||
pattern,
|
||||
tempo: self.tempo,
|
||||
phase: self.phase,
|
||||
};
|
||||
|
||||
match self.script_engine.evaluate(&script, &ctx) {
|
||||
Ok(cmd) => {
|
||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||
step.command = Some(cmd);
|
||||
}
|
||||
self.status_message = Some("Script compiled".to_string());
|
||||
self.flash_until = Some(Instant::now() + std::time::Duration::from_millis(150));
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||
step.command = None;
|
||||
}
|
||||
self.status_message = Some(format!("Script error: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compile_all_steps(&mut self) {
|
||||
let pattern_len = self.current_edit_pattern().length;
|
||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
||||
|
||||
for step_idx in 0..pattern_len {
|
||||
let script = self
|
||||
.project
|
||||
.pattern_at(bank, pattern)
|
||||
.step(step_idx)
|
||||
.map(|s| s.script.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
if script.trim().is_empty() {
|
||||
if let Some(step) = self.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: self.tempo,
|
||||
phase: 0.0,
|
||||
};
|
||||
|
||||
if let Ok(cmd) = self.script_engine.evaluate(&script, &ctx) {
|
||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||
step.command = Some(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 select_edit_pattern(&mut self, pattern: usize) {
|
||||
self.edit_pattern = pattern;
|
||||
self.current_step = 0;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn select_edit_bank(&mut self, bank: usize) {
|
||||
self.edit_bank = bank;
|
||||
self.edit_pattern = 0;
|
||||
self.current_step = 0;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn save(&mut self, path: PathBuf) {
|
||||
self.save_editor_to_step();
|
||||
match file::save(&self.project, &path) {
|
||||
Ok(()) => {
|
||||
self.status_message = Some(format!("Saved: {}", path.display()));
|
||||
self.file_path = Some(path);
|
||||
}
|
||||
Err(e) => {
|
||||
self.status_message = Some(format!("Save error: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&mut self, path: PathBuf) {
|
||||
match file::load(&path) {
|
||||
Ok(project) => {
|
||||
self.project = project;
|
||||
self.current_step = 0;
|
||||
self.load_step_to_editor();
|
||||
self.compile_all_steps();
|
||||
self.status_message = Some(format!("Loaded: {}", path.display()));
|
||||
self.file_path = Some(path);
|
||||
}
|
||||
Err(e) => {
|
||||
self.status_message = Some(format!("Load error: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_status(&mut self) {
|
||||
self.status_message = None;
|
||||
}
|
||||
|
||||
pub fn is_flashing(&self) -> bool {
|
||||
self.flash_until
|
||||
.map(|t| Instant::now() < t)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn copy_step(&mut self) {
|
||||
let step_idx = self.current_step;
|
||||
let script = self
|
||||
.current_edit_pattern()
|
||||
.step(step_idx)
|
||||
.map(|s| s.script.clone());
|
||||
|
||||
if let Some(script) = script {
|
||||
if let Some(clip) = &mut self.clipboard {
|
||||
if clip.set_text(&script).is_ok() {
|
||||
self.status_message = Some("Copied".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paste_step(&mut self) {
|
||||
let text = self
|
||||
.clipboard
|
||||
.as_mut()
|
||||
.and_then(|clip| clip.get_text().ok());
|
||||
|
||||
if let Some(text) = text {
|
||||
let step_idx = self.current_step;
|
||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||
step.script = text;
|
||||
}
|
||||
self.load_step_to_editor();
|
||||
self.compile_current_step();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user