diff --git a/OpenRA.Mods.Cnc/AudioLoaders/AudLoader.cs b/OpenRA.Mods.Cnc/AudioLoaders/AudLoader.cs index 96daac7bb4..fb4e2876d9 100644 --- a/OpenRA.Mods.Cnc/AudioLoaders/AudLoader.cs +++ b/OpenRA.Mods.Cnc/AudioLoaders/AudLoader.cs @@ -24,7 +24,7 @@ namespace OpenRA.Mods.Cnc.AudioLoaders var readFormat = s.ReadByte(); s.Position = start; - return readFormat == (int)SoundFormat.ImaAdpcm; + return readFormat == (int)SoundFormat.ImaAdpcm || readFormat == (int)SoundFormat.WestwoodCompressed; } bool ISoundLoader.TryParseSound(Stream stream, out ISoundFormat sound) diff --git a/OpenRA.Mods.Cnc/FileFormats/AudReader.cs b/OpenRA.Mods.Cnc/FileFormats/AudReader.cs index 839d49f381..e30af10ab6 100644 --- a/OpenRA.Mods.Cnc/FileFormats/AudReader.cs +++ b/OpenRA.Mods.Cnc/FileFormats/AudReader.cs @@ -30,6 +30,24 @@ namespace OpenRA.Mods.Cnc.FileFormats ImaAdpcm = 99, } + struct AudChunk + { + public int CompressedSize; + public int OutputSize; + + public static AudChunk Read(Stream s) + { + AudChunk c; + c.CompressedSize = s.ReadUInt16(); + c.OutputSize = s.ReadUInt16(); + + if (s.ReadUInt32() != 0xdeaf) + throw new InvalidDataException("Chunk header is bogus"); + + return c; + } + } + public static class AudReader { public static bool LoadSound(Stream s, out Func result, out int sampleRate, out int sampleBits, out int channels, out float lengthInSeconds) @@ -50,9 +68,6 @@ namespace OpenRA.Mods.Cnc.FileFormats if (!Enum.IsDefined(typeof(SoundFormat), readFormat)) return false; - if (readFormat == (int)SoundFormat.WestwoodCompressed) - throw new NotImplementedException(); - var offsetPosition = s.Position; var streamLength = s.Length; var segmentLength = (int)(streamLength - offsetPosition); @@ -60,7 +75,18 @@ namespace OpenRA.Mods.Cnc.FileFormats result = () => { var audioStream = SegmentStream.CreateWithoutOwningStream(s, offsetPosition, segmentLength); - return new AudStream(audioStream, outputSize, dataSize); + + switch (readFormat) + { + case (int)SoundFormat.ImaAdpcm: + return new ImaAdpcmAudStream(audioStream, outputSize, dataSize); + + case (int)SoundFormat.WestwoodCompressed: + return new WestwoodCompressedAudStream(audioStream, outputSize, dataSize); + + default: + throw new NotImplementedException(); + } }; } finally @@ -71,7 +97,7 @@ namespace OpenRA.Mods.Cnc.FileFormats return true; } - sealed class AudStream : ReadOnlyAdapterStream + sealed class ImaAdpcmAudStream : ReadOnlyAdapterStream { readonly int outputSize; int dataSize; @@ -80,7 +106,7 @@ namespace OpenRA.Mods.Cnc.FileFormats int baseOffset; int index; - public AudStream(Stream stream, int outputSize, int dataSize) + public ImaAdpcmAudStream(Stream stream, int outputSize, int dataSize) : base(stream) { this.outputSize = outputSize; @@ -94,7 +120,7 @@ namespace OpenRA.Mods.Cnc.FileFormats if (dataSize <= 0) return true; - var chunk = ImaAdpcmChunk.Read(baseStream); + var chunk = AudChunk.Read(baseStream); for (var n = 0; n < chunk.CompressedSize; n++) { var b = baseStream.ReadUInt8(); @@ -119,5 +145,39 @@ namespace OpenRA.Mods.Cnc.FileFormats return dataSize <= 0; } } + + sealed class WestwoodCompressedAudStream : ReadOnlyAdapterStream + { + readonly int outputSize; + int dataSize; + + public WestwoodCompressedAudStream(Stream stream, int outputSize, int dataSize) + : base(stream) + { + this.outputSize = outputSize; + this.dataSize = dataSize; + } + + public override long Length => outputSize; + + protected override bool BufferData(Stream baseStream, Queue data) + { + if (dataSize <= 0) + return true; + + var chunk = AudChunk.Read(baseStream); + + var input = baseStream.ReadBytes(chunk.CompressedSize); + var output = new byte[chunk.OutputSize]; + WestwoodCompressedReader.DecodeWestwoodCompressedSample(input, output); + + foreach (var b in output) + data.Enqueue(b); + + dataSize -= 8 + chunk.CompressedSize; + + return dataSize <= 0; + } + } } } diff --git a/OpenRA.Mods.Common/FileFormats/ImaAdpcmReader.cs b/OpenRA.Mods.Common/FileFormats/ImaAdpcmReader.cs index e98eccabc9..643936ca53 100644 --- a/OpenRA.Mods.Common/FileFormats/ImaAdpcmReader.cs +++ b/OpenRA.Mods.Common/FileFormats/ImaAdpcmReader.cs @@ -13,24 +13,6 @@ using System.IO; namespace OpenRA.Mods.Common.FileFormats { - public struct ImaAdpcmChunk - { - public int CompressedSize; - public int OutputSize; - - public static ImaAdpcmChunk Read(Stream s) - { - ImaAdpcmChunk c; - c.CompressedSize = s.ReadUInt16(); - c.OutputSize = s.ReadUInt16(); - - if (s.ReadUInt32() != 0xdeaf) - throw new InvalidDataException("Chunk header is bogus"); - - return c; - } - } - public class ImaAdpcmReader { static readonly int[] IndexAdjust = { -1, -1, -1, -1, 2, 4, 6, 8 }; diff --git a/OpenRA.Mods.Common/FileFormats/WestwoodCompressedReader.cs b/OpenRA.Mods.Common/FileFormats/WestwoodCompressedReader.cs new file mode 100644 index 0000000000..9d43b80a3c --- /dev/null +++ b/OpenRA.Mods.Common/FileFormats/WestwoodCompressedReader.cs @@ -0,0 +1,86 @@ +#region Copyright & License Information + +/* + * Copyright 2007-2022 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ + +#endregion + +using System; + +namespace OpenRA.Mods.Common.FileFormats +{ + public class WestwoodCompressedReader + { + static readonly int[] AudWsStepTable2 = { -2, -1, 0, 1 }; + static readonly int[] AudWsStepTable4 = { -9, -8, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 8 }; + + public static void DecodeWestwoodCompressedSample(byte[] input, byte[] output) + { + if (input.Length == output.Length) + { + Array.Copy(input, output, output.Length); + + return; + } + + var sample = 0x80; + var r = 0; + var w = 0; + + while (r < input.Length) + { + var count = input[r++] & 0x3f; + + switch (input[r - 1] >> 6) + { + case 0: + for (count++; count > 0; count--) + { + var code = input[r++]; + output[w++] = (byte)(sample = (sample + AudWsStepTable2[(code >> 0) & 0x03]).Clamp(byte.MinValue, byte.MaxValue)); + output[w++] = (byte)(sample = (sample + AudWsStepTable2[(code >> 2) & 0x03]).Clamp(byte.MinValue, byte.MaxValue)); + output[w++] = (byte)(sample = (sample + AudWsStepTable2[(code >> 4) & 0x03]).Clamp(byte.MinValue, byte.MaxValue)); + output[w++] = (byte)(sample = (sample + AudWsStepTable2[(code >> 6) & 0x03]).Clamp(byte.MinValue, byte.MaxValue)); + } + + break; + + case 1: + for (count++; count > 0; count--) + { + var code = input[r++]; + output[w++] = (byte)(sample = (sample + AudWsStepTable4[(code >> 0) & 0x0f]).Clamp(byte.MinValue, byte.MaxValue)); + output[w++] = (byte)(sample = (sample + AudWsStepTable4[(code >> 4) & 0xff]).Clamp(byte.MinValue, byte.MaxValue)); + } + + break; + + case 2 when (count & 0x20) != 0: + output[w++] = (byte)(sample += (sbyte)((sbyte)count << 3) >> 3); + + break; + + case 2: + for (count++; count > 0; count--) + output[w++] = input[r++]; + + sample = input[r - 1]; + + break; + + default: + for (count++; count > 0; count--) + output[w++] = (byte)sample; + + break; + } + } + } + } +}