blob: e2461cbe5d4b8180cfef93b744cd36f3a3ecd452 [file] [log] [blame]
// Copyright 2019 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/effects_stage_v1.h"
#include <fbl/algorithm.h>
#include "src/media/audio/audio_core/logging_flags.h"
#include "src/media/audio/audio_core/mixer/intersect.h"
#include "src/media/audio/audio_core/silence_padding_stream.h"
#include "src/media/audio/audio_core/threading_model.h"
#include "src/media/audio/lib/effects_loader/effects_loader_v1.h"
#include "src/media/audio/lib/effects_loader/effects_processor_v1.h"
namespace media::audio {
namespace {
// Maximum frames per preallocated_source_buffer.
// Maximum bytes is 4096 assuming mono with 32bit frames (float).
constexpr int64_t kMaxFramesPerFrameBuffer = 1024;
// We expect our render flags to be the same between StreamUsageMask and the effects bitmask. Both
// are defined as 1u << static_cast<uint32_t>(RenderUsage).
static_assert(StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::BACKGROUND)}).mask() ==
FUCHSIA_AUDIO_EFFECTS_USAGE_BACKGROUND);
static_assert(StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::MEDIA)}).mask() ==
FUCHSIA_AUDIO_EFFECTS_USAGE_MEDIA);
static_assert(StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::INTERRUPTION)}).mask() ==
FUCHSIA_AUDIO_EFFECTS_USAGE_INTERRUPTION);
static_assert(StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::SYSTEM_AGENT)}).mask() ==
FUCHSIA_AUDIO_EFFECTS_USAGE_SYSTEM_AGENT);
static_assert(StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::COMMUNICATION)}).mask() ==
FUCHSIA_AUDIO_EFFECTS_USAGE_COMMUNICATION);
static constexpr uint32_t kSupportedUsageMask =
StreamUsageMask({StreamUsage::WithRenderUsage(RenderUsage::BACKGROUND),
StreamUsage::WithRenderUsage(RenderUsage::MEDIA),
StreamUsage::WithRenderUsage(RenderUsage::INTERRUPTION),
StreamUsage::WithRenderUsage(RenderUsage::SYSTEM_AGENT),
StreamUsage::WithRenderUsage(RenderUsage::COMMUNICATION)})
.mask();
class MultiLibEffectsLoader {
public:
EffectV1 CreateEffectByName(std::string_view lib_name, std::string_view effect_name,
std::string_view instance_name, uint32_t frame_rate,
uint16_t channels_in, uint16_t channels_out,
std::string_view config) {
auto it = std::find_if(holders_.begin(), holders_.end(),
[lib_name](auto& holder) { return holder.lib_name == lib_name; });
if (it == holders_.end()) {
Holder holder;
holder.lib_name = lib_name;
zx_status_t status =
EffectsLoaderV1::CreateWithModule(holder.lib_name.c_str(), &holder.loader);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Effect " << effect_name << " from " << lib_name
<< " unable to be created";
return {};
}
it = holders_.insert(it, std::move(holder));
}
FX_CHECK(it != holders_.end());
return it->loader->CreateEffectByName(effect_name, instance_name, frame_rate, channels_in,
channels_out, config);
}
private:
struct Holder {
std::string lib_name;
std::unique_ptr<EffectsLoaderV1> loader;
};
std::vector<Holder> holders_;
};
} // namespace
// static
std::shared_ptr<EffectsStageV1> EffectsStageV1::Create(
const std::vector<PipelineConfig::EffectV1>& effects, std::shared_ptr<ReadableStream> source,
VolumeCurve volume_curve) {
TRACE_DURATION("audio", "EffectsStageV1::Create");
if (source->format().sample_format() != fuchsia::media::AudioSampleFormat::FLOAT) {
FX_LOGS(ERROR) << "EffectsStageV1 can only be added to streams with FLOAT samples";
return nullptr;
}
auto processor = std::make_unique<EffectsProcessorV1>();
MultiLibEffectsLoader loader;
uint32_t frame_rate = source->format().frames_per_second();
uint16_t channels_in = source->format().channels();
for (const auto& effect_spec : effects) {
uint16_t channels_out = effect_spec.output_channels.value_or(channels_in);
auto effect = loader.CreateEffectByName(effect_spec.lib_name, effect_spec.effect_name,
effect_spec.instance_name, frame_rate, channels_in,
channels_out, effect_spec.effect_config);
if (!effect) {
FX_LOGS(ERROR) << "Unable to create effect '" << effect_spec.effect_name << "' from lib '"
<< effect_spec.lib_name << "'";
return nullptr;
}
zx_status_t status = processor->AddEffect(std::move(effect));
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Unable to add effect '" << effect_spec.effect_name
<< "' from lib '" << effect_spec.lib_name << "'";
return nullptr;
}
channels_in = channels_out;
}
return std::make_shared<EffectsStageV1>(std::move(source), std::move(processor),
std::move(volume_curve));
}
Format ComputeFormat(const Format& source_format, const EffectsProcessorV1& processor) {
return Format::Create(
fuchsia::media::AudioStreamType{
.sample_format = source_format.sample_format(),
.channels = static_cast<uint32_t>(processor.channels_out()),
.frames_per_second = static_cast<uint32_t>(source_format.frames_per_second()),
})
.take_value();
}
EffectsStageV1::EffectsStageV1(std::shared_ptr<ReadableStream> source,
std::unique_ptr<EffectsProcessorV1> effects_processor,
VolumeCurve volume_curve)
: ReadableStream("EffectsStageV1", ComputeFormat(source->format(), *effects_processor)),
source_(SilencePaddingStream::WrapIfNeeded(std::move(source),
Fixed(effects_processor->ring_out_frames()),
/*fractional_gaps_round_down=*/false)),
effects_processor_(std::move(effects_processor)),
volume_curve_(std::move(volume_curve)),
block_size_frames_(effects_processor_->block_size()),
max_batch_size_frames_(
effects_processor_->max_batch_size() > 0
? std::min(effects_processor_->max_batch_size(), kMaxFramesPerFrameBuffer)
: kMaxFramesPerFrameBuffer),
source_buffer_(source_->format(), max_batch_size_frames_) {
// Check constraints.
if (block_size_frames_ > 0 && max_batch_size_frames_ > 0) {
FX_CHECK(max_batch_size_frames_ % block_size_frames_ == 0)
<< "Max batch size " << max_batch_size_frames_ << " must be divisible by "
<< block_size_frames_ << "; original max batch size is "
<< effects_processor_->max_batch_size();
}
// Initialize our lead time. Passing 0 here will resolve to our effect's lead time
// in our |SetPresentationDelay| override.
SetPresentationDelay(zx::duration(0));
}
std::optional<ReadableStream::Buffer> EffectsStageV1::ReadLockImpl(ReadLockContext& ctx,
Fixed dest_frame,
int64_t frame_count) {
// ReadLockImpl should not be called until we've Trim'd past the last cached buffer.
// See comments for ReadableStream::MakeCachedBuffer.
FX_CHECK(!cache_);
// EffectsStageV1 always produces data on integrally-aligned frames.
dest_frame = Fixed(dest_frame.Floor());
// Advance to our source's next available frame. This is needed when the source stream
// contains gaps. For example, given a sequence of calls:
//
// ReadLock(ctx, 0, 20)
// ReadLock(ctx, 20, 20)
//
// If our block size is 30, then at the first call, we will attempt to produce 30 frames
// starting at frame 0. If the source has data for that range, we'll cache all 30 processed
// frames and the second ReadLock call will be handled by our cache.
//
// However, if the source has no data for the range [0, 30), the first ReadLock call will
// return std::nullopt. At the second call, we shouldn't ask the source for any frames
// before frame 30 because we already know that range is empty.
if (auto next_available = source_->NextAvailableFrame(); next_available) {
// SampleAndHold: source frame 1.X overlaps dest frame 2.0, so always round up.
int64_t frames_to_trim = next_available->Ceiling() - dest_frame.Floor();
if (frames_to_trim > 0) {
frame_count -= frames_to_trim;
dest_frame += Fixed(frames_to_trim);
}
}
while (frame_count > 0) {
int64_t frames_read_from_source = FillCache(ctx, dest_frame, frame_count);
if (cache_) {
FX_CHECK(source_buffer_.length() > 0);
FX_CHECK(cache_->dest_buffer);
return MakeCachedBuffer(source_buffer_.start(), source_buffer_.length(), cache_->dest_buffer,
cache_->source_usage_mask, cache_->source_total_applied_gain_db);
}
// We tried to process an entire block, however the source had no data.
// If frame_count > max_frames_per_call_, try the next block.
dest_frame += Fixed(frames_read_from_source);
frame_count -= frames_read_from_source;
}
// The source has no data for the requested range.
return std::nullopt;
}
void EffectsStageV1::TrimImpl(Fixed dest_frame) {
// EffectsStageV1 always produces data on integrally-aligned frames.
dest_frame = Fixed(dest_frame.Floor());
if (cache_ && dest_frame >= source_buffer_.end()) {
cache_ = std::nullopt;
}
source_->Trim(dest_frame);
}
int64_t EffectsStageV1::FillCache(ReadLockContext& ctx, Fixed dest_frame, int64_t frame_count) {
static_assert(FUCHSIA_AUDIO_EFFECTS_BLOCK_SIZE_ANY == 0);
static_assert(FUCHSIA_AUDIO_EFFECTS_FRAMES_PER_BUFFER_ANY == 0);
cache_ = std::nullopt;
source_buffer_.Reset(dest_frame);
auto source_usage_mask = StreamUsageMask();
float source_total_applied_gain_db = 0;
bool has_data = false;
// The buffer must have a multiple of block_size_frames_ and at most max_batch_size_frames_.
// The buffer must have at most frame_count frames (ideally it has exactly that many).
frame_count = static_cast<int64_t>(
fbl::round_up(static_cast<uint64_t>(frame_count), static_cast<uint64_t>(block_size_frames_)));
frame_count = std::min(frame_count, max_batch_size_frames_);
// Read frame_count frames into source_buffer_.
while (source_buffer_.length() < frame_count) {
Fixed start = source_buffer_.end();
int64_t frames_remaining = frame_count - source_buffer_.length();
auto buf = source_->ReadLock(ctx, start, frames_remaining);
if (buf) {
// SampleAndHold: source frame 1.X overlaps dest frame 2.0, so always round up.
source_buffer_.AppendData(Fixed(buf->start().Ceiling()), buf->length(), buf->payload());
source_usage_mask.insert_all(buf->usage_mask());
source_total_applied_gain_db = buf->total_applied_gain_db();
has_data = true;
} else {
source_buffer_.AppendSilence(start, frames_remaining);
}
}
if (block_size_frames_ > 0) {
FX_CHECK(source_buffer_.length() % block_size_frames_ == 0)
<< "Bad buffer size " << source_buffer_.length() << " must be divisible by "
<< block_size_frames_;
}
// If the source had no frames, we don't need to process anything.
if (!has_data) {
return frame_count;
}
cache_ = Cache{
.source_usage_mask = source_usage_mask,
.source_total_applied_gain_db = source_total_applied_gain_db,
};
// Process this buffer.
fuchsia_audio_effects_stream_info stream_info;
stream_info.usage_mask = source_usage_mask.mask() & kSupportedUsageMask;
stream_info.gain_dbfs = source_total_applied_gain_db;
stream_info.volume = volume_curve_.DbToVolume(source_total_applied_gain_db);
effects_processor_->SetStreamInfo(stream_info);
StageMetricsTimer timer("EffectsStageV1::Process");
timer.Start();
// The transformed output gets written to cache_.dest_buffer.
// We hold onto these buffers until the current frame advances to source_buffer_.end().
auto payload = reinterpret_cast<float*>(source_buffer_.payload());
effects_processor_->Process(source_buffer_.length(), payload, &cache_->dest_buffer);
timer.Stop();
ctx.AddStageMetrics(timer.Metrics());
return frame_count;
}
BaseStream::TimelineFunctionSnapshot EffectsStageV1::ref_time_to_frac_presentation_frame() const {
auto snapshot = source_->ref_time_to_frac_presentation_frame();
// Update our timeline function to include the latency introduced by these effects.
//
// Our effects shift incoming audio into the future by "delay_frames".
// So input frame[N] corresponds to output frame[N + delay_frames].
int64_t delay_frames = effects_processor_->delay_frames();
auto delay_frac_frames = Fixed(delay_frames);
auto source_frac_frame_to_dest_frac_frame =
TimelineFunction(delay_frac_frames.raw_value(), 0, TimelineRate(1, 1));
snapshot.timeline_function = source_frac_frame_to_dest_frac_frame * snapshot.timeline_function;
return snapshot;
}
void EffectsStageV1::SetPresentationDelay(zx::duration external_delay) {
// Add in any additional lead time required by our effects.
zx::duration intrinsic_lead_time = ComputeIntrinsicMinLeadTime();
zx::duration total_delay = external_delay + intrinsic_lead_time;
if constexpr (kLogPresentationDelay) {
FX_LOGS(INFO) << "(" << this << ") " << __FUNCTION__ << " given external_delay "
<< external_delay.to_nsecs() << "ns";
FX_LOGS(INFO) << "Adding it to our intrinsic_lead_time " << intrinsic_lead_time.to_nsecs()
<< "ns; setting our total_delay " << total_delay.to_nsecs() << "ns";
}
// Apply the total lead time to us and propagate that value to our source.
ReadableStream::SetPresentationDelay(total_delay);
source_->SetPresentationDelay(total_delay);
}
fpromise::result<void, fuchsia::media::audio::UpdateEffectError> EffectsStageV1::UpdateEffect(
const std::string& instance_name, const std::string& config) {
for (auto& effect : *effects_processor_) {
if (effect.instance_name() == instance_name) {
if (effect.UpdateConfiguration(config) == ZX_OK) {
return fpromise::ok();
} else {
return fpromise::error(fuchsia::media::audio::UpdateEffectError::INVALID_CONFIG);
}
}
}
return fpromise::error(fuchsia::media::audio::UpdateEffectError::NOT_FOUND);
}
zx::duration EffectsStageV1::ComputeIntrinsicMinLeadTime() const {
TimelineRate ticks_per_frame = format().frames_per_ns().Inverse();
uint32_t lead_frames = effects_processor_->delay_frames();
// Lead time must be extended to fill at least one complete block.
if (block_size_frames_ > 0) {
lead_frames += block_size_frames_ - 1;
}
return zx::duration(ticks_per_frame.Scale(lead_frames));
}
} // namespace media::audio