From 81d21d6e8b5c4f3437bf78f318088d75e81f3b19 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 11 Oct 2025 00:11:25 +0300 Subject: [PATCH 01/28] refactor lua usertypes --- src/logic/scripting/lua/libs/libbjson.cpp | 1 - .../scripting/lua/libs/libgeneration.cpp | 2 +- src/logic/scripting/lua/libs/libgui.cpp | 1 + src/logic/scripting/lua/libs/libutf8.cpp | 1 - src/logic/scripting/lua/lua_commons.hpp | 5 + src/logic/scripting/lua/lua_custom_types.hpp | 134 ------------------ src/logic/scripting/lua/lua_engine.cpp | 5 +- src/logic/scripting/lua/lua_util.hpp | 1 - .../lua/usertypes/lua_type_canvas.cpp | 5 +- .../lua/usertypes/lua_type_canvas.hpp | 46 ++++++ .../lua/usertypes/lua_type_heightmap.cpp | 13 +- .../lua/usertypes/lua_type_heightmap.hpp | 44 ++++++ .../lua/usertypes/lua_type_random.cpp | 2 +- .../lua/usertypes/lua_type_random.hpp | 23 +++ .../lua/usertypes/lua_type_voxelfragment.cpp | 3 +- .../lua/usertypes/lua_type_voxelfragment.hpp | 29 ++++ src/logic/scripting/scripting.cpp | 1 - .../scripting/scripting_world_generation.cpp | 2 +- 18 files changed, 166 insertions(+), 152 deletions(-) delete mode 100644 src/logic/scripting/lua/lua_custom_types.hpp create mode 100644 src/logic/scripting/lua/usertypes/lua_type_canvas.hpp create mode 100644 src/logic/scripting/lua/usertypes/lua_type_heightmap.hpp create mode 100644 src/logic/scripting/lua/usertypes/lua_type_random.hpp create mode 100644 src/logic/scripting/lua/usertypes/lua_type_voxelfragment.hpp diff --git a/src/logic/scripting/lua/libs/libbjson.cpp b/src/logic/scripting/lua/libs/libbjson.cpp index 71498dbc..8c28f04e 100644 --- a/src/logic/scripting/lua/libs/libbjson.cpp +++ b/src/logic/scripting/lua/libs/libbjson.cpp @@ -1,7 +1,6 @@ #include "coders/binary_json.hpp" #include "api_lua.hpp" #include "util/Buffer.hpp" -#include "../lua_custom_types.hpp" static int l_tobytes(lua::State* L) { auto value = lua::tovalue(L, 1); diff --git a/src/logic/scripting/lua/libs/libgeneration.cpp b/src/logic/scripting/lua/libs/libgeneration.cpp index e215b4fb..15bdad5c 100644 --- a/src/logic/scripting/lua/libs/libgeneration.cpp +++ b/src/logic/scripting/lua/libs/libgeneration.cpp @@ -9,7 +9,7 @@ #include "content/Content.hpp" #include "content/ContentControl.hpp" #include "engine/Engine.hpp" -#include "../lua_custom_types.hpp" +#include "../usertypes/lua_type_voxelfragment.hpp" using namespace scripting; diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index 6395dc5b..fc3732a1 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -22,6 +22,7 @@ #include "items/Inventories.hpp" #include "util/stringutil.hpp" #include "world/Level.hpp" +#include "../usertypes/lua_type_canvas.hpp" using namespace gui; using namespace scripting; diff --git a/src/logic/scripting/lua/libs/libutf8.cpp b/src/logic/scripting/lua/libs/libutf8.cpp index ec746caa..e6538c2e 100644 --- a/src/logic/scripting/lua/libs/libutf8.cpp +++ b/src/logic/scripting/lua/libs/libutf8.cpp @@ -3,7 +3,6 @@ #include #include -#include "../lua_custom_types.hpp" #include "util/stringutil.hpp" static int l_tobytes(lua::State* L) { diff --git a/src/logic/scripting/lua/lua_commons.hpp b/src/logic/scripting/lua/lua_commons.hpp index f272dbf0..bb3163c8 100644 --- a/src/logic/scripting/lua/lua_commons.hpp +++ b/src/logic/scripting/lua/lua_commons.hpp @@ -48,4 +48,9 @@ namespace lua { void log_error(const std::string& text); + class Userdata { + public: + virtual ~Userdata() {}; + virtual const std::string& getTypeName() const = 0; + }; } diff --git a/src/logic/scripting/lua/lua_custom_types.hpp b/src/logic/scripting/lua/lua_custom_types.hpp deleted file mode 100644 index c17a78bc..00000000 --- a/src/logic/scripting/lua/lua_custom_types.hpp +++ /dev/null @@ -1,134 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "lua_commons.hpp" - -struct fnl_state; -class Heightmap; -class VoxelFragment; -class Texture; -class ImageData; - -namespace lua { - class Userdata { - public: - virtual ~Userdata() {}; - virtual const std::string& getTypeName() const = 0; - }; - - class LuaHeightmap : public Userdata { - std::shared_ptr map; - std::unique_ptr noise; - public: - LuaHeightmap(const std::shared_ptr& map); - LuaHeightmap(uint width, uint height); - - virtual ~LuaHeightmap(); - - uint getWidth() const; - - uint getHeight() const; - - float* getValues(); - - const float* getValues() const; - - const std::string& getTypeName() const override { - return TYPENAME; - } - - const std::shared_ptr& getHeightmap() const { - return map; - } - - fnl_state* getNoise() { - return noise.get(); - } - - void setSeed(int64_t seed); - - static int createMetatable(lua::State*); - inline static std::string TYPENAME = "Heightmap"; - }; - static_assert(!std::is_abstract()); - - class LuaVoxelFragment : public Userdata { - std::array, 4> fragmentVariants; - public: - LuaVoxelFragment( - std::array, 4> fragmentVariants - ); - - virtual ~LuaVoxelFragment(); - - std::shared_ptr getFragment(size_t rotation) const { - return fragmentVariants.at(rotation & 0b11); - } - - const std::string& getTypeName() const override { - return TYPENAME; - } - - static int createMetatable(lua::State*); - inline static std::string TYPENAME = "VoxelFragment"; - }; - static_assert(!std::is_abstract()); - - class LuaCanvas : public Userdata { - public: - explicit LuaCanvas( - std::shared_ptr inTexture, - std::shared_ptr inData - ); - ~LuaCanvas() override = default; - - const std::string& getTypeName() const override { - return TYPENAME; - } - - [[nodiscard]] auto& texture() const { - return *mTexture; - } - - [[nodiscard]] auto& data() const { - return *mData; - } - - [[nodiscard]] bool hasTexture() const { - return mTexture != nullptr; - } - - auto shareTexture() const { - return mTexture; - } - - void createTexture(); - - static int createMetatable(lua::State*); - inline static std::string TYPENAME = "Canvas"; - private: - std::shared_ptr mTexture; // nullable - std::shared_ptr mData; - }; - static_assert(!std::is_abstract()); - - class LuaRandom : public Userdata { - public: - std::mt19937 rng; - - explicit LuaRandom(uint64_t seed) : rng(seed) {} - virtual ~LuaRandom() override = default; - - const std::string& getTypeName() const override { - return TYPENAME; - } - - static int createMetatable(lua::State*); - inline static std::string TYPENAME = "__vc_Random"; - }; - static_assert(!std::is_abstract()); -} diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index 08d2eb85..05adb354 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -8,7 +8,10 @@ #include "debug/Logger.hpp" #include "util/stringutil.hpp" #include "libs/api_lua.hpp" -#include "lua_custom_types.hpp" +#include "usertypes/lua_type_heightmap.hpp" +#include "usertypes/lua_type_voxelfragment.hpp" +#include "usertypes/lua_type_canvas.hpp" +#include "usertypes/lua_type_random.hpp" #include "engine/Engine.hpp" static debug::Logger logger("lua-state"); diff --git a/src/logic/scripting/lua/lua_util.hpp b/src/logic/scripting/lua/lua_util.hpp index a8fb246e..29f25f34 100644 --- a/src/logic/scripting/lua/lua_util.hpp +++ b/src/logic/scripting/lua/lua_util.hpp @@ -6,7 +6,6 @@ #include #include "data/dv.hpp" -#include "lua_custom_types.hpp" #include "lua_wrapper.hpp" #define GLM_ENABLE_EXPERIMENTAL #include diff --git a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp index ec086904..3343fe71 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp @@ -1,12 +1,13 @@ -#include +#include "lua_type_canvas.hpp" #include "graphics/core/ImageData.hpp" #include "graphics/core/Texture.hpp" -#include "logic/scripting/lua/lua_custom_types.hpp" #include "logic/scripting/lua/lua_util.hpp" #include "engine/Engine.hpp" #include "assets/Assets.hpp" +#include + using namespace lua; LuaCanvas::LuaCanvas( diff --git a/src/logic/scripting/lua/usertypes/lua_type_canvas.hpp b/src/logic/scripting/lua/usertypes/lua_type_canvas.hpp new file mode 100644 index 00000000..43fbbd10 --- /dev/null +++ b/src/logic/scripting/lua/usertypes/lua_type_canvas.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "../lua_commons.hpp" + +class Texture; +class ImageData; + +namespace lua { + class LuaCanvas : public Userdata { + public: + explicit LuaCanvas( + std::shared_ptr inTexture, + std::shared_ptr inData + ); + ~LuaCanvas() override = default; + + const std::string& getTypeName() const override { + return TYPENAME; + } + + [[nodiscard]] auto& texture() const { + return *mTexture; + } + + [[nodiscard]] auto& data() const { + return *mData; + } + + [[nodiscard]] bool hasTexture() const { + return mTexture != nullptr; + } + + auto shareTexture() const { + return mTexture; + } + + void createTexture(); + + static int createMetatable(lua::State*); + inline static std::string TYPENAME = "Canvas"; + private: + std::shared_ptr mTexture; // nullable + std::shared_ptr mData; + }; + static_assert(!std::is_abstract()); +} diff --git a/src/logic/scripting/lua/usertypes/lua_type_heightmap.cpp b/src/logic/scripting/lua/usertypes/lua_type_heightmap.cpp index 290e1345..4e2f05a9 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_heightmap.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_heightmap.cpp @@ -1,9 +1,4 @@ -#include "../lua_custom_types.hpp" - -#include -#include -#include -#include +#include "lua_type_heightmap.hpp" #include "util/functional_util.hpp" #define FNL_IMPL @@ -14,6 +9,12 @@ #include "maths/Heightmap.hpp" #include "engine/Engine.hpp" #include "../lua_util.hpp" +#include "lua_type_heightmap.hpp" + +#include +#include +#include +#include using namespace lua; diff --git a/src/logic/scripting/lua/usertypes/lua_type_heightmap.hpp b/src/logic/scripting/lua/usertypes/lua_type_heightmap.hpp new file mode 100644 index 00000000..4109be67 --- /dev/null +++ b/src/logic/scripting/lua/usertypes/lua_type_heightmap.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "../lua_commons.hpp" + +struct fnl_state; +class Heightmap; + +namespace lua { + class LuaHeightmap : public Userdata { + std::shared_ptr map; + std::unique_ptr noise; + public: + LuaHeightmap(const std::shared_ptr& map); + LuaHeightmap(uint width, uint height); + + virtual ~LuaHeightmap(); + + uint getWidth() const; + + uint getHeight() const; + + float* getValues(); + + const float* getValues() const; + + const std::string& getTypeName() const override { + return TYPENAME; + } + + const std::shared_ptr& getHeightmap() const { + return map; + } + + fnl_state* getNoise() { + return noise.get(); + } + + void setSeed(int64_t seed); + + static int createMetatable(lua::State*); + inline static std::string TYPENAME = "Heightmap"; + }; + static_assert(!std::is_abstract()); +} diff --git a/src/logic/scripting/lua/usertypes/lua_type_random.cpp b/src/logic/scripting/lua/usertypes/lua_type_random.cpp index 99177056..bdbe4a1a 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_random.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_random.cpp @@ -1,5 +1,5 @@ -#include "../lua_custom_types.hpp" #include "../lua_util.hpp" +#include "lua_type_random.hpp" #include diff --git a/src/logic/scripting/lua/usertypes/lua_type_random.hpp b/src/logic/scripting/lua/usertypes/lua_type_random.hpp new file mode 100644 index 00000000..06ab24f8 --- /dev/null +++ b/src/logic/scripting/lua/usertypes/lua_type_random.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "../lua_commons.hpp" + +#include + +namespace lua { + class LuaRandom : public Userdata { + public: + std::mt19937 rng; + + explicit LuaRandom(uint64_t seed) : rng(seed) {} + virtual ~LuaRandom() override = default; + + const std::string& getTypeName() const override { + return TYPENAME; + } + + static int createMetatable(lua::State*); + inline static std::string TYPENAME = "__vc_Random"; + }; + static_assert(!std::is_abstract()); +} diff --git a/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.cpp b/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.cpp index 271a2a5a..17cf37d1 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.cpp @@ -1,7 +1,6 @@ -#include "../lua_custom_types.hpp" +#include "lua_type_voxelfragment.hpp" #include "../lua_util.hpp" - #include "world/generator/VoxelFragment.hpp" #include "util/stringutil.hpp" #include "world/Level.hpp" diff --git a/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.hpp b/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.hpp new file mode 100644 index 00000000..789076cc --- /dev/null +++ b/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "../lua_commons.hpp" + +class VoxelFragment; + +namespace lua { + class LuaVoxelFragment : public Userdata { + std::array, 4> fragmentVariants; + public: + LuaVoxelFragment( + std::array, 4> fragmentVariants + ); + + virtual ~LuaVoxelFragment(); + + std::shared_ptr getFragment(size_t rotation) const { + return fragmentVariants.at(rotation & 0b11); + } + + const std::string& getTypeName() const override { + return TYPENAME; + } + + static int createMetatable(lua::State*); + inline static std::string TYPENAME = "VoxelFragment"; + }; + static_assert(!std::is_abstract()); +} diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 997b5f82..a4eccbe3 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -17,7 +17,6 @@ #include "logic/BlocksController.hpp" #include "logic/LevelController.hpp" #include "lua/lua_engine.hpp" -#include "lua/lua_custom_types.hpp" #include "maths/Heightmap.hpp" #include "objects/Player.hpp" #include "util/stringutil.hpp" diff --git a/src/logic/scripting/scripting_world_generation.cpp b/src/logic/scripting/scripting_world_generation.cpp index bf6c1dd1..3199b46c 100644 --- a/src/logic/scripting/scripting_world_generation.cpp +++ b/src/logic/scripting/scripting_world_generation.cpp @@ -6,7 +6,7 @@ #include "scripting_commons.hpp" #include "typedefs.hpp" #include "lua/lua_engine.hpp" -#include "lua/lua_custom_types.hpp" +#include "lua/usertypes/lua_type_heightmap.hpp" #include "content/Content.hpp" #include "voxels/Block.hpp" #include "voxels/Chunk.hpp" From 53b89cdb8e59162ad914d3cab30e5f771de0aaf2 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 11 Oct 2025 00:34:16 +0300 Subject: [PATCH 02/28] fix windows build --- src/logic/scripting/lua/usertypes/lua_type_voxelfragment.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.hpp b/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.hpp index 789076cc..21c41c04 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.hpp +++ b/src/logic/scripting/lua/usertypes/lua_type_voxelfragment.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "../lua_commons.hpp" class VoxelFragment; From 4b19c29a29621853a858137e51ea2814e784bf83 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 13 Oct 2025 23:34:56 +0300 Subject: [PATCH 03/28] add util::span --- src/util/span.hpp | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/util/span.hpp diff --git a/src/util/span.hpp b/src/util/span.hpp new file mode 100644 index 00000000..a9ab79b2 --- /dev/null +++ b/src/util/span.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +namespace util { + template + class span { + public: + constexpr span(const T* ptr, size_t length) + : ptr(ptr), length(length) {} + + const T& operator[](size_t index) const { + return ptr[index]; + } + + const T& at(size_t index) const { + if (index >= length) { + throw std::out_of_range(); + } + return ptr[index]; + } + + auto begin() const { + return ptr; + } + + auto end() const { + return ptr + length; + } + + const T* data() const { + return ptr; + } + + size_t size() const { + return length; + } + private: + const T* ptr; + size_t length; + }; +} From 31137f83b6d563d857b8e640d7dd31ed02dbea03 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 14 Oct 2025 00:50:23 +0300 Subject: [PATCH 04/28] add MemoryPCMStream --- src/audio/MemoryPCMStream.cpp | 63 +++++++++++++++++++++++++++++++++++ src/audio/MemoryPCMStream.hpp | 42 +++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 src/audio/MemoryPCMStream.cpp create mode 100644 src/audio/MemoryPCMStream.hpp diff --git a/src/audio/MemoryPCMStream.cpp b/src/audio/MemoryPCMStream.cpp new file mode 100644 index 00000000..7fcf4b0c --- /dev/null +++ b/src/audio/MemoryPCMStream.cpp @@ -0,0 +1,63 @@ +#include "MemoryPCMStream.hpp" + +#include + +using namespace audio; + +MemoryPCMStream::MemoryPCMStream( + uint sampleRate, uint channels, uint bitsPerSample +) + : sampleRate(sampleRate), channels(channels), bitsPerSample(bitsPerSample) { +} + +void MemoryPCMStream::feed(util::span bytes) { + buffer.insert(buffer.end(), bytes.begin(), bytes.end()); +} + +bool MemoryPCMStream::isOpen() const { + return open; +} + +void MemoryPCMStream::close() { + open = false; + buffer = {}; +} + +size_t MemoryPCMStream::read(char* dst, size_t bufferSize) { + if (!open) { + return PCMStream::ERROR; + } + if (buffer.empty()) { + return 0; + } + size_t count = std::min(bufferSize, buffer.size()); + std::memcpy(dst, buffer.data(), count); + buffer.erase(buffer.begin(), buffer.begin() + count); + return count; +} + +size_t MemoryPCMStream::getTotalSamples() const { + return 0; +} + +duration_t MemoryPCMStream::getTotalDuration() const { + return 0.0; +} + +uint MemoryPCMStream::getChannels() const { + return channels; +} + +uint MemoryPCMStream::getSampleRate() const { + return sampleRate; +} + +uint MemoryPCMStream::getBitsPerSample() const { + return bitsPerSample; +} + +bool MemoryPCMStream::isSeekable() const { + return false; +} + +void MemoryPCMStream::seek(size_t position) {} diff --git a/src/audio/MemoryPCMStream.hpp b/src/audio/MemoryPCMStream.hpp new file mode 100644 index 00000000..1f94d27e --- /dev/null +++ b/src/audio/MemoryPCMStream.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include "audio.hpp" +#include "util/span.hpp" + +namespace audio { + class MemoryPCMStream : public PCMStream { + public: + MemoryPCMStream(uint sampleRate, uint channels, uint bitsPerSample); + + void feed(util::span bytes); + + bool isOpen() const override; + + void close() override; + + size_t read(char* buffer, size_t bufferSize) override; + + size_t getTotalSamples() const override; + + duration_t getTotalDuration() const override; + + uint getChannels() const override; + + uint getSampleRate() const override; + + uint getBitsPerSample() const override; + + bool isSeekable() const override; + + void seek(size_t position) override; + private: + uint sampleRate; + uint channels; + uint bitsPerSample; + bool open = true; + + std::vector buffer; + }; +} From 84f087049dfcc4cf25f25a87890d31a9934bb857 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 14 Oct 2025 00:51:04 +0300 Subject: [PATCH 05/28] add test PCMStream lua use --- res/scripts/hud.lua | 11 +++ src/audio/audio.hpp | 4 + src/logic/scripting/lua/libs/libaudio.cpp | 20 ++++ src/logic/scripting/lua/lua_engine.cpp | 2 + .../lua/usertypes/lua_type_pcmstream.cpp | 94 +++++++++++++++++++ .../lua/usertypes/lua_type_pcmstream.hpp | 26 +++++ 6 files changed, 157 insertions(+) create mode 100644 src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp create mode 100644 src/logic/scripting/lua/usertypes/lua_type_pcmstream.hpp diff --git a/res/scripts/hud.lua b/res/scripts/hud.lua index 90883d94..cec5fc07 100644 --- a/res/scripts/hud.lua +++ b/res/scripts/hud.lua @@ -116,6 +116,17 @@ function on_hud_open() configure_SSAO() hud.default_hand_controller = update_hand + + local stream = PCMStream(44100, 1, 8) + stream:share("test-stream") + local bytes = Bytearray(44100 * 16) + for i=1,#bytes do + local x = math.sin(i * 0.08) * 127 + 128 + bytes[i] = x + end + stream:feed(bytes) + + audio.play_stream_2d("test-stream", 0.05, 1.0) end function on_hud_render() diff --git a/src/audio/audio.hpp b/src/audio/audio.hpp index 5893c334..a3553b38 100644 --- a/src/audio/audio.hpp +++ b/src/audio/audio.hpp @@ -121,6 +121,10 @@ namespace audio { /// (always equals bufferSize if seekable and looped) virtual size_t readFully(char* buffer, size_t bufferSize, bool loop); + /// @brief Read available data to buffer + /// @param buffer destination buffer + /// @param bufferSize destination buffer size + /// @return count of received bytes or PCMStream::ERROR virtual size_t read(char* buffer, size_t bufferSize) = 0; /// @brief Close stream diff --git a/src/logic/scripting/lua/libs/libaudio.cpp b/src/logic/scripting/lua/libs/libaudio.cpp index db868c57..4d4ad6a6 100644 --- a/src/logic/scripting/lua/libs/libaudio.cpp +++ b/src/logic/scripting/lua/libs/libaudio.cpp @@ -67,6 +67,26 @@ inline audio::speakerid_t play_stream( if (channel == -1) { return 0; } + if (!scripting::engine->isHeadless()) { + auto assets = scripting::engine->getAssets(); + + auto stream = assets->getShared(filename); + if (stream) { + return audio::play( + audio::open_stream(std::move(stream), true), + glm::vec3( + static_cast(x), + static_cast(y), + static_cast(z) + ), + relative, + volume, + pitch, + loop, + channel + ); + } + } io::path file; if (std::strchr(filename, ':')) { file = std::string(filename); diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index 05adb354..c4507bb1 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -12,6 +12,7 @@ #include "usertypes/lua_type_voxelfragment.hpp" #include "usertypes/lua_type_canvas.hpp" #include "usertypes/lua_type_random.hpp" +#include "usertypes/lua_type_pcmstream.hpp" #include "engine/Engine.hpp" static debug::Logger logger("lua-state"); @@ -130,6 +131,7 @@ void lua::init_state(State* L, StateType stateType) { newusertype(L); newusertype(L); newusertype(L); + newusertype(L); } void lua::initialize(const EnginePaths& paths, const CoreParameters& params) { diff --git a/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp new file mode 100644 index 00000000..04c585cf --- /dev/null +++ b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp @@ -0,0 +1,94 @@ +#include "../lua_util.hpp" +#include "lua_type_pcmstream.hpp" +#include "assets/Assets.hpp" +#include "audio/MemoryPCMStream.hpp" +#include "engine/Engine.hpp" + +using namespace lua; +using namespace audio; +using namespace scripting; + +LuaPCMStream::LuaPCMStream(std::shared_ptr&& stream) + : stream(std::move(stream)) { +} + +LuaPCMStream::~LuaPCMStream() = default; + +const std::shared_ptr& LuaPCMStream::getStream() const { + return stream; +} + +#include + +static int l_feed(lua::State* L) { + std::cout << "feed" << std::endl; + auto stream = touserdata(L, 1); + if (stream == nullptr) { + return 0; + } + auto bytes = bytearray_as_string(L, 2); + stream->getStream()->feed( + {reinterpret_cast(bytes.data()), bytes.size()} + ); + return 0; +} + +static int l_share(lua::State* L) { + auto stream = touserdata(L, 1); + if (stream == nullptr) { + return 0; + } + auto alias = require_lstring(L, 2); + if (engine->isHeadless()) { + return 0; + } + auto assets = engine->getAssets(); + assets->store(stream->getStream(), std::string(alias)); + return 0; +} + +static std::unordered_map methods { + {"feed", lua::wrap}, + {"share", lua::wrap}, +}; + +static int l_meta_meta_call(lua::State* L) { + auto sampleRate = touinteger(L, 2); + auto channels = touinteger(L, 3); + auto bitsPerSample = touinteger(L, 4); + auto stream = + std::make_shared(sampleRate, channels, bitsPerSample); + return newuserdata(L, std::move(stream)); +} + +static int l_meta_tostring(lua::State* L) { + return pushstring(L, "PCMStream"); +} + +static int l_meta_index(lua::State* L) { + auto stream = touserdata(L, 1); + if (stream == nullptr) { + return 0; + } + if (isstring(L, 2)) { + auto found = methods.find(tostring(L, 2)); + if (found != methods.end()) { + return pushcfunction(L, found->second); + } + } + return 0; +} + +int LuaPCMStream::createMetatable(lua::State* L) { + createtable(L, 0, 3); + pushcfunction(L, lua::wrap); + setfield(L, "__tostring"); + pushcfunction(L, lua::wrap); + setfield(L, "__index"); + + createtable(L, 0, 1); + pushcfunction(L, lua::wrap); + setfield(L, "__call"); + setmetatable(L); + return 1; +} diff --git a/src/logic/scripting/lua/usertypes/lua_type_pcmstream.hpp b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.hpp new file mode 100644 index 00000000..47979693 --- /dev/null +++ b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "../lua_commons.hpp" + +namespace audio { + class MemoryPCMStream; +} + +namespace lua { + class LuaPCMStream : public Userdata { + public: + explicit LuaPCMStream(std::shared_ptr&& stream); + virtual ~LuaPCMStream() override; + + const std::shared_ptr& getStream() const; + + const std::string& getTypeName() const override { + return TYPENAME; + } + static int createMetatable(lua::State*); + inline static std::string TYPENAME = "PCMStream"; + private: + std::shared_ptr stream; + }; + static_assert(!std::is_abstract()); +} From 2b72f87c6488dee0a5cc899ee753148b07c1b2e4 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 14 Oct 2025 00:53:51 +0300 Subject: [PATCH 06/28] fix --- src/util/span.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/span.hpp b/src/util/span.hpp index a9ab79b2..7c97f5ac 100644 --- a/src/util/span.hpp +++ b/src/util/span.hpp @@ -15,7 +15,7 @@ namespace util { const T& at(size_t index) const { if (index >= length) { - throw std::out_of_range(); + throw std::out_of_range("index is out of range"); } return ptr[index]; } From 27416ab0cd8508324aa3d4fab8c5c5bd4b619275 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 14 Oct 2025 12:43:51 +0300 Subject: [PATCH 07/28] add audio::InputDevice --- src/audio/AL/ALAudio.cpp | 47 ++++++++++++++++++++++++++++++++++++++++ src/audio/AL/ALAudio.hpp | 25 +++++++++++++++++++++ src/audio/NoAudio.hpp | 7 ++++++ src/audio/audio.hpp | 17 +++++++++++++++ 4 files changed, 96 insertions(+) diff --git a/src/audio/AL/ALAudio.cpp b/src/audio/AL/ALAudio.cpp index 7f28882b..92dd209a 100644 --- a/src/audio/AL/ALAudio.cpp +++ b/src/audio/AL/ALAudio.cpp @@ -37,6 +37,38 @@ std::unique_ptr ALSound::newInstance(int priority, int channel) const { return speaker; } +ALInputDevice::ALInputDevice( + ALAudio* al, ALCdevice* device, uint channels, uint bitsPerSample +) + : al(al), device(device), channels(channels), bitsPerSample(bitsPerSample) { +} + +ALInputDevice::~ALInputDevice() { + alcCaptureCloseDevice(device); +} + +void ALInputDevice::startCapture() { + AL_CHECK(alcCaptureStart(device)); +} + +void ALInputDevice::stopCapture() { + AL_CHECK(alcCaptureStop(device)); +} + +uint ALInputDevice::getChannels() const { + return channels; +} + +size_t ALInputDevice::read(char* buffer, size_t bufferSize) { + ALCint samplesCount; + AL_CHECK(alcGetIntegerv(device, ALC_CAPTURE_SAMPLES, 1, &samplesCount)); + size_t samplesRead = std::min( + samplesCount, bufferSize / channels / (bitsPerSample >> 3) + ); + AL_CHECK(alcCaptureSamples(device, buffer, samplesRead)); + return samplesRead; +} + ALStream::ALStream( ALAudio* al, std::shared_ptr source, bool keepSource ) @@ -411,6 +443,21 @@ std::unique_ptr ALAudio::openStream( return std::make_unique(this, stream, keepSource); } +std::unique_ptr ALAudio::openInputDevice( + uint sampleRate, uint channels, uint bitsPerSample +) { + uint bps = bitsPerSample >> 3; + AL_CHECK( + ALCdevice* device = alcCaptureOpenDevice( + nullptr, + sampleRate, + AL::to_al_format(channels, bps), + sampleRate * channels * bps + ) + ); + return std::make_unique(this, device, channels, bps); +} + std::unique_ptr ALAudio::create() { ALCdevice* device = alcOpenDevice(nullptr); if (device == nullptr) return nullptr; diff --git a/src/audio/AL/ALAudio.hpp b/src/audio/AL/ALAudio.hpp index 7e0f0c29..d04d45b6 100644 --- a/src/audio/AL/ALAudio.hpp +++ b/src/audio/AL/ALAudio.hpp @@ -82,6 +82,26 @@ namespace audio { static inline constexpr uint STREAM_BUFFERS = 3; }; + class ALInputDevice : public InputDevice { + public: + ALInputDevice( + ALAudio* al, ALCdevice* device, uint channels, uint bitsPerSample + ); + ~ALInputDevice() override; + + void startCapture() override; + void stopCapture() override; + + uint getChannels() const override; + + size_t read(char* buffer, size_t bufferSize) override; + private: + ALAudio* al; + ALCdevice* device; + uint channels; + uint bitsPerSample; + }; + /// @brief AL source adapter class ALSpeaker : public Speaker { ALAudio* al; @@ -157,10 +177,15 @@ namespace audio { std::unique_ptr createSound( std::shared_ptr pcm, bool keepPCM ) override; + std::unique_ptr openStream( std::shared_ptr stream, bool keepSource ) override; + std::unique_ptr openInputDevice( + uint sampleRate, uint channels, uint bitsPerSample + ) override; + void setListener( glm::vec3 position, glm::vec3 velocity, diff --git a/src/audio/NoAudio.hpp b/src/audio/NoAudio.hpp index edfc3319..bc2e6b49 100644 --- a/src/audio/NoAudio.hpp +++ b/src/audio/NoAudio.hpp @@ -71,10 +71,17 @@ namespace audio { std::unique_ptr createSound( std::shared_ptr pcm, bool keepPCM ) override; + std::unique_ptr openStream( std::shared_ptr stream, bool keepSource ) override; + std::unique_ptr openInputDevice( + uint sampleRate, uint channels, uint bitsPerSample + ) override { + return nullptr; + } + void setListener( glm::vec3 position, glm::vec3 velocity, glm::vec3 at, glm::vec3 up ) override { diff --git a/src/audio/audio.hpp b/src/audio/audio.hpp index a3553b38..56de29ad 100644 --- a/src/audio/audio.hpp +++ b/src/audio/audio.hpp @@ -108,6 +108,20 @@ namespace audio { } }; + class InputDevice { + public: + virtual ~InputDevice() {}; + + virtual void startCapture() = 0; + virtual void stopCapture() = 0; + + /// @brief Get number of audio channels + /// @return 1 if mono, 2 if stereo + virtual uint getChannels() const = 0; + + virtual size_t read(char* buffer, size_t bufferSize) = 0; + }; + /// @brief audio::PCMStream is a data source for audio::Stream class PCMStream { public: @@ -345,6 +359,9 @@ namespace audio { virtual std::unique_ptr openStream( std::shared_ptr stream, bool keepSource ) = 0; + virtual std::unique_ptr openInputDevice( + uint sampleRate, uint channels, uint bitsPerSample + ) = 0; virtual void setListener( glm::vec3 position, glm::vec3 velocity, From 7dca9255dfc3d4ecc393a5a1b60b0c00ba035505 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 15 Oct 2025 22:13:16 +0300 Subject: [PATCH 08/28] add test audio input fetch function --- res/scripts/hud.lua | 13 ++-- src/audio/AL/ALAudio.cpp | 69 ++++++++++++++----- src/audio/audio.cpp | 10 +++ src/audio/audio.hpp | 13 ++++ src/logic/scripting/lua/libs/libaudio.cpp | 24 ++++++- .../lua/usertypes/lua_type_pcmstream.cpp | 3 - 6 files changed, 107 insertions(+), 25 deletions(-) diff --git a/res/scripts/hud.lua b/res/scripts/hud.lua index cec5fc07..ba828491 100644 --- a/res/scripts/hud.lua +++ b/res/scripts/hud.lua @@ -55,6 +55,8 @@ local function update_hand() skeleton.set_model("hand", bone, item.model_name(itemid)) end +local stream + function on_hud_open() input.add_callback("player.pick", function () if hud.is_paused() or hud.is_inventory_open() then @@ -117,16 +119,16 @@ function on_hud_open() hud.default_hand_controller = update_hand - local stream = PCMStream(44100, 1, 8) + stream = PCMStream(44100, 1, 16) stream:share("test-stream") - local bytes = Bytearray(44100 * 16) + local bytes = Bytearray(44100 / 8) for i=1,#bytes do - local x = math.sin(i * 0.08) * 127 + 128 + local x = math.sin(i * 0.08) * 1 + 0 bytes[i] = x end stream:feed(bytes) - audio.play_stream_2d("test-stream", 0.05, 1.0) + audio.play_stream_2d("test-stream", 2.0, 1.0, "ui") end function on_hud_render() @@ -135,4 +137,7 @@ function on_hud_render() else update_hand() end + + local bytes = audio.fetch_input() + stream:feed(bytes) end diff --git a/src/audio/AL/ALAudio.cpp b/src/audio/AL/ALAudio.cpp index 92dd209a..23dfeaa8 100644 --- a/src/audio/AL/ALAudio.cpp +++ b/src/audio/AL/ALAudio.cpp @@ -5,11 +5,40 @@ #include "debug/Logger.hpp" #include "alutil.hpp" +#include "../MemoryPCMStream.hpp" static debug::Logger logger("al-audio"); using namespace audio; +const char* alc_error_to_string(ALCenum error) { + switch (error) { + case ALC_NO_ERROR: + return "no error"; + case ALC_INVALID_DEVICE: + return "invalid device handle"; + case ALC_INVALID_CONTEXT: + return "invalid context handle"; + case ALC_INVALID_ENUM: + return "invalid enum parameter passed to an ALC call"; + case ALC_INVALID_VALUE: + return "invalid value parameter passed to an ALC call"; + case ALC_OUT_OF_MEMORY: + return "out of memory"; + default: + return "unknown ALC error"; + } +} + +static void check_alc_errors(ALCdevice* device, const char* context) { + ALCenum error = alcGetError(device); + if (error == ALC_NO_ERROR) { + return; + } + logger.error() << context << ": " << alc_error_to_string(error) << "(" + << error << ")"; +} + ALSound::ALSound( ALAudio* al, uint buffer, const std::shared_ptr& pcm, bool keepPCM ) @@ -45,14 +74,17 @@ ALInputDevice::ALInputDevice( ALInputDevice::~ALInputDevice() { alcCaptureCloseDevice(device); + check_alc_errors(device, "alcCaptureCloseDevice"); } void ALInputDevice::startCapture() { - AL_CHECK(alcCaptureStart(device)); + alcCaptureStart(device); + check_alc_errors(device, "alcCaptureStart"); } void ALInputDevice::stopCapture() { - AL_CHECK(alcCaptureStop(device)); + alcCaptureStop(device); + check_alc_errors(device, "alcCaptureStop"); } uint ALInputDevice::getChannels() const { @@ -60,13 +92,15 @@ uint ALInputDevice::getChannels() const { } size_t ALInputDevice::read(char* buffer, size_t bufferSize) { - ALCint samplesCount; - AL_CHECK(alcGetIntegerv(device, ALC_CAPTURE_SAMPLES, 1, &samplesCount)); + ALCint samplesCount = 0; + alcGetIntegerv(device, ALC_CAPTURE_SAMPLES, sizeof(samplesCount), &samplesCount); + check_alc_errors(device, "alcGetIntegerv(ALC_CAPTURE_SAMPLES)"); size_t samplesRead = std::min( samplesCount, bufferSize / channels / (bitsPerSample >> 3) ); - AL_CHECK(alcCaptureSamples(device, buffer, samplesRead)); - return samplesRead; + alcCaptureSamples(device, buffer, samplesRead); + check_alc_errors(device, "alcCaptureSamples"); + return samplesRead * channels * (bitsPerSample >> 3); } ALStream::ALStream( @@ -195,7 +229,7 @@ void ALStream::update(double delta) { // alspeaker->stopped is assigned to false at ALSpeaker::play(...) if (p_speaker->isStopped() && !alspeaker->stopped) { //TODO: -V560 false-positive? - if (preloaded) { + if (preloaded || dynamic_cast(source.get())) { p_speaker->play(); } else { p_speaker->stop(); @@ -417,8 +451,10 @@ ALAudio::~ALAudio() { AL_CHECK(alDeleteBuffers(1, &buffer)); } - AL_CHECK(alcMakeContextCurrent(context)); + alcMakeContextCurrent(nullptr); + check_alc_errors(device, "alcMakeContextCurrent"); alcDestroyContext(context); + check_alc_errors(device, "alcDestroyContext"); if (!alcCloseDevice(device)) { logger.error() << "device not closed!"; } @@ -447,15 +483,16 @@ std::unique_ptr ALAudio::openInputDevice( uint sampleRate, uint channels, uint bitsPerSample ) { uint bps = bitsPerSample >> 3; - AL_CHECK( - ALCdevice* device = alcCaptureOpenDevice( - nullptr, - sampleRate, - AL::to_al_format(channels, bps), - sampleRate * channels * bps - ) + ALCdevice* device = alcCaptureOpenDevice( + nullptr, + sampleRate, + AL::to_al_format(channels, bitsPerSample), + sampleRate * channels * bps / 8 + ); + check_alc_errors(device, "alcCaptureOpenDevice"); + return std::make_unique( + this, device, channels, bitsPerSample ); - return std::make_unique(this, device, channels, bps); } std::unique_ptr ALAudio::create() { diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index f98c436e..130e6fac 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -151,6 +151,8 @@ public: } }; +static std::unique_ptr input_device = nullptr; + void audio::initialize(bool enabled, AudioSettings& settings) { enabled = enabled && settings.enabled.get(); if (enabled) { @@ -180,6 +182,13 @@ void audio::initialize(bool enabled, AudioSettings& settings) { audio::get_channel(channel.name)->setVolume(value * value); }, true)); } + + input_device = backend->openInputDevice(44100, 1, 16); + input_device->startCapture(); +} + +InputDevice* audio::get_input_device() { + return input_device.get(); } std::unique_ptr audio::load_PCM(const io::path& file, bool headerOnly) { @@ -458,6 +467,7 @@ void audio::reset_channel(int index) { } void audio::close() { + input_device->stopCapture(); speakers.clear(); delete backend; backend = nullptr; diff --git a/src/audio/audio.hpp b/src/audio/audio.hpp index 56de29ad..e1c19d25 100644 --- a/src/audio/audio.hpp +++ b/src/audio/audio.hpp @@ -24,6 +24,8 @@ namespace audio { /// @brief streams and important sounds constexpr inline int PRIORITY_HIGH = 10; + constexpr inline size_t MAX_INPUT_SAMPLES = 22050; + class Speaker; /// @brief Audio speaker states @@ -423,6 +425,15 @@ namespace audio { std::shared_ptr stream, bool keepSource ); + /// @brief Open audio input device + /// @param sampleRate sample rate + /// @param channels channels count (1 - mono, 2 - stereo) + /// @param bitsPerSample number of bits per sample (8 or 16) + /// @return new InputDevice instance or nullptr + std::unique_ptr open_input_device( + uint sampleRate, uint channels, uint bitsPerSample + ); + /// @brief Configure 3D listener /// @param position listener position /// @param velocity listener velocity (used for Doppler effect) @@ -536,6 +547,8 @@ namespace audio { /// @brief Stop all playing audio in channel, reset channel state void reset_channel(int channel); + InputDevice* get_input_device(); + /// @brief Finalize audio system void close(); }; diff --git a/src/logic/scripting/lua/libs/libaudio.cpp b/src/logic/scripting/lua/libs/libaudio.cpp index 4d4ad6a6..7f68bd6d 100644 --- a/src/logic/scripting/lua/libs/libaudio.cpp +++ b/src/logic/scripting/lua/libs/libaudio.cpp @@ -379,16 +379,35 @@ static int l_audio_get_velocity(lua::State* L) { return 0; } -// @brief audio.count_speakers() -> integer +/// @brief audio.count_speakers() -> integer static int l_audio_count_speakers(lua::State* L) { return lua::pushinteger(L, audio::count_speakers()); } -// @brief audio.count_streams() -> integer +/// @brief audio.count_streams() -> integer static int l_audio_count_streams(lua::State* L) { return lua::pushinteger(L, audio::count_streams()); } +/// @brief audio.fetch_input(size) -> Bytearray +static int l_audio_fetch_input(lua::State* L) { + auto device = audio::get_input_device(); + if (device == nullptr) { + return 0; + } + size_t size = lua::touinteger(L, 1); + const size_t MAX_BUFFER_SIZE = audio::MAX_INPUT_SAMPLES * 4; + if (size == 0) { + size = MAX_BUFFER_SIZE; + } + size = std::min(size, MAX_BUFFER_SIZE); + ubyte buffer[MAX_BUFFER_SIZE]; + size = device->read(reinterpret_cast(buffer), size); + + std::vector bytes(buffer, buffer + size); + return lua::create_bytearray(L, std::move(bytes)); +} + const luaL_Reg audiolib[] = { {"play_sound", lua::wrap}, {"play_sound_2d", lua::wrap}, @@ -414,5 +433,6 @@ const luaL_Reg audiolib[] = { {"get_velocity", lua::wrap}, {"count_speakers", lua::wrap}, {"count_streams", lua::wrap}, + {"fetch_input", lua::wrap}, {nullptr, nullptr} }; diff --git a/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp index 04c585cf..3367813e 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp @@ -18,10 +18,7 @@ const std::shared_ptr& LuaPCMStream::getStream() const { return stream; } -#include - static int l_feed(lua::State* L) { - std::cout << "feed" << std::endl; auto stream = touserdata(L, 1); if (stream == nullptr) { return 0; From d41167fd727404e0087eb1272741dc352167406f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 15 Oct 2025 22:40:00 +0300 Subject: [PATCH 09/28] fix --- src/audio/audio.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index 130e6fac..b45242bb 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -184,7 +184,9 @@ void audio::initialize(bool enabled, AudioSettings& settings) { } input_device = backend->openInputDevice(44100, 1, 16); - input_device->startCapture(); + if (input_device) { + input_device->startCapture(); + } } InputDevice* audio::get_input_device() { @@ -467,7 +469,9 @@ void audio::reset_channel(int index) { } void audio::close() { - input_device->stopCapture(); + if (input_device) { + input_device->stopCapture(); + } speakers.clear(); delete backend; backend = nullptr; From b11bdf0bcc3b56ed2cfb3eaa5d8c1a6afcaa79c3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 23 Oct 2025 22:08:30 +0300 Subject: [PATCH 10/28] add audio.get_input_devices_names() --- src/audio/AL/ALAudio.cpp | 22 ++++++++++++++++++++++ src/audio/AL/ALAudio.hpp | 2 ++ src/audio/NoAudio.hpp | 4 ++++ src/audio/audio.cpp | 10 ++++++++++ src/audio/audio.hpp | 5 +++++ src/logic/scripting/lua/libs/libaudio.cpp | 12 ++++++++++++ 6 files changed, 55 insertions(+) diff --git a/src/audio/AL/ALAudio.cpp b/src/audio/AL/ALAudio.cpp index 23dfeaa8..ce73823c 100644 --- a/src/audio/AL/ALAudio.cpp +++ b/src/audio/AL/ALAudio.cpp @@ -479,6 +479,27 @@ std::unique_ptr ALAudio::openStream( return std::make_unique(this, stream, keepSource); } +std::vector ALAudio::getInputDeviceNames() { + std::vector devices; + + if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT")) { + auto deviceList = alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER); + if (deviceList == nullptr) { + logger.warning() << "no input devices found"; + return devices; + } + while (*deviceList) { + std::string deviceName(deviceList); + devices.push_back(deviceName); + deviceList += deviceName.length() + 1; + } + } else { + logger.warning() << "enumeration extension is not available"; + } + + return devices; +} + std::unique_ptr ALAudio::openInputDevice( uint sampleRate, uint channels, uint bitsPerSample ) { @@ -490,6 +511,7 @@ std::unique_ptr ALAudio::openInputDevice( sampleRate * channels * bps / 8 ); check_alc_errors(device, "alcCaptureOpenDevice"); + return std::make_unique( this, device, channels, bitsPerSample ); diff --git a/src/audio/AL/ALAudio.hpp b/src/audio/AL/ALAudio.hpp index d04d45b6..8e4ad02c 100644 --- a/src/audio/AL/ALAudio.hpp +++ b/src/audio/AL/ALAudio.hpp @@ -186,6 +186,8 @@ namespace audio { uint sampleRate, uint channels, uint bitsPerSample ) override; + std::vector getInputDeviceNames() override; + void setListener( glm::vec3 position, glm::vec3 velocity, diff --git a/src/audio/NoAudio.hpp b/src/audio/NoAudio.hpp index bc2e6b49..c5dbae41 100644 --- a/src/audio/NoAudio.hpp +++ b/src/audio/NoAudio.hpp @@ -82,6 +82,10 @@ namespace audio { return nullptr; } + std::vector getInputDeviceNames() override { + return {}; + } + void setListener( glm::vec3 position, glm::vec3 velocity, glm::vec3 at, glm::vec3 up ) override { diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index b45242bb..72b22df5 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -253,6 +253,16 @@ std::unique_ptr audio::open_stream( return backend->openStream(std::move(stream), keepSource); } +std::unique_ptr audio::open_input_device( + uint sampleRate, uint channels, uint bitsPerSample +) { + return backend->openInputDevice(sampleRate, channels, bitsPerSample); +} + +std::vector audio::get_input_devices_names() { + return backend->getInputDeviceNames(); +} + void audio::set_listener( glm::vec3 position, glm::vec3 velocity, glm::vec3 lookAt, glm::vec3 up ) { diff --git a/src/audio/audio.hpp b/src/audio/audio.hpp index e1c19d25..3364d89e 100644 --- a/src/audio/audio.hpp +++ b/src/audio/audio.hpp @@ -370,6 +370,7 @@ namespace audio { glm::vec3 lookAt, glm::vec3 up ) = 0; + virtual std::vector getInputDeviceNames() = 0; virtual void update(double delta) = 0; /// @brief Check if backend is an abstraction that does not internally @@ -434,6 +435,10 @@ namespace audio { uint sampleRate, uint channels, uint bitsPerSample ); + /// @brief Retrieve names of available audio input devices + /// @return list of device names + std::vector get_input_devices_names(); + /// @brief Configure 3D listener /// @param position listener position /// @param velocity listener velocity (used for Doppler effect) diff --git a/src/logic/scripting/lua/libs/libaudio.cpp b/src/logic/scripting/lua/libs/libaudio.cpp index 7f68bd6d..951522d9 100644 --- a/src/logic/scripting/lua/libs/libaudio.cpp +++ b/src/logic/scripting/lua/libs/libaudio.cpp @@ -408,6 +408,17 @@ static int l_audio_fetch_input(lua::State* L) { return lua::create_bytearray(L, std::move(bytes)); } +static int l_audio_get_input_devices_names(lua::State* L) { + auto device_names = audio::get_input_devices_names(); + lua::createtable(L, device_names.size(), 0); + int index = 1; + for (const auto& name : device_names) { + lua::pushstring(L, name.c_str()); + lua::rawseti(L, index++); + } + return 1; +} + const luaL_Reg audiolib[] = { {"play_sound", lua::wrap}, {"play_sound_2d", lua::wrap}, @@ -434,5 +445,6 @@ const luaL_Reg audiolib[] = { {"count_speakers", lua::wrap}, {"count_streams", lua::wrap}, {"fetch_input", lua::wrap}, + {"get_input_devices_names", lua::wrap}, {nullptr, nullptr} }; From 6df27b89921e822311759b837529ea0a91f74cc0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 23 Oct 2025 22:54:05 +0300 Subject: [PATCH 11/28] add audio.get_output_devices_names, set_input_device, set_output_device --- src/audio/AL/ALAudio.cpp | 114 +++++++++++++++------- src/audio/AL/ALAudio.hpp | 10 +- src/audio/NoAudio.hpp | 8 +- src/audio/audio.cpp | 34 ++++++- src/audio/audio.hpp | 19 +++- src/logic/scripting/lua/libs/libaudio.cpp | 26 +++++ 6 files changed, 169 insertions(+), 42 deletions(-) diff --git a/src/audio/AL/ALAudio.cpp b/src/audio/AL/ALAudio.cpp index ce73823c..9e555133 100644 --- a/src/audio/AL/ALAudio.cpp +++ b/src/audio/AL/ALAudio.cpp @@ -30,13 +30,14 @@ const char* alc_error_to_string(ALCenum error) { } } -static void check_alc_errors(ALCdevice* device, const char* context) { +static bool check_alc_errors(ALCdevice* device, const char* context) { ALCenum error = alcGetError(device); if (error == ALC_NO_ERROR) { - return; + return false; } logger.error() << context << ": " << alc_error_to_string(error) << "(" << error << ")"; + return true; } ALSound::ALSound( @@ -419,6 +420,8 @@ int ALSpeaker::getPriority() const { return priority; } +static bool alc_enumeration_ext = false; + ALAudio::ALAudio(ALCdevice* device, ALCcontext* context) : device(device), context(context) { ALCint size; @@ -431,9 +434,15 @@ ALAudio::ALAudio(ALCdevice* device, ALCcontext* context) maxSources = attrs[i + 1]; } } - auto devices = getAvailableDevices(); - logger.info() << "devices:"; - for (auto& name : devices) { + auto outputDevices = getOutputDeviceNames(); + logger.info() << "output devices:"; + for (auto& name : outputDevices) { + logger.info() << " " << name; + } + + auto inputDevices = getInputDeviceNames(); + logger.info() << "input devices:"; + for (auto& name : inputDevices) { logger.info() << " " << name; } } @@ -482,35 +491,59 @@ std::unique_ptr ALAudio::openStream( std::vector ALAudio::getInputDeviceNames() { std::vector devices; - if (alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT")) { - auto deviceList = alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER); - if (deviceList == nullptr) { - logger.warning() << "no input devices found"; - return devices; - } - while (*deviceList) { - std::string deviceName(deviceList); - devices.push_back(deviceName); - deviceList += deviceName.length() + 1; - } - } else { + if (!alc_enumeration_ext) { logger.warning() << "enumeration extension is not available"; + return devices; + } + + auto deviceList = alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER); + if (deviceList == nullptr) { + logger.warning() << "no input devices found"; + return devices; + } + while (*deviceList) { + std::string deviceName(deviceList); + devices.push_back(deviceName); + deviceList += deviceName.length() + 1; + } + + return devices; +} + +std::vector ALAudio::getOutputDeviceNames() { + std::vector devices; + + if (!alc_enumeration_ext) { + logger.warning() << "enumeration extension is not available"; + return devices; + } + + auto deviceList = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); + if (deviceList == nullptr) { + logger.warning() << "no input devices found"; + return devices; + } + while (*deviceList) { + std::string deviceName(deviceList); + devices.push_back(deviceName); + deviceList += deviceName.length() + 1; } return devices; } std::unique_ptr ALAudio::openInputDevice( - uint sampleRate, uint channels, uint bitsPerSample + const char* deviceName, uint sampleRate, uint channels, uint bitsPerSample ) { uint bps = bitsPerSample >> 3; ALCdevice* device = alcCaptureOpenDevice( - nullptr, + deviceName, sampleRate, AL::to_al_format(channels, bitsPerSample), sampleRate * channels * bps / 8 ); - check_alc_errors(device, "alcCaptureOpenDevice"); + if (check_alc_errors(device, "alcCaptureOpenDevice")) + return nullptr; return std::make_unique( this, device, channels, bitsPerSample @@ -518,6 +551,8 @@ std::unique_ptr ALAudio::openInputDevice( } std::unique_ptr ALAudio::create() { + alc_enumeration_ext = alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT"); + ALCdevice* device = alcOpenDevice(nullptr); if (device == nullptr) return nullptr; ALCcontext* context = alcCreateContext(device, nullptr); @@ -574,22 +609,35 @@ void ALAudio::freeBuffer(uint buffer) { freebuffers.push_back(buffer); } -std::vector ALAudio::getAvailableDevices() const { - std::vector devicesVec; - - const ALCchar* devices; - devices = alcGetString(device, ALC_DEVICE_SPECIFIER); - if (!AL_GET_ERROR()) { - return devicesVec; +void ALAudio::setOutputDevice(const std::string& deviceName) { + ALCdevice* newDevice = alcOpenDevice(deviceName.c_str()); + if (newDevice == nullptr) { + logger.error() << "failed to open output device: " << deviceName; + return; } - const char* ptr = devices; - do { - devicesVec.emplace_back(ptr); - ptr += devicesVec.back().size() + 1; - } while (ptr[0]); + ALCcontext* newContext = alcCreateContext(newDevice, nullptr); + if (!alcMakeContextCurrent(newContext)) { + logger.error() << "failed to make context current for device: " + << deviceName; + alcCloseDevice(newDevice); + return; + } - return devicesVec; + // Clean up old device and context + alcMakeContextCurrent(nullptr); + check_alc_errors(device, "alcMakeContextCurrent"); + alcDestroyContext(context); + check_alc_errors(device, "alcDestroyContext"); + if (!alcCloseDevice(device)) { + logger.error() << "old device not closed"; + } + + // Update to new device and context + device = newDevice; + context = newContext; + + logger.info() << "switched output device to: " << deviceName; } void ALAudio::setListener( diff --git a/src/audio/AL/ALAudio.hpp b/src/audio/AL/ALAudio.hpp index 8e4ad02c..d8fe2000 100644 --- a/src/audio/AL/ALAudio.hpp +++ b/src/audio/AL/ALAudio.hpp @@ -172,8 +172,6 @@ namespace audio { void freeSource(uint source); void freeBuffer(uint buffer); - std::vector getAvailableDevices() const; - std::unique_ptr createSound( std::shared_ptr pcm, bool keepPCM ) override; @@ -183,11 +181,17 @@ namespace audio { ) override; std::unique_ptr openInputDevice( - uint sampleRate, uint channels, uint bitsPerSample + const char* deviceName, + uint sampleRate, + uint channels, + uint bitsPerSample ) override; + std::vector getOutputDeviceNames() override; std::vector getInputDeviceNames() override; + void setOutputDevice(const std::string& deviceName) override; + void setListener( glm::vec3 position, glm::vec3 velocity, diff --git a/src/audio/NoAudio.hpp b/src/audio/NoAudio.hpp index c5dbae41..a7b4232c 100644 --- a/src/audio/NoAudio.hpp +++ b/src/audio/NoAudio.hpp @@ -77,7 +77,7 @@ namespace audio { ) override; std::unique_ptr openInputDevice( - uint sampleRate, uint channels, uint bitsPerSample + const char* deviceName, uint sampleRate, uint channels, uint bitsPerSample ) override { return nullptr; } @@ -85,6 +85,12 @@ namespace audio { std::vector getInputDeviceNames() override { return {}; } + std::vector getOutputDeviceNames() override { + return {}; + } + + void setOutputDevice(const std::string& deviceName) override { + } void setListener( glm::vec3 position, glm::vec3 velocity, glm::vec3 at, glm::vec3 up diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index 72b22df5..06a36c06 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -183,7 +183,7 @@ void audio::initialize(bool enabled, AudioSettings& settings) { }, true)); } - input_device = backend->openInputDevice(44100, 1, 16); + input_device = backend->openInputDevice(nullptr, 44100, 1, 16); if (input_device) { input_device->startCapture(); } @@ -254,15 +254,43 @@ std::unique_ptr audio::open_stream( } std::unique_ptr audio::open_input_device( - uint sampleRate, uint channels, uint bitsPerSample + const char* deviceName, uint sampleRate, uint channels, uint bitsPerSample ) { - return backend->openInputDevice(sampleRate, channels, bitsPerSample); + return backend->openInputDevice( + deviceName, sampleRate, channels, bitsPerSample + ); } std::vector audio::get_input_devices_names() { return backend->getInputDeviceNames(); } +std::vector audio::get_output_devices_names() { + return backend->getOutputDeviceNames(); +} + +void audio::set_input_device(const std::string& deviceName) { + auto newDevice = backend->openInputDevice( + deviceName.empty() ? nullptr : deviceName.c_str(), 44100, 1, 16 + ); + if (newDevice == nullptr) { + logger.error() << "could not open input device: " << deviceName; + return; + } + + if (input_device) { + input_device->stopCapture(); + } + input_device = std::move(newDevice); + if (input_device) { + input_device->startCapture(); + } +} + +void audio::set_output_device(const std::string& deviceName) { + backend->setOutputDevice(deviceName); +} + void audio::set_listener( glm::vec3 position, glm::vec3 velocity, glm::vec3 lookAt, glm::vec3 up ) { diff --git a/src/audio/audio.hpp b/src/audio/audio.hpp index 3364d89e..47fab4f5 100644 --- a/src/audio/audio.hpp +++ b/src/audio/audio.hpp @@ -362,7 +362,10 @@ namespace audio { std::shared_ptr stream, bool keepSource ) = 0; virtual std::unique_ptr openInputDevice( - uint sampleRate, uint channels, uint bitsPerSample + const char* deviceName, + uint sampleRate, + uint channels, + uint bitsPerSample ) = 0; virtual void setListener( glm::vec3 position, @@ -371,6 +374,8 @@ namespace audio { glm::vec3 up ) = 0; virtual std::vector getInputDeviceNames() = 0; + virtual std::vector getOutputDeviceNames() = 0; + virtual void setOutputDevice(const std::string& deviceName) = 0; virtual void update(double delta) = 0; /// @brief Check if backend is an abstraction that does not internally @@ -432,13 +437,23 @@ namespace audio { /// @param bitsPerSample number of bits per sample (8 or 16) /// @return new InputDevice instance or nullptr std::unique_ptr open_input_device( - uint sampleRate, uint channels, uint bitsPerSample + const char* deviceName, + uint sampleRate, + uint channels, + uint bitsPerSample ); /// @brief Retrieve names of available audio input devices /// @return list of device names std::vector get_input_devices_names(); + /// @brief Retrieve names of available audio output devices + /// @return list of device names + std::vector get_output_devices_names(); + + void set_input_device(const std::string& deviceName); + void set_output_device(const std::string& deviceName); + /// @brief Configure 3D listener /// @param position listener position /// @param velocity listener velocity (used for Doppler effect) diff --git a/src/logic/scripting/lua/libs/libaudio.cpp b/src/logic/scripting/lua/libs/libaudio.cpp index 951522d9..d240ec2e 100644 --- a/src/logic/scripting/lua/libs/libaudio.cpp +++ b/src/logic/scripting/lua/libs/libaudio.cpp @@ -419,6 +419,29 @@ static int l_audio_get_input_devices_names(lua::State* L) { return 1; } +static int l_audio_get_output_devices_names(lua::State* L) { + auto device_names = audio::get_output_devices_names(); + lua::createtable(L, device_names.size(), 0); + int index = 1; + for (const auto& name : device_names) { + lua::pushstring(L, name.c_str()); + lua::rawseti(L, index++); + } + return 1; +} + +static int l_audio_set_input_device(lua::State* L) { + auto device_name = lua::tostring(L, 1); + audio::set_input_device(device_name); + return 0; +} + +static int l_audio_set_output_device(lua::State* L) { + auto device_name = lua::tostring(L, 1); + audio::set_output_device(device_name); + return 0; +} + const luaL_Reg audiolib[] = { {"play_sound", lua::wrap}, {"play_sound_2d", lua::wrap}, @@ -446,5 +469,8 @@ const luaL_Reg audiolib[] = { {"count_streams", lua::wrap}, {"fetch_input", lua::wrap}, {"get_input_devices_names", lua::wrap}, + {"get_output_devices_names", lua::wrap}, + {"set_input_device", lua::wrap}, + {"set_output_device", lua::wrap}, {nullptr, nullptr} }; From 001c1b430bddd728ec5add68ec802902f075760a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 23 Oct 2025 23:45:55 +0300 Subject: [PATCH 12/28] minor refactor --- src/audio/AL/ALAudio.cpp | 4 ++-- src/audio/AL/ALAudio.hpp | 2 +- src/audio/NoAudio.hpp | 2 +- src/audio/audio.cpp | 8 +++----- src/audio/audio.hpp | 4 ++-- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/audio/AL/ALAudio.cpp b/src/audio/AL/ALAudio.cpp index 9e555133..4215f3a1 100644 --- a/src/audio/AL/ALAudio.cpp +++ b/src/audio/AL/ALAudio.cpp @@ -533,11 +533,11 @@ std::vector ALAudio::getOutputDeviceNames() { } std::unique_ptr ALAudio::openInputDevice( - const char* deviceName, uint sampleRate, uint channels, uint bitsPerSample + const std::string& deviceName, uint sampleRate, uint channels, uint bitsPerSample ) { uint bps = bitsPerSample >> 3; ALCdevice* device = alcCaptureOpenDevice( - deviceName, + deviceName.empty() ? nullptr : deviceName.c_str(), sampleRate, AL::to_al_format(channels, bitsPerSample), sampleRate * channels * bps / 8 diff --git a/src/audio/AL/ALAudio.hpp b/src/audio/AL/ALAudio.hpp index d8fe2000..99ca7be1 100644 --- a/src/audio/AL/ALAudio.hpp +++ b/src/audio/AL/ALAudio.hpp @@ -181,7 +181,7 @@ namespace audio { ) override; std::unique_ptr openInputDevice( - const char* deviceName, + const std::string& deviceName, uint sampleRate, uint channels, uint bitsPerSample diff --git a/src/audio/NoAudio.hpp b/src/audio/NoAudio.hpp index a7b4232c..cc0c240d 100644 --- a/src/audio/NoAudio.hpp +++ b/src/audio/NoAudio.hpp @@ -77,7 +77,7 @@ namespace audio { ) override; std::unique_ptr openInputDevice( - const char* deviceName, uint sampleRate, uint channels, uint bitsPerSample + const std::string& deviceName, uint sampleRate, uint channels, uint bitsPerSample ) override { return nullptr; } diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index 06a36c06..313deed8 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -183,7 +183,7 @@ void audio::initialize(bool enabled, AudioSettings& settings) { }, true)); } - input_device = backend->openInputDevice(nullptr, 44100, 1, 16); + input_device = backend->openInputDevice("", 44100, 1, 16); if (input_device) { input_device->startCapture(); } @@ -254,7 +254,7 @@ std::unique_ptr audio::open_stream( } std::unique_ptr audio::open_input_device( - const char* deviceName, uint sampleRate, uint channels, uint bitsPerSample + const std::string& deviceName, uint sampleRate, uint channels, uint bitsPerSample ) { return backend->openInputDevice( deviceName, sampleRate, channels, bitsPerSample @@ -270,9 +270,7 @@ std::vector audio::get_output_devices_names() { } void audio::set_input_device(const std::string& deviceName) { - auto newDevice = backend->openInputDevice( - deviceName.empty() ? nullptr : deviceName.c_str(), 44100, 1, 16 - ); + auto newDevice = backend->openInputDevice(deviceName, 44100, 1, 16); if (newDevice == nullptr) { logger.error() << "could not open input device: " << deviceName; return; diff --git a/src/audio/audio.hpp b/src/audio/audio.hpp index 47fab4f5..8b3a1326 100644 --- a/src/audio/audio.hpp +++ b/src/audio/audio.hpp @@ -362,7 +362,7 @@ namespace audio { std::shared_ptr stream, bool keepSource ) = 0; virtual std::unique_ptr openInputDevice( - const char* deviceName, + const std::string& deviceName, uint sampleRate, uint channels, uint bitsPerSample @@ -437,7 +437,7 @@ namespace audio { /// @param bitsPerSample number of bits per sample (8 or 16) /// @return new InputDevice instance or nullptr std::unique_ptr open_input_device( - const char* deviceName, + const std::string& deviceName, uint sampleRate, uint channels, uint bitsPerSample From 6c558eb3d5128e31dd89ac6ebd2a22549470af1a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Oct 2025 01:19:07 +0300 Subject: [PATCH 13/28] add audio.get_input_info() --- src/audio/AL/ALAudio.cpp | 22 +++++++++++++++++++--- src/audio/AL/ALAudio.hpp | 9 ++++++++- src/audio/audio.hpp | 6 ++++++ src/logic/scripting/lua/libs/libaudio.cpp | 16 ++++++++++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/audio/AL/ALAudio.cpp b/src/audio/AL/ALAudio.cpp index 4215f3a1..28877485 100644 --- a/src/audio/AL/ALAudio.cpp +++ b/src/audio/AL/ALAudio.cpp @@ -68,9 +68,17 @@ std::unique_ptr ALSound::newInstance(int priority, int channel) const { } ALInputDevice::ALInputDevice( - ALAudio* al, ALCdevice* device, uint channels, uint bitsPerSample + ALAudio* al, + ALCdevice* device, + uint channels, + uint bitsPerSample, + uint sampleRate ) - : al(al), device(device), channels(channels), bitsPerSample(bitsPerSample) { + : al(al), + device(device), + channels(channels), + bitsPerSample(bitsPerSample), + sampleRate(sampleRate) { } ALInputDevice::~ALInputDevice() { @@ -92,6 +100,14 @@ uint ALInputDevice::getChannels() const { return channels; } +uint ALInputDevice::getSampleRate() const { + return sampleRate; +} + +uint ALInputDevice::getBitsPerSample() const { + return bitsPerSample; +} + size_t ALInputDevice::read(char* buffer, size_t bufferSize) { ALCint samplesCount = 0; alcGetIntegerv(device, ALC_CAPTURE_SAMPLES, sizeof(samplesCount), &samplesCount); @@ -546,7 +562,7 @@ std::unique_ptr ALAudio::openInputDevice( return nullptr; return std::make_unique( - this, device, channels, bitsPerSample + this, device, channels, bitsPerSample, sampleRate ); } diff --git a/src/audio/AL/ALAudio.hpp b/src/audio/AL/ALAudio.hpp index 99ca7be1..3c108b6f 100644 --- a/src/audio/AL/ALAudio.hpp +++ b/src/audio/AL/ALAudio.hpp @@ -85,7 +85,11 @@ namespace audio { class ALInputDevice : public InputDevice { public: ALInputDevice( - ALAudio* al, ALCdevice* device, uint channels, uint bitsPerSample + ALAudio* al, + ALCdevice* device, + uint channels, + uint bitsPerSample, + uint sampleRate ); ~ALInputDevice() override; @@ -93,6 +97,8 @@ namespace audio { void stopCapture() override; uint getChannels() const override; + uint getSampleRate() const override; + uint getBitsPerSample() const override; size_t read(char* buffer, size_t bufferSize) override; private: @@ -100,6 +106,7 @@ namespace audio { ALCdevice* device; uint channels; uint bitsPerSample; + uint sampleRate; }; /// @brief AL source adapter diff --git a/src/audio/audio.hpp b/src/audio/audio.hpp index 8b3a1326..dd648027 100644 --- a/src/audio/audio.hpp +++ b/src/audio/audio.hpp @@ -120,6 +120,12 @@ namespace audio { /// @brief Get number of audio channels /// @return 1 if mono, 2 if stereo virtual uint getChannels() const = 0; + /// @brief Get audio sampling frequency + /// @return number of mono samples per second + virtual uint getSampleRate() const = 0; + /// @brief Get number of bits per mono sample + /// @return 8 or 16 + virtual uint getBitsPerSample() const = 0; virtual size_t read(char* buffer, size_t bufferSize) = 0; }; diff --git a/src/logic/scripting/lua/libs/libaudio.cpp b/src/logic/scripting/lua/libs/libaudio.cpp index d240ec2e..4551af0b 100644 --- a/src/logic/scripting/lua/libs/libaudio.cpp +++ b/src/logic/scripting/lua/libs/libaudio.cpp @@ -442,6 +442,21 @@ static int l_audio_set_output_device(lua::State* L) { return 0; } +static int l_audio_get_input_info(lua::State* L) { + auto device = audio::get_input_device(); + if (device == nullptr) { + return 0; + } + lua::createtable(L, 0, 3); + lua::pushinteger(L, device->getChannels()); + lua::setfield(L, "channels"); + lua::pushinteger(L, device->getSampleRate()); + lua::setfield(L, "sample_rate"); + lua::pushinteger(L, device->getBitsPerSample()); + lua::setfield(L, "bits_per_sample"); + return 1; +} + const luaL_Reg audiolib[] = { {"play_sound", lua::wrap}, {"play_sound_2d", lua::wrap}, @@ -472,5 +487,6 @@ const luaL_Reg audiolib[] = { {"get_output_devices_names", lua::wrap}, {"set_input_device", lua::wrap}, {"set_output_device", lua::wrap}, + {"get_input_info", lua::wrap}, {nullptr, nullptr} }; From ab7bf9c709108e29916cf36fa69932ec202c414b Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Oct 2025 23:10:16 +0300 Subject: [PATCH 14/28] cleanup --- src/audio/AL/ALAudio.cpp | 31 ----------------------- src/audio/AL/ALAudio.hpp | 2 -- src/audio/NoAudio.hpp | 3 --- src/audio/audio.cpp | 4 --- src/audio/audio.hpp | 2 -- src/logic/scripting/lua/libs/libaudio.cpp | 7 ----- 6 files changed, 49 deletions(-) diff --git a/src/audio/AL/ALAudio.cpp b/src/audio/AL/ALAudio.cpp index 28877485..267b20dd 100644 --- a/src/audio/AL/ALAudio.cpp +++ b/src/audio/AL/ALAudio.cpp @@ -625,37 +625,6 @@ void ALAudio::freeBuffer(uint buffer) { freebuffers.push_back(buffer); } -void ALAudio::setOutputDevice(const std::string& deviceName) { - ALCdevice* newDevice = alcOpenDevice(deviceName.c_str()); - if (newDevice == nullptr) { - logger.error() << "failed to open output device: " << deviceName; - return; - } - - ALCcontext* newContext = alcCreateContext(newDevice, nullptr); - if (!alcMakeContextCurrent(newContext)) { - logger.error() << "failed to make context current for device: " - << deviceName; - alcCloseDevice(newDevice); - return; - } - - // Clean up old device and context - alcMakeContextCurrent(nullptr); - check_alc_errors(device, "alcMakeContextCurrent"); - alcDestroyContext(context); - check_alc_errors(device, "alcDestroyContext"); - if (!alcCloseDevice(device)) { - logger.error() << "old device not closed"; - } - - // Update to new device and context - device = newDevice; - context = newContext; - - logger.info() << "switched output device to: " << deviceName; -} - void ALAudio::setListener( glm::vec3 position, glm::vec3 velocity, glm::vec3 at, glm::vec3 up ) { diff --git a/src/audio/AL/ALAudio.hpp b/src/audio/AL/ALAudio.hpp index 3c108b6f..6fef62f0 100644 --- a/src/audio/AL/ALAudio.hpp +++ b/src/audio/AL/ALAudio.hpp @@ -197,8 +197,6 @@ namespace audio { std::vector getOutputDeviceNames() override; std::vector getInputDeviceNames() override; - void setOutputDevice(const std::string& deviceName) override; - void setListener( glm::vec3 position, glm::vec3 velocity, diff --git a/src/audio/NoAudio.hpp b/src/audio/NoAudio.hpp index cc0c240d..5012704b 100644 --- a/src/audio/NoAudio.hpp +++ b/src/audio/NoAudio.hpp @@ -89,9 +89,6 @@ namespace audio { return {}; } - void setOutputDevice(const std::string& deviceName) override { - } - void setListener( glm::vec3 position, glm::vec3 velocity, glm::vec3 at, glm::vec3 up ) override { diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index 313deed8..60f44b04 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -285,10 +285,6 @@ void audio::set_input_device(const std::string& deviceName) { } } -void audio::set_output_device(const std::string& deviceName) { - backend->setOutputDevice(deviceName); -} - void audio::set_listener( glm::vec3 position, glm::vec3 velocity, glm::vec3 lookAt, glm::vec3 up ) { diff --git a/src/audio/audio.hpp b/src/audio/audio.hpp index dd648027..c394398f 100644 --- a/src/audio/audio.hpp +++ b/src/audio/audio.hpp @@ -381,7 +381,6 @@ namespace audio { ) = 0; virtual std::vector getInputDeviceNames() = 0; virtual std::vector getOutputDeviceNames() = 0; - virtual void setOutputDevice(const std::string& deviceName) = 0; virtual void update(double delta) = 0; /// @brief Check if backend is an abstraction that does not internally @@ -458,7 +457,6 @@ namespace audio { std::vector get_output_devices_names(); void set_input_device(const std::string& deviceName); - void set_output_device(const std::string& deviceName); /// @brief Configure 3D listener /// @param position listener position diff --git a/src/logic/scripting/lua/libs/libaudio.cpp b/src/logic/scripting/lua/libs/libaudio.cpp index 4551af0b..ed2e3db6 100644 --- a/src/logic/scripting/lua/libs/libaudio.cpp +++ b/src/logic/scripting/lua/libs/libaudio.cpp @@ -436,12 +436,6 @@ static int l_audio_set_input_device(lua::State* L) { return 0; } -static int l_audio_set_output_device(lua::State* L) { - auto device_name = lua::tostring(L, 1); - audio::set_output_device(device_name); - return 0; -} - static int l_audio_get_input_info(lua::State* L) { auto device = audio::get_input_device(); if (device == nullptr) { @@ -486,7 +480,6 @@ const luaL_Reg audiolib[] = { {"get_input_devices_names", lua::wrap}, {"get_output_devices_names", lua::wrap}, {"set_input_device", lua::wrap}, - {"set_output_device", lua::wrap}, {"get_input_info", lua::wrap}, {nullptr, nullptr} }; From cf561e78a81810fcb70975c7b785938af37b4b64 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 30 Oct 2025 19:56:07 +0300 Subject: [PATCH 15/28] fix: stream stops and dies on underflow --- res/scripts/hud.lua | 9 +------- src/audio/AL/ALAudio.cpp | 46 ++++++++++++++++++++++++++-------------- src/audio/AL/ALAudio.hpp | 9 +++++++- src/audio/NoAudio.hpp | 7 ++++++ src/audio/audio.cpp | 11 ++++++++-- src/audio/audio.hpp | 5 +++++ 6 files changed, 60 insertions(+), 27 deletions(-) diff --git a/res/scripts/hud.lua b/res/scripts/hud.lua index ba828491..7730ba5f 100644 --- a/res/scripts/hud.lua +++ b/res/scripts/hud.lua @@ -121,14 +121,7 @@ function on_hud_open() stream = PCMStream(44100, 1, 16) stream:share("test-stream") - local bytes = Bytearray(44100 / 8) - for i=1,#bytes do - local x = math.sin(i * 0.08) * 1 + 0 - bytes[i] = x - end - stream:feed(bytes) - - audio.play_stream_2d("test-stream", 2.0, 1.0, "ui") + streamid = audio.play_stream_2d("test-stream", 2.0, 1.0, "ui") end function on_hud_render() diff --git a/src/audio/AL/ALAudio.cpp b/src/audio/AL/ALAudio.cpp index 267b20dd..83d5c4f4 100644 --- a/src/audio/AL/ALAudio.cpp +++ b/src/audio/AL/ALAudio.cpp @@ -164,9 +164,10 @@ std::unique_ptr ALStream::createSpeaker(bool loop, int channel) { for (uint i = 0; i < ALStream::STREAM_BUFFERS; i++) { uint free_buffer = al->getFreeBuffer(); if (!preloadBuffer(free_buffer, loop)) { - break; + unusedBuffers.push(free_buffer); + } else { + AL_CHECK(alSourceQueueBuffers(free_source, 1, &free_buffer)); } - AL_CHECK(alSourceQueueBuffers(free_source, 1, &free_buffer)); } return std::make_unique(al, free_source, PRIORITY_HIGH, channel); } @@ -213,11 +214,11 @@ void ALStream::unqueueBuffers(uint alsource) { uint ALStream::enqueueBuffers(uint alsource) { uint preloaded = 0; if (!unusedBuffers.empty()) { - uint first_buffer = unusedBuffers.front(); - if (preloadBuffer(first_buffer, loop)) { + uint firstBuffer = unusedBuffers.front(); + if (preloadBuffer(firstBuffer, loop)) { preloaded++; unusedBuffers.pop(); - AL_CHECK(alSourceQueueBuffers(alsource, 1, &first_buffer)); + AL_CHECK(alSourceQueueBuffers(alsource, 1, &firstBuffer)); } } return preloaded; @@ -227,14 +228,14 @@ void ALStream::update(double delta) { if (this->speaker == 0) { return; } - auto p_speaker = audio::get_speaker(this->speaker); - if (p_speaker == nullptr) { + auto speaker = audio::get_speaker(this->speaker); + if (speaker == nullptr) { this->speaker = 0; return; } - ALSpeaker* alspeaker = dynamic_cast(p_speaker); + ALSpeaker* alspeaker = dynamic_cast(speaker); assert(alspeaker != nullptr); - if (alspeaker->stopped) { + if (alspeaker->manuallyStopped) { this->speaker = 0; return; } @@ -245,11 +246,11 @@ void ALStream::update(double delta) { uint preloaded = enqueueBuffers(alsource); // alspeaker->stopped is assigned to false at ALSpeaker::play(...) - if (p_speaker->isStopped() && !alspeaker->stopped) { //TODO: -V560 false-positive? - if (preloaded || dynamic_cast(source.get())) { - p_speaker->play(); - } else { - p_speaker->stop(); + if (speaker->isStopped() && !alspeaker->manuallyStopped) { //TODO: -V560 false-positive? + if (preloaded) { + speaker->play(); + } else if (isStopOnEnd()){ + speaker->stop(); } } } @@ -290,6 +291,14 @@ void ALStream::setTime(duration_t time) { } } +bool ALStream::isStopOnEnd() const { + return stopOnEnd; +} + +void ALStream::setStopOnEnd(bool flag) { + stopOnEnd = flag; +} + ALSpeaker::ALSpeaker(ALAudio* al, uint source, int priority, int channel) : al(al), priority(priority), channel(channel), source(source) { } @@ -356,7 +365,7 @@ void ALSpeaker::setLoop(bool loop) { void ALSpeaker::play() { paused = false; - stopped = false; + manuallyStopped = false; auto p_channel = get_channel(this->channel); AL_CHECK(alSourcef( source, @@ -372,7 +381,7 @@ void ALSpeaker::pause() { } void ALSpeaker::stop() { - stopped = true; + manuallyStopped = true; if (source) { AL_CHECK(alSourceStop(source)); @@ -436,6 +445,11 @@ int ALSpeaker::getPriority() const { return priority; } + +bool ALSpeaker::isManuallyStopped() const { + return manuallyStopped; +} + static bool alc_enumeration_ext = false; ALAudio::ALAudio(ALCdevice* device, ALCcontext* context) diff --git a/src/audio/AL/ALAudio.hpp b/src/audio/AL/ALAudio.hpp index 6fef62f0..39e6f337 100644 --- a/src/audio/AL/ALAudio.hpp +++ b/src/audio/AL/ALAudio.hpp @@ -58,6 +58,7 @@ namespace audio { bool keepSource; char buffer[BUFFER_SIZE]; bool loop = false; + bool stopOnEnd = false; bool preloadBuffer(uint buffer, bool loop); void unqueueBuffers(uint alsource); @@ -80,6 +81,10 @@ namespace audio { void setTime(duration_t time) override; static inline constexpr uint STREAM_BUFFERS = 3; + + bool isStopOnEnd() const override; + + void setStopOnEnd(bool stopOnEnd) override; }; class ALInputDevice : public InputDevice { @@ -117,7 +122,7 @@ namespace audio { float volume = 0.0f; public: ALStream* stream = nullptr; - bool stopped = true; + bool manuallyStopped = true; bool paused = false; uint source; duration_t duration = 0.0f; @@ -157,6 +162,8 @@ namespace audio { bool isRelative() const override; int getPriority() const override; + + bool isManuallyStopped() const override; }; class ALAudio : public Backend { diff --git a/src/audio/NoAudio.hpp b/src/audio/NoAudio.hpp index 5012704b..0da1b4d6 100644 --- a/src/audio/NoAudio.hpp +++ b/src/audio/NoAudio.hpp @@ -61,6 +61,13 @@ namespace audio { void setTime(duration_t time) override { } + + bool isStopOnEnd() const override { + return false; + } + + void setStopOnEnd(bool stopOnEnd) override { + } }; class NoAudio : public Backend { diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index 60f44b04..0069ff12 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -464,8 +464,15 @@ void audio::update(double delta) { speaker->update(channel); } if (speaker->isStopped()) { - streams.erase(it->first); - it = speakers.erase(it); + auto foundStream = streams.find(it->first); + if (foundStream == streams.end() || + (!speaker->isManuallyStopped() && + foundStream->second->isStopOnEnd())) { + streams.erase(it->first); + it = speakers.erase(it); + } else { + it++; + } } else { it++; } diff --git a/src/audio/audio.hpp b/src/audio/audio.hpp index c394398f..551fd7ec 100644 --- a/src/audio/audio.hpp +++ b/src/audio/audio.hpp @@ -221,6 +221,9 @@ namespace audio { /// @brief Set playhead to the selected time /// @param time selected time virtual void setTime(duration_t time) = 0; + + virtual bool isStopOnEnd() const = 0; + virtual void setStopOnEnd(bool stopOnEnd) = 0; }; /// @brief Sound is an audio asset that supposed to support many @@ -355,6 +358,8 @@ namespace audio { inline bool isStopped() const { return getState() == State::stopped; } + + virtual bool isManuallyStopped() const = 0; }; class Backend { From be2dc1abe54f896864fa2354ee3d17ae57a39f2e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 30 Oct 2025 22:48:53 +0300 Subject: [PATCH 16/28] add PCMStream:create_sound method --- res/scripts/hud.lua | 10 +++++++ src/audio/MemoryPCMStream.cpp | 4 +++ src/audio/MemoryPCMStream.hpp | 2 ++ .../lua/usertypes/lua_type_pcmstream.cpp | 26 +++++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/res/scripts/hud.lua b/res/scripts/hud.lua index 7730ba5f..a5aad766 100644 --- a/res/scripts/hud.lua +++ b/res/scripts/hud.lua @@ -122,6 +122,16 @@ function on_hud_open() stream = PCMStream(44100, 1, 16) stream:share("test-stream") streamid = audio.play_stream_2d("test-stream", 2.0, 1.0, "ui") + + + s = PCMStream(44100, 1, 8) + local buffer = Bytearray(44100) + for i=1, #buffer do + buffer[i] = math.random(1, 8) + end + s:feed(buffer) + s:create_sound("test-sound") + audio.play_sound_2d("test-sound", 2.0, 1.0, "ui") end function on_hud_render() diff --git a/src/audio/MemoryPCMStream.cpp b/src/audio/MemoryPCMStream.cpp index 7fcf4b0c..e0f40d5e 100644 --- a/src/audio/MemoryPCMStream.cpp +++ b/src/audio/MemoryPCMStream.cpp @@ -61,3 +61,7 @@ bool MemoryPCMStream::isSeekable() const { } void MemoryPCMStream::seek(size_t position) {} + +size_t MemoryPCMStream::available() const { + return buffer.size(); +} diff --git a/src/audio/MemoryPCMStream.hpp b/src/audio/MemoryPCMStream.hpp index 1f94d27e..bef2a05b 100644 --- a/src/audio/MemoryPCMStream.hpp +++ b/src/audio/MemoryPCMStream.hpp @@ -31,6 +31,8 @@ namespace audio { bool isSeekable() const override; void seek(size_t position) override; + + size_t available() const; private: uint sampleRate; uint channels; diff --git a/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp index 3367813e..a4a13ea4 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.cpp @@ -44,9 +44,35 @@ static int l_share(lua::State* L) { return 0; } +static int l_create_sound(lua::State* L) { + auto stream = touserdata(L, 1); + if (stream == nullptr) { + return 0; + } + auto alias = require_lstring(L, 2); + auto memoryStream = stream->getStream(); + + std::vector buffer(memoryStream->available()); + memoryStream->readFully(buffer.data(), buffer.size(), true); + + auto pcm = std::make_shared( + std::move(buffer), + 0, + memoryStream->getChannels(), + static_cast(memoryStream->getBitsPerSample()), + memoryStream->getSampleRate(), + memoryStream->isSeekable() + ); + auto sound = audio::create_sound(std::move(pcm), true); + auto assets = engine->getAssets(); + assets->store(std::move(sound), std::string(alias)); + return 0; +} + static std::unordered_map methods { {"feed", lua::wrap}, {"share", lua::wrap}, + {"create_sound", lua::wrap}, }; static int l_meta_meta_call(lua::State* L) { From 0e2b203fb2ad4ec508598abdd76ea2cf874340e7 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Oct 2025 22:53:04 +0300 Subject: [PATCH 17/28] add debug.get_pack_by_frame --- res/scripts/stdmin.lua | 80 +++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index df5c7ce2..4e01e22a 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -1,3 +1,40 @@ +function crc32(bytes, chksum) + local chksum = chksum or 0 + + local length = #bytes + if type(bytes) == "table" then + local buffer_len = _ffi.new('int[1]', length) + local buffer = _ffi.new( + string.format("char[%s]", length) + ) + for i=1, length do + buffer[i - 1] = bytes[i] + end + bytes = _ffi.string(buffer, buffer_len[0]) + end + return _crc32(bytes, chksum) +end + +-- Check if given table is an array +function is_array(x) + if #x > 0 then + return true + end + for k, v in pairs(x) do + return false + end + return true +end + +-- Get entry-point and filename from `entry-point:filename` path +function parse_path(path) + local index = string.find(path, ':') + if index == nil then + error("invalid path syntax (':' missing)") + end + return string.sub(path, 1, index-1), string.sub(path, index+1, -1) +end + local breakpoints = {} local dbg_steps_mode = false local dbg_step_into_func = false @@ -5,6 +42,7 @@ local hook_lock = false local current_func local current_func_stack_size +local __parse_path = parse_path local _debug_getinfo = debug.getinfo local _debug_getlocal = debug.getlocal local __pause = debug.pause @@ -74,6 +112,11 @@ local __sendvalue = debug.__sendvalue debug.__pull_events = nil debug.__sendvalue = nil +function debug.get_pack_by_frame(func) + local prefix, _ = __parse_path(_debug_getinfo(func, "S").source) + return prefix +end + function debug.pull_events() if not is_debugging then return @@ -194,43 +237,6 @@ function __vc_Canvas_set_data(self, data) self:_set_data(tostring(_ffi.cast("uintptr_t", canvas_ffi_buffer))) end -function crc32(bytes, chksum) - local chksum = chksum or 0 - - local length = #bytes - if type(bytes) == "table" then - local buffer_len = _ffi.new('int[1]', length) - local buffer = _ffi.new( - string.format("char[%s]", length) - ) - for i=1, length do - buffer[i - 1] = bytes[i] - end - bytes = _ffi.string(buffer, buffer_len[0]) - end - return _crc32(bytes, chksum) -end - --- Check if given table is an array -function is_array(x) - if #x > 0 then - return true - end - for k, v in pairs(x) do - return false - end - return true -end - --- Get entry-point and filename from `entry-point:filename` path -function parse_path(path) - local index = string.find(path, ':') - if index == nil then - error("invalid path syntax (':' missing)") - end - return string.sub(path, 1, index-1), string.sub(path, index+1, -1) -end - function pack.is_installed(packid) return file.isfile(packid..":package.json") end From 284f24433c15fe3e6fca24468b418f7885f8be0a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Oct 2025 23:18:26 +0300 Subject: [PATCH 18/28] extract debugging code to internal/debugging module --- res/modules/internal/debugging.lua | 146 +++++++++++++++++++++++++ res/scripts/stdlib.lua | 6 +- res/scripts/stdmin.lua | 152 +-------------------------- src/logic/scripting/lua/lua_util.cpp | 10 +- 4 files changed, 157 insertions(+), 157 deletions(-) create mode 100644 res/modules/internal/debugging.lua diff --git a/res/modules/internal/debugging.lua b/res/modules/internal/debugging.lua new file mode 100644 index 00000000..cef40202 --- /dev/null +++ b/res/modules/internal/debugging.lua @@ -0,0 +1,146 @@ +local breakpoints = {} +local dbg_steps_mode = false +local dbg_step_into_func = false +local hook_lock = false +local current_func +local current_func_stack_size + +local __parse_path = parse_path +local _debug_getinfo = debug.getinfo +local _debug_getlocal = debug.getlocal +local __pause = debug.pause +local __error = error +local __sethook = debug.sethook + +-- 'return' hook not called for some functions +-- todo: speedup +local function calc_stack_size() + local s = debug.traceback("", 2) + local count = 0 + for i in s:gmatch("\n") do + count = count + 1 + end + return count +end + +local is_debugging = debug.is_debugging() +if is_debugging then + __sethook(function (e, line) + if e == "return" then + local info = _debug_getinfo(2) + if info.func == current_func then + current_func = nil + end + end + if dbg_steps_mode and not hook_lock then + hook_lock = true + + if not dbg_step_into_func then + local func = _debug_getinfo(2).func + if func ~= current_func then + return + end + if current_func_stack_size ~= calc_stack_size() then + return + end + end + current_func = func + __pause("step") + debug.pull_events() + end + hook_lock = false + local bps = breakpoints[line] + if not bps then + return + end + local source = _debug_getinfo(2).source + if not bps[source] then + return + end + current_func = _debug_getinfo(2).func + current_func_stack_size = calc_stack_size() + __pause("breakpoint") + debug.pull_events() + end, "lr") +end + +local DBG_EVENT_SET_BREAKPOINT = 1 +local DBG_EVENT_RM_BREAKPOINT = 2 +local DBG_EVENT_STEP = 3 +local DBG_EVENT_STEP_INTO_FUNCTION = 4 +local DBG_EVENT_RESUME = 5 +local DBG_EVENT_GET_VALUE = 6 +local __pull_events = debug.__pull_events +local __sendvalue = debug.__sendvalue +debug.__pull_events = nil +debug.__sendvalue = nil + +function debug.get_pack_by_frame(func) + local prefix, _ = __parse_path(_debug_getinfo(func, "S").source) + return prefix +end + +function debug.pull_events() + if not is_debugging then + return + end + if not debug.is_debugging() then + is_debugging = false + __sethook() + end + local events = __pull_events() + if not events then + return + end + for i, event in ipairs(events) do + if event[1] == DBG_EVENT_SET_BREAKPOINT then + debug.set_breakpoint(event[2], event[3]) + elseif event[1] == DBG_EVENT_RM_BREAKPOINT then + debug.remove_breakpoint(event[2], event[3]) + elseif event[1] == DBG_EVENT_STEP then + dbg_steps_mode = true + dbg_step_into_func = false + elseif event[1] == DBG_EVENT_STEP_INTO_FUNCTION then + dbg_steps_mode = true + dbg_step_into_func = true + elseif event[1] == DBG_EVENT_RESUME then + dbg_steps_mode = false + dbg_step_into_func = false + elseif event[1] == DBG_EVENT_GET_VALUE then + local _, value = _debug_getlocal(event[2] + 3, event[3]) + for _, key in ipairs(event[4]) do + if value == nil then + value = "error: index nil value" + break + end + value = value[key] + end + __sendvalue(value, event[2], event[3], event[4]) + __pause() + end + end +end + +function debug.set_breakpoint(source, line) + local bps = breakpoints[line] + if not bps then + bps = {} + breakpoints[line] = bps + end + bps[source] = true +end + +function debug.remove_breakpoint(source, line) + local bps = breakpoints[line] + if not bps then + return + end + bps[source] = nil +end + +function error(message, level) + if is_debugging then + __pause("exception", message) + end + __error(message, level) +end diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 3f732e07..cb25150b 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -177,10 +177,8 @@ function inventory.set_description(invid, slot, description) inventory.set_data(invid, slot, "description", description) end -if enable_experimental then - require "core:internal/maths_inline" -end - +require "core:internal/maths_inline" +require "core:internal/debugging" asserts = require "core:internal/asserts" events = require "core:internal/events" diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 4e01e22a..783e054e 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -1,3 +1,6 @@ +local _ffi = ffi +local _debug_getinfo = debug.getinfo + function crc32(bytes, chksum) local chksum = chksum or 0 @@ -35,153 +38,6 @@ function parse_path(path) return string.sub(path, 1, index-1), string.sub(path, index+1, -1) end -local breakpoints = {} -local dbg_steps_mode = false -local dbg_step_into_func = false -local hook_lock = false -local current_func -local current_func_stack_size - -local __parse_path = parse_path -local _debug_getinfo = debug.getinfo -local _debug_getlocal = debug.getlocal -local __pause = debug.pause -local __error = error -local __sethook = debug.sethook - --- 'return' hook not called for some functions --- todo: speedup -local function calc_stack_size() - local s = debug.traceback("", 2) - local count = 0 - for i in s:gmatch("\n") do - count = count + 1 - end - return count -end - -local is_debugging = debug.is_debugging() -if is_debugging then - __sethook(function (e, line) - if e == "return" then - local info = _debug_getinfo(2) - if info.func == current_func then - current_func = nil - end - end - if dbg_steps_mode and not hook_lock then - hook_lock = true - - if not dbg_step_into_func then - local func = _debug_getinfo(2).func - if func ~= current_func then - return - end - if current_func_stack_size ~= calc_stack_size() then - return - end - end - current_func = func - __pause("step") - debug.pull_events() - end - hook_lock = false - local bps = breakpoints[line] - if not bps then - return - end - local source = _debug_getinfo(2).source - if not bps[source] then - return - end - current_func = _debug_getinfo(2).func - current_func_stack_size = calc_stack_size() - __pause("breakpoint") - debug.pull_events() - end, "lr") -end - -local DBG_EVENT_SET_BREAKPOINT = 1 -local DBG_EVENT_RM_BREAKPOINT = 2 -local DBG_EVENT_STEP = 3 -local DBG_EVENT_STEP_INTO_FUNCTION = 4 -local DBG_EVENT_RESUME = 5 -local DBG_EVENT_GET_VALUE = 6 -local __pull_events = debug.__pull_events -local __sendvalue = debug.__sendvalue -debug.__pull_events = nil -debug.__sendvalue = nil - -function debug.get_pack_by_frame(func) - local prefix, _ = __parse_path(_debug_getinfo(func, "S").source) - return prefix -end - -function debug.pull_events() - if not is_debugging then - return - end - if not debug.is_debugging() then - is_debugging = false - __sethook() - end - local events = __pull_events() - if not events then - return - end - for i, event in ipairs(events) do - if event[1] == DBG_EVENT_SET_BREAKPOINT then - debug.set_breakpoint(event[2], event[3]) - elseif event[1] == DBG_EVENT_RM_BREAKPOINT then - debug.remove_breakpoint(event[2], event[3]) - elseif event[1] == DBG_EVENT_STEP then - dbg_steps_mode = true - dbg_step_into_func = false - elseif event[1] == DBG_EVENT_STEP_INTO_FUNCTION then - dbg_steps_mode = true - dbg_step_into_func = true - elseif event[1] == DBG_EVENT_RESUME then - dbg_steps_mode = false - dbg_step_into_func = false - elseif event[1] == DBG_EVENT_GET_VALUE then - local _, value = _debug_getlocal(event[2] + 3, event[3]) - for _, key in ipairs(event[4]) do - if value == nil then - value = "error: index nil value" - break - end - value = value[key] - end - __sendvalue(value, event[2], event[3], event[4]) - __pause() - end - end -end - -function debug.set_breakpoint(source, line) - local bps = breakpoints[line] - if not bps then - bps = {} - breakpoints[line] = bps - end - bps[source] = true -end - -function debug.remove_breakpoint(source, line) - local bps = breakpoints[line] - if not bps then - return - end - bps[source] = nil -end - -function error(message, level) - if is_debugging then - __pause("exception", message) - end - __error(message, level) -end - -- Lua has no parallelizm, also _set_data does not call any lua functions so -- may be reused one global ffi buffer per lua_State local canvas_ffi_buffer @@ -764,7 +620,7 @@ function __vc__error(msg, frame, n, lastn) if events then local frames = debug.get_traceback(1) events.emit( - "core:error", msg, + "core:error", msg, table.sub(frames, 1 + (n or 0), lastn and #frames-lastn) ) end diff --git a/src/logic/scripting/lua/lua_util.cpp b/src/logic/scripting/lua/lua_util.cpp index 401255c7..a09f5086 100644 --- a/src/logic/scripting/lua/lua_util.cpp +++ b/src/logic/scripting/lua/lua_util.cpp @@ -155,18 +155,18 @@ static int l_error_handler(lua_State* L) { } int lua::call(State* L, int argc, int nresults) { - int handler_pos = gettop(L) - argc; + int handlerPos = gettop(L) - argc; pushcfunction(L, l_error_handler); - insert(L, handler_pos); + insert(L, handlerPos); int top = gettop(L); - if (lua_pcall(L, argc, nresults, handler_pos)) { + if (lua_pcall(L, argc, nresults, handlerPos)) { std::string log = tostring(L, -1); pop(L); - remove(L, handler_pos); + remove(L, handlerPos); throw luaerror(log); } int added = gettop(L) - (top - argc - 1); - remove(L, handler_pos); + remove(L, handlerPos); return added; } From fe8e4ba7b41c8c974c653248054d59cb2e2f255e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Oct 2025 23:30:29 +0300 Subject: [PATCH 19/28] add internal/audio_input module --- res/modules/internal/audio_input.lua | 27 +++++++++++++++++++++++++++ res/scripts/hud.lua | 10 ++++++++-- res/scripts/stdlib.lua | 1 + res/scripts/stdmin.lua | 2 +- res/texts/ru_RU.txt | 3 ++- 5 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 res/modules/internal/audio_input.lua diff --git a/res/modules/internal/audio_input.lua b/res/modules/internal/audio_input.lua new file mode 100644 index 00000000..3540e88b --- /dev/null +++ b/res/modules/internal/audio_input.lua @@ -0,0 +1,27 @@ +local audio_input_tokens_store = {} +audio.input = {} + +local _gui_confirm = gui.confirm +local _base64_encode_urlsafe = base64.encode_urlsafe +local _random_bytes = random.bytes +local _debug_pack_by_frame = debug.get_pack_by_frame +local _audio_fetch_input = audio.fetch_input + +function audio.fetch_input(token, size) + if audio_input_tokens_store[token] then + return _audio_fetch_input(size) + end + error("access denied") +end + +local GRAND_PERMISSION_MSG = "Grant '%{0}' pack audio recording permission?" + +function audio.input.request_open(callback) + local token = _base64_encode_urlsafe(_random_bytes(18)) + local caller = _debug_pack_by_frame(1) + _gui_confirm(gui.str(GRAND_PERMISSION_MSG):gsub("%%{0}", caller), function() + audio_input_tokens_store[token] = caller + callback(token) + menu:reset() + end) +end diff --git a/res/scripts/hud.lua b/res/scripts/hud.lua index a5aad766..73355ddf 100644 --- a/res/scripts/hud.lua +++ b/res/scripts/hud.lua @@ -134,6 +134,10 @@ function on_hud_open() audio.play_sound_2d("test-sound", 2.0, 1.0, "ui") end +audio.input.request_open(function(token) + input_access_token = token +end) + function on_hud_render() if hud.hand_controller then hud.hand_controller() @@ -141,6 +145,8 @@ function on_hud_render() update_hand() end - local bytes = audio.fetch_input() - stream:feed(bytes) + if input_access_token then + local bytes = audio.fetch_input(input_access_token) + stream:feed(bytes) + end end diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index cb25150b..d0077b6f 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -179,6 +179,7 @@ end require "core:internal/maths_inline" require "core:internal/debugging" +require "core:internal/audio_input" asserts = require "core:internal/asserts" events = require "core:internal/events" diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 783e054e..30343d7d 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -2,7 +2,7 @@ local _ffi = ffi local _debug_getinfo = debug.getinfo function crc32(bytes, chksum) - local chksum = chksum or 0 + chksum = chksum or 0 local length = #bytes if type(bytes) == "table" then diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt index 03ee8322..cc6de475 100644 --- a/res/texts/ru_RU.txt +++ b/res/texts/ru_RU.txt @@ -25,7 +25,8 @@ Grant %{0} pack modification permission?=Выдать разрешение на Error at line %{0}=Ошибка на строке %{0} Run=Запустить Filter=Фильтр -Are you sure you want to open the link: =Вы уверены, что хотите открыть ссылку: +Are you sure you want to open the link: =Вы уверены, что хотите открыть ссылку: +Grant '%{0}' pack audio recording permission?=Выдать паку '%{0}' разрешение на запись звука? editor.info.tooltip=CTRL+S - Сохранить\nCTRL+R - Запустить\nCTRL+Z - Отменить\nCTRL+Y - Повторить devtools.traceback=Стек вызовов (от последнего) From fcbbdd649bbad1214bf4bc44b6f87a6328711acf Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Oct 2025 23:54:56 +0300 Subject: [PATCH 20/28] split internal modules --- res/modules/internal/deprecated.lua | 48 ++ res/modules/internal/extensions/file.lua | 45 ++ res/modules/internal/extensions/inventory.lua | 71 +++ res/modules/internal/extensions/math.lua | 37 ++ res/modules/internal/extensions/pack.lua | 13 + res/modules/internal/extensions/string.lua | 126 +++++ res/modules/internal/extensions/table.lua | 179 +++++++ res/modules/internal/rules.lua | 70 +++ res/scripts/stdlib.lua | 206 +------- res/scripts/stdmin.lua | 456 +----------------- 10 files changed, 627 insertions(+), 624 deletions(-) create mode 100644 res/modules/internal/deprecated.lua create mode 100644 res/modules/internal/extensions/file.lua create mode 100644 res/modules/internal/extensions/inventory.lua create mode 100644 res/modules/internal/extensions/math.lua create mode 100644 res/modules/internal/extensions/pack.lua create mode 100644 res/modules/internal/extensions/string.lua create mode 100644 res/modules/internal/extensions/table.lua create mode 100644 res/modules/internal/rules.lua diff --git a/res/modules/internal/deprecated.lua b/res/modules/internal/deprecated.lua new file mode 100644 index 00000000..0be82795 --- /dev/null +++ b/res/modules/internal/deprecated.lua @@ -0,0 +1,48 @@ +-- --------- Deprecated functions ------ -- +local function wrap_deprecated(func, name, alternatives) + return function (...) + on_deprecated_call(name, alternatives) + return func(...) + end +end + +block_index = wrap_deprecated(block.index, "block_index", "block.index") +block_name = wrap_deprecated(block.name, "block_name", "block.name") +blocks_count = wrap_deprecated(block.defs_count, "blocks_count", "block.defs_count") +is_solid_at = wrap_deprecated(block.is_solid_at, "is_solid_at", "block.is_solid_at") +is_replaceable_at = wrap_deprecated(block.is_replaceable_at, "is_replaceable_at", "block.is_replaceable_at") +set_block = wrap_deprecated(block.set, "set_block", "block.set") +get_block = wrap_deprecated(block.get, "get_block", "block.get") +get_block_X = wrap_deprecated(block.get_X, "get_block_X", "block.get_X") +get_block_Y = wrap_deprecated(block.get_Y, "get_block_Y", "block.get_Y") +get_block_Z = wrap_deprecated(block.get_Z, "get_block_Z", "block.get_Z") +get_block_states = wrap_deprecated(block.get_states, "get_block_states", "block.get_states") +set_block_states = wrap_deprecated(block.set_states, "set_block_states", "block.set_states") +get_block_rotation = wrap_deprecated(block.get_rotation, "get_block_rotation", "block.get_rotation") +set_block_rotation = wrap_deprecated(block.set_rotation, "set_block_rotation", "block.set_rotation") +get_block_user_bits = wrap_deprecated(block.get_user_bits, "get_block_user_bits", "block.get_user_bits") +set_block_user_bits = wrap_deprecated(block.set_user_bits, "set_block_user_bits", "block.set_user_bits") + +function load_script(path, nocache) + on_deprecated_call("load_script", "require or loadstring") + return __load_script(path, nocache) +end + +_dofile = dofile +-- Replaces dofile('*/content/packid/*') with load_script('packid:*') +function dofile(path) + on_deprecated_call("dofile", "require or loadstring") + local index = string.find(path, "/content/") + if index then + local newpath = string.sub(path, index+9) + index = string.find(newpath, "/") + if index then + local label = string.sub(newpath, 1, index-1) + newpath = label..':'..string.sub(newpath, index+1) + if file.isfile(newpath) then + return __load_script(newpath, true) + end + end + end + return _dofile(path) +end diff --git a/res/modules/internal/extensions/file.lua b/res/modules/internal/extensions/file.lua new file mode 100644 index 00000000..0f104012 --- /dev/null +++ b/res/modules/internal/extensions/file.lua @@ -0,0 +1,45 @@ +function file.name(path) + return path:match("([^:/\\]+)$") +end + +function file.stem(path) + local name = file.name(path) + return name:match("(.+)%.[^%.]+$") or name +end + +function file.ext(path) + return path:match("%.([^:/\\]+)$") +end + +function file.prefix(path) + return path:match("^([^:]+)") +end + +function file.parent(path) + local dir = path:match("(.*)/") + if not dir then + return file.prefix(path)..":" + end + return dir +end + +function file.path(path) + local pos = path:find(':') + return path:sub(pos + 1) +end + +function file.join(a, b) + if a[#a] == ':' then + return a .. b + end + return a .. "/" .. b +end + +function file.readlines(path) + local str = file.read(path) + local lines = {} + for s in str:gmatch("[^\r\n]+") do + table.insert(lines, s) + end + return lines +end diff --git a/res/modules/internal/extensions/inventory.lua b/res/modules/internal/extensions/inventory.lua new file mode 100644 index 00000000..799eecb0 --- /dev/null +++ b/res/modules/internal/extensions/inventory.lua @@ -0,0 +1,71 @@ +function inventory.get_uses(invid, slot) + local uses = inventory.get_data(invid, slot, "uses") + if uses == nil then + return item.uses(inventory.get(invid, slot)) + end + return uses +end + +function inventory.use(invid, slot) + local itemid, count = inventory.get(invid, slot) + if itemid == nil then + return + end + local item_uses = inventory.get_uses(invid, slot) + if item_uses == nil then + return + end + if item_uses == 1 then + inventory.set(invid, slot, itemid, count - 1) + elseif item_uses > 1 then + inventory.set_data(invid, slot, "uses", item_uses - 1) + end +end + +function inventory.decrement(invid, slot, count) + count = count or 1 + local itemid, itemcount = inventory.get(invid, slot) + if itemcount <= count then + inventory.set(invid, slot, 0) + else + inventory.set_count(invid, slot, itemcount - count) + end +end + +function inventory.get_caption(invid, slot) + local item_id, count = inventory.get(invid, slot) + local caption = inventory.get_data(invid, slot, "caption") + if not caption then return item.caption(item_id) end + + return caption +end + +function inventory.set_caption(invid, slot, caption) + local itemid, itemcount = inventory.get(invid, slot) + if itemid == 0 then + return + end + if caption == nil or type(caption) ~= "string" then + caption = "" + end + inventory.set_data(invid, slot, "caption", caption) +end + +function inventory.get_description(invid, slot) + local item_id, count = inventory.get(invid, slot) + local description = inventory.get_data(invid, slot, "description") + if not description then return item.description(item_id) end + + return description +end + +function inventory.set_description(invid, slot, description) + local itemid, itemcount = inventory.get(invid, slot) + if itemid == 0 then + return + end + if description == nil or type(description) ~= "string" then + description = "" + end + inventory.set_data(invid, slot, "description", description) +end diff --git a/res/modules/internal/extensions/math.lua b/res/modules/internal/extensions/math.lua new file mode 100644 index 00000000..d906e94a --- /dev/null +++ b/res/modules/internal/extensions/math.lua @@ -0,0 +1,37 @@ +function math.clamp(_in, low, high) + return math.min(math.max(_in, low), high) +end + +function math.rand(low, high) + return low + (high - low) * math.random() +end + +function math.normalize(num, conf) + conf = conf or 1 + + return (num / conf) % 1 +end + +function math.round(num, places) + places = places or 0 + + local mult = 10 ^ places + return math.floor(num * mult + 0.5) / mult +end + +function math.sum(...) + local numbers = nil + local sum = 0 + + if type(...) == "table" then + numbers = ... + else + numbers = {...} + end + + for _, v in ipairs(numbers) do + sum = sum + v + end + + return sum +end diff --git a/res/modules/internal/extensions/pack.lua b/res/modules/internal/extensions/pack.lua new file mode 100644 index 00000000..c43c1203 --- /dev/null +++ b/res/modules/internal/extensions/pack.lua @@ -0,0 +1,13 @@ +function pack.is_installed(packid) + return file.isfile(packid..":package.json") +end + +function pack.data_file(packid, name) + file.mkdirs("world:data/"..packid) + return "world:data/"..packid.."/"..name +end + +function pack.shared_file(packid, name) + file.mkdirs("config:"..packid) + return "config:"..packid.."/"..name +end diff --git a/res/modules/internal/extensions/string.lua b/res/modules/internal/extensions/string.lua new file mode 100644 index 00000000..f76470df --- /dev/null +++ b/res/modules/internal/extensions/string.lua @@ -0,0 +1,126 @@ +local pattern_escape_replacements = { + ["("] = "%(", + [")"] = "%)", + ["."] = "%.", + ["%"] = "%%", + ["+"] = "%+", + ["-"] = "%-", + ["*"] = "%*", + ["?"] = "%?", + ["["] = "%[", + ["]"] = "%]", + ["^"] = "%^", + ["$"] = "%$", + ["\0"] = "%z" +} + +function string.pattern_safe(str) + return string.gsub(str, ".", pattern_escape_replacements) +end + +local string_sub = string.sub +local string_find = string.find +local string_len = string.len +function string.explode(separator, str, withpattern) + if (withpattern == nil) then withpattern = false end + + local ret = {} + local current_pos = 1 + + for i = 1, string_len(str) do + local start_pos, end_pos = string_find( + str, separator, current_pos, not withpattern) + if (not start_pos) then break end + ret[i] = string_sub(str, current_pos, start_pos - 1) + current_pos = end_pos + 1 + end + + ret[#ret + 1] = string_sub(str, current_pos) + + return ret +end + +function string.split(str, delimiter) + return string.explode(delimiter, str) +end + +function string.formatted_time(seconds, format) + if (not seconds) then seconds = 0 end + local hours = math.floor(seconds / 3600) + local minutes = math.floor((seconds / 60) % 60) + local millisecs = (seconds - math.floor(seconds)) * 1000 + seconds = math.floor(seconds % 60) + + if (format) then + return string.format(format, minutes, seconds, millisecs) + else + return { h = hours, m = minutes, s = seconds, ms = millisecs } + end +end + +function string.replace(str, tofind, toreplace) + local tbl = string.explode(tofind, str) + if (tbl[1]) then return table.concat(tbl, toreplace) end + return str +end + +function string.trim(s, char) + if char then char = string.pattern_safe(char) else char = "%s" end + return string.match(s, "^" .. char .. "*(.-)" .. char .. "*$") or s +end + +function string.trim_right(s, char) + if char then char = string.pattern_safe(char) else char = "%s" end + return string.match(s, "^(.-)" .. char .. "*$") or s +end + +function string.trim_left(s, char) + if char then char = string.pattern_safe(char) else char = "%s" end + return string.match(s, "^" .. char .. "*(.+)$") or s +end + +function string.pad(str, size, char) + char = char == nil and " " or char + + local padding = math.floor((size - #str) / 2) + local extra_padding = (size - #str) % 2 + + return string.rep(char, padding) .. str .. string.rep(char, padding + extra_padding) +end + +function string.left_pad(str, size, char) + char = char == nil and " " or char + + local left_padding = size - #str + return string.rep(char, left_padding) .. str +end + +function string.right_pad(str, size, char) + char = char == nil and " " or char + + local right_padding = size - #str + return str .. string.rep(char, right_padding) +end + +string.lower = utf8.lower +string.upper = utf8.upper +string.escape = utf8.escape + +local meta = getmetatable("") + +function meta:__index(key) + local val = string[key] + if (val ~= nil) then + return val + elseif (tonumber(key)) then + return string.sub(self, key, key) + end +end + +function string.starts_with(str, start) + return string.sub(str, 1, string.len(start)) == start +end + +function string.ends_with(str, endStr) + return endStr == "" or string.sub(str, -string.len(endStr)) == endStr +end diff --git a/res/modules/internal/extensions/table.lua b/res/modules/internal/extensions/table.lua new file mode 100644 index 00000000..33047df3 --- /dev/null +++ b/res/modules/internal/extensions/table.lua @@ -0,0 +1,179 @@ +function table.copy(t) + local copied = {} + + for k, v in pairs(t) do + copied[k] = v + end + + return copied +end + +function table.deep_copy(t) + local copied = {} + + for k, v in pairs(t) do + if type(v) == "table" then + copied[k] = table.deep_copy(v) + else + copied[k] = v + end + end + + return setmetatable(copied, getmetatable(t)) +end + +function table.count_pairs(t) + local count = 0 + + for k, v in pairs(t) do + count = count + 1 + end + + return count +end + +function table.random(t) + return t[math.random(1, #t)] +end + +function table.shuffle(t) + for i = #t, 2, -1 do + local j = math.random(i) + t[i], t[j] = t[j], t[i] + end + + return t +end + +function table.merge(t1, t2) + for i, v in pairs(t2) do + if type(i) == "number" then + t1[#t1 + 1] = v + elseif t1[i] == nil then + t1[i] = v + end + end + + return t1 +end + +function table.map(t, func) + for i, v in pairs(t) do + t[i] = func(i, v) + end + + return t +end + +function table.filter(t, func) + + for i = #t, 1, -1 do + if not func(i, t[i]) then + table.remove(t, i) + end + end + + local size = #t + + for i, v in pairs(t) do + local i_type = type(i) + if i_type == "number" then + if i < 1 or i > size then + if not func(i, v) then + t[i] = nil + end + end + else + if not func(i, v) then + t[i] = nil + end + end + end + + return t +end + +function table.set_default(t, key, default) + if t[key] == nil then + t[key] = default + return default + end + + return t[key] +end + +function table.flat(t) + local flat = {} + + for _, v in pairs(t) do + if type(v) == "table" then + table.merge(flat, v) + else + table.insert(flat, v) + end + end + + return flat +end + +function table.deep_flat(t) + local flat = {} + + for _, v in pairs(t) do + if type(v) == "table" then + table.merge(flat, table.deep_flat(v)) + else + table.insert(flat, v) + end + end + + return flat +end + +function table.sub(arr, start, stop) + local res = {} + start = start or 1 + stop = stop or #arr + + for i = start, stop do + table.insert(res, arr[i]) + end + + return res +end + +function table.has(t, x) + for i,v in ipairs(t) do + if v == x then + return true + end + end + return false +end + +function table.index(t, x) + for i,v in ipairs(t) do + if v == x then + return i + end + end + return -1 +end + +function table.remove_value(t, x) + local index = table.index(t, x) + if index ~= -1 then + table.remove(t, index) + end +end + +function table.tostring(t) + local s = '[' + for i,v in ipairs(t) do + s = s..tostring(v) + if i < #t then + s = s..', ' + end + end + return s..']' +end diff --git a/res/modules/internal/rules.lua b/res/modules/internal/rules.lua new file mode 100644 index 00000000..a1a5a8c3 --- /dev/null +++ b/res/modules/internal/rules.lua @@ -0,0 +1,70 @@ +local rules = {nexid = 1, rules = {}} + +function rules.get_rule(name) + local rule = rules.rules[name] + if rule == nil then + rule = {listeners={}} + rules.rules[name] = rule + end + return rule +end + +function rules.get(name) + local rule = rules.rules[name] + if rule == nil then + return nil + end + return rule.value +end + +function rules.set(name, value) + local rule = rules.get_rule(name) + rule.value = value + for _, handler in pairs(rule.listeners) do + handler(value) + end +end + +function rules.reset(name) + local rule = rules.get_rule(name) + rules.set(rule.default) +end + +function rules.listen(name, handler) + local rule = rules.get_rule(name) + local id = rules.nexid + rules.nextid = rules.nexid + 1 + rule.listeners[utf8.encode(id)] = handler + return id +end + +function rules.create(name, value, handler) + local rule = rules.get_rule(name) + rule.default = value + + local handlerid + if handler ~= nil then + handlerid = rules.listen(name, handler) + end + if rules.get(name) == nil then + rules.set(name, value) + elseif handler then + handler(rules.get(name)) + end + return handlerid +end + +function rules.unlisten(name, id) + local rule = rules.rules[name] + if rule == nil then + return + end + rule.listeners[utf8.encode(id)] = nil +end + +function rules.clear() + rules.rules = {} + rules.nextid = 1 +end + +return rules diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index d0077b6f..71a8256e 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -105,81 +105,10 @@ elseif __vc_app then complete_app_lib(__vc_app) end -function inventory.get_uses(invid, slot) - local uses = inventory.get_data(invid, slot, "uses") - if uses == nil then - return item.uses(inventory.get(invid, slot)) - end - return uses -end - -function inventory.use(invid, slot) - local itemid, count = inventory.get(invid, slot) - if itemid == nil then - return - end - local item_uses = inventory.get_uses(invid, slot) - if item_uses == nil then - return - end - if item_uses == 1 then - inventory.set(invid, slot, itemid, count - 1) - elseif item_uses > 1 then - inventory.set_data(invid, slot, "uses", item_uses - 1) - end -end - -function inventory.decrement(invid, slot, count) - count = count or 1 - local itemid, itemcount = inventory.get(invid, slot) - if itemcount <= count then - inventory.set(invid, slot, 0) - else - inventory.set_count(invid, slot, itemcount - count) - end -end - -function inventory.get_caption(invid, slot) - local item_id, count = inventory.get(invid, slot) - local caption = inventory.get_data(invid, slot, "caption") - if not caption then return item.caption(item_id) end - - return caption -end - -function inventory.set_caption(invid, slot, caption) - local itemid, itemcount = inventory.get(invid, slot) - if itemid == 0 then - return - end - if caption == nil or type(caption) ~= "string" then - caption = "" - end - inventory.set_data(invid, slot, "caption", caption) -end - -function inventory.get_description(invid, slot) - local item_id, count = inventory.get(invid, slot) - local description = inventory.get_data(invid, slot, "description") - if not description then return item.description(item_id) end - - return description -end - -function inventory.set_description(invid, slot, description) - local itemid, itemcount = inventory.get(invid, slot) - if itemid == 0 then - return - end - if description == nil or type(description) ~= "string" then - description = "" - end - inventory.set_data(invid, slot, "description", description) -end - require "core:internal/maths_inline" require "core:internal/debugging" require "core:internal/audio_input" +require "core:internal/extensions/inventory" asserts = require "core:internal/asserts" events = require "core:internal/events" @@ -306,86 +235,20 @@ else os.pid = ffi.C.getpid() end -ffi = nil -__vc_lock_internal_modules() - math.randomseed(time.uptime() * 1536227939) -rules = {nexid = 1, rules = {}} +rules = require "core:internal/rules" local _rules = rules -function _rules.get_rule(name) - local rule = _rules.rules[name] - if rule == nil then - rule = {listeners={}} - _rules.rules[name] = rule - end - return rule -end - -function _rules.get(name) - local rule = _rules.rules[name] - if rule == nil then - return nil - end - return rule.value -end - -function _rules.set(name, value) - local rule = _rules.get_rule(name) - rule.value = value - for _, handler in pairs(rule.listeners) do - handler(value) - end -end - -function _rules.reset(name) - local rule = _rules.get_rule(name) - _rules.set(rule.default) -end - -function _rules.listen(name, handler) - local rule = _rules.get_rule(name) - local id = _rules.nexid - _rules.nextid = _rules.nexid + 1 - rule.listeners[utf8.encode(id)] = handler - return id -end - -function _rules.create(name, value, handler) - local rule = _rules.get_rule(name) - rule.default = value - - local handlerid - if handler ~= nil then - handlerid = _rules.listen(name, handler) - end - if _rules.get(name) == nil then - _rules.set(name, value) - elseif handler then - handler(_rules.get(name)) - end - return handlerid -end - -function _rules.unlisten(name, id) - local rule = _rules.rules[name] - if rule == nil then - return - end - rule.listeners[utf8.encode(id)] = nil -end - -function _rules.clear() - _rules.rules = {} - _rules.nextid = 1 -end - function __vc_on_hud_open() + local _hud_is_content_access = hud._is_content_access + local _hud_set_content_access = hud._set_content_access + local _hud_set_debug_cheats = hud._set_debug_cheats + _rules.create("allow-cheats", true) - _rules.create("allow-content-access", hud._is_content_access(), function(value) - hud._set_content_access(value) + _rules.create("allow-content-access", _hud_is_content_access(), function(value) + _hud_set_content_access(value) end) _rules.create("allow-flight", true, function(value) input.set_enabled("player.flight", value) @@ -406,7 +269,7 @@ function __vc_on_hud_open() input.set_enabled("player.fast_interaction", value) end) _rules.create("allow-debug-cheats", true, function(value) - hud._set_debug_cheats(value) + _hud_set_debug_cheats(value) end) input.add_callback("devtools.console", function() if menu.page ~= "" then @@ -605,6 +468,7 @@ local _getinfo = debug.getinfo for i, name in ipairs(removed_names) do debug[name] = nil end + debug.getinfo = function(lvl, fields) if type(lvl) == "number" then lvl = lvl + 1 @@ -614,51 +478,7 @@ debug.getinfo = function(lvl, fields) return debuginfo end --- --------- Deprecated functions ------ -- -local function wrap_deprecated(func, name, alternatives) - return function (...) - on_deprecated_call(name, alternatives) - return func(...) - end -end +require "core:internal/deprecated" -block_index = wrap_deprecated(block.index, "block_index", "block.index") -block_name = wrap_deprecated(block.name, "block_name", "block.name") -blocks_count = wrap_deprecated(block.defs_count, "blocks_count", "block.defs_count") -is_solid_at = wrap_deprecated(block.is_solid_at, "is_solid_at", "block.is_solid_at") -is_replaceable_at = wrap_deprecated(block.is_replaceable_at, "is_replaceable_at", "block.is_replaceable_at") -set_block = wrap_deprecated(block.set, "set_block", "block.set") -get_block = wrap_deprecated(block.get, "get_block", "block.get") -get_block_X = wrap_deprecated(block.get_X, "get_block_X", "block.get_X") -get_block_Y = wrap_deprecated(block.get_Y, "get_block_Y", "block.get_Y") -get_block_Z = wrap_deprecated(block.get_Z, "get_block_Z", "block.get_Z") -get_block_states = wrap_deprecated(block.get_states, "get_block_states", "block.get_states") -set_block_states = wrap_deprecated(block.set_states, "set_block_states", "block.set_states") -get_block_rotation = wrap_deprecated(block.get_rotation, "get_block_rotation", "block.get_rotation") -set_block_rotation = wrap_deprecated(block.set_rotation, "set_block_rotation", "block.set_rotation") -get_block_user_bits = wrap_deprecated(block.get_user_bits, "get_block_user_bits", "block.get_user_bits") -set_block_user_bits = wrap_deprecated(block.set_user_bits, "set_block_user_bits", "block.set_user_bits") - -function load_script(path, nocache) - on_deprecated_call("load_script", "require or loadstring") - return __load_script(path, nocache) -end - -_dofile = dofile --- Replaces dofile('*/content/packid/*') with load_script('packid:*') -function dofile(path) - on_deprecated_call("dofile", "require or loadstring") - local index = string.find(path, "/content/") - if index then - local newpath = string.sub(path, index+9) - index = string.find(newpath, "/") - if index then - local label = string.sub(newpath, 1, index-1) - newpath = label..':'..string.sub(newpath, index+1) - if file.isfile(newpath) then - return __load_script(newpath, true) - end - end - end - return _dofile(path) -end +ffi = nil +__vc_lock_internal_modules() diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 30343d7d..7e8678e5 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -42,6 +42,26 @@ end -- may be reused one global ffi buffer per lua_State local canvas_ffi_buffer local canvas_ffi_buffer_size = 0 +local _ffi = ffi +function __vc_Canvas_set_data(self, data) + if type(data) == "cdata" then + self:_set_data(tostring(_ffi.cast("uintptr_t", data))) + end + local width = self.width + local height = self.height + + local size = width * height * 4 + if size > canvas_ffi_buffer_size then + canvas_ffi_buffer = _ffi.new( + string.format("unsigned char[%s]", size) + ) + canvas_ffi_buffer_size = size + end + for i=0, size - 1 do + canvas_ffi_buffer[i] = data[i + 1] + end + self:_set_data(tostring(_ffi.cast("uintptr_t", canvas_ffi_buffer))) +end local ipairs_mt_supported = false for i, _ in ipairs(setmetatable({l={1}}, { @@ -72,42 +92,6 @@ function await(co) return res, err end -local _ffi = ffi -function __vc_Canvas_set_data(self, data) - if type(data) == "cdata" then - self:_set_data(tostring(_ffi.cast("uintptr_t", data))) - end - local width = self.width - local height = self.height - - local size = width * height * 4 - if size > canvas_ffi_buffer_size then - canvas_ffi_buffer = _ffi.new( - string.format("unsigned char[%s]", size) - ) - canvas_ffi_buffer_size = size - end - for i=0, size - 1 do - canvas_ffi_buffer[i] = data[i + 1] - end - self:_set_data(tostring(_ffi.cast("uintptr_t", canvas_ffi_buffer))) -end - -function pack.is_installed(packid) - return file.isfile(packid..":package.json") -end - -function pack.data_file(packid, name) - file.mkdirs("world:data/"..packid) - return "world:data/"..packid.."/"..name -end - -function pack.shared_file(packid, name) - file.mkdirs("config:"..packid) - return "config:"..packid.."/"..name -end - - function timeit(iters, func, ...) local tm = os.clock() for i=1,iters do @@ -118,364 +102,6 @@ end ---------------------------------------------- -function math.clamp(_in, low, high) - return math.min(math.max(_in, low), high) -end - -function math.rand(low, high) - return low + (high - low) * math.random() -end - -function math.normalize(num, conf) - conf = conf or 1 - - return (num / conf) % 1 -end - -function math.round(num, places) - places = places or 0 - - local mult = 10 ^ places - return math.floor(num * mult + 0.5) / mult -end - -function math.sum(...) - local numbers = nil - local sum = 0 - - if type(...) == "table" then - numbers = ... - else - numbers = {...} - end - - for _, v in ipairs(numbers) do - sum = sum + v - end - - return sum -end - ----------------------------------------------- - -function table.copy(t) - local copied = {} - - for k, v in pairs(t) do - copied[k] = v - end - - return copied -end - -function table.deep_copy(t) - local copied = {} - - for k, v in pairs(t) do - if type(v) == "table" then - copied[k] = table.deep_copy(v) - else - copied[k] = v - end - end - - return setmetatable(copied, getmetatable(t)) -end - -function table.count_pairs(t) - local count = 0 - - for k, v in pairs(t) do - count = count + 1 - end - - return count -end - -function table.random(t) - return t[math.random(1, #t)] -end - -function table.shuffle(t) - for i = #t, 2, -1 do - local j = math.random(i) - t[i], t[j] = t[j], t[i] - end - - return t -end - -function table.merge(t1, t2) - for i, v in pairs(t2) do - if type(i) == "number" then - t1[#t1 + 1] = v - elseif t1[i] == nil then - t1[i] = v - end - end - - return t1 -end - -function table.map(t, func) - for i, v in pairs(t) do - t[i] = func(i, v) - end - - return t -end - -function table.filter(t, func) - - for i = #t, 1, -1 do - if not func(i, t[i]) then - table.remove(t, i) - end - end - - local size = #t - - for i, v in pairs(t) do - local i_type = type(i) - if i_type == "number" then - if i < 1 or i > size then - if not func(i, v) then - t[i] = nil - end - end - else - if not func(i, v) then - t[i] = nil - end - end - end - - return t -end - -function table.set_default(t, key, default) - if t[key] == nil then - t[key] = default - return default - end - - return t[key] -end - -function table.flat(t) - local flat = {} - - for _, v in pairs(t) do - if type(v) == "table" then - table.merge(flat, v) - else - table.insert(flat, v) - end - end - - return flat -end - -function table.deep_flat(t) - local flat = {} - - for _, v in pairs(t) do - if type(v) == "table" then - table.merge(flat, table.deep_flat(v)) - else - table.insert(flat, v) - end - end - - return flat -end - -function table.sub(arr, start, stop) - local res = {} - start = start or 1 - stop = stop or #arr - - for i = start, stop do - table.insert(res, arr[i]) - end - - return res -end - ----------------------------------------------- - -local pattern_escape_replacements = { - ["("] = "%(", - [")"] = "%)", - ["."] = "%.", - ["%"] = "%%", - ["+"] = "%+", - ["-"] = "%-", - ["*"] = "%*", - ["?"] = "%?", - ["["] = "%[", - ["]"] = "%]", - ["^"] = "%^", - ["$"] = "%$", - ["\0"] = "%z" -} - -function string.pattern_safe(str) - return string.gsub(str, ".", pattern_escape_replacements) -end - -local string_sub = string.sub -local string_find = string.find -local string_len = string.len -function string.explode(separator, str, withpattern) - if (withpattern == nil) then withpattern = false end - - local ret = {} - local current_pos = 1 - - for i = 1, string_len(str) do - local start_pos, end_pos = string_find( - str, separator, current_pos, not withpattern) - if (not start_pos) then break end - ret[i] = string_sub(str, current_pos, start_pos - 1) - current_pos = end_pos + 1 - end - - ret[#ret + 1] = string_sub(str, current_pos) - - return ret -end - -function string.split(str, delimiter) - return string.explode(delimiter, str) -end - -function string.formatted_time(seconds, format) - if (not seconds) then seconds = 0 end - local hours = math.floor(seconds / 3600) - local minutes = math.floor((seconds / 60) % 60) - local millisecs = (seconds - math.floor(seconds)) * 1000 - seconds = math.floor(seconds % 60) - - if (format) then - return string.format(format, minutes, seconds, millisecs) - else - return { h = hours, m = minutes, s = seconds, ms = millisecs } - end -end - -function string.replace(str, tofind, toreplace) - local tbl = string.explode(tofind, str) - if (tbl[1]) then return table.concat(tbl, toreplace) end - return str -end - -function string.trim(s, char) - if char then char = string.pattern_safe(char) else char = "%s" end - return string.match(s, "^" .. char .. "*(.-)" .. char .. "*$") or s -end - -function string.trim_right(s, char) - if char then char = string.pattern_safe(char) else char = "%s" end - return string.match(s, "^(.-)" .. char .. "*$") or s -end - -function string.trim_left(s, char) - if char then char = string.pattern_safe(char) else char = "%s" end - return string.match(s, "^" .. char .. "*(.+)$") or s -end - -function string.pad(str, size, char) - char = char == nil and " " or char - - local padding = math.floor((size - #str) / 2) - local extra_padding = (size - #str) % 2 - - return string.rep(char, padding) .. str .. string.rep(char, padding + extra_padding) -end - -function string.left_pad(str, size, char) - char = char == nil and " " or char - - local left_padding = size - #str - return string.rep(char, left_padding) .. str -end - -function string.right_pad(str, size, char) - char = char == nil and " " or char - - local right_padding = size - #str - return str .. string.rep(char, right_padding) -end - -string.lower = utf8.lower -string.upper = utf8.upper -string.escape = utf8.escape - -local meta = getmetatable("") - -function meta:__index(key) - local val = string[key] - if (val ~= nil) then - return val - elseif (tonumber(key)) then - return string.sub(self, key, key) - end -end - -function string.starts_with(str, start) - return string.sub(str, 1, string.len(start)) == start -end - -function string.ends_with(str, endStr) - return endStr == "" or string.sub(str, -string.len(endStr)) == endStr -end - -function table.has(t, x) - for i,v in ipairs(t) do - if v == x then - return true - end - end - return false -end - -function table.index(t, x) - for i,v in ipairs(t) do - if v == x then - return i - end - end - return -1 -end - -function table.remove_value(t, x) - local index = table.index(t, x) - if index ~= -1 then - table.remove(t, index) - end -end - -function table.tostring(t) - local s = '[' - for i,v in ipairs(t) do - s = s..tostring(v) - if i < #t then - s = s..', ' - end - end - return s..']' -end - -function file.readlines(path) - local str = file.read(path) - local lines = {} - for s in str:gmatch("[^\r\n]+") do - table.insert(lines, s) - end - return lines -end - function debug.count_frames() local frames = 1 while true do @@ -634,43 +260,11 @@ function __vc_warning(msg, detail, n) end end -function file.name(path) - return path:match("([^:/\\]+)$") -end - -function file.stem(path) - local name = file.name(path) - return name:match("(.+)%.[^%.]+$") or name -end - -function file.ext(path) - return path:match("%.([^:/\\]+)$") -end - -function file.prefix(path) - return path:match("^([^:]+)") -end - -function file.parent(path) - local dir = path:match("(.*)/") - if not dir then - return file.prefix(path)..":" - end - return dir -end - -function file.path(path) - local pos = path:find(':') - return path:sub(pos + 1) -end - -function file.join(a, b) - if a[#a] == ':' then - return a .. b - end - return a .. "/" .. b -end - +require "core:internal/extensions/pack" +require "core:internal/extensions/math" +require "core:internal/extensions/file" +require "core:internal/extensions/table" +require "core:internal/extensions/string" bit.compile = require "core:bitwise/compiler" bit.execute = require "core:bitwise/executor" From 5a65cbe07128a149dba7d44bbd69154da871d6c8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 1 Nov 2025 22:04:39 +0300 Subject: [PATCH 21/28] move PCMStream to audio --- res/scripts/hud.lua | 4 ++-- src/logic/scripting/lua/lua_engine.cpp | 8 +++++++- src/logic/scripting/lua/usertypes/lua_type_pcmstream.hpp | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/res/scripts/hud.lua b/res/scripts/hud.lua index 73355ddf..db6f9b79 100644 --- a/res/scripts/hud.lua +++ b/res/scripts/hud.lua @@ -119,12 +119,12 @@ function on_hud_open() hud.default_hand_controller = update_hand - stream = PCMStream(44100, 1, 16) + stream = audio.PCMStream(44100, 1, 16) stream:share("test-stream") streamid = audio.play_stream_2d("test-stream", 2.0, 1.0, "ui") - s = PCMStream(44100, 1, 8) + s = audio.PCMStream(44100, 1, 8) local buffer = Bytearray(44100) for i=1, #buffer do buffer[i] = math.random(1, 8) diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index c4507bb1..80e2cff7 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -131,7 +131,6 @@ void lua::init_state(State* L, StateType stateType) { newusertype(L); newusertype(L); newusertype(L); - newusertype(L); } void lua::initialize(const EnginePaths& paths, const CoreParameters& params) { @@ -186,5 +185,12 @@ State* lua::create_state(const EnginePaths& paths, StateType stateType) { } pop(L); } + newusertype(L); + if (getglobal(L, "audio")) { + if (getglobal(L, "__vc_PCMStream")) { + setfield(L, "PCMStream"); + } + pop(L); + } return L; } diff --git a/src/logic/scripting/lua/usertypes/lua_type_pcmstream.hpp b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.hpp index 47979693..524c7169 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_pcmstream.hpp +++ b/src/logic/scripting/lua/usertypes/lua_type_pcmstream.hpp @@ -18,7 +18,7 @@ namespace lua { return TYPENAME; } static int createMetatable(lua::State*); - inline static std::string TYPENAME = "PCMStream"; + inline static std::string TYPENAME = "__vc_PCMStream"; private: std::shared_ptr stream; }; From 86a4060a689e133d645429f08fcc217f847c13a9 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 1 Nov 2025 23:06:26 +0300 Subject: [PATCH 22/28] feat: multiple audio fetchers support --- res/modules/internal/audio_input.lua | 21 +++++++++++++++++---- res/modules/internal/bytearray.lua | 21 +++++++++++++++++++-- res/scripts/stdlib.lua | 9 ++++----- res/scripts/stdmin.lua | 6 ++++++ src/logic/scripting/lua/libs/libaudio.cpp | 2 +- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/res/modules/internal/audio_input.lua b/res/modules/internal/audio_input.lua index 3540e88b..c0aa638e 100644 --- a/res/modules/internal/audio_input.lua +++ b/res/modules/internal/audio_input.lua @@ -5,21 +5,34 @@ local _gui_confirm = gui.confirm local _base64_encode_urlsafe = base64.encode_urlsafe local _random_bytes = random.bytes local _debug_pack_by_frame = debug.get_pack_by_frame -local _audio_fetch_input = audio.fetch_input +local _audio_fetch_input = audio.__fetch_input +audio.__fetch_input = nil +local MAX_FETCH = 44100 * 4 + +local total_fetch = Bytearray() + +function audio.__reset_fetch_buffer() + total_fetch:clear() +end function audio.fetch_input(token, size) + size = size or MAX_FETCH if audio_input_tokens_store[token] then - return _audio_fetch_input(size) + if #total_fetch >= size then + return total_fetch:sub(1, size) + end + total_fetch:append(_audio_fetch_input(size - #total_fetch)) + return total_fetch:sub() end error("access denied") end -local GRAND_PERMISSION_MSG = "Grant '%{0}' pack audio recording permission?" +local GRANT_PERMISSION_MSG = "Grant '%{0}' pack audio recording permission?" function audio.input.request_open(callback) local token = _base64_encode_urlsafe(_random_bytes(18)) local caller = _debug_pack_by_frame(1) - _gui_confirm(gui.str(GRAND_PERMISSION_MSG):gsub("%%{0}", caller), function() + _gui_confirm(gui.str(GRANT_PERMISSION_MSG):gsub("%%{0}", caller), function() audio_input_tokens_store[token] = caller callback(token) menu:reset() diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index 56795499..7d5d250e 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -14,6 +14,8 @@ FFI.cdef[[ local malloc = FFI.C.malloc local free = FFI.C.free +local FFIBytearray +local bytearray_type local function grow_buffer(self, elems) local new_capacity = math.ceil(self.capacity / 0.75 + elems) @@ -119,6 +121,20 @@ local function get_capacity(self) return self.capacity end +local function sub(self, offset, length) + offset = offset or 1 + length = length or (self.size - offset + 1) + if offset < 1 or offset > self.size then + return FFIBytearray(0) + end + if offset + length - 1 > self.size then + length = self.size - offset + 1 + end + local buffer = malloc(length) + FFI.copy(buffer, self.bytes + (offset - 1), length) + return bytearray_type(buffer, length, length) +end + local bytearray_methods = { append=append, insert=insert, @@ -127,6 +143,7 @@ local bytearray_methods = { clear=clear, reserve=reserve, get_capacity=get_capacity, + sub=sub, } local bytearray_mt = { @@ -168,9 +185,9 @@ local bytearray_mt = { } bytearray_mt.__pairs = bytearray_mt.__ipairs -local bytearray_type = FFI.metatype("bytearray_t", bytearray_mt) +bytearray_type = FFI.metatype("bytearray_t", bytearray_mt) -local FFIBytearray = { +FFIBytearray = { __call = function (self, n) local t = type(n) if t == "string" then diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 71a8256e..19c17384 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -211,11 +211,6 @@ entities.get_all = function(uids) end end -local bytearray = require "core:internal/bytearray" -Bytearray = bytearray.FFIBytearray -Bytearray_as_string = bytearray.FFIBytearray_as_string -Bytearray_construct = function(...) return Bytearray(...) end - __vc_scripts_registry = require "core:internal/scripts_registry" file.open = require "core:internal/stream_providers/file" @@ -424,6 +419,9 @@ end local __post_runnables = {} +local fn_audio_reset_fetch_buffer = audio.__reset_fetch_buffer +audio.__reset_fetch_buffer = nil + function __process_post_runnables() if #__post_runnables then for _, func in ipairs(__post_runnables) do @@ -449,6 +447,7 @@ function __process_post_runnables() __vc_named_coroutines[name] = nil end + fn_audio_reset_fetch_buffer() debug.pull_events() network.__process_events() block.__process_register_events() diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 7e8678e5..49714b99 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -265,6 +265,12 @@ require "core:internal/extensions/math" require "core:internal/extensions/file" require "core:internal/extensions/table" require "core:internal/extensions/string" + +local bytearray = require "core:internal/bytearray" +Bytearray = bytearray.FFIBytearray +Bytearray_as_string = bytearray.FFIBytearray_as_string +Bytearray_construct = function(...) return Bytearray(...) end + bit.compile = require "core:bitwise/compiler" bit.execute = require "core:bitwise/executor" diff --git a/src/logic/scripting/lua/libs/libaudio.cpp b/src/logic/scripting/lua/libs/libaudio.cpp index ed2e3db6..37c26549 100644 --- a/src/logic/scripting/lua/libs/libaudio.cpp +++ b/src/logic/scripting/lua/libs/libaudio.cpp @@ -476,7 +476,7 @@ const luaL_Reg audiolib[] = { {"get_velocity", lua::wrap}, {"count_speakers", lua::wrap}, {"count_streams", lua::wrap}, - {"fetch_input", lua::wrap}, + {"__fetch_input", lua::wrap}, {"get_input_devices_names", lua::wrap}, {"get_output_devices_names", lua::wrap}, {"set_input_device", lua::wrap}, From aa54e900d9b10f9e2e32dacc340bd65551d4a672 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Nov 2025 01:17:56 +0300 Subject: [PATCH 23/28] add U16view, I16view, U32view, I32view classes --- res/modules/internal/audio_input.lua | 4 +- res/modules/internal/bytearray.lua | 61 ++++++++++++++++++++++++++-- res/scripts/hud.lua | 9 ++-- res/scripts/stdmin.lua | 4 ++ 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/res/modules/internal/audio_input.lua b/res/modules/internal/audio_input.lua index c0aa638e..b87c7071 100644 --- a/res/modules/internal/audio_input.lua +++ b/res/modules/internal/audio_input.lua @@ -19,10 +19,10 @@ function audio.fetch_input(token, size) size = size or MAX_FETCH if audio_input_tokens_store[token] then if #total_fetch >= size then - return total_fetch:sub(1, size) + return total_fetch:slice(1, size) end total_fetch:append(_audio_fetch_input(size - #total_fetch)) - return total_fetch:sub() + return total_fetch:slice() end error("access denied") end diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index 7d5d250e..c292d6a8 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -121,7 +121,7 @@ local function get_capacity(self) return self.capacity end -local function sub(self, offset, length) +local function slice(self, offset, length) offset = offset or 1 length = length or (self.size - offset + 1) if offset < 1 or offset > self.size then @@ -131,6 +131,9 @@ local function sub(self, offset, length) length = self.size - offset + 1 end local buffer = malloc(length) + if not buffer then + error("malloc(" .. length .. ") returned NULL") + end FFI.copy(buffer, self.bytes + (offset - 1), length) return bytearray_type(buffer, length, length) end @@ -143,7 +146,7 @@ local bytearray_methods = { clear=clear, reserve=reserve, get_capacity=get_capacity, - sub=sub, + slice=slice, } local bytearray_mt = { @@ -227,7 +230,59 @@ local function FFIBytearray_as_string(bytes) end end +local function create_FFIview_class(name, typename, typesize) + local FFIU16view_mt = { + __index = function(self, key) + if key <= 0 or key > self.size then + return + end + return self.ptr[key - 1] + end, + __newindex = function(self, key, value) + if key == self.size + 1 then + return append(self, value) + elseif key <= 0 or key > self.size then + return + end + self.ptr[key - 1] = value + end, + __len = function(self) + return self.size + end, + __tostring = function(self) + return string.format(name .. "[%s]{...}", tonumber(self.size)) + end, + __ipairs = function(self) + local i = 0 + return function() + i = i + 1 + if i <= self.size then + return i, self.ptr[i - 1] + end + end + end + } + return function (bytes) + local ptr = FFI.cast(typename .. "*", bytes.bytes) + local x = setmetatable({ + bytes=bytes, + ptr=ptr, + size=math.floor(bytes.size / typesize), + }, FFIU16view_mt) + return x + end +end + +local FFII16view = create_FFIview_class("FFII16view", "int16_t", 2) +local FFIU16view = create_FFIview_class("FFIU16view", "uint16_t", 2) +local FFII32view = create_FFIview_class("FFII32view", "int32_t", 4) +local FFIU32view = create_FFIview_class("FFIU32view", "uint32_t", 4) + return { FFIBytearray = setmetatable(FFIBytearray, FFIBytearray), - FFIBytearray_as_string = FFIBytearray_as_string + FFIBytearray_as_string = FFIBytearray_as_string, + FFIU16view = FFIU16view, + FFII16view = FFII16view, + FFIU32view = FFIU32view, + FFII32view = FFII32view, } diff --git a/res/scripts/hud.lua b/res/scripts/hud.lua index db6f9b79..3e2fecb1 100644 --- a/res/scripts/hud.lua +++ b/res/scripts/hud.lua @@ -124,10 +124,11 @@ function on_hud_open() streamid = audio.play_stream_2d("test-stream", 2.0, 1.0, "ui") - s = audio.PCMStream(44100, 1, 8) - local buffer = Bytearray(44100) - for i=1, #buffer do - buffer[i] = math.random(1, 8) + s = audio.PCMStream(44100, 1, 16) + local buffer = Bytearray(44100 * 4) + local view = U16view(buffer) + for i=1, view.size do + view[i] = 16000 + math.sin(i / 200.0 * (1 + i * 0.0001)) * 2000 end s:feed(buffer) s:create_sound("test-sound") diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 49714b99..63409c36 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -269,6 +269,10 @@ require "core:internal/extensions/string" local bytearray = require "core:internal/bytearray" Bytearray = bytearray.FFIBytearray Bytearray_as_string = bytearray.FFIBytearray_as_string +U16view = bytearray.FFIU16view +I16view = bytearray.FFII16view +U32view = bytearray.FFIU32view +I32view = bytearray.FFII32view Bytearray_construct = function(...) return Bytearray(...) end bit.compile = require "core:bitwise/compiler" From 88e61125d198d0c66482b3b8afa8295910d6f9f5 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 4 Nov 2025 19:47:35 +0300 Subject: [PATCH 24/28] add device select to audio settings --- res/layouts/pages/settings_audio.xml.lua | 34 +++++++++++++++++++++++ res/layouts/templates/problem.xml | 2 +- res/layouts/templates/script_file.xml | 2 +- res/modules/internal/audio_input.lua | 33 ++++++++++++++++++---- res/scripts/hud.lua | 8 ++++-- src/audio/AL/ALAudio.cpp | 11 ++++++++ src/audio/AL/ALAudio.hpp | 2 ++ src/audio/audio.hpp | 5 ++++ src/logic/scripting/lua/libs/libaudio.cpp | 4 ++- 9 files changed, 91 insertions(+), 10 deletions(-) diff --git a/res/layouts/pages/settings_audio.xml.lua b/res/layouts/pages/settings_audio.xml.lua index 9b29bd52..1b14cf04 100644 --- a/res/layouts/pages/settings_audio.xml.lua +++ b/res/layouts/pages/settings_audio.xml.lua @@ -24,10 +24,44 @@ function update_setting(x, id, name, postfix) ) end +local initialized = false + function on_open() + if not initialized then + initialized = true + local token = audio.input.__get_core_token() + document.root:add("") + local prev_amplitude = 0.0 + document.tm:setInterval(16, function() + audio.input.fetch_input(token) + local amplitude = audio.input.get_max_amplitude() + if amplitude > 0.0 then + amplitude = math.sqrt(amplitude) + end + document.input_volume_inner.size = { + prev_amplitude * + document.input_volume_outer.size[1], + document.input_volume_outer.size[2] + } + prev_amplitude = amplitude * 0.25 + prev_amplitude * 0.75 + end) + end create_setting("audio.volume-master", "Master Volume", 0.01) create_setting("audio.volume-regular", "Regular Sounds", 0.01) create_setting("audio.volume-ui", "UI Sounds", 0.01) create_setting("audio.volume-ambient", "Ambient", 0.01) create_setting("audio.volume-music", "Music", 0.01) + document.root:add("") document.root:add("" - .."" + .."" .."") local selectbox = document.input_device_select local devices = {} diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt index cc6de475..9dec5dfd 100644 --- a/res/texts/ru_RU.txt +++ b/res/texts/ru_RU.txt @@ -106,6 +106,7 @@ settings.Shadows quality=Качество теней settings.Conflict=Найдены возможные конфликты settings.Windowed=Оконный settings.Borderless=Безрамочный +settings.Microphone=Микрофон # Управление chunks.reload=Перезагрузить Чанки From ebe82205482159d732a78ef7ee1080c57f2d9044 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 6 Nov 2025 12:25:21 +0300 Subject: [PATCH 26/28] cleanup --- res/scripts/hud.lua | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/res/scripts/hud.lua b/res/scripts/hud.lua index 0124c93d..90883d94 100644 --- a/res/scripts/hud.lua +++ b/res/scripts/hud.lua @@ -55,8 +55,6 @@ local function update_hand() skeleton.set_model("hand", bone, item.model_name(itemid)) end -local stream - function on_hud_open() input.add_callback("player.pick", function () if hud.is_paused() or hud.is_inventory_open() then @@ -118,40 +116,12 @@ function on_hud_open() configure_SSAO() hud.default_hand_controller = update_hand - - debug.print(audio.get_input_devices_names()) - - stream = audio.PCMStream(44100, 1, 16) - stream:share("test-stream") - streamid = audio.play_stream_2d("test-stream", 2.0, 1.0, "ui") - - - s = audio.PCMStream(44100, 1, 16) - local buffer = Bytearray(44100 * 4) - local view = U16view(buffer) - for i=1, view.size do - view[i] = 16000 + math.sin(i / 200.0 * (1 + i * 0.0001)) * 2000 - end - s:feed(buffer) - s:create_sound("test-sound") - audio.play_sound_2d("test-sound", 2.0, 1.0, "ui") end -audio.input.request_open(function(token) - input_access_token = token -end) - function on_hud_render() if hud.hand_controller then hud.hand_controller() else update_hand() end - - if input_access_token then - local bytes = audio.input.fetch_input(input_access_token) - if bytes then - stream:feed(bytes) - end - end end From be1f336d90a7ed90c3099a5326021e5081b275f8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 7 Nov 2025 22:32:16 +0300 Subject: [PATCH 27/28] rename audio.input.fetch_input to audio.input.fetch --- res/layouts/pages/settings_audio.xml.lua | 2 +- res/modules/internal/audio_input.lua | 2 +- src/logic/scripting/lua/libs/libaudio.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/layouts/pages/settings_audio.xml.lua b/res/layouts/pages/settings_audio.xml.lua index 82fc7f0b..29859819 100644 --- a/res/layouts/pages/settings_audio.xml.lua +++ b/res/layouts/pages/settings_audio.xml.lua @@ -33,7 +33,7 @@ function on_open() document.root:add("") local prev_amplitude = 0.0 document.tm:setInterval(16, function() - audio.input.fetch_input(token) + audio.input.fetch(token) local amplitude = audio.input.get_max_amplitude() if amplitude > 0.0 then amplitude = math.sqrt(amplitude) diff --git a/res/modules/internal/audio_input.lua b/res/modules/internal/audio_input.lua index f61d5300..adf44b97 100644 --- a/res/modules/internal/audio_input.lua +++ b/res/modules/internal/audio_input.lua @@ -24,7 +24,7 @@ function audio.input.get_max_amplitude() return max_amplitude / MAX_AMPLITUDE end -function audio.input.fetch_input(token, size) +function audio.input.fetch(token, size) size = size or MAX_FETCH if audio_input_tokens_store[token] then if #total_fetch >= size then diff --git a/src/logic/scripting/lua/libs/libaudio.cpp b/src/logic/scripting/lua/libs/libaudio.cpp index a848f7d0..7ec6f1a6 100644 --- a/src/logic/scripting/lua/libs/libaudio.cpp +++ b/src/logic/scripting/lua/libs/libaudio.cpp @@ -389,7 +389,7 @@ static int l_audio_count_streams(lua::State* L) { return lua::pushinteger(L, audio::count_streams()); } -/// @brief audio.fetch_input(size) -> Bytearray +/// @brief audio.input.fetch(size) -> Bytearray static int l_audio_fetch_input(lua::State* L) { auto device = audio::get_input_device(); if (device == nullptr) { From 8de7f79ed52db8e2b4fbe2f980a959148c98ee41 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 7 Nov 2025 23:05:50 +0300 Subject: [PATCH 28/28] update doc/*/audio.md --- doc/en/audio.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ doc/ru/audio.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) diff --git a/doc/en/audio.md b/doc/en/audio.md index cacd7107..09510402 100644 --- a/doc/en/audio.md +++ b/doc/en/audio.md @@ -203,3 +203,79 @@ audio.count_speakers() -> integer -- get current number of playing streams audio.count_streams() -> integer ``` + +### audio.PCMStream + +```lua +-- Creating a PCM data source +local stream = audio.PCMStream( + -- Sample rate + sample_rate: integer, + -- Number of channels (1 - mono, 2 - stereo) + channels: integer, + -- Number of bits per sample (8 or 16) + bits_per_sample: integer, +) + +-- Feeding PCM data into the stream +stream:feed( + -- PCM data to be fed into the stream + data: Bytearray +) + +-- Publishing the PCM data source for use by the engine systems +stream:share( + -- Alias of the audio stream, which can be referenced in audio.play_stream + alias: string +) + +-- Creating a sound from the PCM data in the stream +stream:create_sound( + -- Name of the created sound + name: string +) +``` + +### Audio Recording + +```lua +-- Requests access to audio recording +-- On confirmation, the callback receives a token for use in audio.input.fetch +audio.input.request_open(callback: function(string)) + +-- Reads new PCM audio input data +audio.input.fetch( + -- Token obtained through audio.input.request_open + access_token: string, + -- Maximum buffer size in bytes (optional) + [optional] max_read_size: integer +) +``` + +### Example of Audio Generation: + +```lua +-- For working with 16-bit samples, use a U16view over Bytearray +-- Example: +local max_amplitude = 32767 +local sample_rate = 44100 +local total_samples = sample_rate * 5 -- 5 seconds of mono +local bytes = Bytearray(total_samples * 2) -- 5 seconds of 16-bit mono +local samples = I16view(bytes) + +local frequency_hz = 400 +for i=1, total_samples do + local value = math.sin(i * math.pi * 2 / sample_rate * frequency_hz) + samples[i] = value * max_amplitude +end + +local stream_name = "test-stream" +local stream = audio.PCMStream(sample_rate, 1, 16) +stream:feed(bytes) +stream:share(stream_name) + +local volume = 1.0 +local pitch = 1.0 +local channel = "ui" +audio.play_stream_2d(stream_name, volume, pitch, channel) +``` diff --git a/doc/ru/audio.md b/doc/ru/audio.md index 1fce6c35..8291e7ba 100644 --- a/doc/ru/audio.md +++ b/doc/ru/audio.md @@ -29,6 +29,7 @@ Доступ к спикерам производится по целочисленным id, которые не повторяются за время работы движка, следует избегать хранения прямых указателей на объекты класса. Нумерация ID спикеров начинается с 1. ID 0 означает невозможность воспроизведения, по какой-либо причине. + ### Звук (Sound) Звуковые данные загруженные в память для возможности одновременного воспроизведения из нескольких источников. Может предоставлять доступ к PCM данным. @@ -203,3 +204,79 @@ audio.count_speakers() -> integer -- получить текущее число проигрываемых аудио-потоков audio.count_streams() -> integer ``` + +### audio.PCMStream + +```lua +-- создание источника PCM данных +local stream = audio.PCMStream( + -- частота дискретизации + sample_rate: integer, + -- число каналов (1 - моно, 2 - стерео) + channels: integer, + -- число бит на сэмпл (8 или 16) + bits_per_sample: integer, +) + +-- подача PCM данных в поток +stream:feed( + -- PCM данные для подачи в поток + data: Bytearray +) + +-- публикация источника PCM данных для использования системами движка +stream:share( + -- имя потокового аудио, которое можно будет указать в audio.play_stream + alias: string +) + +-- создание звука из имеющихся в потоке PCM данных +stream:create_sound( + -- имя создаваемого звука + name: string +) +``` + +### Запись звука + +```lua +-- запрашивает доступ к записи звука +-- при подтверждении, в callback передаётся токен для использовании в audio.input.fetch +audio.input.request_open(callback: function(string)) + +-- читает новые PCM данные аудио ввода +audio.input.fetch( + -- токен, полученный через audio.input.request_open + access_token: string, + -- максимальное размер буфера в байтах + [опционально] max_read_size: integer +) +``` + +### Пример генерации аудио: + +```lua +-- для работы с 16-битными семплами используйте U16view поверх Bytearray +-- пример: +local max_amplitude = 32767 +local sample_rate = 44100 +local total_samples = sample_rate * 5 -- 5 секунд моно +local bytes = Bytearray(total_samples * 2) -- 5 секунд 16 бит моно +local samples = I16view(bytes) + +local frequency_hz = 400 +for i=1, total_samples do + local value = math.sin(i * math.pi * 2 / sample_rate * frequency_hz) + samples[i] = value * max_amplitude +end + +local stream_name = "test-stream" +local stream = audio.PCMStream(sample_rate, 1, 16) +stream:feed(bytes) +stream:share(stream_name) + +local volume = 1.0 +local pitch = 1.0 +local channel = "ui" +audio.play_stream_2d(stream_name, volume, pitch, channel) +```