在iOS中使用AVAudioRecorder无法录制MP3格式的音频文件,虽然你可能会看到过这样一个枚举:
CF_ENUM(AudioFormatID)
{
kAudioFormatLinearPCM = 'lpcm',
kAudioFormatAC3 = 'ac-3',
kAudioFormat60958AC3 = 'cac3',
kAudioFormatAppleIMA4 = 'ima4',
kAudioFormatMPEG4AAC = 'aac ',
kAudioFormatMPEG4CELP = 'celp',
kAudioFormatMPEG4HVXC = 'hvxc',
kAudioFormatMPEG4TwinVQ = 'twvq',
kAudioFormatMACE3 = 'MAC3',
kAudioFormatMACE6 = 'MAC6',
kAudioFormatULaw = 'ulaw',
kAudioFormatALaw = 'alaw',
kAudioFormatQDesign = 'QDMC',
kAudioFormatQDesign2 = 'QDM2',
kAudioFormatQUALCOMM = 'Qclp',
kAudioFormatMPEGLayer1 = '.mp1',
kAudioFormatMPEGLayer2 = '.mp2',
kAudioFormatMPEGLayer3 = '.mp3',
kAudioFormatTimeCode = 'time',
kAudioFormatMIDIStream = 'midi',
kAudioFormatParameterValueStream = 'apvs',
kAudioFormatAppleLossless = 'alac',
kAudioFormatMPEG4AAC_HE = 'aach',
kAudioFormatMPEG4AAC_LD = 'aacl',
kAudioFormatMPEG4AAC_ELD = 'aace',
kAudioFormatMPEG4AAC_ELD_SBR = 'aacf',
kAudioFormatMPEG4AAC_ELD_V2 = 'aacg',
kAudioFormatMPEG4AAC_HE_V2 = 'aacp',
kAudioFormatMPEG4AAC_Spatial = 'aacs',
kAudioFormatAMR = 'samr',
kAudioFormatAMR_WB = 'sawb',
kAudioFormatAudible = 'AUDB',
kAudioFormatiLBC = 'ilbc',
kAudioFormatDVIIntelIMA = 0x6D730011,
kAudioFormatMicrosoftGSM = 0x6D730031,
kAudioFormatAES3 = 'aes3',
kAudioFormatEnhancedAC3 = 'ec-3'
};
也许你也使用了以下代码设置了AVAudioRecorder:
NSDictionary *settings =
@{AVSampleRateKey:@44100,
AVFormatIDKey:[NSNumber numberWithInt:kAudioFormatMPEGLayer3],
AVNumberOfChannelsKey:@2,
AVEncoderAudioQualityKey:[NSNumber numberWithInt:self.quality]};
_recorder = [[AVAudioRecorder alloc] initWithURL:tempFile settings:settings error:&recorderError];
然而上面的代码并没有什么卵用,当你开始录音后也没有什么错误,然后待录音完毕后你会发现录音文件的长度为0。根本没有mp3文件生成。还是乖乖地录制PCM格式的音频,然后再转码成mp3格式。本文的重点不在于讲如何录制PCM音频文件,也不讲解lame的下载和build过程,这些网络上都能搜索到,而是在于如何在录音开始的过程将PCM文件转码成mp3文件。
先展示一下关键代码部分:
- (void)conventToMp3 {
DWDLog(@"convert begin!!");
NSString *cafFilePath = [NSTemporaryDirectory() stringByAppendingString:@"VoiceInputFile"];
_mp3FileName = [[NSUUID UUID] UUIDString];
self.mp3FileName = [self.mp3FileName stringByAppendingString:@".mp3"];
NSString *mp3FilePath = [[NSHomeDirectory() stringByAppendingFormat:@"/Documents/"] stringByAppendingPathComponent:self.mp3FileName];
@try {
int read, write;
FILE *pcm = fopen([cafFilePath cStringUsingEncoding:NSASCIIStringEncoding], "rb");
FILE *mp3 = fopen([mp3FilePath cStringUsingEncoding:NSASCIIStringEncoding], "wb");
const int PCM_SIZE = 8192;
const int MP3_SIZE = 8192;
short int pcm_buffer[PCM_SIZE * 2];
unsigned char mp3_buffer[MP3_SIZE];
lame_t lame = lame_init();
lame_set_in_samplerate(lame, self.sampleRate);
lame_set_VBR(lame, vbr_default);
lame_init_params(lame);
long curpos;
BOOL isSkipPCMHeader = NO;
do {
curpos = ftell(pcm);
long startPos = ftell(pcm);
fseek(pcm, 0, SEEK_END);
long endPos = ftell(pcm);
long length = endPos - startPos;
fseek(pcm, curpos, SEEK_SET);
if (length > PCM_SIZE * 2 * sizeof(short int)) {
if (!isSkipPCMHeader) {
//Uump audio file header, If you do not skip file header
//you will heard some noise at the beginning!!!
fseek(pcm, 4 * 1024, SEEK_SET);
isSkipPCMHeader = YES;
DWDLog(@"skip pcm file header !!!!!!!!!!");
}
read = (int)fread(pcm_buffer, 2 * sizeof(short int), PCM_SIZE, pcm);
write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
fwrite(mp3_buffer, write, 1, mp3);
DWDLog(@"read %d bytes", write);
}
else {
[NSThread sleepForTimeInterval:0.05];
DWDLog(@"sleep");
}
} while (!self.isStopRecorde);
read = (int)fread(pcm_buffer, 2 * sizeof(short int), PCM_SIZE, pcm);
write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
DWDLog(@"read %d bytes and flush to mp3 file", write);
lame_close(lame);
fclose(mp3);
fclose(pcm);
self.isFinishConvert = YES;
}
@catch (NSException *exception) {
DWDLog(@"%@", [exception description]);
}
@finally {
DWDLog(@"convert mp3 finish!!!");
}
}
对于熟悉C语言的大神来说,这根本没有什么难度。
这个方法的大体思路就是在录音没有完成前,我们循环读取PCM文件,当读取到的字节大于我们规定的一个单位后,我们将这些字节交给lame,lame会把转码后的二进制数据输出到目标MP3文件里。
代理有很多开关变量,那都是一个BOOL类型的自定义变量,并没有什么特别。
如果看不懂上面的代码,我建议你先搞清楚这段代码用的几个函数的意思,这段代码是实现边录边播的关键。
curpos = ftell(pcm);
long startPos = ftell(pcm);
fseek(pcm, 0, SEEK_END);
long endPos = ftell(pcm);
long length = endPos - startPos;
fseek(pcm, curpos, SEEK_SET);
代码唯一有注释的部分是为了跳过PCM的header部分,正如注释所说,如果不跳过这一部分,转换成的mp3在播放的最初一秒内会听到一个明显的噪音。
那么这个方法应该怎么用才能实现边录边转呢?只需要像下面这样:
...
self.recorder.meteringEnabled = YES;
[self.recorder prepareToRecord];
[self.recorder record];
self.status = DWDAudioClientStatusRecording;
_timer = [NSTimer scheduledTimerWithTimeInterval:.2
target:self selector:@selector(recordTimerUpdate)
userInfo:nil repeats:YES];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self conventToMp3];
});
...
我们只需要在录音开始后,另起一个线程去完成转码的工作。
这里也讲一个AVAudioRecorder的一些大坑,我们在录音和播放的过程中,经常要获取音量,还有录音时间的长短。
而大坑之一就是有的时候你很可能获取不到正确的音量,而原因可能就是你设置AVAudioSession的方法和设置AVAudioRecorder的代码没有放到同一个方法内。我之前就是因为这个原因而获取不到正确音量(即metering)。
另外一个坑就是,AVAudioRecorder在录音完成之后并不能获取到录音的时长,这个处理起来比较简单,我们可以在录音开始的时候和结束的时候获取时间,然后相减即可得到时长。像下面这样:
...
self.audioStartRecodeTime = CACurrentMediaTime();
...
self.audioEndRecodeTime = CACurrentMediaTime();
这里需要提醒一点,iOS中有很多获取时间的方法,而计算时间最好用上面的方法,至于原因请自己Google。
想要完整源代码的同学可以直接问我索要,QQ2541251578
网友评论
这行提示警告Implicit conversion loses integer precision: 'size_t' (aka 'unsigned long') to 'int'
咋解决啊?
lame.h:101:5: Redefinition of enumerator 'V2'
lame.h:103:5: Redefinition of enumerator 'V1'