Audio: operator re-measure + format override (host<->hook op channel)
Add a per-stream op channel in AudioRingHeader (op_seq + op_* fields, version 2): the host posts re-measure / override commands, the hook applies them and re-publishes (bumping format_generation), and the host rebuilds its render client live on the change. The Audio panel (under Debug details) gains a "Re-measure rate" button and a rate/channels/bit-depth/format override -- for when detection is wrong or the channels/bit-depth were unrecoverable. Also add a debug-only IPC test harness (-DCOOP_TEST_HARNESS, off by default, absent from the shipped host): a file-based command channel that drives the host's real UI code paths (inject / audio / re-measure / override / screenshot / status) for scripted validation, instead of unreliable synthetic mouse input. Used to validate live: late-attach to coop_tone@44100 -> measured 44100, promoted to hooked; override -> 2ch state, re-measure -> reconverge. Trim the README roadmap to what's left; document the harness + rate_estimator_test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,18 @@ namespace coop
|
||||
inline constexpr std::uint32_t kAudioRingMagic = 0x47525541u;
|
||||
|
||||
// Bump whenever AudioRingHeader's layout changes.
|
||||
inline constexpr std::uint32_t kAudioRingVersion = 1;
|
||||
inline constexpr std::uint32_t kAudioRingVersion = 2;
|
||||
|
||||
// Operator commands the host issues per stream (host -> hook), applied via the op_*
|
||||
// fields in the header. The host writes the fields then bumps op_seq; the hook applies
|
||||
// the command once per new op_seq. Lets the Audio panel re-measure a stream's rate or
|
||||
// override its format when detection is wrong/unrecoverable.
|
||||
enum AudioRingOp : std::uint32_t
|
||||
{
|
||||
AudioRingOp_None = 0,
|
||||
AudioRingOp_Remeasure = 1, // re-run the sample-rate measurement for this stream
|
||||
AudioRingOp_Override = 2, // adopt the op_rate/channels/bits/format_tag verbatim
|
||||
};
|
||||
|
||||
// Per-pid mapping name, mirroring kSharedMemoryPrefix: coop_audio_<pid>.
|
||||
inline constexpr wchar_t kAudioRingPrefix[] = L"Local\\coop_audio_";
|
||||
@@ -66,7 +77,17 @@ struct AudioRingHeader
|
||||
std::atomic<std::uint64_t> frames_produced; // cumulative frames pushed
|
||||
std::atomic<std::uint64_t> overruns; // packets dropped on a full ring
|
||||
|
||||
std::uint8_t reserved[64];
|
||||
// --- Operator control (host -> hook) ---------------------------------------
|
||||
// The host writes op_kind + the op_* fields, then bumps op_seq (release); the hook
|
||||
// applies the command once per new op_seq (acquire). See AudioRingOp.
|
||||
std::atomic<std::uint32_t> op_seq; // bumped by the host on each new command
|
||||
std::uint32_t op_kind; // AudioRingOp
|
||||
std::uint32_t op_rate; // override: sample rate
|
||||
std::uint32_t op_channels; // override: channel count
|
||||
std::uint32_t op_bits; // override: bits per sample
|
||||
std::uint32_t op_format_tag; // override: WAVE_FORMAT_PCM / _IEEE_FLOAT
|
||||
|
||||
std::uint8_t reserved[40];
|
||||
|
||||
// std::uint8_t data[capacity] follows immediately in the mapping.
|
||||
};
|
||||
@@ -104,6 +125,12 @@ inline void audio_ring_init(AudioRingHeader& h, std::uint32_t capacity)
|
||||
h.read_pos.store(0, std::memory_order_relaxed);
|
||||
h.frames_produced.store(0, std::memory_order_relaxed);
|
||||
h.overruns.store(0, std::memory_order_relaxed);
|
||||
h.op_seq.store(0, std::memory_order_relaxed);
|
||||
h.op_kind = 0;
|
||||
h.op_rate = 0;
|
||||
h.op_channels = 0;
|
||||
h.op_bits = 0;
|
||||
h.op_format_tag = 0;
|
||||
std::memset(h.reserved, 0, sizeof(h.reserved));
|
||||
}
|
||||
|
||||
@@ -185,6 +212,50 @@ inline std::uint32_t audio_ring_pop(AudioRingHeader& h, void* dst, std::uint32_t
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Host: post an operator command to the hook for this stream. Writes the fields, then
|
||||
// bumps op_seq (release) so the hook applies it exactly once. For a re-measure the
|
||||
// rate/channels/bits are ignored.
|
||||
inline void audio_ring_post_op(AudioRingHeader& h, std::uint32_t kind, std::uint32_t rate = 0,
|
||||
std::uint32_t channels = 0, std::uint32_t bits = 0,
|
||||
std::uint32_t format_tag = 0)
|
||||
{
|
||||
h.op_kind = kind;
|
||||
h.op_rate = rate;
|
||||
h.op_channels = channels;
|
||||
h.op_bits = bits;
|
||||
h.op_format_tag = format_tag;
|
||||
h.op_seq.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
// One operator command read back by the hook.
|
||||
struct AudioRingOpCmd
|
||||
{
|
||||
std::uint32_t kind = AudioRingOp_None;
|
||||
std::uint32_t rate = 0;
|
||||
std::uint32_t channels = 0;
|
||||
std::uint32_t bits = 0;
|
||||
std::uint32_t format_tag = 0;
|
||||
};
|
||||
|
||||
// Hook: if a new op was posted since `last_seq`, read it into `out`, advance `last_seq`,
|
||||
// and return its kind; otherwise returns AudioRingOp_None. Robust to ring re-creation
|
||||
// (op_seq resets to 0 -> a stale higher last_seq just reads the zeroed None command).
|
||||
inline std::uint32_t audio_ring_poll_op(AudioRingHeader& h, std::uint32_t& last_seq, AudioRingOpCmd& out)
|
||||
{
|
||||
const std::uint32_t seq = h.op_seq.load(std::memory_order_acquire);
|
||||
if (seq == last_seq)
|
||||
{
|
||||
return AudioRingOp_None;
|
||||
}
|
||||
last_seq = seq;
|
||||
out.kind = h.op_kind;
|
||||
out.rate = h.op_rate;
|
||||
out.channels = h.op_channels;
|
||||
out.bits = h.op_bits;
|
||||
out.format_tag = h.op_format_tag;
|
||||
return out.kind;
|
||||
}
|
||||
|
||||
// Build the per-pid audio ring name both sides agree on. Stream 0 keeps the bare
|
||||
// coop_audio_<pid> name (backward compatible / the single-stream case); additional
|
||||
// streams append _<index> (coop_audio_<pid>_1, _2, ...). The host captures every
|
||||
|
||||
Reference in New Issue
Block a user