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