Compare commits
35 Commits
fe0b740bff
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b1b92ba45 | |||
| 6c5ac60ccb | |||
| 1a3991db45 | |||
| 3fe10a914a | |||
| 45b1003c9a | |||
| e8afb10f48 | |||
| cc1a2b5b7a | |||
| 7fb3029e8b | |||
| b6df86f49a | |||
| 708249a5ba | |||
| 3cf9607549 | |||
| 25e5cc19d0 | |||
| 6382c57ccc | |||
| b0f6d6d5c9 | |||
| b14ea02694 | |||
| 5b26203533 | |||
| 198dfc1630 | |||
| 02ebb17dc6 | |||
| a8234005df | |||
| a2fdf05cd5 | |||
| 79f37ca46d | |||
| 5045d6615e | |||
| 995c537fd4 | |||
| 3d55765a26 | |||
| 754b96a319 | |||
| 65ea8ba27c | |||
| 754ad3700f | |||
| 63ef5ae503 | |||
| 66a2b86ffc | |||
| d1b57aae82 | |||
| e22478445d | |||
| 804f83340f | |||
| bc8002fd59 | |||
| d2c9a7b2ff | |||
| ef5a0e678c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@
|
|||||||
app/src/main/obj/
|
app/src/main/obj/
|
||||||
.cxx
|
.cxx
|
||||||
/txts
|
/txts
|
||||||
|
/data
|
||||||
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal 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
62
TODO.md
Normal 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
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ plugins {
|
|||||||
android {
|
android {
|
||||||
namespace 'at.lockstep'
|
namespace 'at.lockstep'
|
||||||
compileSdk 34
|
compileSdk 34
|
||||||
|
ndkVersion '29.0.14206865'
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "at.lockstep"
|
applicationId "at.lockstep"
|
||||||
@@ -21,8 +22,19 @@ android {
|
|||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
//path 'src/main/cpp/CMakeLists.txt'
|
//path 'src/main/cpp/CMakeLists.txt'
|
||||||
cppFlags ''
|
//cppFlags ''
|
||||||
arguments "-DANDROID_STL=c++_shared"
|
arguments "-DANDROID_STL=c++_shared"
|
||||||
|
|
||||||
|
//cppFlags "-std=c++14"
|
||||||
|
//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.oboe
|
||||||
implementation libs.slf4j.api
|
implementation libs.slf4j.api
|
||||||
implementation libs.logback.android
|
implementation libs.logback.android
|
||||||
|
implementation libs.gson
|
||||||
|
|
||||||
implementation libs.androidx.core.ktx
|
implementation libs.androidx.core.ktx
|
||||||
implementation libs.androidx.lifecycle.runtime.ktx
|
implementation libs.androidx.lifecycle.runtime.ktx
|
||||||
@@ -73,6 +86,8 @@ dependencies {
|
|||||||
implementation libs.androidx.ui.graphics
|
implementation libs.androidx.ui.graphics
|
||||||
implementation libs.androidx.ui.tooling.preview
|
implementation libs.androidx.ui.tooling.preview
|
||||||
implementation libs.androidx.material3
|
implementation libs.androidx.material3
|
||||||
|
implementation libs.androidx.recyclerview
|
||||||
|
implementation libs.androidx.appcompat
|
||||||
testImplementation libs.junit
|
testImplementation libs.junit
|
||||||
androidTestImplementation libs.androidx.junit
|
androidTestImplementation libs.androidx.junit
|
||||||
androidTestImplementation libs.androidx.espresso.core
|
androidTestImplementation libs.androidx.espresso.core
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
|
<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
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -19,10 +27,26 @@
|
|||||||
android:theme="@style/Theme.Lockstep">
|
android:theme="@style/Theme.Lockstep">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
25
app/src/main/cpp/AudioCallback.h
Normal file
25
app/src/main/cpp/AudioCallback.h
Normal 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
|
||||||
@@ -12,6 +12,9 @@ cmake_minimum_required(VERSION 3.22.1)
|
|||||||
# build script scope).
|
# build script scope).
|
||||||
project("lockstep-native")
|
project("lockstep-native")
|
||||||
|
|
||||||
|
add_subdirectory(libpasada/pasada-lib)
|
||||||
|
add_subdirectory(librubberband)
|
||||||
|
|
||||||
# Creates and names a library, sets it as either STATIC
|
# Creates and names a library, sets it as either STATIC
|
||||||
# or SHARED, and provides the relative paths to its source code.
|
# or SHARED, and provides the relative paths to its source code.
|
||||||
# You can define multiple libraries, and CMake builds them for you.
|
# You can define multiple libraries, and CMake builds them for you.
|
||||||
@@ -27,22 +30,43 @@ project("lockstep-native")
|
|||||||
# used in the AndroidManifest.xml file.
|
# used in the AndroidManifest.xml file.
|
||||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||||
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
||||||
lockstep.cpp
|
|
||||||
PlaybackEngine.cpp
|
PlaybackEngine.cpp
|
||||||
|
mp3file.cpp
|
||||||
|
jni_mpg123.cpp
|
||||||
|
jni_lockstep.cpp
|
||||||
|
jni_stepdetector.cpp
|
||||||
|
StepDetector.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package (oboe REQUIRED CONFIG)
|
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)
|
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
|
# Specifies libraries CMake should link to your target library. You
|
||||||
# can link libraries from various origins, such as libraries defined in this
|
# can link libraries from various origins, such as libraries defined in this
|
||||||
# build script, prebuilt third-party libraries, or Android system libraries.
|
# build script, prebuilt third-party libraries, or Android system libraries.
|
||||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||||
# List libraries link to the target library
|
# List libraries link to the target library
|
||||||
|
pasada
|
||||||
|
rubberband
|
||||||
oboe::oboe
|
oboe::oboe
|
||||||
|
mpg123
|
||||||
android
|
android
|
||||||
log
|
log
|
||||||
ndk-logger
|
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>")
|
||||||
|
|||||||
142
app/src/main/cpp/MixingPlayer.h
Normal file
142
app/src/main/cpp/MixingPlayer.h
Normal 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
|
||||||
@@ -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
|
|
||||||
@@ -6,19 +6,455 @@
|
|||||||
|
|
||||||
#include "PlaybackEngine.h"
|
#include "PlaybackEngine.h"
|
||||||
#include "logging.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("PlaybackEngine()");
|
||||||
LOGI("NDK LOG_LEVEL=%d", LOG_LEVEL);
|
LOGI("NDK LOG_LEVEL=%d", LOG_LEVEL);
|
||||||
// NDK LOG_LEVEL=3 (DEBUG)
|
// NDK LOG_LEVEL=3 (DEBUG)
|
||||||
mPlayer = new OboeSinePlayer();
|
|
||||||
int32_t res = mPlayer->startAudio();
|
// load "bump" sound effect
|
||||||
LOGI("startAudio() = %d", res);
|
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() {
|
PlaybackEngine::~PlaybackEngine() {
|
||||||
LOGI("~PlaybackEngine()");
|
LOGI("~PlaybackEngine()");
|
||||||
|
closeRubberBand();
|
||||||
mPlayer->stopAudio();
|
mPlayer->stopAudio();
|
||||||
delete mPlayer;
|
delete mPlayer;
|
||||||
mPlayer = nullptr;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,14 +5,67 @@
|
|||||||
#ifndef LOCKSTEP_PLAYBACKENGINE_H
|
#ifndef LOCKSTEP_PLAYBACKENGINE_H
|
||||||
#define 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:
|
public:
|
||||||
PlaybackEngine();
|
explicit MusicProvider(RubberBand::RubberBandStretcher *stretcher, size_t buf_size_samples, int num_ch_out, std::atomic<int> *back_pressure);
|
||||||
virtual ~PlaybackEngine();
|
~MusicProvider() override;
|
||||||
|
|
||||||
|
/** Called from separate oboe thread. */
|
||||||
|
void onAudioReady(float *data, int32_t frames) override;
|
||||||
private:
|
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
|
#endif //LOCKSTEP_PLAYBACKENGINE_H
|
||||||
|
|||||||
49
app/src/main/cpp/StepDetector.cpp
Normal file
49
app/src/main/cpp/StepDetector.cpp
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/src/main/cpp/StepDetector.h
Normal file
27
app/src/main/cpp/StepDetector.h
Normal 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
|
||||||
14
app/src/main/cpp/StepListener.h
Normal file
14
app/src/main/cpp/StepListener.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// Created by david on 03.03.2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef LOCKSTEP_STEPLISTENER_H
|
||||||
|
#define LOCKSTEP_STEPLISTENER_H
|
||||||
|
|
||||||
|
class StepListener {
|
||||||
|
public:
|
||||||
|
virtual ~StepListener() {}
|
||||||
|
virtual void playBeat() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif //LOCKSTEP_STEPLISTENER_H
|
||||||
52
app/src/main/cpp/jni_lockstep.cpp
Normal file
52
app/src/main/cpp/jni_lockstep.cpp
Normal 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"
|
||||||
19
app/src/main/cpp/jni_mpg123.cpp
Normal file
19
app/src/main/cpp/jni_mpg123.cpp
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
52
app/src/main/cpp/jni_stepdetector.cpp
Normal file
52
app/src/main/cpp/jni_stepdetector.cpp
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// Created by david on 03.03.2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
#include "StepDetector.h"
|
||||||
|
#include <new>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
jint throwIllegalArgumentException(JNIEnv *env, const char *message)
|
||||||
|
{
|
||||||
|
jclass exClass;
|
||||||
|
const char *className = "java/lang/IllegalArgumentException";
|
||||||
|
exClass = env->FindClass(className);
|
||||||
|
if (exClass == nullptr) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return env->ThrowNew(exClass, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jlong JNICALL
|
||||||
|
Java_at_lockstep_filter_StepDetector_native_1create(
|
||||||
|
JNIEnv *env,
|
||||||
|
jclass /*unused*/, jlong engineHandle) {
|
||||||
|
auto *listener = reinterpret_cast<StepListener *>(engineHandle);
|
||||||
|
// We use std::nothrow so `new` returns a nullptr if the engine creation fails
|
||||||
|
auto *detector = new(std::nothrow) StepDetector(listener);
|
||||||
|
return reinterpret_cast<jlong>(detector);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_at_lockstep_filter_StepDetector_native_1delete(
|
||||||
|
JNIEnv *env,
|
||||||
|
jclass /*unused*/, jlong handle) {
|
||||||
|
delete reinterpret_cast<StepDetector *>(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_at_lockstep_filter_StepDetector_native_1filter(
|
||||||
|
JNIEnv *env,
|
||||||
|
jclass /*unused*/, jlong handle, jlong timestamp, jfloatArray values) {
|
||||||
|
if(values == nullptr) throwIllegalArgumentException(env, "values == null");
|
||||||
|
float* nativeValues = (float *)env->GetFloatArrayElements(values, 0);
|
||||||
|
jsize length = env->GetArrayLength(values);
|
||||||
|
std::vector<float> vecValues(nativeValues, nativeValues + length);
|
||||||
|
auto *detector = reinterpret_cast<StepDetector *>(handle);
|
||||||
|
detector->filter(vecValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
1
app/src/main/cpp/libpasada
Submodule
1
app/src/main/cpp/libpasada
Submodule
Submodule app/src/main/cpp/libpasada added at b919e845c7
1
app/src/main/cpp/librubberband
Submodule
1
app/src/main/cpp/librubberband
Submodule
Submodule app/src/main/cpp/librubberband added at ae0d81599d
@@ -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"
|
|
||||||
164
app/src/main/cpp/mp3file.cpp
Normal file
164
app/src/main/cpp/mp3file.cpp
Normal 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
|
||||||
|
}
|
||||||
34
app/src/main/cpp/mp3file.h
Normal file
34
app/src/main/cpp/mp3file.h
Normal 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
|
||||||
292
app/src/main/java/at/lockstep/app/LstForegroundService.java
Normal file
292
app/src/main/java/at/lockstep/app/LstForegroundService.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,176 @@
|
|||||||
package at.lockstep.app;
|
package at.lockstep.app;
|
||||||
|
|
||||||
import android.app.Activity;
|
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.pb.PlaybackEngine;
|
||||||
|
import at.lockstep.saf.SafPickerActivity;
|
||||||
|
import at.lockstep.ui.SongPickerActivity;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
public class MainActivity extends Activity {
|
import com.google.gson.Gson;
|
||||||
/*
|
import com.google.gson.GsonBuilder;
|
||||||
* 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 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
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.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
|
@Override
|
||||||
protected void onPause() {
|
public void onResult(LstForegroundService.SensorDataArray recording) {
|
||||||
PlaybackEngine.delete();
|
if(!isForeground) {
|
||||||
super.onPause();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/src/main/java/at/lockstep/filter/StepDetector.java
Normal file
21
app/src/main/java/at/lockstep/filter/StepDetector.java
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package at.lockstep.filter;
|
||||||
|
|
||||||
|
import at.lockstep.pb.PlaybackEngine;
|
||||||
|
|
||||||
|
public class StepDetector {
|
||||||
|
long handle;
|
||||||
|
|
||||||
|
public StepDetector(long engineHandle) {
|
||||||
|
handle = native_create(engineHandle);
|
||||||
|
}
|
||||||
|
public void close() {
|
||||||
|
native_delete(handle);
|
||||||
|
}
|
||||||
|
public void filter(long timestamp, float[] values) {
|
||||||
|
native_filter(handle, timestamp, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native long native_create(long engineHandle);
|
||||||
|
private static native void native_delete(long sdHandle);
|
||||||
|
private static native void native_filter(long sdHandle, long timestamp, float[] values);
|
||||||
|
}
|
||||||
60
app/src/main/java/at/lockstep/pb/AudioResources.java
Normal file
60
app/src/main/java/at/lockstep/pb/AudioResources.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,91 @@
|
|||||||
package at.lockstep.pb;
|
package at.lockstep.pb;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
public class PlaybackEngine {
|
public class PlaybackEngine {
|
||||||
static long mEngineHandle = 0;
|
static long mEngineHandle = 0;
|
||||||
|
static final int MPG123_OK = 0;
|
||||||
|
static boolean mFilesystemInitialized = false;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
System.loadLibrary("lockstep-native");
|
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) {
|
if (mEngineHandle == 0) {
|
||||||
//setDefaultStreamValues(context); // TODO
|
setDefaultStreamValues(context);
|
||||||
Log.i("PlaybackEngine", "Hello PlaybackEngine");
|
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);
|
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() {
|
public static void delete() {
|
||||||
if (mEngineHandle != 0) {
|
if (mEngineHandle != 0) {
|
||||||
native_deleteEngine(mEngineHandle);
|
native_deleteEngine(mEngineHandle);
|
||||||
}
|
}
|
||||||
mEngineHandle = 0;
|
mEngineHandle = 0;
|
||||||
}
|
}
|
||||||
|
public static long getEngineHandle() {
|
||||||
private static native long native_createEngine();
|
return mEngineHandle;
|
||||||
private static native void native_deleteEngine(long engineHandle);
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
138
app/src/main/java/at/lockstep/saf/SafPickerActivity.java
Normal file
138
app/src/main/java/at/lockstep/saf/SafPickerActivity.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/src/main/java/at/lockstep/ui/SongItem.java
Normal file
40
app/src/main/java/at/lockstep/ui/SongItem.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
170
app/src/main/java/at/lockstep/ui/SongPickerActivity.java
Normal file
170
app/src/main/java/at/lockstep/ui/SongPickerActivity.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
66
app/src/main/java/at/lockstep/ui/SongPickerAdapter.java
Normal file
66
app/src/main/java/at/lockstep/ui/SongPickerAdapter.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/src/main/lib/mpg123/build_arm.sh
Normal file
23
app/src/main/lib/mpg123/build_arm.sh
Normal 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
|
||||||
25
app/src/main/lib/mpg123/build_win.sh
Normal file
25
app/src/main/lib/mpg123/build_win.sh
Normal 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
|
||||||
135
app/src/main/lib/mpg123/lib/arm64-v8a/include/fmt123.h
Normal file
135
app/src/main/lib/mpg123/lib/arm64-v8a/include/fmt123.h
Normal 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
|
||||||
|
|
||||||
1441
app/src/main/lib/mpg123/lib/arm64-v8a/include/mpg123.h
Normal file
1441
app/src/main/lib/mpg123/lib/arm64-v8a/include/mpg123.h
Normal file
File diff suppressed because it is too large
Load Diff
BIN
app/src/main/lib/mpg123/lib/arm64-v8a/libmpg123.so
Normal file
BIN
app/src/main/lib/mpg123/lib/arm64-v8a/libmpg123.so
Normal file
Binary file not shown.
135
app/src/main/lib/mpg123/lib/armeabi-v7a/include/fmt123.h
Normal file
135
app/src/main/lib/mpg123/lib/armeabi-v7a/include/fmt123.h
Normal 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
|
||||||
|
|
||||||
1441
app/src/main/lib/mpg123/lib/armeabi-v7a/include/mpg123.h
Normal file
1441
app/src/main/lib/mpg123/lib/armeabi-v7a/include/mpg123.h
Normal file
File diff suppressed because it is too large
Load Diff
BIN
app/src/main/lib/mpg123/lib/armeabi-v7a/libmpg123.so
Normal file
BIN
app/src/main/lib/mpg123/lib/armeabi-v7a/libmpg123.so
Normal file
Binary file not shown.
159
app/src/main/lib/mpg123/lib/x86_64/include/fmt123.h
Normal file
159
app/src/main/lib/mpg123/lib/x86_64/include/fmt123.h
Normal 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
|
||||||
|
|
||||||
2356
app/src/main/lib/mpg123/lib/x86_64/include/mpg123.h
Normal file
2356
app/src/main/lib/mpg123/lib/x86_64/include/mpg123.h
Normal file
File diff suppressed because it is too large
Load Diff
751
app/src/main/lib/mpg123/lib/x86_64/include/out123.h
Normal file
751
app/src/main/lib/mpg123/lib/x86_64/include/out123.h
Normal 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
|
||||||
|
|
||||||
1211
app/src/main/lib/mpg123/lib/x86_64/include/syn123.h
Normal file
1211
app/src/main/lib/mpg123/lib/x86_64/include/syn123.h
Normal file
File diff suppressed because it is too large
Load Diff
BIN
app/src/main/lib/mpg123/lib/x86_64/libmpg123.so
Normal file
BIN
app/src/main/lib/mpg123/lib/x86_64/libmpg123.so
Normal file
Binary file not shown.
35
app/src/main/res/layout/activity_main.xml
Normal file
35
app/src/main/res/layout/activity_main.xml
Normal 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>
|
||||||
13
app/src/main/res/layout/activity_media_store_benchmark.xml
Normal file
13
app/src/main/res/layout/activity_media_store_benchmark.xml
Normal 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>
|
||||||
28
app/src/main/res/layout/activity_song_picker.xml
Normal file
28
app/src/main/res/layout/activity_song_picker.xml
Normal 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>
|
||||||
34
app/src/main/res/layout/song_card.xml
Normal file
34
app/src/main/res/layout/song_card.xml
Normal 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>
|
||||||
BIN
app/src/main/res/raw/track_beat.mp3
Normal file
BIN
app/src/main/res/raw/track_beat.mp3
Normal file
Binary file not shown.
@@ -1,3 +1,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Lockstep</string>
|
<string name="app_name">Lockstep</string>
|
||||||
|
<string name="notification_text">Reading your steps …</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<style name="Theme.Lockstep" parent="Theme.AppCompat.DayNight.NoActionBar" />
|
||||||
<style name="Theme.Lockstep" parent="android:Theme.Material.Light.NoActionBar" />
|
|
||||||
</resources>
|
</resources>
|
||||||
46
app/src/test/java/at/lockstep/GsonUnitTest.java
Normal file
46
app/src/test/java/at/lockstep/GsonUnitTest.java
Normal 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]}]}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,9 @@ composeBom = "2024.04.01"
|
|||||||
logbackAndroid = "2.0.0"
|
logbackAndroid = "2.0.0"
|
||||||
oboe = "1.10.0"
|
oboe = "1.10.0"
|
||||||
slf4jApi = "1.7.30"
|
slf4jApi = "1.7.30"
|
||||||
|
recyclerview = "1.3.1"
|
||||||
|
appcompat = "1.7.1"
|
||||||
|
gson = "2.11.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
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" }
|
logback-android = { module = "com.github.tony19:logback-android", version.ref = "logbackAndroid" }
|
||||||
oboe = { module = "com.google.oboe:oboe", version.ref = "oboe" }
|
oboe = { module = "com.google.oboe:oboe", version.ref = "oboe" }
|
||||||
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4jApi" }
|
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]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user