Merge pull request #10069 from penev92/soundLoader

Add plumbing for mod-defined sound loaders
This commit is contained in:
Matthias Mailänder
2015-12-25 14:40:43 +01:00
13 changed files with 171 additions and 62 deletions

View File

@@ -36,16 +36,18 @@ namespace OpenRA.FileFormats
Chunk 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 AudLoader
public class AudLoader : ISoundLoader
{
static int[] indexAdjust = { -1, -1, -1, -1, 2, 4, 6, 8 };
static int[] stepTable =
static readonly int ExpectedSampleRate = 22050;
static readonly int[] IndexAdjust = { -1, -1, -1, -1, 2, 4, 6, 8 };
static readonly int[] StepTable =
{
7, 8, 9, 10, 11, 12, 13, 14, 16,
17, 19, 21, 23, 25, 28, 31, 34, 37,
@@ -64,14 +66,14 @@ namespace OpenRA.FileFormats
var sb = (b & 8) != 0;
b &= 7;
var delta = (stepTable[index] * b) / 4 + stepTable[index] / 8;
var delta = (StepTable[index] * b) / 4 + StepTable[index] / 8;
if (sb) delta = -delta;
current += delta;
if (current > short.MaxValue) current = short.MaxValue;
if (current < short.MinValue) current = short.MinValue;
index += indexAdjust[b];
index += IndexAdjust[b];
if (index < 0) index = 0;
if (index > 88) index = 88;
@@ -117,13 +119,24 @@ namespace OpenRA.FileFormats
return samples / sampleRate;
}
public static byte[] LoadSound(Stream s)
public static bool LoadSound(Stream s, out byte[] rawData)
{
/*var sampleRate =*/ s.ReadUInt16();
rawData = null;
var sampleRate = s.ReadUInt16();
var dataSize = s.ReadInt32();
var outputSize = s.ReadInt32();
/*var flags = (SoundFlags)*/ s.ReadByte();
/*var format = (SoundFormat)*/ s.ReadByte();
var readFlag = s.ReadByte();
var readFormat = s.ReadByte();
if (sampleRate != ExpectedSampleRate)
return false;
if (!Enum.IsDefined(typeof(SoundFlags), readFlag))
return false;
if (!Enum.IsDefined(typeof(SoundFormat), readFormat))
return false;
var output = new byte[outputSize];
var offset = 0;
@@ -153,7 +166,37 @@ namespace OpenRA.FileFormats
dataSize -= 8 + chunk.CompressedSize;
}
return output;
rawData = output;
return true;
}
public bool TryParseSound(Stream stream, string fileName, out byte[] rawData, out int channels, out int sampleBits,
out int sampleRate)
{
channels = sampleBits = sampleRate = 0;
try
{
if (!LoadSound(stream, out rawData))
return false;
}
catch (Exception e)
{
// LoadSound() will check if the stream is in a format that this parser supports.
// If not, it will simply return false so we know we can't use it. If it is, it will start
// parsing the data without any further failsafes, which means that it will crash on corrupted files
// (that end prematurely or otherwise don't conform to the specifications despite the headers being OK).
Log.Write("debug", "Failed to parse AUD file {0}. Error message:".F(fileName));
Log.Write("debug", e.ToString());
rawData = null;
return false;
}
channels = 1;
sampleBits = 16;
sampleRate = ExpectedSampleRate;
return true;
}
}
}

View File

@@ -13,48 +13,53 @@ using System.IO;
namespace OpenRA.FileFormats
{
public class WavLoader
public class WavLoader : ISoundLoader
{
public readonly int FileSize;
public readonly string Format;
public int FileSize;
public string Format;
public readonly int FmtChunkSize;
public readonly int AudioFormat;
public readonly int Channels;
public readonly int SampleRate;
public readonly int ByteRate;
public readonly int BlockAlign;
public readonly int BitsPerSample;
public int FmtChunkSize;
public int AudioFormat;
public int Channels;
public int SampleRate;
public int ByteRate;
public int BlockAlign;
public int BitsPerSample;
public readonly int UncompressedSize;
public readonly int DataSize;
public readonly byte[] RawOutput;
public int UncompressedSize;
public int DataSize;
public byte[] RawOutput;
public enum WaveType { Pcm = 0x1, ImaAdpcm = 0x11 }
public static WaveType Type { get; private set; }
public WavLoader(Stream s)
bool LoadSound(Stream s)
{
var type = s.ReadASCII(4);
if (type != "RIFF")
return false;
FileSize = s.ReadInt32();
Format = s.ReadASCII(4);
if (Format != "WAVE")
return false;
while (s.Position < s.Length)
{
if ((s.Position & 1) == 1)
s.ReadByte(); // Alignment
var type = s.ReadASCII(4);
type = s.ReadASCII(4);
switch (type)
{
case "RIFF":
FileSize = s.ReadInt32();
Format = s.ReadASCII(4);
if (Format != "WAVE")
throw new NotSupportedException("Not a canonical WAVE file.");
break;
case "fmt ":
FmtChunkSize = s.ReadInt32();
AudioFormat = s.ReadInt16();
Type = (WaveType)AudioFormat;
if (Type != WaveType.Pcm && Type != WaveType.ImaAdpcm)
throw new NotSupportedException("Compression type is not supported.");
Channels = s.ReadInt16();
SampleRate = s.ReadInt32();
ByteRate = s.ReadInt32();
@@ -91,6 +96,8 @@ namespace OpenRA.FileFormats
RawOutput = DecodeImaAdpcmData();
BitsPerSample = 16;
}
return true;
}
public static float WaveLength(Stream s)
@@ -176,5 +183,35 @@ namespace OpenRA.FileFormats
return output;
}
public bool TryParseSound(Stream stream, string fileName, out byte[] rawData, out int channels,
out int sampleBits, out int sampleRate)
{
rawData = null;
channels = sampleBits = sampleRate = 0;
try
{
if (!LoadSound(stream))
return false;
}
catch (Exception e)
{
// LoadSound() will check if the stream is in a format that this parser supports.
// If not, it will simply return false so we know we can't use it. If it is, it will start
// parsing the data without any further failsafes, which means that it will crash on corrupted files
// (that end prematurely or otherwise don't conform to the specifications despite the headers being OK).
Log.Write("debug", "Failed to parse WAV file {0}. Error message:".F(fileName));
Log.Write("debug", e.ToString());
return false;
}
rawData = RawOutput;
channels = Channels;
sampleBits = BitsPerSample;
sampleRate = SampleRate;
return true;
}
}
}

View File

@@ -293,10 +293,11 @@ namespace OpenRA
Settings.Game.Mod = mod;
Sound.StopVideo();
Sound.Initialize();
ModData = new ModData(mod, !Settings.Server.Dedicated);
Sound.Initialize();
using (new PerfTimer("LoadMaps"))
ModData.MapCache.LoadMaps();

View File

@@ -72,7 +72,7 @@ namespace OpenRA.Graphics
if (loader.TryParseSprite(stream, out frames))
return frames;
throw new InvalidDataException(filename + " is not a valid sprite file");
throw new InvalidDataException(filename + " is not a valid sprite file!");
}
}
}

View File

@@ -49,12 +49,14 @@ namespace OpenRA
public readonly Dictionary<string, string> RequiresMods;
public readonly Dictionary<string, Pair<string, int>> Fonts;
public readonly string[] SoundFormats = { };
public readonly string[] SpriteFormats = { };
readonly string[] reservedModuleNames = { "Metadata", "Folders", "MapFolders", "Packages", "Rules",
"Sequences", "VoxelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons",
"Voices", "Notifications", "Music", "Translations", "TileSets", "ChromeMetrics", "Missions",
"ServerTraits", "LoadScreen", "LobbyDefaults", "Fonts", "SupportsMapsFrom", "SpriteFormats", "RequiresMods" };
"ServerTraits", "LoadScreen", "LobbyDefaults", "Fonts", "SupportsMapsFrom", "SoundFormats", "SpriteFormats",
"RequiresMods" };
readonly TypeDictionary modules = new TypeDictionary();
readonly Dictionary<string, MiniYaml> yaml;
@@ -113,6 +115,9 @@ namespace OpenRA
MapCompatibility = compat.ToArray();
if (yaml.ContainsKey("SoundFormats"))
SoundFormats = FieldLoader.GetValue<string[]>("SoundFormats", yaml["SoundFormats"].Value);
if (yaml.ContainsKey("SpriteFormats"))
SpriteFormats = FieldLoader.GetValue<string[]>("SpriteFormats", yaml["SpriteFormats"].Value);
}

View File

@@ -25,6 +25,7 @@ namespace OpenRA
public readonly ObjectCreator ObjectCreator;
public readonly WidgetLoader WidgetLoader;
public readonly MapCache MapCache;
public readonly ISoundLoader[] SoundLoaders;
public readonly ISpriteLoader[] SpriteLoaders;
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
public readonly RulesetCache RulesetCache;
@@ -60,17 +61,8 @@ namespace OpenRA
RulesetCache.LoadingProgress += HandleLoadingProgress;
MapCache = new MapCache(this);
var spriteLoaders = new List<ISpriteLoader>();
foreach (var format in Manifest.SpriteFormats)
{
var loader = ObjectCreator.FindType(format + "Loader");
if (loader == null || !loader.GetInterfaces().Contains(typeof(ISpriteLoader)))
throw new InvalidOperationException("Unable to find a sprite loader for type '{0}'.".F(format));
spriteLoaders.Add((ISpriteLoader)ObjectCreator.CreateBasic(loader));
}
SpriteLoaders = spriteLoaders.ToArray();
SoundLoaders = GetLoaders<ISoundLoader>(Manifest.SoundFormats, "sound");
SpriteLoaders = GetLoaders<ISpriteLoader>(Manifest.SpriteFormats, "sprite");
var sequenceFormat = Manifest.Get<SpriteSequenceFormat>();
var sequenceLoader = ObjectCreator.FindType(sequenceFormat.Type + "Loader");
@@ -113,6 +105,21 @@ namespace OpenRA
CursorProvider = new CursorProvider(this);
}
TLoader[] GetLoaders<TLoader>(IEnumerable<string> formats, string name)
{
var loaders = new List<TLoader>();
foreach (var format in formats)
{
var loader = ObjectCreator.FindType(format + "Loader");
if (loader == null || !loader.GetInterfaces().Contains(typeof(TLoader)))
throw new InvalidOperationException("Unable to find a {0} loader for type '{1}'.".F(name, format));
loaders.Add((TLoader)ObjectCreator.CreateBasic(loader));
}
return loaders.ToArray();
}
public IEnumerable<string> Languages { get; private set; }
void LoadTranslations(Map map)

View File

@@ -9,14 +9,19 @@
#endregion
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using OpenRA.FileFormats;
using OpenRA.GameRules;
using OpenRA.Primitives;
namespace OpenRA
{
public interface ISoundLoader
{
bool TryParseSound(Stream stream, string fileName, out byte[] rawData, out int channels, out int sampleBits, out int sampleRate);
}
public sealed class Sound : IDisposable
{
readonly ISoundEngine soundEngine;
@@ -51,22 +56,21 @@ namespace OpenRA
return null;
}
if (filename.ToLowerInvariant().EndsWith("wav"))
using (var s = Game.ModData.ModFiles.Open(filename))
return LoadWave(new WavLoader(s));
using (var stream = Game.ModData.ModFiles.Open(filename))
{
byte[] rawData;
int channels;
int sampleBits;
int sampleRate;
foreach (var loader in Game.ModData.SoundLoaders)
{
stream.Position = 0;
if (loader.TryParseSound(stream, filename, out rawData, out channels, out sampleBits, out sampleRate))
return soundEngine.AddSoundSourceFromMemory(rawData, channels, sampleBits, sampleRate);
}
using (var s = Game.ModData.ModFiles.Open(filename))
return LoadSoundRaw(AudLoader.LoadSound(s), 1, 16, 22050);
}
ISoundSource LoadWave(WavLoader wave)
{
return soundEngine.AddSoundSourceFromMemory(wave.RawOutput, wave.Channels, wave.BitsPerSample, wave.SampleRate);
}
ISoundSource LoadSoundRaw(byte[] rawData, int channels, int sampleBits, int sampleRate)
{
return soundEngine.AddSoundSourceFromMemory(rawData, channels, sampleBits, sampleRate);
throw new InvalidDataException(filename + " is not a valid sound file!");
}
}
public void Initialize()
@@ -121,7 +125,7 @@ namespace OpenRA
public void PlayVideo(byte[] raw, int channels, int sampleBits, int sampleRate)
{
rawSource = LoadSoundRaw(raw, channels, sampleBits, sampleRate);
rawSource = soundEngine.AddSoundSourceFromMemory(raw, channels, sampleBits, sampleRate);
video = soundEngine.Play2D(rawSource, false, true, WPos.Zero, InternalSoundVolume, false);
}

View File

@@ -33,6 +33,8 @@ Fonts:
LobbyDefaults:
SoundFormats:
SpriteFormats:
SpriteSequenceFormat: DefaultSpriteSequence

View File

@@ -214,6 +214,8 @@ MapGrid:
SupportsMapsFrom: cnc
SoundFormats: Aud, Wav
SpriteFormats: ShpTD, TmpTD, ShpTS, TmpRA
SpriteSequenceFormat: TilesetSpecificSpriteSequence

View File

@@ -195,6 +195,8 @@ Missions:
SupportsMapsFrom: d2k
SoundFormats: Aud, Wav
SpriteFormats: R8, ShpTD, TmpRA
SpriteSequenceFormat: DefaultSpriteSequence

View File

@@ -54,6 +54,8 @@ Fonts:
LobbyDefaults:
SoundFormats:
SpriteFormats: ShpTD
SpriteSequenceFormat: DefaultSpriteSequence

View File

@@ -214,6 +214,8 @@ MapGrid:
SupportsMapsFrom: ra
SoundFormats: Aud, Wav
SpriteFormats: ShpD2, ShpTD, TmpRA, TmpTD, ShpTS
SpriteSequenceFormat: TilesetSpecificSpriteSequence

View File

@@ -251,6 +251,8 @@ Fonts:
SupportsMapsFrom: ts
SoundFormats: Aud, Wav
SpriteFormats: ShpTS, TmpTS, ShpTD
SpriteSequenceFormat: TilesetSpecificSpriteSequence