socket TCP 從0實現音頻傳輸 ALSA 播放


RTP標准是采用 UDP 發送,有不少現成的開源庫,但不在本文討論的范圍內。
UDP 用戶數據報,不提供流程,安全傳輸的功能,但速度快,能提供多播,廣播,沒有序列號 SEQ ,有 MTU 限制,1500。
TCP 傳輸控制協議,提供流控,SEQ ,重傳功能,沒有數據長度限制,可以發幾 M 。

但在使用中還是有很多地方需要注意,否則聲音不好聽,斷斷續續或是延時嚴重。

雖然 TCP 在使用上沒有 MTU 限制,但是在真實的2個PC 之音使用 TCP 發送數據,也是被切片的,每次包不能超過 1448 字節,本機對本機,數據包沒有 MTU 限制,因為走的是 LOOPBACK ,裝個 wireshark 一看就懂了。

為什么是 1448 ,MTU 是1500 減去 包頭 20 TCP 頭 20 時間戳 12 。

ALSA 的播放有2種打開模式,阻塞 非阻塞 snd_pcm_nonblock(playback_handle, 1); 

發送接收流控問題,TCP 發送的數據如果大於接收的速度,就會被緩存起來,一直到接收完成以后。

下面只貼出關鍵代碼,來看這一個問題。
send.c 通過 讀取本地的一個 wav 的文件,直接讀取 一塊內容直接發送到本地端口。
這里隱藏了,2個問題
1,用 TCP 來發送,RTP是使用 UDP 發送,而 UDP 不提供 SEQ ,也就是說,接收到的數據順序會亂,所以,RTP 在使用 UDP 承載時都會自己加一個頭信息
自行維護,SEQ ,時間戳,數據校驗
2,UDP 是一發就很快返回,不需要等待接收方的 ACK ,所以用 TCP 發送會慢一點。

play.c

 1 struct sockaddr_in client_addr;
 2 socklen_t client_addr_len;
 3 client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
 4 
 5 if(0 > client_fd)
 6 {
 7     printf("client connect error");
 8     continue;
 9 }
10 printf("client connect addr:%s \n", inet_ntoa(client_addr.sin_addr));
11 while(1)
12 {
13     int  read_len;
14     int  ret;
15     unsigned char buffer[4096];
16     read_len = recv(client_fd, buffer, 4096, 0);
17     if(0 >= read_len) break;
18     ret = snd_pcm_writei(playback_handle, buffer, read_len/4);
19     if(-EPIPE == ret)
20     {
21         snd_pcm_prepare(playback_handle);
22     }
23     printf("read_len:%d \n", read_len);
24 }
25 close(client_fd);

send.c

1 audio_p = audio_buf;
2 send_size = 4096;
3 while((audio_p - audio_buf) <= stat.st_size)
4 {
5     if(-1 != client_fd) send(client_fd, audio_p, send_size, 0);
6     audio_p += send_size;
7 }

以上2個程序,可以運行,也可以播放WAV ,存在的問題是,send 一下子把所有數據都發過去了,當按 ctrl+c 退出時,play 還有數據,還能播放一段時間。

理想的情況是,就像打電話一樣,掛斷后馬上停止播放。

send 中是直接發送的 WAV 中的 PCM 數據,所以要添加合適的 delay 來模擬真實的音頻直播。

delay 時長的計算:4096*1000000/(44100*16*2/8)=23219 ,每次發送的長度是 4096 ,采樣率44k 2通道 16位,算出來,休眠時長是 23219us 。

如果直接在 while 發送 數據包時直接添加 usleep(23219) ,就會發現,播放出來非常難聽,這是因為 TCP 發送還是需要時間的,這也是RTP 用 UDP 的原因,UDP 發送非常快。

所以,這里需要另開一個線程,專門做 發送 TCP 數據包的功能。

同理,play 的里面也需要把 TCP 接收的單獨放到線程中。

原理講明白了,代碼就不貼了。

delay 時長的另一種計算方法:

ffmpeg -i baby.wav
Input #0, wav, from 'baby.wav':
  Metadata:
    artist          : Justin Bieber
    date            : 2010
    genre           : Dance
    title           : Baby
    album           : My World 2.0
    track           : 1
    encoder         : Lavf58.20.100
  Duration: 00:03:34.21, bitrate: 1411 kb/s
    Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 44100 Hz, stereo, s16, 1411 kb/s

ls -l
-rw-r--r-- 1 root root 37786158 1月  31 17:16 baby.wav

(3*60+34)*1000=214000 ms

37786158     4096
________  = _____
214000        x

214000*4096/37786158=23.1974894086877 ms

通過文件大小和播放時長計算,4096長度的需要播放多少時間,這里為了簡單沒有去掉 WAV 文件頭。

 使用 getimeofday 來統計播放需要的時長

printf("chunk_size:%ld \n", chunk_size);
audio_p = audio_buf;
while((audio_p - audio_buf) <= stat.st_size)
{
    struct timeval t1, t2;

    gettimeofday(&t1, NULL);
    int ret = snd_pcm_writei(playback_handle, audio_p, chunk_size);
    if(-EPIPE == ret)
    {
        snd_pcm_prepare(playback_handle);
    }
    gettimeofday(&t2, NULL);

    printf("ms:%ld\n", (t2.tv_sec - t1.tv_sec)*1000 + (t2.tv_usec-t1.tv_usec)/1000);

    audio_p += chunk_size * 4; //16位 雙聲道
}

打出來的結果全是 21 20 。如果 chunk_size 設為 1024 那么打出來的結果很混亂 41 21 0 20 。因為 alsa 的 period_size 不正好是4096 ,所以播放用時不同。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM