diff options
-rw-r--r-- | CMakeLists.txt | 3 | ||||
-rw-r--r-- | LICENSE | 20 | ||||
-rw-r--r-- | README.md | 5 | ||||
-rwxr-xr-x | autobuild/agl/autobuild | 79 | ||||
-rwxr-xr-x | autobuild/linux/autobuild | 79 | ||||
-rw-r--r-- | binding/CMakeLists.txt | 20 | ||||
-rw-r--r-- | binding/audiomixer-binding.c | 342 | ||||
-rw-r--r-- | binding/audiomixer.c | 597 | ||||
-rw-r--r-- | binding/audiomixer.h | 60 | ||||
-rw-r--r-- | conf.d/cmake/config.cmake | 158 | ||||
-rw-r--r-- | conf.d/wgt/config.xml.in | 23 |
11 files changed, 1386 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f757721 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,3 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 3.3) + +include(${CMAKE_CURRENT_SOURCE_DIR}/conf.d/cmake/config.cmake) @@ -0,0 +1,20 @@ +Copyright © 2019 Collabora Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..102189c --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Audio Mixer Service + +Audio mixer binding service for AGL. + +This binding exposes PipeWire mixer controls to applications. diff --git a/autobuild/agl/autobuild b/autobuild/agl/autobuild new file mode 100755 index 0000000..db00c1a --- /dev/null +++ b/autobuild/agl/autobuild @@ -0,0 +1,79 @@ +#!/usr/bin/make -f +# Copyright (C) 2015 - 2018 "IoT.bzh" +# Author "Romain Forlot" <romain.forlot@iot.bzh> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +THISFILE := $(lastword $(MAKEFILE_LIST)) +BUILD_DIR := $(abspath $(dir $(THISFILE))/../../build) +DEST := ${BUILD_DIR} + +.PHONY: all clean distclean configure build package help update + +all: help + +help: + @echo "List of targets available:" + @echo "" + @echo "- all" + @echo "- clean" + @echo "- distclean" + @echo "- configure" + @echo "- build: compilation, link and prepare files for package into a widget" + @echo "- package: output a widget file '*.wgt'" + @echo "- install: install in your ${CMAKE_INSTALL_DIR} directory" + @echo "" + @echo "Usage: ./autobuild/agl/autobuild package DEST=${HOME}/opt" + @echo "Don't use your build dir as DEST as wgt file is generated at this location" + +update: configure + @cmake --build ${BUILD_DIR} --target autobuild + +clean: + @([ -d ${BUILD_DIR} ] && make -C ${BUILD_DIR} ${CLEAN_ARGS} clean) || echo Nothing to clean + +distclean: + @rm -rf ${BUILD_DIR} + +configure: + @[ -d ${BUILD_DIR} ] || mkdir -p ${BUILD_DIR} + @[ -f ${BUILD_DIR}/Makefile ] || (cd ${BUILD_DIR} && cmake ${CONFIGURE_ARGS} ..) + +build: configure + @cmake --build ${BUILD_DIR} ${BUILD_ARGS} --target all + +package: build + @mkdir -p ${BUILD_DIR}/$@/bin + @mkdir -p ${BUILD_DIR}/$@/etc + @mkdir -p ${BUILD_DIR}/$@/lib + @mkdir -p ${BUILD_DIR}/$@/htdocs + @mkdir -p ${BUILD_DIR}/$@/var + @cmake --build ${BUILD_DIR} ${PACKAGE_ARGS} --target widget + @if [ "${DEST}" != "${BUILD_DIR}" ]; then \ + mkdir -p ${DEST} && cp ${BUILD_DIR}/*.wgt ${DEST}; \ + fi + +package-test: build + @mkdir -p ${BUILD_DIR}/$@/bin + @mkdir -p ${BUILD_DIR}/$@/etc + @mkdir -p ${BUILD_DIR}/$@/lib + @mkdir -p ${BUILD_DIR}/$@/htdocs + @mkdir -p ${BUILD_DIR}/$@/var + @cmake --build ${BUILD_DIR} ${PACKAGE_ARGS} --target widget + @cmake --build ${BUILD_DIR} ${PACKAGE_ARGS} --target test_widget + @if [ "${DEST}" != "${BUILD_DIR}" ]; then \ + mkdir -p ${DEST} && cp ${BUILD_DIR}/*.wgt ${DEST}; \ + fi + +install: build + @cmake --build ${BUILD_DIR} ${INSTALL_ARGS} --target install diff --git a/autobuild/linux/autobuild b/autobuild/linux/autobuild new file mode 100755 index 0000000..db00c1a --- /dev/null +++ b/autobuild/linux/autobuild @@ -0,0 +1,79 @@ +#!/usr/bin/make -f +# Copyright (C) 2015 - 2018 "IoT.bzh" +# Author "Romain Forlot" <romain.forlot@iot.bzh> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +THISFILE := $(lastword $(MAKEFILE_LIST)) +BUILD_DIR := $(abspath $(dir $(THISFILE))/../../build) +DEST := ${BUILD_DIR} + +.PHONY: all clean distclean configure build package help update + +all: help + +help: + @echo "List of targets available:" + @echo "" + @echo "- all" + @echo "- clean" + @echo "- distclean" + @echo "- configure" + @echo "- build: compilation, link and prepare files for package into a widget" + @echo "- package: output a widget file '*.wgt'" + @echo "- install: install in your ${CMAKE_INSTALL_DIR} directory" + @echo "" + @echo "Usage: ./autobuild/agl/autobuild package DEST=${HOME}/opt" + @echo "Don't use your build dir as DEST as wgt file is generated at this location" + +update: configure + @cmake --build ${BUILD_DIR} --target autobuild + +clean: + @([ -d ${BUILD_DIR} ] && make -C ${BUILD_DIR} ${CLEAN_ARGS} clean) || echo Nothing to clean + +distclean: + @rm -rf ${BUILD_DIR} + +configure: + @[ -d ${BUILD_DIR} ] || mkdir -p ${BUILD_DIR} + @[ -f ${BUILD_DIR}/Makefile ] || (cd ${BUILD_DIR} && cmake ${CONFIGURE_ARGS} ..) + +build: configure + @cmake --build ${BUILD_DIR} ${BUILD_ARGS} --target all + +package: build + @mkdir -p ${BUILD_DIR}/$@/bin + @mkdir -p ${BUILD_DIR}/$@/etc + @mkdir -p ${BUILD_DIR}/$@/lib + @mkdir -p ${BUILD_DIR}/$@/htdocs + @mkdir -p ${BUILD_DIR}/$@/var + @cmake --build ${BUILD_DIR} ${PACKAGE_ARGS} --target widget + @if [ "${DEST}" != "${BUILD_DIR}" ]; then \ + mkdir -p ${DEST} && cp ${BUILD_DIR}/*.wgt ${DEST}; \ + fi + +package-test: build + @mkdir -p ${BUILD_DIR}/$@/bin + @mkdir -p ${BUILD_DIR}/$@/etc + @mkdir -p ${BUILD_DIR}/$@/lib + @mkdir -p ${BUILD_DIR}/$@/htdocs + @mkdir -p ${BUILD_DIR}/$@/var + @cmake --build ${BUILD_DIR} ${PACKAGE_ARGS} --target widget + @cmake --build ${BUILD_DIR} ${PACKAGE_ARGS} --target test_widget + @if [ "${DEST}" != "${BUILD_DIR}" ]; then \ + mkdir -p ${DEST} && cp ${BUILD_DIR}/*.wgt ${DEST}; \ + fi + +install: build + @cmake --build ${BUILD_DIR} ${INSTALL_ARGS} --target install diff --git a/binding/CMakeLists.txt b/binding/CMakeLists.txt new file mode 100644 index 0000000..587d137 --- /dev/null +++ b/binding/CMakeLists.txt @@ -0,0 +1,20 @@ +PROJECT_TARGET_ADD(audiomixer-binding) + + add_definitions(-DAFB_BINDING_VERSION=3) + add_definitions(-DBUILDING_APPFW_BINDING) + + set(audiomixer_SOURCES + audiomixer-binding.c + audiomixer.c + ) + + add_library(${TARGET_NAME} MODULE ${audiomixer_SOURCES}) + + SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES + PREFIX "libafm-" + LABELS "BINDING" + LINK_FLAGS ${BINDINGS_LINK_FLAG} + OUTPUT_NAME ${TARGET_NAME} + ) + + TARGET_LINK_LIBRARIES(${TARGET_NAME} ${link_libraries}) diff --git a/binding/audiomixer-binding.c b/binding/audiomixer-binding.c new file mode 100644 index 0000000..005f5c7 --- /dev/null +++ b/binding/audiomixer-binding.c @@ -0,0 +1,342 @@ +/* + * Copyright © 2019 Collabora Ltd. + * @author George Kiagiadakis <george.kiagiadakis@collabora.com> + * + * SPDX-License-Identifier: MIT + */ + +#include <string.h> +#include <json-c/json.h> +#include <afb/afb-binding.h> +#include <systemd/sd-event.h> +#include "audiomixer.h" + +static struct audiomixer *audiomixer; +static afb_event_t controls_changed; +static afb_event_t volume_changed; +static afb_event_t mute_changed; +static sd_event_source *controls_changed_source; + +static int +audiomixer_controls_changed_deferred(sd_event_source *s, void *data) +{ + afb_event_push(controls_changed, NULL); + + sd_event_source_unref(controls_changed_source); + controls_changed_source = NULL; + return 0; +} + +struct value_changed_data +{ + unsigned int change_mask; + struct mixer_control control; + sd_event_source *source; +}; + +static int +audiomixer_value_changed_deferred(sd_event_source *s, void *data) +{ + struct value_changed_data *d = data; + json_object *json; + + json = json_object_new_object(); + json_object_object_add(json, "control", + json_object_new_string(d->control.name)); + + if (d->change_mask & MIXER_CONTROL_CHANGE_FLAG_VOLUME) { + json_object_object_add(json, "value", + json_object_new_double(d->control.volume)); + afb_event_push(volume_changed, json); + } else if (d->change_mask & MIXER_CONTROL_CHANGE_FLAG_MUTE) { + json_object_object_add(json, "value", + json_object_new_int(d->control.mute)); + afb_event_push(mute_changed, json); + } + + sd_event_source_unref(d->source); + free(d); + return 0; +} + +/* called in audiomixer's thread */ +static void +audiomixer_controls_changed(void *data) +{ + sd_event *e = afb_daemon_get_event_loop(); + sd_event_add_defer(e, &controls_changed_source, + audiomixer_controls_changed_deferred, NULL); +} + + +/* called in audiomixer's thread */ +static void +audiomixer_value_changed(void *data, + unsigned int change_mask, + const struct mixer_control *control) +{ + sd_event *e = afb_daemon_get_event_loop(); + struct value_changed_data *d = calloc(1, sizeof(*d)); + + d->change_mask = change_mask; + d->control = *control; + + if (sd_event_add_defer(e, &d->source, + audiomixer_value_changed_deferred, d) < 0) + free(d); +} + +static const struct audiomixer_events audiomixer_events = { + .controls_changed = audiomixer_controls_changed, + .value_changed = audiomixer_value_changed, +}; + +static int +cleanup(sd_event_source *s, void *data) +{ + audiomixer_free(audiomixer); + audiomixer = NULL; + return 0; +} + +static int +init(afb_api_t api) +{ + sd_event *e = afb_daemon_get_event_loop(); + + controls_changed = afb_api_make_event(api, "controls_changed"); + volume_changed = afb_api_make_event(api, "volume_changed"); + mute_changed = afb_api_make_event(api, "mute_changed"); + + audiomixer = audiomixer_new(); + sd_event_add_exit(e, NULL, cleanup, NULL); + + audiomixer_add_event_listener(audiomixer, &audiomixer_events, NULL); + + return 0; +} + +static void +list_controls_cb(afb_req_t request) +{ + json_object *ret_json, *nest_json; + const struct mixer_control **ctls; + unsigned int n_controls, i; + + audiomixer_lock(audiomixer); + + if (audiomixer_ensure_connected(audiomixer, 3) < 0) { + afb_req_fail(request, "failed", + "Could not connect to the PipeWire daemon"); + goto unlock; + } + + if (audiomixer_ensure_controls(audiomixer, 3) < 0) { + AFB_REQ_NOTICE(request, "No mixer controls were exposed " + "in PipeWire after 3 seconds"); + } + + ctls = audiomixer_get_active_controls(audiomixer, &n_controls); + + ret_json = json_object_new_array(); + for (i = 0; i < n_controls; i++) { + nest_json = json_object_new_object(); + json_object_object_add(nest_json, "control", + json_object_new_string(ctls[i]->name)); + json_object_object_add(nest_json, "volume", + json_object_new_double(ctls[i]->volume)); + json_object_object_add(nest_json, "mute", + json_object_new_int(ctls[i]->mute)); + json_object_array_add(ret_json, nest_json); + } + afb_req_success(request, ret_json, NULL); + +unlock: + audiomixer_unlock(audiomixer); +} + +static void +volume_cb(afb_req_t request) +{ + json_object *ret_json; + const char *control = afb_req_value(request, "control"); + const char *value = afb_req_value(request, "value"); + const struct mixer_control *ctl; + double volume; + + audiomixer_lock(audiomixer); + + if (!control) { + afb_req_fail(request, "failed", + "Invalid arguments: missing 'control'"); + goto unlock; + } + + if (audiomixer_ensure_connected(audiomixer, 3) < 0) { + afb_req_fail(request, "failed", + "Could not connect to the PipeWire daemon"); + goto unlock; + } + + if (audiomixer_ensure_controls(audiomixer, 3) < 0) { + AFB_REQ_NOTICE(request, "No mixer controls were exposed " + "in PipeWire after 3 seconds"); + } + + ctl = audiomixer_find_control(audiomixer, control); + if (!ctl) { + afb_req_fail(request, "failed", "Could not find control"); + goto unlock; + } + + if(value) { + char *endptr; + volume = strtod(value, &endptr); + if (endptr == value || volume < -0.00001 || volume > 1.00001) { + afb_req_fail(request, "failed", + "Invalid volume value (must be between 0.0 and 1.0)"); + goto unlock; + } + + audiomixer_change_volume(audiomixer, ctl, volume); + } else { + volume = ctl->volume; + } + + ret_json = json_object_new_object(); + json_object_object_add(ret_json, "volume", json_object_new_double(volume)); + afb_req_success(request, ret_json, NULL); + +unlock: + audiomixer_unlock(audiomixer); +} + +static void +mute_cb(afb_req_t request) +{ + json_object *ret_json; + const char *control = afb_req_value(request, "control"); + const char *value = afb_req_value(request, "value"); + const struct mixer_control *ctl; + int mute; + + audiomixer_lock(audiomixer); + + if (!control) { + afb_req_fail(request, "failed", + "Invalid arguments: missing 'control'"); + goto unlock; + } + + if (audiomixer_ensure_connected(audiomixer, 3) < 0) { + afb_req_fail(request, "failed", + "Could not connect to the PipeWire daemon"); + goto unlock; + } + + if (audiomixer_ensure_controls(audiomixer, 3) < 0) { + AFB_REQ_NOTICE(request, "No mixer controls were exposed " + "in PipeWire after 3 seconds"); + } + + ctl = audiomixer_find_control(audiomixer, control); + if (!ctl) { + afb_req_fail(request, "failed", "Could not find control"); + goto unlock; + } + + if(value) { + char *endptr; + mute = (int) strtol(value, &endptr, 10); + if (endptr == value || mute < 0 || mute > 1) { + afb_req_fail(request, "failed", + "Invalid mute value (must be integer 0 or 1)"); + goto unlock; + } + + audiomixer_change_mute(audiomixer, ctl, mute); + } else { + mute = ctl->mute; + } + + ret_json = json_object_new_object(); + json_object_object_add(ret_json, "mute", json_object_new_int(mute)); + afb_req_success(request, ret_json, NULL); + +unlock: + audiomixer_unlock(audiomixer); +} + +static void +subscribe_cb(afb_req_t request) +{ + const char *eventstr = afb_req_value(request, "event"); + afb_event_t event; + + if (!eventstr) { + afb_req_fail(request, "failed", + "Invalid arguments: missing 'event'"); + return; + } + + if (!strcmp(eventstr, "controls_changed")) + event = controls_changed; + else if (!strcmp(eventstr, "volume_changed")) + event = volume_changed; + else if (!strcmp(eventstr, "mute_changed")) + event = mute_changed; + else { + afb_req_fail(request, "failed", "Invalid event name"); + return; + } + + if (afb_req_subscribe(request, event) != 0) + afb_req_fail(request, "failed", "Failed to subscribe to event"); + else + afb_req_success(request, NULL, "Subscribed"); +} + +static void +unsubscribe_cb(afb_req_t request) +{ + const char *eventstr = afb_req_value(request, "event"); + afb_event_t event; + + if (!eventstr) { + afb_req_fail(request, "failed", + "Invalid arguments: missing 'event'"); + return; + } + + if (!strcmp(eventstr, "controls_changed")) + event = controls_changed; + else if (!strcmp(eventstr, "volume_changed")) + event = volume_changed; + else if (!strcmp(eventstr, "mute_changed")) + event = mute_changed; + else { + afb_req_fail(request, "failed", "Invalid event name"); + return; + } + + if (afb_req_unsubscribe(request, event) != 0) + afb_req_fail(request, "failed", "Failed to unsubscribe from event"); + else + afb_req_success(request, NULL, "Unsubscribed"); +} + +static const afb_verb_t verbs[]= { + { .verb = "list_controls", .callback = list_controls_cb, .info = "List the available controls" }, + { .verb = "volume", .callback = volume_cb, .info = "Get/Set volume" }, + { .verb = "mute", .callback = mute_cb, .info = "Get/Set mute" }, + { .verb = "subscribe", .callback = subscribe_cb, .info = "Subscribe to mixer events" }, + { .verb = "unsubscribe", .callback = unsubscribe_cb, .info = "Unsubscribe from mixer events" }, + { } +}; + +const afb_binding_t afbBindingV3 = { + .api = "audiomixer", + .specification = "AudioMixer API", + .verbs = verbs, + .init = init, +}; diff --git a/binding/audiomixer.c b/binding/audiomixer.c new file mode 100644 index 0000000..c4ccc12 --- /dev/null +++ b/binding/audiomixer.c @@ -0,0 +1,597 @@ +/* + * Copyright © 2019 Collabora Ltd. + * @author George Kiagiadakis <george.kiagiadakis@collabora.com> + * + * SPDX-License-Identifier: MIT + */ + +#include "audiomixer.h" +#include <pipewire/pipewire.h> +#include <pipewire/array.h> +#include <pipewire/extensions/endpoint.h> + +#if !defined(BUILDING_APPFW_BINDING) +#define debug(...) fprintf(stdout, __VA_ARGS__) +#else +#include <afb/afb-binding.h> +#define debug(...) AFB_DEBUG(__VA_ARGS__) +#endif + +struct audiomixer +{ + struct pw_thread_loop *main_loop; + + struct pw_core *core; + struct pw_remote *remote; + struct spa_hook remote_listener; + + struct pw_core_proxy *core_proxy; + struct spa_hook remote_core_listener; + struct pw_registry_proxy *registry_proxy; + struct spa_hook registry_listener; + + struct pw_array endpoints; + struct pw_array all_mixer_controls; + + const struct audiomixer_events *events; + void *events_data; +}; + +enum endpoint_state { + EP_STATE_INITIAL, + EP_STATE_COLLECT_ENUM_STREAM, + EP_STATE_COLLECT_ENUM_CONTROL, + EP_STATE_COLLECT_CONTROL, + EP_STATE_ACTIVE, +}; + +struct endpoint +{ + struct audiomixer *audiomixer; + struct pw_endpoint_proxy *proxy; + + struct pw_properties *properties; + enum endpoint_state state; + + struct spa_hook proxy_listener; + struct spa_hook endpoint_listener; + + struct pw_array mixer_controls; +}; + +struct mixer_control_impl +{ + struct mixer_control pub; + struct endpoint *endpoint; + uint32_t stream_id; + uint32_t volume_control_id; + uint32_t mute_control_id; +}; + +static void +emit_controls_changed(struct audiomixer *self) +{ + pw_thread_loop_signal(self->main_loop, false); + + if (!self->events || !self->events->controls_changed) + return; + + self->events->controls_changed(self->events_data); +} + +static void +emit_value_changed(struct audiomixer *self, + unsigned int change_mask, + struct mixer_control *ctl) +{ + if (!self->events || !self->events->value_changed) + return; + + self->events->value_changed(self->events_data, change_mask, ctl); +} + +static void +advance_endpoint_state(struct endpoint *endpoint) +{ + debug("%p advance endpoint state, was:%d", endpoint, endpoint->state); + + switch (endpoint->state) { + case EP_STATE_INITIAL: + endpoint->state = EP_STATE_COLLECT_ENUM_STREAM; + pw_endpoint_proxy_enum_params(endpoint->proxy, 0, + PW_ENDPOINT_PARAM_EnumStream, 0, -1, NULL); + pw_proxy_sync((struct pw_proxy *) endpoint->proxy, 0); + break; + case EP_STATE_COLLECT_ENUM_STREAM: + endpoint->state = EP_STATE_COLLECT_ENUM_CONTROL; + pw_endpoint_proxy_enum_params(endpoint->proxy, 0, + PW_ENDPOINT_PARAM_EnumControl, 0, -1, NULL); + pw_proxy_sync((struct pw_proxy *) endpoint->proxy, 0); + break; + case EP_STATE_COLLECT_ENUM_CONTROL: { + uint32_t ids[1] = { PW_ENDPOINT_PARAM_Control }; + + endpoint->state = EP_STATE_COLLECT_CONTROL; + pw_endpoint_proxy_subscribe_params(endpoint->proxy, ids, 1); + pw_proxy_sync((struct pw_proxy *) endpoint->proxy, 0); + break; + } + case EP_STATE_COLLECT_CONTROL: { + struct mixer_control_impl *ctl; + struct audiomixer *self = endpoint->audiomixer; + + endpoint->state = EP_STATE_ACTIVE; + pw_array_for_each(ctl, &endpoint->mixer_controls) { + pw_array_add_ptr(&self->all_mixer_controls, ctl); + } + emit_controls_changed(self); + break; + } + default: + break; + } +} + +static void +endpoint_param (void *object, int seq, uint32_t id, + uint32_t index, uint32_t next, + const struct spa_pod *param) +{ + struct endpoint *endpoint = object; + struct mixer_control_impl *ctl; + const struct spa_pod_prop *prop; + struct spa_pod_object *obj = (struct spa_pod_object *) param; + + if (!spa_pod_is_object(param)) { + debug("endpoint_param: bad param - not an object"); + return; + } + + switch (id) { + case PW_ENDPOINT_PARAM_EnumStream: + /* verify conditions */ + if (endpoint->state != EP_STATE_COLLECT_ENUM_STREAM) { + debug("endpoint_param EnumStream: wrong state"); + return; + } + if (SPA_POD_OBJECT_TYPE(obj) != PW_ENDPOINT_OBJECT_ParamStream) { + debug("endpoint_param EnumStream: invalid param"); + return; + } + + /* create new mixer control */ + ctl = pw_array_add(&endpoint->mixer_controls, sizeof(*ctl)); + ctl->endpoint = endpoint; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case PW_ENDPOINT_PARAM_STREAM_id: + spa_pod_get_int(&prop->value, &ctl->stream_id); + break; + case PW_ENDPOINT_PARAM_STREAM_name: + spa_pod_copy_string(&prop->value, + SPA_N_ELEMENTS(ctl->pub.name), + ctl->pub.name); + break; + default: + break; + } + } + break; + + case PW_ENDPOINT_PARAM_EnumControl: { + uint32_t tmp_id = -1; + const char *name = NULL; + + /* verify conditions */ + if (endpoint->state != EP_STATE_COLLECT_ENUM_CONTROL) { + debug("endpoint_param EnumControl: wrong state"); + return; + } + if (SPA_POD_OBJECT_TYPE(obj) != PW_ENDPOINT_OBJECT_ParamControl) { + debug("endpoint_param EnumControl: invalid param"); + return; + } + + /* find the mixer control */ + prop = spa_pod_object_find_prop(obj, NULL, + PW_ENDPOINT_PARAM_CONTROL_stream_id); + if (prop) + spa_pod_get_int(&prop->value, &tmp_id); + else { + debug("endpoint_param EnumControl: invalid control without stream"); + return; + } + + pw_array_for_each(ctl, &endpoint->mixer_controls) { + if (ctl->stream_id == tmp_id) + break; + } + + /* check if we reached the end of the array + * without finding the stream */ + if (!pw_array_check(&endpoint->mixer_controls, ctl)) { + debug("endpoint_param EnumControl: could not find " + "stream id %u", tmp_id); + return; + } + + /* store the control id based on the control's name */ + prop = spa_pod_object_find_prop(obj, NULL, + PW_ENDPOINT_PARAM_CONTROL_name); + if (prop) + spa_pod_get_string(&prop->value, &name); + else { + debug("endpoint_param EnumControl: invalid control without name"); + return; + } + + prop = spa_pod_object_find_prop(obj, NULL, + PW_ENDPOINT_PARAM_CONTROL_id); + if (!prop) { + debug("endpoint_param EnumControl: invalid control without id"); + return; + } + + if (strcmp (name, "volume")) { + spa_pod_get_int(&prop->value, &ctl->volume_control_id); + prop = spa_pod_object_find_prop(obj, NULL, + PW_ENDPOINT_PARAM_CONTROL_type); + } else if (strcmp (name, "mute")) { + spa_pod_get_int(&prop->value, &ctl->mute_control_id); + } + + break; + } + case PW_ENDPOINT_PARAM_Control: { + uint32_t tmp_id = -1; + + /* verify conditions */ + if (endpoint->state != EP_STATE_COLLECT_CONTROL || + endpoint->state != EP_STATE_ACTIVE) { + debug("endpoint_param Control: wrong state"); + return; + } + if (SPA_POD_OBJECT_TYPE(obj) != PW_ENDPOINT_OBJECT_ParamControl) { + debug("endpoint_param Control: invalid param"); + return; + } + + /* match the control id and set the value */ + prop = spa_pod_object_find_prop(obj, NULL, + PW_ENDPOINT_PARAM_CONTROL_id); + if (prop) + spa_pod_get_int(&prop->value, &tmp_id); + else { + debug("endpoint_param Control: invalid control without id"); + return; + } + + prop = spa_pod_object_find_prop(obj, NULL, + PW_ENDPOINT_PARAM_CONTROL_value); + if (!prop) { + debug("endpoint_param Control: invalid control without value"); + return; + } + + pw_array_for_each(ctl, &endpoint->mixer_controls) { + if (ctl->volume_control_id == tmp_id) { + spa_pod_get_double(&prop->value, &ctl->pub.volume); + + if (endpoint->state == EP_STATE_ACTIVE) { + emit_value_changed(endpoint->audiomixer, + MIXER_CONTROL_CHANGE_FLAG_VOLUME, + &ctl->pub); + } + break; + } else if (ctl->mute_control_id == tmp_id) { + spa_pod_get_bool(&prop->value, &ctl->pub.mute); + + if (endpoint->state == EP_STATE_ACTIVE) { + emit_value_changed(endpoint->audiomixer, + MIXER_CONTROL_CHANGE_FLAG_MUTE, + &ctl->pub); + } + break; + } + } + + break; + } + default: + break; + } +} + +struct pw_endpoint_proxy_events endpoint_events = { + PW_VERSION_ENDPOINT_PROXY_EVENTS, + .param = endpoint_param, +}; + +static void +endpoint_proxy_destroyed(void *object) +{ + struct endpoint *endpoint = object; + struct audiomixer *self = endpoint->audiomixer; + struct mixer_control_impl *ctl; + struct mixer_control **ctlptr; + struct endpoint **epptr; + + debug("%p endpoint destroyed", endpoint); + + if (endpoint->properties) + pw_properties_free(endpoint->properties); + + pw_array_for_each(ctl, &endpoint->mixer_controls) { + pw_array_for_each(ctlptr, &self->all_mixer_controls) { + if (*ctlptr == &ctl->pub) { + pw_array_remove(&self->all_mixer_controls, ctlptr); + break; + } + } + } + emit_controls_changed(self); + pw_array_clear(&endpoint->mixer_controls); + + pw_array_for_each(epptr, &self->endpoints) { + if (*epptr == endpoint) { + pw_array_remove(&self->endpoints, epptr); + break; + } + } +} + +static void +endpoint_proxy_done(void *object, int seq) +{ + struct endpoint *endpoint = object; + advance_endpoint_state(endpoint); +} + +struct pw_proxy_events proxy_events = { + PW_VERSION_PROXY_EVENTS, + .destroy = endpoint_proxy_destroyed, + .done = endpoint_proxy_done, +}; + +static void +registry_event_global(void *data, uint32_t id, uint32_t parent_id, + uint32_t permissions, uint32_t type, uint32_t version, + const struct spa_dict *props) +{ + struct audiomixer *self = data; + const char *media_class; + struct pw_proxy *proxy; + struct endpoint *endpoint; + + if (type != PW_TYPE_INTERFACE_Endpoint) + return; + + /* we are only interested in mixer endpoints */ + media_class = props ? spa_dict_lookup(props, "media.class") : NULL; + if (!media_class || strcmp(media_class, "Mixer/Audio") != 0) + return; + + proxy = pw_registry_proxy_bind(self->registry_proxy, + id, type, PW_VERSION_ENDPOINT, sizeof(struct endpoint)); + + endpoint = pw_proxy_get_user_data(proxy); + endpoint->audiomixer = self; + endpoint->proxy = (struct pw_endpoint_proxy *) proxy; + endpoint->properties = props ? pw_properties_new_dict(props) : NULL; + endpoint->state = EP_STATE_INITIAL; + pw_array_init(&endpoint->mixer_controls, 4 * sizeof(struct mixer_control)); + + pw_proxy_add_listener(proxy, &endpoint->proxy_listener, + &proxy_events, endpoint); + pw_endpoint_proxy_add_listener(endpoint->proxy, + &endpoint->endpoint_listener, + &endpoint_events, endpoint); + + debug("%p added endpoint: %u", endpoint, id); + + pw_array_add_ptr(&self->endpoints, endpoint); + advance_endpoint_state(endpoint); +} + + +static const struct pw_registry_proxy_events registry_events = { + PW_VERSION_REGISTRY_PROXY_EVENTS, + .global = registry_event_global, +}; + +static void +on_remote_state_changed(void *data, enum pw_remote_state old, + enum pw_remote_state state, const char *error) +{ + struct audiomixer *self = data; + + if (state == PW_REMOTE_STATE_CONNECTED) { + self->core_proxy = pw_remote_get_core_proxy(self->remote); + self->registry_proxy = pw_core_proxy_get_registry( + self->core_proxy, + PW_TYPE_INTERFACE_Registry, + PW_VERSION_REGISTRY, 0); + pw_registry_proxy_add_listener(self->registry_proxy, + &self->registry_listener, + ®istry_events, self); + } + + pw_thread_loop_signal(self->main_loop, false); +} + +static const struct pw_remote_events remote_events = { + PW_VERSION_REMOTE_EVENTS, + .state_changed = on_remote_state_changed, +}; + +struct audiomixer * +audiomixer_new(void) +{ + struct audiomixer *self; + struct pw_loop *loop; + + pw_init(NULL, NULL); + + self = calloc(1, sizeof(struct audiomixer)); + loop = pw_loop_new(NULL); + self->main_loop = pw_thread_loop_new(loop, "audiomixer-loop"); + self->core = pw_core_new(loop, NULL, 0); + self->remote = pw_remote_new(self->core, NULL, 0); + pw_array_init(&self->endpoints, 1 * sizeof(void*)); + pw_array_init(&self->all_mixer_controls, 8 * sizeof(void*)); + + pw_module_load(self->core, "libpipewire-module-endpoint", NULL, NULL, + NULL, NULL); + pw_thread_loop_start(self->main_loop); + + return self; +} + +void +audiomixer_free(struct audiomixer *self) +{ + struct pw_loop *loop; + + pw_thread_loop_lock(self->main_loop); + self->events = NULL; + self->events_data = NULL; + pw_remote_disconnect(self->remote); + pw_thread_loop_unlock(self->main_loop); + pw_thread_loop_stop(self->main_loop); + + pw_array_clear(&self->endpoints); + pw_array_clear(&self->all_mixer_controls); + pw_remote_destroy(self->remote); + pw_core_destroy(self->core); + + loop = pw_thread_loop_get_loop(self->main_loop); + pw_thread_loop_destroy(self->main_loop); + pw_loop_destroy(loop); + + free(self); +} + +void +audiomixer_lock(struct audiomixer *self) +{ + pw_thread_loop_lock(self->main_loop); +} + +void +audiomixer_unlock(struct audiomixer *self) +{ + pw_thread_loop_unlock(self->main_loop); +} + +int +audiomixer_ensure_connected(struct audiomixer *self, int timeout_sec) +{ + enum pw_remote_state state; + int res; + + state = pw_remote_get_state(self->remote, NULL); + if (state == PW_REMOTE_STATE_CONNECTED) + return 0; + + if ((res = pw_remote_connect(self->remote)) < 0) + return res; + + while (true) { + state = pw_remote_get_state(self->remote, NULL); + if (state == PW_REMOTE_STATE_CONNECTED) + return 0; + else if (state == PW_REMOTE_STATE_ERROR) + return -EIO; + + if (pw_thread_loop_timed_wait(self->main_loop, timeout_sec) != 0) + return -ETIMEDOUT; + } +} + +int +audiomixer_ensure_controls(struct audiomixer *self, int timeout_sec) +{ + while (pw_array_get_len(&self->all_mixer_controls, void*) == 0) { + if (pw_thread_loop_timed_wait(self->main_loop, timeout_sec) != 0) + return -ETIMEDOUT; + } + return 0; +} + +const struct mixer_control ** +audiomixer_get_active_controls(struct audiomixer *self, + unsigned int *n_controls) +{ + const struct mixer_control **ret; + + *n_controls = pw_array_get_len(&self->all_mixer_controls, void*); + ret = (const struct mixer_control **) + pw_array_first(&self->all_mixer_controls); + + return ret; +} + +const struct mixer_control * +audiomixer_find_control(struct audiomixer *self, const char *name) +{ + struct mixer_control **ctlptr; + + pw_array_for_each(ctlptr, &self->all_mixer_controls) { + if (!strcmp((*ctlptr)->name, name)) { + return *ctlptr; + } + } + return NULL; +} + +void +audiomixer_add_event_listener(struct audiomixer *self, + const struct audiomixer_events *events, + void *data) +{ + self->events = events; + self->events_data = data; +} + +void +audiomixer_change_volume(struct audiomixer *self, + const struct mixer_control *control, + double volume) +{ + const struct mixer_control_impl *impl = + (const struct mixer_control_impl *) control; + struct endpoint *endpoint = impl->endpoint; + char buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, 1024); + struct spa_pod *param; + + param = spa_pod_builder_add_object(&b, + PW_ENDPOINT_OBJECT_ParamControl, PW_ENDPOINT_PARAM_Control, + PW_ENDPOINT_PARAM_CONTROL_id, SPA_POD_Int(impl->volume_control_id), + PW_ENDPOINT_PARAM_CONTROL_value, SPA_POD_Double(volume), + NULL); + pw_endpoint_proxy_set_param(endpoint->proxy, + PW_ENDPOINT_PARAM_Control, 0, param); +} + +void +audiomixer_change_mute(struct audiomixer *self, + const struct mixer_control *control, + bool mute) +{ + const struct mixer_control_impl *impl = + (const struct mixer_control_impl *) control; + struct endpoint *endpoint = impl->endpoint; + char buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, 1024); + struct spa_pod *param; + + param = spa_pod_builder_add_object(&b, + PW_ENDPOINT_OBJECT_ParamControl, PW_ENDPOINT_PARAM_Control, + PW_ENDPOINT_PARAM_CONTROL_id, SPA_POD_Int(impl->mute_control_id), + PW_ENDPOINT_PARAM_CONTROL_value, SPA_POD_Bool(mute), + NULL); + pw_endpoint_proxy_set_param(endpoint->proxy, + PW_ENDPOINT_PARAM_Control, 0, param); +} diff --git a/binding/audiomixer.h b/binding/audiomixer.h new file mode 100644 index 0000000..f4e83c7 --- /dev/null +++ b/binding/audiomixer.h @@ -0,0 +1,60 @@ +/* + * Copyright © 2019 Collabora Ltd. + * @author George Kiagiadakis <george.kiagiadakis@collabora.com> + * + * SPDX-License-Identifier: MIT + */ + +#include <stdbool.h> + +struct audiomixer; + +struct mixer_control +{ + char name[32]; + double volume; + bool mute; +}; + +struct audiomixer_events +{ + void (*controls_changed) (void *data); + + void (*value_changed) (void *data, +#define MIXER_CONTROL_CHANGE_FLAG_VOLUME (1<<0) +#define MIXER_CONTROL_CHANGE_FLAG_MUTE (1<<1) + unsigned int change_mask, + const struct mixer_control *control); +}; + +struct audiomixer * audiomixer_new(void); +void audiomixer_free(struct audiomixer *self); + +/* locking is required to call any of the methods below + * and to access any structure maintained by audiomixer */ +void audiomixer_lock(struct audiomixer *self); +void audiomixer_unlock(struct audiomixer *self); + +int audiomixer_ensure_connected(struct audiomixer *self, int timeout_sec); +int audiomixer_ensure_controls(struct audiomixer *self, int timeout_sec); + +const struct mixer_control ** audiomixer_get_active_controls( + struct audiomixer *self, + unsigned int *n_controls); + +const struct mixer_control * audiomixer_find_control( + struct audiomixer *self, + const char *name); + +void audiomixer_add_event_listener(struct audiomixer *self, + const struct audiomixer_events *events, + void *data); + +void audiomixer_change_volume(struct audiomixer *self, + const struct mixer_control *control, + double volume); + +void audiomixer_change_mute(struct audiomixer *self, + const struct mixer_control *control, + bool mute); + diff --git a/conf.d/cmake/config.cmake b/conf.d/cmake/config.cmake new file mode 100644 index 0000000..af2e4c2 --- /dev/null +++ b/conf.d/cmake/config.cmake @@ -0,0 +1,158 @@ +########################################################################### +# Copyright 2015, 2016, 2017 IoT.bzh +# +# author: Fulup Ar Foll <fulup@iot.bzh> +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################### + +# Project Info +# ------------------ +set(PROJECT_NAME agl-service-audiomixer) +set(PROJECT_PRETTY_NAME "Audio mixer binding service") +set(PROJECT_DESCRIPTION "Expose PipeWire mixer controls through the AGL Framework") +set(PROJECT_VERSION "0.1") +set(PROJECT_ICON "icon.png") +set(PROJECT_AUTHOR "George Kiagiadakis") +set(PROJECT_AUTHOR_MAIL "george.kiagiadakis@collabora.com") +set(PROJECT_LICENSE "MIT") +set(PROJECT_LANGUAGES,"C") +set(API_NAME "audiomixer") + +# Where are stored the project configuration files +# relative to the root project directory +set(PROJECT_CMAKE_CONF_DIR "conf.d") + +# Where are stored your external libraries for your project. This is 3rd party library that you don't maintain +# but used and must be built and linked. +# set(PROJECT_LIBDIR "libs") + +# Where are stored data for your application. Pictures, static resources must be placed in that folder. +# set(PROJECT_RESOURCES "data") + +# Which directories inspect to find CMakeLists.txt target files +# set(PROJECT_SRC_DIR_PATTERN "*") + +# Compilation Mode (DEBUG, RELEASE) +# ---------------------------------- +set(BUILD_TYPE "RELEASE") + +# Kernel selection if needed. You can choose between a +# mandatory version to impose a minimal version. +# Or check Kernel minimal version and just print a Warning +# about missing features and define a preprocessor variable +# to be used as preprocessor condition in code to disable +# incompatibles features. Preprocessor define is named +# KERNEL_MINIMAL_VERSION_OK. +# +# NOTE*** FOR NOW IT CHECKS KERNEL Yocto environment and +# Yocto SDK Kernel version. +# ----------------------------------------------- +#set(kernel_mandatory_version 4.8) + +# Compiler selection if needed. Impose a minimal version. +# ----------------------------------------------- +set (gcc_minimal_version 4.9) + +# PKG_CONFIG required packages +# ----------------------------- +set (PKG_REQUIRED_LIST + json-c + libsystemd>=222 + afb-daemon + libpipewire-0.3 +) + +# Static constante definition +# ----------------------------- +add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-pthread>) + +# Customize link option +# ----------------------------- +list (APPEND link_libraries -pthread) + +# --------------------------------------------------------------------- +set(INSTALL_PREFIX $ENV{HOME}/opt) + +# Optional location for config.xml.in +# ----------------------------------- +set(WIDGET_CONFIG_TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/conf.d/wgt/config.xml.in) + +# Mandatory widget Mimetype specification of the main unit +# -------------------------------------------------------------------------- +# Choose between : +#- text/html : HTML application, +# content.src designates the home page of the application +# +#- application/vnd.agl.native : AGL compatible native, +# content.src designates the relative path of the binary. +# +# - application/vnd.agl.service: AGL service, content.src is not used. +# +#- ***application/x-executable***: Native application, +# content.src designates the relative path of the binary. +# For such application, only security setup is made. +# +set(WIDGET_TYPE application/vnd.agl.service) + +# Mandatory Widget entry point file of the main unit +# -------------------------------------------------------------- +# This is the file that will be executed, loaded, +# at launch time by the application framework. +# +set(WIDGET_ENTRY_POINT lib/libafm-audiomixer-binding.so) + +# Print a helper message when every thing is finished +# ---------------------------------------------------- +set(CLOSING_MESSAGE "Test with: afb-daemon --rootdir=\$\$(pwd)/package --binding=\$\$(pwd)/package/${WIDGET_ENTRY_POINT} --port=1234 --tracereq=common --token=\"1\" --verbose") +set(PACKAGE_MESSAGE "Install widget file using in the target : afm-util install ${PROJECT_NAME}.wgt") + + + +# Optional dependencies order +# --------------------------- +#set(EXTRA_DEPENDENCIES_ORDER) + +# Optional Extra global include path +# ----------------------------------- +#set(EXTRA_INCLUDE_DIRS) + +# Optional extra libraries +# ------------------------- +#set(EXTRA_LINK_LIBRARIES) + +# Optional force binding installation +# ------------------------------------ +# set(BINDINGS_INSTALL_PREFIX PrefixPath ) + +# Optional force binding Linking flag +# ------------------------------------ +# set(BINDINGS_LINK_FLAG LinkOptions ) + +# Optional force package prefix generation, like widget +# ----------------------------------------------------- +# set(PKG_PREFIX DestinationPath) + +# Optional Application Framework security token +# and port use for remote debugging. +#------------------------------------------------------------ +#set(AFB_TOKEN "" CACHE PATH "Default AFB_TOKEN") +#set(AFB_REMPORT "1234" CACHE PATH "Default AFB_TOKEN") + +# This include is mandatory and MUST happens at the end +# of this file, else you expose you to unexpected behavior +# +# This CMake module could be found at the following url: +# https://gerrit.automotivelinux.org/gerrit/#/admin/projects/src/cmake-apps-module +# ----------------------------------------------------------- +include(CMakeAfbTemplates) diff --git a/conf.d/wgt/config.xml.in b/conf.d/wgt/config.xml.in new file mode 100644 index 0000000..6d535ba --- /dev/null +++ b/conf.d/wgt/config.xml.in @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<widget xmlns="http://www.w3.org/ns/widgets" id="@PROJECT_NAME@" version="@PROJECT_VERSION@"> + <name>@PROJECT_NAME@</name> + <icon src="@PROJECT_ICON@"/> + <content src="@WIDGET_ENTRY_POINT@" type="@WIDGET_TYPE@"/> + <description>@PROJECT_DESCRIPTION@</description> + <author>@PROJECT_AUTHOR@ <@PROJECT_AUTHOR_MAIL@></author> + <license>@PROJECT_LICENSE@</license> + + <feature name="urn:AGL:widget:required-permission"> + <param name="urn:AGL:permission::public:hidden" value="required" /> + <param name="urn:AGL:permission::public:no-htdocs" value="required" /> + </feature> + + <feature name="urn:AGL:widget:provided-api"> + <param name="audiomixer" value="ws" /> + </feature> + + <feature name="urn:AGL:widget:required-binding"> + <param name="@WIDGET_ENTRY_POINT@" value="local" /> + </feature> + +</widget> |