diff options
Diffstat (limited to 'src/AudiomixerService.cpp')
-rw-r--r-- | src/AudiomixerService.cpp | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/src/AudiomixerService.cpp b/src/AudiomixerService.cpp new file mode 100644 index 0000000..f2445b8 --- /dev/null +++ b/src/AudiomixerService.cpp @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2022,2023 Konsulko Group + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include <string> +#include <sstream> +#include <iostream> +#include <algorithm> +#include <thread> +#include <chrono> + +#include "AudiomixerService.h" + +AudiomixerService::AudiomixerService(const KuksaConfig &config, GMainLoop *loop) : + m_loop(loop), + m_config(config) +{ + m_audiomixer = audiomixer_new(); + if (m_audiomixer) { + // Set up callbacks for WirePlumber events + m_audiomixer_events.controls_changed = mixer_control_change_cb; + m_audiomixer_events.value_changed = mixer_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; + } + + // Set initial volume + if (m_audiomixer) { + audiomixer_lock(m_audiomixer); + const struct mixer_control *ctl = audiomixer_find_control(m_audiomixer, "Master Playback"); + if (ctl) { + audiomixer_change_volume(m_audiomixer, ctl, 0.5); + } + audiomixer_unlock(m_audiomixer); + } + + // Create gRPC channel + std::string host = m_config.hostname(); + host += ":"; + std::stringstream ss; + ss << m_config.port(); + host += ss.str(); + + std::shared_ptr<grpc::Channel> channel; + if (!m_config.caCert().empty()) { + grpc::SslCredentialsOptions options; + options.pem_root_certs = m_config.caCert(); + if (!m_config.tlsServerName().empty()) { + grpc::ChannelArguments args; + auto target = m_config.tlsServerName(); + std::cout << "Overriding TLS target name with " << target << std::endl; + args.SetString(GRPC_SSL_TARGET_NAME_OVERRIDE_ARG, target); + channel = grpc::CreateCustomChannel(host, grpc::SslCredentials(options), args); + } else { + channel = grpc::CreateChannel(host, grpc::SslCredentials(options)); + } + } else { + channel = grpc::CreateChannel(host, grpc::InsecureChannelCredentials()); + } + + // Wait for the channel to be ready + std::cout << "Waiting for Databroker gRPC channel" << std::endl; + while (!channel->WaitForConnected(std::chrono::system_clock::now() + + std::chrono::milliseconds(500))) ; + std::cout << "Databroker gRPC channel ready" << std::endl; + + m_broker = new KuksaClient(channel, m_config); + if (m_broker) { + // Listen to actuator target updates for the volume signal and + // values for the steering wheel volume switch signals. + std::map<std::string, bool> signals; + signals["Vehicle.Cabin.Infotainment.Media.Volume"] = true; + signals["Vehicle.Cabin.SteeringWheel.Switches.VolumeUp"] = false; + signals["Vehicle.Cabin.SteeringWheel.Switches.VolumeDown"] = false; + signals["Vehicle.Cabin.SteeringWheel.Switches.VolumeMute"] = false; + m_broker->subscribe(signals, + [this](const std::string &path, const Datapoint &dp) { + HandleSignalChange(path, dp); + }, + [this](const SubscribeRequest *request, const Status &s) { + HandleSubscribeDone(request, s); + }); + + // Set initial volume in VSS + // Push out the default value of 50 which matches the default in the + // homescreen app. Ideally there would be some form of persistence + // scheme to restore the last value on restart. + m_broker->set("Vehicle.Cabin.Infotainment.Media.Volume", + 50U, + [this](const std::string &path, const Error &error) { + HandleSignalSetError(path, error); + }); + } else { + std::cerr << "Could not create Kuksa API client" << std::endl; + } +} + +AudiomixerService::~AudiomixerService() +{ + delete m_broker; + + audiomixer_free(m_audiomixer); +} + +// Private + +void AudiomixerService::HandleSignalChange(const std::string &path, const Datapoint &dp) +{ + if (m_config.verbose() > 1) + std::cout << "AudiomixerService::HandleSignalChange: Value received for " << path << std::endl; + + 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") { + if (dp.has_uint32()) { + uint32_t volume = dp.uint32(); + 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); + + // Push out new value + m_broker->set("Vehicle.Cabin.Infotainment.Media.Volume", + volume, + [this](const std::string &path, const Error &error) { + HandleSignalSetError(path, error); + }); + } + } + } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeUp") { + if (dp.has_bool_() && dp.bool_()) { + 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); + + // Push out new value + m_broker->set("Vehicle.Cabin.Infotainment.Media.Volume", + (unsigned) (volume * 100), + [this](const std::string &path, const Error &error) { + HandleSignalSetError(path, error); + }); + } + } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeDown") { + if (dp.has_bool_() && dp.bool_()) { + 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); + + // Push out new value + m_broker->set("Vehicle.Cabin.Infotainment.Media.Volume", + (unsigned) (volume * 100), + [this](const std::string &path, const Error &error) { + HandleSignalSetError(path, error); + }); + } + } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeMute") { + if (dp.has_bool_() && dp.bool_()) { + 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::HandleSignalSetError(const std::string &path, const Error &error) +{ + std::cerr << "Error setting " << path << ": " << error.code() << " - " << error.reason() << std::endl; +} + +void AudiomixerService::HandleSubscribeDone(const SubscribeRequest *request, const Status &status) +{ + if (m_config.verbose()) + std::cout << "Subscribe status = " << status.error_code() << + " (" << status.error_message() << ")" << std::endl; + + if (status.error_code() == grpc::CANCELLED) { + if (m_config.verbose()) + std::cerr << "Subscribe canceled, assuming shutdown" << std::endl; + return; + } + + // Queue up a resubcribe via the GLib event loop callback + struct resubscribe_data *data = new (struct resubscribe_data); + if (!data) { + std::cerr << "Could not create resubcribe_data" << std::endl; + exit(1); + } + data->self = this; + // Need to copy request since the one we have been handed is from the + // finished subscribe and will be going away. + data->request = new SubscribeRequest(*request); + if (!data->request) { + std::cerr << "Could not create resubscribe SubscribeRequest" << std::endl; + exit(1); + } + + // NOTE: Waiting 100 milliseconds for now; it is possible that some + // randomization and/or back-off may need to be added if many + // subscribes are active, or switching to some other resubscribe + // scheme altogether (e.g. post subscribes to a thread that waits + // for the channel to become connected again). + g_timeout_add_full(G_PRIORITY_DEFAULT, + 100, + resubscribe_cb, + data, + NULL); +} + +void AudiomixerService::Resubscribe(const SubscribeRequest *request) +{ + if (!(m_broker && request)) + return; + + m_broker->subscribe(request, + [this](const std::string &path, const Datapoint &dp) { + HandleSignalChange(path, dp); + }, + [this](const SubscribeRequest *request, const Status &s) { + HandleSubscribeDone(request, s); + }); +} + +void AudiomixerService::HandleMixerControlChange(void) +{ + // Ignore for now +} + +void AudiomixerService::HandleMixerValueChange(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 out new value + unsigned value = control->volume * 100; + m_broker->set("Vehicle.Cabin.Infotainment.Media.Volume", + value, + [this](const std::string &path, const Error &error) { + HandleSignalSetError(path, error); + }); + } + } else if (change_mask & MIXER_CONTROL_CHANGE_FLAG_MUTE) { + // For now, do nothing, new state is in control->mute + } +} |