Search code examples
iosaudiouigesturerecognizeravaudioplayer

AVAudioPlayer and AudioServices both delay sound if >1 s between plays


I want a sound to play with negligible delay when a user taps down on a view. I've tried this with both AVAudioPlayer and AudioServices, but I'm experiencing the same issue with both: a slight delay if there is more than about a second between taps. In other words, here's what happens:

  • Tap many times in quick succession and only the first tap is delayed,
  • Pause for about a second or longer,
  • Tap many times again (or just once) and again only the first tap is delayed.

The delay we're talking about is short, maybe 100 ms or so, but enough so that the playback doesn't feel/sound instantaneous like the others. Here's the AVAudioPlayer version of the code:

import UIKit
import AVFoundation

class ViewController: UIViewController {
    var player:AVAudioPlayer!

    override func viewDidLoad() {
        super.viewDidLoad()

        let soundUrl = Bundle.main.url(forResource: "short_sound", withExtension: "wav")!

        player = try! AVAudioPlayer(contentsOf: soundUrl)
        player.volume = 0 // "Prime the pump" 
        player.play()

        let r = TouchDownGestureRecognizer(target: self, action: #selector(tapped(_:)))
        self.view.addGestureRecognizer(r)
    }

    func tapped(_ gesture: TouchDownGestureRecognizer){
        player.volume = 1
        player.play()
    }
}

And here's the AudioServices version. In this case, there are other posts suggesting that a silent sound could be played during initialization to "prime the pump" but that would only address the first sound. This issue affects all sounds that occur after a 1+ second delay.

import UIKit
import AudioToolbox

class ViewController: UIViewController {
    var soundID:SystemSoundID = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        let soundUrl = Bundle.main.url(forResource: "short_sound", withExtension: "wav")!
        AudioServicesCreateSystemSoundID(soundUrl as CFURL, &soundID)

        let r = TouchDownGestureRecognizer(target: self, action: #selector(tapped(_:)))
        self.view.addGestureRecognizer(r)
    }

    func tapped(_ gesture: TouchDownGestureRecognizer){
        AudioServicesPlaySystemSound(soundID)
    }
}

In both cases, this "touch down" recognizer (by @le-sang) is used:

import Foundation
import UIKit.UIGestureRecognizerSubclass

class TouchDownGestureRecognizer: UIGestureRecognizer {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        if self.state == .possible {
            self.state = .recognized
        }
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        self.state = .failed
    }
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        self.state = .failed
    }
}

I've also tried using a UILongPressGestureRecognizer with a minimumPressDuration=0 and the same thing happens. Also, same thing if I replace the UIView with a UIControl and setup a target/action. Any insights would be much appreciated, thanks for reading.


Solution

  • Here's the fix I found. I'm not totally happy with this solution (still not sure why this was happening), but it does indeed work for both players.

    The idea is simple: if you leave either player alone for a second or so, it starts to nap and then takes a moment to wake up when you need it. So, the solution is to "nudge" the player at regular intervals so it doesn't start napping.

    For the AVAudioPlayer case:

    import UIKit
    import AVFoundation
    
    class ViewController: UIViewController {
    
        var player:AVAudioPlayer!
        var ghostPlayer:AVAudioPlayer!
    
        var timer:Timer!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let soundUrl = Bundle.main.url(forResource: "short_sound", withExtension: "wav")!
    
            player = try! AVAudioPlayer(contentsOf: soundUrl)
    
            ghostPlayer = try! AVAudioPlayer(contentsOf: soundUrl)
            ghostPlayer.volume = 0
            ghostPlayer.play()
    
            timer = Timer.scheduledTimer(timeInterval: 0.5,
                                         target: self,
                                         selector: #selector(nudgeAudio),
                                         userInfo: nil,
                                         repeats: true)
    
            let r = TouchDownGestureRecognizer(target: self, action: #selector(tapped(_:)))
            self.view.addGestureRecognizer(r)
        }
    
        func tapped(_ gesture: TouchDownGestureRecognizer){
            player.play()
        }
    
        func nudgeAudio() {
            ghostPlayer.play()
        }
    }
    

    And for AudioServices:

    import UIKit
    import AudioToolbox
    
    class ViewController: UIViewController {
        var soundID:SystemSoundID = 0
        var ghostID:SystemSoundID = 1
    
        var timer:Timer!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let soundUrl = Bundle.main.url(forResource: "short_sound", withExtension: "wav")!
            AudioServicesCreateSystemSoundID(soundUrl as CFURL, &soundID)
    
            let ghostUrl = Bundle.main.url(forResource: "silence", withExtension: "wav")!
            AudioServicesCreateSystemSoundID(ghostUrl as CFURL, &ghostID)
    
            timer = Timer.scheduledTimer(timeInterval: 0.5,
                                         target: self,
                                         selector: #selector(nudgeAudio),
                                         userInfo: nil,
                                         repeats: true)
    
            let r = TouchDownGestureRecognizer(target: self, action: #selector(tapped(_:)))
            self.view.addGestureRecognizer(r)
        }
    
        func tapped(_ gesture: TouchDownGestureRecognizer){
            AudioServicesPlaySystemSound(soundID)
        }
    
        func nudgeAudio() {
            AudioServicesPlaySystemSound(ghostID)
        }
    }