iOS中聲音播放的各種方法總結

welv5511 8年前發布 | 18K 次閱讀 iOS開發 移動開發

前言

這兩天禁(晉)煙(嫣)的秀恩愛,身為程序員的我們又被默默的送了一把狗糧,這段時間一直在忙公司項目,兩個多月都沒有寫過文章了,今天閑來無事想把iOS中播放音樂(包括段音效)的部分拿出來總結一下。

主要部分:

1.音效的播放

2.音樂的播放(本地, 網絡)

3.音頻隊列服務

1.音效播放(AudioToolbox/AudioToolbox.h)

音頻文件必須打包成.caf、.aif、.wav中的一種(注意這是官方文檔的說法,實際測試發現一些.mp3也可以播放)

這個段音效播放不能大于30s,這個30s不是我說的,是蘋果的API說的

AudioServices_h.png

創建音效的ID,音效的播放和銷毀都靠這個ID來執行

AudioServicesCreateSystemSoundID(CFURLRef inFileURL, SystemSoundID* outSystemSoundID)

播放音效

AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID)

iOS9以后可以用的,帶有block回調的播放

AudioServicesPlaySystemSoundWithCompletion(SystemSoundID inSystemSoundID, void (^__nullable inCompletionBlock)(void))

帶有震動的播放

AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID)

iOS9以后可以用的,帶有block回調的播放

AudioServicesPlayAlertSoundWithCompletion( SystemSoundID inSystemSoundID,void (^__nullable inCompletionBlock)(void))

在iOS9之前,如何判斷一個音效是否播放完成呢?(利用下面的方法)

AudioServicesAddSystemSoundCompletion(SystemSoundID inSystemSoundID,CFRunLoopRef __nullable inRunLoop, CFStringRef __nullable inRunLoopMode,AudioServicesSystemSoundCompletionProc inCompletionRoutine,void * __nullable inClientData)

銷毀音效的播放

AudioServicesDisposeSystemSoundID(SystemSoundID inSystemSoundID)

下面對上面的方法的演示,播放一些音效, 播放48s的mp3時會報錯

static SystemSoundID soundID = 0;

  • (IBAction)play:(id)sender {

// NSString str = [[NSBundle mainBundle] pathForResource:@"vcyber_waiting" ofType:@"wav"]; NSString str = [[NSBundle mainBundle] pathForResource:@"28s" ofType:@"mp3"]; // NSString str = [[NSBundle mainBundle] pathForResource:@"48s" ofType:@"mp3"]; NSURL url = [NSURL fileURLWithPath:str];

AudioServicesCreateSystemSoundID((__bridge CFURLRef _Nonnull)(url), &soundID);

// // AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, soundCompleteCallBack, NULL); //
// //AudioServicesPlaySystemSound(soundID); //
// AudioServicesPlayAlertSound(soundID);

// AudioServicesPlaySystemSoundWithCompletion(soundID, ^{ // NSLog(@"播放完成"); // AudioServicesDisposeSystemSoundID(soundID); // });

AudioServicesPlayAlertSoundWithCompletion(soundID, ^{
    NSLog(@"播放完成");
});

}

void soundCompleteCallBack(SystemSoundID soundID, void * clientDate) { NSLog(@"播放完成"); AudioServicesDisposeSystemSoundID(soundID); }

  • (IBAction)stop:(id)sender { AudioServicesDisposeSystemSoundID(soundID); }</code></pre>

    2.本地音樂播放

    AVAudioPlayer

    AVAudioPlayer是播放本地音樂最常到的,這個類對于大多數人來說應該很常用,這里不多說,說一下它的基本用法和代理的用法,直接上代碼,代碼注釋很詳細

    @interface LocalMusicViewController ()<AVAudioPlayerDelegate>

/* 播放器 / @property (nonatomic, strong) AVAudioPlayer *player;

/* 播放進度條 / @property (weak, nonatomic) IBOutlet UIProgressView *progress;

/* 改變播放進度滑塊 / @property (weak, nonatomic) IBOutlet UISlider *progressSlide;

/* 改變聲音滑塊 / @property (weak, nonatomic) IBOutlet UISlider *volum;

/* 改變進度條滑塊顯示的定時器 / @property (nonatomic, strong) NSTimer *timer;

@end

@implementation LocalMusicViewController

  • (void)viewDidLoad { [super viewDidLoad];

    NSError err; NSURL url = [[NSBundle mainBundle] URLForResource:@"1" withExtension:@"mp3"]; // 初始化播放器 _player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&err]; self.volum.value = 0.5; // 設置播放器聲音 _player.volume = self.volum.value; // 設置代理 _player.delegate = self; // 設置播放速率 _player.rate = 1.0; // 設置播放次數 負數代表無限循環 _player.numberOfLoops = -1; // 準備播放 [_player prepareToPlay]; self.progress.progress = 0; self.progressSlide.value = 0; _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(change) userInfo:nil repeats:YES];

}

  • (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated];

}

  • (void)change { self.progress.progress = _player.currentTime / _player.duration; }

  • (IBAction)progressChange:(UISlider )sender { // 改變當前的播放進度 _player.currentTime = sender.value _player.duration; self.progress.progress = sender.value;

}

  • (IBAction)volumChange:(UISlider *)sender { // 改變聲音大小 _player.volume = sender.value; }

  • (IBAction)player:(id)sender { // 開始播放 [_player play]; }

  • (IBAction)stop:(id)sender { // 暫停播放 [_player stop]; }

pragma mark --AVAudioPlayerDelegate

/* 完成播放, 但是在打斷播放和暫停、停止不會調用 /

  • (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {

}

/* 播放過程中解碼錯誤時會調用 /

  • (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer )player error:(NSError __nullable)error {

}

/** 播放過程被打斷

*/

  • (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player NS_DEPRECATED_IOS(2_2, 8_0) {

}

/* 打斷結束/

  • (void)audioPlayerEndInterruption:(AVAudioPlayer *)player withOptions:(NSUInteger)flags NS_DEPRECATED_IOS(6_0, 8_0) {

}

/** 打斷結束

*/

  • (void)audioPlayerEndInterruption:(AVAudioPlayer *)player withFlags:(NSUInteger)flags NS_DEPRECATED_IOS(4_0, 6_0) {

}

/* 這個方法被上面的方法代替了 /

  • (void)audioPlayerEndInterruption:(AVAudioPlayer *)player NS_DEPRECATED_IOS(2_2, 6_0) {

}</code></pre>

網絡音樂播放(AVPlayer)

AVPlayer是播放網絡音樂和網絡視頻最常用到的,它可以自己緩存網絡數據,然后播放,AVPlayer在播放視頻時必須創建一個AVPlayerLayer用來展示視頻,如果播放音樂,聲音就不用創建這個對象。這里簡單演示一下網絡播放音樂

1. 通過網絡鏈接創建AVPlayerItem

AVPlayerItem的初始化方法很多,我這里直接用 initWithURL: 這個方法創建

- (AVPlayerItem *)getItemWithIndex:(NSInteger)index {
    NSURL *url = [NSURL URLWithString:self.musicArray[index]];
    AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:url];
    //KVO監聽播放狀態
    [item addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    //KVO監聽緩存大小
    [item addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    //通知監聽item播放完畢
     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playOver:) name:AVPlayerItemDidPlayToEndTimeNotification object:item];
    return item;
}

2.實現KVO的方法,根據keyPath來判斷觀察的屬性是哪一個

- (void)observeValueForKeyPath:(NSString )keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> )change context:(void *)context {

AVPlayerItem *item = object;

if ([keyPath isEqualToString:@"status"]) {
    switch (self.player.status) {
        case AVPlayerStatusUnknown:
            NSLog(@"未知狀態,不能播放");
            break;
        case AVPlayerStatusReadyToPlay:
            NSLog(@"準備完畢,可以播放");
            break;
        case AVPlayerStatusFailed:
            NSLog(@"加載失敗, 網絡相關問題");
            break;

        default:
            break;
    }
}

if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
    NSArray *array = item.loadedTimeRanges;
    //本次緩存的時間
    CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];
    NSTimeInterval totalBufferTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration); //緩存的總長度
    self.bufferProgress.progress = totalBufferTime / CMTimeGetSeconds(item.duration);
}

}</code></pre>

3.懶加載AVPlayer

- (AVPlayer *)player {
    if (!_player) {
//        根據鏈接數組獲取第一個播放的item, 用這個item來初始化AVPlayer
        AVPlayerItem *item = [self getItemWithIndex:self.currentIndex];
//        初始化AVPlayer
        _player = [[AVPlayer alloc] initWithPlayerItem:item];
        __weak typeof(self)weakSelf = self;
//        監聽播放的進度的方法,addPeriodicTime: ObserverForInterval: usingBlock:
        /*
         DMTime 每到一定的時間會回調一次,包括開始和結束播放
         block回調,用來獲取當前播放時長
         return 返回一個觀察對象,當播放完畢時需要,移除這個觀察
         */
        _timeObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
            float current = CMTimeGetSeconds(time);
            if (current) {
                [weakSelf.progressView setProgress:current / CMTimeGetSeconds(item.duration) animated:YES];
                weakSelf.progressSlide.value = current / CMTimeGetSeconds(item.duration);
            }
        }];
    }
    return _player;
}

4.播放和暫停

//  播放

  • (IBAction)play:(id)sender { [self.player play]; }

//暫停

  • (IBAction)pause:(id)sender { [self.player pause]; }</code></pre>

    5.下一首和上一首

    - (IBAction)next:(UIButton *)sender {
      [self removeObserver];
     self.currentIndex ++;
      if (self.currentIndex >= self.musicArray.count) {

      self.currentIndex = 0;
    

    } // 這個方法是用一個item取代當前的item [self.player replaceCurrentItemWithPlayerItem:[self getItemWithIndex:self.currentIndex]]; [self.player play]; }

  • (IBAction)last:(UIButton *)sender { [self removeObserver]; self.currentIndex --; if (self.currentIndex < 0) {

      self.currentIndex = 0;
    

    } // 這個方法是用一個item取代當前的item [self.player replaceCurrentItemWithPlayerItem:[self getItemWithIndex:self.currentIndex]]; [self.player play]; }

// 在播放另一個時,要移除當前item的觀察者,還要移除item播放完成的通知

  • (void)removeObserver { [self.player.currentItem removeObserver:self forKeyPath:@"status"]; [self.player.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"]; [[NSNotificationCenter defaultCenter] removeObserver:self]; }</code></pre>

    6.控制播放進度,這個也有很多的方法,如果不是太精確,用 - (void)seekToTime:(CMTime)time: 這個方法就行,如果要精確的用這個 - (void)seekToTime:(CMTime)time toleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter

    - (IBAction)changeProgress:(UISlider *)sender {
      if (self.player.status == AVPlayerStatusReadyToPlay) {
    
      [self.player seekToTime:CMTimeMake(CMTimeGetSeconds(self.player.currentItem.duration) * sender.value, 1)];
    
    } }</code></pre>

    音頻隊列服務(Audio Queue Services)

    在AudioToolbox框架中的音頻隊列服務,是用來播放網絡流媒體的一個框架,它完全可以做到 音頻播放和錄制 ,一個音頻服務隊列有三個部分組成:

    1.三個緩沖器Buffers:沒個緩沖器都是一個存儲音頻數據的臨時倉庫。

    2.一個緩沖隊列Buffer Queue:一個包含音頻緩沖器的有序隊列。

    3.一個回調CallBack:一個自定義的隊列回調函數。

    在音頻播放緩沖隊列中,將音頻讀取到緩沖器中,一旦一個緩沖器填充滿之后就放到緩沖隊列中,然后繼續填充其他緩沖器;當開始播放時,則從第一個緩沖器中讀取音頻進行播放;一旦播放完之后就會觸發回調函數,開始播放下一個緩沖器中的音頻,同時填充第一個緩沖器放;填充滿之后再次放回到緩沖隊列。下面是 官方 詳細的流程:

    Playback_Audio_Queues.png

    AudioQueue的工作大致流程:

    1.創建 AudioQueue ,創建 BufferArray 數組,用于存放 AudioQueueBufferRef

    2.通過 AudioQueueAllocateBuffer 創建 AudioQueueBufferRef 一般2-3個,放入到 BufferArray 數組中

    3.有數據時從 buffer 數組取出一個 buffer , memcpy 數據后用 AudioQueueEnqueueBuffer 方法把 buffer 插入 AudioQueue 中

    4. AudioQueue 中存在 Buffer 后,調用 AudioQueueStart 播放。(具體等到填入多少 buffer 后再播放可以自己控制,只要能保證播放不間斷即可)

    5. AudioQueue 播放音樂后消耗了某個 buffer ,在另一個線程回調并送出該 buffe r,把 buffer 放回 BufferArray 供下一次使用

    6.返回步驟3繼續循環直到播放結束

    常用API

    創建AudioQueue

    第一個參數表示需要播放的音頻數據格式類型,是一個AudioStreamBasicDescription對象,是使用AudioFileStream或者AudioFile解析出來的數據格式信息;
    第二個參數AudioQueueOutputCallback是某塊Buffer被使用之后的回調;
    第三個參數為上下文對象;
    第四個參數inCallbackRunLoop為AudioQueueOutputCallback需要在的哪個RunLoop上被回調,如果傳入NULL的話就會再AudioQueue的內部RunLoop中被回調,所以一般傳NULL就可以了;
    第五個參數inCallbackRunLoopMode為RunLoop模式,如果傳入NULL就相當于kCFRunLoopCommonModes,也傳NULL就可以了;
    第六個參數inFlags是保留字段,目前沒作用,傳0;
    第七個參數,返回生成的AudioQueue實例;
    返回值用來判斷是否成功創建(OSStatus == noErr)。
    extern OSStatus
    AudioQueueNewOutput( const AudioStreamBasicDescription *inFormat,
                    AudioQueueOutputCallback        inCallbackProc,
                   void * __nullable               inUserData,
                     CFRunLoopRef __nullable         inCallbackRunLoop,
                     CFStringRef __nullable          inCallbackRunLoopMode,
                     UInt32                          inFlags,
                    AudioQueueRef __nullable * __nonnull outAQ)          

參數和上面基本相同,只是把RunLoop換成了dispatch queue AudioQueueNewOutputWithDispatchQueue(AudioQueueRef nullable * nonnull outAQ, const AudioStreamBasicDescription inFormat, UInt32 inFlags, dispatch_queue_t inCallbackDispatchQueue, AudioQueueOutputCallbackBlock inCallbackBlock)</code></pre>

創建Buffer

第一個參數方法傳入AudioQueue實例
第二個參數Buffer大小
第三個傳出的BufferArray實例;
extern OSStatus
AudioQueueAllocateBuffer(AudioQueueRef    inAQ,
                          UInt32    inBufferByteSize,
                          AudioQueueBufferRef __nullable  __nonnull outBuffer)

比上面的方法多了一個inNumberPacketDescriptions,這個參數可以指定生成的Buffer中PacketDescriptions的個數 extern OSStatus AudioQueueAllocateBufferWithPacketDescriptions( AudioQueueRef inAQ, UInt32 inBufferByteSize, UInt32 inNumberPacketDescriptions, AudioQueueBufferRef nullable * nonnull outBuffer)</code></pre>

釋放buffer

第一個參數AudioQueue實例
第二個參數指定的buffer
extern OSStatus
AudioQueueFreeBuffer(               AudioQueueRef           inAQ,
                                    AudioQueueBufferRef     inBuffer)

插入buffer

第一個參數AudioQueue實例
第二個參數指定的Buffer
第三個參數數據包的個數
第四個參數數據包描述
extern OSStatus
AudioQueueEnqueueBuffer(            AudioQueueRef                       inAQ,
                                    AudioQueueBufferRef                 inBuffer,
                                    UInt32                              inNumPacketDescs,
                                    const AudioStreamPacketDescription * __nullable inPacketDescs)

上面的方法基本滿足要求,這個方法對插入的buffer進行額外的更多的操作 extern OSStatus AudioQueueEnqueueBufferWithParameters( AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, UInt32 inNumPacketDescs, const AudioStreamPacketDescription __nullable inPacketDescs, UInt32 inTrimFramesAtStart, UInt32 inTrimFramesAtEnd, UInt32 inNumParamValues, const AudioQueueParameterEvent nullable inParamValues, const AudioTimeStamp * nullable inStartTime, AudioTimeStamp nullable outActualStartTime) OSX_AVAILABLE_STARTING(MAC_10_5,IPHONE_2_0);</code></pre>

開始播放

第一個參數AudioQueue實例
第二個參數播放時間,如果直接開始播放 傳NULL
extern OSStatus
AudioQueueStart(                    AudioQueueRef                     inAQ,
                                    const AudioTimeStamp  nullable inStartTime)</code></pre> 
  

解碼數據,不常用,調用開始播放會自動解碼

extern OSStatus
AudioQueuePrime(                    AudioQueueRef           inAQ,
                                    UInt32                  inNumberOfFramesToPrepare,
                                    UInt32 * nullable     outNumberOfFramesPrepared)</code></pre> 
  

停止播放

第二個參數Bool值,控制是否立即停止,如果傳false,會把Enqueue的所有buffer播放完成再停止
extern OSStatus
AudioQueueStop(                     AudioQueueRef           inAQ,
                                    Boolean                 inImmediate)            __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

暫停播放

extern OSStatus
AudioQueuePause(                    AudioQueueRef           inAQ)       __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

重置解碼器

這個方法會播放完隊列中的buffer后重置解碼器,防止當前的解碼器影響下一段音頻,比如切換歌曲的時候,如果和AudioQueueStop(AQ,false)
一起使用并不會起效,因為Stop方法的false參數也會做同樣的事情。
extern OSStatus
AudioQueueFlush(                    AudioQueueRef           inAQ)            __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

重置AudioQueue

重置AudioQueue會清除所有已經Enqueue的buffer,并觸發AudioQueueOutputCallback,調用AudioQueueStop方法時同樣會觸發該方法。這個方法的直接調用一般在seek時使用,用來清除殘留的buffer(seek時還有一種做法是先AudioQueueStop
,等seek完成后重新start)。
extern OSStatus
AudioQueueReset(                    AudioQueueRef           inAQ)            __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

獲取播放時間

調用時傳入AudioTimeStamp,從這個結構體當中獲取播放時間
extern OSStatus
AudioQueueGetCurrentTime(           AudioQueueRef                    inAQ,
                                    AudioQueueTimelineRef __nullable inTimeline,
                                    AudioTimeStamp * __nullable      outTimeStamp,
                                    Boolean * __nullable             outTimelineDiscontinuity)       __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

銷毀AudioQueue

參數的意義基本和AudioQueueStop一樣
extern OSStatus
AudioQueueDispose(                  AudioQueueRef           inAQ, 
                                    Boolean                 inImmediate)            __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);

AudioQueue參數

AudioQueueGetParameter
AudioQueueSetParameter
參數列表
CF_ENUM(AudioQueueParameterID)
{
    kAudioQueueParam_Volume         = 1,
    kAudioQueueParam_PlayRate       = 2,
    kAudioQueueParam_Pitch          = 3,
    kAudioQueueParam_VolumeRampTime = 4,
    kAudioQueueParam_Pan            = 13
};

AudioQueue屬性

AudioQueueGetPropertySize
AudioQueueGetProperty
AudioQueueSetProperty
屬性列表
CF_ENUM(AudioQueuePropertyID) {
    kAudioQueueProperty_IsRunning               = 'aqrn',       // value is UInt32

kAudioQueueDeviceProperty_SampleRate        = 'aqsr',       // value is Float64
kAudioQueueDeviceProperty_NumberChannels    = 'aqdc',       // value is UInt32
kAudioQueueProperty_CurrentDevice           = 'aqcd',       // value is CFStringRef

kAudioQueueProperty_MagicCookie             = 'aqmc',       // value is void*
kAudioQueueProperty_MaximumOutputPacketSize = 'xops',       // value is UInt32
kAudioQueueProperty_StreamDescription       = 'aqft',       // value is AudioStreamBasicDescription

kAudioQueueProperty_ChannelLayout           = 'aqcl',       // value is AudioChannelLayout
kAudioQueueProperty_EnableLevelMetering     = 'aqme',       // value is UInt32
kAudioQueueProperty_CurrentLevelMeter       = 'aqmv',       // value is array of AudioQueueLevelMeterState, 1 per channel
kAudioQueueProperty_CurrentLevelMeterDB     = 'aqmd',       // value is array of AudioQueueLevelMeterState, 1 per channel

kAudioQueueProperty_DecodeBufferSizeFrames  = 'dcbf',       // value is UInt32
kAudioQueueProperty_ConverterError          = 'qcve',       // value is UInt32

kAudioQueueProperty_EnableTimePitch         = 'q_tp',       // value is UInt32, 0/1
kAudioQueueProperty_TimePitchAlgorithm      = 'qtpa',       // value is UInt32. See values below.
kAudioQueueProperty_TimePitchBypass         = 'qtpb',       // value is UInt32, 1=bypassed

};</code></pre>

監聽屬相變化相關方法

AudioQueueAddPropertyListener
AudioQueueRemovePropertyListener

總結:

這里說的東西都比(能)較(力)基(有)礎(限),其實AudioQueue的功能還有很多,如果大家想去研究比較細致的AudioQueue的使用,這里給大家推薦兩個github地址,一個是 AudioStreamer ,一個是 FreeStreamer ,這里的兩個播放都是使用AudioQueue實現的。

 

來自:http://www.jianshu.com/p/548afbe49e67

 

 本文由用戶 welv5511 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!