In Swift, I play a song, it plays at the viewDidLoad. It is 16 seconds long, and it plays all 16 seconds. I want it to repeat forever. I made an NSTimer where every 16 seconds, it plays the song. But, it plays 16 seconds when the app loads, stops for 16 seconds, plays, etc.
The line println("just doing some dandy debugging here.") does print every 16 seconds.
How is this fixed?
CODE:
//these var's are created on the top of the file.
var soundTimer: NSTimer = NSTimer()
var audioPlayer2 = AVAudioPlayer()
var soundTwo = NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("sound", ofType: "wav"))
//this stuff is in the viewDidLoad function.
audioPlayer2 = AVAudioPlayer(contentsOfURL: soundTwo, error: nil)
audioPlayer2.prepareToPlay()
audioPlayer2.play()
soundTimer = NSTimer.scheduledTimerWithTimeInterval(16, target: self, selector: Selector("soundTimerPlayed"), userInfo: nil, repeats: true)
func soundTimerPlayed() {
println("just doing some dandy debugging here.")
audioPlayer2.stop()
audioPlayer2.prepareToPlay()
audioPlayer2.play()
}
Just do it the easy, official way, instead. From the documentation of the numberOfLoops property:
Set any negative integer value to loop the sound indefinitely until you call the stop method.
But your actual problem is almost certainly because you're stopping playback a little early:
The stop method does not reset the value of the currentTime property to 0. In other words, if you call stop during playback and then call play, playback resumes at the point where it left off.
What's happening is that you're stopping the sound playback just a little before the actual end of the sample—your timer is set to just too short a time. The first time the timer is triggered, the "play" is playing the tiny amount of quiet or silent sound that's left at the end, and then stopping. The next time, the currentTime has been reset because the sample has successfully reached the end of playback, so on the next timer interval, after another 16 seconds, playback starts successfully from the beginning. And repeat.
As Jack observes in his comment, this is why there's a delegate callback to give you a kick when the playback has actually finished, but I'd still just set numberOfLoops and not bother with the complicated stuff, if you only need the sound to loop indefinitely.
(If you desperately want to repeat on your exact timer event, then just set currentTime to 0 before you play it again.)
Related
I am using the MPMusicPlayerController to play songs from Apple Music. I am periodically (every several seconds) changing the song queue using
setQueue(with: MPMusicPlayerStoreQueueDescriptor(storeIDs: ids))
I don't have any problems during music playback, the song queue is updated as requested, I can skip to next song and everything is fine. However, whenever I pause the player in the middle of the song, wait a bit till a new queue is set, and play again - the current song is lost and next song starts to play!
So imagine the following situation:
I have songs A, B, C, D
I set it as a song queue and call play()
player.nowPlayingItem returns A
I set the song queue to E, F, G, H while playing
I call player.skipToNext() - song E starts playing, as expected
I call player.pause() - song E pauses
I call player.play() - song E continues to play. Everything is fine till now
I call player.pause() again - song E pauses
I set the song queue to I, J, K, L.
I call player.play() - I expect the paused song E to continue playing. Instead, song I starts playing
I also did some log output for the above scenario:
func togglePlayPause() {
if player.playbackState == .playing {
player.pause()
} else {
NSLog("NP before \(player.nowPlayingItem)") // prints E at step 10
player.play()
NSLog("NP after \(player.nowPlayingItem)") // prints nil at step 10
}
}
Strangely, pause/play from lock screen works fine, even if I change the queue in between.
Has anyone experienced similar problem, any hints/workarounds how to fix this?
As a workaround, I used
player.currentPlaybackRate = 1
instead of player.play(). Seems like play() does a bit more than just playing from paused location.
I have one main AVAudioPlayerNode that plays constantly. And there are other players that are put in the queue and play when the main player reaches a certain point.
These AVAudioPlayerNode`s should be synchronized to a millisecond. Sometimes 4-10 pieces can be started at a time.
How its works - I store lastRenderTime of the main player on point when need start all scheduled players and then start all needed player with
player.start(at: lastRenderTime)
Usually, it's working well and without any latency between sounds.
But, I got some latency on this case:
Running on OLD devices(iPad 2, iPhone 5 etc)
ONLY when MANY players the first time after application startup (For example on iPhone 7 I run 8 players in same time after the application is running and I hear desynchronization between sounds), but all players are running with same player.start(at: lastRenderTime) time.
All player actions I run async with custom concurrent queue.
Spend 3 days on trying fix this. I would like for any pieces of advice.
Some code:
Here is how I starting main player -
self.scheduleBuffer(audioFileBuffer, at: nil, options: [], completionHandler: {
lastRenderTime = lastRenderTime + audioBufferLength //Schematic showing logic that after each loop I calculate current render time value.
if self.isStopPressed == false { //Run fileEndCompletion only when player seek to end.
fileEndCompletionHandler() //here is callback that running all scheduled players.
if loop { // Recursive play this file again.
self.playAudioBuffer(audioFileBuffer: audioFileBuffer, loop: loop, fileEndCompletionHandler: fileEndCompletionHandler)
}
}
})
Example of code that running all scheduled player.
AudioEnginePlayer.sharedInstance.audioQueue.async {
if !self.isPlaying {
self.volume = 1.0
self.audioFile.framePosition = frameCount.startingFrame
self.prepare(withFrameCount: frameCount.frameCount)
self.play(at: AudioEnginePlayer.sharedInstance.loopMsLastStartRenderTime) //Calculated lastRenderTime
}
self.scheduleSegment(self.audioFile, startingFrame: frameCount.startingFrame, frameCount: frameCount.frameCount, at: nil) {
//WARNING: COMPLETION HANDLER RUNNING EARLY THEN FILE IS COMPLETED
if self.isStopPressed == false { //Run fileEndCompletion only when player seek to end.
fileEndCompletionHandler()
if loop {
self.playAudioFile(loop: loop, startTime: 0, fileEndCompletionHandler: fileEndCompletionHandler)
}
}
}
}
Question:
In Swift code, apart from using an NSTimer, how can I get animations
to start at exact points during playback of a music file played using AVFoundation?
Background
I have a method that plays a music file using AVFoundation (below). I also have UIView animations that I want to start at exact points during the music file being played.
One way I could achieve this is using an NSTimer, but that has the potential to get out of sync or not be exact enough.
Is there a method that I can tap into AVFoundation accessing the music file's time elapsed (time counter), so when certain points during the music playback arrive, animations start?
Is there an event / notification that AVFoundation triggers that gives a constant stream of time elapsed since the music file has started playing?
For example
At 0:52.50 (52 seconds and 1/2), call startAnimation1(), at 1:20.75 (1 minute, 20 seconds and 3/4), call startAnimation2(), and so on?
switch musicPlayingTimeElapsed {
case 0:52.50:
startAnimation1()
case 1:20.75:
startAnimation2()
default:
()
}
Playing music using AVFoundation
import AVFoundation
var myMusic : AVAudioPlayer?
func playMusic() {
if let musicFile = self.setupAudioPlayerWithFile("fileName", type:"mp3") {
self.myMusic = musicFile
}
myMusic?.play()
}
func setupAudioPlayerWithFile(file:NSString, type:NSString) -> AVAudioPlayer? {
let path = NSBundle.mainBundle().pathForResource(file as String, ofType: type as String)
let url = NSURL.fileURLWithPath(path!)
var audioPlayer:AVAudioPlayer?
do {
try audioPlayer = AVAudioPlayer(contentsOfURL: url)
} catch {
print("AVAudioPlayer not available")
}
return audioPlayer
}
If you use AVPlayer instead of AVAudioPlayer, you can use the (TBH slightly awkward) addBoundaryTimeObserverForTimes method:
let times = [
NSValue(CMTime:CMTimeMake(...)),
NSValue(CMTime:CMTimeMake(...)),
NSValue(CMTime:CMTimeMake(...)),
// etc
];
var observer: AnyObject? = nil // instance variable
self.observer = self.player.addBoundaryTimeObserverForTimes(times, queue: nil) {
switch self.player.currentTime() {
case 0:52.50:
startAnimation1()
case 1:20.75:
startAnimation2()
default:
break
}
}
// call this to stop observer
self.player.removeTimeObserver(self.observer)
The way I solve this is to divide the music up into separate segments beforehand. I then use one of two approaches:
I play the segments one at a time, each in its own audio player. The audio player's delegate is notified when a segment finishes, and so starting the next segment — along with accompanying action — is up to me.
Alternatively, I queue up all the segments onto an AVQueuePlayer. I then use KVO on the queue player's currentItem. Thus, I am notified exactly when we move to a new segment.
You might try using Key Value Observing to observe the duration property of your sound as it plays. When the duration reaches your time thresholds you'd trigger each animation. You'd need to make the time thresholds match times >= the trigger time, since you will likely not get a perfect match with your desired time.
I don't know how well that would work however. First, I'm not sure if the sound player's duration is KVO-compliant.
Next, KVO is somewhat resource-intensive, and if your KVO listener gets called thousands of times a second it might bog things down. It would at least be worth a try.
I use AVAudioPlayer to play a click sound if the user taps on a button.
Because there is a delay between the tap and the sound, I play the sound once in viewDidAppear with volume = 0
I found that if the user taps on the button within a time period the sound plays immediately, but after a certain time there is a delay between the tap and the sound in this case also.
It seems like in the first case the sound comes from cache of the initial play, and in the second case the app has to load the sound again.
Therefore now I play the sound every 2 seconds with volume = 0 and when the user actually taps on the button the sound comes right away.
My question is there a better approach for this?
My goal would be to keep the sound in cache within the whole lifetime of the app.
Thank you,
To avoid audio lag, use the .prepareToPlay() method of AVAudioPlayer.
Apple's Documentation on Prepare To Play
Calling this method preloads buffers and acquires the audio hardware
needed for playback, which minimizes the lag between calling the
play() method and the start of sound output.
If player is declared as an AVAudioPlayer then player.prepareToPlay() can be called to avoid the audio lag. Example code:
struct AudioPlayerManager {
var player: AVAudioPlayer? = AVAudioPlayer()
mutating func setupPlayer(soundName: String, soundType: SoundType) {
if let soundURL = Bundle.main.url(forResource: soundName, withExtension: soundType.rawValue) {
do {
player = try AVAudioPlayer(contentsOf: soundURL)
player?.prepareToPlay()
}
catch {
print(error.localizedDescription)
}
} else {
print("Sound file was missing, name is misspelled or wrong case.")
}
}
Then play() can be called with minimal lag:
player?.play()
If you save the pointer to AVAudioPlayer then your sound remains in memory and no other lag will occur.
First delay is caused by sound loading, so your 1st playback in viewDidAppear is right.
I'm working in Swift and I have an NSTimer that counts down from 3, 2, 1. I want to play an audio file every time this timer decrements, so that the timer would show 3 and the audio would play once, then it flips to 2 and the audio plays once, finally it goes to 1 and it plays once.
This is how I've created the timer and tried to do that:
var path2 = NSBundle.mainBundle().pathForResource("splatter1", ofType: "wav")
var soundTrack2 = AVAudioPlayer()
func timeToMoveOn() {
let loadingDelay = 0.01 * Double(NSEC_PER_SEC)
let loadingTime = dispatch_time(DISPATCH_TIME_NOW, Int64(loadingDelay))
dispatch_after(loadingTime, dispatch_get_main_queue()) {
//After delay
self.performSegueWithIdentifier("goToGameScene", sender: self)
}
}
func splatterSound() {
soundTrack2 = AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: path2!), error: nil)
soundTrack2.numberOfLoops = 1
soundTrack2.volume = 0.35
soundTrack2.play()
}
func updateCounter() {
countdownTestLabel.text = String(counter--)
splatterSound()
if counter == 0 {
countdown.invalidate()
//Then trigger segue to Game Scene view
timeToMoveOn()
}
}
In view did load:
counter = 3
countdown = NSTimer.scheduledTimerWithTimeInterval(1, target:self, selector: Selector("updateCounter"), userInfo: nil, repeats: true)
My problem is that the audio file plays every time the timer decrements, but it plays multiple times. For example it will one time, then a second time, then a third fourth and fifth time all in rapid succession. It shouldn't do that.
How can I fix this so that it only plays when the timer decrements?
Get rid of the soundTrack2.numberOfLoops = 1 bit. The numberOfLoops setting is the number of times to repeat the sound. So the first time your timer fires, you queue up your sound to play twice in a row.
Then one second later, you queue up the sound to play twice in a row again.
Then, yet one second later, you queue up your sound to play another twice in a row.
If you eliminate that line, the sound will default to numberOfLoops=0, or just playing the sound once (which is what you want.)