12

I've been playing around with Apple's shiny new AVFoundation library, but so far I've unable to set the input or output devices (e.g. a USB sound card) used by an AVAudioEngine, and I can't seem to find anything in the documentation to say it's even possible.

Does anyone have any experience with this?

Benjineer
  • 1,530
  • 18
  • 22
  • As a long shot, I tried casting the `mainMixerNode` of an `AVAudioEngine` to an `AVAudioIONode` which actually worked (for some reason?) - it's a start. – Benjineer Mar 01 '15 at 01:16

2 Answers2

6

Ok, after re-reading the docs for the 10th time, I noticed AVAudioEngine has members inputNode and outputNode (not sure how I missed that!).

The following code seems to do the job:

AudioDeviceID inputDeviceID = 53; // get this using AudioObjectGetPropertyData
AVAudioEngine *engine = [[AVAudioEngine alloc] init];
AudioUnit audioUnit = [[engine inputNode] audioUnit];

OSStatus error = AudioUnitSetProperty(audioUnit,
                                      kAudioOutputUnitProperty_CurrentDevice,
                                      kAudioUnitScope_Global,
                                      0,
                                      &inputDeviceID,
                                      sizeof(inputDeviceID));

I borrowed the non-AVFoundation C code from the CAPlayThrough example.

Benjineer
  • 1,530
  • 18
  • 22
  • When I try to do this for the output node I get the error `required condition is false: numChannelsAggDevice >= numChannelsSubDevice`. – DanielGibbs May 05 '15 at 23:58
  • Did you get the correct device ID using `AudioObjectGetPropertyData`, or just use `53`? How many channels does your input device have? – Benjineer May 06 '15 at 02:26
  • I got the correct device ID and the return status was 0, but no output came out of the device. – DanielGibbs May 06 '15 at 02:33
  • Sorry if I'm stating the obvious, but did you change `[engine inputNode]` to `[engine outputNode]` (assuming you're setting the output device)? – Benjineer May 06 '15 at 02:40
  • I did indeed (after making that mistake the first time I tried) and still no luck. – DanielGibbs May 06 '15 at 03:00
  • I'm outta ideas besides ruling out any hardware/driver issues, but if you wanna post your code showing usage of your audio engine I'd be happy to take a squiz. – Benjineer May 06 '15 at 03:23
  • The code is rather large an complex unfortunately, I just came across this and tried it as it looked like it would help solve a problem I'm having and thought I'd see if there was something basic I'm doing wrong. Ah well, thanks for your help! – DanielGibbs May 06 '15 at 03:37
  • Thanks for the example! Been trying this out for a while now. Setting the output works, but somehow I can't get the input working. I keep getting the following error: `required condition is false: [AVAudioEngineGraph.mm:1345:Initialize: (IsFormatSampleRateAndChannelCountValid(outputHWFormat))]` Not sure what I'm doing wrong. Do you have any suggestions? – Freek Sanders Oct 16 '18 at 14:16
  • @FreekSanders it's hard to say without seeing the rest of your code. If you create a new question and link to it, I'll take a look. – Benjineer Oct 16 '18 at 23:11
  • @benjineer thanks in advance. I've posted a shortened version of my code here: https://stackoverflow.com/q/52818705/2500428 – Freek Sanders Oct 17 '18 at 12:39
  • 4
    Tbh, this is ridiculous how bad Apple documentation and functionality for selecting devices is... I've been reading for a couple of days, and OMG. – akuz Mar 22 '20 at 17:07
5

Here's a complete, if somewhat rough, function which will play some audio for testing purposes (pick a different the file if you don't have GarageBand installed there, of course). To avoid hard-coding a device ID, it switches to your alert ("sound effects") device which you can set in System Preferences.

AVAudioEngine *engine = [[AVAudioEngine alloc] init];
AudioUnit outputUnit = engine.outputNode.audioUnit;

OSStatus err = noErr;
AudioDeviceID outputDeviceID;
UInt32 propertySize;

AudioObjectPropertyAddress propertyAddress = {
    kAudioHardwarePropertyDefaultSystemOutputDevice,
    kAudioObjectPropertyScopeGlobal,
    kAudioObjectPropertyElementMaster };
propertySize = sizeof(outputDeviceID);
err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &propertySize, &outputDeviceID);
if (err) { NSLog(@"AudioHardwareGetProperty: %d", (int)err); return; }

err = AudioUnitSetProperty(outputUnit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0, &outputDeviceID, sizeof(outputDeviceID));
if (err) { NSLog(@"AudioUnitSetProperty: %d", (int)err); return; }

NSURL *url = [NSURL URLWithString:@"/Applications/GarageBand.app/Contents/Frameworks/MAAlchemy.framework/Versions/A/Resources/Libraries/WaveNoise/Liquid.wav"];
NSError *error = nil;
AVAudioFile *file = [[AVAudioFile alloc] initForReading:url error:&error];
if (file == nil) { NSLog(@"AVAudioFile error: %@", error); return; }

AVAudioPlayerNode *player = [[AVAudioPlayerNode alloc] init];
[engine attachNode:player];
[engine connect:player to:engine.outputNode format:nil];

NSLog(@"engine: %@", engine);

if (![engine startAndReturnError:&error]) {
    NSLog(@"engine failed to start: %@", error);
    return;
}

[player scheduleFile:file atTime:[AVAudioTime timeWithHostTime:mach_absolute_time()] completionHandler:^{
    NSLog(@"complete");
}];
[player play];
Nicholas Riley
  • 43,532
  • 6
  • 101
  • 124
  • 1
    This is great, was getting errors passing a format to `[engine connect]` - passing `nil` does the trick – Benjineer Oct 11 '15 at 23:45