feat(clients): add aux outgoing stream (mic + second input device) on Windows + macOS
Lets a user transmit a second hardware input device (e.g. line-in / aux) alongside the mic, with its own device picker and volume, from Audio Settings. No core/ABI/proto changes: the aux is a VC_STREAM_AUX_DEVICE stream started with external_feed=1 and fed via vc_stream_feed_pcm (the same external-feed pipeline screen-audio uses). Per-kind local_streams_ already allows mic + screen + one aux to coexist; volume is a client-side gain multiply (the core's vc_set_input_gain is mic-only/global). Aux is always-on (core never gates AUX_DEVICE on VAD/PTT) and is tied to the voice session. Windows: new Audio/InputDeviceCapture.cs (WASAPI shared-mode capture from a real input endpoint + capture-endpoint enumeration); aux section in AudioSettingsForm.cs; lifecycle in MainForm.cs; persistence in VoiceSettings.cs. macOS: new Audio/InputDeviceCapture.swift (AVAudioEngine input-node tap pinned to the chosen Core Audio device + device enumeration by stable UID); aux section in SettingsWindowController.swift; lifecycle + UserDefaults persistence in MainWindowController.swift; file registered in project.pbxproj. Windows verified (C# solution builds clean; aux confirmed working). macOS build + E2E pending a Mac. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,8 @@ public partial class MainForm : Form
|
||||
private uint _micStreamId; // 0 = not started
|
||||
private uint _screenStreamId; // 0 = not sharing screen audio
|
||||
private ProcessAudioMixer? _screenMixer; // non-null only in per-app capture mode
|
||||
private uint _auxStreamId; // 0 = aux (second input device) stream not active
|
||||
private InputDeviceCapture? _auxCapture; // client-side capture feeding the aux stream
|
||||
private Keys _pttKey = Keys.F8;
|
||||
private bool _pttEngaged; // guards the PTT cue against key-repeat
|
||||
private bool _serverMuted;
|
||||
@@ -169,7 +171,16 @@ public partial class MainForm : Form
|
||||
var miAudio = new ToolStripMenuItem("&Audio...");
|
||||
miAudio.Click += (_, _) =>
|
||||
{
|
||||
using var dlg = new AudioSettingsForm(_client, _voiceSettings, _micStreamId);
|
||||
using var dlg = new AudioSettingsForm(_client, _voiceSettings, _micStreamId,
|
||||
applyAuxEnabled: on =>
|
||||
{
|
||||
if (_micStreamId == 0) return; // not in voice — applied on next Join Voice
|
||||
if (on) StartAuxStream(); else StopAuxStream();
|
||||
},
|
||||
applyAuxDevice: _ =>
|
||||
{
|
||||
if (_auxStreamId != 0) RestartAuxCapture(); // settings.AuxDeviceId already updated
|
||||
});
|
||||
dlg.ShowDialog(this);
|
||||
_pttKey = (Keys)_voiceSettings.PttKey;
|
||||
};
|
||||
@@ -511,6 +522,7 @@ public partial class MainForm : Form
|
||||
_micStreamId = 0;
|
||||
_screenStreamId = 0;
|
||||
_screenMixer?.Stop(); _screenMixer?.Dispose(); _screenMixer = null;
|
||||
DisposeAuxCapture(); _auxStreamId = 0; // connection gone — drop capture, no StopStream
|
||||
txtCompose.Enabled = false;
|
||||
btnSend.Enabled = false;
|
||||
tsbJoinVoice.Enabled = false;
|
||||
@@ -641,6 +653,7 @@ public partial class MainForm : Form
|
||||
SetVoiceJoinedState(true);
|
||||
AddActivity("Joined voice — microphone active");
|
||||
_feedback.PlaySound(SoundEvent.VoiceOn);
|
||||
StartAuxStream(); // no-op unless the aux stream is enabled in settings
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -649,6 +662,7 @@ public partial class MainForm : Form
|
||||
}
|
||||
else
|
||||
{
|
||||
StopAuxStream();
|
||||
_client.SetPushToTalk(false);
|
||||
_client.StopStream(_micStreamId);
|
||||
_micStreamId = 0;
|
||||
@@ -743,6 +757,82 @@ public partial class MainForm : Form
|
||||
AddActivity("Stopped sharing screen audio");
|
||||
}
|
||||
|
||||
// ── Aux input stream (second hardware input device) ─────────────────────────
|
||||
// A second outgoing stream (kind = AUX_DEVICE, external_feed). The core can't open a second
|
||||
// capture device, so we capture the chosen device here and feed PCM in — the same external-
|
||||
// feed pipeline as per-app screen audio. Tied to the voice session: started on Join Voice
|
||||
// (when enabled) and stopped on Leave Voice. The aux is always-on (the core never gates
|
||||
// AUX_DEVICE on VAD/PTT); volume is applied client-side before feeding.
|
||||
|
||||
private void StartAuxStream()
|
||||
{
|
||||
if (_auxStreamId != 0 || !_voiceSettings.AuxEnabled) return;
|
||||
|
||||
var (result, streamId) = _client.StartStreamExternalFeed(VcStreamKind.AuxDevice, "Aux device");
|
||||
if (result != VcResult.Ok)
|
||||
{
|
||||
AddActivity($"Failed to start aux stream: {result}");
|
||||
return;
|
||||
}
|
||||
_auxStreamId = streamId;
|
||||
|
||||
_auxCapture = new InputDeviceCapture(_voiceSettings.AuxDeviceId);
|
||||
_auxCapture.PcmFrameReady += OnAuxPcmFrame;
|
||||
if (!_auxCapture.Start())
|
||||
{
|
||||
AddActivity("Failed to open aux input device");
|
||||
StopAuxStream();
|
||||
return;
|
||||
}
|
||||
AddActivity("Aux input stream active");
|
||||
}
|
||||
|
||||
private void StopAuxStream()
|
||||
{
|
||||
DisposeAuxCapture();
|
||||
if (_auxStreamId != 0)
|
||||
{
|
||||
_client.StopStream(_auxStreamId);
|
||||
_auxStreamId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-open the capture on a different device while the aux stream stays up (the core stream id
|
||||
// is unchanged — only the client-side capture source changes).
|
||||
private void RestartAuxCapture()
|
||||
{
|
||||
if (_auxStreamId == 0) return;
|
||||
DisposeAuxCapture();
|
||||
_auxCapture = new InputDeviceCapture(_voiceSettings.AuxDeviceId);
|
||||
_auxCapture.PcmFrameReady += OnAuxPcmFrame;
|
||||
if (!_auxCapture.Start())
|
||||
AddActivity("Failed to open aux input device");
|
||||
}
|
||||
|
||||
private void DisposeAuxCapture()
|
||||
{
|
||||
if (_auxCapture == null) return;
|
||||
_auxCapture.PcmFrameReady -= OnAuxPcmFrame;
|
||||
_auxCapture.Stop();
|
||||
_auxCapture.Dispose();
|
||||
_auxCapture = null;
|
||||
}
|
||||
|
||||
// Fired on the capture thread. vc_stream_feed_pcm is thread-safe, so feed directly. Gain is
|
||||
// read live from settings each frame (so the volume slider takes effect immediately).
|
||||
private void OnAuxPcmFrame(short[] pcm, int samplesPerChannel, int channels)
|
||||
{
|
||||
if (_auxStreamId == 0) return;
|
||||
float gain = _voiceSettings.AuxGain / 100f;
|
||||
if (gain != 1f)
|
||||
{
|
||||
for (int i = 0; i < pcm.Length; i++)
|
||||
pcm[i] = (short)Math.Clamp((int)MathF.Round(pcm[i] * gain),
|
||||
short.MinValue, short.MaxValue);
|
||||
}
|
||||
_client.StreamFeedPcm(_auxStreamId, pcm, samplesPerChannel, (uint)channels);
|
||||
}
|
||||
|
||||
private void TrkOutputVolume_Scroll(object? sender, EventArgs e) =>
|
||||
_client.SetOutputVolume(trkOutputVolume.Value / 100f);
|
||||
|
||||
@@ -1057,6 +1147,7 @@ public partial class MainForm : Form
|
||||
foreach (var win in _pmWindows.Values.ToList()) win.Close();
|
||||
_pmWindows.Clear();
|
||||
if (_screenStreamId != 0) { _screenMixer?.Stop(); _screenMixer?.Dispose(); _screenMixer = null; _client.StopStream(_screenStreamId); }
|
||||
if (_auxStreamId != 0) StopAuxStream();
|
||||
if (_micStreamId != 0) _client.StopStream(_micStreamId);
|
||||
_client.Disconnect();
|
||||
_client.Dispose();
|
||||
|
||||
Reference in New Issue
Block a user