ios - Loading an MP3 from remote URL and playing, freezing UI - ios

I am building a very simple app that plays a very small (less than 30 seconds of audio) static MP3 file from the Internet. The files are listed in a TableView and clicking on a cell opens a View that in turn loads and plays the remote MP3. Audio is played using the AVAudioPlayer. Please note that I am NOT attempting to stream live, continuous audio - just a brief clip. Here is an example of what I am trying to play - https://s3.amazonaws.com/tevfd-recording/All_Fire_and_EMS_2015-02-0314_38_54_457551.mp3
Initially, I downloaded the data of the MP3 file using [NSData dataWithContentsOfURL:], which worked fine except for the fact that there is a delay between tapping a row and loading the view / playing the audio. One thing that I like about this approach is that the audio stops playing when the user leaves the View (back button press).
NSURL *callAudioURL = [NSURL URLWithString:[self.call objectForKey:#"url"]];
NSData *callAudioData = [NSData dataWithContentsOfURL:callAudioURL];
AVAudioPlayer *audio = [[AVAudioPlayer alloc] initWithData: callAudioData error:nil];
self.player = audio;
// Set slider to change position in audio
self.slider.minimumValue = 0;
self.slider.maximumValue = self.player.duration;
[self.player prepareToPlay];
[[self player] play];
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(updateSeekBar) userInfo:nil repeats:YES];
In an attempt to eliminate the delay between the views, I wrapped the loading of the file in a call to `dispatch_async' -
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSURL *callAudioURL = [NSURL URLWithString:[self.call objectForKey:#"url"]];
NSData *callAudioData = [NSData dataWithContentsOfURL:callAudioURL];
dispatch_async(dispatch_get_main_queue(), ^{
AVAudioPlayer *audio = [[AVAudioPlayer alloc] initWithData: callAudioData error:nil];
self.player = audio;
self.slider.minimumValue = 0;
self.slider.maximumValue = self.player.duration;
[self.player prepareToPlay];
[[self player] play];
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(updateSeekBar) userInfo:nil repeats:YES];
});
});
This works pretty well - the segue completes quickly and the data is loaded off of the main thread. However, if the user presses the Back button while the audio is playing, it continues.
My questions:
Am I on the right track with dispatch_async for this? If so, how should I go about killing the player when the user presses the Back button?
Thanks!

You should probably move away from using -[NSData dataWithContentsOfURL:] and start managing the network operation yourself using NSURLSession. -[NSData dataWithContentsOfURL:] is always going to load 100% of the data before playing anything, and it's synchronous, so calling it on the main thread is a good way to get the OS to kill your app for stalling the main thread. Doing the loading with dispatch_async keeps you from stalling the main thread, but doesn't solve the problem where it's going to load 100% of the data before you can do anything with it.

Related

Pausing Audio file to play different Audio file

I'm using the systemMusicPlayer to play songs inside an app.
If a user pushes noiseButton on the Now Playing view, it will pause the systemMusicPlayer song audio file for 10 seconds to play the noiseButton audio file, and then pick back up on the systemMusicPlayer song audio file.
I'm able to get the systemMusicPlayer song audio file to pause and then pick back up after 10 seconds, but I am not able to get the noiseButton to play its sound in the 10 seconds.
I know the noiseButton audio file works
I'm NSLogging before and after the method that plays the noiseButton file
I'm not sure what I'm missing?
Here is my code:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
musicPlayer = [MPMusicPlayerController systemMusicPlayer];
[self registerMediaPlayerNotifications];
// Construct URL to sound file
NSString *path = [NSString stringWithFormat:#"%#/background-music-aac.caf", [[NSBundle mainBundle] resourcePath]];
NSURL *soundUrl = [NSURL fileURLWithPath:path];
// Create audio player object and initialize with URL to sound
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundUrl error:nil];
}
- (void)willPlayClip {
[NSTimer scheduledTimerWithTimeInterval:10.0
target:self
selector:#selector(willPlayMusicInTenSeconds:)
userInfo:nil
repeats:NO];
}
- (void)willPlayMusicInTenSeconds:(NSTimer *)timer {
NSLog(#"Get ready to Play noise sound");
[_audioPlayer play];
NSLog(#"Play noise sound");
[musicPlayer play];
NSLog(#"Start playing again");
}
And the code inside the noiseButton for when it gets pressed:
if ([musicPlayer playbackState] == MPMusicPlaybackStatePlaying) {
[musicPlayer pause];
NSLog(#"Paused!");
[self willPlayClip];
} else {
NSLog(#"Already paused");
}
What AudioSession category are you setting? If you're not, refer to this documentation:
Audio Session Categories
I believe that you should get the desired behaviour if you set your session to AVAudioSessionCategoryAmbient:
The category for an app in which sound playback is nonprimary—that is, your app can be used successfully with the sound turned off.
This category is also appropriate for “play along” style apps, such as a virtual piano that a user plays while the Music app is playing. When you use this category, audio from other apps mixes with your audio. Your audio is silenced by screen locking and by the Silent switch (called the Ring/Silent switch on iPhone).
Give that a try if you're not already.

Using NSURLSession to play remote audio, code critique

First, to avoid posting a great deal of code on here, I have created a very basic example of what I am after - https://github.com/opheliadesign/AudioTest.
I am brand new to iOS development and I'm having a little trouble understanding the best way to load and play a static MP3 file on a remote server. The audio is not streaming live, is less than a megabyte - about 30 seconds long in average (they are radio dispatches).
It has been suggested that I use NSURLSession to load the MP3 file and then play it within the completion block. This seems to be working but I have a feeling that I could be handling it better, just do not know how. Here is the block where I grab the audio and begin playing:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:YES];
NSLog(#"View loaded");
// Register to receive notification from AppDelegate when entering background
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(stopPlayingAudio) name:#"stopPlayingAudio" object:nil];
// Assign timer to update progress
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(updateSeekBar) userInfo:nil repeats:YES];
// Demo recording URL
NSString *recordingUrl = #"https://s3.amazonaws.com/tevfd-recording/All_Fire_and_EMS_2015-02-0214_48_33_630819.mp3";
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:recordingUrl] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
NSLog(#"No error..");
self.player = [[AVAudioPlayer alloc]initWithData:data error:nil];
// Setup slider for track position
self.slider.minimumValue = 0;
self.slider.maximumValue = self.player.duration;
[self.player prepareToPlay];
[self.player play];
}
}]resume];
}
For example, have a slider that updates the player's position when it is moved. I initialize the AVAudioPlayer in the completion block but create a property for it in the header. If the player has not been initialized, could moving the slider cause a crash? If so, how should I handle this better?
Also, I have a timer that updates the slider position as the AVAudioPlayer plays. When the track reaches its end, the timer continues - clearly this is a potential memory issue. Not sure of the best way to handle this and I would also like for the user to be able to start playing the recording again after it is completed, for example if they moved the slider to the right.
I have searched and searched, cannot seem to find anything related to specifically what I am doing. Any help would be greatly appreciated.
UPDATE
This is what I first began with but I experienced some lag between clicking on a UITableView cell and presenting the view that loaded/played the audio. Several suggested NSURLSession.
- (void)viewDidLoad {
[super viewDidLoad];
// Get the URL
NSURL *callAudioURL = [NSURL URLWithString:[self.call objectForKey:#"url"]];
NSData *callAudioData = [NSData dataWithContentsOfURL:callAudioURL];
AVAudioPlayer *audio = [[AVAudioPlayer alloc] initWithData: callAudioData error:nil];
self.player = audio;
self.slider.minimumValue = 0;
self.slider.maximumValue = self.player.duration;
[[self player] play];
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(updateSeekBar) userInfo:nil repeats:YES];
}
If you're concerned about a small delay, then downloading and caching it is the best solution. You need to download the audio file and then play it locally. Typically playing -- or streaming -- from the network is useful only when the audio is so unique that you cannot store it locally.
To implement something like this (code untested):
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSFileHandle *hFile = [NSFileHandle fileHandleForWritingAtPath:self.localPath];
//did we succeed in opening the existing file?
if (!hFile) { //nope->create that file!
[[NSFileManager defaultManager] createFileAtPath:self.localPath contents:nil attributes:nil];
//try to open it again...
hFile = [NSFileHandle fileHandleForWritingAtPath:self.localPath];
}
//did we finally get an accessable file?
if (!hFile) {
NSLog("could not write to file %#", self.localPath);
return;
}
#try {
//seek to the end of the file
[hFile seekToEndOfFile];
//finally write our data to it
[hFile writeData:data];
} #catch (NSException * e) {
NSLog("exception when writing to file %#", self.localPath);
result = NO;
}
[hFile closeFile];
}
When all of the data has been received, start your audio:
self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:self.localPath error:nil];

AVAudioPlayer returns wrong duration for audio file

I'm trying to play an audio file in my app, and I want to show progress of playing file.
The file completely plays but I can't get correct duration of file to display. The returned value is considerably lower than the actual duration. For example it returns 1 second for a 4-Seconds audio file, or 9 seconds for a 41-Seconds one.
Please do not say it only returns correct value while playing, because I tried that too and it doesn't. I always get 1 second for any file using AVAudioPlayer duration method, and I tried the solution here via AVURLAssets I get that weird 9 seconds instead of 41 !
There's also another problem with AVAudioPlayer. It plays the file but meanwhile its isPlaying method returns NO
I don't know if it makes any difference but my audio file is an AMR file.
Any help is really appreciated. Thanks.
**** UPDATE : I TRIED WITH OTHER FILES, IT WORKS FINE, SO NOW, I NEED TO GET THE DURATION OF AN AMR AUDIO FILE ****
The below code may help you to get the right progress on slider
NSURL *soundFileURL = [NSURL fileURLWithPath:path];
NSError *error;
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL error:&error];
_sliderSong.maximumValue = _audioPlayer.duration; //_sliderSong is a UISlider property
_sliderSong.value = 0.0; //_audioPlayer is a AVAudioPlayer property
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(updateTime:) userInfo:nil repeats:YES];
if (error)
{
NSLog(#"Error in audioPlayer: %#",
[error localizedDescription]);
} else {
_audioPlayer.delegate = self;
[_audioPlayer prepareToPlay];
[_audioPlayer play];
}
}
- (IBAction)slide { //called when we drag the slider explicitly (we have to bind it with slider)
_audioPlayer.currentTime = _sliderSong.value;
}
- (void)updateTime:(NSTimer *)timer { //slider updation
_sliderSong.value = _audioPlayer.currentTime;
}

Why won't this background music stop when I tell it to?

Coding a game for iOS 7, and I set it up so the music plays continuously. I've also set it up so that when the player is dead, to stop the music, but it continues to run the music until the clip of music is done. (To clarify, the clip is 7 seconds long, and for example, if the music is done at 0:03, it will continue into the next scene, until it hits 7)
Here's the code:
[self runAction:[SKAction repeatActionForever:[SKAction playSoundFileNamed:#"bgmusic.m4a" waitForCompletion:YES]]];
if (_dead == YES)
[self removeAllActions];
}
I want the music to stop the second the player dies, as opposed to finishing the action. How can I code it so it does so? I had assumed that [self removeAllActions]; would have done that? Is there a specific thing I can call to stop it instantaneously?
Thanks.
Try using this code (AVAudioPlayer)...
NSURL * backgroundMusicURL = [[NSBundle mainBundle] URLForResource:#"warmovie" withExtension:#"caf"];
self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
self.backgroundMusicPlayer.numberOfLoops = -1;
[self.backgroundMusicPlayer prepareToPlay];
[self.backgroundMusicPlayer play];
To stop...
[self.backgroundMusicPlayer stop]

AVAudioPlayer working fine in 5.0, issues in 4.0-4.3.5

I have an app which plays and controls music across different ViewControllers. To do this, I created two instances of AVAudioPLayer in the app delegate's DidFinishLaunchingMethod:
NSString *path = [[NSBundle mainBundle] pathForResource:#"minnie1" ofType:#"mp3"];
NSURL *path1 = [[NSURL alloc] initFileURLWithPath:path];
menuLoop =[[AVAudioPlayer alloc] initWithContentsOfURL:path1 error:NULL];
[menuLoop setDelegate:self];
menuLoop.numberOfLoops = -1;
[menuLoop play];
NSString *path2 = [[NSBundle mainBundle] pathForResource:#"minnie2" ofType:#"mp3"];
NSURL *path3 = [[NSURL alloc] initFileURLWithPath:path2];
gameLoop=[[AVAudioPlayer alloc] initWithContentsOfURL:path3 error:NULL];
gameLoop.numberOfLoops = -1;
[gameLoop setDelegate:self];
[gameLoop prepareToPlay];
After this I call it in various viewControllers, to stop or restart using code like:
- (IBAction)playGameLoop{
NSLog(#"Begin playGameLoop");
FPAppDelegate *app = (FPAppDelegate *)[[UIApplication sharedApplication] delegate];
if ([FPAVManager audioEnabled] == NO){
//DO NOTHING
}
else {
if (app.gameLoop.playing ==YES ) {
//DO NOTHING
}
else { [app.gameLoop play];
NSLog(#"End playGameLoop");
}
}
The audio files play fine the first time and they stop when asked to stop. Though, on iOS4 devices, they won't start replaying when called again.
Thanks!
From the docs:
Calling this method [stop] or allowing a sound to finish playing,
undoes the setup performed upon calling the play or prepareToPlay
methods.
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.
Where you are looping the sounds, I would expect to be able to call stop and then call play. But I have fuzzy recollection of running into this myself. I found that calling pause, rather than stop, was a better solution. Especially since I could then call play again to resume play. Calling stop always seemed to require calling prepareToPlay again before calling play, at least in my experience.
It's possible that there were some changes in the API implemetation between iOS4.x and iOS5, too. You should check the API diffs on the Developer web site to be sure.

Resources