在iOS Swift中简单实现低延迟音频播放

5
我是iOS的初学者,想用Swift设计一个鼓组应用程序。我设计了一个带有单个按钮的视图,并编写了下面的代码,但它存在一些问题:
  1. 当我快速地点击按钮时,一些声音会丢失,就像打鼓一样。
  2. 在“打鼓滚动”期间,每次触摸按钮时声音都会中断,而不是让样本播放到结束。对于铙钹滚动等情况非常糟糕。即使我再次触摸按钮,我也希望听到每个样本完整的声音。
  3. 触摸和声音之间存在延迟。我知道AVAudioPlayer不是低延迟音频的最佳选择,但作为初学者,很难在Swift中学习OpenAL、AudioUnit而没有代码示例或教程。这个问题类似于这个问题:Which framework should I use to play an audio file (WAV, MP3, AIFF) in iOS with low latency?
代码如下:
override func viewDidLoad() {
    super.viewDidLoad()

    // Enable multiple touch for the button
    for v in view.subviews {
        if v.isKindOfClass(UIButton) {
            v.multipleTouchEnabled = true
        }
    }

    // Init audio
    audioURL = NSBundle.mainBundle().URLForResource("snareDrum", withExtension: "wav")!
    do {
        player = try AVAudioPlayer(contentsOfURL: audioURL)
        player?.prepareToPlay()
    } catch {
        print("AVAudioPlayer Error")
    }
}

override func viewDidDisappear(animated: Bool) {
    super.viewDidDisappear(animated)

    player?.stop()
    player = nil
}

@IBAction func playSound(sender: UIButton) {
    player?.currentTime = 0
    player?.play()
}

我认为音频单元可能是实现您所需的低延迟行为的唯一途径,尽管如果有人告诉我我错了,我会很高兴。我正在学习如何在Swift中使用AudioToolbox,并乐意分享我的学习经验。 - Aaron Rasmussen
你收到我的电子邮件地址了吗?我把它放在这里大约30分钟,然后删除了... - Aaron Rasmussen
抱歉,我没听懂。您介意给我发电子邮件吗?您可以在我的网站上找到它:http://marcos.sampaio.me(在页面底部)。我不确定StackOverflow是否允许在此发布。 - msampaio
我同意 @RomanSausarnes 的观点 - 你无法控制 AVAudioPlayer 的时间,因此在这种应用程序中使用它并不足够。对你来说好消息是,在iOS7中,苹果公司在 AVFoundation 中提供了额外的适用于 Swift 的 API,可能会对你有所帮助。查看该参考资料可能是一个好的起点。 - marko
你尝试过使用prepareToPlay()函数来为AVAudioPlayer预加载音频文件以最小化播放延迟吗?如果不起作用,你可以尝试使用AVPlayer,这是一种更精确的播放器。你也可以预加载AVPlayer的资源以最小化音频播放延迟。 - MikeG
显示剩余4条评论
2个回答

7
如果您需要极低的延迟,我发现AVAudioSession单例上有一个非常简单的解决方案(在应用程序启动时自动实例化):
首先,使用此类方法获取对您的应用程序的AVAudioSession单例的引用:
(来自 AVAudioSession Class Reference):
获取共享音频会话
声明SWIFT
class func sharedInstance() - > AVAudioSession
然后,尝试将首选IO缓冲区持续时间设置为非常短的值(例如.002),使用此实例方法:
设置首选音频I/O缓冲区持续时间,以秒为单位。
声明 SWIFT
func setPreferredIOBufferDuration(_ duration: NSTimeInterval) throws
参数
duration 您想要使用的音频I/O缓冲区持续时间(以秒为单位)。
outError 在输入时,指向错误对象的指针。如果发生错误,则将指针设置为描述错误的NSError对象。如果您不想要错误信息,请传入nil。返回值true表示已成功发出请求,否则为false。
讨论
此方法请求更改I/O缓冲区持续时间。要确定更改是否生效,请使用IOBufferDuration属性。有关详细信息,请参见配置音频会话
请注意上面的注释-无论IOBufferDuration属性是否实际设置为传递到func setPrefferedIOBufferDuration(_ duration: NSTimeInterval) throws方法中的值,取决于函数是否返回错误,以及其他我不完全清楚的因素。此外,在我的测试中,我发现如果将此值设置为极低值,则确实会设置该值(或接近该值),但播放文件时(例如使用AVAudioPlayerNode),声音不会播放。没有错误,只是没有声音。这显然是一个问题。我还没有发现如何测试这个问题,除了在实际设备上测试时注意到缺乏声音。我会研究一下。但是现在,我建议将首选持续时间设置为不少于0.002或0.0015。对于我正在测试的iPad Air(型号A1474),值为0.0015似乎有效。而0.0012的值似乎在我的iPhone 6S上运行良好。
另外一个需要考虑的因素是音频文件的格式对于CPU负载的影响。当播放未压缩格式时,CPU负载非常低。苹果公司建议使用CAF文件以获得最高质量和最低负荷。对于压缩文件和最低负荷,您应该使用IMA4压缩(来自iOS多媒体编程指南):
iOS中首选音频格式: 对于未压缩(最高质量)音频,请使用16位小端线性PCM音频数据打包在CAF文件中。您可以使用Mac OS X中的afconvert命令行工具将音频文件转换为此格式,如下所示:
/usr/bin/afconvert -f caff -d LEI16 {INPUT} {OUTPUT}
为了在同时播放多个声音时减少内存使用,请使用IMA4(IMA/ADPCM)压缩。这将减小文件大小,但在解压缩期间对CPU的影响很小。与线性PCM数据一样,将IMA4数据打包在CAF文件中。
您也可以使用afconvert将其转换为IMA4:
/usr/bin/afconvert -f AIFC -d ima4 [file]

最小的I/O缓冲持续时间至少为0.005秒(256帧),但可能会根据使用的硬件而降低。 - zs2020

3
我花了一个下午尝试使用AVAudioPlayer和AVAudioSession解决这个问题,但是我无法得到任何帮助。(设置IO缓冲区持续时间,如此处所建议,似乎没有帮助。)我还尝试过AudioToolbox,但我发现表现几乎相同 - 相关用户操作和音频之间存在明显可感知的延迟。
在更多的互联网搜索之后,我找到了这个网站:
www.rockhoppertech.com/blog/swift-avfoundation/
其中关于AVAudioEngine的部分非常有用,以下代码有一点改动:
import UIKit
import AVFoundation

class ViewController: UIViewController {

var engine = AVAudioEngine()
var playerNode = AVAudioPlayerNode()
var mixerNode: AVAudioMixerNode?
var audioFile: AVAudioFile?

@IBOutlet var button: UIButton!

override func viewDidLoad() {
    super.viewDidLoad()

    engine.attach(playerNode)
    mixerNode = engine.mainMixerNode

    engine.connect(playerNode, to: mixerNode!, format: mixerNode!.outputFormat(forBus: 0))

    do {
        try engine.start()
    }

    catch let error {
        print("Error starting engine: \(error.localizedDescription)")
    }

    let url = Bundle.main.url(forResource: "click_04", withExtension: ".wav")

    do {
        try audioFile = AVAudioFile(forReading: url!)
    }

    catch let error {
        print("Error opening audio file: \(error.localizedDescription)")
    }
}

@IBAction func playSound(_ sender: Any) {

    engine.connect(playerNode, to: engine.mainMixerNode, format: audioFile?.processingFormat)
    playerNode.scheduleFile(audioFile!, at: nil, completionHandler: nil)

    if engine.isRunning{
        playerNode.play()
    } else {
        print ("engine not running")
    }
}

由于我是Swift新手并且从未使用过AVAudioEngine,因此这可能不完美。不过它似乎是有效的!


1
Starling库在使用AVAudioEngine时做得非常好:https://github.com/matthewreagan/Starling - James
那段代码片段中没有任何迹象表明它正在降低延迟。你能指出对你来说发生了什么变化吗? - WestCoastProjects

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接