mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-20 14:00:32 +02:00
963 lines
34 KiB
C++
963 lines
34 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "mocktail/audio/audio_sink.h"
|
|
#include "mocktail/audio/opensl_simple_buffer_queue.h"
|
|
#include "mocktail/audio/sdl_audio_sink.h"
|
|
|
|
extern "C" std::uint32_t slCreateEngine(void** engine,
|
|
std::uint32_t num_options,
|
|
const void* options,
|
|
std::uint32_t num_interfaces,
|
|
const void* interface_ids,
|
|
const void* interface_required);
|
|
|
|
namespace mocktail::audio {
|
|
namespace {
|
|
|
|
using namespace std::chrono_literals;
|
|
|
|
class ReleaseGate {
|
|
public:
|
|
void WaitBeforeRelease() {
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
waiting_ = true;
|
|
cv_.notify_all();
|
|
cv_.wait(lock, [this] { return open_; });
|
|
}
|
|
|
|
bool WaitUntilBlocked() {
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
return cv_.wait_for(lock, 2s, [this] { return waiting_; });
|
|
}
|
|
|
|
void Open() {
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
open_ = true;
|
|
}
|
|
cv_.notify_all();
|
|
}
|
|
|
|
private:
|
|
std::mutex mutex_;
|
|
std::condition_variable cv_;
|
|
bool waiting_ = false;
|
|
bool open_ = false;
|
|
};
|
|
|
|
class ControlledAudioSink final : public AudioSink {
|
|
public:
|
|
explicit ControlledAudioSink(bool release_on_enqueue = false)
|
|
: release_on_enqueue_(release_on_enqueue) {}
|
|
|
|
const PcmSpec& source_spec() const override { return spec_; }
|
|
|
|
Status Enqueue(const PcmBuffer& buffer) override {
|
|
if (buffer.data == nullptr || buffer.size_bytes == 0) {
|
|
return Status::Error(StatusCode::kInvalidArgument, "empty buffer");
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (stopped_) {
|
|
return Status::Error(StatusCode::kFailedPrecondition, "stopped");
|
|
}
|
|
if (reject_next_enqueue_) {
|
|
reject_next_enqueue_ = false;
|
|
return Status::Error(StatusCode::kUnavailable, "enqueue rejected");
|
|
}
|
|
buffers_.push_back(buffer);
|
|
}
|
|
if (release_on_enqueue_) {
|
|
ReleaseOne();
|
|
}
|
|
return Status::Ok();
|
|
}
|
|
|
|
Status Pause() override {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
playing_ = false;
|
|
return stopped_ ? Status::Error(StatusCode::kFailedPrecondition, "stopped")
|
|
: Status::Ok();
|
|
}
|
|
|
|
Status Resume() override {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
playing_ = true;
|
|
return stopped_ ? Status::Error(StatusCode::kFailedPrecondition, "stopped")
|
|
: Status::Ok();
|
|
}
|
|
|
|
Status SetGain(float linear_gain) override {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (stopped_) {
|
|
return Status::Error(StatusCode::kFailedPrecondition, "stopped");
|
|
}
|
|
gain_ = linear_gain;
|
|
return Status::Ok();
|
|
}
|
|
|
|
Status Flush() override { return Status::Ok(); }
|
|
|
|
Status Clear() override {
|
|
ReleaseAll();
|
|
return Status::Ok();
|
|
}
|
|
|
|
Status GetQueuedBytes(std::size_t* size_bytes) const override {
|
|
if (size_bytes == nullptr) {
|
|
return Status::Error(StatusCode::kInvalidArgument, "null output");
|
|
}
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
*size_bytes = 0;
|
|
for (const PcmBuffer& buffer : buffers_) {
|
|
*size_bytes += buffer.size_bytes;
|
|
}
|
|
return Status::Ok();
|
|
}
|
|
|
|
void Shutdown() override {
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
stopped_ = true;
|
|
}
|
|
release_cv_.notify_all();
|
|
ReleaseAll();
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
release_cv_.wait(lock, [this] { return active_releases_ == 0; });
|
|
}
|
|
|
|
void ReleaseOne(ReleaseGate* gate = nullptr) {
|
|
PcmBuffer buffer;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (buffers_.empty()) {
|
|
return;
|
|
}
|
|
buffer = buffers_.front();
|
|
buffers_.erase(buffers_.begin());
|
|
++active_releases_;
|
|
}
|
|
if (gate != nullptr) {
|
|
gate->WaitBeforeRelease();
|
|
}
|
|
if (buffer.release_callback != nullptr) {
|
|
buffer.release_callback(buffer.release_context, buffer.data,
|
|
buffer.size_bytes);
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
--active_releases_;
|
|
}
|
|
release_cv_.notify_all();
|
|
}
|
|
|
|
bool WaitUntilShutdownStarts() {
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
return release_cv_.wait_for(lock, 2s, [this] { return stopped_; });
|
|
}
|
|
|
|
void RejectNextEnqueue() {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
reject_next_enqueue_ = true;
|
|
}
|
|
|
|
std::size_t buffer_count() const {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return buffers_.size();
|
|
}
|
|
|
|
bool playing() const {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return playing_;
|
|
}
|
|
|
|
float gain() const {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
return gain_;
|
|
}
|
|
|
|
private:
|
|
void ReleaseAll() {
|
|
for (;;) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (buffers_.empty()) {
|
|
return;
|
|
}
|
|
}
|
|
ReleaseOne();
|
|
}
|
|
}
|
|
|
|
const PcmSpec spec_;
|
|
const bool release_on_enqueue_;
|
|
mutable std::mutex mutex_;
|
|
std::condition_variable release_cv_;
|
|
std::vector<PcmBuffer> buffers_;
|
|
std::size_t active_releases_ = 0;
|
|
bool playing_ = false;
|
|
float gain_ = 1.0F;
|
|
bool stopped_ = false;
|
|
bool reject_next_enqueue_ = false;
|
|
};
|
|
|
|
class DelayedBufferRelease {
|
|
public:
|
|
explicit DelayedBufferRelease(ControlledAudioSink* sink)
|
|
: thread_([this, sink] { sink->ReleaseOne(&gate_); }) {}
|
|
|
|
~DelayedBufferRelease() {
|
|
if (thread_.joinable()) {
|
|
gate_.Open();
|
|
thread_.join();
|
|
}
|
|
}
|
|
|
|
bool WaitUntilBlocked() { return gate_.WaitUntilBlocked(); }
|
|
void AllowRelease() { gate_.Open(); }
|
|
void Join() { thread_.join(); }
|
|
|
|
private:
|
|
ReleaseGate gate_;
|
|
std::thread thread_;
|
|
};
|
|
|
|
struct CallbackProbe {
|
|
std::mutex mutex;
|
|
std::condition_variable cv;
|
|
int calls = 0;
|
|
};
|
|
|
|
void OpenSlQueueCallback(opensl_abi::AndroidSimpleBufferQueue,
|
|
void* context) {
|
|
auto* probe = static_cast<CallbackProbe*>(context);
|
|
{
|
|
std::lock_guard<std::mutex> lock(probe->mutex);
|
|
++probe->calls;
|
|
}
|
|
probe->cv.notify_all();
|
|
}
|
|
|
|
struct ReleaseProbe {
|
|
std::mutex mutex;
|
|
std::condition_variable cv;
|
|
const void* data = nullptr;
|
|
std::size_t size = 0;
|
|
int calls = 0;
|
|
};
|
|
|
|
struct BlockingCallbackProbe {
|
|
std::mutex mutex;
|
|
std::condition_variable cv;
|
|
bool started = false;
|
|
bool release = false;
|
|
};
|
|
|
|
void BlockingOpenSlQueueCallback(opensl_abi::AndroidSimpleBufferQueue,
|
|
void* context) {
|
|
auto* probe = static_cast<BlockingCallbackProbe*>(context);
|
|
std::unique_lock<std::mutex> lock(probe->mutex);
|
|
probe->started = true;
|
|
probe->cv.notify_all();
|
|
probe->cv.wait(lock, [&probe] { return probe->release; });
|
|
}
|
|
|
|
void BufferReleased(void* context, const void* data, std::size_t size) {
|
|
auto* probe = static_cast<ReleaseProbe*>(context);
|
|
{
|
|
std::lock_guard<std::mutex> lock(probe->mutex);
|
|
probe->data = data;
|
|
probe->size = size;
|
|
++probe->calls;
|
|
}
|
|
probe->cv.notify_all();
|
|
}
|
|
|
|
TEST(AudioSpecTest, ValidatesTypedPcmAndFrameSize) {
|
|
const PcmSpec stereo_s16{48000, 2, PcmSampleFormat::kSigned16LittleEndian};
|
|
EXPECT_TRUE(ValidatePcmSpec(stereo_s16).ok());
|
|
EXPECT_EQ(BytesPerFrame(stereo_s16), 4u);
|
|
|
|
PcmSpec invalid = stereo_s16;
|
|
invalid.channels = 0;
|
|
EXPECT_EQ(ValidatePcmSpec(invalid).code(), StatusCode::kInvalidArgument);
|
|
invalid = stereo_s16;
|
|
invalid.sample_rate_hz = 1000;
|
|
EXPECT_EQ(ValidatePcmSpec(invalid).code(), StatusCode::kInvalidArgument);
|
|
}
|
|
|
|
TEST(SdlPlaybackDeviceTest, ResolvesDefaultAndExactPhysicalName) {
|
|
const std::vector<SdlPlaybackDevice> devices = {
|
|
{17, "Built-in Audio Analog Stereo"}, {23, "USB Headset"}};
|
|
std::uint32_t device_id = 99;
|
|
std::string resolved;
|
|
|
|
ASSERT_TRUE(
|
|
ResolveSdlPlaybackDevice("default", devices, &device_id, &resolved).ok());
|
|
EXPECT_EQ(device_id, 0U);
|
|
EXPECT_EQ(resolved, "default");
|
|
|
|
ASSERT_TRUE(
|
|
ResolveSdlPlaybackDevice("USB Headset", devices, &device_id, &resolved)
|
|
.ok());
|
|
EXPECT_EQ(device_id, 23U);
|
|
EXPECT_EQ(resolved, "USB Headset");
|
|
}
|
|
|
|
TEST(SdlPlaybackDeviceTest, RejectsMissingAmbiguousAndUnsafeNames) {
|
|
std::uint32_t device_id = 0;
|
|
std::string resolved;
|
|
EXPECT_EQ(ResolveSdlPlaybackDevice("Missing", {{17, "Speakers"}}, &device_id,
|
|
&resolved)
|
|
.code(),
|
|
StatusCode::kUnavailable);
|
|
EXPECT_EQ(
|
|
ResolveSdlPlaybackDevice("Speakers", {{17, "Speakers"}, {23, "Speakers"}},
|
|
&device_id, &resolved)
|
|
.code(),
|
|
StatusCode::kInvalidArgument);
|
|
EXPECT_EQ(
|
|
ResolveSdlPlaybackDevice("Speaker\nInjected", {}, &device_id, &resolved)
|
|
.code(),
|
|
StatusCode::kInvalidArgument);
|
|
}
|
|
|
|
TEST(SdlAudioSubsystemTest, EnforcesMainThreadOwnershipAndLiveSinkOrder) {
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
std::vector<SdlPlaybackDevice> devices;
|
|
std::string resolved;
|
|
ASSERT_TRUE(ConfigureSdlPlaybackDevice("default", &devices, &resolved).ok());
|
|
EXPECT_EQ(resolved, "default");
|
|
SdlAudioSinkOptions options;
|
|
std::unique_ptr<AudioSink> sink;
|
|
ASSERT_TRUE(CreateSdlAudioSink(options, &sink).ok());
|
|
EXPECT_EQ(ConfigureSdlPlaybackDevice("default", &devices, &resolved).code(),
|
|
StatusCode::kFailedPrecondition);
|
|
EXPECT_EQ(ShutdownSdlAudioSubsystem().code(),
|
|
StatusCode::kFailedPrecondition);
|
|
|
|
Status worker_shutdown;
|
|
std::thread worker(
|
|
[&worker_shutdown] { worker_shutdown = ShutdownSdlAudioSubsystem(); });
|
|
worker.join();
|
|
EXPECT_EQ(worker_shutdown.code(), StatusCode::kFailedPrecondition);
|
|
|
|
sink->Shutdown();
|
|
sink.reset();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
}
|
|
|
|
TEST(SdlAudioSubsystemTest, OpensExactEnumeratedPlaybackDevice) {
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
std::vector<SdlPlaybackDevice> devices;
|
|
ASSERT_TRUE(ListSdlPlaybackDevices(&devices).ok());
|
|
if (devices.empty()) {
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
GTEST_SKIP() << "SDL audio driver exposed no physical playback device";
|
|
}
|
|
|
|
const std::string requested = devices.front().name;
|
|
std::string resolved;
|
|
ASSERT_TRUE(ConfigureSdlPlaybackDevice(requested, &devices, &resolved).ok());
|
|
EXPECT_EQ(resolved, requested);
|
|
SdlAudioSinkOptions options;
|
|
std::unique_ptr<AudioSink> sink;
|
|
const Status status = CreateSdlAudioSink(options, &sink);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
sink->Shutdown();
|
|
sink.reset();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
}
|
|
|
|
TEST(SdlAudioSubsystemTest, MigratesLiveStreamWithoutDroppingQueuedPcm) {
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
std::vector<SdlPlaybackDevice> devices;
|
|
std::string resolved;
|
|
ASSERT_TRUE(ConfigureSdlPlaybackDevice("default", &devices, &resolved).ok());
|
|
|
|
SdlAudioSinkOptions options;
|
|
options.start_paused = true;
|
|
std::unique_ptr<AudioSink> sink;
|
|
ASSERT_TRUE(CreateSdlAudioSink(options, &sink).ok());
|
|
const std::vector<std::int16_t> samples(256 * 2, 91);
|
|
ASSERT_TRUE(sink->Enqueue(PcmBuffer{samples.data(),
|
|
samples.size() * sizeof(samples.front())})
|
|
.ok());
|
|
|
|
if (!devices.empty()) {
|
|
ASSERT_TRUE(SwitchSdlPlaybackDevice(devices.front().id, &resolved).ok());
|
|
EXPECT_EQ(resolved, devices.front().name);
|
|
std::uint32_t configured_id = 0;
|
|
std::string configured_name;
|
|
ASSERT_TRUE(
|
|
GetConfiguredSdlPlaybackDevice(&configured_id, &configured_name).ok());
|
|
EXPECT_EQ(configured_id, devices.front().id);
|
|
EXPECT_EQ(configured_name, devices.front().name);
|
|
}
|
|
|
|
std::size_t queued = 0;
|
|
ASSERT_TRUE(sink->GetQueuedBytes(&queued).ok());
|
|
EXPECT_EQ(queued, samples.size() * sizeof(samples.front()));
|
|
ASSERT_TRUE(SwitchSdlPlaybackDevice(0, &resolved).ok());
|
|
EXPECT_EQ(resolved, "default");
|
|
ASSERT_TRUE(sink->GetQueuedBytes(&queued).ok());
|
|
EXPECT_EQ(queued, samples.size() * sizeof(samples.front()));
|
|
|
|
sink->Shutdown();
|
|
sink.reset();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
}
|
|
|
|
TEST(SdlAudioSinkTest, QueuesRealPcmIntoPausedSdlStream) {
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
SdlAudioSinkOptions options;
|
|
options.start_paused = true;
|
|
std::unique_ptr<AudioSink> sink;
|
|
Status status = CreateSdlAudioSink(options, &sink);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
|
|
const std::vector<std::int16_t> samples(512 * 2, 123);
|
|
status = sink->Enqueue(
|
|
PcmBuffer{samples.data(), samples.size() * sizeof(samples.front())});
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
std::size_t queued = 0;
|
|
status = sink->GetQueuedBytes(&queued);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
EXPECT_EQ(queued, samples.size() * sizeof(samples.front()));
|
|
|
|
status = sink->Clear();
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
ASSERT_TRUE(sink->GetQueuedBytes(&queued).ok());
|
|
EXPECT_EQ(queued, 0u);
|
|
sink->Shutdown();
|
|
EXPECT_EQ(sink->Resume().code(), StatusCode::kFailedPrecondition);
|
|
sink.reset();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
}
|
|
|
|
TEST(SdlAudioSinkTest, ReleasesBorrowedBufferWhenStreamIsCleared) {
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
SdlAudioSinkOptions options;
|
|
std::unique_ptr<AudioSink> sink;
|
|
Status status = CreateSdlAudioSink(options, &sink);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
|
|
const std::vector<std::int16_t> samples(128 * 2, 321);
|
|
ReleaseProbe probe;
|
|
status = sink->Enqueue(PcmBuffer{samples.data(),
|
|
samples.size() * sizeof(samples.front()),
|
|
BufferReleased, &probe});
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
ASSERT_TRUE(sink->Clear().ok());
|
|
|
|
std::unique_lock<std::mutex> lock(probe.mutex);
|
|
ASSERT_TRUE(
|
|
probe.cv.wait_for(lock, 2s, [&probe] { return probe.calls == 1; }));
|
|
EXPECT_EQ(probe.data, samples.data());
|
|
EXPECT_EQ(probe.size, samples.size() * sizeof(samples.front()));
|
|
sink->Shutdown();
|
|
sink.reset();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, DeliversPcmToSinkAndDispatchesCompletion) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
ControlledAudioSink* controlled_view = controlled.get();
|
|
CallbackProbe probe;
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
Status status = OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled),
|
|
{}, &adapter);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
|
|
auto queue = adapter->interface();
|
|
ASSERT_NE(queue, nullptr);
|
|
ASSERT_EQ((*queue)->RegisterCallback(queue, OpenSlQueueCallback, &probe),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
const std::vector<std::int16_t> samples(64 * 2, 456);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
samples.size() * sizeof(samples.front()))),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(controlled_view->buffer_count(), 1u);
|
|
|
|
opensl_abi::AndroidSimpleBufferQueueState queue_state{};
|
|
ASSERT_EQ((*queue)->GetState(queue, &queue_state),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(queue_state.count, 1u);
|
|
controlled_view->ReleaseOne();
|
|
|
|
{
|
|
std::unique_lock<std::mutex> lock(probe.mutex);
|
|
ASSERT_TRUE(
|
|
probe.cv.wait_for(lock, 2s, [&probe] { return probe.calls == 1; }));
|
|
}
|
|
ASSERT_EQ((*queue)->GetState(queue, &queue_state),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(queue_state.count, 0u);
|
|
EXPECT_EQ(queue_state.index, 1u);
|
|
const OpenSlSimpleBufferQueueStats stats = adapter->GetStats();
|
|
EXPECT_EQ(stats.submitted_buffers, 1u);
|
|
EXPECT_EQ(stats.consumed_buffers, 1u);
|
|
EXPECT_EQ(stats.discarded_buffers, 0u);
|
|
EXPECT_EQ(stats.pending_buffers, 0u);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, DelegatesLinearGainToTheHostSink) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
ControlledAudioSink* controlled_view = controlled.get();
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled), {},
|
|
&adapter)
|
|
.ok());
|
|
|
|
ASSERT_TRUE(adapter->SetGain(0.25F).ok());
|
|
EXPECT_FLOAT_EQ(controlled_view->gain(), 0.25F);
|
|
adapter->Shutdown();
|
|
EXPECT_EQ(adapter->SetGain(1.0F).code(), StatusCode::kFailedPrecondition);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, SdlCompletionFollowsRealStreamConsumption) {
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
SdlAudioSinkOptions sink_options;
|
|
sink_options.start_paused = true;
|
|
std::unique_ptr<AudioSink> sink;
|
|
Status status = CreateSdlAudioSink(sink_options, &sink);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
status =
|
|
OpenSlSimpleBufferQueueAdapter::Create(std::move(sink), {}, &adapter);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
auto queue = adapter->interface();
|
|
CallbackProbe probe;
|
|
ASSERT_EQ((*queue)->RegisterCallback(queue, OpenSlQueueCallback, &probe),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
const std::vector<std::int16_t> samples(4096 * 2, 222);
|
|
const std::vector<std::int16_t> following_samples(4096 * 2, 333);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
samples.size() * sizeof(samples.front()))),
|
|
opensl_abi::kResultSuccess);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, following_samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
following_samples.size() *
|
|
sizeof(following_samples.front()))),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
std::this_thread::sleep_for(20ms);
|
|
{
|
|
std::lock_guard<std::mutex> lock(probe.mutex);
|
|
EXPECT_EQ(probe.calls, 0) << "paused SDL stream consumed a borrowed buffer";
|
|
}
|
|
opensl_abi::AndroidSimpleBufferQueueState queue_state{};
|
|
ASSERT_EQ((*queue)->GetState(queue, &queue_state),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(queue_state.count, 2u);
|
|
|
|
status = adapter->SetPlaying(true);
|
|
ASSERT_TRUE(status.ok()) << status.message();
|
|
{
|
|
std::unique_lock<std::mutex> lock(probe.mutex);
|
|
ASSERT_TRUE(
|
|
probe.cv.wait_for(lock, 2s, [&probe] { return probe.calls >= 1; }));
|
|
}
|
|
ASSERT_EQ((*queue)->GetState(queue, &queue_state),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_LT(queue_state.count, 2u);
|
|
EXPECT_GE(queue_state.index, 1u);
|
|
adapter->Shutdown();
|
|
adapter.reset();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, ClearSuppressesCallbacksForDiscardedBuffers) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled), {},
|
|
&adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
CallbackProbe probe;
|
|
ASSERT_EQ((*queue)->RegisterCallback(queue, OpenSlQueueCallback, &probe),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
const std::vector<std::int16_t> samples(32 * 2, 789);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
samples.size() * sizeof(samples.front()))),
|
|
opensl_abi::kResultSuccess);
|
|
ASSERT_EQ((*queue)->Clear(queue), opensl_abi::kResultSuccess);
|
|
std::this_thread::sleep_for(20ms);
|
|
std::lock_guard<std::mutex> lock(probe.mutex);
|
|
EXPECT_EQ(probe.calls, 0);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, DelayedReleaseAfterClearPreservesReenqueuedBuffer) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
ControlledAudioSink* controlled_view = controlled.get();
|
|
OpenSlSimpleBufferQueueOptions options;
|
|
options.max_buffers = 1;
|
|
CallbackProbe probe;
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled),
|
|
options, &adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
ASSERT_EQ((*queue)->RegisterCallback(queue, OpenSlQueueCallback, &probe),
|
|
opensl_abi::kResultSuccess);
|
|
const std::vector<std::int16_t> samples(32 * 2, 789);
|
|
const auto bytes =
|
|
static_cast<opensl_abi::Uint32>(samples.size() * sizeof(samples.front()));
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
DelayedBufferRelease release(controlled_view);
|
|
ASSERT_TRUE(release.WaitUntilBlocked());
|
|
ASSERT_EQ(controlled_view->buffer_count(), 0u);
|
|
ASSERT_EQ((*queue)->Clear(queue), opensl_abi::kResultSuccess);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultSuccess);
|
|
release.AllowRelease();
|
|
release.Join();
|
|
|
|
opensl_abi::AndroidSimpleBufferQueueState queue_state{};
|
|
ASSERT_EQ((*queue)->GetState(queue, &queue_state),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(queue_state.count, 1u);
|
|
EXPECT_EQ(queue_state.index, 0u);
|
|
const auto before_release = adapter->GetStats();
|
|
EXPECT_EQ(before_release.submitted_buffers, 2u);
|
|
EXPECT_EQ(before_release.pending_buffers, 1u);
|
|
EXPECT_EQ(before_release.consumed_buffers, 0u);
|
|
EXPECT_EQ(before_release.discarded_buffers, 1u);
|
|
EXPECT_EQ(before_release.consumed_bytes, 0u);
|
|
|
|
controlled_view->ReleaseOne();
|
|
{
|
|
std::unique_lock<std::mutex> lock(probe.mutex);
|
|
EXPECT_TRUE(probe.cv.wait_for(lock, 2s, [&probe] { return probe.calls >= 1; }));
|
|
}
|
|
adapter->Shutdown();
|
|
{
|
|
std::lock_guard<std::mutex> lock(probe.mutex);
|
|
EXPECT_EQ(probe.calls, 1);
|
|
}
|
|
ASSERT_EQ((*queue)->GetState(queue, &queue_state),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(queue_state.count, 0u);
|
|
EXPECT_EQ(queue_state.index, 1u);
|
|
const auto after_release = adapter->GetStats();
|
|
EXPECT_EQ(after_release.pending_buffers, 0u);
|
|
EXPECT_EQ(after_release.consumed_buffers, 1u);
|
|
EXPECT_EQ(after_release.discarded_buffers, 1u);
|
|
EXPECT_EQ(after_release.consumed_bytes, bytes);
|
|
EXPECT_TRUE(adapter->last_error().ok());
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, ClearCanRaceBorrowedBufferRelease) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
ControlledAudioSink* controlled_view = controlled.get();
|
|
OpenSlSimpleBufferQueueOptions options;
|
|
options.max_buffers = 1;
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled),
|
|
options, &adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
const std::vector<std::int16_t> samples(32 * 2, 789);
|
|
const auto bytes =
|
|
static_cast<opensl_abi::Uint32>(samples.size() * sizeof(samples.front()));
|
|
constexpr std::uint64_t kIterations = 64;
|
|
for (std::uint64_t iteration = 0; iteration < kIterations; ++iteration) {
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultSuccess);
|
|
DelayedBufferRelease release(controlled_view);
|
|
ASSERT_TRUE(release.WaitUntilBlocked());
|
|
// Clear and ReleaseBuffer both run after this gate. Joining only after
|
|
// Clear avoids ordering the ticket reset before the callback's first read.
|
|
release.AllowRelease();
|
|
ASSERT_EQ((*queue)->Clear(queue), opensl_abi::kResultSuccess);
|
|
release.Join();
|
|
EXPECT_EQ(adapter->GetStats().pending_buffers, 0u);
|
|
}
|
|
adapter->Shutdown();
|
|
const auto stats = adapter->GetStats();
|
|
EXPECT_EQ(stats.submitted_buffers, kIterations);
|
|
EXPECT_EQ(stats.consumed_buffers + stats.discarded_buffers, kIterations);
|
|
EXPECT_EQ(stats.pending_buffers, 0u);
|
|
EXPECT_TRUE(adapter->last_error().ok());
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, ShutdownWaitsForDelayedReleaseAfterClear) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
ControlledAudioSink* controlled_view = controlled.get();
|
|
OpenSlSimpleBufferQueueOptions options;
|
|
options.max_buffers = 1;
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled),
|
|
options, &adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
const std::vector<std::int16_t> samples(32 * 2, 789);
|
|
const auto bytes =
|
|
static_cast<opensl_abi::Uint32>(samples.size() * sizeof(samples.front()));
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultSuccess);
|
|
DelayedBufferRelease release(controlled_view);
|
|
ASSERT_TRUE(release.WaitUntilBlocked());
|
|
ASSERT_EQ((*queue)->Clear(queue), opensl_abi::kResultSuccess);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
std::atomic<bool> shutdown_done{false};
|
|
std::thread shutdown_thread([&] {
|
|
adapter->Shutdown();
|
|
shutdown_done.store(true);
|
|
});
|
|
EXPECT_TRUE(controlled_view->WaitUntilShutdownStarts());
|
|
EXPECT_FALSE(shutdown_done.load());
|
|
release.AllowRelease();
|
|
release.Join();
|
|
shutdown_thread.join();
|
|
EXPECT_TRUE(shutdown_done.load());
|
|
EXPECT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultPreconditionsViolated);
|
|
const auto stats = adapter->GetStats();
|
|
EXPECT_EQ(stats.pending_buffers, 0u);
|
|
EXPECT_EQ(stats.consumed_buffers, 0u);
|
|
EXPECT_EQ(stats.discarded_buffers, 2u);
|
|
EXPECT_TRUE(adapter->last_error().ok());
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, FailedEnqueuePreservesCapacityAndCounters) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
ControlledAudioSink* controlled_view = controlled.get();
|
|
OpenSlSimpleBufferQueueOptions options;
|
|
options.max_buffers = 1;
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled),
|
|
options, &adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
const std::vector<std::int16_t> samples(32 * 2, 789);
|
|
const auto bytes =
|
|
static_cast<opensl_abi::Uint32>(samples.size() * sizeof(samples.front()));
|
|
controlled_view->RejectNextEnqueue();
|
|
EXPECT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultResourceError);
|
|
EXPECT_EQ(adapter->GetStats().submitted_buffers, 0u);
|
|
EXPECT_EQ(adapter->GetStats().pending_buffers, 0u);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultSuccess);
|
|
controlled_view->ReleaseOne();
|
|
const auto stats = adapter->GetStats();
|
|
EXPECT_EQ(stats.submitted_buffers, 1u);
|
|
EXPECT_EQ(stats.consumed_buffers, 1u);
|
|
EXPECT_EQ(stats.discarded_buffers, 0u);
|
|
EXPECT_EQ(stats.pending_buffers, 0u);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, EnqueueCanReleaseBorrowedBufferSynchronously) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>(true);
|
|
OpenSlSimpleBufferQueueOptions options;
|
|
options.max_buffers = 1;
|
|
CallbackProbe probe;
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled),
|
|
options, &adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
ASSERT_EQ((*queue)->RegisterCallback(queue, OpenSlQueueCallback, &probe),
|
|
opensl_abi::kResultSuccess);
|
|
const std::vector<std::int16_t> samples(32 * 2, 789);
|
|
const auto bytes =
|
|
static_cast<opensl_abi::Uint32>(samples.size() * sizeof(samples.front()));
|
|
for (int enqueue = 0; enqueue < 2; ++enqueue) {
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(adapter->GetStats().pending_buffers, 0u);
|
|
}
|
|
{
|
|
std::unique_lock<std::mutex> lock(probe.mutex);
|
|
EXPECT_TRUE(probe.cv.wait_for(lock, 2s, [&probe] { return probe.calls == 2; }));
|
|
}
|
|
adapter->Shutdown();
|
|
const auto stats = adapter->GetStats();
|
|
EXPECT_EQ(stats.submitted_buffers, 2u);
|
|
EXPECT_EQ(stats.consumed_buffers, 2u);
|
|
EXPECT_EQ(stats.discarded_buffers, 0u);
|
|
EXPECT_EQ(stats.consumed_bytes, 2u * bytes);
|
|
EXPECT_TRUE(adapter->last_error().ok());
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, RejectsOverflowAndShutsDownFailClosed) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
OpenSlSimpleBufferQueueOptions options;
|
|
options.max_buffers = 1;
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled),
|
|
options, &adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
const std::vector<std::int16_t> samples(32 * 2, 111);
|
|
const auto bytes =
|
|
static_cast<opensl_abi::Uint32>(samples.size() * sizeof(samples.front()));
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultBufferInsufficient);
|
|
adapter->Shutdown();
|
|
EXPECT_EQ((*queue)->Enqueue(queue, samples.data(), bytes),
|
|
opensl_abi::kResultPreconditionsViolated);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, ShutdownWaitsForInFlightCallback) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
ControlledAudioSink* controlled_view = controlled.get();
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled), {},
|
|
&adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
BlockingCallbackProbe callback;
|
|
ASSERT_EQ(
|
|
(*queue)->RegisterCallback(queue, BlockingOpenSlQueueCallback, &callback),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
const std::vector<std::int16_t> samples(32 * 2, 444);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
samples.size() * sizeof(samples.front()))),
|
|
opensl_abi::kResultSuccess);
|
|
controlled_view->ReleaseOne();
|
|
{
|
|
std::unique_lock<std::mutex> lock(callback.mutex);
|
|
ASSERT_TRUE(callback.cv.wait_for(lock, 2s,
|
|
[&callback] { return callback.started; }));
|
|
}
|
|
|
|
std::atomic<bool> shutdown_done = false;
|
|
std::thread shutdown_thread([&adapter, &shutdown_done] {
|
|
adapter->Shutdown();
|
|
shutdown_done.store(true);
|
|
});
|
|
std::this_thread::sleep_for(20ms);
|
|
EXPECT_FALSE(shutdown_done.load());
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(callback.mutex);
|
|
callback.release = true;
|
|
}
|
|
callback.cv.notify_all();
|
|
shutdown_thread.join();
|
|
EXPECT_TRUE(shutdown_done.load());
|
|
EXPECT_EQ((*queue)->Enqueue(queue, samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
samples.size() * sizeof(samples.front()))),
|
|
opensl_abi::kResultPreconditionsViolated);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, GetStatePollsWhilePlaybackReleasesBuffers) {
|
|
auto controlled = std::make_unique<ControlledAudioSink>();
|
|
ControlledAudioSink* controlled_view = controlled.get();
|
|
CallbackProbe probe;
|
|
std::unique_ptr<OpenSlSimpleBufferQueueAdapter> adapter;
|
|
ASSERT_TRUE(OpenSlSimpleBufferQueueAdapter::Create(std::move(controlled), {},
|
|
&adapter)
|
|
.ok());
|
|
auto queue = adapter->interface();
|
|
ASSERT_EQ((*queue)->RegisterCallback(queue, OpenSlQueueCallback, &probe),
|
|
opensl_abi::kResultSuccess);
|
|
|
|
std::atomic<bool> stop_polling{false};
|
|
std::atomic<int> polls{0};
|
|
std::atomic<int> poll_failures{0};
|
|
std::thread poller([&] {
|
|
while (!stop_polling.load(std::memory_order_relaxed)) {
|
|
opensl_abi::AndroidSimpleBufferQueueState queue_state{};
|
|
if ((*queue)->GetState(queue, &queue_state) !=
|
|
opensl_abi::kResultSuccess) {
|
|
poll_failures.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
polls.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
});
|
|
|
|
const std::vector<std::int16_t> samples(32 * 2, 321);
|
|
ASSERT_EQ((*queue)->Enqueue(queue, samples.data(),
|
|
static_cast<opensl_abi::Uint32>(
|
|
samples.size() * sizeof(samples.front()))),
|
|
opensl_abi::kResultSuccess);
|
|
opensl_abi::AndroidSimpleBufferQueueState queued{};
|
|
ASSERT_EQ((*queue)->GetState(queue, &queued), opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(queued.count, 1u);
|
|
controlled_view->ReleaseOne();
|
|
{
|
|
std::unique_lock<std::mutex> lock(probe.mutex);
|
|
ASSERT_TRUE(
|
|
probe.cv.wait_for(lock, 2s, [&probe] { return probe.calls == 1; }));
|
|
}
|
|
|
|
// A freshly spawned thread is not guaranteed to have been scheduled yet, so
|
|
// wait for the first concurrent poll before asking the poller to stop.
|
|
const auto poll_deadline = std::chrono::steady_clock::now() + 2s;
|
|
while (polls.load(std::memory_order_relaxed) == 0 &&
|
|
std::chrono::steady_clock::now() < poll_deadline) {
|
|
std::this_thread::yield();
|
|
}
|
|
stop_polling.store(true, std::memory_order_relaxed);
|
|
poller.join();
|
|
EXPECT_GT(polls.load(std::memory_order_relaxed), 0);
|
|
EXPECT_EQ(poll_failures.load(std::memory_order_relaxed), 0);
|
|
|
|
opensl_abi::AndroidSimpleBufferQueueState drained{};
|
|
ASSERT_EQ((*queue)->GetState(queue, &drained), opensl_abi::kResultSuccess);
|
|
EXPECT_EQ(drained.count, 0u);
|
|
EXPECT_EQ(drained.index, 1u);
|
|
}
|
|
|
|
TEST(OpenSlQueueTest, RecorderIsExplicitlyUnsupported) {
|
|
const Status status = OpenSlRecorderSupportStatus();
|
|
EXPECT_FALSE(status.ok());
|
|
EXPECT_EQ(status.code(), StatusCode::kUnsupported);
|
|
}
|
|
|
|
TEST(LegacyOpenSlStubTest, RequiresExactTestOptIn) {
|
|
unsetenv("MOCKTAIL_ENABLE_TEST_AUDIO_STUBS");
|
|
void* engine = reinterpret_cast<void*>(0x1);
|
|
EXPECT_EQ(slCreateEngine(&engine, 0, nullptr, 0, nullptr, nullptr),
|
|
opensl_abi::kResultFeatureUnsupported);
|
|
EXPECT_EQ(engine, nullptr);
|
|
|
|
setenv("MOCKTAIL_ENABLE_TEST_AUDIO_STUBS", "true", 1);
|
|
engine = reinterpret_cast<void*>(0x1);
|
|
EXPECT_EQ(slCreateEngine(&engine, 0, nullptr, 0, nullptr, nullptr),
|
|
opensl_abi::kResultFeatureUnsupported);
|
|
EXPECT_EQ(engine, nullptr);
|
|
|
|
setenv("MOCKTAIL_ENABLE_TEST_AUDIO_STUBS", "1", 1);
|
|
EXPECT_EQ(slCreateEngine(&engine, 0, nullptr, 0, nullptr, nullptr),
|
|
opensl_abi::kResultSuccess);
|
|
EXPECT_NE(engine, nullptr);
|
|
unsetenv("MOCKTAIL_ENABLE_TEST_AUDIO_STUBS");
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace mocktail::audio
|