Browse Source

Add CoreAudio AAC encoder

Palana 10 years ago
parent
commit
df44e5c0ed

+ 1 - 0
plugins/CMakeLists.txt

@@ -5,6 +5,7 @@ if(WIN32)
 	add_subdirectory(win-capture)
 	add_subdirectory(decklink/win)
 elseif(APPLE)
+	add_subdirectory(coreaudio-encoder)
 	add_subdirectory(mac-avcapture)
 	add_subdirectory(mac-capture)
 	add_subdirectory(mac-syphon)

+ 24 - 0
plugins/coreaudio-encoder/CMakeLists.txt

@@ -0,0 +1,24 @@
+project(coreaudio-encoder)
+
+find_library(COREFOUNDATION CoreFoundation)
+find_library(COREAUDIO CoreAudio)
+find_library(AUDIOTOOLBOX AudioToolbox)
+
+include_directories(${COREFOUNDATION}
+	${COREAUDIO}
+	${AUDIOTOOLBOX})
+
+set(coreaudio-encoder_SOURCES
+	encoder.c)
+
+add_library(coreaudio-encoder MODULE
+	${coreaudio-encoder_SOURCES}
+	${coreaudio-encoder_HEADERS})
+
+target_link_libraries(coreaudio-encoder
+	libobs
+	${COREFOUNDATION}
+	${COREAUDIO}
+	${AUDIOTOOLBOX})
+
+install_obs_plugin_with_data(coreaudio-encoder data)

+ 2 - 0
plugins/coreaudio-encoder/data/locale/en-US.ini

@@ -0,0 +1,2 @@
+CoreAudioAAC="CoreAudio AAC encoder"
+Bitrate="Bitrate"

+ 760 - 0
plugins/coreaudio-encoder/encoder.c

@@ -0,0 +1,760 @@
+#include <util/darray.h>
+#include <util/dstr.h>
+#include <obs-module.h>
+
+#include <AudioToolbox/AudioToolbox.h>
+
+#define CA_LOG(level, format, ...) \
+	blog(level, "[CoreAudio encoder]: " format, ##__VA_ARGS__)
+#define CA_LOG_ENCODER(format_name, encoder, level, format, ...) \
+	blog(level, "[CoreAudio %s: '%s']: " format, \
+			format_name, obs_encoder_get_name(encoder), \
+			##__VA_ARGS__)
+#define CA_BLOG(level, format, ...) \
+	CA_LOG_ENCODER(ca->format_name, ca->encoder, level, format, \
+			##__VA_ARGS__)
+
+struct ca_encoder {
+	obs_encoder_t     *encoder;
+	const char        *format_name;
+
+	AudioConverterRef converter;
+
+	size_t            output_buffer_size;
+	uint8_t           *output_buffer;
+
+	size_t            out_frames_per_packet;
+
+	size_t            in_packets;
+	size_t            in_frame_size;
+	size_t            in_bytes_required;
+
+	DARRAY(uint8_t)   input_buffer;
+	size_t            bytes_read;
+
+	uint64_t          total_samples;
+	uint64_t          samples_per_second;
+
+	uint8_t           *extra_data;
+	uint32_t          extra_data_size;
+
+	size_t            channels;
+};
+typedef struct ca_encoder ca_encoder;
+
+
+static const char *aac_get_name(void)
+{
+	return obs_module_text("CoreAudioAAC");
+}
+
+static const char *code_to_str(OSStatus code)
+{
+	switch (code) {
+#define HANDLE_CODE(c) case c: return #c
+	HANDLE_CODE(kAudio_UnimplementedError);
+	HANDLE_CODE(kAudio_FileNotFoundError);
+	HANDLE_CODE(kAudio_FilePermissionError);
+	HANDLE_CODE(kAudio_TooManyFilesOpenError);
+	HANDLE_CODE(kAudio_BadFilePathError);
+	HANDLE_CODE(kAudio_ParamError);
+	HANDLE_CODE(kAudio_MemFullError);
+
+	HANDLE_CODE(kAudioConverterErr_FormatNotSupported);
+	HANDLE_CODE(kAudioConverterErr_OperationNotSupported);
+	HANDLE_CODE(kAudioConverterErr_PropertyNotSupported);
+	HANDLE_CODE(kAudioConverterErr_InvalidInputSize);
+	HANDLE_CODE(kAudioConverterErr_InvalidOutputSize);
+	HANDLE_CODE(kAudioConverterErr_UnspecifiedError);
+	HANDLE_CODE(kAudioConverterErr_BadPropertySizeError);
+	HANDLE_CODE(kAudioConverterErr_RequiresPacketDescriptionsError);
+	HANDLE_CODE(kAudioConverterErr_InputSampleRateOutOfRange);
+	HANDLE_CODE(kAudioConverterErr_OutputSampleRateOutOfRange);
+#undef HANDLE_CODE
+
+	default: break;
+	}
+
+	return NULL;
+}
+
+static void log_osstatus(ca_encoder *ca, const char *context, OSStatus code)
+{
+	CFErrorRef err  = CFErrorCreate(kCFAllocatorDefault,
+			kCFErrorDomainOSStatus, code, NULL);
+	CFStringRef str = CFErrorCopyDescription(err);
+
+	CFIndex length   = CFStringGetLength(str);
+	CFIndex max_size = CFStringGetMaximumSizeForEncoding(length,
+			kCFStringEncodingUTF8);
+
+	char *c_str = malloc(max_size);
+	if (CFStringGetCString(str, c_str, max_size, kCFStringEncodingUTF8)) {
+		if (ca)
+			CA_BLOG(LOG_ERROR, "Error in %s: %s", context, c_str);
+		else
+			CA_LOG(LOG_ERROR, "Error in %s: %s", context, c_str);
+	} else {
+		const char *code_str = code_to_str(code);
+		if (ca)
+			CA_BLOG(LOG_ERROR, "Error in %s: %s%s%d%s", context,
+					code_str ? code_str : "",
+					code_str ? " (" : "",
+					(int)code,
+					code_str ? ")" : "");
+		else
+			CA_LOG(LOG_ERROR, "Error in %s: %s%s%d%s", context,
+					code_str ? code_str : "",
+					code_str ? " (" : "",
+					(int)code,
+					code_str ? ")" : "");
+	}
+	free(c_str);
+
+	CFRelease(str);
+	CFRelease(err);
+}
+
+static void aac_destroy(void *data)
+{
+	ca_encoder *ca = data;
+
+	if (ca->converter)
+		AudioConverterDispose(ca->converter);
+
+	da_free(ca->input_buffer);
+	bfree(ca->extra_data);
+	bfree(ca->output_buffer);
+	bfree(ca);
+}
+
+typedef void (*bitrate_enumeration_func)(void *data, UInt32 min, UInt32 max);
+
+static bool enumerate_bitrates(ca_encoder *ca, AudioConverterRef converter,
+		bitrate_enumeration_func enum_func, void *data)
+{
+	if (!converter && ca)
+		converter = ca->converter;
+
+	UInt32 size;
+	OSStatus code = AudioConverterGetPropertyInfo(converter,
+			kAudioConverterApplicableEncodeBitRates,
+			&size, NULL);
+	if (code) {
+		log_osstatus(ca, "AudioConverterGetPropertyInfo(bitrates)",
+				code);
+		return false;
+	}
+
+	if (!size) {
+		if (ca)
+			CA_BLOG(LOG_ERROR, "Query for applicable bitrates "
+					"returned 0 size");
+		else
+			CA_LOG(LOG_ERROR, "Query for applicable bitrates "
+					"returned 0 size");
+		return false;
+	}
+
+	AudioValueRange *bitrates = malloc(size);
+
+	code = AudioConverterGetProperty(converter,
+			kAudioConverterApplicableEncodeBitRates,
+			&size, bitrates);
+	if (code) {
+		log_osstatus(ca, "AudioConverterGetProperty(bitrates)", code);
+		return false;
+	}
+
+	size_t num_bitrates = size / sizeof(AudioValueRange);
+	for (size_t i = 0; i < num_bitrates; i++)
+		enum_func(data, (UInt32)bitrates[i].mMinimum,
+				(UInt32)bitrates[i].mMaximum);
+
+	free(bitrates);
+
+	return num_bitrates > 0;
+}
+
+struct validate_bitrate_helper {
+	UInt32 bitrate;
+	bool valid;
+};
+typedef struct validate_bitrate_helper validate_bitrate_helper;
+
+static void validate_bitrate_func(void *data, UInt32 min, UInt32 max)
+{
+	validate_bitrate_helper *valid = data;
+
+	if (valid->bitrate >= min && valid->bitrate <= max)
+		valid->valid = true;
+}
+
+static bool bitrate_valid(ca_encoder *ca, AudioConverterRef converter,
+		UInt32 bitrate)
+{
+	validate_bitrate_helper helper = {
+		.bitrate = bitrate,
+		.valid = false,
+	};
+
+	enumerate_bitrates(ca, converter, validate_bitrate_func, &helper);
+
+	return helper.valid;
+}
+
+static void *aac_create(obs_data_t *settings, obs_encoder_t *encoder)
+{
+	UInt32 bitrate = (UInt32)obs_data_get_int(settings, "bitrate") * 1000;
+	if (!bitrate) {
+		CA_LOG_ENCODER("AAC", encoder, LOG_ERROR,
+				"Invalid bitrate specified");
+		return NULL;
+	}
+
+	const enum audio_format format = AUDIO_FORMAT_FLOAT;
+
+	if (is_audio_planar(format)) {
+		CA_LOG_ENCODER("AAC", encoder, LOG_ERROR,
+				"Got non-interleaved audio format %d", format);
+		return NULL;
+	}
+
+	ca_encoder *ca = bzalloc(sizeof(ca_encoder));
+	ca->encoder = encoder;
+	ca->format_name = "AAC";
+
+	audio_t *audio = obs_encoder_audio(encoder);
+	const struct audio_output_info *aoi = audio_output_get_info(audio);
+
+	ca->channels = audio_output_get_channels(audio);
+	ca->samples_per_second = audio_output_get_sample_rate(audio);
+
+	size_t bytes_per_frame  = get_audio_size(format, aoi->speakers, 1);
+	size_t bits_per_channel = get_audio_bytes_per_channel(format) * 8;
+
+	AudioStreamBasicDescription in = {
+		.mSampleRate = (Float64)ca->samples_per_second,
+		.mChannelsPerFrame = (UInt32)ca->channels,
+		.mBytesPerFrame = (UInt32)bytes_per_frame,
+		.mFramesPerPacket = 1,
+		.mBytesPerPacket = (UInt32)(1 * bytes_per_frame),
+		.mBitsPerChannel = (UInt32)bits_per_channel,
+		.mFormatID = kAudioFormatLinearPCM,
+		.mFormatFlags = kAudioFormatFlagsNativeEndian |
+			kAudioFormatFlagIsPacked |
+			kAudioFormatFlagIsFloat |
+			0
+	};
+
+	AudioStreamBasicDescription out = {
+		.mSampleRate = (Float64)ca->samples_per_second,
+		.mChannelsPerFrame = (UInt32)ca->channels,
+		.mBytesPerFrame = 0,
+		.mFramesPerPacket = 0,
+		.mBitsPerChannel = 0,
+		.mFormatID = kAudioFormatMPEG4AAC,
+		.mFormatFlags = 0
+	};
+
+#define STATUS_CHECK(c) \
+	code = c; \
+	if (code) { \
+		log_osstatus(ca, #c, code); \
+		goto free; \
+	}
+
+	UInt32 size = sizeof(out);
+	OSStatus code;
+	STATUS_CHECK(AudioFormatGetProperty(kAudioFormatProperty_FormatInfo,
+			0, NULL, &size, &out));
+
+	STATUS_CHECK(AudioConverterNew(&in, &out, &ca->converter))
+
+	UInt32 converter_quality = kAudioConverterQuality_Max;
+	STATUS_CHECK(AudioConverterSetProperty(ca->converter,
+			kAudioConverterCodecQuality,
+			sizeof(converter_quality), &converter_quality));
+
+	UInt32 rate_control = kAudioCodecBitRateControlMode_Constant;
+	STATUS_CHECK(AudioConverterSetProperty(ca->converter,
+			kAudioCodecPropertyBitRateControlMode,
+			sizeof(rate_control), &rate_control));
+
+	if (!bitrate_valid(ca, NULL, bitrate)) {
+		CA_BLOG(LOG_ERROR, "Encoder does not support bitrate %u",
+				(uint32_t)bitrate);
+		goto free;
+	}
+
+	STATUS_CHECK(AudioConverterSetProperty(ca->converter,
+			kAudioConverterEncodeBitRate,
+			sizeof(bitrate), &bitrate));
+
+	size = sizeof(in);
+	STATUS_CHECK(AudioConverterGetProperty(ca->converter,
+			kAudioConverterCurrentInputStreamDescription,
+			&size, &in));
+
+	size = sizeof(out);
+	STATUS_CHECK(AudioConverterGetProperty(ca->converter,
+			kAudioConverterCurrentOutputStreamDescription,
+			&size, &out));
+
+	ca->in_frame_size     = in.mBytesPerFrame;
+	ca->in_packets        = out.mFramesPerPacket / in.mFramesPerPacket;
+	ca->in_bytes_required = ca->in_packets * ca->in_frame_size;
+
+	ca->out_frames_per_packet = out.mFramesPerPacket;
+
+	da_init(ca->input_buffer);
+
+	ca->output_buffer_size = out.mBytesPerPacket;
+
+	if (out.mBytesPerPacket == 0) {
+		UInt32 max_packet_size = 0;
+		size = sizeof(max_packet_size);
+		
+		code = AudioConverterGetProperty(ca->converter,
+				kAudioConverterPropertyMaximumOutputPacketSize,
+				&size, &max_packet_size);
+		if (code) {
+			log_osstatus(ca, "AudioConverterGetProperty(PacketSz)",
+					code);
+			ca->output_buffer_size = 32768;
+		} else {
+			ca->output_buffer_size = max_packet_size;
+		}
+	}
+
+	ca->output_buffer = bmalloc(ca->output_buffer_size);
+
+	CA_BLOG(LOG_INFO, "settings:\n"
+			"\tbitrate:       %u\n"
+			"\tsample rate:   %llu\n"
+			"\tcbr:           %s\n"
+			"\toutput buffer: %lu",
+			bitrate / 1000, ca->samples_per_second,
+			rate_control == kAudioCodecBitRateControlMode_Constant ?
+			"on" : "off",
+			(unsigned long)ca->output_buffer_size);
+
+	return ca;
+
+free:
+	aac_destroy(ca);
+	return NULL;
+}
+
+static OSStatus complex_input_data_proc(AudioConverterRef inAudioConverter,
+		UInt32 *ioNumberDataPackets, AudioBufferList *ioData,
+		AudioStreamPacketDescription **outDataPacketDescription,
+		void *inUserData)
+{
+	UNUSED_PARAMETER(inAudioConverter);
+	UNUSED_PARAMETER(outDataPacketDescription);
+
+	ca_encoder *ca = inUserData;
+
+	if (ca->bytes_read)
+		da_erase_range(ca->input_buffer, 0, ca->bytes_read);
+
+	if (ca->input_buffer.num < ca->in_bytes_required) {
+		*ioNumberDataPackets = 0;
+		ioData->mBuffers[0].mData = NULL;
+		return 1;
+	}
+
+	*ioNumberDataPackets =
+		(UInt32)(ca->in_bytes_required / ca->in_frame_size);
+	ioData->mNumberBuffers = 1;
+
+	ioData->mBuffers[0].mData = ca->input_buffer.array;
+	ioData->mBuffers[0].mNumberChannels = (UInt32)ca->channels;
+	ioData->mBuffers[0].mDataByteSize = (UInt32)ca->in_bytes_required;
+
+	ca->bytes_read += ca->in_packets * ca->in_frame_size;
+
+	return 0;
+}
+
+static bool aac_encode(void *data, struct encoder_frame *frame,
+		struct encoder_packet *packet, bool *received_packet)
+{
+	ca_encoder *ca = data;
+
+	da_push_back_array(ca->input_buffer, frame->data[0],
+			frame->linesize[0]);
+	ca->bytes_read = 0;
+
+	if (ca->input_buffer.num < ca->in_bytes_required)
+		return true;
+
+	UInt32 packets = 1;
+
+	AudioBufferList buffer_list = {
+		.mNumberBuffers = 1,
+		.mBuffers = { {
+			.mNumberChannels = (UInt32)ca->channels,
+			.mDataByteSize = (UInt32)ca->output_buffer_size,
+			.mData = ca->output_buffer,
+		} },
+	};
+
+	AudioStreamPacketDescription out_desc = { 0 };
+
+	OSStatus code = AudioConverterFillComplexBuffer(ca->converter,
+			complex_input_data_proc, ca, &packets,
+			&buffer_list, &out_desc);
+	if (code && code != 1) {
+		log_osstatus(ca, "AudioConverterFillComplexBuffer", code);
+		return false;
+	}
+
+	if (ca->bytes_read)
+		da_erase_range(ca->input_buffer, 0, ca->bytes_read);
+
+	if (!(*received_packet = packets > 0))
+		return true;
+
+	packet->pts = ca->total_samples;
+	packet->dts = ca->total_samples;
+	packet->timebase_num = 1;
+	packet->timebase_den = (uint32_t)ca->samples_per_second;
+	packet->type = OBS_ENCODER_AUDIO;
+	packet->size = out_desc.mDataByteSize;
+	packet->data =
+		(uint8_t*)buffer_list.mBuffers[0].mData + out_desc.mStartOffset;
+
+	ca->total_samples += ca->bytes_read / ca->in_frame_size;
+
+	return true;
+}
+
+static void aac_audio_info(void *data, struct audio_convert_info *info)
+{
+	UNUSED_PARAMETER(data);
+
+	info->format = AUDIO_FORMAT_FLOAT;
+}
+
+static size_t aac_frame_size(void *data)
+{
+	ca_encoder *ca = data;
+	return ca->out_frames_per_packet;
+}
+
+/* The following code was extracted from encca_aac.c in HandBrake's libhb */
+#define MP4ESDescrTag                   0x03
+#define MP4DecConfigDescrTag            0x04
+#define MP4DecSpecificDescrTag          0x05
+
+// based off of mov_mp4_read_descr_len from mov.c in ffmpeg's libavformat
+static int read_descr_len(uint8_t **buffer)
+{
+	int len = 0;
+	int count = 4;
+	while (count--)
+	{
+		int c = *(*buffer)++;
+		len = (len << 7) | (c & 0x7f);
+		if (!(c & 0x80))
+			break;
+	}
+	return len;
+}
+
+// based off of mov_mp4_read_descr from mov.c in ffmpeg's libavformat
+static int read_descr(uint8_t **buffer, int *tag)
+{
+	*tag = *(*buffer)++;
+	return read_descr_len(buffer);
+}
+
+// based off of mov_read_esds from mov.c in ffmpeg's libavformat
+static void read_esds_desc_ext(uint8_t* desc_ext, uint8_t **buffer,
+		uint32_t *size, bool version_flags)
+{
+	uint8_t *esds = desc_ext;
+	int tag, len;
+	*size = 0;
+
+	if (version_flags)
+		esds += 4; // version + flags
+
+	read_descr(&esds, &tag);
+	esds += 2;     // ID
+	if (tag == MP4ESDescrTag)
+		esds++;    // priority
+
+	read_descr(&esds, &tag);
+	if (tag == MP4DecConfigDescrTag) {
+		esds++;    // object type id
+		esds++;    // stream type
+		esds += 3; // buffer size db
+		esds += 4; // max bitrate
+		esds += 4; // average bitrate
+
+		len = read_descr(&esds, &tag);
+		if (tag == MP4DecSpecificDescrTag) {
+			*buffer = bzalloc(len + 8);
+			if (*buffer) {
+				memcpy(*buffer, esds, len);
+				*size = len;
+			}
+		}
+	}
+}
+/* extracted code ends here */
+
+static void query_extra_data(ca_encoder *ca)
+{
+	UInt32 size = 0;
+
+	OSStatus code;
+	code = AudioConverterGetPropertyInfo(ca->converter,
+			kAudioConverterCompressionMagicCookie,
+			&size, NULL);
+	if (code) {
+		log_osstatus(ca, "AudioConverterGetPropertyInfo(magic_cookie)",
+				code);
+		return;
+	}
+
+	if (!size) {
+		CA_BLOG(LOG_WARNING, "Got 0 data size info for magic_cookie");
+		return;
+	}
+
+	uint8_t *extra_data = malloc(size);
+
+	code = AudioConverterGetProperty(ca->converter,
+			kAudioConverterCompressionMagicCookie,
+			&size, extra_data);
+	if (code) {
+		log_osstatus(ca, "AudioConverterGetProperty(magic_cookie)",
+				code);
+		goto free;
+	}
+
+	if (!size) {
+		CA_BLOG(LOG_WARNING, "Got 0 data size for magic_cookie");
+		goto free;
+	}
+
+	read_esds_desc_ext(extra_data, &ca->extra_data, &ca->extra_data_size,
+			false);
+
+free:
+	free(extra_data);
+}
+
+static bool aac_extra_data(void *data, uint8_t **extra_data, size_t *size)
+{
+	ca_encoder *ca = data;
+	
+	if (!ca->extra_data)
+		query_extra_data(ca);
+
+	if (!ca->extra_data_size)
+		return false;
+
+	*extra_data = ca->extra_data;
+	*size = ca->extra_data_size;
+	return true;
+}
+
+static AudioConverterRef aac_default_converter(void)
+{
+	UInt32 bytes_per_frame = 8;
+	UInt32 channels = 2;
+	UInt32 bits_per_channel = bytes_per_frame / channels * 8;
+
+	AudioStreamBasicDescription in = {
+		.mSampleRate = 44100,
+		.mChannelsPerFrame = channels,
+		.mBytesPerFrame = bytes_per_frame,
+		.mFramesPerPacket = 1,
+		.mBytesPerPacket = 1 * bytes_per_frame,
+		.mBitsPerChannel = bits_per_channel,
+		.mFormatID = kAudioFormatLinearPCM,
+		.mFormatFlags = kAudioFormatFlagsNativeEndian |
+			kAudioFormatFlagIsPacked |
+			kAudioFormatFlagIsFloat |
+			0
+	};
+
+	AudioStreamBasicDescription out = {
+		.mSampleRate = 44100,
+		.mChannelsPerFrame = channels,
+		.mBytesPerFrame = 0,
+		.mFramesPerPacket = 0,
+		.mBitsPerChannel = 0,
+		.mFormatID = kAudioFormatMPEG4AAC,
+		.mFormatFlags = 0
+	};
+
+	UInt32 size = sizeof(out);
+	OSStatus code = AudioFormatGetProperty(kAudioFormatProperty_FormatInfo,
+			0, NULL, &size, &out);
+	if (code) {
+		log_osstatus(NULL, "AudioFormatGetProperty(format_info)", code);
+		return NULL;
+	}
+
+	AudioConverterRef converter;
+	code = AudioConverterNew(&in, &out, &converter);
+	if (code) {
+		log_osstatus(NULL, "AudioConverterNew", code);
+		return NULL;
+	}
+
+	return converter;
+}
+
+struct find_matching_bitrate_helper {
+	UInt32 bitrate;
+	UInt32 best_match;
+	int diff;
+};
+typedef struct find_matching_bitrate_helper find_matching_bitrate_helper;
+
+static void find_matching_bitrate_func(void *data, UInt32 min, UInt32 max)
+{
+	find_matching_bitrate_helper *helper = data;
+
+	int min_diff = abs((int)helper->bitrate - (int)min);
+	int max_diff = abs((int)helper->bitrate - (int)max);
+
+	if (min_diff < helper->diff) {
+		helper->best_match = min;
+		helper->diff = min_diff;
+	}
+
+	if (max_diff < helper->diff) {
+		helper->best_match = max;
+		helper->diff = max_diff;
+	}
+}
+
+static UInt32 find_matching_bitrate(UInt32 bitrate)
+{
+	find_matching_bitrate_helper helper = {
+		.bitrate = bitrate * 1000,
+		.best_match = 0,
+		.diff = INT_MAX,
+	};
+
+	AudioConverterRef converter = aac_default_converter();
+	if (!converter) {
+		CA_LOG(LOG_ERROR, "Could not get converter to match "
+				"default bitrate");
+		return bitrate;
+	}
+
+	bool has_bitrates = enumerate_bitrates(NULL, converter,
+			find_matching_bitrate_func, &helper);
+	AudioConverterDispose(converter);
+
+	if (!has_bitrates) {
+		CA_LOG(LOG_ERROR, "No bitrates found while matching "
+				"default bitrate");
+		AudioConverterDispose(converter);
+		return bitrate;
+	}
+
+	if (helper.best_match != helper.bitrate)
+		CA_LOG(LOG_INFO, "Returning closest matching bitrate %u "
+				"instead of requested bitrate %u",
+				(uint32_t)helper.best_match / 1000,
+				(uint32_t)bitrate);
+
+	return helper.best_match / 1000;
+}
+
+static void aac_defaults(obs_data_t *settings)
+{
+	obs_data_set_default_int(settings, "bitrate",
+			find_matching_bitrate(128));
+}
+
+struct add_bitrates_helper {
+	DARRAY(UInt32) bitrates;
+};
+typedef struct add_bitrates_helper add_bitrates_helper;
+
+static void add_bitrates_func(void *data, UInt32 min, UInt32 max)
+{
+	add_bitrates_helper *helper = data;
+
+	if (da_find(helper->bitrates, &min, 0) == DARRAY_INVALID)
+		da_push_back(helper->bitrates, &min);
+	if (da_find(helper->bitrates, &max, 0) == DARRAY_INVALID)
+		da_push_back(helper->bitrates, &max);
+}
+
+static int bitrate_compare(const void *data1, const void *data2)
+{
+	const UInt32 *bitrate1 = data1;
+	const UInt32 *bitrate2 = data2;
+
+	return (int)*bitrate1 - (int)*bitrate2;
+}
+
+static void add_bitrates(obs_property_t *prop, ca_encoder *ca)
+{
+	add_bitrates_helper helper = { 0 };
+
+	if (!enumerate_bitrates(ca, ca ? NULL : aac_default_converter(),
+			add_bitrates_func, &helper))
+		return;
+
+	qsort(helper.bitrates.array, helper.bitrates.num, sizeof(UInt32),
+			bitrate_compare);
+
+	struct dstr str = { 0 };
+	for (size_t i = 0; i < helper.bitrates.num; i++) {
+		dstr_printf(&str, "%u",
+				(uint32_t)helper.bitrates.array[i]/1000);
+		obs_property_list_add_int(prop, str.array,
+				helper.bitrates.array[i]/1000);
+	}
+	dstr_free(&str);
+}
+
+static obs_properties_t *aac_properties(void *data)
+{
+	ca_encoder *ca = data;
+
+	obs_properties_t *props = obs_properties_create();
+
+	obs_property_t *p = obs_properties_add_list(props, "bitrate",
+			obs_module_text("Bitrate"),
+			OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
+	add_bitrates(p, ca);
+
+	return props;
+}
+
+static struct obs_encoder_info aac_info = {
+	.id = "CoreAudio_AAC",
+	.type = OBS_ENCODER_AUDIO,
+	.codec = "AAC",
+	.get_name = aac_get_name,
+	.destroy = aac_destroy,
+	.create = aac_create,
+	.encode = aac_encode,
+	.get_frame_size = aac_frame_size,
+	.get_audio_info = aac_audio_info,
+	.get_extra_data = aac_extra_data,
+	.get_defaults = aac_defaults,
+};
+
+OBS_DECLARE_MODULE()
+OBS_MODULE_USE_DEFAULT_LOCALE("coreaudio-encoder", "en-US")
+
+bool obs_module_load(void)
+{
+	obs_register_encoder(&aac_info);
+	return true;
+}