On XCODE 8/Swift 3 and Spritekit, i am playing background music (a 5 minute song), calling it from GameViewController's ViewDidLoad (from the parent of all the scenes, not from a specific GameScene), as I want it to play throughout scene changes without stopping. This happens without a problem.
But my problem is, how do i stop the background music at will, when I am inside a scene? Say when user gets to a specific score on the game on the 3rd scene? As i cannot access the methods of the parent file. Here is the code I used to call the music to play:
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var audioPlayer = AVAudioPlayer()
do {
audioPlayer = try AVAudioPlayer(contentsOf: URL.init(fileURLWithPath: Bundle.main.path(forResource: "music", ofType: "mp3")!))
audioPlayer.prepareToPlay()
} catch {
print (error)
}
audioPlayer.play()
Many thanks for any help
Why not create a music helper class that you can access from anywhere. Either the singleton way or a class with static methods. This should also make your code cleaner and easier to manage.
I would also split the setup method and the play method so that you do not set up the player each time you play the file.
e.g Singleton
class MusicManager {
static let shared = MusicManager()
var audioPlayer = AVAudioPlayer()
private init() { } // private singleton init
func setup() {
do {
audioPlayer = try AVAudioPlayer(contentsOf: URL.init(fileURLWithPath: Bundle.main.path(forResource: "music", ofType: "mp3")!))
audioPlayer.prepareToPlay()
} catch {
print (error)
}
}
func play() {
audioPlayer.play()
}
func stop() {
audioPlayer.stop()
audioPlayer.currentTime = 0 // I usually reset the song when I stop it. To pause it create another method and call the pause() method on the audioPlayer.
audioPlayer.prepareToPlay()
}
}
When your project launches just call the setup method
MusicManager.shared.setup()
Than from any where in your project you can say
MusicManager.shared.play()
to play the music.
To than stop it just call the stop method
MusicManager.shared.stop()
For a more feature rich example with multiple tracks check out my helper on GitHub
https://github.com/crashoverride777/SwiftyMusic
Hope this helps
Related
I'm new to swift development and building a simple app for the apple watch. I'd like a short sound to play when a button is tapped. Is this possible?
From what I understand the best way to do this is with AVFoundation and AVAudioPlayer. There seem to have been a lot of updates for this in the last few releases and I'm finding conflicting advice. Based on a few tutorials I've put this simple test together, but I'm getting a "Thread 1:Fatal error: Unexpectedly found nil when unwrapping an Optional value" Here is my code:
import WatchKit
import Foundation
import AVFoundation
var dogSound = AVAudioPlayer()
class InterfaceController: WKInterfaceController {
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
#IBAction func playTapped() {
let path = Bundle.main.path(forResource: "Dog", ofType: ".mp3")!
let url = URL(fileURLWithPath: path)
do {
dogSound = try AVAudioPlayer(contentsOf: url)
dogSound.play()
} catch {
//couldn't load file :(
}
}
}
My audio file is an mp3 named Dog in the Assets.xcassets folder in WatchKit Extension and I have added AVFoundation.framework to the Link Binary with Libraries
What am I doing wrong and is there a tutorial for the right way to implement this? Thanks a lot!
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.
At the moment I have two functions which are almost identical, but only one of them works and I can't figure out why.
The playSound() function works and uses the class's audioPlayer: AVAudioPlayer object to play one audio file at a time.
To make it so I could play multiple sounds simultaneously, I made a new function called playMySound() which creates its own instance of AVAudioPlayer instead of using the class's audioPlayer: AVAudioPlayer object.
The weird thing is, the sound plays fine when I call playSound(), but it doesn't play when I call playMySound().
Here's the code that works:
var audioPlayer: AVAudioPlayer!
override func viewDidLoad() {
super.viewDidLoad()
audioPlayer = AVAudioPlayer()
}
func playSound(audioFileName: String) {
if let sound = NSDataAsset(name: audioFileName) {
do {
try! AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try! AVAudioSession.sharedInstance().setActive(true)
try audioPlayer = AVAudioPlayer(data: sound.data)
audioPlayer.delegate = self
audioPlayer.play()
} catch {
print("Error playing sound: \(audioFileName)")
}
}
}
And the code that doesn't work:
func playMySound(audioFileName: String) {
if let sound = NSDataAsset(name: audioFileName) {
do {
try! AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try! AVAudioSession.sharedInstance().setActive(true)
var myAudioPlayer: AVAudioPlayer = AVAudioPlayer()
myAudioPlayer = try AVAudioPlayer(data: sound.data)
myAudioPlayer.delegate = self
// Calling prepareToPlay() doesn't seem to fix it
// myAudioPlayer.prepareToPlay()
print("Trying to play my audio player")
myAudioPlayer.play()
} catch {
print("Error playing sound: \(audioFileName)")
}
}
}
"Trying to play my audio player" gets outputted to the console, so it's definitely running through the function, but for some reason calling myAudioPlayer.play() doesn't seem to work.
Can anyone help? Thanks in advance!
I have a subclass of SKSpriteNode called 'backgroundMusic' that consists of a SKSpriteNode and a AVAudioPlayer file. The objective is to completely delete 'backgroundMusic' after i instantiate it. I try to do :
backgroundMusic.removeFromParent()
but it only removes the SKSpriteNode and not the AVAudioPlayer.
The main issue is that inside of this subclass, I call a bunch of functions within other functions, and when i try to empty the subclass by:
backgroundMusic = nil
the process of calling all the functions still is occurring and causes issues when i re-instantiate it. What i believe will work is if I delete 'backgroundMusic' completely, which will stop the function calling process, and then later re-instantiate it when i need to, it should work fine with no issues. How can I do this?
EDIT I tried:
self.delete(backgroundMusic)
and it crashed the application. Should I use this? If so how?
This happened because you havent configure Audio Session
Some code for playing:
import AVFoundation
var audioPlayer = AVAudioPlayer()
func playAudio() {
// Set the sound file name & extension
let alertSound = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("Flip 02", ofType: "wav")!)
// Preperation
try! AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, withOptions: AVAudioSessionCategoryOptions.MixWithOthers)
try! AVAudioSession.sharedInstance().setActive(true)
// Play the sound
do {
try audioPlayer = AVAudioPlayer(contentsOfURL: alertSound)
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print("there is \(error)")
}
}
Details from the docs:
AVAudioSessionCategoryOptionMixWithOthers
Mixes audio from this session with audio from other active sessions.
Valid only if the session category is
AVAudioSessionCategoryPlayAndRecord or AVAudioSessionCategoryPlayback.
(Implicit if the session category is AVAudioSessionCategoryAmbient.)
If you activate your session while using this option, your app’s audio
will not interrupt audio from other apps (such as the Music app). If
not using this option (or a category that is implicitly mixable),
activating your session will interrupt other nonmixable sessions.
To stop you can do:
do {
try AVAudioSession.sharedInstance().setActive(false)
} catch let error as NSError {
print(error.localizedDescription)
}
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