OpenSL ES 在不重新创建音频播放器的情况下更改音频源

OpenSL ES change audio source without recreating audio player

我的布局有大约 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 的任何信息。

我们在同一条船上,我目前也在熟悉 NDK 和 OpenSL ES。我的回答基于我的经验,完全由大约 2 天的实验组成,因此可能有更好的方法,但这些信息可能会帮助你。

I have 10 identical native functions (just with uniquely named variables) that work like this..

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

[..]but I want to minimize latency and avoid having to do (*engineEngine)->CreateAudioPlayer() every time I want to play a different file. Is there any way to just change the audioSrc used by the audio player without having to destroy and recreate it from scratch every time?

是的,不断创建和销毁玩家的成本很高,并且会导致堆碎片化(如 OpenSL ES 1.0 规范中所述)。首先,我以为他 DynamicSourceItf 会允许你切换数据源,但似乎这个接口不打算那样使用,至少在 Android 6 this returns 'feature unsupported' 上。

我怀疑为每个独特的声音创建一个播放器是否是一个很好的解决方案,尤其是因为在彼此之上多次播放相同的声音(例如,这在游戏中很常见)需要任意数量的额外相同声音的播放器。

缓冲区Queues

BufferQueues 是 queues 个单独的缓冲区,玩家在玩游戏时将处理这些缓冲区。当所有缓冲区都已处理后,播放器 'stops'(虽然它的官方状态仍然是 'playing')但是一旦新缓冲区入队就会恢复。

这允许您根据需要创建尽可能多的重叠声音播放器。当你想播放声音时,你会遍历这些播放器,直到找到一个当前没有处理缓冲区的播放器(BufferQueueItf->GetState(...) 提供此信息或者可以注册回调,这样你就可以将播放器标记为 'free').然后,您将尽可能多的缓冲区放入您的声音需要的队列中,这些缓冲区将立即开始播放。

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

Android 简单缓冲区队列

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

使用 AndroidSimpleBufferQueue 的主要功能是能够使用 non-PCM 源数据,因此您必须在使用前解码文件。这可以在 OpenSL ES 中使用 AndroidSimpleBufferQueue 作为接收器来完成。最近的 APIs 有使用 MediaCodec 的额外支持,它是 NDK 实现 NDKMedia(查看 native-codec 示例)。

资源

NDK 文档确实包含一些其他地方很难找到的重要信息。 Here's OpenSL ES 特定页面。

它可能接近 600 页且难以消化,但 OpenSL ES 1.0 Specification 应该是您的主要信息资源。我强烈推荐阅读第 4 章,因为它很好地概述了事物的工作原理。第 3 章有更多关于具体设计的信息。然后,我只是跳来跳去使用搜索功能来阅读界面和 objects。

了解 OpenSL ES

一旦您理解了 OpenSL 工作原理的基本原理,它似乎就很简单了。有媒体 objects(播放器和记录器等)和数据源(输入)和数据接收器(输出)。您实际上将输入连接到媒体 object,媒体 object 将处理后的数据路由到其连接的输出。

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

2016 年 7 月 29 日更新

根据我的测试,似乎 BufferQueue 和 AndroidSimpleBufferQueue 都不支持 non-PCM 数据,至少在我测试过的系统上不支持(Nexus 7 @ 6.01,NVidia Shield K1 @ 6.0.1) 所以你需要先解码你的数据才能使用它。

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

  • MediaExtractor 似乎没有正确 return 加密解码所需的 UUID 信息,至少对于我测试过的文件而言是这样。 AMediaExtractor_getPsshInforeturn一个nullptr.

  • API 并不总是像 header 声明中的评论一样。例如,在 MediaExtractor 中检查 EOS(流结束)似乎是 mo通过检查 returned 的字节数而不是检查 AMediaExtractor_advance 函数的 return 值是可靠的。

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