App launch time and AVAudioPlayerNode.play()

I understand that apps should launch quickly and spend as little time as is practical in functions like application(_:didFinishLaunchingWithOptions:) and viewDidLoad().


Not counting audio setup, the amount of synchronous work I'm doing during launch is probably less than 100 milliseconds, which seems acceptable (although maybe I'm wrong and even that's too much).


However, I'm encountering a problem with audio setup due to the AVAudioPlayerNode.play() function. AVAudioPlayerNode.play() takes a long time to execute. It depends on the environment of course, but it seems to take roughly in the 10-30 millisecond range per call, depending.


If you have many AVAudioPlayerNode instances, this adds up. On my test device (an iPad Mini 2 running iOS 12.4.2), starting 16 players takes around 370 milliseconds. This is an old device of course, but on the simulator it still takes around 200 milliseconds. My computer is fairly old as well, but other AVAudioEngine-related operations seem to perform fine. It seems to be specifically AVAudioPlayerNode.play() that's the problem. (For those who might not be familiar with AVAudioEngine, AVAudioPlayerNode.play() isn't a function you call every time you want to play a sound - it simply 'activates' the player, and therefore only needs to be called during initial setup and in a few other isolated cases.)


For clarity, here's some example code showing what I'm talking about:


import AVFoundation
import QuartzCore
import UIKit

class ViewController: UIViewController {
    private let engine = AVAudioEngine()
    private let players = (0 ..< 16).map { _ in AVAudioPlayerNode() }

    override func viewDidLoad() {
        super.viewDidLoad()

        let format =
            AVAudioFormat(standardFormatWithSampleRate: 44100.0, channels: 2)

        for player in players {
            engine.attach(player)
            engine.connect(player, to: engine.mainMixerNode, format: format)
        }

        try! engine.start()

        let startTime = CACurrentMediaTime()

        for player in players {
            player.play()
        }

        print("\(CACurrentMediaTime() - startTime)")
    }
}


In my app, the time it takes to start the players, plus the other work that needs to be done, could lead to around 400 milliseconds of synchronous work during launch.


- Can I get away with doing 400 milliseconds of synchronous work during launch? Or is that too much?


- If it's acceptable to do this much work, would it be best to do it in application(_:didFinishLaunchingWithOptions:), viewDidLoad(), or in some other location?


- Is there any other straightforward way I could address the AVAudioPlayerNode.play() execution time problem? (There might be heavyweight solutions, like moving the entire audio system to a separate thread, but I'm trying to avoid such complexities if I can.)

Replies

After further investigation I've discovered that the execution time for AVAudioPlayerNode.play() can be lowered by requesting a low buffer time value via AVAudioSession.setPreferredIOBufferDuration(). That mitigates the problem somewhat.


I'm probably still looking at around 200 milliseconds of synchronous work during launch. So to revise my question, can I get away with 200 milliseconds? And again, where would it be best to do this work? application(_:didFinishLaunchingWithOptions:)? viewDidLoad()? Or somewhere else?