Audio File 音频文件录制(AudioQueue,AudioUnit,AudioConverter音频来源)

需求:iOS中使用Audio File 实现音频文件录制.


实现原理: 使用Audio File中的API可以将我们采集到的音频数据录制成音频文件,这里采集到的数据包括从Audio Queue/Audio Unit直接采集或Audio Converter间接转换得到的音频数据.


阅读前提:

本文直接为实战篇,如需了解理论基础参考上述链接中的内容,本文侧重于实战中注意点.

本项目需要借助Audio Queue, Audio Unit的采集,才能实现录制.所以提供以下两个Demo.


GitHub地址(附代码) : Audio Queue录制, Audio Unit录制

简书地址 : Audio File Record

掘金地址 : Audio File Record

博客地址 : Audio File Record


具体实现

1. 创建音频文件

这里使用当前格式化时间作为文件名,命名冲突.

下面主要代码为创建一个用于存放声音的音频文件,主要是在沙盒中创建一个目录(名为Voice)存放音频文件.注意,我们一定要先将文件夹创建出来,否则在调用后面AudioFileCreateWithURL函数时将报错.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (NSString *)createFilePath {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @"yyyy_MM_dd__HH_mm_ss";
NSString *date = [dateFormatter stringFromDate:[NSDate date]];

NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask,
YES);

NSString *documentPath = [[searchPaths objectAtIndex:0] stringByAppendingPathComponent:@"Voice"];

// 先创建子目录. 注意,若果直接调用AudioFileCreateWithURL创建一个不存在的目录创建文件会失败
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:documentPath]) {
[fileManager createDirectoryAtPath:documentPath withIntermediateDirectories:YES attributes:nil error:nil];
}

NSString *fullFileName = [NSString stringWithFormat:@"%@.caf",date];
NSString *filePath = [documentPath stringByAppendingPathComponent:fullFileName];
return filePath;
}

2. 创建Audio File

通过上面创建的url,再加上我们要创建的文件类型(iOS中CAF格式文件可以存放任意类型音频数据),音频流的ASBD格式,文件特性的flag,这里设置kAudioFileFlags_EraseFile表明CreateURL调用将清空现有文件的内容,如果未设置,则如果文件已存在则CreateURL调用将失败.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (AudioFileID)createAudioFileWithFilePath:(NSString *)filePath AudioDesc:(AudioStreamBasicDescription)audioDesc {
CFURLRef url = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef)filePath, NULL);
NSLog(@"Audio Recorder: record file path:%@",filePath);

AudioFileID audioFile;
// create the audio file
OSStatus status = AudioFileCreateWithURL(url,
kAudioFileCAFType,
&audioDesc,
kAudioFileFlags_EraseFile,
&audioFile);
if (status != noErr) {
NSLog(@"Audio Recorder: AudioFileCreateWithURL Failed, status:%d",(int)status);
}

CFRelease(url);

return audioFile;
}

magic cookie: 可以理解成是文件的头信息,包含音频文件播放需要的一些必要信息, magic cookie块包含某些音频数据格式(例如MPEG-4 AAC)所需的补充数据,用于解码音频数据。如果CAF文件中包含的音频数据格式需要magic cookie数据,则该文件必须具有此块。

在这里分为两种情况,如果录制文件数据CBR(未压缩数据格式:PCM…),则不需要设置magic cookie, 如果录制文件数据VBR(压缩数据格式:AAC…),则需要设置magic cookie.

注意: 采用不同技术采集到的音频,设置magic cookie的方式是不同的.

  • Audio Queue 设置magic cookie

首先使用kAudioQueueProperty_MagicCookie属性获取当前audio queue是否含有magic cookie,如果有,返回magic cookie长度,然后为它分配一段内存就可以调用kAudioQueueProperty_MagicCookie获取audio queue中的magic cookie,最后,将magic cookie通过kAudioFilePropertyMagicCookieData属性设置到audio file中即可.

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
- (void)copyEncoderCookieToFileByAudioQueue:(AudioQueueRef)inQueue inFile:(AudioFileID)inFile {
OSStatus result = noErr;
UInt32 cookieSize;

result = AudioQueueGetPropertySize (
inQueue,
kAudioQueueProperty_MagicCookie,
&cookieSize
);
if (result == noErr) {
char* magicCookie = (char *) malloc (cookieSize);
result =AudioQueueGetProperty (
inQueue,
kAudioQueueProperty_MagicCookie,
magicCookie,
&cookieSize
);
if (result == noErr) {
result = AudioFileSetProperty (
inFile,
kAudioFilePropertyMagicCookieData,
cookieSize,
magicCookie
);
if (result == noErr) {
NSLog(@"set Magic cookie successful.");
}else {
NSLog(@"set Magic cookie failed.");
}
}else {
NSLog(@"get Magic cookie failed.");
}
free (magicCookie);

}else {
NSLog(@"Magic cookie: get size failed.");
}

}
  • Audio Converter 设置magic cookie

当使用Audio Unit采集音频数据时,我们无法直接采集AAC类型的数据,需要借助Audio Converter,原理同上,即从Audio Converter中获取Magic cookie并设置给audio file.

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
-(void)copyEncoderCookieToFileByAudioConverter:(AudioConverterRef)audioConverter inFile:(AudioFileID)inFile {
// Grab the cookie from the converter and write it to the destination file.
UInt32 cookieSize = 0;
OSStatus error = AudioConverterGetPropertyInfo(audioConverter, kAudioConverterCompressionMagicCookie, &cookieSize, NULL);

if (error == noErr && cookieSize != 0) {
char *cookie = (char *)malloc(cookieSize * sizeof(char));
error = AudioConverterGetProperty(audioConverter, kAudioConverterCompressionMagicCookie, &cookieSize, cookie);

if (error == noErr) {
error = AudioFileSetProperty(inFile, kAudioFilePropertyMagicCookieData, cookieSize, cookie);
if (error == noErr) {
UInt32 willEatTheCookie = false;
error = AudioFileGetPropertyInfo(inFile, kAudioFilePropertyMagicCookieData, NULL, &willEatTheCookie);
if (error == noErr) {
NSLog(@"%@:%s - Writing magic cookie to destination file: %u cookie:%d \n",kModuleName,__func__, (unsigned int)cookieSize, willEatTheCookie);
}else {
NSLog(@"%@:%s - Could not Writing magic cookie to destination file status:%d \n",kModuleName,__func__,(int)error);
}
} else {
NSLog(@"%@:%s - Even though some formats have cookies, some files don't take them and that's OK,set cookie status:%d \n",kModuleName,__func__,(int)error);
}
} else {
NSLog(@"%@:%s - Could not Get kAudioConverterCompressionMagicCookie from Audio Converter!\n status:%d ",kModuleName,__func__,(int)error);
}

free(cookie);
}else {
// If there is an error here, then the format doesn't have a cookie - this is perfectly fine as som formats do not.
NSLog(@"%@:%s - cookie status:%d, %d \n",kModuleName,__func__,(int)error, cookieSize);
}
}

4. 将数据写入文件.

通过AudioFileWritePackets可以将音频数据写入文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)writeFileWithInNumBytes:(UInt32)inNumBytes ioNumPackets:(UInt32 )ioNumPackets inBuffer:(const void *)inBuffer inPacketDesc:(const AudioStreamPacketDescription*)inPacketDesc {
if (!m_recordFile) {
return;
}

// AudioStreamPacketDescription outputPacketDescriptions;
OSStatus status = AudioFileWritePackets(m_recordFile,
false,
inNumBytes,
inPacketDesc,
m_recordCurrentPacket,
&ioNumPackets,
inBuffer);

if (status == noErr) {
m_recordCurrentPacket += ioNumPackets; // 用于记录起始位置
}else {
NSLog(@"%@:%s - write file status = %d \n",kModuleName,__func__,(int)status);
}

}

该函数定义如下.

  • inUseCache: 写入数据时是否缓存数据
  • inNumBytes: 写入数据的大小
  • inPacketDescriptions: VBR格式下音频数据包的描述信息
  • inStartingPacket: 每次从第多少个包开始写入,累加过程,所以需要记录
  • ioNumPackets:当前这次写入多少个数据包
  • inBuffer: 写入的音频数据
    1
    2
    3
    4
    5
    6
    7
    8
    extern OSStatus	
    AudioFileWritePackets ( AudioFileID inAudioFile,
    Boolean inUseCache,
    UInt32 inNumBytes,
    const AudioStreamPacketDescription * __nullable inPacketDescriptions,
    SInt64 inStartingPacket,
    UInt32 *ioNumPackets,
    const void *inBuffer) API_AVAILABLE(macos(10.2), ios(2.0), watchos(2.0), tvos(9.0));

5. 停止录制

注意: 在开启与关闭录制时都需要做一次写magic cookie操作,开始时做是为了使文件具备magic cookie可用,结束时调用是为了更新与校正magic cookie信息.

1
2
3
4
5
6
7
8
9
10
-(void)stopVoiceRecordAudioConverter:(AudioConverterRef)audioConverter needMagicCookie:(BOOL)isNeedMagicCookie {
if (isNeedMagicCookie) {
// reconfirm magic cookie at the end.
[self copyEncoderCookieToFileByAudioConverter:audioConverter
inFile:m_recordFile];
}

AudioFileClose(m_recordFile);
m_recordCurrentPacket = 0;
}
穷则github点星,达可兼济本人
显示 Gitment 评论
0%