Synchronize the playback of two or more AVAudioPlayer in Iphone - iphone

I need to play 2 sounds with 2 AVAudioPlayer objects at the same exact time... so I found this example on Apple AVAudioPlayer Class Reference (https://developer.apple.com/library/mac/#documentation/AVFoundation/Reference/AVAudioPlayerClassReference/Reference/Reference.html):
- (void) startSynchronizedPlayback {
NSTimeInterval shortStartDelay = 0.01; // seconds
NSTimeInterval now = player.deviceCurrentTime;
[player playAtTime: now + shortStartDelay];
[secondPlayer playAtTime: now + shortStartDelay];
// Here, update state and user interface for each player, as appropriate
}
What I don't understand is: why also the secondPlayer has the shorStartDelay?
Shouldn't it be without? I thought the first Player needed a 0.1 sec delay as it is called before the second Player... but in this code the 2 players have the delay...
Anyone can explain me if that is right and why?
Thanks a lot
Massy

If you only use the play method ([firstPlayer play];), firstPlayer will start before the second one as it will receive the call before.
If you set no delay ([firstPlayer playAtTime:now];), the firstPlayer will also start before de second one because firstPlayer will check the time at which it is supposed to start, and will see that it's already passed. Thus, it will have the same behaviour as when you use only the play method.
The delay is here to ensure that the two players start at the same time. It is supposed to be long enough to ensure that the two players receive the call before the 'now+delay' time has passed.
I don't know if I'm clear (English is not my native langage). I can try to be more clear if you have questions

Yeah what he said ^ The play at time will schedule both players to start at that time (sometime in the future).
To make it obvious, you can set "shortStartDelay" to 2 seconds and you will see there will be a two second pause before both items start playing.
Another tip to keep in mind here are that when you play/pause AVAudioPlayer they dont actually STOP at exactly the same time. So when you want to resume, you should also sync the audio tracks.
Swift example:
let currentDeviceTime = firstPlayer.deviceCurrentTime
let trackTime = firstPlayer.currentTime
players.forEach {
$0.currentTime = trackTime
$0.play(atTime: currentDeviceTime + 0.1)
}
Where players is a list of AVAudioPlayers and firstPlayer is the first item in the array.
Notice how I am also resetting the "currentTime" which is how many seconds into the audio track you want to keep playing. Otherwise every time the user plays/pauses the track they drift out of sync!

Related

How to skip ahead n seconds while playing track using web audio API?

Using Web Audio API, I'm trying to build an mp3 player with a "Skip ahead 15 seconds" feature.
I'm able to load an mp3 using a source buffer, and can get it to start playing. I want to do something like this, though I know currentTime is not a settable property:
context.currentTime += 15
How do you skip forward n seconds once the song is already playing?
Unfortunately there is no single API call to achieve the desired effect but it's doable. Every AudioBufferSourceNode can only be used once which is why we have to create a new one in order to change something.
Let's imagine we have two variables coming from somewhere called audioContext and audioBuffer. In addition we define two more variables to store the initial startTime and the currently running AudioBufferSourceNode.
let audioBufferSourceNode;
let startTime;
The first time we play the audioBuffer we play it directly from the start. The only special thing here is that we keep a reference to the audioBufferSourceNode and that we remember the startTime.
audioBufferSourceNode = audioContext.createBufferSource();
audioBufferSourceNode.buffer = audioBuffer;
audioBufferSourceNode.connect(audioContext.destination);
startTime = context.currentTime;
audioBufferSourceNode.start(startTime);
If you want to skip ahead some time later the previously started audioBufferSourceNode needs to be stopped first.
const currentTime = context.currentTime;
audioBufferSourceNode.stop(currentTime);
audioBufferSourceNode.disconnect();
In addition a new one needs to be created by reusing the same audioBuffer as before. The only difference here is that we apply an offset to make sure it skips 15 seconds ahead.
audioBufferSourceNode = audioContext.createBufferSource();
audioBufferSourceNode.buffer = audioBuffer;
audioBufferSourceNode.connect(audioContext.destination);
audioBufferSourceNode.start(currentTime, currentTime - startTime + 15);
To be prepared to skip another time it's necessary to update the startTime.
startTime -= 15;
This is of course an oversimplified example. In reality there should be a check to make sure that there is enough audio data left to skip ahead. You could also apply a little fade-in/out when skipping to avoid click sounds. ... This is only meant to illustrate the general idea.

Looping AVPlayer with custom Start/End Times

I am using an AVPlayer to play video. I would like to be able to loop sections of the video based on the user's input (while the video is playing, the user can press a button to start a loop and then press it once more to end after a few seconds pass -- then it should begin playing at the start time and continue to loop once the current time reaches the specified end time)
I can get these start/end loop times just by getting the player's currentTime
var startLoop : CMTime = player.currentTime()
// seconds pass by ....
var endLoop : CMTime = player.currentTime()
I know there is a way to cleanly loop the video back to the beginning once it has finished playing like so:
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player.currentItem, queue: .main) { [weak self] _ in
self?.player?.seek(to: CMTime.zero)
self?.player?.rate = self?.rate ?? 1.0
}
I was wondering if there is a way to do this with my custom startLoop and endLoop times?
There are a few ways to do looping in AVFoundation. The simple way is like you described by listening to the notification and then calling seek(to: startLoop). you can use addBoundaryTimeObserver to listen for specific time.
https://developer.apple.com/documentation/avfoundation/avplayer/1388027-addboundarytimeobserver
However, a more advanced way to do looping is to try to add a Key Value Observer to AVQueuePlayer and try inserting 2 copies of an AVPlayerItem with the specific range of the looping asset you want. Then you can use looping techniques such as the treadmill technique or AVPlayerLooper. The key to creating custom time ranged assets is to use an AVMutableComposition and insertTimeRange.
Also, if you really want to be experimental, you can try a 3rd option which is possibly using an AVMutableComposition as your original source asset and start mutating its underlying tracks on the fly while it is playing. I've tried something similar and it can work if you are not manipulating time ranges that are being played. However, there is no docs on this approach and Apple may change code in future versions that may break this.
References:
https://developer.apple.com/videos/play/wwdc2016/503/
https://developer.apple.com/library/archive/samplecode/avloopplayer/Introduction/Intro.html
https://developer.apple.com/library/archive/samplecode/AVCustomEdit/Introduction/Intro.html
Recommended way of updating timeRange property on AVPlayerLooper
#spitchay reach out to me if you have other AVFoundation questions.

In IOS core audio, how do you find the true current play head position of a file player audio unit?

I have a program that uses a file player audio unit to play, pause and stop a audio file. The way I am accomplishing this is by initializing the file player audio unit to play the file at position zero and then when the user presses the pause button, I stop the AUGraph, capture the current position, and then use that position as the start position when the user presses the play button. Everything is working as it should, but every 3 or 4 times I hit pause and then play, the song starts playing a half to a full second BEFORE the point where I hit pause.
I can't figure out why this is happening, do any of you have any thoughts? here is a simplified version of my code.
//initialize AUGraph and File player Audio unit
...
...
...
//Start AUGraph
...
...
...
// pause playback
- (void) pauseAUGraph {
//first stop the AuGrpah
result = AUGraphStop (processingGraph);
// get current play head position
AudioTimeStamp ts;
UInt32 size = sizeof(ts);
result = AudioUnitGetProperty(filePlayerUnit,
kAudioUnitProperty_CurrentPlayTime, kAudioUnitScope_Global, 0, &ts,
&size);
//save our play head position for use later
//must add it to itself to take care of multiple presses of the pause button
sampleFrameSavedPosition = sampleFrameSavedPosition + ts.mSampleTime;
//this stops the file player unit from playing
AudioUnitReset(filePlayerUnit, kAudioUnitScope_Global, 0);
NSLog (#"AudioUnitReset - stopped file player from playing");
//all done
}
// Stop playback
- (void) stopAUGraph {
// lets set the play head to zero, so that when we restart, we restart at the beginning of the file.
sampleFrameSavedPosition = 0;
//ok now that we saved the current pleayhead position, lets stop the AUGraph
result = AUGraphStop (processingGraph);
}
May be you should use packet counts instead of timestamps, since you just want to pause and play the music, not display the time information.
See BufferedAudioPlayer for an example of using this method.
It may be due to rounding problems with your code:
For example, if every time you hit the pause button, your timer would record at a 0.5/4 seconds before your actual pause time, you would still see a desired result. But after repeating for four more times, the amount of space you have created is 0.5/4 times 4 which is the half of a second you seem to be experiencing.
Thus, I would pay careful attention to the object types you are using and make sure they don't round inappropriately. Try using a double float for your sample times to try to alleviate that problem!
Hope this is clear and helpful! :)

AVPlayer seektoTime iPhone

I have used seekToTime in my application and its working properly. But I want to have some more information about it. Which is, if I have a streaming video file of 1 minute now I want to play it from second 15th to second 45 (first 15 seconds and the last 15 seconds will not play).
How can I do it?
I know that by use of seekToTime I can play a video from 15th second but how to stop it at 45th second and also get noticed by the method that the video has played for the specified time period?
CMTime timer = CMTimeMake(15, 1);
[player seekToTime:timer];
The above code takes me to the 15th second of the streaming file but how to stop it on 45th second and get notified too?
I have searched a lot but couldn't get any info.
Thanks
EDIT:
As #codeghost suggested, simply use forwardPlaybackEndTime.
You can simply use:
yourAVPlayerItem.forwardPlaybackEndTime = CMTimeMake(10, 1);
Here 10 is the time till the AVPlayerItem will play.
Set the forwardPlaybackEndTime property on your AVPlayerItem.
I don't know if there is something build-in for this in AVPlayer, but what i would do is build an function and call it with :
[self performSelector:#selector(stopPlaying) withObject:nil afterDelay:45];
-(void)stopPlaying{
[player pause];
}
this will stop playing after 45 seconds,and of course you can put instead 45 any number of seconds that you want.
You can use [AVPlayer addPeriodicTimeObserverForInterval:queue:usingBlock:] for that purpose.
Get the AVPlayer.currentTime periodically, and call pause or stop on the exact time.

iPhone - start multiple instances of AVAudioPlayer simultaneously

I am using multiple instances of AVAudioPlayer to play multiple audio files simultaneously. I run a loop to start playing the audio files (prepareToPlay is called beforehand and the loop only makes a call to the play method)
But invariably, one of the players does not play in sync. How can I ensure that all the 4 players start playing audio simultaneously?
Thanks.
The Apple docs talk about how you can "Play multiple sounds simultaneously, one sound per audio player, with precise synchronization". Perhaps you need to call playAtTime: e.g. [myAudioPlayer playAtTime: myAudioPlayer.deviceCurrentTime + playbackDelay];
In fact, the Apple docs for playAtTime: contain the following code snippet:
NSTimeInterval shortStartDelay = 0.01; // seconds
NSTimeInterval now = player.deviceCurrentTime;
[player playAtTime: now + shortStartDelay];
[secondPlayer playAtTime: now + shortStartDelay];
They should play simultaneously (assuming you choose a large enough value for shortStartDelay -- not so soon that it happens before this thread returns or whatever).
Unfortunately, you can't. AVAudioPlayer doesn't provide any mechanism for fine-grained control of start time. The currentTime property sets the point in the file to read from, it doesn't guarantee when the AVAudioPlayer instance will start playing in system time, which is what you need to sync multiple audio streams.
When I need this behavior, I use the RemoteIO Audio Unit + the 3D Mixer Audio Unit + ExtAudioFile.
EDIT
Note that as of iOS 4, you can synchronize multiple AVAudioPlayer instances using playAtTime:
This code segment of mine allows you to do this as long as you don't have to do it instantly. You can pass in the targetTime as a timestamp for when you want to hear the sounds. The trick is to make use of time-stamps and the delay functionality of NSObject. Also, it utilizes the fact that it takes way less time to change the volume of the player than it does to change the current time. Should work almost perfectly precisely.
- (void) moveTrackPlayerTo:(double) timeInSong atTime:(double) targetTime {
[trackPlayer play];
trackPlayer.volume = 0;
double timeOrig = CFAbsoluteTimeGetCurrent();
double delay = targetTime - CFAbsoluteTimeGetCurrent();
[self performSelector:#selector(volumeTo:)
withObject:[NSNumber numberWithFloat:single.GLTrackVolume]
afterDelay:delay];
trackPlayer.currentTime = timeInSong - delay - (CFAbsoluteTimeGetCurrent() - timeOrig);
}
- (void) volumeTo:(NSNumber *) volNumb {
trackPlayer.volume = [volNumb floatValue];
}
Try to set same currentTime property value for every AVAudioPlayer object.