16

I'm really excited about the new AVAudioEngine. It seems like a good API wrapper around audio unit. Unfortunately the documentation is so far nonexistent, and I'm having problems getting a simple graph to work.

Using the following simple code to set up an audio engine graph, the tap block is never called. It mimics some of the sample code floating around the web, though those also did not work.

let inputNode = audioEngine.inputNode
var error: NSError?
let bus = 0
    
inputNode.installTapOnBus(bus, bufferSize: 2048, format: inputNode.inputFormatForBus(bus)) { 
    (buffer: AVAudioPCMBuffer!, time: AVAudioTime!) -> Void in
    println("sfdljk")
}
    
audioEngine.prepare()
if audioEngine.startAndReturnError(&error) {
    println("started audio")
} else {
    if let engineStartError = error {
        println("error starting audio: \(engineStartError.localizedDescription)")
    }
}

All I'm looking for is the raw pcm buffer for analysis. I don't need any effects or output. According to the WWDC talk "502 Audio Engine in Practice", this setup should work.

Now if you want to capture data from the input node, you can install a node tap and we've talked about that.

But what's interesting about this particular example is, if I wanted to work with just the input node, say just capture data from the microphone and maybe examine it, analyze it in real time or maybe write it out to file, I can directly install a tap on the input node.

And the tap will do the work of pulling the input node for data, stuffing it in buffers and then returning that back to the application.

Once you have that data you can do whatever you need to do with it.

Here are some links I tried:

  1. http://hondrouthoughts.blogspot.com/2014/09/avfoundation-audio-monitoring.html
  2. http://jamiebullock.com/post/89243252529/live-coding-audio-with-swift-playgrounds (SIGABRT in playground on startAndReturnError)

Edit: This is the implementation based on Thorsten Karrer's suggestion. It unfortunately does not work.

class AudioProcessor {
    let audioEngine = AVAudioEngine()

    init(){
        let inputNode = audioEngine.inputNode
        let bus = 0
        var error: NSError?
    
        inputNode.installTapOnBus(bus, bufferSize: 2048, format:inputNode.inputFormatForBus(bus)) {
            (buffer: AVAudioPCMBuffer!, time: AVAudioTime!) -> Void in
                println("sfdljk")
        }
    
        audioEngine.prepare()
        audioEngine.startAndReturnError(nil)
        println("started audio")
    }
}
Community
  • 1
  • 1
brodney
  • 1,176
  • 2
  • 14
  • 29

4 Answers4

32

It might be the case that your AVAudioEngine is going out of scope and is released by ARC ("If you liked it then you should have put retain on it...").

The following code (engine is moved to an ivar and thus sticks around) fires the tap:

class AppDelegate: NSObject, NSApplicationDelegate {

    let audioEngine  = AVAudioEngine()

    func applicationDidFinishLaunching(aNotification: NSNotification) {
        let inputNode = audioEngine.inputNode
        let bus = 0
        inputNode.installTapOnBus(bus, bufferSize: 2048, format: inputNode.inputFormatForBus(bus)) {
            (buffer: AVAudioPCMBuffer!, time: AVAudioTime!) -> Void in
            println("sfdljk")
        }

        audioEngine.prepare()
        audioEngine.startAndReturnError(nil)
    }
}

(I removed the error handling for brevity)

Thorsten Karrer
  • 1,345
  • 9
  • 19
  • I attempted this and edited my question with the code used. Is the code in your answer working for you? – brodney Dec 01 '14 at 20:20
  • Jup, works for me. I have not tested it in the playground but it works when compiled an run from Xcode. The tap block is called continuously. – Thorsten Karrer Dec 02 '14 at 09:33
  • Sadly I have to admit that it was an unretained object after all. My AudioProcessor class was being instantiated in a method, not as a class property. – brodney Dec 02 '14 at 16:02
  • Any reason why you chose the appDelegate method with the "notification" argument? The default/de facto one is didFinishLaunchingWithOptions: – God of Biscuits Apr 23 '16 at 00:08
  • The reason is that at the time I wrote the answer (2014), Xcode would give me the notification argument as the default. For the question at hand, it does not matter anyway. – Thorsten Karrer Apr 25 '16 at 07:52
  • 1
    5 years later, but +1 for the retain joke – Brett Aug 27 '21 at 09:04
  • Wow, I feel so stupid now!!! Of course this was the issue for me too. Should have been obvious but yeah, thank you so much for pointing it out! – meow May 22 '22 at 13:27
16

UPDATED: I have implemented a complete working example of Recording mic input, applying some effects (reverbs, delay, distortion) at runtime, and save all these effects to an output file.

var engine = AVAudioEngine()
var distortion = AVAudioUnitDistortion()
var reverb = AVAudioUnitReverb()
var audioBuffer = AVAudioPCMBuffer()
var outputFile = AVAudioFile()
var delay = AVAudioUnitDelay()

//Initialize the audio engine

func initializeAudioEngine() {

    engine.stop()
    engine.reset()
    engine = AVAudioEngine()

    isRealTime = true
    do {
        try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord)

        let ioBufferDuration = 128.0 / 44100.0

        try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(ioBufferDuration)

    } catch {

        assertionFailure("AVAudioSession setup error: \(error)")
    }

    let fileUrl = URLFor("/NewRecording.caf")
    print(fileUrl)
    do {

        try outputFile = AVAudioFile(forWriting:  fileUrl!, settings: engine.mainMixerNode.outputFormatForBus(0).settings)
    }
    catch {

    }

    let input = engine.inputNode!
    let format = input.inputFormatForBus(0)

    //settings for reverb
    reverb.loadFactoryPreset(.MediumChamber)
    reverb.wetDryMix = 40 //0-100 range
    engine.attachNode(reverb)

    delay.delayTime = 0.2 // 0-2 range
    engine.attachNode(delay)

    //settings for distortion
    distortion.loadFactoryPreset(.DrumsBitBrush)
    distortion.wetDryMix = 20 //0-100 range
    engine.attachNode(distortion)


    engine.connect(input, to: reverb, format: format)
    engine.connect(reverb, to: distortion, format: format)
    engine.connect(distortion, to: delay, format: format)
    engine.connect(delay, to: engine.mainMixerNode, format: format)

    assert(engine.inputNode != nil)

    isReverbOn = false

    try! engine.start()
}

//Now the recording function:

func startRecording() {

    let mixer = engine.mainMixerNode
    let format = mixer.outputFormatForBus(0)

    mixer.installTapOnBus(0, bufferSize: 1024, format: format, block:
        { (buffer: AVAudioPCMBuffer!, time: AVAudioTime!) -> Void in

            print(NSString(string: "writing"))
            do{
                try self.outputFile.writeFromBuffer(buffer)
            }
            catch {
                print(NSString(string: "Write failed"));
            }
    })
}

func stopRecording() {

    engine.mainMixerNode.removeTapOnBus(0)
    engine.stop()
}

I hope this might help you. Thanks!

Junaid Mukhtar
  • 815
  • 9
  • 16
  • This works but is there a way to have it work without the effects coming out of the speaker in real-time? – user3344977 Mar 13 '16 at 09:01
  • @user3344977 Im currently working on a similar module which I have not been successful in implementing. i.e. offline rendering of an audio file with effects. Here is a post that im following: [Offline rendering] (http://stackoverflow.com/a/15361251/2429443). Hope it helps – Junaid Mukhtar Apr 26 '16 at 15:08
  • 3
    Where do these values come from?: let ioBufferDuration = 128.0 / 44100.0 – Josh Jun 06 '16 at 09:21
  • helpful code that works. now I have code about how this AudioEngine works with all it's connections between AudioUnits. – myUser Apr 15 '17 at 11:33
  • 1
    It works well. However, it crashes if adding AVAudioUnitTimePitch node : ( – CharlesFly May 08 '20 at 23:10
3

The above answer didn't work for me but the following did. I'm installing a tap on a mixer node.

        mMixerNode?.installTapOnBus(0, bufferSize: 4096, format: mMixerNode?.outputFormatForBus(0),
    {
        (buffer: AVAudioPCMBuffer!, time:AVAudioTime!) -> Void in
            NSLog("tapped")

    }
    )
Pescolly
  • 922
  • 11
  • 18
3

nice topic

hi brodney

in your topic i find my solution . here is similar topic Generate AVAudioPCMBuffer with AVAudioRecorder

see lecture Wwdc 2014 502 - AVAudioEngine in Practice capture microphone => in 20 min create buffer with tap code => in 21 .50

here is swift 3 code

@IBAction func button01Pressed(_ sender: Any) {

    let inputNode = audioEngine.inputNode
    let bus = 0
    inputNode?.installTap(onBus: bus, bufferSize: 2048, format: inputNode?.inputFormat(forBus: bus)) {
        (buffer: AVAudioPCMBuffer!, time: AVAudioTime!) -> Void in

            var theLength = Int(buffer.frameLength)
            print("theLength = \(theLength)")

            var samplesAsDoubles:[Double] = []
            for i in 0 ..< Int(buffer.frameLength)
            {
                var theSample = Double((buffer.floatChannelData?.pointee[i])!)
                samplesAsDoubles.append( theSample )
            }

            print("samplesAsDoubles.count = \(samplesAsDoubles.count)")

    }

    audioEngine.prepare()
    try! audioEngine.start()

}

to stop audio

func stopAudio()
    {

        let inputNode = audioEngine.inputNode
        let bus = 0
        inputNode?.removeTap(onBus: bus)
        self.audioEngine.stop()

    }
Community
  • 1
  • 1
myUser
  • 537
  • 5
  • 9
  • I also came up with a similar solution to this. Things would only work once and then not again for 5 mins. You need to remove the tap and stop the audio engine when your App is going to terminate (which I was doing from the debugger). I also set the AudioSession to inactive – Brett Sep 01 '20 at 08:22