summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt3
-rwxr-xr-xautobuild/agl/autobuild128
-rwxr-xr-xautobuild/linux/autobuild128
-rw-r--r--binding/CMakeLists.txt19
-rw-r--r--binding/audiomixer-binding.c402
-rw-r--r--conf.d/cmake/config.cmake160
-rw-r--r--conf.d/wgt/config.xml.in27
-rw-r--r--meson.build10
-rw-r--r--src/audiomixer-service.cpp129
-rw-r--r--src/audiomixer-service.hpp44
-rw-r--r--src/audiomixer.c (renamed from binding/audiomixer.c)2
-rw-r--r--src/audiomixer.h (renamed from binding/audiomixer.h)12
-rw-r--r--src/main.cpp34
-rw-r--r--src/meson.build18
-rw-r--r--src/vis-config.cpp157
-rw-r--r--src/vis-config.hpp43
-rw-r--r--src/vis-session.cpp374
-rw-r--r--src/vis-session.hpp78
-rw-r--r--systemd/agl-service-audiomixer.service11
-rw-r--r--systemd/meson.build3
-rw-r--r--test/CMakeLists.txt28
-rw-r--r--test/afb-test/CMakeLists.txt21
-rw-r--r--test/afb-test/etc/CMakeLists.txt32
-rw-r--r--test/afb-test/etc/aft-agl-audiomixer.json22
-rw-r--r--test/afb-test/tests/CMakeLists.txt31
-rw-r--r--test/afb-test/tests/audiomixer.lua40
26 files changed, 914 insertions, 1042 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
deleted file mode 100644
index f757721..0000000
--- a/CMakeLists.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-CMAKE_MINIMUM_REQUIRED(VERSION 3.3)
-
-include(${CMAKE_CURRENT_SOURCE_DIR}/conf.d/cmake/config.cmake)
diff --git a/autobuild/agl/autobuild b/autobuild/agl/autobuild
deleted file mode 100755
index 16181b8..0000000
--- a/autobuild/agl/autobuild
+++ /dev/null
@@ -1,128 +0,0 @@
-#!/usr/bin/make -f
-# Copyright (C) 2015 - 2018 "IoT.bzh"
-# Copyright (C) 2020 Konsulko Group
-# 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))
-ROOT_DIR := $(abspath $(dir $(THISFILE))/../..)
-
-# Build directories
-# Note that the debug/test/coverage directories are defined in relation
-# to the release directory (BUILD_DIR), this needs to be kept in mind
-# if over-riding it and building those widget types, the specific widget
-# type variable (e.g. BUILD_DIR_DEBUG) may also need to be specified
-# to yield the desired output hierarchy.
-BUILD_DIR = $(ROOT_DIR)/build
-BUILD_DIR_DEBUG = $(abspath $(BUILD_DIR)/../build-debug)
-BUILD_DIR_TEST = $(abspath $(BUILD_DIR)/../build-test)
-BUILD_DIR_COVERAGE = $(abspath $(BUILD_DIR)/../build-coverage)
-
-# Output directory variable for use in pattern rules.
-# This is intended for internal use only, hence the explicit override
-# definition.
-override OUTPUT_DIR = $(BUILD_DIR)
-
-# Final install directory for widgets
-DEST = $(OUTPUT_DIR)
-
-# Default build type for release/test builds
-BUILD_TYPE = RELEASE
-
-.PHONY: all help update install distclean
-.PHONY: clean clean-release clean-debug clean-test clean-coverage clean-all
-.PHONY: configure configure-release configure-debug configure-test configure-coverage
-.PHONY: build build-release build-debug build-test build-coverage build-all
-.PHONY: package package-release package-debug package-test package-coverage package-all
-
-help:
- @echo "List of targets available:"
- @echo ""
- @echo "- all"
- @echo "- help"
- @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"
-
-all: package-all
-
-# Target specific variable over-rides so static pattern rules can be
-# used for the various type-specific targets.
-
-configure-test build-test package-test clean-test: OUTPUT_DIR = $(BUILD_DIR_TEST)
-
-configure-coverage build-coverage package-coverage clean-coverage: OUTPUT_DIR = $(BUILD_DIR_COVERAGE)
-configure-coverage build-coverage package-coverage: BUILD_TYPE = COVERAGE
-
-configure-debug build-debug package-debug clean-debug: OUTPUT_DIR = $(BUILD_DIR_DEBUG)
-configure-debug build-debug package-debug: BUILD_TYPE = DEBUG
-
-clean-release clean-test clean-debug clean-coverage:
- @if [ -d $(OUTPUT_DIR) ]; then \
- $(MAKE) -C $(OUTPUT_DIR) $(CLEAN_ARGS) clean; \
- else \
- echo Nothing to clean; \
- fi
-
-clean: clean-release
-
-clean-all: clean-release clean-test clean-debug clean-coverage
-
-distclean: clean-all
-
-configure-release configure-test configure-debug configure-coverage:
- @mkdir -p $(OUTPUT_DIR)
- @if [ ! -f $(OUTPUT_DIR)/Makefile ]; then \
- (cd $(OUTPUT_DIR) && cmake -S $(ROOT_DIR) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) $(CONFIGURE_ARGS)); \
- fi
-
-configure: configure-release
-
-build-release build-debug build-coverage: build-%: configure-%
- @cmake --build $(OUTPUT_DIR) $(BUILD_ARGS) --target all
-
-# Kept for consistency, empty to avoid building everything for test widget
-build-test: configure-test
-
-build: build-release
-
-build-all: build-release build-debug build-test build-coverage
-
-package-release package-debug package-coverage: package-%: build-%
- @cmake --build $(OUTPUT_DIR) $(PACKAGE_ARGS) --target widget
- @if [ "$(abspath $(DEST))" != "$(abspath $(OUTPUT_DIR))" ]; then \
- mkdir -p $(DEST) && cp $(OUTPUT_DIR)/*.wgt $(DEST); \
- fi
-
-package-test: build-test
- @cmake --build $(OUTPUT_DIR) $(PACKAGE_ARGS) --target test_widget
- @if [ "$(abspath $(DEST))" != "$(abspath $(OUTPUT_DIR))" ]; then \
- mkdir -p $(DEST) && cp $(OUTPUT_DIR)/*.wgt $(DEST); \
- fi
-
-package: package-release
-
-package-all: package-release package-test package-coverage package-debug
-
-update: configure
- @cmake --build $(BUILD_DIR) --target autobuild
-
-install: build
- @cmake --build $(BUILD_DIR) $(INSTALL_ARGS) --target install
diff --git a/autobuild/linux/autobuild b/autobuild/linux/autobuild
deleted file mode 100755
index 16181b8..0000000
--- a/autobuild/linux/autobuild
+++ /dev/null
@@ -1,128 +0,0 @@
-#!/usr/bin/make -f
-# Copyright (C) 2015 - 2018 "IoT.bzh"
-# Copyright (C) 2020 Konsulko Group
-# 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))
-ROOT_DIR := $(abspath $(dir $(THISFILE))/../..)
-
-# Build directories
-# Note that the debug/test/coverage directories are defined in relation
-# to the release directory (BUILD_DIR), this needs to be kept in mind
-# if over-riding it and building those widget types, the specific widget
-# type variable (e.g. BUILD_DIR_DEBUG) may also need to be specified
-# to yield the desired output hierarchy.
-BUILD_DIR = $(ROOT_DIR)/build
-BUILD_DIR_DEBUG = $(abspath $(BUILD_DIR)/../build-debug)
-BUILD_DIR_TEST = $(abspath $(BUILD_DIR)/../build-test)
-BUILD_DIR_COVERAGE = $(abspath $(BUILD_DIR)/../build-coverage)
-
-# Output directory variable for use in pattern rules.
-# This is intended for internal use only, hence the explicit override
-# definition.
-override OUTPUT_DIR = $(BUILD_DIR)
-
-# Final install directory for widgets
-DEST = $(OUTPUT_DIR)
-
-# Default build type for release/test builds
-BUILD_TYPE = RELEASE
-
-.PHONY: all help update install distclean
-.PHONY: clean clean-release clean-debug clean-test clean-coverage clean-all
-.PHONY: configure configure-release configure-debug configure-test configure-coverage
-.PHONY: build build-release build-debug build-test build-coverage build-all
-.PHONY: package package-release package-debug package-test package-coverage package-all
-
-help:
- @echo "List of targets available:"
- @echo ""
- @echo "- all"
- @echo "- help"
- @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"
-
-all: package-all
-
-# Target specific variable over-rides so static pattern rules can be
-# used for the various type-specific targets.
-
-configure-test build-test package-test clean-test: OUTPUT_DIR = $(BUILD_DIR_TEST)
-
-configure-coverage build-coverage package-coverage clean-coverage: OUTPUT_DIR = $(BUILD_DIR_COVERAGE)
-configure-coverage build-coverage package-coverage: BUILD_TYPE = COVERAGE
-
-configure-debug build-debug package-debug clean-debug: OUTPUT_DIR = $(BUILD_DIR_DEBUG)
-configure-debug build-debug package-debug: BUILD_TYPE = DEBUG
-
-clean-release clean-test clean-debug clean-coverage:
- @if [ -d $(OUTPUT_DIR) ]; then \
- $(MAKE) -C $(OUTPUT_DIR) $(CLEAN_ARGS) clean; \
- else \
- echo Nothing to clean; \
- fi
-
-clean: clean-release
-
-clean-all: clean-release clean-test clean-debug clean-coverage
-
-distclean: clean-all
-
-configure-release configure-test configure-debug configure-coverage:
- @mkdir -p $(OUTPUT_DIR)
- @if [ ! -f $(OUTPUT_DIR)/Makefile ]; then \
- (cd $(OUTPUT_DIR) && cmake -S $(ROOT_DIR) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) $(CONFIGURE_ARGS)); \
- fi
-
-configure: configure-release
-
-build-release build-debug build-coverage: build-%: configure-%
- @cmake --build $(OUTPUT_DIR) $(BUILD_ARGS) --target all
-
-# Kept for consistency, empty to avoid building everything for test widget
-build-test: configure-test
-
-build: build-release
-
-build-all: build-release build-debug build-test build-coverage
-
-package-release package-debug package-coverage: package-%: build-%
- @cmake --build $(OUTPUT_DIR) $(PACKAGE_ARGS) --target widget
- @if [ "$(abspath $(DEST))" != "$(abspath $(OUTPUT_DIR))" ]; then \
- mkdir -p $(DEST) && cp $(OUTPUT_DIR)/*.wgt $(DEST); \
- fi
-
-package-test: build-test
- @cmake --build $(OUTPUT_DIR) $(PACKAGE_ARGS) --target test_widget
- @if [ "$(abspath $(DEST))" != "$(abspath $(OUTPUT_DIR))" ]; then \
- mkdir -p $(DEST) && cp $(OUTPUT_DIR)/*.wgt $(DEST); \
- fi
-
-package: package-release
-
-package-all: package-release package-test package-coverage package-debug
-
-update: configure
- @cmake --build $(BUILD_DIR) --target autobuild
-
-install: build
- @cmake --build $(BUILD_DIR) $(INSTALL_ARGS) --target install
diff --git a/binding/CMakeLists.txt b/binding/CMakeLists.txt
deleted file mode 100644
index c199ad2..0000000
--- a/binding/CMakeLists.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-PROJECT_TARGET_ADD(audiomixer-binding)
-
- add_definitions(-DAFB_BINDING_VERSION=3)
-
- 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
deleted file mode 100644
index 2c8981a..0000000
--- a/binding/audiomixer-binding.c
+++ /dev/null
@@ -1,402 +0,0 @@
-/*
- * Copyright © 2019 Collabora Ltd.
- * Copyright © 2019 Konsulko Group
- * @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 const char *signalcomposer_events[] = {
- "event.volume.up",
- "event.volume.down",
- "event.volume.mute",
- NULL,
-};
-
-static void
-audiomixer_controls_changed_deferred(int signum, void *arg)
-{
- AFB_DEBUG("controls changed");
- afb_event_push(controls_changed, NULL);
-}
-
-struct value_changed_data
-{
- unsigned int change_mask;
- struct mixer_control control;
-};
-
-static void
-audiomixer_value_changed_deferred(int signum, void *data)
-{
- struct value_changed_data *d = data;
- json_object *json;
-
- AFB_DEBUG("value changed");
-
- 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);
- }
-
- free(d);
-}
-
-/* called in audiomixer's thread */
-static void
-audiomixer_controls_changed(void *data)
-{
- afb_api_t api = data;
- afb_api_queue_job (api, audiomixer_controls_changed_deferred, NULL,
- (void *) 0x1, 0);
-}
-
-
-/* called in audiomixer's thread */
-static void
-audiomixer_value_changed(void *data,
- unsigned int change_mask,
- const struct mixer_control *control)
-{
- afb_api_t api = data;
- struct value_changed_data *d = calloc(1, sizeof(*d));
-
- d->change_mask = change_mask;
- d->control = *control;
-
- afb_api_queue_job (api, audiomixer_value_changed_deferred, d,
- (void *) 0x1, 0);
-}
-
-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)
-{
- int ret;
-
- ret = afb_daemon_require_api("signal-composer", 1);
- if (ret) {
- AFB_WARNING("unable to initialize signal-composer binding");
- } else {
- const char **tmp = signalcomposer_events;
- json_object *args = json_object_new_object();
- json_object *signals = json_object_new_array();
-
- while (*tmp) {
- json_object_array_add(signals, json_object_new_string(*tmp++));
- }
- json_object_object_add(args, "signal", signals);
- if(json_object_array_length(signals)) {
- afb_api_call_sync(api, "signal-composer", "subscribe",
- args, NULL, NULL, NULL);
- } else {
- json_object_put(args);
- }
- }
-
- 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, api);
-
- 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_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);
-
- 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_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_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 void
-onevent(afb_api_t api, const char *event, struct json_object *object)
-{
- const struct mixer_control *ctl;
- json_object *tmp = NULL;
- const char *uid;
- const char *value;
-
- json_object_object_get_ex(object, "uid", &tmp);
- if (tmp == NULL)
- return;
-
- uid = json_object_get_string(tmp);
- if (strncmp(uid, "event.volume.", 13))
- return;
-
- json_object_object_get_ex(object, "value", &tmp);
- if (tmp == NULL)
- return;
-
- value = json_object_get_string(tmp);
- if (strncmp(value, "true", 4))
- return;
-
- audiomixer_lock(audiomixer);
-
- ctl = audiomixer_find_control(audiomixer, "Master Playback");
- if (!ctl)
- goto unlock;
-
- if (!strcmp(uid, "event.volume.mute")) {
- audiomixer_change_mute(audiomixer, ctl, !ctl->mute);
- } else {
- double volume = ctl->volume;
-
- if (!strcmp(uid, "event.volume.up")) {
- volume += 0.05; // up 5%
- if (volume > 1.0)
- volume = 1.0; // clamp to 100%
- } else if (!strcmp(uid, "event.volume.down")) {
- volume -= 0.05; // down 5%
- if (volume < 0.0)
- volume = 0.0; // clamp to 0%
- } else {
- AFB_WARNING("Unhandled signal-composer uid '%s'", uid);
- goto unlock;
- }
- audiomixer_change_volume(audiomixer, ctl, volume);
- }
-
-unlock:
- audiomixer_unlock(audiomixer);
-}
-
-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,
- .onevent = onevent,
- .init = init,
-};
diff --git a/conf.d/cmake/config.cmake b/conf.d/cmake/config.cmake
deleted file mode 100644
index 09d91ae..0000000
--- a/conf.d/cmake/config.cmake
+++ /dev/null
@@ -1,160 +0,0 @@
-###########################################################################
-# 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_URL https://gerrit.automotivelinux.org/gerrit/admin/repos/apps/agl-service-audiomixer)
-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
- wireplumber-0.4
-)
-
-# 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
deleted file mode 100644
index 056c64b..0000000
--- a/conf.d/wgt/config.xml.in
+++ /dev/null
@@ -1,27 +0,0 @@
-<?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@ &lt;@PROJECT_AUTHOR_MAIL@&gt;</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" />
- <param name="urn:AGL:permission::system:run-by-default" 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>
-
- <feature name="urn:AGL:widget:required-api">
- <param name="signal-composer" value="ws" />
- </feature>
-</widget>
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..47e88a4
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,10 @@
+project('agl-service-audiomixer',
+ ['cpp', 'c'],
+ license : 'Apache-2.0',
+ default_options : ['c_std=c17', 'cpp_std=c++17'])
+
+systemd_dep = dependency('systemd')
+
+subdir('src')
+subdir('systemd')
+
diff --git a/src/audiomixer-service.cpp b/src/audiomixer-service.cpp
new file mode 100644
index 0000000..5787153
--- /dev/null
+++ b/src/audiomixer-service.cpp
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "audiomixer-service.hpp"
+#include <iostream>
+#include <algorithm>
+
+
+AudiomixerService::AudiomixerService(const VisConfig &config, net::io_context& ioc, ssl::context& ctx) :
+ VisSession(config, ioc, ctx)
+{
+ m_audiomixer = audiomixer_new();
+ if (m_audiomixer) {
+ // Set up callbacks for WirePlumber events
+ m_audiomixer_events.controls_changed = audiomixer_control_change_cb;
+ m_audiomixer_events.value_changed = audiomixer_value_change_cb;
+ audiomixer_add_event_listener(m_audiomixer, &m_audiomixer_events, this);
+
+ // Drive connecting to PipeWire core and refreshing controls list
+ audiomixer_lock(m_audiomixer);
+ audiomixer_ensure_controls(m_audiomixer, 3);
+ audiomixer_unlock(m_audiomixer);
+ } else {
+ std::cerr << "Could not create WirePlumber connection" << std::endl;
+ }
+}
+
+AudiomixerService::~AudiomixerService()
+{
+ audiomixer_free(m_audiomixer);
+}
+
+void AudiomixerService::handle_authorized_response(void)
+{
+ subscribe("Vehicle.Cabin.Infotainment.Media.Volume");
+ subscribe("Vehicle.Cabin.SteeringWheel.Switches.VolumeUp");
+ subscribe("Vehicle.Cabin.SteeringWheel.Switches.VolumeDown");
+ subscribe("Vehicle.Cabin.SteeringWheel.Switches.VolumeMute");
+
+ // Set initial volume in VSS
+ // For now a value of 50 matches the default in the homescreen app.
+ // Ideally there would be some form of persistence scheme to restore
+ // the last value on restart.
+ set("Vehicle.Cabin.Infotainment.Media.Volume", "50");
+}
+
+void AudiomixerService::handle_get_response(std::string &path, std::string &value, std::string &timestamp)
+{
+ // Placeholder since no gets are performed ATM
+}
+
+void AudiomixerService::handle_notification(std::string &path, std::string &value, std::string &timestamp)
+{
+ if (!m_audiomixer) {
+ return;
+ }
+
+ audiomixer_lock(m_audiomixer);
+
+ const struct mixer_control *ctl = audiomixer_find_control(m_audiomixer, "Master Playback");
+ if (!ctl) {
+ audiomixer_unlock(m_audiomixer);
+ return;
+ }
+
+ if (path == "Vehicle.Cabin.Infotainment.Media.Volume") {
+ try {
+ int volume = std::stoi(value);
+ if (volume >= 0 && volume <= 100) {
+ double v = (double) volume / 100.0;
+ if (m_config.verbose() > 1)
+ std::cout << "Setting volume to " << v << std::endl;
+ audiomixer_change_volume(m_audiomixer, ctl, v);
+ }
+ }
+ catch (std::exception ex) {
+ // ignore bad value
+ }
+ } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeUp" && value == "true") {
+ double volume = ctl->volume;
+ volume += 0.05; // up 5%
+ if (volume > 1.0)
+ volume = 1.0; // clamp to 100%
+ if (m_config.verbose() > 1)
+ std::cout << "Increasing volume to " << volume << std::endl;
+ audiomixer_change_volume(m_audiomixer, ctl, volume);
+
+ } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeDown" && value == "true") {
+ double volume = ctl->volume;
+ volume -= 0.05; // down 5%
+ if (volume < 0.0)
+ volume = 0.0; // clamp to 0%
+ if (m_config.verbose() > 1)
+ std::cout << "Decreasing volume to " << volume << std::endl;
+ audiomixer_change_volume(m_audiomixer, ctl, volume);
+
+ } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeMute" && value == "true") {
+ if (m_config.verbose() > 1) {
+ if (ctl->mute)
+ std::cout << "Unmuting" << std::endl;
+ else
+ std::cout << "Muting" << std::endl;
+ }
+ audiomixer_change_mute(m_audiomixer, ctl, !ctl->mute);
+ }
+ // else ignore
+
+ audiomixer_unlock(m_audiomixer);
+}
+
+void AudiomixerService::handle_control_change(void)
+{
+ // Ignore for now
+}
+
+void AudiomixerService::handle_value_change(unsigned int change_mask, const struct mixer_control *control)
+{
+ if (!control)
+ return;
+
+ if (change_mask & MIXER_CONTROL_CHANGE_FLAG_VOLUME) {
+ if (std::string(control->name) == "Master Playback") {
+ // Push change into VIS
+ std::string value = std::to_string((int) (control->volume * 100.0));
+ set("Vehicle.Cabin.Infotainment.Media.Volume", value);
+ }
+ } else if (change_mask & MIXER_CONTROL_CHANGE_FLAG_MUTE) {
+ // For now, do nothing, new state is in control->mute
+ }
+}
diff --git a/src/audiomixer-service.hpp b/src/audiomixer-service.hpp
new file mode 100644
index 0000000..cb00584
--- /dev/null
+++ b/src/audiomixer-service.hpp
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _AUDIOMIXER_SERVICE_HPP
+#define _AUDIOMIXER_SERVICE_HPP
+
+#include "vis-session.hpp"
+#include "audiomixer.h"
+
+class AudiomixerService : public VisSession
+{
+ struct audiomixer *m_audiomixer;
+
+public:
+ AudiomixerService(const VisConfig &config, net::io_context& ioc, ssl::context& ctx);
+
+ ~AudiomixerService();
+
+ static void audiomixer_control_change_cb(void *data) {
+ if (data)
+ ((AudiomixerService*) data)->handle_control_change();
+ };
+
+ static void audiomixer_value_change_cb(void *data,
+ unsigned int change_mask,
+ const struct mixer_control *control) {
+ if (data)
+ ((AudiomixerService*) data)->handle_value_change(change_mask, control);
+ }
+
+protected:
+ struct audiomixer_events m_audiomixer_events;
+
+ virtual void handle_authorized_response(void) override;
+
+ virtual void handle_get_response(std::string &path, std::string &value, std::string &timestamp) override;
+
+ virtual void handle_notification(std::string &path, std::string &value, std::string &timestamp) override;
+
+ virtual void handle_control_change(void);
+
+ virtual void handle_value_change(unsigned int change_mask, const struct mixer_control *control);
+};
+
+#endif // _AUDIOMIXER_SERVICE_HPP
diff --git a/binding/audiomixer.c b/src/audiomixer.c
index 97ad622..a40a38e 100644
--- a/binding/audiomixer.c
+++ b/src/audiomixer.c
@@ -433,7 +433,7 @@ do_change_mute (struct action * action)
gboolean ret = FALSE;
g_variant_builder_add (&b, "{sv}", "mute",
- g_variant_new_double (action->change_mute.mute));
+ g_variant_new_boolean (action->change_mute.mute));
g_signal_emit_by_name (self->mixer_api, "set-volume",
action->change_mute.id, g_variant_builder_end (&b), &ret);
if (!ret)
diff --git a/binding/audiomixer.h b/src/audiomixer.h
index 47bd703..cc67a83 100644
--- a/binding/audiomixer.h
+++ b/src/audiomixer.h
@@ -5,6 +5,13 @@
* SPDX-License-Identifier: MIT
*/
+#ifndef _AUDIOMIXER_H
+#define _AUDIOMIXER_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
#include <stdbool.h>
struct audiomixer;
@@ -57,3 +64,8 @@ void audiomixer_change_mute(struct audiomixer *self,
const struct mixer_control *control,
bool mute);
+#ifdef __cplusplus
+}
+#endif
+
+#endif // _AUDIOMIXER_H
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..0960f1b
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <iostream>
+#include <iomanip>
+#include <boost/asio/signal_set.hpp>
+#include <boost/bind.hpp>
+#include "audiomixer-service.hpp"
+
+using work_guard_type = boost::asio::executor_work_guard<boost::asio::io_context::executor_type>;
+
+int main(int argc, char** argv)
+{
+ // The io_context is required for all I/O
+ net::io_context ioc;
+
+ // Register to stop I/O context on SIGINT and SIGTERM
+ net::signal_set signals(ioc, SIGINT, SIGTERM);
+ signals.async_wait(boost::bind(&net::io_context::stop, &ioc));
+
+ // The SSL context is required, and holds certificates
+ ssl::context ctx{ssl::context::tlsv12_client};
+
+ // Launch the asynchronous operation
+ VisConfig config("agl-service-audiomixer");
+ std::make_shared<AudiomixerService>(config, ioc, ctx)->run();
+
+ // Ensure I/O context continues running even if there's no work
+ work_guard_type work_guard(ioc.get_executor());
+
+ // Run the I/O context
+ ioc.run();
+
+ return 0;
+}
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..282d130
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,18 @@
+boost_dep = dependency('boost',
+ version : '>=1.72',
+ modules : [ 'thread', 'filesystem', 'program_options', 'log', 'system' ])
+openssl_dep = dependency('openssl')
+thread_dep = dependency('threads')
+wp_dep = dependency('wireplumber-0.4')
+
+src = [ 'vis-config.cpp',
+ 'vis-session.cpp',
+ 'audiomixer-service.cpp',
+ 'audiomixer.c',
+ 'main.cpp'
+]
+executable('agl-service-audiomixer',
+ src,
+ dependencies: [boost_dep, openssl_dep, thread_dep, systemd_dep, wp_dep],
+ install: true,
+ install_dir : get_option('sbindir'))
diff --git a/src/vis-config.cpp b/src/vis-config.cpp
new file mode 100644
index 0000000..b8d9266
--- /dev/null
+++ b/src/vis-config.cpp
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "vis-config.hpp"
+#include <iostream>
+#include <iomanip>
+#include <sstream>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+#include <boost/filesystem.hpp>
+
+namespace property_tree = boost::property_tree;
+namespace filesystem = boost::filesystem;
+
+#define DEFAULT_CLIENT_KEY_FILE "/etc/kuksa-val/Client.key"
+#define DEFAULT_CLIENT_CERT_FILE "/etc/kuksa-val/Client.pem"
+#define DEFAULT_CA_CERT_FILE "/etc/kuksa-val/CA.pem"
+
+
+VisConfig::VisConfig(const std::string &hostname,
+ const unsigned port,
+ const std::string &clientKey,
+ const std::string &clientCert,
+ const std::string &caCert,
+ const std::string &authToken,
+ bool verifyPeer) :
+ m_hostname(hostname),
+ m_port(port),
+ m_clientKey(clientKey),
+ m_clientCert(clientCert),
+ m_caCert(caCert),
+ m_authToken(authToken),
+ m_verifyPeer(verifyPeer),
+ m_verbose(0),
+ m_valid(true)
+{
+ // Potentially could do some certificate validation here...
+}
+
+VisConfig::VisConfig(const std::string &appname) :
+ m_valid(false)
+{
+ std::string config("/etc/xdg/AGL/");
+ config += appname;
+ config += ".conf";
+ char *home = getenv("XDG_CONFIG_HOME");
+ if (home) {
+ config = home;
+ config += "/AGL/";
+ config += appname;
+ config += ".conf";
+ }
+
+ std::cout << "Using configuration " << config << std::endl;
+ property_tree::ptree pt;
+ try {
+ property_tree::ini_parser::read_ini(config, pt);
+ }
+ catch (std::exception &ex) {
+ std::cerr << "Could not read " << config << std::endl;
+ return;
+ }
+ const property_tree::ptree &settings =
+ pt.get_child("vis-client", property_tree::ptree());
+
+ m_hostname = settings.get("server", "localhost");
+ std::stringstream ss;
+ ss << m_hostname;
+ ss >> std::quoted(m_hostname);
+ if (m_hostname.empty()) {
+ std::cerr << "Invalid server hostname" << std::endl;
+ return;
+ }
+
+ m_port = settings.get("port", 8090);
+ if (m_port == 0) {
+ std::cerr << "Invalid server port" << std::endl;
+ return;
+ }
+
+ // Default to disabling peer verification for now to be able
+ // to use the default upstream KUKSA.val certificates for
+ // testing. Wrangling server and CA certificate generation
+ // and management to be able to verify will require further
+ // investigation.
+ m_verifyPeer = settings.get("verify-server", false);
+
+ std::string keyFileName = settings.get("key", DEFAULT_CLIENT_KEY_FILE);
+ std::stringstream().swap(ss);
+ ss << keyFileName;
+ ss >> std::quoted(keyFileName);
+ ss.str("");
+ if (keyFileName.empty()) {
+ std::cerr << "Invalid client key filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(keyFileName, m_clientKey);
+ if (m_clientKey.empty()) {
+ std::cerr << "Invalid client key file" << std::endl;
+ return;
+ }
+
+ std::string certFileName = settings.get("certificate", DEFAULT_CLIENT_CERT_FILE);
+ std::stringstream().swap(ss);
+ ss << certFileName;
+ ss >> std::quoted(certFileName);
+ if (certFileName.empty()) {
+ std::cerr << "Invalid client certificate filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(certFileName, m_clientCert);
+ if (m_clientCert.empty()) {
+ std::cerr << "Invalid client certificate file" << std::endl;
+ return;
+ }
+
+ std::string caCertFileName = settings.get("ca-certificate", DEFAULT_CA_CERT_FILE);
+ std::stringstream().swap(ss);
+ ss << caCertFileName;
+ ss >> std::quoted(caCertFileName);
+ if (caCertFileName.empty()) {
+ std::cerr << "Invalid CA certificate filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(caCertFileName, m_caCert);
+ if (m_caCert.empty()) {
+ std::cerr << "Invalid CA certificate file" << std::endl;
+ return;
+ }
+
+ std::string authTokenFileName = settings.get("authorization", "");
+ std::stringstream().swap(ss);
+ ss << authTokenFileName;
+ ss >> std::quoted(authTokenFileName);
+ if (authTokenFileName.empty()) {
+ std::cerr << "Invalid authorization token filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(authTokenFileName, m_authToken);
+ if (m_authToken.empty()) {
+ std::cerr << "Invalid authorization token file" << std::endl;
+ return;
+ }
+
+ m_verbose = 0;
+ std::string verbose = settings.get("verbose", "");
+ std::stringstream().swap(ss);
+ ss << verbose;
+ ss >> std::quoted(verbose);
+ if (!verbose.empty()) {
+ if (verbose == "true" || verbose == "1")
+ m_verbose = 1;
+ if (verbose == "2")
+ m_verbose = 2;
+ }
+
+ m_valid = true;
+}
diff --git a/src/vis-config.hpp b/src/vis-config.hpp
new file mode 100644
index 0000000..b0f72f9
--- /dev/null
+++ b/src/vis-config.hpp
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _VIS_CONFIG_HPP
+#define _VIS_CONFIG_HPP
+
+#include <string>
+
+class VisConfig
+{
+public:
+ explicit VisConfig(const std::string &hostname,
+ const unsigned port,
+ const std::string &clientKey,
+ const std::string &clientCert,
+ const std::string &caCert,
+ const std::string &authToken,
+ bool verifyPeer = true);
+ explicit VisConfig(const std::string &appname);
+ ~VisConfig() {};
+
+ std::string hostname() { return m_hostname; };
+ unsigned port() { return m_port; };
+ std::string clientKey() { return m_clientKey; };
+ std::string clientCert() { return m_clientCert; };
+ std::string caCert() { return m_caCert; };
+ std::string authToken() { return m_authToken; };
+ bool verifyPeer() { return m_verifyPeer; };
+ bool valid() { return m_valid; };
+ unsigned verbose() { return m_verbose; };
+
+private:
+ std::string m_hostname;
+ unsigned m_port;
+ std::string m_clientKey;
+ std::string m_clientCert;
+ std::string m_caCert;
+ std::string m_authToken;
+ bool m_verifyPeer;
+ unsigned m_verbose;
+ bool m_valid;
+};
+
+#endif // _VIS_CONFIG_HPP
diff --git a/src/vis-session.cpp b/src/vis-session.cpp
new file mode 100644
index 0000000..880e3ae
--- /dev/null
+++ b/src/vis-session.cpp
@@ -0,0 +1,374 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "vis-session.hpp"
+#include <iostream>
+#include <sstream>
+#include <thread>
+
+
+// Logging helper
+static void log_error(beast::error_code error, char const* what)
+{
+ std::cerr << what << " error: " << error.message() << std::endl;
+}
+
+
+// Resolver and socket require an io_context
+VisSession::VisSession(const VisConfig &config, net::io_context& ioc, ssl::context& ctx) :
+ m_config(config),
+ m_resolver(net::make_strand(ioc)),
+ m_ws(net::make_strand(ioc), ctx)
+{
+}
+
+// Start the asynchronous operation
+void VisSession::run()
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ // Start by resolving hostname
+ m_resolver.async_resolve(m_config.hostname(),
+ std::to_string(m_config.port()),
+ beast::bind_front_handler(&VisSession::on_resolve,
+ shared_from_this()));
+}
+
+void VisSession::on_resolve(beast::error_code error,
+ tcp::resolver::results_type results)
+{
+ if(error) {
+ log_error(error, "resolve");
+ return;
+ }
+
+ // Set a timeout on the connect operation
+ beast::get_lowest_layer(m_ws).expires_after(std::chrono::seconds(30));
+
+ // Connect to resolved address
+ if (m_config.verbose())
+ std::cout << "Connecting" << std::endl;
+ m_results = results;
+ connect();
+}
+
+void VisSession::connect()
+{
+ beast::get_lowest_layer(m_ws).async_connect(m_results,
+ beast::bind_front_handler(&VisSession::on_connect,
+ shared_from_this()));
+}
+
+void VisSession::on_connect(beast::error_code error,
+ tcp::resolver::results_type::endpoint_type endpoint)
+{
+ if(error) {
+ // The server can take a while to be ready to accept connections,
+ // so keep retrying until we hit the timeout.
+ if (error == net::error::timed_out) {
+ log_error(error, "connect");
+ return;
+ }
+
+ // Delay 500 ms before retrying
+ std::this_thread::sleep_for(std::chrono::milliseconds(500));
+
+ if (m_config.verbose())
+ std::cout << "Connecting" << std::endl;
+
+ connect();
+ return;
+ }
+
+ if (m_config.verbose())
+ std::cout << "Connected" << std::endl;
+
+ // Set handshake timeout
+ beast::get_lowest_layer(m_ws).expires_after(std::chrono::seconds(30));
+
+ // Set SNI Hostname (many hosts need this to handshake successfully)
+ if(!SSL_set_tlsext_host_name(m_ws.next_layer().native_handle(),
+ m_config.hostname().c_str()))
+ {
+ error = beast::error_code(static_cast<int>(::ERR_get_error()),
+ net::error::get_ssl_category());
+ log_error(error, "connect");
+ return;
+ }
+
+ // Update the hostname. This will provide the value of the
+ // Host HTTP header during the WebSocket handshake.
+ // See https://tools.ietf.org/html/rfc7230#section-5.4
+ m_hostname = m_config.hostname() + ':' + std::to_string(endpoint.port());
+
+ if (m_config.verbose())
+ std::cout << "Negotiating SSL handshake" << std::endl;
+
+ // Perform the SSL handshake
+ m_ws.next_layer().async_handshake(ssl::stream_base::client,
+ beast::bind_front_handler(&VisSession::on_ssl_handshake,
+ shared_from_this()));
+}
+
+void VisSession::on_ssl_handshake(beast::error_code error)
+{
+ if(error) {
+ log_error(error, "SSL handshake");
+ return;
+ }
+
+ // Turn off the timeout on the tcp_stream, because
+ // the websocket stream has its own timeout system.
+ beast::get_lowest_layer(m_ws).expires_never();
+
+ // NOTE: Explicitly not setting websocket stream timeout here,
+ // as the client is long-running.
+
+ if (m_config.verbose())
+ std::cout << "Negotiating WSS handshake" << std::endl;
+
+ // Perform handshake
+ m_ws.async_handshake(m_hostname,
+ "/",
+ beast::bind_front_handler(&VisSession::on_handshake,
+ shared_from_this()));
+}
+
+void VisSession::on_handshake(beast::error_code error)
+{
+ if(error) {
+ log_error(error, "WSS handshake");
+ return;
+ }
+
+ if (m_config.verbose())
+ std::cout << "Authorizing" << std::endl;
+
+ // Authorize
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"]= "authorize";
+ req["tokens"] = m_config.authToken();
+
+ m_ws.async_write(net::buffer(req.dump(4)),
+ beast::bind_front_handler(&VisSession::on_authorize,
+ shared_from_this()));
+}
+
+void VisSession::on_authorize(beast::error_code error, std::size_t bytes_transferred)
+{
+ boost::ignore_unused(bytes_transferred);
+
+ if(error) {
+ log_error(error, "authorize");
+ return;
+ }
+
+ // Read response
+ m_ws.async_read(m_buffer,
+ beast::bind_front_handler(&VisSession::on_read,
+ shared_from_this()));
+}
+
+// NOTE: Placeholder for now
+void VisSession::on_write(beast::error_code error, std::size_t bytes_transferred)
+{
+ boost::ignore_unused(bytes_transferred);
+
+ if(error) {
+ log_error(error, "write");
+ return;
+ }
+
+ // Do nothing...
+}
+
+void VisSession::on_read(beast::error_code error, std::size_t bytes_transferred)
+{
+ boost::ignore_unused(bytes_transferred);
+
+ if(error) {
+ log_error(error, "read");
+ return;
+ }
+
+ // Handle message
+ std::string s = beast::buffers_to_string(m_buffer.data());
+ json response = json::parse(s, nullptr, false);
+ if (!response.is_discarded()) {
+ handle_message(response);
+ } else {
+ std::cerr << "json::parse failed? got " << s << std::endl;
+ }
+ m_buffer.consume(m_buffer.size());
+
+ // Read next message
+ m_ws.async_read(m_buffer,
+ beast::bind_front_handler(&VisSession::on_read,
+ shared_from_this()));
+}
+
+void VisSession::get(const std::string &path)
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"] = "get";
+ req["path"] = path;
+ req["tokens"] = m_config.authToken();
+
+ m_ws.write(net::buffer(req.dump(4)));
+}
+
+void VisSession::set(const std::string &path, const std::string &value)
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"] = "set";
+ req["path"] = path;
+ req["value"] = value;
+ req["tokens"] = m_config.authToken();
+
+ m_ws.write(net::buffer(req.dump(4)));
+}
+
+void VisSession::subscribe(const std::string &path)
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"] = "subscribe";
+ req["path"] = path;
+ req["tokens"] = m_config.authToken();
+
+ m_ws.write(net::buffer(req.dump(4)));
+}
+
+bool VisSession::parseData(const json &message, std::string &path, std::string &value, std::string &timestamp)
+{
+ if (message.contains("error")) {
+ std::string error = message["error"];
+ return false;
+ }
+
+ if (!(message.contains("data") && message["data"].is_object())) {
+ std::cerr << "Malformed message (data missing)" << std::endl;
+ return false;
+ }
+ auto data = message["data"];
+ if (!(data.contains("path") && data["path"].is_string())) {
+ std::cerr << "Malformed message (path missing)" << std::endl;
+ return false;
+ }
+ path = data["path"];
+ // Convert '/' to '.' in paths to ensure consistency for clients
+ std::replace(path.begin(), path.end(), '/', '.');
+
+ if (!(data.contains("dp") && data["dp"].is_object())) {
+ std::cerr << "Malformed message (datapoint missing)" << std::endl;
+ return false;
+ }
+ auto dp = data["dp"];
+ if (!dp.contains("value")) {
+ std::cerr << "Malformed message (value missing)" << std::endl;
+ return false;
+ } else if (dp["value"].is_string()) {
+ value = dp["value"];
+ } else if (dp["value"].is_number_float()) {
+ double num = dp["value"];
+ value = std::to_string(num);
+ } else if (dp["value"].is_boolean()) {
+ value = dp["value"] ? "true" : "false";
+ } else {
+ std::cerr << "Malformed message (unsupported value type)" << std::endl;
+ return false;
+ }
+
+ if (!(dp.contains("ts") && dp["ts"].is_string())) {
+ std::cerr << "Malformed message (timestamp missing)" << std::endl;
+ return false;
+ }
+ timestamp = dp["ts"];
+
+ return true;
+}
+
+void VisSession::handle_message(const json &message)
+{
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: enter, message = " << to_string(message) << std::endl;
+
+ if (!message.contains("action")) {
+ std::cerr << "Received unknown message (no action), discarding" << std::endl;
+ return;
+ }
+
+ std::string action = message["action"];
+ if (action == "authorize") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS authorization failed: " << error << std::endl;
+ } else {
+ if (m_config.verbose() > 1)
+ std::cout << "authorized" << std::endl;
+
+ handle_authorized_response();
+ }
+ } else if (action == "subscribe") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS subscription failed: " << error << std::endl;
+ }
+ } else if (action == "get") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS get failed: " << error << std::endl;
+ } else {
+ std::string path, value, ts;
+ if (parseData(message, path, value, ts)) {
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: got response " << path << " = " << value << std::endl;
+
+ handle_get_response(path, value, ts);
+ }
+ }
+ } else if (action == "set") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS set failed: " << error;
+ }
+ } else if (action == "subscription") {
+ std::string path, value, ts;
+ if (parseData(message, path, value, ts)) {
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: got notification " << path << " = " << value << std::endl;
+
+ handle_notification(path, value, ts);
+ }
+ } else {
+ std::cerr << "unhandled VIS response of type: " << action;
+ }
+
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: exit" << std::endl;
+}
+
diff --git a/src/vis-session.hpp b/src/vis-session.hpp
new file mode 100644
index 0000000..8c7b0d9
--- /dev/null
+++ b/src/vis-session.hpp
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _VIS_SESSION_HPP
+#define _VIS_SESSION_HPP
+
+#include "vis-config.hpp"
+#include <atomic>
+#include <string>
+#include <boost/beast/core.hpp>
+#include <boost/beast/ssl.hpp>
+#include <boost/beast/websocket.hpp>
+#include <boost/beast/websocket/ssl.hpp>
+#include <boost/asio/strand.hpp>
+#include <nlohmann/json.hpp>
+
+namespace beast = boost::beast;
+namespace websocket = beast::websocket;
+namespace net = boost::asio;
+namespace ssl = boost::asio::ssl;
+using tcp = boost::asio::ip::tcp;
+using json = nlohmann::json;
+
+
+class VisSession : public std::enable_shared_from_this<VisSession>
+{
+ //net::io_context m_ioc;
+ tcp::resolver m_resolver;
+ tcp::resolver::results_type m_results;
+ std::string m_hostname;
+ websocket::stream<beast::ssl_stream<beast::tcp_stream>> m_ws;
+ beast::flat_buffer m_buffer;
+
+public:
+ // Resolver and socket require an io_context
+ explicit VisSession(const VisConfig &config, net::io_context& ioc, ssl::context& ctx);
+
+ // Start the asynchronous operation
+ void run();
+
+protected:
+ VisConfig m_config;
+ std::atomic_uint m_requestid;
+
+ void on_resolve(beast::error_code error, tcp::resolver::results_type results);
+
+ void connect();
+
+ void on_connect(beast::error_code error, tcp::resolver::results_type::endpoint_type endpoint);
+
+ void on_ssl_handshake(beast::error_code error);
+
+ void on_handshake(beast::error_code error);
+
+ void on_authorize(beast::error_code error, std::size_t bytes_transferred);
+
+ void on_write(beast::error_code error, std::size_t bytes_transferred);
+
+ void on_read(beast::error_code error, std::size_t bytes_transferred);
+
+ void get(const std::string &path);
+
+ void set(const std::string &path, const std::string &value);
+
+ void subscribe(const std::string &path);
+
+ void handle_message(const json &message);
+
+ bool parseData(const json &message, std::string &path, std::string &value, std::string &timestamp);
+
+ virtual void handle_authorized_response(void) = 0;
+
+ virtual void handle_get_response(std::string &path, std::string &value, std::string &timestamp) = 0;
+
+ virtual void handle_notification(std::string &path, std::string &value, std::string &timestamp) = 0;
+
+};
+
+#endif // _VIS_SESSION_HPP
diff --git a/systemd/agl-service-audiomixer.service b/systemd/agl-service-audiomixer.service
new file mode 100644
index 0000000..b6603fa
--- /dev/null
+++ b/systemd/agl-service-audiomixer.service
@@ -0,0 +1,11 @@
+[Unit]
+Requires=kuksa-val.service
+After=kuksa-val.service
+
+[Service]
+Type=simple
+ExecStart=/usr/sbin/agl-service-audiomixer
+Restart=on-failure
+
+[Install]
+WantedBy=default.target
diff --git a/systemd/meson.build b/systemd/meson.build
new file mode 100644
index 0000000..c94ab80
--- /dev/null
+++ b/systemd/meson.build
@@ -0,0 +1,3 @@
+systemd_system_unit_dir = systemd_dep.get_pkgconfig_variable('systemdsystemunitdir')
+
+install_data('agl-service-audiomixer.service', install_dir : systemd_system_unit_dir)
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
deleted file mode 100644
index 8d8fbdc..0000000
--- a/test/CMakeLists.txt
+++ /dev/null
@@ -1,28 +0,0 @@
-###########################################################################
-# Copyright 2020 Fujitsu
-#
-# author: Li Xiaoming <lixm.fnst@cn.fujitsu.com>
-#
-# 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.
-###########################################################################
-
-
-# Include any directory not starting with _
-# -----------------------------------------------------
-PROJECT_SUBDIRS_ADD(${PROJECT_SRC_DIR_PATTERN})
-
-ADD_TEST(NAME AGL_SERVICE_GPS_TESTS
- WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
- COMMAND afm-test ${CMAKE_BINARY_DIR}/package ${CMAKE_BINARY_DIR}/package-test
- )
-
diff --git a/test/afb-test/CMakeLists.txt b/test/afb-test/CMakeLists.txt
deleted file mode 100644
index 4adafb0..0000000
--- a/test/afb-test/CMakeLists.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-###########################################################################
-# Copyright 2020 Fujitsu
-#
-# author: Li Xiaoming <lixm.fnst@cn.fujitsu.com>
-#
-# 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.
-###########################################################################
-
-# Include any directory not starting with _
-# -----------------------------------------------------
-PROJECT_SUBDIRS_ADD(${PROJECT_SRC_DIR_PATTERN})
diff --git a/test/afb-test/etc/CMakeLists.txt b/test/afb-test/etc/CMakeLists.txt
deleted file mode 100644
index 12e2efc..0000000
--- a/test/afb-test/etc/CMakeLists.txt
+++ /dev/null
@@ -1,32 +0,0 @@
-###########################################################################
-# Copyright 2020 Fujitsu
-#
-# author: Li Xiaoming <lixm.fnst@cn.fujitsu.com>
-#
-# 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.
-###########################################################################
-
-##################################################
-# audiomixer test configuration files
-##################################################
-PROJECT_TARGET_ADD(afb-test-config)
-
- file(GLOB CONF_FILES "*.json")
-
- add_input_files("${CONF_FILES}")
-
- SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES
- LABELS "TEST-CONFIG"
- OUTPUT_NAME ${TARGET_NAME}
- )
-
diff --git a/test/afb-test/etc/aft-agl-audiomixer.json b/test/afb-test/etc/aft-agl-audiomixer.json
deleted file mode 100644
index c1e1c96..0000000
--- a/test/afb-test/etc/aft-agl-audiomixer.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "id": "http://iot.bzh/download/public/schema/json/ctl-schema.json#",
- "": "http://iot.bzh/download/public/schema/json/ctl-schema.json#",
- "metadata": {
- "uid": "Test",
- "version": "1.0",
- "api": "aft-audiomixer",
- "info": "AFB-test binding configuration file to test audiomixer api.",
- "require": [
- "audiomixer"
- ]
- },
- "testVerb": {
- "uid": "launch_all_tests",
- "info": "Launch all the tests",
- "action": "lua://AFT#_launch_test",
- "args": {
- "trace": "audiomixer",
- "files": ["audiomixer.lua"]
- }
- }
-}
diff --git a/test/afb-test/tests/CMakeLists.txt b/test/afb-test/tests/CMakeLists.txt
deleted file mode 100644
index 4153151..0000000
--- a/test/afb-test/tests/CMakeLists.txt
+++ /dev/null
@@ -1,31 +0,0 @@
-###########################################################################
-# Copyright 2020 Fujitsu
-#
-# author:Li Xiaoming <lixm.fnst@cn.fujitsu.com>
-#
-# 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.
-###########################################################################
-
-##################################################
-# audiomixer Lua Scripts
-##################################################
-PROJECT_TARGET_ADD(test-files)
-
- file(GLOB LUA_FILES "*.lua" "*.sh")
- add_input_files("${LUA_FILES}")
-
- SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES
- LABELS "TEST-DATA"
- OUTPUT_NAME ${TARGET_NAME}
- )
-
diff --git a/test/afb-test/tests/audiomixer.lua b/test/afb-test/tests/audiomixer.lua
deleted file mode 100644
index a978508..0000000
--- a/test/afb-test/tests/audiomixer.lua
+++ /dev/null
@@ -1,40 +0,0 @@
---[[
- Copyright 2020 Fujitsu
-
- author: Li Xiaoming <lixm.fnst@cn.fujitsu.com>
-
- 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.
---]]
-
-
-
-_AFT.testVerbStatusSuccess("testListControlSuccess","audiomixer","list_controls", {})
-
-_AFT.testVerbStatusSuccess("testMuteChange0Success","audiomixer","mute", {control="Master", value="0"})
-_AFT.testVerbStatusSuccess("testMuteChange1Success","audiomixer","mute", {control="Master", value="1"})
-_AFT.testVerbStatusSuccess("testMuteNotChangeSuccess","audiomixer","mute", {control="Master"})
-
-_AFT.testVerbStatusSuccess("testVolumeChangeSuccess","audiomixer","volume", {control="Master", value="0.5"})
-_AFT.testVerbStatusSuccess("testVolumeNotChangeSuccess","audiomixer","volume", {control="Master"})
-
-_AFT.testVerbStatusSuccess("testSubscribeControls_changedSuccess","audiomixer","subscribe", {event="controls_changed"})
-_AFT.testVerbStatusSuccess("testSubscribeVolume_changedSuccess","audiomixer","subscribe", {event="volume_changed"})
-_AFT.testVerbStatusSuccess("testSubscribeMute_changedSuccess","audiomixer","subscribe", {event="mute_changed"})
-
-_AFT.testVerbStatusSuccess("testUnSubscribeControls_changedSuccess","audiomixer","unsubscribe", {event="controls_changed"})
-_AFT.testVerbStatusSuccess("testUnSubscribeVolume_changedSuccess","audiomixer","unsubscribe", {event="volume_changed"})
-_AFT.testVerbStatusSuccess("testUnSubscribeMute_changedSuccess","audiomixer","unsubscribe", {event="mute_changed"})
-
-
-
-