Stabilize iOS reconnect and screen audio sessions
This commit is contained in:
@@ -88,6 +88,7 @@ internal sealed class IosAudioEngine
|
||||
// right hardware. Rebuilding it there would release an HFP headset and pay a Bluetooth
|
||||
// profile renegotiation for a transport blip that changed no audio configuration.
|
||||
if (engine?.Running == true) { playbackRing.Resynchronize(); return; }
|
||||
if (ResumeStoppedGraph()) return;
|
||||
Volatile.Write(ref microphone, null); Rebuild();
|
||||
}
|
||||
|
||||
@@ -134,6 +135,7 @@ internal sealed class IosAudioEngine
|
||||
int channels = IosAudioRouter.Shared.CaptureChannels;
|
||||
if (!force && engine?.Running == true && tapInstalled && builtCaptureChannels == channels
|
||||
&& voiceProcessing == IosAudioRouter.Shared.UsesVoiceProcessing && !HardwareChanged()) return;
|
||||
if (!force && ResumeStoppedGraph()) return;
|
||||
if (current is not null)
|
||||
{
|
||||
// Stop the old tap before publishing a route with a different sample width. Otherwise
|
||||
@@ -155,9 +157,30 @@ internal sealed class IosAudioEngine
|
||||
// Still starting: report it as running rather than replacing it, since that is what it is
|
||||
// about to be, and a rebuild here would start the cycle over.
|
||||
if (Settling) return true;
|
||||
if (ResumeStoppedGraph()) return true;
|
||||
Rebuild(); return engine?.Running == true;
|
||||
}
|
||||
|
||||
private bool ResumeStoppedGraph()
|
||||
{
|
||||
if (engine is not { } paused || !tapInstalled ||
|
||||
builtCaptureChannels != IosAudioRouter.Shared.CaptureChannels ||
|
||||
voiceProcessing != IosAudioRouter.Shared.UsesVoiceProcessing || HardwareChanged()) return false;
|
||||
IosAudioRouter.Shared.Apply(true);
|
||||
bool started = paused.StartAndReturnError(out NSError? error);
|
||||
if (started)
|
||||
{
|
||||
playbackRing.Resynchronize();
|
||||
// Voice-processing IO may still be starting when Start returns. Give the existing
|
||||
// graph its settling window before a watchdog judges it, just as a fresh graph gets.
|
||||
Volatile.Write(ref lastRebuildAt, Environment.TickCount64);
|
||||
Console.Error.WriteLine($"VC_RESUME running={paused.Running}");
|
||||
return true;
|
||||
}
|
||||
Console.Error.WriteLine($"VC_RESUME running={paused.Running} error={error?.LocalizedDescription ?? "none"}");
|
||||
return false;
|
||||
}
|
||||
|
||||
private void Rebuild([System.Runtime.CompilerServices.CallerMemberName] string cause = "")
|
||||
{
|
||||
Console.Error.WriteLine($"VC_REBUILD cause={cause} running={engine?.Running == true} callbacks={RenderCallbacks}");
|
||||
@@ -182,7 +205,7 @@ internal sealed class IosAudioEngine
|
||||
// formats are queried or nodes are connected so the graph is built from the final IO.
|
||||
input = next.InputNode;
|
||||
if (!input.SetVoiceProcessingEnabled(IosAudioRouter.Shared.UsesVoiceProcessing, out NSError? processingError))
|
||||
throw new InvalidOperationException(processingError.LocalizedDescription);
|
||||
throw new InvalidOperationException(processingError?.LocalizedDescription ?? "Could not configure voice processing.");
|
||||
if (IosAudioRouter.Shared.UsesVoiceProcessing) input.VoiceProcessingAgcEnabled = IosAudioRouter.Shared.AutomaticGainControl;
|
||||
}
|
||||
outputFormat = new(AVAudioCommonFormat.PCMFloat32, 48_000, 2, false);
|
||||
@@ -195,6 +218,8 @@ internal sealed class IosAudioEngine
|
||||
if (captures)
|
||||
{
|
||||
AVAudioFormat inputFormat = input!.GetBusOutputFormat(0);
|
||||
if (inputFormat.SampleRate <= 0 || inputFormat.ChannelCount == 0)
|
||||
throw new InvalidOperationException("No usable microphone input is available on the current audio route.");
|
||||
Console.Error.WriteLine($"VC_GRAPH vpio={IosAudioRouter.Shared.UsesVoiceProcessing} requestedCh={captureChannels} " +
|
||||
$"inputCh={inputFormat.ChannelCount} inputRate={inputFormat.SampleRate}");
|
||||
builtInputRate = inputFormat.SampleRate; builtInputChannels = inputFormat.ChannelCount;
|
||||
@@ -231,15 +256,15 @@ internal sealed class IosAudioEngine
|
||||
captureSink.OutputVolume = 0f;
|
||||
}
|
||||
next.Prepare();
|
||||
if (!next.StartAndReturnError(out NSError? error)) { next.Dispose(); throw new InvalidOperationException(error.LocalizedDescription); }
|
||||
if (!next.StartAndReturnError(out NSError? error)) { next.Dispose(); throw new InvalidOperationException(error?.LocalizedDescription ?? "The iOS audio graph could not start."); }
|
||||
engine = next;
|
||||
{
|
||||
AVAudioSession live = AVAudioSession.SharedInstance();
|
||||
Console.Error.WriteLine($"VC_START running={next.Running} builtCh={builtInputChannels} sessionRate={live.SampleRate} " +
|
||||
$"sessionInCh={live.InputNumberOfChannels} sessionOutCh={live.OutputNumberOfChannels} inputAvailable={live.InputAvailable} " +
|
||||
$"other={live.OtherAudioPlaying} mode={live.Mode} options={live.CategoryOptions} io={live.IOBufferDuration:F4} " +
|
||||
$"out={string.Join(',', live.CurrentRoute.Outputs.Select(value => value.PortType.ToString()))} " +
|
||||
$"in={string.Join(',', live.CurrentRoute.Inputs.Select(value => value.PortType.ToString()))}");
|
||||
$"out={string.Join(',', live.CurrentRoute?.Outputs?.Select(value => value.PortType.ToString()) ?? [])} " +
|
||||
$"in={string.Join(',', live.CurrentRoute?.Inputs?.Select(value => value.PortType.ToString()) ?? [])}");
|
||||
// A graph that starts and then stops on its own is the failure that matters, and it is
|
||||
// invisible at start: check again once the IO has had time to come up.
|
||||
AVAudioEngine started = next;
|
||||
@@ -380,7 +405,7 @@ internal sealed class IosAudioEngine
|
||||
{
|
||||
IsConnected = false; Volatile.Write(ref microphone, null);
|
||||
if (client is { } owner) owner.Audio.MixedPcm -= ReceiveMixedPcm;
|
||||
DestroyGraph(); client = null; IosAudioRouter.Shared.Deactivate();
|
||||
engine?.Pause(); client = null; IosAudioRouter.Shared.Deactivate();
|
||||
}
|
||||
|
||||
private void DestroyGraph()
|
||||
|
||||
Reference in New Issue
Block a user