2

I have iOS app where I am playing audio using AVAudioEngine. I read frames from AudioFile into AVAudioPCMBuffer and then I post buffer to AVAudioPlayerNode.scheduleBuffer. I read frames on DispatchQueue . Problem I have is that when app has a lot of other work to do - syncing with server and downloading file, reading frames on DispatchQueue is executed with few seconds delay, which causes sound interruption. I have set DispatchQueueQos to highest possible .userInteractive and I have no other queues with same qos in app, but it still takes sometimes few seconds to execute code on that queue.

Is there some solution to this? Some way to tell os that this queue is for audio or something similar?

Note: I don't have this problem with AVPlayer if app runs some heavy background operations.

EDIT: some code for better understanding. Problem is that sometimes it takes few seconds to post task on queue (get from comment 1 to comment 2).

var audioProcessingQueue = DispatchQueue(label: "audioProcessing", qos: .userInteractive)
var player = AVAudioPlayerNode()
//comment 1
self.audioProcessingQueue.async(flags: .barrier) {
    //comment 2
    //some buffer processing here...
    player.scheduleBuffer(buffer, at: nil)
}

Thanks

Martin Vandzura
  • 3,047
  • 1
  • 31
  • 63
  • Syncing with server and downloading files should be done with asynchronous calls. Networking is not something you wanna do on the main thread. Main thread is basically for UI stuff. Make sure you don't have any network calls wrapped in dispatch queue main. – andromedainiative Jul 22 '19 at 13:30
  • can you please share some code snippets ? – karthikPrabhu Alagu Jul 22 '19 at 16:51
  • it's not doing any files downloading or anything like that. I added code for better understanding – Martin Vandzura Jul 23 '19 at 06:02
  • I don''t know if this will help, but here it is: https://forums.developer.apple.com/thread/14138 and https://git.kabellmunk.dk/talks/into-the-deep/blob/master/Carthage/Checkouts/AudioKit/AudioKit/macOS/AudioKit/User%20Interface/AKResourceAudioFileLoaderView.swift – jvarela Jul 24 '19 at 04:56
  • Run this under Instruments and see what's blocking your queues (see the System Trace template as a starting point). I do not recommend `.userInteractive` for this; it should be `.userInitiated`. QoS means more than just priority; it has other impacts that can be non-obvious. Use the priority that matches you intent. Why are you adding a barrier here? What else is going onto this queue (should it be a serial queue?) What happens if you remove the processing and just schedule the buffer as-is? – Rob Napier Jul 26 '19 at 13:53
  • @RobNapier there is some buffer processing (e.g. removing silence) so I can't post it directly. Thanks for .userInitiated suggestion – Martin Vandzura Jul 29 '19 at 04:43

1 Answers1

0

Two things:

1) On what queue is that code you provided scheduled? Because its sounds like that queue might share its qos with your download tasks. Dispatching to another queue does take multiple steps. So just because "comment1" is passed, one of the other queues might get the thread time to do some work while you're dispatching. That would lead to some delay yes and seems to be the most likely cause.

2) The WWDC 2016 video Concurrent Programming with GCD in Swift 3, might help. Dispatching something new onto a higher priority queue does not automatically start the task right away if all cores of the cpu are already busy with other tasks. GCD will try its bests, but perhaps your tasks on those other queues are very intense/ doing things where interruption will cause problems.

So if you have something that must be started immediately, ensure that one CPU core/thread is idle. I'd try using an OperationQueue and setting its maxConcurrentOperationCount. Then place all your download operations onto that operation queue, leaving a core/thread ready to launch your audio.

However I don't think option 2 is valid here since you're saying it sometimes takes a few seconds.

Update:

As you've said, your provided code is run on a queue that is also used for your download tasks, this is primarily a faulty architecture design and I'm sorry to say that there's no solution anyone can come up with without seeing your entire project to help structure it better. You'll have to figure out when and what tasks to dispatch onto what priority.

Here are my ideas with the code you provided:

Why are you using the .barrier flag? This flag ensures that no other concurrent tasks are done by the queue, so good if you have data being manipulated by multiple tasks and want to avoid "race conditions". However this also means that after you have dispatched the block, it will wait for other tasks dispatched before it and THEN block the queue to do your tasks.

As other's have commented, profiling your code with instruments will likely show this behaviour.

Here's how I would structure your audio requests:

//A serial (not concurrent) audio queue. Will download one data at a time and play audio
let audioQueue = DispatchQueue(label: "Audio Queue My App", qos: .userInitiated)

//I'm on the main queue here!

audioQueue.async {
    //I'm on the audio queue here, thus not blocking the main thread
    let result = downloadSomeData()
    //Download is done here, and I'm not being blocked by other dispatch queues if their priority is lower.
    switch result {
    case .success(let data):
        //Do something with the data and play audio
    case .failure(let error):
        //Handle Error
    }
}
///Downloads some audio data
func downloadSomeData() -> Swift.Result<Data, Error> {
    //Create dispatch group
    let dg = DispatchGroup()
    dg.enter()
    ///my audio data
    var data: Data?
    //Dispatch onto a background queue OR place a block operation onto your OperationQueue
    DispatchQueue.global(qos: .background).async {
        //Download audio data... Alamofire/ URLSession
        data = Data()
        dg.leave()
    }
    dg.wait()

    //Data was downloaded! We're back on the queue that called us.
    if let nonOptionalData = data {
        return .success(nonOptionalData)
    } else {
        return .failure(NSError(domain: "MyDomain", code: 007, userInfo: [ NSLocalizedDescriptionKey: "No data downloaded, failed?"]))
    }
}

Regarding option #2:

It is possible to retrieve the amount of active cores by using ProcessInfo().activeProcessorCount. Do note that this value could in theory change while running your app due to thermal throttling or other. By using this information and setting the maxConcurrentOperationCount of an OperationQueue equal to the number of active processors you could ensure that operations/tasks do not have to share cpu time. (As long as there are no other DispatchQueues running.) This approach is not safe however. All it takes is a team member to dispatch a task somewhere else in the future.

What is useful about this information however is that you can limit the amount of resources your download tasks use, by having a dedicated OperationQueue with say for example 2 maximum concurrent operations and putting all of your background work on this DispatchQueue. Your CPU will most likely not be used to its full potential and leave some room.

Justin Ganzer
  • 582
  • 4
  • 15
  • It shares queue with other tasks in app. I though that higher priority (. userInteractive) will solve it but that's not enough. I would like to have exactly what you suggested - to have 1 dedicated thread/core reserved for audio. Is it possible to do it? – Martin Vandzura Jul 29 '19 at 04:48
  • @vandzi Updated my answer a bit. I suspect this won't pinhead the problem you have but its worth a read I hope to help you help yourself. – Justin Ganzer Jul 29 '19 at 08:43
  • Actually I said it wrong. my queue var audioProcessingQueue = DispatchQueue(label: "audioProcessing", qos: .userInteractive) is used only for audio processing. I have multiple different queues(all with lower priorities) where I run e.g. db queries, processing some data, downloading (using URLSessionDownloadTask) etc... Main problem is that if all that kicks in, audio queue is slow. In android, there is option to set thread priority to audio, so I am looking to do something similar. There which is guaranteed to execute immediately no matcher what else is running. – Martin Vandzura Jul 29 '19 at 09:06
  • it's same as if you want to create audio player library. You want to guarantee it will work no matter what is library user doing in their app. I don't have this problem when I use AVPlayer so it must do something different. – Martin Vandzura Jul 29 '19 at 09:08