Files
voice-cat/clients/apple/dotnet/VoiceCat.iOS/IosAudioRouter.cs
T
Talon c6715028c1
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
.NET port / cpp-conformance (push) Canceled after 0s
Add managed UIKit iOS client
2026-09-19 15:43:37 +02:00

64 lines
3.6 KiB
C#

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();
}
}