feat: LibPasada API: stretcher for lockstep-player integration

This commit is contained in:
2026-06-03 18:11:56 +02:00
parent f8aa61c1b9
commit 83dea946c3
16 changed files with 220 additions and 33 deletions

View File

@@ -37,11 +37,12 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
jni_stepdetector.cpp jni_stepdetector.cpp
jni_libpasada.cpp jni_libpasada.cpp
LibPasada.cpp LibPasada.cpp
#// jni_logging.cpp // same implemented in jni_libpasada.cpp // JNI_OnLoad and JNI_OnUnload
) )
find_package (oboe REQUIRED CONFIG) find_package (oboe REQUIRED CONFIG)
add_library(ndk-logger SHARED logging.cpp jni_logging.cpp) add_library(ndk-logger SHARED logging.cpp) # jni_logging.cpp
target_link_libraries(ndk-logger log) target_link_libraries(ndk-logger log)
# Add pre-built libmpg123 library # Add pre-built libmpg123 library

View File

@@ -13,6 +13,10 @@ void PasadaPlaybackListener::onError(int errorCode, std::string message) {
emitError(errorCode, message); emitError(errorCode, message);
} }
void PasadaPlaybackListener::onTrackClosed(int fd) {
emitTrackClosed(fd);
}
/** /**
* Mirrors the libpasada state machine documented in DESIGN.md. * Mirrors the libpasada state machine documented in DESIGN.md.
* Keep values in sync with PasadaState.java * Keep values in sync with PasadaState.java
@@ -57,7 +61,7 @@ void LibPasada::init() {
std::lock_guard<std::mutex> lock(mtxDetector); std::lock_guard<std::mutex> lock(mtxDetector);
if(state == LOADED || state == STOPPED) { if(state == LOADED || state == STOPPED) {
// perform init // perform init
auto *playbackEngine = new PlaybackEngine("", 0); auto *playbackEngine = new PlaybackEngine("", 0, this);
engine = playbackEngine; engine = playbackEngine;
auto *stepListener = reinterpret_cast<StepListener *>(playbackEngine); auto *stepListener = reinterpret_cast<StepListener *>(playbackEngine);
@@ -100,6 +104,7 @@ void LibPasada::pause() {
std::lock_guard<std::mutex> lockState(mtxState); std::lock_guard<std::mutex> lockState(mtxState);
if(state != PLAYING) return; if(state != PLAYING) return;
state = PAUSED; state = PAUSED;
// TODO: pause plays a rect noise in PlaybackEngine. debug it!
auto *playbackEngine = reinterpret_cast<PlaybackEngine *>(engine); auto *playbackEngine = reinterpret_cast<PlaybackEngine *>(engine);
playbackEngine->pause(); playbackEngine->pause();
} }

View File

@@ -12,6 +12,7 @@
// JNI helpers for calling into Java code (PasadaPlaybackListener) // JNI helpers for calling into Java code (PasadaPlaybackListener)
void emitTrackFinished(); void emitTrackFinished();
void emitError(int errorCode, const std::string& message); void emitError(int errorCode, const std::string& message);
void emitTrackClosed(int fd);
/** /**
* Mirrors the libpasada state machine documented in DESIGN.md. * Mirrors the libpasada state machine documented in DESIGN.md.

View File

@@ -16,6 +16,7 @@ public:
virtual ~PasadaPlaybackListener() = default; virtual ~PasadaPlaybackListener() = default;
/** Current track reached end; service should advance queue and call {@link LibPasada#play}. */ /** Current track reached end; service should advance queue and call {@link LibPasada#play}. */
virtual void onTrackFinished(); virtual void onTrackFinished();
virtual void onTrackClosed(int fd);
/** Decode / Oboe / pipeline failure; service should stop safely and surface error. */ /** Decode / Oboe / pipeline failure; service should stop safely and surface error. */
virtual void onError(int errorCode, std::string message); virtual void onError(int errorCode, std::string message);

View File

@@ -13,6 +13,7 @@
#include <chrono> #include <chrono>
#include <sys/types.h> #include <sys/types.h>
#include <unistd.h> #include <unistd.h>
#include <random>
/** /**
* Read samples from the next mp3-frame into the struct MP3File's buffer. * Read samples from the next mp3-frame into the struct MP3File's buffer.
@@ -168,11 +169,13 @@ void PlaybackEngine::closeMusicFile() {
haveMusicFile.store(false); haveMusicFile.store(false);
musicFile = nullptr; musicFile = nullptr;
if(android_fd) { if(android_fd) {
close(android_fd); //close(android_fd);
if(listener) listener->onTrackClosed(android_fd);
android_fd = 0; android_fd = 0;
} }
isSetMusic.store(false); isSetMusic.store(false);
mPlayer->setMusic(nullptr); if(mPlayer)
mPlayer->setMusic(nullptr);
} }
void PlaybackEngine::mapChannels(int *channel_map, int num_ch_in, int num_ch_out) { void PlaybackEngine::mapChannels(int *channel_map, int num_ch_in, int num_ch_out) {
@@ -198,6 +201,10 @@ void PlaybackEngine::mapChannels(int *channel_map, int num_ch_in, int num_ch_out
} }
void PlaybackEngine::musicFeedThread() { void PlaybackEngine::musicFeedThread() {
/*
std::mt19937 mt(42);
std::uniform_real_distribution<double> dist(0.98, 1.0);
*/
LOGI("starting musicFeedThread()"); LOGI("starting musicFeedThread()");
// strecher num channels: same as output num channels // strecher num channels: same as output num channels
@@ -304,7 +311,10 @@ void PlaybackEngine::musicFeedThread() {
// 1024, 512, 512 // 1024, 512, 512
// 7 x 512 // 7 x 512
} }
memset(buf, 0, num_samples*num_ch_out*sizeof(float)); //memset(buf, 0, num_samples*num_ch_out*sizeof(float)); // wrong, because buffer is not contiguous for channels
for(int i = 0; i < num_ch_out; i++) {
memset(buf_ptr[i], 0, num_samples * sizeof(float));
}
stretcher.process(buf_ptr, num_samples, false); stretcher.process(buf_ptr, num_samples, false);
continue; continue;
} }
@@ -315,6 +325,13 @@ void PlaybackEngine::musicFeedThread() {
// feed 1024 music samples // feed 1024 music samples
// => stretcher is asking for 1024 = getSamplesRequired() // => stretcher is asking for 1024 = getSamplesRequired()
// ca. 21 ms // ca. 21 ms
/*
int64_t a = mpg123_seek(musicFile->handle, 0, SEEK_CUR);
int64_t b = mpg123_tell(musicFile->handle);
LOGI(" checking mpg123_seek(): len=%lld seek=%lld tell=%lld",
(long long)musicFile->num_samples, (long long)a, (long long)b);
*/
} }
markerPos.store((musicFile->num_samples - musicFile->remaining_samples) / musicFile->samples_per_frame * musicFile->secs_per_frame); markerPos.store((musicFile->num_samples - musicFile->remaining_samples) / musicFile->samples_per_frame * musicFile->secs_per_frame);
@@ -324,30 +341,48 @@ void PlaybackEngine::musicFeedThread() {
double pos = seekPos.load(); double pos = seekPos.load();
// compute seek target in samples, clip to [0..num_samples] // compute seek target in samples, clip to [0..num_samples]
auto seek_samples = static_cast<long>(pos * musicFile->samples_per_frame / musicFile->secs_per_frame); auto seek_samples = static_cast<long>(pos * musicFile->samples_per_frame / musicFile->secs_per_frame);
LOGI("seek to %.3lf sec = %lld samples", pos, (long long) seek_samples);
seek_samples = std::max(seek_samples, 0L); seek_samples = std::max(seek_samples, 0L);
seek_samples = std::min(seek_samples, musicFile->num_samples); seek_samples = std::min(seek_samples, musicFile->num_samples);
// compute seek delta, to update byte 'offset'
auto seek_samples_delta = seek_samples - (musicFile->num_samples - musicFile->remaining_samples);
auto seek_bytes_delta = seek_samples_delta * num_ch_in * sizeof(int16_t);
// perform the seek // perform the seek
off_t seekResult = mpg123_seek(musicFile->handle, musicFile->num_samples - musicFile->remaining_samples, SEEK_SET); off_t seekResult = mp3file_seek(musicFile.get(), seek_samples);
if(seekResult != seek_samples) { idebug = 0;
LOGE("error seeking in mp3 file: mpg123_seek(handle, %d, SEEK_SET)=%d", seek_samples, seekResult); /*if(seekResult > seek_samples) {
} LOGE("error seeking in mp3 file: mpg123_seek(handle, %ld, SEEK_SET)=%lld", seek_samples, (long long) seekResult);
// update structure seekPos.store(pos * dist(mt)); // randomize seek target a bit
musicFile->remaining_samples = static_cast<int>(musicFile->num_samples - seek_samples); } else {*/
musicFile->offset += seek_bytes_delta; // update structure
requestSeek.store(false); // compute seek delta, to update byte 'offset'
//auto seek_samples_delta = seekResult - (musicFile->num_samples - musicFile->remaining_samples);
//auto seek_bytes_delta = seek_samples_delta * num_ch_in * sizeof(int16_t);
musicFile->remaining_samples = static_cast<int>(musicFile->num_samples - seekResult);
//musicFile->offset += seek_bytes_delta;
musicFile->offset = lseek(musicFile->android_fd, 0, SEEK_CUR);
// it is not possible to obtain bytes offset reliably??
//musicFile->num_bytes = -1; // pretend we can read until the end
requestSeek.store(false);
/*}*/
} }
size_t done = 0; // bytes! size_t done = 0; // bytes!
size_t read_size_bytes_calc = std::min(num_samples * num_ch_in * sizeof(int16_t), cbuf_size_bytes); size_t read_size_bytes = std::min(num_samples * num_ch_in * sizeof(int16_t), cbuf_size_bytes);
size_t read_size_bytes = read_size_bytes_calc; /*if (musicFile->offset != 0 && musicFile->num_bytes != -1) { read_size_bytes = std::min(read_size_bytes_calc, (size_t) musicFile->num_bytes - musicFile->offset); }*/
if (musicFile->offset != 0 && musicFile->num_bytes != -1) { read_size_bytes = std::min(read_size_bytes_calc, (size_t) musicFile->num_bytes - musicFile->offset); } //if (idebug < 10) LOGI("mft rsbc=%lld rsb=%lld nb=%lld o=%lld", (long long) read_size_bytes_calc, (long long) read_size_bytes, (long long) musicFile->num_bytes, (long long) musicFile->offset);
int err = mpg123_read(musicFile->handle, cbuf, read_size_bytes, &done); int err = mpg123_read(musicFile->handle, cbuf, read_size_bytes, &done);
if (musicFile->offset != 0 && musicFile->num_bytes != -1 && err == MPG123_OK && read_size_bytes <= read_size_bytes_calc) { err = MPG123_DONE; } //if (idebug < 10) LOGI("mft done=%lld err=%lld small=%d", (long long) done, (long long) err, read_size_bytes < read_size_bytes_calc);
/*if (musicFile->offset != 0 && musicFile->num_bytes != -1 && err == MPG123_OK && read_size_bytes < read_size_bytes_calc) {
LOGW("out of bytes, finished playing");
err = MPG123_DONE;
}*/
musicFile->remaining_samples -= done / sizeof(int16_t); musicFile->remaining_samples -= done / sizeof(int16_t);
musicFile->offset += done; //musicFile->offset += done;
musicFile->offset = lseek(musicFile->android_fd, 0, SEEK_CUR);
if(musicFile->offset >= musicFile->num_bytes) {
// TODO: this is inaccurate. either we do not decode everything, or we overrun past the end
// TODO: maybe decode as a "stream" instead.
LOGW("out of bytes, finished playing");
err = MPG123_DONE;
}
if (err != MPG123_OK && err != MPG123_DONE) { if (err != MPG123_OK && err != MPG123_DONE) {
// error! // error!
LOGE("error reading mp3 file: mpg123_read() err=%d done=%d", err, done); LOGE("error reading mp3 file: mpg123_read() err=%d done=%d", err, done);
@@ -363,6 +398,7 @@ void PlaybackEngine::musicFeedThread() {
// next iteration will play silence // next iteration will play silence
// we keep Stretcher and Oboe alive // we keep Stretcher and Oboe alive
LOGI("finished reading mp3 file (MPG123_DONE)"); LOGI("finished reading mp3 file (MPG123_DONE)");
idebug = 0;
closeMusicFile(); closeMusicFile();
if(listener) listener->onTrackFinished(); if(listener) listener->onTrackFinished();
continue; continue;
@@ -394,6 +430,7 @@ void PlaybackEngine::musicFeedThread() {
} }
void PlaybackEngine::pause() { void PlaybackEngine::pause() {
LOGI("PlaybackEngine::pause() set isPaused.");
// next iteration will play silence // next iteration will play silence
isPaused.store(true); isPaused.store(true);
} }

View File

@@ -4,28 +4,55 @@
#include <jni.h> #include <jni.h>
#include <string> #include <string>
#include "LibPasada.h" #include "LibPasada.h"
#include "logging.h"
static JavaVM* g_vm = nullptr; static JavaVM* g_vm = nullptr;
static jobject g_listener = nullptr; // global ref, or nullptr static jobject g_listener = nullptr; // global ref, or nullptr
static jmethodID g_onTrackFinished = nullptr; static jmethodID g_onTrackFinished = nullptr;
static jmethodID g_onTrackClosed = nullptr;
static jmethodID g_onError = nullptr; static jmethodID g_onError = nullptr;
static LibPasada* g_libpasada = new LibPasada(); static LibPasada* g_libpasada = nullptr;
void clearListener(JNIEnv* env); void clearListener(JNIEnv* env);
extern "C" extern "C"
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
g_vm = vm; g_vm = vm;
JNIEnv * env = NULL;
//jclass clazz;
//LOGV("JNI_Onload vm:%p reserved:%p", vm, reserved);
if (JNI_OK != vm->GetEnv((void**) &env, JNI_VERSION_1_6)) {
return JNI_VERSION_1_6;
}
logback_init(vm, env);
LOGD("liblockstep-native jni_libpasada.cpp JNI_OnLoad()");
return JNI_VERSION_1_6; return JNI_VERSION_1_6;
} }
extern "C" void
JNI_OnUnload(JavaVM * vm, void * reserved)
{
JNIEnv * env = NULL;
//LOGV("JNI_OnUnload vm:%p reserved:%p", vm, reserved);
if (JNI_OK != vm->GetEnv((void**) &env, JNI_VERSION_1_6)) {
return;
}
logback_uninit(env);
}
/** Called each time a run/playlist session starts (Oboe silent, buffers ready). */ /** Called each time a run/playlist session starts (Oboe silent, buffers ready). */
extern "C" extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_at_lockstep_player_pasada_LibPasada_init( Java_at_lockstep_player_pasada_LibPasada_init(
JNIEnv *env, JNIEnv *env,
jclass /*unused*/) { jclass /*unused*/) {
LOGD("liblockstep-native jni_libpasada.cpp LibPasada_init()");
if (g_libpasada) { if (g_libpasada) {
clearListener(env); clearListener(env);
delete g_libpasada; delete g_libpasada;
@@ -33,6 +60,7 @@ Java_at_lockstep_player_pasada_LibPasada_init(
try { try {
g_libpasada = new LibPasada(); g_libpasada = new LibPasada();
g_libpasada->init(); g_libpasada->init();
return;
} catch (const std::bad_alloc&) { } catch (const std::bad_alloc&) {
jclass cls = env->FindClass("java/lang/OutOfMemoryError"); jclass cls = env->FindClass("java/lang/OutOfMemoryError");
if (cls) env->ThrowNew(cls, "native allocation failed"); if (cls) env->ThrowNew(cls, "native allocation failed");
@@ -83,6 +111,7 @@ extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_at_lockstep_player_pasada_LibPasada_play(JNIEnv *env, jclass clazz, jint fd, jlong offset, Java_at_lockstep_player_pasada_LibPasada_play(JNIEnv *env, jclass clazz, jint fd, jlong offset,
jlong length) { jlong length) {
LOGD("liblockstep-native jni_libpasada.cpp LibPasada_play()");
if (g_libpasada == nullptr) if (g_libpasada == nullptr)
return; return;
@@ -306,6 +335,7 @@ Java_at_lockstep_player_pasada_LibPasada_setPlaybackListener(JNIEnv *env, jclass
// 3. Resolve methods once (valid until class unload) // 3. Resolve methods once (valid until class unload)
jclass cls = env->GetObjectClass(g_listener); jclass cls = env->GetObjectClass(g_listener);
g_onTrackFinished = env->GetMethodID(cls, "onTrackFinished", "()V"); g_onTrackFinished = env->GetMethodID(cls, "onTrackFinished", "()V");
g_onTrackClosed = env->GetMethodID(cls, "onTrackClosed", "(I)V");
g_onError = env->GetMethodID( g_onError = env->GetMethodID(
cls, "onError", "(ILjava/lang/String;)V"); cls, "onError", "(ILjava/lang/String;)V");
env->DeleteLocalRef(cls); env->DeleteLocalRef(cls);
@@ -338,6 +368,14 @@ void emitError(int errorCode, const char* message) {
env->ExceptionClear(); env->ExceptionClear();
} }
} }
void emitTrackClosed(int fd) {
if (!g_vm || !g_listener || !g_onTrackClosed) return;
JNIEnv* env = nullptr;
if (g_vm->AttachCurrentThread(&env, nullptr) != JNI_OK) return;
env->CallVoidMethod(g_listener, g_onTrackClosed, static_cast<jint>(fd));
if (env->ExceptionCheck()) env->ExceptionClear();
// DetachCurrentThread only if this thread won't call JNI again
}
void emitError(int errorCode, const std::string& message) { void emitError(int errorCode, const std::string& message) {
emitError(errorCode, message.c_str()); emitError(errorCode, message.c_str());
} }

View File

@@ -22,6 +22,27 @@ Java_at_lockstep_pb_PlaybackEngine_native_1createEngine(
return reinterpret_cast<jlong>(engine); return reinterpret_cast<jlong>(engine);
} }
JNIEXPORT void JNICALL
Java_at_lockstep_pb_PlaybackEngine_native_1stop(
JNIEnv *env,
jclass,
jlong engineHandle) {
auto *engine = reinterpret_cast<PlaybackEngine *>(engineHandle);
engine->stop();
}
JNIEXPORT void JNICALL
Java_at_lockstep_pb_PlaybackEngine_native_1pause(
JNIEnv *env,
jclass,
jlong engineHandle) {
auto *engine = reinterpret_cast<PlaybackEngine *>(engineHandle);
// TODO: this is broken. there is a square wave noise played, instead of silence
engine->pause(); // do not tear down the Oboe pipeline, such that we can play again later (without having to re-construct PlaybackEngine)
}
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_at_lockstep_pb_PlaybackEngine_native_1deleteEngine( Java_at_lockstep_pb_PlaybackEngine_native_1deleteEngine(
JNIEnv *env, JNIEnv *env,

View File

@@ -66,14 +66,14 @@ JNI_OnLoad(JavaVM * vm, void * reserved)
jclass clazz; jclass clazz;
//LOGV("JNI_Onload vm:%p reserved:%p", vm, reserved); //LOGV("JNI_Onload vm:%p reserved:%p", vm, reserved);
if (JNI_OK != vm->GetEnv((void**) &env, JNI_VERSION_1_4)) { if (JNI_OK != vm->GetEnv((void**) &env, JNI_VERSION_1_6)) {
//LOGE("JNI_OnUnload GetEnv JNI_VERSION_1_4 failed"); //LOGE("JNI_OnUnload GetEnv JNI_VERSION_1_6 failed");
return JNI_VERSION_1_4; return JNI_VERSION_1_6;
} }
logback_init(vm, env); logback_init(vm, env);
return JNI_VERSION_1_4; return JNI_VERSION_1_6;
} }
extern "C" void extern "C" void
@@ -82,8 +82,8 @@ JNI_OnUnload(JavaVM * vm, void * reserved)
JNIEnv * env = NULL; JNIEnv * env = NULL;
//LOGV("JNI_OnUnload vm:%p reserved:%p", vm, reserved); //LOGV("JNI_OnUnload vm:%p reserved:%p", vm, reserved);
if (JNI_OK != vm->GetEnv((void**) &env, JNI_VERSION_1_4)) { if (JNI_OK != vm->GetEnv((void**) &env, JNI_VERSION_1_6)) {
//LOGE("JNI_OnUnload GetEnv JNI_VERSION_1_4 failed"); //LOGE("JNI_OnUnload GetEnv JNI_VERSION_1_6 failed");
return; return;
} }

View File

@@ -91,7 +91,7 @@ __logback_print(const int level, const char * format, ...)
JNIEnv * env = NULL; JNIEnv * env = NULL;
jint err = JNI_OK; jint err = JNI_OK;
if (! __logback_jvm) return; if (! __logback_jvm) return;
if (JNI_EDETACHED == (err = __logback_jvm->GetEnv((void**) &env, JNI_VERSION_1_4))) { if (JNI_EDETACHED == (err = __logback_jvm->GetEnv((void**) &env, JNI_VERSION_1_6))) {
if (__logback_jvm->AttachCurrentThread(&env, NULL) < 0) { if (__logback_jvm->AttachCurrentThread(&env, NULL) < 0) {
__android_log_write(LOG_LEVEL_WARN, LOG_TAG, "__logback_write failed to get env neither attach current thread"); __android_log_write(LOG_LEVEL_WARN, LOG_TAG, "__logback_write failed to get env neither attach current thread");
} }
@@ -115,7 +115,7 @@ __logback_vprint(const int level, const char * format, va_list ap)
JNIEnv * env = NULL; JNIEnv * env = NULL;
jint err = JNI_OK; jint err = JNI_OK;
if (! __logback_jvm) return; if (! __logback_jvm) return;
if (JNI_EDETACHED == (err = __logback_jvm->GetEnv((void**) &env, JNI_VERSION_1_4))) { if (JNI_EDETACHED == (err = __logback_jvm->GetEnv((void**) &env, JNI_VERSION_1_6))) {
if (__logback_jvm->AttachCurrentThread(&env, NULL) < 0) { if (__logback_jvm->AttachCurrentThread(&env, NULL) < 0) {
__android_log_write(LOG_LEVEL_WARN, LOG_TAG, "__logback_write failed to get env neither attach current thread"); __android_log_write(LOG_LEVEL_WARN, LOG_TAG, "__logback_write failed to get env neither attach current thread");
} }
@@ -136,7 +136,7 @@ __logback_write(const int level, const char * msg)
JNIEnv * env = NULL; JNIEnv * env = NULL;
jint err = JNI_OK; jint err = JNI_OK;
if (! __logback_jvm) return; if (! __logback_jvm) return;
if (JNI_EDETACHED == (err = __logback_jvm->GetEnv((void**) &env, JNI_VERSION_1_4))) { if (JNI_EDETACHED == (err = __logback_jvm->GetEnv((void**) &env, JNI_VERSION_1_6))) {
if (__logback_jvm->AttachCurrentThread(&env, NULL) < 0) { if (__logback_jvm->AttachCurrentThread(&env, NULL) < 0) {
__android_log_write(LOG_LEVEL_WARN, LOG_TAG, "__logback_write failed to get env neither attach current thread"); __android_log_write(LOG_LEVEL_WARN, LOG_TAG, "__logback_write failed to get env neither attach current thread");
} }

View File

@@ -105,6 +105,8 @@ MP3File* mp3file_open_fd(int fd, long long offset, long long length, int forceEn
goto on_error; \ goto on_error; \
} while(0) } while(0)
int64_t len, a, b;
int err = MPG123_OK; int err = MPG123_OK;
mpg123_handle *mh = mpg123_new(NULL, &err); mpg123_handle *mh = mpg123_new(NULL, &err);
if(err != MPG123_OK || mh == NULL) { if(err != MPG123_OK || mh == NULL) {
@@ -119,6 +121,9 @@ MP3File* mp3file_open_fd(int fd, long long offset, long long length, int forceEn
err = mpg123_open_fd(mh, fd); err = mpg123_open_fd(mh, fd);
if(err != MPG123_OK) handleError("mpg123_open()"); if(err != MPG123_OK) handleError("mpg123_open()");
// hopefully the magic bullet for seeking in the file (otherwise, mpg123_seek() returns totally random offsets)
mpg123_scan(mh);
int encoding; int encoding;
err = mpg123_getformat(mh, &mp3->rate, &mp3->channels, &encoding); err = mpg123_getformat(mh, &mp3->rate, &mp3->channels, &encoding);
if(err != MPG123_OK) handleError("mpg123_getformat()"); if(err != MPG123_OK) handleError("mpg123_getformat()");
@@ -156,6 +161,17 @@ MP3File* mp3file_open_fd(int fd, long long offset, long long length, int forceEn
mp3->remaining_samples = (int) mp3->num_samples; mp3->remaining_samples = (int) mp3->num_samples;
LOGI("channels: %d rate: %ld num_samples: %ld", mp3->channels, mp3->rate, mp3->num_samples); LOGI("channels: %d rate: %ld num_samples: %ld", mp3->channels, mp3->rate, mp3->num_samples);
mp3->android_fd = fd; mp3->android_fd = fd;
// begin debug: seek returns wildly random offsets
len = mpg123_length(mh);
a = mpg123_seek(mh, 0, SEEK_SET);
b = mpg123_tell(mh);
LOGI("using mpg123_scan: len=%lld seek=%lld tell=%lld",
(long long)len, (long long)a, (long long)b);
// end debug
return mp3; return mp3;
on_error: on_error:
@@ -165,3 +181,22 @@ on_error:
#undef handleError #undef handleError
} }
off_t mp3file_seek(MP3File *mp3file, off_t samples) {
LOGD("mp3file_seek(): seek to %ld samples", samples);
off_t seekResult = mpg123_seek(mp3file->handle, samples, SEEK_SET);
LOGD(" mp3file_seek(): %ld samples remaining", samples - seekResult);
if(seekResult > samples + 4800) {
LOGE("mp3file_seek(): error: landed beyond the seek position!");
return seekResult;
}
// try to refine the seek position
int max_iter = 6;
int cur_iter = 0;
for(; cur_iter < max_iter && seekResult < samples - 4800; cur_iter++) {
seekResult = mpg123_seek(mp3file->handle, samples - seekResult, SEEK_CUR);
LOGD(" mp3file_seek(): %ld samples remaining", samples - seekResult);
}
LOGD("mp3file_seek(): finished");
return seekResult;
}

View File

@@ -33,5 +33,6 @@ MP3File* mp3file_init(mpg123_handle *handle);
void mp3file_delete(MP3File *mp3file); void mp3file_delete(MP3File *mp3file);
MP3File* mp3file_open(const char *filename, int forceEncoding = 0); MP3File* mp3file_open(const char *filename, int forceEncoding = 0);
MP3File* mp3file_open_fd(int fd, long long offset = 0, long long length = -1, int forceEncoding = 0); MP3File* mp3file_open_fd(int fd, long long offset = 0, long long length = -1, int forceEncoding = 0);
off_t mp3file_seek(MP3File *mp3file, off_t samples);
#endif //SAMPLES_MP3FILE_H #endif //SAMPLES_MP3FILE_H

View File

@@ -40,6 +40,7 @@ public class LstForegroundService extends Service implements SensorEventListener
public static final String ACTION_START = "at.lockstep.action.START"; public static final String ACTION_START = "at.lockstep.action.START";
public static final String ACTION_STOP = "at.lockstep.action.STOP"; public static final String ACTION_STOP = "at.lockstep.action.STOP";
public static final String ACTION_PAUSE = "at.lockstep.action.PAUSE";
private SensorManager sensorManager; private SensorManager sensorManager;
private Sensor accelerometer; private Sensor accelerometer;
@@ -60,6 +61,11 @@ public class LstForegroundService extends Service implements SensorEventListener
intent.setAction(ACTION_STOP); intent.setAction(ACTION_STOP);
return intent; return intent;
} }
public static Intent pauseIntent(Context context) {
Intent intent = new Intent(context, LstForegroundService.class);
intent.setAction(ACTION_PAUSE);
return intent;
}
@Override @Override
public void onCreate() { public void onCreate() {
@@ -98,7 +104,7 @@ public class LstForegroundService extends Service implements SensorEventListener
String contentUri = intent.getStringExtra("content_uri"); String contentUri = intent.getStringExtra("content_uri");
try { try {
if(contentUri != null) { if(contentUri != null) {
PlaybackEngine.playMusic(uriToFd(contentUri)); PlaybackEngine.playMusic(uriToFd(contentUri), this, R.raw.track_beat);
} }
} catch (IOException e) { } catch (IOException e) {
// TODO proper error handling // TODO proper error handling
@@ -109,6 +115,10 @@ public class LstForegroundService extends Service implements SensorEventListener
} else if (ACTION_STOP.equals(action)) { } else if (ACTION_STOP.equals(action)) {
Log.i("LstForegroundService", "ACTION_STOP"); Log.i("LstForegroundService", "ACTION_STOP");
stopCollectionAndSelf(); stopCollectionAndSelf();
PlaybackEngine.stop();
} else if (ACTION_PAUSE.equals(action)) {
Log.i("LstForegroundService", "ACTION_PAUSE");
PlaybackEngine.pause();
} }
} }
return START_STICKY; return START_STICKY;

View File

@@ -32,6 +32,7 @@ import java.io.Writer;
public class MainActivity extends AppCompatActivity implements LstForegroundService.OnResultListener { public class MainActivity extends AppCompatActivity implements LstForegroundService.OnResultListener {
private Button btnStart; private Button btnStart;
private Button btnStop; private Button btnStop;
private Button btnPause;
private Button btnMediaStoreBenchmark; private Button btnMediaStoreBenchmark;
private Button btnPickSong; private Button btnPickSong;
private final ActivityResultLauncher<Intent> launcher; private final ActivityResultLauncher<Intent> launcher;
@@ -61,6 +62,7 @@ public class MainActivity extends AppCompatActivity implements LstForegroundServ
btnStart = findViewById(R.id.btnStart); btnStart = findViewById(R.id.btnStart);
btnStop = findViewById(R.id.btnStop); btnStop = findViewById(R.id.btnStop);
btnPause = findViewById(R.id.btnPause);
btnMediaStoreBenchmark = findViewById(R.id.btnMediaStoreBenchmark); btnMediaStoreBenchmark = findViewById(R.id.btnMediaStoreBenchmark);
btnPickSong = findViewById(R.id.btnPickSong); btnPickSong = findViewById(R.id.btnPickSong);
@@ -83,6 +85,10 @@ public class MainActivity extends AppCompatActivity implements LstForegroundServ
startService(LstForegroundService.stopIntent(MainActivity.this)) startService(LstForegroundService.stopIntent(MainActivity.this))
); );
btnPause.setOnClickListener(v ->
startService(LstForegroundService.pauseIntent(MainActivity.this))
);
btnMediaStoreBenchmark.setOnClickListener(v -> { btnMediaStoreBenchmark.setOnClickListener(v -> {
Intent intent = new Intent(MainActivity.this, MediaStoreBenchmarkActivity.class); Intent intent = new Intent(MainActivity.this, MediaStoreBenchmarkActivity.class);
startActivity(intent); startActivity(intent);

View File

@@ -76,16 +76,38 @@ public class PlaybackEngine {
return mEngineHandle; return mEngineHandle;
} }
public static void playMusic(int fd) { public static void playMusic(int fd, Context context, int beat_resid) {
if (mEngineHandle == 0) {
create(context, beat_resid);
}
if (mEngineHandle != 0) { if (mEngineHandle != 0) {
native_playMusic(mEngineHandle, fd); native_playMusic(mEngineHandle, fd);
} }
} }
public static void stop() {
if (mEngineHandle != 0) {
//native_stop(mEngineHandle);
// for now, (pause is bugged), deleting engine is the only clean way
native_deleteEngine(mEngineHandle);
mEngineHandle = 0;
}
}
public static void pause() {
if (mEngineHandle != 0) {
Log.i("PlaybackEngine", "native_pause() ...");
native_pause(mEngineHandle);
Log.i("PlaybackEngine", "native_pause() done.");
}
}
private static native long native_createEngine(String filesDir, int resid); private static native long native_createEngine(String filesDir, int resid);
private static native void native_deleteEngine(long engineHandle); private static native void native_deleteEngine(long engineHandle);
private static native void native_setDefaultStreamValues(int sampleRate, int framesPerBurst); private static native void native_setDefaultStreamValues(int sampleRate, int framesPerBurst);
private static native int native_mpg123_init(); private static native int native_mpg123_init();
private static native void native_playMusic(long engineHandle, int fd); private static native void native_playMusic(long engineHandle, int fd);
private static native void native_stop(long engineHandle);
private static native void native_pause(long engineHandle);
} }

View File

@@ -9,6 +9,8 @@ public interface PasadaPlaybackListener {
/** Current track reached end; service should advance queue and call {@link LibPasada#play}. */ /** Current track reached end; service should advance queue and call {@link LibPasada#play}. */
void onTrackFinished(); void onTrackFinished();
void onTrackClosed(int fd);
/** Decode / Oboe / pipeline failure; service should stop safely and surface error. */ /** Decode / Oboe / pipeline failure; service should stop safely and surface error. */
void onError(int errorCode, String message); void onError(int errorCode, String message);
} }

View File

@@ -19,6 +19,13 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="Stop collection" /> android:text="Stop collection" />
<Button
android:id="@+id/btnPause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Pause" />
<Button <Button
android:id="@+id/btnMediaStoreBenchmark" android:id="@+id/btnMediaStoreBenchmark"
android:layout_width="wrap_content" android:layout_width="wrap_content"