iOS开发者图书馆

AVFoundation框架解析(十九)—— AVAudioEn

2018-08-19  本文已影响0人  刀客传奇

版本记录

版本号 时间
V1.0 2018.08.19

前言

AVFoundation框架是ios中很重要的框架,所有与视频音频相关的软硬件控制都在这个框架里面,接下来这几篇就主要对这个框架进行介绍和讲解。感兴趣的可以看我上几篇。
1. AVFoundation框架解析(一)—— 基本概览
2. AVFoundation框架解析(二)—— 实现视频预览录制保存到相册
3. AVFoundation框架解析(三)—— 几个关键问题之关于框架的深度概括
4. AVFoundation框架解析(四)—— 几个关键问题之AVFoundation探索(一)
5. AVFoundation框架解析(五)—— 几个关键问题之AVFoundation探索(二)
6. AVFoundation框架解析(六)—— 视频音频的合成(一)
7. AVFoundation框架解析(七)—— 视频组合和音频混合调试
8. AVFoundation框架解析(八)—— 优化用户的播放体验
9. AVFoundation框架解析(九)—— AVFoundation的变化(一)
10. AVFoundation框架解析(十)—— AVFoundation的变化(二)
11. AVFoundation框架解析(十一)—— AVFoundation的变化(三)
12. AVFoundation框架解析(十二)—— AVFoundation的变化(四)
13. AVFoundation框架解析(十三)—— 构建基本播放应用程序
14. AVFoundation框架解析(十四)—— VAssetWriter和AVAssetReader的Timecode支持(一)
15. AVFoundation框架解析(十五)—— VAssetWriter和AVAssetReader的Timecode支持(二)
16. AVFoundation框架解析(十六)—— 一个简单示例之播放、录制以及混合视频(一)
17. AVFoundation框架解析(十七)—— 一个简单示例之播放、录制以及混合视频之源码及效果展示(二)
18. AVFoundation框架解析(十八)—— AVAudioEngine之基本概览(一)

开始

向大多数iOS开发人员提及音频处理,他们认为很困难甚至是恐惧。这是因为,在iOS 8之前,它意味着深入探讨低级Core Audio框架的深度 - 只有少数勇敢的灵魂才能做到这一点。值得庆幸的是,随着iOS 8和AVAudioEngine的发布,这一切都在2014年发生了变化。本文将向您展示如何使用Apple的新的更高级别的音频工具audio toolkit包来制作音频处理应用程序,而无需深入研究Core Audio

那就对了!您不再需要搜索模糊的基于指针的C / C ++结构和内存缓冲区来收集原始音频数据。

在这个AVAudioEngine教程中,您将使用AVAudioEngine构建下一个优秀的播客应用程序。更具体地说,您将添加由UI控制的音频功能:播放/暂停按钮,跳过前进/后退按钮,进度条和播放速率选择器。当你完成后,你会有一个很棒的应用程序。

注意:写作本文的环境Swift 4, iOS 11, Xcode 9。


iOS Audio Framework Introduction - iOS音频框架介绍

在进入项目之前,首先看一下iOS音频框架的概述:


Setup Audio - 设置Audio

打开ViewController.swift并查看内部。 在顶部,您将看到所有连接的outlets和类变量。 actions还连接到sb中的相应outlets

将以下代码添加到setupAudio()

// 1
audioFileURL = Bundle.main.url(forResource: "Intro", withExtension: "mp4")

// 2
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: audioFormat)
engine.prepare()

do {
  // 3
  try engine.start()
} catch let error {
  print(error.localizedDescription)
}

仔细看看发生了什么:

接下来,将以下内容添加到scheduleAudioFile()

guard let audioFile = audioFile else { return }

skipFrame = 0
player.scheduleFile(audioFile, at: nil) { [weak self] in
  self?.needsFileScheduled = true
}

这会调度播放整个audioFileat:是您希望音频播放的未来时间(AVAudioTime)。 设置为nil会立即开始播放。 该文件仅调度播放一次。 再次点击Play按钮不会从头重新开始。 您需要重新调度再次播放。 播放完音频文件后,在完成块中设置标志needsFileScheduled

还有其他调度音频用于播放:

然后,将以下内容添加到playTapped(_ :)

// 1
sender.isSelected = !sender.isSelected

// 2
if player.isPlaying {
  player.pause()
} else {
  if needsFileScheduled {
    needsFileScheduled = false
    scheduleAudioFile()
  }
  player.play()
}

下面细分一下:

Build并运行,然后点击playPauseButton。 你应该听到声音。 但是,没有UI反馈,你不知道文件有多长或者你现在播放到哪里。


Add Progress Feedback - 增加进度反馈

viewDidLoad()中添加如下代码:

updater = CADisplayLink(target: self, selector: #selector(updateUI))
updater?.add(to: .current, forMode: .defaultRunLoopMode)
updater?.isPaused = true

CADisplayLink是一个计时器对象,与显示器的刷新率同步。 您使用方法updateUI实例化它。 然后,将其添加到运行循环中 - 在本例中为默认运行循环default run loop。 最后,它不需要开始运行,因此将isPaused设置为true

用以下内容替换playTapped(_ :)的实现:

sender.isSelected = !sender.isSelected

if player.isPlaying {
  disconnectVolumeTap()
  updater?.isPaused = true
  player.pause()
} else {
  if needsFileScheduled {
    needsFileScheduled = false
    scheduleAudioFile()
  }
  connectVolumeTap()
  updater?.isPaused = false
  player.play()
}

这里的关键是当播放器暂停时使用updater.isPaused = true暂停UI。 您将在下面的VU Meter部分中了解connectVolumeTap()disconnectVolumeTap()

使用以下内容替换var currentFrame:AVAudioFramePosition = 0

var currentFrame: AVAudioFramePosition {
  // 1
  guard
    let lastRenderTime = player.lastRenderTime,
    // 2
    let playerTime = player.playerTime(forNodeTime: lastRenderTime)
    else {
      return 0
  }
  
  // 3
  return playerTime.sampleTime
}

currentFrame返回播放器呈现的最后一个音频样本。 下面一步步的看:

现在进行UI更新。 将以下内容添加到updateUI()

// 1
currentPosition = currentFrame + skipFrame
currentPosition = max(currentPosition, 0)
currentPosition = min(currentPosition, audioLengthSamples)

// 2
progressBar.progress = Float(currentPosition) / Float(audioLengthSamples)
let time = Float(currentPosition) / audioSampleRate
countUpLabel.text = formatted(time: time)
countDownLabel.text = formatted(time: audioLengthSeconds - time)

// 3
if currentPosition >= audioLengthSamples {
  player.stop()
  updater?.isPaused = true
  playPauseButton.isSelected = false
  disconnectVolumeTap()
}

下面我们一步一步的看:

Build并运行,然后点击playPauseButton。 再次,您将听到声音,但这次progressBar和计时器标签提供以前缺少的状态信息。


Implement the VU Meter - 实现VU Meter

现在是时候添加VU Meter功能了。 这是一个UIView定位在暂停图标的栏之间。 视图的高度由播放音频的平均功率决定。 这是您进行某些音频处理的第一次机会。

您将计算1k音频样本缓冲区的平均功率。 确定音频样本缓冲器的平均功率的常用方法是计算样本的均方根(RMS)。

平均功率是以分贝表示的一系列音频样本数据的平均值。 还有峰值功率,这是一系列样本数据中的最大值。

connectVolumeTap()下面添加以下helper方法:

func scaledPower(power: Float) -> Float {
  // 1
  guard power.isFinite else { return 0.0 }

  // 2
  if power < minDb {
    return 0.0
  } else if power >= 1.0 {
    return 1.0
  } else {
    // 3
    return (fabs(minDb) - fabs(power)) / fabs(minDb)
  }
}

scaledPower(power :)将负功率分贝值转换为正值,以适应调整上面的volumeMeterHeight.constant值。 这是它的作用:

现在,将以下内容添加到connectVolumeTap()

// 1
let format = engine.mainMixerNode.outputFormat(forBus: 0)
// 2
engine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, when in
  // 3
  guard 
    let channelData = buffer.floatChannelData,
    let updater = self.updater 
    else {
      return
  }

  let channelDataValue = channelData.pointee
  // 4
  let channelDataValueArray = stride(from: 0, 
                                     to: Int(buffer.frameLength),
                                     by: buffer.stride).map{ channelDataValue[$0] }
  // 5
  let rms = sqrt(channelDataValueArray.map{ $0 * $0 }.reduce(0, +) / Float(buffer.frameLength))
  // 6
  let avgPower = 20 * log10(rms)
  // 7
  let meterLevel = self.scaledPower(power: avgPower)

  DispatchQueue.main.async {
    self.volumeMeterHeight.constant = !updater.isPaused ? 
           CGFloat(min((meterLevel * self.pauseImageHeight), self.pauseImageHeight)) : 0.0
  }
}

这里进行细分说明:

最后,将以下内容添加到disconnectVolumeTap()

engine.mainMixerNode.removeTap(onBus: 0)
volumeMeterHeight.constant = 0

AVAudioEngine每个总线只允许一次点击。 在不使用时将其删除是一种很好的做法。

Build并运行,然后点击playPauseButtonvuMeter现在处于活动状态,提供音频数据的平均功率反馈。


Implementing Skip - 实现Skip

是时候实现跳过前进和后退按钮了。skipForwardButton在音频文件中向前跳10秒,skipBackwardButton跳回10秒。

添加以下内容到seek(to:)

guard 
  let audioFile = audioFile,
  let updater = updater 
  else {
    return
}

// 1
skipFrame = currentPosition + AVAudioFramePosition(time * audioSampleRate)
skipFrame = max(skipFrame, 0)
skipFrame = min(skipFrame, audioLengthSamples)
currentPosition = skipFrame

// 2
player.stop()

if currentPosition < audioLengthSamples {
  updateUI()
  needsFileScheduled = false

  // 3
  player.scheduleSegment(audioFile, 
                         startingFrame: skipFrame, 
                         frameCount: AVAudioFrameCount(audioLengthSamples - skipFrame), 
                         at: nil) { [weak self] in
    self?.needsFileScheduled = true
  }

  // 4
  if !updater.isPaused {
    player.play()
  }
}

这是进行详细分解:

Build并运行,然后点击playPauseButton。点击skipBackwardButton并使用skipForwardButton跳过前进和后退。观察progressBar和计数标签的变化。


Implementing Rate Change - 实现播放速率的改变

最后要实现的是改变播放速度。 如今,以超过1倍的速度收听播客是一项受欢迎的功能。

setupAudio()中,替换以下内容:

engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: audioFormat)

以及:

engine.attach(player)
engine.attach(rateEffect)
engine.connect(player, to: rateEffect, format: audioFormat)
engine.connect(rateEffect, to: engine.mainMixerNode, format: audioFormat)

这会将rateEffectAVAudioUnitTimePitch节点)连接到音频图并将其连接起来。 此节点类型是效果节点,具体来说,它可以改变播放速率和音频音高。

didChangeRateValue() action处理对rateSlider的更改。 它计算rateSliderValues数组的索引并设置rateValue,它设置rateEffect.raterateSlider的值范围为0.5x到3.0x

Build并运行,然后点击playPauseButton。 调整rateSlider就可以听一下效果声音了。

参考文章

后记

本篇主要讲述了AVAudioEngine之详细说明和一个简单示例,感兴趣的给个赞或者关注~~~

上一篇 下一篇

猜你喜欢

热点阅读