blob: 20f9338b970a32774983562d9d7a688363d46d8e [file] [log] [blame]
// Copyright 2018 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 <fbl/algorithm.h>
#include <limits>
#include <math.h>
#include <utility>
#include "debug-logging.h"
#include "usb-audio-units.h"
namespace audio {
namespace usb {
// a small internal helper methods which handles a bunch of ugly casting for us.
template <typename T, typename U>
static inline const T* offset_ptr(const U* p, size_t offset) {
return ((offset + sizeof(T)) <= p->bLength)
? reinterpret_cast<const T*>(reinterpret_cast<uintptr_t>(p) + offset)
: nullptr;
}
const char* AudioUnit::type_name() const {
switch (type()) {
case Type::InputTerminal: return "InputTerminal";
case Type::OutputTerminal: return "OutputTerminal";
case Type::MixerUnit: return "MixerUnit";
case Type::SelectorUnit: return "SelectorUnit";
case Type::FeatureUnit: return "FeatureUnit";
case Type::ProcessingUnit: return "ProcessingUnit";
case Type::ExtensionUnit: return "ExtensionUnit";
default: return "<Unknown>";
}
}
fbl::RefPtr<AudioUnit> AudioUnit::Create(const DescriptorListMemory::Iterator& iter, uint8_t iid) {
auto hdr = iter.hdr_as<usb_audio_desc_header>();
// This should already have been verified by the code calling us.
ZX_DEBUG_ASSERT(hdr != nullptr);
switch (hdr->bDescriptorSubtype) {
case USB_AUDIO_AC_INPUT_TERMINAL: return InputTerminal::Create(iter, iid);
case USB_AUDIO_AC_OUTPUT_TERMINAL: return OutputTerminal::Create(iter, iid);
case USB_AUDIO_AC_MIXER_UNIT: return MixerUnit::Create(iter, iid);
case USB_AUDIO_AC_SELECTOR_UNIT: return SelectorUnit::Create(iter, iid);
case USB_AUDIO_AC_FEATURE_UNIT: return FeatureUnit::Create(iter, iid);
case USB_AUDIO_AC_PROCESSING_UNIT: return ProcessingUnit::Create(iter, iid);
case USB_AUDIO_AC_EXTENSION_UNIT: return ExtensionUnit::Create(iter, iid);
default:
GLOBAL_LOG(WARN, "Unrecognized audio control descriptor (type %u) @ offset %zu\n",
hdr->bDescriptorSubtype, iter.offset());
return nullptr;
}
}
zx_status_t AudioUnit::CtrlReq(const usb_protocol_t& proto,
uint8_t code, uint16_t val, uint16_t len, void* data) {
if (!len || (data == nullptr)) {
return ZX_ERR_INVALID_ARGS;
}
// For audio class specific control codes, get control codes all have their MSB set.
uint8_t req_type = (code & 0x80)
? USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE
: USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE;
// TODO(johngro) : See about fixing the use of const in the C API for the
// USB bus protocol. There is no good reason why a usb_protocol structure
// should need to be mutable when performing operations such as usb_control.
auto proto_ptr = const_cast<usb_protocol_t*>(&proto);
// TODO(johngro) : Do better than this if we can.
//
// None of these control transactions should every take any
// significant amount of time, and if the turn out to do so, then we really
// need to find a way to use the USB bus driver in an asynchronous fashion.
// Even 500 mSec is just *way* too long to ever block a driver thread, for
// pretty much any reason. Right now, this timeout is here only for safety
// reasons; it would be better to timeout after a half of a second then to
// block the entire USB device forever.
//
// It is tempting to simply kill the driver/process if we ever timeout on
// one of these operations, but at time this code was written, that would
// kill the entire USB bus driver. So, for now, we eat the timeout and rely
// on the code above us taking some action to shut this device down.
constexpr uint64_t kRelativeTimeout = ZX_MSEC(500);
size_t done = 0;
zx_status_t status;
if ((req_type & USB_DIR_MASK) == USB_DIR_OUT) {
status = usb_control_out(proto_ptr, req_type, code, val, index(),
zx_deadline_after(kRelativeTimeout), data, len);
done = len;
} else {
status = usb_control_in(proto_ptr, req_type, code, val, index(),
zx_deadline_after(kRelativeTimeout), data, len, &done);
}
if ((status == ZX_OK) && (done != len)) {
status = ZX_ERR_BUFFER_TOO_SMALL;
}
if (status != ZX_OK) {
GLOBAL_LOG(WARN,
"WARNING: Audio control request failed! Unit (%s:id %u), "
"code 0x%02x val 0x%04hx, ndx 0x%04x [bytes expected %u, got %zu] (status %d)\n",
type_name(), id(), code, val, index(), len, done, status);
}
return status;
}
fbl::RefPtr<InputTerminal> InputTerminal::Create(const DescriptorListMemory::Iterator& iter,
uint8_t iid) {
auto hdr = iter.hdr_as<usb_audio_ac_input_terminal_desc>();
if (hdr == nullptr) {
GLOBAL_LOG(WARN, "InputTerminal header appears invalid @ offset %zu\n", iter.offset());
return nullptr;
}
// TODO(johngro): additional sanity checking and pre-processing goes here.
fbl::AllocChecker ac;
auto ret = fbl::AdoptRef(new (&ac) InputTerminal(iter.desc_list(), hdr, iid));
return ac.check() ? ret : nullptr;
}
fbl::RefPtr<OutputTerminal> OutputTerminal::Create(const DescriptorListMemory::Iterator& iter,
uint8_t iid) {
auto hdr = iter.hdr_as<usb_audio_ac_output_terminal_desc>();
if (hdr == nullptr) {
GLOBAL_LOG(WARN, "OutputTerminal header appears invalid @ offset %zu\n", iter.offset());
return nullptr;
}
// TODO(johngro): additional sanity checking and pre-processing goes here.
fbl::AllocChecker ac;
auto ret = fbl::AdoptRef(new (&ac) OutputTerminal(iter.desc_list(), hdr, iid));
return ac.check() ? ret : nullptr;
}
fbl::RefPtr<MixerUnit> MixerUnit::Create(const DescriptorListMemory::Iterator& iter, uint8_t iid) {
// Find the size of each of the inlined variable length arrays in this
// structure, finding the locations of the constant headers in the process.
// If anything does not look right, complain and move on.
auto hdr0 = iter.hdr_as<usb_audio_ac_mixer_unit_desc_0>();
if (hdr0 != nullptr) {
size_t off = sizeof(*hdr0) + hdr0->bNrInPins;
auto hdr1 = offset_ptr<usb_audio_ac_mixer_unit_desc_1>(hdr0, off);
if (hdr1 != nullptr) {
// Determining the size of bmControls is a bit of a pain. To do so,
// we need to know 'n', which is the sum the number of channels
// across all of the input pins, and 'm' (which should be
// hdr1->bNrChannels). At this stage of parsing our unit/terminal
// graph, we may not have access to all of the sources which might
// feed into the calculation of 'n'. Because of this, for now, just
// assume that the size of bmControls (in bytes) is equal to the
// space remaining in the descriptor, demanding that this be at
// least equal to a single byte (if it was zero, it means that we
// either have no input or no output channels, neither of which
// makes sense).
if (sizeof(usb_audio_ac_mixer_unit_desc_2) < hdr0->bLength) {
size_t off2 = hdr0->bLength - sizeof(usb_audio_ac_mixer_unit_desc_2);
if (off2 > off) {
auto hdr2 = offset_ptr<usb_audio_ac_mixer_unit_desc_2>(hdr0, off2);
ZX_DEBUG_ASSERT(hdr2 != nullptr);
// TODO(johngro): additional sanity checking and pre-processing goes here.
fbl::AllocChecker ac;
auto ret = fbl::AdoptRef(
new (&ac) MixerUnit(iter.desc_list(), hdr0, hdr1, hdr2, iid));
return ac.check() ? ret : nullptr;
}
}
}
}
GLOBAL_LOG(WARN, "MixerUnit header appears invalid @ offset %zu\n", iter.offset());
return nullptr;
}
fbl::RefPtr<SelectorUnit> SelectorUnit::Create(const DescriptorListMemory::Iterator& iter,
uint8_t iid) {
// Find the size of each of the inlined variable length arrays in this
// structure, finding the locations of the constant headers in the process.
// If anything does not look right, complain and move on.
auto hdr0 = iter.hdr_as<usb_audio_ac_selector_unit_desc_0>();
if (hdr0 != nullptr) {
size_t off = sizeof(*hdr0) + hdr0->bNrInPins;
auto hdr1 = offset_ptr<usb_audio_ac_selector_unit_desc_1>(hdr0, off);
if (hdr1 != nullptr) {
// TODO(johngro): additional sanity checking and pre-processing goes here.
fbl::AllocChecker ac;
auto ret = fbl::AdoptRef(new (&ac) SelectorUnit(iter.desc_list(), hdr0, hdr1, iid));
return ac.check() ? ret : nullptr;
}
}
GLOBAL_LOG(WARN, "SelectorUnit header appears invalid @ offset %zu\n", iter.offset());
return nullptr;
}
zx_status_t SelectorUnit::Select(const usb_protocol_t& proto, uint8_t upstream_id) {
// Section 5.2.2.3.3. defines the selector index as being 1s indexed, so
// zero is an easy to use "invalid" value.
uint8_t ndx = 0;
// Find the appropriate index or return an error trying.
uint32_t cnt = source_count();
for (uint32_t i = 0; i < cnt; ++i) {
if (upstream_id == source_id(i)) {
ndx = static_cast<uint8_t>(i + 1);
break;
}
}
if (!ndx) {
return ZX_ERR_INVALID_ARGS;
}
// Now go ahead and set the value;
return CtrlReq(proto, USB_AUDIO_SET_CUR, 0, &ndx);
}
fbl::RefPtr<FeatureUnit> FeatureUnit::Create(const DescriptorListMemory::Iterator& iter,
uint8_t iid) {
// Find the size of each of the inlined variable length arrays in this
// structure, finding the locations of the constant headers in the process.
// If anything does not look right, complain and move on.
auto hdr0 = iter.hdr_as<usb_audio_ac_feature_unit_desc_0>();
if (hdr0 != nullptr) {
// The exact expected size of the Controls bitmap depends on the number
// of channels feeding this feature unit. This information is not
// contained in the feature unit itself, but instead exists upstream of
// the unit in first unit/terminal which contains a channel cluster
// element. At this point in parsing, we have not discovered all of the
// units present in the audio control interface yet, so we cannot trace
// upstream to sanity check the size of this field.
//
// For now, we perform the most basic check we can by assuming that the
// size of the Controls bitmap must be...
//
// 1) Non-zero, and...
// 2) Divisible by bControlSize, which must also be non-zero.
//
// In the future, more stringent checks can be applied during Probe.
constexpr size_t kHdrOverhead = sizeof(*hdr0) + sizeof(usb_audio_ac_feature_unit_desc_1);
size_t ctrl_array_bytes = hdr0->bLength - kHdrOverhead;
if ((kHdrOverhead < hdr0->bLength) &&
(hdr0->bControlSize > 0) &&
(!(ctrl_array_bytes % hdr0->bControlSize))) {
// Allocate memory for our Features capability array.
fbl::AllocChecker ac;
size_t feat_len = ctrl_array_bytes / hdr0->bControlSize;
auto feat_mem = fbl::unique_ptr<Features[]>(new (&ac) Features[feat_len]);
if (ac.check()) {
// We just made sure that this fits, there should be no way for us
// to have run out of data.
size_t off = hdr0->bLength - sizeof(usb_audio_ac_feature_unit_desc_1);
auto hdr1 = offset_ptr<usb_audio_ac_feature_unit_desc_1>(hdr0, off);
ZX_DEBUG_ASSERT(hdr1 != nullptr);
auto ret = fbl::AdoptRef(new (&ac) FeatureUnit(iter.desc_list(),
hdr0, hdr1,
std::move(feat_mem), feat_len,
iid));
if (ac.check()) {
return ret;
}
}
GLOBAL_LOG(WARN, "Out of memory attempting to allocate FeatureUnit @ offset %zu\n",
iter.offset());
return nullptr;
}
}
GLOBAL_LOG(WARN, "FeatureUnit header appears invalid @ offset %zu\n", iter.offset());
return nullptr;
}
zx_status_t FeatureUnit::Probe(const usb_protocol_t& proto) {
zx_status_t res;
// Start by going over our channel feature bitmap and extracting the actual
// feature bits for each channel. Right now, we demand that the size of
// each entry be (at most) a 32 bit integer. The USB Audio 1.0 Spec only
// defines bits up to bit 9, so we really only understand how to handle up
// to there. If we cannot fit each of the bitmap entries in a 32-bit
// integer, then the USB audio spec has come a long way and someone should
// come back here and update this driver.
ZX_DEBUG_ASSERT(feature_desc()->bControlSize != 0); // Create should have checked this already
if (feature_desc()->bControlSize > sizeof(uint32_t)) {
GLOBAL_LOG(WARN, "FeatureUnit id %u has unsupported bControlSize > %zu (%u)\n",
id(), sizeof(uint32_t), feature_desc()->bControlSize);
return ZX_ERR_NOT_SUPPORTED;
}
for (size_t i = 0; i < features_.size(); ++i) {
auto& f = features_[i];
f.supported_ = 0;
for (uint8_t j = 0; j <feature_desc()->bControlSize; ++j) {
uint32_t bits = feature_desc()->bmaControls[(i * feature_desc()->bControlSize) + j];
f.supported_ |= bits << (8 * j);
}
}
// Now, go over our array of features and compute both the union and the
// intersection of the features for all of the individual channels.
uint32_t ch_feat_union = 0;
uint32_t ch_feat_intersection = 0;
if (features_.size() > 1) {
ch_feat_union = features_[1].supported_;
ch_feat_intersection = features_[1].supported_;
for (size_t i = 2; i < features_.size(); ++i) {
ch_feat_union |= features_[i].supported_;
ch_feat_intersection &= features_[i].supported_;
}
}
// Next check for a set of uniformity requirements. In particular, there
// are three types of controls (mute, AGC, and volume/gain) that we want to
// enforce these guarantees for. Specifically,
//
// 1) We can handle these controls at the master level, or the individual
// channel level, but we don't really know what to do if the controls
// exist at both levels.
// 2) If we are controlling these things at the individual control level, we
// are doing so in a way which mimics a master control knob only. So, if
// we have these controls at the per-channel level, it is important that
// they be they be identical for each of the individual channels.
constexpr uint32_t kUniformControls = USB_AUDIO_FU_BMA_MUTE
| USB_AUDIO_FU_BMA_VOLUME
| USB_AUDIO_FU_BMA_AUTOMATIC_GAIN;
ZX_DEBUG_ASSERT(features_.size() > 0); // Create should have checked this already
if (((features_[0].supported_ & ch_feat_union & kUniformControls) != 0) || // Check #1
((ch_feat_union ^ ch_feat_intersection) & kUniformControls)) { // Check #2
GLOBAL_LOG(WARN,
"FeatureUnit id %u has unsupported non-uniform gain controls. "
"Master 0x%08x, Channel Union 0x%08x, Channel Intersection 0x%08x.\n",
id(), features_[0].supported_, ch_feat_union, ch_feat_intersection);
return ZX_ERR_NOT_SUPPORTED;
}
// Stash bitmaps of controls we care about for later.
master_feat_ = features_[0].supported_ & kUniformControls;
ch_feat_ = ch_feat_intersection & kUniformControls;
// If this feature unit has volume control, fetch and sanity check the
// min/max/res of all of the channels.
if (has_vol()) {
// Go over each of the volume controls and cache the min/max/res values.
for (size_t i = 0; i < features_.size(); ++i) {
auto& f = features_[i];
if (!f.has_vol()) {
continue;
}
uint8_t ch = static_cast<uint8_t>(i);
res = FeatCtrlReq(proto, USB_AUDIO_GET_MIN, USB_AUDIO_VOLUME_CONTROL, ch, &f.vol_min_);
if (res != ZX_OK) {
return res;
}
res = FeatCtrlReq(proto, USB_AUDIO_GET_MAX, USB_AUDIO_VOLUME_CONTROL, ch, &f.vol_max_);
if (res != ZX_OK) {
return res;
}
res = FeatCtrlReq(proto, USB_AUDIO_GET_RES, USB_AUDIO_VOLUME_CONTROL, ch, &f.vol_res_);
if (res != ZX_OK) {
return res;
}
}
// If volume control is done at the per-channel level, make sure that all of
// the channels support the same range. Otherwise, our volume control range
// is equal to the master channel's range.
if (features_[0].has_vol()) {
vol_min_ = features_[0].vol_min_;
vol_max_ = features_[0].vol_max_;
vol_res_ = features_[0].vol_res_;
} else {
vol_min_ = features_[1].vol_min_;
vol_max_ = features_[1].vol_max_;
vol_res_ = features_[1].vol_res_;
for (size_t i = 2; i < features_.size(); ++i) {
if ((vol_min_ != features_[i].vol_min_) ||
(vol_max_ != features_[i].vol_max_) ||
(vol_res_ != features_[i].vol_res_)) {
GLOBAL_LOG(WARN,
"FeatureUnit id %u has unsupported non-uniform gain controls. "
"Channel %zu's gain range [%hd, %hd, %hd] does not match Channel 1's "
"range [%hd, %hd, %hd]\n",
id(), i,
vol_min_, vol_max_, vol_res_,
features_[i].vol_min_, features_[i].vol_max_, features_[i].vol_res_);
return ZX_ERR_NOT_SUPPORTED;
}
}
}
if (vol_min_ > vol_max_) {
GLOBAL_LOG(WARN, "FeatureUnit id %u has invalid volume range [%hd, %hd]\n",
id(), vol_min_, vol_max_);
return ZX_ERR_NOT_SUPPORTED;
}
if (!vol_res_) {
GLOBAL_LOG(WARN, "FeatureUnit id %u has invalid volume res %hd\n", id(), vol_res_);
return ZX_ERR_NOT_SUPPORTED;
}
// Fetch the current volume setting from the appropriate source, then
// make certain that all channels are set to the same if there is no
// master control knob.
bool master_control = (master_feat_ & USB_AUDIO_FU_BMA_VOLUME);
uint8_t ch = master_control ? 0 : 1;
res = FeatCtrlReq(proto, USB_AUDIO_GET_CUR, USB_AUDIO_VOLUME_CONTROL, ch, &vol_cur_);
if (res != ZX_OK) {
return res;
}
if (!master_control) {
SetFeature(proto, USB_AUDIO_VOLUME_CONTROL, vol_cur_);
}
}
// If we have mute controls, figure out the current setting.
if (has_mute()) {
res = FeatCtrlReq(proto, USB_AUDIO_GET_CUR, USB_AUDIO_MUTE_CONTROL, 0, &mute_cur_);
if (res != ZX_OK) {
return res;
}
}
// If we have agc controls, figure out the current setting.
if (has_agc()) {
res = FeatCtrlReq(proto, USB_AUDIO_GET_CUR, USB_AUDIO_AUTOMATIC_GAIN_CONTROL, 0, &agc_cur_);
if (res != ZX_OK) {
return res;
}
}
// Dump some diags info if TRACE level logging is enabled.
if (has_vol()) {
GLOBAL_LOG(TRACE,
"FeatureUnit id %u: can%s mute, can%s AGC, gain [%.3f, %.3f: step %.3f] dB\n",
id(),
has_mute() ? "" : "not",
has_agc() ? "" : "not",
vol_min_db(), vol_max_db(), vol_res_db());
} else {
GLOBAL_LOG(TRACE, "FeatureUnit id %u: can%s mute, can%s AGC, and has fixed gain\n",
id(), has_mute() ? "" : "not", has_agc() ? "" : "not");
}
// All done! Declare success and get out.
return ZX_OK;
};
float FeatureUnit::SetVol(const usb_protocol_t& proto, float db) {
// If we have no volume control, then our gain is fixed at 0.0 dB no matter
// what the user asks for.
if (!has_vol()) {
return 0.0;
}
// Convert to our target value. Start by converting to ticks.
float ticks_float = db / kDbPerTick;
// Now snap to the closest allowed tick based on our resolution.
ticks_float = roundf(ticks_float / vol_res_) * vol_res_;
// Now clamp to the acceptable min/max range and convert to integer ticks.
vol_cur_ = static_cast<int16_t>(fbl::clamp<float>(ticks_float, vol_min_, vol_max_));
// Finally apply the setting. If we have no explicit mute control, and we
// are currently supposed to be muted, skip this step. We are using the
// volume control to simulate mute to the best of our abilities; we will
// restore vol_cur_ when the unit finally becomes un-muted.
if (!(mute_cur_ && !has_mute())) {
SetFeature(proto, USB_AUDIO_VOLUME_CONTROL, vol_cur_);
}
return vol_cur_ * kDbPerTick;
}
bool FeatureUnit::SetMute(const usb_protocol_t& proto, bool mute) {
mute_cur_ = mute;
// If we have an explicit mute control, use that. Otherwise, do the best we
// can using the volume control (if present).
if (has_mute()) {
SetFeature(proto, USB_AUDIO_MUTE_CONTROL, mute_cur_);
} else {
// Section 5.2.2.4.3.2 of the USB Audio 1.0 spec defines int16::min as
// -inf dB for the purpose of setting gain.
int16_t tgt = mute ? std::numeric_limits<int16_t>::min() : vol_cur_;
SetFeature(proto, USB_AUDIO_VOLUME_CONTROL, tgt);
}
return !!mute_cur_;
}
bool FeatureUnit::SetAgc(const usb_protocol_t& proto, bool agc) {
if (has_agc()) {
agc_cur_ = agc;
SetFeature(proto, USB_AUDIO_AUTOMATIC_GAIN_CONTROL, static_cast<uint8_t>(agc));
}
return !!agc_cur_;
}
fbl::RefPtr<ProcessingUnit> ProcessingUnit::Create(const DescriptorListMemory::Iterator& iter,
uint8_t iid) {
// Find the size of each of the inlined variable length arrays in this
// structure, finding the locations of the constant headers in the process.
// If anything does not look right, complain and move on.
auto hdr0 = iter.hdr_as<usb_audio_ac_processing_unit_desc_0>();
if (hdr0 != nullptr) {
size_t off = sizeof(*hdr0) + hdr0->bNrInPins;
auto hdr1 = offset_ptr<usb_audio_ac_processing_unit_desc_1>(hdr0, off);
if (hdr1 != nullptr) {
off += sizeof(*hdr1) + hdr1->bControlSize;
auto hdr2 = offset_ptr<usb_audio_ac_processing_unit_desc_2>(hdr0, off);
// TODO(johngro): additional sanity checking and pre-processing goes here.
//
// Note: Processing units actually come in their own pre-defined
// sub-flavors (determined by hdr0->wProcessType). Instead of
// lumping them all together into one ProcessingUnit class, we
// should probably take the time to break them down into the various
// sub-flavors, at which point in time, the big validation switch
// statement would go somewhere in here.
//
// For now, however, we do not expect to have any need to control
// processing units. If we ever encounter one, we really only want
// to understand the size of the baSourceID array so that we can
// successfully walk the graph when attempting to build input/output
// stream paths.
fbl::AllocChecker ac;
auto ret = fbl::AdoptRef(
new (&ac) ProcessingUnit(iter.desc_list(), hdr0, hdr1, hdr2, iid));
return ac.check() ? ret : nullptr;
}
}
GLOBAL_LOG(WARN, "ProcessingUnit header appears invalid @ offset %zu\n", iter.offset());
return nullptr;
}
fbl::RefPtr<ExtensionUnit> ExtensionUnit::Create(const DescriptorListMemory::Iterator& iter,
uint8_t iid) {
// Find the size of each of the inlined variable length arrays in this
// structure, finding the locations of the constant headers in the process.
// If anything does not look right, complain and move on.
auto hdr0 = iter.hdr_as<usb_audio_ac_extension_unit_desc_0>();
if (hdr0 != nullptr) {
size_t off = sizeof(*hdr0) + hdr0->bNrInPins;
auto hdr1 = offset_ptr<usb_audio_ac_extension_unit_desc_1>(hdr0, off);
if (hdr1 != nullptr) {
off += sizeof(*hdr1) + hdr1->bControlSize;
auto hdr2 = offset_ptr<usb_audio_ac_extension_unit_desc_2>(hdr0, off);
// TODO(johngro): additional sanity checking and pre-processing goes here.
fbl::AllocChecker ac;
auto ret = fbl::AdoptRef(
new (&ac) ExtensionUnit(iter.desc_list(), hdr0, hdr1, hdr2, iid));
return ac.check() ? ret : nullptr;
}
}
GLOBAL_LOG(WARN, "ExtensionUnit header appears invalid @ offset %zu\n", iter.offset());
return nullptr;
}
} // namespace usb
} // namespace audio