iOS 音頻的錄制、播放及音頻文件管理

[TOC]

音頻會(huì)話

在使用Apple設(shè)備時(shí),我們注意到有些應(yīng)用打開音頻播放時(shí),其他音頻就會(huì)終止,而有些應(yīng)用卻可以同時(shí)使用音頻,這就出現(xiàn)了多種聲音的情況,這些音頻環(huán)境就涉及了音頻會(huì)話內(nèi)容:iOS中,每個(gè)應(yīng)用都有一個(gè)音頻會(huì)話,及AVAudioSession,它屬于AVFoundation框架中,是一種單例模式。

會(huì)話類型 說明 是否要求輸入 是否需要輸出 是否遵從靜音鍵
AVAudioSessionCategoryAmbient 混音播放,可以與其他音頻應(yīng)用同時(shí)播放
AVAudioSessionCategorySoloAmbient 獨(dú)占播放
AVAudioSessionCategoryPlayback 后臺(tái)播放,也是獨(dú)占的
AVAudioSessionCategoryRecord 錄音模式,用于錄音時(shí)使用
AVAudioSessionCategoryPlayAndRecord 播放和錄音,此時(shí)可以錄音也可以播放
AVAudioSessionCategoryAudioProcessing 硬件解碼音頻,此時(shí)不能播放和錄制
AVAudioSessionCategoryMultiRoute 多種輸入輸出,例如可以耳機(jī)、USB設(shè)備同時(shí)播放

注意:是否遵循靜音鍵表示在播放過程中如果用戶通過硬件設(shè)置為靜音是否能關(guān)閉聲音

例:設(shè)置錄音環(huán)境

NSError *sessionError;
// 設(shè)置音頻會(huì)話為錄音環(huán)境
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord error:&sessionError];
if (sessionError){
    NSLog(@"Error creating session: %@",[sessionError description]);
}else{
    // 啟動(dòng)該會(huì)話
    [[AVAudioSession sharedInstance] setActive:YES error:&sessionError];
}

說明:假如我們?cè)O(shè)置的音頻會(huì)話為獨(dú)占的話(如:后臺(tái)播放),其他正在播放音頻的應(yīng)用就會(huì)被終止播放


音頻的播放

音頻的播放分為音效播放和音樂播放。前者是一些短音頻播放,不需要進(jìn)度、循環(huán)次數(shù)控制。后者是一些較長(zhǎng)的音頻,需要對(duì)進(jìn)度、循環(huán)次數(shù)做精確控制。在iOS中,分別使用AudioToolbox.framework和AVFoundation.framework來完成音效和音頻的播放

[TOC]

音效播放

AudioToolbox.framework是一套基于C語言的框架,使用它來播放音效其本質(zhì)是將短音頻注冊(cè)到系統(tǒng)聲音服務(wù)(System Sound Service)。System Sound Service是一種簡(jiǎn)單、底層的聲音播放服務(wù),但是它本身也存在著一些限制:

  1. 音頻播放時(shí)間不能超過30s
  2. 數(shù)據(jù)必須是PCM或者IMA4格式
  3. 音頻文件必須打包成.caf、.aif、.wav中的一種

使用步驟如下:

  1. 調(diào)用AudioServicesCreateSystemSoundID函數(shù)來獲取系統(tǒng)聲音ID
  2. 調(diào)用AudioServicesPlaySystemSound或者AudioServicesPlayAlertSound 方法播放音效(后者帶有震動(dòng)效果)
  3. 如果需要監(jiān)聽播放完成操作,則使用AudioServicesAddSystemSoundCompletion方法注冊(cè)回調(diào)函數(shù)

例子:

-(void)playAudioWithName:(NSString*)soundName{
    NSURL* system_sound_url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:soundName ofType:nil]];
    //1.獲取系統(tǒng)聲音ID
    SystemSoundID system_sound_id = 0;
    AudioServicesCreateSystemSoundID((__bridge CFURLRef)system_sound_url,&system_sound_id);
    //需要播放完成之后執(zhí)行某些操作,可以調(diào)用下面方法注冊(cè)一個(gè)播放完成回調(diào)函數(shù)
/*    AudioServicesAddSystemSoundCompletion(system_sound_id,
                                          NULL, // uses the main run loop
                                          NULL, // uses kCFRunLoopDefaultMode
                                          soundCompleteCallback, // the name of our custom callback function
                                          NULL // for user data, but we don't need to do that in this case, so we just pass NULL
                                          );
*/
    //2.播放音頻
//    AudioServicesPlaySystemSound(system_sound_id);//播放音效
    AudioServicesPlayAlertSound(system_sound_id);//播放并震動(dòng)
}

void soundCompleteCallback(SystemSoundID soundID,void * userData){
 NSLog(@"播放完成");
 //do what you want to do
 AudioServicesDisposeSystemSoundID(soundID);
}

補(bǔ)充:獲取系統(tǒng)音效

_systemSounds               = [NSMutableArray array];
// 讀取文件系統(tǒng)
NSFileManager *fileManager  = [NSFileManager defaultManager];
NSURL         *directoryURL = [NSURL URLWithString:@"/System/Library/Audio/UISounds"];
NSArray       *keys         = [NSArray arrayWithObject:NSURLIsDirectoryKey];
NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:directoryURL
                                      includingPropertiesForKeys:keys
                                                         options:0
                                                    errorHandler:^(NSURL *url, NSError *error) {
                                                        return YES;
                                                    }];
for (NSURL *url in enumerator) {
    NSError  *error;
    NSNumber *isDirectory = nil;
    if (! [url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:&error]) {
    } else if (![isDirectory boolValue]) {
        SystemSoundID soundID;
        AudioServicesCreateSystemSoundID((__bridge_retained CFURLRef)url, &soundID);
        // 音效的數(shù)據(jù)模型
        SoundInfomation *sound = [[SoundInfomation alloc] init];
        sound.soundID   = soundID;
        sound.soundUrl  = url;
        sound.soundName = url.lastPathComponent;
        [_systemSounds addObject:sound];
    }
}

[TOC]

音樂播放

如果播放較大的音頻或者要對(duì)音頻有精確的控制則System Sound Service可能就很難滿足實(shí)際需求了,通常這種情況會(huì)選擇使用AVFoundation.framework中的AVAudioPlayer來實(shí)現(xiàn)。AVAudioPlayer可以看成一個(gè)播放器,它支持多種音頻格式,而且能夠進(jìn)行進(jìn)度、音量、播放速度等控制

屬性 說明
@property(readonly, getter=isPlaying) BOOL playing 是否正在播放,只讀
@property(readonly) NSUInteger numberOfChannels 音頻聲道數(shù),只讀
@property(readonly) NSTimeInterval duration 音頻時(shí)長(zhǎng)
@property(readonly) NSURL *url 音頻文件路徑,只讀
@property(readonly) NSData *data 音頻數(shù)據(jù),只讀
@property float pan 立體聲平衡,如果為-1.0則完全左聲道,如果0.0則左右聲道平衡,如果為1.0則完全為右聲道
@property float volume 音量大小,范圍0-1.0
@property BOOL enableRate 是否允許改變播放速率
@property float rate 播放速率,范圍0.5-2.0,如果為1.0則正常播放,如果要修改播放速率則必須設(shè)置enableRate為YES
@property NSTimeInterval currentTime 當(dāng)前播放時(shí)長(zhǎng)
@property(readonly) NSTimeInterval deviceCurrentTime 輸出設(shè)備播放音頻的時(shí)間,注意如果播放中被暫停此時(shí)間也會(huì)繼續(xù)累加
@property NSInteger numberOfLoops 循環(huán)播放次數(shù),如果為0則不循環(huán),如果小于0則無限循環(huán),大于0則表示循環(huán)次數(shù)
@property(readonly) NSDictionary *settings 音頻播放設(shè)置信息,只讀
@property(getter=isMeteringEnabled) BOOL meteringEnabled 是否啟用音頻測(cè)量,默認(rèn)為NO,一旦啟用音頻測(cè)量可以通過updateMeters方法更新測(cè)量值
對(duì)象方法 說明
-(instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError 使用文件URL初始化播放器,注意這個(gè)URL不能是HTTP URL,AVAudioPlayer不支持加載網(wǎng)絡(luò)媒體流,只能播放本地文件,如果需要播放網(wǎng)絡(luò)URL,可以嘗試AVPlayer
-(BOOL)prepareToPlay 加載音頻文件到緩沖區(qū),注意即使在播放之前音頻文件沒有加載到緩沖區(qū)程序也會(huì)隱式調(diào)用此方法。
-(BOOL)play 播放音頻文件
-(BOOL)playAtTime:(NSTimeInterval)time 在指定的時(shí)間開始播放音頻
-(void)pause 暫停播放
-(void)stop 停止播放
-(void)updateMeters 更新音頻測(cè)量值,注意如果要更新音頻測(cè)量值必須設(shè)置meteringEnabled為YES,通過音頻測(cè)量值可以即時(shí)獲得音頻分貝等信息
-(float)peakPowerForChannel:(NSUInteger)channelNumber 獲得指定聲道的分貝峰值,注意如果要獲得分貝峰值必須在此之前調(diào)用updateMeters方法
-(float)averagePowerForChannel:(NSUInteger)channelNumber 獲得指定聲道的分貝平均值,注意如果要獲得分貝平均值必須在此之前調(diào)用updateMeters方法
代理方法 說明
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag 音頻播放完成
-(void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error 音頻解碼發(fā)生錯(cuò)誤
-(void)audioPlayerBeginInterruption:(AVAudioPlayer *)player 播放被打斷
-(void)audioPlayerEndInterruption:(AVAudioPlayer *)player 結(jié)束打斷

使用步驟:

  1. 初始化AVAudioPlayer對(duì)象,此時(shí)需要指定本地文件路徑
  2. 設(shè)置播放器屬性,例如重復(fù)次數(shù)、音量大小等
  3. 調(diào)用play方法播放

示例:

-(void)playAudioWithUrl:(NSURL*)url{
    NSError *error=nil;
    // 1、初始化
    _audioPlayer=[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
    // 2、設(shè)置相關(guān)屬性
    _audioPlayer.numberOfLoops=0;   // 設(shè)置為0不循環(huán)
    _audioPlayer.delegate = self;
    if (error) {
        NSLog(@"創(chuàng)建播放器過程中發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
    }
    if (![_audioPlayer isPlaying]) {
        //解決音量小的問題
        AVAudioSession *audioSession = [AVAudioSession sharedInstance];
        NSError *err = nil;
        [audioSession setCategory:AVAudioSessionCategoryPlayback error:&err];
        // 3、播放
        [_audioPlayer play];    // 播放音頻
    }
}
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{
    NSLog(@"音樂播放完成...");
}

[TOC]

音頻錄制

在AVFoundation框架中還要一個(gè)AVAudioRecorder類專門處理錄音操作,它同樣支持多種音頻格式。與AVAudioPlayer類似,你完全可以將它看成是一個(gè)錄音機(jī)控制類

屬性 說明
@property(readonly, getter=isRecording) BOOL recording 是否正在錄音,只讀
@property(readonly) NSURL *url 錄音文件地址,只讀
@property(readonly) NSDictionary *settings 錄音文件設(shè)置,只讀
@property(readonly) NSTimeInterval currentTime 錄音時(shí)長(zhǎng),只讀,注意僅僅在錄音狀態(tài)可用
@property(readonly) NSTimeInterval deviceCurrentTime 輸入設(shè)置的時(shí)間長(zhǎng)度,只讀,注意此屬性一直可訪問
@property(getter=isMeteringEnabled) BOOL meteringEnabled 是否啟用錄音測(cè)量,如果啟用錄音測(cè)量可以獲得錄音分貝等數(shù)據(jù)信息
對(duì)象方法 說明
-(instancetype)initWithURL:(NSURL *)url settings:(NSDictionary *)settings error:(NSError **)outError 錄音機(jī)對(duì)象初始化方法,注意其中的url必須是本地文件url,settings是錄音格式、編碼等設(shè)置
-(BOOL)prepareToRecord 準(zhǔn)備錄音,主要用于創(chuàng)建緩沖區(qū),如果不手動(dòng)調(diào)用,在調(diào)用record錄音時(shí)也會(huì)自動(dòng)調(diào)用
-(BOOL)record 開始錄音/恢復(fù)錄音
-(BOOL)recordAtTime:(NSTimeInterval)time 在指定將來的某個(gè)時(shí)刻開始錄音
-(BOOL)recordForDuration:(NSTimeInterval) duration 指定的時(shí)長(zhǎng)開始錄音,如60'
-(BOOL)recordAtTime:(NSTimeInterval)time forDuration:(NSTimeInterval) duration 在指定的時(shí)間開始錄音,并指定錄音時(shí)長(zhǎng)
-(void)pause 暫停錄音
-(void)stop 停止錄音
-(BOOL)deleteRecording 刪除錄音,注意要?jiǎng)h除錄音此時(shí)錄音機(jī)必須處于停止?fàn)顟B(tài)
-(void)updateMeters 更新測(cè)量數(shù)據(jù),注意只有meteringEnabled為YES此方法才可用
-(float)peakPowerForChannel:(NSUInteger)channelNumber 指定通道的測(cè)量峰值,注意只有調(diào)用完updateMeters才有值
-(float)averagePowerForChannel:(NSUInteger)channelNumber 指定通道的測(cè)量平均值,注意只有調(diào)用完updateMeters才有值
代理方法 說明
-(void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag 完成錄音
-(void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder error:(NSError *)error 錄音編碼發(fā)生錯(cuò)誤
-(void)audioRecorderBeginInterruption:(AVAudioRecorder *)recorder 錄音被打斷
-(void)audioRecorderEndInterruption:(AVAudioRecorder *)recorder 錄音打斷結(jié)束

AVAudioRecorder很多屬性和方法跟AVAudioPlayer都是類似的,但是它的創(chuàng)建有所不同,在創(chuàng)建錄音機(jī)時(shí)除了指定路徑外還必須指定錄音設(shè)置信息,因?yàn)殇浺魴C(jī)必須知道錄音文件的格式、采樣率、通道數(shù)、每個(gè)采樣點(diǎn)的位數(shù)等信息,但是也并不是所有的信息都必須設(shè)置,通常只需要幾個(gè)常用設(shè)置

使用步驟:

  1. 設(shè)置音頻會(huì)話類型為AVAudioSessionCategoryRecord,錄音模式
  2. 創(chuàng)建錄音機(jī)AVAudioRecorder,指定錄音保存的路徑并且設(shè)置錄音屬性
  3. 調(diào)用record開始錄制

示例:

-(void)setupRecorder{
    //設(shè)置音頻會(huì)話
    NSError *sessionError;
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord error:&sessionError];
    if (sessionError){
        NSLog(@"Error creating session: %@",[sessionError description]);
    }else{
        [[AVAudioSession sharedInstance] setActive:YES error:&sessionError];
    }
    //錄音設(shè)置
    //創(chuàng)建錄音文件保存路徑
    NSURL *url = [self getSavePath];
    //創(chuàng)建錄音機(jī)
    NSError *error = nil;
    _audioRecorder = [[AVAudioRecorder alloc] initWithURL:url settings:self.setting error:&error];
    if (error) {
        NSLog(@"創(chuàng)建錄音機(jī)對(duì)象時(shí)發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
    }
    _audioRecorder.delegate = self;
    _audioRecorder.meteringEnabled = YES;//如果要監(jiān)控聲波則必須設(shè)置為YES
    [_audioRecorder prepareToRecord];
    if (![_audioRecorder isRecording]) {
        [_audioRecorder record];//首次使用應(yīng)用時(shí)如果調(diào)用record方法會(huì)詢問用戶是否允許使用麥克風(fēng)
    }
}

// !!!: 錄音設(shè)置
-(NSDictionary *)setting{
    if (_setting==nil) {
        NSMutableDictionary *setting = [NSMutableDictionary dictionary];
        //錄音格式
        [setting setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey];
        //采樣率,8000/11025/22050/44100/96000(影響音頻的質(zhì)量),8000是電話采樣率
        [setting setObject:@(22050) forKey:AVSampleRateKey];
        //通道 , 1/2
        [setting setObject:@(2) forKey:AVNumberOfChannelsKey];
        //采樣點(diǎn)位數(shù),分為8、16、24、32, 默認(rèn)16
        [setting setObject:@(16) forKey:AVLinearPCMBitDepthKey];
        //是否使用浮點(diǎn)數(shù)采樣
        [setting setObject:@(YES) forKey:AVLinearPCMIsFloatKey];
        // 錄音質(zhì)量
        [setting setObject:@(AVAudioQualityHigh) forKey:AVEncoderAudioQualityKey];
        //....其他設(shè)置等
    }
    return _setting;
}

[TOC]

音頻管理

下面是筆者自己寫的一個(gè)音頻管理類??梢赃M(jìn)行音頻錄制、播放、獲取音頻信息、管理音頻文件等功能

.h 文件

//
//  AudioManager.h
//  ExeToExp
//
//  Created by LOLITA on 17/3/28.
//  Copyright ? 2017年 LOLITA. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@protocol AudioManagerDelegate;
@interface AudioManager : NSObject
@property (nonatomic,weak) id <AudioManagerDelegate> delegate;

+(instancetype)sharedInstance;

// !!!: 開始錄制
-(void)startRecord;

// !!!: 暫停錄制
-(void)pauseRecord;

// !!!: 恢復(fù)錄制
-(void)resumeRecord;

// !!!: 停止錄制
-(void)stopRecord;

// !!!: 取消當(dāng)前錄制
-(void)cancelRecord;

// !!!: 播放語音
-(void)playAudioWithUrl:(NSURL*)url;

// !!!: 停止語音播放
-(void)stopPlay;

// !!!: 暫停語音播放
-(void)pausePlay;

// !!!: 恢復(fù)語音播放
-(void)resumePlay;

// !!!: 獲取當(dāng)前錄制文件的路徑
-(NSURL*)recordCurrentAudioFile;

// !!!: 獲取語音時(shí)長(zhǎng)
-(float)durationWithAudio:(NSURL *)audioUrl;

// !!!: 刪除本地音頻文件下所有文件
-(void)removeAllAudioFile;

// !!!: 刪除本地指定音頻文件
-(void)removeAudioFile:(NSURL*)url;

// !!!: 刪除指定后綴的文件,如“.wav”,“.caf”
-(void)removeFileSuffixList:(NSArray<NSString*>*)suffixList filePath:(NSString*)path;
@end

@protocol AudioManagerDelegate <NSObject>
@optional
-(void)audioRecorderDidFinishRecording:(AVAudioRecorder*)recorder successfullyFlag:(BOOL)flag;  // 錄制完成
-(void)audioPowerChange:(CGFloat)power; // 音量
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag; // 播放完成
@end

.m 文件

//
//  AudioManager.m
//  ExeToExp
//
//  Created by LOLITA on 17/3/28.
//  Copyright ? 2017年 LOLITA. All rights reserved.
//

#define kAudioFolder @"AudioFolder" // 音頻文件夾
#import "AudioManager.h"
@interface AudioManager ()<AVAudioRecorderDelegate,AVAudioPlayerDelegate>
@property (nonatomic,strong) AVAudioRecorder *audioRecorder;    // 錄音機(jī)
@property (nonatomic,strong) AVAudioPlayer *audioPlayer;        // 音頻播放器
@property (strong ,nonatomic) NSDictionary *setting;            // 錄音機(jī)的設(shè)置
@property (copy ,nonatomic) NSString *audioDir;                 // 錄音文件夾路徑
@property (nonatomic,strong) NSTimer *timer;    // 錄音聲波監(jiān)控
@property (copy ,nonatomic) NSString *filename; // 記錄當(dāng)前文件名
@property (assign ,nonatomic) BOOL cancelCurrentRecord;    // 取消當(dāng)前錄制
@end;

@implementation AudioManager
+(instancetype)sharedInstance{
    static AudioManager *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[AudioManager alloc] init];
    });
    return instance;
}
#pragma mark - <************************** 一些初始化 **************************>
// !!!: 配置錄音機(jī)
-(void)setupRecorder{
    //設(shè)置音頻會(huì)話
    NSError *sessionError;
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord error:&sessionError];
    if (sessionError){
        NSLog(@"Error creating session: %@",[sessionError description]);
    }else{
        [[AVAudioSession sharedInstance] setActive:YES error:&sessionError];
    }
    //錄音設(shè)置
    //創(chuàng)建錄音文件保存路徑
    NSURL *url = [self getSavePath];
    //創(chuàng)建錄音機(jī)
    NSError *error = nil;
    _audioRecorder = [[AVAudioRecorder alloc] initWithURL:url settings:self.setting error:&error];
    _audioRecorder.delegate = self;
    _audioRecorder.meteringEnabled = YES;//如果要監(jiān)控聲波則必須設(shè)置為YES
    [_audioRecorder prepareToRecord];
    if (error) {
        NSLog(@"創(chuàng)建錄音機(jī)對(duì)象時(shí)發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
    }
}


// !!!: 錄音聲波監(jiān)
-(NSTimer *)timer{
    if (!_timer) {
        _timer=[NSTimer scheduledTimerWithTimeInterval:0.1f target:self selector:@selector(powerChange) userInfo:nil repeats:YES];
    }
    return _timer;
}

// !!!: 錄音設(shè)置
-(NSDictionary *)setting{
    if (_setting==nil) {
        NSMutableDictionary *setting = [NSMutableDictionary dictionary];
        //錄音格式
        [setting setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey];
        //采樣率,8000/11025/22050/44100/96000(影響音頻的質(zhì)量),8000是電話采樣率
        [setting setObject:@(22050) forKey:AVSampleRateKey];
        //通道 , 1/2
        [setting setObject:@(2) forKey:AVNumberOfChannelsKey];
        //采樣點(diǎn)位數(shù),分為8、16、24、32, 默認(rèn)16
        [setting setObject:@(16) forKey:AVLinearPCMBitDepthKey];
        //是否使用浮點(diǎn)數(shù)采樣
        [setting setObject:@(YES) forKey:AVLinearPCMIsFloatKey];
        // 錄音質(zhì)量
        [setting setObject:@(AVAudioQualityHigh) forKey:AVEncoderAudioQualityKey];
        //....其他設(shè)置等
    }
    return _setting;
}

// !!!: 錄音文件夾
-(NSString *)audioDir{
    if (_audioDir==nil) {
        _audioDir = NSTemporaryDirectory();
        _audioDir = [_audioDir stringByAppendingPathComponent:kAudioFolder];
        BOOL isDir = NO;
        NSFileManager *fileManager = [NSFileManager defaultManager];
        BOOL existed = [fileManager fileExistsAtPath:_audioDir isDirectory:&isDir];
        if (!(isDir == YES && existed == YES)){
            [fileManager createDirectoryAtPath:_audioDir withIntermediateDirectories:YES attributes:nil error:nil];
        }
    }
    return _audioDir;
}

#pragma mark - <************************** 事件 **************************>
// !!!: 開始錄制
-(void)startRecord{
    [self setupRecorder];
    if (![self.audioRecorder isRecording]) {
        [self.audioRecorder record];//首次使用應(yīng)用時(shí)如果調(diào)用record方法會(huì)詢問用戶是否允許使用麥克風(fēng)
//        [self.audioRecorder recordForDuration:60];    // 錄音時(shí)長(zhǎng)
        self.timer.fireDate=[NSDate distantPast];
    }
}
// !!!: 暫停錄制
-(void)pauseRecord{
    if ([self.audioRecorder isRecording]) {
        [self.audioRecorder pause];
        self.timer.fireDate=[NSDate distantFuture];
    }
}
// !!!: 恢復(fù)錄制
-(void)resumeRecord{
    if (![self.audioRecorder isRecording]) {
        [self.audioRecorder record];
        self.timer.fireDate=[NSDate distantPast];
    }
}
// !!!: 停止錄制
-(void)stopRecord{
    [self.audioRecorder stop];
    self.timer.fireDate=[NSDate distantFuture];
}

// !!!: 取消當(dāng)前錄制
-(void)cancelRecord{
    self.cancelCurrentRecord = YES;
    [self stopRecord];
    if ([self.audioRecorder deleteRecording]) {
        NSLog(@"刪除錄音文件!");
    }
}


// !!!: 播放音頻文件
-(void)playAudioWithUrl:(NSURL*)url{
    //語音播放
    NSError *error=nil;
    _audioPlayer=[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
    _audioPlayer.numberOfLoops=0;   // 設(shè)置為0不循環(huán)
    _audioPlayer.delegate = self;
    if (error) {
        NSLog(@"創(chuàng)建播放器過程中發(fā)生錯(cuò)誤,錯(cuò)誤信息:%@",error.localizedDescription);
    }
    if (![_audioPlayer isPlaying]) {
        //解決音量小的問題
        AVAudioSession *audioSession = [AVAudioSession sharedInstance];
        NSError *err = nil;
        [audioSession setCategory:AVAudioSessionCategoryPlayback error:&err];
        [_audioPlayer play];    // 播放音頻
    }
}

// !!!: 停止播放語音
-(void)stopPlay{
    [self.audioPlayer stop];
}

// !!!: 暫停語音
-(void)pausePlay{
    [self.audioPlayer pause];
}

// !!!: 恢復(fù)語音
-(void)resumePlay{
    [self.audioPlayer play];
}


#pragma mark - <************************** 獲取數(shù)據(jù) **************************>
// !!!: 獲取錄音保存路徑
-(NSURL*)getSavePath{
    self.filename = [NSString stringWithFormat:@"audio_%@.wav",[self getDateString]];
    NSString* fileUrlString = [self.audioDir stringByAppendingPathComponent:self.filename];
    NSURL *url = [NSURL fileURLWithPath:fileUrlString];
    return url;
}

// !!!: 返回音頻文件地址
-(NSURL *)recordCurrentAudioFile{
    NSString* fileUrlString = [self.audioDir stringByAppendingPathComponent:self.filename];
    NSURL *url = [NSURL fileURLWithPath:fileUrlString];
    return url;
}


// !!!: 獲取語音時(shí)長(zhǎng)
-(float)durationWithAudio:(NSURL *)audioUrl{
    AVURLAsset* audioAsset = [AVURLAsset URLAssetWithURL:audioUrl options:nil];
    CMTime audioDuration = audioAsset.duration;
    float audioDurationSeconds = CMTimeGetSeconds(audioDuration);
    return audioDurationSeconds;
}


// !!!: 刪除所有文件夾
-(void)removeAllAudioFile{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager removeItemAtPath:self.audioDir error:nil]) {
        NSLog(@"刪除文件夾成功?。?);
    }
}

// !!!: 刪除指定文件
-(void)removeAudioFile:(NSURL *)url{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager removeItemAtPath:url.path error:nil]) {
        NSLog(@"刪除錄音文件成功??!");
    }
}

// !!!: 刪除指定后綴的文件
-(void)removeFileSuffixList:(NSArray<NSString *> *)suffixList filePath:(NSString *)path{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSArray *contentOfFolder = [fileManager contentsOfDirectoryAtPath:path error:NULL];
    for (NSString *aPath in contentOfFolder) {
        NSString * fullPath = [path stringByAppendingPathComponent:aPath];
        BOOL isDir = NO;
        if ([fileManager fileExistsAtPath:fullPath isDirectory:&isDir]) {
            if (isDir == YES) {
                // 是文件夾,則繼續(xù)遍歷
                [self removeFileSuffixList:suffixList filePath:fullPath];
            }
            else{
                NSLog(@"file-:%@", aPath);
                for (NSString* suffix in suffixList) {
                    if ([aPath hasSuffix:suffix]) {
                        if ([fileManager removeItemAtPath:fullPath error:nil]) {
                            NSLog(@"刪除文件成功!!");
                        }
                    }
                }
            }
        }
    }
}



#pragma mark - <************************** 代理方法 **************************>
// !!!: 錄音代理事件
-(void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag{
    if (self.cancelCurrentRecord) {
        self.cancelCurrentRecord = NO;
        NSLog(@"取消錄制!");
    }
    else{
        if (self.delegate&&[self.delegate respondsToSelector:@selector(audioRecorderDidFinishRecording:successfullyFlag:)]) {
            [self.delegate audioRecorderDidFinishRecording:recorder successfullyFlag:flag];
        }
        NSLog(@"錄制完成!");
    }
}
// !!!: 播放語音代理事件
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{
    if (self.delegate&&[self.delegate respondsToSelector:@selector(audioPlayerDidFinishPlaying:successfully:)]) {
        [self.delegate audioPlayerDidFinishPlaying:player successfully:flag];
    }
    NSLog(@"播放完成!");
}


#pragma mark - <************************** 私有方法 **************************>
// !!!: 獲取時(shí)刻名稱
-(NSString*)getDateString{
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
    NSInteger unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitWeekday |
    NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
    NSDateComponents *comps  = [calendar components:unitFlags fromDate:[NSDate date]];
    NSInteger year = [comps year];
    NSInteger month = [comps month];
    NSInteger day = [comps day];
    NSInteger hour = [comps hour];
    NSInteger min = [comps minute];
    NSInteger sec = [comps second];
    NSString* formatString = @"%d%02d%02d%02d%02d%02d";
    return [NSString stringWithFormat:formatString, year, month, day, hour, min, sec];
}

// !!!: 錄音聲波狀態(tài)設(shè)置
-(void)powerChange{
    [self.audioRecorder updateMeters];//更新測(cè)量值
    float power = [self.audioRecorder averagePowerForChannel:0];//取得第一個(gè)通道的音頻,注意音頻強(qiáng)度范圍時(shí)-160到0
    CGFloat progress = power+160.0;
    NSLog(@"音頻強(qiáng)度:%f",power);
    if (self.delegate&&[self.delegate respondsToSelector:@selector(audioPowerChange:)]) {
        [self.delegate audioPowerChange:progress];
    }
}

-(void)dealloc{
    [self removeAllAudioFile];
}

@end


[TOC]

補(bǔ)充:音頻隊(duì)列服務(wù)

音頻隊(duì)列服務(wù)Audio Queue Services屬于AudioToolbox框架,使用音頻隊(duì)列服務(wù)可以做到音頻流式播放和錄制

首先看一下錄音音頻服務(wù)隊(duì)列:

錄音音頻服務(wù)隊(duì)列

一個(gè)音頻服務(wù)隊(duì)列Audio Queue有三部分組成:

三個(gè)緩沖器Buffers:每個(gè)緩沖器都是一個(gè)存儲(chǔ)音頻數(shù)據(jù)的臨時(shí)倉庫。

一個(gè)緩沖隊(duì)列Buffer Queue:一個(gè)包含音頻緩沖器的有序隊(duì)列。

一個(gè)回調(diào)Callback:一個(gè)自定義的隊(duì)列回調(diào)函數(shù)。

聲音通過輸入設(shè)備進(jìn)入緩沖隊(duì)列中,首先填充第一個(gè)緩沖器;當(dāng)?shù)谝粋€(gè)緩沖器填充滿之后自動(dòng)填充下一個(gè)緩沖器,同時(shí)會(huì)調(diào)用回調(diào)函數(shù);在回調(diào)函數(shù)中需要將緩沖器中的音頻數(shù)據(jù)寫入磁盤,同時(shí)將緩沖器放回到緩沖隊(duì)列中以便重用。下面是Apple官方關(guān)于音頻隊(duì)列服務(wù)的流程示意圖:

音頻隊(duì)列服務(wù)的流程示意圖

類似的,看一下音頻播放緩沖隊(duì)列,其組成部分和錄音緩沖隊(duì)列類似。

音頻播放緩沖隊(duì)列

但是在音頻播放緩沖隊(duì)列中,回調(diào)函數(shù)調(diào)用的時(shí)機(jī)不同于音頻錄制緩沖隊(duì)列,流程剛好相反。將音頻讀取到緩沖器中,一旦一個(gè)緩沖器填充滿之后就放到緩沖隊(duì)列中,然后繼續(xù)填充其他緩沖器;當(dāng)開始播放時(shí),則從第一個(gè)緩沖器中讀取音頻進(jìn)行播放;一旦播放完之后就會(huì)觸發(fā)回調(diào)函數(shù),開始播放下一個(gè)緩沖器中的音頻,同時(shí)填充第一個(gè)緩沖器放;填充滿之后再次放回到緩沖隊(duì)列。下面是詳細(xì)的流程:

這里寫圖片描述

當(dāng)然,要明白音頻隊(duì)列服務(wù)的原理并不難,問題是如何實(shí)現(xiàn)這個(gè)自定義的回調(diào)函數(shù),這其中我們有大量的工作要做,控制播放狀態(tài)、處理異常中斷、進(jìn)行音頻編碼等等。由于牽扯內(nèi)容過多,而且不是本文目的,如果以后有時(shí)間將另開一篇文章重點(diǎn)介紹,目前有很多第三方優(yōu)秀框架可以直接使用,例如AudioStreamer、FreeStreamer。(前者當(dāng)前只有非ARC版本)


[TOC]

參考地址

iOS開發(fā)系列--音頻播放、錄音、視頻播放、拍照、視頻錄制

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容