2022-04-05 06:33:36 +00:00
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.IO;
|
2021-01-20 03:26:30 +00:00
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
|
|
|
namespace MoonWorks.Audio
|
|
|
|
{
|
2022-02-23 05:14:32 +00:00
|
|
|
public class StaticSound : AudioResource
|
|
|
|
{
|
|
|
|
internal FAudio.FAudioBuffer Handle;
|
2022-04-05 06:33:36 +00:00
|
|
|
public ushort FormatTag { get; }
|
|
|
|
public ushort BitsPerSample { get; }
|
2022-02-23 05:14:32 +00:00
|
|
|
public ushort Channels { get; }
|
|
|
|
public uint SamplesPerSecond { get; }
|
2022-04-05 06:33:36 +00:00
|
|
|
public ushort BlockAlign { get; }
|
2021-01-20 03:26:30 +00:00
|
|
|
|
2022-02-23 05:14:32 +00:00
|
|
|
public uint LoopStart { get; set; } = 0;
|
|
|
|
public uint LoopLength { get; set; } = 0;
|
2021-01-20 03:26:30 +00:00
|
|
|
|
2022-04-05 06:33:36 +00:00
|
|
|
private Stack<StaticSoundInstance> Instances = new Stack<StaticSoundInstance>();
|
|
|
|
|
2023-04-05 00:12:03 +00:00
|
|
|
private bool OwnsBuffer;
|
|
|
|
|
2023-04-05 18:07:16 +00:00
|
|
|
public static unsafe StaticSound LoadOgg(AudioDevice device, string filePath)
|
2022-02-23 05:14:32 +00:00
|
|
|
{
|
|
|
|
var filePointer = FAudio.stb_vorbis_open_filename(filePath, out var error, IntPtr.Zero);
|
2021-01-20 03:26:30 +00:00
|
|
|
|
2022-02-23 05:14:32 +00:00
|
|
|
if (error != 0)
|
|
|
|
{
|
|
|
|
throw new AudioLoadException("Error loading file!");
|
|
|
|
}
|
|
|
|
var info = FAudio.stb_vorbis_get_info(filePointer);
|
2023-04-05 18:07:16 +00:00
|
|
|
var lengthInFloats =
|
|
|
|
FAudio.stb_vorbis_stream_length_in_samples(filePointer) * info.channels;
|
|
|
|
var lengthInBytes = lengthInFloats * Marshal.SizeOf<float>();
|
|
|
|
var buffer = NativeMemory.Alloc((nuint) lengthInBytes);
|
2021-01-20 03:26:30 +00:00
|
|
|
|
2022-02-23 05:14:32 +00:00
|
|
|
FAudio.stb_vorbis_get_samples_float_interleaved(
|
|
|
|
filePointer,
|
|
|
|
info.channels,
|
2023-04-05 18:07:16 +00:00
|
|
|
(nint) buffer,
|
|
|
|
(int) lengthInFloats
|
2022-02-23 05:14:32 +00:00
|
|
|
);
|
2021-01-20 05:33:25 +00:00
|
|
|
|
2022-02-23 05:14:32 +00:00
|
|
|
FAudio.stb_vorbis_close(filePointer);
|
2021-01-20 03:26:30 +00:00
|
|
|
|
2022-02-23 05:14:32 +00:00
|
|
|
return new StaticSound(
|
|
|
|
device,
|
2023-04-05 18:07:16 +00:00
|
|
|
3,
|
|
|
|
32,
|
|
|
|
(ushort) (4 * info.channels),
|
2022-02-23 05:14:32 +00:00
|
|
|
(ushort) info.channels,
|
|
|
|
info.sample_rate,
|
2023-04-05 18:07:16 +00:00
|
|
|
(nint) buffer,
|
|
|
|
(uint) lengthInBytes,
|
|
|
|
true);
|
2022-02-23 05:14:32 +00:00
|
|
|
}
|
2021-01-20 03:26:30 +00:00
|
|
|
|
2022-04-05 06:33:36 +00:00
|
|
|
// mostly borrowed from https://github.com/FNA-XNA/FNA/blob/b71b4a35ae59970ff0070dea6f8620856d8d4fec/src/Audio/SoundEffect.cs#L385
|
2023-04-05 07:47:02 +00:00
|
|
|
public static unsafe StaticSound LoadWav(AudioDevice device, string filePath)
|
2022-04-05 06:33:36 +00:00
|
|
|
{
|
|
|
|
// WaveFormatEx data
|
|
|
|
ushort wFormatTag;
|
|
|
|
ushort nChannels;
|
|
|
|
uint nSamplesPerSec;
|
|
|
|
uint nAvgBytesPerSec;
|
|
|
|
ushort nBlockAlign;
|
|
|
|
ushort wBitsPerSample;
|
|
|
|
int samplerLoopStart = 0;
|
|
|
|
int samplerLoopEnd = 0;
|
|
|
|
|
2023-04-05 07:47:02 +00:00
|
|
|
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")
|
2022-04-05 06:33:36 +00:00
|
|
|
{
|
2023-04-05 07:47:02 +00:00
|
|
|
throw new NotSupportedException("Specified stream is not a wave file.");
|
|
|
|
}
|
2022-04-05 06:33:36 +00:00
|
|
|
|
2023-04-05 07:47:02 +00:00
|
|
|
reader.ReadUInt32(); // Riff Chunk Size
|
2022-04-05 06:33:36 +00:00
|
|
|
|
2023-04-05 07:47:02 +00:00
|
|
|
string wformat = new string(reader.ReadChars(4));
|
|
|
|
if (wformat != "WAVE")
|
|
|
|
{
|
|
|
|
throw new NotSupportedException("Specified stream is not a wave file.");
|
|
|
|
}
|
2022-04-05 06:33:36 +00:00
|
|
|
|
2023-04-05 07:47:02 +00:00
|
|
|
// 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));
|
|
|
|
}
|
2022-04-05 06:33:36 +00:00
|
|
|
|
2023-04-05 07:47:02 +00:00
|
|
|
int format_chunk_size = reader.ReadInt32();
|
2022-04-05 06:33:36 +00:00
|
|
|
|
2023-04-05 07:47:02 +00:00
|
|
|
wFormatTag = reader.ReadUInt16();
|
|
|
|
nChannels = reader.ReadUInt16();
|
|
|
|
nSamplesPerSec = reader.ReadUInt32();
|
|
|
|
nAvgBytesPerSec = reader.ReadUInt32();
|
|
|
|
nBlockAlign = reader.ReadUInt16();
|
|
|
|
wBitsPerSample = reader.ReadUInt16();
|
2022-04-05 06:33:36 +00:00
|
|
|
|
2023-04-05 07:47:02 +00:00
|
|
|
// 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);
|
2022-04-05 06:33:36 +00:00
|
|
|
|
2023-04-05 07:47:02 +00:00
|
|
|
// Scan for other chunks
|
|
|
|
while (reader.PeekChar() != -1)
|
|
|
|
{
|
|
|
|
char[] chunkIDChars = reader.ReadChars(4);
|
|
|
|
if (chunkIDChars.Length < 4)
|
2022-04-05 06:33:36 +00:00
|
|
|
{
|
2023-04-05 07:47:02 +00:00
|
|
|
break; // EOL!
|
2022-04-05 06:33:36 +00:00
|
|
|
}
|
2023-04-05 07:47:02 +00:00
|
|
|
byte[] chunkSizeBytes = reader.ReadBytes(4);
|
|
|
|
if (chunkSizeBytes.Length < 4)
|
2022-04-05 06:33:36 +00:00
|
|
|
{
|
2023-04-05 07:47:02 +00:00
|
|
|
break; // EOL!
|
2022-04-05 06:33:36 +00:00
|
|
|
}
|
2023-04-05 07:47:02 +00:00
|
|
|
string chunk_signature = new string(chunkIDChars);
|
|
|
|
int chunkDataSize = BitConverter.ToInt32(chunkSizeBytes, 0);
|
|
|
|
if (chunk_signature == "smpl") // "smpl", Sampler Chunk Found
|
2022-04-05 06:33:36 +00:00
|
|
|
{
|
2023-04-05 07:47:02 +00:00
|
|
|
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)
|
2022-04-05 06:33:36 +00:00
|
|
|
{
|
2023-04-05 07:47:02 +00:00
|
|
|
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
|
2022-04-05 06:33:36 +00:00
|
|
|
{
|
2023-04-05 07:47:02 +00:00
|
|
|
samplerLoopStart = start;
|
|
|
|
samplerLoopEnd = end;
|
2022-04-05 06:33:36 +00:00
|
|
|
}
|
|
|
|
}
|
2023-04-05 07:47:02 +00:00
|
|
|
|
|
|
|
if (samplerData != 0) // Read Sampler Data if it exists
|
2022-04-05 06:33:36 +00:00
|
|
|
{
|
2023-04-05 07:47:02 +00:00
|
|
|
reader.ReadBytes(samplerData);
|
2022-04-05 06:33:36 +00:00
|
|
|
}
|
|
|
|
}
|
2023-04-05 07:47:02 +00:00
|
|
|
else // Read unwanted chunk data and try again
|
|
|
|
{
|
|
|
|
reader.ReadBytes(chunkDataSize);
|
|
|
|
}
|
2022-04-05 06:33:36 +00:00
|
|
|
}
|
2023-04-05 07:47:02 +00:00
|
|
|
// End scan
|
2022-04-05 06:33:36 +00:00
|
|
|
|
2023-04-05 07:47:02 +00:00
|
|
|
var sound = new StaticSound(
|
2022-04-05 06:33:36 +00:00
|
|
|
device,
|
|
|
|
wFormatTag,
|
|
|
|
wBitsPerSample,
|
|
|
|
nBlockAlign,
|
|
|
|
nChannels,
|
|
|
|
nSamplesPerSec,
|
2023-04-05 07:47:02 +00:00
|
|
|
(nint) waveDataBuffer,
|
|
|
|
(uint) waveDataLength,
|
|
|
|
true
|
2022-04-05 06:33:36 +00:00
|
|
|
);
|
2023-04-05 07:47:02 +00:00
|
|
|
|
|
|
|
return sound;
|
2022-04-05 06:33:36 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 22:26:32 +00:00
|
|
|
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
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-04-05 07:47:02 +00:00
|
|
|
public StaticSound(
|
2022-04-05 06:33:36 +00:00
|
|
|
AudioDevice device,
|
|
|
|
ushort formatTag,
|
|
|
|
ushort bitsPerSample,
|
|
|
|
ushort blockAlign,
|
|
|
|
ushort channels,
|
|
|
|
uint samplesPerSecond,
|
2023-04-05 07:47:02 +00:00
|
|
|
IntPtr bufferPtr,
|
|
|
|
uint bufferLengthInBytes,
|
|
|
|
bool ownsBuffer) : base(device)
|
2022-04-05 06:33:36 +00:00
|
|
|
{
|
|
|
|
FormatTag = formatTag;
|
|
|
|
BitsPerSample = bitsPerSample;
|
|
|
|
BlockAlign = blockAlign;
|
|
|
|
Channels = channels;
|
|
|
|
SamplesPerSecond = samplesPerSecond;
|
|
|
|
|
2023-04-05 07:47:02 +00:00
|
|
|
Handle = new FAudio.FAudioBuffer
|
|
|
|
{
|
|
|
|
Flags = FAudio.FAUDIO_END_OF_STREAM,
|
|
|
|
pContext = IntPtr.Zero,
|
|
|
|
pAudioData = bufferPtr,
|
|
|
|
AudioBytes = bufferLengthInBytes,
|
|
|
|
PlayBegin = 0,
|
|
|
|
PlayLength = 0
|
|
|
|
};
|
2023-04-05 00:12:03 +00:00
|
|
|
|
2023-04-05 07:47:02 +00:00
|
|
|
OwnsBuffer = ownsBuffer;
|
2022-04-05 06:33:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets a sound instance from the pool.
|
|
|
|
/// NOTE: If you lose track of instances, you will create garbage collection pressure!
|
|
|
|
/// </summary>
|
2022-04-05 23:05:42 +00:00
|
|
|
public StaticSoundInstance GetInstance()
|
2022-04-05 06:33:36 +00:00
|
|
|
{
|
|
|
|
if (Instances.Count == 0)
|
|
|
|
{
|
2022-04-07 21:19:43 +00:00
|
|
|
Instances.Push(new StaticSoundInstance(Device, this));
|
2022-04-05 06:33:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return Instances.Pop();
|
|
|
|
}
|
|
|
|
|
|
|
|
internal void FreeInstance(StaticSoundInstance instance)
|
2022-02-23 05:14:32 +00:00
|
|
|
{
|
2022-08-30 05:45:28 +00:00
|
|
|
instance.Reset();
|
2022-04-05 06:33:36 +00:00
|
|
|
Instances.Push(instance);
|
2022-02-23 05:14:32 +00:00
|
|
|
}
|
2021-01-20 03:26:30 +00:00
|
|
|
|
2023-04-05 00:12:03 +00:00
|
|
|
protected override unsafe void Destroy()
|
2022-02-23 05:14:32 +00:00
|
|
|
{
|
2023-04-05 00:12:03 +00:00
|
|
|
if (OwnsBuffer)
|
|
|
|
{
|
|
|
|
NativeMemory.Free((void*) Handle.pAudioData);
|
|
|
|
}
|
2022-02-23 05:14:32 +00:00
|
|
|
}
|
|
|
|
}
|
2021-01-20 03:26:30 +00:00
|
|
|
}
|