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 个等级, 一般不常用。

双声道

裸数据的音频存在双声道,即左右耳,我们看下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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容