use doux::audio::AudioDeviceInfo; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use super::CyclicEnum; #[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] pub enum MainLayout { #[default] Top, Bottom, Left, Right, } impl CyclicEnum for MainLayout { const VARIANTS: &'static [Self] = &[Self::Top, Self::Bottom, Self::Left, Self::Right]; } #[derive(Clone, Copy, PartialEq, Eq, Default)] pub enum ScopeMode { #[default] Line, Filled, } impl ScopeMode { pub fn toggle(self) -> Self { match self { Self::Line => Self::Filled, Self::Filled => Self::Line, } } } #[derive(Clone, Copy, PartialEq, Eq, Default)] pub enum SpectrumMode { #[default] Bars, Line, Filled, } impl SpectrumMode { pub fn cycle(self) -> Self { match self { Self::Bars => Self::Line, Self::Line => Self::Filled, Self::Filled => Self::Bars, } } } #[derive(Clone, Copy, PartialEq, Eq, Default)] pub enum RefreshRate { #[default] Fps60, Fps30, Fps15, } impl RefreshRate { pub fn from_fps(fps: u32) -> Self { if fps >= 60 { RefreshRate::Fps60 } else if fps >= 30 { RefreshRate::Fps30 } else { RefreshRate::Fps15 } } pub fn toggle(self) -> Self { match self { RefreshRate::Fps60 => RefreshRate::Fps30, RefreshRate::Fps30 => RefreshRate::Fps15, RefreshRate::Fps15 => RefreshRate::Fps60, } } pub fn millis(self) -> u64 { match self { RefreshRate::Fps60 => 16, RefreshRate::Fps30 => 33, RefreshRate::Fps15 => 66, } } pub fn label(self) -> &'static str { match self { RefreshRate::Fps60 => "60", RefreshRate::Fps30 => "30", RefreshRate::Fps15 => "15", } } pub fn to_fps(self) -> u32 { match self { RefreshRate::Fps60 => 60, RefreshRate::Fps30 => 30, RefreshRate::Fps15 => 15, } } } #[derive(Clone)] pub struct AudioConfig { pub output_device: Option, pub input_device: Option, pub channels: u16, pub buffer_size: u32, pub max_voices: usize, pub sample_rate: f32, pub host_name: String, pub sample_paths: Vec, pub sample_counts: Vec, pub refresh_rate: RefreshRate, pub show_scope: bool, pub show_spectrum: bool, pub show_lissajous: bool, pub show_preview: bool, pub gain_boost: f32, pub normalize_viz: bool, pub layout: MainLayout, pub scope_mode: ScopeMode, pub scope_vertical: bool, pub lissajous_trails: bool, pub spectrum_mode: SpectrumMode, pub spectrum_peaks: bool, } impl Default for AudioConfig { fn default() -> Self { Self { output_device: None, input_device: None, channels: 2, buffer_size: 512, max_voices: 32, sample_rate: 44100.0, host_name: String::new(), sample_paths: Vec::new(), sample_counts: Vec::new(), refresh_rate: RefreshRate::default(), show_scope: true, show_spectrum: true, show_lissajous: true, show_preview: true, gain_boost: 1.0, normalize_viz: false, layout: MainLayout::default(), scope_mode: ScopeMode::default(), scope_vertical: false, lissajous_trails: false, spectrum_mode: SpectrumMode::default(), spectrum_peaks: false, } } } pub struct ListSelectState { pub cursor: usize, pub scroll_offset: usize, } impl ListSelectState { pub fn move_up(&mut self) { if self.cursor > 0 { self.cursor -= 1; if self.cursor < self.scroll_offset { self.scroll_offset = self.cursor; } } } pub fn move_down(&mut self, item_count: usize) { if self.cursor + 1 < item_count { self.cursor += 1; if self.cursor >= self.scroll_offset + 5 { self.scroll_offset = self.cursor - 4; } } } pub fn page_up(&mut self) { self.cursor = self.cursor.saturating_sub(5); if self.cursor < self.scroll_offset { self.scroll_offset = self.cursor; } } pub fn page_down(&mut self, item_count: usize) { self.cursor = (self.cursor + 5).min(item_count.saturating_sub(1)); if self.cursor >= self.scroll_offset + 5 { self.scroll_offset = self.cursor.saturating_sub(4); } } } #[derive(Clone, Copy, PartialEq, Eq, Default)] pub enum EngineSection { #[default] Devices, Settings, Samples, } impl CyclicEnum for EngineSection { const VARIANTS: &'static [Self] = &[Self::Devices, Self::Settings, Self::Samples]; } #[derive(Clone, Copy, PartialEq, Eq, Default)] pub enum DeviceKind { #[default] Output, Input, } #[derive(Clone, Copy, PartialEq, Eq, Default)] pub enum SettingKind { #[default] Channels, BufferSize, Polyphony, Nudge, } impl CyclicEnum for SettingKind { const VARIANTS: &'static [Self] = &[ Self::Channels, Self::BufferSize, Self::Polyphony, Self::Nudge, ]; } 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; 256], pub scope_right: [f32; 256], pub peak_left: f32, pub peak_right: f32, pub spectrum: [f32; 32], pub nudge_ms: f64, } 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; 256], scope_right: [0.0; 256], peak_left: 0.0, peak_right: 0.0, spectrum: [0.0; 32], nudge_ms: 0.0, } } } pub struct AudioSettings { pub config: AudioConfig, pub section: EngineSection, pub device_kind: DeviceKind, pub setting_kind: SettingKind, pub output_devices: Vec, pub input_devices: Vec, pub output_list: ListSelectState, pub input_list: ListSelectState, pub sample_list: ListSelectState, pub restart_pending: bool, pub error: Option, pub sample_registry: Option>, } impl Default for AudioSettings { fn default() -> Self { Self { config: AudioConfig::default(), section: EngineSection::default(), device_kind: DeviceKind::default(), setting_kind: SettingKind::default(), output_devices: doux::audio::list_output_devices(), input_devices: doux::audio::list_input_devices(), output_list: ListSelectState { cursor: 0, scroll_offset: 0, }, input_list: ListSelectState { cursor: 0, scroll_offset: 0, }, sample_list: ListSelectState { cursor: 0, scroll_offset: 0, }, restart_pending: false, error: None, sample_registry: None, } } } impl AudioSettings { pub fn new_plugin() -> Self { Self { config: AudioConfig::default(), section: EngineSection::default(), device_kind: DeviceKind::default(), setting_kind: SettingKind::default(), output_devices: Vec::new(), input_devices: Vec::new(), output_list: ListSelectState { cursor: 0, scroll_offset: 0, }, input_list: ListSelectState { cursor: 0, scroll_offset: 0, }, sample_list: ListSelectState { cursor: 0, scroll_offset: 0, }, restart_pending: false, error: None, sample_registry: None, } } 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_section(&mut self, plugin_mode: bool) { self.section = if plugin_mode { match self.section { EngineSection::Settings => EngineSection::Samples, EngineSection::Samples => EngineSection::Settings, EngineSection::Devices => EngineSection::Settings, } } else { self.section.next() }; } pub fn prev_section(&mut self, plugin_mode: bool) { self.section = if plugin_mode { match self.section { EngineSection::Settings => EngineSection::Samples, EngineSection::Samples => EngineSection::Settings, EngineSection::Devices => EngineSection::Settings, } } else { self.section.prev() }; } pub fn next_setting(&mut self, plugin_mode: bool) { self.setting_kind = if plugin_mode { match self.setting_kind { SettingKind::Polyphony => SettingKind::Nudge, SettingKind::Nudge => SettingKind::Polyphony, _ => SettingKind::Polyphony, } } else { self.setting_kind.next() }; } pub fn prev_setting(&mut self, plugin_mode: bool) { self.setting_kind = if plugin_mode { match self.setting_kind { SettingKind::Polyphony => SettingKind::Nudge, SettingKind::Nudge => SettingKind::Polyphony, _ => SettingKind::Polyphony, } } else { self.setting_kind.prev() }; } pub 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 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 adjust_max_voices(&mut self, delta: i32) { let new_val = (self.config.max_voices as i32 + delta).clamp(1, 128) as usize; self.config.max_voices = new_val; } pub fn toggle_refresh_rate(&mut self) { self.config.refresh_rate = self.config.refresh_rate.toggle(); } pub fn total_sample_count(&self) -> usize { self.config.sample_counts.iter().sum() } pub fn add_sample_path(&mut self, path: PathBuf, count: usize) { if !self.config.sample_paths.contains(&path) { self.config.sample_paths.push(path); self.config.sample_counts.push(count); } } pub fn remove_sample_path(&mut self, index: usize) { if index < self.config.sample_paths.len() { self.config.sample_paths.remove(index); self.config.sample_counts.remove(index); let len = self.config.sample_paths.len(); if len == 0 { self.sample_list.cursor = 0; self.sample_list.scroll_offset = 0; } else if self.sample_list.cursor >= len { self.sample_list.cursor = len - 1; } } } pub fn trigger_restart(&mut self) { self.restart_pending = true; } }