audio: fix the ASIO backend listing no drivers and breaking on engine restart - #920
Open
ogauthiersat wants to merge 6 commits into
Open
audio: fix the ASIO backend listing no drivers and breaking on engine restart#920ogauthiersat wants to merge 6 commits into
ogauthiersat wants to merge 6 commits into
Conversation
asio_sdk was compiled with UNICODE/_UNICODE defined, but the SDK's host/pc/asiolist.cpp is an ANSI-only translation unit: it reads registry key names with the TCHAR-generic RegEnumKey/RegOpenKeyEx into char buffers, then parses them with explicit -A calls (CharLowerBuffA, strlen, strcmp). With UNICODE defined those resolve to the -W variants and write UTF-16 into the char buffers, so strlen() stops at the first embedded NUL and findDrvPath()'s strcmp() of the CLSID can never match. newDrvStruct() therefore rejects every driver and asioGetNumDev() returns 0, leaving the device list empty with no diagnostic. The size arguments are also character counts, so the -W calls could overflow the buffers. Verified against a machine with 3 installed ASIO drivers: the SDK enumeration returns 0 with UNICODE defined and 3 without it. Also add ossia/audio/asio_diagnostics.hpp, which reads HKLM\SOFTWARE\ASIO directly (explicit -A calls only, so it stays correct whatever UNICODE is) and reports when the SDK returns fewer drivers than are actually installed and loadable. enumerate_drivers() now traces each driver under OSSIA_ASIO_DEBUG=1 and always reports such a mismatch, so this class of failure is no longer silent. Diagnostics go to stderr rather than ossia::logger(): enumeration runs while the audio factories are constructed, before the log sinks are usable. Co-Authored-By: Claude Opus 5.0 <noreply@anthropic.com>
…anel Both problems come from the same gap: nothing tracked *which* driver the active engine owns, and the driver was only released by the destructor. 1. Restarting the engine failed with "The desired audio settings could not be applied". Hosts routinely defer destruction of a stopped engine: score moves it into a pending list and reaps it from a timer once the audio thread has acknowledged the stop. But ASIO allows a single loaded driver per process, so constructing the replacement while the old engine was still alive tripped the "only one ASIO engine can be active at a time" guard, and the engine never came back. Had that succeeded, the old engine's late destructor would then have called ASIOExit() and removeCurrentDriver() on the driver the *new* engine just claimed. Release in stop() instead, and make cleanup() idempotent and guarded on still owning the driver, so a late destructor cannot tear down another engine's driver. stop() now also sets stop_received: ASIOStop() guarantees no further bufferSwitch callback, so the audio thread can never acknowledge the stop itself and the host would otherwise wait forever and never reap the engine. 2. open_control_panel() always showed the running driver's panel. With an engine active it called ASIOControlPanel() unconditionally, which talks to whichever driver is loaded -- ignoring the driver that was actually asked for. Compare against the active driver's name and report other_driver_active instead of silently showing the wrong panel; we cannot load the requested one, because AsioDrivers::loadDriver() calls removeCurrentDriver() first and would release the streaming driver mid-flight. When no engine is running, the temporarily loaded driver is now released again (ASIOExit + removeCurrentDriver) rather than left stranded, so asking for a different driver afterwards works. Record the driver name to make both possible, and expose active_driver() so hosts can avoid disturbing a streaming driver. Co-Authored-By: Claude Opus 5.0 <noreply@anthropic.com>
ogauthiersat
temporarily deployed
to
Apple Certificate
August 3, 2026 14:22 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 3, 2026 14:22 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 3, 2026 14:22 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 3, 2026 14:22 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 3, 2026 14:22 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 3, 2026 14:22 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 3, 2026 14:22 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 3, 2026 14:22 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 3, 2026 14:22 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 3, 2026 14:22 — with
GitHub Actions
Inactive
ogauthiersat
had a problem deploying
to
Apple Certificate
August 3, 2026 14:22 — with
GitHub Actions
Failure
jcelerier
reviewed
Aug 4, 2026
jcelerier
reviewed
Aug 4, 2026
jcelerier
reviewed
Aug 4, 2026
jcelerier
reviewed
Aug 4, 2026
jcelerier
reviewed
Aug 4, 2026
jcelerier
reviewed
Aug 4, 2026
jcelerier
reviewed
Aug 4, 2026
jcelerier
reviewed
Aug 4, 2026
jcelerier
reviewed
Aug 4, 2026
jcelerier
requested changes
Aug 4, 2026
The reasoning belongs in the pull request description rather than in the source. Comment-only change, no code touched. Kept short notes where the code alone would mislead: that asio_sdk must not be built with UNICODE, that cleanup() is idempotent and ownership guarded, and that the driver is released on stop() rather than on destruction. Co-Authored-By: Claude Opus 5.0 <noreply@anthropic.com>
Removes asio_diagnostics.hpp along with its registry cross-check and the OSSIA_ASIO_DEBUG flag. Driver discovery and failures now go through ossia::logger() formatted with fmt::format, so they reach the usual sinks at the usual levels instead of being gated behind an environment variable. Also drops the <iostream> include, unused now that nothing writes to std::cerr. Co-Authored-By: Claude Opus 5.0 <noreply@anthropic.com>
128 was already the right size -- it is what ASIODRVSTRUCT::drvname holds, so asioGetDriverName() can never return more -- but naming the SDK's constant ties the two together. A smaller buffer would not overflow, since asioGetDriverName() truncates, but the truncated name would no longer match in loadAsioDriver() and the driver would silently fail to load. Co-Authored-By: Claude Opus 5.0 <noreply@anthropic.com>
ogauthiersat
temporarily deployed
to
Apple Certificate
August 4, 2026 16:15 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 4, 2026 16:15 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 4, 2026 16:15 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 4, 2026 16:15 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 4, 2026 16:15 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 4, 2026 16:15 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 4, 2026 16:15 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 4, 2026 16:15 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 4, 2026 16:15 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 4, 2026 16:15 — with
GitHub Actions
Inactive
ogauthiersat
had a problem deploying
to
Apple Certificate
August 4, 2026 16:15 — with
GitHub Actions
Failure
ogauthiersat
added a commit
to ossia/score
that referenced
this pull request
Aug 4, 2026
Picks up the logger migration, the comment trim and the MAXDRVNAMELEN change from ossia/libossia#920, so this builds against exactly the libossia code under review rather than an earlier commit on that branch. Still needs re-pointing at the merged commit on libossia master before this can be merged. Co-Authored-By: Claude Opus 5.0 <noreply@anthropic.com>
The submodule pointed at jcelerier/asio-2, which is upstream 2.3.4 plus a single patch doing two things. Neither is needed any more. The patch added windows.h and unknwn.h to common/iasiodrv.h, which declares IASIO as deriving from IUnknown but includes neither. The SDK's own translation units are unaffected, since asiolist.cpp includes plain windows.h first and so gets ole2.h; only asio_protocol.hpp tripped over it, because it defines WIN32_LEAN_AND_MEAN. Including the two headers there fixes it without touching the SDK. The patch's #pragma once turned out to be unnecessary: iasiodrv.h is included once per translation unit everywhere. The patch also rewrote seven registry calls in host/pc/asiolist.cpp to the -A variants, but left RegEnumKey and RegOpenKeyEx generic. That mix is what made a UNICODE build fail so obscurely: the generic calls resolved to -W and wrote UTF-16 into the char buffers the -A calls then parsed. Upstream is uniformly generic, so with UNICODE undefined every call resolves to -A and the file is self-consistent. Verified by dumping the bytes asioGetDriverName returns from both versions against the same registry: identical. Co-Authored-By: Claude Opus 5.0 <noreply@anthropic.com>
ogauthiersat
temporarily deployed
to
Apple Certificate
August 5, 2026 20:40 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 5, 2026 20:40 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 5, 2026 20:40 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 5, 2026 20:40 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 5, 2026 20:40 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 5, 2026 20:40 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 5, 2026 20:40 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 5, 2026 20:40 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 5, 2026 20:40 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 5, 2026 20:40 — with
GitHub Actions
Inactive
ogauthiersat
temporarily deployed
to
Apple Certificate
August 5, 2026 20:40 — with
GitHub Actions
Inactive
ogauthiersat
added a commit
to ossia/score
that referenced
this pull request
Aug 5, 2026
Picks up the switch from the jcelerier/asio-2 fork to upstream audiosdk/asio, along with the logger migration and the MAXDRVNAMELEN change, from ossia/libossia#920. Note this moves the nested 3rdparty/asio submodule URL as well, so a checkout needs `git submodule sync --recursive` rather than just `update`. Still needs re-pointing at the merged commit on libossia master before this can be merged. Co-Authored-By: Claude Opus 5.0 <noreply@anthropic.com>
Contributor
Author
|
@jcelerier I worked around the patch you did so upstream now work. The -A variant of the registry calls are not needed anymore as the translation unit is now configured to build as ANSI translation unit. The missing includes have been moved to our client code. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The ASIO backend added in a750490 could not work in practice. Two independent
bugs, both silent. Base is
master; no functional change to any other backend.Net diff is two files —
cmake/deps/asio-sdk.cmakeandsrc/ossia/audio/asio_protocol.hpp. Reviewing the final diff is easier thangoing commit by commit: an intermediate commit added a diagnostics header that a
later one removed again, so it cancels out.
1.
asioGetNumDev()always returned 0cmake/deps/asio-sdk.cmakecompiled the SDK withUNICODE/_UNICODEdefined,but
host/pc/asiolist.cppis an ANSI-only translation unit. It reads registrykey names with the TCHAR-generic
RegEnumKey/RegOpenKeyExintocharbuffers, then parses them with explicit
-Acalls (CharLowerBuffA,strlen,strcmp).With
UNICODEdefined those resolve to the-Wvariants and write UTF-16 intothe
charbuffers, sostrlen()stops at the first embedded NUL andfindDrvPath()'sstrcmp()of the CLSID can never match.newDrvStruct()therefore rejects every driver and
asioGetNumDev()returns 0 — the device listcomes up empty with no diagnostic. The size arguments are character counts too,
so the
-Wcalls can overflowkeyname[128]/databuf[512].Measured on a machine with three installed ASIO drivers, compiling the vendored
asiolist.cppboth ways against the live registry:asioGetNumDev()-DUNICODE=1 -D_UNICODE=1Nothing in the three compiled SDK sources needs
UNICODE; the TCHAR-genericcalls are confined to
asiolist.cppand are all meant to be ANSI. Removed, witha short comment so it does not come back.
2. The driver was released too late, so restarting the engine failed
Hosts routinely defer destruction of a stopped engine — ossia score parks it in a
list and reaps it from a timer once the audio thread acknowledges the stop. But
ASIO allows a single loaded driver per process and the driver was only released
by
~asio_engine(), so:the "only one ASIO engine can be active at a time" guard, and the engine never
came back;
ASIOExit()andremoveCurrentDriver()on the driver the new engine hadjust claimed.
asio_engine::stop()now releases the driver, andcleanup()is idempotent andguarded on still owning it, so a late destructor cannot tear down another
engine's driver.
stop()also setsstop_received:ASIOStop()guarantees nofurther
bufferSwitchcallback, so the audio thread can never acknowledge thestop itself and a host would otherwise wait forever and never reap the engine.
open_control_panel()had a related bug: with an engine active it calledASIOControlPanel()unconditionally, which talks to whichever driver isloaded — not the one asked for — so it always showed the running driver's
panel. It now compares against the active driver and returns
other_driver_activerather than silently showing the wrong panel. It cannotjust load the requested driver instead, because
AsioDrivers::loadDriver()calls
removeCurrentDriver()first (asiodrivers.cpp:161) and would release thestreaming driver mid-flight. With no engine running, the temporarily loaded
driver is now released again instead of left stranded, so asking for a different
driver afterwards works.
Logging
Driver discovery and every failure path now go through
ossia::logger(),formatted with
fmt::format, atinfofor discovery andwarnfor a driverthat will not initialize or load. Previously the SDK dropped unresolvable
drivers without a word, which is what made bug 1 invisible.
API changes
asio_engine::active_driver()— name of the driver the running engine holds,empty if none. Lets a host avoid disturbing a streaming driver, since probing
drivers requires
loadDriver(), which releases the loaded one.asio_engine::control_panel_result—open_control_panel()now reportsok/other_driver_active/load_failed/init_failedinstead ofreturning
void, so a host can explain the failure. This is the onlybreaking signature change.
Testing
On Windows with three installed ASIO drivers (ASIO4ALL v2, NI Audio 8 DJ, NI
Traktor Kontrol S4 MK2):
driving a real driver, replicating a host's park-then-restart sequence.
Pre-fix:
replacement threw: ASIO error: only one ASIO engine can be active at a time. Post-fix: replacement constructs and runs, the late destructor leavesit alone, three consecutive cycles succeed.
playback, repeated settings re-apply, control panel targeting, and hot-plug.
Unrelated finding, not addressed here
asiolist.cpp'snewDrvStruct()copies the driver description intoASIODRVSTRUCT::drvnamewith an unboundedstrcpyfrom a 256-byte buffer intoa 128-byte field, so a driver whose
descriptionregistry value exceeds 127characters corrupts the adjacent
asiodrvandnextmembers. It lives in the3rdparty/asiosubmodule rather than here, and is latent — no driver I havetriggers it.
Vendored SDK: upstream instead of a fork
3rdparty/asiopointed atjcelerier/asio-2, which is upstreamaudiosdk/asio2.3.4 plus one patch. This switches to upstream and drops the need for it.
The patch added
windows.h/unknwn.htocommon/iasiodrv.h, which declaresIASIO : public IUnknownbut includes neither. The SDK's own translation unitsnever noticed, since
asiolist.cppincludes plainwindows.hand so getsole2.h; onlyasio_protocol.hpptripped over it, because it definesWIN32_LEAN_AND_MEAN. Including the two headers there fixes it with no change tothe SDK. The patch's
#pragma onceis not needed —iasiodrv.his included onceper translation unit in every path here.
The patch also rewrote seven registry calls in
host/pc/asiolist.cppto the-Avariants but left
RegEnumKeyandRegOpenKeyExgeneric. That mix is exactlywhat made a
UNICODEbuild fail so obscurely: the generic calls resolved to-Wand wrote UTF-16 into the
charbuffers the-Aones then parsed. Upstream isuniformly generic, so with
UNICODEundefined every call resolves to-Aand thefile is self-consistent — strictly better than the fork.
Verified by dumping the bytes
asioGetDriverName()returns from both versionsagainst the same registry: byte-identical, including a driver whose registry name
contains non-ASCII characters.
Follow-up
A score-side PR depends on this one; it bumps the submodule and uses
active_driver()andcontrol_panel_result.Disclosure
The implementation in this PR was produced with an AI coding
agent (Anthropic's Claude Opus 5) using omp.
I directed the work, reviewed every change, and ran the verification
described above; I take responsibility for its correctness and licensing.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com