| /* | |
| * QEMU OS X CoreAudio audio driver | |
| * | |
| * Copyright (c) 2005 Mike Kronenberg | |
| * | |
| * Permission is hereby granted, free of charge, to any person obtaining a copy | |
| * of this software and associated documentation files (the "Software"), to deal | |
| * in the Software without restriction, including without limitation the rights | |
| * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| * copies of the Software, and to permit persons to whom the Software is | |
| * furnished to do so, subject to the following conditions: | |
| * | |
| * The above copyright notice and this permission notice shall be included in | |
| * all copies or substantial portions of the Software. | |
| * | |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | |
| * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| * THE SOFTWARE. | |
| */ | |
| #include "qemu/osdep.h" | |
| #include <CoreAudio/CoreAudio.h> | |
| #include <pthread.h> /* pthread_X */ | |
| #include "qemu/main-loop.h" | |
| #include "qemu/module.h" | |
| #include "qemu/error-report.h" | |
| #include "qemu/audio.h" | |
| #include "qom/object.h" | |
| #include "audio_int.h" | |
| #define TYPE_AUDIO_COREAUDIO "audio-coreaudio" | |
| OBJECT_DECLARE_SIMPLE_TYPE(AudioCoreaudio, AUDIO_COREAUDIO) | |
| struct AudioCoreaudio { | |
| AudioMixengBackend parent_obj; | |
| }; | |
| typedef struct coreaudioVoiceOut { | |
| HWVoiceOut hw; | |
| pthread_mutex_t buf_mutex; | |
| AudioDeviceID device_id; | |
| int frame_size_setting; | |
| uint32_t buffer_count; | |
| UInt32 device_frame_size; | |
| AudioDeviceIOProcID ioprocid; | |
| bool enabled; | |
| } CoreaudioVoiceOut; | |
| static const AudioObjectPropertyAddress voice_out_addr = { | |
| kAudioHardwarePropertyDefaultOutputDevice, | |
| kAudioObjectPropertyScopeGlobal, | |
| kAudioObjectPropertyElementMain | |
| }; | |
| static OSStatus coreaudio_get_voice_out(AudioDeviceID *id) | |
| { | |
| UInt32 size = sizeof(*id); | |
| return AudioObjectGetPropertyData(kAudioObjectSystemObject, | |
| &voice_out_addr, | |
| 0, | |
| NULL, | |
| &size, | |
| id); | |
| } | |
| static OSStatus coreaudio_get_out_framesizerange(AudioDeviceID id, | |
| AudioValueRange *framerange) | |
| { | |
| UInt32 size = sizeof(*framerange); | |
| AudioObjectPropertyAddress addr = { | |
| kAudioDevicePropertyBufferFrameSizeRange, | |
| kAudioDevicePropertyScopeOutput, | |
| kAudioObjectPropertyElementMain | |
| }; | |
| return AudioObjectGetPropertyData(id, | |
| &addr, | |
| 0, | |
| NULL, | |
| &size, | |
| framerange); | |
| } | |
| static OSStatus coreaudio_get_out_framesize(AudioDeviceID id, UInt32 *framesize) | |
| { | |
| UInt32 size = sizeof(*framesize); | |
| AudioObjectPropertyAddress addr = { | |
| kAudioDevicePropertyBufferFrameSize, | |
| kAudioDevicePropertyScopeOutput, | |
| kAudioObjectPropertyElementMain | |
| }; | |
| return AudioObjectGetPropertyData(id, | |
| &addr, | |
| 0, | |
| NULL, | |
| &size, | |
| framesize); | |
| } | |
| static OSStatus coreaudio_set_out_framesize(AudioDeviceID id, UInt32 *framesize) | |
| { | |
| UInt32 size = sizeof(*framesize); | |
| AudioObjectPropertyAddress addr = { | |
| kAudioDevicePropertyBufferFrameSize, | |
| kAudioDevicePropertyScopeOutput, | |
| kAudioObjectPropertyElementMain | |
| }; | |
| return AudioObjectSetPropertyData(id, | |
| &addr, | |
| 0, | |
| NULL, | |
| size, | |
| framesize); | |
| } | |
| static OSStatus coreaudio_set_out_streamformat(AudioDeviceID id, | |
| AudioStreamBasicDescription *d) | |
| { | |
| UInt32 size = sizeof(*d); | |
| AudioObjectPropertyAddress addr = { | |
| kAudioDevicePropertyStreamFormat, | |
| kAudioDevicePropertyScopeOutput, | |
| kAudioObjectPropertyElementMain | |
| }; | |
| return AudioObjectSetPropertyData(id, | |
| &addr, | |
| 0, | |
| NULL, | |
| size, | |
| d); | |
| } | |
| static OSStatus coreaudio_get_out_isrunning(AudioDeviceID id, UInt32 *result) | |
| { | |
| UInt32 size = sizeof(*result); | |
| AudioObjectPropertyAddress addr = { | |
| kAudioDevicePropertyDeviceIsRunning, | |
| kAudioDevicePropertyScopeOutput, | |
| kAudioObjectPropertyElementMain | |
| }; | |
| return AudioObjectGetPropertyData(id, | |
| &addr, | |
| 0, | |
| NULL, | |
| &size, | |
| result); | |
| } | |
| static void coreaudio_logstatus(OSStatus status) | |
| { | |
| const char *str = "BUG"; | |
| switch (status) { | |
| case kAudioHardwareNoError: | |
| str = "kAudioHardwareNoError"; | |
| break; | |
| case kAudioHardwareNotRunningError: | |
| str = "kAudioHardwareNotRunningError"; | |
| break; | |
| case kAudioHardwareUnspecifiedError: | |
| str = "kAudioHardwareUnspecifiedError"; | |
| break; | |
| case kAudioHardwareUnknownPropertyError: | |
| str = "kAudioHardwareUnknownPropertyError"; | |
| break; | |
| case kAudioHardwareBadPropertySizeError: | |
| str = "kAudioHardwareBadPropertySizeError"; | |
| break; | |
| case kAudioHardwareIllegalOperationError: | |
| str = "kAudioHardwareIllegalOperationError"; | |
| break; | |
| case kAudioHardwareBadDeviceError: | |
| str = "kAudioHardwareBadDeviceError"; | |
| break; | |
| case kAudioHardwareBadStreamError: | |
| str = "kAudioHardwareBadStreamError"; | |
| break; | |
| case kAudioHardwareUnsupportedOperationError: | |
| str = "kAudioHardwareUnsupportedOperationError"; | |
| break; | |
| case kAudioDeviceUnsupportedFormatError: | |
| str = "kAudioDeviceUnsupportedFormatError"; | |
| break; | |
| case kAudioDevicePermissionsError: | |
| str = "kAudioDevicePermissionsError"; | |
| break; | |
| default: | |
| error_printf(" Reason: status code %" PRId32, (int32_t)status); | |
| return; | |
| } | |
| error_printf(" Reason: %s", str); | |
| } | |
| static void G_GNUC_PRINTF(2, 3) coreaudio_logerr(OSStatus status, | |
| const char *fmt, ...) | |
| { | |
| va_list ap; | |
| error_printf("coreaudio: "); | |
| va_start(ap, fmt); | |
| error_vprintf(fmt, ap); | |
| va_end(ap); | |
| coreaudio_logstatus(status); | |
| error_printf("\n"); | |
| } | |
| static void G_GNUC_PRINTF(3, 4) coreaudio_logerr2(OSStatus status, | |
| const char *typ, | |
| const char *fmt, ...) | |
| { | |
| va_list ap; | |
| error_printf("coreaudio: Could not initialize %s: ", typ); | |
| va_start(ap, fmt); | |
| error_vprintf(fmt, ap); | |
| va_end(ap); | |
| coreaudio_logstatus(status); | |
| error_printf("\n"); | |
| } | |
| #define coreaudio_playback_logerr(status, ...) \ | |
| coreaudio_logerr2(status, "playback", __VA_ARGS__) | |
| static int coreaudio_voice_out_buf_lock(CoreaudioVoiceOut *core, | |
| const char *fn_name) | |
| { | |
| int err; | |
| err = pthread_mutex_lock(&core->buf_mutex); | |
| if (err) { | |
| error_report("coreaudio: Could not lock voice for %s: %s", | |
| fn_name, strerror(err)); | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| static int coreaudio_voice_out_buf_unlock(CoreaudioVoiceOut *core, | |
| const char *fn_name) | |
| { | |
| int err; | |
| err = pthread_mutex_unlock(&core->buf_mutex); | |
| if (err) { | |
| error_report("coreaudio: Could not unlock voice for %s: %s", | |
| fn_name, strerror(err)); | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| #define COREAUDIO_WRAPPER_FUNC(name, ret_type, args_decl, args) \ | |
| static ret_type glue(coreaudio_, name)args_decl \ | |
| { \ | |
| CoreaudioVoiceOut *core = (CoreaudioVoiceOut *)hw; \ | |
| ret_type ret; \ | |
| \ | |
| if (coreaudio_voice_out_buf_lock(core, "coreaudio_" #name)) { \ | |
| return 0; \ | |
| } \ | |
| \ | |
| ret = glue(audio_generic_, name)args; \ | |
| \ | |
| coreaudio_voice_out_buf_unlock(core, "coreaudio_" #name); \ | |
| return ret; \ | |
| } | |
| COREAUDIO_WRAPPER_FUNC(buffer_get_free, size_t, (HWVoiceOut *hw), (hw)) | |
| COREAUDIO_WRAPPER_FUNC(get_buffer_out, void *, (HWVoiceOut *hw, size_t *size), | |
| (hw, size)) | |
| COREAUDIO_WRAPPER_FUNC(put_buffer_out, size_t, | |
| (HWVoiceOut *hw, void *buf, size_t size), | |
| (hw, buf, size)) | |
| COREAUDIO_WRAPPER_FUNC(write, size_t, (HWVoiceOut *hw, void *buf, size_t size), | |
| (hw, buf, size)) | |
| #undef COREAUDIO_WRAPPER_FUNC | |
| /* | |
| * callback to feed audiooutput buffer. called without BQL. | |
| * allowed to lock "buf_mutex", but disallowed to have any other locks. | |
| */ | |
| static OSStatus out_device_ioproc( | |
| AudioDeviceID inDevice, | |
| const AudioTimeStamp *inNow, | |
| const AudioBufferList *inInputData, | |
| const AudioTimeStamp *inInputTime, | |
| AudioBufferList *outOutputData, | |
| const AudioTimeStamp *inOutputTime, | |
| void *hwptr) | |
| { | |
| UInt32 frame_size, pending_frames; | |
| void *out = outOutputData->mBuffers[0].mData; | |
| HWVoiceOut *hw = hwptr; | |
| CoreaudioVoiceOut *core = hwptr; | |
| size_t len; | |
| if (coreaudio_voice_out_buf_lock(core, "out_device_ioproc")) { | |
| inInputTime = 0; | |
| return 0; | |
| } | |
| if (inDevice != core->device_id) { | |
| coreaudio_voice_out_buf_unlock(core, "out_device_ioproc(old device)"); | |
| return 0; | |
| } | |
| frame_size = core->device_frame_size; | |
| pending_frames = hw->pending_emul / hw->info.bytes_per_frame; | |
| /* if there are not enough samples, set signal and return */ | |
| if (pending_frames < frame_size) { | |
| inInputTime = 0; | |
| coreaudio_voice_out_buf_unlock(core, "out_device_ioproc(empty)"); | |
| return 0; | |
| } | |
| len = frame_size * hw->info.bytes_per_frame; | |
| while (len) { | |
| size_t write_len, start; | |
| start = audio_ring_posb(hw->pos_emul, hw->pending_emul, hw->size_emul); | |
| assert(start < hw->size_emul); | |
| write_len = MIN(MIN(hw->pending_emul, len), | |
| hw->size_emul - start); | |
| memcpy(out, hw->buf_emul + start, write_len); | |
| hw->pending_emul -= write_len; | |
| len -= write_len; | |
| out += write_len; | |
| } | |
| coreaudio_voice_out_buf_unlock(core, "out_device_ioproc"); | |
| return 0; | |
| } | |
| static OSStatus init_out_device(CoreaudioVoiceOut *core) | |
| { | |
| AudioDeviceID device_id; | |
| AudioDeviceIOProcID ioprocid; | |
| AudioValueRange value_range; | |
| OSStatus status; | |
| UInt32 device_frame_size; | |
| AudioStreamBasicDescription stream_basic_description = { | |
| .mBitsPerChannel = audio_format_bits(core->hw.info.af), | |
| .mBytesPerFrame = core->hw.info.bytes_per_frame, | |
| .mBytesPerPacket = core->hw.info.bytes_per_frame, | |
| .mChannelsPerFrame = core->hw.info.nchannels, | |
| .mFormatFlags = kLinearPCMFormatFlagIsFloat, | |
| .mFormatID = kAudioFormatLinearPCM, | |
| .mFramesPerPacket = 1, | |
| .mSampleRate = core->hw.info.freq | |
| }; | |
| status = coreaudio_get_voice_out(&device_id); | |
| if (status != kAudioHardwareNoError) { | |
| coreaudio_playback_logerr(status, | |
| "Could not get default output device"); | |
| return status; | |
| } | |
| if (device_id == kAudioDeviceUnknown) { | |
| error_report("coreaudio: Could not initialize playback: " | |
| "Unknown audio device"); | |
| return status; | |
| } | |
| /* get minimum and maximum buffer frame sizes */ | |
| status = coreaudio_get_out_framesizerange(device_id, &value_range); | |
| if (status == kAudioHardwareBadObjectError) { | |
| return 0; | |
| } | |
| if (status != kAudioHardwareNoError) { | |
| coreaudio_playback_logerr(status, | |
| "Could not get device buffer frame range"); | |
| return status; | |
| } | |
| if (value_range.mMinimum > core->frame_size_setting) { | |
| device_frame_size = value_range.mMinimum; | |
| warn_report("coreaudio: Upsizing buffer frames to %f", | |
| value_range.mMinimum); | |
| } else if (value_range.mMaximum < core->frame_size_setting) { | |
| device_frame_size = value_range.mMaximum; | |
| warn_report("coreaudio: Downsizing buffer frames to %f", | |
| value_range.mMaximum); | |
| } else { | |
| device_frame_size = core->frame_size_setting; | |
| } | |
| /* set Buffer Frame Size */ | |
| status = coreaudio_set_out_framesize(device_id, &device_frame_size); | |
| if (status == kAudioHardwareBadObjectError) { | |
| return 0; | |
| } | |
| if (status != kAudioHardwareNoError) { | |
| coreaudio_playback_logerr(status, | |
| "Could not set device buffer frame size %" PRIu32, | |
| (uint32_t)device_frame_size); | |
| return status; | |
| } | |
| /* get Buffer Frame Size */ | |
| status = coreaudio_get_out_framesize(device_id, &device_frame_size); | |
| if (status == kAudioHardwareBadObjectError) { | |
| return 0; | |
| } | |
| if (status != kAudioHardwareNoError) { | |
| coreaudio_playback_logerr(status, | |
| "Could not get device buffer frame size"); | |
| return status; | |
| } | |
| /* set Samplerate */ | |
| status = coreaudio_set_out_streamformat(device_id, | |
| &stream_basic_description); | |
| if (status == kAudioHardwareBadObjectError) { | |
| return 0; | |
| } | |
| if (status != kAudioHardwareNoError) { | |
| coreaudio_playback_logerr(status, | |
| "Could not set samplerate %lf", | |
| stream_basic_description.mSampleRate); | |
| return status; | |
| } | |
| /* | |
| * set Callback. | |
| * | |
| * On macOS 11.3.1, Core Audio calls AudioDeviceIOProc after calling an | |
| * internal function named HALB_Mutex::Lock(), which locks a mutex in | |
| * HALB_IOThread::Entry(void*). HALB_Mutex::Lock() is also called in | |
| * AudioObjectGetPropertyData, which is called by coreaudio driver. | |
| * Therefore, the specified callback must be designed to avoid a deadlock | |
| * with the callers of AudioObjectGetPropertyData. | |
| */ | |
| ioprocid = NULL; | |
| status = AudioDeviceCreateIOProcID(device_id, | |
| out_device_ioproc, | |
| &core->hw, | |
| &ioprocid); | |
| if (status == kAudioHardwareBadDeviceError) { | |
| return 0; | |
| } | |
| if (status != kAudioHardwareNoError || ioprocid == NULL) { | |
| coreaudio_playback_logerr(status, "Could not set IOProc"); | |
| return status; | |
| } | |
| core->device_id = device_id; | |
| core->device_frame_size = device_frame_size; | |
| core->hw.samples = core->buffer_count * core->device_frame_size; | |
| audio_generic_initialize_buffer_out(&core->hw); | |
| core->ioprocid = ioprocid; | |
| return 0; | |
| } | |
| static void fini_out_device(CoreaudioVoiceOut *core) | |
| { | |
| OSStatus status; | |
| UInt32 isrunning; | |
| /* stop playback */ | |
| status = coreaudio_get_out_isrunning(core->device_id, &isrunning); | |
| if (status != kAudioHardwareBadObjectError) { | |
| if (status != kAudioHardwareNoError) { | |
| coreaudio_logerr(status, | |
| "Could not determine whether device is playing"); | |
| } | |
| if (isrunning) { | |
| status = AudioDeviceStop(core->device_id, core->ioprocid); | |
| if (status != kAudioHardwareBadDeviceError && status != kAudioHardwareNoError) { | |
| coreaudio_logerr(status, "Could not stop playback"); | |
| } | |
| } | |
| } | |
| /* remove callback */ | |
| status = AudioDeviceDestroyIOProcID(core->device_id, | |
| core->ioprocid); | |
| if (status != kAudioHardwareBadDeviceError && status != kAudioHardwareNoError) { | |
| coreaudio_logerr(status, "Could not remove IOProc"); | |
| } | |
| core->device_id = kAudioDeviceUnknown; | |
| } | |
| static void update_out_device_playback_state(CoreaudioVoiceOut *core) | |
| { | |
| OSStatus status; | |
| UInt32 isrunning; | |
| status = coreaudio_get_out_isrunning(core->device_id, &isrunning); | |
| if (status != kAudioHardwareNoError) { | |
| if (status != kAudioHardwareBadObjectError) { | |
| coreaudio_logerr(status, | |
| "Could not determine whether device is playing"); | |
| } | |
| return; | |
| } | |
| if (core->enabled) { | |
| /* start playback */ | |
| if (!isrunning) { | |
| status = AudioDeviceStart(core->device_id, core->ioprocid); | |
| if (status != kAudioHardwareBadDeviceError && status != kAudioHardwareNoError) { | |
| coreaudio_logerr(status, "Could not resume playback"); | |
| } | |
| } | |
| } else { | |
| /* stop playback */ | |
| if (isrunning) { | |
| status = AudioDeviceStop(core->device_id, | |
| core->ioprocid); | |
| if (status != kAudioHardwareBadDeviceError && status != kAudioHardwareNoError) { | |
| coreaudio_logerr(status, "Could not pause playback"); | |
| } | |
| } | |
| } | |
| } | |
| /* called without BQL. */ | |
| static OSStatus handle_voice_out_change( | |
| AudioObjectID in_object_id, | |
| UInt32 in_number_addresses, | |
| const AudioObjectPropertyAddress *in_addresses, | |
| void *in_client_data) | |
| { | |
| CoreaudioVoiceOut *core = in_client_data; | |
| bql_lock(); | |
| if (core->device_id) { | |
| fini_out_device(core); | |
| } | |
| init_out_device(core); | |
| if (core->device_id) { | |
| update_out_device_playback_state(core); | |
| } | |
| bql_unlock(); | |
| return 0; | |
| } | |
| static int coreaudio_init_out(HWVoiceOut *hw, struct audsettings *as) | |
| { | |
| OSStatus status; | |
| CoreaudioVoiceOut *core = (CoreaudioVoiceOut *)hw; | |
| int err; | |
| Audiodev *dev = hw->s->dev; | |
| AudiodevCoreaudioPerDirectionOptions *cpdo = dev->u.coreaudio.out; | |
| struct audsettings obt_as; | |
| /* create mutex */ | |
| err = pthread_mutex_init(&core->buf_mutex, NULL); | |
| if (err) { | |
| error_report("coreaudio: Could not create mutex: %s", strerror(err)); | |
| return -1; | |
| } | |
| obt_as = *as; | |
| as = &obt_as; | |
| as->fmt = AUDIO_FORMAT_F32; | |
| audio_pcm_init_info(&hw->info, as); | |
| core->frame_size_setting = audio_buffer_frames( | |
| qapi_AudiodevCoreaudioPerDirectionOptions_base(cpdo), as, 11610); | |
| core->buffer_count = cpdo->has_buffer_count ? cpdo->buffer_count : 4; | |
| status = AudioObjectAddPropertyListener(kAudioObjectSystemObject, | |
| &voice_out_addr, | |
| handle_voice_out_change, | |
| core); | |
| if (status != kAudioHardwareNoError) { | |
| coreaudio_playback_logerr(status, | |
| "Could not listen to voice property change"); | |
| return -1; | |
| } | |
| if (init_out_device(core)) { | |
| status = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, | |
| &voice_out_addr, | |
| handle_voice_out_change, | |
| core); | |
| if (status != kAudioHardwareNoError) { | |
| coreaudio_playback_logerr(status, | |
| "Could not remove voice property change listener"); | |
| } | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| static void coreaudio_fini_out (HWVoiceOut *hw) | |
| { | |
| OSStatus status; | |
| int err; | |
| CoreaudioVoiceOut *core = (CoreaudioVoiceOut *)hw; | |
| status = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, | |
| &voice_out_addr, | |
| handle_voice_out_change, | |
| core); | |
| if (status != kAudioHardwareNoError) { | |
| coreaudio_logerr(status, "Could not remove voice property change listener"); | |
| } | |
| fini_out_device(core); | |
| /* destroy mutex */ | |
| err = pthread_mutex_destroy(&core->buf_mutex); | |
| if (err) { | |
| error_report("coreaudio: Could not destroy mutex: %s", strerror(err)); | |
| } | |
| } | |
| static void coreaudio_enable_out(HWVoiceOut *hw, bool enable) | |
| { | |
| CoreaudioVoiceOut *core = (CoreaudioVoiceOut *)hw; | |
| core->enabled = enable; | |
| update_out_device_playback_state(core); | |
| } | |
| static void audio_coreaudio_class_init(ObjectClass *klass, const void *data) | |
| { | |
| AudioMixengBackendClass *k = AUDIO_MIXENG_BACKEND_CLASS(klass); | |
| k->max_voices_out = 1; | |
| k->max_voices_in = 0; | |
| k->voice_size_out = sizeof(CoreaudioVoiceOut); | |
| k->voice_size_in = 0; | |
| k->init_out = coreaudio_init_out; | |
| k->fini_out = coreaudio_fini_out; | |
| /* wrapper for audio_generic_write */ | |
| k->write = coreaudio_write; | |
| /* wrapper for audio_generic_buffer_get_free */ | |
| k->buffer_get_free = coreaudio_buffer_get_free; | |
| /* wrapper for audio_generic_get_buffer_out */ | |
| k->get_buffer_out = coreaudio_get_buffer_out; | |
| /* wrapper for audio_generic_put_buffer_out */ | |
| k->put_buffer_out = coreaudio_put_buffer_out; | |
| k->enable_out = coreaudio_enable_out; | |
| } | |
| static const TypeInfo audio_types[] = { | |
| { | |
| .name = TYPE_AUDIO_COREAUDIO, | |
| .parent = TYPE_AUDIO_MIXENG_BACKEND, | |
| .instance_size = sizeof(AudioCoreaudio), | |
| .class_init = audio_coreaudio_class_init, | |
| }, | |
| }; | |
| DEFINE_TYPES(audio_types) | |
| module_obj(TYPE_AUDIO_COREAUDIO); |