29

Playing a very short sound (~0.5s) produces a hiccup (like a lag) in my SpriteKit iOS game programmed in Swift. In other questions, I read that I should prepareToPlay() the sound, which I did.

I even used a variable (soundReady) to check if the sound is prepared before playing it. I also re-prepare the sound whenever it is finished playing (audioPlayerDidFinishPlaying()). Here are the relevant parts of the code:

class GameScene: SKScene, AVAudioPlayerDelegate {

   var splashSound = NSURL()
   var audioPlayer = AVAudioPlayer()
   var soundReady = false

   override func didMoveToView(view: SKView) {
      let path = NSBundle.mainBundle().pathForResource("plopSound", ofType: "m4a")
      splashSound = NSURL(fileURLWithPath: path)
      audioPlayer = AVAudioPlayer(contentsOfURL: splashSound, error: nil)
      audioPlayer.delegate = self
      soundReady = audioPlayer.prepareToPlay()
   }

   func playSound(){
      if(soundReady){
         audioPlayer.play()
         soundReady = false
      }
   }

   func audioPlayerDidFinishPlaying(player: AVAudioPlayer!, successfully flag: Bool){
      //Prepare to play after Sound finished playing
      soundReady = audioPlayer.prepareToPlay()
   }
}

I have no idea where I've gone wrong on this one. I feel like I have tried everything (including, but not limited to: only preparing once, preparing right after playing, not using a variable, but just prepareToPlay()).

Additional information:

  • The sound plays without delay.
  • How quickly the sound is played after the last finish does not seem to impact the lag.
Pang
  • 9,564
  • 146
  • 81
  • 122
Timme
  • 415
  • 4
  • 9
  • Doesn't DidMoveToView fire similar to UIViewController.ViewDidAppear() ? That is to say, it only fires after the scene is completely ready. The Apple documentation seems to be borked right now so I'll check in awhile. – Henry Oct 23 '14 at 15:24
  • 2
    Have you tried playing it on another thread? – mbo42 Nov 19 '14 at 12:54

3 Answers3

22

I ran into this same problem and played the sound in the backgroundQueue.

This is a good example: https://stackoverflow.com/a/25070476/586204.

let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(backgroundQueue, {
    audioPlayer.play()
})
Community
  • 1
  • 1
brilliantairic
  • 816
  • 11
  • 20
14

Just adding a Swift 3 version of the solution from @brilliantairic.

DispatchQueue.global().async {
    audioPlayer.play()
}
T R Bremm
  • 161
  • 1
  • 6
2

When I called play multiple times it would cause bad access. I believe the player is being deallocated since this is not thread safe. I created a serial queue to alleviate this problem.

class SoundManager {

    static let shared = SoundManager()

    private init() {
        try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
        try? AVAudioSession.sharedInstance().setActive(true)
    }

    private let serialQueue = DispatchQueue(label: "SoundQueue", qos: .userInitiated)
    private var player: AVAudioPlayer?

    static func play(_ sound: Sound) {
        shared.play(sound)
    }

    func play(_ sound: Sound) {
        guard let url = Bundle.main.url(forResource: sound.fileName, withExtension: "mp3")
            else { return }

        do {
            try serialQueue.sync {
                self.player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileTypeMPEGLayer3)
                DispatchQueue.main.async {
                    self.player?.play()
                }
            }
        } catch let error as NSError {
            print("error: \(error.localizedDescription)")
        }
    }

}
Mocha
  • 2,035
  • 12
  • 29
  • I've got a sound-heavy game and T.R. Bremm's (brilliantairic's) solution worked fine for me on both macOS and iOS without the extra serial queue framework. – AbePralle Jun 11 '21 at 19:50