我的布局有大约60个按钮,每个按钮按下时会播放不同的音频文件。我将所有音频文件作为mp3存储在我的资源文件夹中并播放它们我基本上使用与Google NDK示例“native-audio”项目中使用的相同的代码: https://github.com/googlesamples/android-ndk
我有10个相同的本机函数(只有具有唯一命名的变量),就像这样工作..
播放声音的功能:
jboolean Java_com_example_nativeaudio_Fretboard_player7play(JNIEnv* env, jclass clazz, jobject assetManager, jstring filename)
{
SLresult result;
// convert Java string to UTF-8
const char *utf8 = (*env)->GetStringUTFChars(env, filename, NULL);
assert(NULL != utf8);
// use asset manager to open asset by filename
AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
assert(NULL != mgr);
AAsset* asset = AAssetManager_open(mgr, utf8, AASSET_MODE_UNKNOWN);
// release the Java string and UTF-8
(*env)->ReleaseStringUTFChars(env, filename, utf8);
// the asset might not be found
if (NULL == asset) {
return JNI_FALSE;
}
// open asset as file descriptor
off_t start, length;
int fd = AAsset_openFileDescriptor(asset, &start, &length);
assert(0 <= fd);
AAsset_close(asset);
// configure audio source
SLDataLocator_AndroidFD loc_fd = {SL_DATALOCATOR_ANDROIDFD, fd, start, length};
SLDataFormat_MIME format_mime = {SL_DATAFORMAT_MIME, NULL, SL_CONTAINERTYPE_UNSPECIFIED};
SLDataSource audioSrc = {&loc_fd, &format_mime};
// configure audio sink
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
SLDataSink audioSnk = {&loc_outmix, NULL};
// create audio player
const SLInterfaceID ids[3] = {SL_IID_SEEK, SL_IID_MUTESOLO, SL_IID_VOLUME};
const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
result = (*engineEngine)->CreateAudioPlayer(engineEngine, &p7PlayerObject, &audioSrc, &audioSnk,
3, ids, req);
assert(SL_RESULT_SUCCESS == result);
(void)result;
// realize the player
result = (*p7PlayerObject)->Realize(p7PlayerObject, SL_BOOLEAN_FALSE);
assert(SL_RESULT_SUCCESS == result);
(void)result;
// get the play interface
result = (*p7PlayerObject)->GetInterface(p7PlayerObject, SL_IID_PLAY, &p7PlayerPlay);
assert(SL_RESULT_SUCCESS == result);
(void)result;
if (NULL != p7PlayerPlay) {
// play
result = (*p7PlayerPlay)->SetPlayState(p7PlayerPlay, SL_PLAYSTATE_PLAYING);
assert(SL_RESULT_SUCCESS == result);
(void)result;
}
return JNI_TRUE;
}
停止声音的功能:
void Java_com_example_nativeaudio_Fretboard_player7stop(JNIEnv* env, jclass clazz)
{
SLresult result;
// make sure the asset audio player was created
if (NULL != p7PlayerPlay) {
// set the player's state
result = (*p7PlayerPlay)->SetPlayState(p7PlayerPlay, SL_PLAYSTATE_STOPPED);
assert(SL_RESULT_SUCCESS == result);
(void)result;
// destroy file descriptor audio player object, and invalidate all associated interfaces
(*p7PlayerObject)->Destroy(p7PlayerObject);
p7PlayerObject = NULL;
p7PlayerPlay = NULL;
}
}
这很容易处理,但我希望尽量减少延迟,避免每次我想播放不同的文件时都要(*engineEngine)->CreateAudioPlayer()
。有没有办法只更改音频播放器使用的audioSrc,而不必每次都从头开始重新创建它?
作为奖励,我在哪里可以阅读更多有关这些内容的内容?似乎很难在任何地方找到有关OpenSL ES的任何信息。
答案 0 :(得分:2)
我们在同一条船上,我现在也熟悉NDK和OpenSL ES。我的回答是基于我的经验,完全由大约2天的实验组成,所以可能有更好的方法,但这些信息可能会帮助你。
我有10个相同的本机函数(只有唯一命名的变量),它们的工作原理如下..
如果我理解你的情况,你就不需要有重复的功能了。这些调用中唯一不同的是按下按钮并最终播放声音,这可以通过JNI调用作为参数传递。您可以将创建的播放器和数据存储在全局可访问的结构中,以便在需要停止/重放时可以检索它,也可以使用buttonId作为地图的键。
[..]但是我希望每次我想播放不同的文件时最小化延迟并避免不得不做(* engineEngine) - &gt; CreateAudioPlayer()。有没有办法只更改音频播放器使用的audioSrc,而不必每次都从头开始重新创建它?
是的,不断创建和销毁玩家的成本很高,并且可能导致堆碎(如OpenSL ES 1.0规范中所述)。首先,我认为他的DynamicSourceItf允许你切换数据源,但似乎这个界面并不是这样使用的,至少在Android 6上这会返回&#39;功能不支持&#39;。
我怀疑为每个独特的声音创建一个播放器是一个很好的解决方案,特别是因为在彼此之上多次播放相同的声音(例如,它在游戏中很常见)需要任意数量同样声音的其他玩家。
BufferQueues是玩家在玩游戏时将处理的各个缓冲区的队列。当所有缓冲区都已处理完毕后,播放器会停止播放。 (它的官方状态仍在“玩”但是,一旦新的缓冲区被排队,它就会恢复。
这使您可以创建与您需要的重叠声音一样多的播放器。当你想播放声音时,你会迭代这些播放器,直到找到一个当前没有处理缓冲区的播放器(BufferQueueItf->GetState(...)
提供此信息或者可以注册回叫,这样你就可以将播放器标记为& #39;自由&#39)。然后,你将声音需要的缓冲区排入队列,然后立即开始播放。
据我所知,BufferQueue的格式在创建时被锁定。因此,您必须确保您拥有相同格式的所有输入缓冲区,或者为每种格式创建不同的BufferQueue(和播放器)。
根据Android NDK文档,BufferQueue接口预计将来会发生重大变化。他们提取了一个简化的界面,其中包含大部分BufferQueue的功能,并称之为AndroidSimpleBufferQueue。此接口不会发生变化,从而使您的代码更具未来性。
使用AndroidSimpleBufferQueue释放的主要功能是能够使用非PCM源数据,因此您必须在使用前解码文件。这可以在OpenSL ES中使用AndroidSimpleBufferQueue作为接收器来完成。更新的API使用MediaCodec及其NDK实现NDKMedia(检查本机编解码器示例)提供了额外的支持。
NDK文档确实包含一些在其他地方很难找到的重要信息。 Here's OpenSL ES特定页面。
可能接近600页且难以消化,但OpenSL ES 1.0 Specification应该是您的主要信息资源。我强烈建议阅读第4章,因为它很好地概述了工作原理。第3章有关于具体设计的更多信息。然后,我只是跳过使用搜索功能来读取接口和对象。
一旦你理解了OpenSL如何工作的基本原理,它似乎非常简单。有媒体对象(播放器和录像机等)和数据源(输入)和数据接收器(输出)。实质上,您将输入连接到媒体对象,该媒体对象将处理后的数据路由到其连接的输出。
源规范,接收器和媒体对象都记录在规范中,包括它们的接口。有了这些信息,它实际上只是选择你需要的构建块并将它们连接在一起。
从我的测试来看,似乎BufferQueue和AndroidSimpleBufferQueue都不支持非PCM数据,至少在我测试的系统上没有(Nexus 7 @ 6.01,NVidia Shield K1 @ 6.0.1)所以在使用之前,您需要解码数据。
我尝试使用MediaExtractor和MediaCodec的NDK版本,但有几点需要注意:
MediaExtractor似乎无法正确返回使用加密解码所需的UUID信息,至少对于我测试过的文件没有。 AMediaExtractor_getPsshInfo
会返回nullptr
。
API并不总是与标题声明中的注释相同。例如,通过检查返回的字节数而不是检查AMediaExtractor_advance
函数的返回值,检查MediaExtractor中的EOS(流末尾)似乎是最可靠的。
我建议保留Java用于解码过程,因为这些API更加成熟,绝对经过更多测试,您可能会从中获得更多功能。获得原始PCM数据的缓冲区后,您可以将其传递给本机代码,从而减少延迟。