When scheduling files or buffers with AVAudioPlayerNode
, you can submit a completion callback. You can also specify for what event the callback should be called, the options being:
dataConsumed
dataRendered
dataPlayedBack
In most cases, this works as expected. However, when scheduling files one after the other, the callbacks for files other than the last one are not called if the type is dataRendered
or dataPlayedBack
.
Here's a self-contained example demonstrating the issue. (This code can be pasted over the contents of the 'ViewController.swift' file in the Xcode iOS 'App' template. The project will need to include the audio file referenced in the code in order to work.)
import AVFoundation
import UIKit
class ViewController: UIViewController {
private let engine = AVAudioEngine()
private let filePlayer = AVAudioPlayerNode()
private let bufferPlayer = AVAudioPlayerNode()
private var file: AVAudioFile! = nil
private var buffer: AVAudioPCMBuffer! = nil
override func viewDidLoad() {
super.viewDidLoad()
let url = Bundle.main.resourceURL!.appendingPathComponent("audio.m4a")
file = try! AVAudioFile(forReading: url)
buffer = AVAudioPCMBuffer(
pcmFormat: file.processingFormat,
frameCapacity: AVAudioFrameCount(file.length)
)
try! file.read(into: buffer)
let format = file.processingFormat
engine.attach(filePlayer)
engine.attach(bufferPlayer)
engine.connect(filePlayer, to: engine.mainMixerNode, format: format)
engine.connect(bufferPlayer, to: engine.mainMixerNode, format: format)
try! engine.start()
filePlayer.play()
bufferPlayer.play()
for i in 0 ..< 3 {
filePlayer.scheduleFile(
file, at: nil, completionCallbackType: .dataPlayedBack
) {
type in print("File \(i)")
}
}
for i in 0 ..< 3 {
filePlayer.scheduleBuffer(
buffer, at: nil, completionCallbackType: .dataPlayedBack
) {
type in print("Buff \(i)")
}
}
}
}
Here's the output I get:
File 2
Buff 0
Buff 1
Buff 2
As you can see, the callbacks are called for all three instances of the buffer, but are only called for the last instance of the file.
Again, it's only for dataRendered
or dataPlayedBack
that the callbacks aren't called. For dataConsumed
, it works correctly.
Has anyone encountered this? Can anyone confirm this behavior? It seems like a bug, but it's also possible I could be doing something wrong.
Edit:
Here's another version of the code in response to an idea presented in the comments. In this version, instead of scheduling the same file instance three times, three instances of the same file are scheduled in succession:
import AVFoundation
import UIKit
class ViewController: UIViewController {
private let engine = AVAudioEngine()
private let filePlayer = AVAudioPlayerNode()
private let bufferPlayer = AVAudioPlayerNode()
private var files = [AVAudioFile]()
private var buffer: AVAudioPCMBuffer! = nil
override func viewDidLoad() {
super.viewDidLoad()
let url = Bundle.main.resourceURL!.appendingPathComponent("audio.m4a")
for _ in 0 ..< 3 {
files.append(try! AVAudioFile(forReading: url))
}
let file = files[0]
buffer = AVAudioPCMBuffer(
pcmFormat: file.processingFormat,
frameCapacity: AVAudioFrameCount(file.length)
)
try! file.read(into: buffer)
let format = file.processingFormat
engine.attach(filePlayer)
engine.attach(bufferPlayer)
engine.connect(filePlayer, to: engine.mainMixerNode, format: format)
engine.connect(bufferPlayer, to: engine.mainMixerNode, format: format)
try! engine.start()
filePlayer.play()
bufferPlayer.play()
for i in 0 ..< 3 {
filePlayer.scheduleFile(
files[i], at: nil, completionCallbackType: .dataPlayedBack
) {
type in print("File \(i)")
}
}
for i in 0 ..< 3 {
filePlayer.scheduleBuffer(
buffer, at: nil, completionCallbackType: .dataPlayedBack
) {
type in print("Buff \(i)")
}
}
}
}
The results are the same. The practicality of reading the same file multiple times aside, this is an interesting diagnostic, as it provides some more information about the behavior. If it's a bug, it seems to be independent of whether the scheduled files are the same AVAudioFile instances or not.
Since
AVAudioPlayerNode
is essentially a wrapper forkAudioUnitSubType_ScheduledSoundPlayer
(presumably with some of the file reading and buffering code fromkAudioUnitSubType_AudioFilePlayer
thrown in but usingExtAudioFile
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 inAVAudioPlayerNode
.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 becausekAudioUnitSubType_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 forkAudioUnitSubType_ScheduledSoundPlayer
are handled on the HAL's real-time IO thread and clients can't be trusted not to block the IO thread).Output:
mFlags
of0x03
equates tokScheduledAudioSliceFlag_Complete | kScheduledAudioSliceFlag_BeganToRender
.