blob: b4ac86136c1ff9f34f8b8bb3928f89f5d48e3f15 [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 "src/media/audio/audio_core/driver_output.h"
#include <lib/async/cpp/time.h>
#include <lib/fit/defer.h>
#include <lib/zx/clock.h>
#include <algorithm>
#include <iomanip>
#include <trace/event.h>
#include "src/media/audio/audio_core/reporter.h"
constexpr bool VERBOSE_TIMING_DEBUG = false;
namespace media::audio {
static constexpr uint32_t kDefaultChannelCount = 2;
static constexpr fuchsia::media::AudioSampleFormat kDefaultAudioFmt =
fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32;
static constexpr zx::duration kDefaultMaxRetentionNsec = zx::msec(60);
static constexpr zx::duration kDefaultRetentionGapNsec = zx::msec(10);
static constexpr zx::duration kUnderflowCooldown = zx::msec(1000);
static std::atomic<zx_txid_t> TXID_GEN(1);
static thread_local zx_txid_t TXID = TXID_GEN.fetch_add(1);
// Consts used if kEnableFinalMixWavWriter is set:
//
// This atomic is only used when the final-mix wave-writer is enabled --
// specifically to generate unique ids for each final-mix WAV file.
std::atomic<uint32_t> DriverOutput::final_mix_instance_num_(0u);
// WAV file location: FilePathName+final_mix_instance_num_+FileExtension
constexpr const char* kDefaultWavFilePathName = "/tmp/final_mix_";
constexpr const char* kWavFileExtension = ".wav";
std::shared_ptr<AudioOutput> DriverOutput::Create(zx::channel stream_channel,
ThreadingModel* threading_model,
DeviceRegistry* registry,
LinkMatrix* link_matrix) {
return std::make_shared<DriverOutput>(threading_model, registry, std::move(stream_channel),
link_matrix);
}
DriverOutput::DriverOutput(ThreadingModel* threading_model, DeviceRegistry* registry,
zx::channel initial_stream_channel, LinkMatrix* link_matrix)
: AudioOutput(threading_model, registry, link_matrix),
initial_stream_channel_(std::move(initial_stream_channel)) {}
DriverOutput::~DriverOutput() { wav_writer_.Close(); }
zx_status_t DriverOutput::Init() {
TRACE_DURATION("audio", "DriverOutput::Init");
FX_DCHECK(state_ == State::Uninitialized);
zx_status_t res = AudioOutput::Init();
if (res != ZX_OK) {
return res;
}
res = driver()->Init(std::move(initial_stream_channel_));
if (res != ZX_OK) {
FX_PLOGS(ERROR, res) << "Failed to initialize driver object";
return res;
}
state_ = State::FormatsUnknown;
return res;
}
void DriverOutput::OnWakeup() {
TRACE_DURATION("audio", "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.
FX_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;
}
std::optional<MixStage::FrameSpan> DriverOutput::StartMixJob(zx::time uptime) {
TRACE_DURATION("audio", "DriverOutput::StartMixJob");
if (state_ != State::Started) {
FX_LOGS(ERROR) << "Bad state during StartMixJob " << static_cast<uint32_t>(state_);
state_ = State::Shutdown;
ShutdownSelf();
return std::nullopt;
}
// TODO(mpuryear): 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).
//
bool output_muted = true;
const auto& settings = device_settings();
if (settings != nullptr) {
AudioDeviceSettings::GainState cur_gain_state;
settings->SnapshotGainState(&cur_gain_state);
output_muted = cur_gain_state.muted;
}
FX_DCHECK(driver_ring_buffer() != nullptr);
const auto& clock_monotonic_to_output_frame = clock_monotonic_to_output_frame_;
const auto& output_frames_per_monotonic_tick = clock_monotonic_to_output_frame.rate();
const auto& rb = *driver_ring_buffer();
uint32_t fifo_frames = driver()->fifo_depth_frames();
// output_frames_consumed is the number of frames that the audio output device has read so far.
// output_frames_emitted is the slightly-smaller number of frames that have physically exited
// the device itself (the number of frames that have "made sound" so far);
int64_t output_frames_consumed = clock_monotonic_to_output_frame.Apply(uptime.get());
int64_t output_frames_emitted = output_frames_consumed - fifo_frames;
if (output_frames_consumed >= frames_sent_) {
if (!underflow_start_time_.get()) {
// 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 output_underflow_frames = output_frames_consumed - frames_sent_;
int64_t low_water_frames_underflow = output_underflow_frames + low_water_frames_;
zx::duration output_underflow_duration =
zx::nsec(output_frames_per_monotonic_tick.Inverse().Scale(output_underflow_frames));
FX_CHECK(output_underflow_duration.get() >= 0);
zx::duration output_variance_from_expected_wakeup =
zx::nsec(output_frames_per_monotonic_tick.Inverse().Scale(low_water_frames_underflow));
FX_LOGS(ERROR) << "OUTPUT UNDERFLOW: Missed mix target by (worst-case, expected) = ("
<< std::setprecision(4)
<< static_cast<double>(output_underflow_duration.to_nsecs()) / ZX_MSEC(1)
<< ", " << output_variance_from_expected_wakeup.to_msecs()
<< ") ms. Cooling down for " << kUnderflowCooldown.to_msecs()
<< " milliseconds.";
// Use our Reporter to log this to Cobalt, if enabled.
REPORT(OutputUnderflow(output_underflow_duration, uptime));
underflow_start_time_ = uptime;
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 =
clock_monotonic_to_output_frame.Apply((uptime + kDefaultHighWaterNsec).get()) +
driver()->fifo_depth_frames();
// Are we in the middle of an underflow cooldown? If so, check whether we have recovered yet.
if (underflow_start_time_.get()) {
if (uptime < 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 std::nullopt;
} else {
// Looks like we recovered. Log and go back to mixing.
FX_LOGS(WARNING) << "OUTPUT UNDERFLOW: Recovered after "
<< (uptime - underflow_start_time_).to_msecs() << " ms.";
underflow_start_time_ = zx::time(0);
underflow_cooldown_deadline_ = zx::time(0);
}
}
int64_t frames_in_flight = frames_sent_ - output_frames_emitted;
FX_DCHECK((frames_in_flight >= 0) && (frames_in_flight <= rb.frames()));
FX_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 std::nullopt;
}
uint32_t rb_space = rb.frames() - static_cast<uint32_t>(frames_in_flight);
if (desired_frames > rb.frames()) {
FX_LOGS(ERROR) << "OUTPUT UNDERFLOW: want to produce " << desired_frames
<< " but the ring buffer is only " << rb.frames() << " frames long.";
return std::nullopt;
}
uint32_t frames_to_mix = static_cast<uint32_t>(std::min<int64_t>(rb_space, desired_frames));
auto frames = MixStage::FrameSpan{
.start = frames_sent_,
.length = frames_to_mix,
};
// If we're muted we simply fill the ring buffer with silence.
if (output_muted) {
FillRingWithSilence(frames);
ScheduleNextLowWaterWakeup();
return std::nullopt;
}
return {frames};
}
void DriverOutput::WriteToRing(
const MixStage::FrameSpan& span,
fit::function<void(uint64_t offset, uint32_t length, void* dest_buf)> writer) {
TRACE_DURATION("audio", "DriverOutput::FinishMixJob");
const auto& rb = driver_ring_buffer();
FX_DCHECK(rb != nullptr);
size_t frames_left = span.length;
size_t offset = 0;
while (frames_left > 0) {
uint32_t wr_ptr = (span.start + offset) % rb->frames();
uint32_t contig_space = rb->frames() - wr_ptr;
uint32_t to_send = frames_left;
if (to_send > contig_space) {
to_send = contig_space;
}
void* dest_buf = rb->virt() + (rb->frame_size() * wr_ptr);
writer(offset, to_send, dest_buf);
frames_left -= to_send;
offset += to_send;
}
frames_sent_ += offset;
}
void DriverOutput::FinishMixJob(const MixStage::FrameSpan& span, float* buffer) {
TRACE_DURATION("audio", "DriverOutput::FinishMixJob");
WriteToRing(span, [this, buffer](uint64_t offset, uint32_t frames, void* dest_buf) {
auto job_buf_offset = offset * output_producer_->channels();
output_producer_->ProduceOutput(buffer + job_buf_offset, dest_buf, frames);
size_t dest_buf_len = frames * output_producer_->bytes_per_frame();
wav_writer_.Write(dest_buf, dest_buf_len);
wav_writer_.UpdateHeader();
zx_cache_flush(dest_buf, dest_buf_len, ZX_CACHE_FLUSH_DATA);
});
if (VERBOSE_TIMING_DEBUG) {
auto now = async::Now(mix_domain().dispatcher());
int64_t output_frames_consumed = clock_monotonic_to_output_frame_.Apply(now.get());
int64_t playback_lead_start = frames_sent_ - output_frames_consumed;
int64_t playback_lead_end = playback_lead_start + span.length;
FX_LOGS(INFO) << "PLead [" << std::setw(4) << playback_lead_start << ", " << std::setw(4)
<< playback_lead_end << "]";
}
ScheduleNextLowWaterWakeup();
}
void DriverOutput::FillRingWithSilence(const MixStage::FrameSpan& span) {
WriteToRing(span, [this](auto offset, auto frames, auto dest_buf) {
output_producer_->FillWithSilence(dest_buf, frames);
});
}
void DriverOutput::ApplyGainLimits(fuchsia::media::AudioGainInfo* in_out_info, uint32_t set_flags) {
TRACE_DURATION("audio", "DriverOutput::ApplyGainLimits");
// 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)
FX_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() {
TRACE_DURATION("audio", "DriverOutput::ScheduleNextLowWaterWakeup");
// Schedule next callback for the low water mark behind the write pointer.
const auto& reference_clock_to_ring_buffer_frame = clock_monotonic_to_output_frame_;
int64_t low_water_frames = frames_sent_ - low_water_frames_;
int64_t low_water_time = reference_clock_to_ring_buffer_frame.ApplyInverse(low_water_frames);
SetNextSchedTime(zx::time(low_water_time));
}
void DriverOutput::OnDriverInfoFetched() {
TRACE_DURATION("audio", "DriverOutput::OnDriverInfoFetched");
auto cleanup = fit::defer([this]() FXL_NO_THREAD_SAFETY_ANALYSIS {
state_ = State::Shutdown;
ShutdownSelf();
});
if (state_ != State::FetchingFormats) {
FX_LOGS(ERROR) << "Unexpected GetFormatsComplete while in state "
<< static_cast<uint32_t>(state_);
return;
}
zx_status_t res;
pipeline_config_ = {ProcessConfig::instance()
.device_config()
.output_device_profile(driver()->persistent_unique_id())
.pipeline_config()};
uint32_t pref_fps = pipeline_config_->root().output_rate;
uint32_t pref_chan = kDefaultChannelCount;
fuchsia::media::AudioSampleFormat pref_fmt = kDefaultAudioFmt;
zx::duration min_rb_duration =
kDefaultHighWaterNsec + kDefaultMaxRetentionNsec + kDefaultRetentionGapNsec;
res = SelectBestFormat(driver()->format_ranges(), &pref_fps, &pref_chan, &pref_fmt);
if (res != ZX_OK) {
FX_LOGS(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.
auto format_result = Format::Create(fuchsia::media::AudioStreamType{
.sample_format = pref_fmt,
.channels = pref_chan,
.frames_per_second = pref_fps,
});
if (format_result.is_error()) {
FX_LOGS(ERROR) << "Driver format is invalid";
return;
}
auto& format = format_result.value();
// Update our pipeline to produce audio in the compatible format.
if (pipeline_config_->root().output_rate != pref_fps) {
FX_LOGS(WARNING) << "Hardware does not support the requested rate of "
<< pipeline_config_->root().output_rate << " fps; hardware will run at "
<< pref_fps << " fps";
pipeline_config_->mutable_root().output_rate = pref_fps;
}
// Select our output producer
output_producer_ = OutputProducer::Select(format.stream_type());
if (!output_producer_) {
FX_LOGS(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(format, min_rb_duration);
if (res != ZX_OK) {
FX_LOGS(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;
}
if constexpr (kEnableFinalMixWavWriter) {
std::string file_name_ = kDefaultWavFilePathName;
uint32_t instance_count = final_mix_instance_num_.fetch_add(1);
file_name_ += (std::to_string(instance_count) + kWavFileExtension);
wav_writer_.Initialize(file_name_.c_str(), pref_fmt, pref_chan, pref_fps,
format.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() {
TRACE_DURATION("audio", "DriverOutput::OnDriverConfigComplete");
auto cleanup = fit::defer([this]() FXL_NO_THREAD_SAFETY_ANALYSIS {
state_ = State::Shutdown;
ShutdownSelf();
});
if (state_ != State::Configuring) {
FX_LOGS(ERROR) << "Unexpected ConfigComplete while in state " << static_cast<uint32_t>(state_);
return;
}
// Driver is configured, we have all the needed info to compute minimum lead time for this output.
SetMinLeadTime(driver()->external_delay() + driver()->fifo_depth_duration() +
kDefaultHighWaterNsec);
// Fill our brand new ring buffer with silence
FX_CHECK(driver_ring_buffer() != nullptr);
const auto& rb = *driver_ring_buffer();
FX_DCHECK(output_producer_ != nullptr);
FX_DCHECK(rb.virt() != nullptr);
output_producer_->FillWithSilence(rb.virt(), rb.frames());
// Start the ring buffer running
//
// TODO(13292) : 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.
zx_status_t res = driver()->Start();
if (res != ZX_OK) {
FX_PLOGS(ERROR, res) << "Failed to start ring buffer";
return;
}
// Start monitoring plug state.
res = driver()->SetPlugDetectEnabled(true);
if (res != ZX_OK) {
FX_PLOGS(ERROR, res) << "Failed to enable plug detection";
return;
}
// Success
state_ = State::Starting;
cleanup.cancel();
}
void DriverOutput::OnDriverStartComplete() {
TRACE_DURATION("audio", "DriverOutput::OnDriverStartComplete");
if (state_ != State::Starting) {
FX_LOGS(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.
auto format = driver()->GetFormat();
FX_CHECK(format);
uint32_t bytes_per_frame = format->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);
TimelineFunction t_bytes = device_reference_clock_to_ring_pos_bytes();
clock_monotonic_to_output_frame_ = TimelineFunction::Compose(bytes_to_frames, t_bytes);
clock_monotonic_to_output_frame_generation_.Next();
// Set up the mix task in the AudioOutput.
//
// TODO(39886): 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.
TimelineRate fractional_frames_per_frame =
TimelineRate(FractionalFrames<uint32_t>(1).raw_value());
auto reference_clock_to_fractional_frame = TimelineFunction::Compose(
TimelineFunction(fractional_frames_per_frame), clock_monotonic_to_output_frame_);
FX_DCHECK(pipeline_config_);
SetupMixTask(*pipeline_config_, format->channels(), driver_ring_buffer()->frames(),
reference_clock_to_fractional_frame);
const TimelineFunction& trans = clock_monotonic_to_output_frame_;
uint32_t fd_frames = driver()->fifo_depth_frames();
low_water_frames_ = fd_frames + trans.rate().Scale(kDefaultLowWaterNsec.get());
frames_sent_ = low_water_frames_;
if (VERBOSE_TIMING_DEBUG) {
FX_LOGS(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();
}
} // namespace media::audio