| // 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/audio_output.h" |
| |
| #include "src/media/audio/audio_core/audio_clock.h" |
| #include "src/media/audio/audio_core/audio_device_manager.h" |
| #include "src/media/audio/audio_core/loudness_transform.h" |
| #include "src/media/audio/audio_core/testing/fake_audio_driver.h" |
| #include "src/media/audio/audio_core/testing/fake_audio_renderer.h" |
| #include "src/media/audio/audio_core/testing/fake_stream.h" |
| #include "src/media/audio/audio_core/testing/threading_model_fixture.h" |
| #include "src/media/audio/lib/clock/clone_mono.h" |
| #include "src/media/audio/lib/effects_loader/testing/test_effects.h" |
| |
| namespace media::audio { |
| namespace { |
| |
| static constexpr size_t kFramesPerSecond = 48000; |
| static const TimelineFunction kDriverRefPtsToFractionalFrames = |
| TimelineFunction(0, 0, Fixed(kFramesPerSecond).raw_value(), zx::sec(1).get()); |
| |
| // An OutputPipeline that always returns std::nullopt from |ReadLock|. |
| class TestOutputPipeline : public OutputPipeline { |
| public: |
| TestOutputPipeline(const Format& format, std::shared_ptr<AudioClockManager> clock_manager) |
| : OutputPipeline(format), |
| audio_clock_(clock_manager->CreateClientFixed(clock::AdjustableCloneOfMonotonic())) {} |
| |
| void Enqueue(ReadableStream::Buffer buffer) { buffers_.push_back(std::move(buffer)); } |
| |
| // |media::audio::ReadableStream| |
| std::optional<ReadableStream::Buffer> ReadLock(Fixed frame, int64_t frame_count) override { |
| if (buffers_.empty()) { |
| return std::nullopt; |
| } |
| auto buffer = std::move(buffers_.front()); |
| buffers_.pop_front(); |
| return buffer; |
| } |
| void Trim(Fixed frame) override {} |
| TimelineFunctionSnapshot ref_time_to_frac_presentation_frame() const override { |
| return TimelineFunctionSnapshot{ |
| .timeline_function = kDriverRefPtsToFractionalFrames, |
| .generation = 1, |
| }; |
| } |
| AudioClock& reference_clock() override { return *audio_clock_; } |
| |
| // |media::audio::OutputPipeline| |
| std::shared_ptr<ReadableStream> loopback() const override { return nullptr; } |
| std::shared_ptr<Mixer> AddInput( |
| std::shared_ptr<ReadableStream> stream, const StreamUsage& usage, |
| std::optional<float> initial_dest_gain_db = std::nullopt, |
| Mixer::Resampler sampler_hint = Mixer::Resampler::Default) override { |
| return nullptr; |
| } |
| void RemoveInput(const ReadableStream& stream) override {} |
| fit::result<void, fuchsia::media::audio::UpdateEffectError> UpdateEffect( |
| const std::string& instance_name, const std::string& config) override { |
| return fit::error(fuchsia::media::audio::UpdateEffectError::NOT_FOUND); |
| } |
| |
| private: |
| std::deque<ReadableStream::Buffer> buffers_; |
| |
| std::unique_ptr<AudioClock> audio_clock_; |
| }; |
| |
| class StubDriver : public AudioDriver { |
| public: |
| static constexpr size_t kSafeWriteDelayFrames = 480; |
| static constexpr auto kSafeWriteDelayDuration = zx::msec(10); |
| static constexpr size_t kRingBufferFrames = 48000; |
| |
| StubDriver(AudioDevice* owner) : AudioDriver(owner) {} |
| |
| const TimelineFunction& ref_time_to_frac_presentation_frame() const override { |
| return kDriverRefPtsToFractionalFrames; |
| } |
| const TimelineFunction& ref_time_to_frac_safe_read_or_write_frame() const override { |
| return ref_time_to_safe_read_or_write_frame_; |
| } |
| |
| private: |
| const TimelineFunction ref_time_to_safe_read_or_write_frame_ = |
| TimelineFunction(Fixed(kSafeWriteDelayFrames).raw_value(), 0, |
| Fixed(kFramesPerSecond).raw_value(), zx::sec(1).get()); |
| }; |
| |
| class TestAudioOutput : public AudioOutput { |
| public: |
| TestAudioOutput(ThreadingModel* threading_model, DeviceRegistry* registry, |
| LinkMatrix* link_matrix, std::shared_ptr<AudioClockManager> clock_manager) |
| : AudioOutput("", threading_model, registry, link_matrix, clock_manager, |
| std::make_unique<StubDriver>(this)) { |
| SetPresentationDelay(StubDriver::kSafeWriteDelayDuration); |
| } |
| |
| using AudioOutput::FrameSpan; |
| using AudioOutput::SetNextSchedTimeMono; |
| void SetupMixTask(const DeviceConfig::OutputDeviceProfile& profile, uint32_t max_frames, |
| TimelineFunction clock_mono_to_output_frame) { |
| OBTAIN_EXECUTION_DOMAIN_TOKEN(token, &mix_domain()); |
| AudioOutput::SetupMixTask(profile, max_frames, clock_mono_to_output_frame); |
| } |
| void Process() { |
| OBTAIN_EXECUTION_DOMAIN_TOKEN(token, &mix_domain()); |
| AudioOutput::Process(); |
| } |
| std::unique_ptr<OutputPipeline> CreateOutputPipeline( |
| const PipelineConfig& config, const VolumeCurve& volume_curve, size_t max_block_size_frames, |
| TimelineFunction device_reference_clock_to_fractional_frame, AudioClock& ref_clock) override { |
| if (output_pipeline_) { |
| return std::move(output_pipeline_); |
| } |
| return AudioOutput::CreateOutputPipeline(config, volume_curve, max_block_size_frames, |
| device_reference_clock_to_fractional_frame, ref_clock); |
| } |
| |
| // Allow a test to provide a delegate to handle |AudioOutput::StartMixJob| invocations. |
| using StartMixDelegate = fit::function<std::optional<AudioOutput::FrameSpan>(zx::time)>; |
| void set_start_mix_delegate(StartMixDelegate delegate) { |
| start_mix_delegate_ = std::move(delegate); |
| } |
| |
| // Allow a test to provide a delegate to handle |AudioOutput::FinishMixJob| invocations. |
| using FinishMixDelegate = fit::function<void(const AudioOutput::FrameSpan&, const float* buffer)>; |
| void set_finish_mix_delegate(FinishMixDelegate delegate) { |
| finish_mix_delegate_ = std::move(delegate); |
| } |
| void set_output_pipeline(std::unique_ptr<OutputPipeline> output_pipeline) { |
| output_pipeline_ = std::move(output_pipeline); |
| } |
| |
| // |AudioOutput| |
| std::optional<AudioOutput::FrameSpan> StartMixJob(zx::time device_ref_time) override { |
| if (start_mix_delegate_) { |
| return start_mix_delegate_(device_ref_time); |
| } else { |
| return std::nullopt; |
| } |
| } |
| void FinishMixJob(const AudioOutput::FrameSpan& span, const float* buffer) override { |
| if (finish_mix_delegate_) { |
| finish_mix_delegate_(span, buffer); |
| } |
| } |
| zx::duration MixDeadline() const override { return zx::msec(10); } |
| // |AudioDevice| |
| void ApplyGainLimits(fuchsia::media::AudioGainInfo* in_out_info, |
| fuchsia::media::AudioGainValidFlags set_flags) override {} |
| // TestAudioOutput does not implement enough state machine to fully initialize an AudioDriver . |
| // It gets far enough for the AudioDriver to establish and expose its reference AudioClock. |
| void OnWakeup() { driver()->GetDriverInfo(); } |
| |
| private: |
| StartMixDelegate start_mix_delegate_; |
| FinishMixDelegate finish_mix_delegate_; |
| std::unique_ptr<OutputPipeline> output_pipeline_; |
| }; |
| |
| class AudioOutputTest : public testing::ThreadingModelFixture { |
| protected: |
| void SetUp() override { |
| // Establish and start a remote driver, to respond to a GetDriverInfo request with the clock |
| // domain, so that AudioDriver establishes and passes on an AudioClock for this device. |
| zx::channel c1, c2; |
| ASSERT_EQ(ZX_OK, zx::channel::create(0, &c1, &c2)); |
| remote_driver_ = std::make_unique<testing::FakeAudioDriver>(std::move(c1), dispatcher()); |
| audio_output_->driver()->Init(std::move(c2)); |
| remote_driver_->Start(); |
| |
| threading_model().FidlDomain().ScheduleTask(audio_output_->Startup()); |
| RunLoopUntilIdle(); |
| } |
| |
| void SetupMixTask() { |
| audio_output_->SetupMixTask(DeviceConfig::OutputDeviceProfile(), StubDriver::kRingBufferFrames, |
| stub_driver()->ref_time_to_frac_presentation_frame()); |
| } |
| |
| void CheckBuffer(void* buffer, float expected_sample, size_t num_samples) { |
| float* floats = reinterpret_cast<float*>(buffer); |
| for (size_t i = 0; i < num_samples; ++i) { |
| ASSERT_FLOAT_EQ(expected_sample, floats[i]); |
| } |
| } |
| |
| StubDriver* stub_driver() { return static_cast<StubDriver*>(audio_output_->driver()); } |
| |
| VolumeCurve volume_curve_ = VolumeCurve::DefaultForMinGain(Gain::kMinGainDb); |
| std::shared_ptr<TestAudioOutput> audio_output_ = |
| std::make_shared<TestAudioOutput>(&threading_model(), &context().device_manager(), |
| &context().link_matrix(), context().clock_manager()); |
| |
| std::unique_ptr<testing::FakeAudioDriver> remote_driver_; |
| }; |
| |
| TEST_F(AudioOutputTest, ProcessTrimsInputStreamsIfNoMixJobProvided) { |
| auto renderer = testing::FakeAudioRenderer::CreateWithDefaultFormatInfo( |
| dispatcher(), &context().link_matrix(), context().clock_manager()); |
| SetupMixTask(); |
| context().link_matrix().LinkObjects(renderer, audio_output_, |
| std::make_shared<MappedLoudnessTransform>(volume_curve_)); |
| |
| // StartMixJob always returns nullopt (no work) and schedules another mix 1ms in the future. |
| audio_output_->set_start_mix_delegate([this, audio_output = audio_output_.get()](zx::time) { |
| audio_output->SetNextSchedTimeMono(Now() + zx::msec(1)); |
| return std::nullopt; |
| }); |
| |
| // Enqueue 2 packets: |
| // * packet 1 from 0ms -> 5ms. |
| // * packet 2 from 5ms -> 10ms. |
| bool packet1_released = false; |
| bool packet2_released = false; |
| renderer->EnqueueAudioPacket(1.0, zx::msec(5), [&packet1_released] { |
| FX_LOGS(ERROR) << "Release packet 1"; |
| packet1_released = true; |
| }); |
| renderer->EnqueueAudioPacket(1.0, zx::msec(5), [&packet2_released] { |
| FX_LOGS(ERROR) << "Release packet 2"; |
| packet2_released = true; |
| }); |
| |
| // Process kicks off the periodic mix task. |
| audio_output_->Process(); |
| |
| // After 4ms we should still be retaining packet1. |
| RunLoopFor(zx::msec(4)); |
| ASSERT_FALSE(packet1_released); |
| ASSERT_FALSE(packet2_released); |
| |
| // 5ms; all the audio from packet1 is consumed and it should be released. We should still have |
| // packet2, however. |
| RunLoopFor(zx::msec(1)); |
| ASSERT_TRUE(packet1_released); |
| ASSERT_FALSE(packet2_released); |
| |
| // After 9ms we should still be retaining packet2. |
| RunLoopFor(zx::msec(4)); |
| ASSERT_FALSE(packet2_released); |
| |
| // Finally after 10ms we will have released packet2. |
| RunLoopFor(zx::msec(1)); |
| ASSERT_TRUE(packet2_released); |
| } |
| |
| TEST_F(AudioOutputTest, ProcessRequestsSilenceIfNoSourceBuffer) { |
| auto format = Format::Create({ |
| .sample_format = fuchsia::media::AudioSampleFormat::FLOAT, |
| .channels = 2, |
| .frames_per_second = 48000, |
| }) |
| .take_value(); |
| |
| // Use an output pipeline that will always return nullopt from ReadLock. |
| auto pipeline_owned = std::make_unique<TestOutputPipeline>(format, context().clock_manager()); |
| audio_output_->set_output_pipeline(std::move(pipeline_owned)); |
| SetupMixTask(); |
| |
| // Return some valid, non-silent frame range from StartMixJob. |
| audio_output_->set_start_mix_delegate([](zx::time now) { |
| return TestAudioOutput::FrameSpan{ |
| .start = 0, |
| .length = 100, |
| .is_mute = false, |
| }; |
| }); |
| |
| bool finish_called = false; |
| audio_output_->set_finish_mix_delegate([&finish_called](auto span, auto buffer) { |
| EXPECT_EQ(span.start, 0); |
| EXPECT_EQ(span.length, 100u); |
| EXPECT_TRUE(span.is_mute); |
| EXPECT_EQ(buffer, nullptr); |
| finish_called = true; |
| }); |
| |
| // Now do a mix. |
| audio_output_->Process(); |
| EXPECT_TRUE(finish_called); |
| } |
| |
| // Verify we call StartMixJob multiple times if FinishMixJob does not fill buffer. |
| TEST_F(AudioOutputTest, ProcessMultipleMixJobs) { |
| const Format format = |
| Format::Create({ |
| .sample_format = fuchsia::media::AudioSampleFormat::FLOAT, |
| .channels = 2, |
| .frames_per_second = 48000, |
| }) |
| .take_value(); |
| |
| // Use an output pipeline that will always return nullopt from ReadLock. |
| auto pipeline_owned = std::make_unique<TestOutputPipeline>(format, context().clock_manager()); |
| auto pipeline = pipeline_owned.get(); |
| audio_output_->set_output_pipeline(std::move(pipeline_owned)); |
| SetupMixTask(); |
| |
| const uint32_t kBufferFrames = 25; |
| const uint32_t kBufferSamples = kBufferFrames * 2; |
| const uint32_t kNumBuffers = 4; |
| // Setup our buffer with data that is just the value of frame 'N' is 'N'. |
| std::vector<float> buffer(kBufferSamples); |
| for (size_t sample = 0; sample < kBufferSamples; ++sample) { |
| buffer[sample] = static_cast<float>(sample); |
| } |
| // Enqueue several buffers, each with the same payload buffer. |
| for (size_t i = 0; i < kNumBuffers; ++i) { |
| pipeline->Enqueue(ReadableStream::Buffer(Fixed(i * kBufferFrames), Fixed(kBufferFrames), |
| buffer.data(), true, StreamUsageMask(), |
| Gain::kUnityGainDb)); |
| } |
| |
| // Return some valid, non-silent frame range from StartMixJob. |
| uint32_t mix_jobs = 0; |
| uint32_t frames_finished = 0; |
| audio_output_->set_start_mix_delegate([&frames_finished, &mix_jobs](zx::time now) { |
| ++mix_jobs; |
| return TestAudioOutput::FrameSpan{ |
| .start = frames_finished, |
| .length = (kBufferFrames * kNumBuffers) - frames_finished, |
| .is_mute = false, |
| }; |
| }); |
| |
| audio_output_->set_finish_mix_delegate([&frames_finished](auto span, auto buffer) { |
| EXPECT_EQ(span.start, frames_finished); |
| EXPECT_FALSE(span.is_mute); |
| EXPECT_NE(buffer, nullptr); |
| for (size_t sample = 0; sample < kBufferSamples; ++sample) { |
| EXPECT_FLOAT_EQ(static_cast<float>(sample), buffer[sample]); |
| } |
| frames_finished += span.length; |
| }); |
| |
| // Now do a mix. |
| audio_output_->Process(); |
| EXPECT_EQ(frames_finished, kNumBuffers * kBufferFrames); |
| EXPECT_EQ(mix_jobs, kNumBuffers); |
| } |
| |
| // Verify AudioOutput loudness transform is updated with the |volume_curve| used in SetupMixTask. |
| TEST_F(AudioOutputTest, UpdateLoudnessTransformOnSetupMixTask) { |
| static const TimelineFunction kOneFramePerMs = TimelineFunction(TimelineRate(1, 1'000'000)); |
| static const VolumeCurve volume_curve = VolumeCurve::DefaultForMinGain(-10.); |
| static DeviceConfig::OutputDeviceProfile profile = DeviceConfig::OutputDeviceProfile( |
| /*eligible_for_loopback=*/true, |
| /*supported_usages=*/StreamUsageSetFromRenderUsages(kFidlRenderUsages), volume_curve, |
| /*independent_volume_control=*/false, /*pipeline_config=*/PipelineConfig::Default(), |
| /*driver_gain_db=*/0.0); |
| audio_output_->SetupMixTask(profile, zx::msec(1).to_msecs(), kOneFramePerMs); |
| |
| auto output_transform = audio_output_->profile().loudness_transform(); |
| auto expected_transform = std::make_shared<MappedLoudnessTransform>(volume_curve); |
| EXPECT_FLOAT_EQ(output_transform->Evaluate<1>({VolumeValue{.5}}), |
| expected_transform->Evaluate<1>({VolumeValue{.5}})); |
| } |
| |
| // Verify loudness_transform_ is NoOpLoudnessTransform to honor IndependentVolumeControl. |
| TEST_F(AudioOutputTest, HonorIndpendentVolumeControlLoudnessTransform) { |
| static const TimelineFunction kOneFramePerMs = TimelineFunction(TimelineRate(1, 1'000'000)); |
| audio_output_->SetupMixTask( |
| DeviceConfig::OutputDeviceProfile( |
| /*eligible_for_loopback=*/true, |
| /*supported_usages=*/StreamUsageSetFromRenderUsages(kFidlRenderUsages), |
| VolumeCurve::DefaultForMinGain(VolumeCurve::kDefaultGainForMinVolume), |
| /*independent_volume_control=*/true, PipelineConfig::Default(), /*driver_gain_db*/ 0.0), |
| zx::msec(1).to_msecs(), kOneFramePerMs); |
| |
| auto transform = audio_output_->profile().loudness_transform(); |
| EXPECT_FLOAT_EQ(transform->Evaluate<1>({VolumeValue{0.}}), Gain::kUnityGainDb); |
| EXPECT_FLOAT_EQ(transform->Evaluate<1>({VolumeValue{1.}}), Gain::kUnityGainDb); |
| } |
| |
| TEST_F(AudioOutputTest, UpdateOutputPipeline) { |
| // Setup test. |
| auto test_effects = testing::TestEffectsModule::Open(); |
| test_effects.AddEffect("add_1.0").WithAction(TEST_EFFECTS_ACTION_ADD, 1.0); |
| |
| // Create OutputPipeline with no effects and verify output. |
| SetupMixTask(); |
| auto pipeline = audio_output_->output_pipeline(); |
| |
| // Add an input into our pipeline. Without this we won't run any effects as the stream will be |
| // silent. This actually sends silence through the pipeline, but it's flagged with a gain > MUTE |
| // so that it still gets mixed. |
| auto format = Format::Create({ |
| .sample_format = fuchsia::media::AudioSampleFormat::FLOAT, |
| .channels = 2, |
| .frames_per_second = 48000, |
| }) |
| .take_value(); |
| auto default_stream = std::make_shared<testing::FakeStream>(format, context().clock_manager()); |
| const auto stream_usage = StreamUsage::WithRenderUsage(RenderUsage::MEDIA); |
| default_stream->set_usage_mask({stream_usage}); |
| default_stream->set_gain_db(0.0); |
| default_stream->timeline_function()->Update(TimelineFunction( |
| TimelineRate(Fixed(format.frames_per_second()).raw_value(), zx::sec(1).to_nsecs()))); |
| pipeline->AddInput(default_stream, stream_usage); |
| |
| { |
| auto buf = pipeline->ReadLock(Fixed(0), 48); |
| |
| EXPECT_TRUE(buf); |
| EXPECT_EQ(buf->start().Floor(), 0u); |
| EXPECT_EQ(buf->length().Floor(), 48u); |
| CheckBuffer(buf->payload(), 0.0, 96); |
| } |
| |
| // Update OutputPipeline and VolumeCurve, and verify output. |
| PipelineConfig::MixGroup root{ |
| .name = "linearize", |
| .input_streams = |
| { |
| RenderUsage::BACKGROUND, |
| }, |
| .effects = |
| { |
| { |
| .lib_name = "test_effects.so", |
| .effect_name = "add_1.0", |
| .instance_name = "", |
| .effect_config = "", |
| }, |
| }, |
| .inputs = {{ |
| .name = "mix", |
| .input_streams = |
| { |
| RenderUsage::MEDIA, |
| RenderUsage::SYSTEM_AGENT, |
| RenderUsage::INTERRUPTION, |
| RenderUsage::COMMUNICATION, |
| }, |
| .effects = |
| { |
| { |
| .lib_name = "test_effects.so", |
| .effect_name = "add_1.0", |
| .instance_name = "", |
| .effect_config = "", |
| }, |
| }, |
| .output_rate = kFramesPerSecond, |
| .output_channels = 2, |
| }}, |
| .output_rate = kFramesPerSecond, |
| .output_channels = 2, |
| }; |
| auto volume_curve = VolumeCurve::DefaultForMinGain(-10.); |
| auto profile_params = DeviceConfig::OutputDeviceProfile::Parameters{ |
| .pipeline_config = PipelineConfig(root), .volume_curve = volume_curve}; |
| |
| bool updated_device_profile = false; |
| auto promise = audio_output_->UpdateDeviceProfile(profile_params); |
| context().threading_model().FidlDomain().executor()->schedule_task( |
| promise.then([&updated_device_profile](fit::result<void, zx_status_t>& result) { |
| updated_device_profile = true; |
| })); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(updated_device_profile); |
| pipeline = audio_output_->output_pipeline(); |
| pipeline->AddInput(default_stream, stream_usage); |
| |
| { |
| auto buf = pipeline->ReadLock(Fixed(0), 48); |
| EXPECT_TRUE(buf); |
| EXPECT_EQ(buf->start().Floor(), 0u); |
| EXPECT_EQ(buf->length().Floor(), 48u); |
| CheckBuffer(buf->payload(), 2.0, 96); |
| } |
| |
| auto result_transform = audio_output_->profile().loudness_transform(); |
| auto expected_transform = std::make_shared<MappedLoudnessTransform>(volume_curve); |
| EXPECT_FLOAT_EQ(result_transform->Evaluate<1>({VolumeValue{.5}}), |
| expected_transform->Evaluate<1>({VolumeValue{.5}})); |
| } |
| |
| } // namespace |
| } // namespace media::audio |