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.
This commit is contained in:
RoosterDragon
2025-11-25 20:42:52 +00:00
committed by Gustas Kažukauskas
parent b429ca7879
commit 8b8651dcf7
5 changed files with 71 additions and 57 deletions

View File

@@ -13,7 +13,6 @@ using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@@ -178,13 +177,9 @@ namespace OpenRA
if (s.CanSeek) if (s.CanSeek)
return s.ReadBytes((int)(s.Length - s.Position)); return s.ReadBytes((int)(s.Length - s.Position));
var bytes = new List<byte>(); using var ms = new MemoryStream();
var buffer = new byte[1024]; s.CopyTo(ms);
int count; return ms.Capacity == ms.Length ? ms.GetBuffer() : ms.ToArray();
while ((count = s.Read(buffer, 0, buffer.Length)) > 0)
bytes.AddRange(buffer.Take(count));
return bytes.ToArray();
} }
} }

View File

@@ -101,6 +101,7 @@ namespace OpenRA.Mods.Cnc.FileFormats
{ {
readonly int outputSize; readonly int outputSize;
int dataSize; int dataSize;
byte[] inputBuffer;
int currentSample; int currentSample;
int baseOffset; int baseOffset;
@@ -121,9 +122,12 @@ namespace OpenRA.Mods.Cnc.FileFormats
return true; return true;
var chunk = AudChunk.Read(baseStream); var chunk = AudChunk.Read(baseStream);
var input = EnsureArraySize(ref inputBuffer, chunk.CompressedSize);
baseStream.ReadBytes(input);
for (var n = 0; n < chunk.CompressedSize; n++) 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); var t = ImaAdpcmReader.DecodeImaAdpcmSample(b, ref index, ref currentSample);
data.Enqueue((byte)t); data.Enqueue((byte)t);
@@ -144,6 +148,13 @@ namespace OpenRA.Mods.Cnc.FileFormats
return dataSize <= 0; return dataSize <= 0;
} }
static Span<byte> EnsureArraySize(ref byte[] array, int desiredSize)
{
if (array == null || array.Length < desiredSize)
array = new byte[desiredSize];
return array.AsSpan(..desiredSize);
}
} }
sealed class WestwoodCompressedAudStream : ReadOnlyAdapterStream sealed class WestwoodCompressedAudStream : ReadOnlyAdapterStream

View File

@@ -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) if (AudioChannels == 1)
AudioData = compressed ? ImaAdpcmReader.LoadImaAdpcmSound(audio1.ToArray(), ref adpcmIndex) : audio1.ToArray(); AudioData = GetAudioData(compressed, audio1, ref adpcmIndex);
else else
{ {
byte[] leftData, rightData; adpcmIndex = 0;
if (!compressed) var leftData = GetAudioData(compressed, audio1, ref adpcmIndex);
{ adpcmIndex = 0;
leftData = audio1.ToArray(); var rightData = GetAudioData(compressed, audio2, ref adpcmIndex);
rightData = audio2.ToArray();
}
else
{
adpcmIndex = 0;
leftData = ImaAdpcmReader.LoadImaAdpcmSound(audio1.ToArray(), ref adpcmIndex);
adpcmIndex = 0;
rightData = ImaAdpcmReader.LoadImaAdpcmSound(audio2.ToArray(), ref adpcmIndex);
}
AudioData = new byte[rightData.Length + leftData.Length]; AudioData = new byte[rightData.Length + leftData.Length];
var rightIndex = 0; var rightIndex = 0;

View File

@@ -56,18 +56,18 @@ namespace OpenRA.Mods.Common.FileFormats
return (short)current; return (short)current;
} }
public static byte[] LoadImaAdpcmSound(ReadOnlySpan<byte> raw, ref int index) public static void LoadImaAdpcmSound(ReadOnlySpan<byte> raw, ref int index, Span<byte> output)
{ {
var currentSample = 0; var currentSample = 0;
return LoadImaAdpcmSound(raw, ref index, ref currentSample); LoadImaAdpcmSound(raw, ref index, ref currentSample, output);
} }
public static byte[] LoadImaAdpcmSound(ReadOnlySpan<byte> raw, ref int index, ref int currentSample) public static void LoadImaAdpcmSound(ReadOnlySpan<byte> raw, ref int index, ref int currentSample, Span<byte> output)
{ {
var dataSize = raw.Length; 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; var offset = 0;
while (dataSize-- > 0) while (dataSize-- > 0)
@@ -82,8 +82,6 @@ namespace OpenRA.Mods.Common.FileFormats
output[offset++] = (byte)t; output[offset++] = (byte)t;
output[offset++] = (byte)(t >> 8); output[offset++] = (byte)(t >> 8);
} }
return output;
} }
} }
} }

View File

@@ -12,6 +12,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using OpenRA.Primitives; using OpenRA.Primitives;
namespace OpenRA.Mods.Common.FileFormats namespace OpenRA.Mods.Common.FileFormats
@@ -94,6 +95,9 @@ namespace OpenRA.Mods.Common.FileFormats
if (audioType != WaveType.Pcm) if (audioType != WaveType.Pcm)
sampleBits = 16; sampleBits = 16;
if (channels != 1 && channels != 2)
throw new NotSupportedException($"Expected 1 or 2 channels only for WAV file, received: {channels}");
var chan = channels; var chan = channels;
result = () => result = () =>
{ {
@@ -115,10 +119,8 @@ namespace OpenRA.Mods.Common.FileFormats
readonly int numBlocks; readonly int numBlocks;
readonly int blockDataSize; readonly int blockDataSize;
readonly int outputSize; readonly int outputSize;
readonly int[] predictor; readonly byte[] blockData;
readonly int[] index;
readonly byte[] interleaveBuffer;
int outOffset; int outOffset;
int currentBlock; int currentBlock;
@@ -129,21 +131,25 @@ namespace OpenRA.Mods.Common.FileFormats
numBlocks = dataSize / blockAlign; numBlocks = dataSize / blockAlign;
blockDataSize = blockAlign - channels * 4; blockDataSize = blockAlign - channels * 4;
outputSize = uncompressedSize * channels * 2; 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<byte> data) protected override bool BufferData(Stream baseStream, Queue<byte> data)
{ {
// Decode each block of IMA ADPCM data // Decode each block of IMA ADPCM data
// Each block starts with a initial state per-channel // Each block starts with a initial state per-channel
Span<int> predictor = stackalloc int[channels];
Span<int> index = stackalloc int[channels];
Span<byte> channelData = stackalloc byte[channels * 4];
baseStream.ReadBytes(channelData);
var cd = 0;
for (var c = 0; c < channels; c++) for (var c = 0; c < channels; c++)
{ {
predictor[c] = baseStream.ReadInt16(); predictor[c] = (short)(channelData[cd++] | channelData[cd++] << 8);
index[c] = baseStream.ReadUInt8(); index[c] = channelData[cd++];
baseStream.ReadUInt8(); // Unknown/Reserved cd++; // Unknown/Reserved
// Output first sample from input // Output first sample from input
data.Enqueue((byte)predictor[c]); data.Enqueue((byte)predictor[c]);
@@ -155,15 +161,17 @@ namespace OpenRA.Mods.Common.FileFormats
} }
// Decode and output remaining data in this block // Decode and output remaining data in this block
Span<byte> decoded = stackalloc byte[16];
Span<byte> interleaveBuffer = stackalloc byte[channels * 16];
var blockDataSpan = blockData.AsSpan();
baseStream.ReadBytes(blockDataSpan);
var blockOffset = 0; var blockOffset = 0;
Span<byte> chunk = stackalloc byte[4];
while (blockOffset < blockDataSize) while (blockOffset < blockDataSize)
{ {
for (var c = 0; c < channels; c++) for (var c = 0; c < channels; c++)
{ {
// Decode 4 bytes (to 16 bytes of output) per channel // Decode 4 bytes (to 16 bytes of output) per channel
baseStream.ReadBytes(chunk); ImaAdpcmReader.LoadImaAdpcmSound(blockDataSpan.Slice(blockOffset, 4), ref index[c], ref predictor[c], decoded);
var decoded = ImaAdpcmReader.LoadImaAdpcmSound(chunk, ref index[c], ref predictor[c]);
// Interleave output, one sample per channel // Interleave output, one sample per channel
var interleaveChannelOffset = 2 * c; var interleaveChannelOffset = 2 * c;
@@ -209,6 +217,7 @@ namespace OpenRA.Mods.Common.FileFormats
readonly short channels; readonly short channels;
readonly int blockDataSize; readonly int blockDataSize;
readonly int numBlocks; readonly int numBlocks;
readonly byte[] blockData;
int currentBlock; int currentBlock;
@@ -218,36 +227,35 @@ namespace OpenRA.Mods.Common.FileFormats
this.channels = channels; this.channels = channels;
blockDataSize = blockAlign - channels * 7; blockDataSize = blockAlign - channels * 7;
numBlocks = dataSize / blockAlign; numBlocks = dataSize / blockAlign;
blockData = new byte[blockDataSize];
} }
protected override bool BufferData(Stream baseStream, Queue<byte> data) protected override bool BufferData(Stream baseStream, Queue<byte> data)
{ {
var bpred = new byte[channels]; Span<byte> bpred = stackalloc byte[channels];
var chanIdelta = new short[channels]; Span<short> chanIdelta = stackalloc short[channels];
var s1 = new short[channels]; Span<short> s1 = stackalloc short[channels];
var s2 = new short[channels]; Span<short> s2 = stackalloc short[channels];
baseStream.ReadBytes(bpred);
baseStream.ReadBytes(MemoryMarshal.Cast<short, byte>(chanIdelta));
baseStream.ReadBytes(MemoryMarshal.Cast<short, byte>(s1));
baseStream.ReadBytes(MemoryMarshal.Cast<short, byte>(s2));
for (var c = 0; c < channels; c++) for (var c = 0; c < channels; c++)
bpred[c] = baseStream.ReadUInt8(); s2[c] = WriteSample(s2[c], data);
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);
for (var c = 0; c < channels; c++) for (var c = 0; c < channels; c++)
WriteSample(s1[c], data); WriteSample(s1[c], data);
var channelNumber = channels > 1 ? 1 : 0; var channelNumber = channels > 1 ? 1 : 0;
baseStream.ReadBytes(blockData);
for (var blockindx = 0; blockindx < blockDataSize; blockindx++) for (var blockindx = 0; blockindx < blockDataSize; blockindx++)
{ {
var bytecode = baseStream.ReadUInt8(); var bytecode = blockData[blockindx];
// Decode the first nibble, this is always left channel // 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); WriteSample(DecodeNibble((short)((bytecode >> 4) & 0x0F), bpred[0], ref chanIdelta[0], ref s1[0], ref s2[0]), data);