diff --git a/i18n/en/camera.ftl b/i18n/en/camera.ftl index 6725bed..e191f2d 100644 --- a/i18n/en/camera.ftl +++ b/i18n/en/camera.ftl @@ -34,6 +34,8 @@ settings-video = Video settings-device = Device settings-format = Format settings-microphone = Microphone +settings-record-audio = Record audio +settings-audio-encoder = Audio encoder settings-encoder = Encoder settings-quality = Quality settings-video-encoder = Video encoder diff --git a/src/app/handlers/capture.rs b/src/app/handlers/capture.rs index a9769e4..d3a64ae 100644 --- a/src/app/handlers/capture.rs +++ b/src/app/handlers/capture.rs @@ -732,10 +732,14 @@ impl AppModel { let framerate = format.framerate.unwrap_or(30); let pixel_format = format.pixel_format.clone(); - let audio_device = self - .available_audio_devices - .get(self.current_audio_device_index) - .map(|dev| format!("pipewire-serial-{}", dev.serial)); + // Only get audio device if audio recording is enabled in settings + let audio_device = if self.config.record_audio { + self.available_audio_devices + .get(self.current_audio_device_index) + .map(|dev| format!("pipewire-serial-{}", dev.serial)) + } else { + None + }; let selected_encoder = self .available_video_encoders diff --git a/src/app/handlers/system.rs b/src/app/handlers/system.rs index 7d9a1c5..d6ed5ae 100644 --- a/src/app/handlers/system.rs +++ b/src/app/handlers/system.rs @@ -155,6 +155,44 @@ impl AppModel { Task::none() } + pub(crate) fn handle_toggle_record_audio(&mut self) -> Task> { + use cosmic::cosmic_config::CosmicConfigEntry; + + self.config.record_audio = !self.config.record_audio; + info!( + record_audio = self.config.record_audio, + "Toggled record audio" + ); + + if let Some(handler) = self.config_handler.as_ref() { + if let Err(err) = self.config.write_entry(handler) { + error!(?err, "Failed to save record audio setting"); + } + } + Task::none() + } + + pub(crate) fn handle_select_audio_encoder( + &mut self, + index: usize, + ) -> Task> { + use crate::config::AudioEncoder; + use cosmic::cosmic_config::CosmicConfigEntry; + + if index < AudioEncoder::ALL.len() { + let encoder = AudioEncoder::ALL[index]; + info!(?encoder, "Selected audio encoder"); + self.config.audio_encoder = encoder; + + if let Some(handler) = self.config_handler.as_ref() { + if let Err(err) = self.config.write_entry(handler) { + error!(?err, "Failed to save audio encoder selection"); + } + } + } + Task::none() + } + pub(crate) fn handle_toggle_save_burst_raw(&mut self) -> Task> { self.config.save_burst_raw = !self.config.save_burst_raw; info!( diff --git a/src/app/mod.rs b/src/app/mod.rs index 69aa268..d90d014 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -374,6 +374,10 @@ impl cosmic::Application for AppModel { .iter() .map(|f| f.display_name().to_string()) .collect(), + audio_encoder_dropdown_options: crate::config::AudioEncoder::ALL + .iter() + .map(|e| e.display_name().to_string()) + .collect(), device_info_visible: false, transition_state: crate::app::state::TransitionState::default(), // QR detection enabled by default diff --git a/src/app/settings/view.rs b/src/app/settings/view.rs index 8fe9622..7bf5b7d 100644 --- a/src/app/settings/view.rs +++ b/src/app/settings/view.rs @@ -3,7 +3,7 @@ //! Settings drawer view use crate::app::state::{AppModel, Message}; -use crate::config::{AppTheme, PhotoOutputFormat}; +use crate::config::{AppTheme, AudioEncoder, PhotoOutputFormat}; use crate::constants::BitratePreset; use crate::fl; use cosmic::Element; @@ -91,8 +91,14 @@ impl AppModel { )), ); + // Audio encoder index + let current_audio_encoder_index = AudioEncoder::ALL + .iter() + .position(|e| *e == self.config.audio_encoder) + .unwrap_or(0); // Default to Opus (index 0) + // Video section - let video_section = widget::settings::section() + let mut video_section = widget::settings::section() .title(fl!("settings-video")) .add( widget::settings::item::builder(fl!("settings-encoder")).control(widget::dropdown( @@ -109,15 +115,33 @@ impl AppModel { )), ) .add( - widget::settings::item::builder(fl!("settings-microphone")).control( - widget::dropdown( - &self.audio_dropdown_options, - Some(self.current_audio_device_index), - Message::SelectAudioDevice, - ), - ), + widget::settings::item::builder(fl!("settings-record-audio")) + .toggler(self.config.record_audio, |_| Message::ToggleRecordAudio), ); + // Only show audio encoder and microphone selection when audio is enabled + if self.config.record_audio { + video_section = video_section + .add( + widget::settings::item::builder(fl!("settings-audio-encoder")).control( + widget::dropdown( + &self.audio_encoder_dropdown_options, + Some(current_audio_encoder_index), + Message::SelectAudioEncoder, + ), + ), + ) + .add( + widget::settings::item::builder(fl!("settings-microphone")).control( + widget::dropdown( + &self.audio_dropdown_options, + Some(self.current_audio_device_index), + Message::SelectAudioDevice, + ), + ), + ); + } + // Photo section (output format and HDR+ settings) use crate::config::BurstModeSetting; // Index 0 = Off, 1 = Auto, 2 = 4 frames, 3 = 6 frames, 4 = 8 frames, 5 = 50 frames diff --git a/src/app/state.rs b/src/app/state.rs index 1a4bd19..d9f2edc 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -640,6 +640,8 @@ pub struct AppModel { pub burst_mode_frame_count_dropdown_options: Vec, /// Photo output format dropdown options (JPEG, PNG, DNG) pub photo_output_format_dropdown_options: Vec, + /// Audio encoder dropdown options (Opus, AAC) + pub audio_encoder_dropdown_options: Vec, /// Whether the device info panel is visible pub device_info_visible: bool, @@ -1218,6 +1220,10 @@ pub enum Message { SelectVideoEncoder(usize), /// Select photo output format (JPEG, PNG, DNG) SelectPhotoOutputFormat(usize), + /// Toggle recording audio with video + ToggleRecordAudio, + /// Select audio encoder (Opus, AAC) + SelectAudioEncoder(usize), /// Toggle saving raw burst frames as DNG (debugging feature) ToggleSaveBurstRaw, /// Toggle virtual camera feature enabled diff --git a/src/app/update.rs b/src/app/update.rs index 3f965ac..a44f339 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -228,6 +228,8 @@ impl AppModel { Message::SelectPhotoOutputFormat(index) => { self.handle_select_photo_output_format(index) } + Message::ToggleRecordAudio => self.handle_toggle_record_audio(), + Message::SelectAudioEncoder(index) => self.handle_select_audio_encoder(index), Message::ToggleSaveBurstRaw => self.handle_toggle_save_burst_raw(), // ===== System & Recovery ===== diff --git a/src/config.rs b/src/config.rs index d8f80a7..d2874a8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -92,6 +92,29 @@ impl BurstModeSetting { ]; } +/// Audio encoder preference +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub enum AudioEncoder { + /// Opus codec (preferred - best quality) + #[default] + Opus, + /// AAC codec (fallback - good compatibility) + AAC, +} + +impl AudioEncoder { + /// Get display name for this encoder + pub fn display_name(&self) -> &'static str { + match self { + AudioEncoder::Opus => "Opus", + AudioEncoder::AAC => "AAC", + } + } + + /// Get all available encoders + pub const ALL: [AudioEncoder; 2] = [AudioEncoder::Opus, AudioEncoder::AAC]; +} + /// Application theme preference #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub enum AppTheme { @@ -140,7 +163,7 @@ pub struct FormatSettings { pub type VideoSettings = FormatSettings; #[derive(Debug, Clone, CosmicConfigEntry, Eq, PartialEq, Serialize, Deserialize)] -#[version = 11] +#[version = 12] pub struct Config { /// Application theme preference (System, Dark, Light) pub app_theme: AppTheme, @@ -170,6 +193,10 @@ pub struct Config { pub save_burst_raw: bool, /// Burst mode setting (Off, Auto, or fixed frame count) pub burst_mode_setting: BurstModeSetting, + /// Record audio with video + pub record_audio: bool, + /// Audio encoder preference (Opus or AAC) + pub audio_encoder: AudioEncoder, } impl Default for Config { @@ -191,6 +218,8 @@ impl Default for Config { photo_output_format: PhotoOutputFormat::default(), // Default to JPEG save_burst_raw: false, // Disabled by default (debugging feature) burst_mode_setting: BurstModeSetting::default(), // Default to Auto + record_audio: true, // Enable audio recording by default + audio_encoder: AudioEncoder::default(), // Default to Opus } } }