diff --git a/src/audio/AudioEngine.cpp b/src/audio/AudioEngine.cpp index 024fce6..a164ede 100644 --- a/src/audio/AudioEngine.cpp +++ b/src/audio/AudioEngine.cpp @@ -692,7 +692,16 @@ AudioEngine::DeviceConfigResult AudioEngine::setAudioDevices(const DeviceConfig& // (Extra input devices were closed by the stopAudio() above with their intent // kept; startAudio() below re-opens them at the new config — split mode only.) - if (sameBackendType) + // Only attempt the low-latency COMBINED (duplex) device when input and output + // are the SAME physical endpoint — a true single-clock duplex device. Two + // DIFFERENT endpoints of the same backend (e.g. a USB guitar cable in + separate + // speakers out) are independent hardware clocks; forcing them through one duplex + // device proved unstable across the app lifecycle (no audio until an explicit + // Apply, then distortion / dropouts / silent-in-song on navigation). Those route + // through the split path, whose ring buffer bridges the two clocks. Cross-backend + // pairs split too. (Low-latency for the two-device case is a separate follow-up — + // it needs the device-lifecycle work: startup restore + reconfigure-on-nav.) + if (sameEndpointIntent) { teardownSplitMode(); @@ -711,18 +720,14 @@ AudioEngine::DeviceConfigResult AudioEngine::setAudioDevices(const DeviceConfig& res.ok = true; res.duplex = true; } - else if (sameEndpointIntent) + else { + // A same-device config that can't open combined is a real error, not a + // reason to silently fall to split (which would misrepresent the intent). res.error = err; res.duplex = true; return res; } - else - { - fprintf(stderr, - "[AudioEngine] Same-type combined setup failed (%s); falling back to split mode\n", - err.toRawUTF8()); - } } if (!res.ok) diff --git a/src/audio/SourceChain.cpp b/src/audio/SourceChain.cpp index c07ab4a..eb4f2e6 100644 --- a/src/audio/SourceChain.cpp +++ b/src/audio/SourceChain.cpp @@ -61,15 +61,30 @@ void SourceChain::processBlock(const float* const* inputData, int numInputChanne // range — the broadcast branches fill all of them, the pass-through branch // only fills the overlap. int filledOutputChannels = 0; - if (numInputChannels >= 2 && selectedCh >= 0 && selectedCh < numInputChannels) + if (selectedCh >= 0 && selectedCh < numInputChannels) { - // Single-channel mode (e.g. dry from Valeton GP-5 left channel). - // Broadcast the selected input across all output channels. + // Explicit single-channel pick (e.g. dry from a Valeton GP-5 left + // channel, or a USB guitar cable whose guitar is on a known channel). + // Broadcast the selected input across all output channels. Works for a + // mono device too (selectedCh 0 on a 1-channel input). for (int outCh = 0; outCh < effectiveOutputChannels; ++outCh) for (int i = 0; i < numSamples; ++i) buffer.setSample(outCh, i, inputData[selectedCh][i] * inGain); filledOutputChannels = effectiveOutputChannels; } + else if (numInputChannels == 1) + { + // Mono input device — the common USB guitar cable enumerates as a single + // capture channel. Broadcast that one channel across EVERY output channel + // so the guitar is centred. The old pass-through branch below only filled + // min(numInputChannels, outputChannels) = 1 channel and zeroed the rest, + // which put the guitar in the left speaker only on a stereo duplex device + // (cable-in + speakers-out). This restores mono-in / centred-out. + for (int outCh = 0; outCh < effectiveOutputChannels; ++outCh) + for (int i = 0; i < numSamples; ++i) + buffer.setSample(outCh, i, inputData[0][i] * inGain); + filledOutputChannels = effectiveOutputChannels; + } else if (selectedCh < 0 && numInputChannels > 1) { // Default pair mono mix: average the first two input channels and @@ -93,8 +108,9 @@ void SourceChain::processBlock(const float* const* inputData, int numInputChanne } else { - // Pass-through: single-input device, or stereo in/out with no explicit - // channel selection and no need to mix. + // Pass-through: genuine multi-channel in/out with an out-of-range + // explicit selection, or other configs that map channels 1:1. (The mono + // and default-pair cases are handled above and always broadcast.) const int passThroughChannels = juce::jmin(numInputChannels, effectiveOutputChannels); for (int ch = 0; ch < passThroughChannels; ++ch) for (int i = 0; i < numSamples; ++i)