SKAction playSoundFileNamed failed at 500 mp3 - ios

In my application I need to use a lot of short different mp3s (about 500 items one by one)
So I use SKAction playSoundFileNamed
After ~200 sounds it crashed with 'Failed to load resource - Resource s234.mp3 can not be loaded'. Memory rises to 70mb.
How to avoid this?
What I tried:
recreate sound in every iteration
SKAction *mySound=[SKAction playSoundFileNamed:aa waitForCompletion:YES];
create the one variable in the beggining of .m
SKAction *mySound;
and reuse it in iterations
mySound=[SKAction playSoundFileNamed:aa waitForCompletion:YES];
2.
load all sounds to array once at start
for (int j=0;j<500;j++){
NSString *aa=[NSString stringWithFormat:#"s%d.mp3", j];
[item.sounds addObject:[SKAction playSoundFileNamed:aa waitForCompletion:YES]];
}
...but never changed - it crashes and can't load mp3.
How to clean this memory leaks?
EDITED
I also tried to turn off ARC and manually dealloc it every time. Nothing changed.

This little ditty right here will allow you to use a regular old SKAction but customize the playback in code.
https://github.com/pepelkod/iOS-Examples/tree/master/PlaySoundWithVolume
+(SKAction*)playSoundFileNamed:(NSString*)fileName atVolume:(CGFloat)volume waitForCompletion:(BOOL)wait{
// setup audio
NSString* nameOnly = [fileName stringByDeletingPathExtension];
NSString* extension = [fileName pathExtension];
NSURL *soundPath = [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:nameOnly ofType:extension]];
AVAudioPlayer *player = [[AVAudioPlayer alloc]initWithContentsOfURL:soundPath error:NULL];
[player setVolume:volume];
[player prepareToPlay];
SKAction* playAction = [SKAction runBlock:^{
[player play];
}];
if(wait == YES){
SKAction* waitAction = [SKAction waitForDuration:player.duration];
SKAction* groupActions = [SKAction group:#[playAction, waitAction]];
return groupActions;
}
return playAction;
}

You may be better off with a "proper" sound engine. You could use AVAudioPlayer or even better, ObjectAL (already included in Kobold Kit). That way you have more control over preloading and caching the sound files, and treating streaming audio (MP3) differently from short-lived sound effects (usually CAF or WAV).
For very short sound files, say shorter than 5 seconds, MP3 isn't ideal. You should try CAF/WAV instead.
And do consider how much your sound files use in memory. Say each mp3 file is buffered into a 250 KB buffer, times 500, then that uses over 120 MB of memory. Do not look at the size of the mp3 file because it's a compressed format, and will likely be buffered uncompressed.

I think you're running out of file descriptors. What i've found is that every time playSoundFileNamed is run the sound file is opened and never closed... so the file descriptor is not released.
My testing in instruments leads me to believe there's about 250 available file descriptors. And my research tells me that not only file access uses them up but other things too. So I think your ~200 sound files sounds just about right to crash. Whenever a sound file is the next file that Xcode is trying to access I get nothing from the debugger, but when the next file is a png (I have way more art then sounds) it gives,
error = 24 (Too many open files)
I found the error while play testing and switching back to my menu scene from the game play scene every time I lost. So normally it didn't matter but running the init method for the game play scene over and over was piling up these open sound files.
I've searched and searched for a way to close these files but have come up with nothing. I'm thinking of implementing a singleton to run my sound and moving all of the playSoundFileNamed calls into it so they only ever get called once. I think this is a bug from Apple. These files should be closing with ARC. Has anyone found anything similar?

Related

play a sound from an arbitrary position

I'm looking for a way to play an audio file from just any point in the file.
Currently, whenever I play the file, it just starts from the beginning.
Since it is a loop, I could choose to let it loop, and open/close the volume at will, but I think it is kind of a lame solution, not in the least because of battery concerns.
I really did some research on this, but it is either a freak requirement, or I don't know how to phrase my queries.
Thanks in advance for any tip.
That should be a nice easy problem to solve. You don't post any code, so we can't see what you're trying at the moment. What I'd do is this:
NSString *soundPath = [[NSBundle mainBundle] pathForResource:#"SoundFile" ofType:#"wav"];
AVAudioPlayer * soundData =[[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:soundPath] error:NULL];
[soundData stop];
soundData.currentTime = 22; //This will start playback 22 seconds into the file
[soundData play];
It goes without saying that you need to check the duration of your sound file so that you don't select a time beyond its end!
currentTime is a float (so you can specify millisecond values). You can read more here: https://developer.apple.com/library/ios/documentation/AVFoundation/Reference/AVAudioPlayerClassReference/#//apple_ref/occ/instp/AVAudioPlayer/currentTime

What am I doing wrong with SKAction playSoundFileNamed so it will play .wavs but not .mp3s?

I've run into this numerous times now. Client send me some .mp3s to put in the project. When I try to play them using this code:
SKAction *play = [SKAction playSoundFileNamed:#"birds.mp3" waitForCompletion:NO];
[self runAction:play];
[self runAction:play];
The app gives me an exception and crashes on that line.
I took the mp3 and I converted it to .wav (in Reaper), change the code to load that, and no problem, plays perfect. I thought maybe the original mp3 had some encoding trouble, so I open it in Reaper, re-export is as a new mp3, still crash.
When it crashes, I look in the stack trace (I think I am using that right) and it shows this, which looks like it is having trouble with the ID3 maybe though? Reaper does encode mp3s using LAME, I don't know if it isn't compliant or something? Anyone run into this?
0x187321a98: b 0x187321a54 ; ID3ParserHandle::ID3ParserHandle(void*, int (*)(void*, unsigned int, unsigned int, unsigned int, void**, unsigned int*)) + 492
Things like that are relatively common. Some programs create non-standard audio files, while AVPlayer may have some problems with certain standard but uncommonly used file containers or features or compression format.
The easiest fix is to reencode the file using a different program. Audacity for example. In this case make sure to remove any ID3 tags (author, album name, etc) from the mp3 if the problem persists.

Stream video while downloading iOS

I am using iOS 7 and I have a .mp4 video that I need to download in my app. The video is large (~ 1 GB) which is why it is not included as part of the app. I want the user to be able to start watching the video as soon as is starts downloading. I also want the video to be able to be cached on the iOS device so the user doesn't need to download it again later. Both the normal methods of playing videos (progressive download and live streaming) don't seem to let you cache the video, so I have made my own web service that chunks up my video file and streams the bytes down to the client. I start the streaming HTTP call using NSURLConnection:
self.request = [[NSMutableURLRequest alloc] initWithURL:self.url];
[self.request setTimeoutInterval:10]; // Expect data at least every 10 seconds
[self.request setHTTPMethod:#"GET"];
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:YES];
When I receive a data chunk, I append it to the end of the local copy of the file:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:[self videoFilePath]];
[handle truncateFileAtOffset:[handle seekToEndOfFile]];
[handle writeData:data];
}
If I let the device run, the file is downloaded successfully and I can play it using MPMoviePlayerViewController:
NSURL *url=[NSURL fileURLWithPath:self.videoFilePath];
MPMoviePlayerViewController *controller = [[MPMoviePlayerViewController alloc] initWithContentURL:url];
controller.moviePlayer.scalingMode = MPMovieScalingModeAspectFit;
[self presentMoviePlayerViewControllerAnimated:controller];
However, if I start the player before the file is completely downloaded, the video starts playing just fine. It even has the correct video length displayed at the top scrubber bar. But when the user gets to the position in the video that I had completed downloading before the video started, the video just hangs. If I close and reopen the MPMoviePlayerViewController, then the video plays until it gets to whatever location I was then at when I launched the MPMoviePlayerViewController again. If I wait until the entire video is downloaded, then the video plays without a problem.
I am not getting any events fired, or error messages printed to the console when this happens (MPMoviePlayerPlaybackStateDidChangeNotification and MPMoviePlayerPlaybackDidFinishNotification are never sent after the video starts). It seems like there is something else that is telling the controller what the length of the video is other than what the scrubber is using...
Does anyone know what could be causing this issue? I am not bound to using MPMoviePlayerViewController, so if a different video playback method would work in this situation I am all for it.
Related Unresolved Questions:
AVPlayer and Progressive Video Downloads with AVURLAssets
Progressive Video Download on iOS
How to play an in downloading progress video file in IOS
UPDATE 1
I have found that the video stall is indeed because of the file size when the video starts playing. I can get around this issue by creating a zero-ed out file before I start the download and over overwrite it as I go. Since I have control over the video streaming server, I added a custom header so I know the size of the file being streamed (default file size header for a streaming file is -1). I am creating the file in my didReceiveResponse method as follows:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
// Retrieve the size of the file being streamed.
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSDictionary *headers = httpResponse.allHeaderFields;
NSNumberFormatter * formatter = [[NSNumberFormatter alloc] init];
[formatter setNumberStyle:NSNumberFormatterDecimalStyle];
self.streamingFileSize = [formatter numberFromString:[headers objectForKey:#"StreamingFileSize"]];
// Check if we need to initialize the download file
if (![[NSFileManager defaultManager] fileExistsAtPath:self.path])
{
// Create the file being downloaded
[[NSData data] writeToFile:self.path atomically:YES];
// Allocate the size of the file we are going to download.
const char *cString = [self.path cStringUsingEncoding:NSASCIIStringEncoding];
int success = truncate(cString, self.streamingFileSize.longLongValue);
if (success != 0)
{
/* TODO: handle errors here. Probably not enough space... See 'man truncate' */
}
}
}
This works great, except that truncate causes the app to hang for about 10 seconds while it creates the ~1GB file on disk (on the simulator it is instant, only a real device has this problem). This is where I am stuck now - does anyone know of a way to allocate a file more efficiently, or a different way to get the video player to recognize the size of the file without needing to actually allocate it? I know some filesystems support "file size" and "size on disk" as two different properties... not sure if iOS has something like that?
I figured out how to do this, and it is much simpler than my original idea.
First, since my video is in .mp4, the MPMoviePlayerViewController or AVPlayer class can play it directly from a web server - I don't need to implement anything special and they can still seek to any point in the video. This must be part of how the .mp4 encoding works with the movie players. So, I just have the raw file available on the server - no special headers required.
Next, when the user decides to play the video I immediately start playing the video from the server URL:
NSURL *url=[NSURL fileURLWithPath:serverVidelFileURLString];
controller = [[MPMoviePlayerViewController alloc] initWithContentURL:url];
controller.moviePlayer.scalingMode = MPMovieScalingModeAspectFit;
[self presentMoviePlayerViewControllerAnimated:controller];
This makes it so the user can watch the video and seek to any location they want. Then, I start downloading the file manually using NSURLConnection like I had been doing above, except now I am not streaming the file, I just download it directly. This way I don't need the custom header since the file size is included in the HTTP response.
When my background download completes, I switch the playing item from the server URL to the local file. This is important for network performance because the movie players only download a few seconds ahead of what the user is watching. Being able to switch to the local file as soon as possible is key to avoid downloading too much duplicate data:
NSTimeInterval currentPlaybackTime = videoController.moviePlayer.currentPlaybackTime;
[controller.moviePlayer setContentURL:url];
[controller.moviePlayer setCurrentPlaybackTime:currentPlaybackTime];
[controller.moviePlayer play];
This method does have the user downloading two video files at the same time initially, but initial testing on the network speeds my users will be using shows it only increases the download time by a few seconds. Works for me!
You gotta create an internal webserver that acts like a proxy! Then set your player to play the movie from the localhost.
When using HTTP protocol to play a video with MPMoviePlayerViewController, the first thing the player does is to ask for the byte-range 0-1 (first 2 bytes) just to obtain the file length. Then, the player asks for "chunks" of the video using the "byte-range" HTTP command (the purpose is to save some battery).
What you have to do is to implement this internal server that delivers the video to the player, but your "proxy" must consider the length of your video as the full length of the file, even if the actual file hasn't been completely downloaded from the internet.
Then you you set your player to play a movie from " http:// localhost : someport "
I've done this before... it works perfectly!
Good luck!
I can only assume that the MPMoviePlayerViewController caches the file length of the file when you started it.
The way to fix (just) this issue is to first determine how large the file is. Then create a file of that length. Keeping an offset pointer, as the file downloads, you can overwrite the "null" values in the file with the real data.
So you get to a specific point in the download, start the MPMoviePlayerViewController, and let it run. I'd also suggest you use the "F_NOCACHE" flag (with fcntl()) so you bypass the file block cache (which means you will lower your memory footprint).
The downside to this architecture is that if you get stalled, and the movie player gets ahead of you, well, the user is going to have a pretty bad experience. Not sure if there is any way for you to monitor and take preemptive action.
EDIT: its quite possible that the video is not read sequentially, but certain information requires the player to essentially look ahead for something. If so, then this is doomed to fail. The only other possible solution is to use some software tool to sequentially order the file (I'm no video expert so cannot comment from experience on any of the above).
To test this out, you can construct a "damaged" video of varying lengths, and test that to see what works and what does not. For instance, suppose you have a 100Meg file. Write a little utility program, and over write the last 50Megs of data with zeros. Now play this video. Its should fail 1/2 through. If it fails right away, well, you now know that its seeking in the file.
If non sequential, its possible that its looking at the last 1000 bytes or so, in which case if you don't overwrite that things work as you want. If you get lucky and this is the case, you would eventually download the last 1000 bytes, then then start from the front of the file.
It really gets down to finding some way before introducing real networking into the picture, to play a partial file. You will surely find it easier to artificially introduce the networking conditions without really doing it real time.

Playing short selfmade sound with immediate start in iOS

I have a self-generated DTMF sound (with a wav header) generated by program that I want to be able to play quickly, in fact as soon as the user touches a button. This DTMF sound must play/loop infinitely, until I stop it. Some other sounds must be able to be played at the same time.
I'm very new to Audio programming, and I tested many ways of doing that and I'm lost now.
How can I achieve that ?
Needs :
very quick playback start (including the first time)
many sounds at the same time (short sounds +- 2-6 seconds)
infinite DTMF sound without gaps
having control over the different sounds that are playing / being able to stop just one played sound
AVAudioPlayer if you can live with some latency, OpenAL (for example Finch) if you really need to have the latency as low as possible.
I use already exist .wav file. and I can easily play it.
For run following code. include AudioToolbox framework.
write into .h file #import <AudioToolbox/AudioToolbox.h>
Write into .m file
-(IBAction)startSound{
//Get the filename of the sound file:
NSString *path = [NSString stringWithFormat:#"%#%#", [[NSBundle mainBundle] resourcePath], #"/sound1.wav"];
//declare a system sound
SystemSoundID soundID;
//Get a URL for the sound file
NSURL *filePath = [NSURL fileURLWithPath:path isDirectory:NO];
//Use audio sevices to create the sound
AudioServicesCreateSystemSoundID((CFURLRef)filePath, &soundID);
//Use audio services to play the sound
[self sound];
timer =[ [NSTimer scheduledTimerWithTimeInterval:2.1 target:self selector:#selector(sound) userInfo:nil repeats:YES]retain];
}

iOS AVAudioPlayer multiple instances, multiple sounds at once

I am working on an interactive children's book for the iPad that has a "read to me" option.
For each page (that has an index), theres an audio clip that produces the "read to me" feature. The feature works well, except for the fact that when I turn the page, the previous pages audio still plays, even when the new audio starts, here's my example:
- (void) didTurnToPageAtIndex:(NSUInteger)index
{
if ([delegate respondsToSelector:#selector(leavesView:didTurnToPageAtIndex:)])
[delegate leavesView:self didTurnToPageAtIndex:index];
if([[NSUserDefaults standardUserDefaults] boolForKey:#"kReadToMe"] == YES)
{
NSString* filename = [voices objectAtIndex:index];
NSString *path = [[NSBundle mainBundle] pathForResource:filename ofType:#"m4v"];
NSLog(#"File: %#, Index: %i",path,index);
//Create new audio for next page
AVAudioPlayer * newAudio = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:path] error:nil];
rtmAudio = newAudio; // automatically retain audio and dealloc old file if new file is loaded
//[newAudio release];
[rtmAudio play];
}
}
Say for instance, I turn to page 3 before the audio for page 2 stops playing, both clips play over eachother, which will annoy the sh*t out of kids, I know it does me.
I've tried placing [rtmAudio stop] before I allocate the new file, but that doesnt seem to work. I need a way to kill the prevous audio clip before starting the new clip.
I would suggest that you have one instance of the audio player in your whole application. Then you can check if it playing, if so stop it and then move on.
you are creating a new player in this method before stopping the old.... I believe
As AlexChaffee mentioned the Apple docs, "Play multiple sounds simultaneously, one sound per audio player, with precise synchronization". It seems preferable to use multipe instances across the app with NSNotificationCenter's notifications.

Resources