diff --git a/backend_alsa.md b/backend_alsa.md index e11dc20..ded43ec 100644 --- a/backend_alsa.md +++ b/backend_alsa.md @@ -179,13 +179,18 @@ but are supported by very few devices. Therefore these are checked last. Please also see [Find valid playback and capture parameters](#find-valid-playback-and-capture-parameters). ### Linking volume control to device volume -It is possible to let CamillaDSP follow the a volume control of the capture device. +It is possible to let CamillaDSP link its volume and mute controls to controls on the capture device. This is mostly useful when capturing from the USB Audio Gadget, -which provides a control named `PCM Capture Volume` that is controlled by the USB host. +which provides a volume control named `PCM Capture Volume` +and a mute control called `PCM Capture Switch` that are controlled by the USB host. -This does not alter the signal, and can be used to forward the volume setting from a player to CamillaDSP. -To enable this, set the `follow_volume_control` setting to the name of the volume control. -Any change of the volume then gets applied to the CamillaDSP main volume control. +This volume control does not alter the signal, +and can be used to forward the volume setting from a player to CamillaDSP. +To enable this, set the `link_volume_control` setting to the name of the volume control. +The corresponding setting for the mute control is `link_mute_control`. +Any change of the volume or mute then gets applied to the CamillaDSP main volume control. +The link works in both directions, so that volume and mute changes requested +over the websocket interface also get sent to the USB host. The available controls for a device can be listed with `amixer`. List controls for card 1: @@ -198,9 +203,11 @@ List controls with values and more details: amixer -c 1 contents ``` -The chosen control should be one that does not affect the signal volume, +The chosen volume control should be one that does not affect the signal volume, otherwise the volume gets applied twice. -It must also have a scale in decibel like in this example: +It must also have a scale in decibel, and take a single value (`values=1`). + +Example: ``` numid=15,iface=MIXER,name='Master Playback Volume' ; type=INTEGER,access=rw---R--,values=1,min=0,max=87,step=0 @@ -208,6 +215,16 @@ numid=15,iface=MIXER,name='Master Playback Volume' | dBscale-min=-65.25dB,step=0.75dB,mute=0 ``` +The mute control shoule be a _switch_, meaning that is has states `on` and `off`, +where `on` is not muted and `off` is muted. +It must also take a single value (`values=1`). + +Example: +``` +numid=6,iface=MIXER,name='PCM Capture Switch' + ; type=BOOLEAN,access=rw------,values=1 + : values=on +``` ### Subscribe to Alsa control events The Alsa capture device subscribes to control events from the USB Gadget and Loopback devices. diff --git a/src/alsadevice.rs b/src/alsadevice.rs index f22d7a3..50c2c09 100644 --- a/src/alsadevice.rs +++ b/src/alsadevice.rs @@ -28,8 +28,9 @@ use crate::alsadevice_buffermanager::{ }; use crate::alsadevice_utils::{ find_elem, list_channels_as_text, list_device_names, list_formats_as_text, - list_samplerates_as_text, pick_preferred_format, process_events, state_desc, CaptureElements, - CaptureParams, CaptureResult, ElemData, FileDescriptors, PlaybackParams, + list_samplerates_as_text, pick_preferred_format, process_events, state_desc, + sync_linked_controls, CaptureElements, CaptureParams, CaptureResult, ElemData, FileDescriptors, + PlaybackParams, }; use crate::helpers::PIRateController; use crate::CommandMessage; @@ -37,7 +38,7 @@ use crate::PrcFmt; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; -use crate::{CaptureStatus, PlaybackStatus}; +use crate::{CaptureStatus, PlaybackStatus, ProcessingParameters}; lazy_static! { static ref ALSA_MUTEX: Mutex<()> = Mutex::new(()); @@ -67,7 +68,8 @@ pub struct AlsaCaptureDevice { pub stop_on_rate_change: bool, pub rate_measure_interval: f32, pub stop_on_inactive: bool, - pub follow_volume_control: Option, + pub link_volume_control: Option, + pub link_mute_control: Option, } struct CaptureChannels { @@ -227,7 +229,8 @@ fn capture_buffer( hctl: &Option, elems: &CaptureElements, status_channel: &crossbeam_channel::Sender, - params: &CaptureParams, + params: &mut CaptureParams, + processing_params: &Arc, ) -> Res { let capture_state = pcmdevice.state_raw(); if capture_state == alsa_sys::SND_PCM_STATE_XRUN as i32 { @@ -270,9 +273,10 @@ fn capture_buffer( return Ok(CaptureResult::Stalled); } if pollresult.ctl { - trace!("Got a control events"); + trace!("Got a control event"); if let Some(c) = ctl { - let event_result = process_events(c, elems, status_channel, params); + let event_result = + process_events(c, elems, status_channel, params, processing_params); match event_result { CaptureResult::Done => return Ok(event_result), CaptureResult::Stalled => debug!("Capture device is stalled"), @@ -699,9 +703,10 @@ fn drain_check_eos(audio: &mpsc::Receiver) -> Option fn capture_loop_bytes( channels: CaptureChannels, pcmdevice: &alsa::PCM, - params: CaptureParams, + mut params: CaptureParams, mut resampler: Option>>, buf_manager: &mut CaptureBufferManager, + processing_params: &Arc, ) { let io = pcmdevice.io_bytes(); let pcminfo = pcmdevice.info().unwrap(); @@ -727,6 +732,7 @@ fn capture_loop_bytes( if let Some(c) = &ctl { c.subscribe_events(true).unwrap(); } + if let Some(h) = &hctl { let ctl_fds = h.get().unwrap(); file_descriptors.fds.extend(ctl_fds.iter()); @@ -747,18 +753,36 @@ fn capture_loop_bytes( "Capture Pitch 1000000", ); - capture_elements.find_elements(h, device, subdevice, ¶ms.follow_volume_control); + capture_elements.find_elements( + h, + device, + subdevice, + ¶ms.link_volume_control, + ¶ms.link_mute_control, + ); if let Some(c) = &ctl { if let Some(ref vol_elem) = capture_elements.volume { let vol_db = vol_elem.read_volume_in_db(c); info!("Using initial volume from Alsa: {:?}", vol_db); if let Some(vol) = vol_db { + params.linked_volume_value = Some(vol); channels .status .send(StatusMessage::SetVolume(vol)) .unwrap_or_default(); } } + if let Some(ref mute_elem) = capture_elements.mute { + let active = mute_elem.read_as_bool(); + info!("Using initial active switch from Alsa: {:?}", active); + if let Some(active_val) = active { + params.linked_mute_value = Some(!active_val); + channels + .status + .send(StatusMessage::SetMute(!active_val)) + .unwrap_or_default(); + } + } } } if element_loopback.is_some() || element_uac2_gadget.is_some() { @@ -883,7 +907,8 @@ fn capture_loop_bytes( &hctl, &capture_elements, &channels.status, - ¶ms, + &mut params, + processing_params, ); match capture_res { Ok(CaptureResult::Normal) => { @@ -1012,6 +1037,7 @@ fn capture_loop_bytes( break; } } + sync_linked_controls(processing_params, &mut params, &mut capture_elements, &ctl); } if let Some(h) = thread_handle { match demote_current_thread_from_real_time(h) { @@ -1144,6 +1170,7 @@ impl CaptureDevice for AlsaCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + processing_params: Arc, ) -> Res>> { let devname = self.devname.clone(); let samplerate = self.samplerate; @@ -1159,7 +1186,8 @@ impl CaptureDevice for AlsaCaptureDevice { let stop_on_rate_change = self.stop_on_rate_change; let rate_measure_interval = self.rate_measure_interval; let stop_on_inactive = self.stop_on_inactive; - let follow_volume_control = self.follow_volume_control.clone(); + let link_volume_control = self.link_volume_control.clone(); + let link_mute_control = self.link_mute_control.clone(); let mut buf_manager = CaptureBufferManager::new( chunksize as Frames, samplerate as f32 / capture_samplerate as f32, @@ -1206,7 +1234,10 @@ impl CaptureDevice for AlsaCaptureDevice { stop_on_rate_change, rate_measure_interval, stop_on_inactive, - follow_volume_control, + link_volume_control, + link_mute_control, + linked_mute_value: None, + linked_volume_value: None, }; let cap_channels = CaptureChannels { audio: channel, @@ -1219,6 +1250,7 @@ impl CaptureDevice for AlsaCaptureDevice { cap_params, resampler, &mut buf_manager, + &processing_params, ); } Err(err) => { diff --git a/src/alsadevice_utils.rs b/src/alsadevice_utils.rs index c21e203..e5331dc 100644 --- a/src/alsadevice_utils.rs +++ b/src/alsadevice_utils.rs @@ -11,6 +11,8 @@ use parking_lot::RwLock; use std::ffi::CString; use std::sync::Arc; +use crate::ProcessingParameters; + const STANDARD_RATES: [u32; 17] = [ 5512, 8000, 11025, 16000, 22050, 32000, 44100, 48000, 64000, 88200, 96000, 176400, 192000, 352800, 384000, 705600, 768000, @@ -37,7 +39,10 @@ pub struct CaptureParams { pub stop_on_rate_change: bool, pub rate_measure_interval: f32, pub stop_on_inactive: bool, - pub follow_volume_control: Option, + pub link_volume_control: Option, + pub link_mute_control: Option, + pub linked_volume_value: Option, + pub linked_mute_value: Option, } pub struct PlaybackParams { @@ -310,12 +315,30 @@ impl<'a> ElemData<'a> { }) } + pub fn write_volume_in_db(&self, ctl: &Ctl, value: f32) { + let intval = ctl.convert_from_db( + &self.element.get_id().unwrap(), + alsa::mixer::MilliBel::from_db(value), + alsa::Round::Floor, + ); + if let Ok(val) = intval { + self.write_as_int(val as i32); + } + } + pub fn write_as_int(&self, value: i32) { let mut elval = ElemValue::new(ElemType::Integer).unwrap(); if elval.set_integer(0, value).is_some() { self.element.write(&elval).unwrap_or_default(); } } + + pub fn write_as_bool(&self, value: bool) { + let mut elval = ElemValue::new(ElemType::Boolean).unwrap(); + if elval.set_boolean(0, value).is_some() { + self.element.write(&elval).unwrap_or_default(); + } + } } #[derive(Default)] @@ -326,6 +349,7 @@ pub struct CaptureElements<'a> { // pub loopback_channels: Option>, pub gadget_rate: Option>, pub volume: Option>, + pub mute: Option>, } pub struct FileDescriptors { @@ -374,7 +398,8 @@ pub fn process_events( ctl: &Ctl, elems: &CaptureElements, status_channel: &crossbeam_channel::Sender, - params: &CaptureParams, + params: &mut CaptureParams, + processing_params: &Arc, ) -> CaptureResult { while let Ok(Some(ev)) = ctl.read() { let nid = ev.get_id().get_numid(); @@ -401,9 +426,19 @@ pub fn process_events( } EventAction::SetVolume(vol) => { debug!("Alsa volume change event, set main fader to {} dB", vol); - status_channel - .send(StatusMessage::SetVolume(vol)) - .unwrap_or_default(); + processing_params.set_target_volume(0, vol); + params.linked_volume_value = Some(vol); + //status_channel + // .send(StatusMessage::SetVolume(vol)) + // .unwrap_or_default(); + } + EventAction::SetMute(mute) => { + debug!("Alsa mute change event, set mute state to {}", mute); + processing_params.set_mute(0, mute); + params.linked_mute_value = Some(mute); + //status_channel + // .send(StatusMessage::SetMute(mute)) + // .unwrap_or_default(); } EventAction::None => {} } @@ -414,6 +449,7 @@ pub fn process_events( pub enum EventAction { None, SetVolume(f32), + SetMute(bool), FormatChange(usize), SourceInactive, } @@ -422,7 +458,7 @@ pub fn get_event_action( numid: u32, elems: &CaptureElements, ctl: &Ctl, - params: &CaptureParams, + params: &mut CaptureParams, ) -> EventAction { if let Some(eldata) = &elems.loopback_active { if eldata.numid == numid { @@ -472,10 +508,21 @@ pub fn get_event_action( let vol_db = eldata.read_volume_in_db(ctl); debug!("Mixer volume control: {:?} dB", vol_db); if let Some(vol) = vol_db { + params.linked_volume_value = Some(vol); return EventAction::SetVolume(vol); } } } + if let Some(eldata) = &elems.mute { + if eldata.numid == numid { + let active = eldata.read_as_bool(); + debug!("Mixer switch active: {:?}", active); + if let Some(active_val) = active { + params.linked_mute_value = Some(!active_val); + return EventAction::SetMute(!active_val); + } + } + } if let Some(eldata) = &elems.gadget_rate { if eldata.numid == numid { let value = eldata.read_as_int(); @@ -503,6 +550,7 @@ impl<'a> CaptureElements<'a> { device: u32, subdevice: u32, volume_name: &Option, + mute_name: &Option, ) { self.loopback_active = find_elem( h, @@ -524,6 +572,9 @@ impl<'a> CaptureElements<'a> { self.volume = volume_name .as_ref() .and_then(|name| find_elem(h, ElemIface::Mixer, None, None, name)); + self.mute = mute_name + .as_ref() + .and_then(|name| find_elem(h, ElemIface::Mixer, None, None, name)); } } @@ -550,3 +601,31 @@ pub fn find_elem<'a>( ElemData { element: e, numid } }) } + +pub fn sync_linked_controls( + processing_params: &Arc, + capture_params: &mut CaptureParams, + elements: &mut CaptureElements, + ctl: &Option, +) { + if let Some(c) = ctl { + if let Some(vol) = capture_params.linked_volume_value { + let target_vol = processing_params.target_volume(0); + if (vol - target_vol).abs() > 0.1 { + info!("Updating linked volume control to {} dB", target_vol); + } + if let Some(vol_elem) = &elements.volume { + vol_elem.write_volume_in_db(c, target_vol); + } + } + if let Some(mute) = capture_params.linked_mute_value { + let target_mute = processing_params.is_mute(0); + if mute != target_mute { + info!("Updating linked switch control to {}", !target_mute); + if let Some(mute_elem) = &elements.mute { + mute_elem.write_as_bool(!target_mute); + } + } + } + } +} diff --git a/src/audiodevice.rs b/src/audiodevice.rs index 5ce01ca..da104ca 100644 --- a/src/audiodevice.rs +++ b/src/audiodevice.rs @@ -37,7 +37,7 @@ use crate::CommandMessage; use crate::PrcFmt; use crate::Res; use crate::StatusMessage; -use crate::{CaptureStatus, PlaybackStatus}; +use crate::{CaptureStatus, PlaybackStatus, ProcessingParameters}; pub const RATE_CHANGE_THRESHOLD_COUNT: usize = 3; pub const RATE_CHANGE_THRESHOLD_VALUE: f32 = 0.04; @@ -238,6 +238,7 @@ pub trait CaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + processing_params: Arc, ) -> Res>>; } @@ -555,7 +556,8 @@ pub fn new_capture_device(conf: config::Devices) -> Box { ref device, format, stop_on_inactive, - ref follow_volume_control, + ref link_volume_control, + ref link_mute_control, .. } => Box::new(alsadevice::AlsaCaptureDevice { devname: device.clone(), @@ -570,7 +572,8 @@ pub fn new_capture_device(conf: config::Devices) -> Box { stop_on_rate_change: conf.stop_on_rate_change(), rate_measure_interval: conf.rate_measure_interval(), stop_on_inactive: stop_on_inactive.unwrap_or_default(), - follow_volume_control: follow_volume_control.clone(), + link_volume_control: link_volume_control.clone(), + link_mute_control: link_mute_control.clone(), }), #[cfg(feature = "pulse-backend")] config::CaptureDevice::Pulse { diff --git a/src/bin.rs b/src/bin.rs index 98dc9b6..976d9dc 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -176,6 +176,7 @@ fn run( tx_status_cap, rx_command_cap, status_structs.capture.clone(), + status_structs.processing.clone(), ) .unwrap(); @@ -401,6 +402,10 @@ fn run( debug!("SetVolume message to {} dB received", vol); status_structs.processing.set_target_volume(0, vol); } + StatusMessage::SetMute(mute) => { + debug!("SetMute message to {} received", mute); + status_structs.processing.set_mute(0, mute); + } }, Err(err) => { warn!("Capture, Playback and Processing threads have exited: {}", err); diff --git a/src/config.rs b/src/config.rs index 677ed5c..961f363 100644 --- a/src/config.rs +++ b/src/config.rs @@ -197,7 +197,9 @@ pub enum CaptureDevice { #[serde(default)] stop_on_inactive: Option, #[serde(default)] - follow_volume_control: Option, + link_volume_control: Option, + #[serde(default)] + link_mute_control: Option, #[serde(default)] labels: Option>>, }, diff --git a/src/coreaudiodevice.rs b/src/coreaudiodevice.rs index 6e9a6ef..12ee021 100644 --- a/src/coreaudiodevice.rs +++ b/src/coreaudiodevice.rs @@ -38,6 +38,7 @@ use audio_thread_priority::{ use crate::CommandMessage; use crate::PrcFmt; +use crate::ProcessingParameters; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; @@ -652,6 +653,7 @@ impl CaptureDevice for CoreaudioCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + _processing_params: Arc, ) -> Res>> { let devname = self.devname.clone(); let samplerate = self.samplerate; diff --git a/src/cpaldevice.rs b/src/cpaldevice.rs index ea4c05c..b86e933 100644 --- a/src/cpaldevice.rs +++ b/src/cpaldevice.rs @@ -22,6 +22,7 @@ use std::time; use crate::CommandMessage; use crate::NewValue; use crate::PrcFmt; +use crate::ProcessingParameters; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; @@ -472,6 +473,7 @@ impl CaptureDevice for CpalCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + _processing_params: Arc, ) -> Res>> { let host_cfg = self.host.clone(); let devname = self.devname.clone(); diff --git a/src/filedevice.rs b/src/filedevice.rs index 4c29e9d..66c868a 100644 --- a/src/filedevice.rs +++ b/src/filedevice.rs @@ -31,7 +31,7 @@ use crate::PrcFmt; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; -use crate::{CaptureStatus, PlaybackStatus}; +use crate::{CaptureStatus, PlaybackStatus, ProcessingParameters}; pub struct FilePlaybackDevice { pub destination: PlaybackDest, @@ -538,6 +538,7 @@ impl CaptureDevice for FileCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + _processing_params: Arc, ) -> Res>> { let source = self.source.clone(); let samplerate = self.samplerate; diff --git a/src/generatordevice.rs b/src/generatordevice.rs index 6a58d28..a2b3cd9 100644 --- a/src/generatordevice.rs +++ b/src/generatordevice.rs @@ -14,6 +14,7 @@ use rand_distr::{Distribution, Uniform}; use crate::CaptureStatus; use crate::CommandMessage; use crate::PrcFmt; +use crate::ProcessingParameters; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; @@ -203,6 +204,7 @@ impl CaptureDevice for GeneratorDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + _processing_params: Arc, ) -> Res>> { let samplerate = self.samplerate; let chunksize = self.chunksize; diff --git a/src/lib.rs b/src/lib.rs index f5ee2d5..5ec12c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -156,6 +156,7 @@ pub enum StatusMessage { CaptureDone, SetSpeed(f64), SetVolume(f32), + SetMute(bool), } pub enum CommandMessage { diff --git a/src/pulsedevice.rs b/src/pulsedevice.rs index 78c37b4..ab06f80 100644 --- a/src/pulsedevice.rs +++ b/src/pulsedevice.rs @@ -17,6 +17,7 @@ use std::time::{Duration, Instant}; use crate::CommandMessage; use crate::PrcFmt; +use crate::ProcessingParameters; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; @@ -261,6 +262,7 @@ impl CaptureDevice for PulseCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + _processing_params: Arc, ) -> Res>> { let devname = self.devname.clone(); let samplerate = self.samplerate; diff --git a/src/wasapidevice.rs b/src/wasapidevice.rs index f0c4c44..c1bc24e 100644 --- a/src/wasapidevice.rs +++ b/src/wasapidevice.rs @@ -23,6 +23,7 @@ use audio_thread_priority::{ use crate::CommandMessage; use crate::PrcFmt; +use crate::ProcessingParameters; use crate::ProcessingState; use crate::Res; use crate::StatusMessage; @@ -933,6 +934,7 @@ impl CaptureDevice for WasapiCaptureDevice { status_channel: crossbeam_channel::Sender, command_channel: mpsc::Receiver, capture_status: Arc>, + _processing_params: Arc, ) -> Res>> { let exclusive = self.exclusive; let loopback = self.loopback;