fix(ios): keep the audio preset when speaker output is toggled
Speaker output decides where audio goes, not how it is captured, which is why it sits beside the preset row rather than inside Advanced audio. It was nonetheless moving the preset to Advanced, and the voice-chat preset was clearing it back, so the two controls overwrote each other and a user who wanted the speaker lost the preset that describes their capture setup. SetForceSpeaker no longer touches the preset, and Load and SelectPreset no longer clear the flag. A speakerIsExplicit default records that the user actually chose, so the automatic value older installs inherited from the voice-chat preset is dropped once rather than pinning a headset user to the speaker, and the toggle is honoured from then on. Apply issues the speaker port override again when the flag is set. Without it the toggle barely did anything with a headset attached, because DefaultToSpeaker only decides the route when nothing else is connected. With the toggle off it passes None, so a route left alone still follows an HFP headset. The changed logic sits on AVAudioSession, which the net10.0 test project cannot reference, so this carries no tests; the toggle needs device verification under the voice-chat preset with a Bluetooth headset connected.
This commit is contained in:
@@ -18,6 +18,7 @@ internal sealed class IosAudioRouter
|
|||||||
internal static IosAudioRouter Shared { get; } = new();
|
internal static IosAudioRouter Shared { get; } = new();
|
||||||
private readonly NSUserDefaults defaults = NSUserDefaults.StandardUserDefaults;
|
private readonly NSUserDefaults defaults = NSUserDefaults.StandardUserDefaults;
|
||||||
private bool applying;
|
private bool applying;
|
||||||
|
private bool speakerIsExplicit;
|
||||||
private System.Threading.Timer? watchdog;
|
private System.Threading.Timer? watchdog;
|
||||||
private long lastRenderCallbacks = -1;
|
private long lastRenderCallbacks = -1;
|
||||||
private int watchdogMisses;
|
private int watchdogMisses;
|
||||||
@@ -51,10 +52,11 @@ internal sealed class IosAudioRouter
|
|||||||
if (Enum.TryParse(defaults.StringForKey("cat.voice.audio.preset"), true, out IosAudioPreset preset)) Preset = preset;
|
if (Enum.TryParse(defaults.StringForKey("cat.voice.audio.preset"), true, out IosAudioPreset preset)) Preset = preset;
|
||||||
if (Enum.TryParse(defaults.StringForKey("cat.voice.audio.bluetoothMode"), true, out IosBluetoothMode bluetooth)) BluetoothMode = bluetooth;
|
if (Enum.TryParse(defaults.StringForKey("cat.voice.audio.bluetoothMode"), true, out IosBluetoothMode bluetooth)) BluetoothMode = bluetooth;
|
||||||
if (Enum.TryParse(defaults.StringForKey("cat.voice.audio.micMode"), true, out IosMicMode mic)) MicMode = mic;
|
if (Enum.TryParse(defaults.StringForKey("cat.voice.audio.micMode"), true, out IosMicMode mic)) MicMode = mic;
|
||||||
ForceSpeaker = defaults.BoolForKey("cat.voice.audio.forceSpeaker");
|
// Speaker output is an output-routing choice, orthogonal to the capture preset, which is
|
||||||
// Older voice-chat selections saved an automatic speaker override. Drop that
|
// why it sits outside Advanced audio. An install that predates the explicit choice carries
|
||||||
// preference for the named preset so an existing install can follow HFP again.
|
// a speaker flag the preset set on its behalf; drop that once so it cannot pin a headset
|
||||||
if (Preset == IosAudioPreset.VoiceChat) ForceSpeaker = false;
|
// user to the speaker, and honour the toggle from then on.
|
||||||
|
if (defaults.BoolForKey("cat.voice.audio.speakerIsExplicit")) ForceSpeaker = defaults.BoolForKey("cat.voice.audio.forceSpeaker");
|
||||||
VoiceProcessing = defaults.ValueForKey(new NSString("cat.voice.audio.voiceProcessing")) is null || defaults.BoolForKey("cat.voice.audio.voiceProcessing");
|
VoiceProcessing = defaults.ValueForKey(new NSString("cat.voice.audio.voiceProcessing")) is null || defaults.BoolForKey("cat.voice.audio.voiceProcessing");
|
||||||
AutomaticGainControl = defaults.ValueForKey(new NSString("cat.voice.audio.agc")) is null || defaults.BoolForKey("cat.voice.audio.agc");
|
AutomaticGainControl = defaults.ValueForKey(new NSString("cat.voice.audio.agc")) is null || defaults.BoolForKey("cat.voice.audio.agc");
|
||||||
CaptureChannels = defaults.IntForKey("cat.voice.audio.captureChannels") == 2 ? 2 : Preset == IosAudioPreset.StereoMicrophone ? 2 : 1;
|
CaptureChannels = defaults.IntForKey("cat.voice.audio.captureChannels") == 2 ? 2 : Preset == IosAudioPreset.StereoMicrophone ? 2 : 1;
|
||||||
@@ -77,14 +79,14 @@ internal sealed class IosAudioRouter
|
|||||||
// Named presets never carry an Advanced capsule selection across transitions.
|
// Named presets never carry an Advanced capsule selection across transitions.
|
||||||
// Stereo derives its data source below; mono/voice chat must clear a stale stereo one.
|
// Stereo derives its data source below; mono/voice chat must clear a stale stereo one.
|
||||||
SelectedDataSourceId = null; SelectedPolarPattern = AVAudioDataSourcePolarPattern.Unknown;
|
SelectedDataSourceId = null; SelectedPolarPattern = AVAudioDataSourcePolarPattern.Unknown;
|
||||||
// HFP needs to remain free to follow a connected headset. A speaker route can
|
|
||||||
// still be requested explicitly with the Speaker output control.
|
|
||||||
if (preset == IosAudioPreset.VoiceChat) ForceSpeaker = false;
|
|
||||||
}
|
}
|
||||||
SaveAndReconfigure();
|
SaveAndReconfigure();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void SetForceSpeaker(bool value) { ForceSpeaker = value; Preset = IosAudioPreset.Advanced; SaveAndReconfigure(); }
|
// Deliberately does not move the preset to Advanced: the speaker is an output choice every
|
||||||
|
// named preset supports, so a voice-chat user can take a call on the speaker without losing
|
||||||
|
// the capture configuration the preset stands for.
|
||||||
|
internal void SetForceSpeaker(bool value) { ForceSpeaker = value; speakerIsExplicit = true; SaveAndReconfigure(); }
|
||||||
internal void SetVoiceProcessing(bool value) { VoiceProcessing = value; SaveAndReconfigure(); }
|
internal void SetVoiceProcessing(bool value) { VoiceProcessing = value; SaveAndReconfigure(); }
|
||||||
internal void SetAutomaticGainControl(bool value) { AutomaticGainControl = value; SaveAndReconfigure(); }
|
internal void SetAutomaticGainControl(bool value) { AutomaticGainControl = value; SaveAndReconfigure(); }
|
||||||
internal void SetCaptureChannels(int value) { CaptureChannels = value == 2 ? 2 : 1; Preset = IosAudioPreset.Advanced; SaveAndReconfigure(); }
|
internal void SetCaptureChannels(int value) { CaptureChannels = value == 2 ? 2 : 1; Preset = IosAudioPreset.Advanced; SaveAndReconfigure(); }
|
||||||
@@ -130,9 +132,12 @@ internal sealed class IosAudioRouter
|
|||||||
Console.Error.WriteLine($"VC_ROUTE preset={Preset} requestedCh={CaptureChannels} sessionCh={session.InputNumberOfChannels} " +
|
Console.Error.WriteLine($"VC_ROUTE preset={Preset} requestedCh={CaptureChannels} sessionCh={session.InputNumberOfChannels} " +
|
||||||
$"preferred={session.PreferredInput?.PortName ?? "default"} dataSource={session.InputDataSource?.DataSourceName ?? "default"} " +
|
$"preferred={session.PreferredInput?.PortName ?? "default"} dataSource={session.InputDataSource?.DataSourceName ?? "default"} " +
|
||||||
$"pattern={session.InputDataSource?.SelectedPolarPattern.ToString() ?? "default"}");
|
$"pattern={session.InputDataSource?.SelectedPolarPattern.ToString() ?? "default"}");
|
||||||
// The speaker port override forces both input and output to built-in hardware,
|
// DefaultToSpeaker only decides where audio goes when nothing else is connected, so a
|
||||||
// even with a headset connected. DefaultToSpeaker is an explicit speaker choice
|
// user who asks for the speaker with a headset attached needs the port override as
|
||||||
// only; the voice-chat preset leaves it off so HFP can follow the headset.
|
// well. Only an explicit request forces it: left alone, the route stays free to follow
|
||||||
|
// an HFP headset, which is what the voice-chat preset depends on.
|
||||||
|
session.OverrideOutputAudioPort(ForceSpeaker && BluetoothMode != IosBluetoothMode.BuiltInMicA2dp
|
||||||
|
? AVAudioSessionPortOverride.Speaker : AVAudioSessionPortOverride.None, out _);
|
||||||
RefreshRoutes();
|
RefreshRoutes();
|
||||||
ResetWatchdog(); EnsureWatchdog();
|
ResetWatchdog(); EnsureWatchdog();
|
||||||
}
|
}
|
||||||
@@ -222,6 +227,7 @@ internal sealed class IosAudioRouter
|
|||||||
{
|
{
|
||||||
defaults.SetString(Preset.ToString(), "cat.voice.audio.preset"); defaults.SetString(BluetoothMode.ToString(), "cat.voice.audio.bluetoothMode");
|
defaults.SetString(Preset.ToString(), "cat.voice.audio.preset"); defaults.SetString(BluetoothMode.ToString(), "cat.voice.audio.bluetoothMode");
|
||||||
defaults.SetString(MicMode.ToString(), "cat.voice.audio.micMode"); defaults.SetBool(ForceSpeaker, "cat.voice.audio.forceSpeaker");
|
defaults.SetString(MicMode.ToString(), "cat.voice.audio.micMode"); defaults.SetBool(ForceSpeaker, "cat.voice.audio.forceSpeaker");
|
||||||
|
defaults.SetBool(speakerIsExplicit, "cat.voice.audio.speakerIsExplicit");
|
||||||
defaults.SetBool(VoiceProcessing, "cat.voice.audio.voiceProcessing"); defaults.SetBool(AutomaticGainControl, "cat.voice.audio.agc"); defaults.SetInt(CaptureChannels, "cat.voice.audio.captureChannels");
|
defaults.SetBool(VoiceProcessing, "cat.voice.audio.voiceProcessing"); defaults.SetBool(AutomaticGainControl, "cat.voice.audio.agc"); defaults.SetInt(CaptureChannels, "cat.voice.audio.captureChannels");
|
||||||
Set("cat.voice.audio.inputPortId", SelectedInputId); Set("cat.voice.audio.dataSourceId", SelectedDataSourceId); defaults.SetString(SelectedPolarPattern.ToString(), "cat.voice.audio.polarPattern"); defaults.Synchronize();
|
Set("cat.voice.audio.inputPortId", SelectedInputId); Set("cat.voice.audio.dataSourceId", SelectedDataSourceId); defaults.SetString(SelectedPolarPattern.ToString(), "cat.voice.audio.polarPattern"); defaults.Synchronize();
|
||||||
if (IosAudioEngine.Shared.IsConnected) IosAudioEngine.Shared.Reconfigure(); Changed?.Invoke();
|
if (IosAudioEngine.Shared.IsConnected) IosAudioEngine.Shared.Reconfigure(); Changed?.Invoke();
|
||||||
|
|||||||
Reference in New Issue
Block a user