diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index c5a90ef..be2d297 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -37,6 +37,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED jni_stepdetector.cpp jni_libpasada.cpp LibPasada.cpp + TempoController.cpp #// jni_logging.cpp // same implemented in jni_libpasada.cpp // JNI_OnLoad and JNI_OnUnload ) diff --git a/app/src/main/cpp/LibPasada.cpp b/app/src/main/cpp/LibPasada.cpp index f83b17a..fac0cdd 100644 --- a/app/src/main/cpp/LibPasada.cpp +++ b/app/src/main/cpp/LibPasada.cpp @@ -91,12 +91,13 @@ void LibPasada::feedAccel(float x, float y, float z, long long timestamp_nanos) * @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) + * @param beats beat times of song in sec */ -void LibPasada::play(int fd, long long offset, long long length) { +void LibPasada::play(int fd, long long offset, long long length, std::vector beats) { std::lock_guard lockState(mtxState); state = PLAYING; auto *playbackEngine = reinterpret_cast(engine); - playbackEngine->playMusic(fd, offset, length); + playbackEngine->playMusic(fd, beats, offset, length); } /** PLAYING → PAUSED (silent output, graph kept alive). */ diff --git a/app/src/main/cpp/LibPasada.h b/app/src/main/cpp/LibPasada.h index 0273794..cf8b904 100644 --- a/app/src/main/cpp/LibPasada.h +++ b/app/src/main/cpp/LibPasada.h @@ -8,6 +8,7 @@ #include "PasadaPlaybackListener.h" #include #include +#include // JNI helpers for calling into Java code (PasadaPlaybackListener) void emitTrackFinished(); @@ -56,8 +57,9 @@ public: * @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) + * @param beats beat times of song in sec */ - void play(int fd, long long offset, long long length); + void play(int fd, long long offset, long long length, std::vector beats); /** PLAYING → PAUSED (silent output, graph kept alive). */ void pause(); /** PAUSED → PLAYING (same track, same decode position, same FD). */ diff --git a/app/src/main/cpp/PlaybackEngine.cpp b/app/src/main/cpp/PlaybackEngine.cpp index b19a925..2e6e865 100644 --- a/app/src/main/cpp/PlaybackEngine.cpp +++ b/app/src/main/cpp/PlaybackEngine.cpp @@ -92,6 +92,8 @@ PlaybackEngine::PlaybackEngine(std::string filesDir, int resid, PasadaPlaybackLi android_fd(0), haveTimeRatio(false), timeRatio(1.0), + haveStretchFactor(false), + stretchFactor(1.0), // these 3 values are preliminary -- will be set from MixingPlayer defaults in the ctor body below playbackRate(48000), @@ -114,6 +116,7 @@ PlaybackEngine::PlaybackEngine(std::string filesDir, int resid, PasadaPlaybackLi } mPlayer = new MixingPlayer(samples); + tempoController = new TempoController(&haveStretchFactor, &stretchFactor); // configure stretcher and start musicFeedThread() initRubberBand(); @@ -215,6 +218,7 @@ void PlaybackEngine::musicFeedThread() { std::mt19937 mt(42); std::uniform_real_distribution dist(0.98, 1.0); */ + size_t iBeat = 0, numBeats = 0; bool isOnTrackFinished = false; LOGI("starting musicFeedThread()"); @@ -265,13 +269,15 @@ void PlaybackEngine::musicFeedThread() { if(!isSetMusic.load()) { mPlayer->setMusic(std::make_shared(&stretcher, buf_size_samples, numOutChannels.load(), &back_pressure)); isSetMusic.store(true); + numBeats = beatTimesSec.size(); } - if(haveTimeRatio.load()) { - double ratio = timeRatio.load(); + if(haveTimeRatio.load() || haveStretchFactor.load()) { + double ratio = timeRatio.load() * stretchFactor.load(); stretcher.setTimeRatio(ratio); stretcher.setPitchScale(1.0 / ratio); haveTimeRatio.store(false); + haveStretchFactor.store(false); } // change buffer size, if necessary (changed input channel count) @@ -361,6 +367,11 @@ void PlaybackEngine::musicFeedThread() { markerPos.store((musicFile->num_samples - musicFile->remaining_samples) / musicFile->samples_per_frame * musicFile->secs_per_frame); duration.store(musicFile->duration); + if(!beatTimesSec.empty() && iBeat < numBeats && markerPos.load() > beatTimesSec[iBeat]) { + iBeat++; + if(tempoController) tempoController->onBeat(markerPos.load()); + } + if(requestSeek.load()) { double pos = seekPos.load(); // compute seek target in samples, clip to [0..num_samples] @@ -386,6 +397,10 @@ void PlaybackEngine::musicFeedThread() { //musicFile->num_bytes = -1; // pretend we can read until the end requestSeek.store(false); /*}*/ + + double newMarkerPos = ((musicFile->num_samples - musicFile->remaining_samples) / musicFile->samples_per_frame * musicFile->secs_per_frame); + + if(tempoController) tempoController->seekTo(markerPos.load(), newMarkerPos); } size_t done = 0; // bytes! @@ -466,11 +481,13 @@ void PlaybackEngine::pause() { LOGI("PlaybackEngine::pause() set isPaused."); // next iteration will play silence isPaused.store(true); + if(tempoController) tempoController->pause(); } void PlaybackEngine::resume() { // next iteration will continue to decode isPaused.store(false); + if(tempoController) tempoController->resume(); } double PlaybackEngine::getCurrentPosition() { @@ -494,6 +511,7 @@ void PlaybackEngine::stop() { } markerPos.store(0.0); duration.store(0.0); + if(tempoController) tempoController->pause(); } PlaybackEngine::~PlaybackEngine() { @@ -503,15 +521,18 @@ PlaybackEngine::~PlaybackEngine() { std::lock_guard lock(mMusicLock); closeMusicFile(); } + delete tempoController; + tempoController = nullptr; } -void PlaybackEngine::playBeat() { +void PlaybackEngine::playBeat(double t) { if(mPlayer) mPlayer->setStartBeat(); + if(tempoController) tempoController->onStep(t); } -void PlaybackEngine::playMusic(int fd, long long offset, long long length) { +void PlaybackEngine::playMusic(int fd, std::vector beats, long long offset, long long length) { if(!mPlayer) return; - LOGI("PlaybackEngine::playMusic(fd=%d)", fd); + LOGI("PlaybackEngine::playMusic(fd=%d, beats.size=%d)", fd, (int) beats.size()); { std::lock_guard lock(mMusicLock); @@ -522,6 +543,8 @@ void PlaybackEngine::playMusic(int fd, long long offset, long long length) { // when changing tracks in the UI, close the previous file closeMusicFile(); } + beatTimesSec.assign(beats.begin(), beats.end()); + if(tempoController) tempoController->reset(beatTimesSec); android_fd = fd; if (fd != 0) musicFile.reset(mp3file_open_fd(android_fd, offset, length, 0)); diff --git a/app/src/main/cpp/PlaybackEngine.h b/app/src/main/cpp/PlaybackEngine.h index 4e72c94..164f06c 100644 --- a/app/src/main/cpp/PlaybackEngine.h +++ b/app/src/main/cpp/PlaybackEngine.h @@ -11,10 +11,12 @@ #include "mp3file.h" #include "AudioCallback.h" #include "PasadaPlaybackListener.h" +#include "TempoController.h" #include #include #include #include +#include /** Provides music through a regular callback to oboe. Called from separate oboe thread. */ class MusicProvider : public AudioCallbackProvider { @@ -45,9 +47,9 @@ public: PlaybackEngine(std::string filesDir, int resid, PasadaPlaybackListener *listener = nullptr); virtual ~PlaybackEngine(); /** Play a beat sound. */ - virtual void playBeat(); + virtual void playBeat(double t); /** pass -1 to length for read until eof. */ - void playMusic(int fd, long long offset = 0, long long length = -1); + void playMusic(int fd, std::vector beats, long long offset = 0, long long length = -1); void pause(); void resume(); void stop(); @@ -61,9 +63,11 @@ private: PasadaPlaybackListener *listener; RubberBand::RubberBandStretcher stretcher; MixingPlayer *mPlayer; + TempoController *tempoController; std::string mFilesDir; std::mutex mMusicLock; std::unique_ptr musicFile; + std::vector beatTimesSec; std::atomic haveMusicFile; std::unique_ptr musicFeed; std::atomic exitMusicFeedThread; @@ -80,7 +84,11 @@ private: std::atomic requestSeek; int android_fd; std::atomic haveTimeRatio; + /** time ratio of (playback : recording) fps */ std::atomic timeRatio; + std::atomic haveStretchFactor; + /** live stretch factor based on step rate */ + std::atomic stretchFactor; std::atomic playbackRate; std::atomic numOutChannels; std::atomic numInChannels; diff --git a/app/src/main/cpp/TempoController.cpp b/app/src/main/cpp/TempoController.cpp new file mode 100644 index 0000000..1559415 --- /dev/null +++ b/app/src/main/cpp/TempoController.cpp @@ -0,0 +1,50 @@ +// +// Created by david on 14.06.2026. +// + +#include "TempoController.h" + +TempoController::TempoController(std::atomic *have_stretch_factor, std::atomic *stretch_factor) : + have_stretch_factor(have_stretch_factor), + stretch_factor(stretch_factor), + lastBeatT(0.0), + lastStepT(0.0) +{ + this->have_stretch_factor->store(false); + this->stretch_factor->store(1.0); +} + +void TempoController::reset(std::vector beatTimes) { + std::lock_guard lock(mMutex); + beatTimesSec.assign(beatTimes.begin(), beatTimes.end()); + lastBeatT = (0.0); + lastStepT = (0.0); +} + +void TempoController::resume() {} +void TempoController::pause() {} + +void TempoController::seekTo(double oldMarkerPosSec, double newMarkerPosSec) { + std::lock_guard lock(mMutex); + double dt = newMarkerPosSec - oldMarkerPosSec; + lastBeatT += dt; + lastStepT += dt; +} + +/** + * must be thread-safe since these are called from different threads + * @param t marker position in sec of original music time + */ +void TempoController::onBeat(double t) { + std::lock_guard lock(mMutex); + lastBeatT = t; +} + +/** + * must be thread-safe since these are called from different threads + * @param t time in sec of accelerometer timebase + */ +void TempoController::onStep(double t) { + std::lock_guard lock(mMutex); + lastStepT = t; +} diff --git a/app/src/main/cpp/TempoController.h b/app/src/main/cpp/TempoController.h new file mode 100644 index 0000000..717563b --- /dev/null +++ b/app/src/main/cpp/TempoController.h @@ -0,0 +1,38 @@ +// +// Created by david on 14.06.2026. +// + +#ifndef LOCKSTEP_TEMPOCONTROLLER_H +#define LOCKSTEP_TEMPOCONTROLLER_H + +#include +#include +#include + +class TempoController { +private: + std::mutex mMutex; + std::vector beatTimesSec; + std::atomic *have_stretch_factor; + std::atomic *stretch_factor; + double lastBeatT; + double lastStepT; +public: + TempoController(std::atomic *have_stretch_factor, std::atomic *stretch_factor); + void reset(std::vector beatTimesSec); + void resume(); + void pause(); + void seekTo(double oldMarkerPosSec, double newMarkerPosSec); + /** + * must be thread-safe since these are called from different threads + * @param t marker position in sec of original music time + */ + void onBeat(double t); + /** + * must be thread-safe since these are called from different threads + * @param t marker position in sec of original music time + */ + void onStep(double t); +}; + +#endif //LOCKSTEP_TEMPOCONTROLLER_H diff --git a/app/src/main/cpp/jni_libpasada.cpp b/app/src/main/cpp/jni_libpasada.cpp index 9906eed..22ea47f 100644 --- a/app/src/main/cpp/jni_libpasada.cpp +++ b/app/src/main/cpp/jni_libpasada.cpp @@ -5,6 +5,7 @@ #include #include "LibPasada.h" #include "logging.h" +#include static JavaVM* g_vm = nullptr; static jobject g_listener = nullptr; // global ref, or nullptr @@ -110,13 +111,21 @@ Java_at_lockstep_player_pasada_LibPasada_feedAccel(JNIEnv *env, jclass clazz, jf extern "C" JNIEXPORT void JNICALL Java_at_lockstep_player_pasada_LibPasada_play(JNIEnv *env, jclass clazz, jint fd, jlong offset, - jlong length) { + jlong length, jdoubleArray beat_times_sec) { LOGD("liblockstep-native jni_libpasada.cpp LibPasada_play()"); if (g_libpasada == nullptr) return; + std::vector beats; + if (beat_times_sec != nullptr) { + const jsize n = env->GetArrayLength(beat_times_sec); + beats.resize(static_cast(n)); + env->GetDoubleArrayRegion(beat_times_sec, 0, n, beats.data()); + if (env->ExceptionCheck()) return; + } + try { - g_libpasada->play(fd, offset, length); + g_libpasada->play(fd, offset, length, beats); } catch (const std::bad_alloc&) { jclass cls = env->FindClass("java/lang/OutOfMemoryError"); if (cls) env->ThrowNew(cls, "native allocation failed"); diff --git a/app/src/main/cpp/jni_lockstep.cpp b/app/src/main/cpp/jni_lockstep.cpp index 8584bc1..078150d 100644 --- a/app/src/main/cpp/jni_lockstep.cpp +++ b/app/src/main/cpp/jni_lockstep.cpp @@ -67,7 +67,8 @@ Java_at_lockstep_pb_PlaybackEngine_native_1playMusic(JNIEnv *env, jlong engineHandle, jint fd) { auto engine = reinterpret_cast(engineHandle); - engine->playMusic(fd); + std::vector beats; + engine->playMusic(fd, beats); } } // extern "C" diff --git a/app/src/main/cpp/libpasada b/app/src/main/cpp/libpasada index 60a5b10..435221c 160000 --- a/app/src/main/cpp/libpasada +++ b/app/src/main/cpp/libpasada @@ -1 +1 @@ -Subproject commit 60a5b109bb294c607d335331c0257414658ca413 +Subproject commit 435221c3c6563009fd50f5ea7933d70b83518ee1 diff --git a/app/src/main/java/at/lockstep/player/pasada/LibPasada.java b/app/src/main/java/at/lockstep/player/pasada/LibPasada.java index de3896d..f0e4c54 100644 --- a/app/src/main/java/at/lockstep/player/pasada/LibPasada.java +++ b/app/src/main/java/at/lockstep/player/pasada/LibPasada.java @@ -1,11 +1,14 @@ package at.lockstep.player.pasada; /** - * JNI entry point for libpasada. + * JNI entry point for libpasada. Used by {@link at.lockstep.player.playback.engine.PasadaMusicPlayerEngine}; + * {@link at.lockstep.player.playback.PlaybackService} still defaults to + * {@link at.lockstep.player.playback.engine.ExoPlayerMusicPlayerEngine} until the native library is ready. * *

Native state machine: LOADED → INITIALIZED → PLAYING ↔ PAUSED → FINISHED → STOPPED * - *

Call {@link #loadNative()} once before any other method. + *

Call {@link #loadNative()} once before any other method once the {@code pasada} shared + * library is added via CMake (step 3). */ public final class LibPasada { @@ -34,8 +37,14 @@ public final class LibPasada { * @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) + * @param beatTimesSec beat times in seconds from track start, or {@code null} when unknown */ - public static native void play(int fd, long offset, long length); + public static native void play(int fd, long offset, long length, double[] beatTimesSec); + + /** Same as {@link #play(int, long, long, double[])} with no beat annotation. */ + public static void play(int fd, long offset, long length) { + play(fd, offset, length, null); + } /** PLAYING → PAUSED (silent output, graph kept alive). */ public static native void pause();