本文在 Fedora 5 Linux 下實現了一個基於 libmad 的 mp3 流媒體播放器。此流媒體播放器可以播放基於 HTTP 1.1 協議傳輸的 MP3 流媒體數據。
基本原理是:從 HTTP 服務器獲得 MP3 媒體信息,然後通過網絡傳輸把 MP3 數據以數據流的形式接收到 MP3 流媒體播放器客戶端,由客戶端通過 libmad 解碼 MP3 數據流,得到 PCM 音頻數據,寫入音頻設備,播放音樂。本文的流媒體播放器只是實現了必要的簡單功能,沒有考慮太多情況。比如,沒有考慮實時播放控制,這樣的話就不能隨意選取播放點進行播放。
本文的 MP3 流媒體播放器創建兩個線程,使用兩個緩沖區保存 MP3 數據,可以一邊下載數據,一邊播放音樂。編譯運行此 MP3 流媒體播放器需要安裝 libmad (www.underbit.com/prodUCts/mad/) 以及 ALSA(Advanced Linux Sound Architecture) (http://www.alsa-project.org)相關的軟件。ALSA包括4部分,分別是 sound driver, sound library , sound utilities 以及 tools。至少應該安裝 sound driver, sound library 。編譯程序時連接庫的選項是:-lmad -lasound -lpthread。
本文的 MP3 流媒體播放器使用雙緩沖區,一個是數據接收緩沖區,另一個是數據解碼緩沖區。主程序結構如下圖所示,圖中的藍色線表示數據流向。
圖 1:MP3 流媒體播放器主程序結構圖
2.libmad簡介
MAD (libmad)是一個開源的高精度 MPEG 音頻解碼庫,支持 MPEG-1(Layer I, Layer II 和 LayerIII(也就是 MP3)。LIBMAD 提供 24-bit 的 PCM 輸出,完全是定點計算,非常適合沒有浮點支持的平台上使用。使用 libmad 提供的一系列 API,就可以非常簡單地實現 MP3 數據解碼工作。在 libmad 的源代碼文件目錄下的 mad.h 文件中,可以看到絕大部分該庫的數據結構和 API 等。
本文用到的 libmad 中的主要數據結構有:struct mad_stream, struct mad_synth, struct mad_frame。它們的定義如下:
清單 1:libmad 中的主要數據結構
struct mad_stream { unsigned char const *buffer; /* input bitstream buffer */ unsigned char const *bufend; /* end of buffer */ unsigned long skiplen; /* bytes to skip before next frame */ int sync; /* stream sync found */ unsigned long freerate; /* free bitrate (fixed) */ unsigned char const *this_frame; /* start of current frame */ unsigned char const *next_frame; /* start of next frame */ struct mad_bitptr ptr; /* current processing bit pointer */ struct mad_bitptr anc_ptr; /* ancillary bits pointer */ unsigned int anc_bitlen; /* number of ancillary bits */ unsigned char (*main_data)[MAD_BUFFER_MDLEN]; /* Layer III main_data() */ unsigned int md_len; /* bytes in main_data */ int options; /* decoding options (see below) */ enum mad_error error; /* error code (see above) */ };
更多內容請看流媒體播放器 流媒體文件格式播放技巧專題,或如果緩沖區最後一個 MPEG 數據幀只有部分數據包括在緩沖區中,那麼 struct mad_stream 中的 next_frame 域指到不完整數據的開始地址。 由於緩沖區的 MPEG 數據幀不一定完整,所以不完整的 MPEG 幀的數據必須拷貝到下一次解碼操作的緩沖區中,進行再次解碼。這裡我們還看到 bufend 指向緩沖區數據的最後地址,也就是最後一字節的地址加 1 的位置。mad_stream.bufend – mad_stream.next_frame 就是剩余的未被解碼的 MPEG 幀的數據的字節數量(假設此幀在緩沖區中不完整)。mad_stream 的 error 域用來記錄操作 mad_stream 得到的錯誤代碼。錯誤代碼在 mad.h 中有很詳細的定義。
清單 2:錯誤代碼在 mad.h 中的詳細定義
struct mad_synth { mad_fixed_t filter[2][2][2][16][8]; /* polyphase filterbank outputs */ /* [ch][eo][peo][s][v] */ unsigned int phase; /* current processing phase */ struct mad_pcm pcm; /* PCM output */ };
mad_synth 中的關鍵域 pcm 保存解碼和合成後得到的 PCM 數據。
清單 3:mad_synth 中的關鍵域
struct mad_pcm { unsigned int samplerate; /* sampling frequency (Hz) */ unsigned short channels; /* number of channels */ unsigned short length; /* number of samples per channel */ mad_fixed_t samples[2][1152]; /* PCM output samples [ch][sample] */ };
struct mad_pcm 定義了音頻的采樣率、每個聲道個數以及最後的 PCM 采樣數據。這些參數可用來初始化音頻設備。
清單 4:struct mad_pcm
struct mad_frame { struct mad_header header; /* MPEG audio header */ int options; /* decoding options (from stream) */ mad_fixed_t sbsample[2][36][32]; /* synthesis subband filter samples */ mad_fixed_t (*overlap)[2][32][18]; /* Layer III block overlap data */ };
mad_frame 是記錄 MPEG 幀解碼後的數據的數據結構,其中的 mad_header 尤其重要,其用來記錄 MPEG 幀的一些基本信息,比如 MPEG 層數、聲道模式、流比特率、采樣比特率等等。聲道模式包括單聲道、雙聲道、聯合立體混音聲以及一般立體聲。
清單 5:mad_frame
enum mad_mode { MAD_MODE_SINGLE_CHANNEL = 0, /* single channel */ MAD_MODE_DUAL_CHANNEL = 1, /* dual channel */ MAD_MODE_JOINT_STEREO = 2, /* joint (MS/intensity) stereo */ MAD_MODE_STEREO = 3 /* normal LR stereo */ }; struct mad_header { enum mad_layer layer; /* audio layer (1, 2, or 3) */ enum mad_mode mode; /* channel mode */ int mode_extension; /* additional mode info */ enum mad_emphasis emphasis; /* de-emphasis to use */ unsigned long bitrate; /* stream bitrate (bps) */ unsigned int samplerate; /* sampling frequency (Hz) */ unsigned short crc_check; /* frame CRC accumulator */ unsigned short crc_target; /* final target CRC checksum */ int flags; /* flags */ int private_bits; /* private bits */ mad_timer_t duration; /* audio playing time of frame */ };
下面就本文使用的 API 的功能做簡單介紹。
在本文中用到的 API 包括:
void mad_stream_init(struct mad_stream *) void mad_synth_init(struct mad_synth *); void mad_frame_init(struct mad_frame *);
以上3個 API 初始化解碼需要的數據結構。
void mad_stream_buffer(struct mad_stream *, unsigned char const *, unsigned long);
此函數把原始的未解碼的 MPEG 數據和 mad_stream 數據結構關聯,以便使用 mad_frame_decode( ) 來解碼 MPEG 幀數據。
int mad_frame_decode(struct mad_frame *, struct mad_stream *);
把 mad_stream 中的 MPEG 幀數據解碼。
void mad_synth_frame(struct mad_synth *, struct mad_frame const *);
把解碼後的音頻數據合成 PCM 采樣。
void mad_stream_finish(struct mad_stream *); void mad_frame_finish(struct mad_frame *); mad_synth_finish(struct mad_synth);
以上 3 個 API 在解碼完畢後使用,釋放 libmad 占用的資源等。
更多內容請看流媒體播放器 流媒體文件格式播放技巧專題,或 3.PCM 音頻設備的操作
對音頻設備的操作主要是初始化音頻設備以及往音頻設備發送 PCM(Pulse Code Modulation)數據。為了方便,本文使用 ALSA(Advanced Linux Sound Architecture)提供的庫和驅動。在編譯和運行本文中的 MP3 流媒體播放器的時候,必須先安裝 ALSA 相關的文件。
本文用到的主要對 PCM 設備操作的函數分為 PCM 設備初始化的函數以及 PCM 接口的一些操作函數。
PCM 硬件設備參數設置和初始化的函數有:
int snd_pcm_hw_params_malloc (snd_pcm_hw_params_t **ptr) int snd_pcm_hw_params_any (snd_pcm_t *pcm, snd_pcm_hw_params_t *params) void snd_pcm_hw_params_free (snd_pcm_hw_params_t *obj) int snd_pcm_hw_params_set_Access ( snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_access_t _access) int snd_pcm_hw_params_set_format ( snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_format_t val) int snd_pcm_hw_params_set_channels(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int val) int snd_pcm_hw_params_set_rate_near(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int *val, int *dir)
PCM 接口的操作函數:
int snd_pcm_hw_params (snd_pcm_t *pcm, snd_pcm_hw_params_t *params) int snd_pcm_prepare (snd_pcm_t *pcm) int snd_pcm_open (snd_pcm_t **pcm, const char *name, snd_pcm_stream_t stream, int mode) int snd_pcm_close (snd_pcm_t *pcm) snd_pcm_sframes_t snd_pcm_writei (snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size)
這些函數用到了 snd_pcm_hw_params_t 結構,此結構包含用來播放 PCM 數據流的硬件信息配置。在往音頻設備(聲卡)寫入音頻數據之前,必須設置訪問類型、采樣格式、采樣率、聲道數等。
首先使用 snd_pcm_open () 打開 PCM 設備,在 ALSA 中,PCM 設備都有名字與之對應。比如我們可以定義 PCM 設備名字為 char *pcm_name = "plughw:0,0"。最重要的 PCM 設備接口是“plughw”以及“hw”接口。使用“plughw”接口,程序員不必過多關心硬件,而且如果設置的配置參數和實際硬件支持的參數不一致,ALSA 會自動轉換數據。如果使用“hw”接口,我們就必須檢測硬件是否支持設置的參數了。Plughw 後面的兩個數字分別表示設備號和次設備(subdevice)號。
snd_pcm_hw_params_malloc( ) 在棧中分配 snd_pcm_hw_params_t 結構的空間,然後使用 snd_pcm_hw_params_any( ) 函數用聲卡的全配置空間參數初始化已經分配的 snd_pcm_hw_params_t 結構。snd_pcm_hw_params_set_access ( ) 設置訪問類型,常用訪問類型的宏定義有:
SND_PCM_ACCESS_RW_INTERLEAVED
交錯訪問。在緩沖區的每個 PCM 幀都包含所有設置的聲道的連續的采樣數據。比如聲卡要播放采樣長度是 16-bit 的 PCM 立體聲數據,表示每個 PCM 幀中有 16-bit 的左聲道數據,然後是 16-bit 右聲道數據。
SND_PCM_ACCESS_RW_NONINTERLEAVED
非交錯訪問。每個 PCM 幀只是一個聲道需要的數據,如果使用多個聲道,那麼第一幀是第一個聲道的數據,第二幀是第二個聲道的數據,依此類推。
函數 snd_pcm_hw_params_set_format() 設置數據格式,主要控制輸入的音頻數據的類型、無符號還是有符號、是 little-endian 還是 bit-endian。比如對於 16-bit 長度的采樣數據可以設置為:
SND_PCM_FORMAT_S16_LE 有符號16 bit Little Endian SND_PCM_FORMAT_S16_BE 有符號16 bit Big Endian SND_PCM_FORMAT_U16_LE 無符號16 bit Little Endian SND_PCM_FORMAT_U16_BE 無符號 16 bit Big Endian
比如對於 32-bit 長度的采樣數據可以設置為:
SND_PCM_FORMAT_S32_LE 有符號32 bit Little Endian SND_PCM_FORMAT_S32_BE 有符號32 bit Big Endian SND_PCM_FORMAT_U32_LE 無符號32 bit Little Endian SND_PCM_FORMAT_U32_BE 無符號 32 bit Big Endian
函數 snd_pcm_hw_params_set_channels() 設置音頻設備的聲道,常見的就是單聲道和立體聲,如果是立體聲,設置最後一個參數為2。snd_pcm_hw_params_set_rate_near () 函數設置音頻數據的最接近目標的采樣率。snd_pcm_hw_params( ) 從設備配置空間選擇一個配置,讓函數 snd_pcm_prepare() 准備好 PCM 設備,以便寫入 PCM 數據。snd_pcm_writei() 用來把交錯的音頻數據寫入到音頻設備。
初始化 PCM 設備的例程如下:
清單 6:初始化 PCM 設備的例程
/* open a PCM device */ int open_device(struct mad_header const *header) { int err; snd_pcm_hw_params_t *hw_params; char *pcm_name = "plughw:0,0"; 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 格式是 SND_PCM_FORMAT_S32_LE,采樣的格式是每個采樣有 32-bit 的數據,數據按照 little-endian 存放。如果通過 mad_frame_decode() 函數得到 PCM 數據後,要求每個采樣數據只占 16-bit,需要把數據進行MAD的定點類型到 signed short 類型進行轉換。那麼,PCM 數據如何寫入聲卡中呢?函數實現例程如下所示:
更多內容請看流媒體播放器 流媒體文件格式播放技巧專題,或 清單 7:PCM 數據寫入聲卡函數實現例程
while (nsamples--) { /* nsamples 是采樣的數目 */ signed int sample; sample = pcm->samples[0][j]; *(OutputPtr++) = sample & 0xff; *(OutputPtr++) = (sample >> 8); *(OutputPtr++) = (sample >> 16); *(OutputPtr++) = (sample >> 24); if (nchannels == 2) { sample = pcm->samples[1][j]; *(OutputPtr++) = sample & 0xff; *(OutputPtr++) = sample >> 8; *(OutputPtr++) = (sample >> 16); *(OutputPtr++) = (sample >> 24); } j++; } 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 -1; } }
這裡用到了 http://www.alsa-project.org/ 關於 ALSA 文檔中的例子函數 xrun_recovery( )。詳細例子請參見 http://www.alsa-project.org/alsa-doc/alsa-lib/_2test_2pcm_8c-example.Html。使用此函數的目的是避免出現由於網絡原因,聲卡不能及時得到音頻數據而使得 snd_pcm_writei() 不能正常連續工作。實際上在 xrun_recovery( ) 中,又調用 snd_pcm_prepare() 和 snd_pcm_resume() 以實現能“恢復錯誤”的功能。-EPIPE 錯誤表示應用程序沒有及時把 PCM 采樣數據送入ASLA 庫。xrun_recovery() 函數如下所示:
清單 8:xrun_recovery() 函數
int xrun_recovery(snd_pcm_t *handle, int err) { if (err == -EPIPE) { /* under-run */ err = snd_pcm_prepare(handle); if (err < 0) printf("Can't recovery from underrun, prepare failed: %s\n", snd_strerror(err)); return 0; } else if (err == -ESTRPIPE) { while ((err = snd_pcm_resume(handle)) == -EAGAIN) sleep(1); /* wait until the suspend flag is released */ if (err < 0) { err = snd_pcm_prepare(handle); if (err < 0) printf("Can't recovery from suspend, prepare failed: %s\n", snd_strerror(err)); } return 0; } return err; }
知道了具體的音頻設備操作方法,就該使用 MAD 提供的函數具體實現解碼了。函數 mp3_decode_buf( ) 提供了使用 libmad 解碼的方法。首先調用 mad_stream_buffer() 函數把 MP3 流數據和 decode_stream 關聯,然後開始循環解碼數據。如果在解碼數據過程中,有不完整 PCM 數據幀,那麼 decode_stream.error 的值就是 MAD_ERROR_BUFLEN,且 decode_stream.next_frame 不為 NULL。這時候,把剩余的未解碼的數據再拷貝到數據解碼緩沖區裡。 mad_frame_decode( ) 函數從 decode_stream 中得到 PCM 數據。
清單 9:mad_frame_decode( ) 函數從 decode_stream 中得到 PCM 數據
int mp3_decode_buf(char *input_buf, int size) { int decode_over_flag = 0; int remain_bytes = 0; int ret_val = 0; mad_stream_buffer(&decode_stream, input_buf, size); decode_stream.error = MAD_ERROR_NONE; while (1) { if (decode_stream.error == MAD_ERROR_BUFLEN) { if (decode_stream.next_frame != NULL) { remain_bytes = decode_stream.bufend - decode_stream.next_frame; memcpy(input_buf, decode_stream.next_frame, remain_bytes); return remain_bytes; } } ret_val = mad_frame_decode(&decode_frame, &decode_stream); /* 省略部分代碼 */ ... if (ret_val == 0) { if (play_frame(&decode_frame) == -1) { return -1; } } /* 後面代碼省略 */ ... } return 0; }
更多內容請看流媒體播放器 流媒體文件格式播放技巧專題,或 4.創建線程
本文使用 POSIX 線程庫(pthreads)來創建線程。比如,本文需要兩個線程,一個是數據接收線程,另一個是音樂播放線程。創建線程的程序如下所示:
清單 10:創建線程
ret_val = pthread_create(&thread[0], NULL, get_http_content, &read_val); if (ret_val != 0) { printf("Cannot create get_http_content thread!\n"); return 1; } ret_val = pthread_create(&thread[1], NULL, play_http_content, &read_val); if (ret_val != 0) { printf("Cannot create play_http_content thread!\n"); return 1; } pthread_join(thread[0], NULL); pthread_join(thread[1], NULL);
可以看到,數據接收線程的線程主函數是 get_http_content, 而播放音樂的線程主函數是 play_http_content。創建子線程後,主線程調用 pthread_join() 等待子結束,並釋放線程相關資源。
5.接收 MP3 流媒體數據
由於 MP3 流媒體數據是在 HTTP 服務器的文件目錄中,所以,必須由客戶端發送 HTTP 請求,然後得到相關 URL 的 HTTP 響應。HTTP 的請求格式如下:
<Method> <Request-URI> <HTTP-1.x> CRLF *(( general-header request-header entity-header ) CRLF) CRLF [ message-body ]
這裡 CR(13) 表示回車,LF 表示換行。
根據 HTTP 請求格式,可以構建發送到 HTTP 服務器請求。比如,想要往 192.168.0.123 HTTP 發送獲得文件 http://192.168.0.123/45.MP3 那麼構建的請求是:
GET /45.MP3 HTTP/1.1\r\n HOST: 192.168.0.123\r\n\r\n
發送請求後,HTTP 服務器會就請求做出響應。如果請求合法,那麼響應包括響應的媒體信息,包括 HTTP/1.1 200 OK,表示請求成功。最簡單驗證請求是否有效的方法是使用 telnet。 例如:
[root@localhost netmad]# telnet 192.168.0.123 80 Trying 192.168.0.123... Connected to 192.168.0.123(192.168.0.123). Escape character is '^]'. HEAD /45.MP3 HTTP/1.1 HOST:192.168.0.123 HTTP/1.1 200 OK Date: Tue, 14 Nov 2006 10:11:43 GMT Server: Apache/2.2.0 (Fedora) Last-Modified: Tue, 17 Oct 2006 15:08:16 GMT ETag: "3147c9-32e080-1fb83800" Accept-Ranges: bytes Content-Length: 3334272 Connection: close Content-Type: audio/mpeg X-Pad: avoid browser bug
這裡可以看到在 HTTP 請求的響應中,有關於 45.MP3 的簡單信息,包括文件類型 Content-Type: audio/mpeg,以及文件的長度 Content-Length: 3334272。通過解析 HTTP 響應,很容易從 Content-Length 項得到 MP3 數據總的長度。為了發送 HTTP 請求,首先從播放器程序傳遞的參數解析出請求的資源的 URI,比如程序傳遞參數為 http://192.168.0.123/45.MP3 那麼解析此 URL,得到 HTTP 請求的資源 URI 是 /45.MP3。get_address 函數簡單地解析了 URL,用 gethostbyname( ) 獲得域名以及操作 socket 需要的地址信息。本文用於網絡通信的一些 socket 相關的函數如下:
#include <sys/types.h> #include <sys/socket.h> int socket (int family, int type, int protocol)
此函數創建 socket 。
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
和目標地址服務程序連接,完成 3 次握手。
int recv(int s, void *buf, size_t len, int flags);
此函數從創建的 socket 接收數據。
更多內容請看流媒體播放器 流媒體文件格式播放技巧專題,或 6.數據接收線程和音樂播放線程
由於是兩個線程並發運行,且音樂播放線程線程運行速度較慢。如果網絡速度較快,數據接收線程的接收緩沖區滿後,如果當前音樂播放線程正在播放音樂,那麼數據接收線程必須停止接收數據。如果不讓數據接收線程進入等待狀態,它會一直輪訓音樂播放線程觀察其是否需要數據,簡單的輪詢會浪費 CPU 資源,所以在這種情況下,有必要讓數據接收線程進入等待狀態。本文使用信號量機制,來動態控制線程的運行。數據接收緩沖區必須留出一定的空間,存放解碼緩沖區中沒有被解碼的數據。那麼要留出多少數據空間呢?至少應該留出一幀數據的空間。這裡 8192 字節空間存放剩余的一幀 MPEG 數據,一般情況下應該夠用。因此定義:
#define DECODE_BUF_SIZE (8192*11) #define GARD_SIZE (8192*10) static char decode_buf[DECODE_BUF_SIZE]; static char recv_buf[DECODE_BUF_SIZE];
GARD_SIZE 是一次從 socket 讀取數據字節數的最大值,而解碼緩沖區的大小應該是比 GARD_SIZE 大 8192 字節,因此定義 DECODE_BUF_SIZE 為 (8192*11)。recv_buf 是數據接收緩沖區,decode_buf 是數據解碼緩沖區。在拷貝數據到解碼緩沖區的時候,上次未解碼的數據,還被保存在解碼緩沖區的開始部分,故拷貝數據的時候,必須拷貝到剩余數據的後面,程序例子如下:
memcpy(decode_buf + current_remain, recv_buf, current_read); current_read += current_remain;
這裡的 current_remain 表示上次解碼線程中未解碼的不完整 MP3 幀的數據字節數,current_read 表示當前接收線程接收到的實際數據字節數。兩個緩沖區之間的數據拷貝操作如下圖所示。
圖 2:緩沖區之間的數據拷貝操作
數據接收線程和音樂播放線程之間的同步
由於使用了雙緩沖區保存數據,所以,在音樂播放線程播放音樂的時候,數據接收線程不能把數據拷貝到數據解碼緩沖區,而是需要等待。當數據接收緩沖區滿的時候,接收線程自己也需要等待。本文用到了 POSIX 信號量處理函數,實現了線程之間的同步。它們分別是:
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value);
初始化信號量,第三個參數表示初始的信號量的計數。
int sem_wait(sem_t * sem);
sem_wait 阻塞當前線程的執行,直到信號量的計數非 0;然後,它會把信號量計數減 1,然後程序繼續執行。相當於 P 操作。
int sem_post(sem_t * sem);
把 sem 指向的信號量計數加 1。相當於 V 操作。
int sem_destroy(sem_t * sem);
釋放信號量對象。
在程序中,信號量定義及初始化為:
static sem_t empty_sem; static sem_t decode_sem; static sem_t copy_sem; sem_init(&empty_sem, 0, 1); sem_init(&decode_sem, 0, 0); sem_init(©_sem, 0, 1);
更多內容請看流媒體播放器 流媒體文件格式播放技巧專題,或
empty_sem 信號量的計數表示接收緩沖是否為空,其中如果是 1,表示為空;如果為 0 表示不為空。decode_sem 信號量的計數表示音樂播放線程是否正在對數據解碼緩沖區的數據進行解碼,如果是 1 表示正在進行解碼,如果是 0 表示沒有解碼;copy_sem 信號量的計數表示是否可以從數據接收緩沖區拷貝數據到數據解碼緩沖區,如果是 1 表示可以,如果是 0 表示不能。
兩個線程的同步操作或者說是 PV 操作流程如下圖所示:
圖 3:PV 操作流程示意圖 ·流媒體播放器大對比·ppStream 流媒體播放器最新版發布
7.程序運行實例
圖 4:程序運行實例
8.小結
在實現基於 libmad 的 MP3 流媒體播放器中,我們用到了 libmad 的 API、網絡 socket 編程技術、在音頻設備上播放 PCM 數據技術、POSIX 信號量以及 POSIX 線程。數據接收線程和音樂播放線程通過信號量和共享數據通信,相比單緩沖操作,通過雙緩沖數據操作有效地提高了程序執行效率。同時,通過簡單的信號量操作,線程不必使用輪詢的方法來處理數據,也進一步減少了對 CPU 資源的浪費。
本文的意義在於給出了一個簡單、明了的 MP3 流媒體播放器的實現。但是不足之處在於沒有實現流媒體播放的控制協議,不能動態實現播放拖放操作。
原文鏈接:http://www.ibm.com/developerworks/cn/linux/l-cn-libmadmp3player/index.html
更多內容請看流媒體播放器 流媒體文件格式播放技巧專題,或