more audio data and voice restructuring

pull/50/head
cosmonaut 2023-08-02 17:47:44 -07:00
parent 771dc6e7b3
commit 0500d94930
12 changed files with 342 additions and 372 deletions

56
src/Audio/AudioBuffer.cs Normal file
View File

@ -0,0 +1,56 @@
using System;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
public class AudioBuffer : AudioResource
{
IntPtr BufferDataPtr;
uint BufferDataLength;
private bool OwnsBufferData;
public Format Format { get; }
public AudioBuffer(
AudioDevice device,
Format format,
IntPtr bufferPtr,
uint bufferLengthInBytes,
bool ownsBufferData) : base(device)
{
Format = format;
BufferDataPtr = bufferPtr;
BufferDataLength = bufferLengthInBytes;
OwnsBufferData = ownsBufferData;
}
public AudioBuffer CreateSubBuffer(int offset, uint length)
{
return new AudioBuffer(Device, Format, BufferDataPtr + offset, length, false);
}
public FAudio.FAudioBuffer ToFAudioBuffer(bool loop = false)
{
return new FAudio.FAudioBuffer
{
Flags = FAudio.FAUDIO_END_OF_STREAM,
pContext = IntPtr.Zero,
pAudioData = BufferDataPtr,
AudioBytes = BufferDataLength,
PlayBegin = 0,
PlayLength = 0,
LoopBegin = 0,
LoopLength = 0,
LoopCount = loop ? FAudio.FAUDIO_LOOP_INFINITE : 0
};
}
protected override unsafe void Destroy()
{
if (OwnsBufferData)
{
NativeMemory.Free((void*) BufferDataPtr);
}
}
}
}

View File

@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
public class AudioDataOgg : AudioData
public class AudioDataOgg : AudioDataStreamable
{
private IntPtr FileDataPtr = IntPtr.Zero;
private IntPtr VorbisHandle = IntPtr.Zero;
@ -14,7 +14,7 @@ namespace MoonWorks.Audio
public override bool Loaded => VorbisHandle != IntPtr.Zero;
public override uint DecodeBufferSize => 32768;
public AudioDataOgg(string filePath)
public AudioDataOgg(AudioDevice device, string filePath) : base(device)
{
FilePath = filePath;
@ -92,5 +92,44 @@ namespace MoonWorks.Audio
FileDataPtr = IntPtr.Zero;
}
}
public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath)
{
var filePointer = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero);
if (error != 0)
{
throw new AudioLoadException("Error loading file!");
}
var info = FAudio.stb_vorbis_get_info(filePointer);
var lengthInFloats =
FAudio.stb_vorbis_stream_length_in_samples(filePointer) * info.channels;
var lengthInBytes = lengthInFloats * Marshal.SizeOf<float>();
var buffer = NativeMemory.Alloc((nuint) lengthInBytes);
FAudio.stb_vorbis_get_samples_float_interleaved(
filePointer,
info.channels,
(nint) buffer,
(int) lengthInFloats
);
FAudio.stb_vorbis_close(filePointer);
var format = new Format
{
Tag = FormatTag.IEEE_FLOAT,
BitsPerSample = 32,
Channels = (ushort) info.channels,
SampleRate = info.sample_rate
};
return new AudioBuffer(
device,
format,
(nint) buffer,
(uint) lengthInBytes,
true);
}
}
}

View File

@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
public class AudioDataQoa : AudioData
public class AudioDataQoa : AudioDataStreamable
{
private IntPtr QoaHandle = IntPtr.Zero;
private IntPtr FileDataPtr = IntPtr.Zero;
@ -18,7 +18,7 @@ namespace MoonWorks.Audio
private uint decodeBufferSize;
public override uint DecodeBufferSize => decodeBufferSize;
public AudioDataQoa(string filePath)
public AudioDataQoa(AudioDevice device, string filePath) : base(device)
{
FilePath = filePath;
@ -102,6 +102,42 @@ namespace MoonWorks.Audio
}
}
public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath)
{
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
var fileDataPtr = NativeMemory.Alloc((nuint) fileStream.Length);
var fileDataSpan = new Span<byte>(fileDataPtr, (int) fileStream.Length);
fileStream.ReadExactly(fileDataSpan);
fileStream.Close();
var qoaHandle = FAudio.qoa_open_from_memory((char*) fileDataPtr, (uint) fileDataSpan.Length, 0);
if (qoaHandle == 0)
{
NativeMemory.Free(fileDataPtr);
Logger.LogError("Error opening QOA file!");
throw new AudioLoadException("Error opening QOA file!");
}
FAudio.qoa_attributes(qoaHandle, out var channels, out var samplerate, out var samples_per_channel_per_frame, out var total_samples_per_channel);
var bufferLengthInBytes = total_samples_per_channel * channels * sizeof(short);
var buffer = NativeMemory.Alloc(bufferLengthInBytes);
FAudio.qoa_decode_entire(qoaHandle, (short*) buffer);
FAudio.qoa_close(qoaHandle);
NativeMemory.Free(fileDataPtr);
var format = new Format
{
Tag = FormatTag.PCM,
BitsPerSample = 16,
Channels = (ushort) channels,
SampleRate = samplerate
};
return new AudioBuffer(device, format, (nint) buffer, bufferLengthInBytes, true);
}
private static unsafe UInt64 ReverseEndianness(UInt64 value)
{
byte* bytes = (byte*) &value;

View File

@ -1,19 +1,25 @@
using System;
namespace MoonWorks.Audio
{
public abstract class AudioData
public abstract class AudioDataStreamable : AudioResource
{
public Format Format { get; protected set; }
public abstract bool Loaded { get; }
public abstract uint DecodeBufferSize { get; }
public abstract bool Loaded { get; }
protected AudioDataStreamable(AudioDevice device) : base(device)
{
}
/// <summary>
/// Loads the raw audio data into memory.
/// Loads the raw audio data into memory to prepare it for stream decoding.
/// </summary>
public abstract void Load();
/// <summary>
/// Unloads the raw audio data from memory.
/// </summary>
public abstract void Unload();
/// <summary>
/// Seeks to the given sample frame.
/// </summary>
@ -28,9 +34,9 @@ namespace MoonWorks.Audio
/// <param name="reachedEnd">Whether the end of the data was reached on this decode.</param>
public abstract unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd);
/// <summary>
/// Unloads the raw audio data from memory.
/// </summary>
public abstract void Unload();
protected override void Destroy()
{
Unload();
}
}
}

95
src/Audio/AudioDataWav.cs Normal file
View File

@ -0,0 +1,95 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
public static class AudioDataWav
{
// mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385
public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath)
{
// WaveFormatEx data
ushort wFormatTag;
ushort nChannels;
uint nSamplesPerSec;
uint nAvgBytesPerSec;
ushort nBlockAlign;
ushort wBitsPerSample;
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var reader = new BinaryReader(stream);
// RIFF Signature
string signature = new string(reader.ReadChars(4));
if (signature != "RIFF")
{
throw new NotSupportedException("Specified stream is not a wave file.");
}
reader.ReadUInt32(); // Riff Chunk Size
string wformat = new string(reader.ReadChars(4));
if (wformat != "WAVE")
{
throw new NotSupportedException("Specified stream is not a wave file.");
}
// WAVE Header
string format_signature = new string(reader.ReadChars(4));
while (format_signature != "fmt ")
{
reader.ReadBytes(reader.ReadInt32());
format_signature = new string(reader.ReadChars(4));
}
int format_chunk_size = reader.ReadInt32();
wFormatTag = reader.ReadUInt16();
nChannels = reader.ReadUInt16();
nSamplesPerSec = reader.ReadUInt32();
nAvgBytesPerSec = reader.ReadUInt32();
nBlockAlign = reader.ReadUInt16();
wBitsPerSample = reader.ReadUInt16();
// Reads residual bytes
if (format_chunk_size > 16)
{
reader.ReadBytes(format_chunk_size - 16);
}
// data Signature
string data_signature = new string(reader.ReadChars(4));
while (data_signature.ToLowerInvariant() != "data")
{
reader.ReadBytes(reader.ReadInt32());
data_signature = new string(reader.ReadChars(4));
}
if (data_signature != "data")
{
throw new NotSupportedException("Specified wave file is not supported.");
}
int waveDataLength = reader.ReadInt32();
var waveDataBuffer = NativeMemory.Alloc((nuint) waveDataLength);
var waveDataSpan = new Span<byte>(waveDataBuffer, waveDataLength);
stream.ReadExactly(waveDataSpan);
var format = new Format
{
Tag = (FormatTag) wFormatTag,
BitsPerSample = wBitsPerSample,
Channels = nChannels,
SampleRate = nSamplesPerSec
};
return new AudioBuffer(
device,
format,
(nint) waveDataBuffer,
(uint) waveDataLength,
true
);
}
}
}

View File

@ -0,0 +1,25 @@
namespace MoonWorks.Audio
{
public class PersistentVoice : SourceVoice, IPoolable<PersistentVoice>
{
public PersistentVoice(AudioDevice device, Format format) : base(device, format)
{
}
public static PersistentVoice Create(AudioDevice device, Format format)
{
return new PersistentVoice(device, format);
}
/// <summary>
/// Adds an AudioBuffer to the voice queue.
/// The voice processes and plays back the buffers in its queue in the order that they were submitted.
/// </summary>
/// <param name="buffer">The buffer to submit to the voice.</param>
/// <param name="loop">Whether the voice should loop this buffer.</param>
public void Submit(AudioBuffer buffer, bool loop = false)
{
Submit(buffer.ToFAudioBuffer(loop));
}
}
}

View File

@ -12,7 +12,7 @@ namespace MoonWorks.Audio
}
public SoundSequence(AudioDevice device, StaticSound templateSound) : base(device, templateSound.Format)
public SoundSequence(AudioDevice device, AudioBuffer templateSound) : base(device, templateSound.Format)
{
}
@ -21,7 +21,6 @@ namespace MoonWorks.Audio
{
lock (StateLock)
{
if (IsDisposed) { return; }
if (State != SoundState.Playing) { return; }
if (NeedSoundThreshold > 0)
@ -37,18 +36,18 @@ namespace MoonWorks.Audio
}
}
public void EnqueueSound(StaticSound sound)
public void EnqueueSound(AudioBuffer buffer)
{
#if DEBUG
if (!(sound.Format == Format))
if (!(buffer.Format == Format))
{
Logger.LogWarn("Playlist audio format mismatch!");
Logger.LogWarn("Sound sequence audio format mismatch!");
}
#endif
lock (StateLock)
{
Submit(sound.Buffer);
Submit(buffer.ToFAudioBuffer());
}
}
}

View File

@ -5,13 +5,15 @@ namespace MoonWorks.Audio
/// <summary>
/// Emits audio from submitted audio buffers.
/// </summary>
public class SourceVoice : Voice
public abstract class SourceVoice : Voice
{
private Format format;
public Format Format => format;
protected object StateLock = new object();
/// <summary>
/// The number of buffers queued in the voice.
/// This includes the currently playing voice!
/// </summary>
public uint BuffersQueued
{
get
@ -45,6 +47,8 @@ namespace MoonWorks.Audio
}
}
protected object StateLock = new object();
public SourceVoice(
AudioDevice device,
Format format
@ -124,20 +128,13 @@ namespace MoonWorks.Audio
}
/// <summary>
/// Adds an FAudio buffer to the voice queue.
/// Adds an AudioBuffer to the voice queue.
/// The voice processes and plays back the buffers in its queue in the order that they were submitted.
/// </summary>
/// <param name="buffer">The buffer to submit to the voice.</param>
public void Submit(FAudio.FAudioBuffer buffer)
public void Submit(AudioBuffer buffer)
{
lock (StateLock)
{
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
Handle,
ref buffer,
IntPtr.Zero
);
}
Submit(buffer.ToFAudioBuffer());
}
/// <summary>
@ -152,9 +149,27 @@ namespace MoonWorks.Audio
/// <summary>
/// Called automatically by AudioDevice in the audio thread.
/// Don't call this yourself! You might regret it!
/// </summary>
public virtual void Update() { }
/// <summary>
/// Adds an FAudio buffer to the voice queue.
/// The voice processes and plays back the buffers in its queue in the order that they were submitted.
/// </summary>
/// <param name="buffer">The buffer to submit to the voice.</param>
protected void Submit(FAudio.FAudioBuffer buffer)
{
lock (StateLock)
{
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
Handle,
ref buffer,
IntPtr.Zero
);
}
}
protected override unsafe void Destroy()
{
Stop();

View File

@ -1,272 +0,0 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
public class StaticSound : AudioResource
{
internal FAudio.FAudioBuffer Buffer;
private Format format;
public Format Format => format;
private bool OwnsBuffer;
public static unsafe StaticSound LoadOgg(AudioDevice device, string filePath)
{
var filePointer = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero);
if (error != 0)
{
throw new AudioLoadException("Error loading file!");
}
var info = FAudio.stb_vorbis_get_info(filePointer);
var lengthInFloats =
FAudio.stb_vorbis_stream_length_in_samples(filePointer) * info.channels;
var lengthInBytes = lengthInFloats * Marshal.SizeOf<float>();
var buffer = NativeMemory.Alloc((nuint) lengthInBytes);
FAudio.stb_vorbis_get_samples_float_interleaved(
filePointer,
info.channels,
(nint) buffer,
(int) lengthInFloats
);
FAudio.stb_vorbis_close(filePointer);
var format = new Format
{
Tag = FormatTag.IEEE_FLOAT,
BitsPerSample = 32,
Channels = (ushort) info.channels,
SampleRate = info.sample_rate
};
return new StaticSound(
device,
format,
(nint) buffer,
(uint) lengthInBytes,
true);
}
// mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385
public static unsafe StaticSound LoadWav(AudioDevice device, string filePath)
{
// WaveFormatEx data
ushort wFormatTag;
ushort nChannels;
uint nSamplesPerSec;
uint nAvgBytesPerSec;
ushort nBlockAlign;
ushort wBitsPerSample;
int samplerLoopStart = 0;
int samplerLoopEnd = 0;
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var reader = new BinaryReader(stream);
// RIFF Signature
string signature = new string(reader.ReadChars(4));
if (signature != "RIFF")
{
throw new NotSupportedException("Specified stream is not a wave file.");
}
reader.ReadUInt32(); // Riff Chunk Size
string wformat = new string(reader.ReadChars(4));
if (wformat != "WAVE")
{
throw new NotSupportedException("Specified stream is not a wave file.");
}
// WAVE Header
string format_signature = new string(reader.ReadChars(4));
while (format_signature != "fmt ")
{
reader.ReadBytes(reader.ReadInt32());
format_signature = new string(reader.ReadChars(4));
}
int format_chunk_size = reader.ReadInt32();
wFormatTag = reader.ReadUInt16();
nChannels = reader.ReadUInt16();
nSamplesPerSec = reader.ReadUInt32();
nAvgBytesPerSec = reader.ReadUInt32();
nBlockAlign = reader.ReadUInt16();
wBitsPerSample = reader.ReadUInt16();
// Reads residual bytes
if (format_chunk_size > 16)
{
reader.ReadBytes(format_chunk_size - 16);
}
// data Signature
string data_signature = new string(reader.ReadChars(4));
while (data_signature.ToLowerInvariant() != "data")
{
reader.ReadBytes(reader.ReadInt32());
data_signature = new string(reader.ReadChars(4));
}
if (data_signature != "data")
{
throw new NotSupportedException("Specified wave file is not supported.");
}
int waveDataLength = reader.ReadInt32();
var waveDataBuffer = NativeMemory.Alloc((nuint) waveDataLength);
var waveDataSpan = new Span<byte>(waveDataBuffer, waveDataLength);
stream.ReadExactly(waveDataSpan);
// Scan for other chunks
while (reader.PeekChar() != -1)
{
char[] chunkIDChars = reader.ReadChars(4);
if (chunkIDChars.Length < 4)
{
break; // EOL!
}
byte[] chunkSizeBytes = reader.ReadBytes(4);
if (chunkSizeBytes.Length < 4)
{
break; // EOL!
}
string chunk_signature = new string(chunkIDChars);
int chunkDataSize = BitConverter.ToInt32(chunkSizeBytes, 0);
if (chunk_signature == "smpl") // "smpl", Sampler Chunk Found
{
reader.ReadUInt32(); // Manufacturer
reader.ReadUInt32(); // Product
reader.ReadUInt32(); // Sample Period
reader.ReadUInt32(); // MIDI Unity Note
reader.ReadUInt32(); // MIDI Pitch Fraction
reader.ReadUInt32(); // SMPTE Format
reader.ReadUInt32(); // SMPTE Offset
uint numSampleLoops = reader.ReadUInt32();
int samplerData = reader.ReadInt32();
for (int i = 0; i < numSampleLoops; i += 1)
{
reader.ReadUInt32(); // Cue Point ID
reader.ReadUInt32(); // Type
int start = reader.ReadInt32();
int end = reader.ReadInt32();
reader.ReadUInt32(); // Fraction
reader.ReadUInt32(); // Play Count
if (i == 0) // Grab loopStart and loopEnd from first sample loop
{
samplerLoopStart = start;
samplerLoopEnd = end;
}
}
if (samplerData != 0) // Read Sampler Data if it exists
{
reader.ReadBytes(samplerData);
}
}
else // Read unwanted chunk data and try again
{
reader.ReadBytes(chunkDataSize);
}
}
// End scan
var format = new Format
{
Tag = (FormatTag) wFormatTag,
BitsPerSample = wBitsPerSample,
Channels = nChannels,
SampleRate = nSamplesPerSec
};
var sound = new StaticSound(
device,
format,
(nint) waveDataBuffer,
(uint) waveDataLength,
true
);
return sound;
}
public static unsafe StaticSound FromQOA(AudioDevice device, string path)
{
var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
var fileDataPtr = NativeMemory.Alloc((nuint) fileStream.Length);
var fileDataSpan = new Span<byte>(fileDataPtr, (int) fileStream.Length);
fileStream.ReadExactly(fileDataSpan);
fileStream.Close();
var qoaHandle = FAudio.qoa_open_from_memory((char*) fileDataPtr, (uint) fileDataSpan.Length, 0);
if (qoaHandle == 0)
{
NativeMemory.Free(fileDataPtr);
Logger.LogError("Error opening QOA file!");
throw new AudioLoadException("Error opening QOA file!");
}
FAudio.qoa_attributes(qoaHandle, out var channels, out var samplerate, out var samples_per_channel_per_frame, out var total_samples_per_channel);
var bufferLengthInBytes = total_samples_per_channel * channels * sizeof(short);
var buffer = NativeMemory.Alloc(bufferLengthInBytes);
FAudio.qoa_decode_entire(qoaHandle, (short*) buffer);
FAudio.qoa_close(qoaHandle);
NativeMemory.Free(fileDataPtr);
var format = new Format
{
Tag = FormatTag.PCM,
BitsPerSample = 16,
Channels = (ushort) channels,
SampleRate = samplerate
};
return new StaticSound(
device,
format,
(nint) buffer,
bufferLengthInBytes,
true
);
}
public StaticSound(
AudioDevice device,
Format format,
IntPtr bufferPtr,
uint bufferLengthInBytes,
bool ownsBuffer) : base(device)
{
this.format = format;
Buffer = new FAudio.FAudioBuffer
{
Flags = FAudio.FAUDIO_END_OF_STREAM,
pContext = IntPtr.Zero,
pAudioData = bufferPtr,
AudioBytes = bufferLengthInBytes,
PlayBegin = 0,
PlayLength = 0
};
OwnsBuffer = ownsBuffer;
}
protected override unsafe void Destroy()
{
if (OwnsBuffer)
{
NativeMemory.Free((void*) Buffer.pAudioData);
}
}
}
}

View File

@ -1,54 +0,0 @@
namespace MoonWorks.Audio
{
public class StaticVoice : SourceVoice, IPoolable<StaticVoice>
{
/// <summary>
/// Indicates if the voice should return to the voice pool when the voice is idle.
/// If you set this and then hold on to the voice reference there will be problems!
/// </summary>
public bool DeactivateWhenIdle { get; set; }
public static StaticVoice Create(AudioDevice device, Format format)
{
return new StaticVoice(device, format);
}
public StaticVoice(AudioDevice device, Format format) : base(device, format)
{
}
public override void Update()
{
lock (StateLock)
{
if (DeactivateWhenIdle)
{
if (BuffersQueued == 0)
{
Return();
}
}
}
}
/// <summary>
/// Adds a static sound to the voice queue.
/// The voice processes and plays back the buffers in its queue in the order that they were submitted.
/// </summary>
/// <param name="sound">The sound to submit to the voice.</param>
/// <param name="loop">Designates that the voice will loop the submitted buffer.</param>
public void Submit(StaticSound sound, bool loop = false)
{
if (loop)
{
sound.Buffer.LoopCount = FAudio.FAUDIO_LOOP_INFINITE;
}
else
{
sound.Buffer.LoopCount = 0;
}
Submit(sound.Buffer);
}
}
}

View File

@ -12,19 +12,19 @@ namespace MoonWorks.Audio
public bool Loop { get; set; }
public AudioData AudioData { get; protected set; }
public static StreamingVoice Create(AudioDevice device, Format format)
{
return new StreamingVoice(device, format);
}
public AudioDataStreamable AudioData { get; protected set; }
public unsafe StreamingVoice(AudioDevice device, Format format) : base(device, format)
{
buffers = new IntPtr[BUFFER_COUNT];
}
public void Load(AudioData data)
public static StreamingVoice Create(AudioDevice device, Format format)
{
return new StreamingVoice(device, format);
}
public void Load(AudioDataStreamable data)
{
lock (StateLock)
{
@ -58,15 +58,12 @@ namespace MoonWorks.Audio
{
lock (StateLock)
{
if (!IsDisposed)
if (AudioData == null || State != SoundState.Playing)
{
if (AudioData == null || State != SoundState.Playing)
{
return;
}
QueueBuffers();
return;
}
QueueBuffers();
}
}

View File

@ -0,0 +1,28 @@
namespace MoonWorks.Audio
{
/// <summary>
/// These voices are intended for playing one-off sound effects that don't have a long term reference.
/// </summary>
public class TransientVoice : SourceVoice, IPoolable<TransientVoice>
{
static TransientVoice IPoolable<TransientVoice>.Create(AudioDevice device, Format format)
{
return new TransientVoice(device, format);
}
public TransientVoice(AudioDevice device, Format format) : base(device, format)
{
}
public override void Update()
{
lock (StateLock)
{
if (BuffersQueued == 0)
{
Return();
}
}
}
}
}