From 954e2f32b09d4cd821264e496d6e3a1677d20df8 Mon Sep 17 00:00:00 2001 From: Vincent Lucas <chenzo@jitsi.org> Date: Sat, 28 Sep 2013 03:02:46 +0200 Subject: [PATCH] Adds stream capture and render via CoreAudio for MacOSX. This is a first version which needs more testing. The default audio system on MacOSX is still PortAudio. --- lib/native/mac/libjnmaccoreaudio.jnilib | Bin 35600 -> 74352 bytes src/native/build.xml | 2 + .../macosx/coreaudio/jni/maccoreaudio_util.c | 250 ++++ .../macosx/coreaudio/jni/maccoreaudio_util.h | 42 + .../org_jitsi_impl_neomedia_CoreAudioDevice.c | 55 +- .../org_jitsi_impl_neomedia_CoreAudioDevice.h | 52 +- ...g_jitsi_impl_neomedia_MacCoreAudioDevice.c | 277 ++++ ...g_jitsi_impl_neomedia_MacCoreAudioDevice.h | 125 ++ src/native/macosx/coreaudio/lib/device.c | 1314 ++++++++++++++++- src/native/macosx/coreaudio/lib/device.h | 94 +- .../jitsi/impl/neomedia/CoreAudioDevice.java | 24 + .../impl/neomedia/MacCoreAudioDevice.java | 96 ++ .../impl/neomedia/device/AudioSystem.java | 6 + .../impl/neomedia/device/DeviceSystem.java | 1 + .../neomedia/device/MacCoreaudioSystem.java | 785 ++++++++++ .../protocol/maccoreaudio/DataSource.java | 251 ++++ .../maccoreaudio/MacCoreaudioStream.java | 440 ++++++ .../renderer/audio/MacCoreaudioRenderer.java | 592 ++++++++ 18 files changed, 4290 insertions(+), 116 deletions(-) create mode 100644 src/native/macosx/coreaudio/jni/maccoreaudio_util.c create mode 100644 src/native/macosx/coreaudio/jni/maccoreaudio_util.h create mode 100644 src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_MacCoreAudioDevice.c create mode 100644 src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_MacCoreAudioDevice.h create mode 100644 src/org/jitsi/impl/neomedia/MacCoreAudioDevice.java create mode 100644 src/org/jitsi/impl/neomedia/device/MacCoreaudioSystem.java create mode 100644 src/org/jitsi/impl/neomedia/jmfext/media/protocol/maccoreaudio/DataSource.java create mode 100644 src/org/jitsi/impl/neomedia/jmfext/media/protocol/maccoreaudio/MacCoreaudioStream.java create mode 100644 src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/MacCoreaudioRenderer.java diff --git a/lib/native/mac/libjnmaccoreaudio.jnilib b/lib/native/mac/libjnmaccoreaudio.jnilib index 36b14be8817c9e4c79dcf50bdc6e2017a6509850..5be4b22878f8b79b21dab272c8c80b913127a3f2 100755 GIT binary patch literal 74352 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 NcImbK`KW^-{|{89J?8)b literal 35600 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 MK6R%1YL)K)2liV)+yDRo diff --git a/src/native/build.xml b/src/native/build.xml index 503182eb..6e43b3c9 100644 --- a/src/native/build.xml +++ b/src/native/build.xml @@ -932,6 +932,8 @@ <linkerarg value="Foundation" /> <linkerarg value="-framework" /> <linkerarg value="Coreaudio" /> + <linkerarg value="-framework" /> + <linkerarg value="AudioToolbox" /> <fileset dir="${src}/native/macosx/coreaudio/lib" includes="*.c"/> <fileset dir="${src}/native/macosx/coreaudio/jni" includes="*.c"/> diff --git a/src/native/macosx/coreaudio/jni/maccoreaudio_util.c b/src/native/macosx/coreaudio/jni/maccoreaudio_util.c new file mode 100644 index 00000000..2a744131 --- /dev/null +++ b/src/native/macosx/coreaudio/jni/maccoreaudio_util.c @@ -0,0 +1,250 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ + +#include "maccoreaudio_util.h" + +#include "../lib/device.h" + +#include <string.h> + +/** + * JNI utilities. + * + * @author Vincent Lucas + */ + +// Private static objects. + +static JavaVM * maccoreaudio_VM = NULL; + +static jclass maccoreaudio_devicesChangedCallbackClass = 0; +static jmethodID maccoreaudio_devicesChangedCallbackMethodID = 0; + +void maccoreaudio_initHotplug( + void); +void maccoreaudio_freeHotplug( + void); + + +// Implementation + +JNIEXPORT jint JNICALL +JNI_OnLoad(JavaVM *vm, void *pvt) +{ + maccoreaudio_VM = vm; + maccoreaudio_initHotplug(); + return JNI_VERSION_1_6; +} + +JNIEXPORT void JNICALL +JNI_OnUnload(JavaVM *vm, void *pvt) +{ + maccoreaudio_freeHotplug(); + maccoreaudio_VM = NULL; +} + +/** + * Gets a new <tt>jbyteArray</tt> instance which is initialized with the bytes + * of a specific C string i.e. <tt>const char *</tt>. + * + * @param env + * @param str the bytes/C string to initialize the new <tt>jbyteArray</tt> + * instance with + * @return a new <tt>jbyteArray</tt> instance which is initialized with the + * bytes of the specified <tt>str</tt> + */ +jbyteArray maccoreaudio_getStrBytes( + JNIEnv *env, + const char *str) +{ + jbyteArray bytes; + + if (str) + { + size_t length = strlen(str); + + bytes = (*env)->NewByteArray(env, length); + if (bytes && length) + (*env)->SetByteArrayRegion(env, bytes, 0, length, (jbyte *) str); + } + else + bytes = NULL; + return bytes; +} + +/** + * Returns a callback method identifier. + * + * @param env + * @param callback The object called back. + * @param callbackFunctionName The name of the function used for the callback. + * + * @return A callback method identifier. 0 if the callback function is not + * found. + */ +jmethodID maccoreaudio_getCallbackMethodID( + JNIEnv *env, + jobject callback, + char* callbackFunctionName) +{ + jclass callbackClass; + jmethodID callbackMethodID = NULL; + + if(callback) + { + if((callbackClass = (*env)->GetObjectClass(env, callback))) + { + callbackMethodID = (*env)->GetMethodID( + env, + callbackClass, + callbackFunctionName, + "([BI)V"); + } + } + + return callbackMethodID; +} + +/** + * Calls back the java side when respectively reading / wrtiting the input + * /output stream. + */ +void maccoreaudio_callbackMethod( + char *buffer, + int bufferLength, + void* callback, + void* callbackMethod) +{ + JNIEnv *env = NULL; + + if((*maccoreaudio_VM)->AttachCurrentThreadAsDaemon( + maccoreaudio_VM, + (void**) &env, + NULL) + == 0) + { + jbyteArray bufferBytes = (*env)->NewByteArray(env, bufferLength); + (*env)->SetByteArrayRegion( + env, + bufferBytes, + 0, + bufferLength, + (jbyte *) buffer); + + (*env)->CallVoidMethod( + env, + callback, + (jmethodID) callbackMethod, + bufferBytes, + bufferLength); + + jbyte* bytes = (*env)->GetByteArrayElements(env, bufferBytes, NULL); + memcpy(buffer, bytes, bufferLength); + (*env)->ReleaseByteArrayElements(env, bufferBytes, bytes, 0); + + (*maccoreaudio_VM)->DetachCurrentThread(maccoreaudio_VM); + } +} + +/** + * Calls back the java side when the device list has changed. + */ +void maccoreaudio_devicesChangedCallbackMethod(void) +{ + JNIEnv *env = NULL; + + if((*maccoreaudio_VM)->AttachCurrentThreadAsDaemon( + maccoreaudio_VM, + (void**) &env, + NULL) + == 0) + { + jclass class = maccoreaudio_devicesChangedCallbackClass; + jmethodID methodID = maccoreaudio_devicesChangedCallbackMethodID; + if(class && methodID) + { + (*env)->CallStaticVoidMethod(env, class, methodID); + } + + (*maccoreaudio_VM)->DetachCurrentThread(maccoreaudio_VM); + } +} + +/** + * Initializes the hotplug callback process. + */ +void maccoreaudio_initHotplug( + void) +{ + JNIEnv *env = NULL; + + if((*maccoreaudio_VM)->AttachCurrentThreadAsDaemon( + maccoreaudio_VM, + (void**) &env, + NULL) + == 0) + { + if(maccoreaudio_devicesChangedCallbackClass == NULL + && maccoreaudio_devicesChangedCallbackMethodID == NULL) + { + jclass devicesChangedCallbackClass = (*env)->FindClass( + env, + "org/jitsi/impl/neomedia/CoreAudioDevice"); + + if (devicesChangedCallbackClass) + { + devicesChangedCallbackClass + = (*env)->NewGlobalRef(env, devicesChangedCallbackClass); + if (devicesChangedCallbackClass) + { + jmethodID devicesChangedCallbackMethodID + = (*env)->GetStaticMethodID( + env, + devicesChangedCallbackClass, + "devicesChangedCallback", + "()V"); + + if (devicesChangedCallbackMethodID) + { + maccoreaudio_devicesChangedCallbackClass + = devicesChangedCallbackClass; + maccoreaudio_devicesChangedCallbackMethodID + = devicesChangedCallbackMethodID; + + maccoreaudio_initializeHotplug( + maccoreaudio_devicesChangedCallbackMethod); + } + } + } + } + (*maccoreaudio_VM)->DetachCurrentThread(maccoreaudio_VM); + } +} + +/** + * Frees the hotplug callback process. + */ +void maccoreaudio_freeHotplug( + void) +{ + maccoreaudio_uninitializeHotplug(); + JNIEnv *env = NULL; + + if((*maccoreaudio_VM)->AttachCurrentThreadAsDaemon( + maccoreaudio_VM, + (void**) &env, + NULL) + == 0) + { + (*env)->DeleteGlobalRef( + env, + maccoreaudio_devicesChangedCallbackClass); + (*maccoreaudio_VM)->DetachCurrentThread(maccoreaudio_VM); + } + maccoreaudio_devicesChangedCallbackClass = NULL; + maccoreaudio_devicesChangedCallbackMethodID = NULL; +} diff --git a/src/native/macosx/coreaudio/jni/maccoreaudio_util.h b/src/native/macosx/coreaudio/jni/maccoreaudio_util.h new file mode 100644 index 00000000..9f8b5cb9 --- /dev/null +++ b/src/native/macosx/coreaudio/jni/maccoreaudio_util.h @@ -0,0 +1,42 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ + +#ifndef __maccoreaudio_util_h +#define __maccoreaudio_util_h + +#include <jni.h> + +/** + * JNI utilities. + * + * @author Vincent Lucas + */ +JNIEXPORT jint JNICALL +JNI_OnLoad(JavaVM *vm, void *pvt); + +JNIEXPORT void JNICALL +JNI_OnUnload(JavaVM *vm, void *pvt); + +jbyteArray maccoreaudio_getStrBytes( + JNIEnv *env, + const char *str); + +jmethodID maccoreaudio_getCallbackMethodID( + JNIEnv *env, + jobject callback, + char* callbackFunctionName); + +void maccoreaudio_callbackMethod( + char *buffer, + int bufferLength, + void* callback, + void* callbackMethod); + +void maccoreaudio_devicesChangedCallbackMethod( + void); + +#endif diff --git a/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_CoreAudioDevice.c b/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_CoreAudioDevice.c index 7ad57402..38022b13 100644 --- a/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_CoreAudioDevice.c +++ b/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_CoreAudioDevice.c @@ -8,31 +8,28 @@ #include "org_jitsi_impl_neomedia_CoreAudioDevice.h" #include "../lib/device.h" +#include "maccoreaudio_util.h" /** * JNI code for CoreAudioDevice. * - * @author Vicnent Lucas + * @author Vincent Lucas */ -// Private functions - -static jbyteArray getStrBytes(JNIEnv *env, const char *str); - // Implementation JNIEXPORT jint JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_initDevices (JNIEnv *env, jclass clazz) { - return initDevices(); + return maccoreaudio_initDevices(); } JNIEXPORT void JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_freeDevices (JNIEnv *env, jclass clazz) { - freeDevices(); + maccoreaudio_freeDevices(); } JNIEXPORT jbyteArray JNICALL @@ -40,8 +37,8 @@ Java_org_jitsi_impl_neomedia_CoreAudioDevice_getDeviceNameBytes (JNIEnv *env, jclass clazz, jstring deviceUID) { const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); - char * deviceName = getDeviceName(deviceUIDPtr); - jbyteArray deviceNameBytes = getStrBytes(env, deviceName); + char * deviceName = maccoreaudio_getDeviceName(deviceUIDPtr); + jbyteArray deviceNameBytes = maccoreaudio_getStrBytes(env, deviceName); // Free free(deviceName); (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); @@ -54,9 +51,10 @@ Java_org_jitsi_impl_neomedia_CoreAudioDevice_getDeviceModelIdentifierBytes (JNIEnv *env, jclass clazz, jstring deviceUID) { const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); - char * deviceModelIdentifier = getDeviceModelIdentifier(deviceUIDPtr); + char * deviceModelIdentifier + = maccoreaudio_getDeviceModelIdentifier(deviceUIDPtr); jbyteArray deviceModelIdentifierBytes - = getStrBytes(env, deviceModelIdentifier); + = maccoreaudio_getStrBytes(env, deviceModelIdentifier); // Free free(deviceModelIdentifier); (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); @@ -69,7 +67,7 @@ Java_org_jitsi_impl_neomedia_CoreAudioDevice_setInputDeviceVolume (JNIEnv *env, jclass clazz, jstring deviceUID, jfloat volume) { const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); - jint err = setInputDeviceVolume(deviceUIDPtr, volume); + jint err = maccoreaudio_setInputDeviceVolume(deviceUIDPtr, volume); // Free (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); @@ -81,7 +79,7 @@ Java_org_jitsi_impl_neomedia_CoreAudioDevice_setOutputDeviceVolume (JNIEnv *env, jclass clazz, jstring deviceUID, jfloat volume) { const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); - jint err = setOutputDeviceVolume(deviceUIDPtr, volume); + jint err = maccoreaudio_setOutputDeviceVolume(deviceUIDPtr, volume); // Free (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); @@ -93,7 +91,7 @@ Java_org_jitsi_impl_neomedia_CoreAudioDevice_getInputDeviceVolume (JNIEnv *env, jclass clazz, jstring deviceUID) { const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); - jfloat volume = getInputDeviceVolume(deviceUIDPtr); + jfloat volume = maccoreaudio_getInputDeviceVolume(deviceUIDPtr); // Free (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); @@ -105,36 +103,9 @@ Java_org_jitsi_impl_neomedia_CoreAudioDevice_getOutputDeviceVolume (JNIEnv *env, jclass clazz, jstring deviceUID) { const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); - jfloat volume = getOutputDeviceVolume(deviceUIDPtr); + jfloat volume = maccoreaudio_getOutputDeviceVolume(deviceUIDPtr); // Free (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); return volume; } - -/** - * Gets a new <tt>jbyteArray</tt> instance which is initialized with the bytes - * of a specific C string i.e. <tt>const char *</tt>. - * - * @param env - * @param str the bytes/C string to initialize the new <tt>jbyteArray</tt> - * instance with - * @return a new <tt>jbyteArray</tt> instance which is initialized with the - * bytes of the specified <tt>str</tt> - */ -static jbyteArray getStrBytes(JNIEnv *env, const char *str) -{ - jbyteArray bytes; - - if (str) - { - size_t length = strlen(str); - - bytes = (*env)->NewByteArray(env, length); - if (bytes && length) - (*env)->SetByteArrayRegion(env, bytes, 0, length, (jbyte *) str); - } - else - bytes = NULL; - return bytes; -} diff --git a/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_CoreAudioDevice.h b/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_CoreAudioDevice.h index c02caef4..df7d7a74 100644 --- a/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_CoreAudioDevice.h +++ b/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_CoreAudioDevice.h @@ -9,19 +9,19 @@ extern "C" { #endif /* * Class: org_jitsi_impl_neomedia_CoreAudioDevice - * Method: initDevices - * Signature: ()I + * Method: freeDevices + * Signature: ()V */ -JNIEXPORT jint JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_initDevices +JNIEXPORT void JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_freeDevices (JNIEnv *, jclass); /* * Class: org_jitsi_impl_neomedia_CoreAudioDevice - * Method: freeDevices - * Signature: ()V + * Method: getDeviceModelIdentifierBytes + * Signature: (Ljava/lang/String;)[B */ -JNIEXPORT void JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_freeDevices - (JNIEnv *, jclass); +JNIEXPORT jbyteArray JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_getDeviceModelIdentifierBytes + (JNIEnv *, jclass, jstring); /* * Class: org_jitsi_impl_neomedia_CoreAudioDevice @@ -33,43 +33,43 @@ JNIEXPORT jbyteArray JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_getDev /* * Class: org_jitsi_impl_neomedia_CoreAudioDevice - * Method: getDeviceModelIdentifierBytes - * Signature: (Ljava/lang/String;)[B + * Method: getInputDeviceVolume + * Signature: (Ljava/lang/String;)F */ -JNIEXPORT jbyteArray JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_getDeviceModelIdentifierBytes +JNIEXPORT jfloat JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_getInputDeviceVolume (JNIEnv *, jclass, jstring); /* * Class: org_jitsi_impl_neomedia_CoreAudioDevice - * Method: setInputDeviceVolume - * Signature: (Ljava/lang/String;F)I + * Method: getOutputDeviceVolume + * Signature: (Ljava/lang/String;)F */ -JNIEXPORT jint JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_setInputDeviceVolume - (JNIEnv *, jclass, jstring, jfloat); +JNIEXPORT jfloat JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_getOutputDeviceVolume + (JNIEnv *, jclass, jstring); /* * Class: org_jitsi_impl_neomedia_CoreAudioDevice - * Method: setOutputDeviceVolume - * Signature: (Ljava/lang/String;F)I + * Method: initDevices + * Signature: ()I */ -JNIEXPORT jint JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_setOutputDeviceVolume - (JNIEnv *, jclass, jstring, jfloat); +JNIEXPORT jint JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_initDevices + (JNIEnv *, jclass); /* * Class: org_jitsi_impl_neomedia_CoreAudioDevice - * Method: getInputDeviceVolume - * Signature: (Ljava/lang/String;)F + * Method: setInputDeviceVolume + * Signature: (Ljava/lang/String;F)I */ -JNIEXPORT jfloat JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_getInputDeviceVolume - (JNIEnv *, jclass, jstring); +JNIEXPORT jint JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_setInputDeviceVolume + (JNIEnv *, jclass, jstring, jfloat); /* * Class: org_jitsi_impl_neomedia_CoreAudioDevice - * Method: getOutputDeviceVolume - * Signature: (Ljava/lang/String;)F + * Method: setOutputDeviceVolume + * Signature: (Ljava/lang/String;F)I */ -JNIEXPORT jfloat JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_getOutputDeviceVolume - (JNIEnv *, jclass, jstring); +JNIEXPORT jint JNICALL Java_org_jitsi_impl_neomedia_CoreAudioDevice_setOutputDeviceVolume + (JNIEnv *, jclass, jstring, jfloat); #ifdef __cplusplus } diff --git a/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_MacCoreAudioDevice.c b/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_MacCoreAudioDevice.c new file mode 100644 index 00000000..5ec04cd9 --- /dev/null +++ b/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_MacCoreAudioDevice.c @@ -0,0 +1,277 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ + +#include "org_jitsi_impl_neomedia_MacCoreAudioDevice.h" + +#include "../lib/device.h" +#include "maccoreaudio_util.h" + +/** + * JNI code for CoreAudioDevice. + * + * @author Vicnent Lucas + */ + +// Implementation + +JNIEXPORT jobjectArray JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getDeviceUIDList + (JNIEnv *env, jclass clazz) +{ + jobjectArray javaDeviceUIDList = NULL; + + char ** deviceUIDList; + int nbDevices = maccoreaudio_getDeviceUIDList(&deviceUIDList); + if(nbDevices != -1) + { + jstring deviceUID; + jclass stringClass = (*env)->FindClass(env, "java/lang/String"); + javaDeviceUIDList + = (*env)->NewObjectArray(env, nbDevices, stringClass, NULL); + int i; + for(i = 0; i < nbDevices; ++i) + { + deviceUID = (*env)->NewStringUTF(env, deviceUIDList[i]); + if(deviceUID != NULL) + { + (*env)->SetObjectArrayElement( + env, + javaDeviceUIDList, + i, + deviceUID); + } + + free(deviceUIDList[i]); + } + + free(deviceUIDList); + } + + return javaDeviceUIDList; +} + +JNIEXPORT jboolean JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_isInputDevice + (JNIEnv *env, jclass clazz, jstring deviceUID) +{ + const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); + jint isInputDevice = maccoreaudio_isInputDevice(deviceUIDPtr); + // Free + (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); + + return (isInputDevice != 0); +} + +JNIEXPORT jboolean JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_isOutputDevice + (JNIEnv *env, jclass clazz, jstring deviceUID) +{ + const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); + jint isOutputDevice = maccoreaudio_isOutputDevice(deviceUIDPtr); + // Free + (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); + + return (isOutputDevice != 0); +} + +JNIEXPORT jbyteArray JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getTransportTypeBytes + (JNIEnv *env, jclass clazz, jstring deviceUID) +{ + const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); + const char * transportType = maccoreaudio_getTransportType(deviceUIDPtr); + jbyteArray transportTypeBytes + = maccoreaudio_getStrBytes(env, transportType); + // Free + (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); + + return transportTypeBytes; +} + +JNIEXPORT jfloat JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getNominalSampleRate + (JNIEnv *env, jclass clazz, jstring deviceUID) +{ + const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); + jfloat rate = maccoreaudio_getNominalSampleRate(deviceUIDPtr); + // Free + (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); + + return rate; +} + +JNIEXPORT jfloat JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getMinimalNominalSampleRate + (JNIEnv *env, jclass clazz, jstring deviceUID) +{ + const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); + Float64 minRate; + Float64 maxRate; + if(maccoreaudio_getAvailableNominalSampleRates( + deviceUIDPtr, + &minRate, + &maxRate) + != noErr) + { + fprintf(stderr, + "MacCoreAudioDevice_getMinimalNominalSampleRate\ + \n\tmaccoreaudio_getAvailableNominalSampleRates\n"); + fflush(stderr); + return -1.0; + } + // Free + (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); + + return minRate; +} + +JNIEXPORT jfloat JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getMaximalNominalSampleRate + (JNIEnv *env, jclass clazz, jstring deviceUID) +{ + const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); + Float64 minRate; + Float64 maxRate; + if(maccoreaudio_getAvailableNominalSampleRates( + deviceUIDPtr, + &minRate, + &maxRate) + != noErr) + { + fprintf(stderr, + "MacCoreAudioDevice_getMaximalNominalSampleRate\ + \n\tmaccoreaudio_getAvailableNominalSampleRates\n"); + fflush(stderr); + return -1.0; + } + // Free + (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); + + return maxRate; +} + +JNIEXPORT jbyteArray JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getDefaultInputDeviceUIDBytes + (JNIEnv *env, jclass clazz) +{ + char* defaultInputDeviceUID = maccoreaudio_getDefaultInputDeviceUID(); + jbyteArray defaultInputDeviceUIDBytes + = maccoreaudio_getStrBytes(env, defaultInputDeviceUID); + // Free + free(defaultInputDeviceUID); + + return defaultInputDeviceUIDBytes; +} + +JNIEXPORT jbyteArray JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getDefaultOutputDeviceUIDBytes + (JNIEnv *env, jclass clazz) +{ + char* defaultOutputDeviceUID + = maccoreaudio_getDefaultOutputDeviceUID(); + jbyteArray defaultOutputDeviceUIDBytes + = maccoreaudio_getStrBytes(env, defaultOutputDeviceUID); + // Free + free(defaultOutputDeviceUID); + + return defaultOutputDeviceUIDBytes; +} + +JNIEXPORT jlong JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_startStream + (JNIEnv *env, jclass clazz, jstring deviceUID, jobject callback, + jfloat sampleRate, + jint nbChannels, + jint bitsPerChannel, + jboolean isFloat, + jboolean isBigEndian, + jboolean isNonInterleaved) +{ + const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); + jobject callbackObject = (*env)->NewGlobalRef(env, callback); + maccoreaudio_stream* stream = NULL; + + if(maccoreaudio_isInputDevice(deviceUIDPtr)) // input + { + jmethodID callbackMethod = maccoreaudio_getCallbackMethodID( + env, + callback, + "readInput"); + stream = maccoreaudio_startInputStream( + deviceUIDPtr, + (void*) maccoreaudio_callbackMethod, + callbackObject, + callbackMethod, + sampleRate, + nbChannels, + bitsPerChannel, + isFloat, + isBigEndian, + isNonInterleaved); + } + else if(maccoreaudio_isOutputDevice(deviceUIDPtr)) // output + { + jmethodID callbackMethod = maccoreaudio_getCallbackMethodID( + env, + callback, + "writeOutput"); + stream = maccoreaudio_startOutputStream( + deviceUIDPtr, + (void*) maccoreaudio_callbackMethod, + callbackObject, + callbackMethod, + sampleRate, + nbChannels, + bitsPerChannel, + isFloat, + isBigEndian, + isNonInterleaved); + } + + // Free + (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); + + return (long) stream; +} + +JNIEXPORT void JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_stopStream + (JNIEnv *env, jclass clazz, jstring deviceUID, jlong streamPtr) +{ + const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); + maccoreaudio_stream * stream = (maccoreaudio_stream*) (long) streamPtr; + + maccoreaudio_stopStream(deviceUIDPtr, stream); + + // Free + (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); + (*env)->DeleteGlobalRef(env, stream->callbackObject); +} + +JNIEXPORT jint JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_countInputChannels + (JNIEnv *env, jclass clazz, jstring deviceUID) +{ + const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); + jint nbChannels = maccoreaudio_countInputChannels(deviceUIDPtr); + // Free + (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); + + return nbChannels; +} + +JNIEXPORT jint JNICALL +Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_countOutputChannels + (JNIEnv *env, jclass clazz, jstring deviceUID) +{ + const char * deviceUIDPtr = (*env)->GetStringUTFChars(env, deviceUID, 0); + jint nbChannels = maccoreaudio_countOutputChannels(deviceUIDPtr); + // Free + (*env)->ReleaseStringUTFChars(env, deviceUID, deviceUIDPtr); + + return nbChannels; +} diff --git a/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_MacCoreAudioDevice.h b/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_MacCoreAudioDevice.h new file mode 100644 index 00000000..729cbe1d --- /dev/null +++ b/src/native/macosx/coreaudio/jni/org_jitsi_impl_neomedia_MacCoreAudioDevice.h @@ -0,0 +1,125 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include <jni.h> +/* Header for class org_jitsi_impl_neomedia_MacCoreAudioDevice */ + +#ifndef _Included_org_jitsi_impl_neomedia_MacCoreAudioDevice +#define _Included_org_jitsi_impl_neomedia_MacCoreAudioDevice +#ifdef __cplusplus +extern "C" { +#endif +#undef org_jitsi_impl_neomedia_MacCoreAudioDevice_DEFAULT_SAMPLE_RATE +#define org_jitsi_impl_neomedia_MacCoreAudioDevice_DEFAULT_SAMPLE_RATE 44100.0 +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: getDeviceUIDList + * Signature: ()[Ljava/lang/String; + */ +JNIEXPORT jobjectArray JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getDeviceUIDList + (JNIEnv *, jclass); + +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: isInputDevice + * Signature: (Ljava/lang/String;)Z + */ +JNIEXPORT jboolean JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_isInputDevice + (JNIEnv *, jclass, jstring); + +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: isOutputDevice + * Signature: (Ljava/lang/String;)Z + */ +JNIEXPORT jboolean JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_isOutputDevice + (JNIEnv *, jclass, jstring); + +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: getTransportTypeBytes + * Signature: (Ljava/lang/String;)[B + */ +JNIEXPORT jbyteArray JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getTransportTypeBytes + (JNIEnv *, jclass, jstring); + +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: getNominalSampleRate + * Signature: (Ljava/lang/String;)F + */ +JNIEXPORT jfloat JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getNominalSampleRate + (JNIEnv *, jclass, jstring); + +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: getMinimalNominalSampleRate + * Signature: (Ljava/lang/String;)F + */ +JNIEXPORT jfloat JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getMinimalNominalSampleRate + (JNIEnv *, jclass, jstring); + +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: getMaximalNominalSampleRate + * Signature: (Ljava/lang/String;)F + */ +JNIEXPORT jfloat JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getMaximalNominalSampleRate + (JNIEnv *, jclass, jstring); + +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: getDefaultInputDeviceUIDBytes + * Signature: ()[B + */ +JNIEXPORT jbyteArray JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getDefaultInputDeviceUIDBytes + (JNIEnv *, jclass); + +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: getDefaultOutputDeviceUIDBytes + * Signature: ()[B + */ +JNIEXPORT jbyteArray JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_getDefaultOutputDeviceUIDBytes + (JNIEnv *, jclass); + +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: startStream + * Signature: (Ljava/lang/String;Ljava/lang/Object;)J + */ +JNIEXPORT jlong JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_startStream + (JNIEnv *, jclass, jstring, jobject, + jfloat sampleRate, + jint nbChannels, + jint bitsPerChannel, + jboolean isFloat, + jboolean isBigEndian, + jboolean isNonInterleaved); + +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: stopStream + * Signature: (Ljava/lang/String;J)V + */ +JNIEXPORT void JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_stopStream + (JNIEnv *, jclass, jstring, jlong); + +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: countInputChannels + * Signature: (Ljava/lang/String;)I + */ +JNIEXPORT jint JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_countInputChannels + (JNIEnv *, jclass, jstring); + +/* + * Class: org_jitsi_impl_neomedia_MacCoreAudioDevice + * Method: countOutputChannels + * Signature: (Ljava/lang/String;)I + */ +JNIEXPORT jint JNICALL Java_org_jitsi_impl_neomedia_MacCoreAudioDevice_countOutputChannels + (JNIEnv *, jclass, jstring); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/native/macosx/coreaudio/lib/device.c b/src/native/macosx/coreaudio/lib/device.c index 0894ae2a..d517c2f3 100644 --- a/src/native/macosx/coreaudio/lib/device.c +++ b/src/native/macosx/coreaudio/lib/device.c @@ -9,39 +9,139 @@ #include <CoreAudio/CoreAudio.h> #include <CoreFoundation/CFString.h> #include <stdio.h> + /** * Functions to list, access and modifies audio devices via coreaudio. * * @author Vincent Lucas */ +const char* transportTypeAggregate = "Aggregate"; +const char* transportTypeAirPlay = "AirPlay"; +const char* transportTypeAutoAggregate = "Auto aggregate"; +const char* transportTypeAVB = "AVB"; +const char* transportTypeBlueTooth = "Bluetooth"; +const char* transportTypeBuiltIn = "Built-in"; +const char* transportTypeDisplayPort = "DisplayPort"; +const char* transportTypeFireWire = "FireWire"; +const char* transportTypeHDMI = "HDMI"; +const char* transportTypePCI = "PCI"; +const char* transportTypeThunderbolt = "Thunderbolt"; +const char* transportTypeUnknown = "Unknown"; +const char* transportTypeUSB = "USB"; +const char* transportTypeVirtual = "Virtual"; /** * Private definition of functions, */ -char* getDeviceProperty( + +AudioDeviceID maccoreaudio_getDevice( + const char * deviceUID); + +AudioDeviceID maccoreaudio_getDeviceForSpecificScope( + const char * deviceUID, + UInt32 inputOutputScope); + +char* maccoreaudio_getDeviceProperty( const char * deviceUID, AudioObjectPropertySelector propertySelector); -OSStatus setDeviceVolume( +char* maccoreaudio_getAudioDeviceProperty( + AudioDeviceID device, + AudioObjectPropertySelector propertySelector); + +OSStatus maccoreaudio_setDeviceVolume( const char * deviceUID, Float32 volume, UInt32 inputOutputScope); -Float32 getDeviceVolume( +Float32 maccoreaudio_getDeviceVolume( const char * deviceUID, UInt32 inputOutputScope); -OSStatus getChannelsForStereo( +OSStatus maccoreaudio_getChannelsForStereo( const char * deviceUID, UInt32 * channels); +int maccoreaudio_countChannels( + const char * deviceUID, + AudioObjectPropertyScope inputOutputScope); + +static OSStatus maccoreaudio_devicesChangedCallback( + AudioObjectID inObjectID, + UInt32 inNumberAddresses, + const AudioObjectPropertyAddress inAddresses[], + void *inClientData); + +OSStatus maccoreaudio_initConverter( + const char * deviceUID, + const AudioStreamBasicDescription * javaFormat, + unsigned char isJavaFormatSource, + AudioConverterRef * converter, + double * conversionRatio); + +inline UInt32 CalculateLPCMFlags ( + UInt32 inValidBitsPerChannel, + UInt32 inTotalBitsPerChannel, + bool inIsFloat, + bool inIsBigEndian, + bool inIsNonInterleaved); + +inline void FillOutASBDForLPCM( + AudioStreamBasicDescription * outASBD, + Float64 inSampleRate, + UInt32 inChannelsPerFrame, + UInt32 inValidBitsPerChannel, + UInt32 inTotalBitsPerChannel, + bool inIsFloat, + bool inIsBigEndian, + bool inIsNonInterleaved); + +char* maccoreaudio_getDefaultDeviceUID( + UInt32 inputOutputScope); + +maccoreaudio_stream * maccoreaudio_startStream( + const char * deviceUID, + void* callbackFunction, + void* callbackObject, + void* callbackMethod, + void* readWriteFunction, + unsigned char isJavaFormatSource, + float sampleRate, + UInt32 nbChannels, + UInt32 bitsPerChannel, + unsigned char isFloat, + unsigned char isBigEndian, + unsigned char isNonInterleaved); + +OSStatus maccoreaudio_readInputStream( + AudioDeviceID inDevice, + const AudioTimeStamp* inNow, + const AudioBufferList* inInputData, + const AudioTimeStamp* inInputTime, + AudioBufferList* outOutputData, + const AudioTimeStamp* inOutputTime, + void* inClientData); + +OSStatus maccoreaudio_writeOutputStream( + AudioDeviceID inDevice, + const AudioTimeStamp* inNow, + const AudioBufferList* inInputData, + const AudioTimeStamp* inInputTime, + AudioBufferList* outOutputData, + const AudioTimeStamp* inOutputTime, + void* inClientData); + +OSStatus maccoreaudio_getStreamVirtualFormat( + AudioStreamID stream, + AudioStreamBasicDescription * format); + /** * Do nothing: there is no need to initializes anything to get device * information on MacOsX. * * @return Always returns 0 (always works). */ -int initDevices(void) +int maccoreaudio_initDevices(void) { return 0; } @@ -50,22 +150,75 @@ int initDevices(void) * Do nothing: there is no need to frees anything once getting device * information is finished on MacOsX. */ -void freeDevices(void) +void maccoreaudio_freeDevices(void) { // Nothing to do. } +/** + * Returns if the audio device is an input device. + * + * @param deviceUID The device UID. + * + * @return True if the given device identifier correspond to an input device. + * False otherwise. + */ +int maccoreaudio_isInputDevice( + const char * deviceUID) +{ + return (maccoreaudio_countChannels( + deviceUID, + kAudioDevicePropertyScopeInput) > 0); +} + +/** + * Returns if the audio device is an output device. + * + * @param deviceUID The device UID. + * + * @return True if the given device identifier correspond to an output device. + * False otherwise. + */ +int maccoreaudio_isOutputDevice( + const char * deviceUID) +{ + return (maccoreaudio_countChannels( + deviceUID, + kAudioDevicePropertyScopeOutput) > 0); +} + /** * Returns the audio device corresponding to the UID given in parameter. Or * kAudioObjectUnknown if the device is nonexistant or if anything as failed. * - * @pqrqm deviceUID The device UID. + * @param deviceUID The device UID. * * @return The audio device corresponding to the UID given in parameter. Or * kAudioObjectUnknown if the device is nonexistant or if anything as failed. */ -AudioDeviceID getDevice( +AudioDeviceID maccoreaudio_getDevice( const char * deviceUID) +{ + return maccoreaudio_getDeviceForSpecificScope( + deviceUID, + kAudioObjectPropertyScopeGlobal); +} + +/** + * Returns the audio device corresponding to the UID given in parameter for the + * specified scope (global, input or output). Or kAudioObjectUnknown if the + * device is nonexistant or if anything as failed. + * + * @param deviceUID The device UID. + * @param inputOutputScope The scope to tell if this is an output or an input + * device. + * + * @return The audio device corresponding to the UID given in parameter. Or + * kAudioObjectUnknown if the device is nonexistant or if anything as failed. + */ +AudioDeviceID maccoreaudio_getDeviceForSpecificScope( + const char * deviceUID, + UInt32 inputOutputScope) { OSStatus err = noErr; AudioObjectPropertyAddress address; @@ -93,7 +246,7 @@ AudioDeviceID getDevice( translation.mOutputDataSize = sizeof(device); size = sizeof(translation); address.mSelector = kAudioHardwarePropertyDeviceForUID; - address.mScope = kAudioObjectPropertyScopeGlobal; + address.mScope = inputOutputScope; address.mElement = kAudioObjectPropertyElementMaster; if((err = AudioObjectGetPropertyData( @@ -117,6 +270,84 @@ AudioDeviceID getDevice( return device; } +/** + * Returns the default input device UID. + * + * @return The default input device UID. NULL if an error occurs. + */ +char* maccoreaudio_getDefaultInputDeviceUID(void) +{ + return maccoreaudio_getDefaultDeviceUID(kAudioDevicePropertyScopeInput); +} + +/** + * Returns the default output device UID. + * + * @return The default output device UID. NULL if an error occurs. + */ +char* maccoreaudio_getDefaultOutputDeviceUID(void) +{ + return maccoreaudio_getDefaultDeviceUID(kAudioDevicePropertyScopeOutput); +} + +/** + * Returns the default device UID for input or ouput. + * + * @param inputOutputScope The scope to tell if this is an output or an input + * device. + * + * @return The default device UID for input or ouput. NULL if an error occurs. + */ +char* maccoreaudio_getDefaultDeviceUID( + UInt32 inputOutputScope) +{ + OSStatus err = noErr; + AudioDeviceID device; + UInt32 size = sizeof(AudioDeviceID); + AudioObjectPropertyAddress address; + char * deviceUID = NULL; + + if(inputOutputScope == kAudioDevicePropertyScopeInput) + { + address.mSelector = kAudioHardwarePropertyDefaultInputDevice; + } + else + { + address.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + } + address.mScope = inputOutputScope; + address.mElement = kAudioObjectPropertyElementMaster; + + if((err = AudioObjectGetPropertyData( + kAudioObjectSystemObject, + &address, + 0, + NULL, + &size, + &device)) + != noErr) + { + fprintf(stderr, + "maccoreaudio_getDefaultDeviceUID (coreaudio/device.c): \ + \n\tAudioObjectGetPropertyData, err: %d\n", + ((int) err)); + return NULL; + } + + if((deviceUID = maccoreaudio_getAudioDeviceProperty( + device, + kAudioDevicePropertyDeviceUID)) + == NULL) + { + fprintf(stderr, + "maccoreaudio_getDefaultDeviceUID (coreaudio/device.c): \ + \n\tgetAudioDeviceProperty\n"); + return NULL; + } + + return deviceUID; +} + /** * Returns the device name for the given device. Or NULL, if not available. The * returned string must be freed by the caller. @@ -126,10 +357,10 @@ AudioDeviceID getDevice( * @return The device name for the given device. Or NULL, if not available. The * returned string must be freed by the caller. */ -char* getDeviceName( +char* maccoreaudio_getDeviceName( const char * deviceUID) { - return getDeviceProperty(deviceUID, kAudioObjectPropertyName); + return maccoreaudio_getDeviceProperty(deviceUID, kAudioObjectPropertyName); } /** @@ -141,33 +372,31 @@ char* getDeviceName( * @return The device model identifier for the given device. Or NULL, if not * available. The returned string must be freed by the caller. */ -char* getDeviceModelIdentifier( +char* maccoreaudio_getDeviceModelIdentifier( const char * deviceUID) { - return getDeviceProperty(deviceUID, kAudioDevicePropertyModelUID); + return + maccoreaudio_getDeviceProperty(deviceUID, kAudioDevicePropertyModelUID); } /** - * Returns the requested device property for the given device. Or NULL, if not - * available. The returned string must be freed by the caller. + * Returns the requested device property for the given device UID. Or NULL, if + * not available. The returned string must be freed by the caller. * - * @param device The device to get the name from. + * @param deviceUID The device identifier to get the property from. * @param propertySelector The property we want to retrieve. * - * @return The requested device property for the given device. Or NULL, if not - * available. The returned string must be freed by the caller. + * @return The requested device property for the given device UID. Or NULL, if + * not available. The returned string must be freed by the caller. */ -char* getDeviceProperty( +char* maccoreaudio_getDeviceProperty( const char * deviceUID, AudioObjectPropertySelector propertySelector) { AudioDeviceID device; - OSStatus err = noErr; - AudioObjectPropertyAddress address; - UInt32 size; // Gets the correspoding device - if((device = getDevice(deviceUID)) == kAudioObjectUnknown) + if((device = maccoreaudio_getDevice(deviceUID)) == kAudioObjectUnknown) { fprintf(stderr, "getDeviceProperty (coreaudio/device.c): \ @@ -175,6 +404,27 @@ char* getDeviceProperty( return NULL; } + return maccoreaudio_getAudioDeviceProperty(device, propertySelector); +} + +/** + * Returns the requested device property for the given device. Or NULL, if not + * available. The returned string must be freed by the caller. + * + * @param device The device to get the name from. + * @param propertySelector The property we want to retrieve. + * + * @return The requested device property for the given device. Or NULL, if not + * available. The returned string must be freed by the caller. + */ +char* maccoreaudio_getAudioDeviceProperty( + AudioDeviceID device, + AudioObjectPropertySelector propertySelector) +{ + OSStatus err = noErr; + AudioObjectPropertyAddress address; + UInt32 size; + // Gets the device property CFStringRef deviceProperty; size = sizeof(deviceProperty); @@ -230,11 +480,11 @@ char* getDeviceProperty( * @return noErr if everything works well. Another value if an error has * occured. */ -OSStatus setInputDeviceVolume( +OSStatus maccoreaudio_setInputDeviceVolume( const char * deviceUID, Float32 volume) { - return setDeviceVolume( + return maccoreaudio_setDeviceVolume( deviceUID, volume, kAudioDevicePropertyScopeInput); @@ -250,11 +500,11 @@ OSStatus setInputDeviceVolume( * @return noErr if everything works well. Another value if an error has * occured. */ -OSStatus setOutputDeviceVolume( +OSStatus maccoreaudio_setOutputDeviceVolume( const char * deviceUID, Float32 volume) { - return setDeviceVolume( + return maccoreaudio_setDeviceVolume( deviceUID, volume, kAudioDevicePropertyScopeOutput); @@ -274,7 +524,7 @@ OSStatus setOutputDeviceVolume( * @return noErr if everything works well. Another value if an error has * occured. */ -OSStatus setDeviceVolume( +OSStatus maccoreaudio_setDeviceVolume( const char * deviceUID, Float32 volume, UInt32 inputOutputScope) @@ -286,7 +536,7 @@ OSStatus setDeviceVolume( UInt32 channels[2]; // Gets the correspoding device - if((device = getDevice(deviceUID)) == kAudioObjectUnknown) + if((device = maccoreaudio_getDevice(deviceUID)) == kAudioObjectUnknown) { fprintf(stderr, "setDeviceVolume (coreaudio/device.c): \ @@ -295,7 +545,7 @@ OSStatus setDeviceVolume( } // get the input device stereo channels - if((getChannelsForStereo(deviceUID, channels)) != noErr) + if((maccoreaudio_getChannelsForStereo(deviceUID, channels)) != noErr) { fprintf(stderr, "setDeviceVolume (coreaudio/device.c): \ @@ -355,10 +605,10 @@ OSStatus setDeviceVolume( * @return The device volume as a scalar value between 0.0 and 1.0. Returns -1.0 * if an error occurs. */ -Float32 getInputDeviceVolume( +Float32 maccoreaudio_getInputDeviceVolume( const char * deviceUID) { - return getDeviceVolume( + return maccoreaudio_getDeviceVolume( deviceUID, kAudioDevicePropertyScopeInput); } @@ -371,10 +621,10 @@ Float32 getInputDeviceVolume( * @return The device volume as a scalar value between 0.0 and 1.0. Returns -1.0 * if an error occurs. */ -Float32 getOutputDeviceVolume( +Float32 maccoreaudio_getOutputDeviceVolume( const char * deviceUID) { - return getDeviceVolume( + return maccoreaudio_getDeviceVolume( deviceUID, kAudioDevicePropertyScopeOutput); } @@ -391,7 +641,7 @@ Float32 getOutputDeviceVolume( * @return The device volume as a scalar value between 0.0 and 1.0. Returns -1.0 * if an error occurs. */ -Float32 getDeviceVolume( +Float32 maccoreaudio_getDeviceVolume( const char * deviceUID, UInt32 inputOutputScope) { @@ -403,7 +653,7 @@ Float32 getDeviceVolume( UInt32 channels[2]; // Gets the correspoding device - if((device = getDevice(deviceUID)) == kAudioObjectUnknown) + if((device = maccoreaudio_getDevice(deviceUID)) == kAudioObjectUnknown) { fprintf(stderr, "getDeviceVolume (coreaudio/device.c): \ @@ -412,7 +662,7 @@ Float32 getDeviceVolume( } // get the input device stereo channels - if((getChannelsForStereo(deviceUID, channels)) != noErr) + if((maccoreaudio_getChannelsForStereo(deviceUID, channels)) != noErr) { fprintf(stderr, "getDeviceVolume (coreaudio/device.c): \ @@ -474,7 +724,7 @@ Float32 getDeviceVolume( * @return An OSStatus set to noErr if everything works well. Any other vlaue * otherwise. */ -OSStatus getChannelsForStereo( +OSStatus maccoreaudio_getChannelsForStereo( const char * deviceUID, UInt32 * channels) { @@ -484,7 +734,7 @@ OSStatus getChannelsForStereo( UInt32 size; // Gets the correspoding device - if((device = getDevice(deviceUID)) == kAudioObjectUnknown) + if((device = maccoreaudio_getDevice(deviceUID)) == kAudioObjectUnknown) { fprintf(stderr, "getChannelsForStereo (coreaudio/device.c): \ @@ -514,3 +764,989 @@ OSStatus getChannelsForStereo( return err; } + +/** + * Returns the number of channels avaialable for input device. + * + * @param deviceUID The device UID to get the channels from. + * + * @return The number of channels avaialable for a given input device. + * -1 if an error occurs. + */ +int maccoreaudio_countInputChannels( + const char * deviceUID) +{ + return maccoreaudio_countChannels( + deviceUID, + kAudioDevicePropertyScopeInput); +} + +/** + * Returns the number of channels avaialable for output device. + * + * @param deviceUID The device UID to get the channels from. + * + * @return The number of channels avaialable for a given output device. + * -1 if an error occurs. + */ +int maccoreaudio_countOutputChannels( + const char * deviceUID) +{ + return maccoreaudio_countChannels( + deviceUID, + kAudioDevicePropertyScopeOutput); +} + +/** + * Returns the number of channels avaialable for a given input / output device. + * + * @param deviceUID The device UID to get the channels from. + * @param inputOutputScope The scope to tell if this is an output or an input + * device. + * + * @return The number of channels avaialable for a given input / output device. + * -1 if an error occurs. + */ +int maccoreaudio_countChannels( + const char * deviceUID, + AudioObjectPropertyScope inputOutputScope) +{ + AudioDeviceID device; + OSStatus err = noErr; + AudioObjectPropertyAddress address; + UInt32 size; + AudioBufferList *audioBufferList = NULL; + int nbChannels = 0; + int i; + + // Gets the correspoding device + if((device = maccoreaudio_getDevice(deviceUID)) == kAudioObjectUnknown) + { + fprintf(stderr, + "getChannelsForStereo (coreaudio/device.c): \ + \n\tgetDevice\n"); + return -1; + } + + // Gets the size of the streams for this device. + address.mSelector = kAudioDevicePropertyStreamConfiguration; + address.mScope = inputOutputScope; + address.mElement = kAudioObjectPropertyElementWildcard; // 0 + if((err = AudioObjectGetPropertyDataSize(device, &address, 0, NULL, &size)) + != noErr) + { + fprintf(stderr, + "maccoreaudio_countChannels (coreaudio/device.c): \ + \n\tAudioObjectGetPropertyDataSize, err: %d\n", + ((int) err)); + return -1; + } + + // Gets the number of channels ofr each stream. + if((audioBufferList = (AudioBufferList *) malloc(size)) == NULL) + { + perror("maccoreaudio_countChannels (coreaudio/device.c): \ + \n\tmalloc\n"); + return -1; + } + if((err = AudioObjectGetPropertyData( + device, + &address, + 0, + NULL, + &size, + audioBufferList)) + != noErr) + { + fprintf(stderr, + "maccoreaudio_countChannels (coreaudio/device.c): \ + \n\tAudioObjectGetPropertyData, err: %d\n", + ((int) err)); + return -1; + } + for(i = 0; i < audioBufferList->mNumberBuffers; ++i) + { + nbChannels += audioBufferList->mBuffers[i].mNumberChannels; + } + free(audioBufferList); + + return nbChannels; +} + +/** + * Returns the nominal sample rate for the given device. + * + * @param deviceUID The device UID to get the channels from. + * + * @return The nominal sample rate for the given device. -1.0 if an error + * occurs. + */ +Float64 maccoreaudio_getNominalSampleRate( + const char * deviceUID) +{ + AudioDeviceID device; + OSStatus err = noErr; + AudioObjectPropertyAddress address; + UInt32 size; + Float64 rate = -1.0; + + // Gets the correspoding device + if((device = maccoreaudio_getDevice(deviceUID)) == kAudioObjectUnknown) + { + fprintf(stderr, + "getNominalSampleRate (coreaudio/device.c): \ + \n\tgetDevice\n"); + return -1.0; + } + + // Gets the sample rate. + size = sizeof(Float64); + address.mSelector = kAudioDevicePropertyNominalSampleRate; + address.mScope = kAudioObjectPropertyScopeGlobal; + address.mElement = kAudioObjectPropertyElementMaster; + + if((err = AudioObjectGetPropertyData( + device, + &address, + 0, + NULL, + &size, + &rate)) + != noErr) + { + fprintf(stderr, + "getNominalSampleRate (coreaudio/device.c): \ + \n\tAudioObjactGetPropertyData, err: %d\n", + (int) err); + return -1.0; + } + + return rate; +} + +/** + * Gets the minimal and maximal nominal sample rate for the given device. + * + * @param deviceUID The device UID to get the channels from. + * @param minRate The minimal rate available for this device. + * @param maxRate The maximal rate available for this device. + * + * @return noErr if everything is alright. -1.0 if an error occurs. + */ +OSStatus maccoreaudio_getAvailableNominalSampleRates( + const char * deviceUID, + Float64 * minRate, + Float64 * maxRate) +{ + AudioDeviceID device; + OSStatus err = noErr; + AudioObjectPropertyAddress address; + UInt32 size; + AudioValueRange minMaxRate; + minMaxRate.mMinimum = -1.0; + minMaxRate.mMaximum = -1.0; + + // Gets the correspoding device + if((device = maccoreaudio_getDevice(deviceUID)) == kAudioObjectUnknown) + { + fprintf(stderr, + "getAvailableNominalSampleRates (coreaudio/device.c): \ + \n\tgetDevice\n"); + return -1.0; + } + + // Gets the available sample ratea. + size = sizeof(AudioValueRange); + address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; + address.mScope = kAudioObjectPropertyScopeGlobal; + address.mElement = kAudioObjectPropertyElementMaster; + + if((err = AudioObjectGetPropertyData( + device, + &address, + 0, + NULL, + &size, + &minMaxRate)) + != noErr) + { + fprintf(stderr, + "getAvailableNominalSampleRates (coreaudio/device.c): \ + \n\tAudioObjactGetPropertyData, err: %d\n", + (int) err); + return -1.0; + } + + (*minRate) = minMaxRate.mMinimum; + (*maxRate) = minMaxRate.mMaximum; + + return noErr; +} + +/** + * Lists the audio devices available and stores their UIDs in the provided + * parameter. + * + * @param deviceUIDList A pointer which will be filled in with a list of device + * UID strings. The caller is responsible to free this list and all the items. + * + * @return -1 in case of error. Otherwise, returns the number of devices stored + * in the deviceUIDList. + */ +int maccoreaudio_getDeviceUIDList( + char *** deviceUIDList) +{ + OSStatus err = noErr; + UInt32 propsize; + int nbDevices = -1; + + AudioObjectPropertyAddress address = + { + kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + if((err = AudioObjectGetPropertyDataSize( + kAudioObjectSystemObject, + &address, + 0, + NULL, + &propsize)) + != noErr) + { + fprintf(stderr, + "getDeviceUIDList (coreaudio/device.c): \ + \n\tAudioObjectGetPropertyDataSize, err: %d\n", + ((int) err)); + return -1; + } + + nbDevices = propsize / sizeof(AudioDeviceID); + AudioDeviceID *devices = NULL; + if((devices = (AudioDeviceID*) malloc(nbDevices * sizeof(AudioDeviceID))) + == NULL) + { + perror("getDeviceUIDList (coreaudio/device.c): \ + \n\tmalloc\n"); + return -1; + } + + if((err = AudioObjectGetPropertyData( + kAudioObjectSystemObject, + &address, + 0, + NULL, + &propsize, + devices)) + != noErr) + { + free(devices); + fprintf(stderr, + "getDeviceUIDList (coreaudio/device.c): \ + \n\tAudioObjectGetPropertyData, err: %d\n", + ((int) err)); + return -1; + } + + if(((*deviceUIDList) = (char**) malloc(nbDevices * sizeof(char*))) + == NULL) + { + free(devices); + perror("getDeviceUIDList (coreaudio/device.c): \ + \n\tmalloc\n"); + return -1; + } + + int i; + for(i = 0; i < nbDevices; ++i) + { + if(((*deviceUIDList)[i] = maccoreaudio_getAudioDeviceProperty( + devices[i], + kAudioDevicePropertyDeviceUID)) + == NULL) + { + int j; + for(j = 0; j < i; ++j) + { + free((*deviceUIDList)[j]); + } + free(*deviceUIDList); + free(devices); + fprintf(stderr, + "getDeviceUIDList (coreaudio/device.c): \ + \n\tgetAudioDeviceProperty\n"); + return -1; + } + } + + free(devices); + + return nbDevices; +} + +/** + * Registers the listener for new plugged-in/out devices. + */ +void maccoreaudio_initializeHotplug( + void* callbackFunction) +{ + AudioObjectPropertyAddress address = + { + kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + + AudioObjectAddPropertyListener( + kAudioObjectSystemObject, + &address, + maccoreaudio_devicesChangedCallback, + callbackFunction); +} + +/** + * Unregisters the listener for new plugged-in/out devices. + */ +void maccoreaudio_uninitializeHotplug() +{ + AudioObjectPropertyAddress address = + { + kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster + }; + + AudioObjectRemovePropertyListener( + kAudioObjectSystemObject, + &address, + maccoreaudio_devicesChangedCallback, + NULL); +} + +/** + * The callback function called when a device is plugged-in/out. + * + * @param inObjectID The AudioObject whose properties have changed. + * @param inNumberAddresses The number of elements in the inAddresses array. + * @param inAddresses An array of AudioObjectPropertyAddresses indicating which + * properties changed. + * @param inClientData A pointer to client data established when the listener + * proc was registered with the AudioObject. + * + * @return The return value is currently unused and should always be 0. + */ +static OSStatus maccoreaudio_devicesChangedCallback( + AudioObjectID inObjectID, + UInt32 inNumberAddresses, + const AudioObjectPropertyAddress inAddresses[], + void *inClientData) +{ + void (*callbackFunction) (void) = inClientData; + callbackFunction(); + + return noErr; +} + +/** + * Returns a string identifier of the device transport type. + * + * @param deviceUID The device UID to get the transport type from. + * + * @return The string identifier of the device transport type. Or NULL if + * failed. + */ +const char* maccoreaudio_getTransportType( + const char * deviceUID) +{ + AudioDeviceID device; + + // Gets the correspoding device + if((device = maccoreaudio_getDevice(deviceUID)) == kAudioObjectUnknown) + { + fprintf(stderr, + "maccoreaudio_getTransportType (coreaudio/device.c): \ + \n\tgetDevice\n"); + return NULL; + } + // target device transport type property + AudioObjectPropertyAddress address; + address.mSelector = kAudioDevicePropertyTransportType; + address.mScope = kAudioObjectPropertyScopeGlobal; + address.mElement = kAudioObjectPropertyElementMaster; + + OSStatus err; + unsigned int transportType = 0; + UInt32 size = sizeof(transportType); + if((err = AudioObjectGetPropertyData( + device, + &address, + 0, + NULL, + &size, + &transportType)) + != noErr) + { + fprintf(stderr, + "maccoreaudio_getTransportType (coreaudio/device.c): \ + \n\tAudioObjectGetPropertyData: err: %d\n", + (int) err); + return NULL; + } + + switch(transportType) + { + case kAudioDeviceTransportTypeAggregate: + return transportTypeAggregate; + break; + case kAudioDeviceTransportTypeAirPlay: + return transportTypeAirPlay; + break; + case kAudioDeviceTransportTypeAutoAggregate: + return transportTypeAutoAggregate; + break; + case kAudioDeviceTransportTypeAVB: + return transportTypeAVB; + break; + case kAudioDeviceTransportTypeBluetooth: + return transportTypeBlueTooth; + break; + case kAudioDeviceTransportTypeBuiltIn: + return transportTypeBuiltIn; + break; + case kAudioDeviceTransportTypeDisplayPort: + return transportTypeDisplayPort; + break; + case kAudioDeviceTransportTypeFireWire: + return transportTypeFireWire; + break; + case kAudioDeviceTransportTypeHDMI: + return transportTypeHDMI; + break; + case kAudioDeviceTransportTypePCI: + return transportTypePCI; + break; + case kAudioDeviceTransportTypeThunderbolt: + return transportTypeThunderbolt; + break; + case kAudioDeviceTransportTypeUnknown: + return transportTypeUnknown; + break; + case kAudioDeviceTransportTypeUSB: + return transportTypeUSB; + break; + case kAudioDeviceTransportTypeVirtual: + return transportTypeVirtual; + break; + default: + return NULL; + break; + } +} + +maccoreaudio_stream * maccoreaudio_startInputStream( + const char * deviceUID, + void* callbackFunction, + void* callbackObject, + void* callbackMethod, + float sampleRate, + UInt32 nbChannels, + UInt32 bitsPerChannel, + unsigned char isFloat, + unsigned char isBigEndian, + unsigned char isNonInterleaved) +{ + return maccoreaudio_startStream( + deviceUID, + callbackFunction, + callbackObject, + callbackMethod, + maccoreaudio_readInputStream, + false, + sampleRate, + nbChannels, + bitsPerChannel, + isFloat, + isBigEndian, + isNonInterleaved); +} + +maccoreaudio_stream * maccoreaudio_startOutputStream( + const char * deviceUID, + void* callbackFunction, + void* callbackObject, + void* callbackMethod, + float sampleRate, + UInt32 nbChannels, + UInt32 bitsPerChannel, + unsigned char isFloat, + unsigned char isBigEndian, + unsigned char isNonInterleaved) +{ + return maccoreaudio_startStream( + deviceUID, + callbackFunction, + callbackObject, + callbackMethod, + maccoreaudio_writeOutputStream, + true, + sampleRate, + nbChannels, + bitsPerChannel, + isFloat, + isBigEndian, + isNonInterleaved); +} + +/** + * The the IO processing of a device. + * + * @param deviceUID The device UID to get the data from / to. + * @param callbackFunction A function called + * @param readWriteFunction A function pointer called by the IO when data are + * available for read / write. + */ +maccoreaudio_stream * maccoreaudio_startStream( + const char * deviceUID, + void* callbackFunction, + void* callbackObject, + void* callbackMethod, + void* readWriteFunction, + unsigned char isJavaFormatSource, + float sampleRate, + UInt32 nbChannels, + UInt32 bitsPerChannel, + unsigned char isFloat, + unsigned char isBigEndian, + unsigned char isNonInterleaved) +{ + AudioDeviceID device; + + // Gets the correspoding device + if((device = maccoreaudio_getDevice(deviceUID)) == kAudioObjectUnknown) + { + fprintf(stderr, + "maccoreaudio_startStream (coreaudio/device.c): \ + \n\tgetDevice\n"); + return NULL; + } + + // Init the stream structure. + maccoreaudio_stream * stream; + if((stream = (maccoreaudio_stream*) malloc(sizeof(maccoreaudio_stream))) + == NULL) + { + perror("maccoreaudio_startStream (coreaudio/device.c): \ + \n\tmalloc\n"); + return NULL; + } + stream->ioProcId = NULL; + stream->callbackFunction = callbackFunction; + stream->callbackObject = callbackObject; + stream->callbackMethod = callbackMethod; + + AudioStreamBasicDescription javaFormat; + FillOutASBDForLPCM( + &javaFormat, + sampleRate, + nbChannels, + bitsPerChannel, + bitsPerChannel, + isFloat, + isBigEndian, + isNonInterleaved); + if(maccoreaudio_initConverter( + deviceUID, + &javaFormat, + isJavaFormatSource, + &stream->converter, + &stream->conversionRatio) + != noErr) + { + free(stream); + fprintf(stderr, + "maccoreaudio_startStream (coreaudio/device.c): \ + \n\tmaccoreaudio_initConverter\n"); + return NULL; + } + + // register the IOProc + if(AudioDeviceCreateIOProcID( + device, + readWriteFunction, + stream, + &stream->ioProcId) != noErr) + { + free(stream); + fprintf(stderr, + "maccoreaudio_startStream (coreaudio/device.c): \ + \n\tAudioDeviceIOProcID\n"); + return NULL; + } + + // start IO + AudioDeviceStart(device, stream->ioProcId); + + return stream; +} + +void maccoreaudio_stopStream( + const char * deviceUID, + maccoreaudio_stream * stream) +{ + AudioDeviceID device; + + // Gets the correspoding device + if((device = maccoreaudio_getDevice(deviceUID)) == kAudioObjectUnknown) + { + fprintf(stderr, + "maccoreaudio_stopStream (coreaudio/device.c): \ + \n\tgetDevice: %s\n", + deviceUID); + fflush(stderr); + return; + } + + // stop IO + AudioDeviceStop(device, stream->ioProcId); + + // unregister the IOProc + AudioDeviceDestroyIOProcID(device, stream->ioProcId); + + AudioConverterDispose(stream->converter); + + free(stream); +} + +OSStatus maccoreaudio_readInputStream( + AudioDeviceID inDevice, + const AudioTimeStamp* inNow, + const AudioBufferList* inInputData, + const AudioTimeStamp* inInputTime, + AudioBufferList* outOutputData, + const AudioTimeStamp* inOutputTime, + void* inClientData) +{ + OSStatus err = noErr; + maccoreaudio_stream * stream = (maccoreaudio_stream*) inClientData; + void (*callbackFunction) (char*, int, void*, void*) + = stream->callbackFunction; + UInt32 tmpLength + = inInputData->mBuffers[0].mDataByteSize * stream->conversionRatio; + char tmpBuffer[tmpLength]; + int i; + for(i = 0; i < inInputData->mNumberBuffers; ++i) + { + if(inInputData->mBuffers[i].mData != NULL + && inInputData->mBuffers[i].mDataByteSize > 0) + { + if((err = AudioConverterConvertBuffer( + stream->converter, + inInputData->mBuffers[i].mDataByteSize, + inInputData->mBuffers[i].mData, + &tmpLength, + tmpBuffer)) + != noErr) + { + fprintf(stderr, + "maccoreaudio_readInputStream (coreaudio/device.c): \ + \n\tAudioConverterConvertBuffer: %x\n", + (int) err); + fflush(stderr); + return err; + } + + callbackFunction( + tmpBuffer, + tmpLength, + stream->callbackObject, + stream->callbackMethod); + } + } + + return noErr; +} + +OSStatus maccoreaudio_writeOutputStream( + AudioDeviceID inDevice, + const AudioTimeStamp* inNow, + const AudioBufferList* inInputData, + const AudioTimeStamp* inInputTime, + AudioBufferList* outOutputData, + const AudioTimeStamp* inOutputTime, + void* inClientData) +{ + OSStatus err = noErr; + + maccoreaudio_stream * stream = (maccoreaudio_stream*) inClientData; + void (*callbackFunction) (char*, int, void*, void*) + = stream->callbackFunction; + + if(outOutputData->mNumberBuffers == 0) + { + return err; + } + + int tmpLength + = outOutputData->mBuffers[0].mDataByteSize * stream->conversionRatio; + char tmpBuffer[tmpLength]; + + callbackFunction( + tmpBuffer, + tmpLength, + stream->callbackObject, + stream->callbackMethod); + + if((err = AudioConverterConvertBuffer( + stream->converter, + tmpLength, + tmpBuffer, + &outOutputData->mBuffers[0].mDataByteSize, + outOutputData->mBuffers[0].mData)) + != noErr) + { + fprintf(stderr, + "maccoreaudio_writeOutputStream (coreaudio/device.c): \ + \n\tAudioConverterConvertBuffer\n"); + fflush(stderr); + return err; + } + + // Copies the same data into the other buffers. + int i; + UInt32 length; + for(i = 1; i < outOutputData->mNumberBuffers; ++i) + { + // Copies available data. + length = outOutputData->mBuffers[i].mDataByteSize; + if(length > outOutputData->mBuffers[0].mDataByteSize) + { + length = outOutputData->mBuffers[0].mDataByteSize; + } + memcpy( + outOutputData->mBuffers[i].mData, + outOutputData->mBuffers[0].mData, + length); + + // Resets the resting buffer. + if(outOutputData->mBuffers[i].mDataByteSize + > outOutputData->mBuffers[0].mDataByteSize) + { + memset( + outOutputData->mBuffers[i].mData + + outOutputData->mBuffers[0].mDataByteSize, + 0, + outOutputData->mBuffers[i].mDataByteSize + - outOutputData->mBuffers[0].mDataByteSize); + } + } + + return noErr; +} + +/** + * Returns the stream virtual format for a given stream. + * + * @param stream The stream to get the format. + * @param format The variable to write the forat into. + * + * @return noErr if everything works fine. Any other value if failed. + */ +OSStatus maccoreaudio_getStreamVirtualFormat( + AudioStreamID stream, + AudioStreamBasicDescription * format) +{ + // Gets the audio format of the stream. + OSStatus err = noErr; + UInt32 size = sizeof(AudioStreamBasicDescription); + AudioObjectPropertyAddress address; + address.mSelector = kAudioStreamPropertyVirtualFormat; + address.mScope = kAudioObjectPropertyScopeGlobal; + address.mElement = kAudioObjectPropertyElementMaster; + if((err = AudioObjectGetPropertyData( + stream, + &address, + 0, + NULL, + &size, + format)) + != noErr) + { + fprintf(stderr, + "maccoreaudio_getStreamVirtualFormat (coreaudio/device.c): \ + \n\tAudioObjectGetPropertyData, err: 0x%x\n", + ((int) err)); + fflush(stderr); + return err; + } + + return err; +} + +/** + * Initializes a new audio converter to work between the given device and the + * format description. + * + * @param deviceUID The device identifier. + * @param javaFormat The format needed by the upper layer Java aplication. + * @param isJavaFormatSource True if the Java format is the source of this + * converter and the device the ouput. False otherwise. + * @param converter A pointer to the converter used to store the new created + * converter. + * + * @return noErr if everything works correctly. Any other vlue otherwise. + */ +OSStatus maccoreaudio_initConverter( + const char * deviceUID, + const AudioStreamBasicDescription * javaFormat, + unsigned char isJavaFormatSource, + AudioConverterRef * converter, + double * conversionRatio) +{ + AudioDeviceID device; + OSStatus err = noErr; + AudioObjectPropertyAddress address; + + // Gets the correspoding device + if((device = maccoreaudio_getDevice(deviceUID)) == kAudioObjectUnknown) + { + fprintf(stderr, + "maccoreaudio_initConverter (coreaudio/device.c): \ + \n\tgetDevice\n"); + fflush(stderr); + return kAudioObjectUnknown; + } + + AudioStreamBasicDescription deviceFormat; + AudioStreamID audioStreamIds[1]; + UInt32 size = sizeof(AudioStreamID *); + address.mSelector = kAudioDevicePropertyStreams; + address.mScope = kAudioObjectPropertyScopeGlobal; + address.mElement = kAudioObjectPropertyElementMaster; + if((err = AudioObjectGetPropertyData( + device, + &address, + 0, + NULL, + &size, + &audioStreamIds)) + != noErr) + { + fprintf(stderr, + "maccoreaudio_countChannels (coreaudio/device.c): \ + \n\tAudioObjectGetPropertyData, err: 0x%x\n", + ((int) err)); + fflush(stderr); + return err; + } + + if((err = maccoreaudio_getStreamVirtualFormat( + audioStreamIds[0], + &deviceFormat)) + != noErr) + { + fprintf(stderr, + "maccoreaudio_countChannels (coreaudio/device.c): \ + \n\tmaccoreaudiogetStreamVirtualFormat, err: 0x%x\n", + ((int) err)); + fflush(stderr); + return err; + } + + const AudioStreamBasicDescription *inFormat = javaFormat; + const AudioStreamBasicDescription *outFormat = &deviceFormat; + if(!isJavaFormatSource) + { + inFormat = &deviceFormat; + outFormat = javaFormat; + } + + if((err = AudioConverterNew(inFormat, outFormat, converter)) + != noErr) + { + fprintf(stderr, + "maccoreaudio_countChannels (coreaudio/device.c): \ + \n\tAudioConverterNew, err: 0x%x\n", + ((int) err)); + fflush(stderr); + return err; + } + + *conversionRatio = + ((double) javaFormat->mBytesPerFrame) + / ((double) deviceFormat.mBytesPerFrame) + * javaFormat->mSampleRate / deviceFormat.mSampleRate; + + return err; +} + +/** + * Computes the value for the audio stream basic description mFormatFlags + * field for linear PCM data. This function does not support specifying sample + * formats that are either unsigned integer or low-aligned. + * + * @param inValidBitsPerChannel The number of valid bits in each sample. + * @param inTotalBitsPerChannel The total number of bits in each sample. + * @param inIsFloat Use true if the samples are represented with floating point + numbers. + * @param inIsBigEndian Use true if the samples are big endian. + * @param inIsNonInterleaved Use true if the samples are noninterleaved. + * + * @return A UInt32 value containing the calculated format flags. + */ +inline UInt32 CalculateLPCMFlags ( + UInt32 inValidBitsPerChannel, + UInt32 inTotalBitsPerChannel, + bool inIsFloat, + bool inIsBigEndian, + bool inIsNonInterleaved) +{ + return + (inIsFloat ? kAudioFormatFlagIsFloat : kAudioFormatFlagIsSignedInteger) + | (inIsBigEndian ? ((UInt32)kAudioFormatFlagIsBigEndian) : 0) + | ((!inIsFloat && (inValidBitsPerChannel == inTotalBitsPerChannel)) ? + kAudioFormatFlagIsPacked : kAudioFormatFlagIsAlignedHigh) + | (inIsNonInterleaved ? ((UInt32)kAudioFormatFlagIsNonInterleaved) : 0); +} + +/** + * Fills AudioStreamBasicDescription information. + * + * @param outASBD On output, a filled-out AudioStreamBasicDescription structure. + * @param inSampleRate The number of sample frames per second of the data in the + * stream. + * @param inChannelsPerFrame The number of channels in each frame of data. + * @param inValidBitsPerChannel The number of valid bits in each sample. + * @param inTotalBitsPerChannel The total number of bits in each sample. + * @param inIsFloat Use true if the samples are represented as floating-point + * numbers. + * @param inIsBigEndian Use true if the samples are big endian. + * @param inIsNonInterleaved Use true if the samples are noninterleaved. + */ +inline void FillOutASBDForLPCM( + AudioStreamBasicDescription * outASBD, + Float64 inSampleRate, + UInt32 inChannelsPerFrame, + UInt32 inValidBitsPerChannel, + UInt32 inTotalBitsPerChannel, + bool inIsFloat, + bool inIsBigEndian, + bool inIsNonInterleaved) +{ + outASBD->mSampleRate = inSampleRate; + outASBD->mFormatID = kAudioFormatLinearPCM; + outASBD->mFormatFlags = CalculateLPCMFlags( + inValidBitsPerChannel, + inTotalBitsPerChannel, + inIsFloat, + inIsBigEndian, + inIsNonInterleaved); + outASBD->mBytesPerPacket = + (inIsNonInterleaved ? 1 : inChannelsPerFrame) * + (inTotalBitsPerChannel/8); + outASBD->mFramesPerPacket = 1; + outASBD->mBytesPerFrame = + (inIsNonInterleaved ? 1 : inChannelsPerFrame) * + (inTotalBitsPerChannel/8); + outASBD->mChannelsPerFrame = inChannelsPerFrame; + outASBD->mBitsPerChannel = inValidBitsPerChannel; +} diff --git a/src/native/macosx/coreaudio/lib/device.h b/src/native/macosx/coreaudio/lib/device.h index 3a78e844..b13fc50f 100644 --- a/src/native/macosx/coreaudio/lib/device.h +++ b/src/native/macosx/coreaudio/lib/device.h @@ -7,6 +7,7 @@ #ifndef device_h #define device_h +#include <AudioToolbox/AudioConverter.h> #include <CoreAudio/CoreAudio.h> #include <CoreFoundation/CFString.h> #include <stdio.h> @@ -17,30 +18,105 @@ * * @author Vincent Lucas */ -int initDevices(void); +typedef struct +{ + AudioDeviceIOProcID ioProcId; + void* callbackFunction; + void* callbackObject; + void* callbackMethod; + AudioConverterRef converter; + double conversionRatio; +} maccoreaudio_stream; -void freeDevices(void); +int maccoreaudio_initDevices( + void); -AudioDeviceID getDevice( +void maccoreaudio_freeDevices( + void); + +int maccoreaudio_isInputDevice( + const char * deviceUID); + +int maccoreaudio_isOutputDevice( const char * deviceUID); -char* getDeviceName( +char* maccoreaudio_getDeviceName( const char * deviceUID); -char* getDeviceModelIdentifier( +char* maccoreaudio_getDeviceModelIdentifier( const char * deviceUID); -OSStatus setInputDeviceVolume( +OSStatus maccoreaudio_setInputDeviceVolume( const char * deviceUID, Float32 volume); -OSStatus setOutputDeviceVolume( +OSStatus maccoreaudio_setOutputDeviceVolume( const char * deviceUID, Float32 volume); -Float32 getInputDeviceVolume( +Float32 maccoreaudio_getInputDeviceVolume( const char * deviceUID); -Float32 getOutputDeviceVolume( +Float32 maccoreaudio_getOutputDeviceVolume( const char * deviceUID); + +int maccoreaudio_getDeviceUIDList( + char *** deviceUIDList); + +const char* maccoreaudio_getTransportType( + const char * deviceUID); + +Float64 maccoreaudio_getNominalSampleRate( + const char * deviceUID); + +OSStatus maccoreaudio_getAvailableNominalSampleRates( + const char * deviceUID, + Float64 * minRate, + Float64 * maxRate); + +char* maccoreaudio_getDefaultInputDeviceUID( + void); + +char* maccoreaudio_getDefaultOutputDeviceUID( + void); + +int maccoreaudio_countInputChannels( + const char * deviceUID); + +int maccoreaudio_countOutputChannels( + const char * deviceUID); + +maccoreaudio_stream * maccoreaudio_startInputStream( + const char * deviceUID, + void* callbackFunction, + void* callbackObject, + void* callbackMethod, + float sampleRate, + UInt32 nbChannels, + UInt32 bitsPerChannel, + unsigned char isFloat, + unsigned char isBigEndian, + unsigned char isNonInterleaved); + +maccoreaudio_stream * maccoreaudio_startOutputStream( + const char * deviceUID, + void* callbackFunction, + void* callbackObject, + void* callbackMethod, + float sampleRate, + UInt32 nbChannels, + UInt32 bitsPerChannel, + unsigned char isFloat, + unsigned char isBigEndian, + unsigned char isNonInterleaved); + +void maccoreaudio_stopStream( + const char * deviceUID, + maccoreaudio_stream * stream); + +void maccoreaudio_initializeHotplug( + void* callbackFunction); + +void maccoreaudio_uninitializeHotplug(); + #endif diff --git a/src/org/jitsi/impl/neomedia/CoreAudioDevice.java b/src/org/jitsi/impl/neomedia/CoreAudioDevice.java index 07230e01..ae2d56b4 100644 --- a/src/org/jitsi/impl/neomedia/CoreAudioDevice.java +++ b/src/org/jitsi/impl/neomedia/CoreAudioDevice.java @@ -108,4 +108,28 @@ public static native int setInputDeviceVolume( public static native int setOutputDeviceVolume( String deviceUID, float volume); + + private static Runnable devicesChangedCallback; + + /** + * Implements a callback which gets called by the native coreaudio + * counterpart to notify the Java counterpart that the list of devices has + * changed. + */ + public static void devicesChangedCallback() + { + Runnable devicesChangedCallback + = CoreAudioDevice.devicesChangedCallback; + + if(devicesChangedCallback != null) + { + devicesChangedCallback.run(); + } + } + + public static void setDevicesChangedCallback( + Runnable devicesChangedCallback) + { + CoreAudioDevice.devicesChangedCallback = devicesChangedCallback; + } } diff --git a/src/org/jitsi/impl/neomedia/MacCoreAudioDevice.java b/src/org/jitsi/impl/neomedia/MacCoreAudioDevice.java new file mode 100644 index 00000000..530ce28f --- /dev/null +++ b/src/org/jitsi/impl/neomedia/MacCoreAudioDevice.java @@ -0,0 +1,96 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package org.jitsi.impl.neomedia; + +import org.jitsi.util.*; + +/** + * Extension for the JNI link to the MacOsX CoreAudio library. + * + * @author Vincent Lucas + */ +public class MacCoreAudioDevice + extends CoreAudioDevice +{ + /** + * The number of milliseconds to be read from or written to a native + * CoreAudio stream in a single transfer of data. + */ + public static final int DEFAULT_MILLIS_PER_BUFFER = 20; + + /** + * The default value for the sample rate of the input and the output + * MacCoreaudio streams with which they are to be opened if no other + * specific sample rate is specified to the MacCoreaudio <tt>DataSource</tt> + * or <tt>MacCoreaudioRenderer</tt> that they represent. + */ + public static final double DEFAULT_SAMPLE_RATE = 44100.0; + + public static native String[] getDeviceUIDList(); + + public static native boolean isInputDevice(String deviceUID); + + public static native boolean isOutputDevice(String deviceUID); + + public static String getTransportType(String deviceUID) + { + // Prevent an access violation. + if (deviceUID == null) + throw new NullPointerException("deviceUID"); + + byte[] transportTypeBytes = getTransportTypeBytes(deviceUID); + String transportType = StringUtils.newString(transportTypeBytes); + + return transportType; + } + + public static native byte[] getTransportTypeBytes(String deviceUID); + + public static native float getNominalSampleRate(String deviceUID); + + public static native float getMinimalNominalSampleRate(String deviceUID); + + public static native float getMaximalNominalSampleRate(String deviceUID); + + public static String getDefaultInputDeviceUID() + { + byte[] defaultInputDeviceUIDBytes = getDefaultInputDeviceUIDBytes(); + String defaultInputDeviceUID + = StringUtils.newString(defaultInputDeviceUIDBytes); + + return defaultInputDeviceUID; + } + + public static native byte[] getDefaultInputDeviceUIDBytes(); + + public static String getDefaultOutputDeviceUID() + { + byte[] defaultOutputDeviceUIDBytes = getDefaultOutputDeviceUIDBytes(); + String defaultOutputDeviceUID + = StringUtils.newString(defaultOutputDeviceUIDBytes); + + return defaultOutputDeviceUID; + } + + public static native byte[] getDefaultOutputDeviceUIDBytes(); + + public static native long startStream( + String deviceUID, + Object callback, + float sampleRate, + int nbChannels, + int bitsPerChannel, + boolean isFloat, + boolean isBigEndian, + boolean isNonInterleaved); + + public static native void stopStream(String deviceUID, long stream); + + public static native int countInputChannels(String deviceUID); + + public static native int countOutputChannels(String deviceUID); +} diff --git a/src/org/jitsi/impl/neomedia/device/AudioSystem.java b/src/org/jitsi/impl/neomedia/device/AudioSystem.java index bc3a9d78..962a1cd2 100644 --- a/src/org/jitsi/impl/neomedia/device/AudioSystem.java +++ b/src/org/jitsi/impl/neomedia/device/AudioSystem.java @@ -78,6 +78,12 @@ public enum DataFlow public static final String LOCATOR_PROTOCOL_JAVASOUND = "javasound"; + /** + * The protocol of the <tt>MediaLocator</tt>s identifying + * <tt>CaptureDeviceInfo</tt>s contributed by <tt>MacCoreaudioSystem</tt>. + */ + public static final String LOCATOR_PROTOCOL_MACCOREAUDIO = "maccoreaudio"; + public static final String LOCATOR_PROTOCOL_OPENSLES = "opensles"; public static final String LOCATOR_PROTOCOL_PORTAUDIO = "portaudio"; diff --git a/src/org/jitsi/impl/neomedia/device/DeviceSystem.java b/src/org/jitsi/impl/neomedia/device/DeviceSystem.java index 32751a37..f228d397 100644 --- a/src/org/jitsi/impl/neomedia/device/DeviceSystem.java +++ b/src/org/jitsi/impl/neomedia/device/DeviceSystem.java @@ -205,6 +205,7 @@ public static void initializeDeviceSystems(MediaType mediaType) : null, OSUtils.IS_WINDOWS ? ".WASAPISystem" : null, OSUtils.IS_ANDROID ? null : ".PortAudioSystem", + OSUtils.IS_MAC ? ".MacCoreaudioSystem" : null, ".NoneAudioSystem" }; break; diff --git a/src/org/jitsi/impl/neomedia/device/MacCoreaudioSystem.java b/src/org/jitsi/impl/neomedia/device/MacCoreaudioSystem.java new file mode 100644 index 00000000..484c5ae6 --- /dev/null +++ b/src/org/jitsi/impl/neomedia/device/MacCoreaudioSystem.java @@ -0,0 +1,785 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package org.jitsi.impl.neomedia.device; + +import java.lang.ref.*; +import java.util.*; +import java.util.regex.*; + +import javax.media.*; +import javax.media.format.*; + +import org.jitsi.impl.neomedia.*; +import org.jitsi.impl.neomedia.control.*; +import org.jitsi.impl.neomedia.jmfext.media.renderer.audio.*; +import org.jitsi.util.*; + +/** + * Creates MacCoreaudio capture devices by enumerating all host devices that + * have input channels. + * + * @author Vincent Lucas + */ +public class MacCoreaudioSystem + extends AudioSystem +{ + /** + * Represents a listener which is to be notified before and after + * MacCoreaudio's native function <tt>UpdateAvailableDeviceList()</tt> is + * invoked. + */ + public interface UpdateAvailableDeviceListListener + extends EventListener + { + /** + * Notifies this listener that MacCoreaudio's native function + * <tt>UpdateAvailableDeviceList()</tt> was invoked. + * + * @throws Exception if this implementation encounters an error. Any + * <tt>Throwable</tt> apart from <tt>ThreadDeath</tt> will be ignored + * after it is logged for debugging purposes. + */ + void didUpdateAvailableDeviceList() + throws Exception; + + /** + * Notifies this listener that MacCoreaudio's native function + * <tt>UpdateAvailableDeviceList()</tt> will be invoked. + * + * @throws Exception if this implementation encounters an error. Any + * <tt>Throwable</tt> apart from <tt>ThreadDeath</tt> will be ignored + * after it is logged for debugging purposes. + */ + void willUpdateAvailableDeviceList() + throws Exception; + } + + /** + * The protocol of the <tt>MediaLocator</tt>s identifying MacCoreaudio + * <tt>CaptureDevice</tt>s + */ + private static final String LOCATOR_PROTOCOL + = LOCATOR_PROTOCOL_MACCOREAUDIO; + + /** + * The <tt>Logger</tt> used by the <tt>MacCoreaudioSystem</tt> class and its + * instances for logging output. + */ + private static final Logger logger + = Logger.getLogger(MacCoreaudioSystem.class); + + /** + * The number of times that {@link #willPaOpenStream()} has been + * invoked without an intervening {@link #didPaOpenStream()} i.e. the + * number of MacCoreaudio clients which are currently executing + * <tt>Pa_OpenStream</tt> and which are thus inhibiting + * <tt>Pa_UpdateAvailableDeviceList</tt>. + */ + private static int openStream = 0; + + /** + * The <tt>Object</tt> which synchronizes that access to + * {@link #paOpenStream} and {@link #updateAvailableDeviceList}. + */ + private static final Object openStreamSyncRoot = new Object(); + + /** + * The number of times that {@link #willPaUpdateAvailableDeviceList()} + * has been invoked without an intervening + * {@link #didPaUpdateAvailableDeviceList()} i.e. the number of + * MacCoreaudio clients which are currently executing + * <tt>Pa_UpdateAvailableDeviceList</tt> and which are thus inhibiting + * <tt>Pa_OpenStream</tt>. + */ + private static int updateAvailableDeviceList = 0; + + /** + * The list of <tt>PaUpdateAvailableDeviceListListener</tt>s which are to be + * notified before and after MacCoreaudio's native function + * <tt>Pa_UpdateAvailableDeviceList()</tt> is invoked. + */ + private static final List<WeakReference<UpdateAvailableDeviceListListener>> + updateAvailableDeviceListListeners + = new LinkedList<WeakReference<UpdateAvailableDeviceListListener>>(); + + /** + * The <tt>Object</tt> which ensures that MacCoreaudio's native function + * <tt>UpdateAvailableDeviceList()</tt> will not be invoked concurrently. + * The condition should hold true on the native side but, anyway, it shoul + * not hurt (much) to enforce it on the Java side as well. + */ + private static final Object updateAvailableDeviceListSyncRoot + = new Object(); + + /** + * Adds a listener which is to be notified before and after MacCoreaudio's + * native function <tt>UpdateAvailableDeviceList()</tt> is invoked. + * <p> + * <b>Note</b>: The <tt>MacCoreaudioSystem</tt> class keeps a + * <tt>WeakReference</tt> to the specified <tt>listener</tt> in order to + * avoid memory leaks. + * </p> + * + * @param listener the <tt>UpdateAvailableDeviceListListener</tt> to be + * notified before and after MacCoreaudio's native function + * <tt>UpdateAvailableDeviceList()</tt> is invoked + */ + public static void addUpdateAvailableDeviceListListener( + UpdateAvailableDeviceListListener listener) + { + if(listener == null) + throw new NullPointerException("listener"); + + synchronized(updateAvailableDeviceListListeners) + { + Iterator<WeakReference<UpdateAvailableDeviceListListener>> i + = updateAvailableDeviceListListeners.iterator(); + boolean add = true; + + while(i.hasNext()) + { + UpdateAvailableDeviceListListener l = i.next().get(); + + if(l == null) + i.remove(); + else if(l.equals(listener)) + add = false; + } + if(add) + { + updateAvailableDeviceListListeners.add( + new WeakReference<UpdateAvailableDeviceListListener>( + listener)); + } + } + } + + /** + * Notifies <tt>MacCoreaudioSystem</tt> that a MacCoreaudio client finished + * executing <tt>OpenStream</tt>. + */ + public static void didOpenStream() + { + synchronized (openStreamSyncRoot) + { + openStream--; + if (openStream < 0) + openStream = 0; + + openStreamSyncRoot.notifyAll(); + } + } + + /** + * Notifies <tt>MacCoreaudioSystem</tt> that a MacCoreaudio client finished + * executing <tt>UpdateAvailableDeviceList</tt>. + */ + private static void didUpdateAvailableDeviceList() + { + synchronized(openStreamSyncRoot) + { + updateAvailableDeviceList--; + if (updateAvailableDeviceList < 0) + updateAvailableDeviceList = 0; + + openStreamSyncRoot.notifyAll(); + } + + fireUpdateAvailableDeviceListEvent(false); + } + + /** + * Notifies the registered <tt>UpdateAvailableDeviceListListener</tt>s + * that MacCoreaudio's native function + * <tt>UpdateAvailableDeviceList()</tt> will be or was invoked. + * + * @param will <tt>true</tt> if MacCoreaudio's native function + * <tt>UpdateAvailableDeviceList()</tt> will be invoked or <tt>false</tt> + * if it was invoked + */ + private static void fireUpdateAvailableDeviceListEvent(boolean will) + { + try + { + List<WeakReference<UpdateAvailableDeviceListListener>> ls; + + synchronized(updateAvailableDeviceListListeners) + { + ls = new + ArrayList<WeakReference<UpdateAvailableDeviceListListener>>( + updateAvailableDeviceListListeners); + } + + for(WeakReference<UpdateAvailableDeviceListListener> wr : ls) + { + UpdateAvailableDeviceListListener l = wr.get(); + if(l != null) + { + try + { + if(will) + l.willUpdateAvailableDeviceList(); + else + l.didUpdateAvailableDeviceList(); + } + catch (Throwable t) + { + if(t instanceof ThreadDeath) + throw(ThreadDeath) t; + else + { + logger.error( + "UpdateAvailableDeviceListListener." + + (will ? "will" : "did") + + "UpdateAvailableDeviceList failed.", + t); + } + } + } + } + } + catch(Throwable t) + { + if(t instanceof ThreadDeath) + throw(ThreadDeath) t; + } + } + + /** + * Gets a sample rate supported by a MacCoreaudio device with a specific + * device index with which it is to be registered with JMF. + * + * @param input <tt>true</tt> if the supported sample rate is to be + * retrieved for the MacCoreaudio device with the specified device index as + * an input device or <tt>false</tt> for an output device + * @param deviceUID The device identifier. + * @param channelCount number of channel + * @param sampleFormat sample format + * + * @return a sample rate supported by the MacCoreaudio device with the + * specified device index with which it is to be registered with JMF + */ + private static double getSupportedSampleRate( + boolean input, + String deviceUID) + { + double supportedSampleRate = MacCoreAudioDevice.DEFAULT_SAMPLE_RATE; + double defaultSampleRate + = MacCoreAudioDevice.getNominalSampleRate(deviceUID); + + if (defaultSampleRate >= MediaUtils.MAX_AUDIO_SAMPLE_RATE) + { + supportedSampleRate = defaultSampleRate; + } + + return supportedSampleRate; + } + + /** + * Waits for all MacCoreaudio clients to finish executing + * <tt>OpenStream</tt>. + */ + private static void waitForOpenStream() + { + boolean interrupted = false; + + while(openStream > 0) + { + try + { + openStreamSyncRoot.wait(); + } + catch(InterruptedException ie) + { + interrupted = true; + } + } + if(interrupted) + Thread.currentThread().interrupt(); + } + + /** + * Waits for all MacCoreaudio clients to finish executing + * <tt>UpdateAvailableDeviceList</tt>. + */ + private static void waitForUpdateAvailableDeviceList() + { + boolean interrupted = false; + + while (updateAvailableDeviceList > 0) + { + try + { + openStreamSyncRoot.wait(); + } + catch (InterruptedException ie) + { + interrupted = true; + } + } + if (interrupted) + Thread.currentThread().interrupt(); + } + + /** + * Notifies <tt>MacCoreaudioSystem</tt> that a MacCoreaudio client will + * start executing <tt>OpenStream</tt>. + */ + public static void willOpenStream() + { + synchronized (openStreamSyncRoot) + { + waitForUpdateAvailableDeviceList(); + + openStream++; + openStreamSyncRoot.notifyAll(); + } + } + + /** + * Notifies <tt>MacCoreaudioSystem</tt> that a MacCoreaudio client will + * start executing <tt>UpdateAvailableDeviceList</tt>. + */ + private static void willUpdateAvailableDeviceList() + { + synchronized(openStreamSyncRoot) + { + waitForOpenStream(); + + updateAvailableDeviceList++; + openStreamSyncRoot.notifyAll(); + } + + fireUpdateAvailableDeviceListEvent(true); + } + + private Runnable devicesChangedCallback; + + /** + * Initializes a new <tt>MacCoreaudioSystem</tt> instance which creates + * MacCoreaudio capture and playback devices by enumerating all host devices + * with input channels. + * + * @throws Exception if anything wrong happens while creating the + * MacCoreaudio capture and playback devices + */ + MacCoreaudioSystem() + throws Exception + { + super( + LOCATOR_PROTOCOL, + 0 | FEATURE_NOTIFY_AND_PLAYBACK_DEVICES | FEATURE_REINITIALIZE); + } + + /** + * Sorts a specific list of <tt>CaptureDeviceInfo2</tt>s so that the + * ones representing USB devices appear at the beginning/top of the + * specified list. + * + * @param devices the list of <tt>CaptureDeviceInfo2</tt>s to be + * sorted so that the ones representing USB devices appear at the + * beginning/top of the list + */ + private void bubbleUpUsbDevices(List<CaptureDeviceInfo2> devices) + { + if(!devices.isEmpty()) + { + List<CaptureDeviceInfo2> nonUsbDevices + = new ArrayList<CaptureDeviceInfo2>(devices.size()); + + for(Iterator<CaptureDeviceInfo2> i = devices.iterator(); + i.hasNext();) + { + CaptureDeviceInfo2 d = i.next(); + + if(!d.isSameTransportType("USB")) + { + nonUsbDevices.add(d); + i.remove(); + } + } + if(!nonUsbDevices.isEmpty()) + { + for (CaptureDeviceInfo2 d : nonUsbDevices) + devices.add(d); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void doInitialize() + throws Exception + { + if(!CoreAudioDevice.isLoaded) + { + String message = "MacOSX CoreAudio library is not loaded"; + if (logger.isInfoEnabled()) + { + logger.info(message); + } + throw new Exception(message); + } + + // Initializes the library only at the first run. + if(devicesChangedCallback == null) + { + CoreAudioDevice.initDevices(); + } + + int channels = 1; + int sampleSizeInBits = 16; + String defaultInputdeviceUID + = MacCoreAudioDevice.getDefaultInputDeviceUID(); + String defaultOutputdeviceUID + = MacCoreAudioDevice.getDefaultOutputDeviceUID(); + List<CaptureDeviceInfo2> captureAndPlaybackDevices + = new LinkedList<CaptureDeviceInfo2>(); + List<CaptureDeviceInfo2> captureDevices + = new LinkedList<CaptureDeviceInfo2>(); + List<CaptureDeviceInfo2> playbackDevices + = new LinkedList<CaptureDeviceInfo2>(); + final boolean loggerIsDebugEnabled = logger.isDebugEnabled(); + + + String[] deviceUIDList = MacCoreAudioDevice.getDeviceUIDList(); + for(int i = 0; i < deviceUIDList.length; ++i) + { + String deviceUID = deviceUIDList[i]; + String name = CoreAudioDevice.getDeviceName(deviceUID); + boolean isInputDevice = MacCoreAudioDevice.isInputDevice(deviceUID); + boolean isOutputDevice + = MacCoreAudioDevice.isOutputDevice(deviceUID); + String transportType + = MacCoreAudioDevice.getTransportType(deviceUID); + String modelIdentifier = null; + String locatorRemainder = name; + + if (deviceUID != null) + { + modelIdentifier + = CoreAudioDevice.getDeviceModelIdentifier(deviceUID); + locatorRemainder = deviceUID; + } + + /* + * TODO The intention of reinitialize() was to perform the + * initialization from scratch. However, AudioSystem was later + * changed to disobey. But we should at least search through both + * CAPTURE_INDEX and PLAYBACK_INDEX. + */ + List<CaptureDeviceInfo2> existingCdis + = getDevices(DataFlow.CAPTURE); + CaptureDeviceInfo2 cdi = null; + + if (existingCdis != null) + { + for (CaptureDeviceInfo2 existingCdi : existingCdis) + { + /* + * The deviceUID is optional so a device may be identified + * by deviceUID if it is available or by name if the + * deviceUID is not available. + */ + String id = existingCdi.getIdentifier(); + + if (id.equals(deviceUID) || id.equals(name)) + { + cdi = existingCdi; + break; + } + } + } + + if (cdi == null) + { + cdi + = new CaptureDeviceInfo2( + name, + new MediaLocator( + LOCATOR_PROTOCOL + ":#" + locatorRemainder), + new Format[] + { + new AudioFormat( + AudioFormat.LINEAR, + isInputDevice + ? getSupportedSampleRate( + true, + deviceUID) + : MacCoreAudioDevice.DEFAULT_SAMPLE_RATE, + sampleSizeInBits, + channels, + AudioFormat.LITTLE_ENDIAN, + AudioFormat.SIGNED, + Format.NOT_SPECIFIED /* frameSizeInBits */, + Format.NOT_SPECIFIED /* frameRate */, + Format.byteArray) + }, + deviceUID, + transportType, + modelIdentifier); + } + + boolean isDefaultInputDevice + = deviceUID.equals(defaultInputdeviceUID); + boolean isDefaultOutputDevice + = deviceUID.equals(defaultOutputdeviceUID); + + /* + * When we perform automatic selection of capture and + * playback/notify devices, we would like to pick up devices from + * one and the same hardware because that sound like a natural + * expectation from the point of view of the user. In order to + * achieve that, we will bring the devices which support both + * capture and playback to the top. + */ + if(isInputDevice) + { + List<CaptureDeviceInfo2> devices; + + if(isOutputDevice) + devices = captureAndPlaybackDevices; + else + devices = captureDevices; + + if(isDefaultInputDevice + || (isOutputDevice && isDefaultOutputDevice)) + { + devices.add(0, cdi); + if (loggerIsDebugEnabled) + logger.debug("Added default capture device: " + name); + } + else + { + devices.add(cdi); + if (loggerIsDebugEnabled) + logger.debug("Added capture device: " + name); + } + + if(loggerIsDebugEnabled && isInputDevice) + { + if(isDefaultOutputDevice) + logger.debug("Added default playback device: " + name); + else + logger.debug("Added playback device: " + name); + } + } + else if(isOutputDevice) + { + if(isDefaultOutputDevice) + { + playbackDevices.add(0, cdi); + if (loggerIsDebugEnabled) + logger.debug("Added default playback device: " + name); + } + else + { + playbackDevices.add(cdi); + if (loggerIsDebugEnabled) + logger.debug("Added playback device: " + name); + } + } + } + + /* + * Make sure that devices which support both capture and playback are + * reported as such and are preferred over devices which support either + * capture or playback (in order to achieve our goal to have automatic + * selection pick up devices from one and the same hardware). + */ + bubbleUpUsbDevices(captureDevices); + bubbleUpUsbDevices(playbackDevices); + if(!captureDevices.isEmpty() && !playbackDevices.isEmpty()) + { + /* + * Event if we have not been provided with the information regarding + * the matching of the capture and playback/notify devices from one + * and the same hardware, we may still be able to deduce it by + * examining their names. + */ + matchDevicesByName(captureDevices, playbackDevices); + } + /* + * Of course, of highest reliability is the fact that a specific + * instance supports both capture and playback. + */ + if(!captureAndPlaybackDevices.isEmpty()) + { + bubbleUpUsbDevices(captureAndPlaybackDevices); + for (int i = captureAndPlaybackDevices.size() - 1; i >= 0; i--) + { + CaptureDeviceInfo2 cdi = captureAndPlaybackDevices.get(i); + + captureDevices.add(0, cdi); + playbackDevices.add(0, cdi); + } + } + + setCaptureDevices(captureDevices); + setPlaybackDevices(playbackDevices); + + if(devicesChangedCallback == null) + { + devicesChangedCallback + = new Runnable() + { + public void run() + { + try + { + reinitialize(); + } + catch (Throwable t) + { + if (t instanceof ThreadDeath) + throw (ThreadDeath) t; + + logger.warn( + "Failed to reinitialize MacCoreaudio devices", + t); + } + } + }; + CoreAudioDevice.setDevicesChangedCallback( + devicesChangedCallback); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected String getRendererClassName() + { + return MacCoreaudioRenderer.class.getName(); + } + + /** + * Attempts to reorder specific lists of capture and playback/notify + * <tt>CaptureDeviceInfo2</tt>s so that devices from the same + * hardware appear at the same indices in the respective lists. The judgment + * with respect to the belonging to the same hardware is based on the names + * of the specified <tt>CaptureDeviceInfo2</tt>s. The implementation + * is provided as a fallback to stand in for scenarios in which more + * accurate relevant information is not available. + * + * @param captureDevices + * @param playbackDevices + */ + private void matchDevicesByName( + List<CaptureDeviceInfo2> captureDevices, + List<CaptureDeviceInfo2> playbackDevices) + { + Iterator<CaptureDeviceInfo2> captureIter + = captureDevices.iterator(); + Pattern pattern + = Pattern.compile( + "array|headphones|microphone|speakers|\\p{Space}|\\(|\\)", + Pattern.CASE_INSENSITIVE); + LinkedList<CaptureDeviceInfo2> captureDevicesWithPlayback + = new LinkedList<CaptureDeviceInfo2>(); + LinkedList<CaptureDeviceInfo2> playbackDevicesWithCapture + = new LinkedList<CaptureDeviceInfo2>(); + int count = 0; + + while (captureIter.hasNext()) + { + CaptureDeviceInfo2 captureDevice = captureIter.next(); + String captureName = captureDevice.getName(); + + if (captureName != null) + { + captureName = pattern.matcher(captureName).replaceAll(""); + if (captureName.length() != 0) + { + Iterator<CaptureDeviceInfo2> playbackIter + = playbackDevices.iterator(); + CaptureDeviceInfo2 matchingPlaybackDevice = null; + + while (playbackIter.hasNext()) + { + CaptureDeviceInfo2 playbackDevice + = playbackIter.next(); + String playbackName = playbackDevice.getName(); + + if (playbackName != null) + { + playbackName + = pattern + .matcher(playbackName) + .replaceAll(""); + if (captureName.equals(playbackName)) + { + playbackIter.remove(); + matchingPlaybackDevice = playbackDevice; + break; + } + } + } + if (matchingPlaybackDevice != null) + { + captureIter.remove(); + captureDevicesWithPlayback.add(captureDevice); + playbackDevicesWithCapture.add( + matchingPlaybackDevice); + count++; + } + } + } + } + + for (int i = count - 1; i >= 0; i--) + { + captureDevices.add(0, captureDevicesWithPlayback.get(i)); + playbackDevices.add(0, playbackDevicesWithCapture.get(i)); + } + } + + /** + * Reinitializes this <tt>MacCoreaudioSystem</tt> in order to bring it up to + * date with possible changes in the MacCoreaudio devices. Invokes + * <tt>Pa_UpdateAvailableDeviceList()</tt> to update the devices on the + * native side and then {@link #initialize()} to reflect any changes on the + * Java side. Invoked by MacCoreaudio when it detects that the list of + * devices has changed. + * + * @throws Exception if there was an error during the invocation of + * <tt>Pa_UpdateAvailableDeviceList()</tt> and + * <tt>DeviceSystem.initialize()</tt> + */ + private void reinitialize() + throws Exception + { + synchronized (updateAvailableDeviceListSyncRoot) + { + willUpdateAvailableDeviceList(); + didUpdateAvailableDeviceList(); + } + + /* + * XXX We will likely minimize the risk of crashes on the native side + * even further by invoking initialize() with + * Pa_UpdateAvailableDeviceList locked. Unfortunately, that will likely + * increase the risks of deadlocks on the Java side. + */ + invokeDeviceSystemInitialize(this); + } + + /** + * {@inheritDoc} + * + * The implementation of <tt>MacCoreaudioSystem</tt> always returns + * "MacCoreaudio". + */ + @Override + public String toString() + { + return "MacCoreaudio"; + } +} diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/maccoreaudio/DataSource.java b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/maccoreaudio/DataSource.java new file mode 100644 index 00000000..c149899f --- /dev/null +++ b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/maccoreaudio/DataSource.java @@ -0,0 +1,251 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package org.jitsi.impl.neomedia.jmfext.media.protocol.maccoreaudio; + +import java.io.*; + +import javax.media.*; +import javax.media.control.*; + +import org.jitsi.impl.neomedia.device.*; +import org.jitsi.impl.neomedia.jmfext.media.protocol.*; +import org.jitsi.util.*; + +/** + * Implements <tt>DataSource</tt> and <tt>CaptureDevice</tt> for MacCoreaudio. + * + * @author Vincent Lucas + */ +public class DataSource + extends AbstractPullBufferCaptureDevice +{ + /** + * The <tt>Logger</tt> used by the <tt>DataSource</tt> class and its + * instances for logging output. + */ + private static final Logger logger = Logger.getLogger(DataSource.class); + + /** + * The indicator which determines whether this <tt>DataSource</tt> will + * use audio quality improvement in accord with the preferences of the user. + */ + private final boolean audioQualityImprovement; + + /** + * The list of <tt>Format</tt>s in which this <tt>DataSource</tt> is + * capable of capturing audio data. + */ + private final Format[] supportedFormats; + + /** + * Initializes a new <tt>DataSource</tt> instance. + */ + public DataSource() + { + this.supportedFormats = null; + this.audioQualityImprovement = true; + } + + /** + * Initializes a new <tt>DataSource</tt> instance from a specific + * <tt>MediaLocator</tt>. + * + * @param locator the <tt>MediaLocator</tt> to create the new instance from + */ + public DataSource(MediaLocator locator) + { + this(locator, null, true); + } + + /** + * Initializes a new <tt>DataSource</tt> instance from a specific + * <tt>MediaLocator</tt> and which has a specific list of <tt>Format</tt> + * in which it is capable of capturing audio data overriding its + * registration with JMF and optionally uses audio quality improvement in + * accord with the preferences of the user. + * + * @param locator the <tt>MediaLocator</tt> to create the new instance from + * @param supportedFormats the list of <tt>Format</tt>s in which the new + * instance is to be capable of capturing audio data + * @param audioQualityImprovement <tt>true</tt> if audio quality improvement + * is to be enabled in accord with the preferences of the user or + * <tt>false</tt> to completely disable audio quality improvement + */ + public DataSource( + MediaLocator locator, + Format[] supportedFormats, + boolean audioQualityImprovement) + { + super(locator); + + this.supportedFormats + = (supportedFormats == null) + ? null + : supportedFormats.clone(); + this.audioQualityImprovement = audioQualityImprovement; + } + + /** + * Creates a new <tt>PullBufferStream</tt> which is to be at a specific + * zero-based index in the list of streams of this + * <tt>PullBufferDataSource</tt>. The <tt>Format</tt>-related information of + * the new instance is to be abstracted by a specific + * <tt>FormatControl</tt>. + * + * @param streamIndex the zero-based index of the <tt>PullBufferStream</tt> + * in the list of streams of this <tt>PullBufferDataSource</tt> + * @param formatControl the <tt>FormatControl</tt> which is to abstract the + * <tt>Format</tt>-related information of the new instance + * @return a new <tt>PullBufferStream</tt> which is to be at the specified + * <tt>streamIndex</tt> in the list of streams of this + * <tt>PullBufferDataSource</tt> and which has its <tt>Format</tt>-related + * information abstracted by the specified <tt>formatControl</tt> + * @see AbstractPullBufferCaptureDevice#createStream(int, FormatControl) + */ + @Override + protected MacCoreaudioStream createStream( + int streamIndex, + FormatControl formatControl) + { + return new MacCoreaudioStream( + this, + formatControl, + audioQualityImprovement); + } + + /** + * Opens a connection to the media source specified by the + * <tt>MediaLocator</tt> of this <tt>DataSource</tt>. + * + * @throws IOException if anything goes wrong while opening the connection + * to the media source specified by the <tt>MediaLocator</tt> of this + * <tt>DataSource</tt> + * @see AbstractPullBufferCaptureDevice#doConnect() + */ + @Override + protected void doConnect() + throws IOException + { + super.doConnect(); + + String deviceID = getDeviceID(); + + synchronized (getStreamSyncRoot()) + { + for (Object stream : getStreams()) + ((MacCoreaudioStream) stream).setDeviceUID(deviceID); + } + } + + /** + * Closes the connection to the media source specified by the + * <tt>MediaLocator</tt> of this <tt>DataSource</tt>. Allows extenders to + * override and be sure that there will be no request to close a connection + * if the connection has not been opened yet. + */ + @Override + protected void doDisconnect() + { + try + { + synchronized (getStreamSyncRoot()) + { + Object[] streams = streams(); + + if (streams != null) + { + for (Object stream : streams) + { + ((MacCoreaudioStream) stream).setDeviceUID(null); + } + } + } + } + finally + { + super.doDisconnect(); + } + } + + /** + * Gets the device index of the MacCoreaudio device identified by the + * <tt>MediaLocator</tt> of this <tt>DataSource</tt>. + * + * @return the device index of a MacCoreaudio device identified by the + * <tt>MediaLocator</tt> of this <tt>DataSource</tt> + * @throws IllegalStateException if there is no <tt>MediaLocator</tt> + * associated with this <tt>DataSource</tt> + */ + private String getDeviceID() + { + MediaLocator locator = getLocator(); + + if (locator == null) + throw new IllegalStateException("locator"); + else + return getDeviceID(locator); + } + + /** + * Gets the device index of a MacCoreaudio device from a specific + * <tt>MediaLocator</tt> identifying it. + * + * @param locator the <tt>MediaLocator</tt> identifying the device index of + * a MacCoreaudio device to get + * @return the device index of a MacCoreaudio device identified by + * <tt>locator</tt> + */ + public static String getDeviceID(MediaLocator locator) + { + if (locator == null) + { + /* + * Explicitly throw a NullPointerException because the implicit one + * does not have a message and is thus a bit more difficult to + * debug. + */ + throw new NullPointerException("locator"); + } + else if (AudioSystem.LOCATOR_PROTOCOL_MACCOREAUDIO.equalsIgnoreCase( + locator.getProtocol())) + { + String remainder = locator.getRemainder(); + + if ((remainder != null) && (remainder.charAt(0) == '#')) + remainder = remainder.substring(1); + return remainder; + } + else + { + throw new IllegalArgumentException("locator.protocol"); + } + } + + /** + * Gets the <tt>Format</tt>s which are to be reported by a + * <tt>FormatControl</tt> as supported formats for a + * <tt>PullBufferStream</tt> at a specific zero-based index in the list of + * streams of this <tt>PullBufferDataSource</tt>. + * + * @param streamIndex the zero-based index of the <tt>PullBufferStream</tt> + * for which the specified <tt>FormatControl</tt> is to report the list of + * supported <tt>Format</tt>s + * @return an array of <tt>Format</tt>s to be reported by a + * <tt>FormatControl</tt> as the supported formats for the + * <tt>PullBufferStream</tt> at the specified <tt>streamIndex</tt> in the + * list of streams of this <tt>PullBufferDataSource</tt> + * @see AbstractPullBufferCaptureDevice#getSupportedFormats(int) + */ + @Override + protected Format[] getSupportedFormats(int streamIndex) + { + return + (supportedFormats == null) + ? super.getSupportedFormats(streamIndex) + : supportedFormats; + } +} diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/maccoreaudio/MacCoreaudioStream.java b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/maccoreaudio/MacCoreaudioStream.java new file mode 100644 index 00000000..40711edb --- /dev/null +++ b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/maccoreaudio/MacCoreaudioStream.java @@ -0,0 +1,440 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package org.jitsi.impl.neomedia.jmfext.media.protocol.maccoreaudio; + +import java.io.*; +import java.util.*; + +import javax.media.*; +import javax.media.control.*; +import javax.media.format.*; + +import org.jitsi.impl.neomedia.*; +import org.jitsi.impl.neomedia.codec.*; +import org.jitsi.impl.neomedia.control.*; +import org.jitsi.impl.neomedia.device.*; +import org.jitsi.impl.neomedia.jmfext.media.protocol.*; +import org.jitsi.service.neomedia.*; +import org.jitsi.util.*; + +/** + * Implements <tt>PullBufferStream</tt> for MacCoreaudio. + * + * @author Vincent Lucas + */ +public class MacCoreaudioStream + extends AbstractPullBufferStream<DataSource> +{ + /** + * The <tt>Logger</tt> used by the <tt>MacCoreaudioStream</tt> class and its + * instances for logging output. + */ + private static final Logger logger + = Logger.getLogger(MacCoreaudioStream.class); + + /** + * The indicator which determines whether audio quality improvement is + * enabled for this <tt>MacCoreaudioStream</tt> in accord with the + * preferences of the user. + */ + private final boolean audioQualityImprovement; + + /** + * The number of bytes to read from a native MacCoreaudio stream in a single + * invocation. Based on {@link #framesPerBuffer}. + */ + private int bytesPerBuffer; + + /** + * The device identifier (the device UID, or if not available, the device + * name) of the MacCoreaudio device read through this + * <tt>PullBufferStream</tt>. + */ + private String deviceUID; + + /** + * A mutual eclusion used to avoid conflict when starting / stoping the + * stream for this stream; + */ + private Object startStopMutex = new Object(); + + /** + * The buffer which stores the outgoing data before sending them to + * the RTP stack. + */ + private byte[] buffer = null; + + /** + * A list of already allocated buffers, ready to accept new captured data. + */ + private Vector<byte[]> freeBufferList = new Vector<byte[]>(); + + /** + * A list of already allocated and filled buffers, ready to be send throw + * the network. + */ + private Vector<byte[]> fullBufferList = new Vector<byte[]>(); + + /** + * The number of data available to feed the RTP stack. + */ + private int nbBufferData = 0; + + /** + * The last-known <tt>Format</tt> of the media data made available by this + * <tt>PullBufferStream</tt>. + */ + private AudioFormat format = null; + + /** + * The <tt>GainControl</tt> through which the volume/gain of captured media + * is controlled. + */ + private final GainControl gainControl; + + private final MacCoreaudioSystem.UpdateAvailableDeviceListListener + updateAvailableDeviceListListener + = new MacCoreaudioSystem.UpdateAvailableDeviceListListener() + { + /** + * The device ID (could be deviceUID or name but that is not + * really of concern to MacCoreaudioStream) used before and + * after (if still available) the update. + */ + private String deviceUID = null; + + private boolean start = false; + + public void didUpdateAvailableDeviceList() + throws Exception + { + synchronized(startStopMutex) + { + setDeviceUID(deviceUID); + if(start) + start(); + deviceUID = null; + start = false; + } + } + + public void willUpdateAvailableDeviceList() + throws Exception + { + synchronized(startStopMutex) + { + if (stream == 0) + { + deviceUID = null; + start = false; + } + else + { + deviceUID = MacCoreaudioStream.this.deviceUID; + start = true; + stop(); + setDeviceUID(null); + } + } + } + }; + + /** + * Current sequence number. + */ + private int sequenceNumber = 0; + + /** + * The stream structure used by the native maccoreaudio library. + */ + private long stream = 0; + + /** + * Initializes a new <tt>MacCoreaudioStream</tt> instance which is to have + * its <tt>Format</tt>-related information abstracted by a specific + * <tt>FormatControl</tt>. + * + * @param dataSource the <tt>DataSource</tt> which is creating the new + * instance so that it becomes one of its <tt>streams</tt> + * @param formatControl the <tt>FormatControl</tt> which is to abstract the + * <tt>Format</tt>-related information of the new instance + * @param audioQualityImprovement <tt>true</tt> to enable audio quality + * improvement for the new instance in accord with the preferences of the + * user or <tt>false</tt> to completely disable audio quality improvement + */ + public MacCoreaudioStream( + DataSource dataSource, + FormatControl formatControl, + boolean audioQualityImprovement) + { + super(dataSource, formatControl); + + this.audioQualityImprovement = audioQualityImprovement; + + MediaServiceImpl mediaServiceImpl + = NeomediaServiceUtils.getMediaServiceImpl(); + + gainControl = (mediaServiceImpl == null) + ? null + : (GainControl) mediaServiceImpl.getInputVolumeControl(); + + // XXX We will add a UpdateAvailableDeviceListListener and will not + // remove it because we will rely on MacCoreaudioSystem's use of + // WeakReference. + MacCoreaudioSystem.addUpdateAvailableDeviceListListener( + updateAvailableDeviceListListener); + } + + private void connect() + { + AudioFormat format = (AudioFormat) getFormat(); + int channels = format.getChannels(); + if (channels == Format.NOT_SPECIFIED) + channels = 1; + int sampleSizeInBits = format.getSampleSizeInBits(); + double sampleRate = format.getSampleRate(); + int framesPerBuffer + = (int) ((sampleRate * MacCoreAudioDevice.DEFAULT_MILLIS_PER_BUFFER) + / (channels * 1000)); + bytesPerBuffer = (sampleSizeInBits / 8) * channels * framesPerBuffer; + + // Know the Format in which this MacCoreaudioStream will output audio + // data so that it can report it without going through its DataSource. + this.format = new AudioFormat( + AudioFormat.LINEAR, + sampleRate, + sampleSizeInBits, + channels, + AudioFormat.LITTLE_ENDIAN, + AudioFormat.SIGNED, + Format.NOT_SPECIFIED /* frameSizeInBits */, + Format.NOT_SPECIFIED /* frameRate */, + Format.byteArray); + } + + /** + * Gets the <tt>Format</tt> of this <tt>PullBufferStream</tt> as directly + * known by it. + * + * @return the <tt>Format</tt> of this <tt>PullBufferStream</tt> as directly + * known by it or <tt>null</tt> if this <tt>PullBufferStream</tt> does not + * directly know its <tt>Format</tt> and it relies on the + * <tt>PullBufferDataSource</tt> which created it to report its + * <tt>Format</tt> + * @see AbstractPullBufferStream#doGetFormat() + */ + @Override + protected Format doGetFormat() + { + return (format == null) ? super.doGetFormat() : format; + } + + /** + * Reads media data from this <tt>PullBufferStream</tt> into a specific + * <tt>Buffer</tt> with blocking. + * + * @param buffer the <tt>Buffer</tt> in which media data is to be read from + * this <tt>PullBufferStream</tt> + * @throws IOException if anything goes wrong while reading media data from + * this <tt>PullBufferStream</tt> into the specified <tt>buffer</tt> + */ + public void read(Buffer buffer) + throws IOException + { + int length = 0; + byte[] data = AbstractCodec2.validateByteArraySize( + buffer, + bytesPerBuffer, + false); + + synchronized(startStopMutex) + { + // Waits for the next buffer. + while(this.fullBufferList.size() == 0 && stream != 0) + { + try + { + startStopMutex.wait(); + } + catch(InterruptedException ex) + {} + } + + // If the stream is running. + if(stream != 0) + { + this.freeBufferList.add(data); + data = this.fullBufferList.remove(0); + length = data.length; + } + } + + // Take into account the user's preferences with respect to the + // input volume. + if(length != 0 && gainControl != null) + { + BasicVolumeControl.applyGain( + gainControl, + data, + 0, + length); + } + + long bufferTimeStamp = System.nanoTime(); + + buffer.setData(data); + buffer.setFlags(Buffer.FLAG_SYSTEM_TIME); + if (format != null) + buffer.setFormat(format); + buffer.setHeader(null); + buffer.setLength(length); + buffer.setOffset(0); + buffer.setSequenceNumber(sequenceNumber++); + buffer.setTimeStamp(bufferTimeStamp); + } + + /** + * Sets the device index of the MacCoreaudio device to be read through this + * <tt>PullBufferStream</tt>. + * + * @param deviceID The ID of the device used to be read trough this + * MacCoreaudioStream. This String contains the deviceUID, or if not + * available, the device name. If set to null, then there was no device + * used before the update. + */ + void setDeviceUID(String deviceUID) + { + synchronized(startStopMutex) + { + if (this.deviceUID != null) + { + // If there is a running stream, then close it. + try + { + stop(); + } + catch(IOException ioex) + { + logger.info(ioex); + } + + // Make sure this AbstractPullBufferStream asks its DataSource + // for the Format in which it is supposed to output audio data + // the next time it is opened instead of using its Format from a + // previous open. + this.format = null; + } + this.deviceUID = deviceUID; + + if (this.deviceUID != null) + { + connect(); + } + } + } + + /** + * Starts the transfer of media data from this <tt>PullBufferStream</tt>. + */ + @Override + public void start() + throws IOException + { + synchronized(startStopMutex) + { + if(stream == 0 && deviceUID != null) + { + buffer = new byte[bytesPerBuffer]; + nbBufferData = 0; + this.fullBufferList.clear(); + this.freeBufferList.clear(); + + MacCoreaudioSystem.willOpenStream(); + stream = MacCoreAudioDevice.startStream( + deviceUID, + this, + (float) format.getSampleRate(), + format.getChannels(), + format.getSampleSizeInBits(), + false, + format.getEndian() == AudioFormat.BIG_ENDIAN, + false); + MacCoreaudioSystem.didOpenStream(); + } + } + } + + /** + * Stops the transfer of media data from this <tt>PullBufferStream</tt>. + */ + @Override + public void stop() + throws IOException + { + synchronized(startStopMutex) + { + if(stream != 0 && deviceUID != null) + { + MacCoreAudioDevice.stopStream(deviceUID, stream); + stream = 0; + this.fullBufferList.clear(); + this.freeBufferList.clear(); + startStopMutex.notify(); + } + } + } + + /** + * Callback which receives the data from the coreaudio library. + * + * @param buffer The data captured from the input. + * @param bufferLength The length of the data captured. + */ + public void readInput(byte[] buffer, int bufferLength) + { + int nbCopied = 0; + while(bufferLength > 0) + { + int length = this.buffer.length - nbBufferData; + if(bufferLength < length) + { + length = bufferLength; + } + + System.arraycopy( + buffer, + nbCopied, + this.buffer, + nbBufferData, + length); + + nbBufferData += length; + nbCopied += length; + bufferLength -= length; + + if(nbBufferData == this.buffer.length) + { + this.fullBufferList.add(this.buffer); + this.buffer = null; + nbBufferData = 0; + synchronized(startStopMutex) + { + startStopMutex.notify(); + if(this.freeBufferList.size() > 0) + { + this.buffer = this.freeBufferList.remove(0); + } + } + + if(this.buffer == null) + { + this.buffer = new byte[bytesPerBuffer]; + } + } + } + } +} diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/MacCoreaudioRenderer.java b/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/MacCoreaudioRenderer.java new file mode 100644 index 00000000..1657c645 --- /dev/null +++ b/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/MacCoreaudioRenderer.java @@ -0,0 +1,592 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package org.jitsi.impl.neomedia.jmfext.media.renderer.audio; + +import java.beans.*; +import java.lang.reflect.*; +import java.util.*; + +import javax.media.*; +import javax.media.format.*; + +import org.jitsi.impl.neomedia.*; +import org.jitsi.impl.neomedia.control.*; +import org.jitsi.impl.neomedia.device.*; +import org.jitsi.service.neomedia.*; +import org.jitsi.util.*; + +import java.nio.ByteBuffer; + +/** + * Implements an audio <tt>Renderer</tt> which uses MacOSX Coreaudio. + * + * @author Vincent Lucas + */ +public class MacCoreaudioRenderer + extends AbstractAudioRenderer<MacCoreaudioSystem> +{ + /** + * The <tt>Logger</tt> used by the <tt>MacCoreaudioRenderer</tt> class and + * its instances for logging output. + */ + private static final Logger logger + = Logger.getLogger(MacCoreaudioRenderer.class); + + /** + * The device used for this renderer. + */ + private String deviceUID = null; + + /** + * The stream structure used by the native maccoreaudio library. + */ + private long stream = 0; + + /** + * A mutual eclusion used to avoid conflict when starting / stoping the + * stream for this renderer; + */ + private Object startStopMutex = new Object(); + + /** + * The buffer which stores th incoming data before sending them to + * CoreAudio. + */ + private byte[] buffer = null; + + /** + * The number of data available to feed CoreAudio output. + */ + private int nbBufferData = 0; + + private boolean isStopping = false; + + /** + * The constant which represents an empty array with + * <tt>Format</tt> element type. Explicitly defined in order to + * reduce unnecessary allocations. + */ + private static final Format[] EMPTY_SUPPORTED_INPUT_FORMATS + = new Format[0]; + + /** + * The human-readable name of the <tt>MacCoreaudioRenderer</tt> JMF plug-in. + */ + private static final String PLUGIN_NAME = "MacCoreaudio Renderer"; + + /** + * The list of JMF <tt>Format</tt>s of audio data which + * <tt>MacCoreaudioRenderer</tt> instances are capable of rendering. + */ + private static final Format[] SUPPORTED_INPUT_FORMATS; + + /** + * The list of the sample rates supported by <tt>MacCoreaudioRenderer</tt> + * as input. + */ + private static final double[] SUPPORTED_INPUT_SAMPLE_RATES + = new double[] { 8000, 11025, 16000, 22050, 32000, 44100, 48000 }; + + static + { + int count = SUPPORTED_INPUT_SAMPLE_RATES.length; + + SUPPORTED_INPUT_FORMATS = new Format[count]; + for (int i = 0; i < count; i++) + { + SUPPORTED_INPUT_FORMATS[i] + = new AudioFormat( + AudioFormat.LINEAR, + SUPPORTED_INPUT_SAMPLE_RATES[i], + 16 /* sampleSizeInBits */, + Format.NOT_SPECIFIED /* channels */, + AudioFormat.LITTLE_ENDIAN, + AudioFormat.SIGNED, + Format.NOT_SPECIFIED /* frameSizeInBits */, + Format.NOT_SPECIFIED /* frameRate */, + Format.byteArray); + } + } + + /** + * The <tt>UpdateAvailableDeviceListListener</tt> which is to be notified + * before and after MacCoreaudio's native function + * <tt>UpdateAvailableDeviceList()</tt> is invoked. It will close + * {@link #stream} before the invocation in order to mitigate memory + * corruption afterwards and it will attempt to restore the state of this + * <tt>Renderer</tt> after the invocation. + */ + private final MacCoreaudioSystem.UpdateAvailableDeviceListListener + updateAvailableDeviceListListener + = new MacCoreaudioSystem.UpdateAvailableDeviceListListener() + { + private boolean start = false; + + public void didUpdateAvailableDeviceList() + throws Exception + { + synchronized(startStopMutex) + { + updateDeviceUID(); + if(start) + { + open(); + start(); + } + } + } + + public void willUpdateAvailableDeviceList() + throws Exception + { + synchronized(startStopMutex) + { + start = false; + if(stream != 0) + { + start = true; + stop(); + } + } + } + }; + + /** + * Array of supported input formats. + */ + private Format[] supportedInputFormats; + + /** + * Initializes a new <tt>MacCoreaudioRenderer</tt> instance. + */ + public MacCoreaudioRenderer() + { + this(true); + } + + /** + * Initializes a new <tt>MacCoreaudioRenderer</tt> instance which is to + * either perform playback or sound a notification. + * + * @param playback <tt>true</tt> if the new instance is to perform playback + * or <tt>false</tt> if the new instance is to sound a notification + */ + public MacCoreaudioRenderer(boolean enableVolumeControl) + { + super( + AudioSystem.LOCATOR_PROTOCOL_MACCOREAUDIO, + enableVolumeControl + ? AudioSystem.DataFlow.PLAYBACK + : AudioSystem.DataFlow.NOTIFY); + + // XXX We will add a PaUpdateAvailableDeviceListListener and will not + // remove it because we will rely on MacCoreaudioSystem's use of + // WeakReference. + MacCoreaudioSystem.addUpdateAvailableDeviceListListener( + updateAvailableDeviceListListener); + } + + /** + * Closes this <tt>PlugIn</tt>. + */ + @Override + public void close() + { + stop(); + super.close(); + } + + /** + * Gets the descriptive/human-readable name of this JMF plug-in. + * + * @return the descriptive/human-readable name of this JMF plug-in + */ + public String getName() + { + return PLUGIN_NAME; + } + + /** + * Gets the list of JMF <tt>Format</tt>s of audio data which this + * <tt>Renderer</tt> is capable of rendering. + * + * @return an array of JMF <tt>Format</tt>s of audio data which this + * <tt>Renderer</tt> is capable of rendering + */ + @Override + public Format[] getSupportedInputFormats() + { + if (supportedInputFormats == null) + { + MediaLocator locator = getLocator(); + this.updateDeviceUID(); + + if(deviceUID == null) + { + supportedInputFormats = SUPPORTED_INPUT_FORMATS; + } + else + { + int minOutputChannels = 1; + // The maximum output channels may be a lot and checking all of + // them will take a lot of time. Besides, we currently support + // at most 2. + int maxOutputChannels + = Math.min( + MacCoreAudioDevice.countOutputChannels(deviceUID), + 2); + List<Format> supportedInputFormats + = new ArrayList<Format>(SUPPORTED_INPUT_FORMATS.length); + + for (Format supportedInputFormat : SUPPORTED_INPUT_FORMATS) + { + getSupportedInputFormats( + supportedInputFormat, + minOutputChannels, + maxOutputChannels, + supportedInputFormats); + } + + this.supportedInputFormats + = supportedInputFormats.isEmpty() + ? EMPTY_SUPPORTED_INPUT_FORMATS + : supportedInputFormats.toArray( + EMPTY_SUPPORTED_INPUT_FORMATS); + } + } + return + (supportedInputFormats.length == 0) + ? EMPTY_SUPPORTED_INPUT_FORMATS + : supportedInputFormats.clone(); + } + + private void getSupportedInputFormats( + Format format, + int minOutputChannels, + int maxOutputChannels, + List<Format> supportedInputFormats) + { + AudioFormat audioFormat = (AudioFormat) format; + int sampleSizeInBits = audioFormat.getSampleSizeInBits(); + double sampleRate = audioFormat.getSampleRate(); + float minRate + = MacCoreAudioDevice.getMinimalNominalSampleRate(deviceUID); + float maxRate + = MacCoreAudioDevice.getMaximalNominalSampleRate(deviceUID); + + for(int channels = minOutputChannels; + channels <= maxOutputChannels; + channels++) + { + if(sampleRate >= minRate && sampleRate <= maxRate) + { + supportedInputFormats.add( + new AudioFormat( + audioFormat.getEncoding(), + sampleRate, + sampleSizeInBits, + channels, + audioFormat.getEndian(), + audioFormat.getSigned(), + Format.NOT_SPECIFIED, // frameSizeInBits + Format.NOT_SPECIFIED, // frameRate + audioFormat.getDataType())); + } + } + } + + /** + * Opens the MacCoreaudio device and output stream represented by this + * instance which are to be used to render audio. + * + * @throws ResourceUnavailableException if the MacCoreaudio device or output + * stream cannot be created or opened + */ + @Override + public void open() + throws ResourceUnavailableException + { + synchronized(startStopMutex) + { + if(stream == 0) + { + MacCoreaudioSystem.willOpenStream(); + try + { + if(!this.updateDeviceUID()) + { + throw new ResourceUnavailableException( + "No locator/MediaLocator is set."); + } + } + finally + { + MacCoreaudioSystem.didOpenStream(); + } + + } + super.open(); + } + } + + /** + * Notifies this instance that the value of the + * {@link AudioSystem#PROP_PLAYBACK_DEVICE} property of its associated + * <tt>AudioSystem</tt> has changed. + * + * @param ev a <tt>PropertyChangeEvent</tt> which specifies details about + * the change such as the name of the property and its old and new values + */ + @Override + protected synchronized void playbackDevicePropertyChange( + PropertyChangeEvent ev) + { + synchronized(startStopMutex) + { + stop(); + updateDeviceUID(); + start(); + } + } + + /** + * Renders the audio data contained in a specific <tt>Buffer</tt> onto the + * MacCoreaudio device represented by this <tt>Renderer</tt>. + * + * @param buffer the <tt>Buffer</tt> which contains the audio data to be + * rendered + * @return <tt>BUFFER_PROCESSED_OK</tt> if the specified <tt>buffer</tt> has + * been successfully processed + */ + public int process(Buffer buffer) + { + synchronized(startStopMutex) + { + if(stream != 0 && !isStopping) + { + // Take into account the user's preferences with respect to the + // output volume. + GainControl gainControl = getGainControl(); + if (gainControl != null) + { + BasicVolumeControl.applyGain( + gainControl, + (byte[]) buffer.getData(), + buffer.getOffset(), + buffer.getLength()); + } + + int length = buffer.getLength(); + + // Update the buffer size if too small. + int timeout = 2000; + int maxNbBuffers + = timeout / MacCoreAudioDevice.DEFAULT_MILLIS_PER_BUFFER; + updateBufferLength( + Math.min( + nbBufferData + length, + length * maxNbBuffers)); + + if(nbBufferData + length > this.buffer.length) + { + length = this.buffer.length - nbBufferData; + } + + // Copy the received data. + System.arraycopy( + (byte[]) buffer.getData(), + buffer.getOffset(), + this.buffer, + nbBufferData, + length); + nbBufferData += length; + } + } + return BUFFER_PROCESSED_OK; + } + + /** + * Sets the <tt>MediaLocator</tt> which specifies the device index of the + * MacCoreaudio device to be used by this instance for rendering. + * + * @param locator a <tt>MediaLocator</tt> which specifies the device index + * of the MacCoreaudio device to be used by this instance for rendering + */ + @Override + public void setLocator(MediaLocator locator) + { + super.setLocator(locator); + + this.updateDeviceUID(); + + supportedInputFormats = null; + } + + /** + * Starts the rendering process. Any audio data available in the internal + * resources associated with this <tt>MacCoreaudioRenderer</tt> will begin + * being rendered. + */ + public void start() + { + // Start the stream + synchronized(startStopMutex) + { + if(stream == 0 && deviceUID != null) + { + int nbChannels = inputFormat.getChannels(); + if (nbChannels == Format.NOT_SPECIFIED) + nbChannels = 1; + + MacCoreaudioSystem.willOpenStream(); + stream = MacCoreAudioDevice.startStream( + deviceUID, + this, + (float) inputFormat.getSampleRate(), + nbChannels, + inputFormat.getSampleSizeInBits(), + false, + inputFormat.getEndian() == AudioFormat.BIG_ENDIAN, + false); + MacCoreaudioSystem.didOpenStream(); + } + } + } + + /** + * Stops the rendering process. + */ + public void stop() + { + synchronized(startStopMutex) + { + if(stream != 0 && deviceUID != null && !isStopping) + { + this.isStopping = true; + long timeout = 500; + long startTime = System.currentTimeMillis(); + long currentTime = startTime; + long startNbData = nbBufferData; + while(nbBufferData > 0 + && (currentTime - startTime) < timeout) + { + try + { + startStopMutex.wait(timeout); + } + catch(InterruptedException ex) + { + } + + currentTime = System.currentTimeMillis(); + if(startNbData > nbBufferData) + { + startTime = currentTime; + startNbData = nbBufferData; + } + } + + MacCoreAudioDevice.stopStream(deviceUID, stream); + stream = 0; + buffer = null; + nbBufferData = 0; + this.isStopping = false; + } + } + } + + /** + * Writes the data received to the buffer give in arguments, which is + * provided by the CoreAudio library. + * + * @param buffer The buffer to fill in provided by the CoreAudio library. + * @param bufferLength The length of the buffer provided. + */ + public void writeOutput(byte[] buffer, int bufferLength) + { + synchronized(startStopMutex) + { + updateBufferLength(bufferLength); + + int i = 0; + int length = nbBufferData; + if(bufferLength < length) + { + length = bufferLength; + } + + System.arraycopy(this.buffer, 0, buffer, 0, length); + + // Fiils the end of the buffer with silence. + if(length < bufferLength) + { + Arrays.fill(buffer, length, bufferLength, (byte) 0); + } + + + nbBufferData -= length; + if(nbBufferData > 0) + { + System.arraycopy( + this.buffer, length, this.buffer, 0, nbBufferData); + } + // If the stop process is waiting, notifies it that every sample + // has been consummed. + // (nbBufferData == 0) + else + { + startStopMutex.notify(); + } + } + } + + /** + * Updates the deviceUID based on the current locator. + * + * @return True if the deviceUID has been updated. False otherwise. + */ + private boolean updateDeviceUID() + { + MediaLocator locator = getLocator(); + if(locator != null) + { + String remainder = locator.getRemainder(); + if(remainder != null && remainder.length() > 1) + { + synchronized(startStopMutex) + { + this.deviceUID = remainder.substring(1); + } + return true; + } + } + return false; + } + + private void updateBufferLength(int newLength) + { + synchronized(startStopMutex) + { + if(this.buffer == null) + { + this.buffer = new byte[newLength]; + nbBufferData = 0; + } + else if(newLength > this.buffer.length) + { + byte[] newBuffer = new byte[newLength]; + System.arraycopy( + this.buffer, + 0, + newBuffer, + 0, + nbBufferData); + this.buffer = newBuffer; + } + } + } +} -- GitLab