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

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

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.

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

Related

iOS: audio files used in app sound harsher than when played outside the app

The audio files for our iOS app sound much worse when played through the app than when played outside the app. The sounds seem harsher and more "ecohy".
Here's our code for playing audio. Are we somehow altering the playback or natural sound of the audio?
Two of the audio files we're using can be found here:
private func createAudioPlayer(filename: String) -> AVAudioPlayer {
// Define file URL
let path = Bundle.main.path(forResource: filename, ofType: nil)
let url = URL(fileURLWithPath: path!)
// Create player
let audioPlayer: AVAudioPlayer!
do {
audioPlayer = try AVAudioPlayer(contentsOf: url)
} catch {
audioPlayer = nil
printError("Error creating audio player for \(url): \(error)")
logEvent("Audio Error", userData: nil)
}
// Print status
print("Created audio player for \(filename)")
// Return player
return audioPlayer
}
func play(file: AudioFileEnum) {
if let player = audioPlayers[file] {
if player.isPlaying {
player.pause()
}
player.currentTime = 0
player.play()
} else {
printError("Error finding audio player for \(file)")
}
}
Have you tried setting the volume on the audioPlayer object to something smaller, like 0.05f, and adjusting from there?

Get AVAudioPlayer to play multiple sounds at a time

I'm trying to get multiple sounds files to play on an AVAudioPlayer instance, however when one sound plays, the other stops. I can't get more than one sound to play at a time. Here is my code:
import AVFoundation
class GSAudio{
static var instance: GSAudio!
var soundFileNameURL: NSURL = NSURL()
var soundFileName = ""
var soundPlay = AVAudioPlayer()
func playSound (soundFile: String){
GSAudio.instance = self
soundFileName = soundFile
soundFileNameURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(soundFileName, ofType: "aif", inDirectory:"Sounds")!)
do{
try soundPlay = AVAudioPlayer(contentsOfURL: soundFileNameURL)
} catch {
print("Could not play sound file!")
}
soundPlay.prepareToPlay()
soundPlay.play ()
}
}
Can anyone please help me by telling me how to get more than one sound file to play at a time? Any help is much appreciated.
Many thanks,
Kai
The reason the audio stops is because you only have one AVAudioPlayer set up, so when you ask the class to play another sound you are currently replacing the old instance with a new instance of AVAudioPlayer. You are overwriting it basically.
You can either create two instances of the GSAudio class, and then call playSound on each of them, or make the class a generic audio manager that uses a dictionary of audioPlayers.
I much prefer the latter option, as it allows for cleaner code and is also more efficient. You can check to see if you have already made a player for the sound before, rather than making a new player for example.
Anyways, I re-made your class for you so that it will play multiple sounds at once. It can also play the same sound over itself (it doesn't replace the previous instance of the sound) Hope it helps!
The class is a singleton, so to access the class use:
GSAudio.sharedInstance
for example, to play a sound you would call:
GSAudio.sharedInstance.playSound("AudioFileName")
and to play a number of sounds at once:
GSAudio.sharedInstance.playSounds("AudioFileName1", "AudioFileName2")
or you could load up the sounds in an array somewhere and call the playSounds function that accepts an array:
let sounds = ["AudioFileName1", "AudioFileName2"]
GSAudio.sharedInstance.playSounds(sounds)
I also added a playSounds function that allows you to delay each sound being played in a cascade kind of format. So:
let soundFileNames = ["SoundFileName1", "SoundFileName2", "SoundFileName3"]
GSAudio.sharedInstance.playSounds(soundFileNames, withDelay: 1.0)
would play sound2 a second after sound1, then sound3 would play a second after sound2 etc.
Here is the class:
class GSAudio: NSObject, AVAudioPlayerDelegate {
static let sharedInstance = GSAudio()
private override init() {}
var players = [NSURL:AVAudioPlayer]()
var duplicatePlayers = [AVAudioPlayer]()
func playSound (soundFileName: String){
let soundFileNameURL = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(soundFileName, ofType: "aif", inDirectory:"Sounds")!)
if let player = players[soundFileNameURL] { //player for sound has been found
if player.playing == false { //player is not in use, so use that one
player.prepareToPlay()
player.play()
} else { // player is in use, create a new, duplicate, player and use that instead
let duplicatePlayer = try! AVAudioPlayer(contentsOfURL: soundFileNameURL)
//use 'try!' because we know the URL worked before.
duplicatePlayer.delegate = self
//assign delegate for duplicatePlayer so delegate can remove the duplicate once it's stopped playing
duplicatePlayers.append(duplicatePlayer)
//add duplicate to array so it doesn't get removed from memory before finishing
duplicatePlayer.prepareToPlay()
duplicatePlayer.play()
}
} else { //player has not been found, create a new player with the URL if possible
do{
let player = try AVAudioPlayer(contentsOfURL: soundFileNameURL)
players[soundFileNameURL] = player
player.prepareToPlay()
player.play()
} catch {
print("Could not play sound file!")
}
}
}
func playSounds(soundFileNames: [String]){
for soundFileName in soundFileNames {
playSound(soundFileName)
}
}
func playSounds(soundFileNames: String...){
for soundFileName in soundFileNames {
playSound(soundFileName)
}
}
func playSounds(soundFileNames: [String], withDelay: Double) { //withDelay is in seconds
for (index, soundFileName) in soundFileNames.enumerate() {
let delay = withDelay*Double(index)
let _ = NSTimer.scheduledTimerWithTimeInterval(delay, target: self, selector: #selector(playSoundNotification(_:)), userInfo: ["fileName":soundFileName], repeats: false)
}
}
func playSoundNotification(notification: NSNotification) {
if let soundFileName = notification.userInfo?["fileName"] as? String {
playSound(soundFileName)
}
}
func audioPlayerDidFinishPlaying(player: AVAudioPlayer, successfully flag: Bool) {
duplicatePlayers.removeAtIndex(duplicatePlayers.indexOf(player)!)
//Remove the duplicate player once it is done
}
}
Here's a Swift 4 version of #Oliver Wilkinson code with some safechecks and improved code formatting:
import Foundation
import AVFoundation
class GSAudio: NSObject, AVAudioPlayerDelegate {
static let sharedInstance = GSAudio()
private override init() { }
var players: [URL: AVAudioPlayer] = [:]
var duplicatePlayers: [AVAudioPlayer] = []
func playSound(soundFileName: String) {
guard let bundle = Bundle.main.path(forResource: soundFileName, ofType: "aac") else { return }
let soundFileNameURL = URL(fileURLWithPath: bundle)
if let player = players[soundFileNameURL] { //player for sound has been found
if !player.isPlaying { //player is not in use, so use that one
player.prepareToPlay()
player.play()
} else { // player is in use, create a new, duplicate, player and use that instead
do {
let duplicatePlayer = try AVAudioPlayer(contentsOf: soundFileNameURL)
duplicatePlayer.delegate = self
//assign delegate for duplicatePlayer so delegate can remove the duplicate once it's stopped playing
duplicatePlayers.append(duplicatePlayer)
//add duplicate to array so it doesn't get removed from memory before finishing
duplicatePlayer.prepareToPlay()
duplicatePlayer.play()
} catch let error {
print(error.localizedDescription)
}
}
} else { //player has not been found, create a new player with the URL if possible
do {
let player = try AVAudioPlayer(contentsOf: soundFileNameURL)
players[soundFileNameURL] = player
player.prepareToPlay()
player.play()
} catch let error {
print(error.localizedDescription)
}
}
}
func playSounds(soundFileNames: [String]) {
for soundFileName in soundFileNames {
playSound(soundFileName: soundFileName)
}
}
func playSounds(soundFileNames: String...) {
for soundFileName in soundFileNames {
playSound(soundFileName: soundFileName)
}
}
func playSounds(soundFileNames: [String], withDelay: Double) { //withDelay is in seconds
for (index, soundFileName) in soundFileNames.enumerated() {
let delay = withDelay * Double(index)
let _ = Timer.scheduledTimer(timeInterval: delay, target: self, selector: #selector(playSoundNotification(_:)), userInfo: ["fileName": soundFileName], repeats: false)
}
}
#objc func playSoundNotification(_ notification: NSNotification) {
if let soundFileName = notification.userInfo?["fileName"] as? String {
playSound(soundFileName: soundFileName)
}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if let index = duplicatePlayers.index(of: player) {
duplicatePlayers.remove(at: index)
}
}
}
I have created a helper library that simplifies playing sounds in Swift. It creates multiple instances of AVAudioPlayer to allow playing the same sound multiple times concurrently. You can download it from Github or import with Cocoapods.
Here is the link: SwiftySound
The usage is as simple as it can be:
Sound.play(file: "sound.mp3")
All answers are posting pages of code; it doesn't need to be that complicated.
// Create a new player for the sound; it doesn't matter which sound file this is
let soundPlayer = try AVAudioPlayer( contentsOf: url )
soundPlayer.numberOfLoops = 0
soundPlayer.volume = 1
soundPlayer.play()
soundPlayers.append( soundPlayer )
// In an timer based loop or other callback such as display link, prune out players that are done, thus deallocating them
checkSfx: for player in soundPlayers {
if player.isPlaying { continue } else {
if let index = soundPlayers.index(of: player) {
soundPlayers.remove(at: index)
break checkSfx
}
}
}
Swift 5+
Compiling some of the previous answers, improving code style and reusability
I usually avoid loose strings throughout my projects and use, instead, custom protocols for objects that will hold those string properties.
I prefer this to the enum approach simply because enumerations tend to couple your project together quite quickly. Everytime you add a new case you must edit the same file with the enumeration, breaking somewhat the Open-Closed principle from SOLID and increasing chances for error.
In this particular case, you could have a protocol that defines sounds:
protocol Sound {
func getFileName() -> String
func getFileExtension() -> String
func getVolume() -> Float
func isLoop() -> Bool
}
extension Sound {
func getVolume() -> Float { 1 }
func isLoop() -> Bool { false }
}
And when you need a new sound you can simply create a new structure or class that implements this protocol (It will even be suggested on autocomplete if your IDE, just like Xcode, supports it, giving you similar benefits to those of the enumeration... and it works way better in medium to large multi framework projects).
(Usually I leave volume and other configurations with default implementations as they are less frequently customized).
For instance, you could have a coin drop sound:
struct CoinDropSound: Sound {
func getFileName() -> String { "coin_drop" }
func getFileExtension() -> String { "wav" }
}
Then, you could use a singleton SoundManager that would take care of managing playing audio files
import AVFAudio
final class SoundManager: NSObject, AVAudioPlayerDelegate {
static let shared = SoundManager()
private var audioPlayers: [URL: AVAudioPlayer] = [:]
private var duplicateAudioPlayers: [AVAudioPlayer] = []
private override init() {}
func play(sound: Sound) {
let fileName = sound.getFileName()
let fileExtension = sound.getFileExtension()
guard let url = Bundle.main.url(forResource: fileName, withExtension: fileExtension),
let player = getAudioPlayer(for: url) else { return }
player.volume = sound.getVolume()
player.numberOfLoops = numberOfLoops
player.prepareToPlay()
player.play()
}
private func getAudioPlayer(for url: URL) -> AVAudioPlayer? {
guard let player = audioPlayers[url] else {
let player = try? AVAudioPlayer(contentsOf: url)
audioPlayers[url] = player
return player
}
guard player.isPlaying else { return player }
guard let duplicatePlayer = try? AVAudioPlayer(contentsOf: url) else { return nil }
duplicatePlayer.delegate = self
duplicateAudioPlayers.append(duplicatePlayer)
return duplicatePlayer
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
duplicateAudioPlayers.removeAll { $0 == player }
}
}
Here I created a helper getAudioPlayer to be able to return early from code execution and make use of the guard let.
Using guard let more often and preferring less nested code can, most of the time, highly improve readability.
To use this SoundManager from anywhere in your project, simply access its shared instance and pass an object that conforms to Sound.
For example, given the previous CoinDropSound:
SoundManager.shared.play(sound: CoinDropSound())
You could maybe omit the sound parameter as it may improve readability
class SoundManager {
// ...
func play(_ sound: Sound) {
// ...
}
// ...
}
And then:
SoundManager.shared.play(CoinDropSound())

Background Music Stop/Mute on iOS

I'm creating my first app. I have an app with music playing in the background with the following code:
var backgroundMusicPlayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
//background Music
func playBackgroundMusic(filename: String) {
let url = NSBundle.mainBundle().URLForResource(filename, withExtension: nil)
guard let newURL = url else {
print("Could not find file: \(filename)")
return
}
do {
backgroundMusicPlayer = try AVAudioPlayer(contentsOfURL: newURL)
backgroundMusicPlayer.numberOfLoops = -1
backgroundMusicPlayer.prepareToPlay()
backgroundMusicPlayer.play()
} catch let error as NSError {
print(error.description)
}
}
playBackgroundMusic("Starship.wav")
}
So what should I do in order to stop/mute the background music when I switch to another ViewController? Should I do this my FirstViewController or SecondViewController?
Obviously, I don't want the sound to be off in the SecondViewController as I have other stuff that will be playing there.
To mute sound I simply mute the volume.
backgroundMusicPlayer.volume = 0
and set it to normal if I want sound
backgroundMusicPlayer.volume = 1
If you just want to pause music you can call
backgroundMusicPlayer.pause()
To resume you call
backgroundMusicPlayer.resume()
If you want to stop music and reset it to the beginning you say this
backgroundMusicPlayer.stop()
backgroundMusicPlayer.currentTime = 0
backgroundMusicPlayer.prepareToPlay()
Did you also consider putting your music into a singleton class so its easier to play music in your different viewControllers.
Not sure this is what you are looking for as your question is a bit vague.

Background Music Restarting

I am programming a game which switches between SKScenes. When I start playing the background music from the start screen, switch to a new screen and back to the start screen the music starts playing again.
I can't find out how to run the music once and not restarting again and again. I could make a workaround with storing a bool in NSUserDefaults to check if the music is running but that seems like an awful workaround. There must be a more simple solution. I have attempted with static but that doesn't work.
Create a new empty swift file and add this code into it:
var backgroundMusicPlayer: AVAudioPlayer!
var musicOnPlayScene = Bool()
func playBackgroundMusic(filename: String) {
let url = NSBundle.mainBundle().URLForResource(filename, withExtension: nil)
if (url == nil) {
print("Could not find file: \(filename)")
return
}
var error: NSError? = nil
do {
backgroundMusicPlayer = try AVAudioPlayer(contentsOfURL: url!)
} catch let error1 as NSError {
error = error1
backgroundMusicPlayer = nil
}
if backgroundMusicPlayer == nil {
print("Could not create audio player: \(error!)")
return
}
backgroundMusicPlayer.numberOfLoops = -1
backgroundMusicPlayer.prepareToPlay()
backgroundMusicPlayer.play()
}
Now play your background music into start scene this way:
playBackgroundMusic("yourSong.mp3")
and when you switch a scene add this code into next scene's didMoveToView:
backgroundMusicPlayer.play()
This will resume your background music when you switch any scene.
Hope it will help.

How can I use a function in several UIView

In nomal when i want to playSound in a View i have to:
- import AVFoundation
- make 2 function for helper method:
// MARK: Audio Function
func configureAudioSession() {
self.audioSession = AVAudioSession.sharedInstance()
var categoryError:NSError?
var activeError:NSError?
// set category cho audio session
self.audioSession.setCategory(AVAudioSessionCategoryPlayback, error: &categoryError)
// set active cho audio session
var success = self.audioSession.setActive(true, error: &activeError)
if !success {
println("error making audio session active :\(activeError)")
}
}
func playSound(withName:String, ext:String) {
if let songPath = NSBundle.mainBundle().pathForResource(withName, ofType: ext) {
if let songURL = NSURL(fileURLWithPath: songPath) {
println("songURL: \(songURL)")
var songError:NSError?
// Tao audioplayer
self.player = AVAudioPlayer(contentsOfURL: songURL, error: &songError)
self.player.numberOfLoops = 0
self.player.play()
}
//
}
}
when i want to play sound i only call a function playsound.
But now, i want to playsound can be use in several View . I don't know how to that. I already tried to make a new Swift file call SoundFunction and add my helper playsound methods to my SoundFunction.swift:
import Foundation
import AVFoundation
class SoundFunction {
var audioSession:AVAudioSession!
var player:AVAudioPlayer!
func check() {
println("check")
}
func configureAudioSession() {
self.audioSession = AVAudioSession.sharedInstance()
var categoryError:NSError?
var activeError:NSError?
// set category cho audio session
self.audioSession.setCategory(AVAudioSessionCategoryPlayback, error: &categoryError)
// println("error: \(categoryError)")
// set active cho audio session
var success = self.audioSession.setActive(true, error: &activeError)
if !success {
println("error making audio session active :\(activeError)")
}
}
func playSound(withName:String, ext:String) {
// Lay song path
if let songPath = NSBundle.mainBundle().pathForResource(withName, ofType: ext) {
if let songURL = NSURL(fileURLWithPath: songPath) {
println("songURL: \(songURL)")
var songError:NSError?
// Tao audioplayer
self.player = AVAudioPlayer(contentsOfURL: songURL, error: &songError)
// println("songerror:\(songError)")
self.player.numberOfLoops = 0
self.player.play()
}
//
}
// Chuyen thanh URL
// println("songpath: \(songPath)")
}
}
and after that I come back to my Views, and tried to call function from SoundFunction.Swift:
SoundFunction.configureAudioSession()
but I got an error "Missing argument for parameter #1 in call"
I don't know how to call same function for several Views. Please help
With this line SoundFunction.configureAudioSession() you are trying to call a class function. Which is not the case here, that's why you get an error (even if the message don't mention that).
Even if you fix that with class func configureAudio... you'll still might have an issue where you'll sound will stop play once you'll be out of scope.
In order to do what you want, you should make a singleton. The code you wrote for your SoundFunction seems to be a good start. A slight modification will complete it.
Here is a Q/A of mine : Swift - AVAudioPlayer, sound doesn't play correctly
With my comment at the bottom :
SoundPlayer.swift : http://pastebin.com/eX4iv3S4 .
To be used like so : SoundPlayer.sharedInstance.playSound().
Currently it use only one sound (because that is what I wanted), but you could easily add a parameter to the playSound method for example.
Please, tell me if it helped you :)
Edit :
With my code you should : change the playSound method to add parameter(s) like you did in yours. Finally, make sure the category you apply to your AVAudioSession is the one that fits the most to you.
From #Ichamp's advice. I successfully create a new SoundPlayer.swift file and using SingleTon to share function and use it from everywhere i want:
import Foundation
import AVFoundation
class SoundPlayer {
// Singleton in order to access the player from 'everywhere'
class var sharedInstance : SoundPlayer {
struct Static {
static let instance : SoundPlayer = SoundPlayer()
}
return Static.instance
}
// Properties
var error:NSError?
var audioSession:AVAudioSession!
var player = AVAudioPlayer()
init() {
self.audioSession = AVAudioSession.sharedInstance()
var categoryError:NSError?
var activeError:NSError?
// set category cho audio session
self.audioSession.setCategory(AVAudioSessionCategoryPlayback, error: &categoryError)
// set active cho audio session
var success = self.audioSession.setActive(true, error: &activeError)
if !success {
println("error making audio session active :\(activeError)")
}
}
func playSound(withName:String, ext:String) {
// Lay song path
if let songPath = NSBundle.mainBundle().pathForResource(withName, ofType: ext) {
if let songURL = NSURL(fileURLWithPath: songPath) {
var songError:NSError?
// Tao audioplayer
self.player = AVAudioPlayer(contentsOfURL: songURL, error: &songError)
self.player.numberOfLoops = 0
self.player.play()
}
}
}
}
when i wan to use the function i will use:
SoundPlayer.sharedInstance.playSound("restart", ext: "wav")

Resources