feat: detect steps and play audio
This commit is contained in:
9
TODO.md
9
TODO.md
@@ -1,3 +1,12 @@
|
|||||||
|
## TODO
|
||||||
|
|
||||||
|
O> 16 KB paging for NDK libs
|
||||||
|
* target SDK level 35
|
||||||
|
|
||||||
|
* minimum amplitude for accelero
|
||||||
|
* filter away bad SQI areas of the signal (do not detect steps if we have bad SQI)
|
||||||
|
|
||||||
|
## Nice-To
|
||||||
* myband PlaybackEngine.cpp has latency management and other audio performance related features.
|
* myband PlaybackEngine.cpp has latency management and other audio performance related features.
|
||||||
Check if the app can be improved (audio wise) by using that code instead.
|
Check if the app can be improved (audio wise) by using that code instead.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
namespace 'at.lockstep'
|
namespace 'at.lockstep'
|
||||||
compileSdk 34
|
compileSdk 34
|
||||||
|
ndkVersion '29.0.14206865'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "at.lockstep"
|
applicationId "at.lockstep"
|
||||||
@@ -21,15 +22,19 @@ android {
|
|||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
//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 "-std=c++14"
|
//cppFlags "-std=c++14"
|
||||||
//arguments '-DANDROID_STL=c++_static'
|
//arguments '-DANDROID_STL=c++_static'
|
||||||
|
|
||||||
|
//cppFlags "-Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=4096"
|
||||||
|
// should be provided by default by newer NDK (NDK r29)
|
||||||
|
|
||||||
// armeabi and mips are deprecated in NDK r16 so we don't want to build for them
|
// armeabi and mips are deprecated in NDK r16 so we don't want to build for them
|
||||||
// TODO: android manifest filters to include only these hardware archs
|
// TODO: android manifest filters to include only these hardware archs
|
||||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64' //, 'x86', 'x86_64'
|
//abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64' //, 'x86', 'x86_64'
|
||||||
|
abiFilters 'armeabi-v7a', 'arm64-v8a' //, 'aarch64' // 'arm64-v8a' ???
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -23,6 +27,11 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<service
|
||||||
|
android:name="at.lockstep.app.LstForegroundService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="mediaPlayback"
|
||||||
|
/>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -12,6 +12,8 @@ cmake_minimum_required(VERSION 3.22.1)
|
|||||||
# build script scope).
|
# build script scope).
|
||||||
project("lockstep-native")
|
project("lockstep-native")
|
||||||
|
|
||||||
|
add_subdirectory(libpasada/pasada-lib)
|
||||||
|
|
||||||
# Creates and names a library, sets it as either STATIC
|
# Creates and names a library, sets it as either STATIC
|
||||||
# or SHARED, and provides the relative paths to its source code.
|
# or SHARED, and provides the relative paths to its source code.
|
||||||
# You can define multiple libraries, and CMake builds them for you.
|
# You can define multiple libraries, and CMake builds them for you.
|
||||||
@@ -31,6 +33,8 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
|
|||||||
PlaybackEngine.cpp
|
PlaybackEngine.cpp
|
||||||
mp3file.cpp
|
mp3file.cpp
|
||||||
jni_bridge.cpp
|
jni_bridge.cpp
|
||||||
|
jni_stepdetector.cpp
|
||||||
|
StepDetector.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package (oboe REQUIRED CONFIG)
|
find_package (oboe REQUIRED CONFIG)
|
||||||
@@ -45,11 +49,14 @@ set_target_properties(mpg123 PROPERTIES IMPORTED_LOCATION
|
|||||||
${mpg123_DIR}/lib/${ANDROID_ABI}/libmpg123.so)
|
${mpg123_DIR}/lib/${ANDROID_ABI}/libmpg123.so)
|
||||||
include_directories(${mpg123_DIR}/lib/${ANDROID_ABI}/include)
|
include_directories(${mpg123_DIR}/lib/${ANDROID_ABI}/include)
|
||||||
|
|
||||||
|
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE libpasada/pasada-lib/include)
|
||||||
|
|
||||||
# Specifies libraries CMake should link to your target library. You
|
# Specifies libraries CMake should link to your target library. You
|
||||||
# can link libraries from various origins, such as libraries defined in this
|
# can link libraries from various origins, such as libraries defined in this
|
||||||
# build script, prebuilt third-party libraries, or Android system libraries.
|
# build script, prebuilt third-party libraries, or Android system libraries.
|
||||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||||
# List libraries link to the target library
|
# List libraries link to the target library
|
||||||
|
pasada
|
||||||
oboe::oboe
|
oboe::oboe
|
||||||
mpg123
|
mpg123
|
||||||
android
|
android
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public:
|
|||||||
std::lock_guard<std::mutex> lock(mLock);
|
std::lock_guard<std::mutex> lock(mLock);
|
||||||
oboe::AudioStreamBuilder builder;
|
oboe::AudioStreamBuilder builder;
|
||||||
// The builder set methods can be chained for convenience.
|
// The builder set methods can be chained for convenience.
|
||||||
Result result = builder.setSharingMode(oboe::SharingMode::Exclusive)
|
Result result = builder.setSharingMode(oboe::SharingMode::Shared)
|
||||||
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
|
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
|
||||||
->setChannelCount(kChannelCount)
|
->setChannelCount(kChannelCount)
|
||||||
->setSampleRate(kSampleRate)
|
->setSampleRate(kSampleRate)
|
||||||
|
|||||||
@@ -56,3 +56,7 @@ PlaybackEngine::~PlaybackEngine() {
|
|||||||
delete mPlayer;
|
delete mPlayer;
|
||||||
mPlayer = nullptr;
|
mPlayer = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlaybackEngine::playBeat() {
|
||||||
|
if(mPlayer) mPlayer->setStartBeat();
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,13 +5,16 @@
|
|||||||
#ifndef LOCKSTEP_PLAYBACKENGINE_H
|
#ifndef LOCKSTEP_PLAYBACKENGINE_H
|
||||||
#define LOCKSTEP_PLAYBACKENGINE_H
|
#define LOCKSTEP_PLAYBACKENGINE_H
|
||||||
|
|
||||||
|
#include "StepListener.h"
|
||||||
#include "MixingPlayer.h"
|
#include "MixingPlayer.h"
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
class PlaybackEngine {
|
class PlaybackEngine : public StepListener {
|
||||||
public:
|
public:
|
||||||
PlaybackEngine(std::string filesDir, int resid);
|
PlaybackEngine(std::string filesDir, int resid);
|
||||||
virtual ~PlaybackEngine();
|
virtual ~PlaybackEngine();
|
||||||
|
/** Play a beat sound. */
|
||||||
|
virtual void playBeat();
|
||||||
private:
|
private:
|
||||||
MixingPlayer *mPlayer;
|
MixingPlayer *mPlayer;
|
||||||
std::string mFilesDir;
|
std::string mFilesDir;
|
||||||
|
|||||||
44
app/src/main/cpp/StepDetector.cpp
Normal file
44
app/src/main/cpp/StepDetector.cpp
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// Created by david on 03.03.2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "StepDetector.h"
|
||||||
|
|
||||||
|
// TODO: we are hardcoding filter coefficients for 60 Hz
|
||||||
|
// TODO: this is tolerable for 50 Hz
|
||||||
|
|
||||||
|
// TODO: check if we can do with floats instead of doubles
|
||||||
|
// (check how much the [already bad] accuracy of filtering suffers)
|
||||||
|
|
||||||
|
// TODO: in Java, check if delta timestamps effectively match FPS
|
||||||
|
// TODO: FPS constant should be passed as argument to C++ (but we keep an FPS define to validate the coefficients)
|
||||||
|
|
||||||
|
// Butterworth filter: order=5, fc=0.5, fs=60, btype='highpass'
|
||||||
|
static std::vector hpf_taps_b {0.91875845, -4.59379227, 9.18758454, -9.18758454, 4.59379227, -0.91875845};
|
||||||
|
static std::vector hpf_taps_a {1. , -4.83056552, 9.33652742, -9.02545247, 4.36360803, -0.8441171};
|
||||||
|
static size_t upslope_width = 4;
|
||||||
|
const size_t len_refr = (size_t) (FPS / (MAX_BPM / 60));
|
||||||
|
|
||||||
|
StepDetector::StepDetector(StepListener *listener) :
|
||||||
|
listener(listener),
|
||||||
|
f_highpass(hpf_taps_b, hpf_taps_a),
|
||||||
|
f_neg(1, 0, 0, std::vector {-1.0}),
|
||||||
|
f_ssf(upslope_width),
|
||||||
|
f_ssd(len_refr)
|
||||||
|
{}
|
||||||
|
|
||||||
|
#if (FPS != 60)
|
||||||
|
#error "FPS must currently be 60, as highpass taps are pre-computed for that value"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void StepDetector::filter(std::vector<float> values) {
|
||||||
|
// TODO: later on, we should use a vector projection towards gravity
|
||||||
|
auto s0 = (double) values[1]; // take y-axis value for now
|
||||||
|
auto s1 = f_highpass.filter(s0);
|
||||||
|
auto s2 = f_neg.filter(s1);
|
||||||
|
auto s3 = f_ssf.filter(s2);
|
||||||
|
auto s4 = f_ssd.filter(s3);
|
||||||
|
if(s4 > 0.0 && listener != nullptr) {
|
||||||
|
listener->playBeat();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/src/main/cpp/StepDetector.h
Normal file
26
app/src/main/cpp/StepDetector.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// Created by david on 03.03.2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef LOCKSTEP_STEPDETECTOR_H
|
||||||
|
#define LOCKSTEP_STEPDETECTOR_H
|
||||||
|
|
||||||
|
#include "StepListener.h"
|
||||||
|
#include "iir_filter.h"
|
||||||
|
#include "ssf_filter.h"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class StepDetector {
|
||||||
|
protected:
|
||||||
|
StepListener *listener;
|
||||||
|
IirFilter f_highpass;
|
||||||
|
Filt f_neg;
|
||||||
|
SsfFilter f_ssf;
|
||||||
|
SsfStepDetector f_ssd;
|
||||||
|
|
||||||
|
public:
|
||||||
|
StepDetector(StepListener *listener);
|
||||||
|
void filter(std::vector<float> values);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //LOCKSTEP_STEPDETECTOR_H
|
||||||
14
app/src/main/cpp/StepListener.h
Normal file
14
app/src/main/cpp/StepListener.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
52
app/src/main/cpp/jni_stepdetector.cpp
Normal file
52
app/src/main/cpp/jni_stepdetector.cpp
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// Created by david on 03.03.2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
#include "StepDetector.h"
|
||||||
|
#include <new>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
jint throwIllegalArgumentException(JNIEnv *env, const char *message)
|
||||||
|
{
|
||||||
|
jclass exClass;
|
||||||
|
const char *className = "java/lang/IllegalArgumentException";
|
||||||
|
exClass = env->FindClass(className);
|
||||||
|
if (exClass == nullptr) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return env->ThrowNew(exClass, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_at_lockstep_filter_StepDetector_native_1create(
|
||||||
|
JNIEnv *env,
|
||||||
|
jclass /*unused*/, jlong engineHandle) {
|
||||||
|
auto *listener = reinterpret_cast<StepListener *>(engineHandle);
|
||||||
|
// We use std::nothrow so `new` returns a nullptr if the engine creation fails
|
||||||
|
auto *detector = new(std::nothrow) StepDetector(listener);
|
||||||
|
return reinterpret_cast<jlong>(detector);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_at_lockstep_filter_StepDetector_native_1delete(
|
||||||
|
JNIEnv *env,
|
||||||
|
jclass /*unused*/, jlong handle) {
|
||||||
|
delete reinterpret_cast<StepDetector *>(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_at_lockstep_filter_StepDetector_native_1filter(
|
||||||
|
JNIEnv *env,
|
||||||
|
jclass /*unused*/, jlong handle, jlong timestamp, jfloatArray values) {
|
||||||
|
if(values == nullptr) throwIllegalArgumentException(env, "values == null");
|
||||||
|
float* nativeValues = (float *)env->GetFloatArrayElements(values, 0);
|
||||||
|
jsize length = env->GetArrayLength(values);
|
||||||
|
std::vector<float> vecValues(nativeValues, nativeValues + length);
|
||||||
|
auto *detector = reinterpret_cast<StepDetector *>(handle);
|
||||||
|
detector->filter(vecValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
182
app/src/main/java/at/lockstep/app/LstForegroundService.java
Normal file
182
app/src/main/java/at/lockstep/app/LstForegroundService.java
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package at.lockstep.app;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.hardware.Sensor;
|
||||||
|
import android.hardware.SensorEvent;
|
||||||
|
import android.hardware.SensorEventListener;
|
||||||
|
import android.hardware.SensorManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.PowerManager;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
|
import at.lockstep.filter.StepDetector;
|
||||||
|
import at.lockstep.pb.PlaybackEngine;
|
||||||
|
import at.lockstep.R;
|
||||||
|
|
||||||
|
public class LstForegroundService extends Service implements SensorEventListener {
|
||||||
|
|
||||||
|
private static final String CHANNEL_ID = "sensor_collection_channel";
|
||||||
|
private static final int NOTIFICATION_ID = 1001;
|
||||||
|
|
||||||
|
public static final String ACTION_START = "at.lockstep.action.START";
|
||||||
|
public static final String ACTION_STOP = "at.lockstep.action.STOP";
|
||||||
|
|
||||||
|
private SensorManager sensorManager;
|
||||||
|
private Sensor accelerometer;
|
||||||
|
private PowerManager.WakeLock wakeLock;
|
||||||
|
private boolean isCollecting = false;
|
||||||
|
|
||||||
|
private StepDetector stepDetector;
|
||||||
|
|
||||||
|
public static Intent startIntent(Context context) {
|
||||||
|
Intent intent = new Intent(context, LstForegroundService.class);
|
||||||
|
intent.setAction(ACTION_START);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Intent stopIntent(Context context) {
|
||||||
|
Intent intent = new Intent(context, LstForegroundService.class);
|
||||||
|
intent.setAction(ACTION_STOP);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
|
||||||
|
int resid = R.raw.track_beat;
|
||||||
|
PlaybackEngine.create(this, resid);
|
||||||
|
stepDetector = new StepDetector(PlaybackEngine.getEngineHandle());
|
||||||
|
|
||||||
|
sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
|
||||||
|
if (sensorManager != null) {
|
||||||
|
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotificationChannel();
|
||||||
|
|
||||||
|
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||||
|
if (powerManager != null) {
|
||||||
|
wakeLock = powerManager.newWakeLock(
|
||||||
|
PowerManager.PARTIAL_WAKE_LOCK,
|
||||||
|
getPackageName() + ":SensorCollectionWakeLock"
|
||||||
|
);
|
||||||
|
wakeLock.setReferenceCounted(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
if (intent != null) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
if (ACTION_START.equals(action)) {
|
||||||
|
startCollection();
|
||||||
|
} else if (ACTION_STOP.equals(action)) {
|
||||||
|
stopCollectionAndSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCollection() {
|
||||||
|
if (isCollecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startForeground(NOTIFICATION_ID, buildNotification("Collecting sensor data"));
|
||||||
|
|
||||||
|
if (wakeLock != null && !wakeLock.isHeld()) {
|
||||||
|
// TODO: provide a timeout reasonable for a run
|
||||||
|
wakeLock.acquire(2*60*60*1000L /*2 hours*/);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accelerometer != null && sensorManager != null) {
|
||||||
|
sensorManager.registerListener(
|
||||||
|
this,
|
||||||
|
accelerometer,
|
||||||
|
SensorManager.SENSOR_DELAY_GAME
|
||||||
|
);
|
||||||
|
isCollecting = true;
|
||||||
|
} else {
|
||||||
|
stopCollectionAndSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopCollectionAndSelf() {
|
||||||
|
if (isCollecting && sensorManager != null) {
|
||||||
|
sensorManager.unregisterListener(this);
|
||||||
|
isCollecting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wakeLock != null && wakeLock.isHeld()) {
|
||||||
|
wakeLock.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
if (sensorManager != null) {
|
||||||
|
sensorManager.unregisterListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wakeLock != null && wakeLock.isHeld()) {
|
||||||
|
wakeLock.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check threading to see if these run in separate threads - if so, deleting PlaybackEngine will leave a dangling pointer in StepDetector.
|
||||||
|
// 2026-03-04 01:26:11.741 12507-12507 libc at.lockstep A Fatal signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0xb4000071d3a79000 in tid 12507 (at.lockstep), pid 12507 (at.lockstep)
|
||||||
|
stepDetector.close();
|
||||||
|
PlaybackEngine.delete();
|
||||||
|
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSensorChanged(SensorEvent event) {
|
||||||
|
stepDetector.filter(event.timestamp, event.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
|
||||||
|
|
||||||
|
private Notification buildNotification(String contentText) {
|
||||||
|
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setContentTitle("Lockstep is reading your pace.")
|
||||||
|
.setContentText(contentText)
|
||||||
|
.setSmallIcon(android.R.drawable.ic_menu_compass)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"Lockstep",
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
);
|
||||||
|
|
||||||
|
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||||
|
if (nm != null) {
|
||||||
|
nm.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,42 @@
|
|||||||
package at.lockstep.app;
|
package at.lockstep.app;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import at.lockstep.R;
|
||||||
import at.lockstep.pb.PlaybackEngine;
|
import at.lockstep.pb.PlaybackEngine;
|
||||||
|
|
||||||
public class MainActivity extends Activity {
|
public class MainActivity extends Activity {
|
||||||
/*
|
private Button btnStart;
|
||||||
* Creating engine in onResume() and destroying in onPause() so the stream retains exclusive
|
private Button btnStop;
|
||||||
* mode only while in focus. This allows other apps to reclaim exclusive stream mode.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
int resid = at.lockstep.R.raw.track_beat;
|
|
||||||
PlaybackEngine.create(this, resid); // note: called twice (is permission request causing Activity to go out of focus?)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
PlaybackEngine.delete();
|
super.onCreate(savedInstanceState);
|
||||||
super.onPause();
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
|
btnStart = findViewById(R.id.btnStart);
|
||||||
|
btnStop = findViewById(R.id.btnStop);
|
||||||
|
|
||||||
|
btnStart.setOnClickListener(v ->
|
||||||
|
ContextCompat.startForegroundService(
|
||||||
|
MainActivity.this,
|
||||||
|
LstForegroundService.startIntent(MainActivity.this)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
//if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
// startForeground(SERVICE_ID, notification)
|
||||||
|
//} else {
|
||||||
|
// startForeground(SERVICE_ID, notification,
|
||||||
|
//FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
|
||||||
|
//}
|
||||||
|
|
||||||
|
btnStop.setOnClickListener(v ->
|
||||||
|
startService(LstForegroundService.stopIntent(MainActivity.this))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
app/src/main/java/at/lockstep/filter/StepDetector.java
Normal file
21
app/src/main/java/at/lockstep/filter/StepDetector.java
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package at.lockstep.filter;
|
||||||
|
|
||||||
|
import at.lockstep.pb.PlaybackEngine;
|
||||||
|
|
||||||
|
public class StepDetector {
|
||||||
|
long handle;
|
||||||
|
|
||||||
|
public StepDetector(long engineHandle) {
|
||||||
|
handle = native_create(engineHandle);
|
||||||
|
}
|
||||||
|
public void close() {
|
||||||
|
native_delete(handle);
|
||||||
|
}
|
||||||
|
public void filter(long timestamp, float[] values) {
|
||||||
|
native_filter(handle, timestamp, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native long native_create(long engineHandle);
|
||||||
|
private static native void native_delete(long sdHandle);
|
||||||
|
private static native void native_filter(long sdHandle, long timestamp, float[] values);
|
||||||
|
}
|
||||||
@@ -67,6 +67,9 @@ public class PlaybackEngine {
|
|||||||
}
|
}
|
||||||
mEngineHandle = 0;
|
mEngineHandle = 0;
|
||||||
}
|
}
|
||||||
|
public static long getEngineHandle() {
|
||||||
|
return mEngineHandle;
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
21
app/src/main/res/layout/activity_main.xml
Normal file
21
app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnStart"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Start collection" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnStop"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="Stop collection" />
|
||||||
|
</LinearLayout>
|
||||||
Reference in New Issue
Block a user