iOS实时录音编码保存Mp3-Demo-使用Lame实现

2,809 阅读8分钟

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

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

  1. PCM数据头有四个字节的头信息,这里我们跳过,避免编码产生头噪音
  2. 我们设置了一个Buffer 为256 *1024大小,从文件流每次读取一定数量buffer转码MP3写入,直到全部读取完文件流
  3. 需要特别注意的是下面我们从文件流每次读取两个字节的数据,依次存入buffer,这里由于demo处理的是16位PCM数据,所以左右声道各占两个字节,如果是8bit或者32bit则需要分别读取1个字节和4个字节数据。这样才能分离出左右声道数据

readBufferSize = fread(buffer, 2, bufferSize/2, pcmFile)

  1. 编码Mp3区分左右声道

lame_encode_buffer(lameClient, (short int *)leftBuffer, (short int *)rightBuffer, (int)(readBufferSize / 2), mp3_buffer, bufferSize)

  1. 编码完成之后,写入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