diff --git a/packages/play-audio/build.sh b/packages/play-audio/build.sh new file mode 100644 index 000000000..95ed3adef --- /dev/null +++ b/packages/play-audio/build.sh @@ -0,0 +1,11 @@ +TERMUX_PKG_HOMEPAGE=http://termux.com +TERMUX_PKG_DESCRIPTION="Simple commandline audio player for Android" +TERMUX_PKG_VERSION=0.1 + +termux_step_make_install () { + $CXX $CFLAGS $LDFLAGS \ + -std=c++14 -Wall -Wextra -pedantic -Werror \ + -lOpenSLES \ + $TERMUX_PKG_BUILDER_DIR/play-audio.cpp -o $TERMUX_PREFIX/bin/play-audio + cp $TERMUX_PKG_BUILDER_DIR/play-audio.1 $TERMUX_PREFIX/share/man/man1/ +} diff --git a/packages/play-audio/play-audio.1 b/packages/play-audio/play-audio.1 new file mode 100644 index 000000000..727a355ea --- /dev/null +++ b/packages/play-audio/play-audio.1 @@ -0,0 +1,22 @@ +.Dd July 27 2015 +.Dt play-audio 1 +.Sh NAME +.Nm play-audio +.Nd audio player using the Android media system +.Sh SYNOPSIS +.Nm play-audio +.Op Ar files +.Sh DESCRIPTION +The +.Nm play-audio +utility plays the files listed as arguments, in order, using the Android media system. +.Pp +The supported media formats may vary across difference devices and Android versions. +.Sh EXAMPLES +Play two ogg files in succession: +.Pp +.Dl $ play-audio path/to/first.ogg path/to/second.ogg +.Pp +.Sh AUTHOR +.An Fredrik Fornwall Aq Mt fredrik@fornwall.net + diff --git a/packages/play-audio/play-audio.cpp b/packages/play-audio/play-audio.cpp new file mode 100644 index 000000000..4073258df --- /dev/null +++ b/packages/play-audio/play-audio.cpp @@ -0,0 +1,212 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class AudioPlayer { + public: + AudioPlayer(); + ~AudioPlayer(); + void play(char const* uri); + private: + SLObjectItf mSlEngineObject{NULL}; + SLEngineItf mSlEngineInterface{NULL}; + SLObjectItf mSlOutputMixObject{NULL}; +}; + +class MutexWithCondition { + public: + MutexWithCondition() { pthread_mutex_lock(&mutex); } + ~MutexWithCondition() { pthread_mutex_unlock(&mutex); } + void waitFor() { while (!occurred) pthread_cond_wait(&condition, &mutex); } + /** From waking thread. */ + void lockAndSignal() { + pthread_mutex_lock(&mutex); + occurred = true; + pthread_cond_signal(&condition); + pthread_mutex_unlock(&mutex); + } + private: + volatile bool occurred{false}; + pthread_mutex_t mutex{PTHREAD_MUTEX_INITIALIZER}; + pthread_cond_t condition{PTHREAD_COND_INITIALIZER}; +}; + +AudioPlayer::AudioPlayer() { + // "OpenSL ES for Android is designed for multi-threaded applications, and is thread-safe. + // OpenSL ES for Android supports a single engine per application, and up to 32 objects. + // Available device memory and CPU may further restrict the usable number of objects. + // slCreateEngine recognizes, but ignores, these engine options: SL_ENGINEOPTION_THREADSAFE SL_ENGINEOPTION_LOSSOFCONTROL" + SLresult result = slCreateEngine(&mSlEngineObject, + /*numOptions=*/0, /*options=*/NULL, + /*numWantedInterfaces=*/0, /*wantedInterfaces=*/NULL, /*wantedInterfacesRequired=*/NULL); + assert(SL_RESULT_SUCCESS == result); + + result = (*mSlEngineObject)->Realize(mSlEngineObject, SL_BOOLEAN_FALSE); + assert(SL_RESULT_SUCCESS == result); + + result = (*mSlEngineObject)->GetInterface(mSlEngineObject, SL_IID_ENGINE, &mSlEngineInterface); + assert(SL_RESULT_SUCCESS == result); + + SLuint32 const numWantedInterfaces = 1; + SLInterfaceID wantedInterfaces[numWantedInterfaces]{ SL_IID_ENVIRONMENTALREVERB }; + SLboolean wantedInterfacesRequired[numWantedInterfaces]{ SL_BOOLEAN_TRUE }; + result = (*mSlEngineInterface)->CreateOutputMix(mSlEngineInterface, &mSlOutputMixObject, numWantedInterfaces, wantedInterfaces, wantedInterfacesRequired); + assert(SL_RESULT_SUCCESS == result); + + result = (*mSlOutputMixObject)->Realize(mSlOutputMixObject, SL_BOOLEAN_FALSE); + assert(SL_RESULT_SUCCESS == result); + +} + +void opensl_prefetch_callback(SLPrefetchStatusItf caller, void* pContext, SLuint32 event) { + if (event & SL_PREFETCHEVENT_STATUSCHANGE) { + SLpermille level = 0; + (*caller)->GetFillLevel(caller, &level); + if (level == 0) { + SLuint32 status; + (*caller)->GetPrefetchStatus(caller, &status); + if (status == SL_PREFETCHSTATUS_UNDERFLOW) { + // Level is 0 but we have SL_PREFETCHSTATUS_UNDERFLOW, implying an error. + printf("- ERROR: Underflow when prefetching data and fill level zero\n"); + MutexWithCondition* cond = (MutexWithCondition*) pContext; + cond->lockAndSignal(); + } + } + } +} + +void opensl_player_callback(SLPlayItf /*caller*/, void* pContext, SLuint32 /*event*/) { + MutexWithCondition* condition = (MutexWithCondition*) pContext; + condition->lockAndSignal(); +} + +void AudioPlayer::play(char const* uri) +{ + SLDataLocator_URI loc_uri = {SL_DATALOCATOR_URI, (SLchar *) uri}; + SLDataFormat_MIME format_mime = {SL_DATAFORMAT_MIME, NULL, SL_CONTAINERTYPE_UNSPECIFIED}; + SLDataSource audioSrc = {&loc_uri, &format_mime}; + + SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, mSlOutputMixObject}; + SLDataSink audioSnk = {&loc_outmix, NULL}; + + // SL_IID_ANDROIDCONFIGURATION is Android specific interface, SL_IID_VOLUME is general: + SLuint32 const numWantedInterfaces = 5; + SLInterfaceID wantedInterfaces[numWantedInterfaces]{ + SL_IID_ANDROIDCONFIGURATION, + SL_IID_VOLUME, + SL_IID_PREFETCHSTATUS, + SL_IID_PLAYBACKRATE, + SL_IID_EFFECTSEND + }; + SLboolean wantedInterfacesRequired[numWantedInterfaces]{ SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE }; + + SLObjectItf uriPlayerObject = NULL; + SLresult result = (*mSlEngineInterface)->CreateAudioPlayer(mSlEngineInterface, &uriPlayerObject, &audioSrc, &audioSnk, + numWantedInterfaces, wantedInterfaces, wantedInterfacesRequired); + assert(SL_RESULT_SUCCESS == result); + + // Android specific interface - usage: + // SLresult (*GetInterface) (SLObjectItf self, const SLInterfaceID iid, void* pInterface); + // This function gives different interfaces. One is android-specific, from + // , done before realization: + SLAndroidConfigurationItf androidConfig; + result = (*uriPlayerObject)->GetInterface(uriPlayerObject, SL_IID_ANDROIDCONFIGURATION, &androidConfig); + assert(SL_RESULT_SUCCESS == result); + + // This allows setting the stream type (default:SL_ANDROID_STREAM_MEDIA): + /* same as android.media.AudioManager.STREAM_VOICE_CALL */ + // #define SL_ANDROID_STREAM_VOICE ((SLint32) 0x00000000) + /* same as android.media.AudioManager.STREAM_SYSTEM */ + // #define SL_ANDROID_STREAM_SYSTEM ((SLint32) 0x00000001) + /* same as android.media.AudioManager.STREAM_RING */ + // #define SL_ANDROID_STREAM_RING ((SLint32) 0x00000002) + /* same as android.media.AudioManager.STREAM_MUSIC */ + // #define SL_ANDROID_STREAM_MEDIA ((SLint32) 0x00000003) + /* same as android.media.AudioManager.STREAM_ALARM */ + // #define SL_ANDROID_STREAM_ALARM ((SLint32) 0x00000004) + /* same as android.media.AudioManager.STREAM_NOTIFICATION */ + // #define SL_ANDROID_STREAM_NOTIFICATION ((SLint32) 0x00000005) + SLint32 androidStreamType = SL_ANDROID_STREAM_ALARM; + result = (*androidConfig)->SetConfiguration(androidConfig, SL_ANDROID_KEY_STREAM_TYPE, &androidStreamType, sizeof(SLint32)); + assert(SL_RESULT_SUCCESS == result); + + // We now Realize(). Note that the android config needs to be done before, but getting the SLPrefetchStatusItf after. + result = (*uriPlayerObject)->Realize(uriPlayerObject, /*async=*/SL_BOOLEAN_FALSE); + assert(SL_RESULT_SUCCESS == result); + + SLPrefetchStatusItf prefetchInterface; + result = (*uriPlayerObject)->GetInterface(uriPlayerObject, SL_IID_PREFETCHSTATUS, &prefetchInterface); + assert(SL_RESULT_SUCCESS == result); + + SLPlayItf uriPlayerPlay = NULL; + result = (*uriPlayerObject)->GetInterface(uriPlayerObject, SL_IID_PLAY, &uriPlayerPlay); + assert(SL_RESULT_SUCCESS == result); + + SLPlaybackRateItf playbackRateInterface; + result = (*uriPlayerObject)->GetInterface(uriPlayerObject, SL_IID_PLAYBACKRATE, &playbackRateInterface); + assert(SL_RESULT_SUCCESS == result); + + if (NULL == uriPlayerPlay) { + fprintf(stderr, "Cannot play '%s'\n", uri); + } else { + result = (*uriPlayerPlay)->SetCallbackEventsMask(uriPlayerPlay, SL_PLAYEVENT_HEADSTALLED | SL_PLAYEVENT_HEADATEND); + assert(SL_RESULT_SUCCESS == result); + + MutexWithCondition condition; + result = (*uriPlayerPlay)->RegisterCallback(uriPlayerPlay, opensl_player_callback, &condition); + assert(SL_RESULT_SUCCESS == result); + + result = (*prefetchInterface)->RegisterCallback(prefetchInterface, opensl_prefetch_callback, &condition); + assert(SL_RESULT_SUCCESS == result); + result = (*prefetchInterface)->SetCallbackEventsMask(prefetchInterface, SL_PREFETCHEVENT_FILLLEVELCHANGE | SL_PREFETCHEVENT_STATUSCHANGE); + + // "For an audio player with URI data source, Object::Realize allocates resources but does not + // connect to the data source (i.e. "prepare") or begin pre-fetching data. These occur once the + // player state is set to either SL_PLAYSTATE_PAUSED or SL_PLAYSTATE_PLAYING." + // - http://mobilepearls.com/labs/native-android-api/ndk/docs/opensles/index.html + result = (*uriPlayerPlay)->SetPlayState(uriPlayerPlay, SL_PLAYSTATE_PLAYING); + assert(SL_RESULT_SUCCESS == result); + + condition.waitFor(); + } + + if (uriPlayerObject != NULL) (*uriPlayerObject)->Destroy(uriPlayerObject); +} + + +AudioPlayer::~AudioPlayer() +{ + // "Be sure to destroy all objects on exit from your application. Objects should be destroyed in reverse order of their creation, + // as it is not safe to destroy an object that has any dependent objects. For example, destroy in this order: audio players + // and recorders, output mix, then finally the engine." + if (mSlOutputMixObject != NULL) { (*mSlOutputMixObject)->Destroy(mSlOutputMixObject); mSlOutputMixObject = NULL; } + if (mSlEngineObject != NULL) { (*mSlEngineObject)->Destroy(mSlEngineObject); mSlEngineObject = NULL; } +} + + +int main(int argc, char** argv) +{ + bool help = false; + int c; + while ((c = getopt(argc, argv, "h")) != -1) { + switch (c) { + case 'h': help = true; break; + } + } + + if (help || optind == argc) { + printf("usage: %s [files]\n", argv[0]); + exit(0); + } + + AudioPlayer player; + for (int i = optind; i < argc; i++) player.play(argv[i]); + + return 0; +}