| /* $Id$ */ |
| /* |
| * Copyright (C) 2008-2010 Teluu Inc. (http://www.teluu.com) |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
| */ |
| #include <pjmedia-audiodev/audiodev_imp.h> |
| #include <pj/assert.h> |
| #include <pj/log.h> |
| #include <pj/os.h> |
| |
| #if PJMEDIA_AUDIO_DEV_HAS_COREAUDIO |
| |
| #include "TargetConditionals.h" |
| #if TARGET_OS_IPHONE |
| #define COREAUDIO_MAC 0 |
| #else |
| #define COREAUDIO_MAC 1 |
| #endif |
| |
| #include <AudioUnit/AudioUnit.h> |
| #if !COREAUDIO_MAC |
| #include <AudioToolbox/AudioServices.h> |
| |
| #define AudioDeviceID unsigned |
| |
| /* For iPhone 2.x and earlier */ |
| #if __IPHONE_OS_VERSION_MIN_REQUIRED <= __IPHONE_2_2 |
| #define kAudioUnitSubType_VoiceProcessingIO kAudioUnitSubType_RemoteIO |
| #define kAudioSessionProperty_OverrideCategoryEnableBluetoothInput -1 |
| #endif |
| |
| #endif |
| |
| /* For Mac OS 10.5.x and earlier */ |
| #if AUDIO_UNIT_VERSION < 1060 |
| #define AudioComponent Component |
| #define AudioComponentDescription ComponentDescription |
| #define AudioComponentInstance ComponentInstance |
| #define AudioComponentFindNext FindNextComponent |
| #define AudioComponentInstanceNew OpenAComponent |
| #define AudioComponentInstanceDispose CloseComponent |
| #endif |
| |
| |
| #define THIS_FILE "coreaudio_dev.c" |
| |
| /* coreaudio device info */ |
| struct coreaudio_dev_info |
| { |
| pjmedia_aud_dev_info info; |
| AudioDeviceID dev_id; |
| }; |
| |
| /* coreaudio factory */ |
| struct coreaudio_factory |
| { |
| pjmedia_aud_dev_factory base; |
| pj_pool_t *pool; |
| pj_pool_factory *pf; |
| |
| unsigned dev_count; |
| struct coreaudio_dev_info *dev_info; |
| |
| AudioComponent io_comp; |
| struct coreaudio_stream *stream; |
| }; |
| |
| /* Sound stream. */ |
| struct coreaudio_stream |
| { |
| pjmedia_aud_stream base; /**< Base stream */ |
| pjmedia_aud_param param; /**< Settings */ |
| pj_pool_t *pool; /**< Memory pool. */ |
| struct coreaudio_factory *cf; |
| |
| pjmedia_aud_rec_cb rec_cb; /**< Capture callback. */ |
| pjmedia_aud_play_cb play_cb; /**< Playback callback. */ |
| void *user_data; /**< Application data. */ |
| |
| pj_timestamp play_timestamp; |
| pj_timestamp rec_timestamp; |
| |
| pj_int16_t *rec_buf; |
| unsigned rec_buf_count; |
| pj_int16_t *play_buf; |
| unsigned play_buf_count; |
| |
| pj_bool_t interrupted; |
| pj_bool_t quit_flag; |
| |
| pj_bool_t rec_thread_exited; |
| pj_bool_t rec_thread_initialized; |
| pj_thread_desc rec_thread_desc; |
| pj_thread_t *rec_thread; |
| |
| pj_bool_t play_thread_exited; |
| pj_bool_t play_thread_initialized; |
| pj_thread_desc play_thread_desc; |
| pj_thread_t *play_thread; |
| |
| AudioUnit io_units[2]; |
| AudioStreamBasicDescription streamFormat; |
| AudioBufferList *audio_buf; |
| }; |
| |
| |
| /* Prototypes */ |
| static pj_status_t ca_factory_init(pjmedia_aud_dev_factory *f); |
| static pj_status_t ca_factory_destroy(pjmedia_aud_dev_factory *f); |
| static unsigned ca_factory_get_dev_count(pjmedia_aud_dev_factory *f); |
| static pj_status_t ca_factory_get_dev_info(pjmedia_aud_dev_factory *f, |
| unsigned index, |
| pjmedia_aud_dev_info *info); |
| static pj_status_t ca_factory_default_param(pjmedia_aud_dev_factory *f, |
| unsigned index, |
| pjmedia_aud_param *param); |
| static pj_status_t ca_factory_create_stream(pjmedia_aud_dev_factory *f, |
| const pjmedia_aud_param *param, |
| pjmedia_aud_rec_cb rec_cb, |
| pjmedia_aud_play_cb play_cb, |
| void *user_data, |
| pjmedia_aud_stream **p_aud_strm); |
| |
| static pj_status_t ca_stream_get_param(pjmedia_aud_stream *strm, |
| pjmedia_aud_param *param); |
| static pj_status_t ca_stream_get_cap(pjmedia_aud_stream *strm, |
| pjmedia_aud_dev_cap cap, |
| void *value); |
| static pj_status_t ca_stream_set_cap(pjmedia_aud_stream *strm, |
| pjmedia_aud_dev_cap cap, |
| const void *value); |
| static pj_status_t ca_stream_start(pjmedia_aud_stream *strm); |
| static pj_status_t ca_stream_stop(pjmedia_aud_stream *strm); |
| static pj_status_t ca_stream_destroy(pjmedia_aud_stream *strm); |
| static pj_status_t create_audio_unit(AudioComponent io_comp, |
| AudioDeviceID dev_id, |
| pjmedia_dir dir, |
| struct coreaudio_stream *strm, |
| AudioUnit *io_unit); |
| #if !COREAUDIO_MAC |
| static void interruptionListener(void *inClientData, UInt32 inInterruption); |
| #endif |
| |
| /* Operations */ |
| static pjmedia_aud_dev_factory_op factory_op = |
| { |
| &ca_factory_init, |
| &ca_factory_destroy, |
| &ca_factory_get_dev_count, |
| &ca_factory_get_dev_info, |
| &ca_factory_default_param, |
| &ca_factory_create_stream |
| }; |
| |
| static pjmedia_aud_stream_op stream_op = |
| { |
| &ca_stream_get_param, |
| &ca_stream_get_cap, |
| &ca_stream_set_cap, |
| &ca_stream_start, |
| &ca_stream_stop, |
| &ca_stream_destroy |
| }; |
| |
| |
| /**************************************************************************** |
| * Factory operations |
| */ |
| /* |
| * Init coreaudio audio driver. |
| */ |
| pjmedia_aud_dev_factory* pjmedia_coreaudio_factory(pj_pool_factory *pf) |
| { |
| struct coreaudio_factory *f; |
| pj_pool_t *pool; |
| |
| pool = pj_pool_create(pf, "core audio", 1000, 1000, NULL); |
| f = PJ_POOL_ZALLOC_T(pool, struct coreaudio_factory); |
| f->pf = pf; |
| f->pool = pool; |
| f->base.op = &factory_op; |
| |
| return &f->base; |
| } |
| |
| |
| /* API: init factory */ |
| static pj_status_t ca_factory_init(pjmedia_aud_dev_factory *f) |
| { |
| struct coreaudio_factory *cf = (struct coreaudio_factory*)f; |
| unsigned i; |
| AudioComponentDescription desc; |
| #if COREAUDIO_MAC |
| unsigned dev_count; |
| AudioObjectPropertyAddress addr; |
| AudioDeviceID *dev_ids; |
| UInt32 buf_size, dev_size, size = sizeof(AudioDeviceID); |
| AudioBufferList *buf = NULL; |
| OSStatus ostatus; |
| #endif |
| |
| desc.componentType = kAudioUnitType_Output; |
| #if COREAUDIO_MAC |
| desc.componentSubType = kAudioUnitSubType_HALOutput; |
| #else |
| desc.componentSubType = kAudioUnitSubType_VoiceProcessingIO; |
| #endif |
| desc.componentManufacturer = kAudioUnitManufacturer_Apple; |
| desc.componentFlags = 0; |
| desc.componentFlagsMask = 0; |
| |
| cf->io_comp = AudioComponentFindNext(NULL, &desc); |
| if (cf->io_comp == NULL) |
| return PJMEDIA_EAUD_INIT; // cannot find IO unit; |
| |
| cf->stream = NULL; |
| |
| #if COREAUDIO_MAC |
| /* Find out how many audio devices there are */ |
| addr.mSelector = kAudioHardwarePropertyDevices; |
| addr.mScope = kAudioObjectPropertyScopeGlobal; |
| addr.mElement = kAudioObjectPropertyElementMaster; |
| ostatus = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &addr, |
| 0, NULL, &dev_size); |
| if (ostatus != noErr) { |
| dev_size = 0; |
| } |
| |
| /* Calculate the number of audio devices available */ |
| dev_count = dev_size / size; |
| if (dev_count==0) { |
| PJ_LOG(4,(THIS_FILE, "core audio found no sound devices")); |
| /* Enabling this will cause pjsua-lib initialization to fail when |
| * there is no sound device installed in the system, even when pjsua |
| * has been run with --null-audio. Moreover, it might be better to |
| * think that the core audio backend initialization is successful, |
| * regardless there is no audio device installed, as later application |
| * can check it using get_dev_count(). |
| return PJMEDIA_EAUD_NODEV; |
| */ |
| return PJ_SUCCESS; |
| } |
| PJ_LOG(4, (THIS_FILE, "core audio initialized with %d devices", |
| dev_count)); |
| |
| /* Get all the audio device IDs */ |
| dev_ids = (AudioDeviceID *)pj_pool_calloc(cf->pool, dev_size, size); |
| if (!dev_ids) |
| return PJ_ENOMEM; |
| pj_bzero(dev_ids, dev_count); |
| ostatus = AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, |
| 0, NULL, |
| &dev_size, (void *)dev_ids); |
| if (ostatus != noErr ) { |
| /* This should not happen since we have successfully retrieved |
| * the property data size before |
| */ |
| return PJMEDIA_EAUD_INIT; |
| } |
| |
| /* Build the devices' info */ |
| cf->dev_info = (struct coreaudio_dev_info*) |
| pj_pool_calloc(cf->pool, dev_count, |
| sizeof(struct coreaudio_dev_info)); |
| buf_size = 0; |
| for (i = 0; i < dev_count; i++) { |
| struct coreaudio_dev_info *cdi; |
| Float64 sampleRate; |
| |
| cdi = &cf->dev_info[i]; |
| pj_bzero(cdi, sizeof(*cdi)); |
| cdi->dev_id = dev_ids[i]; |
| |
| /* Get device name */ |
| addr.mSelector = kAudioDevicePropertyDeviceName; |
| addr.mScope = kAudioObjectPropertyScopeGlobal; |
| addr.mElement = kAudioObjectPropertyElementMaster; |
| size = sizeof(cdi->info.name); |
| AudioObjectGetPropertyData(cdi->dev_id, &addr, |
| 0, NULL, |
| &size, (void *)cdi->info.name); |
| |
| strcpy(cdi->info.driver, "core audio"); |
| |
| /* Get the number of input channels */ |
| addr.mSelector = kAudioDevicePropertyStreamConfiguration; |
| addr.mScope = kAudioDevicePropertyScopeInput; |
| size = 0; |
| ostatus = AudioObjectGetPropertyDataSize(cdi->dev_id, &addr, |
| 0, NULL, &size); |
| if (ostatus == noErr && size > 0) { |
| |
| if (size > buf_size) { |
| buf = pj_pool_alloc(cf->pool, size); |
| buf_size = size; |
| } |
| if (buf) { |
| UInt32 idx; |
| |
| /* Get the input stream configuration */ |
| ostatus = AudioObjectGetPropertyData(cdi->dev_id, &addr, |
| 0, NULL, |
| &size, buf); |
| if (ostatus == noErr) { |
| /* Count the total number of input channels in |
| * the stream |
| */ |
| for (idx = 0; idx < buf->mNumberBuffers; idx++) { |
| cdi->info.input_count += |
| buf->mBuffers[idx].mNumberChannels; |
| } |
| } |
| } |
| } |
| |
| /* Get the number of output channels */ |
| addr.mScope = kAudioDevicePropertyScopeOutput; |
| size = 0; |
| ostatus = AudioObjectGetPropertyDataSize(cdi->dev_id, &addr, |
| 0, NULL, &size); |
| if (ostatus == noErr && size > 0) { |
| |
| if (size > buf_size) { |
| buf = pj_pool_alloc(cf->pool, size); |
| buf_size = size; |
| } |
| if (buf) { |
| UInt32 idx; |
| |
| /* Get the output stream configuration */ |
| ostatus = AudioObjectGetPropertyData(cdi->dev_id, &addr, |
| 0, NULL, |
| &size, buf); |
| if (ostatus == noErr) { |
| /* Count the total number of output channels in |
| * the stream |
| */ |
| for (idx = 0; idx < buf->mNumberBuffers; idx++) { |
| cdi->info.output_count += |
| buf->mBuffers[idx].mNumberChannels; |
| } |
| } |
| } |
| } |
| |
| /* Get default sample rate */ |
| addr.mSelector = kAudioDevicePropertyNominalSampleRate; |
| addr.mScope = kAudioObjectPropertyScopeGlobal; |
| size = sizeof(Float64); |
| ostatus = AudioObjectGetPropertyData (cdi->dev_id, &addr, |
| 0, NULL, |
| &size, &sampleRate); |
| cdi->info.default_samples_per_sec = (ostatus == noErr ? |
| sampleRate: |
| 16000); |
| |
| /* Set device capabilities here */ |
| if (cdi->info.input_count > 0) { |
| cdi->info.caps |= PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY; |
| } |
| if (cdi->info.output_count > 0) { |
| cdi->info.caps |= PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY; |
| addr.mSelector = kAudioDevicePropertyVolumeScalar; |
| addr.mScope = kAudioDevicePropertyScopeOutput; |
| if (AudioObjectHasProperty(cdi->dev_id, &addr)) { |
| cdi->info.caps |= PJMEDIA_AUD_DEV_CAP_OUTPUT_VOLUME_SETTING; |
| } |
| } |
| |
| cf->dev_count++; |
| |
| PJ_LOG(4, (THIS_FILE, " dev_id %d: %s (in=%d, out=%d) %dHz", |
| i, |
| cdi->info.name, |
| cdi->info.input_count, |
| cdi->info.output_count, |
| cdi->info.default_samples_per_sec)); |
| } |
| #else |
| cf->dev_count = 1; |
| cf->dev_info = (struct coreaudio_dev_info*) |
| pj_pool_calloc(cf->pool, cf->dev_count, |
| sizeof(struct coreaudio_dev_info)); |
| for (i = 0; i < cf->dev_count; i++) { |
| struct coreaudio_dev_info *cdi; |
| |
| cdi = &cf->dev_info[i]; |
| pj_bzero(cdi, sizeof(*cdi)); |
| cdi->dev_id = 0; |
| strcpy(cdi->info.name, "iPhone IO device"); |
| strcpy(cdi->info.driver, "apple"); |
| cdi->info.input_count = 1; |
| cdi->info.output_count = 1; |
| cdi->info.default_samples_per_sec = 8000; |
| |
| /* Set the device capabilities here */ |
| cdi->info.caps = PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY | |
| PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY | |
| PJMEDIA_AUD_DEV_CAP_OUTPUT_VOLUME_SETTING | |
| PJMEDIA_AUD_DEV_CAP_INPUT_ROUTE | |
| PJMEDIA_AUD_DEV_CAP_OUTPUT_ROUTE | |
| PJMEDIA_AUD_DEV_CAP_EC; |
| cdi->info.routes = PJMEDIA_AUD_DEV_ROUTE_LOUDSPEAKER | |
| PJMEDIA_AUD_DEV_ROUTE_EARPIECE | |
| PJMEDIA_AUD_DEV_ROUTE_BLUETOOTH; |
| } |
| |
| if (AudioSessionInitialize(NULL, NULL, interruptionListener, cf) != |
| kAudioSessionNoError) |
| { |
| PJ_LOG(4, (THIS_FILE, |
| "Warning: cannot initialize audio session services")); |
| } |
| |
| PJ_LOG(4, (THIS_FILE, "core audio initialized")); |
| |
| #endif |
| |
| return PJ_SUCCESS; |
| } |
| |
| /* API: destroy factory */ |
| static pj_status_t ca_factory_destroy(pjmedia_aud_dev_factory *f) |
| { |
| struct coreaudio_factory *cf = (struct coreaudio_factory*)f; |
| pj_pool_t *pool = cf->pool; |
| |
| cf->pool = NULL; |
| pj_pool_release(pool); |
| |
| return PJ_SUCCESS; |
| } |
| |
| /* API: get number of devices */ |
| static unsigned ca_factory_get_dev_count(pjmedia_aud_dev_factory *f) |
| { |
| struct coreaudio_factory *cf = (struct coreaudio_factory*)f; |
| return cf->dev_count; |
| } |
| |
| /* API: get device info */ |
| static pj_status_t ca_factory_get_dev_info(pjmedia_aud_dev_factory *f, |
| unsigned index, |
| pjmedia_aud_dev_info *info) |
| { |
| struct coreaudio_factory *cf = (struct coreaudio_factory*)f; |
| |
| PJ_ASSERT_RETURN(index < cf->dev_count, PJMEDIA_EAUD_INVDEV); |
| |
| pj_memcpy(info, &cf->dev_info[index].info, sizeof(*info)); |
| |
| return PJ_SUCCESS; |
| } |
| |
| /* API: create default device parameter */ |
| static pj_status_t ca_factory_default_param(pjmedia_aud_dev_factory *f, |
| unsigned index, |
| pjmedia_aud_param *param) |
| { |
| struct coreaudio_factory *cf = (struct coreaudio_factory*)f; |
| struct coreaudio_dev_info *di = &cf->dev_info[index]; |
| |
| PJ_ASSERT_RETURN(index < cf->dev_count, PJMEDIA_EAUD_INVDEV); |
| |
| pj_bzero(param, sizeof(*param)); |
| if (di->info.input_count && di->info.output_count) { |
| param->dir = PJMEDIA_DIR_CAPTURE_PLAYBACK; |
| param->rec_id = index; |
| param->play_id = index; |
| } else if (di->info.input_count) { |
| param->dir = PJMEDIA_DIR_CAPTURE; |
| param->rec_id = index; |
| param->play_id = PJMEDIA_AUD_INVALID_DEV; |
| } else if (di->info.output_count) { |
| param->dir = PJMEDIA_DIR_PLAYBACK; |
| param->play_id = index; |
| param->rec_id = PJMEDIA_AUD_INVALID_DEV; |
| } else { |
| return PJMEDIA_EAUD_INVDEV; |
| } |
| |
| /* Set the mandatory settings here */ |
| param->clock_rate = di->info.default_samples_per_sec; |
| param->channel_count = 1; |
| param->samples_per_frame = di->info.default_samples_per_sec * 20 / 1000; |
| param->bits_per_sample = 16; |
| |
| /* Set the param for device capabilities here */ |
| param->flags = PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY | |
| PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY; |
| param->input_latency_ms = PJMEDIA_SND_DEFAULT_REC_LATENCY; |
| param->output_latency_ms = PJMEDIA_SND_DEFAULT_PLAY_LATENCY; |
| |
| return PJ_SUCCESS; |
| } |
| |
| static OSStatus input_callback(void *inRefCon, |
| AudioUnitRenderActionFlags *ioActionFlags, |
| const AudioTimeStamp *inTimeStamp, |
| UInt32 inBusNumber, |
| UInt32 inNumberFrames, |
| AudioBufferList *ioData) |
| { |
| struct coreaudio_stream *strm = (struct coreaudio_stream*)inRefCon; |
| OSStatus ostatus; |
| pj_status_t status = 0; |
| unsigned nsamples; |
| AudioBufferList *buf = strm->audio_buf; |
| pj_int16_t *input; |
| |
| if (strm->quit_flag) |
| goto on_break; |
| |
| /* Known cases of callback's thread: |
| * - The thread may be changed in the middle of a session |
| * it happens when plugging/unplugging headphone. |
| * - The same thread may be reused in consecutive sessions. The first |
| * session will leave TLS set, but release the TLS data address, |
| * so the second session must re-register the callback's thread. |
| */ |
| if (strm->rec_thread_initialized == 0 || !pj_thread_is_registered()) |
| { |
| status = pj_thread_register("ca_rec", strm->rec_thread_desc, |
| &strm->rec_thread); |
| strm->rec_thread_initialized = 1; |
| PJ_LOG(5,(THIS_FILE, "Recorder thread started")); |
| } |
| |
| buf->mBuffers[0].mData = NULL; |
| buf->mBuffers[0].mDataByteSize = inNumberFrames * |
| strm->streamFormat.mChannelsPerFrame; |
| /* Render the unit to get input data */ |
| ostatus = AudioUnitRender(strm->io_units[0], |
| ioActionFlags, |
| inTimeStamp, |
| inBusNumber, |
| inNumberFrames, |
| buf); |
| |
| if (ostatus != noErr) { |
| PJ_LOG(5, (THIS_FILE, "Core audio unit render error %i", ostatus)); |
| goto on_break; |
| } |
| input = (pj_int16_t *)buf->mBuffers[0].mData; |
| |
| /* Calculate number of samples we've got */ |
| nsamples = inNumberFrames * strm->param.channel_count + strm->rec_buf_count; |
| if (nsamples >= strm->param.samples_per_frame) |
| { |
| pjmedia_frame frame; |
| |
| frame.type = PJMEDIA_FRAME_TYPE_AUDIO; |
| frame.size = strm->param.samples_per_frame * |
| strm->param.bits_per_sample >> 3; |
| frame.bit_info = 0; |
| |
| /* If buffer is not empty, combine the buffer with the just incoming |
| * samples, then call put_frame. |
| */ |
| if (strm->rec_buf_count) { |
| unsigned chunk_count = 0; |
| |
| chunk_count = strm->param.samples_per_frame - strm->rec_buf_count; |
| pjmedia_copy_samples(strm->rec_buf + strm->rec_buf_count, |
| input, chunk_count); |
| |
| frame.buf = (void*) strm->rec_buf; |
| frame.timestamp.u64 = strm->rec_timestamp.u64; |
| |
| status = (*strm->rec_cb)(strm->user_data, &frame); |
| |
| input = input + chunk_count; |
| nsamples -= strm->param.samples_per_frame; |
| strm->rec_buf_count = 0; |
| strm->rec_timestamp.u64 += strm->param.samples_per_frame / |
| strm->param.channel_count; |
| } |
| |
| /* Give all frames we have */ |
| while (nsamples >= strm->param.samples_per_frame && status == 0) { |
| frame.buf = (void*) input; |
| frame.timestamp.u64 = strm->rec_timestamp.u64; |
| |
| status = (*strm->rec_cb)(strm->user_data, &frame); |
| |
| input = (pj_int16_t*) input + strm->param.samples_per_frame; |
| nsamples -= strm->param.samples_per_frame; |
| strm->rec_timestamp.u64 += strm->param.samples_per_frame / |
| strm->param.channel_count; |
| } |
| |
| /* Store the remaining samples into the buffer */ |
| if (nsamples && status == 0) { |
| strm->rec_buf_count = nsamples; |
| pjmedia_copy_samples(strm->rec_buf, input, |
| nsamples); |
| } |
| |
| } else { |
| /* Not enough samples, let's just store them in the buffer */ |
| pjmedia_copy_samples(strm->rec_buf + strm->rec_buf_count, |
| input, |
| inNumberFrames * strm->param.channel_count); |
| strm->rec_buf_count += inNumberFrames * strm->param.channel_count; |
| } |
| |
| return noErr; |
| |
| on_break: |
| strm->rec_thread_exited = 1; |
| return -1; |
| } |
| |
| static OSStatus output_renderer(void *inRefCon, |
| AudioUnitRenderActionFlags *ioActionFlags, |
| const AudioTimeStamp *inTimeStamp, |
| UInt32 inBusNumber, |
| UInt32 inNumberFrames, |
| AudioBufferList *ioData) |
| { |
| struct coreaudio_stream *stream = (struct coreaudio_stream*)inRefCon; |
| pj_status_t status = 0; |
| unsigned nsamples_req = inNumberFrames * stream->param.channel_count; |
| pj_int16_t *output = ioData->mBuffers[0].mData; |
| |
| if (stream->quit_flag) |
| goto on_break; |
| |
| /* Known cases of callback's thread: |
| * - The thread may be changed in the middle of a session |
| * it happens when plugging/unplugging headphone. |
| * - The same thread may be reused in consecutive sessions. The first |
| * session will leave TLS set, but release the TLS data address, |
| * so the second session must re-register the callback's thread. |
| */ |
| if (stream->play_thread_initialized == 0 || !pj_thread_is_registered()) |
| { |
| status = pj_thread_register("coreaudio", stream->play_thread_desc, |
| &stream->play_thread); |
| stream->play_thread_initialized = 1; |
| PJ_LOG(5,(THIS_FILE, "Player thread started")); |
| } |
| |
| |
| /* Check if any buffered samples */ |
| if (stream->play_buf_count) { |
| /* samples buffered >= requested by sound device */ |
| if (stream->play_buf_count >= nsamples_req) { |
| pjmedia_copy_samples((pj_int16_t*)output, stream->play_buf, |
| nsamples_req); |
| stream->play_buf_count -= nsamples_req; |
| pjmedia_move_samples(stream->play_buf, |
| stream->play_buf + nsamples_req, |
| stream->play_buf_count); |
| nsamples_req = 0; |
| |
| return noErr; |
| } |
| |
| /* samples buffered < requested by sound device */ |
| pjmedia_copy_samples((pj_int16_t*)output, stream->play_buf, |
| stream->play_buf_count); |
| nsamples_req -= stream->play_buf_count; |
| output = (pj_int16_t*)output + stream->play_buf_count; |
| stream->play_buf_count = 0; |
| } |
| |
| /* Fill output buffer as requested */ |
| while (nsamples_req && status == 0) { |
| pjmedia_frame frame; |
| |
| frame.type = PJMEDIA_FRAME_TYPE_AUDIO; |
| frame.size = stream->param.samples_per_frame * |
| stream->param.bits_per_sample >> 3; |
| frame.timestamp.u64 = stream->play_timestamp.u64; |
| frame.bit_info = 0; |
| |
| if (nsamples_req >= stream->param.samples_per_frame) { |
| frame.buf = output; |
| status = (*stream->play_cb)(stream->user_data, &frame); |
| if (status != PJ_SUCCESS) |
| goto on_break; |
| |
| if (frame.type != PJMEDIA_FRAME_TYPE_AUDIO) |
| pj_bzero(frame.buf, frame.size); |
| |
| nsamples_req -= stream->param.samples_per_frame; |
| output = (pj_int16_t*)output + stream->param.samples_per_frame; |
| } else { |
| frame.buf = stream->play_buf; |
| status = (*stream->play_cb)(stream->user_data, &frame); |
| if (status != PJ_SUCCESS) |
| goto on_break; |
| |
| if (frame.type != PJMEDIA_FRAME_TYPE_AUDIO) |
| pj_bzero(frame.buf, frame.size); |
| |
| pjmedia_copy_samples((pj_int16_t*)output, stream->play_buf, |
| nsamples_req); |
| stream->play_buf_count = stream->param.samples_per_frame - |
| nsamples_req; |
| pjmedia_move_samples(stream->play_buf, |
| stream->play_buf+nsamples_req, |
| stream->play_buf_count); |
| nsamples_req = 0; |
| } |
| |
| stream->play_timestamp.u64 += stream->param.samples_per_frame / |
| stream->param.channel_count; |
| } |
| |
| return noErr; |
| |
| on_break: |
| stream->play_thread_exited = 1; |
| return -1; |
| } |
| |
| #if !COREAUDIO_MAC |
| static void propListener(void *inClientData, |
| AudioSessionPropertyID inID, |
| UInt32 inDataSize, |
| const void * inData) |
| { |
| struct coreaudio_stream *strm = (struct coreaudio_stream*)inClientData; |
| |
| if (inID == kAudioSessionProperty_AudioRouteChange) { |
| |
| PJ_LOG(3, (THIS_FILE, "audio route changed")); |
| if (strm->interrupted) |
| return; |
| |
| ca_stream_stop((pjmedia_aud_stream *)strm); |
| AudioUnitUninitialize(strm->io_units[0]); |
| AudioComponentInstanceDispose(strm->io_units[0]); |
| |
| if (create_audio_unit(strm->cf->io_comp, 0, |
| strm->param.dir, strm, |
| &strm->io_units[0]) != PJ_SUCCESS) |
| { |
| PJ_LOG(3, (THIS_FILE, |
| "Error: failed to create a new instance of audio unit")); |
| return; |
| } |
| if (ca_stream_start((pjmedia_aud_stream *)strm) != PJ_SUCCESS) { |
| PJ_LOG(3, (THIS_FILE, |
| "Error: failed to restart audio unit")); |
| } |
| PJ_LOG(3, (THIS_FILE, "core audio unit successfully reinstantiated")); |
| } |
| } |
| |
| static void interruptionListener(void *inClientData, UInt32 inInterruption) |
| { |
| struct coreaudio_stream *strm = ((struct coreaudio_factory*)inClientData)-> |
| stream; |
| if (!strm) |
| return; |
| |
| PJ_LOG(3, (THIS_FILE, "Session interrupted! --- %s ---", |
| inInterruption == kAudioSessionBeginInterruption ? |
| "Begin Interruption" : "End Interruption")); |
| |
| if (inInterruption == kAudioSessionEndInterruption) { |
| strm->interrupted = PJ_FALSE; |
| /* There may be an audio route change during the interruption |
| * (such as when the alarm rings), so we have to notify the |
| * listener as well. |
| */ |
| propListener(strm, kAudioSessionProperty_AudioRouteChange, |
| 0, NULL); |
| } else if (inInterruption == kAudioSessionBeginInterruption) { |
| strm->interrupted = PJ_TRUE; |
| AudioOutputUnitStop(strm->io_units[0]); |
| } |
| } |
| |
| #endif |
| |
| /* Internal: create audio unit for recorder/playback device */ |
| static pj_status_t create_audio_unit(AudioComponent io_comp, |
| AudioDeviceID dev_id, |
| pjmedia_dir dir, |
| struct coreaudio_stream *strm, |
| AudioUnit *io_unit) |
| { |
| OSStatus ostatus; |
| #if !COREAUDIO_MAC |
| UInt32 audioCategory = kAudioSessionCategory_PlayAndRecord; |
| if (!(dir & PJMEDIA_DIR_CAPTURE)) { |
| audioCategory = kAudioSessionCategory_MediaPlayback; |
| } else if (!(dir & PJMEDIA_DIR_PLAYBACK)) { |
| audioCategory = kAudioSessionCategory_RecordAudio; |
| } |
| AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, |
| sizeof(audioCategory), &audioCategory); |
| #endif |
| |
| /* Create an audio unit to interface with the device */ |
| ostatus = AudioComponentInstanceNew(io_comp, io_unit); |
| if (ostatus != noErr) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| |
| /* Set audio unit's properties for capture device */ |
| if (dir & PJMEDIA_DIR_CAPTURE) { |
| UInt32 enable = 1; |
| |
| /* Enable input */ |
| ostatus = AudioUnitSetProperty(*io_unit, |
| kAudioOutputUnitProperty_EnableIO, |
| kAudioUnitScope_Input, |
| 1, |
| &enable, |
| sizeof(enable)); |
| if (ostatus != noErr) { |
| PJ_LOG(4, (THIS_FILE, |
| "Warning: cannot enable IO of capture device %d", |
| dev_id)); |
| } |
| |
| /* Disable output */ |
| if (!(dir & PJMEDIA_DIR_PLAYBACK)) { |
| enable = 0; |
| ostatus = AudioUnitSetProperty(*io_unit, |
| kAudioOutputUnitProperty_EnableIO, |
| kAudioUnitScope_Output, |
| 0, |
| &enable, |
| sizeof(enable)); |
| if (ostatus != noErr) { |
| PJ_LOG(4, (THIS_FILE, |
| "Warning: cannot disable IO of capture device %d", |
| dev_id)); |
| } |
| } |
| } |
| |
| /* Set audio unit's properties for playback device */ |
| if (dir & PJMEDIA_DIR_PLAYBACK) { |
| UInt32 enable = 1; |
| |
| /* Enable output */ |
| ostatus = AudioUnitSetProperty(*io_unit, |
| kAudioOutputUnitProperty_EnableIO, |
| kAudioUnitScope_Output, |
| 0, |
| &enable, |
| sizeof(enable)); |
| if (ostatus != noErr) { |
| PJ_LOG(4, (THIS_FILE, |
| "Warning: cannot enable IO of playback device %d", |
| dev_id)); |
| } |
| |
| } |
| |
| #if COREAUDIO_MAC |
| PJ_LOG(5, (THIS_FILE, "Opening device %d", dev_id)); |
| ostatus = AudioUnitSetProperty(*io_unit, |
| kAudioOutputUnitProperty_CurrentDevice, |
| kAudioUnitScope_Global, |
| 0, |
| &dev_id, |
| sizeof(dev_id)); |
| if (ostatus != noErr) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| #endif |
| |
| if (dir & PJMEDIA_DIR_CAPTURE) { |
| #if COREAUDIO_MAC |
| AudioStreamBasicDescription deviceFormat; |
| UInt32 size; |
| #endif |
| |
| /* When setting the stream format, we have to make sure the sample |
| * rate is supported. Setting an unsupported sample rate will cause |
| * AudioUnitRender() to fail later. |
| */ |
| ostatus = AudioUnitSetProperty(*io_unit, |
| kAudioUnitProperty_StreamFormat, |
| kAudioUnitScope_Output, |
| 1, |
| &strm->streamFormat, |
| sizeof(strm->streamFormat)); |
| if (ostatus != noErr) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| |
| #if COREAUDIO_MAC |
| size = sizeof(AudioStreamBasicDescription); |
| ostatus = AudioUnitGetProperty (*io_unit, |
| kAudioUnitProperty_StreamFormat, |
| kAudioUnitScope_Input, |
| 1, |
| &deviceFormat, |
| &size); |
| if (ostatus == noErr) { |
| if (strm->streamFormat.mSampleRate != deviceFormat.mSampleRate) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| } else { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| #endif |
| } |
| |
| if (dir & PJMEDIA_DIR_PLAYBACK) { |
| AURenderCallbackStruct output_cb; |
| |
| /* Set the stream format */ |
| ostatus = AudioUnitSetProperty(*io_unit, |
| kAudioUnitProperty_StreamFormat, |
| kAudioUnitScope_Input, |
| 0, |
| &strm->streamFormat, |
| sizeof(strm->streamFormat)); |
| if (ostatus != noErr) { |
| PJ_LOG(4, (THIS_FILE, |
| "Warning: cannot set playback stream format of dev %d", |
| dev_id)); |
| } |
| |
| /* Set render callback */ |
| output_cb.inputProc = output_renderer; |
| output_cb.inputProcRefCon = strm; |
| ostatus = AudioUnitSetProperty(*io_unit, |
| kAudioUnitProperty_SetRenderCallback, |
| kAudioUnitScope_Input, |
| 0, |
| &output_cb, |
| sizeof(output_cb)); |
| if (ostatus != noErr) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| |
| /* Allocate playback buffer */ |
| strm->play_buf = (pj_int16_t*)pj_pool_alloc(strm->pool, |
| strm->param.samples_per_frame * |
| strm->param.bits_per_sample >> 3); |
| if (!strm->play_buf) |
| return PJ_ENOMEM; |
| strm->play_buf_count = 0; |
| } |
| |
| if (dir & PJMEDIA_DIR_CAPTURE) { |
| AURenderCallbackStruct input_cb; |
| #if COREAUDIO_MAC |
| AudioBuffer *ab; |
| UInt32 size, buf_size; |
| #endif |
| |
| /* Set input callback */ |
| input_cb.inputProc = input_callback; |
| input_cb.inputProcRefCon = strm; |
| ostatus = AudioUnitSetProperty(*io_unit, |
| kAudioOutputUnitProperty_SetInputCallback, |
| kAudioUnitScope_Global, |
| 0, |
| &input_cb, |
| sizeof(input_cb)); |
| if (ostatus != noErr) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| |
| #if COREAUDIO_MAC |
| /* Get device's buffer frame size */ |
| size = sizeof(UInt32); |
| ostatus = AudioUnitGetProperty(*io_unit, |
| kAudioDevicePropertyBufferFrameSize, |
| kAudioUnitScope_Global, |
| 0, |
| &buf_size, |
| &size); |
| if (ostatus != noErr) |
| { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| |
| /* Allocate audio buffer */ |
| strm->audio_buf = (AudioBufferList*)pj_pool_alloc(strm->pool, |
| sizeof(AudioBufferList) + sizeof(AudioBuffer)); |
| if (!strm->audio_buf) |
| return PJ_ENOMEM; |
| |
| strm->audio_buf->mNumberBuffers = 1; |
| ab = &strm->audio_buf->mBuffers[0]; |
| ab->mNumberChannels = strm->streamFormat.mChannelsPerFrame; |
| ab->mDataByteSize = buf_size * ab->mNumberChannels * |
| strm->param.bits_per_sample >> 3; |
| ab->mData = pj_pool_alloc(strm->pool, |
| ab->mDataByteSize); |
| if (!ab->mData) |
| return PJ_ENOMEM; |
| |
| #else |
| /* We will let AudioUnitRender() to allocate the buffer |
| * for us later |
| */ |
| strm->audio_buf = (AudioBufferList*)pj_pool_alloc(strm->pool, |
| sizeof(AudioBufferList) + sizeof(AudioBuffer)); |
| if (!strm->audio_buf) |
| return PJ_ENOMEM; |
| |
| strm->audio_buf->mNumberBuffers = 1; |
| strm->audio_buf->mBuffers[0].mNumberChannels = |
| strm->streamFormat.mChannelsPerFrame; |
| #endif |
| |
| /* Allocate recording buffer */ |
| strm->rec_buf = (pj_int16_t*)pj_pool_alloc(strm->pool, |
| strm->param.samples_per_frame * |
| strm->param.bits_per_sample >> 3); |
| if (!strm->rec_buf) |
| return PJ_ENOMEM; |
| strm->rec_buf_count = 0; |
| } |
| |
| /* Initialize the audio unit */ |
| ostatus = AudioUnitInitialize(*io_unit); |
| if (ostatus != noErr) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| |
| return PJ_SUCCESS; |
| } |
| |
| /* API: create stream */ |
| static pj_status_t ca_factory_create_stream(pjmedia_aud_dev_factory *f, |
| const pjmedia_aud_param *param, |
| pjmedia_aud_rec_cb rec_cb, |
| pjmedia_aud_play_cb play_cb, |
| void *user_data, |
| pjmedia_aud_stream **p_aud_strm) |
| { |
| struct coreaudio_factory *cf = (struct coreaudio_factory*)f; |
| pj_pool_t *pool; |
| struct coreaudio_stream *strm; |
| pj_status_t status; |
| |
| /* Create and Initialize stream descriptor */ |
| pool = pj_pool_create(cf->pf, "coreaudio-dev", 1000, 1000, NULL); |
| PJ_ASSERT_RETURN(pool != NULL, PJ_ENOMEM); |
| |
| strm = PJ_POOL_ZALLOC_T(pool, struct coreaudio_stream); |
| cf->stream = strm; |
| strm->cf = cf; |
| pj_memcpy(&strm->param, param, sizeof(*param)); |
| strm->pool = pool; |
| strm->rec_cb = rec_cb; |
| strm->play_cb = play_cb; |
| strm->user_data = user_data; |
| |
| /* Set the stream format */ |
| strm->streamFormat.mSampleRate = param->clock_rate; |
| strm->streamFormat.mFormatID = kAudioFormatLinearPCM; |
| strm->streamFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger |
| | kLinearPCMFormatFlagIsPacked; |
| strm->streamFormat.mBitsPerChannel = strm->param.bits_per_sample; |
| strm->streamFormat.mChannelsPerFrame = param->channel_count; |
| strm->streamFormat.mBytesPerFrame = strm->streamFormat.mChannelsPerFrame |
| * strm->param.bits_per_sample >> 3; |
| strm->streamFormat.mFramesPerPacket = 1; |
| strm->streamFormat.mBytesPerPacket = strm->streamFormat.mBytesPerFrame * |
| strm->streamFormat.mFramesPerPacket; |
| |
| /* Apply input/output routes settings before we create the audio units */ |
| if (param->flags & PJMEDIA_AUD_DEV_CAP_INPUT_ROUTE) { |
| ca_stream_set_cap(&strm->base, |
| PJMEDIA_AUD_DEV_CAP_INPUT_ROUTE, |
| ¶m->input_route); |
| } |
| if (param->flags & PJMEDIA_AUD_DEV_CAP_OUTPUT_ROUTE) { |
| ca_stream_set_cap(&strm->base, |
| PJMEDIA_AUD_DEV_CAP_OUTPUT_ROUTE, |
| ¶m->output_route); |
| } |
| if (param->flags & PJMEDIA_AUD_DEV_CAP_EC) { |
| ca_stream_set_cap(&strm->base, |
| PJMEDIA_AUD_DEV_CAP_EC, |
| ¶m->ec_enabled); |
| } |
| |
| strm->io_units[0] = strm->io_units[1] = NULL; |
| if (param->dir == PJMEDIA_DIR_CAPTURE_PLAYBACK && |
| param->rec_id == param->play_id) |
| { |
| /* If both input and output are on the same device, only create |
| * one audio unit to interface with the device. |
| */ |
| status = create_audio_unit(cf->io_comp, |
| cf->dev_info[param->rec_id].dev_id, |
| param->dir, strm, &strm->io_units[0]); |
| if (status != PJ_SUCCESS) |
| goto on_error; |
| } else { |
| unsigned nunits = 0; |
| |
| if (param->dir & PJMEDIA_DIR_CAPTURE) { |
| status = create_audio_unit(cf->io_comp, |
| cf->dev_info[param->rec_id].dev_id, |
| PJMEDIA_DIR_CAPTURE, |
| strm, &strm->io_units[nunits++]); |
| if (status != PJ_SUCCESS) |
| goto on_error; |
| } |
| if (param->dir & PJMEDIA_DIR_PLAYBACK) { |
| |
| status = create_audio_unit(cf->io_comp, |
| cf->dev_info[param->play_id].dev_id, |
| PJMEDIA_DIR_PLAYBACK, |
| strm, &strm->io_units[nunits++]); |
| if (status != PJ_SUCCESS) |
| goto on_error; |
| } |
| } |
| |
| /* Apply the remaining settings */ |
| if (param->flags & PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY) { |
| ca_stream_get_cap(&strm->base, |
| PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY, |
| &strm->param.input_latency_ms); |
| } |
| if (param->flags & PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY) { |
| ca_stream_get_cap(&strm->base, |
| PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY, |
| &strm->param.output_latency_ms); |
| } |
| if (param->flags & PJMEDIA_AUD_DEV_CAP_OUTPUT_VOLUME_SETTING) { |
| ca_stream_set_cap(&strm->base, |
| PJMEDIA_AUD_DEV_CAP_OUTPUT_VOLUME_SETTING, |
| ¶m->output_vol); |
| } |
| |
| /* Done */ |
| strm->base.op = &stream_op; |
| *p_aud_strm = &strm->base; |
| |
| return PJ_SUCCESS; |
| |
| on_error: |
| ca_stream_destroy((pjmedia_aud_stream *)strm); |
| return status; |
| } |
| |
| /* API: Get stream info. */ |
| static pj_status_t ca_stream_get_param(pjmedia_aud_stream *s, |
| pjmedia_aud_param *pi) |
| { |
| struct coreaudio_stream *strm = (struct coreaudio_stream*)s; |
| |
| PJ_ASSERT_RETURN(strm && pi, PJ_EINVAL); |
| |
| pj_memcpy(pi, &strm->param, sizeof(*pi)); |
| |
| /* Update the device capabilities' values */ |
| if (ca_stream_get_cap(s, PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY, |
| &pi->input_latency_ms) == PJ_SUCCESS) |
| { |
| pi->flags |= PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY; |
| } |
| if (ca_stream_get_cap(s, PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY, |
| &pi->output_latency_ms) == PJ_SUCCESS) |
| { |
| pi->flags |= PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY; |
| } |
| if (ca_stream_get_cap(s, PJMEDIA_AUD_DEV_CAP_OUTPUT_VOLUME_SETTING, |
| &pi->output_vol) == PJ_SUCCESS) |
| { |
| pi->flags |= PJMEDIA_AUD_DEV_CAP_OUTPUT_VOLUME_SETTING; |
| } |
| if (ca_stream_get_cap(s, PJMEDIA_AUD_DEV_CAP_INPUT_ROUTE, |
| &pi->input_route) == PJ_SUCCESS) |
| { |
| pi->flags |= PJMEDIA_AUD_DEV_CAP_INPUT_ROUTE; |
| } |
| if (ca_stream_get_cap(s, PJMEDIA_AUD_DEV_CAP_OUTPUT_ROUTE, |
| &pi->output_route) == PJ_SUCCESS) |
| { |
| pi->flags |= PJMEDIA_AUD_DEV_CAP_OUTPUT_ROUTE; |
| } |
| if (ca_stream_get_cap(s, PJMEDIA_AUD_DEV_CAP_EC, |
| &pi->ec_enabled) == PJ_SUCCESS) |
| { |
| pi->flags |= PJMEDIA_AUD_DEV_CAP_EC; |
| } |
| |
| return PJ_SUCCESS; |
| } |
| |
| /* API: get capability */ |
| static pj_status_t ca_stream_get_cap(pjmedia_aud_stream *s, |
| pjmedia_aud_dev_cap cap, |
| void *pval) |
| { |
| struct coreaudio_stream *strm = (struct coreaudio_stream*)s; |
| |
| PJ_UNUSED_ARG(strm); |
| |
| PJ_ASSERT_RETURN(s && pval, PJ_EINVAL); |
| |
| if (cap==PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY && |
| (strm->param.dir & PJMEDIA_DIR_CAPTURE)) |
| { |
| #if COREAUDIO_MAC |
| UInt32 latency, size = sizeof(UInt32); |
| |
| /* Recording latency */ |
| if (AudioUnitGetProperty (strm->io_units[0], |
| kAudioDevicePropertyLatency, |
| kAudioUnitScope_Input, |
| 1, |
| &latency, |
| &size) == noErr) |
| { |
| UInt32 latency2; |
| if (AudioUnitGetProperty (strm->io_units[0], |
| kAudioDevicePropertyBufferFrameSize, |
| kAudioUnitScope_Input, |
| 1, |
| &latency2, |
| &size) == noErr) |
| { |
| strm->param.input_latency_ms = (latency + latency2) * 1000 / |
| strm->param.clock_rate; |
| strm->param.input_latency_ms++; |
| } |
| } |
| #else |
| Float32 latency, latency2; |
| UInt32 size = sizeof(Float32); |
| |
| if ((AudioSessionGetProperty( |
| kAudioSessionProperty_CurrentHardwareInputLatency, |
| &size, &latency) == kAudioSessionNoError) && |
| (AudioSessionGetProperty( |
| kAudioSessionProperty_CurrentHardwareIOBufferDuration, |
| &size, &latency2) == kAudioSessionNoError)) |
| { |
| strm->param.input_latency_ms = (unsigned)((latency + latency2) * 1000); |
| strm->param.input_latency_ms++; |
| } |
| #endif |
| |
| *(unsigned*)pval = strm->param.input_latency_ms; |
| return PJ_SUCCESS; |
| } else if (cap==PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY && |
| (strm->param.dir & PJMEDIA_DIR_PLAYBACK)) |
| { |
| #if COREAUDIO_MAC |
| UInt32 latency, size = sizeof(UInt32); |
| AudioUnit *io_unit = strm->io_units[1] ? &strm->io_units[1] : |
| &strm->io_units[0]; |
| |
| /* Playback latency */ |
| if (AudioUnitGetProperty (*io_unit, |
| kAudioDevicePropertyLatency, |
| kAudioUnitScope_Output, |
| 0, |
| &latency, |
| &size) == noErr) |
| { |
| UInt32 latency2; |
| if (AudioUnitGetProperty (*io_unit, |
| kAudioDevicePropertyBufferFrameSize, |
| kAudioUnitScope_Output, |
| 0, |
| &latency2, |
| &size) == noErr) |
| { |
| strm->param.output_latency_ms = (latency + latency2) * 1000 / |
| strm->param.clock_rate; |
| strm->param.output_latency_ms++; |
| } |
| } |
| #else |
| Float32 latency, latency2; |
| UInt32 size = sizeof(Float32); |
| |
| if ((AudioSessionGetProperty( |
| kAudioSessionProperty_CurrentHardwareOutputLatency, |
| &size, &latency) == kAudioSessionNoError) && |
| (AudioSessionGetProperty( |
| kAudioSessionProperty_CurrentHardwareIOBufferDuration, |
| &size, &latency2) == kAudioSessionNoError)) |
| { |
| strm->param.output_latency_ms = (unsigned)((latency + latency2) * 1000); |
| strm->param.output_latency_ms++; |
| } |
| #endif |
| *(unsigned*)pval = (++strm->param.output_latency_ms * 2); |
| return PJ_SUCCESS; |
| } else if (cap==PJMEDIA_AUD_DEV_CAP_OUTPUT_VOLUME_SETTING && |
| (strm->param.dir & PJMEDIA_DIR_PLAYBACK)) |
| { |
| OSStatus ostatus; |
| Float32 volume; |
| UInt32 size = sizeof(Float32); |
| |
| /* Output volume setting */ |
| #if COREAUDIO_MAC |
| ostatus = AudioUnitGetProperty (strm->io_units[1] ? strm->io_units[1] : |
| strm->io_units[0], |
| kAudioDevicePropertyVolumeScalar, |
| kAudioUnitScope_Output, |
| 0, |
| &volume, |
| &size); |
| if (ostatus != noErr) |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| #else |
| ostatus = AudioSessionGetProperty( |
| kAudioSessionProperty_CurrentHardwareOutputVolume, |
| &size, &volume); |
| if (ostatus != kAudioSessionNoError) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| #endif |
| |
| *(unsigned*)pval = (unsigned)(volume * 100); |
| return PJ_SUCCESS; |
| #if !COREAUDIO_MAC |
| } else if (cap==PJMEDIA_AUD_DEV_CAP_INPUT_ROUTE && |
| (strm->param.dir & PJMEDIA_DIR_CAPTURE)) |
| { |
| UInt32 btooth, size = sizeof(UInt32); |
| OSStatus ostatus; |
| |
| ostatus = AudioSessionGetProperty ( |
| kAudioSessionProperty_OverrideCategoryEnableBluetoothInput, |
| &size, &btooth); |
| if (ostatus != kAudioSessionNoError) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| |
| *(pjmedia_aud_dev_route*)pval = btooth? |
| PJMEDIA_AUD_DEV_ROUTE_BLUETOOTH: |
| PJMEDIA_AUD_DEV_ROUTE_DEFAULT; |
| return PJ_SUCCESS; |
| } else if (cap==PJMEDIA_AUD_DEV_CAP_OUTPUT_ROUTE && |
| (strm->param.dir & PJMEDIA_DIR_PLAYBACK)) |
| { |
| CFStringRef route; |
| UInt32 size = sizeof(CFStringRef); |
| OSStatus ostatus; |
| |
| ostatus = AudioSessionGetProperty (kAudioSessionProperty_AudioRoute, |
| &size, &route); |
| if (ostatus != kAudioSessionNoError) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| |
| if (!route) { |
| *(pjmedia_aud_dev_route*)pval = PJMEDIA_AUD_DEV_ROUTE_DEFAULT; |
| } else if (CFStringHasPrefix(route, CFSTR("Headset"))) { |
| *(pjmedia_aud_dev_route*)pval = PJMEDIA_AUD_DEV_ROUTE_EARPIECE; |
| } else { |
| *(pjmedia_aud_dev_route*)pval = PJMEDIA_AUD_DEV_ROUTE_DEFAULT; |
| } |
| |
| CFRelease(route); |
| |
| return PJ_SUCCESS; |
| } else if (cap==PJMEDIA_AUD_DEV_CAP_EC) { |
| AudioComponentDescription desc; |
| OSStatus ostatus; |
| |
| ostatus = AudioComponentGetDescription(strm->cf->io_comp, &desc); |
| if (ostatus != noErr) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| |
| *(pj_bool_t*)pval = (desc.componentSubType == |
| kAudioUnitSubType_VoiceProcessingIO); |
| return PJ_SUCCESS; |
| #endif |
| } else { |
| return PJMEDIA_EAUD_INVCAP; |
| } |
| } |
| |
| /* API: set capability */ |
| static pj_status_t ca_stream_set_cap(pjmedia_aud_stream *s, |
| pjmedia_aud_dev_cap cap, |
| const void *pval) |
| { |
| struct coreaudio_stream *strm = (struct coreaudio_stream*)s; |
| |
| PJ_UNUSED_ARG(strm); |
| |
| PJ_ASSERT_RETURN(s && pval, PJ_EINVAL); |
| |
| #if COREAUDIO_MAC |
| if (cap==PJMEDIA_AUD_DEV_CAP_OUTPUT_VOLUME_SETTING && |
| (strm->param.dir & PJMEDIA_DIR_PLAYBACK)) |
| { |
| OSStatus ostatus; |
| Float32 volume = *(unsigned*)pval; |
| |
| /* Output volume setting */ |
| volume /= 100.0; |
| ostatus = AudioUnitSetProperty (strm->io_units[1] ? strm->io_units[1] : |
| strm->io_units[0], |
| kAudioDevicePropertyVolumeScalar, |
| kAudioUnitScope_Output, |
| 0, |
| &volume, |
| sizeof(Float32)); |
| if (ostatus != noErr) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| strm->param.output_vol = *(unsigned*)pval; |
| return PJ_SUCCESS; |
| } |
| #endif |
| |
| #if !COREAUDIO_MAC |
| if (cap==PJMEDIA_AUD_DEV_CAP_INPUT_ROUTE && |
| (strm->param.dir & PJMEDIA_DIR_CAPTURE)) |
| { |
| UInt32 btooth = *(pjmedia_aud_dev_route*)pval == |
| PJMEDIA_AUD_DEV_ROUTE_BLUETOOTH ? 1 : 0; |
| OSStatus ostatus; |
| |
| ostatus = AudioSessionSetProperty ( |
| kAudioSessionProperty_OverrideCategoryEnableBluetoothInput, |
| sizeof(btooth), &btooth); |
| if (ostatus != kAudioSessionNoError) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| strm->param.input_route = *(pjmedia_aud_dev_route*)pval; |
| return PJ_SUCCESS; |
| } else if (cap==PJMEDIA_AUD_DEV_CAP_OUTPUT_ROUTE && |
| (strm->param.dir & PJMEDIA_DIR_PLAYBACK)) |
| { |
| OSStatus ostatus; |
| UInt32 route = *(pjmedia_aud_dev_route*)pval == |
| PJMEDIA_AUD_DEV_ROUTE_LOUDSPEAKER ? |
| kAudioSessionOverrideAudioRoute_Speaker : |
| kAudioSessionOverrideAudioRoute_None; |
| |
| ostatus = AudioSessionSetProperty ( |
| kAudioSessionProperty_OverrideAudioRoute, |
| sizeof(route), &route); |
| if (ostatus != kAudioSessionNoError) { |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| strm->param.output_route = *(pjmedia_aud_dev_route*)pval; |
| return PJ_SUCCESS; |
| } else if (cap==PJMEDIA_AUD_DEV_CAP_EC) { |
| AudioComponentDescription desc; |
| AudioComponent io_comp; |
| |
| desc.componentType = kAudioUnitType_Output; |
| desc.componentSubType = (*(pj_bool_t*)pval)? |
| kAudioUnitSubType_VoiceProcessingIO : |
| kAudioUnitSubType_RemoteIO; |
| desc.componentManufacturer = kAudioUnitManufacturer_Apple; |
| desc.componentFlags = 0; |
| desc.componentFlagsMask = 0; |
| |
| io_comp = AudioComponentFindNext(NULL, &desc); |
| if (io_comp == NULL) |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(-1); |
| strm->cf->io_comp = io_comp; |
| strm->param.ec_enabled = *(pj_bool_t*)pval; |
| |
| return PJ_SUCCESS; |
| } |
| #endif |
| |
| return PJMEDIA_EAUD_INVCAP; |
| } |
| |
| /* API: Start stream. */ |
| static pj_status_t ca_stream_start(pjmedia_aud_stream *strm) |
| { |
| struct coreaudio_stream *stream = (struct coreaudio_stream*)strm; |
| OSStatus ostatus; |
| UInt32 i; |
| |
| stream->quit_flag = 0; |
| stream->rec_thread_exited = 0; |
| stream->play_thread_exited = 0; |
| stream->interrupted = PJ_FALSE; |
| |
| for (i = 0; i < 2; i++) { |
| if (stream->io_units[i] == NULL) break; |
| ostatus = AudioOutputUnitStart(stream->io_units[i]); |
| if (ostatus != noErr) { |
| if (i == 1) |
| AudioOutputUnitStop(stream->io_units[0]); |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| } |
| |
| #if !COREAUDIO_MAC |
| AudioSessionSetActive(true); |
| |
| AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange, |
| propListener, strm); |
| #endif |
| |
| PJ_LOG(4, (THIS_FILE, "core audio stream started")); |
| |
| return PJ_SUCCESS; |
| } |
| |
| /* API: Stop stream. */ |
| static pj_status_t ca_stream_stop(pjmedia_aud_stream *strm) |
| { |
| struct coreaudio_stream *stream = (struct coreaudio_stream*)strm; |
| OSStatus ostatus; |
| unsigned i; |
| |
| stream->quit_flag = 1; |
| for (i=0; !stream->rec_thread_exited && i<100; ++i) |
| pj_thread_sleep(10); |
| for (i=0; !stream->play_thread_exited && i<100; ++i) |
| pj_thread_sleep(10); |
| |
| pj_thread_sleep(1); |
| pj_bzero(stream->rec_thread_desc, sizeof(pj_thread_desc)); |
| pj_bzero(stream->play_thread_desc, sizeof(pj_thread_desc)); |
| |
| #if !COREAUDIO_MAC |
| AudioSessionSetActive(false); |
| #endif |
| |
| for (i = 0; i < 2; i++) { |
| if (stream->io_units[i] == NULL) break; |
| ostatus = AudioOutputUnitStop(stream->io_units[i]); |
| if (ostatus != noErr) { |
| if (i == 0 && stream->io_units[1]) |
| AudioOutputUnitStop(stream->io_units[1]); |
| return PJMEDIA_AUDIODEV_ERRNO_FROM_COREAUDIO(ostatus); |
| } |
| } |
| stream->play_thread_initialized = 0; |
| stream->rec_thread_initialized = 0; |
| |
| PJ_LOG(4, (THIS_FILE, "core audio stream stopped")); |
| |
| return PJ_SUCCESS; |
| } |
| |
| |
| /* API: Destroy stream. */ |
| static pj_status_t ca_stream_destroy(pjmedia_aud_stream *strm) |
| { |
| struct coreaudio_stream *stream = (struct coreaudio_stream*)strm; |
| unsigned i; |
| |
| PJ_ASSERT_RETURN(stream != NULL, PJ_EINVAL); |
| |
| #if !COREAUDIO_MAC |
| AudioSessionRemovePropertyListenerWithUserData( |
| kAudioSessionProperty_AudioRouteChange, propListener, strm); |
| #endif |
| |
| ca_stream_stop(strm); |
| |
| for (i = 0; i < 2; i++) { |
| if (stream->io_units[i]) { |
| AudioUnitUninitialize(stream->io_units[i]); |
| AudioComponentInstanceDispose(stream->io_units[i]); |
| stream->io_units[i] = NULL; |
| } |
| } |
| |
| stream->cf->stream = NULL; |
| pj_pool_release(stream->pool); |
| |
| return PJ_SUCCESS; |
| } |
| |
| #endif /* PJMEDIA_AUDIO_DEV_HAS_COREAUDIO */ |