diff --git a/.gitmodules b/.gitmodules index f8e219f..2878dde 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "bin/sokol-tools-bin"] path = bin/sokol-tools-bin url = https://github.com/floooh/sokol-tools-bin.git +[submodule "3rd/miniaudio"] + path = 3rd/miniaudio + url = https://github.com/mackron/miniaudio.git diff --git a/3rd/miniaudio b/3rd/miniaudio new file mode 160000 index 0000000..9634bed --- /dev/null +++ b/3rd/miniaudio @@ -0,0 +1 @@ +Subproject commit 9634bedb5b5a2ca38c1ee7108a9358a4e233f14d diff --git a/Makefile b/Makefile index 3af379d..b50c8e6 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,7 @@ all : $(BIN)/$(APPNAME) 3RDINC=-I3rd YOGAINC=-I3rd/yoga +MINIAUDIOINC=-I3rd/miniaudio LUAINC=-I3rd/lua LUASRC:=$(wildcard 3rd/lua/*.c 3rd/lua/*.h) @@ -125,7 +126,7 @@ $(BUILD)/soluna_entry.o : src/entry.c src/version.h $(COMPILE_C) $(LUAINC) $(3RDINC) -DSOLUNA_HASH_VERSION=\"$(VERSION)\" $(BUILD)/soluna_%.o : src/%.c - $(COMPILE_C) $(LUAINC) $(3RDINC) $(SHADERINC) $(YOGAINC) $(ZLIBINC) + $(COMPILE_C) $(LUAINC) $(3RDINC) $(SHADERINC) $(YOGAINC) $(ZLIBINC) $(MINIAUDIOINC) $(BUILD)/platform_%.o : src/platform/windows/%.c $(COMPILE_C) $(LUAINC) $(3RDINC) $(SHADERINC) $(YOGAINC) $(ZLIBINC) -Isrc diff --git a/asset/sounds.dl b/asset/sounds.dl new file mode 100644 index 0000000..b3cf841 --- /dev/null +++ b/asset/sounds.dl @@ -0,0 +1,3 @@ +-- +name : bloop +filename : asset/sounds/bloop_x.wav diff --git a/asset/sounds/bloop_x.wav b/asset/sounds/bloop_x.wav new file mode 100644 index 0000000..85bdb2f Binary files /dev/null and b/asset/sounds/bloop_x.wav differ diff --git a/clibs/soluna/make.lua b/clibs/soluna/make.lua index ae29550..bef6293 100644 --- a/clibs/soluna/make.lua +++ b/clibs/soluna/make.lua @@ -39,10 +39,11 @@ lm:source_set "soluna_src" { includes = { "build", "src", - "3rd/lua", "3rd", + "3rd/lua", "3rd/yoga", "3rd/zlib", + "3rd/miniaudio", }, clang = { sources = lm.os == "macos" and { @@ -52,6 +53,7 @@ lm:source_set "soluna_src" { "-x objective-c", }, frameworks = lm.os == "macos" and { + "AudioToolbox", "IOKit", "CoreText", "CoreFoundation", diff --git a/make.lua b/make.lua index bc23894..84d97c8 100644 --- a/make.lua +++ b/make.lua @@ -81,7 +81,7 @@ lm:conf { }, }, emcc = { - c = "c11", + c = "gnu11", flags = { "-Wall", "-pthread", diff --git a/src/audio.c b/src/audio.c new file mode 100644 index 0000000..04c8fd0 --- /dev/null +++ b/src/audio.c @@ -0,0 +1,215 @@ +#include +#include + +#include "zipreader.h" + +#define MA_NO_WIN32_FILEIO +#define MA_NO_MP3 +#define MA_NO_FLAC +#define MINIAUDIO_IMPLEMENTATION +#include "miniaudio.h" + +FILE * fopen_utf8(const char *filename, const char *mode); + +static ma_result +vfs_open_local(ma_vfs* pVFS, const char* pFilePath, ma_uint32 openMode, ma_vfs_file* pFile) { + FILE* pFileStd; + const char* pOpenModeStr; + + MA_ASSERT(pFilePath != NULL); + MA_ASSERT(openMode != 0); + MA_ASSERT(pFile != NULL); + + (void)pVFS; + + if ((openMode & MA_OPEN_MODE_READ) != 0) { + if ((openMode & MA_OPEN_MODE_WRITE) != 0) { + pOpenModeStr = "r+"; + } else { + pOpenModeStr = "rb"; + } + } else { + pOpenModeStr = "wb"; + } + + pFileStd = fopen_utf8(pFilePath, pOpenModeStr); + + if (pFileStd == NULL) { + return MA_ERROR; + } + + *pFile = pFileStd; + + return MA_SUCCESS; +} + +struct custom_vfs { + ma_default_vfs base; + struct zipreader_name *zipnames; +}; + +struct custom_engine { + struct ma_engine engine; + struct ma_resource_manager rm; + struct custom_vfs vfs; +}; + +static ma_result +zr_open(ma_vfs* pVFS, const char* pFilePath, ma_uint32 openMode, ma_vfs_file* pFile) { + struct custom_vfs *vfs = (struct custom_vfs *)pVFS; + if (openMode != MA_OPEN_MODE_READ) + return MA_NOT_IMPLEMENTED; + zipreader_file zf = zipreader_open(vfs->zipnames, pFilePath); + if (zf == NULL) { + return MA_ERROR; + } + *pFile = (ma_vfs_file)zf; + return MA_SUCCESS; +} + +static ma_result +zr_close(ma_vfs* pVFS, ma_vfs_file file) { + (void)pVFS; + zipreader_close((zipreader_file)file); + return MA_SUCCESS; +} + +static ma_result +zr_read(ma_vfs* pVFS, ma_vfs_file file, void* pDst, size_t sizeInBytes, size_t* pBytesRead) { + (void)pVFS; + int bytes = (int)sizeInBytes; + if (bytes!= sizeInBytes || bytes < 0) + return MA_OUT_OF_RANGE; + int rd = zipreader_read((zipreader_file)file, pDst, bytes); + if (rd < 0) + return MA_IO_ERROR; + *pBytesRead = rd; + return MA_SUCCESS; +} + +static ma_result +zr_seek(ma_vfs* pVFS, ma_vfs_file file, ma_int64 offset, ma_seek_origin origin) { + (void)pVFS; + int whence; + switch (origin) { + case ma_seek_origin_start : + whence = SEEK_SET; + break; + case ma_seek_origin_current : + whence = SEEK_CUR; + break; + case ma_seek_origin_end : + whence = SEEK_END; + break; + default : + return MA_INVALID_ARGS; + } + if (zipreader_seek((zipreader_file)file, offset, whence) != 0) { + return MA_ERROR; + } + return MA_SUCCESS; +} + +static ma_result +zr_tell(ma_vfs* pVFS, ma_vfs_file file, ma_int64* pCursor) { + (void)pVFS; + *pCursor = zipreader_tell((zipreader_file)file); + if (*pCursor < 0) + return MA_ERROR; + return MA_SUCCESS; +} + +static ma_result +zr_info(ma_vfs* pVFS, ma_vfs_file file, ma_file_info* pInfo) { + (void)pVFS; + pInfo->sizeInBytes = zipreader_size((zipreader_file)file); + return MA_SUCCESS; +} + +static int +laudio_init(lua_State *L) { + lua_settop(L, 1); + struct custom_engine *e = (struct custom_engine *)lua_newuserdatauv(L, sizeof(*e), 1); + + ma_default_vfs_init(&e->vfs.base, NULL); + e->vfs.base.cb.onOpen = vfs_open_local; + e->vfs.zipnames = NULL; + + if (lua_isuserdata(L, 1)) { + e->vfs.zipnames = lua_touserdata(L, 1); + e->vfs.base.cb.onOpen = zr_open; + e->vfs.base.cb.onOpenW = NULL; + e->vfs.base.cb.onClose = zr_close; + e->vfs.base.cb.onRead = zr_read; + e->vfs.base.cb.onWrite = NULL; + e->vfs.base.cb.onSeek = zr_seek; + e->vfs.base.cb.onTell = zr_tell; + e->vfs.base.cb.onInfo = zr_info; + lua_pushvalue(L, 1); + lua_setiuservalue(L, -2, 1); + } + + ma_resource_manager_config config = ma_resource_manager_config_init(); + config.pVFS = &e->vfs; + + ma_result r = ma_resource_manager_init(&config, &e->rm); + if (r != MA_SUCCESS) { + return luaL_error(L, "ma_resource_manager_init() error : %s", ma_result_description(r)); + } + + ma_engine_config ec = ma_engine_config_init(); + ec.pResourceManager = &e->rm; + r = ma_engine_init(&ec, &e->engine); + if (r != MA_SUCCESS) { + return luaL_error(L, "ma_engine_init() error : %s", ma_result_description(r)); + } + lua_pushlightuserdata(L, (void *)e); + + return 2; +} + +static int +laudio_deinit(lua_State *L) { + luaL_checktype(L, 1, LUA_TUSERDATA); + ma_engine *engine = (ma_engine *)lua_touserdata(L, 1); + ma_engine_uninit(engine); + + return 0; +} + +/* +// todo : call ma_sound_init_from_file() + +static int +laudio_load(lua_State *L) { + return 0; +} + +static int +laudio_unload(lua_State *L) { + return 0; +} +*/ + +static int +laudio_play(lua_State *L) { + luaL_checktype(L, 1, LUA_TLIGHTUSERDATA); + ma_engine *engine = (ma_engine *)lua_touserdata(L, 1); + const char *filename = luaL_checkstring(L, 2); + + ma_engine_play_sound(engine, filename, NULL); + return 0; +} + +int +luaopen_soluna_audio(lua_State *L) { + luaL_checkversion(L); + luaL_Reg l[] = { + { "init", laudio_init }, + { "deinit", laudio_deinit }, + { "play", laudio_play }, + { NULL, NULL }, + }; + luaL_newlib(L, l); + return 1; +} diff --git a/src/embedlua.c b/src/embedlua.c index 3db97d7..de9c046 100644 --- a/src/embedlua.c +++ b/src/embedlua.c @@ -21,6 +21,7 @@ #include "util.lua.h" #include "coroutine.lua.h" #include "packageloader.lua.h" +#include "audio.lua.h" #include "lua.h" #include "lauxlib.h" @@ -84,6 +85,7 @@ luaopen_embedsource(lua_State *L) { REG_SOURCE(render) REG_SOURCE(gamepad) REG_SOURCE(settings) + REG_SOURCE(audio) lua_setfield(L, -2, "service"); lua_newtable(L); // data list diff --git a/src/lualib/main.lua b/src/lualib/main.lua index 74402e0..11c0aef 100644 --- a/src/lualib/main.lua +++ b/src/lualib/main.lua @@ -180,6 +180,10 @@ function api.start(app) name = "loader", unique = true, }, + { + name = "audio", + unique = true, + }, }, } end diff --git a/src/lualib/soluna.lua b/src/lualib/soluna.lua index 5d18fa5..773c8b5 100644 --- a/src/lualib/soluna.lua +++ b/src/lualib/soluna.lua @@ -76,6 +76,19 @@ function soluna.load_sprites(filename) return sprites end + +local audio_service, audio_sounds + +function soluna.load_sounds(filename) + audio_service = audio_service or ltask.uniqueservice "audio" + audio_sounds = ltask.call(audio_service, "init", filename) + return audio_sounds +end + +function soluna.play_sound(name) + ltask.send(audio_service, true, audio_sounds[name]) +end + function soluna.preload(spr) local loader = ltask.uniqueservice "loader" if #spr == 0 then diff --git a/src/luamods.c b/src/luamods.c index 3e52c9a..65dd451 100644 --- a/src/luamods.c +++ b/src/luamods.c @@ -34,6 +34,7 @@ int luaopen_url(lua_State *L); int luaopen_skynet_crypt(lua_State *L); int luaopen_zip(lua_State *L); int luaopen_extlua(lua_State *L); +int luaopen_soluna_audio(lua_State *L); void soluna_embed(lua_State* L) { static const luaL_Reg modules[] = { @@ -69,6 +70,7 @@ void soluna_embed(lua_State* L) { { "soluna.crypt", luaopen_skynet_crypt }, { "soluna.zip", luaopen_zip }, { "soluna.extlua", luaopen_extlua }, + { "soluna.audio", luaopen_soluna_audio }, { NULL, NULL }, }; diff --git a/src/service/audio.lua b/src/service/audio.lua new file mode 100644 index 0000000..27f2526 --- /dev/null +++ b/src/service/audio.lua @@ -0,0 +1,65 @@ +local ltask = require "ltask" +local audio = require "soluna.audio" +local file = require "soluna.file" +local datalist = require "soluna.datalist" + +global print, assert, setmetatable, tostring, error, ipairs + +local DEVICE, BANK + +local api = {} + +local play = audio.play + +-- play +api[true] = function(id) + play(DEVICE, BANK[id]) +end + +local S = {} + +global type, tonumber, error, assert, ipairs, print, pairs + +for k in pairs(api) do + S[k] = function() + error "Init audio first" + end +end + +local M = {} + +local function load_bundle(filename) + local b = datalist.parse(file.load(filename)) + local bank = {} + local map = {} + for i, v in ipairs(b) do + bank[i] = assert(v.filename) + map[assert(v.name)] = i + end + return bank, map +end + +function S.init(filename) + assert(DEVICE == nil) + DEVICE = false + local bank, ret = load_bundle(filename) + local d = ltask.call(ltask.queryservice "render", "audio_engine") + if not d then + return {} + else + -- todo : load file list + BANK = bank + DEVICE = d + local inject = ltask.dispatch() + for k, v in pairs(api) do + inject[k] = v + end + return ret + end +end + +function S.deinit() + DEVICE = nil +end + +return S diff --git a/src/service/render.lua b/src/service/render.lua index 0ccebc7..13045ab 100644 --- a/src/service/render.lua +++ b/src/service/render.lua @@ -228,6 +228,21 @@ end S.register_batch = assert(batch.register) S.submit_batch = assert(batch.submit) +local audio_engine + +function S.audio_engine() + if audio_engine then + local from = ltask.current_session().from + function audio_engine.quit() + local audio = require "soluna.audio" + ltask.call(from, "deinit") + audio.deinit(audio_engine.engine) + audio_engine = nil + end + return audio_engine.ptr + end +end + function S.quit() local workers = {} for _, v in ipairs(batch) do @@ -247,6 +262,9 @@ function S.quit() ltask.call(addr, "quit") end font.shutdown() + if audio_engine and audio_engine.quit then + audio_engine.quit() + end end function S.load_sprites(name) @@ -268,6 +286,13 @@ function S.load_sprites(name) end local function render_init(arg) + local audio = require "soluna.audio" + local engine, ptr = audio.init() + audio_engine = { + engine = engine, + ptr = ptr, + } + font.init() local texture_size = setting.texture_size diff --git a/test/audio.lua b/test/audio.lua new file mode 100644 index 0000000..b2ba0ff --- /dev/null +++ b/test/audio.lua @@ -0,0 +1,14 @@ +local soluna = require "soluna" + +soluna.load_sounds "asset/sounds.dl" +soluna.set_window_title "Soluna sound sample" + +local callback = {} + +soluna.play_sound "bloop" + +function callback.frame(count) +end + +return callback +