I am making a counting game for kids. At the start the child is asked find a number of items, for example: "Can you find five bikes". This is all randomly put together from seperate arrays of sounds with parts of the sentence: "Can you find" + "5" + "bikes" =three seperate mp3s. When the child starts clicking on the items, they disappear and the voice counts up. "1", "2", "3","4", "5", and in the end "bikes".
I use the audioPlayerDidFinishPlaying: delegate method to put the sounds together and that works out fine ... most of the time. But sometimes the app crashes, with a "bad_access" error. After using NSZombie I got: -[AVAudioPlayer performSelector:withObject:]: message sent to deallocated instance
I think this is because either the audioplayer itself or the delegate is released prematurely.
I always use this function to play the sounds:
-(void)spillVoice:(NSString*) filnavn{
NSString *audioFilePath=[[NSBundle mainBundle] pathForResource:filnavn ofType:#"mp3"];
NSURL *audioFileURL=[NSURL fileURLWithPath:audioFilePath];
self.voicespiller=[[[AVAudioPlayer alloc] initWithContentsOfURL:audioFileURL error:nil] autorelease];
self.voicespiller.delegate=self;
[self.voicespiller prepareToPlay];
[self.voicespiller play];
NSLog(#"spiller lyden");
}
And here is the delegate (it performs different actions based on what sound has finnished):
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer*)which_player successfully:(BOOL)the_flag{
NSLog(#"ny lyd?");
[self.jenteBilde stopAnimating];
if(self.etterLyd==#"ingenting"){
NSLog(#"ingenting");
}
else if(self.etterLyd==#"lesobjekt"){
NSLog(#"lesobjekt");
self.etterLyd=#"ros";
[self spillVoice:[self.objektNedArray objectAtIndex: [self.objektnr intValue]]];
[self.jenteBilde startAnimating];
}
else if(self.etterLyd==#"introtall"){
NSLog(#"introtall");
self.etterLyd=#"introobjekt";
[self spillVoice:[self.telleOppArray objectAtIndex: [self.tilfeldig intValue]]];
[self.jenteBilde startAnimating];
}
else if(self.etterLyd==#"introobjekt"){
NSLog(#"introobjekt");
self.etterLyd=#"ingenting";
[self spillVoice:[self.objektOppArray objectAtIndex: [self.objektnr intValue]]];
[self.jenteBilde startAnimating];
}
else if(self.etterLyd==#"ros"){
NSLog(#"ros");
NSMutableArray *rosArray=[[NSMutableArray alloc] initWithObjects:#"TT_flott",#"TT_bravo",#"TT_fint",#"TT_du_er_kjempeflink",#"TT_hurra",#"TT_helt_riktig",nil];
int result=(arc4random() % (rosArray.count));
self.etterLyd=#"ingenting";
[self spillVoice:[rosArray objectAtIndex: result]];
[self.jenteBilde startAnimating];
}
}
It seems to me that the AVaudioplayers autorelease hits in too early, even though the sound has not yet finnished. I have tried not autoreleasing, and instead releasing explicitly in the delegate function. But the problem is that the sound doesnt always get to play to the end (when the child finds a new item before the voice has finnished reading)...
Can any of you shed any light on this. I would be very grateful!
You need to retain the AVAudioPlayer until you're done with it. The easiest way to do this would be to make your voicespiller property a retained property, and change your implementation to the following:
-(void)spillVoice:(NSString*)filnavn {
// stop any previous player
[self.voicespiller stop];
NSString *audioFilePath=[[NSBundle mainBundle] pathForResource:filnavn ofType:#"mp3"];
NSURL *audioFileURL=[NSURL fileURLWithPath:audioFilePath];
[self setVoicespiller:[[AVAudioPlayer alloc] initWithContentsOfURL:audioFileURL error:nil] autorelease];
self.voicespiller.delegate=self;
[self.voicespiller prepareToPlay];
[self.voicespiller play];
NSLog(#"spiller lyden");
}
When you call the property's mutator ([self setVoicespiller:...]), the previous player will be released and the new one will be set. Then just make sure to call [voicespiller release] in your dealloc method.
Related
I have created an iPhone card game with a BGM and some sound FX. The game is working fine when I cancel the sound voids, but it seems to generate memory leaks each time I link a sound to a card. For each sound void, the code is like this:
- (void)cardLostSound {
NSString *pathForCardLostSoundFile = [[NSBundle mainBundle]
pathForResource:#"CardLost" ofType:#"wav"];
NSURL *cardLostSoundFile = [[NSURL alloc]
initFileURLWithPath:pathForCardLostSoundFile];
cardLostPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:cardLostSoundFile
error:NULL];
[cardLostSoundFile release];
[cardLostPlayer prepareToPlay];
[cardLostPlayer play];
[self performSelector:#selector(audioPlayerDidFinishPlaying:successfully:)
withObject:cardLostPlayer afterDelay:[cardLostPlayer duration]];//This is a hack I
found to force application of void audioPlayerDidFinishPlaying, which doesn't seem to
work on its own…
}
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
if (player == self. cardLostPlayer){
[self.characterComboPlayer release];
else if (player == self.cardBackPlayer){
[self.cardBackPlayer release];
}//etc.
}
Having audioPlayerDidFinishPlaying release the players doesn't seem to do a thing against memory leaks. Without it, the app always crashes after the same amount of time. With it, the game is slow, but lasts longer. Then, the app crashes anyway. When I just cancel these lines, I don't have any crash. Is there a way to keep the sounds, while having the app run smoothly and without memory leaks?
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
[self NextHeading]; // this plays an mp3 file
[self NextHeadingMeaning]; // this plays an Mp3 file
}
Only [self NextHeadingMeaning] method is called and NextHeading method is missed each time
-(IBAction) NextHeading{
[audio stop];
NSString *Filename = [[NSString alloc]initWithFormat:#"CH%#S%#",Heading,Meaning];
Filepath = [[NSBundle mainBundle]pathForResource:Filename ofType:#"mp3"];
audio = [[AVAudioPlayer alloc]initWithContentsOfURL:[NSURL fileURLWithPath:Filepath] error:NULL];
audio.delegate = self;
[audio play];
[Filename autorelease];
}
-(IBAction) NextHeadingMeaning {
[audio stop];
NSString *Filename = [[NSString alloc] initWithFormat:#"CH%#S%#",bold**Chapter**bold, Meaning];
Filepath = [[NSBundle mainBundle]pathForResource:Filename ofType:#"mp3"];
audio = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:Filepath] error:NULL];
audio.delegate = self;
[audio play];
[Filename autorelease];
}
Why is this happening and how can I resolve it ?
Please advice, thanks in advance.
You just used a single iVar (audio) as an player, and when you send NextHeading & NextHeadingMeaning message, the audio init with your sound_1.mp3 file firstly (it'll take some seconds if the mp3 file is big), then at the next moment (your first mp3 file might not inited, or has inited, but stopped followed by next message), you redo the init action with another mp3 file (sound_2.mp3), and finally, when the second mp3 file init done, audio plays sound_2.mp3. That's why you think the NextHeading is skipped.
So, to solve this problem, you can use a NSMutableArray iVar (e.g. audioPlayers), and create a local audio for both NextHeading & NextHeadingMeaning, and push it to audioPlayers.
And I think it is better to preload sound files if you can. :)
EDIT:
There's a playAtTime: method instead of play, you can delay the second audio player's playing time by this method, just like this:
[audioPlayer playAtTime:(audioPlayer.deviceCurrentTime + delay)];
delay is in seconds (NSTimeInterval).
There is no way that the first call is skipped, put a breakpoint in it or output something with NSLog() and you'll see. Most probable cause is that the first method doesn't do what you expect and this could be for various reasons - for example condition or specific timeout.
Edit:
After looking your code, it seems that you're missing some basic stuff like variable naming, variable scope and so. To simply make your code run, just replace the NSString *Filename.. string from the second method and probably it'll work. A better choice would be to visit Start Developing iOS Apps Today and follow the roadmap.
I am trying to call objectForKey: on an nsdictionary ivar, but I get an EXC_BAD_ACCESS error.
The nsdictionary is created using the JSON-framework and then retained. The first time I use it (just after I create it, same run loop) it works perfectly fine, but when I try to access it later nothing works. I am doing this code to try to figure out what is wrong:
if (resultsDic == nil) {
NSLog(#"results dic is nil.");
}
if ( [resultsDic respondsToSelector:#selector(objectForKey:)] ) {
NSLog(#"resultsDic should respond to objectForKey:");
}
The dictionary is never nil, but it always crashes on respondsToSelector. any ideas?
addition:
These are the other places, besides above, that the dictionary gets interacted with:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[connection release];
//get the data in a usable form
NSString *jsonString = [[NSString alloc] initWithData:downloadedData encoding:NSUTF8StringEncoding];
resultsDic = [jsonString JSONValue];
[self processResults];
NSLog(#"Success. Received %d bytes of data",[downloadedData length]);
[downloadedData release];
[jsonString release];
}
- (void)processResults
{
NSArray *resultsArr = [resultsDic objectForKey:#"results"];
CLLocationCoordinate2D coordinate = [self coordinateFromResult:[resultsArr objectAtIndex:0]];
NSLog(#"lat: %f lng: %f", coordinate.latitude, coordinate.longitude);
}
- (void)dealloc {
[resultsDic release];
[super dealloc];
}
After somethings retain count is decreased to 0, the object gets deallocated. This is not the same as setting it to nil. It will not be nil. Whilst you can send messages to nil, sending a message to a released object will result in an EXC_BAD_ACCESS error. If you post some of the code where it is created and used, maybe we can help you debug it. Try retaining it twice at the beginning. it's nit an elegant solution, but it might work as a quick fix.
Sounds like a classic zombie. Run it again with the environment variable NSZombieEnabled set to YES (or use the Zombies instrument in Instruments.app). That should give you much more information about what's going on.
I am running into some serious memory leaks in one of my applications I am building. I have a UINavigatonController that is inside a UITabBarview. Inside the NavView is a MKMap view. When you click an accessory button on a callout a detail view is loaded. In that detail view I am trying to populate a table from a plist using a for(object in array) loop. The plist is an array of dictionaries. I am running though the dictionaries to find one with a key that is the title of the callout and then get an array from inside that dictionary. It all works fine in the simulaor but I am getting massive memory leaks doing it the way I am. Any Idea whats going on?
- (void)viewDidLoad {
self.title = #"Route Details";
NSString *path = [[NSBundle mainBundle] pathForResource:#"stopLocation" ofType:#"plist"];
holderArray = [[NSMutableArray alloc] initWithContentsOfFile:path];
[self getRouteArray];
routeDetails.delegate = self;
routeDetails.dataSource = self;
}
-(void)getRouteArray{
for (NSMutableDictionary *dictionary in holderArray) {
//NSString *stopName = [dictionary objectForKey:#"StopName"];
//NSString *stopName = [[NSString alloc] initWithString:[dictionary objectForKey:#"StopName"]];
BOOL testString = [currentRoute isEqualToString:[dictionary objectForKey:#"StopName"]];
if (testString) {
routeArray = [[NSMutableArray alloc] initWithArray:[dictionary objectForKey:#"RouteService"]];
}
}
}
- (void)dealloc {
[routeArray release];
[routeDetails release];
[super dealloc];
}
holderArray is an ivar and so is route array. As you can see I have tried a few ways of allocating the nstrings and arrays but all seem to yield the same leaks. According to the performance tool I am leaking from NSCFString, NSCFDictionary, and the NSCFArry. I released the routeArray in the dealloc and it works fine, but if I release holderArray it crashes whenever I go back to my map from the detail view. I guess I am just really unsure as to how to deal with the strings and dictionary used in the for loop.
Just to add the detail view is being created like so:
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control{
NSString *selectedRouteName = [NSString stringWithFormat:#"%#",view.annotation.title];
RouteDetailView *rdc = [[RouteDetailView alloc] initWithNibName:#"RouteDetailView" bundle:nil];
[rdc setCurrentRoute:selectedRouteName];
[self.navigationController pushViewController:rdc animated:YES];
[rdc release];
}
Sorry if any of the above is unclear. Let me know and I can try to rephrase it.
Will testString be true for at most one key in holderArray? If so, you should probably break out of the loop after setting routeArray. If not, then you may be setting routeArray multiple times, and all but the last array you assigned to it would be leaked.
Also, I don't see you releasing holderArray.
Is there a general template or tutorial or web page that describes the procedure for creating a UIPickerview which selects short sound files and plays them upon selection or with a player? Thanks
You'll need a delegate/data-source class for your picker view - something that implements the protocols UIPickerViewDelegate and UIPickerViewDataSource. This can be whatever view controller you've got handling everything else or a separate class - either way, set the UIPickerView's delegate and dataSource properties to your instance of that class.
The class should have three instance variables - an NSArray soundArr to contain the sounds, an NSTimer timer to provide a delay after selection before the sound plays (more on that below), and an AVAudioPlayer audioPlayer to play the selected sound (for which you'll need to import the AVFoundation framework - it's only available in 2.2, as sound playback used to be a lot more complicated).
When you first load the sounds (in your controller class's -init method or whatever), stick 'em in an array along with the title you want them to display, something like this:
NSBundle *bdl = [NSBundle mainBundle];
soundArr = [[NSArray alloc] initWithObjects:[NSDictionary dictionaryWithObjectsAndKeys:#"Sound One",#"title",[NSURL URLWithString:[bdl pathForResource:#"sound1" ofType:#"wav"]],#"url",nil],
[NSDictionary dictionaryWithObjectsAndKeys:#"Sound Two",#"title",[NSURL URLWithString:[bdl pathForResource:#"sound2" ofType:#"wav"]],#"url",nil],
nil];
The methods you'll need to implement are:
-pickerView:numberOfRowsInComponent: - should return the size of soundArr
-pickerView:titleForRow:forComponent: - should return [[soundArr objectAtIndex:row] objectForKey:#"title"]
-numberOfComponentsInPickerView: - should return 1, since you've only got one column (component) to select from
-pickerView:didSelectRow:inComponent: - see below
You don't want the sound to start immediately when the row-selection delegate method gets called, or snippets of sounds will play continuously as the user scrolls the picker. Instead, use a timer with a short delay, something like this:
if(timer != nil)
{
[timer invalidate]; // remove any timer from an earlier selection
timer = nil;
}
timer = [NSTimer scheduledTimerWithTimeInterval:0.4 target:self selector:#selector(startSoundAtURL:) userInfo:[[soundArr objectAtIndex:row] objectForKey:#"url"] repeats:NO]; // and create the new one
Then, implement a -startSoundAtURL: method that sets up the AVAudioPlayer to play that sound:
- (void)startSoundAtURL:(NSURL *)url
{
if(audioPlayer != nil)
{
[audioPlayer stop];
[audioPlayer release];
}
NSError *err;
audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&err];
if(err != nil)
{
NSLog([err description]);
return;
}
[audioPlayer play];
}
That should pretty much do it.