iOS中声音播放的各种方法总结
welv5511
8年前
<h3>前言</h3> <p>这两天禁(晋)烟(嫣)的秀恩爱,身为程序员的我们又被默默的送了一把狗粮,这段时间一直在忙公司项目,两个多月都没有写过文章了,今天闲来无事想把iOS中播放音乐(包括段音效)的部分拿出来总结一下。</p> <p>主要部分:</p> <p>1.音效的播放</p> <p>2.音乐的播放(本地, 网络)</p> <p>3.音频队列服务</p> <p>1.音效播放(AudioToolbox/AudioToolbox.h)</p> <p>音频文件必须打包成.caf、.aif、.wav中的一种(注意这是官方文档的说法,实际测试发现一些.mp3也可以播放)</p> <p>这个段音效播放不能大于30s,这个30s不是我说的,是苹果的API说的</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/74e78dfa32fbd802592938118a8e6cef.png"></p> <p style="text-align:center">AudioServices_h.png</p> <p>创建音效的ID,音效的播放和销毁都靠这个ID来执行</p> <p>AudioServicesCreateSystemSoundID(CFURLRef inFileURL, SystemSoundID* outSystemSoundID)</p> <p>播放音效</p> <p>AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID)</p> <p>iOS9以后可以用的,带有block回调的播放</p> <p>AudioServicesPlaySystemSoundWithCompletion(SystemSoundID inSystemSoundID, void (^__nullable inCompletionBlock)(void))</p> <p>带有震动的播放</p> <p>AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID)</p> <p>iOS9以后可以用的,带有block回调的播放</p> <p>AudioServicesPlayAlertSoundWithCompletion( SystemSoundID inSystemSoundID,void (^__nullable inCompletionBlock)(void))</p> <p>在iOS9之前,如何判断一个音效是否播放完成呢?(利用下面的方法)</p> <p>AudioServicesAddSystemSoundCompletion(SystemSoundID inSystemSoundID,CFRunLoopRef __nullable inRunLoop, CFStringRef __nullable inRunLoopMode,AudioServicesSystemSoundCompletionProc inCompletionRoutine,void * __nullable inClientData)</p> <p>销毁音效的播放</p> <p>AudioServicesDisposeSystemSoundID(SystemSoundID inSystemSoundID)</p> <p>下面对上面的方法的演示,播放一些音效, <strong>播放48s的mp3时会报错</strong></p> <pre> <code class="language-objectivec">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> <p>2.本地音乐播放</p> <p>AVAudioPlayer</p> <p>AVAudioPlayer是播放本地音乐最常到的,这个类对于大多数人来说应该很常用,这里不多说,说一下它的基本用法和代理的用法,直接上代码,代码注释很详细</p> <pre> <code class="language-objectivec">@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> <p>网络音乐播放(AVPlayer)</p> <p>AVPlayer是播放网络音乐和网络视频最常用到的,它可以自己缓存网络数据,然后播放,AVPlayer在播放视频时必须创建一个AVPlayerLayer用来展示视频,如果播放音乐,声音就不用创建这个对象。这里简单演示一下网络播放音乐</p> <p>1. 通过网络链接创建AVPlayerItem</p> <p>AVPlayerItem的初始化方法很多,我这里直接用 initWithURL: 这个方法创建</p> <pre> <code class="language-objectivec">- (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; }</code></pre> <p>2.实现KVO的方法,根据keyPath来判断观察的属性是哪一个</p> <pre> <code class="language-objectivec">- (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> <p>3.懒加载AVPlayer</p> <pre> <code class="language-objectivec">- (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; }</code></pre> <p>4.播放和暂停</p> <pre> <code class="language-objectivec">// 播放 - (IBAction)play:(id)sender { [self.player play]; } //暂停 - (IBAction)pause:(id)sender { [self.player pause]; }</code></pre> <p>5.下一首和上一首</p> <pre> <code class="language-objectivec">- (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> <p>6.控制播放进度,这个也有很多的方法,如果不是太精确,用 - (void)seekToTime:(CMTime)time: 这个方法就行,如果要精确的用这个 - (void)seekToTime:(CMTime)time toleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter</p> <pre> <code class="language-objectivec">- (IBAction)changeProgress:(UISlider *)sender { if (self.player.status == AVPlayerStatusReadyToPlay) { [self.player seekToTime:CMTimeMake(CMTimeGetSeconds(self.player.currentItem.duration) * sender.value, 1)]; } }</code></pre> <p>音频队列服务(Audio Queue Services)</p> <p>在AudioToolbox框架中的音频队列服务,是用来播放网络流媒体的一个框架,它完全可以做到 <strong> <em>音频播放和录制</em> </strong> ,一个音频服务队列有三个部分组成:</p> <p>1.三个缓冲器Buffers:没个缓冲器都是一个存储音频数据的临时仓库。</p> <p>2.一个缓冲队列Buffer Queue:一个包含音频缓冲器的有序队列。</p> <p>3.一个回调CallBack:一个自定义的队列回调函数。</p> <p>在音频播放缓冲队列中,将音频读取到缓冲器中,一旦一个缓冲器填充满之后就放到缓冲队列中,然后继续填充其他缓冲器;当开始播放时,则从第一个缓冲器中读取音频进行播放;一旦播放完之后就会触发回调函数,开始播放下一个缓冲器中的音频,同时填充第一个缓冲器放;填充满之后再次放回到缓冲队列。下面是 <a href="/misc/goto?guid=4959729346355194351" rel="nofollow,noindex">官方</a> 详细的流程:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/0471e94914de35e6fa3d790dec5cd1b0.png"></p> <p>Playback_Audio_Queues.png</p> <p>AudioQueue的工作大致流程:</p> <p>1.创建 AudioQueue ,创建 BufferArray 数组,用于存放 AudioQueueBufferRef</p> <p>2.通过 AudioQueueAllocateBuffer 创建 AudioQueueBufferRef 一般2-3个,放入到 BufferArray 数组中</p> <p>3.有数据时从 buffer 数组取出一个 buffer , memcpy 数据后用 AudioQueueEnqueueBuffer 方法把 buffer 插入 AudioQueue 中</p> <p>4. AudioQueue 中存在 Buffer 后,调用 AudioQueueStart 播放。(具体等到填入多少 buffer 后再播放可以自己控制,只要能保证播放不间断即可)</p> <p>5. AudioQueue 播放音乐后消耗了某个 buffer ,在另一个线程回调并送出该 buffe r,把 buffer 放回 BufferArray 供下一次使用</p> <p>6.返回步骤3继续循环直到播放结束</p> <p>常用API</p> <p>创建AudioQueue</p> <pre> <code class="language-objectivec">第一个参数表示需要播放的音频数据格式类型,是一个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> <p>创建Buffer</p> <pre> <code class="language-objectivec">第一个参数方法传入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> <p>释放buffer</p> <pre> <code class="language-objectivec">第一个参数AudioQueue实例 第二个参数指定的buffer extern OSStatus AudioQueueFreeBuffer( AudioQueueRef inAQ, AudioQueueBufferRef inBuffer)</code></pre> <p>插入buffer</p> <pre> <code class="language-objectivec">第一个参数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> <p>开始播放</p> <pre> <code class="language-objectivec">第一个参数AudioQueue实例 第二个参数播放时间,如果直接开始播放 传NULL extern OSStatus AudioQueueStart( AudioQueueRef inAQ, const AudioTimeStamp * __nullable inStartTime)</code></pre> <p>解码数据,不常用,调用开始播放会自动解码</p> <pre> <code class="language-objectivec">extern OSStatus AudioQueuePrime( AudioQueueRef inAQ, UInt32 inNumberOfFramesToPrepare, UInt32 * __nullable outNumberOfFramesPrepared)</code></pre> <p>停止播放</p> <pre> <code class="language-objectivec">第二个参数Bool值,控制是否立即停止,如果传false,会把Enqueue的所有buffer播放完成再停止 extern OSStatus AudioQueueStop( AudioQueueRef inAQ, Boolean inImmediate) __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);</code></pre> <p>暂停播放</p> <pre> <code class="language-objectivec">extern OSStatus AudioQueuePause( AudioQueueRef inAQ) __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);</code></pre> <p>重置解码器</p> <pre> <code class="language-objectivec">这个方法会播放完队列中的buffer后重置解码器,防止当前的解码器影响下一段音频,比如切换歌曲的时候,如果和AudioQueueStop(AQ,false) 一起使用并不会起效,因为Stop方法的false参数也会做同样的事情。 extern OSStatus AudioQueueFlush( AudioQueueRef inAQ) __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);</code></pre> <p>重置AudioQueue</p> <pre> <code class="language-objectivec">重置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);</code></pre> <p>获取播放时间</p> <pre> <code class="language-objectivec">调用时传入AudioTimeStamp,从这个结构体当中获取播放时间 extern OSStatus AudioQueueGetCurrentTime( AudioQueueRef inAQ, AudioQueueTimelineRef __nullable inTimeline, AudioTimeStamp * __nullable outTimeStamp, Boolean * __nullable outTimelineDiscontinuity) __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);</code></pre> <p>销毁AudioQueue</p> <pre> <code class="language-objectivec">参数的意义基本和AudioQueueStop一样 extern OSStatus AudioQueueDispose( AudioQueueRef inAQ, Boolean inImmediate) __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);</code></pre> <p>AudioQueue参数</p> <pre> <code class="language-objectivec">AudioQueueGetParameter AudioQueueSetParameter 参数列表 CF_ENUM(AudioQueueParameterID) { kAudioQueueParam_Volume = 1, kAudioQueueParam_PlayRate = 2, kAudioQueueParam_Pitch = 3, kAudioQueueParam_VolumeRampTime = 4, kAudioQueueParam_Pan = 13 };</code></pre> <p>AudioQueue属性</p> <pre> <code class="language-objectivec">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> <p>监听属相变化相关方法</p> <pre> <code class="language-objectivec">AudioQueueAddPropertyListener AudioQueueRemovePropertyListener</code></pre> <p>总结:</p> <p>这里说的东西都比(能)较(力)基(有)础(限),其实AudioQueue的功能还有很多,如果大家想去研究比较细致的AudioQueue的使用,这里给大家推荐两个github地址,一个是 AudioStreamer ,一个是 FreeStreamer ,这里的两个播放都是使用AudioQueue实现的。</p> <p> </p> <p>来自:http://www.jianshu.com/p/548afbe49e67</p> <p> </p>