繁体   English   中英

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

[英]OpenSL ES change audio source without recreating audio player

我的布局有大约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上,此接口返回“功能不受支持”。

我怀疑为每种独特的声音创建一个播放器是否会是一个好的解决方案,尤其是因为多次反复播放相同的声音(例如在游戏中很常见)会需要任意数量的其他播放器来播放相同的声音。

缓冲区队列

BufferQueues是玩家在播放时要处理的各个缓冲区的队列。 处理完所有缓冲区后,播放器会“停止”(尽管它的官方状态仍在“播放”中),但是一旦新缓冲区入队,它将立即恢复。

这允许您做的是根据需要的重叠声音创建尽可能多的播放器。 当您想播放声音时,请遍历这些播放器,直到找到一个当前不正在处理缓冲区的播放器为止( BufferQueueItf->GetState(...)提供了此信息,或者可以注册一个回调,以便可以将播放器标记为是“免费的”)。 然后,根据您的声音需要加入尽可能多的缓冲区,这些缓冲区将立即开始播放。

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

Android简单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如何工作的基本原理,这似乎很简单。 有媒体对象(播放器和记录器等)以及数据源(输入)和数据接收器(输出)。 本质上,您将输入连接到媒体对象,该对象将处理后的数据路由到其连接的输出。

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

更新16年7月29日

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM