mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2026-06-13 14:33:03 -04:00
spa: alsa: Add a mechanism for external volume control
Currently enabled at device creation and delegated to an external entity via spa_device events.
This commit is contained in:
parent
c81c66826e
commit
eb9a751257
9 changed files with 614 additions and 85 deletions
|
|
@ -19,6 +19,7 @@ extern "C" {
|
||||||
/* object id of SPA_TYPE_EVENT_Device */
|
/* object id of SPA_TYPE_EVENT_Device */
|
||||||
enum spa_device_event {
|
enum spa_device_event {
|
||||||
SPA_DEVICE_EVENT_ObjectConfig,
|
SPA_DEVICE_EVENT_ObjectConfig,
|
||||||
|
SPA_DEVICE_EVENT_ExtVolumeControl,
|
||||||
};
|
};
|
||||||
|
|
||||||
#define SPA_DEVICE_EVENT_ID(ev) SPA_EVENT_ID(ev, SPA_TYPE_EVENT_Device)
|
#define SPA_DEVICE_EVENT_ID(ev) SPA_EVENT_ID(ev, SPA_TYPE_EVENT_Device)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ extern "C" {
|
||||||
|
|
||||||
static const struct spa_type_info spa_type_device_event_id[] = {
|
static const struct spa_type_info spa_type_device_event_id[] = {
|
||||||
{ SPA_DEVICE_EVENT_ObjectConfig, SPA_TYPE_EVENT_Device, SPA_TYPE_INFO_DEVICE_EVENT_ID_BASE "ObjectConfig", NULL },
|
{ SPA_DEVICE_EVENT_ObjectConfig, SPA_TYPE_EVENT_Device, SPA_TYPE_INFO_DEVICE_EVENT_ID_BASE "ObjectConfig", NULL },
|
||||||
|
{ SPA_DEVICE_EVENT_ExtVolumeControl, SPA_TYPE_EVENT_Device, SPA_TYPE_INFO_DEVICE_EVENT_ID_BASE "ExtVolumeControl", NULL },
|
||||||
{ 0, 0, NULL, NULL },
|
{ 0, 0, NULL, NULL },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "acp.h"
|
#include "acp.h"
|
||||||
#include "alsa-mixer.h"
|
#include "alsa-mixer.h"
|
||||||
#include "alsa-ucm.h"
|
#include "alsa-ucm.h"
|
||||||
|
#include "ext-volume.h"
|
||||||
|
|
||||||
#include <spa/utils/string.h>
|
#include <spa/utils/string.h>
|
||||||
#include <spa/utils/json.h>
|
#include <spa/utils/json.h>
|
||||||
|
|
@ -1246,6 +1247,28 @@ static void init_eld_ctls(pa_card *impl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void ext_volume_notifier_cb (void *data, struct spa_event *event) {
|
||||||
|
struct pa_card *impl = data;
|
||||||
|
|
||||||
|
if (impl->events && impl->events->ext_vol_event_available)
|
||||||
|
impl->events->ext_vol_event_available(impl->user_data, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void init_ext_volume(pa_card *impl)
|
||||||
|
{
|
||||||
|
struct acp_card *card = &impl->card;
|
||||||
|
int res;
|
||||||
|
|
||||||
|
if (!impl->ext_volume_ctrl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
res = spa_acp_ext_volume_init(&card->ext_volume, impl->name, ext_volume_notifier_cb, impl);
|
||||||
|
if (res < 0) {
|
||||||
|
pa_log_notice("failed to init ACP external volume: %s", snd_strerror(res));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *name)
|
uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *name)
|
||||||
{
|
{
|
||||||
uint32_t i;
|
uint32_t i;
|
||||||
|
|
@ -1357,21 +1380,50 @@ static int read_volume(pa_alsa_device *dev)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dev->mixer_handle)
|
if (impl->card.ext_volume.initialized &&
|
||||||
|
(impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME)) {
|
||||||
|
pa_cvolume ext_vol;
|
||||||
|
|
||||||
|
if (!dev->active_port)
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
/* Externally managed volume */
|
||||||
|
if ((res = spa_acp_ext_volume_read_volume(&impl->card.ext_volume, impl->name,
|
||||||
|
dev->active_port->name, dev->active_port->port.index, &ext_vol)) < 0) {
|
||||||
|
pa_log_error("Could not read volume: %s", snd_strerror(res));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FIXME: scale to the range from capabilities */
|
||||||
|
if (ext_vol.channels == 1) {
|
||||||
|
r.channels = dev->device.format.channels;
|
||||||
|
for (unsigned int i = 0; i < r.channels; i++)
|
||||||
|
r.values[i] = ext_vol.values[0];
|
||||||
|
} else if (ext_vol.channels == dev->device.format.channels) {
|
||||||
|
r = ext_vol;
|
||||||
|
} else {
|
||||||
|
pa_log_error("Mismatch channel count: device %u != volume %u",
|
||||||
|
dev->device.format.channels, ext_vol.channels);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else if (dev->mixer_handle) {
|
||||||
|
/* ALSA mixer for volume */
|
||||||
|
if (dev->mixer_path->has_volume_mute && dev->muted) {
|
||||||
|
/* Shift up by the base volume */
|
||||||
|
pa_sw_cvolume_divide_scalar(&r, &dev->hardware_volume, dev->base_volume);
|
||||||
|
pa_log_debug("Reading cached volume only.");
|
||||||
|
} else {
|
||||||
|
if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle,
|
||||||
|
&dev->mapping->channel_map, &r)) < 0)
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shift down by the base volume, so that 0dB becomes maximum volume */
|
||||||
|
pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume);
|
||||||
|
|
||||||
|
} else
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
if (dev->mixer_path->has_volume_mute && dev->muted) {
|
|
||||||
/* Shift up by the base volume */
|
|
||||||
pa_sw_cvolume_divide_scalar(&r, &dev->hardware_volume, dev->base_volume);
|
|
||||||
pa_log_debug("Reading cached volume only.");
|
|
||||||
} else {
|
|
||||||
if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle,
|
|
||||||
&dev->mapping->channel_map, &r)) < 0)
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Shift down by the base volume, so that 0dB becomes maximum volume */
|
|
||||||
pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume);
|
|
||||||
|
|
||||||
if (pa_cvolume_equal(&dev->hardware_volume, &r))
|
if (pa_cvolume_equal(&dev->hardware_volume, &r))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -1394,12 +1446,10 @@ static int read_volume(pa_alsa_device *dev)
|
||||||
|
|
||||||
static void set_volume(pa_alsa_device *dev, const pa_cvolume *v)
|
static void set_volume(pa_alsa_device *dev, const pa_cvolume *v)
|
||||||
{
|
{
|
||||||
|
pa_card *impl = dev->card;
|
||||||
pa_cvolume r;
|
pa_cvolume r;
|
||||||
bool write_to_hw;
|
bool write_to_hw;
|
||||||
|
|
||||||
if (v != &dev->real_volume)
|
|
||||||
dev->real_volume = *v;
|
|
||||||
|
|
||||||
if (dev->ucm_context) {
|
if (dev->ucm_context) {
|
||||||
if (!dev->active_port)
|
if (!dev->active_port)
|
||||||
return;
|
return;
|
||||||
|
|
@ -1408,52 +1458,100 @@ static void set_volume(pa_alsa_device *dev, const pa_cvolume *v)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dev->mixer_handle)
|
if (impl->card.ext_volume.initialized) {
|
||||||
return;
|
if (!dev->active_port)
|
||||||
|
return;
|
||||||
|
if (impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_VALUE) {
|
||||||
|
/* External volume control by value */
|
||||||
|
if (spa_acp_ext_volume_write_volume_absolute(&impl->card.ext_volume, impl->name,
|
||||||
|
dev->active_port->name, dev->active_port->port.index, &dev->real_volume) < 0) {
|
||||||
|
pa_log_error("Could not write volume");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* Shift up by the base volume */
|
/* Update volume if we were successful */
|
||||||
pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume);
|
if (v != &dev->real_volume)
|
||||||
|
dev->real_volume = *v;
|
||||||
|
} else if ((impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME) &&
|
||||||
|
(impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_UPDOWN)) {
|
||||||
|
/* External volume control by increment/decrement only */
|
||||||
|
pa_volume_t cur, new;
|
||||||
|
float step;
|
||||||
|
int i;
|
||||||
|
|
||||||
write_to_hw = !(dev->mixer_path->has_volume_mute && dev->muted);
|
cur = pa_cvolume_max(&dev->real_volume);
|
||||||
|
new = pa_cvolume_max(v);
|
||||||
|
|
||||||
if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map,
|
if (cur < new)
|
||||||
&r, false, write_to_hw) < 0)
|
step = 1;
|
||||||
return;
|
else if (cur > new)
|
||||||
|
step = -1;
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
|
||||||
/* Shift down by the base volume, so that 0dB becomes maximum volume */
|
if (spa_acp_ext_volume_write_volume_relative(&impl->card.ext_volume, impl->name,
|
||||||
pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume);
|
dev->active_port->name, dev->active_port->port.index, step) < 0) {
|
||||||
|
pa_log_error("Could not write volume");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dev->hardware_volume = r;
|
/* Update volume if we were successful */
|
||||||
|
dev->real_volume.channels = v->channels;
|
||||||
|
for (i = 0; i < (int)v->channels; i++)
|
||||||
|
dev->real_volume.values[i] = v->values[i] + (int)step;
|
||||||
|
} else {
|
||||||
|
pa_log_debug("Ignoring volume setting, ext volume control does not support it");
|
||||||
|
}
|
||||||
|
} else if (dev->mixer_handle) {
|
||||||
|
/* ALSA mixer control for volume */
|
||||||
|
|
||||||
if (dev->mixer_path->has_dB) {
|
if (v != &dev->real_volume)
|
||||||
pa_cvolume new_soft_volume;
|
dev->real_volume = *v;
|
||||||
bool accurate_enough;
|
|
||||||
|
|
||||||
/* Match exactly what the user requested by software */
|
/* Shift up by the base volume */
|
||||||
pa_sw_cvolume_divide(&new_soft_volume, &dev->real_volume, &dev->hardware_volume);
|
pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume);
|
||||||
|
|
||||||
/* If the adjustment to do in software is only minimal we
|
write_to_hw = !(dev->mixer_path->has_volume_mute && dev->muted);
|
||||||
* can skip it. That saves us CPU at the expense of a bit of
|
|
||||||
* accuracy */
|
|
||||||
accurate_enough =
|
|
||||||
(pa_cvolume_min(&new_soft_volume) >= (PA_VOLUME_NORM - VOLUME_ACCURACY)) &&
|
|
||||||
(pa_cvolume_max(&new_soft_volume) <= (PA_VOLUME_NORM + VOLUME_ACCURACY));
|
|
||||||
|
|
||||||
pa_log_debug("Requested volume: %d", pa_cvolume_max(&dev->real_volume));
|
if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map,
|
||||||
pa_log_debug("Got hardware volume: %d", pa_cvolume_max(&dev->hardware_volume));
|
&r, false, write_to_hw) < 0)
|
||||||
pa_log_debug("Calculated software volume: %d (accurate-enough=%s)",
|
return;
|
||||||
pa_cvolume_max(&new_soft_volume),
|
|
||||||
pa_yes_no(accurate_enough));
|
|
||||||
|
|
||||||
if (accurate_enough)
|
/* Shift down by the base volume, so that 0dB becomes maximum volume */
|
||||||
pa_cvolume_reset(&new_soft_volume, new_soft_volume.channels);
|
pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume);
|
||||||
|
|
||||||
dev->soft_volume = new_soft_volume;
|
dev->hardware_volume = r;
|
||||||
} else {
|
|
||||||
pa_log_debug("Wrote hardware volume: %d", pa_cvolume_max(&r));
|
if (dev->mixer_path->has_dB) {
|
||||||
/* We can't match exactly what the user requested, hence let's
|
pa_cvolume new_soft_volume;
|
||||||
* at least tell the user about it */
|
bool accurate_enough;
|
||||||
dev->real_volume = r;
|
|
||||||
|
/* Match exactly what the user requested by software */
|
||||||
|
pa_sw_cvolume_divide(&new_soft_volume, &dev->real_volume, &dev->hardware_volume);
|
||||||
|
|
||||||
|
/* If the adjustment to do in software is only minimal we
|
||||||
|
* can skip it. That saves us CPU at the expense of a bit of
|
||||||
|
* accuracy */
|
||||||
|
accurate_enough =
|
||||||
|
(pa_cvolume_min(&new_soft_volume) >= (PA_VOLUME_NORM - VOLUME_ACCURACY)) &&
|
||||||
|
(pa_cvolume_max(&new_soft_volume) <= (PA_VOLUME_NORM + VOLUME_ACCURACY));
|
||||||
|
|
||||||
|
pa_log_debug("Requested volume: %d", pa_cvolume_max(&dev->real_volume));
|
||||||
|
pa_log_debug("Got hardware volume: %d", pa_cvolume_max(&dev->hardware_volume));
|
||||||
|
pa_log_debug("Calculated software volume: %d (accurate-enough=%s)",
|
||||||
|
pa_cvolume_max(&new_soft_volume),
|
||||||
|
pa_yes_no(accurate_enough));
|
||||||
|
|
||||||
|
if (accurate_enough)
|
||||||
|
pa_cvolume_reset(&new_soft_volume, new_soft_volume.channels);
|
||||||
|
|
||||||
|
dev->soft_volume = new_soft_volume;
|
||||||
|
} else {
|
||||||
|
pa_log_debug("Wrote hardware volume: %d", pa_cvolume_max(&r));
|
||||||
|
/* We can't match exactly what the user requested, hence let's
|
||||||
|
* at least tell the user about it */
|
||||||
|
dev->real_volume = r;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1471,22 +1569,34 @@ static int read_mute(pa_alsa_device *dev)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dev->mixer_handle)
|
if (impl->card.ext_volume.initialized &&
|
||||||
|
(impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_MUTE)) {
|
||||||
|
if (!dev->active_port)
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
/* Externally managed mute state */
|
||||||
|
if (spa_acp_ext_volume_read_mute(&impl->card.ext_volume, impl->name,
|
||||||
|
dev->active_port->name, dev->active_port->port.index, &mute) < 0) {
|
||||||
|
pa_log_error("Could not read mute state");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else if (dev->mixer_handle) {
|
||||||
|
/* ALSA mixer for mute state */
|
||||||
|
if (dev->mixer_path->has_volume_mute) {
|
||||||
|
pa_cvolume mute_vol;
|
||||||
|
pa_cvolume r;
|
||||||
|
|
||||||
|
pa_cvolume_mute(&mute_vol, dev->mapping->channel_map.channels);
|
||||||
|
if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, &r)) < 0)
|
||||||
|
return res;
|
||||||
|
mute = pa_cvolume_equal(&mute_vol, &r);
|
||||||
|
} else {
|
||||||
|
if ((res = pa_alsa_path_get_mute(dev->mixer_path, dev->mixer_handle, &mute)) < 0)
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
} else
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
if (dev->mixer_path->has_volume_mute) {
|
|
||||||
pa_cvolume mute_vol;
|
|
||||||
pa_cvolume r;
|
|
||||||
|
|
||||||
pa_cvolume_mute(&mute_vol, dev->mapping->channel_map.channels);
|
|
||||||
if ((res = pa_alsa_path_get_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map, &r)) < 0)
|
|
||||||
return res;
|
|
||||||
mute = pa_cvolume_equal(&mute_vol, &r);
|
|
||||||
} else {
|
|
||||||
if ((res = pa_alsa_path_get_mute(dev->mixer_path, dev->mixer_handle, &mute)) < 0)
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mute == dev->muted)
|
if (mute == dev->muted)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
|
@ -1501,7 +1611,7 @@ static int read_mute(pa_alsa_device *dev)
|
||||||
|
|
||||||
static void set_mute(pa_alsa_device *dev, bool mute)
|
static void set_mute(pa_alsa_device *dev, bool mute)
|
||||||
{
|
{
|
||||||
dev->muted = mute;
|
pa_card *impl = dev->card;
|
||||||
|
|
||||||
if (dev->ucm_context) {
|
if (dev->ucm_context) {
|
||||||
if (!dev->active_port)
|
if (!dev->active_port)
|
||||||
|
|
@ -1511,27 +1621,54 @@ static void set_mute(pa_alsa_device *dev, bool mute)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dev->mixer_handle)
|
if (impl->card.ext_volume.initialized) {
|
||||||
return;
|
if (!dev->active_port)
|
||||||
|
return;
|
||||||
|
if ((impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_VALUE)) {
|
||||||
|
/* Externally managed mute state by value*/
|
||||||
|
if (spa_acp_ext_volume_write_mute_value(&impl->card.ext_volume, impl->name,
|
||||||
|
dev->active_port->name, dev->active_port->port.index, mute) < 0) {
|
||||||
|
pa_log_error("Could not write mute state");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (dev->mixer_path->has_volume_mute) {
|
dev->muted = mute;
|
||||||
pa_cvolume r;
|
} else if ((impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_MUTE) &&
|
||||||
|
(impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_TOGGLE)) {
|
||||||
|
/* Externally managed mute state toggle */
|
||||||
|
if (spa_acp_ext_volume_write_mute_toggle(&impl->card.ext_volume, impl->name,
|
||||||
|
dev->active_port->name, dev->active_port->port.index) < 0) {
|
||||||
|
pa_log_error("Could not write mute toggle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (mute) {
|
dev->muted = mute;
|
||||||
pa_cvolume_mute(&r, dev->mapping->channel_map.channels);
|
|
||||||
pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map,
|
|
||||||
&r, false, true);
|
|
||||||
} else {
|
} else {
|
||||||
/* Shift up by the base volume */
|
pa_log_debug("Ignoring mute setting, ext volume control does not support it");
|
||||||
pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume);
|
}
|
||||||
pa_log_debug("Restoring volume: %d", pa_cvolume_max(&dev->real_volume));
|
} else if (dev->mixer_handle) {
|
||||||
if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map,
|
/* ALSA mixer for mute state */
|
||||||
&r, false, true) < 0)
|
dev->muted = mute;
|
||||||
pa_log_error("Unable to restore volume %d during unmute",
|
|
||||||
pa_cvolume_max(&dev->real_volume));
|
if (dev->mixer_path->has_volume_mute) {
|
||||||
|
pa_cvolume r;
|
||||||
|
|
||||||
|
if (mute) {
|
||||||
|
pa_cvolume_mute(&r, dev->mapping->channel_map.channels);
|
||||||
|
pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map,
|
||||||
|
&r, false, true);
|
||||||
|
} else {
|
||||||
|
/* Shift up by the base volume */
|
||||||
|
pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume);
|
||||||
|
pa_log_debug("Restoring volume: %d", pa_cvolume_max(&dev->real_volume));
|
||||||
|
if (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map,
|
||||||
|
&r, false, true) < 0)
|
||||||
|
pa_log_error("Unable to restore volume %d during unmute",
|
||||||
|
pa_cvolume_max(&dev->real_volume));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pa_alsa_path_set_mute(dev->mixer_path, dev->mixer_handle, mute);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
pa_alsa_path_set_mute(dev->mixer_path, dev->mixer_handle, mute);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1539,7 +1676,15 @@ static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev)
|
||||||
{
|
{
|
||||||
pa_assert(dev);
|
pa_assert(dev);
|
||||||
|
|
||||||
if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_volume) {
|
if (impl->ext_volume_ctrl) {
|
||||||
|
dev->device.flags |= ACP_DEVICE_HW_VOLUME;
|
||||||
|
if (impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME)
|
||||||
|
dev->read_volume = read_volume;
|
||||||
|
if (impl->card.ext_volume.flags &
|
||||||
|
(SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_VALUE |
|
||||||
|
SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_UPDOWN))
|
||||||
|
dev->set_volume = set_volume;
|
||||||
|
} else if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_volume) {
|
||||||
dev->read_volume = NULL;
|
dev->read_volume = NULL;
|
||||||
dev->set_volume = NULL;
|
dev->set_volume = NULL;
|
||||||
pa_log_info("Driver does not support hardware volume control, "
|
pa_log_info("Driver does not support hardware volume control, "
|
||||||
|
|
@ -1587,7 +1732,15 @@ static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev)
|
||||||
dev->device.base_volume = (float)pa_sw_volume_to_linear(dev->base_volume);
|
dev->device.base_volume = (float)pa_sw_volume_to_linear(dev->base_volume);
|
||||||
dev->device.volume_step = 1.0f / dev->n_volume_steps;
|
dev->device.volume_step = 1.0f / dev->n_volume_steps;
|
||||||
|
|
||||||
if (impl->soft_mixer || !dev->mixer_path ||
|
if (impl->ext_volume_ctrl) {
|
||||||
|
dev->device.flags |= ACP_DEVICE_HW_MUTE;
|
||||||
|
if (impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_MUTE)
|
||||||
|
dev->read_mute = read_mute;
|
||||||
|
if (impl->card.ext_volume.flags &
|
||||||
|
(SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_VALUE |
|
||||||
|
SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_TOGGLE))
|
||||||
|
dev->set_mute = set_mute;
|
||||||
|
} else if (impl->soft_mixer || !dev->mixer_path ||
|
||||||
(!dev->mixer_path->has_mute && !dev->mixer_path->has_volume_mute)) {
|
(!dev->mixer_path->has_mute && !dev->mixer_path->has_volume_mute)) {
|
||||||
dev->read_mute = NULL;
|
dev->read_mute = NULL;
|
||||||
dev->set_mute = NULL;
|
dev->set_mute = NULL;
|
||||||
|
|
@ -1656,6 +1809,12 @@ static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (impl->ext_volume_ctrl) {
|
||||||
|
/* We've been told to use an external service to manage volume */
|
||||||
|
init_ext_volume(impl);
|
||||||
|
dev->device.ext_volume_flags = impl->card.ext_volume.flags;
|
||||||
|
}
|
||||||
|
|
||||||
mixer_volume_init(impl, dev);
|
mixer_volume_init(impl, dev);
|
||||||
|
|
||||||
/* Will we need to register callbacks? */
|
/* Will we need to register callbacks? */
|
||||||
|
|
@ -1975,6 +2134,8 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
|
||||||
impl->disable_pro_audio = spa_atob(s);
|
impl->disable_pro_audio = spa_atob(s);
|
||||||
if ((s = acp_dict_lookup(props, "api.acp.use-eld-channels")) != NULL)
|
if ((s = acp_dict_lookup(props, "api.acp.use-eld-channels")) != NULL)
|
||||||
impl->use_eld_channels = spa_atob(s);
|
impl->use_eld_channels = spa_atob(s);
|
||||||
|
if ((s = acp_dict_lookup(props, "api.alsa.external-volume-control")) != NULL)
|
||||||
|
impl->ext_volume_ctrl = spa_atob(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if SND_LIB_VERSION < 0x10207
|
#if SND_LIB_VERSION < 0x10207
|
||||||
|
|
@ -2360,6 +2521,7 @@ int acp_device_set_mute(struct acp_device *dev, bool mute)
|
||||||
|
|
||||||
if (d->set_mute) {
|
if (d->set_mute) {
|
||||||
d->set_mute(d, mute);
|
d->set_mute(d, mute);
|
||||||
|
mute = d->muted;
|
||||||
} else {
|
} else {
|
||||||
d->muted = mute;
|
d->muted = mute;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <spa/param/audio/volume.h>
|
||||||
|
|
||||||
|
#include "ext-volume.h"
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#else
|
#else
|
||||||
|
|
@ -188,6 +192,8 @@ struct acp_card_events {
|
||||||
|
|
||||||
void (*volume_changed) (void *data, struct acp_device *dev);
|
void (*volume_changed) (void *data, struct acp_device *dev);
|
||||||
void (*mute_changed) (void *data, struct acp_device *dev);
|
void (*mute_changed) (void *data, struct acp_device *dev);
|
||||||
|
|
||||||
|
void (*ext_vol_event_available) (void *data, struct spa_event *event);
|
||||||
};
|
};
|
||||||
|
|
||||||
struct acp_port {
|
struct acp_port {
|
||||||
|
|
@ -239,6 +245,9 @@ struct acp_device {
|
||||||
int64_t latency_ns;
|
int64_t latency_ns;
|
||||||
uint32_t codecs[32];
|
uint32_t codecs[32];
|
||||||
uint32_t n_codecs;
|
uint32_t n_codecs;
|
||||||
|
|
||||||
|
uint32_t ext_volume_flags;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct acp_card_profile {
|
struct acp_card_profile {
|
||||||
|
|
@ -277,6 +286,8 @@ struct acp_card {
|
||||||
struct acp_port **ports;
|
struct acp_port **ports;
|
||||||
uint32_t preferred_input_port_index;
|
uint32_t preferred_input_port_index;
|
||||||
uint32_t preferred_output_port_index;
|
uint32_t preferred_output_port_index;
|
||||||
|
|
||||||
|
struct spa_acp_ext_volume ext_volume;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props);
|
struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props);
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,8 @@ struct pa_card {
|
||||||
|
|
||||||
const struct acp_card_events *events;
|
const struct acp_card_events *events;
|
||||||
void *user_data;
|
void *user_data;
|
||||||
|
|
||||||
|
bool ext_volume_ctrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool pa_alsa_device_init_description(pa_proplist *p, pa_card *card);
|
bool pa_alsa_device_init_description(pa_proplist *p, pa_card *card);
|
||||||
|
|
|
||||||
281
spa/plugins/alsa/acp/ext-volume.c
Normal file
281
spa/plugins/alsa/acp/ext-volume.c
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
/* External Volume Control */
|
||||||
|
/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */
|
||||||
|
/* SPDX-FileCopyrightText: Copyright © 2026 Julian Bouzas */
|
||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
|
||||||
|
#include <spa/utils/cleanup.h>
|
||||||
|
#include <spa/utils/result.h>
|
||||||
|
#include <spa/pod/builder.h>
|
||||||
|
#include <spa/monitor/event.h>
|
||||||
|
#include <spa/param/props.h>
|
||||||
|
|
||||||
|
#include "alsa-mixer.h"
|
||||||
|
#include "channelmap.h"
|
||||||
|
#include "volume.h"
|
||||||
|
#include "ext-volume.h"
|
||||||
|
|
||||||
|
int
|
||||||
|
spa_acp_ext_volume_init(struct spa_acp_ext_volume *ext_volume, const char *device,
|
||||||
|
ext_volume_notifier_t notifier_cb, void *notifier_data)
|
||||||
|
{
|
||||||
|
struct spa_event *ev;
|
||||||
|
uint8_t buffer[4096];
|
||||||
|
struct spa_pod_builder b = { 0 };
|
||||||
|
struct spa_pod_frame f[3];
|
||||||
|
|
||||||
|
ext_volume->initialized = false;
|
||||||
|
ext_volume->flags = 0;
|
||||||
|
|
||||||
|
ext_volume->notifier_cb = notifier_cb;
|
||||||
|
ext_volume->notifier_data = notifier_data;
|
||||||
|
|
||||||
|
/* Create event */
|
||||||
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
||||||
|
spa_pod_builder_push_object(&b, &f[0],
|
||||||
|
SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl);
|
||||||
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
|
||||||
|
spa_pod_builder_push_object(&b, &f[1],
|
||||||
|
SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props);
|
||||||
|
spa_pod_builder_prop(&b, SPA_PROP_params, 0);
|
||||||
|
spa_pod_builder_push_struct(&b, &f[2]);
|
||||||
|
spa_pod_builder_string(&b, "GetCapabilities");
|
||||||
|
spa_pod_builder_string(&b, device ? device : "");
|
||||||
|
spa_pod_builder_pop(&b, &f[2]);
|
||||||
|
spa_pod_builder_pop(&b, &f[1]);
|
||||||
|
ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]);
|
||||||
|
|
||||||
|
pa_log_info("sending 'GetCapabilities' ext-vol-control event to %s", device);
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
ext_volume->notifier_cb (ext_volume->notifier_data, ev);
|
||||||
|
|
||||||
|
pa_log_info("read caps after emitting event: caps:%d", ext_volume->read_caps);
|
||||||
|
|
||||||
|
ext_volume->flags = ext_volume->read_caps;
|
||||||
|
ext_volume->initialized = true;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
spa_acp_ext_volume_read_volume(struct spa_acp_ext_volume *ext_volume,
|
||||||
|
const char *device, const char *port_name, uint32_t port_index, pa_cvolume *cvol)
|
||||||
|
{
|
||||||
|
struct spa_event *ev;
|
||||||
|
uint8_t buffer[4096];
|
||||||
|
struct spa_pod_builder b = { 0 };
|
||||||
|
struct spa_pod_frame f[3];
|
||||||
|
|
||||||
|
/* Create event */
|
||||||
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
||||||
|
spa_pod_builder_push_object(&b, &f[0],
|
||||||
|
SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl);
|
||||||
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
|
||||||
|
spa_pod_builder_push_object(&b, &f[1],
|
||||||
|
SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props);
|
||||||
|
spa_pod_builder_prop(&b, SPA_PROP_params, 0);
|
||||||
|
spa_pod_builder_push_struct(&b, &f[2]);
|
||||||
|
spa_pod_builder_string(&b, "ReadVolume");
|
||||||
|
spa_pod_builder_string(&b, device ? device : "");
|
||||||
|
spa_pod_builder_string(&b, port_name ? port_name : "");
|
||||||
|
spa_pod_builder_pop(&b, &f[2]);
|
||||||
|
spa_pod_builder_pop(&b, &f[1]);
|
||||||
|
ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]);
|
||||||
|
|
||||||
|
pa_log_info("sending 'ReadVolume' ext-vol-control event to %s on port %s", device, port_name);
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
ext_volume->notifier_cb (ext_volume->notifier_data, ev);
|
||||||
|
|
||||||
|
pa_log_info("read volumes after emitting event: channels:%u",
|
||||||
|
ext_volume->read_volumes[port_index].channels);
|
||||||
|
|
||||||
|
cvol->channels = ext_volume->read_volumes[port_index].channels;
|
||||||
|
for (uint32_t i = 0; i < cvol->channels && i < PA_CHANNELS_MAX && i < SPA_AUDIO_MAX_CHANNELS; i++) {
|
||||||
|
double v = ext_volume->read_volumes[port_index].values[i];
|
||||||
|
cvol->values[i] = lrint(v * PA_VOLUME_NORM);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
spa_acp_ext_volume_write_volume_absolute(struct spa_acp_ext_volume *ext_volume,
|
||||||
|
const char *device, const char *port_name, uint32_t port_index, pa_cvolume *cvol)
|
||||||
|
{
|
||||||
|
struct spa_event *ev;
|
||||||
|
uint8_t buffer[4096];
|
||||||
|
struct spa_pod_builder b = { 0 };
|
||||||
|
struct spa_pod_frame f[4];
|
||||||
|
|
||||||
|
/* Create event */
|
||||||
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
||||||
|
spa_pod_builder_push_object(&b, &f[0],
|
||||||
|
SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl);
|
||||||
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
|
||||||
|
spa_pod_builder_push_object(&b, &f[1],
|
||||||
|
SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props);
|
||||||
|
spa_pod_builder_prop(&b, SPA_PROP_params, 0);
|
||||||
|
spa_pod_builder_push_struct(&b, &f[2]);
|
||||||
|
spa_pod_builder_string(&b, "WriteVolumeAbsolute");
|
||||||
|
spa_pod_builder_string(&b, device ? device : "");
|
||||||
|
spa_pod_builder_string(&b, port_name ? port_name : "");
|
||||||
|
|
||||||
|
/* FIXME: scale to the range from capabilities */
|
||||||
|
spa_pod_builder_push_array(&b, &f[3]);
|
||||||
|
if (ext_volume->flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_BALANCE) {
|
||||||
|
/* Write all channels */
|
||||||
|
for (uint32_t i = 0; i < cvol->channels; i++)
|
||||||
|
spa_pod_builder_double(&b, cvol->values[i] / PA_VOLUME_NORM);
|
||||||
|
} else {
|
||||||
|
/* Single volume */
|
||||||
|
spa_pod_builder_double(&b, pa_cvolume_max(cvol) / PA_VOLUME_NORM);
|
||||||
|
}
|
||||||
|
spa_pod_builder_pop(&b, &f[3]);
|
||||||
|
|
||||||
|
spa_pod_builder_pop(&b, &f[2]);
|
||||||
|
spa_pod_builder_pop(&b, &f[1]);
|
||||||
|
ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]);
|
||||||
|
|
||||||
|
pa_log_info("sending 'WriteVolumeAbsolute' ext-vol-control event to %s on port %s", device, port_name);
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
ext_volume->notifier_cb (ext_volume->notifier_data, ev);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
spa_acp_ext_volume_write_volume_relative(struct spa_acp_ext_volume *ext_volume,
|
||||||
|
const char *device, const char *port_name, uint32_t port_index, float step)
|
||||||
|
{
|
||||||
|
struct spa_event *ev;
|
||||||
|
uint8_t buffer[4096];
|
||||||
|
struct spa_pod_builder b = { 0 };
|
||||||
|
struct spa_pod_frame f[3];
|
||||||
|
|
||||||
|
/* Create event */
|
||||||
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
||||||
|
spa_pod_builder_push_object(&b, &f[0],
|
||||||
|
SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl);
|
||||||
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
|
||||||
|
spa_pod_builder_push_object(&b, &f[1],
|
||||||
|
SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props);
|
||||||
|
spa_pod_builder_prop(&b, SPA_PROP_params, 0);
|
||||||
|
spa_pod_builder_push_struct(&b, &f[2]);
|
||||||
|
spa_pod_builder_string(&b, "WriteVolumeRelative");
|
||||||
|
spa_pod_builder_string(&b, device ? device : "");
|
||||||
|
spa_pod_builder_string(&b, port_name ? port_name : "");
|
||||||
|
spa_pod_builder_float(&b, step);
|
||||||
|
spa_pod_builder_pop(&b, &f[2]);
|
||||||
|
spa_pod_builder_pop(&b, &f[1]);
|
||||||
|
ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]);
|
||||||
|
|
||||||
|
pa_log_info("sending 'WriteVolumeRelative' ext-vol-control event to %s on port %s", device, port_name);
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
ext_volume->notifier_cb (ext_volume->notifier_data, ev);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
spa_acp_ext_volume_read_mute(struct spa_acp_ext_volume *ext_volume,
|
||||||
|
const char *device, const char *port_name, uint32_t port_index, bool *mute)
|
||||||
|
{
|
||||||
|
struct spa_event *ev;
|
||||||
|
uint8_t buffer[4096];
|
||||||
|
struct spa_pod_builder b = { 0 };
|
||||||
|
struct spa_pod_frame f[3];
|
||||||
|
|
||||||
|
/* Create event */
|
||||||
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
||||||
|
spa_pod_builder_push_object(&b, &f[0],
|
||||||
|
SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl);
|
||||||
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
|
||||||
|
spa_pod_builder_push_object(&b, &f[1],
|
||||||
|
SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props);
|
||||||
|
spa_pod_builder_prop(&b, SPA_PROP_params, 0);
|
||||||
|
spa_pod_builder_push_struct(&b, &f[2]);
|
||||||
|
spa_pod_builder_string(&b, "ReadMute");
|
||||||
|
spa_pod_builder_string(&b, device ? device : "");
|
||||||
|
spa_pod_builder_string(&b, port_name ? port_name : "");
|
||||||
|
spa_pod_builder_pop(&b, &f[2]);
|
||||||
|
spa_pod_builder_pop(&b, &f[1]);
|
||||||
|
ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]);
|
||||||
|
|
||||||
|
pa_log_info("sending 'ReadMute' ext-vol-control event to %s on port %s", device, port_name);
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
ext_volume->notifier_cb (ext_volume->notifier_data, ev);
|
||||||
|
|
||||||
|
pa_log_info("read mute after emitting event: mute:%u", ext_volume->read_mutes[port_index]);
|
||||||
|
|
||||||
|
*mute = ext_volume->read_mutes[port_index];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
spa_acp_ext_volume_write_mute_value(struct spa_acp_ext_volume *ext_volume,
|
||||||
|
const char *device, const char *port_name, uint32_t port_index, bool mute)
|
||||||
|
{
|
||||||
|
struct spa_event *ev;
|
||||||
|
uint8_t buffer[4096];
|
||||||
|
struct spa_pod_builder b = { 0 };
|
||||||
|
struct spa_pod_frame f[3];
|
||||||
|
|
||||||
|
/* Create event */
|
||||||
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
||||||
|
spa_pod_builder_push_object(&b, &f[0],
|
||||||
|
SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl);
|
||||||
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
|
||||||
|
spa_pod_builder_push_object(&b, &f[1],
|
||||||
|
SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props);
|
||||||
|
spa_pod_builder_prop(&b, SPA_PROP_params, 0);
|
||||||
|
spa_pod_builder_push_struct(&b, &f[2]);
|
||||||
|
spa_pod_builder_string(&b, "WriteMuteValue");
|
||||||
|
spa_pod_builder_string(&b, device ? device : "");
|
||||||
|
spa_pod_builder_string(&b, port_name ? port_name : "");
|
||||||
|
spa_pod_builder_bool(&b, mute);
|
||||||
|
spa_pod_builder_pop(&b, &f[2]);
|
||||||
|
spa_pod_builder_pop(&b, &f[1]);
|
||||||
|
ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]);
|
||||||
|
|
||||||
|
pa_log_info("sending 'WriteMuteValue' ext-vol-control event to %s on port %s", device, port_name);
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
ext_volume->notifier_cb (ext_volume->notifier_data, ev);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
spa_acp_ext_volume_write_mute_toggle(struct spa_acp_ext_volume *ext_volume,
|
||||||
|
const char *device, const char *port_name, uint32_t port_index)
|
||||||
|
{
|
||||||
|
struct spa_event *ev;
|
||||||
|
uint8_t buffer[4096];
|
||||||
|
struct spa_pod_builder b = { 0 };
|
||||||
|
struct spa_pod_frame f[3];
|
||||||
|
|
||||||
|
/* Create event */
|
||||||
|
spa_pod_builder_init(&b, buffer, sizeof(buffer));
|
||||||
|
spa_pod_builder_push_object(&b, &f[0],
|
||||||
|
SPA_TYPE_EVENT_Device, SPA_DEVICE_EVENT_ExtVolumeControl);
|
||||||
|
spa_pod_builder_prop(&b, SPA_EVENT_DEVICE_Props, 0);
|
||||||
|
spa_pod_builder_push_object(&b, &f[1],
|
||||||
|
SPA_TYPE_OBJECT_Props, SPA_EVENT_DEVICE_Props);
|
||||||
|
spa_pod_builder_prop(&b, SPA_PROP_params, 0);
|
||||||
|
spa_pod_builder_push_struct(&b, &f[2]);
|
||||||
|
spa_pod_builder_string(&b, "WriteMuteToggle");
|
||||||
|
spa_pod_builder_string(&b, device ? device : "");
|
||||||
|
spa_pod_builder_string(&b, port_name ? port_name : "");
|
||||||
|
spa_pod_builder_pop(&b, &f[2]);
|
||||||
|
spa_pod_builder_pop(&b, &f[1]);
|
||||||
|
ev = (struct spa_event *)spa_pod_builder_pop(&b, &f[0]);
|
||||||
|
|
||||||
|
pa_log_info("sending 'WriteMuteToggle' ext-vol-control event to %s on port %s", device, port_name);
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
ext_volume->notifier_cb (ext_volume->notifier_data, ev);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
58
spa/plugins/alsa/acp/ext-volume.h
Normal file
58
spa/plugins/alsa/acp/ext-volume.h
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
/* External Volume Control */
|
||||||
|
/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */
|
||||||
|
/* SPDX-FileCopyrightText: Copyright © 2026 Julian Bouzas */
|
||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
|
||||||
|
#ifndef ACP_EXT_VOLUME_H
|
||||||
|
#define ACP_EXT_VOLUME_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <spa/param/audio/volume.h>
|
||||||
|
#include <spa/pod/event.h>
|
||||||
|
|
||||||
|
#define MAX_PORTS 256
|
||||||
|
|
||||||
|
typedef struct pa_cvolume pa_cvolume;
|
||||||
|
|
||||||
|
typedef void (*ext_volume_notifier_t) (void *data, struct spa_event *event);
|
||||||
|
|
||||||
|
struct read_volume {
|
||||||
|
uint32_t channels;
|
||||||
|
double values[SPA_AUDIO_MAX_CHANNELS];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct spa_acp_ext_volume {
|
||||||
|
bool initialized;
|
||||||
|
enum spa_audio_volume_control_flags flags;
|
||||||
|
|
||||||
|
ext_volume_notifier_t notifier_cb;
|
||||||
|
void *notifier_data;
|
||||||
|
|
||||||
|
enum spa_audio_volume_control_flags read_caps;
|
||||||
|
bool read_mutes[MAX_PORTS];
|
||||||
|
struct read_volume read_volumes[MAX_PORTS];
|
||||||
|
};
|
||||||
|
|
||||||
|
int spa_acp_ext_volume_init(struct spa_acp_ext_volume *ext_volume, const char *device,
|
||||||
|
ext_volume_notifier_t notifier_cb, void *notifier_data);
|
||||||
|
|
||||||
|
int spa_acp_ext_volume_read_volume(struct spa_acp_ext_volume *ext_volume,
|
||||||
|
const char *device, const char *port_name, uint32_t port_index, pa_cvolume *cvol);
|
||||||
|
|
||||||
|
int spa_acp_ext_volume_write_volume_absolute(struct spa_acp_ext_volume *ext_volume,
|
||||||
|
const char *device, const char *port_name, uint32_t port_index, pa_cvolume *cvol);
|
||||||
|
|
||||||
|
int spa_acp_ext_volume_write_volume_relative(struct spa_acp_ext_volume *ext_volume,
|
||||||
|
const char *device, const char *port_name, uint32_t port_index, float step);
|
||||||
|
|
||||||
|
int spa_acp_ext_volume_read_mute(struct spa_acp_ext_volume *ext_volume,
|
||||||
|
const char *device, const char *port_name, uint32_t port_index, bool *mute);
|
||||||
|
|
||||||
|
int spa_acp_ext_volume_write_mute_value(struct spa_acp_ext_volume *ext_volume,
|
||||||
|
const char *device, const char *port_name, uint32_t port_index, bool mute);
|
||||||
|
|
||||||
|
int spa_acp_ext_volume_write_mute_toggle(struct spa_acp_ext_volume *ext_volume,
|
||||||
|
const char *device, const char *port_name, uint32_t port_index);
|
||||||
|
|
||||||
|
#endif /* ACP_EXT_VOLUME_H */
|
||||||
|
|
@ -5,6 +5,7 @@ acp_sources = [
|
||||||
'alsa-ucm.c',
|
'alsa-ucm.c',
|
||||||
'alsa-util.c',
|
'alsa-util.c',
|
||||||
'conf-parser.c',
|
'conf-parser.c',
|
||||||
|
'ext-volume.c',
|
||||||
]
|
]
|
||||||
|
|
||||||
acp_c_args = [
|
acp_c_args = [
|
||||||
|
|
|
||||||
|
|
@ -490,6 +490,11 @@ static struct spa_pod *build_route(struct spa_pod_builder *b, uint32_t id,
|
||||||
dev->n_codecs, dev->codecs);
|
dev->n_codecs, dev->codecs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dev->ext_volume_flags) {
|
||||||
|
spa_pod_builder_prop(b, SPA_PROP_volumeControlFlags, 0);
|
||||||
|
spa_pod_builder_id(b, dev->ext_volume_flags);
|
||||||
|
}
|
||||||
|
|
||||||
spa_pod_builder_pop(b, &f[1]);
|
spa_pod_builder_pop(b, &f[1]);
|
||||||
}
|
}
|
||||||
spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0);
|
spa_pod_builder_prop(b, SPA_PARAM_ROUTE_devices, 0);
|
||||||
|
|
@ -1083,6 +1088,12 @@ static void on_mute_changed(void *data, struct acp_device *dev)
|
||||||
spa_device_emit_event(&this->hooks, event);
|
spa_device_emit_event(&this->hooks, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void on_ext_vol_event_available(void *data, struct spa_event *event)
|
||||||
|
{
|
||||||
|
struct impl *this = data;
|
||||||
|
spa_device_emit_event(&this->hooks, event);
|
||||||
|
}
|
||||||
|
|
||||||
static const struct acp_card_events card_events = {
|
static const struct acp_card_events card_events = {
|
||||||
ACP_VERSION_CARD_EVENTS,
|
ACP_VERSION_CARD_EVENTS,
|
||||||
.props_changed = card_props_changed,
|
.props_changed = card_props_changed,
|
||||||
|
|
@ -1092,6 +1103,7 @@ static const struct acp_card_events card_events = {
|
||||||
.port_available = card_port_available,
|
.port_available = card_port_available,
|
||||||
.volume_changed = on_volume_changed,
|
.volume_changed = on_volume_changed,
|
||||||
.mute_changed = on_mute_changed,
|
.mute_changed = on_mute_changed,
|
||||||
|
.ext_vol_event_available = on_ext_vol_event_available,
|
||||||
};
|
};
|
||||||
|
|
||||||
static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
|
static int impl_get_interface(struct spa_handle *handle, const char *type, void **interface)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue