Compare commits
2 Commits
74d7d2e064
...
a924331ede
| Author | SHA1 | Date | |
|---|---|---|---|
| a924331ede | |||
| 9d12fe411f |
@@ -35,6 +35,8 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
|
|||||||
jni_mpg123.cpp
|
jni_mpg123.cpp
|
||||||
jni_lockstep.cpp
|
jni_lockstep.cpp
|
||||||
jni_stepdetector.cpp
|
jni_stepdetector.cpp
|
||||||
|
jni_libpasada.cpp
|
||||||
|
LibPasada.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package (oboe REQUIRED CONFIG)
|
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;
|
||||||
|
}
|
||||||
@@ -26,7 +26,8 @@ Java_at_lockstep_filter_StepDetector_native_1create(
|
|||||||
jclass /*unused*/, jlong engineHandle) {
|
jclass /*unused*/, jlong engineHandle) {
|
||||||
auto *listener = reinterpret_cast<StepListener *>(engineHandle);
|
auto *listener = reinterpret_cast<StepListener *>(engineHandle);
|
||||||
// We use std::nothrow so `new` returns a nullptr if the engine creation fails
|
// 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);
|
return reinterpret_cast<jlong>(detector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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