Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
Caleb Cornett | a62d8a5383 |
|
@ -10,6 +10,6 @@
|
|||
[submodule "lib/WellspringCS"]
|
||||
path = lib/WellspringCS
|
||||
url = https://gitea.moonside.games/MoonsideGames/WellspringCS.git
|
||||
[submodule "lib/dav1dfile"]
|
||||
path = lib/dav1dfile
|
||||
url = https://github.com/MoonsideGames/dav1dfile.git
|
||||
[submodule "lib/Theorafile"]
|
||||
path = lib/Theorafile
|
||||
url = https://github.com/FNA-XNA/Theorafile.git
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Platforms>x64</Platforms>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<LangVersion>11</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
@ -14,29 +14,22 @@
|
|||
<Compile Include="lib\FAudio\csharp\FAudio.cs" />
|
||||
<Compile Include="lib\RefreshCS\src\Refresh.cs" />
|
||||
<Compile Include="lib\SDL2-CS\src\SDL2.cs" />
|
||||
<Compile Include="lib\Theorafile\csharp\Theorafile.cs" />
|
||||
<Compile Include="lib\WellspringCS\WellspringCS.cs" />
|
||||
<Compile Include="lib\dav1dfile\csharp\dav1dfile.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="MoonWorks.dll.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="src\Graphics\StockShaders\Binary\video_fullscreen.vert.refresh">
|
||||
<LogicalName>MoonWorks.Graphics.StockShaders.VideoFullscreen.vert.refresh</LogicalName>
|
||||
<EmbeddedResource Include="src\Video\Shaders\Compiled\FullscreenVert.spv">
|
||||
<LogicalName>MoonWorks.Shaders.FullscreenVert.spv</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="src\Graphics\StockShaders\Binary\video_yuv2rgba.frag.refresh">
|
||||
<LogicalName>MoonWorks.Graphics.StockShaders.VideoYUV2RGBA.frag.refresh</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="src\Graphics\StockShaders\Binary\text_transform.vert.refresh">
|
||||
<LogicalName>MoonWorks.Graphics.StockShaders.TextTransform.vert.refresh</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="src\Graphics\StockShaders\Binary\text_msdf.frag.refresh">
|
||||
<LogicalName>MoonWorks.Graphics.StockShaders.TextMSDF.frag.refresh</LogicalName>
|
||||
<EmbeddedResource Include="src\Video\Shaders\Compiled\YUV2RGBAFrag.spv">
|
||||
<LogicalName>MoonWorks.Shaders.YUV2RGBAFrag.spv</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -5,18 +5,18 @@
|
|||
<dllmap dll="SDL2" os="linux,freebsd,netbsd" target="libSDL2-2.0.so.0"/>
|
||||
|
||||
<dllmap dll="Refresh" os="windows" target="Refresh.dll"/>
|
||||
<dllmap dll="Refresh" os="osx" target="libRefresh.1.dylib"/>
|
||||
<dllmap dll="Refresh" os="linux,freebsd,netbsd" target="libRefresh.so.1"/>
|
||||
<dllmap dll="Refresh" os="osx" target="libRefresh.0.dylib"/>
|
||||
<dllmap dll="Refresh" os="linux,freebsd,netbsd" target="libRefresh.so.0"/>
|
||||
|
||||
<dllmap dll="FAudio" os="windows" target="FAudio.dll"/>
|
||||
<dllmap dll="FAudio" os="osx" target="libFAudio.0.dylib"/>
|
||||
<dllmap dll="FAudio" os="linux,freebsd,netbsd" target="libFAudio.so.0"/>
|
||||
|
||||
<dllmap dll="Wellspring" os="windows" target="Wellspring.dll"/>
|
||||
<dllmap dll="Wellspring" os="osx" target="libWellspring.1.dylib"/>
|
||||
<dllmap dll="Wellspring" os="linux,freebsd,netbsd" target="libWellspring.so.1"/>
|
||||
<dllmap dll="Wellspring" os="osx" target="libWellspring.0.dylib"/>
|
||||
<dllmap dll="Wellspring" os="linux,freebsd,netbsd" target="libWellspring.so.0"/>
|
||||
|
||||
<dllmap dll="dav1dfile" os="windows" target="dav1dfile.dll"/>
|
||||
<dllmap dll="dav1dfile" os="osx" target="libdav1dfile.1.dylib"/>
|
||||
<dllmap dll="dav1dfile" os="linux,freebsd,netbsd,openbsd" target="libdav1dfile.so.1"/>
|
||||
<dllmap dll="Theorafile" os="windows" target="libtheorafile.dll"/>
|
||||
<dllmap dll="Theorafile" os="osx" target="libtheorafile.dylib"/>
|
||||
<dllmap dll="Theorafile" os="linux,freebsd,netbsd" target="libtheorafile.so"/>
|
||||
</configuration>
|
||||
|
|
12
README.md
12
README.md
|
@ -12,13 +12,9 @@ MoonWorks uses strictly Free Open Source Software. It will never have any kind o
|
|||
|
||||
## Documentation
|
||||
|
||||
API Reference: https://moonside.games/docs/moonworksapi/
|
||||
High-level documentation is provided here: http://moonside.games/docs/moonworks/
|
||||
|
||||
High-level documentation is provided here: https://moonside.games/docs/moonworks/
|
||||
|
||||
The source is documented in doc comments that your preferred IDE can read.
|
||||
|
||||
Join our Discord! https://discord.gg/ujhwdkHmhN
|
||||
For an actual API reference, the source is documented in doc comments that your preferred IDE can read.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
@ -26,9 +22,9 @@ Join our Discord! https://discord.gg/ujhwdkHmhN
|
|||
* [Refresh](https://gitea.moonside.games/MoonsideGames/Refresh) - Graphics
|
||||
* [FAudio](https://github.com/FNA-XNA/FAudio) - Audio
|
||||
* [Wellspring](https://gitea.moonside.games/MoonsideGames/Wellspring) - Font Rendering
|
||||
* [dav1dfile](https://github.com/MoonsideGames/dav1dfile) - Compressed Video
|
||||
* [Theorafile](https://github.com/FNA-XNA/Theorafile) - Compressed Video
|
||||
|
||||
Prebuilt dependencies can be obtained here: https://moonside.games/files/moonlibs.tar.bz2
|
||||
Prebuilt dependencies can be obtained here: http://moonside.games/files/moonlibs.tar.bz2
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 60480416bda930bf7544e6abe31b937f0daa0256
|
||||
Subproject commit 0b6d5dabbf428633482fe3a956fbdb53228fcf35
|
|
@ -1 +1 @@
|
|||
Subproject commit b5325e6d0329eeb35b074091a569a5f679852d28
|
||||
Subproject commit 98c590ae77c3b6a64a370bac439f20728959a8b6
|
|
@ -1 +1 @@
|
|||
Subproject commit e4afbb848586fca530b6538320f799f81a18b941
|
||||
Subproject commit b35aaa494e44d08242788ff0ba2cb7a508f4d8f0
|
|
@ -0,0 +1 @@
|
|||
Subproject commit dd8c7fa69e678b6182cdaa71458ad08dd31c65da
|
|
@ -1 +1 @@
|
|||
Subproject commit 074f2afc833b221906bb2468735041ce78f2cb89
|
||||
Subproject commit f8872bae59e394b0f8a35224bb39ab8fd041af97
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 5065e2cd4662dbe023b77a45ef967f975170dfff
|
|
@ -1,79 +0,0 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MoonWorks.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains raw audio data in a specified Format. <br/>
|
||||
/// Submit this to a SourceVoice to play audio.
|
||||
/// </summary>
|
||||
public class AudioBuffer : AudioResource
|
||||
{
|
||||
IntPtr BufferDataPtr;
|
||||
uint BufferDataLength;
|
||||
private bool OwnsBufferData;
|
||||
|
||||
public Format Format { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new AudioBuffer.
|
||||
/// </summary>
|
||||
/// <param name="ownsBufferData">If true, the buffer data will be destroyed when this AudioBuffer is destroyed.</param>
|
||||
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 Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (OwnsBufferData)
|
||||
{
|
||||
NativeMemory.Free((void*) BufferDataPtr);
|
||||
}
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
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 InvalidOperationException("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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the Ogg data for streaming.
|
||||
/// </summary>
|
||||
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 InvalidOperationException("Error opening OGG file!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Seek(uint sampleFrame)
|
||||
{
|
||||
FAudio.stb_vorbis_seek(VorbisHandle, sampleFrame);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unloads the Ogg data, freeing resources.
|
||||
/// </summary>
|
||||
public override unsafe void Unload()
|
||||
{
|
||||
if (Loaded)
|
||||
{
|
||||
FAudio.stb_vorbis_close(VorbisHandle);
|
||||
NativeMemory.Free((void*) FileDataPtr);
|
||||
|
||||
VorbisHandle = IntPtr.Zero;
|
||||
FileDataPtr = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an entire ogg file into an AudioBuffer. Useful for static audio.
|
||||
/// </summary>
|
||||
public static unsafe AudioBuffer CreateBuffer(AudioDevice device, string filePath)
|
||||
{
|
||||
var filePointer = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero);
|
||||
|
||||
if (error != 0)
|
||||
{
|
||||
throw new InvalidOperationException("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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
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 InvalidOperationException("Specified file is not a QOA file.");
|
||||
}
|
||||
|
||||
uint totalSamplesPerChannel = (uint) (fileHeader & (0xFFFFFFFF));
|
||||
if (totalSamplesPerChannel == 0)
|
||||
{
|
||||
throw new InvalidOperationException("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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares qoa data for streaming.
|
||||
/// </summary>
|
||||
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 InvalidOperationException("Error opening QOA file!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Seek(uint sampleFrame)
|
||||
{
|
||||
FAudio.qoa_seek_frame(QoaHandle, (int) sampleFrame);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unloads the qoa data, freeing resources.
|
||||
/// </summary>
|
||||
public override unsafe void Unload()
|
||||
{
|
||||
if (Loaded)
|
||||
{
|
||||
FAudio.qoa_close(QoaHandle);
|
||||
NativeMemory.Free((void*) FileDataPtr);
|
||||
|
||||
QoaHandle = IntPtr.Zero;
|
||||
FileDataPtr = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the entire qoa file into an AudioBuffer. Useful for static audio.
|
||||
/// </summary>
|
||||
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 InvalidOperationException("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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
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 Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
Unload();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
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,54 +1,31 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace MoonWorks.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// AudioDevice manages all audio-related concerns.
|
||||
/// </summary>
|
||||
public class AudioDevice : IDisposable
|
||||
{
|
||||
public IntPtr Handle { get; }
|
||||
public byte[] Handle3D { get; }
|
||||
public IntPtr MasteringVoice { 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 IntPtr ReverbVoice { get; }
|
||||
|
||||
public float CurveDistanceScalar = 1f;
|
||||
public float DopplerScale = 1f;
|
||||
public float SpeedOfSound = 343.5f;
|
||||
|
||||
private readonly HashSet<GCHandle> resourceHandles = new HashSet<GCHandle>();
|
||||
private readonly HashSet<UpdatingSourceVoice> updatingSourceVoices = new HashSet<UpdatingSourceVoice>();
|
||||
internal FAudio.FAudioVoiceSends ReverbSends;
|
||||
|
||||
private AudioTweenManager AudioTweenManager;
|
||||
private readonly List<WeakReference<AudioResource>> resources = new List<WeakReference<AudioResource>>();
|
||||
private readonly List<WeakReference<StreamingSound>> streamingSounds = new List<WeakReference<StreamingSound>>();
|
||||
|
||||
private SourceVoicePool VoicePool;
|
||||
private List<SourceVoice> VoicesToReturn = new List<SourceVoice>();
|
||||
private bool IsDisposed;
|
||||
|
||||
private const int Step = 200;
|
||||
private TimeSpan UpdateInterval;
|
||||
private System.Diagnostics.Stopwatch TickStopwatch = new System.Diagnostics.Stopwatch();
|
||||
private long previousTickTime;
|
||||
private Thread Thread;
|
||||
private AutoResetEvent WakeSignal;
|
||||
internal readonly object StateLock = new object();
|
||||
|
||||
private bool Running;
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
internal unsafe AudioDevice()
|
||||
public unsafe AudioDevice()
|
||||
{
|
||||
UpdateInterval = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / Step);
|
||||
|
||||
FAudio.FAudioCreate(out var handle, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR);
|
||||
FAudio.FAudioCreate(out var handle, 0, 0);
|
||||
Handle = handle;
|
||||
|
||||
/* Find a suitable device */
|
||||
|
@ -58,8 +35,8 @@ namespace MoonWorks.Audio
|
|||
if (devices == 0)
|
||||
{
|
||||
Logger.LogError("No audio devices found!");
|
||||
FAudio.FAudio_Release(Handle);
|
||||
Handle = IntPtr.Zero;
|
||||
FAudio.FAudio_Release(Handle);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -92,24 +69,25 @@ namespace MoonWorks.Audio
|
|||
}
|
||||
|
||||
/* Init Mastering Voice */
|
||||
var result = FAudio.FAudio_CreateMasteringVoice(
|
||||
IntPtr masteringVoice;
|
||||
|
||||
if (FAudio.FAudio_CreateMasteringVoice(
|
||||
Handle,
|
||||
out trueMasteringVoice,
|
||||
out masteringVoice,
|
||||
FAudio.FAUDIO_DEFAULT_CHANNELS,
|
||||
FAudio.FAUDIO_DEFAULT_SAMPLERATE,
|
||||
0,
|
||||
i,
|
||||
IntPtr.Zero
|
||||
);
|
||||
|
||||
if (result != 0)
|
||||
) != 0)
|
||||
{
|
||||
Logger.LogError("Failed to create a mastering voice!");
|
||||
Logger.LogError("Audio device creation failed!");
|
||||
Logger.LogError("No mastering voice found!");
|
||||
Handle = IntPtr.Zero;
|
||||
FAudio.FAudio_Release(Handle);
|
||||
return;
|
||||
}
|
||||
|
||||
fauxMasteringVoice = SubmixVoice.CreateFauxMasteringVoice(this);
|
||||
MasteringVoice = masteringVoice;
|
||||
|
||||
/* Init 3D Audio */
|
||||
|
||||
|
@ -120,169 +98,144 @@ namespace MoonWorks.Audio
|
|||
Handle3D
|
||||
);
|
||||
|
||||
AudioTweenManager = new AudioTweenManager();
|
||||
VoicePool = new SourceVoicePool(this);
|
||||
/* Init reverb */
|
||||
|
||||
WakeSignal = new AutoResetEvent(true);
|
||||
IntPtr reverbVoice;
|
||||
|
||||
Thread = new Thread(ThreadMain);
|
||||
Thread.IsBackground = true;
|
||||
Thread.Start();
|
||||
IntPtr reverb;
|
||||
FAudio.FAudioCreateReverb(out reverb, 0);
|
||||
|
||||
Running = true;
|
||||
IntPtr chainPtr;
|
||||
chainPtr = Marshal.AllocHGlobal(
|
||||
sizeof(FAudio.FAudioEffectChain)
|
||||
);
|
||||
|
||||
TickStopwatch.Start();
|
||||
previousTickTime = 0;
|
||||
FAudio.FAudioEffectChain* reverbChain = (FAudio.FAudioEffectChain*) chainPtr;
|
||||
reverbChain->EffectCount = 1;
|
||||
reverbChain->pEffectDescriptors = Marshal.AllocHGlobal(
|
||||
sizeof(FAudio.FAudioEffectDescriptor)
|
||||
);
|
||||
|
||||
FAudio.FAudioEffectDescriptor* reverbDescriptor =
|
||||
(FAudio.FAudioEffectDescriptor*) reverbChain->pEffectDescriptors;
|
||||
|
||||
reverbDescriptor->InitialState = 1;
|
||||
reverbDescriptor->OutputChannels = (uint) (
|
||||
(DeviceDetails.OutputFormat.Format.nChannels == 6) ? 6 : 1
|
||||
);
|
||||
reverbDescriptor->pEffect = reverb;
|
||||
|
||||
FAudio.FAudio_CreateSubmixVoice(
|
||||
Handle,
|
||||
out reverbVoice,
|
||||
1, /* omnidirectional reverb */
|
||||
DeviceDetails.OutputFormat.Format.nSamplesPerSec,
|
||||
0,
|
||||
0,
|
||||
IntPtr.Zero,
|
||||
chainPtr
|
||||
);
|
||||
FAudio.FAPOBase_Release(reverb);
|
||||
|
||||
Marshal.FreeHGlobal(reverbChain->pEffectDescriptors);
|
||||
Marshal.FreeHGlobal(chainPtr);
|
||||
|
||||
ReverbVoice = reverbVoice;
|
||||
|
||||
/* Init reverb params */
|
||||
// Defaults based on FAUDIOFX_I3DL2_PRESET_GENERIC
|
||||
|
||||
IntPtr reverbParamsPtr = Marshal.AllocHGlobal(
|
||||
sizeof(FAudio.FAudioFXReverbParameters)
|
||||
);
|
||||
|
||||
FAudio.FAudioFXReverbParameters* reverbParams = (FAudio.FAudioFXReverbParameters*) reverbParamsPtr;
|
||||
reverbParams->WetDryMix = 100.0f;
|
||||
reverbParams->ReflectionsDelay = 7;
|
||||
reverbParams->ReverbDelay = 11;
|
||||
reverbParams->RearDelay = FAudio.FAUDIOFX_REVERB_DEFAULT_REAR_DELAY;
|
||||
reverbParams->PositionLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION;
|
||||
reverbParams->PositionRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION;
|
||||
reverbParams->PositionMatrixLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX;
|
||||
reverbParams->PositionMatrixRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX;
|
||||
reverbParams->EarlyDiffusion = 15;
|
||||
reverbParams->LateDiffusion = 15;
|
||||
reverbParams->LowEQGain = 8;
|
||||
reverbParams->LowEQCutoff = 4;
|
||||
reverbParams->HighEQGain = 8;
|
||||
reverbParams->HighEQCutoff = 6;
|
||||
reverbParams->RoomFilterFreq = 5000f;
|
||||
reverbParams->RoomFilterMain = -10f;
|
||||
reverbParams->RoomFilterHF = -1f;
|
||||
reverbParams->ReflectionsGain = -26.0200005f;
|
||||
reverbParams->ReverbGain = 10.0f;
|
||||
reverbParams->DecayTime = 1.49000001f;
|
||||
reverbParams->Density = 100.0f;
|
||||
reverbParams->RoomSize = FAudio.FAUDIOFX_REVERB_DEFAULT_ROOM_SIZE;
|
||||
FAudio.FAudioVoice_SetEffectParameters(
|
||||
ReverbVoice,
|
||||
0,
|
||||
reverbParamsPtr,
|
||||
(uint) sizeof(FAudio.FAudioFXReverbParameters),
|
||||
0
|
||||
);
|
||||
Marshal.FreeHGlobal(reverbParamsPtr);
|
||||
|
||||
/* Init reverb sends */
|
||||
|
||||
ReverbSends = new FAudio.FAudioVoiceSends
|
||||
{
|
||||
SendCount = 2,
|
||||
pSends = Marshal.AllocHGlobal(
|
||||
2 * sizeof(FAudio.FAudioSendDescriptor)
|
||||
)
|
||||
};
|
||||
FAudio.FAudioSendDescriptor* sendDesc = (FAudio.FAudioSendDescriptor*) ReverbSends.pSends;
|
||||
sendDesc[0].Flags = 0;
|
||||
sendDesc[0].pOutputVoice = MasteringVoice;
|
||||
sendDesc[1].Flags = 0;
|
||||
sendDesc[1].pOutputVoice = ReverbVoice;
|
||||
}
|
||||
|
||||
private void ThreadMain()
|
||||
public void SetMasteringVolume(float volume)
|
||||
{
|
||||
while (Running)
|
||||
FAudio.FAudioVoice_SetVolume(MasteringVoice, volume, 0);
|
||||
}
|
||||
|
||||
internal void Update()
|
||||
{
|
||||
for (var i = streamingSounds.Count - 1; i >= 0; i--)
|
||||
{
|
||||
lock (StateLock)
|
||||
var weakReference = streamingSounds[i];
|
||||
if (weakReference.TryGetTarget(out var streamingSound))
|
||||
{
|
||||
try
|
||||
{
|
||||
ThreadMainTick();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e.ToString());
|
||||
}
|
||||
streamingSound.Update();
|
||||
}
|
||||
|
||||
WakeSignal.WaitOne(UpdateInterval);
|
||||
}
|
||||
}
|
||||
|
||||
private void ThreadMainTick()
|
||||
{
|
||||
long tickDelta = TickStopwatch.Elapsed.Ticks - previousTickTime;
|
||||
previousTickTime = TickStopwatch.Elapsed.Ticks;
|
||||
float elapsedSeconds = (float) tickDelta / System.TimeSpan.TicksPerSecond;
|
||||
|
||||
AudioTweenManager.Update(elapsedSeconds);
|
||||
|
||||
foreach (var voice in updatingSourceVoices)
|
||||
{
|
||||
voice.Update();
|
||||
}
|
||||
|
||||
foreach (var voice in VoicesToReturn)
|
||||
{
|
||||
if (voice is UpdatingSourceVoice updatingSourceVoice)
|
||||
else
|
||||
{
|
||||
updatingSourceVoices.Remove(updatingSourceVoice);
|
||||
}
|
||||
|
||||
voice.Reset();
|
||||
VoicePool.Return(voice);
|
||||
}
|
||||
|
||||
VoicesToReturn.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers all pending operations with the given syncGroup value.
|
||||
/// </summary>
|
||||
public void TriggerSyncGroup(uint syncGroup)
|
||||
{
|
||||
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);
|
||||
|
||||
if (voice is UpdatingSourceVoice updatingSourceVoice)
|
||||
{
|
||||
updatingSourceVoices.Add(updatingSourceVoice);
|
||||
}
|
||||
|
||||
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(
|
||||
Voice voice,
|
||||
AudioTweenProperty property,
|
||||
System.Func<float, float> easingFunction,
|
||||
float start,
|
||||
float end,
|
||||
float duration,
|
||||
float delayTime
|
||||
) {
|
||||
lock (StateLock)
|
||||
{
|
||||
AudioTweenManager.CreateTween(
|
||||
voice,
|
||||
property,
|
||||
easingFunction,
|
||||
start,
|
||||
end,
|
||||
duration,
|
||||
delayTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
internal void ClearTweens(
|
||||
Voice voice,
|
||||
AudioTweenProperty property
|
||||
) {
|
||||
lock (StateLock)
|
||||
{
|
||||
AudioTweenManager.ClearTweens(voice, property);
|
||||
}
|
||||
}
|
||||
|
||||
internal void WakeThread()
|
||||
{
|
||||
WakeSignal.Set();
|
||||
}
|
||||
|
||||
internal void AddResourceReference(GCHandle resourceReference)
|
||||
{
|
||||
lock (StateLock)
|
||||
{
|
||||
resourceHandles.Add(resourceReference);
|
||||
|
||||
if (resourceReference.Target is UpdatingSourceVoice updatableVoice)
|
||||
{
|
||||
updatingSourceVoices.Add(updatableVoice);
|
||||
streamingSounds.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveResourceReference(GCHandle resourceReference)
|
||||
internal void AddDynamicSoundInstance(StreamingSound instance)
|
||||
{
|
||||
lock (StateLock)
|
||||
{
|
||||
resourceHandles.Remove(resourceReference);
|
||||
streamingSounds.Add(new WeakReference<StreamingSound>(instance));
|
||||
}
|
||||
|
||||
if (resourceReference.Target is UpdatingSourceVoice updatableVoice)
|
||||
{
|
||||
updatingSourceVoices.Remove(updatableVoice);
|
||||
}
|
||||
internal void AddResourceReference(WeakReference<AudioResource> resourceReference)
|
||||
{
|
||||
lock (resources)
|
||||
{
|
||||
resources.Add(resourceReference);
|
||||
}
|
||||
}
|
||||
|
||||
internal void RemoveResourceReference(WeakReference<AudioResource> resourceReference)
|
||||
{
|
||||
lock (resources)
|
||||
{
|
||||
resources.Remove(resourceReference);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -290,54 +243,29 @@ namespace MoonWorks.Audio
|
|||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
Running = false;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
Thread.Join();
|
||||
|
||||
// dispose all source voices first
|
||||
foreach (var handle in resourceHandles)
|
||||
for (var i = resources.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (handle.Target is SourceVoice voice)
|
||||
{
|
||||
voice.Dispose();
|
||||
}
|
||||
}
|
||||
var weakReference = resources[i];
|
||||
|
||||
// dispose all submix voices except the faux mastering voice
|
||||
foreach (var handle in resourceHandles)
|
||||
{
|
||||
if (handle.Target is SubmixVoice voice && voice != fauxMasteringVoice)
|
||||
{
|
||||
voice.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// dispose the faux mastering voice
|
||||
fauxMasteringVoice.Dispose();
|
||||
|
||||
// dispose the true mastering voice
|
||||
FAudio.FAudioVoice_DestroyVoice(trueMasteringVoice);
|
||||
|
||||
// destroy all other audio resources
|
||||
foreach (var handle in resourceHandles)
|
||||
{
|
||||
if (handle.Target is AudioResource resource)
|
||||
if (weakReference.TryGetTarget(out var resource))
|
||||
{
|
||||
resource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
resourceHandles.Clear();
|
||||
resources.Clear();
|
||||
}
|
||||
|
||||
FAudio.FAudioVoice_DestroyVoice(ReverbVoice);
|
||||
FAudio.FAudioVoice_DestroyVoice(MasteringVoice);
|
||||
FAudio.FAudio_Release(Handle);
|
||||
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
|
||||
~AudioDevice()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
|
|
|
@ -4,9 +4,6 @@ using MoonWorks.Math.Float;
|
|||
|
||||
namespace MoonWorks.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// An emitter for 3D spatial audio.
|
||||
/// </summary>
|
||||
public class AudioEmitter : AudioResource
|
||||
{
|
||||
internal FAudio.F3DAUDIO_EMITTER emitterData;
|
||||
|
@ -132,5 +129,7 @@ namespace MoonWorks.Audio
|
|||
emitterData.pReverbCurve = IntPtr.Zero;
|
||||
emitterData.CurveDistanceScaler = 1.0f;
|
||||
}
|
||||
|
||||
protected override void Destroy() { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,6 @@ using MoonWorks.Math.Float;
|
|||
|
||||
namespace MoonWorks.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// A listener for 3D spatial audio. Usually attached to a camera.
|
||||
/// </summary>
|
||||
public class AudioListener : AudioResource
|
||||
{
|
||||
internal FAudio.F3DAUDIO_LISTENER listenerData;
|
||||
|
@ -94,5 +91,7 @@ namespace MoonWorks.Audio
|
|||
/* Unexposed variables, defaults based on XNA behavior */
|
||||
listenerData.pCone = IntPtr.Zero;
|
||||
}
|
||||
|
||||
protected override void Destroy() { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MoonWorks.Audio
|
||||
{
|
||||
|
@ -9,24 +8,28 @@ namespace MoonWorks.Audio
|
|||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
private GCHandle SelfReference;
|
||||
private WeakReference<AudioResource> selfReference;
|
||||
|
||||
protected AudioResource(AudioDevice device)
|
||||
public AudioResource(AudioDevice device)
|
||||
{
|
||||
Device = device;
|
||||
|
||||
SelfReference = GCHandle.Alloc(this, GCHandleType.Weak);
|
||||
Device.AddResourceReference(SelfReference);
|
||||
selfReference = new WeakReference<AudioResource>(this);
|
||||
Device.AddResourceReference(selfReference);
|
||||
}
|
||||
|
||||
protected abstract void Destroy();
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
Destroy();
|
||||
|
||||
if (selfReference != null)
|
||||
{
|
||||
Device.RemoveResourceReference(SelfReference);
|
||||
SelfReference.Free();
|
||||
Device.RemoveResourceReference(selfReference);
|
||||
selfReference = null;
|
||||
}
|
||||
|
||||
IsDisposed = true;
|
||||
|
@ -35,12 +38,8 @@ namespace MoonWorks.Audio
|
|||
|
||||
~AudioResource()
|
||||
{
|
||||
#if DEBUG
|
||||
// If you see this log message, you leaked an audio resource without disposing it!
|
||||
// We can't clean it up for you because this can cause catastrophic issues.
|
||||
// You should really fix this when it happens.
|
||||
Logger.LogWarn($"A resource of type {GetType().Name} was not Disposed.");
|
||||
#endif
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using EasingFunction = System.Func<float, float>;
|
||||
|
||||
namespace MoonWorks.Audio
|
||||
{
|
||||
internal enum AudioTweenProperty
|
||||
{
|
||||
Pan,
|
||||
Pitch,
|
||||
Volume,
|
||||
FilterFrequency,
|
||||
Reverb
|
||||
}
|
||||
|
||||
internal class AudioTween
|
||||
{
|
||||
public Voice Voice;
|
||||
public AudioTweenProperty Property;
|
||||
public EasingFunction EasingFunction;
|
||||
public float Time;
|
||||
public float StartValue;
|
||||
public float EndValue;
|
||||
public float DelayTime;
|
||||
public float Duration;
|
||||
}
|
||||
|
||||
internal class AudioTweenPool
|
||||
{
|
||||
private Queue<AudioTween> Tweens = new Queue<AudioTween>(16);
|
||||
|
||||
public AudioTweenPool()
|
||||
{
|
||||
for (int i = 0; i < 16; i += 1)
|
||||
{
|
||||
Tweens.Enqueue(new AudioTween());
|
||||
}
|
||||
}
|
||||
|
||||
public AudioTween Obtain()
|
||||
{
|
||||
if (Tweens.Count > 0)
|
||||
{
|
||||
var tween = Tweens.Dequeue();
|
||||
return tween;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new AudioTween();
|
||||
}
|
||||
}
|
||||
|
||||
public void Free(AudioTween tween)
|
||||
{
|
||||
tween.Voice = null;
|
||||
Tweens.Enqueue(tween);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MoonWorks.Audio
|
||||
{
|
||||
internal class AudioTweenManager
|
||||
{
|
||||
private AudioTweenPool AudioTweenPool = new AudioTweenPool();
|
||||
private readonly Dictionary<(Voice, AudioTweenProperty), AudioTween> AudioTweens = new Dictionary<(Voice, AudioTweenProperty), AudioTween>();
|
||||
private readonly List<AudioTween> DelayedAudioTweens = new List<AudioTween>();
|
||||
|
||||
public void Update(float elapsedSeconds)
|
||||
{
|
||||
for (var i = DelayedAudioTweens.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var audioTween = DelayedAudioTweens[i];
|
||||
var voice = audioTween.Voice;
|
||||
|
||||
audioTween.Time += elapsedSeconds;
|
||||
|
||||
if (audioTween.Time >= audioTween.DelayTime)
|
||||
{
|
||||
// set the tween start value to the current value of the property
|
||||
switch (audioTween.Property)
|
||||
{
|
||||
case AudioTweenProperty.Pan:
|
||||
audioTween.StartValue = voice.Pan;
|
||||
break;
|
||||
|
||||
case AudioTweenProperty.Pitch:
|
||||
audioTween.StartValue = voice.Pitch;
|
||||
break;
|
||||
|
||||
case AudioTweenProperty.Volume:
|
||||
audioTween.StartValue = voice.Volume;
|
||||
break;
|
||||
|
||||
case AudioTweenProperty.FilterFrequency:
|
||||
audioTween.StartValue = voice.FilterFrequency;
|
||||
break;
|
||||
|
||||
case AudioTweenProperty.Reverb:
|
||||
audioTween.StartValue = voice.Reverb;
|
||||
break;
|
||||
}
|
||||
|
||||
audioTween.Time = 0;
|
||||
DelayedAudioTweens.RemoveAt(i);
|
||||
|
||||
AddTween(audioTween);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (key, audioTween) in AudioTweens)
|
||||
{
|
||||
bool finished = UpdateAudioTween(audioTween, elapsedSeconds);
|
||||
|
||||
if (finished)
|
||||
{
|
||||
AudioTweenPool.Free(audioTween);
|
||||
AudioTweens.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateTween(
|
||||
Voice voice,
|
||||
AudioTweenProperty property,
|
||||
System.Func<float, float> easingFunction,
|
||||
float start,
|
||||
float end,
|
||||
float duration,
|
||||
float delayTime
|
||||
) {
|
||||
var tween = AudioTweenPool.Obtain();
|
||||
tween.Voice = voice;
|
||||
tween.Property = property;
|
||||
tween.EasingFunction = easingFunction;
|
||||
tween.StartValue = start;
|
||||
tween.EndValue = end;
|
||||
tween.Duration = duration;
|
||||
tween.Time = 0;
|
||||
tween.DelayTime = delayTime;
|
||||
|
||||
if (delayTime == 0)
|
||||
{
|
||||
AddTween(tween);
|
||||
}
|
||||
else
|
||||
{
|
||||
DelayedAudioTweens.Add(tween);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearTweens(Voice voice, AudioTweenProperty property)
|
||||
{
|
||||
AudioTweens.Remove((voice, property));
|
||||
}
|
||||
|
||||
private void AddTween(
|
||||
AudioTween audioTween
|
||||
) {
|
||||
// if a tween with the same sound and property already exists, get rid of it
|
||||
if (AudioTweens.TryGetValue((audioTween.Voice, audioTween.Property), out var currentTween))
|
||||
{
|
||||
AudioTweenPool.Free(currentTween);
|
||||
}
|
||||
|
||||
AudioTweens[(audioTween.Voice, audioTween.Property)] = audioTween;
|
||||
}
|
||||
|
||||
private static bool UpdateAudioTween(AudioTween audioTween, float delta)
|
||||
{
|
||||
float value;
|
||||
audioTween.Time += delta;
|
||||
|
||||
var finished = audioTween.Time >= audioTween.Duration;
|
||||
if (finished)
|
||||
{
|
||||
value = audioTween.EndValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = MoonWorks.Math.Easing.Interp(
|
||||
audioTween.StartValue,
|
||||
audioTween.EndValue,
|
||||
audioTween.Time,
|
||||
audioTween.Duration,
|
||||
audioTween.EasingFunction
|
||||
);
|
||||
}
|
||||
|
||||
switch (audioTween.Property)
|
||||
{
|
||||
case AudioTweenProperty.Pan:
|
||||