I actually got it working fine with a render notify, not a render callback. I'm putting up a lengthy blog post about it with a companion project, but until I do that here's how I achieved it:
- a handle to whatever selector you want called when playback stops (or the class itself)
- Add a render notify to the player unit. Pass in the struct instance as the refCon.
- In the render notify, increment the struct's samplesPlayed variable by the number of incoming frames. Then check to see if it's greater than or equal to the length of the region (region.mFramesToPlay) and if so, call the selector.
I don't know if I'm sample-accurate or not, but I'm accurate to the point where I can't hear any obvious lag. Certainly it'll be more on-time than using an NSTimer, which is subject to all kinds of disclaimers about timing.
I'll have sample project and the explanatory post up over the weekend, but for now here's some snippets:
- (void) startPlayback
{
_curPlayback.framesPlayed = 0;
// Apply the region to the file player AU
checkError(AudioUnitSetProperty(_playerUnit,
kAudioUnitProperty_ScheduledFileRegion,
kAudioUnitScope_Global,
0,
&_curPlayback.region,
sizeof(_curPlayback.region)),
"AudioUnitSetProperty failed: setting the player AU's region property", false);
// Prime the player
UInt32 defaultVal = 0;
checkError(AudioUnitSetProperty(_playerUnit,
kAudioUnitProperty_ScheduledFilePrime,
kAudioUnitScope_Global,
0,
&defaultVal,
sizeof(defaultVal)),
"AudioUnitSetProperty failed: priming the player", false);
// Set up the start time
AudioTimeStamp startTime;
memset(&startTime, 0, sizeof(startTime));
startTime.mFlags = kAudioTimeStampSampleTimeValid;
startTime.mSampleTime = -1;
checkError(AudioUnitSetProperty(_playerUnit,
kAudioUnitProperty_ScheduleStartTimeStamp,
kAudioUnitScope_Global,
0,
&startTime,
sizeof(startTime)),
"AudioUnitSetProperty failed: setting start time", false);
// Add render notify to playback unit
checkError(AudioUnitAddRenderNotify(_playerUnit,
&playRenderNotify,
&_curPlayback),
"AudioUnitAddRenderNotify failed: adding playback notify to player", false);
_curPlayback.isPlaying = YES;
}
The callback:
OSStatus playRenderNotify(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData)
{
if (*ioActionFlags & kAudioUnitRenderAction_PostRender) {
PlaybackInfo *info = (PlaybackInfo *)inRefCon;
info->framesPlayed += inNumberFrames;
if (info->framesPlayed >= info->region.mFramesToPlay) {
[info->cab stopPlayback];
[info->cab startPlayback]; // I want to loop, so I start again immediately
}
}
return noErr;
}
And stopPlayback:
- (void) stopPlayback
{
checkError(AudioUnitReset(_playerUnit,
kAudioUnitScope_Global,
0),
"AudioUnitReset failed: stopping AudioFilePlayer playback", false);
// Remove render notify from playback unit
checkError(AudioUnitRemoveRenderNotify(_playerUnit,
&playRenderNotify,
&_curPlayback),
"AudioUnitAddRenderNotify failed: adding playback notify to player", false);
_curPlayback.isPlaying = NO;
}