iOS, swift: play background music and sound effects without delay

4.2k views Asked by At

I am trying to create a game in iOS without using SpriteKit. I am stuck in getting the sound effects to play in a timely manner. I've been using the following code which I have found online and the background music plays great. However, when I use the "playSoundEffect" method it plays ok the first time but then starts to lag behind and becomes out of sync. I guess that happens because it initializes an AVAudioPlayer every time. Anyone have a good idea in how to play sound effects in a timely manner, while also playing background music? Thanks!

import AVFoundation

public class SKTAudio: NSObject, AVAudioPlayerDelegate {
public var backgroundMusicPlayer: AVAudioPlayer?
public var soundEffectPlayer: AVAudioPlayer?

private var mainLoopFileName:String! {

    let randomSong = Int(arc4random_uniform(3))

    switch randomSong {
    //case 0: return "Test.mp3"
    //case 1: return "Test2.mp3"
    case 0: return "SneakySnitch.mp3"
    case 1: return "FasterDoesIt.mp3"
    case 2: return "MonkeysSpinningMonkeys.mp3"
    default:
        break
    }

    return "SneakySnitch.mp3"
}

public class func sharedInstance() -> SKTAudio {
    return SKTAudioInstance
}

public func playBackgroundMusic() {
    let filename = mainLoopFileName
    let url = NSBundle.mainBundle().URLForResource(filename, withExtension: nil)
    if (url == nil) {
        println("Could not find file: \(filename)")
        return
    }

    var error: NSError? = nil
    backgroundMusicPlayer = AVAudioPlayer(contentsOfURL: url, error: &error)
    if let player = backgroundMusicPlayer {
        player.numberOfLoops = 0
        player.delegate = self
        player.prepareToPlay()
        player.play()
    } else {
        println("Could not create audio player: \(error!)")
    }
}

public func pauseBackgroundMusic() {
    if let player = backgroundMusicPlayer {
        if player.playing {
            player.pause()
        }
    }
}

public func resumeBackgroundMusic() {
    if let player = backgroundMusicPlayer {
        if !player.playing {
            player.play()
        }
    }
}

public func playSoundEffect(filename: String) {
    let url = NSBundle.mainBundle().URLForResource(filename, withExtension: nil)
    if (url == nil) {
        println("Could not find file: \(filename)")
        return
    }

    var error: NSError? = nil
    soundEffectPlayer = AVAudioPlayer(contentsOfURL: url, error: &error)
    if let player = soundEffectPlayer {
        player.numberOfLoops = 0
        player.prepareToPlay()
        player.play()
    } else {
        println("Could not create audio player: \(error!)")
    }
}

// MARK: AVAudioPlayerDelegate
public func audioPlayerDidFinishPlaying(player: AVAudioPlayer!, successfully flag: Bool) {
    println("finished playing \(flag)")

    delay(5.0, {
        self.playBackgroundMusic()
    })
}

public func audioPlayerDecodeErrorDidOccur(player: AVAudioPlayer!, error: NSError!) {
    println("\(error.localizedDescription)")
}

}
2

There are 2 answers

0
jlw On

You could using AVPlayer to play your sound file. Keep one player, but change its AVPlayerItem to a new item when you need to play a new sound. It might be faster than recreating the player every time.

While AVAudioPlayer/AVPlayer is the simplest option, it will not give you the shortest delay or perfect synchronization when playing audio files. You should look into Audio Queues or Audio Units within Core Audio for more accurate sound playback.

0
Nicolas Yuste On

The problem is that you are playing the second sound effect before the first one is finished. You are killing the reference to the first when you set the new AVAudioPlayer to your soundEffectPlayer variable and the first will stop playing.

If you don't mind loosing the availability of controlling the volume of the sound you could use this:

var mySound: SystemSoundID = 0
AudioServicesCreateSystemSoundID(url, &mySound)
// Play
AudioServicesPlaySystemSound(mySound)

Otherwise you can use AVAudioPlayer if you add each sound that you create into an array, keeping the reference. Then you can delete it when the sound is done by implementing AVAudioPlayer delegate.

func playSound(){
    let path : NSString? = NSBundle.mainBundle().pathForResource("sound", ofType: "wav")!
    let url = NSURL(fileURLWithPath: path!)

    var error : NSError?
    let sound = AVAudioPlayer(contentsOfURL: url, error: &error)
    sound.delegate = self
    sound.volume = 0.5

    self.soundsEffectsArray.addObject(sound)
    sound.prepareToPlay()
    sound.play()
}

func audioPlayerDidFinishPlaying(player: AVAudioPlayer!, successfully flag: Bool) {
    self.soundsEffectsArray.removeObject(player)
}