@@ -1,0 +1,590 @@
/**
* @file tests/unit/platform/linux/qemu/test_audio_mixer.cpp
* @brief Test the conversion, resampling, remixing and buffering of QEMU guest audio.
*/
#ifdef SUNSHINE_BUILD_QEMU
// test includes
#include "../../../../tests_common.h"
#include "audio_analysis.h"
// standard includes
#include <array>
#include <atomic>
#include <bit>
#include <cstring>
#include <thread>
#include <vector>
// local includes
#include <src/platform/common.h>
#include <src/platform/linux/qemu/audio_mixer.h>
using namespace std::literals;
namespace {
using platf::speaker::speaker_e;
/**
* @brief Build a PCM layout.
*
* @param bits Bits per sample.
* @param is_signed Whether integers are signed.
* @param is_float Whether samples are floats.
* @param freq Sample rate.
* @param channels Channels.
* @param big_endian Whether samples are big-endian.
* @return Layout with a consistent frame size.
*/
qemu::pcm_format_t pcm(std::uint8_t bits, bool is_signed, bool is_float, std::uint32_t freq, std::uint8_t channels, bool big_endian = false) {
qemu::pcm_format_t format;
format.bits = bits;
format.is_signed = is_signed;
format.is_float = is_float;
format.freq = freq;
format.channels = channels;
format.bytes_per_frame = bits / 8 * channels;
format.big_endian = big_endian;
return format;
}
/**
* @brief Signed 16-bit little-endian stereo at 44.1 kHz, QEMU's default playback layout.
*
* @return Layout.
*/
qemu::pcm_format_t s16le_44100_stereo() {
return pcm(16, true, false, 44100, 2);
}
/**
* @brief Encode 32-bit float samples.
*
* @param values Samples.
* @param big_endian Byte order.
* @return Bytes.
*/
std::vector<std::uint8_t> f32_bytes(const std::vector<float> &values, bool big_endian = false) {
std::vector<std::uint8_t> out;
for (auto v : values) {
auto bits = std::bit_cast<std::uint32_t>(v);
if (big_endian) {
bits = std::byteswap(bits);
}
std::array<std::uint8_t, 4> b {};
std::memcpy(b.data(), &bits, 4);
out.insert(out.end(), b.begin(), b.end());
}
return out;
}
/**
* @brief Read frames until `count` samples were collected or a read doesn't return `ok`.
*
* @param mixer Mixer to drain.
* @param channels Output channels.
* @param frame_size Frames per read.
* @param frames Frames to read.
* @return Interleaved samples read.
*/
std::vector<float> read_frames(qemu::audio_mixer_t &mixer, int channels, std::uint32_t frame_size, std::size_t frames) {
std::vector<float> out;
std::vector<float> frame(frame_size * channels);
while (out.size() < frames * channels) {
if (mixer.read(frame, 50ms) != qemu::read_status_e::ok) {
break;
}
out.insert(out.end(), frame.begin(), frame.end());
}
return out;
}
const std::vector<std::uint8_t> stereo(platf::speaker::map_stereo.begin(), platf::speaker::map_stereo.end()); ///< Sunshine's stereo layout.
const std::vector<std::uint8_t> surround51(platf::speaker::map_surround51.begin(), platf::speaker::map_surround51.end()); ///< Sunshine's 5.1 layout.
const std::vector<std::uint8_t> surround71(platf::speaker::map_surround71.begin(), platf::speaker::map_surround71.end()); ///< Sunshine's 7.1 layout.
} // namespace
// @tag requirements: [REQ-AUD-001]
TEST(QemuPcmTest, ConvertsEveryIntegerAndFloatLayout) {
std::vector<float> out;
// s16le: 0x4000 = 0.5, 0x8000 = -1
qemu::pcm_to_float(pcm(16, true, false, 48000, 2), std::vector<std::uint8_t> {0x00, 0x40, 0x00, 0x80}, out);
ASSERT_EQ(out.size(), 2u);
EXPECT_FLOAT_EQ(out[0], 0.5f);
EXPECT_FLOAT_EQ(out[1], -1.0f);
// s16be
out.clear();
qemu::pcm_to_float(pcm(16, true, false, 48000, 1, true), std::vector<std::uint8_t> {0xc0, 0x00}, out);
ASSERT_EQ(out.size(), 1u);
EXPECT_FLOAT_EQ(out[0], -0.5f);
// u16le: 0x8000 is silence
out.clear();
qemu::pcm_to_float(pcm(16, false, false, 48000, 1), std::vector<std::uint8_t> {0x00, 0x80, 0x00, 0xc0}, out);
ASSERT_EQ(out.size(), 2u);
EXPECT_FLOAT_EQ(out[0], 0.0f);
EXPECT_FLOAT_EQ(out[1], 0.5f);
// u8 and s8
out.clear();
qemu::pcm_to_float(pcm(8, false, false, 48000, 2), std::vector<std::uint8_t> {0x80, 0x40}, out);
qemu::pcm_to_float(pcm(8, true, false, 48000, 1), std::vector<std::uint8_t> {0xc0}, out);
ASSERT_EQ(out.size(), 3u);
EXPECT_FLOAT_EQ(out[0], 0.0f);
EXPECT_FLOAT_EQ(out[1], -0.5f);
EXPECT_FLOAT_EQ(out[2], -0.5f);
// s32le and u32be
out.clear();
qemu::pcm_to_float(pcm(32, true, false, 48000, 1), std::vector<std::uint8_t> {0x00, 0x00, 0x00, 0xc0}, out);
qemu::pcm_to_float(pcm(32, false, false, 48000, 1, true), std::vector<std::uint8_t> {0xc0, 0x00, 0x00, 0x00}, out);
ASSERT_EQ(out.size(), 2u);
EXPECT_FLOAT_EQ(out[0], -0.5f);
EXPECT_FLOAT_EQ(out[1], 0.5f);
// f32 in both byte orders
out.clear();
qemu::pcm_to_float(pcm(32, true, true, 48000, 1), f32_bytes({0.25f}), out);
qemu::pcm_to_float(pcm(32, true, true, 48000, 1, true), f32_bytes({-0.75f}, true), out);
ASSERT_EQ(out.size(), 2u);
EXPECT_FLOAT_EQ(out[0], 0.25f);
EXPECT_FLOAT_EQ(out[1], -0.75f);
// a trailing partial frame is ignored
out.clear();
qemu::pcm_to_float(pcm(16, true, false, 48000, 2), std::vector<std::uint8_t> {0, 0, 0, 0, 0, 0x40}, out);
EXPECT_EQ(out.size(), 2u);
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuPcmTest, ValidatesAndDescribesLayouts) {
EXPECT_EQ(qemu::to_string(pcm(16, true, false, 44100, 2)), "16-bit signed, 44100 Hz, 2 channel(s), 4 bytes per frame");
EXPECT_EQ(qemu::to_string(pcm(32, true, true, 48000, 1, true)), "32-bit float big-endian, 48000 Hz, 1 channel(s), 4 bytes per frame");
EXPECT_EQ(qemu::to_string(pcm(8, false, false, 8000, 1)), "8-bit unsigned, 8000 Hz, 1 channel(s), 1 bytes per frame");
EXPECT_TRUE(pcm(16, true, false, 44100, 2).valid());
EXPECT_TRUE(pcm(8, false, false, 8000, 1).valid());
EXPECT_TRUE(pcm(32, true, true, 48000, 8).valid());
EXPECT_FALSE(pcm(24, true, false, 48000, 2).valid());
EXPECT_FALSE(pcm(16, true, true, 48000, 2).valid());
EXPECT_FALSE(pcm(16, true, false, 48000, 0).valid());
EXPECT_FALSE(pcm(16, true, false, 100, 2).valid());
auto bad_frame = pcm(16, true, false, 48000, 2);
bad_frame.bytes_per_frame = 3;
EXPECT_FALSE(bad_frame.valid());
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuResamplerTest, Converts44100To48000KeepingFrequencyLevelAndContinuity) {
// one second of a 1 kHz sine at half scale, in QEMU's 441-frame blocks
std::vector<float> input;
qemu::pcm_to_float(s16le_44100_stereo(), qemu_test::sine_s16le(44100, 44100, {1000, 1000}, 0.5), input);
qemu::resampler_t chunked {2, 44100, 48000};
EXPECT_FALSE(chunked.passthrough());
std::vector<float> out;
for (std::size_t offset = 0; offset < input.size(); offset += 441 * 2) {
chunked.process(std::span<const float> {input}.subspan(offset, std::min<std::size_t>(441 * 2, input.size() - offset)), out);
}
qemu::resampler_t whole {2, 44100, 48000};
std::vector<float> reference;
whole.process(input, reference);
// 48000 frames minus the filter delay
ASSERT_EQ(out.size(), reference.size());
ASSERT_NEAR((double) out.size() / 2, 48000.0, 60.0);
for (std::size_t i = 0; i < out.size(); ++i) {
ASSERT_NEAR(out[i], reference[i], 1e-5f) << "block boundaries must not change the output, sample " << i;
}
const auto left = qemu_test::channel_of(out, 2, 0);
const std::span<const float> steady {left.data() + 1000, left.size() - 2000};
EXPECT_NEAR(qemu_test::dominant_frequency(steady, 48000), 1000.0, 2.0);
EXPECT_NEAR(qemu_test::rms(steady), 0.5 / std::sqrt(2.0), 0.005);
// no clicks: a 1 kHz sine at amplitude 0.5 changes by at most 2*pi*1000/48000*0.5 per sample
float largest_step = 0;
for (std::size_t i = 1001; i < steady.size(); ++i) {
largest_step = std::max(largest_step, std::abs(steady[i] - steady[i - 1]));
}
EXPECT_LT(largest_step, 0.07f);
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuResamplerTest, PassesEqualRatesThroughAndResets) {
qemu::resampler_t resampler {1, 48000, 48000};
EXPECT_TRUE(resampler.passthrough());
std::vector<float> out;
resampler.process(std::vector<float> {0.1f, 0.2f, 0.3f}, out);
EXPECT_EQ(out, (std::vector<float> {0.1f, 0.2f, 0.3f}));
qemu::resampler_t converting {1, 44100, 48000};
std::vector<float> first;
std::vector<float> ones(4410, 1.0f);
converting.process(ones, first);
converting.reset();
std::vector<float> second;
converting.process(ones, second);
EXPECT_EQ(first, second) << "reset starts over like a new converter";
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuResamplerTest, FiltersFrequenciesAboveTheOutputNyquistWhenDownsampling) {
// 30 kHz fits at 96 kHz but not at 44.1 kHz; unfiltered it would alias down to an audible 14.1 kHz
std::vector<float> input(96000);
for (std::size_t i = 0; i < input.size(); ++i) {
input[i] = 0.5f * (float) std::sin(2 * std::numbers::pi * 30000.0 * i / 96000) + 0.5f * (float) std::sin(2 * std::numbers::pi * 1000.0 * i / 96000);
}
qemu::resampler_t resampler {1, 96000, 44100};
std::vector<float> out;
resampler.process(input, out);
ASSERT_GT(out.size(), 40000u);
const std::span<const float> steady {out.data() + 500, out.size() - 1000};
EXPECT_NEAR(qemu_test::dominant_frequency(steady, 44100), 1000.0, 2.0);
EXPECT_NEAR(qemu_test::rms(steady), 0.5 / std::sqrt(2.0), 0.005);
EXPECT_LT(qemu_test::relative_level_db(steady, 44100, 44100 - 30000.0), -60.0);
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuChannelMatrixTest, MapsGuestLayoutsToSunshineSpeakers) {
const float h = (float) std::numbers::sqrt2 / 2;
EXPECT_EQ(qemu::channel_matrix(2, stereo), (std::vector<float> {1, 0, 0, 1}));
EXPECT_EQ(qemu::channel_matrix(1, stereo), (std::vector<float> {h, h}));
// stereo into 5.1: only the front pair plays
EXPECT_EQ(qemu::channel_matrix(2, surround51), (std::vector<float> {1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}));
// 5.1 (FL FR FC LFE BL BR) down to stereo: center and back at -3 dB, LFE dropped
EXPECT_EQ(qemu::channel_matrix(6, stereo), (std::vector<float> {1, 0, h, 0, h, 0, 0, 1, h, 0, 0, h}));
// 7.1 to 5.1: sides go to the back speakers
auto m = qemu::channel_matrix(8, surround51);
ASSERT_EQ(m.size(), 6u * 8);
EXPECT_EQ((std::vector<float>(m.begin() + 4 * 8, m.begin() + 5 * 8)), (std::vector<float> {0, 0, 0, 0, 1, 0, 1, 0}));
EXPECT_EQ((std::vector<float>(m.begin() + 5 * 8, m.begin() + 6 * 8)), (std::vector<float> {0, 0, 0, 0, 0, 1, 0, 1}));
// quad (FL FR BL BR) to 7.1 keeps the back pair on the back speakers
m = qemu::channel_matrix(4, surround71);
EXPECT_EQ(m[4 * 4 + 2], 1.0f);
EXPECT_EQ(m[5 * 4 + 3], 1.0f);
// the output order follows the mapping, not the speaker numbering
const std::vector<std::uint8_t> swapped {speaker_e::FRONT_RIGHT, speaker_e::FRONT_LEFT};
EXPECT_EQ(qemu::channel_matrix(2, swapped), (std::vector<float> {0, 1, 1, 0}));
// channels past 7.1 are ignored and unknown speakers stay silent
m = qemu::channel_matrix(10, stereo);
ASSERT_EQ(m.size(), 20u);
EXPECT_EQ(m[8], 0.0f);
EXPECT_EQ(m[9], 0.0f);
const std::vector<std::uint8_t> unknown {speaker_e::FRONT_LEFT, 42};
EXPECT_EQ(qemu::channel_matrix(2, unknown), (std::vector<float> {1, 0, 0, 0}));
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuAudioMixerTest, ConvertsS16le44100StereoToFloat48000Frames) {
qemu::audio_mixer_t mixer {stereo, 48000, 240, false};
mixer.init(7, s16le_44100_stereo());
mixer.set_enabled(7, true);
// left 1 kHz at half scale, right 3 kHz at quarter scale
std::size_t written = 0;
std::vector<float> out;
while (written < 44100) {
mixer.write(7, qemu_test::sine_s16le(441, 44100, {1000, 3000}, 0.5, written));
written += 441;
auto frames = read_frames(mixer, 2, 240, mixer.buffered_frames() / 240 * 240);
out.insert(out.end(), frames.begin(), frames.end());
}
ASSERT_GT(out.size(), 2u * 47000);
const auto left = qemu_test::channel_of(out, 2, 0);
const auto right = qemu_test::channel_of(out, 2, 1);
const std::span<const float> l {left.data() + 1000, left.size() - 2000};
const std::span<const float> r {right.data() + 1000, right.size() - 2000};
EXPECT_NEAR(qemu_test::dominant_frequency(l, 48000), 1000.0, 2.0);
EXPECT_NEAR(qemu_test::dominant_frequency(r, 48000), 3000.0, 2.0);
EXPECT_NEAR(qemu_test::rms(l), 0.5 / std::sqrt(2.0), 0.005);
EXPECT_NEAR(qemu_test::rms(r), 0.5 / std::sqrt(2.0), 0.005);
EXPECT_EQ(mixer.stats().dropped_frames, 0u);
EXPECT_EQ(mixer.stats().silent_frames, 0u);
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuAudioMixerTest, RemixesStereoIntoSurroundLayouts) {
qemu::audio_mixer_t mixer {surround71, 48000, 480, false};
mixer.init(1, pcm(16, true, false, 48000, 2));
mixer.write(1, qemu_test::sine_s16le(4800, 48000, {1000, 2000}, 0.5));
auto out = read_frames(mixer, 8, 480, 4800);
ASSERT_EQ(out.size(), 8u * 4800);
EXPECT_NEAR(qemu_test::dominant_frequency(qemu_test::channel_of(out, 8, 0), 48000), 1000.0, 5.0);
EXPECT_NEAR(qemu_test::dominant_frequency(qemu_test::channel_of(out, 8, 1), 48000), 2000.0, 5.0);
for (int c = 2; c < 8; ++c) {
EXPECT_EQ(qemu_test::rms(qemu_test::channel_of(out, 8, c)), 0.0) << "channel " << c;
}
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuAudioMixerTest, AppliesVolumeAndMutePerChannel) {
qemu::audio_mixer_t mixer {stereo, 48000, 480, false};
mixer.init(1, pcm(16, true, false, 48000, 2));
// left at 51/255 = 0.2, right at unity
mixer.set_volume(1, false, std::vector<std::uint8_t> {51, 255});
mixer.write(1, qemu_test::sine_s16le(4800, 48000, {1000, 1000}, 0.5));
auto out = read_frames(mixer, 2, 480, 4800);
ASSERT_EQ(out.size(), 2u * 4800);
EXPECT_NEAR(qemu_test::rms(qemu_test::channel_of(out, 2, 0)), 0.2 * 0.5 / std::sqrt(2.0), 0.002);
EXPECT_NEAR(qemu_test::rms(qemu_test::channel_of(out, 2, 1)), 0.5 / std::sqrt(2.0), 0.002);
// a single entry applies to every channel
mixer.set_volume(1, false, std::vector<std::uint8_t> {0});
mixer.write(1, qemu_test::sine_s16le(4800, 48000, {1000, 1000}, 0.5));
EXPECT_EQ(qemu_test::rms(read_frames(mixer, 2, 480, 4800)), 0.0);
// mute silences regardless of the volume; unmuting with an empty list restores unity
mixer.set_volume(1, true, std::vector<std::uint8_t> {255, 255});
mixer.write(1, qemu_test::sine_s16le(4800, 48000, {1000, 1000}, 0.5));
EXPECT_EQ(qemu_test::rms(read_frames(mixer, 2, 480, 4800)), 0.0);
mixer.set_volume(1, false, {});
mixer.write(1, qemu_test::sine_s16le(4800, 48000, {1000, 1000}, 0.5));
EXPECT_NEAR(qemu_test::rms(read_frames(mixer, 2, 480, 4800)), 0.5 / std::sqrt(2.0), 0.002);
// volume for a stream that doesn't exist is ignored
mixer.set_volume(99, true, {});
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuAudioMixerTest, MixesMultipleStreams) {
qemu::audio_mixer_t mixer {stereo, 48000, 480, false};
mixer.init(1, pcm(16, true, false, 48000, 2));
mixer.init(2, pcm(16, true, false, 44100, 1));
for (int block = 0; block < 10; ++block) {
mixer.write(1, qemu_test::sine_s16le(480, 48000, {1000, 1000}, 0.25, block * 480));
mixer.write(2, qemu_test::sine_s16le(441, 44100, {3000}, 0.25, block * 441));
}
auto out = read_frames(mixer, 2, 480, 4320);
ASSERT_EQ(out.size(), 2u * 4320);
const auto left = qemu_test::channel_of(out, 2, 0);
const std::span<const float> steady {left.data() + 100, left.size() - 200};
EXPECT_GT(qemu_test::relative_level_db(steady, 48000, 1000), -1.0);
// the mono stream reaches each front speaker at -3 dB
EXPECT_NEAR(qemu_test::relative_level_db(steady, 48000, 3000), -3.0, 1.5);
EXPECT_NEAR(qemu_test::rms(steady), std::sqrt(0.25 * 0.25 / 2 + 0.125 * 0.125), 0.01);
// after Fini, writes for the stream are ignored
mixer.fini(2);
const auto before = mixer.buffered_frames();
mixer.write(2, qemu_test::sine_s16le(441, 44100, {3000}, 0.25));
EXPECT_EQ(mixer.buffered_frames(), before);
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuAudioMixerTest, ReadsOnlyAudioThatEveryPlayingStreamWrote) {
const std::vector<std::uint8_t> mono {speaker_e::FRONT_CENTER};
qemu::audio_mixer_t mixer {mono, 48000, 240, false};
mixer.init(1, pcm(32, true, true, 48000, 1));
mixer.init(2, pcm(32, true, true, 48000, 1));
mixer.write(1, f32_bytes(std::vector<float>(480, 0.25f)));
mixer.write(2, f32_bytes(std::vector<float>(480, 0.5f)));
std::vector<float> frame(240);
for (int block = 0; block < 5; ++block) {
ASSERT_EQ(mixer.read(frame, 5ms), qemu::read_status_e::ok);
ASSERT_EQ(frame.front(), 0.75f);
ASSERT_EQ(mixer.read(frame, 5ms), qemu::read_status_e::ok);
ASSERT_EQ(frame.back(), 0.75f);
// stream 1's next block alone isn't readable while stream 2 still plays
mixer.write(1, f32_bytes(std::vector<float>(480, 0.25f)));
EXPECT_EQ(mixer.buffered_frames(), 480u);
EXPECT_EQ(mixer.read(frame, 5ms), qemu::read_status_e::timeout);
mixer.write(2, f32_bytes(std::vector<float>(480, 0.5f)));
}
// a suspended stream no longer holds the others back
mixer.set_enabled(2, false);
mixer.read(frame, 5ms);
mixer.read(frame, 5ms);
mixer.write(1, f32_bytes(std::vector<float>(240, 0.25f)));
ASSERT_EQ(mixer.read(frame, 5ms), qemu::read_status_e::ok);
EXPECT_EQ(frame.front(), 0.25f);
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuAudioMixerTest, DropsTheOldestAudioWhenTheReaderFallsBehind) {
qemu::audio_mixer_t mixer {std::vector<std::uint8_t> {speaker_e::FRONT_CENTER}, 48000, 240, false};
mixer.init(1, pcm(32, true, true, 48000, 1));
const auto capacity = mixer.capacity_frames();
ASSERT_GE(capacity, 48000u / 10) << "room for jitter";
ASSERT_LE(capacity, 48000u / 2) << "latency stays bounded";
// one second of blocks of 480 frames, each block a constant value that identifies it
const std::size_t blocks = 100;
for (std::size_t block = 0; block < blocks; ++block) {
mixer.write(1, f32_bytes(std::vector<float>(480, (float) block / 1000)));
}
EXPECT_EQ(mixer.buffered_frames(), capacity);
EXPECT_EQ(mixer.stats().dropped_frames, blocks * 480 - capacity);
EXPECT_GT(mixer.stats().overflows, 0u);
// the first frame read is the oldest audio still kept, and the last one is the newest
std::vector<float> frame(240);
ASSERT_EQ(mixer.read(frame, 10ms), qemu::read_status_e::ok);
const auto first_kept = blocks * 480 - capacity;
EXPECT_FLOAT_EQ(frame[0], (float) (first_kept / 480) / 1000);
auto rest = read_frames(mixer, 1, 240, capacity - 240);
ASSERT_FALSE(rest.empty());
EXPECT_FLOAT_EQ(rest.back(), (float) (blocks - 1) / 1000);
// a single write larger than the buffer keeps its newest part
mixer.write(1, f32_bytes(std::vector<float>(capacity + 480, 0.5f)));
EXPECT_EQ(mixer.buffered_frames(), capacity);
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuAudioMixerTest, WaitsOnUnderrunWithoutContinuousAudio) {
qemu::audio_mixer_t mixer {stereo, 48000, 480, false};
std::vector<float> frame(960, 1.0f);
// nothing plays: a read times out without touching the frame
auto start = std::chrono::steady_clock::now();
EXPECT_EQ(mixer.read(frame, 30ms), qemu::read_status_e::timeout);
EXPECT_GE(std::chrono::steady_clock::now() - start, 25ms);
EXPECT_EQ(frame[0], 1.0f);
// a partial frame waits for the rest of the audio while the stream keeps writing
mixer.init(1, pcm(16, true, false, 48000, 2));
mixer.write(1, qemu_test::sine_s16le(300, 48000, {1000, 1000}, 0.5));
std::thread writer {[&]() {
std::this_thread::sleep_for(10ms);
mixer.write(1, qemu_test::sine_s16le(300, 48000, {1000, 1000}, 0.5, 300));
}};
const auto first = mixer.read(frame, 1s);
writer.join();
ASSERT_EQ(first, qemu::read_status_e::ok);
EXPECT_EQ(mixer.stats().silent_frames, 0u);
EXPECT_EQ(mixer.buffered_frames(), 120u);
// when the stream stops, the tail is completed with silence once audio counts as stopped
start = std::chrono::steady_clock::now();
ASSERT_EQ(mixer.read(frame, 1s), qemu::read_status_e::ok);
EXPECT_GE(std::chrono::steady_clock::now() - start, mixer.underrun_threshold() - 15ms);
EXPECT_NE(frame[2 * 119], 0.0f);
EXPECT_EQ(frame[2 * 120], 0.0f);
EXPECT_EQ(frame[959], 0.0f);
EXPECT_EQ(mixer.stats().silent_frames, 360u);
// then reads time out again
EXPECT_EQ(mixer.read(frame, 20ms), qemu::read_status_e::timeout);
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuAudioMixerTest, ProducesPacedSilenceOnUnderrunWithContinuousAudio) {
qemu::audio_mixer_t mixer {stereo, 48000, 240, true};
std::vector<float> frame(480, 1.0f);
// nothing ever played: silence right away, at the real-time frame rate (5 ms per frame)
const auto start = std::chrono::steady_clock::now();
for (int i = 0; i < 40; ++i) {
ASSERT_EQ(mixer.read(frame, 1ms), qemu::read_status_e::ok);
ASSERT_EQ(frame[0], 0.0f);
}
const auto elapsed = std::chrono::steady_clock::now() - start;
EXPECT_GE(elapsed, 180ms) << "silence must not be produced faster than real time";
EXPECT_LT(elapsed, 600ms);
EXPECT_EQ(mixer.stats().silent_frames, 40u * 240);
// audio that arrives is returned as soon as a frame is complete
mixer.init(1, pcm(16, true, false, 48000, 2));
mixer.write(1, qemu_test::sine_s16le(480, 48000, {1000, 1000}, 0.5));
ASSERT_EQ(mixer.read(frame, 1ms), qemu::read_status_e::ok);
EXPECT_NE(qemu_test::rms(frame), 0.0);
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuAudioMixerTest, KeepsWaitingThroughNormalWriteJitterWithContinuousAudio) {
qemu::audio_mixer_t mixer {stereo, 48000, 240, true};
mixer.init(1, pcm(16, true, false, 44100, 2));
// prime the stream so it is playing
mixer.write(1, qemu_test::sine_s16le(441, 44100, {1000, 1000}, 0.5));
std::vector<float> frame(480);
ASSERT_EQ(mixer.read(frame, 1ms), qemu::read_status_e::ok);
const auto silent_before = mixer.stats().silent_frames;
// QEMU's 10 ms blocks, one of them 30 ms late
std::atomic<bool> done {false};
std::thread writer {[&]() {
std::size_t frame_index = 441;
for (int block = 0; block < 30; ++block) {
std::this_thread::sleep_for(block == 15 ? 30ms : 10ms);
if (block == 29) {
// the reader stops once the last block is drained, so it never waits past the end
done = true;
}
mixer.write(1, qemu_test::sine_s16le(441, 44100, {1000, 1000}, 0.5, frame_index));
frame_index += 441;
}
}};
bool reads_ok = true;
while (reads_ok && (!done || mixer.buffered_frames() >= 240)) {
reads_ok = mixer.read(frame, 1ms) == qemu::read_status_e::ok;
}
writer.join();
EXPECT_TRUE(reads_ok);
EXPECT_EQ(mixer.stats().silent_frames, silent_before) << "jitter below the underrun threshold must not insert silence";
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuAudioMixerTest, RestartsStreamsAndIgnoresUnusableOnes) {
qemu::audio_mixer_t mixer {stereo, 48000, 480, false};
// writes without Init, and writes of a format Sunshine can't convert, are ignored
mixer.write(5, qemu_test::sine_s16le(480, 48000, {1000, 1000}, 0.5));
mixer.init(6, pcm(24, true, false, 48000, 2));
mixer.write(6, std::vector<std::uint8_t>(6 * 480, 0x40));
EXPECT_EQ(mixer.buffered_frames(), 0u);
// a frame split across writes is joined
mixer.init(1, pcm(16, true, false, 48000, 2));
const auto bytes = qemu_test::sine_s16le(480, 48000, {1000, 1000}, 0.5);
mixer.write(1, std::span<const std::uint8_t> {bytes}.subspan(0, 3));
mixer.write(1, std::span<const std::uint8_t> {bytes}.subspan(3));
EXPECT_EQ(mixer.buffered_frames(), 480u);
// disabling and enabling keep the stream usable; a re-Init with another rate replaces the
// converter, and the restarted stream continues after its own buffered audio
mixer.set_enabled(1, false);
mixer.set_enabled(1, true);
mixer.init(1, pcm(16, true, false, 24000, 2));
mixer.write(1, qemu_test::sine_s16le(2400, 24000, {1000, 1000}, 0.5));
EXPECT_NEAR((double) mixer.buffered_frames(), 480 + 4800, 60);
mixer.set_enabled(77, true);
mixer.fini(77);
}
// @tag requirements: [REQ-AUD-001]
TEST(QemuAudioMixerTest, CloseWakesBlockedReaders) {
qemu::audio_mixer_t mixer {stereo, 48000, 480, false};
std::vector<float> frame(960);
std::thread closer {[&]() {
std::this_thread::sleep_for(20ms);
mixer.close();
}};
const auto start = std::chrono::steady_clock::now();
const auto status = mixer.read(frame, 5s);
closer.join();
EXPECT_EQ(status, qemu::read_status_e::closed);
EXPECT_LT(std::chrono::steady_clock::now() - start, 2s);
// closed stays closed, also with continuous audio and buffered data
qemu::audio_mixer_t continuous {stereo, 48000, 480, true};
continuous.init(1, pcm(16, true, false, 48000, 2));
continuous.write(1, qemu_test::sine_s16le(4800, 48000, {1000, 1000}, 0.5));
continuous.close();
EXPECT_EQ(continuous.read(frame, 5s), qemu::read_status_e::closed);
}
#endif