diff --git a/CMakeLists.txt b/CMakeLists.txt index 86a7ba81..cf5af864 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") OUTPUT_STRIP_TRAILING_WHITESPACE ) endif() +include(FetchContent) set(LOVE_VERSION "12.0") set(APP_VERSION "3.1.0") @@ -161,6 +162,48 @@ add_library(lua53 ) target_link_libraries(lua53 PRIVATE PkgConfig::lua51) +FetchContent_Declare(lua-https + GIT_REPOSITORY https://github.com/bartbes/lua-https + GIT_TAG selectable_library_loaders +) + +FetchContent_GetProperties(lua-https) +if(NOT lua-https_POPULATED) + FetchContent_Populate(lua-https) +endif() + +add_library(lua-https STATIC + ${lua-https_SOURCE_DIR}/src/common/config.h + ${lua-https_SOURCE_DIR}/src/common/LibraryLoader.h + ${lua-https_SOURCE_DIR}/src/common/Connection.h + ${lua-https_SOURCE_DIR}/src/common/ConnectionClient.h + ${lua-https_SOURCE_DIR}/src/common/HTTPRequest.cpp + ${lua-https_SOURCE_DIR}/src/common/HTTPRequest.h + ${lua-https_SOURCE_DIR}/src/common/HTTPS.cpp + ${lua-https_SOURCE_DIR}/src/common/HTTPS.h + ${lua-https_SOURCE_DIR}/src/common/HTTPSClient.cpp + ${lua-https_SOURCE_DIR}/src/common/HTTPSClient.h + ${lua-https_SOURCE_DIR}/src/common/PlaintextConnection.cpp + ${lua-https_SOURCE_DIR}/src/common/PlaintextConnection.h + ${lua-https_SOURCE_DIR}/src/generic/LinktimeLibraryLoader.cpp + ${lua-https_SOURCE_DIR}/src/generic/CurlClient.cpp + ${lua-https_SOURCE_DIR}/src/generic/CurlClient.h + ${lua-https_SOURCE_DIR}/src/generic/OpenSSLConnection.cpp + ${lua-https_SOURCE_DIR}/src/generic/OpenSSLConnection.h + ${lua-https_SOURCE_DIR}/src/lua/main.cpp +) + +set(LIBRARY_LOADER "linktime") +target_compile_definitions(lua-https PRIVATE + HTTPS_LIBRARY_LOADER_LINKTIME + HTTPS_BACKEND_CURL +) + +# link curl +pkg_check_modules(libcurl REQUIRED IMPORTED_TARGET libcurl) +target_link_libraries(lua-https PRIVATE PkgConfig::lua51 PkgConfig::libcurl) + + add_library(love_physfs libraries/physfs/physfs.c libraries/physfs/physfs.h @@ -215,7 +258,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE wuff) # link everything else target_link_libraries(${PROJECT_NAME} PRIVATE - ${APP_LIBS} z luabit lua53 vorbisidec ogg modplug + ${APP_LIBS} z luabit lua53 vorbisidec ogg modplug lua-https ) include_directories( @@ -225,6 +268,7 @@ include_directories( libraries/noise1234 libraries/physfs libraries/wuff + libraries/utf8 ) # find source -type f -name \*.cpp | clip @@ -239,6 +283,11 @@ source/common/Stream.cpp source/common/types.cpp source/common/Variant.cpp source/main.cpp +source/modules/audio/Audio.cpp +source/modules/audio/Pool.cpp +source/modules/audio/Source.cpp +source/modules/audio/wrap_Audio.cpp +source/modules/audio/wrap_Source.cpp source/modules/data/ByteData.cpp source/modules/data/CompressedData.cpp source/modules/data/DataModule.cpp @@ -262,6 +311,7 @@ source/modules/filesystem/wrap_Filesystem.cpp source/modules/joystick/JoystickModule.cpp source/modules/joystick/wrap_Joystick.cpp source/modules/joystick/wrap_JoystickModule.cpp +source/modules/keyboard/wrap_Keyboard.cpp source/modules/love/love.cpp source/modules/sensor/Sensor.cpp source/modules/sensor/wrap_Sensor.cpp diff --git a/debug/Cargo.lock b/debug/Cargo.lock deleted file mode 100644 index ec71b599..00000000 --- a/debug/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "debug" -version = "0.1.0" diff --git a/debug/Cargo.toml b/debug/Cargo.toml deleted file mode 100644 index 5af177f6..00000000 --- a/debug/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "debug" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/debug/debug.py b/debug/debug.py new file mode 100644 index 00000000..9524dbf7 --- /dev/null +++ b/debug/debug.py @@ -0,0 +1,75 @@ +import sys +import argparse +import time +import os + +from io import TextIOWrapper +from socket import gaierror, socket, AF_INET, SOCK_STREAM +from pathlib import Path + + +def clear_console() -> None: + match os.name: + case "nt": + os.system("cls") + case _: + os.system("clear") + + +def main() -> None: + parser = argparse.ArgumentParser() + + parser.add_argument("host", help="the host to connect to") + parser.add_argument( + "-l", "--log", action="store_true", help="write data to log file" + ) + + args = parser.parse_args() + + clear_console() + + tcp_socket = socket(AF_INET, SOCK_STREAM, 0) + + try: + tcp_socket.connect((args.host, 8000)) + except (ConnectionRefusedError, TimeoutError): + print(f"Failed to connect to {args.host}:8000") + sys.exit(1) + except gaierror: + print(f"Invalid host: {args.host}") + sys.exit(1) + + log_file: TextIOWrapper = None + + if args.log: + Path("logs").mkdir(exist_ok=True) + timestamp = str(int(time.time())) + log_file = open(f"logs/{timestamp}.txt", "w") + + while True: + try: + raw_data = tcp_socket.recv(0x400) + + if not len(raw_data): + break + + data = raw_data.decode("utf-8") + + if args.log: + log_file.write(data) + + print(data, end="") + except ConnectionResetError: + print("Connection reset by peer") + break + except KeyboardInterrupt: + break + + tcp_socket.close() + + if args.log: + log_file.close() + + +if __name__ == "__main__": + main() diff --git a/debug/meta.py b/debug/meta.py new file mode 100644 index 00000000..dd9dc76a --- /dev/null +++ b/debug/meta.py @@ -0,0 +1,56 @@ +import argparse +import re + +from pathlib import Path + + +def read_file(file_path) -> Path | None: + try: + return Path(file_path).read_text(encoding="utf-8") + except FileNotFoundError: + print(f"File '{file_path}' not found.") + + return None + + +MATCH_STRING = r"(love\.\w+\.\w+)\(\)\s+==>\s+\w+\s+-\s+(\d+\.\d+)" +TIME_REGEX = re.compile(MATCH_STRING, re.MULTILINE) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Read a file.") + parser.add_argument("file", type=str, help="Path to the file") + parser.add_argument( + "--top", "-t", action="store_true", help="Get top 3 slowest methods" + ) + + args = parser.parse_args() + + content = read_file(args.file) + + if not content: + print("No content found in the file.") + return + + times = re.findall(TIME_REGEX, content) + + if not times: + print("No times found in the file.") + return + + sorted_times = sorted(times, key=lambda x: float(x[1][:-1]), reverse=True) + + if args.top: + for time in sorted_times[:3]: + method, total_time = time + print(f"- {method}: {total_time}s") + + return + + for time in sorted_times: + method, total_time = time + print(f"- {method}: {total_time}s") + + +if __name__ == "__main__": + main() diff --git a/debug/src/main.rs b/debug/src/main.rs deleted file mode 100644 index 2d12b904..00000000 --- a/debug/src/main.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::time::{SystemTime, UNIX_EPOCH}; -use std::io::{Read, Write}; -use std::net::TcpStream; -use std::fs::File; -use std::fs; - - -fn main() { - let args: Vec = std::env::args().collect(); - - let ip = match args.get(1) { - Some(ip) => ip, - None => { - eprintln!("IP address of the device is required"); - return; - } - }; - - let verbose = match args.get(2) { - Some(verbose) => verbose.parse().unwrap_or(false), - None => false, - }; - - fs::create_dir_all("logs").expect("Failed to create logs directory"); - - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Failed to get current timestamp") - .as_secs(); - - let filename = format!("logs/{}.log", timestamp); - - let mut stream = TcpStream::connect(format!("{}:8000", ip)) - .expect("Failed to connect to the IP address"); - - let mut buffer = [0; 1024]; - let mut file = File::create(&filename).expect("Failed to create log file"); - - loop { - let bytes_read = stream.read(&mut buffer).expect("Failed to read data from stream"); - - if bytes_read == 0 { - break; - } - - let data_str = &buffer[..bytes_read]; - let data = String::from_utf8_lossy(data_str).to_string(); - - if verbose { - print!("{}", data); - } - - file.write_all(data.as_bytes()).expect("Failed to write data to file"); - } -} diff --git a/include/common/Map.hpp b/include/common/Map.hpp index d9894dc8..33ad3d38 100644 --- a/include/common/Map.hpp +++ b/include/common/Map.hpp @@ -174,8 +174,12 @@ using StringMap = MapT; static inline bool getConstant(type in, std::string_view& out) \ { \ return name.getKey(in, out); \ - } -// clang-format on + } \ + static inline const char* getConstant(type in) \ + { \ + std::string_view out {}; \ + return name.getKey(in, out) ? out.data() : nullptr; \ + } // clang-format on template concept MapTIsEnumType = std::is_enum_v; diff --git a/include/common/Range.hpp b/include/common/Range.hpp new file mode 100644 index 00000000..d666b213 --- /dev/null +++ b/include/common/Range.hpp @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2006-2024 LOVE Development Team + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + **/ + +#pragma once + +#include +#include +#include + +namespace love +{ + + struct Range + { + size_t first; + size_t last; + + Range() : first(std::numeric_limits::max()), last(0) + {} + + Range(size_t offset, size_t size) : first(offset), last(offset + size - 1) + {} + + bool isValid() const + { + return first <= last; + } + + void invalidate() + { + first = std::numeric_limits::max(); + last = 0; + } + + size_t getMin() const + { + return first; + } + size_t getMax() const + { + return last; + } + + size_t getOffset() const + { + return first; + } + size_t getSize() const + { + return (last - first) + 1; + } + + bool contains(const Range& other) const + { + return first <= other.first && last >= other.last; + } + + bool intersects(const Range& other) const + { + return !(first > other.last || last < other.first); + } + + void intersect(const Range& other) + { + first = std::max(first, other.first); + last = std::min(last, other.last); + } + + void encapsulate(size_t index) + { + first = std::min(first, index); + last = std::max(last, index); + } + + void encapsulate(size_t offset, size_t size) + { + first = std::min(first, offset); + last = std::max(last, offset + size - 1); + } + + void encapsulate(const Range& other) + { + first = std::min(first, other.first); + last = std::max(last, other.last); + } + }; + +} // namespace love diff --git a/include/common/vector.hpp b/include/common/Vector.hpp similarity index 100% rename from include/common/vector.hpp rename to include/common/Vector.hpp diff --git a/include/common/error.hpp b/include/common/error.hpp index 2f5f8554..c6f85649 100644 --- a/include/common/error.hpp +++ b/include/common/error.hpp @@ -3,9 +3,9 @@ namespace love // Startup #define E_UNEXPECTED_ALIGNMENT \ "Cannot push love object to Lua: unexpected alignment\ - (pointer is %p but alignment should be %d)." + (pointer is {} but alignment should be {:d})." -#define E_POINTER_TOO_LARGE "Cannot push love object to Lua: pointer value %p is too large!" +#define E_POINTER_TOO_LARGE "Cannot push love object to Lua: pointer value {} is too large!" // General #define E_OUT_OF_MEMORY "Out of memory." diff --git a/include/common/luax.hpp b/include/common/luax.hpp index be0f5ab4..102e2217 100644 --- a/include/common/luax.hpp +++ b/include/common/luax.hpp @@ -108,6 +108,8 @@ namespace love Type* luax_type(lua_State* L, int index); + int luax_enumerror(lua_State* L, const char* enumName, const char* value); + template T* luax_totype(lua_State* L, int index, const Type& type) { @@ -135,6 +137,29 @@ namespace love int luax_typeerror(lua_State* L, int argc, const char* name); + template + void luax_checktablefields(lua_State* L, int idx, const char* enumName, + bool (*getConstant)(const char*, T&)) + { + luaL_checktype(L, idx, LUA_TTABLE); + + // We want to error for invalid / misspelled fields in the table. + lua_pushnil(L); + while (lua_next(L, idx)) + { + if (lua_type(L, -2) != LUA_TSTRING) + luax_typeerror(L, -2, "string"); + + const char* key = luaL_checkstring(L, -2); + T value; + + if (!getConstant(key, value)) + luax_enumerror(L, enumName, key); + + lua_pop(L, 1); + } + } + template T* luax_checktype(lua_State* L, int index, const Type& type) { diff --git a/include/common/math.hpp b/include/common/math.hpp index 68a5bbdf..2d123b88 100644 --- a/include/common/math.hpp +++ b/include/common/math.hpp @@ -79,7 +79,7 @@ namespace love if (x <= 2) return x; - unsigned result = 1u << (32 - __builtin_clz(x - 1)); + size_t result = 1U << (32 - __builtin_clz(x - 1)); return std::clamp(result, LOVE_TEX3DS_MIN, LOVE_TEX3DS_MAX); } } // namespace love diff --git a/include/driver/DigitalSound.tcc b/include/driver/DigitalSound.tcc index ed5f2dc6..46e235d1 100644 --- a/include/driver/DigitalSound.tcc +++ b/include/driver/DigitalSound.tcc @@ -11,8 +11,8 @@ namespace love public: enum InterpretedFormat { - FORMAT_MONO, - FORMAT_STEREO, + FORMAT_MONO = 0x01, + FORMAT_STEREO = 0x02, FORMAT_MAX_ENUM }; diff --git a/include/driver/EventQueue.tcc b/include/driver/EventQueue.tcc index 52451993..062c19cc 100644 --- a/include/driver/EventQueue.tcc +++ b/include/driver/EventQueue.tcc @@ -3,7 +3,11 @@ #include "common/Singleton.tcc" #include "events.hpp" +#include "modules/joystick/Joystick.hpp" + #include +#include +#include namespace love { @@ -77,15 +81,28 @@ namespace love event.gamepadStatus.which = which; } + void sendTextInput(std::unique_ptr& text) + { + auto& event = this->events.emplace_back(); + + event.type = TYPE_KEYBOARD; + event.subtype = SUBTYPE_TEXTINPUT; + event.keyboardInput.text = text.get(); + } + + void sendTextInput(std::string_view text) + { + auto& event = this->events.emplace_back(); + + event.type = TYPE_KEYBOARD; + event.subtype = SUBTYPE_TEXTINPUT; + event.keyboardInput.text = text; + } + private: bool hysteresis; protected: - bool touchHeld; - bool focused; - - std::list events; - virtual void pollInternal() = 0; void sendTouchEvent(SubEventType type, size_t id, float x, float y, float dx, float dy, @@ -97,5 +114,52 @@ namespace love event.subtype = type; event.finger = { id, x, y, dx, dy, pressure }; } + + void sendGamepadButtonEvent(SubEventType type, int which, int input) + { + auto& event = this->events.emplace_back(); + + event.type = TYPE_GAMEPAD; + event.subtype = type; + event.gamepadButton = { which, input }; + + // also send a joystick event + auto& jevent = this->events.emplace_back(); + + jevent.type = TYPE_GAMEPAD; + auto jtype = type == SUBTYPE_GAMEPADDOWN ? SUBTYPE_JOYSTICKDOWN : SUBTYPE_JOYSTICKUP; + + jevent.subtype = jtype; + jevent.gamepadButton = { which, input }; + } + + void sendGamepadAxisEvent(int which, int axis, float value) + { + auto& event = this->events.emplace_back(); + + event.type = TYPE_GAMEPAD; + event.subtype = SUBTYPE_GAMEPADAXIS; + event.gamepadAxis = { which, axis, value }; + + // also send a joystick event + auto& jevent = this->events.emplace_back(); + jevent.type = TYPE_GAMEPAD; + jevent.subtype = SUBTYPE_JOYSTICKAXIS; + jevent.gamepadAxis = { which, axis, value }; + } + + void sendGamepadSensorEvent(int which, int sensor, const std::vector& data) + { + auto& event = this->events.emplace_back(); + + event.type = TYPE_GAMEPAD; + event.subtype = SUBTYPE_GAMEPADSENSORUPDATED; + event.gamepadSensor = { which, sensor, data }; + } + + bool touchHeld; + bool focused; + + std::list events; }; } // namespace love diff --git a/include/events.hpp b/include/events.hpp index 29f9df3b..70851fce 100644 --- a/include/events.hpp +++ b/include/events.hpp @@ -5,25 +5,30 @@ #include #include +#include + namespace love { struct GamepadButton { - size_t id; - std::string name; - + int which; int button; }; struct GamepadAxis { - size_t id; - std::string name; - - size_t axis; + int which; + int axis; float value; }; + struct GamepadSensor + { + int which; + int sensor; + std::vector data; + }; + struct GamepadStatus { bool added; @@ -66,8 +71,11 @@ namespace love enum SubEventType { SUBTYPE_GAMEPADAXIS, + SUBTYPE_JOYSTICKAXIS, SUBTYPE_GAMEPADDOWN, + SUBTYPE_JOYSTICKDOWN, SUBTYPE_GAMEPADUP, + SUBTYPE_JOYSTICKUP, SUBTYPE_GAMEPADADDED, SUBTYPE_GAMEPADREMOVED, @@ -98,6 +106,7 @@ namespace love GamepadButton gamepadButton; GamepadAxis gamepadAxis; + GamepadSensor gamepadSensor; GamepadStatus gamepadStatus; diff --git a/include/modules/audio/Audio.hpp b/include/modules/audio/Audio.hpp new file mode 100644 index 00000000..fabce477 --- /dev/null +++ b/include/modules/audio/Audio.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include "common/Module.hpp" + +#include "modules/audio/Source.hpp" +#include "modules/thread/Threadable.hpp" + +namespace love +{ + class Audio : public Module + { + public: + Audio(); + + ~Audio(); + + Source* newSource(SoundData* soundData) const; + + Source* newSource(Decoder* decoder) const; + + Source* newSource(int sampleRate, int bitDepth, int channels, int buffers) const; + + int getActiveSourceCount() const; + + int getMaxSources() const; + + bool play(Source* source); + + void stop(Source* source); + + void pause(Source* source); + + void setVolume(float volume); + + float getVolume() const; + + private: + Pool* pool; + + class PoolThread : public Threadable + { + public: + PoolThread(Pool* pool); + + void setFinish() + { + this->finish = true; + } + + void run(); + + protected: + Pool* pool; + std::atomic finish; + std::recursive_mutex mutex; + }; + + PoolThread* poolThread; + }; +} // namespace love diff --git a/include/modules/audio/Pool.hpp b/include/modules/audio/Pool.hpp new file mode 100644 index 00000000..e3c08bf2 --- /dev/null +++ b/include/modules/audio/Pool.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "modules/audio/Source.hpp" + +#include +#include +#include + +namespace love +{ + class Pool + { + public: + Pool(); + + ~Pool(); + + bool isAvailable(); + + bool isPlaying(Source* source); + + void update(); + + int getActiveSourceCount() const; + + int getMaxSources() const; + + std::recursive_mutex& getMutex() + { + return this->mutex; + } + + std::unique_lock lock(); + + private: + friend class Source; + + static constexpr size_t MAX_SOURCES = 24; + int totalSources; + + std::queue available; + std::map playing; + std::recursive_mutex mutex; + + std::vector getPlayingSources(); + + bool releaseSource(Source* source, bool stop = true); + + void addSource(Source* source, size_t channel); + + bool assignSource(Source* source, size_t& channel, uint8_t& wasPlaying); + + bool findSource(Source* source, size_t& channel); + }; +} // namespace love diff --git a/include/modules/audio/Source.hpp b/include/modules/audio/Source.hpp new file mode 100644 index 00000000..b8228a85 --- /dev/null +++ b/include/modules/audio/Source.hpp @@ -0,0 +1,212 @@ +#pragma once + +#include "common/Object.hpp" +#include "driver/DigitalSound.hpp" + +#include "modules/sound/Decoder.hpp" +#include "modules/sound/SoundData.hpp" + +#include +#include +#include + +namespace love +{ + class Audio; + class Pool; + + class StaticDataBuffer : public Object + { + public: + StaticDataBuffer(const void* data, size_t size, size_t nsamples); + + virtual ~StaticDataBuffer(); + + size_t getSampleCount() const + { + return this->nsamples; + } + + void* getBuffer() const + { + return this->buffer; + } + + size_t getSize() const + { + return this->size; + } + + private: + int16_t* buffer; + size_t size; + size_t nsamples; + + size_t alignSize; + }; + + class Source : public Object + { + public: + static love::Type type; + + enum Type + { + TYPE_STATIC, + TYPE_STREAM, + TYPE_QUEUE, + TYPE_MAX_ENUM + }; + + enum Unit + { + UNIT_SECONDS, + UNIT_SAMPLES, + UNIT_MAX_ENUM + }; + + Source(Type type) : sourceType(type) + {} + + Source(Pool* pool, SoundData* soundData); + + Source(Pool* pool, Decoder* decoder); + + Source(Pool* pool, int sampleRate, int bitDepth, int channels, int buffers); + + Source(const Source& other); + + virtual ~Source(); + + virtual Source* clone(); + + bool play(); + + void stop(); + + void pause(); + + bool isPlaying() const; + + bool isFinished() const; + + bool update(); + + void setPitch(float pitch); + + float getPitch() const; + + void setVolume(float volume); + + float getVolume() const; + + void seek(double offset, Unit unit); + + double tell(Unit unit); + + double getDuration(Unit unit); + + void setLooping(bool looping); + + bool isLooping() const; + + int getChannelCount() const + { + return this->channels; + } + + Type getType() const + { + return this->sourceType; + } + + void setMinVolume(float minVolume); + + float getMinVolume() const + { + return this->minVolume; + } + + void setMaxVolume(float maxVolume); + + float getMaxVolume() const + { + return this->maxVolume; + } + + int getFreeBufferCount() const; + + void prepareAtomic(); + + void teardownAtomic(); + + bool playAtomic(AudioBuffer* buffer); + + void stopAtomic(); + + void pauseAtomic(); + + void resumeAtomic(); + + static bool play(const std::vector& sources); + + static void stop(const std::vector& sources); + + static void pause(const std::vector& sources); + + static std::vector pause(Pool* pool); + + static void stop(Pool* pool); + + // clang-format off + STRINGMAP_DECLARE(SourceTypes, Type, + { "static", TYPE_STATIC }, + { "stream", TYPE_STREAM }, + { "queue", TYPE_QUEUE } + ); + + STRINGMAP_DECLARE(SourceUnits, Unit, + { "seconds", UNIT_SECONDS }, + { "samples", UNIT_SAMPLES } + ); + // clang-format on + + private: + void reset(); + + int streamAtomic(AudioBuffer* buffer, Decoder* decoder); + + Type sourceType; + Pool* pool = nullptr; + + AudioBuffer* source = nullptr; + bool valid = false; + + static constexpr int DEFAULT_BUFFERS = 8; + static constexpr int MAX_BUFFERS = 64; + + std::queue streamBuffers; + std::stack unusedBuffers; + + StrongRef staticBuffer; + + float pitch = 1.0f; + float volume = 1.0f; + bool looping = false; + + float minVolume = 0.0f; + float maxVolume = 1.0f; + + int offsetSamples = 0; + + int sampleRate = 0; + int channels = 0; + int bitDepth = 0; + + int buffers = 0; + + StrongRef decoder; + + size_t channel = 0; + }; +} // namespace love diff --git a/include/modules/audio/wrap_Audio.hpp b/include/modules/audio/wrap_Audio.hpp new file mode 100644 index 00000000..3e45618c --- /dev/null +++ b/include/modules/audio/wrap_Audio.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "common/luax.hpp" +#include "modules/audio/Audio.hpp" + +namespace Wrap_Audio +{ + int newSource(lua_State* L); + + int play(lua_State* L); + + int pause(lua_State* L); + + int stop(lua_State* L); + + int open(lua_State* L); +} // namespace Wrap_Audio diff --git a/include/modules/audio/wrap_Source.hpp b/include/modules/audio/wrap_Source.hpp new file mode 100644 index 00000000..b812115c --- /dev/null +++ b/include/modules/audio/wrap_Source.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "common/luax.hpp" +#include "modules/audio/Source.hpp" + +namespace love +{ + Source* luax_checksource(lua_State* L, int index); + + int open_source(lua_State* L); +} // namespace love + +namespace Wrap_Source +{ + int clone(lua_State* L); + + int play(lua_State* L); + + int pause(lua_State* L); + + int stop(lua_State* L); + + int setLooping(lua_State* L); +} // namespace Wrap_Source diff --git a/include/modules/joystick/Joystick.tcc b/include/modules/joystick/Joystick.tcc index e4964a5a..ae02ecaf 100644 --- a/include/modules/joystick/Joystick.tcc +++ b/include/modules/joystick/Joystick.tcc @@ -158,6 +158,10 @@ namespace love virtual bool isDown(std::span buttons) const = 0; + virtual bool isUp(std::span buttons) const = 0; + + virtual bool isAxisChanged(GamepadAxis axis) const = 0; + virtual void setPlayerIndex(int index) = 0; virtual int getPlayerIndex() const = 0; @@ -256,11 +260,21 @@ namespace love return std::clamp(value / MAX_AXIS_VALUE, -1.0f, 1.0f); } - JoystickBase(int id) : joystickType(JOYSTICK_TYPE_UNKNOWN), instanceId(-1), id(id) - {} + JoystickBase(int id) : + joystickType(JOYSTICK_TYPE_UNKNOWN), + instanceId(-1), + id(id), + sensors() + { + this->sensors[Sensor::SENSOR_ACCELEROMETER] = false; + this->sensors[Sensor::SENSOR_GYROSCOPE] = false; + } - JoystickBase(int id, int) : instanceId(-1), id(id) - {} + JoystickBase(int id, int) : instanceId(-1), id(id), sensors() + { + this->sensors[Sensor::SENSOR_ACCELEROMETER] = false; + this->sensors[Sensor::SENSOR_GYROSCOPE] = false; + } JoystickType joystickType; GamepadType gamepadType; diff --git a/include/modules/keyboard/Keyboard.tcc b/include/modules/keyboard/Keyboard.tcc new file mode 100644 index 00000000..5494b3db --- /dev/null +++ b/include/modules/keyboard/Keyboard.tcc @@ -0,0 +1,110 @@ +#pragma once + +#include "common/Map.hpp" +#include "common/Module.hpp" +#include "common/Result.hpp" + +#include + +namespace love +{ + class KeyboardBase : public Module + { + public: + enum KeyboardType + { + TYPE_NORMAL, + TYPE_QWERTY, + TYPE_NUMPAD + }; + + struct KeyboardOptions + { + uint8_t type; + bool password; + std::string_view hint; + uint32_t maxLength; + }; + + enum KeyboardOption + { + OPTION_TYPE, + OPTION_PASSCODE, + OPTION_HINT, + OPTION_MAX_LENGTH, + OPTION_MAX_ENUM + }; + + static constexpr uint32_t DEFAULT_INPUT_LENGTH = 0x14; + static constexpr uint32_t MINIMUM_INPUT_LENGTH = 0x01; + +#if defined(__SWITCH__) + static constexpr uint32_t MAX_INPUT_LENGTH = 0x1F4; + static constexpr uint32_t MULTIPLIER = 4; +#else + static constexpr uint32_t MAX_INPUT_LENGTH = 0x100; + static constexpr uint32_t MULTIPLIER = 3; +#endif + + KeyboardBase() : + Module(M_KEYBOARD, "love.keyboard"), + keyRepeat(false), + showing(false), + text(nullptr) + {} + + virtual ~KeyboardBase() + {} + + void setKeyRepeat(bool enable) + { + this->keyRepeat = enable; + } + + bool hasKeyRepeat() const + { + return this->keyRepeat; + } + + bool hasScreenKeyboard() const + { + return true; + } + + bool hasTextInput() const + { + return this->showing; + } + + std::string_view getText() const + { + return this->text.get(); + } + + // clang-format off + STRINGMAP_DECLARE(Options, KeyboardOption, + { "type", OPTION_TYPE }, + { "password", OPTION_PASSCODE }, + { "hint", OPTION_HINT }, + { "maxLength", OPTION_MAX_LENGTH } + ); + + STRINGMAP_DECLARE(KeyboardTypes, KeyboardType, + { "normal", TYPE_NORMAL }, + { "qwerty", TYPE_QWERTY }, + { "numpad", TYPE_NUMPAD } + ); + // clang-format on + + protected: + virtual uint32_t getMaxEncodingLength(const uint32_t length) const + { + return std::clamp(length, MINIMUM_INPUT_LENGTH, MAX_INPUT_LENGTH) * MULTIPLIER; + } + + bool keyRepeat; + bool showing; + + std::unique_ptr text; + }; +} // namespace love diff --git a/include/modules/keyboard/wrap_Keyboard.hpp b/include/modules/keyboard/wrap_Keyboard.hpp new file mode 100644 index 00000000..9968973a --- /dev/null +++ b/include/modules/keyboard/wrap_Keyboard.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "common/luax.hpp" +#include "modules/keyboard/Keyboard.hpp" + +namespace Wrap_Keyboard +{ + int setKeyRepeat(lua_State* L); + + int hasKeyRepeat(lua_State* L); + + int setTextInput(lua_State* L); + + int hasTextInput(lua_State* L); + + int hasScreenKeyboard(lua_State* L); + + int isDown(lua_State* L); + + int isScancodeDown(lua_State* L); + + int getScancodeFromKey(lua_State* L); + + int getKeyFromScancode(lua_State* L); + + int isModifierActive(lua_State* L); + + int open(lua_State* L); +} // namespace Wrap_Keyboard diff --git a/include/modules/love/love.hpp b/include/modules/love/love.hpp index ec841ca3..170f185a 100644 --- a/include/modules/love/love.hpp +++ b/include/modules/love/love.hpp @@ -2,6 +2,11 @@ struct lua_State; +extern "C" +{ + extern int luaopen_https(lua_State*); +} + static constexpr int STDIO_PORT = 8000; const char* love_getVersion(); diff --git a/platform/cafe/CMakeLists.txt b/platform/cafe/CMakeLists.txt index ada47d95..67c1fb49 100644 --- a/platform/cafe/CMakeLists.txt +++ b/platform/cafe/CMakeLists.txt @@ -22,9 +22,12 @@ target_include_directories(${PROJECT_NAME} PRIVATE target_sources(${PROJECT_NAME} PRIVATE source/boot.cpp source/common/screen.cpp +source/driver/DigitalSound.cpp source/driver/EventQueue.cpp +source/modules/audio/Source.cpp source/modules/joystick/Joystick.cpp source/modules/joystick/JoystickModule.cpp +source/modules/keyboard/Keyboard.cpp source/modules/system/System.cpp source/modules/timer/Timer.cpp ) diff --git a/platform/cafe/include/driver/DigitalSound.hpp b/platform/cafe/include/driver/DigitalSound.hpp new file mode 100644 index 00000000..7fbd58b2 --- /dev/null +++ b/platform/cafe/include/driver/DigitalSound.hpp @@ -0,0 +1,263 @@ +#pragma once + +#include "driver/DigitalSound.tcc" +#include "driver/DigitalSoundMix.hpp" + +#include + +#include +#include +#include + +#include + +extern "C" +{ + void AXSetMasterVolume(uint32_t volume); + uint16_t AXGetMasterVolume(); +} + +namespace love +{ + using AudioBuffer = AXVoice; + + class DigitalSound : public DigitalSoundBase + { + + public: + ~DigitalSound(); + + virtual void initialize() override; + + void updateImpl(); + + void setMasterVolume(float volume); + + float getMasterVolume() const; + + AudioBuffer* createBuffer(int size = 0); + + bool isBufferDone(AudioBuffer* buffer) const; + + void prepareBuffer(AudioBuffer* buffer, size_t nsamples, void* data, size_t size, + bool looping); + + void setLooping(AudioBuffer* buffer, bool looping); + + bool channelReset(size_t id, AudioBuffer* buffer, int channels, int bitDepth, + int sampleRate); + + void channelSetVolume(size_t id, float volume); + + float channelGetVolume(size_t id) const; + + size_t channelGetSampleOffset(size_t id); + + bool channelAddBuffer(size_t id, AudioBuffer* buffer); + + void channelPause(size_t id, bool paused); + + bool isChannelPaused(size_t id) const; + + bool isChannelPlaying(size_t id) const; + + void channelStop(size_t id); + + static int32_t getFormat(int channels, int bitDepth); + + // clang-format off + ENUMMAP_DECLARE(AudioFormats, EncodingFormat, AX_VOICE_FORMAT, + { ENCODING_PCM8, AX_VOICE_FORMAT_LPCM8 }, + { ENCODING_PCM16, AX_VOICE_FORMAT_LPCM16 } + ); + // clang-format on + + OSEvent& getEvent() + { + return event; + } + + private: + OSEvent event; + + struct Channel + { + public: + enum ChannelState + { + STATE_PLAYING, + STATE_PAUSED, + STATE_STOPPED, + STATE_DONE + }; + + Channel() : buffers {}, state(STATE_DONE), channels(0), volume(1.0f) + {} + + void begin(AXVoice* voice, int channels) + { + for (int index = 0; index < channels; index++) + this->buffers[index] = voice; + } + + bool reset(int channels, int bitDepth, int sampleRate, float volume) + { + volume = std::clamp(volume, 0.0f, 1.0f); + + AXVoiceVeData ve {}; + ve.volume = volume * 0x8000; + + int32_t format = 0; + if (DigitalSound::getFormat(channels, bitDepth) < 0) + return false; + + for (int index = 0; index < channels; index++) + { + if (this->buffers[index] == nullptr) + continue; + + AXVoiceBegin(this->buffers[index]); + AXSetVoiceType(this->buffers[index], AX_VOICE_TYPE_UNKNOWN); + AXSetVoiceVe(this->buffers[index], &ve); + + // clang-format off + switch (channels) + { + case AX_VOICE_MONO: + { + AXSetVoiceDeviceMix(this->buffers[index], AX_DEVICE_TYPE_DRC, 0, MONO_MIX[index]); + AXSetVoiceDeviceMix(this->buffers[index], AX_DEVICE_TYPE_TV, 0, MONO_MIX[index]); + break; + } + case AX_VOICE_STEREO: + { + AXSetVoiceDeviceMix(this->buffers[index], AX_DEVICE_TYPE_DRC, 0, STEREO_MIX[index]); + AXSetVoiceDeviceMix(this->buffers[index], AX_DEVICE_TYPE_TV, 0, STEREO_MIX[index]); + break; + } + default: + break; + } + // clang-format on + + float ratio = (float)sampleRate / (float)AXGetInputSamplesPerSec(); + if (auto result = AXSetVoiceSrcRatio(this->buffers[index], ratio); result != 0) + return false; + + AXSetVoiceSrcType(this->buffers[index], AX_VOICE_SRC_TYPE_LINEAR); + this->buffers[index]->offsets.dataType = (AXVoiceFormat)format; + + AXVoiceEnd(this->buffers[index]); + } + + this->channels = channels; + return true; + } + + void setVolume(float volume) + { + volume = std::clamp(volume, 0.0f, 1.0f); + + AXVoiceVeData ve {}; + ve.volume = volume * 0x8000; + + for (int index = 0; index < channels; index++) + { + AXVoiceBegin(this->buffers[index]); + AXSetVoiceVe(this->buffers[index], &ve); + AXVoiceEnd(this->buffers[index]); + } + + this->volume = volume; + } + + float getVolume() const + { + return this->volume; + } + + void setLooping(bool looping) + { + auto voiceLooping = looping ? AX_VOICE_LOOP_ENABLED : AX_VOICE_LOOP_DISABLED; + + for (int index = 0; index < this->channels; index++) + { + AXVoiceBegin(this->buffers[index]); + AXSetVoiceLoop(this->buffers[index], voiceLooping); + AXVoiceEnd(this->buffers[index]); + } + } + + bool play() + { + for (int index = 0; index < this->channels; index++) + { + AXVoiceBegin(this->buffers[index]); + // AXSetVoiceOffsets(this->buffers[index], &this->offsets); + AXSetVoiceState(this->buffers[index], AX_VOICE_STATE_PLAYING); + AXVoiceEnd(this->buffers[index]); + } + + this->state = STATE_PLAYING; + return true; + } + + bool isPlaying() const + { + return this->state == STATE_PLAYING; + } + + void stop() + { + for (int index = 0; index < this->channels; index++) + { + AXVoiceBegin(this->buffers[index]); + AXSetVoiceState(this->buffers[index], AX_VOICE_STATE_STOPPED); + AXVoiceEnd(this->buffers[index]); + } + + this->state = STATE_STOPPED; + } + + void setPaused(bool paused) + { + auto voiceState = paused ? AX_VOICE_STATE_STOPPED : AX_VOICE_STATE_PLAYING; + + for (int index = 0; index < this->channels; index++) + { + AXVoiceBegin(this->buffers[index]); + AXSetVoiceState(this->buffers[index], voiceState); + AXVoiceEnd(this->buffers[index]); + } + + this->state = paused ? STATE_PAUSED : STATE_PLAYING; + } + + bool isPaused() const + { + return this->state == STATE_PAUSED; + } + + size_t getSampleOffset() const + { + AXVoiceOffsets offsets {}; + + AXVoiceBegin(this->buffers[0]); + AXGetVoiceOffsets(this->buffers[0], &offsets); + AXVoiceEnd(this->buffers[0]); + + return offsets.currentOffset; + } + + private: + std::array buffers; + ChannelState state; + + int channels; + float volume; + }; + + std::array channels; + bool shutdown = false; + }; +} // namespace love diff --git a/platform/cafe/include/driver/DigitalSoundMix.hpp b/platform/cafe/include/driver/DigitalSoundMix.hpp new file mode 100644 index 00000000..a6e24085 --- /dev/null +++ b/platform/cafe/include/driver/DigitalSoundMix.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#define AX_VOICE(x) x +#define AX_CHANNEL_LEFT 0 +#define AX_CHANNEL_RIGHT 1 +#define AX_BUS_MASTER 0 + +#define AX_NUM_CHANNELS 6 + +#define WIIU_MAX_VALID_CHANNELS 2 + +#define AX_VOICE_MONO 1 +#define AX_VOICE_STEREO 2 + +// clang-format off +static AXVoiceDeviceMixData STEREO_MIX[2][AX_NUM_CHANNELS] = +{ + { // AX_VOICE(0) + { // AX_CHANNEL_LEFT + .bus = { { .volume = 0x8000 } } + } + }, + { // AX_VOICE(1) + { // AX_CHANNEL_RIGHT + .bus = { { .volume = 0x8000 } } + } + } +}; + +static AXVoiceDeviceMixData MONO_MIX[1][AX_NUM_CHANNELS] = { + { // AX_VOICE(0) + { // AX_CHANNEL_LEFT + .bus = { { .volume = 0x8000 } } + }, + { // AX_CHANNEL_RIGHT + .bus = { { .volume = 0x8000 } } + } + } +}; +// clang-format on diff --git a/platform/cafe/include/modules/joystick/Joystick.hpp b/platform/cafe/include/modules/joystick/Joystick.hpp index 62f830f6..c909c5df 100644 --- a/platform/cafe/include/modules/joystick/Joystick.hpp +++ b/platform/cafe/include/modules/joystick/Joystick.hpp @@ -40,6 +40,10 @@ namespace love virtual bool isDown(std::span buttons) const override; + virtual bool isUp(std::span buttons) const override; + + virtual bool isAxisChanged(GamepadAxis axis) const override; + virtual void setPlayerIndex(int index) override; virtual int getPlayerIndex() const override; @@ -66,6 +70,24 @@ namespace love using JoystickBase::getConstant; + enum VPADAxis + { + VPADAXIS_LEFTX = VPAD_STICK_L_EMULATION_LEFT | VPAD_STICK_L_EMULATION_RIGHT, + VPADAXIS_LEFTY = VPAD_STICK_L_EMULATION_UP | VPAD_STICK_L_EMULATION_DOWN, + VPADAXIS_RIGHTX = VPAD_STICK_R_EMULATION_LEFT | VPAD_STICK_R_EMULATION_RIGHT, + VPADAXIS_RIGHTY = VPAD_STICK_R_EMULATION_UP | VPAD_STICK_R_EMULATION_DOWN, + VPADAXIS_TRIGGERLEFT = VPAD_BUTTON_ZL, + VPADAXIS_TRIGGERRIGHT = VPAD_BUTTON_ZR + }; + + enum WPADProAxis + { + WPADPROAXIS_LEFTX = WPAD_PRO_STICK_L_EMULATION_LEFT | WPAD_PRO_STICK_L_EMULATION_RIGHT, + WPADPROAXIS_LEFTY = WPAD_PRO_STICK_L_EMULATION_UP | WPAD_PRO_STICK_L_EMULATION_DOWN, + WPADPROAXIS_RIGHTX = WPAD_PRO_STICK_R_EMULATION_LEFT | WPAD_PRO_STICK_R_EMULATION_RIGHT, + WPADPROAXIS_RIGHTY = WPAD_PRO_STICK_R_EMULATION_UP | WPAD_PRO_STICK_R_EMULATION_DOWN + }; + // clang-format off ENUMMAP_DECLARE(WpadProButtons, GamepadButton, WPADProButton, { GAMEPAD_BUTTON_A, WPAD_PRO_BUTTON_A }, @@ -87,6 +109,15 @@ namespace love { GAMEPAD_BUTTON_RIGHTSTICK, WPAD_PRO_BUTTON_STICK_R } ); + ENUMMAP_DECLARE(VpadAxes, GamepadAxis, VPADAxis, + { GAMEPAD_AXIS_LEFTX, VPADAXIS_LEFTX }, + { GAMEPAD_AXIS_LEFTY, VPADAXIS_LEFTY }, + { GAMEPAD_AXIS_RIGHTX, VPADAXIS_RIGHTX }, + { GAMEPAD_AXIS_RIGHTY, VPADAXIS_RIGHTY }, + { GAMEPAD_AXIS_TRIGGERLEFT, VPADAXIS_TRIGGERLEFT }, + { GAMEPAD_AXIS_TRIGGERRIGHT, VPADAXIS_TRIGGERRIGHT } + ); + ENUMMAP_DECLARE(VpadButtons, GamepadButton, VPADButtons, { GAMEPAD_BUTTON_A, VPAD_BUTTON_A }, { GAMEPAD_BUTTON_B, VPAD_BUTTON_B }, diff --git a/platform/cafe/include/modules/keyboard/Keyboard.hpp b/platform/cafe/include/modules/keyboard/Keyboard.hpp new file mode 100644 index 00000000..c3e2b7a0 --- /dev/null +++ b/platform/cafe/include/modules/keyboard/Keyboard.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "modules/keyboard/Keyboard.tcc" + +#include +#include + +#include +#include + +namespace love +{ + class Keyboard : public KeyboardBase + { + public: + Keyboard(); + + ~Keyboard(); + + void initialize(); + + void hide() + { + this->showing = false; + nn::swkbd::DisappearInputForm(); + } + + void setTextInput(const KeyboardOptions& options); + + using KeyboardBase::getConstant; + + // clang-format off + ENUMMAP_DECLARE(CafeKeyboardType, KeyboardType, nn::swkbd::KeyboardMode, + { TYPE_NORMAL, nn::swkbd::KeyboardMode::Full }, + { TYPE_QWERTY, nn::swkbd::KeyboardMode::Utf8 }, + { TYPE_NUMPAD, nn::swkbd::KeyboardMode::Numpad } + ); + // clang-format on + + private: + nn::swkbd::CreateArg createArgs; + nn::swkbd::AppearArg appearArgs; + FSClient* client; + + bool inited; + }; +} // namespace love diff --git a/platform/cafe/source/driver/DigitalSound.cpp b/platform/cafe/source/driver/DigitalSound.cpp new file mode 100644 index 00000000..2127c2a0 --- /dev/null +++ b/platform/cafe/source/driver/DigitalSound.cpp @@ -0,0 +1,182 @@ +#include "common/Exception.hpp" +#include "common/int.hpp" + +#include "driver/DigitalSound.hpp" + +#include +#include + +namespace love +{ + void audioCallback() + {} + + // clang-format off + static AXInitParams AX_INIT_PARAMS = { + .renderer = AX_INIT_RENDERER_48KHZ, + .pipeline = AX_INIT_PIPELINE_SINGLE + }; + // clang-format on + + DigitalSound::~DigitalSound() + { + AXQuit(); + } + + void DigitalSound::initialize() + { + if (!AXIsInit()) + AXInitWithParams(&AX_INIT_PARAMS); + + if (!AXIsInit()) + throw love::Exception("Failed to initialize AX."); + + // OSInitEvent(&this->event, false, OS_EVENT_MODE_AUTO); + // AXRegisterAppFrameCallback(audioCallback); + } + + void DigitalSound::updateImpl() + { + OSSleepTicks(OSMillisecondsToTicks(3)); + } + + void DigitalSound::setMasterVolume(float volume) + { + AXSetMasterVolume(volume * 0x8000); + } + + float DigitalSound::getMasterVolume() const + { + uint16_t volume = AXGetMasterVolume(); + return volume / (float)0x8000; + } + + AudioBuffer* DigitalSound::createBuffer(int size) + { + AXVoice* voice = new AXVoice(); + + AXVoiceBegin(voice); + + AXSetVoiceState(voice, AX_VOICE_STATE_STOPPED); + + AXVoiceEnd(voice); + + return voice; + } + + bool DigitalSound::isBufferDone(AudioBuffer* buffer) const + { + if (buffer == nullptr) + return false; + + bool done = false; + + AXVoiceBegin(buffer); + done = buffer->state == AX_VOICE_STATE_STOPPED; + AXVoiceEnd(buffer); + + return done; + } + + void DigitalSound::prepareBuffer(AudioBuffer* buffer, size_t nsamples, void* data, size_t size, + bool looping) + { + if (buffer == nullptr || data == nullptr) + return; + + AXVoiceBegin(buffer); + + DCStoreRange(data, size); + + AXSetVoiceState(buffer, AX_VOICE_STATE_STOPPED); + AXSetVoiceLoop(buffer, looping ? AX_VOICE_LOOP_ENABLED : AX_VOICE_LOOP_DISABLED); + + AXVoiceOffsets offsets {}; + AXGetVoiceOffsets(buffer, &offsets); + + offsets.data = (int16_t*)data; + offsets.currentOffset = 0; + offsets.endOffset = nsamples; + + AXSetVoiceOffsets(buffer, &offsets); + + AXVoiceEnd(buffer); + } + + void DigitalSound::setLooping(AudioBuffer* buffer, bool looping) + { + if (buffer == nullptr) + return; + + AXVoiceBegin(buffer); + + AXSetVoiceLoop(buffer, looping ? AX_VOICE_LOOP_ENABLED : AX_VOICE_LOOP_DISABLED); + + AXVoiceEnd(buffer); + } + + bool DigitalSound::channelReset(size_t id, AudioBuffer* buffer, int channels, int bitDepth, + int sampleRate) + { + if (id >= this->channels.size()) + return false; + + this->channels[id].begin(buffer, channels); + return this->channels[id].reset(channels, bitDepth, sampleRate, 1.0f); + } + + void DigitalSound::channelSetVolume(size_t id, float volume) + { + this->channels[id].setVolume(volume); + } + + float DigitalSound::channelGetVolume(size_t id) const + { + return this->channels[id].getVolume(); + } + + size_t DigitalSound::channelGetSampleOffset(size_t id) + { + return this->channels[id].getSampleOffset(); + } + + bool DigitalSound::channelAddBuffer(size_t id, AudioBuffer*) + { + return this->channels[id].play(); + } + + void DigitalSound::channelStop(size_t id) + { + this->channels[id].stop(); + } + + void DigitalSound::channelPause(size_t id, bool paused) + { + this->channels[id].setPaused(paused); + } + + bool DigitalSound::isChannelPaused(size_t id) const + { + return this->channels[id].isPaused(); + } + + bool DigitalSound::isChannelPlaying(size_t id) const + { + return this->channels[id].isPlaying(); + } + + int32_t DigitalSound::getFormat(int channels, int bitDepth) + { + if (bitDepth != 8 && bitDepth != 16) + return -1; + + if (channels < 1 || channels > 2) + return -2; + + AX_VOICE_FORMAT format; + DigitalSound::getConstant((EncodingFormat)bitDepth, format); + + return format; + } + +} // namespace love diff --git a/platform/cafe/source/driver/EventQueue.cpp b/platform/cafe/source/driver/EventQueue.cpp index f21226a1..b274d5a1 100644 --- a/platform/cafe/source/driver/EventQueue.cpp +++ b/platform/cafe/source/driver/EventQueue.cpp @@ -1,11 +1,16 @@ #include "driver/EventQueue.hpp" #include "modules/joystick/JoystickModule.hpp" +#include "modules/keyboard/Keyboard.hpp" + #include "utility/guid.hpp" +#include + using namespace love; #define JOYSTICK_MODULE() Module::getInstance(Module::M_JOYSTICK) +#define KEYBOARD_MODULE() Module::getInstance(Module::M_KEYBOARD) namespace love { @@ -15,6 +20,33 @@ namespace love EventQueue::~EventQueue() {} + void checkSoftwareKeyboard(VPADStatus& status) + { + VPADGetTPCalibratedPoint(VPAD_CHAN_0, &status.tpNormal, &status.tpNormal); + + nn::swkbd::ControllerInfo info {}; + info.vpad = &status; + + nn::swkbd::Calc(info); + + if (nn::swkbd::IsNeedCalcSubThreadFont()) + nn::swkbd::CalcSubThreadFont(); + + if (nn::swkbd::IsNeedCalcSubThreadPredict()) + nn::swkbd::CalcSubThreadPredict(); + + bool okPressed = nn::swkbd::IsDecideOkButton(nullptr); + bool cancelPressed = nn::swkbd::IsDecideCancelButton(nullptr); + + if (okPressed || cancelPressed) + { + KEYBOARD_MODULE()->hide(); + + if (okPressed) + EventQueue::getInstance().sendTextInput(KEYBOARD_MODULE()->getText()); + } + } + void EventQueue::pollInternal() { if (!JOYSTICK_MODULE()) @@ -30,7 +62,36 @@ namespace love if (joystick->getGamepadType() == GAMEPAD_TYPE_NINTENDO_WII_U_GAMEPAD) this->gamepad = (Joystick*)joystick; - JOYSTICK_MODULE()->getJoystick(index)->update(); + joystick->update(); + + for (int input = 0; input < Joystick::GAMEPAD_BUTTON_MAX_ENUM; input++) + { + std::vector inputs = { Joystick::GamepadButton(input) }; + + if (joystick->isDown(inputs)) + this->sendGamepadButtonEvent(SUBTYPE_GAMEPADDOWN, 0, input); + + if (joystick->isUp(inputs)) + this->sendGamepadButtonEvent(SUBTYPE_GAMEPADUP, 0, input); + } + + for (int input = 0; input < Joystick::GAMEPAD_AXIS_MAX_ENUM; input++) + { + if (joystick->isAxisChanged(Joystick::GamepadAxis(input))) + { + float value = joystick->getAxis(Joystick::GamepadAxis(input)); + this->sendGamepadAxisEvent(0, input, value); + } + } + + for (int input = 0; input < Sensor::SENSOR_MAX_ENUM; input++) + { + if (!joystick->isSensorEnabled(Sensor::SensorType(input))) + continue; + + auto data = joystick->getSensorData(Sensor::SensorType(input)); + this->sendGamepadSensorEvent(0, input, data); + } } if (this->gamepad) diff --git a/platform/cafe/source/modules/audio/Source.cpp b/platform/cafe/source/modules/audio/Source.cpp new file mode 100644 index 00000000..1518b49c --- /dev/null +++ b/platform/cafe/source/modules/audio/Source.cpp @@ -0,0 +1,26 @@ +#include "modules/audio/Source.hpp" + +#include + +#include + +namespace love +{ + StaticDataBuffer::StaticDataBuffer(const void* data, size_t size, size_t nsamples) : + size(size), + nsamples(nsamples) + { + this->buffer = (int16_t*)malloc(size); + + if (this->buffer == nullptr) + throw love::Exception(E_OUT_OF_MEMORY); + + std::memcpy(this->buffer, data, size); + } + + StaticDataBuffer::~StaticDataBuffer() + { + if (this->buffer) + free(this->buffer); + } +} // namespace love diff --git a/platform/cafe/source/modules/joystick/Joystick.cpp b/platform/cafe/source/modules/joystick/Joystick.cpp index bbbbb086..f40c43fb 100644 --- a/platform/cafe/source/modules/joystick/Joystick.cpp +++ b/platform/cafe/source/modules/joystick/Joystick.cpp @@ -239,6 +239,41 @@ namespace love return false; } + bool Joystick::isUp(std::span buttons) const + { + if (!this->isConnected()) + return false; + + VPADButtons result; + + for (GamepadButton button : buttons) + { + if (!Joystick::getConstant(button, result)) + continue; + + if (this->vpadStatus.release & result) + return true; + } + + return false; + } + + bool Joystick::isAxisChanged(GamepadAxis axis) const + { + if (!this->isConnected()) + return false; + + VPADAxis result; + + if (!Joystick::getConstant(axis, result)) + return false; + + if ((this->vpadStatus.hold & result) || (this->vpadStatus.release & result)) + return true; + + return false; + } + void Joystick::setPlayerIndex(int) {} diff --git a/platform/cafe/source/modules/keyboard/Keyboard.cpp b/platform/cafe/source/modules/keyboard/Keyboard.cpp new file mode 100644 index 00000000..90150ce2 --- /dev/null +++ b/platform/cafe/source/modules/keyboard/Keyboard.cpp @@ -0,0 +1,79 @@ +#include "modules/keyboard/Keyboard.hpp" + +#include "utf8.h" + +using namespace nn::swkbd; + +namespace love +{ + Keyboard::Keyboard() : + KeyboardBase(), + createArgs {}, + appearArgs {}, + client(nullptr), + inited(false) + {} + + Keyboard::~Keyboard() + { + if (!this->inited) + return; + + FSDelClient(this->client, FS_ERROR_FLAG_ALL); + MEMFreeToDefaultHeap(this->client); + + nn::swkbd::Destroy(); + MEMFreeToDefaultHeap(this->createArgs.workMemory); + } + + void Keyboard::initialize() + { + this->client = (FSClient*)MEMAllocFromDefaultHeap(sizeof(FSClient)); + + if (!this->client) + throw love::Exception("Failed to allocate FSClient for nn::swkbd!"); + + if (auto result = Result(FSAddClient(this->client, FS_ERROR_FLAG_ALL)); !result) + throw love::Exception("FSAddClient: %x", result.get()); + + this->createArgs.regionType = nn::swkbd::RegionType::USA; + this->createArgs.workMemory = MEMAllocFromDefaultHeap(nn::swkbd::GetWorkMemorySize(0)); + this->appearArgs.keyboardArg.configArg.languageType = nn::swkbd::LanguageType::English; + + if (!this->createArgs.workMemory) + throw love::Exception("No work memory for nn::swkbd!"); + + this->createArgs.fsClient = this->client; + + if (!nn::swkbd::Create(this->createArgs)) + throw love::Exception("Failed to initialize nn:swkbd!"); + + this->inited = true; + } + + void Keyboard::setTextInput(const KeyboardOptions& options) + { + const auto length = this->getMaxEncodingLength(options.maxLength); + + try + { + this->text = std::make_unique(length); + } + catch (std::bad_alloc&) + { + throw love::Exception(E_OUT_OF_MEMORY); + } + + const auto hint = utf8::utf8to16(options.hint); + + this->appearArgs.inputFormArg.hintText = hint.c_str(); + this->appearArgs.inputFormArg.maxTextLength = length; + + const auto password = options.password ? PasswordMode::Fade : PasswordMode::Clear; + this->appearArgs.inputFormArg.passwordMode = password; + + this->showing = true; + + nn::swkbd::AppearInputForm(this->appearArgs); + } +} // namespace love diff --git a/platform/cafe/source/modules/system/System.cpp b/platform/cafe/source/modules/system/System.cpp index 82e9d338..8475f793 100644 --- a/platform/cafe/source/modules/system/System.cpp +++ b/platform/cafe/source/modules/system/System.cpp @@ -73,6 +73,8 @@ namespace love signal = (status > 0) ? 100 : 0; state = (status > 0) ? NetworkState::NETWORK_CONNECTED : NetworkState::NETWORK_DISCONNECTED; + + return state; } System::FriendInfo System::getFriendInfo() const diff --git a/platform/ctr/CMakeLists.txt b/platform/ctr/CMakeLists.txt index 30c414a2..cdfc5ae3 100644 --- a/platform/ctr/CMakeLists.txt +++ b/platform/ctr/CMakeLists.txt @@ -37,9 +37,12 @@ dkp_install_assets(${PROJECT_NAME}_ctr_romfs target_sources(${PROJECT_NAME} PRIVATE source/boot.cpp source/common/screen.cpp +source/driver/DigitalSound.cpp source/driver/EventQueue.cpp +source/modules/audio/Source.cpp source/modules/joystick/Joystick.cpp source/modules/joystick/JoystickModule.cpp +source/modules/keyboard/Keyboard.cpp source/modules/system/System.cpp source/modules/system/wrap_System.cpp source/modules/timer/Timer.cpp diff --git a/platform/ctr/include/driver/DigitalSound.hpp b/platform/ctr/include/driver/DigitalSound.hpp index 66cce8e6..6c9ea370 100644 --- a/platform/ctr/include/driver/DigitalSound.hpp +++ b/platform/ctr/include/driver/DigitalSound.hpp @@ -24,6 +24,15 @@ namespace love float getMasterVolume() const; + AudioBuffer* createBuffer(int size = 0); + + bool isBufferDone(AudioBuffer* buffer) const; + + void prepareBuffer(AudioBuffer* buffer, size_t nsamples, const void* data, size_t size, + bool looping); + + void setLooping(AudioBuffer* buffer, bool looping); + bool channelReset(size_t id, int channels, int bitDepth, int sampleRate); void channelSetVolume(size_t id, float volume); @@ -54,11 +63,11 @@ namespace love ); // clang-format on + static int8_t getFormat(int channels, int bitDepth); + private: static constexpr int32_t DSP_FIRM_MISSING_ERROR_CODE = 0xD880A7FA; LightEvent event; - - static uint8_t getNdspFormat(int channels, int bitDepth); }; } // namespace love diff --git a/platform/ctr/include/modules/joystick/Joystick.hpp b/platform/ctr/include/modules/joystick/Joystick.hpp index 57de75d3..1f15835b 100644 --- a/platform/ctr/include/modules/joystick/Joystick.hpp +++ b/platform/ctr/include/modules/joystick/Joystick.hpp @@ -29,6 +29,10 @@ namespace love virtual bool isDown(std::span buttons) const override; + virtual bool isUp(std::span buttons) const override; + + virtual bool isAxisChanged(GamepadAxis axis) const override; + virtual void setPlayerIndex(int index) override; virtual int getPlayerIndex() const override; @@ -53,6 +57,16 @@ namespace love virtual std::vector getSensorData(Sensor::SensorType type) const override; + enum HidAxisType + { + HIDAXIS_LEFTX = KEY_CPAD_LEFT | KEY_CPAD_RIGHT, + HIDAXIS_LEFTY = KEY_CPAD_UP | KEY_CPAD_DOWN, + HIDAXIS_RIGHTX = KEY_CSTICK_LEFT | KEY_CSTICK_RIGHT, + HIDAXIS_RIGHTY = KEY_CSTICK_UP | KEY_CSTICK_DOWN, + HIDAXIS_TRIGGERLEFT = KEY_ZL, + HIDAXIS_TRIGGERRIGHT = KEY_ZR + }; + // clang-format off ENUMMAP_DECLARE(CtrGamepadButtons, GamepadButton, HidKeyType, { GAMEPAD_BUTTON_A, KEY_A }, @@ -72,6 +86,15 @@ namespace love { GAMEPAD_BUTTON_RIGHTSHOULDER, KEY_R } ); + ENUMMAP_DECLARE(CtrGamepadAxes, GamepadAxis, HidAxisType, + { GAMEPAD_AXIS_LEFTX, HIDAXIS_LEFTX }, + { GAMEPAD_AXIS_LEFTY, HIDAXIS_LEFTY }, + { GAMEPAD_AXIS_RIGHTX, HIDAXIS_RIGHTX }, + { GAMEPAD_AXIS_RIGHTY, HIDAXIS_RIGHTY }, + { GAMEPAD_AXIS_TRIGGERLEFT, HIDAXIS_TRIGGERLEFT }, + { GAMEPAD_AXIS_TRIGGERRIGHT, HIDAXIS_TRIGGERRIGHT } + ); + ENUMMAP_DECLARE(CtrGamepadTypes, GamepadType, CFG_SystemModel, { GAMEPAD_TYPE_NINTENDO_3DS, CFG_MODEL_3DS }, { GAMEPAD_TYPE_NINTENDO_3DS_XL, CFG_MODEL_3DSXL }, diff --git a/platform/ctr/include/modules/keyboard/Keyboard.hpp b/platform/ctr/include/modules/keyboard/Keyboard.hpp new file mode 100644 index 00000000..d1f3b91a --- /dev/null +++ b/platform/ctr/include/modules/keyboard/Keyboard.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "modules/keyboard/Keyboard.tcc" + +#include <3ds.h> + +namespace love +{ + class Keyboard : public KeyboardBase + { + public: + Keyboard(); + + virtual ~Keyboard() + {} + + void setTextInput(const KeyboardOptions& options); + + using KeyboardBase::getConstant; + + // clang-format off + ENUMMAP_DECLARE(CtrKeyboardTypes, KeyboardType, SwkbdType, + { TYPE_NORMAL, SWKBD_TYPE_NORMAL }, + { TYPE_QWERTY, SWKBD_TYPE_QWERTY }, + { TYPE_NUMPAD, SWKBD_TYPE_NUMPAD } + ); + // clang-format on + + private: + SwkbdState state; + }; +} // namespace love diff --git a/platform/ctr/source/driver/DigitalSound.cpp b/platform/ctr/source/driver/DigitalSound.cpp index 273dd8e8..12807d57 100644 --- a/platform/ctr/source/driver/DigitalSound.cpp +++ b/platform/ctr/source/driver/DigitalSound.cpp @@ -21,11 +21,15 @@ namespace love if (result.failed(DSP_FIRM_MISSING_ERROR_CODE)) throw love::Exception(E_AUDIO_NOT_INITIALIZED " (dspfirm.cdc not found)"); - else - throw love::Exception(E_AUDIO_NOT_INITIALIZED ": %x", result.get()); + + if (result.failed()) + throw love::Exception(E_AUDIO_NOT_INITIALIZED ": {:x}", result.get()); LightEvent_Init(&this->event, RESET_ONESHOT); ndspSetCallback(audioCallback, &this->event); + + for (size_t index = 0; index < 24; index++) + this->channelSetVolume(index, 1.0f); } void DigitalSound::updateImpl() @@ -43,14 +47,66 @@ namespace love return ndspGetMasterVol(); } + AudioBuffer* DigitalSound::createBuffer(int size) + { + AudioBuffer* buffer = new AudioBuffer(); + + if (size != 0) + { + buffer->data_pcm16 = (int16_t*)linearAlloc(size); + + if (buffer->data_pcm16 == nullptr) + throw love::Exception(E_OUT_OF_MEMORY); + } + else + buffer->data_pcm16 = nullptr; + + buffer->status = NDSP_WBUF_DONE; + + return buffer; + } + + bool DigitalSound::isBufferDone(AudioBuffer* buffer) const + { + if (buffer == nullptr) + return false; + + return buffer->status == NDSP_WBUF_DONE; + } + + void DigitalSound::prepareBuffer(AudioBuffer* buffer, size_t nsamples, const void* data, + size_t size, bool looping) + { + if (buffer == nullptr || data == nullptr) + return; + + if (buffer->data_pcm16 != nullptr) + std::copy_n((int16_t*)data, size, buffer->data_pcm16); + else + buffer->data_pcm16 = (int16_t*)data; + + DSP_FlushDataCache(buffer->data_pcm16, size); + + buffer->nsamples = nsamples; + buffer->looping = looping; + } + + void DigitalSound::setLooping(AudioBuffer* buffer, bool looping) + { + if (buffer == nullptr) + return; + + buffer->looping = looping; + } + // #region Channels bool DigitalSound::channelReset(size_t id, int channels, int bitDepth, int sampleRate) { ndspChnReset(id); - uint8_t format = 0; - if ((format = DigitalSound::getNdspFormat(bitDepth, channels)) < 0) + int8_t format = 0; + if ((format = DigitalSound::getFormat(channels, bitDepth)) < 0) return false; ndspInterpType interpType; @@ -116,12 +172,12 @@ namespace love // #endregion - uint8_t DigitalSound::getNdspFormat(int bitDepth, int channels) + int8_t DigitalSound::getFormat(int channels, int bitDepth) { if (bitDepth != 8 && bitDepth != 16) return -1; - if (channels < 1 && channels > 2) + if (channels < 1 || channels > 2) return -2; NdspEncodingType encoding; diff --git a/platform/ctr/source/driver/EventQueue.cpp b/platform/ctr/source/driver/EventQueue.cpp index c420c9b1..60412919 100644 --- a/platform/ctr/source/driver/EventQueue.cpp +++ b/platform/ctr/source/driver/EventQueue.cpp @@ -1,5 +1,14 @@ #include "driver/EventQueue.hpp" +#include "modules/joystick/Joystick.hpp" +#include "modules/joystick/JoystickModule.hpp" + +#include + +using namespace love; + +#define JOYSTICK_MODULE() Module::getInstance(Module::M_JOYSTICK) + static aptHookCookie s_aptHookCookie; static void aptEventHook(const APT_HookType type, void*) @@ -74,5 +83,39 @@ namespace love float x = this->touchState.current.px, y = this->touchState.current.py; this->sendTouchEvent(SUBTYPE_TOUCHRELEASE, 0, x, y, 0.0f, 0.0, 0.0f); } + + const auto* joystick = JOYSTICK_MODULE()->getJoystick(0); + + if (!joystick) + return; + + for (int input = 0; input < Joystick::GAMEPAD_BUTTON_MAX_ENUM; input++) + { + std::vector inputs = { Joystick::GamepadButton(input) }; + + if (joystick->isDown(inputs)) + this->sendGamepadButtonEvent(SUBTYPE_GAMEPADDOWN, 0, input); + + if (joystick->isUp(inputs)) + this->sendGamepadButtonEvent(SUBTYPE_GAMEPADUP, 0, input); + } + + for (int input = 0; input < Joystick::GAMEPAD_AXIS_MAX_ENUM; input++) + { + if (joystick->isAxisChanged(Joystick::GamepadAxis(input))) + { + float value = joystick->getAxis(Joystick::GamepadAxis(input)); + this->sendGamepadAxisEvent(0, input, value); + } + } + + for (int input = 0; input < Sensor::SENSOR_MAX_ENUM; input++) + { + if (!joystick->isSensorEnabled(Sensor::SensorType(input))) + continue; + + auto data = joystick->getSensorData(Sensor::SensorType(input)); + this->sendGamepadSensorEvent(0, input, data); + } } } // namespace love diff --git a/platform/ctr/source/modules/audio/Source.cpp b/platform/ctr/source/modules/audio/Source.cpp new file mode 100644 index 00000000..8adddcc0 --- /dev/null +++ b/platform/ctr/source/modules/audio/Source.cpp @@ -0,0 +1,25 @@ +#include "modules/audio/Source.hpp" + +#include + +#include <3ds.h> + +namespace love +{ + StaticDataBuffer::StaticDataBuffer(const void* data, size_t size, size_t nsamples) : + size(size), + nsamples(nsamples) + { + this->buffer = (int16_t*)linearAlloc(size); + + if (this->buffer == nullptr) + throw love::Exception(E_OUT_OF_MEMORY); + + std::memcpy(this->buffer, (int16_t*)data, size); + } + + StaticDataBuffer::~StaticDataBuffer() + { + linearFree(this->buffer); + } +} // namespace love diff --git a/platform/ctr/source/modules/joystick/Joystick.cpp b/platform/ctr/source/modules/joystick/Joystick.cpp index 80e0d9b4..ad874915 100644 --- a/platform/ctr/source/modules/joystick/Joystick.cpp +++ b/platform/ctr/source/modules/joystick/Joystick.cpp @@ -115,6 +115,41 @@ namespace love return false; } + bool Joystick::isUp(std::span buttons) const + { + if (!this->isConnected()) + return false; + + HidKeyType result; + + for (GamepadButton button : buttons) + { + if (!Joystick::getConstant(button, result)) + continue; + + if (hidKeysUp() & result) + return true; + } + + return false; + } + + bool Joystick::isAxisChanged(GamepadAxis axis) const + { + if (!this->isConnected()) + return false; + + HidAxisType result; + + if (!Joystick::getConstant(axis, result)) + return false; + + if ((hidKeysHeld() & result) || (hidKeysUp() & result)) + return true; + + return false; + } + void Joystick::setPlayerIndex(int) {} diff --git a/platform/ctr/source/modules/keyboard/Keyboard.cpp b/platform/ctr/source/modules/keyboard/Keyboard.cpp new file mode 100644 index 00000000..25e3c929 --- /dev/null +++ b/platform/ctr/source/modules/keyboard/Keyboard.cpp @@ -0,0 +1,38 @@ +#include "modules/keyboard/Keyboard.hpp" +#include "driver/EventQueue.hpp" + +namespace love +{ + Keyboard::Keyboard() : KeyboardBase(), state {} + {} + + void Keyboard::setTextInput(const KeyboardOptions& options) + { + const auto length = this->getMaxEncodingLength(options.maxLength); + + try + { + this->text = std::make_unique(length); + } + catch (std::bad_alloc&) + { + throw love::Exception(E_OUT_OF_MEMORY); + } + + const auto type = (SwkbdType)options.type; + + swkbdInit(&this->state, type, 2, length); + swkbdSetInitialText(&this->state, this->text.get()); + swkbdSetHintText(&this->state, options.hint.data()); + + if (options.password) + swkbdSetPasswordMode(&this->state, SWKBD_PASSWORD_HIDE_DELAY); + + this->showing = true; + + const auto button = swkbdInputText(&this->state, this->text.get(), length); + + if (button != SWKBD_BUTTON_LEFT) + EventQueue::getInstance().sendTextInput(this->text); + } +} // namespace love diff --git a/platform/hac/CMakeLists.txt b/platform/hac/CMakeLists.txt index c70266da..551a421c 100644 --- a/platform/hac/CMakeLists.txt +++ b/platform/hac/CMakeLists.txt @@ -29,9 +29,12 @@ target_include_directories(${PROJECT_NAME} PRIVATE target_sources(${PROJECT_NAME} PRIVATE source/boot.cpp source/common/screen.cpp +source/driver/DigitalSound.cpp source/driver/EventQueue.cpp +source/modules/audio/Source.cpp source/modules/joystick/Joystick.cpp source/modules/joystick/JoystickModule.cpp +source/modules/keyboard/Keyboard.cpp source/modules/system/System.cpp source/modules/timer/Timer.cpp ) diff --git a/platform/hac/include/driver/DigitalSound.hpp b/platform/hac/include/driver/DigitalSound.hpp new file mode 100644 index 00000000..ccbc9fac --- /dev/null +++ b/platform/hac/include/driver/DigitalSound.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include "driver/DigitalSound.tcc" + +#include +#include + +#include + +namespace love +{ + using AudioBuffer = AudioDriverWaveBuf; + + class DigitalSound : public DigitalSoundBase + { + public: + ~DigitalSound(); + + virtual void initialize() override; + + void updateImpl(); + + void setMasterVolume(float volume); + + float getMasterVolume() const; + + AudioBuffer* createBuffer(int size = 0); + + bool isBufferDone(AudioBuffer* buffer) const; + + void prepareBuffer(AudioBuffer* buffer, size_t nsamples, const void* data, size_t size, + bool looping); + + void setLooping(AudioBuffer* buffer, bool looping); + + bool channelReset(size_t id, int channels, int bitDepth, int sampleRate); + + void channelSetVolume(size_t id, float volume); + + float channelGetVolume(size_t id) const; + + size_t channelGetSampleOffset(size_t id); + + bool channelAddBuffer(size_t id, AudioBuffer* buffer); + + void channelPause(size_t id, bool paused); + + bool isChannelPaused(size_t id); + + bool isChannelPlaying(size_t id); + + void channelStop(size_t id); + + // clang-format off + ENUMMAP_DECLARE(AudioFormats, EncodingFormat, PcmFormat, + { ENCODING_PCM8, PcmFormat_Int8 }, + { ENCODING_PCM16, PcmFormat_Int16 } + ); + // clang-format on + + static int8_t getFormat(int channels, int bitDepth); + + private: + std::mutex mutex; + AudioDriver driver; + bool resetChannel; + }; +} // namespace love diff --git a/platform/hac/include/driver/DigitalSoundMem.hpp b/platform/hac/include/driver/DigitalSoundMem.hpp new file mode 100644 index 00000000..5e23e1ba --- /dev/null +++ b/platform/hac/include/driver/DigitalSoundMem.hpp @@ -0,0 +1,316 @@ +#pragma once + +#include "common/Exception.hpp" +#include "common/Singleton.tcc" + +#include +#include +#include + +#include + +#include +#include + +namespace love +{ + class DigitalSoundMemory : public Singleton + { + private: + struct Chunk + { + uint8_t* address; + size_t size; + }; + + struct Block + { + Block* prev; + Block* next; + + uint8_t* base; + size_t size; + + static Block* create(uint8_t* base, size_t size) + { + auto* block = (Block*)malloc(sizeof(Block)); + + if (block == nullptr) + return nullptr; + + block->prev = nullptr; + block->next = nullptr; + + block->base = base; + block->size = size; + + return block; + } + }; + + struct Pool + { + Block* first; + Block* last; + + bool ready() + { + return this->first != nullptr; + } + + void addBlock(Block* block) + { + block->prev = this->last; + + if (this->last != nullptr) + this->last->next = block; + + if (!this->first) + this->first = block; + + this->last = block; + } + + void deleteBlock(Block* block) + { + auto* prev = block->prev; + auto*& pNext = (prev) ? prev->next : this->first; + + auto* next = block->next; + auto*& nNext = (next) ? next->prev : this->last; + + pNext = next; + nNext = prev; + + std::free(block); + } + + void insertBefore(Block* block, Block* newBlock) + { + auto* prev = block->prev; + auto& pNext = (prev) ? prev->next : this->first; + + block->prev = newBlock; + newBlock->next = block; + + newBlock->prev = prev; + + pNext = newBlock; + } + + void insertAfter(Block* block, Block* newBlock) + { + auto* next = block->next; + auto& nPrev = (next) ? next->prev : this->last; + + block->next = newBlock; + newBlock->prev = block; + + newBlock->next = next; + + nPrev = newBlock; + } + + void coalesceRight(Block* block) + { + auto* current = block->base + block->size; + auto* next = block->next; + + for (auto* n = next; n; n = next) + { + next = n->next; + + if (n->base != current) + break; + + block->size += n->size; + current += n->size; + + this->deleteBlock(n); + } + } + + bool allocate(Chunk& chunk, size_t size) + { + size_t alignMask = (AUDREN_BUFFER_ALIGNMENT - 1); + + if (size && alignMask) + { + if (size > UINTPTR_MAX - alignMask) + return false; + + size = (size + alignMask) & ~alignMask; + } + + for (auto* block = this->first; block; block = block->next) + { + auto* address = block->base; + + size_t waste = (size_t)address & alignMask; + + if (waste > 0) + waste = alignMask + 1 - waste; + + if (waste > block->size) + continue; + + address += waste; + + size_t blockSize = block->size - waste; + + if (blockSize < size) + continue; + + // found space + chunk.address = address; + chunk.size = size; + + if (!waste) + { + block->base += size; + block->size -= size; + + if (block->size == 0) + this->deleteBlock(block); + } + else + { + auto* nextBlock = address + size; + uint64_t nextSize = blockSize - size; + + block->size = waste; + + if (nextSize) + { + auto* newBlock = Block::create(nextBlock, nextSize); + + if (newBlock) + this->insertAfter(block, newBlock); + else + chunk.size += nextSize; + } + } + + return true; + } + + return false; + } + + void deallocate(uint8_t* address, size_t size) + { + bool done = false; + + for (auto* block = first; !done && block; block = block->next) + { + auto* baseAddress = block->base; + + if (baseAddress > address) + { + if ((address + size) == baseAddress) + { + block->base = address; + block->size += size; + } + else + { + auto* chunk = Block::create(address, size); + + if (chunk) + this->insertBefore(block, chunk); + } + done = true; + } + else if ((block->base + block->size) == address) + { + block->size += size; + this->coalesceRight(block); + done = true; + } + } + + if (!done) + { + auto* block = Block::create(address, size); + + if (block) + this->addBlock(block); + } + } + + void destroy() + { + Block* next = nullptr; + + for (auto* block = this->first; block; block = next) + { + next = block->next; + std::free(block); + } + + this->first = nullptr; + this->last = nullptr; + } + }; + + bool initialize() + { + if (!this->initMemoryPool()) + return false; + + auto* block = Block::create((uint8_t*)this->base, this->size); + + if (!block) + return false; + + this->pool.addBlock(block); + return true; + } + + public: + bool initMemoryPool() + { + if (!this->base) + this->base = aligned_alloc(AUDREN_MEMPOOL_ALIGNMENT, size); + + if (!this->base) + return false; + + return true; + } + + void* align(size_t size, size_t& alignment) + { + if (!this->pool.ready() && !this->initialize()) + return nullptr; + + Chunk chunk {}; + if (!this->pool.allocate(chunk, size)) + return nullptr; + + alignment = chunk.size; + return chunk.address; + } + + void free(const void* memory, size_t size) + { + this->pool.deallocate((uint8_t*)memory, size); + } + + void* getBaseAddress() + { + return this->base; + } + + size_t getSize() + { + return this->size; + } + + private: + Pool pool; + + void* base; + static constexpr size_t size = 0x1000000; + }; +} // namespace love diff --git a/platform/hac/include/modules/joystick/Joystick.hpp b/platform/hac/include/modules/joystick/Joystick.hpp index cce001db..5e2ede85 100644 --- a/platform/hac/include/modules/joystick/Joystick.hpp +++ b/platform/hac/include/modules/joystick/Joystick.hpp @@ -27,6 +27,10 @@ namespace love virtual bool isDown(std::span buttons) const override; + virtual bool isUp(std::span buttons) const override; + + virtual bool isAxisChanged(GamepadAxis axis) const override; + virtual void setPlayerIndex(int index) override; virtual int getPlayerIndex() const override; @@ -51,6 +55,23 @@ namespace love virtual std::vector getSensorData(Sensor::SensorType type) const override; + using JoystickBase::getConstant; + + virtual void update() + { + padUpdate(&this->state); + } + + enum HidNpadAxis + { + HidNpadAxis_LeftX = HidNpadButton_StickLLeft | HidNpadButton_StickLRight, + HidNpadAxis_LeftY = HidNpadButton_StickLDown | HidNpadButton_StickLUp, + HidNpadAxis_RightX = HidNpadButton_StickRLeft | HidNpadButton_StickRRight, + HidNpadAxis_RightY = HidNpadButton_StickRDown | HidNpadButton_StickRUp, + HidNpadAxis_TriggerL = HidNpadButton_ZL, + HidNpadAxis_TriggerR = HidNpadButton_ZR + }; + // clang-format off ENUMMAP_DECLARE(HacButtonTypes, GamepadButton, HidNpadButton, { GAMEPAD_BUTTON_A, HidNpadButton_A }, @@ -72,6 +93,15 @@ namespace love { GAMEPAD_BUTTON_RIGHTSTICK, HidNpadButton_StickR } ); + ENUMMAP_DECLARE(HacGamepadAxes, GamepadAxis, HidNpadAxis, + { GAMEPAD_AXIS_LEFTX, HidNpadAxis_LeftX }, + { GAMEPAD_AXIS_LEFTY, HidNpadAxis_LeftY }, + { GAMEPAD_AXIS_RIGHTX, HidNpadAxis_RightX }, + { GAMEPAD_AXIS_RIGHTY, HidNpadAxis_RightY }, + { GAMEPAD_AXIS_TRIGGERLEFT, HidNpadAxis_TriggerL }, + { GAMEPAD_AXIS_TRIGGERRIGHT, HidNpadAxis_TriggerR } + ); + ENUMMAP_DECLARE(HacGamepadTypes, GamepadType, HidNpadStyleTag, { GAMEPAD_TYPE_JOYCON_LEFT, HidNpadStyleTag_NpadJoyLeft }, { GAMEPAD_TYPE_JOYCON_RIGHT, HidNpadStyleTag_NpadJoyRight }, @@ -81,13 +111,6 @@ namespace love ); // clang-format on - using JoystickBase::getConstant; - - virtual void update() - { - padUpdate(&this->state); - } - private: HidNpadIdType getHandleCount(int& handles, HidNpadStyleTag& tag) const; diff --git a/platform/hac/include/modules/keyboard/Keyboard.hpp b/platform/hac/include/modules/keyboard/Keyboard.hpp new file mode 100644 index 00000000..4b2e3757 --- /dev/null +++ b/platform/hac/include/modules/keyboard/Keyboard.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "modules/keyboard/Keyboard.tcc" + +#include + +namespace love +{ + class Keyboard : public KeyboardBase + { + public: + Keyboard(); + + virtual ~Keyboard() + {} + + void setTextInput(const KeyboardOptions& options); + + using KeyboardBase::getConstant; + + // clang-format off + ENUMMAP_DECLARE(HacKeyboardTypes, KeyboardType, SwkbdType, + { TYPE_NORMAL, SwkbdType_Normal }, + { TYPE_NUMPAD, SwkbdType_NumPad }, + { TYPE_QWERTY, SwkbdType_QWERTY } + ); + // clang-format on + + private: + SwkbdConfig config; + }; +} // namespace love diff --git a/platform/hac/source/driver/DigitalSound.cpp b/platform/hac/source/driver/DigitalSound.cpp new file mode 100644 index 00000000..d380c36d --- /dev/null +++ b/platform/hac/source/driver/DigitalSound.cpp @@ -0,0 +1,234 @@ +#include "common/Exception.hpp" +#include "common/Result.hpp" + +#include "driver/DigitalSound.hpp" +#include "driver/DigitalSoundMem.hpp" + +namespace love +{ + static constexpr AudioRendererConfig config = { + .output_rate = AudioRendererOutputRate_48kHz, + .num_voices = 24, + .num_effects = 0, + .num_sinks = 1, + .num_mix_objs = 1, + .num_mix_buffers = 2, + }; + + static constexpr uint8_t sinkChannels[2] = { 0, 1 }; + + void DigitalSound::initialize() + { + if (bool result = DigitalSoundMemory::getInstance().initMemoryPool(); !result) + throw love::Exception("Failed to create audio memory pool!"); + + if (auto result = Result(audrenInitialize(&config)); !result) + throw love::Exception("Failed to initialize audren: {:x}", result.get()); + + if (auto result = Result(audrvCreate(&this->driver, &config, 2)); !result) + throw love::Exception("Failed to create audio driver: {:x}", result.get()); + + // clang-format off + int poolId = audrvMemPoolAdd(&this->driver, DigitalSoundMemory::getInstance().getBaseAddress(), DigitalSoundMemory::getInstance().getSize()); + // clang-format on + + if (poolId == -1) + throw love::Exception("Failed to add memory pool!"); + + bool attached = audrvMemPoolAttach(&this->driver, poolId); + + if (!attached) + throw love::Exception("Failed to attach memory pool!"); + + int sinkId = audrvDeviceSinkAdd(&this->driver, AUDREN_DEFAULT_DEVICE_NAME, 2, sinkChannels); + + if (sinkId == -1) + throw love::Exception("Failed to add sink to driver!"); + + if (auto result = Result(audrvUpdate(&this->driver)); !result) + throw love::Exception("Failed to update audio driver: {:x}", result.get()); + + if (auto result = Result(audrenStartAudioRenderer()); !result) + throw love::Exception("Failed to start audio renderer: {:x}", result.get()); + } + + DigitalSound::~DigitalSound() + { + audrvClose(&this->driver); + audrenExit(); + } + + void DigitalSound::updateImpl() + { + { + std::unique_lock lock(this->mutex); + audrvUpdate(&this->driver); + } + + audrenWaitFrame(); + } + + void DigitalSound::setMasterVolume(float volume) + { + std::unique_lock lock(this->mutex); + + for (int mix = 0; mix < 2; mix++) + audrvMixSetVolume(&this->driver, mix, volume); + } + + float DigitalSound::getMasterVolume() const + { + return this->driver.in_mixes[0].volume; + } + + AudioBuffer* DigitalSound::createBuffer(int size) + { + AudioBuffer* buffer = new AudioBuffer(); + + if (size != 0) + { + size_t aligned = 0; + buffer->data_pcm16 = (int16_t*)DigitalSoundMemory::getInstance().align(size, aligned); + + if (buffer->data_pcm16 == nullptr) + throw love::Exception(E_OUT_OF_MEMORY); + } + else + buffer->data_pcm16 = nullptr; + + buffer->state = AudioDriverWaveBufState_Done; + + return buffer; + } + + bool DigitalSound::isBufferDone(AudioBuffer* buffer) const + { + if (buffer == nullptr) + return true; + + return buffer->state == AudioDriverWaveBufState_Done; + } + + void DigitalSound::prepareBuffer(AudioBuffer* buffer, size_t nsamples, const void* data, + size_t size, bool looping) + { + if (buffer == nullptr || data == nullptr) + return; + + if (buffer->data_pcm16 != nullptr) + std::copy_n((int16_t*)data, size, buffer->data_pcm16); + else + buffer->data_pcm16 = (int16_t*)data; + + armDCacheFlush(buffer->data_pcm16, size); + + buffer->size = size; + buffer->end_sample_offset = nsamples; + buffer->is_looping = looping; + } + + void DigitalSound::setLooping(AudioBuffer* buffer, bool looping) + { + if (buffer == nullptr) + return; + + buffer->is_looping = looping; + } + + bool DigitalSound::channelReset(size_t id, int channels, int bitDepth, int sampleRate) + { + std::unique_lock lock(this->mutex); + + int8_t format = PcmFormat_Invalid; + if ((format = DigitalSound::getFormat(channels, bitDepth)) < 0) + return false; + + this->resetChannel = + audrvVoiceInit(&this->driver, id, channels, (PcmFormat)format, sampleRate); + + if (this->resetChannel) + { + audrvVoiceSetDestinationMix(&this->driver, id, AUDREN_FINAL_MIX_ID); + audrvVoiceSetMixFactor(&this->driver, id, 1.0f, 0, 0); + + if (channels == 2) + audrvVoiceSetMixFactor(&this->driver, id, 1.0f, 0, 1); + } + + return this->resetChannel; + } + + void DigitalSound::channelSetVolume(size_t id, float volume) + { + std::unique_lock lock(this->mutex); + + audrvVoiceSetVolume(&this->driver, id, volume); + } + + float DigitalSound::channelGetVolume(size_t id) const + { + return this->driver.in_voices[id].volume; + } + + size_t DigitalSound::channelGetSampleOffset(size_t id) + { + std::unique_lock lock(this->mutex); + + return audrvVoiceGetPlayedSampleCount(&this->driver, id); + } + + bool DigitalSound::channelAddBuffer(size_t id, AudioBuffer* buffer) + { + if (this->resetChannel) + { + std::unique_lock lock(this->mutex); + + bool success = audrvVoiceAddWaveBuf(&this->driver, id, buffer); + + if (success) + audrvVoiceStart(&this->driver, id); + + return success; + } + + return false; + } + + void DigitalSound::channelPause(size_t id, bool paused) + { + std::unique_lock lock(this->mutex); + + audrvVoiceSetPaused(&this->driver, id, paused); + } + + bool DigitalSound::isChannelPaused(size_t id) + { + return audrvVoiceIsPaused(&this->driver, id); + } + + bool DigitalSound::isChannelPlaying(size_t id) + { + return audrvVoiceIsPlaying(&this->driver, id); + } + + void DigitalSound::channelStop(size_t id) + { + std::unique_lock lock(this->mutex); + + audrvVoiceStop(&this->driver, id); + } + + int8_t DigitalSound::getFormat(int channels, int bitDepth) + { + if (bitDepth != 8 && bitDepth != 16) + return -1; + + if (channels < 1 || channels > 2) + return -2; + + PcmFormat format; + DigitalSound::getConstant((EncodingFormat)bitDepth, format); + + return format; + } +} // namespace love diff --git a/platform/hac/source/driver/EventQueue.cpp b/platform/hac/source/driver/EventQueue.cpp index e101b43e..246c7b28 100644 --- a/platform/hac/source/driver/EventQueue.cpp +++ b/platform/hac/source/driver/EventQueue.cpp @@ -74,16 +74,16 @@ namespace love int current = JOYSTICK_MODULE()->getJoystickCount(); - // for (int index = 0; index < 0x08; index++) - // { - // if (R_SUCCEEDED(eventWait(&this->padStyleUpdates[index], 0))) - // { - // int newCount = JOYSTICK_MODULE()->getJoystickCount(); + for (int index = 0; index < 0x08; index++) + { + if (R_SUCCEEDED(eventWait(&this->padStyleUpdates[index], 0))) + { + int newCount = JOYSTICK_MODULE()->getJoystickCount(); - // if (newCount != current) - // this->sendJoystickStatus(newCount > current, index); - // } - // } + if (newCount != current) + this->sendJoystickStatus(newCount > current, index); + } + } for (int index = 0; index < current; index++) { @@ -93,6 +93,35 @@ namespace love continue; joystick->update(); + + for (int input = 0; input < Joystick::GAMEPAD_BUTTON_MAX_ENUM; input++) + { + std::vector inputs = { Joystick::GamepadButton(input) }; + + if (joystick->isDown(inputs)) + this->sendGamepadButtonEvent(SUBTYPE_GAMEPADDOWN, 0, input); + + if (joystick->isUp(inputs)) + this->sendGamepadButtonEvent(SUBTYPE_GAMEPADUP, 0, input); + } + + for (int input = 0; input < Joystick::GAMEPAD_AXIS_MAX_ENUM; input++) + { + if (joystick->isAxisChanged(Joystick::GamepadAxis(input))) + { + float value = joystick->getAxis(Joystick::GamepadAxis(input)); + this->sendGamepadAxisEvent(0, input, value); + } + } + + for (int input = 0; input < Sensor::SENSOR_MAX_ENUM; input++) + { + if (!joystick->isSensorEnabled(Sensor::SensorType(input))) + continue; + + auto data = joystick->getSensorData(Sensor::SensorType(input)); + this->sendGamepadSensorEvent(0, input, data); + } } } } // namespace love diff --git a/platform/hac/source/modules/audio/Source.cpp b/platform/hac/source/modules/audio/Source.cpp new file mode 100644 index 00000000..ba7a3dd1 --- /dev/null +++ b/platform/hac/source/modules/audio/Source.cpp @@ -0,0 +1,24 @@ +#include "modules/audio/Source.hpp" +#include "driver/DigitalSoundMem.hpp" + +namespace love +{ + StaticDataBuffer::StaticDataBuffer(const void* data, size_t size, size_t nsamples) : + buffer(nullptr), + size(size), + nsamples(nsamples) + { + this->buffer = (int16_t*)DigitalSoundMemory::getInstance().align(size, this->alignSize); + + if (this->buffer == nullptr) + throw love::Exception(E_OUT_OF_MEMORY); + + std::copy_n((int16_t*)data, size, this->buffer); + } + + StaticDataBuffer::~StaticDataBuffer() + { + if (this->buffer != nullptr) + DigitalSoundMemory::getInstance().free(this->buffer, this->alignSize); + } +} // namespace love diff --git a/platform/hac/source/modules/joystick/Joystick.cpp b/platform/hac/source/modules/joystick/Joystick.cpp index d013c7be..619fc358 100644 --- a/platform/hac/source/modules/joystick/Joystick.cpp +++ b/platform/hac/source/modules/joystick/Joystick.cpp @@ -120,6 +120,41 @@ namespace love return false; } + bool Joystick::isUp(std::span buttons) const + { + if (!this->isConnected()) + return false; + + HidNpadButton result; + + for (GamepadButton button : buttons) + { + if (!Joystick::getConstant(button, result)) + continue; + + if (padGetButtonsUp(&this->state) & result) + return true; + } + + return false; + } + + bool Joystick::isAxisChanged(GamepadAxis axis) const + { + if (!this->isConnected()) + return false; + + HidNpadAxis result; + + if (!Joystick::getConstant(axis, result)) + return false; + + if ((padGetButtons(&this->state) & result) || (padGetButtonsUp(&this->state) & result)) + return true; + + return false; + } + void Joystick::setPlayerIndex(int) {} diff --git a/platform/hac/source/modules/keyboard/Keyboard.cpp b/platform/hac/source/modules/keyboard/Keyboard.cpp new file mode 100644 index 00000000..06cd2bf2 --- /dev/null +++ b/platform/hac/source/modules/keyboard/Keyboard.cpp @@ -0,0 +1,42 @@ +#include "modules/keyboard/Keyboard.hpp" +#include "driver/EventQueue.hpp" + +namespace love +{ + Keyboard::Keyboard() : KeyboardBase(), config {} + {} + + void Keyboard::setTextInput(const KeyboardOptions& options) + { + if (!Result(swkbdCreate(&this->config, 0))) + throw love::Exception("Failed to create software keyboard."); + + const auto length = this->getMaxEncodingLength(options.maxLength); + + try + { + this->text = std::make_unique(length); + } + catch (std::bad_alloc&) + { + throw love::Exception(E_OUT_OF_MEMORY); + } + + const auto type = (SwkbdType)options.type; + swkbdConfigSetType(&this->config, type); + + swkbdConfigSetInitialCursorPos(&this->config, 1); + swkbdConfigSetBlurBackground(&this->config, 1); + + swkbdConfigSetPasswordFlag(&this->config, options.password); + swkbdConfigSetStringLenMax(&this->config, length); + swkbdConfigSetDicFlag(&this->config, 1); + + swkbdConfigSetGuideText(&this->config, options.hint.data()); + + this->showing = true; + + if (Result(swkbdShow(&this->config, this->text.get(), length))) + EventQueue::getInstance().sendTextInput(this->text); + } +} // namespace love diff --git a/source/common/luax.cpp b/source/common/luax.cpp index 027f5de6..ed75ea03 100644 --- a/source/common/luax.cpp +++ b/source/common/luax.cpp @@ -620,6 +620,11 @@ namespace love lua_pushlstring(L, string, sizeof(void*)); } + int luax_enumerror(lua_State* L, const char* enumName, const char* value) + { + return luaL_error(L, "Invalid %s: %s", enumName, value); + } + int luax_ioerror(lua_State* L, const char* format, ...) { std::va_list args; diff --git a/source/main.cpp b/source/main.cpp index 9bb2802f..88cddb2a 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -18,7 +18,7 @@ enum DoneAction DONE_QUIT, DONE_RESTART }; -#include "utility/logfile.hpp" + static DoneAction runLove(char** argv, int argc, int& result, love::Variant& restartValue) { if (love::preInit() != 0) @@ -42,9 +42,6 @@ static DoneAction runLove(char** argv, int argc, int& result, love::Variant& res lua_rawseti(L, -2, -2); } - for (int index = 0; index < argc; index++) - LOG("argv[{}] = {}", index, argv[index]); - // on 3DS and Switch, argv[0] is the binary path // on Wii U, argv[0] is "safe.rpx" // args[-1] is "embedded boot.lua" diff --git a/source/modules/audio/Audio.cpp b/source/modules/audio/Audio.cpp new file mode 100644 index 00000000..22546185 --- /dev/null +++ b/source/modules/audio/Audio.cpp @@ -0,0 +1,93 @@ +#include "modules/audio/Audio.hpp" +#include "modules/audio/Pool.hpp" + +#include "driver/DigitalSound.hpp" + +namespace love +{ + Audio::PoolThread::PoolThread(Pool* pool) : pool(pool), finish(false) + { + this->threadName = "AudioPool"; + } + + void Audio::PoolThread::run() + { + while (true) + { + if (this->finish) + return; + + this->pool->update(); + DigitalSound::getInstance().update(); + } + } + + Audio::Audio() : Module(M_AUDIO, "love.audio"), pool(nullptr), poolThread(nullptr) + { + try + { + this->pool = new Pool(); + } + catch (love::Exception&) + { + throw; + } + + this->poolThread = new PoolThread(this->pool); + this->poolThread->start(); + } + + Audio::~Audio() + { + this->poolThread->setFinish(); + this->poolThread->wait(); + + delete this->poolThread; + delete this->pool; + } + + Source* Audio::newSource(SoundData* soundData) const + { + return new Source(this->pool, soundData); + } + + Source* Audio::newSource(Decoder* decoder) const + { + return new Source(this->pool, decoder); + } + + Source* Audio::newSource(int sampleRate, int bitDepth, int channels, int buffers) const + { + return new Source(this->pool, sampleRate, bitDepth, channels, buffers); + } + + int Audio::getActiveSourceCount() const + { + return this->pool->getActiveSourceCount(); + } + + bool Audio::play(Source* source) + { + return source->play(); + } + + void Audio::stop(Source* source) + { + source->stop(); + } + + void Audio::pause(Source* source) + { + source->pause(); + } + + void Audio::setVolume(float volume) + { + DigitalSound::getInstance().setMasterVolume(volume); + } + + float Audio::getVolume() const + { + return DigitalSound::getInstance().getMasterVolume(); + } +} // namespace love diff --git a/source/modules/audio/Pool.cpp b/source/modules/audio/Pool.cpp new file mode 100644 index 00000000..4b633a28 --- /dev/null +++ b/source/modules/audio/Pool.cpp @@ -0,0 +1,139 @@ +#include "modules/audio/Pool.hpp" + +namespace love +{ + Pool::Pool() : totalSources(0) + { + DigitalSound::getInstance().initialize(); + + for (size_t index = 0; index < Pool::MAX_SOURCES; index++) + this->available.push(index); + + this->totalSources = this->available.size(); + } + + Pool::~Pool() + { + Source::stop(this); + } + + bool Pool::isAvailable() + { + bool has = false; + + { + std::unique_lock lock(this->mutex); + has = !this->available.empty(); + } + + return has; + } + + bool Pool::isPlaying(Source* source) + { + bool playing = false; + + { + std::unique_lock lock(this->mutex); + playing = (this->playing.find(source) != this->playing.end()); + } + + return playing; + } + + void Pool::update() + { + std::unique_lock lock(this->mutex); + std::vector release; + + for (const auto& iterator : this->playing) + { + if (!iterator.first->update()) + release.push_back(iterator.first); + } + + for (auto* source : release) + this->releaseSource(source); + } + + int Pool::getActiveSourceCount() const + { + return this->playing.size(); + } + + int Pool::getMaxSources() const + { + return this->totalSources; + } + + void Pool::addSource(Source* source, size_t channel) + { + this->playing.insert({ source, channel }); + source->retain(); + } + + bool Pool::assignSource(Source* source, size_t& channel, uint8_t& wasPlaying) + { + channel = 0; + + if (this->findSource(source, channel)) + return (wasPlaying = true); + + wasPlaying = false; + + if (this->available.empty()) + return false; + + channel = this->available.front(); + this->available.pop(); + + return true; + } + + bool Pool::releaseSource(Source* source, bool stop) + { + size_t channel; + + if (this->findSource(source, channel)) + { + if (stop) + source->stopAtomic(); + + source->release(); + + this->available.push(channel); + this->playing.erase(source); + + return true; + } + + return false; + } + + bool Pool::findSource(Source* source, size_t& channel) + { + auto iterator = this->playing.find(source); + + if (iterator == this->playing.end()) + return false; + + channel = iterator->second; + return true; + } + + std::vector Pool::getPlayingSources() + { + std::vector sources; + sources.reserve(this->playing.size()); + + for (const auto& iterator : this->playing) + sources.push_back(iterator.first); + + return sources; + } + + std::unique_lock Pool::lock() + { + return std::unique_lock(this->mutex); + } +} // namespace love diff --git a/source/modules/audio/Source.cpp b/source/modules/audio/Source.cpp new file mode 100644 index 00000000..38f48f17 --- /dev/null +++ b/source/modules/audio/Source.cpp @@ -0,0 +1,673 @@ +#include "common/Exception.hpp" + +#include "modules/audio/Source.hpp" + +#include "modules/audio/Pool.hpp" + +#include + +namespace love +{ + Type Source::type("Source", &Object::type); + + class InvalidFormatException : public love::Exception + { + public: + InvalidFormatException(int channels, int bitdepth) : + Exception("{:d}-channel Sources with {:d} bits per sample are not supported.", channels, + bitdepth) + {} + }; + + class QueueFormatMismatchException : public love::Exception + { + public: + QueueFormatMismatchException() : + Exception("Queued sound data must have same format as sound Source.") + {} + }; + + class QueueTypeMismatchException : public love::Exception + { + public: + QueueTypeMismatchException() : + Exception("Only queueable Sources can be queued with sound data.") + {} + }; + + class QueueMalformedLengthException : public love::Exception + { + public: + QueueMalformedLengthException(int bytes) : + Exception("Data length must be a multiple of sample size ({:d} bytes).", bytes) + {} + }; + + class QueueLoopingException : public love::Exception + { + public: + QueueLoopingException() : Exception("Queueable Sources can not be looped.") + {} + }; + + Source::Source(Pool* pool, SoundData* soundData) : + sourceType(TYPE_STATIC), + pool(pool), + sampleRate(soundData->getSampleRate()), + channels(soundData->getChannelCount()), + bitDepth(soundData->getBitDepth()) + { + if (DigitalSound::getFormat(soundData->getChannelCount(), soundData->getBitDepth()) < 0) + throw InvalidFormatException(soundData->getChannelCount(), soundData->getBitDepth()); + + // clang-format off + this->staticBuffer.set(new StaticDataBuffer(soundData->getData(), soundData->getSize(), soundData->getSampleCount()), Acquire::NO_RETAIN); + this->unusedBuffers.push(DigitalSound::getInstance().createBuffer()); + // clang-format on + } + + Source::Source(Pool* pool, Decoder* decoder) : + sourceType(TYPE_STREAM), + pool(pool), + sampleRate(decoder->getSampleRate()), + channels(decoder->getChannelCount()), + bitDepth(decoder->getBitDepth()), + decoder(decoder), + buffers(DEFAULT_BUFFERS) + { + if (DigitalSound::getFormat(decoder->getChannelCount(), decoder->getBitDepth()) < 0) + throw InvalidFormatException(decoder->getChannelCount(), decoder->getBitDepth()); + + for (int index = 0; index < this->buffers; index++) + this->unusedBuffers.push(DigitalSound::getInstance().createBuffer(decoder->getSize())); + } + + Source::Source(Pool* pool, int sampleRate, int bitDepth, int channels, int buffers) : + sourceType(TYPE_QUEUE), + pool(pool), + sampleRate(sampleRate), + channels(channels), + bitDepth(bitDepth), + buffers(buffers) + { + if (DigitalSound::getFormat(channels, bitDepth) < 0) + throw InvalidFormatException(channels, bitDepth); + + this->buffers = std::clamp(buffers, 1, MAX_BUFFERS); + + for (int index = 0; index < this->buffers; index++) + this->unusedBuffers.push(DigitalSound::getInstance().createBuffer()); + } + + Source::Source(const Source& other) : + sourceType(other.sourceType), + pool(other.pool), + valid(false), + staticBuffer(other.staticBuffer), + pitch(other.pitch), + volume(other.volume), + looping(other.looping), + minVolume(other.minVolume), + maxVolume(other.maxVolume), + offsetSamples(0), + sampleRate(other.sampleRate), + channels(other.channels), + bitDepth(other.bitDepth), + decoder(nullptr), + buffers(other.buffers) + { + if (this->sourceType == TYPE_STREAM) + { + if (other.decoder.get()) + this->decoder.set(other.decoder->clone(), Acquire::NO_RETAIN); + } + + if (this->sourceType != TYPE_STATIC) + { + for (int index = 0; index < this->buffers; index++) + this->unusedBuffers.push(new AudioBuffer()); + } + } + + Source::~Source() + { + this->stop(); + + if (this->sourceType != TYPE_STATIC) + { + while (!this->streamBuffers.empty()) + { + delete this->streamBuffers.front(); + this->streamBuffers.pop(); + } + } + + while (!this->unusedBuffers.empty()) + { + delete this->unusedBuffers.top(); + this->unusedBuffers.pop(); + } + } + + bool Source::update() + { + if (!this->valid) + return false; + + switch (this->sourceType) + { + case TYPE_STATIC: + return !this->isFinished(); + case TYPE_STREAM: + { + if (!this->isFinished()) + { + while (!this->unusedBuffers.empty()) + { + auto* buffer = this->unusedBuffers.top(); + if (this->streamAtomic(buffer, this->decoder.get()) > 0) + { + DigitalSound::getInstance().channelAddBuffer(this->channel, buffer); + this->unusedBuffers.pop(); + } + else + break; + } + return true; + } + return false; + } + case TYPE_QUEUE: + break; + default: + break; + } + + return false; + } + + Source* Source::clone() + { + return new Source(*this); + } + + bool Source::play() + { + uint8_t wasPlaying; + + { + auto lock = this->pool->lock(); + + if (!this->pool->assignSource(this, this->channel, wasPlaying)) + return this->valid = false; + } + + if (!wasPlaying) + { + if (!(this->valid = this->playAtomic(this->unusedBuffers.top()))) + return false; + + this->unusedBuffers.pop(); + this->resumeAtomic(); + + { + auto lock = this->pool->lock(); + this->pool->addSource(this, this->channel); + } + + return this->valid; + } + + this->resumeAtomic(); + return this->valid = true; + } + + // clang-format off + void Source::reset() + { + #if !defined(__WIIU__) + if (!DigitalSound::getInstance().channelReset(this->channel, this->channels, this->bitDepth, this->sampleRate)) + std::printf("Failed to reset channel %zu\n", this->channel); + #else + if (!DigitalSound::getInstance().channelReset(this->channel, this->source, this->channels, this->bitDepth, this->sampleRate)) + std::printf("Failed to reset channel %zu\n", this->channel); + #endif + } + // clang-format on + + void Source::stop() + { + if (!this->valid) + return; + + auto lock = this->pool->lock(); + this->pool->releaseSource(this); + } + + bool Source::play(const std::vector& sources) + { + // if (sources.size() == 0) + // return true; + + // auto* pool = ((Source*)sources[0])->pool; + // auto lock = pool->lock(); + + // std::vector wasPlaying(sources.size()); + // std::vector channels(sources.size()); + + // for (size_t index = 0; index < sources.size(); index++) + // { + // auto* source = (Source*)sources[index]; + + // if (!pool->assignSource(source, channels[index], wasPlaying[index])) + // { + // for (size_t j = 0; j < index; j++) + // { + // if (!wasPlaying[j]) + // pool->releaseSource(sources[j], false); + + // return false; + // } + // } + // } + + // std::vector channelsToPlay; + // channelsToPlay.reserve(sources.size()); + + // for (size_t index = 0; index < sources.size(); index++) + // { + // if (wasPlaying[index] && sources[index]->isPlaying()) + // continue; + + // if (!wasPlaying[index]) + // { + // auto* source = (Source*)sources[index]; + // source->channel = channels[index]; + // source->prepareAtomic(); + // } + + // channelsToPlay.push_back(channels[index]); + // } + + // bool success = false; + // for (auto& channel : channelsToPlay) + // { + // if (DigitalSound::getInstance().channelAddBuffer(channel, sources[channel])) + // } + } + + void Source::stop(const std::vector& sources) + { + if (sources.size() == 0) + return; + + auto pool = ((Source*)sources[0])->pool; + auto lock = pool->lock(); + + for (auto& _source : sources) + { + auto* source = (Source*)_source; + + if (source->valid) + source->teardownAtomic(); + + pool->releaseSource(source, false); + } + } + + void Source::pause(const std::vector& sources) + { + if (sources.size() == 0) + return; + + auto lock = ((Source*)sources[0])->pool->lock(); + + std::vector channels; + channels.reserve(sources.size()); + + for (auto& _source : sources) + { + auto* source = (Source*)_source; + + if (source->valid) + channels.push_back(source->channel); + } + + for (auto& channel : channels) + DigitalSound::getInstance().channelPause(channel, true); + } + + std::vector Source::pause(Pool* pool) + { + auto lock = pool->lock(); + auto sources = pool->getPlayingSources(); + + // clang-format off + auto end = std::remove_if(sources.begin(), sources.end(), [](Source* source) { return !source->isPlaying(); }); + // clang-format on + + sources.erase(end, sources.end()); + Source::pause(sources); + + return sources; + } + + void Source::pause() + { + auto lock = this->pool->lock(); + + if (this->pool->isPlaying(this)) + this->pauseAtomic(); + } + + void Source::stop(Pool* pool) + { + auto lock = pool->lock(); + Source::stop(pool->getPlayingSources()); + } + + bool Source::isPlaying() const + { + if (!this->valid) + return false; + + return DigitalSound::getInstance().isChannelPaused(this->channel) == false; + } + + bool Source::isFinished() const + { + if (!this->valid) + return false; + + if (this->sourceType == TYPE_STREAM && (this->isLooping() || !this->decoder->isFinished())) + return false; + + if (this->sourceType == TYPE_STATIC) + return DigitalSound::getInstance().isBufferDone(this->source); + + return DigitalSound::getInstance().isChannelPlaying(this->channel) == false; + } + + void Source::setVolume(float volume) + { + if (this->valid) + DigitalSound::getInstance().channelSetVolume(this->channel, this->volume); + + this->volume = volume; + } + + float Source::getVolume() const + { + if (this->valid) + return DigitalSound::getInstance().channelGetVolume(this->channel); + + return this->volume; + } + + void Source::seek(double offset, Unit unit) + { + auto lock = this->pool->lock(); + + int offsetSamples = 0; + double offsetSeconds = 0.0; + + switch (unit) + { + case UNIT_SAMPLES: + { + offsetSamples = (int)offset; + offsetSeconds = offset / ((double)this->sampleRate / this->channels); + break; + } + case UNIT_SECONDS: + default: + { + offsetSamples = (int)(offset * this->sampleRate * this->channels); + offsetSeconds = offset; + break; + } + } + + bool wasPlaying = this->isPlaying(); + + switch (this->sourceType) + { + case TYPE_STATIC: + { + if (this->valid) + this->stop(); + + this->offsetSamples = offsetSamples; + + if (wasPlaying) + this->play(); + + break; + } + case TYPE_STREAM: + { + if (this->valid) + this->stop(); + + this->decoder->seek(offsetSeconds); + + if (wasPlaying) + this->play(); + + break; + } + case TYPE_QUEUE: + break; + } + + if (wasPlaying && (this->sourceType == TYPE_STREAM && !this->isPlaying())) + { + this->stop(); + + if (this->isLooping()) + this->play(); + + return; + } + + this->offsetSamples = offsetSamples; + } + + double Source::tell(Unit unit) + { + auto lock = this->pool->lock(); + + int offset = 0; + + if (this->valid) + offset = DigitalSound::getInstance().channelGetSampleOffset(this->channel); + + offset += this->offsetSamples; + + return (unit == UNIT_SAMPLES) ? offset : (offset / this->sampleRate); + } + + double Source::getDuration(Unit unit) + { + auto lock = this->pool->lock(); + + switch (this->sourceType) + { + case TYPE_STATIC: + { + const auto size = this->staticBuffer->getSize(); + const auto nsamples = ((size / this->channels) / (this->bitDepth / 8)) - + (this->offsetSamples / this->channels); + + return (unit == UNIT_SAMPLES) ? nsamples : (nsamples / this->sampleRate); + } + case TYPE_STREAM: + { + double seconds = this->decoder->getDuration(); + return (unit == UNIT_SAMPLES) ? (seconds * this->sampleRate) : seconds; + } + case TYPE_QUEUE: + return 0.0; + default: + return 0.0; + } + + return 0.0; + } + + int Source::getFreeBufferCount() const + { + switch (this->sourceType) + { + case TYPE_STATIC: + return 0; + case TYPE_STREAM: + return this->unusedBuffers.size(); + case TYPE_QUEUE: + return this->unusedBuffers.size(); + case TYPE_MAX_ENUM: + return 0; + } + + return 0; + } + + void Source::setLooping(bool looping) + { + if (this->sourceType == TYPE_STREAM) + throw QueueLoopingException(); + + if (this->valid && this->sourceType == TYPE_STATIC) + DigitalSound::getInstance().setLooping(this->source, this->looping); + + this->looping = looping; + } + + bool Source::isLooping() const + { + return this->looping; + } + + // clang-format off + void Source::prepareAtomic() + { + this->reset(); + + switch (this->sourceType) + { + case TYPE_STATIC: + { + const auto size = this->staticBuffer->getSize(); + const auto nsamples = this->staticBuffer->getSampleCount() - (this->offsetSamples / this->channels); + + auto* buffer = this->staticBuffer->getBuffer() + this->offsetSamples; + + DigitalSound::getInstance().prepareBuffer(this->source, nsamples, buffer, size, this->looping); + break; + } + case TYPE_STREAM: + { + while (!this->unusedBuffers.empty()) + { + auto* buffer = this->unusedBuffers.top(); + if (this->streamAtomic(buffer, this->decoder.get()) == 0) + break; + + DigitalSound::getInstance().channelAddBuffer(this->channel, buffer); + this->unusedBuffers.pop(); + + if (this->decoder->isFinished()) + break; + } + break; + } + case TYPE_QUEUE: + break; + default: + break; + } + } + // clang-format on + + void Source::teardownAtomic() + { + DigitalSound::getInstance().channelStop(this->channel); + std::printf("Stopped channel %zu\n", this->channel); + + switch (this->sourceType) + { + case TYPE_STATIC: + std::printf("Creating buffer\n"); + this->unusedBuffers.push(DigitalSound::getInstance().createBuffer()); + break; + case TYPE_STREAM: + break; //??? + case TYPE_QUEUE: + break; + default: + break; + } + + this->valid = false; + this->offsetSamples = 0; + } + + int Source::streamAtomic(AudioBuffer* buffer, Decoder* decoder) + { + int decoded = std::max(decoder->decode(), 0); + + if (decoded > 0) + { + int nsamples = ((decoded / this->channels) / (this->bitDepth / 8)); + auto* data = decoder->getBuffer(); + + DigitalSound::getInstance().prepareBuffer(buffer, nsamples, data, decoded, + this->looping); + } + + if (decoder->isFinished() && this->isLooping()) + decoder->rewind(); + + return decoded; + } + + bool Source::playAtomic(AudioBuffer* buffer) + { + this->source = buffer; + this->prepareAtomic(); + + DigitalSound::getInstance().channelAddBuffer(this->channel, this->source); + + if (this->sourceType != TYPE_STREAM) + this->offsetSamples = 0; + + if (this->sourceType == TYPE_STREAM) + this->valid = true; + + return true; + } + + void Source::stopAtomic() + { + if (!this->valid) + return; + + this->teardownAtomic(); + } + + void Source::pauseAtomic() + { + if (!this->valid) + return; + + DigitalSound::getInstance().channelPause(this->channel, true); + } + + void Source::resumeAtomic() + { + if (!this->valid) + return; + + DigitalSound::getInstance().channelPause(this->channel, false); + } +} // namespace love diff --git a/source/modules/audio/wrap_Audio.cpp b/source/modules/audio/wrap_Audio.cpp new file mode 100644 index 00000000..5e9afb14 --- /dev/null +++ b/source/modules/audio/wrap_Audio.cpp @@ -0,0 +1,145 @@ +#include "modules/audio/wrap_Audio.hpp" +#include "modules/audio/wrap_Source.hpp" + +#include "modules/filesystem/wrap_Filesystem.hpp" + +using namespace love; + +#define instance() Module::getInstance