Skip to content

Commit

Permalink
Merge pull request #365 from HEnquist/sync_gadget_ctrls
Browse files Browse the repository at this point in the history
Sync gadget ctrls
  • Loading branch information
HEnquist authored Sep 26, 2024
2 parents cb4d31a + 4758efa commit 42e5c67
Show file tree
Hide file tree
Showing 12 changed files with 141 additions and 41 deletions.
31 changes: 24 additions & 7 deletions backend_alsa.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -198,16 +203,28 @@ 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
: values=52
| 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.
Expand Down
45 changes: 29 additions & 16 deletions src/alsadevice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,17 @@ 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;
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(());
Expand Down Expand Up @@ -67,8 +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<String>,
pub follow_mute_control: Option<String>,
pub link_volume_control: Option<String>,
pub link_mute_control: Option<String>,
}

struct CaptureChannels {
Expand Down Expand Up @@ -228,7 +229,8 @@ fn capture_buffer(
hctl: &Option<HCtl>,
elems: &CaptureElements,
status_channel: &crossbeam_channel::Sender<StatusMessage>,
params: &CaptureParams,
params: &mut CaptureParams,
processing_params: &Arc<ProcessingParameters>,
) -> Res<CaptureResult> {
let capture_state = pcmdevice.state_raw();
if capture_state == alsa_sys::SND_PCM_STATE_XRUN as i32 {
Expand Down Expand Up @@ -271,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"),
Expand Down Expand Up @@ -700,9 +703,10 @@ fn drain_check_eos(audio: &mpsc::Receiver<AudioMessage>) -> Option<AudioMessage>
fn capture_loop_bytes(
channels: CaptureChannels,
pcmdevice: &alsa::PCM,
params: CaptureParams,
mut params: CaptureParams,
mut resampler: Option<Box<dyn VecResampler<PrcFmt>>>,
buf_manager: &mut CaptureBufferManager,
processing_params: &Arc<ProcessingParameters>,
) {
let io = pcmdevice.io_bytes();
let pcminfo = pcmdevice.info().unwrap();
Expand All @@ -728,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());
Expand All @@ -752,14 +757,15 @@ fn capture_loop_bytes(
h,
device,
subdevice,
&params.follow_volume_control,
&params.follow_mute_control,
&params.link_volume_control,
&params.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))
Expand All @@ -770,6 +776,7 @@ fn capture_loop_bytes(
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))
Expand Down Expand Up @@ -900,7 +907,8 @@ fn capture_loop_bytes(
&hctl,
&capture_elements,
&channels.status,
&params,
&mut params,
processing_params,
);
match capture_res {
Ok(CaptureResult::Normal) => {
Expand Down Expand Up @@ -1029,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) {
Expand Down Expand Up @@ -1161,6 +1170,7 @@ impl CaptureDevice for AlsaCaptureDevice {
status_channel: crossbeam_channel::Sender<StatusMessage>,
command_channel: mpsc::Receiver<CommandMessage>,
capture_status: Arc<RwLock<CaptureStatus>>,
processing_params: Arc<ProcessingParameters>,
) -> Res<Box<thread::JoinHandle<()>>> {
let devname = self.devname.clone();
let samplerate = self.samplerate;
Expand All @@ -1176,8 +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 follow_mute_control = self.follow_mute_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,
Expand Down Expand Up @@ -1224,8 +1234,10 @@ impl CaptureDevice for AlsaCaptureDevice {
stop_on_rate_change,
rate_measure_interval,
stop_on_inactive,
follow_volume_control,
follow_mute_control,
link_volume_control,
link_mute_control,
linked_mute_value: None,
linked_volume_value: None,
};
let cap_channels = CaptureChannels {
audio: channel,
Expand All @@ -1238,6 +1250,7 @@ impl CaptureDevice for AlsaCaptureDevice {
cap_params,
resampler,
&mut buf_manager,
&processing_params,
);
}
Err(err) => {
Expand Down
77 changes: 67 additions & 10 deletions src/alsadevice_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -37,8 +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<String>,
pub follow_mute_control: Option<String>,
pub link_volume_control: Option<String>,
pub link_mute_control: Option<String>,
pub linked_volume_value: Option<f32>,
pub linked_mute_value: Option<bool>,
}

pub struct PlaybackParams {
Expand Down Expand Up @@ -311,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)]
Expand Down Expand Up @@ -376,7 +398,8 @@ pub fn process_events(
ctl: &Ctl,
elems: &CaptureElements,
status_channel: &crossbeam_channel::Sender<StatusMessage>,
params: &CaptureParams,
params: &mut CaptureParams,
processing_params: &Arc<ProcessingParameters>,
) -> CaptureResult {
while let Ok(Some(ev)) = ctl.read() {
let nid = ev.get_id().get_numid();
Expand All @@ -403,15 +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);
status_channel
.send(StatusMessage::SetMute(mute))
.unwrap_or_default();
processing_params.set_mute(0, mute);
params.linked_mute_value = Some(mute);
//status_channel
// .send(StatusMessage::SetMute(mute))
// .unwrap_or_default();
}
EventAction::None => {}
}
Expand All @@ -431,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 {
Expand Down Expand Up @@ -481,6 +508,7 @@ 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);
}
}
Expand All @@ -490,6 +518,7 @@ pub fn get_event_action(
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);
}
}
Expand Down Expand Up @@ -572,3 +601,31 @@ pub fn find_elem<'a>(
ElemData { element: e, numid }
})
}

pub fn sync_linked_controls(
processing_params: &Arc<ProcessingParameters>,
capture_params: &mut CaptureParams,
elements: &mut CaptureElements,
ctl: &Option<Ctl>,
) {
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);
}
}
}
}
}
11 changes: 6 additions & 5 deletions src/audiodevice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -238,6 +238,7 @@ pub trait CaptureDevice {
status_channel: crossbeam_channel::Sender<StatusMessage>,
command_channel: mpsc::Receiver<CommandMessage>,
capture_status: Arc<RwLock<CaptureStatus>>,
processing_params: Arc<ProcessingParameters>,
) -> Res<Box<thread::JoinHandle<()>>>;
}

Expand Down Expand Up @@ -555,8 +556,8 @@ pub fn new_capture_device(conf: config::Devices) -> Box<dyn CaptureDevice> {
ref device,
format,
stop_on_inactive,
ref follow_volume_control,
ref follow_mute_control,
ref link_volume_control,
ref link_mute_control,
..
} => Box::new(alsadevice::AlsaCaptureDevice {
devname: device.clone(),
Expand All @@ -571,8 +572,8 @@ pub fn new_capture_device(conf: config::Devices) -> Box<dyn CaptureDevice> {
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(),
follow_mute_control: follow_mute_control.clone(),
link_volume_control: link_volume_control.clone(),
link_mute_control: link_mute_control.clone(),
}),
#[cfg(feature = "pulse-backend")]
config::CaptureDevice::Pulse {
Expand Down
Loading

0 comments on commit 42e5c67

Please sign in to comment.