Init
This commit is contained in:
292
src/state/audio.rs
Normal file
292
src/state/audio.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
use doux::audio::AudioDeviceInfo;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum RefreshRate {
|
||||
#[default]
|
||||
Fps60,
|
||||
Fps30,
|
||||
}
|
||||
|
||||
impl RefreshRate {
|
||||
pub fn from_fps(fps: u32) -> Self {
|
||||
if fps >= 60 {
|
||||
RefreshRate::Fps60
|
||||
} else {
|
||||
RefreshRate::Fps30
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(self) -> Self {
|
||||
match self {
|
||||
RefreshRate::Fps60 => RefreshRate::Fps30,
|
||||
RefreshRate::Fps30 => RefreshRate::Fps60,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn millis(self) -> u64 {
|
||||
match self {
|
||||
RefreshRate::Fps60 => 16,
|
||||
RefreshRate::Fps30 => 33,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
RefreshRate::Fps60 => "60",
|
||||
RefreshRate::Fps30 => "30",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_fps(self) -> u32 {
|
||||
match self {
|
||||
RefreshRate::Fps60 => 60,
|
||||
RefreshRate::Fps30 => 30,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
pub refresh_rate: RefreshRate,
|
||||
}
|
||||
|
||||
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,
|
||||
refresh_rate: RefreshRate::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum AudioFocus {
|
||||
#[default]
|
||||
OutputDevice,
|
||||
InputDevice,
|
||||
Channels,
|
||||
BufferSize,
|
||||
RefreshRate,
|
||||
RuntimeHighlight,
|
||||
SamplePaths,
|
||||
LinkEnabled,
|
||||
StartStopSync,
|
||||
Quantum,
|
||||
}
|
||||
|
||||
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],
|
||||
pub peak_left: f32,
|
||||
pub peak_right: f32,
|
||||
}
|
||||
|
||||
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],
|
||||
peak_left: 0.0,
|
||||
peak_right: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AudioSettings {
|
||||
pub config: AudioConfig,
|
||||
pub focus: AudioFocus,
|
||||
pub output_devices: Vec<AudioDeviceInfo>,
|
||||
pub input_devices: Vec<AudioDeviceInfo>,
|
||||
pub restart_pending: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
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,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::RefreshRate,
|
||||
AudioFocus::RefreshRate => AudioFocus::RuntimeHighlight,
|
||||
AudioFocus::RuntimeHighlight => AudioFocus::SamplePaths,
|
||||
AudioFocus::SamplePaths => AudioFocus::LinkEnabled,
|
||||
AudioFocus::LinkEnabled => AudioFocus::StartStopSync,
|
||||
AudioFocus::StartStopSync => AudioFocus::Quantum,
|
||||
AudioFocus::Quantum => AudioFocus::OutputDevice,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn prev_focus(&mut self) {
|
||||
self.focus = match self.focus {
|
||||
AudioFocus::OutputDevice => AudioFocus::Quantum,
|
||||
AudioFocus::InputDevice => AudioFocus::OutputDevice,
|
||||
AudioFocus::Channels => AudioFocus::InputDevice,
|
||||
AudioFocus::BufferSize => AudioFocus::Channels,
|
||||
AudioFocus::RefreshRate => AudioFocus::BufferSize,
|
||||
AudioFocus::RuntimeHighlight => AudioFocus::RefreshRate,
|
||||
AudioFocus::SamplePaths => AudioFocus::RuntimeHighlight,
|
||||
AudioFocus::LinkEnabled => AudioFocus::SamplePaths,
|
||||
AudioFocus::StartStopSync => AudioFocus::LinkEnabled,
|
||||
AudioFocus::Quantum => AudioFocus::StartStopSync,
|
||||
};
|
||||
}
|
||||
|
||||
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 toggle_refresh_rate(&mut self) {
|
||||
self.config.refresh_rate = self.config.refresh_rate.toggle();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
42
src/state/editor.rs
Normal file
42
src/state/editor.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use tui_textarea::TextArea;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Focus {
|
||||
Sequencer,
|
||||
Editor,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PatternField {
|
||||
Length,
|
||||
Speed,
|
||||
}
|
||||
|
||||
pub struct EditorContext {
|
||||
pub bank: usize,
|
||||
pub pattern: usize,
|
||||
pub step: usize,
|
||||
pub focus: Focus,
|
||||
pub text: TextArea<'static>,
|
||||
pub copied_step: Option<CopiedStep>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct CopiedStep {
|
||||
pub bank: usize,
|
||||
pub pattern: usize,
|
||||
pub step: usize,
|
||||
}
|
||||
|
||||
impl Default for EditorContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
step: 0,
|
||||
focus: Focus::Sequencer,
|
||||
text: TextArea::default(),
|
||||
copied_step: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/state/live_keys.rs
Normal file
21
src/state/live_keys.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LiveKeyState {
|
||||
fill: AtomicBool,
|
||||
}
|
||||
|
||||
impl LiveKeyState {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn fill(&self) -> bool {
|
||||
self.fill.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn flip_fill(&self) {
|
||||
let current = self.fill.load(Ordering::Relaxed);
|
||||
self.fill.store(!current, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
17
src/state/mod.rs
Normal file
17
src/state/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
pub mod audio;
|
||||
pub mod editor;
|
||||
pub mod live_keys;
|
||||
pub mod modal;
|
||||
pub mod patterns_nav;
|
||||
pub mod playback;
|
||||
pub mod project;
|
||||
pub mod ui;
|
||||
|
||||
pub use audio::{AudioFocus, AudioSettings, Metrics};
|
||||
pub use editor::{CopiedStep, EditorContext, Focus, PatternField};
|
||||
pub use live_keys::LiveKeyState;
|
||||
pub use modal::Modal;
|
||||
pub use patterns_nav::{PatternsColumn, PatternsNav};
|
||||
pub use playback::PlaybackState;
|
||||
pub use project::ProjectState;
|
||||
pub use ui::UiState;
|
||||
42
src/state/modal.rs
Normal file
42
src/state/modal.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use crate::state::editor::PatternField;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum Modal {
|
||||
None,
|
||||
ConfirmQuit {
|
||||
selected: bool,
|
||||
},
|
||||
ConfirmDeleteStep {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
step: usize,
|
||||
selected: bool,
|
||||
},
|
||||
ConfirmResetPattern {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
selected: bool,
|
||||
},
|
||||
ConfirmResetBank {
|
||||
bank: usize,
|
||||
selected: bool,
|
||||
},
|
||||
SaveAs(String),
|
||||
LoadFrom(String),
|
||||
RenameBank {
|
||||
bank: usize,
|
||||
name: String,
|
||||
},
|
||||
RenamePattern {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
name: String,
|
||||
},
|
||||
SetPattern {
|
||||
field: PatternField,
|
||||
input: String,
|
||||
},
|
||||
SetTempo(String),
|
||||
AddSamplePath(String),
|
||||
Editor,
|
||||
}
|
||||
53
src/state/patterns_nav.rs
Normal file
53
src/state/patterns_nav.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum PatternsColumn {
|
||||
#[default]
|
||||
Banks,
|
||||
Patterns,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct PatternsNav {
|
||||
pub column: PatternsColumn,
|
||||
pub bank_cursor: usize,
|
||||
pub pattern_cursor: usize,
|
||||
}
|
||||
|
||||
impl PatternsNav {
|
||||
pub fn move_left(&mut self) {
|
||||
self.column = PatternsColumn::Banks;
|
||||
}
|
||||
|
||||
pub fn move_right(&mut self) {
|
||||
self.column = PatternsColumn::Patterns;
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self) {
|
||||
match self.column {
|
||||
PatternsColumn::Banks => {
|
||||
self.bank_cursor = (self.bank_cursor + 15) % 16;
|
||||
}
|
||||
PatternsColumn::Patterns => {
|
||||
self.pattern_cursor = (self.pattern_cursor + 15) % 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_down(&mut self) {
|
||||
match self.column {
|
||||
PatternsColumn::Banks => {
|
||||
self.bank_cursor = (self.bank_cursor + 1) % 16;
|
||||
}
|
||||
PatternsColumn::Patterns => {
|
||||
self.pattern_cursor = (self.pattern_cursor + 1) % 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_bank(&self) -> usize {
|
||||
self.bank_cursor
|
||||
}
|
||||
|
||||
pub fn selected_pattern(&self) -> usize {
|
||||
self.pattern_cursor
|
||||
}
|
||||
}
|
||||
21
src/state/playback.rs
Normal file
21
src/state/playback.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use crate::engine::PatternChange;
|
||||
|
||||
pub struct PlaybackState {
|
||||
pub playing: bool,
|
||||
pub queued_changes: Vec<PatternChange>,
|
||||
}
|
||||
|
||||
impl Default for PlaybackState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
playing: true,
|
||||
queued_changes: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlaybackState {
|
||||
pub fn toggle(&mut self) {
|
||||
self.playing = !self.playing;
|
||||
}
|
||||
}
|
||||
41
src/state/project.rs
Normal file
41
src/state/project.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::config::{MAX_BANKS, MAX_PATTERNS};
|
||||
use crate::model::Project;
|
||||
|
||||
pub struct ProjectState {
|
||||
pub project: Project,
|
||||
pub file_path: Option<PathBuf>,
|
||||
pub dirty_patterns: HashSet<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl Default for ProjectState {
|
||||
fn default() -> Self {
|
||||
let mut state = Self {
|
||||
project: Project::default(),
|
||||
file_path: None,
|
||||
dirty_patterns: HashSet::new(),
|
||||
};
|
||||
state.mark_all_dirty();
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectState {
|
||||
pub fn mark_dirty(&mut self, bank: usize, pattern: usize) {
|
||||
self.dirty_patterns.insert((bank, pattern));
|
||||
}
|
||||
|
||||
pub fn mark_all_dirty(&mut self) {
|
||||
for bank in 0..MAX_BANKS {
|
||||
for pattern in 0..MAX_PATTERNS {
|
||||
self.dirty_patterns.insert((bank, pattern));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_dirty(&mut self) -> HashSet<(usize, usize)> {
|
||||
std::mem::take(&mut self.dirty_patterns)
|
||||
}
|
||||
}
|
||||
50
src/state/ui.rs
Normal file
50
src/state/ui.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::state::Modal;
|
||||
|
||||
pub struct UiState {
|
||||
pub status_message: Option<String>,
|
||||
pub flash_until: Option<Instant>,
|
||||
pub modal: Modal,
|
||||
pub doc_topic: usize,
|
||||
pub doc_scroll: usize,
|
||||
pub doc_category: usize,
|
||||
pub show_title: bool,
|
||||
pub runtime_highlight: bool,
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
status_message: None,
|
||||
flash_until: None,
|
||||
modal: Modal::None,
|
||||
doc_topic: 0,
|
||||
doc_scroll: 0,
|
||||
doc_category: 0,
|
||||
show_title: true,
|
||||
runtime_highlight: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UiState {
|
||||
pub fn flash(&mut self, msg: &str, duration_ms: u64) {
|
||||
self.status_message = Some(msg.to_string());
|
||||
self.flash_until = Some(Instant::now() + Duration::from_millis(duration_ms));
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, msg: String) {
|
||||
self.status_message = Some(msg);
|
||||
}
|
||||
|
||||
pub fn clear_status(&mut self) {
|
||||
self.status_message = None;
|
||||
}
|
||||
|
||||
pub fn is_flashing(&self) -> bool {
|
||||
self.flash_until
|
||||
.map(|t| Instant::now() < t)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user