summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJonathan Bradley <jcb@pikum.xyz>2025-06-25 17:50:44 -0400
committerJonathan Bradley <jcb@pikum.xyz>2025-06-25 17:50:44 -0400
commit9e791d26560b566bb21b5cd39d9042a41f29714c (patch)
tree7e95ca82423feb1009b6916bca82142d1326a94a /src
parent3c73b503330eb67ad9489da6941ae3b28a686780 (diff)
audio: first-pass, pipewire
Diffstat (limited to 'src')
-rw-r--r--src/audio-impl-pw.cpp364
-rw-r--r--src/audio-impl-pw.hpp71
-rw-r--r--src/audio-types.hpp36
-rw-r--r--src/audio.cpp32
-rw-r--r--src/audio.hpp18
-rw-r--r--src/game.cpp3
6 files changed, 524 insertions, 0 deletions
diff --git a/src/audio-impl-pw.cpp b/src/audio-impl-pw.cpp
new file mode 100644
index 0000000..61e27a2
--- /dev/null
+++ b/src/audio-impl-pw.cpp
@@ -0,0 +1,364 @@
+
+#include "audio-impl-pw.hpp"
+#include "audio-types.hpp"
+#include "pipewire/keys.h"
+#include "pipewire/thread-loop.h"
+#include "pk.h"
+
+// see header file
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wpedantic"
+#include "spa/utils/json.h"
+#pragma GCC diagnostic pop
+
+#define PKE_AUDIO_PW_NAME_RESERVE_LEN 256
+
+pke_audio_implementation_pipewire pke_audio_impl_pw{};
+
+int metadata_event_property(void *data, uint32_t subject, const char *key, const char *type, const char *value);
+
+static const struct pw_stream_events stream_events = {
+ .version = PW_VERSION_STREAM_EVENTS,
+ .destroy = nullptr,
+ .state_changed = nullptr,
+ .control_info = nullptr,
+ .io_changed = nullptr,
+ .param_changed = on_pipewire_stream_param_changed,
+ .add_buffer = nullptr,
+ .remove_buffer = nullptr,
+ .process = on_pipewire_process,
+ .drained = nullptr,
+ .command = nullptr,
+ .trigger_done = nullptr,
+};
+static const struct pw_registry_events registry_events = {
+ .version = PW_VERSION_REGISTRY_EVENTS,
+ .global = on_registry_event_global,
+ .global_remove = on_registry_event_global_removal,
+};
+static const struct pw_metadata_events metadata_events {
+ .version = PW_VERSION_METADATA_EVENTS,
+ .property = metadata_event_property,
+};
+
+bool pw_objects_find_pw_object_by_id(void *removed_id, void *item)
+{
+ return reinterpret_cast<pke_audio_pw_object *>(item)->id == *(uint32_t *)removed_id;
+}
+
+void pke_audio_pw_init() {
+ int i;
+
+ pke_audio_impl_pw.bkt = pk_mem_bucket_create("pipewire bucket", PK_MEM_DEFAULT_BUCKET_SIZE, PK_MEMBUCKET_FLAG_NONE);
+ pke_audio_impl_pw.loop = nullptr;
+ pke_audio_impl_pw.pod_buffer = nullptr;
+ pke_audio_impl_pw.stream = nullptr;
+ pke_audio_impl_pw.core = nullptr;
+ pke_audio_impl_pw.registry = nullptr;
+ pke_audio_impl_pw.registry_listener = {};
+ pke_audio_impl_pw.metadata = nullptr;
+ pke_audio_impl_pw.metadata_listener = {};
+ pke_audio_impl_pw.pw_objects.bkt = pke_audio_impl_pw.bkt;
+ pke_audio_impl_pw.created_objects.bkt = pke_audio_impl_pw.bkt;
+ pke_audio_impl_pw.default_sink_name = pk_new<char>(PKE_AUDIO_PW_NAME_RESERVE_LEN, pke_audio_impl_pw.bkt);
+ pke_audio_impl_pw.default_sink_id = 0;
+ pke_audio_impl_pw.is_needing_output_remapped = true;
+
+ pke_audio_impl_pw.pod_buffer = pk_new<uint8_t>(pke_audio_mstr.channel_count * PKE_AUDIO_IMPL_POD_BUFFER_LEN, pke_audio_impl_pw.bkt);
+
+ pw_init(NULL, NULL);
+
+ pke_audio_impl_pw.loop = pw_thread_loop_new("pke audio thread", NULL);
+ if (pke_audio_impl_pw.loop == NULL) {
+ fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to create pw_loop");
+ return;
+ }
+
+ pw_properties *props = pw_properties_new(
+ PW_KEY_MEDIA_NAME, "pke-audio-pw",
+ PW_KEY_APP_NAME, "pke",
+ PW_KEY_MEDIA_TYPE, "Audio",
+ PW_KEY_MEDIA_CATEGORY, "Playback",
+ PW_KEY_MEDIA_ROLE, "Music",
+ nullptr);
+ assert(props != NULL);
+ pke_audio_impl_pw.stream = pw_stream_new_simple(
+ pw_thread_loop_get_loop(pke_audio_impl_pw.loop),
+ "pke-audio-pw",
+ props,
+ &stream_events,
+ NULL /* user-data */);
+ if (pke_audio_impl_pw.stream == NULL) {
+ fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to create pw_stream");
+ return;
+ }
+
+ spa_pod_builder b{};
+ b.data = pke_audio_impl_pw.pod_buffer;
+ b.size = PKE_AUDIO_IMPL_POD_BUFFER_LEN * pke_audio_mstr.channel_count;
+ b._padding = 0;
+ b.callbacks.data = NULL;
+ b.callbacks.funcs = NULL;
+ b.state.flags = 0;
+ b.state.offset = 0;
+ b.state.frame = NULL;
+
+ spa_audio_info_raw spa_audio_info_raw_init{};
+ spa_audio_info_raw_init.flags = 0;
+ spa_audio_info_raw_init.format = SPA_AUDIO_FORMAT_F32;
+ spa_audio_info_raw_init.rate = 48000;
+ spa_audio_info_raw_init.channels = pke_audio_mstr.channel_count;
+ spa_audio_info_raw_init.position[0] = SPA_AUDIO_CHANNEL_FL;
+ spa_audio_info_raw_init.position[1] = SPA_AUDIO_CHANNEL_FR;
+
+ const struct spa_pod *params[1];
+ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat,
+ &spa_audio_info_raw_init);
+
+ if (i = pw_stream_connect(pke_audio_impl_pw.stream,
+ PW_DIRECTION_OUTPUT,
+ PW_ID_ANY,
+ pw_stream_flags(PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS),
+ params, 1), i != 0) {
+ fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to connect to pw_stream");
+ return;
+ }
+
+ pke_audio_impl_pw.core = pw_stream_get_core(pke_audio_impl_pw.stream);
+ if (pke_audio_impl_pw.core == NULL) {
+ fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to get core");
+ return;
+ }
+
+ pke_audio_impl_pw.registry = pw_core_get_registry(pke_audio_impl_pw.core, PW_VERSION_REGISTRY, 0);
+ if (pke_audio_impl_pw.registry == NULL) {
+ fprintf(stderr, "[" __FILE__ "][pke_audio_pw_init] failed to get registry");
+ return;
+ }
+
+ memset(&pke_audio_impl_pw.registry_listener, 0, sizeof(pke_audio_impl_pw.registry_listener));
+ pw_registry_add_listener(pke_audio_impl_pw.registry, &pke_audio_impl_pw.registry_listener, &registry_events, NULL);
+
+ pw_thread_loop_start(pke_audio_impl_pw.loop);
+ return;
+}
+
+void pke_audio_pw_teardown() {
+ uint32_t i;
+ pke_audio_mstr.mtx_buffer.lock();
+ pw_thread_loop_stop(pke_audio_impl_pw.loop);
+ for (i = pke_audio_impl_pw.created_objects.next; i > 0; --i) {
+ pw_core_destroy(pke_audio_impl_pw.core, ((void **)pke_audio_impl_pw.created_objects.data)[i - 1]);
+ }
+ for (i = pke_audio_impl_pw.pw_objects.next; i > 0; --i) {
+ pw_properties_free(pke_audio_impl_pw.pw_objects[i - 1].props);
+ }
+ if (pke_audio_impl_pw.metadata != NULL) {
+ pw_proxy_destroy((struct pw_proxy*)pke_audio_impl_pw.metadata);
+ }
+ if (pke_audio_impl_pw.registry != NULL) {
+ pw_proxy_destroy((struct pw_proxy*)pke_audio_impl_pw.registry);
+ }
+ if (pke_audio_impl_pw.core != NULL) {
+ pw_core_disconnect(pke_audio_impl_pw.core);
+ }
+ if (pke_audio_impl_pw.stream != NULL) {
+ pw_stream_destroy(pke_audio_impl_pw.stream);
+ }
+ if (pke_audio_impl_pw.loop != NULL) {
+ pw_thread_loop_destroy(pke_audio_impl_pw.loop);
+ }
+ pw_deinit();
+ pk_delete<char>(pke_audio_impl_pw.default_sink_name, PKE_AUDIO_PW_NAME_RESERVE_LEN, pke_audio_impl_pw.bkt);
+ pk_delete<uint8_t>(pke_audio_impl_pw.pod_buffer, pke_audio_mstr.channel_count * PKE_AUDIO_IMPL_POD_BUFFER_LEN, pke_audio_impl_pw.bkt);
+ pk_mem_bucket_destroy(pke_audio_impl_pw.bkt);
+ pke_audio_impl_pw.bkt = CAFE_BABE(pk_membucket);
+ pke_audio_mstr.mtx_buffer.unlock();
+}
+
+void on_pipewire_process(void *user_data) {
+ (void)user_data;
+ pw_buffer *b;
+ spa_buffer *buf;
+ int stride;
+ uint64_t n_frames;
+ float *dst;
+
+ if ((b = pw_stream_dequeue_buffer(pke_audio_impl_pw.stream)) == NULL) {
+ fprintf(stderr, "[" __FILE__ "][on_pipewire_process] out of buffers");
+ return;
+ }
+
+ buf = b->buffer;
+ if ((dst = (float*)buf->datas[0].data) == NULL) {
+ fprintf(stderr, "[" __FILE__ "][on_pipewire_process] given buffer was null");
+ return;
+ }
+
+ stride = sizeof(float) * pke_audio_mstr.channel_count;
+ n_frames = buf->datas[0].maxsize / stride;
+ if (b->requested) {
+ n_frames = PK_MIN(b->requested, n_frames);
+ }
+
+ pke_audio_mstr.mtx_buffer.lock();
+ memset(dst, 0, sizeof(float) * n_frames * pke_audio_mstr.channel_count);
+ buf->datas[0].chunk->offset = 0;
+ buf->datas[0].chunk->stride = stride;
+ buf->datas[0].chunk->size = n_frames * stride;
+ pw_stream_queue_buffer(pke_audio_impl_pw.stream, b);
+ pke_audio_mstr.mtx_buffer.unlock();
+ return;
+}
+
+void on_pipewire_stream_param_changed(void *, uint32_t id, const struct spa_pod *param) {
+ /* NULL means to clear the format */
+ if (param == NULL || id != SPA_PARAM_Format) {
+ // memset(&pke_audio_impl_pw.format, 0, sizeof(struct spa_audio_info));
+ return;
+ }
+
+ if (spa_format_parse(param, &pke_audio_impl_pw.format.media_type, &pke_audio_impl_pw.format.media_subtype) < 0) {
+ return;
+ }
+
+ /* only accept raw audio */
+ if (pke_audio_impl_pw.format.media_type != SPA_MEDIA_TYPE_audio || pke_audio_impl_pw.format.media_subtype != SPA_MEDIA_SUBTYPE_raw) {
+ return;
+ }
+
+ /* call a helper function to parse the format for us. */
+ spa_format_audio_raw_parse(param, &pke_audio_impl_pw.format.info.raw);
+
+ fprintf(stdout, "\r\ncapturing rate changed:%d channels:%d\n", pke_audio_impl_pw.format.info.raw.rate, pke_audio_impl_pw.format.info.raw.channels);
+ return;
+}
+
+int metadata_event_property(void *data, uint32_t subject, const char *key, const char *type, const char *value)
+{
+ if (subject == PW_ID_CORE) {
+ if (key == NULL || spa_streq(key, "default.audio.sink")) {
+ if (pke_audio_impl_pw.default_sink_id != 0) {
+ // the default just changed??
+ pke_audio_impl_pw.is_needing_output_remapped = true;
+ }
+ if (value == NULL || spa_json_str_object_find(value, strlen(value), "name", pke_audio_impl_pw.default_sink_name, sizeof(pke_audio_impl_pw.default_sink_name)) < 0) {
+ pke_audio_impl_pw.default_sink_name[0] = '\0';
+ }
+ }
+ }
+ fprintf(stdout, "\r\nPW_METADATA_CB: data: %p, subject: %d, key: '%s', type: '%s', value: '%s'\n", data, subject, key, type, value);
+ return 0;
+}
+
+void on_registry_event_global(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) {
+ (void)data;
+ (void)permissions;
+ const char *str;
+ pke_audio_pw_object obj{};
+ enum PK_STN_RES res;
+
+ // if (props == NULL) return; // why
+ if (props == NULL) {
+ printf("\r\nPW CALLBACK: NULL PROPS: object: id:%u type:%s/%d\n", id, type, version);
+ return; // why - in what scenario does this happen?
+ }
+
+ if (spa_streq(type, PW_TYPE_INTERFACE_Node)) {
+ printf("\r\nPW CALLBACK NODE: object: id:%u type:%s/%d\n", id, type, version);
+ obj.type = PKE_AUDIO_PW_OBJECT_TYPE_NODE;
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Port)) {
+ obj.type = PKE_AUDIO_PW_OBJECT_TYPE_PORT;
+ if ((str = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION)) == NULL) {
+ return;
+ }
+ if (spa_streq(str, "in")) {
+ obj.data.port.direction = PW_DIRECTION_INPUT;
+ } else if (spa_streq(str, "out")) {
+ obj.data.port.direction = PW_DIRECTION_OUTPUT;
+ } else {
+ return;
+ }
+ if ((str = spa_dict_lookup(props, PW_KEY_NODE_ID)) == NULL) {
+ return;
+ }
+ if (res = pk_stn(&obj.data.port.node, str, 0), res != PK_STN_RES_SUCCESS) {
+ fprintf(stderr, "\r\nPW_CALLBACK: failed to parse port node from str: '%s', PK_STN_RES: %i\n", str, res);
+ return;
+ }
+ if ((str = spa_dict_lookup(props, PW_KEY_PORT_ID)) == NULL) {
+ return;
+ }
+ if (res = pk_stn(&obj.data.port.id, str, 0), res != PK_STN_RES_SUCCESS) {
+ fprintf(stderr, "\r\nPW_CALLBACK: failed to parse port id from str: '%s', PK_STN_RES: %i\n", str, res);
+ return;
+ }
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Link)) {
+ obj.type = PKE_AUDIO_PW_OBJECT_TYPE_LINK;
+ if ((str = spa_dict_lookup(props, PW_KEY_LINK_OUTPUT_PORT)) == NULL) {
+ return;
+ }
+ if (res = pk_stn(&obj.data.link.output_port, str, 0), res != PK_STN_RES_SUCCESS) {
+ fprintf(stderr, "\r\nPW_CALLBACK: failed to parse link output port from str: '%s', PK_STN_RES: %i\n", str, res);
+ return;
+ }
+ if ((str = spa_dict_lookup(props, PW_KEY_LINK_INPUT_PORT)) == NULL) {
+ return;
+ }
+ if (res = pk_stn(&obj.data.link.input_port, str, 0), res != PK_STN_RES_SUCCESS) {
+ fprintf(stderr, "\r\nPW_CALLBACK: failed to parse link input port from str: '%s', PK_STN_RES: %i\n", str, res);
+ return;
+ }
+ } else if (spa_streq(type, PW_TYPE_INTERFACE_Metadata)) {
+ obj.type = PKE_AUDIO_PW_OBJECT_TYPE_METADATA;
+ if ((str = spa_dict_lookup(props, PW_KEY_METADATA_NAME)) == NULL) {
+ return;
+ }
+ if (!spa_streq(str, "default")) {
+ return;
+ }
+ pke_audio_impl_pw.metadata = (pw_metadata*)pw_registry_bind(pke_audio_impl_pw.registry, id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0);
+
+ spa_zero(pke_audio_impl_pw.metadata_listener);
+
+ pw_metadata_add_listener(pke_audio_impl_pw.metadata, &pke_audio_impl_pw.metadata_listener, &metadata_events, NULL);
+ } else {
+ return;
+ }
+
+#if 0
+ int i;
+ for (i = 0; i < props->n_items; ++i) {
+ printf("\r\n\t'%s': '%s'", props->items[i].key, props->items[i].value);
+ }
+ printf("\r\n");
+#endif
+
+ obj.id = id;
+ obj.props = pw_properties_new_dict(props);
+
+ printf("\r\nPW CALLBACK: object: id:%u type:%s/%d\n", id, type, version);
+
+ pk_arr_append_t(&pke_audio_impl_pw.pw_objects, obj);
+ return;
+}
+
+void on_registry_event_global_removal(void *data, uint32_t id) {
+ (void)data;
+ uint32_t i;
+ // printf("\r\nPW CALLBACK: object remove: id:%u ...", id);
+ if (i = pk_arr_find_first_index(&pke_audio_impl_pw.pw_objects, &id, pw_objects_find_pw_object_by_id), i != 0xFFFFFFFF)
+ {
+ if (pke_audio_impl_pw.pw_objects[i].type != PKE_AUDIO_PW_OBJECT_TYPE_LINK) {
+ pke_audio_impl_pw.is_needing_output_remapped = true;
+ }
+ if (pke_audio_impl_pw.pw_objects[i].id == pke_audio_impl_pw.default_sink_id) {
+ // we just lost our target audio device
+ pke_audio_impl_pw.default_sink_id = 0;
+ pke_audio_impl_pw.default_sink_name[0] = '\0';
+ }
+ pk_arr_remove_at(&pke_audio_impl_pw.pw_objects, i);
+ // printf(" removed.\n");
+ }
+ return;
+}
diff --git a/src/audio-impl-pw.hpp b/src/audio-impl-pw.hpp
new file mode 100644
index 0000000..c371973
--- /dev/null
+++ b/src/audio-impl-pw.hpp
@@ -0,0 +1,71 @@
+#ifndef PKE_AUDIO_IMPL_PW_HPP
+#define PKE_AUDIO_IMPL_PW_HPP
+
+#include <atomic>
+
+#include "pk.h"
+
+// 2025-06-24 spa does not have c++ headers so we have to use c headers.
+// They throw a ton of pedantic warnings that spam the build output.
+// Might be too large of a swath, but none of this is my code anyway.
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wpedantic"
+#include <pipewire/pipewire.h>
+#include <pipewire/extensions/metadata.h>
+#include <spa/param/audio/format-utils.h>
+#include <spa/param/audio/raw.h>
+#include <spa/param/param.h>
+#include <spa/utils/string.h>
+#pragma GCC diagnostic pop
+
+#define PKE_AUDIO_IMPL_POD_BUFFER_LEN 1024
+enum pke_audio_pw_object_type : uint8_t {
+ PKE_AUDIO_PW_OBJECT_TYPE_ANY,
+ PKE_AUDIO_PW_OBJECT_TYPE_NODE,
+ PKE_AUDIO_PW_OBJECT_TYPE_PORT,
+ PKE_AUDIO_PW_OBJECT_TYPE_LINK,
+ PKE_AUDIO_PW_OBJECT_TYPE_METADATA,
+};
+struct pke_audio_pw_object {
+ union {
+ struct {
+ enum pw_direction direction;
+ uint32_t node;
+ uint32_t id;
+ } port;
+ struct {
+ uint32_t output_port;
+ uint32_t input_port;
+ uint32_t dummy;
+ } link;
+ } data;
+ uint32_t id;
+ enum pke_audio_pw_object_type type;
+ struct pw_properties *props;
+};
+struct pke_audio_implementation_pipewire {
+ pk_membucket *bkt;
+ pw_thread_loop *loop;
+ uint8_t *pod_buffer;
+ pw_stream *stream;
+ pw_core *core;
+ pw_registry *registry;
+ spa_hook registry_listener;
+ pw_metadata *metadata;
+ spa_hook metadata_listener;
+ pk_arr_t<pke_audio_pw_object> pw_objects;
+ pk_arr_t<void*> created_objects;
+ char *default_sink_name;
+ spa_audio_info format;
+ uint32_t default_sink_id;
+ std::atomic_bool is_needing_output_remapped;
+};
+extern struct pke_audio_implementation_pipewire pke_audio_impl_pw;
+void pke_audio_pw_init();
+void pke_audio_pw_teardown();
+void on_pipewire_process(void *user_data);
+void on_pipewire_stream_param_changed(void *, uint32_t id, const struct spa_pod *param);
+void on_registry_event_global(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props);
+void on_registry_event_global_removal(void *data, uint32_t id);
+
+#endif /* PKE_AUDIO_IMPL_PW_HPP */
diff --git a/src/audio-types.hpp b/src/audio-types.hpp
new file mode 100644
index 0000000..84ab155
--- /dev/null
+++ b/src/audio-types.hpp
@@ -0,0 +1,36 @@
+#ifndef PKE_AUDIO_TYPES_INTERNAL_HPP
+#define PKE_AUDIO_TYPES_INTERNAL_HPP
+
+#include "asset-manager.hpp"
+#include "pk.h"
+
+#define PKE_AUDIO_MAX_CONCURRENT_COUNT 8
+
+TypeSafeInt_constexpr(pke_audio_flags, uint8_t, 0xFF);
+TypeSafeInt_constexpr(pke_audio_source, uint8_t, 0x0F);
+
+const pke_audio_flags pke_audio_flag_none = pke_audio_flags{0x00};
+
+const pke_audio_source pke_audio_source_music = pke_audio_source{0x00};
+const pke_audio_source pke_audio_source_ambient = pke_audio_source{0x01};
+const pke_audio_source pke_audio_source_voices = pke_audio_source{0x02};
+const pke_audio_source pke_audio_source_sfx = pke_audio_source{0x03};
+
+struct pke_audio_obj {
+ AssetHandle handle; // key
+ pke_audio_source source; // key
+ pke_audio_flags flags[PKE_AUDIO_MAX_CONCURRENT_COUNT];
+ uint32_t play_heads[PKE_AUDIO_MAX_CONCURRENT_COUNT];
+ uint8_t play_count;
+};
+
+struct pke_audio_master {
+ pk_arr_t<pke_audio_obj> playing_objects;
+ float source_volumes[pke_audio_source_T_MAX];
+ uint32_t channel_count; // mono, stereo, 7.1
+ std::mutex mtx_buffer;
+};
+
+extern struct pke_audio_master pke_audio_mstr;
+
+#endif /* PKE_AUDIO_TYPES_INTERNAL_HPP */
diff --git a/src/audio.cpp b/src/audio.cpp
new file mode 100644
index 0000000..002d1be
--- /dev/null
+++ b/src/audio.cpp
@@ -0,0 +1,32 @@
+#define PKE_AUDIO_IMPL_PIPEWIRE
+
+#include "audio.hpp"
+
+#include "audio-impl-pw.hpp"
+
+struct pke_audio_master pke_audio_mstr{};
+
+void pke_audio_init() {
+ pke_audio_mstr.channel_count = 2;
+#ifdef PKE_AUDIO_IMPL_PIPEWIRE
+ pke_audio_pw_init();
+#endif
+}
+void pke_audio_teardown() {
+#ifdef PKE_AUDIO_IMPL_PIPEWIRE
+ pke_audio_pw_teardown();
+#endif
+}
+void pke_audio_tick() {
+}
+
+float pke_audio_get_volume(pke_audio_source source) {
+ return pke_audio_mstr.source_volumes[static_cast<pke_audio_source_T>(source)];
+}
+
+void pke_audio_set_volume(pke_audio_source source, float volume) {
+ pke_audio_mstr.source_volumes[static_cast<pke_audio_source_T>(source)] = volume;
+}
+
+void pke_audio_play(AssetHandle handle, pke_audio_source audio_source, pke_audio_flags flags);
+void pke_audio_stop_all();
diff --git a/src/audio.hpp b/src/audio.hpp
new file mode 100644
index 0000000..7ba0f1d
--- /dev/null
+++ b/src/audio.hpp
@@ -0,0 +1,18 @@
+#ifndef PKE_AUDIO_HPP
+#define PKE_AUDIO_HPP
+
+#include "audio-types.hpp"
+
+#include "asset-manager.hpp"
+
+void pke_audio_init();
+void pke_audio_teardown();
+void pke_audio_tick();
+
+float pke_audio_get_volume(pke_audio_source source);
+void pke_audio_set_volume(pke_audio_source source, float volume);
+
+void pke_audio_play(AssetHandle handle, pke_audio_source audio_source, pke_audio_flags flags = pke_audio_flag_none);
+void pke_audio_stop_all();
+
+#endif /* PKE_AUDIO_HPP */
diff --git a/src/game.cpp b/src/game.cpp
index 67a9600..25473ed 100644
--- a/src/game.cpp
+++ b/src/game.cpp
@@ -1,6 +1,7 @@
#include "game.hpp"
+#include "audio.hpp"
#include "camera.hpp"
#include "components.hpp"
#include "ecs.hpp"
@@ -122,6 +123,7 @@ void Game_Main(PKEWindowProperties windowProps, const char *executablePath) {
PkeThreads_Init();
AM_Init();
ECS_Init();
+ pke_audio_init();
Physics_Init();
PkeCamera_Init();
pke_level_init();
@@ -310,6 +312,7 @@ GAME_SHUTDOWN:
Physics_Teardown();
ECS_Teardown();
DestroyWindow();
+ pke_audio_teardown();
AM_DebugPrint();
AM_Teardown();
PkeThreads_Teardown();