Skip to content

Commit

Permalink
fixed an oversight where metadata wasn't ignored for .wav audio strea…
Browse files Browse the repository at this point in the history
…ms, cleaned up the audio initialization in FormatWav
  • Loading branch information
absoluteAquarian committed Jan 14, 2025
1 parent 289cc2f commit b764ebd
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 212 deletions.
2 changes: 1 addition & 1 deletion API/MonoSoundLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public static class MonoSoundLibrary {
internal const string VersionLiteral_FilterOverhaul = "1.8";

/// <inheritdoc cref="Version"/>
public const string VersionLiteral = "1.8";
public const string VersionLiteral = "1.8.0.1";

/// <summary>
/// The version for MonoSound
Expand Down
254 changes: 58 additions & 196 deletions Audio/FormatWav.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ public sealed class FormatWav : IDisposable {
// http://soundfile.sapp.org/doc/WaveFormat/
// https://medium.com/swlh/reversing-a-wav-file-in-c-482fc3dfe3c4

private const int RIFF_HEADER_SIZE = 12; // "RIFF" + Size + "WAVE"
private const int SUBCHUNK_HEADER_SIZE = 8;
private const int FMT_SUBCHUNK_DATA_LENGTH = 16;

private byte[] data;
private int _fmtSubchunkOffset = RIFF_HEADER_SIZE;
private int _dataSubchunkOffset = RIFF_HEADER_SIZE + SUBCHUNK_HEADER_SIZE + FMT_SUBCHUNK_DATA_LENGTH;

//Header data is always the first 44 bytes
/// <summary>
/// "RIFF" if the file stores its values as Little-Endian, "RIFX" otherwise
/// </summary>
Expand All @@ -36,51 +41,51 @@ public sealed class FormatWav : IDisposable {
/// <summary>
/// The format chunk marker. Always set to "fmt "
/// </summary>
public string FormatChunkMarker => Encoding.UTF8.GetString(data, 12, 4);
public string FormatChunkMarker => Encoding.UTF8.GetString(data, _fmtSubchunkOffset, 4);
/// <summary>
/// The length of the format data in bytes
/// </summary>
public int FormatLength => BitConverter.ToInt32(data, 16);
public int FormatLength => BitConverter.ToInt32(data, _fmtSubchunkOffset + 4);
/// <summary>
/// The audio format each sample is saved as.
/// <para>PCM is 1</para>
/// <para>Any other values are assumed to be some other form of compression</para>
/// </summary>
public short FormatType => BitConverter.ToInt16(data, 20);
public short FormatType => BitConverter.ToInt16(data, _fmtSubchunkOffset + 8);
/// <summary>
/// What channels this WAV sound will use.
/// <para>Mono is 1</para>
/// <para>Stereo is 2</para>
/// </summary>
public short ChannelCount => BitConverter.ToInt16(data, 22);
public short ChannelCount => BitConverter.ToInt16(data, _fmtSubchunkOffset + 10);
/// <summary>
/// How many samples are played per second. Measured in Hertz (Hz)
/// </summary>
public int SampleRate => BitConverter.ToInt32(data, 24);
public int SampleRate => BitConverter.ToInt32(data, _fmtSubchunkOffset + 12);
/// <summary>
/// How many bytes are played per second
/// <para>Usually set to: <c>SampleRate * BitsPerSample * ChannelCount / 8</c></para>
/// </summary>
public int ByteRate => BitConverter.ToInt32(data, 28);
public int ByteRate => BitConverter.ToInt32(data, _fmtSubchunkOffset + 16);
/// <summary>
/// The number of bytes per sample including all channels
/// <para>Usually set to: <c>BitsPerSample * ChannelCount / 8</c></para>
/// </summary>
public short BlockAlign => BitConverter.ToInt16(data, 32);
public short BlockAlign => BitConverter.ToInt16(data, _fmtSubchunkOffset + 20);
/// <summary>
/// How many bits PER CHANNEL are in one sample
/// </summary>
public short BitsPerSample => BitConverter.ToInt16(data, 34);
public short BitsPerSample => BitConverter.ToInt16(data, _fmtSubchunkOffset + 22);
/// <summary>
/// The data chunk marker. Always set to "data"
/// </summary>
public string DataChunkMarker => Encoding.UTF8.GetString(data, 36, 4);
public string DataChunkMarker => Encoding.UTF8.GetString(data, _fmtSubchunkOffset + 24, 4);
/// <summary>
/// The length of the sample data in bytes
/// </summary>
public int DataLength {
get => BitConverter.ToInt32(data, 40);
private set => BitConverter.GetBytes(value).CopyTo(data, 40);
get => BitConverter.ToInt32(data, _dataSubchunkOffset + 4);
private set => BitConverter.GetBytes(value).CopyTo(data, _dataSubchunkOffset + 4);
}

/// <summary>
Expand All @@ -105,7 +110,7 @@ public WavSample[] GetSamples() {
/// <returns></returns>
public byte[] GetSoundBytes() {
byte[] audioData = new byte[DataLength];
Buffer.BlockCopy(data, 44, audioData, 0, audioData.Length);
Buffer.BlockCopy(data, _dataSubchunkOffset + 8, audioData, 0, audioData.Length);
return audioData;
}

Expand Down Expand Up @@ -269,39 +274,14 @@ public static FormatWav FromFileOGG(Stream readStream) {

using VorbisReader reader = new VorbisReader(readStream, closeStreamOnDispose: true);

byte[] fmtChunk = new byte[16];

//Type of Format
fmtChunk[0] = 0x01;

//Number of Channels
byte[] arr = BitConverter.GetBytes((short)reader.Channels);
fmtChunk[2] = arr[0];
fmtChunk[3] = arr[1];

//Samples per Second
arr = BitConverter.GetBytes(reader.SampleRate);
fmtChunk[4] = arr[0];
fmtChunk[5] = arr[1];
fmtChunk[6] = arr[2];
fmtChunk[7] = arr[3];

//Bytes per Second
arr = BitConverter.GetBytes(reader.SampleRate * reader.Channels * 2);
fmtChunk[8] = arr[0];
fmtChunk[9] = arr[1];
fmtChunk[10] = arr[2];
fmtChunk[11] = arr[3];

//Block Align
arr = BitConverter.GetBytes((short)(reader.Channels * 2));
fmtChunk[12] = arr[0];
fmtChunk[13] = arr[1];

//Bits per Sample
arr = BitConverter.GetBytes((short)16);
fmtChunk[14] = arr[0];
fmtChunk[15] = arr[1];
byte[] fmtChunk = [
0x01, 0x00,
.. BitConverter.GetBytes((short)reader.Channels),
.. BitConverter.GetBytes(reader.SampleRate),
.. BitConverter.GetBytes(reader.SampleRate * reader.Channels * 2),
.. BitConverter.GetBytes((short)(reader.Channels * 2)),
0x10, 0x00 // 16-bit PCM
];

//Read the samples
float[] buffer = new float[reader.SampleRate / 10 * reader.Channels];
Expand Down Expand Up @@ -378,54 +358,18 @@ private static IEnumerable<byte> AsBytes(string word) {
}

internal static FormatWav FromDecompressorData(byte[] sampleData, byte[] fmtChunk) {
byte[] addon = new byte[44];
addon[0] = (byte)'R';
addon[1] = (byte)'I';
addon[2] = (byte)'F';
addon[3] = (byte)'F';

//Excluded: Total file size

addon[8] = (byte)'W';
addon[9] = (byte)'A';
addon[10] = (byte)'V';
addon[11] = (byte)'E';

addon[12] = (byte)'f';
addon[13] = (byte)'m';
addon[14] = (byte)'t';
addon[15] = (byte)' ';

//Format header length
addon[16] = 16;

//Type of Format, Number of Channels, Samples per Second, Bytes per Second, Block Align, Bits per Sample
Buffer.BlockCopy(fmtChunk, 0, addon, 20, 16);

addon[36] = (byte)'d';
addon[37] = (byte)'a';
addon[38] = (byte)'t';
addon[39] = (byte)'a';

byte[] arr = BitConverter.GetBytes(sampleData.Length);
addon[40] = arr[0];
addon[41] = arr[1];
addon[42] = arr[2];
addon[43] = arr[3];

byte[] actualStream = new byte[addon.Length + sampleData.Length];

//Copy the data
Buffer.BlockCopy(addon, 0, actualStream, 0, addon.Length);
Buffer.BlockCopy(sampleData, 0, actualStream, addon.Length, sampleData.Length);

arr = BitConverter.GetBytes(actualStream.Length);
actualStream[4] = arr[0];
actualStream[5] = arr[1];
actualStream[6] = arr[2];
actualStream[7] = arr[3];
byte[] header = [
.. AsBytes("RIFF"),
.. BitConverter.GetBytes(RIFF_HEADER_SIZE + SUBCHUNK_HEADER_SIZE + FMT_SUBCHUNK_DATA_LENGTH + SUBCHUNK_HEADER_SIZE + sampleData.Length),
.. AsBytes("WAVE"),
.. AsBytes("fmt "),
0x10, 0x00, 0x00, 0x00, // Length of format chunk = 16 bytes
.. fmtChunk,
.. AsBytes("data"),
.. BitConverter.GetBytes(sampleData.Length) // Sample data length
];

return FromBytes(actualStream);
return FromBytes([ .. header, .. sampleData ]);
}

/// <summary>
Expand All @@ -450,39 +394,14 @@ public static FormatWav FromFileMP3(Stream readStream) {
try {
using MP3Stream stream = new MP3Stream(readStream);

byte[] fmtChunk = new byte[16];

//Type of Format
fmtChunk[0] = 0x01;

//Number of Channels
byte[] arr = BitConverter.GetBytes((short)AudioChannels.Stereo); //MP3 decoder forces the samples to align to stereo
fmtChunk[2] = arr[0];
fmtChunk[3] = arr[1];

//Samples per Second
arr = BitConverter.GetBytes(stream.Frequency);
fmtChunk[4] = arr[0];
fmtChunk[5] = arr[1];
fmtChunk[6] = arr[2];
fmtChunk[7] = arr[3];

//Bytes per Second
arr = BitConverter.GetBytes(stream.Frequency * 4);
fmtChunk[8] = arr[0];
fmtChunk[9] = arr[1];
fmtChunk[10] = arr[2];
fmtChunk[11] = arr[3];

//Block Align
arr = BitConverter.GetBytes((short)4);
fmtChunk[12] = arr[0];
fmtChunk[13] = arr[1];

//Bits per Sample
arr = BitConverter.GetBytes((short)16);
fmtChunk[14] = arr[0];
fmtChunk[15] = arr[1];
byte[] fmtChunk = [
0x01, 0x00, // PCM format
0x02, 0x00, // MP3 decoder forces the samples to align to Stereo
.. BitConverter.GetBytes(stream.Frequency),
.. BitConverter.GetBytes(stream.Frequency * 4),
0x04, 0x00, // Block Align is always 4 bytes
0x10, 0x00 // 16-bit PCM
];

//Read the samples
byte[] sampleWrite = new byte[1024];
Expand Down Expand Up @@ -513,7 +432,7 @@ public static FormatWav FromFileMP3(Stream readStream) {
/// Loads a <see cref="FormatWav"/> from a .wav byte stream
/// </summary>
public static FormatWav FromBytes(byte[] data) {
if (data.Length < 44)
if (data.Length < RIFF_HEADER_SIZE + SUBCHUNK_HEADER_SIZE + FMT_SUBCHUNK_DATA_LENGTH + SUBCHUNK_HEADER_SIZE)
throw new ArgumentException("Data was too short to contain a header.", nameof(data));

FormatWav wav = new FormatWav() {
Expand All @@ -522,50 +441,11 @@ public static FormatWav FromBytes(byte[] data) {

//Verify that the input data was correct
try {
HeaderCheckStart:

string eHeader = wav.EndianHeader;
if (eHeader != "RIFF" && eHeader != "RIFX")
throw new Exception("Endian header string was not \"RIFF\" nor \"RIFX\".");

if (wav.FileTypeHeader != "WAVE")
throw new Exception("File type header string was not \"WAVE\".");

if (wav.FormatChunkMarker != "fmt ")
throw new Exception("Format chunk header string was not \"fmt \".");

if (wav.DataChunkMarker != "data") {
//If the data chunk marker was instead "LIST", then there's metadata in the WAV file
//That metadata is completely irrelevant for MonoSound, so the data array needs to be rebuilt with the "LIST" chunk missing
if (wav.DataChunkMarker == "LIST") {
int infoLength = wav.DataLength;
using MemoryStream ms = new MemoryStream(data);
using WAVEReader reader = new WAVEReader(ms);

byte[] overwrite = new byte[data.Length - infoLength];
Buffer.BlockCopy(data, 0, overwrite, 0, 36);

int afterInfo = 44 + infoLength;
Buffer.BlockCopy(data, afterInfo, overwrite, 36, data.Length - afterInfo);

byte[] bytes = BitConverter.GetBytes(overwrite.Length);
overwrite[4] = bytes[0];
overwrite[5] = bytes[1];
overwrite[6] = bytes[2];
overwrite[7] = bytes[3];

wav.data = overwrite;

goto HeaderCheckStart;
}

throw new Exception("Data chunk header string was not \"data\".");
}

if (wav.data.Length != wav.Size)
throw new Exception("File size did not match stored size.");

int sampleRate = wav.SampleRate;
if (sampleRate < 8000 || sampleRate > 48000)
throw new Exception("Sample rate was outside the range of valid values.");
wav._fmtSubchunkOffset = reader.ReadFormat(out _, out _, out _, out _, out _);
wav._dataSubchunkOffset = reader.ReadDataHeader(out _);
} catch (Exception ex) {
throw new ArgumentException("Data was invalid for the WAV format.", nameof(data), ex);
}
Expand All @@ -574,32 +454,14 @@ public static FormatWav FromBytes(byte[] data) {
}

internal static FormatWav FromSoundEffectConstructor(XACT.MiniFormatTag codec, byte[] buffer, int channels, int sampleRate, int blockAlignment, int loopStart, int loopLength) {
//WaveBank sounds always have 16 bits/sample for some reason
const int bitsPerSample = 16;

byte[] fmtChunk = new byte[16];
var bytes = BitConverter.GetBytes((short)1); //Force the PCM encoding... Others aren't allowed
fmtChunk[0] = bytes[0];
fmtChunk[1] = bytes[1];
bytes = BitConverter.GetBytes((short)channels);
fmtChunk[2] = bytes[0];
fmtChunk[3] = bytes[1];
bytes = BitConverter.GetBytes(sampleRate);
fmtChunk[4] = bytes[0];
fmtChunk[5] = bytes[1];
fmtChunk[6] = bytes[2];
fmtChunk[7] = bytes[3];
bytes = BitConverter.GetBytes(sampleRate * channels * bitsPerSample / 8);
fmtChunk[8] = bytes[0];
fmtChunk[9] = bytes[1];
fmtChunk[10] = bytes[2];
fmtChunk[11] = bytes[3];
bytes = BitConverter.GetBytes((short)blockAlignment);
fmtChunk[12] = bytes[0];
fmtChunk[13] = bytes[1];
bytes = BitConverter.GetBytes((short)bitsPerSample);
fmtChunk[14] = bytes[0];
fmtChunk[15] = bytes[1];
byte[] fmtChunk = [
0x01, 0x00, // Force the PCM encoding... Others aren't allowed
.. BitConverter.GetBytes((short)channels),
.. BitConverter.GetBytes(sampleRate),
.. BitConverter.GetBytes(sampleRate * channels * 2),
.. BitConverter.GetBytes((short)blockAlignment),
0x10, 0x00 // WaveBank sounds always have 16 bits/sample for some reason
];

return FromDecompressorData(buffer, fmtChunk);
}
Expand Down
Loading

0 comments on commit b764ebd

Please sign in to comment.