OpenSL ES 无需重新创建音频播放器即可更改音频源



我有一个布局,大约有 60 个按钮,每个按钮在按下时都会播放不同的音频文件。我的所有音频文件都以mp3的形式保存在我的资产文件夹中,为了播放它们,我基本上使用与Google NDK示例"本机音频"项目中使用的代码相同的代码: 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的任何信息。

我们在同一条船上,我目前正在熟悉NDK和OpenSL ES。我的答案是基于我的经验,完全包括~2天的实验,所以可能有更好的方法,但这些信息可能会帮助你。

我有 10 个相同的本机函数(只是具有唯一命名的变量)像这样工作。

如果我正确理解了您的情况,则不需要为此使用重复的功能。在这些调用中唯一不同的是按下按钮,最终是要播放的声音,这可以通过 JNI 调用作为参数传递。您可以将创建的播放器和数据存储在全局可访问的结构中,以便在需要停止/重播时可以检索它,也许可以使用 buttonId 作为地图的键。

[..]但我想尽量减少延迟并避免每次我想播放不同的文件时都必须执行(*engineEngine)->CreateAudioPlayer()。有没有办法只更改音频播放器使用的 audioSrc,而不必每次都从头开始销毁和重新创建它?

是的,不断创建和销毁玩家的成本很高,并且可能导致堆碎片化(如 OpenSL ES 1.0 规范中所述)。首先,我以为他 DynamicSourceItf 会允许您切换数据源,但似乎这个接口不打算这样使用,至少在 Android 6 上它会返回"功能不受支持"。

我怀疑为每个独特的声音创建一个播放器是一个很好的解决方案,特别是因为多次播放相同的声音(例如,在游戏中很常见)将需要任意数量的额外播放器来获得相同的声音。

缓冲区队列

缓冲区队列是玩家在玩游戏时将处理的单个缓冲区的队列。当所有缓冲区都处理完毕后,玩家会"停止"(官方状态仍在"播放"),但会在新缓冲区排队后立即恢复。

这允许您做的是创建尽可能多的播放器,因为您需要重叠的声音。当您想要播放声音时,您可以遍历这些播放器,直到找到当前未处理缓冲区的播放器(BufferQueueItf->GetState(...)提供此信息,或者可以注册回调,以便您可以将播放器标记为"免费")。然后,您可以根据声音需要对任意数量的缓冲区进行排队,这些缓冲区将立即开始播放。

据我所知,BufferQueue 的格式在创建时是锁定的。因此,您必须确保所有输入缓冲区采用相同的格式,或者为每种格式创建不同的 BufferQueue(和播放器)。

Android Simple BufferQueue

根据Android NDK文档,BufferQueue接口预计将在未来发生重大变化。他们提取了一个简化的接口,其中包含BufferQueue的大部分功能,并将其命名为AndroidSimpleBufferQueue。此接口预计不会更改,从而使您的代码更加面向未来。

使用AndroidSimpleBufferQueue失去的主要功能是能够使用非PCM源数据,因此您必须在使用前解码文件。这可以在OpenSL ES中使用AndroidSimpleBufferQueue作为接收器来完成。更新的 API 对使用 MediaCodec 及其 NDK 实现 NDKMedia 提供了额外的支持(查看本机编解码器示例)。

资源

NDK 文档确实包含一些在其他任何地方都很难找到的重要信息。这是OpenSL ES特定的页面。

它可能接近600页,难以消化,但OpenSL ES 1.0规范应该是你的主要信息资源。我强烈建议阅读第4章,因为它很好地概述了事物的工作原理。第3章提供了有关具体设计的更多信息。然后,我只是使用搜索功能跳来跳去,随时阅读接口和对象。

了解 OpenSL ES

一旦你了解了OpenSL工作原理的基本原理,它似乎非常简单。有媒体对象(播放器和录像机等)和数据源(输入)和数据接收器(输出)。实质上是将输入连接到媒体对象,该媒体对象将处理后的数据路由到其连接的输出。

源、接收器和媒体对象都记录在规范中,包括它们的接口。有了这些信息,实际上只需选择您需要的构建块并将它们插入在一起。

更新 07/29/16

从我的测试来看,似乎BufferQueue和AndroidSimpleBufferQueue都不支持非PCM数据,至少在我测试过的系统(Nexus 7 @ 6.01,NVidia Shield K1 @ 6.0.1)上不支持,因此您需要先解码数据才能使用它。

我尝试使用NDK版本的MediaExtractor和MediaCodec,但有几个注意事项需要注意:

  • MediaExtractor 似乎没有正确返回使用加密解码所需的 UUID 信息,至少对于我测试过的文件没有。AMediaExtractor_getPsshInfo返回一个nullptr

  • API 并不总是表现为标头声明中的注释。例如,在MediaExtractor中检查EOS(流结束)似乎是最可靠的,方法是检查返回的字节量而不是检查AMediaExtractor_advance函数的返回值。

我建议在解码过程中留在 Java 中,因为这些 API 更成熟,肯定经过更多测试,您可能会从中获得更多功能。获得原始 PCM 数据的缓冲区后,可以将其传递给本机代码,从而减少延迟。

最新更新