Activate iOS audio session before stereo input selection
This commit is contained in:
@@ -54,6 +54,12 @@ no longer held mapped through suspension. A TestFlight report from build 2026092
|
||||
`0xdead10cc` shared-file-lock termination; physical-device validation of that mitigation remains
|
||||
open.
|
||||
|
||||
Stereo capture now selects the preferred input only after activating the audio session and no
|
||||
longer calls `SetInputDataSource` with a source from a port that may not yet be current. A Debug
|
||||
build installed on an iPhone 16 Pro Max connected with the StereoMicrophone preset and recorded
|
||||
a two-channel input graph with distinct left/right PCM and encoded packets. The simulator's mono
|
||||
input also completed stereo-preset login, voice join, disconnect, and reconnect.
|
||||
|
||||
Voice-chat capture is not yet reliable on the first attempt: a graph with voice processing still
|
||||
sometimes fails to start and is only recovered by a watchdog rebuild, which costs seconds before
|
||||
audio appears. Speaker output is an explicit choice that no longer changes the preset. Every
|
||||
|
||||
@@ -147,8 +147,9 @@ internal sealed class IosAudioRouter
|
||||
: BluetoothMode == IosBluetoothMode.BuiltInMicA2dp ? AVAudioSessionMode.VideoRecording : AVAudioSessionMode.VoiceChat;
|
||||
if (!session.SetCategory(AVAudioSessionCategory.PlayAndRecord, mode, options, out NSError? categoryError)) throw new InvalidOperationException(categoryError?.LocalizedDescription ?? "Could not configure the iOS audio session.");
|
||||
session.SetPreferredSampleRate(48_000, out _); session.SetPreferredIOBufferDuration(0.02, out _);
|
||||
if (configureInput) ApplyInputSelection(session);
|
||||
if (!session.SetActive(true, AVAudioSessionSetActiveOptions.NotifyOthersOnDeactivation, out NSError? activeError)) throw new InvalidOperationException(activeError?.LocalizedDescription ?? "Could not activate the iOS audio session.");
|
||||
// Apple requires an active session before selecting a preferred input or data source.
|
||||
if (configureInput) ApplyInputSelection(session);
|
||||
Console.Error.WriteLine($"VC_ROUTE preset={Preset} requestedCh={CaptureChannels} sessionCh={session.InputNumberOfChannels} " +
|
||||
$"preferred={session.PreferredInput?.PortName ?? "default"} dataSource={session.InputDataSource?.DataSourceName ?? "default"} " +
|
||||
$"pattern={session.InputDataSource?.SelectedPolarPattern.ToString() ?? "default"}");
|
||||
@@ -172,6 +173,7 @@ 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 && stereoApplied) ClearStereo(session); return; }
|
||||
if (!session.SetPreferredInput(port, out NSError? inputError)) throw new InvalidOperationException(inputError?.LocalizedDescription ?? "Could not select the microphone input.");
|
||||
if (CaptureChannels == 2)
|
||||
{
|
||||
// Polar-pattern discovery alone is insufficient on current iPhones: until a stereo
|
||||
@@ -195,13 +197,11 @@ internal sealed class IosAudioRouter
|
||||
!source.SetPreferredPolarPattern(SelectedPolarPattern, out NSError? patternError))
|
||||
throw new InvalidOperationException(patternError?.LocalizedDescription ?? "Could not select the microphone polar pattern.");
|
||||
}
|
||||
if (!session.SetPreferredInput(port, out NSError? inputError)) throw new InvalidOperationException(inputError?.LocalizedDescription ?? "Could not select the microphone input.");
|
||||
// The working Swift client deliberately does not call
|
||||
// SetPreferredInputNumberOfChannels: doing so disrupts stereo + A2DP routing. The stereo
|
||||
// capsule and polar pattern above cause the input node to expose its two-channel format.
|
||||
if (CaptureChannels == 2 && source is not null &&
|
||||
!session.SetInputDataSource(source, out NSError? sourceError))
|
||||
throw new InvalidOperationException(sourceError?.LocalizedDescription ?? "Could not activate the stereo microphone source.");
|
||||
// A port preference is enough. SetInputDataSource only accepts a member of the *current*
|
||||
// port's InputDataSources, which can still be a different route after SetPreferredInput.
|
||||
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()) ?? [])}");
|
||||
|
||||
@@ -32,6 +32,8 @@ internal static class SimulatorSmoke
|
||||
model.UpsertProfile(profile, parts[2]);
|
||||
}
|
||||
model.Load();
|
||||
if (Environment.GetEnvironmentVariable("VOICECAT_SIM_SMOKE_MODE") == "stereo")
|
||||
IosAudioRouter.Shared.SelectPreset(IosAudioPreset.StereoMicrophone);
|
||||
profile = model.Profiles.Single(value => value.Id == id);
|
||||
Console.Error.WriteLine("VC_SIM phase=connect");
|
||||
await model.ConnectAsync(profile);
|
||||
|
||||
@@ -163,6 +163,8 @@ synthetic screen audio with the microphone off and during a voice call, voice jo
|
||||
and reconnect. Look for
|
||||
`VC_SIM phase=screenSharePassed` and `VC_SIM phase=passed`. Terminate the app, then launch
|
||||
again with mode `resume` to verify the saved account after a process restart. The simulator
|
||||
gate also accepts mode `stereo` to select the StereoMicrophone preset before login and voice join;
|
||||
the simulator exposes only a mono microphone, so check stereo capture on an iPhone. It also
|
||||
does not validate Bluetooth hardware, the iOS 27 ScreenCaptureKit picker, or suspension on
|
||||
a physical device.
|
||||
|
||||
|
||||
@@ -207,8 +207,11 @@ public class PublishServerScriptTests
|
||||
string router = await File.ReadAllTextAsync(Path.Combine(root, "clients", "apple", "VoiceCat.iOS", "IosAudioRouter.cs"));
|
||||
string engine = await File.ReadAllTextAsync(Path.Combine(root, "clients", "apple", "VoiceCat.iOS", "IosAudioEngine.cs"));
|
||||
|
||||
Assert.True(router.IndexOf("ApplyInputSelection(session)", StringComparison.Ordinal) <
|
||||
router.IndexOf("SetActive(true", StringComparison.Ordinal));
|
||||
Assert.True(router.IndexOf("SetActive(true", StringComparison.Ordinal) <
|
||||
router.IndexOf("ApplyInputSelection(session)", StringComparison.Ordinal));
|
||||
Assert.True(router.IndexOf("session.SetPreferredInput(port", StringComparison.Ordinal) <
|
||||
router.IndexOf("port.SetPreferredDataSource(source", StringComparison.Ordinal));
|
||||
Assert.DoesNotContain("session.SetInputDataSource(source", router);
|
||||
Assert.DoesNotContain("session.SetPreferredInputNumberOfChannels", router);
|
||||
Assert.DoesNotContain("MaximumInputNumberOfChannels", router);
|
||||
Assert.Contains("SetPreferredInputOrientation(AVAudioStereoOrientation.Portrait", router);
|
||||
|
||||
Reference in New Issue
Block a user