more audio implementation

main
cosmonaut 2021-01-19 19:26:30 -08:00
parent 768bf38e3f
commit 2b58edb257
6 changed files with 370 additions and 85 deletions

61
src/Audio/DynamicSound.cs Normal file
View File

@ -0,0 +1,61 @@
using System;
using System.IO;
namespace MoonWorks.Audio
{
/// <summary>
/// For streaming long playback.
/// </summary>
public class DynamicSound : Sound, IDisposable
{
public const int BUFFER_SIZE = 1024 * 128;
internal IntPtr FileHandle { get; }
internal FAudio.stb_vorbis_info Info { get; }
private bool IsDisposed;
// FIXME: what should this value be?
public DynamicSound(FileInfo fileInfo, ushort channels, uint samplesPerSecond) : base(channels, samplesPerSecond)
{
FileHandle = FAudio.stb_vorbis_open_filename(fileInfo.FullName, out var error, IntPtr.Zero);
if (error != 0)
{
Logger.LogError("Error opening OGG file!");
throw new AudioLoadException("Error opening OGG file!");
}
Info = FAudio.stb_vorbis_get_info(FileHandle);
}
protected virtual void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
// dispose managed state (managed objects)
}
FAudio.stb_vorbis_close(FileHandle);
IsDisposed = true;
}
}
// override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
~DynamicSound()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: false);
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
public class DynamicSoundInstance : SoundInstance
{
private List<IntPtr> queuedBuffers;
private List<uint> queuedSizes;
private const int MINIMUM_BUFFER_CHECK = 3;
public int PendingBufferCount => queuedBuffers.Count;
private readonly float[] buffer;
public override SoundState State { get; protected set; }
public DynamicSoundInstance(
AudioDevice device,
DynamicSound parent,
bool is3D
) : base(device, parent, is3D)
{
queuedBuffers = new List<IntPtr>();
queuedSizes = new List<uint>();
buffer = new float[DynamicSound.BUFFER_SIZE];
}
public void Play()
{
Update();
if (State == SoundState.Playing)
{
return;
}
QueueBuffers();
FAudio.FAudioSourceVoice_Start(Handle, 0, 0);
State = SoundState.Playing;
}
public void Pause()
{
if (State == SoundState.Playing)
{
FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
State = SoundState.Paused;
}
}
public void Stop()
{
FAudio.FAudioSourceVoice_Stop(Handle, 0, 0);
FAudio.FAudioSourceVoice_FlushSourceBuffers(Handle);
State = SoundState.Stopped;
ClearBuffers();
}
private void Update()
{
if (State != SoundState.Playing)
{
return;
}
FAudio.FAudioSourceVoice_GetState(
Handle,
out var state,
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
);
while (PendingBufferCount > state.BuffersQueued)
lock (queuedBuffers)
{
Marshal.FreeHGlobal(queuedBuffers[0]);
queuedBuffers.RemoveAt(0);
queuedSizes.RemoveAt(0);
}
}
private void QueueBuffers()
{
for (
int i = MINIMUM_BUFFER_CHECK - PendingBufferCount;
i > 0;
i -= 1
) {
AddBuffer();
}
}
private void ClearBuffers()
{
lock (queuedBuffers)
{
foreach (IntPtr buf in queuedBuffers)
{
Marshal.FreeHGlobal(buf);
}
queuedBuffers.Clear();
queuedBuffers.Clear();
}
}
private void AddBuffer()
{
var parent = (DynamicSound) Parent;
var samples = FAudio.stb_vorbis_get_samples_float_interleaved(
parent.FileHandle,
parent.Info.channels,
buffer,
buffer.Length
);
IntPtr next = Marshal.AllocHGlobal(buffer.Length);
Marshal.Copy(buffer, 0, next, buffer.Length);
lock (queuedBuffers)
{
var lengthInBytes = (uint) buffer.Length * sizeof(float);
queuedBuffers.Add(next);
if (State != SoundState.Stopped)
{
FAudio.FAudioBuffer buf = new FAudio.FAudioBuffer
{
AudioBytes = lengthInBytes,
pAudioData = next,
PlayLength = (
lengthInBytes /
(uint) parent.Info.channels /
(uint) (parent.Format.wBitsPerSample / 8)
)
};
FAudio.FAudioSourceVoice_SubmitSourceBuffer(
Handle,
ref buf,
IntPtr.Zero
);
}
else
{
queuedSizes.Add(lengthInBytes);
}
}
}
}
}

View File

@ -1,73 +1,26 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio namespace MoonWorks.Audio
{ {
public class Sound public abstract class Sound
{ {
internal FAudio.FAudioBuffer Handle; internal FAudio.FAudioWaveFormatEx Format { get; }
internal FAudio.FAudioWaveFormatEx Format;
public uint LoopStart { get; set; } = 0; /* NOTE: we only support float decoding! WAV sucks! */
public uint LoopLength { get; set; } = 0;
public static Sound FromFile(FileInfo fileInfo)
{
var filePointer = FAudio.stb_vorbis_open_filename(fileInfo.FullName, out var error, IntPtr.Zero);
if (error != 0)
{
throw new AudioLoadException("Error loading file!");
}
var info = FAudio.stb_vorbis_get_info(filePointer);
var bufferSize = (uint)(info.sample_rate * info.channels);
var buffer = new float[bufferSize];
var align = (ushort) (4 * info.channels);
FAudio.stb_vorbis_close(filePointer);
return new Sound(
buffer,
0,
(ushort) info.channels,
info.sample_rate,
align
);
}
/* we only support float decoding! WAV sucks! */
public Sound( public Sound(
float[] buffer,
uint bufferOffset,
ushort channels, ushort channels,
uint samplesPerSecond, uint samplesPerSecond
ushort blockAlign
) { ) {
var bufferLength = 4 * buffer.Length; var blockAlign = (ushort) (4 * channels);
Format = new FAudio.FAudioWaveFormatEx(); Format = new FAudio.FAudioWaveFormatEx
Format.wFormatTag = 3; {
Format.wBitsPerSample = 32; wFormatTag = 3,
Format.nChannels = channels; wBitsPerSample = 32,
Format.nBlockAlign = (ushort) (4 * Format.nChannels); nChannels = channels,
Format.nSamplesPerSec = samplesPerSecond; nBlockAlign = blockAlign,
Format.nAvgBytesPerSec = Format.nBlockAlign * Format.nSamplesPerSec; nSamplesPerSec = samplesPerSecond,
Format.nBlockAlign = blockAlign; nAvgBytesPerSec = blockAlign * samplesPerSecond,
Format.cbSize = 0; cbSize = 0
};
Handle = new FAudio.FAudioBuffer();
Handle.Flags = FAudio.FAUDIO_END_OF_STREAM;
Handle.pContext = IntPtr.Zero;
Handle.AudioBytes = (uint) bufferLength;
Handle.pAudioData = Marshal.AllocHGlobal((int) bufferLength);
Marshal.Copy(buffer, (int) bufferOffset, Handle.pAudioData, (int) bufferLength);
Handle.PlayBegin = 0;
Handle.PlayLength = (
Handle.AudioBytes /
(uint) Format.nChannels /
(uint) (Format.wBitsPerSample / 8)
);
} }
} }
} }

View File

@ -7,15 +7,15 @@ namespace MoonWorks.Audio
{ {
protected AudioDevice Device { get; } protected AudioDevice Device { get; }
internal IntPtr Handle { get; } internal IntPtr Handle { get; }
protected Sound Parent { get; } public Sound Parent { get; }
protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings; protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings;
public SoundState State { get; protected set; }
protected bool is3D; protected bool is3D;
private float _pan = 0;
private bool IsDisposed; private bool IsDisposed;
public abstract SoundState State { get; protected set; }
private float _pan = 0;
public float Pan public float Pan
{ {
get => _pan; get => _pan;
@ -168,8 +168,11 @@ namespace MoonWorks.Audio
} }
} }
public SoundInstance(AudioDevice device, Sound parent, bool is3D) public SoundInstance(
{ AudioDevice device,
Sound parent,
bool is3D
) {
Device = device; Device = device;
Parent = parent; Parent = parent;
@ -195,7 +198,6 @@ namespace MoonWorks.Audio
Handle = handle; Handle = handle;
this.is3D = is3D; this.is3D = is3D;
InitDSPSettings(Parent.Format.nChannels); InitDSPSettings(Parent.Format.nChannels);
} }
private void InitDSPSettings(uint srcChannels) private void InitDSPSettings(uint srcChannels)

87
src/Audio/StaticSound.cs Normal file
View File

@ -0,0 +1,87 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace MoonWorks.Audio
{
public class StaticSound : Sound, IDisposable
{
internal FAudio.FAudioBuffer Handle;
private bool IsDisposed;
public uint LoopStart { get; set; } = 0;
public uint LoopLength { get; set; } = 0;
public static StaticSound FromOgg(FileInfo fileInfo)
{
var filePointer = FAudio.stb_vorbis_open_filename(fileInfo.FullName, out var error, IntPtr.Zero);
if (error != 0)
{
throw new AudioLoadException("Error loading file!");
}
var info = FAudio.stb_vorbis_get_info(filePointer);
var bufferSize = (uint)(info.sample_rate * info.channels);
var buffer = new float[bufferSize];
FAudio.stb_vorbis_close(filePointer);
return new StaticSound(
buffer,
0,
(ushort) info.channels,
info.sample_rate
);
}
public StaticSound(
float[] buffer,
uint bufferOffset,
ushort channels,
uint samplesPerSecond
) : base(channels, samplesPerSecond) {
var bufferLength = 4 * buffer.Length;
Handle = new FAudio.FAudioBuffer();
Handle.Flags = FAudio.FAUDIO_END_OF_STREAM;
Handle.pContext = IntPtr.Zero;
Handle.AudioBytes = (uint) bufferLength;
Handle.pAudioData = Marshal.AllocHGlobal((int) bufferLength);
Marshal.Copy(buffer, (int) bufferOffset, Handle.pAudioData, (int) bufferLength);
Handle.PlayBegin = 0;
Handle.PlayLength = (
Handle.AudioBytes /
(uint) Format.nChannels /
(uint) (Format.wBitsPerSample / 8)
);
}
protected virtual void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
// dispose managed state (managed objects)
}
Marshal.FreeHGlobal(Handle.pAudioData);
IsDisposed = true;
}
}
// override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
~StaticSound()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: false);
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@ -4,39 +4,67 @@ namespace MoonWorks.Audio
{ {
public class StaticSoundInstance : SoundInstance public class StaticSoundInstance : SoundInstance
{ {
public bool Loop { get; protected set; } public bool Loop { get; }
private SoundState _state = SoundState.Stopped;
public override SoundState State
{
get
{
FAudio.FAudioSourceVoice_GetState(
Handle,
out var state,
FAudio.FAUDIO_VOICE_NOSAMPLESPLAYED
);
if (state.BuffersQueued == 0)
{
Stop(true);
}
return _state;
}
protected set
{
_state = value;
}
}
public StaticSoundInstance( public StaticSoundInstance(
AudioDevice device, AudioDevice device,
Sound parent, StaticSound parent,
bool is3D bool is3D,
) : base(device, parent, is3D) { } bool loop = false
) : base(device, parent, is3D)
public void Play(bool loop = false)
{ {
Loop = loop;
}
public void Play()
{
var parent = (StaticSound) Parent;
if (State == SoundState.Playing) if (State == SoundState.Playing)
{ {
return; return;
} }
if (loop) if (Loop)
{ {
Loop = true; parent.Handle.LoopCount = 255;
Parent.Handle.LoopCount = 255; parent.Handle.LoopBegin = parent.LoopStart;
Parent.Handle.LoopBegin = 0; parent.Handle.LoopLength = parent.LoopLength;
Parent.Handle.LoopLength = Parent.LoopLength;
} }
else else
{ {
Loop = false; parent.Handle.LoopCount = 0;
Parent.Handle.LoopCount = 0; parent.Handle.LoopBegin = 0;
Parent.Handle.LoopBegin = 0; parent.Handle.LoopLength = 0;
Parent.Handle.LoopLength = 0;
} }
FAudio.FAudioSourceVoice_SubmitSourceBuffer( FAudio.FAudioSourceVoice_SubmitSourceBuffer(
Handle, Handle,
ref Parent.Handle, ref parent.Handle,
IntPtr.Zero IntPtr.Zero
); );