5

I am trying to write (what should be) a simple app that has a bunch of audio units in sequence in an AUGraph and then writes the output to a file. I added a callback using AUGraphAddRenderNotify. Here is my callback function:

OSStatus MyAURenderCallback(void *inRefCon,
                        AudioUnitRenderActionFlags *actionFlags,
                        const AudioTimeStamp *inTimeStamp,
                        UInt32 inBusNumber,
                        UInt32 inNumberFrames,
                        AudioBufferList *ioData) {
    if (*actionFlags & kAudioUnitRenderAction_PostRender) {
        ExtAudioFileRef outputFile = (ExtAudioFileRef)inRefCon;
        ExtAudioFileWriteAsync(outputFile, inNumberFrames, ioData);
    }
}

This sort of works. The file is playable and I can hear what I recorded but there is horrible amounts of static that makes it barely audible.

Does anybody know what is wrong with this? Or does anyone know of a better way to record the AUGraph output to a file?

Thanks for the help.

HowsItStack
  • 923
  • 1
  • 8
  • 13
  • Do the formats of your AUGraph and ExtAudioFile (client data format) match? Also, do you need to record in real time? – sbooth Jan 21 '12 at 13:53
  • I think they match. I used the same AudioStreamBasicDescription to configure all the audio units and to create the file. I'm not sure of how to actually check though. Do you know how to do that? I guess I will look into it. I guess I don't need to record in real time but is there any other way to do it? If I don't save the data some where in real time then it is lost. – HowsItStack Jan 23 '12 at 02:40
  • I believe instead of calling `AUGraphStart` you want to call `AudioUnitRender` repeatedly on the head of the graph. I think the problem with doing it in realtime is that `ExtAudioFile`'s internal ring buffer fills up and loses data. – sbooth Jan 23 '12 at 03:24
  • Oh, that's what you meant by not real time. I think I do need it to be real time then since I'm recording from the microphone and also passing the audio through to the speakers. – HowsItStack Jan 23 '12 at 03:38

3 Answers3

6

I had a epiphany with regards to Audio Units just now which helped me solve my own problem. I had a misconception about how audio unit connections and render callbacks work. I thought they were completely separate things but it turns out that a connection is just short hand for a render callback.

Doing an kAudioUnitProperty_MakeConnection from the output of audio unit A to the input of audio unit B is the same as doing kAudioUnitProperty_SetRenderCallback on the input of unit B and having the callback function call AudioUnitRender on the output of audio unit A.

I tested this by doing a make connection after setting my render callback and the render callback was no longer invoked.

Therefore, I was able to solve my problem by doing the following:

AURenderCallbackStruct callbackStruct = {0};
callbackStruct.inputProc = MyAURenderCallback;
callbackStruct.inputProcRefCon = mixerUnit;

AudioUnitSetProperty(ioUnit,
                     kAudioUnitProperty_SetRenderCallback,
                     kAudioUnitScope_Input,
                     0,
                     &callbackStruct,
                     sizeof(callbackStruct));

And them my callback function did something like this:

OSStatus MyAURenderCallback(void *inRefCon,
                        AudioUnitRenderActionFlags *actionFlags,
                        const AudioTimeStamp *inTimeStamp,
                        UInt32 inBusNumber,
                        UInt32 inNumberFrames,
                        AudioBufferList *ioData) {

    AudioUnit mixerUnit = (AudioUnit)inRefCon;

    AudioUnitRender(mixerUnit,
                    actionFlags,
                    inTimeStamp,
                    0,
                    inNumberFrames,
                    ioData);

    ExtAudioFileWriteAsync(outputFile, 
                           inNumberFrames, 
                           ioData);

    return noErr;
}

This probably should have been obvious to me but since it wasn't I'll bet there are others that were confused in the same way so hopefully this is helpful to them too.

I'm still not sure why I had trouble with the AUGraphAddRenderNotify callback. I will dig deeper into this later but for now I found a solution that seems to work.

HowsItStack
  • 923
  • 1
  • 8
  • 13
  • Does the above code actually work? I would think it would throw an error since you set the refCon to 'self' and then in the callback cast it as a AUMixer. – spring Apr 08 '12 at 23:35
  • You are correct. I slightly simplified the code from what is in my project. I passed in self and then accessed mixerUnit through a property. I updated my post to pass in mixerUnit which is the unit before the output unit whose output I wanted to write to file before passing it to the output unit. – HowsItStack Apr 13 '12 at 05:48
  • Ok- was wondering. You might be interested in my solution for saving graph output to file - putting a renderCallback on the remoteIO instance and then getting the rendered data in the 'post_Render' stage. Don't know if that is a 'good' way to do it but it works. Also got saving live to ACC compressed format working (yay!): http://stackoverflow.com/questions/10113977/recording-to-aac-from-remoteio-data-is-getting-written-but-file-unplayable – spring Apr 13 '12 at 06:03
  • 1
    Your answer surely works, but at the cost of _breaking_ the `AUGraph` API in fact. Please take a look at [this SO post](http://stackoverflow.com/questions/37575137/callback-function-of-output-unit-of-audio-graph-cant-read-data/37589489#37589489), in answer to which I have provided a working solution using `AUGraphAddRenderNotify()`. – user3078414 Jun 04 '16 at 09:07
1

Here is some sample code from Apple (the project is PlaySequence, but it isn't MIDI specific) that might help:

{
    CAStreamBasicDescription clientFormat = CAStreamBasicDescription();
    ca_require_noerr (result = AudioUnitGetProperty(outputUnit,
                                                    kAudioUnitProperty_StreamFormat,
                                                    kAudioUnitScope_Output, 0,
                                                    &clientFormat, &size), fail);
    size = sizeof(clientFormat);
    ca_require_noerr (result = ExtAudioFileSetProperty(outfile, kExtAudioFileProperty_ClientDataFormat, size, &clientFormat), fail);

    {
        MusicTimeStamp currentTime;
        AUOutputBL outputBuffer (clientFormat, numFrames);
        AudioTimeStamp tStamp;
        memset (&tStamp, 0, sizeof(AudioTimeStamp));
        tStamp.mFlags = kAudioTimeStampSampleTimeValid;
        int i = 0;
        int numTimesFor10Secs = (int)(10. / (numFrames / srate));
        do {
            outputBuffer.Prepare();
            AudioUnitRenderActionFlags actionFlags = 0;
            ca_require_noerr (result = AudioUnitRender (outputUnit, &actionFlags, &tStamp, 0, numFrames, outputBuffer.ABL()), fail);

            tStamp.mSampleTime += numFrames;

            ca_require_noerr (result = ExtAudioFileWrite(outfile, numFrames, outputBuffer.ABL()), fail);    

            ca_require_noerr (result = MusicPlayerGetTime (player, &currentTime), fail);
            if (shouldPrint && (++i % numTimesFor10Secs == 0))
                printf ("current time: %6.2f beats\n", currentTime);
        } while (currentTime < sequenceLength);
    }
}
sbooth
  • 16,646
  • 2
  • 55
  • 81
0

Maybe try this. Copy the data from the audio unit callback to a long buffer. Play the buffer to test it, then write the entire buffer to a file after you have verified that the whole buffer is OK.

hotpaw2
  • 70,107
  • 14
  • 90
  • 153