Search code examples
iosswiftaudioavaudioplayerlow-latency

Simple low-latency audio playback in iOS Swift


I'm a beginner in iOS and I'm trying to design a drum set app with Swift. I designed a view with a single button and wrote the code below, but it has some problems:

  1. Some sounds are lost when I touch the button quickly like a drum roll.
  2. Still in a 'drum roll', the sound is interrupted every time the button is touched, instead of letting the sample play till its ending. It's awful in cymbal roll, for instance. I'd like to hear every sample sounding completely, even if I touch the button again.
  3. There is a latency between the touch and the sound. I know that AVAudioPlayer is not the best choice for low-latency audio, but as a beginner, it's hard to learn OpenAL, AudioUnit without code samples or tutorials in Swift. The problem is similar to this: Which framework should I use to play an audio file (WAV, MP3, AIFF) in iOS with low latency?.

The code:

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()
}

Solution

  • If you need extremely low latency, I discovered a very simple solution available on the AVAudioSession singleton (which is instantiated automatically when an app launches):

    First, get a reference to your app's AVAudioSession singleton using this class method:

    (from the AVAudioSession Class Reference) :

    Getting the Shared Audio Session

    Declaration SWIFT

    class func sharedInstance() -> AVAudioSession

    Then, attempt to set the preferred IO buffer duration to something very short ( such as .002) using this instance method:

    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.


    Keep in mind the note directly above- whether the IOBufferDuration property is actually set to the value passed into the func setPrefferedIOBufferDuration(_ duration: NSTimeInterval) throws method, depends on the function not returning an error, and other factors that I am not completely clear about. Also- in my testing- I discovered that if you set this value to an extremely low value, the value (or something close to it) does indeed get set, but when playing a file (for instance using the AVAudioPlayerNode) the sound will not be played. No error, just no sound. This is obviously a problem. And I haven't discovered how to test for this issue, except by noticing a lack of sound hitting my ear while testing on an actual device. I'll look into it. But for now, I would recommend setting the preferred duration to no less than .002 or .0015. The value of .0015 seems to work for the iPad Air (Model A1474) I am testing on. While as low as .0012 seems to work well on my iPhone 6S.

    And another thing to consider from a CPU overhead standpoint is the format of the audio file. Uncompressed formats will have a very low CPU overhead when played. Apple recommends that for the highest quality and lowest overhead you should use CAF files. For compressed files and the lowest overhead you should use IMA4 compression:

    (From the 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.

    You can convert to IMA4 using afconvert as well:

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