using AVFoundation; using Foundation; namespace VoiceCat.iOS; internal enum IosAudioPreset { VoiceChat, StereoMicrophone, MonoMicrophone, Advanced } internal sealed class IosAudioRouter { internal static IosAudioRouter Shared { get; } = new(); private readonly NSUserDefaults defaults = NSUserDefaults.StandardUserDefaults; internal IosAudioPreset Preset { get; private set; } = IosAudioPreset.VoiceChat; internal bool ForceSpeaker { get; set; } internal bool VoiceProcessing { get; set; } = true; internal bool AutomaticGainControl { get; set; } = true; internal int CaptureChannels => Preset == IosAudioPreset.StereoMicrophone ? 2 : 1; internal bool UsesVoiceProcessing => VoiceProcessing && CaptureChannels == 1 && Preset != IosAudioPreset.MonoMicrophone; private IosAudioRouter() { NSNotificationCenter.DefaultCenter.AddObserver(AVAudioSession.RouteChangeNotification, _ => Recover()); NSNotificationCenter.DefaultCenter.AddObserver(AVAudioSession.InterruptionNotification, note => HandleInterruption(note)); NSNotificationCenter.DefaultCenter.AddObserver(AVAudioSession.MediaServicesWereResetNotification, _ => Recover()); } internal void Load() { string? preset = defaults.StringForKey("cat.voice.audio.preset"); if (Enum.TryParse(preset, true, out IosAudioPreset value)) Preset = value; ForceSpeaker = defaults.BoolForKey("cat.voice.audio.forceSpeaker"); 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"); } internal void SelectPreset(IosAudioPreset preset) { Preset = preset; defaults.SetString(preset.ToString(), "cat.voice.audio.preset"); defaults.Synchronize(); if (IosAudioEngine.Shared.IsConnected) IosAudioEngine.Shared.Reconfigure(); } internal void Apply() { AVAudioSession session = AVAudioSession.SharedInstance(); AVAudioSessionCategoryOptions options = AVAudioSessionCategoryOptions.AllowBluetooth; if (Preset is IosAudioPreset.StereoMicrophone or IosAudioPreset.MonoMicrophone) options = AVAudioSessionCategoryOptions.AllowBluetoothA2DP | AVAudioSessionCategoryOptions.DefaultToSpeaker; if (!session.SetCategory(AVAudioSessionCategory.PlayAndRecord, AVAudioSessionMode.Default, options, out NSError? categoryError)) throw new InvalidOperationException(categoryError.LocalizedDescription); session.SetPreferredSampleRate(48_000, out _); session.SetPreferredIOBufferDuration(0.02, out _); if (!session.SetActive(true, AVAudioSessionSetActiveOptions.NotifyOthersOnDeactivation, out NSError? activeError)) throw new InvalidOperationException(activeError.LocalizedDescription); session.OverrideOutputAudioPort(ForceSpeaker ? AVAudioSessionPortOverride.Speaker : AVAudioSessionPortOverride.None, out _); } internal void Deactivate() => AVAudioSession.SharedInstance().SetActive(false, AVAudioSessionSetActiveOptions.NotifyOthersOnDeactivation, out _); private void Recover() { if (IosAudioEngine.Shared.IsConnected) UIApplication.SharedApplication.BeginInvokeOnMainThread(IosAudioEngine.Shared.Reconfigure); } private void HandleInterruption(NSNotification note) { NSNumber? type = note.UserInfo?[new NSString("AVAudioSessionInterruptionTypeKey")] as NSNumber; if ((AVAudioSessionInterruptionType)(type?.UInt32Value ?? 0) == AVAudioSessionInterruptionType.Ended) Recover(); } }