mirror of
https://gitlab.freedesktop.org/pipewire/pipewire.git
synced 2026-03-25 09:05:57 -04:00
Merge branch 'alsa-ext-vol' into 'master'
Add an external volume control mechanism See merge request pipewire/pipewire!2722
This commit is contained in:
commit
cc53257729
33 changed files with 2361 additions and 103 deletions
|
|
@ -8,6 +8,7 @@
|
|||
#include <spa/utils/defs.h>
|
||||
#include <spa/utils/hook.h>
|
||||
#include <spa/utils/dict.h>
|
||||
#include <spa/pod/command.h>
|
||||
#include <spa/pod/event.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
|
@ -126,7 +127,35 @@ struct spa_device_events {
|
|||
#define SPA_DEVICE_METHOD_SYNC 1
|
||||
#define SPA_DEVICE_METHOD_ENUM_PARAMS 2
|
||||
#define SPA_DEVICE_METHOD_SET_PARAM 3
|
||||
#define SPA_DEVICE_METHOD_NUM 4
|
||||
#define SPA_DEVICE_METHOD_SEND_COMMAND 4
|
||||
#define SPA_DEVICE_METHOD_NUM 5
|
||||
|
||||
/* object id of SPA_TYPE_COMMAND_Device */
|
||||
enum spa_device_commands {
|
||||
SPA_DEVICE_COMMAND_User,
|
||||
SPA_DEVICE_COMMAND_VolumeControl, /**< Volume control for devices which do not
|
||||
* support standard volume control via route
|
||||
* props */
|
||||
};
|
||||
|
||||
#define SPA_DEVICE_COMMAND_ID(cmd) SPA_COMMAND_ID(cmd, SPA_TYPE_COMMAND_Device)
|
||||
#define SPA_DEVICE_COMMAND_INIT(id) SPA_COMMAND_INIT(SPA_TYPE_COMMAND_Device, id)
|
||||
|
||||
|
||||
/* properties for SPA_TYPE_COMMAND_Device */
|
||||
enum spa_command_device {
|
||||
SPA_COMMAND_DEVICE_START,
|
||||
|
||||
SPA_COMMAND_DEVICE_START_User = 0x1000,
|
||||
SPA_COMMAND_DEVICE_extra, /** extra info (String) */
|
||||
|
||||
SPA_COMMAND_DEVICE_START_VolumeControl = 0x2000,
|
||||
SPA_COMMAND_DEVICE_volumeUp, /** Send a volume up command. route id (Id) */
|
||||
SPA_COMMAND_DEVICE_volumeDown, /** Send a volume down command. route id (Id) */
|
||||
SPA_COMMAND_DEVICE_muteToggle, /** Send a mute toggle command. route id (Id) */
|
||||
|
||||
SPA_COMMAND_DEVICE_START_CUSTOM = 0x1000000,
|
||||
};
|
||||
|
||||
/**
|
||||
* spa_device_methods:
|
||||
|
|
@ -134,7 +163,7 @@ struct spa_device_events {
|
|||
struct spa_device_methods {
|
||||
/* the version of the methods. This can be used to expand this
|
||||
* structure in the future */
|
||||
#define SPA_VERSION_DEVICE_METHODS 0
|
||||
#define SPA_VERSION_DEVICE_METHODS 1
|
||||
uint32_t version;
|
||||
|
||||
/**
|
||||
|
|
@ -226,6 +255,23 @@ struct spa_device_methods {
|
|||
int (*set_param) (void *object,
|
||||
uint32_t id, uint32_t flags,
|
||||
const struct spa_pod *param);
|
||||
|
||||
/**
|
||||
* Send a command to a device.
|
||||
*
|
||||
* This function must be called from the main thread.
|
||||
*
|
||||
* \param object a \ref spa_device
|
||||
* \param command a \ref spa_command of type \ref spa_command_device
|
||||
* \return 0 on success
|
||||
* -EINVAL when node or command is NULL
|
||||
* -ENOTSUP when this node can't process commands
|
||||
* -EINVAL \a command is an invalid command
|
||||
*
|
||||
* \since 1
|
||||
*/
|
||||
int (*send_command) (void *object, const struct spa_command *command);
|
||||
|
||||
};
|
||||
|
||||
SPA_API_DEVICE int spa_device_add_listener(struct spa_device *object,
|
||||
|
|
@ -256,6 +302,12 @@ SPA_API_DEVICE int spa_device_set_param(struct spa_device *object,
|
|||
return spa_api_method_r(int, -ENOTSUP, spa_device, &object->iface, set_param, 0,
|
||||
id, flags, param);
|
||||
}
|
||||
SPA_API_DEVICE int spa_device_send_command(struct spa_device *object,
|
||||
const struct spa_command *command)
|
||||
{
|
||||
return spa_api_method_r(int, -ENOTSUP, spa_device, &object->iface, send_command, 1,
|
||||
command);
|
||||
}
|
||||
|
||||
#define SPA_KEY_DEVICE_ENUM_API "device.enum.api" /**< the api used to discover this
|
||||
* device */
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@
|
|||
#ifndef SPA_DEVICE_TYPE_INFO_H
|
||||
#define SPA_DEVICE_TYPE_INFO_H
|
||||
|
||||
#include <spa/utils/type-info.h>
|
||||
#include <spa/utils/type.h>
|
||||
|
||||
#include <spa/monitor/device.h>
|
||||
#include <spa/monitor/event.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
|
@ -36,6 +37,27 @@ static const struct spa_type_info spa_type_device_event[] = {
|
|||
{ 0, 0, NULL, NULL },
|
||||
};
|
||||
|
||||
#define SPA_TYPE_INFO_DeviceCommand SPA_TYPE_INFO_COMMAND_BASE "Device"
|
||||
#define SPA_TYPE_INFO_DEVICE_COMMAND_BASE SPA_TYPE_INFO_DeviceCommand ":"
|
||||
|
||||
static const struct spa_type_info spa_type_device_command_id[] = {
|
||||
{ SPA_DEVICE_COMMAND_User, SPA_TYPE_COMMAND_Device, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "User", NULL },
|
||||
{ SPA_DEVICE_COMMAND_VolumeControl, SPA_TYPE_COMMAND_Device, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "VolumeControl", NULL },
|
||||
{ 0, 0, NULL, NULL },
|
||||
};
|
||||
|
||||
static const struct spa_type_info spa_type_device_command[] = {
|
||||
{ SPA_COMMAND_DEVICE_START, SPA_TYPE_Id, SPA_TYPE_INFO_DEVICE_COMMAND_BASE, spa_type_device_command_id },
|
||||
|
||||
{ SPA_COMMAND_DEVICE_extra, SPA_TYPE_String, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "extra", NULL },
|
||||
|
||||
{ SPA_COMMAND_DEVICE_volumeUp, SPA_TYPE_Id, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "volumeUp", NULL },
|
||||
{ SPA_COMMAND_DEVICE_volumeDown, SPA_TYPE_Id, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "volumeDown", NULL },
|
||||
{ SPA_COMMAND_DEVICE_muteToggle, SPA_TYPE_Id, SPA_TYPE_INFO_DEVICE_COMMAND_BASE "muteToggle", NULL },
|
||||
|
||||
{ 0, 0, NULL, NULL },
|
||||
};
|
||||
|
||||
/**
|
||||
* \}
|
||||
*/
|
||||
|
|
|
|||
29
spa/include/spa/param/audio/volume.h
Normal file
29
spa/include/spa/param/audio/volume.h
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* Simple Plugin API */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
#ifndef SPA_AUDIO_VOLUME_H
|
||||
#define SPA_AUDIO_VOLUME_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Flags for volume control capabilities */
|
||||
enum spa_audio_volume_control_flags {
|
||||
SPA_AUDIO_VOLUME_CONTROL_NONE = 0, /**<< No flags */
|
||||
SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME = (1 << 0), /**<< Volume value can be read */
|
||||
SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_VALUE = (1 << 1), /**<< Volume value can be set */
|
||||
SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_UPDOWN = (1 << 2), /**<< Volume value can incremented/decremented */
|
||||
SPA_AUDIO_VOLUME_CONTROL_READ_MUTE = (1 << 3), /**<< Mute state can be read */
|
||||
SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_VALUE = (1 << 4), /**<< Mute state can be set */
|
||||
SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_TOGGLE = (1 << 5), /**<< Mute state can be toggled */
|
||||
SPA_AUDIO_VOLUME_CONTROL_READ_BALANCE = (1 << 6), /**<< Per-channel volumes can be read */
|
||||
SPA_AUDIO_VOLUME_CONTROL_WRITE_BALANCE = (1 << 7), /**<< Per-channel volumes can be set */
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif /* SPA_AUDIO_VOLUME_H */
|
||||
|
|
@ -66,6 +66,7 @@ static const struct spa_type_info spa_type_props[] = {
|
|||
{ SPA_PROP_volumeRampTime, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeRampTime", NULL },
|
||||
{ SPA_PROP_volumeRampStepTime, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeRampStepTime", NULL },
|
||||
{ SPA_PROP_volumeRampScale, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "volumeRampScale", spa_type_audio_volume_ramp_scale },
|
||||
{ SPA_PROP_volumeControlFlags, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "volumeControlFlags", NULL },
|
||||
|
||||
{ SPA_PROP_brightness, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "brightness", NULL },
|
||||
{ SPA_PROP_contrast, SPA_TYPE_Float, SPA_TYPE_INFO_PROPS_BASE "contrast", NULL },
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ enum spa_prop {
|
|||
* to ramp the */
|
||||
SPA_PROP_volumeRampScale, /**< the scale or graph to used to ramp the
|
||||
* volume */
|
||||
SPA_PROP_volumeControlFlags, /**< Available volume control features
|
||||
* (Id enum spa_audio_volume_control_flags) */
|
||||
|
||||
SPA_PROP_START_Video = 0x20000, /**< video related properties */
|
||||
SPA_PROP_brightness,
|
||||
|
|
|
|||
|
|
@ -122,10 +122,9 @@ struct spa_dbus_methods {
|
|||
*
|
||||
* \param dbus the dbus manager
|
||||
* \param type the bus type to wrap
|
||||
* \param error location for the DBusError
|
||||
* \return a new dbus connection wrapper or NULL on error
|
||||
*/
|
||||
struct spa_dbus_connection * (*get_connection) (void *object,
|
||||
struct spa_dbus_connection * (*get_connection) (void *dbus,
|
||||
enum spa_dbus_type type);
|
||||
};
|
||||
|
||||
|
|
|
|||
163
spa/include/spa/support/varlink.h
Normal file
163
spa/include/spa/support/varlink.h
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/* Simple Plugin API */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
#ifndef SPA_VARLINK_H
|
||||
#define SPA_VARLINK_H
|
||||
|
||||
#include <spa/utils/hook.h>
|
||||
#include <spa/utils/json.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef SPA_API_VARLINK
|
||||
#ifdef SPA_API_IMPL
|
||||
#define SPA_API_VARLINK SPA_API_IMPL
|
||||
#else
|
||||
#define SPA_API_VARLINK static inline
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/** \defgroup spa_varlink Varlink
|
||||
* Varlink communication
|
||||
*/
|
||||
|
||||
/**
|
||||
* \addtogroup spa_varlink
|
||||
* \{
|
||||
*/
|
||||
|
||||
#define SPA_TYPE_INTERFACE_Varlink SPA_TYPE_INFO_INTERFACE_BASE "Varlink"
|
||||
|
||||
typedef void (*spa_varlink_reply_func_t) (void *data, const char *params,
|
||||
const char *error, size_t len, bool continues);
|
||||
|
||||
struct spa_varlink_client_events {
|
||||
#define SPA_VERSION_VARLINK_CLIENT_EVENTS 0
|
||||
uint32_t version;
|
||||
|
||||
/** The client was destroyed. */
|
||||
void (*destroy) (void *data);
|
||||
|
||||
/** The client was disconnected. */
|
||||
void (*disconnect) (void *data);
|
||||
};
|
||||
|
||||
struct spa_varlink_client {
|
||||
#define SPA_VERSION_VARLINK_CLIENT 0
|
||||
uint32_t version;
|
||||
|
||||
/** Add a listener fo events */
|
||||
void (*add_listener) (void *object, struct spa_hook *listener,
|
||||
const struct spa_varlink_client_events *events,
|
||||
void *data);
|
||||
|
||||
/** Call a single- or no-reply method on a varlink client.
|
||||
*
|
||||
* \param method Fully qualified (`interface.Method`) method name to
|
||||
* call.
|
||||
* \param params Method parameters as a string (must be a valid JSON
|
||||
* object).
|
||||
* \param oneway Signal that the server should not send a reply.
|
||||
* \param more Expect multiple replies from the server for this method
|
||||
* call.
|
||||
* \param cb Callback to invok when a reply to this call arrives.
|
||||
* \param userdata Userdata to supply to the callback.
|
||||
* \return 0 on success, or a negative error code on failure.
|
||||
*/
|
||||
int (*call) (void *object, const char *method, const char *params,
|
||||
bool oneway, bool more, spa_varlink_reply_func_t cb,
|
||||
void *userdata);
|
||||
|
||||
/** Call a single-reply method and block until a reply is received.
|
||||
*
|
||||
* \param method Fully qualified (`interface.Method`) method name to
|
||||
* call.
|
||||
* \param params Method parameters as a string (must be a valid JSON
|
||||
* object).
|
||||
* \param reply The reply string. The caller is responsible for freeing
|
||||
* this data with `free()`
|
||||
* \return 0 on success, or a negative error code on failure.
|
||||
*/
|
||||
int (*call_sync) (void *object, const char *method, const char *params,
|
||||
char **reply);
|
||||
|
||||
/** Destroy a varlink client.
|
||||
*
|
||||
* \param client The client to destroy
|
||||
*/
|
||||
void (*destroy) (void *object);
|
||||
};
|
||||
|
||||
/** \copydoc spa_varlink_client_methods.add_listener
|
||||
* \sa spa_varlink_client_methods.add_listener */
|
||||
SPA_API_VARLINK void
|
||||
spa_varlink_client_add_listener(struct spa_varlink_client *client,
|
||||
struct spa_hook *listener,
|
||||
const struct spa_varlink_client_events *events,
|
||||
void *data)
|
||||
{
|
||||
spa_api_func_v(client, add_listener, 0, listener, events, data);
|
||||
}
|
||||
|
||||
/** \copydoc spa_varlink_client_methods.call
|
||||
* \sa spa_varlink_client_methods.call */
|
||||
SPA_API_VARLINK int
|
||||
spa_varlink_client_call(struct spa_varlink_client *client, const char *method,
|
||||
const char *params, bool oneway, bool more,
|
||||
spa_varlink_reply_func_t cb, void *userdata)
|
||||
{
|
||||
return spa_api_func_r(int, -EINVAL, client, call, 0, method, params,
|
||||
oneway, more, cb, userdata);
|
||||
}
|
||||
|
||||
/** \copydoc spa_varlink_client_methods.call_sync
|
||||
* \sa spa_varlink_client_methods.call_sync */
|
||||
SPA_API_VARLINK int
|
||||
spa_varlink_client_call_sync(struct spa_varlink_client *client, const char *method,
|
||||
const char *params, char **reply)
|
||||
{
|
||||
return spa_api_func_r(int, -EINVAL, client, call_sync, 0, method,
|
||||
params, reply);
|
||||
}
|
||||
|
||||
/** \copydoc spa_varlink_client_methods.destroy
|
||||
* \sa spa_varlink_client_methods.destroy */
|
||||
SPA_API_VARLINK void
|
||||
spa_varlink_client_destroy(struct spa_varlink_client *client)
|
||||
{
|
||||
spa_api_func_v(client, destroy, 0);
|
||||
}
|
||||
|
||||
#define SPA_VERSION_VARLINK 0
|
||||
struct spa_varlink { struct spa_interface iface; };
|
||||
|
||||
struct spa_varlink_methods {
|
||||
#define SPA_VERSION_VARLINK_METHODS 0
|
||||
uint32_t version;
|
||||
|
||||
/**
|
||||
* Connect to a varlink service.
|
||||
*
|
||||
* \param path Path to connect to, for example `unix:/path/to/socket`
|
||||
* \return A `spa_varlink_client` on success, NULL on failure.
|
||||
*/
|
||||
struct spa_varlink_client * (*connect) (void *object, const char *path);
|
||||
};
|
||||
|
||||
/** \copydoc spa_varlink_methods.connect
|
||||
* \sa spa_varlink_methods.connect */
|
||||
SPA_API_VARLINK void *
|
||||
spa_varlink_connect(struct spa_varlink *varlink, const char *path)
|
||||
{
|
||||
return spa_api_method_r(struct spa_varlink_client *, NULL, spa_varlink,
|
||||
&varlink->iface, connect, 0, path);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
#endif /* SPA_VARLINK_H */
|
||||
|
|
@ -25,6 +25,7 @@ extern "C" {
|
|||
#define SPA_NAME_SUPPORT_LOOP "support.loop" /**< A Loop/LoopControl/LoopUtils
|
||||
* interface */
|
||||
#define SPA_NAME_SUPPORT_SYSTEM "support.system" /**< A System interface */
|
||||
#define SPA_NAME_SUPPORT_VARLINK "support.varlink" /**< A Varlink interface */
|
||||
|
||||
#define SPA_NAME_SUPPORT_NODE_DRIVER "support.node.driver" /**< A dummy driver node */
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ static const struct spa_type_info spa_types[] = {
|
|||
{ SPA_TYPE_EVENT_Node, SPA_TYPE_Object, SPA_TYPE_INFO_EVENT_BASE "Node", spa_type_node_event },
|
||||
|
||||
{ SPA_TYPE_COMMAND_START, SPA_TYPE_Object, SPA_TYPE_INFO_Command, NULL },
|
||||
{ SPA_TYPE_COMMAND_Device, SPA_TYPE_Object, SPA_TYPE_INFO_COMMAND_BASE "Device", NULL },
|
||||
{ SPA_TYPE_COMMAND_Device, SPA_TYPE_Object, SPA_TYPE_INFO_COMMAND_BASE "Device", spa_type_device_command },
|
||||
{ SPA_TYPE_COMMAND_Node, SPA_TYPE_Object, SPA_TYPE_INFO_COMMAND_BASE "Node", spa_type_node_command },
|
||||
|
||||
{ SPA_TYPE_OBJECT_START, SPA_TYPE_Object, SPA_TYPE_INFO_Object, NULL },
|
||||
|
|
|
|||
|
|
@ -5,18 +5,23 @@
|
|||
#include "acp.h"
|
||||
#include "alsa-mixer.h"
|
||||
#include "alsa-ucm.h"
|
||||
#include "ext-volume.h"
|
||||
|
||||
#include <spa/utils/string.h>
|
||||
#include <spa/utils/json.h>
|
||||
#include <spa/utils/json-builder.h>
|
||||
#include <spa/utils/cleanup.h>
|
||||
#include <spa/param/audio/iec958-types.h>
|
||||
#include <spa/param/audio/raw.h>
|
||||
#include <spa/param/audio/volume.h>
|
||||
#include <spa/support/varlink.h>
|
||||
|
||||
int _acp_log_level = 1;
|
||||
acp_log_func _acp_log_func;
|
||||
void *_acp_log_data;
|
||||
|
||||
struct spa_i18n *acp_i18n;
|
||||
struct spa_varlink *acp_varlink;
|
||||
|
||||
#define DEFAULT_CHANNELS 255u
|
||||
#define DEFAULT_RATE 48000u
|
||||
|
|
@ -1245,6 +1250,30 @@ static void init_eld_ctls(pa_card *impl)
|
|||
}
|
||||
}
|
||||
|
||||
static void init_ext_volume(pa_card *impl)
|
||||
{
|
||||
struct acp_card *card = &impl->card;
|
||||
int res;
|
||||
|
||||
if (!impl->ext_volume_path)
|
||||
return;
|
||||
|
||||
if (!acp_varlink) {
|
||||
pa_log_error("External volume control requires support.varlink = true");
|
||||
return;
|
||||
}
|
||||
|
||||
res = spa_acp_ext_volume_init(&card->ext_volume, acp_varlink, impl->ext_volume_path, impl->name, NULL);
|
||||
if (res < 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
/* FIXME: Set up monitor callback */
|
||||
|
||||
error:
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t acp_card_find_best_profile_index(struct acp_card *card, const char *name)
|
||||
{
|
||||
uint32_t i;
|
||||
|
|
@ -1356,21 +1385,47 @@ static int read_volume(pa_alsa_device *dev)
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (!dev->mixer_handle)
|
||||
if (impl->card.ext_volume.client &&
|
||||
(impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME)) {
|
||||
/* Externally managed volume */
|
||||
pa_cvolume ext_vol;
|
||||
|
||||
if ((res = spa_acp_ext_volume_read_volume(&impl->card.ext_volume, impl->name,
|
||||
dev->active_port->name, &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;
|
||||
|
||||
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))
|
||||
return 0;
|
||||
|
|
@ -1393,12 +1448,10 @@ static int read_volume(pa_alsa_device *dev)
|
|||
|
||||
static void set_volume(pa_alsa_device *dev, const pa_cvolume *v)
|
||||
{
|
||||
pa_card *impl = dev->card;
|
||||
pa_cvolume r;
|
||||
bool write_to_hw;
|
||||
|
||||
if (v != &dev->real_volume)
|
||||
dev->real_volume = *v;
|
||||
|
||||
if (dev->ucm_context) {
|
||||
if (!dev->active_port)
|
||||
return;
|
||||
|
|
@ -1407,52 +1460,99 @@ static void set_volume(pa_alsa_device *dev, const pa_cvolume *v)
|
|||
return;
|
||||
}
|
||||
|
||||
if (!dev->mixer_handle)
|
||||
return;
|
||||
if (impl->card.ext_volume.client) {
|
||||
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->real_volume) < 0) {
|
||||
pa_log_error("Could not write volume");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Shift up by the base volume */
|
||||
pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_volume);
|
||||
/* Update volume if we were successful */
|
||||
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,
|
||||
&r, false, write_to_hw) < 0)
|
||||
return;
|
||||
if (cur < new)
|
||||
step = 1;
|
||||
else if (cur > new)
|
||||
step = -1;
|
||||
else
|
||||
return;
|
||||
|
||||
/* Shift down by the base volume, so that 0dB becomes maximum volume */
|
||||
pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume);
|
||||
if (spa_acp_ext_volume_write_volume_relative(&impl->card.ext_volume,
|
||||
impl->name, dev->active_port->name, 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) {
|
||||
pa_cvolume new_soft_volume;
|
||||
bool accurate_enough;
|
||||
if (v != &dev->real_volume)
|
||||
dev->real_volume = *v;
|
||||
|
||||
/* Match exactly what the user requested by software */
|
||||
pa_sw_cvolume_divide(&new_soft_volume, &dev->real_volume, &dev->hardware_volume);
|
||||
/* Shift up by the base volume */
|
||||
pa_sw_cvolume_divide_scalar(&r, &dev->real_volume, dev->base_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));
|
||||
write_to_hw = !(dev->mixer_path->has_volume_mute && dev->muted);
|
||||
|
||||
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 (pa_alsa_path_set_volume(dev->mixer_path, dev->mixer_handle, &dev->mapping->channel_map,
|
||||
&r, false, write_to_hw) < 0)
|
||||
return;
|
||||
|
||||
if (accurate_enough)
|
||||
pa_cvolume_reset(&new_soft_volume, new_soft_volume.channels);
|
||||
/* Shift down by the base volume, so that 0dB becomes maximum volume */
|
||||
pa_sw_cvolume_multiply_scalar(&r, &r, dev->base_volume);
|
||||
|
||||
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;
|
||||
dev->hardware_volume = r;
|
||||
|
||||
if (dev->mixer_path->has_dB) {
|
||||
pa_cvolume new_soft_volume;
|
||||
bool accurate_enough;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1470,22 +1570,31 @@ static int read_mute(pa_alsa_device *dev)
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (!dev->mixer_handle)
|
||||
if (impl->card.ext_volume.client &&
|
||||
(impl->card.ext_volume.flags & SPA_AUDIO_VOLUME_CONTROL_READ_MUTE)) {
|
||||
/* Externally managed mute state */
|
||||
if (spa_acp_ext_volume_read_mute(&impl->card.ext_volume, impl->name,
|
||||
dev->active_port->name, &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;
|
||||
|
||||
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)
|
||||
return 0;
|
||||
|
||||
|
|
@ -1500,7 +1609,7 @@ static int read_mute(pa_alsa_device *dev)
|
|||
|
||||
static void set_mute(pa_alsa_device *dev, bool mute)
|
||||
{
|
||||
dev->muted = mute;
|
||||
pa_card *impl = dev->card;
|
||||
|
||||
if (dev->ucm_context) {
|
||||
if (!dev->active_port)
|
||||
|
|
@ -1510,27 +1619,52 @@ static void set_mute(pa_alsa_device *dev, bool mute)
|
|||
return;
|
||||
}
|
||||
|
||||
if (!dev->mixer_handle)
|
||||
return;
|
||||
if (impl->card.ext_volume.client) {
|
||||
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, mute) < 0) {
|
||||
pa_log_error("Could not write mute state");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dev->mixer_path->has_volume_mute) {
|
||||
pa_cvolume r;
|
||||
dev->muted = mute;
|
||||
} 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) < 0) {
|
||||
pa_log_error("Could not write mute toggle");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
dev->muted = mute;
|
||||
} 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));
|
||||
pa_log_debug("Ignoring mute setting, ext volume control does not support it");
|
||||
}
|
||||
} else if (dev->mixer_handle) {
|
||||
/* ALSA mixer for mute state */
|
||||
dev->muted = mute;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1538,7 +1672,15 @@ static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev)
|
|||
{
|
||||
pa_assert(dev);
|
||||
|
||||
if (impl->soft_mixer || !dev->mixer_path || !dev->mixer_path->has_volume) {
|
||||
if (impl->ext_volume_path) {
|
||||
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->set_volume = NULL;
|
||||
pa_log_info("Driver does not support hardware volume control, "
|
||||
|
|
@ -1586,7 +1728,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.volume_step = 1.0f / dev->n_volume_steps;
|
||||
|
||||
if (impl->soft_mixer || !dev->mixer_path ||
|
||||
if (impl->ext_volume_path) {
|
||||
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->read_mute = NULL;
|
||||
dev->set_mute = NULL;
|
||||
|
|
@ -1596,7 +1746,7 @@ static void mixer_volume_init(pa_card *impl, pa_alsa_device *dev)
|
|||
dev->read_mute = read_mute;
|
||||
dev->set_mute = set_mute;
|
||||
pa_log_info("Using hardware %smute control.",
|
||||
dev->mixer_path->has_volume_mute ? "volume-" : "");
|
||||
dev->mixer_path->has_volume_mute ? "volume-" : NULL);
|
||||
dev->device.flags |= ACP_DEVICE_HW_MUTE;
|
||||
}
|
||||
}
|
||||
|
|
@ -1655,6 +1805,12 @@ static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB)
|
|||
return 0;
|
||||
}
|
||||
|
||||
if (impl->ext_volume_path) {
|
||||
/* 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);
|
||||
|
||||
/* Will we need to register callbacks? */
|
||||
|
|
@ -1677,6 +1833,7 @@ static int setup_mixer(pa_card *impl, pa_alsa_device *dev, bool ignore_dB)
|
|||
else
|
||||
pa_alsa_path_set_callback(dev->mixer_path, dev->mixer_handle, mixer_callback, dev);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -1972,6 +2129,8 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
|
|||
impl->disable_pro_audio = spa_atob(s);
|
||||
if ((s = acp_dict_lookup(props, "api.acp.use-eld-channels")) != NULL)
|
||||
impl->use_eld_channels = spa_atob(s);
|
||||
if ((s = acp_dict_lookup(props, "api.alsa.external-volume-control")) != NULL)
|
||||
impl->ext_volume_path = strdup(s);
|
||||
}
|
||||
|
||||
#if SND_LIB_VERSION < 0x10207
|
||||
|
|
@ -2057,6 +2216,7 @@ struct acp_card *acp_card_new(uint32_t index, const struct acp_dict *props)
|
|||
return &impl->card;
|
||||
error:
|
||||
pa_alsa_refcnt_dec();
|
||||
free(impl->ext_volume_path);
|
||||
free(impl);
|
||||
errno = -res;
|
||||
return NULL;
|
||||
|
|
@ -2089,6 +2249,10 @@ void acp_card_destroy(struct acp_card *card)
|
|||
pa_alsa_ucm_free(&impl->ucm);
|
||||
pa_proplist_free(impl->proplist);
|
||||
pa_alsa_refcnt_dec();
|
||||
if (impl->ext_volume_path) {
|
||||
spa_acp_ext_volume_destroy(&impl->card.ext_volume);
|
||||
free(impl->ext_volume_path);
|
||||
}
|
||||
free(impl);
|
||||
}
|
||||
|
||||
|
|
@ -2322,6 +2486,24 @@ int acp_device_set_volume(struct acp_device *dev, const float *volume, uint32_t
|
|||
return 0;
|
||||
}
|
||||
|
||||
int acp_device_set_volume_updown(struct acp_device *dev, bool up)
|
||||
{
|
||||
pa_alsa_device *d = (pa_alsa_device*)dev;
|
||||
pa_card *impl = d->card;
|
||||
|
||||
if (!impl->card.ext_volume.client)
|
||||
return -EINVAL;
|
||||
|
||||
if (!d->active_port)
|
||||
return -EINVAL;
|
||||
|
||||
pa_log_info("Volume %s", up ? "up" : "down");
|
||||
|
||||
/* TODO: use step size from capabilities */
|
||||
return spa_acp_ext_volume_write_volume_relative(&impl->card.ext_volume, impl->name,
|
||||
d->active_port->name, up ? 1.0 : -1.0);
|
||||
}
|
||||
|
||||
static int get_volume(pa_cvolume *v, float *volume, uint32_t n_volume)
|
||||
{
|
||||
uint32_t i;
|
||||
|
|
@ -2357,6 +2539,7 @@ int acp_device_set_mute(struct acp_device *dev, bool mute)
|
|||
|
||||
if (d->set_mute) {
|
||||
d->set_mute(d, mute);
|
||||
mute = d->muted;
|
||||
} else {
|
||||
d->muted = mute;
|
||||
}
|
||||
|
|
@ -2374,6 +2557,23 @@ int acp_device_get_mute(struct acp_device *dev, bool *mute)
|
|||
return 0;
|
||||
}
|
||||
|
||||
int acp_device_toggle_mute(struct acp_device *dev)
|
||||
{
|
||||
pa_alsa_device *d = (pa_alsa_device*)dev;
|
||||
pa_card *impl = d->card;
|
||||
|
||||
if (!impl->card.ext_volume.client)
|
||||
return -EINVAL;
|
||||
|
||||
if (!d->active_port)
|
||||
return -EINVAL;
|
||||
|
||||
pa_log_info("Toggle mute");
|
||||
|
||||
return spa_acp_ext_volume_write_mute_toggle(&impl->card.ext_volume, impl->name,
|
||||
d->active_port->name);
|
||||
}
|
||||
|
||||
void acp_set_log_func(acp_log_func func, void *data)
|
||||
{
|
||||
_acp_log_func = func;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@
|
|||
#include <poll.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <spa/param/audio/volume.h>
|
||||
#include <spa/support/varlink.h>
|
||||
|
||||
#include "ext-volume.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#else
|
||||
|
|
@ -239,6 +244,8 @@ struct acp_device {
|
|||
int64_t latency_ns;
|
||||
uint32_t codecs[32];
|
||||
uint32_t n_codecs;
|
||||
|
||||
uint32_t ext_volume_flags;
|
||||
};
|
||||
|
||||
struct acp_card_profile {
|
||||
|
|
@ -277,6 +284,8 @@ struct acp_card {
|
|||
struct acp_port **ports;
|
||||
uint32_t preferred_input_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);
|
||||
|
|
@ -299,9 +308,11 @@ uint32_t acp_device_find_best_port_index(struct acp_device *dev, const char *nam
|
|||
int acp_device_set_port(struct acp_device *dev, uint32_t port_index, uint32_t flags);
|
||||
|
||||
int acp_device_set_volume(struct acp_device *dev, const float *volume, uint32_t n_volume);
|
||||
int acp_device_set_volume_updown(struct acp_device *dev, bool up);
|
||||
int acp_device_get_soft_volume(struct acp_device *dev, float *volume, uint32_t n_volume);
|
||||
int acp_device_get_volume(struct acp_device *dev, float *volume, uint32_t n_volume);
|
||||
int acp_device_set_mute(struct acp_device *dev, bool mute);
|
||||
int acp_device_toggle_mute(struct acp_device *dev);
|
||||
int acp_device_get_mute(struct acp_device *dev, bool *mute);
|
||||
|
||||
typedef void (*acp_log_func) (void *data,
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ struct pa_card {
|
|||
|
||||
const struct acp_card_events *events;
|
||||
void *user_data;
|
||||
|
||||
char *ext_volume_path;
|
||||
};
|
||||
|
||||
bool pa_alsa_device_init_description(pa_proplist *p, pa_card *card);
|
||||
|
|
|
|||
414
spa/plugins/alsa/acp/ext-volume.c
Normal file
414
spa/plugins/alsa/acp/ext-volume.c
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
/* External Volume Control */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
#include <spa/support/varlink.h>
|
||||
#include <spa/utils/cleanup.h>
|
||||
#include <spa/utils/json.h>
|
||||
#include <spa/utils/json-builder.h>
|
||||
#include <spa/utils/result.h>
|
||||
|
||||
#include "alsa-mixer.h"
|
||||
#include "channelmap.h"
|
||||
#include "volume.h"
|
||||
#include "ext-volume.h"
|
||||
|
||||
#define VOLUME_CONTROL_BASE "org.pipewire.ExternalVolume."
|
||||
#define METHOD_GET_CAPABILITIES VOLUME_CONTROL_BASE "GetCapabilities"
|
||||
#define METHOD_READ_VOLUME VOLUME_CONTROL_BASE "ReadVolume"
|
||||
#define METHOD_WRITE_VOLUME_ABSOLUTE VOLUME_CONTROL_BASE "WriteVolumeAbsolute"
|
||||
#define METHOD_WRITE_VOLUME_RELATIVE VOLUME_CONTROL_BASE "WriteVolumeRelative"
|
||||
#define METHOD_READ_MUTE VOLUME_CONTROL_BASE "ReadMute"
|
||||
#define METHOD_WRITE_MUTE_VALUE VOLUME_CONTROL_BASE "WriteMuteValue"
|
||||
#define METHOD_WRITE_MUTE_TOGGLE VOLUME_CONTROL_BASE "WriteMuteToggle"
|
||||
|
||||
static void client_disconnect(void *userdata)
|
||||
{
|
||||
/* TODO: reconnect? bail? */
|
||||
}
|
||||
|
||||
static void client_destroy(void *userdata)
|
||||
{
|
||||
pa_log_debug("varlink client destroyed\n");
|
||||
}
|
||||
|
||||
static struct spa_varlink_client_events client_events = {
|
||||
.disconnect = client_disconnect,
|
||||
.destroy = client_destroy,
|
||||
};
|
||||
|
||||
static void check_reply(void *data, const char *params, const char *error,
|
||||
size_t len, bool continues)
|
||||
{
|
||||
const char *method = (const char *) data;
|
||||
|
||||
if (error != NULL)
|
||||
pa_log_error("Error calling %s: %s", method, error);
|
||||
}
|
||||
|
||||
int
|
||||
spa_acp_ext_volume_init(struct spa_acp_ext_volume *ext_volume, struct spa_varlink *varlink,
|
||||
const char *path, const char *device, const char *route)
|
||||
{
|
||||
struct spa_json_builder params_json;
|
||||
struct spa_json reply_json;
|
||||
spa_autofree char *params = NULL;
|
||||
spa_autofree char *reply = NULL;
|
||||
char key[64];
|
||||
const char *str;
|
||||
size_t len;
|
||||
int res;
|
||||
|
||||
pa_log_info("Connecting to external volume control at '%s'", path);
|
||||
|
||||
ext_volume->client = spa_varlink_connect(varlink, path);
|
||||
|
||||
if (!ext_volume->client) {
|
||||
pa_log_error("Could not connect to volume control service at: %s", path);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
spa_varlink_client_add_listener(ext_volume->client, &ext_volume->listener, &client_events, ext_volume);
|
||||
|
||||
spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0);
|
||||
|
||||
spa_json_builder_object_push(¶ms_json, NULL, "{");
|
||||
spa_json_builder_object_string(¶ms_json, "device", device ? device : "");
|
||||
spa_json_builder_pop(¶ms_json, "}");
|
||||
|
||||
spa_json_builder_close(¶ms_json);
|
||||
|
||||
res = spa_varlink_client_call_sync(ext_volume->client, METHOD_GET_CAPABILITIES, params, &reply);
|
||||
|
||||
if (res < 0) {
|
||||
pa_log_error("Failed to query volume control capabilities: %s", spa_strerror(res));
|
||||
return res;
|
||||
}
|
||||
|
||||
spa_json_begin_object(&reply_json, reply, res);
|
||||
|
||||
while ((res = spa_json_object_next(&reply_json, key, sizeof(key), &str)) > 0) {
|
||||
struct spa_json sub;
|
||||
char key2[64];
|
||||
const char *val;
|
||||
int len;
|
||||
bool value;
|
||||
|
||||
if (spa_streq(key, "error")) {
|
||||
pa_log_error("Error reading volume control capabilities: %s", str);
|
||||
res = -EINVAL;
|
||||
goto done;
|
||||
}
|
||||
|
||||
if (!spa_streq(key, "parameters"))
|
||||
continue;
|
||||
|
||||
len = spa_json_container_len(&reply_json, str, res);
|
||||
spa_json_begin_object(&sub, str, len);
|
||||
|
||||
while ((len = spa_json_object_next(&sub, key2, sizeof(key2), &val)) > 0) {
|
||||
if (spa_streq(key2, "readVolume") && spa_json_parse_bool(val, len, &value) && value)
|
||||
ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME;
|
||||
if (spa_streq(key2, "writeVolumeAbsolute") && spa_json_parse_bool(val, len, &value) && value)
|
||||
ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_VALUE;
|
||||
if (spa_streq(key2, "writeVolumeRelative") && spa_json_parse_bool(val, len, &value) && value)
|
||||
ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_UPDOWN;
|
||||
if (spa_streq(key2, "readMute") && spa_json_parse_bool(val, len, &value) && value)
|
||||
ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_READ_MUTE;
|
||||
if (spa_streq(key2, "writeMuteValue") && spa_json_parse_bool(val, len, &value) && value)
|
||||
ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_VALUE;
|
||||
if (spa_streq(key2, "writeMuteToggle") && spa_json_parse_bool(val, len, &value) && value)
|
||||
ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_TOGGLE;
|
||||
if (spa_streq(key2, "readBalance") && spa_json_parse_bool(val, len, &value) && value)
|
||||
ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_READ_BALANCE;
|
||||
if (spa_streq(key2, "writeBalance") && spa_json_parse_bool(val, len, &value) && value)
|
||||
ext_volume->flags |= SPA_AUDIO_VOLUME_CONTROL_WRITE_BALANCE;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
res = 0;
|
||||
done:
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
spa_acp_ext_volume_destroy(struct spa_acp_ext_volume *ext_volume)
|
||||
{
|
||||
if (ext_volume->client) {
|
||||
spa_varlink_client_destroy(ext_volume->client);
|
||||
ext_volume->client = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
spa_acp_ext_volume_read_volume(struct spa_acp_ext_volume *ext_volume,
|
||||
const char *device, const char *route, pa_cvolume *cvol)
|
||||
{
|
||||
struct spa_json_builder params_json;
|
||||
struct spa_json reply_json;
|
||||
spa_autofree char *params = NULL;
|
||||
spa_autofree char *reply = NULL;
|
||||
char key[64];
|
||||
const char *str;
|
||||
size_t len;
|
||||
int res;
|
||||
|
||||
spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0);
|
||||
|
||||
spa_json_builder_object_push(¶ms_json, NULL, "{");
|
||||
spa_json_builder_object_string(¶ms_json, "device", device ? device : "");
|
||||
spa_json_builder_object_string(¶ms_json, "route", route ? route : "");
|
||||
spa_json_builder_pop(¶ms_json, "}");
|
||||
|
||||
spa_json_builder_close(¶ms_json);
|
||||
|
||||
res = spa_varlink_client_call_sync(ext_volume->client, METHOD_READ_VOLUME, params, &reply);
|
||||
|
||||
if (res < 0) {
|
||||
pa_log_error("Failed to query volume : %s", spa_strerror(res));
|
||||
return res;
|
||||
}
|
||||
|
||||
spa_json_begin_object(&reply_json, reply, res);
|
||||
|
||||
while ((res = spa_json_object_next(&reply_json, key, sizeof(key), &str)) > 0) {
|
||||
struct spa_json sub = {0,}, volume_json;
|
||||
float v = 0;
|
||||
|
||||
if (spa_streq(key, "error")) {
|
||||
pa_log_error("Error reading volume: %s", str);
|
||||
res = -EINVAL;
|
||||
goto done;
|
||||
}
|
||||
|
||||
if (!spa_streq(key, "parameters"))
|
||||
continue;
|
||||
|
||||
if (spa_json_enter_object(&reply_json, &sub) < 0) {
|
||||
pa_log_error("Could not read volume parameters");
|
||||
res = -EINVAL;
|
||||
goto done;
|
||||
}
|
||||
|
||||
res = spa_json_object_find(&sub, "volume", &str);
|
||||
|
||||
if (res < 0) {
|
||||
pa_log_error("Could not read volume");
|
||||
res = -EINVAL;
|
||||
goto done;
|
||||
}
|
||||
|
||||
spa_json_enter_array(&sub, &volume_json);
|
||||
|
||||
cvol->channels = 0;
|
||||
while (spa_json_get_float(&volume_json, &v) && cvol->channels < PA_CHANNELS_MAX) {
|
||||
cvol->values[cvol->channels++] = lrint(v * PA_VOLUME_NORM);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
res = 0;
|
||||
done:
|
||||
return res;
|
||||
}
|
||||
|
||||
int
|
||||
spa_acp_ext_volume_write_volume_absolute(struct spa_acp_ext_volume *ext_volume,
|
||||
const char *device, const char *route, pa_cvolume *cvol)
|
||||
{
|
||||
struct spa_json_builder params_json;
|
||||
spa_autofree char *params = NULL;
|
||||
size_t len;
|
||||
int i, res;
|
||||
|
||||
spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0);
|
||||
|
||||
spa_json_builder_object_push(¶ms_json, NULL, "{");
|
||||
spa_json_builder_object_string(¶ms_json, "device", device ? device : "");
|
||||
spa_json_builder_object_string(¶ms_json, "route", route ? route : "");
|
||||
|
||||
spa_json_builder_object_push(¶ms_json, "volume", "[");
|
||||
|
||||
/* FIXME: scale to the range from capabilities */
|
||||
if (ext_volume->flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_BALANCE) {
|
||||
/* Write all channels */
|
||||
for (i = 0; i < (int)cvol->channels; i++)
|
||||
spa_json_builder_array_double(¶ms_json,
|
||||
(double) cvol->values[i] / PA_VOLUME_NORM);
|
||||
} else {
|
||||
/* Single volume */
|
||||
spa_json_builder_array_double(¶ms_json,
|
||||
(double) pa_cvolume_max(cvol) / PA_VOLUME_NORM);
|
||||
}
|
||||
|
||||
spa_json_builder_pop(¶ms_json, "]");
|
||||
spa_json_builder_pop(¶ms_json, "}");
|
||||
spa_json_builder_close(¶ms_json);
|
||||
|
||||
res = spa_varlink_client_call(ext_volume->client, METHOD_WRITE_VOLUME_ABSOLUTE,
|
||||
params, false, false, check_reply, METHOD_WRITE_VOLUME_ABSOLUTE);
|
||||
|
||||
if (res < 0) {
|
||||
pa_log_error("Failed to write volume: %s", spa_strerror(res));
|
||||
return res;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
int
|
||||
spa_acp_ext_volume_write_volume_relative(struct spa_acp_ext_volume *ext_volume,
|
||||
const char *device, const char *route, float step)
|
||||
{
|
||||
struct spa_json_builder params_json;
|
||||
spa_autofree char *params = NULL;
|
||||
size_t len;
|
||||
int res;
|
||||
|
||||
spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0);
|
||||
|
||||
spa_json_builder_object_push(¶ms_json, NULL, "{");
|
||||
spa_json_builder_object_string(¶ms_json, "device", device ? device : "");
|
||||
spa_json_builder_object_string(¶ms_json, "route", route ? route : "");
|
||||
spa_json_builder_object_double(¶ms_json, "step", step);
|
||||
spa_json_builder_pop(¶ms_json, "}");
|
||||
|
||||
spa_json_builder_close(¶ms_json);
|
||||
|
||||
res = spa_varlink_client_call(ext_volume->client, METHOD_WRITE_VOLUME_RELATIVE,
|
||||
params, false, false, check_reply, METHOD_WRITE_VOLUME_RELATIVE);
|
||||
|
||||
if (res < 0) {
|
||||
pa_log_error("Failed to write volume: %s", spa_strerror(res));
|
||||
return res;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
int
|
||||
spa_acp_ext_volume_read_mute(struct spa_acp_ext_volume *ext_volume,
|
||||
const char *device, const char *route, bool *mute)
|
||||
{
|
||||
struct spa_json_builder params_json;
|
||||
struct spa_json reply_json;
|
||||
spa_autofree char *params = NULL;
|
||||
spa_autofree char *reply = NULL;
|
||||
char key[64];
|
||||
const char *str;
|
||||
size_t len;
|
||||
int res;
|
||||
|
||||
spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0);
|
||||
|
||||
spa_json_builder_object_push(¶ms_json, NULL, "{");
|
||||
spa_json_builder_object_string(¶ms_json, "device", device ? device : "");
|
||||
spa_json_builder_object_string(¶ms_json, "route", route ? route : "");
|
||||
spa_json_builder_pop(¶ms_json, "}");
|
||||
|
||||
spa_json_builder_close(¶ms_json);
|
||||
|
||||
res = spa_varlink_client_call_sync(ext_volume->client, METHOD_READ_MUTE, params, &reply);
|
||||
|
||||
if (res < 0) {
|
||||
pa_log_error("Failed to query mute state: %s", spa_strerror(res));
|
||||
return res;
|
||||
}
|
||||
|
||||
spa_json_begin_object(&reply_json, reply, res);
|
||||
|
||||
while ((res = spa_json_object_next(&reply_json, key, sizeof(key), &str)) > 0) {
|
||||
struct spa_json sub = { 0, };
|
||||
|
||||
if (spa_streq(key, "error")) {
|
||||
pa_log_error("Error reading mute state: %s", str);
|
||||
res = -EINVAL;
|
||||
goto done;
|
||||
}
|
||||
|
||||
if (!spa_streq(key, "parameters"))
|
||||
continue;
|
||||
|
||||
if (spa_json_enter_object(&reply_json, &sub) < 0) {
|
||||
pa_log_error("Could not read mute parameters");
|
||||
res = -EINVAL;
|
||||
goto done;
|
||||
}
|
||||
|
||||
res = spa_json_object_find(&sub, "mute", &str);
|
||||
|
||||
if (res < 0 || spa_json_get_bool(&reply_json, mute) < 0) {
|
||||
pa_log_error("Could not read mute");
|
||||
res = -EINVAL;
|
||||
goto done;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
res = 0;
|
||||
done:
|
||||
return res;
|
||||
}
|
||||
|
||||
int
|
||||
spa_acp_ext_volume_write_mute_value(struct spa_acp_ext_volume *ext_volume,
|
||||
const char *device, const char *route, bool mute)
|
||||
{
|
||||
struct spa_json_builder params_json;
|
||||
spa_autofree char *params = NULL;
|
||||
size_t len;
|
||||
int res;
|
||||
|
||||
spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0);
|
||||
|
||||
spa_json_builder_object_push(¶ms_json, NULL, "{");
|
||||
spa_json_builder_object_string(¶ms_json, "device", device ? device : "");
|
||||
spa_json_builder_object_string(¶ms_json, "route", route ? route : "");
|
||||
spa_json_builder_object_bool(¶ms_json, "mute", mute);
|
||||
spa_json_builder_pop(¶ms_json, "}");
|
||||
|
||||
spa_json_builder_close(¶ms_json);
|
||||
|
||||
res = spa_varlink_client_call(ext_volume->client, METHOD_WRITE_MUTE_VALUE,
|
||||
params, false, false, check_reply, METHOD_WRITE_MUTE_VALUE);
|
||||
|
||||
if (res < 0) {
|
||||
pa_log_error("Failed to write mute state: %s", spa_strerror(res));
|
||||
return res;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
int
|
||||
spa_acp_ext_volume_write_mute_toggle(struct spa_acp_ext_volume *ext_volume,
|
||||
const char *device, const char *route)
|
||||
{
|
||||
struct spa_json_builder params_json;
|
||||
spa_autofree char *params = NULL;
|
||||
size_t len;
|
||||
int res;
|
||||
|
||||
spa_json_builder_memstream(¶ms_json, ¶ms, &len, 0);
|
||||
|
||||
spa_json_builder_object_push(¶ms_json, NULL, "{");
|
||||
spa_json_builder_object_string(¶ms_json, "device", device ? device : "");
|
||||
spa_json_builder_object_string(¶ms_json, "route", route ? route : "");
|
||||
spa_json_builder_pop(¶ms_json, "}");
|
||||
|
||||
spa_json_builder_close(¶ms_json);
|
||||
|
||||
res = spa_varlink_client_call(ext_volume->client, METHOD_WRITE_MUTE_TOGGLE,
|
||||
params, false, false, check_reply, METHOD_WRITE_MUTE_TOGGLE);
|
||||
|
||||
if (res < 0) {
|
||||
pa_log_error("Failed to write mute toggle: %s", spa_strerror(res));
|
||||
return res;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
47
spa/plugins/alsa/acp/ext-volume.h
Normal file
47
spa/plugins/alsa/acp/ext-volume.h
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* External Volume Control */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */
|
||||
/* 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/support/varlink.h>
|
||||
|
||||
typedef struct pa_cvolume pa_cvolume;
|
||||
|
||||
struct acp_card;
|
||||
|
||||
struct spa_acp_ext_volume {
|
||||
struct spa_varlink_client *client;
|
||||
struct spa_hook listener;
|
||||
|
||||
enum spa_audio_volume_control_flags flags;
|
||||
};
|
||||
|
||||
int spa_acp_ext_volume_init(struct spa_acp_ext_volume *ext_volume, struct spa_varlink *varlink,
|
||||
const char *path, const char *device, const char *route);
|
||||
|
||||
void spa_acp_ext_volume_destroy(struct spa_acp_ext_volume *ext_volume);
|
||||
|
||||
int spa_acp_ext_volume_read_volume(struct spa_acp_ext_volume *ext_volume,
|
||||
const char *device, const char *route, pa_cvolume *cvol);
|
||||
|
||||
int spa_acp_ext_volume_write_volume_absolute(struct spa_acp_ext_volume *ext_volume,
|
||||
const char *device, const char *route, pa_cvolume *cvol);
|
||||
|
||||
int spa_acp_ext_volume_write_volume_relative(struct spa_acp_ext_volume *ext_volume,
|
||||
const char *device, const char *route, float step);
|
||||
|
||||
int spa_acp_ext_volume_read_mute(struct spa_acp_ext_volume *ext_volume,
|
||||
const char *device, const char *route, bool *mute);
|
||||
|
||||
int spa_acp_ext_volume_write_mute_value(struct spa_acp_ext_volume *ext_volume,
|
||||
const char *device, const char *route, bool mute);
|
||||
|
||||
int spa_acp_ext_volume_write_mute_toggle(struct spa_acp_ext_volume *ext_volume,
|
||||
const char *device, const char *route);
|
||||
|
||||
#endif /* ACP_EXT_VOLUME_H */
|
||||
|
|
@ -5,6 +5,7 @@ acp_sources = [
|
|||
'alsa-ucm.c',
|
||||
'alsa-util.c',
|
||||
'conf-parser.c',
|
||||
'ext-volume.c',
|
||||
]
|
||||
|
||||
acp_c_args = [
|
||||
|
|
@ -17,6 +18,6 @@ acp_lib = static_library(
|
|||
acp_sources,
|
||||
c_args : acp_c_args,
|
||||
include_directories : [configinc, includes_inc ],
|
||||
dependencies : [ spa_dep, alsa_dep, mathlib, ]
|
||||
dependencies : [ spa_dep, alsa_dep, mathlib ]
|
||||
)
|
||||
acp_dep = declare_dependency(link_with: acp_lib)
|
||||
|
|
|
|||
89
spa/plugins/alsa/acp/org.pipewire.ExternalVolume.varlink
Normal file
89
spa/plugins/alsa/acp/org.pipewire.ExternalVolume.varlink
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# Allows an external service to provide volume control for a PipeWire device.
|
||||
interface org.pipewire.ExternalVolume
|
||||
|
||||
# Describes what kind of volume control operations are supported for a given
|
||||
# device.
|
||||
type Capabilities (
|
||||
# Whether the current volume value can be read
|
||||
readVolume: bool,
|
||||
|
||||
# Whether volume values are reported per-channel
|
||||
readBalance: bool,
|
||||
|
||||
# The range of valid volume values and the granularity of steps
|
||||
volumeRange: (
|
||||
min: float,
|
||||
max: float,
|
||||
step: float
|
||||
),
|
||||
|
||||
# Whether the volume can be set as an absolute value
|
||||
writeVolumeAbsolute: bool,
|
||||
|
||||
# Whether volume adjustments can be made relative to the current volume
|
||||
writeVolumeRelative: bool,
|
||||
|
||||
# The size of relative volume adjustments, if known
|
||||
writeVolumeRelativeStep: (min: float, max: float),
|
||||
|
||||
# Whether per-channel volumes can be written
|
||||
writeBalance: bool,
|
||||
|
||||
# Whether the current mute state can be read
|
||||
readMute: bool,
|
||||
|
||||
# Whether the current mute state can be set
|
||||
writeMuteValue: bool,
|
||||
|
||||
# Whether the current mute state can be toggled
|
||||
writeMuteToggle: bool,
|
||||
|
||||
# The known set of routes for the device
|
||||
routes: []string
|
||||
)
|
||||
|
||||
# Query volume control capabilities for the given device.
|
||||
method GetCapabilities(device: string) -> (capabilities: Capabilities)
|
||||
|
||||
# Query the volume for the given device route. If the volume can be read, the
|
||||
# returned value will be an array of floats (if per-channel volumes are not
|
||||
# supported, the array will have one float value).
|
||||
method ReadVolume(device: string, route: string) -> (volume: []float)
|
||||
|
||||
# Query the mute state for the given device route.
|
||||
method ReadMute(device: string, route: string) -> (mute: bool)
|
||||
|
||||
# Monitor changes to volume or mute state. Volume changes will be signalled by
|
||||
# a non-empty volume array (with a single value if per-channel volumes are not
|
||||
# supported). Mute state changes will be signalled by a non-null mute value.
|
||||
method Monitor(device: string) -> (
|
||||
route: string,
|
||||
volume: []float,
|
||||
mute: ?bool
|
||||
)
|
||||
|
||||
# Set the volume of the given device route. If supported, the provided value
|
||||
# can be an array of per-channel float values. If per-channel volumes are not
|
||||
# supported, the array should consist of a single value.
|
||||
method WriteVolumeAbsolute(
|
||||
device: string,
|
||||
route: string,
|
||||
volume: []float
|
||||
) -> ()
|
||||
|
||||
# Increase or decrease the volume of the device route by the given amount.
|
||||
method WriteVolumeRelative(
|
||||
device: string,
|
||||
route: string,
|
||||
step: float
|
||||
) -> ()
|
||||
|
||||
# Set the mute state for the given device route.
|
||||
method WriteMuteValue(
|
||||
device: string,
|
||||
route: string,
|
||||
mute: bool
|
||||
) -> ()
|
||||
|
||||
# Toggle the mute state for the given device route.
|
||||
method WriteMuteToggle(device: string, route: string) -> ()
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
#include <spa/support/loop.h>
|
||||
#include <spa/support/plugin.h>
|
||||
#include <spa/support/i18n.h>
|
||||
#include <spa/support/varlink.h>
|
||||
#include <spa/monitor/device.h>
|
||||
#include <spa/monitor/utils.h>
|
||||
#include <spa/monitor/event.h>
|
||||
|
|
@ -36,6 +37,7 @@
|
|||
#include "acp/acp.h"
|
||||
|
||||
extern struct spa_i18n *acp_i18n;
|
||||
extern struct spa_varlink *acp_varlink;
|
||||
|
||||
#define MAX_POLL 16
|
||||
#define MAX_CHANNELS SPA_AUDIO_MAX_CHANNELS
|
||||
|
|
@ -63,6 +65,7 @@ struct impl {
|
|||
|
||||
struct spa_log *log;
|
||||
struct spa_loop *loop;
|
||||
struct spa_varlink *varlink;
|
||||
|
||||
uint32_t info_all;
|
||||
struct spa_device_info info;
|
||||
|
|
@ -490,6 +493,11 @@ static struct spa_pod *build_route(struct spa_pod_builder *b, uint32_t id,
|
|||
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_prop(b, SPA_PARAM_ROUTE_devices, 0);
|
||||
|
|
@ -885,12 +893,74 @@ static int impl_set_param(void *object,
|
|||
return 0;
|
||||
}
|
||||
|
||||
static int impl_send_command(void *object, const struct spa_command *command)
|
||||
{
|
||||
struct impl *this = object;
|
||||
struct acp_card *card = this->card;
|
||||
|
||||
spa_return_val_if_fail(this != NULL, -EINVAL);
|
||||
spa_return_val_if_fail(command != NULL, -EINVAL);
|
||||
|
||||
switch (SPA_DEVICE_COMMAND_ID(command)) {
|
||||
case SPA_DEVICE_COMMAND_VolumeControl:
|
||||
{
|
||||
const struct spa_pod_prop *prop;
|
||||
|
||||
/* We support some volume control commands for external volumes */
|
||||
if (card->ext_volume.client == NULL)
|
||||
return -ENOTSUP;
|
||||
|
||||
SPA_POD_OBJECT_FOREACH(&command->body, prop) {
|
||||
struct acp_port *port;
|
||||
struct acp_device *dev = NULL;
|
||||
uint32_t id, i;
|
||||
|
||||
if (spa_pod_get_id(&prop->value, &id) < 0 || id > card->n_ports)
|
||||
return -EINVAL;
|
||||
|
||||
port = card->ports[id];
|
||||
|
||||
for (i = 0; i < port->n_devices; i++) {
|
||||
if (port->devices[i]->flags & ACP_DEVICE_ACTIVE) {
|
||||
dev = port->devices[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dev == NULL)
|
||||
return -EINVAL;
|
||||
|
||||
switch (prop->key) {
|
||||
case SPA_COMMAND_DEVICE_volumeUp:
|
||||
acp_device_set_volume_updown(dev, true);
|
||||
break;
|
||||
case SPA_COMMAND_DEVICE_volumeDown:
|
||||
acp_device_set_volume_updown(dev, false);
|
||||
break;
|
||||
case SPA_COMMAND_DEVICE_muteToggle:
|
||||
acp_device_toggle_mute(dev);
|
||||
break;
|
||||
default:
|
||||
return -ENOTSUP;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct spa_device_methods impl_device = {
|
||||
SPA_VERSION_DEVICE_METHODS,
|
||||
.add_listener = impl_add_listener,
|
||||
.sync = impl_sync,
|
||||
.enum_params = impl_enum_params,
|
||||
.set_param = impl_set_param,
|
||||
.send_command = impl_send_command,
|
||||
};
|
||||
|
||||
static void card_props_changed(void *data)
|
||||
|
|
@ -1152,6 +1222,7 @@ impl_init(const struct spa_handle_factory *factory,
|
|||
|
||||
this->loop = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Loop);
|
||||
acp_i18n = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_I18N);
|
||||
acp_varlink = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Varlink);
|
||||
if (this->loop == NULL) {
|
||||
spa_log_error(this->log, "a Loop interface is needed");
|
||||
return -EINVAL;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,17 @@ spa_support_lib = shared_library('spa-support',
|
|||
install_dir : spa_plugindir / 'support')
|
||||
spa_support_dep = declare_dependency(link_with: spa_support_lib)
|
||||
|
||||
spa_varlink_sources = [
|
||||
'varlink.c'
|
||||
]
|
||||
|
||||
spa_varlink_lib = shared_library('spa-varlink',
|
||||
spa_varlink_sources,
|
||||
dependencies : [ spa_dep ],
|
||||
install : true,
|
||||
install_dir : spa_plugindir / 'support')
|
||||
spa_varlink_dep = declare_dependency(link_with: spa_varlink_lib)
|
||||
|
||||
if get_option('evl').allowed()
|
||||
evl_inc = include_directories('/usr/include')
|
||||
evl_lib = cc.find_library('evl',
|
||||
|
|
|
|||
623
spa/plugins/support/varlink.c
Normal file
623
spa/plugins/support/varlink.c
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
/* Spa Varlink plugin */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#include <spa/support/log.h>
|
||||
#include <spa/support/loop.h>
|
||||
#include <spa/support/plugin.h>
|
||||
#include <spa/support/varlink.h>
|
||||
#include <spa/utils/defs.h>
|
||||
#include <spa/utils/hook.h>
|
||||
#include <spa/utils/json-builder.h>
|
||||
#include <spa/utils/list.h>
|
||||
#include <spa/utils/names.h>
|
||||
#include <spa/utils/result.h>
|
||||
|
||||
SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.varlink");
|
||||
|
||||
#undef SPA_LOG_TOPIC_DEFAULT
|
||||
#define SPA_LOG_TOPIC_DEFAULT &log_topic
|
||||
|
||||
struct impl {
|
||||
struct spa_handle handle;
|
||||
struct spa_varlink varlink;
|
||||
|
||||
struct spa_log *log;
|
||||
struct spa_system *system;
|
||||
struct spa_loop_utils *loop_utils;
|
||||
|
||||
struct spa_list clients;
|
||||
};
|
||||
|
||||
enum spa_varlink_transport {
|
||||
SPA_VARLINK_TRANSPORT_UNIX,
|
||||
};
|
||||
|
||||
#define BUFFER_SIZE 16384
|
||||
|
||||
struct client {
|
||||
struct spa_list link;
|
||||
struct spa_varlink_client client;
|
||||
|
||||
struct impl *impl; /* The owning impl */
|
||||
int fd; /* The socket fd */
|
||||
struct spa_source *source; /* I/O source if we have a call_more() call */
|
||||
bool reply_pending; /* A call was made for which a reply is pending */
|
||||
bool more_pending; /* A call with more=true was made, and a reply is pending */
|
||||
|
||||
spa_varlink_reply_func_t cb; /* Callback for the next reply we receive */
|
||||
void *cb_userdata;
|
||||
|
||||
struct spa_hook_list listeners;
|
||||
|
||||
size_t pos;
|
||||
size_t avail;
|
||||
char buf[BUFFER_SIZE];
|
||||
};
|
||||
|
||||
static const char *varlink_parse_path(const char *path,
|
||||
enum spa_varlink_transport *transport)
|
||||
{
|
||||
if (spa_strstartswith(path, "unix:")) {
|
||||
*transport = SPA_VARLINK_TRANSPORT_UNIX;
|
||||
return &path[5];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
client_add_listener(void *object, struct spa_hook *listener,
|
||||
const struct spa_varlink_client_events *events, void *data)
|
||||
{
|
||||
struct client *this = SPA_CONTAINER_OF(object, struct client, client);
|
||||
spa_hook_list_append(&this->listeners, listener, events, data);
|
||||
}
|
||||
|
||||
static char *
|
||||
build_call(const char *method, const char *params, bool oneway, bool more,
|
||||
size_t *len)
|
||||
{
|
||||
struct spa_json_builder b;
|
||||
char *json;
|
||||
|
||||
spa_json_builder_memstream(&b, &json, len, 0);
|
||||
|
||||
/* Top-level object */
|
||||
spa_json_builder_object_push(&b, NULL, "{");
|
||||
|
||||
spa_json_builder_object_string(&b, "method", method);
|
||||
spa_json_builder_object_value(&b, true, "parameters", params);
|
||||
spa_json_builder_object_bool(&b, "oneway", oneway);
|
||||
spa_json_builder_object_bool(&b, "more", more);
|
||||
|
||||
spa_json_builder_pop(&b, "}");
|
||||
|
||||
spa_json_builder_close(&b);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
static int
|
||||
client_call(void *object, const char *method, const char *params,
|
||||
bool oneway, bool more, spa_varlink_reply_func_t cb,
|
||||
void *userdata)
|
||||
{
|
||||
struct client *this = SPA_CONTAINER_OF(object, struct client, client);
|
||||
struct impl *impl = this->impl;
|
||||
char *buf;
|
||||
size_t len, pos = 0;
|
||||
int res;
|
||||
|
||||
if (this->reply_pending) {
|
||||
res = -EINVAL;
|
||||
goto done;
|
||||
}
|
||||
|
||||
buf = build_call(method, params, oneway, more, &len);
|
||||
|
||||
spa_log_trace(impl->log, "sending message: %s", buf);
|
||||
|
||||
/* We write the whole string including the NULL terminator */
|
||||
do {
|
||||
res = spa_system_write(this->impl->system, this->fd, &buf[pos], len - pos + 1);
|
||||
pos += res;
|
||||
} while ((res > 0 && len >= pos) || res == -EAGAIN || res == -EWOULDBLOCK);
|
||||
|
||||
free(buf);
|
||||
|
||||
if (res < 0) {
|
||||
spa_log_error(impl->log, "Error writing message: %s",
|
||||
spa_strerror(res));
|
||||
goto done;
|
||||
}
|
||||
|
||||
if (!oneway) {
|
||||
this->reply_pending = true;
|
||||
this->cb = cb;
|
||||
this->cb_userdata = userdata;
|
||||
}
|
||||
if (more)
|
||||
this->more_pending = true;
|
||||
|
||||
res = 0;
|
||||
|
||||
done:
|
||||
return res;
|
||||
}
|
||||
|
||||
static int
|
||||
client_call_sync(void *object, const char *method, const char *params,
|
||||
char **reply)
|
||||
{
|
||||
struct client *this = SPA_CONTAINER_OF(object, struct client, client);
|
||||
struct impl *impl = this->impl;
|
||||
char *buf;
|
||||
size_t len, pos = 0;
|
||||
int res = 0;
|
||||
|
||||
if (this->reply_pending || this->more_pending ||
|
||||
(this->pos != 0 && this->avail != 0)) {
|
||||
spa_log_error(impl->log, "Invalid state for sync call");
|
||||
res = -EINVAL;
|
||||
}
|
||||
|
||||
if (res < 0)
|
||||
return res;
|
||||
|
||||
spa_loop_utils_update_io(impl->loop_utils, this->source,
|
||||
this->source->mask & ~SPA_IO_IN);
|
||||
|
||||
fcntl(this->fd, F_SETFL, fcntl(this->fd, F_GETFL) & ~O_NONBLOCK);
|
||||
|
||||
buf = build_call(method, params, false, false, &len);
|
||||
|
||||
spa_log_trace(impl->log, "sending message: %s", buf);
|
||||
/* We write the whole string including the NULL terminator */
|
||||
do {
|
||||
res = spa_system_write(this->impl->system, this->fd, &buf[pos], len - pos + 1);
|
||||
pos += res;
|
||||
} while (res > 0 && len >= pos);
|
||||
|
||||
free(buf);
|
||||
|
||||
if (res < 0) {
|
||||
spa_log_error(impl->log, "Error writing message: %s",
|
||||
spa_strerror(res));
|
||||
goto done;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
size_t eom;
|
||||
|
||||
res = spa_system_read(impl->system, this->fd, &this->buf[this->avail],
|
||||
BUFFER_SIZE - this->avail);
|
||||
if (res <= 0) {
|
||||
if (res == -EINTR)
|
||||
continue;
|
||||
|
||||
if (res == -EAGAIN || res == -EWOULDBLOCK)
|
||||
continue;
|
||||
|
||||
spa_log_error(impl->log, "Error reading from socket: %s",
|
||||
spa_strerror(res));
|
||||
|
||||
goto done;
|
||||
}
|
||||
|
||||
spa_log_debug(impl->log, "Got %d bytes (avail: %zu)", res, this->avail);
|
||||
this->avail += res;
|
||||
|
||||
for (eom = 0; eom < this->avail; eom++) {
|
||||
if (this->buf[eom] == '\0')
|
||||
break;
|
||||
}
|
||||
|
||||
/* We need more data */
|
||||
if (eom == this->avail)
|
||||
continue;
|
||||
|
||||
if (eom != this->avail - 1)
|
||||
spa_log_warn(impl->log, "Received more than one reply");
|
||||
|
||||
spa_log_debug(impl->log, "Consuming %zu bytes", eom + 1);
|
||||
|
||||
res = eom;
|
||||
*reply = strndup(this->buf, res + 1);
|
||||
|
||||
this->pos = this->avail = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
done:
|
||||
fcntl(this->fd, F_SETFL, fcntl(this->fd, F_GETFL) | O_NONBLOCK);
|
||||
|
||||
spa_loop_utils_update_io(impl->loop_utils, this->source,
|
||||
this->source->mask | SPA_IO_IN);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static void
|
||||
client_disconnect(struct client *this)
|
||||
{
|
||||
if (this->source) {
|
||||
spa_loop_utils_destroy_source(this->impl->loop_utils, this->source);
|
||||
this->source = NULL;
|
||||
|
||||
spa_system_close(this->impl->system, this->fd);
|
||||
|
||||
spa_hook_list_call(&this->listeners, struct spa_varlink_client_events,
|
||||
disconnect, SPA_VERSION_VARLINK_CLIENT_EVENTS);
|
||||
}
|
||||
}
|
||||
|
||||
static void do_destroy(struct client *client)
|
||||
{
|
||||
client_disconnect(client);
|
||||
|
||||
spa_hook_list_call(&client->listeners, struct spa_varlink_client_events,
|
||||
destroy, SPA_VERSION_VARLINK_CLIENT_EVENTS);
|
||||
|
||||
spa_hook_list_clean(&client->listeners);
|
||||
}
|
||||
|
||||
static void
|
||||
client_destroy(void *object)
|
||||
{
|
||||
struct client *this = SPA_CONTAINER_OF(object, struct client, client);
|
||||
do_destroy(this);
|
||||
}
|
||||
|
||||
static void
|
||||
process_message(struct client *this, const char *msg, size_t len)
|
||||
{
|
||||
struct impl *impl = this->impl;
|
||||
struct spa_json json;
|
||||
char key[64];
|
||||
const char *val, *error = NULL, *params = NULL;
|
||||
int res;
|
||||
bool continues = false;
|
||||
|
||||
spa_log_trace(impl->log, "got message: %.*s", (int)len, msg);
|
||||
|
||||
spa_json_begin_object(&json, msg, len);
|
||||
|
||||
while ((res = spa_json_object_next(&json, key, sizeof(key), &val)) > 0) {
|
||||
if (spa_streq(key, "error")) {
|
||||
len = spa_json_container_len(&json, val, res);
|
||||
error = val;
|
||||
break;
|
||||
}
|
||||
|
||||
if (spa_streq(key, "continues")) {
|
||||
res = spa_json_get_bool(&json, &continues);
|
||||
if (res < 0)
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (spa_streq(key, "parameters")) {
|
||||
len = spa_json_container_len(&json, val, res);
|
||||
params = val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->cb)
|
||||
this->cb(this->cb_userdata, params, error, len, continues);
|
||||
|
||||
if (!this->more_pending || !continues) {
|
||||
/* We didn't ask for more, but server might send */
|
||||
if (continues)
|
||||
spa_log_warn(impl->log, "Unexpected continues");
|
||||
|
||||
this->reply_pending = false;
|
||||
this->more_pending = false;
|
||||
this->cb = NULL;
|
||||
this->cb_userdata = NULL;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
error:
|
||||
spa_log_error(impl->log, "Could not parse: %.*s", (int)len, msg);
|
||||
}
|
||||
|
||||
static void
|
||||
client_on_reply(void *userdata, int fd, uint32_t mask)
|
||||
{
|
||||
struct client *this = (struct client *)userdata;
|
||||
struct impl *impl = this->impl;
|
||||
int res;
|
||||
|
||||
if (mask & SPA_IO_HUP) {
|
||||
spa_log_info(impl->log, "Got hangup event");
|
||||
client_disconnect(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mask & SPA_IO_ERR) {
|
||||
spa_log_info(impl->log, "Got error event");
|
||||
client_disconnect(this);
|
||||
return;
|
||||
}
|
||||
|
||||
spa_log_debug(impl->log, "Ready to read");
|
||||
|
||||
/* SPA_IO_IN */
|
||||
while (this->pos <= this->avail) {
|
||||
size_t eom;
|
||||
|
||||
res = spa_system_read(impl->system, fd, &this->buf[this->avail],
|
||||
BUFFER_SIZE - this->avail);
|
||||
if (res <= 0) {
|
||||
if (res == -EINTR)
|
||||
continue;
|
||||
|
||||
if (res == 0 || res == -EAGAIN || res == -EWOULDBLOCK)
|
||||
return;
|
||||
|
||||
spa_log_error(impl->log, "Error reading from socket: %s",
|
||||
spa_strerror(res));
|
||||
client_disconnect(this);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
spa_log_debug(impl->log, "Got %d bytes (avail: %zu)", res, this->avail);
|
||||
this->avail += res;
|
||||
|
||||
for (eom = this->pos; eom < this->avail; eom++) {
|
||||
if (this->buf[eom] == '\0')
|
||||
break;
|
||||
}
|
||||
|
||||
if (eom == this->avail) {
|
||||
if (eom == BUFFER_SIZE) {
|
||||
spa_log_error(impl->log, "Message larger than available buffer");
|
||||
client_disconnect(this);
|
||||
}
|
||||
|
||||
spa_log_debug(impl->log, "Need more data");
|
||||
return;
|
||||
}
|
||||
|
||||
process_message(this, &this->buf[this->pos], eom - this->pos);
|
||||
|
||||
spa_log_debug(impl->log, "Consumed %zu bytes", eom + 1 - this->pos);
|
||||
|
||||
/* Consume message so far and NUL terminator */
|
||||
this->pos = eom + 1;
|
||||
|
||||
if (this->pos == this->avail) {
|
||||
/* All data has been consumed, reset */
|
||||
this->pos = this->avail = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
static const struct spa_varlink_client impl_client = {
|
||||
SPA_VERSION_VARLINK_CLIENT,
|
||||
.add_listener = client_add_listener,
|
||||
.call = client_call,
|
||||
.call_sync = client_call_sync,
|
||||
.destroy = client_destroy,
|
||||
};
|
||||
|
||||
static struct spa_varlink_client *
|
||||
impl_connect(void *object, const char *path)
|
||||
{
|
||||
struct impl *this = object;
|
||||
struct client *client;
|
||||
const char *socket_path;
|
||||
struct sockaddr_un sa;
|
||||
enum spa_varlink_transport transport;
|
||||
int res = 0;
|
||||
|
||||
client = calloc(1, sizeof(struct client));
|
||||
if (client == NULL)
|
||||
return NULL;
|
||||
|
||||
socket_path = varlink_parse_path(path, &transport);
|
||||
/* We only support UNIX sockets for now */
|
||||
if (socket_path == NULL || transport != SPA_VARLINK_TRANSPORT_UNIX) {
|
||||
spa_log_error(this->log, "Could not connect to socket path '%s'",
|
||||
path);
|
||||
goto error;
|
||||
}
|
||||
|
||||
client->fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
||||
if (client->fd < 0) {
|
||||
spa_log_error(this->log, "Could not create socket %s",
|
||||
spa_strerror(res));
|
||||
goto error;
|
||||
}
|
||||
|
||||
sa.sun_family = AF_UNIX;
|
||||
res = snprintf(sa.sun_path, sizeof(sa.sun_path), "%s", socket_path);
|
||||
if (res < 0 || res > (int) sizeof(sa.sun_path)) {
|
||||
spa_log_error(this->log, "Socket path too long: %s", socket_path);
|
||||
goto error;
|
||||
}
|
||||
|
||||
res = connect(client->fd, (struct sockaddr *)&sa, sizeof(sa));
|
||||
if (res < 0) {
|
||||
spa_log_error(this->log, "Could not open socket %s: %s",
|
||||
socket_path, spa_strerror(res));
|
||||
goto error;
|
||||
}
|
||||
|
||||
client->client = impl_client;
|
||||
client->impl = this;
|
||||
client->pos = 0;
|
||||
client->avail = 0;
|
||||
client->reply_pending = false;
|
||||
client->more_pending = false;
|
||||
client->cb = NULL;
|
||||
client->cb_userdata = NULL;
|
||||
spa_hook_list_init(&client->listeners);
|
||||
|
||||
client->source = spa_loop_utils_add_io(this->loop_utils, client->fd,
|
||||
SPA_IO_IN | SPA_IO_ERR | SPA_IO_HUP, false,
|
||||
client_on_reply, client);
|
||||
if (client->source == NULL) {
|
||||
spa_log_error(this->log, "Could not create source");
|
||||
goto error;
|
||||
}
|
||||
|
||||
spa_list_append(&this->clients, &client->link);
|
||||
|
||||
spa_log_debug(this->log, "new client %p", client);
|
||||
|
||||
return &client->client;
|
||||
|
||||
error:
|
||||
free(client);
|
||||
errno = -res;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const struct spa_varlink_methods impl_varlink = {
|
||||
SPA_VERSION_VARLINK_METHODS,
|
||||
.connect = impl_connect,
|
||||
};
|
||||
|
||||
static int
|
||||
impl_get_interface(struct spa_handle *handle, const char *type,
|
||||
void **interface)
|
||||
{
|
||||
struct impl *this;
|
||||
|
||||
spa_return_val_if_fail(handle != NULL, -EINVAL);
|
||||
spa_return_val_if_fail(interface != NULL, -EINVAL);
|
||||
|
||||
this = (struct impl *) handle;
|
||||
|
||||
if (spa_streq(type, SPA_TYPE_INTERFACE_Varlink))
|
||||
*interface = &this->varlink;
|
||||
else
|
||||
return -ENOENT;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int impl_clear(struct spa_handle *handle)
|
||||
{
|
||||
struct impl *this = (struct impl *) handle;
|
||||
struct client *client;
|
||||
|
||||
spa_return_val_if_fail(handle != NULL, -EINVAL);
|
||||
|
||||
spa_list_consume(client, &this->clients, link)
|
||||
do_destroy(client);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t
|
||||
impl_get_size(const struct spa_handle_factory *factory,
|
||||
const struct spa_dict *params)
|
||||
{
|
||||
return sizeof(struct impl);
|
||||
}
|
||||
|
||||
static int
|
||||
impl_init(const struct spa_handle_factory *factory,
|
||||
struct spa_handle *handle,
|
||||
const struct spa_dict *info,
|
||||
const struct spa_support *support,
|
||||
uint32_t n_support)
|
||||
{
|
||||
struct impl *this;
|
||||
|
||||
spa_return_val_if_fail(factory != NULL, -EINVAL);
|
||||
spa_return_val_if_fail(handle != NULL, -EINVAL);
|
||||
|
||||
handle->get_interface = impl_get_interface;
|
||||
handle->clear = impl_clear;
|
||||
|
||||
this = (struct impl *)handle;
|
||||
spa_list_init(&this->clients);
|
||||
|
||||
this->varlink.iface = SPA_INTERFACE_INIT(
|
||||
SPA_TYPE_INTERFACE_Varlink,
|
||||
SPA_VERSION_VARLINK,
|
||||
&impl_varlink, this);
|
||||
|
||||
this->system = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_System);
|
||||
this->log = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_Log);
|
||||
spa_log_topic_init(this->log, &log_topic);
|
||||
|
||||
this->loop_utils = spa_support_find(support, n_support, SPA_TYPE_INTERFACE_LoopUtils);
|
||||
if (this->loop_utils == NULL) {
|
||||
spa_log_error(this->log, "Loop utils is required for varlink support");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
spa_log_debug(this->log, "%p: varlink iface initialised", this);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct spa_interface_info impl_interfaces[] = {
|
||||
{SPA_TYPE_INTERFACE_Varlink,},
|
||||
};
|
||||
|
||||
static int
|
||||
impl_enum_interface_info(const struct spa_handle_factory *factory,
|
||||
const struct spa_interface_info **info,
|
||||
uint32_t *index)
|
||||
{
|
||||
spa_return_val_if_fail(factory != NULL, -EINVAL);
|
||||
spa_return_val_if_fail(info != NULL, -EINVAL);
|
||||
spa_return_val_if_fail(index != NULL, -EINVAL);
|
||||
|
||||
switch (*index) {
|
||||
case 0:
|
||||
*info = &impl_interfaces[*index];
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
(*index)++;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct spa_handle_factory varlink_factory = {
|
||||
SPA_VERSION_HANDLE_FACTORY,
|
||||
SPA_NAME_SUPPORT_VARLINK,
|
||||
NULL,
|
||||
impl_get_size,
|
||||
impl_init,
|
||||
impl_enum_interface_info,
|
||||
};
|
||||
|
||||
SPA_LOG_TOPIC_ENUM_DEFINE_REGISTERED;
|
||||
|
||||
SPA_EXPORT
|
||||
int spa_handle_factory_enum(const struct spa_handle_factory **factory, uint32_t *index)
|
||||
{
|
||||
spa_return_val_if_fail(factory != NULL, -EINVAL);
|
||||
spa_return_val_if_fail(index != NULL, -EINVAL);
|
||||
|
||||
switch (*index) {
|
||||
case 0:
|
||||
*factory = &varlink_factory;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
(*index)++;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -27,6 +27,17 @@ if find.found()
|
|||
endforeach
|
||||
endif
|
||||
|
||||
utils = [
|
||||
['varlink-call', []],
|
||||
]
|
||||
|
||||
foreach a : utils
|
||||
executable('spa-' + a[0], a[0] + '.c',
|
||||
dependencies : [ spa_dep, dl_lib, pthread_lib, mathlib ] + a[1],
|
||||
include_directories : [configinc],
|
||||
)
|
||||
endforeach
|
||||
|
||||
benchmark_apps = [
|
||||
['stress-ringbuffer', []],
|
||||
['benchmark-pod', []],
|
||||
|
|
|
|||
260
spa/tests/varlink-call.c
Normal file
260
spa/tests/varlink-call.c
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
/* Spa */
|
||||
/* SPDX-FileCopyrightText: Copyright © 2026 Arun Raghavan */
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
|
||||
#include <spa/support/log.h>
|
||||
#include <spa/support/loop.h>
|
||||
#include <spa/support/plugin.h>
|
||||
#include <spa/support/varlink.h>
|
||||
#include <spa/utils/dict.h>
|
||||
#include <spa/utils/hook.h>
|
||||
#include <spa/utils/names.h>
|
||||
#include <spa/utils/json-core.h>
|
||||
#include <spa/utils/result.h>
|
||||
#include <time.h>
|
||||
|
||||
struct data {
|
||||
const char *plugin_dir;
|
||||
|
||||
struct spa_support support[16];
|
||||
uint32_t n_support;
|
||||
struct spa_log *log;
|
||||
struct spa_loop *loop;
|
||||
struct spa_loop_control *loop_control;
|
||||
struct spa_loop_utils *loop_utils;
|
||||
struct spa_system *system;
|
||||
|
||||
struct spa_varlink *varlink;
|
||||
struct spa_varlink_client *client;
|
||||
struct spa_hook listener;
|
||||
|
||||
bool running;
|
||||
};
|
||||
|
||||
static int load_handle(struct data *data, struct spa_handle **handle, const
|
||||
char *lib, const char *name, struct spa_dict *info)
|
||||
{
|
||||
int res;
|
||||
void *hnd;
|
||||
spa_handle_factory_enum_func_t enum_func;
|
||||
uint32_t i;
|
||||
char *path;
|
||||
|
||||
if ((path = spa_aprintf("%s/%s", data->plugin_dir, lib)) == NULL)
|
||||
return -ENOMEM;
|
||||
|
||||
hnd = dlopen(path, RTLD_NOW);
|
||||
free(path);
|
||||
|
||||
if (hnd == NULL) {
|
||||
printf("can't load %s: %s\n", lib, dlerror());
|
||||
return -ENOENT;
|
||||
}
|
||||
if ((enum_func = dlsym(hnd, SPA_HANDLE_FACTORY_ENUM_FUNC_NAME)) == NULL) {
|
||||
printf("can't find enum function\n");
|
||||
res = -ENOENT;
|
||||
goto exit_cleanup;
|
||||
}
|
||||
|
||||
for (i = 0;;) {
|
||||
const struct spa_handle_factory *factory;
|
||||
|
||||
if ((res = enum_func(&factory, &i)) <= 0) {
|
||||
if (res != 0)
|
||||
printf("can't enumerate factories: %s\n", spa_strerror(res));
|
||||
break;
|
||||
}
|
||||
if (factory->version < 1)
|
||||
continue;
|
||||
if (!spa_streq(factory->name, name))
|
||||
continue;
|
||||
|
||||
*handle = calloc(1, spa_handle_factory_get_size(factory, NULL));
|
||||
if ((res = spa_handle_factory_init(factory, *handle,
|
||||
info, data->support,
|
||||
data->n_support)) < 0) {
|
||||
printf("can't make factory instance: %d\n", res);
|
||||
goto exit_cleanup;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return -EBADF;
|
||||
|
||||
exit_cleanup:
|
||||
dlclose(hnd);
|
||||
return res;
|
||||
}
|
||||
|
||||
static int init(struct data *data)
|
||||
{
|
||||
struct spa_handle *handle;
|
||||
struct spa_dict_item items[1];
|
||||
struct spa_dict info;
|
||||
const char *str;
|
||||
void *iface;
|
||||
int res;
|
||||
|
||||
if ((str = getenv("SPA_PLUGIN_DIR")) == NULL)
|
||||
str = PLUGINDIR;
|
||||
data->plugin_dir = str;
|
||||
|
||||
/* enable the debug messages in SPA */
|
||||
items[0] = SPA_DICT_ITEM_INIT(SPA_KEY_LOG_TIMESTAMP, "true");
|
||||
info = SPA_DICT_ARRAY(items);
|
||||
if ((res = load_handle(data, &handle, "support/libspa-support.so",
|
||||
SPA_NAME_SUPPORT_LOG, &info)) < 0)
|
||||
return res;
|
||||
if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Log, &iface)) < 0) {
|
||||
printf("can't get System interface %d\n", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
data->log = iface;
|
||||
data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Log, data->log);
|
||||
|
||||
if ((str = getenv("SPA_DEBUG")))
|
||||
data->log->level = atoi(str);
|
||||
|
||||
/* load and set support system */
|
||||
if ((res = load_handle(data, &handle,
|
||||
"support/libspa-support.so",
|
||||
SPA_NAME_SUPPORT_SYSTEM, NULL)) < 0)
|
||||
return res;
|
||||
if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_System, &iface)) < 0) {
|
||||
printf("can't get System interface %d\n", res);
|
||||
return res;
|
||||
}
|
||||
data->system = iface;
|
||||
data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_System, data->system);
|
||||
data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataSystem, data->system);
|
||||
|
||||
/* load and set support loop and loop control */
|
||||
if ((res = load_handle(data, &handle,
|
||||
"support/libspa-support.so",
|
||||
SPA_NAME_SUPPORT_LOOP, NULL)) < 0)
|
||||
return res;
|
||||
|
||||
if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Loop, &iface)) < 0) {
|
||||
printf("can't get interface %d\n", res);
|
||||
return res;
|
||||
}
|
||||
data->loop = iface;
|
||||
data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Loop, data->loop);
|
||||
data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DataLoop, data->loop);
|
||||
|
||||
if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopControl, &iface)) < 0) {
|
||||
printf("can't get interface %d\n", res);
|
||||
return res;
|
||||
}
|
||||
data->loop_control = iface;
|
||||
data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_LoopControl, data->loop_control);
|
||||
|
||||
if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_LoopUtils, &iface)) < 0) {
|
||||
printf("can't get interface %d\n", res);
|
||||
return res;
|
||||
}
|
||||
data->loop_utils = iface;
|
||||
data->support[data->n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_LoopUtils, data->loop_utils);
|
||||
|
||||
/* load varlink */
|
||||
if ((res = load_handle(data, &handle,
|
||||
"support/libspa-varlink.so",
|
||||
SPA_NAME_SUPPORT_VARLINK, NULL)) < 0)
|
||||
return res;
|
||||
|
||||
if ((res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Varlink, &iface)) < 0) {
|
||||
printf("can't get interface %d\n", res);
|
||||
return res;
|
||||
}
|
||||
data->varlink = iface;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
on_reply(void *userdata, const char *params, const char *error, size_t len, bool continues)
|
||||
{
|
||||
struct data *data = userdata;
|
||||
|
||||
if (params) {
|
||||
printf("Got reply: params: %.*s, continues: %s\n",
|
||||
(int) len, params, continues ? "true" : "false");
|
||||
} else {
|
||||
printf("Got reply: error: %.*s\n", (int) len, error);
|
||||
}
|
||||
|
||||
data->running = false;
|
||||
}
|
||||
|
||||
static void on_disconnect(void *userdata)
|
||||
{
|
||||
struct data *data = userdata;
|
||||
printf("Disconnected\n");
|
||||
data->running = false;
|
||||
}
|
||||
|
||||
static void on_destroy(void *userdata)
|
||||
{
|
||||
printf("Destroyed\n");
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
struct data data = { 0 };
|
||||
struct spa_varlink_client_events events = {
|
||||
.disconnect = on_disconnect,
|
||||
.destroy = on_destroy,
|
||||
};
|
||||
int res;
|
||||
bool sync = true;
|
||||
|
||||
if (argc < 4 || argc > 5) {
|
||||
printf("usage: %s <socket> <method> <parameters> <sync: 1|0>\n", argv[0]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
res = init(&data);
|
||||
if (res < 0)
|
||||
return res;
|
||||
|
||||
if (argc == 5)
|
||||
sync = argv[4][0] == '1';
|
||||
|
||||
data.running = true;
|
||||
|
||||
data.client = spa_varlink_connect(data.varlink, argv[1]);
|
||||
if (data.client == NULL) {
|
||||
printf("Could not connect to socket: %s\n", spa_strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
spa_varlink_client_add_listener(data.client, &data.listener, &events, &data);
|
||||
|
||||
if (sync) {
|
||||
char *reply;
|
||||
res = spa_varlink_client_call_sync(data.client, argv[2], argv[3], &reply);
|
||||
if (res < 0) {
|
||||
printf("Call failed: %s\n", spa_strerror(res));
|
||||
return -1;
|
||||
}
|
||||
printf("Got reply (%d): %s\n", res, reply);
|
||||
} else {
|
||||
res = spa_varlink_client_call(data.client, argv[2], argv[3], false, false, on_reply, &data);
|
||||
if (res < 0) {
|
||||
printf("Could not connect to socket: %s\n", spa_strerror(res));
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (data.running) {
|
||||
spa_loop_control_iterate(data.loop_control, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
spa_varlink_client_destroy(data.client);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -200,6 +200,36 @@ static int device_demarshal_set_param(void *object, const struct pw_protocol_nat
|
|||
return 0;
|
||||
}
|
||||
|
||||
static int device_marshal_send_command(void *object,
|
||||
const struct spa_command *command)
|
||||
{
|
||||
struct pw_resource *resource = object;
|
||||
struct spa_pod_builder *b;
|
||||
|
||||
b = pw_protocol_native_begin_resource(resource, SPA_DEVICE_METHOD_SEND_COMMAND, NULL);
|
||||
|
||||
spa_pod_builder_add_struct(b,
|
||||
SPA_POD_Pod(command));
|
||||
|
||||
return pw_protocol_native_end_resource(resource, b);
|
||||
}
|
||||
|
||||
static int device_demarshal_send_command(void *object, const struct pw_protocol_native_message *msg)
|
||||
{
|
||||
struct pw_proxy *proxy = object;
|
||||
struct spa_pod_parser prs;
|
||||
struct spa_command *command;
|
||||
|
||||
spa_pod_parser_init(&prs, msg->data, msg->size);
|
||||
if (spa_pod_parser_get_struct(&prs,
|
||||
SPA_POD_Pod(&command)) < 0)
|
||||
return -EINVAL;
|
||||
|
||||
pw_proxy_notify(proxy, struct spa_device_methods, send_command, 1,
|
||||
command);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void device_marshal_info(void *data,
|
||||
const struct spa_device_info *info)
|
||||
{
|
||||
|
|
@ -482,7 +512,8 @@ static const struct spa_device_methods pw_protocol_native_device_method_marshal
|
|||
.add_listener = &device_marshal_add_listener,
|
||||
.sync = &device_marshal_sync,
|
||||
.enum_params = &device_marshal_enum_params,
|
||||
.set_param = &device_marshal_set_param
|
||||
.set_param = &device_marshal_set_param,
|
||||
.send_command = &device_marshal_send_command,
|
||||
};
|
||||
|
||||
static const struct pw_protocol_native_demarshal
|
||||
|
|
@ -492,6 +523,7 @@ pw_protocol_native_device_method_demarshal[SPA_DEVICE_METHOD_NUM] =
|
|||
[SPA_DEVICE_METHOD_SYNC] = { &device_demarshal_sync, 0 },
|
||||
[SPA_DEVICE_METHOD_ENUM_PARAMS] = { &device_demarshal_enum_params, 0 },
|
||||
[SPA_DEVICE_METHOD_SET_PARAM] = { &device_demarshal_set_param, 0 },
|
||||
[SPA_DEVICE_METHOD_SEND_COMMAND] = { &device_demarshal_send_command, 0 },
|
||||
};
|
||||
|
||||
static const struct spa_device_events pw_protocol_native_device_event_marshal = {
|
||||
|
|
|
|||
|
|
@ -1056,6 +1056,34 @@ static int device_demarshal_set_param(void *object, const struct pw_protocol_nat
|
|||
return pw_resource_notify(resource, struct pw_device_methods, set_param, 0, id, flags, param);
|
||||
}
|
||||
|
||||
static int device_marshal_send_command(void *object, const struct spa_command *command)
|
||||
{
|
||||
struct pw_proxy *proxy = object;
|
||||
struct spa_pod_builder *b;
|
||||
|
||||
b = pw_protocol_native_begin_proxy(proxy, PW_NODE_METHOD_SEND_COMMAND, NULL);
|
||||
spa_pod_builder_add_struct(b,
|
||||
SPA_POD_Pod(command));
|
||||
return pw_protocol_native_end_proxy(proxy, b);
|
||||
}
|
||||
|
||||
static int device_demarshal_send_command(void *object, const struct pw_protocol_native_message *msg)
|
||||
{
|
||||
struct pw_resource *resource = object;
|
||||
struct spa_pod_parser prs;
|
||||
struct spa_command *command;
|
||||
|
||||
spa_pod_parser_init(&prs, msg->data, msg->size);
|
||||
if (spa_pod_parser_get_struct(&prs,
|
||||
SPA_POD_Pod(&command)) < 0)
|
||||
return -EINVAL;
|
||||
|
||||
if (command == NULL)
|
||||
return -EINVAL;
|
||||
|
||||
return pw_resource_notify(resource, struct pw_device_methods, send_command, 1, command);
|
||||
}
|
||||
|
||||
static int factory_method_marshal_add_listener(void *object,
|
||||
struct spa_hook *listener,
|
||||
const struct pw_factory_events *events,
|
||||
|
|
@ -2110,6 +2138,7 @@ static const struct pw_device_methods pw_protocol_native_device_method_marshal =
|
|||
.subscribe_params = &device_marshal_subscribe_params,
|
||||
.enum_params = &device_marshal_enum_params,
|
||||
.set_param = &device_marshal_set_param,
|
||||
.send_command = &device_marshal_send_command,
|
||||
};
|
||||
|
||||
static const struct pw_protocol_native_demarshal
|
||||
|
|
@ -2118,6 +2147,7 @@ pw_protocol_native_device_method_demarshal[PW_DEVICE_METHOD_NUM] = {
|
|||
[PW_DEVICE_METHOD_SUBSCRIBE_PARAMS] = { &device_demarshal_subscribe_params, 0, },
|
||||
[PW_DEVICE_METHOD_ENUM_PARAMS] = { &device_demarshal_enum_params, 0, },
|
||||
[PW_DEVICE_METHOD_SET_PARAM] = { &device_demarshal_set_param, PW_PERM_W, },
|
||||
[PW_DEVICE_METHOD_SEND_COMMAND] = { &device_demarshal_send_command, PW_PERM_W, },
|
||||
};
|
||||
|
||||
static const struct pw_device_events pw_protocol_native_device_event_marshal = {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include <regex.h>
|
||||
#include <malloc.h>
|
||||
|
||||
#include <spa/monitor/device.h>
|
||||
#include <spa/param/props.h>
|
||||
#include <spa/pod/builder.h>
|
||||
#include <spa/pod/pod.h>
|
||||
|
|
@ -23,8 +24,62 @@
|
|||
#include "log.h"
|
||||
#include "manager.h"
|
||||
#include "module.h"
|
||||
#include "pipewire/properties.h"
|
||||
#include "message-handler.h"
|
||||
|
||||
static int sink_object_message_handler(struct client *client, struct pw_manager_object *o, const char *message, const char *params, FILE *response)
|
||||
{
|
||||
struct device_info dev_info;
|
||||
struct pw_manager_object *card = NULL;
|
||||
char buf[1024];
|
||||
struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
|
||||
struct spa_pod_frame f[1];
|
||||
struct spa_command *command;
|
||||
uint32_t card_id, command_id;
|
||||
|
||||
pw_log_debug(": sink %p object message:'%s' params:'%s'", o, message, params);
|
||||
|
||||
get_device_info(o, &dev_info, PW_DIRECTION_OUTPUT, false);
|
||||
|
||||
if (spa_streq(message, "volume-up"))
|
||||
command_id = SPA_COMMAND_DEVICE_volumeUp;
|
||||
else if (spa_streq(message, "volume-down"))
|
||||
command_id = SPA_COMMAND_DEVICE_volumeDown;
|
||||
else if (spa_streq(message, "mute-toggle"))
|
||||
command_id = SPA_COMMAND_DEVICE_muteToggle;
|
||||
else {
|
||||
fprintf(response, "Unknown message %s (must be volume-up, volume-down or mute-toggle)", message);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
card_id = pw_properties_get_uint32(o->props, PW_KEY_DEVICE_ID, SPA_ID_INVALID);
|
||||
|
||||
spa_list_for_each(o, &client->manager->object_list, link) {
|
||||
if (o->id == card_id) {
|
||||
card = o;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!card) {
|
||||
pw_log_error("Could not find card");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
spa_pod_builder_push_object(&b, &f[0],
|
||||
SPA_TYPE_COMMAND_Device, SPA_DEVICE_COMMAND_VolumeControl);
|
||||
spa_pod_builder_add(&b,
|
||||
command_id, SPA_POD_Id(dev_info.active_port), 0);
|
||||
|
||||
command = spa_pod_builder_pop(&b, &f[0]);
|
||||
|
||||
pw_device_send_command((struct pw_device *)card->proxy, command);
|
||||
|
||||
fprintf(response, "true");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bluez_card_object_message_handler(struct client *client, struct pw_manager_object *o, const char *message, const char *params, FILE *response)
|
||||
{
|
||||
struct transport_codec_info codecs[64];
|
||||
|
|
@ -285,4 +340,14 @@ void register_object_message_handlers(struct pw_manager_object *o)
|
|||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pw_manager_object_is_sink(o)) {
|
||||
str = pw_properties_get(o->props, PW_KEY_NODE_NAME);
|
||||
if (str) {
|
||||
free(o->message_object_path);
|
||||
o->message_object_path = spa_aprintf("/sink/%s/volume-control", str);
|
||||
o->message_handler = sink_object_message_handler;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3692,7 +3692,7 @@ static int fill_card_info(struct client *client, struct message *m,
|
|||
}
|
||||
|
||||
static int fill_sink_info_proplist(struct message *m, const struct spa_dict *sink_props,
|
||||
const struct pw_manager_object *card)
|
||||
const struct device_info *dev_info, const struct pw_manager_object *card)
|
||||
{
|
||||
struct pw_device_info *card_info = card ? card->info : NULL;
|
||||
spa_autoptr(pw_properties) props = NULL;
|
||||
|
|
@ -3706,6 +3706,26 @@ static int fill_sink_info_proplist(struct message *m, const struct spa_dict *sin
|
|||
sink_props = &props->dict;
|
||||
}
|
||||
|
||||
if (dev_info->volume_info.flags & VOLUME_CONTROL_MASK) {
|
||||
props = pw_properties_new_dict(sink_props);
|
||||
if (props == NULL)
|
||||
return -ENOMEM;
|
||||
|
||||
#define SET_PROP(k, f) pw_properties_set(props, k, dev_info->volume_info.flags & (f) ? "true" : "false")
|
||||
|
||||
SET_PROP("device.volume.read-volume", VOLUME_READ);
|
||||
SET_PROP("device.volume.write-volume-value", VOLUME_WRITE);
|
||||
SET_PROP("device.volume.write-volume-updown", VOLUME_UPDOWN);
|
||||
SET_PROP("device.volume.read-mute", VOLUME_READ_MUTE);
|
||||
SET_PROP("device.volume.write-mute-value", VOLUME_WRITE_MUTE);
|
||||
SET_PROP("device.volume.write-mute-toggle", VOLUME_TOGGLE_MUTE);
|
||||
SET_PROP("device.volume.read-balance", VOLUME_READ_BALANCE);
|
||||
SET_PROP("device.volume.write-balance", VOLUME_WRITE_BALANCE);
|
||||
|
||||
pw_properties_add(props, card_info->props);
|
||||
sink_props = &props->dict;
|
||||
}
|
||||
|
||||
message_put(m, TAG_PROPLIST, sink_props, TAG_INVALID);
|
||||
|
||||
return 0;
|
||||
|
|
@ -3804,7 +3824,7 @@ static int fill_sink_info(struct client *client, struct message *m,
|
|||
|
||||
if (client->version >= 13) {
|
||||
int res;
|
||||
if ((res = fill_sink_info_proplist(m, info->props, card)) < 0)
|
||||
if ((res = fill_sink_info_proplist(m, info->props, &dev_info, card)) < 0)
|
||||
return res;
|
||||
message_put(m,
|
||||
TAG_USEC, 0LL, /* requested latency */
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include <spa/param/props.h>
|
||||
#include <spa/param/audio/raw.h>
|
||||
#include <spa/param/audio/volume.h>
|
||||
#include <spa/pod/iter.h>
|
||||
#include <spa/utils/defs.h>
|
||||
#include <pipewire/log.h>
|
||||
|
|
@ -86,6 +87,29 @@ int volume_parse_param(const struct spa_pod *param, struct volume_info *info, bo
|
|||
info->map.channels = spa_pod_copy_array(&prop->value, SPA_TYPE_Id,
|
||||
info->map.map, SPA_N_ELEMENTS(info->map.map));
|
||||
break;
|
||||
case SPA_PROP_volumeControlFlags:
|
||||
{
|
||||
uint32_t flags;
|
||||
if (spa_pod_get_id(&prop->value, &flags) >= 0) {
|
||||
SPA_FLAG_UPDATE(info->flags, VOLUME_READ,
|
||||
flags & SPA_AUDIO_VOLUME_CONTROL_READ_VOLUME);
|
||||
SPA_FLAG_UPDATE(info->flags, VOLUME_WRITE,
|
||||
flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_VALUE);
|
||||
SPA_FLAG_UPDATE(info->flags, VOLUME_UPDOWN,
|
||||
flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_VOLUME_UPDOWN);
|
||||
SPA_FLAG_UPDATE(info->flags, VOLUME_READ_MUTE,
|
||||
flags & SPA_AUDIO_VOLUME_CONTROL_READ_MUTE);
|
||||
SPA_FLAG_UPDATE(info->flags, VOLUME_WRITE_MUTE,
|
||||
flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_VALUE);
|
||||
SPA_FLAG_UPDATE(info->flags, VOLUME_TOGGLE_MUTE,
|
||||
flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_MUTE_TOGGLE);
|
||||
SPA_FLAG_UPDATE(info->flags, VOLUME_READ_BALANCE,
|
||||
flags & SPA_AUDIO_VOLUME_CONTROL_READ_BALANCE);
|
||||
SPA_FLAG_UPDATE(info->flags, VOLUME_WRITE_BALANCE,
|
||||
flags & SPA_AUDIO_VOLUME_CONTROL_WRITE_BALANCE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,15 @@ struct volume_info {
|
|||
uint32_t steps;
|
||||
#define VOLUME_HW_VOLUME (1<<0)
|
||||
#define VOLUME_HW_MUTE (1<<1)
|
||||
#define VOLUME_READ (1<<2)
|
||||
#define VOLUME_WRITE (1<<3)
|
||||
#define VOLUME_UPDOWN (1<<4)
|
||||
#define VOLUME_READ_MUTE (1<<5)
|
||||
#define VOLUME_WRITE_MUTE (1<<6)
|
||||
#define VOLUME_TOGGLE_MUTE (1<<7)
|
||||
#define VOLUME_READ_BALANCE (1<<8)
|
||||
#define VOLUME_WRITE_BALANCE (1<<9)
|
||||
#define VOLUME_CONTROL_MASK (0x01FC)
|
||||
uint32_t flags;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
#include <spa/support/dbus.h>
|
||||
#include <spa/support/plugin.h>
|
||||
#include <spa/support/plugin-loader.h>
|
||||
#include <spa/support/varlink.h>
|
||||
#include <spa/node/utils.h>
|
||||
#include <spa/utils/atomic.h>
|
||||
#include <spa/utils/names.h>
|
||||
|
|
@ -55,6 +56,7 @@ struct data_loop {
|
|||
struct impl {
|
||||
struct pw_context this;
|
||||
struct spa_handle *dbus_handle;
|
||||
struct spa_handle *varlink_handle;
|
||||
struct spa_plugin_loader plugin_loader;
|
||||
unsigned int recalc:1;
|
||||
unsigned int recalc_pending:1;
|
||||
|
|
@ -402,7 +404,7 @@ struct pw_context *pw_context_new(struct pw_loop *main_loop,
|
|||
struct impl *impl;
|
||||
struct pw_context *this;
|
||||
const char *lib, *str;
|
||||
void *dbus_iface = NULL;
|
||||
void *dbus_iface = NULL, *varlink_iface = NULL;
|
||||
uint32_t i, n_support, vm_type;
|
||||
struct pw_properties *conf;
|
||||
struct spa_cpu *cpu;
|
||||
|
|
@ -554,6 +556,26 @@ struct pw_context *pw_context_new(struct pw_loop *main_loop,
|
|||
this->support[n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_DBus, dbus_iface);
|
||||
}
|
||||
}
|
||||
/* Only load varlink support if explicitly requested */
|
||||
if ((str = pw_properties_get(properties, "support.varlink")) != NULL &&
|
||||
pw_properties_parse_bool(str)) {
|
||||
lib = pw_properties_get(properties, PW_KEY_LIBRARY_NAME_VARLINK);
|
||||
if (lib == NULL)
|
||||
lib = "support/libspa-varlink";
|
||||
|
||||
impl->varlink_handle = pw_load_spa_handle(lib,
|
||||
SPA_NAME_SUPPORT_VARLINK, NULL,
|
||||
n_support, this->support);
|
||||
|
||||
if (impl->varlink_handle == NULL) {
|
||||
pw_log_warn("%p: can't load varlink library: %s", this, lib);
|
||||
} else if ((res = spa_handle_get_interface(impl->varlink_handle,
|
||||
SPA_TYPE_INTERFACE_Varlink, &varlink_iface)) < 0) {
|
||||
pw_log_warn("%p: can't load varlink interface: %s", this, spa_strerror(res));
|
||||
} else {
|
||||
this->support[n_support++] = SPA_SUPPORT_INIT(SPA_TYPE_INTERFACE_Varlink, varlink_iface);
|
||||
}
|
||||
}
|
||||
this->n_support = n_support;
|
||||
spa_assert(n_support <= SPA_N_ELEMENTS(this->support));
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#ifndef PIPEWIRE_DEVICE_H
|
||||
#define PIPEWIRE_DEVICE_H
|
||||
|
||||
#include <spa/pod/command.h>
|
||||
#include <spa/utils/defs.h>
|
||||
#include <spa/utils/hook.h>
|
||||
|
||||
|
|
@ -92,11 +93,12 @@ struct pw_device_events {
|
|||
#define PW_DEVICE_METHOD_SUBSCRIBE_PARAMS 1
|
||||
#define PW_DEVICE_METHOD_ENUM_PARAMS 2
|
||||
#define PW_DEVICE_METHOD_SET_PARAM 3
|
||||
#define PW_DEVICE_METHOD_NUM 4
|
||||
#define PW_DEVICE_METHOD_SEND_COMMAND 4
|
||||
#define PW_DEVICE_METHOD_NUM 5
|
||||
|
||||
/** Device methods */
|
||||
struct pw_device_methods {
|
||||
#define PW_VERSION_DEVICE_METHODS 0
|
||||
#define PW_VERSION_DEVICE_METHODS 1
|
||||
uint32_t version;
|
||||
|
||||
int (*add_listener) (void *object,
|
||||
|
|
@ -143,6 +145,15 @@ struct pw_device_methods {
|
|||
*/
|
||||
int (*set_param) (void *object, uint32_t id, uint32_t flags,
|
||||
const struct spa_pod *param);
|
||||
|
||||
/**
|
||||
* Send a command to the device
|
||||
*
|
||||
* \param command the command to send
|
||||
*
|
||||
* This requires X and W permissions on the device.
|
||||
*/
|
||||
int (*send_command) (void *object, const struct spa_command *command);
|
||||
};
|
||||
|
||||
/** \copydoc pw_device_methods.add_listener
|
||||
|
|
@ -183,6 +194,13 @@ PW_API_DEVICE_IMPL int pw_device_set_param(struct pw_device *object, uint32_t id
|
|||
pw_device, (struct spa_interface*)object, set_param, 0,
|
||||
id, flags, param);
|
||||
}
|
||||
/** \copydoc pw_device_methods.send_command
|
||||
* \sa pw_device_methods.send_command */
|
||||
PW_API_DEVICE_IMPL int pw_device_send_command(struct pw_device *object, const struct spa_command *command)
|
||||
{
|
||||
return spa_api_method_r(int, -ENOTSUP,
|
||||
pw_device, (struct spa_interface*)object, send_command, 0, command);
|
||||
}
|
||||
|
||||
/**
|
||||
* \}
|
||||
|
|
|
|||
|
|
@ -529,11 +529,30 @@ static int device_set_param(void *object, uint32_t id, uint32_t flags,
|
|||
return res;
|
||||
}
|
||||
|
||||
static int device_send_command(void *object, const struct spa_command *command)
|
||||
{
|
||||
struct resource_data *data = object;
|
||||
struct pw_impl_device *device = data->device;
|
||||
uint32_t id = SPA_DEVICE_COMMAND_ID(command);
|
||||
int res;
|
||||
|
||||
pw_log_debug("%p: got command %d (%s)", device, id,
|
||||
spa_debug_type_find_name(spa_type_device_command_id, id));
|
||||
|
||||
switch (id) {
|
||||
default:
|
||||
res = spa_device_send_command(device->device, command);
|
||||
break;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static const struct pw_device_methods device_methods = {
|
||||
PW_VERSION_DEVICE_METHODS,
|
||||
.subscribe_params = device_subscribe_params,
|
||||
.enum_params = device_enum_params,
|
||||
.set_param = device_set_param
|
||||
.set_param = device_set_param,
|
||||
.send_command = device_send_command,
|
||||
};
|
||||
|
||||
static int
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ extern "C" {
|
|||
#define PW_KEY_LIBRARY_NAME_SYSTEM "library.name.system" /**< name of the system library to use */
|
||||
#define PW_KEY_LIBRARY_NAME_LOOP "library.name.loop" /**< name of the loop library to use */
|
||||
#define PW_KEY_LIBRARY_NAME_DBUS "library.name.dbus" /**< name of the dbus library to use */
|
||||
#define PW_KEY_LIBRARY_NAME_VARLINK "library.name.varlink" /**< name of the varlink library to use */
|
||||
|
||||
/** object properties */
|
||||
#define PW_KEY_OBJECT_PATH "object.path" /**< unique path to construct the object */
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ static void test_device_abi(void)
|
|||
const struct spa_pod *filter);
|
||||
int (*set_param) (void *object, uint32_t id, uint32_t flags,
|
||||
const struct spa_pod *param);
|
||||
int (*send_command) (void *object, const struct spa_command *command);
|
||||
} methods = { PW_VERSION_DEVICE_METHODS, };
|
||||
struct {
|
||||
uint32_t version;
|
||||
|
|
@ -167,7 +168,8 @@ static void test_device_abi(void)
|
|||
TEST_FUNC(m, methods, subscribe_params);
|
||||
TEST_FUNC(m, methods, enum_params);
|
||||
TEST_FUNC(m, methods, set_param);
|
||||
spa_assert_se(PW_VERSION_DEVICE_METHODS == 0);
|
||||
TEST_FUNC(m, methods, send_command);
|
||||
spa_assert_se(PW_VERSION_DEVICE_METHODS == 1);
|
||||
spa_assert_se(sizeof(m) == sizeof(methods));
|
||||
|
||||
TEST_FUNC(e, events, version);
|
||||
|
|
|
|||
|
|
@ -2047,6 +2047,8 @@ static bool do_send_command(struct data *data, const char *cmd, char *args, char
|
|||
|
||||
if (spa_streq(global->type, PW_TYPE_INTERFACE_Node)) {
|
||||
ti = spa_debug_type_find_short(spa_type_node_command_id, a[1]);
|
||||
} else if (spa_streq(global->type, PW_TYPE_INTERFACE_Device)) {
|
||||
ti = spa_debug_type_find_short(spa_type_device_command_id, a[1]);
|
||||
} else {
|
||||
*error = spa_aprintf("send-command not implemented on object %d type:%s",
|
||||
atoi(a[0]), global->type);
|
||||
|
|
@ -2054,7 +2056,7 @@ static bool do_send_command(struct data *data, const char *cmd, char *args, char
|
|||
}
|
||||
|
||||
if (ti == NULL) {
|
||||
*error = spa_aprintf("%s: unknown node command type: %s", cmd, a[1]);
|
||||
*error = spa_aprintf("%s: unknown command type: %s", cmd, a[1]);
|
||||
return false;
|
||||
}
|
||||
if ((res = spa_json_to_pod(&b.b, 0, ti, a[2], strlen(a[2]))) < 0) {
|
||||
|
|
@ -2067,7 +2069,12 @@ static bool do_send_command(struct data *data, const char *cmd, char *args, char
|
|||
}
|
||||
spa_debug_pod(0, NULL, pod);
|
||||
|
||||
pw_node_send_command((struct pw_node*)global->proxy, (struct spa_command*)pod);
|
||||
if (spa_streq(global->type, PW_TYPE_INTERFACE_Node)) {
|
||||
pw_node_send_command((struct pw_node*)global->proxy, (struct spa_command*)pod);
|
||||
} else if (spa_streq(global->type, PW_TYPE_INTERFACE_Device)) {
|
||||
pw_device_send_command((struct pw_device*)global->proxy, (struct spa_command*)pod);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue