From 8b8651dcf74c2ec00f64b1ac23cf207d7251eeb6 Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Tue, 25 Nov 2025 20:42:52 +0000 Subject: [PATCH] Improve audio parsing performance. - Improve the AudReader and WavReader by performing fewer, larger stream reads to consume bytes more efficiently. - Improve ImaAdpcmReader.LoadImaAdpcmSound by accepting an output buffer rather than allocating a new array. - Improve StreamExts.ReadAllBytes by using MemoryStream.CopyTo. This internally rents a much larger buffer from the array pool, allowing for fewer, larger stream reads. --- OpenRA.Game/StreamExts.cs | 11 +--- OpenRA.Mods.Cnc/FileFormats/AudReader.cs | 13 +++- OpenRA.Mods.Cnc/FileFormats/VqaVideo.cs | 30 ++++----- .../FileFormats/ImaAdpcmReader.cs | 12 ++-- OpenRA.Mods.Common/FileFormats/WavReader.cs | 62 +++++++++++-------- 5 files changed, 71 insertions(+), 57 deletions(-) diff --git a/OpenRA.Game/StreamExts.cs b/OpenRA.Game/StreamExts.cs index 770d4c4a43..ddf7fc0036 100644 --- a/OpenRA.Game/StreamExts.cs +++ b/OpenRA.Game/StreamExts.cs @@ -13,7 +13,6 @@ using System; using System.Buffers; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Text; @@ -178,13 +177,9 @@ namespace OpenRA if (s.CanSeek) return s.ReadBytes((int)(s.Length - s.Position)); - var bytes = new List(); - var buffer = new byte[1024]; - int count; - while ((count = s.Read(buffer, 0, buffer.Length)) > 0) - bytes.AddRange(buffer.Take(count)); - - return bytes.ToArray(); + using var ms = new MemoryStream(); + s.CopyTo(ms); + return ms.Capacity == ms.Length ? ms.GetBuffer() : ms.ToArray(); } } diff --git a/OpenRA.Mods.Cnc/FileFormats/AudReader.cs b/OpenRA.Mods.Cnc/FileFormats/AudReader.cs index 66a97ddf84..72576516be 100644 --- a/OpenRA.Mods.Cnc/FileFormats/AudReader.cs +++ b/OpenRA.Mods.Cnc/FileFormats/AudReader.cs @@ -101,6 +101,7 @@ namespace OpenRA.Mods.Cnc.FileFormats { readonly int outputSize; int dataSize; + byte[] inputBuffer; int currentSample; int baseOffset; @@ -121,9 +122,12 @@ namespace OpenRA.Mods.Cnc.FileFormats return true; var chunk = AudChunk.Read(baseStream); + var input = EnsureArraySize(ref inputBuffer, chunk.CompressedSize); + baseStream.ReadBytes(input); + for (var n = 0; n < chunk.CompressedSize; n++) { - var b = baseStream.ReadUInt8(); + var b = input[n]; var t = ImaAdpcmReader.DecodeImaAdpcmSample(b, ref index, ref currentSample); data.Enqueue((byte)t); @@ -144,6 +148,13 @@ namespace OpenRA.Mods.Cnc.FileFormats return dataSize <= 0; } + + static Span EnsureArraySize(ref byte[] array, int desiredSize) + { + if (array == null || array.Length < desiredSize) + array = new byte[desiredSize]; + return array.AsSpan(..desiredSize); + } } sealed class WestwoodCompressedAudStream : ReadOnlyAdapterStream diff --git a/OpenRA.Mods.Cnc/FileFormats/VqaVideo.cs b/OpenRA.Mods.Cnc/FileFormats/VqaVideo.cs index 7884ed513d..03c02aaeea 100644 --- a/OpenRA.Mods.Cnc/FileFormats/VqaVideo.cs +++ b/OpenRA.Mods.Cnc/FileFormats/VqaVideo.cs @@ -225,23 +225,25 @@ namespace OpenRA.Mods.Cnc.FileFormats } } + static byte[] GetAudioData(bool compressed, MemoryStream audio, ref int adpcmIndex) + { + var audioArray = audio.ToArray(); + if (!compressed) + return audioArray; + + var result = new byte[audio.Length * 4]; + ImaAdpcmReader.LoadImaAdpcmSound(audioArray, ref adpcmIndex, result); + return result; + } + if (AudioChannels == 1) - AudioData = compressed ? ImaAdpcmReader.LoadImaAdpcmSound(audio1.ToArray(), ref adpcmIndex) : audio1.ToArray(); + AudioData = GetAudioData(compressed, audio1, ref adpcmIndex); else { - byte[] leftData, rightData; - if (!compressed) - { - leftData = audio1.ToArray(); - rightData = audio2.ToArray(); - } - else - { - adpcmIndex = 0; - leftData = ImaAdpcmReader.LoadImaAdpcmSound(audio1.ToArray(), ref adpcmIndex); - adpcmIndex = 0; - rightData = ImaAdpcmReader.LoadImaAdpcmSound(audio2.ToArray(), ref adpcmIndex); - } + adpcmIndex = 0; + var leftData = GetAudioData(compressed, audio1, ref adpcmIndex); + adpcmIndex = 0; + var rightData = GetAudioData(compressed, audio2, ref adpcmIndex); AudioData = new byte[rightData.Length + leftData.Length]; var rightIndex = 0; diff --git a/OpenRA.Mods.Common/FileFormats/ImaAdpcmReader.cs b/OpenRA.Mods.Common/FileFormats/ImaAdpcmReader.cs index 6f61732788..3a6d0a368c 100644 --- a/OpenRA.Mods.Common/FileFormats/ImaAdpcmReader.cs +++ b/OpenRA.Mods.Common/FileFormats/ImaAdpcmReader.cs @@ -56,18 +56,18 @@ namespace OpenRA.Mods.Common.FileFormats return (short)current; } - public static byte[] LoadImaAdpcmSound(ReadOnlySpan raw, ref int index) + public static void LoadImaAdpcmSound(ReadOnlySpan raw, ref int index, Span output) { var currentSample = 0; - return LoadImaAdpcmSound(raw, ref index, ref currentSample); + LoadImaAdpcmSound(raw, ref index, ref currentSample, output); } - public static byte[] LoadImaAdpcmSound(ReadOnlySpan raw, ref int index, ref int currentSample) + public static void LoadImaAdpcmSound(ReadOnlySpan raw, ref int index, ref int currentSample, Span output) { var dataSize = raw.Length; - var outputSize = raw.Length * 4; + if (output.Length != raw.Length * 4) + throw new ArgumentException($"{nameof(output)} must be 4 times the length of {nameof(raw)}.", nameof(output)); - var output = new byte[outputSize]; var offset = 0; while (dataSize-- > 0) @@ -82,8 +82,6 @@ namespace OpenRA.Mods.Common.FileFormats output[offset++] = (byte)t; output[offset++] = (byte)(t >> 8); } - - return output; } } } diff --git a/OpenRA.Mods.Common/FileFormats/WavReader.cs b/OpenRA.Mods.Common/FileFormats/WavReader.cs index 76e9e7f381..99899994fe 100644 --- a/OpenRA.Mods.Common/FileFormats/WavReader.cs +++ b/OpenRA.Mods.Common/FileFormats/WavReader.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using OpenRA.Primitives; namespace OpenRA.Mods.Common.FileFormats @@ -94,6 +95,9 @@ namespace OpenRA.Mods.Common.FileFormats if (audioType != WaveType.Pcm) sampleBits = 16; + if (channels != 1 && channels != 2) + throw new NotSupportedException($"Expected 1 or 2 channels only for WAV file, received: {channels}"); + var chan = channels; result = () => { @@ -115,10 +119,8 @@ namespace OpenRA.Mods.Common.FileFormats readonly int numBlocks; readonly int blockDataSize; readonly int outputSize; - readonly int[] predictor; - readonly int[] index; + readonly byte[] blockData; - readonly byte[] interleaveBuffer; int outOffset; int currentBlock; @@ -129,21 +131,25 @@ namespace OpenRA.Mods.Common.FileFormats numBlocks = dataSize / blockAlign; blockDataSize = blockAlign - channels * 4; outputSize = uncompressedSize * channels * 2; - predictor = new int[channels]; - index = new int[channels]; - interleaveBuffer = new byte[channels * 16]; + blockData = new byte[blockDataSize]; } protected override bool BufferData(Stream baseStream, Queue data) { // Decode each block of IMA ADPCM data // Each block starts with a initial state per-channel + Span predictor = stackalloc int[channels]; + Span index = stackalloc int[channels]; + + Span channelData = stackalloc byte[channels * 4]; + baseStream.ReadBytes(channelData); + var cd = 0; for (var c = 0; c < channels; c++) { - predictor[c] = baseStream.ReadInt16(); - index[c] = baseStream.ReadUInt8(); - baseStream.ReadUInt8(); // Unknown/Reserved + predictor[c] = (short)(channelData[cd++] | channelData[cd++] << 8); + index[c] = channelData[cd++]; + cd++; // Unknown/Reserved // Output first sample from input data.Enqueue((byte)predictor[c]); @@ -155,15 +161,17 @@ namespace OpenRA.Mods.Common.FileFormats } // Decode and output remaining data in this block + Span decoded = stackalloc byte[16]; + Span interleaveBuffer = stackalloc byte[channels * 16]; + var blockDataSpan = blockData.AsSpan(); + baseStream.ReadBytes(blockDataSpan); var blockOffset = 0; - Span chunk = stackalloc byte[4]; while (blockOffset < blockDataSize) { for (var c = 0; c < channels; c++) { // Decode 4 bytes (to 16 bytes of output) per channel - baseStream.ReadBytes(chunk); - var decoded = ImaAdpcmReader.LoadImaAdpcmSound(chunk, ref index[c], ref predictor[c]); + ImaAdpcmReader.LoadImaAdpcmSound(blockDataSpan.Slice(blockOffset, 4), ref index[c], ref predictor[c], decoded); // Interleave output, one sample per channel var interleaveChannelOffset = 2 * c; @@ -209,6 +217,7 @@ namespace OpenRA.Mods.Common.FileFormats readonly short channels; readonly int blockDataSize; readonly int numBlocks; + readonly byte[] blockData; int currentBlock; @@ -218,36 +227,35 @@ namespace OpenRA.Mods.Common.FileFormats this.channels = channels; blockDataSize = blockAlign - channels * 7; numBlocks = dataSize / blockAlign; + + blockData = new byte[blockDataSize]; } protected override bool BufferData(Stream baseStream, Queue data) { - var bpred = new byte[channels]; - var chanIdelta = new short[channels]; + Span bpred = stackalloc byte[channels]; + Span chanIdelta = stackalloc short[channels]; - var s1 = new short[channels]; - var s2 = new short[channels]; + Span s1 = stackalloc short[channels]; + Span s2 = stackalloc short[channels]; + + baseStream.ReadBytes(bpred); + baseStream.ReadBytes(MemoryMarshal.Cast(chanIdelta)); + baseStream.ReadBytes(MemoryMarshal.Cast(s1)); + baseStream.ReadBytes(MemoryMarshal.Cast(s2)); for (var c = 0; c < channels; c++) - bpred[c] = baseStream.ReadUInt8(); - - for (var c = 0; c < channels; c++) - chanIdelta[c] = baseStream.ReadInt16(); - - for (var c = 0; c < channels; c++) - s1[c] = baseStream.ReadInt16(); - - for (var c = 0; c < channels; c++) - s2[c] = WriteSample(baseStream.ReadInt16(), data); + s2[c] = WriteSample(s2[c], data); for (var c = 0; c < channels; c++) WriteSample(s1[c], data); var channelNumber = channels > 1 ? 1 : 0; + baseStream.ReadBytes(blockData); for (var blockindx = 0; blockindx < blockDataSize; blockindx++) { - var bytecode = baseStream.ReadUInt8(); + var bytecode = blockData[blockindx]; // Decode the first nibble, this is always left channel WriteSample(DecodeNibble((short)((bytecode >> 4) & 0x0F), bpred[0], ref chanIdelta[0], ref s1[0], ref s2[0]), data);