diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..1b9fa9897e25dff2bf7cf2c9f0caa97af12411a9
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*,ogg filter=lfs diff=lfs merge=lfs -text
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e1527a8acd015982bf54426d5818d02f8bef4972..851e31324dd4f2e0756babc065a3f58ffb64426b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -28,6 +28,7 @@ if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
                 spdlog/1.10.0
                 freetype/2.11.1
                 bullet3/3.22a
+                openal/1.21.1
             GENERATORS 
                 cmake_find_package
                 cmake_find_package_multi
diff --git a/demo/data/click.ogg b/demo/data/click.ogg
new file mode 100644
index 0000000000000000000000000000000000000000..62de5e45638cfd2a568406ff86f25ff98e2738e1
Binary files /dev/null and b/demo/data/click.ogg differ
diff --git a/demo/demo/main.cpp b/demo/demo/main.cpp
index 45846b301cff1f443d9ba2641711d985ea8a16f5..8eec8551a933e04f336533ead3c98223f6572f5c 100644
--- a/demo/demo/main.cpp
+++ b/demo/demo/main.cpp
@@ -23,6 +23,7 @@
 #include <memory>
 
 #include "fggl/app.hpp"
+#include "fggl/audio/openal/audio.hpp"
 
 #include "fggl/gfx/atlas.hpp"
 #include "fggl/gfx/window.hpp"
@@ -77,6 +78,7 @@ static void setupServiceLocators(fggl::util::ServiceLocator& locator) {
 
 	locator.supply<fggl::gui::FontLibrary>(std::make_shared<fggl::gui::FontLibrary>());
 	locator.supply<fggl::ecs3::TypeRegistry>(std::make_shared<fggl::ecs3::TypeRegistry>());
+	locator.supply<fggl::audio::AudioService>(std::make_shared<fggl::audio::openAL>());
 }
 
 int main(int argc, const char* argv[]) {
@@ -107,9 +109,19 @@ int main(int argc, const char* argv[]) {
     auto *menu = app.add_state<fggl::scenes::BasicMenu>("menu");
 
 	// add some menu items for the game states
-    menu->add("terrain", [&app]() { app.change_state("game"); });
-	menu->add("rollball", [&app]() { app.change_state("rollball"); });
-	menu->add("quit", [&app]() { app.running(false); });
+	auto audio = locator.get<fggl::audio::AudioService>();
+    menu->add("terrain", [&app, &audio]() {
+		audio->play("click.ogg", false);
+		app.change_state("game");
+	});
+	menu->add("rollball", [&app, &audio]() {
+		audio->play("click.ogg", false);
+		app.change_state("rollball");
+	});
+	menu->add("quit", [&app, &audio]() {
+		audio->play("click.ogg", false);
+		app.running(false);
+	});
 
 	// the game states themselves
     app.add_state<GameScene>("game");
diff --git a/fggl/CMakeLists.txt b/fggl/CMakeLists.txt
index 0d344924a1dfc983a20ac90e5d35f6a7c5df987b..7305b5b474370d83bb0b468c004e0c758b9c75b0 100644
--- a/fggl/CMakeLists.txt
+++ b/fggl/CMakeLists.txt
@@ -69,6 +69,7 @@ target_link_libraries(${PROJECT_NAME} PUBLIC freetype)
 
 # Graphics backend
 add_subdirectory(gfx)
+add_subdirectory(audio)
 
 # physics integration
 add_subdirectory(phys/bullet)
diff --git a/fggl/audio/CMakeLists.txt b/fggl/audio/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f4c1f309d78973af88e548d274c9664eea0d5367
--- /dev/null
+++ b/fggl/audio/CMakeLists.txt
@@ -0,0 +1,7 @@
+target_sources(fggl
+        PRIVATE
+            types.cpp
+
+)
+
+add_subdirectory(openal)
\ No newline at end of file
diff --git a/fggl/audio/openal/CMakeLists.txt b/fggl/audio/openal/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..1b7284d2cb481502cb37b21c40367960bfc6ab60
--- /dev/null
+++ b/fggl/audio/openal/CMakeLists.txt
@@ -0,0 +1,8 @@
+find_package( OpenAL REQUIRED )
+target_link_libraries(fggl PUBLIC OpenAL::OpenAL )
+
+target_sources(fggl
+    PRIVATE
+        audio.cpp
+)
+
diff --git a/fggl/audio/openal/audio.cpp b/fggl/audio/openal/audio.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6afd5402cc69bb5d0f458a5f30537b0977a73778
--- /dev/null
+++ b/fggl/audio/openal/audio.cpp
@@ -0,0 +1,49 @@
+/*
+ * ${license.title}
+ * Copyright (C) 2022 ${license.owner}
+ * ${license.mailto}
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+
+#include "fggl/audio/openal/audio.hpp"
+
+#include "fggl/util/service.h"
+#include "fggl/data/storage.hpp"
+
+namespace fggl::audio::openal {
+
+	void AudioServiceOAL::play(const std::string &filename, bool looping) {
+
+		// load audio clip into temp storage
+		AudioClip clip;
+		auto& locator = util::ServiceLocator::instance();
+		auto storage = locator.get<data::Storage>();
+		bool result = storage->load(data::StorageType::Data, filename, &clip);
+		if ( !result ) {
+			std::cerr << "error: can't load audio data" << std::endl;
+		}
+
+		play(clip, looping);
+	}
+
+	void AudioServiceOAL::play(AudioClip &clip, bool looping) {
+		// play the audio on the default (non-positioned) source
+		if ( m_defaultSource != nullptr ) {
+			m_defaultSource->play(clip, looping);
+		}
+	}
+}
\ No newline at end of file
diff --git a/fggl/audio/types.cpp b/fggl/audio/types.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..40df669d73a3dfc8b1273c0d67a6676e9e7eea59
--- /dev/null
+++ b/fggl/audio/types.cpp
@@ -0,0 +1,42 @@
+/*
+ * ${license.title}
+ * Copyright (C) 2022 ${license.owner}
+ * ${license.mailto}
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include "fggl/data/storage.hpp"
+#include "fggl/audio/audio.hpp"
+
+#include "../stb/stb_vorbis.h"
+
+namespace fggl::audio {
+
+
+} // namespace fggl::audio
+
+namespace fggl::data {
+
+	template<>
+	bool fggl_deserialize<audio::AudioClip>(std::filesystem::path &data, audio::AudioClip* out) {
+		out->sampleCount = stb_vorbis_decode_filename(data.c_str(),
+													 &out->channels,
+													 &out->sampleRate,
+													 &out->data);
+		return out->sampleCount != -1;
+	}
+
+} // namespace fggl::data
\ No newline at end of file
diff --git a/include/fggl/audio/audio.hpp b/include/fggl/audio/audio.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..0b2eee08a7370689c20cb3da3736cf3c8ae1e211
--- /dev/null
+++ b/include/fggl/audio/audio.hpp
@@ -0,0 +1,51 @@
+/*
+ * ${license.title}
+ * Copyright (C) 2022 ${license.owner}
+ * ${license.mailto}
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef FGGL_AUDIO_AUDIO_HPP
+#define FGGL_AUDIO_AUDIO_HPP
+
+#include <string>
+
+namespace fggl::audio {
+
+	/**
+	 * AudioClip is bit of audio loaded into memory.
+	 *
+	 * If the sampleCount is -1, the clip is invalid.
+	 */
+	struct AudioClip {
+		int channels;
+		int sampleRate;
+		int sampleCount;
+		short* data;
+	};
+
+	class AudioService {
+		public:
+			AudioService() = default;
+			virtual ~AudioService() = default;
+
+			virtual void play(const std::string& filename, bool looping = false) = 0;
+			virtual void play(AudioClip& clip, bool looping = false) = 0;
+	};
+
+} // namespace fggl::audio
+
+#endif //FGGL_AUDIO_AUDIO_HPP
diff --git a/include/fggl/audio/openal/audio.hpp b/include/fggl/audio/openal/audio.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b9de3eea7a940f9b2c4ea1f3099a71f8d6ea54e8
--- /dev/null
+++ b/include/fggl/audio/openal/audio.hpp
@@ -0,0 +1,197 @@
+/*
+ * ${license.title}
+ * Copyright (C) 2022 ${license.owner}
+ * ${license.mailto}
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+//
+// Created by webpigeon on 01/05/22.
+//
+
+#ifndef FGGL_AUDIO_OPENAL_AUDIO_HPP
+#define FGGL_AUDIO_OPENAL_AUDIO_HPP
+
+#include <AL/al.h>
+#include <AL/alc.h>
+
+#include "fggl/audio/audio.hpp"
+#include "fggl/math/types.hpp"
+
+#include <string>
+#include <iostream>
+#include <memory>
+
+namespace fggl::audio::openal {
+
+	enum class AudioType {
+		MONO_8 = AL_FORMAT_MONO8,
+		MONO_16 = AL_FORMAT_MONO16,
+		STEREO_8 = AL_FORMAT_STEREO8,
+		STEREO_16 = AL_FORMAT_STEREO16
+	};
+
+	static void checkError(std::string context) {
+		auto code = alGetError();
+		if ( code == AL_NO_ERROR) {
+			return;
+		}
+
+		// now we check the error message
+		std::string error = "unknown";
+		switch ( code ) {
+			case ALC_INVALID_DEVICE:
+				error = "Invalid Device";
+				break;
+			case ALC_INVALID_CONTEXT:
+				error = "Invalid Context";
+				break;
+			case ALC_INVALID_ENUM:
+				error = "Invalid enum";
+				break;
+			case ALC_INVALID_VALUE:
+				error = "Invalid value";
+				break;
+			case ALC_OUT_OF_MEMORY:
+				error = "Out of memory";
+				break;
+			default:
+				error = "unknown error";
+		}
+
+		std::cerr << "OpenAL error: " << context << ": " << error << std::endl;
+	}
+
+	class AudioBuffer {
+		public:
+			AudioBuffer() : m_buffer(0) {
+				alGenBuffers(1, &m_buffer);
+			}
+			~AudioBuffer() {
+				alDeleteBuffers(1, &m_buffer);
+			}
+
+			inline void setData(AudioType type, void* data, ALsizei size, ALsizei frequency) {
+				alBufferData(m_buffer, (ALenum)type, data, size, frequency);
+			}
+
+			inline ALuint value() {
+				return m_buffer;
+			}
+		private:
+			ALuint m_buffer;
+	};
+
+
+	class AudioSource {
+		public:
+			AudioSource() : m_source(0), m_splat() {
+				alGenSources(1, &m_source);
+			}
+			~AudioSource(){
+				alDeleteSources(1, &m_source);
+			}
+
+			inline void play() {
+				alSourcePlay( m_source );
+			}
+
+			inline void stop() {
+				alSourceStop(m_source);
+			}
+
+			inline void pause() {
+				alSourcePause( m_source );
+			}
+
+			inline void rewind() {
+				alSourceRewind( m_source );
+			}
+
+			inline void play(AudioBuffer& buffer, bool looping) {
+				alSourcei(m_source, AL_BUFFER, buffer.value());
+				alSourcei( m_source, AL_LOOPING, looping ? AL_TRUE : AL_FALSE);
+			}
+
+			inline void play(AudioClip& clip, bool looping) {
+				checkError("pre play");
+
+				AudioType format = clip.channels == 1 ? AudioType::MONO_8 : AudioType::STEREO_8;
+				m_splat.setData(format, clip.data, clip.sampleCount, clip.sampleRate);
+				checkError("saving to buffer");
+
+				alSourcei(m_source, AL_BUFFER, m_splat.value());
+				alSourcei( m_source, AL_LOOPING, looping ? AL_TRUE : AL_FALSE);
+				checkError("setting parameters");
+
+				play();
+			}
+
+			inline void velocity(math::vec3& value) {
+				alSource3f(m_source, AL_VELOCITY, value.x, value.y, value.z);
+			}
+			inline void position(math::vec3& value) {
+				alSource3f(m_source, AL_POSITION, value.x, value.y, value.z);
+			}
+
+			void direction(math::vec3& value) {
+				alSource3f(m_source, AL_DIRECTION, value.x, value.y, value.z);
+			}
+
+		private:
+			ALuint m_source;
+			AudioBuffer m_splat;
+	};
+
+
+	class AudioServiceOAL : public AudioService {
+		public:
+			AudioServiceOAL() : m_device(alcOpenDevice(nullptr)) {
+				if ( m_device != nullptr ) {
+					m_context = alcCreateContext(m_device, nullptr);
+					alcMakeContextCurrent(m_context);
+					checkError("context setup");
+
+					m_defaultSource = std::make_unique<AudioSource>();
+					checkError("default source setup");
+				}
+			}
+
+			~AudioServiceOAL() override {
+				if ( m_device != nullptr ) {
+					alcDestroyContext(m_context);
+					alcCloseDevice(m_device);
+				}
+			}
+
+			void play(const std::string& filename, bool looping = false) override;
+			void play(AudioClip& clip, bool looping = false) override;
+
+		private:
+			ALCdevice* m_device;
+			ALCcontext* m_context{nullptr};
+			std::unique_ptr<AudioSource> m_defaultSource{nullptr};
+	};
+
+
+} // namespace fggl::audio::openal
+
+namespace fggl::audio {
+	using openAL = openal::AudioServiceOAL;
+
+} // namepace fggl::audio
+
+#endif //FGGL_AUDIO_OPENAL_AUDIO_HPP
diff --git a/include/fggl/data/storage.hpp b/include/fggl/data/storage.hpp
index 6ae22d0eb609c6616df547b8f095e65e7edd5aae..872c753e20e78ed6c9804e214c4f62ccb344c64c 100644
--- a/include/fggl/data/storage.hpp
+++ b/include/fggl/data/storage.hpp
@@ -22,7 +22,7 @@ namespace fggl::data {
 			bool load(StorageType pool, const std::string &name, T *out) {
 				auto path = resolvePath(pool, name);
 				if (!std::filesystem::exists(path)) {
-					//spdlog::warn("Path {} does not exist!", path.c_str());
+					std::cerr << "Path " << path << " does not exist!" << std::endl;
 					return false;
 				}
 				return fggl_deserialize<T>(path, out);