Tuesday, 30 September 2014

AVAudioPlayerNode Audio Loop

Well, I think I've finally concluded that AVAudioPlayerNode can't really be used for any sensible audio synthesis or effects in Swift. It's a shame as Swift certainly seems to be up to the job and the processing isn't really even touching the CPU at 3% but despite the seeming promise of the application descriptions given in the WWDC notes, playback in all the modes I've tried seems glitchy even when using extremely long buffers, so it seems that the ScheduleBuffer function is really not useful for anything more than basic trivial sound generation.

So, following the previous posts my last little test to ensure that my generation code itself was not just the problem was to try a loop-back and see how this worked. To do this I created a simple test case which has the default input going into a mixer with it's volume set to zero and then a player also going into the other mixer that is connecting to the output. I have then installed a tap on the input and used that buffer to pump straight into ScheduleBuffer of the player.

Here's the example code:

import Cocoa
import AVFoundation

// Setup engine and node instances
var engine = AVAudioEngine()
var mixer = engine.mainMixerNode

var inputstub = AVAudioMixerNode()
var player = AVAudioPlayerNode()
var input = engine.inputNode
var output = engine.outputNode
var format = input.inputFormatForBus(0)
var error:NSError?

engine.attachNode(inputstub)
engine.attachNode(player)

// Connect nodes
engine.connect(player,to:mixer, format:format)

// Start engine
engine.startAndReturnError(&error)

player.play()

engine.connect(input, to: inputstub, format: format)
engine.connect(inputstub, to: mixer, format: format)
engine.connect(mixer, to: output, format: format)

inputstub.outputVolume = 1.0
mixer.outputVolume = 1.0

let length = 24256

var audioBuffer = AVAudioPCMBuffer(PCMFormat: player.outputFormatForBus(0), 
                                   frameCapacity: UInt32(length))
audioBuffer.frameLength = UInt32(length)

input.installTapOnBus(0, bufferSize:UInt32(length), format: input.outputFormatForBus(0))
{
        (buffer, time) in
        
        var sum:Float = 0
        
        // fill up the buffer with some samples
        for (var i=0; i<length; i++)
        {
            audioBuffer.floatChannelData.memory[i] = 1.0 * buffer.floatChannelData.memory[i]
        }
                          
        player.scheduleBuffer(audioBuffer,atTime:nil,options:.InterruptsAtLoop,
                              completionHandler:nil)        
}


while (true)
{
    NSThread.sleepForTimeInterval(1)
}

You'll want to put headphones on to test this and have the input take an external feed and put this into the headphones so that it does not create an oscillating feedback loop.

Testing this, there is a sense that the loop must be working ok to fill the buffer without too much trouble and the CPU is barely hitting 3% but the audio playback is glitching which becomes more pronounced as the buffer size is reduced (try playing around with length) until values below 1000 or so become too choppy for anything sensible.

I tried scheduleBuffer with various different options, nil and InterruptsAtLoop, neither made much difference. This is a shame as it means that the scheduleBuffer function is just a bit too simplistic in implementation and is not implementing some fairly basic double buffering to transition between two buffers. Hmmm.... will have to look at another way of doing this then and get back to basics.

For information, I'm using two separate buffers as if the buffer in the tap closure is used the memory is not freed and just increases.


3 comments:

  1. Is there a sample code to select sound card for output node?

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. hey had the same issue as you. The docs says that .loops should fire the completion handler but it doesnt. They still haven't fixed it but I found a workaround.

    ReplyDelete