For an even more in-depth explanation have a look at my answer in
AVAudioEngine multiple AVAudioInputNodes do not play in perfect sync
Here in short:
If your engine is already running you got a @property lastRenderTime in AVAudioNode - your player's superclass - This is your ticket to 100% sample-frame accurate sync...
AVAudioFormat *outputFormat = [playerA outputFormatForBus:0];
const float kStartDelayTime = 0.0; // seconds - in case you wanna delay the start
AVAudioFramePosition startSampleTime = playerA.lastRenderTime.sampleTime;
AVAudioTime *startTime = [AVAudioTime timeWithSampleTime:(startSampleTime + (kStartDelayTime * outputFormat.sampleRate)) atRate:outputFormat.sampleRate];
[playerA playAtTime: startTime];
[playerB playAtTime: startTime];
[playerC playAtTime: startTime];
[playerD playAtTime: startTime];
[player...
By the way - you can achieve the same 100% sample-frame accurate result with the AVAudioPlayer class...
NSTimeInterval startDelayTime = 0.0; // seconds - in case you wanna delay the start
NSTimeInterval now = playerA.deviceCurrentTime;
NSTimeIntervall startTime = now + startDelayTime;
[playerA playAtTime: startTime];
[playerB playAtTime: startTime];
[playerC playAtTime: startTime];
[playerD playAtTime: startTime];
[player...
With no startDelayTime the first 100-200ms of all players will get clipped off because the start command actually takes its time to the run loop although the players have already started (well, been scheduled) 100% in sync at now. But with a startDelayTime = 0.25 you are good to go. And never forget to prepareToPlay your players in advance so that at start time no additional buffering or setup has to be done - just starting them guys ;-)