using VoiceCat.Codec; namespace VoiceCat.Tests; public sealed class CodecTests { [Fact] public void ExpectedPacketLossCanChangeWithoutRecreatingEncoder() { using var encoder = new OpusEncoder(new() { ExpectedPacketLossPercent = 5 }); encoder.SetExpectedPacketLossPercent(17); Assert.Equal(17, encoder.ExpectedPacketLossPercent); Assert.Throws(() => encoder.SetExpectedPacketLossPercent(101)); } public static IEnumerable Formats() { foreach (int rate in new[] { 8000, 12000, 16000, 24000, 48000 }) foreach (int channels in new[] { 1, 2 }) foreach (int duration in new[] { 10, 20, 40, 60 }) yield return [rate, channels, duration]; } [Theory] [MemberData(nameof(Formats))] public void RoundTripAndLossConcealment(int sampleRate, int channels, int duration) { var options = new OpusOptions { SampleRate = sampleRate, Channels = channels, FrameDurationMilliseconds = duration, Bitrate = 64000 }; using var encoder = new OpusEncoder(options); using var decoder = new OpusDecoder(sampleRate, channels); short[] input = new short[options.SamplesPerChannel * channels]; short[] output = new short[input.Length]; byte[] packet = new byte[4000]; for (int frame = 0; frame < 12; frame++) { FillTone(input, options.SamplesPerChannel, channels, sampleRate, frame); int bytes = encoder.Encode(input, packet); Assert.InRange(bytes, 1, packet.Length); Assert.Equal(options.SamplesPerChannel, decoder.Decode(packet.AsSpan(0, bytes), output, options.SamplesPerChannel)); } double rms = Rms(output); Assert.InRange(rms, 2000, 12000); Assert.Equal(options.SamplesPerChannel, decoder.Decode([], output, options.SamplesPerChannel)); Assert.True(Rms(output) > 100); } [Fact] public void RejectsInvalidStorageAndOptionsBeforeNativeCalls() { Assert.Throws(() => new OpusEncoder(new() { Channels = 3 })); Assert.Throws(() => new OpusEncoder(new() { FrameDurationMilliseconds = 30 })); using var encoder = new OpusEncoder(); using var decoder = new OpusDecoder(); Assert.Throws(() => encoder.Encode(new short[959], new byte[4000])); Assert.Throws(() => decoder.Decode([], new short[959], 960)); encoder.Dispose(); Assert.Throws(() => encoder.Encode(new short[960], new byte[4000])); } [Fact] public void DredIsExplicitlySupportedOrRejected() { using var probe = new OpusEncoder(); Assert.Contains("libopus", OpusEncoder.Version); if (!probe.SupportsDeepRedundancy) { Assert.Throws(() => new OpusEncoder(new() { DeepRedundancy = true })); Assert.Throws(() => new OpusDeepRedundancy()); return; } VerifyDredRecovery(new() { DeepRedundancy = true, ExpectedPacketLossPercent = 20, Bitrate = 64000 }); } [Theory] [MemberData(nameof(Formats))] public void DredRecoversDroppedFrames(int sampleRate, int channels, int duration) { using var probe = new OpusEncoder(); Assert.True(probe.SupportsDeepRedundancy, "Build native bindings with scripts/build-native.ps1 for DRED recovery tests."); if (sampleRate < 16000) Assert.Throws(() => new OpusEncoder(new() { SampleRate = sampleRate, DeepRedundancy = true })); VerifyDredRecovery(new() { SampleRate = sampleRate, Channels = channels, FrameDurationMilliseconds = duration, DeepRedundancy = true, ExpectedPacketLossPercent = 20, Bitrate = 64000 }); } private static void VerifyDredRecovery(OpusOptions options) { // The pinned encoder cannot emit DRED at 8/12 kHz; packets can still be decoded at those rates. var encoderOptions = options with { SampleRate = Math.Max(16000, options.SampleRate) }; using var encoder = new OpusEncoder(encoderOptions); using var decoder = new OpusDecoder(options.SampleRate, options.Channels); using var recovery = new OpusDeepRedundancy(); short[] input = new short[encoderOptions.SamplesPerChannel * options.Channels]; short[] output = new short[options.SamplesPerChannel * options.Channels]; byte[] packet = new byte[4000]; bool missing = false; int recovered = 0; for (int frame = 0; frame < 40; frame++) { FillTone(input, encoderOptions.SamplesPerChannel, options.Channels, encoderOptions.SampleRate, frame); int bytes = encoder.Encode(input, packet); if (missing) { Assert.True(recovery.TryRecover(decoder, packet.AsSpan(0, bytes), output, options.SamplesPerChannel)); Assert.True(Rms(output) > 10); recovered++; missing = false; } if (frame > 20 && frame % 5 == 0) { missing = true; continue; } decoder.Decode(packet.AsSpan(0, bytes), output, options.SamplesPerChannel); } Assert.Equal(3, recovered); } internal static void FillTone(Span pcm, int samples, int channels, int rate, int frame) { for (int i = 0; i < samples; i++) for (int channel = 0; channel < channels; channel++) pcm[i * channels + channel] = (short)(8000 * Math.Sin(2 * Math.PI * (440 + 220 * channel) * (frame * samples + i) / rate)); } internal static double Rms(ReadOnlySpan pcm) { double sum = 0; foreach (short value in pcm) sum += (double)value * value; return Math.Sqrt(sum / pcm.Length); } }