From d704a54aa1b329efbecdfa0a5aed2150ffdc0ed6 Mon Sep 17 00:00:00 2001 From: Neel R <51024479+nrbyte@users.noreply.github.com> Date: Sat, 9 May 2026 00:42:54 +0100 Subject: [PATCH 01/20] Audio setup with WAV file loading OpenAL code to load PCM data and play it back, along with basic WAV file loader --- src/rasdaq/Application.cs | 5 + src/rasdaq/Audio/Audio.cs | 23 +++++ src/rasdaq/Audio/AudioManager.cs | 143 ++++++++++++++++++++++++++ src/rasdaq/Audio/AudioSource.cs | 23 +++++ src/rasdaq/Audio/WAVLoader.cs | 167 +++++++++++++++++++++++++++++++ 5 files changed, 361 insertions(+) create mode 100644 src/rasdaq/Audio/Audio.cs create mode 100644 src/rasdaq/Audio/AudioManager.cs create mode 100644 src/rasdaq/Audio/AudioSource.cs create mode 100644 src/rasdaq/Audio/WAVLoader.cs diff --git a/src/rasdaq/Application.cs b/src/rasdaq/Application.cs index 59a4c51..98705c8 100644 --- a/src/rasdaq/Application.cs +++ b/src/rasdaq/Application.cs @@ -1,6 +1,8 @@ using OpenTK.Graphics.OpenGL4; using OpenTK.Windowing.Common; using OpenTK.Windowing.Desktop; + +using rasdaq.Audio; using rasdaq.Core.ECS; using rasdaq.Graphics; using rasdaq.Inputs; @@ -22,6 +24,8 @@ public sealed class Application : IDisposable public static Application? Instance { get; private set; } internal InputManager InputManager { get; set; } + public AudioManager AudioManager { get; private set; } + private List _worlds = new(); private GameWindow _gameWindow; @@ -57,6 +61,7 @@ public Application(int width, int height, string title) _gameWindow.FramebufferResize += OnFramebufferResize; InputManager = new InputManager(new GameWindowWrapper(_gameWindow)); + AudioManager = new AudioManager(); } internal void RegisterWorld(World world) diff --git a/src/rasdaq/Audio/Audio.cs b/src/rasdaq/Audio/Audio.cs new file mode 100644 index 0000000..95b0179 --- /dev/null +++ b/src/rasdaq/Audio/Audio.cs @@ -0,0 +1,23 @@ +using OpenTK.Audio.OpenAL; + +namespace rasdaq.Audio; + +public class Audio : IDisposable +{ + public int Handle; + public Audio() + { + Handle = AL.GenBuffer(); + // Check for errors + ALError error = AL.GetError(); + if (error != ALError.NoError) + { + throw new Exception("OpenAL error while trying to create audio buffer: " + error); + } + } + + public void Dispose() + { + AL.DeleteBuffer(Handle); + } +} diff --git a/src/rasdaq/Audio/AudioManager.cs b/src/rasdaq/Audio/AudioManager.cs new file mode 100644 index 0000000..e2011cb --- /dev/null +++ b/src/rasdaq/Audio/AudioManager.cs @@ -0,0 +1,143 @@ +using OpenTK.Audio.OpenAL; + +namespace rasdaq.Audio; + +public class AudioManager +{ + private static void CheckALCError(ALDevice device) + { + AlcError error = ALC.GetError(device); + if (error != AlcError.NoError) + { + throw new Exception("OpenAL error: " + error); + } + } + + private static void CheckALError() + { + ALError error = AL.GetError(); + if (error != ALError.NoError) + { + throw new Exception("OpenAL error: " + error); + } + } + + /// + /// Initialize Audio for the Application, using the default audio device. + /// + /// + public AudioManager() + { + // Open the default audio device. + ALDevice device = ALC.OpenDevice(null); + if (device == ALDevice.Null) + { + throw new Exception("Failed to open the default audio device."); + } + + // Create a context for the device. + ALContext context = ALC.CreateContext(device, new ALContextAttributes(null, null, null, null, null)); + if (context == ALContext.Null) + { + ALC.CloseDevice(device); + throw new Exception("Failed to create an OpenAL context."); + } + + // Check if we have any errors. + AlcError error = ALC.GetError(device); + CheckALCError(device); + // If no errors, make the context current. + ALC.MakeContextCurrent(context); + } + + /// + /// Load a WAV file that contains PCM data only. + /// + /// Path to WAV file + /// A integer handle referring to the loaded audio + /// + public Audio LoadAudio(string path) + { + // Create an OpenAL buffer for the data to go in. + Audio audio = new Audio(); + + // Load the WAV file + WAVLoader.WAVData wavData = WAVLoader.LoadWav(path); + + // Discern the format of the audio data based on number of channels and bits per sample. + ALFormat format; + if (wavData.Format.NumChannels == 1) + { + if (wavData.Format.BitsPerSample == 8) + { + format = ALFormat.Mono8; + } + else if (wavData.Format.BitsPerSample == 16) + { + format = ALFormat.Mono16; + } + else + { + throw new Exception("Unsupported bits per sample: " + wavData.Format.BitsPerSample); + } + } + else if (wavData.Format.NumChannels == 2) + { + if (wavData.Format.BitsPerSample == 8) + { + format = ALFormat.Stereo8; + } + else if (wavData.Format.BitsPerSample == 16) + { + format = ALFormat.Stereo16; + } + else + { + throw new Exception("Unsupported bits per sample: " + wavData.Format.BitsPerSample); + } + } + else + { + throw new Exception("Unsupported number of channels: " + wavData.Format.NumChannels); + } + + // Load the audio data into the OpenAL buffer. + AL.BufferData(audio.Handle, format, wavData.Data.Data, (int)wavData.Format.SampleRate); + + // Check if we have any errors from trying to load the audio data. + CheckALError(); + + return audio; + } + + /// + /// Add a loaded audio file to an audio source, so that it can be played. + /// + /// Integer handle of loaded audio file + /// Integer handle of audio source + public void AttachAudioToSource(Audio audio, AudioSource audioSource) + { + AL.Source(audio.Handle, ALSourcei.Buffer, audioSource.Handle); + CheckALError(); + } + + /// + /// Play back an audio source that has audio attached to it. + /// + /// + public void PlaySource(AudioSource audioSource) + { + AL.SourcePlay(audioSource.Handle); + CheckALError(); + } + + /// + /// Stop playback of an audio source. + /// + /// + public void StopSource(AudioSource audioSource) + { + AL.SourceStop(audioSource.Handle); + CheckALError(); + } +} \ No newline at end of file diff --git a/src/rasdaq/Audio/AudioSource.cs b/src/rasdaq/Audio/AudioSource.cs new file mode 100644 index 0000000..2064905 --- /dev/null +++ b/src/rasdaq/Audio/AudioSource.cs @@ -0,0 +1,23 @@ +using OpenTK.Audio.OpenAL; + +namespace rasdaq.Audio; + +public class AudioSource : IDisposable +{ + public int Handle; + public AudioSource() + { + Handle = AL.GenSource(); + // Check for errors + ALError error = AL.GetError(); + if (error != ALError.NoError) + { + throw new Exception("OpenAL error while trying to create audio source: " + error); + } + } + + public void Dispose() + { + AL.DeleteSource(Handle); + } +} diff --git a/src/rasdaq/Audio/WAVLoader.cs b/src/rasdaq/Audio/WAVLoader.cs new file mode 100644 index 0000000..d2e5f8a --- /dev/null +++ b/src/rasdaq/Audio/WAVLoader.cs @@ -0,0 +1,167 @@ +using System.Buffers.Binary; +using System.Text; + +namespace rasdaq.Audio; + +internal class WAVLoader +{ + /// + /// Format information about the WAV file. + /// + public struct FmtChunk + { + public ushort AudioFormat; + public ushort NumChannels; + public uint SampleRate; + public uint ByteRate; + public ushort BlockAlign; + public ushort BitsPerSample; + public FmtChunkExtension? Extension; + }; + /// + /// Extra format information for certain WAV files. + /// + public struct FmtChunkExtension + { + public ushort ValidBitsPerSample; + public uint SpeakerPositionMask; + public Guid SubFormat; + } + + /// + /// Holds the data of the WAV file, should be PCM data, as that is all is supported currently. + /// + public struct DataChunk + { + public uint Size; + public byte[] Data; + } + + /// + /// Holds all the information necessary about a WAV file, namely, + /// the format information and the data itself. + /// + public struct WAVData + { + public FmtChunk Format; + public DataChunk Data; + } + + private static byte[] ReadBytesFromStream(FileStream stream, int count) + { + byte[] buffer = new byte[count]; + stream.ReadExactly(buffer, 0, count); + return buffer; + } + + private static FmtChunk ProcessFmtChunck(FileStream stream) + { + uint size = BinaryPrimitives.ReadUInt32LittleEndian(ReadBytesFromStream(stream, 4)); + + FmtChunk chunk = new FmtChunk(); + chunk.AudioFormat = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytesFromStream(stream, 2)); + chunk.NumChannels = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytesFromStream(stream, 2)); + chunk.SampleRate = BinaryPrimitives.ReadUInt32LittleEndian(ReadBytesFromStream(stream, 4)); + chunk.ByteRate = BinaryPrimitives.ReadUInt32LittleEndian(ReadBytesFromStream(stream, 4)); + chunk.BlockAlign = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytesFromStream(stream, 2)); + chunk.BitsPerSample = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytesFromStream(stream, 2)); + + if (size > 16) + { + ushort extensionSize = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytesFromStream(stream, 2)); + if (extensionSize == 22) + { + chunk.Extension = new FmtChunkExtension() + { + ValidBitsPerSample = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytesFromStream(stream, 2)), + SpeakerPositionMask = BinaryPrimitives.ReadUInt32LittleEndian(ReadBytesFromStream(stream, 4)), + SubFormat = new Guid(ReadBytesFromStream(stream, 16)) + }; + } + } + + return chunk; + } + + private static DataChunk ProcessDataChunk(FileStream stream) + { + DataChunk chunk = new DataChunk(); + chunk.Size = BinaryPrimitives.ReadUInt32LittleEndian(ReadBytesFromStream(stream, 4)); + chunk.Data = ReadBytesFromStream(stream, (int)chunk.Size); + return chunk; + } + + /// + /// Load the WAV file at the given path and return a WAVData struct containing the format information and the data itself. + /// + /// The path to the WAV file. + /// A WAVData struct containing the format information and the data. + /// Thrown if the WAV file is not found or is invalid. + public static WAVData LoadWav(string path) + { + if (!File.Exists(path)) + { + throw new Exception("WAV file not found: " + path); + } + + using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + // Read the RIFF header at the start of the WAV file. + byte[] fourcc = new byte[4]; + stream.ReadExactly(fourcc, 0, 4); + if (Encoding.ASCII.GetString(fourcc) != "RIFF") + { + throw new Exception("Invalid WAV file: missing RIFF header."); + } + + // Read the file size + byte[] byteFileSize = new byte[4]; + stream.ReadExactly(byteFileSize, 0, 4); + int fileSize = BinaryPrimitives.ReadInt32LittleEndian(byteFileSize) - 4; + + // Check file type is WAVE + byte[] byteFileType = new byte[4]; + stream.ReadExactly(byteFileType, 0, 4); + Encoding.UTF8.GetString(byteFileType); + if (Encoding.ASCII.GetString(byteFileType) != "WAVE") + { + throw new Exception("Invalid WAV file: file type not listed as WAVE in RIFF header."); + } + + FmtChunk fmtChunk = new FmtChunk(); + DataChunk dataChunk = new DataChunk(); + + // Now read the remaining chunks, we want a fmt chunk and a data chunk. + while (stream.Position < stream.Length - 4) + { + byte[] chunkId = new byte[4]; + stream.ReadExactly(chunkId, 0, 4); + + if (chunkId.SequenceEqual(Encoding.ASCII.GetBytes("fmt "))) + { + fmtChunk = ProcessFmtChunck(stream); + + if (fmtChunk.AudioFormat != 1) + { + throw new Exception("Unsupported WAV file: only PCM format is supported."); + } + } + else if (chunkId.SequenceEqual(Encoding.ASCII.GetBytes("data"))) + { + dataChunk = ProcessDataChunk(stream); + break; + } + else + { + throw new Exception("Unsupported WAV file: unknown chunk type " + Encoding.ASCII.GetString(chunkId)); + } + } + + return new WAVData() + { + Format = fmtChunk, + Data = dataChunk + }; + } + } +} From 1bb66802ee4b42e8b92d7ae755f8f31240e189fe Mon Sep 17 00:00:00 2001 From: Neel R <51024479+nrbyte@users.noreply.github.com> Date: Wed, 13 May 2026 23:25:56 +0100 Subject: [PATCH 02/20] Log errors too --- src/rasdaq/Audio/Audio.cs | 1 + src/rasdaq/Audio/AudioManager.cs | 36 +++++++++++++++++++++++--------- src/rasdaq/Audio/AudioSource.cs | 6 ++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/rasdaq/Audio/Audio.cs b/src/rasdaq/Audio/Audio.cs index 95b0179..6ef2feb 100644 --- a/src/rasdaq/Audio/Audio.cs +++ b/src/rasdaq/Audio/Audio.cs @@ -5,6 +5,7 @@ namespace rasdaq.Audio; public class Audio : IDisposable { public int Handle; + public Audio() { Handle = AL.GenBuffer(); diff --git a/src/rasdaq/Audio/AudioManager.cs b/src/rasdaq/Audio/AudioManager.cs index e2011cb..34c6251 100644 --- a/src/rasdaq/Audio/AudioManager.cs +++ b/src/rasdaq/Audio/AudioManager.cs @@ -1,5 +1,7 @@ using OpenTK.Audio.OpenAL; +using rasdaq.Logging; + namespace rasdaq.Audio; public class AudioManager @@ -9,7 +11,9 @@ private static void CheckALCError(ALDevice device) AlcError error = ALC.GetError(device); if (error != AlcError.NoError) { - throw new Exception("OpenAL error: " + error); + string err = $"OpenAL error: {error}"; + Log.Error(err) + throw new Exception(err); } } @@ -18,7 +22,9 @@ private static void CheckALError() ALError error = AL.GetError(); if (error != ALError.NoError) { - throw new Exception("OpenAL error: " + error); + string err = $"OpenAL error: {error}"; + Log.Error(err); + throw new Exception(err); } } @@ -32,7 +38,9 @@ public AudioManager() ALDevice device = ALC.OpenDevice(null); if (device == ALDevice.Null) { - throw new Exception("Failed to open the default audio device."); + string err = "Failed to open the default audio device."; + Log.Error(err); + throw new Exception(err); } // Create a context for the device. @@ -40,7 +48,9 @@ public AudioManager() if (context == ALContext.Null) { ALC.CloseDevice(device); - throw new Exception("Failed to create an OpenAL context."); + string err = "Failed to create an OpenAL context."; + Log.Error(err); + throw new Exception(err); } // Check if we have any errors. @@ -78,7 +88,9 @@ public Audio LoadAudio(string path) } else { - throw new Exception("Unsupported bits per sample: " + wavData.Format.BitsPerSample); + string err = $"Unsupported bits per sample: {wavData.Format.BitsPerSample}"; + Log.Error(err); + throw new Exception(err); } } else if (wavData.Format.NumChannels == 2) @@ -93,12 +105,16 @@ public Audio LoadAudio(string path) } else { - throw new Exception("Unsupported bits per sample: " + wavData.Format.BitsPerSample); + string err = $"Unsupported bits per sample: {wavData.Format.BitsPerSample}"; + Log.Error(err); + throw new Exception(err); } } else { - throw new Exception("Unsupported number of channels: " + wavData.Format.NumChannels); + string err = $"Unsupported number of channels: {wavData.Format.NumChannels}"; + Log.Error(err); + throw new Exception(err); } // Load the audio data into the OpenAL buffer. @@ -113,11 +129,11 @@ public Audio LoadAudio(string path) /// /// Add a loaded audio file to an audio source, so that it can be played. /// - /// Integer handle of loaded audio file - /// Integer handle of audio source + /// Audio file + /// Audio source public void AttachAudioToSource(Audio audio, AudioSource audioSource) { - AL.Source(audio.Handle, ALSourcei.Buffer, audioSource.Handle); + AL.Source(audioSource.Handle, ALSourcei.Buffer, audio.Handle); CheckALError(); } diff --git a/src/rasdaq/Audio/AudioSource.cs b/src/rasdaq/Audio/AudioSource.cs index 2064905..09cf8ba 100644 --- a/src/rasdaq/Audio/AudioSource.cs +++ b/src/rasdaq/Audio/AudioSource.cs @@ -1,10 +1,13 @@ using OpenTK.Audio.OpenAL; +using rasdaq.Logging; + namespace rasdaq.Audio; public class AudioSource : IDisposable { public int Handle; + public AudioSource() { Handle = AL.GenSource(); @@ -12,6 +15,9 @@ public AudioSource() ALError error = AL.GetError(); if (error != ALError.NoError) { + string err = $"OpenAL error while trying to create audio source: {error}"; + + Log.Error(err); throw new Exception("OpenAL error while trying to create audio source: " + error); } } From 083702b4135b22192f9fc83707cead89bfacc497 Mon Sep 17 00:00:00 2001 From: Neel R <51024479+nrbyte@users.noreply.github.com> Date: Wed, 13 May 2026 23:31:37 +0100 Subject: [PATCH 03/20] Ran formatter --- src/rasdaq/Audio/Audio.cs | 2 +- src/rasdaq/Audio/AudioSource.cs | 2 +- src/rasdaq/Audio/WAVLoader.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rasdaq/Audio/Audio.cs b/src/rasdaq/Audio/Audio.cs index 6ef2feb..890dd4f 100644 --- a/src/rasdaq/Audio/Audio.cs +++ b/src/rasdaq/Audio/Audio.cs @@ -21,4 +21,4 @@ public void Dispose() { AL.DeleteBuffer(Handle); } -} +} \ No newline at end of file diff --git a/src/rasdaq/Audio/AudioSource.cs b/src/rasdaq/Audio/AudioSource.cs index 09cf8ba..b94f547 100644 --- a/src/rasdaq/Audio/AudioSource.cs +++ b/src/rasdaq/Audio/AudioSource.cs @@ -26,4 +26,4 @@ public void Dispose() { AL.DeleteSource(Handle); } -} +} \ No newline at end of file diff --git a/src/rasdaq/Audio/WAVLoader.cs b/src/rasdaq/Audio/WAVLoader.cs index d2e5f8a..508643e 100644 --- a/src/rasdaq/Audio/WAVLoader.cs +++ b/src/rasdaq/Audio/WAVLoader.cs @@ -164,4 +164,4 @@ public static WAVData LoadWav(string path) }; } } -} +} \ No newline at end of file From b705c2bb5773aafb030580d34056c57d5efd31bf Mon Sep 17 00:00:00 2001 From: Neel R <51024479+nrbyte@users.noreply.github.com> Date: Wed, 13 May 2026 23:34:09 +0100 Subject: [PATCH 04/20] Log error --- src/rasdaq/Audio/Audio.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/rasdaq/Audio/Audio.cs b/src/rasdaq/Audio/Audio.cs index 890dd4f..d912484 100644 --- a/src/rasdaq/Audio/Audio.cs +++ b/src/rasdaq/Audio/Audio.cs @@ -1,5 +1,7 @@ using OpenTK.Audio.OpenAL; +using rasdaq.Logging; + namespace rasdaq.Audio; public class Audio : IDisposable @@ -13,7 +15,10 @@ public Audio() ALError error = AL.GetError(); if (error != ALError.NoError) { - throw new Exception("OpenAL error while trying to create audio buffer: " + error); + string err = $"OpenAL error while trying to create audio buffer: {error}"; + + Log.Error(err); + throw new Exception(err); } } From 12cbba652a33eaabcf7aca573a1895c35dc5e182 Mon Sep 17 00:00:00 2001 From: Neel R <51024479+nrbyte@users.noreply.github.com> Date: Wed, 13 May 2026 23:35:09 +0100 Subject: [PATCH 05/20] Fix syntax error --- src/rasdaq/Audio/AudioManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rasdaq/Audio/AudioManager.cs b/src/rasdaq/Audio/AudioManager.cs index 34c6251..aff771d 100644 --- a/src/rasdaq/Audio/AudioManager.cs +++ b/src/rasdaq/Audio/AudioManager.cs @@ -12,7 +12,7 @@ private static void CheckALCError(ALDevice device) if (error != AlcError.NoError) { string err = $"OpenAL error: {error}"; - Log.Error(err) + Log.Error(err); throw new Exception(err); } } From ed72e69718b459e8ab8af852aa15e4347189cec5 Mon Sep 17 00:00:00 2001 From: Neel R <51024479+nrbyte@users.noreply.github.com> Date: Sat, 9 May 2026 00:42:54 +0100 Subject: [PATCH 06/20] Audio setup with WAV file loading OpenAL code to load PCM data and play it back, along with basic WAV file loader --- src/rasdaq/Application.cs | 9 +- src/rasdaq/Audio/Audio.cs | 23 +++++ src/rasdaq/Audio/AudioManager.cs | 143 ++++++++++++++++++++++++++ src/rasdaq/Audio/AudioSource.cs | 23 +++++ src/rasdaq/Audio/WAVLoader.cs | 167 +++++++++++++++++++++++++++++++ 5 files changed, 364 insertions(+), 1 deletion(-) create mode 100644 src/rasdaq/Audio/Audio.cs create mode 100644 src/rasdaq/Audio/AudioManager.cs create mode 100644 src/rasdaq/Audio/AudioSource.cs create mode 100644 src/rasdaq/Audio/WAVLoader.cs diff --git a/src/rasdaq/Application.cs b/src/rasdaq/Application.cs index fb04ec6..8137cb2 100644 --- a/src/rasdaq/Application.cs +++ b/src/rasdaq/Application.cs @@ -1,6 +1,8 @@ using OpenTK.Graphics.OpenGL4; using OpenTK.Windowing.Common; using OpenTK.Windowing.Desktop; + +using rasdaq.Audio; using rasdaq.Core.ECS; using rasdaq.Graphics; using rasdaq.Inputs; @@ -58,6 +60,11 @@ internal void RemoveWorld(World world) _worlds.Remove(world); } + public AudioManager AudioManager { get; private set; } + + private List _worlds = new(); + private GameWindow _gameWindow; + /// /// Starts the application window. /// @@ -85,7 +92,7 @@ public void Run(int width, int height, string title) GameWindow.RenderFrame += OnRenderFrame; GameWindow.FramebufferResize += OnFramebufferResize; - InputManager = new InputManager(new GameWindowWrapper(GameWindow)); + InputManager = new InputManager(new GameWindowWrapper(_gameWindow)); InputManager.SetEventListeners(); GameWindow.Run(); diff --git a/src/rasdaq/Audio/Audio.cs b/src/rasdaq/Audio/Audio.cs new file mode 100644 index 0000000..95b0179 --- /dev/null +++ b/src/rasdaq/Audio/Audio.cs @@ -0,0 +1,23 @@ +using OpenTK.Audio.OpenAL; + +namespace rasdaq.Audio; + +public class Audio : IDisposable +{ + public int Handle; + public Audio() + { + Handle = AL.GenBuffer(); + // Check for errors + ALError error = AL.GetError(); + if (error != ALError.NoError) + { + throw new Exception("OpenAL error while trying to create audio buffer: " + error); + } + } + + public void Dispose() + { + AL.DeleteBuffer(Handle); + } +} diff --git a/src/rasdaq/Audio/AudioManager.cs b/src/rasdaq/Audio/AudioManager.cs new file mode 100644 index 0000000..e2011cb --- /dev/null +++ b/src/rasdaq/Audio/AudioManager.cs @@ -0,0 +1,143 @@ +using OpenTK.Audio.OpenAL; + +namespace rasdaq.Audio; + +public class AudioManager +{ + private static void CheckALCError(ALDevice device) + { + AlcError error = ALC.GetError(device); + if (error != AlcError.NoError) + { + throw new Exception("OpenAL error: " + error); + } + } + + private static void CheckALError() + { + ALError error = AL.GetError(); + if (error != ALError.NoError) + { + throw new Exception("OpenAL error: " + error); + } + } + + /// + /// Initialize Audio for the Application, using the default audio device. + /// + /// + public AudioManager() + { + // Open the default audio device. + ALDevice device = ALC.OpenDevice(null); + if (device == ALDevice.Null) + { + throw new Exception("Failed to open the default audio device."); + } + + // Create a context for the device. + ALContext context = ALC.CreateContext(device, new ALContextAttributes(null, null, null, null, null)); + if (context == ALContext.Null) + { + ALC.CloseDevice(device); + throw new Exception("Failed to create an OpenAL context."); + } + + // Check if we have any errors. + AlcError error = ALC.GetError(device); + CheckALCError(device); + // If no errors, make the context current. + ALC.MakeContextCurrent(context); + } + + /// + /// Load a WAV file that contains PCM data only. + /// + /// Path to WAV file + /// A integer handle referring to the loaded audio + /// + public Audio LoadAudio(string path) + { + // Create an OpenAL buffer for the data to go in. + Audio audio = new Audio(); + + // Load the WAV file + WAVLoader.WAVData wavData = WAVLoader.LoadWav(path); + + // Discern the format of the audio data based on number of channels and bits per sample. + ALFormat format; + if (wavData.Format.NumChannels == 1) + { + if (wavData.Format.BitsPerSample == 8) + { + format = ALFormat.Mono8; + } + else if (wavData.Format.BitsPerSample == 16) + { + format = ALFormat.Mono16; + } + else + { + throw new Exception("Unsupported bits per sample: " + wavData.Format.BitsPerSample); + } + } + else if (wavData.Format.NumChannels == 2) + { + if (wavData.Format.BitsPerSample == 8) + { + format = ALFormat.Stereo8; + } + else if (wavData.Format.BitsPerSample == 16) + { + format = ALFormat.Stereo16; + } + else + { + throw new Exception("Unsupported bits per sample: " + wavData.Format.BitsPerSample); + } + } + else + { + throw new Exception("Unsupported number of channels: " + wavData.Format.NumChannels); + } + + // Load the audio data into the OpenAL buffer. + AL.BufferData(audio.Handle, format, wavData.Data.Data, (int)wavData.Format.SampleRate); + + // Check if we have any errors from trying to load the audio data. + CheckALError(); + + return audio; + } + + /// + /// Add a loaded audio file to an audio source, so that it can be played. + /// + /// Integer handle of loaded audio file + /// Integer handle of audio source + public void AttachAudioToSource(Audio audio, AudioSource audioSource) + { + AL.Source(audio.Handle, ALSourcei.Buffer, audioSource.Handle); + CheckALError(); + } + + /// + /// Play back an audio source that has audio attached to it. + /// + /// + public void PlaySource(AudioSource audioSource) + { + AL.SourcePlay(audioSource.Handle); + CheckALError(); + } + + /// + /// Stop playback of an audio source. + /// + /// + public void StopSource(AudioSource audioSource) + { + AL.SourceStop(audioSource.Handle); + CheckALError(); + } +} \ No newline at end of file diff --git a/src/rasdaq/Audio/AudioSource.cs b/src/rasdaq/Audio/AudioSource.cs new file mode 100644 index 0000000..2064905 --- /dev/null +++ b/src/rasdaq/Audio/AudioSource.cs @@ -0,0 +1,23 @@ +using OpenTK.Audio.OpenAL; + +namespace rasdaq.Audio; + +public class AudioSource : IDisposable +{ + public int Handle; + public AudioSource() + { + Handle = AL.GenSource(); + // Check for errors + ALError error = AL.GetError(); + if (error != ALError.NoError) + { + throw new Exception("OpenAL error while trying to create audio source: " + error); + } + } + + public void Dispose() + { + AL.DeleteSource(Handle); + } +} diff --git a/src/rasdaq/Audio/WAVLoader.cs b/src/rasdaq/Audio/WAVLoader.cs new file mode 100644 index 0000000..d2e5f8a --- /dev/null +++ b/src/rasdaq/Audio/WAVLoader.cs @@ -0,0 +1,167 @@ +using System.Buffers.Binary; +using System.Text; + +namespace rasdaq.Audio; + +internal class WAVLoader +{ + /// + /// Format information about the WAV file. + /// + public struct FmtChunk + { + public ushort AudioFormat; + public ushort NumChannels; + public uint SampleRate; + public uint ByteRate; + public ushort BlockAlign; + public ushort BitsPerSample; + public FmtChunkExtension? Extension; + }; + /// + /// Extra format information for certain WAV files. + /// + public struct FmtChunkExtension + { + public ushort ValidBitsPerSample; + public uint SpeakerPositionMask; + public Guid SubFormat; + } + + /// + /// Holds the data of the WAV file, should be PCM data, as that is all is supported currently. + /// + public struct DataChunk + { + public uint Size; + public byte[] Data; + } + + /// + /// Holds all the information necessary about a WAV file, namely, + /// the format information and the data itself. + /// + public struct WAVData + { + public FmtChunk Format; + public DataChunk Data; + } + + private static byte[] ReadBytesFromStream(FileStream stream, int count) + { + byte[] buffer = new byte[count]; + stream.ReadExactly(buffer, 0, count); + return buffer; + } + + private static FmtChunk ProcessFmtChunck(FileStream stream) + { + uint size = BinaryPrimitives.ReadUInt32LittleEndian(ReadBytesFromStream(stream, 4)); + + FmtChunk chunk = new FmtChunk(); + chunk.AudioFormat = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytesFromStream(stream, 2)); + chunk.NumChannels = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytesFromStream(stream, 2)); + chunk.SampleRate = BinaryPrimitives.ReadUInt32LittleEndian(ReadBytesFromStream(stream, 4)); + chunk.ByteRate = BinaryPrimitives.ReadUInt32LittleEndian(ReadBytesFromStream(stream, 4)); + chunk.BlockAlign = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytesFromStream(stream, 2)); + chunk.BitsPerSample = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytesFromStream(stream, 2)); + + if (size > 16) + { + ushort extensionSize = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytesFromStream(stream, 2)); + if (extensionSize == 22) + { + chunk.Extension = new FmtChunkExtension() + { + ValidBitsPerSample = BinaryPrimitives.ReadUInt16LittleEndian(ReadBytesFromStream(stream, 2)), + SpeakerPositionMask = BinaryPrimitives.ReadUInt32LittleEndian(ReadBytesFromStream(stream, 4)), + SubFormat = new Guid(ReadBytesFromStream(stream, 16)) + }; + } + } + + return chunk; + } + + private static DataChunk ProcessDataChunk(FileStream stream) + { + DataChunk chunk = new DataChunk(); + chunk.Size = BinaryPrimitives.ReadUInt32LittleEndian(ReadBytesFromStream(stream, 4)); + chunk.Data = ReadBytesFromStream(stream, (int)chunk.Size); + return chunk; + } + + /// + /// Load the WAV file at the given path and return a WAVData struct containing the format information and the data itself. + /// + /// The path to the WAV file. + /// A WAVData struct containing the format information and the data. + /// Thrown if the WAV file is not found or is invalid. + public static WAVData LoadWav(string path) + { + if (!File.Exists(path)) + { + throw new Exception("WAV file not found: " + path); + } + + using (FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + // Read the RIFF header at the start of the WAV file. + byte[] fourcc = new byte[4]; + stream.ReadExactly(fourcc, 0, 4); + if (Encoding.ASCII.GetString(fourcc) != "RIFF") + { + throw new Exception("Invalid WAV file: missing RIFF header."); + } + + // Read the file size + byte[] byteFileSize = new byte[4]; + stream.ReadExactly(byteFileSize, 0, 4); + int fileSize = BinaryPrimitives.ReadInt32LittleEndian(byteFileSize) - 4; + + // Check file type is WAVE + byte[] byteFileType = new byte[4]; + stream.ReadExactly(byteFileType, 0, 4); + Encoding.UTF8.GetString(byteFileType); + if (Encoding.ASCII.GetString(byteFileType) != "WAVE") + { + throw new Exception("Invalid WAV file: file type not listed as WAVE in RIFF header."); + } + + FmtChunk fmtChunk = new FmtChunk(); + DataChunk dataChunk = new DataChunk(); + + // Now read the remaining chunks, we want a fmt chunk and a data chunk. + while (stream.Position < stream.Length - 4) + { + byte[] chunkId = new byte[4]; + stream.ReadExactly(chunkId, 0, 4); + + if (chunkId.SequenceEqual(Encoding.ASCII.GetBytes("fmt "))) + { + fmtChunk = ProcessFmtChunck(stream); + + if (fmtChunk.AudioFormat != 1) + { + throw new Exception("Unsupported WAV file: only PCM format is supported."); + } + } + else if (chunkId.SequenceEqual(Encoding.ASCII.GetBytes("data"))) + { + dataChunk = ProcessDataChunk(stream); + break; + } + else + { + throw new Exception("Unsupported WAV file: unknown chunk type " + Encoding.ASCII.GetString(chunkId)); + } + } + + return new WAVData() + { + Format = fmtChunk, + Data = dataChunk + }; + } + } +} From 749cd27915be6bdb6fe65cdcb77a2f894264fde7 Mon Sep 17 00:00:00 2001 From: Neel R <51024479+nrbyte@users.noreply.github.com> Date: Wed, 13 May 2026 23:25:56 +0100 Subject: [PATCH 07/20] Log errors too --- src/rasdaq/Audio/Audio.cs | 1 + src/rasdaq/Audio/AudioManager.cs | 36 +++++++++++++++++++++++--------- src/rasdaq/Audio/AudioSource.cs | 6 ++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/rasdaq/Audio/Audio.cs b/src/rasdaq/Audio/Audio.cs index 95b0179..6ef2feb 100644 --- a/src/rasdaq/Audio/Audio.cs +++ b/src/rasdaq/Audio/Audio.cs @@ -5,6 +5,7 @@ namespace rasdaq.Audio; public class Audio : IDisposable { public int Handle; + public Audio() { Handle = AL.GenBuffer(); diff --git a/src/rasdaq/Audio/AudioManager.cs b/src/rasdaq/Audio/AudioManager.cs index e2011cb..34c6251 100644 --- a/src/rasdaq/Audio/AudioManager.cs +++ b/src/rasdaq/Audio/AudioManager.cs @@ -1,5 +1,7 @@ using OpenTK.Audio.OpenAL; +using rasdaq.Logging; + namespace rasdaq.Audio; public class AudioManager @@ -9,7 +11,9 @@ private static void CheckALCError(ALDevice device) AlcError error = ALC.GetError(device); if (error != AlcError.NoError) { - throw new Exception("OpenAL error: " + error); + string err = $"OpenAL error: {error}"; + Log.Error(err) + throw new Exception(err); } } @@ -18,7 +22,9 @@ private static void CheckALError() ALError error = AL.GetError(); if (error != ALError.NoError) { - throw new Exception("OpenAL error: " + error); + string err = $"OpenAL error: {error}"; + Log.Error(err); + throw new Exception(err); } } @@ -32,7 +38,9 @@ public AudioManager() ALDevice device = ALC.OpenDevice(null); if (device == ALDevice.Null) { - throw new Exception("Failed to open the default audio device."); + string err = "Failed to open the default audio device."; + Log.Error(err); + throw new Exception(err); } // Create a context for the device. @@ -40,7 +48,9 @@ public AudioManager() if (context == ALContext.Null) { ALC.CloseDevice(device); - throw new Exception("Failed to create an OpenAL context."); + string err = "Failed to create an OpenAL context."; + Log.Error(err); + throw new Exception(err); } // Check if we have any errors. @@ -78,7 +88,9 @@ public Audio LoadAudio(string path) } else { - throw new Exception("Unsupported bits per sample: " + wavData.Format.BitsPerSample); + string err = $"Unsupported bits per sample: {wavData.Format.BitsPerSample}"; + Log.Error(err); + throw new Exception(err); } } else if (wavData.Format.NumChannels == 2) @@ -93,12 +105,16 @@ public Audio LoadAudio(string path) } else { - throw new Exception("Unsupported bits per sample: " + wavData.Format.BitsPerSample); + string err = $"Unsupported bits per sample: {wavData.Format.BitsPerSample}"; + Log.Error(err); + throw new Exception(err); } } else { - throw new Exception("Unsupported number of channels: " + wavData.Format.NumChannels); + string err = $"Unsupported number of channels: {wavData.Format.NumChannels}"; + Log.Error(err); + throw new Exception(err); } // Load the audio data into the OpenAL buffer. @@ -113,11 +129,11 @@ public Audio LoadAudio(string path) /// /// Add a loaded audio file to an audio source, so that it can be played. /// - /// Integer handle of loaded audio file - /// Integer handle of audio source + /// Audio file + /// Audio source public void AttachAudioToSource(Audio audio, AudioSource audioSource) { - AL.Source(audio.Handle, ALSourcei.Buffer, audioSource.Handle); + AL.Source(audioSource.Handle, ALSourcei.Buffer, audio.Handle); CheckALError(); } diff --git a/src/rasdaq/Audio/AudioSource.cs b/src/rasdaq/Audio/AudioSource.cs index 2064905..09cf8ba 100644 --- a/src/rasdaq/Audio/AudioSource.cs +++ b/src/rasdaq/Audio/AudioSource.cs @@ -1,10 +1,13 @@ using OpenTK.Audio.OpenAL; +using rasdaq.Logging; + namespace rasdaq.Audio; public class AudioSource : IDisposable { public int Handle; + public AudioSource() { Handle = AL.GenSource(); @@ -12,6 +15,9 @@ public AudioSource() ALError error = AL.GetError(); if (error != ALError.NoError) { + string err = $"OpenAL error while trying to create audio source: {error}"; + + Log.Error(err); throw new Exception("OpenAL error while trying to create audio source: " + error); } } From 2acd571eed21198f0b0c513db770c64aec01cbfc Mon Sep 17 00:00:00 2001 From: Neel R <51024479+nrbyte@users.noreply.github.com> Date: Wed, 13 May 2026 23:31:37 +0100 Subject: [PATCH 08/20] Ran formatter --- src/rasdaq/Audio/Audio.cs | 2 +- src/rasdaq/Audio/AudioSource.cs | 2 +- src/rasdaq/Audio/WAVLoader.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rasdaq/Audio/Audio.cs b/src/rasdaq/Audio/Audio.cs index 6ef2feb..890dd4f 100644 --- a/src/rasdaq/Audio/Audio.cs +++ b/src/rasdaq/Audio/Audio.cs @@ -21,4 +21,4 @@ public void Dispose() { AL.DeleteBuffer(Handle); } -} +} \ No newline at end of file diff --git a/src/rasdaq/Audio/AudioSource.cs b/src/rasdaq/Audio/AudioSource.cs index 09cf8ba..b94f547 100644 --- a/src/rasdaq/Audio/AudioSource.cs +++ b/src/rasdaq/Audio/AudioSource.cs @@ -26,4 +26,4 @@ public void Dispose() { AL.DeleteSource(Handle); } -} +} \ No newline at end of file diff --git a/src/rasdaq/Audio/WAVLoader.cs b/src/rasdaq/Audio/WAVLoader.cs index d2e5f8a..508643e 100644 --- a/src/rasdaq/Audio/WAVLoader.cs +++ b/src/rasdaq/Audio/WAVLoader.cs @@ -164,4 +164,4 @@ public static WAVData LoadWav(string path) }; } } -} +} \ No newline at end of file From 8da3490812624ce8a0744681ceb4e0607bde0b0d Mon Sep 17 00:00:00 2001 From: Neel R <51024479+nrbyte@users.noreply.github.com> Date: Wed, 13 May 2026 23:34:09 +0100 Subject: [PATCH 09/20] Log error --- src/rasdaq/Audio/Audio.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/rasdaq/Audio/Audio.cs b/src/rasdaq/Audio/Audio.cs index 890dd4f..d912484 100644 --- a/src/rasdaq/Audio/Audio.cs +++ b/src/rasdaq/Audio/Audio.cs @@ -1,5 +1,7 @@ using OpenTK.Audio.OpenAL; +using rasdaq.Logging; + namespace rasdaq.Audio; public class Audio : IDisposable @@ -13,7 +15,10 @@ public Audio() ALError error = AL.GetError(); if (error != ALError.NoError) { - throw new Exception("OpenAL error while trying to create audio buffer: " + error); + string err = $"OpenAL error while trying to create audio buffer: {error}"; + + Log.Error(err); + throw new Exception(err); } } From 5fa95a2c7e7ef112abd7b17a10efb0c5fff43f44 Mon Sep 17 00:00:00 2001 From: Neel R <51024479+nrbyte@users.noreply.github.com> Date: Wed, 13 May 2026 23:35:09 +0100 Subject: [PATCH 10/20] Fix syntax error --- src/rasdaq/Audio/AudioManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rasdaq/Audio/AudioManager.cs b/src/rasdaq/Audio/AudioManager.cs index 34c6251..aff771d 100644 --- a/src/rasdaq/Audio/AudioManager.cs +++ b/src/rasdaq/Audio/AudioManager.cs @@ -12,7 +12,7 @@ private static void CheckALCError(ALDevice device) if (error != AlcError.NoError) { string err = $"OpenAL error: {error}"; - Log.Error(err) + Log.Error(err); throw new Exception(err); } } From ad5e86f474b0308b750f21b3264a90491a12e846 Mon Sep 17 00:00:00 2001 From: Neel R <51024479+nrbyte@users.noreply.github.com> Date: Fri, 15 May 2026 22:01:12 +0100 Subject: [PATCH 11/20] Convert AudioSource into a Component --- src/rasdaq/Audio/AudioManager.cs | 6 +++--- src/rasdaq/Audio/AudioSource.cs | 36 +++++++++++++++++++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/rasdaq/Audio/AudioManager.cs b/src/rasdaq/Audio/AudioManager.cs index aff771d..3a10331 100644 --- a/src/rasdaq/Audio/AudioManager.cs +++ b/src/rasdaq/Audio/AudioManager.cs @@ -6,7 +6,7 @@ namespace rasdaq.Audio; public class AudioManager { - private static void CheckALCError(ALDevice device) + internal static void CheckALCError(ALDevice device) { AlcError error = ALC.GetError(device); if (error != AlcError.NoError) @@ -17,7 +17,7 @@ private static void CheckALCError(ALDevice device) } } - private static void CheckALError() + internal static void CheckALError() { ALError error = AL.GetError(); if (error != ALError.NoError) @@ -156,4 +156,4 @@ public void StopSource(AudioSource audioSource) AL.SourceStop(audioSource.Handle); CheckALError(); } -} \ No newline at end of file +} diff --git a/src/rasdaq/Audio/AudioSource.cs b/src/rasdaq/Audio/AudioSource.cs index b94f547..583b783 100644 --- a/src/rasdaq/Audio/AudioSource.cs +++ b/src/rasdaq/Audio/AudioSource.cs @@ -1,14 +1,15 @@ using OpenTK.Audio.OpenAL; +using rasdaq.Core.ECS; using rasdaq.Logging; namespace rasdaq.Audio; -public class AudioSource : IDisposable +public class AudioSource : Component, IDisposable { - public int Handle; + internal int? Handle = null; - public AudioSource() + internal override void Init() { Handle = AL.GenSource(); // Check for errors @@ -22,8 +23,33 @@ public AudioSource() } } + public void Play() + { + if (!Handle.HasValue) + { + Log.Error("Audio source handle is null. Cannot play audio."); + return; + } + AL.SourcePlay(Handle.Value); + AudioManager.CheckALError(); + } + + public void Stop() + { + if (!Handle.HasValue) + { + Log.Error("Audio source handle is null. Cannot play audio."); + return; + } + AL.SourceStop(Handle.Value); + AudioManager.CheckALError(); + } + public void Dispose() { - AL.DeleteSource(Handle); + if (Handle.HasValue) + { + AL.DeleteSource(Handle.Value); + } } -} \ No newline at end of file +} From a2ec1cea6a301c147051a839338bfec12a95ded2 Mon Sep 17 00:00:00 2001 From: Neel R <51024479+nrbyte@users.noreply.github.com> Date: Thu, 28 May 2026 23:11:26 +0100 Subject: [PATCH 12/20] Refactor audio around ECS + ResourceManager --- samples/pong/Soldier.cs | 37 ++++++++- src/rasdaq/Application.cs | 3 +- src/rasdaq/Audio/AudioManager.cs | 99 +------------------------ src/rasdaq/Audio/AudioSource.cs | 24 ++++++ src/rasdaq/Audio/WAVLoader.cs | 76 ++++++++++++++++++- src/rasdaq/Resources/ResourceManager.cs | 4 +- 6 files changed, 138 insertions(+), 105 deletions(-) diff --git a/samples/pong/Soldier.cs b/samples/pong/Soldier.cs index 6a7850d..97dec95 100644 --- a/samples/pong/Soldier.cs +++ b/samples/pong/Soldier.cs @@ -1,10 +1,16 @@ -using rasdaq; +using rasdaq; using rasdaq.Core.ECS; using rasdaq.Graphics; using rasdaq.Inputs; using rasdaq.Logging; using rasdaq.Resources; using rasdaq.Transformations; +using OpenTK.Audio.OpenAL; +using rasdaq.Audio; +using rasdaq.Core.ECS; +using rasdaq.Inputs; +using rasdaq.Logging; +using rasdaq.Resources; namespace pong; @@ -13,16 +19,37 @@ internal class Soldier : Component public override void Start() { base.Start(); + + Entity?.AddComponent(new AudioSource()); + + Audio audio1 = ResourceManager.Load