Some checks failed
CI / check (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Failing after 1m21s
Deploy Website / deploy (push) Has been skipped
CI / check (macos-14, aarch64-apple-darwin) (push) Has been cancelled
CI / check (windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
470 lines
12 KiB
Rust
470 lines
12 KiB
Rust
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<String>,
|
|
pub input_device: Option<String>,
|
|
pub channels: u16,
|
|
pub buffer_size: u32,
|
|
pub max_voices: usize,
|
|
pub sample_rate: f32,
|
|
pub host_name: String,
|
|
pub sample_paths: Vec<PathBuf>,
|
|
pub sample_counts: Vec<usize>,
|
|
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<AudioDeviceInfo>,
|
|
pub input_devices: Vec<AudioDeviceInfo>,
|
|
pub output_list: ListSelectState,
|
|
pub input_list: ListSelectState,
|
|
pub sample_list: ListSelectState,
|
|
pub restart_pending: bool,
|
|
pub error: Option<String>,
|
|
pub sample_registry: Option<std::sync::Arc<doux::SampleRegistry>>,
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|