blob: ba0f0c6e7e801c4984c3cc88ce4ce7a1feec4067 [file] [log] [blame]
// Copyright 2020 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/audio_renderer.h"
#include <fuchsia/media/audio/cpp/fidl.h>
#include <lib/fit/defer.h>
#include <lib/syslog/cpp/macros.h>
#include <string>
#include "src/media/audio/audio_core/audio_admin.h"
#include "src/media/audio/audio_core/reporter.h"
#include "src/media/audio/audio_core/stream_usage.h"
#include "src/media/audio/audio_core/stream_volume_manager.h"
#include "src/media/audio/lib/clock/utils.h"
namespace media::audio {
AudioRenderer::AudioRenderer(
fidl::InterfaceRequest<fuchsia::media::AudioRenderer> audio_renderer_request, Context* context)
: BaseRenderer(std::move(audio_renderer_request), context) {
context->volume_manager().AddStream(this);
reporter().SetUsage(RenderUsageFromFidlRenderUsage(usage_));
}
AudioRenderer::~AudioRenderer() {
AudioRenderer::ReportStop();
context().volume_manager().RemoveStream(this);
}
void AudioRenderer::OnLinkAdded() {
// With a link, our Mixer and Gain objects have been created, so we can set initial gain levels.
context().volume_manager().NotifyStreamChanged(this);
BaseRenderer::OnLinkAdded();
}
void AudioRenderer::Shutdown() {
BaseRenderer::Shutdown();
gain_control_bindings_.CloseAll();
}
void AudioRenderer::ReportStart() {
BaseRenderer::ReportStart();
context().audio_admin().UpdateRendererState(usage_, true, this);
}
void AudioRenderer::ReportStop() {
BaseRenderer::ReportStop();
context().audio_admin().UpdateRendererState(usage_, false, this);
}
void AudioRenderer::SetUsage(fuchsia::media::AudioRenderUsage usage) {
TRACE_DURATION("audio", "AudioRenderer::SetUsage");
if (format_) {
FX_LOGS(WARNING) << "SetUsage called after SetPcmStreamType.";
context().route_graph().RemoveRenderer(*this);
return;
}
reporter().SetUsage(RenderUsageFromFidlRenderUsage(usage));
usage_ = usage;
}
// If received clock is null, use our adjustable clock. Else, use this new clock. Fail/disconnect,
// if the client-submitted clock has insufficient rights. Strip off other rights such as WRITE.
void AudioRenderer::SetReferenceClock(zx::clock ref_clock) {
TRACE_DURATION("audio", "AudioRenderer::SetReferenceClock");
auto cleanup = fit::defer([this]() { context().route_graph().RemoveRenderer(*this); });
// Lock after storing |cleanup| to ensure |mutex_| is released upon function return, rather than
// |cleanup| completion.
std::lock_guard<std::mutex> lock(mutex_);
// We cannot change the reference clock, once it is set. Also, calling `SetPcmStreamType` will
// automatically sets the default reference clock, if one has not been explicitly set.
if (reference_clock_is_set_) {
FX_LOGS(WARNING) << "Attempted to change reference clock after setting it.";
return;
}
zx_status_t status;
if (ref_clock.is_valid()) {
status = SetCustomReferenceClock(std::move(ref_clock));
} else {
status = SetAdjustableReferenceClock();
}
if (status != ZX_OK) {
return;
}
reference_clock_is_set_ = true;
cleanup.cancel();
}
void AudioRenderer::SetPcmStreamType(fuchsia::media::AudioStreamType stream_type) {
TRACE_DURATION("audio", "AudioRenderer::SetPcmStreamType");
std::lock_guard<std::mutex> lock(mutex_);
auto cleanup = fit::defer([this]() { context().route_graph().RemoveRenderer(*this); });
// We cannot change the format while we are currently operational
if (IsOperating()) {
FX_LOGS(WARNING) << "Attempted to set format while in operational mode.";
return;
}
auto format_result = Format::Create(stream_type);
if (format_result.is_error()) {
FX_LOGS(WARNING) << "AudioRenderer: PcmStreamType is invalid";
return;
}
format_ = {format_result.take_value()};
reporter().SetFormat(*format_);
context().route_graph().SetRendererRoutingProfile(
*this, {.routable = true, .usage = StreamUsage::WithRenderUsage(usage_)});
// Once we route the renderer, we accept the default reference clock if one hasn't yet been set.
reference_clock_is_set_ = true;
// Things went well, cancel the cleanup hook. If our config had been validated previously, it will
// have to be revalidated as we move into the operational phase of our life.
InvalidateConfiguration();
cleanup.cancel();
}
// To eliminate audible pops from discontinuity-on-immediate-start, ramp up from silence.
constexpr bool kEnableRampUpOnPlay = true;
constexpr float kInitialRampUpGainDb = fuchsia::media::audio::MUTED_GAIN_DB;
constexpr zx::duration kRampUpOnPlayDuration = zx::msec(5);
// To eliminate audible pops from discontinuity-on-pause, ramp down to silence, then pause.
constexpr bool kEnableRampDownOnPause = true;
constexpr float kFinalRampDownGainDb = fuchsia::media::audio::MUTED_GAIN_DB;
constexpr zx::duration kRampDownOnPauseDuration = zx::msec(5);
constexpr zx::duration kAdditionalDelayBeforePauseDuration = zx::msec(5);
void AudioRenderer::PlayInternal(zx::time reference_time, zx::time media_time,
PlayCallback callback) {
if constexpr (kEnableRampUpOnPlay) {
// As a workaround until time-stamped Play/Pause/Gain commands, start a ramp-up then call Play.
// Set gain to silent, before starting the ramp-up to current val.
PostStreamGainMute({kInitialRampUpGainDb,
GainRamp{stream_gain_db_, kRampUpOnPlayDuration,
fuchsia::media::audio::RampType::SCALE_LINEAR},
std::nullopt});
}
BaseRenderer::PlayInternal(reference_time, media_time, std::move(callback));
}
void AudioRenderer::PauseInternal(PauseCallback callback) {
// As a short-term workaround until time-stamped Play/Pause/Gain commands are in place, start the
// ramp-down immediately, and post a delayed task for the actual Pause.
if constexpr (kEnableRampDownOnPause) {
auto prev_gain_db = stream_gain_db_;
// Start ramping to kFinalRampDownGainDb. Post a task to Pause (delayed longer than ramp-down).
// On receiving the Pause callback, restore stream gain to its original value.
// Use internal SetGain/SetGainWithRamp versions, to avoid gain notifications.
PostStreamGainMute({.ramp = GainRamp{kFinalRampDownGainDb, kRampDownOnPauseDuration,
fuchsia::media::audio::RampType::SCALE_LINEAR}});
auto pause_callback = [this, prev_gain_db, client_callback = std::move(callback)](
int64_t ref_time, int64_t media_time) {
if (client_callback != nullptr) {
client_callback(ref_time, media_time);
}
PostStreamGainMute({.gain_db = prev_gain_db});
};
// We add a shared self-reference, in case renderer is unbound before/during the delayed task.
context().threading_model().FidlDomain().PostDelayedTask(
[this, self = shared_from_this(), callback = std::move(pause_callback)]() mutable {
BaseRenderer::PauseInternal(std::move(callback));
},
kRampDownOnPauseDuration + kAdditionalDelayBeforePauseDuration);
} else {
BaseRenderer::PauseInternal(std::move(callback));
}
}
void AudioRenderer::BindGainControl(
fidl::InterfaceRequest<fuchsia::media::audio::GainControl> request) {
TRACE_DURATION("audio", "AudioRenderer::BindGainControl");
gain_control_bindings_.AddBinding(GainControlBinding::Create(this), std::move(request));
}
fuchsia::media::Usage AudioRenderer::GetStreamUsage() const {
return fuchsia::media::Usage::WithRenderUsage(fidl::Clone(usage_));
}
// Set a change to the usage volume+gain_adjustment
void AudioRenderer::RealizeVolume(VolumeCommand volume_command) {
context().link_matrix().ForEachDestLink(
*this, [this, volume_command](LinkMatrix::LinkHandle link) {
FX_CHECK(link.mix_domain) << "Renderer dest link should have a defined mix_domain";
float gain_db = link.loudness_transform->Evaluate<2>({
VolumeValue{volume_command.volume},
GainDbFsValue{volume_command.gain_db_adjustment},
});
// TODO(fxbug.dev/51049) Swap this logging for inspect or other real-time gain observation
FX_LOGS(INFO) << static_cast<const void*>(this) << " (mixer "
<< static_cast<const void*>(link.mixer.get()) << ") "
<< StreamUsage::WithRenderUsage(usage_).ToString() << " dest_gain("
<< (volume_command.ramp.has_value() ? "ramping to " : "") << gain_db
<< "db) = Vol(" << volume_command.volume << ") + GainAdjustment("
<< volume_command.gain_db_adjustment << "db)";
link.mix_domain->PostTask([link, volume_command, gain_db, reporter = &reporter()]() {
auto& gain = link.mixer->bookkeeping().gain;
// Stop any in-progress ramping; use this new ramp or gain_db instead
if (volume_command.ramp.has_value()) {
gain.SetDestGainWithRamp(gain_db, volume_command.ramp->duration,
volume_command.ramp->ramp_type);
} else {
gain.SetDestGain(gain_db);
}
reporter->SetFinalGain(link.mixer->bookkeeping().gain.GetGainDb());
});
});
}
constexpr bool kLogSetGainMuteRampCalls = false;
constexpr bool kLogSetGainMuteRampActions = true;
void AudioRenderer::PostStreamGainMute(StreamGainCommand gain_command) {
context().link_matrix().ForEachDestLink(
*this, [this, gain_command](LinkMatrix::LinkHandle link) mutable {
FX_CHECK(link.mix_domain) << "Renderer dest link should have a defined mix_domain";
if constexpr (kLogSetGainMuteRampActions) {
// TODO(fxbug.dev/51049) Swap this logging for inspect or other real-time gain observation
std::stringstream stream;
stream << static_cast<const void*>(this) << " (mixer "
<< static_cast<const void*>(link.mixer.get()) << ") stream (source) Gain: ";
std::string log_string = stream.str();
if (gain_command.mute.has_value()) {
FX_LOGS(INFO) << log_string << "setting mute to "
<< (gain_command.mute.value() ? "TRUE" : "FALSE");
}
if (gain_command.gain_db.has_value()) {
FX_LOGS(INFO) << log_string << "setting gain to " << gain_command.gain_db.value()
<< " db";
}
if (gain_command.ramp.has_value()) {
FX_LOGS(INFO) << log_string << "ramping gain to " << gain_command.ramp->end_gain_db
<< " db, over " << gain_command.ramp->duration.to_usecs() << " usec";
}
}
link.mix_domain->PostTask([link, gain_command, reporter = &reporter()]() mutable {
auto& gain = link.mixer->bookkeeping().gain;
if (gain_command.mute.has_value()) {
gain.SetSourceMute(gain_command.mute.value());
}
if (gain_command.gain_db.has_value()) {
gain.SetSourceGain(gain_command.gain_db.value());
}
if (gain_command.ramp.has_value()) {
gain.SetSourceGainWithRamp(gain_command.ramp->end_gain_db, gain_command.ramp->duration,
gain_command.ramp->ramp_type);
}
// Potentially post this as a delayed task instead, if there is a ramp....
auto final_gain_db = gain.GetGainDb();
reporter->SetFinalGain(final_gain_db);
});
});
}
// Set the stream gain, in each Renderer -> Output audio path. The Gain object contains multiple
// stages. In playback, renderer gain is pre-mix and hence is "source" gain; the usage gain (or
// output gain, if the mixer topology is single-tier) is "dest" gain.
void AudioRenderer::SetGain(float gain_db) {
TRACE_DURATION("audio", "AudioRenderer::SetGain");
if constexpr (kLogSetGainMuteRampCalls) {
FX_LOGS(INFO) << __FUNCTION__ << "(" << gain_db << " dB)";
}
// Before setting stream_gain_db_, always perform this range check.
if (gain_db > fuchsia::media::audio::MAX_GAIN_DB ||
gain_db < fuchsia::media::audio::MUTED_GAIN_DB || isnan(gain_db)) {
FX_LOGS(WARNING) << "SetGain(" << gain_db << " dB) out of range.";
context().route_graph().RemoveRenderer(*this);
return;
}
PostStreamGainMute({.gain_db = gain_db});
stream_gain_db_ = gain_db;
reporter().SetGain(gain_db);
NotifyGainMuteChanged();
}
// Set a stream gain ramp, in each Renderer -> Output audio path. Renderer gain is pre-mix and
// hence is the Source component in the Gain object.
void AudioRenderer::SetGainWithRamp(float gain_db, int64_t duration_ns,
fuchsia::media::audio::RampType ramp_type) {
TRACE_DURATION("audio", "AudioRenderer::SetGainWithRamp");
if constexpr (kLogSetGainMuteRampCalls) {
FX_LOGS(INFO) << __FUNCTION__ << "(to " << gain_db << " dB over " << duration_ns / 1000
<< " usec)";
}
if (gain_db > fuchsia::media::audio::MAX_GAIN_DB ||
gain_db < fuchsia::media::audio::MUTED_GAIN_DB || isnan(gain_db)) {
FX_LOGS(WARNING) << "SetGainWithRamp(" << gain_db << " dB) out of range.";
context().route_graph().RemoveRenderer(*this);
return;
}
zx::duration duration = zx::nsec(duration_ns);
PostStreamGainMute({.ramp = GainRamp{gain_db, duration, ramp_type}});
stream_gain_db_ = gain_db;
reporter().SetGainWithRamp(gain_db, duration, ramp_type);
// TODO(mpuryear): implement GainControl notifications for gain ramps.
}
// Set a stream mute, in each Renderer -> Output audio path. For now, mute is handled by setting
// gain to a value guaranteed to be silent, but going forward we may pass this thru to the Gain
// object. Renderer gain/mute is pre-mix and hence is the Source component in the Gain object.
void AudioRenderer::SetMute(bool mute) {
TRACE_DURATION("audio", "AudioRenderer::SetMute");
if constexpr (kLogSetGainMuteRampCalls) {
FX_LOGS(INFO) << __FUNCTION__ << "(" << mute << ")";
}
// Only do the work if the request represents a change in state.
FX_LOGS(DEBUG) << " (mute: " << mute << ")";
if (mute_ == mute) {
return;
}
PostStreamGainMute({.mute = mute});
mute_ = mute;
reporter().SetMute(mute);
NotifyGainMuteChanged();
}
void AudioRenderer::NotifyGainMuteChanged() {
TRACE_DURATION("audio", "AudioRenderer::NotifyGainMuteChanged");
// TODO(mpuryear): consider whether GainControl events should be disable-able, like
// MinLeadTime.
FX_LOGS(DEBUG) << " (" << stream_gain_db_ << " dB, mute: " << mute_ << ")";
for (auto& gain_binding : gain_control_bindings_.bindings()) {
gain_binding->events().OnGainMuteChanged(stream_gain_db_, mute_);
}
}
// Implementation of the GainControl FIDL interface. Just forward to AudioRenderer
void AudioRenderer::GainControlBinding::SetGain(float gain_db) {
TRACE_DURATION("audio", "AudioRenderer::SetGain");
owner_->SetGain(gain_db);
}
void AudioRenderer::GainControlBinding::SetGainWithRamp(float gain_db, int64_t duration_ns,
fuchsia::media::audio::RampType ramp_type) {
TRACE_DURATION("audio", "AudioRenderer::SetSourceGainWithRamp");
owner_->SetGainWithRamp(gain_db, duration_ns, ramp_type);
}
void AudioRenderer::GainControlBinding::SetMute(bool mute) {
TRACE_DURATION("audio", "AudioRenderer::SetMute");
owner_->SetMute(mute);
}
} // namespace media::audio