From de614ccc9e7300da7540bbf14a13c23558c6722d Mon Sep 17 00:00:00 2001 From: Abror Date: Thu, 10 Sep 2026 11:14:56 +0200 Subject: [PATCH] cuttlefish: Add Audio Generator and PCM Stream Server for Virtual Tuner Introduce the audio emulation pipeline for the Cuttlefish Virtual Tuner: - AudioGenerator: Generates 48 kHz, 16-bit stereo PCM audio frames (white noise while tuned/playing, silence while stopped/untuned). - PcmStreamServer: Multi-client PCM stream server delivering 48 kHz 16-bit stereo audio frames over a UNIX domain stream socket to Cuttlefish's virtio-snd virtual sound card backend (CrosVM). - Unit and integration tests covering audio frame synthesis and socket streaming. Test: bazel test //cuttlefish/host/commands/virtual_tuner_daemon:... Bug: 521329213 TAG=agy CONV=809829e8-ac68-4c32-b9b7-632f61743e47 --- .../cuttlefish/common/libs/fs/shared_buf.cc | 13 +- .../cuttlefish/common/libs/fs/shared_buf.h | 13 +- .../commands/virtual_tuner_daemon/BUILD.bazel | 33 +++ .../virtual_tuner_daemon/audio_generator.cpp | 55 ++++ .../virtual_tuner_daemon/audio_generator.h | 49 ++++ .../pcm_stream_server.cpp | 196 ++++++++++++++ .../virtual_tuner_daemon/pcm_stream_server.h | 71 +++++ .../virtual_tuner_audio_test.cpp | 253 ++++++++++++++++++ 8 files changed, 678 insertions(+), 5 deletions(-) create mode 100644 base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/audio_generator.cpp create mode 100644 base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/audio_generator.h create mode 100644 base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/pcm_stream_server.cpp create mode 100644 base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/pcm_stream_server.h create mode 100644 base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/virtual_tuner_audio_test.cpp diff --git a/base/cvd/cuttlefish/common/libs/fs/shared_buf.cc b/base/cvd/cuttlefish/common/libs/fs/shared_buf.cc index b8762e66262..0809051f539 100644 --- a/base/cvd/cuttlefish/common/libs/fs/shared_buf.cc +++ b/base/cvd/cuttlefish/common/libs/fs/shared_buf.cc @@ -74,14 +74,15 @@ ssize_t WriteAll(SharedFD fd, const std::vector& buf) { return WriteAll(fd, buf.data(), buf.size()); } -bool SendAll(SharedFD sock, std::string_view msg) { +bool SendAll(SharedFD sock, const void* buf, size_t size, int flags) { ssize_t total_written{}; if (!sock->IsOpen()) { return false; } - while (total_written < static_cast(msg.size())) { - auto just_written = sock->Send(msg.data() + total_written, - msg.size() - total_written, MSG_NOSIGNAL); + const char* cursor = static_cast(buf); + while (total_written < static_cast(size)) { + ssize_t just_written = + sock->Send(cursor + total_written, size - total_written, flags); if (just_written <= 0) { return false; } @@ -90,4 +91,8 @@ bool SendAll(SharedFD sock, std::string_view msg) { return true; } +bool SendAll(SharedFD sock, std::string_view msg, int flags) { + return SendAll(sock, msg.data(), msg.size(), flags); +} + } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/common/libs/fs/shared_buf.h b/base/cvd/cuttlefish/common/libs/fs/shared_buf.h index 3b764cfa8b2..cd6cc99b656 100644 --- a/base/cvd/cuttlefish/common/libs/fs/shared_buf.h +++ b/base/cvd/cuttlefish/common/libs/fs/shared_buf.h @@ -148,6 +148,17 @@ ssize_t WriteAllBinary(SharedFD fd, const T* binary_data) { return WriteAll(fd, (const char*)binary_data, sizeof(*binary_data)); } +/** + * Sends contents of buf through sock, checking for socket error conditions + * + * On successful Send, returns true + * + * If a Send error is encountered, returns false. Some data may have already + * been written to 'sock' at that point. + */ +bool SendAll(SharedFD sock, const void* buf, size_t size, + int flags = MSG_NOSIGNAL); + /** * Sends contents of msg through sock, checking for socket error conditions * @@ -156,6 +167,6 @@ ssize_t WriteAllBinary(SharedFD fd, const T* binary_data) { * If a Send error is encountered, returns false. Some data may have already * been written to 'sock' at that point. */ -bool SendAll(SharedFD sock, std::string_view msg); +bool SendAll(SharedFD sock, std::string_view msg, int flags = MSG_NOSIGNAL); } // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/BUILD.bazel b/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/BUILD.bazel index c50f1edd93c..3e33a3b6d17 100644 --- a/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/BUILD.bazel @@ -50,3 +50,36 @@ cf_cc_test( ":virtual_tuner_cc_proto", ], ) + +cf_cc_library( + name = "virtual_tuner_audio", + srcs = [ + "audio_generator.cpp", + "pcm_stream_server.cpp", + ], + hdrs = [ + "audio_generator.h", + "pcm_stream_server.h", + ], + deps = [ + ":tuner_state", + "//cuttlefish/common/libs/fs", + "//cuttlefish/common/libs/fs:fd", + "//cuttlefish/result", + "@abseil-cpp//absl/log", + ], +) + +cf_cc_test( + name = "virtual_tuner_audio_test", + srcs = [ + "virtual_tuner_audio_test.cpp", + ], + deps = [ + ":tuner_state", + ":virtual_tuner_audio", + ":virtual_tuner_cc_proto", + "//cuttlefish/common/libs/fs", + "//cuttlefish/result", + ], +) diff --git a/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/audio_generator.cpp b/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/audio_generator.cpp new file mode 100644 index 00000000000..d2fd312f60e --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/audio_generator.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cuttlefish/host/commands/virtual_tuner_daemon/audio_generator.h" + +#include +#include +#include +#include +#include + +#include "cuttlefish/host/commands/virtual_tuner_daemon/tuner_state.h" + +namespace cuttlefish { +namespace virtualtuner { +namespace { + +constexpr int16_t kNoiseAmplitude = 4000; + +} // namespace + +AudioGenerator::AudioGenerator() : rng_(std::random_device()()) {} + +void AudioGenerator::GenerateChunk(std::span samples, + const TunerStateSnapshot& snapshot) { + if (!snapshot.is_playing) { + std::fill(samples.begin(), samples.end(), 0); + return; + } + + std::uniform_int_distribution noise(-kNoiseAmplitude, kNoiseAmplitude); + for (size_t frame = 0; frame + kChannels <= samples.size(); + frame += kChannels) { + const auto sample = static_cast(noise(rng_)); + for (size_t channel = 0; channel < kChannels; ++channel) { + samples[frame + channel] = sample; + } + } +} + +} // namespace virtualtuner +} // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/audio_generator.h b/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/audio_generator.h new file mode 100644 index 00000000000..d7b25d303eb --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/audio_generator.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include "cuttlefish/host/commands/virtual_tuner_daemon/tuner_state.h" + +namespace cuttlefish { +namespace virtualtuner { + +inline constexpr size_t kSampleRate = 48000; +inline constexpr size_t kChannels = 2; +inline constexpr size_t kBytesPerSample = sizeof(int16_t); +inline constexpr size_t kFrameSizeBytes = kChannels * kBytesPerSample; + +inline constexpr size_t kChunkFrames = 4096; +inline constexpr size_t kChunkSizeBytes = kChunkFrames * kFrameSizeBytes; + +class AudioGenerator { + public: + AudioGenerator(); + + void GenerateChunk(std::span samples, + const TunerStateSnapshot& snapshot); + + private: + std::minstd_rand rng_; +}; + +} // namespace virtualtuner +} // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/pcm_stream_server.cpp b/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/pcm_stream_server.cpp new file mode 100644 index 00000000000..6dfff70893b --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/pcm_stream_server.cpp @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "cuttlefish/host/commands/virtual_tuner_daemon/pcm_stream_server.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/log/log.h" + +#include "cuttlefish/common/libs/fs/fd.h" +#include "cuttlefish/common/libs/fs/shared_buf.h" +#include "cuttlefish/common/libs/fs/shared_fd.h" +#include "cuttlefish/host/commands/virtual_tuner_daemon/audio_generator.h" +#include "cuttlefish/host/commands/virtual_tuner_daemon/tuner_state.h" +#include "cuttlefish/result/result.h" + +namespace cuttlefish { +namespace virtualtuner { +namespace { + +using ChunkDuration = + std::chrono::duration>; + +constexpr auto kMaxSchedulingLag = std::chrono::milliseconds(500); +constexpr int kPrebufferChunks = 2; + +} // namespace + +PcmStreamServer::PcmStreamServer(TunerState& tuner_state, SharedFD server_fd) + : tuner_state_(tuner_state), server_fd_(std::move(server_fd)) {} + +PcmStreamServer::~PcmStreamServer() { Stop(); } + +Result PcmStreamServer::Start() { + if (is_running_) { + return {}; + } + + CF_EXPECT(server_fd_->IsOpen(), + "Failed to start PCM streaming server: server_fd is not open"); + + LOG(INFO) << "PCM streaming server running with inherited server fd."; + is_running_ = true; + accept_thread_ = std::thread(&PcmStreamServer::AcceptLoop, this); + return {}; +} + +void PcmStreamServer::Stop() { + if (!is_running_.exchange(false)) { + return; + } + + if (server_fd_->IsOpen()) { + server_fd_->Shutdown(SHUT_RDWR); + server_fd_->Close(); + } + stop_cv_.notify_all(); + if (accept_thread_.joinable()) { + accept_thread_.join(); + } + + std::vector> sessions; + { + std::lock_guard lock(clients_mutex_); + sessions.swap(clients_); + } + for (const auto& session : sessions) { + if (session->fd->IsOpen()) { + session->fd->Shutdown(SHUT_RDWR); + session->fd->Close(); + } + } + for (auto& session : sessions) { + if (session->thread.joinable()) { + session->thread.join(); + } + } + + LOG(INFO) << "PCM streaming server stopped."; +} + +void PcmStreamServer::AcceptLoop() { + while (is_running_) { + SharedFD client_fd = Fd::Accept(*server_fd_).value_or(Fd()); + if (!is_running_) { + break; + } + if (!client_fd->IsOpen()) { + LOG(ERROR) << "Accept failed on PCM server socket: " + << client_fd->StrError(); + continue; + } + + std::lock_guard lock(clients_mutex_); + ReapFinishedClients(); + if (clients_.size() >= kMaxConcurrentClients) { + LOG(WARNING) << "Refusing PCM client: already serving " << clients_.size() + << " clients."; + continue; + } + + LOG(INFO) << "PCM streaming server accepted a client."; + auto session = std::make_unique(); + session->fd = client_fd; + ClientSession* session_ptr = session.get(); + clients_.push_back(std::move(session)); + session_ptr->thread = std::thread(&PcmStreamServer::StreamClient, this, + std::ref(*session_ptr)); + } +} + +void PcmStreamServer::ReapFinishedClients() { + std::erase_if(clients_, [](const std::unique_ptr& session) { + if (!session->finished.load(std::memory_order_acquire)) { + return false; + } + if (session->thread.joinable()) { + session->thread.join(); + } + return true; + }); +} + +void PcmStreamServer::StreamClient(ClientSession& session) { + const SharedFD& client_fd = session.fd; + AudioGenerator generator; + std::vector buffer(kChunkFrames * kChannels); + + const auto write_next_chunk = [&]() -> bool { + generator.GenerateChunk(buffer, tuner_state_.GetSnapshot()); + return SendAll(client_fd, buffer.data(), buffer.size() * sizeof(int16_t)); + }; + + bool connected = true; + for (int i = 0; i < kPrebufferChunks && connected && is_running_; ++i) { + connected = write_next_chunk(); + } + + auto anchor = std::chrono::steady_clock::now(); + int64_t chunks_since_anchor = 0; + + while (connected && is_running_) { + ++chunks_since_anchor; + const auto deadline = anchor + ChunkDuration(chunks_since_anchor); + { + std::unique_lock lock(stop_mutex_); + if (stop_cv_.wait_until(lock, deadline, + [&]() -> bool { return !is_running_; })) { + break; + } + } + + const auto now = std::chrono::steady_clock::now(); + if (now > deadline + kMaxSchedulingLag) { + LOG(WARNING) << "PCM stream fell behind schedule by " + << std::chrono::duration_cast( + now - deadline) + .count() + << " ms; re-anchoring."; + anchor = now; + chunks_since_anchor = 0; + } + + connected = write_next_chunk(); + } + + LOG(INFO) << "PCM client disconnected."; + session.finished.store(true, std::memory_order_release); +} + +} // namespace virtualtuner +} // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/pcm_stream_server.h b/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/pcm_stream_server.h new file mode 100644 index 00000000000..f519671a6f6 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/pcm_stream_server.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "cuttlefish/common/libs/fs/shared_fd.h" +#include "cuttlefish/host/commands/virtual_tuner_daemon/tuner_state.h" +#include "cuttlefish/result/result.h" + +namespace cuttlefish { +namespace virtualtuner { + +inline constexpr size_t kMaxConcurrentClients = 8; + +class PcmStreamServer { + public: + PcmStreamServer(TunerState& tuner_state, SharedFD server_fd); + ~PcmStreamServer(); + + PcmStreamServer(const PcmStreamServer&) = delete; + PcmStreamServer& operator=(const PcmStreamServer&) = delete; + + Result Start(); + void Stop(); + + bool IsRunning() const { return is_running_; } + + private: + struct ClientSession { + SharedFD fd; + std::thread thread; + std::atomic finished = false; + }; + + void AcceptLoop(); + void StreamClient(ClientSession& session); + void ReapFinishedClients(); + + TunerState& tuner_state_; + std::atomic is_running_ = false; + SharedFD server_fd_; + std::thread accept_thread_; + std::mutex clients_mutex_; + std::vector> clients_; + std::condition_variable stop_cv_; + std::mutex stop_mutex_; +}; + +} // namespace virtualtuner +} // namespace cuttlefish diff --git a/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/virtual_tuner_audio_test.cpp b/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/virtual_tuner_audio_test.cpp new file mode 100644 index 00000000000..d9ab07bf0bc --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/virtual_tuner_daemon/virtual_tuner_audio_test.cpp @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "cuttlefish/common/libs/fs/shared_buf.h" +#include "cuttlefish/common/libs/fs/shared_fd.h" +#include "cuttlefish/host/commands/virtual_tuner_daemon/VirtualTuner.pb.h" +#include "cuttlefish/host/commands/virtual_tuner_daemon/audio_generator.h" +#include "cuttlefish/host/commands/virtual_tuner_daemon/pcm_stream_server.h" +#include "cuttlefish/host/commands/virtual_tuner_daemon/tuner_state.h" +#include "cuttlefish/result/result.h" + +namespace cuttlefish { +namespace virtualtuner { +namespace { + +bool ReadExactly(const SharedFD& fd, std::span buffer) { + return cuttlefish::ReadExact(fd, reinterpret_cast(buffer.data()), + buffer.size()) == + static_cast(buffer.size()); +} + +constexpr int16_t kNoiseAmplitude = 4000; + +TunerStateSnapshot TunedSnapshot() { + return TunerStateSnapshot{RadioBand::FM, 88500000, 0, /* is_playing= */ true}; +} + +bool AnyNonZero(std::span buffer) { + return std::any_of(buffer.begin(), buffer.end(), + [](uint8_t byte) { return byte != 0; }); +} + +bool AllSamplesZero(std::span samples) { + return std::all_of(samples.begin(), samples.end(), + [](int16_t sample) { return sample == 0; }); +} + +bool AllSamplesWithinAmplitude(std::span samples, + int16_t amplitude) { + return std::all_of(samples.begin(), samples.end(), [amplitude](int16_t s) { + return s >= -amplitude && s <= amplitude; + }); +} + +bool EveryFrameIsMono(std::span samples) { + for (size_t frame = 0; frame + kChannels <= samples.size(); + frame += kChannels) { + for (size_t channel = 1; channel < kChannels; ++channel) { + if (samples[frame + channel] != samples[frame]) { + return false; + } + } + } + return true; +} + +TEST(AudioGeneratorTest, AudioConstantsAreStandard) { + EXPECT_EQ(kSampleRate, 48000u); + EXPECT_EQ(kChannels, 2u); + EXPECT_EQ(kBytesPerSample, 2u); + EXPECT_EQ(kFrameSizeBytes, 4u); + EXPECT_EQ(kChunkFrames, 4096u); + EXPECT_EQ(kChunkSizeBytes, 16384u); +} + +TEST(AudioGeneratorTest, GenerateChunkSilenceWhenUntuned) { + AudioGenerator generator; + std::vector buffer(kChunkFrames * kChannels, 0x55); + + const TunerStateSnapshot untuned{RadioBand::FM, 0, 0, + /* is_playing= */ false}; + generator.GenerateChunk(buffer, untuned); + + EXPECT_TRUE(AllSamplesZero(buffer)); +} + +TEST(AudioGeneratorTest, GenerateChunkNoiseWhenPlaying) { + AudioGenerator generator; + std::vector buffer(kChunkFrames * kChannels, 0); + + generator.GenerateChunk(buffer, TunedSnapshot()); + + EXPECT_FALSE(AllSamplesZero(buffer)); + EXPECT_TRUE(AllSamplesWithinAmplitude(buffer, kNoiseAmplitude)); + EXPECT_TRUE(EveryFrameIsMono(buffer)); +} + +TEST(AudioGeneratorTest, GeneratorsAreIndependent) { + AudioGenerator first; + AudioGenerator second; + std::vector first_buffer(kChunkFrames * kChannels, 0); + std::vector second_buffer(kChunkFrames * kChannels, 0); + + first.GenerateChunk(first_buffer, TunedSnapshot()); + second.GenerateChunk(second_buffer, TunedSnapshot()); + + EXPECT_NE(first_buffer, second_buffer); +} + +class PcmStreamServerTest : public ::testing::Test { + protected: + void SetUp() override { + // Avoid testing::TempDir() which exceeds the 108-byte sockaddr_un limit + // under Bazel. + std::string directory_template = "/tmp/cf_virtual_tuner_XXXXXX"; + ASSERT_NE(::mkdtemp(directory_template.data()), nullptr); + temp_dir_ = directory_template; + socket_path_ = temp_dir_ + "/pcm.sock"; + } + + void TearDown() override { + ::unlink(socket_path_.c_str()); + ::rmdir(temp_dir_.c_str()); + } + + SharedFD CreateServerSocket() { + SharedFD sock = SharedFD::SocketLocalServer( + socket_path_, /* is_abstract= */ false, SOCK_STREAM, 0600); + EXPECT_TRUE(sock->IsOpen()); + return sock; + } + + std::string temp_dir_; + std::string socket_path_; +}; + +TEST_F(PcmStreamServerTest, StreamsAudioToConnectedClient) { + TunerState state; + state.SetTune(RadioBand::FM, 88500000, 0); + + PcmStreamServer server(state, CreateServerSocket()); + ASSERT_TRUE(server.Start().has_value()); + EXPECT_TRUE(server.IsRunning()); + + SharedFD client = + SharedFD::SocketLocalClient(socket_path_, false, SOCK_STREAM); + ASSERT_TRUE(client->IsOpen()) << "connect failed: " << client->StrError(); + + std::vector chunk(kChunkSizeBytes); + for (int i = 0; i < 2; ++i) { + ASSERT_TRUE(ReadExactly(client, chunk)) << "short read on chunk " << i; + EXPECT_TRUE(AnyNonZero(chunk)) << "expected audio, got silence"; + } + + client->Close(); + server.Stop(); + EXPECT_FALSE(server.IsRunning()); +} + +TEST_F(PcmStreamServerTest, ServesMultipleClientsConcurrently) { + TunerState state; + state.SetTune(RadioBand::FM, 88500000, 0); + + PcmStreamServer server(state, CreateServerSocket()); + ASSERT_TRUE(server.Start().has_value()); + + SharedFD first = + SharedFD::SocketLocalClient(socket_path_, false, SOCK_STREAM); + SharedFD second = + SharedFD::SocketLocalClient(socket_path_, false, SOCK_STREAM); + ASSERT_TRUE(first->IsOpen()) << first->StrError(); + ASSERT_TRUE(second->IsOpen()) << second->StrError(); + + std::vector chunk(kChunkSizeBytes); + EXPECT_TRUE(ReadExactly(first, chunk)); + EXPECT_TRUE(AnyNonZero(chunk)); + EXPECT_TRUE(ReadExactly(second, chunk)); + EXPECT_TRUE(AnyNonZero(chunk)); + + first->Close(); + second->Close(); +} + +TEST_F(PcmStreamServerTest, UntunedClientReceivesSilence) { + TunerState state; + + PcmStreamServer server(state, CreateServerSocket()); + ASSERT_TRUE(server.Start().has_value()); + + SharedFD client = + SharedFD::SocketLocalClient(socket_path_, false, SOCK_STREAM); + ASSERT_TRUE(client->IsOpen()) << client->StrError(); + + std::vector chunk(kChunkSizeBytes); + ASSERT_TRUE(ReadExactly(client, chunk)); + EXPECT_FALSE(AnyNonZero(chunk)); + + client->Close(); +} + +TEST_F(PcmStreamServerTest, DestructorDisconnectsActiveClient) { + TunerState state; + state.SetTune(RadioBand::FM, 88500000, 0); + + SharedFD client; + { + PcmStreamServer server(state, CreateServerSocket()); + ASSERT_TRUE(server.Start().has_value()); + + client = SharedFD::SocketLocalClient(socket_path_, false, SOCK_STREAM); + ASSERT_TRUE(client->IsOpen()) << client->StrError(); + + std::vector chunk(kChunkSizeBytes); + ASSERT_TRUE(ReadExactly(client, chunk)); + } + + std::vector drain(kChunkSizeBytes); + Result bytes_read = client->Read(drain.data(), drain.size()); + while (bytes_read.has_value() && *bytes_read > 0) { + bytes_read = client->Read(drain.data(), drain.size()); + } + ASSERT_TRUE(bytes_read.has_value()) << bytes_read.error().FormatForEnv(); + EXPECT_EQ(*bytes_read, 0u) << "expected EOF after server teardown"; +} + +TEST_F(PcmStreamServerTest, StopIsIdempotent) { + TunerState state; + PcmStreamServer server(state, CreateServerSocket()); + ASSERT_TRUE(server.Start().has_value()); + + server.Stop(); + server.Stop(); + EXPECT_FALSE(server.IsRunning()); +} + +} // namespace +} // namespace virtualtuner +} // namespace cuttlefish