Audio render-hook M1: shared audio ring + protocol diag fields
First milestone of the injection render-hook audio path (see docs/audio-render-hook-plan.md) that fixes the local audio echo without a virtual device. - common/include/coop/audio_ring.hpp: new lock-free SPSC shared-memory ring for PCM, separate from the input/status SharedBlock. Free-running 64-bit positions (release/acquire), format handshake, host-owned capture_enabled gate, drop-whole-packet overrun policy. - common/include/coop/protocol.hpp: add AudioStreamInfo + audio_streams_seen / audio_streams[] to the always-present HookStatus for the render-stream-count debug view; bump kProtocolVersion 3->4 (new members appended). - tests/audio_ring_test.cpp: in-process unit test (push/pop integrity, wrap-around, format handshake, overrun/drop). No hook or audio device. - docs/audio-render-hook-plan.md: the green-lit design this implements. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
131
tests/audio_ring_test.cpp
Normal file
131
tests/audio_ring_test.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
// In-process unit test for the shared audio ring (coop/audio_ring.hpp). No
|
||||
// shared memory, no hook, no audio device: it exercises the lock-free SPSC
|
||||
// push/pop, wrap-around, the format handshake, and the overrun/drop policy in a
|
||||
// single process. Exits 0 on pass, 1 on failure.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "coop/audio_ring.hpp"
|
||||
|
||||
using namespace coop;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
void check(bool ok, const char* what)
|
||||
{
|
||||
if (!ok)
|
||||
{
|
||||
std::printf(" FAIL: %s\n", what);
|
||||
++g_failures;
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate a ring of `capacity` data bytes in a plain heap buffer (placement-new
|
||||
// the header so its atomics are constructed) and return it ready to use.
|
||||
AudioRingHeader* make_ring(std::vector<std::uint8_t>& storage, std::uint32_t capacity)
|
||||
{
|
||||
storage.assign(audio_ring_total_size(capacity), 0);
|
||||
auto* h = new (storage.data()) AudioRingHeader();
|
||||
audio_ring_init(*h, capacity);
|
||||
return h;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
// --- Validity + format handshake. ---
|
||||
{
|
||||
std::vector<std::uint8_t> storage;
|
||||
AudioRingHeader* h = make_ring(storage, 4096);
|
||||
check(audio_ring_valid(*h), "freshly inited ring is valid");
|
||||
check(!audio_ring_format_ready(*h), "format not ready before set");
|
||||
audio_ring_set_format(*h, 48000, 2, 32, 3 /*IEEE_FLOAT*/, 8);
|
||||
check(audio_ring_format_ready(*h), "format ready after set");
|
||||
check(h->sample_rate == 48000 && h->channels == 2 && h->bits == 32 && h->format_tag == 3 &&
|
||||
h->block_align == 8,
|
||||
"format fields round-trip");
|
||||
}
|
||||
|
||||
// --- Basic push/pop integrity. ---
|
||||
{
|
||||
std::vector<std::uint8_t> storage;
|
||||
AudioRingHeader* h = make_ring(storage, 4096);
|
||||
|
||||
std::uint8_t src[256];
|
||||
for (int i = 0; i < 256; ++i)
|
||||
{
|
||||
src[i] = static_cast<std::uint8_t>(i);
|
||||
}
|
||||
check(audio_ring_push(*h, src, sizeof(src), 32), "push 256 bytes");
|
||||
check(audio_ring_available(*h) == sizeof(src), "available == pushed");
|
||||
check(h->frames_produced.load() == 32, "frames_produced tracked");
|
||||
|
||||
std::uint8_t dst[256] = {};
|
||||
check(audio_ring_pop(*h, dst, sizeof(dst)) == sizeof(src), "pop returns all bytes");
|
||||
check(std::memcmp(src, dst, sizeof(src)) == 0, "popped bytes match pushed");
|
||||
check(audio_ring_available(*h) == 0, "ring empty after drain");
|
||||
}
|
||||
|
||||
// --- Wrap-around: keep pushing/popping past capacity to force a split copy. ---
|
||||
{
|
||||
const std::uint32_t cap = 1024;
|
||||
std::vector<std::uint8_t> storage;
|
||||
AudioRingHeader* h = make_ring(storage, cap);
|
||||
|
||||
std::uint8_t counter = 0;
|
||||
std::uint8_t expect = 0;
|
||||
const std::uint32_t chunk = 300; // not a divisor of cap, so offsets drift across the seam
|
||||
for (int iter = 0; iter < 50; ++iter)
|
||||
{
|
||||
std::uint8_t buf[300];
|
||||
for (std::uint32_t i = 0; i < chunk; ++i)
|
||||
{
|
||||
buf[i] = counter++;
|
||||
}
|
||||
check(audio_ring_push(*h, buf, chunk, chunk), "wrap push fits");
|
||||
|
||||
std::uint8_t out[300] = {};
|
||||
check(audio_ring_pop(*h, out, chunk) == chunk, "wrap pop full chunk");
|
||||
bool ok = true;
|
||||
for (std::uint32_t i = 0; i < chunk; ++i)
|
||||
{
|
||||
ok = ok && out[i] == expect++;
|
||||
}
|
||||
check(ok, "wrap data integrity across the ring seam");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Overrun: a packet that doesn't fit is dropped whole, bumping overruns. ---
|
||||
{
|
||||
const std::uint32_t cap = 512;
|
||||
std::vector<std::uint8_t> storage;
|
||||
AudioRingHeader* h = make_ring(storage, cap);
|
||||
|
||||
std::vector<std::uint8_t> half(cap / 2, 0xAB);
|
||||
check(audio_ring_push(*h, half.data(), cap / 2, 1), "first half fits");
|
||||
check(audio_ring_push(*h, half.data(), cap / 2, 1), "second half fills ring");
|
||||
check(audio_ring_available(*h) == cap, "ring full");
|
||||
|
||||
std::vector<std::uint8_t> more(16, 0xCD);
|
||||
check(!audio_ring_push(*h, more.data(), 16, 1), "push into full ring rejected");
|
||||
check(h->overruns.load() == 1, "overrun counted");
|
||||
check(audio_ring_available(*h) == cap, "rejected push left ring untouched");
|
||||
|
||||
// Draining makes room again; subsequent pushes succeed.
|
||||
std::vector<std::uint8_t> drain(cap, 0);
|
||||
check(audio_ring_pop(*h, drain.data(), cap) == cap, "drain full ring");
|
||||
check(audio_ring_push(*h, more.data(), 16, 1), "push succeeds after drain");
|
||||
check(h->overruns.load() == 1, "overruns not bumped on success");
|
||||
}
|
||||
|
||||
std::printf(g_failures == 0 ? "AUDIO RING TEST PASS\n" : "AUDIO RING TEST FAILED (%d)\n", g_failures);
|
||||
return g_failures == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user