iOS实时录音编码保存Mp3 Demo-使用Lame实现
Lame开源库
Lame是一款优秀的mp3开源跨平台编码库,可以将音频裸PCM数据编码成mp3。
先去官方下载Lame源代码: Lame下载地址
然后编译静态库,这里呢不再累述,可以自己写编译脚本,也可以去Github上下载编译脚本。脚本下载链接: lame-build-script
这里呢我已经编译好了Lame静态库,包含了x86,arm64架构,需要的童鞋可以直接下载,Lame版本是最新的V3.100。网盘下载地址: iOSLame静态库
PCM
PCM(Pulse Code Modulation):脉码编码调制。是没有压缩的音频数据,也可以叫音频裸数据。我们经常可以看到音频参数中有44100HZ 16bit,或者是22050HZ 8bit。
这里呢其实是两个参数
采样率:自然界的音频即声波转换为数字数据保存,即模-》数,单位时间采样个数即采样率。很明显,采样率越高,精确度越大。人对频率的识别范围是 20HZ - 20000HZ。所以22050的采样频率是常用的音频采样率,而44100采样率即是CD级别。
16bit pcm意味着使用两个字节去保存采样值。
采样数据记录的是振幅, 采样精度取决于储存空间的大小:
1 字节(也就是8bit) 256, 也就是只能将振幅划分成 256 个等级;
2 字节(也就是16bit) 65536个等级 , CD级别,16bit pcm就是最常见的。
4 字节(也就是32bit) 能把振幅细分到 4294967296 个等级, 一般不常用。
双声道
我们可与看到16bit的PCM和8Bit的PCM双声道都是左右声道交替存储的,所不同的是,16位是每两个字节存储一个声道数据,而8位是一个字节,然后再交替存储。
这里了解下PCM存储结构是为了后面我们从文件流取出对应声道数据。
本地PCM文件转码为Mp3文件
本地PCM文件,我在上面的网盘保存了一份,需要的可以下载,也可以自己通过FFMpeg指令生成PCM裸数据,以MP3转PCM为例
ffmpeg -i test.mp3 -f s16le -ar 8000 test.pcm
实际项目中音视频相关的底层接口通常是跨平台设计的,为了兼容iOS/Android/Windows/Linux等,通常底层接口使用C++编写封装。
这里我们写一个简单的C++类 Mp3Encoder
使用Objective-C也是同样的接口调用,在Demo中也存放了一个OC封装类,需要的可以下载查看。
class Mp3Encoder {
private:
FILE* pcmFile;
FILE* mp3File;
lame_t lameClient;
public:
Mp3Encoder();
~Mp3Encoder();
/**
pcm编码成Mp3文件
@param pcmFilePath pcm源文件路径
@param mp3FilePath 编码完成mp3文件路径
@param sampleRate 采样率
@param channels 通道数
@param bitRate 码率
*/
//每个任务都需要初始化一次
int Init(const char* pcmFilePath,const char *mp3FilePath,int sampleRate,int channels,int bitRate);
//编码本地文件
void EncodeLocalFile();
//销毁资源
void Destroy();
};
初始化Mp3Encoder类
int Mp3Encoder::Init(const char *pcmFilePath, const char *mp3FilePath, int sampleRate, int channels, int bitRate){
encodeEnd = false;
int ret = -1;
//只读文件流,读取原PCM数据路径
pcmFile = fopen(pcmFilePath, "rb");
if(pcmFile){
//读写文件流,目标Mp3写入生成路径
mp3File = fopen(mp3FilePath, "wb+");
}
if(mp3File){
//初始化Lame
lameClient = lame_init();
lame_set_in_samplerate(lameClient,sampleRate); //设置输入采样率
lame_set_out_samplerate(lameClient, sampleRate); //设置输出采样率
lame_set_num_channels(lameClient, channels); //设置声道数
lame_set_brate(lameClient, bitRate); //设置码率
lame_set_quality(lameClient,2); //设置转码质量高
lame_init_params(lameClient); //完成设置
}
return ret;
}
转码Mp3
void Mp3Encoder::EncodeLocalFile(){
//跳过 PCM header 否者会有一些噪音在MP3开始播放处
fseek(pcmFile, 4*1024, SEEK_CUR);
int bufferSize = 256 * 1024;
short *buffer = new short[bufferSize/2];
short *leftBuffer = new short[bufferSize/4];
short *rightBuffer = new short[bufferSize/4];
unsigned char* mp3_buffer = new unsigned char[bufferSize];
size_t readBufferSize = 0;
//双声道获取比特率的数据
while ((readBufferSize = fread(buffer, 2, bufferSize/2, pcmFile))>0) {
for(int i = 0;i < readBufferSize;i++){
if(i % 2 == 0){
leftBuffer[i/2] = buffer[I];
}
else{
rightBuffer[i/2] = buffer[I];
}
}
size_t wroteSize = lame_encode_buffer(lameClient, (short int *)leftBuffer, (short int *)rightBuffer, (int)(readBufferSize / 2), mp3_buffer, bufferSize);
fwrite(mp3_buffer, 1, wroteSize, mp3File);
}
//写入Mp3 VBR Tag,不是必须的步骤
lame_mp3_tags_fid(lameClient, mp3File);
delete []buffer;
delete []leftBuffer;
delete []rightBuffer;
delete []mp3_buffer;
}
转码Mp3这里有几点注意事项
- PCM数据头有四个字节的头信息,这里我们跳过,避免编码产生头噪音
- 我们设置了一个Buffer 为256 *1024大小,从文件流每次读取一定数量buffer转码MP3写入,直到全部读取完文件流
- 需要特别注意的是下面我们从文件流每次读取两个字节的数据,依次存入buffer,这里由于demo处理的是16位PCM数据,所以左右声道各占两个字节,如果是8bit或者32bit则需要分别读取1个字节和4个字节数据。这样才能分离出左右声道数据
readBufferSize = fread(buffer, 2, bufferSize/2, pcmFile)
- 编码Mp3区分左右声道
lame_encode_buffer(lameClient, (short int *)leftBuffer, (short int *)rightBuffer, (int)(readBufferSize / 2), mp3_buffer, bufferSize)
- 编码完成之后,写入Mp3的VBR tag,如果不写入的话,可能会导致某些播放器播放时获取时长出现问题,所以建议写入。(VBR Tag这里不再介绍,需要了解的可以自行查阅Mp3封装格式哈)
//写入Mp3 VBR Tag,不是必须的步骤
lame_mp3_tags_fid(lameClient, mp3File);
最后外部调用编码接口
//异步转换本地PCM文件
dispatch_async(localMp3EncodeQueue(), ^{
[self testLocalPCMToMp3];
});
- (void)testLocalPCMToMp3{
//获取原PCM路径 需要PCM,自己放一段,或者在我的blog网盘上面获取下载Demo PCM
NSString *pcmPath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"pcm"];
//输出目标MP3路径
NSString *mp3Path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@/LoacalTest.mp3",MP3SaveFilePath]];
NSLog(@"%@",mp3Path);
//编码Mp3 sampleRate使用标准Mp3 44.1khz 双声道 码率使用128kb
Mp3Encoder encode;
encode.Init([pcmPath cStringUsingEncoding:NSUTF8StringEncoding], [mp3Path cStringUsingEncoding:NSUTF8StringEncoding], 44100, 2, 128);
//开始编码
encode.EncodeLocalFile();
//释放资源
encode.Destroy();
}
至此我们就实现了简单的PCM文件本地编码成Mp3文件
实时录音编码Mp3实现
其实实时录音实现流程如下
实时录音编码Mp3保存流程图
其实和本地编码保存不同的是,我们需要循环读取源文件的PCM数据,直到录音结束,停止循环,保存最终mp3,核心代码如下
class Mp3Encoder {
private:
FILE* pcmFile;
FILE* mp3File;
lame_t lameClient;
public:
//标志位,用于编录音编解码的录音结束标识符
bool encodeEnd;
Mp3Encoder();
~Mp3Encoder();
/**
pcm编码成Mp3文件
@param pcmFilePath pcm源文件路径
@param mp3FilePath 编码完成mp3文件路径
@param sampleRate 采样率
@param channels 通道数
@param bitRate 码率
*/
//每个任务都需要初始化一次
int Init(const char* pcmFilePath,const char *mp3FilePath,int sampleRate,int channels,int bitRate);
//编码本地文件
void EncodeLocalFile();
//边录制边解码
void EncodeStreamFile();
//销毁资源
void Destroy();
};
int Mp3Encoder::Init(const char *pcmFilePath, const char *mp3FilePath, int sampleRate, int channels, int bitRate){
encodeEnd = false;
int ret = -1;
//只读文件流,读取原PCM数据路径
pcmFile = fopen(pcmFilePath, "rb");
if(pcmFile){
//读写文件流,目标Mp3写入生成路径
mp3File = fopen(mp3FilePath, "wb+");
}
if(mp3File){
//初始化Lame
lameClient = lame_init();
lame_set_in_samplerate(lameClient,sampleRate); //设置输入采样率
lame_set_out_samplerate(lameClient, sampleRate); //设置输出采样率
lame_set_num_channels(lameClient, channels); //设置声道数
lame_set_brate(lameClient, bitRate); //设置码率
lame_set_quality(lameClient,2); //设置转码质量高
lame_init_params(lameClient); //完成设置
}
return ret;
}
void Mp3Encoder::EncodeStreamFile(){
//双声道获取比特率的数据
int bufferSize = 256 * 1024;
short *buffer = new short[bufferSize/2];
short *leftBuffer = new short[bufferSize/4];
short *rightBuffer = new short[bufferSize/4];
unsigned char* mp3_buffer = new unsigned char[bufferSize];
size_t readBufferSize = 0;
bool isSkipPcmHeader = false;
long curPos;
//循环读取数据编码
do {
curPos = ftell(pcmFile);
long startPos = ftell(pcmFile);
fseek(pcmFile, 0, SEEK_END);
long endPos = ftell(pcmFile);
long totalDataLength = endPos - startPos;
fseek(pcmFile, curPos, SEEK_SET);
if (totalDataLength > bufferSize) {
if (!isSkipPcmHeader) {
//跳过 PCM header 否者会有一些噪音在MP3开始播放处
fseek(pcmFile, 4*1024, SEEK_CUR);
isSkipPcmHeader = true;
}
readBufferSize = fread(buffer, 2, bufferSize/2, pcmFile);
//双声道的处理
for(int i = 0;i < readBufferSize;i++){
if(i % 2 == 0){
leftBuffer[i/2] = buffer[i];
}
else{
rightBuffer[i/2] = buffer[i];
}
}
size_t wroteSize = lame_encode_buffer(lameClient, (short int *)leftBuffer, (short int *)rightBuffer, (int)(readBufferSize / 2), mp3_buffer, bufferSize);
fwrite(mp3_buffer, 1, wroteSize, mp3File);
}
//sleep 0.05s
sleep(0.05);
} while (!encodeEnd);
//这里需要注意的是,一旦录音结束encodeEnd就会导致上面的函数结束,有可能出现解码慢,导致录音结束,仍然没有解码完所有数据的可能
//循环读取剩余数据进行编码
while ((readBufferSize = fread(buffer, 2, bufferSize/2, pcmFile))>0) {
for(int i = 0;i < readBufferSize;i++){
if(i % 2 == 0){
leftBuffer[i/2] = buffer[i];
}
else{
rightBuffer[i/2] = buffer[i];
}
}
size_t wroteSize = lame_encode_buffer(lameClient, (short int *)leftBuffer, (short int *)rightBuffer, (int)(readBufferSize / 2), mp3_buffer, bufferSize);
fwrite(mp3_buffer, 1, wroteSize, mp3File);
}
//写入Mp3 VBR Tag,不是必须的步骤
lame_mp3_tags_fid(lameClient, mp3File);
delete []buffer;
delete []leftBuffer;
delete []rightBuffer;
delete []mp3_buffer;
}
这里使用AVAudioRecord录制音频
录音核心参数如下
/**
* 录音参数设置
*/
- (NSDictionary *)getAudioSetting{
NSMutableDictionary *dicM = [NSMutableDictionary dictionary];
[dicM setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey];
[dicM setObject:@(sampleRate) forKey:AVSampleRateKey]; //44.1khz的采样率
[dicM setObject:@(2) forKey:AVNumberOfChannelsKey];
[dicM setObject:@(16) forKey:AVLinearPCMBitDepthKey]; //16bit的PCM数据
[dicM setObject:[NSNumber numberWithInt:AVAudioQualityMax] forKey:AVEncoderAudioQualityKey];
return dicM;
}
源代码
项目源代码github地址:iOS-Record-Transcoding-mp3-lameDemo