mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-09-11 05:44:10 +00:00
fix(audio): centre mono input; limit duplex to same-endpoint devices
Two issues found while testing the USB-guitar-cable path on Windows: 1. Centre a mono input. SourceChain::processBlock fell into the pass-through branch for a 1-channel input, filling only min(inputChannels, outputChannels) = 1 output channel and zeroing the rest, so a mono USB guitar cable played out of the left speaker only. A single-channel input is now broadcast across every output channel. 2. Only attempt the combined (duplex) device when input and output are the SAME physical endpoint. Two different endpoints of the same backend (USB cable in + separate speakers out) are independent hardware clocks; routing them through one duplex device was unstable across the app lifecycle (no audio until an explicit Apply, then distortion / dropouts / silent-in-song on navigation). Different endpoints now use the split path, whose ring bridges the two clocks. Same-endpoint duplex (one interface for in and out) keeps the low-latency win. Low latency for the two-device case is a follow-up that needs the device-lifecycle work (startup restore + reconfigure on navigation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEoFeTPSnz4NpwwCG52hnu
This commit is contained in:
co-authored by
Claude Fable 5
parent
81b2fece10
commit
e0fa65add5
@@ -692,7 +692,16 @@ AudioEngine::DeviceConfigResult AudioEngine::setAudioDevices(const DeviceConfig&
|
|||||||
// (Extra input devices were closed by the stopAudio() above with their intent
|
// (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.)
|
// 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();
|
teardownSplitMode();
|
||||||
|
|
||||||
@@ -711,18 +720,14 @@ AudioEngine::DeviceConfigResult AudioEngine::setAudioDevices(const DeviceConfig&
|
|||||||
res.ok = true;
|
res.ok = true;
|
||||||
res.duplex = 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.error = err;
|
||||||
res.duplex = true;
|
res.duplex = true;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
fprintf(stderr,
|
|
||||||
"[AudioEngine] Same-type combined setup failed (%s); falling back to split mode\n",
|
|
||||||
err.toRawUTF8());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.ok)
|
if (!res.ok)
|
||||||
|
|||||||
@@ -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
|
// range — the broadcast branches fill all of them, the pass-through branch
|
||||||
// only fills the overlap.
|
// only fills the overlap.
|
||||||
int filledOutputChannels = 0;
|
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).
|
// Explicit single-channel pick (e.g. dry from a Valeton GP-5 left
|
||||||
// Broadcast the selected input across all output channels.
|
// 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 outCh = 0; outCh < effectiveOutputChannels; ++outCh)
|
||||||
for (int i = 0; i < numSamples; ++i)
|
for (int i = 0; i < numSamples; ++i)
|
||||||
buffer.setSample(outCh, i, inputData[selectedCh][i] * inGain);
|
buffer.setSample(outCh, i, inputData[selectedCh][i] * inGain);
|
||||||
filledOutputChannels = effectiveOutputChannels;
|
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)
|
else if (selectedCh < 0 && numInputChannels > 1)
|
||||||
{
|
{
|
||||||
// Default pair mono mix: average the first two input channels and
|
// 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
|
else
|
||||||
{
|
{
|
||||||
// Pass-through: single-input device, or stereo in/out with no explicit
|
// Pass-through: genuine multi-channel in/out with an out-of-range
|
||||||
// channel selection and no need to mix.
|
// 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);
|
const int passThroughChannels = juce::jmin(numInputChannels, effectiveOutputChannels);
|
||||||
for (int ch = 0; ch < passThroughChannels; ++ch)
|
for (int ch = 0; ch < passThroughChannels; ++ch)
|
||||||
for (int i = 0; i < numSamples; ++i)
|
for (int i = 0; i < numSamples; ++i)
|
||||||
|
|||||||
Reference in New Issue
Block a user