wip
This commit is contained in:
515
seq/src/app.rs
515
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<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;
|
||||
}
|
||||
}
|
||||
|
||||
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<SlotChange>,
|
||||
|
||||
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<PathBuf>,
|
||||
pub status_message: Option<String>,
|
||||
pub editor: TextArea<'static>,
|
||||
pub flash_until: Option<Instant>,
|
||||
pub modal: Modal,
|
||||
pub clipboard: Option<arboard::Clipboard>,
|
||||
pub doc_topic: usize,
|
||||
pub doc_scroll: usize,
|
||||
|
||||
pub audio_config: AudioConfig,
|
||||
pub audio_focus: AudioFocus,
|
||||
pub available_output_devices: Vec<AudioDeviceInfo>,
|
||||
pub available_input_devices: Vec<AudioDeviceInfo>,
|
||||
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<String> = 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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user