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

时间:2016-06-29 07:17:47

标签: java android android-ndk opensl

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

1 个答案:

答案 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 Simple 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 ES

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

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

更新07/29/16

从我的测试来看,似乎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数据的缓冲区后,您可以将其传递给本机代码,从而减少延迟。