From 7e41c0d12f64a56c86c178cbf88a95326d336287 Mon Sep 17 00:00:00 2001 From: Charles <252065487+tobleromed@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:44:08 -0700 Subject: [PATCH] Default --outputs to a stereo pair instead of every output channel Writing to every output channel is actively harmful on an Aggregate Device built around a loopback. On a BlackHole + speakers aggregate, channels 0-1 are BlackHole's outputs, which feed BlackHole's input - so the previous default routed the circuit's output back into its own input and produced runaway feedback through whatever distortion was being simulated. Default to the first two channels, and when a device has more outputs than were selected, print the full channel list marking which are in use. This is a defensive default, not automatic routing: it does NOT work out which pair is audible. On an aggregate the first pair is often the loopback rather than the speakers, so --outputs is still required there - the change turns a howling feedback loop into silence plus a printed list showing what to pass. Devices with two output channels are unaffected. Doing this properly would mean reading kAudioAggregateDevicePropertyFullSubDeviceList, mapping channel ranges to sub-devices, and preferring outputs on a sub-device that contributes no input. That is worth doing if aggregates become a common setup, but it is a heuristic that can guess wrong on unusual hardware, so it is deliberately left out of this change. Co-Authored-By: Claude Fable 5 --- LiveSPICE.CLI/Program.cs | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/LiveSPICE.CLI/Program.cs b/LiveSPICE.CLI/Program.cs index 2fd9cd5b..6df59dd3 100644 --- a/LiveSPICE.CLI/Program.cs +++ b/LiveSPICE.CLI/Program.cs @@ -57,7 +57,9 @@ Render a wav through a circuit offline. No audio device needed. livespice play --schematic [--device ] [--inputs 0] [--outputs 0,1] [--oversample 8] [--iterations 8] [--input-gain 1] [--output-gain 1] [--seconds ] - Play live through a circuit. Runs until Ctrl-C unless --seconds is given."); + Play live through a circuit. Runs until Ctrl-C unless --seconds is given. + --outputs defaults to the first two channels. On an Aggregate Device those may + be the loopback rather than the speakers - check 'livespice list'."); } // ---------------------------------------------------------------- list @@ -182,7 +184,8 @@ static int Play(Args a) Audio.Device device = FindDevice(deviceName); Audio.Channel[] inputs = SelectChannels(device.InputChannels, a.String("inputs", null), 1); - Audio.Channel[] outputs = SelectChannels(device.OutputChannels, a.String("outputs", null), int.MaxValue); + Audio.Channel[] outputs = SelectChannels(device.OutputChannels, a.String("outputs", null), DefaultOutputChannels); + WarnUnselectedOutputs(device, outputs); Console.WriteLine("Device: {0}", device.Name); Console.WriteLine("Inputs: {0}", inputs.Length > 0 ? string.Join(", ", inputs.Select(i => i.Name)) : "(none)"); @@ -259,7 +262,8 @@ static int Loopback(Args a) Audio.Device device = FindDevice(deviceName); Audio.Channel[] inputs = SelectChannels(device.InputChannels, a.String("inputs", null), 1); - Audio.Channel[] outputs = SelectChannels(device.OutputChannels, a.String("outputs", null), int.MaxValue); + Audio.Channel[] outputs = SelectChannels(device.OutputChannels, a.String("outputs", null), DefaultOutputChannels); + WarnUnselectedOutputs(device, outputs); Console.WriteLine("Device: {0} ({1} in, {2} out)", device.Name, inputs.Length, outputs.Length); List captured = new List(); @@ -372,6 +376,26 @@ static Audio.Device FindDevice(string Name) return device; } + /// + /// Default to a stereo pair rather than every output. Writing to every channel of a device + /// that also provides input - an Aggregate Device built around a loopback like BlackHole - + /// feeds the output straight back into the input. + /// + const int DefaultOutputChannels = 2; + + static void WarnUnselectedOutputs(Audio.Device Device, Audio.Channel[] Selected) + { + if (Device.OutputChannels.Length <= Selected.Length) + return; + Console.WriteLine("Note: this device has {0} output channels and {1} were selected. " + + "On an Aggregate Device the first channels are not necessarily the ones " + + "you hear - use --outputs to pick. Available:", + Device.OutputChannels.Length, Selected.Length); + for (int i = 0; i < Device.OutputChannels.Length; ++i) + Console.WriteLine(" [{0}] {1}{2}", i, Device.OutputChannels[i].Name, + Selected.Contains(Device.OutputChannels[i]) ? " (selected)" : ""); + } + static Audio.Channel[] SelectChannels(Audio.Channel[] Available, string Spec, int DefaultCount) { if (Available.Length == 0)