Compare commits
6 Commits
a924331ede
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f39cecc25 | |||
| 21645b9746 | |||
| 80ee56dfe8 | |||
| 83dea946c3 | |||
| f8aa61c1b9 | |||
| 85da003cb5 |
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}. */
|
||||||
@@ -11,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
|
||||||
@@ -29,21 +35,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, this);
|
||||||
|
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 +93,107 @@ 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;
|
||||||
|
// TODO: pause plays a rect noise in PlaybackEngine. debug it!
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,23 +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);
|
||||||
|
void emitTrackClosed(int fd);
|
||||||
/**
|
|
||||||
* 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.
|
||||||
@@ -40,14 +31,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 +77,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
|
||||||
|
|||||||
25
app/src/main/cpp/PasadaPlaybackListener.h
Normal file
25
app/src/main/cpp/PasadaPlaybackListener.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// 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();
|
||||||
|
virtual void onTrackClosed(int fd);
|
||||||
|
|
||||||
|
/** Decode / Oboe / pipeline failure; service should stop safely and surface error. */
|
||||||
|
virtual void onError(int errorCode, std::string message);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //LOCKSTEP_PASADAPLAYBACKLISTENER_H
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <sys/types.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.
|
||||||
@@ -67,7 +70,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 +84,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 +105,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);
|
||||||
|
|
||||||
@@ -145,7 +159,9 @@ void PlaybackEngine::initRubberBand() {
|
|||||||
void PlaybackEngine::closeRubberBand() {
|
void PlaybackEngine::closeRubberBand() {
|
||||||
if(musicFeed) {
|
if(musicFeed) {
|
||||||
exitMusicFeedThread.store(true);
|
exitMusicFeedThread.store(true);
|
||||||
|
LOGI("PlaybackEngine: musicFeed->join() ...");
|
||||||
musicFeed->join();
|
musicFeed->join();
|
||||||
|
LOGI("PlaybackEngine: musicFeed->join() done.");
|
||||||
musicFeed = nullptr;
|
musicFeed = nullptr;
|
||||||
}
|
}
|
||||||
closeMusicFile();
|
closeMusicFile();
|
||||||
@@ -155,11 +171,15 @@ 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);
|
||||||
|
LOGI("onTrackClosed(fd=%d) ...", android_fd);
|
||||||
|
if(listener) listener->onTrackClosed(android_fd);
|
||||||
|
LOGI("onTrackClosed(fd=%d) done.", 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) {
|
||||||
@@ -185,6 +205,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
|
||||||
@@ -211,7 +235,8 @@ void PlaybackEngine::musicFeedThread() {
|
|||||||
int* channel_map = (int*) malloc(num_ch_out * sizeof(int));
|
int* channel_map = (int*) malloc(num_ch_out * sizeof(int));
|
||||||
|
|
||||||
// initial guess, as long as we do not have a music file (otherwise we should divide by mp3->rate)
|
// initial guess, as long as we do not have a music file (otherwise we should divide by mp3->rate)
|
||||||
size_t loop_delay_us = 1000000 * buf_size_samples / playbackRate.load();
|
size_t pr = std::max(1, playbackRate.load());
|
||||||
|
size_t loop_delay_us = std::min((size_t) 50000, (size_t) 1000000 * buf_size_samples / pr);
|
||||||
|
|
||||||
int idebug = 0;
|
int idebug = 0;
|
||||||
|
|
||||||
@@ -284,31 +309,87 @@ 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();
|
pr = std::max(1, playbackRate.load());
|
||||||
|
loop_delay_us = std::min((size_t) 50000, (size_t) 1000000 * num_samples / pr);
|
||||||
if(idebug++ < 10) {
|
if(idebug++ < 10) {
|
||||||
LOGI("feed %d silence samples", num_samples);
|
LOGI("feed %d silence samples", num_samples);
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(idebug++ < 10) {
|
if(idebug++ < 10) {
|
||||||
loop_delay_us = 1000000 * num_samples / musicFile->rate;
|
pr = std::max(1L, musicFile->rate);
|
||||||
|
loop_delay_us = std::min((size_t) 50000, (size_t) 1000000 * num_samples / pr);
|
||||||
LOGI("feed %d music samples", num_samples);
|
LOGI("feed %d music samples", num_samples);
|
||||||
// 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);
|
||||||
|
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);
|
||||||
|
LOGI("seek to %.3lf sec = %lld samples", pos, (long long) seek_samples);
|
||||||
|
seek_samples = std::max(seek_samples, 0L);
|
||||||
|
seek_samples = std::min(seek_samples, musicFile->num_samples);
|
||||||
|
// perform the seek
|
||||||
|
off_t seekResult = mp3file_seek(musicFile.get(), seek_samples);
|
||||||
|
idebug = 0;
|
||||||
|
/*if(seekResult > seek_samples) {
|
||||||
|
LOGE("error seeking in mp3 file: mpg123_seek(handle, %ld, SEEK_SET)=%lld", seek_samples, (long long) seekResult);
|
||||||
|
seekPos.store(pos * dist(mt)); // randomize seek target a bit
|
||||||
|
} else {*/
|
||||||
|
// update structure
|
||||||
|
// 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 = 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);
|
||||||
|
/*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);
|
||||||
musicFile->remaining_samples -= done / sizeof(int16_t);
|
//if (idebug < 10) LOGI("mft done=%lld err=%lld small=%d", (long long) done, (long long) err, read_size_bytes < read_size_bytes_calc);
|
||||||
musicFile->offset = 0; // unused here
|
/*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 / num_ch_in / sizeof(int16_t);
|
||||||
|
//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);
|
||||||
@@ -322,12 +403,11 @@ 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)");
|
||||||
|
idebug = 0;
|
||||||
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 +436,67 @@ void PlaybackEngine::musicFeedThread() {
|
|||||||
LOGI("musicFeedThread() exited.");
|
LOGI("musicFeedThread() exited.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlaybackEngine::pause() {
|
||||||
|
LOGI("PlaybackEngine::pause() set isPaused.");
|
||||||
|
// 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) {
|
||||||
|
LOGI("PlaybackEngine: mPlayer->stopAudio() ...");
|
||||||
|
mPlayer->stopAudio();
|
||||||
|
LOGI("PlaybackEngine: mPlayer->stopAudio() done.");
|
||||||
|
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));
|
if(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 +506,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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -4,34 +4,75 @@
|
|||||||
#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;
|
||||||
}
|
}
|
||||||
g_libpasada = new LibPasada();
|
try {
|
||||||
g_libpasada->init();
|
g_libpasada = new LibPasada();
|
||||||
|
g_libpasada->init();
|
||||||
|
return;
|
||||||
|
} 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 +82,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,26 +111,73 @@ 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;
|
||||||
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 +185,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 +210,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 +278,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"
|
||||||
@@ -160,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);
|
||||||
@@ -192,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());
|
||||||
}
|
}
|
||||||
@@ -203,3 +387,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());
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { \
|
||||||
@@ -104,6 +105,8 @@ MP3File* mp3file_open_fd(int fd, int forceEncoding) {
|
|||||||
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) {
|
||||||
@@ -113,9 +116,14 @@ 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()");
|
||||||
|
|
||||||
|
// 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()");
|
||||||
@@ -149,10 +157,21 @@ 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;
|
||||||
|
|
||||||
|
// 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:
|
||||||
@@ -162,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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,13 +22,17 @@ 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);
|
||||||
|
off_t mp3file_seek(MP3File *mp3file, off_t samples);
|
||||||
|
|
||||||
#endif //SAMPLES_MP3FILE_H
|
#endif //SAMPLES_MP3FILE_H
|
||||||
|
|||||||
@@ -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,9 @@ 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);
|
||||||
|
} else {
|
||||||
|
PlaybackEngine.playMusic(0, this, R.raw.track_beat);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// TODO proper error handling
|
// TODO proper error handling
|
||||||
@@ -109,6 +117,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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user