读取USB摄像头的音频数据

音视频 专栏收录该内容
8 篇文章 3 订阅


之前的一直在操作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文件格式如下图所示:
在这里插入图片描述
wav文件分为RIFF区、format区、data区,下面进行详细说明

RIFF区

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

fromat区

名称偏移量字节数存储格式内容
ChunkID0x000x04大端fmt(0x666D7420)
ChunkSize0x040x04小端0x10
AudioFormat0x080x02小端数据格式,1表示PCM数据格式
NumChannel0x0A0x02小端音频通道数量
SampleRate0x0C0x04小端音频采样率
ByteRate0x100x04小端每秒钟音频数据字节数
BlockAlign0x140x02小端每个采样数据块字节数
BitsPerSample0x160x02小端每个采样量化数据的位数
  • 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位采样

常见音频数据格式:
在这里插入图片描述

data区

名称偏移量字节数存储格式内容
ChunkID0x000x04大端data(0x64617461)
ChunkSize0x040x04小端音频数据字节数
data0x080x04小端实际的音频数据
  • 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的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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sched.h>
#include <pthread.h>
#include <time.h>
#include <sys/sem.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/fs.h>
#include <dirent.h>
#include <sys/poll.h>
#include <assert.h>

#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进行简单测试,发现文件在播放时,视频瞬间播放完毕,音频正常播放,估计是合成文件时不同步的原因,有待进一步查明。

  • 3
    点赞
  • 0
    评论
  • 6
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值