AVFoundation框架解析(二十七) —— 基于AVAud

2021-05-18  本文已影响0人  刀客传奇

版本记录

版本号 时间
V1.0 2021.05.18 星期二

前言

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之基本概览(一)
19. AVFoundation框架解析(十九)—— AVAudioEngine之详细说明和一个简单示例(二)
20. AVFoundation框架解析(二十)—— AVAudioEngine之详细说明和一个简单示例源码(三)
21. AVFoundation框架解析(二十一)—— 一个简单的视频流预览和播放示例之解析(一)
22. AVFoundation框架解析(二十二)—— 一个简单的视频流预览和播放示例之源码(二)
23. AVFoundation框架解析(二十三) —— 向视频层添加叠加层和动画(一)
24. AVFoundation框架解析(二十四) —— 向视频层添加叠加层和动画(二)
25. AVFoundation框架解析(二十五) —— 播放、录制和合并视频简单示例(一)
26. AVFoundation框架解析(二十六) —— 播放、录制和合并视频简单示例(二)

开始

首先看下主要内容:

了解如何使用AVAudioEngine构建下一个最佳播客应用程序! 实施音频功能以暂停,跳过,加快,放慢速度并更改应用程序中音频的音调。内容来自翻译

下面就是写作环境:

Swift 5, iOS 14, Xcode 12

接着就是正文了。

向大多数iOS开发人员提及音频处理,他们会给您带来恐惧的感觉。 这是因为,在iOS 8之前,这意味着要深入研究底层的Core Audio框架 —— 只有少数勇敢的人敢于这样做。 值得庆幸的是,随着iOS 8AVAudioEngine的发布,这一切都在2014年发生了变化。 该AVAudioEngine教程将向您展示如何使用Apple的新的高级音频工具包来制作音频处理应用程序,而无需深入研究Core Audio

这是正确的! 您不再需要搜索模糊的,基于指针的C / C ++结构和内存缓冲区来收集原始音频数据。 如果您了解基本的Swift代码,则本教程将指导您完成向应用程序添加音频功能的过程。

在本教程中,您将使用AVAudioEngine构建下一个出色的播客应用:Raycast

您将在此应用中实现的功能包括:

完成后,您将拥有一个出色的应用程序,可以收听播客和音频文件。

下载入门项目,在Xcode中构建并运行您的项目,您将看到基本的用户界面:

控件尚无任何作用。 实际上,由于音频尚未准备好播放,因此暂时将其禁用。 但是,将控件设置为调用将要实现的各自的视图模型方法。

1. Understanding iOS Audio Frameworks

在进入项目之前,这里是iOS音频框架的快速概述:

AVAudioEngine是一个类,它定义一组连接的音频节点。 您将在项目中添加两个节点:AVAudioPlayerNodeAVAudioUnitTimePitch

通过利用这些框架,您可以避免深入研究音频信息的底层处理,而专注于要添加到应用程序中的高级功能。

2. Setting up Audio

打开Models / PlayerViewModel.swift并查看内部。 在顶部的Public properties下,您将看到视图中用于布置音频播放器的所有属性。 提供了用于制作播放器的方法供您填写。

将以下代码添加到setupAudio()

// 1
guard let fileURL = Bundle.main.url(
  forResource: "Intro",
  withExtension: "mp3")
else {
  return
}

do {
  // 2
  let file = try AVAudioFile(forReading: fileURL)
  let format = file.processingFormat
  
  audioLengthSamples = file.length
  audioSampleRate = format.sampleRate
  audioLengthSeconds = Double(audioLengthSamples) / audioSampleRate
  
  audioFile = file
  
  // 3
  configureEngine(with: format)
} catch {
  print("Error reading the audio file: \(error.localizedDescription)")
}

仔细看看发生了什么:

将此代码添加到configureEngine(with :)

// 1
engine.attach(player)
engine.attach(timeEffect)

// 2
engine.connect(
  player,
  to: timeEffect,
  format: format)
engine.connect(
  timeEffect,
  to: engine.mainMixerNode,
  format: format)

engine.prepare()

do {
  // 3
  try engine.start()
  
  scheduleAudioFile()
  isPlayerReady = true
} catch {
  print("Error starting the player: \(error.localizedDescription)")
}

详细看下:

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

guard
  let file = audioFile,
  needsFileScheduled
else {
  return
}

needsFileScheduled = false
seekFrame = 0

player.scheduleFile(file, at: nil) {
  self.needsFileScheduled = true
}

这样可以安排播放整个音频文件。 at:的参数是时间 —— AVAudioTime —— 将来您要播放音频。 将其设置为nil立即开始播放。 该文件仅调度播放一次。 再次点击播放不会从头开始重新播放。 您需要重新调度才能再次播放。 音频文件结束播放后,在完成block中设置了标记needsFileScheduled

调度音频播放的其他方式包括:

接下来,您将解决用户交互。 将以下内容添加到playOrPause()中:

// 1
isPlaying.toggle()

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

这是在做什么:

构建并运行。

轻按播放,您应该会听到RayThe raywenderlich.com Podcast播客精彩介绍。但是,没有UI反馈-您不知道文件有多长时间或文件在其中。


Adding Progress Feedback

现在您可以听到音频了,如何去看呢? 嗯,本教程未涵盖内容。 但是,您当然可以查看音频文件的进度!

Models / PlayerViewModel.swift的底部,将以下内容添加到setupDisplayLink()中:

displayLink = CADisplayLink(target: self, selector: #selector(updateDisplay))
displayLink?.add(to: .current, forMode: .default)
displayLink?.isPaused = true

提示:您可以通过按Control-6并键入要查找的名称的一部分,在更长的文件(如PlayerViewModel.swift)中找到方法和属性!

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

用以下代码替换playOrPause()的实现:

isPlaying.toggle()

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

此处的关键是通过在播放器状态更改时设置displayLink?.isPaused来暂停或启动displayLink。 您将在下面的VU计量器部分中了解connectVolumeTap()disconnectVolumeTap()

现在,您需要实现关联的UI更新。 将以下内容添加到updateDisplay()

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

// 2
if currentPosition >= audioLengthSamples {
  player.stop()
  
  seekFrame = 0
  currentPosition = 0
  
  isPlaying = false
  displayLink?.isPaused = true
  
  disconnectVolumeTap()
}

// 3
playerProgress = Double(currentPosition) / Double(audioLengthSamples)

let time = Double(currentPosition) / audioSampleRate
playerTime = PlayerTime(
  elapsedTime: time,
  remainingTime: audioLengthSeconds - time
)

这是怎么回事:

该接口已经连接到显示playerProgresselapsedTimeremainingTime

构建并运行,然后点击播放/暂停。 您会再次听到Ray的介绍,但是这次进度条和计时器标签会提供缺少的状态信息。


Implementing the VU Meter

现在是时候添加VU Meter功能了。 VU Meter通过根据音频的音量描绘跳动图形来指示实时音频。

您将使用定位在暂停图标栏之间的视图。 播放音频的平均功率决定了视图的高度。 这是您进行音频处理的第一个机会。

您将在1k音频样本缓冲区上计算平均功率。 确定音频样本缓冲区平均功率的一种常用方法是计算样本的Root Mean Square (RMS)

平均功率是音频样本数据范围的平均值的分贝表示。 您还应该注意峰值功率,它是样本数据范围内的最大值。

用以下代码替换scaledPower(power :)中的代码:

// 1
guard power.isFinite else {
  return 0.0
}

let minDb: Float = -80

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

scaledPower(power :)将负功率分贝值转换为正值,以调整meterLevel值。 它的作用是:

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

// 1
let format = engine.mainMixerNode.outputFormat(forBus: 0)
// 2
engine.mainMixerNode.installTap(
  onBus: 0,
  bufferSize: 1024,
  format: format
) { buffer, _ in
  // 3
  guard let channelData = buffer.floatChannelData 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 {
    return $0 * $0
  }
  .reduce(0, +) / Float(buffer.frameLength))
  
  // 6
  let avgPower = 20 * log10(rms)
  // 7
  let meterLevel = self.scaledPower(power: avgPower)

  DispatchQueue.main.async {
    self.meterLevel = self.isPlaying ? meterLevel : 0
  }
}

这里发生了很多事情,所以这里是细分:

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

engine.mainMixerNode.removeTap(onBus: 0)
meterLevel = 0

AVAudioEngine每条总线仅允许单击一次。 最好在不使用时将其删除。

构建并运行,然后点击播放/暂停:

VU表现在处于活动状态,提供音频数据的平均功率反馈。 播放音频时,您应用的用户将可以轻松地从视觉上辨别。


Implementing Skip

是时候实现向前和向后跳过按钮了。 在此应用程序中,每个按钮向前或向后搜索10秒钟。

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

guard let audioFile = audioFile else {
  return
}

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

// 2
let wasPlaying = player.isPlaying
player.stop()

if currentPosition < audioLengthSamples {
  updateDisplay()
  needsFileScheduled = false

  let frameCount = AVAudioFrameCount(audioLengthSamples - seekFrame)
  // 3
  player.scheduleSegment(
    audioFile,
    startingFrame: seekFrame,
    frameCount: frameCount,
    at: nil
  ) {
    self.needsFileScheduled = true
  }

  // 4
  if wasPlaying {
    player.play()
  }
}

这是逐一播放:

是时候用这种方法去seek了。 添加以下内容到skip(forwards:)

let timeToSeek: Double

if forwards {
  timeToSeek = 10
} else {
  timeToSeek = -10
}

seek(to: timeToSeek)

视图中的两个跳过按钮均调用此方法。 如果forwards参数为true,则音频会向前跳过10秒钟。 相反,如果参数为false,则音频向后跳。

构建并运行,然后点击播放/暂停。 点击向前跳过和向后跳过按钮以向前和向后跳过。 观察progressBar和计数标签的变化。


Implementing Rate Change

下一个要添加的功能是任何音频应用程序的良好质量。 如今,以高于1x倍的速度收听播客是一种流行的功能。

将以下内容添加到updateForRateSelection()

let selectedRate = allPlaybackRates[playbackRateIndex]
timeEffect.rate = Float(selectedRate.value)

在界面中,用户将点击分段选择器以选择播放速度。 您将选定的选项转换为乘法器以发送到音频播放器。

构建并运行,然后播放音频。 调整速率控制,以听取RayDru咖啡过多或过少时的声音。


Implementing Pitch Change

要实现的最后一件事是更改播放的音调。 尽管音调控制不如改变播放速率实用,但仍然很有趣。

将以下内容添加到updateForPitchSelection()

let selectedPitch = allPlaybackPitches[playbackPitchIndex]

timeEffect.pitch = 1200 * Float(selectedPitch.value)

根据AVAudioUnitTimePitch.pitch的文档,该值以cents为单位。 一个度等于1200 cents。 在文件顶部声明的allPlaybackPitches的值为-0.5、0、0.5。 将音调改变半个度可以使音频保持完整,因此您仍然可以听到每个单词。 随意玩这个数量或多或少会使声音失真。

构建并运行。 调整音调以听到令人毛骨悚然和/或松鼠的声音。

回顾一下AVAudioEngine的简介,主要关注点是:

有了这些,您就可以在设备上播放音频。在创建自己的播放器时有用的其他关键主题是:

要了解有关AVAudioEngine和相关的iOS音频主题的更多信息,请查看:

有关媒体播放的更多信息,请参阅AppleAVFoundation上的文档。

后记

本篇主要讲述了基于AVAudioEngine的简单使用示例,感兴趣的给个赞或者关注~~~

上一篇下一篇

猜你喜欢

热点阅读