feat: API for LibPasada music player
This commit is contained in:
@@ -35,6 +35,8 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
jni_mpg123.cpp
|
||||
jni_lockstep.cpp
|
||||
jni_stepdetector.cpp
|
||||
jni_libpasada.cpp
|
||||
LibPasada.cpp
|
||||
)
|
||||
|
||||
find_package (oboe REQUIRED CONFIG)
|
||||
|
||||
95
app/src/main/cpp/LibPasada.cpp
Normal file
95
app/src/main/cpp/LibPasada.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
#include "LibPasada.h"
|
||||
#include <string>
|
||||
|
||||
/** Current track reached end; service should advance queue and call {@link LibPasada#play}. */
|
||||
void PasadaPlaybackListener::onTrackFinished() {
|
||||
emitTrackFinished();
|
||||
}
|
||||
|
||||
/** Decode / Oboe / pipeline failure; service should stop safely and surface error. */
|
||||
void PasadaPlaybackListener::onError(int errorCode, std::string message) {
|
||||
emitError(errorCode, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors the libpasada state machine documented in DESIGN.md.
|
||||
* Keep values in sync with PasadaState.java
|
||||
*/
|
||||
/*enum PasadaState {
|
||||
LOADED = 0,
|
||||
INITIALIZED = 1,
|
||||
PLAYING = 2,
|
||||
PAUSED = 3,
|
||||
FINISHED = 4,
|
||||
STOPPED = 5
|
||||
};*/
|
||||
|
||||
/**
|
||||
* JNI entry point for libpasada.
|
||||
* <p>Native state machine: LOADED → INITIALIZED → PLAYING ↔ PAUSED → FINISHED → STOPPED
|
||||
*/
|
||||
|
||||
LibPasada::LibPasada() : state(LOADED)
|
||||
{}
|
||||
|
||||
LibPasada::~LibPasada() {}
|
||||
|
||||
/** Called each time a run/playlist session starts (Oboe silent, buffers ready). */
|
||||
void LibPasada::init() {
|
||||
if(state == LOADED || state == STOPPED) {
|
||||
// perform init
|
||||
state = INITIALIZED;
|
||||
}
|
||||
}
|
||||
|
||||
/** Submit one accelerometer sample (m/s²); may be called from a sensor thread. */
|
||||
void LibPasada::feedAccel(float x, float y, float z, long long timestamp_nanos) {}
|
||||
|
||||
/**
|
||||
* Open MP3 from an already-open file descriptor and begin adaptive playback.
|
||||
*
|
||||
* @param fd open read FD (Java retains ownership; do not close until track changes)
|
||||
* @param offset start offset within the FD (0 for whole file)
|
||||
* @param length byte length from offset ({@code -1} if unknown / to EOF)
|
||||
*/
|
||||
void LibPasada::play(int fd, long long offset, long long length) {
|
||||
state = PLAYING;
|
||||
}
|
||||
/** PLAYING → PAUSED (silent output, graph kept alive). */
|
||||
void LibPasada::pause() {
|
||||
if(state != PLAYING) return;
|
||||
state = PAUSED;
|
||||
}
|
||||
/** PAUSED → PLAYING (same track, same decode position, same FD). */
|
||||
void LibPasada::resume() {
|
||||
if(state != PAUSED) return;
|
||||
state = PLAYING;
|
||||
}
|
||||
/** Tear down Oboe for this run segment → STOPPED. */
|
||||
void LibPasada::stop() {
|
||||
state = STOPPED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Milliseconds from the start of the current track — same timebase as ExoPlayer
|
||||
* {@code getCurrentPosition()}. Safe to poll from background threads.
|
||||
*/
|
||||
long LibPasada::getCurrentPositionMs() { return 0; }
|
||||
|
||||
/** Track duration in ms, or {@code 0} if not yet known. */
|
||||
long LibPasada::getDurationMs() { return 0; }
|
||||
|
||||
/** Whether adapted audio is actively being output (not paused, not finished). */
|
||||
bool LibPasada::isPlaying() { return false; }
|
||||
|
||||
/** Current native state; see {@link PasadaState}. */
|
||||
int LibPasada::getState() { return state; }
|
||||
|
||||
/** Seek within the current track. */
|
||||
void LibPasada::seekTo(long positionMs) {}
|
||||
|
||||
/** Runtime metrics / last error string for logging and debug UI. */
|
||||
std::string LibPasada::getDiagnostics() { return ""; }
|
||||
|
||||
/** Register listener for async events raised from the audio/native thread. */
|
||||
//void LibPasada::setPlaybackListener(PasadaPlaybackListener listener) {} // JNI-side only
|
||||
98
app/src/main/cpp/LibPasada.h
Normal file
98
app/src/main/cpp/LibPasada.h
Normal file
@@ -0,0 +1,98 @@
|
||||
//
|
||||
// Created by david on 24.05.2026.
|
||||
//
|
||||
|
||||
#ifndef LOCKSTEP_LIBPASADA_H
|
||||
#define LOCKSTEP_LIBPASADA_H
|
||||
|
||||
#include <string>
|
||||
|
||||
// JNI helpers for calling into Java code (PasadaPlaybackListener)
|
||||
void emitTrackFinished();
|
||||
void emitError(int errorCode, const std::string& message);
|
||||
|
||||
/**
|
||||
* Callbacks invoked from native (Oboe or internal worker thread).
|
||||
*/
|
||||
class PasadaPlaybackListener {
|
||||
public:
|
||||
/** Current track reached end; service should advance queue and call {@link LibPasada#play}. */
|
||||
void onTrackFinished();
|
||||
|
||||
/** Decode / Oboe / pipeline failure; service should stop safely and surface error. */
|
||||
void onError(int errorCode, std::string message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mirrors the libpasada state machine documented in DESIGN.md.
|
||||
* Keep values in sync with PasadaState.java
|
||||
*/
|
||||
enum PasadaState {
|
||||
LOADED = 0,
|
||||
INITIALIZED = 1,
|
||||
PLAYING = 2,
|
||||
PAUSED = 3,
|
||||
FINISHED = 4,
|
||||
STOPPED = 5
|
||||
};
|
||||
|
||||
/**
|
||||
* JNI entry point for libpasada.
|
||||
* <p>Native state machine: LOADED → INITIALIZED → PLAYING ↔ PAUSED → FINISHED → STOPPED
|
||||
*/
|
||||
class LibPasada {
|
||||
private:
|
||||
PasadaState state;
|
||||
//PasadaPlaybackListener listener;
|
||||
|
||||
public:
|
||||
LibPasada();
|
||||
~LibPasada();
|
||||
|
||||
/** Called each time a run/playlist session starts (Oboe silent, buffers ready). */
|
||||
void init();
|
||||
|
||||
/** Submit one accelerometer sample (m/s²); may be called from a sensor thread. */
|
||||
void feedAccel(float x, float y, float z, long long timestamp_nanos);
|
||||
|
||||
/**
|
||||
* Open MP3 from an already-open file descriptor and begin adaptive playback.
|
||||
*
|
||||
* @param fd open read FD (Java retains ownership; do not close until track changes)
|
||||
* @param offset start offset within the FD (0 for whole file)
|
||||
* @param length byte length from offset ({@code -1} if unknown / to EOF)
|
||||
*/
|
||||
void play(int fd, long long offset, long long length);
|
||||
/** PLAYING → PAUSED (silent output, graph kept alive). */
|
||||
void pause();
|
||||
/** PAUSED → PLAYING (same track, same decode position, same FD). */
|
||||
void resume();
|
||||
/** Tear down Oboe for this run segment → STOPPED. */
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Milliseconds from the start of the current track — same timebase as ExoPlayer
|
||||
* {@code getCurrentPosition()}. Safe to poll from background threads.
|
||||
*/
|
||||
long getCurrentPositionMs();
|
||||
|
||||
/** Track duration in ms, or {@code 0} if not yet known. */
|
||||
long getDurationMs();
|
||||
|
||||
/** Whether adapted audio is actively being output (not paused, not finished). */
|
||||
bool isPlaying();
|
||||
|
||||
/** Current native state; see {@link PasadaState}. */
|
||||
int getState();
|
||||
|
||||
/** Seek within the current track. */
|
||||
void seekTo(long positionMs);
|
||||
|
||||
/** Runtime metrics / last error string for logging and debug UI. */
|
||||
std::string getDiagnostics();
|
||||
|
||||
/** Register listener for async events raised from the audio/native thread. */
|
||||
//void setPlaybackListener(PasadaPlaybackListener listener); // JNI-side only
|
||||
};
|
||||
|
||||
#endif //LOCKSTEP_LIBPASADA_H
|
||||
205
app/src/main/cpp/jni_libpasada.cpp
Normal file
205
app/src/main/cpp/jni_libpasada.cpp
Normal file
@@ -0,0 +1,205 @@
|
||||
//
|
||||
// Created by david on 24.05.2026.
|
||||
//
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
#include "LibPasada.h"
|
||||
|
||||
static JavaVM* g_vm = nullptr;
|
||||
static jobject g_listener = nullptr; // global ref, or nullptr
|
||||
static jmethodID g_onTrackFinished = nullptr;
|
||||
static jmethodID g_onError = nullptr;
|
||||
|
||||
static LibPasada* g_libpasada = new LibPasada();
|
||||
|
||||
void clearListener(JNIEnv* env);
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
|
||||
g_vm = vm;
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
/** Called each time a run/playlist session starts (Oboe silent, buffers ready). */
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_init(
|
||||
JNIEnv *env,
|
||||
jclass /*unused*/) {
|
||||
if (g_libpasada) {
|
||||
clearListener(env);
|
||||
delete g_libpasada;
|
||||
}
|
||||
g_libpasada = new LibPasada();
|
||||
g_libpasada->init();
|
||||
}
|
||||
|
||||
/** Submit one accelerometer sample (m/s^2); may be called from a sensor thread. */
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_feedAccel(JNIEnv *env, jclass clazz, jfloat x, jfloat y,
|
||||
jfloat z, jlong timestamp_nanos) {
|
||||
if (g_libpasada == nullptr)
|
||||
return;
|
||||
g_libpasada->feedAccel(x, y, z, timestamp_nanos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open MP3 from an already-open file descriptor and begin adaptive playback.
|
||||
*
|
||||
* @param fd open read FD (Java retains ownership; do not close until track changes)
|
||||
* @param offset start offset within the FD (0 for whole file)
|
||||
* @param length byte length from offset ({@code -1} if unknown / to EOF)
|
||||
*/
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_play(JNIEnv *env, jclass clazz, jint fd, jlong offset,
|
||||
jlong length) {
|
||||
if (g_libpasada == nullptr)
|
||||
return;
|
||||
g_libpasada->play(fd, offset, length);
|
||||
}
|
||||
/** PLAYING → PAUSED (silent output, graph kept alive). */
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_pause(JNIEnv *env, jclass clazz) {
|
||||
if (g_libpasada == nullptr)
|
||||
return;
|
||||
g_libpasada->pause();
|
||||
}
|
||||
/** PAUSED → PLAYING (same track, same decode position, same FD). */
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_resume(JNIEnv *env, jclass clazz) {
|
||||
if (g_libpasada == nullptr)
|
||||
return;
|
||||
g_libpasada->resume();
|
||||
}
|
||||
/** Tear down Oboe for this run segment → STOPPED. */
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_stop(JNIEnv *env, jclass clazz) {
|
||||
clearListener(env);
|
||||
if (g_libpasada == nullptr)
|
||||
return;
|
||||
g_libpasada->stop();
|
||||
delete g_libpasada;
|
||||
g_libpasada = nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Milliseconds from the start of the current track — same timebase as ExoPlayer
|
||||
* {@code getCurrentPosition()}. Safe to poll from background threads.
|
||||
*/
|
||||
extern "C"
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_getCurrentPositionMs(JNIEnv *env, jclass clazz) {
|
||||
if (g_libpasada == nullptr)
|
||||
return 0;
|
||||
return g_libpasada->getCurrentPositionMs();
|
||||
}
|
||||
/** Track duration in ms, or {@code 0} if not yet known. */
|
||||
extern "C"
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_getDurationMs(JNIEnv *env, jclass clazz) {
|
||||
if (g_libpasada == nullptr)
|
||||
return 0;
|
||||
return g_libpasada->getDurationMs();
|
||||
}
|
||||
/** Whether adapted audio is actively being output (not paused, not finished). */
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_isPlaying(JNIEnv *env, jclass clazz) {
|
||||
if (g_libpasada == nullptr)
|
||||
return false;
|
||||
return g_libpasada->isPlaying();
|
||||
}
|
||||
/** Current native state; see {@link PasadaState}. */
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_getState(JNIEnv *env, jclass clazz) {
|
||||
if(g_libpasada == nullptr)
|
||||
return STOPPED;
|
||||
return g_libpasada->getState();
|
||||
}
|
||||
/** Seek within the current track. */
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_seekTo(JNIEnv *env, jclass clazz, jlong position_ms) {
|
||||
if (g_libpasada == nullptr)
|
||||
return;
|
||||
g_libpasada->seekTo((long) position_ms);
|
||||
}
|
||||
/** Runtime metrics / last error string for logging and debug UI. */
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_getDiagnostics(JNIEnv *env, jclass clazz) {
|
||||
if (g_libpasada == nullptr) {
|
||||
return env->NewStringUTF("");
|
||||
}
|
||||
std::string message = g_libpasada->getDiagnostics();
|
||||
return env->NewStringUTF(message.c_str());
|
||||
}
|
||||
|
||||
|
||||
/** Register listener for async events raised from the audio/native thread. */
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_at_lockstep_player_pasada_LibPasada_setPlaybackListener(JNIEnv *env, jclass clazz,
|
||||
jobject listener) {
|
||||
// 1. Drop previous listener
|
||||
if (g_listener != nullptr) {
|
||||
clearListener(env);
|
||||
}
|
||||
if (listener == nullptr) {
|
||||
clearListener(env);
|
||||
return;
|
||||
}
|
||||
// 2. Keep listener alive past this call
|
||||
g_listener = env->NewGlobalRef(listener);
|
||||
// 3. Resolve methods once (valid until class unload)
|
||||
jclass cls = env->GetObjectClass(g_listener);
|
||||
g_onTrackFinished = env->GetMethodID(cls, "onTrackFinished", "()V");
|
||||
g_onError = env->GetMethodID(
|
||||
cls, "onError", "(ILjava/lang/String;)V");
|
||||
env->DeleteLocalRef(cls);
|
||||
if (g_onTrackFinished == nullptr || g_onError == nullptr) {
|
||||
env->DeleteGlobalRef(g_listener);
|
||||
g_listener = nullptr;
|
||||
// ExceptionPending if method missing
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// JNI helpers for calling into Java code (PasadaPlaybackListener)
|
||||
//
|
||||
void emitTrackFinished() {
|
||||
if (!g_vm || !g_listener || !g_onTrackFinished) return;
|
||||
JNIEnv* env = nullptr;
|
||||
if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) return;
|
||||
env->CallVoidMethod(g_listener, g_onTrackFinished);
|
||||
if (env->ExceptionCheck()) env->ExceptionClear();
|
||||
// DetachCurrentThread only if this thread won't call JNI again
|
||||
}
|
||||
void emitError(int errorCode, const char* message) {
|
||||
if (!g_vm || !g_listener || !g_onError) return;
|
||||
JNIEnv* env = nullptr;
|
||||
if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) return;
|
||||
jstring jMessage = env->NewStringUTF(message != nullptr ? message : "");
|
||||
env->CallVoidMethod(g_listener, g_onError, static_cast<jint>(errorCode), jMessage);
|
||||
env->DeleteLocalRef(jMessage);
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionClear();
|
||||
}
|
||||
}
|
||||
void emitError(int errorCode, const std::string& message) {
|
||||
emitError(errorCode, message.c_str());
|
||||
}
|
||||
void clearListener(JNIEnv* env) {
|
||||
if (g_listener) {
|
||||
env->DeleteGlobalRef(g_listener);
|
||||
g_listener = nullptr;
|
||||
}
|
||||
g_onTrackFinished = nullptr;
|
||||
g_onError = nullptr;
|
||||
}
|
||||
72
app/src/main/java/at/lockstep/player/pasada/LibPasada.java
Normal file
72
app/src/main/java/at/lockstep/player/pasada/LibPasada.java
Normal file
@@ -0,0 +1,72 @@
|
||||
package at.lockstep.player.pasada;
|
||||
|
||||
/**
|
||||
* JNI entry point for libpasada.
|
||||
*
|
||||
* <p>Native state machine: LOADED → INITIALIZED → PLAYING ↔ PAUSED → FINISHED → STOPPED
|
||||
*
|
||||
* <p>Call {@link #loadNative()} once before any other method.
|
||||
*/
|
||||
public final class LibPasada {
|
||||
|
||||
private static boolean loaded;
|
||||
|
||||
private LibPasada() {}
|
||||
|
||||
/** Loads {@code libpasada.so}. Safe to call multiple times. */
|
||||
public static synchronized void loadNative() {
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
System.loadLibrary("lockstep-native");
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
/** Called each time a run/playlist session starts (Oboe silent, buffers ready). */
|
||||
public static native void init();
|
||||
|
||||
/** Submit one accelerometer sample (m/s²); may be called from a sensor thread. */
|
||||
public static native void feedAccel(float x, float y, float z, long timestampNanos);
|
||||
|
||||
/**
|
||||
* Open MP3 from an already-open file descriptor and begin adaptive playback.
|
||||
*
|
||||
* @param fd open read FD (Java retains ownership; do not close until track changes)
|
||||
* @param offset start offset within the FD (0 for whole file)
|
||||
* @param length byte length from offset ({@code -1} if unknown / to EOF)
|
||||
*/
|
||||
public static native void play(int fd, long offset, long length);
|
||||
|
||||
/** PLAYING → PAUSED (silent output, graph kept alive). */
|
||||
public static native void pause();
|
||||
|
||||
/** PAUSED → PLAYING (same track, same decode position, same FD). */
|
||||
public static native void resume();
|
||||
|
||||
/** Tear down Oboe for this run segment → STOPPED. */
|
||||
public static native void stop();
|
||||
|
||||
/**
|
||||
* Milliseconds from the start of the current track — same timebase as ExoPlayer
|
||||
* {@code getCurrentPosition()}. Safe to poll from background threads.
|
||||
*/
|
||||
public static native long getCurrentPositionMs();
|
||||
|
||||
/** Track duration in ms, or {@code 0} if not yet known. */
|
||||
public static native long getDurationMs();
|
||||
|
||||
/** Whether adapted audio is actively being output (not paused, not finished). */
|
||||
public static native boolean isPlaying();
|
||||
|
||||
/** Current native state; see {@link PasadaState}. */
|
||||
public static native int getState();
|
||||
|
||||
/** Seek within the current track. */
|
||||
public static native void seekTo(long positionMs);
|
||||
|
||||
/** Runtime metrics / last error string for logging and debug UI. */
|
||||
public static native String getDiagnostics();
|
||||
|
||||
/** Register listener for async events raised from the audio/native thread. */
|
||||
public static native void setPlaybackListener(PasadaPlaybackListener listener);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package at.lockstep.player.pasada;
|
||||
|
||||
/**
|
||||
* Callbacks invoked from native (Oboe or internal worker thread).
|
||||
* Implementations must post to the main thread if they touch UI or service state.
|
||||
*/
|
||||
public interface PasadaPlaybackListener {
|
||||
|
||||
/** Current track reached end; service should advance queue and call {@link LibPasada#play}. */
|
||||
void onTrackFinished();
|
||||
|
||||
/** Decode / Oboe / pipeline failure; service should stop safely and surface error. */
|
||||
void onError(int errorCode, String message);
|
||||
}
|
||||
29
app/src/main/java/at/lockstep/player/pasada/PasadaState.java
Normal file
29
app/src/main/java/at/lockstep/player/pasada/PasadaState.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package at.lockstep.player.pasada;
|
||||
|
||||
/**
|
||||
* Mirrors the libpasada state machine documented in DESIGN.md.
|
||||
* Keep values in sync with LibPasada.h
|
||||
*/
|
||||
public enum PasadaState {
|
||||
LOADED(0),
|
||||
INITIALIZED(1),
|
||||
PLAYING(2),
|
||||
PAUSED(3),
|
||||
FINISHED(4),
|
||||
STOPPED(5);
|
||||
|
||||
public final int code;
|
||||
|
||||
PasadaState(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public static PasadaState fromCode(int code) {
|
||||
for (PasadaState state : values()) {
|
||||
if (state.code == code) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown PasadaState code: " + code);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user