/* catest * * Build with: clang -x c -o catest catest.c.txt -framework CoreFoundation -framework CoreAudio -framework AudioToolbox * * Tested on macOS 26.5, MacBook Pro M2 Pro * * This app outputs noise to channel 0. Typically this will output from the left channel. * I'm setting a kAudioOutputUnitProperty_ChannelMap on the output unit that should result in * the channels being reversed, but this has no effect (that's the bug I'm trying to figure out). * * This app is hard-coded for a 2 channel output device, I recommend using headphone output * so L and R can be clearly distinguished. * * Passing '-l' will list all output devices (friendly name and UID) * Passing '-d ' will use the device with the provided UID * With no options, the default output device will be used. */ #include #include #include #include #include #include #include #include #define SAMPLE_RATE_HZ 48000 #define SINE_FREQ_HZ 440 static AudioDeviceID get_default_output_device(void) { const AudioObjectPropertyAddress addr = { .mScope = kAudioObjectPropertyScopeGlobal, .mElement = kAudioObjectPropertyElementMain, .mSelector = kAudioHardwarePropertyDefaultOutputDevice, }; AudioDeviceID default_id; UInt32 size = sizeof(default_id); OSStatus sc = AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, 0, NULL, &size, &default_id); if(sc != noErr){ printf("Getting _DefaultInputDevice property failed: %x\n", (int)sc); return -1; } return default_id; } static int list_output_devices(AudioDeviceID default_id) { UInt32 devsize; AudioObjectPropertyAddress addr = { .mScope = kAudioObjectPropertyScopeGlobal, .mElement = kAudioObjectPropertyElementMain, .mSelector = kAudioHardwarePropertyDevices, }; OSStatus sc = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &addr, 0, NULL, &devsize); if(sc != noErr){ printf("Getting _Devices property size failed: %x\n", (int)sc); return 1; } unsigned int num_devices = devsize / sizeof(AudioDeviceID); AudioDeviceID *devices = malloc(devsize); sc = AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, 0, NULL, &devsize, devices); if(sc != noErr){ printf("Getting _Devices property failed: %x\n", (int)sc); return 1; } for (unsigned int i = 0; i < num_devices; i++) { CFStringRef cfname, cfuid; UInt32 size = sizeof(CFStringRef); addr.mSelector = kAudioObjectPropertyName; sc = AudioObjectGetPropertyData(devices[i], &addr, 0, NULL, &size, &cfname); if(sc != noErr){ printf("Unable to get _Name property for device %u: %x\n", (unsigned int)devices[i], (int)sc); return 1; } addr.mSelector = kAudioDevicePropertyDeviceUID; sc = AudioObjectGetPropertyData(devices[i], &addr, 0, NULL, &size, &cfuid); if(sc != noErr){ printf("Unable to get UID property for device %u: %x\n", (unsigned int)devices[i], (int)sc); return 1; } char *name = (char *)CFStringGetCStringPtr(cfname, kCFStringEncodingUTF8); if (!name) { size_t size = CFStringGetMaximumSizeForEncoding(CFStringGetLength(cfname), kCFStringEncodingUTF8); name = malloc(size); CFStringGetCString(cfname, name, size, kCFStringEncodingUTF8); } char *uid = (char *)CFStringGetCStringPtr(cfuid, kCFStringEncodingUTF8); if (!uid) { size_t size = CFStringGetMaximumSizeForEncoding(CFStringGetLength(cfuid), kCFStringEncodingUTF8); uid = malloc(size); CFStringGetCString(cfuid, uid, size, kCFStringEncodingUTF8); } printf("Device %d%s: %s\tUID: %s\n", i, devices[i] == default_id ? " (default)" : "", name, uid); } return 0; } static OSStatus ca_render_cb(void *user, AudioUnitRenderActionFlags *flags, const AudioTimeStamp *ts, UInt32 bus, UInt32 nframes, AudioBufferList *data) { static bool running = false; if (!running) { printf("ca_render_cb is being called\n"); running = true; } static unsigned int total_samples = 0; const int sine_freq = SINE_FREQ_HZ; float *buffer = data->mBuffers[0].mData; for (unsigned int i = 0; i < nframes; i += 2 /* TODO: number of channels */) { float time = (float)total_samples++ / SAMPLE_RATE_HZ; buffer[i] = (float)(sinf(2.0f * M_PI * sine_freq * time) / 6.0f); } return noErr; } int main(int argc, char **argv) { OSStatus sc; AudioDeviceID id; AudioDeviceID default_id = get_default_output_device(); if (default_id == -1) { return 1; } if (argc > 1 && !strcmp(argv[1], "-l")) { /* list all output devices and exit */ return list_output_devices(default_id); } if (argc > 2 && !strcmp(argv[1], "-d")) { /* use the device with UID provided in argv[2] */ const AudioObjectPropertyAddress addr = { .mScope = kAudioObjectPropertyScopeGlobal, .mElement = kAudioObjectPropertyElementMain, .mSelector = kAudioHardwarePropertyTranslateUIDToDevice, }; CFStringRef uid = CFStringCreateWithCStringNoCopy(NULL, argv[2], kCFStringEncodingUTF8, kCFAllocatorNull); UInt32 size = sizeof(id); sc = AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, sizeof(uid), &uid, &size, &id); CFRelease(uid); if(sc != noErr){ printf("Failed to get device ID for UID %s: %x\n", argv[2], (int)sc); return kAudioObjectUnknown; } if (id == kAudioObjectUnknown) { printf("Failed to get device ID for UID %s\n", argv[2]); return 1; } CFStringRef cfname; const AudioObjectPropertyAddress addr2 = { .mScope = kAudioObjectPropertyScopeGlobal, .mElement = kAudioObjectPropertyElementMain, .mSelector = kAudioObjectPropertyName, }; size = sizeof(cfname); sc = AudioObjectGetPropertyData(id, &addr2, 0, NULL, &size, &cfname); if(sc != noErr){ printf("Unable to get _Name property for device %u: %x\n", (unsigned int)id, (int)sc); return 1; } { size_t size = CFStringGetMaximumSizeForEncoding(CFStringGetLength(cfname), kCFStringEncodingUTF8); char *name = malloc(size); CFStringGetCString(cfname, name, size, kCFStringEncodingUTF8); printf("Using device %s\n", name); free(name); CFRelease(cfname); } } else { printf("Using default output device\n"); id = default_id; } const AudioComponentDescription desc = { .componentType = kAudioUnitType_Output, .componentSubType = kAudioUnitSubType_HALOutput, .componentManufacturer = kAudioUnitManufacturer_Apple, }; AudioComponent comp; if(!(comp = AudioComponentFindNext(NULL, &desc))){ printf("AudioComponentFindNext failed\n"); return 1; } AudioComponentInstance unit; sc = AudioComponentInstanceNew(comp, &unit); if(sc != noErr){ printf("AudioComponentInstanceNew failed: %x\n", (int)sc); return 1; } sc = AudioUnitSetProperty(unit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0, &id, sizeof(id)); if(sc != noErr){ printf("Couldn't set audio unit device\n"); AudioComponentInstanceDispose(unit); return 1; } #if 0 /* Retrieve the unit's output-side AudioChannelLayout, to discover which channel * corresponds to which speaker position. * * In Audio MIDI Setup.app "Configure Speakers" lets you swap left/right/etc. * channels, but this only changes the speaker position assigned to each channel, * without doing any of the actual remapping. Applications seemingly need to * discover the position for each channel and do remapping themselves. * * Also, some output configurations over HDMI have the Center and LFE channel * positions swapped compared to the WAVE/Windows ordering, and this is * seemingly the only way to discover that. */ { AudioChannelLayout *outputLayout; UInt32 size; Boolean writable; sc = AudioUnitGetPropertyInfo(unit, kAudioUnitProperty_AudioChannelLayout, kAudioUnitScope_Output, 0, &size, &writable); if (sc != noErr) { printf("Couldn't get AudioChannelLayout size\n"); return 1; } outputLayout = malloc(size); sc = AudioUnitGetProperty(unit, kAudioUnitProperty_AudioChannelLayout, kAudioUnitScope_Output, 0, outputLayout, &size); if (sc != noErr) { printf("Couldn't get AudioChannelLayout\n"); return 1; } printf("Got channel layout (output): {tag: 0x%x, bitmap: 0x%x, num_descs: %u}\n", (unsigned int)outputLayout->mChannelLayoutTag, (unsigned int)outputLayout->mChannelBitmap, (unsigned int)outputLayout->mNumberChannelDescriptions); for (unsigned int i = 0; i < outputLayout->mNumberChannelDescriptions; i++) { printf("\tdesc %d: mChannelLabel %d mChannelFlags %x\n", i, outputLayout->mChannelDescriptions[i].mChannelLabel, outputLayout->mChannelDescriptions[i].mChannelFlags); } free(outputLayout); } #endif /* Set format */ int channels = 2; // TODO AudioStreamBasicDescription dev_desc = { .mFormatID = kAudioFormatLinearPCM, .mFormatFlags = kAudioFormatFlagIsFloat, .mSampleRate = 48000, .mBytesPerPacket = (32 * channels) / 8, .mFramesPerPacket = 1, .mBytesPerFrame = (32 * channels) / 8, .mChannelsPerFrame = channels, .mBitsPerChannel = 32, }; sc = AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &dev_desc, sizeof(dev_desc)); if(sc != noErr){ printf("Couldn't set format: %x\n", (int)sc); return 1; } /* Use kAudioFormatProperty_ChannelMap to translate an input/output AudioChannelLayout into a channel map suitable for kAudioOutputUnitProperty_ChannelMap. * This works correctly, but for simplicity it's easier to just hardcode { 1, 0} below. */ #if 0 SInt32 channelMap[2]; { AudioChannelLayout channelLayout = { .mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelBitmap, .mChannelBitmap = 0x3, // TODO .mNumberChannelDescriptions = 0, }; AudioChannelLayout *channelLayouts[] = { &channelLayout, outputLayout }; UInt32 dataSize = sizeof(channelMap); sc = AudioFormatGetProperty(kAudioFormatProperty_ChannelMap, sizeof(channelLayouts), channelLayouts, &dataSize, channelMap); if (sc != noErr) { printf("Couldn't get kAudioFormatProperty_ChannelMap\n"); return 1; } for (unsigned int i = 0; i < sizeof(channelMap) / sizeof(SInt32); i++) { printf("channelMap[%d] = %d\n", i, channelMap[i]); } } #endif /* Set a channel map on the output unit that should reverse the L/R channels, and result in noise out of the right channel instead of left. * It seems to have no effect. * Using { -1, -1 } does mute the audio, but any other combination of values has no effect on the audio. */ { SInt32 array[2] = { 1, 0 }; sc = AudioUnitSetProperty(unit, kAudioOutputUnitProperty_ChannelMap, kAudioUnitScope_Output, 0, array, sizeof(array)); if (sc != noErr) { printf("Couldn't set ChannelMap\n"); return 1; } } AURenderCallbackStruct input = { .inputProc = ca_render_cb, }; sc = AudioUnitSetProperty(unit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &input, sizeof(input)); if(sc != noErr){ printf("Couldn't set callback: %x\n", (int)sc); return 1; } sc = AudioUnitInitialize(unit); if(sc != noErr){ printf("Couldn't initialize: %x\n", (int)sc); return 1; } sc = AudioOutputUnitStart(unit); if(sc != noErr){ printf("Unit failed to start: %x\n", (int)sc); return 1; } CFRunLoopRun(); return 0; }