如何在iOS上创建可暂停的音频录制应用

我们的应用程序允许志愿者记录盲人和印刷品受损人士的文章。 使用AVAudioRecorder非常简单AVAudioRecorder ,它还允许暂停,但是它没有提供任何方法来监听录音,直到过程暂停为止。 必须停止记录才能将数据写入磁盘,并使播放器(例如AVAudioPlayerAVPlayerAVQueuePlayer等)可以播放数据。

下图希望有助于突出显示本文中使用的类之间的联系。 NSObject隐式隐含为没有继承箭头的超类。

注意:黑色继承箭头的方向选择可能很不幸。 要解决此问题,请阅读下图,
AVQueuePlayer 继承了 AVPlayer
要么
AVComposition 继承了 AVAsset

一个重要的推论是AVPlayerItem将在其init接受AVAssetAVCompositionAVMutableComposition 。 (有关官方证明,请参阅AVMutableComposition的文档。)

CAVEAT:该应用程序已经超出了这些控件的范围,但是仍然适用于一般用例(并且将提供全面的文档)。

发现这一事实后,很明显

  1. 在部分录音中必须保留参考(以最终由所选音频播放器控制的类型为准-参见下文),以及
  2. 阅读完文章后,使用AVAssetExportSession将它们连接起来。

所有这些都应在后台完成,使控件对用户透明:

  • 记录 :实例化AVAudioRecorder对象,并开始记录。
  • STOP / PAUSE :停止录音,取消self.audioRecorder属性,并将音频追加到self.articleChunks数组。
  • 播放 :用该点的部分录音实例化播放器(即self.articleChunks )并开始播放。
  • 队列提交 :除了目标略有不同外,它们都将连接录音片段,并使生成的音频文件充满元数据。

在使用RECORD,STOP / PAUSE和PLAY时,记录尚未完成,但在按QUEUE或SUBMIT后,将导出最终结果,并且UI会重置为其初始状态(即, self.articleChunks设置为空数组,没有发布)已选择,标题已清除等)。

AVQueuePlayer选择AVQueuePlayer作为播放器,是因为在我们的方案中,它是最简单,最快的管理方式。 反对其他玩家的原因:

  • 可以直接使用URLData初始化AVAudioPlayer ,然后将播放器对象存储在数组中,但是需要花费大量精力才能实现连续播放。
  • AVPlayer有点高级,它仍然允许通过URL进行初始化,并且可以使用replaceCurrentItem替换当前播放的项目,因此一个对象就足够了。 缺点是后一种方法需要提供AVPlayerItem ,并且需要帮助方法来以正确的顺序连续播放音频块。

AVQueuePlayerAVPlayer的子类,这意味着它还要求其项目是中间的AVPlayerItem ,但是至少它需要它们的数组,并且要照顾正确的播放顺序(如果提供的数组将它们保存在中间)。正确的顺序)。

AVQueuePlayer有其怪癖

  • 没有标准的方法可以向前/向后跳到上一个/下一个音频项目²
  • 重新初始化现有的AVQueuePlayer很麻烦

重新初始化是必需的,因为即使可以将新项目插入队列中,也只能在队列中的项目之后插入。 在播放过程中,当前项目也会从队列中删除,因此,如果我们在队列的末尾添加带有insertAVPlayerItem ,那么在添加更多录音并播放结果之后,我们仍然会错过前面的部分。 如果用户始终聆听整个录音,则不必担心,只需一个接一个地重新插入所有内容。 但是,如果读者只听某个部分,该怎么办? 这将使某些人排队,使我们检查AVQueuePlayeritems方法并决定如何清理。

作为替代方案,我们可以使用removeAllItems然后insert每个音频块中insert一个insert ,但是使用正确的数组³从头开始创建新的AVQueuePlayer更为干净。

“一个AVPlayerItem不能与一个以上的AVPlayer实例关联”

第一种方法是使用[AVPlayerItem]类型将部分录音存储在self.articleChunks 。 由于一个AVPlayerItem只能分配给一个(并且只有一个) AVPlayer对象,并且由于每次播放都重新创建AVQueuePlayer因此需要将其更改为[AVAsset]

事实证明这不是问题,因为当需要连接音频块时,所有涉及的类无论如何都需要AVAsset ,并且简单的map可以为AVQueuePlayer返回正确的数组以进行播放:

  func startPlayer(){ 
让assetKeys = [“可玩”]
让playerItems =
self.articleChunks.map {
AVPlayerItem(
资产:$ 0,
automaticLoadedAssetKeys:assetKeys)
} self.queuePlayer = AVQueuePlayer(item:playerItems)
self.queuePlayer?.actionAtItemEnd = .advance self.queuePlayer?.play()
}

我最初遵循两个Stackoverflow答案(1、2),但是答案是在Objective-C中,仅限于连接两个音频文件,并且(至少在我们的情况下)不必要地弄乱了音轨。

AVMutableComposition用于创建支架,以便将每个通过AVAsset表示的部分音频准确地插入到前一个资产的末尾。

AVMutableCompositionAVMutableTrack都具有insertTimeRange方法,由于在我们的情况下,我们只有一个轨道,因此AVAssetTrack AVAssetTracks⁵是没有意义的。 如果是视频文件,则可以分开插入轨道,但是如果它们具有相同的长度并且已经同步,则我认为它们可以以相同的方式连接。 否则,在组合级别上包含insertTimeRange的意义是什么?

AVAssetExportSession需要初始化一个AVAsset ,但是AVCompositionAVMutableComposition都是AVMutableComposition的子类,因此可以在此处使用它们。

AVAssetExportSessionstatus属性符合键值观察(KVO)⁶,因此可以代替switch语句使用它。


  1. [^]请参阅《访问新闻》的github RecordViewController中的RecordViewController或此简单指南。
  2. [^]解决方法上有一个不错的Stackoverflow线程,但是我个人仍然认为,重新创建AVQueuePlayer对象和处理以某种形式保存音频实体的数组会更容易。
  3. [^]我担心性能,但是我发现即使在旧的(6年以上)iPad上,质量也不会降低。 让我知道我是否错了。
  4. [^]我可能仍然没有正确的心理模型,什么是音轨,但是根据我目前的理解,它们允许混合多个不同的视听输入。 因此,立体声视频将具有3条轨道:1条视频和2条声音。 如果您像在移动设备上那样以单声道录制,最终将得到一个具有1个AVAssetTrack轨道的AVAssetTrack
  5. [^]注释行显示了如何完成此操作。
  6. [^]有关KVO的一些资源,从Swift的官方文档开始:
    *将Swift与Cocoa和Objective-C结合使用(Swift 4.1)-关键价值观察
    * CocoaCasts —键值观察(KVO)和Swift 3
    * Erica Millado-玩键值观察(KVO)(Swift3)

Interesting Posts