From d3d200375722574d6c4e61371362c1e584cd0ed7 Mon Sep 17 00:00:00 2001 From: Mack Straight Date: Sat, 3 Oct 2026 10:44:10 -0400 Subject: [PATCH 1/2] test: check that SAR offers a stereo pair before first-time setup Adds SarTest no-interface and an issue-54-no-interface scenario: open SAR with a configuration that has no hardware interface, and fail unless it offers at least two inputs and two outputs. Co-Authored-By: Claude Opus 5.5 --- SarTest/SarTest.cpp | 68 +++++++++++++++++++++++++++++++++-- tools/test/Invoke-SarTest.ps1 | 2 ++ tools/test/README.md | 3 ++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/SarTest/SarTest.cpp b/SarTest/SarTest.cpp index c42c483..c8f784a 100644 --- a/SarTest/SarTest.cpp +++ b/SarTest/SarTest.cpp @@ -55,6 +55,10 @@ int usage() " Open SAR's control panel on a running host, press the hardware\n" " interface's Configure button and close it again; fail if a second\n" " clock instance was created (issue #133).\n" + " SarTest no-interface\n" + " Open SAR with no hardware interface configured, the way a DAW sees\n" + " it before first-time setup; fail unless it offers a stereo input\n" + " and output (issues #31, #54).\n" "\n" "Layout options: --endpoints N (playback/recording pairs, default 2)\n" " --channels C (per endpoint, default 2) --prefix \n" @@ -117,8 +121,10 @@ WasapiOptions wasapiOptions(const Args& args, const EndpointLayout& layout) } // Writes default.json for the layout unless --keep-config, backing up an -// existing configuration the first time. -bool prepareConfig(const Args& args, const EndpointLayout& layout) +// existing configuration the first time. An empty driverClsid leaves SAR +// without a hardware interface. +bool prepareConfig(const Args& args, const EndpointLayout& layout, + const std::wstring& driverClsid = SAR_TEST_CLOCK_CLSID_STR) { if (args.has(L"keep-config")) { return true; @@ -134,7 +140,7 @@ bool prepareConfig(const Args& args, const EndpointLayout& layout) } } - if (!writeDriverConfig(layout, SAR_TEST_CLOCK_CLSID_STR, + if (!writeDriverConfig(layout, driverClsid, args.getInt(L"wavert-min-frames", 0), path)) { return false; } @@ -651,6 +657,60 @@ int cmdControlPanel(const Args& args) return rc; } +// Issues #31 and #54: before a hardware interface is chosen, SAR offered one +// mono input and one mono output, and DAWs that need a stereo output (Live, +// Reason) refused to load it, so its control panel couldn't be reached. +int cmdNoInterface(const Args& args) +{ + EndpointLayout layout = EndpointLayout::fromArgs(args); + + layout.pairs = 0; + + if (!prepareConfig(args, layout, L"")) { + return 1; + } + + AsioHost host(hostOptions(args, layout)); + int rc = 0; + std::string failure; + + if (!host.load()) { + return 1; + } + + if (!host.open()) { + rc = 2; + failure = "open failed"; + } else if (!host.start() || !host.stop()) { + rc = 2; + failure = "start/stop failed"; + } else if (host.stats().physicalInputs < 2 || host.stats().physicalOutputs < 2) { + rc = 2; + failure = "no stereo pair without a hardware interface"; + } + + host.close(); + + if (rc != 0) { + logf("%s", failure.c_str()); + } + + JsonObject result; + + result.setString("command", "no-interface") + .setBool("passed", rc == 0) + .setInt("exitCode", rc) + .setRaw("host", host.toJson()); + + if (!failure.empty()) { + result.setString("failure", failure); + } + + writeResults(args, result.str()); + logf("%s", rc == 0 ? "PASSED" : "FAILED"); + return rc; +} + } // namespace int wmain(int argc, wchar_t **argv) @@ -686,6 +746,8 @@ int wmain(int argc, wchar_t **argv) rc = cmdRace(args); } else if (args.command == L"control-panel") { rc = cmdControlPanel(args); + } else if (args.command == L"no-interface") { + rc = cmdNoInterface(args); } else { rc = usage(); } diff --git a/tools/test/Invoke-SarTest.ps1 b/tools/test/Invoke-SarTest.ps1 index 559139e..7ba008d 100644 --- a/tools/test/Invoke-SarTest.ps1 +++ b/tools/test/Invoke-SarTest.ps1 @@ -149,6 +149,8 @@ if ($Scenarios -contains 'issues') { # Regression checks for specific GitHub issues, one short host each. $summary.scenarios += Invoke-Scenario 'issue-133-control-panel' ( @('control-panel', '--endpoints', 1) + $common) -Timeout 120 + $summary.scenarios += Invoke-Scenario 'issue-54-no-interface' ( + @('no-interface') + $common) -Timeout 120 } if ($Scenarios -contains 'matrix') { diff --git a/tools/test/README.md b/tools/test/README.md index fa74263..06374d1 100644 --- a/tools/test/README.md +++ b/tools/test/README.md @@ -86,6 +86,8 @@ The `issue-*` scenarios are regression checks for specific GitHub issues. `SarTest.exe control-panel` (#133) opens SAR's control panel on a running host, presses the hardware interface's Configure button and checks that the clock never had a second instance alive. It needs an interactive desktop. +`SarTest.exe no-interface` (#31, #54) opens SAR with no hardware interface +configured and checks that it still offers a stereo input and output. `Invoke-SarTest.ps1 -Scenarios` selects any of `issues`, `matrix`, `race` and `kill`. @@ -100,6 +102,7 @@ SarTest wasapi [--duration S] [--wait S] [--expect-invalidation] SarTest run [--iterations K] [--duration S] SarTest race [--cycles K] [--up S] [--down MS] [--openers T] [--hold MS] SarTest control-panel +SarTest no-interface ``` `install` creates the SAR software device node if needed, installs the From 6fa189ec821f11c89f120d9f17f69250d3578fd8 Mon Sep 17 00:00:00 2001 From: Mack Straight Date: Sat, 3 Oct 2026 10:44:25 -0400 Subject: [PATCH 2/2] SarAsio: offer a stereo pair when no hardware interface is selected Without a hardware interface (first run, or after the selected driver failed to load), SAR offered one mono input and one mono output. Live ("only supports audio devices that have at least one stereo output") and Reason refuse such a device, so SAR's control panel was out of reach until it was configured from another host. Offer two of each. Also free the previous fake buffers when createBuffers is called again instead of dropping the pointers. Fixes #31, fixes #54. Co-Authored-By: Claude Opus 5.5 --- SarAsio/wrapper.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/SarAsio/wrapper.cpp b/SarAsio/wrapper.cpp index 92d0853..76fd1f6 100644 --- a/SarAsio/wrapper.cpp +++ b/SarAsio/wrapper.cpp @@ -27,6 +27,9 @@ using namespace Sar; SarAsioWrapper *gActiveWrapper; static const char kNoInterfaceSelected[] = "No Interface Selected"; +// Channels per direction offered while no hardware interface is selected: +// a stereo pair, since some hosts (Live, Reason) refuse devices without one. +static const long kFakeChannelCount = 2; SarAsioWrapper::SarAsioWrapper() { @@ -109,7 +112,7 @@ AsioStatus SarAsioWrapper::getChannels(long *inputCount, long *outputCount) LOG(INFO) << "SarAsioWrapper::getChannels"; if (!_innerDriver) { - *inputCount = *outputCount = 1; + *inputCount = *outputCount = kFakeChannelCount; return AsioStatus::OK; } @@ -276,7 +279,7 @@ AsioStatus SarAsioWrapper::getSamplePosition(int64_t *pos, int64_t *timestamp) AsioStatus SarAsioWrapper::getChannelInfo(AsioChannelInfo *info) { if (!_innerDriver) { - if (info->index != 0) { + if (info->index < 0 || info->index >= kFakeChannelCount) { return AsioStatus::NotPresent; } @@ -335,18 +338,22 @@ AsioStatus SarAsioWrapper::createBuffers( LOG(INFO) << "SarAsioWrapper::createBuffers: no inner driver, trying " << "to create fake channels."; - // Allow allocating a single fake channel if hardware device is not yet - // selected. Supports one input and one output. - if (channelCount > 2) { + // Allow allocating the fake channels if a hardware device is not yet + // selected, so a host can load SAR to reach its control panel. + if (channelCount > 2 * kFakeChannelCount) { return AsioStatus::InvalidMode; } for (long i = 0; i < channelCount; ++i) { - if (infos[i].index != 0) { + if (infos[i].index < 0 || infos[i].index >= kFakeChannelCount) { return AsioStatus::InvalidMode; } } + for (auto buf : _fakeBuffers) { + free(buf); + } + _fakeBuffers.clear(); for (long i = 0; i < channelCount; ++i) {