using System.Net; using System.Net.Sockets; using VoiceCat.Audio; using VoiceCat.Codec; using VoiceCat.Core; using VoiceCat.Crypto; using VoiceCat.Protocol; using VoiceCat.Transport; using Voicecat.V1; 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); } [Fact] public void MediaSenderWakesOnQueueSignalInsteadOfTimedPolling() { string source = File.ReadAllText(Path.Combine(FindRoot(), "src", "VoiceCat.Core", "ClientMediaTransport.cs")); Assert.Contains("sendReady.WaitOne(", source); Assert.Contains("senderNapping", source); Assert.DoesNotContain("Thread.Sleep(1);", source); } [Fact] public async Task MediaSenderDeliversQueuedVoicePromptlyWithoutTimedPolling() { // A loopback "relay" answers the binding so the sender's own keepalive pass falls back to // its five second schedule: only the queue signal can then deliver queued voice promptly, // so a lost wake or a polling loop shows up here as a multi-second delay. using var relay = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); relay.Bind(new IPEndPoint(IPAddress.Loopback, 0)); byte[] key = new byte[32]; Random.Shared.NextBytes(key); using var crypto = new MediaSessionCrypto(new MediaEncryptor(key), new MediaDecryptor(key)); await using var transport = new ClientMediaTransport((IPEndPoint)relay.LocalEndPoint!, new byte[16], crypto, CancellationToken.None); byte[] datagram = new byte[512]; using (var startup = new CancellationTokenSource(TimeSpan.FromSeconds(5))) { SocketReceiveFromResult probe = await relay.ReceiveFromAsync(datagram, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), startup.Token); Assert.Equal(VoiceFrameHeader.Size + 16, probe.ReceivedBytes); byte[] keepalive = new byte[VoiceFrameHeader.Size]; new VoiceFrameHeader(MediaFrameType.Keepalive, 0, 0, 0, 0, 0).Write(keepalive); await relay.SendToAsync(keepalive, SocketFlags.None, probe.RemoteEndPoint); } await transport.Bound.WaitAsync(TimeSpan.FromSeconds(5)); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); Assert.True(transport.TrySend(new(MediaFrameType.Voice, VoiceFrameFlags.None, 0, 7, 0, 0), new byte[80])); await ReceiveVoiceAsync(relay, datagram, 1, TimeSpan.FromSeconds(2)); Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(2)); // A burst queued while the sender naps must drain in one wake, not one keepalive pass. stopwatch.Restart(); for (uint i = 1; i <= 30; i++) Assert.True(transport.TrySend(new(MediaFrameType.Voice, VoiceFrameFlags.None, 0, 7, 0, i * 960), new byte[80])); await ReceiveVoiceAsync(relay, datagram, 30, TimeSpan.FromSeconds(2)); Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(2)); } private static async Task ReceiveVoiceAsync(Socket relay, byte[] datagram, int count, TimeSpan timeout) { int voice = VoiceFrameHeader.Size + 80 + MediaEncryptor.TagSize; using var deadline = new CancellationTokenSource(timeout); while (count > 0) { SocketReceiveFromResult packet = await relay.ReceiveFromAsync(datagram, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), deadline.Token); if (packet.ReceivedBytes == voice) count--; } } [Theory] [InlineData(-1000)] [InlineData(1000)] public void AdaptivePcmBufferAbsorbsIndependentClockDrift(int partsPerMillion) { var buffer = new AdaptivePcmBuffer(1, 40); short[] input = new short[962], output = new short[960]; input.AsSpan().Fill(1234); Assert.True(buffer.TryWrite(input.AsSpan(0, 960))); Assert.True(buffer.TryWrite(input.AsSpan(0, 960))); double produced = 0; for (int cycle = 0; cycle < 10_000; cycle++) { produced += 960 * (1 + partsPerMillion / 1_000_000.0); int frames = (int)produced; produced -= frames; Assert.True(buffer.TryWrite(input.AsSpan(0, frames))); Assert.Equal(output.Length, buffer.Read(output)); } Assert.InRange(buffer.CountFrames, 480, 3840); long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 100; i++) { buffer.TryWrite(input.AsSpan(0, 960)); buffer.Read(output); } 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); } [Theory] [InlineData(-1000)] [InlineData(1000)] public void AdaptivePcmBufferKeepsBurstingIosCaptureBoundedOverLongCalls(int partsPerMillion) { var buffer = new AdaptivePcmBuffer(1, 120, 65_536); short[] callback = new short[4_800], encoded = new short[960]; long produced = 0; int lateStarvation = 0; for (long consumerFrame = 0; consumerFrame < 48_000L * 600; consumerFrame += 960) { while (produced / (1 + partsPerMillion / 1_000_000.0) <= consumerFrame) { Assert.True(buffer.TryWrite(callback)); produced += callback.Length; } if (buffer.Read(encoded) == 0 && consumerFrame >= 48_000) lateStarvation++; Assert.InRange(buffer.CountFrames, 0, 20_000); } Assert.Equal(0, lateStarvation); } [Theory] [InlineData(20)] [InlineData(40)] [InlineData(120)] public void AdaptivePcmBufferResynchronizeDropsStallBacklogToTheTarget(int bufferMilliseconds) { var buffer = new AdaptivePcmBuffer(1, bufferMilliseconds, 65_536); short[] chunk = new short[960], output = new short[960]; chunk.AsSpan().Fill(1234); // A stalled consumer leaves every chunk queued: feed and drain rates are equal in steady // state, so this backlog would otherwise sit at a fixed offset until overflow drops begin. for (int i = 0; i < 50; i++) Assert.True(buffer.TryWrite(chunk)); Assert.Equal(50 * 960, buffer.CountFrames); buffer.Resynchronize(); Assert.Equal(bufferMilliseconds * 48, buffer.CountFrames); Assert.Equal(960, buffer.Read(output)); Assert.All(output, value => Assert.Equal(1234, value)); // At or below the target the resynchronization drops nothing. buffer.Resynchronize(); Assert.Equal(bufferMilliseconds * 48 - 960, buffer.CountFrames); } [Fact] public void OneCaptureMissDoesNotRestartTalkspurtButSustainedStarvationDoes() { var sent = new List<(uint Timestamp, VoiceFrameFlags Flags)>(); using var engine = new AudioEngine((_, timestamp, _, flags) => { sent.Add((timestamp, flags)); return true; }, false) { InputMode = AudioInputMode.AlwaysOn, DeviceBufferMilliseconds = 20 }; engine.AddLocalStream(Stream()); short[] tone = Tone(); engine.FeedPcm(1, tone, 1); engine.ProcessCycle(); 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); } [Fact] public void AutomaticPacketLossUpdatesOnAudioOwnerAndManualStreamsIgnoreIt() { StreamInfo automatic = Stream(); automatic.Audio.PacketLossMode = PacketLossMode.PacketLossAutoBalanced; using var engine = new AudioEngine((_, _, _, _) => true, false); engine.AddLocalStream(automatic); engine.SetExpectedPacketLoss(7); Assert.Equal(20, engine.GetAppliedExpectedPacketLoss(1)); engine.ProcessCycle(); Assert.Equal(7, engine.GetAppliedExpectedPacketLoss(1)); engine.RemoveLocalStream(1); engine.ProcessCycle(); engine.AddLocalStream(Stream()); engine.SetExpectedPacketLoss(4); engine.ProcessCycle(); Assert.Equal(20, engine.GetAppliedExpectedPacketLoss(1)); } [Theory] [InlineData(5)] [InlineData(10)] [InlineData(20)] [InlineData(40)] [InlineData(60)] public void RecoveryLookaheadTracksChannelFrameDuration(int frameMilliseconds) { using var stream = new ReceiveStream(2, Stream(frameMilliseconds)); Assert.Equal(frameMilliseconds * 48, stream.TargetDepthSamples); } [Fact] public void JitterTargetAdaptsInSampleTimeAndRemainsCapped() { var clock = new ManualAudioClock(); StreamInfo info = Stream(); info.Audio.Fec = false; using var stream = new ReceiveStream(2, info, clock); using var encoder = new OpusEncoder(new() { Bitrate = 32000 }); byte[] packet = new byte[1275]; int length = encoder.Encode(Tone(), packet); for (uint i = 0; i < 40; i++) { clock.Advance(i % 2 == 0 ? 5 : 35); stream.Enqueue(new(MediaFrameType.Voice, 0, 0, 42, i, i * 960), packet.AsSpan(0, length)); } int[] output = new int[1920]; stream.Mix(output, false, null); Assert.InRange(stream.TargetDepthSamples, 960, 5760); } [Fact] public void DredUsesTimestampOffsetForConsecutiveMissingShortFrames() { using var probe = new OpusEncoder(); if (!probe.SupportsDeepRedundancy) return; StreamInfo info = Stream(10, dred: true); using var stream = new ReceiveStream(2, info); using var encoder = new OpusEncoder(new() { FrameDurationMilliseconds = 10, DeepRedundancy = true, ExpectedPacketLossPercent = 30, Bitrate = 64000 }); byte[] packet = new byte[1275]; short[] tone = new short[480]; int[] output = new int[1920]; for (uint i = 0; i < 50; i++) { CodecTests.FillTone(tone, 480, 1, 48000, (int)i); int length = encoder.Encode(tone, packet); if (i is not (25 or 26)) stream.Enqueue(new(MediaFrameType.Voice, 0, 0, 42, i, i * 480), packet.AsSpan(0, length)); if (i % 2 == 1) { output.AsSpan().Clear(); stream.Mix(output, false, null); } } Assert.True(stream.DredFrames >= 2); } [Theory] [InlineData(true)] [InlineData(false)] public void LostFramesUseDredThenFecBeforeBoundedPlc(bool useDred) { using var receive = new ReceiveStream(2, Stream(dred: useDred)); using var encoder = new OpusEncoder(new() { DeepRedundancy = useDred, ForwardErrorCorrection = true, ExpectedPacketLossPercent = 30, Complexity = 10, Bitrate = 64000 }); byte[] packet = new byte[1275]; short[] tone = Tone(); int[] output = new int[1920]; for (uint i = 0; i < 40; i++) { CodecTests.FillTone(tone, 960, 1, 48000, (int)i); int size = encoder.Encode(tone, packet); if (i != 25 && i != 30 && i != 35) receive.Enqueue(new(MediaFrameType.Voice, 0, 0, 42, i, i * 960), packet.AsSpan(0, size)); output.AsSpan().Clear(); receive.Mix(output, false, null); } if (useDred) Assert.True(receive.DredFrames > 0); else Assert.True(receive.FecFrames > 0); for (int i = 0; i < 20; i++) { output.AsSpan().Clear(); receive.Mix(output, false, null); } Assert.True(receive.ConcealedFrames > 0); Assert.All(output, sample => Assert.Equal(0, sample)); } [Fact] public void PcmRingDropsWholeFramesWhenFullAndPreservesOrderAcrossWraps() { var ring = new PcmRing(8); short[] output = new short[8]; for (int i = 0; i < 100; i++) { Assert.True(ring.TryWrite([1, 2, 3, 4, 5, 6])); Assert.False(ring.TryWrite([7, 8, 9])); Assert.Equal(4, ring.Read(output.AsSpan(0, 4))); Assert.Equal(new short[] { 1, 2, 3, 4 }, output[..4]); Assert.True(ring.TryWrite([7, 8])); Assert.Equal(4, ring.Read(output)); Assert.Equal(new short[] { 5, 6, 7, 8 }, output[..4]); Assert.Equal(0, ring.Count); } } internal static StreamInfo Stream(int frame = 20, bool stereo = false, bool dred = false) => new() { StreamId = 1, Ssrc = 42, Kind = StreamKind.StreamMic, Audio = new() { SampleRate = 48000, BitrateBps = 32000, FrameMs = (uint)frame, Complexity = 5, Mode = stereo ? ChannelMode.ModeStereo : ChannelMode.ModeMono, Fec = true, ExpectedPacketLoss = 20, Dred = dred } }; private static short[] Tone(int channels = 1) { var pcm = new short[960 * channels]; for (int i = 0; i < 960; i++) for (int c = 0; c < channels; c++) pcm[i * channels + c] = (short)(Math.Sin(i * 2 * Math.PI * (c == 0 ? 440 : 660) / 48000) * 8000); return pcm; } [Theory] [InlineData(5, false)] [InlineData(10, false)] [InlineData(20, false)] [InlineData(40, false)] [InlineData(60, false)] [InlineData(20, true)] public void ReframedEncodedPcmIsDecodedAndMixedForMonoAndStereo(int frame, bool stereo) { StreamInfo stream = Stream(frame, stereo); using var receive = new AudioEngine((_, _, _, _) => true, false); receive.SetRemoteStreams([new() { Id = 2, ChannelId = 1, Streams = { stream } }], 1, 1); using var send = new AudioEngine((ssrc, timestamp, payload, flags) => { receive.Receive(new(MediaFrameType.Voice, flags, 0, ssrc, 0, timestamp), payload); return true; }, false); send.InputMode = AudioInputMode.AlwaysOn; send.AddLocalStream(stream, stereo ? 2 : 1); long energy = 0; int sinkChannels = 0; receive.MixedPcm += pcm => { foreach (short sample in pcm) energy += Math.Abs((int)sample); }; receive.StreamPcm += (_, _, _, channels) => sinkChannels = channels; short[] tone = Tone(stereo ? 2 : 1); for (int i = 0; i < 30; i++) { Assert.True(send.FeedPcm(1, tone, stereo ? 2 : 1)); send.ProcessCycle(); receive.ProcessCycle(); } Assert.True(energy > 100000); Assert.Equal(stereo ? 2 : 1, sinkChannels); receive.SetRemotePlayback(2, 1, 1, true, false); energy = 0; for (int i = 0; i < 5; i++) { send.FeedPcm(1, tone, stereo ? 2 : 1); send.ProcessCycle(); receive.ProcessCycle(); } Assert.Equal(0, energy); } [Fact] public void AudioCyclesAllocateZeroBytesWithEncodeDecodeStereoNoiseReductionAndMixing() { StreamInfo stream = Stream(20, true); using var receive = new AudioEngine((_, _, _, _) => true, false); receive.SetRemoteStreams([new() { Id = 2, ChannelId = 1, Streams = { stream } }], 1, 1); receive.SetRemotePlayback(2, 1, 0.8f, false, true); using var send = new AudioEngine((ssrc, timestamp, payload, flags) => { receive.Receive(new(MediaFrameType.Voice, flags, 0, ssrc, 0, timestamp), payload); return true; }, false); send.InputMode = AudioInputMode.AlwaysOn; send.InputNoiseReduction = true; send.AddLocalStream(stream, 2); short[] tone = Tone(2); for (int i = 0; i < 30; i++) { send.FeedPcm(1, tone, 2); send.ProcessCycle(); receive.ProcessCycle(); } long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 100; i++) { send.FeedPcm(1, tone, 2); send.ProcessCycle(); receive.ProcessCycle(); } Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before); } [Fact] public void DeviceClockedRunCyclePacesMixAndCaptureWithNoWorkerThread() { var sent = new List<(uint Timestamp, VoiceFrameFlags Flags)>(512); StreamInfo stream = Stream(); using var receive = new AudioEngine((_, _, _, _) => true, false); receive.SetRemoteStreams([new() { Id = 2, ChannelId = 1, Streams = { stream } }], 1, 1); using var send = new AudioEngine((ssrc, timestamp, payload, flags) => { sent.Add((timestamp, flags)); receive.Receive(new(MediaFrameType.Voice, flags, 0, ssrc, 0, timestamp), payload); return true; }, false) { InputMode = AudioInputMode.AlwaysOn, DeviceBufferMilliseconds = 20 }; send.AddLocalStream(stream); short[] tone = Tone(); long energy = 0; receive.MixedPcm += pcm => { foreach (short sample in pcm) energy += Math.Abs((int)sample); }; // Each iteration is one 20 ms render demand quantum: feed the capture handoff, then pull // both mixes, exactly as the iOS render callback drives its device-clocked engine. for (int i = 0; i < 30; i++) { Assert.True(send.FeedPcm(1, tone, 1)); send.RunCycle(); receive.RunCycle(); } Assert.Equal(30, send.GetLocalDiagnostics(1).Cycles); Assert.Equal(0, send.GetLocalDiagnostics(1).StarvedCycles); Assert.Equal(30, sent.Count); Assert.True(energy > 100000); long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 100; i++) { send.FeedPcm(1, tone, 1); send.RunCycle(); receive.RunCycle(); } Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before); } [Fact] public void RunCycleContainsFailuresInsteadOfThrowingIntoRealtimeCallbacks() { using var engine = new AudioEngine((_, _, _, _) => throw new InvalidOperationException("boom"), false) { InputMode = AudioInputMode.AlwaysOn, DeviceBufferMilliseconds = 20 }; engine.AddLocalStream(Stream()); Assert.True(engine.FeedPcm(1, Tone(), 1)); engine.RunCycle(); Assert.IsType(engine.Failure); engine.RunCycle(); Assert.Equal(1, engine.GetLocalDiagnostics(1).Cycles); } [Fact] public async Task DeviceClockedClientLeavesMixCadenceToTheAudioCallback() { string directory = Path.Combine(Path.GetTempPath(), $"voicecat-{Guid.NewGuid():N}"); Directory.CreateDirectory(directory); try { await using var paced = new VoiceCatClient(tofuStorePath: Path.Combine(directory, "paced.txt")); await using var pulled = new VoiceCatClient(tofuStorePath: Path.Combine(directory, "pulled.txt"), deviceClockedAudio: true); paced.Audio.AddLocalStream(Stream()); pulled.Audio.AddLocalStream(Stream()); await Task.Delay(250); Assert.True(paced.Audio.GetLocalDiagnostics(1).Cycles > 0); Assert.Equal(0, pulled.Audio.GetLocalDiagnostics(1).Cycles); pulled.Audio.RunCycle(); Assert.Equal(1, pulled.Audio.GetLocalDiagnostics(1).Cycles); } finally { try { Directory.Delete(directory, true); } catch (IOException) { } catch (UnauthorizedAccessException) { } } } [Fact] public void ManagedAudioWorkerResynchronizesProducerBacklogAfterStalls() { string source = File.ReadAllText(Path.Combine(FindRoot(), "src", "VoiceCat.Audio", "AudioEngine.cs")); string buffer = File.ReadAllText(Path.Combine(FindRoot(), "src", "VoiceCat.Audio", "AdaptivePcmBuffer.cs")); Assert.Contains("stream.Input.Resynchronize()", source); Assert.Contains("public void Resynchronize()", buffer); } [Fact] public void ResynchronizeInputsDropsStalledBacklogToTheBufferTarget() { using var send = new AudioEngine((_, _, _, _) => true, false); send.DeviceBufferMilliseconds = 40; send.AddLocalStream(Stream()); short[] mono = Tone(); for (int i = 0; i < 40; i++) send.FeedPcm(1, mono, 1); Assert.True(send.GetLocalDiagnostics(1).BufferedFrames > 1920 * 2); send.ResynchronizeInputs(); Assert.Equal(1920, send.GetLocalDiagnostics(1).BufferedFrames); send.ProcessCycle(); Assert.Equal(0, send.GetLocalDiagnostics(1).StarvedCycles); } [Fact] public void RemotePlaybackSettingsAreIndependentForEachUserStream() { StreamInfo microphone = Stream(); StreamInfo screen = Stream(); screen.StreamId = 2; screen.Ssrc = 43; screen.Kind = StreamKind.StreamScreenAudio; screen.Label = "Screen audio"; using var receive = new AudioEngine((_, _, _, _) => true, false); receive.SetRemoteStreams([new() { Id = 7, ChannelId = 1, Streams = { microphone, screen } }], 1, 1); receive.SetRemotePlayback(7, microphone.StreamId, 0.5f, true, true); receive.SetRemotePlayback(7, screen.StreamId, 1.75f, false, false); Assert.Equal((0.5f, true, true), receive.GetRemotePlayback(7, microphone.StreamId)); Assert.Equal((1.75f, false, false), receive.GetRemotePlayback(7, screen.StreamId)); } [Fact] public void JitterBacklogIsBoundedAndPlcEventuallyBecomesSilence() { var info = Stream(); using var stream = new ReceiveStream(2, info); using var encoder = new OpusEncoder(new() { Bitrate = 32000 }); byte[] packet = new byte[1275]; int length = encoder.Encode(Tone(), packet); int[] output = new int[1920]; for (uint i = 0; i < 64; i++) stream.Enqueue(new(MediaFrameType.Voice, 0, 0, 42, i, i * 960), packet.AsSpan(0, length)); stream.Mix(output, false, null); Assert.InRange(stream.Depth, 0, 6); for (int i = 0; i < 20; i++) { output.AsSpan().Clear(); stream.Mix(output, false, null); } Assert.All(output, value => Assert.Equal(0, value)); Assert.InRange(stream.ConcealedFrames, 1, 10); } [Fact] public void PttResumeAndCaptureChannelChangesKeepTimestampProgressAndAudio() { var info = Stream(60, true); using var receive = new AudioEngine((_, _, _, _) => true, false); receive.SetRemoteStreams([new() { Id = 2, ChannelId = 1, Streams = { info } }], 1, 1); using var send = new AudioEngine((ssrc, timestamp, payload, flags) => { receive.Receive(new(MediaFrameType.Voice, flags, 0, ssrc, 0, timestamp), payload); return true; }, false); send.InputMode = AudioInputMode.PushToTalk; send.PushToTalk = true; send.AddLocalStream(info); short[] mono = Tone(), stereo = Tone(2); long energy = 0; receive.MixedPcm += pcm => { foreach (short value in pcm) energy += Math.Abs((int)value); }; for (int i = 0; i < 15; i++) { send.FeedPcm(1, mono, 1); send.ProcessCycle(); receive.ProcessCycle(); } send.PushToTalk = false; for (int i = 0; i < 16; i++) { send.FeedPcm(1, mono, 1); send.ProcessCycle(); receive.ProcessCycle(); } send.SetCaptureChannels(1, 2); send.PushToTalk = true; energy = 0; for (int i = 0; i < 15; i++) { send.FeedPcm(1, stereo, 2); send.ProcessCycle(); receive.ProcessCycle(); } 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; public override long TimestampFrequency => 1000; public override long GetTimestamp() => milliseconds; internal void Advance(int value) => milliseconds += value; } }