Compare commits

...

2 Commits

11 changed files with 452 additions and 74 deletions

View File

@@ -24,6 +24,7 @@ android {
//path 'src/main/cpp/CMakeLists.txt' //path 'src/main/cpp/CMakeLists.txt'
//cppFlags '' //cppFlags ''
arguments "-DANDROID_STL=c++_shared" arguments "-DANDROID_STL=c++_shared"
cppFlags "-DLIBPASADA_VERSION=1.0.4"
//cppFlags "-std=c++14" //cppFlags "-std=c++14"
//arguments '-DANDROID_STL=c++_static' //arguments '-DANDROID_STL=c++_static'

View File

@@ -1,4 +1,6 @@
#include "LibPasada.h" #include "LibPasada.h"
#include "PlaybackEngine.h"
#include "step_detector.h"
#include <string> #include <string>
/** 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}. */
@@ -29,21 +31,55 @@ void PasadaPlaybackListener::onError(int errorCode, std::string message) {
* <p>Native state machine: LOADED → INITIALIZED → PLAYING ↔ PAUSED → FINISHED → STOPPED * <p>Native state machine: LOADED → INITIALIZED → PLAYING ↔ PAUSED → FINISHED → STOPPED
*/ */
LibPasada::LibPasada() : state(LOADED) LibPasada::LibPasada() : state(LOADED), engine(nullptr), detector(nullptr)
{} {}
LibPasada::~LibPasada() {} /** We must clean up here whatever is left over when init() or stop() fails. */
LibPasada::~LibPasada() {
// delete StepDetector first, lest it spuriously pushes StepListener events
std::lock_guard<std::mutex> lock(mtxDetector);
if (detector != nullptr) {
auto *stepDetector = reinterpret_cast<StepDetector *>(detector);
delete stepDetector;
detector = nullptr;
}
// init(): if new StepDetector() fails, we have a leftover 'engine'
if (engine != nullptr) {
auto *playbackEngine = reinterpret_cast<PlaybackEngine *>(engine);
delete playbackEngine;
engine = nullptr;
}
}
/** Called each time a run/playlist session starts (Oboe silent, buffers ready). */ /** Called each time a run/playlist session starts (Oboe silent, buffers ready). */
void LibPasada::init() { void LibPasada::init() {
std::lock_guard<std::mutex> lockState(mtxState);
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);
engine = playbackEngine;
auto *stepListener = reinterpret_cast<StepListener *>(playbackEngine);
// the hardcoded 'fps' is only an initial value, which is re-computed in StepDetector
auto *stepDetector = new StepDetector(60.0 /* FPS */, stepListener);
detector = stepDetector;
state = INITIALIZED; state = INITIALIZED;
} }
} }
/** Submit one accelerometer sample (m/s²); may be called from a sensor thread. */ /** 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) {} void LibPasada::feedAccel(float x, float y, float z, long long timestamp_nanos) {
std::lock_guard<std::mutex> lockState(mtxState);
std::lock_guard<std::mutex> lock(mtxDetector);
if(state == LOADED || state == STOPPED) {
// player is not in an initialized state - ignore accelerometer data
return;
}
auto *stepDetector = reinterpret_cast<StepDetector *>(detector);
stepDetector->filter(static_cast<double>(timestamp_nanos), std::vector<float> {x, y, z});
}
/** /**
* Open MP3 from an already-open file descriptor and begin adaptive playback. * Open MP3 from an already-open file descriptor and begin adaptive playback.
@@ -53,43 +89,106 @@ void LibPasada::feedAccel(float x, float y, float z, long long timestamp_nanos)
* @param length byte length from offset ({@code -1} if unknown / to EOF) * @param length byte length from offset ({@code -1} if unknown / to EOF)
*/ */
void LibPasada::play(int fd, long long offset, long long length) { void LibPasada::play(int fd, long long offset, long long length) {
std::lock_guard<std::mutex> lockState(mtxState);
state = PLAYING; state = PLAYING;
auto *playbackEngine = reinterpret_cast<PlaybackEngine *>(engine);
playbackEngine->playMusic(fd, offset, length);
} }
/** PLAYING → PAUSED (silent output, graph kept alive). */ /** PLAYING → PAUSED (silent output, graph kept alive). */
void LibPasada::pause() { void LibPasada::pause() {
std::lock_guard<std::mutex> lockState(mtxState);
if(state != PLAYING) return; if(state != PLAYING) return;
state = PAUSED; state = PAUSED;
auto *playbackEngine = reinterpret_cast<PlaybackEngine *>(engine);
playbackEngine->pause();
} }
/** PAUSED → PLAYING (same track, same decode position, same FD). */ /** PAUSED → PLAYING (same track, same decode position, same FD). */
void LibPasada::resume() { void LibPasada::resume() {
std::lock_guard<std::mutex> lockState(mtxState);
if(state != PAUSED) return; if(state != PAUSED) return;
state = PLAYING; state = PLAYING;
auto *playbackEngine = reinterpret_cast<PlaybackEngine *>(engine);
playbackEngine->resume();
} }
/** Tear down Oboe for this run segment → STOPPED. */ /** Tear down Oboe for this run segment → STOPPED. */
void LibPasada::stop() { void LibPasada::stop() {
std::lock_guard<std::mutex> lockState(mtxState);
state = STOPPED; state = STOPPED;
auto *playbackEngine = reinterpret_cast<PlaybackEngine *>(engine);
playbackEngine->stop();
} }
/** /**
* Milliseconds from the start of the current track — same timebase as ExoPlayer * Milliseconds from the start of the current track — same timebase as ExoPlayer
* {@code getCurrentPosition()}. Safe to poll from background threads. * {@code getCurrentPosition()}. Safe to poll from background threads.
*/ */
long LibPasada::getCurrentPositionMs() { return 0; } long LibPasada::getCurrentPositionMs() {
std::lock_guard<std::mutex> lockState(mtxState);
if(state == LOADED)
return 0;
auto *playbackEngine = reinterpret_cast<PlaybackEngine *>(engine);
return static_cast<long>(playbackEngine->getCurrentPosition() * 1000);
}
/** Track duration in ms, or {@code 0} if not yet known. */ /** Track duration in ms, or {@code 0} if not yet known. */
long LibPasada::getDurationMs() { return 0; } long LibPasada::getDurationMs() {
std::lock_guard<std::mutex> lockState(mtxState);
if(state == LOADED)
return 0;
auto *playbackEngine = reinterpret_cast<PlaybackEngine *>(engine);
return static_cast<long>(playbackEngine->getDuration() * 1000);
}
// defined in jni_libpasada.cpp
void emitTrackFinished();
void LibPasada::onTrackFinished() {
{
std::lock_guard<std::mutex> lockState(mtxState);
if (state != PLAYING)
return; // this should never happen
state = FINISHED;
}
emitTrackFinished();
}
/** Whether adapted audio is actively being output (not paused, not finished). */ /** Whether adapted audio is actively being output (not paused, not finished). */
bool LibPasada::isPlaying() { return false; } bool LibPasada::isPlaying() {
std::lock_guard<std::mutex> lockState(mtxState);
return state == PLAYING;
}
/** Current native state; see {@link PasadaState}. */ /** Current native state; see {@link PasadaState}. Must not throw. */
int LibPasada::getState() { return state; } int LibPasada::getState() {
std::lock_guard<std::mutex> lockState(mtxState);
return state;
}
/** Seek within the current track. */ /** Seek within the current track. */
void LibPasada::seekTo(long positionMs) {} void LibPasada::seekTo(long positionMs) {
std::lock_guard<std::mutex> lockState(mtxState);
if(state != PLAYING && state != PAUSED)
return; // silently ignore seeks if we do not have a music file loaded
/** Runtime metrics / last error string for logging and debug UI. */ auto *playbackEngine = reinterpret_cast<PlaybackEngine *>(engine);
playbackEngine->seekTo(positionMs / 1000.0);
}
/** Runtime metrics / last error string for logging and debug UI. Must not throw. */
std::string LibPasada::getDiagnostics() { return ""; } std::string LibPasada::getDiagnostics() { return ""; }
/** Register listener for async events raised from the audio/native thread. */ /** Register listener for async events raised from the audio/native thread. */
//void LibPasada::setPlaybackListener(PasadaPlaybackListener listener) {} // JNI-side only //void LibPasada::setPlaybackListener(PasadaPlaybackListener listener) {} // JNI-side only
#define STRINGIFY(s) #s
std::string LibPasada::getVersion() {
#ifdef LIBPASADA_VERSION
return STRINGIFY(LIBPASADA_VERSION);
#else
return "0.0.1";
#endif
}

View File

@@ -5,24 +5,14 @@
#ifndef LOCKSTEP_LIBPASADA_H #ifndef LOCKSTEP_LIBPASADA_H
#define LOCKSTEP_LIBPASADA_H #define LOCKSTEP_LIBPASADA_H
#include "PasadaPlaybackListener.h"
#include <string> #include <string>
#include <mutex>
// 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);
/**
* 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. * Mirrors the libpasada state machine documented in DESIGN.md.
* Keep values in sync with PasadaState.java * Keep values in sync with PasadaState.java
@@ -40,14 +30,18 @@ enum PasadaState {
* JNI entry point for libpasada. * JNI entry point for libpasada.
* <p>Native state machine: LOADED → INITIALIZED → PLAYING ↔ PAUSED → FINISHED → STOPPED * <p>Native state machine: LOADED → INITIALIZED → PLAYING ↔ PAUSED → FINISHED → STOPPED
*/ */
class LibPasada { class LibPasada : public PasadaPlaybackListener {
private: private:
PasadaState state; PasadaState state;
std::mutex mtxState;
//PasadaPlaybackListener listener; //PasadaPlaybackListener listener;
void *engine;
void *detector;
std::mutex mtxDetector;
public: public:
LibPasada(); LibPasada();
~LibPasada(); virtual ~LibPasada();
/** Called each time a run/playlist session starts (Oboe silent, buffers ready). */ /** Called each time a run/playlist session starts (Oboe silent, buffers ready). */
void init(); void init();
@@ -82,17 +76,22 @@ public:
/** Whether adapted audio is actively being output (not paused, not finished). */ /** Whether adapted audio is actively being output (not paused, not finished). */
bool isPlaying(); bool isPlaying();
/** Current native state; see {@link PasadaState}. */ /** Current native state; see {@link PasadaState}. Must not throw. */
int getState(); int getState();
/** Seek within the current track. */ /** Seek within the current track. */
void seekTo(long positionMs); void seekTo(long positionMs);
/** Runtime metrics / last error string for logging and debug UI. */ /** Runtime metrics / last error string for logging and debug UI. Must not throw. */
std::string getDiagnostics(); std::string getDiagnostics();
/** Register listener for async events raised from the audio/native thread. */ /** Register listener for async events raised from the audio/native thread. */
//void setPlaybackListener(PasadaPlaybackListener listener); // JNI-side only //void setPlaybackListener(PasadaPlaybackListener listener); // JNI-side only
/** Current track reached end; service should advance queue and call {@link LibPasada#play}. */
void onTrackFinished() override;
std::string getVersion();
}; };
#endif //LOCKSTEP_LIBPASADA_H #endif //LOCKSTEP_LIBPASADA_H

View File

@@ -0,0 +1,24 @@
//
// Created by david on 02.06.2026.
//
#ifndef LOCKSTEP_PASADAPLAYBACKLISTENER_H
#define LOCKSTEP_PASADAPLAYBACKLISTENER_H
#include <string>
/**
* Callbacks invoked from native (Oboe or internal worker thread).
*/
class PasadaPlaybackListener {
public:
PasadaPlaybackListener() = default;
virtual ~PasadaPlaybackListener() = default;
/** Current track reached end; service should advance queue and call {@link LibPasada#play}. */
virtual void onTrackFinished();
/** Decode / Oboe / pipeline failure; service should stop safely and surface error. */
virtual void onError(int errorCode, std::string message);
};
#endif //LOCKSTEP_PASADAPLAYBACKLISTENER_H

View File

@@ -11,6 +11,8 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <chrono> #include <chrono>
#include <sys/types.h>
#include <unistd.h>
/** /**
* 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.
@@ -67,7 +69,8 @@ struct RbLogger : public RubberBand::RubberBandStretcher::Logger {
virtual ~RbLogger() { } virtual ~RbLogger() { }
}; };
PlaybackEngine::PlaybackEngine(std::string filesDir, int resid): PlaybackEngine::PlaybackEngine(std::string filesDir, int resid, PasadaPlaybackListener *listener):
listener(listener),
stretcher( stretcher(
/* sampleRate: */ 48000, /* sampleRate: */ 48000,
/* channels: */ 2, /* channels: */ 2,
@@ -80,6 +83,11 @@ PlaybackEngine::PlaybackEngine(std::string filesDir, int resid):
haveMusicFile(false), haveMusicFile(false),
exitMusicFeedThread(false), exitMusicFeedThread(false),
isSetMusic(false), isSetMusic(false),
isPaused(false),
markerPos(0.0),
duration(0.0),
seekPos(0.0),
requestSeek(false),
android_fd(0), android_fd(0),
haveTimeRatio(false), haveTimeRatio(false),
timeRatio(1.0), timeRatio(1.0),
@@ -96,8 +104,13 @@ PlaybackEngine::PlaybackEngine(std::string filesDir, int resid):
// load "bump" sound effect // load "bump" sound effect
std::vector<float> samples; std::vector<float> samples;
bool is_ok = read_mp3(mFilesDir + "/" + std::to_string(resid) + ".mp3", samples); if (!filesDir.empty()) {
LOGI("read_mp3() for bump effect, is_ok=%d", is_ok); bool is_ok = read_mp3(mFilesDir + "/" + std::to_string(resid) + ".mp3", samples);
LOGI("read_mp3() for bump effect, is_ok=%d", is_ok);
} else {
samples.resize(1);
LOGI("keeping bump effect silent, since have no filesDir");
}
mPlayer = new MixingPlayer(samples); mPlayer = new MixingPlayer(samples);
@@ -284,7 +297,7 @@ void PlaybackEngine::musicFeedThread() {
num_samples = num_buf_samples; num_samples = num_buf_samples;
} }
if (!haveMusicFile.load()) { if (!haveMusicFile.load() || isPaused.load()) {
loop_delay_us = 1000000 * num_samples / playbackRate.load(); loop_delay_us = 1000000 * num_samples / playbackRate.load();
if(idebug++ < 10) { if(idebug++ < 10) {
LOGI("feed %d silence samples", num_samples); LOGI("feed %d silence samples", num_samples);
@@ -304,11 +317,37 @@ void PlaybackEngine::musicFeedThread() {
// ca. 21 ms // ca. 21 ms
} }
markerPos.store((musicFile->num_samples - musicFile->remaining_samples) / musicFile->samples_per_frame * musicFile->secs_per_frame);
duration.store(musicFile->duration);
if(requestSeek.load()) {
double pos = seekPos.load();
// 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);
seek_samples = std::max(seek_samples, 0L);
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
off_t seekResult = mpg123_seek(musicFile->handle, musicFile->num_samples - musicFile->remaining_samples, SEEK_SET);
if(seekResult != seek_samples) {
LOGE("error seeking in mp3 file: mpg123_seek(handle, %d, SEEK_SET)=%d", seek_samples, seekResult);
}
// update structure
musicFile->remaining_samples = static_cast<int>(musicFile->num_samples - seek_samples);
musicFile->offset += seek_bytes_delta;
requestSeek.store(false);
}
size_t done = 0; // bytes! size_t done = 0; // bytes!
size_t read_size_bytes = std::min(num_samples * num_ch_in * sizeof(int16_t), cbuf_size_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 = 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); }
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; }
musicFile->remaining_samples -= done / sizeof(int16_t); musicFile->remaining_samples -= done / sizeof(int16_t);
musicFile->offset = 0; // unused here musicFile->offset += 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);
@@ -322,12 +361,10 @@ void PlaybackEngine::musicFeedThread() {
} }
if(err == MPG123_DONE) { if(err == MPG123_DONE) {
// next iteration will play silence // next iteration will play silence
// we keep Stretcher and Oboe alive
LOGI("finished reading mp3 file (MPG123_DONE)"); LOGI("finished reading mp3 file (MPG123_DONE)");
closeMusicFile(); closeMusicFile();
stretcher.setTimeRatio(1.0); // buffer size for playing silence is computed from 'playbackRate', so reset timeRatio if(listener) listener->onTrackFinished();
stretcher.setPitchScale(1.0);
stretcher.process(buf_ptr, 0, true); // set end of playback
mPlayer->stopAudio();
continue; continue;
} }
@@ -356,23 +393,63 @@ void PlaybackEngine::musicFeedThread() {
LOGI("musicFeedThread() exited."); LOGI("musicFeedThread() exited.");
} }
void PlaybackEngine::pause() {
// next iteration will play silence
isPaused.store(true);
}
void PlaybackEngine::resume() {
// next iteration will continue to decode
isPaused.store(false);
}
double PlaybackEngine::getCurrentPosition() {
return markerPos.load();
}
double PlaybackEngine::getDuration() {
return duration.load();
}
void PlaybackEngine::seekTo(double markerPosSec) {
seekPos.store(markerPosSec);
requestSeek.store(true);
}
void PlaybackEngine::stop() {
LOGI("PlaybackEngine::stop()");
closeRubberBand();
if (mPlayer) {
mPlayer->stopAudio();
delete mPlayer;
mPlayer = nullptr;
}
markerPos.store(0.0);
duration.store(0.0);
}
PlaybackEngine::~PlaybackEngine() { PlaybackEngine::~PlaybackEngine() {
LOGI("~PlaybackEngine()"); LOGI("~PlaybackEngine()");
closeRubberBand(); closeRubberBand();
mPlayer->stopAudio(); if (mPlayer) {
delete mPlayer; mPlayer->stopAudio();
mPlayer = nullptr; delete mPlayer;
mPlayer = nullptr;
}
} }
void PlaybackEngine::playBeat() { void PlaybackEngine::playBeat() {
if(mPlayer) mPlayer->setStartBeat(); if(mPlayer) mPlayer->setStartBeat();
} }
void PlaybackEngine::playMusic(int fd) { void PlaybackEngine::playMusic(int fd, long long offset, long long length) {
if(!mPlayer) return; if(!mPlayer) return;
LOGI("PlaybackEngine::playMusic(fd=%d)", fd); LOGI("PlaybackEngine::playMusic(fd=%d)", fd);
if(musicFile) {
// when changing tracks in the UI, close the previous file
closeMusicFile();
}
android_fd = fd; android_fd = fd;
musicFile.reset(mp3file_open_fd(android_fd, 0)); musicFile.reset(mp3file_open_fd(android_fd, offset, length, 0));
if(musicFile) { if(musicFile) {
timeRatio.store(((double) playbackRate.load()) / ((double) musicFile->rate)); timeRatio.store(((double) playbackRate.load()) / ((double) musicFile->rate));
haveTimeRatio.store(true); haveTimeRatio.store(true);
@@ -382,6 +459,7 @@ void PlaybackEngine::playMusic(int fd) {
bool is_finished = (stretcher.available() == -1); bool is_finished = (stretcher.available() == -1);
if(is_finished) { if(is_finished) {
LOGE("stretcher.available() == -1, this should not happen anymore with current wiring");
// so that we may play again after "final chunk" // so that we may play again after "final chunk"
closeRubberBand(); closeRubberBand();
initRubberBand(); initRubberBand();

View File

@@ -5,11 +5,12 @@
#ifndef LOCKSTEP_PLAYBACKENGINE_H #ifndef LOCKSTEP_PLAYBACKENGINE_H
#define LOCKSTEP_PLAYBACKENGINE_H #define LOCKSTEP_PLAYBACKENGINE_H
#include "StepListener.h" #include "step_detector.h"
#include "MixingPlayer.h" #include "MixingPlayer.h"
#include "RubberBandStretcher.h" #include "RubberBandStretcher.h"
#include "mp3file.h" #include "mp3file.h"
#include "AudioCallback.h" #include "AudioCallback.h"
#include "PasadaPlaybackListener.h"
#include <string> #include <string>
#include <thread> #include <thread>
#include <memory> #include <memory>
@@ -36,12 +37,28 @@ private:
class PlaybackEngine : public StepListener { class PlaybackEngine : public StepListener {
public: public:
PlaybackEngine(std::string filesDir, int resid); /**
* @param filesDir resources directory, used only to fetch the beat sound template
* @param resid beat sound template file
* @param listener contract: onTrackFinished() only fires during regular playback finished - not when stopped, when exiting, etc.
*/
PlaybackEngine(std::string filesDir, int resid, PasadaPlaybackListener *listener = nullptr);
virtual ~PlaybackEngine(); virtual ~PlaybackEngine();
/** Play a beat sound. */ /** Play a beat sound. */
virtual void playBeat(); virtual void playBeat();
void playMusic(int fd); /** pass -1 to length for read until eof. */
void playMusic(int fd, long long offset = 0, long long length = -1);
void pause();
void resume();
void stop();
/** read marker in the music file in sec */
double getCurrentPosition();
/** duration of the music file in sec */
double getDuration();
/** seek to a marker in the music file in sec */
void seekTo(double markerPos);
private: private:
PasadaPlaybackListener *listener;
RubberBand::RubberBandStretcher stretcher; RubberBand::RubberBandStretcher stretcher;
MixingPlayer *mPlayer; MixingPlayer *mPlayer;
std::string mFilesDir; std::string mFilesDir;
@@ -49,8 +66,17 @@ private:
std::atomic<bool> haveMusicFile; std::atomic<bool> haveMusicFile;
std::unique_ptr<std::thread> musicFeed; std::unique_ptr<std::thread> musicFeed;
std::atomic<bool> exitMusicFeedThread; std::atomic<bool> exitMusicFeedThread;
/** where musicFeedThread() keeps track of the fact that we have music set -- will start the audio cb */ /** where musicFeedThread() keeps track of the fact that we have music set -- will start the audio callback */
std::atomic<bool> isSetMusic; std::atomic<bool> isSetMusic;
std::atomic<bool> isPaused;
/** read marker in the music file in sec */
std::atomic<double> markerPos;
/** duration of the music file in sec */
std::atomic<double> duration;
/** seek position request in the music file in sec, valid during seek requests */
std::atomic<double> seekPos;
/** whether a seek request is active */
std::atomic<bool> requestSeek;
int android_fd; int android_fd;
std::atomic<bool> haveTimeRatio; std::atomic<bool> haveTimeRatio;
std::atomic<double> timeRatio; std::atomic<double> timeRatio;

View File

@@ -1,14 +0,0 @@
//
// Created by david on 03.03.2026.
//
#ifndef LOCKSTEP_STEPLISTENER_H
#define LOCKSTEP_STEPLISTENER_H
class StepListener {
public:
virtual ~StepListener() {}
virtual void playBeat() = 0;
};
#endif //LOCKSTEP_STEPLISTENER_H

View File

@@ -30,8 +30,21 @@ Java_at_lockstep_player_pasada_LibPasada_init(
clearListener(env); clearListener(env);
delete g_libpasada; delete g_libpasada;
} }
g_libpasada = new LibPasada(); try {
g_libpasada->init(); g_libpasada = new LibPasada();
g_libpasada->init();
} catch (const std::bad_alloc&) {
jclass cls = env->FindClass("java/lang/OutOfMemoryError");
if (cls) env->ThrowNew(cls, "native allocation failed");
} catch (const std::exception& e) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, e.what());
} catch (...) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, "unknown native exception");
}
delete g_libpasada;
g_libpasada = nullptr;
} }
/** Submit one accelerometer sample (m/s^2); may be called from a sensor thread. */ /** Submit one accelerometer sample (m/s^2); may be called from a sensor thread. */
@@ -41,7 +54,22 @@ Java_at_lockstep_player_pasada_LibPasada_feedAccel(JNIEnv *env, jclass clazz, jf
jfloat z, jlong timestamp_nanos) { jfloat z, jlong timestamp_nanos) {
if (g_libpasada == nullptr) if (g_libpasada == nullptr)
return; return;
g_libpasada->feedAccel(x, y, z, timestamp_nanos);
try {
g_libpasada->feedAccel(x, y, z, timestamp_nanos);
} catch (const std::bad_alloc&) {
jclass cls = env->FindClass("java/lang/OutOfMemoryError");
if (cls) env->ThrowNew(cls, "native allocation failed");
return;
} catch (const std::exception& e) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, e.what());
return;
} catch (...) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, "unknown native exception");
return;
}
} }
/** /**
@@ -57,24 +85,70 @@ Java_at_lockstep_player_pasada_LibPasada_play(JNIEnv *env, jclass clazz, jint fd
jlong length) { jlong length) {
if (g_libpasada == nullptr) if (g_libpasada == nullptr)
return; return;
g_libpasada->play(fd, offset, length);
try {
g_libpasada->play(fd, offset, length);
} catch (const std::bad_alloc&) {
jclass cls = env->FindClass("java/lang/OutOfMemoryError");
if (cls) env->ThrowNew(cls, "native allocation failed");
return;
} catch (const std::exception& e) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, e.what());
return;
} catch (...) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, "unknown native exception");
return;
}
} }
/** PLAYING → PAUSED (silent output, graph kept alive). */ /** PLAYING → PAUSED (silent output, graph kept alive). */
extern "C" extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_at_lockstep_player_pasada_LibPasada_pause(JNIEnv *env, jclass clazz) { Java_at_lockstep_player_pasada_LibPasada_pause(JNIEnv *env, jclass clazz) {
if (g_libpasada == nullptr) if (g_libpasada == nullptr)
return; return;
g_libpasada->pause(); try {
g_libpasada->pause();
} catch (const std::bad_alloc&) {
jclass cls = env->FindClass("java/lang/OutOfMemoryError");
if (cls) env->ThrowNew(cls, "native allocation failed");
return;
} catch (const std::exception& e) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, e.what());
return;
} catch (...) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, "unknown native exception");
return;
}
} }
/** PAUSED → PLAYING (same track, same decode position, same FD). */ /** PAUSED → PLAYING (same track, same decode position, same FD). */
extern "C" extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_at_lockstep_player_pasada_LibPasada_resume(JNIEnv *env, jclass clazz) { Java_at_lockstep_player_pasada_LibPasada_resume(JNIEnv *env, jclass clazz) {
if (g_libpasada == nullptr) if (g_libpasada == nullptr)
return; return;
g_libpasada->resume(); try {
g_libpasada->resume();
} catch (const std::bad_alloc&) {
jclass cls = env->FindClass("java/lang/OutOfMemoryError");
if (cls) env->ThrowNew(cls, "native allocation failed");
return;
} catch (const std::exception& e) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, e.what());
return;
} catch (...) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, "unknown native exception");
return;
}
} }
/** Tear down Oboe for this run segment → STOPPED. */ /** Tear down Oboe for this run segment → STOPPED. */
extern "C" extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
@@ -82,7 +156,18 @@ Java_at_lockstep_player_pasada_LibPasada_stop(JNIEnv *env, jclass clazz) {
clearListener(env); clearListener(env);
if (g_libpasada == nullptr) if (g_libpasada == nullptr)
return; return;
g_libpasada->stop(); try {
g_libpasada->stop();
} catch (const std::bad_alloc&) {
jclass cls = env->FindClass("java/lang/OutOfMemoryError");
if (cls) env->ThrowNew(cls, "native allocation failed");
} catch (const std::exception& e) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, e.what());
} catch (...) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, "unknown native exception");
}
delete g_libpasada; delete g_libpasada;
g_libpasada = nullptr; g_libpasada = nullptr;
} }
@@ -96,23 +181,67 @@ JNIEXPORT jlong JNICALL
Java_at_lockstep_player_pasada_LibPasada_getCurrentPositionMs(JNIEnv *env, jclass clazz) { Java_at_lockstep_player_pasada_LibPasada_getCurrentPositionMs(JNIEnv *env, jclass clazz) {
if (g_libpasada == nullptr) if (g_libpasada == nullptr)
return 0; return 0;
return g_libpasada->getCurrentPositionMs(); try {
return g_libpasada->getCurrentPositionMs();
} catch (const std::bad_alloc&) {
jclass cls = env->FindClass("java/lang/OutOfMemoryError");
if (cls) env->ThrowNew(cls, "native allocation failed");
return 0;
} catch (const std::exception& e) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, e.what());
return 0;
} catch (...) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, "unknown native exception");
return 0;
}
} }
/** Track duration in ms, or {@code 0} if not yet known. */ /** Track duration in ms, or {@code 0} if not yet known. */
extern "C" extern "C"
JNIEXPORT jlong JNICALL JNIEXPORT jlong JNICALL
Java_at_lockstep_player_pasada_LibPasada_getDurationMs(JNIEnv *env, jclass clazz) { Java_at_lockstep_player_pasada_LibPasada_getDurationMs(JNIEnv *env, jclass clazz) {
if (g_libpasada == nullptr) if (g_libpasada == nullptr)
return 0; return 0;
return g_libpasada->getDurationMs(); try {
return g_libpasada->getDurationMs();
} catch (const std::bad_alloc&) {
jclass cls = env->FindClass("java/lang/OutOfMemoryError");
if (cls) env->ThrowNew(cls, "native allocation failed");
return 0;
} catch (const std::exception& e) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, e.what());
return 0;
} catch (...) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, "unknown native exception");
return 0;
}
} }
/** Whether adapted audio is actively being output (not paused, not finished). */ /** Whether adapted audio is actively being output (not paused, not finished). */
extern "C" extern "C"
JNIEXPORT jboolean JNICALL JNIEXPORT jboolean JNICALL
Java_at_lockstep_player_pasada_LibPasada_isPlaying(JNIEnv *env, jclass clazz) { Java_at_lockstep_player_pasada_LibPasada_isPlaying(JNIEnv *env, jclass clazz) {
if (g_libpasada == nullptr) if (g_libpasada == nullptr)
return false; return false;
return g_libpasada->isPlaying(); try {
return g_libpasada->isPlaying();
} catch (const std::bad_alloc&) {
jclass cls = env->FindClass("java/lang/OutOfMemoryError");
if (cls) env->ThrowNew(cls, "native allocation failed");
return false;
} catch (const std::exception& e) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, e.what());
return false;
} catch (...) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, "unknown native exception");
return false;
}
} }
/** Current native state; see {@link PasadaState}. */ /** Current native state; see {@link PasadaState}. */
extern "C" extern "C"
@@ -120,15 +249,32 @@ JNIEXPORT jint JNICALL
Java_at_lockstep_player_pasada_LibPasada_getState(JNIEnv *env, jclass clazz) { Java_at_lockstep_player_pasada_LibPasada_getState(JNIEnv *env, jclass clazz) {
if(g_libpasada == nullptr) if(g_libpasada == nullptr)
return STOPPED; return STOPPED;
// explicitly no C++/JNI exception translation - keep getState() simple!
// there is no reasonable state for us to return here otherwise.
return g_libpasada->getState(); return g_libpasada->getState();
} }
/** Seek within the current track. */ /** Seek within the current track. */
extern "C" extern "C"
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_at_lockstep_player_pasada_LibPasada_seekTo(JNIEnv *env, jclass clazz, jlong position_ms) { Java_at_lockstep_player_pasada_LibPasada_seekTo(JNIEnv *env, jclass clazz, jlong position_ms) {
if (g_libpasada == nullptr) if (g_libpasada == nullptr)
return; return;
g_libpasada->seekTo((long) position_ms); try {
g_libpasada->seekTo((long) position_ms);
} catch (const std::bad_alloc&) {
jclass cls = env->FindClass("java/lang/OutOfMemoryError");
if (cls) env->ThrowNew(cls, "native allocation failed");
return;
} catch (const std::exception& e) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, e.what());
return;
} catch (...) {
jclass cls = env->FindClass("java/lang/RuntimeException");
if (cls) env->ThrowNew(cls, "unknown native exception");
return;
}
} }
/** Runtime metrics / last error string for logging and debug UI. */ /** Runtime metrics / last error string for logging and debug UI. */
extern "C" extern "C"
@@ -203,3 +349,13 @@ void clearListener(JNIEnv* env) {
g_onTrackFinished = nullptr; g_onTrackFinished = nullptr;
g_onError = nullptr; g_onError = nullptr;
} }
extern "C"
JNIEXPORT jstring JNICALL
Java_at_lockstep_player_pasada_LibPasada_getVersion(JNIEnv *env, jclass clazz) {
if (g_libpasada == nullptr) {
return env->NewStringUTF("");
}
std::string version = g_libpasada->getVersion();
return env->NewStringUTF(version.c_str());
}

View File

@@ -15,6 +15,7 @@ MP3File* mp3file_init(mpg123_handle *handle) {
if(mp3file == NULL) return NULL; if(mp3file == NULL) return NULL;
memset(mp3file, 0, sizeof(MP3File)); memset(mp3file, 0, sizeof(MP3File));
mp3file->handle = handle; mp3file->handle = handle;
mp3file->num_bytes = -1;
return mp3file; return mp3file;
} }
@@ -96,7 +97,7 @@ on_error:
#undef handleError #undef handleError
} }
MP3File* mp3file_open_fd(int fd, int forceEncoding) { MP3File* mp3file_open_fd(int fd, long long offset, long long length, int forceEncoding) {
const char *errorText = ""; const char *errorText = "";
#define handleError(text) \ #define handleError(text) \
do { \ do { \
@@ -113,6 +114,8 @@ MP3File* mp3file_open_fd(int fd, int forceEncoding) {
MP3File* mp3 = mp3file_init(mh); MP3File* mp3 = mp3file_init(mh);
if(mp3 == NULL) handleError("malloc() failed"); if(mp3 == NULL) handleError("malloc() failed");
lseek(fd, static_cast<off_t>(offset), SEEK_SET);
mp3->num_bytes = length;
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()");
@@ -149,7 +152,7 @@ MP3File* mp3file_open_fd(int fd, int forceEncoding) {
else else
mp3->duration = mp3->num_samples / mp3->samples_per_frame * mp3->secs_per_frame; mp3->duration = mp3->num_samples / mp3->samples_per_frame * mp3->secs_per_frame;
mp3->offset = 0; mp3->offset = lseek(fd, 0, SEEK_CUR);
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;

View File

@@ -22,13 +22,16 @@ struct MP3File
size_t buffer_size; size_t buffer_size;
unsigned char* buffer; unsigned char* buffer;
/** total samples (stereo of 10 frames remaining will have 20 'remaining_samples' here) */ /** total samples (stereo of 10 frames remaining will have 20 'remaining_samples' here) */
int remaining_samples; long remaining_samples;
/** zero if opened using a filename, otherwise bytes offset currently read into 'android_fd' */
size_t offset; size_t offset;
/** number of bytes to read at most from 'android_fd' */
long long num_bytes;
}; };
MP3File* mp3file_init(mpg123_handle *handle); 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, int forceEncoding = 0); MP3File* mp3file_open_fd(int fd, long long offset = 0, long long length = -1, int forceEncoding = 0);
#endif //SAMPLES_MP3FILE_H #endif //SAMPLES_MP3FILE_H

View File

@@ -69,4 +69,7 @@ public final class LibPasada {
/** Register listener for async events raised from the audio/native thread. */ /** Register listener for async events raised from the audio/native thread. */
public static native void setPlaybackListener(PasadaPlaybackListener listener); public static native void setPlaybackListener(PasadaPlaybackListener listener);
/** native version string */
public static native String getVersion();
} }