Skip to content

Instantly share code, notes, and snippets.

@NtFreX
Created January 5, 2022 17:53
Show Gist options
  • Save NtFreX/ff4666df7c37e888cf828a47e75b40da to your computer and use it in GitHub Desktop.
Save NtFreX/ff4666df7c37e888cf828a47e75b40da to your computer and use it in GitHub Desktop.
using SDL2;
using System.Collections;
namespace NtFreX.BuildingBlocks
{
public class SdlAudioRenderer : IDisposable
{
private SDL.SDL_AudioSpec? loadedFormat = null;
private bool isOpen = false;
// keep this here so it is not garbage collected
private readonly SDL.SDL_AudioCallback callbackDelegate;
private readonly List<SdlAudioContext> audioContexts = new List<SdlAudioContext>();
public SdlAudioRenderer()
{
this.callbackDelegate = SDL_AudioCallback;
}
public void Dispose()
{
SDL.SDL_CloseAudio();
}
public SdlAudioContext PlayWav(string file, int volume = SDL.SDL_MIX_MAXVOLUME, bool loop = false)
{
if (SDL.SDL_Init(SDL.SDL_INIT_AUDIO) < 0)
throw new Exception("Couldn't initialize sdl");
if (SDL.SDL_LoadWAV(file, out var spec, out var bufferPtr, out var length) == IntPtr.Zero)
throw new Exception($"Couldn't load the wav file {file}");
var audioContext = new SdlAudioContext(bufferPtr, length)
{
Loop = loop,
Volume = volume
};
if (loadedFormat != null && !AreEqual(spec, loadedFormat.Value))
throw new Exception($"Can only play one audio format, loaded format = {loadedFormat}, audio format = {spec}");
if(!isOpen)
{
spec.callback = this.callbackDelegate;
spec.userdata = IntPtr.Zero;
if (SDL.SDL_OpenAudio(ref spec, IntPtr.Zero) < 0)
throw new Exception($"Couldn't open audio: {SDL.SDL_GetError()}");
loadedFormat = spec;
isOpen = true;
}
audioContexts.Add(audioContext);
SDL.SDL_PauseAudio(0);
return audioContext;
}
private bool AreEqual(SDL.SDL_AudioSpec first, SDL.SDL_AudioSpec second)
{
// https://wiki.libsdl.org/SDL_AudioFormat
var firstBits = new BitArray(first.format);
var secondBits = new BitArray(second.format);
var firstSampleRate = BitConverter.GetBytes(first.format)[0];
var secondSampleRate = BitConverter.GetBytes(second.format)[0];
return firstBits.Get(15) == secondBits.Get(15) /* is signed */ &&
firstBits.Get(12) == secondBits.Get(12) /* is big endian */ &&
firstBits.Get(8) == secondBits.Get(8) /* is float */ &&
firstSampleRate == secondSampleRate &&
first.channels == second.channels &&
first.freq == second.freq;
}
private unsafe void SDL_AudioCallback(IntPtr userdata, IntPtr stream, int len)
{
for(var i = 0; i < audioContexts.Count; i++)
{
if (audioContexts[i].RemainingLength == 0 || audioContexts[i].IsStopped)
{
if (audioContexts[i].Loop && !audioContexts[i].IsStopped)
{
audioContexts[i].Reset();
}
else
{
audioContexts[i].Dispose();
audioContexts.RemoveAt(i);
i--;
}
}
}
var buffer = new byte[len];
fixed (byte* ptr = buffer)
{
SDL.SDL_memcpy(stream, new IntPtr(ptr), new IntPtr(len));
}
if (audioContexts.Count == 0)
{
SDL.SDL_PauseAudio(1);
return;
}
foreach (var context in audioContexts.Where(x => !x.IsPaused))
{
var contextLen = len > context.RemainingLength ? context.RemainingLength : (uint) len;
SDL.SDL_MixAudioFormat(stream, context.AudioPtr, loadedFormat!.Value.format, contextLen, context.Volume);
context.AudioPtr += (int) contextLen;
context.RemainingLength -= contextLen;
}
}
}
public class SdlAudioContext : IDisposable
{
private readonly IntPtr audioStartPtr;
private readonly uint audioLength;
internal IntPtr AudioPtr { get; set; }
internal uint RemainingLength { get; set; }
public bool Loop { get; set; }
public int Volume { get; set; } = SDL.SDL_MIX_MAXVOLUME;
public bool IsPaused { get; set; }
public bool IsStopped { get; private set; }
internal SdlAudioContext(IntPtr audioPtr, uint audioLength)
{
this.audioStartPtr = audioPtr;
this.AudioPtr = audioPtr;
this.audioLength = audioLength;
this.RemainingLength = audioLength;
}
public void Reset()
{
AudioPtr = audioStartPtr;
RemainingLength = audioLength;
}
public void Dispose()
{
IsStopped = true;
if (!IsStopped)
{
SDL.SDL_FreeWAV(AudioPtr);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment