简体   繁体   English

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

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

I have a layout that has about 60 buttons and each one, when pressed, plays a different audio file. 我的布局有大约60个按钮,每个按钮在按下时都会播放不同的音频文件。 I have all my audio files as mp3s in my assets folder and to play them I'm basically using the same code as is used in the Google NDK samples "native-audio" project: https://github.com/googlesamples/android-ndk 我将所有音频文件都保存为资产文件夹中的mp3,要播放它们,我基本上使用与Google NDK示例“本地音频”项目中使用的相同的代码: https : //github.com/googlesamples/android -ndk

I have 10 identical native functions (just with uniquely named variables) that work like this.. 我有10个相同的本机函数(仅带有唯一命名的变量)就可以像这样工作。

function to play sound: 播放声音的功能:

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;
}

function to stop that sound: 停止声音的功能:

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;
    }
}

this is easy to deal with, but I want to minimize latency and avoid having to do (*engineEngine)->CreateAudioPlayer() every time I want to play a different file. 这很容易处理,但是我想最小化延迟并避免每次我想播放不同文件时都必须做(*engineEngine)->CreateAudioPlayer() 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? 有什么方法可以只更改音频播放器使用的audioSrc,而不必每次都从头开始销毁它并重新创建它?

As a bonus, where can I read more about this stuff? 作为奖励,我在哪里可以阅读有关该材料的更多信息? Seems pretty difficult to find any information on OpenSL ES anywhere. 似乎很难在任何地方找到有关OpenSL ES的任何信息。

We're in the same boat, I'm currently familiarizing myself too with the NDK and OpenSL ES. 我们在同一条船上,我目前也在使自己熟悉NDK和OpenSL ES。 My answer is based on my experience entirely consisting of ~2 days of experimentation so there might be better approaches but the information might help you on your way. 我的答案是基于我的经验,该经验完全由大约2天的实验组成,因此可能会有更好的方法,但是这些信息可能会对您有所帮助。

I have 10 identical native functions (just with uniquely named variables) that work like this.. 我有10个相同的本机函数(仅带有唯一命名的变量)就可以像这样工作。

If I understood your case correctly, you don't need to have duplicate functions for this. 如果我正确理解了您的情况,则无需为此重复功能。 The only thing which differs in these calls is the button pressed and ultimately the sound to play and this can be passed as parameters through the JNI call. 这些调用中唯一不同的是按下的按钮以及最终播放的声音,并且可以将其作为参数通过JNI调用进行传递。 You can store the created player and data in a globally accessible structure so you can retrieve it when you need to stop/replay it, maybe using the buttonId as a key to a map. 您可以将创建的播放器和数据存储在全局可访问的结构中,以便在需要停止/重放它时可以将其检索,也许可以使用buttonId作为地图的键。

[..]but I want to minimize latency and avoid having to do (*engineEngine)->CreateAudioPlayer() every time I want to play a different file. [..]但是我想最小化延迟并避免每次我想播放不同文件时都必须做(* engineEngine)-> CreateAudioPlayer()。 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? 有什么方法可以只更改音频播放器使用的audioSrc,而不必每次都从头开始销毁它并重新创建它?

Yes, constantly creating and destroying players is costly and can lead to fragmentation of the heap (as stated in the OpenSL ES 1.0 Specification). 是的,不断创建和销毁播放器成本很高,并且可能导致堆碎片化(如OpenSL ES 1.0规范中所述)。 First, I thought he DynamicSourceItf would allow you to switch data sources but it seems that this interface is not intended to be used like that, at least on Android 6 this returns 'feature unsupported'. 首先,我以为他可以使用DynamicSourceItf切换数据源,但似乎该接口不打算那样使用,至少在Android 6上,此接口返回“功能不受支持”。

I doubt that creating a player for each unique sound would be a good solution especially since playing the same sound multiple times on top of each other (as it's common in a game for example) would require an arbitrary amount of additional players for that same sound. 我怀疑为每种独特的声音创建一个播放器是否会是一个好的解决方案,尤其是因为多次反复播放相同的声音(例如在游戏中很常见)会需要任意数量的其他播放器来播放相同的声音。

Buffer Queues 缓冲区队列

BufferQueues are queues of individual buffers which a player will process when playing. BufferQueues是玩家在播放时要处理的各个缓冲区的队列。 When all the buffers have been processed, the player 'stops' (it's official state is still 'playing' though) but will resume as soon as new buffers are being enqueued. 处理完所有缓冲区后,播放器会“停止”(尽管它的官方状态仍在“播放”中),但是一旦新缓冲区入队,它将立即恢复。

What this allows you to do is to create as many players as overlapping sounds you require. 这允许您做的是根据需要的重叠声音创建尽可能多的播放器。 When you want to play a sound, you iterate over these players until you've found one which is not currently processing buffers ( BufferQueueItf->GetState(...) provides this information or a callback can be registered so you can tag players as being 'free'). 当您想播放声音时,请遍历这些播放器,直到找到一个当前不正在处理缓冲区的播放器为止( BufferQueueItf->GetState(...)提供了此信息,或者可以注册一个回调,以便可以将播放器标记为是“免费的”)。 Then, you enqueue as many buffers as your sound needs which will start playing immediately. 然后,根据您的声音需要加入尽可能多的缓冲区,这些缓冲区将立即开始播放。

The format of a BufferQueue is, as far as I know, locked at creation. 据我所知,BufferQueue的格式在创建时就被锁定了。 So you have to make sure that you either have all your input buffers in the same format or you create different BufferQueue (and players) for each format. 因此,您必须确保所有输入缓冲区都具有相同的格式,或者为每种格式创建不同的BufferQueue(和播放器)。

Android Simple BufferQueue Android简单BufferQueue

According to the Android NDK documentation, the BufferQueue interface is expected to have significant changes in the future. 根据Android NDK文档,预计BufferQueue接口将来会发生重大变化。 They have extracted a simplified interface with most of BufferQueue's functionality and called it AndroidSimpleBufferQueue. 他们提取了具有大多数BufferQueue功能的简化接口,并将其称为AndroidSimpleBufferQueue。 This interface is not expected to change and thus makes your code more future proof. 预计该接口不会更改,因此可以使您的代码更富未来性。

The main functionality you loose by using the AndroidSimpleBufferQueue is to be able to use non-PCM source data, so you'd have to decode your files before use. 通过使用AndroidSimpleBufferQueue释放的主要功能是能够使用非PCM源数据,因此您必须在使用之前解码文件。 This can be done in OpenSL ES using a AndroidSimpleBufferQueue as a sink. 这可以在OpenSL ES中使用AndroidSimpleBufferQueue作为接收器来完成。 More recent APIs have additional support using the MediaCodec and it's NDK implementation NDKMedia (checkout the native-codec example). 最新的API使用MediaCodec及其NDK实现NDKMedia(请参见本机编解码器示例)具有更多支持。

Resources 资源

The NDK documentation does contain some important information which are hard to find anywhere else. NDK文档确实包含一些重要的信息,这些信息很难在其他任何地方找到。 Here's the OpenSL ES specific page. 这是 OpenSL ES的特定页面。

It might be close to 600 pages and hard to digest, but the OpenSL ES 1.0 Specification should be your primary resource of information. 它可能接近600页,难以理解,但是OpenSL ES 1.0规范应该是您的主要信息资源。 I highly recommend reading chapter 4 as it gives a good overview of how things work. 我强烈建议您阅读第4章,因为它很好地概述了工作原理。 Chapter 3 has a bit more information on the specific design. 第3章提供了有关特定设计的更多信息。 Then, I just jump around using the search function to read up on interfaces and objects as I go. 然后,我随即使用搜索功能跳来跳去阅读接口和对象。

Understanding OpenSL ES 了解OpenSL ES

Once you have understood the basic principles of how OpenSL works, it seems to be quite straightforward. 一旦您了解了OpenSL如何工作的基本原理,这似乎很简单。 There are media objects (players and recorders, etc) and data sources (inputs) and data sinks (outputs). 有媒体对象(播放器和记录器等)以及数据源(输入)和数据接收器(输出)。 You essentially connect an input to a media object which routes the processed data to its connected output. 本质上,您将输入连接到媒体对象,该对象将处理后的数据路由到其连接的输出。

Sources, Sinks and Media Objects are all documented in the specification including their interfaces. 规范中记录了源,接收器和媒体对象,包括它们的接口。 With that information, it really is just about picking the building blocks you require and plugging them together. 有了这些信息,实际上就是选择所需的构建基块并将其插入在一起。

Update 07/29/16 更新16年7月29日

From my tests, it seems as if both BufferQueue and AndroidSimpleBufferQueue do not support non-PCM data, at least not on the systems I've tested (Nexus 7 @ 6.01, NVidia Shield K1 @ 6.0.1) so you will need to decode your data before you can use it. 根据我的测试,似乎BufferQueue和AndroidSimpleBufferQueue都不支持非PCM数据,至少在我测试过的系统(Nexus 7 @ 6.01,NVidia Shield K1 @ 6.0.1)上不支持,因此您需要解码您的数据,然后才能使用。

I tried using the NDK versions of the MediaExtractor and MediaCodec but there are several caveats to watch out for: 我尝试使用MediaExtractor和MediaCodec的NDK版本,但需要注意以下几点:

  • MediaExtractor does not seem to correctly return the UUID information required for decoding with crypto, at least not for the files I've tested. MediaExtractor似乎无法正确返回使用加密进行解码所需的UUID信息,至少不是针对我测试过的文件。 AMediaExtractor_getPsshInfo returns a nullptr . AMediaExtractor_getPsshInfo返回nullptr

  • The API does not always behave as the comments in the header claim. API并不总是像标头声明中的注释一样起作用。 Checking for EOS (end of stream) in the MediaExtractor for example seems to be most reliable by checking the amount of bytes returned instead of checking the AMediaExtractor_advance function's return value. 例如,通过检查返回的字节数而不是检查AMediaExtractor_advance函数的返回值,在MediaExtractor中检查EOS(流的末尾)似乎是最可靠的。

I'd recommend staying in Java for the decoding process as these APIs are more mature, definitely more tested and you might get more functionality out of it. 我建议您继续使用Java进行解码,因为这些API更加成熟,经过了严格的测试,并且您可能会从中获得更多的功能。 Once you have the buffers of raw PCM data, you can pass it to native code which allows you to reduce latency. 一旦有了原始PCM数据的缓冲区,就可以将其传递给本机代码,从而减少延迟。

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

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