// Shared-memory audio ring for the injection render-hook audio path. // // The injected hook (coop_hook.dll) captures the game's WASAPI render frames and // is the sole *producer*; the host (coop_host.exe) is the sole *consumer* and // re-renders the frames for Steam Remote Play Together. This is a separate, // larger mapping from the input/status SharedBlock (which is only 20-byte pads // and can't hold PCM): a header followed by a byte ring of `capacity` bytes. // // Lock-free SPSC with free-running 64-bit positions (release on publish, acquire // on read) — the same cross-process atomic model as the input seqlock. POD and // version-locked: both modules compile this identical header. #pragma once #include #include #include #include #include namespace coop { // 'AURG' little-endian; sanity-checks the mapping before either side trusts it. inline constexpr std::uint32_t kAudioRingMagic = 0x47525541u; // Bump whenever AudioRingHeader's layout changes. 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_. inline constexpr wchar_t kAudioRingPrefix[] = L"Local\\coop_audio_"; // Byte capacity of the PCM ring. 1 MiB is >1 s even at 48 kHz / 2 ch / 32-bit // float (384 kB/s); the host should always keep up, so this is pure slack. inline constexpr std::uint32_t kAudioRingCapacity = 1u << 20; // Header preceding the PCM data. All multi-process-shared counters are atomic; // the format fields are written once by the producer *before* it publishes // format_valid (release), and read by the consumer *after* it observes // format_valid (acquire), so they need no atomicity of their own. struct AudioRingHeader { std::uint32_t magic; std::uint32_t version; // Host-owned gate. The hook copies+silences frames only while this is 1; // when 0 the game audio passes through locally and nothing is mirrored // (stream counting in HookStatus still runs regardless of this flag). std::atomic capture_enabled; // Producer publishes the captured stream's format once, then sets // format_valid=1 (release). format_generation is reserved so a future // mid-session device re-init can be made forward-compatible; v1 sets once. std::atomic format_valid; std::atomic format_generation; std::uint32_t sample_rate; std::uint32_t channels; std::uint32_t bits; std::uint32_t format_tag; // WAVE_FORMAT_* (PCM=1, IEEE_FLOAT=3, EXTENSIBLE=0xFFFE) std::uint32_t block_align; // bytes per frame (all channels) std::uint32_t capacity; // bytes in the trailing data region std::atomic write_pos; // producer cursor, free-running std::atomic read_pos; // consumer cursor, free-running std::atomic frames_produced; // cumulative frames pushed std::atomic overruns; // packets dropped on a full ring // --- 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 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 // Host -> hook: format-verification co-capture. While 1, the hook pushes a still-being-measured // (guessed) stream's raw pre-mix bytes into the ring WITHOUT silencing the game, so the host can // capture both the hook (pre-mix) and a parallel process-loopback (post-mix) of the same audio // and cross-correlate them to recover the true sample rate (and, in step b, channels/bit-depth) // from ground truth instead of guessing. Inert (0) by default -- normal capture is unaffected, // so it never changes the shipping no-echo path. Repurposed from `reserved`, so the layout and // size are unchanged (old builds saw it as a zero reserved byte). std::atomic verify_capture; std::uint8_t reserved[36]; // std::uint8_t data[capacity] follows immediately in the mapping. }; static_assert(std::atomic::is_always_lock_free, "audio ring needs a lock-free 64-bit atomic for cross-process use"); // Total mapping size for a ring of `capacity` bytes. inline constexpr std::size_t audio_ring_total_size(std::uint32_t capacity) { return sizeof(AudioRingHeader) + capacity; } // Pointer to the PCM data region following the header. inline std::uint8_t* audio_ring_data(AudioRingHeader* h) { return reinterpret_cast(h) + sizeof(AudioRingHeader); } // Host side: stamp a freshly created mapping into a valid empty ring. inline void audio_ring_init(AudioRingHeader& h, std::uint32_t capacity) { h.magic = kAudioRingMagic; h.version = kAudioRingVersion; h.capture_enabled.store(0, std::memory_order_relaxed); h.format_valid.store(0, std::memory_order_relaxed); h.format_generation.store(0, std::memory_order_relaxed); h.sample_rate = 0; h.channels = 0; h.bits = 0; h.format_tag = 0; h.block_align = 0; h.capacity = capacity; h.write_pos.store(0, std::memory_order_relaxed); 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; h.verify_capture.store(0, std::memory_order_relaxed); std::memset(h.reserved, 0, sizeof(h.reserved)); } // Validate a mapping the other side created/opened. inline bool audio_ring_valid(const AudioRingHeader& h) { return h.magic == kAudioRingMagic && h.version == kAudioRingVersion && h.capacity != 0; } // Producer (hook): publish the captured stream format, then mark it valid. inline void audio_ring_set_format(AudioRingHeader& h, std::uint32_t sample_rate, std::uint32_t channels, std::uint32_t bits, std::uint32_t format_tag, std::uint32_t block_align) { h.sample_rate = sample_rate; h.channels = channels; h.bits = bits; h.format_tag = format_tag; h.block_align = block_align; h.format_generation.fetch_add(1, std::memory_order_relaxed); h.format_valid.store(1, std::memory_order_release); } // Consumer (host): true once the producer has published a format. inline bool audio_ring_format_ready(const AudioRingHeader& h) { return h.format_valid.load(std::memory_order_acquire) != 0; } // Producer: push `bytes` of PCM. Returns false (and bumps overruns) if the ring // can't hold the whole packet, in which case nothing is written — drop the // packet rather than tear a frame. `frames` is recorded for the diagnostics. inline bool audio_ring_push(AudioRingHeader& h, const void* src, std::uint32_t bytes, std::uint32_t frames) { const std::uint64_t w = h.write_pos.load(std::memory_order_relaxed); const std::uint64_t r = h.read_pos.load(std::memory_order_acquire); const std::uint32_t used = static_cast(w - r); if (bytes > h.capacity - used) { h.overruns.fetch_add(1, std::memory_order_relaxed); return false; } std::uint8_t* data = audio_ring_data(&h); const std::uint32_t off = static_cast(w % h.capacity); const std::uint32_t first = std::min(bytes, h.capacity - off); std::memcpy(data + off, src, first); if (bytes > first) { std::memcpy(data, static_cast(src) + first, bytes - first); } h.write_pos.store(w + bytes, std::memory_order_release); h.frames_produced.fetch_add(frames, std::memory_order_relaxed); return true; } // Consumer: bytes currently available to read. inline std::uint32_t audio_ring_available(const AudioRingHeader& h) { const std::uint64_t w = h.write_pos.load(std::memory_order_acquire); const std::uint64_t r = h.read_pos.load(std::memory_order_relaxed); return static_cast(w - r); } // Producer: bytes currently free (so a multi-part packet can be checked to fit before any of it is // written -- keeps a self-describing [header][payload] framing from tearing on a full ring). inline std::uint32_t audio_ring_free_space(const AudioRingHeader& h) { const std::uint64_t w = h.write_pos.load(std::memory_order_relaxed); const std::uint64_t r = h.read_pos.load(std::memory_order_acquire); return h.capacity - static_cast(w - r); } // Consumer: copy up to `bytes` into `dst`; returns the number actually popped. inline std::uint32_t audio_ring_pop(AudioRingHeader& h, void* dst, std::uint32_t bytes) { const std::uint64_t r = h.read_pos.load(std::memory_order_relaxed); const std::uint64_t w = h.write_pos.load(std::memory_order_acquire); const std::uint32_t avail = static_cast(w - r); bytes = std::min(bytes, avail); const std::uint8_t* data = audio_ring_data(&h); const std::uint32_t off = static_cast(r % h.capacity); const std::uint32_t first = std::min(bytes, h.capacity - off); std::memcpy(dst, data + off, first); if (bytes > first) { std::memcpy(static_cast(dst) + first, data, bytes - first); } h.read_pos.store(r + bytes, std::memory_order_release); 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_ name (backward compatible / the single-stream case); additional // streams append _ (coop_audio__1, _2, ...). The host captures every // render stream into its own ring and mixes them. inline std::wstring audio_ring_name(unsigned long target_pid, unsigned index = 0) { std::wstring name = std::wstring(kAudioRingPrefix) + std::to_wstring(target_pid); if (index != 0) { name += L"_" + std::to_wstring(index); } return name; } } // namespace coop