Object oriented sound loader approach

Reshapes the ISoundLoader interface and
adds a new ISoundFormat interface to allow streaming in the near future
This commit is contained in:
teees
2016-02-02 10:18:50 +01:00
committed by Paul Chote
parent c32bf9f8f7
commit 0193ee5b3c
7 changed files with 180 additions and 151 deletions

View File

@@ -45,6 +45,55 @@ namespace OpenRA.FileFormats
} }
public class AudLoader : ISoundLoader public class AudLoader : ISoundLoader
{
bool ISoundLoader.TryParseSound(Stream stream, out ISoundFormat sound)
{
try
{
sound = new AudFormat(stream);
return true;
}
catch
{
// Not a supported AUD
}
sound = null;
return false;
}
}
public class AudFormat : ISoundFormat
{
public int Channels { get { return 1; } }
public int SampleBits { get { return 16; } }
public int SampleRate { get { return sampleRate; } }
public float LengthInSeconds { get { return AudReader.SoundLength(stream); } }
public Stream GetPCMInputStream() { return new MemoryStream(rawData); }
int sampleRate;
byte[] rawData;
Stream stream;
public AudFormat(Stream stream)
{
this.stream = stream;
var position = stream.Position;
try
{
if (!AudReader.LoadSound(stream, out rawData, out sampleRate))
throw new InvalidDataException();
}
finally
{
stream.Position = position;
}
}
}
public class AudReader
{ {
static readonly int[] IndexAdjust = { -1, -1, -1, -1, 2, 4, 6, 8 }; static readonly int[] IndexAdjust = { -1, -1, -1, -1, 2, 4, 6, 8 };
static readonly int[] StepTable = static readonly int[] StepTable =
@@ -166,38 +215,5 @@ namespace OpenRA.FileFormats
rawData = output; rawData = output;
return true; 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;
var position = stream.Position;
try
{
if (!LoadSound(stream, out rawData, out sampleRate))
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("sound", "Failed to parse AUD file {0}. Error message:".F(fileName));
Log.Write("sound", e.ToString());
rawData = null;
return false;
}
finally
{
stream.Position = position;
}
channels = 1;
sampleBits = 16;
return true;
}
} }
} }

View File

@@ -18,38 +18,30 @@ namespace OpenRA.FileFormats
{ {
public class VocLoader : ISoundLoader public class VocLoader : ISoundLoader
{ {
bool ISoundLoader.TryParseSound(Stream stream, string fileName, out byte[] rawData, out int channels, out int sampleBits, out int sampleRate) bool ISoundLoader.TryParseSound(Stream stream, out ISoundFormat sound)
{ {
var position = stream.Position;
try try
{ {
var vocStream = new VocStream(stream); sound = new VocFormat(stream);
channels = vocStream.Channels; return true;
sampleBits = vocStream.BitsPerSample;
sampleRate = vocStream.SampleRate;
rawData = vocStream.ReadAllBytes();
} }
catch catch
{ {
rawData = null; // Not a (supported) WAV
channels = sampleBits = sampleRate = 0;
return false;
}
finally
{
stream.Position = position;
} }
return true; sound = null;
return false;
} }
} }
public class VocStream : Stream public class VocFormat : ISoundFormat
{ {
public int BitsPerSample { get { return 8; } } public int SampleBits { get { return 8; } }
public int Channels { get { return 1; } } public int Channels { get { return 1; } }
public int SampleRate { get; private set; } public int SampleRate { get; private set; }
public float LengthInSeconds { get { return (float)totalSamples / SampleRate; } }
public Stream GetPCMInputStream() { return new VocStream(this); }
int totalSamples = 0; int totalSamples = 0;
int samplePosition = 0; int samplePosition = 0;
@@ -98,9 +90,10 @@ namespace OpenRA.FileFormats
public int Count; public int Count;
} }
public VocStream(Stream stream) public VocFormat(Stream stream)
{ {
this.stream = stream; this.stream = stream;
CheckVocHeader(); CheckVocHeader();
Preload(); Preload();
} }
@@ -187,7 +180,7 @@ namespace OpenRA.FileFormats
break; break;
} }
// Silence // Silence
case 3: case 3:
{ {
if (block.Length != 3) if (block.Length != 3)
@@ -199,7 +192,7 @@ namespace OpenRA.FileFormats
break; break;
} }
// Repeat start // Repeat start
case 6: case 6:
{ {
if (block.Length != 2) if (block.Length != 2)
@@ -208,11 +201,11 @@ namespace OpenRA.FileFormats
break; break;
} }
// Repeat end // Repeat end
case 7: case 7:
break; break;
// Extra info // Extra info
case 8: case 8:
{ {
if (block.Length != 4) if (block.Length != 4)
@@ -316,44 +309,7 @@ namespace OpenRA.FileFormats
} }
} }
public byte[] ReadAllBytes() int Read(byte[] buffer, int offset, int count)
{
Rewind();
var buffer = new byte[totalSamples];
Read(buffer, 0, totalSamples);
return buffer;
}
public override bool CanRead { get { return true; } }
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return false; } }
public override long Length { get { return totalSamples; } }
public override long Position
{
get { return samplePosition; }
set { throw new NotImplementedException(); }
}
public override void Flush()
{
throw new NotImplementedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override int Read(byte[] buffer, int offset, int count)
{ {
var bytesWritten = 0; var bytesWritten = 0;
var samplesLeft = Math.Min(count, buffer.Length - offset); var samplesLeft = Math.Min(count, buffer.Length - offset);
@@ -362,7 +318,7 @@ namespace OpenRA.FileFormats
var len = FillBuffer(samplesLeft); var len = FillBuffer(samplesLeft);
if (len == 0) if (len == 0)
break; break;
Array.Copy(this.buffer, 0, buffer, offset, len); Buffer.BlockCopy(this.buffer, 0, buffer, offset, len);
samplesLeft -= len; samplesLeft -= len;
offset += len; offset += len;
bytesWritten += len; bytesWritten += len;
@@ -371,9 +327,34 @@ namespace OpenRA.FileFormats
return bytesWritten; return bytesWritten;
} }
public override void Write(byte[] buffer, int offset, int count) public class VocStream : Stream
{ {
throw new NotImplementedException(); VocFormat format;
public VocStream(VocFormat format)
{
this.format = format;
}
public override bool CanRead { get { return format.samplePosition < format.totalSamples; } }
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return false; } }
public override long Length { get { return format.totalSamples; } }
public override long Position
{
get { return format.samplePosition; }
set { throw new NotImplementedException(); }
}
public override int Read(byte[] buffer, int offset, int count)
{
return format.Read(buffer, offset, count);
}
public override void Flush() { throw new NotImplementedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); }
public override void SetLength(long value) { throw new NotImplementedException(); }
public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); }
} }
} }
} }

View File

@@ -15,6 +15,63 @@ using System.IO;
namespace OpenRA.FileFormats namespace OpenRA.FileFormats
{ {
public class WavLoader : ISoundLoader public class WavLoader : ISoundLoader
{
bool ISoundLoader.TryParseSound(Stream stream, out ISoundFormat sound)
{
try
{
sound = new WavFormat(stream);
return true;
}
catch
{
// Not a (supported) WAV
}
sound = null;
return false;
}
}
public class WavFormat : ISoundFormat
{
public int Channels { get { return channels; } }
public int SampleBits { get { return sampleBits; } }
public int SampleRate { get { return sampleRate; } }
public float LengthInSeconds { get { return WavReader.WaveLength(stream); } }
public Stream GetPCMInputStream() { return new MemoryStream(rawData); }
int channels;
int sampleBits;
int sampleRate;
byte[] rawData;
readonly Stream stream;
public WavFormat(Stream stream)
{
this.stream = stream;
var wavReader = new WavReader();
var position = stream.Position;
try
{
if (!wavReader.LoadSound(stream))
throw new InvalidDataException();
}
finally
{
stream.Position = position;
}
rawData = wavReader.RawOutput;
channels = wavReader.Channels;
sampleBits = wavReader.BitsPerSample;
sampleRate = wavReader.SampleRate;
}
}
public class WavReader
{ {
public int FileSize; public int FileSize;
public string Format; public string Format;
@@ -34,7 +91,7 @@ namespace OpenRA.FileFormats
public enum WaveType { Pcm = 0x1, ImaAdpcm = 0x11 } public enum WaveType { Pcm = 0x1, ImaAdpcm = 0x11 }
public static WaveType Type { get; private set; } public static WaveType Type { get; private set; }
bool LoadSound(Stream s) public bool LoadSound(Stream s)
{ {
var type = s.ReadASCII(4); var type = s.ReadASCII(4);
if (type != "RIFF") if (type != "RIFF")
@@ -176,40 +233,5 @@ namespace OpenRA.FileFormats
return output; 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;
var position = stream.Position;
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("sound", "Failed to parse WAV file {0}. Error message:".F(fileName));
Log.Write("sound", e.ToString());
return false;
}
finally
{
stream.Position = position;
}
rawData = RawOutput;
channels = Channels;
sampleBits = BitsPerSample;
sampleRate = SampleRate;
return true;
}
} }
} }

View File

@@ -68,7 +68,7 @@ namespace OpenRA.FileSystem
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE")); waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE"));
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt ")); waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt "));
waveHeaderMemoryStream.Write(BitConverter.GetBytes(16)); waveHeaderMemoryStream.Write(BitConverter.GetBytes(16));
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)WavLoader.WaveType.Pcm)); waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)WavReader.WaveType.Pcm));
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels)); waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels));
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate)); waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate));
waveHeaderMemoryStream.Write(BitConverter.GetBytes(2 * channels * entry.SampleRate)); waveHeaderMemoryStream.Write(BitConverter.GetBytes(2 * channels * entry.SampleRate));
@@ -90,7 +90,7 @@ namespace OpenRA.FileSystem
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE")); waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("WAVE"));
waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt ")); waveHeaderMemoryStream.Write(Encoding.ASCII.GetBytes("fmt "));
waveHeaderMemoryStream.Write(BitConverter.GetBytes(20)); waveHeaderMemoryStream.Write(BitConverter.GetBytes(20));
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)WavLoader.WaveType.ImaAdpcm)); waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)WavReader.WaveType.ImaAdpcm));
waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels)); waveHeaderMemoryStream.Write(BitConverter.GetBytes((short)channels));
waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate)); waveHeaderMemoryStream.Write(BitConverter.GetBytes(entry.SampleRate));
waveHeaderMemoryStream.Write(BitConverter.GetBytes(bytesPerSec)); waveHeaderMemoryStream.Write(BitConverter.GetBytes(bytesPerSec));

View File

@@ -44,9 +44,9 @@ namespace OpenRA.GameRules
using (var s = filesystem.Open(Filename)) using (var s = filesystem.Open(Filename))
{ {
if (Filename.ToLowerInvariant().EndsWith("wav")) if (Filename.ToLowerInvariant().EndsWith("wav"))
Length = (int)WavLoader.WaveLength(s); Length = (int)WavReader.WaveLength(s);
else else
Length = (int)AudLoader.SoundLength(s); Length = (int)AudReader.SoundLength(s);
} }
} }
} }

View File

@@ -21,7 +21,16 @@ namespace OpenRA
{ {
public interface ISoundLoader public interface ISoundLoader
{ {
bool TryParseSound(Stream stream, string fileName, out byte[] rawData, out int channels, out int sampleBits, out int sampleRate); bool TryParseSound(Stream stream, out ISoundFormat sound);
}
public interface ISoundFormat
{
int Channels { get; }
int SampleBits { get; }
int SampleRate { get; }
float LengthInSeconds { get; }
Stream GetPCMInputStream();
} }
public sealed class Sound : IDisposable public sealed class Sound : IDisposable
@@ -35,7 +44,7 @@ namespace OpenRA
public Sound(string engineName) public Sound(string engineName)
{ {
var enginePath = Platform.ResolvePath(Path.Combine(".", "OpenRA.Platforms." + engineName + ".dll")); var enginePath = Platform.ResolvePath(".", "OpenRA.Platforms." + engineName + ".dll");
soundEngine = CreateDevice(Assembly.LoadFile(enginePath)); soundEngine = CreateDevice(Assembly.LoadFile(enginePath));
} }
@@ -60,16 +69,17 @@ namespace OpenRA
using (var stream = fileSystem.Open(filename)) using (var stream = fileSystem.Open(filename))
{ {
byte[] rawData; ISoundFormat soundFormat;
int channels; foreach (var loader in Game.ModData.SoundLoaders)
int sampleBits; {
int sampleRate; stream.Position = 0;
foreach (var loader in loaders) if (loader.TryParseSound(stream, out soundFormat))
if (loader.TryParseSound(stream, filename, out rawData, out channels, out sampleBits, out sampleRate)) return soundEngine.AddSoundSourceFromMemory(
return soundEngine.AddSoundSourceFromMemory(rawData, channels, sampleBits, sampleRate); soundFormat.GetPCMInputStream().ReadAllBytes(), soundFormat.Channels, soundFormat.SampleBits, soundFormat.SampleRate);
}
throw new InvalidDataException(filename + " is not a valid sound file!");
} }
throw new InvalidDataException(filename + " is not a valid sound file!");
} }
public void Initialize(ISoundLoader[] loaders, IReadOnlyFileSystem fileSystem) public void Initialize(ISoundLoader[] loaders, IReadOnlyFileSystem fileSystem)

View File

@@ -221,7 +221,7 @@ namespace OpenRA.Mods.Common.FileFormats
} }
if (audioChannels == 1) if (audioChannels == 1)
audioData = compressed ? AudLoader.LoadSound(audio1.ToArray(), ref adpcmIndex) : audio1.ToArray(); audioData = compressed ? AudReader.LoadSound(audio1.ToArray(), ref adpcmIndex) : audio1.ToArray();
else else
{ {
byte[] leftData, rightData; byte[] leftData, rightData;
@@ -233,9 +233,9 @@ namespace OpenRA.Mods.Common.FileFormats
else else
{ {
adpcmIndex = 0; adpcmIndex = 0;
leftData = AudLoader.LoadSound(audio1.ToArray(), ref adpcmIndex); leftData = AudReader.LoadSound(audio1.ToArray(), ref adpcmIndex);
adpcmIndex = 0; adpcmIndex = 0;
rightData = AudLoader.LoadSound(audio2.ToArray(), ref adpcmIndex); rightData = AudReader.LoadSound(audio2.ToArray(), ref adpcmIndex);
} }
audioData = new byte[rightData.Length + leftData.Length]; audioData = new byte[rightData.Length + leftData.Length];