mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-18 15:00:46 +02:00
187 lines
6.8 KiB
C++
187 lines
6.8 KiB
C++
#include "audio/native_input_capture.h"
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <gtest/gtest.h>
|
|
#include <unistd.h>
|
|
|
|
#include <chrono>
|
|
#include <thread>
|
|
|
|
#include "mocktail/audio/sdl_audio_sink.h"
|
|
|
|
namespace mocktail::audio {
|
|
namespace {
|
|
|
|
struct TestSink {
|
|
const std::uintptr_t* vtable;
|
|
std::thread::id poll_thread = std::this_thread::get_id();
|
|
int frames = 0;
|
|
bool nonzero = false;
|
|
bool invalid = false;
|
|
};
|
|
|
|
struct TestOwner {
|
|
const std::uintptr_t* vtable;
|
|
long shared_owners = 0;
|
|
long weak_owners = 0;
|
|
int destroyed = 0;
|
|
int released = 0;
|
|
};
|
|
|
|
std::size_t ReceivePcm(void* self, const void* pcm, std::size_t samples,
|
|
std::uint64_t delay, const NativePcmFormat* format) {
|
|
auto& sink = *static_cast<TestSink*>(self);
|
|
sink.invalid |= samples != 480 || format->rate != 48000 ||
|
|
format->channels != 1 || format->bytes_per_sample != 2 ||
|
|
delay < 10000000 || delay > 200000000 ||
|
|
std::this_thread::get_id() != sink.poll_thread;
|
|
const auto* data = static_cast<const std::int16_t*>(pcm);
|
|
sink.nonzero |= std::any_of(data, data + samples,
|
|
[](auto sample) { return sample != 0; });
|
|
++sink.frames;
|
|
return samples;
|
|
}
|
|
|
|
void DestroySink(void* self) { ++static_cast<TestOwner*>(self)->destroyed; }
|
|
void ReleaseOwner(void* self) { ++static_cast<TestOwner*>(self)->released; }
|
|
void OriginalLatency(void*, std::uint32_t* playback, std::uint32_t* recording) {
|
|
if (playback) *playback = 42;
|
|
if (recording) *recording = 99;
|
|
}
|
|
|
|
const std::array<std::uintptr_t, 3> kSinkVtable{
|
|
0, 0, reinterpret_cast<std::uintptr_t>(&ReceivePcm)};
|
|
const std::array<std::uintptr_t, 5> kOwnerVtable{
|
|
0, 0, reinterpret_cast<std::uintptr_t>(&DestroySink), 0,
|
|
reinterpret_cast<std::uintptr_t>(&ReleaseOwner)};
|
|
|
|
bool TestRelro(std::uintptr_t base, std::uintptr_t rva, std::size_t size) {
|
|
return (base + rva == reinterpret_cast<std::uintptr_t>(kSinkVtable.data()) &&
|
|
size <= sizeof(kSinkVtable)) ||
|
|
(base + rva == reinterpret_cast<std::uintptr_t>(kOwnerVtable.data()) &&
|
|
size <= sizeof(kOwnerVtable));
|
|
}
|
|
|
|
bool TestCode(std::uintptr_t base, std::uintptr_t rva, std::size_t) {
|
|
return base + rva == kSinkVtable[2] || base + rva == kOwnerVtable[2] ||
|
|
base + rva == kOwnerVtable[4];
|
|
}
|
|
|
|
class NativeInputCaptureTest : public testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
const int fd = mkstemp(pcm_path_);
|
|
ASSERT_GE(fd, 0);
|
|
FILE* file = fdopen(fd, "wb");
|
|
if (!file) close(fd);
|
|
ASSERT_NE(file, nullptr);
|
|
const std::vector<unsigned char> samples(1024 * 1024, 0x3f);
|
|
const auto written = std::fwrite(samples.data(), 1, samples.size(), file);
|
|
const int closed = std::fclose(file);
|
|
ASSERT_EQ(written, samples.size());
|
|
ASSERT_EQ(closed, 0);
|
|
ASSERT_TRUE(SDL_SetHintWithPriority(SDL_HINT_AUDIO_DRIVER, "disk",
|
|
SDL_HINT_OVERRIDE));
|
|
ASSERT_TRUE(SDL_SetHintWithPriority(SDL_HINT_AUDIO_DISK_INPUT_FILE,
|
|
pcm_path_, SDL_HINT_OVERRIDE));
|
|
ASSERT_TRUE(SDL_SetHintWithPriority(SDL_HINT_AUDIO_DISK_OUTPUT_FILE,
|
|
"/dev/null", SDL_HINT_OVERRIDE));
|
|
ASSERT_TRUE(InitializeSdlAudioSubsystem().ok());
|
|
ASSERT_STREQ(SDL_GetCurrentAudioDriver(), "disk");
|
|
}
|
|
|
|
void TearDown() override {
|
|
std::atomic_store(&g_native_capture, std::shared_ptr<NativeInputCapture>{});
|
|
if (capture_) capture_->Shutdown();
|
|
EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok());
|
|
SDL_ResetHint(SDL_HINT_AUDIO_DRIVER);
|
|
SDL_ResetHint(SDL_HINT_AUDIO_DISK_INPUT_FILE);
|
|
SDL_ResetHint(SDL_HINT_AUDIO_DISK_OUTPUT_FILE);
|
|
std::remove(pcm_path_);
|
|
}
|
|
|
|
void Publish(bool enabled) {
|
|
capture_ = std::make_shared<NativeInputCapture>(
|
|
NativeInputCapture::SinkAbi{1, &TestRelro, &TestCode, &OriginalLatency},
|
|
enabled);
|
|
std::atomic_store(&g_native_capture, capture_);
|
|
}
|
|
|
|
char pcm_path_[64] = "/tmp/mocktail-native-capture-XXXXXX";
|
|
std::shared_ptr<NativeInputCapture> capture_;
|
|
TestSink sink_{kSinkVtable.data()};
|
|
TestOwner owner_{kOwnerVtable.data()};
|
|
int device_ = 0;
|
|
};
|
|
|
|
TEST_F(NativeInputCaptureTest, DeliversPcmAndReopensAfterStop) {
|
|
Publish(true);
|
|
const auto format = NativeInputCapture::Format(&device_);
|
|
EXPECT_EQ(format.present, 1u);
|
|
EXPECT_EQ(format.format.rate, 48000u);
|
|
EXPECT_EQ(format.format.channels, 1u);
|
|
for (int pass = 0; pass < 2; ++pass) {
|
|
owner_ = {kOwnerVtable.data()};
|
|
sink_ = {kSinkVtable.data()};
|
|
NativeSharedSink parameter{&sink_, &owner_};
|
|
ASSERT_TRUE(NativeInputCapture::Start(&device_, ¶meter));
|
|
EXPECT_EQ(parameter.object, nullptr);
|
|
EXPECT_EQ(parameter.owner, nullptr);
|
|
EXPECT_TRUE(NativeInputCapture::Recording(&device_));
|
|
const auto deadline =
|
|
std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
|
while (sink_.frames < 3 && std::chrono::steady_clock::now() < deadline) {
|
|
NativeInputCapture::Poll(&device_);
|
|
SDL_Delay(10);
|
|
}
|
|
EXPECT_GE(sink_.frames, 3);
|
|
EXPECT_TRUE(sink_.nonzero);
|
|
EXPECT_FALSE(sink_.invalid);
|
|
std::uint32_t playback = 0, recording = 0;
|
|
NativeInputCapture::Latency(&device_, &playback, &recording);
|
|
EXPECT_EQ(playback, 42u);
|
|
EXPECT_GE(recording, 10u);
|
|
NativeSharedSink stop{&sink_, &owner_};
|
|
NativeInputCapture::Stop(&device_, &stop);
|
|
EXPECT_FALSE(NativeInputCapture::Recording(&device_));
|
|
EXPECT_EQ(owner_.destroyed, 1);
|
|
EXPECT_EQ(owner_.released, 1);
|
|
const int frames = sink_.frames;
|
|
NativeInputCapture::Poll(&device_);
|
|
NativeInputCapture::Stop(&device_, &stop);
|
|
EXPECT_EQ(sink_.frames, frames);
|
|
EXPECT_EQ(owner_.released, 1);
|
|
}
|
|
}
|
|
|
|
TEST_F(NativeInputCaptureTest, RejectsDisabledAndClosedCapture) {
|
|
Publish(false);
|
|
NativeSharedSink parameter{&sink_, &owner_};
|
|
EXPECT_EQ(NativeInputCapture::Format(&device_).present, 0u);
|
|
EXPECT_FALSE(NativeInputCapture::Start(&device_, ¶meter));
|
|
EXPECT_EQ(parameter.object, &sink_);
|
|
Publish(true);
|
|
ASSERT_TRUE(NativeInputCapture::Start(&device_, ¶meter));
|
|
capture_->Shutdown();
|
|
EXPECT_EQ(NativeInputCapture::Format(&device_).present, 0u);
|
|
EXPECT_FALSE(NativeInputCapture::Recording(&device_));
|
|
parameter = {&sink_, &owner_};
|
|
EXPECT_FALSE(NativeInputCapture::Start(&device_, ¶meter));
|
|
EXPECT_EQ(owner_.released, 1);
|
|
}
|
|
|
|
TEST_F(NativeInputCaptureTest, RejectsUnverifiedSinkWithoutTakingOwnership) {
|
|
Publish(true);
|
|
const std::array<std::uintptr_t, 3> unknown_vtable{};
|
|
TestSink unknown{unknown_vtable.data()};
|
|
NativeSharedSink parameter{&unknown, &owner_};
|
|
EXPECT_FALSE(NativeInputCapture::Start(&device_, ¶meter));
|
|
EXPECT_EQ(parameter.object, &unknown);
|
|
EXPECT_EQ(parameter.owner, &owner_);
|
|
EXPECT_FALSE(NativeInputCapture::Recording(&device_));
|
|
EXPECT_EQ(owner_.released, 0);
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace mocktail::audio
|