Skip to content

Commit 6e48a90

Browse files
eizclaude
andcommitted
Remove the format change broadcast
Whenever any audio endpoint became active, SarAsio had the driver send KSEVENT_PINCAPS_FORMATCHANGE to every SAR filter on its next tick, and the audio engine invalidated every stream open on a SAR endpoint. Its check for SAR endpoints passes for any endpoint (IPropertyStore returns S_OK and VT_EMPTY for a missing key), so starting a host invalidated its own fresh streams, and so did every endpoint Windows rebuilt and any other device appearing. The event was added for #2, so the audio engine would re-read the format of an endpoint recreated at a different sample rate. It wasn't enough on Windows 10, whose format cache ignored it, so the driver also puts the channel count, sample rate and sample size into each endpoint's KS reference string there, which makes such an endpoint a new one to Windows. With Windows 10 required, the event does nothing useful. Remove it, SarAsio's endpoint notification client that only existed to trigger it, the driver's SAR_SEND_FORMAT_CHANGE_EVENT IOCTL, and SarConfigure's -u switch that sent it by hand. On the test VM (16 endpoint pairs, three start/stop iterations), the loopback run saw 34 to 116 stream invalidations per run with the broadcast (3 runs) and none without it (8 runs). The format-* checks (same endpoint IDs at 48 kHz, 96 kHz and 48 kHz again) pass with and without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent cbcda7a commit 6e48a90

7 files changed

Lines changed: 16 additions & 326 deletions

File tree

‎SarAsio/sarclient.cpp‎

Lines changed: 0 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,6 @@ void SarClient::tick(long bufferIndex)
4343
if (!_registers)
4444
return;
4545

46-
if (_updateSampleRateOnTick.exchange(false)) {
47-
DWORD dummy;
48-
49-
DeviceIoControl(_device, SAR_SEND_FORMAT_CHANGE_EVENT,
50-
nullptr, 0, nullptr, 0, &dummy, nullptr);
51-
}
52-
5346
// for each endpoint
5447
// read isActive, generation and buffer offset/size/position
5548
// if offset/size invalid, skip endpoint (fill asio buffers with 0)
@@ -187,12 +180,6 @@ bool SarClient::start()
187180
return false;
188181
}
189182

190-
if (!openMmNotificationClient()) {
191-
LOG(ERROR) << "Couldn't open MMDevice notification client";
192-
stop();
193-
return false;
194-
}
195-
196183
if (!setBufferLayout()) {
197184
LOG(ERROR) << "Couldn't set layout";
198185
stop();
@@ -214,21 +201,6 @@ bool SarClient::start()
214201

215202
void SarClient::stop()
216203
{
217-
if (_mmNotificationClientRegistered) {
218-
_mmEnumerator->UnregisterEndpointNotificationCallback(
219-
_mmNotificationClient);
220-
_mmNotificationClientRegistered = false;
221-
}
222-
223-
if (_mmNotificationClient) {
224-
_mmNotificationClient->Release();
225-
_mmNotificationClient = nullptr;
226-
}
227-
228-
if (_mmEnumerator) {
229-
_mmEnumerator = nullptr;
230-
}
231-
232204
if (_device != INVALID_HANDLE_VALUE) {
233205
_registersLock.lock();
234206
CancelIoEx(_device, nullptr);
@@ -327,34 +299,6 @@ bool SarClient::openControlDevice()
327299
return true;
328300
}
329301

330-
bool SarClient::openMmNotificationClient()
331-
{
332-
if (FAILED(CoCreateInstance(
333-
__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
334-
__uuidof(IMMDeviceEnumerator), (LPVOID *)&_mmEnumerator))) {
335-
336-
return false;
337-
}
338-
339-
if (FAILED(CComObject<NotificationClient>::CreateInstance(
340-
&_mmNotificationClient))) {
341-
342-
return false;
343-
}
344-
345-
_mmNotificationClient->AddRef();
346-
_mmNotificationClient->setClient(shared_from_this());
347-
348-
if (FAILED(_mmEnumerator->RegisterEndpointNotificationCallback(
349-
_mmNotificationClient))) {
350-
351-
return false;
352-
}
353-
354-
_mmNotificationClientRegistered = true;
355-
return true;
356-
}
357-
358302
bool SarClient::setBufferLayout()
359303
{
360304
SarSetBufferLayoutRequest request = {};
@@ -580,89 +524,4 @@ void SarClient::mux(
580524
}
581525
}
582526

583-
HRESULT STDMETHODCALLTYPE SarClient::NotificationClient::OnDeviceStateChanged(
584-
_In_ LPCWSTR pwstrDeviceId,
585-
_In_ DWORD dwNewState)
586-
{
587-
// When a SAR endpoint is re-activated after its initial creation, its
588-
// supported sample rate may be different. To force the audio engine to
589-
// notice the possible format change, we listen for device state change
590-
// events and tell the kernel mode driver to broadcast a
591-
// KSEVENT_PINCAPS_FORMATCHANGE event, which causes the audio engine to
592-
// re-query the pin capabilities. This isn't needed for newly added
593-
// endpoints or non-SAR endpoints, so we filter out those events.
594-
if (dwNewState != DEVICE_STATE_ACTIVE) {
595-
return S_OK;
596-
}
597-
598-
if (auto client = _client.lock()) {
599-
do {
600-
CComPtr<IMMDeviceEnumerator> mmEnumerator;
601-
CComPtr<IMMDevice> device;
602-
CComPtr<IPropertyStore> ps;
603-
PROPVARIANT pvalue = {};
604-
605-
// This seems a bit shady, but MSDN's device events example
606-
// initializes COM the same way. It's not clear what the ownership
607-
// of the thread that delivers the IMMNotificationClient events is.
608-
CoInitialize(NULL);
609-
610-
if (FAILED(CoCreateInstance(
611-
__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
612-
__uuidof(IMMDeviceEnumerator), (LPVOID *)&mmEnumerator))) {
613-
614-
break;
615-
}
616-
617-
if (FAILED(mmEnumerator->GetDevice(pwstrDeviceId, &device))) {
618-
break;
619-
}
620-
621-
if (FAILED(device->OpenPropertyStore(STGM_READ, &ps))) {
622-
break;
623-
}
624-
625-
if (FAILED(ps->GetValue(
626-
PKEY_SynchronousAudioRouter_EndpointId, &pvalue))) {
627-
628-
break;
629-
}
630-
631-
client->updateSampleRateOnTick();
632-
PropVariantClear(&pvalue);
633-
} while(false);
634-
635-
CoUninitialize();
636-
}
637-
638-
return S_OK;
639-
}
640-
641-
HRESULT STDMETHODCALLTYPE SarClient::NotificationClient::OnDeviceAdded(
642-
_In_ LPCWSTR pwstrDeviceId)
643-
{
644-
return S_OK;
645-
}
646-
647-
HRESULT STDMETHODCALLTYPE SarClient::NotificationClient::OnDeviceRemoved(
648-
_In_ LPCWSTR pwstrDeviceId)
649-
{
650-
return S_OK;
651-
}
652-
653-
HRESULT STDMETHODCALLTYPE SarClient::NotificationClient::OnDefaultDeviceChanged(
654-
_In_ EDataFlow flow,
655-
_In_ ERole role,
656-
_In_ LPCWSTR pwstrDefaultDeviceId)
657-
{
658-
return S_OK;
659-
}
660-
661-
HRESULT STDMETHODCALLTYPE SarClient::NotificationClient::OnPropertyValueChanged(
662-
_In_ LPCWSTR pwstrDeviceId,
663-
_In_ const PROPERTYKEY key)
664-
{
665-
return S_OK;
666-
}
667-
668527
} // namespace Sar

‎SarAsio/sarclient.h‎

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@ struct SarClient: public std::enable_shared_from_this<SarClient>
3939
void tick(long bufferIndex);
4040
bool start();
4141
void stop();
42-
void updateSampleRateOnTick()
43-
{
44-
_updateSampleRateOnTick = true;
45-
}
4642

4743
private:
4844
struct NotificationHandle
@@ -64,42 +60,7 @@ struct SarClient: public std::enable_shared_from_this<SarClient>
6460
SarHandleQueueResponse responses[32];
6561
};
6662

67-
struct ATL_NO_VTABLE NotificationClient:
68-
public CComObjectRootEx<CComMultiThreadModel>,
69-
public IMMNotificationClient
70-
{
71-
BEGIN_COM_MAP(NotificationClient)
72-
COM_INTERFACE_ENTRY(IMMNotificationClient)
73-
END_COM_MAP()
74-
75-
DECLARE_NO_REGISTRY()
76-
77-
void setClient(std::weak_ptr<SarClient> client)
78-
{
79-
_client = client;
80-
}
81-
82-
virtual HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(
83-
_In_ LPCWSTR pwstrDeviceId,
84-
_In_ DWORD dwNewState) override;
85-
virtual HRESULT STDMETHODCALLTYPE OnDeviceAdded(
86-
_In_ LPCWSTR pwstrDeviceId) override;
87-
virtual HRESULT STDMETHODCALLTYPE OnDeviceRemoved(
88-
_In_ LPCWSTR pwstrDeviceId) override;
89-
virtual HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(
90-
_In_ EDataFlow flow,
91-
_In_ ERole role,
92-
_In_ LPCWSTR pwstrDefaultDeviceId) override;
93-
virtual HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(
94-
_In_ LPCWSTR pwstrDeviceId,
95-
_In_ const PROPERTYKEY key) override;
96-
97-
private:
98-
std::weak_ptr<SarClient> _client;
99-
};
100-
10163
bool openControlDevice();
102-
bool openMmNotificationClient();
10364
bool setBufferLayout();
10465
bool createEndpoints();
10566
bool enableRegistryFilter();
@@ -127,10 +88,6 @@ struct SarClient: public std::enable_shared_from_this<SarClient>
12788
volatile SarEndpointRegisters *_registers;
12889
HandleQueueCompletion _handleQueueCompletion;
12990
bool _handleQueueStarted;
130-
CComPtr<IMMDeviceEnumerator> _mmEnumerator;
131-
CComObject<NotificationClient> *_mmNotificationClient = nullptr;
132-
bool _mmNotificationClientRegistered = false;
133-
std::atomic<bool> _updateSampleRateOnTick = false;
13491
std::mutex _registersLock;
13592
};
13693

‎SarConfigure/SarConfigure.cpp‎

Lines changed: 11 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -59,89 +59,21 @@ static int openConfigurationDialog(Sar::AsioDriver& driver) {
5959
}
6060
}
6161

62-
static bool broadcastPinFormatChange() {
63-
HDEVINFO devinfo;
64-
SP_DEVICE_INTERFACE_DATA interfaceData;
65-
PSP_DEVICE_INTERFACE_DETAIL_DATA interfaceDetail;
66-
DWORD requiredSize;
67-
HANDLE _device;
68-
DWORD dummy;
69-
70-
interfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
71-
devinfo = SetupDiGetClassDevs(
72-
&GUID_DEVINTERFACE_SYNCHRONOUSAUDIOROUTER, nullptr, nullptr,
73-
DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
74-
75-
if (devinfo == INVALID_HANDLE_VALUE) {
76-
return false;
77-
}
78-
79-
if (!SetupDiEnumDeviceInterfaces(devinfo, NULL,
80-
&GUID_DEVINTERFACE_SYNCHRONOUSAUDIOROUTER, 0, &interfaceData)) {
81-
82-
return false;
83-
}
84-
85-
SetLastError(0);
86-
SetupDiGetDeviceInterfaceDetail(
87-
devinfo, &interfaceData, nullptr, 0, &requiredSize, nullptr);
88-
89-
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
90-
return false;
91-
}
92-
93-
interfaceDetail = (PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(requiredSize);
94-
interfaceDetail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
95-
96-
if (!SetupDiGetDeviceInterfaceDetail(
97-
devinfo, &interfaceData, interfaceDetail,
98-
requiredSize, nullptr, nullptr)) {
99-
100-
free(interfaceDetail);
101-
return false;
102-
}
103-
104-
_device = CreateFile(interfaceDetail->DevicePath,
105-
GENERIC_ALL, 0, nullptr, OPEN_EXISTING,
106-
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, nullptr);
107-
108-
if (_device == INVALID_HANDLE_VALUE) {
109-
free(interfaceDetail);
110-
return false;
111-
}
112-
113-
free(interfaceDetail);
114-
115-
DeviceIoControl(_device, SAR_SEND_FORMAT_CHANGE_EVENT,
116-
nullptr, 0, nullptr, 0, &dummy, nullptr);
117-
printf("Sent SAR_SEND_FORMAT_CHANGE_EVENT\n");
118-
119-
CloseHandle(_device);
120-
121-
return true;
122-
}
123-
12462
int main(int argc, char *argv[])
12563
{
64+
CoInitialize(NULL);
12665

127-
if (argc >= 2 && !strcmp(argv[1], "-u")) {
128-
broadcastPinFormatChange();
129-
}
130-
else {
131-
CoInitialize(NULL);
132-
133-
std::vector<Sar::AsioDriver> asioDrivers = Sar::InstalledAsioDrivers();
66+
std::vector<Sar::AsioDriver> asioDrivers = Sar::InstalledAsioDrivers();
13467

135-
printf("\n");
136-
for (size_t i = 0; i < asioDrivers.size(); i++) {
137-
printf("Checking driver: %s == %s\n", asioDrivers[i].clsid.c_str(), CLSID_STR_SynchronousAudioRouter);
138-
if (asioDrivers[i].clsid == CLSID_STR_SynchronousAudioRouter) {
139-
Sar::AsioDriver& driver = asioDrivers[i];
140-
printf("Found SAR ASIO driver: %s\n", driver.name.c_str());
141-
return openConfigurationDialog(driver);
142-
}
68+
printf("\n");
69+
for (size_t i = 0; i < asioDrivers.size(); i++) {
70+
printf("Checking driver: %s == %s\n", asioDrivers[i].clsid.c_str(), CLSID_STR_SynchronousAudioRouter);
71+
if (asioDrivers[i].clsid == CLSID_STR_SynchronousAudioRouter) {
72+
Sar::AsioDriver& driver = asioDrivers[i];
73+
printf("Found SAR ASIO driver: %s\n", driver.name.c_str());
74+
return openConfigurationDialog(driver);
14375
}
144-
145-
CoUninitialize();
14676
}
77+
78+
CoUninitialize();
14779
}

‎SarTest/wasapi.h‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,10 @@ struct WasapiOptions
9999
bool expectInvalidation = false;
100100
double minValidRatio = 0.5;
101101
long long maxDiscontinuities = -1; // -1: report only
102-
// SarAsio broadcasts a format change whenever one of its endpoints
103-
// becomes active, which invalidates open streams: wait before streaming,
104-
// and reopen invalidated streams like a well-behaved client would.
102+
// Streams opened just as a host's endpoints appear can be invalidated
103+
// (SarAsio used to broadcast a format change on every endpoint
104+
// activation): wait before streaming, and reopen invalidated streams like
105+
// a well-behaved client would.
105106
double settleSeconds = 2.0;
106107
int maxReopens = 20;
107108
long long maxTransitionFrames = 4800;

0 commit comments

Comments
 (0)