From ce0014020f38a2d05fddfab5bb1e4b3ac121b65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Tue, 20 Jan 2026 02:11:51 +0100 Subject: [PATCH] wip --- Cargo.lock | 1 + seq/Cargo.toml | 1 + seq/src/app.rs | 515 +++++++++++++++++++-------------- seq/src/audio.rs | 187 ++++-------- seq/src/config.rs | 7 + seq/src/link.rs | 4 +- seq/src/main.rs | 273 ++++++++--------- seq/src/model.rs | 12 +- seq/src/script.rs | 67 ++--- seq/src/sequencer.rs | 328 +++++++++++++++++++++ seq/src/ui.rs | 8 +- seq/src/views/audio_view.rs | 36 +-- seq/src/views/main_view.rs | 29 +- seq/src/views/patterns_view.rs | 14 +- seq/src/widgets/scope.rs | 5 - src/lib.rs | 49 +++- 16 files changed, 941 insertions(+), 595 deletions(-) create mode 100644 seq/src/config.rs create mode 100644 seq/src/sequencer.rs diff --git a/Cargo.lock b/Cargo.lock index e22e5eb..93e293c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2091,6 +2091,7 @@ dependencies = [ "arboard", "clap", "cpal", + "crossbeam-channel", "crossterm", "doux", "minimad", diff --git a/seq/Cargo.toml b/seq/Cargo.toml index 4f36807..9ae01cc 100644 --- a/seq/Cargo.toml +++ b/seq/Cargo.toml @@ -21,3 +21,4 @@ serde_json = "1" tui-textarea = "0.7" arboard = "3" minimad = "0.13" +crossbeam-channel = "0.5" diff --git a/seq/src/app.rs b/seq/src/app.rs index 220b8dd..000167a 100644 --- a/seq/src/app.rs +++ b/seq/src/app.rs @@ -1,19 +1,26 @@ use doux::audio::AudioDeviceInfo; use rand::rngs::StdRng; use rand::SeedableRng; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; 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::config::{MAX_BANKS, MAX_PATTERNS, MAX_SLOTS}; use crate::file; use crate::link::LinkState; use crate::model::{Pattern, Project}; use crate::page::Page; -use crate::script::{Rng, ScriptEngine, StepContext, Variables}; +use crate::script::{ScriptEngine, StepContext, Variables, Rng}; +use crate::sequencer::SlotState; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum SlotChange { + Add { slot: usize, bank: usize, pattern: usize }, + Remove { slot: usize }, +} #[derive(Clone, Copy, PartialEq, Eq)] pub enum Focus { @@ -81,6 +88,209 @@ pub enum AudioFocus { SamplePaths, } +pub struct Metrics { + pub event_count: usize, + pub active_voices: usize, + pub peak_voices: usize, + pub cpu_load: f32, + pub schedule_depth: usize, + pub scope: [f32; 64], +} + +impl Default for Metrics { + fn default() -> Self { + Self { + event_count: 0, + active_voices: 0, + peak_voices: 0, + cpu_load: 0.0, + schedule_depth: 0, + scope: [0.0; 64], + } + } +} + +pub struct EditorContext { + pub bank: usize, + pub pattern: usize, + pub step: usize, + pub focus: Focus, + pub text: TextArea<'static>, +} + +impl Default for EditorContext { + fn default() -> Self { + Self { + bank: 0, + pattern: 0, + step: 0, + focus: Focus::Sequencer, + text: TextArea::default(), + } + } +} + +pub struct AudioSettings { + pub config: AudioConfig, + pub focus: AudioFocus, + pub output_devices: Vec, + pub input_devices: Vec, + pub restart_pending: bool, +} + +impl Default for AudioSettings { + fn default() -> Self { + Self { + config: AudioConfig::default(), + focus: AudioFocus::default(), + output_devices: doux::audio::list_output_devices(), + input_devices: doux::audio::list_input_devices(), + restart_pending: false, + } + } +} + +impl AudioSettings { + pub fn refresh_devices(&mut self) { + self.output_devices = doux::audio::list_output_devices(); + self.input_devices = doux::audio::list_input_devices(); + } + + pub fn next_focus(&mut self) { + self.focus = match self.focus { + AudioFocus::OutputDevice => AudioFocus::InputDevice, + AudioFocus::InputDevice => AudioFocus::Channels, + AudioFocus::Channels => AudioFocus::BufferSize, + AudioFocus::BufferSize => AudioFocus::SamplePaths, + AudioFocus::SamplePaths => AudioFocus::OutputDevice, + }; + } + + pub fn prev_focus(&mut self) { + self.focus = match self.focus { + AudioFocus::OutputDevice => AudioFocus::SamplePaths, + AudioFocus::InputDevice => AudioFocus::OutputDevice, + AudioFocus::Channels => AudioFocus::InputDevice, + AudioFocus::BufferSize => AudioFocus::Channels, + AudioFocus::SamplePaths => AudioFocus::BufferSize, + }; + } + + pub fn next_output_device(&mut self) { + if self.output_devices.is_empty() { + return; + } + let current_idx = self.current_output_device_index(); + let next_idx = (current_idx + 1) % self.output_devices.len(); + self.config.output_device = Some(self.output_devices[next_idx].name.clone()); + } + + pub fn prev_output_device(&mut self) { + if self.output_devices.is_empty() { + return; + } + let current_idx = self.current_output_device_index(); + let prev_idx = (current_idx + self.output_devices.len() - 1) % self.output_devices.len(); + self.config.output_device = Some(self.output_devices[prev_idx].name.clone()); + } + + fn current_output_device_index(&self) -> usize { + match &self.config.output_device { + Some(name) => self + .output_devices + .iter() + .position(|d| &d.name == name) + .unwrap_or(0), + None => self + .output_devices + .iter() + .position(|d| d.is_default) + .unwrap_or(0), + } + } + + pub fn next_input_device(&mut self) { + if self.input_devices.is_empty() { + return; + } + let current_idx = self.current_input_device_index(); + let next_idx = (current_idx + 1) % self.input_devices.len(); + self.config.input_device = Some(self.input_devices[next_idx].name.clone()); + } + + pub fn prev_input_device(&mut self) { + if self.input_devices.is_empty() { + return; + } + let current_idx = self.current_input_device_index(); + let prev_idx = (current_idx + self.input_devices.len() - 1) % self.input_devices.len(); + self.config.input_device = Some(self.input_devices[prev_idx].name.clone()); + } + + fn current_input_device_index(&self) -> usize { + match &self.config.input_device { + Some(name) => self + .input_devices + .iter() + .position(|d| &d.name == name) + .unwrap_or(0), + None => self + .input_devices + .iter() + .position(|d| d.is_default) + .unwrap_or(0), + } + } + + pub fn adjust_channels(&mut self, delta: i16) { + let new_val = (self.config.channels as i16 + delta).clamp(1, 64) as u16; + self.config.channels = new_val; + } + + pub fn adjust_buffer_size(&mut self, delta: i32) { + let new_val = (self.config.buffer_size as i32 + delta).clamp(64, 4096) as u32; + self.config.buffer_size = new_val; + } + + pub fn current_output_device_name(&self) -> &str { + match &self.config.output_device { + Some(name) => name, + None => self + .output_devices + .iter() + .find(|d| d.is_default) + .map(|d| d.name.as_str()) + .unwrap_or("Default"), + } + } + + pub fn current_input_device_name(&self) -> &str { + match &self.config.input_device { + Some(name) => name, + None => self + .input_devices + .iter() + .find(|d| d.is_default) + .map(|d| d.name.as_str()) + .unwrap_or("None"), + } + } + + pub fn add_sample_path(&mut self, path: PathBuf) { + if !self.config.sample_paths.contains(&path) { + self.config.sample_paths.push(path); + } + } + + pub fn remove_last_sample_path(&mut self) { + self.config.sample_paths.pop(); + } + + pub fn trigger_restart(&mut self) { + self.restart_pending = true; + } +} + pub struct App { pub tempo: f64, pub beat: f64, @@ -91,49 +301,39 @@ pub struct App { pub quantum: f64, pub project: Project, - pub focus: Focus, pub page: Page, - pub current_step: usize, - - pub edit_bank: usize, - pub edit_pattern: usize, + pub editor_ctx: EditorContext, 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_data: [SlotState; MAX_SLOTS], pub slot_steps: [usize; MAX_SLOTS], pub queued_changes: Vec, - pub event_count: usize, - pub active_voices: usize, - pub peak_voices: usize, - pub cpu_load: f32, - pub schedule_depth: usize, + pub metrics: Metrics, pub sample_pool_mb: f32, - pub scope: [f32; 64], pub script_engine: ScriptEngine, pub variables: Variables, pub rng: Rng, pub file_path: Option, pub status_message: Option, - pub editor: TextArea<'static>, pub flash_until: Option, pub modal: Modal, pub clipboard: Option, pub doc_topic: usize, pub doc_scroll: usize, - pub audio_config: AudioConfig, - pub audio_focus: AudioFocus, - pub available_output_devices: Vec, - pub available_input_devices: Vec, - pub restart_pending: bool, + pub audio: AudioSettings, + pub dirty_patterns: HashSet<(usize, usize)>, } impl App { pub fn new(tempo: f64, quantum: f64) -> 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 { tempo, beat: 0.0, @@ -143,44 +343,39 @@ impl App { quantum, project: Project::default(), - focus: Focus::Sequencer, page: Page::default(), - current_step: 0, - - edit_bank: 0, - edit_pattern: 0, + editor_ctx: EditorContext::default(), patterns_view_level: PatternsViewLevel::default(), patterns_cursor: 0, - slot_data: [(false, 0, 0); MAX_SLOTS], + slot_data: [SlotState::default(); MAX_SLOTS], slot_steps: [0; MAX_SLOTS], queued_changes: Vec::new(), - event_count: 0, - active_voices: 0, - peak_voices: 0, - cpu_load: 0.0, - schedule_depth: 0, + metrics: Metrics::default(), 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))), + variables, + rng, + script_engine, 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, - audio_config: AudioConfig::default(), - audio_focus: AudioFocus::default(), - available_output_devices: doux::audio::list_output_devices(), - available_input_devices: doux::audio::list_input_devices(), - restart_pending: false, + audio: AudioSettings::default(), + dirty_patterns: HashSet::new(), + } + } + + pub fn mark_all_patterns_dirty(&mut self) { + for bank in 0..MAX_BANKS { + for pattern in 0..MAX_PATTERNS { + self.dirty_patterns.insert((bank, pattern)); + } } } @@ -207,32 +402,32 @@ impl App { } pub fn toggle_focus(&mut self) { - match self.focus { + match self.editor_ctx.focus { Focus::Sequencer => { - self.focus = Focus::Editor; + self.editor_ctx.focus = Focus::Editor; self.load_step_to_editor(); } Focus::Editor => { self.save_editor_to_step(); self.compile_current_step(); - self.focus = Focus::Sequencer; + self.editor_ctx.focus = Focus::Sequencer; } } } pub fn current_edit_pattern(&self) -> &Pattern { - self.project.pattern_at(self.edit_bank, self.edit_pattern) + self.project.pattern_at(self.editor_ctx.bank, self.editor_ctx.pattern) } pub fn next_step(&mut self) { let len = self.current_edit_pattern().length; - self.current_step = (self.current_step + 1) % len; + 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.current_step = (self.current_step + len - 1) % len; + self.editor_ctx.step = (self.editor_ctx.step + len - 1) % len; self.load_step_to_editor(); } @@ -246,10 +441,10 @@ impl App { }; let steps_per_row = len.div_ceil(num_rows); - if self.current_step >= steps_per_row { - self.current_step -= steps_per_row; + if self.editor_ctx.step >= steps_per_row { + self.editor_ctx.step -= steps_per_row; } else { - self.current_step = (self.current_step + len - steps_per_row) % len; + self.editor_ctx.step = (self.editor_ctx.step + len - steps_per_row) % len; } self.load_step_to_editor(); } @@ -264,75 +459,81 @@ impl App { }; let steps_per_row = len.div_ceil(num_rows); - self.current_step = (self.current_step + steps_per_row) % len; + self.editor_ctx.step = (self.editor_ctx.step + steps_per_row) % 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); + let step_idx = self.editor_ctx.step; + let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern); if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) { step.active = !step.active; } + self.dirty_patterns.insert((bank, pattern)); } pub fn length_increase(&mut self) { - let (bank, pattern) = (self.edit_bank, self.edit_pattern); + let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern); let current_len = self.project.pattern_at(bank, pattern).length; self.project .pattern_at_mut(bank, pattern) .set_length(current_len + 1); + self.dirty_patterns.insert((bank, pattern)); } pub fn length_decrease(&mut self) { - let (bank, pattern) = (self.edit_bank, self.edit_pattern); + let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.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; + if self.editor_ctx.step >= new_len { + self.editor_ctx.step = new_len - 1; self.load_step_to_editor(); } + self.dirty_patterns.insert((bank, pattern)); } pub fn speed_increase(&mut self) { - let (bank, pattern) = (self.edit_bank, self.edit_pattern); + let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern); let pat = self.project.pattern_at_mut(bank, pattern); pat.speed = pat.speed.next(); + self.dirty_patterns.insert((bank, pattern)); } pub fn speed_decrease(&mut self) { - let (bank, pattern) = (self.edit_bank, self.edit_pattern); + let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern); let pat = self.project.pattern_at_mut(bank, pattern); pat.speed = pat.speed.prev(); + self.dirty_patterns.insert((bank, pattern)); } fn load_step_to_editor(&mut self) { - let step_idx = self.current_step; + let step_idx = self.editor_ctx.step; if let Some(step) = self.current_edit_pattern().step(step_idx) { let lines: Vec = if step.script.is_empty() { vec![String::new()] } else { step.script.lines().map(String::from).collect() }; - self.editor = TextArea::new(lines); + self.editor_ctx.text = 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); + let text = self.editor_ctx.text.lines().join("\n"); + let step_idx = self.editor_ctx.step; + let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern); if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) { step.script = text; } + self.dirty_patterns.insert((bank, pattern)); } pub fn compile_current_step(&mut self) { - let step_idx = self.current_step; - let (bank, pattern) = (self.edit_bank, self.edit_pattern); + let step_idx = self.editor_ctx.step; + let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern); let script = self .project @@ -358,7 +559,7 @@ impl App { slot: 0, }; - match self.script_engine.evaluate(&script, &ctx, &self.variables, &self.rng) { + 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); @@ -377,7 +578,7 @@ impl App { pub fn compile_all_steps(&mut self) { let pattern_len = self.current_edit_pattern().length; - let (bank, pattern) = (self.edit_bank, self.edit_pattern); + let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern); for step_idx in 0..pattern_len { let script = self @@ -404,7 +605,7 @@ impl App { slot: 0, }; - if let Ok(cmd) = self.script_engine.evaluate(&script, &ctx, &self.variables, &self.rng) { + 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); } @@ -418,8 +619,8 @@ impl App { Some(true) } SlotChange::Remove { slot } => { - let (active, b, p) = self.slot_data[*slot]; - if active && b == bank && p == pattern { + let s = self.slot_data[*slot]; + if s.active && s.bank == bank && s.pattern == pattern { Some(false) } else { None @@ -430,8 +631,8 @@ impl App { } 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 { + let playing_slot = self.slot_data.iter().enumerate().find_map(|(i, s)| { + if s.active && s.bank == bank && s.pattern == pattern { Some(i) } else { None @@ -441,8 +642,8 @@ impl App { 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 + let s = self.slot_data[*slot]; + s.bank == bank && s.pattern == pattern } }); @@ -453,7 +654,7 @@ impl App { 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); + let free_slot = (0..MAX_SLOTS).find(|&i| !self.slot_data[i].active); 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)); @@ -464,15 +665,15 @@ impl App { } pub fn select_edit_pattern(&mut self, pattern: usize) { - self.edit_pattern = pattern; - self.current_step = 0; + 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.edit_bank = bank; - self.edit_pattern = 0; - self.current_step = 0; + self.editor_ctx.bank = bank; + self.editor_ctx.pattern = 0; + self.editor_ctx.step = 0; self.load_step_to_editor(); } @@ -493,9 +694,10 @@ impl App { match file::load(&path) { Ok(project) => { self.project = project; - self.current_step = 0; + self.editor_ctx.step = 0; self.load_step_to_editor(); self.compile_all_steps(); + self.mark_all_patterns_dirty(); self.status_message = Some(format!("Loaded: {}", path.display())); self.file_path = Some(path); } @@ -516,7 +718,7 @@ impl App { } pub fn copy_step(&mut self) { - let step_idx = self.current_step; + let step_idx = self.editor_ctx.step; let script = self .current_edit_pattern() .step(step_idx) @@ -538,11 +740,12 @@ impl App { .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); + let step_idx = self.editor_ctx.step; + let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern); if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) { step.script = text; } + self.dirty_patterns.insert((bank, pattern)); self.load_step_to_editor(); self.compile_current_step(); } @@ -556,142 +759,4 @@ impl App { self.modal = Modal::SetPattern { field, input: current }; } - pub fn refresh_audio_devices(&mut self) { - self.available_output_devices = doux::audio::list_output_devices(); - self.available_input_devices = doux::audio::list_input_devices(); - } - - pub fn next_audio_focus(&mut self) { - self.audio_focus = match self.audio_focus { - AudioFocus::OutputDevice => AudioFocus::InputDevice, - AudioFocus::InputDevice => AudioFocus::Channels, - AudioFocus::Channels => AudioFocus::BufferSize, - AudioFocus::BufferSize => AudioFocus::SamplePaths, - AudioFocus::SamplePaths => AudioFocus::OutputDevice, - }; - } - - pub fn prev_audio_focus(&mut self) { - self.audio_focus = match self.audio_focus { - AudioFocus::OutputDevice => AudioFocus::SamplePaths, - AudioFocus::InputDevice => AudioFocus::OutputDevice, - AudioFocus::Channels => AudioFocus::InputDevice, - AudioFocus::BufferSize => AudioFocus::Channels, - AudioFocus::SamplePaths => AudioFocus::BufferSize, - }; - } - - pub fn next_output_device(&mut self) { - if self.available_output_devices.is_empty() { - return; - } - let current_idx = self.current_output_device_index(); - let next_idx = (current_idx + 1) % self.available_output_devices.len(); - self.audio_config.output_device = Some(self.available_output_devices[next_idx].name.clone()); - } - - pub fn prev_output_device(&mut self) { - if self.available_output_devices.is_empty() { - return; - } - let current_idx = self.current_output_device_index(); - let prev_idx = (current_idx + self.available_output_devices.len() - 1) % self.available_output_devices.len(); - self.audio_config.output_device = Some(self.available_output_devices[prev_idx].name.clone()); - } - - fn current_output_device_index(&self) -> usize { - match &self.audio_config.output_device { - Some(name) => self - .available_output_devices - .iter() - .position(|d| &d.name == name) - .unwrap_or(0), - None => self - .available_output_devices - .iter() - .position(|d| d.is_default) - .unwrap_or(0), - } - } - - pub fn next_input_device(&mut self) { - if self.available_input_devices.is_empty() { - return; - } - let current_idx = self.current_input_device_index(); - let next_idx = (current_idx + 1) % self.available_input_devices.len(); - self.audio_config.input_device = Some(self.available_input_devices[next_idx].name.clone()); - } - - pub fn prev_input_device(&mut self) { - if self.available_input_devices.is_empty() { - return; - } - let current_idx = self.current_input_device_index(); - let prev_idx = (current_idx + self.available_input_devices.len() - 1) % self.available_input_devices.len(); - self.audio_config.input_device = Some(self.available_input_devices[prev_idx].name.clone()); - } - - fn current_input_device_index(&self) -> usize { - match &self.audio_config.input_device { - Some(name) => self - .available_input_devices - .iter() - .position(|d| &d.name == name) - .unwrap_or(0), - None => self - .available_input_devices - .iter() - .position(|d| d.is_default) - .unwrap_or(0), - } - } - - pub fn adjust_channels(&mut self, delta: i16) { - let new_val = (self.audio_config.channels as i16 + delta).clamp(1, 64) as u16; - self.audio_config.channels = new_val; - } - - pub fn adjust_buffer_size(&mut self, delta: i32) { - let new_val = (self.audio_config.buffer_size as i32 + delta).clamp(64, 4096) as u32; - self.audio_config.buffer_size = new_val; - } - - pub fn trigger_restart(&mut self) { - self.restart_pending = true; - } - - pub fn current_output_device_name(&self) -> &str { - match &self.audio_config.output_device { - Some(name) => name, - None => self - .available_output_devices - .iter() - .find(|d| d.is_default) - .map(|d| d.name.as_str()) - .unwrap_or("Default"), - } - } - - pub fn current_input_device_name(&self) -> &str { - match &self.audio_config.input_device { - Some(name) => name, - None => self - .available_input_devices - .iter() - .find(|d| d.is_default) - .map(|d| d.name.as_str()) - .unwrap_or("None"), - } - } - - pub fn add_sample_path(&mut self, path: PathBuf) { - if !self.audio_config.sample_paths.contains(&path) { - self.audio_config.sample_paths.push(path); - } - } - - pub fn remove_last_sample_path(&mut self) { - self.audio_config.sample_paths.pop(); - } } diff --git a/seq/src/audio.rs b/seq/src/audio.rs index 71ee223..945ff90 100644 --- a/seq/src/audio.rs +++ b/seq/src/audio.rs @@ -1,12 +1,34 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::Stream; -use doux::Engine; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; +use crossbeam_channel::Receiver; +use doux::{Engine, EngineMetrics}; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::Arc; -use crate::link::LinkState; -use crate::model::Project; -use crate::script::{Rng, ScriptEngine, StepContext, Variables}; +use crate::sequencer::AudioCommand; + +pub struct ScopeBuffer { + pub samples: [AtomicU32; 64], +} + +impl ScopeBuffer { + pub fn new() -> Self { + Self { + samples: std::array::from_fn(|_| AtomicU32::new(0)), + } + } + + pub fn write(&self, data: &[f32]) { + for (i, atom) in self.samples.iter().enumerate() { + let val = data.get(i * 2).copied().unwrap_or(0.0); + atom.store(val.to_bits(), Ordering::Relaxed); + } + } + + pub fn read(&self) -> [f32; 64] { + std::array::from_fn(|i| f32::from_bits(self.samples[i].load(Ordering::Relaxed))) + } +} pub struct AudioStreamConfig { pub output_device: Option, @@ -14,8 +36,6 @@ pub struct AudioStreamConfig { pub buffer_size: u32, } -pub const MAX_SLOTS: usize = 8; - #[derive(Clone, Copy, Default)] pub struct PatternSlot { pub bank: usize, @@ -24,38 +44,12 @@ pub struct PatternSlot { pub active: bool, } -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum SlotChange { - Add { slot: usize, bank: usize, pattern: usize }, - Remove { slot: usize }, -} - -pub struct AudioState { - prev_beat: f64, - pub slots: [PatternSlot; MAX_SLOTS], -} - -impl AudioState { - fn new() -> Self { - Self { - prev_beat: -1.0, - slots: [PatternSlot::default(); MAX_SLOTS], - } - } -} - pub fn build_stream( config: &AudioStreamConfig, - engine: Arc>, - link: Arc, - playing: Arc, - project: Arc>, - slot_steps: [Arc; MAX_SLOTS], - event_count: Arc, - slot_data: Arc>, - slot_changes: Arc>>, - variables: Variables, - rng: Rng, + audio_rx: Receiver, + scope_buffer: Arc, + metrics: Arc, + initial_samples: Vec, ) -> Result<(Stream, f32), String> { let host = cpal::default_host(); @@ -80,12 +74,13 @@ pub fn build_stream( buffer_size, }; - let quantum = 4.0; - let audio_state = Arc::new(Mutex::new(AudioState::new())); - let script_engine = ScriptEngine::new(); - let sr = sample_rate; let channels = config.channels as usize; + let metrics_clone = Arc::clone(&metrics); + + let mut engine = Engine::new_with_metrics(sample_rate, channels, Arc::clone(&metrics)); + engine.sample_index = initial_samples; + let stream = device .build_output_stream( &stream_config, @@ -93,97 +88,31 @@ pub fn build_stream( let buffer_samples = data.len() / channels; let buffer_time_ns = (buffer_samples as f64 / sr as f64 * 1e9) as u64; - let is_playing = playing.load(Ordering::Relaxed); - - if is_playing { - let state = link.capture_audio_state(); - let time = link.clock_micros(); - let beat = state.beat_at_time(time, quantum); - let tempo = state.tempo(); - - let mut audio = audio_state.lock().unwrap(); - let proj = project.lock().unwrap(); - - // Apply queued slot changes at bar boundaries (every 4 beats) - let bar = (beat / quantum).floor() as i64; - let prev_bar = (audio.prev_beat / quantum).floor() as i64; - if bar != prev_bar && audio.prev_beat >= 0.0 { - let mut changes = slot_changes.lock().unwrap(); - for change in changes.drain(..) { - match change { - SlotChange::Add { slot, bank, pattern } => { - audio.slots[slot] = PatternSlot { - bank, - pattern, - step_index: 0, - active: true, - }; - } - SlotChange::Remove { slot } => { - audio.slots[slot].active = false; - } - } + while let Ok(cmd) = audio_rx.try_recv() { + match cmd { + AudioCommand::Evaluate(s) => { + engine.evaluate(&s); + } + AudioCommand::Hush => { + engine.hush(); + } + AudioCommand::Panic => { + engine.panic(); + } + AudioCommand::LoadSamples(samples) => { + engine.sample_index.extend(samples); + } + AudioCommand::ResetEngine => { + let old_samples = std::mem::take(&mut engine.sample_index); + engine = Engine::new_with_metrics(sr, channels, Arc::clone(&metrics_clone)); + engine.sample_index = old_samples; } } - - // Read prev_beat before the mutable borrow of slots - let prev_beat = audio.prev_beat; - - // Iterate all active slots - for (slot_idx, slot) in audio.slots.iter_mut().enumerate() { - if !slot.active { - continue; - } - - let pattern = proj.pattern_at(slot.bank, slot.pattern); - let speed_mult = pattern.speed.multiplier(); - - let beat_int = (beat * 4.0 * speed_mult).floor() as i64; - let prev_beat_int = (prev_beat * 4.0 * speed_mult).floor() as i64; - - if beat_int != prev_beat_int && prev_beat >= 0.0 { - let step_idx = slot.step_index % pattern.length; - - slot_steps[slot_idx].store(step_idx, Ordering::Relaxed); - - if let Some(step) = pattern.step(step_idx) { - if step.active && !step.script.trim().is_empty() { - let ctx = StepContext { - step: step_idx, - beat, - bank: slot.bank, - pattern: slot.pattern, - tempo, - phase: beat % quantum, - slot: slot_idx, - }; - if let Ok(cmd) = - script_engine.evaluate(&step.script, &ctx, &variables, &rng) - { - engine.lock().unwrap().evaluate(&cmd); - event_count.fetch_add(1, Ordering::Relaxed); - } - } - } - - slot.step_index = (slot.step_index + 1) % pattern.length; - } - } - - // Update shared slot data for UI - { - let mut sd = slot_data.lock().unwrap(); - for (i, slot) in audio.slots.iter().enumerate() { - sd[i] = (slot.active, slot.bank, slot.pattern); - } - } - - audio.prev_beat = beat; } - let mut eng = engine.lock().unwrap(); - eng.metrics.load.set_buffer_time(buffer_time_ns); - eng.process_block(data, &[], &[]); + engine.metrics.load.set_buffer_time(buffer_time_ns); + engine.process_block(data, &[], &[]); + scope_buffer.write(&engine.output); }, |err| eprintln!("stream error: {err}"), None, diff --git a/seq/src/config.rs b/seq/src/config.rs new file mode 100644 index 0000000..0c1399a --- /dev/null +++ b/seq/src/config.rs @@ -0,0 +1,7 @@ +pub const MAX_SLOTS: usize = 8; +pub const MAX_BANKS: usize = 16; +pub const MAX_PATTERNS: usize = 16; +pub const MAX_STEPS: usize = 32; +pub const DEFAULT_LENGTH: usize = 16; +pub const DEFAULT_TEMPO: f64 = 120.0; +pub const DEFAULT_QUANTUM: f64 = 4.0; diff --git a/seq/src/link.rs b/seq/src/link.rs index 57acb33..5e49c01 100644 --- a/seq/src/link.rs +++ b/seq/src/link.rs @@ -38,9 +38,9 @@ impl LinkState { self.link.commit_app_session_state(&state); } - pub fn capture_audio_state(&self) -> SessionState { + pub fn capture_app_state(&self) -> SessionState { let mut state = SessionState::new(); - self.link.capture_audio_session_state(&mut state); + self.link.capture_app_session_state(&mut state); state } } diff --git a/seq/src/main.rs b/seq/src/main.rs index a53a772..bdd74a8 100644 --- a/seq/src/main.rs +++ b/seq/src/main.rs @@ -1,18 +1,20 @@ mod app; mod audio; +mod config; mod file; mod link; mod model; mod page; mod script; +mod sequencer; mod ui; mod views; mod widgets; use std::io; use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::Duration; use clap::Parser; @@ -21,15 +23,15 @@ use crossterm::terminal::{ disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, }; use crossterm::ExecutableCommand; -use doux::Engine; +use doux::EngineMetrics; use ratatui::prelude::CrosstermBackend; use ratatui::Terminal; use app::{App, AudioFocus, Focus, Modal, PatternField}; -use audio::{AudioStreamConfig, SlotChange, MAX_SLOTS}; +use audio::{AudioStreamConfig, ScopeBuffer}; use link::LinkState; -use model::Project; use page::Page; +use sequencer::{spawn_sequencer, AudioCommand, PatternSnapshot, SeqCommand, StepSnapshot}; #[derive(Parser)] #[command(name = "seq", about = "A step sequencer with Ableton Link support")] @@ -55,72 +57,59 @@ struct Args { buffer: u32, } -const TEMPO: f64 = 120.0; -const QUANTUM: f64 = 4.0; +use config::{DEFAULT_QUANTUM, DEFAULT_TEMPO}; fn main() -> io::Result<()> { let args = Args::parse(); - let link = Arc::new(LinkState::new(TEMPO, QUANTUM)); + let link = Arc::new(LinkState::new(DEFAULT_TEMPO, DEFAULT_QUANTUM)); link.enable(); let playing = Arc::new(AtomicBool::new(true)); - let event_count = Arc::new(AtomicUsize::new(0)); - // Slot state shared between audio thread and UI - let slot_steps: [Arc; MAX_SLOTS] = std::array::from_fn(|_| Arc::new(AtomicUsize::new(0))); - let slot_data: Arc> = - Arc::new(Mutex::new([(false, 0, 0); MAX_SLOTS])); - let slot_changes: Arc>> = Arc::new(Mutex::new(Vec::new())); + let mut app = App::new(DEFAULT_TEMPO, DEFAULT_QUANTUM); - let mut app = App::new(TEMPO, QUANTUM); + app.audio.config.output_device = args.output; + app.audio.config.input_device = args.input; + app.audio.config.channels = args.channels; + app.audio.config.buffer_size = args.buffer; + app.audio.config.sample_paths = args.samples; - // Apply CLI args to audio config - app.audio_config.output_device = args.output; - app.audio_config.input_device = args.input; - app.audio_config.channels = args.channels; - app.audio_config.buffer_size = args.buffer; - app.audio_config.sample_paths = args.samples; + let metrics = Arc::new(EngineMetrics::default()); + let scope_buffer = Arc::new(ScopeBuffer::new()); - let engine = Arc::new(Mutex::new(Engine::new(44100.0))); - let project = Arc::new(Mutex::new(Project::default())); - - // Load sample directories - for path in &app.audio_config.sample_paths { + let mut initial_samples = Vec::new(); + for path in &app.audio.config.sample_paths { let index = doux::loader::scan_samples_dir(path); - let count = index.len(); - engine.lock().unwrap().sample_index.extend(index); - app.audio_config.sample_count += count; + app.audio.config.sample_count += index.len(); + initial_samples.extend(index); } + let sequencer = spawn_sequencer( + Arc::clone(&link), + Arc::clone(&playing), + Arc::clone(&app.variables), + Arc::clone(&app.rng), + DEFAULT_QUANTUM, + ); + let stream_config = AudioStreamConfig { - output_device: app.audio_config.output_device.clone(), - channels: app.audio_config.channels, - buffer_size: app.audio_config.buffer_size, + output_device: app.audio.config.output_device.clone(), + channels: app.audio.config.channels, + buffer_size: app.audio.config.buffer_size, }; let (mut stream, sample_rate) = audio::build_stream( &stream_config, - Arc::clone(&engine), - Arc::clone(&link), - Arc::clone(&playing), - Arc::clone(&project), - slot_steps.clone(), - Arc::clone(&event_count), - Arc::clone(&slot_data), - Arc::clone(&slot_changes), - Arc::clone(&app.variables), - Arc::clone(&app.rng), + sequencer.audio_rx.clone(), + Arc::clone(&scope_buffer), + Arc::clone(&metrics), + initial_samples, ) .expect("Failed to start audio"); - app.audio_config.sample_rate = sample_rate; - - { - let mut eng = engine.lock().unwrap(); - eng.sr = sample_rate; - eng.isr = 1.0 / sample_rate; - } + app.audio.config.sample_rate = sample_rate; + app.mark_all_patterns_dirty(); enable_raw_mode()?; io::stdout().execute(EnterAlternateScreen)?; @@ -129,62 +118,52 @@ fn main() -> io::Result<()> { let mut terminal = Terminal::new(backend)?; loop { - if app.restart_pending { - app.restart_pending = false; + if app.audio.restart_pending { + app.audio.restart_pending = false; drop(stream); let new_config = AudioStreamConfig { - output_device: app.audio_config.output_device.clone(), - channels: app.audio_config.channels, - buffer_size: app.audio_config.buffer_size, + output_device: app.audio.config.output_device.clone(), + channels: app.audio.config.channels, + buffer_size: app.audio.config.buffer_size, }; - { - let mut eng = engine.lock().unwrap(); - *eng = Engine::new_with_channels(eng.sr, new_config.channels as usize); + let mut restart_samples = Vec::new(); + for path in &app.audio.config.sample_paths { + let index = doux::loader::scan_samples_dir(path); + restart_samples.extend(index); } + app.audio.config.sample_count = restart_samples.len(); match audio::build_stream( &new_config, - Arc::clone(&engine), - Arc::clone(&link), - Arc::clone(&playing), - Arc::clone(&project), - slot_steps.clone(), - Arc::clone(&event_count), - Arc::clone(&slot_data), - Arc::clone(&slot_changes), - Arc::clone(&app.variables), - Arc::clone(&app.rng), + sequencer.audio_rx.clone(), + Arc::clone(&scope_buffer), + Arc::clone(&metrics), + restart_samples, ) { Ok((new_stream, sr)) => { stream = new_stream; - app.audio_config.sample_rate = sr; - { - let mut eng = engine.lock().unwrap(); - eng.sr = sr; - eng.isr = 1.0 / sr; - } + app.audio.config.sample_rate = sr; app.status_message = Some("Audio restarted".to_string()); } Err(e) => { app.status_message = Some(format!("Restart failed: {e}")); + let mut fallback_samples = Vec::new(); + for path in &app.audio.config.sample_paths { + let index = doux::loader::scan_samples_dir(path); + fallback_samples.extend(index); + } let (fallback_stream, _) = audio::build_stream( &AudioStreamConfig { output_device: None, channels: 2, buffer_size: 512, }, - Arc::clone(&engine), - Arc::clone(&link), - Arc::clone(&playing), - Arc::clone(&project), - slot_steps.clone(), - Arc::clone(&event_count), - Arc::clone(&slot_data), - Arc::clone(&slot_changes), - Arc::clone(&app.variables), - Arc::clone(&app.rng), + sequencer.audio_rx.clone(), + Arc::clone(&scope_buffer), + Arc::clone(&metrics), + fallback_samples, ) .expect("Failed to restart with defaults"); stream = fallback_stream; @@ -194,37 +173,42 @@ fn main() -> io::Result<()> { app.update_from_link(&link); app.playing = playing.load(Ordering::Relaxed); - app.event_count = event_count.load(Ordering::Relaxed); { - let eng = engine.lock().unwrap(); - app.active_voices = eng.active_voices; - app.peak_voices = app.peak_voices.max(eng.active_voices); - app.cpu_load = eng.metrics.load.get_load(); - app.schedule_depth = eng.schedule.len(); - for (i, s) in app.scope.iter_mut().enumerate() { - *s = eng.output.get(i * 2).copied().unwrap_or(0.0); + app.metrics.active_voices = metrics.active_voices.load(Ordering::Relaxed) as usize; + app.metrics.peak_voices = app.metrics.peak_voices.max(app.metrics.active_voices); + app.metrics.cpu_load = metrics.load.get_load(); + app.metrics.schedule_depth = metrics.schedule_depth.load(Ordering::Relaxed) as usize; + app.metrics.scope = scope_buffer.read(); + } + + let seq_snapshot = sequencer.snapshot(); + app.slot_data = seq_snapshot.slot_data; + app.slot_steps = seq_snapshot.slot_steps; + app.metrics.event_count = seq_snapshot.event_count; + + for change in app.queued_changes.drain(..) { + match change { + app::SlotChange::Add { slot, bank, pattern } => { + let _ = sequencer.cmd_tx.send(SeqCommand::SlotAdd { slot, bank, pattern }); + } + app::SlotChange::Remove { slot } => { + let _ = sequencer.cmd_tx.send(SeqCommand::SlotRemove { slot }); + } } } - // Sync slot state from audio thread - { - let sd = slot_data.lock().unwrap(); - app.slot_data = *sd; - } - for (i, step_atomic) in slot_steps.iter().enumerate() { - app.slot_steps[i] = step_atomic.load(Ordering::Relaxed); - } - - // Push queued changes to audio thread - if !app.queued_changes.is_empty() { - let mut changes = slot_changes.lock().unwrap(); - changes.extend(app.queued_changes.drain(..)); - } - - { - let mut proj = project.lock().unwrap(); - proj.banks = app.project.banks.clone(); + for (bank, pattern) in app.dirty_patterns.drain() { + let pat = app.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 _ = sequencer.cmd_tx.send(SeqCommand::PatternUpdate { bank, pattern, data: snapshot }); } terminal.draw(|frame| ui::render(frame, &mut app))?; @@ -324,15 +308,16 @@ fn main() -> io::Result<()> { Modal::SetPattern { field, input } => match key.code { KeyCode::Enter => { let field = *field; - let (bank, pattern) = (app.edit_bank, app.edit_pattern); + let (bank, pattern) = (app.editor_ctx.bank, app.editor_ctx.pattern); match field { PatternField::Length => { if let Ok(len) = input.parse::() { app.project.pattern_at_mut(bank, pattern).set_length(len); let new_len = app.project.pattern_at(bank, pattern).length; - if app.current_step >= new_len { - app.current_step = new_len - 1; + if app.editor_ctx.step >= new_len { + app.editor_ctx.step = new_len - 1; } + app.dirty_patterns.insert((bank, pattern)); app.status_message = Some(format!("Length set to {new_len}")); } else { app.status_message = Some("Invalid length".to_string()); @@ -341,6 +326,7 @@ fn main() -> io::Result<()> { PatternField::Speed => { if let Some(speed) = model::PatternSpeed::from_label(input) { app.project.pattern_at_mut(bank, pattern).speed = speed; + app.dirty_patterns.insert((bank, pattern)); app.status_message = Some(format!("Speed set to {}", speed.label())); } else { app.status_message = Some("Invalid speed (try 1/8x, 1/4x, 1/2x, 1x, 2x, 4x, 8x)".to_string()); @@ -366,9 +352,9 @@ fn main() -> io::Result<()> { if sample_path.is_dir() { let index = doux::loader::scan_samples_dir(&sample_path); let count = index.len(); - engine.lock().unwrap().sample_index.extend(index); - app.audio_config.sample_count += count; - app.add_sample_path(sample_path); + let _ = sequencer.audio_tx.send(AudioCommand::LoadSamples(index)); + app.audio.config.sample_count += count; + app.audio.add_sample_path(sample_path); app.status_message = Some(format!("Added {count} samples")); } else { app.status_message = Some("Path is not a directory".to_string()); @@ -407,7 +393,7 @@ fn main() -> io::Result<()> { } match app.page { - Page::Main => match app.focus { + Page::Main => match app.editor_ctx.focus { Focus::Sequencer => match key.code { KeyCode::Char('q') => { app.modal = Modal::ConfirmQuit { selected: false }; @@ -456,7 +442,7 @@ fn main() -> io::Result<()> { app.compile_current_step(); } _ => { - app.editor.input(Event::Key(key)); + app.editor_ctx.text.input(Event::Key(key)); } }, }, @@ -537,57 +523,48 @@ fn main() -> io::Result<()> { app.modal = Modal::ConfirmQuit { selected: false }; } KeyCode::Up | KeyCode::Char('k') => { - app.prev_audio_focus(); + app.audio.prev_focus(); } KeyCode::Down | KeyCode::Char('j') => { - app.next_audio_focus(); + app.audio.next_focus(); } - KeyCode::Left => match app.audio_focus { - AudioFocus::OutputDevice => app.prev_output_device(), - AudioFocus::InputDevice => app.prev_input_device(), - AudioFocus::Channels => app.adjust_channels(-1), - AudioFocus::BufferSize => app.adjust_buffer_size(-64), - AudioFocus::SamplePaths => app.remove_last_sample_path(), + KeyCode::Left => match app.audio.focus { + AudioFocus::OutputDevice => app.audio.prev_output_device(), + AudioFocus::InputDevice => app.audio.prev_input_device(), + AudioFocus::Channels => app.audio.adjust_channels(-1), + AudioFocus::BufferSize => app.audio.adjust_buffer_size(-64), + AudioFocus::SamplePaths => app.audio.remove_last_sample_path(), }, - KeyCode::Right => match app.audio_focus { - AudioFocus::OutputDevice => app.next_output_device(), - AudioFocus::InputDevice => app.next_input_device(), - AudioFocus::Channels => app.adjust_channels(1), - AudioFocus::BufferSize => app.adjust_buffer_size(64), + KeyCode::Right => match app.audio.focus { + AudioFocus::OutputDevice => app.audio.next_output_device(), + AudioFocus::InputDevice => app.audio.next_input_device(), + AudioFocus::Channels => app.audio.adjust_channels(1), + AudioFocus::BufferSize => app.audio.adjust_buffer_size(64), AudioFocus::SamplePaths => {} }, KeyCode::Char('R') => { - app.trigger_restart(); - // Reload samples on restart - let mut eng = engine.lock().unwrap(); - eng.sample_index.clear(); - app.audio_config.sample_count = 0; - for path in &app.audio_config.sample_paths { - let index = doux::loader::scan_samples_dir(path); - app.audio_config.sample_count += index.len(); - eng.sample_index.extend(index); - } + app.audio.trigger_restart(); } KeyCode::Char('A') => { app.modal = Modal::AddSamplePath(String::new()); } KeyCode::Char('D') => { - app.refresh_audio_devices(); - let out_count = app.available_output_devices.len(); - let in_count = app.available_input_devices.len(); + app.audio.refresh_devices(); + let out_count = app.audio.output_devices.len(); + let in_count = app.audio.input_devices.len(); app.status_message = Some(format!("Found {out_count} output, {in_count} input devices")); } KeyCode::Char('h') => { - engine.lock().unwrap().hush(); + let _ = sequencer.audio_tx.send(AudioCommand::Hush); } KeyCode::Char('p') => { - engine.lock().unwrap().panic(); + let _ = sequencer.audio_tx.send(AudioCommand::Panic); } KeyCode::Char('r') => { - app.peak_voices = 0; + app.metrics.peak_voices = 0; } KeyCode::Char('t') => { - engine.lock().unwrap().evaluate("sin 440 * 0.3"); + let _ = sequencer.audio_tx.send(AudioCommand::Evaluate("sin 440 * 0.3".into())); } KeyCode::Char(' ') => { app.toggle_playing(); @@ -628,5 +605,7 @@ fn main() -> io::Result<()> { disable_raw_mode()?; io::stdout().execute(LeaveAlternateScreen)?; + sequencer.shutdown(); + Ok(()) } diff --git a/seq/src/model.rs b/seq/src/model.rs index 70e5446..296741c 100644 --- a/seq/src/model.rs +++ b/seq/src/model.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use crate::config::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS}; + #[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq)] pub enum PatternSpeed { Eighth, // 1/8x @@ -106,8 +108,8 @@ pub struct Pattern { impl Default for Pattern { fn default() -> Self { Self { - steps: (0..32).map(|_| Step::default()).collect(), - length: 16, + steps: (0..MAX_STEPS).map(|_| Step::default()).collect(), + length: DEFAULT_LENGTH, speed: PatternSpeed::default(), name: None, } @@ -124,7 +126,7 @@ impl Pattern { } pub fn set_length(&mut self, length: usize) { - let length = length.clamp(2, 32); + let length = length.clamp(2, MAX_STEPS); while self.steps.len() < length { self.steps.push(Step::default()); } @@ -142,7 +144,7 @@ pub struct Bank { impl Default for Bank { fn default() -> Self { Self { - patterns: (0..16).map(|_| Pattern::default()).collect(), + patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(), name: None, } } @@ -156,7 +158,7 @@ pub struct Project { impl Default for Project { fn default() -> Self { Self { - banks: (0..16).map(|_| Bank::default()).collect(), + banks: (0..MAX_BANKS).map(|_| Bank::default()).collect(), } } } diff --git a/seq/src/script.rs b/seq/src/script.rs index 8a11ae9..aa771ac 100644 --- a/seq/src/script.rs +++ b/seq/src/script.rs @@ -62,40 +62,20 @@ pub struct StepContext { pub slot: usize, } -pub struct ScriptEngine; +pub struct ScriptEngine { + engine: Engine, +} impl ScriptEngine { - pub fn new() -> Self { - Self - } - - pub fn evaluate( - &self, - script: &str, - ctx: &StepContext, - vars: &Variables, - rng: &Rng, - ) -> Result { - if script.trim().is_empty() { - return Err("empty script".to_string()); - } - - let mut scope = Scope::new(); - scope.push("step", ctx.step as i64); - scope.push("beat", ctx.beat); - scope.push("bank", ctx.bank as i64); - scope.push("pattern", ctx.pattern as i64); - scope.push("tempo", ctx.tempo); - scope.push("phase", ctx.phase); - scope.push("slot", ctx.slot as i64); - - let vars_for_set = Arc::clone(vars); - let vars_for_get = Arc::clone(vars); - + pub fn new(vars: Variables, rng: Rng) -> Self { let mut engine = Engine::new(); engine.set_max_expr_depths(64, 32); + register_cmd(&mut engine); + let vars_for_set = Arc::clone(&vars); + let vars_for_get = Arc::clone(&vars); + engine.register_fn("set", move |name: &str, value: Dynamic| { vars_for_set .lock() @@ -112,11 +92,11 @@ impl ScriptEngine { .unwrap_or(Dynamic::UNIT) }); - let rng_rand_ff = Arc::clone(rng); - let rng_rand_ii = Arc::clone(rng); - let rng_rrand_ff = Arc::clone(rng); - let rng_rrand_ii = Arc::clone(rng); - let rng_seed = Arc::clone(rng); + let rng_rand_ff = Arc::clone(&rng); + let rng_rand_ii = Arc::clone(&rng); + let rng_rrand_ff = Arc::clone(&rng); + let rng_rrand_ii = Arc::clone(&rng); + let rng_seed = Arc::clone(&rng); engine.register_fn("rand", move |min: f64, max: f64| -> f64 { rng_rand_ff.lock().unwrap().gen_range(min..max) @@ -136,11 +116,28 @@ impl ScriptEngine { *rng_seed.lock().unwrap() = StdRng::seed_from_u64(s as u64); }); - if let Ok(cmd) = engine.eval_with_scope::(&mut scope, script) { + Self { engine } + } + + pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result { + if script.trim().is_empty() { + return Err("empty script".to_string()); + } + + let mut scope = Scope::new(); + scope.push("step", ctx.step as i64); + scope.push("beat", ctx.beat); + scope.push("bank", ctx.bank as i64); + scope.push("pattern", ctx.pattern as i64); + scope.push("tempo", ctx.tempo); + scope.push("phase", ctx.phase); + scope.push("slot", ctx.slot as i64); + + if let Ok(cmd) = self.engine.eval_with_scope::(&mut scope, script) { return Ok(cmd.to_string()); } - engine + self.engine .eval_with_scope::(&mut scope, script) .map_err(|e| e.to_string()) } diff --git a/seq/src/sequencer.rs b/seq/src/sequencer.rs new file mode 100644 index 0000000..af89c52 --- /dev/null +++ b/seq/src/sequencer.rs @@ -0,0 +1,328 @@ +use crossbeam_channel::{bounded, Receiver, Sender, TrySendError}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::Arc; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +use crate::audio::PatternSlot; +use crate::config::{MAX_BANKS, MAX_PATTERNS, MAX_SLOTS}; +use crate::link::LinkState; +use crate::script::{ScriptEngine, StepContext, Variables, Rng}; + +pub enum AudioCommand { + Evaluate(String), + Hush, + Panic, + LoadSamples(Vec), + #[allow(dead_code)] + ResetEngine, +} + +pub enum SeqCommand { + PatternUpdate { bank: usize, pattern: usize, data: PatternSnapshot }, + SlotAdd { slot: usize, bank: usize, pattern: usize }, + SlotRemove { slot: usize }, + Shutdown, +} + +#[derive(Clone)] +pub struct PatternSnapshot { + pub speed: crate::model::PatternSpeed, + pub length: usize, + pub steps: Vec, +} + +#[derive(Clone)] +pub struct StepSnapshot { + pub active: bool, + pub script: String, +} + +#[derive(Clone, Copy, Default)] +pub struct SlotState { + pub active: bool, + pub bank: usize, + pub pattern: usize, +} + +pub struct AtomicSlotData { + active: AtomicBool, + bank: AtomicUsize, + pattern: AtomicUsize, +} + +impl AtomicSlotData { + pub fn new() -> Self { + Self { + active: AtomicBool::new(false), + bank: AtomicUsize::new(0), + pattern: AtomicUsize::new(0), + } + } + + pub fn load(&self) -> SlotState { + SlotState { + active: self.active.load(Ordering::Relaxed), + bank: self.bank.load(Ordering::Relaxed), + pattern: self.pattern.load(Ordering::Relaxed), + } + } + + pub fn store(&self, state: SlotState) { + self.active.store(state.active, Ordering::Relaxed); + self.bank.store(state.bank, Ordering::Relaxed); + self.pattern.store(state.pattern, Ordering::Relaxed); + } +} + +pub struct SequencerSnapshot { + pub slot_data: [SlotState; MAX_SLOTS], + pub slot_steps: [usize; MAX_SLOTS], + pub event_count: usize, +} + +pub struct SequencerHandle { + pub cmd_tx: Sender, + pub audio_tx: Sender, + pub audio_rx: Receiver, + slot_data: Arc<[AtomicSlotData; MAX_SLOTS]>, + slot_steps: [Arc; MAX_SLOTS], + event_count: Arc, + thread: JoinHandle<()>, +} + +impl SequencerHandle { + pub fn snapshot(&self) -> SequencerSnapshot { + SequencerSnapshot { + slot_data: std::array::from_fn(|i| self.slot_data[i].load()), + slot_steps: std::array::from_fn(|i| self.slot_steps[i].load(Ordering::Relaxed)), + event_count: self.event_count.load(Ordering::Relaxed), + } + } + + pub fn shutdown(self) { + let _ = self.cmd_tx.send(SeqCommand::Shutdown); + let _ = self.thread.join(); + } +} + +struct AudioState { + prev_beat: f64, + slots: [PatternSlot; MAX_SLOTS], + pending_changes: Vec, +} + +enum PendingChange { + Add { slot: usize, bank: usize, pattern: usize }, + Remove { slot: usize }, +} + +impl AudioState { + fn new() -> Self { + Self { + prev_beat: -1.0, + slots: [PatternSlot::default(); MAX_SLOTS], + pending_changes: Vec::new(), + } + } +} + +pub fn spawn_sequencer( + link: Arc, + playing: Arc, + variables: Variables, + rng: Rng, + quantum: f64, +) -> SequencerHandle { + let (cmd_tx, cmd_rx) = bounded::(64); + let (audio_tx, audio_rx) = bounded::(256); + + let slot_data: Arc<[AtomicSlotData; MAX_SLOTS]> = + Arc::new(std::array::from_fn(|_| AtomicSlotData::new())); + let slot_steps: [Arc; MAX_SLOTS] = + std::array::from_fn(|_| Arc::new(AtomicUsize::new(0))); + let event_count = Arc::new(AtomicUsize::new(0)); + + let slot_data_clone = Arc::clone(&slot_data); + let slot_steps_clone = slot_steps.clone(); + let event_count_clone = Arc::clone(&event_count); + let audio_tx_clone = audio_tx.clone(); + + let thread = thread::Builder::new() + .name("sequencer".into()) + .spawn(move || { + sequencer_loop( + cmd_rx, + audio_tx_clone, + link, + playing, + variables, + rng, + quantum, + slot_data_clone, + slot_steps_clone, + event_count_clone, + ); + }) + .expect("Failed to spawn sequencer thread"); + + SequencerHandle { + cmd_tx, + audio_tx, + audio_rx, + slot_data, + slot_steps, + event_count, + thread, + } +} + +struct PatternCache { + patterns: [[Option; MAX_PATTERNS]; MAX_BANKS], +} + +impl PatternCache { + fn new() -> Self { + Self { + patterns: std::array::from_fn(|_| std::array::from_fn(|_| None)), + } + } + + fn get(&self, bank: usize, pattern: usize) -> Option<&PatternSnapshot> { + self.patterns.get(bank).and_then(|b| b.get(pattern)).and_then(|p| p.as_ref()) + } + + fn set(&mut self, bank: usize, pattern: usize, data: PatternSnapshot) { + if bank < MAX_BANKS && pattern < MAX_PATTERNS { + self.patterns[bank][pattern] = Some(data); + } + } +} + +fn sequencer_loop( + cmd_rx: Receiver, + audio_tx: Sender, + link: Arc, + playing: Arc, + variables: Variables, + rng: Rng, + quantum: f64, + slot_data: Arc<[AtomicSlotData; MAX_SLOTS]>, + slot_steps: [Arc; MAX_SLOTS], + event_count: Arc, +) { + let script_engine = ScriptEngine::new(variables, rng); + let mut audio_state = AudioState::new(); + let mut pattern_cache = PatternCache::new(); + + loop { + while let Ok(cmd) = cmd_rx.try_recv() { + match cmd { + SeqCommand::PatternUpdate { bank, pattern, data } => { + pattern_cache.set(bank, pattern, data); + } + SeqCommand::SlotAdd { slot, bank, pattern } => { + audio_state.pending_changes.push(PendingChange::Add { slot, bank, pattern }); + } + SeqCommand::SlotRemove { slot } => { + audio_state.pending_changes.push(PendingChange::Remove { slot }); + } + SeqCommand::Shutdown => { + return; + } + } + } + + if !playing.load(Ordering::Relaxed) { + thread::sleep(Duration::from_micros(500)); + continue; + } + + let state = link.capture_app_state(); + let time = link.clock_micros(); + let beat = state.beat_at_time(time, quantum); + let tempo = state.tempo(); + + let bar = (beat / quantum).floor() as i64; + let prev_bar = (audio_state.prev_beat / quantum).floor() as i64; + if bar != prev_bar && audio_state.prev_beat >= 0.0 { + for change in audio_state.pending_changes.drain(..) { + match change { + PendingChange::Add { slot, bank, pattern } => { + audio_state.slots[slot] = PatternSlot { + bank, + pattern, + step_index: 0, + active: true, + }; + } + PendingChange::Remove { slot } => { + audio_state.slots[slot].active = false; + } + } + } + } + + let prev_beat = audio_state.prev_beat; + + for (slot_idx, slot) in audio_state.slots.iter_mut().enumerate() { + if !slot.active { + continue; + } + + let Some(pattern) = pattern_cache.get(slot.bank, slot.pattern) else { + continue; + }; + + let speed_mult = pattern.speed.multiplier(); + let beat_int = (beat * 4.0 * speed_mult).floor() as i64; + let prev_beat_int = (prev_beat * 4.0 * speed_mult).floor() as i64; + + if beat_int != prev_beat_int && prev_beat >= 0.0 { + let step_idx = slot.step_index % pattern.length; + + slot_steps[slot_idx].store(step_idx, Ordering::Relaxed); + + if let Some(step) = pattern.steps.get(step_idx) { + if step.active && !step.script.trim().is_empty() { + let ctx = StepContext { + step: step_idx, + beat, + bank: slot.bank, + pattern: slot.pattern, + tempo, + phase: beat % quantum, + slot: slot_idx, + }; + if let Ok(cmd) = script_engine.evaluate(&step.script, &ctx) + { + match audio_tx.try_send(AudioCommand::Evaluate(cmd)) { + Ok(()) => { + event_count.fetch_add(1, Ordering::Relaxed); + } + Err(TrySendError::Full(_)) => {} + Err(TrySendError::Disconnected(_)) => { + return; + } + } + } + } + } + + slot.step_index = (slot.step_index + 1) % pattern.length; + } + } + + for (i, slot) in audio_state.slots.iter().enumerate() { + slot_data[i].store(SlotState { + active: slot.active, + bank: slot.bank, + pattern: slot.pattern, + }); + } + + audio_state.prev_beat = beat; + + thread::sleep(Duration::from_micros(500)); + } +} diff --git a/seq/src/ui.rs b/seq/src/ui.rs index fe329a9..7b1d3f3 100644 --- a/seq/src/ui.rs +++ b/seq/src/ui.rs @@ -40,7 +40,7 @@ fn render_header(frame: &mut Frame, app: &App, area: Rect) { Color::Red }; - let cpu_pct = (app.cpu_load * 100.0).min(100.0); + let cpu_pct = (app.metrics.cpu_load * 100.0).min(100.0); let cpu_color = if cpu_pct > 80.0 { Color::Red } else if cpu_pct > 50.0 { @@ -52,7 +52,7 @@ fn render_header(frame: &mut Frame, app: &App, area: Rect) { let left_spans = vec![ Span::styled("EDIT ", Style::new().fg(Color::Cyan)), Span::styled( - format!("B{:02}:P{:02}", app.edit_bank + 1, app.edit_pattern + 1), + format!("B{:02}:P{:02}", app.editor_ctx.bank + 1, app.editor_ctx.pattern + 1), Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD), ), Span::raw(" "), @@ -61,7 +61,7 @@ fn render_header(frame: &mut Frame, app: &App, area: Rect) { frame.render_widget(Paragraph::new(Line::from(left_spans)), left_area); - let pattern = app.project.pattern_at(app.edit_bank, app.edit_pattern); + let pattern = app.project.pattern_at(app.editor_ctx.bank, app.editor_ctx.pattern); let right_spans = vec![ Span::styled(format!("L:{:02}", pattern.length), Style::new().fg(Color::Rgb(180, 140, 90))), Span::raw(" "), @@ -71,7 +71,7 @@ fn render_header(frame: &mut Frame, app: &App, area: Rect) { Span::raw(" "), Span::styled(format!("CPU:{cpu_pct:.0}%"), Style::new().fg(cpu_color)), Span::raw(" "), - Span::styled(format!("V:{}", app.active_voices), Style::new().fg(Color::Cyan)), + Span::styled(format!("V:{}", app.metrics.active_voices), Style::new().fg(Color::Cyan)), ]; frame.render_widget( diff --git a/seq/src/views/audio_view.rs b/seq/src/views/audio_view.rs index 1a6f7ed..6741534 100644 --- a/seq/src/views/audio_view.rs +++ b/seq/src/views/audio_view.rs @@ -48,8 +48,8 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) { let normal = Style::new().fg(Color::White); let dim = Style::new().fg(Color::DarkGray); - let output_name = truncate_name(app.current_output_device_name(), 25); - let output_style = if app.audio_focus == AudioFocus::OutputDevice { + let output_name = truncate_name(app.audio.current_output_device_name(), 25); + let output_style = if app.audio.focus == AudioFocus::OutputDevice { highlight } else { normal @@ -62,8 +62,8 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) { ]); frame.render_widget(Paragraph::new(output_line), output_area); - let input_name = truncate_name(app.current_input_device_name(), 25); - let input_style = if app.audio_focus == AudioFocus::InputDevice { + let input_name = truncate_name(app.audio.current_input_device_name(), 25); + let input_style = if app.audio.focus == AudioFocus::InputDevice { highlight } else { normal @@ -76,7 +76,7 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) { ]); frame.render_widget(Paragraph::new(input_line), input_area); - let channels_style = if app.audio_focus == AudioFocus::Channels { + let channels_style = if app.audio.focus == AudioFocus::Channels { highlight } else { normal @@ -84,12 +84,12 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) { let channels_line = Line::from(vec![ Span::styled("Channels ", dim), Span::styled("< ", channels_style), - Span::styled(format!("{:2}", app.audio_config.channels), channels_style), + Span::styled(format!("{:2}", app.audio.config.channels), channels_style), Span::styled(" >", channels_style), ]); frame.render_widget(Paragraph::new(channels_line), channels_area); - let buffer_style = if app.audio_focus == AudioFocus::BufferSize { + let buffer_style = if app.audio.focus == AudioFocus::BufferSize { highlight } else { normal @@ -97,18 +97,18 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) { let buffer_line = Line::from(vec![ Span::styled("Buffer ", dim), Span::styled("< ", buffer_style), - Span::styled(format!("{:4}", app.audio_config.buffer_size), buffer_style), + Span::styled(format!("{:4}", app.audio.config.buffer_size), buffer_style), Span::styled(" >", buffer_style), ]); frame.render_widget(Paragraph::new(buffer_line), buffer_area); let rate_line = Line::from(vec![ Span::styled("Rate ", dim), - Span::styled(format!("{:.0} Hz", app.audio_config.sample_rate), normal), + Span::styled(format!("{:.0} Hz", app.audio.config.sample_rate), normal), ]); frame.render_widget(Paragraph::new(rate_line), rate_area); - let samples_style = if app.audio_focus == AudioFocus::SamplePaths { + let samples_style = if app.audio.focus == AudioFocus::SamplePaths { highlight } else { normal @@ -117,12 +117,12 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) { let mut sample_lines = vec![Line::from(vec![ Span::styled("Samples ", dim), Span::styled( - format!("{} paths, {} indexed", app.audio_config.sample_paths.len(), app.audio_config.sample_count), + format!("{} paths, {} indexed", app.audio.config.sample_paths.len(), app.audio.config.sample_count), samples_style, ), ])]; - for (i, path) in app.audio_config.sample_paths.iter().take(2).enumerate() { + for (i, path) in app.audio.config.sample_paths.iter().take(2).enumerate() { let path_str = path.to_string_lossy(); let display = truncate_name(&path_str, 35); sample_lines.push(Line::from(vec![ @@ -157,7 +157,7 @@ fn render_stats(frame: &mut Frame, app: &App, area: Rect) { ]) .areas(inner); - let cpu_pct = (app.cpu_load * 100.0).min(100.0); + let cpu_pct = (app.metrics.cpu_load * 100.0).min(100.0); let cpu_color = if cpu_pct > 80.0 { Color::Red } else if cpu_pct > 50.0 { @@ -174,9 +174,9 @@ fn render_stats(frame: &mut Frame, app: &App, area: Rect) { frame.render_widget(gauge, cpu_area); - let voice_color = if app.active_voices > 24 { + let voice_color = if app.metrics.active_voices > 24 { Color::Red - } else if app.active_voices > 16 { + } else if app.metrics.active_voices > 16 { Color::Yellow } else { Color::Cyan @@ -185,12 +185,12 @@ fn render_stats(frame: &mut Frame, app: &App, area: Rect) { let voices = Paragraph::new(Line::from(vec![ Span::raw("Active: "), Span::styled( - format!("{:3}", app.active_voices), + format!("{:3}", app.metrics.active_voices), Style::new().fg(voice_color).add_modifier(Modifier::BOLD), ), Span::raw(" Peak: "), Span::styled( - format!("{:3}", app.peak_voices), + format!("{:3}", app.metrics.peak_voices), Style::new().fg(Color::Yellow), ), ])); @@ -200,7 +200,7 @@ fn render_stats(frame: &mut Frame, app: &App, area: Rect) { let extra = Paragraph::new(vec![ Line::from(vec![ Span::raw("Schedule: "), - Span::styled(format!("{}", app.schedule_depth), Style::new().fg(Color::White)), + Span::styled(format!("{}", app.metrics.schedule_depth), Style::new().fg(Color::White)), ]), Line::from(vec![ Span::raw("Pool: "), diff --git a/seq/src/views/main_view.rs b/seq/src/views/main_view.rs index 78a52a0..f8f28fb 100644 --- a/seq/src/views/main_view.rs +++ b/seq/src/views/main_view.rs @@ -22,13 +22,13 @@ pub fn render(frame: &mut Frame, app: &mut App, area: Rect) { } fn render_sequencer(frame: &mut Frame, app: &App, area: Rect) { - let focus_indicator = if app.focus == Focus::Sequencer { + let focus_indicator = if app.editor_ctx.focus == Focus::Sequencer { "*" } else { " " }; - let border_style = if app.focus == Focus::Sequencer { + let border_style = if app.editor_ctx.focus == Focus::Sequencer { Style::new().fg(Color::Rgb(100, 160, 180)) } else { Style::new().fg(Color::Rgb(70, 75, 85)) @@ -106,15 +106,14 @@ fn render_tile(frame: &mut Frame, area: Rect, app: &App, step_idx: usize) { let pattern = app.current_edit_pattern(); let step = pattern.step(step_idx); let is_active = step.map(|s| s.active).unwrap_or(false); - let is_selected = step_idx == app.current_step; + let is_selected = step_idx == app.editor_ctx.step; - // Check if any slot is playing this step on the current edit pattern let playing_slot = if app.playing { (0..8).find(|&i| { - let (slot_active, bank, pat) = app.slot_data[i]; - slot_active - && bank == app.edit_bank - && pat == app.edit_pattern + let s = app.slot_data[i]; + s.active + && s.bank == app.editor_ctx.bank + && s.pattern == app.editor_ctx.pattern && app.slot_steps[i] == step_idx }) } else { @@ -146,7 +145,7 @@ fn render_tile(frame: &mut Frame, area: Rect, app: &App, step_idx: usize) { } fn render_editor(frame: &mut Frame, app: &mut App, area: Rect) { - let focus_indicator = if app.focus == Focus::Editor { + let focus_indicator = if app.editor_ctx.focus == Focus::Editor { "*" } else { " " @@ -154,13 +153,13 @@ fn render_editor(frame: &mut Frame, app: &mut App, area: Rect) { let border_style = if app.is_flashing() { Style::new().fg(Color::Green) - } else if app.focus == Focus::Editor { + } else if app.editor_ctx.focus == Focus::Editor { Style::new().fg(Color::Rgb(100, 160, 180)) } else { Style::new().fg(Color::Rgb(70, 75, 85)) }; - let step_num = app.current_step + 1; + let step_num = app.editor_ctx.step + 1; let block = Block::default() .borders(Borders::ALL) .border_style(border_style) @@ -169,14 +168,14 @@ fn render_editor(frame: &mut Frame, app: &mut App, area: Rect) { let inner = block.inner(area); frame.render_widget(block, area); - let cursor_style = if app.focus == Focus::Editor { + let cursor_style = if app.editor_ctx.focus == Focus::Editor { Style::new().bg(Color::White).fg(Color::Black) } else { Style::default() }; - app.editor.set_cursor_style(cursor_style); + app.editor_ctx.text.set_cursor_style(cursor_style); - frame.render_widget(&app.editor, inner); + frame.render_widget(&app.editor_ctx.text, inner); } fn render_scope(frame: &mut Frame, app: &App, area: Rect) { @@ -188,7 +187,7 @@ fn render_scope(frame: &mut Frame, app: &App, area: Rect) { let inner = block.inner(area); frame.render_widget(block, area); - let scope = Scope::new(&app.scope) + let scope = Scope::new(&app.metrics.scope) .orientation(Orientation::Vertical) .color(Color::Green); frame.render_widget(scope, inner); diff --git a/seq/src/views/patterns_view.rs b/seq/src/views/patterns_view.rs index 17936a1..2a97008 100644 --- a/seq/src/views/patterns_view.rs +++ b/seq/src/views/patterns_view.rs @@ -33,8 +33,8 @@ fn render_banks(frame: &mut Frame, app: &App, area: Rect) { let banks_with_playback: Vec = app .slot_data .iter() - .filter(|(active, _, _)| *active) - .map(|(_, bank, _)| *bank) + .filter(|s| s.active) + .map(|s| s.bank) .collect(); let bank_names: Vec> = app @@ -44,7 +44,7 @@ fn render_banks(frame: &mut Frame, app: &App, area: Rect) { .map(|b| b.name.as_deref()) .collect(); - render_grid(frame, inner, app.patterns_cursor, app.edit_bank, &banks_with_playback, &bank_names); + render_grid(frame, inner, app.patterns_cursor, app.editor_ctx.bank, &banks_with_playback, &bank_names); } fn render_patterns(frame: &mut Frame, app: &App, area: Rect, bank: usize) { @@ -77,12 +77,12 @@ fn render_patterns(frame: &mut Frame, app: &App, area: Rect, bank: usize) { let playing_patterns: Vec = app .slot_data .iter() - .filter(|(active, b, _)| *active && *b == bank) - .map(|(_, _, pattern)| *pattern) + .filter(|s| s.active && s.bank == bank) + .map(|s| s.pattern) .collect(); - let edit_pattern = if app.edit_bank == bank { - app.edit_pattern + let edit_pattern = if app.editor_ctx.bank == bank { + app.editor_ctx.pattern } else { usize::MAX }; diff --git a/seq/src/widgets/scope.rs b/seq/src/widgets/scope.rs index 4e4e558..d7fbcec 100644 --- a/seq/src/widgets/scope.rs +++ b/seq/src/widgets/scope.rs @@ -34,11 +34,6 @@ impl<'a> Scope<'a> { self.color = c; self } - - pub fn gain(mut self, g: f32) -> Self { - self.gain = g; - self - } } impl Widget for Scope<'_> { diff --git a/src/lib.rs b/src/lib.rs index 8291b35..f7440b2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,9 @@ use orbit::{EffectParams, Orbit}; use sample::{FileSource, SampleEntry, SampleInfo, SamplePool, WebSampleSource}; use schedule::Schedule; #[cfg(feature = "native")] -use telemetry::EngineMetrics; +use std::sync::Arc; +#[cfg(feature = "native")] +pub use telemetry::EngineMetrics; use types::{DelayType, Source, BLOCK_SIZE, CHANNELS, MAX_ORBITS, MAX_VOICES}; use voice::{Voice, VoiceParams}; @@ -55,7 +57,7 @@ pub struct Engine { pub effect_params: EffectParams, // Telemetry (native only) #[cfg(feature = "native")] - pub metrics: EngineMetrics, + pub metrics: Arc, } impl Engine { @@ -96,7 +98,48 @@ impl Engine { comb_damp: 0.1, }, #[cfg(feature = "native")] - metrics: EngineMetrics::default(), + metrics: Arc::new(EngineMetrics::default()), + } + } + + #[cfg(feature = "native")] + pub fn new_with_metrics( + sample_rate: f32, + output_channels: usize, + metrics: Arc, + ) -> Self { + let mut orbits = Vec::with_capacity(MAX_ORBITS); + for _ in 0..MAX_ORBITS { + orbits.push(Orbit::new(sample_rate)); + } + + Self { + sr: sample_rate, + isr: 1.0 / sample_rate, + voices: vec![Voice::default(); MAX_VOICES], + active_voices: 0, + orbits, + schedule: Schedule::new(), + time: 0.0, + tick: 0, + output_channels, + output: vec![0.0; BLOCK_SIZE * output_channels], + sample_pool: SamplePool::new(), + samples: Vec::with_capacity(256), + sample_index: Vec::new(), + effect_params: EffectParams { + delay_time: 0.333, + delay_feedback: 0.6, + delay_type: DelayType::Standard, + verb_decay: 0.75, + verb_damp: 0.95, + verb_predelay: 0.1, + verb_diff: 0.7, + comb_freq: 220.0, + comb_feedback: 0.9, + comb_damp: 0.1, + }, + metrics, } }