I have a SpriteKit game where I have a number of different Sprites Lasers, Planes, etc. A number of these Sprites have a sounds that will play whenever they are visible. Right now I play the sound in the SKSpriteNode itself. This probably isn't what I want because if I have I have 50 of the same sprites, they'll all be playing the same sound and gobbing resources. However, I'm not really sure where would be the best place to play the the sound, because for some of these sprites I would need to check if it should be playing or not. I suppose I could put the sounds in the the GameScene and loop through all sprites during every update to determine what sounds should and should not still be playing, that way there would only be one instance of every sound. I was hoping to keep all the sprite code within the sprite itself, but this would solve the issue with multiple instances of the same sound playing. Is this the correct way to handle sounds?
It is hard to give you a proper code example without seeing your code. You can however get the gist of what you can try with what I have below.
In MyScene add a property BOOL propSound.
Whenever you create a new plane and wonder about adding the prop sound do something like this:
if(propSound == false)
{
propSound = true;
// add your code to play prop sound
}
If you add every newly created plane sprite into a NSMutableArray like this [yourArrayName addObject:newPlane];, you can check the array count every time you remove a plane in order to know if the last plane was removed and the sound needs to be stopped.
if([yourArrayName count] == 0)
{
propSound = false;
// add your code to stop prop sound
}
Maybe you are looking for something like this?
NSMutableArray *coin = [[NSMutableArray alloc] initWithCapacity:1];
SKAction *_coinSound = [SKAction playSoundFileNamed:#"Pickup_Coin4.wav" waitForCompletion:NO];
[coin addObject:_coinSound];
SKAction *_playCoinSound = [SKAction sequence:coin];
Then I just check if the "character" in my case touched the coin. If so it will play the sound.
if([_adventurer intersectsNode:coin]){
[coin runAction:_playCoinSound];
}
Edit:
Saw your comment on your question. I guess you could check if there is at least 1 plane on the screen, if so then you could play a loop of the sound. If there is less than 1 no sound.
Do not play the same sound again if there already are playing. You could use a BOOL to see if the sound is already playing.
BOOL isPlaneSoundOn = false;
if(!isPlaneSoundOn && _planeNode.count > 0){
//play the sound
isPlaneSoundOn = true;
} else if(_plane.count == 0) {
//stop the sound
isPlaneSoundOn = false;
}
Something like that. Maybe you get the idea :)
I would personally play any community sounds off the scene using the SKAction playSoundWithFilename, and assign this sound a key.
Then I would check if the key exists, and if it does not, then play the sound
Related
I need to send notifications when the AVPlayer is Play/Paused and Stopped.
For play and pause below is the code
if (self.player.rate > 0.0f) {
NSLog(#" Playing ..")
}
if (self.player.rate == 0.0f) {
NSLog(#" Paused ..")
}
But for stopped also the rate = 0.0 then is there any other property or way to identify the difference between paused and stopped.
For both, paused and stopped the rate = 0.0 and hence need another way for it.
Thanks
There is no stop command for an AVPlayer. So there is no such thing as stopped as distinct from paused. Either the rate is zero (not playing) or it is more than zero (playing).
You can distinguish where the player is within its item (currentTime) so you can tell whether we are at the start, end, or middle; and you can arrange to be notified periodically during play or when the end is reached.
Apart from that, there are no distinctions to be drawn.
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: MPMoviePlayerController to show video.
Below I put list of thumbs from the video.
When pressing a thumb I want to jump to a specific place in the video using: setCurrentPlaybackTime.
I also have a timer updating the selected thumb according to the location of the video using: currentPlaybackTime.
My problem: when calling: setCurrentPlaybackTime, the player keeps giving the seconds before seeking to the specific second. It take few seconds to the player to reflect the new seconds. In the mean time the experience of the user is bad: Pressing a thumb shows it selected for a show time, then the timer updates to the previous thumb, then it jumps back to the thumb I selected.
I tried using (in the timer):
if (moviePlayer.playbackState != MPMoviePlaybackStatePlaying && !(moviePlayer.loadState & MPMovieLoadStatePlaythroughOK)) return;
In order to prevent from the timer to update the selected thumb as long the player is in a transition phase between showing the previous thumb and the new thumb, but it doesn't seem to work. The "playbackState" and "loadState" seems to be totally inconstant and unpredictable.
For solving this issue, this how I have implemented this nasty state coverage in one of my projects. This is nasty and fragile but worked good enough for me.
I used two flags and two time intervals;
BOOL seekInProgress_;
BOOL seekRecoveryInProgress_;
NSTimeInterval seekingTowards_;
NSTimeInterval seekingRecoverySince_;
All of the above should be defaulted to NO and 0.0.
When initiating the seek:
//are we supposed to seek?
if (movieController_.currentPlaybackTime != seekToTime)
{ //yes->
movieController_.currentPlaybackTime = seekToTime;
seekingTowards_ = seekToTime;
seekInProgress_ = YES;
}
Within the timer callback:
//are we currently seeking?
if (seekInProgress_)
{ //yes->did the playback-time change since the seeking has been triggered?
if (seekingTowards_ != movieController_.currentPlaybackTime)
{ //yes->we are now in seek-recovery state
seekingRecoverySince_ = movieController_.currentPlaybackTime;
seekRecoveryInProgress_ = YES;
seekInProgress_ = NO;
seekingTowards_ = 0.0;
}
}
//are we currently recovering from seeking?
else if (seekRecoveryInProgress_)
{ //yes->did the playback-time change since the seeking-recovery has been triggered?
if (seekingRecoverySince_ != movieController_.currentPlaybackTime)
{ //yes->seek recovery done!
seekRecoveryInProgress_ = NO;
seekingRecoverySince_ = 0.0;
}
}
In the end, MPMoviePlayerController simply is not really meant for such "micro-management". I had to throw in at least half a dozen flags for state coverage in all kinds of situations and I would never recommend to repeat this within other projects. Once you reach this level, it might be a great idea to think about using AVPlayer instead.
I have the following code in Draw() in Game1. However, the Music only plays when I'm pressing the close button for the program. Where should I be putting MediaPlayer.Play() if not there? normS, fastS, slowS, and playing are all Song types. If you need me to clear anything up, just ask.
if (stateS == "normal")
{
if (!MediaPlayer.Equals(playing, normS))
{
playing = normS;
}
spriteBatch.Draw(norm, pos, Color.White);
}
else if (stateS == "fast")
{
if (!MediaPlayer.Equals(playing, fastS))
{
playing = fastS;
}
spriteBatch.Draw(fast, pos, Color.White);
}
else if (stateS == "slow")
{
if (!MediaPlayer.Equals(playing, slowS))
{
playing = slowS;
}
spriteBatch.Draw(slow, pos, Color.White);
}
MediaPlayer.Play(playing);
Is there a particular reason why you need to have the songs play out of the Draw method? Draw should be reserved for drawing things.
Like user1306322 said in the comment above, I would recommend moving the
MediaPlayer.Play(playing);
into the Update method, and wrap it in a conditional (so you dont just keep playing a new song every update, which I think is what is happening for you now since clicking and holding the Close Window x stops the updates. You could try this by dragging the window around as well):
if (!MediaPlayer.IsPlaying)
{
MediaPlayer.Play(playing);
}
If necessary, you can leave the rest of the code in Draw, but most likely this whole thing should go in Update.
edit I just realised you may not have a conditional on this because you want to be able to change the song at any time. In that case, you should set a variable to hold your "Last Song", so you can compare it in your conditional like this:
if (MediaPlayer.IsPlaying == false || playing != lastPlaying)
{
MediaPlayer.Play(playing);
}
There seems to be a problem with the MPMoviePlayerController where once you're in fullscreen mode and you hold down the fast forward button, letting it seek forward (playing at fast speed) all the way to the end of the video.
Thereafter the you just get a black screen and it's stuck. In other words it does not respond to any taps gestures and you can not get out of this situation. Has anyone else encountered this problem?
Is there anyway to work around it in code?
It seems it's an iOS bug since fast backward to the very beginning won't cause the black screen but fast forward to the end will, and after that the 'play'/'pause' call to the video player never works. I temporarily fix this by adding protected logic into the scrubber refresh callback:
let's assume that monitorPlaybackTime will be called in 'PLAY_BACK_TIME_MONITOR_INTERVAL' seconds period to refresh the scrubber, and in it I add a check logic:
NSTimeInterval duration = self.moviePlayer.duration;
NSTimeInterval current = self.moviePlayer.currentPlaybackTime;
if (isnan(current) || current > duration) {
current = duration;
} else if (self.moviePlayer.playbackState == MPMoviePlaybackStateSeekingForward) {
if (current + self.moviePlayer.currentPlaybackRate*PLAY_BACK_TIME_MONITOR_INTERVAL > duration) {
[self.moviePlayer endSeeking];
}
}
A workaround to solve the black screen, not perfect, hope it can help.
I'm guessing you are not handling the MPMoviePlayerPlaybackDidFinishNotification. You really should if you're not.
Still its unexpected for me that the movie player would go into a "stuck" state like you describe. I would more readily expect it to stop playback automatically and reset when it reaches the end. Anyway, I think your problem will go away if you observe the MPMoviePlayerPlaybackDidFinishNotification and handle the movie controller appropriately.
Ran into the same issue on iOS6. Managed to fix it by registering for the MPMoviePlayerPlaybackDidFinishNotification (as suggested by Leuguimerius) with the following implementation:
- (void)playbackDidFisnish:(NSNotification *)notif {
if (self.player.currentPlaybackTime <= 0.1) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.player stop];
[self.player play];
[self.player pause];
});
}
}
Where self.player is the associated MPMoviePlayerController instance. The check against currentPlaybackTime serves to distinguish the more standard invocations of playbackDidFinish (where the movie is allowed to play at normal speed until its end) from those scenarios where the user fast forwards until the end. Stopping then playing and pausing results in a usable, visually consistent interface even when fast-forwarding to the end.
None of the aforementioned solutions worked for me, so this is what I ended up doing:
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("moviePlayerLoadStateDidChange"), name: MPMoviePlayerLoadStateDidChangeNotification, object: nil)
func moviePlayerLoadStateDidChange() {
let loadState = moviePlayerController?.loadState
if loadState == MPMovieLoadState.Unknown {
moviePlayerController?.contentURL = currentmovieURL
moviePlayerController?.prepareToPlay()
}
}
I think the issue is that when the seek foraward button is single pressed, it wants to skip to the next video, that's why a loading indicator appears. Listening for the load state change event, you can specify what the next video should be, and if you don't have any, you can just give it the same url.