iOS Swift 中的简单低延迟音频播放

Simple low-latency audio playback in iOS Swift

我是 iOS 的初学者,我正在尝试使用 Swift 设计一个架子鼓应用程序。我设计了一个只有一个按钮的视图并写了下面的代码,但是它有一些问题:

  1. 当我像击鼓一样快速触摸按钮时,一些声音会丢失。
  2. 仍然是 'drum roll',每次触摸按钮时声音都会中断,而不是让样本播放到结束。例如,它在铙钹卷中很糟糕。我想听到每个样本的完整声音,即使我再次触摸按钮也是如此。
  3. 触摸和声音之间存在延迟。我知道 AVAudioPlayer 不是低延迟音频的最佳选择,但作为初学者,如果没有 Swift 中的代码示例或教程,很难学习 OpenALAudioUnit .问题类似这样: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()
}

如果您需要极低的延迟,我在 AVAudioSession 单例(应用程序启动时自动实例化)上发现了一个非常简单的解决方案:

首先,使用此 class 方法获取对您应用的 AVAudioSession 单例的引用:

(来自AVAudioSession Class Reference):

Getting the Shared Audio Session

Declaration SWIFT

class func sharedInstance() -> AVAudioSession

然后,尝试将首选 IO 缓冲持续时间设置为非常合适的值 使用此实例方法的短(例如 .002):

Sets the preferred audio I/O buffer duration, in seconds.

Declaration SWIFT

func setPreferredIOBufferDuration(_ duration: NSTimeInterval) throws

Parameters

duration The audio I/O buffer duration, in seconds, that you want to use.

outError On input, a pointer to an error object. If an error occurs, the pointer is set to an NSError object that describes the error. If you do not want error information, pass in nil. Return Value true if a request was successfully made, or false otherwise.

Discussion

This method requests a change to the I/O buffer duration. To determine whether the change takes effect, use the IOBufferDuration property. For details see Configuring the Audio Session.


请记住上面的注释 - IOBufferDuration 属性 是否 实际上 设置为传递给 func setPrefferedIOBufferDuration(_ duration: NSTimeInterval) throws 方法的值, 取决于函数不返回错误, 其他我不完全清楚的因素。另外——在我的测试中——我发现如果你将这个值设置为一个极低的值,这个值(或接近它的值)确实会被设置,但是当播放一个文件(例如使用 AVAudioPlayerNode)时,声音不会被播放。没有错误,只是没有声音。这显然是个问题。而且我还没有发现如何测试这个问题,除了在实际设备上测试时注意到没有声音进入我的耳朵。我会调查一下。但就目前而言,我建议将首选持续时间设置为不少于 .002 或 .0015。 .0015 的值似乎适用于我正在测试的 iPad Air(型号 A1474)。虽然低至 .0012 似乎在我的 iPhone 6S 上运行良好。

从 CPU 开销的角度考虑的另一件事是音频文件的格式。播放未压缩格式时,CPU 开销非常低。 Apple 建议您应使用 CAF 文件以获得最高质量和最低开销。对于压缩文件和最低开销,您应该使用 IMA4 压缩:

(来自iOS Multimedia Programming Guide) :

Preferred Audio Formats in iOS For uncompressed (highest quality) audio, use 16-bit, little endian, linear PCM audio data packaged in a CAF file. You can convert an audio file to this format in Mac OS X using the afconvert command-line tool, as shown here:

/usr/bin/afconvert -f caff -d LEI16 {INPUT} {OUTPUT}

For less memory usage when you need to play multiple sounds simultaneously, use IMA4 (IMA/ADPCM) compression. This reduces file size but entails minimal CPU impact during decompression. As with linear PCM data, package IMA4 data in a CAF file.

您也可以使用 afconvert 转换为 IMA4:

/usr/bin/afconvert -f AIFC -d ima4 [file]

我花了一个下午试图通过玩弄 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。不过,它似乎确实有效!