aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core.Tests/Audio
diff options
context:
space:
mode:
Diffstat (limited to 'src/Godosa.Core.Tests/Audio')
-rw-r--r--src/Godosa.Core.Tests/Audio/AudioMixTests.cs22
-rw-r--r--src/Godosa.Core.Tests/Audio/Mp3Tests.cs90
-rw-r--r--src/Godosa.Core.Tests/Audio/SynthTests.cs122
-rw-r--r--src/Godosa.Core.Tests/Audio/WavReaderTests.cs127
4 files changed, 361 insertions, 0 deletions
diff --git a/src/Godosa.Core.Tests/Audio/AudioMixTests.cs b/src/Godosa.Core.Tests/Audio/AudioMixTests.cs
new file mode 100644
index 0000000..93a0bed
--- /dev/null
+++ b/src/Godosa.Core.Tests/Audio/AudioMixTests.cs
@@ -0,0 +1,22 @@
+using Godosa.Core.Audio;
+using Xunit;
+
+namespace Godosa.Core.Tests.Audio;
+
+public class AudioMixTests
+{
+ private sealed class Plain : IAudioMix
+ {
+ public HrtfMode Hrtf => default;
+ public float ChannelGain(int channel, AudioPauseState state) => 1f;
+ public bool Pausable(int channel) => true;
+ public bool WorldPaused(AudioPauseState state) => false;
+ }
+
+ [Fact]
+ public void ChannelName_DefaultsToTheNumber()
+ {
+ IAudioMix mix = new Plain();
+ Assert.Equal("2", mix.ChannelName(2));
+ }
+}
diff --git a/src/Godosa.Core.Tests/Audio/Mp3Tests.cs b/src/Godosa.Core.Tests/Audio/Mp3Tests.cs
new file mode 100644
index 0000000..9cdca1e
--- /dev/null
+++ b/src/Godosa.Core.Tests/Audio/Mp3Tests.cs
@@ -0,0 +1,90 @@
+using System;
+using Godosa.Core.Audio;
+using Xunit;
+
+namespace Godosa.Core.Tests.Audio;
+
+/// <summary>Fixtures: our own sine tones (ffmpeg lavfi sine, amplitude 1/8, 0.25 s) encoded with lame 3.100: mono
+/// 440 Hz at 22050 Hz (64 kbit/s, --resample 22.05); stereo 440 Hz left / 880 Hz right at 44100 Hz (128 kbit/s,
+/// --resample 44.1). Oracle: a sine of f Hz crosses zero 2·f times a second; counted in a 0.1 s window inside the tone
+/// (lame's encoder delay, ≈ 1105 samples, and frame padding are near-silent and cross zero at random).</summary>
+public class Mp3Tests
+{
+ private static readonly byte[] Mono = Convert.FromBase64String("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");
+ private static readonly byte[] Stereo = Convert.FromBase64String("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");
+
+ private static (float[] Left, float[] Right) Render(Mp3Stream s, int frames)
+ {
+ var buf = new float[2 * frames];
+ s.Render(buf, frames);
+ var (l, r) = (new float[frames], new float[frames]);
+ for (var i = 0; i < frames; i++)
+ (l[i], r[i]) = (buf[2 * i], buf[2 * i + 1]);
+ return (l, r);
+ }
+
+ [Fact]
+ public void Reader_decodes_mono_at_the_file_rate()
+ {
+ var clip = Mp3Reader.Read(Mono)!.Value;
+ Assert.Equal(22050, clip.SampleRate);
+ Assert.InRange(clip.Samples.Length, 5512, 5512 + 3 * 1152);
+ var f = Array.ConvertAll(clip.Samples, s => s / 32767f);
+ Assert.InRange(SignalStats.ZeroCrossings(f, 2400, 2400 + 2205), 86, 90); // 0.1 s of 440 Hz → 88
+ }
+
+ [Fact]
+ public void Reader_downmixes_stereo_to_mono()
+ {
+ var clip = Mp3Reader.Read(Stereo)!.Value;
+ Assert.Equal(44100, clip.SampleRate);
+ Assert.InRange(clip.Samples.Length, 11025, 11025 + 3 * 1152);
+ }
+
+ [Fact]
+ public void Reader_rejects_garbage() => Assert.Null(Mp3Reader.Read(new byte[300]));
+
+ /// <summary>Oracle: both tones are 0.25 s; the LAME tag's encoder delay and padding are not counted (the Info header's
+ /// 12 and 11 whole frames would be 0.313 and 0.287 s). Within 1 sample of rounding.</summary>
+ [Fact]
+ public void Seconds_come_from_the_headers_without_encoder_delay()
+ {
+ Assert.InRange(Mp3Reader.Seconds(Mono)!.Value, 0.25, 0.25 + 1 / 22050.0);
+ Assert.InRange(Mp3Reader.Seconds(Stereo)!.Value, 0.25, 0.25 + 1 / 44100.0);
+ Assert.Null(Mp3Reader.Seconds(new byte[300]));
+ }
+
+ [Fact]
+ public void Stream_keeps_channels_apart()
+ {
+ using var s = new Mp3Stream(Stereo, loop: false);
+ var (l, r) = Render(s, 11025);
+ Assert.InRange(SignalStats.ZeroCrossings(l, 4000, 4000 + 4410), 86, 90); // 440 Hz → 88 in 0.1 s
+ Assert.InRange(SignalStats.ZeroCrossings(r, 4000, 4000 + 4410), 174, 178); // 880 Hz → 176
+ }
+
+ [Fact]
+ public void Stream_resamples_mono_to_44100_and_duplicates_it()
+ {
+ using var s = new Mp3Stream(Mono, loop: false);
+ var (l, r) = Render(s, 11025);
+ Assert.InRange(SignalStats.ZeroCrossings(l, 4000, 4000 + 4410), 86, 90); // still 440 Hz (176 if played as 44.1 kHz)
+ Assert.Equal(l, r);
+ }
+
+ [Fact]
+ public void Once_only_goes_silent_and_finishes_looping_does_not()
+ {
+ using var once = new Mp3Stream(Mono, loop: false);
+ Render(once, 44100); // 1 s, past the 0.25 s file
+ Assert.True(once.Finished);
+ var tail = new float[200];
+ Array.Fill(tail, 1f); // the stream must write the silence, not rely on a cleared buffer
+ once.Render(tail, 100);
+ Assert.All(tail, x => Assert.Equal(0f, x));
+ using var loop = new Mp3Stream(Mono, loop: true);
+ var (l, _) = Render(loop, 44100);
+ Assert.False(loop.Finished);
+ Assert.InRange(SignalStats.Rms(l, 22050), 0.05f, 0.1f); // second half still sounding (sine 1/8: RMS 0.088 less gaps)
+ }
+}
diff --git a/src/Godosa.Core.Tests/Audio/SynthTests.cs b/src/Godosa.Core.Tests/Audio/SynthTests.cs
new file mode 100644
index 0000000..7c2c29b
--- /dev/null
+++ b/src/Godosa.Core.Tests/Audio/SynthTests.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Linq;
+using Godosa.Core.Audio;
+using Xunit;
+
+namespace Godosa.Core.Tests.Audio;
+
+public class SynthTests
+{
+ [Fact]
+ public void Random_IsDeterministicAndInUnitRange()
+ {
+ var a = new SynthRandom(42);
+ var b = new SynthRandom(42);
+ var values = Enumerable.Range(0, 1000).Select(_ => a.Next01()).ToArray();
+
+ Assert.Equal(values, Enumerable.Range(0, 1000).Select(_ => b.Next01()).ToArray());
+ Assert.All(values, v => Assert.InRange(v, 0f, 0.99999f));
+ Assert.InRange(values.Average(), 0.45f, 0.55f);
+ }
+
+ [Fact]
+ public void Random_IsXorshift32OfTheMixedSeed() // pinned: seeded songs must sound the same in every run
+ {
+ // oracle: independent Python murmur3 fmix32 + xorshift32 — fmix32(1) = 0x514E28B7, first state 0x1F48D1FB
+ Assert.Equal(0x514E28B7u, SynthRandom.Mix(1));
+ Assert.Equal(2050257f / 16777216f, new SynthRandom(1).Next01());
+ }
+
+ [Fact]
+ public void Random_NearbySeeds_StartFarApart()
+ {
+ var firsts = Enumerable.Range(0, 20).Select(s => new SynthRandom((uint)s).Next01()).ToList();
+
+ Assert.True(firsts.Max() - firsts.Min() > 0.5f);
+ }
+
+ [Fact]
+ public void Loop_CrossfadesTheTailIntoTheHead()
+ {
+ var x = new float[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+
+ var y = Synth.Loop(x, overlapSamples: 2);
+
+ Assert.Equal(new float[] { 8, 5, 2, 3, 4, 5, 6, 7 }, y); // y[^1] = 7 wraps to y[0] = 8, like x[7] → x[8]
+ }
+
+ [Fact]
+ public void Hash_IsFnv1a() // stable across runs, unlike string.GetHashCode
+ {
+ Assert.Equal(0xE40C292Cu, SynthRandom.Hash("a"));
+ Assert.Equal(0x1A47E90Bu, SynthRandom.Hash("abc"));
+ }
+
+ [Fact]
+ public void Sine_SteadyPitch()
+ {
+ var x = Synth.Sine(1f, 100f);
+
+ Assert.Equal(44100, x.Length);
+ Assert.InRange(SignalStats.ZeroCrossings(x), 198, 201);
+ }
+
+ [Fact]
+ public void Sine_SweepIsExponential()
+ {
+ var x = Synth.Sine(1f, 100f, 400f); // f = 100·4^t: 144 crossings in the first half, 289 in the second
+
+ Assert.InRange(SignalStats.ZeroCrossings(x, 0, 22050), 141, 148);
+ Assert.InRange(SignalStats.ZeroCrossings(x, 22050), 285, 292);
+ }
+
+ [Fact]
+ public void LowPass_PassesLowCutsHigh()
+ {
+ float low = SignalStats.Rms(Synth.Sine(0.5f, 200f).LowPass(1000f), 2000) / SignalStats.Rms(Synth.Sine(0.5f, 200f), 2000);
+ float high = SignalStats.Rms(Synth.Sine(0.5f, 8000f).LowPass(1000f), 2000) / SignalStats.Rms(Synth.Sine(0.5f, 8000f), 2000);
+
+ Assert.InRange(low, 0.89f, 1.05f); // within 1 dB
+ Assert.InRange(high, 0f, 0.1f); // −20 dB or more (2nd order: ≈ −36 dB at 3 octaves)
+ }
+
+ [Fact]
+ public void HighPass_PassesHighCutsLow()
+ {
+ float low = SignalStats.Rms(Synth.Sine(0.5f, 100f).HighPass(1000f), 4000) / SignalStats.Rms(Synth.Sine(0.5f, 100f), 4000);
+ float high = SignalStats.Rms(Synth.Sine(0.5f, 6000f).HighPass(1000f), 4000) / SignalStats.Rms(Synth.Sine(0.5f, 6000f), 4000);
+
+ Assert.InRange(low, 0f, 0.05f);
+ Assert.InRange(high, 0.89f, 1.05f);
+ }
+
+ [Fact]
+ public void BandPass_PassesCentreOnly()
+ {
+ float Ratio(float hz) => SignalStats.Rms(Synth.Sine(0.5f, hz).BandPass(1000f, 2f), 4000) / SignalStats.Rms(Synth.Sine(0.5f, hz), 4000);
+
+ Assert.InRange(Ratio(1000f), 0.85f, 1.05f);
+ Assert.InRange(Ratio(100f), 0f, 0.1f);
+ Assert.InRange(Ratio(8000f), 0f, 0.1f);
+ }
+
+ [Fact]
+ public void Perc_RisesThenDecaysByTimeConstant()
+ {
+ var x = Enumerable.Repeat(1f, 44100).ToArray().Perc(attack: 0.01f, decay: 0.1f);
+
+ Assert.Equal(0f, x[0]);
+ Assert.InRange(x[441], 0.99f, 1f); // top at the end of the attack
+ Assert.InRange(x[441 + 4410], 0.36f, 0.375f); // e^-1 one time constant later
+ }
+
+ [Fact]
+ public void ToPcm_NormalisesToLevel_AndFadesEnds()
+ {
+ var pcm = Synth.ToPcm(Enumerable.Repeat(0.3f, 4410).ToArray(), level: 0.5f);
+
+ Assert.Equal((short)16384, pcm.Max());
+ Assert.Equal(0, pcm[0]);
+ Assert.Equal(0, pcm[^1]);
+ }
+}
diff --git a/src/Godosa.Core.Tests/Audio/WavReaderTests.cs b/src/Godosa.Core.Tests/Audio/WavReaderTests.cs
new file mode 100644
index 0000000..6ed5485
--- /dev/null
+++ b/src/Godosa.Core.Tests/Audio/WavReaderTests.cs
@@ -0,0 +1,127 @@
+using System;
+using System.IO;
+using Godosa.Core.Audio;
+using Xunit;
+
+namespace Godosa.Core.Tests.Audio;
+
+public class WavReaderTests
+{
+ private static byte[] Wav(short format, short channels, int rate, short bits, byte[] data, bool extensible = false)
+ {
+ using var ms = new MemoryStream();
+ using var w = new BinaryWriter(ms);
+ int fmtSize = extensible ? 40 : 16;
+ w.Write("RIFF"u8); w.Write(4 + 8 + fmtSize + 8 + data.Length); w.Write("WAVE"u8);
+ w.Write("fmt "u8); w.Write(fmtSize);
+ w.Write(extensible ? (short)-2 : format); w.Write(channels); w.Write(rate);
+ w.Write(rate * channels * bits / 8); w.Write((short)(channels * bits / 8)); w.Write(bits);
+ if (extensible)
+ {
+ w.Write((short)22); w.Write(bits); w.Write(0);
+ w.Write((int)format); w.Write(new byte[12]); // sub-format GUID: first 4 bytes = format tag
+ }
+ w.Write("data"u8); w.Write(data.Length); w.Write(data);
+ return ms.ToArray();
+ }
+
+ private static byte[] Shorts(params short[] s)
+ {
+ var b = new byte[s.Length * 2];
+ Buffer.BlockCopy(s, 0, b, 0, b.Length);
+ return b;
+ }
+
+ [Fact]
+ public void Pcm16Stereo_DownmixesToMono()
+ {
+ var clip = WavReader.Read(Wav(1, 2, 22050, 16, Shorts(1000, 3000, -2000, -4000)));
+
+ Assert.NotNull(clip);
+ Assert.Equal(22050, clip!.Value.SampleRate);
+ Assert.Equal(new short[] { 2000, -3000 }, clip.Value.Samples);
+ }
+
+ [Fact]
+ public void Pcm24Mono()
+ {
+ // 0x400000 = +0.5 FS, 0xC00000 = −0.5 FS (little-endian 3-byte)
+ var clip = WavReader.Read(Wav(1, 1, 44100, 24, new byte[] { 0, 0, 0x40, 0, 0, 0xC0 }));
+
+ Assert.Equal(new short[] { 16384, -16384 }, clip!.Value.Samples);
+ }
+
+ [Fact]
+ public void Float32_Extensible()
+ {
+ var data = new byte[8];
+ BitConverter.GetBytes(0.5f).CopyTo(data, 0);
+ BitConverter.GetBytes(-1f).CopyTo(data, 4);
+
+ var clip = WavReader.Read(Wav(3, 1, 48000, 32, data, extensible: true));
+
+ Assert.Equal(new short[] { 16384, -32767 }, clip!.Value.Samples);
+ Assert.Equal(48000, clip.Value.SampleRate);
+ }
+
+ [Theory]
+ [InlineData(new byte[0])]
+ [InlineData(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 })]
+ public void Garbage_IsNull(byte[] bytes) => Assert.Null(WavReader.Read(bytes));
+
+ [Fact]
+ public void Pcm8_UnsignedCentredOn128()
+ {
+ var clip = WavReader.Read(Wav(1, 1, 8000, 8, new byte[] { 128, 192, 0 }));
+ Assert.Equal(new short[] { 0, 16384, -32768 }, clip!.Value.Samples);
+ }
+
+ // MS ADPCM fixtures: our own tones encoded by ffmpeg 7 (adpcm_ms, block 256, standard 7 coefficient pairs). Oracle:
+ // ffmpeg's own adpcm_ms decode (pcm_s16le), stereo averaged with truncation, cut to the fact chunk's frame count.
+ private static readonly byte[] AdpcmMono = Convert.FromBase64String("UklGRnQEAABXQVZFZm10IDIAAAACAAEAIlYAAIA+AAAAAQQAIAD0AQcAAAEAAAACAP8AAAAAwABAAPAAAADMATD/iAEY/2ZhY3QEAAAACAYAAExJU1QaAAAASU5GT0lTRlQNAAAATGF2ZjYxLjcuMTAzAABkYXRhAAQAAAAQAPwHAAB3d3MhAQAP/dzNvd3M7d3d3u8BNFNTRDNSMzQjIyEQ/tvM293M3dzd7d7vASREUzUzQ0MzNCIiAf7b283b3c3dzd7e7wEjVEQ1JDM0MzMzIhH+zL3MzdzN3dzt7f/xE1Q1NEM0M0MzMjIQDtrczM3czd3c7d7+ACNFQ0RDNDNDMzMiEQ7bzMzc3N3Nzt7e/gATRTRENDNDQyMzIhEO6829zN3M7c7t7u/xEkREQ0NEJCMjMiIgD+vb3M3Nzdzd3d7f8BNTVDRDQ0MzNCMiEQ7trb3b3c3c3d3d7/8iU1Q0Q0M0NCMjIiAf3bzM3M3czt3d7e7wEkU0REMASgLk/uj2QkIyMyIRH+zMzb3N3Nze3d3u8BI2Q0RDQzQzQyMiIQ/tvM292+3O3d3d7vADNjU0QzUjNCMyMiEP/Nrc3M3Nztzt7u7wETRTRENDNDQjMyIhH/zbzb3czd3N3d3e/xI1REQ0NDQzNCMjEgDtzL3M3Nzdze3d3+8RNUQ1NDQzQzNCIxEQ/czMzczdzc7d3t7/AjRFNTNDQ0MzMzMhEP28zNzM7N7O3e3u/wE0RERDQ0M0IzMzIhDuvbzc293c3dzt7v8BM1RENDRDM0IzMiIQ/sy9zNzc3N3d3d7vASRUNTQ0NDM0MjIiEP7a3Mzczd3N3O3u/wAjVDU0AEkC1PUA7jRCMzMzIhD+zMzM3M3dzc7e7u8AI1NTUzQ0MzQzIyIR/tvMzczc3c3dzu7vARNEU0NEM0M0MjIiEf/b283Nzc3N7c7u//ESREQ1M0M0M0IyIhAP3Mvc2+ztzt3e3u4AIlQ1NEM0NDMzMzIh/9vMzNzczs7e3e7uABNEU0Q0NDNDMzMjEQ7rzbzdvd3N3O3t/vESRERDRDNDQzIzIxEP3Mvczb7c7d3O3v4PEkQ1Q0NDQzQjMjIRD+2tvc3NzO3c7e7v8BFDVDRDQ0M0MjMiIQ/trczNzN3Ozt7t7/ACNUNTQ0NDNDIzIxIP7a3Mzczd3Nzt7e/wAjRTUwDmAfTseOVEMzQzMzIiAOzMzM3M3c3d3O3u8AJ3cQDx8fHx8fHx8fAQ8BDx8B8Q8fAfHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8="); // 440 Hz ×4 gain, 22050 Hz, fact 1544
+ private static readonly byte[] AdpcmStereo = Convert.FromBase64String("UklGRnQFAABXQVZFZm10IDIAAAACAAIAIlYAAIA+AAAAAQQAIAD0AAcAAAEAAAACAP8AAAAAwABAAPAAAADMATD/iAEY/2ZhY3QEAAAATwQAAExJU1QaAAAASU5GT0lTRlQNAAAATGF2ZjYxLjcuMTAzAABkYXRhAAUAAAAAEAAQAP8Bs/gAAJLxeHd4RyATGycbEQ8EDeTs0sHAz87RwsLa0NPN39TP4NDi+eDxAh8uQ0JOTj5QPUcwMUtELjIwMT4wPSQgHQ7w5820ztC/38Pew9DN1d7SzN/U793k4v4BESpPQ14+Q08wQT5GPUMxMj9OLz4vJy8PEf/j27LMwt6y38Pa08Dez9Pf1dzU3+zj7g8VIE5RPl80TUAxQjtDPzNOMC8wMDUtLBUOAOHAwsC93cbdxM/S2sHQwu3Q1O3Q4uHwDR8TQExETEQwXzI6Qz8wTjNNITMpMSAQIwD+/dKw0rrB38XRzcLd0dDD3NTcwe7i/vTxHBQ8QUAAAEoACA649VUMRPRlAm4/NlAsQS8zPzExSyEvMR8THv700c2w37LSztHKwt7T38zW39Lf3dLg4w4CKDFjMF4/QUI9RE8/PjZNMDM+Mz8oMREd8fPMws3DzcTC28/R0MHO5N7U3t/e4e/n8RsSMVBNQ0A9UDU/TzJAOjBCPiQ9MiAhHxIN/NKz0rjP0rHhzNDS0s3SzuTd4tvg4uACCxMxQEpQNE5BPkNNPzQ/TjI/QiwhLiUgDgDx48/DysDO1sDO3tDB38PQ3e/X0NHu8vvyIitRP1NAMEBNPkc/MU8/QC4yMC4vIiUQDf/nwMyw0cLN0MLez9XP0tzS0M7j4eHvAABTAH8GBvDJ2Q3wlegXEDFMRE9OQTFCP04+TzEyPT8xQSAnER0MBe3Gz87RsMHS0LDo0cDi3t/U393Q4eXtDxQwTk9SPl81TTNAP0I+Pk0kPDMwJDsSLw7w4sXAvdHCydKx7cDg0dTcz9Tg0ODg7fT8ES80bzFPQUExTE89P0AwN04iPi42ICEeAfL5wMDDwM3UzsLfwdvB0NDS3dDD4uDp8QEQET1RPFM9UjIwTzdMMUM7IUAhIi0gIw8f/vviwr3exMLQuuPfwN/d0cHX7t7V7+7w8BUtM19ATTJUPkAyTTFPMTAwQTwlLz0fJgAA7OK+w9K4083A0t3BwOTd3wAAhACACpH0huoO9tQc1O/g4P31HR8yQ1k0XTBDPUBDPTFOIEMlLD8hIBUtDwTw3dOg0bzfw9LN3cHQ3s3Q19DV39zg8fIPIj5bREBPTUUwTTIwTUIvMTQ9NTwiPh8UAu/hsM3NxtC91N3D3MTc08Df7dLQ7uPqBQ4fMENcQTJRO0ExQTFPPj4wRD8jPCMvHBEA8uvAtt+w0sDQysLQ0M3S0NLQzuHu5O0DCREhMm0+UD9HPk9BMzwzQT5BLT4kLCERHgUP0dG+wMHD2LPf393Ewe/f09Dd0NDb9f3/ICRNUUM8VDBNQzwzQDE/TCMwPDIyLhArBPDdw8Kw2rHTy9IAAIUA0RBwAFYCbwKB6s7SzeHU0dzR3uPs8fAUME9DWjFQPjRRPjE+T0QgPTQ7IjAgIB4O8+zToNW70cDC3s7V3s7Swu/e0OXR7ODwAwskTk5CQ0swUTA/RD1PMz0wNU0jLDQeHxT/7dTAvcLO1MDcv+PQz+Hd1N7UzeHi/fwDBxA7QVAwUT4wUiBBHwAAAAEPAQ8BDwEAH/AR8B/wEf8R8A8AAQAPAQ8AAQ8BAA8BDwEPAQ8AAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8BDwEPAQ8B"); // 440 Hz left, noise right, fact 1103
+
+ private static readonly byte[] AdpcmQuiet = Convert.FromBase64String("UklGRnQBAABXQVZFZm10IDIAAAACAAEAESsAAIA+AAAAAQQAIAD0AQcAAAEAAAACAP8AAAAAwABAAPAAAADMATD/iAEY/2ZhY3QEAAAASwEAAExJU1QaAAAASU5GT0lTRlQNAAAATGF2ZjYxLjcuMTAzAABkYXRhAAEAAAAQAAMAAAAAAQAAAADwAAAPAAAAAAAAAQAAAQAAAAAPAAAPAAAAAAAAABAAABAAAAAA8AAA8AAAAAAAAAEAAAEAAAAADwAADwAAAAAAAAEAAAAQAAAADwAAAPAAAAAAAAAQAAAQAAAAAPAAAPAAAAAAAAABAAABAAAAAA8AAA8AAAAAAAAAEAAAEAAAAADwAADwAAAAAAAAEAAAAQAAAADwAAAPAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); // 300 Hz ×0.004, 11025 Hz, fact 331
+
+ [Fact]
+ public void MsAdpcm_QuietKeepsTheStepFloorOf16()
+ {
+ var s = WavReader.Read(AdpcmQuiet)!.Value.Samples; // ffmpeg: steps stay at the floor, samples move by 16
+ Assert.Equal(331, s.Length);
+ Assert.Equal(new short[] { 0, 3, 3, 3, 3, 19, 19, 19, 19, 19, 19, 19 }, s[..12]);
+ Assert.Equal((302, 3682), (Sum(s), AbsSum(s)));
+ }
+
+ private static int Sum(short[] s) { int n = 0; foreach (var x in s) n += x; return n; }
+ private static int AbsSum(short[] s) { int n = 0; foreach (var x in s) n += Math.Abs(x); return n; }
+
+ [Fact]
+ public void MsAdpcm_Mono_MatchesFfmpeg()
+ {
+ var s = WavReader.Read(AdpcmMono)!.Value.Samples;
+ Assert.Equal(1544, s.Length);
+ Assert.Equal(new short[] { 0, 2044, 2156, 2422, 3059, 4585 }, s[..6]);
+ Assert.Equal(new short[] { -6016, -4057, -2328, -284, 2060, 3464 }, s[498..504]); // across the block edge at 500
+ Assert.Equal(new short[] { -16364, -16364, -15876 }, s[^3..]);
+ Assert.Equal((74278, 16090432), (Sum(s), AbsSum(s)));
+ }
+
+ [Fact]
+ public void MsAdpcm_Stereo_MatchesFfmpeg()
+ {
+ var clip = WavReader.Read(AdpcmStereo)!.Value;
+ Assert.Equal(22050, clip.SampleRate);
+ var s = clip.Samples;
+ Assert.Equal(1103, s.Length);
+ Assert.Equal(new short[] { -1847, -679, -687, -386, -527, 1116 }, s[..6]);
+ Assert.Equal(new short[] { 277, 3542, 2416, 255, -1687, -2380 }, s[498..504]);
+ Assert.Equal(new short[] { -1115, -937, 606 }, s[^3..]);
+ Assert.Equal((-183764, 2966742), (Sum(s), AbsSum(s)));
+ }
+
+ [Fact]
+ public void Downmix_InterleavedFloat()
+ {
+ Assert.Equal(new short[] { 16384, 0 }, Pcm.Downmix(new[] { 0.25f, 0.75f, 1f, -1f }, 2));
+ }
+}