aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core.Tests/Audio/WavReaderTests.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Godosa.Core.Tests/Audio/WavReaderTests.cs')
-rw-r--r--src/Godosa.Core.Tests/Audio/WavReaderTests.cs127
1 files changed, 127 insertions, 0 deletions
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));
+ }
+}