可以通过alsa-lib库编译出的工具arecord
查看USB的音频设备文件,命令:arecord -l
card 2: C920 [HD Pro Webcam C920], device 0: USB Audio [USB Audio]
Subdevices: 1/1
Subdevice #0: subdevice #0
命令录制音频为WAV文件:arecord -D hw:2,0 -d 10 -f cd -r 32000 -c 2 -t wav test.wav
,命令输出如下:
Recording WAVE 'test.wav' : Signed 16 bit Little Endian, Rate 32000 Hz, Stereo
既然提到wav文件,这里就不得不写一下wav文件的格式,以备后面查阅
wav文件格式如下图所示:
wav文件分为RIFF区、format区、data区,下面进行详细说明
名称 | 偏移量 | 字节数 | 存储格式 | 内容 |
---|---|---|---|---|
ChunkID | 0x00 | 0x04 | 大端 | RIFF (0x52494646) |
ChunkSize | 0x04 | 0x04 | 小端 | 文件总长度 - 8,不含ChunkID与ChunkSize的字节数 |
Format | 0x08 | 0x04 | 大端 | ‘WAVE’(0x57415645),文件格式名称 |
名称 | 偏移量 | 字节数 | 存储格式 | 内容 |
---|---|---|---|---|
ChunkID | 0x00 | 0x04 | 大端 | fmt (0x666D7420) |
ChunkSize | 0x04 | 0x04 | 小端 | 0x10 |
AudioFormat | 0x08 | 0x02 | 小端 | 数据格式,1表示PCM数据格式 |
NumChannel | 0x0A | 0x02 | 小端 | 音频通道数量 |
SampleRate | 0x0C | 0x04 | 小端 | 音频采样率 |
ByteRate | 0x10 | 0x04 | 小端 | 每秒钟音频数据字节数 |
BlockAlign | 0x14 | 0x02 | 小端 | 每个采样数据块字节数 |
BitsPerSample | 0x16 | 0x02 | 小端 | 每个采样量化数据的位数 |
fmt
字符为标识名称 | 偏移量 | 字节数 | 存储格式 | 内容 |
---|---|---|---|---|
ChunkID | 0x00 | 0x04 | 大端 | data (0x64617461) |
ChunkSize | 0x04 | 0x04 | 小端 | 音频数据字节数 |
data | 0x08 | 0x04 | 小端 | 实际的音频数据 |
从以上可以看出,wav格式的文件起始部分占用44字节,经过以上命令保存的wav格式文件示例如下:
52 49 46 46 24 88 13 00 57 41 56 45 66 6D 74 20 10 00 00 00 01 00 02 00 00 7D 00 00 00 F4 01 00 04 00 10 00 64 61 74 61 00 88 13 00
gst-launch-1.0 alsasrc device="hw:2,0" ! audioconvert ! imxmp3enc ! filesink location=mic.mp3
查看MP3文件信息如下:
这里遇到一个问题,当将USB的H264帧与音频进行合成时,最后视频文件在播放时视频会瞬间播放完成而且会花屏,而音频则数据播放与文件播放时长一致,这个问题还有待查明。
命令如下:
gst-launch-1.0 -e avimux name=mux1 ! filesink location=test.avi \ v4l2src device=/dev/video0 ! video/x-h264, framerate=30/1, width=640, height=360 ! mux1. \ alsasrc device=hw:2,0 ! audio/x-raw, rate=32000, channels=2, layout=interleaved, format=S16LE ! mux1.
使用代码有两种方式:
由于tinyalsa-lib库比较轻量,所以我使用这个库,其仅包含源文件limits.c
、mixer.c
、pcm.c
;三个源代码文件以及头文件,因此可以直接将其添加到自己的工程文件中。
在读取USB设备的音频之前,必须先找出其对应的设备文件,USB摄像头会生成2种设备文件,分别是video类与Audio类。
如下代码首先定义一个USB摄像头的数据结构,以下定义的数据结构为我自己的工程应用中的简化版
#define USB_VIDEO_BUF_REQ_CNT 16
typedef struct camera_node
{
struct list_head node;
char id[16];
int ch;
//V4L2
char devname[16];
int fd;
struct v4l2_format fmt;
struct v4l2_streamparm parm;
struct v4l2_requestbuffers req;
struct buffer *buffers;
int n_buffers;
//ALSA
int card;
int device;
unsigned int pcm_bytes_per_frame;
struct pcm_config config;
struct pcm *pcm;
int thread_return_value;
unsigned char ch_online;
unsigned char thread_online;
unsigned char fps;
unsigned char has_audio;
unsigned int width;
unsigned int height;
unsigned int bitrate;
unsigned int audio_buf_head;
unsigned char audio_buf[AUDIO_BUF_SIZE];
}camera_struct;
程序需要在/sys/class/video4linux
目录查找视频设备的文件的信息,在/sys/class/sound
目录查找音频设备文件的信息,代码如下:
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define VIDEO4LINUX_PATH "/sys/class/video4linux"
#define AUDIO_PATH "/sys/class/sound"
#define CAMERA_PRODUCT0 "HD Pro Webcam C920"
#define CAMERA_PRODUCT1 "HD USB Camera"
#define USB_PORT_CHANNEL0 0
#define USB_PORT_CHANNEL1 1
#define USB_PORT_CHANNEL2 2
#define USB_PORT_CHANNEL3 3
#define MAX_USB_CHANNEL (4)
#define RECORD_BUF_HOLE (96*1024)
#define VIDEO_BUF_SIZE (48 * 1024 * 1024)
#define AUDIO_BUF_HOLE (8*1024)
#define AUDIO_BUF_SIZE (16 * 1024 * 1024)
camera_struct usbcamera[MAX_USB_CHANNEL];
static long int get_card_no(char *name)
{
char *find_controlC = NULL;
char cardno[128] = {0};
int i = 0;
int j = 0;
long int num = 0;
find_controlC = strstr(name, "controlC");
for(;;)
{
if(j > 64 || !isdigit(find_controlC[i + 8]))
{
break;
}
cardno[j++] = find_controlC[i + 8];
i++;
}
cardno[j] = '\0';
num = strtoul(cardno, NULL, 10);
return num;
}
static char *xreadlink(const char *path)
{
static const int GROWBY = 80; /* how large we will grow strings by */
char *buf = NULL;
int bufsize = 0, readsize = 0;
do {
buf = realloc(buf, bufsize += GROWBY);
readsize = readlink(path, buf, bufsize); /* 1st try */
if (readsize == -1) {
free(buf);
return NULL;
}
}
while (bufsize < readsize + 1);
buf[readsize] = '\0';
return buf;
}
static int update_camera_video(char *name, int port)
{
struct camera_node *usb_node = NULL;
if(port >= MAX_USB_CHANNEL)
{
printf("update_camera_video faild, port:%d > MAX_USB_CHANNEL!\n", port);
return -1;
}
usb_node = &usbcamera[port];
if(usb_node->thread_online == 0)
{
sprintf(usb_node->id, "%s", "5357:3001");
sprintf(usb_node->devname, "/dev/%s", name);
usb_node->ch = port;
usb_node->ch_online = 1;
printf("update_camera_video name:%s, id:%s, dev name:%s port:%d\n",
name, usb_node->id, usb_node->devname, port);
}
return 0;
}
static int update_camera_audio(int port, int card, int device)
{
struct camera_node *usb_node;
if(port >= MAX_USB_CHANNEL)
{
printf("update_camera_audio faild, port:%d > MAX_USB_CHANNEL!\n", port);
return -1;
}
usb_node = &usbcamera[port];
if((usb_node->ch == port) && (usb_node->thread_online == 0))
{
usb_node->card = card;
usb_node->device = device;
usb_node->has_audio = 1;
printf("usb_node->has_audio:%d, update_camera_audio port %d card %d\n", usb_node->has_audio, port, card);
memset(&usb_node->config, 0, sizeof(usb_node->config));
usb_node->config.channels = 2;
usb_node->config.rate = 16000;
usb_node->config.period_size = 1024;
usb_node->config.period_count = 2;
usb_node->config.format = PCM_FORMAT_S16_LE;
usb_node->config.start_threshold = 0;//usb_node->config.period_count * usb_node->config.period_size;
usb_node->config.stop_threshold = 0;//usb_node->config.period_count * usb_node->config.period_size;;
usb_node->config.silence_threshold = 0;
}
return 0;
}
//path = "/sys/class/video4linux/video0"
int find_camera(char *path, int *port, char *usb_path)
{
FILE *fp = NULL;
int ret = -1;
char name[128] = {0};
char dev_name[128] = {0};
char part0[16] = {0};
char part1[16] = {0};
char index = -1;
char *device_link = NULL;
long int num = 0;
sprintf(name, "%s/name", path);
fp = fopen(name, "r");
ret = fread(dev_name, 1, 128, fp);
dev_name[ret - 1] = '\0';
fclose(fp);
//以上步骤用于获取设备节点的名称:/sys/class/video4linux/video0/name
if(strcmp(dev_name, CAMERA_PRODUCT0) == 0 || strcmp(dev_name, CAMERA_PRODUCT1) == 0)
{
memset(name, 0, sizeof(name));
sprintf(name, "%s/device", path);
device_link = xreadlink(name);
//printf("device_link %s\n", device_link); // device_link: "../../../2-1.2:1.0"
sscanf(strrchr(device_link, '/') + 1, "%[^:]:%s", part0, part1);
//格式化输出字符串,“%[^:]:%s”,表示将device_link最后一次出现/字符之后的字符按照:分割,分别放到part0、part1,其中part0取到:截止,不包含:
if(strrchr(part0, '.'))
{
num = strtoul(strrchr(part0, '.') + 1, NULL, 10); //字符.后面的数字就是USB通道号
*port = num;
strcpy(usb_path, part0);
printf("path:%s, device link:%s, part0 %s part1 %s ch %ld\n", name, device_link, part0, part1, num-1);
}
else
{
*port = -1;
}
free(device_link);
return 1;
}
return 0;
}
int usbcamera_scan(void)
{
int ret = -1;
DIR *vdir;
DIR *adir;
struct dirent *ptr;
struct dirent *aptr;
char path[64] = {0};
char usb_path[16] = {0};
int usb_port = -1;
int cardno = -1;
char *card_link = NULL;
vdir = opendir(VIDEO4LINUX_PATH);
if(vdir == NULL)
{
printf("open vdir %s failed\n", VIDEO4LINUX_PATH);
return -1;
}
while((ptr = readdir(vdir)) != NULL)
{
//printf("ptr->d_name = %s ptr->d_type = %d strlen(ptr->d_name) = %d\n",ptr->d_name,ptr->d_type, strlen(ptr->d_name));
if(strcmp(ptr->d_name,".") == 0 || strcmp(ptr->d_name,"..") == 0)
{
continue;
}
// "/sys/class/video4linux/video0"
sprintf(path, "%s/%s", VIDEO4LINUX_PATH, ptr->d_name);
ret = find_camera(path, &usb_port, usb_path);
if(ret == 0)
{
continue;
}
ret = update_camera_video(ptr->d_name, usb_port - 1);
if(ret < 0)
{
continue;
}
adir = opendir(AUDIO_PATH);
if(adir == NULL)
{
printf("open adir %s failed\n", AUDIO_PATH);
continue;
}
while((aptr = readdir(adir)) != NULL)
{
if(strcmp(aptr->d_name,".") == 0 || strcmp(aptr->d_name,"..") == 0)
{
continue;
}
memset(path, 0, sizeof(path));
sprintf(path, "%s/%s", AUDIO_PATH, aptr->d_name);
card_link = xreadlink(path);
if(strstr(card_link, "usb") && strstr(card_link, usb_path) && strstr(card_link, "controlC"))
{
cardno = get_card_no(aptr->d_name);
ret = update_camera_audio(usb_port - 1, cardno, 0);
if(ret < 0)
{
continue;
}
}
free(card_link);
}
closedir(adir);
}
closedir(vdir);
return 0;
}
static int audio_init(struct camera_node *usb_node)
{
usb_node->pcm = pcm_open(usb_node->card, usb_node->device, PCM_IN, &usb_node->config);
printf("pcm_open card %d device %d\n", usb_node->card, usb_node->device);
if (!usb_node->pcm || !pcm_is_ready(usb_node->pcm))
{
fprintf(stderr, "Unable to open PCM device (%s)\n", pcm_get_error(usb_node->pcm));
usb_node->pcm = NULL;
return -1;
}
usb_node->pcm_bytes_per_frame = pcm_frames_to_bytes(usb_node->pcm, 1);
printf("ch:%d, pcm_bytes_per_frame:%d\n", usb_node->ch, usb_node->pcm_bytes_per_frame);
return 0;
}
最后使用Poll的方式进行读取操作,代码如下:
usb_av_poll[0].fd = *(int*)usb_node->pcm;
//注意此处的赋值方式,因为usb_node->pcm对应的数据结构在pcm.c文件中定义,
//其第一个成员为PCM设备对应的文件描述符fd,
//不能使用指针指向方式,因而将首地址转为int型指针,取出的值即为文件描述符fd
usb_av_poll[0].events = POLL_IN;
usb_av_poll[0].revents = 0;
while(1)
{
ret = poll(usb_av_poll, 1, -1);
if (usb_av_poll[1].revents & POLLIN)
{
if (unlikely((AUDIO_BUF_SIZE - usb_node->audio_buf_head) < AUDIO_BUF_HOLE))
{
printf("usb ch:%d, audio run a loop, audio_buf_head:%d--------------------------\n",
usb_node->ch, usb_node->audio_buf_head);
usb_node->audio_buf_head = 0;
}
struct timespec tstamp = {0};
unsigned int avail = 0;
unsigned int frames_read = 0;
vbuf = (void *)&usb_node->audio_buf[usb_node->audio_buf_head];
ret = pcm_get_htimestamp(usb_node->pcm, &avail, &tstamp);
frames_read = pcm_readi(usb_node->pcm, vbuf, pcm_get_buffer_size(usb_node->pcm));
usb_av_poll[1].revents = 0;
if(ret < 0) continue;
}
}
经过测试,以上代码在poll的第一次返回时,会带上的POLLERR标志位,不知是什么原因,但是可以忽略此错误标志,正常读取音频数据。
至此USB摄像头的视频与音频都能够进行读取,下一步就要将音频与视频合成为一个音视频文件,进行播放;经过使用Gstreamer进行简单测试,发现文件在播放时,视频瞬间播放完毕,音频正常播放,估计是合成文件时不同步的原因,有待进一步查明。