I thought maybe the fastest way was to go with Sound Services. It is quite efficient, but I need to play sounds in a sequence, not overlapped. Therefore I used a callback method to check when the sound has finished. This cycle produces around 0.3 seconds in lag. I know this sounds very strict, but it is basically the main axis of the program.
EDIT: I now tried using AVAudioPlayer, but I can't play sounds in a sequence without using audioPlayerDidFinishPlaying since that would put me in the same situation as with the callback method of SoundServices.
EDIT2: I think that if I could somehow get to join the parts of the sounds I want to play into a large file, I could get the whole audio file to sound continuously.
EDIT3: I thought this would work, but the audio overlaps:
waitTime = player.deviceCurrentTime;
for (int k = 0; k < [colores count]; k++)
{
player.currentTime = 0;
[player playAtTime:waitTime];
waitTime += player.duration;
}
Thanks
I just tried a technique that I think will work well for you. Build an audio file with your sounds concatenated. Then build some meta data about your sounds like this:
#property (strong, nonatomic) NSMutableDictionary *soundData;
#synthesize soundData=_soundData;
- (void)viewDidLoad {
[super viewDidLoad];
_soundData = [NSMutableDictionary dictionary];
NSArray *sound = [NSArray arrayWithObjects:[NSNumber numberWithFloat:5.0], [NSNumber numberWithFloat:0.5], nil];
[self.soundData setValue:sound forKey:#"soundA"];
sound = [NSArray arrayWithObjects:[NSNumber numberWithFloat:6.0], [NSNumber numberWithFloat:0.5], nil];
[self.soundData setValue:sound forKey:#"soundB"];
sound = [NSArray arrayWithObjects:[NSNumber numberWithFloat:7.0], [NSNumber numberWithFloat:0.5], nil];
[self.soundData setValue:sound forKey:#"soundC"];
}
The first number is the offset of the sound in the file, the second is the duration. Then get your player ready to play like this...
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:#"%#/audiofile.mp3", [[NSBundle mainBundle] resourcePath]]];
NSError *error;
audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
audioPlayer.numberOfLoops = -1;
if (audioPlayer == nil)
NSLog(#"%#", [error description]);
else {
[audioPlayer prepareToPlay];
}
}
Then you can build a low-level sound playing method like this ...
- (void)playSound:(NSString *)name withCompletion:(void (^)(void))completion {
NSArray *sound = [self.soundData valueForKey:name];
if (!sound) return;
NSTimeInterval offset = [[sound objectAtIndex:0] floatValue];
NSTimeInterval duration = [[sound objectAtIndex:1] floatValue];
audioPlayer.currentTime = offset;
[audioPlayer play];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, duration * NSEC_PER_SEC), dispatch_get_current_queue(), ^{
[audioPlayer pause];
completion();
});
}
And you can play sounds in rapid combination like this ...
- (IBAction)playAB:(id)sender {
[self playSound:#"soundA" withCompletion:^{
[self playSound:#"soundB" withCompletion:^{}];
}];
}
Rather than nesting blocks, you could build a higher-level method that takes a list of sound names and plays them one after the other, that would look like this:
- (void)playSoundList:(NSArray *)soundNames withCompletion:(void (^)(void))completion {
if (![soundNames count]) return completion();
NSString *firstSound = [soundNames objectAtIndex:0];
NSRange remainingRange = NSMakeRange(1, [soundNames count]-1);
NSArray *remainingSounds = [soundNames subarrayWithRange:remainingRange];
[self playSound:firstSound withCompletion:^{
[self playSoundList:remainingSounds withCompletion:completion];
}];
}
Call it like this...
NSArray *list = [NSArray arrayWithObjects:#"soundB", #"soundC", #"soundA", nil];
[self playSoundList:list withCompletion:^{ NSLog(#"done"); }];
I'm assuming you want to change the sequence or omit sounds sometimes. (Otherwise you would just build the asset with all three sounds in a row and play that).
There might be a better idea out there, but to get things very tight, you could consider producing that concatenated asset, pre-loading it - moving up all the latency to that one load, then seeking around it to change the sound.
Related
How can I save time when audio was stopped in session and continue playback from the stop point in next session?
My code:
- (void)initPlayer:(NSString*) audioFile fileExtension:(NSString*)fileExtension
{
NSURL *audioFileLocationURL = [[NSBundle mainBundle] URLForResource:audioFile withExtension:fileExtension];
NSError *error;
self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:audioFileLocationURL error:&error];
if ([audioFile isEqualToString:#"2"]) {
_index = 1;
}
else if ([audioFile isEqualToString:#"3"]) {
_index = 2;
}
[self song];
}
- (void)playAudio {
[self.audioPlayer play];
}
- (void)pauseAudio {
[self.audioPlayer pause];
}
- (BOOL)isPlaying {
return [self.audioPlayer isPlaying];
}
-(NSString*)timeFormat:(float)value{
float minutes = floor(lroundf(value)/60);
float seconds = lroundf(value) - (minutes * 60);
int roundedSeconds = lroundf(seconds);
int roundedMinutes = lroundf(minutes);
NSString *time = [[NSString alloc]
initWithFormat:#"%d:%02d",
roundedMinutes, roundedSeconds];
return time;
}
- (void)setCurrentAudioTime:(float)value {
[self.audioPlayer setCurrentTime:value];
}
- (NSTimeInterval)getCurrentAudioTime {
return [self.audioPlayer currentTime];
}
- (float)getAudioDuration {
return [self.audioPlayer duration];
}
You can use AVPlayer's currentTime property. It returns the playback time of the current AVPlayerItem.
To restore the playback time in the next session, you can pass the stored time to AVPlayer's seekToTime:
[self.player seekToTime:storedPlaybackTime];
https://developer.apple.com/library/mac/documentation/AVFoundation/Reference/AVPlayer_Class/index.html#//apple_ref/doc/uid/TP40009530-CH1-SW2
https://developer.apple.com/library/mac/documentation/AVFoundation/Reference/AVPlayer_Class/index.html#//apple_ref/occ/instm/AVPlayer/seekToTime%3a
To persist the CMTime returned by currentTime, you can use the AVFoundation convenience methods provided by NSValue.
To wrap CMTime in an NSValue, use valueWithCMTime:
[NSValue valueWithCMTime:player.currentTime];
To get an CMTime struct from the persisted value, use:
CMTime persistedTime = [storeValue CMTimeValue];
After you wrapped the CMTime struct in a NSValue instance, you can use keyed archiver & NSData to write the time to disk.
NSHipster has a good article about that topic:http://nshipster.com/nscoding/
The most easy way will be to keep a local db with the song name and when it is stopped add that data to the db. Then when the playback resumes later check the local db first if it has any entries in the past. If not continue from starting.
Also make sure that there is no entry made if the song finishes.
Hope this idea helps you...
In my iOS application, I'm trying to play list of videos downloaded to applications' Documents directory. To achieve that target, I used AVQueuePlayer. Following is my code which leads to app crash after 6/7 times looping.
#interface PlayYTVideoViewController () <NSURLConnectionDataDelegate, UITableViewDataSource, UITableViewDelegate>
{
AVQueuePlayer *avQueuePlayer;
}
- (void)playlistLoop
{
NSLog(#"%s - %d", __PRETTY_FUNCTION__, __LINE__);
lastPlayedVideoNumber = 0;
_loadingVideoLabel.hidden = YES;
avPlayerItemsMutArray = [[NSMutableArray alloc] init];
for (NSString *videoPath in clipUrlsMutArr)
{
NSURL *vidPathUrl = [NSURL fileURLWithPath:videoPath];
AVPlayerItem *avpItem = [AVPlayerItem playerItemWithURL:vidPathUrl];
[avPlayerItemsMutArray addObject:avpItem];
}
avPlayerItemsArray = [avPlayerItemsMutArray copy];
for(AVPlayerItem *item in avPlayerItemsArray)
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(itemDidPlayToEndTime:) name:AVPlayerItemDidPlayToEndTimeNotification object:item];
}
avQueuePlayer = [AVQueuePlayer queuePlayerWithItems:avPlayerItemsArray];
avQueuePlayer.actionAtItemEnd = AVPlayerActionAtItemEndAdvance;
introVideoLayer = [AVPlayerLayer playerLayerWithPlayer:avQueuePlayer];
introVideoLayer.frame = _mpIntroVideoView.bounds;
[_mpContainerView.layer addSublayer:introVideoLayer];
[avQueuePlayer play];
}
- (void)itemDidPlayToEndTime:(NSNotification *)notification
{
NSLog(#"%s - %d", __PRETTY_FUNCTION__, __LINE__);
AVPlayerItem *endedAVPlayerItem = [notification object];
[endedAVPlayerItem seekToTime:kCMTimeZero];
for (AVPlayerItem *item in avPlayerItemsArray)
{
if (item == endedAVPlayerItem)
{
lastPlayedVideoNumber++;
break;
}
}
[self reloadVideoClipsTable];
if ([endedAVPlayerItem isEqual:[avPlayerItemsArray lastObject]])
{
[self playlistLoop];
}
}
After getting memory issue, I tried to make some changes to above code.
I tried to set avQueuePlayer variable public and set it as strong variable
#property (strong, nonatomic) AVQueuePlayer *avQueuePlayer;
By doing that I expected avQueuePlayer variable remain in the memory till we manually set to nil. But that didn't solve the problem.
Then I tried to set player, related arrays and layers to nil and created again for new loop session.
if (avPlayerItemsMutArray != nil)
{
avPlayerItemsMutArray = nil;
}
avPlayerItemsMutArray = [[NSMutableArray alloc] init];
if (avPlayerItemsArray != nil)
{
avPlayerItemsArray = nil;
}
avPlayerItemsArray = [avPlayerItemsMutArray copy];
if (avQueuePlayer != nil)
{
avQueuePlayer = nil;
}
avQueuePlayer = [AVQueuePlayer queuePlayerWithItems:avPlayerItemsArray];
if(introVideoLayer != nil)
{
[introVideoLayer removeFromSuperlayer];
introVideoLayer = nil;
}
introVideoLayer = [AVPlayerLayer playerLayerWithPlayer:avQueuePlayer];
But that also didn't help to solve the issue.
Next I try to remove the observer before it re-initialized in a new loop
if (avPlayerItemsArray != nil)
{
avPlayerItemsArray = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self
name:AVPlayerItemDidPlayToEndTimeNotification
object:nil];
}
But that also didn't help.
Next I used Instrument to find out memory usages and leaks. Application is not exceeding 18 MB when it is crashing and also there were more than 200 MB remaining as free. Instruments is little more complicated but still I didn't find any memory leaks related to this code.
Actually the error was not with the AVQueuePlayer. In my application I'm listing all the videos inside a table below the video playing view. In that table, each row consists with video thumbnail that I taken from below code.
+ (UIImage *)createThumbForVideo:(NSString *)vidFileName
{
NSString *videoFolder = [Video getVideoFolder];
NSString *videoFilePath = [videoFolder stringByAppendingFormat:#"/trickbook/videos/edited/%#",vidFileName];
NSURL *url = [NSURL fileURLWithPath:videoFilePath];
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil];
AVAssetImageGenerator *generateImg = [[AVAssetImageGenerator alloc] initWithAsset:asset];
generateImg.appliesPreferredTrackTransform = YES;
NSError *error = NULL;
CMTime time = CMTimeMake(1, 65);
CGImageRef refImg = [generateImg copyCGImageAtTime:time actualTime:NULL error:&error];
UIImage *frameImage = [[UIImage alloc] initWithCGImage:refImg];
return frameImage;
}
Every time a video clip ends playing and also playlist begins a new loop, I update the table view. So each time I call above method and that's the reason for memory issue.
As the solution I call this method only once for a single video clip and store the returning UIImage in a mutable array. That solved the issue.
Heading of the question and the tags may not adequate with the answer, but I thought this is worth existing as a Q & A rather than deleting the post.
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];
How can I play music from the ipod music library (like user-defined playlists, etc.) at a different volume than the system volume?
This is for anyone who is trying to play music / playlists from the ipod music library at a different volume than the system volume. There are several posts out there saying that the [MPMusicPlayerController applicationMusicPlayer] can do this, but I have found that anytime I change the volume of the applicationMusicPlayer, the system volume changes too.
There is a more involved method of playing music using the AVAudioPlayer class, but it requires you to copy music files from the ipod library to the application bundle, and that can get tricky when you're playing dynamic things, like user generated playlists. That technique does give you access to the bytes though, and is the way to go if you want to do processing on the data (like a DJ app). Link to that solution HERE.
The solution I went with uses the AVPlayer class, there are several good posts out there about how to do it. This post is basically a composite of several different solutions I found on Stackoverflow and elsewhere.
I have the following Frameworks linked:
AVFoundation
MediaPlayer
AudioToolbox
CoreAudio
CoreMedia
(I'm not sure if all of those are critical, but that's what I have. I have some OpenAL stuff implemented too that I don't show in the following code)
// Presumably in your SoundManage.m file (or whatever you call it) ...
#import <CoreAudio/CoreAudioTypes.h>
#import <AudioToolbox/AudioToolbox.h>
#interface SoundManager()
#property (retain, nonatomic) AVPlayer* audioPlayer;
#property (retain, nonatomic) AVPlayerItem* currentItem;
#property (retain, nonatomic) MPMediaItemCollection* currentPlaylist;
#property (retain, nonatomic) MPMediaItem* currentTrack;
#property (assign, nonatomic) MPMusicPlaybackState currentPlaybackState;
#end
#implementation SoundManager
#synthesize audioPlayer;
#synthesize currentItem = m_currentItem;
#synthesize currentPlaylist;
#synthesize currentTrack;
#synthesize currentPlaybackState;
- (id) init
{
...
//Define an AVPlayer instance
AVPlayer* tempPlayer = [[AVPlayer alloc] init];
self.audioPlayer = tempPlayer;
[tempPlayer release];
...
//load the playlist you want to play
MPMediaItemCollection* playlist = [self getPlaylistWithName: #"emo-pop-unicorn-blood-rage-mix-to-the-max"];
if(playlist)
[self loadPlaylist: playlist];
...
//initialize the playback state
self.currentPlaybackState = MPMusicPlaybackStateStopped;
//start the music playing
[self playMusic];
...
}
//Have a way to get a playlist reference (as an MPMediaItemCollection in this case)
- (MPMediaItemCollection*) getPlaylistWithName:(NSString *)playlistName
{
MPMediaQuery* query = [[MPMediaQuery alloc] init];
MPMediaPropertyPredicate* mediaTypePredicate = [MPMediaPropertyPredicate predicateWithValue: [NSNumber numberWithInteger: MPMediaTypeMusic] forProperty:MPMediaItemPropertyMediaType];
[query addFilterPredicate: mediaTypePredicate];
[query setGroupingType: MPMediaGroupingPlaylist];
NSArray* playlists = [query collections];
[query release];
for(MPMediaItemCollection* testPlaylist in playlists)
{
NSString* testPlaylistName = [testPlaylist valueForProperty: MPMediaPlaylistPropertyName];
if([testPlaylistName isEqualToString: playlistName])
return testPlaylist;
}
return nil;
}
//Override the setter on currentItem so that you can add/remove
//the notification listener that will tell you when the song has completed
- (void) setCurrentItem:(AVPlayerItem *)currentItem
{
if(m_currentItem)
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:m_currentItem];
[m_currentItem release];
}
if(currentItem)
m_currentItem = [currentItem retain];
else
m_currentItem = nil;
if(m_currentItem)
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleMusicTrackFinished) name:AVPlayerItemDidPlayToEndTimeNotification object:m_currentItem];
}
}
//handler that gets called when the name:AVPlayerItemDidPlayToEndTimeNotification notification fires
- (void) handleMusicTrackFinished
{
[self skipSongForward]; //or something similar
}
//Have a way to load a playlist
- (void) loadPlaylist:(MPMediaItemCollection *)playlist
{
self.currentPlaylist = playlist;
self.currentTrack = [playlist.items objectAtIndex: 0];
}
//Play the beats, yo
- (void) playMusic
{
//check the current playback state and exit early if we're already playing something
if(self.currentPlaybackState == MPMusicPlaybackStatePlaying)
return;
if(self.currentPlaybackState == MPMusicPlaybackStatePaused)
{
[self.audioPlayer play];
}
else if(self.currentTrack)
{
//Get the system url of the current track, and use that to make an AVAsset object
NSURL* url = [self.currentTrack valueForProperty:MPMediaItemPropertyAssetURL];
AVAsset* asset = [AVURLAsset URLAssetWithURL:url options:nil];
//Get the track object from the asset object - we'll need to trackID to tell the
//AVPlayer that it needs to modify the volume of this track
AVAssetTrack* track = [[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
//Build the AVPlayerItem - this is where you modify the volume, etc. Not the AVPlayer itself
AVPlayerItem* playerItem = [[AVPlayerItem alloc] initWithAsset: asset]; //initWithURL:url];
self.currentItem = playerItem;
//Set up some audio mix parameters to tell the AVPlayer what to do with this AVPlayerItem
AVMutableAudioMixInputParameters* audioParams = [AVMutableAudioMixInputParameters audioMixInputParameters];
[audioParams setVolume: 0.5 atTime:kCMTimeZero]; //replace 0.5 with your volume
[audioParams setTrackID: track.trackID]; //here's the track id
//Set up the actual AVAudioMix object, which aggregates effects
AVMutableAudioMix* audioMix = [AVMutableAudioMix audioMix];
[audioMix setInputParameters: [NSArray arrayWithObject: audioParams]];
//apply your AVAudioMix object to the AVPlayerItem
[playerItem setAudioMix:audioMix];
//refresh the AVPlayer object, and play the track
[self.audioPlayer replaceCurrentItemWithPlayerItem: playerItem];
[self.audioPlayer play];
}
self.currentPlaybackState = MPMusicPlaybackStatePlaying;
}
- (void) pauseMusic
{
if(self.currentPlaybackState == MPMusicPlaybackStatePaused)
return;
[self.audioPlayer pause];
self.currentPlaybackState = MPMusicPlaybackStatePaused;
}
- (void) skipSongForward
{
//adjust self.currentTrack to be the next object in self.currentPlaylist
//start the new track in a manner similar to that used in -playMusic
}
- (void) skipSongBackward
{
float currentTime = self.audioPlayer.currentItem.currentTime.value / self.audioPlayer.currentItem.currentTime.timescale;
//if we're more than a second into the song, just skip back to the beginning of the current track
if(currentTime > 1.0)
{
[self.audioPlayer seekToTime: CMTimeMake(0, 1)];
}
else
{
//otherwise, adjust self.currentTrack to be the previous object in self.currentPlaylist
//start the new track in a manner similar to that used in -playMusic
}
}
//Set volume mid-song - more or less the same process we used in -playMusic
- (void) setMusicVolume:(float)vol
{
AVPlayerItem* item = self.audioPlayer.currentItem;
AVAssetTrack* track = [[item.asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];
AVMutableAudioMixInputParameters* audioParams = [AVMutableAudioMixInputParameters audioMixInputParameters];
[audioParams setVolume: vol atTime:kCMTimeZero];
[audioParams setTrackID: track.trackID];
AVMutableAudioMix* audioMix = [AVMutableAudioMix audioMix];
[audioMix setInputParameters: [NSArray arrayWithObject: audioParams]];
[item setAudioMix:audioMix];
}
#end
Please forgive any errors you see - let me know in the comments and I'll fix them. Otherwise, I hope this helps if anyone runs into the same challenge I did!
Actually I found a really easy way to do this by loading iPod URL's from MPMusicPlayer, but then doing playback through AVAudioPlayer.
// Get-da iTunes player thing
MPMusicPlayerController* iTunes = [MPMusicPlayerController iPodMusicPlayer];
// whazzong
MPMediaItem *currentSong = [iTunes nowPlayingItem];
// whazzurl
NSURL *currentSongURL = [currentSong valueForProperty:MPMediaItemPropertyAssetURL];
info( "AVAudioPlayer playing %s", [currentSongURL.absoluteString UTF8String] ) ;
// mamme AVAudioPlayer
NSError *err;
avAudioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:currentSongURL error:&err] ;
if( err!=nil )
{
error( "AVAudioPlayer couldn't load %s", [currentSongURL.absoluteString UTF8String] ) ;
}
avAudioPlayer.numberOfLoops = -1; //infinite
// Play that t
[avAudioPlayer prepareToPlay] ;
[avAudioPlayer play];
[avAudioPlayer setVolume:0.5]; // set the AVAUDIO PLAYER's volume to only 50%. This
// does NOT affect system volume. You can adjust this music volume anywhere else too.
I have 5 songs in my app that I would like to play one after the other with AVAudioPlayer.
Are there any examples of this? How can I accomplish this?
Any example code would be greatly appreciated!
Thanks!
AVQueuePlayer work for this situation.
AVQueuePlayer is a subclass of AVPlayer you use to play a number of items in sequence.
Instead of AVAudioPlayer you can use AVQueuePlayer which suits this use case better as suggested by Ken.
Here is a bit of code you can use:
#interface AVSound : NSObject
#property (nonatomic, retain) AVQueuePlayer* queuePlayer;
- (void)addToPlaylist:(NSString*)pathForResource ofType:(NSString*)ofType;
- (void)playQueue;
#end
#implementation AVSound
- (void)addToPlaylist:(NSString*)pathForResource ofType:(NSString*)ofType
{
// Path to the audio file
NSString *path = [[NSBundle mainBundle] pathForResource:pathForResource ofType:ofType];
// If we can access the file...
if ([[NSFileManager defaultManager] fileExistsAtPath:path])
{
AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL fileURLWithPath:path]];
if (_queuePlayer == nil) {
_queuePlayer = [[AVQueuePlayer alloc] initWithPlayerItem:item];
}else{
[_queuePlayer insertItem:item afterItem:nil];
}
}
}
- (void)playQueue
{
[_queuePlayer play];
}
#end
Then to use it:
In your interface file:
#property (strong, nonatomic) AVSound *pageSound;
In your implementation file:
- (void)addAudio:(Book*)book pageNum:(int)pageNum
{
NSString *soundFileEven = [NSString stringWithFormat:#"%02d", pageNum-1];
NSString *soundPathEven = [NSString stringWithFormat:#"%#_%#", book.productId, soundFileEven];
NSString *soundFileOdd = [NSString stringWithFormat:#"%02d", pageNum];
NSString *soundPathOdd = [NSString stringWithFormat:#"%#_%#", book.productId, soundFileOdd];
if (_pageSound == nil) {
_pageSound = [[AVSound alloc]init];
_pageSound.player.volume = 0.5;
}
[_pageSound clearQueue];
[_pageSound addToPlaylist:soundPathEven ofType:#"mp3"];
[_pageSound addToPlaylist:soundPathOdd ofType:#"mp3"];
[_pageSound playQueue];
}
HTH
For every song you want to make make a single AVPlayer.
NSURL *url = [NSURL URLWithString:pathToYourFile];
AVPlayer *audioPlayer = [[AVPlayer alloc] initWithURL:url];
[audioPlayer play];
You can get a Notification when the player ends. Check AVPlayerItemDidPlayToEndTimeNotification when setting up the player:
audioPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:[audioPlayer currentItem]];
this will prevent the player to pause at the end.
in the notification:
- (void)playerItemDidReachEnd:(NSNotification *)notification
{
// start your next song here
}
You can start your next song as soon as you get a notification that the current playing song is done. Maintain some counter which is persistent across selector calls. That way using counter % [songs count] will give you an infinite looping playlist :)
Don't forget un unregister the notification when releasing the player.
Unfortunately, AVAudioPlayer can only play one file. To play two files, you have to kill the first instance of the AVAudioPlayer and recreate it a second time (it can be initiated using - (id)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError). The problem with this approach is that there is a slight delay between when the first file finishes playing and when the second file starts playing. If you want to get rid of this delay you have to dig into Core Audio and come up with a much more complex solution.