AVFoundation-01音频播放与录制

概述

AVFoundation 是一个可以用来使用和创建基于时间的视听媒体数据的框架。AVFoundation 的构建考虑到了目前的硬件环境和应用程序,其设计过程高度依赖多线程机制。充分利用了多核硬件的优势并大量使用block和GCD机制,将复杂的计算机进程放到了后台线程运行。会自动提供硬件加速操作,确保在大部分设备上应用程序能以最佳性能运行。该框架就是针对64位处理器设计的,可以发挥64位处理器的所有优势。

AVFoundation-01音频播放与录制_第1张图片
iOS 媒体环境.png

数字媒体采样

对媒体内容进行数字化主要有两种方式。第一种称为时间采样,这种方法捕捉一个信号周期内的变化。第二种采样方式是空间采样,一般用在图片数字化和其它可视媒体内容数字化的过程。空间采样包含对一副图片在一定分辨率之下捕捉其亮度和色度,进而创建由该图片的像素点数据所构成的数字化结果。

音频采样

当我们记录一个声音时,一般会使用麦克风设备。麦克风设备是将机械能量(声波)转换成(电压信号)的转换设备。目前在用的麦克风种类很多,但是这里讨论的麦克风类型我们称为电动式麦克风。人类可以听到的音频范围是20Hz~20KHz。

AVFoundation-01音频播放与录制_第2张图片
电动式麦克风内部图.png

音频数字化的过程包含一个编码方法,称为线性脉冲编码调制(linear pulse-code modulation),比较常见的说法是Linear PCM或LPCM。这个过程采样或测量一个固定的音频信号,过程的周期率被称为采样率。如果不断提高采样的频率,我们就有可能以数字化方式准确表现原始信号的信息。鉴于硬件条件我们还不能复制出完全一样的效果,但是我们能找打一个采样率用于生成足够好的数字呈现效果。我们称其为奈奎斯特频率(Nyquist rate).Harry Nyquist是贝尔实验室的一名工程师,他精确地捕捉到了一个特定频率,该频率为需要采样对象的最高频率的两倍。除采样率外,数字音频采样的另一个重要方面是我们能够捕捉到什么精度的音频样本。振幅在线性坐标系中进行测量,所以会有Linear PCM这个术语。用于保存样本值的字节数定义了在线性维度上可行的离散度,同时这个信息也被称为音频的位元深度。为每个样本的整体量化分配过少的位结果信息会导致数字音频信号产生噪声和扭曲。使用位元深度为8的方法可以提供256个离散级别数据。对于一些音频资源来说,这个级别的采样率已经足够了,但对于大部分音频内容来说还不够高。CD音质的位元深度为16,可以达到65536个离散级别。专业级别的音频录制环境的位元深度可以达到24或更高。

AVAudioPlayer

音频播放器是很多应用程序的需求,AVAudioPlayer 让这一需求变得简单,它提供了一种简单地从文本或内存中播放视频的方法。它提供了Audio Queue Services 中所能找到的核心功能。除非你需要从网络流中播放音频、需要访问原始音频样本或者需要非常低的时延,否则AVAudioPlayer都能胜任。

  • 创建 AVAudioPlayer。可以通过NSData和本地音频文件的NSURL来创建。
- (nullable instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError;
- (nullable instancetype)initWithData:(NSData *)data error:(NSError **)outError;
  • 播放控制。如果返回一个有效的播放实例,可以调用 - (BOOL)prepareToPlay; ,这样可以取得需要的音频硬件并预加载Audio Queue的缓冲区。当然,调用 - (void)play; 方法的时候会调用 - (BOOL)prepareToPlay;,如果优先调用 - (BOOL)prepareToPlay; 方法,播放的时候再调用 - (void)play; 方法,可以降低一定的延时。
@property float pan; 
@property float volume;
@property float rate;
@property NSInteger numberOfLoops;

- (BOOL)prepareToPlay;
- (void)play;
- (BOOL)playAtTime:(NSTimeInterval)time;
- (void)pause;
- (void)stop;
  • 音频计量。默认没有开启音频计量,一旦启用音频测量可以通过 - (void)updateMeters; 方法更新测量值,我们通过 - (float)peakPowerForChannel:(NSUInteger)channelNumber;- (float)averagePowerForChannel:(NSUInteger)channelNumber; 这两个函数得到db,平均分贝,峰值分贝。
@property(getter=isMeteringEnabled) BOOL meteringEnabled; 

- (void)updateMeters; 
- (float)peakPowerForChannel:(NSUInteger)channelNumber; 
- (float)averagePowerForChannel:(NSUInteger)channelNumber; 
  • 静音问题。当手机在铃声、静音间切换的时候,我们希望声音不会停止播放,可以加下面的代码。
NSError *error;
if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]) {
    NSLog(@"setCategory error: %@", [error localizedDescription]);
    return;
}

if (![[AVAudioSession sharedInstance] setActive:YES error:&error]) {
    NSLog(@"setActive error: %@", [error localizedDescription]);
    return;
}
  • 锁屏播放的问题。在 Info.plist 文件中,加入以下配置。
UIBackgroundModes

    audio

  • 中断的问题。当电话呼入、闹铃响起等,在它们终止的时候,音频是不会如预期的恢复,我们可以监听相关通知来恢复。
[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleInterruption:)
                                                     name:AVAudioSessionInterruptionNotification
                                                   object:nil];

- (void)handleInterruption:(NSNotification *)notice
{
    AVAudioSessionInterruptionType type = [notice.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    if (type == AVAudioSessionInterruptionTypeBegan) {
        [_musicPlayer pause];
    }else if (type == AVAudioSessionInterruptionTypeEnded) {
        [_musicPlayer play];
    }
}
  • 线路切换的问题。比如:在插上耳机时,我们希望声音从耳机内传出,但我们拔掉耳机的时候,我们希望的是音乐停止播放。
[[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleRouteChange:)
                                                     name:AVAudioSessionRouteChangeNotification
                                                   object:nil];

- (void)handleRouteChange:(NSNotification *)notice
{
    AVAudioSessionRouteChangeReason reason = [notice.userInfo[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
    if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        AVAudioSessionRouteDescription *preRoute = notice.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
        NSString *portType = [[preRoute.outputs firstObject] portType];
        if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
            [_musicPlayer pause];
        }
    }
}

AVAudioRecorder

AVAudioRecorder 也是构建于 Audio Queue Services 之上,是一个功能强大且简单易用的音频录制类。

  • 创建 AVAudioRecorder。需要提供本文件的NSURL 以及 相关录音参数设置。
- (nullable instancetype)initWithURL:(NSURL *)url settings:(NSDictionary *)settings error:(NSError **)outError;
- (nullable instancetype)initWithURL:(NSURL *)url format:(AVAudioFormat *)format error:(NSError **)outError;
  • 音频格式。AVFormatIDKey 定义录音文件的音频格式。下面的常量都是设备所支持的值。当你指定的格式和URL的文件类型不一致的时候会出现 The operation couldn’t be completed. (OSStatus error 1718449215.) 错误。
kAudioFormatLinearPCM
kAudioFormatMPEG4AAC
kAudioFormatAppleLossless
kAudioFormatAppleIMA4
kAudioFormatiLBC
kAudioFormatULaw
  • 采样率。AVSampleRateKey 定义录音文件的采样率。一般来说采样率越大,文件内容也越大。对于使用什么样的采样率,我们可以尽量使用标准的采样率。如 8000Hz、16000Hz、22050Hz、44100Hz。

  • 通道数。AVNumberOfChannelsKey 定义录音文件的通道数。一般使用默认值1,即单声道。

NSDictionary *setting = @{
                           AVFormatIDKey : @(kAudioFormatMPEG4AAC),
                           AVSampleRateKey : @(44100),
                           AVNumberOfChannelsKey : @(1),
                           AVLinearPCMBitDepthKey : @(16),
                           AVEncoderAudioQualityKey : @(AVAudioQualityMedium)
                          };
  • 录音控制。录音的时候也可以先调用 - (BOOL)prepareToPlay; 真正录制的时候再调用 - (void) record; 方法,可以降低一定的延时。
- (BOOL)prepareToRecord; 

- (BOOL)record;
- (BOOL)recordAtTime:(NSTimeInterval)time;
- (BOOL)recordForDuration:(NSTimeInterval) duration;
- (BOOL)recordAtTime:(NSTimeInterval)time forDuration:(NSTimeInterval) duration;

- (void)pause;
- (void)stop;

- (BOOL)deleteRecording;
  • 音频计量。默认没有开启音频计量,一旦启用音频测量可以通过 - (void)updateMeters; 方法更新测量值,我们通过 - (float)peakPowerForChannel:(NSUInteger)channelNumber;- (float)averagePowerForChannel:(NSUInteger)channelNumber; 这两个函数得到db,平均分贝,峰值分贝。
@property(getter=isMeteringEnabled) BOOL meteringEnabled; 
- (void)updateMeters; 

- (float)peakPowerForChannel:(NSUInteger)channelNumber; 
- (float)averagePowerForChannel:(NSUInteger)channelNumber; 

音频播放实例

1、新建 QMAudioController 播放控制类。

//
//  QMAudioController.m
//  AVFoundation
//
//  Created by mac on 17/6/20.
//  Copyright © 2017年 Qinmin. All rights reserved.
//

#import "QMAudioController.h"
#import "QMMeterTable.h"

@interface QMAudioController ()
@property (nonatomic, strong) QMMeterTable *meterTable;
@end

@implementation QMAudioController

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (instancetype)initWithContentsOfURL:(NSURL *)url
{
    if (url && (self = [super init])) {
        NSError *error = nil;
        _musicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
        if (error) {
            NSLog(@"%@", [error localizedDescription]);
            return nil;
        }
       
        _musicPlayer.volume = 0.5f;
        _musicPlayer.pan = 0.0f;
        _musicPlayer.rate = 1.0f;
        _musicPlayer.numberOfLoops = -1;
        _musicPlayer.meteringEnabled = YES;
        [_musicPlayer prepareToPlay];
        
        _meterTable = [[QMMeterTable alloc] init];
        
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleInterruption:)
                                                     name:AVAudioSessionInterruptionNotification
                                                   object:nil];
        
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleRouteChange:)
                                                     name:AVAudioSessionRouteChangeNotification
                                                   object:nil];
        
        return self;
    }
    
    return nil;
}

- (void)play
{
    NSError *error;
    if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]) {
        NSLog(@"setCategory error: %@", [error localizedDescription]);
        return;
    }

    if (![[AVAudioSession sharedInstance] setActive:YES error:&error]) {
        NSLog(@"setActive error: %@", [error localizedDescription]);
        return;
    }
    
    if (![_musicPlayer isPlaying]) {
        [_musicPlayer play];
    }
}

- (BOOL)playAtTime:(NSTimeInterval)time
{
    return [_musicPlayer playAtTime:time];
}

- (void)pause
{
    [_musicPlayer pause];
}

- (void)stop
{
    if ([_musicPlayer isPlaying]) {
        [_musicPlayer stop];
    }
}

- (BOOL)isPlaying
{
    return [_musicPlayer isPlaying];
}

- (void)updateMeters
{
    [_musicPlayer updateMeters];
}

- (float)peakValueForChannel:(NSUInteger)channelNumber
{
    return [_meterTable valueForPower:[_musicPlayer peakPowerForChannel:channelNumber]];
}

- (float)averageValueForChannel:(NSUInteger)channelNumber
{
    return [_meterTable valueForPower:[_musicPlayer averagePowerForChannel:channelNumber]];
}

#pragma mark - Notification
- (void)handleInterruption:(NSNotification *)notice
{
    AVAudioSessionInterruptionType type = [notice.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
    if (type == AVAudioSessionInterruptionTypeBegan) {
        [_musicPlayer pause];
    }else if (type == AVAudioSessionInterruptionTypeEnded) {
        [_musicPlayer play];
    }
}

- (void)handleRouteChange:(NSNotification *)notice
{
    AVAudioSessionRouteChangeReason reason = [notice.userInfo[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
    if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        AVAudioSessionRouteDescription *preRoute = notice.userInfo[AVAudioSessionRouteChangePreviousRouteKey];
        NSString *portType = [[preRoute.outputs firstObject] portType];
        if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
            [_musicPlayer pause];
        }
    }
}

@end

2、新建QMMeterTable音频计量转化类。我们将前面得到的分贝值的结果转化我线性的0到1形式,这需要一个转化的方式,如果我们把一次计算的结果记录下来,那么下次的计算就不需要了,因此提供下面的一个方法。

//
//  QMMeterTable.m
//  AVFoundation
//
//  Created by mac on 17/6/21.
//  Copyright © 2017年 Qinmin. All rights reserved.
//

#import "QMMeterTable.h"

#define MIN_DB          -60.0f
#define TABLE_SIZE      300


@interface QMMeterTable ()
{
    float                _scaleFactor;
    NSMutableArray      *_meterTable;
}
@end

@implementation QMMeterTable

static float dbToAmp(float dB)
{
    return powf(10.0f, 0.05f * dB);
}

- (id)init
{
    if (self = [super init]) {
        float dbResolution = MIN_DB / (TABLE_SIZE - 1);
        
        _meterTable = [NSMutableArray arrayWithCapacity:TABLE_SIZE];
        _scaleFactor = 1.0f / dbResolution;
        
        float minAmp = dbToAmp(MIN_DB);
        float ampRange = 1.0 - minAmp;
        float invAmpRange = 1.0 / ampRange;
        
        for (int i = 0; i < TABLE_SIZE; i++) {
            float decibels = i * dbResolution;
            float amp = dbToAmp(decibels);
            float adjAmp = (amp - minAmp) * invAmpRange;
            _meterTable[i] = @(adjAmp);
        }
    }
    return self;
}

- (float)valueForPower:(float)power
{
    if (power < MIN_DB) {
        return 0.0f;
    } else if (power >= 0.0f) {
        return 1.0f;
    } else {
        int index = (int) (power * _scaleFactor);
        return [_meterTable[index] floatValue];
    }
}
@end

3、使用播放控制类。每秒刷新12次去更新和获取音频计量。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _slider.transform = CGAffineTransformMakeRotation(-M_PI_2);
    
    NSURL *musicURL = [[NSBundle mainBundle] URLForResource:@"1" withExtension:@"mp3"];
    _musicPlayer = [[QMAudioController alloc] initWithContentsOfURL:musicURL];
    
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateMeter:)];
    self.displayLink.frameInterval = 5;
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)updateMeter:(CADisplayLink *)link
{
        [_musicPlayer updateMeters];
        _slider.value = [_musicPlayer averageValueForChannel:0];
}

音频录制实例

1、新建 QMAudioRecorderController 音频录制管理类。

//
//  QMAudioRecorderController.m
//  AVFoundation
//
//  Created by mac on 17/6/21.
//  Copyright © 2017年 Qinmin. All rights reserved.
//

#import "QMAudioRecorderController.h"
#import "QMMeterTable.h"

@interface QMAudioRecorderController()
@property (nonatomic, strong) QMMeterTable *meterTable;
@end

@implementation QMAudioRecorderController

- (instancetype)initWithContentsOfURL:(NSURL *)url
{
    if (url && (self = [super init])) {
        NSDictionary *setting = @{
                               AVFormatIDKey : @(kAudioFormatMPEG4AAC),
                               AVSampleRateKey : @(44100),
                               AVNumberOfChannelsKey : @(1),
                               AVLinearPCMBitDepthKey : @(16),
                               AVEncoderAudioQualityKey : @(AVAudioQualityMedium)
                               };
        
        NSError *error;
        _audioRecorder = [[AVAudioRecorder alloc] initWithURL:url settings:setting error:&error];
        if (error) {
            NSLog(@"%@", [error localizedDescription]);
            return nil;
        }
        
        _meterTable = [[QMMeterTable alloc] init];
        _audioRecorder.meteringEnabled = YES;
        _audioRecorder.delegate = self;
        [_audioRecorder prepareToRecord];
        
        return self;
    }
    
    return nil;
}

- (BOOL)isRecording
{
    return [_audioRecorder isRecording];
}

- (BOOL)record
{
    NSError *error;
    if (![[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
        NSLog(@"setCategory error: %@", [error localizedDescription]);
        return NO;
    }
    
    if (![[AVAudioSession sharedInstance] setActive:YES error:&error]) {
        NSLog(@"setActive error: %@", [error localizedDescription]);
        return NO;
    }
    
    return [_audioRecorder record];
}

- (BOOL)recordAtTime:(NSTimeInterval)time
{
    return [_audioRecorder recordAtTime:time];
}

- (BOOL)recordForDuration:(NSTimeInterval) duration
{
    return [_audioRecorder recordForDuration:duration];
}

- (BOOL)recordAtTime:(NSTimeInterval)time forDuration:(NSTimeInterval) duration
{
    return [_audioRecorder recordAtTime:time forDuration:duration];
}

- (void)pause
{
    [_audioRecorder pause];
}

- (void)stop
{
    [_audioRecorder stop];
}

- (BOOL)deleteRecording
{
    return [_audioRecorder deleteRecording];
}

- (void)updateMeters
{
    [_audioRecorder updateMeters];
}

- (float)peakValueForChannel:(NSUInteger)channelNumber
{
    return [_meterTable valueForPower:[_audioRecorder peakPowerForChannel:channelNumber]];
}

- (float)averageValueForChannel:(NSUInteger)channelNumber
{
    return [_meterTable valueForPower:[_audioRecorder averagePowerForChannel:channelNumber]];
}

#pragma mark - AVAudioRecorderDelegate
- (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag
{
    if (self.finishCallback) {
        self.finishCallback(flag, nil);
    }
}

- (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder error:(NSError * __nullable)error;
{
    NSLog(@"RecorderEncodeError error:%@", [error localizedDescription]);
    
    if (self.finishCallback) {
        self.finishCallback(NO, error);
    }
}

@end

2、新建QMMeterTable音频计量转化类。我们将前面得到的分贝值的结果转化我线性的0到1形式,这需要一个转化的方式,如果我们把一次计算的结果记录下来,那么下次的计算就不需要了,因此提供下面的一个方法。

//
//  QMMeterTable.m
//  AVFoundation
//
//  Created by mac on 17/6/21.
//  Copyright © 2017年 Qinmin. All rights reserved.
//

#import "QMMeterTable.h"

#define MIN_DB          -60.0f
#define TABLE_SIZE      300


@interface QMMeterTable ()
{
    float                _scaleFactor;
    NSMutableArray      *_meterTable;
}
@end

@implementation QMMeterTable

static float dbToAmp(float dB)
{
    return powf(10.0f, 0.05f * dB);
}

- (id)init
{
    if (self = [super init]) {
        float dbResolution = MIN_DB / (TABLE_SIZE - 1);
        
        _meterTable = [NSMutableArray arrayWithCapacity:TABLE_SIZE];
        _scaleFactor = 1.0f / dbResolution;
        
        float minAmp = dbToAmp(MIN_DB);
        float ampRange = 1.0 - minAmp;
        float invAmpRange = 1.0 / ampRange;
        
        for (int i = 0; i < TABLE_SIZE; i++) {
            float decibels = i * dbResolution;
            float amp = dbToAmp(decibels);
            float adjAmp = (amp - minAmp) * invAmpRange;
            _meterTable[i] = @(adjAmp);
        }
    }
    return self;
}

- (float)valueForPower:(float)power
{
    if (power < MIN_DB) {
        return 0.0f;
    } else if (power >= 0.0f) {
        return 1.0f;
    } else {
        int index = (int) (power * _scaleFactor);
        return [_meterTable[index] floatValue];
    }
}

@end

3、进行音频录制。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _slider.transform = CGAffineTransformMakeRotation(-M_PI_2);
    
    NSURL *recordFileURL = [NSURL fileURLWithPath:kDocumentPath(@"1.aac")];
    _audioRecorder = [[QMAudioRecorderController alloc] initWithContentsOfURL:recordFileURL];
    
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateMeter:)];
    self.displayLink.frameInterval = 5;
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

#pragma mark - Timer
- (void)updateMeter:(CADisplayLink *)link
{
     [_audioRecorder updateMeters];
     _slider.value = [_audioRecorder averageValueForChannel:0];
}

参考

AVFoundation开发秘籍:实践掌握iOS & OSX应用的视听处理技术

源码地址:AVFoundation开发 https://github.com/QinminiOS/AVFoundation

你可能感兴趣的:(AVFoundation-01音频播放与录制)