iOS 开发学习成长之路iOS 知识点iOS点点滴滴

iOS 使用 Lame 转码 MP3 的最正确姿势

2017-07-18  本文已影响1654人  Civel_Xu

前言

概述

我将会在本篇文章分析以下几点内容

AVAudioRecorder 配置 和 Lame 编码压缩配置

AVAudioRecorder 配置的注意事项

关于 AVAudioRecorder 录音的相关配置 和 Lame 包的编译工作, 这里忽略不讲, 主要是想说一下需要注意的地方

Lame 编码压缩 的相关配置

//source 被转换的音频文件位置
FILE *pcm = fopen([cafFilePath cStringUsingEncoding:1], "rb");  
//skip file header 跳过 PCM header 能保证录音的开头没有噪音 
fseek(pcm, 4*1024,  SEEK_CUR); 
//output 输出生成的Mp3文件位置
FILE *mp3 = fopen([mp3FilePath cStringUsingEncoding:1], "wb+");  
C 语言的 文件打开模式

w+以纯文本方式读写,而wb+是以二进制方式进行读写。
mode说明:
w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
wb 只写方式打开或新建一个二进制文件,只允许写数据。
wb+ 读写方式打开或建立一个二进制文件,允许读和写。
r 打开只读文件,该文件必须存在,否则报错。
r+ 打开可读写的文件,该文件必须存在,否则报错。
rb+ 读写方式打开一个二进制文件,只允许读写数据。
a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)
a+ 以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 (原来的EOF符不保留)
ab+ 读写打开一个二进制文件,允许读或在文件末追加数据。
加入b 字符用来告诉函数库打开的文件为二进制文件,而非纯文字文件。

解决录音时长读取不正确的问题

Lame 的转码配置网上有很多, 网上可以搜到很多相关的代码, 作为小白 copy 使用, 由于不懂源码实现,直接拿来用就出现了不可预料的问题. 我出现的播放时间不准确的问题, 无论是 AVPlayer 或者 AVAudioPlayer 均无法读取正确的长度, 要么是多几秒, 要么是少几秒, 还可能是超过10s的的误差, 但是播放的过程中, 定时器的计数 会和 总时间显示不吻合, 就比如 一个显示 2:30 的录音, 活生生 放到了 2:50, 你能想象是多么的尴尬Bug.

问题猜测

我把录制完成的文件, 使用 iTunes 来播放可以显示出正确的长度, 但是使用 QuickTime Player 会出现和 AVPlayer 一样的错误时长 !!!

 CMTime time = _player.currentItem.duration;
    if (time.timescale == 0) {
        return 0;
    }
    return time.value / time.timescale;
    if (self.player && self.player.currentItem && self.player.currentItem.asset) {
        return  CMTimeGetSeconds(self.player.currentItem.asset.duration);

    } else{
        return 0;
    }
    
    AVURLAsset* audioAsset = [AVURLAsset URLAssetWithURL:self.playingURL options:nil];
    CMTime audioDuration = audioAsset.duration;
    float audioDurationSeconds = CMTimeGetSeconds(audioDuration);
    return (NSInteger)audioDurationSeconds;

了解MP3编码格式

然后,通过对MP3编码格式调研, 了解到如下信息:

Lame 源码分析

/*
  1 = write a Xing VBR header frame.
  default = 1
  this variable must have been added by a Hungarian notation Windows programmer :-)
*/
int CDECL lame_set_bWriteVbrTag(lame_global_flags *, int);
int CDECL lame_get_bWriteVbrTag(const lame_global_flags *);
/*
 * OPTIONAL:
 * lame_mp3_tags_fid will rewrite a Xing VBR tag to the mp3 file with file
 * pointer fid.  These calls perform forward and backwards seeks, so make
 * sure fid is a real file.  Make sure lame_encode_flush has been called,
 * and all mp3 data has been written to the file before calling this
 * function.
 * NOTE:
 * if VBR  tags are turned off by the user, or turned off by LAME because
 * the output is not a regular file, this call does nothing
 * NOTE:
 * LAME wants to read from the file to skip an optional ID3v2 tag, so
 * make sure you opened the file for writing and reading.
 * NOTE:
 * You can call lame_get_lametag_frame instead, if you want to insert
 * the lametag yourself.
*/
void CDECL lame_mp3_tags_fid(lame_global_flags *, FILE* fid);

问题解决

边录制边转码的实现

通常我们是在录制结束之后, 再进行转码; 当录制的时间较长, 会消耗的时间比较长. 用户需要等待转码结束后,才能操作; 但是如果我们使用边录制,边转码的方式, 开另外一个线程同时进行转码,则几乎没有等待的时间,效率上会比较的高.

            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_CUR);
                        isSkipPCMHeader = YES;
                        NSLog(@"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);
                    NSLog(@"read %d bytes", write);
                } else {
                    [NSThread sleepForTimeInterval:0.05];
                    NSLog(@"sleep");
                }
                
            } while (! weakself.stopRecord);

测试 Demo

为了让遇到相同问题的人, 能够更加对这些问题有一点的了解, 我会 在这里贴一个我测试的Demo 这只是一个实例程序, 并不具备完整的逻辑功能, 请熟知.

        [[ConvertAudioFile sharedInstance] conventToMp3WithCafFilePath:self.cafPath
                                                           mp3FilePath:self.mp3Path
                                                            sampleRate:ETRECORD_RATE callback:^(BOOL result)
        {
            NSLog(@"---- 转码完成  --- result %d  ---- ", result);
        }];;
        
    [ConvertAudioFile conventToMp3WithCafFilePath:self.cafPath
                      mp3FilePath:self.mp3Path
                       sampleRate:ETRECORD_RATE callback:^(BOOL result)
     {
         NSLog(@"---- 转码完成  --- result %d  ---- ", result);
     }];
        

结语

由于时间有限, 我并不会 写太多细致的内容, 只是对这几天的研究做一个总结,和列举一些注意事项,如果在做音频录制转码中遇到相同的问题,则会有比较大的帮助.

总结

这次解决这个问题,让我受益匪浅, 很多地方的收获是超过问题本身的:

Link

致谢

对我有帮助的文章

上一篇下一篇

猜你喜欢

热点阅读