Refactoring

This commit is contained in:
2026-01-20 02:48:09 +01:00
parent ce0014020f
commit a81716cbd5
18 changed files with 1197 additions and 53253 deletions

View File

@@ -1,4 +1,3 @@
use doux::audio::AudioDeviceInfo;
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::collections::{HashMap, HashSet};
@@ -6,299 +5,20 @@ use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tui_textarea::TextArea;
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::{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 {
Sequencer,
Editor,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PatternField {
Length,
Speed,
}
#[derive(Clone, PartialEq, Eq)]
pub enum Modal {
None,
ConfirmQuit { selected: bool },
SaveAs(String),
LoadFrom(String),
RenameBank { bank: usize, name: String },
RenamePattern { bank: usize, pattern: usize, name: String },
SetPattern { field: PatternField, input: String },
AddSamplePath(String),
}
#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum PatternsViewLevel {
#[default]
Banks,
Patterns { bank: usize },
}
#[derive(Clone)]
pub struct AudioConfig {
pub output_device: Option<String>,
pub input_device: Option<String>,
pub channels: u16,
pub buffer_size: u32,
pub sample_rate: f32,
pub sample_paths: Vec<PathBuf>,
pub sample_count: usize,
}
impl Default for AudioConfig {
fn default() -> Self {
Self {
output_device: None,
input_device: None,
channels: 2,
buffer_size: 512,
sample_rate: 44100.0,
sample_paths: Vec::new(),
sample_count: 0,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum AudioFocus {
#[default]
OutputDevice,
InputDevice,
Channels,
BufferSize,
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<AudioDeviceInfo>,
pub input_devices: Vec<AudioDeviceInfo>,
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;
}
}
use crate::script::{Rng, ScriptEngine, StepContext, Variables};
use crate::sequencer::{SequencerSnapshot, SlotChange};
use crate::services::pattern_editor;
use crate::state::{
AudioSettings, EditorContext, Focus, Metrics, Modal, PatternField, PatternsViewLevel,
};
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 page: Page,
@@ -307,8 +27,6 @@ pub struct App {
pub patterns_view_level: PatternsViewLevel,
pub patterns_cursor: usize,
pub slot_data: [SlotState; MAX_SLOTS],
pub slot_steps: [usize; MAX_SLOTS],
pub queued_changes: Vec<SlotChange>,
pub metrics: Metrics,
@@ -329,18 +47,13 @@ pub struct App {
}
impl App {
pub fn new(tempo: f64, quantum: f64) -> Self {
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 {
tempo,
beat: 0.0,
phase: 0.0,
peers: 0,
playing: true,
quantum,
project: Project::default(),
page: Page::default(),
@@ -349,8 +62,6 @@ impl App {
patterns_view_level: PatternsViewLevel::default(),
patterns_cursor: 0,
slot_data: [SlotState::default(); MAX_SLOTS],
slot_steps: [0; MAX_SLOTS],
queued_changes: Vec::new(),
metrics: Metrics::default(),
@@ -371,6 +82,14 @@ impl App {
}
}
fn current_bank_pattern(&self) -> (usize, usize) {
(self.editor_ctx.bank, self.editor_ctx.pattern)
}
fn mark_current_dirty(&mut self) {
self.dirty_patterns.insert(self.current_bank_pattern());
}
pub fn mark_all_patterns_dirty(&mut self) {
for bank in 0..MAX_BANKS {
for pattern in 0..MAX_PATTERNS {
@@ -379,29 +98,21 @@ impl App {
}
}
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_up(&self, link: &LinkState) {
let current = link.tempo();
link.set_tempo((current + 1.0).min(300.0));
}
pub fn tempo_down(&mut self, link: &LinkState) {
self.tempo = (self.tempo - 1.0).max(20.0);
link.set_tempo(self.tempo);
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) {
pub fn toggle_focus(&mut self, link: &LinkState) {
match self.editor_ctx.focus {
Focus::Sequencer => {
self.editor_ctx.focus = Focus::Editor;
@@ -409,14 +120,15 @@ impl App {
}
Focus::Editor => {
self.save_editor_to_step();
self.compile_current_step();
self.compile_current_step(link);
self.editor_ctx.focus = Focus::Sequencer;
}
}
}
pub fn current_edit_pattern(&self) -> &Pattern {
self.project.pattern_at(self.editor_ctx.bank, self.editor_ctx.pattern)
let (bank, pattern) = self.current_bank_pattern();
self.project.pattern_at(bank, pattern)
}
pub fn next_step(&mut self) {
@@ -464,86 +176,80 @@ impl App {
}
pub fn toggle_step(&mut self) {
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));
let (bank, pattern) = self.current_bank_pattern();
pattern_editor::toggle_step(&mut self.project, bank, pattern, self.editor_ctx.step);
self.mark_current_dirty();
}
pub fn length_increase(&mut self) {
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));
let (bank, pattern) = self.current_bank_pattern();
pattern_editor::increase_length(&mut self.project, bank, pattern);
self.mark_current_dirty();
}
pub fn length_decrease(&mut self) {
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;
let (bank, pattern) = self.current_bank_pattern();
pattern_editor::decrease_length(&mut self.project, bank, pattern);
let new_len = pattern_editor::get_length(&self.project, bank, pattern);
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));
self.mark_current_dirty();
}
pub fn speed_increase(&mut self) {
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));
let (bank, pattern) = self.current_bank_pattern();
pattern_editor::increase_speed(&mut self.project, bank, pattern);
self.mark_current_dirty();
}
pub fn speed_decrease(&mut self) {
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));
let (bank, pattern) = self.current_bank_pattern();
pattern_editor::decrease_speed(&mut self.project, bank, pattern);
self.mark_current_dirty();
}
fn load_step_to_editor(&mut self) {
let step_idx = self.editor_ctx.step;
if let Some(step) = self.current_edit_pattern().step(step_idx) {
let lines: Vec<String> = if step.script.is_empty() {
let (bank, pattern) = self.current_bank_pattern();
if let Some(script) =
pattern_editor::get_step_script(&self.project, bank, pattern, self.editor_ctx.step)
{
let lines: Vec<String> = if script.is_empty() {
vec![String::new()]
} else {
step.script.lines().map(String::from).collect()
script.lines().map(String::from).collect()
};
self.editor_ctx.text = TextArea::new(lines);
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 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));
let (bank, pattern) = self.current_bank_pattern();
pattern_editor::set_step_script(
&mut self.project,
bank,
pattern,
self.editor_ctx.step,
text,
);
self.mark_current_dirty();
}
pub fn compile_current_step(&mut self) {
pub fn compile_current_step(&mut self, link: &LinkState) {
let step_idx = self.editor_ctx.step;
let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern);
let (bank, pattern) = self.current_bank_pattern();
let script = self
.project
.pattern_at(bank, pattern)
.step(step_idx)
.map(|s| s.script.clone())
let script = pattern_editor::get_step_script(&self.project, bank, pattern, step_idx)
.unwrap_or_default();
if script.trim().is_empty() {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
if let Some(step) = self
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = None;
}
return;
@@ -551,24 +257,32 @@ impl App {
let ctx = StepContext {
step: step_idx,
beat: self.beat,
beat: link.beat(),
bank,
pattern,
tempo: self.tempo,
phase: self.phase,
tempo: link.tempo(),
phase: link.phase(),
slot: 0,
};
match self.script_engine.evaluate(&script, &ctx) {
Ok(cmd) => {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
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) {
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}"));
@@ -576,20 +290,20 @@ impl App {
}
}
pub fn compile_all_steps(&mut self) {
pub fn compile_all_steps(&mut self, link: &LinkState) {
let pattern_len = self.current_edit_pattern().length;
let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern);
let (bank, pattern) = self.current_bank_pattern();
for step_idx in 0..pattern_len {
let script = self
.project
.pattern_at(bank, pattern)
.step(step_idx)
.map(|s| s.script.clone())
let script = pattern_editor::get_step_script(&self.project, bank, pattern, step_idx)
.unwrap_or_default();
if script.trim().is_empty() {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
if let Some(step) = self
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = None;
}
continue;
@@ -600,26 +314,37 @@ impl App {
beat: 0.0,
bank,
pattern,
tempo: self.tempo,
tempo: link.tempo(),
phase: 0.0,
slot: 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) {
if let Some(step) = self
.project
.pattern_at_mut(bank, pattern)
.step_mut(step_idx)
{
step.command = Some(cmd);
}
}
}
}
pub fn is_pattern_queued(&self, bank: usize, pattern: usize) -> Option<bool> {
pub fn is_pattern_queued(
&self,
bank: usize,
pattern: usize,
snapshot: &SequencerSnapshot,
) -> Option<bool> {
self.queued_changes.iter().find_map(|c| match c {
SlotChange::Add { slot: _, bank: b, pattern: p } if *b == bank && *p == pattern => {
Some(true)
}
SlotChange::Add {
slot: _,
bank: b,
pattern: p,
} if *b == bank && *p == pattern => Some(true),
SlotChange::Remove { slot } => {
let s = self.slot_data[*slot];
let s = snapshot.slot_data[*slot];
if s.active && s.bank == bank && s.pattern == pattern {
Some(false)
} else {
@@ -630,8 +355,13 @@ impl App {
})
}
pub fn toggle_pattern_playback(&mut self, bank: usize, pattern: usize) {
let playing_slot = self.slot_data.iter().enumerate().find_map(|(i, s)| {
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 {
@@ -640,24 +370,45 @@ impl App {
});
let pending = self.queued_changes.iter().position(|c| match c {
SlotChange::Add { bank: b, pattern: p, .. } => *b == bank && *p == pattern,
SlotChange::Add {
bank: b,
pattern: p,
..
} => *b == bank && *p == pattern,
SlotChange::Remove { slot } => {
let s = self.slot_data[*slot];
let s = snapshot.slot_data[*slot];
s.bank == bank && s.pattern == pattern
}
});
if let Some(idx) = pending {
self.queued_changes.remove(idx);
self.status_message = Some(format!("B{:02}:P{:02} change cancelled", bank + 1, pattern + 1));
self.status_message = Some(format!(
"B{:02}:P{:02} change cancelled",
bank + 1,
pattern + 1
));
} else if let Some(slot_idx) = playing_slot {
self.queued_changes.push(SlotChange::Remove { slot: slot_idx });
self.status_message = Some(format!("B{:02}:P{:02} queued to stop", bank + 1, pattern + 1));
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].active);
let free_slot = (0..MAX_SLOTS).find(|&i| !snapshot.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));
self.queued_changes.push(SlotChange::Add {
slot: slot_idx,
bank,
pattern,
});
self.status_message = Some(format!(
"B{:02}:P{:02} queued to play",
bank + 1,
pattern + 1
));
} else {
self.status_message = Some("All slots occupied".to_string());
}
@@ -690,13 +441,13 @@ impl App {
}
}
pub fn load(&mut self, path: PathBuf) {
pub fn load(&mut self, path: PathBuf, link: &LinkState) {
match file::load(&path) {
Ok(project) => {
self.project = project;
self.editor_ctx.step = 0;
self.load_step_to_editor();
self.compile_all_steps();
self.compile_all_steps(link);
self.mark_all_patterns_dirty();
self.status_message = Some(format!("Loaded: {}", path.display()));
self.file_path = Some(path);
@@ -718,11 +469,9 @@ impl App {
}
pub fn copy_step(&mut self) {
let step_idx = self.editor_ctx.step;
let script = self
.current_edit_pattern()
.step(step_idx)
.map(|s| s.script.clone());
let (bank, pattern) = self.current_bank_pattern();
let script =
pattern_editor::get_step_script(&self.project, bank, pattern, self.editor_ctx.step);
if let Some(script) = script {
if let Some(clip) = &mut self.clipboard {
@@ -733,21 +482,24 @@ impl App {
}
}
pub fn paste_step(&mut self) {
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 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));
let (bank, pattern) = self.current_bank_pattern();
pattern_editor::set_step_script(
&mut self.project,
bank,
pattern,
self.editor_ctx.step,
text,
);
self.mark_current_dirty();
self.load_step_to_editor();
self.compile_current_step();
self.compile_current_step(link);
}
}
@@ -756,7 +508,9 @@ impl App {
PatternField::Length => self.current_edit_pattern().length.to_string(),
PatternField::Speed => self.current_edit_pattern().speed.label().to_string(),
};
self.modal = Modal::SetPattern { field, input: current };
self.modal = Modal::SetPattern {
field,
input: current,
};
}
}