Since AVAudioPlayerNode
is essentially a wrapper for kAudioUnitSubType_ScheduledSoundPlayer
(presumably with some of the file reading and buffering code from kAudioUnitSubType_AudioFilePlayer
thrown in but using ExtAudioFile
instead) I did an experiment to see if the lower-level counterpart exhibited the same behavior.
This is not exactly an apples-to-apples comparison but it seems that kAudioUnitSubType_ScheduledSoundPlayer
works as expected so this may be a bug in AVAudioPlayerNode
.
The code I used for testing is below. kAudioUnitSubType_ScheduledSoundPlayer
is used to schedule three slices (buffers). They are from the same file but it's irrelevant because kAudioUnitSubType_ScheduledSoundPlayer
only knows about buffers and not files.
The callbacks are invoked as expected for all three slices. So it seems the problem is likely how AVAudioPlayerNode
handles these callbacks internally and routes them to a non-real-time dispatch queue (since the callbacks for kAudioUnitSubType_ScheduledSoundPlayer
are handled on the HAL's real-time IO thread and clients can't be trusted not to block the IO thread).
// ViewController.m
#import "ViewController.h"
@import AudioToolbox;
@import AVFoundation;
@import os.log;
@interface ViewController ()
{
AUGraph _graph;
AUNode _player;
AUNode _mixer;
AUNode _output;
ScheduledAudioSlice _slice [3];
AVAudioPCMBuffer *_buf;
}
- (void)scheduledAudioSliceCompleted:(ScheduledAudioSlice *)slice;
@end
void myScheduledAudioSliceCompletionProc(void * __nullable userData, ScheduledAudioSlice *slice)
{
// ⚠️ WARNING ⚠️
// THIS FUNCTION IS CALLED FROM THE REAL TIME RENDERING THREAD.
// OBJ-C USE HERE IS FOR TESTING CALLBACK FUNCTIONALITY ONLY
// OBJ-C IS NOT REAL TIME SAFE
// DO NOT DO THIS IN PRODUCTION CODE!!!
[(__bridge ViewController *)userData scheduledAudioSliceCompleted:slice];
}
@implementation ViewController
- (void)dealloc {
[self closeGraph];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self openGraph];
[self schedule];
[self startPlayer];
[self startGraph];
}
-(OSStatus)openGraph {
OSStatus result = NewAUGraph(&_graph);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "NewAUGraph failed: %d", result);
return result;
}
// The graph will look like:
// Player -> MultiChannelMixer -> Output
AudioComponentDescription desc;
// Player
desc.componentType = kAudioUnitType_Generator;
desc.componentSubType = kAudioUnitSubType_ScheduledSoundPlayer;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = kAudioComponentFlag_SandboxSafe;
desc.componentFlagsMask = 0;
result = AUGraphAddNode(_graph, &desc, &_player);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphAddNode failed: %d", result);
result = DisposeAUGraph(_graph);
if(noErr != result)
os_log_error(OS_LOG_DEFAULT, "DisposeAUGraph failed: %d", result);
_graph = NULL;
return result;
}
// Mixer
desc.componentType = kAudioUnitType_Mixer;
desc.componentSubType = kAudioUnitSubType_MultiChannelMixer;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = kAudioComponentFlag_SandboxSafe;
desc.componentFlagsMask = 0;
result = AUGraphAddNode(_graph, &desc, &_mixer);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphAddNode failed: %d", result);
result = DisposeAUGraph(_graph);
if(noErr != result)
os_log_error(OS_LOG_DEFAULT, "DisposeAUGraph failed: %d", result);
_graph = NULL;
return result;
}
// Output
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_HALOutput;
desc.componentFlags = kAudioComponentFlag_SandboxSafe;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlagsMask = 0;
result = AUGraphAddNode(_graph, &desc, &_output);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphAddNode failed: %d", result);
result = DisposeAUGraph(_graph);
if(noErr != result)
os_log_error(OS_LOG_DEFAULT, "DisposeAUGraph failed: %d", result);
_graph = NULL;
return result;
}
// Connections
result = AUGraphConnectNodeInput(_graph, _player, 0, _mixer, 0);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphConnectNodeInput failed: %d", result);
result = DisposeAUGraph(_graph);
if(noErr != result)
os_log_error(OS_LOG_DEFAULT, "DisposeAUGraph failed: %d", result);
_graph = NULL;
return result;
}
result = AUGraphConnectNodeInput(_graph, _mixer, 0, _output, 0);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphConnectNodeInput failed: %d", result);
result = DisposeAUGraph(_graph);
if(noErr != result)
os_log_error(OS_LOG_DEFAULT, "DisposeAUGraph failed: %d", result);
_graph = NULL;
return result;
}
// Open the graph
result = AUGraphOpen(_graph);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphOpen failed: %d", result);
result = DisposeAUGraph(_graph);
if(noErr != result)
os_log_error(OS_LOG_DEFAULT, "DisposeAUGraph failed: %d", result);
_graph = NULL;
return result;
}
// Set the mixer's volume on the input and output
AudioUnit au = NULL;
result = AUGraphNodeInfo(_graph, _mixer, NULL, &au);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphNodeInfo failed: %d", result);
result = DisposeAUGraph(_graph);
if(noErr != result)
os_log_error(OS_LOG_DEFAULT, "DisposeAUGraph failed: %d", result);
_graph = NULL;
return result;
}
result = AudioUnitSetParameter(au, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, 1.f, 0);
if(noErr != result)
os_log_error(OS_LOG_DEFAULT, "AudioUnitSetParameter (kMultiChannelMixerParam_Volume, kAudioUnitScope_Input) failed: %d", result);
result = AudioUnitSetParameter(au, kMultiChannelMixerParam_Volume, kAudioUnitScope_Output, 0, 1.f, 0);
if(noErr != result)
os_log_error(OS_LOG_DEFAULT, "AudioUnitSetParameter (kMultiChannelMixerParam_Volume, kAudioUnitScope_Output) failed: %d", result);
// Initialize the graph
result = AUGraphInitialize(_graph);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphInitialize failed: %d", result);
result = DisposeAUGraph(_graph);
if(noErr != result)
os_log_error(OS_LOG_DEFAULT, "DisposeAUGraph failed: %d", result);
_graph = NULL;
return result;
}
return noErr;
}
- (OSStatus)closeGraph {
Boolean graphIsRunning = NO;
OSStatus result = AUGraphIsRunning(_graph, &graphIsRunning);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphIsRunning failed: %d", result);
return result;
}
if(graphIsRunning) {
result = AUGraphStop(_graph);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphStop failed: %d", result);
return result;
}
}
Boolean graphIsInitialized = false;
result = AUGraphIsInitialized(_graph, &graphIsInitialized);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphIsInitialized failed: %d", result);
return result;
}
if(graphIsInitialized) {
result = AUGraphUninitialize(_graph);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphUninitialize failed: %d", result);
return result;
}
}
result = AUGraphClose(_graph);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphClose failed: %d", result);
return result;
}
result = DisposeAUGraph(_graph);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "DisposeAUGraph failed: %d", result);
return result;
}
_graph = NULL;
_player = -1;
_mixer = -1;
_output = -1;
return noErr;
}
- (OSStatus)startGraph {
OSStatus result = AUGraphStart(_graph);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphStart failed: %d", result);
return result;
}
return noErr;
}
- (OSStatus)stopGraph {
OSStatus result = AUGraphStop(_graph);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphStop failed: %d", result);
return result;
}
return noErr;
}
- (OSStatus)startPlayer {
AudioUnit au;
OSStatus result = AUGraphNodeInfo(_graph, _player, NULL, &au);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphNodeInfo failed: %d", result);
return result;
}
AudioTimeStamp ts = {0};
ts.mFlags = kAudioTimeStampSampleTimeValid;
ts.mSampleTime = 0;
result = AudioUnitSetProperty(au, kAudioUnitProperty_ScheduleStartTimeStamp, kAudioUnitScope_Global, 0, &ts, sizeof(ts));
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AudioUnitSetProperty failed: %d", result);
return result;
}
return noErr;
}
- (OSStatus)schedule {
AudioUnit au;
OSStatus result = AUGraphNodeInfo(_graph, _player, NULL, &au);
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AUGraphNodeInfo failed: %d", result);
return result;
}
AVAudioFile *file = [[AVAudioFile alloc] initForReading:[NSURL fileURLWithPath:@"/tmp/test.wav" isDirectory:NO] commonFormat:AVAudioPCMFormatFloat32 interleaved:NO error:nil];
if(!file)
return paramErr;
_buf = [[AVAudioPCMBuffer alloc] initWithPCMFormat:file.processingFormat frameCapacity:(file.processingFormat.sampleRate * 2)];
if(![file readIntoBuffer:_buf error:nil])
return paramErr;
AudioTimeStamp ts = {0};
ts.mFlags = kAudioTimeStampSampleTimeValid;
ts.mSampleTime = 0;
_slice[0].mTimeStamp = ts;
_slice[0].mCompletionProc = myScheduledAudioSliceCompletionProc;
_slice[0].mCompletionProcUserData = (__bridge void *)self;
_slice[0].mNumberFrames = _buf.frameLength;
_slice[0].mBufferList = _buf.mutableAudioBufferList;
result = AudioUnitSetProperty(au, kAudioUnitProperty_ScheduleAudioSlice, kAudioUnitScope_Global, 0, &_slice[0], sizeof(_slice[0]));
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AudioUnitSetProperty failed: %d", result);
return result;
}
ts.mSampleTime += _slice[0].mNumberFrames;
_slice[1] = _slice[0];
_slice[1].mTimeStamp = ts;
result = AudioUnitSetProperty(au, kAudioUnitProperty_ScheduleAudioSlice, kAudioUnitScope_Global, 0, &_slice[1], sizeof(_slice[1]));
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AudioUnitSetProperty failed: %d", result);
return result;
}
ts.mSampleTime += _slice[1].mNumberFrames;
_slice[2] = _slice[1];
_slice[2].mTimeStamp = ts;
result = AudioUnitSetProperty(au, kAudioUnitProperty_ScheduleAudioSlice, kAudioUnitScope_Global, 0, &_slice[2], sizeof(_slice[2]));
if(noErr != result) {
os_log_error(OS_LOG_DEFAULT, "AudioUnitSetProperty failed: %d", result);
return result;
}
return noErr;
}
- (void)scheduledAudioSliceCompleted:(ScheduledAudioSlice *)slice {
if(slice == &_slice[0])
NSLog(@"_slice[0] scheduledAudioSliceCompleted:%p, mFlags = 0x%.2x", slice, slice->mFlags);
else if(slice == &_slice[1])
NSLog(@"_slice[1] scheduledAudioSliceCompleted:%p, mFlags = 0x%.2x", slice, slice->mFlags);
else if(slice == &_slice[2])
NSLog(@"_slice[2] scheduledAudioSliceCompleted:%p, mFlags = 0x%.2x", slice, slice->mFlags);
else
NSLog(@"scheduledAudioSliceCompleted:%p, mFlags = 0x%.2x for unknown slice", slice, slice->mFlags);
}
@end
Output:
XXX _slice[0] scheduledAudioSliceCompleted:0x7f82ee41add0, mFlags = 0x03
XXX _slice[1] scheduledAudioSliceCompleted:0x7f82ee41ae40, mFlags = 0x03
XXX _slice[2] scheduledAudioSliceCompleted:0x7f82ee41aeb0, mFlags = 0x03
mFlags
of 0x03
equates to kScheduledAudioSliceFlag_Complete | kScheduledAudioSliceFlag_BeganToRender
.