diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml
new file mode 100644
index 0000000..7d60e7b
--- /dev/null
+++ b/.idea/caches/deviceStreaming.xml
@@ -0,0 +1,1186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b86273d
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/editor.xml b/.idea/editor.xml
new file mode 100644
index 0000000..fef3201
--- /dev/null
+++ b/.idea/editor.xml
@@ -0,0 +1,249 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 2996d53..639c779 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -1,14 +1,18 @@
+
-
-
-
-
+
-
+
+
+
+
+
+
+
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 37a7509..2731899 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,9 @@
-
-
+
+
+
+
+
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
index 7f68460..72f00ed 100644
--- a/.idea/runConfigurations.xml
+++ b/.idea/runConfigurations.xml
@@ -3,6 +3,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/build.gradle b/app/build.gradle
index 6c98870..91de39a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,44 +1,67 @@
-apply plugin: 'com.android.application'
+plugins {
+ id 'com.android.application'
+}
android {
- compileSdkVersion 22
+ namespace 'com.github.projectm_android'
+ compileSdk 35
+
defaultConfig {
applicationId "com.github.projectm_android"
- minSdkVersion 22
- targetSdkVersion 22
+ minSdk 21
+ targetSdk 35
+
versionCode 1
versionName "1.0"
+
+ ndk {
+ abiFilters 'x86_64', 'arm64-v8a', 'armeabi-v7a'
+ }
+
externalNativeBuild {
cmake {
- cppFlags "-std=c++11"
- arguments "-DANDROID_STL=c++_shared"
+ cppFlags "-std=c++17 -fexceptions -frtti"
+ // optional: use local projectM sources
+ // arguments "-DPROJECTM_SOURCE_DIR=/my/local/projectm"
}
}
- ndk {
- abiFilters "armeabi-v7a"
- }
}
+
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
+ debug {
+ debuggable true
+ }
}
+
externalNativeBuild {
cmake {
- path "src/main/cpp/CMakeLists.txt"
+ path file('src/main/cpp/CMakeLists.txt')
}
}
- sourceSets {
- main {
- jniLibs.srcDirs 'jniLibs', 'jniLibs/armeabi-v7a'
- }
+
+ buildFeatures {
+ viewBinding true
}
- productFlavors {
+
+ packaging {
+ jniLibs {
+ useLegacyPackaging = true
+ }
+ resources {
+ excludes += ['/META-INF/{AL2.0,LGPL2.1}']
+ }
}
}
+
dependencies {
- implementation fileTree(include: ['*.jar'], dir: 'libs')
- implementation 'com.android.support:appcompat-v7:22.2.1'
- implementation 'com.android.support.constraint:constraint-layout:1.1.3'
+ // Ensure consistent Kotlin stdlib versions across all transitive deps.
+ implementation platform('org.jetbrains.kotlin:kotlin-bom:1.9.24')
+
+ implementation 'androidx.appcompat:appcompat:1.7.0'
+ implementation 'androidx.core:core-ktx:1.13.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 244feba..ed19c3f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,9 +1,6 @@
-
+
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" android:exported="true">
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
index 7b25830..5867d6d 100644
--- a/app/src/main/cpp/CMakeLists.txt
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -2,22 +2,89 @@
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
+cmake_minimum_required(VERSION 3.22.1)
+project(projectm_android LANGUAGES C CXX)
-cmake_minimum_required(VERSION 3.4.1)
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# Install/staging prefix inside this ABI build directory
+set(PROJECTM_INSTALL_PREFIX "${CMAKE_BINARY_DIR}/projectm-install" CACHE PATH "")
+
+# Force install prefix so projectM installs headers into:
+# ${PROJECTM_INSTALL_PREFIX}/include/projectM-4/...
+set(CMAKE_INSTALL_PREFIX "${PROJECTM_INSTALL_PREFIX}" CACHE PATH "" FORCE)
+
+
+# ---- Fetch OR use local libprojectM (projectM 4.x) ----
+include(FetchContent)
+
+# If set, use local source instead of fetching.
+# Example: -DPROJECTM_SOURCE_DIR=/home/mba/dev/widerup/projectm
+set(PROJECTM_SOURCE_DIR "" CACHE PATH "Path to local projectM source directory (with top-level CMakeLists.txt)")
+
+# Configure projectM build options (mirrors upstream Android workflow).
+set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE)
+set(BUILD_TESTING NO CACHE BOOL "" FORCE)
+set(ENABLE_SDL_UI OFF CACHE BOOL "" FORCE)
+
+if (PROJECTM_SOURCE_DIR AND EXISTS "${PROJECTM_SOURCE_DIR}/CMakeLists.txt")
+ message(STATUS "Using local projectM source: ${PROJECTM_SOURCE_DIR}")
+
+ # FetchContent will treat SOURCE_DIR as the content and won't hit the network.
+ FetchContent_Declare(
+ projectm
+ SOURCE_DIR "${PROJECTM_SOURCE_DIR}"
+ )
+else()
+ message(STATUS "Fetching projectM via Git (PROJECTM_SOURCE_DIR not set or invalid)")
+
+ FetchContent_Declare(
+ projectm
+ GIT_REPOSITORY https://github.com/projectM-visualizer/projectm.git
+ GIT_TAG master
+ )
+endif()
+
+FetchContent_MakeAvailable(projectm)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
-add_library( # Sets the name of the library.
- jniwrapper
+# ---- JNI wrapper ----
+add_library(jniwrapper SHARED
+ jni.cpp
+)
- # Sets the library as a shared library.
- SHARED
+# Always produce libjniwrapper.so (no debug postfix)
+set_target_properties(jniwrapper PROPERTIES
+ OUTPUT_NAME "jniwrapper"
+ DEBUG_POSTFIX ""
+ RELEASE_POSTFIX ""
+ RELWITHDEBINFO_POSTFIX ""
+ MINSIZEREL_POSTFIX ""
+)
+
+# Also guard against a global postfix set by a toolchain / parent project
+set(CMAKE_DEBUG_POSTFIX "" CACHE STRING "" FORCE)
+
+# find projectM headers
+if (DEFINED projectm_SOURCE_DIR)
+ target_include_directories(jniwrapper PRIVATE
+ "${projectm_SOURCE_DIR}/src/playlist/api"
+ )
+endif()
- # Provides a relative path to your source file(s).
- jni.cpp)
+if (DEFINED projectm_BINARY_DIR)
+ target_include_directories(jniwrapper PRIVATE
+ "${projectm_BINARY_DIR}/src/api/include"
+ "${projectm_BINARY_DIR}/src/playlist/include"
+ )
+else()
+ message(FATAL_ERROR "projectm_BINARY_DIR not defined; cannot set build-tree include paths")
+endif()
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
@@ -25,25 +92,23 @@ add_library( # Sets the name of the library.
# you want to add. CMake verifies that the library exists before
# completing its build.
-find_library(
- log-lib
- log)
+# Android + GLES
+find_library(log-lib log)
+find_library(android-lib android)
+find_library(glesv2-lib GLESv2)
+find_library(egl-lib EGL)
-
-add_library(
- projectM
- SHARED
- IMPORTED
+target_include_directories(jniwrapper
+ PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
)
-set_target_properties(
- projectM
- PROPERTIES IMPORTED_LOCATION
- ${CMAKE_CURRENT_SOURCE_DIR}/../../../jniLibs/armeabi-v7a/libprojectM.so)
-
-include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../../../jniLibs/include/ )
-
-target_link_libraries(
- jniwrapper
+target_link_libraries(jniwrapper
+ PRIVATE
${log-lib}
- projectM)
\ No newline at end of file
+ ${android-lib}
+ ${glesv2-lib}
+ ${egl-lib}
+ libprojectM::playlist
+ projectM
+)
diff --git a/app/src/main/cpp/jni.cpp b/app/src/main/cpp/jni.cpp
index fcf78f2..7fd6a89 100644
--- a/app/src/main/cpp/jni.cpp
+++ b/app/src/main/cpp/jni.cpp
@@ -1,102 +1,136 @@
-#include
#include
-#include
-#include
+#include
#include
-#include
-#include "libprojectM/projectM.hpp"
-#include "libprojectM/PCM.hpp"
+#include
+#include
+
+#include
+#include
+
+#define LOG_TAG "projectm-android"
+#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+
+// Global state
+static projectm_handle g_pm = nullptr;
+static projectm_playlist_handle g_playlist = nullptr;
+static std::mutex g_lock;
-#define TAG "ProjectM"
-#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
-#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
-#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
-#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
+// Helper: build a stable C-string array for projectM properties.
+static std::vector g_path_storage;
-projectM *instance = NULL;
+static void set_preset_search_path(const std::string& assetRoot) {
+ // Assets are extracted by ProjectMApplication into /projectM/...
+ // That directory contains: config.inp, fonts/, presets/...
+ const std::string presetsDir = assetRoot + "/presets";
-void next_preset(bool hard_cut) {
- if (!instance) {
- ALOGE("libprojectM not initialized");
+ g_path_storage.clear();
+ g_path_storage.push_back(presetsDir);
+
+ for (auto& s : g_path_storage) {
+ projectm_playlist_add_path(g_playlist, s.c_str(), true, false);
+ ALOGI("Preset search path: %s", presetsDir.c_str());
+ }
+
+}
+
+static void ensure_initialized(int width, int height, const std::string& assetRoot) {
+ if (g_pm) return;
+
+ g_pm = projectm_create();
+ if (!g_pm) {
+ ALOGE("projectm_create() failed (GL context not ready?)");
return;
}
- srand((unsigned) time(NULL));
- int preset_list_size = instance->getPlaylistSize();
- if (preset_list_size <= 0) {
- ALOGE("Could not load any presets");
+
+ projectm_set_window_size(g_pm, width, height);
+
+ // Basic config.
+ projectm_set_aspect_correction(g_pm, true);
+ projectm_set_hard_cut_enabled(g_pm, true);
+ projectm_set_soft_cut_duration(g_pm, 10);
+ projectm_set_hard_cut_duration(g_pm, 10);
+ projectm_set_hard_cut_sensitivity(g_pm, 1.0);
+ projectm_set_beat_sensitivity(g_pm, 0.5);
+ projectm_set_preset_duration(g_pm, 15);
+
+ // Playlist is the easiest way to do preset navigation with projectM 4.
+ g_playlist = projectm_playlist_create(g_pm);
+
+ set_preset_search_path(assetRoot);
+
+ if (g_playlist) {
+ projectm_playlist_set_shuffle(g_playlist, false);
+ projectm_playlist_set_position(g_playlist, 0, true);
+ } else {
+ ALOGE("projectm_playlist_create() failed");
}
- int preset_number = (int)(rand() % (preset_list_size-1));
- ALOGD("Switching to preset %d", preset_number);
- instance->selectPreset(preset_number, hard_cut);
+
+ ALOGI("Initialized projectM (%dx%d)", width, height);
}
-extern "C" JNIEXPORT void JNICALL
+extern "C" {
+
+// Java: libprojectMJNIWrapper.onSurfaceCreated(int w,int h,String assetPath)
+JNIEXPORT void JNICALL
Java_com_github_projectm_1android_libprojectMJNIWrapper_onSurfaceCreated(
- JNIEnv *env,
- jobject obj,
- jint window_width,
- jint window_height,
- jstring jasset_path) {
- if (instance) {
- ALOGD("Destroy existing instance");
- delete instance;
- }
- const char* asset_path_chars = env->GetStringUTFChars(jasset_path, NULL);
- std::string asset_path(asset_path_chars);
- projectM::Settings settings;
- settings.windowHeight = window_height;
- settings.windowWidth = window_width;
- settings.presetURL = asset_path + "/presets";
- settings.smoothPresetDuration = 7;
- settings.presetDuration = 3;
- settings.shuffleEnabled = true;
- ALOGD("presetURL: %s", settings.presetURL.c_str());
- env->ReleaseStringUTFChars(jasset_path, asset_path_chars);
- ALOGD("Creating new instance");
- instance = new projectM(settings);
- srand(time(0));
- next_preset(true);
+ JNIEnv* env, jclass, jint window_width, jint window_height, jstring assetPath) {
+ const char* pathChars = env->GetStringUTFChars(assetPath, nullptr);
+ std::string assetRoot = pathChars ? pathChars : "";
+ env->ReleaseStringUTFChars(assetPath, pathChars);
+
+ std::lock_guard lk(g_lock);
+ ensure_initialized((int)window_width, (int)window_height, assetRoot);
}
-extern "C" JNIEXPORT void JNICALL
+JNIEXPORT void JNICALL
Java_com_github_projectm_1android_libprojectMJNIWrapper_onSurfaceChanged(
- JNIEnv *env,
- jobject obj,
- jint window_width,
- jint window_height) {
- if (!instance) {
- return;
- }
- instance->projectM_resetGL(window_width, window_height);
+ JNIEnv*, jclass, jint width, jint height) {
+ std::lock_guard lk(g_lock);
+ if (!g_pm) return;
+ projectm_set_window_size(g_pm, (int)width, (int)height);
}
-extern "C" JNIEXPORT void JNICALL
+JNIEXPORT void JNICALL
Java_com_github_projectm_1android_libprojectMJNIWrapper_onDrawFrame(
- JNIEnv *env,
- jobject obj) {
- if (!instance) {
- return;
- }
- instance->renderFrame();
+ JNIEnv*, jclass) {
+ std::lock_guard lk(g_lock);
+ if (!g_pm) return;
+
+ // Render one frame into the current GL framebuffer.
+ projectm_opengl_render_frame(g_pm);
}
-extern "C" JNIEXPORT void JNICALL
+JNIEXPORT void JNICALL
Java_com_github_projectm_1android_libprojectMJNIWrapper_addPCM(
- JNIEnv *env,
- jobject obj,
- jshortArray pcm_data,
- jshort nsamples) {
- if (!instance) {
- return;
- }
- jshort *data = env->GetShortArrayElements(pcm_data, NULL);
- instance->pcm()->addPCM16Data(data, nsamples);
- env->ReleaseShortArrayElements(pcm_data, data, 0);
+ JNIEnv* env, jclass, jshortArray pcm_data, jshort nsamples) {
+ std::lock_guard lk(g_lock);
+ if (!g_pm) return;
+
+ jsize len = env->GetArrayLength(pcm_data);
+ if (len <= 0) return;
+
+ // nsamples is passed in as "bufferSize" by AudioThread; treat it as frames.
+ // AudioThread records mono int16 PCM.
+
+ jboolean isCopy = JNI_FALSE;
+ jshort* data = env->GetShortArrayElements(pcm_data, &isCopy);
+ if (!data) return;
+
+ // Guard: never read beyond actual array length.
+ const int frames = (int)std::min(len, (jsize)nsamples);
+
+ projectm_pcm_add_int16(g_pm, reinterpret_cast(data), frames, PROJECTM_MONO);
+
+ env->ReleaseShortArrayElements(pcm_data, data, JNI_ABORT);
}
-extern "C" JNIEXPORT void JNICALL
+JNIEXPORT void JNICALL
Java_com_github_projectm_1android_libprojectMJNIWrapper_nextPreset(
- JNIEnv *env,
- jobject obj) {
- next_preset(true);
+ JNIEnv*, jclass) {
+ std::lock_guard lk(g_lock);
+ if (!g_playlist) return;
+ projectm_playlist_play_next(g_playlist, true);
}
+
+} // extern "C"
diff --git a/app/src/main/java/com/github/projectm_android/MainActivity.java b/app/src/main/java/com/github/projectm_android/MainActivity.java
index 6b7f389..5fe4876 100644
--- a/app/src/main/java/com/github/projectm_android/MainActivity.java
+++ b/app/src/main/java/com/github/projectm_android/MainActivity.java
@@ -3,23 +3,35 @@
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
-import android.support.v7.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import android.content.pm.PackageManager;
+
+import android.util.Log;
import android.view.MotionEvent;
import android.view.WindowManager;
import java.io.File;
public class MainActivity extends AppCompatActivity {
+ private static final String TAG = "PM";
+ private static final int REQ_AUDIO = 1001;
+
private AudioThread audioThread;
+ private projectMGLView glSurfaceView;
public class projectMGLView extends GLSurfaceView {
- private RendererWrapper mRenderer;
+ private final RendererWrapper mRenderer;
public projectMGLView(Context context, String assetPath) {
super(context);
- this.setEGLContextClientVersion(2);
+ setEGLContextClientVersion(2);
mRenderer = new RendererWrapper(assetPath);
- this.setRenderer(mRenderer);
+ setRenderer(mRenderer);
+
+ // Keep rendering even if audio isn't available (emulator often provides silence).
+ setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
}
@Override
@@ -27,47 +39,90 @@ public boolean onTouchEvent(MotionEvent e) {
mRenderer.NextPreset();
return true;
}
-
- @Override
- public void onPause() {
- super.onPause();
- }
-
- @Override
- public void onResume() {
- super.onResume();
- }
-
}
- private projectMGLView glSurfaceView;
-
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
String assetPath = new File(getCacheDir(), "projectM").toString();
glSurfaceView = new projectMGLView(this, assetPath);
setContentView(glSurfaceView);
+
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
@Override
protected void onPause() {
super.onPause();
- glSurfaceView.onPause();
- audioThread.stop_recording();
- try {
- audioThread.join();
- } catch (Exception e) {
- e.printStackTrace();
- }
+ if (glSurfaceView != null) glSurfaceView.onPause();
+
+ stopAudioThreadIfRunning();
}
@Override
protected void onResume() {
super.onResume();
- glSurfaceView.onResume();
+ if (glSurfaceView != null) glSurfaceView.onResume();
+
+ ensureAudioPermissionAndStart();
+ }
+
+ private void ensureAudioPermissionAndStart() {
+ if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO)
+ == PackageManager.PERMISSION_GRANTED) {
+ startAudioThreadIfNeeded();
+ } else {
+ // Trigger runtime permission prompt
+ ActivityCompat.requestPermissions(
+ this,
+ new String[]{android.Manifest.permission.RECORD_AUDIO},
+ REQ_AUDIO
+ );
+ }
+ }
+
+ private void startAudioThreadIfNeeded() {
+ if (audioThread != null) return;
+
+ Log.i(TAG, "Starting AudioThread");
audioThread = new AudioThread();
audioThread.start();
}
+
+ private void stopAudioThreadIfRunning() {
+ if (audioThread == null) return;
+
+ Log.i(TAG, "Stopping AudioThread");
+ try {
+ audioThread.stop_recording();
+ } catch (Throwable t) {
+ Log.w(TAG, "stop_recording() threw", t);
+ }
+
+ try {
+ audioThread.join(1500);
+ } catch (InterruptedException ignored) {
+ } catch (Throwable t) {
+ Log.w(TAG, "join() threw", t);
+ }
+
+ audioThread = null;
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ if (requestCode == REQ_AUDIO) {
+ boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
+ if (granted) {
+ Log.i(TAG, "RECORD_AUDIO granted");
+ startAudioThreadIfNeeded();
+ } else {
+ Log.w(TAG, "RECORD_AUDIO denied; running without audio input");
+ // Visuals still render due to RENDERMODE_CONTINUOUSLY.
+ }
+ }
+ }
}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 7d8d24e..a4e8d40 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,5 +1,5 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 6942f5e..581c2f9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,11 +3,10 @@
buildscript {
repositories {
google()
- jcenter()
-
- }
+ mavenCentral()
+ }
dependencies {
- classpath 'com.android.tools.build:gradle:3.3.2'
+ classpath 'com.android.tools.build:gradle:8.13.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -17,8 +16,19 @@ buildscript {
allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
+ }
+ // Align all Kotlin stdlib artifacts to a single version.
+ // AGP (data binding, etc.) can pull an older kotlin-stdlib-jdk7/jdk8 which
+ // then conflicts with newer androidx core-ktx transitive kotlin-stdlib.
+ configurations.configureEach {
+ resolutionStrategy.eachDependency { details ->
+ if (details.requested.group == 'org.jetbrains.kotlin') {
+ details.useVersion '1.9.24'
+ details.because 'Avoid duplicate Kotlin stdlib classes (mixed versions)'
+ }
+ }
}
}
diff --git a/gradle.properties b/gradle.properties
index 82618ce..9592636 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -11,5 +11,5 @@ org.gradle.jvmargs=-Xmx1536m
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
-
-
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 5974ea3..8c4b512 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-all.zip