more audio data and voice restructuring
parent
771dc6e7b3
commit
0500d94930
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
public class AudioDataOgg : AudioData
|
public class AudioDataOgg : AudioDataStreamable
|
||||||
{
|
{
|
||||||
private IntPtr FileDataPtr = IntPtr.Zero;
|
private IntPtr FileDataPtr = IntPtr.Zero;
|
||||||
private IntPtr VorbisHandle = IntPtr.Zero;
|
private IntPtr VorbisHandle = IntPtr.Zero;
|
||||||
|
@ -14,7 +14,7 @@ namespace MoonWorks.Audio
|
||||||
public override bool Loaded => VorbisHandle != IntPtr.Zero;
|
public override bool Loaded => VorbisHandle != IntPtr.Zero;
|
||||||
public override uint DecodeBufferSize => 32768;
|
public override uint DecodeBufferSize => 32768;
|
||||||
|
|
||||||
public AudioDataOgg(string filePath)
|
public AudioDataOgg(AudioDevice device, string filePath) : base(device)
|
||||||
{
|
{
|
||||||
FilePath = filePath;
|
FilePath = filePath;
|
||||||
|
|
||||||
|
@ -92,5 +92,44 @@ namespace MoonWorks.Audio
|
||||||
FileDataPtr = IntPtr.Zero;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
public class AudioDataQoa : AudioData
|
public class AudioDataQoa : AudioDataStreamable
|
||||||
{
|
{
|
||||||
private IntPtr QoaHandle = IntPtr.Zero;
|
private IntPtr QoaHandle = IntPtr.Zero;
|
||||||
private IntPtr FileDataPtr = IntPtr.Zero;
|
private IntPtr FileDataPtr = IntPtr.Zero;
|
||||||
|
@ -18,7 +18,7 @@ namespace MoonWorks.Audio
|
||||||
private uint decodeBufferSize;
|
private uint decodeBufferSize;
|
||||||
public override uint DecodeBufferSize => decodeBufferSize;
|
public override uint DecodeBufferSize => decodeBufferSize;
|
||||||
|
|
||||||
public AudioDataQoa(string filePath)
|
public AudioDataQoa(AudioDevice device, string filePath) : base(device)
|
||||||
{
|
{
|
||||||
FilePath = filePath;
|
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)
|
private static unsafe UInt64 ReverseEndianness(UInt64 value)
|
||||||
{
|
{
|
||||||
byte* bytes = (byte*) &value;
|
byte* bytes = (byte*) &value;
|
||||||
|
|
|
@ -1,19 +1,25 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
public abstract class AudioData
|
public abstract class AudioDataStreamable : AudioResource
|
||||||
{
|
{
|
||||||
public Format Format { get; protected set; }
|
public Format Format { get; protected set; }
|
||||||
|
public abstract bool Loaded { get; }
|
||||||
public abstract uint DecodeBufferSize { get; }
|
public abstract uint DecodeBufferSize { get; }
|
||||||
|
|
||||||
public abstract bool Loaded { get; }
|
protected AudioDataStreamable(AudioDevice device) : base(device)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Loads the raw audio data into memory.
|
/// Loads the raw audio data into memory to prepare it for stream decoding.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract void Load();
|
public abstract void Load();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unloads the raw audio data from memory.
|
||||||
|
/// </summary>
|
||||||
|
public abstract void Unload();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Seeks to the given sample frame.
|
/// Seeks to the given sample frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -28,9 +34,9 @@ namespace MoonWorks.Audio
|
||||||
/// <param name="reachedEnd">Whether the end of the data was reached on this decode.</param>
|
/// <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);
|
public abstract unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd);
|
||||||
|
|
||||||
/// <summary>
|
protected override void Destroy()
|
||||||
/// Unloads the raw audio data from memory.
|
{
|
||||||
/// </summary>
|
Unload();
|
||||||
public abstract void Unload();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
lock (StateLock)
|
||||||
{
|
{
|
||||||
if (IsDisposed) { return; }
|
|
||||||
if (State != SoundState.Playing) { return; }
|
if (State != SoundState.Playing) { return; }
|
||||||
|
|
||||||
if (NeedSoundThreshold > 0)
|
if (NeedSoundThreshold > 0)
|
||||||
|
@ -37,18 +36,18 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void EnqueueSound(StaticSound sound)
|
public void EnqueueSound(AudioBuffer buffer)
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (!(sound.Format == Format))
|
if (!(buffer.Format == Format))
|
||||||
{
|
{
|
||||||
Logger.LogWarn("Playlist audio format mismatch!");
|
Logger.LogWarn("Sound sequence audio format mismatch!");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
lock (StateLock)
|
lock (StateLock)
|
||||||
{
|
{
|
||||||
Submit(sound.Buffer);
|
Submit(buffer.ToFAudioBuffer());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,15 @@ namespace MoonWorks.Audio
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Emits audio from submitted audio buffers.
|
/// Emits audio from submitted audio buffers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SourceVoice : Voice
|
public abstract class SourceVoice : Voice
|
||||||
{
|
{
|
||||||
private Format format;
|
private Format format;
|
||||||
public Format 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
|
public uint BuffersQueued
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
@ -45,6 +47,8 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected object StateLock = new object();
|
||||||
|
|
||||||
public SourceVoice(
|
public SourceVoice(
|
||||||
AudioDevice device,
|
AudioDevice device,
|
||||||
Format format
|
Format format
|
||||||
|
@ -124,20 +128,13 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// The voice processes and plays back the buffers in its queue in the order that they were submitted.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="buffer">The buffer to submit to the voice.</param>
|
/// <param name="buffer">The buffer to submit to the voice.</param>
|
||||||
public void Submit(FAudio.FAudioBuffer buffer)
|
public void Submit(AudioBuffer buffer)
|
||||||
{
|
{
|
||||||
lock (StateLock)
|
Submit(buffer.ToFAudioBuffer());
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
|
|
||||||
Handle,
|
|
||||||
ref buffer,
|
|
||||||
IntPtr.Zero
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -152,9 +149,27 @@ namespace MoonWorks.Audio
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called automatically by AudioDevice in the audio thread.
|
/// Called automatically by AudioDevice in the audio thread.
|
||||||
|
/// Don't call this yourself! You might regret it!
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Update() { }
|
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()
|
protected override unsafe void Destroy()
|
||||||
{
|
{
|
||||||
Stop();
|
Stop();
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,19 +12,19 @@ namespace MoonWorks.Audio
|
||||||
|
|
||||||
public bool Loop { get; set; }
|
public bool Loop { get; set; }
|
||||||
|
|
||||||
public AudioData AudioData { get; protected set; }
|
public AudioDataStreamable AudioData { get; protected set; }
|
||||||
|
|
||||||
public static StreamingVoice Create(AudioDevice device, Format format)
|
|
||||||
{
|
|
||||||
return new StreamingVoice(device, format);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe StreamingVoice(AudioDevice device, Format format) : base(device, format)
|
public unsafe StreamingVoice(AudioDevice device, Format format) : base(device, format)
|
||||||
{
|
{
|
||||||
buffers = new IntPtr[BUFFER_COUNT];
|
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)
|
lock (StateLock)
|
||||||
{
|
{
|
||||||
|
@ -58,15 +58,12 @@ namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
lock (StateLock)
|
lock (StateLock)
|
||||||
{
|
{
|
||||||
if (!IsDisposed)
|
if (AudioData == null || State != SoundState.Playing)
|
||||||
{
|
{
|
||||||
if (AudioData == null || State != SoundState.Playing)
|
return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueBuffers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueueBuffers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue