Linux下音频编程
一、ALSA音频架构简单介绍
ALSA是Advanced Linux Sound Architecture,高级Linux声音架构的简称,它在Linux操作系统上提供了音频和MIDI(Musical Instrument Digital Interface,音乐设备数字化接口)的支持。在2.6系列内核中,ALSA已经成为默认的声音子系统,用来替换2.4系列内核中的OSS(Open Sound System。开放声音系统)。ALSA的主要特性包含:高效地支持从消费类入门级声卡到专业级音频设备全部类型的音频接口,全然模块化的设计。 支持对称多处理(SMP)和线程安全。对OSS的向后兼容,以及提供了用户空间的alsa-lib库来简化应用程序的开发。
ALSA是一个全然开放源码的音频驱动程序集,除了像OSS那样提供了一组内核驱动程序模块之外,ALSA还专门为简化应用程序的编写提供了对应的函数库,与OSS提供的基于ioctl的原始编程接口相比。ALSA函数库使用起来要更加方便一些。利用该函数库,开发人员能够方便快捷的开发出自己的应用程序,细节则留给函数库内部处理。当然 ALSA也提供了类似于OSS的系统接口,只是ALSA的开发人员建议应用程序开发人员使用音频函数库而不是驱动程序的API。
二、下载安装alsa-lib库
在ubuntu系统上安装alsa-lib库方法:
sudo apt-get install libasound2-dev
如果是在其他发行版linux系统上或者需要在嵌入式linux系统上使用alsa-lib库,可以下载alsa-lib源码包,自行编译。
开源ALSA架构的官网地址:https://www.alsa-project.org/wiki/Main_Page
图2-1
图2-2
图2-3
三、获取本机上所有声卡设备
3.1 通过arecord -L命令获取
获取声卡可以使用arecord -L命令。
在ubuntu系统下如果没有这个命令,直接根据提示安装一个即可。
示例:
wbyq@wbyq:/mnt/hgfs/linux-share-dir/linux_c/linux_pcm_save$ arecord -L
default
Playback/recording through the PulseAudio sound server
null
Discard all samples (playback) or generate zero samples (capture)
pulse
PulseAudio Sound Server
sysdefault:CARD=AudioPCI
Ensoniq AudioPCI, ES1371 DAC2/ADC
Default Audio Device
front:CARD=AudioPCI,DEV=0
Ensoniq AudioPCI, ES1371 DAC2/ADC
Front speakers
iec958:CARD=AudioPCI,DEV=0
Ensoniq AudioPCI, ES1371 DAC2/ADC
IEC958 (S/PDIF) Digital Audio Output
dmix:CARD=AudioPCI,DEV=0
Ensoniq AudioPCI, ES1371 DAC2/ADC
Direct sample mixing device
dsnoop:CARD=AudioPCI,DEV=0
Ensoniq AudioPCI, ES1371 DAC2/ADC
Direct sample snooping device
hw:CARD=AudioPCI,DEV=0
Ensoniq AudioPCI, ES1371 DAC2/ADC
Direct hardware device without any conversions
plughw:CARD=AudioPCI,DEV=0
Ensoniq AudioPCI, ES1371 DAC2/ADC
Hardware device with all software conversions
3.2 通过代码获取
#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
static void device_list(void)
{
snd_ctl_t *handle;
int card, err, dev,idx;
snd_ctl_card_info_t *info;
snd_pcm_info_t *pcminfo;
snd_ctl_card_info_alloca(&info);
snd_pcm_info_alloca(&pcminfo);
card = -1;
if (snd_card_next(&card) < 0 || card < 0) {
error(_("no soundcards found..."));
return;
}
printf(_("**** List of %s Hardware Devices ****/n"),
snd_pcm_stream_name(stream));
while (card >= 0) {
char name[32];
sprintf(name, "hw:%d", card);
if ((err = snd_ctl_open(&handle, name, 0)) < 0) {
error("control open (%i): %s", card, snd_strerror(err));
goto next_card;
}
if ((err = snd_ctl_card_info(handle, info)) < 0) {
error("control hardware info (%i): %s", card, snd_strerror(err));
snd_ctl_close(handle);
goto next_card;
}
dev = -1;
while (1) {
unsigned int count;
if (snd_ctl_pcm_next_device(handle, &dev)<0)
error("snd_ctl_pcm_next_device");
if (dev < 0)
break;
snd_pcm_info_set_device(pcminfo, dev);
snd_pcm_info_set_subdevice(pcminfo, 0);
snd_pcm_info_set_stream(pcminfo, stream);
if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0) {
if (err != -ENOENT)
error("control digital audio info (%i): %s", card, snd_strerror(err));
continue;
}
printf(_("card %i: %s [%s], device %i: %s [%s]/n"),
card, snd_ctl_card_info_get_id(info), snd_ctl_card_info_get_name(info),
dev,
snd_pcm_info_get_id(pcminfo),
snd_pcm_info_get_name(pcminfo));
count = snd_pcm_info_get_subdevices_count(pcminfo);
printf( _(" Subdevices: %i/%i/n"),snd_pcm_info_get_subdevices_avail(pcminfo), count);
for(idx = 0; idx < (int)count; idx++)
{
snd_pcm_info_set_subdevice(pcminfo, idx);
if ((err = snd_ctl_pcm_info(handle, pcminfo)) < 0)
{
error("control digital audio playback info (%i): %s", card, snd_strerror(err));
}
else
{
printf(_(" Subdevice #%i: %s/n"),idx, snd_pcm_info_get_subdevice_name(pcminfo));
}
}
}
snd_ctl_close(handle);
next_card:
if(snd_card_next(&card)<0)
{
error("snd_card_next");
break;
}
}
}
int main(int argc,char **argv)
{
device_list();
return 0;
}
四、读取声卡设备PCM数据保存到文件
程序功能: 读取声卡设备的PCM原始数据保存到文件。
参考资料: https://www.alsa-project.org/wiki/ALSA_Library_API
https://users.suse.com/~mana/alsa090_howto.html
示例代码:
/*
进行音频采集,采集pcm数据并直接保存pcm数据
音频参数:
声道数: 2
采样位数: 16bit、LE格式
采样频率: 44100Hz
*/
#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
#include <signal.h>
FILE *pcm_data_file=NULL;
int run_flag=0;
void exit_sighandler(int sig)
{
run_flag=1;
}
int main(int argc, char *argv[])
{
int i;
int err;
char *buffer;
int buffer_frames = 128;
unsigned int rate = 44100;// 常用的采样频率: 44100Hz 、16000HZ、8000HZ、48000HZ、22050HZ
snd_pcm_t *capture_handle;// 一个指向PCM设备的句柄
snd_pcm_hw_params_t *hw_params; //此结构包含有关硬件的信息,可用于指定PCM流的配置
/*注册信号捕获退出接口*/
signal(2,exit_sighandler);
/*PCM的采样格式在pcm.h文件里有定义*/
snd_pcm_format_t format=SND_PCM_FORMAT_S16_LE; // 采样位数:16bit、LE格式
/*打开音频采集卡硬件,并判断硬件是否打开成功,若打开失败则打印出错误提示*/
if ((err = snd_pcm_open (&capture_handle, argv[1],SND_PCM_STREAM_CAPTURE,0))<0)
{
printf("无法打开音频设备: %s (%s)\n", argv[1],snd_strerror (err));
exit(1);
}
printf("音频接口打开成功.\n");
/*创建一个保存PCM数据的文件*/
if((pcm_data_file = fopen(argv[2], "wb")) == NULL)
{
printf("无法创建%s音频文件.\n",argv[2]);
exit(1);
}
printf("用于录制的音频文件已打开.\n");
/*分配硬件参数结构对象,并判断是否分配成功*/
if((err = snd_pcm_hw_params_malloc(&hw_params)) < 0)
{
printf("无法分配硬件参数结构%s)\n",snd_strerror(err));
exit(1);
}
printf("硬件参数结构已分配成功.\n");
/*按照默认设置对硬件对象进行设置,并判断是否设置成功*/
if((err=snd_pcm_hw_params_any(capture_handle,hw_params)) < 0)
{
printf("无法初始化硬件参数结构%s)\n", snd_strerror(err));
exit(1);
}
printf("硬件参数结构初始化成功.\n");
/*
设置数据为交叉模式,并判断是否设置成功
交叉/非交叉模式。
表示在多声道数据传输的过程中是采样交叉的模式还是非交叉的模式。
对多声道数据,如果采样交叉模式,使用一块buffer即可,其中各声道的数据交叉传输;
如果使用非交叉模式,需要为各声道分别分配一个buffer,各声道数据分别传输。
*/
if((err = snd_pcm_hw_params_set_access (capture_handle,hw_params,SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
{
printf("无法设置访问类型(%s)\n",snd_strerror(err));
exit(1);
}
printf("访问类型设置成功.\n");
/*设置数据编码格式,并判断是否设置成功*/
if ((err=snd_pcm_hw_params_set_format(capture_handle, hw_params,format)) < 0)
{
printf("无法设置格式%s)\n",snd_strerror(err));
exit(1);
}
fprintf(stdout, "PCM数据格式设置成功.\n");
/*设置采样频率,并判断是否设置成功*/
if((err=snd_pcm_hw_params_set_rate_near (capture_handle,hw_params,&rate,0))<0)
{
printf("无法设置采样率(%s)\n",snd_strerror(err));
exit(1);
}
printf("采样率设置成功\n");
/*设置声道,并判断是否设置成功*/
if((err = snd_pcm_hw_params_set_channels(capture_handle, hw_params,2)) < 0)
{
printf("无法设置声道数(%s)\n",snd_strerror(err));
exit(1);
}
printf("声道数设置成功.\n");
/*将配置写入驱动程序中,并判断是否配置成功*/
if ((err=snd_pcm_hw_params (capture_handle,hw_params))<0)
{
printf("无法向驱动程序设置参数(%s)\n",snd_strerror(err));
exit(1);
}
printf("参数设置成功.\n");
/*使采集卡处于空闲状态*/
snd_pcm_hw_params_free(hw_params);
/*准备音频接口,并判断是否准备好*/
if((err=snd_pcm_prepare(capture_handle))<0)
{
printf("无法使用音频接口%s)\n",snd_strerror(err));
exit(1);
}
printf("音频接口准备好.\n");
/*配置一个数据缓冲区用来缓冲数据*/
buffer=malloc(128*snd_pcm_format_width(format)/8*2);
printf("缓冲区分配成功.\n");
/*开始采集音频pcm数据*/
printf("开始采集数据...\n");
while(1)
{
/*从声卡设备读取一帧音频数据*/
if((err=snd_pcm_readi(capture_handle,buffer,buffer_frames))!=buffer_frames)
{
printf("从音频接口读取失败(%s)\n",snd_strerror(err));
exit(1);
}
/*写数据到文件*/
fwrite(buffer,(buffer_frames*2),sizeof(short),pcm_data_file);
if(run_flag)
{
printf("停止采集.\n");
break;
}
}
/*释放数据缓冲区*/
free(buffer);
/*关闭音频采集卡硬件*/
snd_pcm_close(capture_handle);
/*关闭文件流*/
fclose(pcm_data_file);
return 0;
}
编译示例:
gcc linux_pcm_save.c -lasound
运行示例: (选择系统默认声卡)
./a.out hw:0 123.pcm
五、播放采集的PCM声音
Windows下可以使用audacity软件进行播放PCM裸流数据。
或者通过ffplay工具(安装ffmpeg会带上这个工具)。
ffplay -ar 44100 -channels 2 -f s16le -i 123.pcm
图5-1
图5-2
图5-3
六、编码PCM数据到wav格式文件
struct WAVFILEHEADER
{
// RIFF 头
char RiffName[4];
unsigned long nRiffLength;
// 数据类型标识符
char WavName[4];
// 格式块中的块头
char FmtName[4];
unsigned long nFmtLength;
// 格式块中的块数据
unsigned short nAudioFormat;
unsigned short nChannleNumber;
unsigned long nSampleRate;
unsigned long nBytesPerSecond;
unsigned short nBytesPerSample;
unsigned short nBitsPerSample;
// 数据块中的块头
char DATANAME[4];
unsigned long nDataLength;
};
// 将生成的.raw文件转成.wav格式文件;
qint64 MainWindow::CreateWavFile(QString PcmFileName, QString wavFileName)
{
// 开始设置WAV的文件头
WAVFILEHEADER WavFileHeader;
qstrcpy(WavFileHeader.RiffName, "RIFF");
qstrcpy(WavFileHeader.WavName, "WAVE");
qstrcpy(WavFileHeader.FmtName, "fmt ");
qstrcpy(WavFileHeader.DATANAME, "data");
// 表示 FMT块 的长度
WavFileHeader.nFmtLength = 16;
// 表示 按照PCM 编码;
WavFileHeader.nAudioFormat = 1;
// 声道数目;
WavFileHeader.nChannleNumber = 1;
// 采样频率;
WavFileHeader.nSampleRate = 16000;
// nBytesPerSample 和 nBytesPerSecond这两个值通过设置的参数计算得到;
// 数据块对齐单位(每个采样需要的字节数 = 通道数 × 每次采样得到的样本数据位数 / 8 )
WavFileHeader.nBytesPerSample = 2;
// 波形数据传输速率
// (每秒平均字节数 = 采样频率 × 通道数 × 每次采样得到的样本数据位数 / 8 = 采样频率 × 每个采样需要的字节数 )
WavFileHeader.nBytesPerSecond = 32000;
// 每次采样得到的样本数据位数;
WavFileHeader.nBitsPerSample = 16;
QFile cacheFile(PcmFileName);
QFile wavFile(wavFileName);
if (!cacheFile.open(QIODevice::ReadWrite))
{
return -1;
}
if (!wavFile.open(QIODevice::WriteOnly))
{
return -2;
}
int nSize = sizeof(WavFileHeader);
qint64 nFileLen = cacheFile.bytesAvailable();
WavFileHeader.nRiffLength = static_cast<unsigned long>(nFileLen - 8 + nSize);
//static_cast<类型>(变量); //新式的强制转换
WavFileHeader.nDataLength = static_cast<unsigned long>(nFileLen);
// 先将wav文件头信息写入,再将音频数据写入;
wavFile.write((const char *)&WavFileHeader, nSize);
wavFile.write(cacheFile.readAll());
cacheFile.close();
wavFile.close();
return nFileLen;
}