Sound causing game to lag in swift sprite kit game? - swift

New code
class SceneTwo: SKScene, SKPhysicsContactDelegate {
let flap = SKAction.playSoundFileNamed("flap.caf", waitForCompletion: false)
let whack = SKAction.playSoundFileNamed("whack.caf", waitForCompletion: false)
let tap = SKAction.playSoundFileNamed("tap.caf", waitForCompletion: false)
Then I simply have put
run(tap)
run(flap) etc
where necessary..
Hi just wondering if I am using the correct coding to play sounds in my game. For some context my game is similar to Flappy bird. One sound is played each time the screen is touched (when the bird has impulse upwards) the second sound is when the bird collects a coin in between each wall.
I have noticed that both of these sounds are causing my game to lag.
Below is my relative sound code for the game.
import AVFoundation
var flap: AVAudioPlayer?
var tap: AVAudioPlayer?
override func didMove(to view: SKView) {
tap?.prepareToPlay()
flap?.prepareToPlay()
func playFlap() {
let url = Bundle.main.url(forResource: "flap", withExtension: "caf")!
do {
flap = try AVAudioPlayer(contentsOf: url)
guard let flap = flap else { return }
flap.play()
} catch let error {
print(error.localizedDescription)
}
}
func playTap() {
let url = Bundle.main.url(forResource: "tap", withExtension: "caf")!
do {
tap = try AVAudioPlayer(contentsOf: url)
guard let tap = tap else { return }
tap.play()
} catch let error {
print(error.localizedDescription)
}
}
After this I have simply
playTap()
playFlap()
to where they are needed.
The sound is clear it just seems to make my spawning walls jump a little bit when the sound is made.
Is there something I am doing that is wrong?

You are getting lag because you are not preloading the sound files. You can preload them at App Launch, and then when you need just play them. For reference look into this stackoverflow's post
And if you still face the same issue then you can add sound in background queue, as demostrated here
let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(backgroundQueue, {
audioPlayer.play()
})

Related

Audio not playing in SwiftUI

I'm having an issue getting an audio file to play when the function is called.
I am using an AVAudioPlayer to try and play the file following the instructions form here:
https://www.hackingwithswift.com/example-code/media/how-to-play-sounds-using-avaudioplayer
After the button is pressed in the view, it calls a func to play the sound, but from what I can tell, nothing is played. There are no errors thrown, and the file is found. The app also uses a speech synthesizer when a button is pushed, and that plays fine.
I looked around stack overflow and followed the instructions from here:
Where to place code for audio playback in a SwiftUI app
But still the audio is not played when the button is pushed.
Here is the func:
func playSound() {
var sound = AVAudioPlayer()
if let path = Bundle.main.path(forResource: "TryAgain", ofType: "wav") {
do {
sound = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
print("Playing sound")
sound.play()
} catch {
print( "Could not find file")
}
}
Here is the class:
class Player: BindableObject {
let willChange = PassthroughSubject<Player, Never>()
var isPlaying: Bool = false {
willSet {
willChange.send(self)
}
}
func playSound() {
var sound = AVAudioPlayer()
if let path = Bundle.main.path(forResource: "TryAgainWav", ofType: "wav") {
do {
sound = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
print("Playing sound")
sound.play()
} catch {
print( "Could not find file")
}
}
}
}
Update: Not sure if this helps, but I built this in with IB instead of SwiftUI and noticed the same message is printed in the console when I click the button to play the audio file:
2019-07-22 11:29:41.075568-0700 PlaySoundPrac[13952:46549155] [plugin] AddInstanceForFactory: No factory registered for id F8BB1C28-BAE8-11D6-9C31-00039315CD46
Any help would be greatly appreciated
I'm pretty new myself, but I'll do my best. What happens if you move the declaration of AVAudioPlayer outside the function? For example:
import Combine
class Player: ObservableObject {
var sound: AVAudioPlayer!
let willChange = PassthroughSubject<Player, Never>()
var isPlaying: Bool = false {
willSet {
willChange.send(self)
}
}
func playSound() {
if let path = Bundle.main.path(forResource: "TryAgainWav", ofType: "wav") {
do {
sound = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
print("Playing sound")
sound.play()
} catch {
print( "Could not find file")
}
}
}
}
Try this out and let me know if it works or not! Thanks.
I had a problem that audio was not played on iOS 13 Simulator in Xcode 11.5 while it was okay in Xcode SwiftUI preview and physical device.

How to loop background music without lag/delay when song repeats (seamless)

I am trying to make a game with background music looping in it. I made the song file in Adobe Audition (which is similar to audacity) and when I play it in a loop in Adobe Audition it loops how I want it.
When I play it in Xcode however, it has a lag in between the loops. I am using AVFoundations for the sound playing.
I have searched everywhere but I can't find the solution for the problem.
Is there any way you can loop audio files without there being any lags in between ? (I believe its called "seamless looping" )
here is the code:
class GameScene: SKScene {
...// Other Code
var ButtonAudio = URL(fileURLWithPath: Bundle.main.path(forResource: "Gamescene(new)", ofType: "mp3")!)
var ButtonAudioPlayer = AVAudioPlayer()
... //Other Code
}
And When I Call it:
override func didMove(to view: SKView) {
...//Code
ButtonAudioPlayer = try! AVAudioPlayer(contentsOf: ButtonAudio, fileTypeHint: nil)
ButtonAudioPlayer.numberOfLoops = -1
ButtonAudioPlayer.prepareToPlay()
ButtonAudioPlayer.play()
...//More Code
}
Can someone help me with this issue ?
Thank you in advance!
You can use AVPlayerLooper and AVQueuePlayer to do this.
import UIKit
import AVFoundation
class ViewController: UIViewController {
var queuePlayer = AVQueuePlayer()
var playerLooper: AVPlayerLooper?
override func viewDidLoad() {
super.viewDidLoad()
guard let url = Bundle.main.url(forResource: "Gamescene(new)", withExtension: "mp3") else { return }
let playerItem = AVPlayerItem(asset: AVAsset(url: url))
playerLooper = AVPlayerLooper(player: queuePlayer, templateItem: playerItem)
queuePlayer.play()
}
}
The solution proposed by #dave234 only works in iOS > 10. Since I needed to make seamless playback in iOS > 9, I did something different:
Instead of AVPlayer, I created AVQueuePlayer and immediately added two identical melodies to the queue.
Next, I made a listener to the penultimate melody.
When the listener was triggered, I added another similar record to the queue after the last one.
In fact, in order to avoid delay, I always play the penultimate record.
My code:
var player: AVQueuePlayer?
override func viewDidLoad() {
super.viewDidLoad()
if let path = Bundle.main.path(forResource: "music_file", ofType: "mp3") {
player = createPlayer(url: URL(fileURLWithPath: path))
}
}
func createPlayer(url: URL) -> AVQueuePlayer {
let player = AVQueuePlayer(items: [AVPlayerItem(url: url), AVPlayerItem(url: url)])
loopPlayer(playerItem: player.items()[player.items().count - 2])
return player
}
func loopPlayer(playerItem: AVPlayerItem) {
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: playerItem, queue: .main) { _ in
if let player = self.player, let url = (playerItem.asset as? AVURLAsset)?.url {
player.insert(AVPlayerItem(url: url), after: player.items()[player.items().count - 1])
self.loopPlayer(playerItem: player.items()[player.items().count - 2])
}
}
}

SpriteKit game crashes when two SKPhysicsContacts are detected

I have a SpriteKit game where you must shoot down asteroids before they hit the base. But if you shoot and hit the asteroid and/or the base when the asteroid is hitting the base, the program crashes.
Here's my physics contact code.
public func didBegin(_ contact: SKPhysicsContact) {
//Asteroid is object 1, other item is object 2
var object1 = SKSpriteNode()
var object2 = SKSpriteNode()
//Test for asteroid/projectile contact, then remove appropriate sprites, change game values and play sounds
if contact.bodyA.contactTestBitMask == ColliderType.Asteroid.rawValue && contact.bodyB.contactTestBitMask == ColliderType.Asteroid.rawValue{
if contact.bodyA.categoryBitMask == ColliderType.Asteroid.rawValue{
}else if contact.bodyB.categoryBitMask == ColliderType.Asteroid.rawValue{
object2 = contact.bodyA.node as! SKSpriteNode
object1 = contact.bodyB.node as! SKSpriteNode
let explosionPath = URL(fileURLWithPath: Bundle.main.path(forResource: "astd", ofType: "m4a")!)
do {
audioPlayer = try AVAudioPlayer(contentsOf: explosionPath)
} catch {
print("error")
}
audioPlayer.prepareToPlay()
audioPlayer.play()
}
if object2.physicsBody?.categoryBitMask == ColliderType.Object.rawValue{
object1.removeAllActions()
object1.removeFromParent()
let hitPath = URL(fileURLWithPath: Bundle.main.path(forResource: "craftHit", ofType: "mp3")!)
do {
audioPlayer = try AVAudioPlayer(contentsOf: hitPath)
} catch {
print("error")
}
audioPlayer.prepareToPlay()
audioPlayer.play()
reduceHealthBy(num: 0.075)
}else if object2.physicsBody?.categoryBitMask == ColliderType.Projectile.rawValue{
object1.removeAllActions()
object1.removeFromParent()
object2.removeAllActions()
object2.removeFromParent()
score += 1
}
}
}
Sounds like a duplicate of Optional unwrapping SKPhysics error
SK is generating multiple contact events for one actual contact and so didBegin() is being called multiple times.
On the first call to didBegin you handle the contact and remove one or mode nodes etc, and on the second call to didBegin (with the same contact: argument) the nodes you removed are now nil, so when you try to remove or reference them you get a crash.
There are several ways to handle this such as adding the nodes to be removed to a set and then removing them in didFinishUpdate() or checking if a node is nil and then returning if it is etc.
Trying to get SK to NOT generate the multiple contacts currently seems unachievable.

Ringer volume slider, swift

so my problem is that I'm creating an app which uses local notifications as an alarm. The only issue is that if the device has its ringer volume down or on silent, it will not play any sound. I am aware that you are unable to do this in the background however is it possible to have a slider linked up to the ringer volume (like a slider for MPVolume)? Also is it possible to check whether the device is muted? Again I just want to point out I am aware that you cannot simply change it without the user knowing but I was wondering whether the two methods stated earlier are possible. Any help on the topic will be greatly appreciated.
It is not possible to detect if an iPhone has its silence switch on or to change the "Ringer" (Not Volume) as Apple does not provide access to them.
There used to be a workaround a while back using Objective-C but I'm not quite sure if it still works, you are welcome to try if you want to.
AVSystemController
Also is it possible to check whether the device is muted?
This issue is really frustrating and I can't believe Apple makes it so hard :(
Checking the volume itself is rather simple:
let audioSession = AVAudioSession.sharedInstance()
let volume: Float = audioSession.outputVolume
If volume is 0.0, it's silent. But the real problem is the ringer switch.
My solution is to play a silent sound (called "silence.mp3" which is 1.5 sec long and all silent) and check after 0.5 sec if it's still being played.
This is totally inspired by the SoundSwitch: https://github.com/moshegottlieb/SoundSwitch
In usage:
MyAudioPlayer.isLoudCheck()
{
isLoud in
print("%%% - device is loud = \(isLoud)")
}
I changed the audio player class to this (usually I just use it to play sound files):
import AVFoundation
class MyAudioPlayer: NSObject, AVAudioPlayerDelegate
{
private static let sharedPlayer: MyAudioPlayer =
{
return MyAudioPlayer()
}()
public var container = [String : AVAudioPlayer]()
static func isLoudCheck(completionHandler: #escaping (Bool?) -> ())
{
let name = "silence"
let key = name
var player: AVAudioPlayer?
for (file, thePlayer) in sharedPlayer.container
{
if file == key
{
player = thePlayer
break
}
}
if player == nil, let resource = Bundle.main.path(forResource: name, ofType: "mp3")
{
do
{
player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: resource), fileTypeHint: AVFileTypeMPEGLayer3)
}
catch
{
print("%%% - audio error?")
}
}
if let thePlayer = player
{
print("%%% - the player plays")
thePlayer.delegate = sharedPlayer
sharedPlayer.container[key] = thePlayer
thePlayer.play()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute:
{
if let thePlayer = player
{
if thePlayer.isPlaying
{
print("%%% - check: playing")
completionHandler(true)
}
else
{
print("%%% - check: not playing")
completionHandler(false)
}
}
})
}
static func isPlaying(key: String) -> Bool?
{
return sharedPlayer.container[key]?.isPlaying
}
}
Hope this helps someone :)

How to play the same sound overlapping with AVAudioPlayer?

This code plays the sound when the button is tapped but cancels the previous if it is pressed again. I do not want this to happen I want the same sound to overlap when repeatedly pressed. I believe it might be due to using the same AVAudioPlayer as I have looked on the internet but I am new to swift and want to know how to create a new AVAudioPlayer everytime the method runs so the sounds overlap.
func playSound(sound:String){
// Set the sound file name & extension
let soundPath = NSURL(fileURLWithPath:NSBundle.mainBundle().pathForResource(sound, ofType: "mp3")!)
do {
//Preperation
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
} catch _{
}
do {
try AVAudioSession.sharedInstance().setActive(true)
}
catch _ {
}
//Play the sound
var error:NSError?
do{
audioPlayer = try AVAudioPlayer(contentsOfURL: soundPath)
}catch let error1 as NSError {
error = error1
}
audioPlayer.prepareToPlay()
audioPlayer.play()
}
To play two sounds simultaneously with AVAudioPlayer you just have to use a different player for each sound.
In my example I've declared two players, playerBoom and playerCrash, in the Viewcontroller, and I'm populating them with a sound to play via a function, then trigger the play at once:
import AVFoundation
class ViewController: UIViewController {
var playerBoom:AVAudioPlayer?
var playerCrash:AVAudioPlayer?
override func viewDidLoad() {
super.viewDidLoad()
playerBoom = preparePlayerForSound(named: "sound1")
playerCrash = preparePlayerForSound(named: "sound2")
playerBoom?.prepareToPlay()
playerCrash?.prepareToPlay()
playerBoom?.play()
playerCrash?.play()
}
func preparePlayerForSound(named sound: String) -> AVAudioPlayer? {
do {
if let soundPath = NSBundle.mainBundle().pathForResource(sound, ofType: "mp3") {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
return try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: soundPath))
} else {
print("The file '\(sound).mp3' is not available")
}
} catch let error as NSError {
print(error)
}
return nil
}
}
It works very well but IMO is not suitable if you have many sounds to play. It's a perfectly valid solution for just a few ones, though.
This example is with two different sounds but of course the idea is exactly the same for two identic sounds.
I could not find a solution using just AVAudioPlayer.
Instead, I have found a solution to this problem with a library that is built on top of AVAudioPlayer.
The library allows same sounds to be played overlapped with each other.
https://github.com/adamcichy/SwiftySound