diff --git a/.gitmodules b/.gitmodules index 16daad0..ac5c6fc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule "subprojects/libintl-lite"] path = subprojects/libintl-lite url = https://github.com/hathlife/libintl-lite.git +[submodule "subprojects/oboe"] + path = subprojects/oboe + url = https://github.com/google/oboe.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 30f5c6b..cc86d85 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -249,6 +249,11 @@ else() set(LIBINTL_LIBRARY Intl::Intl CACHE INTERNAL "") set(LIBINTL_INCDIRS ${Intl_INCLUDE_DIRS} CACHE INTERNAL "") endif() + +if (DEFINED ANDROID_NDK) + add_subdirectory(subprojects/oboe) + target_link_libraries(liblooper PUBLIC oboe) +endif() target_include_directories(liblooper PUBLIC ${LIBINTL_INCDIRS}) target_link_libraries(liblooper PUBLIC ${LIBINTL_LIBRARY}) if(DEFINED EMSCRIPTEN) @@ -264,7 +269,7 @@ else() pkg_check_modules(jsoncpp IMPORTED_TARGET jsoncpp) endif() if (NOT BUILD_SDL) - find_package(SDL2 REQUIRED) + find_package(SDL2 REQUIRED) endif() if (ENABLE_DBUS) find_package(sdbus-c++) @@ -273,7 +278,11 @@ else() message("Warning: Dbus support not found - Not enabling DBus") endif() endif() - target_link_libraries(liblooper PUBLIC SDL2::SDL2 SDL2-static SDL2main ${SDL_MIXER_X_TARGET} ${SOUNDTOUCH_TARGET} libvgmstream libvgmstream_shared ${JSONCPP_TARGET}) + set(SDL2_TARGET SDL2::SDL2) + if (TARGET SDL2-static) + set(SDL2_TARGET SDL2-static) + endif() + target_link_libraries(liblooper PUBLIC ${SDL2_TARGET} SDL2main ${SDL_MIXER_X_TARGET} ${SOUNDTOUCH_TARGET} libvgmstream libvgmstream_shared ${JSONCPP_TARGET}) endif() if (${ENABLE_DBUS}) target_link_libraries(liblooper PUBLIC SDBusCpp::sdbus-c++) diff --git a/assets/app.dbus.xml b/assets/app.dbus.xml index e0a2272..f0ba313 100644 --- a/assets/app.dbus.xml +++ b/assets/app.dbus.xml @@ -13,7 +13,7 @@ - + @@ -81,7 +81,7 @@ - + diff --git a/assets/com.complecwaft.Looper.metainfo.xml b/assets/com.complecwaft.Looper.metainfo.xml index 85779f5..fcbc903 100644 --- a/assets/com.complecwaft.Looper.metainfo.xml +++ b/assets/com.complecwaft.Looper.metainfo.xml @@ -1,6 +1,6 @@ - com.complecwaft.Looper + com.complecwaft.looper Catmeow72 Looper diff --git a/assets/dbus_stub_adaptor.hpp b/assets/dbus_stub_adaptor.hpp index 215fc0a..d25c515 100644 --- a/assets/dbus_stub_adaptor.hpp +++ b/assets/dbus_stub_adaptor.hpp @@ -48,13 +48,13 @@ private: namespace com { namespace complecwaft { -class Looper_adaptor +class looper_adaptor { public: - static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper"; + static constexpr const char* INTERFACE_NAME = "com.complecwaft.looper"; protected: - Looper_adaptor(sdbus::IObject& object) + looper_adaptor(sdbus::IObject& object) : object_(&object) { object_->registerMethod("CreateHandle").onInterface(INTERFACE_NAME).withOutputParamNames("new_handle").implementedAs([this](){ return this->CreateHandle(); }); @@ -90,12 +90,12 @@ protected: object_->registerProperty("StreamIdx").onInterface(INTERFACE_NAME).withGetter([this](){ return this->StreamIdx(); }); } - Looper_adaptor(const Looper_adaptor&) = delete; - Looper_adaptor& operator=(const Looper_adaptor&) = delete; - Looper_adaptor(Looper_adaptor&&) = default; - Looper_adaptor& operator=(Looper_adaptor&&) = default; + looper_adaptor(const looper_adaptor&) = delete; + looper_adaptor& operator=(const looper_adaptor&) = delete; + looper_adaptor(looper_adaptor&&) = default; + looper_adaptor& operator=(looper_adaptor&&) = default; - ~Looper_adaptor() = default; + ~looper_adaptor() = default; public: void emitPlaybackEngineStarted() @@ -183,12 +183,12 @@ private: namespace com { namespace complecwaft { -namespace Looper { +namespace looper { class Errors_adaptor { public: - static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper.Errors"; + static constexpr const char* INTERFACE_NAME = "com.complecwaft.looper.Errors"; protected: Errors_adaptor(sdbus::IObject& object) diff --git a/assets/dbus_stub_proxy.hpp b/assets/dbus_stub_proxy.hpp index d9c9bb8..64190a9 100644 --- a/assets/dbus_stub_proxy.hpp +++ b/assets/dbus_stub_proxy.hpp @@ -56,13 +56,13 @@ private: namespace com { namespace complecwaft { -class Looper_proxy +class looper_proxy { public: - static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper"; + static constexpr const char* INTERFACE_NAME = "com.complecwaft.looper"; protected: - Looper_proxy(sdbus::IProxy& proxy) + looper_proxy(sdbus::IProxy& proxy) : proxy_(&proxy) { proxy_->uponSignal("PlaybackEngineStarted").onInterface(INTERFACE_NAME).call([this](){ this->onPlaybackEngineStarted(); }); @@ -76,12 +76,12 @@ protected: proxy_->uponSignal("FileChanged").onInterface(INTERFACE_NAME).call([this](const std::string& path, const std::string& title){ this->onFileChanged(path, title); }); } - Looper_proxy(const Looper_proxy&) = delete; - Looper_proxy& operator=(const Looper_proxy&) = delete; - Looper_proxy(Looper_proxy&&) = default; - Looper_proxy& operator=(Looper_proxy&&) = default; + looper_proxy(const looper_proxy&) = delete; + looper_proxy& operator=(const looper_proxy&) = delete; + looper_proxy(looper_proxy&&) = default; + looper_proxy& operator=(looper_proxy&&) = default; - ~Looper_proxy() = default; + ~looper_proxy() = default; virtual void onPlaybackEngineStarted() = 0; virtual void onSpeedChanged(const double& new_speed) = 0; @@ -247,12 +247,12 @@ private: namespace com { namespace complecwaft { -namespace Looper { +namespace looper { class Errors_proxy { public: - static constexpr const char* INTERFACE_NAME = "com.complecwaft.Looper.Errors"; + static constexpr const char* INTERFACE_NAME = "com.complecwaft.looper.Errors"; protected: Errors_proxy(sdbus::IProxy& proxy) diff --git a/backend_glue.cpp b/backend_glue.cpp index e8250b2..d46c01b 100644 --- a/backend_glue.cpp +++ b/backend_glue.cpp @@ -1,6 +1,8 @@ #include "backend.hpp" #include "backends/ui/imgui/ui_backend.hpp" +#include "backends/ui/gtk/main.h" void init_backends() { UIBackend::register_backend(); + UIBackend::register_backend(); } diff --git a/backends/ui/imgui/CMakeLists.txt b/backends/ui/imgui/CMakeLists.txt index 2174ea9..03178db 100644 --- a/backends/ui/imgui/CMakeLists.txt +++ b/backends/ui/imgui/CMakeLists.txt @@ -6,8 +6,9 @@ else() set(GLES_NORMALLY_REQUIRED_FOR_ARCHITECTURE OFF) endif() option(USE_GLES "Enable using OpenGL ES" ${GLES_NORMALLY_REQUIRED_FOR_ARCHITECTURE}) +option(GLES_VERSION "Version of OpenGL ES" 3) set(IMGUI_SRC imgui_demo.cpp imgui_draw.cpp imgui_tables.cpp imgui_widgets.cpp imgui.cpp misc/cpp/imgui_stdlib.cpp) -set(IMGUI_BACKEND_SRC imgui_impl_opengl3.cpp imgui_impl_sdl2.cpp) +set(IMGUI_BACKEND_SRC imgui_impl_sdlrenderer2.cpp imgui_impl_sdl2.cpp) set(BACKEND_IMGUI_SRC_BASE main.cpp base85.cpp file_browser.cpp main.cpp RendererBackend.cpp theme.cpp) foreach(SRC IN ITEMS ${IMGUI_BACKEND_SRC}) list(APPEND IMGUI_SRC backends/${SRC}) @@ -26,8 +27,8 @@ foreach(INCDIR IN ITEMS ${BACKEND_IMGUI_INC_BASE}) set(BACKEND_IMGUI_INC ${BACKEND_IMGUI_INC} ${CMAKE_CURRENT_SOURCE_DIR}/${INCDIR}) endforeach() if(USE_GLES) - set(GLComponents GLES2) - set(GLTarget GLES2) + set(GLComponents GLES${GLES_VERSION}) + set(GLTarget GLES${GLES_VERSION}) else() set(GLComponents OpenGL) set(GLTarget GL) @@ -36,9 +37,9 @@ find_package(OpenGL COMPONENTS ${GLComponents}) if (NOT ${OpenGL_FOUND}) if (DEFINED CMAKE_ANDROID_NDK) if (USE_GLES) - find_path(GLES2_INCLUDE_DIR GLES2/gl2.h HINTS ${CMAKE_ANDROID_NDK}) - find_library(GLES2_LIBRARY libGLESv2.so HINTS ${GLES2_INCLUDE_DIR}/../lib) - find_library(GLES3_LIBRARY libGLESv3.so HINTS ${GLES2_INCLUDE_DIR}/../lib) + find_path(GLES_INCLUDE_DIR GLES${GLES_VERSION}/gl${GLES_VERSION}.h HINTS ${CMAKE_ANDROID_NDK}) + find_library(GLES2_LIBRARY libGLESv2.so HINTS ${GLES_INCLUDE_DIR}/../lib) + find_library(GLES3_LIBRARY libGLESv3.so HINTS ${GLES_INCLUDE_DIR}/../lib) add_library(OpenGL::${GLTarget} INTERFACE IMPORTED) target_include_directories(OpenGL::${GLTarget} INTERFACE ${GLES2_INCLUDE_DIR}) target_link_libraries(OpenGL::${GLTarget} INTERFACE ${GLES2_LIBRARY} ${GLES3_LIBRARY}) @@ -47,7 +48,7 @@ if (NOT ${OpenGL_FOUND}) endif() add_ui_backend(imgui_ui ${BACKEND_IMGUI_SRC}) if(USE_GLES) - target_compile_definitions(imgui_ui PRIVATE IMGUI_IMPL_OPENGL_ES2=1) + target_compile_definitions(imgui_ui PRIVATE IMGUI_IMPL_OPENGL_ES${GLES_VERSION}) endif() if(DEFINED EMSCRIPTEN) target_compile_options(imgui_ui PRIVATE "-sUSE_SDL_IMAGE=2") diff --git a/backends/ui/imgui/RendererBackend.cpp b/backends/ui/imgui/RendererBackend.cpp index df7b293..381b1cd 100644 --- a/backends/ui/imgui/RendererBackend.cpp +++ b/backends/ui/imgui/RendererBackend.cpp @@ -5,18 +5,23 @@ #include "config.h" #include #include +#include +#include #include "theme.h" #include "imgui_stdlib.h" #include "imgui_impl_sdl2.h" -#include "imgui_impl_opengl3.h" +#include "imgui_impl_sdlrenderer2.h" #include "base85.h" #include #include #include +#include using std::vector; +using namespace Looper::Options; #ifdef __EMSCRIPTEN__ extern "C" { extern void get_size(int32_t *x, int32_t *y); + extern double get_dpi(); } #endif void RendererBackend::on_resize() { @@ -24,6 +29,7 @@ void RendererBackend::on_resize() { int32_t x, y; get_size(&x, &y); SDL_SetWindowSize(window, (int)x, (int)y); + UpdateScale(); #endif } static RendererBackend *renderer_backend; @@ -40,14 +46,41 @@ void main_loop() { } #endif } + +struct FontData { + const ImWchar *ranges; + std::map> data; + void Init(const ImWchar *ranges_in, std::map> data_in) { + ranges = ranges_in; + data = data_in; + } + FontData(const ImWchar *ranges, std::initializer_list>> data) { + std::map> out_data; + for (auto pair : data) { + out_data[pair.first] = pair.second; + } + Init(ranges, out_data); + } + FontData(const ImWchar *ranges, std::initializer_list> data) { + std::map> out_data; + for (auto tuple : data) { + std::pair outPair = {std::get<1>(tuple), std::get<2>(tuple)}; + out_data[std::get<0>(tuple)] = outPair; + } + Init(ranges, out_data); + } + FontData(const ImWchar *ranges, std::map> data) { + Init(ranges, data); + } +}; void RendererBackend::BackendDeinit() { ImGuiIO& io = ImGui::GetIO(); (void)io; // Cleanup - ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplSDLRenderer2_Shutdown(); ImGui_ImplSDL2_Shutdown(); ImGui::DestroyContext(); - SDL_GL_DeleteContext(gl_context); + SDL_DestroyRenderer(rend); SDL_DestroyWindow(window); IMG_Quit(); SDL_Quit(); @@ -55,7 +88,11 @@ void RendererBackend::BackendDeinit() { Deinit(); renderer_backend = nullptr; } +bool RendererBackend::isTouchScreenMode() { + return touchScreenModeOverride.value_or(SDL_GetNumTouchDevices() > 0); +} void RendererBackend::LoopFunction() { + SDL_RenderSetVSync(rend, vsync ? 1 : 0); ImGuiIO& io = ImGui::GetIO(); (void)io; if (resize_needed) { on_resize(); @@ -92,78 +129,71 @@ void RendererBackend::LoopFunction() { } } } + bool touchScreenMode = isTouchScreenMode(); + if (touchScreenMode) { + io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen; + } else { + io.ConfigFlags &= ~ImGuiConfigFlags_IsTouchScreen; + } // Start the Dear ImGui frame - ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplSDLRenderer2_NewFrame(); ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); // Run the GUI GuiFunction(); // Rendering ImGui::Render(); - // Update the window size. - glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y); - // Clear the screen. - glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w); - glClear(GL_COLOR_BUFFER_BIT); + SDL_SetRenderDrawColor(rend, (Uint8)(clear_color.x * clear_color.w * 255), (Uint8)(clear_color.y * clear_color.w * 255), (Uint8)(clear_color.z * clear_color.w * 255), (Uint8)(clear_color.w * 255)); + SDL_RenderClear(rend); // Tell ImGui to render. - ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData()); - // Update and Render additional Platform Windows - // (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere. - // For this specific demo app we could also call SDL_GL_MakeCurrent(window, gl_context) directly) - if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) - { - SDL_Window* backup_current_window = SDL_GL_GetCurrentWindow(); - SDL_GLContext backup_current_context = SDL_GL_GetCurrentContext(); - ImGui::UpdatePlatformWindows(); - ImGui::RenderPlatformWindowsDefault(); - SDL_GL_MakeCurrent(backup_current_window, backup_current_context); - } // Swap the buffers, and do VSync if enabled. - SDL_GL_SwapWindow(window); + SDL_RenderPresent(rend); // If not doing VSync, wait until the next frame needs to be rendered. if (!vsync) { std::this_thread::sleep_until(next_frame); } } -struct FontData { - const char* data; - const ImWchar *ranges; -}; -ImFont *add_font(vector data_vec, int size = 13) { - ImFont* font = nullptr; +std::map add_font(FontData data_vec, double scale) { ImGuiIO& io = ImGui::GetIO(); - for (auto data : data_vec) { - ImFontConfig font_cfg = ImFontConfig(); - font_cfg.SizePixels = size; - font_cfg.OversampleH = font_cfg.OversampleV = 1; - font_cfg.PixelSnapH = true; - if (font_cfg.SizePixels <= 0.0f) - font_cfg.SizePixels = 13.0f * 1.0f; - if (font != nullptr) { - font_cfg.DstFont = font; - font_cfg.MergeMode = true; - } - //font_cfg.EllipsisChar = (ImWchar)0x0085; - //font_cfg.GlyphOffset.y = 1.0f * IM_FLOOR(font_cfg.SizePixels / 13.0f); // Add +1 offset per 13 units + std::map output; + for (auto value : data_vec.data) { + ImFont* font = nullptr; + std::string id = value.first; + const char *data = value.second.first; + double size = value.second.second * scale; + { + ImFontConfig font_cfg = ImFontConfig(); + font_cfg.SizePixels = size; + font_cfg.OversampleH = font_cfg.OversampleV = 1; + font_cfg.PixelSnapH = true; + if (font_cfg.SizePixels <= 0.0f) + font_cfg.SizePixels = 13.0f * 1.0f; + //font_cfg.EllipsisChar = (ImWchar)0x0085; + //font_cfg.GlyphOffset.y = 1.0f * IM_FLOOR(font_cfg.SizePixels / 13.0f); // Add +1 offset per 13 units - const char* ttf_compressed_base85 = data.data; - const ImWchar* glyph_ranges = data.ranges; - auto new_font = io.Fonts->AddFontFromMemoryCompressedBase85TTF(ttf_compressed_base85, font_cfg.SizePixels, &font_cfg, glyph_ranges); - if (font == nullptr) font = new_font; + const char *ttf_compressed_base85 = data; + const ImWchar *glyph_ranges = data_vec.ranges; + font = io.Fonts->AddFontFromMemoryCompressedBase85TTF(ttf_compressed_base85, + font_cfg.SizePixels, + &font_cfg, glyph_ranges); + } + { + ImFontConfig config; + config.MergeMode = true; + config.GlyphMinAdvanceX = size; + config.SizePixels = size; + config.DstFont = font; + static const ImWchar icon_ranges[] = {ICON_MIN_FK, ICON_MAX_FK, 0}; + io.Fonts->AddFontFromMemoryCompressedBase85TTF(forkawesome_compressed_data_base85, + float(size), &config, icon_ranges); + } + output[id] = font; } - { - ImFontConfig config; - config.MergeMode = true; - config.GlyphMinAdvanceX = size; - config.SizePixels = size; - config.DstFont = font; - static const ImWchar icon_ranges[] = { ICON_MIN_FK, ICON_MAX_FK, 0 }; - io.Fonts->AddFontFromMemoryCompressedBase85TTF(forkawesome_compressed_data_base85, float(size), &config, icon_ranges); - } - return font; + return output; } RendererBackend::RendererBackend() { } @@ -184,6 +214,9 @@ void RendererBackend::UpdateScale() { #else 96.0; #endif +#ifdef __EMSCRIPTEN__ + scale = get_dpi() / defaultDPI; +#else float dpi = defaultDPI; if (SDL_GetDisplayDPI(SDL_GetWindowDisplayIndex(window), NULL, &dpi, NULL) == 0){ scale = dpi / defaultDPI; @@ -191,13 +224,18 @@ void RendererBackend::UpdateScale() { WARNING.writeln("DPI couldn't be determined!"); scale = 1.0; } +#ifndef __ANDROID__ SDL_SetWindowSize(window, window_width * scale, window_height * scale); +#endif +#endif AddFonts(); } void RendererBackend::SetWindowSize(int w, int h) { +#if !(defined(__ANDROID__) || defined(__EMSCRIPTEN__)) window_width = w; window_height = h; SDL_SetWindowSize(window, w * scale, h * scale); +#endif } void RendererBackend::GetWindowsize(int *w, int *h) { int ww, wh; @@ -208,12 +246,25 @@ void RendererBackend::GetWindowsize(int *w, int *h) { if (h) *h = wh; } void RendererBackend::AddFonts() { - ImGui_ImplOpenGL3_DestroyFontsTexture(); + ImGui_ImplSDLRenderer2_DestroyFontsTexture(); auto& io = ImGui::GetIO(); (void)io; io.Fonts->Clear(); - add_font(vector({FontData {notosans_regular_compressed_data_base85, io.Fonts->GetGlyphRangesDefault()}, FontData {notosansjp_regular_compressed_data_base85, io.Fonts->GetGlyphRangesJapanese()}}), 13 * scale); - title = add_font(vector({FontData {notosans_thin_compressed_data_base85, io.Fonts->GetGlyphRangesDefault()}, FontData {notosansjp_thin_compressed_data_base85, io.Fonts->GetGlyphRangesJapanese()}}), 48 * scale); - ImGui_ImplOpenGL3_CreateFontsTexture(); + std::string font_type = get_option("font_type", "default"); + std::string default_font = "default"; + std::string jp_font = "japanese"; + auto glyph_ranges_default = io.Fonts->GetGlyphRangesDefault(); + auto glyph_ranges_jp = io.Fonts->GetGlyphRangesJapanese(); + FontData latin(glyph_ranges_default, {{"normal", notosans_regular_compressed_data_base85, 13}, {"title", notosans_thin_compressed_data_base85, 32}}); + FontData jp(glyph_ranges_jp, {{"normal", notosansjp_regular_compressed_data_base85, 13}, {"title", notosansjp_thin_compressed_data_base85, 32}}); + std::map font_map; + if (font_type == jp_font) { + font_map = add_font(jp, scale); + } else { + font_map = add_font(latin, scale); + } + title = font_map["title"]; + io.FontDefault = font_map["normal"]; + ImGui_ImplSDLRenderer2_CreateFontsTexture(); } #ifdef __EMSCRIPTEN__ static EM_BOOL resize_callback(int event_type, const EmscriptenUiEvent *event, void *userdata) { @@ -244,56 +295,24 @@ int RendererBackend::Run() { #endif Theme::prefPath = prefPath; - // Decide GL+GLSL versions -#if defined(IMGUI_IMPL_OPENGL_ES2) - // GL ES 2.0 + GLSL 100 - const char* glsl_version = "#version 100"; - SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_EGL, 2); -#elif defined(__APPLE__) - // GL 3.2 Core + GLSL 150 - const char* glsl_version = "#version 150"; - SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); // Always required on Mac - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); -#else - // GL 3.0 + GLSL 130 - const char* glsl_version = "#version 130"; - SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); -#endif - // From 2.0.18: Enable native IME. #ifdef SDL_HINT_IME_SHOW_UI SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1"); #endif - // Create window with graphics context - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); - SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); - SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); - SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN); - window = SDL_CreateWindow(NAME, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, window_width, window_height, window_flags); + SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN); + SDL_CreateWindowAndRenderer(window_width, window_height, window_flags, &window, &rend); + +#ifndef __ANDROID__ SDL_SetWindowMinimumSize(window, window_width, window_height); if (enable_kms) { SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); } +#endif SDL_EventState(SDL_DROPFILE, SDL_ENABLE); const vector icon_data = DecodeBase85(icon_compressed_data_base85); SDL_Surface* icon = IMG_Load_RW(SDL_RWFromConstMem(icon_data.data(), icon_data.size()), 1); SDL_SetWindowIcon(window, icon); - gl_context = SDL_GL_CreateContext(window); - SDL_GL_MakeCurrent(window, gl_context); -#ifdef __ANDROID__ - vsync = true; - SDL_GL_SetSwapInterval(1); -#endif // Setup Dear ImGui context IMGUI_CHECKVERSION(); @@ -302,6 +321,9 @@ int RendererBackend::Run() { io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + if (SDL_GetNumTouchDevices() > 0) { + io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen; + } //io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; io.IniFilename = strdup((std::string(prefPath) + "imgui.ini").c_str()); if (enable_kms) { @@ -313,8 +335,8 @@ int RendererBackend::Run() { //ImGui::StyleColorsLight(); // Setup Platform/Renderer backends - ImGui_ImplSDL2_InitForOpenGL(window, gl_context); - ImGui_ImplOpenGL3_Init(glsl_version); + ImGui_ImplSDL2_InitForSDLRenderer(window, rend); + ImGui_ImplSDLRenderer2_Init(rend); // Load Fonts // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them. // - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple. @@ -345,9 +367,7 @@ int RendererBackend::Run() { #endif ); #endif -#ifndef __EMSCRIPTEN__ - SDL_GL_SetSwapInterval(vsync ? 1 : 0); -#endif + SDL_RenderSetVSync(rend, vsync ? 1 : 0); theme->Apply(accent_color); Init(); SDL_ShowWindow(window); diff --git a/backends/ui/imgui/RendererBackend.h b/backends/ui/imgui/RendererBackend.h index 3c4bbaa..4ef0e88 100644 --- a/backends/ui/imgui/RendererBackend.h +++ b/backends/ui/imgui/RendererBackend.h @@ -19,13 +19,16 @@ static const char* NAME = "Looper"; class RendererBackend { void BackendDeinit(); void LoopFunction(); - SDL_GLContext gl_context; + //SDL_GLContext gl_context; bool resize_needed = true; void on_resize(); public: + std::optional touchScreenModeOverride; + bool isTouchScreenMode(); static void resize_static(); double scale = 1.0; SDL_Window *window; + SDL_Renderer *rend; int window_width = 475; int window_height = 354; bool done = false; diff --git a/backends/ui/imgui/file_browser.cpp b/backends/ui/imgui/file_browser.cpp index 72f2394..e736416 100644 --- a/backends/ui/imgui/file_browser.cpp +++ b/backends/ui/imgui/file_browser.cpp @@ -5,7 +5,39 @@ #include #include #ifdef __ANDROID__ -#include +#include +jclass MainActivity; +jobject mainActivity; +jmethodID GetUserDir_Method; +jmethodID OpenFilePicker_Method; +jmethodID GetPickedFile_Method; +jmethodID ClearSelected_Method; +jmethodID IsLoading_Method; +JNIEnv *env; +bool IsLoading() { + return env->CallStaticBooleanMethod(MainActivity, IsLoading_Method, 0); +} +const char *GetUserDir() { + jstring str = (jstring)env->CallStaticObjectMethod(MainActivity, GetUserDir_Method, 0);; + jboolean tmp = true; + return env->GetStringUTFChars(str, &tmp); +} +void OpenFilePicker(const char *pwd) { + if (pwd == nullptr) { + jstring string = env->NewStringUTF(pwd); + env->CallStaticVoidMethod(MainActivity, OpenFilePicker_Method, string); + } else { + env->CallStaticVoidMethod(MainActivity, OpenFilePicker_Method, nullptr); + } +} +const char *GetPickedFile() { + jstring str = (jstring)env->CallStaticObjectMethod(MainActivity, GetPickedFile_Method, 0); + jboolean tmp = true; + return env->GetStringUTFChars(str, &tmp); +} +void ClearSelected() { + env->CallStaticVoidMethod(MainActivity, ClearSelected_Method, 0); +} #endif #ifdef __EMSCRIPTEN__ extern "C" { @@ -31,9 +63,6 @@ FileBrowser::FileBrowser(bool save, ImGuiFileBrowserFlags extra_fallback_flags) this->save = save; this->flags = (save ? ImGuiFileBrowserFlags_CreateNewDir|ImGuiFileBrowserFlags_EnterNewFilename : 0) | extra_fallback_flags; fallback = ImGui::FileBrowser(this->flags); - #ifdef __ANDROID__ - fallback.SetPwd(SDL_AndroidGetExternalStoragePath()); - #endif } void FileBrowser::SetTypeFilters(string name, vector filters) { @@ -75,13 +104,15 @@ void FileBrowser::SetTypeFilters(string name, vector filters) { } void FileBrowser::SetPwd(path path) { pwd = path; - #ifndef PORTALS + #if !(defined(PORTALS) || defined(__ANDROID__)) fallback.SetPwd(path); #endif } bool FileBrowser::HasSelected() { #ifdef PORTALS return selected.has_value(); + #elif defined(__ANDROID__) + return strlen(GetPickedFile()) > 0; #elif defined(__EMSCRIPTEN__) return file_picker_confirmed(); #else @@ -91,6 +122,13 @@ bool FileBrowser::HasSelected() { path FileBrowser::GetSelected() { #ifdef PORTALS return selected.value_or(path()); + #elif defined(__ANDROID__) + const char *file = GetPickedFile(); + if (strlen(file) > 0) { + return std::string(file); + } else { + return {}; + } #elif defined(__EMSCRIPTEN__) if (HasSelected()) { const char *c_file = get_first_file(); @@ -120,6 +158,10 @@ void FileBrowser::Open() { } else { xdp_portal_open_file(portal, NULL, title.c_str(), variant, NULL, NULL, XDP_OPEN_FILE_FLAG_NONE, NULL, &FileBrowser::FileBrowserOpenCallback, this); } + #elif defined(__ANDROID__) + ClearSelected(); + open = true; + ::OpenFilePicker(nullptr); #elif defined(__EMSCRIPTEN__) open_filepicker(); #else @@ -201,6 +243,27 @@ void FileBrowser::FileBrowserSaveCallback(GObject *src, GAsyncResult *res, gpoin void FileBrowser::Display() { #ifdef PORTALS g_main_context_iteration(main_context, false); + #elif defined(__ANDROID__) + if (HasSelected()) { + open = false; + } + if (IsLoading()) { + ImVec2 pos(0, 0); + ImVec2 size = ImGui::GetMainViewport()->Size; + ImGui::SetNextWindowPos(pos); + ImGui::SetNextWindowSize(size); + ImGui::Begin("Loading...", NULL, ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_Modal|ImGuiWindowFlags_NoCollapse|ImGuiWindowFlags_NoTitleBar); + ImVec2 textSize = ImGui::CalcTextSize("Loading..."); + ImVec2 textPos = size; + textPos.x -= textSize.x; + textPos.y -= textSize.y; + textPos.x /= 2; + textPos.y /= 2; + ImGui::SetCursorPos(textPos); + ImGui::TextUnformatted("Loading..."); + ImGui::End(); + + } #elif defined(__EMSCRIPTEN__) if (file_picker_visible() || file_picker_loading()) { if((flags & ImGuiFileBrowserFlags_NoModal)) @@ -252,6 +315,9 @@ void FileBrowser::ClearSelected() { #ifdef __EMSCRIPTEN__ clear_file_selection(); #endif + #ifdef __ANDROID__ + ::ClearSelected(); + #endif #ifndef PORTALS fallback.ClearSelected(); #endif @@ -265,6 +331,8 @@ void FileBrowser::SetTitle(string title) { bool FileBrowser::IsOpened() { #ifdef PORTALS return open; + #elif defined(__ANDROID__) + return open; #elif defined(__EMSCRIPTEN__) return !file_picker_closed() || file_picker_confirmed(); #else @@ -284,4 +352,4 @@ FileBrowser::~FileBrowser() { } g_main_loop_quit(main_loop); #endif -} \ No newline at end of file +} diff --git a/backends/ui/imgui/main.cpp b/backends/ui/imgui/main.cpp index 7f378f2..8690b83 100644 --- a/backends/ui/imgui/main.cpp +++ b/backends/ui/imgui/main.cpp @@ -12,7 +12,7 @@ extern "C" { extern void enable_puter(bool enable); } #endif - +using namespace Looper::Options; void MainLoop::Init() { #ifdef PORTALS g_set_application_name("Looper"); @@ -35,15 +35,12 @@ void MainLoop::Init() { { Json::Value config; std::ifstream stream; - stream.open(path(prefPath) / "config.json"); + path jsonConfigPath = path(prefPath) / "config.json"; + stream.open(jsonConfigPath); if (stream.is_open()) { stream >> config; if (config.isMember("theme_name")) { - path themePath = theme->themeDir / config["theme_name"].asString(); - if (exists(themePath)) { - delete theme; - theme = new Theme(themePath); - } + init_option("ui.imgui.theme", config["theme_name"].asString()); } if (config.isMember("accent_color")) { if (config["accent_color"].isNumeric()) { @@ -52,27 +49,52 @@ void MainLoop::Init() { Json::Value accentColor = config["accent_color"]; accent_color = ImVec4(accentColor["h"].asFloat(), accentColor["s"].asFloat(), accentColor["v"].asFloat(), accentColor["a"].asFloat()); } + toml::table accent_color_table; + accent_color_table.insert("h", accent_color.x); + accent_color_table.insert("s", accent_color.y); + accent_color_table.insert("v", accent_color.z); + accent_color_table.insert("a", accent_color.w); + init_option("ui.imgui.accent_color", accent_color_table); } if (config.isMember("demo_window")) { - show_demo_window = config["demo_window"].asBool(); + init_option("ui.imgui.demo_window", config["demo_window"].asBool()); } if (config.isMember("vsync")) { - vsync = config["vsync"].asBool(); + init_option("ui.imgui.vsync", config["vsync"].asBool()); } if (config.isMember("framerate")) { - framerate = config["framerate"].asUInt(); + init_option("ui.imgui.framerate", (int64_t)config["framerate"].asUInt()); } if (config.isMember("lang")) { Json::Value langValue; - if (langValue.isNull()) { - lang = DEFAULT_LANG; - } else { - lang = config["lang"].asString(); + if (!langValue.isNull()) { + init_option("ui.imgui.lang", config["lang"].asString()); } - SET_LANG(lang.c_str()); } stream.close(); - } + std::remove(jsonConfigPath.c_str()); + } + { + std::string themeName = get_option("ui.imgui.theme", "light"); + path themePath = Theme::themeDir / path(themeName + ".toml"); + if (exists(themePath)) { + delete theme; + theme = new Theme(themePath); + } + if (option_set("ui.imgui.lang")) { + lang = get_option("ui.imgui.lang"); + } else { + lang = DEFAULT_LANG; + } + SET_LANG(lang.c_str()); + show_demo_window = get_option("ui.imgui.demo_window", false); + vsync = get_option("ui.imgui.vsync", true); + framerate = (unsigned)get_option("ui.imgui.framerate", 60); + accent_color.x = (float)get_option("ui.imgui.accent_color.h", accent_color.x); + accent_color.y = (float)get_option("ui.imgui.accent_color.s", accent_color.y); + accent_color.z = (float)get_option("ui.imgui.accent_color.v", accent_color.z); + accent_color.w = (float)get_option("ui.imgui.accent_color.a", accent_color.w); + } if (is_empty(Theme::themeDir)) { path lightPath = Theme::themeDir / "light.toml"; path darkPath = Theme::themeDir / "dark.toml"; @@ -114,7 +136,9 @@ void MainLoop::FileLoaded() { streams = playback->get_streams(); } void MainLoop::GuiFunction() { +#if defined(__EMSCRIPTEN__)||defined(__ANDROID__) playback->LoopHook(); +#endif position = playback->GetPosition(); length = playback->GetLength(); // Set the window title if the file changed, or playback stopped. @@ -278,9 +302,7 @@ void MainLoop::GuiFunction() { } ImGui::EndCombo(); } - if (ImGui::Checkbox(_TR_CTX("Preference | VSync checkbox", "Enable VSync"), &vsync)) { - SDL_GL_SetSwapInterval(vsync ? 1 : 0); - } + ImGui::Checkbox(_TR_CTX("Preference | VSync checkbox", "Enable VSync"), &vsync); ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetWindowWidth() - ImGui::GetCursorPosX() - ImGui::GetStyle().WindowPadding.x); ImGui::SliderInt("##Framerate", &framerate, 10, 480, _TR_CTX("Preferences | Framerate slider", "Max framerate without VSync: %d")); @@ -309,6 +331,21 @@ void MainLoop::GuiFunction() { SET_LANG(lang.c_str()); } } + bool overrideTouchscreenMode = touchScreenModeOverride.has_value(); + if (ImGui::Checkbox(_TR_CTX("Preference | override enable checkbox", "Override touchscreen mode"), &overrideTouchscreenMode)) { + if (overrideTouchscreenMode) { + touchScreenModeOverride = isTouchScreenMode(); + } else { + touchScreenModeOverride = {}; + } + } + if (overrideTouchscreenMode) { + bool touchScreenMode = isTouchScreenMode(); + if (ImGui::Checkbox(_TRI_CTX(ICON_FK_HAND_O_UP, "Preference | Checkbox (Only shown when override enabled)", "Enable touch screen mode."), &touchScreenMode)) { + touchScreenModeOverride = touchScreenMode; + } + } + static string filter = ""; ImGui::TextUnformatted(_TR_CTX("Preference | Theme selector | Filter label", "Filter:")); ImGui::SameLine(); ImGui::SetNextItemWidth(ImGui::GetWindowWidth() - ImGui::GetCursorPosX() - ImGui::GetStyle().WindowPadding.x); @@ -429,32 +466,23 @@ void MainLoop::LoadFile(std::string file) { void MainLoop::Deinit() { { - Json::Value config; - std::ofstream stream; - stream.open(path(prefPath) / "config.json"); path themePath(theme->file_path); themePath = themePath.filename(); if (!themePath.empty()) { - config["theme_name"] = themePath.filename().string(); + set_option("ui.imgui.theme", themePath.filename().string()); } - { - Json::Value accentColor; - accentColor["h"] = accent_color.x; - accentColor["s"] = accent_color.y; - accentColor["v"] = accent_color.z; - accentColor["a"] = accent_color.w; - config["accent_color"] = accentColor; - } - config["demo_window"] = show_demo_window; - config["vsync"] = vsync; - config["framerate"] = framerate; + set_option("ui.imgui.accent_color.h", accent_color.x); + set_option("ui.imgui.accent_color.s", accent_color.y); + set_option("ui.imgui.accent_color.v", accent_color.z); + set_option("ui.imgui.accent_color.a", accent_color.w); + set_option("ui.imgui.demo_window", show_demo_window); + set_option("ui.imgui.vsync", vsync); + set_option("ui.imgui.framerate", framerate); if (lang == DEFAULT_LANG) { - config["lang"] = Json::Value::nullSingleton(); + delete_option("ui.imgui.lang"); } else { - config["lang"] = lang; + set_option("ui.imgui.lang", lang); } - stream << config; - stream.close(); } } MainLoop::MainLoop() : RendererBackend() { diff --git a/build-android.sh b/build-android.sh index 8b9ab2b..8816393 100755 --- a/build-android.sh +++ b/build-android.sh @@ -1,9 +1,11 @@ -#!/bin/sh +#!/bin/env -S NOT_SOURCED=1 /bin/sh +if ! [ "$NOT_SOURCED" = "1" ]; then + echo "Error: This script must not be sourced!" >&2 + return 1 +fi pushd "$(dirname "$0")" +./setup-android-project.sh cd sdl-android-project ln -fs "$(dirname "$(pwd)")" ./app/jni -#android update project --name looper --path . --target "$ANDROID_PLATFORM" ./gradlew build popd - -#cp bin/*.so ../andr diff --git a/dbus.cpp b/dbus.cpp index ec5e880..de70ca9 100644 --- a/dbus.cpp +++ b/dbus.cpp @@ -159,8 +159,8 @@ DBusAPI::DBusAPI(Playback *playback, sdbus::IConnection &connection, std::string connection.enterEventLoopAsync(); } #endif -const char *DBusAPI::objectPath = "/com/complecwaft/Looper"; -const char *DBusAPI::busName = "com.complecwaft.Looper"; +const char *DBusAPI::objectPath = "/com/complecwaft/looper"; +const char *DBusAPI::busName = "com.complecwaft.looper"; DBusAPI *DBusAPI::Create(Playback *playback, bool daemon) { #ifdef DBUS_ENABLED auto connection = sdbus::createSessionBusConnection(busName); diff --git a/dbus.hpp b/dbus.hpp index a5ed14e..6d70a94 100644 --- a/dbus.hpp +++ b/dbus.hpp @@ -105,7 +105,7 @@ class MprisAPI : public sdbus::AdaptorInterfaces + : public sdbus::AdaptorInterfaces #endif { std::map handles; @@ -189,7 +189,7 @@ class DBusAPI }; class DBusAPISender : public Playback #ifdef DBUS_ENABLED -, public sdbus::ProxyInterfaces +, public sdbus::ProxyInterfaces #endif { // Cache diff --git a/log.cpp b/log.cpp index bee3875..fddac4e 100644 --- a/log.cpp +++ b/log.cpp @@ -1,6 +1,9 @@ #include "log.hpp" #include #include +#ifdef __ANDROID__ +#include +#endif namespace Looper::Log { std::set LogStream::global_outputs; int LogStream::log_level = 0; @@ -14,6 +17,7 @@ namespace Looper::Log { } return used_outputs; } + std::string line; void LogStream::writec(const char chr) { bool is_newline = (chr == '\n' || chr == '\r'); if (my_log_level < log_level) { @@ -25,6 +29,15 @@ namespace Looper::Log { stream->writec(chr); } } else { +#ifdef __ANDROID__ + if (!is_newline) { + line += chr; + } else { + for (auto logPriority : android_outputs) { + __android_log_print(logPriority, "Looper", "%s", line.c_str()); + } + } +#endif std::set used_outputs = get_used_outputs(); for (auto &file : used_outputs) { fwrite(&chr, 1, 1, file); @@ -104,7 +117,7 @@ namespace Looper::Log { vwritefln(fmt, args); va_end(args); } - LogStream::LogStream(std::initializer_list names, int log_level, bool nested) + LogStream::LogStream(std::initializer_list names, int log_level, bool nested, void *discriminator) : names(names), need_prefix(true), my_log_level(log_level), @@ -113,24 +126,52 @@ namespace Looper::Log { } LogStream::LogStream(std::initializer_list names, std::initializer_list streams, int log_level) - : LogStream(names, log_level, true) + : LogStream(names, log_level, true, nullptr) { this->streams = std::set(streams); } - LogStream::LogStream(std::initializer_list names, std::initializer_list outputs, int log_level) - : LogStream(names, log_level, false) +#ifdef __ANDROID__ + LogStream::LogStream(std::initializer_list names, std::initializer_list> outputs, int log_level) +#else + LogStream::LogStream(std::initializer_list names, std::initializer_list outputs, int log_level) +#endif + : LogStream(names, log_level, false, nullptr) { +#ifdef __ANDROID__ + std::set file_outputs; + std::set android_outputs; + for (auto output : outputs) { + android_LogPriority *logPriority = std::get_if(&output); + FILE **file = std::get_if(&output); + if (logPriority != nullptr) { + android_outputs.insert(*logPriority); + } else if (file != nullptr){ + file_outputs.insert(*file); + } + } + this->android_outputs = android_outputs; + this->outputs = file_outputs; +#else + this->outputs = std::set(outputs); +#endif } static LogStream *debug_stream; static LogStream *info_stream; static LogStream *warning_stream; static LogStream *error_stream; void init_logging() { +#ifdef __ANDROID__ + debug_stream = new LogStream({"DEBUG"}, {ANDROID_LOG_DEBUG}, -1); + info_stream = new LogStream({"INFO"}, {ANDROID_LOG_INFO}, 0); + warning_stream = new LogStream({"WARNING"}, {ANDROID_LOG_WARN}, 1); + error_stream = new LogStream({"ERROR"}, {ANDROID_LOG_ERROR}, 2); +#else debug_stream = new LogStream({"DEBUG"}, {stderr}, -1); info_stream = new LogStream({"INFO"}, {stdout}, 0); warning_stream = new LogStream({"WARNING"}, {stderr}, 1); error_stream = new LogStream({"ERROR"}, {stderr}, 2); +#endif } LogStream &get_log_stream_by_level(int level) { switch (level) { diff --git a/log.hpp b/log.hpp index b9afcbe..954255a 100644 --- a/log.hpp +++ b/log.hpp @@ -4,6 +4,10 @@ #include #include #include +#include +#ifdef __ANDROID__ +#include +#endif namespace Looper::Log { struct LogStream { std::set outputs; @@ -14,7 +18,13 @@ namespace Looper::Log { bool need_prefix; std::vector names; std::set get_used_outputs(); - LogStream(std::initializer_list names, int log_level, bool nested); + +#ifdef __ANDROID__ + std::string line; + std::set android_outputs; +#endif + LogStream(std::initializer_list names, int log_level, bool nested, void* discriminator); + public: static int log_level; void writeprefix(); @@ -30,7 +40,12 @@ namespace Looper::Log { void vwritefln(const char *fmt, va_list args); void writefln(const char *fmt, ...); LogStream(std::initializer_list names, std::initializer_list streams, int log_level = 0); + +#ifdef __ANDROID__ + LogStream(std::initializer_list names, std::initializer_list> outputs, int log_level = 0); +#else LogStream(std::initializer_list names, std::initializer_list outputs, int log_level = 0); +#endif }; void init_logging(); LogStream &get_log_stream_by_level(int level); diff --git a/looper b/looper deleted file mode 120000 index e3f85de..0000000 --- a/looper +++ /dev/null @@ -1 +0,0 @@ -/home/catmeow/looper \ No newline at end of file diff --git a/main.cpp b/main.cpp index 3adc381..8d4f343 100644 --- a/main.cpp +++ b/main.cpp @@ -19,7 +19,36 @@ std::unordered_set license_data; std::unordered_set &get_license_data() { return license_data; } +#ifdef __ANDROID__ +#include +#include +extern jclass MainActivity; +extern jobject mainActivity; +extern jmethodID GetUserDir_Method; +extern jmethodID OpenFilePicker_Method; +extern jmethodID GetPickedFile_Method; +extern jmethodID ClearSelected_Method; +extern jmethodID IsLoading_Method; +extern JNIEnv *env; +void initNative() { + MainActivity = reinterpret_cast(env->NewGlobalRef(env->FindClass("com/complecwaft/looper/MainActivity"))); + GetUserDir_Method = env->GetStaticMethodID(MainActivity, "GetUserDir", + "()Ljava/lang/String;"); + OpenFilePicker_Method = env->GetStaticMethodID(MainActivity, "OpenFilePicker", + "(Ljava/lang/Object;)V"); + GetPickedFile_Method = env->GetStaticMethodID(MainActivity, "GetPickedFile", + "()Ljava/lang/String;"); + ClearSelected_Method = env->GetStaticMethodID(MainActivity, "ClearSelected", "()V"); + IsLoading_Method = env->GetStaticMethodID(MainActivity, "IsLoading", "()Z"); + jfieldID singleton = env->GetStaticFieldID(MainActivity, "mSingleton", "Lcom/complecwaft/looper/MainActivity;"); + mainActivity = reinterpret_cast(env->NewGlobalRef(env->GetStaticObjectField(MainActivity, singleton))); +} +#endif int main(int argc, char **argv) { +#ifdef __ANDROID__ + env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + initNative(); +#endif std::vector args; for (int i = 1; i < argc; i++) { args.push_back(std::string(argv[i])); diff --git a/options.hpp b/options.hpp index d56c567..4236e29 100644 --- a/options.hpp +++ b/options.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #define OPTIONS (*Looper::Options::options) namespace Looper::Options { extern toml::table *options; @@ -46,6 +47,37 @@ namespace Looper::Options { return false; } } + inline void delete_option(std::string name) { + DEBUG.writefln("Deleting option '%s'...", name.c_str()); + toml::path path(name); + auto *tmp = &OPTIONS; + std::vector components; + for (auto component : path) { + std::string component_str = (std::string)component; + components.push_back(component_str); + } + while (!path.empty()) { + if (option_set(path.str())) { + toml::path parent_path = path.parent(); + auto &parent = OPTIONS.at(parent_path.str()); + auto last_component = path[path.size() - 1]; + if (parent.is_table()) { + auto &table = *parent.as_table(); + table.erase((std::string)last_component); + if (!table.empty()) { + return; + } + } else if (parent.is_array()) { + auto &array = *parent.as_array(); + array.erase(array.begin() + last_component.index()); + if (!array.empty()) { + return; + } + } + path = parent_path; + } + } + } template void set_option(std::string name, T value) { DEBUG.writefln("Setting option '%s'...", name.c_str()); diff --git a/playback.cpp b/playback.cpp index 78d9b9c..f81c55c 100644 --- a/playback.cpp +++ b/playback.cpp @@ -102,6 +102,9 @@ void PlaybackInstance::SDLCallbackInner(Uint8 *stream, int len) { #endif if (samples > new_samples) { reset_vgmstream(this->stream); + position = 0.0; + } else { + position += samples / this->stream->sample_rate; } samples = new_samples; for (int i = 0; i < new_bufsize / sizeof(SAMPLETYPE); i++) { @@ -115,6 +118,15 @@ void PlaybackInstance::SDLCallbackInner(Uint8 *stream, int len) { } st->receiveSamples((SAMPLETYPE*)stream, len / unit); } +#ifdef __ANDROID__ +oboe::DataCallbackResult PlaybackInstance::onAudioReady( + oboe::AudioStream *audioStream, + void *audioData, + int32_t numFrames) { + SDLCallbackInner((Uint8*)audioData, numFrames * audioStream->getBytesPerFrame()); + return oboe::DataCallbackResult::Continue; +} +#endif void PlaybackInstance::SDLCallback(void *userdata, Uint8 *stream, int len) { ((PlaybackInstance*)userdata)->SDLCallbackInner(stream, len); } @@ -340,6 +352,7 @@ void PlaybackInstance::InitLoopFunction() { desired.userdata = this; st = new SoundTouch(); Mix_Init(MIX_INIT_FLAC|MIX_INIT_MID|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG|MIX_INIT_OPUS|MIX_INIT_WAVPACK); +#ifndef __ANDROID__ if ((device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_CHANNELS_CHANGE|SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE)) == 0) { ERROR.writefln("Error opening audio device: '%s'", SDL_GetError()); set_error("Failed to open audio device!"); @@ -347,6 +360,56 @@ void PlaybackInstance::InitLoopFunction() { loop_started = false; return; } +#else + oboe::AudioStreamBuilder builder; + builder.setDirection(oboe::Direction::Output); + builder.setSharingMode(oboe::SharingMode::Shared); + builder.setPerformanceMode(oboe::PerformanceMode::None); + builder.setFormat( +#ifdef SOUNDTOUCH_INTEGER_SAMPLES + oboe::AudioFormat::I16 +#else + oboe::AudioFormat::Float +#endif + ); + builder.setDataCallback(this); + auto res = builder.openStream(ostream); + if (res != oboe::Result::OK) { + ERROR.writefln("Error opening audio device."); + set_error("Failed to open audio device!"); + running = false; + loop_started = false; + return; + } + obtained = desired; + obtained.channels = ostream->getChannelCount(); + obtained.freq = ostream->getSampleRate(); + auto bufferFrames = ostream->getBufferSizeInFrames(); + auto bytesPerFrame = ostream->getBytesPerFrame(); + auto bytesPerSample = ostream->getBytesPerSample(); + obtained.size = bufferFrames * bytesPerFrame; + obtained.samples = obtained.size / bytesPerSample; + oboe::AudioFormat format = ostream->getFormat(); + DEBUG.writefln("About to start audio stream.\n\ +Format: %s\n\ +Channel count: %u\n\ +Sample rate: %u\n\ +Buffer size (frames): %u\n\ +Bytes per frame: %u\n\ +Total bytes: %u\n\ +Bytes per sample: %u\n\ +Total samples: %u" + , oboe::convertToText(format) + , obtained.channels + , obtained.freq + , bufferFrames + , bytesPerFrame + , obtained.size + , bytesPerSample + , obtained.samples + ); + ostream->requestStart(); +#endif spec = obtained; st->setSampleRate(spec.freq); st->setChannels(spec.channels); @@ -384,6 +447,7 @@ void PlaybackInstance::InitLoopFunction() { } else { playback_ready.store(false); } + load_finished.store(true); set_signal(PlaybackSignalStarted); } void PlaybackInstance::LoopFunction() { @@ -407,34 +471,38 @@ void PlaybackInstance::LoopFunction() { } } if (stream_changed.exchange(false)) { - std::string file = current_file.value(); - if (streams[current_stream].name == "" || streams[current_stream].length <= 0 || current_stream < 0 || current_stream >= streams.size()) { - if (stream != nullptr) { - current_stream = stream->stream_index; + current_file_mutex.lock(); + if (current_file.has_value()) { + std::string file = current_file.value(); + if (current_stream >= streams.size() || current_stream < 0 || + streams[current_stream].name == "" || streams[current_stream].length <= 0) { + if (stream != nullptr) { + current_stream = stream->stream_index; + } else { + current_stream = 0; + } } else { - current_stream = 0; + if (stream != nullptr) { + UnloadVgm(stream); + stream = LoadVgm(file.c_str(), current_stream); + } else if (music != nullptr) { + UnloadMix(music); + music = LoadMix(file.c_str()); + } } - } else { - if (stream != nullptr) { - UnloadVgm(stream); - stream = LoadVgm(file.c_str(), current_stream); - } else if (music != nullptr) { - UnloadMix(music); - music = LoadMix(file.c_str()); + if (music || stream) { + playback_ready.store(true); + } else { + playback_ready.store(false); } } - if (music || stream) { - playback_ready.store(true); - } else { - playback_ready.store(false); - } + current_file_mutex.unlock(); } if (flag_mutex.try_lock()) { if (seeking.exchange(false)) { if (stream != nullptr) { SDL_LockAudioDevice(device); seek_vgmstream(stream, (int32_t)((double)stream->sample_rate * position)); - st->flush(); SDL_UnlockAudioDevice(device); } else { @@ -493,13 +561,7 @@ void PlaybackInstance::LoopFunction() { } flag_mutex.unlock(); } - if (stream != nullptr) { - double maybe_new_position = (double)stream->current_sample / stream->sample_rate; - if (position > maybe_new_position) { - position = maybe_new_position; - } - position += 0.02 * (speed * tempo); - } else if (music != nullptr) { + if (music != nullptr) { position = Mix_GetMusicPosition(music); } @@ -513,7 +575,15 @@ void PlaybackInstance::DeinitLoopFunction() { if (stream != nullptr) { UnloadVgm(stream); } +#ifndef __ANDROID__ SDL_CloseAudioDevice(device); +#else + if (ostream && ostream->getState() != oboe::StreamState::Closed) { + ostream->stop(); + ostream->close(); + } + ostream.reset(); +#endif Mix_CloseAudio(); Mix_Quit(); SDL_QuitSubSystem(SDL_INIT_AUDIO); @@ -573,10 +643,11 @@ void PlaybackInstance::Load(std::string filePath) { if (running.exchange(true)) { load_requested.store(true); } else { - #ifdef __EMSCRIPTEN__ + #if defined(__EMSCRIPTEN__)||defined(__ANDROID__) start_loop(); #else thread = std::thread(&PlaybackInstance::ThreadFunc, this); + loop_started = true; #endif } flag_mutex.lock(); @@ -589,7 +660,7 @@ void PlaybackInstance::Load(std::string filePath) { void PlaybackInstance::Start(std::string filePath, int streamIdx) { Load(filePath); while (loop_started && !load_finished.exchange(false)) { - #ifdef __EMSCRIPTEN__ + #if defined(__EMSCRIPTEN__)||defined(__ANDROID__) LoopHook(); #endif std::this_thread::sleep_for(20ms); @@ -644,7 +715,7 @@ bool PlaybackInstance::IsPaused() { void PlaybackInstance::Stop() { if (running.exchange(false)) { - #ifdef __EMSCRIPTEN__ + #if defined(__EMSCRIPTEN__)||defined(__ANDROID__) stop_loop(); #else thread.join(); diff --git a/playback.h b/playback.h index 432e3a1..79fd52f 100644 --- a/playback.h +++ b/playback.h @@ -3,6 +3,9 @@ extern "C" { #include } +#ifdef __ANDROID__ +#include +#endif #include #include #include @@ -212,8 +215,21 @@ class Playback { static Playback *Create(bool *daemon_found, bool daemon = false); }; class DBusAPISender; -class PlaybackInstance : public Playback { +class PlaybackInstance : public Playback +#ifdef __ANDROID__ +, public oboe::AudioStreamDataCallback +#endif +{ private: +#ifdef __ANDROID__ + std::shared_ptr ostream; +public: + oboe::DataCallbackResult onAudioReady( + oboe::AudioStream *audioStream, + void *audioData, + int32_t numFrames) override; +private: +#endif std::string filePath; std::atomic_bool running; std::atomic_bool file_changed; diff --git a/sdl-android-project/.idea/.gitignore b/sdl-android-project/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/sdl-android-project/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/sdl-android-project/.idea/compiler.xml b/sdl-android-project/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/sdl-android-project/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/sdl-android-project/.idea/deploymentTargetDropDown.xml b/sdl-android-project/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..5b9f854 --- /dev/null +++ b/sdl-android-project/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sdl-android-project/.idea/gradle.xml b/sdl-android-project/.idea/gradle.xml new file mode 100644 index 0000000..0897082 --- /dev/null +++ b/sdl-android-project/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/sdl-android-project/.idea/inspectionProfiles/Project_Default.xml b/sdl-android-project/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..4120039 --- /dev/null +++ b/sdl-android-project/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/sdl-android-project/.idea/migrations.xml b/sdl-android-project/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/sdl-android-project/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/sdl-android-project/.idea/misc.xml b/sdl-android-project/.idea/misc.xml new file mode 100644 index 0000000..0ad17cb --- /dev/null +++ b/sdl-android-project/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/sdl-android-project/.idea/vcs.xml b/sdl-android-project/.idea/vcs.xml new file mode 100644 index 0000000..281a486 --- /dev/null +++ b/sdl-android-project/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/sdl-android-project/app/build.gradle b/sdl-android-project/app/build.gradle index 8556dd8..bd1fe04 100644 --- a/sdl-android-project/app/build.gradle +++ b/sdl-android-project/app/build.gradle @@ -12,7 +12,7 @@ android { prefab true } if (buildAsApplication) { - namespace "com.complecwaft.Looper" + namespace "com.complecwaft.looper" } compileSdkVersion 34 defaultConfig { @@ -56,7 +56,7 @@ android { variant.outputs.each { output -> def outputFile = output.outputFile if (outputFile != null && outputFile.name.endsWith(".aar")) { - def fileName = "com.complecwaft.Looper.app.aar"; + def fileName = "com.complecwaft.looper.app.aar"; output.outputFile = new File(outputFile.parent, fileName); } } diff --git a/sdl-android-project/app/src/main/AndroidManifest.xml b/sdl-android-project/app/src/main/AndroidManifest.xml index 64f1ede..bbf805c 100644 --- a/sdl-android-project/app/src/main/AndroidManifest.xml +++ b/sdl-android-project/app/src/main/AndroidManifest.xml @@ -3,12 +3,16 @@ com.gamemaker.game --> - + android:installLocation="auto" + package="com.complecwaft.looper"> + + + + + + - + + + diff --git a/sdl-android-project/app/src/main/java/com/complecwaft/Looper/MainActivity.java b/sdl-android-project/app/src/main/java/com/complecwaft/Looper/MainActivity.java deleted file mode 100644 index 72efe83..0000000 --- a/sdl-android-project/app/src/main/java/com/complecwaft/Looper/MainActivity.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.complecwaft.Looper; - -import org.libsdl.app.SDLActivity; - -public class MainActivity extends SDLActivity -{ - private static native void nativeInit(); - static { - nativeInit(); - } - public static String GetUserDir() { - return System.getProperty("user.home"); - } - public static void OpenFilePicker(Object types, Object extraInitialUri) { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - if (types instanceof String) { - intent.setType(types); - } else if (types instanceof Array) { - Array type_arr = (Array)types; - String output; - for (String type : type_arr) { - - } - } - if (extraInitialUri instanceof String) { - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, (String)extraInitialUri); - } - } -} diff --git a/sdl-android-project/app/src/main/java/com/complecwaft/looper/MainActivity.java b/sdl-android-project/app/src/main/java/com/complecwaft/looper/MainActivity.java new file mode 100644 index 0000000..d53741e --- /dev/null +++ b/sdl-android-project/app/src/main/java/com/complecwaft/looper/MainActivity.java @@ -0,0 +1,114 @@ +package com.complecwaft.looper; + +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.util.Log; + +import org.libsdl.app.SDLActivity; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Objects; +import android.media.AudioManager; +public class MainActivity extends SDLActivity +{ + public static MainActivity mSingleton; + @Override + protected void onCreate(Bundle savedInstanceState) { + mSingleton = this; + super.onCreate(savedInstanceState); + } + private static Intent openDocumentIntent = null; + public static String GetUserDir() { + return System.getProperty("user.home"); + } + private static final int PICK_FILE = 0x33; + public static void OpenFilePicker(Object extraInitialUri) { + openDocumentIntent = new Intent(Intent.ACTION_GET_CONTENT); + openDocumentIntent.addCategory(Intent.CATEGORY_OPENABLE); + openDocumentIntent.setType("*/*"); + if (extraInitialUri instanceof String) { + openDocumentIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, (String)extraInitialUri); + } + if (openDocumentIntent.resolveActivity(mSingleton.getPackageManager()) != null) { + mSingleton.startActivityForResult(Intent.createChooser(openDocumentIntent, "Open a file..."), PICK_FILE); + } else { + Log.d("Looper", "Unable to resolve Intent.ACTION_OPEN_DOCUMENT {}"); + } + + } + @Override + public void onActivityResult(int requestCode, int resultCode, Intent resultData) { + if (requestCode == PICK_FILE && resultCode == RESULT_OK) { + Uri uri = null; + if (resultData != null) { + uri = resultData.getData(); + InputStream input = null; + loading = true; + try { + input = getApplication().getContentResolver().openInputStream(uri); + } catch (FileNotFoundException ex) { + ex.printStackTrace(); + PickedFile = null; + + loading = false; + return; + } + File file = new File(getCacheDir(), uri.getLastPathSegment()); + try { + file.getParentFile().mkdirs(); + file.createNewFile(); + FileOutputStream os = null; + os = new FileOutputStream(file); + int len = 0; + int pos = 0; + byte[] buf = new byte[1024*4]; + + while ((len = input.read(buf)) >= 0) { + os.write(buf, 0, len); + pos += len; + } + } catch (IOException e) { + e.printStackTrace(); + PickedFile = null; + loading = false; + return; + } + loading = false; + PickedFile = file.getAbsolutePath(); + } else { + PickedFile = null; + } + } + } + private static boolean loading; + public static boolean IsLoading() { + return loading; + } + private static String PickedFile = null; + public static String GetPickedFile() { + if (openDocumentIntent == null) { + return ""; + } else if (PickedFile == null) { + return ""; + } else { + return PickedFile; + } + } + public static void ClearSelected() { + PickedFile = null; + } +} diff --git a/sdl-android-project/build.gradle b/sdl-android-project/build.gradle index 2c911c6..803ce7f 100644 --- a/sdl-android-project/build.gradle +++ b/sdl-android-project/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.1' + classpath 'com.android.tools.build:gradle:8.3.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/sdl-android-project/gradle/wrapper/gradle-wrapper.properties b/sdl-android-project/gradle/wrapper/gradle-wrapper.properties index 5b9d759..f2308e4 100644 --- a/sdl-android-project/gradle/wrapper/gradle-wrapper.properties +++ b/sdl-android-project/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Nov 11 18:20:34 PST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/sdl-android-project/local.properties b/sdl-android-project/local.properties new file mode 100644 index 0000000..dd4f943 --- /dev/null +++ b/sdl-android-project/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Thu Apr 25 11:32:33 PDT 2024 +sdk.dir=/home/catmeow/Android/Sdk diff --git a/setup-android-project.ps1 b/setup-android-project.ps1 new file mode 100644 index 0000000..0ff136e --- /dev/null +++ b/setup-android-project.ps1 @@ -0,0 +1,13 @@ +function Get-ScriptDirectory { + $scriptpath = $MyInvocation.MyCommand.Path + return Split-Path $scriptpath +} +$scriptdir = Get-ScriptDirectory +Push-Location $scriptdir +$android_project_dir = Join-Path($scriptdir, "sdl-android-project") +$android_app_dir = Join-Path($android_project_dir, "app") +$android_jni_dir = Join-Path($android_app_dir, "jni") +if (test-path $android_jni_dir) { + rm $android_jni_dir +} +New-Item -ItemType Junction -Path $android_jni_dir -Value $scriptdir \ No newline at end of file diff --git a/setup-android-project.sh b/setup-android-project.sh new file mode 100755 index 0000000..246471b --- /dev/null +++ b/setup-android-project.sh @@ -0,0 +1,20 @@ +#!/bin/env -S NOT_SOURCED=1 /bin/sh +if ! [ "$NOT_SOURCED" = "1" ]; then + echo "Error: This script must not be sourced!" >&2 + return 1 +fi +get_abs_filename() { + echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" | sed 's@/\./@/@g' | sed 's@/\.$@@g' +} + +export PROJECT_DIR="$(get_abs_filename $(dirname "$0"))" +export ANDROID_PROJECT_DIR="${PROJECT_DIR}/sdl-android-project" +export ANDROID_APP_DIR="${ANDROID_PROJECT_DIR}/app" +export ANDROID_JNI_DIR="${ANDROID_APP_DIR}/jni" +echo "Project directory: $PROJECT_DIR" +echo "Android project directory: $ANDROID_PROJECT_DIR" +echo "Android JNI symlink: $ANDROID_JNI_DIR -> $PROJECT_DIR" +pushd "${PROJECT_DIR}" +[ -d "$ANDROID_JNI_DIR" ] && rm -rf "$ANDROID_JNI_DIR" +ln -sf "$PROJECT_DIR" "$ANDROID_JNI_DIR" +popd diff --git a/subprojects/oboe b/subprojects/oboe new file mode 160000 index 0000000..86165b8 --- /dev/null +++ b/subprojects/oboe @@ -0,0 +1 @@ +Subproject commit 86165b8249bc22b9ef70b69e20323244b6f08d88