STM32实现Airplay音乐播放器

2022-10-17,,,

airplay是苹果公司推出的一套无线音乐解决方案,我们手里的iphone、ipad甚至是apple watch等设备还有电脑上的itunes都支持airplay,但是支持airplay功能的音响设备都是比较贵的,荷包扁扁的我自然是感觉买那么贵的音响实在是不合算。前两天突发奇想,如果stm32可以支持airplay的功能,那么不就可以让我享受一把无线音乐的自由自在了吗?于是马上登陆github搜了一下,发现还真有解决方案不过基本上所有的方案都是在linux或者windows上运行的,精挑细选之后选择了这个airplay开源项目,主要是该代码是用c语言实现移植到stm32比较方便。

在开始之前我们有必要先了解一下airplay, airplay是苹果公司收购airtunes后升级airtunes的协议库,在airtunes增加了视频,照片的传输,完整的变为airplay非开源功能,实现随时随地的家庭音乐无线流媒体传输。airplay可以将iphone 、ipad、ipod touch 等ios 设备上的包括图片、音频、视频及镜像传输到支持airplay的设备中播放,airplay的实现过程中包含多个协议,其中有的协议是完全标准的, 有一部分协议进行了一些修改,有的则是完全私有的。

    • multicast dns用于发布服务, 启动后, 在ios的控制中心菜单中就能看到对应的设备;

    • http / rtsp / rtp  用于流媒体服务, 传输音视频数据, 进行播放控制等;

    • ntp 时间同步;

    • fairplay drm加密  完全私有的加密协议。

我们需要准备一部iphone手机并安装网易云音乐,w5500evb开发板(stm32f103+w5500),pcm5102a音频模块。iphone手机用来作为客户端搜索设备及发送音频数据,w5500evb是wiznet的开发板具有以太网功能用来作为服务器接收音频数据,开发板的操作可以参考www.w5500.com中的例程。pcm5102a音频模块是将解码后的音频数据进行播放。经过分析后我们要实现airplay音频播放主要是实现以下三个方面:

1、 iphone在网络中发现 w5500设备并建立连接

2、 w5500evb接收并解码音频数据

3、 w5500evb通过i2s接口将音频传送到pcm5102a音频模块

1、发现设备并建立连接

airplay发现设备是基于mdns协议实现,iphone与w5500evb需要连入同一网络且w5500evb要加入组播组224.0.0.251才可以接收mdns报文。w5500evb收到iphone发出的querry查询报文后回复response报文,报文的内容可以参考文档《unofficial airplay protocol specification》(http://nto.github.io/airplay.html),下方为mdns设备发现代码:

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

 2 {

 3     uint8 ip[4];

 4     uint16 len, port;

 5     switch (getsn_sr(s)) {

 6     case sock_udp:

 7         if ((len = getsn_rx_rsr(s)) > 0) {

 8             if (len > max_dns_buf_size) {

 9                 len = max_dns_buf_size;

10             }

11             len = recvfrom(s, bufpub, len, ip, &port);

12             len=dns_makequery(0,name,rname,bufpub,max_dns_buf_size);

13             sendto(s, bufpub, len, dip,dport);

14             len=dns_makeresponse(0,name,rname,bufpub,max_dns_buf_size);

15             sendto(s, bufpub, len, dip,dport);

16         }

17         break;

18     case sock_closed:

19         setdipr(s,dip);/* 设置目标ip 224.0.0.251*/

20         setdhar(s,dhar);/*设置目标mac 01:00:5e:00:00:fb*/

21         setdport(s,dport);/*设置目标端口5353*/

22         socket(s,sn_mr_udp, 5353,sn_mr_multi);/*打开socket加入组播组*/

23         break;

24     }

25     return dns_ret_progress;

26 }

代码中12行的dns_makequery()函数用来拼接查询报文,代码14行dns_makeresponse()函数用来拼接响应报文,我们将代码编译下载到w5500evb中运行,打开iphone的选项列表点击音乐选项会显示如下图所示的界面,点击右上方标志搜索同一网络下的设备。界面如下图所示:

图1-1 iphone选项列表

此时iphone向224.0.0.251组播组发送querry查询报文,w5500evb收到查询报文后向224.0.0.251组播组发送response响应报文。w5500evb发送的response响应报文中该报文中包含raop服务,该服务用于音频流的投影。 raop从本质上来说是实时流协议,只不过增加了身份验证请求-应答的步骤,raop服务用两个信道实现流媒体:一个是用实时流协议的控制信道;另一个是数据信道用来发送数据。raop服务名称格式:mac地址@设备名._raop._tcp.local。通过抓包工具抓取响应报文我们可以看到raop服务的相关信息。

 

图1-2 raop服务报文

service字段是服务名称。protocol服务的类别:_airplay是视频服务(未用到),_raop是音频服务。name说明数据传输的协议,可以通过tcp或者udp传输。port声明了rtsp命令交互的端口号为5005,客户端可以通过端口号与服务端建立连接。下图中wiznet就是iphone发现的支持airplay的设备(w5500evb)。

图1-3 iphone发现设备

 

iphone成功发现w500  evb设备后就需要连接设备,此时我们点击列表中显示的设备,连接成功后对应设备的后面会显示对勾,如下图所示:

 

图1-4 iphone连接设备

上文介绍的iphone发现设备设备的过程中指定了rtsp是通过tcp进行通信且端口号为5005,所以我们要创建一个端口号为5005的tcp服务器来接收数据包,对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         }

26

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请求来确定w5500evb支持的方法,w5500evb回复支持的全部方法包含announce, setup, record, pause, flush, teardown, options, get_parameter, set_parameter等,方法具体含义可参考rtsp协议相关文档。

iphone options 请求报文:

options * rtsp/1.0

cseq: 0

dacp-id: 4cb06073c86450d8

active-remote: 2937221397

user-agent: airplay/373.9.1

图1-5 options请求报文

w5500evb响应报文:

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

图1-6 options响应报文

iphone收到w5500evb的响应后,会向w5500evb发送包含apple-challenge的options数据包,apple-challenge后的参数是随机生成且经过了rsa算法加密,w550evb要将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

图1-7 apple-challenge报文

 

接收到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

图1-8 apple-response报文

 

iphone收到w5500evb的response后,对apple-response后的内容后进行解析校验,校验结果正确则设备连接成功可以继续发送数据否则断开连接。

2、音频数据接收与解码

iphone与w5500evb建立连接成功后,就开始通过udp协议发送音频数据但是iphone通过airplay传输的音数据都是加密过的,对于接收端来说,需要正确解密后才能对音视频数据进行处理。音频数据采用aes cbc128算法进行加密,该算法解密时需输入参数rsaaeskey、aeskiv,这两个参数通过解析iphone发送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

图2-1 announce报文

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 中补发回来。

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

图2-2 setup报文

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

图2-3 setup响应报文

 

   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接口进行通信,直接将解码后的数据发送到pcm5102a模块即可。为了能与pcm512a模块正常通信要初始化w5500evb的iis接口,项目中中使用到的是i2s3接口,需要注意的是i2s3接口的时钟脚pb3,该引脚默认为jtag的jtdo脚,初始化时需要禁止jtag以使pb3能够作为i2s的时钟脚,初始化代码如下所示:

 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 philips标准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的spi2口用来与w5500进行通信所以我们只能选择i2s3接口,i2s,w5500evb与pcm5102a模块连接示意图如下所示:

 

图3-1硬件连接图

 

将编译好的程序下载到w5500evb,将耳机插入pcm5102a模块,用iphone手机搜索并连接w5500evb设备,点击播放音乐就可以用耳机听音乐了。

本文的项目中只是简单的实现了通过airplay播放音乐,由于时间匆忙项目功能还可以继续优化例如对各个音乐播放器的兼容性问题,qq音乐、网易音乐等实现都不太一样本文项目中用的是网易云音乐;音乐播放过程中的音量设置问题,;音乐播放过程中的噪音问题,由于手上只有带stm32f103 的w5500evb开发板,f103芯片在运行加密解密时会比较慢ram空间也比较小等。大家如果想要做的话建议选用处理速度快一些的芯片。

 

《STM32实现Airplay音乐播放器.doc》

下载本文的Word格式文档,以方便收藏与打印。