How to detect when sound effect finish playing? - ios

I'm using SimpleAudioEngine and I'm trying to detect if a sound effect is finish playing before continuing.
I'm looking for any method, but the one I'm trying to implement doesn't work!
CDSoundEngine *engine = [CDAudioManager sharedManager].soundEngine;
ALuint soundId = [[SimpleAudioEngine sharedEngine] playEffect:soundId];
float seconds = [engine bufferDurationInSeconds:soundId];
Every time I use bufferDurationInSeconds, it returns a float value of -1 to variable seconds. I checked the implementation, and it returns a -1 when the id is not valid, but I'm 100% the ID is valid!
Can anyone help me on this problem, or suggest me another way to detect the end of an sound effect?

ViolĂ ! Getting CDSoundSource, and then the soundId from that works.
(The second line is optional, it just plays the sound).
CDSoundEngine *engine = [CDAudioManager sharedManager].soundEngine;
[[SimpleAudioEngine sharedEngine] playEffect:#"soundeffect.m4a"];
CDSoundSource *aSound =[[SimpleAudioEngine sharedEngine] soundSourceForFile:#"soundeffect.m4a"];
float seconds = [engine bufferDurationInSeconds:aSound.soundId];
Also to run a method right when it finishes playing I would use an NSTimer that uses the seconds result.
[NSTimer scheduledTimerWithTimeInterval:seconds target:self selector:#selector(aMethod) userInfo:nil repeats:NO];
And then the final method implements something.
-(void)aMethod
{
NSLog(#"finished playing");
}
Although, this doesn't effect the -1 result, I have to point out that the soundId variable appears twice as two different kinds in your code, in the same line, which will cause problems. No worries though, I have tested my method above with success.
ALuint **soundId** = [[SimpleAudioEngine sharedEngine] playEffect:**soundId**];

Related

MPNowPlayingInfoCenter throwing EXC_BAD_ACCESS

I am making an app that plays back audio and I have set it up so that the lock screen gets updated through MPNowPlayingInfoCenter, but I've run into a problem.
At seemingly random times, I get an EXC_BAD_ACCESS error when trying to update the now playing info.
Here's the code that does so:
- (void)updatePlayback
{
if(!active)
return;
NowPlayingController* npc = [AudioController nowPlayingController];
CMTime elapsed = player.currentTime;
Float64 elInterval = CMTimeGetSeconds(elapsed);
[npc setElapsed:elInterval];
CMTime duration = player.currentItem.duration;
Float64 durInterval = CMTimeGetSeconds(duration);
[npc setRemaining:ceilf(durInterval - elInterval)];
[npc setPlayPauseValue:isPlaying];
if(durInterval > 0)
{
[npc setProgressValue:elInterval/durInterval];
[npc setAudioDuration:durInterval];
}
_activeMetadata[MPMediaItemPropertyPlaybackDuration] = #(durInterval);
_activeMetadata[MPNowPlayingInfoPropertyPlaybackRate] = #(isPlaying);
_activeMetadata[MPNowPlayingInfoPropertyElapsedPlaybackTime] = #(elInterval);
MPNowPlayingInfoCenter* npInfoCenter = [MPNowPlayingInfoCenter defaultCenter];
if(npInfoCenter && _activeMetadata)
{
if([npInfoCenter respondsToSelector:#selector(setNowPlayingInfo:)])
{
//////////THE FOLLOWING LINE TRIGGERS EXC_BAD_ACCESS SOMETIMES////////////
[npInfoCenter setNowPlayingInfo:_activeMetadata];
}
}
}
99.9% of the time, this works, but sometimes when resigning the app to the background or when changing audio files, or just randomly,
[npInfoCenter setNowPlayingInfo:_activeMetadata];
throws EXC_BAD_ACCESS.
Also, _activeMetadata is declared as:
#property (atomic, strong, retain) NSMutableDictionary* activeMetadata;
It is instantiated when the AVPlayer is created:
AVAsset* asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:path]];
AVPlayerItem* playerItem = [AVPlayerItem playerItemWithAsset:asset];
player = [AVPlayer playerWithPlayerItem:playerItem];
CMTime duration = player.currentItem.duration;
NSTimeInterval durInterval = CMTimeGetSeconds(duration);
NSLog(#"%f", durInterval);
MPMediaItemArtwork* albumArtwork = [[MPMediaItemArtwork alloc] initWithImage:[downloader useCachedImage:CacheKeySeriesBanners withName:nil withURL:info[#"image"]]];
NSDictionary* nowPlayingInfo = #{MPMediaItemPropertyTitle:ptString,
MPMediaItemPropertyArtist:spString,
MPMediaItemPropertyArtwork:albumArtwork,
MPMediaItemPropertyAlbumTitle:info[#"title"],
MPMediaItemPropertyPlaybackDuration:#(durInterval),
MPNowPlayingInfoPropertyPlaybackRate:#(1),
MPNowPlayingInfoPropertyElapsedPlaybackTime:#(0)};
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:nowPlayingInfo];
_activeMetadata = [nowPlayingInfo mutableCopy];
updatePlayback is called via a CADisplayLink on every frame.
Any ideas what could be causing the exception?
I think you're calling setNowPlayingInfo way too often. Granted, it really shouldn't crash but there's no need to use CADisplayLink to call it 60 times a second.
So why are you calling it so often? If it's because you want to progress bar to track smoothly, there's still no need. From the MPNowPlayingInfoPropertyElapsedPlaybackTime declaration:
// The elapsed time of the now playing item, in seconds.
// Note the elapsed time will be automatically extrapolated from the previously
// provided elapsed time and playback rate, so updating this property frequently
// is not required (or recommended.)
p.s. I tried the code with an m4a file and found durInterval was NotANumber. With the correct duration and calling setNowPlayingInfo only once, the progress bar tracked fine & nothing crashed.
Apple fixed this crash in iOS 10.3 and above.
So if you want to support iOS 10.2.1 and below, be sure to throttle how often you set [MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo property. Perhaps limiting setting the property only once a second.

Best Practices - sequence execution of commands in ObjC

What's the best way to execute a series of commands in a certain order? I keep getting frustrated by the darn concurrency of execution in ObjC. This is one of these cases where I realize that I'm a designer, not a "real" coder.
I'm experimenting with SpriteKit on iOS, and I want a sequence of things to occur when an energy gauge reaches <=0.
Call a method to create an explosion at the final contact. The method takes arguments about position and size of explosion.
Call another method afterwards that calls up a new scene, a results screen.
My problem occurs when the new scene gets called before I get a chance to see the last explosion.
Here's the relevant code:
- (void) doGameOver
{
damageIndicator.progress = 0;
energyLeft.text = #"Energy:0%";
GameOver *newScene = [[GameOver alloc]initWithSize:self.size];
newScene.timeElapsed = [started timeIntervalSinceNow];
[self.view presentScene:newScene transition:[SKTransition fadeWithColor:[SKColor whiteColor] duration:1]];
[damageIndicator removeFromSuperview];
}
- (void) makeExplosionWithSize:(float)myBoomSize inPosition:(CGPoint)boomPosition
{
NSString *myFile = [[NSBundle mainBundle] pathForResource:#"explosion" ofType:#"sks"];
SKEmitterNode *boom = [NSKeyedUnarchiver unarchiveObjectWithFile:myFile];
boom.position = boomPosition;
boom.particleSize = CGSizeMake(myBoomSize, myBoomSize);
[self addChild:boom];
[self runAction:self.playMySound];
}
- (void)adjustScoreWithDamage:(float)hitDamage atPosition:(CGPoint)pos
{
_damage = _damage -(hitDamage);
if (_damage < 0) {
//these are the two things I need to execute sequentially
[self makeExplosionWithSize:500 inPosition:pos];
[self doGameOver]
}
}
I've tried schemes using bools (gameOver = YES), but think I may need to create a completion handler, which just makes my head spin.
Can anyone suggest the easiest way to accomplish this?
Thank you in advance.
Easiest (not best) probably would be to replace
[self doGameOver];
with
[self performSelector:#selector(doGameOver) withObject:nil afterDelay:2.0];
I may misunderstand what you're going for, but it sounds like what should happen is:
The explosion begins.
There's a pause of [n] seconds.
The Game Over screen is presented.
To accomplish that, you might want to just fire "doGameOver" with an NSTimer rather than worry about having it fire immediately after the explosion completes.
Here's an example with a 3 second delay:
NSTimer *gameOverTimer = [NSTimer timerWithTimeInterval:3.0 target:self selector:#selector(doGameOver:) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:gameOverTimer forMode:NSDefaultRunLoopMode];

Play and stop a sound sample on iOS (Novocaine)

I'm trying to make a simple Drumpad app. The app needs to be super fast and play the sound with as little latency as possible. The premise is store audio samples in an array and each one is played when you hit a pad.
The catch is that some pads are chords (and not drums) so they need to stop right away On Touch Up.
Here are the approaches I have tried:
System Sound - Very simple to implement, super responsive, no way to stop sounds without destroying them, sounds cant be more than 30 seconds.
AV Framework - Way to slow.
The Amazing Audio Engine - Seems nice, but not sure what the advantage is over CoreAudio as setup was rather complicated I wasn't able to get a sound to play. Also not sure about latency.
Novocaine - I settled on this for now, it seems very fast, and I can play sounds, but I've seen no way to stop them once they start. I don't know how I can stop a single sound without stopping the whole audioManager?
- (void) playSoundN:(int)padNum
{
__weak ViewController * wself = self;
NSURL *inputFileURL = [[NSBundle mainBundle] URLForResource:#"Drum - Kick" withExtension:#"wav"];
self.fileReader = [[AudioFileReader alloc]
initWithAudioFileURL:inputFileURL
samplingRate:self.audioManager.samplingRate
numChannels:self.audioManager.numOutputChannels];
[self.fileReader play];
self.fileReader.currentTime = 0.0;
[self.audioManager setOutputBlock:^(float *data, UInt32 numFrames, UInt32 numChannels)
{
[wself.fileReader retrieveFreshAudio:data numFrames:numFrames numChannels:numChannels];
NSLog(#"Time: %f", wself.fileReader.currentTime);
}];
[self.audioManager play];
}
Seems like a simple task, starting and stoping a sound with super low latency.
Should I use one of the above approaches or just bite the bullet and dive into Core Audio Units.
Anyone have any ideas?
I just made an app that has similar functionality to what you are trying to accomplish. My recommendation:
Use AVAudioPlayer (and AVAudioRecorder for recording).
Use the prepareToPlay method of the class to preload the sound and reduce lag.
You can read more about the classes in the AVFoundation documentation:
https://developer.apple.com/library/ios/DOCUMENTATION/AVFoundation/Reference/AVFoundationFramework/_index.html
Surprisedly, alexbw accepted the patch quickly. If you updated to newest code, you may simply use following lines in output block:
[weakSelf.reader retrieveFreshAudio:data numFrames:numFrames numChannels:numChannels];
if (!weakSelf.reader.playing) {
weakSelf.audioManager.outputBlock = nil;
dispatch_async(dispatch_get_main_queue(), ^{
// UI stuff
});
}
Novocaine cannot tell EOF of the file, the last buffer is used repeat.
Considering the author think AudioFileReader is just a piece of sugar and not likely to fix it, here is a dirty solution.
__block float lastTimeLeft = 0.0f;
[self.audioManager setOutputBlock:^(float *data, UInt32 numFrames, UInt32 numChannels) {
float timeLeft = weakSelf.reader.duration - [weakSelf.reader getCurrentTime];
if ( timeLeft > 0.01 && timeLeft != lastTimeLeft ) {
lastTimeLeft = timeLeft;
[weakSelf.reader retrieveFreshAudio:data numFrames:numFrames numChannels:numChannels];
} else {
[weakSelf.reader retrieveFreshAudio:data numFrames:numFrames numChannels:numChannels]; //forget this line will cause a buzz at the end.
[weakSelf.reader pause];
weakSelf.audioManager.outputBlock = nil;
}
}];
A better solution would be to patch the AudioFileReader.

How do I audio crossfade using cocoalibspotify?

I'd like to crossfade from one track to the next in a Spotify enabled app. Both tracks are Spotify tracks, and since only one data stream at a time can come from Spotify, I suspect I need to buffer (I think I can read ahead 1.5 x playback speed) the last few seconds of the first track, start the stream for track two, fade out one and fade in two using an AudioUnit.
I've reviewed sample apps:
Viva - https://github.com/iKenndac/Viva SimplePlayer with EQ - https://github.com/iKenndac/SimplePlayer-with-EQ and tried to get my mind around the SPCircularBuffer, but I still need help. Could someone point me to another example or help bullet-point a track crossfade game plan?
Update: Thanks to iKenndac, I'm about 95% there. I'll post what I have so far:
in SPPlaybackManager.m: initWithPlaybackSession:(SPSession *)aSession {
added:
self.audioController2 = [[SPCoreAudioController alloc] init];
self.audioController2.delegate = self;
and in
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
...
self.audioController.audioOutputEnabled = self.playbackSession.isPlaying;
// for crossfade, add
self.audioController2.audioOutputEnabled = self.playbackSession.isPlaying;
and added a new method based on playTrack
-(void)crossfadeTrack:(SPTrack *)aTrack callback:(SPErrorableOperationCallback)block {
// switch audiocontroller from current to other
if (self.playbackSession.audioDeliveryDelegate == self.audioController)
{
self.playbackSession.audioDeliveryDelegate = self.audioController2;
self.audioController2.delegate = self;
self.audioController.delegate = nil;
}
else
{
self.playbackSession.audioDeliveryDelegate = self.audioController;
self.audioController.delegate = self;
self.audioController2.delegate = nil;
}
if (aTrack.availability != SP_TRACK_AVAILABILITY_AVAILABLE) {
if (block) block([NSError spotifyErrorWithCode:SP_ERROR_TRACK_NOT_PLAYABLE]);
self.currentTrack = nil;
}
self.currentTrack = aTrack;
self.trackPosition = 0.0;
[self.playbackSession playTrack:self.currentTrack callback:^(NSError *error) {
if (!error)
self.playbackSession.playing = YES;
else
self.currentTrack = nil;
if (block) {
block(error);
}
}];
}
this starts a timer for crossfade
crossfadeTimer = [NSTimer scheduledTimerWithTimeInterval: 0.5
target: self
selector: #selector ( crossfadeCountdown)
userInfo: nil
repeats: YES];
And in order to keep the first track playing after its data has loaded in SPCoreAudioController.m I changed target buffer length:
static NSTimeInterval const kTargetBufferLength = 20;
and in SPSession.m : end_of_track(sp_session *session) {
I removed
// sess.playing = NO;
I call preloadTrackForPlayback: about 15 seconds before end of track, then crossfadeTrack: at 10 seconds before.
Then set crossfadeCountdownTime = [how many seconds you want the crossfade]*2;
I fade volume over the crosssfade with:
- (void) crossfadeCountdown
{
[UIAppDelegate.playbackSPManager setVolume:(1- (((float)crossfadeCountdownTime/ (thisCrossfadeSeconds*2.0)) *0.2) )];
crossfadeCountdownTime -= 0.5;
if (crossfadeCountdownTime == 1.0)
{
NSLog(#"Crossfade countdown done");
crossfadeCountdownTime = 0;
[crossfadeTimer invalidate];
crossfadeTimer = nil;
[UIAppDelegate.playbackSPManager setVolume:1.0];
}
}
I'll keep working on it, and update if I can make it better. Thanks again to iKenndac for his always spot-on help!
There isn't a pre-written crossfade example that I'm aware of that uses CocoaLibSpotify. However, a (perhaps not ideal) game plan would be:
Make two separate audio queues. SPCoreAudioController is an encapsulation of an audio queue, so you should just be able to instantiate two of them.
Play music as normal to one queue. When you're approaching the end of the track, call SPSession's preloadTrackForPlayback:callback: method with the next track to get it ready.
When all audio data for the playing track has been delivered, SPSession will fire the audio delegate method sessionDidEndPlayback:. This means that all audio data has been delivered. However, since CocoaLibSpotify buffers the audio from libspotify, there's still some time before audio stops.
At this point, start playing the new track but divert the audio data to the second audio queue. Start ramping down the volume of the first queue while ramping up the volume of the next one. This should give a pleasing crossfade.
A few pointers:
In SPCoreAudioController.m, you'll find the following line, which defines how much audio CocoaLibSpotify buffers, in seconds. If you want a bigger crossfade, you'll need to increase it.
static NSTimeInterval const kTargetBufferLength = 0.5;
Since you get audio data at a maximum of 1.5x actual playback speed, be careful not to do, for example, a 5 second crossfade when the user has just skipped near to the end of the track. You might not have enough audio data available to pull it off.
Take a good look at SPPlaybackManager.m. This class is the interface between CocoaLibSpotify and Core Audio. It's not too complicated, and understanding it will get you a long way. SPCoreAudioController and SPCircularBuffer are pretty much implementation details of getting the audio into Core Audio, and you shouldn't need to understand their implementations to achieve what you want.
Also, make sure you understand the various delegates SPSession has. The audio delivery delegate only has one job - to receive audio data. The playback delegate gets all other playback events - when audio has finished being delivered to the audio delivery delegate, etc. There's nothing stopping one class being both, but in the current implementation, SPPlaybackManager is the playback delegate, which creates an instance of SPCoreAudioController to be the audio delivery delegate. If you modify SPPlaybackManager to have two Core Audio controllers and alternate which one is the audio delivery delegate, you should be golden.

Playing sounds in sequence with SimpleAudioEngine

I'm building an iOS app with cocos2d 2, and I'm using SimpleAudioEngine to play some effects.
Is there a way to sequence multiple sounds to be played after the previous sound is complete?
For example in my code:
[[SimpleAudioEngine sharedEngine] playEffect:#"yay.wav"];
[[SimpleAudioEngine sharedEngine] playEffect:#"youDidIt.wav"];
When this code is run, yay.wav and youDidIt.wav play at the exact same time, over each other.
I want yay.wav to play and once complete, youDidIt.wav to play.
If not with SimpleAudioEngine, is there a way with AudioToolbox, or something else?
Thank you!
== UPDATE ==
I think I'm going to go with this method using AVFoundation:
AVPlayerItem *sound1 = [[AVPlayerItem alloc] initWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"yay" ofType:#"wav"]]];
AVPlayerItem *sound2 = [[AVPlayerItem alloc] initWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"YouDidIt" ofType:#"wav"]]];
AVQueuePlayer *player = [[AVQueuePlayer alloc] initWithItems: [NSArray arrayWithObjects:sound1, sound2, nil]];
[player play];
The easy way would be getting the track duration using -[CDSoundSource durationInSeconds] and then schedule the second effect playing after a proper delay:
[[SimpleAudioEngine sharedEngine] performSelector:#selector(playEffect:) withObject:#"youDidIt.wav" afterDelay:duration];
An easier way to get the audio duration would be patching SimpleAudioManager and add a method that queries its CDSoundEngine (a static global) for the audio duration:
- (float)soundDuration {
return [_engine bufferDurationInSeconds:_soundId];
}
The second approach would be polling on the status of the audio engine and wait for it to stop playing.
alGetSourcei(sourceId, AL_SOURCE_STATE, &state);
if (state == AL_PLAYING) {
...
The sourceId is the ALuint returned by playEffect.
It's simple, tested with Cocos2D 2.1:
Creates a property durationInSeconds on SimpleAudioEngine.h :
#property (readonly) float durationInSeconds;
Synthesize them :
#synthesize durationInSeconds;
Modify playEffect like this :
-(ALuint) playEffect:(NSString*) filePath pitch:(Float32) pitch pan:(Float32) pan gain:(Float32) gain {
int soundId = [bufferManager bufferForFile:filePath create:YES];
if (soundId != kCDNoBuffer) {
durationInSeconds = [soundEngine bufferDurationInSeconds:soundId];
return [soundEngine playSound:soundId sourceGroupId:0 pitch:pitch pan:pan gain:gain loop:false];
} else {
return CD_MUTE;
}
}
As you can see, I inserted durationInSeconds value directly from soundEngine bufferDurationInSeconds from the result of bufferManager.
Now, only ask for the value [SimpleAudioEngine sharedEngine].durationInSeconds and you've got the float duration in seconds of this sound.
Put a timer this seconds and after this, play the next iteration of your sound or put the flag OFF or something.

Resources