feat(audio): stereo mic capture on Windows & macOS desktop clients
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled

Both desktop mics were hard-mono: the core defaults capture_channels=1 and
neither client ever called vc_set_capture_channels (only iOS did). Add a
persisted "Stereo microphone" toggle to each client's Audio settings, applied
when the mic stream starts and live via vc_set_capture_channels + vc_audio_restart.
Expose both ABI calls in the Windows interop; the macOS wrapper already had them.

Core fix: encode_and_send_frame now folds a stereo mic frame to mono on a mono
channel - previously the channels==2 branch encoded interleaved L/R directly even
on a mono channel, feeding a mono opus_encode 2x its samples (wrong pitch/garbage).
Real stereo still only reaches the wire on a stereo channel; on a mono channel the
mic is cleanly downmixed.

Test: test_stereo_mic_mono_channel. ctest --preset dev green (28/28). Docs: voice.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 20:48:26 +02:00
parent b14cf2a4e8
commit f72219ddf3
11 changed files with 210 additions and 20 deletions

View File

@@ -28,6 +28,7 @@ public sealed class AudioSettingsForm : Form
private readonly int _origVadSlider;
private readonly int _origMicGain;
private readonly bool _origMicNoiseReduction;
private readonly bool _origStereoMic;
private readonly Keys _origPttKey;
private readonly bool _origAuxEnabled;
private readonly string? _origAuxDeviceId;
@@ -44,6 +45,7 @@ public sealed class AudioSettingsForm : Form
private readonly TrackBar _trkVad;
private readonly TrackBar _trkGain;
private readonly CheckBox _chkNoiseReduction;
private readonly CheckBox _chkStereoMic;
private readonly CheckBox _chkAux;
private readonly Label _lblAuxDevice;
private readonly ComboBox _cboAuxDevice;
@@ -70,6 +72,7 @@ public sealed class AudioSettingsForm : Form
_origVadSlider = settings.VadThresholdSlider;
_origMicGain = settings.MicGain;
_origMicNoiseReduction = settings.MicNoiseReduction;
_origStereoMic = settings.StereoMic;
_origPttKey = _pttKey;
_origAuxEnabled = settings.AuxEnabled;
_origAuxDeviceId = settings.AuxDeviceId;
@@ -81,7 +84,7 @@ public sealed class AudioSettingsForm : Form
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(420, 583);
ClientSize = new Size(420, 611);
// ── Device row ────────────────────────────────────────────────────────
var lblDevice = new Label
@@ -217,6 +220,23 @@ public sealed class AudioSettingsForm : Form
};
_chkNoiseReduction.CheckedChanged += ChkNoiseReduction_CheckedChanged;
// ── Stereo microphone ─────────────────────────────────────────────────
// Opens the mic capture device in stereo (interleaved L/R) instead of mono. Real stereo
// only reaches the wire on a stereo channel; the core folds a stereo mic to mono on a mono
// channel. Toggling while connected restarts the capture device (vc_audio_restart) so the
// new channel count takes effect immediately.
_chkStereoMic = new CheckBox
{
Text = "&Stereo microphone",
Location = new Point(12, 364),
AutoSize = true,
Checked = settings.StereoMic,
AccessibleName = "Stereo microphone",
AccessibleDescription =
"Capture your microphone in stereo. Only transmitted in stereo on a stereo channel.",
};
_chkStereoMic.CheckedChanged += ChkStereoMic_CheckedChanged;
// ── Aux input stream ──────────────────────────────────────────────────
// A second outgoing stream from another hardware input device (e.g. line-in / aux),
// captured client-side and fed to the core. Device + volume only — aux is always-on
@@ -224,7 +244,7 @@ public sealed class AudioSettingsForm : Form
_chkAux = new CheckBox
{
Text = "&Aux stream (second input device)",
Location = new Point(12, 376),
Location = new Point(12, 404),
AutoSize = true,
Checked = settings.AuxEnabled,
AccessibleName = "Enable aux input stream",
@@ -236,12 +256,12 @@ public sealed class AudioSettingsForm : Form
_lblAuxDevice = new Label
{
Text = "Aux d&evice:",
Location = new Point(12, 406),
Location = new Point(12, 434),
AutoSize = true,
};
_cboAuxDevice = new ComboBox
{
Location = new Point(12, 426),
Location = new Point(12, 454),
Width = 300,
DropDownStyle = ComboBoxStyle.DropDownList,
DisplayMember = "Name",
@@ -254,7 +274,7 @@ public sealed class AudioSettingsForm : Form
_btnAuxRefresh = new Button
{
Text = "Re&fresh",
Location = new Point(320, 424),
Location = new Point(320, 452),
Size = new Size(80, 26),
};
_btnAuxRefresh.Click += (_, _) => LoadAuxDevices();
@@ -262,12 +282,12 @@ public sealed class AudioSettingsForm : Form
_lblAuxGain = new Label
{
Text = "Aux vo&lume:",
Location = new Point(12, 462),
Location = new Point(12, 490),
AutoSize = true,
};
_trkAuxGain = new TrackBar
{
Location = new Point(12, 482),
Location = new Point(12, 510),
Size = new Size(200, 45),
Minimum = 0,
Maximum = 300,
@@ -286,14 +306,14 @@ public sealed class AudioSettingsForm : Form
{
Text = "&OK",
DialogResult = DialogResult.OK,
Location = new Point(228, 544),
Location = new Point(228, 572),
Size = new Size(80, 27),
};
var btnCancel = new Button
{
Text = "&Cancel",
DialogResult = DialogResult.Cancel,
Location = new Point(316, 544),
Location = new Point(316, 572),
Size = new Size(80, 27),
};
@@ -310,7 +330,7 @@ public sealed class AudioSettingsForm : Form
lblDevice, _cboDevice, _btnRefresh,
lblMode, _radioVad, _lblSensitivity, _trkVad,
_radioPtt, _lblPttKey, _btnChangePtt, _radioAlwaysOn,
lblGain, _trkGain, _chkNoiseReduction,
lblGain, _trkGain, _chkNoiseReduction, _chkStereoMic,
_chkAux, _lblAuxDevice, _cboAuxDevice, _btnAuxRefresh, _lblAuxGain, _trkAuxGain,
btnOk, btnCancel,
]);
@@ -455,6 +475,17 @@ public sealed class AudioSettingsForm : Form
_client.SetInputNoiseReduction(_chkNoiseReduction.Checked);
}
private void ChkStereoMic_CheckedChanged(object? sender, EventArgs e)
{
_settings.StereoMic = _chkStereoMic.Checked;
// Channel count only takes effect when the capture device (re)starts, so restart it live.
if (_micStreamId != 0)
{
_client.SetCaptureChannels(_micStreamId, _chkStereoMic.Checked ? 2u : 1u);
_client.AudioRestart();
}
}
private void BtnChangePtt_Click(object? sender, EventArgs e)
{
using var dlg = new PttKeyCaptureDialog(_pttKey);
@@ -476,6 +507,7 @@ public sealed class AudioSettingsForm : Form
_settings.VadThresholdSlider = _origVadSlider;
_settings.MicGain = _origMicGain;
_settings.MicNoiseReduction = _origMicNoiseReduction;
_settings.StereoMic = _origStereoMic;
_settings.PttKey = (int)_origPttKey;
if (_micStreamId != 0)
@@ -486,6 +518,12 @@ public sealed class AudioSettingsForm : Form
_client.SetVadThreshold(0.1f * (1f - (_origVadSlider - 1f) / 99f));
_client.SetInputGain(_origMicGain / 100f);
_client.SetInputNoiseReduction(_origMicNoiseReduction);
// Restore capture channel count; restart the device only if it actually changed.
if (_origStereoMic != _chkStereoMic.Checked)
{
_client.SetCaptureChannels(_micStreamId, _origStereoMic ? 2u : 1u);
_client.AudioRestart();
}
}
// Aux: restore originals to settings and re-apply live (order: device + gain first so a

View File

@@ -646,6 +646,7 @@ public partial class MainForm : Form
var mode = (VcInputMode)_voiceSettings.InputMode;
if (_voiceSettings.InputDeviceId is string devId)
_client.SetInputDevice(streamId, devId);
_client.SetCaptureChannels(streamId, _voiceSettings.StereoMic ? 2u : 1u);
_client.SetInputMode(mode);
if (mode == VcInputMode.VoiceActivation)
_client.SetVadThreshold(VadThresholdFromSettings());