Audio Restructuring #50
|
@ -0,0 +1,71 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains raw audio data in the format specified by Format.
|
||||||
|
/// Submit this to a SourceVoice to play audio.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create another AudioBuffer from this audio buffer.
|
||||||
|
/// It will not own the buffer data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="offset">Offset in bytes from the top of the original buffer.</param>
|
||||||
|
/// <param name="length">Length in bytes of the new buffer.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public AudioBuffer Slice(int offset, uint length)
|
||||||
|
{
|
||||||
|
return new AudioBuffer(Device, Format, BufferDataPtr + offset, length, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an FAudioBuffer struct from this AudioBuffer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loop">Whether we should set the FAudioBuffer to loop.</param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Streamable audio in Ogg format.
|
||||||
|
/// </summary>
|
||||||
|
public class AudioDataOgg : AudioDataStreamable
|
||||||
|
{
|
||||||
|
private IntPtr FileDataPtr = IntPtr.Zero;
|
||||||
|
private IntPtr VorbisHandle = IntPtr.Zero;
|
||||||
|
|
||||||
|
private string FilePath;
|
||||||
|
|
||||||
|
public override bool Loaded => VorbisHandle != IntPtr.Zero;
|
||||||
|
public override uint DecodeBufferSize => 32768;
|
||||||
|
|
||||||
|
public AudioDataOgg(AudioDevice device, string filePath) : base(device)
|
||||||
|
{
|
||||||
|
FilePath = filePath;
|
||||||
|
|
||||||
|
var handle = 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(handle);
|
||||||
|
|
||||||
|
Format = new Format
|
||||||
|
{
|
||||||
|
Tag = FormatTag.IEEE_FLOAT,
|
||||||
|
BitsPerSample = 32,
|
||||||
|
Channels = (ushort) info.channels,
|
||||||
|
SampleRate = info.sample_rate
|
||||||
|
};
|
||||||
|
|
||||||
|
FAudio.stb_vorbis_close(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd)
|
||||||
|
{
|
||||||
|
var lengthInFloats = bufferLengthInBytes / sizeof(float);
|
||||||
|
|
||||||
|
/* NOTE: this function returns samples per channel, not total samples */
|
||||||
|
var samples = FAudio.stb_vorbis_get_samples_float_interleaved(
|
||||||
|
VorbisHandle,
|
||||||
|
Format.Channels,
|
||||||
|
(IntPtr) buffer,
|
||||||
|
lengthInFloats
|
||||||
|
);
|
||||||
|
|
||||||
|
var sampleCount = samples * Format.Channels;
|
||||||
|
reachedEnd = sampleCount < lengthInFloats;
|
||||||
|
filledLengthInBytes = sampleCount * sizeof(float);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override unsafe void Load()
|
||||||
|
{
|
||||||
|
if (!Loaded)
|
||||||
|
{
|
||||||
|
var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length);
|
||||||
|
var fileDataSpan = new Span<byte>((void*) FileDataPtr, (int) fileStream.Length);
|
||||||
|
fileStream.ReadExactly(fileDataSpan);
|
||||||
|
fileStream.Close();
|
||||||
|
|
||||||
|
VorbisHandle = FAudio.stb_vorbis_open_memory(FileDataPtr, fileDataSpan.Length, out int error, IntPtr.Zero);
|
||||||
|
if (error != 0)
|
||||||
|
{
|
||||||
|
NativeMemory.Free((void*) FileDataPtr);
|
||||||
|
Logger.LogError("Error opening OGG file!");
|
||||||
|
Logger.LogError("Error: " + error);
|
||||||
|
throw new AudioLoadException("Error opening OGG file!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Seek(uint sampleFrame)
|
||||||
|
{
|
||||||
|
FAudio.stb_vorbis_seek(VorbisHandle, sampleFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override unsafe void Unload()
|
||||||
|
{
|
||||||
|
if (Loaded)
|
||||||
|
{
|
||||||
|
FAudio.stb_vorbis_close(VorbisHandle);
|
||||||
|
NativeMemory.Free((void*) FileDataPtr);
|
||||||
|
|
||||||
|
VorbisHandle = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Streamable audio in QOA format.
|
||||||
|
/// </summary>
|
||||||
|
public class AudioDataQoa : AudioDataStreamable
|
||||||
|
{
|
||||||
|
private IntPtr QoaHandle = IntPtr.Zero;
|
||||||
|
private IntPtr FileDataPtr = IntPtr.Zero;
|
||||||
|
|
||||||
|
private string FilePath;
|
||||||
|
|
||||||
|
private const uint QOA_MAGIC = 0x716f6166; /* 'qoaf' */
|
||||||
|
|
||||||
|
public override bool Loaded => QoaHandle != IntPtr.Zero;
|
||||||
|
|
||||||
|
private uint decodeBufferSize;
|
||||||
|
public override uint DecodeBufferSize => decodeBufferSize;
|
||||||
|
|
||||||
|
public AudioDataQoa(AudioDevice device, string filePath) : base(device)
|
||||||
|
{
|
||||||
|
FilePath = filePath;
|
||||||
|
|
||||||
|
using var stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
using var reader = new BinaryReader(stream);
|
||||||
|
|
||||||
|
UInt64 fileHeader = ReverseEndianness(reader.ReadUInt64());
|
||||||
|
if ((fileHeader >> 32) != QOA_MAGIC)
|
||||||
|
{
|
||||||
|
throw new AudioLoadException("Specified file is not a QOA file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint totalSamplesPerChannel = (uint) (fileHeader & (0xFFFFFFFF));
|
||||||
|
if (totalSamplesPerChannel == 0)
|
||||||
|
{
|
||||||
|
throw new AudioLoadException("Specified file is not a valid QOA file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
UInt64 frameHeader = ReverseEndianness(reader.ReadUInt64());
|
||||||
|
uint channels = (uint) ((frameHeader >> 56) & 0x0000FF);
|
||||||
|
uint samplerate = (uint) ((frameHeader >> 32) & 0xFFFFFF);
|
||||||
|
uint samplesPerChannelPerFrame = (uint) ((frameHeader >> 16) & 0x00FFFF);
|
||||||
|
|
||||||
|
Format = new Format
|
||||||
|
{
|
||||||
|
Tag = FormatTag.PCM,
|
||||||
|
BitsPerSample = 16,
|
||||||
|
Channels = (ushort) channels,
|
||||||
|
SampleRate = samplerate
|
||||||
|
};
|
||||||
|
|
||||||
|
decodeBufferSize = channels * samplesPerChannelPerFrame * sizeof(short);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override unsafe void Decode(void* buffer, int bufferLengthInBytes, out int filledLengthInBytes, out bool reachedEnd)
|
||||||
|
{
|
||||||
|
var lengthInShorts = bufferLengthInBytes / sizeof(short);
|
||||||
|
|
||||||
|
// NOTE: this function returns samples per channel!
|
||||||
|
var samples = FAudio.qoa_decode_next_frame(QoaHandle, (short*) buffer);
|
||||||
|
|
||||||
|
var sampleCount = samples * Format.Channels;
|
||||||
|
reachedEnd = sampleCount < lengthInShorts;
|
||||||
|
filledLengthInBytes = (int) (sampleCount * sizeof(short));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override unsafe void Load()
|
||||||
|
{
|
||||||
|
if (!Loaded)
|
||||||
|
{
|
||||||
|
var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length);
|
||||||
|
var fileDataSpan = new Span<byte>((void*) FileDataPtr, (int) fileStream.Length);
|
||||||
|
fileStream.ReadExactly(fileDataSpan);
|
||||||
|
fileStream.Close();
|
||||||
|
|
||||||
|
QoaHandle = FAudio.qoa_open_from_memory((char*) FileDataPtr, (uint) fileDataSpan.Length, 0);
|
||||||
|
if (QoaHandle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
NativeMemory.Free((void*) FileDataPtr);
|
||||||
|
Logger.LogError("Error opening QOA file!");
|
||||||
|
throw new AudioLoadException("Error opening QOA file!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Seek(uint sampleFrame)
|
||||||
|
{
|
||||||
|
FAudio.qoa_seek_frame(QoaHandle, (int) sampleFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override unsafe void Unload()
|
||||||
|
{
|
||||||
|
if (Loaded)
|
||||||
|
{
|
||||||
|
FAudio.qoa_close(QoaHandle);
|
||||||
|
NativeMemory.Free((void*) FileDataPtr);
|
||||||
|
|
||||||
|
QoaHandle = IntPtr.Zero;
|
||||||
|
FileDataPtr = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
return
|
||||||
|
((UInt64)(bytes[0]) << 56) | ((UInt64)(bytes[1]) << 48) |
|
||||||
|
((UInt64)(bytes[2]) << 40) | ((UInt64)(bytes[3]) << 32) |
|
||||||
|
((UInt64)(bytes[4]) << 24) | ((UInt64)(bytes[5]) << 16) |
|
||||||
|
((UInt64)(bytes[6]) << 8) | ((UInt64)(bytes[7]) << 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Use this in conjunction with a StreamingVoice to play back streaming audio data.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class AudioDataStreamable : AudioResource
|
||||||
|
{
|
||||||
|
public Format Format { get; protected set; }
|
||||||
|
public abstract bool Loaded { get; }
|
||||||
|
public abstract uint DecodeBufferSize { get; }
|
||||||
|
|
||||||
|
protected AudioDataStreamable(AudioDevice device) : base(device)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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>
|
||||||
|
public abstract void Seek(uint sampleFrame);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to decodes data of length bufferLengthInBytes into the provided buffer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The buffer that decoded bytes will be placed into.</param>
|
||||||
|
/// <param name="bufferLengthInBytes">Requested length of decoded audio data.</param>
|
||||||
|
/// <param name="filledLengthInBytes">How much data was actually filled in by the 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);
|
||||||
|
|
||||||
|
protected override void Destroy()
|
||||||
|
{
|
||||||
|
Unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
public static class AudioDataWav
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create an AudioBuffer containing all the WAV audio data in a file.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public unsafe static AudioBuffer CreateBuffer(AudioDevice device, string filePath)
|
||||||
|
{
|
||||||
|
// mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385
|
||||||
|
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
namespace MoonWorks.Audio
|
||||||
|
@ -9,31 +8,27 @@ namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
public IntPtr Handle { get; }
|
public IntPtr Handle { get; }
|
||||||
public byte[] Handle3D { get; }
|
public byte[] Handle3D { get; }
|
||||||
public IntPtr MasteringVoice { get; }
|
|
||||||
public FAudio.FAudioDeviceDetails DeviceDetails { get; }
|
public FAudio.FAudioDeviceDetails DeviceDetails { get; }
|
||||||
|
|
||||||
|
private IntPtr trueMasteringVoice;
|
||||||
|
|
||||||
|
// this is a fun little trick where we use a submix voice as a "faux" mastering voice
|
||||||
|
// this lets us maintain API consistency for effects like panning and reverb
|
||||||
|
private SubmixVoice fauxMasteringVoice;
|
||||||
|
public SubmixVoice MasteringVoice => fauxMasteringVoice;
|
||||||
|
|
||||||
public float CurveDistanceScalar = 1f;
|
public float CurveDistanceScalar = 1f;
|
||||||
public float DopplerScale = 1f;
|
public float DopplerScale = 1f;
|
||||||
public float SpeedOfSound = 343.5f;
|
public float SpeedOfSound = 343.5f;
|
||||||
|
|
||||||
private float masteringVolume = 1f;
|
|
||||||
public float MasteringVolume
|
|
||||||
{
|
|
||||||
get => masteringVolume;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
masteringVolume = value;
|
|
||||||
FAudio.FAudioVoice_SetVolume(MasteringVoice, masteringVolume, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly HashSet<WeakReference> resources = new HashSet<WeakReference>();
|
private readonly HashSet<WeakReference> resources = new HashSet<WeakReference>();
|
||||||
private readonly List<StreamingSound> autoUpdateStreamingSoundReferences = new List<StreamingSound>();
|
private readonly HashSet<SourceVoice> activeSourceVoices = new HashSet<SourceVoice>();
|
||||||
private readonly List<StaticSoundInstance> autoFreeStaticSoundInstanceReferences = new List<StaticSoundInstance>();
|
|
||||||
private readonly List<WeakReference<SoundSequence>> soundSequenceReferences = new List<WeakReference<SoundSequence>>();
|
|
||||||
|
|
||||||
private AudioTweenManager AudioTweenManager;
|
private AudioTweenManager AudioTweenManager;
|
||||||
|
|
||||||
|
private SourceVoicePool VoicePool;
|
||||||
|
private List<SourceVoice> VoicesToReturn = new List<SourceVoice>();
|
||||||
|
|
||||||
private const int Step = 200;
|
private const int Step = 200;
|
||||||
private TimeSpan UpdateInterval;
|
private TimeSpan UpdateInterval;
|
||||||
private System.Diagnostics.Stopwatch TickStopwatch = new System.Diagnostics.Stopwatch();
|
private System.Diagnostics.Stopwatch TickStopwatch = new System.Diagnostics.Stopwatch();
|
||||||
|
@ -93,25 +88,24 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Init Mastering Voice */
|
/* Init Mastering Voice */
|
||||||
IntPtr masteringVoice;
|
var result = FAudio.FAudio_CreateMasteringVoice(
|
||||||
|
|
||||||
if (FAudio.FAudio_CreateMasteringVoice(
|
|
||||||
Handle,
|
Handle,
|
||||||
out masteringVoice,
|
out trueMasteringVoice,
|
||||||
FAudio.FAUDIO_DEFAULT_CHANNELS,
|
FAudio.FAUDIO_DEFAULT_CHANNELS,
|
||||||
FAudio.FAUDIO_DEFAULT_SAMPLERATE,
|
FAudio.FAUDIO_DEFAULT_SAMPLERATE,
|
||||||
0,
|
0,
|
||||||
i,
|
i,
|
||||||
IntPtr.Zero
|
IntPtr.Zero
|
||||||
) != 0)
|
);
|
||||||
|
|
||||||
|
if (result != 0)
|
||||||
{
|
{
|
||||||
Logger.LogError("No mastering voice found!");
|
Logger.LogError("Failed to create a mastering voice!");
|
||||||
FAudio.FAudio_Release(Handle);
|
Logger.LogError("Audio device creation failed!");
|
||||||
Handle = IntPtr.Zero;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MasteringVoice = masteringVoice;
|
fauxMasteringVoice = new SubmixVoice(this, DeviceDetails.OutputFormat.Format.nChannels, DeviceDetails.OutputFormat.Format.nSamplesPerSec, int.MaxValue);
|
||||||
|
|
||||||
/* Init 3D Audio */
|
/* Init 3D Audio */
|
||||||
|
|
||||||
|
@ -123,6 +117,7 @@ namespace MoonWorks.Audio
|
||||||
);
|
);
|
||||||
|
|
||||||
AudioTweenManager = new AudioTweenManager();
|
AudioTweenManager = new AudioTweenManager();
|
||||||
|
VoicePool = new SourceVoicePool(this);
|
||||||
|
|
||||||
Logger.LogInfo("Setting up audio thread...");
|
Logger.LogInfo("Setting up audio thread...");
|
||||||
WakeSignal = new AutoResetEvent(true);
|
WakeSignal = new AutoResetEvent(true);
|
||||||
|
@ -163,53 +158,60 @@ namespace MoonWorks.Audio
|
||||||
previousTickTime = TickStopwatch.Elapsed.Ticks;
|
previousTickTime = TickStopwatch.Elapsed.Ticks;
|
||||||
float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond;
|
float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond;
|
||||||
|
|
||||||
for (var i = autoUpdateStreamingSoundReferences.Count - 1; i >= 0; i -= 1)
|
|
||||||
{
|
|
||||||
var streamingSound = autoUpdateStreamingSoundReferences[i];
|
|
||||||
|
|
||||||
if (streamingSound.Loaded)
|
|
||||||
{
|
|
||||||
streamingSound.Update();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
autoUpdateStreamingSoundReferences.RemoveAt(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = autoFreeStaticSoundInstanceReferences.Count - 1; i >= 0; i -= 1)
|
|
||||||
{
|
|
||||||
var staticSoundInstance = autoFreeStaticSoundInstanceReferences[i];
|
|
||||||
|
|
||||||
if (staticSoundInstance.State == SoundState.Stopped)
|
|
||||||
{
|
|
||||||
staticSoundInstance.Free();
|
|
||||||
autoFreeStaticSoundInstanceReferences.RemoveAt(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = soundSequenceReferences.Count - 1; i >= 0; i -= 1)
|
|
||||||
{
|
|
||||||
if (soundSequenceReferences[i].TryGetTarget(out var soundSequence))
|
|
||||||
{
|
|
||||||
soundSequence.Update();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
soundSequenceReferences.RemoveAt(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioTweenManager.Update(elapsedSeconds);
|
AudioTweenManager.Update(elapsedSeconds);
|
||||||
|
|
||||||
|
foreach (var voice in activeSourceVoices)
|
||||||
|
{
|
||||||
|
voice.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var voice in VoicesToReturn)
|
||||||
|
{
|
||||||
|
voice.Reset();
|
||||||
|
activeSourceVoices.Remove(voice);
|
||||||
|
VoicePool.Return(voice);
|
||||||
|
}
|
||||||
|
|
||||||
|
VoicesToReturn.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SyncPlay()
|
/// <summary>
|
||||||
|
/// Triggers all pending operations with the given syncGroup value.
|
||||||
|
/// </summary>
|
||||||
|
public void TriggerSyncGroup(uint syncGroup)
|
||||||
{
|
{
|
||||||
FAudio.FAudio_CommitChanges(Handle, 1);
|
FAudio.FAudio_CommitChanges(Handle, syncGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtains an appropriate source voice from the voice pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="format">The format that the voice must match.</param>
|
||||||
|
/// <returns>A source voice with the given format.</returns>
|
||||||
|
public T Obtain<T>(Format format) where T : SourceVoice, IPoolable<T>
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
var voice = VoicePool.Obtain<T>(format);
|
||||||
|
activeSourceVoices.Add(voice);
|
||||||
|
return voice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the source voice to the voice pool.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="voice"></param>
|
||||||
|
internal void Return(SourceVoice voice)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
VoicesToReturn.Add(voice);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void CreateTween(
|
internal void CreateTween(
|
||||||
SoundInstance soundInstance,
|
Voice voice,
|
||||||
AudioTweenProperty property,
|
AudioTweenProperty property,
|
||||||
System.Func<float, float> easingFunction,
|
System.Func<float, float> easingFunction,
|
||||||
float start,
|
float start,
|
||||||
|
@ -220,7 +222,7 @@ namespace MoonWorks.Audio
|
||||||
lock (StateLock)
|
lock (StateLock)
|
||||||
{
|
{
|
||||||
AudioTweenManager.CreateTween(
|
AudioTweenManager.CreateTween(
|
||||||
soundInstance,
|
voice,
|
||||||
property,
|
property,
|
||||||
easingFunction,
|
easingFunction,
|
||||||
start,
|
start,
|
||||||
|
@ -232,12 +234,12 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void ClearTweens(
|
internal void ClearTweens(
|
||||||
SoundInstance soundReference,
|
Voice voice,
|
||||||
AudioTweenProperty property
|
AudioTweenProperty property
|
||||||
) {
|
) {
|
||||||
lock (StateLock)
|
lock (StateLock)
|
||||||
{
|
{
|
||||||
AudioTweenManager.ClearTweens(soundReference, property);
|
AudioTweenManager.ClearTweens(voice, property);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,21 +264,6 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void AddAutoUpdateStreamingSoundInstance(StreamingSound instance)
|
|
||||||
{
|
|
||||||
autoUpdateStreamingSoundReferences.Add(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void AddAutoFreeStaticSoundInstance(StaticSoundInstance instance)
|
|
||||||
{
|
|
||||||
autoFreeStaticSoundInstanceReferences.Add(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void AddSoundSequenceReference(SoundSequence sequence)
|
|
||||||
{
|
|
||||||
soundSequenceReferences.Add(new WeakReference<SoundSequence>(sequence));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (!IsDisposed)
|
if (!IsDisposed)
|
||||||
|
@ -286,6 +273,18 @@ namespace MoonWorks.Audio
|
||||||
|
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
|
// stop all source voices
|
||||||
|
foreach (var weakReference in resources)
|
||||||
|
{
|
||||||
|
var target = weakReference.Target;
|
||||||
|
|
||||||
|
if (target != null && target is SourceVoice voice)
|
||||||
|
{
|
||||||
|
voice.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// destroy all audio resources
|
||||||
foreach (var weakReference in resources)
|
foreach (var weakReference in resources)
|
||||||
{
|
{
|
||||||
var target = weakReference.Target;
|
var target = weakReference.Target;
|
||||||
|
@ -295,10 +294,11 @@ namespace MoonWorks.Audio
|
||||||
(target as IDisposable).Dispose();
|
(target as IDisposable).Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resources.Clear();
|
resources.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
FAudio.FAudioVoice_DestroyVoice(MasteringVoice);
|
FAudio.FAudioVoice_DestroyVoice(trueMasteringVoice);
|
||||||
FAudio.FAudio_Release(Handle);
|
FAudio.FAudio_Release(Handle);
|
||||||
|
|
||||||
IsDisposed = true;
|
IsDisposed = true;
|
||||||
|
|
|
@ -14,7 +14,7 @@ namespace MoonWorks.Audio
|
||||||
|
|
||||||
internal class AudioTween
|
internal class AudioTween
|
||||||
{
|
{
|
||||||
public SoundInstance SoundInstance;
|
public Voice Voice;
|
||||||
public AudioTweenProperty Property;
|
public AudioTweenProperty Property;
|
||||||
public EasingFunction EasingFunction;
|
public EasingFunction EasingFunction;
|
||||||
public float Time;
|
public float Time;
|
||||||
|
@ -51,7 +51,7 @@ namespace MoonWorks.Audio
|
||||||
|
|
||||||
public void Free(AudioTween tween)
|
public void Free(AudioTween tween)
|
||||||
{
|
{
|
||||||
tween.SoundInstance = null;
|
tween.Voice = null;
|
||||||
Tweens.Enqueue(tween);
|
Tweens.Enqueue(tween);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ namespace MoonWorks.Audio
|
||||||
internal class AudioTweenManager
|
internal class AudioTweenManager
|
||||||
{
|
{
|
||||||
private AudioTweenPool AudioTweenPool = new AudioTweenPool();
|
private AudioTweenPool AudioTweenPool = new AudioTweenPool();
|
||||||
private readonly Dictionary<(SoundInstance, AudioTweenProperty), AudioTween> AudioTweens = new Dictionary<(SoundInstance, AudioTweenProperty), AudioTween>();
|
private readonly Dictionary<(Voice, AudioTweenProperty), AudioTween> AudioTweens = new Dictionary<(Voice, AudioTweenProperty), AudioTween>();
|
||||||
private readonly List<AudioTween> DelayedAudioTweens = new List<AudioTween>();
|
private readonly List<AudioTween> DelayedAudioTweens = new List<AudioTween>();
|
||||||
|
|
||||||
public void Update(float elapsedSeconds)
|
public void Update(float elapsedSeconds)
|
||||||
|
@ -14,7 +14,7 @@ namespace MoonWorks.Audio
|
||||||
for (var i = DelayedAudioTweens.Count - 1; i >= 0; i--)
|
for (var i = DelayedAudioTweens.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
var audioTween = DelayedAudioTweens[i];
|
var audioTween = DelayedAudioTweens[i];
|
||||||
var soundInstance = audioTween.SoundInstance;
|
var voice = audioTween.Voice;
|
||||||
|
|
||||||
audioTween.Time += elapsedSeconds;
|
audioTween.Time += elapsedSeconds;
|
||||||
|
|
||||||
|
@ -24,23 +24,23 @@ namespace MoonWorks.Audio
|
||||||
switch (audioTween.Property)
|
switch (audioTween.Property)
|
||||||
{
|
{
|
||||||
case AudioTweenProperty.Pan:
|
case AudioTweenProperty.Pan:
|
||||||
audioTween.StartValue = soundInstance.Pan;
|
audioTween.StartValue = voice.Pan;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AudioTweenProperty.Pitch:
|
case AudioTweenProperty.Pitch:
|
||||||
audioTween.StartValue = soundInstance.Pitch;
|
audioTween.StartValue = voice.Pitch;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AudioTweenProperty.Volume:
|
case AudioTweenProperty.Volume:
|
||||||
audioTween.StartValue = soundInstance.Volume;
|
audioTween.StartValue = voice.Volume;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AudioTweenProperty.FilterFrequency:
|
case AudioTweenProperty.FilterFrequency:
|
||||||
audioTween.StartValue = soundInstance.FilterFrequency;
|
audioTween.StartValue = voice.FilterFrequency;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AudioTweenProperty.Reverb:
|
case AudioTweenProperty.Reverb:
|
||||||
audioTween.StartValue = soundInstance.Reverb;
|
audioTween.StartValue = voice.Reverb;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateTween(
|
public void CreateTween(
|
||||||
SoundInstance soundInstance,
|
Voice voice,
|
||||||
AudioTweenProperty property,
|
AudioTweenProperty property,
|
||||||
System.Func<float, float> easingFunction,
|
System.Func<float, float> easingFunction,
|
||||||
float start,
|
float start,
|
||||||
|
@ -73,7 +73,7 @@ namespace MoonWorks.Audio
|
||||||
float delayTime
|
float delayTime
|
||||||
) {
|
) {
|
||||||
var tween = AudioTweenPool.Obtain();
|
var tween = AudioTweenPool.Obtain();
|
||||||
tween.SoundInstance = soundInstance;
|
tween.Voice = voice;
|
||||||
tween.Property = property;
|
tween.Property = property;
|
||||||
tween.EasingFunction = easingFunction;
|
tween.EasingFunction = easingFunction;
|
||||||
tween.StartValue = start;
|
tween.StartValue = start;
|
||||||
|
@ -92,21 +92,21 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearTweens(SoundInstance soundInstance, AudioTweenProperty property)
|
public void ClearTweens(Voice voice, AudioTweenProperty property)
|
||||||
{
|
{
|
||||||
AudioTweens.Remove((soundInstance, property));
|
AudioTweens.Remove((voice, property));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddTween(
|
private void AddTween(
|
||||||
AudioTween audioTween
|
AudioTween audioTween
|
||||||
) {
|
) {
|
||||||
// if a tween with the same sound and property already exists, get rid of it
|
// if a tween with the same sound and property already exists, get rid of it
|
||||||
if (AudioTweens.TryGetValue((audioTween.SoundInstance, audioTween.Property), out var currentTween))
|
if (AudioTweens.TryGetValue((audioTween.Voice, audioTween.Property), out var currentTween))
|
||||||
{
|
{
|
||||||
AudioTweenPool.Free(currentTween);
|
AudioTweenPool.Free(currentTween);
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioTweens[(audioTween.SoundInstance, audioTween.Property)] = audioTween;
|
AudioTweens[(audioTween.Voice, audioTween.Property)] = audioTween;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool UpdateAudioTween(AudioTween audioTween, float delta)
|
private static bool UpdateAudioTween(AudioTween audioTween, float delta)
|
||||||
|
@ -133,23 +133,23 @@ namespace MoonWorks.Audio
|
||||||
switch (audioTween.Property)
|
switch (audioTween.Property)
|
||||||
{
|
{
|
||||||
case AudioTweenProperty.Pan:
|
case AudioTweenProperty.Pan:
|
||||||
audioTween.SoundInstance.Pan = value;
|
audioTween.Voice.Pan = value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AudioTweenProperty.Pitch:
|
case AudioTweenProperty.Pitch:
|
||||||
audioTween.SoundInstance.Pitch = value;
|
audioTween.Voice.Pitch = value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AudioTweenProperty.Volume:
|
case AudioTweenProperty.Volume:
|
||||||
audioTween.SoundInstance.Volume = value;
|
audioTween.Voice.Volume = value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AudioTweenProperty.FilterFrequency:
|
case AudioTweenProperty.FilterFrequency:
|
||||||
audioTween.SoundInstance.FilterFrequency = value;
|
audioTween.Voice.FilterFrequency = value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AudioTweenProperty.Reverb:
|
case AudioTweenProperty.Reverb:
|
||||||
audioTween.SoundInstance.Reverb = value;
|
audioTween.Voice.Reverb = value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
public static class AudioUtils
|
|
||||||
{
|
|
||||||
public struct WaveHeaderData
|
|
||||||
{
|
|
||||||
public int FileLength;
|
|
||||||
public short FormatTag;
|
|
||||||
public short Channels;
|
|
||||||
public int SampleRate;
|
|
||||||
public short BitsPerSample;
|
|
||||||
public short BlockAlign;
|
|
||||||
public int DataLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WaveHeaderData ReadWaveHeaderData(string filePath)
|
|
||||||
{
|
|
||||||
WaveHeaderData headerData;
|
|
||||||
var fileInfo = new FileInfo(filePath);
|
|
||||||
using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
|
||||||
using BinaryReader br = new BinaryReader(fs);
|
|
||||||
|
|
||||||
headerData.FileLength = (int)fileInfo.Length - 8;
|
|
||||||
fs.Position = 20;
|
|
||||||
headerData.FormatTag = br.ReadInt16();
|
|
||||||
fs.Position = 22;
|
|
||||||
headerData.Channels = br.ReadInt16();
|
|
||||||
fs.Position = 24;
|
|
||||||
headerData.SampleRate = br.ReadInt32();
|
|
||||||
fs.Position = 32;
|
|
||||||
headerData.BlockAlign = br.ReadInt16();
|
|
||||||
fs.Position = 34;
|
|
||||||
headerData.BitsPerSample = br.ReadInt16();
|
|
||||||
fs.Position = 40;
|
|
||||||
headerData.DataLength = br.ReadInt32();
|
|
||||||
|
|
||||||
return headerData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
public enum FormatTag : ushort
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
PCM = 1,
|
||||||
|
MSADPCM = 2,
|
||||||
|
IEEE_FLOAT = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
public record struct Format
|
||||||
|
{
|
||||||
|
public FormatTag Tag;
|
||||||
|
public ushort Channels;
|
||||||
|
public uint SampleRate;
|
||||||
|
public ushort BitsPerSample;
|
||||||
|
|
||||||
|
internal FAudio.FAudioWaveFormatEx ToFAudioFormat()
|
||||||
|
{
|
||||||
|
var blockAlign = (ushort) ((BitsPerSample / 8) * Channels);
|
||||||
|
|
||||||
|
return new FAudio.FAudioWaveFormatEx
|
||||||
|
{
|
||||||
|
wFormatTag = (ushort) Tag,
|
||||||
|
nChannels = Channels,
|
||||||
|
nSamplesPerSec = SampleRate,
|
||||||
|
wBitsPerSample = BitsPerSample,
|
||||||
|
nBlockAlign = blockAlign,
|
||||||
|
nAvgBytesPerSec = blockAlign * SampleRate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
public interface IPoolable<T>
|
||||||
|
{
|
||||||
|
static abstract T Create(AudioDevice device, Format format);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PersistentVoice should be used when you need to maintain a long-term reference to a source voice.
|
||||||
|
/// </summary>
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,54 +3,34 @@ using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
// sound instances can send their audio to this voice to add reverb
|
/// <summary>
|
||||||
public unsafe class ReverbEffect : AudioResource
|
/// Use this in conjunction with SourceVoice.SetReverbEffectChain to add reverb to a voice.
|
||||||
|
/// </summary>
|
||||||
|
public unsafe class ReverbEffect : SubmixVoice
|
||||||
{
|
{
|
||||||
private IntPtr voice;
|
public ReverbEffect(AudioDevice audioDevice, uint processingStage) : base(audioDevice, 1, audioDevice.DeviceDetails.OutputFormat.Format.nSamplesPerSec, processingStage)
|
||||||
public IntPtr Voice => voice;
|
|
||||||
|
|
||||||
public ReverbEffect(AudioDevice audioDevice) : base(audioDevice)
|
|
||||||
{
|
{
|
||||||
/* Init reverb */
|
/* Init reverb */
|
||||||
|
|
||||||
IntPtr reverb;
|
IntPtr reverb;
|
||||||
FAudio.FAudioCreateReverb(out reverb, 0);
|
FAudio.FAudioCreateReverb(out reverb, 0);
|
||||||
|
|
||||||
IntPtr chainPtr;
|
var chain = new FAudio.FAudioEffectChain();
|
||||||
chainPtr = (nint) NativeMemory.Alloc(
|
var descriptor = new FAudio.FAudioEffectDescriptor();
|
||||||
(nuint) Marshal.SizeOf<FAudio.FAudioEffectChain>()
|
|
||||||
|
descriptor.InitialState = 1;
|
||||||
|
descriptor.OutputChannels = 1;
|
||||||
|
descriptor.pEffect = reverb;
|
||||||
|
|
||||||
|
chain.EffectCount = 1;
|
||||||
|
chain.pEffectDescriptors = (nint) (&descriptor);
|
||||||
|
|
||||||
|
FAudio.FAudioVoice_SetEffectChain(
|
||||||
|
Handle,
|
||||||
|
ref chain
|
||||||
);
|
);
|
||||||
|
|
||||||
FAudio.FAudioEffectChain* reverbChain = (FAudio.FAudioEffectChain*) chainPtr;
|
|
||||||
reverbChain->EffectCount = 1;
|
|
||||||
reverbChain->pEffectDescriptors = (nint) NativeMemory.Alloc(
|
|
||||||
(nuint) Marshal.SizeOf<FAudio.FAudioEffectDescriptor>()
|
|
||||||
);
|
|
||||||
|
|
||||||
FAudio.FAudioEffectDescriptor* reverbDescriptor =
|
|
||||||
(FAudio.FAudioEffectDescriptor*) reverbChain->pEffectDescriptors;
|
|
||||||
|
|
||||||
reverbDescriptor->InitialState = 1;
|
|
||||||
reverbDescriptor->OutputChannels = (uint) (
|
|
||||||
(audioDevice.DeviceDetails.OutputFormat.Format.nChannels == 6) ? 6 : 1
|
|
||||||
);
|
|
||||||
reverbDescriptor->pEffect = reverb;
|
|
||||||
|
|
||||||
FAudio.FAudio_CreateSubmixVoice(
|
|
||||||
audioDevice.Handle,
|
|
||||||
out voice,
|
|
||||||
1, /* omnidirectional reverb */
|
|
||||||
audioDevice.DeviceDetails.OutputFormat.Format.nSamplesPerSec,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
IntPtr.Zero,
|
|
||||||
chainPtr
|
|
||||||
);
|
|
||||||
FAudio.FAPOBase_Release(reverb);
|
FAudio.FAPOBase_Release(reverb);
|
||||||
|
|
||||||
NativeMemory.Free((void*) reverbChain->pEffectDescriptors);
|
|
||||||
NativeMemory.Free((void*) chainPtr);
|
|
||||||
|
|
||||||
/* Init reverb params */
|
/* Init reverb params */
|
||||||
// Defaults based on FAUDIOFX_I3DL2_PRESET_GENERIC
|
// Defaults based on FAUDIOFX_I3DL2_PRESET_GENERIC
|
||||||
|
|
||||||
|
@ -86,7 +66,7 @@ namespace MoonWorks.Audio
|
||||||
fixed (FAudio.FAudioFXReverbParameters* reverbParamsPtr = &reverbParams)
|
fixed (FAudio.FAudioFXReverbParameters* reverbParamsPtr = &reverbParams)
|
||||||
{
|
{
|
||||||
FAudio.FAudioVoice_SetEffectParameters(
|
FAudio.FAudioVoice_SetEffectParameters(
|
||||||
voice,
|
Handle,
|
||||||
0,
|
0,
|
||||||
(nint) reverbParamsPtr,
|
(nint) reverbParamsPtr,
|
||||||
(uint) Marshal.SizeOf<FAudio.FAudioFXReverbParameters>(),
|
(uint) Marshal.SizeOf<FAudio.FAudioFXReverbParameters>(),
|
||||||
|
@ -94,10 +74,5 @@ namespace MoonWorks.Audio
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Destroy()
|
|
||||||
{
|
|
||||||
FAudio.FAudioVoice_DestroyVoice(Voice);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
// NOTE: all sounds played with a SoundSequence must have the same audio format!
|
|
||||||
public class SoundSequence : SoundInstance
|
|
||||||
{
|
|
||||||
public int NeedSoundThreshold = 0;
|
|
||||||
public delegate void OnSoundNeededFunc();
|
|
||||||
public OnSoundNeededFunc OnSoundNeeded;
|
|
||||||
|
|
||||||
private object StateLock = new object();
|
|
||||||
|
|
||||||
public SoundSequence(AudioDevice device, ushort formatTag, ushort bitsPerSample, ushort blockAlign, ushort channels, uint samplesPerSecond) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond)
|
|
||||||
{
|
|
||||||
device.AddSoundSequenceReference(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SoundSequence(AudioDevice device, StaticSound templateSound) : base(device, templateSound.FormatTag, templateSound.BitsPerSample, templateSound.BlockAlign, templateSound.Channels, templateSound.SamplesPerSecond)
|
|
||||||
{
|
|
||||||
device.AddSoundSequenceReference(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Update()
|
|
||||||
{
|
|
||||||
lock (StateLock)
|
|
||||||
{
|
|
||||||
if (IsDisposed) { return; }
|
|
||||||
if (State != SoundState.Playing) { return; }
|
|
||||||
|
|
||||||
if (NeedSoundThreshold > 0)
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_GetState(
|
|
||||||
Voice,
|
|
||||||
out var state,
|
|
||||||
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
|
|
||||||
);
|
|
||||||
|
|
||||||
var queuedBufferCount = state.BuffersQueued;
|
|
||||||
for (int i = 0; i < NeedSoundThreshold - queuedBufferCount; i += 1)
|
|
||||||
{
|
|
||||||
if (OnSoundNeeded != null)
|
|
||||||
{
|
|
||||||
OnSoundNeeded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EnqueueSound(StaticSound sound)
|
|
||||||
{
|
|
||||||
#if DEBUG
|
|
||||||
if (
|
|
||||||
sound.FormatTag != Format.wFormatTag ||
|
|
||||||
sound.BitsPerSample != Format.wBitsPerSample ||
|
|
||||||
sound.Channels != Format.nChannels ||
|
|
||||||
sound.SamplesPerSecond != Format.nSamplesPerSec
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Logger.LogWarn("Playlist audio format mismatch!");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
lock (StateLock)
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
|
|
||||||
Voice,
|
|
||||||
ref sound.Handle,
|
|
||||||
IntPtr.Zero
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Pause()
|
|
||||||
{
|
|
||||||
lock (StateLock)
|
|
||||||
{
|
|
||||||
if (State == SoundState.Playing)
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
|
|
||||||
State = SoundState.Paused;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Play()
|
|
||||||
{
|
|
||||||
PlayUsingOperationSet(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void QueueSyncPlay()
|
|
||||||
{
|
|
||||||
PlayUsingOperationSet(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PlayUsingOperationSet(uint operationSet)
|
|
||||||
{
|
|
||||||
lock (StateLock)
|
|
||||||
{
|
|
||||||
if (State == SoundState.Playing)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet);
|
|
||||||
State = SoundState.Playing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Stop()
|
|
||||||
{
|
|
||||||
lock (StateLock)
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_ExitLoop(Voice, 0);
|
|
||||||
State = SoundState.Stopped;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void StopImmediate()
|
|
||||||
{
|
|
||||||
lock (StateLock)
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
|
|
||||||
FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice);
|
|
||||||
State = SoundState.Stopped;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Plays back a series of AudioBuffers in sequence. Set the OnSoundNeeded callback to add AudioBuffers dynamically.
|
||||||
|
/// </summary>
|
||||||
|
public class SoundSequence : SourceVoice
|
||||||
|
{
|
||||||
|
public int NeedSoundThreshold = 0;
|
||||||
|
public delegate void OnSoundNeededFunc();
|
||||||
|
public OnSoundNeededFunc OnSoundNeeded;
|
||||||
|
|
||||||
|
public SoundSequence(AudioDevice device, Format format) : base(device, format)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public SoundSequence(AudioDevice device, AudioBuffer templateSound) : base(device, templateSound.Format)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update()
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
if (State != SoundState.Playing) { return; }
|
||||||
|
|
||||||
|
if (NeedSoundThreshold > 0)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < NeedSoundThreshold - BuffersQueued; i += 1)
|
||||||
|
{
|
||||||
|
if (OnSoundNeeded != null)
|
||||||
|
{
|
||||||
|
OnSoundNeeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnqueueSound(AudioBuffer buffer)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
if (!(buffer.Format == Format))
|
||||||
|
{
|
||||||
|
Logger.LogWarn("Sound sequence audio format mismatch!");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
Submit(buffer.ToFAudioBuffer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Emits audio from submitted audio buffers.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class SourceVoice : Voice
|
||||||
|
{
|
||||||
|
private Format format;
|
||||||
|
public Format Format => format;
|
||||||
|
|
||||||
|
protected bool PlaybackInitiated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of buffers queued in the voice.
|
||||||
|
/// This includes the currently playing voice!
|
||||||
|
/// </summary>
|
||||||
|
public uint BuffersQueued
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
FAudio.FAudioSourceVoice_GetState(
|
||||||
|
Handle,
|
||||||
|
out var state,
|
||||||
|
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
|
||||||
|
);
|
||||||
|
|
||||||
|
return state.BuffersQueued;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SoundState state;
|
||||||
|
public SoundState State
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (BuffersQueued == 0)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
state = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected object StateLock = new object();
|
||||||
|
|
||||||
|
public SourceVoice(
|
||||||
|
AudioDevice device,
|
||||||
|
Format format
|
||||||
|
) : base(device, format.Channels, device.DeviceDetails.OutputFormat.Format.nChannels)
|
||||||
|
{
|
||||||
|
this.format = format;
|
||||||
|
var fAudioFormat = format.ToFAudioFormat();
|
||||||
|
|
||||||
|
FAudio.FAudio_CreateSourceVoice(
|
||||||
|
device.Handle,
|
||||||
|
out handle,
|
||||||
|
ref fAudioFormat,
|
||||||
|
FAudio.FAUDIO_VOICE_USEFILTER,
|
||||||
|
FAudio.FAUDIO_DEFAULT_FREQ_RATIO,
|
||||||
|
IntPtr.Zero,
|
||||||
|
IntPtr.Zero, // default sends to mastering voice!
|
||||||
|
IntPtr.Zero
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts consumption and processing of audio by the voice.
|
||||||
|
/// Delivers the result to any connected submix or mastering voice.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
|
||||||
|
public void Play(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
FAudio.FAudioSourceVoice_Start(Handle, 0, syncGroup);
|
||||||
|
|
||||||
|
State = SoundState.Playing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pauses playback.
|
||||||
|
/// All source buffers that are queued on the voice and the current cursor position are preserved.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
|
||||||
|
public void Pause(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup);
|
||||||
|
|
||||||
|
State = SoundState.Paused;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops looping the voice when it reaches the end of the current loop region.
|
||||||
|
/// If the cursor for the voice is not in a loop region, ExitLoop does nothing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
|
||||||
|
public void ExitLoop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
FAudio.FAudioSourceVoice_ExitLoop(Handle, syncGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops playback and removes all pending audio buffers from the voice queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="syncGroup">Optional. Denotes that the operation will be pending until AudioDevice.TriggerSyncGroup is called.</param>
|
||||||
|
public void Stop(uint syncGroup = FAudio.FAUDIO_COMMIT_NOW)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
FAudio.FAudioSourceVoice_Stop(Handle, 0, syncGroup);
|
||||||
|
FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
|
||||||
|
|
||||||
|
State = SoundState.Stopped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
public void Submit(AudioBuffer buffer)
|
||||||
|
{
|
||||||
|
Submit(buffer.ToFAudioBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates positional sound. This must be called continuously to update positional sound.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="listener"></param>
|
||||||
|
/// <param name="emitter"></param>
|
||||||
|
public unsafe void Apply3D(AudioListener listener, AudioEmitter emitter)
|
||||||
|
{
|
||||||
|
Is3D = true;
|
||||||
|
|
||||||
|
emitter.emitterData.CurveDistanceScaler = Device.CurveDistanceScalar;
|
||||||
|
emitter.emitterData.ChannelCount = SourceChannelCount;
|
||||||
|
|
||||||
|
var dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS
|
||||||
|
{
|
||||||
|
DopplerFactor = DopplerFactor,
|
||||||
|
SrcChannelCount = SourceChannelCount,
|
||||||
|
DstChannelCount = DestinationChannelCount,
|
||||||
|
pMatrixCoefficients = (nint) pMatrixCoefficients
|
||||||
|
};
|
||||||
|
|
||||||
|
FAudio.F3DAudioCalculate(
|
||||||
|
Device.Handle3D,
|
||||||
|
ref listener.listenerData,
|
||||||
|
ref emitter.emitterData,
|
||||||
|
FAudio.F3DAUDIO_CALCULATE_MATRIX | FAudio.F3DAUDIO_CALCULATE_DOPPLER,
|
||||||
|
ref dspSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
UpdatePitch();
|
||||||
|
|
||||||
|
FAudio.FAudioVoice_SetOutputMatrix(
|
||||||
|
Handle,
|
||||||
|
OutputVoice.Handle,
|
||||||
|
SourceChannelCount,
|
||||||
|
DestinationChannelCount,
|
||||||
|
(nint) pMatrixCoefficients,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies that this source voice can be returned to the voice pool.
|
||||||
|
/// Holding on to the reference after calling this will cause problems!
|
||||||
|
/// </summary>
|
||||||
|
public void Return()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
Device.Return(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Reset()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
PlaybackInitiated = false;
|
||||||
|
base.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override unsafe void Destroy()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
base.Destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
internal class SourceVoicePool
|
||||||
|
{
|
||||||
|
private AudioDevice Device;
|
||||||
|
|
||||||
|
Dictionary<(System.Type, Format), Queue<SourceVoice>> VoiceLists = new Dictionary<(System.Type, Format), Queue<SourceVoice>>();
|
||||||
|
|
||||||
|
public SourceVoicePool(AudioDevice device)
|
||||||
|
{
|
||||||
|
Device = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Obtain<T>(Format format) where T : SourceVoice, IPoolable<T>
|
||||||
|
{
|
||||||
|
if (!VoiceLists.ContainsKey((typeof(T), format)))
|
||||||
|
{
|
||||||
|
VoiceLists.Add((typeof(T), format), new Queue<SourceVoice>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = VoiceLists[(typeof(T), format)];
|
||||||
|
|
||||||
|
if (list.Count == 0)
|
||||||
|
{
|
||||||
|
list.Enqueue(T.Create(Device, format));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (T) list.Dequeue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Return(SourceVoice voice)
|
||||||
|
{
|
||||||
|
var list = VoiceLists[(voice.GetType(), voice.Format)];
|
||||||
|
list.Enqueue(voice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,332 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
public class StaticSound : AudioResource
|
|
||||||
{
|
|
||||||
internal FAudio.FAudioBuffer Handle;
|
|
||||||
public ushort FormatTag { get; }
|
|
||||||
public ushort BitsPerSample { get; }
|
|
||||||
public ushort Channels { get; }
|
|
||||||
public uint SamplesPerSecond { get; }
|
|
||||||
public ushort BlockAlign { get; }
|
|
||||||
|
|
||||||
public uint LoopStart { get; set; } = 0;
|
|
||||||
public uint LoopLength { get; set; } = 0;
|
|
||||||
|
|
||||||
private Stack<StaticSoundInstance> AvailableInstances = new Stack<StaticSoundInstance>();
|
|
||||||
private HashSet<StaticSoundInstance> UsedInstances = new HashSet<StaticSoundInstance>();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
return new StaticSound(
|
|
||||||
device,
|
|
||||||
3,
|
|
||||||
32,
|
|
||||||
(ushort) (4 * info.channels),
|
|
||||||
(ushort) info.channels,
|
|
||||||
info.sample_rate,
|
|
||||||
(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 sound = new StaticSound(
|
|
||||||
device,
|
|
||||||
wFormatTag,
|
|
||||||
wBitsPerSample,
|
|
||||||
nBlockAlign,
|
|
||||||
nChannels,
|
|
||||||
nSamplesPerSec,
|
|
||||||
(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);
|
|
||||||
|
|
||||||
return new StaticSound(
|
|
||||||
device,
|
|
||||||
1,
|
|
||||||
16,
|
|
||||||
(ushort) (channels * 2),
|
|
||||||
(ushort) channels,
|
|
||||||
samplerate,
|
|
||||||
(nint) buffer,
|
|
||||||
bufferLengthInBytes,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public StaticSound(
|
|
||||||
AudioDevice device,
|
|
||||||
ushort formatTag,
|
|
||||||
ushort bitsPerSample,
|
|
||||||
ushort blockAlign,
|
|
||||||
ushort channels,
|
|
||||||
uint samplesPerSecond,
|
|
||||||
IntPtr bufferPtr,
|
|
||||||
uint bufferLengthInBytes,
|
|
||||||
bool ownsBuffer) : base(device)
|
|
||||||
{
|
|
||||||
FormatTag = formatTag;
|
|
||||||
BitsPerSample = bitsPerSample;
|
|
||||||
BlockAlign = blockAlign;
|
|
||||||
Channels = channels;
|
|
||||||
SamplesPerSecond = samplesPerSecond;
|
|
||||||
|
|
||||||
Handle = new FAudio.FAudioBuffer
|
|
||||||
{
|
|
||||||
Flags = FAudio.FAUDIO_END_OF_STREAM,
|
|
||||||
pContext = IntPtr.Zero,
|
|
||||||
pAudioData = bufferPtr,
|
|
||||||
AudioBytes = bufferLengthInBytes,
|
|
||||||
PlayBegin = 0,
|
|
||||||
PlayLength = 0
|
|
||||||
};
|
|
||||||
|
|
||||||
OwnsBuffer = ownsBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a sound instance from the pool.
|
|
||||||
/// NOTE: If AutoFree is false, you will have to call StaticSoundInstance.Free() yourself or leak the instance!
|
|
||||||
/// </summary>
|
|
||||||
public StaticSoundInstance GetInstance(bool autoFree = true)
|
|
||||||
{
|
|
||||||
StaticSoundInstance instance;
|
|
||||||
|
|
||||||
lock (AvailableInstances)
|
|
||||||
{
|
|
||||||
if (AvailableInstances.Count == 0)
|
|
||||||
{
|
|
||||||
AvailableInstances.Push(new StaticSoundInstance(Device, this));
|
|
||||||
}
|
|
||||||
|
|
||||||
instance = AvailableInstances.Pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.AutoFree = autoFree;
|
|
||||||
|
|
||||||
lock (UsedInstances)
|
|
||||||
{
|
|
||||||
UsedInstances.Add(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void FreeInstance(StaticSoundInstance instance)
|
|
||||||
{
|
|
||||||
instance.Reset();
|
|
||||||
|
|
||||||
lock (UsedInstances)
|
|
||||||
{
|
|
||||||
UsedInstances.Remove(instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (AvailableInstances)
|
|
||||||
{
|
|
||||||
AvailableInstances.Push(instance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override unsafe void Destroy()
|
|
||||||
{
|
|
||||||
foreach (var instance in UsedInstances)
|
|
||||||
{
|
|
||||||
instance.Free();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var instance in AvailableInstances)
|
|
||||||
{
|
|
||||||
instance.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
AvailableInstances.Clear();
|
|
||||||
|
|
||||||
if (OwnsBuffer)
|
|
||||||
{
|
|
||||||
NativeMemory.Free((void*) Handle.pAudioData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
public class StaticSoundInstance : SoundInstance
|
|
||||||
{
|
|
||||||
public StaticSound Parent { get; }
|
|
||||||
|
|
||||||
public bool Loop { get; set; }
|
|
||||||
|
|
||||||
private SoundState _state = SoundState.Stopped;
|
|
||||||
public override SoundState State
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_GetState(
|
|
||||||
Voice,
|
|
||||||
out var state,
|
|
||||||
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
|
|
||||||
);
|
|
||||||
if (state.BuffersQueued == 0)
|
|
||||||
{
|
|
||||||
StopImmediate();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _state;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected set
|
|
||||||
{
|
|
||||||
_state = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutoFree { get; internal set; }
|
|
||||||
|
|
||||||
internal StaticSoundInstance(
|
|
||||||
AudioDevice device,
|
|
||||||
StaticSound parent
|
|
||||||
) : base(device, parent.FormatTag, parent.BitsPerSample, parent.BlockAlign, parent.Channels, parent.SamplesPerSecond)
|
|
||||||
{
|
|
||||||
Parent = parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Play()
|
|
||||||
{
|
|
||||||
PlayUsingOperationSet(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void QueueSyncPlay()
|
|
||||||
{
|
|
||||||
PlayUsingOperationSet(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PlayUsingOperationSet(uint operationSet)
|
|
||||||
{
|
|
||||||
if (State == SoundState.Playing)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Loop)
|
|
||||||
{
|
|
||||||
Parent.Handle.LoopCount = 255;
|
|
||||||
Parent.Handle.LoopBegin = Parent.LoopStart;
|
|
||||||
Parent.Handle.LoopLength = Parent.LoopLength;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Parent.Handle.LoopCount = 0;
|
|
||||||
Parent.Handle.LoopBegin = 0;
|
|
||||||
Parent.Handle.LoopLength = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
|
|
||||||
Voice,
|
|
||||||
ref Parent.Handle,
|
|
||||||
IntPtr.Zero
|
|
||||||
);
|
|
||||||
|
|
||||||
FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet);
|
|
||||||
State = SoundState.Playing;
|
|
||||||
|
|
||||||
if (AutoFree)
|
|
||||||
{
|
|
||||||
Device.AddAutoFreeStaticSoundInstance(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Pause()
|
|
||||||
{
|
|
||||||
if (State == SoundState.Playing)
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
|
|
||||||
State = SoundState.Paused;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Stop()
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_ExitLoop(Voice, 0);
|
|
||||||
State = SoundState.Stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void StopImmediate()
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
|
|
||||||
FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice);
|
|
||||||
State = SoundState.Stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Seek(uint sampleFrame)
|
|
||||||
{
|
|
||||||
if (State == SoundState.Playing)
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
|
|
||||||
FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice);
|
|
||||||
}
|
|
||||||
|
|
||||||
Parent.Handle.PlayBegin = sampleFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call this when you no longer need the sound instance.
|
|
||||||
// If AutoFree is set, this will automatically be called when the sound instance stops playing.
|
|
||||||
// If the sound isn't stopped when you call this, things might get weird!
|
|
||||||
public void Free()
|
|
||||||
{
|
|
||||||
Parent.FreeInstance(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Reset()
|
|
||||||
{
|
|
||||||
Pan = 0;
|
|
||||||
Pitch = 0;
|
|
||||||
Volume = 1;
|
|
||||||
Loop = false;
|
|
||||||
Is3D = false;
|
|
||||||
FilterType = FilterType.None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,239 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// For streaming long playback.
|
|
||||||
/// Must be extended with a decoder routine called by FillBuffer.
|
|
||||||
/// See StreamingSoundOgg for an example.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class StreamingSound : SoundInstance
|
|
||||||
{
|
|
||||||
// Are we actively consuming buffers?
|
|
||||||
protected bool ConsumingBuffers = false;
|
|
||||||
|
|
||||||
private const int BUFFER_COUNT = 3;
|
|
||||||
private nuint BufferSize;
|
|
||||||
private readonly IntPtr[] buffers;
|
|
||||||
private int nextBufferIndex = 0;
|
|
||||||
private uint queuedBufferCount = 0;
|
|
||||||
|
|
||||||
private readonly object StateLock = new object();
|
|
||||||
|
|
||||||
public bool AutoUpdate { get; }
|
|
||||||
|
|
||||||
public abstract bool Loaded { get; }
|
|
||||||
|
|
||||||
public unsafe StreamingSound(
|
|
||||||
AudioDevice device,
|
|
||||||
ushort formatTag,
|
|
||||||
ushort bitsPerSample,
|
|
||||||
ushort blockAlign,
|
|
||||||
ushort channels,
|
|
||||||
uint samplesPerSecond,
|
|
||||||
uint bufferSize,
|
|
||||||
bool autoUpdate // should the AudioDevice thread automatically update this sound?
|
|
||||||
) : base(device, formatTag, bitsPerSample, blockAlign, channels, samplesPerSecond)
|
|
||||||
{
|
|
||||||
BufferSize = bufferSize;
|
|
||||||
|
|
||||||
buffers = new IntPtr[BUFFER_COUNT];
|
|
||||||
for (int i = 0; i < BUFFER_COUNT; i += 1)
|
|
||||||
{
|
|
||||||
buffers[i] = (IntPtr) NativeMemory.Alloc(bufferSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
AutoUpdate = autoUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Play()
|
|
||||||
{
|
|
||||||
PlayUsingOperationSet(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void QueueSyncPlay()
|
|
||||||
{
|
|
||||||
PlayUsingOperationSet(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PlayUsingOperationSet(uint operationSet)
|
|
||||||
{
|
|
||||||
lock (StateLock)
|
|
||||||
{
|
|
||||||
if (!Loaded)
|
|
||||||
{
|
|
||||||
Logger.LogError("Cannot play StreamingSound before calling Load!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (State == SoundState.Playing)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
State = SoundState.Playing;
|
|
||||||
|
|
||||||
ConsumingBuffers = true;
|
|
||||||
if (AutoUpdate)
|
|
||||||
{
|
|
||||||
Device.AddAutoUpdateStreamingSoundInstance(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueBuffers();
|
|
||||||
FAudio.FAudioSourceVoice_Start(Voice, 0, operationSet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Pause()
|
|
||||||
{
|
|
||||||
lock (StateLock)
|
|
||||||
{
|
|
||||||
if (State == SoundState.Playing)
|
|
||||||
{
|
|
||||||
ConsumingBuffers = false;
|
|
||||||
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
|
|
||||||
State = SoundState.Paused;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Stop()
|
|
||||||
{
|
|
||||||
lock (StateLock)
|
|
||||||
{
|
|
||||||
ConsumingBuffers = false;
|
|
||||||
State = SoundState.Stopped;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void StopImmediate()
|
|
||||||
{
|
|
||||||
lock (StateLock)
|
|
||||||
{
|
|
||||||
ConsumingBuffers = false;
|
|
||||||
FAudio.FAudioSourceVoice_Stop(Voice, 0, 0);
|
|
||||||
FAudio.FAudioSourceVoice_FlushSourceBuffers(Voice);
|
|
||||||
ClearBuffers();
|
|
||||||
|
|
||||||
State = SoundState.Stopped;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal unsafe void Update()
|
|
||||||
{
|
|
||||||
lock (StateLock)
|
|
||||||
{
|
|
||||||
if (!IsDisposed)
|
|
||||||
{
|
|
||||||
if (State != SoundState.Playing)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueBuffers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void QueueBuffers()
|
|
||||||
{
|
|
||||||
FAudio.FAudioSourceVoice_GetState(
|
|
||||||
Voice,
|
|
||||||
out var state,
|
|
||||||
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
|
|
||||||
);
|
|
||||||
|
|
||||||
queuedBufferCount = state.BuffersQueued;
|
|
||||||
|
|
||||||
if (ConsumingBuffers)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < BUFFER_COUNT - queuedBufferCount; i += 1)
|
|
||||||
{
|
|
||||||
AddBuffer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (queuedBufferCount == 0)
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected unsafe void ClearBuffers()
|
|
||||||
{
|
|
||||||
nextBufferIndex = 0;
|
|
||||||
queuedBufferCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected unsafe void AddBuffer()
|
|
||||||
{
|
|
||||||
var buffer = buffers[nextBufferIndex];
|
|
||||||
nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT;
|
|
||||||
|
|
||||||
FillBuffer(
|
|
||||||
(void*) buffer,
|
|
||||||
(int) BufferSize,
|
|
||||||
out int filledLengthInBytes,
|
|
||||||
out bool reachedEnd
|
|
||||||
);
|
|
||||||
|
|
||||||
if (filledLengthInBytes > 0)
|
|
||||||
{
|
|
||||||
FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer
|
|
||||||
{
|
|
||||||
AudioBytes = (uint) filledLengthInBytes,
|
|
||||||
pAudioData = (IntPtr) buffer,
|
|
||||||
PlayLength = (
|
|
||||||
(uint) (filledLengthInBytes /
|
|
||||||
Format.nChannels /
|
|
||||||
(uint) (Format.wBitsPerSample / 8))
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
|
|
||||||
Voice,
|
|
||||||
ref buf,
|
|
||||||
IntPtr.Zero
|
|
||||||
);
|
|
||||||
|
|
||||||
queuedBufferCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reachedEnd)
|
|
||||||
{
|
|
||||||
/* We have reached the end of the data, what do we do? */
|
|
||||||
ConsumingBuffers = false;
|
|
||||||
OnReachedEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void Load();
|
|
||||||
public abstract void Unload();
|
|
||||||
|
|
||||||
protected unsafe abstract void FillBuffer(
|
|
||||||
void* buffer,
|
|
||||||
int bufferLengthInBytes, /* in bytes */
|
|
||||||
out int filledLengthInBytes, /* in bytes */
|
|
||||||
out bool reachedEnd
|
|
||||||
);
|
|
||||||
|
|
||||||
protected abstract void OnReachedEnd();
|
|
||||||
|
|
||||||
protected unsafe override void Destroy()
|
|
||||||
{
|
|
||||||
lock (StateLock)
|
|
||||||
{
|
|
||||||
if (!IsDisposed)
|
|
||||||
{
|
|
||||||
StopImmediate();
|
|
||||||
Unload();
|
|
||||||
|
|
||||||
for (int i = 0; i < BUFFER_COUNT; i += 1)
|
|
||||||
{
|
|
||||||
NativeMemory.Free((void*) buffers[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
public class StreamingSoundOgg : StreamingSoundSeekable
|
|
||||||
{
|
|
||||||
private IntPtr FileDataPtr = IntPtr.Zero;
|
|
||||||
private IntPtr VorbisHandle = IntPtr.Zero;
|
|
||||||
private FAudio.stb_vorbis_info Info;
|
|
||||||
|
|
||||||
public override bool Loaded => VorbisHandle != IntPtr.Zero;
|
|
||||||
private string FilePath;
|
|
||||||
|
|
||||||
public unsafe static StreamingSoundOgg Create(AudioDevice device, string filePath)
|
|
||||||
{
|
|
||||||
var handle = FAudio.stb_vorbis_open_filename(filePath, out int error, IntPtr.Zero);
|
|
||||||
if (error != 0)
|
|
||||||
{
|
|
||||||
Logger.LogError("Error: " + error);
|
|
||||||
throw new AudioLoadException("Error opening ogg file!");
|
|
||||||
}
|
|
||||||
|
|
||||||
var info = FAudio.stb_vorbis_get_info(handle);
|
|
||||||
|
|
||||||
var streamingSound = new StreamingSoundOgg(
|
|
||||||
device,
|
|
||||||
filePath,
|
|
||||||
info
|
|
||||||
);
|
|
||||||
|
|
||||||
FAudio.stb_vorbis_close(handle);
|
|
||||||
|
|
||||||
return streamingSound;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal unsafe StreamingSoundOgg(
|
|
||||||
AudioDevice device,
|
|
||||||
string filePath,
|
|
||||||
FAudio.stb_vorbis_info info,
|
|
||||||
uint bufferSize = 32768
|
|
||||||
) : base(
|
|
||||||
device,
|
|
||||||
3, /* float type */
|
|
||||||
32, /* size of float */
|
|
||||||
(ushort) (4 * info.channels),
|
|
||||||
(ushort) info.channels,
|
|
||||||
info.sample_rate,
|
|
||||||
bufferSize,
|
|
||||||
true
|
|
||||||
) {
|
|
||||||
Info = info;
|
|
||||||
FilePath = filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Seek(uint sampleFrame)
|
|
||||||
{
|
|
||||||
FAudio.stb_vorbis_seek(VorbisHandle, sampleFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override unsafe void Load()
|
|
||||||
{
|
|
||||||
var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
|
|
||||||
FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length);
|
|
||||||
var fileDataSpan = new Span<byte>((void*) FileDataPtr, (int) fileStream.Length);
|
|
||||||
fileStream.ReadExactly(fileDataSpan);
|
|
||||||
fileStream.Close();
|
|
||||||
|
|
||||||
VorbisHandle = FAudio.stb_vorbis_open_memory(FileDataPtr, fileDataSpan.Length, out int error, IntPtr.Zero);
|
|
||||||
if (error != 0)
|
|
||||||
{
|
|
||||||
NativeMemory.Free((void*) FileDataPtr);
|
|
||||||
Logger.LogError("Error opening OGG file!");
|
|
||||||
Logger.LogError("Error: " + error);
|
|
||||||
throw new AudioLoadException("Error opening OGG file!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override unsafe void Unload()
|
|
||||||
{
|
|
||||||
if (Loaded)
|
|
||||||
{
|
|
||||||
FAudio.stb_vorbis_close(VorbisHandle);
|
|
||||||
NativeMemory.Free((void*) FileDataPtr);
|
|
||||||
|
|
||||||
VorbisHandle = IntPtr.Zero;
|
|
||||||
FileDataPtr = IntPtr.Zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected unsafe override void FillBuffer(
|
|
||||||
void* buffer,
|
|
||||||
int bufferLengthInBytes,
|
|
||||||
out int filledLengthInBytes,
|
|
||||||
out bool reachedEnd
|
|
||||||
) {
|
|
||||||
var lengthInFloats = bufferLengthInBytes / sizeof(float);
|
|
||||||
|
|
||||||
/* NOTE: this function returns samples per channel, not total samples */
|
|
||||||
var samples = FAudio.stb_vorbis_get_samples_float_interleaved(
|
|
||||||
VorbisHandle,
|
|
||||||
Info.channels,
|
|
||||||
(IntPtr) buffer,
|
|
||||||
lengthInFloats
|
|
||||||
);
|
|
||||||
|
|
||||||
var sampleCount = samples * Info.channels;
|
|
||||||
reachedEnd = sampleCount < lengthInFloats;
|
|
||||||
filledLengthInBytes = sampleCount * sizeof(float);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
public class StreamingSoundQoa : StreamingSoundSeekable
|
|
||||||
{
|
|
||||||
private IntPtr QoaHandle = IntPtr.Zero;
|
|
||||||
private IntPtr FileDataPtr = IntPtr.Zero;
|
|
||||||
|
|
||||||
uint Channels;
|
|
||||||
uint SamplesPerChannelPerFrame;
|
|
||||||
uint TotalSamplesPerChannel;
|
|
||||||
|
|
||||||
public override bool Loaded => QoaHandle != IntPtr.Zero;
|
|
||||||
private string FilePath;
|
|
||||||
|
|
||||||
private const uint QOA_MAGIC = 0x716f6166; /* 'qoaf' */
|
|
||||||
|
|
||||||
private static unsafe UInt64 ReverseEndianness(UInt64 value)
|
|
||||||
{
|
|
||||||
byte* bytes = (byte*) &value;
|
|
||||||
|
|
||||||
return
|
|
||||||
((UInt64)(bytes[0]) << 56) | ((UInt64)(bytes[1]) << 48) |
|
|
||||||
((UInt64)(bytes[2]) << 40) | ((UInt64)(bytes[3]) << 32) |
|
|
||||||
((UInt64)(bytes[4]) << 24) | ((UInt64)(bytes[5]) << 16) |
|
|
||||||
((UInt64)(bytes[6]) << 8) | ((UInt64)(bytes[7]) << 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe static StreamingSoundQoa Create(AudioDevice device, string filePath)
|
|
||||||
{
|
|
||||||
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
|
||||||
using var reader = new BinaryReader(stream);
|
|
||||||
|
|
||||||
UInt64 fileHeader = ReverseEndianness(reader.ReadUInt64());
|
|
||||||
if ((fileHeader >> 32) != QOA_MAGIC)
|
|
||||||
{
|
|
||||||
throw new AudioLoadException("Specified file is not a QOA file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
uint totalSamplesPerChannel = (uint) (fileHeader & (0xFFFFFFFF));
|
|
||||||
if (totalSamplesPerChannel == 0)
|
|
||||||
{
|
|
||||||
throw new AudioLoadException("Specified file is not a valid QOA file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
UInt64 frameHeader = ReverseEndianness(reader.ReadUInt64());
|
|
||||||
uint channels = (uint) ((frameHeader >> 56) & 0x0000FF);
|
|
||||||
uint samplerate = (uint) ((frameHeader >> 32) & 0xFFFFFF);
|
|
||||||
uint samplesPerChannelPerFrame = (uint) ((frameHeader >> 16) & 0x00FFFF);
|
|
||||||
|
|
||||||
return new StreamingSoundQoa(
|
|
||||||
device,
|
|
||||||
filePath,
|
|
||||||
channels,
|
|
||||||
samplerate,
|
|
||||||
samplesPerChannelPerFrame,
|
|
||||||
totalSamplesPerChannel
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal unsafe StreamingSoundQoa(
|
|
||||||
AudioDevice device,
|
|
||||||
string filePath,
|
|
||||||
uint channels,
|
|
||||||
uint samplesPerSecond,
|
|
||||||
uint samplesPerChannelPerFrame,
|
|
||||||
uint totalSamplesPerChannel
|
|
||||||
) : base(
|
|
||||||
device,
|
|
||||||
1,
|
|
||||||
16,
|
|
||||||
(ushort) (2 * channels),
|
|
||||||
(ushort) channels,
|
|
||||||
samplesPerSecond,
|
|
||||||
samplesPerChannelPerFrame * channels * sizeof(short),
|
|
||||||
true
|
|
||||||
) {
|
|
||||||
Channels = channels;
|
|
||||||
SamplesPerChannelPerFrame = samplesPerChannelPerFrame;
|
|
||||||
TotalSamplesPerChannel = totalSamplesPerChannel;
|
|
||||||
FilePath = filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Seek(uint sampleFrame)
|
|
||||||
{
|
|
||||||
FAudio.qoa_seek_frame(QoaHandle, (int) sampleFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override unsafe void Load()
|
|
||||||
{
|
|
||||||
var fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read);
|
|
||||||
FileDataPtr = (nint) NativeMemory.Alloc((nuint) fileStream.Length);
|
|
||||||
var fileDataSpan = new Span<byte>((void*) FileDataPtr, (int) fileStream.Length);
|
|
||||||
fileStream.ReadExactly(fileDataSpan);
|
|
||||||
fileStream.Close();
|
|
||||||
|
|
||||||
QoaHandle = FAudio.qoa_open_from_memory((char*) FileDataPtr, (uint) fileDataSpan.Length, 0);
|
|
||||||
if (QoaHandle == IntPtr.Zero)
|
|
||||||
{
|
|
||||||
NativeMemory.Free((void*) FileDataPtr);
|
|
||||||
Logger.LogError("Error opening QOA file!");
|
|
||||||
throw new AudioLoadException("Error opening QOA file!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override unsafe void Unload()
|
|
||||||
{
|
|
||||||
if (Loaded)
|
|
||||||
{
|
|
||||||
FAudio.qoa_close(QoaHandle);
|
|
||||||
NativeMemory.Free((void*) FileDataPtr);
|
|
||||||
|
|
||||||
QoaHandle = IntPtr.Zero;
|
|
||||||
FileDataPtr = IntPtr.Zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override unsafe void FillBuffer(
|
|
||||||
void* buffer,
|
|
||||||
int bufferLengthInBytes,
|
|
||||||
out int filledLengthInBytes,
|
|
||||||
out bool reachedEnd
|
|
||||||
) {
|
|
||||||
var lengthInShorts = bufferLengthInBytes / sizeof(short);
|
|
||||||
|
|
||||||
// NOTE: this function returns samples per channel!
|
|
||||||
var samples = FAudio.qoa_decode_next_frame(QoaHandle, (short*) buffer);
|
|
||||||
|
|
||||||
var sampleCount = samples * Channels;
|
|
||||||
reachedEnd = sampleCount < lengthInShorts;
|
|
||||||
filledLengthInBytes = (int) (sampleCount * sizeof(short));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
namespace MoonWorks.Audio
|
|
||||||
{
|
|
||||||
public abstract class StreamingSoundSeekable : StreamingSound
|
|
||||||
{
|
|
||||||
public bool Loop { get; set; }
|
|
||||||
|
|
||||||
protected StreamingSoundSeekable(
|
|
||||||
AudioDevice device,
|
|
||||||
ushort formatTag,
|
|
||||||
ushort bitsPerSample,
|
|
||||||
ushort blockAlign,
|
|
||||||
ushort channels,
|
|
||||||
uint samplesPerSecond,
|
|
||||||
uint bufferSize,
|
|
||||||
bool autoUpdate
|
|
||||||
) : base(
|
|
||||||
device,
|
|
||||||
formatTag,
|
|
||||||
bitsPerSample,
|
|
||||||
blockAlign,
|
|
||||||
channels,
|
|
||||||
samplesPerSecond,
|
|
||||||
bufferSize,
|
|
||||||
autoUpdate
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void Seek(uint sampleFrame);
|
|
||||||
|
|
||||||
protected override void OnReachedEnd()
|
|
||||||
{
|
|
||||||
if (Loop)
|
|
||||||
{
|
|
||||||
ConsumingBuffers = true;
|
|
||||||
Seek(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Use in conjunction with an AudioDataStreamable object to play back streaming audio data.
|
||||||
|
/// </summary>
|
||||||
|
public class StreamingVoice : SourceVoice, IPoolable<StreamingVoice>
|
||||||
|
{
|
||||||
|
private const int BUFFER_COUNT = 3;
|
||||||
|
private readonly IntPtr[] buffers;
|
||||||
|
private int nextBufferIndex = 0;
|
||||||
|
private uint BufferSize;
|
||||||
|
|
||||||
|
public bool Loop { get; set; }
|
||||||
|
|
||||||
|
public AudioDataStreamable AudioData { get; protected set; }
|
||||||
|
|
||||||
|
public unsafe StreamingVoice(AudioDevice device, Format format) : base(device, format)
|
||||||
|
{
|
||||||
|
buffers = new IntPtr[BUFFER_COUNT];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StreamingVoice Create(AudioDevice device, Format format)
|
||||||
|
{
|
||||||
|
return new StreamingVoice(device, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads and prepares an AudioDataStreamable for streaming playback.
|
||||||
|
/// This automatically calls Load on the given AudioDataStreamable.
|
||||||
|
/// </summary>
|
||||||
|
public void Load(AudioDataStreamable data)
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
if (AudioData != null)
|
||||||
|
{
|
||||||
|
AudioData.Unload();
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Load();
|
||||||
|
AudioData = data;
|
||||||
|
|
||||||
|
InitializeBuffers();
|
||||||
|
QueueBuffers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unloads AudioDataStreamable from this voice.
|
||||||
|
/// This automatically calls Unload on the given AudioDataStreamable.
|
||||||
|
/// </summary>
|
||||||
|
public void Unload()
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
if (AudioData != null)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
AudioData.Unload();
|
||||||
|
AudioData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Reset()
|
||||||
|
{
|
||||||
|
Unload();
|
||||||
|
base.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update()
|
||||||
|
{
|
||||||
|
lock (StateLock)
|
||||||
|
{
|
||||||
|
if (AudioData == null || State != SoundState.Playing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueBuffers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void QueueBuffers()
|
||||||
|
{
|
||||||
|
int buffersNeeded = BUFFER_COUNT - (int) BuffersQueued; // don't get got by uint underflow!
|
||||||
|
for (int i = 0; i < buffersNeeded; i += 1)
|
||||||
|
{
|
||||||
|
AddBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void AddBuffer()
|
||||||
|
{
|
||||||
|
var buffer = buffers[nextBufferIndex];
|
||||||
|
nextBufferIndex = (nextBufferIndex + 1) % BUFFER_COUNT;
|
||||||
|
|
||||||
|
AudioData.Decode(
|
||||||
|
(void*) buffer,
|
||||||
|
(int) BufferSize,
|
||||||
|
out int filledLengthInBytes,
|
||||||
|
out bool reachedEnd
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filledLengthInBytes > 0)
|
||||||
|
{
|
||||||
|
var buf = new FAudio.FAudioBuffer
|
||||||
|
{
|
||||||
|
AudioBytes = (uint) filledLengthInBytes,
|
||||||
|
pAudioData = buffer,
|
||||||
|
PlayLength = (
|
||||||
|
(uint) (filledLengthInBytes /
|
||||||
|
Format.Channels /
|
||||||
|
(uint) (Format.BitsPerSample / 8))
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Submit(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reachedEnd)
|
||||||
|
{
|
||||||
|
/* We have reached the end of the data, what do we do? */
|
||||||
|
if (Loop)
|
||||||
|
{
|
||||||
|
AudioData.Seek(0);
|
||||||
|
AddBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void InitializeBuffers()
|
||||||
|
{
|
||||||
|
BufferSize = AudioData.DecodeBufferSize;
|
||||||
|
|
||||||
|
for (int i = 0; i < BUFFER_COUNT; i += 1)
|
||||||
|
{
|
||||||
|
if (buffers[i] != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
NativeMemory.Free((void*) buffers[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffers[i] = (IntPtr) NativeMemory.Alloc(BufferSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SourceVoices can send audio to a SubmixVoice for convenient effects processing.
|
||||||
|
/// Submixes process in order of processingStage, from lowest to highest.
|
||||||
|
/// Therefore submixes early in a chain should have a low processingStage, and later in the chain they should have a higher one.
|
||||||
|
/// </summary>
|
||||||
|
public class SubmixVoice : Voice
|
||||||
|
{
|
||||||
|
public SubmixVoice(
|
||||||
|
AudioDevice device,
|
||||||
|
uint sourceChannelCount,
|
||||||
|
uint sampleRate,
|
||||||
|
uint processingStage
|
||||||
|
) : base(device, sourceChannelCount, device.DeviceDetails.OutputFormat.Format.nChannels)
|
||||||
|
{
|
||||||
|
FAudio.FAudio_CreateSubmixVoice(
|
||||||
|
device.Handle,
|
||||||
|
out handle,
|
||||||
|
sourceChannelCount,
|
||||||
|
sampleRate,
|
||||||
|
FAudio.FAUDIO_VOICE_USEFILTER,
|
||||||
|
processingStage,
|
||||||
|
IntPtr.Zero, // default sends to mastering voice
|
||||||
|
IntPtr.Zero
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
namespace MoonWorks.Audio
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// TransientVoice is intended for playing one-off sound effects that don't have a long term reference.
|
||||||
|
/// It will be automatically returned to the source voice pool once it is done playing back.
|
||||||
|
/// </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 (PlaybackInitiated && BuffersQueued == 0)
|
||||||
|
{
|
||||||
|
Return();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,58 +4,60 @@ using EasingFunction = System.Func<float, float>;
|
||||||
|
|
||||||
namespace MoonWorks.Audio
|
namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
public abstract class SoundInstance : AudioResource
|
public abstract unsafe class Voice : AudioResource
|
||||||
{
|
{
|
||||||
internal IntPtr Voice;
|
protected IntPtr handle;
|
||||||
|
public IntPtr Handle => handle;
|
||||||
|
|
||||||
private FAudio.FAudioWaveFormatEx format;
|
public uint SourceChannelCount { get; }
|
||||||
public FAudio.FAudioWaveFormatEx Format => format;
|
public uint DestinationChannelCount { get; }
|
||||||
|
|
||||||
protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings;
|
|
||||||
|
|
||||||
|
protected SubmixVoice OutputVoice;
|
||||||
private ReverbEffect ReverbEffect;
|
private ReverbEffect ReverbEffect;
|
||||||
private FAudio.FAudioVoiceSends ReverbSends;
|
|
||||||
|
protected byte* pMatrixCoefficients;
|
||||||
|
|
||||||
public bool Is3D { get; protected set; }
|
public bool Is3D { get; protected set; }
|
||||||
|
|
||||||
public virtual SoundState State { get; protected set; }
|
private float dopplerFactor;
|
||||||
|
/// <summary>
|
||||||
private float pan = 0;
|
/// The strength of the doppler effect on this voice.
|
||||||
public float Pan
|
/// </summary>
|
||||||
|
public float DopplerFactor
|
||||||
{
|
{
|
||||||
get => pan;
|
get => dopplerFactor;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (dopplerFactor != value)
|
||||||
|
{
|
||||||
|
dopplerFactor = value;
|
||||||
|
UpdatePitch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float volume = 1;
|
||||||
|
/// <summary>
|
||||||
|
/// The overall volume level for the voice.
|
||||||
|
/// </summary>
|
||||||
|
public float Volume
|
||||||
|
{
|
||||||
|
get => volume;
|
||||||
internal set
|
internal set
|
||||||
{
|
{
|
||||||
value = Math.MathHelper.Clamp(value, -1f, 1f);
|
value = Math.MathHelper.Max(0, value);
|
||||||
if (pan != value)
|
if (volume != value)
|
||||||
{
|
{
|
||||||
pan = value;
|
volume = value;
|
||||||
|
FAudio.FAudioVoice_SetVolume(Handle, volume, 0);
|
||||||
if (pan < -1f)
|
|
||||||
{
|
|
||||||
pan = -1f;
|
|
||||||
}
|
|
||||||
if (pan > 1f)
|
|
||||||
{
|
|
||||||
pan = 1f;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Is3D) { return; }
|
|
||||||
|
|
||||||
SetPanMatrixCoefficients();
|
|
||||||
FAudio.FAudioVoice_SetOutputMatrix(
|
|
||||||
Voice,
|
|
||||||
Device.MasteringVoice,
|
|
||||||
dspSettings.SrcChannelCount,
|
|
||||||
dspSettings.DstChannelCount,
|
|
||||||
dspSettings.pMatrixCoefficients,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private float pitch = 0;
|
private float pitch = 0;
|
||||||
|
/// <summary>
|
||||||
|
/// The pitch of the voice.
|
||||||
|
/// </summary>
|
||||||
public float Pitch
|
public float Pitch
|
||||||
{
|
{
|
||||||
get => pitch;
|
get => pitch;
|
||||||
|
@ -70,21 +72,6 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private float volume = 1;
|
|
||||||
public float Volume
|
|
||||||
{
|
|
||||||
get => volume;
|
|
||||||
internal set
|
|
||||||
{
|
|
||||||
value = Math.MathHelper.Max(0, value);
|
|
||||||
if (volume != value)
|
|
||||||
{
|
|
||||||
volume = value;
|
|
||||||
FAudio.FAudioVoice_SetVolume(Voice, volume, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const float MAX_FILTER_FREQUENCY = 1f;
|
private const float MAX_FILTER_FREQUENCY = 1f;
|
||||||
private const float MAX_FILTER_ONEOVERQ = 1.5f;
|
private const float MAX_FILTER_ONEOVERQ = 1.5f;
|
||||||
|
|
||||||
|
@ -95,6 +82,9 @@ namespace MoonWorks.Audio
|
||||||
OneOverQ = 1f
|
OneOverQ = 1f
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The frequency cutoff on the voice filter.
|
||||||
|
/// </summary>
|
||||||
public float FilterFrequency
|
public float FilterFrequency
|
||||||
{
|
{
|
||||||
get => filterParameters.Frequency;
|
get => filterParameters.Frequency;
|
||||||
|
@ -106,7 +96,7 @@ namespace MoonWorks.Audio
|
||||||
filterParameters.Frequency = value;
|
filterParameters.Frequency = value;
|
||||||
|
|
||||||
FAudio.FAudioVoice_SetFilterParameters(
|
FAudio.FAudioVoice_SetFilterParameters(
|
||||||
Voice,
|
Handle,
|
||||||
ref filterParameters,
|
ref filterParameters,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
@ -114,6 +104,10 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reciprocal of Q factor.
|
||||||
|
/// Controls how quickly frequencies beyond the filter frequency are dampened.
|
||||||
|
/// </summary>
|
||||||
public float FilterOneOverQ
|
public float FilterOneOverQ
|
||||||
{
|
{
|
||||||
get => filterParameters.OneOverQ;
|
get => filterParameters.OneOverQ;
|
||||||
|
@ -125,7 +119,7 @@ namespace MoonWorks.Audio
|
||||||
filterParameters.OneOverQ = value;
|
filterParameters.OneOverQ = value;
|
||||||
|
|
||||||
FAudio.FAudioVoice_SetFilterParameters(
|
FAudio.FAudioVoice_SetFilterParameters(
|
||||||
Voice,
|
Handle,
|
||||||
ref filterParameters,
|
ref filterParameters,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
@ -134,6 +128,9 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
|
|
||||||
private FilterType filterType;
|
private FilterType filterType;
|
||||||
|
/// <summary>
|
||||||
|
/// The frequency filter that is applied to the voice.
|
||||||
|
/// </summary>
|
||||||
public FilterType FilterType
|
public FilterType FilterType
|
||||||
{
|
{
|
||||||
get => filterType;
|
get => filterType;
|
||||||
|
@ -170,7 +167,7 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
|
|
||||||
FAudio.FAudioVoice_SetFilterParameters(
|
FAudio.FAudioVoice_SetFilterParameters(
|
||||||
Voice,
|
Handle,
|
||||||
ref filterParameters,
|
ref filterParameters,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
@ -178,7 +175,49 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected float pan = 0;
|
||||||
|
/// <summary>
|
||||||
|
/// Left-right panning. -1 is hard left pan, 1 is hard right pan.
|
||||||
|
/// </summary>
|
||||||
|
public float Pan
|
||||||
|
{
|
||||||
|
get => pan;
|
||||||
|
internal set
|
||||||
|
{
|
||||||
|
value = Math.MathHelper.Clamp(value, -1f, 1f);
|
||||||
|
if (pan != value)
|
||||||
|
{
|
||||||
|
pan = value;
|
||||||
|
|
||||||
|
if (pan < -1f)
|
||||||
|
{
|
||||||
|
pan = -1f;
|
||||||
|
}
|
||||||
|
if (pan > 1f)
|
||||||
|
{
|
||||||
|
pan = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Is3D) { return; }
|
||||||
|
|
||||||
|
SetPanMatrixCoefficients();
|
||||||
|
FAudio.FAudioVoice_SetOutputMatrix(
|
||||||
|
Handle,
|
||||||
|
OutputVoice.Handle,
|
||||||
|
SourceChannelCount,
|
||||||
|
DestinationChannelCount,
|
||||||
|
(nint) pMatrixCoefficients,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private float reverb;
|
private float reverb;
|
||||||
|
/// <summary>
|
||||||
|
/// The wet-dry mix of the reverb effect.
|
||||||
|
/// Has no effect if SetReverbEffectChain has not been called.
|
||||||
|
/// </summary>
|
||||||
public unsafe float Reverb
|
public unsafe float Reverb
|
||||||
{
|
{
|
||||||
get => reverb;
|
get => reverb;
|
||||||
|
@ -191,19 +230,19 @@ namespace MoonWorks.Audio
|
||||||
{
|
{
|
||||||
reverb = value;
|
reverb = value;
|
||||||
|
|
||||||
float* outputMatrix = (float*) dspSettings.pMatrixCoefficients;
|
float* outputMatrix = (float*) pMatrixCoefficients;
|
||||||
outputMatrix[0] = reverb;
|
outputMatrix[0] = reverb;
|
||||||
if (dspSettings.SrcChannelCount == 2)
|
if (SourceChannelCount == 2)
|
||||||
{
|
{
|
||||||
outputMatrix[1] = reverb;
|
outputMatrix[1] = reverb;
|
||||||
}
|
}
|
||||||
|
|
||||||
FAudio.FAudioVoice_SetOutputMatrix(
|
FAudio.FAudioVoice_SetOutputMatrix(
|
||||||
Voice,
|
Handle,
|
||||||
ReverbEffect.Voice,
|
ReverbEffect.Handle,
|
||||||
dspSettings.SrcChannelCount,
|
SourceChannelCount,
|
||||||
1,
|
1,
|
||||||
dspSettings.pMatrixCoefficients,
|
(nint) pMatrixCoefficients,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -218,225 +257,236 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe SoundInstance(
|
public Voice(AudioDevice device, uint sourceChannelCount, uint destinationChannelCount) : base(device)
|
||||||
AudioDevice device,
|
|
||||||
ushort formatTag,
|
|
||||||
ushort bitsPerSample,
|
|
||||||
ushort blockAlign,
|
|
||||||
ushort channels,
|
|
||||||
uint samplesPerSecond
|
|
||||||
) : base(device)
|
|
||||||
{
|
{
|
||||||
format = new FAudio.FAudioWaveFormatEx
|
SourceChannelCount = sourceChannelCount;
|
||||||
{
|
DestinationChannelCount = destinationChannelCount;
|
||||||
wFormatTag = formatTag,
|
OutputVoice = device.MasteringVoice;
|
||||||
wBitsPerSample = bitsPerSample,
|
nuint memsize = 4 * sourceChannelCount * destinationChannelCount;
|
||||||
nChannels = channels,
|
pMatrixCoefficients = (byte*) NativeMemory.AllocZeroed(memsize);
|
||||||
nBlockAlign = blockAlign,
|
SetPanMatrixCoefficients();
|
||||||
nSamplesPerSec = samplesPerSecond,
|
|
||||||
nAvgBytesPerSec = blockAlign * samplesPerSecond
|
|
||||||
};
|
|
||||||
|
|
||||||
FAudio.FAudio_CreateSourceVoice(
|
|
||||||
Device.Handle,
|
|
||||||
out Voice,
|
|
||||||
ref format,
|
|
||||||
FAudio.FAUDIO_VOICE_USEFILTER,
|
|
||||||
FAudio.FAUDIO_DEFAULT_FREQ_RATIO,
|
|
||||||
IntPtr.Zero,
|
|
||||||
IntPtr.Zero,
|
|
||||||
IntPtr.Zero
|
|
||||||
);
|
|
||||||
|
|
||||||
if (Voice == IntPtr.Zero)
|
|
||||||
{
|
|
||||||
Logger.LogError("SoundInstance failed to initialize!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
InitDSPSettings(Format.nChannels);
|
|
||||||
|
|
||||||
State = SoundState.Stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Apply3D(AudioListener listener, AudioEmitter emitter)
|
|
||||||
{
|
|
||||||
Is3D = true;
|
|
||||||
|
|
||||||
emitter.emitterData.CurveDistanceScaler = Device.CurveDistanceScalar;
|
|
||||||
emitter.emitterData.ChannelCount = dspSettings.SrcChannelCount;
|
|
||||||
|
|
||||||
FAudio.F3DAudioCalculate(
|
|
||||||
Device.Handle3D,
|
|
||||||
ref listener.listenerData,
|
|
||||||
ref emitter.emitterData,
|
|
||||||
FAudio.F3DAUDIO_CALCULATE_MATRIX | FAudio.F3DAUDIO_CALCULATE_DOPPLER,
|
|
||||||
ref dspSettings
|
|
||||||
);
|
|
||||||
|
|
||||||
UpdatePitch();
|
|
||||||
FAudio.FAudioVoice_SetOutputMatrix(
|
|
||||||
Voice,
|
|
||||||
Device.MasteringVoice,
|
|
||||||
dspSettings.SrcChannelCount,
|
|
||||||
dspSettings.DstChannelCount,
|
|
||||||
dspSettings.pMatrixCoefficients,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe void ApplyReverb(ReverbEffect reverbEffect)
|
|
||||||
{
|
|
||||||
ReverbSends = new FAudio.FAudioVoiceSends();
|
|
||||||
ReverbSends.SendCount = 2;
|
|
||||||
ReverbSends.pSends = (nint) NativeMemory.Alloc((nuint) (2 * Marshal.SizeOf<FAudio.FAudioSendDescriptor>()));
|
|
||||||
|
|
||||||
FAudio.FAudioSendDescriptor* sendDesc = (FAudio.FAudioSendDescriptor*) ReverbSends.pSends;
|
|
||||||
sendDesc[0].Flags = 0;
|
|
||||||
sendDesc[0].pOutputVoice = Device.MasteringVoice;
|
|
||||||
sendDesc[1].Flags = 0;
|
|
||||||
sendDesc[1].pOutputVoice = reverbEffect.Voice;
|
|
||||||
|
|
||||||
FAudio.FAudioVoice_SetOutputVoices(
|
|
||||||
Voice,
|
|
||||||
ref ReverbSends
|
|
||||||
);
|
|
||||||
|
|
||||||
ReverbEffect = reverbEffect;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetPan(float targetValue)
|
|
||||||
{
|
|
||||||
Pan = targetValue;
|
|
||||||
Device.ClearTweens(this, AudioTweenProperty.Pan);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetPan(float targetValue, float duration, EasingFunction easingFunction)
|
|
||||||
{
|
|
||||||
Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetPan(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
|
||||||
{
|
|
||||||
Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, delayTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the pitch of the voice. Valid input range is -1f to 1f.
|
||||||
|
/// </summary>
|
||||||
public void SetPitch(float targetValue)
|
public void SetPitch(float targetValue)
|
||||||
{
|
{
|
||||||
Pitch = targetValue;
|
Pitch = targetValue;
|
||||||
Device.ClearTweens(this, AudioTweenProperty.Pitch);
|
Device.ClearTweens(this, AudioTweenProperty.Pitch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the pitch of the voice over a time duration in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
public void SetPitch(float targetValue, float duration, EasingFunction easingFunction)
|
public void SetPitch(float targetValue, float duration, EasingFunction easingFunction)
|
||||||
{
|
{
|
||||||
Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pan, targetValue, duration, 0);
|
Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pitch, targetValue, duration, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the pitch of the voice over a time duration in seconds after a delay in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
public void SetPitch(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
public void SetPitch(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
||||||
{
|
{
|
||||||
Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pan, targetValue, duration, delayTime);
|
Device.CreateTween(this, AudioTweenProperty.Pitch, easingFunction, Pitch, targetValue, duration, delayTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the volume of the voice. Minimum value is 0f.
|
||||||
|
/// </summary>
|
||||||
public void SetVolume(float targetValue)
|
public void SetVolume(float targetValue)
|
||||||
{
|
{
|
||||||
Volume = targetValue;
|
Volume = targetValue;
|
||||||
Device.ClearTweens(this, AudioTweenProperty.Volume);
|
Device.ClearTweens(this, AudioTweenProperty.Volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the volume of the voice over a time duration in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
public void SetVolume(float targetValue, float duration, EasingFunction easingFunction)
|
public void SetVolume(float targetValue, float duration, EasingFunction easingFunction)
|
||||||
{
|
{
|
||||||
Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, 0);
|
Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the volume of the voice over a time duration in seconds after a delay in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
public void SetVolume(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
public void SetVolume(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
||||||
{
|
{
|
||||||
Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, delayTime);
|
Device.CreateTween(this, AudioTweenProperty.Volume, easingFunction, Volume, targetValue, duration, delayTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the frequency cutoff on the voice filter. Valid range is 0.01f to 1f.
|
||||||
|
/// </summary>
|
||||||
public void SetFilterFrequency(float targetValue)
|
public void SetFilterFrequency(float targetValue)
|
||||||
{
|
{
|
||||||
FilterFrequency = targetValue;
|
FilterFrequency = targetValue;
|
||||||
Device.ClearTweens(this, AudioTweenProperty.FilterFrequency);
|
Device.ClearTweens(this, AudioTweenProperty.FilterFrequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the frequency cutoff on the voice filter over a time duration in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
public void SetFilterFrequency(float targetValue, float duration, EasingFunction easingFunction)
|
public void SetFilterFrequency(float targetValue, float duration, EasingFunction easingFunction)
|
||||||
{
|
{
|
||||||
Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, 0);
|
Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the frequency cutoff on the voice filter over a time duration in seconds after a delay in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
public void SetFilterFrequency(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
public void SetFilterFrequency(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
||||||
{
|
{
|
||||||
Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, delayTime);
|
Device.CreateTween(this, AudioTweenProperty.FilterFrequency, easingFunction, FilterFrequency, targetValue, duration, delayTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets reciprocal of Q factor on the frequency filter.
|
||||||
|
/// Controls how quickly frequencies beyond the filter frequency are dampened.
|
||||||
|
/// </summary>
|
||||||
public void SetFilterOneOverQ(float targetValue)
|
public void SetFilterOneOverQ(float targetValue)
|
||||||
{
|
{
|
||||||
FilterOneOverQ = targetValue;
|
FilterOneOverQ = targetValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetReverb(float targetValue)
|
/// <summary>
|
||||||
|
/// Sets a left-right panning value. -1f is hard left pan, 1f is hard right pan.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void SetPan(float targetValue)
|
||||||
|
{
|
||||||
|
Pan = targetValue;
|
||||||
|
Device.ClearTweens(this, AudioTweenProperty.Pan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a left-right panning value over a time duration in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public virtual void SetPan(float targetValue, float duration, EasingFunction easingFunction)
|
||||||
|
{
|
||||||
|
Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a left-right panning value over a time duration in seconds after a delay in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public virtual void SetPan(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
||||||
|
{
|
||||||
|
Device.CreateTween(this, AudioTweenProperty.Pan, easingFunction, Pan, targetValue, duration, delayTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the wet-dry mix value of the reverb effect. Minimum value is 0f.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void SetReverb(float targetValue)
|
||||||
{
|
{
|
||||||
Reverb = targetValue;
|
Reverb = targetValue;
|
||||||
Device.ClearTweens(this, AudioTweenProperty.Reverb);
|
Device.ClearTweens(this, AudioTweenProperty.Reverb);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetReverb(float targetValue, float duration, EasingFunction easingFunction)
|
/// <summary>
|
||||||
|
/// Sets the wet-dry mix value of the reverb effect over a time duration in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public virtual void SetReverb(float targetValue, float duration, EasingFunction easingFunction)
|
||||||
{
|
{
|
||||||
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, 0);
|
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetReverb(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
/// <summary>
|
||||||
|
/// Sets the wet-dry mix value of the reverb effect over a time duration in seconds after a delay in seconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="easingFunction">An easing function. See MoonWorks.Math.Easing.Function.Float</param>
|
||||||
|
public virtual void SetReverb(float targetValue, float delayTime, float duration, EasingFunction easingFunction)
|
||||||
{
|
{
|
||||||
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, delayTime);
|
Device.CreateTween(this, AudioTweenProperty.Reverb, easingFunction, Volume, targetValue, duration, delayTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void Play();
|
/// <summary>
|
||||||
public abstract void QueueSyncPlay();
|
/// Sets the output voice for this voice.
|
||||||
public abstract void Pause();
|
/// </summary>
|
||||||
public abstract void Stop();
|
/// <param name="send">Where the output should be sent.</param>
|
||||||
public abstract void StopImmediate();
|
public unsafe void SetOutputVoice(SubmixVoice send)
|
||||||
|
|
||||||
private unsafe void InitDSPSettings(uint srcChannels)
|
|
||||||
{
|
{
|
||||||
dspSettings = new FAudio.F3DAUDIO_DSP_SETTINGS();
|
OutputVoice = send;
|
||||||
dspSettings.DopplerFactor = 1f;
|
|
||||||
dspSettings.SrcChannelCount = srcChannels;
|
|
||||||
dspSettings.DstChannelCount = Device.DeviceDetails.OutputFormat.Format.nChannels;
|
|
||||||
|
|
||||||
nuint memsize = (
|
if (ReverbEffect != null)
|
||||||
4 *
|
|
||||||
dspSettings.SrcChannelCount *
|
|
||||||
dspSettings.DstChannelCount
|
|
||||||
);
|
|
||||||
|
|
||||||
dspSettings.pMatrixCoefficients = (nint) NativeMemory.Alloc(memsize);
|
|
||||||
byte* memPtr = (byte*) dspSettings.pMatrixCoefficients;
|
|
||||||
for (uint i = 0; i < memsize; i += 1)
|
|
||||||
{
|
{
|
||||||
memPtr[i] = 0;
|
SetReverbEffectChain(ReverbEffect);
|
||||||
}
|
|
||||||
|
|
||||||
SetPanMatrixCoefficients();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePitch()
|
|
||||||
{
|
|
||||||
float doppler;
|
|
||||||
float dopplerScale = Device.DopplerScale;
|
|
||||||
if (!Is3D || dopplerScale == 0.0f)
|
|
||||||
{
|
|
||||||
doppler = 1.0f;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
doppler = dspSettings.DopplerFactor * dopplerScale;
|
FAudio.FAudioSendDescriptor* sendDesc = stackalloc FAudio.FAudioSendDescriptor[1];
|
||||||
}
|
sendDesc[0].Flags = 0;
|
||||||
|
sendDesc[0].pOutputVoice = send.Handle;
|
||||||
|
|
||||||
FAudio.FAudioSourceVoice_SetFrequencyRatio(
|
var sends = new FAudio.FAudioVoiceSends();
|
||||||
Voice,
|
sends.SendCount = 1;
|
||||||
(float) System.Math.Pow(2.0, pitch) * doppler,
|
sends.pSends = (nint) sendDesc;
|
||||||
0
|
|
||||||
|
FAudio.FAudioVoice_SetOutputVoices(
|
||||||
|
Handle,
|
||||||
|
ref sends
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a reverb effect chain to this voice.
|
||||||
|
/// </summary>
|
||||||
|
public unsafe void SetReverbEffectChain(ReverbEffect reverbEffect)
|
||||||
|
{
|
||||||
|
var sendDesc = stackalloc FAudio.FAudioSendDescriptor[2];
|
||||||
|
sendDesc[0].Flags = 0;
|
||||||
|
sendDesc[0].pOutputVoice = OutputVoice.Handle;
|
||||||
|
sendDesc[1].Flags = 0;
|
||||||
|
sendDesc[1].pOutputVoice = reverbEffect.Handle;
|
||||||
|
|
||||||
|
var sends = new FAudio.FAudioVoiceSends();
|
||||||
|
sends.SendCount = 2;
|
||||||
|
sends.pSends = (nint) sendDesc;
|
||||||
|
|
||||||
|
FAudio.FAudioVoice_SetOutputVoices(
|
||||||
|
Handle,
|
||||||
|
ref sends
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ReverbEffect = reverbEffect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the reverb effect chain from this voice.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveReverbEffectChain()
|
||||||
|
{
|
||||||
|
if (ReverbEffect != null)
|
||||||
|
{
|
||||||
|
ReverbEffect = null;
|
||||||
|
reverb = 0;
|
||||||
|
SetOutputVoice(OutputVoice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets all voice parameters to defaults.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void Reset()
|
||||||
|
{
|
||||||
|
RemoveReverbEffectChain();
|
||||||
|
Volume = 1;
|
||||||
|
Pan = 0;
|
||||||
|
Pitch = 0;
|
||||||
|
FilterType = FilterType.None;
|
||||||
|
SetOutputVoice(Device.MasteringVoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Taken from https://github.com/FNA-XNA/FNA/blob/master/src/Audio/SoundEffectInstance.cs
|
// Taken from https://github.com/FNA-XNA/FNA/blob/master/src/Audio/SoundEffectInstance.cs
|
||||||
|
@ -449,10 +499,10 @@ namespace MoonWorks.Audio
|
||||||
* entire channel; the two channels are blended on each side.
|
* entire channel; the two channels are blended on each side.
|
||||||
* -flibit
|
* -flibit
|
||||||
*/
|
*/
|
||||||
float* outputMatrix = (float*) dspSettings.pMatrixCoefficients;
|
float* outputMatrix = (float*) pMatrixCoefficients;
|
||||||
if (dspSettings.SrcChannelCount == 1)
|
if (SourceChannelCount == 1)
|
||||||
{
|
{
|
||||||
if (dspSettings.DstChannelCount == 1)
|
if (DestinationChannelCount == 1)
|
||||||
{
|
{
|
||||||
outputMatrix[0] = 1.0f;
|
outputMatrix[0] = 1.0f;
|
||||||
}
|
}
|
||||||
|
@ -464,7 +514,7 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (dspSettings.DstChannelCount == 1)
|
if (DestinationChannelCount == 1)
|
||||||
{
|
{
|
||||||
outputMatrix[0] = 1.0f;
|
outputMatrix[0] = 1.0f;
|
||||||
outputMatrix[1] = 1.0f;
|
outputMatrix[1] = 1.0f;
|
||||||
|
@ -493,16 +543,30 @@ namespace MoonWorks.Audio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void UpdatePitch()
|
||||||
|
{
|
||||||
|
float doppler;
|
||||||
|
float dopplerScale = Device.DopplerScale;
|
||||||
|
if (!Is3D || dopplerScale == 0.0f)
|
||||||
|
{
|
||||||
|
doppler = 1.0f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
doppler = DopplerFactor * dopplerScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
FAudio.FAudioSourceVoice_SetFrequencyRatio(
|
||||||
|
Handle,
|
||||||
|
(float) System.Math.Pow(2.0, pitch) * doppler,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected unsafe override void Destroy()
|
protected unsafe override void Destroy()
|
||||||
{
|
{
|
||||||
StopImmediate();
|
NativeMemory.Free(pMatrixCoefficients);
|
||||||
FAudio.FAudioVoice_DestroyVoice(Voice);
|
FAudio.FAudioVoice_DestroyVoice(Handle);
|
||||||
NativeMemory.Free((void*) dspSettings.pMatrixCoefficients);
|
|
||||||
|
|
||||||
if (ReverbEffect != null)
|
|
||||||
{
|
|
||||||
NativeMemory.Free((void*) ReverbSends.pSends);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue