Redo lost work

This commit is contained in:
2026-03-20 00:08:57 +01:00
parent 44fe435770
commit af3c5c0985
15 changed files with 237 additions and 56 deletions

3
Cargo.lock generated
View File

@@ -1824,8 +1824,7 @@ dependencies = [
[[package]] [[package]]
name = "doux" name = "doux"
version = "0.0.15" version = "0.0.18"
source = "git+https://github.com/sova-org/doux?tag=v0.0.15#29d8f055612f6141d7546d72b91e60026937b0fd"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"clap", "clap",

View File

@@ -51,7 +51,7 @@ cagire-forth = { path = "crates/forth" }
cagire-markdown = { path = "crates/markdown" } cagire-markdown = { path = "crates/markdown" }
cagire-project = { path = "crates/project" } cagire-project = { path = "crates/project" }
cagire-ratatui = { path = "crates/ratatui" } cagire-ratatui = { path = "crates/ratatui" }
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.15", features = ["native", "soundfont"] } doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.18", features = ["native", "soundfont"] }
rusty_link = "0.4" rusty_link = "0.4"
ratatui = "0.30" ratatui = "0.30"
crossterm = "0.29" crossterm = "0.29"
@@ -105,6 +105,9 @@ egui-baseview = { path = "plugins/egui-baseview" }
[patch."https://github.com/RustAudio/baseview.git"] [patch."https://github.com/RustAudio/baseview.git"]
baseview = { path = "plugins/baseview" } baseview = { path = "plugins/baseview" }
[patch.'https://github.com/sova-org/doux']
doux = { path = "/Users/bubo/doux" }
[package.metadata.bundle.bin.cagire-desktop] [package.metadata.bundle.bin.cagire-desktop]
name = "Cagire" name = "Cagire"
identifier = "com.sova.cagire" identifier = "com.sova.cagire"

View File

@@ -14,7 +14,7 @@ cagire = { path = "../..", default-features = false, features = ["block-renderer
cagire-forth = { path = "../../crates/forth" } cagire-forth = { path = "../../crates/forth" }
cagire-project = { path = "../../crates/project" } cagire-project = { path = "../../crates/project" }
cagire-ratatui = { path = "../../crates/ratatui" } cagire-ratatui = { path = "../../crates/ratatui" }
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.15", features = ["native", "soundfont"] } doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.18", features = ["native", "soundfont"] }
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] } nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" } nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
egui_ratatui = "2.1" egui_ratatui = "2.1"

View File

@@ -375,7 +375,11 @@ impl App {
AppCommand::AudioSettingPrev => self.audio.prev_setting(self.plugin_mode), AppCommand::AudioSettingPrev => self.audio.prev_setting(self.plugin_mode),
AppCommand::SetOutputDevice(name) => self.audio.config.output_device = Some(name), AppCommand::SetOutputDevice(name) => self.audio.config.output_device = Some(name),
AppCommand::SetInputDevice(name) => self.audio.config.input_device = Some(name), AppCommand::SetInputDevice(name) => self.audio.config.input_device = Some(name),
AppCommand::SetDeviceKind(kind) => self.audio.device_kind = kind, AppCommand::SetDevicesFocus(focus) => self.audio.devices_focus = focus,
AppCommand::CycleHost { right } => {
self.audio.cycle_host(right);
self.audio.trigger_restart();
}
AppCommand::AdjustAudioSetting { setting, delta } => { AppCommand::AdjustAudioSetting { setting, delta } => {
use crate::state::SettingKind; use crate::state::SettingKind;
match setting { match setting {

View File

@@ -16,6 +16,7 @@ impl App {
pub fn save_settings(&self, link: &LinkState) { pub fn save_settings(&self, link: &LinkState) {
let settings = Settings { let settings = Settings {
audio: crate::settings::AudioSettings { audio: crate::settings::AudioSettings {
host: self.audio.config.selected_host.clone(),
output_device: self.audio.config.output_device.clone(), output_device: self.audio.config.output_device.clone(),
input_device: self.audio.config.input_device.clone(), input_device: self.audio.config.input_device.clone(),
channels: self.audio.config.channels, channels: self.audio.config.channels,

View File

@@ -131,6 +131,7 @@ impl CagireDesktop {
let new_audio_rx = sequencer.swap_audio_channel(); let new_audio_rx = sequencer.swap_audio_channel();
let new_config = AudioStreamConfig { let new_config = AudioStreamConfig {
host: self.app.audio.config.selected_host.clone(),
output_device: self.app.audio.config.output_device.clone(), output_device: self.app.audio.config.output_device.clone(),
input_device: self.app.audio.config.input_device.clone(), input_device: self.app.audio.config.input_device.clone(),
channels: self.app.audio.config.channels, channels: self.app.audio.config.channels,

View File

@@ -4,7 +4,7 @@ use std::path::PathBuf;
use crate::model::{FollowUp, LaunchQuantization, PatternSpeed}; use crate::model::{FollowUp, LaunchQuantization, PatternSpeed};
use crate::page::Page; use crate::page::Page;
use crate::state::{ColorScheme, DeviceKind, Modal, OptionsFocus, PatternField, ScriptField, SettingKind}; use crate::state::{ColorScheme, DevicesFocus, Modal, OptionsFocus, PatternField, ScriptField, SettingKind};
pub enum AppCommand { pub enum AppCommand {
// Undo/Redo // Undo/Redo
@@ -252,7 +252,8 @@ pub enum AppCommand {
AudioSettingPrev, AudioSettingPrev,
SetOutputDevice(String), SetOutputDevice(String),
SetInputDevice(String), SetInputDevice(String),
SetDeviceKind(DeviceKind), SetDevicesFocus(DevicesFocus),
CycleHost { right: bool },
AdjustAudioSetting { AdjustAudioSetting {
setting: SettingKind, setting: SettingKind,
delta: i32, delta: i32,

View File

@@ -279,6 +279,7 @@ use super::AudioCommand;
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
pub struct AudioStreamConfig { pub struct AudioStreamConfig {
pub host: Option<String>,
pub output_device: Option<String>, pub output_device: Option<String>,
pub input_device: Option<String>, pub input_device: Option<String>,
pub channels: u16, pub channels: u16,
@@ -317,10 +318,16 @@ pub fn build_stream(
sample_paths: &[std::path::PathBuf], sample_paths: &[std::path::PathBuf],
device_lost: Arc<AtomicBool>, device_lost: Arc<AtomicBool>,
) -> Result<BuildStreamResult, String> { ) -> Result<BuildStreamResult, String> {
let selection = match &config.host {
Some(name) => doux::audio::HostSelection::Named(name.to_lowercase()),
None => doux::audio::HostSelection::Auto,
};
let host = doux::audio::get_host(selection).map_err(|e| format!("{e}"))?;
let device = match &config.output_device { let device = match &config.output_device {
Some(name) => doux::audio::find_output_device(name) Some(name) => doux::audio::find_output_device_for(&host, name)
.ok_or_else(|| format!("Device not found: {name}"))?, .ok_or_else(|| format!("Device not found: {name}"))?,
None => doux::audio::default_output_device().ok_or("No default output device")?, None => doux::audio::default_output_device_for(&host).ok_or("No default output device")?,
}; };
let default_config = device.default_output_config().map_err(|e| e.to_string())?; let default_config = device.default_output_config().map_err(|e| e.to_string())?;
@@ -329,10 +336,10 @@ pub fn build_stream(
let max_channels = doux::audio::max_output_channels(&device); let max_channels = doux::audio::max_output_channels(&device);
let channels = config.channels.min(max_channels).max(2); let channels = config.channels.min(max_channels).max(2);
let host_name = doux::audio::preferred_host().id().name().to_string(); let host_name = host.id().name().to_string();
let is_jack = host_name.to_lowercase().contains("jack"); let host_managed_buffer = doux::audio::host_controls_buffer_size(&host);
let buffer_size = if config.buffer_size > 0 && !is_jack { let buffer_size = if config.buffer_size > 0 && !host_managed_buffer {
cpal::BufferSize::Fixed(config.buffer_size) cpal::BufferSize::Fixed(config.buffer_size)
} else { } else {
cpal::BufferSize::Default cpal::BufferSize::Default
@@ -370,7 +377,7 @@ pub fn build_stream(
.input_device .input_device
.as_ref() .as_ref()
.and_then(|name| { .and_then(|name| {
let dev = doux::audio::find_input_device(name); let dev = doux::audio::find_input_device_for(&host, name);
if dev.is_none() { if dev.is_none() {
eprintln!("input device not found: {name}"); eprintln!("input device not found: {name}");
} }

View File

@@ -99,6 +99,18 @@ pub fn init(args: InitArgs) -> Init {
}); });
} }
app.audio.config.selected_host = settings.audio.host.clone();
if let Some(ref host_name) = app.audio.config.selected_host {
if let Some(idx) = app
.audio
.available_hosts
.iter()
.position(|h| &h.name == host_name)
{
app.audio.host_index = idx;
app.audio.refresh_devices_for_host();
}
}
app.audio.config.output_device = args.output.or(settings.audio.output_device.clone()); app.audio.config.output_device = args.output.or(settings.audio.output_device.clone());
app.audio.config.input_device = args.input.or(settings.audio.input_device.clone()); app.audio.config.input_device = args.input.or(settings.audio.input_device.clone());
app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels); app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels);
@@ -210,6 +222,7 @@ pub fn init(args: InitArgs) -> Init {
let (stream_error_tx, stream_error_rx) = crossbeam_channel::bounded(16); let (stream_error_tx, stream_error_rx) = crossbeam_channel::bounded(16);
let stream_config = AudioStreamConfig { let stream_config = AudioStreamConfig {
host: app.audio.config.selected_host.clone(),
output_device: app.audio.config.output_device.clone(), output_device: app.audio.config.output_device.clone(),
input_device: app.audio.config.input_device.clone(), input_device: app.audio.config.input_device.clone(),
channels: app.audio.config.channels, channels: app.audio.config.channels,

View File

@@ -4,7 +4,7 @@ use std::sync::atomic::Ordering;
use super::{InputContext, InputResult}; use super::{InputContext, InputResult};
use crate::commands::AppCommand; use crate::commands::AppCommand;
use crate::engine::{AudioCommand, SeqCommand}; use crate::engine::{AudioCommand, SeqCommand};
use crate::state::{ConfirmAction, DeviceKind, EngineSection, LinkSetting, Modal, SettingKind}; use crate::state::{ConfirmAction, DevicesFocus, EngineSection, LinkSetting, Modal, SettingKind};
pub(crate) fn cycle_engine_setting(ctx: &mut InputContext, right: bool) { pub(crate) fn cycle_engine_setting(ctx: &mut InputContext, right: bool) {
let sign = if right { 1 } else { -1 }; let sign = if right { 1 } else { -1 };
@@ -13,10 +13,14 @@ pub(crate) fn cycle_engine_setting(ctx: &mut InputContext, right: bool) {
setting: SettingKind::Channels, setting: SettingKind::Channels,
delta: sign, delta: sign,
}), }),
SettingKind::BufferSize => ctx.dispatch(AppCommand::AdjustAudioSetting { SettingKind::BufferSize => {
if !ctx.app.audio.host_controls_buffer() {
ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::BufferSize, setting: SettingKind::BufferSize,
delta: sign * 64, delta: sign * 64,
}), });
}
}
SettingKind::Polyphony => ctx.dispatch(AppCommand::AdjustAudioSetting { SettingKind::Polyphony => ctx.dispatch(AppCommand::AdjustAudioSetting {
setting: SettingKind::Polyphony, setting: SettingKind::Polyphony,
delta: sign, delta: sign,
@@ -146,9 +150,22 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
KeyCode::Tab => ctx.dispatch(AppCommand::AudioNextSection), KeyCode::Tab => ctx.dispatch(AppCommand::AudioNextSection),
KeyCode::BackTab => ctx.dispatch(AppCommand::AudioPrevSection), KeyCode::BackTab => ctx.dispatch(AppCommand::AudioPrevSection),
KeyCode::Up => match ctx.app.audio.section { KeyCode::Up => match ctx.app.audio.section {
EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.device_kind { EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.devices_focus {
DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputListUp), DevicesFocus::Host => {}
DeviceKind::Input => ctx.dispatch(AppCommand::AudioInputListUp), DevicesFocus::Output => {
if ctx.app.audio.output_list.cursor == 0 {
ctx.dispatch(AppCommand::SetDevicesFocus(DevicesFocus::Host));
} else {
ctx.dispatch(AppCommand::AudioOutputListUp);
}
}
DevicesFocus::Input => {
if ctx.app.audio.input_list.cursor == 0 {
ctx.dispatch(AppCommand::SetDevicesFocus(DevicesFocus::Host));
} else {
ctx.dispatch(AppCommand::AudioInputListUp);
}
}
}, },
EngineSection::Settings => { EngineSection::Settings => {
ctx.dispatch(AppCommand::AudioSettingPrev); ctx.dispatch(AppCommand::AudioSettingPrev);
@@ -168,12 +185,15 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
_ => {} _ => {}
}, },
KeyCode::Down => match ctx.app.audio.section { KeyCode::Down => match ctx.app.audio.section {
EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.device_kind { EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.devices_focus {
DeviceKind::Output => { DevicesFocus::Host => {
ctx.dispatch(AppCommand::SetDevicesFocus(DevicesFocus::Output));
}
DevicesFocus::Output => {
let count = ctx.app.audio.output_devices.len(); let count = ctx.app.audio.output_devices.len();
ctx.dispatch(AppCommand::AudioOutputListDown(count)); ctx.dispatch(AppCommand::AudioOutputListDown(count));
} }
DeviceKind::Input => { DevicesFocus::Input => {
let count = ctx.app.audio.input_devices.len(); let count = ctx.app.audio.input_devices.len();
ctx.dispatch(AppCommand::AudioInputListDown(count)); ctx.dispatch(AppCommand::AudioInputListDown(count));
} }
@@ -198,20 +218,22 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
}, },
KeyCode::PageUp => { KeyCode::PageUp => {
if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices { if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices {
match ctx.app.audio.device_kind { match ctx.app.audio.devices_focus {
DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputPageUp), DevicesFocus::Host => {}
DeviceKind::Input => ctx.app.audio.input_list.page_up(), DevicesFocus::Output => ctx.dispatch(AppCommand::AudioOutputPageUp),
DevicesFocus::Input => ctx.app.audio.input_list.page_up(),
} }
} }
} }
KeyCode::PageDown => { KeyCode::PageDown => {
if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices { if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices {
match ctx.app.audio.device_kind { match ctx.app.audio.devices_focus {
DeviceKind::Output => { DevicesFocus::Host => {}
DevicesFocus::Output => {
let count = ctx.app.audio.output_devices.len(); let count = ctx.app.audio.output_devices.len();
ctx.dispatch(AppCommand::AudioOutputPageDown(count)); ctx.dispatch(AppCommand::AudioOutputPageDown(count));
} }
DeviceKind::Input => { DevicesFocus::Input => {
let count = ctx.app.audio.input_devices.len(); let count = ctx.app.audio.input_devices.len();
ctx.dispatch(AppCommand::AudioInputPageDown(count)); ctx.dispatch(AppCommand::AudioInputPageDown(count));
} }
@@ -220,8 +242,9 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
} }
KeyCode::Enter => { KeyCode::Enter => {
if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices { if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices {
match ctx.app.audio.device_kind { match ctx.app.audio.devices_focus {
DeviceKind::Output => { DevicesFocus::Host => {}
DevicesFocus::Output => {
let cursor = ctx.app.audio.output_list.cursor; let cursor = ctx.app.audio.output_list.cursor;
if cursor < ctx.app.audio.output_devices.len() { if cursor < ctx.app.audio.output_devices.len() {
let index = ctx.app.audio.output_devices[cursor].index; let index = ctx.app.audio.output_devices[cursor].index;
@@ -229,7 +252,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
ctx.app.save_settings(ctx.link); ctx.app.save_settings(ctx.link);
} }
} }
DeviceKind::Input => { DevicesFocus::Input => {
let cursor = ctx.app.audio.input_list.cursor; let cursor = ctx.app.audio.input_list.cursor;
if cursor < ctx.app.audio.input_devices.len() { if cursor < ctx.app.audio.input_devices.len() {
let index = ctx.app.audio.input_devices[cursor].index; let index = ctx.app.audio.input_devices[cursor].index;
@@ -241,9 +264,16 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
} }
} }
KeyCode::Left => match ctx.app.audio.section { KeyCode::Left => match ctx.app.audio.section {
EngineSection::Devices if !ctx.app.plugin_mode => { EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.devices_focus {
ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Output)); DevicesFocus::Host => {
ctx.dispatch(AppCommand::CycleHost { right: false });
ctx.app.save_settings(ctx.link);
} }
DevicesFocus::Output => {}
DevicesFocus::Input => {
ctx.dispatch(AppCommand::SetDevicesFocus(DevicesFocus::Output));
}
},
EngineSection::Settings => cycle_engine_setting(ctx, false), EngineSection::Settings => cycle_engine_setting(ctx, false),
EngineSection::Link => cycle_link_setting(ctx, false), EngineSection::Link => cycle_link_setting(ctx, false),
EngineSection::MidiOutput => cycle_midi_output(ctx, false), EngineSection::MidiOutput => cycle_midi_output(ctx, false),
@@ -251,9 +281,16 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
_ => {} _ => {}
}, },
KeyCode::Right => match ctx.app.audio.section { KeyCode::Right => match ctx.app.audio.section {
EngineSection::Devices if !ctx.app.plugin_mode => { EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.devices_focus {
ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Input)); DevicesFocus::Host => {
ctx.dispatch(AppCommand::CycleHost { right: true });
ctx.app.save_settings(ctx.link);
} }
DevicesFocus::Output => {
ctx.dispatch(AppCommand::SetDevicesFocus(DevicesFocus::Input));
}
DevicesFocus::Input => {}
},
EngineSection::Settings => cycle_engine_setting(ctx, true), EngineSection::Settings => cycle_engine_setting(ctx, true),
EngineSection::Link => cycle_link_setting(ctx, true), EngineSection::Link => cycle_link_setting(ctx, true),
EngineSection::MidiOutput => cycle_midi_output(ctx, true), EngineSection::MidiOutput => cycle_midi_output(ctx, true),

View File

@@ -126,6 +126,7 @@ fn main() -> io::Result<()> {
let new_audio_rx = sequencer.swap_audio_channel(); let new_audio_rx = sequencer.swap_audio_channel();
let new_config = AudioStreamConfig { let new_config = AudioStreamConfig {
host: app.audio.config.selected_host.clone(),
output_device: app.audio.config.output_device.clone(), output_device: app.audio.config.output_device.clone(),
input_device: app.audio.config.input_device.clone(), input_device: app.audio.config.input_device.clone(),
channels: app.audio.config.channels, channels: app.audio.config.channels,

View File

@@ -26,6 +26,8 @@ pub struct Settings {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct AudioSettings { pub struct AudioSettings {
#[serde(default)]
pub host: Option<String>,
pub output_device: Option<String>, pub output_device: Option<String>,
pub input_device: Option<String>, pub input_device: Option<String>,
pub channels: u16, pub channels: u16,
@@ -96,6 +98,7 @@ pub struct LinkSettings {
impl Default for AudioSettings { impl Default for AudioSettings {
fn default() -> Self { fn default() -> Self {
Self { Self {
host: None,
output_device: None, output_device: None,
input_device: None, input_device: None,
channels: 2, channels: 2,

View File

@@ -105,6 +105,7 @@ impl RefreshRate {
#[derive(Clone)] #[derive(Clone)]
pub struct AudioConfig { pub struct AudioConfig {
pub selected_host: Option<String>,
pub output_device: Option<String>, pub output_device: Option<String>,
pub input_device: Option<String>, pub input_device: Option<String>,
pub channels: u16, pub channels: u16,
@@ -133,6 +134,7 @@ pub struct AudioConfig {
impl Default for AudioConfig { impl Default for AudioConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
selected_host: None,
output_device: None, output_device: None,
input_device: None, input_device: None,
channels: 2, channels: 2,
@@ -234,7 +236,8 @@ impl CyclicEnum for LinkSetting {
} }
#[derive(Clone, Copy, PartialEq, Eq, Default)] #[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum DeviceKind { pub enum DevicesFocus {
Host,
#[default] #[default]
Output, Output,
Input, Input,
@@ -293,11 +296,13 @@ impl Default for Metrics {
pub struct AudioSettings { pub struct AudioSettings {
pub config: AudioConfig, pub config: AudioConfig,
pub section: EngineSection, pub section: EngineSection,
pub device_kind: DeviceKind, pub devices_focus: DevicesFocus,
pub setting_kind: SettingKind, pub setting_kind: SettingKind,
pub link_setting: LinkSetting, pub link_setting: LinkSetting,
pub midi_output_slot: usize, pub midi_output_slot: usize,
pub midi_input_slot: usize, pub midi_input_slot: usize,
pub available_hosts: Vec<doux::audio::AudioHostInfo>,
pub host_index: usize,
pub output_devices: Vec<AudioDeviceInfo>, pub output_devices: Vec<AudioDeviceInfo>,
pub input_devices: Vec<AudioDeviceInfo>, pub input_devices: Vec<AudioDeviceInfo>,
pub output_list: ListSelectState, pub output_list: ListSelectState,
@@ -310,16 +315,25 @@ pub struct AudioSettings {
impl Default for AudioSettings { impl Default for AudioSettings {
fn default() -> Self { fn default() -> Self {
let hosts = doux::audio::list_hosts();
let preferred = doux::audio::preferred_host();
let preferred_name = preferred.id().name().to_string();
let host_index = hosts
.iter()
.position(|h| h.name == preferred_name)
.unwrap_or(0);
Self { Self {
config: AudioConfig::default(), config: AudioConfig::default(),
section: EngineSection::default(), section: EngineSection::default(),
device_kind: DeviceKind::default(), devices_focus: DevicesFocus::default(),
setting_kind: SettingKind::default(), setting_kind: SettingKind::default(),
link_setting: LinkSetting::default(), link_setting: LinkSetting::default(),
midi_output_slot: 0, midi_output_slot: 0,
midi_input_slot: 0, midi_input_slot: 0,
output_devices: doux::audio::list_output_devices(), available_hosts: hosts,
input_devices: doux::audio::list_input_devices(), host_index,
output_devices: doux::audio::list_output_devices_for(&preferred),
input_devices: doux::audio::list_input_devices_for(&preferred),
output_list: ListSelectState { output_list: ListSelectState {
cursor: 0, cursor: 0,
scroll_offset: 0, scroll_offset: 0,
@@ -344,11 +358,13 @@ impl AudioSettings {
Self { Self {
config: AudioConfig::default(), config: AudioConfig::default(),
section: EngineSection::Settings, section: EngineSection::Settings,
device_kind: DeviceKind::default(), devices_focus: DevicesFocus::default(),
setting_kind: SettingKind::Polyphony, setting_kind: SettingKind::Polyphony,
link_setting: LinkSetting::default(), link_setting: LinkSetting::default(),
midi_output_slot: 0, midi_output_slot: 0,
midi_input_slot: 0, midi_input_slot: 0,
available_hosts: Vec::new(),
host_index: 0,
output_devices: Vec::new(), output_devices: Vec::new(),
input_devices: Vec::new(), input_devices: Vec::new(),
output_list: ListSelectState { output_list: ListSelectState {
@@ -370,8 +386,75 @@ impl AudioSettings {
} }
pub fn refresh_devices(&mut self) { pub fn refresh_devices(&mut self) {
self.output_devices = doux::audio::list_output_devices(); self.refresh_devices_for_host();
self.input_devices = doux::audio::list_input_devices(); }
pub fn refresh_devices_for_host(&mut self) {
let host_name = self
.available_hosts
.get(self.host_index)
.map(|h| h.name.clone());
let selection = match host_name {
Some(name) => doux::audio::HostSelection::Named(name.to_lowercase()),
None => doux::audio::HostSelection::Auto,
};
if let Ok(host) = doux::audio::get_host(selection) {
self.output_devices = doux::audio::list_output_devices_for(&host);
self.input_devices = doux::audio::list_input_devices_for(&host);
} else {
self.output_devices = Vec::new();
self.input_devices = Vec::new();
}
self.output_list.cursor = 0;
self.output_list.scroll_offset = 0;
self.input_list.cursor = 0;
self.input_list.scroll_offset = 0;
}
pub fn cycle_host(&mut self, right: bool) {
let available: Vec<usize> = self
.available_hosts
.iter()
.enumerate()
.filter(|(_, h)| h.available)
.map(|(i, _)| i)
.collect();
if available.len() <= 1 {
return;
}
let current_pos = available
.iter()
.position(|&i| i == self.host_index)
.unwrap_or(0);
let new_pos = if right {
(current_pos + 1) % available.len()
} else if current_pos == 0 {
available.len() - 1
} else {
current_pos - 1
};
self.host_index = available[new_pos];
let name = self.available_hosts[self.host_index].name.clone();
self.config.selected_host = Some(name);
self.config.output_device = None;
self.config.input_device = None;
self.refresh_devices_for_host();
}
pub fn host_controls_buffer(&self) -> bool {
let host_name = self
.available_hosts
.get(self.host_index)
.map(|h| h.name.clone());
let selection = match host_name {
Some(name) => doux::audio::HostSelection::Named(name.to_lowercase()),
None => doux::audio::HostSelection::Auto,
};
if let Ok(host) = doux::audio::get_host(selection) {
doux::audio::host_controls_buffer_size(&host)
} else {
false
}
} }
pub fn next_section(&mut self, plugin_mode: bool) { pub fn next_section(&mut self, plugin_mode: bool) {

View File

@@ -28,7 +28,7 @@ pub mod sample_browser;
pub mod undo; pub mod undo;
pub mod ui; pub mod ui;
pub use audio::{AudioSettings, DeviceKind, EngineSection, LinkSetting, MainLayout, Metrics, ScopeMode, SettingKind, SpectrumMode}; pub use audio::{AudioSettings, DevicesFocus, EngineSection, LinkSetting, MainLayout, Metrics, ScopeMode, SettingKind, SpectrumMode};
pub use color_scheme::ColorScheme; pub use color_scheme::ColorScheme;
pub use editor::{ pub use editor::{
CopiedStepData, CopiedSteps, EditorContext, EditorTarget, EuclideanField, PatternField, CopiedStepData, CopiedSteps, EditorContext, EditorTarget, EuclideanField, PatternField,

View File

@@ -8,7 +8,7 @@ use ratatui::Frame;
use crate::app::App; use crate::app::App;
use crate::engine::LinkState; use crate::engine::LinkState;
use crate::midi; use crate::midi;
use crate::state::{DeviceKind, EngineSection, LinkSetting, SettingKind}; use crate::state::{DevicesFocus, EngineSection, LinkSetting, SettingKind};
use crate::theme; use crate::theme;
use crate::widgets::{ use crate::widgets::{
render_scroll_indicators, render_section_header, IndicatorAlign, Scope, render_scroll_indicators, render_section_header, IndicatorAlign, Scope,
@@ -607,18 +607,46 @@ pub fn list_height(item_count: usize) -> u16 {
pub fn devices_section_height(app: &App) -> u16 { pub fn devices_section_height(app: &App) -> u16 {
let output_h = list_height(app.audio.output_devices.len()); let output_h = list_height(app.audio.output_devices.len());
let input_h = list_height(app.audio.input_devices.len()); let input_h = list_height(app.audio.input_devices.len());
3 + output_h.max(input_h) 4 + output_h.max(input_h)
} }
fn render_devices(frame: &mut Frame, app: &App, area: Rect) { fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get(); let theme = theme::get();
let section_focused = app.audio.section == EngineSection::Devices; let section_focused = app.audio.section == EngineSection::Devices;
let [header_area, content_area] = let [header_area, host_area, content_area] =
Layout::vertical([Constraint::Length(2), Constraint::Min(1)]).areas(area); Layout::vertical([Constraint::Length(2), Constraint::Length(1), Constraint::Min(1)])
.areas(area);
render_section_header(frame, "DEVICES", section_focused, header_area); render_section_header(frame, "DEVICES", section_focused, header_area);
// Host row
let host_focused = section_focused && app.audio.devices_focus == DevicesFocus::Host;
let host_name = app
.audio
.available_hosts
.get(app.audio.host_index)
.map(|h| h.name.as_str())
.unwrap_or("-");
let highlight = Style::new()
.fg(theme.engine.focused)
.add_modifier(Modifier::BOLD);
let normal = Style::new().fg(theme.engine.normal);
let label_style = Style::new().fg(theme.engine.label);
let host_line = Line::from(vec![
Span::styled(
if host_focused {
"> Driver "
} else {
" Driver "
},
label_style,
),
render_selector(host_name, host_focused, highlight, normal),
]);
frame.render_widget(Paragraph::new(host_line), host_area);
let [output_col, separator, input_col] = Layout::horizontal([ let [output_col, separator, input_col] = Layout::horizontal([
Constraint::Percentage(48), Constraint::Percentage(48),
Constraint::Length(3), Constraint::Length(3),
@@ -626,8 +654,8 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
]) ])
.areas(content_area); .areas(content_area);
let output_focused = section_focused && app.audio.device_kind == DeviceKind::Output; let output_focused = section_focused && app.audio.devices_focus == DevicesFocus::Output;
let input_focused = section_focused && app.audio.device_kind == DeviceKind::Input; let input_focused = section_focused && app.audio.devices_focus == DevicesFocus::Input;
render_device_column( render_device_column(
frame, frame,
@@ -758,8 +786,8 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
label_style, label_style,
), ),
render_selector( render_selector(
&if app.audio.config.host_name.to_lowercase().contains("jack") { &if app.audio.host_controls_buffer() {
"JACK managed".to_string() "Host managed".to_string()
} else { } else {
format!("{}", app.audio.config.buffer_size) format!("{}", app.audio.config.buffer_size)
}, },