1. 程式人生 > >基於bsd socket傳輸libmad解碼,alsa驅動的mp3流媒體播放器

基於bsd socket傳輸libmad解碼,alsa驅動的mp3流媒體播放器

1. 前言

折騰了也有差不多一個來月,之前沒怎麼上手過linux下的C,很多東西都是想當然的狀態就開始東看看西敲敲了。這篇本章意在讓一個小白學會做一個流媒體播放器。我最開始就是個小白。

2. 準備工作

2.1 系統環境

系統環境: ubuntu 12.04

編輯器: code::blocks

2.2 相關庫

alsa-lib: 使用者空間的音效卡介面(不知道這麼描述對不對,其實ubuntu裡alsa音效卡驅動已經有了,這個庫則是提供一個可使用的API)。下載地址: alsa-lib-1.0.25.tar.bz2

關於libmad和alsa的詳細介紹建議搜搜看下。

2.2 庫的安裝

一般步驟

解壓 - 進入解壓目錄,然後

./configure
make
sudo make instal

在make libmad的時候,由於gcc版本問題,可能編譯不過,開啟跟目錄下的Makefile去掉裡面出現的"-fforce-mem"即可解決。

3. 總體設計

3.1 概述

大致思想有接觸過socket程式設計應該很明瞭,問題在於如何邊接收mp3資料邊播放。

有興趣可以看一下下面幾篇文章,我也是看了這幾篇文章才知道怎麼做的。

關於文中提到的minimad.c檔案在編譯完的libmad庫裡面就有,看懂這個程式對之後工作的開展有很大益處。

3.2 需求分析

這個程式是別人的課程設計,我拿來練練手。

主要需求有以下幾個點:

1. Mp3檔案以幀為單位進行傳送,資料包格式可參照RTP。

2. 可手動建立多條socket進行傳輸,socket的傳輸率從10%~100%可自動以。eg. 傳輸率為80%的socket,每10個包中傳送8個包,另2個包丟棄。

3. 接收端有一個適當大小的快取,講包按順序排滿後即可播放,缺失的包需要打印出來。eg. 快取[1,2,3,5,6]已排滿,進行播放,缺少序號為4的包,列印提示。

4. 可雙向播放。

3.3 核心問題

1. Mp3檔案的分割,打包以及傳輸。

2. BSD socket的建立和傳輸。

3. 資料包的重組及播放。

4. 合理的建立、刪除執行緒。

5. 執行緒同步

我這篇文章想寫的是1和3,其他的打算另外寫文章來自我總結。

3.3 問題分析

3.3.1 MP3的檔案格式

想要分割和重組MP3自然需要了解其檔案格式

然後再畫個圖吧。


ID3V1就個人自動忽略了。

關於ID3V2,虛線之間的部分為一個標籤幀,一個IDV3V2可以有多個標籤幀。標籤幀記錄著專輯名稱,專輯封面,歌手資訊等等。

3.3.2 ID3V2頭的解析。

實際上我並不需要解析出所有資料,我只想將ID3V2的資訊跳過,知道哪裡是MP3幀資料(也就是聲音資料主體內容)的開始,然後分隔出每個幀進行打包就好了。

當然以學習的心態還是寫了簡單的解析程式碼,標籤幀就沒有解析了,利用Size[4]記錄的標籤大小直接跳過。

關於標籤大小的計算,如果看了3.3.1連結裡的那篇文章應該就會明白了。這裡將公式再寫一下。

total_size =    (Size[0]&0x7F)*0x200000 + (Size[1]&0x7F)*0x400 + (Size[2]&0x7F)*0x80 + (Size[3]&0x7F)

將每一位的最高位捨棄,只取低七位,然後帶入公式進行計算。

3.3.2 Frame Header的解析

這個部分就按照結構體頭部的格式細心的進行位操作即可。剛開始寫的時候自己給自己開了不少玩笑,一個位元組的高位低位愣是搞了半天才搞清楚。

解析完頭部,一幀的大小就可以比對著下表進行計算。

表1 MP3 幀頭位元組說明表

名稱 位長
同步資訊 11 12位元組 所有位均為1,第1位元組恆為FF
版本 2 00-MPEG 2.5   01-未定義     10-MPEG 2     11-MPEG 1
2 00-未定義      01-Layer 3     10-Layer 2      11-Layer 1
CRC校驗 1 0-校驗        1-不校驗
位率 4 3位元組 取樣率,單位是kbps,例如採用MPEG-1 Layer 364kbps是,值為0101
bits V1,L1 V1,L2 V1,L3 V2,L1 V2,L2 V2,L3
0000 free free free free free free
0001 32 32 32 32(32) 32(8) 8 (8)
0010 64 48 40 64(48) 48(16) 16 (16)
0011 96 56 48 96(56) 56(24) 24 (24)
0100 128 64 56 128(64) 64(32) 32 (32)
0101 160 80 64 160(80) 80(40) 64 (40)
0110 192 96 80 192(96) 96(48) 80 (48)
0111 224 112 96 224(112) 112(56) 56 (56)
1000 256 128 112 256(128) 128(64) 64 (64)
1001 288 160 128 288(144) 160(80) 128 (80)
1010 320 192 160 320(160) 192(96) 160 (96)
1011 352 224 192 352(176) 224(112) 112 (112)
1100 384 256 224 384(192) 256(128) 128 (128)
1101 416 320 256 416(224) 320(144) 256 (144)
1110 448 384 320 448(256) 384(160) 320 (160)
1111 bad bad bad bad bad bad
V1 - MPEG 1    V2 - MPEG 2 and MPEG 2.5
L1 - Layer 1     L2 - Layer 2     L3 - Layer 3
"free" 
表示位率可變    "bad"  表示不允許值
取樣頻率 2 取樣頻率,對於MPEG-1  00-44.1kHz    01-48kHz    10-32kHz      11-未定義 對於MPEG-2  00-22.05kHz   01-24kHz    10-16kHz      11-未定義 對於MPEG-2.5 00-11.025kHz 01-12kHz    10-8kHz       11-未定義
幀長調節 1 用來調整檔案頭長度,0-無需調整,1-調整,具體調整計算方法見下文。
保留字 1 沒有使用。
聲道模式 2 4位元組 表示聲道, 00-立體聲Stereo    01-Joint Stereo    10-雙聲道        11-單聲道
擴充模式 2 當聲道模式為01是才使用。
Value 強度立體聲 MS立體聲
00 off off
01 on off
10 off on
11 on on
版權 1 檔案是否合法,0-不合法   1-合法
原版標誌 1 是否原版,    0-非原版   1-原版
強調方式 2 用於聲音經降噪壓縮後再補償的分類,很少用到,今後也可能不會用。 00-未定義     01-50/15ms     10-保留       11-CCITT J.17

MP3幀長取決於位率和頻率,計算公式為:. 

mpeg1.0

layer1:幀長= (48000*bitrate)/sampling_freq + padding

layer2&3: 幀長= (144000*bitrate)/sampling_freq + padding

mpeg2.0

layer1 : 幀長= (24000*bitrate)/sampling_freq + padding

layer2&3 : 幀長= (72000*bitrate)/sampling_freq + padding

3.3.4 資料幀的打包,傳輸以及重組

這裡的工作其實想法很簡單。就是將每一幀的資料截取出來後,在頭部加上MP3的編碼(在程式的初始化時我會預先載入Mp3檔案然後賦予一個唯一的編碼),序號,時間戳和幀的長度作為一個包。接收方讀取完一個包之後就根據序號對包進行排序。當快取滿了之後,將幀資料從每一個包裡提取出來拼接成一個位元組流,然後進行解碼,播放。

4 關鍵程式碼說明

其實如果是我看別人寫的文章的話,前面可能就晃一眼,直接看這裡了。前面的部分其實都能想到。恩,廢話說太多了。直接程式碼吧。

4.1 libmad 如何解碼

首先來看官方的例子: minimad.c

# include <stdio.h>
# include <unistd.h>
# include <sys/stat.h>
# include <sys/mman.h>

# include "mad.h"

/*
 * This is perhaps the simplest example use of the MAD high-level API.
 * Standard input is mapped into memory via mmap(), then the high-level API
 * is invoked with three callbacks: input, output, and error. The output
 * callback converts MAD's high-resolution PCM samples to 16 bits, then
 * writes them to standard output in little-endian, stereo-interleaved
 * format.
 */

static int decode(unsigned char const *, unsigned long);

int main(int argc, char *argv[])
{
  struct stat stat;
  void *fdm;

  if (argc != 1)
    return 1;

  if (fstat(STDIN_FILENO, &stat) == -1 ||
      stat.st_size == 0)
    return 2;

  fdm = mmap(0, stat.st_size, PROT_READ, MAP_SHARED, STDIN_FILENO, 0);
  if (fdm == MAP_FAILED)
    return 3;

  decode(fdm, stat.st_size);

  if (munmap(fdm, stat.st_size) == -1)
    return 4;

  return 0;
}

首先看main方法裡面,傳入的第一個引數無疑是檔案的路徑。fstat和mmap兩個函式在一起做的事情簡單來說就是獲取檔案資訊,然後將檔案資料對映到記憶體中。這和用open代開一個檔案,然後read檔案中的資料到一個數組中是一樣的效果,decode後的兩個引數也可以寫成陣列的首地址和長度。

接下來再看decode函式做了什麼。

/*
 * This is a private message structure. A generic pointer to this structure
 * is passed to each of the callback functions. Put here any data you need
 * to access from within the callbacks.
 */


struct buffer {
  unsigned char const *start;
  unsigned long length;
};
/*
 * This is the function called by main() above to perform all the decoding.
 * It instantiates a decoder object and configures it with the input,
 * output, and error callback functions above. A single call to
 * mad_decoder_run() continues until a callback function returns
 * MAD_FLOW_STOP (to stop decoding) or MAD_FLOW_BREAK (to stop decoding and
 * signal an error).
 */<pre name="code" class="cpp">static
int decode(unsigned char const *start, unsigned long length)
{
  struct buffer buffer;
  struct mad_decoder decoder;
  int result;
  /* initialize our private message structure */
  buffer.start  = start;
  buffer.length = length;
  /* configure input, output, and error functions */
  mad_decoder_init(&decoder, &buffer,
		   input, 0 /* header */, 0 /* filter */, output,
		   error, 0 /* message */);
  /* start decoding */
  result = mad_decoder_run(&decoder, MAD_DECODER_MODE_SYNC);
  /* release the decoder */
  mad_decoder_finish(&decoder);
  return result;
}

首先,buffer是一個自定義的結構體,記錄需要解碼的陣列的起始位置和長度。可以看到buffer物件是引用傳遞的方式傳入到mad_decoder_init方法中的,為什麼特地說下這個?因為當我們進行流媒體播放時,每次載入的都是一部分資料,所以需要不停的解碼同一個快取裡存放的資料。當快取裡的資料解碼完畢,新的資料寫進來時,我們無須再次呼叫mad_docoder_init的方法進行初始化,而是更改buffer的length的值,(在後面會講到,buffer.length的值將被置為0來表示資料已解析完畢)再呼叫mad_decoder_run方法來繼續進行解碼。mad_decoder_init 同時還為decorder綁定了input,output,和error函式。

/*
 * This is the input callback. The purpose of this callback is to (re)fill
 * the stream buffer which is to be decoded. In this example, an entire file
 * has been mapped into memory, so we just call mad_stream_buffer() with the
 * address and length of the mapping. When this callback is called a second
 * time, we are finished decoding.
 */

static
enum mad_flow input(void *data,
		    struct mad_stream *stream)
{
  struct buffer *buffer = data;

  if (!buffer->length)
    return MAD_FLOW_STOP;

  mad_stream_buffer(stream, buffer->start, buffer->length);

  buffer->length = 0;

  return MAD_FLOW_CONTINUE;
}

/*
 * The following utility routine performs simple rounding, clipping, and
 * scaling of MAD's high-resolution samples down to 16 bits. It does not
 * perform any dithering or noise shaping, which would be recommended to
 * obtain any exceptional audio quality. It is therefore not recommended to
 * use this routine if high-quality output is desired.
 */

static inline
signed int scale(mad_fixed_t sample)
{
  /* round */
  sample += (1L << (MAD_F_FRACBITS - 16));

  /* clip */
  if (sample >= MAD_F_ONE)
    sample = MAD_F_ONE - 1;
  else if (sample < -MAD_F_ONE)
    sample = -MAD_F_ONE;

  /* quantize */
  return sample >> (MAD_F_FRACBITS + 1 - 16);
}

/*
 * This is the output callback function. It is called after each frame of
 * MPEG audio data has been completely decoded. The purpose of this callback
 * is to output (or play) the decoded PCM audio.
 */

static
enum mad_flow output(void *data,
		     struct mad_header const *header,
		     struct mad_pcm *pcm)
{
  unsigned int nchannels, nsamples;
  mad_fixed_t const *left_ch, *right_ch;

  /* pcm->samplerate contains the sampling frequency */

  nchannels = pcm->channels;
  nsamples  = pcm->length;
  left_ch   = pcm->samples[0];
  right_ch  = pcm->samples[1];

  while (nsamples--) {
    signed int sample;

    /* output sample(s) in 16-bit signed little-endian PCM */

    sample = scale(*left_ch++);
    putchar((sample >> 0) & 0xff);
    putchar((sample >> 8) & 0xff);

    if (nchannels == 2) {
      sample = scale(*right_ch++);
      putchar((sample >> 0) & 0xff);
      putchar((sample >> 8) & 0xff);
    }
  }

  return MAD_FLOW_CONTINUE;
}

/*
 * This is the error callback function. It is called whenever a decoding
 * error occurs. The error is indicated by stream->error; the list of
 * possible MAD_ERROR_* errors can be found in the mad.h (or stream.h)
 * header file.
 */

static
enum mad_flow error(void *data,
		    struct mad_stream *stream,
		    struct mad_frame *frame)
{
  struct buffer *buffer = data;

  fprintf(stderr, "decoding error 0x%04x (%s) at byte offset %u\n",
	  stream->error, mad_stream_errorstr(stream),
	  stream->this_frame - buffer->start);

  /* return MAD_FLOW_BREAK here to stop decoding (and propagate an error) */

  return MAD_FLOW_CONTINUE;
}

scale方法我不懂,事實上後來再自己寫程式的時候也把這個函式刪除掉了。

看到input函式,很直觀的,mad_stream_buffer將資料寫入解碼工作流水線,然後將buffer.length置為0(去掉這句在流處理時會出錯,我猜測在其內部實現時應該還會用到這個結構體)。然後就是output函式。我們知道,一般的音訊資料可分為兩個左右聲道,所以會有sample[2]兩組資料。最後解碼出的資料叫做PCM,pcm是音效卡可以直接識別並播放的資料。

4.2 PCM資料如何播放

minimad中的方法僅僅只是將pcm資料列印在了控制檯。如果你在網上也搜尋過類似的文章,也許會看到有人在這裡將pcm資料寫到了/dev/dsp裝置中,我的UBUNTU沒有這個裝置,所以用到的詩alsa,我的output方法詩這麼寫的

enum mad_flow Mp3Player::output(void *data, struct mad_header const *header, struct mad_pcm *pcm)
{
    unsigned int nchannels, nsamples;
    if(!playback_handle) //alsa裝置未開啟
    {
        if(setupSndMixer() < 0)
        {
            fprintf(stdout, "setup Snd Mixer error");
            return MAD_FLOW_STOP;
        }

        if(open_device(header) < 0)
        {
            fprintf(stdout, "open device error");
            return MAD_FLOW_STOP;
        }
    }
    /* pcm->samplerate contains the sampling frequency */

    nchannels = pcm->channels;
    nsamples  = pcm->length;
    int j=0;
    unsigned char* buf = new unsigned char[nsamples * 4 * nchannels];
    unsigned char* OutputPtr = buf;
    unsigned int samples = nsamples;
    while (nsamples--) {
        signed int sample;
        /* output sample(s) in 32-bit signed little-endian PCM */
        sample = pcm->samples[0][j];
        *(OutputPtr++) = sample & 0xff;
        *(OutputPtr++) = (sample >> 8) & 0xff;
        *(OutputPtr++) = (sample >> 16) & 0xff;
        *(OutputPtr++) = (sample >> 24) & 0xff;
        if (nchannels == 2) {
            sample = pcm->samples[1][j];
            *(OutputPtr++) = sample & 0xff;
            *(OutputPtr++) = (sample >> 8) & 0xff;
            *(OutputPtr++) = (sample >> 16) & 0xff;
            *(OutputPtr++) = (sample >> 24) & 0xff;
        }
        j++;
    }
    //printf("writing data from data to dev\r\n");
    int err;
    if ((err = snd_pcm_writei (playback_handle, buf, samples)) < 0)
    {
        err = xrun_recovery(playback_handle, err);
        if (err < 0) {
            printf("Write error: %s\n", snd_strerror(err));
            return MAD_FLOW_CONTINUE;
        }
    }
    //printf("finished! release resource\r\n");
    delete[] buf;
    return MAD_FLOW_CONTINUE;
}

playback_handle是音效卡的控制控制代碼(個人理解),open_device做的工作一些播放參數進行初始化設定,傳入的header是libmad的mad_header物件,包含了幀的取樣率等資訊,這些在初始化alsa驅動的時候是必不可少的。之後我們定義一個臨時快取,將兩個通道的資料寫入同一個變數中,通過snd_pcm_writei方法寫入裝置進行播放。

對於open_device, 具體是這樣實現的。

int Mp3Player::open_device(struct mad_header const *header)
{
    int err;
    snd_pcm_hw_params_t *hw_params;
    char  *pcm_name = "default";
    unsigned int rate = header->samplerate;
    int channels = 2;
    if (header->mode == 0) {
      channels = 1;
    } else {
      channels = 2;
    }

    if ((err = snd_pcm_open (&playback_handle,
                            pcm_name, SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
      printf("cannot open audio device %s (%s)\n",
      pcm_name,
      snd_strerror (err));
      return -1;
    }

    if ((err = snd_pcm_hw_params_malloc (&hw_params)) < 0) {
      printf("cannot allocate hardware parameter structure (%s)\n",
      snd_strerror (err));
      return -1;
    }

    if ((err = snd_pcm_hw_params_any (playback_handle, hw_params)) < 0) {
      printf("cannot initialize hardware parameter structure (%s)\n",
      snd_strerror (err));
      return -1;
    }


    if ((err = snd_pcm_hw_params_set_access (playback_handle, hw_params,
              SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
      printf("cannot set access type (%s)\n",
      snd_strerror (err));
      return -1;
    }


    if ((err = snd_pcm_hw_params_set_format (playback_handle,
              hw_params, SND_PCM_FORMAT_S32_LE)) < 0) {
      printf("cannot set sample format (%s)\n",
      snd_strerror (err));
      return -1;
    }
    if ((err = snd_pcm_hw_params_set_rate_near (playback_handle,
              hw_params, &rate, 0)) < 0) {
      printf("cannot set sample rate (%s)\n",
      snd_strerror (err));
      return -1;
    }

    if ((err = snd_pcm_hw_params_set_channels (playback_handle,
              hw_params, channels)) < 0) {
      printf("cannot set channel count (%s)\n",
      snd_strerror (err));
      return -1;
    }

    if ((err = snd_pcm_hw_params (playback_handle,
              hw_params)) < 0) {
      printf("cannot set parameters (%s)\n",
      snd_strerror (err));
      return -1;
    }

    snd_pcm_hw_params_free (hw_params);
    if ((err = snd_pcm_prepare (playback_handle)) < 0) {
      printf("cannot prepare audio interface for use (%s)\n",
      snd_strerror (err));
      return -1;
    }

    return 0;
}

pcm_name設定為“default”是下放控制權的做法,也是最傻瓜式的做法,另外還有三個可選引數。(我寫程式碼的時候除了default都是找不到裝置)

大多數引數都是MP3資料幀的直接對映,SND_PCM_FORMAT_S32_LE說明採用的詩32位的取樣資訊,對應的output函式中就需要寫入32位的PCM資料。

這裡寫到的都過於簡單了,有興趣還可以多找找關於alsa的資料,不過中文資料很有限就是了。

4.3 流播放的大致工作流程

我並不確定這是不是最合理的方法,我的想法是,一個執行緒(如果是多個socket那就是多執行緒)負責往快取佇列裡寫資料,寫完之後將佇列裡的資料複製到一個數組快取中,寫完之後通知播放執行緒開始對陣列快取進行解碼播放,而寫入執行緒可以繼續往快取佇列中寫資料。

請看程式碼:

int Mp3Player::start_Player()
{
    pthread_t tid;
    int err;
    if((err = pthread_create(&tid, NULL, frame_play, NULL)) != 0 )
    {
        fprintf(stderr,"error - start_Player:pthread_create FAILED \r\n");
        return -1;
    }
    return 0;
}

void* Mp3Player::frame_play(void* arg)
{

    while(1)
    {
        sem_wait(&_ms_full);
        pthread_mutex_lock(&playBufMutex);
        _m_buffer.start = _ms_byteBuffer;
        _m_buffer.length = _ms_offset;
        mad_decoder_run(&decoder, MAD_DECODER_MODE_SYNC);
        pthread_mutex_unlock(&playBufMutex);
        sem_post(&_ms_empty);

    }
    return ((void*)arg);
}
這部分是播放執行緒,採用了執行緒同步中的訊號量和鎖機制,一共兩個訊號量,並進行如下初始化
sem_init(&_ms_empty, 0, 1);
sem_init(&_ms_full, 0, 0);
當陣列快取寫滿之後,會sem_post(_ms_full),訊號量_ms_full值加1,則播放執行緒會進一步獲取快取鎖,然後進行解碼。每次解碼前都要定義好start和length,雖然start不會變,但是每次快取的長度還是會略有差別。mad_decoder_run方法之前已經提到過,它是解碼的起點。

播放器的資料寫入入口是addFrame函式,首先我自定義個一個幀的結構體mp3_frame

struct mp3_frame
        {
            unsigned int _order;
            unsigned int _length;
            unsigned char* _data;
            ~mp3_frame()
            {
                delete[] _data;
            }
        };
序號,幀長度和資料。
int Mp3Player::addFrame(mp3_frame* f)
{
    //插入排序放出快取中
    int pos = 0;
    for(int i=0; i<=_ms_size; i++)
    {
        if(_ms_frameBuffer[i] == 0)// 空位,直接填補
        {
            _ms_frameBuffer[i] = f;
            pos = i;
        }
        else if(f->_order == _ms_frameBuffer[i]->_order) // 出現重複幀
        {
            delete f;
            f=0;
            return -1;
        }

        else if(f->_order < _ms_frameBuffer[i]->_order) // 幀優先,i後幀全部後移
        {
            for(int j=_ms_size; j > i; j--)
                _ms_frameBuffer[j] = _ms_frameBuffer[j-1];
            _ms_frameBuffer[i] = f;
            pos = i;
            break;
        }

    }
    _ms_size++;
    _ms_totalBufLength += f->_length;
    if(_ms_size == BUFSIZE)//快取已滿
    {
        //組裝
        if(_ms_totalBufLength > BYTEBUFSIZE)
        {
            printf("error - byteBuffer is too small.\r\n");
            return -1;
        }
        sem_wait(&_ms_empty);
        pthread_mutex_lock(&playBufMutex);
        _ms_offset = 0;
        for(int i=0; i<BUFSIZE; i++)
        {
            if(i>0)//列印缺失的幀
            {
                int lostFrames = _ms_frameBuffer[i]->_order - _ms_frameBuffer[i-1]->_order;
                if(lostFrames == 2)
                {
                    printf("\033[%dC\033[%dAlost frame %d.                 \r\n\033[%dB",70,1,_ms_frameBuffer[i-1]->_order+1,1);
                }
                else if(lostFrames > 2)
                {
                    printf("\033[%dC\033[%dAlost frame %d~%d.\r\n\033[%dB",70,1,_ms_frameBuffer[i-1]->_order+1,_ms_frameBuffer[i]->_order-1,1);
                }

            }
            memcpy(&_ms_byteBuffer[_ms_offset], _ms_frameBuffer[i]->_data, _ms_frameBuffer[i]->_length * sizeof(unsigned char));
            _ms_offset += _ms_frameBuffer[i]->_length;
        }
        pthread_mutex_unlock(&playBufMutex);
        sem_post(&_ms_full);
        for(int i=0; i < BUFSIZE; i++)//清空佇列快取
        {
            delete _ms_frameBuffer[i];
            _ms_frameBuffer[i] = 0;
        }
        //初始化相關變數
        _ms_size = 0;
        _ms_totalBufLength = 0;
    }
    return pos;
}
第一個迴圈裡的方法就是將幀寫入快取佇列_ms_frameBuffer,每次寫入都是安順序插入,保證每次寫入後佇列裡都是按照序號從小到大排好的順序。當快取寫滿後,將_ms_frameBuffer裡的資料寫入_ms_byteBuffer中,每次寫入前都要確保之前_ms_byteBuffer裡的資料已經被解碼了,故要等待_ms_empty訊號,寫完之後就發出_ms_full訊號讓播放執行緒結束等待開始解碼。

5.寫在後面

本來雄心滿滿的想寫的豐滿一點,寫到這裡還是覺得骨感。

很多東西之前想寫,卻發現純屬個人設計範疇,沒必要寫出來阻礙了各位程式設計師的想象力。

有些東西想繼續寫卻發現瞭解不深寫不出來。

最後遺留的問題是播放雖然沒有問題了,但播放時會出現明顯的頓卡感覺,懷疑不應該多次呼叫mad_decoder_run方法來進行解碼,而是應該修改input的函式,來達到迴圈解碼的目的。不過大致功能已經完成了就不想再多深究了。程式碼寫累了哎。