Compare commits

...

2 Commits

Author SHA1 Message Date
a924331ede feat: API for LibPasada music player 2026-05-24 16:02:06 +02:00
9d12fe411f docs 2026-05-20 01:03:54 +02:00
8 changed files with 517 additions and 1 deletions

View File

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

View 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

View 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

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

View File

@@ -26,7 +26,8 @@ Java_at_lockstep_filter_StepDetector_native_1create(
jclass /*unused*/, jlong engineHandle) {
auto *listener = reinterpret_cast<StepListener *>(engineHandle);
// We use std::nothrow so `new` returns a nullptr if the engine creation fails
auto *detector = new(std::nothrow) StepDetector(60.0 /* FPS */, listener); // TODO
// the hardcoded 'fps' is only an initial value, which is re-computed in StepDetector
auto *detector = new(std::nothrow) StepDetector(60.0 /* FPS */, listener);
return reinterpret_cast<jlong>(detector);
}

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

View File

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

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