基于 libmad 的简单 MP3 流媒体播放器的实现-程序员宅基地

技术标签: 基于 libmad 的简单 MP3 流媒  

1.介绍

基于 libmad 的简单 MP3 流媒体播放器的实现

本文在 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 流媒体播放器主程序结构图
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_streamerror 域用来记录操作 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.MP3get_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(&copy_sem, 0, 1);

empty_sem 信号量的计数表示接收缓冲是否为空,其中如果是 1,表示为空;如果为 0 表示不为空。decode_sem 信号量的计数表示音乐播放线程是否正在对数据解码缓冲区的数据进行解码,如果是 1 表示正在进行解码,如果是 0 表示没有解码;copy_sem 信号量的计数表示是否可以从数据接收缓冲区拷贝数据到数据解码缓冲区,如果是 1 表示可以,如果是 0 表示不能。

两个线程的同步操作或者说是 PV 操作流程如下图所示:


图 3:PV 操作流程示意图
PV 操作流程示意图

7.程序运行实例


图 4:程序运行实例
程序运行实例

8.小结

在实现基于 libmad 的 MP3 流媒体播放器中,我们用到了 libmad 的 API、网络 socket 编程技术、在音频设备上播放 PCM 数据技术、POSIX 信号量以及 POSIX 线程。数据接收线程和音乐播放线程通过信号量和共享数据通信,相比单缓冲操作,通过双缓冲数据操作有效地提高了程序执行效率。同时,通过简单的信号量操作,线程不必使用轮询的方法来处理数据,也进一步减少了对 CPU 资源的浪费。

本文的意义在于给出了一个简单、明了的 MP3 流媒体播放器的实现。但是不足之处在于没有实现流媒体播放的控制协议,不能动态实现播放拖放操作。


参考资料

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/xuwuhao/article/details/8525218

智能推荐

python简易爬虫v1.0-程序员宅基地

文章浏览阅读1.8k次,点赞4次,收藏6次。python简易爬虫v1.0作者:William Ma (the_CoderWM)进阶python的首秀,大部分童鞋肯定是做个简单的爬虫吧,众所周知,爬虫需要各种各样的第三方库,例如scrapy, bs4, requests, urllib3等等。此处,我们先从最简单的爬虫开始。首先,我们需要安装两个第三方库:requests和bs4。在cmd中输入以下代码:pip install requestspip install bs4等安装成功后,就可以进入pycharm来写爬虫了。爬

安装flask后vim出现:error detected while processing /home/zww/.vim/ftplugin/python/pyflakes.vim:line 28_freetorn.vim-程序员宅基地

文章浏览阅读2.6k次。解决方法:解决方法可以去github重新下载一个pyflakes.vim。执行如下命令git clone --recursive git://github.com/kevinw/pyflakes-vim.git然后进入git克降目录,./pyflakes-vim/ftplugin,通过如下命令将python目录下的所有文件复制到~/.vim/ftplugin目录下即可。cp -R ...._freetorn.vim

HIT CSAPP大作业:程序人生—Hello‘s P2P-程序员宅基地

文章浏览阅读210次,点赞7次,收藏3次。本文简述了hello.c源程序的预处理、编译、汇编、链接和运行的主要过程,以及hello程序的进程管理、存储管理与I/O管理,通过hello.c这一程序周期的描述,对程序的编译、加载、运行有了初步的了解。_hit csapp

18个顶级人工智能平台-程序员宅基地

文章浏览阅读1w次,点赞2次,收藏27次。来源:机器人小妹  很多时候企业拥有重复,乏味且困难的工作流程,这些流程往往会减慢生产速度并增加运营成本。为了降低生产成本,企业别无选择,只能自动化某些功能以降低生产成本。  通过数字化..._人工智能平台

electron热加载_electron-reloader-程序员宅基地

文章浏览阅读2.2k次。热加载能够在每次保存修改的代码后自动刷新 electron 应用界面,而不必每次去手动操作重新运行,这极大的提升了开发效率。安装 electron 热加载插件热加载虽然很方便,但是不是每个 electron 项目必须的,所以想要舒服的开发 electron 就只能给 electron 项目单独的安装热加载插件[electron-reloader]:// 在项目的根目录下安装 electron-reloader,国内建议使用 cnpm 代替 npmnpm install electron-relo._electron-reloader

android 11.0 去掉recovery模式UI页面的选项_android recovery 删除 部分菜单-程序员宅基地

文章浏览阅读942次。在11.0 进行定制化开发,会根据需要去掉recovery模式的一些选项 就是在device.cpp去掉一些选项就可以了。_android recovery 删除 部分菜单

随便推点

echart省会流向图(物流运输、地图)_java+echart地图+物流跟踪-程序员宅基地

文章浏览阅读2.2k次,点赞2次,收藏6次。继续上次的echart博客,由于省会流向图是从echart画廊中直接取来的。所以直接上代码<!DOCTYPE html><html><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /&_java+echart地图+物流跟踪

Ceph源码解析:读写流程_ceph 发送数据到其他副本的源码-程序员宅基地

文章浏览阅读1.4k次。一、OSD模块简介1.1 消息封装:在OSD上发送和接收信息。cluster_messenger -与其它OSDs和monitors沟通client_messenger -与客户端沟通1.2 消息调度:Dispatcher类,主要负责消息分类1.3 工作队列:1.3.1 OpWQ: 处理ops(从客户端)和sub ops(从其他的OSD)。运行在op_tp线程池。1...._ceph 发送数据到其他副本的源码

进程调度(一)——FIFO算法_进程调度fifo算法代码-程序员宅基地

文章浏览阅读7.9k次,点赞3次,收藏22次。一 定义这是最早出现的置换算法。该算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面予以淘汰。该算法实现简单,只需把一个进程已调入内存的页面,按先后次序链接成一个队列,并设置一个指针,称为替换指针,使它总是指向最老的页面。但该算法与进程实际运行的规律不相适应,因为在进程中,有些页面经常被访问,比如,含有全局变量、常用函数、例程等的页面,FIFO 算法并不能保证这些页面不被淘汰。这里,我_进程调度fifo算法代码

mysql rownum写法_mysql应用之类似oracle rownum写法-程序员宅基地

文章浏览阅读133次。rownum是oracle才有的写法,rownum在oracle中可以用于取第一条数据,或者批量写数据时限定批量写的数量等mysql取第一条数据写法SELECT * FROM t order by id LIMIT 1;oracle取第一条数据写法SELECT * FROM t where rownum =1 order by id;ok,上面是mysql和oracle取第一条数据的写法对比,不过..._mysql 替换@rownum的写法

eclipse安装教程_ecjelm-程序员宅基地

文章浏览阅读790次,点赞3次,收藏4次。官网下载下载链接:http://www.eclipse.org/downloads/点击Download下载完成后双击运行我选择第2个,看自己需要(我选择企业级应用,如果只是单纯学习java选第一个就行)进入下一步后选择jre和安装路径修改jvm/jre的时候也可以选择本地的(点后面的文件夹进去),但是我们没有11版本的,所以还是用他的吧选择接受安装中安装过程中如果有其他界面弹出就点accept就行..._ecjelm

Linux常用网络命令_ifconfig 删除vlan-程序员宅基地

文章浏览阅读245次。原文链接:https://linux.cn/article-7801-1.htmlifconfigping &lt;IP地址&gt;:发送ICMP echo消息到某个主机traceroute &lt;IP地址&gt;:用于跟踪IP包的路由路由:netstat -r: 打印路由表route add :添加静态路由路径routed:控制动态路由的BSD守护程序。运行RIP路由协议gat..._ifconfig 删除vlan