blob: b9a7d3039bd2a06095026b42e2e27d1361e7a285 [file] [log] [blame]
// Copyright 2018 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 <fuchsia/media/cpp/fidl.h>
#include <lib/zx/vmo.h>
#include "src/media/audio/lib/test/hermetic_audio_test.h"
namespace media::audio::test {
// Just an arbitrary |AudioStreamType| that is valid to be used. Intended for
// tests that don't care about the specific audio frames being sent.
constexpr fuchsia::media::AudioStreamType kTestStreamType = {
.sample_format = fuchsia::media::AudioSampleFormat::FLOAT,
.channels = 2,
.frames_per_second = 48000,
};
// The following are valid/invalid when used with |kTestStreamType|.
constexpr uint64_t kValidPayloadSize = sizeof(float) * kTestStreamType.channels;
constexpr uint64_t kInvalidPayloadSize = kValidPayloadSize - 1;
constexpr size_t kDefaultPayloadBufferSize = PAGE_SIZE;
//
// AudioRendererTest
//
// This set of tests verifies asynchronous usage of AudioRenderer.
class AudioRendererTest : public HermeticAudioCoreTest {
protected:
void SetUp() override;
void TearDown() override;
void SetNegativeExpectations() override;
// Discards all in-flight packets and waits for the response from the audio
// renderer. This can be used as a simple round-trip through the audio
// renderer, indicating that all FIDL messages have been read out of the
// channel.
//
// In other words, calling this method also asserts that all prior FIDL
// messages have been handled successfully (no disconnect was triggered).
void AssertConnectedAndDiscardAllPackets();
// Creates a VMO with |buffer_size| and then passes it to
// |AudioRenderer::AddPayloadBuffer| with |id|. This is purely a convenience
// method and doesn't provide access to the buffer VMO.
void CreateAndAddPayloadBuffer(uint32_t id);
fuchsia::media::AudioRendererPtr audio_renderer_;
fuchsia::media::audio::GainControlPtr gain_control_;
bool bound_renderer_expected_ = true;
};
//
// AudioRendererTest implementation
//
void AudioRendererTest::SetUp() {
HermeticAudioCoreTest::SetUp();
audio_core_->CreateAudioRenderer(audio_renderer_.NewRequest());
audio_renderer_.set_error_handler(ErrorHandler());
}
void AudioRendererTest::TearDown() {
gain_control_.Unbind();
EXPECT_EQ(bound_renderer_expected_, audio_renderer_.is_bound());
audio_renderer_.Unbind();
HermeticAudioCoreTest::TearDown();
}
void AudioRendererTest::SetNegativeExpectations() {
HermeticAudioCoreTest::SetNegativeExpectations();
bound_renderer_expected_ = false;
}
void AudioRendererTest::AssertConnectedAndDiscardAllPackets() {
audio_renderer_->DiscardAllPackets(CompletionCallback());
ExpectCallback();
}
void AudioRendererTest::CreateAndAddPayloadBuffer(uint32_t id) {
zx::vmo payload_buffer;
constexpr uint32_t kVmoOptionsNone = 0;
ASSERT_EQ(zx::vmo::create(kDefaultPayloadBufferSize, kVmoOptionsNone, &payload_buffer), ZX_OK);
audio_renderer_->AddPayloadBuffer(id, std::move(payload_buffer));
}
//
// AudioRenderer implements the base classes StreamBufferSet and StreamSink.
//
// StreamBufferSet validation
//
// Sanity test adding a payload buffer. Just verify we don't get a disconnect.
TEST_F(AudioRendererTest, AddPayloadBuffer) {
CreateAndAddPayloadBuffer(0);
CreateAndAddPayloadBuffer(1);
CreateAndAddPayloadBuffer(2);
AssertConnectedAndDiscardAllPackets();
}
// TODO(tjdetwiler): This is out of spec but there are currently clients that
// rely on this behavior. This test should be updated to fail once all clients
// are fixed.
TEST_F(AudioRendererTest, AddPayloadBufferDuplicateId) {
CreateAndAddPayloadBuffer(0);
CreateAndAddPayloadBuffer(0);
AssertConnectedAndDiscardAllPackets();
}
// It is invalid to add a payload buffer while there are queued packets.
TEST_F(AudioRendererTest, AddPayloadBufferWhileOperationalCausesDisconnect) {
// Configure with one buffer and a valid stream type.
CreateAndAddPayloadBuffer(0);
audio_renderer_->SetPcmStreamType(kTestStreamType);
AssertConnectedAndDiscardAllPackets();
// Send Packet moves connection into the operational state.
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = 0;
packet.payload_offset = 0;
packet.payload_size = kValidPayloadSize;
audio_renderer_->SendPacketNoReply(std::move(packet));
// Attempt to add new payload buffer while the packet is in flight. This
// should fail.
CreateAndAddPayloadBuffer(0);
ExpectDisconnect();
}
// Test removing payload buffers.
TEST_F(AudioRendererTest, RemovePayloadBuffer) {
CreateAndAddPayloadBuffer(0);
CreateAndAddPayloadBuffer(1);
CreateAndAddPayloadBuffer(2);
audio_renderer_->RemovePayloadBuffer(0);
audio_renderer_->RemovePayloadBuffer(1);
audio_renderer_->RemovePayloadBuffer(2);
AssertConnectedAndDiscardAllPackets();
}
// Test RemovePayloadBuffer with an invalid ID (does not have a corresponding
// AddPayloadBuffer).
TEST_F(AudioRendererTest, RemovePayloadBufferInvalidBufferIdCausesDisconnect) {
audio_renderer_->RemovePayloadBuffer(0);
ExpectDisconnect();
}
// It is invalid to remove a payload buffer while there are queued packets.
TEST_F(AudioRendererTest, RemovePayloadBufferWhileOperationalCausesDisconnect) {
// Configure with one buffer and a valid stream type.
CreateAndAddPayloadBuffer(0);
audio_renderer_->SetPcmStreamType(kTestStreamType);
AssertConnectedAndDiscardAllPackets();
// Send Packet moves connection into the operational state.
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = 0;
packet.payload_offset = 0;
packet.payload_size = kValidPayloadSize;
audio_renderer_->SendPacketNoReply(std::move(packet));
// Attempt to add new payload buffer while the packet is in flight. This
// should fail.
audio_renderer_->RemovePayloadBuffer(0);
ExpectDisconnect();
}
//
// StreamSink validation
//
// TODO(mpuryear): test SendPacket(StreamPacket packet) -> ();
// Also negative testing: malformed packet
//
// SendPacketNoReply tests.
//
TEST_F(AudioRendererTest, SendPacketNoReply) {
// Configure with one buffer and a valid stream type.
CreateAndAddPayloadBuffer(0);
audio_renderer_->SetPcmStreamType(kTestStreamType);
// Send a packet (we don't care about the actual packet data here).
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = 0;
packet.payload_offset = 0;
packet.payload_size = kValidPayloadSize;
audio_renderer_->SendPacketNoReply(std::move(packet));
AssertConnectedAndDiscardAllPackets();
}
TEST_F(AudioRendererTest, SendPacketNoReplyInvalidPayloadBufferIdCausesDisconnect) {
// Configure with one buffer and a valid stream type.
CreateAndAddPayloadBuffer(0);
audio_renderer_->SetPcmStreamType(kTestStreamType);
// Send a packet (we don't care about the actual packet data here).
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = 1234;
packet.payload_offset = 0;
packet.payload_size = kValidPayloadSize;
audio_renderer_->SendPacketNoReply(std::move(packet));
ExpectDisconnect();
}
// It is invalid to SendPacket before the stream type has been configured
// (SetPcmStreamType).
TEST_F(AudioRendererTest, SendPacketBeforeSetStreamTypeCausesDisconnect) {
// Add a payload buffer but no stream type.
CreateAndAddPayloadBuffer(0);
// SendPacket. This should trigger a disconnect due to a lack of a configured
// stream type.
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = 0;
packet.payload_offset = 0;
packet.payload_size = kValidPayloadSize;
audio_renderer_->SendPacketNoReply(std::move(packet));
ExpectDisconnect();
}
// SendPacket with a |payload_size| that is invalid.
TEST_F(AudioRendererTest, SendPacketNoReplyInvalidPayloadBufferSizeCausesDisconnect) {
// Configure with one buffer and a valid stream type.
CreateAndAddPayloadBuffer(0);
audio_renderer_->SetPcmStreamType(kTestStreamType);
// Send Packet moves connection into the operational state.
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = 0;
packet.payload_offset = 0;
packet.payload_size = kInvalidPayloadSize;
audio_renderer_->SendPacketNoReply(std::move(packet));
ExpectDisconnect();
}
TEST_F(AudioRendererTest, SendPacketNoReplyBufferOutOfBoundsCausesDisconnect) {
// Configure with one buffer and a valid stream type.
CreateAndAddPayloadBuffer(0);
audio_renderer_->SetPcmStreamType(kTestStreamType);
// Send Packet moves connection into the operational state.
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = 0;
// |payload_offset| is beyond the end of the payload buffer.
packet.payload_offset = kDefaultPayloadBufferSize;
packet.payload_size = kValidPayloadSize;
audio_renderer_->SendPacketNoReply(std::move(packet));
ExpectDisconnect();
}
TEST_F(AudioRendererTest, SendPacketNoReplyBufferOverrunCausesDisconnect) {
// Configure with one buffer and a valid stream type.
CreateAndAddPayloadBuffer(0);
audio_renderer_->SetPcmStreamType(kTestStreamType);
// Send Packet moves connection into the operational state.
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = 0;
// |payload_offset| + |payload_size| is beyond the end of the payload buffer.
packet.payload_size = kValidPayloadSize * 2;
packet.payload_offset = kDefaultPayloadBufferSize - kValidPayloadSize;
audio_renderer_->SendPacketNoReply(std::move(packet));
ExpectDisconnect();
}
// TODO(mpuryear): test EndOfStream();
// Also proper sequence of callbacks/completions
// TODO(mpuryear): test DiscardAllPackets() -> ();
// Also when no packets, when started
// TODO(mpuryear): test DiscardAllPacketsNoReply();
// Also when no packets, when started
//
// AudioRenderer validation
//
// AudioRenderer contains an internal state machine. To enter the "configured"
// state, it must receive and successfully execute both SetPcmStreamType and
// SetPayloadBuffer calls. From a Configured state only, it then transitions to
// "operational" mode when any packets are enqueued (received and not yet played
// and/or released).
// **** Before we enter Configured mode:
// SendPacket before SetPcmStreamType must fail.
// SendPacket before SetPayloadBuffer must fail.
// **** While in Configured mode:
// Before SendPacket, all valid SetPayloadBuffer should succeed.
// **** While in Operational mode:
// After SetPcmStreamType+SetPayloadBuffer, valid SendPacket should succeed.
// While renderer Operational, SetPcmStreamType must fail.
// While renderer Operational, SetPayloadBuffer must fail.
// Calling Flush must cancel+return all enqueued (sent) packets.
// **** Once back in Configured (non-Operational) mode
// Flush OR "enqueued packets drain" take renderer out of Operational.
// Once no packets are queued, all valid SetPcmStreamType should succeed.
// Once no packets are queued, all valid SetPayloadBuffer should succeed.
//
// Setting PCM format within known-supportable range of values should succeed.
// Before renderers are operational, multiple SetPcmStreamTypes should succeed.
// We test twice because of previous bug, where the first succeeded but any
// subsequent call (before Play) would cause a FIDL channel disconnect.
TEST_F(AudioRendererTest, SetPcmStreamType) {
fuchsia::media::AudioStreamType format;
format.sample_format = fuchsia::media::AudioSampleFormat::FLOAT;
format.channels = 2;
format.frames_per_second = 48000;
audio_renderer_->SetPcmStreamType(format);
fuchsia::media::AudioStreamType format2;
format2.sample_format = fuchsia::media::AudioSampleFormat::UNSIGNED_8;
format2.channels = 1;
format2.frames_per_second = 44100;
audio_renderer_->SetPcmStreamType(format2);
// Allow an error Disconnect callback, but we expect a timeout instead.
audio_renderer_->GetMinLeadTime(CompletionCallback([](int64_t x) {}));
ExpectCallback();
}
// TODO(mpuryear): test SetPtsUnits(uint32 tick_per_sec_num,uint32 denom);
// Also negative testing: zero values, nullptrs, huge num/small denom
// TODO(mpuryear): test SetPtsContinuityThreshold(float32 threshold_sec);
// Also negative testing: NaN, negative, very large, infinity
// TODO(mpuryear): test SetReferenceClock(handle reference_clock);
// Also negative testing: null handle, bad handle, handle to something else
// TODO(mpuryear): Also: when already in Play, very positive vals, very negative
// vals
TEST_F(AudioRendererTest, Play) {
// Configure with one buffer and a valid stream type.
CreateAndAddPayloadBuffer(0);
audio_renderer_->SetPcmStreamType(kTestStreamType);
// Send a packet (we don't care about the actual packet data here).
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = 0;
packet.payload_offset = 0;
packet.payload_size = kValidPayloadSize;
audio_renderer_->SendPacket(std::move(packet), CompletionCallback());
int64_t ref_time_received = -1;
int64_t media_time_received = -1;
audio_renderer_->Play(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP,
[&ref_time_received, &media_time_received](auto ref_time, auto media_time) {
ref_time_received = ref_time;
media_time_received = media_time;
});
// Note we expect that we receive the |Play| callback _before_ the
// |SendPacket| callback.
ExpectCallback();
ASSERT_NE(ref_time_received, -1);
ASSERT_NE(media_time_received, -1);
}
// TODO(mpuryear): Also: when already in Play, very positive vals, very negative
// vals
TEST_F(AudioRendererTest, PlayNoReply) {
// Configure with one buffer and a valid stream type.
CreateAndAddPayloadBuffer(0);
audio_renderer_->SetPcmStreamType(kTestStreamType);
// Send a packet (we don't care about the actual packet data here).
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = 0;
packet.payload_offset = 0;
packet.payload_size = kValidPayloadSize;
audio_renderer_->SendPacket(std::move(packet), CompletionCallback());
audio_renderer_->PlayNoReply(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP);
ExpectCallback();
}
// TODO(mpuryear): test Pause()->(int64 reference_time, int64 media_time);
// Verify success after setting format and submitting buffers.
// Also: when already in Pause
// TODO(mpuryear): test PauseNoReply();
// Verify success after setting format and submitting buffers.
// Also: when already in Pause
// Validate MinLeadTime events, when enabled.
TEST_F(AudioRendererTest, EnableMinLeadTimeEvents) {
int64_t min_lead_time = -1;
audio_renderer_.events().OnMinLeadTimeChanged = [&min_lead_time](int64_t min_lead_time_nsec) {
min_lead_time = min_lead_time_nsec;
};
audio_renderer_->EnableMinLeadTimeEvents(true);
// After enabling MinLeadTime events, we expect an initial notification.
// Because we have not yet set the format, we expect MinLeadTime to be 0.
EXPECT_TRUE(RunLoopWithTimeoutOrUntil(
[this, &min_lead_time]() { return error_occurred_ || (min_lead_time >= 0); },
kDurationResponseExpected, kDurationGranularity))
<< kTimeoutErr;
EXPECT_EQ(min_lead_time, 0);
// FYI: after setting format, MinLeadTime should change to be greater than 0
// IF the target has AudioOutput devices, or remain 0 (no callback) if it has
// none. Both are valid possibilities, so we don't test that aspect here.
}
// Validate MinLeadTime events, when disabled.
TEST_F(AudioRendererTest, DisableMinLeadTimeEvents) {
audio_renderer_.events().OnMinLeadTimeChanged =
CompletionCallback([](int64_t x) { EXPECT_FALSE(true) << kCallbackErr; });
audio_renderer_->EnableMinLeadTimeEvents(false);
// We should not receive a OnMinLeadTimeChanged callback (or Disconnect)
// before receiving this direct GetMinLeadTime callback.
audio_renderer_->GetMinLeadTime(CompletionCallback([](int64_t x) {}));
ExpectCallback();
}
//
// Basic validation of GetMinLeadTime() for the asynchronous AudioRenderer.
// Before SetPcmStreamType is called, MinLeadTime should equal zero.
TEST_F(AudioRendererTest, GetMinLeadTime) {
int64_t min_lead_time = -1;
audio_renderer_->GetMinLeadTime(
[&min_lead_time](int64_t min_lead_time_nsec) { min_lead_time = min_lead_time_nsec; });
// Wait to receive Lead time callback (will loop timeout? EXPECT_FALSE)
EXPECT_TRUE(RunLoopWithTimeoutOrUntil(
[this, &min_lead_time]() { return error_occurred_ || (min_lead_time >= 0); },
kDurationResponseExpected, kDurationGranularity))
<< kTimeoutErr;
EXPECT_EQ(min_lead_time, 0);
}
// Test creation and interface independence of GainControl.
// In a number of tests below, we run the message loop to give the AudioRenderer
// or GainControl binding a chance to disconnect, if an error occurred.
TEST_F(AudioRendererTest, BindGainControl) {
// Validate AudioRenderers can create GainControl interfaces.
audio_renderer_->BindGainControl(gain_control_.NewRequest());
bool gc_error_occurred = false;
auto gc_err_handler = [&gc_error_occurred](zx_status_t error) { gc_error_occurred = true; };
gain_control_.set_error_handler(gc_err_handler);
fuchsia::media::AudioRendererPtr audio_renderer_2;
audio_core_->CreateAudioRenderer(audio_renderer_2.NewRequest());
bool ar2_error_occurred = false;
auto ar2_err_handler = [&ar2_error_occurred](zx_status_t error) { ar2_error_occurred = true; };
audio_renderer_2.set_error_handler(ar2_err_handler);
fuchsia::media::audio::GainControlPtr gain_control_2;
audio_renderer_2->BindGainControl(gain_control_2.NewRequest());
bool gc2_error_occurred = false;
auto gc2_err_handler = [&gc2_error_occurred](zx_status_t error) { gc2_error_occurred = true; };
gain_control_2.set_error_handler(gc2_err_handler);
// Validate GainControl2 does NOT persist after audio_renderer_2 is unbound
audio_renderer_2.Unbind();
// Validate that audio_renderer_ persists without gain_control_
gain_control_.Unbind();
// Give audio_renderer_2 a chance to disconnect gain_control_2
EXPECT_TRUE(RunLoopWithTimeoutOrUntil(
[this, &ar2_error_occurred, &gc_error_occurred, &gc2_error_occurred]() {
return (error_occurred_ || ar2_error_occurred || gc_error_occurred || gc2_error_occurred);
},
kDurationResponseExpected, kDurationGranularity));
// Let audio_renderer_ show it is still alive (and allow other disconnects)
audio_renderer_->GetMinLeadTime(CompletionCallback([](int64_t x) {}));
ExpectCallback();
// Explicitly unbinding audio_renderer_2 should not trigger its disconnect
// (ar2_error_occurred), but should trigger gain_control_2's disconnect.
EXPECT_FALSE(ar2_error_occurred);
EXPECT_TRUE(gc2_error_occurred);
EXPECT_FALSE(gain_control_2.is_bound());
// Explicitly unbinding gain_control_ should not trigger its disconnect, nor
// its parent audio_renderer_'s.
EXPECT_FALSE(gc_error_occurred);
EXPECT_TRUE(audio_renderer_.is_bound());
}
//
// SetStreamType is not yet implemented. We expect the AudioRenderer binding to
// disconnect, and our AudioRenderer interface ptr to be reset.
TEST_F(AudioRendererTest, SetStreamType) {
fuchsia::media::AudioStreamType stream_format;
stream_format.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16;
stream_format.channels = 1;
stream_format.frames_per_second = 8000;
fuchsia::media::StreamType stream_type;
stream_type.encoding = fuchsia::media::AUDIO_ENCODING_LPCM;
stream_type.medium_specific.set_audio(stream_format);
audio_renderer_->SetStreamType(std::move(stream_type));
// Binding should Disconnect (EXPECT loop to NOT timeout)
ExpectDisconnect();
}
// Before setting format, Play should not succeed.
TEST_F(AudioRendererTest, PlayWithoutFormat) {
int64_t ref_time_received = -1;
int64_t media_time_received = -1;
audio_renderer_->Play(
fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP,
[&ref_time_received, &media_time_received](int64_t ref_time, int64_t media_time) {
ref_time_received = ref_time;
media_time_received = media_time;
});
// Disconnect callback should be received
ExpectDisconnect();
EXPECT_EQ(ref_time_received, -1);
EXPECT_EQ(media_time_received, -1);
}
// After setting format but before submitting buffers, Play should not succeed.
TEST_F(AudioRendererTest, PlayWithoutBuffers) {
fuchsia::media::AudioStreamType format;
format.sample_format = fuchsia::media::AudioSampleFormat::FLOAT;
format.channels = 1;
format.frames_per_second = 32000;
audio_renderer_->SetPcmStreamType(format);
int64_t ref_time_received = -1;
int64_t media_time_received = -1;
audio_renderer_->Play(
fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP,
[&ref_time_received, &media_time_received](int64_t ref_time, int64_t media_time) {
ref_time_received = ref_time;
media_time_received = media_time;
});
// Disconnect callback should be received
ExpectDisconnect();
EXPECT_EQ(ref_time_received, -1);
EXPECT_EQ(media_time_received, -1);
}
// Before setting format, PlayNoReply should cause a Disconnect.
TEST_F(AudioRendererTest, PlayNoReplyWithoutFormat) {
audio_renderer_->PlayNoReply(fuchsia::media::NO_TIMESTAMP, fuchsia::media::NO_TIMESTAMP);
// Disconnect callback should be received.
ExpectDisconnect();
}
// Before setting format, Pause should not succeed.
TEST_F(AudioRendererTest, PauseWithoutFormat) {
int64_t ref_time_received = -1;
int64_t media_time_received = -1;
audio_renderer_->Pause(
[&ref_time_received, &media_time_received](int64_t ref_time, int64_t media_time) {
ref_time_received = ref_time;
media_time_received = media_time;
});
// Disconnect callback should be received
ExpectDisconnect();
EXPECT_EQ(ref_time_received, -1);
EXPECT_EQ(media_time_received, -1);
}
// After setting format but before submitting buffers, Pause should not succeed.
TEST_F(AudioRendererTest, PauseWithoutBuffers) {
fuchsia::media::AudioStreamType format;
format.sample_format = fuchsia::media::AudioSampleFormat::FLOAT;
format.channels = 1;
format.frames_per_second = 32000;
audio_renderer_->SetPcmStreamType(format);
int64_t ref_time_received = -1;
int64_t media_time_received = -1;
audio_renderer_->Pause(
[&ref_time_received, &media_time_received](int64_t ref_time, int64_t media_time) {
ref_time_received = ref_time;
media_time_received = media_time;
});
// Disconnect callback should be received
ExpectDisconnect();
EXPECT_EQ(ref_time_received, -1);
EXPECT_EQ(media_time_received, -1);
}
// Before setting format, PauseNoReply should cause a Disconnect.
TEST_F(AudioRendererTest, PauseNoReplyWithoutFormat) {
audio_renderer_->PauseNoReply();
// Disconnect callback should be received.
ExpectDisconnect();
}
} // namespace media::audio::test