概述
AVFoundation 是一個可以用來使用和創(chuàng)建基于時間的視聽媒體數(shù)據(jù)的框架浑彰。AVFoundation 的構(gòu)建考慮到了目前的硬件環(huán)境和應(yīng)用程序蒙畴,其設(shè)計過程高度依賴多線程機(jī)制。充分利用了多核硬件的優(yōu)勢并大量使用block和GCD機(jī)制缀程,將復(fù)雜的計算機(jī)進(jìn)程放到了后臺線程運(yùn)行纪铺。會自動提供硬件加速操作,確保在大部分設(shè)備上應(yīng)用程序能以最佳性能運(yùn)行秩彤。該框架就是針對64位處理器設(shè)計的,可以發(fā)揮64位處理器的所有優(yōu)勢事哭。
數(shù)字媒體采樣
對媒體內(nèi)容進(jìn)行數(shù)字化主要有兩種方式呐舔。第一種稱為時間采樣,這種方法捕捉一個信號周期內(nèi)的變化慷蠕。第二種采樣方式是空間采樣,一般用在圖片數(shù)字化和其它可視媒體內(nèi)容數(shù)字化的過程食呻×骺唬空間采樣包含對一副圖片在一定分辨率之下捕捉其亮度和色度,進(jìn)而創(chuàng)建由該圖片的像素點(diǎn)數(shù)據(jù)所構(gòu)成的數(shù)字化結(jié)果仅胞。
音頻采樣
當(dāng)我們記錄一個聲音時每辟,一般會使用麥克風(fēng)設(shè)備。麥克風(fēng)設(shè)備是將機(jī)械能量(聲波)轉(zhuǎn)換成(電壓信號)的轉(zhuǎn)換設(shè)備干旧。目前在用的麥克風(fēng)種類很多渠欺,但是這里討論的麥克風(fēng)類型我們稱為電動式麥克風(fēng)。人類可以聽到的音頻范圍是20Hz~20KHz椎眯。
音頻數(shù)字化的過程包含一個編碼方法挠将,稱為線性脈沖編碼調(diào)制(linear pulse-code modulation),比較常見的說法是Linear PCM或LPCM胳岂。這個過程采樣或測量一個固定的音頻信號,過程的周期率被稱為采樣率舔稀。如果不斷提高采樣的頻率乳丰,我們就有可能以數(shù)字化方式準(zhǔn)確表現(xiàn)原始信號的信息。鑒于硬件條件我們還不能復(fù)制出完全一樣的效果内贮,但是我們能找打一個采樣率用于生成足夠好的數(shù)字呈現(xiàn)效果产园。我們稱其為奈奎斯特頻率(Nyquist rate).Harry Nyquist是貝爾實(shí)驗(yàn)室的一名工程師,他精確地捕捉到了一個特定頻率夜郁,該頻率為需要采樣對象的最高頻率的兩倍什燕。除采樣率外,數(shù)字音頻采樣的另一個重要方面是我們能夠捕捉到什么精度的音頻樣本竞端。振幅在線性坐標(biāo)系中進(jìn)行測量屎即,所以會有Linear PCM這個術(shù)語。用于保存樣本值的字節(jié)數(shù)定義了在線性維度上可行的離散度婶熬,同時這個信息也被稱為音頻的位元深度剑勾。為每個樣本的整體量化分配過少的位結(jié)果信息會導(dǎo)致數(shù)字音頻信號產(chǎn)生噪聲和扭曲。使用位元深度為8的方法可以提供256個離散級別數(shù)據(jù)赵颅。對于一些音頻資源來說虽另,這個級別的采樣率已經(jīng)足夠了,但對于大部分音頻內(nèi)容來說還不夠高饺谬。CD音質(zhì)的位元深度為16捂刺,可以達(dá)到65536個離散級別。專業(yè)級別的音頻錄制環(huán)境的位元深度可以達(dá)到24或更高募寨。
AVAudioPlayer
音頻播放器是很多應(yīng)用程序的需求族展,AVAudioPlayer 讓這一需求變得簡單,它提供了一種簡單地從文本或內(nèi)存中播放視頻的方法拔鹰。它提供了Audio Queue Services 中所能找到的核心功能仪缸。除非你需要從網(wǎng)絡(luò)流中播放音頻、需要訪問原始音頻樣本或者需要非常低的時延列肢,否則AVAudioPlayer都能勝任恰画。
- 創(chuàng)建 AVAudioPlayer〈陕恚可以通過NSData和本地音頻文件的NSURL來創(chuàng)建拴还。
- (nullable instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError;
- (nullable instancetype)initWithData:(NSData *)data error:(NSError **)outError;
- 播放控制。如果返回一個有效的播放實(shí)例欧聘,可以調(diào)用
- (BOOL)prepareToPlay;
片林,這樣可以取得需要的音頻硬件并預(yù)加載Audio Queue的緩沖區(qū)。當(dāng)然,調(diào)用- (void)play;
方法的時候會調(diào)用- (BOOL)prepareToPlay;
费封,如果優(yōu)先調(diào)用- (BOOL)prepareToPlay;
方法焕妙,播放的時候再調(diào)用- (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;
- 音頻計量访敌。默認(rèn)沒有開啟音頻計量,一旦啟用音頻測量可以通過
- (void)updateMeters;
方法更新測量值衣盾,我們通過- (float)peakPowerForChannel:(NSUInteger)channelNumber;
寺旺、- (float)averagePowerForChannel:(NSUInteger)channelNumber;
這兩個函數(shù)得到db,平均分貝势决,峰值分貝阻塑。
@property(getter=isMeteringEnabled) BOOL meteringEnabled;
- (void)updateMeters;
- (float)peakPowerForChannel:(NSUInteger)channelNumber;
- (float)averagePowerForChannel:(NSUInteger)channelNumber;
- 靜音問題。當(dāng)手機(jī)在鈴聲果复、靜音間切換的時候陈莽,我們希望聲音不會停止播放,可以加下面的代碼虽抄。
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 文件中,加入以下配置迈窟。
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
- 中斷的問題私植。當(dāng)電話呼入、鬧鈴響起等车酣,在它們終止的時候曲稼,音頻是不會如預(yù)期的恢復(fù),我們可以監(jiān)聽相關(guān)通知來恢復(fù)湖员。
[[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];
}
}
- 線路切換的問題贫悄。比如:在插上耳機(jī)時,我們希望聲音從耳機(jī)內(nèi)傳出娘摔,但我們拔掉耳機(jī)的時候窄坦,我們希望的是音樂停止播放。
[[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 也是構(gòu)建于 Audio Queue Services 之上凳寺,是一個功能強(qiáng)大且簡單易用的音頻錄制類嫡丙。
- 創(chuàng)建 AVAudioRecorder。需要提供本文件的NSURL 以及 相關(guān)錄音參數(shù)設(shè)置读第。
- (nullable instancetype)initWithURL:(NSURL *)url settings:(NSDictionary<NSString *, id> *)settings error:(NSError **)outError;
- (nullable instancetype)initWithURL:(NSURL *)url format:(AVAudioFormat *)format error:(NSError **)outError;
- 音頻格式。AVFormatIDKey 定義錄音文件的音頻格式拥刻。下面的常量都是設(shè)備所支持的值怜瞒。當(dāng)你指定的格式和URL的文件類型不一致的時候會出現(xiàn)
The operation couldn’t be completed. (OSStatus error 1718449215.)
錯誤。
kAudioFormatLinearPCM
kAudioFormatMPEG4AAC
kAudioFormatAppleLossless
kAudioFormatAppleIMA4
kAudioFormatiLBC
kAudioFormatULaw
采樣率。AVSampleRateKey 定義錄音文件的采樣率吴汪。一般來說采樣率越大惠窄,文件內(nèi)容也越大。對于使用什么樣的采樣率漾橙,我們可以盡量使用標(biāo)準(zhǔn)的采樣率杆融。如 8000Hz、16000Hz霜运、22050Hz脾歇、44100Hz。
通道數(shù)淘捡。AVNumberOfChannelsKey 定義錄音文件的通道數(shù)藕各。一般使用默認(rèn)值1,即單聲道焦除。
NSDictionary *setting = @{
AVFormatIDKey : @(kAudioFormatMPEG4AAC),
AVSampleRateKey : @(44100),
AVNumberOfChannelsKey : @(1),
AVLinearPCMBitDepthKey : @(16),
AVEncoderAudioQualityKey : @(AVAudioQualityMedium)
};
- 錄音控制激况。錄音的時候也可以先調(diào)用
- (BOOL)prepareToPlay;
真正錄制的時候再調(diào)用- (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;
- 音頻計量乌逐。默認(rèn)沒有開啟音頻計量,一旦啟用音頻測量可以通過
- (void)updateMeters;
方法更新測量值创葡,我們通過- (float)peakPowerForChannel:(NSUInteger)channelNumber;
浙踢、- (float)averagePowerForChannel:(NSUInteger)channelNumber;
這兩個函數(shù)得到db,平均分貝蹈丸,峰值分貝成黄。
@property(getter=isMeteringEnabled) BOOL meteringEnabled;
- (void)updateMeters;
- (float)peakPowerForChannel:(NSUInteger)channelNumber;
- (float)averagePowerForChannel:(NSUInteger)channelNumber;
音頻播放實(shí)例
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音頻計量轉(zhuǎn)化類。我們將前面得到的分貝值的結(jié)果轉(zhuǎn)化我線性的0到1形式荸百,這需要一個轉(zhuǎn)化的方式闻伶,如果我們把一次計算的結(jié)果記錄下來,那么下次的計算就不需要了够话,因此提供下面的一個方法蓝翰。
//
// 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];
}
音頻錄制實(shí)例
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音頻計量轉(zhuǎn)化類停蕉。我們將前面得到的分貝值的結(jié)果轉(zhuǎn)化我線性的0到1形式,這需要一個轉(zhuǎn)化的方式钙态,如果我們把一次計算的結(jié)果記錄下來慧起,那么下次的計算就不需要了,因此提供下面的一個方法册倒。
//
// 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蚓挤、進(jìn)行音頻錄制。
- (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開發(fā)秘籍:實(shí)踐掌握iOS & OSX應(yīng)用的視聽處理技術(shù)
源碼地址:AVFoundation開發(fā) https://github.com/QinminiOS/AVFoundation