ios - 如何使用 AVFoundation 以正确的音高播放不同采样率的音频文件?
问题描述
当音频文件具有不同的采样率时,应该如何配置AVAudioEngine
并AVAudioPlayerNodes
以正确的音高播放音频文件?
我读到混音器可以处理采样率转换,但我还没有实现这种行为。我正在使用扩展来播放循环音频片段,播放器应该可以使用压缩和 PCM 文件;我不知道这是否会决定解决方案。
我试图在一个installTap(onBus:bufferSize:format: block:)
块内使用 AVAudioConverter,AVAudioPlayerNode
但遇到了各种崩溃,并且不确定这是否是正确的解决方案。我是在正确的轨道上还是有更简单的解决方案可用?
import AVFoundation
import SwiftUI
@main
struct SampleRateMixerApp: App {
private let audioEngine = AVAudioEngine()
private let playerA = AVAudioPlayerNode()
private let playerB = AVAudioPlayerNode()
var body: some Scene {
WindowGroup {
Button("Play") { try? playFiles() }
Button("Stop") { playerA.stop(); playerB.stop() }
}
}
func playFiles() throws {
_ = audioEngine.mainMixerNode
try audioEngine.start()
audioEngine.prepare()
let pathA = Bundle.main.path(forResource: "32khz_sample_rate", ofType: "aif")!
try setupPlayerNode(player: playerA, withAudioEngine: audioEngine, atPath: pathA)
let pathB = Bundle.main.path(forResource: "48khz_sample_rate", ofType: "mp3")!
try setupPlayerNode(player: playerB, withAudioEngine: audioEngine, atPath: pathB)
}
func setupPlayerNode(player: AVAudioPlayerNode, withAudioEngine engine: AVAudioEngine, atPath path: String) throws {
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: nil)
let url = URL(string: path)!
let file = try AVAudioFile(forReading: url)
let frameCount = AVAudioFrameCount(file.length)
let buffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: frameCount)!
try file.read(into: buffer)
player.scheduleBufferSegment(buffer, range: 0.25...0.75, looping: true)
player.play()
}
}
extension AVAudioPlayerNode {
func scheduleBufferSegment(_ buffer: AVAudioPCMBuffer, range: ClosedRange<Double>, looping: Bool) {
let length = Double(buffer.frameLength)
let startFrame = AVAudioFramePosition(range.lowerBound * length)
let endFrame = AVAudioFramePosition(range.upperBound * length)
guard let bufferSegment = buffer.segment(from: startFrame, to: endFrame) else { return }
if looping {
scheduleBuffer(bufferSegment, at: nil, options: [.loops, .interrupts])
} else {
scheduleBuffer(bufferSegment, at: nil, options: [.interrupts]) {
DispatchQueue.main.async { [weak self] in
self?.stop()
}
}
}
}
}
extension AVAudioPCMBuffer {
func segment(from startFrame: AVAudioFramePosition, to endFrame: AVAudioFramePosition) -> AVAudioPCMBuffer? {
let framesToCopy = AVAudioFrameCount(endFrame - startFrame)
guard let segment = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: framesToCopy) else { return nil }
let sourcePointer = UnsafeMutableAudioBufferListPointer(mutableAudioBufferList)
let destinationPointer = UnsafeMutableAudioBufferListPointer(segment.mutableAudioBufferList)
let sampleSize = format.streamDescription.pointee.mBytesPerFrame
for (source, destination) in zip(sourcePointer, destinationPointer) {
memcpy(destination.mData,
source.mData?.advanced(by: Int(startFrame) * Int(sampleSize)),
Int(framesToCopy) * Int(sampleSize))
}
segment.frameLength = framesToCopy
return segment
}
}
解决方案
调用AVAudioEngine
'在加载每个文件后connect(:to:format:)
传入AVAudioFiles
' 。processingFormat
这可确保将AVAudioPlayerNode
' 的输出设置为与其将播放的文件相同的采样率。
以下代码说明了这是如何完成的。该问题与播放缓冲区段无关,因此未将其包含在解决方案中。尽管无需先断开播放器即可正常播放音频,但我怀疑它可能会泄漏内存,因此请先断开节点以确保安全。
import AVFoundation
let audioEngine = AVAudioEngine()
let player = AVAudioPlayerNode()
func playFiles() throws {
_ = audioEngine.mainMixerNode // Called to ensure mainMixerNode singleton is instantiated
try audioEngine.start()
audioEngine.prepare()
audioEngine.attach(player)
let path = Bundle.main.path(forResource: "32khz", ofType: "aif")!
let url = URL(string: path)!
let file = try AVAudioFile(forReading: url)
let frameCount = AVAudioFrameCount(file.length)
let buffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: frameCount)!
try file.read(into: buffer)
audioEngine.disconnectNodeOutput(player) // May be unnecessary
audioEngine.connect(player, to: audioEngine.mainMixerNode, format: file.processingFormat)
player.scheduleBuffer(buffer)
player.play()
}
推荐阅读
- python - 在 WSL2 下使用 selenium 时出现控制台错误
- html - Google 跟踪代码管理器 - 跟踪表单中的下拉问题
- android - 使用 Single.zip() 时成功的 API 调用返回空值
- mysql - 将一行中的每一列与数据库sql中的每一行进行比较
- cadence-workflow - 在实现 Uber Cadence 工作流程时,Java 客户端与 Go 客户端有哪些重大差异?
- css - Tailwind-CSS 在将 .hidden 应用于元素后不应用 flex/block
- java-8 - drools-6.5 是否向后兼容 drools-2.5?
- apache-spark - 之间语句不适用于 Hive Map 列 - Spark SQL
- kotlin - 如何将 Kotlin 的多平台依赖源附加到 IDEA?
- python - 使用 XlsxWriter 将 CSV 保存在 Excel 工作簿的不同工作表中