How do you set the input level (gain) on the built-in input (OSX Core Audio / Audio Unit)?

前端 未结 1 2027
轮回少年
轮回少年 2021-02-06 02:46

I\'ve got an OSX app that records audio data using an Audio Unit. The Audio Unit\'s input can be set to any available source with inputs, including the built-in input. The pro

1条回答
  •  陌清茗
    陌清茗 (楼主)
    2021-02-06 02:58

    This works for me to set the input volume on my MacBook Pro (2011 model). It is a bit funky, I had to try setting the master channel volume, then each independent stereo channel volume until I found one that worked. Look through the comments in my code, I suspect the best way to tell if your code is working is to find a get/set-property combination that works, then do something like get/set (something else)/get to verify that your setter is working.

    Oh, and I'll point out of course that I wouldn't rely on the values in address staying the same across getProperty calls as I'm doing here. It seems to work but it's definitely bad practice to rely on struct values being the same when you pass one by reference to a function. This is of course sample code so please forgive my laziness. ;)

    //
    //  main.c
    //  testInputVolumeSetter
    //
    
    #include 
    #include 
    
    OSStatus setDefaultInputDeviceVolume( Float32 toVolume );
    
    int main(int argc, const char * argv[]) {
        OSStatus                        err;
    
        // Load the Sound system preference, select a default
        // input device, set its volume to max.  Now set
        // breakpoints at each of these lines.  As you step over
        // them you'll see the input volume change in the Sound
        // preference panel.
        //
        // On my MacBook Pro setting the channel[ 1 ] volume
        // on the default microphone input device seems to do
        // the trick.  channel[ 0 ] reports that it works but
        // seems to have no effect and the master channel is
        // unsettable.
        //
        // I do not know how to tell which one will work so
        // probably the best thing to do is write your code
        // to call getProperty after you call setProperty to
        // determine which channel(s) work.
        err = setDefaultInputDeviceVolume( 0.0 );
        err = setDefaultInputDeviceVolume( 0.5 );
        err = setDefaultInputDeviceVolume( 1.0 );
    }
    
    // 0.0 == no volume, 1.0 == max volume
    OSStatus setDefaultInputDeviceVolume( Float32 toVolume ) {
        AudioObjectPropertyAddress      address;
        AudioDeviceID                   deviceID;
        OSStatus                        err;
        UInt32                          size;
        UInt32                          channels[ 2 ];
        Float32                         volume;
    
        // get the default input device id
        address.mSelector = kAudioHardwarePropertyDefaultInputDevice;
        address.mScope = kAudioObjectPropertyScopeGlobal;
        address.mElement = kAudioObjectPropertyElementMaster;
    
        size = sizeof(deviceID);
        err = AudioObjectGetPropertyData( kAudioObjectSystemObject, &address, 0, nil, &size, &deviceID );
    
        // get the input device stereo channels
        if ( ! err ) {
            address.mSelector = kAudioDevicePropertyPreferredChannelsForStereo;
            address.mScope = kAudioDevicePropertyScopeInput;
            address.mElement = kAudioObjectPropertyElementWildcard;
            size = sizeof(channels);
            err = AudioObjectGetPropertyData( deviceID, &address, 0, nil, &size, &channels );
        }
    
        // run some tests to see what channels might respond to volume changes
        if ( ! err ) {
            Boolean                     hasProperty;
    
            address.mSelector = kAudioDevicePropertyVolumeScalar;
            address.mScope = kAudioDevicePropertyScopeInput;
    
            // On my MacBook Pro using the default microphone input:
    
            address.mElement = kAudioObjectPropertyElementMaster;
            // returns false, no VolumeScalar property for the master channel
            hasProperty = AudioObjectHasProperty( deviceID, &address );
    
            address.mElement = channels[ 0 ];
            // returns true, channel 0 has a VolumeScalar property
            hasProperty = AudioObjectHasProperty( deviceID, &address );
    
            address.mElement = channels[ 1 ];
            // returns true, channel 1 has a VolumeScalar property
            hasProperty = AudioObjectHasProperty( deviceID, &address );
        }
    
        // try to get the input volume
        if ( ! err ) {
            address.mSelector = kAudioDevicePropertyVolumeScalar;
            address.mScope = kAudioDevicePropertyScopeInput;
    
            size = sizeof(volume);
            address.mElement = kAudioObjectPropertyElementMaster;
            // returns an error which we expect since it reported not having the property
            err = AudioObjectGetPropertyData( deviceID, &address, 0, nil, &size, &volume );
    
            size = sizeof(volume);
            address.mElement = channels[ 0 ];
            // returns noErr, but says the volume is always zero (weird)
            err = AudioObjectGetPropertyData( deviceID, &address, 0, nil, &size, &volume );
    
            size = sizeof(volume);
            address.mElement = channels[ 1 ];
            // returns noErr, but returns the correct volume!
            err = AudioObjectGetPropertyData( deviceID, &address, 0, nil, &size, &volume );
        }
    
        // try to set the input volume
        if ( ! err ) {
            address.mSelector = kAudioDevicePropertyVolumeScalar;
            address.mScope = kAudioDevicePropertyScopeInput;
    
            size = sizeof(volume);
    
            if ( toVolume < 0.0 ) volume = 0.0;
            else if ( toVolume > 1.0 ) volume = 1.0;
            else volume = toVolume;
    
            address.mElement = kAudioObjectPropertyElementMaster;
            // returns an error which we expect since it reported not having the property
            err = AudioObjectSetPropertyData( deviceID, &address, 0, nil, size, &volume );
    
            address.mElement = channels[ 0 ];
            // returns noErr, but doesn't affect my input gain
            err = AudioObjectSetPropertyData( deviceID, &address, 0, nil, size, &volume );
    
            address.mElement = channels[ 1 ];
            // success! correctly sets the input device volume.
            err = AudioObjectSetPropertyData( deviceID, &address, 0, nil, size, &volume );
        }
    
        return err;
    }
    

    EDIT in response to your question, "How'd [I] figure this out?"

    I've spent a lot of time using Apple's audio code over the last five or so years and I've developed some intuition/process when it comes to where and how to look for solutions. My business partner and I co-wrote the original iHeartRadio apps for the first-generation iPhone and a few other devices and one of my responsibilities on that project was the audio portion, specifically writing an AAC Shoutcast stream decoder/player for iOS. There weren't any docs or open-source examples at the time so it involved a lot of trial-and-error and I learned a ton.

    At any rate, when I read your question and saw the bounty I figured this was just low-hanging fruit (i.e. you hadn't RTFM ;-). I wrote a few lines of code to set the volume property and when that didn't work I genuinely got interested.

    Process-wise maybe you'll find this useful:

    Once I knew it wasn't a straightforward answer I started thinking about how to solve the problem. I knew the Sound System Preference lets you set the input gain so I started by disassembling it with otool to see whether Apple was making use of old or new Audio Toolbox routines (new as it happens):

    Try using:

    otool -tV /System/Library/PreferencePanes/Sound.prefPane/Contents/MacOS/Sound | bbedit

    then search for Audio to see what methods are called (if you don't have bbedit, which every Mac developer should IMO, dump it to a file and open in some other text editor).

    I'm most familiar with the older, deprecated Audio Toolbox routines (three years to obsolescence in this industry) so I looked at some Technotes from Apple. They have one that shows how to get the default input device and set its volume using the newest CoreAudio methods but as you undoubtedly saw their code doesn't work properly (at least on my MBP).

    Once I got to that point I fell back on the tried-and-true: Start googling for keywords that were likely to be involved (e.g. AudioObjectSetPropertyData, kAudioDevicePropertyVolumeScalar, etc.) looking for example usage.

    One interesting thing I've found about CoreAudio and using the Apple Toolbox in general is that there is a lot of open-source code out there where people try various things (tons of pastebins and GoogleCode projects etc.). If you're willing to dig through a bunch of this code you'll typically either find the answer outright or get some very good ideas.

    In my search the most relevant things I found were the Apple technote showing how to get the default input device and set the master input gain using the new Toolbox routines (even though it didn't work on my hardware), and I found some code that showed setting the gain by channel on an output device. Since input devices can be multichannel I figured this was the next logical thing to try.

    Your question is really good because at least right now there is no correct documentation from Apple that shows how to do what you asked. It's goofy too because both channels report that they set the volume but clearly only one of them does (the input mic is a mono source so this isn't surprising, but I consider having a no-op channel and no documentation about it a bit of a bug on Apple).

    This happens pretty consistently when you start dealing with Apple's cutting-edge technologies. You can do amazing things with their toolbox and it blows every other OS I've worked on out of the water but it doesn't take too long to get ahead of their documentation, particularly if you're trying to do anything moderately sophisticated.

    If you ever decide to write a kernel driver for example you'll find the documentation on IOKit to be woefully inadequate. Ultimately you've got to get online and dig through source code, either other people's projects or the OS X source or both, and pretty soon you'll conclude as I have that the source is really the best place for answers (even though StackOverflow is pretty awesome).

    Thanks for the points and good luck with your project :)

    0 讨论(0)
提交回复
热议问题