AirPlay是苹果公司推出的一套无线音视频解决方案,我们手里的iPhone、iPad甚至是Apple Watch等设备还有电脑上的iTunes都支持AirPlay。使用AirPlay可以方便的使移动设备的音频流,视频流可以投射到音箱和显示设备上,而无需蓝牙设备的配对过程。但是支持AirPlay功能的音响设备普遍都比较昂贵,而且家里的3.5毫米的插口的老音箱也没有利用起来,本着“喜新不厌旧,改造旧物发挥余热”的精神,我开始了新一轮的折腾。

我的想法是用嵌入式方案STM32+W5500的方式实现AirPlay协议,并使用I2S接口接PCM5102A音频模块来实现音频播放。于是马上上网查资料,发现成熟的方案还不太多,现有的方案都是在linux或者windows上运行的,精挑细选之后选择了https://github.com/juhovh/shAirPlay这个AirPlay开源项目作为参考,主要是该代码是用C语言实现移植到stm32比较方便。

在开始之前我们有必要先了解一下AirPlay, AirPlay是苹果公司收购airtunes后,在airtunes协议的基础上增加了视频,照片的传输,从而变为完整的AirPlay协议。AirPlay可以将iPhone 、iPad、iPod touch 等iOS 设备上的包括图片、音频、视频及镜像传输到支持AirPlay协议的设备中播放,实现随时随地的无线流媒体传输。在我们的这个项目中,我们只需要实现AirPlay协议中的音频流部分。AirPlay的实现过程中包含多个子协议,其中有的协议是完全标准的,有一部分协议苹果公司进行了一些修改,有的则是完全私有的。

• Multicast DNS:用于发布服务,启动后,在iOS的控制中心菜单中就能看到支持AirPlay的设备列表;

• HTTP / RTSP / RTP:用于流媒体服务,传输音视频数据,进行播放控制等;

• NTP:网络时间协议,用于时间同步;

• FAirPlay DRM加密协议:用于进行数据加密,这个是完全私有的加密协议。

开始工作前我们需要进行一些前期准备,如下图:

图1 硬件框图及接线

iPhone用来播放音乐,并通过Airplay协议发送音频流。W5500EVB是WIZnet的W5500开发板,其中的W5500除了包含以太网的MAC和PHY外,还内置了硬件的TCP/IP协议栈,是目前比较常用的以太网方案。我们使用W5500EVB作为服务器接收并解码音频数据,开发板的操作可以参考http://www.w5500.com中的例程。PCM5102A音频模块可以将解码后的音频数据进行播放。经过分析后我们要实现AirPlay音频播放主要是实现以下三个方面:

  1. iPhone在网络中发现Airplay设备(W5500EVB)并建立连接;
  2. W5500EVB接收并解码音频数据;
  3. W5500EVB通过I2S接口将音频传送到PCM5102A音频模块;

接下来我们将分别实现这三个步骤:

1、发现Airplay设备并建立连接

AirPlay发现设备是基于mDNS协议(Multicast DNS)实现,iPhone与W5500EVB需要连入同一网络且W5500EVB要加入组播组224.0.0.251:5353才可以接收mDNS报文。W5500EVB收到iPhone发出的Querry查询报文后回复Response报文,报文的内容可以参考文档《Unofficial AirPlay Protocol Specification》(http://nto.github.io/AirPlay.html),下方为mDNS设备发现和设备注册代码:

1 uint8 mdns_query(uint8 s, uint8 * name,uint8* rname)

2 {

3     uint8 ip[4];

4     uint16 len, port;

5     switch (getSn_SR(s)) {

6     case SOCK_CLOSED:/*打开SOCKET并加入组播组224.0.0.251*/

7         setDIPR(s,DIP);/* 设置目标IP 224.0.0.251*/

8         setDHAR(s,DHAR);/*设置目标MAC 01:00:5e:00:00:FB */

9         setDPORT(s,DPORT);/*设置目标端口5353*/

10         socket(s, Sn_MR_UDP, 5353,Sn_MR_MULTI);/*打开SOCKET并加入组播组*/

11         break;

12     case SOCK_UDP:

13         if ((len = getSn_RX_RSR(s)) > 0) {

14             if (len > MAX_DNS_BUF_SIZE) {

15                 len = MAX_DNS_BUF_SIZE;

16             }

17             len = recvfrom(s, BUFPUB, len, ip, &port);

18             /*检查收到报文的flag确定报文是否为查询报文*/

19             if ((BUFPUB[2]&0x80)==0) {

20                 len = mdns_makeresponse(0,name,rname,BUFPUB,MAX_DNS_BUF_SIZE);

21                 sendto(s, BUFPUB, len, DIP,DPORT);

22             }

23         }

24         break;

25     }

26     return DNS_RET_PROGRESS;

27 }

mdns_query()函数中状态机的case SOCK_CLOSED部分用来初始化网络并加入mDNS组播。当我们点击iOS播放页面的AirPlay图标的时候,会自动发送服务请求。case SOCK_UDP部分第19行函数用来判断是否iPhone发过来的mDNS服务请求。代码20行的mdns_makeresponse()函数用来拼接响应报文,该响应报文中包含设备信息以及将要提供的RAOP服务类型,RAOP服务是AirPlay的音频流协议,RAOP从本质上来说是实时流协议,只不过增加了身份验证请求-应答的步骤,RAOP服务用两个信道实现流媒体:一个是用实时流协议的控制信道;另一个是数据信道用来发送数据。通过21行的sendto ()函数可以将响应报文发送到组播组,iPhone就可以接收到这个响应数据包。通过抓包工具抓取响应报文我们可以看到RAOP服务的相关信息,如下图:

图2 RAOP服务报文

其中:
Service:此字段是服务名称,格式:MAC地址@设备名._raop._tcp.local

Protocol:服务的类别:_AirPlay是视频服务(未用到),_raop是音频服务。

Name:数据传输的协议,可以通过TCP或者UDP传输。

Port:声明了RTSP命令交互的端口号为5005,客户端可以通过此端口号与服务端建立连接。

这部分如果没有错误的话,应该能在iPhone的控制中心的音乐播放页面看到我们用W5500EVB虚拟出来的AirPlay设备,就是下图中“wiznet”。

图3  iPhone发现设备

iPhone成功发现AirPlay设备后就可以连接设备,此时我们点击列表中显示的设备,连接成功后对应设备的后面会显示对勾,如下图所示:

图4  iPhone连接设备

话虽简短,但实现起来可一点也不容易。AirPlay的握手使用的是RTSP(Real-Time Stream Protocol)协议,RTSP是一个基于文本的多媒体播放控制协议。上文介绍的iPhone发现设备的过程中指定了RTSP是通过TCP进行通信且端口号为5005,所以我们要创建一个端口号为5005的TCP服务器来接收数据包,并通过下图中的几个步骤完成握手:Apple-Challen

图5  AirPlay握手通讯步骤

对RTSP数据包的解析是通过rtsp_parase_request()函数进行的如下方代码20行所示。

1 void do_tcp_server(SOCKET s,uint16 localport)

2 {

3     uint16 len;

4     uint8 send_buffer[1024];

5     switch (getSn_SR(s)) {

6     case SOCK_INIT:

7         listen(s);

8         break;

9     case SOCK_ESTABLISHED:

10         if (getSn_IR(s) & Sn_IR_CON) {

11             setSn_IR(s, Sn_IR_CON);

12         }

13         len=getSn_RX_RSR(s);

14         if (len>0) {

15             memset(buffer,0,sizeof(buffer));

16             querry_flag=1;

17             recv(s,buffer,len);

18             memset(send_buffer,0,sizeof(send_buffer));

19             /*解析RTSP数据包并拼接响应数据*/

20             rtsp_parase_request((char*)buffer,(char*)send_buffer,s,len);

21             /*发送响应数据包*/

22             if (0==send(s,send_buffer,strlen(send_buffer))) {

23                 send(s,send_buffer,strlen(send_buffer));

24             }

25         }

27         break;

28     case SOCK_CLOSE_WAIT:

29         disconnect(s);

30         querry_flag=0;

31         break;

32     case SOCK_CLOSED:

33         querry_flag=0;

34         socket(s,Sn_MR_TCP,localport,Sn_MR_ND);

35         break;

36     }

37 }

由于苹果的AirPlay协议为了防止其他未经苹果允许的设备的接入,对传输的数据用非对称性RSA加密算法进行加密,非对称性的意思就是加密和解密用的不是同一份密钥,RSA加密算法的密钥分为公钥和私钥,两者内容不同,用途也不同。公钥用于加密,一般交给客户端使用;私钥用于解密,一般由服务器管理。iPhone中存有公钥用来对iPhone输出的数据流进行加密,接收端设备利用私钥对接收的数据(音频)流进行解密。W5500EVB是作为服务器接收数据所以我们只需要知道私钥就可以解析数据,我们可以直接百度网上已有大神破译出的私钥。RSA加密算法的实现可以参考开源项目https://github.com/juhovh/shAirPlay工程中的RSA加密解密相关函数。

iPhone会先发送OPTIONS请求来确定AirPlay设备(W5500EVB)支持的方法,W5500EVB回复支持的全部方法包含ANNOUNCE,SETUP,RECORD,PAUSE,FLUSH,TEARDOWN,OPTIONS,GET_PARAMETER,SET_PARAMETER等,方法具体含义可参考RTSP协议相关文档。下面分别是由iPhone发出的OPTIONS请求报文和AirPlay设备回复的OPTIONS响应报文

iPhone OPTIONS 请求报文:

OPTIONS * RTSP/1.0

CSeq: 0

DACP-ID: 4CB06073C86450D8

Active-Remote: 2937221397

User-Agent: AirPlay/373.9.1

AirPlay设备OPTIONS响应报文:

RTSP/1.0 200 OK

CSeq: 0

Apple-Jack-Status: connected; type=analog

Public:ANNOUNCE,SETUP,RECORD,PAUSE,FLUSH,TEARDOWN,OPTIONS,GET_PARAMETER,SET_PARAMETER

iPhone收到W5500EVB的响应报文后,会向W5500EVB发送包含Apple-Challenge的OPTIONS数据包,Apple-Challenge后的参数是随机生成一个字符串且经过了RSA算法加密,W5500EVB要将Apple-Challenge中的参数先进行base64解码,解码后的数据尾部添加W5500EVB的IP地址和MAC地址然后通过RSA私钥加密后用base64编码,W5500EVB将加密处理后的数据作为Apple-Response的参数发送给iPhone,iPhone对该数据进行验证,数据正确则进行下一步,数据不正确则断开连接。下图为包含Apple-Challenge的OPTIONS 数据包:

OPTIONS * RTSP/1.0

Apple-Challenge: UJPWMzMloBFr98cQQHX3OQ==

CSeq: 2

DACP-ID: 4CB06073C86450D8

Active-Remote: 2937221397

User-Agent: AirPlay/373.9.1

接收到OPTIONS数据包后,截取Apple-Challenge相关数据,并进行解密代码如下:

1if(strstr(rcv_buffer,"Apple-Challenge:")!=NULL)

2 {

3     rsakey_t *rsakey;

4     rsakey = rsakey_init_pem(pemstr);

5     if (!rsakey) {

6         printf("Initializing RSA failed\n");

7         return;

8     }

9     memset(response,0x00,1024);

10     /*获取Apple-Challenge参数*/

11     mid(rcv_buffer,"Apple-Challenge: ","\r\n",CHALLENGE);

12     /*获取加密Apple-Response*/

13     rsakey_sign(rsakey, response, sizeof(response), CHALLENGE,ipaddr, sizeof(ipaddr), hwaddr, sizeof(hwaddr));

14     mid(rcv_buffer,"CSeq: ","\r\n",CHALLENGE);

15     sprintf(send_buffer,"RTSP/1.0 200 OK\r\nCSeq: %s\r\nApple-Jack-Status:connected; type=analog\r\nApple-Response: %s\r\nPublic: ANNOUNCE, SETUP,RECORD,PAUSE, FLUSH, TEARDOWN, OPTIONS,SET_PARAMETER\r\n\r\n",CHALLENGE,response);

16 }

通过11行处的mid()函数来获取Apple-Challenge后的参数然后14行处的rsakey_sign()函数对获取数据进行加密解密,15行处完成对RTSP响应报文的拼接。拼接生成的报文如下图所示:

RTSP/1.0 200 OK

CSeq: 2

Apple-Jack-Status: connected; type=analog

Apple-Response:Dw5Jrbs1mhjks3YErCo1tSOUV8/G8pOOShS3dUocjWzDGQR6DfqiSEovks+G4nHmCw9BccjlpVHzzRUINYZenWhUy8zlGsVGNwuO4okfi86PjGp5VAS6RPeYbW/CpAPgrzpDsVCblSGt8kQbn+sWuku9WMfa4gYU82DgfmL3laphZlidEIZd8D6FwzAth4pbRdtL3N8GuM2kWGRSpT6FL4VGk326a58g0kUNqNDxHp0fTa4ijk8VORzkyKO9ByFeysmZqGDBurLuSvDoAs0c1zR9aHAIXfJkWd0Ii3WviC2F0+vEODcRgOh7gOvy/i5+OOTiUfvHiDFIqlhVCRnZ2g

Public:ANNOUNCE,SETUP,RECORD,PAUSE,FLUSH,TEARDOWN,OPTIONS,SET_PARAMETER

iPhone收到AirPlay设备的response后,如果验证Apple-Response数据正确,成功完整设备间的握手,下一步就是传输音频数据给AirPlay设备。

2、音频数据接收与解码

iPhone与AirPlay设备连接成功后,就开始通过UDP协议发送音频数据但是iPhone通过AirPlay传输的音数据都是加密过的,对于接收端来说,需要正确解密后才能对音视频数据进行处理。音频数据采用AES CBC128算法进行加密,该算法解密时需输入参数rsaaeskey、aeskiv,这两个参数通过解析iPhone基于RTSP协议所发送的ANNOUNCE请求报文来获取, ANNOUNCE在传输的时候遵循了SDP协议。SDP协议用来描述媒体信息,下图是ANNOUNCE请求报文

ANNOUNCE rtsp://192.168.1.150/1561243076001349804 RTSP/1.0

Content-Length: 652

Content-Type: application/sdp

CSeq: 3

DACP-ID: 4CB06073C86450D8

Active-Remote: 2937221397

User-Agent: AirPlay/373.9.1

v=0

o=AirTunes 1561243076001349804 0 IN IP4 192.168.1.100

s=AirTunes

i=Wenlong... iPhone

c=IN IP4 192.168.1.100

t=0 0

m=audio 0 RTP/AVP 96

a=rtpmap:96 AppleLossless

a=fmtp:96 352 0 16 40 10 14 2 255 0 0 44100

a=rsaaeskey:bx0eKFGbphzETu16PLtXyP8s2CDKHpjIclJCmChdw6b12YSEvzDR3jlQwTWQdRRRrr99cek6JzdE0pgv0TzAF++FK8g63la8H9ioEcLFq84zWT/7atIlPNFC7RELlQG5ff/yTXHJ7LkzxQF12DvzQzIPd8GMx5ik/rxnLObZ+GQAbB2xtW/By2JT5gapEMBsx8+t+0sZXNwA3GXrjcjF+h6+oAD37A3U04rR/iK+Pvzglvy/13ZOrXL1VJpTkE1O+TIflAzfl0BkBbtfd3lX/+Te+Og8+gXXe516Dg4/v1Veddj4HQYZ/vrxE/qYFGDZIFZUdmpBtmtVMqAYwt1n5w==

a=aesiv:UohAefAQLdnT4BIBimuhfg==

a=min-latency:11025

a=max-latency:88200

W5500EVB解析收到ANNOUNCE请求包获取rsaaeskey,aesiv并解码。

1 void raop_announce(char *recv_buffer)

2 {

3     mid(recv_buffer,"Active-Remote: ","\r\n",remotestr);

4     mid(recv_buffer,"rtpmap:","\r\n",rtpmapstr );

5     mid(recv_buffer,"fmtp:","\r\n",fmtpstr);

6     mid(recv_buffer,"rsaaeskey:","\r\n",rsaaeskeystr);

7     mid(recv_buffer,"aesiv:","\r\n",aesivstr);

8     /*解码aeskey*/

9     rsakey_decrypt(rsakey, aeskey, sizeof(aeskey), rsaaeskeystr);

10     /*解码aesiv*/

11     rsakey_decode(rsakey, aesiv, sizeof(aesiv), aesivstr);

12     /*init alac*/

13     raop_buffer_init(&alac,fmtpstr);

14     return;

15 }

iPhone会继续向W5500EVB发送SETUP数据包,数据包中包含timing_port 与control_port。timing_port 用来传输 AirPlay 的时间同步包,同时也可以主动向iPhone请求当前的时间戳来校准流的时间戳。control_port是用来发送resendTransmit Request的端口,也就是当接收端发现收到的音乐流数据包中有丢失帧的时候,可以通过 control port 发送 resendTransmit 的 request 给iPhone,iPhone收到后会将帧在 response 中补发回来。下面的分别是iPhone发送的SETUP报文及W5500EVB回复的SETUP响应报文。

SETUP rtsp://192.168.1.150/1561243076001349804 RTSP/1.0

Transport: RTP/AVP/UDP;unicast;mode=record;timing_port=55703;control_port=56616

CSeq: 4

DACP-ID: 4CB06073C86450D8

Active-Remote: 2937221397

User-Agent: AirPlay/373.9.1

W5500EVB回复的响应报文中的server_port, server port用来传输音频流数据包。

RTSP/1.0 200 OK

CSeq: 4

Apple-Jack-Status: connected; type=analog

Transport: RTP/AVP/UDP;unicast;mode=record;timing_port=56461;events;control_port=51196;server_port=55641

Session:DEADBEEF

SETUP数据包确定音频流传输方式与传输端口号后,iPhone就开始发送音频数据到W5500EVB指定的server_port 55641端口,W5500EVB接收音频数据,通过解密过程后,我们会得到AAC编码的音频数据,播放器播放AAC数据还需要对其进行解码,话不多说,直接通过部分代码来说明音频解密过程。

1 int  decode_audio_data(unsigned char *data, unsigned short

2 datalen, int use_seqnum)

3 {

4     unsigned short seqnum;

5     raop_buffer_entry_t entry;

6     int encryptedlen;

7     AES_CTX aes_ctx;

8     int outputlen;

9     /* Check packet data length is valid */

10     if (datalen < 12 || datalen > 1472) {

11         return -1;

12     }

13     /* Get correct seqnum for the packet */

14     if (use_seqnum) {

15         seqnum = (data[2] << 8) | data[3];

16     }

17     /* Update the raop_buffer entry header */

18     entry.flags = data[0];

19     entry.type = data[1];

20     entry.seqnum = seqnum;

21     entry.timestamp = (data[4] << 24) | (data[5] << 16) |

22                       (data[6] << 8) | data[7];

23     entry.ssrc = (data[8] << 24) | (data[9] << 16) |

24                  (data[10] << 8) | data[11];

25     entry.available = 1;

26     /* Decrypt audio data */

27     encryptedlen = (datalen-12)/16*16;

28     AES_set_key(&aes_ctx, aeskey, aesiv, AES_MODE_128);

29     AES_convert_key(&aes_ctx);

30     memset(packetbuf,0,sizeof(data));

31     AES_cbc_decrypt(&aes_ctx, &data[12], (uint8*)packetbuf,

32     encryptedlen);

33     memcpy(packetbuf+encryptedlen, &data[12+encryptedlen],

34     datalen-12-encryptedlen);

35     /* Decode ALAC audio data */

36     outputlen = audio_buffer_size;

37     alac_decode_frame(&alac, (uint8*)packetbuf ,audiobuf,

38     &outputlen);

39     entry.audio_buffer_len = outputlen;

40     return outputlen;

41 }

在程序中W5500EVB通过UDP端口每收到数据包先会判断数据包的长度是否小于12因为RTP的包头为12个字节,小于12字节就会直接丢弃掉,大于12字节且小于1472(UDP包的最大长度)就会通过31行AES_cbc_decrypt()函数的对数据解密然后把解密后的数据通过alac_decode_frame()函数转换为PCM5102A模块可播放的数据并将数据存储在audiobuf中等待发送给音频模块,返回可播放数据长度outputlen,该值在我们初始化I2S的DMA功能时会用到。

3、音频数据的播放

音频播放采用的是PCM5102A的DAC模块,该模块是通过I2S接口进行通信,由于STM32的I2S2的针脚与SPI2复用,而SPI2已经用来与W5500进行通信,所以我们只能选择I2S3接口, W5500EVB与PCM5102A模块连接示意图如下所示:

图6 模块硬件连接图

程序上直接将解码后的数据发送到PCM5102A模块即可。为了能与PCM512A模块正常通信要初始化W5500EVB的I2S3接口,需要注意的是I2S3接口的时钟脚PB3,该引脚默认为JTAG的JTDO脚,初始化时需要禁止JTAG以使PB3能够作为I2S3的时钟脚,初始化代码如下所示:

1 void I2S_Config(void)

2 {

3     I2S_InitTypeDef I2S_InitStructure;

4     GPIO_InitTypeDef GPIO_InitStruct;

5

6     /*Init GPIO*/

7     RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI3, ENABLE);

8     /*SPI*/

9     RCC_APB2PeriphClockCmd(

10     RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB|RCC_APB2Periph_

11     GPIOC|RCC_APB2Periph_AFIO, ENABLE);

12     GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable,ENABLE);

13     /*GPIO_Pin7 --> I2S_MCK*/

14     GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7;

15     GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;

16     GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;

17     GPIO_Init(GPIOC, &GPIO_InitStruct);

18     /*GPIO_Pin_15 -->I2S3_WS*/

19     GPIO_InitStruct.GPIO_Pin = GPIO_Pin_15;

20     GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;

21     GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;

22     GPIO_Init(GPIOA, &GPIO_InitStruct);

23     /*GPIO_Pin_3 -->I2S3_CK

24       GPIO_Pin_5 -->I2S3_SD

25     */

26     GPIO_InitStruct.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_5;

27     GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;

28     GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;

29     GPIO_Init(GPIOB, &GPIO_InitStruct);

30     /*Init IIS*/

31     SPI_I2S_DeInit(SPI3);

32     I2S_InitStructure.I2S_Mode = I2S_Mode_MasterTx;

33     I2S_InitStructure.I2S_Standard = I2S_Standard_Phillips;

34     I2S_InitStructure.I2S_DataFormat = I2S_DataFormat_16b;

35     I2S_InitStructure.I2S_MCLKOutput=I2S_MCLKOutput_Disable;

36     I2S_InitStructure.I2S_AudioFreq = I2S_AudioFreq_44k;

37     /*I2S clock steady state is low level */

38     I2S_InitStructure.I2S_CPOL = I2S_CPOL_Low;

39     I2S_Init(SPI3, &I2S_InitStructure);

40     I2S_Cmd(SPI3, ENABLE);

41 }

代码12行处通过调用GPIO_PinRemapConfig()函数禁用JTAG, 32行处模式配置为主设备发送I2S_Mode_MasterTx,通信标准设置为I2S_Standard_Phillips,数据格式为标准16位格式I2S_DataFormat_16b,采样频率设置为44kHz I2S_AudioFreq_44k, I2S时钟线空闲状态的为低电平。

为了提高数据的传输速度与效率,要打开IIS的DMA发送功能,每次发送SPI_I2S_DMAReq_Tx 请求后会将指定的buf0内的数据发送到SPI3的DR数据寄存器。该函数是buf0即为存储音频数据的audiobuf,需要特别注意的是:我们的数据是按照16bit传送的,而audiobuf内的数据为uint8型,所以 num值为audiobuf内的有效数据长度/2。

1 void I2S2_TX_DMA_Init(u8* buf0,u16 num)

2 {

3   NVIC_InitTypeDef   NVIC_InitStructure;

4   DMA_InitTypeDef  DMA_InitStructure;

5   RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE);

6   DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)(&SPI3->DR);

7   DMA_InitStructure.DMA_MemoryBaseAddr = (u32)buf0;

8   DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;

9   DMA_InitStructure.DMA_BufferSize = num;

10   DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;

11   DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;

12 DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;

13   DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;

14   DMA_InitStructure.DMA_Mode = DMA_Mode_Circular   ;

15   DMA_InitStructure.DMA_Priority = DMA_Priority_High;

16   DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

17   DMA_Init(DMA2_Channel2, &DMA_InitStructure);

18   DMA_Cmd(DMA2_Channel2, ENABLE);

19   SPI_I2S_DMACmd(SPI3,SPI_I2S_DMAReq_Tx,ENABLE);

20 }

音频流的处理过程为通过UDP接收音频数据包,然后对收到的数据包进行解码,并将解码后的数据存储到audiobuf,通过I2S3的DMA功能将数据发送到PCM5102A模块,代码如下所示:

1 void do_raop(uint8 s)

2 {

3     int outputlen;

4     uint8 ip[4];

5     uint16 len, port;

6     switch (getSn_SR(s)) {

7     case SOCK_UDP:

8         if ((len = getSn_RX_RSR(s)) > 0) {

9             /*接收音频数据*/

10             recvfrom(s,buffer,len,ip,&port);

11             /*解码收到的音频数据*/

12             outputlen=decode_audio_data(buffer, len ,1);

13             /*配置DMA*/

14             I2S2_TX_DMA_Init((uint8*)audiobuf,outputlen/2);

15         }

16         break;

17     case SOCK_CLOSED:

18         socket(s, Sn_MR_UDP,55641,0);

19         break;

20     }

21 }

将编译好的程序下载到W5500EVB,将耳机插入PCM5102A模块,然后用iPhone手机搜索并连接W5500EVB设备,点击播放音乐就可以用耳机或者音响听音乐了,玄学部分直接略过,老音箱终于有枯木逢春的感觉了。由于时间匆忙,本文的项目中虽然实现了通过AirPlay播放音乐,但还有很大的优化空间,例如对各个音乐播放器的兼容性问题,QQ音乐、网易音乐等实现都不太一样(本文中用的是网易云音乐);音乐播放过程中的音量设置问题;音乐播放过程中的噪音问题等等。而且由于手上只有带STM32F103的W5500EVB开发板,STM32F103芯片在运行加密解密时会比较慢,RAM空间也比较小,偶尔出现的卡顿就是由于这部分原因造成的。而相对来说,可能使用WiFi会更适合本方案,我也会继续优化此项目以实现更稳定的播放效果。

基于STM32和W5500实现AirPlay音频播放相关推荐

  1. 基于STM32F103C8T6片内Flash的音频播放(DAC通道)

    基于STM32F103C8T6片内Flash的音频播放(DAC通道) 一.STM32的DAC通道介绍 1.DAC 简介 2.STM32中的DAC主要特点 3.DAC 功能框图剖析 4.DAC配置 二. ...

  2. 基于STM32和W5500的Modbus TCP通讯

     在最近的一个项目中需要实现Modbus TCP通讯,而选用的硬件平台则是STM32F103和W5500,软件平台则选用IAR EWAR6.4来实现. 1.移植前的准备工作 为了实现Modbus ...

  3. 基于stm32之w5500以太网应用

    基于stm32之w5500以太网应用 强调一下前半段为基础知识普及(这段还是很重要的.不管用什么工具开发精通协议才是王道),后半段为实战代码干货. 如上图所示,最底下的一层叫做"物理层&qu ...

  4. 【Arduino + Linux】基于 Helix 解码库实现 MP3 音频播放

    目录 一.MP3 文件结构 1.1.ID3V2.3 1.1.1.标签头 1.1.2.扩展标签头 1.1.3.标签帧 1.2.音频数据 1.3.ID3V1 1.4.MP3文件结构图 二.MP3 解码库 ...

  5. 基于STM32单片机实现多功能MP3播放器系统设计

    百度网盘下载地址(942):点击下载 本项目是基于STM32F103来制作一个多功能MP3,除了可以实现MP3播放的基本功能之外,同时拥有丰富并实用的外扩功能,整个系统的功能包括:MP3播放功能.收音 ...

  6. Linux下实现苹果AirPlay音频服务器

    一.背景 背景:在华清学习之余,自行研究了智能家居的东西,为了解决智能家居中背景音乐问题研究如下:调查发现现有技术中有DLNA.AirPlay.Miracast三种.文章后有些项并未验证,后续慢慢实验 ...

  7. 嵌入式linux音频播放器设计,基于嵌入式Linux下Madplay音频播放器设计论文.docx

    基于嵌入式Linux下Madplay音频播放器设计论文 滁州职业技术学院计算机应用技术专业毕业论文PAGE I 滁州职业技术学院信息工程系--2015届计算机应用专业毕业论文 姓 名: 周杰 班 级: ...

  8. DAC+DMA+TIM实现音频播放问题记录

    目录 1. 概述 2. 音频采样率 2.1 定时器触发周期 2.2 音频文件的格式 3. DAC的左对齐和右对齐 3.1 为什么要使用左对齐 3.2 左对齐数据的读写 3.3 音频数据的使用 3.3. ...

  9. android 音频播放过程,一种Android系统中的音频播放方法与流程

    本申请涉及android系统技术,特别涉及一种android系统中的音频播放方法. 背景技术: 在android系统中,现有的使用audiotrack进行音频播放时,audiotrack应用与andr ...

  10. 10个jQuery HTML5音频播放器

    根据Buzz Angle Music的数据,2017年第一季度,仅美国就消费了830亿个音频流.这比上一季度增长了61.2%. 同时,音乐销量下降了23.8%. 2017年5月8日:此热门文章已更新, ...

最新文章

  1. 树套树 ----- P1975 [国家集训队]排队(树状数组套权值线段树求动态逆序对)
  2. 存储过程内建临时表和临时函数,合并一个由存储过程返回的表
  3. 通过idea将maven工程转为web项目
  4. 浓烟滚滚!某市联通集体断网,谁的锅?
  5. 成年人的低头,从拼多多开始
  6. Redis分布式锁加时效和不加时效两种方案的最全代码实现
  7. 2656: [Zjoi2012]数列(sequence)(递归+高精度)
  8. 美国计算机科学专业申请要求,美国计算机科学专业好申请吗?申请要求高不高...
  9. [JDK]找不到或无法加载主类 java
  10. 解决Linux因非正常关机或死机重启后进入 initramfs 问题
  11. 消费流程图_SpringBoot+RabbitMQ ,保证消息100%投递成功并被消费(附源码)
  12. 内核kernel以及根文件系统rootfs是如何映射到对应的nand flash的
  13. try catch finally的执行顺序到底是怎样的?
  14. java画虚线_在java中绘制虚线
  15. neatupload上传文件配置
  16. 我的世界夜视指令java_我的世界状态效果大全及指令使用方法
  17. 操作系统的工作流程(流程图表示)
  18. hdoj 4747 线段树
  19. 咨询博客园文章如何维权
  20. 北京今年将新建提升1000个生活性服务业网点

热门文章

  1. oracle sql列转行_oracle 行转列 列转行 转载
  2. 自制solidworks图框步骤_SolidWorks教你如何快速制作工程图模板
  3. 论文解读-用于人口流动数据模拟的人口流动模型
  4. t检验及python代码实现
  5. C++ gbk与utf8互转
  6. 非越狱逆向开发总结文档(含iOS Extension)
  7. librdkafka问题小记
  8. 机器人动力学-拉格朗日方程
  9. php读取 rinex,用Pandas读取GPS RINEX数据
  10. C语言输出图形:宝塔形(三角形)回文数字。即:第一行1,第二行121,第三行12321……