当前位置: > 投稿>正文

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

04-30 互联网 未知 投稿

关于【正点原子linux 开发版评测】,今天乾乾小编给您分享一下,如果对您有所帮助别忘了关注本站哦。

1、正点原子linux 开发版评测:正点原子预售款STM32MP157开发板

正点原子STM32MP157开发板Linux板双A7+M4异构双核STM32

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

本次试用名额由厂家确定,且试用产品不回收

2、正点原子linux 开发版评测,正点原子I.MX6U嵌入式Linux

音频应用编程

ALPHA I.MX6U开发板支持音频,板上搭载了音频编解码芯片WM8960,支持播放以及录音功能!

本章我们来学习Linux下的音频应用编程,音频应用编程相比于前面几个章节所介绍的内容、其难度有所上升,但是笔者仅向大家介绍Linux音频应用编程中的基础知识,而更多细节、更加深入的内容需要大家自己去学习。

本章将会讨论如下主题内容。

  • Linux下ALSA框架概述;
  • alsa-lib库介绍;
  • alsa-lib库移植;
  • alsa-lib库的使用;
  • 音频应用编程之播放;
  • 音频应用编程之录音。
ALSA概述

ALSA是Advanced Linux Sound Architecture(高级的Linux声音体系)的缩写,目前已经成为了linux下的主流音频体系架构,提供了音频和MIDI的支持,替代了原先旧版本中的OSS(开发声音系统);学习过Linux音频驱动开发的读者肯定知道这个;事实上,ALSA是Linux系统下一套标准的、先进的音频驱动框架,那么这套框架的设计本身是比较复杂的,采用分离、分层思想设计而成,具体的细节便不给大家介绍了!作为音频应用编程,我们不用去研究这个。

在应用层,ALSA为我们提供了一套标准的API,应用程序只需要调用这些API就可完成对底层音频硬件设备的控制,譬如播放、录音等,这一套API称为alsa-lib。如下图所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.1.1 alsa音频示意图

alsa-lib简介

如上所述,alsa-lib是一套Linux应用层的C语言函数库,为音频应用程序开发提供了一套统一、标准的接口,应用程序只需调用这一套API即可完成对底层声卡设备的操控,譬如播放与录音。

用户空间的alsa-lib对应用程序提供了统一的API接口,这样可以隐藏驱动层的实现细节,简化了应用程序的实现难度、无需应用程序开发人员直接去读写音频设备节点。所以本章,对于我们来说,学习音频应用编程其实就是学习alsa-lib库函数的使用、如何基于alsa-lib库函数开发音频应用程序。

ALSA提供了关于alsa-lib的使用说明文档,其链接地址为:https://www.alsa-project.org/alsa-doc/alsa-lib/,进入到该链接地址后,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.2.1 alsa-lib使用参考手册

alsa-lib库支持功能比较多,提供了丰富的API接口供应用程序开发人员调用,根据函数的功能、作用将这些API进行了分类,可以点击上图中Modules按钮查看其模块划分,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.2.2 alsa-lib模块

一个分类就是一个模块(module),有些模块下可能该包含了子模块,譬如上图中,模块名称前面有三角箭头的表示该模块包含有子模块。

  • Global defines and functions:包括一些全局的定义,譬如函数、宏等;
  • Constants for Digital Audio Interfaces:数字音频接口相关的常量;
  • Input Interface:输入接口;
  • Output Interface:输出接口;
  • Error handling:错误处理相关接口;
  • Configuration Interface:配置接口;
  • Control Interface:控制接口;
  • PCM Interface:PCM设备接口;
  • RawMidi Interface:RawMidi接口;
  • Timer Interface:定时器接口;
  • Hardware Dependant Interface:硬件相关接口;
  • MIDI Sequencer:MIDI音序器;
  • External PCM plugin SDK:外部PCM插件SDK;
  • External Control Plugin SDK:外部控制插件SDK;
  • Mixer Interface:混音器接口;
  • Use Case Interface:用例接口;
  • Topology Interface:拓扑接口。

可以看到,alsa-lib提供的接口确实非常多、模块很多,以上所列举出来的这些模块,很多模块笔者也不是很清楚它们的具体功能、作用,但是本章我们仅涉及到三个模块下的API函数,包括:PCM Interface、Error Interface以及Mixer Interface。

PCM Interface

PCM Interface,提供了PCM设备相关的操作接口,譬如打开/关闭PCM设备、配置PCM设备硬件或软件参数、控制PCM设备(启动、暂停、恢复、写入/读取数据),该模块下还包含了一些子模块,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.2.3 PCM Interface下的子模块

点击模块名称可以查看到该模块提供的API接口有哪些以及相应的函数说明,这里就不给大家演示了!

Error Interface

该模块提供了关于错误处理相关的接口,譬如函数调用发生错误时,可调用该模块下提供的函数打印错误描述信息。

Mixer Interface

提供了关于混音器相关的一系列操作接口,譬如音量、声道控制、增益等等。

sound设备节点

在Linux内核设备驱动层、基于ALSA音频驱动框架注册的sound设备会在/dev/snd目录下生成相应的设备节点文件,譬如ALPHA I.MX6U开发板出厂系统/dev/snd目录下有如下文件:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.3.1 /dev/snd目录下的文件

Tips:注意,Mini I.MX6U开发板出厂系统/dev/snd目录下是没有这些文件的,因为Mini板不支持音频、没有板载音频编解码芯片,所以本章实验例程无法在Mini板上进行测试,请悉知!

从上图可以看到有如下设备文件:

  • controlC0:用于声卡控制的设备节点,譬如通道选择、混音器、麦克风的控制等,C0表示声卡0(card0);
  • pcmC0D0c:用于录音的PCM设备节点。其中C0表示card0,也就是声卡0;而D0表示device 0,也就是设备0;最后一个字母c是capture的缩写,表示录音;所以pcmC0D0c便是系统的声卡0中的录音设备0;
  • pcmC0D0p:用于播放(或叫放音、回放)的PCM设备节点。其中C0表示card0,也就是声卡0;而D0表示device 0,也就是设备0;最后一个字母p是playback的缩写,表示播放;所以pcmC0D0p便是系统的声卡0中的播放设备0;
  • pcmC0D1c:用于录音的PCM设备节点。对应系统的声卡0中的录音设备1;
  • pcmC0D1p:用于播放的PCM设备节点。对应系统的声卡0中的播放设备1。
  • timer:定时器。

本章我们编写的应用程序,虽然是调用alsa-lib库函数去控制底层音频硬件,但最终也是落实到对sound设备节点的I/O操作,只不过alsa-lib已经帮我们封装好了。在Linux系统的/proc/asound目录下,有很多的文件,这些文件记录了系统中声卡相关的信息,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.3.2 /proc/asound目录下的文件

cards:

通过"cat /proc/asound/cards"命令、查看cards文件的内容,可列出系统中可用的、注册的声卡,如下所示:

cat /proc/asound/cards

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.3.3 查看系统中注册的所有声卡

我们的阿尔法板子上只有一个声卡(WM8960音频编解码器),所以它的编号为0,也就是card0。系统中注册的所有声卡都会在/proc/asound/目录下存在一个相应的目录,该目录的命名方式为cardX(X表示声卡的编号),譬如图 29.3.2中的card0;card0目录下记录了声卡0相关的信息,譬如声卡的名字以及声卡注册的PCM设备,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.3.4 card0目录下的文件

devices:

列出系统中所有声卡注册的设备,包括control、pcm、timer、seq等等。如下所示:

cat /proc/asound/devices

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.3.5 列出所有设备

pcm:

列出系统中的所有PCM设备,包括playback和capture:

cat /proc/asound/pcm

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.3.6 列出系统中所有PCM设备

alsa-lib移植

因为alsa-lib是ALSA提供的一套Linux下的C语言函数库,需要将alsa-lib移植到开发板上,这样基于alsa-lib编写的应用程序才能成功运行,除了移植alsa-lib库之外,通常还需要移植alsa-utils,alsa-utils包含了一些用于测试、配置声卡的工具。

事实上,ALPHA I.MX6U开发板出厂系统中已经移植了alsa-lib和alsa-utils,本章我们直接使用出厂系统移植好的alsa-lib和alsa-utils进行测试,笔者也就不再介绍移植过程了。其实它们的移植方法也非常简单,如果你想自己尝试移植,网上有很多参考,大家可以自己去看看。

alsa-utils提供了一些用于测试、配置声卡的工具,譬如aplay、arecord、alsactl、alsaloop、alsamixer、amixer等,在开发板出厂系统上可以直接使用这些工具,这些应用程序也都是基于alsa-lib编写的。

aplay

aplay是一个用于测试音频播放功能程序,可以使用aplay播放wav格式的音频文件,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.4.1 使用aplay播放wav音乐

程序运行之后就会开始播放音乐,因为ALPHA开发板支持喇叭和耳机自动切换,如果不插耳机默认从喇叭播放音乐,插上耳机以后喇叭就会停止播放,切换为耳机播放音乐,这个大家可以自己进行测试。

需要注意的是,aplay工具只能解析wav格式音频文件,不支持mp3格式解码,所以无法使用aplay工具播放mp3音频文件。稍后笔者会向大家介绍如何基于alsa-lib编写一个简单地音乐播放器,实现与aplay相同的效果。

alsamixer

alsamixer是一个很重要的工具,用于配置声卡的混音器,它是一个字符图形化的配置工具,直接在开发板串口终端运行alsamixer命令,打开图形化配置界面,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.4.2 alsamixer界面

alsamixer可对声卡的混音器进行配置,左上角“Card: wm8960-audio”表示当前配置的声卡为wm8960-audio,如果你的系统中注册了多个声卡,可以按F6进行选择。

按下H键可查看界面的操作说明,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.4.3 alsamixer界面操作说明

不同声卡支持的混音器配置选项是不同的,这个与具体硬件相关,需要硬件上的支持!上图展示的便是开发板WM8960声卡所支持的配置项,包括Playback播放和Capture录音,左上角View处提示:

View: F3:[Playback] F4: Capture F5: All

表示当前显示的是[Playback]的配置项,通过F4按键切换为Capture、或按F5显示所有配置项。

Tips:在终端按下F4或F5按键时,可能会直接退出配置界面,这个原因可能是F4或F5快捷键被其它程序给占用了,大家可以试试在Ubuntu系统下使用ssh远程登录开发板,然后在Ubuntu ssh终端执行alsamixer程序,笔者测试F4、F5都是正常的。

左上角Item处提示:

Item: Headphone [dB gain: -8.00, -8.00]

表示当前选择的是Headphone配置项,可通过键盘上的LEFT(向左)和RIGHT(向右)按键切换到其它配置项。当用户对配置项进行修改时,只能修改被选中的配置项,而中括号[dB gain: -7.00, -7.00]中的内容显示了该配置项当前的配置值。

上图中只是列出了其中一部分,还有一部分配置项并未显示出来,可以通过左右按键移动查看到其余配置项。WM8960声卡所支持的配置项特别多,包括播放音量、耳机音量、喇叭音量、capture录音音量、通道使能、ZC、AC、DC、ALC、3D等,配置项特别多,很多配置项笔者也不懂。以下列出了其中一些配置项及其说明:

Headphone:耳机音量,使用上(音量增加)、下(音量降低)按键可以调节播放时耳机输出的音量大小,当然可以通过Q(左声道音量增加)、Z(左声道音量降低)按键单独调节左声道音量或通过E(右声道音量增加)、C(右声道音量降低)按键单独调节右声道音量。

Headphone Playback ZC:耳机播放ZC(交流),通过M键打开或关闭ZC。

Speaker:喇叭播放音量,音量调节方法与Headphon相同。

Speaker AC:喇叭ZC,通过上下按键可调节大小。

Speaker DC:喇叭DC,通过上下按键可调节大小。

Speaker Playback ZC:喇叭播放ZC,通过M键打开或关闭ZC。

Playback:播放音量,播放音量作用于喇叭、也能作用于耳机,能同时控制喇叭和耳机的输出音量。调节方法与Headphon相同。

Capture:采集音量,也就是录音时的音量大小,调节方法与Headphon相同。

其它的配置项就不再介绍了,笔者也看不懂,后面会用到时再给大家解释!

开发板出厂系统中有一个配置文件/var/lib/alsa/asound.state,这其实就是WM8960声卡的配置文件,每当开发板启动进入系统时会自动读取该文件加载声卡配置;而每次系统关机时,又会将声卡当前的配置写入到该文件中进行保存,以便下一次启动时加载。加载与保存操作其实是通过alsactl工具完成的,稍后向大家介绍。

alsactl

配置好声卡之后,如果直接关机,下一次重启之后之前的设置都会消失,必须要重新设置,所以我们需要对配置进行保存,如何保存呢?可通过alsactl工具完成。

使用alsactl工具可以将当前声卡的配置保存在一个文件中,这个文件默认是/var/lib/alsa/asound.state,譬如使用alsactl工具将声卡配置保存在该文件中:

alsactl -f /var/lib/alsa/asound.state store

-f选项指定保存在哪一个文件中,当然也可以不用指定,如果不指定则使用alsactl默认的配置文件/var/lib/alsa/asound.state,store表示保存配置。保存成功以后就会生成/var/lib/alsa/asound.state这个文件,asound.state文件中保存了声卡的各种设置信息,大家可以打开此文件查看里面的内容,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.4.4 asound.state文件部分内容

除了保存配置之外,还可以加载配置,譬如使用/var/lib/alsa/asound.state文件中的配置信息来配置声卡,可执行如下命令:

alsactl -f /var/lib/alsa/asound.state restore

restore表示加载配置,读取/var/lib/alsa/asound.state文件中的配置信息并对声卡进行设置。关于alsactl的详细使用方法,可以执行"alsactl -h"进行查看。

开发板出厂系统每次开机启动时便会自动从/var/lib/alsa/asound.state文件中读取配置信息并配置声卡,而每次关机时(譬如执行reset或poweroff命令)又会将声卡当前的配置写入到该文件中进行保存,以便下一次启动时加载。其实也就是在系统启动(或关机)时通过alsactl工具加载(或保存)配置。

amixer

amixer工具也是一个声卡配置工具,与alsamixer功能相同,区别在于,alsamixer是一个基于字符图形化的配置工具、而amixer不是图形化配置工具,直接使用命令行配置即可,详细地用法大家可以执行"amixer --help"命令查看,下面笔者简单地提一下该工具怎么用:

执行命令"amixer scontrols"可以查看到有哪些配置项,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.4.5 查看有哪些配置项

从打印信息可知,这里打印出来的配置项与alsamixer配置界面中所看到的配置项是相同的,那如何进去配置呢?不同的配置项对应的配置方法(配置值或值类型)是不一样的,可以先使用命令"amixer scontents"查看配置项的说明,如下所示:

amixer scontents

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.4.6 每一个配置项的配置说明

“Headphone”配置项用于设置耳机音量,音量可调节范围为0-127,当前音量为115(左右声道都是115);有些设置项是bool类型,只有on和off两种状态。

譬如将耳机音量左右声道都设置为100,可执行如下命令进行设置:

amixer sset Headphone 100,100

譬如打开或关闭Headphone Playback ZC:

amixer sset "Headphone Playback ZC" off #关闭ZCamixer sset "Headphone Playback ZC" on #打开ZC

以上给大家举了两个例子,配置方法还是很简单地!

arecord

arecord工具是一个用于录音测试的应用程序,这里笔者简单地给大家介绍一下工具的使用方法,详细的使用方法大家可以执行"arecord --help"命令查看帮助信息。譬如使用arecord录制一段10秒钟的音频,可以执行如下命令:

arecord -f cd -d 10 test.wav

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.4.7 使用arecord工具录音

-f选项指定音频格式,cd则表示cd级别音频,也就是“16 bit little endian, 44100, stereo”;-d选项指定音频录制时间长度,单位是秒;test.wav指定音频数据保存的文件。当录制完成之后,会生成test.wav文件,接着我们可以使用aplay工具播放这一段音频。

以上给大家介绍了alsa-utils提供的几个测试音频、配置声卡的工具,当然,本文也只是进行了简单地介绍,更加详细的使用方法还需要大家自己查看帮助信息。

编写一个简单地alsa-lib应用程序

本小节开始,我们来学习如何基于alsa-lib编写音频应用程序,alsa-lib提供的库函数也别多,笔者肯定不会全部给大家介绍,只介绍基础的使用方法,关于更加深入、更加详细的使用方法需要大家自己去研究、学习。

对于alsa-lib库的使用,ALSA提供了一些参考资料来帮助应用程序开发人员快速上手alsa-lib、基于alsa-lib进行应用编程,以下笔者给出了链接:

https://users.suse.com/~mana/alsa090_howto.htmlhttps://www.alsa-project.org/alsa-doc/alsa-lib/examples.html

第一份文档向用户介绍了如何使用alsa-lib编写简单的音频应用程序,包括PCM播放音频、PCM录音等,笔者也是参考了这份文档来编写本章教程,对应初学者,建议大家看一看。

第二个链接地址是ALSA提供的一些示例代码,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.1 ALSA提供的参考代码

点击对应源文件即可查看源代码。

以上便是ALSA提供的帮助文档以及参考代码,链接地址已经给出了,大家有兴趣可以看一下。

本小节笔者将向大家介绍如何基于alsa-lib编写一个简单地音频应用程序,譬如播放音乐、录音等;但在此之前,首先我们需要先来了解一些基本的概念,为后面的学习打下一个坚实的基础!

一些基本概念

主要是与音频相关的基本概念,因为在alsa-lib应用编程中会涉及到这些概念,所以先给大家进行一个简单地介绍。

样本长度(Sample)

样本是记录音频数据最基本的单元,样本长度就是采样位数,也称为位深度(Bit Depth、Sample Size、Sample Width)。是指计算机在采集和播放声音文件时,所使用数字声音信号的二进制位数,或者说每个采样样本所包含的位数(计算机对每个通道采样量化时数字比特位数),通常有8bit、16bit、24bit等。

声道数(channel)

分为单声道(Mono)和双声道/立体声(Stereo)。1表示单声道、2表示立体声。

帧(frame)

帧记录了一个声音单元,其长度为样本长度与声道数的乘积,一段音频数据就是由苦干帧组成的。

把所有声道中的数据加在一起叫做一帧,对于单声道:一帧 = 样本长度 * 1;双声道:一帧 = 样本长度 * 2。譬如对于样本长度为16bit的双声道来说,一帧的大小等于:16 * 2 / 8 = 4个字节。

采样率(Sample rate)

也叫采样频率,是指每秒钟采样次数,该次数是针对桢而言。譬如常见的采样率有:

8KHz - 电话所用采样率

22.05KHz - FM调频广播所用采样率

44.1KHz - 音频 CD,也常用于MPEG-1音频(VCD、SVCD、MP3)所用采样率

48KHz - miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率。

交错模式(interleaved)

交错模式是一种音频数据的记录方式,分为交错模式和非交错模式。在交错模式下,数据以连续桢的形式存放,即首先记录完桢1的左声道样本和右声道样本(假设为立体声格式),再记录桢2的左声道样本和右声道样本。而在非交错模式下,首先记录的是一个周期内所有桢的左声道样本,再记录右声道样本,数据是以连续通道的方式存储。不过多数情况下,我们一般都是使用交错模式。

周期(period)

周期是音频设备处理(读、写)数据的单位,换句话说,也就是音频设备读写数据的单位是周期,每一次读或写一个周期的数据,一个周期包含若干个帧;譬如周期的大小为1024帧,则表示音频设备进行一次读或写操作的数据量大小为1024帧,假设一帧为4个字节,那么也就是1024*4=4096个字节数据。

一个周期其实就是两次硬件中断之间的帧数,音频设备每处理(读或写)完一个周期的数据就会产生一个中断,所以两个中断之间相差一个周期,关于中断的问题,稍后再向大家介绍!

缓冲区(buffer)

数据缓冲区,一个缓冲区包含若干个周期,所以buffer是由若干个周期所组成的一块空间。下面一张图直观地表示了buffer、period、frame、sample(样本长度)之间的关系,假设一个buffer包含4个周期、而一个周包含1024帧、一帧包含两个样本(左、右两个声道):

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.2 buffer/period/frame/sample之间的关系示例图

音频设备底层驱动程序使用DMA来搬运数据,这个buffer中有4个period,每当DMA搬运完一个period的数据就会触发一次中断,因此搬运整个buffer中的数据将产生4次中断。ALSA为什么这样做?直接把整个buffer中的数据一次性搬运过去岂不是更快?情况并非如此,我们没有考虑到一个很重要的问题,那就是延迟;如果数据缓存区buffer很大,一次传输整个buffer中的数据可能会导致不可接受的延迟,因为一次搬运的数据量越大,所花费的时间就越长,那么必然会导致数据从传输开始到发出声音(以播放为例)这个过程所经历的时间就会越长,这就是延迟。为了解决这个问题,ALSA把缓存区拆分成多个周期,以周期为传输单元进行传输数据。

所以,周期不宜设置过大,周期过大会导致延迟过高;但周期也不能太小,周期太小会导致频繁触发中断,这样会使得CPU被频繁中断而无法执行其它的任务,使得效率降低!所以,周期大小要合适,在延迟可接受的情况下,尽量设置大一些,不过这个需要根据实际应用场合而定,有些应用场合,可能要求低延迟、实时性高,但有些应用场合没有这种需求。

数据之间的传输

这里再介绍一下数据之间传输的问题,这个问题很重要,大家一定要理解,这样会更好的帮助我们理解代码、理解代码的逻辑。

  • PCM播放情况下

在播放情况下,buffer中存放了需要播放的PCM音频数据,由应用程序向buffer中写入音频数据,buffer中的音频数据由DMA传输给音频设备进行播放,所以应用程序向buffer写入数据、音频设备从buffer读取数据,这就是buffer中数据的传输情况。

图 29.5.2中标识有read pointer和write pointer指针,write pointer指向当前应用程序写buffer的位置、read pointer指向当前音频设备读buffer的位置。在数据传输之前(播放之前),buffer缓冲区是没有数据的,此时write/read pointer均指向了buffer的起始位置,也就是第一个周期的起始位置,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.3 pointer指向buffer起始位置

应用程序向buffer写入多少帧数据,则write pointer指针向前移动多少帧,当应用程序向buffer中写入一个周期的数据时,write pointer指针将向前移动一个周期;接着再写入一个周期,指针再向前移动一个周期,以此类推!当write pointer移动到buffer末尾时,又会回到buffer的起始位置,以此循环!所以由此可知,这是一个环形缓冲区。

以上是应用程序写buffer的一个过程,接着再来看看音频设备读buffer(播放)的过程。在播放开始之前,read pointer指向了buffer的起始位置,也就是第一个周期的起始位置。音频设备每次只播放一个周期的数据(读取一个周期),每一次都是从read pointer所指位置开始读取;每读取一个周期,read pointer指针向前移动一个周期,同样,当read pointer指针移动到buffer末尾时,又会回到buffer的起始位置,以此构成一个循环!

应用程序需要向buffer中写入音频数据,音频设备才能读取数据进行播放,如果read pointer所指向的周期并没有填充音频数据,则无法播放!当buffer数据满时,应用程序将不能再写入数据,否则就会覆盖之前的数据,必须要等待音频设备播放完一个周期,音频设备每播放完一个周期,这个周期就变成空闲状态了,此时应用程序就可以写入一个周期的数据以填充这个空闲周期。

  • PCM录音情况下

在录音情况下,buffer中存放了音频设备采集到的音频数据(外界模拟声音通过ADC转为数字声音),由音频设备向buffer中写入音频数据(DMA搬运),而应用程序从buffer中读取数据,所以音频设备向buffer写入数据、应用程序从buffer读取数据,这就是录音情况下buffer中数据的传输情况。

回到图 29.5.2中,此时write pointer指向音频设备写buffer的位置、read pointer指向应用程序读buffer的位置。在录音开始之前,buffer缓冲区是没有数据的,此时write/read pointer均指向了buffer的起始位置,也就是第一个周期的起始位置,如图 29.5.3中所示。

音频设备向buffer写入多少帧数据,则write pointer指针向前移动多少帧,音频设备每次只采集一个周期,将采集到的数据写入buffer中,从write pointer所指位置开始写入;当音频设备向buffer中写入一个周期的数据时,write pointer指针将向前移动一个周期;接着再写入一个周期,指针再向前移动一个周期,以此类推!当write pointer移动到buffer末尾时,又会回到buffer的起始位置,以此构成循环!

以上是音频设备写buffer的一个过程,接着再来看看应用程序读buffer的过程。在录音开始之前,read pointer指向了buffer的起始位置,也就是第一个周期的起始位置。同样,应用程序从buffer读取了多少帧数据,则read pointer指针向前移动多少帧;从read pointer所指位置开始读取,当read pointer指针移动到buffer末尾时,又会回到buffer的起始位置,以此构成一个循环!

音频设备需要向buffer中写入音频数据,应用程序才能从buffer中读取数据(录音),如果read pointer所指向的周期并没有填充音频数据,则无法读取!当buffer中没有数据时,需要等待音频设备向buffer中写入数据,音频设备每次写入一个周期,当应用程序读取完这个周期的数据后,这个周期又变成了空闲周期,需要等待音频设备写入数据。

Over and Under Run

当一个声卡处于工作状态时,环形缓冲区buffer中的数据总是连续地在音频设备和应用程序缓存区间传输,如下图所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.4 buffer中数据的传输

上图展示了声卡在工作状态下,buffer中数据的传输情况,总是连续地在音频设备和应用程序缓存区间传输,但事情并不总是那么完美、也会出现有例外;譬如在录音例子中,如果应用程序读取数据不够快,环形缓冲区buffer中的数据已经被音频设备写满了、而应用程序还未来得及读走,那么数据将会被覆盖;这种数据的丢失被称为overrun。在播放例子中,如果应用程序写入数据到环形缓冲区buffer中的速度不够快,缓存区将会“饿死”(缓冲区中无数据可播放);这样的错误被称为underrun(欠载)。在ALSA文档中,将这两种情形统称为"XRUN",适当地设计应用程序可以最小化XRUN并且可以从中恢复过来。

打开PCM设备

从本小节开始,将正式介绍如何编写一个音频应用程序,首先我们需要在应用程序中包含alsa-lib库的头文件<alsa/asoundlib.h>,这样才能在应用程序中调用alsa-lib库函数以及使用相关宏。

第一步需要打开PCM设备,调用函数snd_pcm_open(),该函数原型如下所示:

int snd_pcm_open(snd_pcm_t **pcmp, const char *name, snd_pcm_stream_t stream, int mode)

该函数一共有4个参数,如下所示:

  • pcmp:snd_pcm_t用于描述一个PCM设备,所以一个snd_pcm_t对象表示一个PCM设备;snd_pcm_open函数会打开参数name所指定的设备,实例化snd_pcm_t对象,并将对象的指针(也就是PCM设备的句柄)通过pcmp返回出来。
  • name:参数name指定PCM设备的名字。alsa-lib库函数中使用逻辑设备名而不是设备文件名,命名方式为"hw:i,j",i表示声卡的卡号,j则表示这块声卡上的设备号;譬如"hw:0,0"表示声卡0上的PCM设备0,在播放情况下,这其实就对应/dev/snd/pcmC0D0p(如果是录音,则对应/dev/snd/pcmC0D0c)。除了使用"hw:i,j"这种方式命名之外,还有其它两种常用的命名方式,譬如"plughw:i,j"、"default"等,关于这些名字的不同,本章最后再向大家进行简单地介绍,这里暂时先不去理会这个问题。
  • stream:参数stream指定流类型,有两种不同类型:SND_PCM_STREAM_PLAYBACK和SND_PCM_STREAM_CAPTURE;SND_PCM_STREAM_PLAYBACK表示播放,SND_PCM_STREAM_CAPTURE则表示采集。
  • mode:最后一个参数mode指定了open模式,通常情况下,我们会将其设置为0,表示默认打开模式,默认情况下使用阻塞方式打开设备;当然,也可将其设置为SND_PCM_NONBLOCK,表示以非阻塞方式打开设备。

设备打开成功,snd_pcm_open函数返回0;打开失败,返回一个小于0的错误编号,可以使用alsa-lib提供的库函数snd_strerror()来得到对应的错误描述信息,该函数与C库函数strerror()用法相同。

与snd_pcm_open相对应的是snd_pcm_close(),函数snd_pcm_close()用于关闭PCM设备,函数原型如下所示:

int snd_pcm_close(snd_pcm_t *pcm);

使用示例:

调用snd_pcm_open()函数打开声卡0的PCM播放设备0:

snd_pcm_t *pcm_handle = NULL;int ret;ret = snd_pcm_open(&pcm_handle, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0);if (0 > ret) {fprintf(stderr, "snd_pcm_open error: %s\n", snd_strerror(ret));return -1;}

设置硬件参数

打开PCM设备之后,接着我们需要对设备进行设置,包括硬件配置和软件配置。软件配置就不再介绍了,使用默认配置即可!我们主要是对硬件参数进行配置,譬如采样率、声道数、格式、访问类型、period周期大小、buffer大小等。

实例化snd_pcm_hw_params_t对象

alsa-lib使用snd_pcm_hw_params_t数据类型来描述PCM设备的硬件配置参数,在配置参数之前,我们需要实例化一个snd_pcm_hw_params_t对象,使用snd_pcm_hw_params_malloc或snd_pcm_hw_params_alloca()来实例化一个snd_pcm_hw_params_t对象,如下所示:

snd_pcm_hw_params_t *hwparams = NULL;snd_pcm_hw_params_malloc(&hwparams);

snd_pcm_hw_params_alloca(&hwparams);

它们之间的区别也就是C库函数malloc和alloca之间的区别。当然,你也可以直接使用malloc()或alloca()来分配一个snd_pcm_hw_params_t对象,亦或者直接定义全局变量或栈自动变量。与snd_pcm_hw_params_malloc/snd_pcm_hw_params_alloca相对应的是snd_pcm_hw_params_free,snd_pcm_hw_params_free()函数用于释放snd_pcm_hw_params_t对象占用的内存空间。函数原型如下所示:

void snd_pcm_hw_params_free(snd_pcm_hw_params_t *obj)

初始化snd_pcm_hw_params_t对象

snd_pcm_hw_params_t对象实例化完成之后,接着我们需要对其进行初始化操作,调用snd_pcm_hw_params_any()对snd_pcm_hw_params_t对象进行初始化操作,调用该函数会使用PCM设备当前的配置参数去初始化snd_pcm_hw_params_t对象,如下所示:

snd_pcm_hw_params_any(pcm_handle, hwparams);

第一个参数为PCM设备的句柄,第二个参数传入snd_pcm_hw_params_t对象的指针。

对硬件参数进行设置

alsa-lib提供了一系列的snd_pcm_hw_params_set_xxx函数用于设置PCM设备的硬件参数,同样也提供了一系列的snd_pcm_hw_params_get_xxx函数用于获取硬件参数。

(1)设置access访问类型:snd_pcm_hw_params_set_access()

调用snd_pcm_hw_params_set_access设置访问类型,其函数原型如下所示:

int snd_pcm_hw_params_set_access(snd_pcm_t *pcm,snd_pcm_hw_params_t * params,snd_pcm_access_t access)

参数access指定设备的访问类型,是一个snd_pcm_access_t类型常量,这是一个枚举类型,如下所示:

enum snd_pcm_access_t {SND_PCM_ACCESS_MMAP_INTERLEAVED = 0, //mmap access with simple interleaved channelsSND_PCM_ACCESS_MMAP_NONINTERLEAVED, //mmap access with simple non interleaved channelsSND_PCM_ACCESS_MMAP_COMPLEX, //mmap access with complex placementSND_PCM_ACCESS_RW_INTERLEAVED, //snd_pcm_readi/snd_pcm_writei accessSND_PCM_ACCESS_RW_NONINTERLEAVED, //snd_pcm_readn/snd_pcm_writen accessSND_PCM_ACCESS_LAST = SND_PCM_ACCESS_RW_NONINTERLEAVED};

通常,将访问类型设置为SND_PCM_ACCESS_RW_INTERLEAVED,交错访问模式,通过snd_pcm_readi/snd_pcm_writei对PCM设备进行读/写操作。

函数调用成功返回0;失败将返回一个小于0的错误码,可通过snd_strerror()函数获取错误描述信息。

使用示例:

ret = snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);if (0 > ret)fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));

(2)设置数据格式:snd_pcm_hw_params_set_format()

调用snd_pcm_hw_params_set_format()函数设置PCM设备的数据格式,函数原型如下所示:

int snd_pcm_hw_params_set_format(snd_pcm_t *pcm,snd_pcm_hw_params_t *params,snd_pcm_format_t format)

参数format指定数据格式,该参数是一个snd_pcm_format_t类型常量,这是一个枚举类型,如下所示:

enum snd_pcm_format_t {SND_PCM_FORMAT_UNKNOWN = -1,SND_PCM_FORMAT_S8 = 0,SND_PCM_FORMAT_U8,SND_PCM_FORMAT_S16_LE,SND_PCM_FORMAT_S16_BE,SND_PCM_FORMAT_U16_LE,SND_PCM_FORMAT_U16_BE,SND_PCM_FORMAT_S24_LE,SND_PCM_FORMAT_S24_BE,SND_PCM_FORMAT_U24_LE,SND_PCM_FORMAT_U24_BE,SND_PCM_FORMAT_S32_LE,SND_PCM_FORMAT_S32_BE,SND_PCM_FORMAT_U32_LE,SND_PCM_FORMAT_U32_BE,SND_PCM_FORMAT_FLOAT_LE,SND_PCM_FORMAT_FLOAT_BE,SND_PCM_FORMAT_FLOAT64_LE,SND_PCM_FORMAT_FLOAT64_BE,SND_PCM_FORMAT_IEC958_SUBFRAME_LE,SND_PCM_FORMAT_IEC958_SUBFRAME_BE,SND_PCM_FORMAT_MU_LAW,SND_PCM_FORMAT_A_LAW,SND_PCM_FORMAT_IMA_ADPCM,SND_PCM_FORMAT_MPEG,SND_PCM_FORMAT_GSM,SND_PCM_FORMAT_S20_LE,SND_PCM_FORMAT_S20_BE,SND_PCM_FORMAT_U20_LE,SND_PCM_FORMAT_U20_BE,SND_PCM_FORMAT_SPECIAL = 31,SND_PCM_FORMAT_S24_3LE = 32,SND_PCM_FORMAT_S24_3BE,SND_PCM_FORMAT_U24_3LE,SND_PCM_FORMAT_U24_3BE,SND_PCM_FORMAT_S20_3LE,SND_PCM_FORMAT_S20_3BE,SND_PCM_FORMAT_U20_3LE,SND_PCM_FORMAT_U20_3BE,SND_PCM_FORMAT_S18_3LE,SND_PCM_FORMAT_S18_3BE,SND_PCM_FORMAT_U18_3LE,SND_PCM_FORMAT_U18_3BE,SND_PCM_FORMAT_G723_24,SND_PCM_FORMAT_G723_24_1B,SND_PCM_FORMAT_G723_40,SND_PCM_FORMAT_G723_40_1B,SND_PCM_FORMAT_DSD_U8,SND_PCM_FORMAT_DSD_U16_LE,SND_PCM_FORMAT_DSD_U32_LE,SND_PCM_FORMAT_DSD_U16_BE,SND_PCM_FORMAT_DSD_U32_BE,SND_PCM_FORMAT_LAST = SND_PCM_FORMAT_DSD_U32_BE,SND_PCM_FORMAT_S16 = SND_PCM_FORMAT_S16_LE,SND_PCM_FORMAT_U16 = SND_PCM_FORMAT_U16_LE,SND_PCM_FORMAT_S24 = SND_PCM_FORMAT_S24_LE,SND_PCM_FORMAT_U24 = SND_PCM_FORMAT_U24_LE,SND_PCM_FORMAT_S32 = SND_PCM_FORMAT_S32_LE,SND_PCM_FORMAT_U32 = SND_PCM_FORMAT_U32_LE,SND_PCM_FORMAT_FLOAT = SND_PCM_FORMAT_FLOAT_LE,SND_PCM_FORMAT_FLOAT64 = SND_PCM_FORMAT_FLOAT64_LE,SND_PCM_FORMAT_IEC958_SUBFRAME = SND_PCM_FORMAT_IEC958_SUBFRAME_LE,SND_PCM_FORMAT_S20 = SND_PCM_FORMAT_S20_LE,SND_PCM_FORMAT_U20 = SND_PCM_FORMAT_U20_LE};

用的最多的格式是SND_PCM_FORMAT_S16_LE,有符号16位、小端模式。当然,音频设备不一定支持用户所指定的格式,在此之前,用户可以调用snd_pcm_hw_params_test_format()函数测试PCM设备是否支持某种格式,如下所示:

if (snd_pcm_hw_params_test_format(pcm_handle, hwparams, SND_PCM_FORMAT_S16_LE)) {// 返回一个非零值 表示不支持该格式}else {// 返回0表示支持}

(3)设置声道数:snd_pcm_hw_params_set_channels()

调用snd_pcm_hw_params_set_channels()函数设置PCM设备的声道数,函数原型如下所示:

int snd_pcm_hw_params_set_channels(snd_pcm_t *pcm,snd_pcm_hw_params_t *params,unsigned int val)

参数val指定声道数量,val=2表示双声道,也就是立体声。函数调用成功返回0,失败返回小于0的错误码。

使用示例:

ret = snd_pcm_hw_params_set_channels(pcm_handle, hwparams, 2);if (0 > ret)fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));

(4)设置采样率大小:snd_pcm_hw_params_set_rate()

调用snd_pcm_hw_params_set_rate设置采样率大小,其函数原型如下所示:

int snd_pcm_hw_params_set_rate(snd_pcm_t *pcm,snd_pcm_hw_params_t *params,unsigned int val,int dir)

参数val指定采样率大小,譬如44100;参数dir用于控制方向,若dir=-1,则实际采样率小于参数val指定的值;dir=0表示实际采样率等于参数val;dir=1表示实际采样率大于参数val。

函数调用成功返回0;失败将返回小于0的错误码。

使用示例:

ret = snd_pcm_hw_params_set_rate(pcm_handle, hwparams, 44100, 0);if (0 > ret)fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));

(5)设置周期大小:snd_pcm_hw_params_set_period_size()

这里说的周期,也就是29.5.1小节中向大家介绍的周期,一个周期的大小使用帧来衡量,譬如一个周期1024帧;调用snd_pcm_hw_params_set_period_size()函数设置周期大小,其函数原型如下所示:

int snd_pcm_hw_params_set_period_size(snd_pcm_t *pcm,snd_pcm_hw_params_t *params,snd_pcm_uframes_t val,int dir)

alsa-lib使用snd_pcm_uframes_t类型表示帧的数量;参数dir与snd_pcm_hw_params_set_rate()函数的dir参数意义相同。

使用示例(将周期大小设置为1024帧):

ret = snd_pcm_hw_params_set_period_size(pcm_handle, hwparams, 1024, 0);if (0 > ret)fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));

注意,参数val的单位是帧、而不是字节。

(6)设置buffer大小:snd_pcm_hw_params_set_buffer_size()

调用snd_pcm_hw_params_set_buffer_size()函数设置buffer的大小,其函数原型如下所示:

int snd_pcm_hw_params_set_buffer_size(snd_pcm_t *pcm,snd_pcm_hw_params_t *params,snd_pcm_uframes_t val)

参数val指定buffer的大小,以帧为单位,通常buffer的大小是周期大小的整数倍,譬如16个周期;但函数snd_pcm_hw_params_set_buffer_size()是以帧为单位来表示buffer的大小,所以需要转换一下,譬如将buffer大小设置为16个周期,则参数val等于16 * 1024(假设一个周期为1024帧)=16384帧。

函数调用成功返回0;失败返回一个小于0的错误码。

使用示例:

ret = snd_pcm_hw_params_set_buffer_size(pcm_handle, hwparams, 16*1024);if (0 > ret)fprintf(stderr, "snd_pcm_hw_params_set_buffer_size error: %s\n", snd_strerror(ret));

除了snd_pcm_hw_params_set_buffer_size()函数之外,我们还可以调用snd_pcm_hw_params_set_periods()函数设置buffer大小,其函数原型如下所示:

int snd_pcm_hw_params_set_periods(snd_pcm_t *pcm,snd_pcm_hw_params_t *params,unsigned int val,int dir)

参数val指定了buffer的大小,该大小以周期为单位、并不是以帧为单位,注意区分!

参数dir与snd_pcm_hw_params_set_rate()函数的dir参数意义相同。

函数调用成功返回0;失败将返回一个小于0的错误码。

使用示例:

ret = snd_pcm_hw_params_set_periods(pcm_handle, hwparams, 16, 0); //buffer大小为16个周期

if (0 > ret)fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));

(7)安装/加载硬件配置参数:snd_pcm_hw_params()

参数设置完成之后,最后调用snd_pcm_hw_params()加载/安装配置、将配置参数写入硬件使其生效,其函数原型如下所示:

int snd_pcm_hw_params(snd_pcm_t *pcm, snd_pcm_hw_params_t *params)

函数调用成功返回0,失败将返回一个小于0的错误码。函数snd_pcm_hw_params()调用之后,其内部会自动调用snd_pcm_prepare()函数,PCM设备的状态被更改为SND_PCM_STATE_PREPARED。

设备有多种不同的状态,SND_PCM_STATE_PREPARED为其中一种,关于状态的问题,后面在向大家介绍。调用snd_pcm_prepare()函数会使得PCM设备处于SND_PCM_STATE_PREPARED状态(也就是处于一种准备好的状态)。

使用示例:

ret = snd_pcm_hw_params(pcm_handle, hwparams);if (0 > ret)fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));

读/写数据

接下来就可以进行读/写数据了,如果是PCM播放,则调用snd_pcm_writei()函数向播放缓冲区buffer中写入音频数据;如果是PCM录音,则调用snd_pcm_readi()函数从录音缓冲区buffer中读取数据,它们的函数原型如下所示:

snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm,const void *buffer,snd_pcm_uframes_t size)snd_pcm_sframes_t snd_pcm_readi(snd_pcm_t *pcm,void *buffer,snd_pcm_uframes_t size)

参数pcm为PCM设备的句柄;调用snd_pcm_writei()函数,将参数buffer(应用程序的缓冲区)缓冲区中的数据写入到驱动层的播放环形缓冲区buffer中,参数size指定写入数据的大小,以帧为单位;通常情况下,每次调用snd_pcm_writei()写入一个周期数据。

调用snd_pcm_readi()函数,将从驱动层的录音环形缓冲区buffer中读取数据到参数buffer指定的缓冲区中(应用程序的缓冲区),参数size指定读取数据的大小,以帧为单位;通常情况下,每次调用snd_pcm_readi()读取一个周期数据。

Tips:snd_pcm_writei/snd_pcm_readi函数原型中,参数buffer指的是应用程序的缓冲区,不要与驱动层的环形缓冲区搞混了!

snd_pcm_readi/snd_pcm_writei调用成功,返回实际读取/写入的帧数;调用失败将返回一个负数错误码。即使调用成功,实际读取/写入的帧数不一定等于参数size所指定的帧数,仅当发生信号或XRUN时,返回的帧数可能会小于参数size。

阻塞与非阻塞

调用snd_pcm_open()打开设备时,若指定为阻塞方式,则调用snd_pcm_readi/snd_pcm_writei以阻塞方式进行读/写。对于PCM录音来说,当buffer缓冲区中无数据可读时,调用snd_pcm_readi()函数将会阻塞,直到音频设备向buffer中写入采集到的音频数据;同理,对于PCM播放来说,当buffer缓冲区中的数据满时,调用snd_pcm_writei()函数将会阻塞,直到音频设备从buffer中读走数据进行播放。

若调用snd_pcm_open()打开设备时,指定为非阻塞方式,则调用snd_pcm_readi/snd_pcm_writei以非阻塞方式进行读/写。对于PCM录音来说,当buffer缓冲区中无数据可读时,调用snd_pcm_readi()不会阻塞、而是立即以错误形式返回;同理,对于PCM播放来说,当buffer缓冲区中的数据满时,调用snd_pcm_writei()函数也不会阻塞、而是立即以错误形式返回。

snd_pcm_readn和snd_pcm_writen

snd_pcm_readi/snd_pcm_writei适用于交错模式(interleaved)读/写数据,如果用户设置的访问类型并不是交错模式,而是非交错模式(non interleaved),此时便不可再使用snd_pcm_readi/snd_pcm_writei进行读写操作了,而需要使用snd_pcm_readn和snd_pcm_writen进行读写。

示例代码之PCM播放

通过上小节的一个介绍,相信大家对alsa-lib音频应用编程已经有了基本的认识和理解,本小节我们来编写一个简单地音乐播放器,可以播放WAV音频文件,代码笔者已经写好了,如下所示:

本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->29_alsa-lib->pcm_playback.c。

示例代码 29.5.1 一个简单地PCM播放示例程序/***************************************************************Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved.文件名 : pcm_playback.c作者 : 邓涛版本 : V1.0描述 : 一个简单地PCM播放示例代码--播放WAV音频文件其他 : 无论坛 : www.openedv.com日志 : 初版 V1.0 2021/7/20 邓涛创建***************************************************************/#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <string.h>#include <alsa/asoundlib.h>/************************************宏定义************************************/#define PCM_PLAYBACK_DEV "hw:0,0"/************************************WAV音频文件解析相关数据结构申明************************************/typedef struct WAV_RIFF {char ChunkID[4]; u_int32_t ChunkSize; char Format[4]; } __attribute__ ((packed)) RIFF_t;typedef struct WAV_FMT {char Subchunk1ID[4]; u_int32_t Subchunk1Size; u_int16_t AudioFormat; u_int16_t NumChannels; u_int32_t SampleRate; u_int32_t ByteRate; /* = SampleRate * NumChannels * BitsPerSample/8 */u_int16_t BlockAlign; /* = NumChannels * BitsPerSample/8 */u_int16_t BitsPerSample; } __attribute__ ((packed)) FMT_t;static FMT_t wav_fmt;typedef struct WAV_DATA {char Subchunk2ID[4]; u_int32_t Subchunk2Size; } __attribute__ ((packed)) DATA_t;/************************************static静态全局变量定义************************************/static snd_pcm_t *pcm = NULL; //pcm句柄static unsigned int buf_bytes; //应用程序缓冲区的大小(字节为单位)static void *buf = NULL; //指向应用程序缓冲区的指针static int fd = -1; //指向WAV音频文件的文件描述符static snd_pcm_uframes_t period_size = 1024; //周期大小(单位: 帧)static unsigned int periods = 16; //周期数(设备驱动层buffer的大小)static int snd_pcm_init(void){snd_pcm_hw_params_t *hwparams = NULL;int ret;ret = snd_pcm_open(&pcm, PCM_PLAYBACK_DEV, SND_PCM_STREAM_PLAYBACK, 0);if (0 > ret) {fprintf(stderr, "snd_pcm_open error: %s: %s\n",PCM_PLAYBACK_DEV, snd_strerror(ret));return -1;}snd_pcm_hw_params_malloc(&hwparams);ret = snd_pcm_hw_params_any(pcm, hwparams);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));goto err2;}/**************设置参数***************/ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));goto err2;}ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));goto err2;}ret = snd_pcm_hw_params_set_rate(pcm, hwparams, wav_fmt.SampleRate, 0);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));goto err2;}ret = snd_pcm_hw_params_set_channels(pcm, hwparams, wav_fmt.NumChannels);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));goto err2;}ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));goto err2;}ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));goto err2;}ret = snd_pcm_hw_params(pcm, hwparams);snd_pcm_hw_params_free(hwparams); //释放hwparams对象占用的内存if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));goto err1;}buf_bytes = period_size * wav_fmt.BlockAlign; //变量赋值,一个周期的字节大小return 0;err2:snd_pcm_hw_params_free(hwparams); //释放内存err1:snd_pcm_close(pcm); //关闭pcm设备return -1;}static int open_wav_file(const char *file){RIFF_t wav_riff;DATA_t wav_data;int ret;fd = open(file, O_RDONLY);if (0 > fd) {fprintf(stderr, "open error: %s: %s\n", file, strerror(errno));return -1;}ret = read(fd, &wav_riff, sizeof(RIFF_t));if (sizeof(RIFF_t) != ret) {if (0 > ret)perror("read error");elsefprintf(stderr, "check error: %s\n", file);close(fd);return -1;}if (strncmp("RIFF", wav_riff.ChunkID, 4) ||//校验strncmp("WAVE", wav_riff.Format, 4)) {fprintf(stderr, "check error: %s\n", file);close(fd);return -1;}ret = read(fd, &wav_fmt, sizeof(FMT_t));if (sizeof(FMT_t) != ret) {if (0 > ret)perror("read error");elsefprintf(stderr, "check error: %s\n", file);close(fd);return -1;}if (strncmp("fmt ", wav_fmt.Subchunk1ID, 4)) {//校验fprintf(stderr, "check error: %s\n", file);close(fd);return -1;}printf("<<<<音频文件格式信息>>>>\n\n");printf(" file name: %s\n", file);printf(" Subchunk1Size: %u\n", wav_fmt.Subchunk1Size);printf(" AudioFormat: %u\n", wav_fmt.AudioFormat);printf(" NumChannels: %u\n", wav_fmt.NumChannels);printf(" SampleRate: %u\n", wav_fmt.SampleRate);printf(" ByteRate: %u\n", wav_fmt.ByteRate);printf(" BlockAlign: %u\n", wav_fmt.BlockAlign);printf(" BitsPerSample: %u\n\n", wav_fmt.BitsPerSample);if (0 > lseek(fd, sizeof(RIFF_t) 8 wav_fmt.Subchunk1Size,SEEK_SET)) {perror("lseek error");close(fd);return -1;}while(sizeof(DATA_t) == read(fd, &wav_data, sizeof(DATA_t))) {if (!strncmp("data", wav_data.Subchunk2ID, 4))//校验return 0;if (0 > lseek(fd, wav_data.Subchunk2Size, SEEK_CUR)) {perror("lseek error");close(fd);return -1;}}fprintf(stderr, "check error: %s\n", file);return -1;}/************************************main主函数************************************/int main(int argc, char *argv[]){int ret;if (2 != argc) {fprintf(stderr, "Usage: %s <audio_file>\n", argv[0]);exit(EXIT_FAILURE);}if (open_wav_file(argv[1]))exit(EXIT_FAILURE);if (snd_pcm_init())goto err1;buf = malloc(buf_bytes);if (NULL == buf) {perror("malloc error");goto err2;}for ( ; ; ) {memset(buf, 0x00, buf_bytes); //buf清零ret = read(fd, buf, buf_bytes); //从音频文件中读取数据if (0 >= ret) // 如果读取出错或文件读取完毕goto err3;ret = snd_pcm_writei(pcm, buf, period_size);if (0 > ret) {fprintf(stderr, "snd_pcm_writei error: %s\n", snd_strerror(ret));goto err3;}else if (ret < period_size) {//实际写入的帧数小于指定的帧数//此时我们需要调整下音频文件的读位置//将读位置向后移动(往回移)(period_size-ret)*frame_bytes个字节//frame_bytes表示一帧的字节大小if (0 > lseek(fd, (ret-period_size) * wav_fmt.BlockAlign, SEEK_CUR)) {perror("lseek error");goto err3;}}}err3:free(buf); //释放内存err2:snd_pcm_close(pcm); //关闭pcm设备err1:close(fd); //关闭打开的音频文件exit(EXIT_FAILURE);}

本应用程序实现可以播放WAV音频文件,关于WAV文件格式的解析,本文档不作说明,WAV文件格式其实非常简单,大家自己百度了解。

在main()函数中,首先对参数进行了校验,执行测试程序需要用户传入一个参数,这个参数用于指定一个需要播放的WAV音频文件。接着调用自定义函数open_wav_file()对WAV文件进行解析,其实也就是对它的头部数据进行校验、解析,获取音频格式信息以及音频数据的位置偏移量。

接着调用自定义函数snd_pcm_init()对PCM设备进行初始化,在snd_pcm_init()函数中,首先调用alsa-lib库函数snd_pcm_open()打开PCM播放设备,接着对PCM设备硬件参数进行设置,包括:访问类型、数据格式、采样率、声道数、周期大小以及buffer的大小,这些内容前面已经给大家详细介绍过,这里不再重述!

回到main()函数,调用C库函数malloc()申请分配一个缓冲区,用于存放从音频文件中读取出来的音频数据。

一切准备好之后,就可以播放音频了,在for循环中,首先调用read()函数从音频文件中读取出音频数据,每次读取一个周期,将读取到的数据存放在buf指向的缓冲区中,接着调用alsa-lib库函数snd_pcm_writei()写入数据进行播放。示例程序中调用snd_pcm_open()时使用的是阻塞方式,当驱动层环形缓冲区buffer还未满时,调用snd_pcm_writei()并不会阻塞,而是会将数据写入到环形缓冲区中、然后返回;调用一次snd_pcm_writei()写入一个周期数据、调用一次再写入一个周期;当环形缓冲区数据满时,调用snd_pcm_writei()会阻塞,直到音频设备播放完一个周期、此时会出现一个空闲周期,接着snd_pcm_writei()将数据填充到这个空闲周期后返回。

以上对示例代码进行了一个简单地介绍,代码本身非常简单,没什么难点,代码中注释信息也已经描述地比较清楚了,相信大家都可以看懂。需要注意,必须要在源码中包含alsa-lib的头文件<alsa/asoundlib.h>!

编译示例代码

接下来编译上述示例代码,编译的方法非常简单,按照以前的惯例,编译时无非是要指定两个路径(alsa-lib头文件所在路径、alsa-lib库文件所在路径)以及链接库(需要链接的库文件名称),譬如:

${CC} -o testApp testApp.c -Ixxx -Lyyy -lzzz

xxx表示头文件的路径,yyy表示库文件的路径,zzz表示链接库。

但是我们并没有自己移植alsa-lib,也就意味着我们在Ubuntu下并没有移植、安装alsa-lib,所以这些路径无法指定。其实,我们使用的交叉编译工具对应的安装目录下已经安装了alsa-lib,进入到交叉编译工具安装目录下的sysroots/cortexa7hf-neon-poky-linux-gnueabi目录,譬如笔者使用的Ubuntu系统,交叉编译工具安装路径为/opt/fsl-imx-x11/4.1.15-2.1.0。

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.5 cortexa7hf-neon-poky-linux-gnueabi目录下的文件夹

该目录下有两个目录,lib和usr,这两个目录其实就是Linux系统根目录下的lib和usr;所以lib目录下存放了一些链接库文件,usr目录下包含了include和lib目录,分别存放了头文件和链接库文件。usr/include/alsa目录下存放了alsa-lib的头文件,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.6 alsa-lib的头文件

我们需要包含的头文件asoundlib.h头文件就在该目录下。

usr/lib目录下包含了alsa-lib库文件,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.7 alsa-lib库文件

alsa-lib链接库libasound.so就在该目录下。那既然找到了alsa-lib的头文件路径和库文件路径,编译应用程序时直接指定这些路径即可。但我们不需要自己手动指定这些路径,交叉编译器已经把这些路径添加到它的搜索路径中了,使用echo ${CC}查看环境变量CC的内容,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.8 CC环境变量的内容

其中交叉编译器arm-poky-linux-gnueabi-gcc有一个选--sysroot,它指定了一个路径,这个路径就是交叉编译工具安装目录下的sysroots/cortexa7hf-neon-poky-linux-gnueabi目录,--sysroot选项用于设置目标平台的根目录,设置了平台根目录之后,当编译应用程序时,编译器会将根目录下的usr/include添加到头文件搜索路径中、将根目录下的lib和usr/lib添加到库文件搜索路径中。

所以由此可知,编译应用程序时,我们只需指定链接库即可,如下所示:

${CC} -o testApp testApp.c -lasound

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.9 编译应用程序

测试应用程序

将编译得到的可执行文件拷贝到开发板Linux系统/home/root目录下,并拷贝一个WAV音频文件到/home/root目录下,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.10 将测试程序和WAV音频文件拷贝到开发板家目录

接着进行测试,在测试之前,我们还需要对声卡混音器进行配置,当然,你也可以不配置,因为开发板出厂系统中声卡是已经配置好的。这里我们直接使用amixer工具进行配置,配置如下:

# 打开耳机播放ZCamixer sset 'Headphone Playback ZC' on# 打开喇叭播放ZCamixer sset 'Speaker Playback ZC' onamixer sset 'Speaker AC' 3amixer sset 'Speaker DC' 3# 音量设置amixer sset Headphone 105,105 //耳机音量设置amixer sset Playback 230,230 //播放音量设置amixer sset Speaker 118,118 //喇叭音量设置# 打开左右声道amixer sset 'Right Output Mixer PCM' on //打开右声道amixer sset 'Left Output Mixer PCM' on //打开左声道

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.11 声卡设置

由于篇幅有限,打印信息不能给大家全部截取出来。声音的大小,大家根据情况进行调节。

声卡设置完成之后,接着运行测试程序,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.12 执行测试程序

程序运行之后,对传入的WAV文件进行解析,并将其音频格式信息打印出来。

此时开发板喇叭便会开始播放音乐,如果连接了耳机,则会通过耳机播放音乐。

示例代码值PCM录音

本小节我们来编写一个PCM音频录制(录音)的测试程序,示例代码笔者已经给出,如下所示:

本例程源码对应的路径为:开发板光盘->11、Linux C应用编程例程源码->29_alsa-lib->pcm_capture.c。

示例代码 29.5.2 一个简单地PCM录音示例程序/***************************************************************Copyright © ALIENTEK Co., Ltd. 1998-2021. All rights reserved.文件名 : pcm_capture.c作者 : 邓涛版本 : V1.0描述 : 一个简单地PCM音频采集示例代码--录音其他 : 无论坛 : www.openedv.com日志 : 初版 V1.0 2021/7/20 邓涛创建***************************************************************/#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <string.h>#include <alsa/asoundlib.h>/************************************宏定义************************************/#define PCM_CAPTURE_DEV "hw:0,0"/************************************static静态全局变量定义************************************/static snd_pcm_t *pcm = NULL; //pcm句柄static snd_pcm_uframes_t period_size = 1024; //周期大小(单位: 帧)static unsigned int periods = 16; //周期数(buffer的大小)static unsigned int rate = 44100; //采样率static int snd_pcm_init(void){snd_pcm_hw_params_t *hwparams = NULL;int ret;ret = snd_pcm_open(&pcm, PCM_CAPTURE_DEV, SND_PCM_STREAM_CAPTURE, 0);if (0 > ret) {fprintf(stderr, "snd_pcm_open error: %s: %s\n",PCM_CAPTURE_DEV, snd_strerror(ret));return -1;}snd_pcm_hw_params_malloc(&hwparams);ret = snd_pcm_hw_params_any(pcm, hwparams);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_any error: %s\n", snd_strerror(ret));goto err2;}/**************设置参数***************/ret = snd_pcm_hw_params_set_access(pcm, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_set_access error: %s\n", snd_strerror(ret));goto err2;}ret = snd_pcm_hw_params_set_format(pcm, hwparams, SND_PCM_FORMAT_S16_LE);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_set_format error: %s\n", snd_strerror(ret));goto err2;}ret = snd_pcm_hw_params_set_rate(pcm, hwparams, rate, 0);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_set_rate error: %s\n", snd_strerror(ret));goto err2;}ret = snd_pcm_hw_params_set_channels(pcm, hwparams, 2);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_set_channels error: %s\n", snd_strerror(ret));goto err2;}ret = snd_pcm_hw_params_set_period_size(pcm, hwparams, period_size, 0);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_set_period_size error: %s\n", snd_strerror(ret));goto err2;}ret = snd_pcm_hw_params_set_periods(pcm, hwparams, periods, 0);if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params_set_periods error: %s\n", snd_strerror(ret));goto err2;}ret = snd_pcm_hw_params(pcm, hwparams);snd_pcm_hw_params_free(hwparams); //释放hwparams对象占用的内存if (0 > ret) {fprintf(stderr, "snd_pcm_hw_params error: %s\n", snd_strerror(ret));goto err1;}return 0;err2:snd_pcm_hw_params_free(hwparams); //释放内存err1:snd_pcm_close(pcm); //关闭pcm设备return -1;}/************************************main主函数************************************/int main(int argc, char *argv[]){unsigned char *buf = NULL;unsigned int buf_bytes;int fd = -1;int ret;if (2 != argc) {fprintf(stderr, "Usage: %s <output_file>\n", argv[0]);exit(EXIT_FAILURE);}if (snd_pcm_init())exit(EXIT_FAILURE);buf_bytes = period_size * 4; //字节大小 = 周期大小*帧的字节大小 16位双声道buf = malloc(buf_bytes);if (NULL == buf) {perror("malloc error");goto err1;}fd = open(argv[1], O_WRONLY | O_CREAT | O_EXCL);if (0 > fd) {fprintf(stderr, "open error: %s: %s\n", argv[1], strerror(errno));goto err2;}for ( ; ; ) {//memset(buf, 0x00, buf_bytes); //buf清零ret = snd_pcm_readi(pcm, buf, period_size);//读取PCM数据 一个周期if (0 > ret) {fprintf(stderr, "snd_pcm_readi error: %s\n", snd_strerror(ret));goto err3;}// snd_pcm_readi的返回值ret等于实际读取的帧数 * 4 转为字节数ret = write(fd, buf, ret * 4); //将读取到的数据写入文件中if (0 >= ret)goto err3;}err3:close(fd); //关闭文件err2:free(buf); //释放内存err1:snd_pcm_close(pcm); //关闭pcm设备exit(EXIT_FAILURE);}

在main()函数中,首先对参数进行了校验,执行测试程序需要用户传入一个参数,指定输出文件,因为示例程序中会将录制的音频数据保存到该文件中。

接着调用自定义函数snd_pcm_init()对PCM设备进行初始化,在snd_pcm_init()函数中,首先调用alsa-lib库函数snd_pcm_open()打开PCM录音设备,接着对PCM设备硬件参数进行设置,访问类型设置交错模式SND_PCM_ACCESS_RW_INTERLEAVED、数据格式设置为SND_PCM_FORMAT_S16_LE、采样率设置为44100、双声道、周期大小设置为1024帧、buffer大小设置为16个周期。

回到main()函数,调用C库函数malloc()申请分配一个缓冲区,用于存放从驱动层环形缓冲区buffer读取出来的音频数据。并打开一个新建文件(因为使用了O_CREAT | O_EXCL标志)。

一切准备好之后,就可以进行音频录制了,在for循环中,首先调用alsa-lib库函数snd_pcm_readi()从环形缓冲区中读取音频设备采集到的音频数据,读取出来之后调用write()函数将数据写入到文件中。示例程序中调用snd_pcm_open()时使用的是阻塞方式,当环形缓冲区buffer中有数据可读时,调用snd_pcm_readi()并不会阻塞,而是读取出数据、然后返回;调用一次snd_pcm_readi()读取一个周期、调用一次再读取一个周期;当环形缓冲区为空时,调用snd_pcm_readi()会阻塞,直到音频设备采集到一个周期数据、此时被阻塞snd_pcm_readi()调用被唤醒、读取这一个周期然后返回。

编译示例代码

接下来我们编译示例代码,如下所示:

${CC} -o testApp testApp.c -lasound

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.13 编译示例代码

测试应用程序

将编译得到的可执行文件拷贝到开发板Linux系统/home/root目录下,在执行测试程序之前,我们需要对声卡进行配置,同样使用amixer工具进行配置,如下:

amixer sset Capture 58,58 //录制音量大小amixer sset 'ADC PCM' 200,200 //PCM ADC# 左声道Mixer Boost管理amixer sset 'Left Input Mixer Boost' offamixer sset 'Left Boost Mixer LINPUT1' offamixer sset 'Left Input Boost Mixer LINPUT1' 0amixer sset 'Left Boost Mixer LINPUT2' offamixer sset 'Left Input Boost Mixer LINPUT2' 0amixer sset 'Left Boost Mixer LINPUT3' offamixer sset 'Left Input Boost Mixer LINPUT3' 0# 右声道Mixer Boost管理amixer sset 'Right Input Mixer Boost' onamixer sset 'Right Boost Mixer RINPUT1' onamixer sset 'Right Input Boost Mixer RINPUT1' 5amixer sset 'Right Boost Mixer RINPUT2' onamixer sset 'Right Input Boost Mixer RINPUT2' 5amixer sset 'Right Boost Mixer RINPUT3' offamixer sset 'Right Input Boost Mixer RINPUT3' 0

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.14 声卡配置(录音)

左右声道的Mixer Boost(混音器增强)为什么要这样去配置?这个与硬件设计有关系,我们就不去解释这个了。具体详情可以参考《I.MX6U嵌入式Linux驱动开发指南》文档中音频驱动章节的内容。

接下来,执行测试程序进行录音,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.15 录音

执行测试程序之后,就开始录音了,接着我们可以对着底板上的麦(MIC)说话,板载的MIC如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.16 板载麦克风

程序就会把我们说的话录进去;如果想要停止录音、只能终止进程,按Ctrl C终止应用程序;此时在当前目录下会生成cap.wav音频文件,如下所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.17 生成cap.wav文件

生成的文件是一个纯音频数据的文件,并不是WAV格式的文件,因为这个文件没有头部信息,程序中如果检测到该文件不是WAV格式文件、会直接退出,所以不能直接使用上小节29.5.5的测试程序播放cap.wav文件,这里要注意!当然你可以对上小节的示例代码进行修改,也可直接使用aplay工具播放这段录制的音频,如下:

aplay -f cd cap.wav

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.18 使用aplay播放录制的音频

如果录制正常,使用aplay播放出来的声音就是我们录制的声音!

LINE_IN测试

除了麦克风之外,开发板底板上还有一个LINE_IN接口,也就是线路输入,如下图所示:

正点原子linux 开发版评测,正点原子预售款STM32MP157开发板

图 29.5.19 LINE_IN接口

上图中左边的是耳机接口、右边的是LINE_IN接口,支持音频输入,我们通过本测试程序对LINE_IN接口进行测试,采集LINE_IN接口输入的音频。测试时我们使用一根3.5mm公对公音频线,一头连接到手机或者电脑、另外一头连接到LINE_IN接口上,然后手机或电脑端播放音乐,那么音频数据就会通过LINE_IN接口输入到开发板被我们的应用程序采集(录制)。

在测试之前,我们需要对声卡进行配置,如下所示:

amixer sset Capture 58,58 //录制音量大小amixer sset 'ADC PCM' 200,200 //PCM ADC# 左声道Mixer Boost管理amixer sset 'Left Input Mixer Boost' offamixer sset 'Left Boost Mixer LINPUT1' offamixer sset 'Left Input Boost Mixer LINPUT1' 0amixer sset 'Left Boost Mixer LINPUT2' onamixer sset 'Left Input Boost Mixer LINPUT2' 5amixer sset 'Left Boost Mixer LINPUT3' offamixer sset 'Left Input Boost Mixer LINPUT3' 0# 右声道Mixer Boost管理amixer sset 'Right Input Mixer Boost' onamixer sset 'Right Boost Mixer RINPUT1' offamixer sset 'Right Input Boost Mixer RINPUT1' 0amixer sset 'Right Boost Mixer RINPUT2' offamixer sset 'Right Input Boost Mixer RINPUT2' 0amixer sset 'Right Boost Mixer RINPUT3' onamixer sset 'Right Input Boost Mixer RINPUT3' 5

配置 好之后就可以进行测试了,执行程序之后,手机或电脑端播放音乐,开发板采集从LINE_IN接口输入的音频数据,测试方式跟MIC麦克风一样,大家自己去测试!

这就是关于《正点原子linux 开发版评测,正点原子预售款STM32MP157开发板》的所有内容,希望对您能有所帮助!更多的知识请继续关注《犇涌向乾》百科知识网站:http://www.029ztxx.com!

版权声明: 本站仅提供信息存储空间服务,旨在传递更多信息,不拥有所有权,不承担相关法律责任,不代表本网赞同其观点和对其真实性负责。如因作品内容、版权和其它问题需要同本网联系的,请发送邮件至 举报,一经查实,本站将立刻删除。

猜你喜欢