refactor sound API to support generic decoding

main
cosmonaut 2021-01-20 12:39:31 -08:00
parent 2d5d70106f
commit 41636e923c
8 changed files with 230 additions and 191 deletions

View File

@ -18,7 +18,7 @@ namespace MoonWorks.Audio
internal FAudio.FAudioVoiceSends ReverbSends; internal FAudio.FAudioVoiceSends ReverbSends;
private readonly List<WeakReference<DynamicSoundInstance>> dynamicSoundInstances = new List<WeakReference<DynamicSoundInstance>>(); private readonly List<WeakReference<StreamingSound>> streamingSounds = new List<WeakReference<StreamingSound>>();
public unsafe AudioDevice() public unsafe AudioDevice()
{ {
@ -196,23 +196,23 @@ namespace MoonWorks.Audio
public void Update() public void Update()
{ {
for (var i = dynamicSoundInstances.Count - 1; i >= 0; i--) for (var i = streamingSounds.Count - 1; i >= 0; i--)
{ {
var weakReference = dynamicSoundInstances[i]; var weakReference = streamingSounds[i];
if (weakReference.TryGetTarget(out var dynamicSoundInstance)) if (weakReference.TryGetTarget(out var streamingSound))
{ {
dynamicSoundInstance.Update(); streamingSound.Update();
} }
else else
{ {
dynamicSoundInstances.RemoveAt(i); streamingSounds.RemoveAt(i);
} }
} }
} }
internal void AddDynamicSoundInstance(DynamicSoundInstance instance) internal void AddDynamicSoundInstance(StreamingSound instance)
{ {
dynamicSoundInstances.Add(new WeakReference<DynamicSoundInstance>(instance)); streamingSounds.Add(new WeakReference<StreamingSound>(instance));
} }
} }
} }

View File

@ -1,82 +0,0 @@
using System;
using System.IO;
namespace MoonWorks.Audio
{
/// <summary>
/// For streaming long playback. Reads an OGG file.
/// </summary>
public class DynamicSound : Sound, IDisposable
{
internal override FAudio.FAudioWaveFormatEx Format { get; }
// FIXME: what should this value be?
public const int BUFFER_SIZE = 1024 * 128;
internal IntPtr FileHandle { get; }
internal FAudio.stb_vorbis_info Info { get; }
private bool IsDisposed;
public DynamicSound(AudioDevice device, FileInfo fileInfo) : base(device)
{
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);
var blockAlign = (ushort)(4 * Info.channels);
Format = new FAudio.FAudioWaveFormatEx
{
wFormatTag = 3,
wBitsPerSample = 32,
nChannels = (ushort) Info.channels,
nBlockAlign = blockAlign,
nSamplesPerSec = Info.sample_rate,
nAvgBytesPerSec = blockAlign * Info.sample_rate,
cbSize = 0
};
}
public DynamicSoundInstance CreateInstance(bool loop = false)
{
var instance = new DynamicSoundInstance(Device, this, false, loop);
Device.AddDynamicSoundInstance(instance);
return instance;
}
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

@ -1,13 +0,0 @@
namespace MoonWorks.Audio
{
public abstract class Sound
{
internal AudioDevice Device { get; }
internal abstract FAudio.FAudioWaveFormatEx Format { get; }
public Sound(AudioDevice device)
{
Device = device;
}
}
}

View File

@ -7,7 +7,7 @@ namespace MoonWorks.Audio
{ {
protected AudioDevice Device { get; } protected AudioDevice Device { get; }
internal IntPtr Handle { get; } internal IntPtr Handle { get; }
public Sound Parent { get; } internal FAudio.FAudioWaveFormatEx Format { get; }
public bool Loop { get; } public bool Loop { get; }
protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings; protected FAudio.F3DAUDIO_DSP_SETTINGS dspSettings;
@ -118,10 +118,12 @@ namespace MoonWorks.Audio
{ {
_lowPassFilter = value; _lowPassFilter = value;
FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters(); FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters
p.Type = FAudio.FAudioFilterType.FAudioLowPassFilter; {
p.Frequency = _lowPassFilter; Type = FAudio.FAudioFilterType.FAudioLowPassFilter,
p.OneOverQ = 1f; Frequency = _lowPassFilter,
OneOverQ = 1f
};
FAudio.FAudioVoice_SetFilterParameters( FAudio.FAudioVoice_SetFilterParameters(
Handle, Handle,
ref p, ref p,
@ -138,10 +140,12 @@ namespace MoonWorks.Audio
{ {
_highPassFilter = value; _highPassFilter = value;
FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters(); FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters
p.Type = FAudio.FAudioFilterType.FAudioHighPassFilter; {
p.Frequency = _highPassFilter; Type = FAudio.FAudioFilterType.FAudioHighPassFilter,
p.OneOverQ = 1f; Frequency = _highPassFilter,
OneOverQ = 1f
};
FAudio.FAudioVoice_SetFilterParameters( FAudio.FAudioVoice_SetFilterParameters(
Handle, Handle,
ref p, ref p,
@ -158,10 +162,12 @@ namespace MoonWorks.Audio
{ {
_bandPassFilter = value; _bandPassFilter = value;
FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters(); FAudio.FAudioFilterParameters p = new FAudio.FAudioFilterParameters
p.Type = FAudio.FAudioFilterType.FAudioBandPassFilter; {
p.Frequency = _bandPassFilter; Type = FAudio.FAudioFilterType.FAudioBandPassFilter,
p.OneOverQ = 1f; Frequency = _bandPassFilter,
OneOverQ = 1f
};
FAudio.FAudioVoice_SetFilterParameters( FAudio.FAudioVoice_SetFilterParameters(
Handle, Handle,
ref p, ref p,
@ -172,14 +178,25 @@ namespace MoonWorks.Audio
public SoundInstance( public SoundInstance(
AudioDevice device, AudioDevice device,
Sound parent, ushort channels,
uint samplesPerSecond,
bool is3D, bool is3D,
bool loop bool loop
) { ) {
Device = device; Device = device;
Parent = parent;
FAudio.FAudioWaveFormatEx format = Parent.Format; var blockAlign = (ushort)(4 * channels);
var format = new FAudio.FAudioWaveFormatEx
{
wFormatTag = 3,
wBitsPerSample = 32,
nChannels = channels,
nBlockAlign = blockAlign,
nSamplesPerSec = samplesPerSecond,
nAvgBytesPerSec = blockAlign * samplesPerSecond
};
Format = format;
FAudio.FAudio_CreateSourceVoice( FAudio.FAudio_CreateSourceVoice(
Device.Handle, Device.Handle,
@ -200,7 +217,7 @@ namespace MoonWorks.Audio
Handle = handle; Handle = handle;
this.is3D = is3D; this.is3D = is3D;
InitDSPSettings(Parent.Format.nChannels); InitDSPSettings(Format.nChannels);
FAudio.FAudioVoice_SetOutputVoices( FAudio.FAudioVoice_SetOutputVoices(
handle, handle,
@ -208,6 +225,7 @@ namespace MoonWorks.Audio
); );
Loop = loop; Loop = loop;
State = SoundState.Stopped;
} }
private void InitDSPSettings(uint srcChannels) private void InitDSPSettings(uint srcChannels)

View File

@ -4,10 +4,12 @@ using System.Runtime.InteropServices;
namespace MoonWorks.Audio namespace MoonWorks.Audio
{ {
public class StaticSound : Sound, IDisposable public class StaticSound : IDisposable
{ {
internal override FAudio.FAudioWaveFormatEx Format { get; } internal AudioDevice Device { get; }
internal FAudio.FAudioBuffer Handle; internal FAudio.FAudioBuffer Handle;
public ushort Channels { get; }
public uint SamplesPerSecond { get; }
public uint LoopStart { get; set; } = 0; public uint LoopStart { get; set; } = 0;
public uint LoopLength { get; set; } = 0; public uint LoopLength { get; set; } = 0;
@ -37,33 +39,26 @@ namespace MoonWorks.Audio
return new StaticSound( return new StaticSound(
device, device,
(ushort) info.channels,
info.sample_rate,
buffer, buffer,
0, 0,
(uint) buffer.Length, (uint) buffer.Length
(ushort) info.channels,
info.sample_rate
); );
} }
public StaticSound( public StaticSound(
AudioDevice device, AudioDevice device,
ushort channels,
uint samplesPerSecond,
float[] buffer, float[] buffer,
uint bufferOffset, /* in floats */ uint bufferOffset, /* in floats */
uint bufferLength, /* in floats */ uint bufferLength /* in floats */
ushort channels, ) {
uint samplesPerSecond Device = device;
) : base(device) {
var blockAlign = (ushort)(4 * channels);
Format = new FAudio.FAudioWaveFormatEx Channels = channels;
{ SamplesPerSecond = samplesPerSecond;
wFormatTag = 3,
wBitsPerSample = 32,
nChannels = channels,
nBlockAlign = blockAlign,
nSamplesPerSec = samplesPerSecond,
nAvgBytesPerSec = blockAlign * samplesPerSecond
};
var bufferLengthInBytes = (int) (bufferLength * sizeof(float)); var bufferLengthInBytes = (int) (bufferLength * sizeof(float));
Handle = new FAudio.FAudioBuffer(); Handle = new FAudio.FAudioBuffer();

View File

@ -4,6 +4,8 @@ namespace MoonWorks.Audio
{ {
public class StaticSoundInstance : SoundInstance public class StaticSoundInstance : SoundInstance
{ {
public StaticSound Parent { get; }
private SoundState _state = SoundState.Stopped; private SoundState _state = SoundState.Stopped;
public override SoundState State public override SoundState State
{ {
@ -33,15 +35,13 @@ namespace MoonWorks.Audio
StaticSound parent, StaticSound parent,
bool is3D, bool is3D,
bool loop bool loop
) : base(device, parent, is3D, loop) ) : base(device, parent.Channels, parent.SamplesPerSecond, is3D, loop)
{ {
State = SoundState.Stopped; Parent = parent;
} }
public void Play() public void Play()
{ {
var parent = (StaticSound) Parent;
if (State == SoundState.Playing) if (State == SoundState.Playing)
{ {
return; return;
@ -49,20 +49,20 @@ namespace MoonWorks.Audio
if (Loop) if (Loop)
{ {
parent.Handle.LoopCount = 255; Parent.Handle.LoopCount = 255;
parent.Handle.LoopBegin = parent.LoopStart; Parent.Handle.LoopBegin = Parent.LoopStart;
parent.Handle.LoopLength = parent.LoopLength; Parent.Handle.LoopLength = Parent.LoopLength;
} }
else else
{ {
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
); );

View File

@ -4,32 +4,26 @@ using System.Runtime.InteropServices;
namespace MoonWorks.Audio namespace MoonWorks.Audio
{ {
public class DynamicSoundInstance : SoundInstance /// <summary>
/// For streaming long playback. Reads an OGG file.
/// </summary>
public abstract class StreamingSound : SoundInstance
{ {
private List<IntPtr> queuedBuffers; private readonly List<IntPtr> queuedBuffers = new List<IntPtr>();
private List<uint> queuedSizes; private readonly List<uint> queuedSizes = new List<uint>();
private const int MINIMUM_BUFFER_CHECK = 3; private const int MINIMUM_BUFFER_CHECK = 3;
public int PendingBufferCount => queuedBuffers.Count; public int PendingBufferCount => queuedBuffers.Count;
private readonly float[] buffer; private bool IsDisposed;
public override SoundState State { get; protected set; } public StreamingSound(
internal DynamicSoundInstance(
AudioDevice device, AudioDevice device,
DynamicSound parent, ushort channels,
uint samplesPerSecond,
bool is3D, bool is3D,
bool loop bool loop
) : base(device, parent, is3D, loop) ) : base(device, channels, samplesPerSecond, is3D, loop) { }
{
queuedBuffers = new List<IntPtr>();
queuedSizes = new List<uint>();
buffer = new float[DynamicSound.BUFFER_SIZE];
State = SoundState.Stopped;
}
public void Play() public void Play()
{ {
@ -78,27 +72,28 @@ namespace MoonWorks.Audio
); );
while (PendingBufferCount > state.BuffersQueued) while (PendingBufferCount > state.BuffersQueued)
lock (queuedBuffers) lock (queuedBuffers)
{ {
Marshal.FreeHGlobal(queuedBuffers[0]); Marshal.FreeHGlobal(queuedBuffers[0]);
queuedBuffers.RemoveAt(0); queuedBuffers.RemoveAt(0);
} }
QueueBuffers(); QueueBuffers();
} }
private void QueueBuffers() protected void QueueBuffers()
{ {
for ( for (
int i = MINIMUM_BUFFER_CHECK - PendingBufferCount; int i = MINIMUM_BUFFER_CHECK - PendingBufferCount;
i > 0; i > 0;
i -= 1 i -= 1
) { )
{
AddBuffer(); AddBuffer();
} }
} }
private void ClearBuffers() protected void ClearBuffers()
{ {
lock (queuedBuffers) lock (queuedBuffers)
{ {
@ -111,23 +106,19 @@ namespace MoonWorks.Audio
} }
} }
private void AddBuffer() protected void AddBuffer()
{ {
var parent = (DynamicSound) Parent; AddBuffer(
out var buffer,
/* NOTE: this function returns samples per channel, not total samples */ out var bufferOffset,
var samples = FAudio.stb_vorbis_get_samples_float_interleaved( out var bufferLength,
parent.FileHandle, out var reachedEnd
parent.Info.channels,
buffer,
buffer.Length
); );
var sampleCount = samples * parent.Info.channels; var lengthInBytes = bufferLength * sizeof(float);
var lengthInBytes = (uint) sampleCount * sizeof(float);
IntPtr next = Marshal.AllocHGlobal((int) lengthInBytes); IntPtr next = Marshal.AllocHGlobal((int) lengthInBytes);
Marshal.Copy(buffer, 0, next, sampleCount); Marshal.Copy(buffer, (int) bufferOffset, next, (int) bufferLength);
lock (queuedBuffers) lock (queuedBuffers)
{ {
@ -140,8 +131,8 @@ namespace MoonWorks.Audio
pAudioData = next, pAudioData = next,
PlayLength = ( PlayLength = (
lengthInBytes / lengthInBytes /
(uint) parent.Info.channels / Format.nChannels /
(uint) (parent.Format.wBitsPerSample / 8) (uint)(Format.wBitsPerSample / 8)
) )
}; };
@ -158,11 +149,11 @@ namespace MoonWorks.Audio
} }
/* We have reached the end of the file, what do we do? */ /* We have reached the end of the file, what do we do? */
if (sampleCount < buffer.Length) if (reachedEnd)
{ {
if (Loop) if (Loop)
{ {
FAudio.stb_vorbis_seek_start(parent.FileHandle); SeekStart();
} }
else else
{ {
@ -170,5 +161,31 @@ namespace MoonWorks.Audio
} }
} }
} }
protected abstract void AddBuffer(
out float[] buffer,
out uint bufferOffset, /* in floats */
out uint bufferLength, /* in floats */
out bool reachedEnd
);
protected abstract void SeekStart();
protected override void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
// dispose managed state (managed objects)
Stop(true);
}
// dispose unmanaged state
IsDisposed = true;
}
base.Dispose(disposing);
}
} }
} }

104
StreamingSoundOgg.cs Normal file
View File

@ -0,0 +1,104 @@
using System;
using System.IO;
namespace MoonWorks.Audio
{
public class StreamingSoundOgg : StreamingSound
{
// FIXME: what should this value be?
public const int BUFFER_SIZE = 1024 * 128;
internal IntPtr FileHandle { get; }
internal FAudio.stb_vorbis_info Info { get; }
private readonly float[] buffer;
public override SoundState State { get; protected set; }
private bool IsDisposed;
public static StreamingSoundOgg Load(
AudioDevice device,
FileInfo fileInfo,
bool is3D = false,
bool loop = false
) {
var 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!");
}
var info = FAudio.stb_vorbis_get_info(fileHandle);
return new StreamingSoundOgg(
device,
fileHandle,
info,
is3D,
loop
);
}
internal StreamingSoundOgg(
AudioDevice device,
IntPtr fileHandle,
FAudio.stb_vorbis_info info,
bool is3D,
bool loop
) : base(device, (ushort) info.channels, info.sample_rate, is3D, loop)
{
FileHandle = fileHandle;
Info = info;
buffer = new float[BUFFER_SIZE];
device.AddDynamicSoundInstance(this);
}
protected override void AddBuffer(
out float[] buffer,
out uint bufferOffset,
out uint bufferLength,
out bool reachedEnd
) {
buffer = this.buffer;
/* NOTE: this function returns samples per channel, not total samples */
var samples = FAudio.stb_vorbis_get_samples_float_interleaved(
FileHandle,
Info.channels,
buffer,
buffer.Length
);
var sampleCount = samples * Info.channels;
bufferOffset = 0;
bufferLength = (uint) sampleCount;
reachedEnd = sampleCount < buffer.Length;
}
protected override void SeekStart()
{
FAudio.stb_vorbis_seek_start(FileHandle);
}
protected override void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
// dispose managed state (managed objects)
}
// dispose unmanaged state
FAudio.stb_vorbis_close(FileHandle);
IsDisposed = true;
}
base.Dispose(disposing);
}
}
}