iOS AVAudioEngine使用教程

翻译: AK
声明:转发本文,请联系作者授权
原文地址

在这个AVAudioEngine教程中,您将学习如何使用Apple的更高级音频工具包添加高级音频功能.

向大多数iOS开发人员提及音频处理,它们会给你带来恐惧和恐惧。这是因为,在iOS 8之前,要深入了解非常低层的Core Audio frameworw - 只有少数勇敢才才能做到这一点。值得庆幸的是,随着iOS 8和AVAudioEngine的发布,这一切都在2014年发生了变化。这个AVAudioEngine教程将向您展示如何使用Apple的新的更高级别的音频工具包来制作音频处理应用程序,而无需深入研究Core Audio。

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

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

开始

在开始之前,下载这个教程的材料(在文章的最下面,你可以看到这个下载按钮),使用Xcode编译运行,你就可以看到基础界面了.

这些控制没有做任何事, 但是他们都联接到了IBOutlets和关联了IBActions 在view controllers中.

iOS Audio Framework 介绍

在进入工作之前,我们先快速浏览一下iOS Audio frameworks

  • CoreAudio and AudioToolbox 都是c低级接口的 frameworks.
  • AVFoundation 是 Objective-C/Swift framework.
  • AVAudioEngine 是 AVFoundation 的一部分.

    这里写图片描述

  • AVAudioEngine是一个定义一组连接的音频节点的类,你在项目添加两个节点 AVAudioPlayerNode 和 AVAudioUnitTimePitch

这里写图片描述

设置音频

打开ViewController.swift并查看类内容。在顶部,您将看到所有连接的接口和类变量。这些事件已经连接到了 storyboard中.
在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)
}

仔细看都发生哪些事情:

  1. 获取bundle中声音文件的URL, 把audioFileURL 传值给 audioFile
  2. 在连接其它节点之前先要把播放器连接到engine,这些节点将处理和输出音频.这些节点将生成,处音频引擎提供连接到播放器节点的主混音器节点。默认情况下,主混音器连接到engine默认输出节点(iOS设备扬声器)。 prepare()预分配所需的资源.

    接下来,将下面的代码scheduleAudioFile()方法中

 guard let audioFile = audioFile else { return }

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

这会播放整个audioFile。 at:是您希望音频播放的指定时间(AVAudioTime).设置为nil会立即开始播放。该文件仅会播放一次。再次点击“播放”按钮不会从头重新开始。您需要重新安排再次播放。播放完音频文件后,在完成回调中设置needsFileScheduled

其它的播放方法

//提供了预加载音频数据的缓冲区
 - scheduleBuffer(AVAudioPCMBuffer, completionHandler: AVAudioNodeCompletionHandler? = nil): 

//这就像scheduleFile,可以指定开始播放的音频帧和播放的帧数
- scheduleSegment(AVAudioFile, startingFrame: AVAudioFramePosition, frameCount: AVAudioFrameCount, at: AVAudioTime?, completionHandler: AVAudioNodeCompletionHandler? = nil): 

下面代码添加到 playTapped 方法中

// 1
sender.isSelected = !sender.isSelected

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

标记
- 1 切换按钮的选择状态,这会更改storyboard中设置的按钮图像
- 2 使用player.isPlaying来判断当前播放器的状态,如果正常可以暂停 如果没有播放,你要检查一下needsFileScheduled 和音频文件

编译并运行 然后点 playPauseButton 你应该可以听到Ray’s的什么歌, 但没有UI反馈 你不知道文件一共多长, 现在播放到哪了.

添加进度回调

添加下面代码 到 viewDidLoad()方法中

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

CADisplayLink是一个计时器对象,与显示器的刷新率同步。您使用selector 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 是播放器返回的最新一个音频数据,下面我们仔细看

1, player.lastRenderTime 返回引擎的的开始时间,如果没有播放 lastRenderTime 返回NIL
2,player.playerTime(forNodeTime:) 转换 lastRenderTime 到播放器的开始时间,如果播放器没有播放playerTime 返回nil
3,sampleTime 是音频文件数据中的时间戳

下面更新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()
}

让我们一步步查看
1,属性skipFrame是添加到currentFrame或从currentFrame中减去的偏移量,最初设置为零。确保currentPosition不超出文件范围
2,将progressBar.progress更新为audioFile中的currentPosition。通过将currentPosition除以audioFile的sampleRate来计算时间。将countUpLabel和countDownLabel文本更新为audioFile中的当前时间

3,如果 currentPosition已经到了文件的结尾 然后:

  • 停止播放器
  • 暂停timer
  • 重新设置playPauseButton的选中状
  • 断开音量的事件

    编译运行 然后再一次点击 playPauseButton,你可以听到Ray’s intro,同时可以看到进度条的时间信息

实现VU Meter

现在是时候添加VU Meter功能了。这是一个UIView定位在两栏之间。视图的高度由播放音频的平均功率决定。这是您进行某些音频处理的第一次机会。
您将计算1k音频样本缓冲区的平均功率。确定音频样本缓冲器的平均功率的常用方法是计算样本的均方根(RMS)。
平均功率是以分贝表示的一系列音频样本数据的平均值。还有峰值功率,这是一系列样本数据中的最大值

在connectVolumeTap方法下面 添加一个工具方法

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值。这是它的作用

  • 1,power.isFinite检查以确保功率是有效值 - 即,不是NaN - 如果无效则返回0.0
  • 2,这里我们的vuMeter的动态范围设置为80db。对于低于-80.0的任何值,返回0.0。 iOS上的分贝值范围为-160db,接近静音,为0db,最大功率。 minDb设置为-80.0,动态范围为80db。您可以更改此值以查看它如何影响vuMeter
  • 3,完成 scaled的0.0和1.0之间.

把下面的代码添加到 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
  }
}

这里的工作很多, 所以我们一步一步查看

  • 1,从mainMixerNode‘s中取到数据格式
  • 2,installTap(onBus:0,bufferSize:1024,format:format)使您可以访问mainMixerNode输出总线上的音频数据。您请求1024字节的缓冲区大小,但不保证请求的大小,特别是如果您请求的缓冲区太小或太大。 Apple的文档没有说明这些限制是什么。完成块接收AVAudioPCMBuffer和AVAudioTime作为参数。您可以检查buffer.frameLength以确定实际的缓冲区大小。
  • 3,buffer.floatChannelData为您提供了指向每个样本数据的指针数组。 channelDataValue是UnsafeMutablePointer 的数组
  • 4,从UnsafeMutablePointer 数组转换为Float数组会使以后的计算更容易。为此,请使用stride(from:to:by :)在channelDataValue中创建索引数组。然后映射{channelDataValue [$ 0]}以访问和存储channelDataValueArray中的数据值
  • 5,计算RMS涉及映射/缩减/除法操作。首先,映射操作对数组中的所有值进行平方,reduce操作求和。将平方和除以缓冲区大小,然后取平方根,生成缓冲区中音频样本数据的RMS。这应该是介于0.0和1.0之间的值,但可能存在一些边缘情况,它是负值。
  • 6,将RMS转换为分贝(声学分贝参考)。这应该是-160和0之间的值,但如果rms为负,则该值为NaN。

最后添加下面的代码到disconnectVolumeTap():

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

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

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

实现快进

是时候实现跳过前进和后退按钮了。 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()
  }
}

一步步详解

  • 1,通过乘以audioSampleRate将时间(以秒为单位)转换为帧位置,并将其添加到currentPosition。然后,确保skipFrame不在文件开头之前,而不是超过文件末尾。
  • 2,player.stop()不仅停止播放,还清除所有先前安排的事件。调用updateUI()将UI设置为新的currentPosition值
  • 3,player.scheduleSegment(_:startingFrame:frameCount:at :)安排从audioFile的skipFrame位置开始播放。 frameCount是要播放的帧数。您想要播放到文件末尾,因此将其设置为audioLengthSamples - skipFrame。最后,at:nil指定立即开始播放,而不是在将来的某个时间开始播放。
  • 4,如果在调用skip之前播放器正在播放,则调用player.play()以恢复播放。 updater.isPaused可以方便地确定这一点,因为只有先前暂停了播放器才会生效

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

实现码率变化

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

把setupAudio()方法替换成下面代码

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

这会将rateEffect(AVAudioUnitTimePitch节点)连接到音频图并将其连接起来。此节点类型是效果节点,具体来说,它可以改变播放速率和音频音高。
didChangeRateValue()动作处理对rateSlider的更改。它计算rateSliderValues数组的索引并设置rateValue,它设置rateEffect.rate。 rateSlider的值范围为0.5x到3.0x

构建并运行,然后点击playPauseButton。调整rateSlider,听听Ray新的效果

接下来要做什么?
你可以下载工程代码在这个教程的最上面和最下面下载连接 进行下载
查看其它的效果 你可以添加到 audioSetup()方法中,一个方法是可以使用滑动条控制rateEffect.pitch
学习更多的关于ios AVAudioEngine 可以查看

我们希望您喜欢AVAudioEngine上的这个教程。如果您有任何问题或意见,请加入以下论坛讨论!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值