1. 程式人生 > >一步步實現windows版ijkplayer系列文章之三——Ijkplayer播放器原始碼分析之音視訊輸出——音訊篇

一步步實現windows版ijkplayer系列文章之三——Ijkplayer播放器原始碼分析之音視訊輸出——音訊篇

一步步實現windows版ijkplayer系列文章之三——Ijkplayer播放器原始碼分析之音視訊輸出——音訊篇

這篇文章的ijkplayer音訊原始碼研究我們還是選擇Android平臺,它的音訊解碼是不支援硬解的,音訊播放使用的API是OpenSL ES或AudioTrack。

OpenSL ES & AudioTrack

  • OpenSL ES

什麼是OpenSL ES?下面來自官網的說明:

OpenSL ES™ is a royalty-free, cross-platform, hardware-accelerated audio API tuned for embedded systems. It provides a standardized, high-performance, low-latency method to access audio functionality for developers of native applications on embedded mobile multimedia devices, enabling straightforward cross-platform deployment of hardware and software audio capabilities, reducing implementation effort, and promoting the market for advanced audio.

可見OpenGL ES是專門為嵌入式裝置設計的音訊API,所以不適合在PC上使用。

  • AudioTrack

AudioTrack是專門為Android應用提供的java API,顯然也不適合在PC上使用。

使用AudioTrack API來輸出音訊就需要把音訊資料從java層拷貝到native層。而OpenSL ES API是Android NDK提供的native介面,它可以在native層直接獲取和處理資料,因此為了提高效率,應該使用OpenSL ES API。通過如下java介面設定音訊輸出API:

  ijkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 0);
  

Ijkplayer使用jni4android來為AudioTrack的java API自動生成JNI native程式碼。

我們儘量選擇底層的程式碼來進行研究,因此本篇文章梳理一遍OpenSL ES API在ijkplayer中的使用。

原始碼分析

建立播放器音訊輸出物件

呼叫如下函式生成音訊輸出物件:

SDL_Aout *SDL_AoutAndroid_CreateForOpenSLES()

建立並初始化Audio Engine:


//建立
SLObjectItf slObject = NULL;
ret = slCreateEngine(&slObject, 0, NULL, 0, NULL, NULL);
CHECK_OPENSL_ERROR(ret, "%s: slCreateEngine() failed", __func__);
opaque->slObject = slObject;
//初始化
ret = (*slObject)->Realize(slObject, SL_BOOLEAN_FALSE);
CHECK_OPENSL_ERROR(ret, "%s: slObject->Realize() failed", __func__);
//獲取SLEngine介面物件slEngine
SLEngineItf slEngine = NULL;
ret = (*slObject)->GetInterface(slObject, SL_IID_ENGINE, &slEngine);
CHECK_OPENSL_ERROR(ret, "%s: slObject->GetInterface() failed", __func__);
opaque->slEngine = slEngine;

開啟音訊輸出裝置:


//使用slEngine開啟輸出裝置
SLObjectItf slOutputMixObject = NULL;
const SLInterfaceID ids1[] = {SL_IID_VOLUME};
const SLboolean req1[] = {SL_BOOLEAN_FALSE};
ret = (*slEngine)->CreateOutputMix(slEngine, &slOutputMixObject, 1, ids1, req1);
CHECK_OPENSL_ERROR(ret, "%s: slEngine->CreateOutputMix() failed", __func__);
opaque->slOutputMixObject = slOutputMixObject;
//初始化
ret = (*slOutputMixObject)->Realize(slOutputMixObject, SL_BOOLEAN_FALSE);
CHECK_OPENSL_ERROR(ret, "%s: slOutputMixObject->Realize() failed", __func__);

將上述建立的OpenSL ES相關物件儲存到SDL_Aout_Opaque中。

設定播放器音訊輸出物件的回撥函式:

aout->free_l       = aout_free_l;
aout->opaque_class = &g_opensles_class;
aout->open_audio   = aout_open_audio;
aout->pause_audio  = aout_pause_audio;
aout->flush_audio  = aout_flush_audio;
aout->close_audio  = aout_close_audio;
aout->set_volume   = aout_set_volume;
aout->func_get_latency_seconds = aout_get_latency_seconds;

配置並建立音訊播放器

通過如下函式進行:

static int aout_open_audio(SDL_Aout *aout, const SDL_AudioSpec *desired, SDL_AudioSpec *obtained)

  • 配置資料來源

    SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {
    SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
    OPENSLES_BUFFERS
    };

    SLDataFormat_PCM *format_pcm = &opaque->format_pcm;
    format_pcm->formatType = SL_DATAFORMAT_PCM;
    format_pcm->numChannels = desired->channels;
    format_pcm->samplesPerSec = desired->freq * 1000; // milli Hz

    format_pcm->bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;
    format_pcm->containerSize = SL_PCMSAMPLEFORMAT_FIXED_16;
    switch (desired->channels) {

    case 2:
        format_pcm->channelMask  = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
        break;
    case 1:
        format_pcm->channelMask  = SL_SPEAKER_FRONT_CENTER;
    break;
    default:
    ALOGE("%s, invalid channel %d", __func__, desired->channels);
    goto fail;

    }
    format_pcm->endianness = SL_BYTEORDER_LITTLEENDIAN;

    SLDataSource audio_source = {&loc_bufq, format_pcm};

  • 配置資料管道

    SLDataLocator_OutputMix loc_outmix = {
    SL_DATALOCATOR_OUTPUTMIX,
    opaque->slOutputMixObject
    };
    SLDataSink audio_sink = {&loc_outmix, NULL};

  • 其它引數

    const SLInterfaceID ids2[] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_VOLUME, SL_IID_PLAY };
    static const SLboolean req2[] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE };

  • 建立播放器

    ret = (*slEngine)->CreateAudioPlayer(slEngine, &slPlayerObject, &audio_source,

                        &audio_sink, sizeof(ids2) / sizeof(*ids2),
                        ids2, req2);
    
  • 獲取相關介面

       
       //獲取seek和play介面
       ret = (*slPlayerObject)->GetInterface(slPlayerObject, SL_IID_PLAY, &opaque->slPlayItf);
       CHECK_OPENSL_ERROR(ret, "%s: slPlayerObject->GetInterface(SL_IID_PLAY) failed", __func__);
       //音量調節介面
       ret = (*slPlayerObject)->GetInterface(slPlayerObject, SL_IID_VOLUME, &opaque->slVolumeItf);
       CHECK_OPENSL_ERROR(ret, "%s: slPlayerObject->GetInterface(SL_IID_VOLUME) failed", __func__);
       //獲取音訊輸出的BufferQueue介面
       ret = (*slPlayerObject)->GetInterface(slPlayerObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &opaque->slBufferQueueItf);
       CHECK_OPENSL_ERROR(ret, "%s: slPlayerObject->GetInterface(SL_IID_ANDROIDSIMPLEBUFFERQUEUE) failed", __func__);      
    
  • 設定回撥函式

回撥函式並不傳遞音訊資料,它只是告訴程式:我已經準備好接受處理(播放)資料了。這時候就可以呼叫Enqueue向BufferQueue中插入音訊資料了。

    ret = (*opaque->slBufferQueueItf)->RegisterCallback(opaque->slBufferQueueItf, aout_opensles_callback, (void*)aout);
    CHECK_OPENSL_ERROR(ret, "%s: slBufferQueueItf->RegisterCallback() failed", __func__);    
  • 初始化其它引數

       opaque->bytes_per_frame   = format_pcm->numChannels * format_pcm->bitsPerSample / 8;//每一幀的bytes數,此處將一個取樣點作為一幀
       opaque->milli_per_buffer  = OPENSLES_BUFLEN;//一個buffer中的音訊時長,單位為milliseconds
       opaque->frames_per_buffer = opaque->milli_per_buffer * format_pcm->samplesPerSec / 1000000; // samplesPerSec is in milli,一個buffer中的音訊時長*每秒的樣本(幀)數,得到每個音訊buffer中的幀數
       opaque->bytes_per_buffer  = opaque->bytes_per_frame * opaque->frames_per_buffer;//最後求出每個buffer中含有的byte數目。
       opaque->buffer_capacity   = OPENSLES_BUFFERS * opaque->bytes_per_buffer;      
        

    此回撥函式每執行一次Dequeue會被執行一次。

音訊資料的處理

音訊資料的處理為典型的生產者消費者模型,解碼執行緒解碼出音訊資料插入到佇列中,音訊驅動程式取出資料將聲音播放出來。

audio_thread函式為音訊解碼執行緒主函式:

static int audio_thread(void *arg){

 do {
    ffp_audio_statistic_l(ffp);
    if ((got_frame = decoder_decode_frame(ffp, &is->auddec, frame, NULL)) < 0)//從PacketQueue中取出pakcet並進行解碼,生成一幀資料
    ...
    if (!(af = frame_queue_peek_writable(&is->sampq)))
        goto the_end;

    af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
    af->pos = frame->pkt_pos;
    af->serial = is->auddec.pkt_serial;
    af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});

    av_frame_move_ref(af->frame, frame);
    frame_queue_push(&is->sampq);//將幀資料插入幀佇列 FrameQueue

}

aout_thread_n 為音訊輸出執行緒主函式:

static int aout_thread_n(SDL_Aout *aout){
...
    SDL_LockMutex(opaque->wakeup_mutex);
    //如果沒有退出播放&&(當前播放器狀態為暫停||插入音訊BufferQueue中的資料條數大於OPENSLES_BUFFERS)
    if (!opaque->abort_request && (opaque->pause_on || slState.count >= OPENSLES_BUFFERS)) {
    //不知道為什麼if下面又加了一層while??
        while (!opaque->abort_request && (opaque->pause_on || slState.count >= OPENSLES_BUFFERS)) {
            //如果此時為非暫停狀態,將播放器狀態置為PLAYING
            if (!opaque->pause_on) {
                (*slPlayItf)->SetPlayState(slPlayItf, SL_PLAYSTATE_PLAYING);
            }
            //如果暫停或者佇列中資料過多,這裡都會等待一個條件變數,並將過期時間置為1秒,應該是防止BufferQueue中的資料不再快速增加
            SDL_CondWaitTimeout(opaque->wakeup_cond, opaque->wakeup_mutex, 1000);
            SLresult slRet = (*slBufferQueueItf)->GetState(slBufferQueueItf, &slState);
            if (slRet != SL_RESULT_SUCCESS) {
                ALOGE("%s: slBufferQueueItf->GetState() failed\n", __func__);
                SDL_UnlockMutex(opaque->wakeup_mutex);
            }
            //暫停播放
            if (opaque->pause_on)
                (*slPlayItf)->SetPlayState(slPlayItf, SL_PLAYSTATE_PAUSED);
        }
        //恢復播放
        if (!opaque->abort_request && !opaque->pause_on) {
            (*slPlayItf)->SetPlayState(slPlayItf, SL_PLAYSTATE_PLAYING);
        }
    }
    ...
    next_buffer = opaque->buffer + next_buffer_index * bytes_per_buffer;
    next_buffer_index = (next_buffer_index + 1) % OPENSLES_BUFFERS;
    //呼叫回撥函式生成插入到BufferQueue中的資料
    audio_cblk(userdata, next_buffer, bytes_per_buffer);
    //如果需要重新整理BufferQueue資料,則清除資料,何時需要清理資料??解釋在下面
    if (opaque->need_flush) {
        (*slBufferQueueItf)->Clear(slBufferQueueItf);
        opaque->need_flush = false;
    }
    //不知道為什麼會判斷兩次??

    if (opaque->need_flush) {
        ALOGE("flush");
        opaque->need_flush = 0;
        (*slBufferQueueItf)->Clear(slBufferQueueItf);
    } else {
    //最終將資料插入到BufferQueue中。
    slRet = (*slBufferQueueItf)->Enqueue(slBufferQueueItf, next_buffer, bytes_per_buffer);
    ...
}

以下是為條件變數opaque->wakeup_cond 傳送signal的幾個函式,目的是讓輸出執行緒快速響應

  • static void aout_opensles_callback(SLAndroidSimpleBufferQueueItf caller, void *pContext)
  • static void aout_close_audio(SDL_Aout *aout)
  • static void aout_pause_audio(SDL_Aout *aout, int pause_on)
  • static void aout_flush_audio(SDL_Aout *aout)
  • static void aout_set_volume(SDL_Aout *aout, float left_volume, float right_volume)
  • 第一個為音訊播放器的BufferQueue設定的回撥函式,每從佇列中取出一條資料執行一次,這個可以理解,佇列中去除一條資料,立刻喚醒執行緒Enqueue資料。
  • 第二個為關閉音訊播放器的時候呼叫的函式,立馬退出執行緒。
  • 第三個為暫停/播放音訊播放器函式,馬上設定播放器狀態。
  • 第四個為清空BufferQueue時呼叫的函式,立刻喚醒執行緒Enqueue資料。
  • 第五個為設定音量函式,馬上設定音量。

通過呼叫如下函式生成插入到BufferQueue中的資料 :

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len){
       ...
        if (is->audio_buf_index >= is->audio_buf_size) {
        //如果buffer中沒有資料了,生成新資料。
               audio_size = audio_decode_frame(ffp);
       ...     
       
        if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
            //直接拷貝到stream
            memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
        else {
            memset(stream, 0, len1);
            if (!is->muted && is->audio_buf)
            //進行音量調整和混音
                SDL_MixAudio(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1, is->audio_volume);
    }

}

生成新資料的函式不是對音訊資料進行解碼,而是對幀資料進行了二次處理,對音訊進行了必要的重取樣或者變速變調。


static int audio_decode_frame(FFPlayer *ffp){
    ...
    //重取樣
    len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);
    ...
    //音訊變速變調
    int ret_len = ijk_soundtouch_translate(is->handle, is->audio_new_buf, (float)(ffp->pf_playback_rate), (float)(1.0f/ffp->pf_playback_rate),
                resampled_data_size / 2, bytes_per_sample, is->audio_tgt.channels, af->frame->sample_rate);
    ...
    //最後將資料儲存到audio_buf中
     is->audio_buf = (uint8_t*)is->audio_new_buf;
    ...
}

最後一個比較讓人困惑的問題是何時才會清理BufferQueue,看一下清理的命令是在何時發出的:

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
    ...
         if (is->auddec.pkt_serial != is->audioq.serial) {
        is->audio_buf_index = is->audio_buf_size;
        memset(stream, 0, len);
        // stream += len;
        // len = 0;
        SDL_AoutFlushAudio(ffp->aout);
        break;
    }
    ...
}

它是在音訊輸出執行緒中獲取即將插入到BufferQueue的音訊資料,呼叫回撥函式時發出的,發出的條件如上所示,其中pkt_serial 為從PacketQueue佇列中取出的需要解碼的packet的serial,serial為當前PacketQueue佇列的serial。也就是說,如果兩者不等,就需要清理BufferQueue。這裡的serial是要保證前後資料包的連續性,例如發生了Seek,資料不連續,就需要清理舊資料。

注:在播放器中的VideoState成員中,audioq和解碼成員auddec中的queue是同一個佇列。


decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);

結束語

筆者從頭到尾把和音訊輸出相關的自認為重要的原始碼做了一些解釋和記錄,有些細節沒有去深入研究。以後有時間慢慢學習。

參考

音訊的相關知識

AAC 到 PCM 音訊解碼

SoundTouch實現音訊變速變調

Android音訊開發之OpenSL ES

淺聊OpenSL ES音訊開發

ffplay packet queue分析