Fix iOS voice capture and screen sharing
This commit is contained in:
@@ -7,6 +7,27 @@ namespace VoiceCat.Tests;
|
||||
|
||||
public class AudioEngineTests
|
||||
{
|
||||
[Fact]
|
||||
public void ManagedAudioWorkerUsesDeadlineSchedulingAtRealtimePriority()
|
||||
{
|
||||
string source = File.ReadAllText(Path.Combine(FindRoot(), "src", "VoiceCat.Audio", "AudioEngine.cs"));
|
||||
|
||||
Assert.Contains("Priority = ThreadPriority.Highest", source);
|
||||
Assert.Contains("WaitUntil(deadline);", source);
|
||||
Assert.DoesNotContain("Thread.Sleep((int)Math.Ceiling(remaining))", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MediaSenderDoesNotDependOnCoalescedAsyncTimers()
|
||||
{
|
||||
string source = File.ReadAllText(Path.Combine(FindRoot(), "src", "VoiceCat.Core", "ClientMediaTransport.cs"));
|
||||
|
||||
Assert.Contains("new Thread(Send)", source);
|
||||
Assert.Contains("socket.Send(packet.AsSpan(0, size)", source);
|
||||
Assert.DoesNotContain("Task.Delay(5", source);
|
||||
Assert.DoesNotContain("SendAsync(packet.AsMemory", source);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1000)]
|
||||
[InlineData(1000)]
|
||||
@@ -29,6 +50,58 @@ public class AudioEngineTests
|
||||
Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdaptivePcmBufferPreservesPartialFrameAcrossCaptureCallbacks()
|
||||
{
|
||||
var buffer = new AdaptivePcmBuffer(1, 20);
|
||||
short[] half = Enumerable.Range(0, 480).Select(value => (short)value).ToArray();
|
||||
short[] full = new short[960];
|
||||
|
||||
Assert.True(buffer.TryWrite(half));
|
||||
Assert.Equal(0, buffer.Read(full));
|
||||
Assert.Equal(480, buffer.CountFrames);
|
||||
|
||||
Assert.True(buffer.TryWrite(half));
|
||||
Assert.Equal(960, buffer.Read(full));
|
||||
Assert.Equal(0, buffer.CountFrames);
|
||||
Assert.Equal(half, full.AsSpan(0, 480).ToArray());
|
||||
Assert.Equal(half, full.AsSpan(480, 480).ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(20)]
|
||||
[InlineData(40)]
|
||||
[InlineData(60)]
|
||||
public void AdaptivePcmBufferSustainsIosSizedCaptureCallbacks(int bufferMilliseconds)
|
||||
{
|
||||
var buffer = new AdaptivePcmBuffer(1, bufferMilliseconds);
|
||||
short[] callback = new short[1_024], encoded = new short[960];
|
||||
long producedFrames = 0;
|
||||
int lateStarvation = 0, fullReads = 0;
|
||||
|
||||
// AVAudioEngine commonly delivers 1,024 frames every 21 1/3 ms while the codec owner
|
||||
// requests 960 frames every 20 ms. Replay those independent clocks for two minutes.
|
||||
for (long consumerFrame = 0; consumerFrame < 48_000L * 120; consumerFrame += 960)
|
||||
{
|
||||
while (producedFrames <= consumerFrame)
|
||||
{
|
||||
for (int i = 0; i < callback.Length; i++) callback[i] = (short)(producedFrames + i);
|
||||
Assert.True(buffer.TryWrite(callback));
|
||||
producedFrames += callback.Length;
|
||||
}
|
||||
|
||||
int read = buffer.Read(encoded);
|
||||
if (read == encoded.Length) fullReads++;
|
||||
// A 20 ms target initially contains one 1,024-frame callback, so the codec's
|
||||
// second deadline precedes the next callback by 1.33 ms. Re-priming once during
|
||||
// that first second is expected; recurring starvation is the audible defect.
|
||||
else if (consumerFrame >= 48_000) lateStarvation++;
|
||||
}
|
||||
|
||||
Assert.Equal(0, lateStarvation);
|
||||
Assert.True(fullReads > 5_000);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OneCaptureMissDoesNotRestartTalkspurtButSustainedStarvationDoes()
|
||||
{
|
||||
@@ -40,6 +113,8 @@ public class AudioEngineTests
|
||||
engine.ProcessCycle();
|
||||
engine.FeedPcm(1, tone, 1); engine.ProcessCycle();
|
||||
Assert.Equal(2, sent.Count); Assert.True((sent[0].Flags & VoiceFrameFlags.Marker) != 0); Assert.Equal(VoiceFrameFlags.None, sent[1].Flags & VoiceFrameFlags.Marker);
|
||||
LocalAudioDiagnostics diagnostic = engine.GetLocalDiagnostics(1);
|
||||
Assert.Equal(3, diagnostic.Cycles); Assert.Equal(1, diagnostic.StarvedCycles); Assert.Equal(2, diagnostic.EncodedPackets);
|
||||
for (int i = 0; i < 10; i++) engine.ProcessCycle();
|
||||
engine.FeedPcm(1, tone, 1); engine.ProcessCycle();
|
||||
Assert.True((sent[^1].Flags & VoiceFrameFlags.Marker) != 0);
|
||||
@@ -193,6 +268,14 @@ public class AudioEngineTests
|
||||
Assert.True(energy > 100000);
|
||||
}
|
||||
|
||||
private static string FindRoot()
|
||||
{
|
||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "VoiceCat.slnx")))
|
||||
directory = directory.Parent;
|
||||
return directory?.FullName ?? throw new DirectoryNotFoundException("Repository root not found.");
|
||||
}
|
||||
|
||||
private sealed class ManualAudioClock : TimeProvider
|
||||
{
|
||||
private long milliseconds;
|
||||
|
||||
Reference in New Issue
Block a user