读取USB摄像头的音频数据

文章目录

  • 命令操作USB音频设备文件
  • wav文件格式解析
    • RIFF区
    • fromat区
    • data区
  • gstreamer 合成音频为MP3文件
  • 用代码读取USB音频文件

之前的一直在操作USB摄像头的视频数据,如今需要读取USB摄像头的音频数据,进行音视频的合成。读取音频数据需要Linux层的ALSA驱动支持,应用层可以采用alsa-lib库,也可以采用tinyalsa库。我这里用的摄像头是罗技C920。

命令操作USB音频设备文件

可以通过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文件格式如下图所示:
读取USB摄像头的音频数据_第1张图片
wav文件分为RIFF区、format区、data区,下面进行详细说明

RIFF区

名称 偏移量 字节数 存储格式 内容
ChunkID 0x00 0x04 大端 RIFF(0x52494646)
ChunkSize 0x04 0x04 小端 文件总长度 - 8,不含ChunkID与ChunkSize的字节数
Format 0x08 0x04 大端 ‘WAVE’(0x57415645),文件格式名称
  • wav文件的开始均以’RIFF’为标识
  • ChunkSize:是整个文件的长度减去ChunkID和ChunkSize本身的长度
  • format:用‘WAVE’4个字符表示为wav格式的音频文件

fromat区

名称 偏移量 字节数 存储格式 内容
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 小端 每个采样量化数据的位数
  • ChunkID:fromat区以fmt字符为标识
  • ChunkSize:表示该区块数据的长度,固定为16字节
  • AudioFormat:表示Data区块存储的音频数据的格式,PCM格式值为1
  • NumChannels:表示音频数据的声道数,1:单声道,2:双声道
  • SampleRate:表示音频数据的采样率
  • ByteRate:每秒数据字节数 = SampleRate * NumChannels * BitsPerSample / 8
  • BlockAlign:每个采样所需的字节数 = NumChannels * BitsPerSample / 8
  • BitsPerSample:每个采样存储的bit数,8:8bit,16:16bit,32:32bit,一般采用16位采样

常见音频数据格式:
读取USB摄像头的音频数据_第2张图片

data区

名称 偏移量 字节数 存储格式 内容
ChunkID 0x00 0x04 大端 data(0x64617461)
ChunkSize 0x04 0x04 小端 音频数据字节数
data 0x08 0x04 小端 实际的音频数据
  • wav文件的开始均以’RIFF’为标识
  • ChunkSize:是整个文件的长度减去ChunkID和ChunkSize本身的长度
  • format:用‘WAVE’4个字符表示为wav格式的音频文件

从以上可以看出,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

gstreamer 合成音频为MP3文件

gst-launch-1.0 alsasrc device="hw:2,0" ! audioconvert ! imxmp3enc ! filesink location=mic.mp3
查看MP3文件信息如下:
读取USB摄像头的音频数据_第3张图片
这里遇到一个问题,当将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.

用代码读取USB音频文件

使用代码有两种方式:

  • 使用alsa-lib库,
  • 使用tinyalsa-lib库

由于tinyalsa-lib库比较轻量,所以我使用这个库,其仅包含源文件limits.cmixer.cpcm.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, 0sizeof(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进行简单测试,发现文件在播放时,视频瞬间播放完毕,音频正常播放,估计是合成文件时不同步的原因,有待进一步查明。

你可能感兴趣的:(音视频)