| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/media/audio/services/device_registry/validate.h" |
| |
| #include <fidl/fuchsia.hardware.audio.signalprocessing/cpp/common_types.h> |
| #include <fidl/fuchsia.hardware.audio.signalprocessing/cpp/natural_types.h> |
| #include <fidl/fuchsia.hardware.audio/cpp/fidl.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <lib/zx/clock.h> |
| #include <zircon/errors.h> |
| |
| #include <cmath> |
| #include <cstdint> |
| #include <unordered_set> |
| |
| #include "src/media/audio/services/device_registry/logging.h" |
| #include "src/media/audio/services/device_registry/signal_processing_utils.h" |
| |
| namespace media_audio { |
| |
| namespace { |
| |
| ///////////////////////////////////////////////////// |
| // Utility functions |
| // In the enclosed vector<SampleFormat>, how many are 'format_to_match'? |
| size_t CountFormatMatches(const std::vector<fuchsia_hardware_audio::SampleFormat>& sample_formats, |
| fuchsia_hardware_audio::SampleFormat format_to_match) { |
| return std::count_if(sample_formats.begin(), sample_formats.end(), |
| [format_to_match](const auto& rb_sample_format) { |
| return rb_sample_format == format_to_match; |
| }); |
| } |
| |
| // In the enclosed vector<ChannelSet>, how many num_channels equal 'channel_count_to_match'? |
| size_t CountChannelMatches(const std::vector<fuchsia_hardware_audio::ChannelSet>& channel_sets, |
| size_t channel_count_to_match) { |
| return std::count_if( |
| channel_sets.begin(), channel_sets.end(), |
| [channel_count_to_match](const fuchsia_hardware_audio::ChannelSet& channel_set) { |
| return channel_set.attributes()->size() == channel_count_to_match; |
| }); |
| } |
| |
| // In the enclosed vector<uint8_t>, how many values equal 'uchar_to_match'? |
| size_t CountUcharMatches(const std::vector<uint8_t>& uchars, size_t uchar_to_match) { |
| return std::count_if(uchars.begin(), uchars.end(), |
| [uchar_to_match](const auto& uchar) { return uchar == uchar_to_match; }); |
| } |
| |
| } // namespace |
| |
| bool ClientIsValidForDeviceType(const fuchsia_audio_device::DeviceType& device_type, |
| const fuchsia_audio_device::DriverClient& driver_client) { |
| switch (driver_client.Which()) { |
| case fuchsia_audio_device::DriverClient::Tag::kCodec: |
| return (device_type == fuchsia_audio_device::DeviceType::kCodec); |
| case fuchsia_audio_device::DriverClient::Tag::kComposite: |
| return (device_type == fuchsia_audio_device::DeviceType::kComposite); |
| case fuchsia_audio_device::DriverClient::Tag::kDai: |
| return (device_type == fuchsia_audio_device::DeviceType::kDai); |
| case fuchsia_audio_device::DriverClient::Tag::kStreamConfig: |
| return (device_type == fuchsia_audio_device::DeviceType::kInput || |
| device_type == fuchsia_audio_device::DeviceType::kOutput); |
| default: |
| return false; |
| } |
| } |
| |
| // Translate from fuchsia_hardware_audio::SupportedFormats to fuchsia_audio_device::PcmFormatSet. |
| std::vector<fuchsia_audio_device::PcmFormatSet> TranslateRingBufferFormatSets( |
| const std::vector<fuchsia_hardware_audio::SupportedFormats>& ring_buffer_format_sets) { |
| // translated_ring_buffer_format_sets is more complex to copy, since fuchsia_audio_device defines |
| // its tables from scratch instead of reusing types from fuchsia_hardware_audio. We build from the |
| // inside-out: populating attributes then channel_sets then translated_ring_buffer_format_sets. |
| std::vector<fuchsia_audio_device::PcmFormatSet> translated_ring_buffer_format_sets; |
| for (auto& ring_buffer_format_set : ring_buffer_format_sets) { |
| auto& pcm_formats = *ring_buffer_format_set.pcm_supported_formats(); |
| |
| const uint32_t max_format_rate = |
| *std::max_element(pcm_formats.frame_rates()->begin(), pcm_formats.frame_rates()->end()); |
| |
| // Construct channel_sets |
| std::vector<fuchsia_audio_device::ChannelSet> channel_sets; |
| for (const auto& chan_set : *pcm_formats.channel_sets()) { |
| std::vector<fuchsia_audio_device::ChannelAttributes> attributes; |
| for (const auto& attribs : *chan_set.attributes()) { |
| std::optional<uint32_t> max_channel_frequency; |
| if (attribs.max_frequency()) { |
| max_channel_frequency = std::min(*attribs.max_frequency(), max_format_rate / 2); |
| } |
| attributes.push_back({{ |
| .min_frequency = attribs.min_frequency(), |
| .max_frequency = max_channel_frequency, |
| }}); |
| } |
| channel_sets.push_back({{.attributes = attributes}}); |
| } |
| if (channel_sets.empty()) { |
| FX_LOGS(WARNING) << "Could not translate a format set - channel_sets was empty"; |
| continue; |
| } |
| |
| // Construct our sample_types by intersecting vectors received from the device. |
| // fuchsia_audio::SampleType defines a sparse set of types, so we populate the vector |
| // in a bespoke manner (first unsigned, then signed, then float). |
| std::vector<fuchsia_audio::SampleType> sample_types; |
| if (CountFormatMatches(*pcm_formats.sample_formats(), |
| fuchsia_hardware_audio::SampleFormat::kPcmUnsigned) > 0 && |
| CountUcharMatches(*pcm_formats.bytes_per_sample(), 1) > 0) { |
| sample_types.push_back(fuchsia_audio::SampleType::kUint8); |
| } |
| if (CountFormatMatches(*pcm_formats.sample_formats(), |
| fuchsia_hardware_audio::SampleFormat::kPcmSigned) > 0) { |
| if (CountUcharMatches(*pcm_formats.bytes_per_sample(), 2) > 0) { |
| sample_types.push_back(fuchsia_audio::SampleType::kInt16); |
| } |
| if (CountUcharMatches(*pcm_formats.bytes_per_sample(), 4) > 0) { |
| sample_types.push_back(fuchsia_audio::SampleType::kInt32); |
| } |
| } |
| if (CountFormatMatches(*pcm_formats.sample_formats(), |
| fuchsia_hardware_audio::SampleFormat::kPcmFloat) > 0 && |
| CountUcharMatches(*pcm_formats.bytes_per_sample(), 4) > 0) { |
| if (CountUcharMatches(*pcm_formats.bytes_per_sample(), 4) > 0) { |
| sample_types.push_back(fuchsia_audio::SampleType::kFloat32); |
| } |
| if (CountUcharMatches(*pcm_formats.bytes_per_sample(), 8) > 0) { |
| sample_types.push_back(fuchsia_audio::SampleType::kFloat64); |
| } |
| } |
| if (sample_types.empty()) { |
| FX_LOGS(WARNING) << "Could not translate a format set - sample_types was empty"; |
| continue; |
| } |
| |
| if (pcm_formats.frame_rates()->empty()) { |
| FX_LOGS(WARNING) << "Could not translate a format set - frame_rates was empty"; |
| continue; |
| } |
| // Make a copy of the frame_rates result, so we can sort it. |
| std::vector<uint32_t> frame_rates = *pcm_formats.frame_rates(); |
| std::sort(frame_rates.begin(), frame_rates.end()); |
| |
| fuchsia_audio_device::PcmFormatSet pcm_format_set = {{ |
| .channel_sets = channel_sets, |
| .sample_types = sample_types, |
| .frame_rates = frame_rates, |
| }}; |
| translated_ring_buffer_format_sets.emplace_back(pcm_format_set); |
| } |
| return translated_ring_buffer_format_sets; |
| } |
| |
| bool ValidateStreamProperties(const fuchsia_hardware_audio::StreamProperties& stream_props, |
| std::optional<const fuchsia_hardware_audio::GainState> gain_state, |
| std::optional<const fuchsia_hardware_audio::PlugState> plug_state) { |
| ADR_LOG(kLogDeviceMethods); |
| LogStreamProperties(stream_props); |
| |
| if (!stream_props.is_input() || !stream_props.min_gain_db() || !stream_props.max_gain_db() || |
| !stream_props.gain_step_db() || !stream_props.plug_detect_capabilities() || |
| !stream_props.clock_domain()) { |
| FX_LOGS(WARNING) << "Incomplete StreamConfig/GetProperties response"; |
| return false; |
| } |
| |
| if ((stream_props.manufacturer().has_value() && stream_props.manufacturer()->empty()) || |
| (stream_props.product().has_value() && stream_props.product()->empty())) { |
| FX_LOGS(WARNING) << __func__ |
| << ": manufacturer and product, if present, must not be empty strings"; |
| return false; |
| } |
| |
| // Eliminate NaN or infinity values |
| if (!std::isfinite(*stream_props.min_gain_db())) { |
| FX_LOGS(WARNING) << "Reported min_gain_db is NaN or infinity"; |
| return false; |
| } |
| if (!std::isfinite(*stream_props.max_gain_db())) { |
| FX_LOGS(WARNING) << "Reported max_gain_db is NaN or infinity"; |
| return false; |
| } |
| if (!std::isfinite(*stream_props.gain_step_db())) { |
| FX_LOGS(WARNING) << "Reported gain_step_db is NaN or infinity"; |
| return false; |
| } |
| |
| if (*stream_props.min_gain_db() > *stream_props.max_gain_db()) { |
| FX_LOGS(WARNING) << "GetProperties: min_gain_db cannot exceed max_gain_db: " |
| << *stream_props.min_gain_db() << "," << *stream_props.max_gain_db(); |
| return false; |
| } |
| if (*stream_props.gain_step_db() > *stream_props.max_gain_db() - *stream_props.min_gain_db()) { |
| FX_LOGS(WARNING) << "GetProperties: gain_step_db cannot exceed max_gain_db-min_gain_db: " |
| << *stream_props.gain_step_db() << "," |
| << *stream_props.max_gain_db() - *stream_props.min_gain_db(); |
| return false; |
| } |
| if (*stream_props.gain_step_db() < 0.0f) { |
| FX_LOGS(WARNING) << "GetProperties: gain_step_db (" << *stream_props.gain_step_db() |
| << ") cannot be negative"; |
| return false; |
| } |
| |
| // If we already have this device's GainState, double-check against that. |
| if (gain_state) { |
| if (*gain_state->gain_db() < *stream_props.min_gain_db() || |
| *gain_state->gain_db() > *stream_props.max_gain_db()) { |
| FX_LOGS(WARNING) << "Gain range reported by GetProperties does not include current gain_db: " |
| << *gain_state->gain_db(); |
| return false; |
| } |
| |
| // Device can't mute (or doesn't say it can), but says it is currently muted... |
| if (!stream_props.can_mute().value_or(false) && gain_state->muted().value_or(false)) { |
| FX_LOGS(WARNING) << "GetProperties reports can_mute FALSE, but device is muted"; |
| return false; |
| } |
| // Device doesn't have AGC (or doesn't say it does), but says AGC is currently enabled... |
| if (!stream_props.can_agc().value_or(false) && gain_state->agc_enabled().value_or(false)) { |
| FX_LOGS(WARNING) << "GetProperties reports can_agc FALSE, but AGC is enabled"; |
| return false; |
| } |
| } |
| |
| // If we already have this device's PlugState, double-check against that. |
| if (plug_state && !(*plug_state->plugged()) && |
| *stream_props.plug_detect_capabilities() == |
| fuchsia_hardware_audio::PlugDetectCapabilities::kHardwired) { |
| FX_LOGS(WARNING) << "GetProperties reports HARDWIRED, but StreamConfig reports as UNPLUGGED"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool ValidateRingBufferFormatSets( |
| const std::vector<fuchsia_hardware_audio::SupportedFormats>& ring_buffer_format_sets) { |
| LogRingBufferFormatSets(ring_buffer_format_sets); |
| |
| if (ring_buffer_format_sets.empty()) { |
| FX_LOGS(WARNING) << "GetRingBufferFormatSets: ring_buffer_format_sets[] is empty"; |
| return false; |
| } |
| |
| for (const auto& rb_format_set : ring_buffer_format_sets) { |
| if (!rb_format_set.pcm_supported_formats()) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: pcm_supported_formats is absent"; |
| return false; |
| } |
| const auto& pcm_format_set = *rb_format_set.pcm_supported_formats(); |
| |
| // Frame rates |
| if (!pcm_format_set.frame_rates() || pcm_format_set.frame_rates()->empty()) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: frame_rates[] is " |
| << (pcm_format_set.frame_rates() ? "empty" : "absent"); |
| return false; |
| } |
| // While testing frame_rates, we can determine max_supported_frame_rate. |
| uint32_t prev_frame_rate = 0, max_supported_frame_rate = 0; |
| for (const auto& rate : *pcm_format_set.frame_rates()) { |
| if (rate < kMinSupportedRingBufferFrameRate || rate > kMaxSupportedRingBufferFrameRate) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: frame_rate (" << rate << ") out of range [" |
| << kMinSupportedRingBufferFrameRate << "," |
| << kMaxSupportedRingBufferFrameRate << "] "; |
| return false; |
| } |
| // Checking for "strictly ascending" also eliminates duplicate entries. |
| if (rate <= prev_frame_rate) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: frame_rate must be in ascending order: " |
| << prev_frame_rate << " was listed before " << rate; |
| return false; |
| } |
| prev_frame_rate = rate; |
| max_supported_frame_rate = std::max(max_supported_frame_rate, rate); |
| } |
| |
| // Channel sets |
| if (!pcm_format_set.channel_sets() || pcm_format_set.channel_sets()->empty()) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: channel_sets[] is " |
| << (pcm_format_set.channel_sets() ? "empty" : "absent"); |
| return false; |
| } |
| auto max_allowed_frequency = max_supported_frame_rate / 2; |
| for (const fuchsia_hardware_audio::ChannelSet& chan_set : *pcm_format_set.channel_sets()) { |
| if (!chan_set.attributes() || chan_set.attributes()->empty()) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: ChannelSet.attributes[] is " |
| << (chan_set.attributes() ? "empty" : "absent"); |
| return false; |
| } |
| if (CountChannelMatches(*pcm_format_set.channel_sets(), chan_set.attributes()->size()) > 1) { |
| FX_LOGS(WARNING) |
| << "GetSupportedFormats: channel-count must be unique across channel_sets: " |
| << chan_set.attributes()->size(); |
| return false; |
| } |
| for (const auto& attrib : *chan_set.attributes()) { |
| if (attrib.min_frequency()) { |
| if (*attrib.min_frequency() > max_allowed_frequency) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: ChannelAttributes.min_frequency (" |
| << *attrib.min_frequency() << ") out of range: " << "[0, " |
| << max_allowed_frequency << "]"; |
| return false; |
| } |
| if (attrib.max_frequency() && *attrib.min_frequency() > *attrib.max_frequency()) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: min_frequency (" << *attrib.min_frequency() |
| << ") cannot exceed max_frequency (" << *attrib.max_frequency() << ")"; |
| return false; |
| } |
| } |
| |
| if (attrib.max_frequency()) { |
| if (*attrib.max_frequency() > max_allowed_frequency) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: ChannelAttrib.max_frequency " |
| << *attrib.max_frequency() << " will be limited to " |
| << max_allowed_frequency; |
| } |
| } |
| } |
| } |
| |
| // Sample format |
| if (!pcm_format_set.sample_formats() || pcm_format_set.sample_formats()->empty()) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: sample_formats[] is " |
| << (pcm_format_set.sample_formats() ? "empty" : "absent"); |
| return false; |
| } |
| const auto& rb_sample_formats = *pcm_format_set.sample_formats(); |
| for (const auto& format : rb_sample_formats) { |
| if (CountFormatMatches(rb_sample_formats, format) > 1) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: no duplicate SampleFormat values allowed: " |
| << format; |
| return false; |
| } |
| } |
| |
| // Bytes per sample |
| if (!pcm_format_set.bytes_per_sample() || pcm_format_set.bytes_per_sample()->empty()) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: bytes_per_sample[] is " |
| << (pcm_format_set.bytes_per_sample() ? "empty" : "absent"); |
| return false; |
| } |
| uint8_t prev_bytes_per_sample = 0, max_bytes_per_sample = 0; |
| for (const auto& bytes : *pcm_format_set.bytes_per_sample()) { |
| if (CountFormatMatches(rb_sample_formats, fuchsia_hardware_audio::SampleFormat::kPcmSigned) && |
| (bytes != 2 && bytes != 4)) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: bytes_per_sample (" |
| << static_cast<uint16_t>(bytes) |
| << ") must be 2 or 4 for PCM_SIGNED format"; |
| return false; |
| } |
| if (CountFormatMatches(rb_sample_formats, fuchsia_hardware_audio::SampleFormat::kPcmFloat) && |
| (bytes != 4 && bytes != 8)) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: bytes_per_sample (" |
| << static_cast<uint16_t>(bytes) << ") must be 4 or 8 for PCM_FLOAT format"; |
| return false; |
| } |
| if (CountFormatMatches(rb_sample_formats, |
| fuchsia_hardware_audio::SampleFormat::kPcmUnsigned) && |
| bytes != 1) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: bytes_per_sample (" |
| << static_cast<uint16_t>(bytes) << ") must be 1 for PCM_UNSIGNED format"; |
| return false; |
| } |
| // Checking for "strictly ascending" also eliminates duplicate entries. |
| if (bytes <= prev_bytes_per_sample) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: bytes_per_sample must be in ascending order: " |
| << static_cast<uint16_t>(prev_bytes_per_sample) << " was listed before " |
| << static_cast<uint16_t>(bytes); |
| return false; |
| } |
| prev_bytes_per_sample = bytes; |
| |
| max_bytes_per_sample = std::max(max_bytes_per_sample, bytes); |
| } |
| |
| // Valid bits per sample |
| if (!pcm_format_set.valid_bits_per_sample() || |
| pcm_format_set.valid_bits_per_sample()->empty()) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: valid_bits_per_sample[] is " |
| << (pcm_format_set.valid_bits_per_sample() ? "empty" : "absent"); |
| return false; |
| } |
| uint8_t prev_valid_bits = 0; |
| for (const auto& valid_bits : *pcm_format_set.valid_bits_per_sample()) { |
| if (valid_bits == 0 || valid_bits > max_bytes_per_sample * 8) { |
| FX_LOGS(WARNING) << "GetSupportedFormats: valid_bits_per_sample (" |
| << static_cast<uint16_t>(valid_bits) << ") out of range [1, " |
| << max_bytes_per_sample * 8 << "]"; |
| return false; |
| } |
| // Checking for "strictly ascending" also eliminates duplicate entries. |
| if (valid_bits <= prev_valid_bits) { |
| FX_LOGS(WARNING) |
| << "GetSupportedFormats: valid_bits_per_sample must be in ascending order: " |
| << static_cast<uint16_t>(prev_valid_bits) << " was listed before " |
| << static_cast<uint16_t>(valid_bits); |
| return false; |
| } |
| prev_valid_bits = valid_bits; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool ValidateCodecProperties(const fuchsia_hardware_audio::CodecProperties& codec_props, |
| std::optional<const fuchsia_hardware_audio::PlugState> plug_state) { |
| ADR_LOG(kLogDeviceMethods); |
| LogCodecProperties(codec_props); |
| |
| if ((codec_props.manufacturer().has_value() && codec_props.manufacturer()->empty()) || |
| (codec_props.product().has_value() && codec_props.product()->empty())) { |
| FX_LOGS(WARNING) << __func__ |
| << ": manufacturer and product, if present, must not be empty strings"; |
| return false; |
| } |
| |
| if (!codec_props.plug_detect_capabilities()) { |
| FX_LOGS(WARNING) << "Incomplete Codec/GetProperties response"; |
| return false; |
| } |
| |
| // If we already have this device's PlugState, double-check against that. |
| if (plug_state && !(*plug_state->plugged()) && |
| *codec_props.plug_detect_capabilities() == |
| fuchsia_hardware_audio::PlugDetectCapabilities::kHardwired) { |
| FX_LOGS(WARNING) << "GetProperties reports HARDWIRED, but Codec reports as UNPLUGGED"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool ValidateDaiFormatSets( |
| const std::vector<fuchsia_hardware_audio::DaiSupportedFormats>& dai_format_sets) { |
| LogDaiFormatSets(dai_format_sets); |
| |
| if (dai_format_sets.empty()) { |
| FX_LOGS(WARNING) << "GetDaiSupportedFormats: response is empty"; |
| return false; |
| } |
| |
| for (const auto& dai_format_set : dai_format_sets) { |
| if (dai_format_set.number_of_channels().empty()) { |
| FX_LOGS(WARNING) << "Non-compliant DaiSupportedFormats: empty number_of_channels vector"; |
| return false; |
| } |
| uint32_t previous_chans = 0; |
| for (const auto& chans : dai_format_set.number_of_channels()) { |
| if (chans <= previous_chans || |
| chans > fuchsia_hardware_audio::kMaxCountDaiSupportedNumberOfChannels) { |
| FX_LOGS(WARNING) << "Non-compliant DaiSupportedFormats number_of_channels: " << chans; |
| return false; |
| } |
| previous_chans = chans; |
| } |
| if (dai_format_set.sample_formats().empty()) { |
| FX_LOGS(WARNING) << "Non-compliant DaiSupportedFormats: empty sample_formats vector"; |
| return false; |
| } |
| for (const auto& dai_sample_format : dai_format_set.sample_formats()) { |
| if (std::count(dai_format_set.sample_formats().begin(), dai_format_set.sample_formats().end(), |
| dai_sample_format) > 1) { |
| FX_LOGS(WARNING) << "Duplicate DaiSupportedFormats sample_format: " << dai_sample_format; |
| return false; |
| } |
| } |
| if (dai_format_set.frame_formats().empty()) { |
| FX_LOGS(WARNING) << "Non-compliant DaiSupportedFormats: empty frame_formats vector"; |
| return false; |
| } |
| for (const auto& dai_frame_format : dai_format_set.frame_formats()) { |
| if (std::count(dai_format_set.frame_formats().begin(), dai_format_set.frame_formats().end(), |
| dai_frame_format) > 1) { |
| FX_LOGS(WARNING) << "Duplicate DaiSupportedFormats frame_format: " << dai_frame_format; |
| return false; |
| } |
| } |
| if (dai_format_set.frame_rates().empty()) { |
| FX_LOGS(WARNING) << "Non-compliant DaiSupportedFormats: empty frame_rates vector"; |
| return false; |
| } |
| uint32_t previous_rate = 0; |
| for (const auto& rate : dai_format_set.frame_rates()) { |
| if (rate <= previous_rate || rate < kMinSupportedDaiFrameRate || |
| rate > kMaxSupportedDaiFrameRate) { |
| FX_LOGS(WARNING) << "Non-compliant DaiSupportedFormats frame_rate: " << rate; |
| return false; |
| } |
| previous_rate = rate; |
| } |
| if (dai_format_set.bits_per_slot().empty()) { |
| FX_LOGS(WARNING) << "Non-compliant DaiSupportedFormats: empty bits_per_slot vector"; |
| return false; |
| } |
| uint32_t previous_bits_per_slot = 0; |
| uint32_t max_bits_per_slot = 0; |
| for (const auto& bits : dai_format_set.bits_per_slot()) { |
| if (bits <= previous_bits_per_slot || bits == 0 || bits > kMaxSupportedDaiFormatBitsPerSlot) { |
| FX_LOGS(WARNING) << "Non-compliant DaiSupportedFormats bits_per_slot: " |
| << static_cast<uint16_t>(bits); |
| return false; |
| } |
| max_bits_per_slot = std::max<uint32_t>(bits, max_bits_per_slot); |
| previous_bits_per_slot = bits; |
| } |
| if (dai_format_set.bits_per_sample().empty()) { |
| FX_LOGS(WARNING) << "Non-compliant DaiSupportedFormats: empty bits_per_sample vector"; |
| return false; |
| } |
| uint32_t previous_bits_per_sample = 0; |
| for (const auto& bits : dai_format_set.bits_per_sample()) { |
| if (bits <= previous_bits_per_sample || bits > max_bits_per_slot || bits == 0) { |
| FX_LOGS(WARNING) << "Non-compliant DaiSupportedFormats bits_per_sample: " |
| << static_cast<uint16_t>(bits); |
| return false; |
| } |
| previous_bits_per_sample = bits; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool ValidateDaiFormat(const fuchsia_hardware_audio::DaiFormat& dai_format) { |
| ADR_LOG(kLogDeviceMethods); |
| LogDaiFormat(dai_format); |
| |
| if (dai_format.number_of_channels() == 0 || |
| dai_format.number_of_channels() > |
| fuchsia_hardware_audio::kMaxCountDaiSupportedNumberOfChannels) { |
| FX_LOGS(WARNING) << "Non-compliant DaiFormat number_of_channels: " |
| << dai_format.number_of_channels(); |
| return false; |
| } |
| |
| if (dai_format.channels_to_use_bitmask() == 0) { |
| FX_LOGS(WARNING) << "Non-compliant DaiFormat channels_to_use_bitmask: 0"; |
| return false; |
| } |
| if (dai_format.number_of_channels() < 64 && |
| (dai_format.channels_to_use_bitmask() >> dai_format.number_of_channels()) > 0) { |
| FX_LOGS(WARNING) << "Non-compliant DaiFormat channels_to_use_bitmask: 0x" << std::hex |
| << dai_format.channels_to_use_bitmask() << " is too large for " << std::dec |
| << dai_format.number_of_channels() << " channels"; |
| return false; |
| } |
| |
| switch (dai_format.sample_format()) { |
| case fuchsia_hardware_audio::DaiSampleFormat::kPdm: |
| case fuchsia_hardware_audio::DaiSampleFormat::kPcmSigned: |
| case fuchsia_hardware_audio::DaiSampleFormat::kPcmUnsigned: |
| case fuchsia_hardware_audio::DaiSampleFormat::kPcmFloat: |
| break; |
| default: |
| FX_LOGS(WARNING) << "Non-compliant DaiFormat sample_format: UNKNOWN enum"; |
| return false; |
| } |
| |
| if (!dai_format.frame_format().frame_format_custom().has_value() && |
| !dai_format.frame_format().frame_format_standard().has_value()) { |
| FX_LOGS(WARNING) << "Non-compliant DaiFormat frame_format: UNKNOWN union enum"; |
| return false; |
| } |
| switch (dai_format.frame_format().Which()) { |
| case fuchsia_hardware_audio::DaiFrameFormat::Tag::kFrameFormatStandard: |
| case fuchsia_hardware_audio::DaiFrameFormat::Tag::kFrameFormatCustom: |
| break; |
| default: |
| FX_LOGS(WARNING) << "Non-compliant DaiFormat frame_format: UNKNOWN union tag"; |
| return false; |
| } |
| |
| if (dai_format.frame_rate() < kMinSupportedDaiFrameRate || |
| dai_format.frame_rate() > kMaxSupportedDaiFrameRate) { |
| FX_LOGS(WARNING) << "Non-compliant DaiFormat frame_rate: " << dai_format.frame_rate(); |
| return false; |
| } |
| |
| if (dai_format.bits_per_slot() == 0 || |
| dai_format.bits_per_slot() > kMaxSupportedDaiFormatBitsPerSlot) { |
| FX_LOGS(WARNING) << "Non-compliant DaiFormat bits_per_slot: " |
| << static_cast<uint16_t>(dai_format.bits_per_slot()); |
| return false; |
| } |
| |
| if (dai_format.bits_per_sample() == 0 || |
| dai_format.bits_per_sample() > dai_format.bits_per_slot()) { |
| FX_LOGS(WARNING) << "Non-compliant DaiFormat bits_per_sample: " |
| << static_cast<uint16_t>(dai_format.bits_per_sample()); |
| return false; |
| } |
| return true; |
| } |
| |
| bool ValidateCodecFormatInfo(const fuchsia_hardware_audio::CodecFormatInfo& format_info) { |
| ADR_LOG(kLogDeviceMethods); |
| LogCodecFormatInfo(format_info); |
| |
| if (format_info.external_delay() && *format_info.external_delay() < 0) { |
| FX_LOGS(WARNING) << "Invalid Codec::SetDaiFormat response - external_delay cannot be negative"; |
| return false; |
| } |
| if (format_info.turn_on_delay() && *format_info.turn_on_delay() < 0) { |
| FX_LOGS(WARNING) << "Invalid Codec::SetDaiFormat response - turn_on_delay cannot be negative"; |
| return false; |
| } |
| if (format_info.turn_off_delay() && *format_info.turn_off_delay() < 0) { |
| FX_LOGS(WARNING) << "Invalid Codec::SetDaiFormat response - turn_off_delay cannot be negative"; |
| return false; |
| } |
| return true; |
| } |
| |
| bool ValidateCompositeProperties( |
| const fuchsia_hardware_audio::CompositeProperties& composite_props) { |
| LogCompositeProperties(composite_props); |
| |
| if (!composite_props.clock_domain()) { |
| FX_LOGS(WARNING) << "Incomplete Composite/GetProperties response"; |
| return false; |
| } |
| |
| if ((composite_props.manufacturer().has_value() && composite_props.manufacturer()->empty()) || |
| (composite_props.product().has_value() && composite_props.product()->empty())) { |
| FX_LOGS(WARNING) << __func__ |
| << ": manufacturer and product, if present, must not be empty strings"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool ValidateGainState(const fuchsia_hardware_audio::GainState& gain_state, |
| std::optional<const fuchsia_hardware_audio::StreamProperties> stream_props) { |
| ADR_LOG(kLogDeviceMethods); |
| LogGainState(gain_state); |
| |
| if (!gain_state.gain_db()) { |
| FX_LOGS(WARNING) << "Incomplete StreamConfig/WatchGainState response"; |
| return false; |
| } |
| |
| // Eliminate NaN or infinity values |
| if (!std::isfinite(*gain_state.gain_db())) { |
| FX_LOGS(WARNING) << "Reported gain_db is NaN or infinity"; |
| return false; |
| } |
| |
| // If we already have this device's GainCapabilities, double-check against those. |
| if (stream_props) { |
| if (*gain_state.gain_db() < *stream_props->min_gain_db() || |
| *gain_state.gain_db() > *stream_props->max_gain_db()) { |
| FX_LOGS(WARNING) << "Reported gain_db is out of range: " << *gain_state.gain_db(); |
| return false; |
| } |
| // Device reports it can't mute (or doesn't say it can), then DOES say that it is muted.... |
| if (!stream_props->can_mute().value_or(false) && gain_state.muted().value_or(false)) { |
| FX_LOGS(WARNING) << "Reported 'muted' state (TRUE) is unsupported"; |
| return false; |
| } |
| // Device reports it can't AGC (or doesn't say it can), then DOES say that AGC is enabled.... |
| if (!stream_props->can_agc().value_or(false) && gain_state.agc_enabled().value_or(false)) { |
| FX_LOGS(WARNING) << "Reported 'agc_enabled' state (TRUE) is unsupported"; |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool ValidatePlugState( |
| const fuchsia_hardware_audio::PlugState& plug_state, |
| std::optional<fuchsia_hardware_audio::PlugDetectCapabilities> plug_detect_capabilities) { |
| LogPlugState(plug_state); |
| |
| if (!plug_state.plugged() || !plug_state.plug_state_time()) { |
| FX_LOGS(WARNING) << "Incomplete StreamConfig/WatchPlugState response: required field missing"; |
| return false; |
| } |
| |
| int64_t now = zx::clock::get_monotonic().get(); |
| if (*plug_state.plug_state_time() > now) { |
| FX_LOGS(WARNING) << "Reported plug_time is in the future: " << *plug_state.plug_state_time(); |
| return false; |
| } |
| |
| // If we already have this device's PlugDetectCapabilities, double-check against those. |
| if (plug_detect_capabilities) { |
| if (*plug_detect_capabilities == fuchsia_hardware_audio::PlugDetectCapabilities::kHardwired && |
| !plug_state.plugged().value_or(true)) { |
| FX_LOGS(WARNING) << "Reported 'plug_state' (UNPLUGGED) is unsupported (HARDWIRED)"; |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| // Validate only DeviceInfo-specific aspects. For example, don't re-validate format correctness. |
| bool ValidateDeviceInfo(const fuchsia_audio_device::Info& device_info) { |
| LogDeviceInfo(device_info); |
| |
| // Validate top-level required members. |
| if (!device_info.token_id().has_value() || !device_info.device_type().has_value() || |
| !device_info.device_name().has_value() || device_info.device_name()->empty()) { |
| FX_LOGS(WARNING) << __func__ << ": incomplete DeviceInfo instance"; |
| return false; |
| } |
| // These strings must not be empty, if present. |
| if ((device_info.manufacturer().has_value() && device_info.manufacturer()->empty()) || |
| (device_info.product().has_value() && device_info.product()->empty())) { |
| FX_LOGS(WARNING) << __func__ |
| << ": manufacturer and product, if present, must not be empty strings"; |
| return false; |
| } |
| // These vectors must not be empty, if present. |
| if ((device_info.signal_processing_elements().has_value() && |
| device_info.signal_processing_elements()->empty()) || |
| (device_info.signal_processing_topologies().has_value() && |
| device_info.signal_processing_topologies()->empty())) { |
| FX_LOGS(WARNING) |
| << __func__ |
| << ": signal_processing elements/topologies, if present, must have at least one entry"; |
| return false; |
| } |
| switch (*device_info.device_type()) { |
| case fuchsia_audio_device::DeviceType::kCodec: |
| if (!device_info.dai_format_sets().has_value() || device_info.dai_format_sets()->empty() || |
| !device_info.plug_detect_caps().has_value()) { |
| FX_LOGS(WARNING) << __func__ << ": incomplete DeviceInfo instance"; |
| return false; |
| } |
| if (device_info.ring_buffer_format_sets().has_value() || |
| device_info.gain_caps().has_value() || device_info.clock_domain().has_value()) { |
| FX_LOGS(WARNING) << __func__ << ": invalid DeviceInfo fields are populated"; |
| return false; |
| } |
| break; |
| case fuchsia_audio_device::DeviceType::kComposite: |
| if (!device_info.clock_domain().has_value() || |
| !device_info.signal_processing_elements().has_value() || |
| !device_info.signal_processing_topologies().has_value()) { |
| FX_LOGS(WARNING) << __func__ << ": incomplete DeviceInfo instance"; |
| return false; |
| } |
| if (device_info.is_input().has_value() || device_info.gain_caps().has_value() || |
| device_info.plug_detect_caps().has_value()) { |
| FX_LOGS(WARNING) << __func__ << ": invalid DeviceInfo fields are populated"; |
| return false; |
| } |
| break; |
| case fuchsia_audio_device::DeviceType::kInput: |
| case fuchsia_audio_device::DeviceType::kOutput: |
| if (!device_info.is_input().has_value() || !device_info.gain_caps().has_value() || |
| !device_info.ring_buffer_format_sets().has_value() || |
| device_info.ring_buffer_format_sets()->empty() || |
| !device_info.plug_detect_caps().has_value() || !device_info.clock_domain().has_value()) { |
| FX_LOGS(WARNING) << __func__ << ": incomplete DeviceInfo instance"; |
| return false; |
| } |
| if (device_info.dai_format_sets().has_value()) { |
| FX_LOGS(WARNING) << __func__ << ": invalid DeviceInfo fields are populated"; |
| return false; |
| } |
| break; |
| case fuchsia_audio_device::DeviceType::kDai: |
| default: |
| FX_LOGS(WARNING) << __func__ << ": unsupported DeviceType: " << device_info.device_type(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool ValidateRingBufferProperties(const fuchsia_hardware_audio::RingBufferProperties& rb_props) { |
| ADR_LOG(kLogDeviceMethods); |
| LogRingBufferProperties(rb_props); |
| |
| if (!rb_props.needs_cache_flush_or_invalidate()) { |
| FX_LOGS(WARNING) << "Reported RingBufferProperties.needs_cache_flush_or_invalidate is missing"; |
| return false; |
| } |
| if (rb_props.turn_on_delay() && *rb_props.turn_on_delay() < 0) { |
| FX_LOGS(WARNING) << "Reported RingBufferProperties.turn_on_delay is negative"; |
| return false; |
| } |
| if (!rb_props.driver_transfer_bytes()) { |
| FX_LOGS(WARNING) << "Reported RingBufferProperties.driver_transfer_bytes is missing"; |
| return false; |
| } |
| return true; |
| } |
| |
| bool ValidateRingBufferFormat(const fuchsia_hardware_audio::Format& ring_buffer_format) { |
| ADR_LOG(kLogDeviceMethods); |
| LogRingBufferFormat(ring_buffer_format); |
| if (!ring_buffer_format.pcm_format()) { |
| FX_LOGS(WARNING) << "ring_buffer_format must set pcm_format"; |
| return false; |
| } |
| auto& pcm_format = ring_buffer_format.pcm_format().value(); |
| if (pcm_format.number_of_channels() == 0) { |
| FX_LOGS(WARNING) << "RingBuffer number_of_channels is too low"; |
| return false; |
| } |
| // Is there an upper limit on RingBuffer channels? |
| |
| if (pcm_format.bytes_per_sample() == 0) { |
| FX_LOGS(WARNING) << "RingBuffer bytes_per_sample is too low"; |
| return false; |
| } |
| if (pcm_format.sample_format() == fuchsia_hardware_audio::SampleFormat::kPcmUnsigned && |
| pcm_format.bytes_per_sample() > sizeof(uint8_t)) { |
| FX_LOGS(WARNING) << "RingBuffer bytes_per_sample is too high"; |
| return false; |
| } |
| if (pcm_format.sample_format() == fuchsia_hardware_audio::SampleFormat::kPcmSigned && |
| pcm_format.bytes_per_sample() > sizeof(uint32_t)) { |
| FX_LOGS(WARNING) << "RingBuffer bytes_per_sample is too high"; |
| return false; |
| } |
| if (pcm_format.sample_format() == fuchsia_hardware_audio::SampleFormat::kPcmFloat && |
| pcm_format.bytes_per_sample() > sizeof(double)) { |
| FX_LOGS(WARNING) << "RingBuffer bytes_per_sample is too high"; |
| return false; |
| } |
| |
| if (pcm_format.valid_bits_per_sample() == 0) { |
| FX_LOGS(WARNING) << "RingBuffer valid_bits_per_sample is too low"; |
| return false; |
| } |
| auto bytes_per_sample = pcm_format.bytes_per_sample(); |
| if (pcm_format.valid_bits_per_sample() > bytes_per_sample * 8) { |
| FX_LOGS(WARNING) << "RingBuffer valid_bits_per_sample (" |
| << static_cast<uint16_t>(pcm_format.valid_bits_per_sample()) |
| << ") cannot exceed bytes_per_sample (" |
| << static_cast<uint16_t>(bytes_per_sample) << ") * 8"; |
| return false; |
| } |
| |
| if (pcm_format.frame_rate() > kMaxSupportedRingBufferFrameRate || |
| pcm_format.frame_rate() < kMinSupportedRingBufferFrameRate) { |
| FX_LOGS(WARNING) << "RingBuffer frame rate (" << pcm_format.frame_rate() |
| << ") must be within range [" << kMinSupportedRingBufferFrameRate << ", " |
| << kMaxSupportedRingBufferFrameRate << "]"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool ValidateSampleFormatCompatibility(uint8_t bytes_per_sample, |
| fuchsia_hardware_audio::SampleFormat sample_format) { |
| // Explicitly check for fuchsia_audio::SampleType kUint8, kInt16, kInt32, kFloat32, kFloat64 |
| if ((sample_format == fuchsia_hardware_audio::SampleFormat::kPcmUnsigned && |
| bytes_per_sample == 1) || |
| (sample_format == fuchsia_hardware_audio::SampleFormat::kPcmSigned && |
| bytes_per_sample == 2) || |
| (sample_format == fuchsia_hardware_audio::SampleFormat::kPcmSigned && |
| bytes_per_sample == 4) || |
| (sample_format == fuchsia_hardware_audio::SampleFormat::kPcmFloat && bytes_per_sample == 4) || |
| (sample_format == fuchsia_hardware_audio::SampleFormat::kPcmFloat && bytes_per_sample == 8)) { |
| return true; |
| } |
| |
| FX_LOGS(WARNING) << "No valid fuchsia_audio::SampleType exists, for " |
| << static_cast<uint16_t>(bytes_per_sample) << "-byte " << sample_format; |
| return false; |
| } |
| |
| bool ValidateRingBufferVmo(const zx::vmo& vmo, uint32_t num_frames, |
| const fuchsia_hardware_audio::Format& rb_format) { |
| ADR_LOG(kLogDeviceMethods); |
| LogRingBufferVmo(vmo, num_frames, rb_format); |
| |
| uint64_t size; |
| if (!ValidateRingBufferFormat(rb_format)) { |
| return false; |
| } |
| if (!ValidateSampleFormatCompatibility(rb_format.pcm_format()->bytes_per_sample(), |
| rb_format.pcm_format()->sample_format())) { |
| return false; |
| } |
| |
| auto status = vmo.get_size(&size); |
| if (status != ZX_OK) { |
| FX_LOGS(WARNING) << "get_size returned size " << size << " and error " << status; |
| return false; |
| } |
| if (size < static_cast<uint64_t>(num_frames) * rb_format.pcm_format()->number_of_channels() * |
| rb_format.pcm_format()->bytes_per_sample()) { |
| FX_LOGS(WARNING) << "Reported RingBuffer.GetVmo num_frames does not match VMO size"; |
| return false; |
| } |
| return true; |
| } |
| |
| bool ValidateDelayInfo(const fuchsia_hardware_audio::DelayInfo& delay_info) { |
| ADR_LOG(kLogDeviceMethods); |
| LogDelayInfo(delay_info); |
| |
| if (!delay_info.internal_delay()) { |
| FX_LOGS(WARNING) << "Reported DelayInfo.internal_delay is missing"; |
| return false; |
| } |
| const auto internal_delay = *delay_info.internal_delay(); |
| if (internal_delay < 0) { |
| FX_LOGS(WARNING) << "WatchDelayInfo: reported 'internal_delay' (" << internal_delay |
| << " ns) cannot be negative"; |
| return false; |
| } |
| |
| if (delay_info.external_delay() && *delay_info.external_delay() < 0) { |
| FX_LOGS(WARNING) << "WatchDelayInfo: reported 'external_delay' (" |
| << *delay_info.external_delay() << " ns) cannot be negative"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool ValidateElementState( |
| const fuchsia_hardware_audio_signalprocessing::ElementState& element_state, |
| const fuchsia_hardware_audio_signalprocessing::Element& element) { |
| LogElementState(element_state); |
| |
| if (!ValidateElement(element)) { |
| return false; |
| } |
| |
| bool type_specific_matches_element_type; |
| switch (*element.type()) { |
| case fuchsia_hardware_audio_signalprocessing::ElementType::kVendorSpecific: |
| type_specific_matches_element_type = (element_state.type_specific().has_value() && |
| element_state.type_specific()->Which() == |
| fuchsia_hardware_audio_signalprocessing:: |
| TypeSpecificElementState::Tag::kVendorSpecific); |
| |
| // Need additional vendor-specific checks? |
| // Is .vendor_specific_data required? |
| break; |
| case fuchsia_hardware_audio_signalprocessing::ElementType::kGain: |
| type_specific_matches_element_type = |
| (element_state.type_specific().has_value() && |
| element_state.type_specific()->Which() == |
| fuchsia_hardware_audio_signalprocessing::TypeSpecificElementState::Tag::kGain && |
| element_state.type_specific()->gain().has_value() && |
| element_state.type_specific()->gain()->gain().has_value() && |
| *element_state.type_specific()->gain()->gain() >= |
| element.type_specific()->gain()->min_gain() && |
| *element_state.type_specific()->gain()->gain() <= |
| element.type_specific()->gain()->max_gain()); |
| break; |
| case fuchsia_hardware_audio_signalprocessing::ElementType::kEqualizer: |
| type_specific_matches_element_type = |
| (element_state.type_specific().has_value() && |
| element_state.type_specific()->Which() == |
| fuchsia_hardware_audio_signalprocessing::TypeSpecificElementState::Tag::kEqualizer && |
| element_state.type_specific()->equalizer().has_value() && |
| element_state.type_specific()->equalizer()->band_states().has_value() && |
| !element_state.type_specific()->equalizer()->band_states()->empty()); |
| |
| // Need additional EQ-specific checks on each EqualizerBandState. |
| break; |
| case fuchsia_hardware_audio_signalprocessing::ElementType::kDynamics: |
| type_specific_matches_element_type = |
| (element_state.type_specific().has_value() && |
| element_state.type_specific()->Which() == |
| fuchsia_hardware_audio_signalprocessing::TypeSpecificElementState::Tag::kDynamics && |
| element_state.type_specific()->dynamics().has_value() && |
| element_state.type_specific()->dynamics()->band_states().has_value() && |
| !element_state.type_specific()->dynamics()->band_states()->empty()); |
| |
| // Need additional dynamics-specific checks on each DynamicsBandState. |
| break; |
| case fuchsia_hardware_audio_signalprocessing::ElementType::kEndpoint: |
| type_specific_matches_element_type = |
| (element_state.type_specific().has_value() && |
| element_state.type_specific()->Which() == |
| fuchsia_hardware_audio_signalprocessing::TypeSpecificElementState::Tag::kEndpoint && |
| element_state.type_specific()->endpoint().has_value() && |
| element_state.type_specific()->endpoint()->plug_state().has_value() && |
| element_state.type_specific()->endpoint()->plug_state()->plugged().has_value() && |
| element_state.type_specific()->endpoint()->plug_state()->plug_state_time().has_value() && |
| (element_state.type_specific()->endpoint()->plug_state()->plugged() || |
| *element.type_specific()->endpoint()->plug_detect_capabilities() == |
| fuchsia_hardware_audio_signalprocessing::PlugDetectCapabilities::kCanAsyncNotify)); |
| break; |
| default: |
| type_specific_matches_element_type = true; |
| break; |
| } |
| |
| if (!type_specific_matches_element_type) { |
| FX_LOGS(WARNING) |
| << "WatchElementState: type_specific is missing or does not match element_type " |
| << element.type(); |
| return false; |
| } |
| |
| if (element_state.enabled().has_value()) { |
| if (!element.can_disable().value_or(false) && !element_state.enabled().value_or(true)) { |
| FX_LOGS(WARNING) |
| << "WatchElementState: element_state is disabled, but element.can_disable is false"; |
| return false; |
| } |
| } |
| if (element_state.latency().has_value()) { |
| if (element_state.latency()->Which() == |
| fuchsia_hardware_audio_signalprocessing::Latency::Tag::kLatencyTime && |
| element_state.latency()->latency_time().value() < 0) { |
| FX_LOGS(WARNING) << "WatchElementState: latency, if a duration, must not be negative"; |
| } |
| } |
| |
| if (element_state.vendor_specific_data().has_value()) { |
| if (element_state.vendor_specific_data()->empty()) { |
| FX_LOGS(WARNING) << "WatchElementState: vendor_specific_data, if present, must not be empty"; |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool ValidateElement(const fuchsia_hardware_audio_signalprocessing::Element& element) { |
| LogElement(element); |
| |
| if (!element.id().has_value()) { |
| FX_LOGS(WARNING) << "SignalProcessing.element.id is missing"; |
| return false; |
| } |
| if (!element.type().has_value()) { |
| FX_LOGS(WARNING) << "SignalProcessing.element.type is missing"; |
| return false; |
| } |
| if (*element.type() == fuchsia_hardware_audio_signalprocessing::ElementType::kEndpoint && |
| !element.type_specific().has_value()) { |
| FX_LOGS(WARNING) << "SignalProcessing.element.type is ENDPOINT but type_specific is missing"; |
| return false; |
| } |
| bool mismatch = false; |
| if (element.type_specific().has_value()) { |
| const auto type = *element.type(); |
| switch (element.type_specific()->Which()) { |
| case fuchsia_hardware_audio_signalprocessing::TypeSpecificElement::Tag::kVendorSpecific: |
| mismatch = (type != fuchsia_hardware_audio_signalprocessing::ElementType::kVendorSpecific); |
| break; |
| case fuchsia_hardware_audio_signalprocessing::TypeSpecificElement::Tag::kGain: |
| mismatch = (type != fuchsia_hardware_audio_signalprocessing::ElementType::kGain); |
| break; |
| case fuchsia_hardware_audio_signalprocessing::TypeSpecificElement::Tag::kEqualizer: |
| mismatch = (type != fuchsia_hardware_audio_signalprocessing::ElementType::kEqualizer); |
| break; |
| case fuchsia_hardware_audio_signalprocessing::TypeSpecificElement::Tag::kDynamics: |
| mismatch = (type != fuchsia_hardware_audio_signalprocessing::ElementType::kDynamics); |
| break; |
| case fuchsia_hardware_audio_signalprocessing::TypeSpecificElement::Tag::kEndpoint: |
| mismatch = (type != fuchsia_hardware_audio_signalprocessing::ElementType::kEndpoint); |
| break; |
| default: |
| FX_LOGS(WARNING) << "Unknown element.type_specific union found"; |
| return false; |
| } |
| if (mismatch) { |
| FX_LOGS(WARNING) << "element(" << *element.id() << "): type " << type |
| << " does not match type_specific union"; |
| return false; |
| } |
| } |
| if (element.description().has_value() && element.description()->empty()) { |
| FX_LOGS(WARNING) << "SignalProcessing.element.description cannot be empty, if present"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool ValidateElements( |
| const std::vector<fuchsia_hardware_audio_signalprocessing::Element>& elements) { |
| LogElements(elements); |
| |
| if (elements.empty()) { |
| FX_LOGS(WARNING) << "Reported SignalProcessing.elements[] is empty"; |
| return false; |
| } |
| |
| for (const auto& element : elements) { |
| if (auto status = ValidateElement(element); !status) { |
| return status; |
| } |
| } |
| |
| return (!MapElements(elements).empty()); |
| } |
| |
| bool ValidateTopology(const fuchsia_hardware_audio_signalprocessing::Topology& topology, |
| const std::unordered_map<ElementId, ElementRecord>& element_map) { |
| LogTopology(topology); |
| if (!topology.id().has_value()) { |
| FX_LOGS(WARNING) << "Reported SignalProcessing.topology.id is missing"; |
| return false; |
| } |
| if (!topology.processing_elements_edge_pairs().has_value()) { |
| FX_LOGS(WARNING) |
| << "Reported SignalProcessing.topology.processing_elements_edge_pairs is missing"; |
| return false; |
| } |
| if (topology.processing_elements_edge_pairs()->empty()) { |
| FX_LOGS(WARNING) |
| << "Reported SignalProcessing.topology.processing_elements_edge_pairs[] is empty"; |
| return false; |
| } |
| |
| std::unordered_set<ElementId> source_elements, destination_elements; |
| for (const auto& edge_pair : *topology.processing_elements_edge_pairs()) { |
| // Check that all the mentioned element_ids are contained in the elements set. |
| if (element_map.find(edge_pair.processing_element_id_from()) == element_map.end()) { |
| FX_LOGS(WARNING) << "Element_id_from " << edge_pair.processing_element_id_from() |
| << " not found in element list"; |
| return false; |
| } |
| if (element_map.find(edge_pair.processing_element_id_to()) == element_map.end()) { |
| FX_LOGS(WARNING) << "Element_id_to " << edge_pair.processing_element_id_to() |
| << " not found in element list"; |
| return false; |
| } |
| // Check that no EdgePair is self-referential. |
| if (edge_pair.processing_element_id_from() == edge_pair.processing_element_id_to()) { |
| FX_LOGS(WARNING) << "Edge_pair connects element_id " << edge_pair.processing_element_id_to() |
| << " to itself"; |
| return false; |
| } |
| |
| source_elements.insert(edge_pair.processing_element_id_from()); |
| destination_elements.insert(edge_pair.processing_element_id_to()); |
| } |
| |
| // Check that only ENDPOINTs are terminal |
| for (auto& [id, element_record] : element_map) { |
| if (source_elements.find(id) != source_elements.end() && |
| destination_elements.find(id) == destination_elements.end()) { |
| if (*element_record.element.type() != |
| fuchsia_hardware_audio_signalprocessing::ElementType::kEndpoint) { |
| FX_LOGS(WARNING) << "Element " << id << " has no incoming edges but is not an Endpoint! Is " |
| << *element_record.element.type(); |
| return false; |
| } |
| } |
| if (source_elements.find(id) == source_elements.end() && |
| destination_elements.find(id) != destination_elements.end()) { |
| if (*element_record.element.type() != |
| fuchsia_hardware_audio_signalprocessing::ElementType::kEndpoint) { |
| FX_LOGS(WARNING) << "Element " << id << " has no outgoing edges but is not an Endpoint! Is " |
| << *element_record.element.type(); |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| bool ValidateTopologies( |
| const std::vector<fuchsia_hardware_audio_signalprocessing::Topology>& topologies, |
| const std::unordered_map<ElementId, ElementRecord>& element_map) { |
| LogTopologies(topologies); |
| |
| if (topologies.empty()) { |
| FX_LOGS(WARNING) << "Reported SignalProcessing.topologies[] is empty"; |
| return false; |
| } |
| if (element_map.empty()) { |
| FX_LOGS(WARNING) << "Reported SignalProcessing.elements[] is empty"; |
| return false; |
| } |
| |
| // Check each topology |
| if (MapTopologies(topologies).empty()) { |
| return false; |
| } |
| |
| std::unordered_set<ElementId> elements_remaining; |
| for (const auto& element_entry_pair : element_map) { |
| elements_remaining.insert(element_entry_pair.first); |
| } |
| |
| for (auto& topology : topologies) { |
| if (!ValidateTopology(topology, element_map)) { |
| return false; |
| } |
| for (const auto& edge_pair : *topology.processing_elements_edge_pairs()) { |
| elements_remaining.erase(edge_pair.processing_element_id_from()); |
| elements_remaining.erase(edge_pair.processing_element_id_to()); |
| } |
| } |
| if (!elements_remaining.empty()) { |
| FX_LOGS(WARNING) << "topologies did not cover all elements. Example: element_id " |
| << *elements_remaining.begin(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace media_audio |