133 lines
5.9 KiB
C#
133 lines
5.9 KiB
C#
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<ArgumentOutOfRangeException>(() => encoder.SetExpectedPacketLossPercent(101));
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|