Problems with AVAudioPlayerNode's scheduleBuffer function

Hi,

I'm having two problems using the scheduleBuffer function of AVAudioPlayerNode.

Background: my app generates audio programatically, which is why I am using this function. I also need low latency. Therefore, I'm using a strategy of scheduling a small number of buffers, and using the completion handler to keep the process moving forward by scheduling one more buffer for each one that completes.

I'm seeing two problems with this approach:

One, the total memory consumed by my app grows steadily while the audio is playing, which suggests that the audio buffers are never being deallocated or some other runaway process is underway. (The Leaks tool doesn't detect any leaks, however).

Two, audio playback sometimes stops, particularly on slower devices. By "stops", what I mean is that at some point I schedule a buffer and the completion block for that buffer is never called. When this happens, I can't even clear the problem by stopping the player.

Now, regarding the first issue, I suspected that if my completion block recursively scheduled another buffer with another completion block, I would probably end up blowing out the stack with an infinite recursion. To get around this, instead of directly scheduling the buffer in the completion block, I set it up to enqueue the schedule in a dispatch queue. However, this doesn't seem to solve the problem.

Any advice would be appreciated. Thanks.

Replies

Update: I had the address sanitizer turned on, and when I turned it off, the memory problem disappeared.


I'm still concerned about the occasional loss of audio. Also, I'm not able to get the kind of latency I expect. Using a fairly modern device (iPhone 6) I would expect to be able to get 30ms or less (based on having worked on AudioQueue apps in the past) but in actuality the best I can do with any reliability is only about 140.

FYI, once my app gets into the state where playback stops (the completion handlers never get called), I'm also unable to deallocate my AVAudioPlayerNode. Even though I have called the stop and reset functions on the player, disconnected its output, and detached it from the engine, it never gets deallocated from memory. I presume this is because the engine has somehow retained it. Note that in my scheduleBuffer completion block I do not have any strong references to the player itself, only weak references, so I don't think I'm causing this. I can verify that the object does get released when playback is happening normally and that the problem only occurs after the engine fails.


Frank

Im seeing this exact memory leak, however I do not have address sanitizer turned on.


When I comment out the call to scheduleBuffer, memory is steady, which suggests that AVAudioPlayerNode is retaining the AVAudioPCMBuffers even after they are played.

It would be interesting to know if the mediaserverd crashed. Are you listening for AVAudioSessionMediaServicesWereResetNotification?


This would be a good reason for playback all of a sudden stopping and the audio system being messed up -- you'd need to completely dispose of the engine and associated objects then rebuild everything again.


Regarding latency, your app should not be mixable if you want the lowest latency and you should be setting a preferred I/O buffer duration (we have samples that set this value to 0.0029), you can then check the actual value the system gives you after session activation by using the IOBufferDuration property. You may also want to have a look at the reported AVAudioIONode's presentationLatency.

Follow up: I only see the leak with the latest Xcode beta. I wonder if you were using the beta as well?


Attached are 2 screenshots. The only difference between the runs is Xcode 8.2.1 vs. Xcode 8.3 beta 3. No settings or code was changed.


Edit: The screenshots didn't appear to show up after I posted the message.


http://imgur.com/RJ3XvAz

http://imgur.com/LujHPYm

Thanks for the tip. I wasn't aware of the reset notification. I've added it to my code, and if I get it, I'll make sure I deallocate and recreate all my audio objects.


That said, I haven't had the problem for a few days. I think it's because I changed the way I schedule buffers slightly. I wasn't sure whether the completion block for the scheduleBuffer: function was guaranteed to be called on the same thread as the function itself (it isn't documented either way). Since I was unsure, I was manually scheduling my completion code on an execution queue, which is probably what was slowing things down. I since used the debugger to observe that the completion block was in fact being called on the right thread already, so I removed the extra code.


I still get audio gaps if I set either the buffer size or the number of concurrently scheduled buffers too low, but it isn't crashing.


Is there a way I can detect when audio gaps have occurred due to my not filling the buffer fast enough? If I could detect this, I could dynamically start sending larger buffers on slower devices.


Frank

I realize this is an old post but posting my experience in case it helps someone. I had the same issue with the completion handler not getting called after a while and my audio engine (wrapper) stalling as a result. Turned out to be something very simple (and stupid!). In my code I was doing a avPlayerNode reset before calling scheduleBuffer. Everything started working once I took out the reset! My guess is somehow the reset was getting called out of sequence in one of the functions lower down causing this issue.

I also ran into this issue. My problem was that I was using an offset on inputNode.lastRenderTime. To resolve it, I needed to convert it using inputNode.playerTime(forNodeTime: inputNode.lastRenderTime) before I schedule with scheduleBuffer(buffer, at: futureTime)

See here https://developer.apple.com/documentation/avfaudio/avaudioplayernode/1390449-playertime

Just chiming in here too. I found I get a memory leak when re-using the player nodes. I have an app that is essentially a sound board. Playing sounds repeatedly would cause the memory to continue growing. I was able to clear this up by calling Stop() on the node before re-using.