| // 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 <lib/fidl/llcpp/server.h> |
| #include <lib/fit/defer.h> |
| #include <lib/zx/clock.h> |
| #include <string.h> |
| |
| #include <algorithm> |
| #include <limits> |
| #include <utility> |
| |
| #include <audio-proto-utils/format-utils.h> |
| #include <audio-proto/audio-proto.h> |
| #include <fbl/algorithm.h> |
| #include <intel-hda/codec-utils/codec-driver-base.h> |
| #include <intel-hda/codec-utils/stream-base.h> |
| #include <intel-hda/utils/intel-hda-proto.h> |
| |
| #include "debug-logging.h" |
| |
| namespace { |
| namespace audio_fidl = fuchsia_hardware_audio; |
| } // namespace |
| |
| namespace audio { |
| namespace intel_hda { |
| namespace codecs { |
| |
| zx_protocol_device_t IntelHDAStreamBase::STREAM_DEVICE_THUNKS = []() { |
| zx_protocol_device_t sdt = {}; |
| sdt.version = DEVICE_OPS_VERSION; |
| sdt.message = [](void* ctx, fidl_incoming_msg_t* msg, fidl_txn_t* txn) { |
| IntelHDAStreamBase* thiz = static_cast<IntelHDAStreamBase*>(ctx); |
| DdkTransaction transaction(txn); |
| fidl::WireDispatch<fuchsia_hardware_audio::Device>(thiz, msg, &transaction); |
| return transaction.Status(); |
| }; |
| return sdt; |
| }(); |
| |
| IntelHDAStreamBase::IntelHDAStreamBase(uint32_t id, bool is_input) |
| : id_(id), is_input_(is_input), loop_(&kAsyncLoopConfigNeverAttachToThread) { |
| plug_time_ = zx::clock::get_monotonic().get(); |
| snprintf(dev_name_, sizeof(dev_name_), "%s-stream-%03u", is_input_ ? "input" : "output", id_); |
| loop_.StartThread("intel-hda-stream-loop"); |
| } |
| |
| IntelHDAStreamBase::~IntelHDAStreamBase() {} |
| |
| void IntelHDAStreamBase::PrintDebugPrefix() const { printf("[%s] ", dev_name_); } |
| |
| void IntelHDAStreamBase::SetPersistentUniqueId(const audio_stream_unique_id_t& id) { |
| fbl::AutoLock obj_lock(&obj_lock_); |
| SetPersistentUniqueIdLocked(id); |
| } |
| |
| void IntelHDAStreamBase::SetPersistentUniqueIdLocked(const audio_stream_unique_id_t& id) { |
| persistent_unique_id_ = id; |
| } |
| |
| zx_status_t IntelHDAStreamBase::Activate(fbl::RefPtr<IntelHDACodecDriverBase>&& parent_codec, |
| const fbl::RefPtr<Channel>& codec_channel) { |
| ZX_DEBUG_ASSERT(codec_channel != nullptr); |
| |
| fbl::AutoLock obj_lock(&obj_lock_); |
| if (is_active() || (codec_channel_ != nullptr)) |
| return ZX_ERR_BAD_STATE; |
| |
| ZX_DEBUG_ASSERT(parent_codec_ == nullptr); |
| |
| // Remember our parent codec and our codec channel. If something goes wrong |
| // during activation, make sure we let go of these references. |
| // |
| // Note; the cleanup lambda needs to have thread analysis turned off because |
| // the compiler is not quite smart enough to figure out that the obj_lock |
| // AutoLock will destruct (and release the lock) after the AutoCall runs, |
| // and that the AutoCall will never leave this scope. |
| auto cleanup = fit::defer([this]() __TA_NO_THREAD_SAFETY_ANALYSIS { |
| parent_codec_.reset(); |
| codec_channel_.reset(); |
| }); |
| parent_codec_ = std::move(parent_codec); |
| codec_channel_ = codec_channel; |
| |
| // Allow our implementation to send its initial stream setup commands to the |
| // codec. |
| zx_status_t res = OnActivateLocked(); |
| if (res != ZX_OK) |
| return res; |
| |
| // Request a DMA context |
| ihda_proto::RequestStreamReq req; |
| |
| req.hdr.transaction_id = id(); |
| req.hdr.cmd = IHDA_CODEC_REQUEST_STREAM; |
| req.input = is_input(); |
| |
| res = codec_channel_->Write(&req, sizeof(req)); |
| if (res != ZX_OK) |
| return res; |
| |
| cleanup.cancel(); |
| return ZX_OK; |
| } |
| |
| void IntelHDAStreamBase::Deactivate() { |
| { |
| fbl::AutoLock obj_lock(&obj_lock_); |
| DEBUG_LOG("Deactivating stream\n"); |
| |
| // Let go of any unsolicited stream tags we may be holding. |
| if (unsol_tag_count_) { |
| ZX_DEBUG_ASSERT(parent_codec_ != nullptr); |
| parent_codec_->ReleaseAllUnsolTags(*this); |
| unsol_tag_count_ = 0; |
| } |
| |
| // Clear out our parent_codec_ pointer. This will mark us as being |
| // inactive and prevent any new connections from being made. |
| parent_codec_.reset(); |
| |
| // We should already have been removed from our codec's active stream list |
| // at this point. |
| ZX_DEBUG_ASSERT(!this->InContainer()); |
| } |
| |
| loop_.Shutdown(); |
| |
| { |
| fbl::AutoLock obj_lock(&obj_lock_); |
| ZX_DEBUG_ASSERT(stream_channel_ == nullptr); |
| |
| // Allow our implementation to send the commands needed to tear down the |
| // widgets which make up this stream. |
| OnDeactivateLocked(); |
| |
| // If we have been given a DMA stream by the IHDA controller, attempt to |
| // return it now. |
| if ((dma_stream_id_ != IHDA_INVALID_STREAM_ID) && (codec_channel_ != nullptr)) { |
| ihda_proto::ReleaseStreamReq req; |
| |
| req.hdr.transaction_id = id(); |
| req.hdr.cmd = IHDA_CODEC_RELEASE_STREAM_NOACK, req.stream_id = dma_stream_id_; |
| |
| codec_channel_->Write(&req, sizeof(req)); |
| |
| dma_stream_id_ = IHDA_INVALID_STREAM_ID; |
| dma_stream_tag_ = IHDA_INVALID_STREAM_TAG; |
| } |
| |
| // Let go of our reference to the codec device channel. |
| codec_channel_ = nullptr; |
| |
| // If we had published a device node, remove it now. |
| if (parent_device_ != nullptr) { |
| device_async_remove(stream_device_); |
| parent_device_ = nullptr; |
| } |
| } |
| |
| DEBUG_LOG("Deactivate complete\n"); |
| } |
| |
| zx_status_t IntelHDAStreamBase::PublishDeviceLocked() { |
| if (!is_active() || (parent_device_ != nullptr)) |
| return ZX_ERR_BAD_STATE; |
| ZX_DEBUG_ASSERT(parent_codec_ != nullptr); |
| |
| // Initialize our device and fill out the protocol hooks |
| device_add_args_t args = {}; |
| args.version = DEVICE_ADD_ARGS_VERSION; |
| args.name = dev_name_; |
| args.ctx = this; |
| args.ops = &STREAM_DEVICE_THUNKS; |
| args.proto_id = (is_input() ? ZX_PROTOCOL_AUDIO_INPUT : ZX_PROTOCOL_AUDIO_OUTPUT); |
| |
| // Publish the device. |
| zx_status_t res = device_add(parent_codec_->codec_device(), &args, &stream_device_); |
| if (res != ZX_OK) { |
| LOG("Failed to add stream device for \"%s\" (res %d)\n", dev_name_, res); |
| return res; |
| } |
| |
| // Record our parent. |
| parent_device_ = parent_codec_->codec_device(); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t IntelHDAStreamBase::ProcessResponse(const CodecResponse& resp) { |
| fbl::AutoLock obj_lock(&obj_lock_); |
| |
| if (!is_active()) { |
| DEBUG_LOG("Ignoring codec response (0x%08x, 0x%08x) for inactive stream id %u\n", resp.data, |
| resp.data_ex, id()); |
| return ZX_OK; |
| } |
| |
| return resp.unsolicited() ? OnUnsolicitedResponseLocked(resp) : OnSolicitedResponseLocked(resp); |
| } |
| |
| zx_status_t IntelHDAStreamBase::ProcessRequestStream(const ihda_proto::RequestStreamResp& resp) { |
| fbl::AutoLock obj_lock(&obj_lock_); |
| zx_status_t res; |
| |
| if (!is_active()) |
| return ZX_ERR_BAD_STATE; |
| |
| res = SetDMAStreamLocked(resp.stream_id, resp.stream_tag); |
| if (res != ZX_OK) { |
| // TODO(johngro) : If we failed to set the DMA info because this stream |
| // is in the process of shutting down, we really should return the |
| // stream to the controller. |
| // |
| // Right now, we are going to return an error which will cause the lower |
| // level infrastructure to close the codec device channel. This will |
| // prevent a leak (the core controller driver will re-claim the stream), |
| // but it will also ruin all of the other streams in this codec are |
| // going to end up being destroyed. For simple codec driver who never |
| // change stream topology, this is probably fine, but for more |
| // complicated ones it probably is not. |
| return res; |
| } |
| |
| return OnDMAAssignedLocked(); |
| } |
| |
| zx_status_t IntelHDAStreamBase::ProcessSetStreamFmt( |
| const ihda_proto::SetStreamFmtResp& codec_resp) { |
| fbl::AutoLock obj_lock(&obj_lock_); |
| zx_status_t res = ZX_OK; |
| |
| // Are we shutting down? |
| if (!is_active()) |
| return ZX_ERR_BAD_STATE; |
| |
| // If we don't have a set format operation in flight, or the stream channel |
| // has been closed, this set format operation has been canceled. Do not |
| // return an error up the stack; we don't want to close the connection to |
| // our codec device. |
| if ((!IsFormatChangeInProgress()) || (stream_channel_ == nullptr)) |
| goto finished; |
| |
| // Let the implementation send the commands required to finish changing the |
| // stream format. |
| res = FinishChangeStreamFormatLocked(encoded_fmt_); |
| if (res != ZX_OK) { |
| DEBUG_LOG("Failed to finish set format (enc fmt 0x%04hx res %d)\n", encoded_fmt_, res); |
| goto finished; |
| } |
| |
| finished: |
| // Something went fatally wrong when trying to send the result back to the |
| // caller. Close the stream channel. |
| if ((res != ZX_OK) && (stream_channel_ != nullptr)) { |
| OnChannelDeactivateLocked(*stream_channel_); |
| stream_channel_ = nullptr; |
| } |
| |
| // Set format operation is finished. There is no reply sent in CreateRingBuffer. |
| SetFormatChangeInProgress(false); |
| |
| return ZX_OK; |
| } |
| |
| // TODO(johngro) : Refactor this; this sample_format of parameters is 95% the same |
| // between both the codec and stream base classes. |
| zx_status_t IntelHDAStreamBase::SendCodecCommandLocked(uint16_t nid, CodecVerb verb, Ack do_ack) { |
| if (codec_channel_ == nullptr) |
| return ZX_ERR_BAD_STATE; |
| |
| ihda_codec_send_corb_cmd_req_t cmd; |
| |
| cmd.hdr.cmd = (do_ack == Ack::NO) ? IHDA_CODEC_SEND_CORB_CMD_NOACK : IHDA_CODEC_SEND_CORB_CMD; |
| cmd.hdr.transaction_id = id(); |
| cmd.nid = nid; |
| cmd.verb = verb.val; |
| |
| return codec_channel_->Write(&cmd, sizeof(cmd)); |
| } |
| |
| zx_status_t IntelHDAStreamBase::SetDMAStreamLocked(uint16_t id, uint8_t tag) { |
| if ((id == IHDA_INVALID_STREAM_ID) || (tag == IHDA_INVALID_STREAM_TAG)) |
| return ZX_ERR_INVALID_ARGS; |
| |
| ZX_DEBUG_ASSERT((dma_stream_id_ == IHDA_INVALID_STREAM_ID) == |
| (dma_stream_tag_ == IHDA_INVALID_STREAM_TAG)); |
| |
| if (dma_stream_id_ != IHDA_INVALID_STREAM_ID) |
| return ZX_ERR_BAD_STATE; |
| |
| dma_stream_id_ = id; |
| dma_stream_tag_ = tag; |
| |
| return ZX_OK; |
| } |
| |
| void IntelHDAStreamBase::GetChannel(GetChannelCompleter::Sync& completer) { |
| fbl::AutoLock obj_lock(&obj_lock_); |
| |
| // Do not allow any new connections if we are in the process of shutting down |
| if (!is_active()) { |
| completer.Close(ZX_ERR_BAD_STATE); |
| return; |
| } |
| |
| // For now, block new connections if we currently have no privileged |
| // connection, but there is a SetFormat request in flight to the codec |
| // driver. We are trying to avoid the following sequence... |
| // |
| // 1) A privileged connection starts a set format. |
| // 2) After we ask the controller to set the format, our privileged channel |
| // is closed. |
| // 3) A new user connects. |
| // 4) The response to the first client's request arrives and gets sent |
| // to the second client. |
| // 5) Confusion ensues. |
| // |
| // Denying new connections while the old request is in flight avoids this, |
| // but is generally a terrible solution. What we should really do is tag |
| // the requests to the codec driver with a unique ID which we can use to |
| // filter responses. One option might be to split the transaction ID so |
| // that a portion of the TID is used for stream routing, while another |
| // portion is used for requests like this. |
| bool privileged = (stream_channel_ == nullptr); |
| if (privileged && IsFormatChangeInProgress()) { |
| completer.Close(ZX_ERR_SHOULD_WAIT); |
| return; |
| } |
| |
| // Attempt to allocate a new driver channel and bind it to us. If we don't |
| // already have a stream_channel_, flag this channel is the privileged |
| // connection (The connection which is allowed to do things like change |
| // formats). |
| auto endpoints = fidl::CreateEndpoints<audio_fidl::StreamConfig>(); |
| if (!endpoints.is_ok()) { |
| completer.Close(ZX_ERR_NO_MEMORY); |
| return; |
| } |
| auto [stream_channel_remote, stream_channel_local] = *std::move(endpoints); |
| |
| fbl::RefPtr<StreamChannel> stream_channel = StreamChannel::Create(this); |
| if (stream_channel == nullptr) { |
| completer.Close(ZX_ERR_NO_MEMORY); |
| return; |
| } |
| stream_channels_.push_back(stream_channel); |
| |
| fidl::OnUnboundFn<fidl::WireInterface<audio_fidl::StreamConfig>> on_unbound = |
| [this, stream_channel](fidl::WireInterface<audio_fidl::StreamConfig>*, fidl::UnbindInfo, |
| fidl::ServerEnd<fuchsia_hardware_audio::StreamConfig>) { |
| fbl::AutoLock channel_lock(&this->obj_lock_); |
| this->ProcessClientDeactivateLocked(stream_channel.get()); |
| }; |
| |
| fidl::BindServer<fidl::WireInterface<audio_fidl::StreamConfig>>( |
| loop_.dispatcher(), std::move(stream_channel_local), stream_channel.get(), |
| std::move(on_unbound)); |
| |
| if (privileged) { |
| stream_channel_ = stream_channel; |
| } |
| |
| completer.Reply(std::move(stream_channel_remote)); |
| } |
| |
| void IntelHDAStreamBase::GetSupportedFormats( |
| StreamChannel::GetSupportedFormatsCompleter::Sync& completer) { |
| fbl::AutoLock channel_lock(&obj_lock_); |
| |
| if (supported_formats_.size() > std::numeric_limits<uint16_t>::max()) { |
| LOG("Too many formats (%zu) to send during AUDIO_STREAM_CMD_GET_FORMATS request!\n", |
| supported_formats_.size()); |
| return; |
| } |
| |
| // Build formats compatible with FIDL from a vector of audio_stream_format_range_t. |
| // Needs to be alive until the reply is sent. |
| struct FidlCompatibleFormats { |
| fbl::Vector<uint8_t> number_of_channels; |
| fbl::Vector<audio_fidl::wire::SampleFormat> sample_formats; |
| fbl::Vector<uint32_t> frame_rates; |
| fbl::Vector<uint8_t> valid_bits_per_sample; |
| fbl::Vector<uint8_t> bytes_per_sample; |
| }; |
| fbl::Vector<FidlCompatibleFormats> fidl_compatible_formats; |
| for (auto& i : supported_formats_) { |
| auto formats = audio::utils::GetAllFormats(i.sample_formats); |
| ZX_ASSERT(formats.size() >= 1); |
| for (auto& j : formats) { |
| fbl::Vector<uint32_t> rates; |
| // Ignore flags if min and max are equal. |
| if (i.min_frames_per_second == i.max_frames_per_second) { |
| rates.push_back(i.min_frames_per_second); |
| } else { |
| ZX_DEBUG_ASSERT(!(i.flags & ASF_RANGE_FLAG_FPS_CONTINUOUS)); |
| audio::utils::FrameRateEnumerator enumerator(i); |
| for (uint32_t rate : enumerator) { |
| rates.push_back(rate); |
| } |
| } |
| |
| fbl::Vector<uint8_t> number_of_channels; |
| for (uint8_t j = i.min_channels; j <= i.max_channels; ++j) { |
| number_of_channels.push_back(j); |
| } |
| |
| fidl_compatible_formats.push_back({ |
| std::move(number_of_channels), |
| {j.format}, |
| std::move(rates), |
| {j.valid_bits_per_sample}, |
| {j.bytes_per_sample}, |
| }); |
| } |
| } |
| |
| fidl::FidlAllocator allocator; |
| fidl::VectorView<audio_fidl::wire::SupportedFormats> fidl_formats(allocator, |
| fidl_compatible_formats.size()); |
| for (size_t i = 0; i < fidl_compatible_formats.size(); ++i) { |
| // Get FIDL PcmSupportedFormats from FIDL compatible vectors. |
| // Needs to be alive until the reply is sent. |
| FidlCompatibleFormats& src = fidl_compatible_formats[i]; |
| audio_fidl::wire::PcmSupportedFormats formats; |
| formats.number_of_channels = ::fidl::VectorView<uint8_t>::FromExternal( |
| src.number_of_channels.data(), src.number_of_channels.size()); |
| formats.sample_formats = ::fidl::VectorView<audio_fidl::wire::SampleFormat>::FromExternal( |
| src.sample_formats.data(), src.sample_formats.size()); |
| formats.frame_rates = |
| ::fidl::VectorView<uint32_t>::FromExternal(src.frame_rates.data(), src.frame_rates.size()); |
| formats.bytes_per_sample = ::fidl::VectorView<uint8_t>::FromExternal( |
| src.bytes_per_sample.data(), src.bytes_per_sample.size()); |
| formats.valid_bits_per_sample = ::fidl::VectorView<uint8_t>::FromExternal( |
| src.valid_bits_per_sample.data(), src.valid_bits_per_sample.size()); |
| fidl_formats[i].Allocate(allocator); |
| fidl_formats[i].set_pcm_supported_formats(allocator, std::move(formats)); |
| } |
| |
| completer.Reply(std::move(fidl_formats)); |
| } |
| |
| void IntelHDAStreamBase::CreateRingBuffer( |
| StreamChannel* channel, audio_fidl::wire::Format format, |
| ::fidl::ServerEnd<audio_fidl::RingBuffer> ring_buffer, |
| StreamChannel::CreateRingBufferCompleter::Sync& completer) { |
| ZX_DEBUG_ASSERT(channel != nullptr); |
| ihda_proto::SetStreamFmtReq req; |
| uint16_t encoded_fmt; |
| zx_status_t res; |
| |
| fbl::AutoLock channel_lock(&obj_lock_); |
| // Only the privileged stream channel is allowed to change the format. |
| if (channel != stream_channel_.get()) { |
| LOG("Unprivileged channel cannot set the format"); |
| completer.Close(ZX_ERR_INVALID_ARGS); |
| return; |
| } |
| |
| // If we don't have a DMA stream assigned to us, or there is already a set |
| // format operation in flight, we cannot proceed. |
| if ((dma_stream_id_ == IHDA_INVALID_STREAM_ID) || IsFormatChangeInProgress()) { |
| completer.Close(ZX_ERR_BAD_STATE); |
| return; |
| } |
| |
| auto format_pcm = format.pcm_format(); |
| audio_sample_format_t sample_format = audio::utils::GetSampleFormat( |
| format_pcm.valid_bits_per_sample, 8 * format_pcm.bytes_per_sample); |
| |
| if (sample_format == 0) { |
| LOG("Unsupported format: Invalid bits per sample (%u/%u)\n", format_pcm.valid_bits_per_sample, |
| 8 * format_pcm.bytes_per_sample); |
| completer.Close(ZX_ERR_INVALID_ARGS); |
| return; |
| } |
| |
| if (format_pcm.sample_format == audio_fidl::wire::SampleFormat::kPcmFloat) { |
| sample_format = AUDIO_SAMPLE_FORMAT_32BIT_FLOAT; |
| if (format_pcm.valid_bits_per_sample != 32 || format_pcm.bytes_per_sample != 4) { |
| LOG("Unsupported format: Not 32 per sample/channel for float\n"); |
| completer.Close(ZX_ERR_INVALID_ARGS); |
| return; |
| } |
| } |
| |
| if (format_pcm.sample_format == audio_fidl::wire::SampleFormat::kPcmUnsigned) { |
| sample_format |= AUDIO_SAMPLE_FORMAT_FLAG_UNSIGNED; |
| } |
| |
| bool found_one = false; |
| // Check the format for compatibility |
| for (const auto& fmt : supported_formats_) { |
| if (audio::utils::FormatIsCompatible(format_pcm.frame_rate, |
| static_cast<uint16_t>(format_pcm.number_of_channels), |
| sample_format, fmt)) { |
| found_one = true; |
| break; |
| } |
| } |
| |
| if (!found_one) { |
| LOG("Could not find a suitable format in %s", __PRETTY_FUNCTION__); |
| completer.Close(ZX_ERR_INVALID_ARGS); |
| return; |
| } |
| |
| audio_proto::StreamSetFmtReq fmt = {}; |
| fmt.sample_format = sample_format; |
| fmt.channels = format_pcm.number_of_channels; |
| fmt.frames_per_second = format_pcm.frame_rate; |
| |
| // The upper level stream told us that they support this format, we had |
| // better be able to encode it into an IHDA format specifier. |
| res = EncodeStreamFormat(fmt, &encoded_fmt); |
| if (res != ZX_OK) { |
| DEBUG_LOG("Failed to encode stream format %u:%hu:%s (res %d)\n", fmt.frames_per_second, |
| fmt.channels, audio_proto::SampleFormatToString(fmt.sample_format), res); |
| completer.Close(res); |
| return; |
| } |
| |
| // Let our implementation start the process of a format change. This gives |
| // it a chance to check the format for compatibility, and send commands to |
| // quiesce the converters and amplifiers if it approves of the format. |
| res = BeginChangeStreamFormatLocked(fmt); |
| if (res != ZX_OK) { |
| DEBUG_LOG("Stream impl rejected stream format %u:%hu:%s (res %d)\n", fmt.frames_per_second, |
| fmt.channels, audio_proto::SampleFormatToString(fmt.sample_format), res); |
| completer.Close(res); |
| return; |
| } |
| |
| // Set the format of DMA stream. This will stop any stream in progress and |
| // close any connection to its clients. At this point, all of our checks |
| // are done and we expect success. If anything goes wrong, consider it to |
| // be a fatal internal error and close the connection to our client by |
| // returning an error. |
| ZX_DEBUG_ASSERT(codec_channel_ != nullptr); |
| req.hdr.cmd = IHDA_CODEC_SET_STREAM_FORMAT; |
| req.hdr.transaction_id = id(); |
| req.stream_id = dma_stream_id_; |
| req.format = encoded_fmt; |
| res = codec_channel_->Write(&req, sizeof(req), ring_buffer.TakeChannel()); |
| if (res != ZX_OK) { |
| DEBUG_LOG("Failed to write set stream format %u:%hu:%s to codec channel (res %d)\n", |
| fmt.frames_per_second, fmt.channels, |
| audio_proto::SampleFormatToString(fmt.sample_format), res); |
| completer.Close(res); |
| return; |
| } |
| |
| // Success! Record that the format change is in progress. |
| SetFormatChangeInProgress(true); |
| encoded_fmt_ = encoded_fmt; |
| } |
| |
| void IntelHDAStreamBase::WatchGainState(StreamChannel* channel, |
| StreamChannel::WatchGainStateCompleter::Sync& completer) { |
| ZX_DEBUG_ASSERT(!channel->gain_completer_); |
| channel->gain_completer_ = completer.ToAsync(); |
| |
| fbl::AutoLock obj_lock(&obj_lock_); |
| OnGetGainLocked(&cur_gain_state_); |
| |
| // Reply is delayed if there is no change since the last reported gain state. |
| if (channel->last_reported_gain_state_ != cur_gain_state_) { |
| fidl::FidlAllocator allocator; |
| audio_fidl::wire::GainState gain_state(allocator); |
| if (cur_gain_state_.can_mute) { |
| gain_state.set_muted(allocator, cur_gain_state_.cur_mute); |
| } |
| if (cur_gain_state_.can_agc) { |
| gain_state.set_agc_enabled(allocator, cur_gain_state_.cur_mute); |
| } |
| gain_state.set_gain_db(allocator, cur_gain_state_.cur_gain); |
| channel->last_reported_gain_state_ = cur_gain_state_; |
| channel->gain_completer_->Reply(std::move(gain_state)); |
| channel->gain_completer_.reset(); |
| } |
| } |
| |
| void IntelHDAStreamBase::SetGain(audio_fidl::wire::GainState target_state, |
| StreamChannel::SetGainCompleter::Sync& completer) { |
| fbl::AutoLock obj_lock(&obj_lock_); |
| OnGetGainLocked(&cur_gain_state_); |
| |
| // Sanity check the request before passing it along |
| if (target_state.has_muted() && target_state.muted() && !cur_gain_state_.can_mute) { |
| LOG("Can't mute\n"); |
| return; |
| } |
| |
| if (target_state.has_agc_enabled() && target_state.agc_enabled() && !cur_gain_state_.can_agc) { |
| LOG("Can't enable AGC\n"); |
| return; |
| } |
| |
| if (target_state.has_gain_db() && ((target_state.gain_db() < cur_gain_state_.min_gain) || |
| (target_state.gain_db() > cur_gain_state_.max_gain))) { |
| LOG("Can't set gain outside valid range\n"); |
| return; |
| } |
| |
| audio_stream_cmd_set_gain_req_t req = {}; |
| cur_gain_state_.can_mute = target_state.has_muted(); |
| if (cur_gain_state_.can_mute) { |
| req.flags |= AUDIO_SGF_MUTE_VALID; |
| if (target_state.muted()) { |
| req.flags |= AUDIO_SGF_MUTE; |
| } |
| cur_gain_state_.cur_mute = target_state.muted(); |
| } |
| cur_gain_state_.can_agc = target_state.has_agc_enabled(); |
| if (cur_gain_state_.can_agc) { |
| req.flags |= AUDIO_SGF_AGC_VALID; |
| if (target_state.agc_enabled()) { |
| req.flags |= AUDIO_SGF_AGC; |
| } |
| cur_gain_state_.cur_agc = target_state.agc_enabled(); |
| } |
| if (target_state.has_gain_db()) { |
| req.flags |= AUDIO_SGF_GAIN_VALID; |
| req.gain = target_state.gain_db(); |
| cur_gain_state_.cur_gain = req.gain; |
| } |
| |
| audio_proto::SetGainResp out = {}; |
| OnSetGainLocked(req, &out); |
| if (out.result != ZX_OK && out.result != ZX_ERR_NOT_SUPPORTED) { |
| LOG("Error setting the gain state %d\n", out.result); |
| } |
| for (auto& channel : stream_channels_) { |
| if (channel.gain_completer_) { |
| channel.gain_completer_->Reply(std::move(target_state)); |
| channel.gain_completer_.reset(); |
| } |
| } |
| } |
| |
| void IntelHDAStreamBase::WatchPlugState(StreamChannel* channel, |
| StreamChannel::WatchPlugStateCompleter::Sync& completer) { |
| ZX_DEBUG_ASSERT(!channel->plug_completer_); |
| channel->plug_completer_ = completer.ToAsync(); |
| |
| audio_proto::PlugDetectResp plug = {}; |
| |
| fbl::AutoLock lock(obj_lock()); |
| OnPlugDetectLocked(channel, &plug); |
| |
| bool plugged = plug.flags & AUDIO_PDNF_PLUGGED; |
| // Reply is delayed if there is no change since the last reported plugged state. |
| if (channel->last_reported_plugged_state_ == StreamChannel::Plugged::kNotReported || |
| (channel->last_reported_plugged_state_ == StreamChannel::Plugged::kPlugged) != plugged) { |
| fidl::FidlAllocator allocator; |
| audio_fidl::wire::PlugState plug_state(allocator); |
| plug_state.set_plugged(allocator, plugged).set_plug_state_time(allocator, plug.plug_state_time); |
| channel->last_reported_plugged_state_ = |
| plugged ? StreamChannel::Plugged::kPlugged : StreamChannel::Plugged::kUnplugged; |
| channel->plug_completer_->Reply(std::move(plug_state)); |
| channel->plug_completer_.reset(); |
| } |
| } |
| |
| void IntelHDAStreamBase::NotifyPlugStateLocked(bool plugged, int64_t plug_time) { |
| for (auto& channel : stream_channels_) { |
| if (channel.plug_completer_) { |
| fidl::FidlAllocator allocator; |
| audio_fidl::wire::PlugState plug_state(allocator); |
| plug_state.set_plugged(allocator, plugged).set_plug_state_time(allocator, plug_time); |
| channel.plug_completer_->Reply(std::move(plug_state)); |
| channel.plug_completer_.reset(); |
| } |
| } |
| } |
| |
| void IntelHDAStreamBase::GetProperties( |
| StreamChannel* channel, |
| fidl::WireInterface<audio_fidl::StreamConfig>::GetPropertiesCompleter::Sync& completer) { |
| fbl::AutoLock obj_lock(&obj_lock_); |
| fidl::FidlAllocator allocator; |
| audio_fidl::wire::StreamProperties response(allocator); |
| |
| fidl::Array<uint8_t, audio_fidl::wire::kUniqueIdSize> unique_id = {}; |
| for (size_t i = 0; i < audio_fidl::wire::kUniqueIdSize; ++i) { |
| unique_id.data_[i] = persistent_unique_id_.data[i]; |
| } |
| response.set_unique_id(allocator, unique_id); |
| response.set_is_input(allocator, is_input()); |
| |
| OnGetGainLocked(&cur_gain_state_); |
| |
| response.set_can_mute(allocator, cur_gain_state_.can_mute); |
| response.set_can_agc(allocator, cur_gain_state_.can_agc); |
| response.set_min_gain_db(allocator, cur_gain_state_.min_gain); |
| response.set_max_gain_db(allocator, cur_gain_state_.max_gain); |
| response.set_gain_step_db(allocator, cur_gain_state_.gain_step); |
| |
| audio_proto::GetStringResp resp_product = {}; |
| audio_proto::GetStringReq req = {}; |
| req.id = AUDIO_STREAM_STR_ID_PRODUCT; |
| OnGetStringLocked(req, &resp_product); |
| auto product = fidl::StringView::FromExternal(reinterpret_cast<char*>(resp_product.str), |
| resp_product.strlen); |
| response.set_product(fidl::ObjectView<fidl::StringView>::FromExternal(&product)); |
| |
| req.id = AUDIO_STREAM_STR_ID_MANUFACTURER; |
| audio_proto::GetStringResp resp_manufacturer = {}; |
| OnGetStringLocked(req, &resp_manufacturer); |
| auto manufacturer = fidl::StringView::FromExternal(reinterpret_cast<char*>(resp_manufacturer.str), |
| resp_manufacturer.strlen); |
| response.set_manufacturer(fidl::ObjectView<fidl::StringView>::FromExternal(&manufacturer)); |
| |
| audio_proto::GetClockDomainResp domain_resp = {}; |
| OnGetClockDomainLocked(&domain_resp); |
| response.set_clock_domain(allocator, domain_resp.clock_domain); |
| |
| audio_proto::PlugDetectResp plug = {}; |
| OnPlugDetectLocked(channel, &plug); |
| if (plug.flags & AUDIO_PDNF_CAN_NOTIFY) { |
| response.set_plug_detect_capabilities( |
| allocator, audio_fidl::wire::PlugDetectCapabilities::kCanAsyncNotify); |
| } else if (plug.flags & AUDIO_PDNF_HARDWIRED) { |
| response.set_plug_detect_capabilities(allocator, |
| audio_fidl::wire::PlugDetectCapabilities::kHardwired); |
| } |
| completer.Reply(std::move(response)); |
| } |
| |
| void IntelHDAStreamBase::ProcessClientDeactivateLocked(StreamChannel* channel) { |
| ZX_DEBUG_ASSERT(channel != nullptr); |
| |
| // Let our subclass know that this channel is going away. |
| OnChannelDeactivateLocked(*channel); |
| |
| // Is this the privileged stream channel? |
| if (stream_channel_.get() == channel) { |
| stream_channel_.reset(); |
| } |
| |
| stream_channels_.erase(*channel); |
| } |
| |
| zx_status_t IntelHDAStreamBase::AllocateUnsolTagLocked(uint8_t* out_tag) { |
| if (!parent_codec_) |
| return ZX_ERR_BAD_STATE; |
| |
| zx_status_t res = parent_codec_->AllocateUnsolTag(*this, out_tag); |
| if (res == ZX_OK) |
| unsol_tag_count_++; |
| |
| return res; |
| } |
| |
| void IntelHDAStreamBase::ReleaseUnsolTagLocked(uint8_t tag) { |
| ZX_DEBUG_ASSERT(unsol_tag_count_ > 0); |
| ZX_DEBUG_ASSERT(parent_codec_ != nullptr); |
| parent_codec_->ReleaseUnsolTag(*this, tag); |
| unsol_tag_count_--; |
| } |
| |
| // TODO(johngro) : Move this out to a utils library? |
| #define MAKE_RATE(_rate, _base, _mult, _div) \ |
| { .rate = _rate, .encoded = (_base << 14) | ((_mult - 1) << 11) | ((_div - 1) << 8) } |
| zx_status_t IntelHDAStreamBase::EncodeStreamFormat(const audio_proto::StreamSetFmtReq& fmt, |
| uint16_t* encoded_fmt_out) { |
| ZX_DEBUG_ASSERT(encoded_fmt_out != nullptr); |
| |
| // See section 3.7.1 |
| // Start with the channel count. Intel HDA DMA streams support between 1 |
| // and 16 channels. |
| uint32_t channels = fmt.channels - 1; |
| if ((fmt.channels < 1) || (fmt.channels > 16)) |
| return ZX_ERR_NOT_SUPPORTED; |
| |
| // Next determine the bit sample_format format |
| uint32_t bits; |
| switch (fmt.sample_format) { |
| case AUDIO_SAMPLE_FORMAT_8BIT: |
| bits = 0; |
| break; |
| case AUDIO_SAMPLE_FORMAT_16BIT: |
| bits = 1; |
| break; |
| case AUDIO_SAMPLE_FORMAT_20BIT_IN32: |
| bits = 2; |
| break; |
| case AUDIO_SAMPLE_FORMAT_24BIT_IN32: |
| bits = 3; |
| break; |
| case AUDIO_SAMPLE_FORMAT_32BIT: |
| case AUDIO_SAMPLE_FORMAT_32BIT_FLOAT: |
| bits = 4; |
| break; |
| default: |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| // Finally, determine the base frame rate, as well as the multiplier and |
| // divisor. |
| static const struct { |
| uint32_t rate; |
| uint32_t encoded; |
| } RATE_ENCODINGS[] = { |
| // 48 KHz family |
| MAKE_RATE(6000, 0, 1, 8), |
| MAKE_RATE(8000, 0, 1, 6), |
| MAKE_RATE(9600, 0, 1, 5), |
| MAKE_RATE(16000, 0, 1, 3), |
| MAKE_RATE(24000, 0, 1, 2), |
| MAKE_RATE(32000, 0, 2, 3), |
| MAKE_RATE(48000, 0, 1, 1), |
| MAKE_RATE(96000, 0, 2, 1), |
| MAKE_RATE(144000, 0, 3, 1), |
| MAKE_RATE(192000, 0, 4, 1), |
| // 44.1 KHz family |
| MAKE_RATE(11025, 1, 1, 4), |
| MAKE_RATE(22050, 1, 1, 2), |
| MAKE_RATE(44100, 1, 1, 1), |
| MAKE_RATE(88200, 1, 2, 1), |
| MAKE_RATE(176400, 1, 4, 1), |
| }; |
| |
| for (const auto& r : RATE_ENCODINGS) { |
| if (r.rate == fmt.frames_per_second) { |
| *encoded_fmt_out = static_cast<uint16_t>(r.encoded | channels | (bits << 4)); |
| return ZX_OK; |
| } |
| } |
| |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| #undef MAKE_RATE |
| |
| ///////////////////////////////////////////////////////////////////// |
| // |
| // Default handlers |
| // |
| ///////////////////////////////////////////////////////////////////// |
| zx_status_t IntelHDAStreamBase::OnActivateLocked() { return ZX_OK; } |
| |
| void IntelHDAStreamBase::OnDeactivateLocked() {} |
| void IntelHDAStreamBase::OnChannelDeactivateLocked(const StreamChannel& channel) {} |
| |
| zx_status_t IntelHDAStreamBase::OnDMAAssignedLocked() { return PublishDeviceLocked(); } |
| |
| zx_status_t IntelHDAStreamBase::OnSolicitedResponseLocked(const CodecResponse& resp) { |
| return ZX_OK; |
| } |
| |
| zx_status_t IntelHDAStreamBase::OnUnsolicitedResponseLocked(const CodecResponse& resp) { |
| return ZX_OK; |
| } |
| |
| zx_status_t IntelHDAStreamBase::BeginChangeStreamFormatLocked( |
| const audio_proto::StreamSetFmtReq& fmt) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t IntelHDAStreamBase::FinishChangeStreamFormatLocked(uint16_t encoded_fmt) { |
| return ZX_ERR_INTERNAL; |
| } |
| |
| void IntelHDAStreamBase::OnGetGainLocked(audio_proto::GainState* out_resp) { |
| ZX_DEBUG_ASSERT(out_resp != nullptr); |
| |
| // By default we claim to have a fixed, un-mute-able gain stage. |
| out_resp->cur_mute = false; |
| out_resp->cur_agc = false; |
| out_resp->cur_gain = 0.0; |
| |
| out_resp->can_mute = false; |
| out_resp->can_agc = false; |
| out_resp->min_gain = 0.0; |
| out_resp->max_gain = 0.0; |
| out_resp->gain_step = 0.0; |
| } |
| |
| void IntelHDAStreamBase::OnSetGainLocked(const audio_proto::SetGainReq& req, |
| audio_proto::SetGainResp* out_resp) { |
| // Nothing to do if no response is expected. |
| if (out_resp == nullptr) { |
| ZX_DEBUG_ASSERT(req.hdr.cmd & AUDIO_FLAG_NO_ACK); |
| return; |
| } |
| |
| bool illegal_mute = (req.flags & AUDIO_SGF_MUTE_VALID) && (req.flags & AUDIO_SGF_MUTE); |
| bool illegal_agc = (req.flags & AUDIO_SGF_AGC_VALID) && (req.flags & AUDIO_SGF_AGC); |
| bool illegal_gain = (req.flags & AUDIO_SGF_GAIN_VALID) && (req.gain != 0.0f); |
| |
| out_resp->cur_mute = false; |
| out_resp->cur_gain = 0.0; |
| out_resp->result = (illegal_mute || illegal_agc || illegal_gain) ? ZX_ERR_INVALID_ARGS : ZX_OK; |
| } |
| |
| void IntelHDAStreamBase::OnPlugDetectLocked(StreamChannel* response_channel, |
| audio_proto::PlugDetectResp* out_resp) { |
| // Nothing to do if no response is expected. |
| if (out_resp == nullptr) { |
| return; |
| } |
| |
| ZX_DEBUG_ASSERT(parent_codec_ != nullptr); |
| out_resp->flags = static_cast<audio_pd_notify_flags_t>(AUDIO_PDNF_HARDWIRED | AUDIO_PDNF_PLUGGED); |
| out_resp->plug_state_time = parent_codec_->create_time(); |
| } |
| |
| void IntelHDAStreamBase::OnGetStringLocked(const audio_proto::GetStringReq& req, |
| audio_proto::GetStringResp* out_resp) { |
| ZX_DEBUG_ASSERT(out_resp); |
| |
| switch (req.id) { |
| case AUDIO_STREAM_STR_ID_MANUFACTURER: |
| case AUDIO_STREAM_STR_ID_PRODUCT: { |
| int res = |
| snprintf(reinterpret_cast<char*>(out_resp->str), sizeof(out_resp->str), "<unknown>"); |
| ZX_DEBUG_ASSERT(res >= 0); // there should be no way for snprintf to fail here. |
| out_resp->strlen = std::min<uint32_t>(res, sizeof(out_resp->str) - 1); |
| out_resp->result = ZX_OK; |
| break; |
| } |
| |
| default: |
| out_resp->strlen = 0; |
| out_resp->result = ZX_ERR_NOT_FOUND; |
| break; |
| } |
| } |
| |
| void IntelHDAStreamBase::OnGetClockDomainLocked(audio_proto::GetClockDomainResp* out_resp) { |
| ZX_DEBUG_ASSERT(out_resp != nullptr); |
| |
| // By default we claim to be in the MONOTONIC clock domain. |
| // TODO(mpuryear): if the audio clock might possibly ever be in a different domain than the local |
| // system clock (either because it is trimmable [unlikely] or uses a different oscillator [even |
| // less likely]), handle that case here. |
| out_resp->clock_domain = 0; |
| } |
| |
| } // namespace codecs |
| } // namespace intel_hda |
| } // namespace audio |