using Concentus; namespace Nonemm.Recorder.Tests; /// Reads back what OpusFileWriter wrote: checks every page checksum and /// decodes the audio with Concentus, so the tests see what a player would. internal sealed class OggOpusReader { public int Channels { get; private set; } public int MappingFamily { get; private set; } public int PreSkip { get; private set; } public long FinalGranule { get; private set; } public bool HasEndMark { get; private set; } /// Decoded samples, one array for each channel, with the pre-skip removed. public float[][] Pcm { get; private set; } = []; public static OggOpusReader Read(string path) { byte[] file = File.ReadAllBytes(path); OggOpusReader result = new(); List packets = []; int position = 0; while (position < file.Length) { Assert.Equal("OggS", System.Text.Encoding.ASCII.GetString(file, position, 4)); int segments = file[position + 26]; int size = 0; for (int i = 0; i < segments; i++) { size += file[position + 27 + i]; } int total = 27 + segments + size; uint stored = BitConverter.ToUInt32(file, position + 22); byte[] page = file[position..(position + total)]; Array.Clear(page, 22, 4); Assert.Equal(Crc(page), stored); result.FinalGranule = BitConverter.ToInt64(file, position + 6); result.HasEndMark = (file[position + 5] & 0x04) != 0; packets.Add(file[(position + 27 + segments)..(position + total)]); position += total; } byte[] head = packets[0]; result.Channels = head[9]; result.PreSkip = BitConverter.ToUInt16(head, 10); result.MappingFamily = head[18]; byte[] mapping = result.MappingFamily == 0 ? [0] : head[21..(21 + result.Channels)]; int streams = result.MappingFamily == 0 ? 1 : head[19]; int coupled = result.MappingFamily == 0 ? 0 : head[20]; IOpusMultiStreamDecoder decoder = OpusCodecFactory.CreateMultiStreamDecoder( RecorderEngine.SampleRate, result.Channels, streams, coupled, mapping); List[] decoded = [.. Enumerable.Range(0, result.Channels).Select(_ => new List())]; foreach (byte[] packet in packets.Skip(2)) { float[] frame = new float[OpusFileWriter.FrameSamples * result.Channels]; int got = decoder.DecodeMultistream(packet, frame, OpusFileWriter.FrameSamples, false); for (int sample = 0; sample < got; sample++) { for (int channel = 0; channel < result.Channels; channel++) { decoded[channel].Add(frame[sample * result.Channels + channel]); } } } result.Pcm = [.. decoded.Select(list => list.Skip(result.PreSkip).ToArray())]; return result; } private static uint Crc(byte[] data) { uint crc = 0; foreach (byte b in data) { crc ^= (uint)b << 24; for (int bit = 0; bit < 8; bit++) { crc = (crc & 0x80000000) != 0 ? (crc << 1) ^ 0x04C11DB7 : crc << 1; } } return crc; } public static float Rms(float[] samples, int from, int count) { double sum = 0; for (int i = from; i < from + count; i++) { sum += samples[i] * samples[i]; } return (float)Math.Sqrt(sum / count); } }