iOS实时录音编码保存Mp3-使用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 个等级, 一般不常用。

双声道
裸数据的音频存在双声道,即左右耳,我们看下PCM双声道的存储结构。PCM存储结构

我们可与看到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封装类,需要的可以下载查看。

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
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类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

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
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这里有几点注意事项

  1. PCM数据头有四个字节的头信息,这里我们跳过,避免编码产生头噪音
  2. 我们设置了一个Buffer 为256 *1024大小,从文件流每次读取一定数量buffer转码MP3写入,直到全部读取完文件流
  3. 需要特别注意的是下面我们从文件流每次读取两个字节的数据,依次存入buffer,这里由于demo处理的是16位PCM数据,所以左右声道各占两个字节,如果是8bit或者32bit则需要分别读取1个字节和4个字节数据。这样才能分离出左右声道数据
    readBufferSize = fread(buffer, 2, bufferSize/2, pcmFile)
  4. 编码Mp3区分左右声道
    lame_encode_buffer(lameClient, (short int *)leftBuffer, (short int *)rightBuffer, (int)(readBufferSize / 2), mp3_buffer, bufferSize)
  5. 编码完成之后,写入Mp3的VBR tag,如果不写入的话,可能会导致某些播放器播放时获取时长出现问题,所以建议写入。(VBR Tag这里不再介绍,需要了解的可以自行查阅Mp3封装格式哈)
    //写入Mp3 VBR Tag,不是必须的步骤 lame_mp3_tags_fid(lameClient, mp3File);

最后外部调用编码接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
     //异步转换本地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,核心代码如下

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
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录制音频
录音核心参数如下

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 录音参数设置
*/
- (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