Files
voice-cat/tests/VoiceCat.Tests/CodecTests.cs
T

124 lines
5.5 KiB
C#
Raw Normal View History

using VoiceCat.Codec;
namespace VoiceCat.Tests;
public sealed class CodecTests
{
public static IEnumerable<object[]> 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<ArgumentOutOfRangeException>(() => new OpusEncoder(new() { Channels = 3 }));
Assert.Throws<ArgumentOutOfRangeException>(() => new OpusEncoder(new() { FrameDurationMilliseconds = 30 }));
using var encoder = new OpusEncoder();
using var decoder = new OpusDecoder();
Assert.Throws<ArgumentException>(() => encoder.Encode(new short[959], new byte[4000]));
Assert.Throws<ArgumentException>(() => decoder.Decode([], new short[959], 960));
encoder.Dispose();
Assert.Throws<ObjectDisposedException>(() => 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<NotSupportedException>(() => new OpusEncoder(new() { DeepRedundancy = true }));
Assert.Throws<NotSupportedException>(() => 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<NotSupportedException>(() => 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<short> 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<short> pcm)
{
double sum = 0;
foreach (short value in pcm) sum += (double)value * value;
return Math.Sqrt(sum / pcm.Length);
}
}