From a558873c432537e8886ff427078252940419c7bc Mon Sep 17 00:00:00 2001 From: Viktor Olausson Date: Fri, 17 Jul 2026 15:18:33 +0200 Subject: [PATCH] fix(audio): reuse live ASIO device capabilities --- src/audio/engine/DeviceSetup.cpp | 37 ++++++++++++++++++++++++++++ tests/device-setup-lifecycle.test.js | 21 ++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/audio/engine/DeviceSetup.cpp b/src/audio/engine/DeviceSetup.cpp index 71912ed..b806069 100644 --- a/src/audio/engine/DeviceSetup.cpp +++ b/src/audio/engine/DeviceSetup.cpp @@ -131,6 +131,43 @@ DeviceOptions DeviceSetup::probeDual(const juce::String& inputTypeName, if (isDuplex) { + // ASIO drivers commonly allow only one live device object. If + // this exact duplex endpoint is already open, constructing a + // second object can succeed but report zero channel names (the + // settings UI then incorrectly falls back to Inputs 1-2). Reuse + // the live device's immutable capability lists instead. The + // endpoint checks keep a newly selected device on the normal + // temporary-probe path. + auto* liveDevice = inMgr.getCurrentAudioDevice(); + auto* liveType = inMgr.getCurrentDeviceTypeObject(); + const auto liveSetup = inMgr.getAudioDeviceSetup(); + const bool requestedEndpointIsLive = + liveDevice != nullptr + && liveDevice->isOpen() + && liveType != nullptr + && liveType->getTypeName() == options.inputType + && liveSetup.inputDeviceName == probeInputName + && liveSetup.outputDeviceName == probeOutputName; + + if (requestedEndpointIsLive) + { + options.inputChannels = liveDevice->getInputChannelNames(); + options.outputChannels = liveDevice->getOutputChannelNames(); + for (auto rate : liveDevice->getAvailableSampleRates()) + options.sampleRates.addIfNotAlreadyThere(rate); + for (auto size : liveDevice->getAvailableBufferSizes()) + options.bufferSizes.addIfNotAlreadyThere(size); + + fprintf(stderr, "[AudioEngine] Probed live device options: " + "inType='%s' outType='%s' in='%s' out='%s' " + "inputs=%d outputs=%d rates=%d buffers=%d compatible=1\n", + options.inputType.toRawUTF8(), options.outputType.toRawUTF8(), + options.input.toRawUTF8(), options.output.toRawUTF8(), + options.inputChannels.size(), options.outputChannels.size(), + options.sampleRates.size(), options.bufferSizes.size()); + return options; + } + std::unique_ptr dev( inputType->createDevice(probeOutputName, probeInputName)); if (dev) diff --git a/tests/device-setup-lifecycle.test.js b/tests/device-setup-lifecycle.test.js index 8e54d4f..6a3eb79 100644 --- a/tests/device-setup-lifecycle.test.js +++ b/tests/device-setup-lifecycle.test.js @@ -20,6 +20,27 @@ const end = source.indexOf('DeviceConfigResult DeviceSetup::applySplit', start); // a bad slice would otherwise make every assertion below fail confusingly. assert.ok(start >= 0 && end > start, 'could not locate applyDuplex in DeviceSetup.cpp'); const applyDuplex = source.slice(start, end); +const probeStart = source.indexOf('DeviceOptions DeviceSetup::probeDual'); +const probeEnd = source.indexOf('juce::String DeviceSetup::applyDuplex', probeStart); +const probeDual = source.slice(probeStart, probeEnd); + +test('duplex probe reuses an exact live endpoint before constructing a competing device', () => { + assert.ok(probeStart >= 0 && probeEnd > probeStart, 'could not locate probeDual'); + + const inspectLive = probeDual.indexOf('inMgr.getCurrentAudioDevice()'); + const exactEndpoint = probeDual.indexOf('const bool requestedEndpointIsLive'); + const requireOpen = probeDual.indexOf('liveDevice->isOpen()', exactEndpoint); + const reuseChannels = probeDual.indexOf( + 'options.inputChannels = liveDevice->getInputChannelNames()'); + const temporaryProbe = probeDual.indexOf( + 'inputType->createDevice(probeOutputName, probeInputName)'); + + assert.ok(inspectLive >= 0 && exactEndpoint > inspectLive, + 'the probe must inspect and identity-check the live endpoint'); + assert.ok(requireOpen > exactEndpoint && reuseChannels > requireOpen + && temporaryProbe > reuseChannels, + 'matching live capabilities must be returned before a temporary device is created'); +}); test('duplex setup closes Windows ASIO before constructing its channel probe', () => { assert.match(