iOS 音频采集并生成pcm文件,并转化wav格式音频文件

1,184 阅读6分钟
CSAudioCapture类
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>

@interface CSAudioCapture : NSObject

@property(nonatomic,copy)void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 音频采集数据回调
@property(nonatomic,copy)void (^errorCallBack)(NSError *error); // 音频采集错误回调

-(void)startRunning; // 开始采集音频数据
-(void)stopRunning; // 停止采集音频数据

@end
#import "CSAudioCapture.h"
#import <AVFoundation/AVFoundation.h>
#import <mach/mach_time.h>

@interface CSAudioCapture ()

@property(nonatomic,assign)AudioComponentInstance audioComponentInstance; // 音频采集实例
@property(nonatomic,assign)AudioStreamBasicDescription audioFormat; // 音频采集参数
@property(nonatomic,strong)dispatch_queue_t captureQueue;
@property(nonatomic,assign)BOOL isError;

@end

@implementation CSAudioCapture

- (instancetype)init
{
    self = [super init];
    if (self) {
        _captureQueue = dispatch_queue_create("com.KeyFrameKit.audioCapture", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)dealloc
{
    // 清理音频采集实例
    if (_audioComponentInstance) {
        AudioOutputUnitStop(_audioComponentInstance);
        AudioComponentInstanceDispose(_audioComponentInstance);
        _audioComponentInstance = nil;
    }
}

#pragma mark - 开始录音
- (void)startRunning
{
    if (self.isError) {
        return;
    }
    __weak typeof(self) weakSelf = self;
    dispatch_async(_captureQueue, ^{
        if (!weakSelf.audioComponentInstance) {
            NSError *error = nil;
            // 第一次 startRunning 时创建音频采集实例
            [weakSelf setupAudioCaptureInstance:&error];
            if (error) {
                // 捕捉并回调创建音频实例时的错误。
                [weakSelf callBackError:error];
                return;
            }
        }
        
        // 开始采集。
        OSStatus startStatus = AudioOutputUnitStart(weakSelf.audioComponentInstance);
        if (startStatus != noErr) {
            // 捕捉并回调开始采集时的错误。
            [weakSelf callBackError:[NSError errorWithDomain:NSStringFromClass([CSAudioCapture class]) code:startStatus userInfo:nil]];
        }
    });
}

#pragma mark - 结束录音
- (void)stopRunning {
    if (self.isError) {
        return;
    }
    __weak typeof(self) weakSelf = self;
    dispatch_async(_captureQueue, ^{
        if (weakSelf.audioComponentInstance) {
            // 停止采集。
            OSStatus stopStatus = AudioOutputUnitStop(weakSelf.audioComponentInstance);
            if (stopStatus != noErr) {
                // 捕捉并回调停止采集时的错误。
                [weakSelf callBackError:[NSError errorWithDomain:NSStringFromClass([CSAudioCapture class]) code:stopStatus userInfo:nil]];
            }
        }
    });
}

- (void)callBackError:(NSError *)error {
    self.isError = YES;
    if (error && self.errorCallBack) {
        dispatch_async(dispatch_get_main_queue(), ^{
            self.errorCallBack(error);
        });
    }
}

#pragma mark - 创建采集实例
- (void)setupAudioCaptureInstance:(NSError **)error {
    // 1、设置音频组件描述。
    AudioComponentDescription acd = {
        .componentType = kAudioUnitType_Output,
        //.componentSubType = kAudioUnitSubType_VoiceProcessingIO, // 回声消除模式
        .componentSubType = kAudioUnitSubType_RemoteIO,
        .componentManufacturer = kAudioUnitManufacturer_Apple,
        .componentFlags = 0,
        .componentFlagsMask = 0,
    };
    // 2、查找符合指定描述的音频组件。
    AudioComponent component = AudioComponentFindNext(NULL, &acd);
    // 3、创建音频组件实例。
    OSStatus status = AudioComponentInstanceNew(component, &_audioComponentInstance);
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
    // 4、设置实例的属性:可读写。0 不可读写,1 可读写。
    UInt32 flagOne = 1;
    AudioUnitSetProperty(_audioComponentInstance, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &flagOne, sizeof(flagOne));
    
    // 5、设置实例的属性:音频参数,如:数据格式、声道数、采样位深、采样率等。
    AudioStreamBasicDescription asbd = {0};
    asbd.mFormatID = kAudioFormatLinearPCM; // 原始数据为 PCM,采用声道交错格式。
    asbd.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
    asbd.mChannelsPerFrame = 1; // 每帧的声道数
    asbd.mFramesPerPacket = 1; // 每个数据包帧数
    asbd.mBitsPerChannel = 16; // 采样位深
    asbd.mBytesPerFrame = asbd.mChannelsPerFrame * asbd.mBitsPerChannel / 8; // 每帧字节数 (byte = bit / 8)
    asbd.mBytesPerPacket = asbd.mFramesPerPacket * asbd.mBytesPerFrame; // 每个包的字节数
    asbd.mSampleRate = 8000; // 采样率
    self.audioFormat = asbd;
    status = AudioUnitSetProperty(_audioComponentInstance, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &asbd, sizeof(asbd));
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
    // 6、设置实例的属性:数据回调函数。
    AURenderCallbackStruct cb;
    cb.inputProcRefCon = (__bridge void *) self;
    cb.inputProc = audioBufferCallBack;
    status = AudioUnitSetProperty(_audioComponentInstance, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 1, &cb, sizeof(cb));
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
    
    // 7、初始化实例。
    status = AudioUnitInitialize(_audioComponentInstance);
    if (status != noErr) {
        *error = [NSError errorWithDomain:NSStringFromClass(self.class) code:status userInfo:nil];
        return;
    }
}

#pragma mark - Capture CallBack
static OSStatus audioBufferCallBack(void *inRefCon,
                                    AudioUnitRenderActionFlags *ioActionFlags,
                                    const AudioTimeStamp *inTimeStamp,
                                    UInt32 inBusNumber,
                                    UInt32 inNumberFrames,
                                    AudioBufferList *ioData) {
    @autoreleasepool {
        CSAudioCapture *capture = (__bridge CSAudioCapture *) inRefCon;
        if (!capture) {
            return -1;
        }
        
        // 1、创建 AudioBufferList 空间,用来接收采集回来的数据。
        AudioBuffer buffer;
        buffer.mData = NULL;
        buffer.mDataByteSize = 0;
        // 采集的时候设置了数据格式是 kAudioFormatLinearPCM,即声道交错格式,所以即使是双声道这里也设置 mNumberChannels 为 1。
        // 对于双声道的数据,会按照采样位深 16 bit 每组,一组接一组地进行两个声道数据的交错拼装。
        buffer.mNumberChannels = 1;
        AudioBufferList buffers;
        buffers.mNumberBuffers = 1;
        buffers.mBuffers[0] = buffer;
        
        // 2、获取音频 PCM 数据,存储到 AudioBufferList 中。
        // 这里有几个问题要说明清楚:
        // 1)每次回调会过来多少数据?
        // 按照上面采集音频参数的设置:PCM 为声道交错格式、每帧的声道数为 2、采样位深为 16 bit。这样每帧的字节数是 4 字节(左右声道各 2 字节)。
        // 返回数据的帧数是 inNumberFrames。这样一次回调回来的数据字节数是多少就是:mBytesPerFrame(4) * inNumberFrames。
        // 2)这个数据回调的频率跟音频采样率有关系吗?
        // 这个数据回调的频率与音频采样率(上面设置的 mSampleRate 44100)是没关系的。声道数、采样位深、采样率共同决定了设备单位时间里采样数据的大小,这些数据是会缓冲起来,然后一块一块的通过这个数据回调给我们,这个回调的频率是底层一块一块给我们数据的速度,跟采样率无关。
        // 3)这个数据回调的频率是多少?
        // 这个数据回调的间隔是 [AVAudioSession sharedInstance].preferredIOBufferDuration,频率即该值的倒数。我们可以通过 [[AVAudioSession sharedInstance] setPreferredIOBufferDuration:1 error:nil] 设置这个值来控制回调频率。
        OSStatus status = AudioUnitRender(capture.audioComponentInstance,
                                          ioActionFlags,
                                          inTimeStamp,
                                          inBusNumber,
                                          inNumberFrames,
                                          &buffers);
        
        // 3、数据封装及回调。
        if (status == noErr) {
            // 使用工具方法将数据封装为 CMSampleBuffer。
            CMSampleBufferRef sampleBuffer = [CSAudioCapture sampleBufferFromAudioBufferList:buffers inTimeStamp:inTimeStamp inNumberFrames:inNumberFrames description:capture.audioFormat];
            // 回调数据。
            if (capture.sampleBufferOutputCallBack) {
                capture.sampleBufferOutputCallBack(sampleBuffer);
            }
            if (sampleBuffer) {
                CFRelease(sampleBuffer);
            }
        }
        
        return status;
    }
}

+ (CMSampleBufferRef)sampleBufferFromAudioBufferList:(AudioBufferList)buffers inTimeStamp:(const AudioTimeStamp *)inTimeStamp inNumberFrames:(UInt32)inNumberFrames description:(AudioStreamBasicDescription)description {
    CMSampleBufferRef sampleBuffer = NULL; // 待生成的 CMSampleBuffer 实例的引用。
    
    // 1、创建音频流的格式描述信息。
    CMFormatDescriptionRef format = NULL;
    OSStatus status = CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &description, 0, NULL, 0, NULL, NULL, &format);
    if (status != noErr) {
        CFRelease(format);
        return nil;
    }
    
    // 2、处理音频帧的时间戳信息。
    mach_timebase_info_data_t info = {0, 0};
    mach_timebase_info(&info);
    uint64_t time = inTimeStamp->mHostTime;
    // 转换为纳秒。
    time *= info.numer;
    time /= info.denom;
    // PTS。
    CMTime presentationTime = CMTimeMake(time, 1000000000.0f);
    // 对于音频,PTS 和 DTS 是一样的。
    CMSampleTimingInfo timing = {CMTimeMake(1, description.mSampleRate), presentationTime, presentationTime};
    
    // 3、创建 CMSampleBuffer 实例。
    status = CMSampleBufferCreate(kCFAllocatorDefault, NULL, false, NULL, NULL, format, (CMItemCount) inNumberFrames, 1, &timing, 0, NULL, &sampleBuffer);
    if (status != noErr) {
        CFRelease(format);
        return nil;
    }
    
    // 4、创建 CMBlockBuffer 实例。其中数据拷贝自 AudioBufferList,并将 CMBlockBuffer 实例关联到 CMSampleBuffer 实例。
    status = CMSampleBufferSetDataBufferFromAudioBufferList(sampleBuffer, kCFAllocatorDefault, kCFAllocatorDefault, 0, &buffers);
    if (status != noErr) {
        CFRelease(format);
        return nil;
    }
    
    CFRelease(format);
    return sampleBuffer;
}

@end
录制功能
  • 懒加载
@property(nonatomic,strong)CSAudioCapture *audioCapture;
#pragma mark - lazy
- (CSAudioCapture *)audioCapture {
    if (!_audioCapture) {
        __weak typeof(self) weakSelf = self;
        _audioCapture = [[CSAudioCapture alloc] init];
        _audioCapture.errorCallBack = ^(NSError* error) {
            NSLog(@"KFAudioCapture error: %zi %@", error.code, error.localizedDescription);
        };
        // 音频采集数据回调。在这里将 PCM 数据写入文件。
        _audioCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) {
            if (sampleBuffer) {
                
                // 1、获取 CMBlockBuffer,这里面封装着 PCM 数据。
                CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
                size_t lengthAtOffsetOutput, totalLengthOutput;
                char *dataPointer;

                // 2、从 CMBlockBuffer 中获取 PCM 数据存储到文件中。
                CMBlockBufferGetDataPointer(blockBuffer, 0, &lengthAtOffsetOutput, &totalLengthOutput, &dataPointer);
                [weakSelf.fileHandle writeData:[NSData dataWithBytes:dataPointer length:totalLengthOutput]];
            }
        };
    }
    return _audioCapture;
}
  • 文件写入
- (NSFileHandle *)fileHandle {
    if (!_fileHandle) {
        NSString *audioPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.pcm"];
        NSLog(@"PCM file path: %@", audioPath);
        [[NSFileManager defaultManager] removeItemAtPath:audioPath error:nil];
        [[NSFileManager defaultManager] createFileAtPath:audioPath contents:nil attributes:nil];
        _fileHandle = [NSFileHandle fileHandleForWritingAtPath:audioPath];
    }
    return _fileHandle;
}
  • 在开始之前初始化设置音频
- (void)setupAudioSession {
    NSError *error = nil;
    
    // 1、获取音频会话实例。
    AVAudioSession *session = [AVAudioSession sharedInstance];
    // 2、设置分类和选项。
    [session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
    if (error) {
        NSLog(@"AVAudioSession setCategory error.");
        error = nil;
        return;
    }
    // 3、设置模式。
    [session setMode:AVAudioSessionModeDefault error:&error];
    if (error) {
        NSLog(@"AVAudioSession setMode error.");
        error = nil;
        return;
    }
    // 4、激活会话。
    [session setActive:YES error:&error];
    if (error) {
        NSLog(@"AVAudioSession setActive error.");
        error = nil;
        return;
    }
}
  • 麦克风权限
#pragma mark - 检测麦克风权限
-(BOOL)requestMicrophoneAuthorition
{
    AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
    switch (authStatus) {
        case AVAuthorizationStatusNotDetermined: //没有询问是否开启麦克风
        case AVAuthorizationStatusRestricted://未授权
        case AVAuthorizationStatusDenied: //未授权
            return NO;
        break;
        case AVAuthorizationStatusAuthorized://授权
            return YES;
        break;
        default:
            return NO;
        break;
    }
}
  • 开始采集音频数据
[self.audioCapture startRunning];
  • 结束采集音频
[self.audioCapture stopRunning];
转wav格式
- (NSString *)getWavFileFromPcmData:(NSString *)filePath
{
    NSString *wavFilePath = [self wavFilePath];  //wav文件的路径
    NSLog(@"PCM file path : %@",filePath); //pcm文件的路径
    FILE *fout;
    short NumChannels = 1;       //录音通道数
    short BitsPerSample = 16;    //线性采样位数
    int SamplingRate = 8000;     //录音采样率(Hz)
    int numOfSamples = (int)[[NSData dataWithContentsOfURL:[NSURL fileURLWithPath:filePath]] length];
    int ByteRate = NumChannels*BitsPerSample*SamplingRate/8;
    short BlockAlign = NumChannels*BitsPerSample/8;
    int DataSize = NumChannels*numOfSamples*BitsPerSample/8;
    int chunkSize = 16;
    int totalSize = 46 + DataSize;
    short audioFormat = 1;
    if((fout = fopen([wavFilePath cStringUsingEncoding:1], "w")) == NULL){
        printf("Error opening out file ");
    }
    fwrite("RIFF", sizeof(char), 4,fout);
    fwrite(&totalSize, sizeof(int), 1, fout);
    fwrite("WAVE", sizeof(char), 4, fout);
    fwrite("fmt ", sizeof(char), 4, fout);
    fwrite(&chunkSize, sizeof(int),1,fout);
    fwrite(&audioFormat, sizeof(short), 1, fout);
    fwrite(&NumChannels, sizeof(short),1,fout);
    fwrite(&SamplingRate, sizeof(int), 1, fout);
    fwrite(&ByteRate, sizeof(int), 1, fout);
    fwrite(&BlockAlign, sizeof(short), 1, fout);
    fwrite(&BitsPerSample, sizeof(short), 1, fout);
    fwrite("data", sizeof(char), 4, fout);
    fwrite(&DataSize, sizeof(int), 1, fout);
    fclose(fout);
    NSMutableData *pamdata = [NSMutableData dataWithContentsOfURL:[NSURL fileURLWithPath:filePath]];
    NSFileHandle *handle;
    handle = [NSFileHandle fileHandleForUpdatingAtPath:wavFilePath];
    [handle seekToEndOfFile];
    [handle writeData:pamdata];
    [handle closeFile];
    return wavFilePath;
}

- (NSString *)wavFilePath
{
    NSDate* date = [NSDate dateWithTimeIntervalSinceNow:0];//获取当前时间0秒后的时间
    NSTimeInterval time= [date timeIntervalSince1970];// *1000 是精确到毫秒,不乘就是精确到秒
    NSString *fileName = [NSString stringWithFormat:@"%d.wav",(int)time];
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *urlPath = [path stringByAppendingPathComponent:fileName];
    return urlPath;
}