blob: effd9f134fee8480c223a0bb65e2fb3ece8ef3cb [file] [log] [blame]
// Copyright 2017 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 <audio-proto-utils/format-utils.h>
#include <ddk/device.h>
#include <digest/digest.h>
#include <dispatcher-pool/dispatcher-thread-pool.h>
#include <fbl/algorithm.h>
#include <fbl/auto_call.h>
#include <lib/zx/vmar.h>
#include <limits>
#include <string.h>
#include <usb/usb-request.h>
#include <utility>
#include <zircon/hw/usb/audio.h>
#include <zircon/process.h>
#include <zircon/time.h>
#include <zircon/types.h>
#include "usb-audio.h"
#include "usb-audio-device.h"
#include "usb-audio-stream.h"
#include "usb-audio-stream-interface.h"
namespace audio {
namespace usb {
static constexpr uint32_t MAX_OUTSTANDING_REQ = 8;
static constexpr uint32_t ExtractSampleRate(const usb_audio_as_samp_freq& sr) {
return static_cast<uint32_t>(sr.freq[0])
| (static_cast<uint32_t>(sr.freq[1]) << 8)
| (static_cast<uint32_t>(sr.freq[2]) << 16);
}
UsbAudioStream::UsbAudioStream(UsbAudioDevice* parent,
fbl::unique_ptr<UsbAudioStreamInterface> ifc,
fbl::RefPtr<dispatcher::ExecutionDomain> default_domain)
: UsbAudioStreamBase(parent->zxdev()),
AudioStreamProtocol(ifc->direction() == Direction::Input),
parent_(*parent),
ifc_(std::move(ifc)),
default_domain_(std::move(default_domain)),
create_time_(zx_clock_get_monotonic()) {
snprintf(log_prefix_, sizeof(log_prefix_),
"UsbAud %04hx:%04hx %s-%03d",
parent_.vid(),
parent_.pid(),
is_input() ? "input" : "output",
ifc_->term_link());
}
UsbAudioStream::~UsbAudioStream() {
// We destructing. All of our requests should be sitting in the free list.
ZX_DEBUG_ASSERT(allocated_req_cnt_ == free_req_cnt_);
while (!list_is_empty(&free_req_)) {
usb_request_release(usb_req_list_remove_head(&free_req_, parent_.parent_req_size()));
}
}
fbl::RefPtr<UsbAudioStream> UsbAudioStream::Create(UsbAudioDevice* parent,
fbl::unique_ptr<UsbAudioStreamInterface> ifc) {
ZX_DEBUG_ASSERT(parent != nullptr);
ZX_DEBUG_ASSERT(ifc != nullptr);
auto domain = dispatcher::ExecutionDomain::Create();
if (domain == nullptr) {
LOG_EX(ERROR, *parent,
"Failed to create execution domain while trying to create UsbAudioStream\n");
return nullptr;
}
fbl::AllocChecker ac;
auto stream = fbl::AdoptRef(
new (&ac) UsbAudioStream(parent, std::move(ifc), std::move(domain)));
if (!ac.check()) {
LOG_EX(ERROR, *parent,
"Out of memory while attempting to allocate UsbAudioStream\n");
return nullptr;
}
stream->ComputePersistentUniqueId();
return stream;
}
zx_status_t UsbAudioStream::Bind() {
// TODO(johngro): Do this differently when we have the ability to queue io
// transactions to a USB isochronous endpoint and can have the bus driver
// DMA directly from the ring buffer we have set up with our user.
{
fbl::AutoLock req_lock(&req_lock_);
list_initialize(&free_req_);
free_req_cnt_ = 0;
allocated_req_cnt_ = 0;
uint64_t req_size = parent_.parent_req_size() + sizeof(usb_req_internal_t);
for (uint32_t i = 0; i < MAX_OUTSTANDING_REQ; ++i) {
usb_request_t* req;
zx_status_t status = usb_request_alloc(&req, ifc_->max_req_size(),
ifc_->ep_addr(), req_size);
if (status != ZX_OK) {
LOG(ERROR, "Failed to allocate usb request %u/%u (size %u): %d\n",
i + 1, MAX_OUTSTANDING_REQ, ifc_->max_req_size(), status);
return status;
}
status = usb_req_list_add_head(&free_req_, req, parent_.parent_req_size());
ZX_DEBUG_ASSERT(status == ZX_OK);
++free_req_cnt_;
++allocated_req_cnt_;
}
}
char name[64];
snprintf(name, sizeof(name), "usb-audio-%s-%03d",
is_input() ? "input" : "output", ifc_->term_link());
zx_status_t status = UsbAudioStreamBase::DdkAdd(name);
if (status == ZX_OK) {
// If bind/setup has succeeded, then the devmgr now holds a reference to us.
// Manually increase our reference count to account for this.
this->AddRef();
} else {
LOG(ERROR, "Failed to publish UsbAudioStream device node (name \"%s\", status %d)\n",
name, status);
}
return status;
}
void UsbAudioStream::RequestCompleteCallback(usb_request_t* request, void* cookie) {
ZX_DEBUG_ASSERT(cookie != nullptr);
reinterpret_cast<UsbAudioStream*>(cookie)->RequestComplete(request);
}
void UsbAudioStream::ComputePersistentUniqueId() {
// Do the best that we can to generate a persistent ID unique to this audio
// stream by blending information from a number of sources. In particular,
// consume...
//
// 1) This USB device's top level device descriptor (this contains the
// VID/PID of the device, among other things)
// 2) The contents of the descriptor list used to describe the control and
// streaming interfaces present in the device.
// 3) The manufacturer, product, and serial number string descriptors (if
// present)
// 4) The stream interface ID.
//
// The goal here is to produce something like a UUID which is as unique to a
// specific instance of a specific device as we can make it, but which
// should persist across boots even in the presence of driver updates an
// such. Even so, upper levels of code will still need to deal with the sad
// reality that some types of devices may end up looking the same between
// two different instances. If/when this becomes an issue, we may need to
// pursue other options. One choice might be to change the way devices are
// enumerated in the USB section of the device tree so that their path has
// only to do with physical topology, and has no runtime enumeration order
// dependencies. At that point in time, adding the topology into the hash
// should do the job, but would imply that the same device plugged into two
// different ports will have a different unique ID for the purposes of
// saving and restoring driver settings (as it does in some operating
// systems today).
//
uint16_t vid = parent_.desc().idVendor;
uint16_t pid = parent_.desc().idProduct;
audio_stream_unique_id_t fallback_id { .data = {
'U', 'S', 'B', ' ',
static_cast<uint8_t>(vid >> 8),
static_cast<uint8_t>(vid),
static_cast<uint8_t>(pid >> 8),
static_cast<uint8_t>(pid),
ifc_->iid()
}};
persistent_unique_id_ = fallback_id;
digest::Digest sha;
zx_status_t res = sha.Init();
if (res != ZX_OK) {
LOG(WARN, "Failed to initialize digest while computing unique ID. "
"Falling back on defaults (res %d)\n", res);
return;
}
// #1: Top level descriptor.
sha.Update(&parent_.desc(), sizeof(parent_.desc()));
// #2: The descriptor list
const auto& desc_list = parent_.desc_list();
ZX_DEBUG_ASSERT((desc_list != nullptr) && (desc_list->size() > 0));
sha.Update(desc_list->data(), desc_list->size());
// #3: The various descriptor strings which may exist.
const fbl::Array<uint8_t>* desc_strings[] = {
&parent_.mfr_name(),
&parent_.prod_name(),
&parent_.serial_num()
};
for (const auto str : desc_strings) {
if (str->size()) {
sha.Update(str->get(), str->size());
}
}
// #4: The stream interface's ID.
auto iid = ifc_->iid();
sha.Update(&iid, sizeof(iid));
// Finish the SHA and attempt to copy as much of the results to our internal
// cached representation as we can.
uint8_t digest_out[digest::Digest::kLength];
sha.Final();
res = sha.CopyTo(digest_out, sizeof(digest_out));
if (res != ZX_OK) {
LOG(WARN, "Failed to copy digest while computing unique ID. "
"Falling back on defaults (res %d)\n", res);
return;
}
constexpr size_t todo = fbl::min(sizeof(digest_out), sizeof(persistent_unique_id_.data));
if (todo < sizeof(persistent_unique_id_.data)) {
::memset(&persistent_unique_id_.data, 0, sizeof(persistent_unique_id_.data));
}
::memcpy(persistent_unique_id_.data, digest_out, todo);
}
void UsbAudioStream::ReleaseRingBufferLocked() {
if (ring_buffer_virt_ != nullptr) {
ZX_DEBUG_ASSERT(ring_buffer_size_ != 0);
zx::vmar::root_self()->unmap(reinterpret_cast<uintptr_t>(ring_buffer_virt_),
ring_buffer_size_);
ring_buffer_virt_ = nullptr;
ring_buffer_size_ = 0;
}
ring_buffer_vmo_.reset();
}
void UsbAudioStream::DdkUnbind() {
// Close all of our client event sources if we have not already.
default_domain_->Deactivate();
// Unpublish our device node.
DdkRemove();
}
void UsbAudioStream::DdkRelease() {
// Reclaim our reference from the driver framework and let it go out of
// scope. If this is our last reference (it should be), we will destruct
// immediately afterwards.
auto stream = fbl::internal::MakeRefPtrNoAdopt(this);
// Make sure that our parent is no longer holding a reference to us.
parent_.RemoveAudioStream(stream);
}
zx_status_t UsbAudioStream::DdkIoctl(uint32_t op,
const void* in_buf, size_t in_len,
void* out_buf, size_t out_len, size_t* out_actual) {
// The only IOCTL we support is get channel.
if (op != AUDIO_IOCTL_GET_CHANNEL) {
return ZX_ERR_NOT_SUPPORTED;
}
if ((out_buf == nullptr) ||
(out_actual == nullptr) ||
(out_len != sizeof(zx_handle_t))) {
return ZX_ERR_INVALID_ARGS;
}
fbl::AutoLock lock(&lock_);
// Attempt to allocate a new driver channel and bind it to us. If we don't
// already have an stream_channel_, flag this channel is the privileged
// connection (The connection which is allowed to do things like change
// formats).
bool privileged = (stream_channel_ == nullptr);
auto channel = dispatcher::Channel::Create();
if (channel == nullptr)
return ZX_ERR_NO_MEMORY;
dispatcher::Channel::ProcessHandler phandler(
[stream = fbl::WrapRefPtr(this), privileged](dispatcher::Channel* channel) -> zx_status_t {
OBTAIN_EXECUTION_DOMAIN_TOKEN(t, stream->default_domain_);
return stream->ProcessStreamChannel(channel, privileged);
});
dispatcher::Channel::ChannelClosedHandler chandler;
if (privileged) {
chandler = dispatcher::Channel::ChannelClosedHandler(
[stream = fbl::WrapRefPtr(this)](const dispatcher::Channel* channel) -> void {
OBTAIN_EXECUTION_DOMAIN_TOKEN(t, stream->default_domain_);
stream->DeactivateStreamChannel(channel);
});
}
zx::channel client_endpoint;
zx_status_t res = channel->Activate(&client_endpoint,
default_domain_,
std::move(phandler),
std::move(chandler));
if (res == ZX_OK) {
if (privileged) {
ZX_DEBUG_ASSERT(stream_channel_ == nullptr);
stream_channel_ = channel;
}
*(reinterpret_cast<zx_handle_t*>(out_buf)) = client_endpoint.release();
*out_actual = sizeof(zx_handle_t);
}
return res;
}
#define HREQ(_cmd, _payload, _handler, _allow_noack, ...) \
case _cmd: \
if (req_size != sizeof(req._payload)) { \
LOG(TRACE, "Bad " #_cmd " response length (%u != %zu)\n", \
req_size, sizeof(req._payload)); \
return ZX_ERR_INVALID_ARGS; \
} \
if (!_allow_noack && (req.hdr.cmd & AUDIO_FLAG_NO_ACK)) { \
LOG(TRACE, "NO_ACK flag not allowed for " #_cmd "\n"); \
return ZX_ERR_INVALID_ARGS; \
} \
return _handler(channel, req._payload, ##__VA_ARGS__);
zx_status_t UsbAudioStream::ProcessStreamChannel(dispatcher::Channel* channel, bool priv) {
ZX_DEBUG_ASSERT(channel != nullptr);
fbl::AutoLock lock(&lock_);
// TODO(johngro) : Factor all of this behavior around accepting channels and
// dispatching audio driver requests into some form of utility class so it
// can be shared with the IntelHDA codec implementations as well.
union {
audio_proto::CmdHdr hdr;
audio_proto::StreamGetFmtsReq get_formats;
audio_proto::StreamSetFmtReq set_format;
audio_proto::GetGainReq get_gain;
audio_proto::SetGainReq set_gain;
audio_proto::PlugDetectReq plug_detect;
audio_proto::GetUniqueIdReq get_unique_id;
audio_proto::GetStringReq get_string;
// TODO(johngro) : add more commands here
} req;
static_assert(sizeof(req) <= 256,
"Request buffer is getting to be too large to hold on the stack!");
uint32_t req_size;
zx_status_t res = channel->Read(&req, sizeof(req), &req_size);
if (res != ZX_OK)
return res;
if ((req_size < sizeof(req.hdr) ||
(req.hdr.transaction_id == AUDIO_INVALID_TRANSACTION_ID)))
return ZX_ERR_INVALID_ARGS;
// Strip the NO_ACK flag from the request before selecting the dispatch target.
auto cmd = static_cast<audio_proto::Cmd>(req.hdr.cmd & ~AUDIO_FLAG_NO_ACK);
switch (cmd) {
HREQ(AUDIO_STREAM_CMD_GET_FORMATS, get_formats, OnGetStreamFormatsLocked, false);
HREQ(AUDIO_STREAM_CMD_SET_FORMAT, set_format, OnSetStreamFormatLocked, false, priv);
HREQ(AUDIO_STREAM_CMD_GET_GAIN, get_gain, OnGetGainLocked, false);
HREQ(AUDIO_STREAM_CMD_SET_GAIN, set_gain, OnSetGainLocked, true);
HREQ(AUDIO_STREAM_CMD_PLUG_DETECT, plug_detect, OnPlugDetectLocked, true);
HREQ(AUDIO_STREAM_CMD_GET_UNIQUE_ID, get_unique_id, OnGetUniqueIdLocked, false);
HREQ(AUDIO_STREAM_CMD_GET_STRING, get_string, OnGetStringLocked, false);
default:
LOG(TRACE, "Unrecognized stream command 0x%04x\n", req.hdr.cmd);
return ZX_ERR_NOT_SUPPORTED;
}
}
zx_status_t UsbAudioStream::ProcessRingBufferChannel(dispatcher::Channel* channel) {
ZX_DEBUG_ASSERT(channel != nullptr);
fbl::AutoLock lock(&lock_);
union {
audio_proto::CmdHdr hdr;
audio_proto::RingBufGetFifoDepthReq get_fifo_depth;
audio_proto::RingBufGetBufferReq get_buffer;
audio_proto::RingBufStartReq rb_start;
audio_proto::RingBufStopReq rb_stop;
// TODO(johngro) : add more commands here
} req;
static_assert(sizeof(req) <= 256,
"Request buffer is getting to be too large to hold on the stack!");
uint32_t req_size;
zx_status_t res = channel->Read(&req, sizeof(req), &req_size);
if (res != ZX_OK)
return res;
if ((req_size < sizeof(req.hdr) ||
(req.hdr.transaction_id == AUDIO_INVALID_TRANSACTION_ID)))
return ZX_ERR_INVALID_ARGS;
// Strip the NO_ACK flag from the request before selecting the dispatch target.
auto cmd = static_cast<audio_proto::Cmd>(req.hdr.cmd & ~AUDIO_FLAG_NO_ACK);
switch (cmd) {
HREQ(AUDIO_RB_CMD_GET_FIFO_DEPTH, get_fifo_depth, OnGetFifoDepthLocked, false);
HREQ(AUDIO_RB_CMD_GET_BUFFER, get_buffer, OnGetBufferLocked, false);
HREQ(AUDIO_RB_CMD_START, rb_start, OnStartLocked, false);
HREQ(AUDIO_RB_CMD_STOP, rb_stop, OnStopLocked, false);
default:
LOG(TRACE, "Unrecognized ring buffer command 0x%04x\n", req.hdr.cmd);
return ZX_ERR_NOT_SUPPORTED;
}
return ZX_ERR_NOT_SUPPORTED;
}
#undef HREQ
zx_status_t UsbAudioStream::OnGetStreamFormatsLocked(dispatcher::Channel* channel,
const audio_proto::StreamGetFmtsReq& req) {
ZX_DEBUG_ASSERT(channel != nullptr);
audio_proto::StreamGetFmtsResp resp = { };
const auto& formats = ifc_->formats();
if (formats.size() > std::numeric_limits<uint16_t>::max()) {
LOG(ERROR, "Too many formats (%zu) to send during AUDIO_STREAM_CMD_GET_FORMATS request!\n",
formats.size());
return ZX_ERR_INTERNAL;
}
size_t formats_sent = 0;
resp.hdr = req.hdr;
resp.format_range_count = static_cast<uint16_t>(formats.size());
do {
size_t todo, payload_sz, __UNUSED to_send;
zx_status_t res;
todo = fbl::min<size_t>(formats.size() - formats_sent,
AUDIO_STREAM_CMD_GET_FORMATS_MAX_RANGES_PER_RESPONSE);
payload_sz = sizeof(resp.format_ranges[0]) * todo;
to_send = offsetof(audio_proto::StreamGetFmtsResp, format_ranges) + payload_sz;
resp.first_format_range_ndx = static_cast<uint16_t>(formats_sent);
for (uint32_t i = 0; i < todo; ++i) {
resp.format_ranges[i] = formats[formats_sent + i].range_;
}
res = channel->Write(&resp, sizeof(resp));
if (res != ZX_OK) {
LOG(TRACE, "Failed to send get stream formats response (res %d)\n", res);
return res;
}
formats_sent += todo;
} while (formats_sent < formats.size());
return ZX_OK;
}
zx_status_t UsbAudioStream::OnSetStreamFormatLocked(dispatcher::Channel* channel,
const audio_proto::StreamSetFmtReq& req,
bool privileged) {
ZX_DEBUG_ASSERT(channel != nullptr);
zx::channel client_rb_channel;
audio_proto::StreamSetFmtResp resp = { };
resp.hdr = req.hdr;
// Only the privileged stream channel is allowed to change the format.
if (!privileged) {
ZX_DEBUG_ASSERT(channel != stream_channel_.get());
resp.result = ZX_ERR_ACCESS_DENIED;
goto finished;
}
// Look up the details about the interface and the endpoint which will be
// used for the requested format.
size_t format_ndx;
resp.result = ifc_->LookupFormat(req.frames_per_second,
req.channels,
req.sample_format,
&format_ndx);
if (resp.result != ZX_OK) {
goto finished;
}
// Determine the frame size needed for this requested format, then compute
// the size of our short packets, and the constants used to generate the
// short/long packet cadence. For now, assume that we will be operating at
// a 1mSec isochronous rate.
//
// Make sure that we can fit our longest payload length into one of our
// usb requests.
//
// Store the results of all of these calculations in local variables. Do
// not commit them to member variables until we are certain that we are
// going to go ahead with this format change.
//
// TODO(johngro) : Unless/until we can find some way to set the USB bus
// driver to perform direct DMA to/from the Ring Buffer VMO without the need
// for software intervention, we may want to expose ways to either increase
// the isochronous interval (to minimize load) or to use USB 2.0 125uSec
// sub-frame timing (to decrease latency) if possible.
uint32_t frame_size;
frame_size = audio::utils::ComputeFrameSize(req.channels, req.sample_format);
if (!frame_size) {
LOG(ERROR, "Failed to compute frame size (ch %hu fmt 0x%08x)\n",
req.channels, req.sample_format);
resp.result = ZX_ERR_INTERNAL;
goto finished;
}
static constexpr uint32_t iso_packet_rate = 1000;
uint32_t bytes_per_packet, fractional_bpp_inc, long_payload_len;
bytes_per_packet = (req.frames_per_second / iso_packet_rate) * frame_size;
fractional_bpp_inc = (req.frames_per_second % iso_packet_rate);
long_payload_len = bytes_per_packet + (fractional_bpp_inc ? frame_size : 0);
ZX_DEBUG_ASSERT(format_ndx < ifc_->formats().size());
if (long_payload_len > ifc_->formats()[format_ndx].max_req_size_) {
resp.result = ZX_ERR_INVALID_ARGS;
goto finished;
}
// Deny the format change request if the ring buffer is not currently stopped.
{
// TODO(johngro) : If the ring buffer is running, should we automatically
// stop it instead of returning bad state?
fbl::AutoLock req_lock(&req_lock_);
if (ring_buffer_state_ != RingBufferState::STOPPED) {
resp.result = ZX_ERR_BAD_STATE;
goto finished;
}
}
// Looks like we are going ahead with this format change. Tear down any
// exiting ring buffer interface before proceeding.
if (rb_channel_ != nullptr) {
rb_channel_->Deactivate();
rb_channel_.reset();
}
// Record the details of our cadence and format selection
selected_format_ndx_ = format_ndx;
selected_frame_rate_ = req.frames_per_second;
frame_size_ = frame_size;
iso_packet_rate_ = iso_packet_rate;
bytes_per_packet_ = bytes_per_packet;
fractional_bpp_inc_ = fractional_bpp_inc;
// Compute the effective fifo depth for this stream. Right now, we assume
// that the controller will never get farther ahead than two isochronous usb
// requests, so we report this as the worst case fifo_depth.
//
// Based on our cadence generation parameters, adjust this number based on
// whether or not it is possible to have 0, 1 or 2 long packets back to back
// at any point in time during the sequence.
//
// TODO(johngro): This is not the proper way to report the FIFO depth. How
// far ahead the USB controller will read ahead into its FIFO is going to be
// a property of the controller and the properties of the endpoint. It is
// possible that this is negotiable to some extent as well. I need to work
// with voydanof@ to determine what we can expose from the USB bus driver in
// order to report this accurately.
fifo_bytes_ = bytes_per_packet_ << 1;
// If we have no fractional portion to accumulate, we always send
// short packets. If our fractional portion is <= 1/2 of our
// isochronous rate, then we will never send two long packets back
// to back.
if (fractional_bpp_inc_) {
fifo_bytes_ += frame_size_;
if (fractional_bpp_inc_ > (iso_packet_rate_ >> 1)) {
fifo_bytes_ += frame_size_;
}
}
// Create a new ring buffer channel which can be used to move bulk data and
// bind it to us.
rb_channel_ = dispatcher::Channel::Create();
if (rb_channel_ == nullptr) {
resp.result = ZX_ERR_NO_MEMORY;
} else {
dispatcher::Channel::ProcessHandler phandler(
[stream = fbl::WrapRefPtr(this)](dispatcher::Channel* channel) -> zx_status_t {
OBTAIN_EXECUTION_DOMAIN_TOKEN(t, stream->default_domain_);
return stream->ProcessRingBufferChannel(channel);
});
dispatcher::Channel::ChannelClosedHandler chandler(
[stream = fbl::WrapRefPtr(this)](const dispatcher::Channel* channel) -> void {
OBTAIN_EXECUTION_DOMAIN_TOKEN(t, stream->default_domain_);
stream->DeactivateRingBufferChannel(channel);
});
resp.result = rb_channel_->Activate(&client_rb_channel,
default_domain_,
std::move(phandler),
std::move(chandler));
if (resp.result != ZX_OK) {
rb_channel_.reset();
}
}
finished:
if (resp.result == ZX_OK) {
// TODO(johngro): Report the actual external delay.
resp.external_delay_nsec = 0;
return channel->Write(&resp, sizeof(resp), std::move(client_rb_channel));
} else {
return channel->Write(&resp, sizeof(resp));
}
}
zx_status_t UsbAudioStream::OnGetGainLocked(dispatcher::Channel* channel,
const audio_proto::GetGainReq& req) {
ZX_DEBUG_ASSERT(channel != nullptr);
audio_proto::GetGainResp resp = { };
resp.hdr = req.hdr;
ZX_DEBUG_ASSERT(ifc_->path() != nullptr);
const auto& path = *(ifc_->path());
resp.can_mute = path.has_mute();
resp.cur_mute = path.cur_mute();
resp.can_agc = path.has_agc();
resp.cur_agc = path.cur_agc();
resp.cur_gain = path.cur_gain();
resp.min_gain = path.min_gain();
resp.max_gain = path.max_gain();
resp.gain_step = path.gain_res();
return channel->Write(&resp, sizeof(resp));
}
zx_status_t UsbAudioStream::OnSetGainLocked(dispatcher::Channel* channel,
const audio_proto::SetGainReq& req) {
// TODO(johngro): Actually perform the set operation on our audio path.
ZX_DEBUG_ASSERT(channel != nullptr);
audio_proto::SetGainResp resp = { };
resp.hdr = req.hdr;
ZX_DEBUG_ASSERT(ifc_->path() != nullptr);
auto& path = *(ifc_->path());
bool req_mute = req.flags & AUDIO_SGF_MUTE;
bool req_agc = req.flags & AUDIO_SGF_AGC;
bool illegal_mute = (req.flags & AUDIO_SGF_MUTE_VALID) && req_mute && !path.has_mute();
bool illegal_agc = (req.flags & AUDIO_SGF_AGC_VALID) && req_agc && !path.has_agc();
bool illegal_gain = (req.flags & AUDIO_SGF_GAIN_VALID) && (req.gain != 0) && !path.has_gain();
if (illegal_mute || illegal_agc || illegal_gain) {
// If this request is illegal, make no changes but attempt to report the
// current state of the world.
resp.cur_mute = path.cur_mute();
resp.cur_agc = path.cur_agc();
resp.cur_gain = path.cur_gain();
resp.result = ZX_ERR_INVALID_ARGS;
} else {
if (req.flags & AUDIO_SGF_MUTE_VALID) {
resp.cur_mute = path.SetMute(parent_.usb_proto(), req_mute);
}
if (req.flags & AUDIO_SGF_AGC_VALID) {
resp.cur_agc = path.SetAgc(parent_.usb_proto(), req_agc);
}
if (req.flags & AUDIO_SGF_GAIN_VALID) {
resp.cur_gain = path.SetGain(parent_.usb_proto(), req.gain);
}
resp.result = ZX_OK;
}
return (req.hdr.cmd & AUDIO_FLAG_NO_ACK) ? ZX_OK : channel->Write(&resp, sizeof(resp));
}
zx_status_t UsbAudioStream::OnPlugDetectLocked(dispatcher::Channel* channel,
const audio_proto::PlugDetectReq& req) {
if (req.hdr.cmd & AUDIO_FLAG_NO_ACK)
return ZX_OK;
audio_proto::PlugDetectResp resp = { };
resp.hdr = req.hdr;
resp.flags = static_cast<audio_pd_notify_flags_t>(AUDIO_PDNF_HARDWIRED | AUDIO_PDNF_PLUGGED);
resp.plug_state_time = create_time_;
return channel->Write(&resp, sizeof(resp));
}
zx_status_t UsbAudioStream::OnGetUniqueIdLocked(dispatcher::Channel* channel,
const audio_proto::GetUniqueIdReq& req) {
audio_proto::GetUniqueIdResp resp;
static_assert(sizeof(resp.unique_id) == sizeof(persistent_unique_id_),
"Unique ID sizes much match!");
resp.hdr = req.hdr;
resp.unique_id = persistent_unique_id_;
return channel->Write(&resp, sizeof(resp));
}
zx_status_t UsbAudioStream::OnGetStringLocked(dispatcher::Channel* channel,
const audio_proto::GetStringReq& req) {
audio_proto::GetStringResp resp;
const fbl::Array<uint8_t>* str;
resp.hdr = req.hdr;
resp.id = req.id;
switch (req.id) {
case AUDIO_STREAM_STR_ID_MANUFACTURER: str = &parent_.mfr_name(); break;
case AUDIO_STREAM_STR_ID_PRODUCT: str = &parent_.prod_name(); break;
default: str = nullptr; break;
}
if (str == nullptr) {
resp.result = ZX_ERR_NOT_FOUND;
resp.strlen = 0;
} else {
size_t todo = fbl::min<size_t>(sizeof(resp.str), str->size());
ZX_DEBUG_ASSERT(todo <= std::numeric_limits<uint32_t>::max());
::memset(resp.str, 0, sizeof(resp.str));
if (todo) {
::memcpy(resp.str, str->get(), todo);
}
resp.result = ZX_OK;
resp.strlen = static_cast<uint32_t>(todo);
}
return channel->Write(&resp, sizeof(resp));
}
zx_status_t UsbAudioStream::OnGetFifoDepthLocked(dispatcher::Channel* channel,
const audio_proto::RingBufGetFifoDepthReq& req) {
audio_proto::RingBufGetFifoDepthResp resp = { };
resp.hdr = req.hdr;
resp.result = ZX_OK;
resp.fifo_depth = fifo_bytes_;
return channel->Write(&resp, sizeof(resp));
}
zx_status_t UsbAudioStream::OnGetBufferLocked(dispatcher::Channel* channel,
const audio_proto::RingBufGetBufferReq& req) {
audio_proto::RingBufGetBufferResp resp = { };
zx::vmo client_rb_handle;
uint32_t map_flags, client_rights;
resp.hdr = req.hdr;
resp.result = ZX_ERR_INTERNAL;
{
// We cannot create a new ring buffer if we are not currently stopped.
fbl::AutoLock req_lock(&req_lock_);
if (ring_buffer_state_ != RingBufferState::STOPPED) {
resp.result = ZX_ERR_BAD_STATE;
goto finished;
}
}
// Unmap and release any previous ring buffer.
ReleaseRingBufferLocked();
// Compute the ring buffer size. It needs to be at least as big
// as the virtual fifo depth.
ZX_DEBUG_ASSERT(frame_size_ && ((fifo_bytes_ % frame_size_) == 0));
ZX_DEBUG_ASSERT(fifo_bytes_ && ((fifo_bytes_ % fifo_bytes_) == 0));
ring_buffer_size_ = req.min_ring_buffer_frames;
ring_buffer_size_ *= frame_size_;
if (ring_buffer_size_ < fifo_bytes_)
ring_buffer_size_ = fbl::round_up(fifo_bytes_, frame_size_);
// Set up our state for generating notifications.
if (req.notifications_per_ring) {
bytes_per_notification_ = ring_buffer_size_ / req.notifications_per_ring;
} else {
bytes_per_notification_ = 0;
}
// Create the ring buffer vmo we will use to share memory with the client.
resp.result = zx::vmo::create(ring_buffer_size_, 0, &ring_buffer_vmo_);
if (resp.result != ZX_OK) {
LOG(ERROR, "Failed to create ring buffer (size %u, res %d)\n",
ring_buffer_size_, resp.result);
goto finished;
}
// Map the VMO into our address space.
//
// TODO(johngro): skip this step when APIs in the USB bus driver exist to
// DMA directly from the VMO.
map_flags = ZX_VM_PERM_READ;
if (is_input())
map_flags |= ZX_VM_PERM_WRITE;
resp.result = zx::vmar::root_self()->map(0, ring_buffer_vmo_,
0, ring_buffer_size_,
map_flags,
reinterpret_cast<uintptr_t*>(&ring_buffer_virt_));
if (resp.result != ZX_OK) {
LOG(ERROR, "Failed to map ring buffer (size %u, res %d)\n",
ring_buffer_size_, resp.result);
goto finished;
}
// Create the client's handle to the ring buffer vmo and set it back to them.
client_rights = ZX_RIGHT_TRANSFER | ZX_RIGHT_MAP | ZX_RIGHT_READ;
if (!is_input())
client_rights |= ZX_RIGHT_WRITE;
resp.result = ring_buffer_vmo_.duplicate(client_rights, &client_rb_handle);
if (resp.result != ZX_OK) {
LOG(ERROR, "Failed to duplicate ring buffer handle (res %d)\n", resp.result);
goto finished;
}
resp.num_ring_buffer_frames = ring_buffer_size_ / frame_size_;
finished:
zx_status_t res;
if (resp.result == ZX_OK) {
ZX_DEBUG_ASSERT(client_rb_handle.is_valid());
res = channel->Write(&resp, sizeof(resp), std::move(client_rb_handle));
} else {
res = channel->Write(&resp, sizeof(resp));
}
if (res != ZX_OK)
ReleaseRingBufferLocked();
return res;
}
zx_status_t UsbAudioStream::OnStartLocked(dispatcher::Channel* channel,
const audio_proto::RingBufStartReq& req) {
audio_proto::RingBufStartResp resp = { };
resp.hdr = req.hdr;
fbl::AutoLock req_lock(&req_lock_);
if (ring_buffer_state_ != RingBufferState::STOPPED) {
// The ring buffer is running, do not linger in the lock while we send
// the error code back to the user.
req_lock.release();
resp.result = ZX_ERR_BAD_STATE;
return channel->Write(&resp, sizeof(resp));
}
// We are idle, all of our usb requests should be sitting in the free list.
ZX_DEBUG_ASSERT(allocated_req_cnt_ == free_req_cnt_);
// Activate the format.
resp.result = ifc_->ActivateFormat(selected_format_ndx_, selected_frame_rate_);
if (resp.result != ZX_OK) {
return channel->Write(&resp, sizeof(resp));
}
// Initialize the counters used to...
// 1) generate the short/long packet cadence.
// 2) generate notifications.
// 3) track the position in the ring buffer.
fractional_bpp_acc_ = 0;
notification_acc_ = 0;
ring_buffer_offset_ = 0;
ring_buffer_pos_ = 0;
// Schedule the frame number which the first transaction will go out on.
//
// TODO(johngro): This cannot be the current frame number, that train
// has already left the station. It probably should not be the next frame
// number either as that train might be just about to leave the station.
//
// For now, set this to be the current frame number +2 and use the first
// transaction complete callback to estimate the DMA start time. Moving
// forward, when the USB bus driver can tell us which frame a transaction
// went out on, schedule the transaction using the special "on the next USB
// isochronous frame" sentinel value and figure out which frame that was
// during the callback.
usb_frame_num_ = usb_get_current_frame(&parent_.usb_proto()) + 2;
// Flag ourselves as being in the starting state, then queue up all of our
// transactions.
ring_buffer_state_ = RingBufferState::STARTING;
while (!list_is_empty(&free_req_))
QueueRequestLocked();
// Record the transaction ID we will send back to our client when we have
// successfully started, then get out.
pending_job_resp_.start = resp;
return ZX_OK;
}
zx_status_t UsbAudioStream::OnStopLocked(dispatcher::Channel* channel,
const audio_proto::RingBufStopReq& req) {
fbl::AutoLock req_lock(&req_lock_);
// TODO(johngro): Fix this to use the cancel transaction capabilities added
// to the USB bus driver.
//
// Also, investigate whether or not the cancel interface is synchronous or
// whether we will need to maintain an intermediate stopping state.
if (ring_buffer_state_ != RingBufferState::STARTED) {
audio_proto::RingBufStopResp resp = { };
req_lock.release();
resp.hdr = req.hdr;
resp.result = ZX_ERR_BAD_STATE;
return channel->Write(&resp, sizeof(resp));
}
ring_buffer_state_ = RingBufferState::STOPPING;
pending_job_resp_.stop.hdr = req.hdr;
return ZX_OK;
}
void UsbAudioStream::RequestComplete(usb_request_t* req) {
enum class Action {
NONE,
SIGNAL_STARTED,
SIGNAL_STOPPED,
NOTIFY_POSITION,
HANDLE_UNPLUG,
};
union {
audio_proto::RingBufStopResp stop;
audio_proto::RingBufStartResp start;
audio_proto::RingBufPositionNotify notify_pos;
} resp;
uint64_t complete_time = zx_clock_get_monotonic();
Action when_finished = Action::NONE;
// TODO(johngro) : See MG-940. Eliminate this as soon as we have a more
// official way of meeting real-time latency requirements. Also, the fact
// that this boosting gets done after the first transaction completes
// degrades the quality of the startup time estimate (if the system is under
// high load when the system starts up). As a general issue, there are
// better ways of refining this estimate than bumping the thread prio before
// the first transaction gets queued. Therefor, we just have a poor
// estimate for now and will need to live with the consequences.
if (!req_complete_prio_bumped_) {
zx_thread_set_priority(24 /* HIGH_PRIORITY in LK */);
req_complete_prio_bumped_ = true;
}
{
fbl::AutoLock req_lock(&req_lock_);
// Cache the status and length of this usb request.
zx_status_t req_status = req->response.status;
uint32_t req_length = static_cast<uint32_t>(req->header.length);
// Complete the usb request. This will return the transaction to the free
// list and (in the case of an input stream) copy the payload to the
// ring buffer, and update the ring buffer position.
//
// TODO(johngro): copying the payload out of the ring buffer is an
// operation which goes away when we get to the zero copy world.
CompleteRequestLocked(req);
// Did the transaction fail because the device was unplugged? If so,
// enter the stopping state and close the connections to our clients.
if (req_status == ZX_ERR_IO_NOT_PRESENT) {
ring_buffer_state_ = RingBufferState::STOPPING_AFTER_UNPLUG;
} else {
// If we are supposed to be delivering notifications, check to see
// if it is time to do so.
if (bytes_per_notification_) {
notification_acc_ += req_length;
if ((ring_buffer_state_ == RingBufferState::STARTED) &&
(notification_acc_ >= bytes_per_notification_)) {
when_finished = Action::NOTIFY_POSITION;
notification_acc_ = (notification_acc_ % bytes_per_notification_);
resp.notify_pos.ring_buffer_pos = ring_buffer_pos_;
}
}
}
switch (ring_buffer_state_) {
case RingBufferState::STOPPING:
if (free_req_cnt_ == allocated_req_cnt_) {
resp.stop = pending_job_resp_.stop;
when_finished = Action::SIGNAL_STOPPED;
}
break;
case RingBufferState::STOPPING_AFTER_UNPLUG:
if (free_req_cnt_ == allocated_req_cnt_) {
resp.stop = pending_job_resp_.stop;
when_finished = Action::HANDLE_UNPLUG;
}
break;
case RingBufferState::STARTING:
resp.start = pending_job_resp_.start;
when_finished = Action::SIGNAL_STARTED;
break;
case RingBufferState::STARTED:
QueueRequestLocked();
break;
case RingBufferState::STOPPED:
default:
LOG(ERROR, "Invalid state (%u) in %s\n",
static_cast<uint32_t>(ring_buffer_state_), __PRETTY_FUNCTION__);
ZX_DEBUG_ASSERT(false);
break;
}
}
if (when_finished != Action::NONE) {
fbl::AutoLock lock(&lock_);
switch (when_finished) {
case Action::SIGNAL_STARTED:
if (rb_channel_ != nullptr) {
// TODO(johngro) : this start time estimate is not as good as it
// could be. We really need to have the USB bus driver report
// the relationship between the USB frame counter and the system
// tick counter (and track the relationship in the case that the
// USB oscillator is not derived from the system oscillator).
// Then we can accurately report the start time as the time of
// the tick on which we scheduled the first transaction.
resp.start.result = ZX_OK;
resp.start.start_time = zx_time_sub_duration(complete_time, ZX_MSEC(1));
rb_channel_->Write(&resp.start, sizeof(resp.start));
}
{
fbl::AutoLock req_lock(&req_lock_);
ring_buffer_state_ = RingBufferState::STARTED;
}
break;
case Action::HANDLE_UNPLUG:
if (rb_channel_ != nullptr) {
rb_channel_->Deactivate();
rb_channel_.reset();
}
if (stream_channel_ != nullptr) {
stream_channel_->Deactivate();
stream_channel_.reset();
}
{
fbl::AutoLock req_lock(&req_lock_);
ring_buffer_state_ = RingBufferState::STOPPED;
}
break;
case Action::SIGNAL_STOPPED:
if (rb_channel_ != nullptr) {
resp.stop.result = ZX_OK;
rb_channel_->Write(&resp.stop, sizeof(resp.stop));
}
{
fbl::AutoLock req_lock(&req_lock_);
ring_buffer_state_ = RingBufferState::STOPPED;
ifc_->ActivateIdleFormat();
}
break;
case Action::NOTIFY_POSITION:
resp.notify_pos.hdr.cmd = AUDIO_RB_POSITION_NOTIFY;
resp.notify_pos.hdr.transaction_id = AUDIO_INVALID_TRANSACTION_ID;
rb_channel_->Write(&resp.notify_pos, sizeof(resp.notify_pos));
break;
default:
ZX_DEBUG_ASSERT(false);
break;
}
}
}
void UsbAudioStream::QueueRequestLocked() {
ZX_DEBUG_ASSERT((ring_buffer_state_ == RingBufferState::STARTING) ||
(ring_buffer_state_ == RingBufferState::STARTED));
ZX_DEBUG_ASSERT(!list_is_empty(&free_req_));
// Figure out how much we want to send or receive this time (short or long
// packet)
uint32_t todo = bytes_per_packet_;
fractional_bpp_acc_ += fractional_bpp_inc_;
if (fractional_bpp_acc_ >= iso_packet_rate_) {
fractional_bpp_acc_ -= iso_packet_rate_;
todo += frame_size_;
ZX_DEBUG_ASSERT(fractional_bpp_acc_ < iso_packet_rate_);
}
// Grab a free usb request.
auto req = usb_req_list_remove_head(&free_req_, parent_.parent_req_size());
ZX_DEBUG_ASSERT(req != nullptr);
ZX_DEBUG_ASSERT(free_req_cnt_ > 0);
--free_req_cnt_;
// If this is an output stream, copy our data into the usb request.
// TODO(johngro): eliminate this when we can get to a zero-copy world.
if (!is_input()) {
uint32_t avail = ring_buffer_size_ - ring_buffer_offset_;
ZX_DEBUG_ASSERT(ring_buffer_offset_ < ring_buffer_size_);
ZX_DEBUG_ASSERT((avail % frame_size_) == 0);
uint32_t amt = fbl::min(avail, todo);
const uint8_t* src = reinterpret_cast<uint8_t*>(ring_buffer_virt_) + ring_buffer_offset_;
usb_request_copy_to(req, src, amt, 0);
if (amt == avail) {
ring_buffer_offset_ = todo - amt;
if (ring_buffer_offset_ > 0) {
usb_request_copy_to(req, ring_buffer_virt_, ring_buffer_offset_, amt);
}
} else {
ring_buffer_offset_ += amt;
}
}
req->header.frame = usb_frame_num_++;
req->header.length = todo;
usb_request_queue(&parent_.usb_proto(), req,
UsbAudioStream::RequestCompleteCallback, this);
}
void UsbAudioStream::CompleteRequestLocked(usb_request_t* req) {
ZX_DEBUG_ASSERT(req);
// If we are an input stream, copy the payload into the ring buffer.
if (is_input()) {
uint32_t todo = static_cast<uint32_t>(req->header.length);
uint32_t avail = ring_buffer_size_ - ring_buffer_offset_;
ZX_DEBUG_ASSERT(ring_buffer_offset_ < ring_buffer_size_);
ZX_DEBUG_ASSERT((avail % frame_size_) == 0);
uint32_t amt = fbl::min(avail, todo);
uint8_t* dst = reinterpret_cast<uint8_t*>(ring_buffer_virt_) + ring_buffer_offset_;
if (req->response.status == ZX_OK) {
usb_request_copy_from(req, dst, amt, 0);
if (amt < todo) {
usb_request_copy_from(req, ring_buffer_virt_, todo - amt, amt);
}
} else {
// TODO(johngro): filling with zeros is only the proper thing to do
// for signed formats. USB does support unsigned 8-bit audio; if
// that is our format, we should fill with 0x80 instead in order to
// fill with silence.
memset(dst, 0, amt);
if (amt < todo) {
memset(ring_buffer_virt_, 0, todo - amt);
}
}
}
// Update the ring buffer position.
ring_buffer_pos_ += static_cast<uint32_t>(req->header.length);
if (ring_buffer_pos_ >= ring_buffer_size_) {
ring_buffer_pos_ -= ring_buffer_size_;
ZX_DEBUG_ASSERT(ring_buffer_pos_ < ring_buffer_size_);
}
// If this is an input stream, the ring buffer offset should always be equal
// to the stream position.
if (is_input()) {
ring_buffer_offset_ = ring_buffer_pos_;
}
// Return the transaction to the free list.
zx_status_t status = usb_req_list_add_head(&free_req_, req, parent_.parent_req_size());
ZX_DEBUG_ASSERT(status == ZX_OK);
++free_req_cnt_;
ZX_DEBUG_ASSERT(free_req_cnt_ <= allocated_req_cnt_);
}
void UsbAudioStream::DeactivateStreamChannel(const dispatcher::Channel* channel) {
fbl::AutoLock lock(&lock_);
ZX_DEBUG_ASSERT(stream_channel_.get() == channel);
ZX_DEBUG_ASSERT(rb_channel_.get() != channel);
stream_channel_.reset();
}
void UsbAudioStream::DeactivateRingBufferChannel(const dispatcher::Channel* channel) {
fbl::AutoLock lock(&lock_);
ZX_DEBUG_ASSERT(stream_channel_.get() != channel);
ZX_DEBUG_ASSERT(rb_channel_.get() == channel);
{
fbl::AutoLock req_lock(&req_lock_);
if (ring_buffer_state_ != RingBufferState::STOPPED) {
ring_buffer_state_ = RingBufferState::STOPPING;
}
}
rb_channel_.reset();
}
} // namespace usb
} // namespace audio