Improve iOS voice stability and user audio controls
Build and test / test (macos-latest) (push) Canceled after 0s
Build and test / test (ubuntu-24.04) (push) Canceled after 0s
Build and test / test (windows-latest) (push) Canceled after 0s
Build and test / apple-client (push) Canceled after 0s

This commit is contained in:
2026-09-22 22:07:00 +02:00
parent 7b0c003ad4
commit ea76d5157a
15 changed files with 460 additions and 68 deletions
+9 -27
View File
@@ -24,9 +24,10 @@ internal sealed class IosAudioEngine
{
internal readonly uint StreamId = streamId;
internal readonly int Channels = channels;
internal readonly PcmRing Ring = new(131_072);
internal bool Primed;
internal int TargetFrames = 3;
// Capture hardware and the managed 20 ms sender have independent clocks. Correct
// their small rate difference before the queue eventually reaches its hard edge.
// RemoteIO can deliver capture in 100 ms bursts, so retain one burst of headroom.
internal readonly AdaptivePcmBuffer Ring = new(channels, 120, 65_536);
}
private MicrophoneRoute? microphone;
private readonly short[] microphoneFrame = new short[960 * 2];
@@ -77,13 +78,7 @@ internal sealed class IosAudioEngine
private static MicrophoneRoute CreateMicrophoneRoute(uint streamId, int channels)
{
var route = new MicrophoneRoute(streamId, Math.Clamp(channels, 1, 2));
// Physical RemoteIO capture is commonly delivered in 100 ms bursts. Starting its 20 ms
// pacer with only the VPIO-oriented 60 ms cushion guarantees several underruns after every
// mono/stereo rebuild before the adaptive path catches up. Prime one full burst plus one
// frame for non-VPIO routes; VPIO remains at the proven low-latency three-frame cushion.
route.TargetFrames = IosAudioRouter.Shared.UsesVoiceProcessing ? 3 : 6;
return route;
return new MicrophoneRoute(streamId, Math.Clamp(channels, 1, 2));
}
internal bool EnsureRunning()
{
@@ -198,23 +193,10 @@ internal sealed class IosAudioEngine
if (route is not null && owner is not null)
{
int required = 960 * route.Channels;
int completeFrames = route.Ring.Count / required;
if (!route.Primed)
{
if (completeFrames >= route.TargetFrames) route.Primed = true;
}
if (route.Primed)
{
if (completeFrames == 0)
{
route.Primed = false;
route.TargetFrames = Math.Min(route.TargetFrames + 1, 6);
}
else if (route.Ring.Read(microphoneFrame.AsSpan(0, required)) == required &&
ReferenceEquals(route, Volatile.Read(ref microphone)) &&
!owner.Audio.FeedPcm(route.StreamId, microphoneFrame.AsSpan(0, required), route.Channels))
Interlocked.Increment(ref rejectedFeeds);
}
if (route.Ring.Read(microphoneFrame.AsSpan(0, required)) == required &&
ReferenceEquals(route, Volatile.Read(ref microphone)) &&
!owner.Audio.FeedPcm(route.StreamId, microphoneFrame.AsSpan(0, required), route.Channels))
Interlocked.Increment(ref rejectedFeeds);
}
deadline += System.Diagnostics.Stopwatch.Frequency / 50;
while (true)