Add android support, and other changes.

This commit is contained in:
Zachary Hall 2024-04-28 12:31:40 -07:00
parent 0d236857b8
commit 6159f52e8a
40 changed files with 815 additions and 260 deletions

3
.gitmodules vendored
View file

@ -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

View file

@ -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++)

View file

@ -13,7 +13,7 @@
<arg type='a{sv}' name='platform_data' direction='in'/>
</method>
</interface>
<interface name="com.complecwaft.Looper">
<interface name="com.complecwaft.looper">
<method name="CreateHandle">
<arg type='s' name='new_handle' direction='out' />
</method>
@ -81,7 +81,7 @@
<property name="IsDaemon" type="b" access="read" />
<property name="StreamIdx" type="u" access="read" />
</interface>
<interface name="com.complecwaft.Looper.Errors" >
<interface name="com.complecwaft.looper.Errors" >
<method name="PopFront">
<arg name="handle" direction="in" type="s" />
<arg name="error" direction="out" type="s" />

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.complecwaft.Looper</id>
<id>com.complecwaft.looper</id>
<developer_name>Catmeow72</developer_name>
<name>Looper</name>

View file

@ -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)

View file

@ -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)

View file

@ -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<ImGuiUIBackend>();
UIBackend::register_backend<GtkBackend>();
}

View file

@ -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")

View file

@ -5,18 +5,23 @@
#include "config.h"
#include <SDL_image.h>
#include <string>
#include <tuple>
#include <initializer_list>
#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 <thread>
#include <translation.hpp>
#include <log.hpp>
#include <options.hpp>
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<std::string, std::pair<const char *, double>> data;
void Init(const ImWchar *ranges_in, std::map<std::string, std::pair<const char *, double>> data_in) {
ranges = ranges_in;
data = data_in;
}
FontData(const ImWchar *ranges, std::initializer_list<std::pair<std::string, std::pair<const char*, double>>> data) {
std::map<std::string, std::pair<const char*, double>> out_data;
for (auto pair : data) {
out_data[pair.first] = pair.second;
}
Init(ranges, out_data);
}
FontData(const ImWchar *ranges, std::initializer_list<std::tuple<std::string, const char*, double>> data) {
std::map<std::string, std::pair<const char*, double>> out_data;
for (auto tuple : data) {
std::pair<const char*, double> 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<std::string, std::pair<const char*, double>> 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<FontData> data_vec, int size = 13) {
ImFont* font = nullptr;
std::map<std::string, ImFont *> 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<std::string, ImFont*> 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>({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>({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<std::string>("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<std::string, ImFont*> 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<unsigned char> 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);

View file

@ -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<bool> 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;

View file

@ -5,7 +5,39 @@
#include <stdio.h>
#include <log.hpp>
#ifdef __ANDROID__
#include <SDL.h>
#include <jni.h>
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<string> filters) {
@ -75,13 +104,15 @@ void FileBrowser::SetTypeFilters(string name, vector<string> 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
}
}

View file

@ -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<std::string>("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<toml::table>("ui.imgui.accent_color", accent_color_table);
}
if (config.isMember("demo_window")) {
show_demo_window = config["demo_window"].asBool();
init_option<bool>("ui.imgui.demo_window", config["demo_window"].asBool());
}
if (config.isMember("vsync")) {
vsync = config["vsync"].asBool();
init_option<bool>("ui.imgui.vsync", config["vsync"].asBool());
}
if (config.isMember("framerate")) {
framerate = config["framerate"].asUInt();
init_option<int64_t>("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<std::string>("ui.imgui.lang", config["lang"].asString());
}
SET_LANG(lang.c_str());
}
stream.close();
}
std::remove(jsonConfigPath.c_str());
}
{
std::string themeName = get_option<std::string>("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<std::string>("ui.imgui.lang");
} else {
lang = DEFAULT_LANG;
}
SET_LANG(lang.c_str());
show_demo_window = get_option<bool>("ui.imgui.demo_window", false);
vsync = get_option<bool>("ui.imgui.vsync", true);
framerate = (unsigned)get_option<int64_t>("ui.imgui.framerate", 60);
accent_color.x = (float)get_option<double>("ui.imgui.accent_color.h", accent_color.x);
accent_color.y = (float)get_option<double>("ui.imgui.accent_color.s", accent_color.y);
accent_color.z = (float)get_option<double>("ui.imgui.accent_color.v", accent_color.z);
accent_color.w = (float)get_option<double>("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<std::string>("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<double>("ui.imgui.accent_color.h", accent_color.x);
set_option<double>("ui.imgui.accent_color.s", accent_color.y);
set_option<double>("ui.imgui.accent_color.v", accent_color.z);
set_option<double>("ui.imgui.accent_color.a", accent_color.w);
set_option<double>("ui.imgui.demo_window", show_demo_window);
set_option<bool>("ui.imgui.vsync", vsync);
set_option<int64_t>("ui.imgui.framerate", framerate);
if (lang == DEFAULT_LANG) {
config["lang"] = Json::Value::nullSingleton();
delete_option("ui.imgui.lang");
} else {
config["lang"] = lang;
set_option<std::string>("ui.imgui.lang", lang);
}
stream << config;
stream.close();
}
}
MainLoop::MainLoop() : RendererBackend() {

View file

@ -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

View file

@ -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);

View file

@ -105,7 +105,7 @@ class MprisAPI : public sdbus::AdaptorInterfaces<org::mpris::MediaPlayer2_adapto
#endif
class DBusAPI
#ifdef DBUS_ENABLED
: public sdbus::AdaptorInterfaces<com::complecwaft::Looper_adaptor, com::complecwaft::Looper::Errors_adaptor, org::freedesktop::Application_adaptor>
: public sdbus::AdaptorInterfaces<com::complecwaft::looper_adaptor, com::complecwaft::looper::Errors_adaptor, org::freedesktop::Application_adaptor>
#endif
{
std::map<std::string, void*> handles;
@ -189,7 +189,7 @@ class DBusAPI
};
class DBusAPISender : public Playback
#ifdef DBUS_ENABLED
, public sdbus::ProxyInterfaces<com::complecwaft::Looper_proxy, com::complecwaft::Looper::Errors_proxy, org::freedesktop::Application_proxy, sdbus::Peer_proxy>
, public sdbus::ProxyInterfaces<com::complecwaft::looper_proxy, com::complecwaft::looper::Errors_proxy, org::freedesktop::Application_proxy, sdbus::Peer_proxy>
#endif
{
// Cache

49
log.cpp
View file

@ -1,6 +1,9 @@
#include "log.hpp"
#include <stdarg.h>
#include <string.h>
#ifdef __ANDROID__
#include <android/log.h>
#endif
namespace Looper::Log {
std::set<FILE*> 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<FILE*> 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<std::string> names, int log_level, bool nested)
LogStream::LogStream(std::initializer_list<std::string> 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<std::string> names, std::initializer_list<LogStream*> 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<std::string> names, std::initializer_list<FILE*> outputs, int log_level)
: LogStream(names, log_level, false)
#ifdef __ANDROID__
LogStream::LogStream(std::initializer_list<std::string> names, std::initializer_list<std::variant<FILE*, android_LogPriority>> outputs, int log_level)
#else
LogStream::LogStream(std::initializer_list<std::string> names, std::initializer_list<FILE*> outputs, int log_level)
#endif
: LogStream(names, log_level, false, nullptr)
{
#ifdef __ANDROID__
std::set<FILE*> file_outputs;
std::set<android_LogPriority> android_outputs;
for (auto output : outputs) {
android_LogPriority *logPriority = std::get_if<android_LogPriority>(&output);
FILE **file = std::get_if<FILE*>(&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) {

17
log.hpp
View file

@ -4,6 +4,10 @@
#include <ostream>
#include <set>
#include <vector>
#include <variant>
#ifdef __ANDROID__
#include <android/log.h>
#endif
namespace Looper::Log {
struct LogStream {
std::set<FILE *> outputs;
@ -14,7 +18,13 @@ namespace Looper::Log {
bool need_prefix;
std::vector<std::string> names;
std::set<FILE*> get_used_outputs();
LogStream(std::initializer_list<std::string> names, int log_level, bool nested);
#ifdef __ANDROID__
std::string line;
std::set<android_LogPriority> android_outputs;
#endif
LogStream(std::initializer_list<std::string> 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<std::string> names, std::initializer_list<LogStream*> streams, int log_level = 0);
#ifdef __ANDROID__
LogStream(std::initializer_list<std::string> names, std::initializer_list<std::variant<FILE*, android_LogPriority>> outputs, int log_level = 0);
#else
LogStream(std::initializer_list<std::string> names, std::initializer_list<FILE*> outputs, int log_level = 0);
#endif
};
void init_logging();
LogStream &get_log_stream_by_level(int level);

1
looper
View file

@ -1 +0,0 @@
/home/catmeow/looper

View file

@ -19,7 +19,36 @@ std::unordered_set<LicenseData> license_data;
std::unordered_set<LicenseData> &get_license_data() {
return license_data;
}
#ifdef __ANDROID__
#include <SDL.h>
#include <jni.h>
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<jclass>(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<jobject>(env->NewGlobalRef(env->GetStaticObjectField(MainActivity, singleton)));
}
#endif
int main(int argc, char **argv) {
#ifdef __ANDROID__
env = (JNIEnv*)SDL_AndroidGetJNIEnv();
initNative();
#endif
std::vector<std::string> args;
for (int i = 1; i < argc; i++) {
args.push_back(std::string(argv[i]));

View file

@ -5,6 +5,7 @@
#include <vector>
#include <typeinfo>
#include <typeindex>
#include <optional>
#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<std::string> 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<class T>
void set_option(std::string name, T value) {
DEBUG.writefln("Setting option '%s'...", name.c_str());

View file

@ -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();

View file

@ -3,6 +3,9 @@
extern "C" {
#include <vgmstream.h>
}
#ifdef __ANDROID__
#include <oboe/Oboe.h>
#endif
#include <thread>
#include <SDL.h>
#include <SDL_audio.h>
@ -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<oboe::AudioStream> 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;

3
sdl-android-project/.idea/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State>
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Copy_of_Pixel_3a_API_34_extension_level_7_x86_64.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2024-04-28T16:05:58.159965481Z" />
</State>
</entry>
</value>
</component>
</project>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="JniFindClass" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$/app/jni" vcs="Git" />
</component>
</project>

View file

@ -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);
}
}

View file

@ -3,12 +3,16 @@
com.gamemaker.game
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0"
android:installLocation="auto">
android:installLocation="auto"
package="com.complecwaft.looper">
<queries>
<intent>
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<category android:name="android.intent.category.OPENABLE" />
</intent>
</queries>
<!-- OpenGL ES 2.0 -->
<uses-feature android:glEsVersion="0x00020000" />
<uses-feature android:glEsVersion="0x00030000" />
<!-- Touchscreen support -->
<uses-feature
@ -39,6 +43,8 @@
<!-- Allow downloading to the external storage on Android 5.1 and older -->
<!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="22" /> -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="22" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- Allow access to Bluetooth devices -->
<!-- Currently this is just for Steam Controller support and requires setting SDL_HINT_JOYSTICK_HIDAPI_STEAM -->

View file

@ -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<String>) {
Array<String> type_arr = (Array<String>)types;
String output;
for (String type : type_arr) {
}
}
if (extraInitialUri instanceof String) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, (String)extraInitialUri);
}
}
}

View file

@ -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;
}
}

View file

@ -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

View file

@ -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

View file

@ -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

13
setup-android-project.ps1 Normal file
View file

@ -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

20
setup-android-project.sh Executable file
View file

@ -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

1
subprojects/oboe Submodule

@ -0,0 +1 @@
Subproject commit 86165b8249bc22b9ef70b69e20323244b6f08d88