| // 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 "garnet/bin/media/audio_core/driver_output.h" |
| |
| #include <audio-proto-utils/format-utils.h> |
| #include <dispatcher-pool/dispatcher-channel.h> |
| #include <lib/fit/defer.h> |
| #include <zircon/process.h> |
| #include <iomanip> |
| #include <optional> |
| |
| #include "garnet/bin/media/audio_core/audio_device_manager.h" |
| #include "garnet/lib/media/wav_writer/wav_writer.h" |
| #include "lib/fxl/logging.h" |
| |
| constexpr bool VERBOSE_TIMING_DEBUG = false; |
| |
| namespace media::audio { |
| |
| static constexpr uint32_t kDefaultFramesPerSec = 48000; |
| static constexpr uint32_t kDefaultChannelCount = 2; |
| static constexpr fuchsia::media::AudioSampleFormat kDefaultAudioFmt = |
| fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32; |
| static constexpr int64_t kDefaultLowWaterNsec = ZX_MSEC(20); |
| static constexpr int64_t kDefaultHighWaterNsec = ZX_MSEC(30); |
| static constexpr int64_t kDefaultMaxRetentionNsec = ZX_MSEC(60); |
| static constexpr int64_t kDefaultRetentionGapNsec = ZX_MSEC(10); |
| static constexpr zx_duration_t kUnderflowCooldown = ZX_SEC(1); |
| |
| static std::atomic<zx_txid_t> TXID_GEN(1); |
| static thread_local zx_txid_t TXID = TXID_GEN.fetch_add(1); |
| |
| fbl::RefPtr<AudioOutput> DriverOutput::Create(zx::channel stream_channel, |
| AudioDeviceManager* manager) { |
| return fbl::AdoptRef(new DriverOutput(manager, std::move(stream_channel))); |
| } |
| |
| DriverOutput::DriverOutput(AudioDeviceManager* manager, |
| zx::channel initial_stream_channel) |
| : StandardOutputBase(manager), |
| initial_stream_channel_(std::move(initial_stream_channel)) {} |
| |
| DriverOutput::~DriverOutput() { wav_writer_.Close(); } |
| |
| zx_status_t DriverOutput::Init() { |
| FXL_DCHECK(state_ == State::Uninitialized); |
| |
| zx_status_t res = StandardOutputBase::Init(); |
| if (res != ZX_OK) { |
| return res; |
| } |
| |
| res = driver_->Init(std::move(initial_stream_channel_)); |
| if (res != ZX_OK) { |
| FXL_LOG(ERROR) << "Failed to initialize driver object (res " << res << ")"; |
| return res; |
| } |
| |
| state_ = State::FormatsUnknown; |
| return res; |
| } |
| |
| void DriverOutput::OnWakeup() { |
| // If we are not in the FormatsUnknown state, then we have already started the |
| // state machine. There is (currently) nothing else to do here. |
| FXL_DCHECK(state_ != State::Uninitialized); |
| if (state_ != State::FormatsUnknown) { |
| return; |
| } |
| |
| // Kick off the process of driver configuration by requesting the basic driver |
| // info, which will include the modes which the driver supports. |
| driver_->GetDriverInfo(); |
| state_ = State::FetchingFormats; |
| } |
| |
| bool DriverOutput::StartMixJob(MixJob* job, fxl::TimePoint process_start) { |
| if (state_ != State::Started) { |
| FXL_LOG(ERROR) << "Bad state during StartMixJob " |
| << static_cast<uint32_t>(state_); |
| state_ = State::Shutdown; |
| ShutdownSelf(); |
| return false; |
| } |
| |
| // TODO(johngro): Depending on policy, use send appropriate commands to the |
| // driver to control gain as well. Some policy settings which might be useful |
| // include... |
| // |
| // ++ Never use HW gain, even if it supports it. |
| // ++ Always use HW gain when present, regardless of its limitations. |
| // ++ Use HW gain when present, but only if it reaches a minimum bar of |
| // functionality. |
| // ++ Implement a hybrid of HW/SW gain. IOW - Get as close as possible to our |
| // target using HW, and then get the rest of the way there using SW |
| // scaling. This approach may end up being unreasonably tricky as we may |
| // not be able to synchronize the HW and SW changes in gain well enough to |
| // avoid strange situations where the jumps in one direction (because of |
| // the SW component), and then in the other (as the HW gain command takes |
| // effect). |
| // |
| if (device_settings_ != nullptr) { |
| AudioDeviceSettings::GainState cur_gain_state; |
| device_settings_->SnapshotGainState(&cur_gain_state); |
| job->sw_output_gain_db = cur_gain_state.gain_db; |
| job->sw_output_muted = cur_gain_state.muted; |
| } else { |
| job->sw_output_gain_db = Gain::kUnityGainDb; |
| // TODO(mpuryear): make this false, consistent w/audio_device_settings.h? |
| job->sw_output_muted = true; |
| } |
| |
| FXL_DCHECK(driver_ring_buffer() != nullptr); |
| int64_t now = process_start.ToEpochDelta().ToNanoseconds(); |
| const auto& cm2rd_pos = clock_mono_to_ring_buf_pos_frames_; |
| const auto& cm2frames = cm2rd_pos.rate(); |
| const auto& rb = *driver_ring_buffer(); |
| uint32_t fifo_frames = driver_->fifo_depth_frames(); |
| |
| // If frames_to_mix_ is 0, then this is the start of a new cycle. Check to |
| // make sure we have not underflowed while we were sleeping, then compute how |
| // many frames we need to mix during this wakeup cycle, and return a job |
| // containing the largest contiguous buffer we can mix during this phase of |
| // this cycle. |
| if (!frames_to_mix_) { |
| int64_t rd_ptr_frames = cm2rd_pos.Apply(now); |
| int64_t fifo_threshold = rd_ptr_frames + fifo_frames; |
| |
| if (fifo_threshold >= frames_sent_) { |
| if (!underflow_start_time_) { |
| // If this was the first time we missed our limit, log a message, mark |
| // the start time of the underflow event, and fill our entire ring |
| // buffer with silence. |
| int64_t rd_limit_miss = rd_ptr_frames - frames_sent_; |
| int64_t fifo_limit_miss = rd_limit_miss + fifo_frames; |
| int64_t low_water_limit_miss = rd_limit_miss + low_water_frames_; |
| |
| FXL_LOG(ERROR) |
| << "UNDERFLOW: Missed mix target by (Rd, Fifo, LowWater) = (" |
| << std::fixed << std::setprecision(3) |
| << cm2frames.Inverse().Scale(rd_limit_miss) / 1000000.0 << ", " |
| << cm2frames.Inverse().Scale(fifo_limit_miss) / 1000000.0 << ", " |
| << cm2frames.Inverse().Scale(low_water_limit_miss) / 1000000.0 |
| << ") mSec. Cooling down for at least " |
| << kUnderflowCooldown / 1000000.0 << " mSec."; |
| |
| underflow_start_time_ = now; |
| output_producer_->FillWithSilence(rb.virt(), rb.frames()); |
| zx_cache_flush(rb.virt(), rb.size(), ZX_CACHE_FLUSH_DATA); |
| |
| wav_writer_.Close(); |
| } |
| |
| // Regardless of whether this was the first or a subsequent underflow, |
| // update the cooldown deadline (the time at which we will start producing |
| // frames again, provided we don't underflow again) |
| underflow_cooldown_deadline_ = zx_deadline_after(kUnderflowCooldown); |
| } |
| |
| int64_t fill_target = |
| fifo_frames + cm2rd_pos.Apply(now + kDefaultHighWaterNsec); |
| |
| // Are we in the middle of an underflow cooldown? If so, check to see if we |
| // have recovered yet. |
| if (underflow_start_time_) { |
| if (static_cast<zx_time_t>(now) < underflow_cooldown_deadline_) { |
| // Looks like we have not recovered yet. Pretend to have produced the |
| // frames we were going to produce and schedule the next wakeup time. |
| frames_sent_ = fill_target; |
| ScheduleNextLowWaterWakeup(); |
| return false; |
| } else { |
| // Looks like we recovered. Log and go back to mixing. |
| FXL_LOG(INFO) << "UNDERFLOW: Recovered after " << std::fixed |
| << std::setprecision(3) |
| << (now - underflow_start_time_) / 1000000.0 << " mSec."; |
| underflow_start_time_ = 0; |
| underflow_cooldown_deadline_ = 0; |
| } |
| } |
| |
| int64_t frames_in_flight = frames_sent_ - rd_ptr_frames; |
| FXL_DCHECK((frames_in_flight >= 0) && (frames_in_flight <= rb.frames())); |
| FXL_DCHECK(frames_sent_ <= fill_target); |
| int64_t desired_frames = fill_target - frames_sent_; |
| |
| // If we woke up too early to have any work to do, just get out now. |
| if (desired_frames == 0) { |
| return false; |
| } |
| |
| uint32_t rb_space = rb.frames() - static_cast<uint32_t>(frames_in_flight); |
| if (desired_frames > rb.frames()) { |
| FXL_LOG(ERROR) << "Fatal underflow: want to produce " << desired_frames |
| << " but the ring buffer is only " << rb.frames() |
| << " frames long."; |
| return false; |
| } |
| |
| frames_to_mix_ = |
| static_cast<uint32_t>(fbl::min<int64_t>(rb_space, desired_frames)); |
| } |
| |
| uint32_t to_mix = frames_to_mix_; |
| uint32_t wr_ptr = frames_sent_ % rb.frames(); |
| uint32_t contig_space = rb.frames() - wr_ptr; |
| |
| if (to_mix > contig_space) { |
| to_mix = contig_space; |
| } |
| |
| job->buf = rb.virt() + (rb.frame_size() * wr_ptr); |
| job->buf_frames = to_mix; |
| job->start_pts_of = frames_sent_; |
| job->local_to_output = &cm2rd_pos; |
| job->local_to_output_gen = clock_mono_to_ring_buf_pos_id_.get(); |
| |
| return true; |
| } |
| |
| bool DriverOutput::FinishMixJob(const MixJob& job) { |
| const auto& rb = driver_ring_buffer(); |
| FXL_DCHECK(rb != nullptr); |
| size_t buf_len = job.buf_frames * rb->frame_size(); |
| |
| wav_writer_.Write(job.buf, buf_len); |
| wav_writer_.UpdateHeader(); |
| zx_cache_flush(job.buf, buf_len, ZX_CACHE_FLUSH_DATA); |
| |
| if (VERBOSE_TIMING_DEBUG) { |
| const auto& cm2rd_pos = clock_mono_to_ring_buf_pos_frames_; |
| uint32_t fifo_frames = driver_->fifo_depth_frames(); |
| int64_t now = fxl::TimePoint::Now().ToEpochDelta().ToNanoseconds(); |
| int64_t rd_ptr_frames = cm2rd_pos.Apply(now); |
| int64_t playback_lead_start = frames_sent_ - rd_ptr_frames; |
| int64_t playback_lead_end = playback_lead_start + job.buf_frames; |
| int64_t dma_lead_start = playback_lead_start - fifo_frames; |
| int64_t dma_lead_end = playback_lead_end - fifo_frames; |
| |
| FXL_LOG(INFO) << "PLead [" << std::setw(4) << playback_lead_start << ", " |
| << std::setw(4) << playback_lead_end << "] DLead [" |
| << std::setw(4) << dma_lead_start << ", " << std::setw(4) |
| << dma_lead_end << "]"; |
| } |
| |
| FXL_DCHECK(frames_to_mix_ >= job.buf_frames); |
| frames_sent_ += job.buf_frames; |
| frames_to_mix_ -= job.buf_frames; |
| |
| if (!frames_to_mix_) { |
| ScheduleNextLowWaterWakeup(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void DriverOutput::ApplyGainLimits(::fuchsia::media::AudioGainInfo* in_out_info, |
| uint32_t set_flags) { |
| // See the comment at the start of StartMixJob. The actual limits we set here |
| // are going to eventually depend on what our HW gain control capabilities |
| // are, and how we choose to apply them (based on policy) |
| FXL_DCHECK(in_out_info != nullptr); |
| |
| // We do not currently allow more than unity gain for audio outputs. |
| if (in_out_info->gain_db > 0.0) { |
| in_out_info->gain_db = 0; |
| } |
| |
| // Audio outputs should never support AGC |
| in_out_info->flags &= ~(::fuchsia::media::AudioGainInfoFlag_AgcEnabled); |
| } |
| |
| void DriverOutput::ScheduleNextLowWaterWakeup() { |
| // Schedule next callback for the low water mark behind the write pointer. |
| const auto& cm2rd_pos = clock_mono_to_ring_buf_pos_frames_; |
| int64_t low_water_frames = frames_sent_ - low_water_frames_; |
| int64_t low_water_time = cm2rd_pos.ApplyInverse(low_water_frames); |
| SetNextSchedTime(fxl::TimePoint::FromEpochDelta( |
| fxl::TimeDelta::FromNanoseconds(low_water_time))); |
| } |
| |
| void DriverOutput::OnDriverInfoFetched() { |
| auto cleanup = fit::defer([this]() FXL_NO_THREAD_SAFETY_ANALYSIS { |
| state_ = State::Shutdown; |
| ShutdownSelf(); |
| }); |
| |
| if (state_ != State::FetchingFormats) { |
| FXL_LOG(WARNING) << "Unexpected GetFormatsComplete while in state " |
| << static_cast<uint32_t>(state_); |
| return; |
| } |
| |
| zx_status_t res; |
| |
| // TODO(mpuryear): Use the best driver-supported format, not hardwired default |
| uint32_t pref_fps = kDefaultFramesPerSec; |
| uint32_t pref_chan = kDefaultChannelCount; |
| fuchsia::media::AudioSampleFormat pref_fmt = kDefaultAudioFmt; |
| zx_duration_t min_rb_duration = kDefaultHighWaterNsec + |
| kDefaultMaxRetentionNsec + |
| kDefaultRetentionGapNsec; |
| |
| res = SelectBestFormat(driver_->format_ranges(), &pref_fps, &pref_chan, |
| &pref_fmt); |
| |
| if (res != ZX_OK) { |
| FXL_LOG(ERROR) << "Output: cannot match a driver format to this request: " |
| << pref_fps << " Hz, " << pref_chan |
| << "-channel, sample format 0x" << std::hex |
| << static_cast<uint32_t>(pref_fmt); |
| return; |
| } |
| |
| // TODO(mpuryear): Save to the hub the configured format for this output. |
| |
| TimelineRate ns_to_frames(pref_fps, ZX_SEC(1)); |
| int64_t retention_frames = ns_to_frames.Scale(kDefaultMaxRetentionNsec); |
| FXL_DCHECK(retention_frames != TimelineRate::kOverflow); |
| FXL_DCHECK(retention_frames <= std::numeric_limits<uint32_t>::max()); |
| driver_->SetEndFenceToStartFenceFrames( |
| static_cast<uint32_t>(retention_frames)); |
| |
| // Select our output producer |
| fuchsia::media::AudioStreamTypePtr config( |
| fuchsia::media::AudioStreamType::New()); |
| config->frames_per_second = pref_fps; |
| config->channels = pref_chan; |
| config->sample_format = pref_fmt; |
| |
| output_producer_ = OutputProducer::Select(config); |
| if (!output_producer_) { |
| FXL_LOG(ERROR) << "Output: OutputProducer cannot support this request: " |
| << pref_fps << " Hz, " << pref_chan |
| << "-channel, sample format 0x" << std::hex |
| << static_cast<uint32_t>(pref_fmt); |
| return; |
| } |
| |
| // Start the process of configuring our driver |
| res = driver_->Configure(pref_fps, pref_chan, pref_fmt, min_rb_duration); |
| if (res != ZX_OK) { |
| FXL_LOG(ERROR) << "Output: failed to configure driver for: " << pref_fps |
| << " Hz, " << pref_chan << "-channel, sample format 0x" |
| << std::hex << static_cast<uint32_t>(pref_fmt) << " (res " |
| << std::dec << res << ")"; |
| return; |
| } |
| |
| wav_writer_.Initialize(nullptr, pref_fmt, pref_chan, pref_fps, |
| driver_->bytes_per_frame() * 8 / pref_chan); |
| |
| // Tell AudioDeviceManager we are ready to be an active audio device. |
| ActivateSelf(); |
| |
| // Success; now wait until configuration completes. |
| state_ = State::Configuring; |
| cleanup.cancel(); |
| } |
| |
| void DriverOutput::OnDriverConfigComplete() { |
| auto cleanup = fit::defer([this]() FXL_NO_THREAD_SAFETY_ANALYSIS { |
| state_ = State::Shutdown; |
| ShutdownSelf(); |
| }); |
| |
| if (state_ != State::Configuring) { |
| FXL_LOG(ERROR) << "Unexpected ConfigComplete while in state " |
| << static_cast<uint32_t>(state_); |
| return; |
| } |
| |
| // Now that our driver is completely configured, we have all the info needed |
| // to compute the minimum clock lead time requrirement for this output. |
| int64_t fifo_depth_nsec = TimelineRate::Scale( |
| driver_->fifo_depth_frames(), ZX_SEC(1), driver_->frames_per_sec()); |
| min_clock_lead_time_nsec_ = |
| driver_->external_delay_nsec() + fifo_depth_nsec + kDefaultHighWaterNsec; |
| |
| // Fill our brand new ring buffer with silence |
| FXL_CHECK(driver_ring_buffer() != nullptr); |
| const auto& rb = *driver_ring_buffer(); |
| FXL_DCHECK(output_producer_ != nullptr); |
| FXL_DCHECK(rb.virt() != nullptr); |
| output_producer_->FillWithSilence(rb.virt(), rb.frames()); |
| |
| // Set up the intermediate buffer at the StandardOutputBase level |
| // |
| // TODO(johngro): The intermediate buffer probably does not need to be as |
| // large as the entire ring buffer. Consider limiting this to be something |
| // only slightly larger than a nominal mix job. |
| SetupMixBuffer(rb.frames()); |
| |
| // Start the ring buffer running |
| // |
| // TODO(johngro) : Don't actually start things up here. We should start only |
| // when we have clients with work to do, and we should stop when we have no |
| // work to do. See MTWN-5 |
| zx_status_t res = driver_->Start(); |
| if (res != ZX_OK) { |
| FXL_LOG(ERROR) << "Failed to start ring buffer (res = " << res << ")"; |
| return; |
| } |
| |
| // Start monitoring plug state. |
| res = driver_->SetPlugDetectEnabled(true); |
| if (res != ZX_OK) { |
| FXL_LOG(ERROR) << "Failed to enable plug detection (res = " << res << ")"; |
| return; |
| } |
| |
| // Success |
| state_ = State::Starting; |
| cleanup.cancel(); |
| } |
| |
| void DriverOutput::OnDriverStartComplete() { |
| if (state_ != State::Starting) { |
| FXL_LOG(ERROR) << "Unexpected StartComplete while in state " |
| << static_cast<uint32_t>(state_); |
| return; |
| } |
| |
| // Compute the transformation from clock mono to the ring buffer read position |
| // in frames, rounded up. Then compute our low water mark (in frames) and |
| // where we want to start mixing. Finally kick off the mixing engine by |
| // manually calling Process. |
| uint32_t bytes_per_frame = driver_->bytes_per_frame(); |
| int64_t offset = static_cast<int64_t>(1) - bytes_per_frame; |
| const TimelineFunction bytes_to_frames(0, offset, 1, bytes_per_frame); |
| const TimelineFunction& t_bytes = driver_clock_mono_to_ring_pos_bytes(); |
| |
| clock_mono_to_ring_buf_pos_frames_ = |
| TimelineFunction::Compose(bytes_to_frames, t_bytes); |
| clock_mono_to_ring_buf_pos_id_.Next(); |
| |
| const TimelineFunction& trans = clock_mono_to_ring_buf_pos_frames_; |
| uint32_t fd_frames = driver_->fifo_depth_frames(); |
| low_water_frames_ = fd_frames + trans.rate().Scale(kDefaultLowWaterNsec); |
| frames_sent_ = low_water_frames_; |
| frames_to_mix_ = 0; |
| |
| if (VERBOSE_TIMING_DEBUG) { |
| FXL_LOG(INFO) << "Audio output: FIFO depth (" << fd_frames << " frames " |
| << std::fixed << std::setprecision(3) |
| << trans.rate().Inverse().Scale(fd_frames) / 1000000.0 |
| << " mSec) Low Water (" << low_water_frames_ << " frames " |
| << std::fixed << std::setprecision(3) |
| << trans.rate().Inverse().Scale(low_water_frames_) / 1000000.0 |
| << " mSec)"; |
| } |
| |
| state_ = State::Started; |
| Process(); |
| } |
| |
| void DriverOutput::OnDriverPlugStateChange(bool plugged, zx_time_t plug_time) { |
| // Reflect this message to the AudioDeviceManager so it can deal with the plug |
| // state change. |
| manager_->ScheduleMainThreadTask([manager = manager_, |
| output = fbl::WrapRefPtr(this), plugged, |
| plug_time]() { |
| manager->HandlePlugStateChange(output, plugged, plug_time); |
| }); |
| } |
| |
| } // namespace media::audio |