In my mainmenuviewcontroller in viewDidLoad, I tell it to play a background song. Problem is, when a user goes to another view and then goes back to the main menu, it starts playing the song again, and the two copies overlap. I tried to put this in the viewDidLoad but it didn't work
if themePlayer.playing == false {
themePlayer.prepareToPlay()
themePlayer.play()
}
It just kind of ignores the if condition and plays anyways. How can I fix this?
-viewDidLoad will only run when the view is initially loaded. Since your player is still playing the song when you navigate away from the mainmenuviewcontroller this indicates that the view controller is never being deallocated. Try putting the if condition in the -viewDidAppear or -viewWillAppear method.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated);
if themePlayer == nil {
// Either it's the first time this view is loaded, or themePlayer was deallocated
let soundURL = NSBundle.mainBundle().URLForResource("pres song", withExtension: "mp3")
themePlayer = AVAudioPlayer(contentsOfURL: soundURL, error: nil)
themePlayer.numberOfLoops = -1
themePlayer.volume = 0.1
themePlayer.prepareToPlay()
themePlayer.play()
}
if themePlayer.playing == false {
themePlayer.prepareToPlay()
themePlayer.play()
}
}
If in doubt in the future on if something is being called, use breakpoints and step through the code.
Related
I have an app that I'm adding sounds to. It has a keypad and when the user taps a button, the number animates to show the user that their press went through.
However, since both are happening on the main thread, adding the following code below, the play() function causes a slight delay in the animation. If the user waits ~2 seconds and taps a keypad number again, they see the delay again. So as long as they're hitting keypad numbers under 2s, they don't see another lag.
I tried wrapping the code below in a DispatchQueue.main.async {} block with no luck.
if let sound = CashSound(rawValue: "buttonPressMelody\(count)"),
let url = Bundle.main.url(forResource: sound.rawValue, withExtension: sound.fileType) {
self.audioPlayer = try? AVAudioPlayer(contentsOf: url)
self.audioPlayer?.prepareToPlay()
self.audioPlayer?.play()
}
How can I play this audio and have the animation run without them interfering and with the audio coinciding with the press?
Thanks
I experienced a rather similar problem in my SwiftUI app, and in my case the solution was in proper resource loading / AVAudioPlayer initialization and preparing.
I use the following function to load audio from disk (inspired by ImageStore class from Apple's Landmarks SwiftUI Tutorial)
final class AudioStore {
typealias Resources = [String:AVAudioPlayer]
fileprivate var audios: Resources = [:]
static var shared = AudioStore()
func audio(with name: String) -> AVAudioPlayer {
let index = guaranteeAudio(for: name)
return audios.values[index]
}
static func loadAudio(with name: String) -> AVAudioPlayer {
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else { fatalError("Couldn't find audio \(name).mp3 in main bundle.") }
do { return try AVAudioPlayer(contentsOf: url) }
catch { fatalError("Couldn't load audio \(name).mp3 from main bundle.") }
}
fileprivate func guaranteeAudio(for name: String) -> Resources.Index {
if let index = audios.index(forKey: name) { return index }
audios[name] = AudioStore.loadAudio(with: name)
return audios.index(forKey: name)!
}
}
In the view's init I initialize the player's instance by calling audio(with:) with proper resource name.
In onAppear() I call prepareToPlay() on view's already initialized player with proper optional unwrapping, and finally
I play audio when the gesture fires.
Also, in my case I needed to delay the actual playback by some 0.3 seconds, and for that I despatched it to the global queue. I should stress that the animation with the audio playback was smooth even without dispatching it to the background queue, so I concluded the key was in proper initialization and preparation. To delay the playback, however, you can only utilize the background thread, otherwise you will get the lag.
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.3) {
///your audio.play() analog here
}
Hope that will help some soul out there.
I am using xcode 9 and swift 4 for my app. In my app i have music playing in the viewDidLoad. When i exit the view controller to go to another View, it continues to play like it should. How ever, when i return to that view controller the song starts to play again. This song is overlapping the song that first loaded. Do you guys have any ideas on how to stop this from happening?
do
{
let audioPath = Bundle.main.path(forResource: "APP4", ofType: "mp3")
try player = AVAudioPlayer(contentsOf: NSURL(fileURLWithPath: audioPath!) as URL)
}
catch
{
//catch error
}
let session = AVAudioSession.sharedInstance()
do
{
try session.setCategory(AVAudioSessionCategoryPlayback)
}
catch
{
}
player.numberOfLoops = -1
player.play()
It starts playing again, because your viewDidLoad is called again, which asks it to play it again. A simplest fix would be to keep a static bool variable to keep track if you have already made this call.
static var isMusicPlaying: Bool = false
In your viewDidLoad, you can put code before the code that calls the play.
guard !isMusicPlaying else {
return
}
isMusicPlaying = true
so here is my use case - we are loading a video using avplayer and playing it, and user can click on the default fullscreen button to take the video fullscreen. The user may be viewing the video in 2 different conditions, if he is logged in and if he is not logged in.
If the user is logged in (determined by a variables value), he can watch full video, otherwise, playback should stop after playing a defined number of seconds (depending on the video the no. of seconds changes), and a banner comes up over the player asking him to login.
Everything is working fine while the video is being played inline. However, when the video is playing fullscreen, then even if we stop playback using didTapPause(), the fullscreen window does not get dismissed. I have even tried dismissing it using self.playerController.dismiss(animated: true, completion: nil), the fullscreen modal is not dismissed. The code snippet it as follows -
playerController.player?.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, 1), queue: DispatchQueue.main) { (CMTime) -> Void in
if self.playerController.player?.currentItem?.status == .readyToPlay {
self.videoCurrentTimeDuration = CMTimeGetSeconds((self.playerController.player?.currentItem!.currentTime())!);
self.videoTimeDuration = CMTimeGetSeconds((self.playerController.player?.currentItem?.duration)!);
if self.moveToTime != nil{
let timeWithSecond = CMTimeMakeWithSeconds(self.videoTimeDuration! * self.moveToTime! / 100, Int32(kCMTimeMaxTimescale))
self.playerController.player?.seek(to: timeWithSecond, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
self.moveToTime = nil
}
guard let videoD = self.videoData else { return }
let timeTToPlay: Double = Double(videoD.freeDuration)
if videoD.isFree {
if videoD.registrationNeeded && !CurrentLoginUser.shared.isLogin{
if self.videoCurrentTimeDuration! > timeTToPlay {
self.didTapPause()
self.playerController.dismiss(animated: true, completion: nil)
self.loginNeedView = UINib.get(withNib: self)
self.loginNeedView?.frame = self.bounds
self.loginNeedView?.autoresizingMask = [
UIViewAutoresizing.flexibleWidth,
UIViewAutoresizing.flexibleHeight
]
self.addSubview(self.loginNeedView!)
AppUtility.lockOrientation(UIInterfaceOrientationMask.portrait, andRotateTo: UIInterfaceOrientation.portrait)
}
}
else{
self.loginNeedView?.removeFromSuperview()
AppUtility.lockOrientation(UIInterfaceOrientationMask.all)
}
}
The player controller is added onto the view by calling the setupView function which is as follows -
private func setUpView() {
self.backgroundColor = .black
addVideoPlayerView()
configurateControls()
}
fileprivate func addVideoPlayerView() {
playerController.view.frame = self.bounds
playerController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
playerController.showsPlaybackControls = true
playerController.addObserver(self, forKeyPath: "videoBounds", options: NSKeyValueObservingOptions.new, context: nil)
self.insertSubview(playerController.view, at: 0)
}
I am not sure if this is the proper way to do it, any ideas ?
EDIT:
Next as per anbu.karthik's advice I tried to forcibly remove the fullscreen view by locating the top view controller as :
func currentTopViewController() -> UIViewController {
var topVC: UIViewController? = UIApplication.shared.delegate?.window??.rootViewController
while ((topVC?.presentedViewController) != nil) {
topVC = topVC?.presentedViewController
}
return topVC!
}
and then, used it as follows-
let currentTopVC: UIViewController? = self.currentTopViewController()
if (currentTopVC?.description.contains("AVFullScreenViewController"))! {
print("found it")
currentTopVC?.dismiss(animated: true) { _ in }
}
And it works but crashes the app with the following exception -
Terminating app due to uncaught exception 'UIViewControllerHierarchyInconsistency', reason: 'child view controller:<AVEmbeddedPlaybackControlsViewController: 0x7fdcc2264400> should have parent view controller:<AVPlayerViewController: 0x7fdcc222a800> but actual parent is:<AVFullScreenViewController: 0x7fdcce2f3c50>'
To my knowledge it is not possible to play full screen directly. It seems that AVPlayerViewController comes pretty much as is and does not offer much in the way of customization of UI or behavior. If you wanted to play full screen directly you would need to either present the controller containing your AVPlayerViewController modally in full screen or change the frame yourself to make it full screen. But there is no API to control full screen programmatically on AVPlayerViewController...
When i launch my app, a background music starts to play. But when i press start and play the game (in another viewcontroller) and then go back to menu (the first view controller) the song starts again but while the same song is still playing (result = hearing it twice at the same time).Very annoying. This is my viewDidLoad function (probably where my problem is). Can someone help me (by explaining or giving code) playing the music only the first time that the view loads ?
override func viewDidLoad() {
super.viewDidLoad()
// get path of audio file
let myFilePathString = NSBundle.mainBundle().pathForResource("Background Music Loop (Free to Use)", ofType: "mp3")
if let myFilePathString = myFilePathString {
let myFilePathURL = NSURL(fileURLWithPath: myFilePathString)
do { try myAudioPlayer = AVAudioPlayer(contentsOfURL: myFilePathURL)
myAudioPlayer.play()
myAudioPlayer.numberOfLoops = -1
}catch{
print("error")
}
}
}
myAudioPlayer.play will simply be called again, since the VC runs the viewDidLoad() method again.
My idea:
declare a variable
var x = false
next step - make a condition on your audio player to play.
if x == false { // your code here }
when triggering the Segue from the other VC try to use the prepareForSegue method.
So on 2nd ViewController
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
{
if segue.identifier == "yourIdentifier" {
let destinationController = segue.destinationViewController as! // Controller Name here
destinationController.x = true
}
}
This will set the x to true and the music doesn't run again.
It all depends on the functionality you want.
If you only want the background music to play when the menu view controller is on screen you could make the file name and player variables properties on the view controller and then in viewWillAppear do
Player.play
And then when a user navigates away from the page stop it playing in the viewDidDissappear function.
If you want the music to always play in the background then it might be worth creating your own class for it and providing a sharedInstance on the app delegate so that you can manage a single instance of the player throughout the application.
I am trying to add sound to my app. I would like it for when I tap a button, it plays a quick sound. However, when the button is tapped quickly and repeatedly, the sound does not work as well (It only plays 1 or 2 times when I tap the button 5 or 6 times). Here is my code in the button
player.play()
I have this outside
var player = AVAudioPlayer()
let audioPath = NSBundle.mainBundle().pathForResource("illuminati", ofType: "wav")
Viewdidload:
do {
try player = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: audioPath!))
} catch {}
How can I play the sound repeatedly better? Thanks.
The problem is that you are calling the play multiple time before the previous call finished. You need to keep track of how many times the user click the button and the play song one by one.
This is what you can do:
Use an integer in you class to keep track of number of times that the button is clicked
var numClicks = 0
var buttonClickTime:NSDate? = nil // The last time when the button is clicked
#IBAction func yourbuttonclickfunction() {
numClicks++;
buttonClickTime = NSDate()
player.play()
}
Register the delegate of AVAudioPlayerDelegate
do {
try player = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: audioPath!))
// Add this
player.delegate = self
} catch {}
In the delegate function, play the song again when the previous one reach the end:
optional func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer,
successfully flag: Bool)
{
if --numClicks > 0
{
let now = NSDate()
let duration = now.timeIntervalSinceDate(buttonClickTime!)
// If the button click was less than 0.5 seconds before
if duration < 0.5
{
// Play the song again
player.play();
}
}
}
The reason your sound only plays a few time is because the song plays until finished or until you stop it, even if you press the button multiple times in succession.
You could just stop the music manually, so before you press a button that plays a sound you say
player.stop()
and than
player.play()
Is that helping?