From 74ccc5d14446b4a5bfe8602da9c06a02e97266df Mon Sep 17 00:00:00 2001 From: Sergii Ushakov Date: Tue, 3 Mar 2026 18:20:14 +0000 Subject: [PATCH] webrtc: Add Sound volume and mute controls support This change implements volume and mute controls for the WebRTC audio backend in Cuttlefish. These controls allow the guest to dynamically adjust playback and capture audio levels via Virtio Sound control messages. - AudioMixer: Apply playback volume scaling during audio stream resampling by modifying the channel mix map. - Implement read/write logic for Virtio audio volume/mute control commands. - Guest Config: Allow enabling of Virtio Sound controls per stream. --- .../host/commands/assemble_cvd/flags.cc | 4 + .../assemble_cvd/proto/guest_config.proto | 14 +- .../host/frontend/webrtc/audio_handler.cpp | 238 ++++++++++++++++-- .../host/frontend/webrtc/audio_handler.h | 31 +++ .../host/frontend/webrtc/audio_mixer.cpp | 27 +- .../host/frontend/webrtc/audio_mixer.h | 23 +- .../host/frontend/webrtc/audio_settings.h | 10 +- .../cuttlefish/host/frontend/webrtc/main.cpp | 58 +++-- .../host/libs/audio_connector/commands.cpp | 24 ++ .../host/libs/audio_connector/commands.h | 24 ++ .../host/libs/audio_connector/server.cpp | 56 ++++- .../host/libs/audio_connector/server.h | 5 +- .../host/libs/audio_connector/shm_layout.h | 98 +++++++- 13 files changed, 544 insertions(+), 68 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/assemble_cvd/flags.cc b/base/cvd/cuttlefish/host/commands/assemble_cvd/flags.cc index 9f916d74877..5103714f973 100644 --- a/base/cvd/cuttlefish/host/commands/assemble_cvd/flags.cc +++ b/base/cvd/cuttlefish/host/commands/assemble_cvd/flags.cc @@ -841,6 +841,10 @@ Result InitializeCuttlefishConfiguration( instance.set_audio_output_streams_count( guest_configs[instance_index].output_audio_streams_count); + const auto& audio_settings = guest_configs[instance_index].audio_settings; + if (audio_settings.has_value()) { + instance.set_audio_settings(audio_settings.value()); + } // jcardsim instance.set_enable_jcard_simulator( diff --git a/base/cvd/cuttlefish/host/commands/assemble_cvd/proto/guest_config.proto b/base/cvd/cuttlefish/host/commands/assemble_cvd/proto/guest_config.proto index f826bbfd460..fb588bf603d 100644 --- a/base/cvd/cuttlefish/host/commands/assemble_cvd/proto/guest_config.proto +++ b/base/cvd/cuttlefish/host/commands/assemble_cvd/proto/guest_config.proto @@ -60,9 +60,21 @@ message Audio { } message PCMDevice { + + message Controls { + message Volume { + uint32 min = 1; + uint32 max = 2; + uint32 step = 3; + } + bool mute_control_enabled = 1; + Volume volume_control = 2; + } + message Stream { uint32 id = 1; ChannelLayout channel_layout = 2; + Controls controls = 3; } repeated Stream capture_streams = 1; repeated Stream playback_streams = 2; @@ -120,7 +132,7 @@ enum DeviceType { } // GuestConfigFile represents the configuration for a Cuttlefish guest device, -// typically parsed from an cuttlefish-guest-config.txtpb file. It defines the +// typically parsed from a cuttlefish-guest-config.txtpb file. It defines the // device's hardware capabilities, software features, and virtualization // settings. message GuestConfigFile { diff --git a/base/cvd/cuttlefish/host/frontend/webrtc/audio_handler.cpp b/base/cvd/cuttlefish/host/frontend/webrtc/audio_handler.cpp index 85dc5ab8387..408bd473bc3 100644 --- a/base/cvd/cuttlefish/host/frontend/webrtc/audio_handler.cpp +++ b/base/cvd/cuttlefish/host/frontend/webrtc/audio_handler.cpp @@ -17,13 +17,12 @@ #include "cuttlefish/host/frontend/webrtc/audio_handler.h" #include -#include -#include +#include #include +#include +#include #include -#include "absl/log/check.h" -#include "absl/log/log.h" #include "cuttlefish/host/frontend/webrtc/audio_mixer.h" @@ -50,8 +49,7 @@ virtio_snd_chmap_info GetVirtioSndChmapInfo( const AudioStreamSettings& settings) { const static std::unordered_map> kChannelPositions = { - {AudioChannelsLayout::Mono, - {AudioChannelMap::VIRTIO_SND_CHMAP_MONO}}, + {AudioChannelsLayout::Mono, {AudioChannelMap::VIRTIO_SND_CHMAP_MONO}}, {AudioChannelsLayout::Stereo, {AudioChannelMap::VIRTIO_SND_CHMAP_FL, AudioChannelMap::VIRTIO_SND_CHMAP_FR}}, @@ -77,6 +75,59 @@ virtio_snd_chmap_info GetVirtioSndChmapInfo( return info; } +inline constexpr const char* GetDirectionString( + AudioStreamSettings::Direction direction) { + switch (direction) { + case AudioStreamSettings::Direction::Capture: + return "Capture"; + case AudioStreamSettings::Direction::Playback: + return "Playback"; + } +} + +virtio_snd_ctl_info GetVirtioCtlInfoVolume( + const AudioStreamSettings::VolumeControl& settings, + AudioStreamSettings::Direction stream_direction, uint32_t card_id, + uint32_t device_id, uint32_t ctl_id) { + virtio_snd_ctl_info info = { + .hdr = {.hda_fn_nid = Le32(ctl_id)}, + .role = Le32(AudioControlRole::VIRTIO_SND_CTL_ROLE_VOLUME), + .type = Le32(AudioControlType::VIRTIO_SND_CTL_TYPE_INTEGER), + .access = Le32((1 << AudioControlAccess::VIRTIO_SND_CTL_ACCESS_READ) | + (1 << AudioControlAccess::VIRTIO_SND_CTL_ACCESS_WRITE)), + .count = Le32(1), + .index = Le32(0), + .name = {}, + .value = {.integer = { + .min = Le32(settings.min), + .max = Le32(settings.max), + .step = Le32(settings.step), + }}}; + std::format_to_n(info.name, sizeof(info.name) - 1, + "Master {} Volume (C{}D{})", + GetDirectionString(stream_direction), card_id, device_id); + return info; +} + +virtio_snd_ctl_info GetVirtioCtlInfoMute( + AudioStreamSettings::Direction stream_direction, uint32_t card_id, + uint32_t device_id, uint32_t ctl_id) { + virtio_snd_ctl_info info = { + .hdr = {.hda_fn_nid = Le32(ctl_id)}, + .role = Le32(AudioControlRole::VIRTIO_SND_CTL_ROLE_MUTE), + .type = Le32(AudioControlType::VIRTIO_SND_CTL_TYPE_BOOLEAN), + .access = Le32((1 << AudioControlAccess::VIRTIO_SND_CTL_ACCESS_READ) | + (1 << AudioControlAccess::VIRTIO_SND_CTL_ACCESS_WRITE)), + .count = Le32(1), + .index = Le32(0), + .name = {}, + .value = {} // Ignored when VIRTIO_SND_CTL_TYPE_BOOLEAN + }; + std::format_to_n(info.name, sizeof(info.name) - 1, "Master {} Mute (C{}D{})", + GetDirectionString(stream_direction), card_id, device_id); + return info; +} + virtio_snd_pcm_info GetVirtioSndPcmInfo(const AudioStreamSettings& settings) { return { .hdr = @@ -278,6 +329,31 @@ AudioHandler::AudioHandler( : 0); streams_[stream_id] = GetVirtioSndPcmInfo(settings); chmaps_[stream_id] = GetVirtioSndChmapInfo(settings); + + constexpr uint32_t kCardId = 0; // As of now only one card is supported + if (settings.has_mute_control) { + controls_.push_back(GetVirtioCtlInfoMute(settings.direction, kCardId, + settings.id, controls_.size())); + controls_to_streams_map_.push_back( + ControlDesc{.type = ControlDesc::Type::Mute, .stream_id = stream_id}); + } + if (settings.master_volume_control.has_value()) { + const auto& control = settings.master_volume_control.value(); + CHECK(control.max > control.min) + << "Volume control 'min' value must be lower than 'max'."; + CHECK((control.max - control.min) % control.step == 0) + << "Volume control 'step' value must divide the total volume range " + "evenly."; + controls_.push_back(GetVirtioCtlInfoVolume( + control, settings.direction, kCardId, settings.id, controls_.size())); + controls_to_streams_map_.push_back(ControlDesc{ + .type = ControlDesc::Type::Volume, .stream_id = stream_id}); + stream_descs_[stream_id].volume = { + .min = control.min, + .max = control.max, + .current = control.max, + }; + } } } @@ -291,8 +367,8 @@ void AudioHandler::Start() { [[noreturn]] void AudioHandler::Loop() { for (;;) { auto audio_client = audio_server_->AcceptClient( - streams_.size(), NUM_JACKS, chmaps_.size(), 262144 /* tx_shm_len */, - 262144 /* rx_shm_len */); + streams_.size(), NUM_JACKS, chmaps_.size(), controls_.size(), + 262144 /* tx_shm_len */, 262144 /* rx_shm_len */); CHECK(audio_client) << "Failed to create audio client connection instance"; std::thread playback_thread([this, &audio_client]() { @@ -404,6 +480,94 @@ void AudioHandler::JacksInfo(JackInfoCommand& cmd) { cmd.Reply(AudioStatus::VIRTIO_SND_S_OK, jack_info); } +void AudioHandler::ControlsInfo(ControlInfoCommand& cmd) { + LOG(DEBUG) << "AudioHandler::ControlsInfo: start_id=" << cmd.start_id() + << ", count=" << cmd.count(); + + if (cmd.start_id() + cmd.count() > controls_.size()) { + cmd.Reply(AudioStatus::VIRTIO_SND_S_BAD_MSG, {}); + return; + } + + cmd.Reply(AudioStatus::VIRTIO_SND_S_OK, + {controls_.cbegin() + cmd.start_id(), cmd.count()}); +} + +void AudioHandler::OnControlCommand(ControlCommand& cmd) { + const auto id = cmd.control_id(); + if (id >= controls_.size()) { + cmd.Reply(AudioStatus::VIRTIO_SND_S_BAD_MSG); + return; + } + + LOG(DEBUG) << "AudioHandler::OnControlCommand: control_id=" << id; + + const auto stream_id = controls_to_streams_map_[id].stream_id; + auto& stream = stream_descs_[stream_id]; + + const auto control_mute = [stream_id, &stream, &cmd]() -> AudioStatus { + std::lock_guard lock(stream.mtx); + + if (cmd.type() == AudioCommandType::VIRTIO_SND_R_CTL_READ) { + auto& val = cmd.value()->value.integer; + val.value[0] = Le32(stream.muted ? 1 : 0); + return AudioStatus::VIRTIO_SND_S_OK; + } + + if (cmd.type() == AudioCommandType::VIRTIO_SND_R_CTL_WRITE) { + const auto val = cmd.value()->value.integer.value[0].as_uint32_t(); + // For VIRTIO_SND_CTL_TYPE_BOOLEAN, the guest driver (virtio_snd) + // inherently knows that the control acts as a toggle. It implicitly + // enforces a minimum of 0 and a maximum of 1. + if (val > 1) { + LOG(ERROR) << "Wrongs boolean value for control " << cmd.control_id() + << " provided: " << val; + return AudioStatus::VIRTIO_SND_S_BAD_MSG; + } + LOG(DEBUG) << "Setting mute for stream " << stream_id << " to " << val; + stream.muted = val == 1; + return AudioStatus::VIRTIO_SND_S_OK; + } + + return AudioStatus::VIRTIO_SND_S_NOT_SUPP; + }; + + const auto control_volume = [stream_id, &stream, &cmd]() -> AudioStatus { + std::lock_guard lock(stream.mtx); + + if (cmd.type() == AudioCommandType::VIRTIO_SND_R_CTL_READ) { + auto& val = cmd.value()->value.integer; + val.value[0] = Le32(stream.volume.current); + return AudioStatus::VIRTIO_SND_S_OK; + } + + if (cmd.type() == AudioCommandType::VIRTIO_SND_R_CTL_WRITE) { + const auto val = cmd.value()->value.integer.value[0].as_uint32_t(); + if (val < stream.volume.min || val > stream.volume.max) { + LOG(ERROR) << "Wrongs volume value for control " << cmd.control_id() + << " provided: " << val; + return AudioStatus::VIRTIO_SND_S_BAD_MSG; + } + LOG(DEBUG) << "Setting volume for stream " << stream_id << " to " << val; + stream.volume.current = val; + return AudioStatus::VIRTIO_SND_S_OK; + } + + return AudioStatus::VIRTIO_SND_S_NOT_SUPP; + }; + + auto result = AudioStatus::VIRTIO_SND_S_NOT_SUPP; + switch (controls_to_streams_map_[id].type) { + case ControlDesc::Type::Mute: + result = control_mute(); + break; + case ControlDesc::Type::Volume: + result = control_volume(); + break; + } + cmd.Reply(result); +} + void AudioHandler::OnPlaybackBuffer(TxBuffer buffer) { const auto stream_id = buffer.stream_id(); // Invalid or capture streams shouldn't send tx buffers @@ -416,14 +580,17 @@ void AudioHandler::OnPlaybackBuffer(TxBuffer buffer) { uint32_t sample_rate = 0; uint8_t channels = 0; uint8_t bits_per_channel = 0; + float volume = 0; { auto& stream_desc = stream_descs_[stream_id]; std::lock_guard lock(stream_desc.mtx); + volume = stream_desc.volume.GetCurrentVolumeLevel(); + // A buffer may be received for an inactive stream if we were slow to // process it and the other side stopped the stream. Quietly ignore it in // that case - if (!stream_desc.active) { + if (!stream_desc.active || stream_desc.muted || volume == 0) { buffer.SendStatus(AudioStatus::VIRTIO_SND_S_OK, 0, buffer.len()); return; } @@ -433,11 +600,17 @@ void AudioHandler::OnPlaybackBuffer(TxBuffer buffer) { bits_per_channel = stream_desc.bits_per_sample; } audio_mixer_->OnPlayback(stream_id, sample_rate, channels, bits_per_channel, - buffer.get(), buffer.len()); + volume, buffer.get(), buffer.len()); buffer.SendStatus(AudioStatus::VIRTIO_SND_S_OK, 0, buffer.len()); } void AudioHandler::OnCaptureBuffer(RxBuffer buffer) { + const auto rx_buffer = buffer.get(); + size_t bytes_read = 0; + float volume = 0; + uint32_t bytes_per_sample = 0; + bool is_muted_by_control = false; + auto stream_id = buffer.stream_id(); auto& stream_desc = stream_descs_[stream_id]; { @@ -448,6 +621,7 @@ void AudioHandler::OnCaptureBuffer(RxBuffer buffer) { buffer.SendStatus(AudioStatus::VIRTIO_SND_S_BAD_MSG, 0, 0); return; } + // A buffer may be received for an inactive stream if we were slow to // process it and the other side stopped the stream. Quietly ignore it in // that case @@ -455,24 +629,24 @@ void AudioHandler::OnCaptureBuffer(RxBuffer buffer) { buffer.SendStatus(AudioStatus::VIRTIO_SND_S_OK, 0, buffer.len()); return; } - const auto bytes_per_sample = stream_desc.bits_per_sample / 8; + volume = stream_desc.volume.GetCurrentVolumeLevel(); + is_muted_by_control = stream_desc.muted; + bytes_per_sample = stream_desc.bits_per_sample / 8; const auto samples_per_channel = stream_desc.sample_rate / 100; const auto bytes_per_request = samples_per_channel * bytes_per_sample * stream_desc.channels; + + // Fill remaining data from previous iteration auto& holding_buffer = stream_descs_[stream_id].holding_buffer; - size_t bytes_read = 0; - const auto rx_buffer = buffer.get(); - if (!holding_buffer.empty()) { - // Fill remaining data from previous iteration - bytes_read = holding_buffer.size(); - std::copy(holding_buffer.cbegin(), holding_buffer.cend(), rx_buffer); - holding_buffer.clear(); - } + bytes_read = holding_buffer.size(); + std::copy(holding_buffer.cbegin(), holding_buffer.cend(), rx_buffer); + holding_buffer.clear(); + bool muted = false; while (buffer.len() - bytes_read >= bytes_per_request) { // Skip the holding buffer in as many reads as possible to avoid the extra // copies - auto write_pos = rx_buffer + bytes_read; + const auto write_pos = rx_buffer + bytes_read; auto res = audio_source_->GetMoreAudioData( write_pos, bytes_per_sample, samples_per_channel, stream_desc.channels, stream_desc.sample_rate, muted); @@ -521,6 +695,30 @@ void AudioHandler::OnCaptureBuffer(RxBuffer buffer) { } } } + + if (is_muted_by_control) { + memset(rx_buffer, 0, bytes_read); + } else if (volume < 1.){ + static const auto apply_volume = [](auto* data, size_t size, float volume) { + for (auto& val : std::span{data, size}) { + val *= volume; + } + }; + switch (bytes_per_sample) { + case 1: + apply_volume(rx_buffer, bytes_read, volume); + break; + case 2: + apply_volume(reinterpret_cast(rx_buffer), bytes_read / 2, + volume); + break; + case 4: + apply_volume(reinterpret_cast(rx_buffer), bytes_read / 4, + volume); + break; + } + } + buffer.SendStatus(AudioStatus::VIRTIO_SND_S_OK, 0, buffer.len()); } diff --git a/base/cvd/cuttlefish/host/frontend/webrtc/audio_handler.h b/base/cvd/cuttlefish/host/frontend/webrtc/audio_handler.h index 929d7aaa201..156a2b28557 100644 --- a/base/cvd/cuttlefish/host/frontend/webrtc/audio_handler.h +++ b/base/cvd/cuttlefish/host/frontend/webrtc/audio_handler.h @@ -16,8 +16,11 @@ #pragma once +#include +#include #include #include +#include #include #include @@ -37,6 +40,30 @@ class AudioHandler : public AudioServerExecutor { uint8_t bits_per_sample = 0; uint8_t channels = 0; bool active = false; + + // Controls + struct Volume { + uint32_t min = 0; + uint32_t max = 1; + uint32_t current = max; + + float GetCurrentVolumeLevel() { + return static_cast(current - min) / + static_cast(max - min); + }; + }; + Volume volume; + bool muted = false; + }; + + struct ControlDesc { + enum class Type { + Mute, + Volume, + }; + + Type type = Type::Mute; + size_t stream_id = 0; }; public: @@ -58,6 +85,8 @@ class AudioHandler : public AudioServerExecutor { void StopStream(StreamControlCommand& cmd) override; void ChmapsInfo(ChmapInfoCommand& cmd) override; void JacksInfo(JackInfoCommand& cmd) override; + void ControlsInfo(ControlInfoCommand& cmd) override; + void OnControlCommand(ControlCommand& cmd) override; void OnPlaybackBuffer(TxBuffer buffer) override; void OnCaptureBuffer(RxBuffer buffer) override; @@ -72,6 +101,8 @@ class AudioHandler : public AudioServerExecutor { std::vector streams_; std::vector stream_descs_ = {}; std::vector chmaps_; + std::vector controls_; + std::vector controls_to_streams_map_; std::unique_ptr audio_mixer_; }; } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/frontend/webrtc/audio_mixer.cpp b/base/cvd/cuttlefish/host/frontend/webrtc/audio_mixer.cpp index 21a30cb56f7..a37bb4a4123 100644 --- a/base/cvd/cuttlefish/host/frontend/webrtc/audio_mixer.cpp +++ b/base/cvd/cuttlefish/host/frontend/webrtc/audio_mixer.cpp @@ -3,8 +3,9 @@ #include #include #include +#include "audio_settings.h" -#include "absl/log/check.h" +#include namespace cuttlefish { namespace { @@ -57,7 +58,8 @@ size_t ConvertAudioStream(void* dst, uint8_t dst_channels, uint32_t dst_rate, const void* src, uint8_t src_channels, uint32_t src_rate, size_t src_frames_count, const std::vector>& channel_map) { - constexpr uint8_t kMaxChannelsCount = 6; + constexpr uint8_t kMaxChannelsCount = + GetChannelsCount(AudioChannelsLayout::Surround51); CHECK(src_channels <= kMaxChannelsCount); CHECK(channel_map.size() >= dst_channels); @@ -117,7 +119,7 @@ using ConvertAudioStreamFn = size_t(void*, uint8_t, uint32_t, const void*, * way to look up the correct conversion function based on the source and * destination audio formats. * 0-byte and 3-bytes samples are not supported. - * */ + */ constexpr std::array, 5> kConvertAudioStreamFunctionMap = { {{nullptr, nullptr, nullptr, nullptr, nullptr}, @@ -161,23 +163,20 @@ void AudioMixer::OnStreamStopped(uint32_t stream_id) { void AudioMixer::OnPlayback(uint32_t stream_id, uint32_t stream_sample_rate, uint8_t stream_channels_count, - uint8_t stream_bits_per_channel, + uint8_t stream_bits_per_channel, float volume, const uint8_t* buffer, size_t size) { - static const std::vector> kDirectMap{{ - {1, 0, 0, 0, 0, 0}, - {0, 1, 0, 0, 0, 0}, - {0, 0, 1, 0, 0, 0}, - {0, 0, 0, 1, 0, 0}, - {0, 0, 0, 0, 1, 0}, - {0, 0, 0, 0, 0, 1}, - }}; - const auto stream_frames_count = GetFramesCount(size, stream_channels_count, stream_bits_per_channel); const auto frames_count = GetFrameCountAfterResampling( sample_rate_, stream_sample_rate, stream_frames_count); std::unique_lock lock(mutex_); + + // As of now we only use direct channel mapping + for(size_t i = 0; i < channles_map.size(); ++i) { + channles_map[i][i] = volume; + } + const bool need_notify = next_frame_.empty(); // no active streams const auto it = next_frame_.find(stream_id); @@ -205,7 +204,7 @@ void AudioMixer::OnPlayback(uint32_t stream_id, uint32_t stream_sample_rate, const auto filled_frames_count = convert_fn(mixed_buffer_.data() + next_frame_id * frame_size_bytes_, channels_count_, sample_rate_, buffer, stream_channels_count, - stream_sample_rate, stream_frames_count, kDirectMap); + stream_sample_rate, stream_frames_count, channles_map); CHECK(filled_frames_count <= frames_count); next_frame_[stream_id] = next_frame_id + filled_frames_count; diff --git a/base/cvd/cuttlefish/host/frontend/webrtc/audio_mixer.h b/base/cvd/cuttlefish/host/frontend/webrtc/audio_mixer.h index 5f5220fa101..4ecd8e3d2a2 100644 --- a/base/cvd/cuttlefish/host/frontend/webrtc/audio_mixer.h +++ b/base/cvd/cuttlefish/host/frontend/webrtc/audio_mixer.h @@ -5,8 +5,8 @@ #include #include #include -#include #include +#include #include "cuttlefish/host/frontend/webrtc/libdevice/audio_sink.h" #include "cuttlefish/host/frontend/webrtc/audio_settings.h" @@ -25,9 +25,9 @@ class AudioMixer { // Called by auido_handler whenever new playback data chunk is given // Can be called on different threads void OnPlayback(uint32_t stream_id, uint32_t stream_sample_rate, - uint8_t stream_channels_count, - uint8_t stream_bits_per_channel, const uint8_t* buffer, - size_t size); + uint8_t stream_channels_count, + uint8_t stream_bits_per_channel, float volume, const uint8_t* buffer, + size_t size); void OnStreamStopped(uint32_t stream_id); private: @@ -48,15 +48,26 @@ class AudioMixer { ///////////////// Guarded by mutex_ //////////////// //////////////////////////////////////////////////// - // Buffer stores mixed auido data for every active stream. Consumed by MixerLoop + // Buffer stores mixed auido data for every active stream. Consumed by + // MixerLoop std::vector mixed_buffer_; // Index of the last frame with available (i.e. not yet played) audio data size_t last_active_frame_ = 0; - // Frame index per stream to put next avaiable data to + // Frame index per stream to put next available data to std::unordered_map next_frame_; + // Used to remap channels and apply volume levels + std::vector> channles_map = {{ + {1, 0, 0, 0, 0, 0}, + {0, 1, 0, 0, 0, 0}, + {0, 0, 1, 0, 0, 0}, + {0, 0, 0, 1, 0, 0}, + {0, 0, 0, 0, 1, 0}, + {0, 0, 0, 0, 0, 1}, + }}; + //////////////////////////////////////////////////// //////////////////////////////////////////////////// diff --git a/base/cvd/cuttlefish/host/frontend/webrtc/audio_settings.h b/base/cvd/cuttlefish/host/frontend/webrtc/audio_settings.h index 5ee07b7fe6f..e4406160857 100644 --- a/base/cvd/cuttlefish/host/frontend/webrtc/audio_settings.h +++ b/base/cvd/cuttlefish/host/frontend/webrtc/audio_settings.h @@ -32,11 +32,19 @@ struct AudioStreamSettings { Capture, }; + struct VolumeControl { + uint32_t min = 0; + uint32_t max = 100; + uint32_t step = 1; + }; + uint8_t id = 0; AudioChannelsLayout channels_layout = AudioChannelsLayout::Stereo; Direction direction = Direction::Playback; -}; + bool has_mute_control = false; + std::optional master_volume_control; +}; struct AudioMixerSettings { AudioChannelsLayout channels_layout = AudioChannelsLayout::Stereo; diff --git a/base/cvd/cuttlefish/host/frontend/webrtc/main.cpp b/base/cvd/cuttlefish/host/frontend/webrtc/main.cpp index e34eaf54a39..dd5e05af25e 100644 --- a/base/cvd/cuttlefish/host/frontend/webrtc/main.cpp +++ b/base/cvd/cuttlefish/host/frontend/webrtc/main.cpp @@ -205,6 +205,31 @@ std::shared_ptr SetupAudio( {SampleRate::Audio_SampleRate_RATE_64000, 64000}, }; + static const auto parse_stream_settings = + [](const ::cuttlefish::config::Audio_PCMDevice_Stream& stream, + AudioStreamSettings::Direction direction) + -> cuttlefish::AudioStreamSettings { + const auto id = stream.id(); + CHECK(id <= std::numeric_limits::max()); + cuttlefish::AudioStreamSettings settings = { + .id = static_cast(id), + .channels_layout = kChannelLayoutMap.at(stream.channel_layout()), + .direction = direction}; + if (stream.has_controls()) { + const auto& controls = stream.controls(); + settings.has_mute_control = controls.mute_control_enabled(); + if (controls.has_volume_control()) { + const auto& volume = controls.volume_control(); + settings.master_volume_control = {{ + .min = volume.min(), + .max = volume.max(), + .step = volume.step(), + }}; + } + } + return settings; + }; + if (!instance.enable_audio()) { return nullptr; } @@ -214,13 +239,22 @@ std::shared_ptr SetupAudio( const auto audio_settings = instance.audio_settings(); if (!audio_settings.has_value()) { const auto output_streams_count = instance.audio_output_streams_count(); - streams.push_back({.id = 0, - .channels_layout = AudioChannelsLayout::Stereo, - .direction = AudioStreamSettings::Direction::Capture}); + streams.push_back({ + .id = 0, + .channels_layout = AudioChannelsLayout::Stereo, + .direction = AudioStreamSettings::Direction::Capture, + .has_mute_control = true, + }); for (auto i = 0; i < output_streams_count; ++i) { streams.push_back({.id = static_cast(i), .channels_layout = AudioChannelsLayout::Stereo, - .direction = AudioStreamSettings::Direction::Playback}); + .direction = AudioStreamSettings::Direction::Playback, + .has_mute_control = true, + .master_volume_control = {{ + .min = 0, + .max = 10, + .step = 1, + }}}); } } else { CHECK(!audio_settings->pcm_devices().empty()); @@ -229,20 +263,12 @@ std::shared_ptr SetupAudio( } const auto& pcm = audio_settings->pcm_devices()[0]; for (const auto& stream : pcm.playback_streams()) { - const auto id = stream.id(); - CHECK(id <= std::numeric_limits::max()); - streams.push_back( - {.id = static_cast(id), - .channels_layout = kChannelLayoutMap.at(stream.channel_layout()), - .direction = AudioStreamSettings::Direction::Playback}); + streams.push_back(parse_stream_settings( + stream, AudioStreamSettings::Direction::Playback)); } for (const auto& stream : pcm.capture_streams()) { - const auto id = stream.id(); - CHECK(id <= std::numeric_limits::max()); - streams.push_back( - {.id = static_cast(id), - .channels_layout = kChannelLayoutMap.at(stream.channel_layout()), - .direction = AudioStreamSettings::Direction::Capture}); + streams.push_back(parse_stream_settings( + stream, AudioStreamSettings::Direction::Capture)); } if (pcm.has_mixer()) { const auto& mixer = pcm.mixer(); diff --git a/base/cvd/cuttlefish/host/libs/audio_connector/commands.cpp b/base/cvd/cuttlefish/host/libs/audio_connector/commands.cpp index cc8339c562f..b253b129749 100644 --- a/base/cvd/cuttlefish/host/libs/audio_connector/commands.cpp +++ b/base/cvd/cuttlefish/host/libs/audio_connector/commands.cpp @@ -75,6 +75,24 @@ void ChmapInfoCommand::Reply(AudioStatus status, } } +ControlInfoCommand::ControlInfoCommand(uint32_t start_id, size_t count, + virtio_snd_ctl_info* ctl_info) + : InfoCommand(AudioCommandType::VIRTIO_SND_R_CTL_INFO, start_id, count, + ctl_info) {} + +void ControlInfoCommand::Reply( + AudioStatus status, std::span reply) { + MarkReplied(status); + if (status != AudioStatus::VIRTIO_SND_S_OK) { + return; + } + CHECK(reply.size() == count()) + << "Returned unmatching info count: " << reply.size() << " vs " + << count(); + + std::copy(reply.begin(), reply.end(), info_reply()); +} + StreamInfoCommand::StreamInfoCommand(uint32_t start_id, size_t count, virtio_snd_pcm_info* pcm_info) : InfoCommand(AudioCommandType::VIRTIO_SND_R_PCM_INFO, start_id, count, @@ -124,4 +142,10 @@ StreamSetParamsCommand::StreamSetParamsCommand( format_(format), rate_(rate) {} +ControlCommand::ControlCommand(AudioCommandType type, uint32_t control_id, + virtio_snd_ctl_value* value) + : AudioCommand(type), control_id_(control_id), value_(value) {} + +void ControlCommand::Reply(AudioStatus status) { MarkReplied(status); } + } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/libs/audio_connector/commands.h b/base/cvd/cuttlefish/host/libs/audio_connector/commands.h index 39c825e0cc8..f3b9dcdc030 100644 --- a/base/cvd/cuttlefish/host/libs/audio_connector/commands.h +++ b/base/cvd/cuttlefish/host/libs/audio_connector/commands.h @@ -16,6 +16,7 @@ #include +#include #include #include "cuttlefish/host/libs/audio_connector/shm_layout.h" @@ -78,6 +79,14 @@ class JackInfoCommand : public InfoCommand { const std::vector& reply); }; +class ControlInfoCommand : public InfoCommand { + public: + ControlInfoCommand(uint32_t start_id, size_t count, + virtio_snd_ctl_info* ctl_info); + + void Reply(AudioStatus status, std::span reply); +}; + class StreamInfoCommand : public InfoCommand { public: StreamInfoCommand(uint32_t start_id, size_t count, @@ -122,4 +131,19 @@ struct StreamSetParamsCommand : public StreamControlCommand { const uint8_t rate_; }; +struct ControlCommand : public AudioCommand { + public: + ControlCommand(AudioCommandType type, uint32_t control_id, + virtio_snd_ctl_value* value); + + uint32_t control_id() const { return control_id_; } + virtio_snd_ctl_value* value() { return value_; } + + void Reply(AudioStatus status); + + private: + const uint32_t control_id_; + virtio_snd_ctl_value* value_; +}; + } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/libs/audio_connector/server.cpp b/base/cvd/cuttlefish/host/libs/audio_connector/server.cpp index 0ead049d20f..796a2dce404 100644 --- a/base/cvd/cuttlefish/host/libs/audio_connector/server.cpp +++ b/base/cvd/cuttlefish/host/libs/audio_connector/server.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -99,7 +100,7 @@ std::function SendStatusCallback( } // namespace std::unique_ptr AudioServer::AcceptClient( - uint32_t num_streams, uint32_t num_jacks, uint32_t num_chmaps, + uint32_t num_streams, uint32_t num_jacks, uint32_t num_chmaps, uint32_t num_controls, size_t tx_shm_len, size_t rx_shm_len) { auto conn_fd = SharedFD::Accept(*server_socket_, nullptr, 0); if (!conn_fd->IsOpen()) { @@ -107,13 +108,13 @@ std::unique_ptr AudioServer::AcceptClient( return nullptr; } return AudioClientConnection::Create(conn_fd, num_streams, num_jacks, - num_chmaps, tx_shm_len, rx_shm_len); + num_chmaps, num_controls, tx_shm_len, rx_shm_len); } /* static */ std::unique_ptr AudioClientConnection::Create( SharedFD client_socket, uint32_t num_streams, uint32_t num_jacks, - uint32_t num_chmaps, size_t tx_shm_len, size_t rx_shm_len) { + uint32_t num_chmaps, uint32_t num_controls, size_t tx_shm_len, size_t rx_shm_len) { SharedFD event_socket, event_pair; SharedFD tx_socket, tx_pair; SharedFD rx_socket, rx_pair; @@ -143,6 +144,7 @@ std::unique_ptr AudioClientConnection::Create( .jacks = num_jacks, .streams = num_streams, .chmaps = num_chmaps, + .controls = num_controls, }; auto sent = client_socket->SendFileDescriptors( @@ -160,10 +162,9 @@ std::unique_ptr AudioClientConnection::Create( } bool AudioClientConnection::ReceiveCommands(AudioServerExecutor& executor) { - // The largest msg the client will send is 24 bytes long, using uint64_t - // guarantees it's aligned to 64 bits. - uint64_t recv_buffer[3]; - auto recv_size = + constexpr auto kBufSize = sizeof(virtio_snd_ctl_hdr) + sizeof(virtio_snd_ctl_value); + alignas(uint64_t) uint8_t recv_buffer[kBufSize]; + const auto recv_size = ReceiveMsg(control_socket_, &recv_buffer, sizeof(recv_buffer)); if (recv_size <= 0) { return false; @@ -298,6 +299,47 @@ bool AudioClientConnection::ReceiveCommands(AudioServerExecutor& executor) { case AudioCommandType::VIRTIO_SND_R_JACK_REMAP: LOG(ERROR) << "Unsupported command type: " << cmd_hdr->code.as_uint32_t(); return CmdReply(AudioStatus::VIRTIO_SND_S_NOT_SUPP); + case AudioCommandType::VIRTIO_SND_R_CTL_INFO: { + if (recv_size < sizeof(virtio_snd_query_info)) { + LOG(ERROR) << "Received QUERY_INFO message is too small: " << recv_size; + return false; + } + const auto& query_info = *reinterpret_cast(recv_buffer); + const auto info_count = query_info.count.as_uint32_t(); + const auto start_id = query_info.start_id.as_uint32_t(); + std::vector reply(info_count); + ControlInfoCommand cmd(start_id, info_count, reply.data()); + VLOG(0) << "VIRTIO_SND_R_CTL_INFO: start_id=" << start_id + << ", count=" << info_count; + + executor.ControlsInfo(cmd); + return CmdReply(cmd.status(), reply.data(), reply.size() * sizeof(virtio_snd_ctl_info)); + } + case AudioCommandType::VIRTIO_SND_R_CTL_READ: { + if (recv_size < sizeof(virtio_snd_ctl_hdr)) { + LOG(ERROR) << "Received CTL_READ message is too small: " << recv_size; + return false; + } + const auto& ctl_hdr = *reinterpret_cast(recv_buffer); + virtio_snd_ctl_value reply_value = {}; + ControlCommand cmd(AudioCommandType::VIRTIO_SND_R_CTL_READ, + ctl_hdr.control_id.as_uint32_t(), &reply_value); + executor.OnControlCommand(cmd); + return CmdReply(cmd.status(), &reply_value, sizeof(reply_value)); + } + case AudioCommandType::VIRTIO_SND_R_CTL_WRITE: { + constexpr auto kHdrSize = sizeof(virtio_snd_ctl_hdr); + if (recv_size < sizeof(virtio_snd_ctl_value) + kHdrSize) { + LOG(ERROR) << "Received CTL_WRITE message is too small: " << recv_size; + return false; + } + const auto& ctl_hdr = *reinterpret_cast(recv_buffer); + const auto ctl_value = + reinterpret_cast(&recv_buffer[kHdrSize]); + ControlCommand cmd(AudioCommandType::VIRTIO_SND_R_CTL_WRITE, ctl_hdr.control_id.as_uint32_t(), ctl_value); + executor.OnControlCommand(cmd); + return CmdReply(cmd.status()); + } default: LOG(ERROR) << "Unknown command type: " << cmd_hdr->code.as_uint32_t(); return CmdReply(AudioStatus::VIRTIO_SND_S_BAD_MSG); diff --git a/base/cvd/cuttlefish/host/libs/audio_connector/server.h b/base/cvd/cuttlefish/host/libs/audio_connector/server.h index 96a3c0f9d53..3f20902ba42 100644 --- a/base/cvd/cuttlefish/host/libs/audio_connector/server.h +++ b/base/cvd/cuttlefish/host/libs/audio_connector/server.h @@ -42,6 +42,8 @@ class AudioServerExecutor { virtual void StopStream(StreamControlCommand& cmd) = 0; virtual void ChmapsInfo(ChmapInfoCommand& cmd) = 0; virtual void JacksInfo(JackInfoCommand& cmd) = 0; + virtual void ControlsInfo(ControlInfoCommand& cmd) = 0; + virtual void OnControlCommand(ControlCommand& cmd) = 0; // Implementations must call buffer.SendStatus() before destroying the buffer // to notify the other side of the release of the buffer. Failure to do so @@ -54,7 +56,7 @@ class AudioClientConnection { public: static std::unique_ptr Create( SharedFD client_socket, uint32_t num_streams, uint32_t num_jacks, - uint32_t num_chmaps, size_t tx_shm_len, size_t rx_shm_len); + uint32_t num_chmaps, uint32_t num_controls, size_t tx_shm_len, size_t rx_shm_len); AudioClientConnection() = delete; AudioClientConnection(const AudioClientConnection&) = delete; @@ -104,6 +106,7 @@ class AudioServer { std::unique_ptr AcceptClient(uint32_t num_streams, uint32_t num_jacks, uint32_t num_chmaps, + uint32_t num_controls, size_t tx_shm_len, size_t rx_shm_len); diff --git a/base/cvd/cuttlefish/host/libs/audio_connector/shm_layout.h b/base/cvd/cuttlefish/host/libs/audio_connector/shm_layout.h index 7ef4bd581de..f6243307912 100644 --- a/base/cvd/cuttlefish/host/libs/audio_connector/shm_layout.h +++ b/base/cvd/cuttlefish/host/libs/audio_connector/shm_layout.h @@ -35,6 +35,15 @@ enum class AudioCommandType : uint32_t { /* channel map control request types */ VIRTIO_SND_R_CHMAP_INFO = 0x0200, + + /* control element request types */ + VIRTIO_SND_R_CTL_INFO = 0x0300, + VIRTIO_SND_R_CTL_ENUM_ITEMS, + VIRTIO_SND_R_CTL_READ, + VIRTIO_SND_R_CTL_WRITE, + VIRTIO_SND_R_CTL_TLV_READ, + VIRTIO_SND_R_CTL_TLV_WRITE, + VIRTIO_SND_R_CTL_TLV_COMMAND, }; enum class AudioStatus : uint32_t { @@ -217,14 +226,96 @@ struct virtio_snd_pcm_status { Le32 latency_bytes; }; -// Update this value when the msg layouts change -const uint32_t VIOS_VERSION = 2; +enum AudioControlRole { + VIRTIO_SND_CTL_ROLE_UNDEFINED = 0, + VIRTIO_SND_CTL_ROLE_VOLUME, + VIRTIO_SND_CTL_ROLE_MUTE, + VIRTIO_SND_CTL_ROLE_GAIN, +}; + +enum AudioControlType { + VIRTIO_SND_CTL_TYPE_BOOLEAN = 0, + VIRTIO_SND_CTL_TYPE_INTEGER, + VIRTIO_SND_CTL_TYPE_INTEGER64, + VIRTIO_SND_CTL_TYPE_ENUMERATED, + VIRTIO_SND_CTL_TYPE_BYTES, + VIRTIO_SND_CTL_TYPE_IEC958, +}; + +enum AudioControlAccess { + VIRTIO_SND_CTL_ACCESS_READ = 0, + VIRTIO_SND_CTL_ACCESS_WRITE, + VIRTIO_SND_CTL_ACCESS_VOLATILE, + VIRTIO_SND_CTL_ACCESS_INACTIVE, + VIRTIO_SND_CTL_ACCESS_TLV_READ, + VIRTIO_SND_CTL_ACCESS_TLV_WRITE, + VIRTIO_SND_CTL_ACCESS_TLV_COMMAND +}; + +struct virtio_snd_ctl_info { + struct virtio_snd_info hdr; + Le32 role; + Le32 type; + Le32 access; // 1 << VIRTIO_SND_CTL_ACCESS_XXX + Le32 count; + Le32 index; + uint8_t name[44]; + union { + struct { + Le32 min; + Le32 max; + Le32 step; + } integer; + struct { + Le64 min; + Le64 max; + Le64 step; + } integer64; + struct { + Le32 items; + } enumerated; + } value; +}; + +struct virtio_snd_ctl_hdr { + struct virtio_snd_hdr hdr; + Le32 control_id; +}; + +struct virtio_snd_ctl_value { + union { + struct { + Le32 value[128]; + } integer; + struct { + Le64 value[64]; + } integer64; + struct { + Le32 item[128]; + } enumerated; + struct { + uint8_t data[512]; + } bytes; + struct { + uint8_t data[16]; + } iec958; + } value; +}; + +struct virtio_snd_ctl_event { + struct virtio_snd_hdr hdr; + Le16 control_id; + Le16 mask; +}; + +const uint32_t VIOS_VERSION = 3; struct VioSConfig { uint32_t version; uint32_t jacks; uint32_t streams; uint32_t chmaps; + uint32_t controls; }; struct IoTransferMsg { @@ -248,6 +339,9 @@ ASSERT_VALID_MSG_TYPE(virtio_snd_chmap_info, 24); ASSERT_VALID_MSG_TYPE(virtio_snd_pcm_info, 32); ASSERT_VALID_MSG_TYPE(virtio_snd_pcm_set_params, 24); ASSERT_VALID_MSG_TYPE(virtio_snd_pcm_hdr, 8); +ASSERT_VALID_MSG_TYPE(virtio_snd_ctl_info, 96); +ASSERT_VALID_MSG_TYPE(virtio_snd_ctl_hdr, 8); +ASSERT_VALID_MSG_TYPE(virtio_snd_ctl_value, 512); ASSERT_VALID_MSG_TYPE(IoTransferMsg, 12); ASSERT_VALID_MSG_TYPE(IoStatusMsg, 16); #undef ASSERT_VALID_MSG_TYPE