美文网首页iOS开发ios专题iOS入的那些坑
音频录制和播放的详细分析及实现

音频录制和播放的详细分析及实现

作者: gitKong | 来源:发表于2017-04-13 16:51 被阅读1243次

    一、前言

    • 这段时间确实忙,好久没更新文章了,实在抱歉!中间换了工作环境,暂时稳定下来了,抽空整理了之前看的资料,具体分析了音频录制以及播放的实现,并封装了一个录音和播放音频的小轮子,欢迎star。

    • 至于为什么写这个,因为我确实是对音视频方面比较感兴趣!而且之前也在看这部分的文档,我目的是要逐步研究音视频底层的东西,而 AudioToolboxVideoToolbox 就是底层实现了,在此之前,我觉得熟悉上层接口是很有必要的

    图片来源于官方文档
    • 那么本文研究的都是 AVFoundationAVKitMediaPlayer 这些上层接口类

    二、上层接口以及基本用法

    注意:此处只针对录音和播放的上层相关核心类,稍作解释,整理了两张图说明,图片尺寸有点大,建议新标签打开或者另存为打开。

    核心类 核心类的简单介绍

    录音

    • 1、AVAudioRecorder 文档详细介绍了相关API以及使用,通过这个类,可简单实现音频录制、暂停、停止、指定时间录制等,缺点是:没办法设置录音时长,无法监听录音各种状态;当然,本框架中已经实现,可仔细阅读源码

    • 2、Audio Queue Services 文档描述也是相当详细了,暂时没仔细研究,本框架中暂时没使用


    播放

    • 1、System Sound Services 提供C接口实现,可播放本地bundle的短暂音效(30秒以内)和震动效果,会马上播放,不支持多个同时播放,可通过 OSStatus 判断是否操作成功

      摘自文档:
      System Sound Services provides a C interface for playing short sounds and for invoking vibration on iOS devices that support vibration.You can use System Sound Services to play short (30 seconds or shorter) sounds.

      用法如下:

      // kSystemSoundID_Vibrate 震动 iPod 无效
        // kSystemSoundID_UserPreferredAlert 播放用户在系统设置的音效
        SystemSoundID soundID;
        // 必须是bundle文件
        NSURL *soundUrl = [[NSBundle mainBundle] URLForResource:@"sound" withExtension:@"wav"];
        // 创建
        AudioServicesCreateSystemSoundID((__bridge CFURLRef)soundUrl, &soundID);
        // 播放
        //AudioServicesPlayAlertSound(soundID);// 提醒音效
        AudioServicesPlayAlertSoundWithCompletion(soundID, ^{
            
        });
        //AudioServicesPlaySystemSound(soundID);// 普通系统声音
        //AudioServicesPlaySystemSoundWithCompletion(soundID, ^{
            
        //});
        
        // 停止
        //AudioServicesDisposeSystemSoundID(soundID);
        
        // 播放完毕回调
        AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, (void *)finishPlayCallback, NULL);
      
      
      /**
      

    回调

    @param soundID ID
    */
    static void finishPlayCallback(SystemSoundID soundID){
    NSLog(@"stop");
    }

    
    ---
    
    - **2、[AVAudioPlayer](https://developer.apple.com/reference/avfoundation/avaudioplayer),可实现播放本地音频文件或缓存data音频数据、延时播放、循环播放、可同时播放多个音频、可控制播放优先级、播放速度等;支持播放iOS或macOS所支持的所有音频格式,具体API官方文档有详细介绍**
    
    用法如下:
    
    

    NSURL* fileURL = [[NSBundle mainBundle] URLForResource:@"gitKong"withExtension:@"mp3"];
    NSError *error = nil;
    // 创建
    AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error];
    if (error) {
    NSLog(@"error:%@",error);
    }
    // 音频时长
    //player.duration
    // 循环次数,-1为无限循环
    player.numberOfLoops = -1;
    // 设置代理,监听播放完成、失败、被打断
    player.delegate = self;

    // 准备播放、开始、暂停、结束
    [player prepareToPlay];
    [player play];
    [player pause];
    [player stop];
    
    
    ---
    
    - 3、[MPMusicPlayerController](https://developer.apple.com/reference/mediaplayer/mpmusicplayercontroller),在 `MediaPlayer` 框架中,需要配合 `MPMediaPickerController` 使用,支持列表播放,由于系统封装好,因此定制性不强,MPMusicPlayerController有两种播放器:applicationMusicPlayer和systemMusicPlayer,前者在应用退出后音乐播放会自动停止,后者在应用停止后不会退出播放状态。 具体用法可参考官方文档
    
    用法:可参考:[Paddy的一篇博客](http://blog.csdn.net/u013346305/article/details/47280731#t8),页内搜 "**扩展--播放音乐库中的音乐**"
    
    ---
    
    - **4、[Audio Queue Services](https://developer.apple.com/reference/audiotoolbox/audio_queue_services)**,支持网络流媒体播放、支持常用格式、支持列表播放、定制性十分强,因为功能都需要自行实现,作者暂时没在此框架中使用,其实在此之前,本人已经翻译了 `Audio Queue Services` 的 `Playing Audio`,推荐先看看[Audio Queue Services 解读之 Playing Audio(上)](http://www.jianshu.com/p/d2ef4d15356c)、[Audio Queue Services 解读之 Playing Audio(下)](http://www.jianshu.com/p/5d1f466afab1),大家可对比官方文档查阅
    
    ---
    
    - **5、[MPMoviePlayerController](https://developer.apple.com/reference/mediaplayer/mpmovieplayercontroller)**,注意:这个类在iOS9.0之后就被废弃了,系统建议使用 `AVPictureInPictureController` 或 `AVPlayerViewController` 代替,系统已经高度封装了,因此定制性不强,但支持播放音频,因此也可以使用此类来进行音频播放,至于view就可以不显示出来
    
    > 摘自官方文档:
    The MPMoviePlayerController class is formally deprecated in iOS 9. (The [MPMoviePlayerViewController
    ](https://developer.apple.com/reference/mediaplayer/mpmovieplayerviewcontroller) class is also formally deprecated.) To play video content in iOS 9 and later, instead use the [AVPictureInPictureController
    ](https://developer.apple.com/reference/avkit/avpictureinpicturecontroller) or [AVPlayerViewController
    ](https://developer.apple.com/reference/avkit/avplayerviewcontroller) class from the AVKit framework, or the [WKWebView
    ](https://developer.apple.com/reference/webkit/wkwebview) class from WebKit.
    
    用法如下:(**代码摘自官方文档**)
    
    

    MPMoviePlayerController *player =
    [[MPMoviePlayerController alloc] initWithContentURL: myURL];
    [player prepareToPlay];
    [player.view setFrame: myView.bounds]; // player's frame must match parent's
    [myView addSubview: player.view];
    // ...
    [player play];

    
    具体范例可参考:[Paddy的一篇博客](http://blog.csdn.net/u013346305/article/details/47280731#t8),页内搜 "** MPMoviePlayerController**"
    
    ---
    
    - **6、[MPMoviePlayerViewController](https://developer.apple.com/reference/mediaplayer/mpmovieplayerviewcontroller)**,和 `MPMoviePlayerController ` 一样,iOS9.0之后就被废弃了,具体用法和 `MPMoviePlayerController ` 类似,这里不作过多分析,用到的朋友可先查阅官方文档。
    
    具体范例可参考:[Paddy的一篇博客](http://blog.csdn.net/u013346305/article/details/47280731#t8),页内搜 "** MPMoviePlayerViewController**"
    
    ---
    
    - **7、[AVPictureInPictureController](https://developer.apple.com/reference/avkit/avpictureinpicturecontroller)**,看命名就知道,这个类就是专门处理画中画的,目前暂时只支持iPad,在此就不作过多分析,有需求的朋友自行查阅官方文档。
    
    > 摘自官方文档:
    An AVPictureInPictureController lets you respond to user-initiated playback of video in a floating, resizable window on iPad.
    
    ---
    
    - **8、[AVPlayerViewController](https://developer.apple.com/reference/avkit/avplayerviewcontroller)**,系统针对 `AVPlayer` 封装的一个视频播放器,当然支持播放音频,支持 .mov、.mp4、.mpv、.3gp 格式,其他格式没一一考究,按理来说,`AVPlayerViewController` 是对 `AVPlayer` 进行封装的,而 `AVPlayer` 支持多种格式,那么 `AVPlayerViewController` 也应该支持,具体就交给各位去检验一下,有问题@我一下喔,此类具体用法在此也不作过多分析
    
    ---
    
    - **9、[AVPlayer](https://developer.apple.com/reference/avfoundation/avplayer)**,通过上表可以知道, `AVPlayer` 功能确实已经足够强大,iOS4.0以上都支持,支持网络流媒体、支持多种常用类型,可在 `AVMediaFormat.h` 通过 类 `AVURLAsset` 调用 [audiovisualTypes](https://developer.apple.com/reference/avfoundation/avurlasset/1386800-audiovisualtypes?language=objc) 方法返回支持类型;[stack over flow](http://stackoverflow.com/questions/21879981/avfoundation-avplayer-supported-formats-no-vob-or-mpg-containers) 有提供答案,可参考。
    
    下面介绍一下相关类:
    
    -  [AVAsset
    ](https://developer.apple.com/reference/avfoundation/avasset):是一个抽象类,不能直接使用,主要用于准确获取多媒体信息,例如媒体总时长duration、音量、播放速度等。
    
    -  [AVURLAsset
    ](https://developer.apple.com/reference/avfoundation/avurlasset):`AVAsset` 的子类,可根据一个本地或网络URL路径创建一个包含多媒体信息的 `AVURLAsset` 对象
    
    -  [AVAssetTrack
    ](https://developer.apple.com/reference/avfoundation/avassettrack):传输轨道,一般播放视频至少两个轨道,一个播放声音,一个播放画面;而 `AVAssetTrack` 就是专门管理这些轨道的,可以查看到媒体类型、轨道ID、采样数据的长度等
    
    -  [AVPlayerItem
    ](https://developer.apple.com/reference/avfoundation/avplayeritem) :媒体资源管理对象,管理视频或音频的一些基本信息和状态,通过KVO方便监听播放状态、缓冲进度等信息,一个 `AVPlayerItem` 对应一个视频或音频,可通过 `NSURL` 或 `AVAsset` 创建
    
    -  [AVQueuePlayer
    ](https://developer.apple.com/reference/avfoundation/avqueueplayer):`AVPlayer` 的子类,通过此类可以方便实现多媒体列表播放、可切换下一条播放
    
    ---
    
    # 三、需求是什么
    
    - ####**音频录制**
    
    > 由于不需要实时录制传输,只要录制到指定本地文件,保证格式可用,而且还没仔细研究 `Audio Queue Services` ,因此暂时使用 `AVAudioRecorder` ,需要提供什么API?
    
    -  1、一些基本配置参数,例如通道数、采样率、音频格式等
    
    -  2、录音状态,正在录音、暂停录音、停止录音
    
    -  3、录音结束时间,设置录音在某一时间停止
    
    -  4、音频控制接口,准备播放(参数配置后)、开始播放、暂停播放、停止播放
    
    -  5、音频播放状态监听,通过代理监听:录音开始播放、正在录音、结束录音、录音出现错误
    
    - ####**音频播放**
    
    > 涉及播放必须支持本地和网络媒体,支持常用音频格式、支持列表播放,而且可定制性必须强,通过上文表格可以发现,只有 `Audio Queue Services` 和 `AVPlayer` 满足,当然,由于没有仔细研究 `Audio Queue Services`,因此暂时使用 `AVPlayer` ,那么需要什么API呢?
    
    -  1、根据URL创建播放器,可传入单个URL字符串或者URL字符串数组
    
    -  2、动态添加新的URL地址,可传入单个URL字符串或者URL字符串数组
    
    -  3、播放器当前状态,正在播放、暂停播放、停止播放
    
    -  4、当前播放URL的信息,包括当前播放器音量大小、播放总时长、当前播放到的时间、当前播放URL在播放队列中的下标、播放器播放URL队列数组、剩余播放URL数
    
    -  5、播放器控制接口,开始播放、暂停播放、停止播放、设置播放进度、移动播放指定下标开始播放(实现播放上一条、下一条)
    
    -  6、播放器监听,通过代理监听:播放器开始播放、正在播放、当前缓冲进度、结束播放、播放错误
    
    ---
    
    # 四、功能封装实现
    
    - ####**录音,AVAudioRecorder封装**
    > 大概流程:检测授权 - 创建录音器 - 配置基本信息 - 准备录音 - 录音 - 暂停/停止
    
    -  **1、检测授权**
    
      由于录音需要访问麦克风,需要在 `Info.plist` 添加 `Privacy - Microphone Usage Description` key,但是添加后,系统会首次进入的时候提示,如果拒绝了就什么都不处理,此时需要通过 `AVCaptureDevice` 手动监听,如果之前同意授权,那么就正常创建录音器,否则提示用户去开启,具体代码如下:
    
      ```
      - (void)checkAuth{
      switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]) {
          case AVAuthorizationStatusAuthorized:{
              [self fl_createAudioRecorder];
              break;
          }
          case AVAuthorizationStatusNotDetermined:{
              [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
                  if (granted) {
                      [self fl_createAudioRecorder];
                  }
                  else{
                      [self fl_showAuthTip];
                  }
              }];
              break;
          }
          default:
              [self fl_showAuthTip];
              break;
      }
    }
      ```
    
      ```
      - (void)fl_showAuthTip{
      UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"温馨提示" message:@"您还没开启授权麦克风,请打开--> 设置 -- > 隐私 --> 通用等权限设置" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
      [alert show];
    }
      ```
    
    -  **2、创建录音器**
    
      在这方法内部,就可以配置默认基本信息(例如音频支持类型、默认通道数、默认采样率等等)、创建高精度定时器、准备录音、监听相关事件(例如应用程序进入后台、前台处理、录音结束、录音编码失败、录音被打断、打断结束等)
    
      ```
      - (void)fl_createAudioRecorder{
      NSError *error;
      AVAudioSession * audioSession = [AVAudioSession sharedInstance];
      // 设置类别,表示该应用同时支持播放和录音
      [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error: &error];
      // 启动音频会话管理,此时会阻断后台音乐的播放.
      [audioSession setActive:YES error: &error];
      
      // 音频使用内置扬声器和麦克风
      [audioSession overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
      
      self.Recorder = [[AVAudioRecorder alloc] initWithURL:[NSURL fileURLWithPath:[self fl_filePath]] settings:[self fl_recorderSetting] error:&error];
      if (error) {
          [self fl_delegateResponseFailureWithCode:FLAudioRecorderErrorByRecorderIsNotInit];
          return;
      }
      
      [self fl_createTimer];
      
      self.Recorder.delegate = self;
      // 开启音量检测
      self.Recorder.meteringEnabled = YES;
      
      [self fl_prepare];
      
      [self fl_addObserver];
    }
      ```
    
    -  **3、高精度定时器创建**
    
     由于 `AVAudioRecorder` 并没有提供类似 `AVPlayer` 的高精度监听正在播放机制,因此需要手动创建一个监听,内部计算,从而实现定时结束录音,此处使用 **`GCD`**来创建定时器。
    
      为啥不用 `NSTimer` ,首先 `NSTimer` 只提供创建和销毁,配合录音的开始、暂停就需要不断创建和销毁,相对来说消耗多点性能,其次回调都是使用 `SEL`(iOS 10之前),相对来说,没那么方便;相反,使用 **`GCD`**来创建定时器只需要创建一次,而且很方便地开始(`dispatch_resume`)、暂停(`dispatch_suspend`)和停止(`dispatch_suspend`同时重置定时计数达到停止效果)定时器,和录音配合简直完美
    
      为啥说是高精度,这个其实是相对而言的,一般录音、播放基本单位都是秒,那么此时录音定时则0.01s执行一次,就是说,定时器可获取到小数点后两位,部分具体代码如下:
    
      ```
      - (void)fl_createTimer{
      // 创建定时器对象
      self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
      // 设置时间间隔
      dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC, 0);
      // 定时器回调
      __weak typeof(self) weakSelf = self;
      dispatch_source_set_event_handler(self.timer, ^{
          typeof(self) strongSelf = weakSelf;
          if (strongSelf.count >= strongSelf.endTime * 100) {
              strongSelf.count = strongSelf.endTime * 100;
              [strongSelf fl_stop:nil];
          }
          else{
              FL_DELEGATE_RESPONSE(strongSelf.delegate, @selector(fl_audioRecorder:recordingWithCurrentTime:), @[strongSelf,@(strongSelf.count++ / 100)], nil);
          }
      });
    }
      ```
    
    -  **4、开始录音、暂停录音、停止录音**
      
      这些API都是直接封装 `AVAudioRecorder` 提供的,只是在此基础上添加一些监听、状态更新、错误处理,这里谈谈代理相应如何处理,其他具体处理可查看源码。
    
      也许大家看上面示例代码会发现有个C函数 `FL_DELEGATE_RESPONSE(<#id delegate#>, <#SEL selector#>, <#NSArray<id> *objects#>, <#^(void)complete#>)` ,这个是一个私有方法,专门处理代理回调实现,delegate就是要 response 的
    target,selector 就是 response 的方法,objects 就是 selector 所需要的参数,complete是 response 后的操作回调。
    
      一般做法是:
    
      ```
      if (self.delegate && [self.delegate respondsToSelector:@selector(fl_audioRecorder:recordingWithCurrentTime:)]) {
                  [self.delegate fl_audioRecorder:self recordingWithCurrentTime:@(strongSelf.count++ / 100)];
              }
      ```
    
      当然,此时可以利用 `performSelector` 进行封装,变成如下:(其中 `FLSuppressPerformSelectorLeakWarning ` 宏是用作忽略编译器警告)
    
      ```
      if (self.delegate && [self.delegate respondsToSelector:@selector(fl_audioRecorder:recordingWithCurrentTime:)]) {
                  FLSuppressPerformSelectorLeakWarning(
                      [self.delegate performSelector:@selector(fl_audioRecorder:recordingWithCurrentTime:) withObject:self withObject:@(strongSelf.count++ / 100)];
                  );
              }
      ```
    
      很快就会发现, `performSelector` 就不适用了,因为某些代理需要传递的参数不止两个,可能是3个、4个或更多,那么此时就有个终极解决办法,就是通过获取方法签名 `NSMethodSignature` ,然后获取方法实现 `NSInvocation` 通过设置 `target` 、 `selector` 和 `argument`,然后调用 `invoke` 方法去执行,因此参数可以通过数组传入,具体实现代码如下:
    
      ```
      id FL_PERFORM_SELECTOR(id target,SEL selector,NSArray <id>* objects){
      // 获取方法签名
      NSMethodSignature *sig = [target methodSignatureForSelector:selector];
      if (sig){
          NSInvocation* invo = [NSInvocation invocationWithMethodSignature:sig];
          [invo setTarget:target];
          [invo setSelector:selector];
          for (NSInteger index = 0; index < objects.count; index ++) {
              id object = objects[index];
              // 参数从下标2开始
              [invo setArgument:&object atIndex:index + 2];
          }
          [invo invoke];
          if (sig.methodReturnLength) {
              id anObject;
              [invo getReturnValue:&anObject];
              return anObject;
          }
          else {
              return nil;
          }
      }
      else {
          return nil;
      }
    }
      ```
    
    ---
    
    - ####**播放,AVPlayer封装**
    > 大概流程:创建播放器-设置监听-开始播放-暂停/停止播放
    
    -  **1、创建播放器**
    
      通过传入`URL` 字符串 或者 `URL` 字符串数组,将 `URL` 添加到内部队列中,并设置监听当前的 `URL` ,核心代码如下:
    
      ```
      - (void)fl_createPlayWithItem:(AVPlayerItem *)item andStartImmediately:(BOOL)startImmediately{
      // 销毁之前的
      [self fl_removeObserve];
      // 初始化播放器
      if (!self.player) {
          self.player = [AVPlayer playerWithPlayerItem:item];
      }
      else{
          [self.player replaceCurrentItemWithPlayerItem:item];
      }
      // 监听
      [self fl_addObserve];
      self.playerStatus = Player_Stoping;
      if (startImmediately) {
          [self fl_startAtBegining];
      }
    }
      ```
    
    -  **2、设置监听**
    
      监听开始播放、正在播放(通过 `addPeriodicTimeObserverForInterval` 监听)、缓冲进度(通过 `KVO` 监听 `AVPlayerItem` 的 `loadedTimeRanges` 属性)、结束播放、播放失败、应用进入前后台、耳机拔插事件,核心代码如下:
    
      ```
      - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
      AVPlayerItem *playerItem = object;
      if ([keyPath isEqualToString:@"status"]) {
          AVPlayerStatus status= [[change objectForKey:@"new"] intValue];
          if(status == AVPlayerStatusReadyToPlay){
          }
          else if(status == AVPlayerStatusUnknown){
              [self fl_delegateResponseFailureWithCode:FLAudioPlayerErrorByUnknow];
          }
          else if (status == AVPlayerStatusFailed){
              [self fl_delegateResponseFailureWithCode:FLAudioPlayerErrorByPlayerStatusFailed];
          }
      }
      else if([keyPath isEqualToString:@"loadedTimeRanges"]){
          NSArray *array = playerItem.loadedTimeRanges;
          //本次缓冲时间范围
          CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];
          float startSeconds = CMTimeGetSeconds(timeRange.start);
          float durationSeconds = CMTimeGetSeconds(timeRange.duration);
          //缓冲总长度
          NSTimeInterval totalBuffer = startSeconds + durationSeconds;
          CGFloat bufferProgress = totalBuffer / self.totalTime.doubleValue;
          [self fl_delegateResponseToSelector:@selector(fl_audioPlayer:cacheToCurrentBufferProgress:) withObject:@[self,@(FL_SAVE_PROGRESS(bufferProgress))] complete:nil];
      }
      else if ([keyPath isEqualToString:@"playbackBufferEmpty"]){
      }
      else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]){
      }
    }
      ```
    
      ```
      __weak typeof(self) weakSelf = self;
      self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
          typeof(self) strongSelf = weakSelf;
          CGFloat progress = strongSelf.currentTime.doubleValue / strongSelf.totalTime.doubleValue;
           [strongSelf fl_delegateResponseToSelector:@selector(fl_audioPlayer:playingToCurrentProgress:withBufferProgress:) withObject:@[strongSelf,@(FL_SAVE_PROGRESS(progress)),strongSelf.bufferProgress] complete:nil];
      }];
      ```
    
    -  **3、播放器管理,开始、暂停、停止、新增播放URL地址、指定播放等**
    
      开始、暂停、seek都是针对 `AVPlayer` 提供的接口进行二次封装,添加一些事件监听、状态更新以及错误处理,停止则是通过 `pause` 和 `seek` 配合实现,这里主要谈谈队列播放的实现。
    
      通过上文可以知道,系统提供的 `AVQueuePlayer` 能实现队列播放,只需要传入一个 `AVPlayerItem` 数组即可,能实现播放下一条,它是 `AVPlayer` 的子类,就是说, `AVQueuePlayer` 只不过是针对 `AVPlayer` 进行二次封装来实现队列播放而已,而且效果不太如意,通过 `advanceToNextItem` 方法播放下一条,会移除之前的 `item`,那么就没办法实现播放上一条和指定播放任一条的需求,而且监听起来十分麻烦,因此自行实现队列播放
    
      内部主要是通过两个数组实现,`valiableItems ` 是所有添加进去的 `AVPlayerItem` 数组,而 `lastItems` 是剩下需要播放的 `AVPlayerItem` 数组,当前播放的Item永远都是 `lastItems` 的第一个元素,意味着,通过`valiableItems ` 数组动态修改 `lastItems` 数组的元素,即可实现播放 `valiableItems` 数组中的任意一条地址,具体代码如下:
    
      ```
      @property (nonatomic,strong)NSMutableArray<AVPlayerItem *> *valiableItems;
    @property (nonatomic,strong)NSMutableArray<AVPlayerItem *> *lastItems;
      ```
    
      ```
      - (void)fl_moveToIndex:(NSInteger)index andStartImmediately:(BOOL)startImmediately{
      if (!self.player) {
          [self fl_delegateResponseFailureWithCode:FLAudioPlayerErrorByPlayerIsNotInit];
          return;
      }
      if (!self.valiableUrls.count) {
          [self fl_delegateResponseFailureWithCode:FLAudioPlayerErrorByPlayerNoMoreValiableUrl];
          return;
      }
      
      if (index < 0) {
          index = 0;
      }
      else if (index > self.valiableUrls.count - 1){
          index = self.valiableUrls.count - 1;
      }
      
      // stop current
      [self fl_stop];
      
      [self.lastItems removeAllObjects];
      NSMutableArray <AVPlayerItem *>*tempArrM = self.valiableItems.mutableCopy;
      NSIndexSet *se = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(index, tempArrM.count - index)];
      [self.lastItems addObjectsFromArray:[tempArrM objectsAtIndexes:se]];
      AVPlayerItem *firstItem = self.lastItems.firstObject;
      self.currentUrl = self.valiableUrls[index];
      [self fl_createPlayWithItem:firstItem andStartImmediately:startImmediately];
    }
      ```
    
    # 五、细节点
    
    - 1、传入URL地址时候,不需要关心是本地还是网络,内部会自动处理,核心代码如下:
    
    

    BOOL FL_ISNETURL(NSString *urlString){
    return [[urlString substringToIndex:4] caseInsensitiveCompare:@"http"] == NSOrderedSame || [[urlString substringToIndex:5] caseInsensitiveCompare:@"https"] == NSOrderedSame;
    }

    • (NSURL *)fl_getSuitableUrl:(NSString *)urlString{
      NSURL *url = nil;
      if (FL_ISNETURL(urlString)) {
      url = [NSURL URLWithString:urlString];
      }
      else{
      url = [NSURL fileURLWithPath:urlString];
      }
      return url;
      }
    
    - 2、内置一个时间格式转换,通过C函数 `FL_COVERTTIME` 传入时间戳(单位秒),核心代码如下:
    
    

    NSString *FL_COVERTTIME(CGFloat second){
    NSDate *date = [NSDate dateWithTimeIntervalSince1970:second];
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    if (second/3600 >= 1) {
    [formatter setDateFormat:@"HH:mm:ss"];
    } else {
    [formatter setDateFormat:@"mm:ss"];
    }
    NSString *showtimeNew = [formatter stringFromDate:date];
    return showtimeNew;
    }

    
    - 3、内置音量控制,通过 `MPVolumeView` 控制系统声音来实现,外部只需要修改 `currentVolum` 属性即可
    
    

    /**
    当前播放的音量大小(0.0-1.0),注意,播放音频的时候设置才生效
    */
    @property (nonatomic,assign)CGFloat currentVolum;

    
    - 4、框架内代理方法全部基本数据类型都包装成NSNumber,方便统一处理,因此外界获取时,可通过 `.doubleValue` 获取
    
    - 5、统一处理了所有错误信息,有一一对应的error,可根据code判断,具体的code,可查看相应的错误代理方法解释
    
    # 六、总结
    
    - 1、通过研究录音播放的上层接口类,了解API的设计思路,可以更好的理解底层实现,以及为日后封装提供API设计规范
    
    - 2、具体API以及实现、调用方法,可以去 [Github](https://github.com/gitkong/FLAudioServices) clone 代码,里面有详细完整的Demo 演示
    
    - 3、Demo中附带自定义滑动进度条,监听事件完善,带有缓冲进度,[FLSlider Github 地址](https://github.com/gitkong/FLSlider),详细使用Demo中也有演示
    
    - 4、如果大家发现上文有哪里说得不对或者有更好的建议,欢迎评论或简信我!如果你觉得写得不错,欢迎关注我!给个like 和 start,谢谢支持

    相关文章

      网友评论

      • 不随心不随欲:大神 !!有没有 怎么使用Audio Unit 播放 PCM(没有wav头的文件) 音频文件的 Demo 啊?
        gitKong:@5c6db923d9ec 回声消除这块确实很少资料,我还没仔细去研究,方便加个QQ么,279761135
        不随心不随欲:@gitKong 我这边需要回音消除 就是需要使用AudioUnit 要准确的控制 每次 误差。我现在可以播放WAV 格式的 就不知道怎么播放 没有WAV头音频 和播放 data 数据 资料也很少
        gitKong:@5c6db923d9ec 你可以用AudioQueueService来播放,我是写了录制PCM,播放还有问题
      • Eugene_iOS:考虑过播放安卓录的amr格式音频播放问题吗?
        gitKong:@Eugene_iOS 这个没考虑到
      • 秋雨无痕:我也造过轮子
      • Raindew:可以可以。。。支持!
        gitKong:@Raindew 谢谢支持~

      本文标题:音频录制和播放的详细分析及实现

      本文链接:https://www.haomeiwen.com/subject/afybnttx.html