iOS音频格式之间的相互转化

首先介绍一下常用的音频文件格式
PCM格式:

PCM属于编码格式,PCM是经过话筒后直接得到的未经压缩的数据流
数据的大小 = 采样率 * 采样位数 * 声道 * 秒数 / 8
采样率一般是:22K或者是44K
位数一般是:8位或者16位
声道一般是:双声道或者单声道
PCM是一串由多个样本值组成的数据流,本身没有任何头信息或者帧的概念。只有一段PCM数据是没有办法知道它的采样率的信息的。

WAV格式:

WAV是封装格式,里面本身可以存放多种编码格式,不过一般都存放PCM数据。
WAV文件是由“WAV文件头”和“WAV文件体”组成。前44字节存放采样率,通道数,数据部分的标识符等头信息,后面就是存放数据部分。很显然WAV的头信息一旦损坏了,播放就会产生问题。

MP3格式:

MP3是封装格式,里面存放的数据使用的编码方式为:MPEG1 Layer-3
MP3是由TAG_V2结构体,TAG_V1结构体,和一组Frame组成。头部和尾部的TAG结构体不一定存在,需要判断。中间的Frame,每一个Frame都是由帧头和数据实体组成,帧头记录了MP3的位率,采样率,版本等信息。每个帧之间相互独立,也就是说即使前面的帧损坏了,后面的也可以播放。

AMR格式:

AMR是封装格式。
AMR文件包含一个文件头,后面就是一帧一帧的AMR帧。
文件头里面的值就固定值,用于标记文件为AMR文件。
每个帧分为帧头和数据部分,帧头里面包含编发方式和辅助信息。AMR的采样率跟编码方式的不同而不同。

AAC格式:

AAC文件可以没有文件头,全部由帧序列组成。
每个帧包含帧头和数据部分。
帧头包含采样率,声道数,帧长度等有点类似MP3格式。

CAF格式:

CAF是苹果的一种音频封装格式,与WAV差不多,里面可以存放LPCM,MP3等多种编码方式。

M4A格式:

M4A:M4A是MPEG4音频标准的文件的扩展名。在MPEG4标准中提到,普通的MPEG4文件扩搜索展名是.mp4,但自从苹果开始在它的iTunes以及iPod中使用m4a格式音频文件以区别MPEG4的视频和音频文件以来,.m4a这个扩展名变得流行了。

一、m4a格式转caf格式
/**
 把.m4a转为.caf格式
 @param originalUrlStr .m4a文件路径
 @param destUrlStr .caf文件路径
 @param completed 转化完成的block
 */
+ (void)convetM4aToWav:(NSString *)originalUrlStr
               destUrl:(NSString *)destUrlStr
             completed:(void (^)(NSError *error)) completed {
    
    if ([[NSFileManager defaultManager] fileExistsAtPath:destUrlStr]) {
        [[NSFileManager defaultManager] removeItemAtPath:destUrlStr error:nil];
    }
    NSURL *originalUrl = [NSURL fileURLWithPath:originalUrlStr];
    NSURL *destUrl     = [NSURL fileURLWithPath:destUrlStr];
    AVURLAsset *songAsset = [AVURLAsset URLAssetWithURL:originalUrl options:nil];
    
    //读取原始文件信息
    NSError *error = nil;
    AVAssetReader *assetReader = [AVAssetReader assetReaderWithAsset:songAsset error:&error];
    if (error) {
        DebugLog (@"error: %@", error);
        completed(error);
        return;
    }
    AVAssetReaderOutput *assetReaderOutput = [AVAssetReaderAudioMixOutput
                                              assetReaderAudioMixOutputWithAudioTracks:songAsset.tracks
                                              audioSettings: nil];
    if (![assetReader canAddOutput:assetReaderOutput]) {
        DebugLog (@"can't add reader output... die!");
        completed(error);
        return;
    }
    [assetReader addOutput:assetReaderOutput];
    
    AVAssetWriter *assetWriter = [AVAssetWriter assetWriterWithURL:destUrl
                                                          fileType:AVFileTypeCoreAudioFormat
                                                             error:&error];
    if (error) {
        DebugLog (@"error: %@", error);
        completed(error);
        return;
    }
    AudioChannelLayout channelLayout;
    memset(&channelLayout, 0, sizeof(AudioChannelLayout));
    channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
    NSDictionary *outputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                                    [NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey,
                                    [NSNumber numberWithFloat:44100], AVSampleRateKey,
                                    [NSNumber numberWithInt:2], AVNumberOfChannelsKey,
                                    [NSNumber numberWithInt:16], AVLinearPCMBitDepthKey,
                                    [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved,
                                    [NSNumber numberWithBool:NO],AVLinearPCMIsFloatKey,
                                    [NSNumber numberWithBool:NO], AVLinearPCMIsBigEndianKey,
                                    nil];
    AVAssetWriterInput *assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio
                                                                              outputSettings:outputSettings];
    if ([assetWriter canAddInput:assetWriterInput]) {
        [assetWriter addInput:assetWriterInput];
    } else {
        DebugLog (@"can't add asset writer input... die!");
        completed(error);
        return;
    }
    
    assetWriterInput.expectsMediaDataInRealTime = NO;
    
    [assetWriter startWriting];
    [assetReader startReading];
    
    AVAssetTrack *soundTrack = [songAsset.tracks objectAtIndex:0];
    CMTime startTime = CMTimeMake (0, soundTrack.naturalTimeScale);
    [assetWriter startSessionAtSourceTime:startTime];
    
    __block UInt64 convertedByteCount = 0;
    
    dispatch_queue_t mediaInputQueue = dispatch_queue_create("mediaInputQueue", NULL);
    [assetWriterInput requestMediaDataWhenReadyOnQueue:mediaInputQueue
                                            usingBlock: ^
     {
         while (assetWriterInput.readyForMoreMediaData) {
             CMSampleBufferRef nextBuffer = [assetReaderOutput copyNextSampleBuffer];
             if (nextBuffer) {
                 // append buffer
                 [assetWriterInput appendSampleBuffer: nextBuffer];
                 DebugLog (@"appended a buffer (%zu bytes)",
                        CMSampleBufferGetTotalSampleSize (nextBuffer));
                 convertedByteCount += CMSampleBufferGetTotalSampleSize (nextBuffer);
                 
                 
             } else {
                 [assetWriterInput markAsFinished];
                 [assetWriter finishWritingWithCompletionHandler:^{
                     
                 }];
                 [assetReader cancelReading];
                 NSDictionary *outputFileAttributes = [[NSFileManager defaultManager]
                                                       attributesOfItemAtPath:[destUrl path]
                                                       error:nil];
                 DebugLog (@"FlyElephant %lld",[outputFileAttributes fileSize]);
                 break;
             }
         }
         DebugLog(@"转换结束");
         // 删除临时temprecordAudio.m4a文件
         NSError *removeError = nil;
         if ([[NSFileManager defaultManager] fileExistsAtPath:originalUrlStr]) {
             BOOL success = [[NSFileManager defaultManager] removeItemAtPath:originalUrlStr error:&removeError];
             if (!success) {
                 DebugLog(@"删除临时temprecordAudio.m4a文件失败:%@",removeError);
                 completed(removeError);
             }else{
                 DebugLog(@"删除临时temprecordAudio.m4a文件:%@成功",originalUrlStr);
                 completed(removeError);
             }
         }
         
     }];
}
二、caf格式转m4a格式
/**
 把.caf转为.m4a格式
 @param cafUrlStr .m4a文件路径
 @param m4aUrlStr .caf文件路径
 @param completed 转化完成的block
 */
+ (void)convetCafToM4a:(NSString *)cafUrlStr
               destUrl:(NSString *)m4aUrlStr
             completed:(void (^)(NSError *error)) completed {
    
    AVMutableComposition* mixComposition = [AVMutableComposition composition];
    //  音频插入的开始时间
    CMTime beginTime = kCMTimeZero;
    //  获取音频合并音轨
    AVMutableCompositionTrack *compositionAudioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
    //  用于记录错误的对象
    NSError *error = nil;
    //  音频原文件资源
    AVURLAsset *cafAsset = [[AVURLAsset alloc]initWithURL:[NSURL fileURLWithPath:cafUrlStr] options:nil];
    //  原音频需要合并的音频文件的区间
    CMTimeRange audio_timeRange = CMTimeRangeMake(kCMTimeZero, cafAsset.duration);
    BOOL success = [compositionAudioTrack insertTimeRange:audio_timeRange ofTrack:[[cafAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] atTime:beginTime error:&error];
    if (!success) {
        DebugLog(@"插入原音频失败: %@",error);
    }else {
        DebugLog(@"插入原音频成功");
    }
    // 创建一个导入M4A格式的音频的导出对象
    AVAssetExportSession* assetExport = [[AVAssetExportSession alloc] initWithAsset:mixComposition presetName:AVAssetExportPresetAppleM4A];
    // 导入音视频的URL
    assetExport.outputURL = [NSURL fileURLWithPath:m4aUrlStr];
    // 导出音视频的文件格式
    assetExport.outputFileType = @"com.apple.m4a-audio";
    [assetExport exportAsynchronouslyWithCompletionHandler:^{
        // 分发到主线程
        dispatch_async(dispatch_get_main_queue(), ^{
            int exportStatus = assetExport.status;
            if (exportStatus == AVAssetExportSessionStatusCompleted) {
                // 合成成功
                completed(nil);
                NSError *removeError = nil;
                if([cafUrlStr hasSuffix:@"caf"]) {
                    // 删除老录音caf文件
                    if ([[NSFileManager defaultManager] fileExistsAtPath:cafUrlStr]) {
                        BOOL success = [[NSFileManager defaultManager] removeItemAtPath:cafUrlStr error:&removeError];
                        if (!success) {
                            DebugLog(@"删除老录音caf文件失败:%@",removeError);
                        }else{
                            DebugLog(@"删除老录音caf文件:%@成功",cafUrlStr);
                        }
                    }
                }
                
            }else {
                completed(assetExport.error);
            }
            
        });
    }];
}
三、caf或m4a转为aac或mp3

这个的转换需要引入一个第三方库,就是lame.大家可以百度下载这个库,主要使用以下两个文件,找到这两个文件之后,直接拖进项目即可,拖进去之后,在需要使用的文件里面加入lame.h的头文件,即可使用:


lame.png
+ (BOOL)audio_PCMtoMP3WithPCMUrl:(NSString *)pcmUrl andTimesTamps:(NSString *)timesTamps{
    
    // 导出aac的地址
    NSString *timestampsPath = [kRecorderPath stringByAppendingPathComponent: timesTamps];
    NSString *mp3FilePath = [timestampsPath stringByAppendingPathComponent: kRecordAACSaveName];
    
    NSError *removeError = nil;
    if ([[NSFileManager defaultManager] fileExistsAtPath:mp3FilePath]) {
        // 如果有旧文件则删除
        BOOL success = [[NSFileManager defaultManager] removeItemAtPath:mp3FilePath error:&removeError];
        if (!success) {
            DebugLog(@"删除老aac文件失败:%@",removeError);
        }else{
            DebugLog(@"删除老aac文件:%@成功",mp3FilePath);
        }
    }
    @try {
        int read, write;
        FILE *pcm = fopen([pcmUrl cStringUsingEncoding:1], "rb");  //source 被转换的音频文件位置
        
        fseek(pcm, 4*1024, SEEK_CUR);                                   //skip file header
        
        FILE *mp3 = fopen([mp3FilePath cStringUsingEncoding:1], "wb");  //output 输出生成的Mp3文件位置
        
        const int PCM_SIZE = 8192;//8192
        
        const int MP3_SIZE = 8192;//8192
        
        short int pcm_buffer[PCM_SIZE*2];
        
        unsigned char mp3_buffer[MP3_SIZE];
        
        lame_t lame = lame_init();
        
        lame_set_in_samplerate(lame, 44100);//采样播音速度,值越大播报速度越快,反之。
        
        lame_set_VBR(lame, vbr_default);
        
        lame_init_params(lame);
        
        do {
            read = fread(pcm_buffer, 2*sizeof(short int), PCM_SIZE, pcm);
            
            if (read == 0) {
                write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
            } else {
                write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
            }
            fwrite(mp3_buffer, write, 1, mp3);
            
        } while (read != 0);
        
        lame_close(lame);
        
        fclose(mp3);
        
        fclose(pcm);
        
    }
    @catch (NSException *exception) {
        DebugLog(@"CAF转AAC失败:%@",[exception description]);
        return NO;
    }
    @finally {
        DebugLog(@"CAF转AAC成功!");
        return YES;
    }
}
四、caf转mp3

这里也是需要lame文件的

- (void)audio_PCMtoMP3
{
    NSString*tempDir =NSTemporaryDirectory();
    NSString*urlPatch = [tempDir stringByAppendingString:@"/ebemate_record.caf"];//原录音保存位置
    NSString *mp3FilePath = [tempDir stringByAppendingPathComponent:@"/ebemate_record.mp3"];//转码mp3格式保存位置
   
    @try {
        int read, write;
       
        FILE *pcm = fopen([urlPatch cStringUsingEncoding:1], "rb");  //source 被转换的音频文件位置
        if(pcm == NULL){
            NSLog(@"file not found");
            return;
        }
        
        fseek(pcm, 4*1024, SEEK_CUR);                                   //skip file header
        FILE *mp3 = fopen([mp3FilePath cStringUsingEncoding:1], "wb");  //output 输出生成的Mp3文件位置
       
        const int PCM_SIZE = 8192;
        const int MP3_SIZE = 8192;
        short int pcm_buffer[PCM_SIZE*2];
        unsigned char mp3_buffer[MP3_SIZE];
       
        //以下为转码需要的重点代码
        lame_t lame = lame_init();
        
        lame_set_num_channels(lame,2);//设置1为单通道,默认为2双通道
        
        lame_set_in_samplerate(lame, 8000);//这里的值必须要和上面s录音的时候设置的一直,否则转码出来的mp3格式播放会变音
        
        lame_set_VBR(lame, vbr_default);
        
        lame_set_brate(lame,0);
        
        lame_set_mode(lame,3);
        
        lame_set_quality(lame,2); /* 2=high 5 = medium 7=low 音质*/
                
        lame_set_out_samplerate(lame, 44100);
        
        lame_init_params(lame);


        do {
            read = (int)fread(pcm_buffer, 2*sizeof(short int), PCM_SIZE, pcm);
            if (read == 0)
                write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
            else
                write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
           
            fwrite(mp3_buffer, write, 1, mp3);
           
        } while (read != 0);
       
        lame_close(lame);
        fclose(mp3);
        fclose(pcm);
    }
    @catch (NSException *exception) {
        NSLog(@"audio_PCMtoMP3audio_PCMtoMP3%@",[exception description]);
    }
    @finally {
        NSString*tempDir =NSTemporaryDirectory();
        NSString *mp3FilePath = [tempDir stringByAppendingPathComponent:@"/ebemate_record.mp3"];
        NSURL *mp3_url=[NSURL fileURLWithPath:mp3FilePath];
        [[NSUserDefaults standardUserDefaults]setObject:mp3FilePath forKey:file_name];
        NSLog(@"MP3生成成功: %@ mp3二进制文件=%@",mp3_url,[NSData dataWithContentsOfURL:mp3_url]);
    }
}

需要下载lame库的同学,可以直接下载下面👇这个录音demo,里面包含了lame需要的两个主要文件,可直接拖到项目中去:
录音demo

五、caf和amr的互转

这两者的互相转换,我用到了一个三方库,libopencore-amrnb,百度可以搜到。
下载完拖到项目中,主要文件如下:


image.png

转换需要的主要代码为amrFileCodec.m文件中的:

//此处将一个录制的caf直接转换为amr格式
//调用方式为 EncodeWAVEToAMR(pcmData,1,16);
NSData* EncodeWAVEToAMR(NSData* data, int nChannels, int nBitsPerSample)
{
//    NSArray *paths               = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
//    NSString *documentPath       = [paths objectAtIndex:0];
//    NSString *wavFile        = [documentPath stringByAppendingPathComponent:[NSString stringWithFormat:@"11.caf"]];
//    NSLog(@"documentPath=%@", documentPath);
  
  if (data==nil){
      //data = [NSData dataWithContentsOfFile:wavFile];
      return nil;
  }
  
  int nPos  = 0;
  char* buf =(char *) [data bytes];
  int maxLen = [data length];
  

  nPos += SkipCaffHead(buf);
  if (nPos>=maxLen) {
      return nil;
  }
  
  //这时取出来的是纯pcm数据
  buf += nPos;
  
  return EncodePCMToAMR(buf,maxLen- nPos,nChannels,nBitsPerSample);
}

转换的过程为:


*  写入文件开始转换
*/
//tmpFileUrl 为录音文件的路径
- (void)writeAuToAmrFile:(NSURL*)tmpFileUrl callback:(writeSuccessBlock)block{
  if ([tmpFileUrl isKindOfClass:[NSURL class]]) {
      NSData *amrData = [self encodeWAVEToAMROfFile:tmpFileUrl]; //开始转换,这里调用的就是上面说的主要的方法
      [self writeToAmrFile:tmpFileUrl amrData:amrData call:block];//得到转换为amr格式的data文件,在保存下来
  }
}
//重点:根据录音文件(caf格式)的url转换为amr格式
- (NSData*)encodeWAVEToAMROfFile:(NSURL*)cafFileUrl{
  NSData *data = EncodeWAVEToAMR([NSData dataWithContentsOfURL:cafFileUrl], 1, 16);
  return data;
}
//保存转换好的amr格式的文件
- (void)writeToAmrFile:(NSURL*)tempFile0 amrData:(NSData*)curAudioData call:(writeSuccessBlock)block{
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  NSString *documentPath = [paths objectAtIndex:0];
  NSString *amrName = [[[tempFile0 lastPathComponent] componentsSeparatedByString:@"."] firstObject];
  NSString *amrFile = [documentPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.amr",amrName]];
  NSLog(@"转换之后的amr文件=%@",amrFile);
  BOOL ist = [curAudioData writeToFile:amrFile atomically:YES];
  if (block) {
      block(ist,curAudioData);
  }

}

caf格式转amr格式demo

推荐阅读更多精彩内容