/* * Copyright (C) 2022,2023 Konsulko Group * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #include #include #include #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 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 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 } }