Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions SarTest/SarTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ int usage()
" SarTest endpoint-names\n"
" Rename an endpoint keeping its ID, then give it a new ID, and report\n"
" the name Windows shows each time (issues #15, #66).\n"
" SarTest meter [--duration S] [--process <name.exe>] [--min-peak P]\n"
" Watch the audio sessions on every active playback endpoint and\n"
" report which processes played and how loud; with --process, fail\n"
" unless that process reached --min-peak (default 0.01).\n"
"\n"
"Layout options: --endpoints N (playback/recording pairs, default 2)\n"
" --channels C (per endpoint, default 2) --prefix <name>\n"
Expand Down Expand Up @@ -822,6 +826,35 @@ int cmdEndpointNames(const Args& args)
return rc;
}

int cmdMeter(const Args& args)
{
SessionMeterOptions options;

options.durationSeconds = args.getDouble(L"duration", 10.0);
options.process = args.get(L"process", L"");
options.minPeak = args.getDouble(L"min-peak", 0.01);

SessionMeterResult meter = runSessionMeter(options);
int rc = meter.passed ? 0 : 2;
JsonObject result;

result.setString("command", "meter")
.setBool("passed", meter.passed)
.setInt("exitCode", rc)
.setString("process", narrow(options.process))
.setDouble("processPeak", meter.processPeak)
.setRaw("sessions", meter.sessionsJson);

if (!meter.failure.empty()) {
result.setString("failure", meter.failure);
logf("%s", meter.failure.c_str());
}

writeResults(args, result.str());
logf("%s", rc == 0 ? "PASSED" : "FAILED");
return rc;
}

} // namespace

int wmain(int argc, wchar_t **argv)
Expand Down Expand Up @@ -861,6 +894,8 @@ int wmain(int argc, wchar_t **argv)
rc = cmdNoInterface(args);
} else if (args.command == L"endpoint-names") {
rc = cmdEndpointNames(args);
} else if (args.command == L"meter") {
rc = cmdMeter(args);
} else {
rc = usage();
}
Expand Down
169 changes: 169 additions & 0 deletions SarTest/wasapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1564,4 +1564,173 @@ void RaceOpeners::work(Worker *worker)
}


namespace {

std::wstring processImageName(DWORD pid)
{
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
std::wstring name;

if (!process) {
return name;
}

wchar_t path[MAX_PATH] = {};
DWORD size = MAX_PATH;

if (QueryFullProcessImageNameW(process, 0, path, &size)) {
name = path;

auto slash = name.find_last_of(L"\\/");

if (slash != std::wstring::npos) {
name = name.substr(slash + 1);
}
}

CloseHandle(process);
return name;
}

struct SessionSeen
{
std::string endpoint;
DWORD pid = 0;
std::wstring process;
bool active = false;
float maxPeak = 0.0f;
};

} // namespace

SessionMeterResult runSessionMeter(const SessionMeterOptions& options)
{
SessionMeterResult result;
ComPtr<IMMDeviceEnumerator> enumerator;

if (!createEnumerator(&enumerator)) {
result.failure = "no device enumerator";
result.sessionsJson = "[]";
return result;
}

// Keyed by endpoint and process ID.
std::map<std::string, SessionSeen> seen;
double deadline = nowMs() + options.durationSeconds * 1000.0;

while (nowMs() < deadline) {
ComPtr<IMMDeviceCollection> devices;
UINT count = 0;

if (SUCCEEDED(enumerator->EnumAudioEndpoints(
eRender, DEVICE_STATE_ACTIVE, devices.put()))) {
devices->GetCount(&count);
}

for (UINT i = 0; i < count; ++i) {
ComPtr<IMMDevice> device;
ComPtr<IPropertyStore> store;
ComPtr<IAudioSessionManager2> manager;
ComPtr<IAudioSessionEnumerator> sessions;
std::string endpoint;
int sessionCount = 0;

if (FAILED(devices->Item(i, device.put()))) {
continue;
}

if (SUCCEEDED(device->OpenPropertyStore(STGM_READ, store.put()))) {
endpoint = narrow(propertyString(store.get(), PKEY_Device_FriendlyName));
}

if (FAILED(device->Activate(__uuidof(IAudioSessionManager2),
CLSCTX_ALL, nullptr, manager.putVoid())) ||
FAILED(manager->GetSessionEnumerator(sessions.put()))) {
continue;
}

sessions->GetCount(&sessionCount);

for (int k = 0; k < sessionCount; ++k) {
ComPtr<IAudioSessionControl> control;
ComPtr<IAudioSessionControl2> control2;
ComPtr<IAudioMeterInformation> meter;
AudioSessionState state = AudioSessionStateInactive;
DWORD pid = 0;
float peak = 0.0f;

if (FAILED(sessions->GetSession(k, control.put())) ||
FAILED(control->QueryInterface(
__uuidof(IAudioSessionControl2), control2.putVoid()))) {
continue;
}

control2->GetProcessId(&pid);

if (pid == 0) {
continue; // system sounds
}

control->GetState(&state);

if (SUCCEEDED(control->QueryInterface(
__uuidof(IAudioMeterInformation), meter.putVoid()))) {
meter->GetPeakValue(&peak);
}

SessionSeen& entry = seen[endpoint + "|" + std::to_string(pid)];

if (entry.pid == 0) {
entry.endpoint = endpoint;
entry.pid = pid;
entry.process = processImageName(pid);
logf("Session of %s (pid %lu) on %s",
narrow(entry.process).c_str(), pid, endpoint.c_str());
}

entry.active = entry.active || state == AudioSessionStateActive;
entry.maxPeak = std::max(entry.maxPeak, peak);
}
}

Sleep(100);
}

std::vector<std::string> items;

for (auto& pair : seen) {
const SessionSeen& entry = pair.second;
JsonObject item;

logf("%s (pid %lu) on %s: %s, peak %.3f", narrow(entry.process).c_str(),
entry.pid, entry.endpoint.c_str(), entry.active ? "active" : "never active",
entry.maxPeak);
item.setString("endpoint", entry.endpoint)
.setInt("pid", entry.pid)
.setString("process", narrow(entry.process))
.setBool("active", entry.active)
.setDouble("maxPeak", entry.maxPeak);
items.push_back(item.str());

if (!options.process.empty() &&
_wcsicmp(entry.process.c_str(), options.process.c_str()) == 0) {
result.processPeak = std::max(result.processPeak, (double)entry.maxPeak);
}
}

result.sessionsJson = jsonArray(items);

if (options.process.empty() || result.processPeak >= options.minPeak) {
result.passed = true;
} else {
char buf[160];

sprintf_s(buf, "%s never played louder than %.3f (needs %.3f)",
narrow(options.process).c_str(), result.processPeak, options.minPeak);
result.failure = buf;
}

return result;
}

} // namespace SarTest
22 changes: 22 additions & 0 deletions SarTest/wasapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,27 @@ class RaceOpeners
bool _clean = true;
};

// Watches the audio sessions on every active playback endpoint for a while:
// which process each belongs to and how loud it got. Tells whether an
// application outside the harness (a browser) produced audio at all.
struct SessionMeterOptions
{
double durationSeconds = 10.0;
// Image name (e.g. msedge.exe) that must reach minPeak; empty to only
// report.
std::wstring process;
double minPeak = 0.01;
};

struct SessionMeterResult
{
bool passed = false;
double processPeak = 0.0;
std::string failure;
std::string sessionsJson;
};

SessionMeterResult runSessionMeter(const SessionMeterOptions& options);

} // namespace SarTest
#endif // _SAR_TEST_WASAPI_H
52 changes: 52 additions & 0 deletions SynchronousAudioRouter/entry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -746,8 +746,60 @@ VOID SarUnload(PDRIVER_OBJECT driverObject)
}
}

// The buffer of a ProcessMitigationPolicy query (what user mode's
// GetProcessMitigationPolicy uses): the policy to read, then its flags.
typedef struct SarProcessMitigationPolicyInformation
{
ULONG policy;
ULONG flags;
} SarProcessMitigationPolicyInformation;

// Exported by the kernel and documented, but not declared by the headers
// this driver includes.
extern "C" NTSYSAPI NTSTATUS NTAPI ZwQueryInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_Out_writes_bytes_(ProcessInformationLength) PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength);

#define SAR_PROCESS_MITIGATION_POLICY_CLASS ((PROCESSINFOCLASS)52)
#define SAR_PROCESS_SIGNATURE_POLICY 8
// PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY's MicrosoftSignedOnly,
// StoreSignedOnly and MitigationOptIn (Microsoft, Store or WHQL) flags.
#define SAR_SIGNATURE_POLICY_RESTRICTIONS 0x7

// Whether the current process may only load images signed by Microsoft (or
// the Store, or WHQL). SarAsio.dll is none of those.
static BOOL SarCurrentProcessRequiresMicrosoftSignedImages()
{
SarProcessMitigationPolicyInformation info = {};

info.policy = SAR_PROCESS_SIGNATURE_POLICY;

NTSTATUS status = ZwQueryInformationProcess(
ZwCurrentProcess(), SAR_PROCESS_MITIGATION_POLICY_CLASS,
&info, sizeof(info), nullptr);

if (!NT_SUCCESS(status)) {
return FALSE;
}

return (info.flags & SAR_SIGNATURE_POLICY_RESTRICTIONS) != 0;
}

BOOL SarFilterMatchesCurrentProcess(SarDriverExtension *extension)
{
// Pointing a process that may only load Microsoft-signed images at
// SarAsio.dll makes Code Integrity refuse the load, so the audio classes
// can't be created there at all: Chromium's sandboxed audio service
// (Chrome, Edge, Discord) does this and then plays nothing. Leave such a
// process with the system's classes; it loses application routing but
// keeps its audio.
if (SarCurrentProcessRequiresMicrosoftSignedImages()) {
return FALSE;
}

PTOKEN_USER tokenUser = nullptr;
NTSTATUS status = SarCopyProcessUser(PsGetCurrentProcess(), &tokenUser);

Expand Down
Loading
Loading