Search code examples
iosaudio-playermixeraugraph

Setting audio pan programmatically on iPhone not working


In my code, I set up an audio processing graph with two audio units: an I/O unit, and a multichannel mixer unit. First the I/O unit:

bool RtApiIos::setupAU(void *handle, AURenderCallbackStruct inRenderProc, AudioStreamBasicDescription &outFormat)
{

AudioUnitHandle *auHandle = (AudioUnitHandle *)handle;

AudioComponentDescription desc;
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_RemoteIO;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;

AudioComponent comp = AudioComponentFindNext( NULL, &desc );

if (AudioComponentInstanceNew(comp, &auHandle->audioUnit))
    return false;

if (AudioUnitSetProperty(auHandle->audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &inRenderProc, sizeof(inRenderProc)))
    return false;

Then the multichannel mixer unit:

    AudioComponentDescription mixerDesc;
    mixerDesc.componentType = kAudioUnitType_Mixer;
    mixerDesc.componentSubType = kAudioUnitSubType_MultiChannelMixer;
    mixerDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
    mixerDesc.componentFlags = 0;
    mixerDesc.componentFlagsMask = 0;

    comp = AudioComponentFindNext( NULL, &mixerDesc );

    if (AudioComponentInstanceNew(comp, &auHandle->mixerUnit))
        return false;

    UInt32 busCount = 1;
    if( AudioUnitSetProperty(auHandle->mixerUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, 0, &busCount, sizeof(busCount)) )
        return false;

    if (AudioUnitSetProperty(auHandle->mixerUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &inRenderProc, sizeof(inRenderProc)))
        return false;

    size = sizeof(localFormat);
    if (AudioUnitGetProperty(auHandle->mixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &localFormat, &size ))
        return false;

    if( AudioUnitSetProperty(auHandle->mixerUnit, kAudioUnitProperty_SampleRate, kAudioUnitScope_Output, 0, &localFormat.mSampleRate, sizeof(localFormat.mSampleRate)))
        return false;

I start up the processing graph, and off we go. The audio plays OK, but the last call of the routine has no audible effect on the pan (and "err" == 0).

    // Declare and instantiate an audio processing graph
    NewAUGraph(&auHandle->processingGraph);

    AUNode mixerNode;
    AUGraphAddNode(auHandle->processingGraph, &mixerDesc, &mixerNode);

    AUNode ioNode;
    AUGraphAddNode(auHandle->processingGraph, &desc, &ioNode);

    // Indirectly performs audio unit instantiation
    if (AUGraphOpen(auHandle->processingGraph))
        return false;

    if( AUGraphConnectNodeInput(auHandle->processingGraph, mixerNode,  0, ioNode, 0) )
        return false;

    if (AUGraphInitialize(auHandle->processingGraph))
        return false;

    AudioUnitParameterValue panValue = 0.9; // panned almost dead-right. possible values are between -1 and 1
    OSStatus err = AudioUnitSetParameter(auHandle->mixerUnit, kMultiChannelMixerParam_Pan, kAudioUnitScope_Output, 0, panValue, 0);
    if (err != noErr) {
        //error setting pan
    }
}

What am I doing wrong? Thanks!!


Solution

  • Got this working, with a lot of help from Apple's old MixerHost sample project.

    First created a structure to bundle my resources:

    struct AudioUnitHandle {
        AudioUnit audioUnit;
        AUGraph processingGraph;       
        AudioUnit mixerUnit;
    };
    

    Specified my audio unit descriptions, created an audio processing graph, and added two nodes - one for my I/O node, and one for the multichannel mixer:

    bool setupAU(void *handle, AURenderCallbackStruct inRenderProc, AudioStreamBasicDescription &outFormat)
    {
    AudioUnitHandle *auHandle = (AudioUnitHandle *)handle;
    
    // I/O Unit
    AudioComponentDescription desc;
    desc.componentType = kAudioUnitType_Output;
    desc.componentSubType = kAudioUnitSubType_RemoteIO;
    desc.componentManufacturer = kAudioUnitManufacturer_Apple;
    desc.componentFlags = 0;
    desc.componentFlagsMask = 0;
    
    // Multichannel mixer unit
    AudioComponentDescription mixerDesc;
    mixerDesc.componentType = kAudioUnitType_Mixer;
    mixerDesc.componentSubType = kAudioUnitSubType_MultiChannelMixer;
    mixerDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
    mixerDesc.componentFlags = 0;
    mixerDesc.componentFlagsMask = 0;
    
    //............................................................................
    // Create a new audio processing graph.
    if( NewAUGraph(&auHandle->processingGraph) )
        return false;
    
    //............................................................................
    // Add nodes to the audio processing graph.
    AUNode ioNode;
    if( AUGraphAddNode(auHandle->processingGraph, &desc, &ioNode) )
        return false;
    
    AUNode mixerNode;
    if( AUGraphAddNode(auHandle->processingGraph, &mixerDesc, &mixerNode) )
        return false;
    
    //............................................................................
    // Open the audio processing graph
    // Following this call, the audio units are instantiated but not initialized
    //    (no resource allocation occurs and the audio units are not in a state to
    //    process audio).
    if( AUGraphOpen(auHandle->processingGraph) )
        return false;
    

    Then quite a bit of setup for the multichannel mixer itself:

    // Obtain the mixer unit instance from its corresponding node.
    if( AUGraphNodeInfo(auHandle->processingGraph, mixerNode, NULL, &auHandle->mixerUnit) )
        return false;
    
    //............................................................................
    // Multichannel Mixer unit Setup
    UInt32 busCount = 1;    // bus count for mixer unit input
    UInt16 busNumber = 0;
    if( AudioUnitSetProperty(auHandle->mixerUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, busNumber, &busCount, sizeof(busCount)) )
        return false;
    
    // Increase the maximum frames per slice allows the mixer unit to accommodate the
    //    larger slice size used when the screen is locked.
    UInt32 maximumFramesPerSlice = 4096;
    if( AudioUnitSetProperty(auHandle->mixerUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, busNumber, &maximumFramesPerSlice, sizeof(maximumFramesPerSlice)) )
        return false;
    
    // Set a callback for the mixer node's specified input
    if( AUGraphSetNodeInputCallback(auHandle->processingGraph, mixerNode, busNumber, &inRenderProc) )
        return false;
    
    // Set the format of bus 0 of the mixer unit
    AudioStreamBasicDescription localFormat = {0};
    UInt32 size = sizeof(localFormat);
    
    if (AudioUnitGetProperty(auHandle->mixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, busNumber, &localFormat, &size ))
        return false;
    
    localFormat.mSampleRate = outFormat.mSampleRate;
    localFormat.mChannelsPerFrame = outFormat.mChannelsPerFrame;
    localFormat.mFormatID = outFormat.mFormatID;
    localFormat.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked | kAudioFormatFlagIsNonInterleaved ;
    localFormat.mBitsPerChannel = outFormat.mBitsPerChannel;
    localFormat.mFramesPerPacket = outFormat.mFramesPerPacket;
    localFormat.mBytesPerFrame = outFormat.mBytesPerFrame;
    localFormat.mBytesPerPacket = outFormat.mBytesPerPacket;
    
    OSStatus err = 0;
    err = AudioUnitSetProperty(auHandle->mixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, busNumber, &localFormat, size);
    if(noErr != err)            //kAudioUnitErr_FormatNotSupported        = -10868,
        return false;
    
    // Set the mixer unit's output sample rate format. This is the only aspect of the output stream
    //    format that must be explicitly set.
    if( AudioUnitSetProperty(auHandle->mixerUnit, kAudioUnitProperty_SampleRate, kAudioUnitScope_Output, 0, &localFormat.mSampleRate, sizeof(localFormat.mSampleRate)) )
        return false;
    

    Finally, connected the nodes and initialized the processing graph:

    // Connect the nodes of the audio processing graph
    //  Connect the mixer output to the input of the I/O unit output element
    if( AUGraphConnectNodeInput(auHandle->processingGraph,
                                mixerNode,                  //Source node
                                0,                          //Source node output bus number
                                ioNode,                     //Destination node
                                0) )                        //Destination node input bus number
        return false;
    
    //............................................................................
    // Initialize the audio processing graph, configure audio data stream formats for
    //    each input and output, and validate the connections between audio units.
    if (AUGraphInitialize(auHandle->processingGraph))
        return false;
    
    return true;
    
    }
    

    Very simple matter from here to start and stop this whole mess ala:

    //Start
    OSStatus err = AUGraphStart( auHandle->processingGraph );
    
    //Stop
    OSStatus err = AUGraphStop( auHandle->processingGraph );
    

    Whew!