Mirror the real game's audio so Steam Remote Play Together (which streams the host's own audio session) carries it to the guest. The host captures the game by PID via WASAPI process loopback and re-renders it on the default endpoint; the game still plays locally too (accepted "double audio" for now). - ProcessLoopbackCapture: process-loopback capture client, frame-sink + stats. The completion handler must be agile (IAgileObject) or ActivateAudioInterfaceAsync rejects every call with E_ILLEGAL_METHOD_CALL. - AudioMirror: wraps capture with an event-driven render client and a primed ring buffer; AudioPanel drives it from the injected game's window/PID. - coop_tone: standalone WASAPI sine-wave process used as a known audio source. - audio_loopback_test (CTest): captures coop_tone by PID and asserts non-silent audio arrives, so the path is verifiable without a second Steam account. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
168 lines
4.1 KiB
C++
168 lines
4.1 KiB
C++
// coop_tone: a minimal WASAPI render process that plays a continuous sine wave on
|
|
// the default output endpoint. Used as a known audio source for the audio-mirror
|
|
// integration test (a real process actively rendering audio to capture from).
|
|
//
|
|
// coop_tone [seconds] [frequencyHz]
|
|
//
|
|
// Default: runs ~3 s at 440 Hz. Prints "TONE_RENDERING" once audio is flowing so
|
|
// a parent can synchronize before it starts capturing.
|
|
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <vector>
|
|
|
|
#include <windows.h>
|
|
|
|
#include <audioclient.h>
|
|
#include <mmdeviceapi.h>
|
|
#include <mmreg.h>
|
|
|
|
namespace
|
|
{
|
|
|
|
constexpr double kPi = 3.14159265358979323846;
|
|
|
|
template <typename T>
|
|
void release(T*& p)
|
|
{
|
|
if (p)
|
|
{
|
|
p->Release();
|
|
p = nullptr;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int wmain(int argc, wchar_t** argv)
|
|
{
|
|
const double seconds = (argc > 1) ? _wtof(argv[1]) : 3.0;
|
|
const double freq = (argc > 2) ? _wtof(argv[2]) : 440.0;
|
|
|
|
if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED)))
|
|
{
|
|
std::fprintf(stderr, "CoInitializeEx failed\n");
|
|
return 1;
|
|
}
|
|
|
|
IMMDeviceEnumerator* enumerator = nullptr;
|
|
IMMDevice* endpoint = nullptr;
|
|
IAudioClient* client = nullptr;
|
|
IAudioRenderClient* render = nullptr;
|
|
WAVEFORMATEX* fmt = nullptr;
|
|
int rc = 1;
|
|
|
|
do
|
|
{
|
|
if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
|
|
__uuidof(IMMDeviceEnumerator), reinterpret_cast<void**>(&enumerator))))
|
|
{
|
|
break;
|
|
}
|
|
if (FAILED(enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &endpoint)))
|
|
{
|
|
break;
|
|
}
|
|
if (FAILED(endpoint->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr,
|
|
reinterpret_cast<void**>(&client))))
|
|
{
|
|
break;
|
|
}
|
|
if (FAILED(client->GetMixFormat(&fmt)))
|
|
{
|
|
break;
|
|
}
|
|
|
|
HANDLE buffer_event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
|
constexpr REFERENCE_TIME kBuffer = 30 * 10000; // 30 ms
|
|
if (FAILED(client->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, kBuffer,
|
|
0, fmt, nullptr)))
|
|
{
|
|
break;
|
|
}
|
|
client->SetEventHandle(buffer_event);
|
|
if (FAILED(client->GetService(__uuidof(IAudioRenderClient), reinterpret_cast<void**>(&render))))
|
|
{
|
|
break;
|
|
}
|
|
UINT32 buffer_frames = 0;
|
|
client->GetBufferSize(&buffer_frames);
|
|
|
|
const bool is_float =
|
|
fmt->wFormatTag == WAVE_FORMAT_IEEE_FLOAT ||
|
|
(fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
|
|
reinterpret_cast<WAVEFORMATEXTENSIBLE*>(fmt)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT);
|
|
const unsigned channels = fmt->nChannels;
|
|
const double rate = fmt->nSamplesPerSec;
|
|
const double step = 2.0 * kPi * freq / rate;
|
|
|
|
auto write_frames = [&](UINT32 frames, double& phase) {
|
|
BYTE* data = nullptr;
|
|
if (FAILED(render->GetBuffer(frames, &data)))
|
|
{
|
|
return;
|
|
}
|
|
for (UINT32 i = 0; i < frames; ++i)
|
|
{
|
|
const double s = std::sin(phase) * 0.25; // -12 dB, gentle
|
|
phase += step;
|
|
if (phase > 2.0 * kPi)
|
|
{
|
|
phase -= 2.0 * kPi;
|
|
}
|
|
for (unsigned c = 0; c < channels; ++c)
|
|
{
|
|
if (is_float)
|
|
{
|
|
reinterpret_cast<float*>(data)[i * channels + c] = static_cast<float>(s);
|
|
}
|
|
else
|
|
{
|
|
reinterpret_cast<INT16*>(data)[i * channels + c] =
|
|
static_cast<INT16>(s * 32767.0);
|
|
}
|
|
}
|
|
}
|
|
render->ReleaseBuffer(frames, 0);
|
|
};
|
|
|
|
double phase = 0.0;
|
|
write_frames(buffer_frames, phase); // pre-roll
|
|
client->Start();
|
|
std::printf("TONE_RENDERING pid=%lu %.0fHz %s %.0fHz %uch\n", GetCurrentProcessId(), freq,
|
|
is_float ? "float" : "pcm16", rate, channels);
|
|
std::fflush(stdout);
|
|
|
|
const DWORD end_tick = GetTickCount() + static_cast<DWORD>(seconds * 1000.0);
|
|
while (GetTickCount() < end_tick)
|
|
{
|
|
if (WaitForSingleObject(buffer_event, 200) != WAIT_OBJECT_0)
|
|
{
|
|
continue;
|
|
}
|
|
UINT32 padding = 0;
|
|
if (FAILED(client->GetCurrentPadding(&padding)))
|
|
{
|
|
break;
|
|
}
|
|
write_frames(buffer_frames - padding, phase);
|
|
}
|
|
client->Stop();
|
|
CloseHandle(buffer_event);
|
|
rc = 0;
|
|
} while (false);
|
|
|
|
release(render);
|
|
release(client);
|
|
release(endpoint);
|
|
release(enumerator);
|
|
if (fmt)
|
|
{
|
|
CoTaskMemFree(fmt);
|
|
}
|
|
CoUninitialize();
|
|
return rc;
|
|
}
|