Compare commits

...

35 Commits

Author SHA1 Message Date
3b1b92ba45 fix: adjust pitch (fix pitch direction) 2026-03-28 04:42:46 +01:00
6c5ac60ccb fix: adjust pitch, not just speed 2026-03-28 04:39:19 +01:00
1a3991db45 fix: fixes for buffer level handling, player lifecycle handling 2026-03-23 01:07:05 +01:00
3fe10a914a chore: more debug output of states 2026-03-22 23:58:40 +01:00
45b1003c9a fix: fix mpg123_read() read_size_bytes 2026-03-22 23:27:07 +01:00
e8afb10f48 bug: tweak loop delay to fit music rate 2026-03-22 15:16:54 +01:00
cc1a2b5b7a feat: record accelerometer to file 2026-03-22 15:16:13 +01:00
7fb3029e8b chore: improve debug logging of audio lag from stretcher 2026-03-22 14:28:44 +01:00
b6df86f49a chore: generic LstForegroundService housekeeping 2026-03-22 09:20:13 +01:00
708249a5ba docs: some more debug prints, for cleanup diag 2026-03-22 08:57:55 +01:00
3cf9607549 docs 2026-03-22 08:50:39 +01:00
25e5cc19d0 feat: actually map channels 2026-03-22 08:44:41 +01:00
6382c57ccc feat: reduce buffers to 1024 2026-03-22 07:49:05 +01:00
b0f6d6d5c9 fix: play with rate of mp3 file 2026-03-22 07:37:27 +01:00
b14ea02694 feat: play music through librubberband 2026-03-20 23:17:34 +01:00
5b26203533 chore: bump libpasada 2026-03-19 19:22:29 +01:00
198dfc1630 feat: SAF file picker, fd to jni code 2026-03-19 19:17:57 +01:00
02ebb17dc6 feat: SongPickerActivity 2026-03-19 12:27:38 +01:00
a8234005df feat: check SQI before playing step sound 2026-03-12 22:20:00 +01:00
a2fdf05cd5 docs 2026-03-07 23:17:06 +01:00
79f37ca46d docs: build notes 2026-03-07 23:15:41 +01:00
5045d6615e build: reorg jni files 2026-03-07 23:02:15 +01:00
995c537fd4 build: submodules for libpasada, librubberband. Add librubberband. 2026-03-07 22:57:11 +01:00
3d55765a26 docs: cleanup 2026-03-07 22:18:06 +01:00
754b96a319 feat: benchmark MediaStore API of Android 2026-03-07 18:48:57 +01:00
65ea8ba27c docs: update TODO.md 2026-03-06 00:55:09 +01:00
754ad3700f docs: cleanup excessive logging 2026-03-06 00:53:04 +01:00
63ef5ae503 docs: clean up logging a bit 2026-03-06 00:50:38 +01:00
66a2b86ffc fix: fixup readBuffer2() to return samples not bytes 2026-03-06 00:45:20 +01:00
d1b57aae82 fix: fixed mpg123_read() logic 2026-03-05 14:28:23 +01:00
e22478445d feat: detect steps and play audio 2026-03-04 01:31:35 +01:00
804f83340f feat: MixingPlayer - plays event sounds 2026-03-03 14:19:56 +01:00
bc8002fd59 feat: audio resources 2026-03-03 12:04:17 +01:00
d2c9a7b2ff jni: updated build scripts 2026-03-02 09:43:56 +01:00
ef5a0e678c jni: add libmpg123 2026-03-02 09:42:44 +01:00
55 changed files with 10205 additions and 161 deletions

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@
app/src/main/obj/
.cxx
/txts
/data

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "app/src/main/cpp/rubberband"]
path = app/src/main/cpp/librubberband
url = https://donkey.abanbytes.eu/david/librubberband.git
[submodule "app/src/main/cpp/libpasada"]
path = app/src/main/cpp/libpasada
url = https://donkey.abanbytes.eu/david/libpasada.git

62
TODO.md Normal file
View File

@@ -0,0 +1,62 @@
## TODO
* do not open oboe upon app startup, only if we are actually recording
* PlaybackEngine - Buffer overrun on output for channel (1.000000)
- we are feeding too much data into 'stretcher'
* E attributionTag not declared in manifest of at.lockstep
* analyze the (secondly or so) noise beeps in the mp3 playback
- introduced with this commit
- is it librubberband, my failure to feed it properly (buffer exhaustion), or sth else?
- the sizes of my buffers?
* correct sampling rate of libmpg123 vs. 48000 Hz using librubberband
* record accelero and write recording to file
* set feedForDelay in musicFeedThread() in PlaybackEngine.cpp
- LOGI("feed %d silence samples", num_samples);
- set the sleep delay
- ideally, set the buf size
* check playback buf size, and reduce buffer sizes
- LOGI("onAudioReady() frames=%d", frames);
* reduce lib size, librubberband is 1.3 M (one .so file)
- maybe we are compiling too many source files
- # TODO: see Android.mk in librubberband and copy options from `LOCAL_CFLAGS`
O> 16 KB paging for NDK libs
* target SDK level 35
* record the accelero and check why repeated fast beats result in occasional detection outages
- guess: the threshold gets pushed so high that the later beats are not detected properly
- maybe we need the `*0.6` factor after all (but verify this!)
* minimum amplitude for accelero (avoid detecting e.g. when phone is lying flat and you hammer on the table)
* 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.
Check if the app can be improved (audio wise) by using that code instead.
* Sampling rate for accelerometer - do we need to measure actual sensor FPS, or is it stable 50 Hz?
* re-calculate IIR filter coefficients. probably not critical for 50 Hz vs. 60 Hz.
* re-visit sampling rate and channel count.
MixingPlayer currently forces both to 48000 and 2 respectively,
regardless of what Android says would be optimal.
* nice-to nice-to: ramp up audio pipelines twice - once to collect params, another one to use those
## Technical Debt
* remove duplication of `mp3file_open()` in `mp3file_open_fd()`
* malloc() nullptr result handling
## Before release
* check librubberband license

View File

@@ -6,6 +6,7 @@ plugins {
android {
namespace 'at.lockstep'
compileSdk 34
ndkVersion '29.0.14206865'
defaultConfig {
applicationId "at.lockstep"
@@ -21,8 +22,19 @@ android {
externalNativeBuild {
cmake {
//path 'src/main/cpp/CMakeLists.txt'
cppFlags ''
//cppFlags ''
arguments "-DANDROID_STL=c++_shared"
//cppFlags "-std=c++14"
//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
// 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' //, 'aarch64' // 'arm64-v8a' ???
}
}
}
@@ -64,6 +76,7 @@ dependencies {
implementation libs.oboe
implementation libs.slf4j.api
implementation libs.logback.android
implementation libs.gson
implementation libs.androidx.core.ktx
implementation libs.androidx.lifecycle.runtime.ktx
@@ -73,6 +86,8 @@ dependencies {
implementation libs.androidx.ui.graphics
implementation libs.androidx.ui.tooling.preview
implementation libs.androidx.material3
implementation libs.androidx.recyclerview
implementation libs.androidx.appcompat
testImplementation libs.junit
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core

View File

@@ -2,6 +2,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -19,10 +27,26 @@
android:theme="@style/Theme.Lockstep">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="at.lockstep.app.MediaStoreBenchmarkActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="at.lockstep.ui.SongPickerActivity" />
<activity android:name="at.lockstep.saf.SafPickerActivity" />
<service
android:name="at.lockstep.app.LstForegroundService"
android:exported="false"
android:foregroundServiceType="mediaPlayback"
/>
</application>
</manifest>

View File

@@ -0,0 +1,25 @@
//
// Created by david on 20.03.2026.
//
#ifndef LOCKSTEP_AUDIOCALLBACK_H
#define LOCKSTEP_AUDIOCALLBACK_H
#include <cstdint>
/**
* Provides audio through a regular callback to oboe.
*/
class AudioCallbackProvider {
public:
virtual ~AudioCallbackProvider() {}
/**
* in current impl, this passes a buffer where data may already live.
* the provider may add to it and re-normalize to [-1.0, 1.0].
*
* upon is_finished=true it will not do anything - the caller is responsible for 0-ing the data buffer.
* */
virtual void onAudioReady(float *data, int32_t frames) {}
};
#endif //LOCKSTEP_AUDIOCALLBACK_H

View File

@@ -12,6 +12,9 @@ cmake_minimum_required(VERSION 3.22.1)
# build script scope).
project("lockstep-native")
add_subdirectory(libpasada/pasada-lib)
add_subdirectory(librubberband)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
@@ -27,22 +30,43 @@ project("lockstep-native")
# used in the AndroidManifest.xml file.
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
lockstep.cpp
PlaybackEngine.cpp
mp3file.cpp
jni_mpg123.cpp
jni_lockstep.cpp
jni_stepdetector.cpp
StepDetector.cpp
)
find_package (oboe REQUIRED CONFIG)
add_library(ndk-logger SHARED logging.cpp logging_jni.cpp)
add_library(ndk-logger SHARED logging.cpp jni_logging.cpp)
target_link_libraries(ndk-logger log)
# Add pre-built libmpg123 library
add_library(mpg123 SHARED IMPORTED)
set(mpg123_DIR ${CMAKE_SOURCE_DIR}/../lib/mpg123)
set_target_properties(mpg123 PROPERTIES IMPORTED_LOCATION
${mpg123_DIR}/lib/${ANDROID_ABI}/libmpg123.so)
include_directories(${mpg123_DIR}/lib/${ANDROID_ABI}/include)
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE libpasada/pasada-lib/include)
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE librubberband/rubberband)
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
pasada
rubberband
oboe::oboe
mpg123
android
log
ndk-logger
)
# Enable optimization flags: if having problems with source level debugging,
# disable -Ofast ( and debug ), re-enable after done debugging.
target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE -Wall -Werror "$<$<CONFIG:RELEASE>:-Ofast>")

View File

@@ -0,0 +1,142 @@
//
// Created by david on 01.02.2026.
//
#ifndef LOCKSTEP_MIXINGPLAYER_H
#define LOCKSTEP_MIXINGPLAYER_H
#include <oboe/Oboe.h>
#include <cmath>
#include <atomic>
#include "AudioCallback.h"
using namespace oboe;
/**
* Plays event sounds, potentially overlapping in time.
* Use <c>setStartBeat()</c> to trigger playing sound for an event.
*/
class MixingPlayer: public oboe::AudioStreamDataCallback {
protected:
std::vector<float> beatSound;
std::vector<int> beatIdx;
std::atomic<int> startBeat;
int numBeatsPlaying;
bool mIsPlaying;
public:
explicit MixingPlayer(std::vector<float> beatSound) : beatSound(beatSound), startBeat(0), numBeatsPlaying(0), mIsPlaying(false), mHaveMusic(false) {}
virtual ~MixingPlayer() = default;
/**
* Play a beat sound by setting the startBeat register.
* A delay of the audio callback interval will be added.
*/
void setStartBeat() {
startBeat.store(1);
}
// Call this from Activity onResume()
int32_t startAudio() {
std::lock_guard<std::mutex> lock(mLock);
oboe::AudioStreamBuilder builder;
// The builder set methods can be chained for convenience.
Result result = builder.setSharingMode(oboe::SharingMode::Shared)
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
->setChannelCount(kChannelCount)
->setSampleRate(kSampleRate)
->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Medium)
->setFormat(oboe::AudioFormat::Float)
->setDataCallback(this)
->openStream(mStream);
if (result != Result::OK) return (int32_t) result;
// Typically, start the stream after querying some stream information, as well as some input from the user
result = mStream->requestStart();
mIsPlaying = (result == Result::OK);
return (int32_t) result;
}
int getRate() { return kSampleRate; }
int getNumChannels() { return kChannelCount; }
// Call this from Activity onPause()
void stopAudio() {
// Stop, close and delete in case not already closed.
std::lock_guard<std::mutex> lock(mLock);
if (mStream) {
mStream->stop();
mStream->close();
mStream.reset();
}
mIsPlaying = false;
}
bool isPlaying() { return mIsPlaying; }
void setMusic(std::shared_ptr<AudioCallbackProvider> cb) {
std::lock_guard<std::mutex> lock(mLock);
mMusic = std::move(cb);
mHaveMusic.store((bool) mMusic);
}
oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) override {
// fetch startBeat register
int isStartBeat = startBeat.load();
if(isStartBeat) {
if(beatIdx.size() < numBeatsPlaying+1) beatIdx.resize(numBeatsPlaying+1);
beatIdx[numBeatsPlaying++] = 0;
startBeat.store(0); // reset startBeat register
}
//
// mix audio events, each event has a separate idx counter in beatIdx
//
float *floatData = (float *) audioData;
int N = (int) beatSound.size();
for (int i = 0; i < numFrames; i++) {
float sample = 0.0;
float norm = (float) (std::max(numBeatsPlaying, 1));
for (int k = 0; k < numBeatsPlaying; k++) {
sample += beatSound[beatIdx[k]];
beatIdx[k] += 1;
}
// reduce beatIdx to the events which have not completed playing
int l = 0;
int m = 0;
for (; m < numBeatsPlaying; l++, m++) {
while(m < numBeatsPlaying && beatIdx[m] >= N) m++;
if(m < numBeatsPlaying)
beatIdx[l] = beatIdx[m];
else
break; // avoid incrementing l
}
//beatIdx.resize(l); // we avoid re-allocating
numBeatsPlaying = l;
// set left and right output channels
for (int j = 0; j < kChannelCount; j++) {
// normalize sample by numBeatsPlaying (avoids clipping)
floatData[i * kChannelCount + j] = sample / norm;
}
}
if(mHaveMusic.load()) {
// note: the contract for onAudioReady() upon is_finished=true implies it will not do anything
// (the buffer must be set to all-0 here in the caller)
mMusic->onAudioReady(floatData, numFrames);
}
return oboe::DataCallbackResult::Continue;
}
private:
std::mutex mLock;
std::shared_ptr<oboe::AudioStream> mStream;
std::shared_ptr<AudioCallbackProvider> mMusic;
std::atomic<bool> mHaveMusic;
// Stream params
static int constexpr kChannelCount = 2;
static int constexpr kSampleRate = 48000;
};
#endif //LOCKSTEP_MIXINGPLAYER_H

View File

@@ -1,78 +0,0 @@
//
// Created by david on 01.02.2026.
//
#ifndef LOCKSTEP_OBOESINEPLAYER_H
#define LOCKSTEP_OBOESINEPLAYER_H
#include <oboe/Oboe.h>
#include <math.h>
using namespace oboe;
class OboeSinePlayer: public oboe::AudioStreamDataCallback {
public:
virtual ~OboeSinePlayer() = default;
// Call this from Activity onResume()
int32_t startAudio() {
std::lock_guard<std::mutex> lock(mLock);
oboe::AudioStreamBuilder builder;
// The builder set methods can be chained for convenience.
Result result = builder.setSharingMode(oboe::SharingMode::Exclusive)
->setPerformanceMode(oboe::PerformanceMode::LowLatency)
->setChannelCount(kChannelCount)
->setSampleRate(kSampleRate)
->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::Medium)
->setFormat(oboe::AudioFormat::Float)
->setDataCallback(this)
->openStream(mStream);
if (result != Result::OK) return (int32_t) result;
// Typically, start the stream after querying some stream information, as well as some input from the user
result = mStream->requestStart();
return (int32_t) result;
}
// Call this from Activity onPause()
void stopAudio() {
// Stop, close and delete in case not already closed.
std::lock_guard<std::mutex> lock(mLock);
if (mStream) {
mStream->stop();
mStream->close();
mStream.reset();
}
}
oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) override {
float *floatData = (float *) audioData;
for (int i = 0; i < numFrames; ++i) {
float sampleValue = kAmplitude * sinf(mPhase);
for (int j = 0; j < kChannelCount; j++) {
floatData[i * kChannelCount + j] = sampleValue;
}
mPhase += mPhaseIncrement;
if (mPhase >= kTwoPi) mPhase -= kTwoPi;
}
return oboe::DataCallbackResult::Continue;
}
private:
std::mutex mLock;
std::shared_ptr<oboe::AudioStream> mStream;
// Stream params
static int constexpr kChannelCount = 2;
static int constexpr kSampleRate = 48000;
// Wave params, these could be instance variables in order to modify at runtime
static float constexpr kAmplitude = 0.5f;
static float constexpr kFrequency = 440;
static float constexpr kPI = M_PI;
static float constexpr kTwoPi = kPI * 2;
static double constexpr mPhaseIncrement = kFrequency * kTwoPi / (double) kSampleRate;
// Keeps track of where the wave is
float mPhase = 0.0;
};
#endif //LOCKSTEP_OBOESINEPLAYER_H

View File

@@ -6,19 +6,455 @@
#include "PlaybackEngine.h"
#include "logging.h"
#include "mpg123.h"
#include "mp3file.h"
#include <memory>
#include <vector>
#include <chrono>
PlaybackEngine::PlaybackEngine() {
/**
* Read samples from the next mp3-frame into the struct MP3File's buffer.
* @return number of samples read (uses buffer_size which is much smaller than total length)
*/
static inline int readBuffer2(MP3File* mp3)
{
size_t done = 0;
int err = mpg123_read(mp3->handle, mp3->buffer, mp3->buffer_size, &done);
mp3->remaining_samples -= done / sizeof(int16_t);
mp3->offset = 0;
if (err != MPG123_OK && err != MPG123_DONE) LOGE("mpg123_read() err=%d done=%d", err, done);
if(err == MPG123_DONE) return done / sizeof(int16_t);
return err != MPG123_OK ? 0 : done / sizeof(int16_t);
}
static bool read_mp3(std::string filename, std::vector<float>& samples) {
// TODO: assumes 48000 Hz sampling rate of the file
// note: our resource file is mono (1 channel = 1 samples_per_frame)!
MP3File *mFile = mp3file_open(filename.c_str(), 0); // MPG123_ENC_FLOAT_32 (but does not seem to work)
memset(mFile->buffer, 0, mFile->buffer_size);
bool ok1 = mFile != nullptr;
int ok2 = true;
int i = 0;
if(ok1) {
samples.resize(mFile->num_samples);
while (ok2 && mFile->remaining_samples > 0) {
ok2 = readBuffer2(mFile);
if (!ok2) break;
for (int j = 0; j < ok2 && i < mFile->num_samples; j++, i++) {
int16_t *src = ((int16_t *) mFile->buffer) + j;
samples[i] = (*src) / 32768.0f;
}
}
}
if(!ok2) {
LOGI("ok2=false at i=%d", i);
}
if(mFile)
mp3file_delete(mFile);
return ok1 && ok2;
}
struct RbLogger : public RubberBand::RubberBandStretcher::Logger {
virtual void log(const char *s) {
LOGI("%s", s);
}
virtual void log(const char *s, double val) {
LOGI("%s (%lf)", s, val);
}
virtual void log(const char *s, double val1, double val2) {
LOGI("%s (%lf, %lf)", s, val1, val2);
}
virtual ~RbLogger() { }
};
PlaybackEngine::PlaybackEngine(std::string filesDir, int resid):
stretcher(
/* sampleRate: */ 48000,
/* channels: */ 2,
/* logger: */ std::make_shared<RbLogger>(),
/* options: */
RubberBand::RubberBandStretcher::OptionEngineFaster |
RubberBand::RubberBandStretcher::OptionProcessRealTime
),
mFilesDir(filesDir),
haveMusicFile(false),
exitMusicFeedThread(false),
isSetMusic(false),
android_fd(0),
haveTimeRatio(false),
timeRatio(1.0),
// these 3 values are preliminary -- will be set from MixingPlayer defaults in the ctor body below
playbackRate(48000),
numOutChannels(2),
numInChannels(2),
back_pressure(0)
{
LOGI("PlaybackEngine()");
LOGI("NDK LOG_LEVEL=%d", LOG_LEVEL);
// NDK LOG_LEVEL=3 (DEBUG)
mPlayer = new OboeSinePlayer();
int32_t res = mPlayer->startAudio();
LOGI("startAudio() = %d", res);
// load "bump" sound effect
std::vector<float> samples;
bool is_ok = read_mp3(mFilesDir + "/" + std::to_string(resid) + ".mp3", samples);
LOGI("read_mp3() for bump effect, is_ok=%d", is_ok);
mPlayer = new MixingPlayer(samples);
// configure stretcher and start musicFeedThread()
initRubberBand();
}
void PlaybackEngine::initRubberBand() {
// we do not yet have a music file with actual sampling rate, so set the default ratio
stretcher.reset();
stretcher.setTimeRatio(1.0);
stretcher.setPitchScale(1.0);
stretcher.setDebugLevel(1); // 1: errors only. generally 0..4
// feed samples into 'stretcher' and read bogus output
// getPreferredStartPad() -> [ ... ] -> getStartDelay()
// write silence
// process() input: de-interleaved audio data with one float array per channel.
size_t num_pad = stretcher.getPreferredStartPad();
float* buf = (float*) malloc(num_pad*2*sizeof(float));
float* buf_ptr[] {buf, buf + num_pad};
memset(buf, 0, num_pad*2*sizeof(float));
stretcher.process(buf_ptr, num_pad, false);
free(buf);
//LOGI("start_pad = %d", num_pad);
// read bogus output
size_t start_delay = stretcher.getStartDelay();
float* out_buf = (float*) malloc(start_delay*2*sizeof(float));
float* out_buf_ptr[] {out_buf, out_buf + start_delay};
memset(out_buf, 0, start_delay*2*sizeof(float));
stretcher.retrieve(out_buf_ptr, start_delay);
//LOGI("start_delay = %d", start_delay);
// thread 1: oboe cb fetching via retrieve()
//mPlayer->setMusic();
// thread 2: polling for decoding more mp3 -> process() -- getSamplesRequired()
// setTimeRatio(), setPitchScale() -- always call them from thread 2
musicFeed = std::make_unique<std::thread>(&PlaybackEngine::musicFeedThread, this);
}
void PlaybackEngine::closeRubberBand() {
if(musicFeed) {
exitMusicFeedThread.store(true);
musicFeed->join();
musicFeed = nullptr;
}
closeMusicFile();
}
void PlaybackEngine::closeMusicFile() {
haveMusicFile.store(false);
musicFile = nullptr;
if(android_fd) {
close(android_fd);
android_fd = 0;
}
isSetMusic.store(false);
mPlayer->setMusic(nullptr);
}
void PlaybackEngine::mapChannels(int *channel_map, int num_ch_in, int num_ch_out) {
if(num_ch_in == num_ch_out) {
// map each channel as-is
for(int i = 0; i < num_ch_out; i++)
channel_map[i] = i;
} else if(num_ch_in == 1) {
// map mono to all output channels
for(int i = 0; i < num_ch_out; i++)
channel_map[i] = 0;
} else if(num_ch_in >= 2) {
// use a stereo mapping
for(int i = 0; i < num_ch_out; i++)
channel_map[i] = i;
} else {
LOGE("mapChannels(): strange channel layout, mapping to mono. num_ch_in=%d num_ch_out=%d", num_ch_in, num_ch_out);
// map mono to all output channels
for(int i = 0; i < num_ch_out; i++)
channel_map[i] = 0;
}
// TODO: check broken input (0 channels etc) and bubble up an error to app
}
void PlaybackEngine::musicFeedThread() {
LOGI("starting musicFeedThread()");
// strecher num channels: same as output num channels
// (this is because we play silence even without any input file, so we cannot set stretcher
// channel count for the music file's channel count)
int num_ch_in = numInChannels.load();
int num_ch_out = numOutChannels.load();
size_t num_buf_samples = buf_size_samples;
size_t buf_stride = num_buf_samples;
size_t buf_size_bytes = num_buf_samples * num_ch_out * sizeof(float);
float* buf = (float*) malloc(buf_size_bytes);
float** buf_ptr = (float**) malloc(num_ch_out * sizeof(float*));
for(int i = 0; i < num_ch_out; i++) {
buf_ptr[i] = buf + i * num_buf_samples;
}
memset(buf, 0, buf_size_bytes);
// preliminary allocation (actual music file buffer is unknown due to unknown channel count)
size_t cbuf_size_bytes = num_buf_samples * num_ch_in * sizeof(int16_t);
//size_t cbuf_load_bytes = num_buf_samples * num_ch_in * sizeof(int16_t);
unsigned char* cbuf = (unsigned char*) malloc(cbuf_size_bytes);
memset(cbuf, 0, cbuf_size_bytes);
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)
size_t loop_delay_us = 1000000 * buf_size_samples / playbackRate.load();
int idebug = 0;
// thread 2: polling for decoding more mp3 -> process() -- getSamplesRequired()
while(!exitMusicFeedThread.load()) {
if(!haveMusicFile.load()) {
// while no MusicProvider is connected, no samples will be read from 'stretcher'
// therefore, we do not write any samples into it!
std::this_thread::sleep_for(std::chrono::milliseconds(50));
continue;
}
if(!isSetMusic.load()) {
mPlayer->setMusic(std::make_shared<MusicProvider>(&stretcher, buf_size_samples, numOutChannels.load(), &back_pressure));
isSetMusic.store(true);
}
if(haveTimeRatio.load()) {
double ratio = timeRatio.load();
stretcher.setTimeRatio(ratio);
stretcher.setPitchScale(1.0 / ratio);
haveTimeRatio.store(false);
}
// change buffer size, if necessary (changed input channel count)
if(numInChannels.load() != num_ch_in) {
LOGD("changed buffer size (changed input channel count)");
num_ch_in = numInChannels.load();
free(cbuf);
cbuf_size_bytes = num_buf_samples * num_ch_in * sizeof(int16_t);
cbuf = (unsigned char*) malloc(cbuf_size_bytes);
memset(cbuf, 0, cbuf_size_bytes);
}
mapChannels(channel_map, num_ch_in, num_ch_out);
// do work ...
// note: getSamplesRequired() itself only gives us how many samples to create another
// output buffer increment, not if the output buffer has been emptied.
// We need to manage the buffer sizes ourselves.
// this draft should always keep the output buffers filled at 50-100 ms
int target_output_buffer_frames = 100 * playbackRate.load() / 1000; // 100 ms worth of audio
if(idebug < 10) {
LOGI("back_pressure available=%d target=%d", back_pressure.load(), target_output_buffer_frames);
}
if(back_pressure.load() >= target_output_buffer_frames) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
// at 48000 Hz playbackRate, the 512-1024 frames returned here give us additional (10-21 ms) output buffer
// (this is somewhat approximate, but the above control loop should keep us within a reasonable buffer size)
size_t num_samples = stretcher.getSamplesRequired();
// note: how much to sleep until output has played x samples...?
// how large is one buffer, and when do we feed it more data?
// (is it like double-buffering implemented in 'stretcher'?)
if (num_samples == 0) {
// this was never the case in actual testing -- see note above.
LOGD("waiting %d us for getSamplesRequired()", loop_delay_us);
std::this_thread::sleep_for(std::chrono::microseconds(loop_delay_us));
continue;
}
if (num_samples > num_buf_samples) {
LOGE("wanted %d samples but buf is only %d samples", num_samples, num_buf_samples);
num_samples = num_buf_samples;
}
if (!haveMusicFile.load()) {
loop_delay_us = 1000000 * num_samples / playbackRate.load();
if(idebug++ < 10) {
LOGI("feed %d silence samples", num_samples);
// 1024, 512, 512
// 7 x 512
}
memset(buf, 0, num_samples*num_ch_out*sizeof(float));
stretcher.process(buf_ptr, num_samples, false);
continue;
}
if(idebug++ < 10) {
loop_delay_us = 1000000 * num_samples / musicFile->rate;
LOGI("feed %d music samples", num_samples);
// feed 1024 music samples
// => stretcher is asking for 1024 = getSamplesRequired()
// ca. 21 ms
}
size_t done = 0; // bytes!
size_t read_size_bytes = std::min(num_samples * num_ch_in * sizeof(int16_t), cbuf_size_bytes);
int err = mpg123_read(musicFile->handle, cbuf, read_size_bytes, &done);
musicFile->remaining_samples -= done / sizeof(int16_t);
musicFile->offset = 0; // unused here
if (err != MPG123_OK && err != MPG123_DONE) {
// error!
LOGE("error reading mp3 file: mpg123_read() err=%d done=%d", err, done);
// next iteration will play silence
closeMusicFile();
stretcher.setTimeRatio(1.0); // buffer size for playing silence is computed from 'playbackRate', so reset timeRatio
stretcher.setPitchScale(1.0);
stretcher.process(buf_ptr, 0, true); // set end of playback
mPlayer->stopAudio();
continue;
}
if(err == MPG123_DONE) {
// next iteration will play silence
LOGI("finished reading mp3 file (MPG123_DONE)");
closeMusicFile();
stretcher.setTimeRatio(1.0); // buffer size for playing silence is computed from 'playbackRate', so reset timeRatio
stretcher.setPitchScale(1.0);
stretcher.process(buf_ptr, 0, true); // set end of playback
mPlayer->stopAudio();
continue;
}
size_t num_decoded_samples = done / sizeof(int16_t) / num_ch_in;
//LOGD("num_decoded_samples = %d", num_decoded_samples);
// * convert interleaved int16 to de-interleaved float [-1.0, 1.0] format
// * map input to output channels
for(size_t i = 0; i < num_decoded_samples; i++) {
for(size_t j = 0; j < num_ch_out; j++) {
buf[i + buf_stride * j] = static_cast<float>(*(reinterpret_cast<int16_t*>(cbuf) + i * num_ch_in + channel_map[j])) / 32768.0f;
}
}
//LOGD("calling stretcher.process()");
stretcher.process(buf_ptr, num_decoded_samples, false);
}
LOGI("musicFeedThread() exiting ...");
free(buf);
free(buf_ptr);
free(cbuf);
free(channel_map);
LOGI("musicFeedThread() exited.");
}
PlaybackEngine::~PlaybackEngine() {
LOGI("~PlaybackEngine()");
closeRubberBand();
mPlayer->stopAudio();
delete mPlayer;
mPlayer = nullptr;
}
void PlaybackEngine::playBeat() {
if(mPlayer) mPlayer->setStartBeat();
}
void PlaybackEngine::playMusic(int fd) {
if(!mPlayer) return;
LOGI("PlaybackEngine::playMusic(fd=%d)", fd);
android_fd = fd;
musicFile.reset(mp3file_open_fd(android_fd, 0));
if(musicFile) {
timeRatio.store(((double) playbackRate.load()) / ((double) musicFile->rate));
haveTimeRatio.store(true);
numInChannels.store(musicFile->channels);
haveMusicFile.store(true);
}
bool is_finished = (stretcher.available() == -1);
if(is_finished) {
// so that we may play again after "final chunk"
closeRubberBand();
initRubberBand();
}
if(!mPlayer->isPlaying()) {
int32_t res = mPlayer->startAudio();
playbackRate.store(mPlayer->getRate());
numOutChannels.store(mPlayer->getNumChannels());
LOGI("startAudio() = %d rate=%d channels=%d", res, playbackRate.load(), numOutChannels.load());
}
// to wait the 50 ms that the musicFeedThread() is idling when it first receives a file
// we don't call mPlayer->setMusic() here, but in the musicFeedThread()
}
MusicProvider::MusicProvider(RubberBand::RubberBandStretcher *stretcher, size_t buf_size_samples, int num_ch_out, std::atomic<int> *back_pressure) :
stretcher(stretcher),
idebug(0),
buf_size_samples(buf_size_samples),
num_ch_out(num_ch_out),
back_pressure(back_pressure)
{
buf = (float*) malloc(buf_size_samples*num_ch_out*sizeof(float));
buf_ptr = (float**) malloc(num_ch_out * sizeof(float*));
for(int i = 0; i < num_ch_out; i++) {
buf_ptr[i] = buf + i * buf_size_samples;
}
}
MusicProvider::~MusicProvider() {
free(buf);
free(buf_ptr);
}
void MusicProvider::onAudioReady(float *data, int32_t frames) {
if(idebug++ < 10) {
LOGI("onAudioReady() frames=%d", frames);
// frames=96 (48 kHz => 2 ms!!)
}
if(frames > buf_size_samples) {
LOGE("audio buffer too small! adapt PlaybackEngine::buf_size_samples!! asked for frames=%d but buf_size=%d", frames, buf_size_samples);
}
// 1. read from oboe into our temp de-interleaved buffer 'buf'
int num_frames_requested = std::min((int) frames, (int) buf_size_samples);
int num_frames_available = stretcher->available();
bool is_finished = (num_frames_available == -1);
(*back_pressure).store((int) num_frames_available);
if(is_finished) {
return;
}
if(idebug < 10) {
LOGI("onAudioReady() available=%d", num_frames_available);
}
if(num_frames_available < num_frames_requested) {
// this is an audio glitch
// TODO: bubble info upwards, in a counter (so we can collect device-specific glitch stats)
LOGI("stretcher lag: %d requested, %d available", num_frames_requested, num_frames_available);
}
size_t num_frames = std::min(num_frames_available, num_frames_requested);
stretcher->retrieve(buf_ptr, num_frames);
// 2. convert to add samples to interleaved *data
for(size_t i = 0; i < num_frames; i++) {
for(size_t j = 0; j < num_ch_out; j++) {
float sample = data[i*num_ch_out + j];
sample += buf_ptr[j][i];
sample /= 2.0;
data[i*num_ch_out + j] = sample;
}
}
}

View File

@@ -5,14 +5,67 @@
#ifndef LOCKSTEP_PLAYBACKENGINE_H
#define LOCKSTEP_PLAYBACKENGINE_H
#include "OboeSinePlayer.h"
#include "StepListener.h"
#include "MixingPlayer.h"
#include "RubberBandStretcher.h"
#include "mp3file.h"
#include "AudioCallback.h"
#include <string>
#include <thread>
#include <memory>
#include <atomic>
class PlaybackEngine {
/** Provides music through a regular callback to oboe. Called from separate oboe thread. */
class MusicProvider : public AudioCallbackProvider {
public:
PlaybackEngine();
virtual ~PlaybackEngine();
explicit MusicProvider(RubberBand::RubberBandStretcher *stretcher, size_t buf_size_samples, int num_ch_out, std::atomic<int> *back_pressure);
~MusicProvider() override;
/** Called from separate oboe thread. */
void onAudioReady(float *data, int32_t frames) override;
private:
OboeSinePlayer *mPlayer;
RubberBand::RubberBandStretcher *stretcher;
float *buf;
float **buf_ptr;
int idebug;
size_t buf_size_samples;
int num_ch_out;
/** contains the current available() frames from 'stretcher' in the audio callback thread 2 (oboe) */
std::atomic<int> *back_pressure;
};
class PlaybackEngine : public StepListener {
public:
PlaybackEngine(std::string filesDir, int resid);
virtual ~PlaybackEngine();
/** Play a beat sound. */
virtual void playBeat();
void playMusic(int fd);
private:
RubberBand::RubberBandStretcher stretcher;
MixingPlayer *mPlayer;
std::string mFilesDir;
std::unique_ptr<MP3File> musicFile;
std::atomic<bool> haveMusicFile;
std::unique_ptr<std::thread> musicFeed;
std::atomic<bool> exitMusicFeedThread;
/** where musicFeedThread() keeps track of the fact that we have music set -- will start the audio cb */
std::atomic<bool> isSetMusic;
int android_fd;
std::atomic<bool> haveTimeRatio;
std::atomic<double> timeRatio;
std::atomic<int> playbackRate;
std::atomic<int> numOutChannels;
std::atomic<int> numInChannels;
/** contains the current available() frames from 'stretcher' in the audio callback thread 2 (oboe) */
std::atomic<int> back_pressure;
/** this is actually in frames, not samples */
static size_t constexpr buf_size_samples = 1024;
void initRubberBand();
void closeRubberBand();
void closeMusicFile();
void musicFeedThread();
void mapChannels(int *channel_map, int num_ch_in, int num_ch_out);
};
#endif //LOCKSTEP_PLAYBACKENGINE_H

View File

@@ -0,0 +1,49 @@
//
// Created by david on 03.03.2026.
//
#include "StepDetector.h"
// TODO: repeated code in libpasada. We could use the StepDetector class from there in jni_stepdetector.cpp
// 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),
f_sqi(upslope_width)
{}
#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);
auto q5 = f_sqi.filter(s2, s3, s4);
// is step, step quality is OK, and we have a listener?
if(s4 > 0.0 && q5 > 0.0 && listener != nullptr) {
listener->playBeat();
}
}

View File

@@ -0,0 +1,27 @@
//
// 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;
RunningQualityFilter f_sqi;
public:
StepDetector(StepListener *listener);
void filter(std::vector<float> values);
};
#endif //LOCKSTEP_STEPDETECTOR_H

View 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

View File

@@ -0,0 +1,52 @@
#include "PlaybackEngine.h"
#include <jni.h>
#include <oboe/Oboe.h>
extern "C" {
/**
* Creates the audio engine
*
* @return a pointer to the audio engine. This should be passed to other methods
*/
JNIEXPORT jlong JNICALL
Java_at_lockstep_pb_PlaybackEngine_native_1createEngine(
JNIEnv *env,
jclass /*unused*/, jstring filesDir, jint resid) {
const char* filesDirTemp = env->GetStringUTFChars(filesDir, NULL);
std::string filesDirString(filesDirTemp);
env->ReleaseStringUTFChars(filesDir, filesDirTemp);
// We use std::nothrow so `new` returns a nullptr if the engine creation fails
auto *engine = new(std::nothrow) PlaybackEngine(filesDirString, resid);
return reinterpret_cast<jlong>(engine);
}
JNIEXPORT void JNICALL
Java_at_lockstep_pb_PlaybackEngine_native_1deleteEngine(
JNIEnv *env,
jclass,
jlong engineHandle) {
delete reinterpret_cast<PlaybackEngine *>(engineHandle);
}
JNIEXPORT void JNICALL
Java_at_lockstep_pb_PlaybackEngine_native_1setDefaultStreamValues(JNIEnv *env,
jclass type,
jint sampleRate,
jint framesPerBurst) {
oboe::DefaultStreamValues::SampleRate = (int32_t) sampleRate;
oboe::DefaultStreamValues::FramesPerBurst = (int32_t) framesPerBurst;
}
JNIEXPORT void JNICALL
Java_at_lockstep_pb_PlaybackEngine_native_1playMusic(JNIEnv *env,
jclass type,
jlong engineHandle,
jint fd) {
auto engine = reinterpret_cast<PlaybackEngine *>(engineHandle);
engine->playMusic(fd);
}
} // extern "C"

View File

@@ -0,0 +1,19 @@
//
// Created by david on 02.03.2026.
//
#include <jni.h>
#include "mpg123.h"
#include <oboe/Oboe.h>
extern "C" {
// nice-to: merge with lockstep.cpp
JNIEXPORT jint JNICALL
Java_at_lockstep_pb_PlaybackEngine_native_1mpg123_1init
(JNIEnv *env, jclass) {
return mpg123_init();
}
}

View 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);
}
}

View File

@@ -1,54 +0,0 @@
// Write C++ code here.
//
// Do not forget to dynamically load the C++ library into your application.
//
// For instance,
//
// In at.lockstep.app.MainActivity.java:
// static {
// System.loadLibrary("lockstep");
// }
//
// Or, in at.lockstep.app.MainActivity.kt:
// companion object {
// init {
// System.loadLibrary("lockstep")
// }
// }
#include "PlaybackEngine.h"
#include <jni.h>
#include <oboe/Oboe.h>
extern "C" {
/**
* Creates the audio engine
*
* @return a pointer to the audio engine. This should be passed to other methods
*/
JNIEXPORT jlong JNICALL
Java_at_lockstep_pb_PlaybackEngine_native_1createEngine(
JNIEnv *env,
jclass /*unused*/) {
/*
const char* filesDirTemp = env->GetStringUTFChars(filesDir, NULL);
std::string filesDirString(filesDirTemp);
env->ReleaseStringUTFChars(filesDir, filesDirTemp);
*/
// We use std::nothrow so `new` returns a nullptr if the engine creation fails
auto *engine = new(std::nothrow) PlaybackEngine();
return reinterpret_cast<jlong>(engine);
}
JNIEXPORT void JNICALL
Java_at_lockstep_pb_PlaybackEngine_native_1deleteEngine(
JNIEnv *env,
jclass,
jlong engineHandle) {
delete reinterpret_cast<PlaybackEngine *>(engineHandle);
}
} // extern "C"

View File

@@ -0,0 +1,164 @@
//
// Created by david on 17.05.20.
//
#define LOG_TAG "mp3file"
#include "mp3file.h"
#include <unistd.h>
#include <string.h>
#include <cstdlib>
#include "logging.h"
MP3File* mp3file_init(mpg123_handle *handle) {
MP3File* mp3file = (MP3File *) malloc(sizeof(MP3File));
if(mp3file == NULL) return NULL;
memset(mp3file, 0, sizeof(MP3File));
mp3file->handle = handle;
return mp3file;
}
void mp3file_delete(MP3File *mp3file) {
if(mp3file == NULL) return;
mpg123_close(mp3file->handle);
mpg123_delete(mp3file->handle);
if(mp3file->buffer) {
free(mp3file->buffer);
mp3file->buffer = 0;
}
if(mp3file->android_fd) close(mp3file->android_fd);
free(mp3file);
}
MP3File* mp3file_open(const char *filename, int forceEncoding) {
const char *errorText = "";
#define handleError(text) \
do { \
errorText = text; \
goto on_error; \
} while(0)
int err = MPG123_OK;
mpg123_handle *mh = mpg123_new(NULL, &err);
if(err != MPG123_OK || mh == NULL) {
LOGE("mpg123_new() failed: %s", mpg123_plain_strerror(err));
return NULL;
}
MP3File* mp3 = mp3file_init(mh);
if(mp3 == NULL) handleError("malloc() failed");
err = mpg123_open(mh, filename);
if(err != MPG123_OK) handleError("mpg123_open()");
int encoding;
err = mpg123_getformat(mh, &mp3->rate, &mp3->channels, &encoding);
if(err != MPG123_OK) handleError("mpg123_getformat()");
if(encoding != MPG123_ENC_SIGNED_16) handleError("unknown file encoding");
if(forceEncoding != 0) {
encoding = forceEncoding;
}
// Ensure that this output format will not change
// (it could, when we allow it).
mpg123_format_none(mh);
err = mpg123_format(mh, mp3->rate, mp3->channels, encoding);
if(err != MPG123_OK) handleError("could not set mpg123_format()");
mp3->buffer_size = mpg123_outblock(mh);
mp3->buffer = (unsigned char*) malloc(mp3->buffer_size);
if(mp3->buffer == NULL) handleError("malloc() failed");
mp3->num_samples = mpg123_length(mh);
mp3->samples_per_frame = mpg123_spf(mh);
mp3->secs_per_frame = mpg123_tpf(mh);
if (mp3->num_samples == MPG123_ERR || mp3->samples_per_frame < 0)
mp3->num_frames = 0;
else
mp3->num_frames = mp3->num_samples / mp3->samples_per_frame;
if (mp3->num_samples == MPG123_ERR || mp3->samples_per_frame < 0 || mp3->secs_per_frame < 0)
mp3->duration = 0;
else
mp3->duration = mp3->num_samples / mp3->samples_per_frame * mp3->secs_per_frame;
mp3->offset = 0;
mp3->remaining_samples = (int) mp3->num_samples;
LOGV("channels: %d rate: %ld num_samples: %ld", mp3->channels, mp3->rate, mp3->num_samples);
return mp3;
on_error:
LOGE("%s, err = %s", errorText, mpg123_plain_strerror(err));
mp3file_delete(mp3);
return NULL;
#undef handleError
}
MP3File* mp3file_open_fd(int fd, int forceEncoding) {
const char *errorText = "";
#define handleError(text) \
do { \
errorText = text; \
goto on_error; \
} while(0)
int err = MPG123_OK;
mpg123_handle *mh = mpg123_new(NULL, &err);
if(err != MPG123_OK || mh == NULL) {
LOGE("mpg123_new() failed: %s", mpg123_plain_strerror(err));
return NULL;
}
MP3File* mp3 = mp3file_init(mh);
if(mp3 == NULL) handleError("malloc() failed");
err = mpg123_open_fd(mh, fd);
if(err != MPG123_OK) handleError("mpg123_open()");
int encoding;
err = mpg123_getformat(mh, &mp3->rate, &mp3->channels, &encoding);
if(err != MPG123_OK) handleError("mpg123_getformat()");
if(encoding != MPG123_ENC_SIGNED_16) handleError("unknown file encoding");
if(forceEncoding != 0) {
encoding = forceEncoding;
}
// Ensure that this output format will not change
// (it could, when we allow it).
mpg123_format_none(mh);
err = mpg123_format(mh, mp3->rate, mp3->channels, encoding);
if(err != MPG123_OK) handleError("could not set mpg123_format()");
mp3->buffer_size = mpg123_outblock(mh);
mp3->buffer = (unsigned char*) malloc(mp3->buffer_size);
if(mp3->buffer == NULL) handleError("malloc() failed");
mp3->num_samples = mpg123_length(mh);
mp3->samples_per_frame = mpg123_spf(mh);
mp3->secs_per_frame = mpg123_tpf(mh);
if (mp3->num_samples == MPG123_ERR || mp3->samples_per_frame < 0)
mp3->num_frames = 0;
else
mp3->num_frames = mp3->num_samples / mp3->samples_per_frame;
if (mp3->num_samples == MPG123_ERR || mp3->samples_per_frame < 0 || mp3->secs_per_frame < 0)
mp3->duration = 0;
else
mp3->duration = mp3->num_samples / mp3->samples_per_frame * mp3->secs_per_frame;
mp3->offset = 0;
mp3->remaining_samples = (int) mp3->num_samples;
LOGI("channels: %d rate: %ld num_samples: %ld", mp3->channels, mp3->rate, mp3->num_samples);
mp3->android_fd = fd;
return mp3;
on_error:
LOGE("%s, err = %s", errorText, mpg123_plain_strerror(err));
mp3file_delete(mp3);
return NULL;
#undef handleError
}

View File

@@ -0,0 +1,34 @@
//
// Created by david on 17.05.20.
//
#ifndef SAMPLES_MP3FILE_H
#define SAMPLES_MP3FILE_H
#include "mpg123.h"
struct MP3File
{
mpg123_handle* handle;
int android_fd;
int channels;
long rate;
/** num samples in total (stereo of 10 frames will have 20 'samples' here) */
long num_samples;
int samples_per_frame;
double secs_per_frame;
long num_frames;
double duration;
size_t buffer_size;
unsigned char* buffer;
/** total samples (stereo of 10 frames remaining will have 20 'remaining_samples' here) */
int remaining_samples;
size_t offset;
};
MP3File* mp3file_init(mpg123_handle *handle);
void mp3file_delete(MP3File *mp3file);
MP3File* mp3file_open(const char *filename, int forceEncoding = 0);
MP3File* mp3file_open_fd(int fd, int forceEncoding = 0);
#endif //SAMPLES_MP3FILE_H

View File

@@ -0,0 +1,292 @@
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.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.PowerManager;
import android.util.Log;
import android.widget.Toast;
import android.os.SystemClock;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
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, String contentUri) {
Intent intent = new Intent(context, LstForegroundService.class);
intent.setAction(ACTION_START);
intent.putExtra("content_uri", contentUri);
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();
Log.i("LstForegroundService", "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)) {
Log.i("LstForegroundService", "onStartCommand() ACTION_START");
String contentUri = intent.getStringExtra("content_uri");
try {
if(contentUri != null) {
PlaybackEngine.playMusic(uriToFd(contentUri));
}
} catch (IOException e) {
// TODO proper error handling
Toast.makeText(this, "Could not open music file contentUri", Toast.LENGTH_LONG).show();
throw new RuntimeException(e);
}
startCollection(contentUri);
} else if (ACTION_STOP.equals(action)) {
Log.i("LstForegroundService", "ACTION_STOP");
stopCollectionAndSelf();
}
}
return START_STICKY;
}
/** caller is responsible for close()-ing returned file descriptor! */
private int uriToFd(String persistedUriString) throws IOException {
Uri uri = Uri.parse(persistedUriString);
ParcelFileDescriptor pfd =
getContentResolver().openFileDescriptor(uri, "r");
if (pfd == null) {
throw new IOException("openFileDescriptor() returned null for URI: " + uri);
}
int fd = -1;
try {
fd = pfd.detachFd(); // Native side must close(fd)
} finally {
pfd.close(); // Safe after detach; Java wrapper no longer owns the fd
}
return fd;
}
private void startCollection(String meta) {
if (isCollecting) {
return;
}
startForeground(NOTIFICATION_ID, buildNotification());
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) {
// TODO: use a HandlerThread to handle sensor events in background thread, not the main thread
// see https://stackoverflow.com/q/17681870/1616948
sensorManager.registerListener(
this,
accelerometer,
SensorManager.SENSOR_DELAY_GAME
);
isCollecting = true;
onStartRecording(meta);
} else {
stopCollectionAndSelf();
}
}
private void stopCollectionAndSelf() {
if (isCollecting && sensorManager != null) {
sensorManager.unregisterListener(this);
isCollecting = false;
onStopRecording();
}
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)
Log.d("LstForegroundService", "onDestroy(), calling PlaybackEngine.delete()");
if(stepDetector != null) {
stepDetector.close();
PlaybackEngine.delete();
stepDetector = null;
}
super.onDestroy();
}
public class LocalBinder extends Binder {
LstForegroundService getService() { return LstForegroundService.this; }
}
private final LocalBinder binder = new LocalBinder();
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.i("LstForegroundService", "onBind()");
return binder;
}
public interface OnResultListener {
void onResult(SensorDataArray recording);
}
private OnResultListener listener;
public void setOnResultListener(OnResultListener listener) { this.listener = listener; }
/** single sensor sample */
public static class SensorData {
private long timestamp;
private float[] values;
public SensorData(SensorEvent event) {
timestamp = event.timestamp;
values = Arrays.copyOf(event.values, event.values.length);
}
public SensorData(long timestamp, float[] values) {
this.timestamp = timestamp;
this.values = values;
}
}
/** array of sensor samples */
public static class SensorDataArray {
private ArrayList<SensorData> data = new ArrayList<SensorData>();
private String meta;
public void add(SensorEvent event) { data.add(new SensorData(event)); }
public void add(SensorData d) { data.add(d); }
public void clear() { data.clear(); }
public void setMeta(String meta) { this.meta = meta; }
}
private final SensorDataArray recording = new SensorDataArray();
private long recordingStartTime = 0;
private void onStartRecording(String meta) {
recordingStartTime = SystemClock.elapsedRealtimeNanos();
recording.setMeta(meta);
}
private void onStopRecording() {
if(listener != null) {
listener.onResult(recording);
}
recording.clear();
}
@Override
public void onSensorChanged(SensorEvent event) {
// pass on to C++ filter bank
stepDetector.filter(event.timestamp, event.values);
// collect accelerometer recording - adjust timebase to 0.0 sec beginning
recording.add(new SensorData(event.timestamp - recordingStartTime, event.values));
// TODO: acquires at 8 ms intervals ... 125 Hz?!
// TODO: must compute actual sampling rate. and either downsample, or adapt the IIR filter parameters. (& length??) - easier to resample.
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
private Notification buildNotification() {
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.notification_text))
.setSmallIcon(getApplicationInfo().icon)
.setOngoing(true)
.build();
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
getString(R.string.app_name),
NotificationManager.IMPORTANCE_LOW
);
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm != null) {
nm.createNotificationChannel(channel);
}
}
}
}

View File

@@ -1,23 +1,176 @@
package at.lockstep.app;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.util.Log;
import android.widget.Button;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import at.lockstep.R;
import at.lockstep.pb.PlaybackEngine;
import at.lockstep.saf.SafPickerActivity;
import at.lockstep.ui.SongPickerActivity;
import android.widget.Toast;
public class MainActivity extends Activity {
/*
* Creating engine in onResume() and destroying in onPause() so the stream retains exclusive
* mode only while in focus. This allows other apps to reclaim exclusive stream mode.
*/
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
public class MainActivity extends AppCompatActivity implements LstForegroundService.OnResultListener {
private Button btnStart;
private Button btnStop;
private Button btnMediaStoreBenchmark;
private Button btnPickSong;
private final ActivityResultLauncher<Intent> launcher;
private String contentUri;
public MainActivity() {
launcher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
Intent data = result.getData();
if (data != null) {
String contentUri = data.getStringExtra("content_uri");
Toast.makeText(this, "Item clicked: " + contentUri, Toast.LENGTH_LONG).show();
this.contentUri = contentUri;
}
}
}
);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnStart = findViewById(R.id.btnStart);
btnStop = findViewById(R.id.btnStop);
btnMediaStoreBenchmark = findViewById(R.id.btnMediaStoreBenchmark);
btnPickSong = findViewById(R.id.btnPickSong);
// TODO: handle clicking START button twice
btnStart.setOnClickListener(v ->
ContextCompat.startForegroundService(
MainActivity.this,
LstForegroundService.startIntent(MainActivity.this, contentUri)
)
);
//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))
);
btnMediaStoreBenchmark.setOnClickListener(v -> {
Intent intent = new Intent(MainActivity.this, MediaStoreBenchmarkActivity.class);
startActivity(intent);
});
btnPickSong.setOnClickListener(v -> {
Intent intent = new Intent(this, SafPickerActivity.class);
launcher.launch(intent);
});
}
private ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
LstForegroundService service = ((LstForegroundService.LocalBinder) iBinder).getService();
service.setOnResultListener(MainActivity.this);
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
}
};
@Override
protected void onStart() {
super.onStart();
// attach ServiceConnection (so we can attach a listener). incidentally, it seems to also create the service. (will currently create a PlaybackEngine, etc.)
// TODO: check if this delays starting the application
bindService(new Intent(this, LstForegroundService.class), conn, BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
unbindService(conn);
Log.i("MainActivity", "onStop()");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.i("MainActivity", "onDestroy()");
// TODO: since the Service keeps running, we must signal oboe to stop playing
// TODO: signal the pause to the C++ lib
startService(LstForegroundService.stopIntent(MainActivity.this));
}
private boolean isForeground = false;
@Override
protected void onPause() {
super.onPause();
isForeground = false;
//
// telltale signs: logcat: "PlaybackEngine - Buffer overrun on output for channel (0.000000)" or (1.000000)
Log.i("MainActivity", "onPause()");
}
@Override
protected void onResume() {
super.onResume();
PlaybackEngine.create(this); // note: called twice (is permission request causing Activity to go out of focus?)
isForeground = true;
}
LstForegroundService.SensorDataArray recording;
@Override
protected void onPause() {
PlaybackEngine.delete();
super.onPause();
public void onResult(LstForegroundService.SensorDataArray recording) {
if(!isForeground) {
Log.i("MainActivity", "ignore onResult() from LstForegroundService due to backgrounded MainActivity");
return;
}
this.recording = recording;
//
// write accelero recording to file
//
File f = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
String dir = f != null ? f.toString() : "/"; // make compiler happy
long unixTime = System.currentTimeMillis() / 1000L;
String fileName = dir + "/acc_" + unixTime + ".json";
Log.i("MainActivity", "written acc rec to " + fileName);
try (Writer writer = new FileWriter(fileName)) {
Gson gson = new GsonBuilder().create();
gson.toJson(recording, writer);
} catch (IOException e) {
// TODO error handling
Log.e("MainActivity", "IOException writing recording: " + e.getMessage());
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,136 @@
package at.lockstep.app;
import android.app.Activity;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;
import at.lockstep.R;
public class MediaStoreBenchmarkActivity extends Activity {
private TextView resultTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_media_store_benchmark);
resultTextView = findViewById(R.id.resultTextView);
if (hasReadPermission()) {
loadMusic();
} else {
requestReadPermission();
}
}
private static final int REQUEST_READ_PERMISSION = 1001;
@Override
public void onRequestPermissionsResult(
int requestCode,
String[] permissions,
int[] grantResults
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_READ_PERMISSION) {
boolean granted = grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (granted) {
loadMusic();
} else {
resultTextView.setText("Permission denied.");
}
}
}
private boolean hasReadPermission() {
String permission;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permission = Manifest.permission.READ_MEDIA_AUDIO;
} else {
permission = Manifest.permission.READ_EXTERNAL_STORAGE;
}
return ContextCompat.checkSelfPermission(this, permission)
== PackageManager.PERMISSION_GRANTED;
}
private void requestReadPermission() {
String permission;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permission = Manifest.permission.READ_MEDIA_AUDIO;
} else {
permission = Manifest.permission.READ_EXTERNAL_STORAGE;
}
requestPermissions(
new String[]{ permission },
REQUEST_READ_PERMISSION
);
}
private void loadMusic() {
List<String> musicList = new ArrayList<>();
android.net.Uri collection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = new String[] {
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.DATA
};
String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0";
long start = SystemClock.elapsedRealtime();
try (android.database.Cursor cursor = getContentResolver().query(
collection,
projection,
selection,
null,
MediaStore.Audio.Media.TITLE + " ASC"
)) {
if (cursor != null) {
int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE);
int dataColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);
while (cursor.moveToNext()) {
String title = cursor.getString(titleColumn);
String path = dataColumn != -1 ? cursor.getString(dataColumn) : null;
if (path != null) {
musicList.add(title + "\n" + path);
} else {
musicList.add(title + "\n[path unavailable]");
}
}
}
}
long elapsedMs = SystemClock.elapsedRealtime() - start;
StringBuilder header = new StringBuilder();
header.append("Found ").append(musicList.size()).append(" music files\n");
header.append("Query time: ").append(elapsedMs).append(" ms\n\n");
resultTextView.setText(header.toString() + String.join("\n\n", musicList));
// 200 music files in 32 ms
// paths like "/storage/emulated/0/Download/...mp3"
}
}

View 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);
}

View File

@@ -0,0 +1,60 @@
package at.lockstep.pb;
import android.content.Context;
import android.os.SystemClock;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import at.lockstep.R;
/**
* Resource helpers.
*
* @author David Madl (git@abanbytes.eu)
* @date 2020-05-16
*/
public class AudioResources {
/**
* Copy raw tracks to filesystem:
* otherwise, packaged resources cannot directly be accessed from C code.
*/
public static void copyRawTracksToFilesystem(Context context) throws IOException {
Field[] fields = R.raw.class.getFields();
long before = SystemClock.uptimeMillis();
try {
for (int i = 0; i < fields.length; i++) {
String assetName = fields[i].getName();
if (assetName.startsWith("track_")) {
int resourceId = fields[i].getInt(null);
copyFileUsingStream(context.getResources().openRawResource(resourceId), new File(context.getFilesDir() + "/" + resourceId + ".mp3"));
}
}
} catch(IllegalAccessException iae) {
// reflection should always work on R.raw
iae.printStackTrace();
}
long after = SystemClock.uptimeMillis();
Log.i("LoadTracks", String.format("copyRawTracksToFilesystem() took %.3f s", ((float)(after-before))/1e3f));
}
private static void copyFileUsingStream(InputStream is, File dest) throws IOException {
OutputStream os = null;
try {
os = new FileOutputStream(dest);
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
} finally {
is.close();
os.close();
}
}
}

View File

@@ -1,31 +1,91 @@
package at.lockstep.pb;
import android.content.Context;
import android.media.AudioManager;
import android.os.Build;
import android.util.Log;
import java.io.IOException;
public class PlaybackEngine {
static long mEngineHandle = 0;
static final int MPG123_OK = 0;
static boolean mFilesystemInitialized = false;
static {
System.loadLibrary("lockstep-native");
// mpg123_init() must be called before createEngine()
int ok = native_mpg123_init();
if (ok != MPG123_OK)
throw new IllegalStateException("mpg123_init() failed");
}
/**
* Initialize Oboe library and start playing silence.
* @param context Context for filesystem operations (e.g. Activity)
* @param resid Beat sound (step detection audio-feedback sample)
* @return true if successful
*/
public static boolean create(Context context, int resid) {
try {
if (!mFilesystemInitialized) {
AudioResources.copyRawTracksToFilesystem(context);
mFilesystemInitialized = true;
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
public static boolean create(Context context) {
if (mEngineHandle == 0) {
//setDefaultStreamValues(context); // TODO
setDefaultStreamValues(context);
Log.i("PlaybackEngine", "Hello PlaybackEngine");
mEngineHandle = native_createEngine();
mEngineHandle = native_createEngine(context.getFilesDir().toString(), resid);
// david:
// int fd = ParcelFileDescriptor.detachFd()
// -> C++ code (or upon failure, it is *NOT* closed)
// -> except, ParcelFileDescriptor.adoptFd(fd).close()
}
return (mEngineHandle != 0);
}
private static void setDefaultStreamValues(Context context) {
// nice-to:
// * re-visit sampling rate and channel count.
// In C++ code, MixingPlayer currently forces both to 48000 and 2 respectively,
// regardless of what Android says would be optimal.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
AudioManager myAudioMgr = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
String sampleRateStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
int defaultSampleRate = Integer.parseInt(sampleRateStr);
String framesPerBurstStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
int defaultFramesPerBurst = Integer.parseInt(framesPerBurstStr);
native_setDefaultStreamValues(defaultSampleRate, defaultFramesPerBurst);
}
}
public static void delete() {
if (mEngineHandle != 0){
if (mEngineHandle != 0) {
native_deleteEngine(mEngineHandle);
}
mEngineHandle = 0;
}
public static long getEngineHandle() {
return mEngineHandle;
}
private static native long native_createEngine();
public static void playMusic(int fd) {
if (mEngineHandle != 0) {
native_playMusic(mEngineHandle, fd);
}
}
private static native long native_createEngine(String filesDir, int resid);
private static native void native_deleteEngine(long engineHandle);
private static native void native_setDefaultStreamValues(int sampleRate, int framesPerBurst);
private static native int native_mpg123_init();
private static native void native_playMusic(long engineHandle, int fd);
}

View File

@@ -0,0 +1,138 @@
package at.lockstep.saf;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import androidx.appcompat.app.AppCompatActivity;
public class SafPickerActivity extends AppCompatActivity {
private static final String TAG = "SafExample";
private static final int REQ_OPEN_DOCUMENT = 1001;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
pickDocumentForPersistentReadAccess();
}
/**
* Method 1:
* Opens the SAF picker so the user can choose a file.
*
* You can call this from a button click, menu item, etc.
*/
public void pickDocumentForPersistentReadAccess() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Use "*/*" for any file, or narrow it, e.g. "audio/*", "application/pdf", etc.
//intent.setType("audio/*");
intent.setType("audio/mpeg");
// Optional: allow only local files
// intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
startActivityForResult(intent, REQ_OPEN_DOCUMENT);
/*
ActivityResultLauncher<Intent> startActivityIntent = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
// Add same code that you want to add in onActivityResult method
}
});*/
}
/**
* Method 2:
* Takes a previously persisted URI string and reads the file contents as bytes.
*
* Returns the full contents in memory. For large files, stream instead.
*/
public byte[] readPersistedUriBytes(@NonNull String persistedUriString) throws IOException {
Uri uri = Uri.parse(persistedUriString);
ContentResolver resolver = getContentResolver();
try (InputStream in = resolver.openInputStream(uri)) {
if (in == null) {
throw new IOException("openInputStream() returned null for URI: " + uri);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int n;
while ((n = in.read(buffer)) != -1) {
out.write(buffer, 0, n);
}
return out.toByteArray();
}
}
@SuppressLint("WrongConstant")
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQ_OPEN_DOCUMENT && resultCode == Activity.RESULT_OK && data != null) {
Uri uri = data.getData();
if (uri == null) {
Log.w(TAG, "Picker returned null URI");
return;
}
// Keep only the permission bits the system granted in this result intent.
final int takeFlags = data.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
try {
// Persist the granted access so it survives app/device restarts.
getContentResolver().takePersistableUriPermission(uri, takeFlags);
// Persist the URI string somewhere durable.
// Example only; real persistence not implemented per your request.
String uriToPersist = uri.toString();
Log.d(TAG, "Persist this URI string: " + uriToPersist);
// Example immediate read:
byte[] bytes = readPersistedUriBytes(uriToPersist);
Log.d(TAG, "Read " + bytes.length + " bytes");
Intent ct = new Intent();
ct.putExtra("content_uri", uriToPersist);
setResult(Activity.RESULT_OK, ct);
finish();
} catch (SecurityException e) {
Log.e(TAG, "Failed to persist URI permission for " + uri, e);
} catch (IOException e) {
Log.e(TAG, "Failed to read URI " + uri, e);
}
}
}
}

View File

@@ -0,0 +1,40 @@
package at.lockstep.ui;
/**
* Item DTO for song picker.
*/
public class SongItem {
private String title;
private String artist;
private String contentUri;
public SongItem(String title, String artist, String contentUri) {
this.title = title;
this.artist = artist;
this.contentUri = contentUri;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getArtist() {
return artist;
}
public void setArtist(String artist) {
this.artist = artist;
}
public String getContentUri() {
return contentUri;
}
public void setContentUri(String contentUri) {
this.contentUri = contentUri;
}
}

View File

@@ -0,0 +1,170 @@
package at.lockstep.ui;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.provider.MediaStore;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import at.lockstep.R;
/**
* Choose a song from the device library.
*/
public class SongPickerActivity extends Activity implements SongPickerAdapter.OnItemClickListener {
private List<SongItem> songs = new ArrayList<>();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_song_picker);
RecyclerView recyclerView = findViewById(R.id.recyclerView);
if (hasReadPermission()) {
loadSongList();
} else {
requestReadPermission();
}
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(new SongPickerAdapter(songs, this));
}
private static final int REQUEST_READ_PERMISSION = 1001;
@Override
public void onRequestPermissionsResult(
int requestCode,
String[] permissions,
int[] grantResults
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_READ_PERMISSION) {
boolean granted = grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (granted) {
loadSongList();
} else {
songs.add(new SongItem("Have No Songs - Permission denied", "Re-open app and try again.", null));
//resultTextView.setText("Permission denied.");
}
}
}
private boolean hasReadPermission() {
String permission;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permission = Manifest.permission.READ_MEDIA_AUDIO;
} else {
permission = Manifest.permission.READ_EXTERNAL_STORAGE;
}
return ContextCompat.checkSelfPermission(this, permission)
== PackageManager.PERMISSION_GRANTED;
}
private void requestReadPermission() {
String permission;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permission = Manifest.permission.READ_MEDIA_AUDIO;
} else {
permission = Manifest.permission.READ_EXTERNAL_STORAGE;
}
requestPermissions(
new String[]{ permission },
REQUEST_READ_PERMISSION
);
}
private void loadSongList() {
// TODO: cleanup, remove this list, etc
List<String> musicList = new ArrayList<>();
android.net.Uri collection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = new String[] {
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.DATA
};
String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0";
long start = SystemClock.elapsedRealtime();
//
// perform MediaStore query
//
try (android.database.Cursor cursor = getContentResolver().query(
collection,
projection,
selection,
null,
MediaStore.Audio.Media.TITLE + " ASC"
)) {
if (cursor != null) {
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE);
int artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST);
int dataColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);
while (cursor.moveToNext()) {
String contentUri = cursor.getString(idColumn); // the content:// Uri for the MediaStore item
String title = cursor.getString(titleColumn);
String artist = cursor.getString(artistColumn);
String path = dataColumn != -1 ? cursor.getString(dataColumn) : null;
if (path != null) {
musicList.add(title + "\n" + path);
songs.add(new SongItem(title, artist, contentUri));
} else {
musicList.add(title + "\n[path unavailable]");
}
}
}
}
long elapsedMs = SystemClock.elapsedRealtime() - start;
StringBuilder header = new StringBuilder();
header.append("Found ").append(musicList.size()).append(" music files\n");
header.append("Query time: ").append(elapsedMs).append(" ms\n\n");
//resultTextView.setText(header.toString() + String.join("\n\n", musicList));
// 200 music files in 32 ms
// paths like "/storage/emulated/0/Download/...mp3"
}
@Override
public void onItemClick(SongItem item) {
if(item.getContentUri() == null) {
// clicked the prompt for missing permissions?
// try acquiring them again.
// nice-to: test this.
requestReadPermission();
return;
}
Intent data = new Intent();
data.putExtra("content_uri", item.getContentUri());
setResult(Activity.RESULT_OK, data);
finish();
}
}

View File

@@ -0,0 +1,66 @@
package at.lockstep.ui;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import at.lockstep.R;
/**
* RecyclerView Adapter for song picker.
*/
public class SongPickerAdapter extends RecyclerView.Adapter<SongPickerAdapter.SongPickerViewHolder> {
private List<SongItem> songList;
private final OnItemClickListener listener;
public interface OnItemClickListener {
void onItemClick(SongItem item);
}
public SongPickerAdapter(List<SongItem> songList, OnItemClickListener listener) {
this.songList = songList;
this.listener = listener;
}
@NonNull
@Override
public SongPickerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.song_card, parent, false);
return new SongPickerViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SongPickerViewHolder holder, int position) {
SongItem songItem = songList.get(position);
holder.songTitle.setText(songItem.getTitle());
holder.songArtist.setText(songItem.getArtist());
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
listener.onItemClick(songItem);
}
});
}
@Override
public int getItemCount() {
return songList.size();
}
static class SongPickerViewHolder extends RecyclerView.ViewHolder {
TextView songTitle;
TextView songArtist;
public SongPickerViewHolder(@NonNull View itemView) {
super(itemView);
songTitle = itemView.findViewById(R.id.songTitle);
songArtist = itemView.findViewById(R.id.songArtist);
}
}
}

View File

@@ -0,0 +1,23 @@
#export TOOLCHAIN=/opt/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64
export TOOLCHAIN=/c/Users/david/AppData/Local/Android/Sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/windows-x86_64
export TARGET=armv7a-linux-androideabi
#export TARGET=x86_64-linux-android
export API=21
export AR=$TOOLCHAIN/bin/llvm-ar
export AS=$TOOLCHAIN/bin/llvm-as
export CC=$TOOLCHAIN/bin/$TARGET$API-clang
export CXX=$TOOLCHAIN/bin/$TARGET$API-clang++
export LD=$TOOLCHAIN/bin/ld
export RANLIB=$TOOLCHAIN/bin/llvm-ranlib
export STRIP=$TOOLCHAIN/bin/llvm-strip
export CFLAGS=-DNOXFERMEM
./configure --host $TARGET --with-audio=dummy --with-cpu=arm_fpu --prefix=$(pwd)/install
make
make install
# add NOXFERMEM ifdefs to buffer.c and xfermem.c
# see https://android.googlesource.com/platform/external/mpg123/+/refs/heads/master/src/buffer.c
#
# run 'make' with MSYS2, not Chocolatey make -> avoid 'C:/Program Files/' style SHELL

View File

@@ -0,0 +1,25 @@
#export TOOLCHAIN=/opt/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64
export TOOLCHAIN=/c/Users/david/AppData/Local/Android/Sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/windows-x86_64
#export TARGET=armv7a-linux-androideabi
#export TARGET2=arm-linux-androideabi
export TARGET=x86_64-linux-android
#export TARGET2=x86_64-linux-android
export API=21
export AR=$TOOLCHAIN/bin/llvm-ar
export AS=$TOOLCHAIN/bin/llvm-as
export CC=$TOOLCHAIN/bin/$TARGET$API-clang
export CXX=$TOOLCHAIN/bin/$TARGET$API-clang++
export LD=$TOOLCHAIN/bin/ld
export RANLIB=$TOOLCHAIN/bin/llvm-ranlib
export STRIP=$TOOLCHAIN/bin/llvm-strip
export CFLAGS=-DNOXFERMEM
#--with-cpu=arm_fpu
./configure --host $TARGET --with-audio=dummy --prefix=$(pwd)/install-files
make
make install
# add NOXFERMEM ifdefs to buffer.c and xfermem.c
# see https://android.googlesource.com/platform/external/mpg123/+/refs/heads/master/src/buffer.c
# run 'make' with MSYS2, not Chocolatey make -> avoid 'C:/Program Files/' style SHELL

View File

@@ -0,0 +1,135 @@
/*
libmpg123: MPEG Audio Decoder library
separate header just for audio format definitions not tied to
library code
copyright 1995-2015 by the mpg123 project
free software under the terms of the LGPL 2.1
see COPYING and AUTHORS files in distribution or http://mpg123.org
*/
#ifndef MPG123_ENC_H
#define MPG123_ENC_H
/** \file fmt123.h Audio format definitions. */
/** \defgroup mpg123_enc mpg123 PCM sample encodings
* These are definitions for audio formats used by libmpg123 and
* libout123.
*
* @{
*/
/** An enum over all sample types possibly known to mpg123.
* The values are designed as bit flags to allow bitmasking for encoding
* families.
* This is also why the enum is not used as type for actual encoding variables,
* plain integers (at least 16 bit, 15 bit being used) cover the possible
* combinations of these flags.
*
* Note that (your build of) libmpg123 does not necessarily support all these.
* Usually, you can expect the 8bit encodings and signed 16 bit.
* Also 32bit float will be usual beginning with mpg123-1.7.0 .
* What you should bear in mind is that (SSE, etc) optimized routines may be
* absent for some formats. We do have SSE for 16, 32 bit and float, though.
* 24 bit integer is done via postprocessing of 32 bit output -- just cutting
* the last byte, no rounding, even. If you want better, do it yourself.
*
* All formats are in native byte order. If you need different endinaness, you
* can simply postprocess the output buffers (libmpg123 wouldn't do anything
* else). The macro MPG123_SAMPLESIZE() can be helpful there.
*/
enum mpg123_enc_enum
{
/* 0000 0000 0000 1111 Some 8 bit integer encoding. */
MPG123_ENC_8 = 0x00f
/* 0000 0000 0100 0000 Some 16 bit integer encoding. */
, MPG123_ENC_16 = 0x040
/* 0100 0000 0000 0000 Some 24 bit integer encoding. */
, MPG123_ENC_24 = 0x4000
/* 0000 0001 0000 0000 Some 32 bit integer encoding. */
, MPG123_ENC_32 = 0x100
/* 0000 0000 1000 0000 Some signed integer encoding. */
, MPG123_ENC_SIGNED = 0x080
/* 0000 1110 0000 0000 Some float encoding. */
, MPG123_ENC_FLOAT = 0xe00
/* 0000 0000 1101 0000 signed 16 bit */
, MPG123_ENC_SIGNED_16 = (MPG123_ENC_16|MPG123_ENC_SIGNED|0x10)
/* 0000 0000 0110 0000 unsigned 16 bit */
, MPG123_ENC_UNSIGNED_16 = (MPG123_ENC_16|0x20)
/* 0000 0000 0000 0001 unsigned 8 bit */
, MPG123_ENC_UNSIGNED_8 = 0x01
/* 0000 0000 1000 0010 signed 8 bit */
, MPG123_ENC_SIGNED_8 = (MPG123_ENC_SIGNED|0x02)
/* 0000 0000 0000 0100 ulaw 8 bit */
, MPG123_ENC_ULAW_8 = 0x04
/* 0000 0000 0000 1000 alaw 8 bit */
, MPG123_ENC_ALAW_8 = 0x08
/* 0001 0001 1000 0000 signed 32 bit */
, MPG123_ENC_SIGNED_32 = MPG123_ENC_32|MPG123_ENC_SIGNED|0x1000
/* 0010 0001 0000 0000 unsigned 32 bit */
, MPG123_ENC_UNSIGNED_32 = MPG123_ENC_32|0x2000
/* 0101 0000 1000 0000 signed 24 bit */
, MPG123_ENC_SIGNED_24 = MPG123_ENC_24|MPG123_ENC_SIGNED|0x1000
/* 0110 0000 0000 0000 unsigned 24 bit */
, MPG123_ENC_UNSIGNED_24 = MPG123_ENC_24|0x2000
/* 0000 0010 0000 0000 32bit float */
, MPG123_ENC_FLOAT_32 = 0x200
/* 0000 0100 0000 0000 64bit float */
, MPG123_ENC_FLOAT_64 = 0x400
/* Any possibly known encoding from the list above. */
, MPG123_ENC_ANY = ( MPG123_ENC_SIGNED_16 | MPG123_ENC_UNSIGNED_16
| MPG123_ENC_UNSIGNED_8 | MPG123_ENC_SIGNED_8
| MPG123_ENC_ULAW_8 | MPG123_ENC_ALAW_8
| MPG123_ENC_SIGNED_32 | MPG123_ENC_UNSIGNED_32
| MPG123_ENC_SIGNED_24 | MPG123_ENC_UNSIGNED_24
| MPG123_ENC_FLOAT_32 | MPG123_ENC_FLOAT_64 )
};
/** Get size of one PCM sample with given encoding.
* This is included both in libmpg123 and libout123. Both offer
* an API function to provide the macro results from library
* compile-time, not that of you application. This most likely
* does not matter as I do not expect any fresh PCM sample
* encoding to appear. But who knows? Perhaps the encoding type
* will be abused for funny things in future, not even plain PCM.
* And, by the way: Thomas really likes the ?: operator.
* \param enc the encoding (mpg123_enc_enum value)
* \return size of one sample in bytes
*/
#define MPG123_SAMPLESIZE(enc) ( \
(enc) & MPG123_ENC_8 \
? 1 \
: ( (enc) & MPG123_ENC_16 \
? 2 \
: ( (enc) & MPG123_ENC_24 \
? 3 \
: ( ( (enc) & MPG123_ENC_32 \
|| (enc) == MPG123_ENC_FLOAT_32 ) \
? 4 \
: ( (enc) == MPG123_ENC_FLOAT_64 \
? 8 \
: 0 \
) ) ) ) )
/** Structure defining an audio format.
* Providing the members as individual function arguments to define a certain
* output format is easy enough. This struct makes is more comfortable to deal
* with a list of formats.
* Negative values for the members might be used to communicate use of default
* values.
*/
struct mpg123_fmt
{
long rate; /**< sampling rate in Hz */
int channels; /**< channel count */
/** encoding code, can be single value or bitwise or of members of
* mpg123_enc_enum */
int encoding;
};
/* @} */
#endif

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,135 @@
/*
libmpg123: MPEG Audio Decoder library
separate header just for audio format definitions not tied to
library code
copyright 1995-2015 by the mpg123 project
free software under the terms of the LGPL 2.1
see COPYING and AUTHORS files in distribution or http://mpg123.org
*/
#ifndef MPG123_ENC_H
#define MPG123_ENC_H
/** \file fmt123.h Audio format definitions. */
/** \defgroup mpg123_enc mpg123 PCM sample encodings
* These are definitions for audio formats used by libmpg123 and
* libout123.
*
* @{
*/
/** An enum over all sample types possibly known to mpg123.
* The values are designed as bit flags to allow bitmasking for encoding
* families.
* This is also why the enum is not used as type for actual encoding variables,
* plain integers (at least 16 bit, 15 bit being used) cover the possible
* combinations of these flags.
*
* Note that (your build of) libmpg123 does not necessarily support all these.
* Usually, you can expect the 8bit encodings and signed 16 bit.
* Also 32bit float will be usual beginning with mpg123-1.7.0 .
* What you should bear in mind is that (SSE, etc) optimized routines may be
* absent for some formats. We do have SSE for 16, 32 bit and float, though.
* 24 bit integer is done via postprocessing of 32 bit output -- just cutting
* the last byte, no rounding, even. If you want better, do it yourself.
*
* All formats are in native byte order. If you need different endinaness, you
* can simply postprocess the output buffers (libmpg123 wouldn't do anything
* else). The macro MPG123_SAMPLESIZE() can be helpful there.
*/
enum mpg123_enc_enum
{
/* 0000 0000 0000 1111 Some 8 bit integer encoding. */
MPG123_ENC_8 = 0x00f
/* 0000 0000 0100 0000 Some 16 bit integer encoding. */
, MPG123_ENC_16 = 0x040
/* 0100 0000 0000 0000 Some 24 bit integer encoding. */
, MPG123_ENC_24 = 0x4000
/* 0000 0001 0000 0000 Some 32 bit integer encoding. */
, MPG123_ENC_32 = 0x100
/* 0000 0000 1000 0000 Some signed integer encoding. */
, MPG123_ENC_SIGNED = 0x080
/* 0000 1110 0000 0000 Some float encoding. */
, MPG123_ENC_FLOAT = 0xe00
/* 0000 0000 1101 0000 signed 16 bit */
, MPG123_ENC_SIGNED_16 = (MPG123_ENC_16|MPG123_ENC_SIGNED|0x10)
/* 0000 0000 0110 0000 unsigned 16 bit */
, MPG123_ENC_UNSIGNED_16 = (MPG123_ENC_16|0x20)
/* 0000 0000 0000 0001 unsigned 8 bit */
, MPG123_ENC_UNSIGNED_8 = 0x01
/* 0000 0000 1000 0010 signed 8 bit */
, MPG123_ENC_SIGNED_8 = (MPG123_ENC_SIGNED|0x02)
/* 0000 0000 0000 0100 ulaw 8 bit */
, MPG123_ENC_ULAW_8 = 0x04
/* 0000 0000 0000 1000 alaw 8 bit */
, MPG123_ENC_ALAW_8 = 0x08
/* 0001 0001 1000 0000 signed 32 bit */
, MPG123_ENC_SIGNED_32 = MPG123_ENC_32|MPG123_ENC_SIGNED|0x1000
/* 0010 0001 0000 0000 unsigned 32 bit */
, MPG123_ENC_UNSIGNED_32 = MPG123_ENC_32|0x2000
/* 0101 0000 1000 0000 signed 24 bit */
, MPG123_ENC_SIGNED_24 = MPG123_ENC_24|MPG123_ENC_SIGNED|0x1000
/* 0110 0000 0000 0000 unsigned 24 bit */
, MPG123_ENC_UNSIGNED_24 = MPG123_ENC_24|0x2000
/* 0000 0010 0000 0000 32bit float */
, MPG123_ENC_FLOAT_32 = 0x200
/* 0000 0100 0000 0000 64bit float */
, MPG123_ENC_FLOAT_64 = 0x400
/* Any possibly known encoding from the list above. */
, MPG123_ENC_ANY = ( MPG123_ENC_SIGNED_16 | MPG123_ENC_UNSIGNED_16
| MPG123_ENC_UNSIGNED_8 | MPG123_ENC_SIGNED_8
| MPG123_ENC_ULAW_8 | MPG123_ENC_ALAW_8
| MPG123_ENC_SIGNED_32 | MPG123_ENC_UNSIGNED_32
| MPG123_ENC_SIGNED_24 | MPG123_ENC_UNSIGNED_24
| MPG123_ENC_FLOAT_32 | MPG123_ENC_FLOAT_64 )
};
/** Get size of one PCM sample with given encoding.
* This is included both in libmpg123 and libout123. Both offer
* an API function to provide the macro results from library
* compile-time, not that of you application. This most likely
* does not matter as I do not expect any fresh PCM sample
* encoding to appear. But who knows? Perhaps the encoding type
* will be abused for funny things in future, not even plain PCM.
* And, by the way: Thomas really likes the ?: operator.
* \param enc the encoding (mpg123_enc_enum value)
* \return size of one sample in bytes
*/
#define MPG123_SAMPLESIZE(enc) ( \
(enc) & MPG123_ENC_8 \
? 1 \
: ( (enc) & MPG123_ENC_16 \
? 2 \
: ( (enc) & MPG123_ENC_24 \
? 3 \
: ( ( (enc) & MPG123_ENC_32 \
|| (enc) == MPG123_ENC_FLOAT_32 ) \
? 4 \
: ( (enc) == MPG123_ENC_FLOAT_64 \
? 8 \
: 0 \
) ) ) ) )
/** Structure defining an audio format.
* Providing the members as individual function arguments to define a certain
* output format is easy enough. This struct makes is more comfortable to deal
* with a list of formats.
* Negative values for the members might be used to communicate use of default
* values.
*/
struct mpg123_fmt
{
long rate; /**< sampling rate in Hz */
int channels; /**< channel count */
/** encoding code, can be single value or bitwise or of members of
* mpg123_enc_enum */
int encoding;
};
/* @} */
#endif

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,159 @@
/*
libmpg123: MPEG Audio Decoder library
separate header just for audio format definitions not tied to
library code
copyright 1995-2020 by the mpg123 project
free software under the terms of the LGPL 2.1
see COPYING and AUTHORS files in distribution or http://mpg123.org
*/
#ifndef MPG123_ENC_H
#define MPG123_ENC_H
/** \file fmt123.h Audio format definitions. */
/** \defgroup mpg123_enc mpg123 PCM sample encodings
* These are definitions for audio formats used by libmpg123 and
* libout123.
*
* @{
*/
/** An enum over all sample types possibly known to mpg123.
* The values are designed as bit flags to allow bitmasking for encoding
* families.
* This is also why the enum is not used as type for actual encoding variables,
* plain integers (at least 16 bit, 15 bit being used) cover the possible
* combinations of these flags.
*
* Note that (your build of) libmpg123 does not necessarily support all these.
* Usually, you can expect the 8bit encodings and signed 16 bit.
* Also 32bit float will be usual beginning with mpg123-1.7.0 .
* What you should bear in mind is that (SSE, etc) optimized routines may be
* absent for some formats. We do have SSE for 16, 32 bit and float, though.
* 24 bit integer is done via postprocessing of 32 bit output -- just cutting
* the last byte, no rounding, even. If you want better, do it yourself.
*
* All formats are in native byte order. If you need different endinaness, you
* can simply postprocess the output buffers (libmpg123 wouldn't do anything
* else). The macro MPG123_SAMPLESIZE() can be helpful there.
*/
enum mpg123_enc_enum
{
/* 0000 0000 0000 1111 Some 8 bit integer encoding. */
MPG123_ENC_8 = 0x00f
/* 0000 0000 0100 0000 Some 16 bit integer encoding. */
, MPG123_ENC_16 = 0x040
/* 0100 0000 0000 0000 Some 24 bit integer encoding. */
, MPG123_ENC_24 = 0x4000
/* 0000 0001 0000 0000 Some 32 bit integer encoding. */
, MPG123_ENC_32 = 0x100
/* 0000 0000 1000 0000 Some signed integer encoding. */
, MPG123_ENC_SIGNED = 0x080
/* 0000 1110 0000 0000 Some float encoding. */
, MPG123_ENC_FLOAT = 0xe00
/* 0000 0000 1101 0000 signed 16 bit */
, MPG123_ENC_SIGNED_16 = (MPG123_ENC_16|MPG123_ENC_SIGNED|0x10)
/* 0000 0000 0110 0000 unsigned 16 bit */
, MPG123_ENC_UNSIGNED_16 = (MPG123_ENC_16|0x20)
/* 0000 0000 0000 0001 unsigned 8 bit */
, MPG123_ENC_UNSIGNED_8 = 0x01
/* 0000 0000 1000 0010 signed 8 bit */
, MPG123_ENC_SIGNED_8 = (MPG123_ENC_SIGNED|0x02)
/* 0000 0000 0000 0100 ulaw 8 bit */
, MPG123_ENC_ULAW_8 = 0x04
/* 0000 0000 0000 1000 alaw 8 bit */
, MPG123_ENC_ALAW_8 = 0x08
/* 0001 0001 1000 0000 signed 32 bit */
, MPG123_ENC_SIGNED_32 = MPG123_ENC_32|MPG123_ENC_SIGNED|0x1000
/* 0010 0001 0000 0000 unsigned 32 bit */
, MPG123_ENC_UNSIGNED_32 = MPG123_ENC_32|0x2000
/* 0101 0000 1000 0000 signed 24 bit */
, MPG123_ENC_SIGNED_24 = MPG123_ENC_24|MPG123_ENC_SIGNED|0x1000
/* 0110 0000 0000 0000 unsigned 24 bit */
, MPG123_ENC_UNSIGNED_24 = MPG123_ENC_24|0x2000
/* 0000 0010 0000 0000 32bit float */
, MPG123_ENC_FLOAT_32 = 0x200
/* 0000 0100 0000 0000 64bit float */
, MPG123_ENC_FLOAT_64 = 0x400
/* Any possibly known encoding from the list above. */
, MPG123_ENC_ANY = ( MPG123_ENC_SIGNED_16 | MPG123_ENC_UNSIGNED_16
| MPG123_ENC_UNSIGNED_8 | MPG123_ENC_SIGNED_8
| MPG123_ENC_ULAW_8 | MPG123_ENC_ALAW_8
| MPG123_ENC_SIGNED_32 | MPG123_ENC_UNSIGNED_32
| MPG123_ENC_SIGNED_24 | MPG123_ENC_UNSIGNED_24
| MPG123_ENC_FLOAT_32 | MPG123_ENC_FLOAT_64 )
};
/** Get size of one PCM sample with given encoding.
* This is included both in libmpg123 and libout123. Both offer
* an API function to provide the macro results from library
* compile-time, not that of you application. This most likely
* does not matter as I do not expect any fresh PCM sample
* encoding to appear. But who knows? Perhaps the encoding type
* will be abused for funny things in future, not even plain PCM.
* And, by the way: Thomas really likes the ?: operator.
* \param enc the encoding (mpg123_enc_enum value)
* \return size of one sample in bytes
*/
#define MPG123_SAMPLESIZE(enc) ( \
(enc) < 1 \
? 0 \
: ( (enc) & MPG123_ENC_8 \
? 1 \
: ( (enc) & MPG123_ENC_16 \
? 2 \
: ( (enc) & MPG123_ENC_24 \
? 3 \
: ( ( (enc) & MPG123_ENC_32 \
|| (enc) == MPG123_ENC_FLOAT_32 ) \
? 4 \
: ( (enc) == MPG123_ENC_FLOAT_64 \
? 8 \
: 0 \
) ) ) ) ) )
/** Representation of zero in differing encodings.
* This exists to define proper silence in various encodings without
* having to link to libsyn123 to do actual conversions at runtime.
* You have to handle big/little endian order yourself, though.
* This takes the shortcut that any signed encoding has a zero with
* all-zero bits. Unsigned linear encodings just have the highest bit set
* (2^(n-1) for n bits), while the nonlinear 8-bit ones are special.
* \param enc the encoding (mpg123_enc_enum value)
* \param siz bytes per sample (return value of MPG123_SAMPLESIZE(enc))
* \param off byte (octet) offset counted from LSB
* \return unsigned byte value for the designated octet
*/
#define MPG123_ZEROSAMPLE(enc, siz, off) ( \
(enc) == MPG123_ENC_ULAW_8 \
? (off == 0 ? 0xff : 0x00) \
: ( (enc) == MPG123_ENC_ALAW_8 \
? (off == 0 ? 0xd5 : 0x00) \
: ( (((enc) & (MPG123_ENC_SIGNED|MPG123_ENC_FLOAT)) || (siz) != ((off)+1)) \
? 0x00 \
: 0x80 \
) ) )
/** Structure defining an audio format.
* Providing the members as individual function arguments to define a certain
* output format is easy enough. This struct makes is more comfortable to deal
* with a list of formats.
* Negative values for the members might be used to communicate use of default
* values.
*/
struct mpg123_fmt
{
long rate; /**< sampling rate in Hz */
int channels; /**< channel count */
/** encoding code, can be single value or bitwise or of members of
* mpg123_enc_enum */
int encoding;
};
/** @} */
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,751 @@
/*
out123: audio output interface
copyright 1995-2016 by the mpg123 project,
free software under the terms of the LGPL 2.1
see COPYING and AUTHORS files in distribution or http://mpg123.org
initially written as audio.h by Michael Hipp, reworked into out123 API
by Thomas Orgis
*/
#ifndef _OUT123_H_
#define _OUT123_H_
/** \file out123.h The header file for the libout123 audio output facility. */
/** A macro to check at compile time which set of API functions to expect.
* This must be incremented at least each time a new symbol is added
* to the header.
*/
#define OUT123_API_VERSION 5
/** library patch level at client build time */
#define OUT123_PATCHLEVEL 2
/* We only need size_t definition. */
#include <stddef.h>
/* Common audio encoding specification, including a macro for getting
* size of encoded samples in bytes. Said macro is still hardcoded
* into out123_encsize(). Relying on this one may help an old program
* know sizes of encodings added to fmt123.h later on.
* If you don't care, just use the macro.
*/
#include "fmt123.h"
#ifndef MPG123_EXPORT
/** Defines needed for MS Visual Studio(tm) DLL builds.
* Every public function must be prefixed with MPG123_EXPORT. When building
* the DLL ensure to define BUILD_MPG123_DLL. This makes the function accessible
* for clients and includes it in the import library which is created together
* with the DLL. When consuming the DLL ensure to define LINK_MPG123_DLL which
* imports the functions from the DLL.
*/
#ifdef BUILD_MPG123_DLL
/* The dll exports. */
#define MPG123_EXPORT __declspec(dllexport)
#else
#ifdef LINK_MPG123_DLL
/* The exe imports. */
#define MPG123_EXPORT __declspec(dllimport)
#else
/* Nothing on normal/UNIX builds */
#define MPG123_EXPORT
#endif
#endif
#endif
/* Earlier versions of libout123 put enums into public API calls,
* thich is not exactly safe. There are ABI rules, but you can use
* compiler switches to change the sizes of enums. It is safer not
* to have them in API calls. Thus, the default is to remap calls and
* structs to variants that use plain ints. Define MPG123_ENUM_API to
* prevent that remapping.
*
* You might want to define this to increase the chance of your binary
* working with an older version of the library. But if that is your goal,
* you should better build with an older version to begin with.
*/
#ifndef MPG123_ENUM_API
#define out123_param out123_param2
#define out123_getparam out123_getparam2
#endif
#ifdef __cplusplus
extern "C" {
#endif
/** \defgroup out123_api out123 library API
* This is out123, a library focused on continuous playback of audio streams
* via various platform-specific output methods. It glosses over details of
* the native APIs to give an interface close to simply writing data to a
* file. There might be the option to tune details like buffer (period) sizes
* and the number of them on the device side in future, but the focus of the
* library is to ease the use case of just getting that raw audio data out
* there, without interruptions.
*
* The basic idea is to create a handle with out123_new() and open a certain
* output device (using a certain driver module, possibly build-time defaults)
* with out123_open(). Now, you can query the output device for supported
* encodings for given rate and channel count with out123_get_encodings() and
* decide what to use for actually starting playback with out123_start().
*
* Then, you just need to provide (interleaved pcm) data for playback with
* out123_play(), which will block when the device's buffers are full. You get
* your timing from that (instead of callbacks). If your program does the
* production of the audio data just a little bit faster than the playback,
* causing out123_play() to block ever so briefly, you're fine.
*
* You stop playback with out123_stop(), or just close the device and driver
* via out123_close(), or even just decide to drop it all and do out123_del()
* right away when you're done.
*
* There are other functions for specific needs, but the basic idea should be
* covered by the above.
*
* Note that the driver modules that bind to the operating system API for
* output might impose restrictions on what you can safely do regarding your
* out123_handle and multiple threads or processes. You should be on the safe
* side ensuring that you confine usage of a handle to a single thread instead
* of passing it around.
@{
*/
/** Opaque structure for the libout123 handle. */
struct out123_struct;
/** Typedef shortcut as preferrend name for the handle type. */
typedef struct out123_struct out123_handle;
/** Get version of the mpg123 distribution this library build came with.
* (optional means non-NULL)
* \param major optional address to store major version number
* \param minor optional address to store minor version number
* \param patch optional address to store patchlevel version number
* \return full version string (like "1.2.3-beta4 (experimental)")
*/
MPG123_EXPORT
const char *out123_distversion(unsigned int *major, unsigned int *minor, unsigned int *patch);
/** Get API version of library build.
* \param patch optional address to store patchlevel
* \return API version of library
*/
MPG123_EXPORT
unsigned int out123_libversion(unsigned int *patch);
/** Enumeration of codes for the parameters that it is possible to set/get. */
enum out123_parms
{
OUT123_FLAGS = 1 /**< integer, various flags, see enum #out123_flags */
, OUT123_PRELOAD /**< float, fraction of buffer to fill before playback */
, OUT123_GAIN /**< integer, output device gain (module-specific) */
, OUT123_VERBOSE /**< integer, verbosity to stderr, >= 0 */
, OUT123_DEVICEBUFFER /**<
* float, length of device buffer in seconds;
* This might be ignored, might have only a loose relation to actual
* buffer sizes and latency, depending on output driver. Try to tune
* this before opening a device if you want to influcence latency or reduce
* dropouts. Value <= 0 uses some default, usually favouring stable playback
* over low latency. Values above 0.5 are probably too much.
*/
, OUT123_PROPFLAGS /**< integer, query driver/device property flags (r/o) */
, OUT123_NAME /**< string, name of this instance (NULL restores default);
* The value returned by out123_getparam() might be different if the audio
* backend changed it (to be unique among clients, p.ex.).
* TODO: The name provided here is used as prefix in diagnostic messages. */
, OUT123_BINDIR /**< string, path to a program binary directory to use
* as starting point in the search for the output module directory
* (e.g. ../lib/mpg123 or ./plugins). The environment variable MPG123_MODDIR
* is always tried first and the in-built installation path last.
*/
, OUT123_ADD_FLAGS /**< enable given flags */
, OUT123_REMOVE_FLAGS /**< disable diven flags */
};
/** Flags to tune out123 behaviour */
enum out123_flags
{
OUT123_HEADPHONES = 0x01 /**< output to headphones (if supported) */
, OUT123_INTERNAL_SPEAKER = 0x02 /**< output to speaker (if supported) */
, OUT123_LINE_OUT = 0x04 /**< output to line out (if supported) */
, OUT123_QUIET = 0x08 /**< no printouts to standard error */
, OUT123_KEEP_PLAYING = 0x10 /**<
* When this is set (default), playback continues in a loop when the device
* does not consume all given data at once. This happens when encountering
* signals (like SIGSTOP, SIGCONT) that cause interruption of the underlying
* functions.
* Note that this flag is meaningless when the optional buffer is employed,
* There, your program will always block until the buffer completely took
* over the data given to it via out123_play(), unless a communication error
* arises.
*/
, OUT123_MUTE = 0x20 /**< software mute (play silent audio) */
};
/** Read-only output driver/device property flags (OUT123_PROPFLAGS). */
enum out123_propflags
{
OUT123_PROP_LIVE = 0x01 /**< This is a live output, meaning that
* special care might be needed for pauses in playback (p.ex. stream
* of silence instead of interruption), as opposed to files on disk.
*/
, OUT123_PROP_PERSISTENT = 0x02 /**< This (live) output does not need
* special care for pauses (continues with silence itself),
* out123_pause() does nothing to the device.
*/
};
/** Create a new output handle.
* This only allocates and initializes memory, so the only possible
* error condition is running out of memory.
* \return pointer to new handle or NULL on error
*/
MPG123_EXPORT
out123_handle *out123_new(void);
/** Delete output handle.
* This implies out123_close().
*/
MPG123_EXPORT
void out123_del(out123_handle *ao);
/** Free plain memory allocated within libout123.
* This is for library users that are not sure to use the same underlying
* memory allocator as libout123. It is just a wrapper over free() in
* the underlying C library.
*/
MPG123_EXPORT void out123_free(void *ptr);
/** Error code enumeration
* API calls return a useful (positve) value or zero (OUT123_OK) on simple
* success. A negative value (-1 == OUT123_ERR) usually indicates that some
* error occured. Which one, that can be queried using out123_errcode()
* and friends.
*/
enum out123_error
{
OUT123_ERR = -1 /**< generic alias for verbosity, always == -1 */
, OUT123_OK = 0 /**< just a name for zero, not going to change */
, OUT123_DOOM /**< dazzled, out of memory */
, OUT123_BAD_DRIVER_NAME /**< bad driver name given */
, OUT123_BAD_DRIVER /**< unspecified issue loading a driver */
, OUT123_NO_DRIVER /**< no driver loaded */
, OUT123_NOT_LIVE /**< no active audio device */
, OUT123_DEV_PLAY /**< some device playback error */
, OUT123_DEV_OPEN /**< error opening device */
, OUT123_BUFFER_ERROR /**<
* Some (really unexpected) error in buffer infrastructure.
*/
, OUT123_MODULE_ERROR /**< basic failure in module loading */
, OUT123_ARG_ERROR /**< some bad function arguments supplied */
, OUT123_BAD_PARAM /**< unknown parameter code */
, OUT123_SET_RO_PARAM /**< attempt to set read-only parameter */
, OUT123_BAD_HANDLE /**< bad handle pointer (NULL, usually) */
, OUT123_NOT_SUPPORTED /**< some requested operation is not supported (right now) */
, OUT123_DEV_ENUMERATE /**< device enumeration itself failed */
, OUT123_ERRCOUNT /**< placeholder for shaping arrays */
};
/** Get string representation of last encountered error in the
* context of given handle.
* \param ao handle
* \return error string
*/
MPG123_EXPORT
const char* out123_strerror(out123_handle *ao);
/** Get the plain errcode intead of a string.
* Note that this used to return OUT123_ERR instead of
* OUT123_BAD_HANDLE in case of ao==NULL before mpg123-1.23.5 .
* \param ao handle
* \return error code recorded in handle or OUT123_BAD_HANDLE
*/
MPG123_EXPORT
int out123_errcode(out123_handle *ao);
/** Return the error string for a given error code.
* \param errcode the integer error code
* \return error string
*/
MPG123_EXPORT
const char* out123_plain_strerror(int errcode);
/** Set a desired output buffer size.
* This starts a separate process that handles the audio output, decoupling
* the latter from the main process with a memory buffer and saving you the
* burden to ensure sparing CPU cycles for actual playback.
* This is for applicatons that prefer continuous playback over small latency.
* In other words: The kind of applications that out123 is designed for.
* This routine always kills off any currently active audio output module /
* device, even if you just disable the buffer when there is no buffer.
*
* Keep this in mind for memory-constrainted systems: Activating the
* buffer causes a fork of the calling process, doubling the virtual memory
* use. Depending on your operating system kernel's behaviour regarding
* memory overcommit, it might be wise to call out123_set_buffer() very
* early in your program before allocating lots of memory.
*
* There _might_ be a change to threads in future, but for now this is
* classic fork with shared memory, working without any threading library.
* If your platform or build does not support that, you will always get an
* error on trying to set up a non-zero buffer (but the API call will be
* present).
*
* Also, if you do intend to use this from a multithreaded program, think
* twice and make sure that your setup is happy with forking full-blown
* processes off threaded programs. Probably you are better off spawning a
* buffer thread yourself.
*
* \param ao handle
* \param buffer_bytes size (bytes) of a memory buffer for decoded audio,
* a value of zero disables the buffer.
* \return 0 on success, OUT123_ERR on error
*/
MPG123_EXPORT
int out123_set_buffer(out123_handle *ao, size_t buffer_bytes);
#ifdef MPG123_ENUM_API
/** Set a parameter on a out123_handle.
*
* Note that this name is mapped to out123_param2() instead unless
* MPG123_ENUM_API is defined.
*
* The parameters usually only change what happens on next out123_open, not
* incfluencing running operation. There are macros To ease the API a bit:
* You can call out123_param_int(ao, code, value) for integer (long) values,
* same with out123_param_float() and out123_param_string().
*
* \param ao handle
* \param code parameter code
* \param value input value for integer parameters
* \param fvalue input value for floating point parameters
* \param svalue input value for string parameters (contens are copied)
* \return 0 on success, OUT123_ERR on error.
*/
MPG123_EXPORT
int out123_param( out123_handle *ao, enum out123_parms code
, long value, double fvalue, const char *svalue );
#endif
/** Set a parameter on a out123_handle. No enum.
*
* This is actually called instead of out123_param()
* unless MPG123_ENUM_API is defined.
*
* The parameters usually only change what happens on next out123_open, not
* incfluencing running operation. There are macros To ease the API a bit:
* You can call out123_param_int(ao, code, value) for integer (long) values,
* same with out123_param_float() and out123_param_string().
*
* \param ao handle
* \param code parameter code (from enum #out123_parms)
* \param value input value for integer parameters
* \param fvalue input value for floating point parameters
* \param svalue input value for string parameters (contens are copied)
* \return 0 on success, OUT123_ERR on error.
*/
MPG123_EXPORT
int out123_param2( out123_handle *ao, int code
, long value, double fvalue, const char *svalue );
/** Shortcut for out123_param() to set an integer parameter. */
#define out123_param_int(ao, code, value) \
out123_param((ao), (code), (value), 0., NULL)
/** Shortcut for out123_param() to set a float parameter. */
#define out123_param_float(ao, code, value) \
out123_param((ao), (code), 0, (value), NULL)
/** Shortcut for out123_param() to set an string parameter. */
#define out123_param_string(ao, code, value) \
out123_param((ao), (code), 0, 0., (value))
#ifdef MPG123_ENUM_API
/** Get a parameter from an out123_handle.
*
* Note that this name is mapped to out123_param2() instead unless
* MPG123_ENUM_API is defined.
*
* \param ao handle
* \param code parameter code
* \param ret_value output address for integer parameters
* \param ret_fvalue output address for floating point parameters
* \param ret_svalue output address for string parameters (pointer to
* internal memory, so no messing around, please)
* \return 0 on success, OUT123_ERR on error (bad parameter name or bad handle).
*/
MPG123_EXPORT
int out123_getparam( out123_handle *ao, enum out123_parms code
, long *ret_value, double *ret_fvalue, char* *ret_svalue );
#endif
/** Get a parameter from an out123_handle. No enum.
*
* This is actually called instead of out123_getparam()
* unless MPG123_ENUM_API is defined.
*
* \param ao handle
* \param code parameter code (from enum #out123_parms)
* \param ret_value output address for integer parameters
* \param ret_fvalue output address for floating point parameters
* \param ret_svalue output address for string parameters (pointer to
* internal memory, so no messing around, please)
* \return 0 on success, OUT123_ERR on error (bad parameter name or bad handle).
*/
MPG123_EXPORT
int out123_getparam2( out123_handle *ao, int code
, long *ret_value, double *ret_fvalue, char* *ret_svalue );
/** Shortcut for out123_getparam() to get an integer parameter. */
#define out123_getparam_int(ao, code, value) \
out123_getparam((ao), (code), (value), NULL, NULL)
/** Shortcut for out123_getparam() to get a float parameter. */
#define out123_getparam_float(ao, code, value) \
out123_getparam((ao), (code), NULL, (value), NULL)
/** Shortcut for out123_getparam() to get a string parameter. */
#define out123_getparam_string(ao, code, value) \
out123_getparam((ao), (code), NULL, NULL, (value))
/** Copy parameters from another out123_handle.
* \param ao handle
* \param from_ao the handle to copy parameters from
* \return 0 in success, -1 on error
*/
MPG123_EXPORT
int out123_param_from(out123_handle *ao, out123_handle* from_ao);
/** Get list of driver modules reachable in system in C argv-style format.
*
* The client is responsible for freeing the memory of both the individual
* strings and the lists themselves. There is out123_stringlists_free()
* to assist.
*
* A module that is not loadable because of missing libraries is simply
* skipped. You will get stderr messages about that unless OUT123_QUIET was
* was set, though. Failure to open the module directory is a serious error,
* resulting in negative return value.
*
* \param ao handle
* \param names address for storing list of names
* \param descr address for storing list of descriptions
* \return number of drivers found, -1 on error
*/
MPG123_EXPORT
int out123_drivers(out123_handle *ao, char ***names, char ***descr);
/** Get a list of available output devices for a given driver.
*
* If the driver supports enumeration, you can get a listing of possible
* output devices. If this list is exhaustive, depends on the driver.
* Note that this implies out123_close(). When you have a device already
* open, you don't need to look for one anymore. If you really do, just
* create another handle.
*
* Your provided pointers are only used for non-negative return values.
* In this case, you are responsible for freeing the associated memory of
* the strings and the lists themselves. The format of the lists is an
* array of char pointers, with the returned count just like the usual
* C argv and argc. There is out123_stringlists_free() to assist.
*
* Note: Calling this on a handle with a configured buffer process will
* yield #OUT123_NOT_SUPPORTED.
*
* \param ao handle
* \param driver driver name or comma-separated list of names
* to try, just like for out123_open(), possibly NULL for some default
* \param names address for storing list of names
* \param descr address for storing list of descriptions
* \param active_driver address for storing a copy of the actually active
* driver name (in case you gave a list or NULL as driver), can be NULL
* if not interesting
* \return count of devices or #OUT123_ERR if some error was encountered,
* possibly just #OUT123_NOT_SUPPORTED if the driver lacks enumeration support
*/
MPG123_EXPORT
int out123_devices( out123_handle *ao, const char *driver
, char ***names, char ***descr, char **active_driver );
/** Helper to free string list memory.
*
* This aids in freeing the memory allocated by out123_devices() and
* out123_drivers().
*
* Any of the given lists can be NULL and nothing will happen to it.
*
* \param name first string list
* \param descr second string list
* \param count count of strings
*/
MPG123_EXPORT
void out123_stringlists_free(char **name, char **descr, int count);
/** Open an output device with a certain driver
* Note: Opening means that the driver code is loaded and the desired
* device name recorded, possibly tested for availability or tentatively
* opened. After out123_open(), you can ask for supported encodings
* and then really open the device for playback with out123_start().
* \param ao handle
* \param driver (comma-separated list of) output driver name(s to try),
* NULL for default
* \param device device name to open, NULL for default
* (stdout for file-based drivers)
* \return 0 on success, -1 on error.
*/
MPG123_EXPORT
int out123_open(out123_handle *ao, const char* driver, const char* device);
/** Give info about currently loaded driver and device
* Any of the return addresses can be NULL if you are not interested in
* everything. You get pointers to internal storage. They are valid
* as long as the driver/device combination is opened.
* The device may be NULL indicating some unnamed default.
* TODO: Make the driver modules return names for such defaults.
* \param ao handle
* \param driver return address for driver name
* \param device return address for device name
* \return 0 on success, -1 on error (i.e. no driver loaded)
*/
MPG123_EXPORT
int out123_driver_info(out123_handle *ao, char **driver, char **device);
/** Close the current output device and driver.
* This implies out123_drain() to ensure no data is lost.
* With a buffer, that might cause considerable delay during
* which your main application is blocked waiting.
* Call out123_drop() beforehand if you want to end things
* quickly.
* \param ao handle
*/
MPG123_EXPORT
void out123_close(out123_handle *ao);
/** Get supported audio encodings for given rate and channel count,
* for the currently openend audio device.
* Usually, a wider range of rates is supported, but the number
* of sample encodings is limited, as is the number of channels.
* So you can call this with some standard rate and hope that the
* returned encodings work also for others, with the tested channel
* count.
* The return value of -1 on some encountered error conveniently also
* does not match any defined format (only 15 bits used for encodings,
* so this would even work with 16 bit integers).
* This implies out123_stop() to enter query mode.
* \param ao handle
* \param rate sampling rate
* \param channels number of channels
* \return supported encodings combined with bitwise or, to be checked
* against your favourite bitmask, -1 on error
*/
MPG123_EXPORT
int out123_encodings(out123_handle *ao, long rate, int channels);
/** Return the size (in bytes) of one mono sample of the named encoding.
* \param encoding The encoding value to analyze.
* \return positive size of encoding in bytes, 0 on invalid encoding. */
MPG123_EXPORT int out123_encsize(int encoding);
/** Get list of supported formats for currently opened audio device.
* Given a list of sampling rates and minimal/maximal channel count,
* this quickly checks what formats are supported with these
* constraints. The first entry is always reserved for a default
* format for the output device. If there is no such default,
* all values of the format are -1.
* For each requested combination of rate and channels, a format entry is
* created, possible with encoding value 0 to indicate that this combination
* has been tested and rejected. So, when there is no basic error, the
* number of returned format entries should be
* (ratecount*(maxchannels-minchannels+1)+1)
* . But instead of forcing you to guess, this will be allocated by
* successful run.
* For the first entry, the encoding member is supposed to be a definite
* encoding, for the others it is a bitwise combination of all possible
* encodings.
* This function is more efficient than many calls to out123_encodings().
* \param ao handle
* \param rates pointer to an array of sampling rates, may be NULL for none
* \param ratecount number of provided sampling rates
* \param minchannels minimal channel count
* \param maxchannels maximal channel count
* \param fmtlist return address for array of supported formats
* the encoding field of each entry is a combination of all
* supported encodings at this rate and channel count;
* Memory shall be freed by user.
* \return number of returned format enries, -1 on error
*/
MPG123_EXPORT
int out123_formats( out123_handle *ao, const long *rates, int ratecount
, int minchannels, int maxchannels
, struct mpg123_fmt **fmtlist );
/** Get list of encodings known to the library.
* You are responsible for freeing the allocated array.
* \param enclist return address for allocated array of encoding codes
* \return number of encodings, -1 on error
*/
MPG123_EXPORT
int out123_enc_list(int **enclist);
/** Find encoding code by name.
* \param name short or long name to find encoding code for
* \return encoding if found (enum #mpg123_enc_enum), else 0
*/
MPG123_EXPORT
int out123_enc_byname(const char *name);
/** Get name of encoding.
* \param encoding code (enum #mpg123_enc_enum)
* \return short name for valid encodings, NULL otherwise
*/
MPG123_EXPORT
const char* out123_enc_name(int encoding);
/** Get long name of encoding.
* \param encoding code (enum #mpg123_enc_enum)
* \return long name for valid encodings, NULL otherwise
*/
MPG123_EXPORT
const char* out123_enc_longname(int encoding);
/** Start playback with a certain output format
* It might be a good idea to have audio data handy to feed after this
* returns with success.
* Rationale for not taking a pointer to struct mpg123_fmt: This would
* always force you to deal with that type and needlessly enlarge the
* shortest possible program.
* \param ao handle
* \param encoding sample encoding (values matching libmpg123 API)
* \param channels number of channels (1 or 2, usually)
* \param rate sampling rate
* \return 0 on success, negative on error (bad format, usually)
*/
MPG123_EXPORT
int out123_start( out123_handle *ao
, long rate, int channels, int encoding );
/** Pause playback
* Interrupt playback, holding any data in the optional buffer.
*
* This closes the audio device if it is a live sink, ready to be re-opened
* by out123_continue() or out123_play() with the existing parameters.
* \param ao handle
*/
MPG123_EXPORT
void out123_pause(out123_handle *ao);
/** Continue playback
* The counterpart to out123_pause(). Announce to the driver that playback
* shall continue.
*
* Playback might not resume immediately if the optional buffer is configured
* to wait for a minimum fill and close to being empty. You can force playback
* of the last scrap with out123_drain(), or just by feeding more data with
* out123_play(), which will trigger out123_continue() for you, too.
* \param ao handle
*/
MPG123_EXPORT
void out123_continue(out123_handle *ao);
/** Stop playback.
* This waits for pending audio data to drain to the speakers.
* You might want to call out123_drop() before stopping if you want
* to end things right away.
* \param ao handle
*/
MPG123_EXPORT
void out123_stop(out123_handle *ao);
/** Hand over data for playback and wait in case audio device is busy.
* This survives non-fatal signals like SIGSTOP/SIGCONT and keeps on
* playing until the buffer is done with if the flag
* OUT123_KEEP_PLAYING ist set (default). So, per default, if
* you provided a byte count divisible by the PCM frame size, it is an
* error when less bytes than given are played.
* To be sure if an error occured, check out123_errcode().
* Also note that it is no accident that the buffer parameter is not marked
* as constant. Some output drivers might need to do things like swap
* byte order. This is done in-place instead of wasting memory on yet
* another copy. Software muting also overwrites the data.
* \param ao handle
* \param buffer pointer to raw audio data to be played
* \param bytes number of bytes to read from the buffer
* \return number of bytes played (might be less than given, even zero)
*/
MPG123_EXPORT
size_t out123_play( out123_handle *ao
, void *buffer, size_t bytes );
/** Drop any buffered data, making next provided data play right away.
* This does not imply an actual pause in playback.
* You are expected to play something, unless you called out123_pause().
* Feel free to call out123_stop() afterwards instead for a quicker
* exit than the implied out123_drain().
* For live sinks, this may include dropping data from their buffers.
* For others (files), this only concerns data in the optional buffer.
* \param ao handle
*/
MPG123_EXPORT
void out123_drop(out123_handle *ao);
/** Drain the output, waiting until all data went to the hardware.
* This does imply out123_continue() before and out123_pause()
* after draining.
* This might involve only the optional buffer process, or the
* buffers on the audio driver side, too.
* \param ao handle
*/
MPG123_EXPORT
void out123_drain(out123_handle *ao);
/** Drain the output, but only partially up to the given number of
* bytes. This gives you the opportunity to do something while
* the optional buffer is writing remaining data instead of having
* one atomic API call for it all.
*
* It is wholly expected that the return value of out123_buffered()
* before and after calling this has a bigger difference than the
* provided limit, as the buffer is writing all the time in the
* background.
*
* This is just a plain out123_drain() if the optional buffer is not
* in use. Also triggers out123_continue(), but only out123_pause()
* if there is no buffered data anymore.
* \param ao handle
* \param bytes limit of buffered bytes to drain
* \return number of bytes drained from buffer
*/
MPG123_EXPORT
void out123_ndrain(out123_handle *ao, size_t bytes);
/** Get an indication of how many bytes reside in the optional buffer.
* This might get extended to tell the number of bytes queued up in the
* audio backend, too.
* \param ao handle
* \return number of bytes in out123 library buffer
*/
MPG123_EXPORT
size_t out123_buffered(out123_handle *ao);
/** Extract currently used audio format from handle.
* matching mpg123_getformat().
* Given return addresses may be NULL to indicate no interest.
* \param ao handle
* \param rate address for sample rate
* \param channels address for channel count
* \param encoding address for encoding
* \param framesize size of a full PCM frame (for convenience)
* \return 0 on success, -1 on error
*/
MPG123_EXPORT
int out123_getformat( out123_handle *ao
, long *rate, int *channels, int *encoding, int *framesize );
/** @} */
#ifdef __cplusplus
}
#endif
#endif

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,35 @@
<?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" />
<Button
android:id="@+id/btnMediaStoreBenchmark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Benchmark media store" />
<Button
android:id="@+id/btnPickSong"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Pick song" />
</LinearLayout>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/resultTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="Loading..."
android:textSize="14sp" />
</ScrollView>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/background_light"
tools:context=".ui.SongPickerActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Song Picker"
android:textAlignment="center"
android:background="@android:color/darker_gray"
android:paddingTop="54sp"
android:paddingBottom="20sp"
android:textSize="24sp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:overScrollMode="always"/>
</LinearLayout>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
>
<!-- android:layout_height="96dp" -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/songTitle"
android:textSize="16sp"
android:text="Song Title"
android:textColor="@color/black"
android:maxLines="1"/>
<TextView
android:id="@+id/songArtist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/songTitle"
android:text="Artist name"
android:textSize="16sp"/>
</RelativeLayout>

Binary file not shown.

View File

@@ -1,3 +1,4 @@
<resources>
<string name="app_name">Lockstep</string>
<string name="notification_text">Reading your steps …</string>
</resources>

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Lockstep" parent="android:Theme.Material.Light.NoActionBar" />
<style name="Theme.Lockstep" parent="Theme.AppCompat.DayNight.NoActionBar" />
</resources>

View File

@@ -0,0 +1,46 @@
package at.lockstep;
import android.hardware.SensorEvent;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import com.google.gson.Gson;
public class GsonUnitTest {
/** single sensor sample */
static class SensorData {
private long timestamp;
private float[] values;
public SensorData(SensorEvent event) {
timestamp = event.timestamp;
values = Arrays.copyOf(event.values, event.values.length);
}
public SensorData(long timestamp, float[] values) {
this.timestamp = timestamp;
this.values = values;
}
}
/** array of sensor samples */
public static class SensorDataArray {
private ArrayList<SensorData> data = new ArrayList<SensorData>();
public void add(long timestamp, float[] values) { data.add(new SensorData(timestamp, values)); }
public void add(SensorEvent event) { data.add(new SensorData(event)); }
public void clear() { data.clear(); }
}
@Test
public void testGson() {
SensorDataArray recording = new SensorDataArray();
recording.add(0, new float[]{1, 2, 3});
recording.add(1, new float[]{10, 20, 30});
Gson gson = new Gson();
String json = gson.toJson(recording);
System.out.println(json);
// {"data":[{"timestamp":0,"values":[1.0,2.0,3.0]},{"timestamp":1,"values":[10.0,20.0,30.0]}]}
}
}

View File

@@ -11,6 +11,9 @@ composeBom = "2024.04.01"
logbackAndroid = "2.0.0"
oboe = "1.10.0"
slf4jApi = "1.7.30"
recyclerview = "1.3.1"
appcompat = "1.7.1"
gson = "2.11.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -30,6 +33,9 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"
logback-android = { module = "com.github.tony19:logback-android", version.ref = "logbackAndroid" }
oboe = { module = "com.google.oboe:oboe", version.ref = "oboe" }
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4jApi" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
gson = { group = "com.google.code.gson", name="gson", version.ref = "gson" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }