Fix iOS stereo microphone capture
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using AVFoundation;
|
||||
using Foundation;
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace VoiceCat.iOS;
|
||||
@@ -12,6 +13,8 @@ internal sealed record IosAudioDataSource(string Id, string Name, IReadOnlyList<
|
||||
|
||||
internal sealed class IosAudioRouter
|
||||
{
|
||||
private static readonly NativeHandle SupportedPolarPatternsSelector = Selector.GetHandle("supportedPolarPatterns");
|
||||
private static readonly NativeHandle SetPreferredPolarPatternSelector = Selector.GetHandle("setPreferredPolarPattern:error:");
|
||||
internal static IosAudioRouter Shared { get; } = new();
|
||||
private readonly NSUserDefaults defaults = NSUserDefaults.StandardUserDefaults;
|
||||
private bool applying;
|
||||
@@ -123,17 +126,26 @@ internal sealed class IosAudioRouter
|
||||
AVAudioSessionPortDescription? port = session.AvailableInputs?.FirstOrDefault(value => value.UID == SelectedInputId);
|
||||
if (CaptureChannels == 2) port ??= session.AvailableInputs?.FirstOrDefault(value => value.PortType == AVAudioSession.PortBuiltInMic);
|
||||
if (port is null) { if (CaptureChannels == 1) ClearStereoPolarPattern(session); return; }
|
||||
if (CaptureChannels == 2)
|
||||
{
|
||||
// Polar-pattern discovery alone is insufficient on current iPhones: until a stereo
|
||||
// orientation is requested, the built-in port can expose only its mono Bottom source
|
||||
// and AVAudioEngine consequently binds a one-channel input node. This is Apple's
|
||||
// dedicated switch for built-in stereo recording. Portrait matches this portrait UI;
|
||||
// it also gives Core Audio an unambiguous left/right mapping before graph creation.
|
||||
if (!session.SetPreferredInputOrientation(AVAudioStereoOrientation.Portrait, out NSError? orientationError))
|
||||
throw new InvalidOperationException(orientationError?.LocalizedDescription ?? "Could not set the stereo microphone orientation.");
|
||||
}
|
||||
AVAudioSessionDataSourceDescription? source = CaptureChannels == 2
|
||||
? port.DataSources?.FirstOrDefault(value => value.SupportedPolarPatterns?.Any(pattern => pattern.ToString().Contains("Stereo", StringComparison.OrdinalIgnoreCase)) == true)
|
||||
? port.DataSources?.FirstOrDefault(SupportsStereoPolarPattern)
|
||||
: port.DataSources?.FirstOrDefault(value => value.DataSourceID.ToString() == SelectedDataSourceId);
|
||||
if (CaptureChannels == 1 && source is null) ClearStereoPolarPattern(session);
|
||||
if (source is not null)
|
||||
{
|
||||
if (!port.SetPreferredDataSource(source, out NSError? portError)) throw new InvalidOperationException(portError.LocalizedDescription);
|
||||
AVAudioDataSourcePolarPattern pattern = CaptureChannels == 2
|
||||
? source.SupportedPolarPatterns?.FirstOrDefault(value => value.ToString().Contains("Stereo", StringComparison.OrdinalIgnoreCase)) ?? AVAudioDataSourcePolarPattern.Unknown
|
||||
: SelectedPolarPattern;
|
||||
if (pattern != AVAudioDataSourcePolarPattern.Unknown && !source.SetPreferredPolarPattern(pattern, out NSError? patternError))
|
||||
if (CaptureChannels == 2) SetStereoPolarPattern(source);
|
||||
else if (SelectedPolarPattern != AVAudioDataSourcePolarPattern.Unknown &&
|
||||
!source.SetPreferredPolarPattern(SelectedPolarPattern, out NSError? patternError))
|
||||
throw new InvalidOperationException(patternError.LocalizedDescription);
|
||||
}
|
||||
if (!session.SetPreferredInput(port, out NSError? inputError)) throw new InvalidOperationException(inputError.LocalizedDescription);
|
||||
@@ -144,15 +156,47 @@ internal sealed class IosAudioRouter
|
||||
!session.SetInputDataSource(source, out NSError? sourceError))
|
||||
throw new InvalidOperationException(sourceError.LocalizedDescription);
|
||||
Console.Error.WriteLine($"VC_ROUTE_SELECT port={port.PortName} source={source?.DataSourceName ?? "none"} " +
|
||||
$"stereoPattern={source is not null && SupportsStereoPolarPattern(source)} " +
|
||||
$"patterns={string.Join(',', source?.SupportedPolarPatterns?.Select(value => value.ToString()) ?? [])}");
|
||||
}
|
||||
|
||||
// Microsoft.iOS 27 exposes AVAudioSessionPolarPatternStereo as an NSString constant but its
|
||||
// AVAudioDataSourcePolarPattern smart enum still has no Stereo member. Reading the native
|
||||
// NSArray and setting the native NSString avoids silently mapping Stereo to Unknown.
|
||||
private static bool SupportsStereoPolarPattern(AVAudioSessionDataSourceDescription source)
|
||||
{
|
||||
NativeHandle handle = NativeMethods.GetObject(source.Handle, SupportedPolarPatternsSelector);
|
||||
NSArray? patterns = Runtime.GetNSObject<NSArray>(handle);
|
||||
if (patterns is null) return false;
|
||||
for (nuint index = 0; index < patterns.Count; index++)
|
||||
if (patterns.GetItem<NSString>(index)?.IsEqualTo(AVAudioSession.PolarPatternStereo.Handle) == true) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void SetStereoPolarPattern(AVAudioSessionDataSourceDescription source)
|
||||
{
|
||||
NativeHandle error = NativeHandle.Zero;
|
||||
if (NativeMethods.SetObject(source.Handle, SetPreferredPolarPatternSelector,
|
||||
AVAudioSession.PolarPatternStereo.Handle, ref error) == 0)
|
||||
throw new InvalidOperationException(Runtime.GetNSObject<NSError>(error)?.LocalizedDescription ?? "Could not enable stereo microphone capture.");
|
||||
}
|
||||
|
||||
private static class NativeMethods
|
||||
{
|
||||
[System.Runtime.InteropServices.DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
|
||||
internal static extern NativeHandle GetObject(NativeHandle receiver, NativeHandle selector);
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")]
|
||||
internal static extern byte SetObject(NativeHandle receiver, NativeHandle selector, NativeHandle value, ref NativeHandle error);
|
||||
}
|
||||
|
||||
private static void ClearStereoPolarPattern(AVAudioSession session)
|
||||
{
|
||||
session.SetPreferredInputOrientation(AVAudioStereoOrientation.None, out _);
|
||||
AVAudioSessionPortDescription? builtIn = session.AvailableInputs?.FirstOrDefault(value => value.PortType == AVAudioSession.PortBuiltInMic);
|
||||
if (builtIn is null) return;
|
||||
foreach (AVAudioSessionDataSourceDescription source in builtIn.DataSources ?? [])
|
||||
if (source.SelectedPolarPattern.ToString().Contains("Stereo", StringComparison.OrdinalIgnoreCase))
|
||||
if (source.SelectedPolarPattern == AVAudioDataSourcePolarPattern.Unknown && SupportsStereoPolarPattern(source))
|
||||
source.SetPreferredPolarPattern(AVAudioDataSourcePolarPattern.Unknown, out _);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user