读取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
- 1
- 2
- 3
- 1
- 2
- 3
命令录制音频为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区
名称 | 偏移量 | 字节数 | 存储格式 | 内容 |
---|---|---|---|---|
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位采样
常见音频数据格式:
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的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.c
、mixer.c
、pcm.c
;三个源代码文件以及头文件,因此可以直接将其添加到自己的工程文件中。
在读取USB设备的音频之前,必须先找出其对应的设备文件,USB摄像头会生成2种设备文件,分别是video类与Audio类。
如下代码首先定义一个USB摄像头的数据结构,以下定义的数据结构为我自己的工程应用中的简化版
#define USB_VIDEO_BUF_REQ_CNT16typedef struct camera_node{struct list_head node;char id[16];int ch;//V4L2char devname[16];int fd;struct v4l2_format fmt;struct v4l2_streamparm parm;struct v4l2_requestbuffers req;struct buffer *buffers;int n_buffers;//ALSAint 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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
程序需要在
/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, 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/nameif(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;}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 229
- 230
- 231
- 232
- 233
- 234
- 235
- 236
- 237
- 238
- 239
- 240
- 241
- 242
- 243
- 244
- 245
- 246
- 247
- 248
- 249
- 250
- 251
- 252
- 253
- 254
- 255
- 256
- 257
- 258
- 259
- 260
- 261
- 262
- 263
- 264
- 265
- 266
- 267
- 268
- 269
最后使用Poll的方式进行读取操作,代码如下:
usb_av_poll[0].fd = *(int*)usb_node->pcm;//注意此处的赋值方式,因为usb_node->pcm对应的数据结构在pcm.c文件中定义,//其第一个成员为PCM设备对应的文件描述符fd,//不能使用指针指向方式,因而将首地址转为int型指针,取出的值即为文件描述符fdusb_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;}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
经过测试,以上代码在poll的第一次返回时,会带上的POLLERR标志位,不知是什么原因,但是可以忽略此错误标志,正常读取音频数据。
至此USB摄像头的视频与音频都能够进行读取,下一步就要将音频与视频合成为一个音视频文件,进行播放;经过使用Gstreamer进行简单测试,发现文件在播放时,视频瞬间播放完毕,音频正常播放,估计是合成文件时不同步的原因,有待进一步查明。