From 8cf7c46c8fd33dcf8b7eee76d23b4afcecef2c3e Mon Sep 17 00:00:00 2001 From: Benno van den Bogaard Date: Sun, 1 Mar 2015 19:33:51 +0100 Subject: [PATCH] Added audio.bag/audio.idx support used in RA2 --- OpenRA.Game/FileFormats/IdxReader.cs | 44 +++++ OpenRA.Game/FileSystem/BagFile.cs | 192 +++++++++++++++++++++ OpenRA.Game/FileSystem/GlobalFileSystem.cs | 2 + OpenRA.Game/FileSystem/IdxEntry.cs | 94 ++++++++++ OpenRA.Game/OpenRA.Game.csproj | 4 + OpenRA.Game/Primitives/MergedStream.cs | 116 +++++++++++++ 6 files changed, 452 insertions(+) create mode 100644 OpenRA.Game/FileFormats/IdxReader.cs create mode 100644 OpenRA.Game/FileSystem/BagFile.cs create mode 100644 OpenRA.Game/FileSystem/IdxEntry.cs create mode 100644 OpenRA.Game/Primitives/MergedStream.cs diff --git a/OpenRA.Game/FileFormats/IdxReader.cs b/OpenRA.Game/FileFormats/IdxReader.cs new file mode 100644 index 0000000000..906441fcaa --- /dev/null +++ b/OpenRA.Game/FileFormats/IdxReader.cs @@ -0,0 +1,44 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 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. For more information, + * see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.IO; +using OpenRA.FileSystem; + +namespace OpenRA.FileFormats +{ + public class IdxReader + { + public readonly int SoundCount; + public List Entries; + + public IdxReader(Stream s) + { + s.Seek(0, SeekOrigin.Begin); + + var id = s.ReadASCII(4); + + if (id != "GABA") + throw new InvalidDataException("Unable to load Idx file, did not find magic id, found {0} instead".F(id)); + + var two = s.ReadInt32(); + + if (two != 2) + throw new InvalidDataException("Unable to load Idx file, did not find magic number 2, found {0} instead".F(two)); + + SoundCount = s.ReadInt32(); + + Entries = new List(); + + for (var i = 0; i < SoundCount; i++) + Entries.Add(new IdxEntry(s)); + } + } +} \ No newline at end of file diff --git a/OpenRA.Game/FileSystem/BagFile.cs b/OpenRA.Game/FileSystem/BagFile.cs new file mode 100644 index 0000000000..7cc6abea80 --- /dev/null +++ b/OpenRA.Game/FileSystem/BagFile.cs @@ -0,0 +1,192 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using OpenRA.FileFormats; +using OpenRA.Primitives; + +namespace OpenRA.FileSystem +{ + public sealed class BagFile : IFolder, IDisposable + { + static readonly uint[] Nothing = { }; + + readonly string bagFilename; + readonly Stream s; + readonly int bagFilePriority; + readonly Dictionary index; + + public BagFile(string filename, int priority) + { + bagFilename = filename; + bagFilePriority = priority; + + // A bag file is always accompanied with an .idx counterpart + // For example: audio.bag requires the audio.idx file + var indexFilename = Path.ChangeExtension(filename, ".idx"); + + s = GlobalFileSystem.Open(filename); + + // Build the index and dispose the stream, it is no longer needed after this + List entries; + using (var indexStream = GlobalFileSystem.Open(indexFilename)) + { + var reader = new IdxReader(indexStream); + + entries = reader.Entries; + } + + index = entries.ToDictionaryWithConflictLog(x => x.Hash, + "{0} (bag format)".F(filename), + null, x => "(offs={0}, len={1})".F(x.Offset, x.Length)); + } + + public int Priority { get { return 1000 + bagFilePriority; } } + public string Name { get { return bagFilename; } } + + public Stream GetContent(uint hash) + { + IdxEntry entry; + if (!index.TryGetValue(hash, out entry)) + return null; + + s.Seek(entry.Offset, SeekOrigin.Begin); + + var waveHeaderMemoryStream = new MemoryStream(); + + var channels = (entry.Flags & 1) > 0 ? 2 : 1; + + if ((entry.Flags & 2) > 0) + { + // PCM + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("RIFF")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length + 36)); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE")); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt ")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(16)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)WavLoader.WaveType.Pcm)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(2 * channels * entry.SampleRate)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)(2 * channels))); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)16)); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("data")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length)); + } + + if ((entry.Flags & 8) > 0) + { + // IMA ADPCM + var samplesPerChunk = (2 * (entry.ChunkSize - 4)) + 1; + var bytesPerSec = (int)Math.Floor(((double)(2 * entry.ChunkSize) / samplesPerChunk) * ((double)entry.SampleRate / 2)); + var chunkSize = entry.ChunkSize > entry.Length ? entry.Length : entry.ChunkSize; + + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("RIFF")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length + 52)); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE")); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt ")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(20)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)WavLoader.WaveType.ImaAdpcm)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(bytesPerSec)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)chunkSize)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)4)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)2)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)samplesPerChunk)); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fact")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(4)); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(4 * entry.Length)); + waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("data")); + waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.Length)); + } + + waveHeaderMemoryStream.Seek(0, SeekOrigin.Begin); + + // Construct a merged stream + var mergedStream = new MergedStream(waveHeaderMemoryStream, s); + mergedStream.SetLength(waveHeaderMemoryStream.Length + entry.Length); + + return mergedStream; + } + + uint? FindMatchingHash(string filename) + { + var hash = IdxEntry.HashFilename(filename, PackageHashType.CRC32); + if (index.ContainsKey(hash)) + return hash; + + // Maybe we were given a raw hash? + uint raw; + if (!uint.TryParse(filename, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out raw)) + return null; + + if ("{0:X}".F(raw) == filename && index.ContainsKey(raw)) + return raw; + + return null; + } + + public Stream GetContent(string filename) + { + var hash = FindMatchingHash(filename); + return hash.HasValue ? GetContent(hash.Value) : null; + } + + public bool Exists(string filename) + { + return FindMatchingHash(filename).HasValue; + } + + public IEnumerable ClassicHashes() + { + return Nothing; + } + + public IEnumerable CrcHashes() + { + return index.Keys; + } + + public IEnumerable AllFileNames() + { + var lookup = new Dictionary(); + if (GlobalFileSystem.Exists("global mix database.dat")) + { + var db = new XccGlobalDatabase(GlobalFileSystem.Open("global mix database.dat")); + foreach (var e in db.Entries) + { + var hash = IdxEntry.HashFilename(e, PackageHashType.CRC32); + if (!lookup.ContainsKey(hash)) + lookup.Add(hash, e); + } + } + + return index.Keys.Select(k => lookup.ContainsKey(k) ? lookup[k] : "{0:X}".F(k)); + } + + public void Write(Dictionary contents) + { + GlobalFileSystem.Unmount(this); + throw new NotImplementedException("Updating bag files unsupported"); + } + + public void Dispose() + { + if (s != null) + s.Dispose(); + } + } +} diff --git a/OpenRA.Game/FileSystem/GlobalFileSystem.cs b/OpenRA.Game/FileSystem/GlobalFileSystem.cs index a7c9337832..42e9cba512 100644 --- a/OpenRA.Game/FileSystem/GlobalFileSystem.cs +++ b/OpenRA.Game/FileSystem/GlobalFileSystem.cs @@ -101,6 +101,8 @@ namespace OpenRA.FileSystem return new PakFile(filename, order); if (filename.EndsWith(".big", StringComparison.InvariantCultureIgnoreCase)) return new BigFile(filename, order); + if (filename.EndsWith(".bag", StringComparison.InvariantCultureIgnoreCase)) + return new BagFile(filename, order); return new Folder(filename, order); } diff --git a/OpenRA.Game/FileSystem/IdxEntry.cs b/OpenRA.Game/FileSystem/IdxEntry.cs new file mode 100644 index 0000000000..8b71792379 --- /dev/null +++ b/OpenRA.Game/FileSystem/IdxEntry.cs @@ -0,0 +1,94 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 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. For more information, + * see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.IO; + +namespace OpenRA.FileSystem +{ + public class IdxEntry + { + public const string DefaultExtension = "wav"; + + public readonly uint Hash; + public readonly string Name; + public readonly string Extension; + public readonly uint Offset; + public readonly uint Length; + public readonly uint SampleRate; + public readonly uint Flags; + public readonly uint ChunkSize; + + public IdxEntry(uint hash, uint offset, uint length, uint sampleRate, uint flags, uint chuckSize) + { + Hash = hash; + Offset = offset; + Length = length; + SampleRate = sampleRate; + Flags = flags; + ChunkSize = chuckSize; + } + + public IdxEntry(Stream s) + { + var asciiname = s.ReadASCII(16); + + var pos = asciiname.IndexOf('\0'); + if (pos != 0) + asciiname = asciiname.Substring(0, pos); + + Name = asciiname; + Extension = DefaultExtension; + Offset = s.ReadUInt32(); + Length = s.ReadUInt32(); + SampleRate = s.ReadUInt32(); + Flags = s.ReadUInt32(); + ChunkSize = s.ReadUInt32(); + Hash = HashFilename(string.Concat(Name, ".", Extension), PackageHashType.CRC32); + } + + public void Write(BinaryWriter w) + { + w.Write(Name.PadRight(16, '\0')); + w.Write(Offset); + w.Write(Length); + w.Write(SampleRate); + w.Write(Flags); + w.Write(ChunkSize); + } + + public override string ToString() + { + string filename; + if (names.TryGetValue(Hash, out filename)) + return "{0} - offset 0x{1:x8} - length 0x{2:x8}".F(filename, Offset, Length); + else + return "0x{0:x8} - offset 0x{1:x8} - length 0x{2:x8}".F(Hash, Offset, Length); + } + + public static uint HashFilename(string name, PackageHashType type) + { + return PackageEntry.HashFilename(name, type); + } + + static Dictionary names = new Dictionary(); + + public static void AddStandardName(string s) + { + // RA1 and TD + var hash = HashFilename(s, PackageHashType.Classic); + names.Add(hash, s); + + // TS + var crcHash = HashFilename(s, PackageHashType.CRC32); + names.Add(crcHash, s); + } + } +} diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 3329e3bd84..053f561dca 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -85,7 +85,10 @@ + + + @@ -139,6 +142,7 @@ + diff --git a/OpenRA.Game/Primitives/MergedStream.cs b/OpenRA.Game/Primitives/MergedStream.cs new file mode 100644 index 0000000000..e196af559e --- /dev/null +++ b/OpenRA.Game/Primitives/MergedStream.cs @@ -0,0 +1,116 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 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. For more information, + * see COPYING. + */ +#endregion + +using System.IO; + +namespace OpenRA.Primitives +{ + public class MergedStream : Stream + { + public Stream Stream1 { get; set; } + public Stream Stream2 { get; set; } + + long VirtualLength { get; set; } + + public MergedStream(Stream stream1, Stream stream2) + { + Stream1 = stream1; + Stream2 = stream2; + + VirtualLength = Stream1.Length + Stream2.Length; + } + + public override void Flush() + { + Stream1.Flush(); + Stream2.Flush(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + var position = Position; + + switch (origin) + { + case SeekOrigin.Begin: + position = offset; + break; + case SeekOrigin.Current: + position += offset; + break; + case SeekOrigin.End: + position = Length; + position += offset; + break; + } + + if (position >= Stream1.Length) + Position = Stream1.Length + Stream2.Seek(offset - Stream1.Length, SeekOrigin.Begin); + else + Position = Stream1.Seek(offset, SeekOrigin.Begin); + + return position; + } + + public override void SetLength(long value) + { + VirtualLength = value; + } + + public override int Read(byte[] buffer, int offset, int count) + { + int bytesRead; + + if (Position >= Stream1.Length) + bytesRead = Stream2.Read(buffer, offset, count); + else if (count > Stream1.Length) + { + bytesRead = Stream1.Read(buffer, offset, (int)Stream1.Length); + bytesRead += Stream2.Read(buffer, (int)Stream1.Length, count - (int)Stream1.Length); + } + else + bytesRead = Stream1.Read(buffer, offset, count); + + Position += bytesRead; + + return bytesRead; + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (Position >= Stream1.Length) + Stream2.Write(buffer, offset - (int)Stream1.Length, count); + else + Stream1.Write(buffer, offset, count); + } + + public override bool CanRead + { + get { return Stream1.CanRead && Stream2.CanRead; } + } + + public override bool CanSeek + { + get { return Stream1.CanSeek && Stream2.CanSeek; } + } + + public override bool CanWrite + { + get { return Stream1.CanWrite && Stream2.CanWrite; } + } + + public override long Length + { + get { return VirtualLength; } + } + + public override long Position { get; set; } + } +}