64 lines
3.6 KiB
C#
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();
|
||
|
|
}
|
||
|
|
}
|