Rogare
Rogare

Reputation: 3274

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:

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.

Upvotes: 0

Views: 242

Answers (1)

Rogare
Rogare

Reputation: 3274

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

Upvotes: 1

Related Questions