From 7ae1ffc2c9eb3b631d888711f7892e575fa302aa Mon Sep 17 00:00:00 2001 From: "Charles J. Cliffe" Date: Thu, 4 Dec 2014 19:03:02 -0500 Subject: [PATCH 1/6] RtAudio source integrated, preparing to implement --- CMakeLists.txt | 20 +- external/rtaudio/RtAudio.cpp | 10136 +++++++++++++++++++++++++++++++++ external/rtaudio/RtAudio.h | 1162 ++++ external/rtaudio/readme | 61 + src/audio/AudioThread.cpp | 74 +- src/audio/AudioThread.h | 6 +- 6 files changed, 11411 insertions(+), 48 deletions(-) create mode 100644 external/rtaudio/RtAudio.cpp create mode 100644 external/rtaudio/RtAudio.h create mode 100644 external/rtaudio/readme diff --git a/CMakeLists.txt b/CMakeLists.txt index b20606b..ced68ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,13 +66,16 @@ if (DEFINED WIN32) include_directories ( ${PROJECT_SOURCE_DIR}/external/fftw-3.3.4-dll64 ${PROJECT_SOURCE_DIR}/external/rtl-sdr-release ) link_directories ( ${PROJECT_SOURCE_DIR}/external/fftw-3.3.4-dll64 ${PROJECT_SOURCE_DIR}/external/rtl-sdr-release/x64 ) set(FFTW_LIB fftw3-3) - - include_directories ( ${PROJECT_SOURCE_DIR}/external/portaudio/include ) - link_directories ( ${PROJECT_SOURCE_DIR}/external/portaudio/libs/64 ) - SET (PORTAUDIO_LIBRARY libportaudio_x86.dll winmm) link_directories ( ${PROJECT_SOURCE_DIR}/external/liquid-dsp/lib/64 ) include_directories ( ${PROJECT_SOURCE_DIR}/external/liquid-dsp/include ) + + ADD_DEFINITIONS( + -D__WINDOWS_DS__ + ) + + SET(OTHER_LIBRARIES -ldsound) + else (DEFINED WIN32) set(RTLSDR_INCLUDE "/opt/local/include" CACHE FILEPATH "RTL-SDR Include Path") set(RTLSDR_LIB "/opt/local/lib" CACHE FILEPATH "RTL-SDR Lib Path") @@ -80,8 +83,6 @@ else (DEFINED WIN32) link_directories(${RTLSDR_LIB}) set(FFTW_LIB fftw3) - - SET (PORTAUDIO_LIBRARY portaudio) endif (DEFINED WIN32) @@ -105,6 +106,7 @@ SET (cubicsdr_sources src/visual/SpectrumContext.cpp src/visual/WaterfallCanvas.cpp src/visual/WaterfallContext.cpp + external/rtaudio/RtAudio.cpp ) SET (cubicsdr_headers @@ -128,6 +130,7 @@ SET (cubicsdr_headers src/visual/SpectrumContext.h src/visual/WaterfallCanvas.h src/visual/WaterfallContext.h + external/rtaudio/RtAudio.h ) include_directories ( ${PROJECT_SOURCE_DIR}/src/sdr @@ -135,7 +138,8 @@ include_directories ( ${PROJECT_SOURCE_DIR}/src/sdr ${PROJECT_SOURCE_DIR}/src/audio ${PROJECT_SOURCE_DIR}/src/util ${PROJECT_SOURCE_DIR}/src/visual - ${PROJECT_SOURCE_DIR}/src ) + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/external/rtaudio ) ADD_DEFINITIONS( -std=c++0x # or -std=c++11 @@ -147,7 +151,7 @@ ADD_DEFINITIONS( add_executable(CubicSDR ${cubicsdr_sources} ${cubicsdr_headers}) -target_link_libraries(CubicSDR rtlsdr liquid ${FFTW_LIB} ${wxWidgets_LIBRARIES} ${OPENGL_LIBRARIES} ${PORTAUDIO_LIBRARY}) +target_link_libraries(CubicSDR rtlsdr liquid ${FFTW_LIB} ${wxWidgets_LIBRARIES} ${OPENGL_LIBRARIES} ${OTHER_LIBRARIES}) # cubicvr2 glfw ${GLFW_LIBRARIES} diff --git a/external/rtaudio/RtAudio.cpp b/external/rtaudio/RtAudio.cpp new file mode 100644 index 0000000..24cee6d --- /dev/null +++ b/external/rtaudio/RtAudio.cpp @@ -0,0 +1,10136 @@ +/************************************************************************/ +/*! \class RtAudio + \brief Realtime audio i/o C++ classes. + + RtAudio provides a common API (Application Programming Interface) + for realtime audio input/output across Linux (native ALSA, Jack, + and OSS), Macintosh OS X (CoreAudio and Jack), and Windows + (DirectSound, ASIO and WASAPI) operating systems. + + RtAudio WWW site: http://www.music.mcgill.ca/~gary/rtaudio/ + + RtAudio: realtime audio i/o C++ classes + Copyright (c) 2001-2014 Gary P. Scavone + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + Any person wishing to distribute modifications to the Software is + asked to send the modifications to the original developer so that + they can be incorporated into the canonical version. This is, + however, not a binding provision of this license. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +/************************************************************************/ + +// RtAudio: Version 4.1.1 + +#include "RtAudio.h" +#include +#include +#include +#include + +// Static variable definitions. +const unsigned int RtApi::MAX_SAMPLE_RATES = 14; +const unsigned int RtApi::SAMPLE_RATES[] = { + 4000, 5512, 8000, 9600, 11025, 16000, 22050, + 32000, 44100, 48000, 88200, 96000, 176400, 192000 +}; + +#if defined(__WINDOWS_DS__) || defined(__WINDOWS_ASIO__) || defined(__WINDOWS_WASAPI__) + #define MUTEX_INITIALIZE(A) InitializeCriticalSection(A) + #define MUTEX_DESTROY(A) DeleteCriticalSection(A) + #define MUTEX_LOCK(A) EnterCriticalSection(A) + #define MUTEX_UNLOCK(A) LeaveCriticalSection(A) +#elif defined(__LINUX_ALSA__) || defined(__LINUX_PULSE__) || defined(__UNIX_JACK__) || defined(__LINUX_OSS__) || defined(__MACOSX_CORE__) + // pthread API + #define MUTEX_INITIALIZE(A) pthread_mutex_init(A, NULL) + #define MUTEX_DESTROY(A) pthread_mutex_destroy(A) + #define MUTEX_LOCK(A) pthread_mutex_lock(A) + #define MUTEX_UNLOCK(A) pthread_mutex_unlock(A) +#else + #define MUTEX_INITIALIZE(A) abs(*A) // dummy definitions + #define MUTEX_DESTROY(A) abs(*A) // dummy definitions +#endif + +// *************************************************** // +// +// RtAudio definitions. +// +// *************************************************** // + +std::string RtAudio :: getVersion( void ) throw() +{ + return RTAUDIO_VERSION; +} + +void RtAudio :: getCompiledApi( std::vector &apis ) throw() +{ + apis.clear(); + + // The order here will control the order of RtAudio's API search in + // the constructor. +#if defined(__UNIX_JACK__) + apis.push_back( UNIX_JACK ); +#endif +#if defined(__LINUX_ALSA__) + apis.push_back( LINUX_ALSA ); +#endif +#if defined(__LINUX_PULSE__) + apis.push_back( LINUX_PULSE ); +#endif +#if defined(__LINUX_OSS__) + apis.push_back( LINUX_OSS ); +#endif +#if defined(__WINDOWS_ASIO__) + apis.push_back( WINDOWS_ASIO ); +#endif +#if defined(__WINDOWS_WASAPI__) + apis.push_back( WINDOWS_WASAPI ); +#endif +#if defined(__WINDOWS_DS__) + apis.push_back( WINDOWS_DS ); +#endif +#if defined(__MACOSX_CORE__) + apis.push_back( MACOSX_CORE ); +#endif +#if defined(__RTAUDIO_DUMMY__) + apis.push_back( RTAUDIO_DUMMY ); +#endif +} + +void RtAudio :: openRtApi( RtAudio::Api api ) +{ + if ( rtapi_ ) + delete rtapi_; + rtapi_ = 0; + +#if defined(__UNIX_JACK__) + if ( api == UNIX_JACK ) + rtapi_ = new RtApiJack(); +#endif +#if defined(__LINUX_ALSA__) + if ( api == LINUX_ALSA ) + rtapi_ = new RtApiAlsa(); +#endif +#if defined(__LINUX_PULSE__) + if ( api == LINUX_PULSE ) + rtapi_ = new RtApiPulse(); +#endif +#if defined(__LINUX_OSS__) + if ( api == LINUX_OSS ) + rtapi_ = new RtApiOss(); +#endif +#if defined(__WINDOWS_ASIO__) + if ( api == WINDOWS_ASIO ) + rtapi_ = new RtApiAsio(); +#endif +#if defined(__WINDOWS_WASAPI__) + if ( api == WINDOWS_WASAPI ) + rtapi_ = new RtApiWasapi(); +#endif +#if defined(__WINDOWS_DS__) + if ( api == WINDOWS_DS ) + rtapi_ = new RtApiDs(); +#endif +#if defined(__MACOSX_CORE__) + if ( api == MACOSX_CORE ) + rtapi_ = new RtApiCore(); +#endif +#if defined(__RTAUDIO_DUMMY__) + if ( api == RTAUDIO_DUMMY ) + rtapi_ = new RtApiDummy(); +#endif +} + +RtAudio :: RtAudio( RtAudio::Api api ) +{ + rtapi_ = 0; + + if ( api != UNSPECIFIED ) { + // Attempt to open the specified API. + openRtApi( api ); + if ( rtapi_ ) return; + + // No compiled support for specified API value. Issue a debug + // warning and continue as if no API was specified. + std::cerr << "\nRtAudio: no compiled support for specified API argument!\n" << std::endl; + } + + // Iterate through the compiled APIs and return as soon as we find + // one with at least one device or we reach the end of the list. + std::vector< RtAudio::Api > apis; + getCompiledApi( apis ); + for ( unsigned int i=0; igetDeviceCount() ) break; + } + + if ( rtapi_ ) return; + + // It should not be possible to get here because the preprocessor + // definition __RTAUDIO_DUMMY__ is automatically defined if no + // API-specific definitions are passed to the compiler. But just in + // case something weird happens, we'll thow an error. + std::string errorText = "\nRtAudio: no compiled API support found ... critical error!!\n\n"; + throw( RtAudioError( errorText, RtAudioError::UNSPECIFIED ) ); +} + +RtAudio :: ~RtAudio() throw() +{ + if ( rtapi_ ) + delete rtapi_; +} + +void RtAudio :: openStream( RtAudio::StreamParameters *outputParameters, + RtAudio::StreamParameters *inputParameters, + RtAudioFormat format, unsigned int sampleRate, + unsigned int *bufferFrames, + RtAudioCallback callback, void *userData, + RtAudio::StreamOptions *options, + RtAudioErrorCallback errorCallback ) +{ + return rtapi_->openStream( outputParameters, inputParameters, format, + sampleRate, bufferFrames, callback, + userData, options, errorCallback ); +} + +// *************************************************** // +// +// Public RtApi definitions (see end of file for +// private or protected utility functions). +// +// *************************************************** // + +RtApi :: RtApi() +{ + stream_.state = STREAM_CLOSED; + stream_.mode = UNINITIALIZED; + stream_.apiHandle = 0; + stream_.userBuffer[0] = 0; + stream_.userBuffer[1] = 0; + MUTEX_INITIALIZE( &stream_.mutex ); + showWarnings_ = true; + firstErrorOccurred_ = false; +} + +RtApi :: ~RtApi() +{ + MUTEX_DESTROY( &stream_.mutex ); +} + +void RtApi :: openStream( RtAudio::StreamParameters *oParams, + RtAudio::StreamParameters *iParams, + RtAudioFormat format, unsigned int sampleRate, + unsigned int *bufferFrames, + RtAudioCallback callback, void *userData, + RtAudio::StreamOptions *options, + RtAudioErrorCallback errorCallback ) +{ + if ( stream_.state != STREAM_CLOSED ) { + errorText_ = "RtApi::openStream: a stream is already open!"; + error( RtAudioError::INVALID_USE ); + return; + } + + // Clear stream information potentially left from a previously open stream. + clearStreamInfo(); + + if ( oParams && oParams->nChannels < 1 ) { + errorText_ = "RtApi::openStream: a non-NULL output StreamParameters structure cannot have an nChannels value less than one."; + error( RtAudioError::INVALID_USE ); + return; + } + + if ( iParams && iParams->nChannels < 1 ) { + errorText_ = "RtApi::openStream: a non-NULL input StreamParameters structure cannot have an nChannels value less than one."; + error( RtAudioError::INVALID_USE ); + return; + } + + if ( oParams == NULL && iParams == NULL ) { + errorText_ = "RtApi::openStream: input and output StreamParameters structures are both NULL!"; + error( RtAudioError::INVALID_USE ); + return; + } + + if ( formatBytes(format) == 0 ) { + errorText_ = "RtApi::openStream: 'format' parameter value is undefined."; + error( RtAudioError::INVALID_USE ); + return; + } + + unsigned int nDevices = getDeviceCount(); + unsigned int oChannels = 0; + if ( oParams ) { + oChannels = oParams->nChannels; + if ( oParams->deviceId >= nDevices ) { + errorText_ = "RtApi::openStream: output device parameter value is invalid."; + error( RtAudioError::INVALID_USE ); + return; + } + } + + unsigned int iChannels = 0; + if ( iParams ) { + iChannels = iParams->nChannels; + if ( iParams->deviceId >= nDevices ) { + errorText_ = "RtApi::openStream: input device parameter value is invalid."; + error( RtAudioError::INVALID_USE ); + return; + } + } + + bool result; + + if ( oChannels > 0 ) { + + result = probeDeviceOpen( oParams->deviceId, OUTPUT, oChannels, oParams->firstChannel, + sampleRate, format, bufferFrames, options ); + if ( result == false ) { + error( RtAudioError::SYSTEM_ERROR ); + return; + } + } + + if ( iChannels > 0 ) { + + result = probeDeviceOpen( iParams->deviceId, INPUT, iChannels, iParams->firstChannel, + sampleRate, format, bufferFrames, options ); + if ( result == false ) { + if ( oChannels > 0 ) closeStream(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + } + + stream_.callbackInfo.callback = (void *) callback; + stream_.callbackInfo.userData = userData; + stream_.callbackInfo.errorCallback = (void *) errorCallback; + + if ( options ) options->numberOfBuffers = stream_.nBuffers; + stream_.state = STREAM_STOPPED; +} + +unsigned int RtApi :: getDefaultInputDevice( void ) +{ + // Should be implemented in subclasses if possible. + return 0; +} + +unsigned int RtApi :: getDefaultOutputDevice( void ) +{ + // Should be implemented in subclasses if possible. + return 0; +} + +void RtApi :: closeStream( void ) +{ + // MUST be implemented in subclasses! + return; +} + +bool RtApi :: probeDeviceOpen( unsigned int /*device*/, StreamMode /*mode*/, unsigned int /*channels*/, + unsigned int /*firstChannel*/, unsigned int /*sampleRate*/, + RtAudioFormat /*format*/, unsigned int * /*bufferSize*/, + RtAudio::StreamOptions * /*options*/ ) +{ + // MUST be implemented in subclasses! + return FAILURE; +} + +void RtApi :: tickStreamTime( void ) +{ + // Subclasses that do not provide their own implementation of + // getStreamTime should call this function once per buffer I/O to + // provide basic stream time support. + + stream_.streamTime += ( stream_.bufferSize * 1.0 / stream_.sampleRate ); + +#if defined( HAVE_GETTIMEOFDAY ) + gettimeofday( &stream_.lastTickTimestamp, NULL ); +#endif +} + +long RtApi :: getStreamLatency( void ) +{ + verifyStream(); + + long totalLatency = 0; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) + totalLatency = stream_.latency[0]; + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) + totalLatency += stream_.latency[1]; + + return totalLatency; +} + +double RtApi :: getStreamTime( void ) +{ + verifyStream(); + +#if defined( HAVE_GETTIMEOFDAY ) + // Return a very accurate estimate of the stream time by + // adding in the elapsed time since the last tick. + struct timeval then; + struct timeval now; + + if ( stream_.state != STREAM_RUNNING || stream_.streamTime == 0.0 ) + return stream_.streamTime; + + gettimeofday( &now, NULL ); + then = stream_.lastTickTimestamp; + return stream_.streamTime + + ((now.tv_sec + 0.000001 * now.tv_usec) - + (then.tv_sec + 0.000001 * then.tv_usec)); +#else + return stream_.streamTime; +#endif +} + +void RtApi :: setStreamTime( double time ) +{ + verifyStream(); + + if ( time >= 0.0 ) + stream_.streamTime = time; +} + +unsigned int RtApi :: getStreamSampleRate( void ) +{ + verifyStream(); + + return stream_.sampleRate; +} + + +// *************************************************** // +// +// OS/API-specific methods. +// +// *************************************************** // + +#if defined(__MACOSX_CORE__) + +// The OS X CoreAudio API is designed to use a separate callback +// procedure for each of its audio devices. A single RtAudio duplex +// stream using two different devices is supported here, though it +// cannot be guaranteed to always behave correctly because we cannot +// synchronize these two callbacks. +// +// A property listener is installed for over/underrun information. +// However, no functionality is currently provided to allow property +// listeners to trigger user handlers because it is unclear what could +// be done if a critical stream parameter (buffer size, sample rate, +// device disconnect) notification arrived. The listeners entail +// quite a bit of extra code and most likely, a user program wouldn't +// be prepared for the result anyway. However, we do provide a flag +// to the client callback function to inform of an over/underrun. + +// A structure to hold various information related to the CoreAudio API +// implementation. +struct CoreHandle { + AudioDeviceID id[2]; // device ids +#if defined( MAC_OS_X_VERSION_10_5 ) && ( MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 ) + AudioDeviceIOProcID procId[2]; +#endif + UInt32 iStream[2]; // device stream index (or first if using multiple) + UInt32 nStreams[2]; // number of streams to use + bool xrun[2]; + char *deviceBuffer; + pthread_cond_t condition; + int drainCounter; // Tracks callback counts when draining + bool internalDrain; // Indicates if stop is initiated from callback or not. + + CoreHandle() + :deviceBuffer(0), drainCounter(0), internalDrain(false) { nStreams[0] = 1; nStreams[1] = 1; id[0] = 0; id[1] = 0; xrun[0] = false; xrun[1] = false; } +}; + +RtApiCore:: RtApiCore() +{ +#if defined( AVAILABLE_MAC_OS_X_VERSION_10_6_AND_LATER ) + // This is a largely undocumented but absolutely necessary + // requirement starting with OS-X 10.6. If not called, queries and + // updates to various audio device properties are not handled + // correctly. + CFRunLoopRef theRunLoop = NULL; + AudioObjectPropertyAddress property = { kAudioHardwarePropertyRunLoop, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + OSStatus result = AudioObjectSetPropertyData( kAudioObjectSystemObject, &property, 0, NULL, sizeof(CFRunLoopRef), &theRunLoop); + if ( result != noErr ) { + errorText_ = "RtApiCore::RtApiCore: error setting run loop property!"; + error( RtAudioError::WARNING ); + } +#endif +} + +RtApiCore :: ~RtApiCore() +{ + // The subclass destructor gets called before the base class + // destructor, so close an existing stream before deallocating + // apiDeviceId memory. + if ( stream_.state != STREAM_CLOSED ) closeStream(); +} + +unsigned int RtApiCore :: getDeviceCount( void ) +{ + // Find out how many audio devices there are, if any. + UInt32 dataSize; + AudioObjectPropertyAddress propertyAddress = { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster }; + OSStatus result = AudioObjectGetPropertyDataSize( kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize ); + if ( result != noErr ) { + errorText_ = "RtApiCore::getDeviceCount: OS-X error getting device info!"; + error( RtAudioError::WARNING ); + return 0; + } + + return dataSize / sizeof( AudioDeviceID ); +} + +unsigned int RtApiCore :: getDefaultInputDevice( void ) +{ + unsigned int nDevices = getDeviceCount(); + if ( nDevices <= 1 ) return 0; + + AudioDeviceID id; + UInt32 dataSize = sizeof( AudioDeviceID ); + AudioObjectPropertyAddress property = { kAudioHardwarePropertyDefaultInputDevice, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster }; + OSStatus result = AudioObjectGetPropertyData( kAudioObjectSystemObject, &property, 0, NULL, &dataSize, &id ); + if ( result != noErr ) { + errorText_ = "RtApiCore::getDefaultInputDevice: OS-X system error getting device."; + error( RtAudioError::WARNING ); + return 0; + } + + dataSize *= nDevices; + AudioDeviceID deviceList[ nDevices ]; + property.mSelector = kAudioHardwarePropertyDevices; + result = AudioObjectGetPropertyData( kAudioObjectSystemObject, &property, 0, NULL, &dataSize, (void *) &deviceList ); + if ( result != noErr ) { + errorText_ = "RtApiCore::getDefaultInputDevice: OS-X system error getting device IDs."; + error( RtAudioError::WARNING ); + return 0; + } + + for ( unsigned int i=0; i= nDevices ) { + errorText_ = "RtApiCore::getDeviceInfo: device ID is invalid!"; + error( RtAudioError::INVALID_USE ); + return info; + } + + AudioDeviceID deviceList[ nDevices ]; + UInt32 dataSize = sizeof( AudioDeviceID ) * nDevices; + AudioObjectPropertyAddress property = { kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + OSStatus result = AudioObjectGetPropertyData( kAudioObjectSystemObject, &property, + 0, NULL, &dataSize, (void *) &deviceList ); + if ( result != noErr ) { + errorText_ = "RtApiCore::getDeviceInfo: OS-X system error getting device IDs."; + error( RtAudioError::WARNING ); + return info; + } + + AudioDeviceID id = deviceList[ device ]; + + // Get the device name. + info.name.erase(); + CFStringRef cfname; + dataSize = sizeof( CFStringRef ); + property.mSelector = kAudioObjectPropertyManufacturer; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &cfname ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceInfo: system error (" << getErrorCode( result ) << ") getting device manufacturer."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + //const char *mname = CFStringGetCStringPtr( cfname, CFStringGetSystemEncoding() ); + int length = CFStringGetLength(cfname); + char *mname = (char *)malloc(length * 3 + 1); +#if defined( UNICODE ) || defined( _UNICODE ) + CFStringGetCString(cfname, mname, length * 3 + 1, kCFStringEncodingUTF8); +#else + CFStringGetCString(cfname, mname, length * 3 + 1, CFStringGetSystemEncoding()); +#endif + info.name.append( (const char *)mname, strlen(mname) ); + info.name.append( ": " ); + CFRelease( cfname ); + free(mname); + + property.mSelector = kAudioObjectPropertyName; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &cfname ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceInfo: system error (" << getErrorCode( result ) << ") getting device name."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + //const char *name = CFStringGetCStringPtr( cfname, CFStringGetSystemEncoding() ); + length = CFStringGetLength(cfname); + char *name = (char *)malloc(length * 3 + 1); +#if defined( UNICODE ) || defined( _UNICODE ) + CFStringGetCString(cfname, name, length * 3 + 1, kCFStringEncodingUTF8); +#else + CFStringGetCString(cfname, name, length * 3 + 1, CFStringGetSystemEncoding()); +#endif + info.name.append( (const char *)name, strlen(name) ); + CFRelease( cfname ); + free(name); + + // Get the output stream "configuration". + AudioBufferList *bufferList = nil; + property.mSelector = kAudioDevicePropertyStreamConfiguration; + property.mScope = kAudioDevicePropertyScopeOutput; + // property.mElement = kAudioObjectPropertyElementWildcard; + dataSize = 0; + result = AudioObjectGetPropertyDataSize( id, &property, 0, NULL, &dataSize ); + if ( result != noErr || dataSize == 0 ) { + errorStream_ << "RtApiCore::getDeviceInfo: system error (" << getErrorCode( result ) << ") getting output stream configuration info for device (" << device << ")."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + // Allocate the AudioBufferList. + bufferList = (AudioBufferList *) malloc( dataSize ); + if ( bufferList == NULL ) { + errorText_ = "RtApiCore::getDeviceInfo: memory error allocating output AudioBufferList."; + error( RtAudioError::WARNING ); + return info; + } + + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, bufferList ); + if ( result != noErr || dataSize == 0 ) { + free( bufferList ); + errorStream_ << "RtApiCore::getDeviceInfo: system error (" << getErrorCode( result ) << ") getting output stream configuration for device (" << device << ")."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + // Get output channel information. + unsigned int i, nStreams = bufferList->mNumberBuffers; + for ( i=0; imBuffers[i].mNumberChannels; + free( bufferList ); + + // Get the input stream "configuration". + property.mScope = kAudioDevicePropertyScopeInput; + result = AudioObjectGetPropertyDataSize( id, &property, 0, NULL, &dataSize ); + if ( result != noErr || dataSize == 0 ) { + errorStream_ << "RtApiCore::getDeviceInfo: system error (" << getErrorCode( result ) << ") getting input stream configuration info for device (" << device << ")."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + // Allocate the AudioBufferList. + bufferList = (AudioBufferList *) malloc( dataSize ); + if ( bufferList == NULL ) { + errorText_ = "RtApiCore::getDeviceInfo: memory error allocating input AudioBufferList."; + error( RtAudioError::WARNING ); + return info; + } + + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, bufferList ); + if (result != noErr || dataSize == 0) { + free( bufferList ); + errorStream_ << "RtApiCore::getDeviceInfo: system error (" << getErrorCode( result ) << ") getting input stream configuration for device (" << device << ")."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + // Get input channel information. + nStreams = bufferList->mNumberBuffers; + for ( i=0; imBuffers[i].mNumberChannels; + free( bufferList ); + + // If device opens for both playback and capture, we determine the channels. + if ( info.outputChannels > 0 && info.inputChannels > 0 ) + info.duplexChannels = (info.outputChannels > info.inputChannels) ? info.inputChannels : info.outputChannels; + + // Probe the device sample rates. + bool isInput = false; + if ( info.outputChannels == 0 ) isInput = true; + + // Determine the supported sample rates. + property.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; + if ( isInput == false ) property.mScope = kAudioDevicePropertyScopeOutput; + result = AudioObjectGetPropertyDataSize( id, &property, 0, NULL, &dataSize ); + if ( result != kAudioHardwareNoError || dataSize == 0 ) { + errorStream_ << "RtApiCore::getDeviceInfo: system error (" << getErrorCode( result ) << ") getting sample rate info."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + UInt32 nRanges = dataSize / sizeof( AudioValueRange ); + AudioValueRange rangeList[ nRanges ]; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &rangeList ); + if ( result != kAudioHardwareNoError ) { + errorStream_ << "RtApiCore::getDeviceInfo: system error (" << getErrorCode( result ) << ") getting sample rates."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + // The sample rate reporting mechanism is a bit of a mystery. It + // seems that it can either return individual rates or a range of + // rates. I assume that if the min / max range values are the same, + // then that represents a single supported rate and if the min / max + // range values are different, the device supports an arbitrary + // range of values (though there might be multiple ranges, so we'll + // use the most conservative range). + Float64 minimumRate = 1.0, maximumRate = 10000000000.0; + bool haveValueRange = false; + info.sampleRates.clear(); + for ( UInt32 i=0; i minimumRate ) minimumRate = rangeList[i].mMinimum; + if ( rangeList[i].mMaximum < maximumRate ) maximumRate = rangeList[i].mMaximum; + } + } + + if ( haveValueRange ) { + for ( unsigned int k=0; k= (unsigned int) minimumRate && SAMPLE_RATES[k] <= (unsigned int) maximumRate ) + info.sampleRates.push_back( SAMPLE_RATES[k] ); + } + } + + // Sort and remove any redundant values + std::sort( info.sampleRates.begin(), info.sampleRates.end() ); + info.sampleRates.erase( unique( info.sampleRates.begin(), info.sampleRates.end() ), info.sampleRates.end() ); + + if ( info.sampleRates.size() == 0 ) { + errorStream_ << "RtApiCore::probeDeviceInfo: No supported sample rates found for device (" << device << ")."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + // CoreAudio always uses 32-bit floating point data for PCM streams. + // Thus, any other "physical" formats supported by the device are of + // no interest to the client. + info.nativeFormats = RTAUDIO_FLOAT32; + + if ( info.outputChannels > 0 ) + if ( getDefaultOutputDevice() == device ) info.isDefaultOutput = true; + if ( info.inputChannels > 0 ) + if ( getDefaultInputDevice() == device ) info.isDefaultInput = true; + + info.probed = true; + return info; +} + +static OSStatus callbackHandler( AudioDeviceID inDevice, + const AudioTimeStamp* /*inNow*/, + const AudioBufferList* inInputData, + const AudioTimeStamp* /*inInputTime*/, + AudioBufferList* outOutputData, + const AudioTimeStamp* /*inOutputTime*/, + void* infoPointer ) +{ + CallbackInfo *info = (CallbackInfo *) infoPointer; + + RtApiCore *object = (RtApiCore *) info->object; + if ( object->callbackEvent( inDevice, inInputData, outOutputData ) == false ) + return kAudioHardwareUnspecifiedError; + else + return kAudioHardwareNoError; +} + +static OSStatus xrunListener( AudioObjectID /*inDevice*/, + UInt32 nAddresses, + const AudioObjectPropertyAddress properties[], + void* handlePointer ) +{ + CoreHandle *handle = (CoreHandle *) handlePointer; + for ( UInt32 i=0; ixrun[1] = true; + else + handle->xrun[0] = true; + } + } + + return kAudioHardwareNoError; +} + +static OSStatus rateListener( AudioObjectID inDevice, + UInt32 /*nAddresses*/, + const AudioObjectPropertyAddress /*properties*/[], + void* ratePointer ) +{ + Float64 *rate = (Float64 *) ratePointer; + UInt32 dataSize = sizeof( Float64 ); + AudioObjectPropertyAddress property = { kAudioDevicePropertyNominalSampleRate, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + AudioObjectGetPropertyData( inDevice, &property, 0, NULL, &dataSize, rate ); + return kAudioHardwareNoError; +} + +bool RtApiCore :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ) +{ + // Get device ID + unsigned int nDevices = getDeviceCount(); + if ( nDevices == 0 ) { + // This should not happen because a check is made before this function is called. + errorText_ = "RtApiCore::probeDeviceOpen: no devices found!"; + return FAILURE; + } + + if ( device >= nDevices ) { + // This should not happen because a check is made before this function is called. + errorText_ = "RtApiCore::probeDeviceOpen: device ID is invalid!"; + return FAILURE; + } + + AudioDeviceID deviceList[ nDevices ]; + UInt32 dataSize = sizeof( AudioDeviceID ) * nDevices; + AudioObjectPropertyAddress property = { kAudioHardwarePropertyDevices, + kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMaster }; + OSStatus result = AudioObjectGetPropertyData( kAudioObjectSystemObject, &property, + 0, NULL, &dataSize, (void *) &deviceList ); + if ( result != noErr ) { + errorText_ = "RtApiCore::probeDeviceOpen: OS-X system error getting device IDs."; + return FAILURE; + } + + AudioDeviceID id = deviceList[ device ]; + + // Setup for stream mode. + bool isInput = false; + if ( mode == INPUT ) { + isInput = true; + property.mScope = kAudioDevicePropertyScopeInput; + } + else + property.mScope = kAudioDevicePropertyScopeOutput; + + // Get the stream "configuration". + AudioBufferList *bufferList = nil; + dataSize = 0; + property.mSelector = kAudioDevicePropertyStreamConfiguration; + result = AudioObjectGetPropertyDataSize( id, &property, 0, NULL, &dataSize ); + if ( result != noErr || dataSize == 0 ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting stream configuration info for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Allocate the AudioBufferList. + bufferList = (AudioBufferList *) malloc( dataSize ); + if ( bufferList == NULL ) { + errorText_ = "RtApiCore::probeDeviceOpen: memory error allocating AudioBufferList."; + return FAILURE; + } + + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, bufferList ); + if (result != noErr || dataSize == 0) { + free( bufferList ); + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting stream configuration for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Search for one or more streams that contain the desired number of + // channels. CoreAudio devices can have an arbitrary number of + // streams and each stream can have an arbitrary number of channels. + // For each stream, a single buffer of interleaved samples is + // provided. RtAudio prefers the use of one stream of interleaved + // data or multiple consecutive single-channel streams. However, we + // now support multiple consecutive multi-channel streams of + // interleaved data as well. + UInt32 iStream, offsetCounter = firstChannel; + UInt32 nStreams = bufferList->mNumberBuffers; + bool monoMode = false; + bool foundStream = false; + + // First check that the device supports the requested number of + // channels. + UInt32 deviceChannels = 0; + for ( iStream=0; iStreammBuffers[iStream].mNumberChannels; + + if ( deviceChannels < ( channels + firstChannel ) ) { + free( bufferList ); + errorStream_ << "RtApiCore::probeDeviceOpen: the device (" << device << ") does not support the requested channel count."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Look for a single stream meeting our needs. + UInt32 firstStream, streamCount = 1, streamChannels = 0, channelOffset = 0; + for ( iStream=0; iStreammBuffers[iStream].mNumberChannels; + if ( streamChannels >= channels + offsetCounter ) { + firstStream = iStream; + channelOffset = offsetCounter; + foundStream = true; + break; + } + if ( streamChannels > offsetCounter ) break; + offsetCounter -= streamChannels; + } + + // If we didn't find a single stream above, then we should be able + // to meet the channel specification with multiple streams. + if ( foundStream == false ) { + monoMode = true; + offsetCounter = firstChannel; + for ( iStream=0; iStreammBuffers[iStream].mNumberChannels; + if ( streamChannels > offsetCounter ) break; + offsetCounter -= streamChannels; + } + + firstStream = iStream; + channelOffset = offsetCounter; + Int32 channelCounter = channels + offsetCounter - streamChannels; + + if ( streamChannels > 1 ) monoMode = false; + while ( channelCounter > 0 ) { + streamChannels = bufferList->mBuffers[++iStream].mNumberChannels; + if ( streamChannels > 1 ) monoMode = false; + channelCounter -= streamChannels; + streamCount++; + } + } + + free( bufferList ); + + // Determine the buffer size. + AudioValueRange bufferRange; + dataSize = sizeof( AudioValueRange ); + property.mSelector = kAudioDevicePropertyBufferFrameSizeRange; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &bufferRange ); + + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting buffer size range for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + if ( bufferRange.mMinimum > *bufferSize ) *bufferSize = (unsigned long) bufferRange.mMinimum; + else if ( bufferRange.mMaximum < *bufferSize ) *bufferSize = (unsigned long) bufferRange.mMaximum; + if ( options && options->flags & RTAUDIO_MINIMIZE_LATENCY ) *bufferSize = (unsigned long) bufferRange.mMinimum; + + // Set the buffer size. For multiple streams, I'm assuming we only + // need to make this setting for the master channel. + UInt32 theSize = (UInt32) *bufferSize; + dataSize = sizeof( UInt32 ); + property.mSelector = kAudioDevicePropertyBufferFrameSize; + result = AudioObjectSetPropertyData( id, &property, 0, NULL, dataSize, &theSize ); + + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") setting the buffer size for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // If attempting to setup a duplex stream, the bufferSize parameter + // MUST be the same in both directions! + *bufferSize = theSize; + if ( stream_.mode == OUTPUT && mode == INPUT && *bufferSize != stream_.bufferSize ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error setting buffer size for duplex stream on device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + stream_.bufferSize = *bufferSize; + stream_.nBuffers = 1; + + // Try to set "hog" mode ... it's not clear to me this is working. + if ( options && options->flags & RTAUDIO_HOG_DEVICE ) { + pid_t hog_pid; + dataSize = sizeof( hog_pid ); + property.mSelector = kAudioDevicePropertyHogMode; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &hog_pid ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting 'hog' state!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + if ( hog_pid != getpid() ) { + hog_pid = getpid(); + result = AudioObjectSetPropertyData( id, &property, 0, NULL, dataSize, &hog_pid ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") setting 'hog' state!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + } + + // Check and if necessary, change the sample rate for the device. + Float64 nominalRate; + dataSize = sizeof( Float64 ); + property.mSelector = kAudioDevicePropertyNominalSampleRate; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &nominalRate ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting current sample rate."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Only change the sample rate if off by more than 1 Hz. + if ( fabs( nominalRate - (double)sampleRate ) > 1.0 ) { + + // Set a property listener for the sample rate change + Float64 reportedRate = 0.0; + AudioObjectPropertyAddress tmp = { kAudioDevicePropertyNominalSampleRate, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster }; + result = AudioObjectAddPropertyListener( id, &tmp, rateListener, (void *) &reportedRate ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") setting sample rate property listener for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + nominalRate = (Float64) sampleRate; + result = AudioObjectSetPropertyData( id, &property, 0, NULL, dataSize, &nominalRate ); + if ( result != noErr ) { + AudioObjectRemovePropertyListener( id, &tmp, rateListener, (void *) &reportedRate ); + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") setting sample rate for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Now wait until the reported nominal rate is what we just set. + UInt32 microCounter = 0; + while ( reportedRate != nominalRate ) { + microCounter += 5000; + if ( microCounter > 5000000 ) break; + usleep( 5000 ); + } + + // Remove the property listener. + AudioObjectRemovePropertyListener( id, &tmp, rateListener, (void *) &reportedRate ); + + if ( microCounter > 5000000 ) { + errorStream_ << "RtApiCore::probeDeviceOpen: timeout waiting for sample rate update for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // Now set the stream format for all streams. Also, check the + // physical format of the device and change that if necessary. + AudioStreamBasicDescription description; + dataSize = sizeof( AudioStreamBasicDescription ); + property.mSelector = kAudioStreamPropertyVirtualFormat; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &description ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting stream format for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the sample rate and data format id. However, only make the + // change if the sample rate is not within 1.0 of the desired + // rate and the format is not linear pcm. + bool updateFormat = false; + if ( fabs( description.mSampleRate - (Float64)sampleRate ) > 1.0 ) { + description.mSampleRate = (Float64) sampleRate; + updateFormat = true; + } + + if ( description.mFormatID != kAudioFormatLinearPCM ) { + description.mFormatID = kAudioFormatLinearPCM; + updateFormat = true; + } + + if ( updateFormat ) { + result = AudioObjectSetPropertyData( id, &property, 0, NULL, dataSize, &description ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") setting sample rate or data format for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // Now check the physical format. + property.mSelector = kAudioStreamPropertyPhysicalFormat; + result = AudioObjectGetPropertyData( id, &property, 0, NULL, &dataSize, &description ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error (" << getErrorCode( result ) << ") getting stream physical format for device (" << device << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + //std::cout << "Current physical stream format:" << std::endl; + //std::cout << " mBitsPerChan = " << description.mBitsPerChannel << std::endl; + //std::cout << " aligned high = " << (description.mFormatFlags & kAudioFormatFlagIsAlignedHigh) << ", isPacked = " << (description.mFormatFlags & kAudioFormatFlagIsPacked) << std::endl; + //std::cout << " bytesPerFrame = " << description.mBytesPerFrame << std::endl; + //std::cout << " sample rate = " << description.mSampleRate << std::endl; + + if ( description.mFormatID != kAudioFormatLinearPCM || description.mBitsPerChannel < 16 ) { + description.mFormatID = kAudioFormatLinearPCM; + //description.mSampleRate = (Float64) sampleRate; + AudioStreamBasicDescription testDescription = description; + UInt32 formatFlags; + + // We'll try higher bit rates first and then work our way down. + std::vector< std::pair > physicalFormats; + formatFlags = (description.mFormatFlags | kLinearPCMFormatFlagIsFloat) & ~kLinearPCMFormatFlagIsSignedInteger; + physicalFormats.push_back( std::pair( 32, formatFlags ) ); + formatFlags = (description.mFormatFlags | kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked) & ~kLinearPCMFormatFlagIsFloat; + physicalFormats.push_back( std::pair( 32, formatFlags ) ); + physicalFormats.push_back( std::pair( 24, formatFlags ) ); // 24-bit packed + formatFlags &= ~( kAudioFormatFlagIsPacked | kAudioFormatFlagIsAlignedHigh ); + physicalFormats.push_back( std::pair( 24.2, formatFlags ) ); // 24-bit in 4 bytes, aligned low + formatFlags |= kAudioFormatFlagIsAlignedHigh; + physicalFormats.push_back( std::pair( 24.4, formatFlags ) ); // 24-bit in 4 bytes, aligned high + formatFlags = (description.mFormatFlags | kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked) & ~kLinearPCMFormatFlagIsFloat; + physicalFormats.push_back( std::pair( 16, formatFlags ) ); + physicalFormats.push_back( std::pair( 8, formatFlags ) ); + + bool setPhysicalFormat = false; + for( unsigned int i=0; iflags & RTAUDIO_NONINTERLEAVED ) stream_.userInterleaved = false; + else stream_.userInterleaved = true; + stream_.deviceInterleaved[mode] = true; + if ( monoMode == true ) stream_.deviceInterleaved[mode] = false; + + // Set flags for buffer conversion. + stream_.doConvertBuffer[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.nUserChannels[mode] < stream_.nDeviceChannels[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( streamCount == 1 ) { + if ( stream_.nUserChannels[mode] > 1 && + stream_.userInterleaved != stream_.deviceInterleaved[mode] ) + stream_.doConvertBuffer[mode] = true; + } + else if ( monoMode && stream_.userInterleaved ) + stream_.doConvertBuffer[mode] = true; + + // Allocate our CoreHandle structure for the stream. + CoreHandle *handle = 0; + if ( stream_.apiHandle == 0 ) { + try { + handle = new CoreHandle; + } + catch ( std::bad_alloc& ) { + errorText_ = "RtApiCore::probeDeviceOpen: error allocating CoreHandle memory."; + goto error; + } + + if ( pthread_cond_init( &handle->condition, NULL ) ) { + errorText_ = "RtApiCore::probeDeviceOpen: error initializing pthread condition variable."; + goto error; + } + stream_.apiHandle = (void *) handle; + } + else + handle = (CoreHandle *) stream_.apiHandle; + handle->iStream[mode] = firstStream; + handle->nStreams[mode] = streamCount; + handle->id[mode] = id; + + // Allocate necessary internal buffers. + unsigned long bufferBytes; + bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + // stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + stream_.userBuffer[mode] = (char *) malloc( bufferBytes * sizeof(char) ); + memset( stream_.userBuffer[mode], 0, bufferBytes * sizeof(char) ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiCore::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + + // If possible, we will make use of the CoreAudio stream buffers as + // "device buffers". However, we can't do this if using multiple + // streams. + if ( stream_.doConvertBuffer[mode] && handle->nStreams[mode] > 1 ) { + + bool makeBuffer = true; + bufferBytes = stream_.nDeviceChannels[mode] * formatBytes( stream_.deviceFormat[mode] ); + if ( mode == INPUT ) { + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + if ( bufferBytes <= bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiCore::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + stream_.sampleRate = sampleRate; + stream_.device[mode] = device; + stream_.state = STREAM_STOPPED; + stream_.callbackInfo.object = (void *) this; + + // Setup the buffer conversion information structure. + if ( stream_.doConvertBuffer[mode] ) { + if ( streamCount > 1 ) setConvertInfo( mode, 0 ); + else setConvertInfo( mode, channelOffset ); + } + + if ( mode == INPUT && stream_.mode == OUTPUT && stream_.device[0] == device ) + // Only one callback procedure per device. + stream_.mode = DUPLEX; + else { +#if defined( MAC_OS_X_VERSION_10_5 ) && ( MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 ) + result = AudioDeviceCreateIOProcID( id, callbackHandler, (void *) &stream_.callbackInfo, &handle->procId[mode] ); +#else + // deprecated in favor of AudioDeviceCreateIOProcID() + result = AudioDeviceAddIOProc( id, callbackHandler, (void *) &stream_.callbackInfo ); +#endif + if ( result != noErr ) { + errorStream_ << "RtApiCore::probeDeviceOpen: system error setting callback for device (" << device << ")."; + errorText_ = errorStream_.str(); + goto error; + } + if ( stream_.mode == OUTPUT && mode == INPUT ) + stream_.mode = DUPLEX; + else + stream_.mode = mode; + } + + // Setup the device property listener for over/underload. + property.mSelector = kAudioDeviceProcessorOverload; + property.mScope = kAudioObjectPropertyScopeGlobal; + result = AudioObjectAddPropertyListener( id, &property, xrunListener, (void *) handle ); + + return SUCCESS; + + error: + if ( handle ) { + pthread_cond_destroy( &handle->condition ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.state = STREAM_CLOSED; + return FAILURE; +} + +void RtApiCore :: closeStream( void ) +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiCore::closeStream(): no open stream to close!"; + error( RtAudioError::WARNING ); + return; + } + + CoreHandle *handle = (CoreHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + if ( stream_.state == STREAM_RUNNING ) + AudioDeviceStop( handle->id[0], callbackHandler ); +#if defined( MAC_OS_X_VERSION_10_5 ) && ( MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 ) + AudioDeviceDestroyIOProcID( handle->id[0], handle->procId[0] ); +#else + // deprecated in favor of AudioDeviceDestroyIOProcID() + AudioDeviceRemoveIOProc( handle->id[0], callbackHandler ); +#endif + } + + if ( stream_.mode == INPUT || ( stream_.mode == DUPLEX && stream_.device[0] != stream_.device[1] ) ) { + if ( stream_.state == STREAM_RUNNING ) + AudioDeviceStop( handle->id[1], callbackHandler ); +#if defined( MAC_OS_X_VERSION_10_5 ) && ( MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 ) + AudioDeviceDestroyIOProcID( handle->id[1], handle->procId[1] ); +#else + // deprecated in favor of AudioDeviceDestroyIOProcID() + AudioDeviceRemoveIOProc( handle->id[1], callbackHandler ); +#endif + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + // Destroy pthread condition variable. + pthread_cond_destroy( &handle->condition ); + delete handle; + stream_.apiHandle = 0; + + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; +} + +void RtApiCore :: startStream( void ) +{ + verifyStream(); + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiCore::startStream(): the stream is already running!"; + error( RtAudioError::WARNING ); + return; + } + + OSStatus result = noErr; + CoreHandle *handle = (CoreHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + result = AudioDeviceStart( handle->id[0], callbackHandler ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::startStream: system error (" << getErrorCode( result ) << ") starting callback procedure on device (" << stream_.device[0] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + if ( stream_.mode == INPUT || + ( stream_.mode == DUPLEX && stream_.device[0] != stream_.device[1] ) ) { + + result = AudioDeviceStart( handle->id[1], callbackHandler ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::startStream: system error starting input callback procedure on device (" << stream_.device[1] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + handle->drainCounter = 0; + handle->internalDrain = false; + stream_.state = STREAM_RUNNING; + + unlock: + if ( result == noErr ) return; + error( RtAudioError::SYSTEM_ERROR ); +} + +void RtApiCore :: stopStream( void ) +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiCore::stopStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + OSStatus result = noErr; + CoreHandle *handle = (CoreHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + if ( handle->drainCounter == 0 ) { + handle->drainCounter = 2; + pthread_cond_wait( &handle->condition, &stream_.mutex ); // block until signaled + } + + result = AudioDeviceStop( handle->id[0], callbackHandler ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::stopStream: system error (" << getErrorCode( result ) << ") stopping callback procedure on device (" << stream_.device[0] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + if ( stream_.mode == INPUT || ( stream_.mode == DUPLEX && stream_.device[0] != stream_.device[1] ) ) { + + result = AudioDeviceStop( handle->id[1], callbackHandler ); + if ( result != noErr ) { + errorStream_ << "RtApiCore::stopStream: system error (" << getErrorCode( result ) << ") stopping input callback procedure on device (" << stream_.device[1] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + stream_.state = STREAM_STOPPED; + + unlock: + if ( result == noErr ) return; + error( RtAudioError::SYSTEM_ERROR ); +} + +void RtApiCore :: abortStream( void ) +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiCore::abortStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + CoreHandle *handle = (CoreHandle *) stream_.apiHandle; + handle->drainCounter = 2; + + stopStream(); +} + +// This function will be called by a spawned thread when the user +// callback function signals that the stream should be stopped or +// aborted. It is better to handle it this way because the +// callbackEvent() function probably should return before the AudioDeviceStop() +// function is called. +static void *coreStopStream( void *ptr ) +{ + CallbackInfo *info = (CallbackInfo *) ptr; + RtApiCore *object = (RtApiCore *) info->object; + + object->stopStream(); + pthread_exit( NULL ); +} + +bool RtApiCore :: callbackEvent( AudioDeviceID deviceId, + const AudioBufferList *inBufferList, + const AudioBufferList *outBufferList ) +{ + if ( stream_.state == STREAM_STOPPED || stream_.state == STREAM_STOPPING ) return SUCCESS; + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiCore::callbackEvent(): the stream is closed ... this shouldn't happen!"; + error( RtAudioError::WARNING ); + return FAILURE; + } + + CallbackInfo *info = (CallbackInfo *) &stream_.callbackInfo; + CoreHandle *handle = (CoreHandle *) stream_.apiHandle; + + // Check if we were draining the stream and signal is finished. + if ( handle->drainCounter > 3 ) { + ThreadHandle threadId; + + stream_.state = STREAM_STOPPING; + if ( handle->internalDrain == true ) + pthread_create( &threadId, NULL, coreStopStream, info ); + else // external call to stopStream() + pthread_cond_signal( &handle->condition ); + return SUCCESS; + } + + AudioDeviceID outputDevice = handle->id[0]; + + // Invoke user callback to get fresh output data UNLESS we are + // draining stream or duplex mode AND the input/output devices are + // different AND this function is called for the input device. + if ( handle->drainCounter == 0 && ( stream_.mode != DUPLEX || deviceId == outputDevice ) ) { + RtAudioCallback callback = (RtAudioCallback) info->callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && handle->xrun[0] == true ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + handle->xrun[0] = false; + } + if ( stream_.mode != OUTPUT && handle->xrun[1] == true ) { + status |= RTAUDIO_INPUT_OVERFLOW; + handle->xrun[1] = false; + } + + int cbReturnValue = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, info->userData ); + if ( cbReturnValue == 2 ) { + stream_.state = STREAM_STOPPING; + handle->drainCounter = 2; + abortStream(); + return SUCCESS; + } + else if ( cbReturnValue == 1 ) { + handle->drainCounter = 1; + handle->internalDrain = true; + } + } + + if ( stream_.mode == OUTPUT || ( stream_.mode == DUPLEX && deviceId == outputDevice ) ) { + + if ( handle->drainCounter > 1 ) { // write zeros to the output stream + + if ( handle->nStreams[0] == 1 ) { + memset( outBufferList->mBuffers[handle->iStream[0]].mData, + 0, + outBufferList->mBuffers[handle->iStream[0]].mDataByteSize ); + } + else { // fill multiple streams with zeros + for ( unsigned int i=0; inStreams[0]; i++ ) { + memset( outBufferList->mBuffers[handle->iStream[0]+i].mData, + 0, + outBufferList->mBuffers[handle->iStream[0]+i].mDataByteSize ); + } + } + } + else if ( handle->nStreams[0] == 1 ) { + if ( stream_.doConvertBuffer[0] ) { // convert directly to CoreAudio stream buffer + convertBuffer( (char *) outBufferList->mBuffers[handle->iStream[0]].mData, + stream_.userBuffer[0], stream_.convertInfo[0] ); + } + else { // copy from user buffer + memcpy( outBufferList->mBuffers[handle->iStream[0]].mData, + stream_.userBuffer[0], + outBufferList->mBuffers[handle->iStream[0]].mDataByteSize ); + } + } + else { // fill multiple streams + Float32 *inBuffer = (Float32 *) stream_.userBuffer[0]; + if ( stream_.doConvertBuffer[0] ) { + convertBuffer( stream_.deviceBuffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + inBuffer = (Float32 *) stream_.deviceBuffer; + } + + if ( stream_.deviceInterleaved[0] == false ) { // mono mode + UInt32 bufferBytes = outBufferList->mBuffers[handle->iStream[0]].mDataByteSize; + for ( unsigned int i=0; imBuffers[handle->iStream[0]+i].mData, + (void *)&inBuffer[i*stream_.bufferSize], bufferBytes ); + } + } + else { // fill multiple multi-channel streams with interleaved data + UInt32 streamChannels, channelsLeft, inJump, outJump, inOffset; + Float32 *out, *in; + + bool inInterleaved = ( stream_.userInterleaved ) ? true : false; + UInt32 inChannels = stream_.nUserChannels[0]; + if ( stream_.doConvertBuffer[0] ) { + inInterleaved = true; // device buffer will always be interleaved for nStreams > 1 and not mono mode + inChannels = stream_.nDeviceChannels[0]; + } + + if ( inInterleaved ) inOffset = 1; + else inOffset = stream_.bufferSize; + + channelsLeft = inChannels; + for ( unsigned int i=0; inStreams[0]; i++ ) { + in = inBuffer; + out = (Float32 *) outBufferList->mBuffers[handle->iStream[0]+i].mData; + streamChannels = outBufferList->mBuffers[handle->iStream[0]+i].mNumberChannels; + + outJump = 0; + // Account for possible channel offset in first stream + if ( i == 0 && stream_.channelOffset[0] > 0 ) { + streamChannels -= stream_.channelOffset[0]; + outJump = stream_.channelOffset[0]; + out += outJump; + } + + // Account for possible unfilled channels at end of the last stream + if ( streamChannels > channelsLeft ) { + outJump = streamChannels - channelsLeft; + streamChannels = channelsLeft; + } + + // Determine input buffer offsets and skips + if ( inInterleaved ) { + inJump = inChannels; + in += inChannels - channelsLeft; + } + else { + inJump = 1; + in += (inChannels - channelsLeft) * inOffset; + } + + for ( unsigned int i=0; idrainCounter ) { + handle->drainCounter++; + goto unlock; + } + + AudioDeviceID inputDevice; + inputDevice = handle->id[1]; + if ( stream_.mode == INPUT || ( stream_.mode == DUPLEX && deviceId == inputDevice ) ) { + + if ( handle->nStreams[1] == 1 ) { + if ( stream_.doConvertBuffer[1] ) { // convert directly from CoreAudio stream buffer + convertBuffer( stream_.userBuffer[1], + (char *) inBufferList->mBuffers[handle->iStream[1]].mData, + stream_.convertInfo[1] ); + } + else { // copy to user buffer + memcpy( stream_.userBuffer[1], + inBufferList->mBuffers[handle->iStream[1]].mData, + inBufferList->mBuffers[handle->iStream[1]].mDataByteSize ); + } + } + else { // read from multiple streams + Float32 *outBuffer = (Float32 *) stream_.userBuffer[1]; + if ( stream_.doConvertBuffer[1] ) outBuffer = (Float32 *) stream_.deviceBuffer; + + if ( stream_.deviceInterleaved[1] == false ) { // mono mode + UInt32 bufferBytes = inBufferList->mBuffers[handle->iStream[1]].mDataByteSize; + for ( unsigned int i=0; imBuffers[handle->iStream[1]+i].mData, bufferBytes ); + } + } + else { // read from multiple multi-channel streams + UInt32 streamChannels, channelsLeft, inJump, outJump, outOffset; + Float32 *out, *in; + + bool outInterleaved = ( stream_.userInterleaved ) ? true : false; + UInt32 outChannels = stream_.nUserChannels[1]; + if ( stream_.doConvertBuffer[1] ) { + outInterleaved = true; // device buffer will always be interleaved for nStreams > 1 and not mono mode + outChannels = stream_.nDeviceChannels[1]; + } + + if ( outInterleaved ) outOffset = 1; + else outOffset = stream_.bufferSize; + + channelsLeft = outChannels; + for ( unsigned int i=0; inStreams[1]; i++ ) { + out = outBuffer; + in = (Float32 *) inBufferList->mBuffers[handle->iStream[1]+i].mData; + streamChannels = inBufferList->mBuffers[handle->iStream[1]+i].mNumberChannels; + + inJump = 0; + // Account for possible channel offset in first stream + if ( i == 0 && stream_.channelOffset[1] > 0 ) { + streamChannels -= stream_.channelOffset[1]; + inJump = stream_.channelOffset[1]; + in += inJump; + } + + // Account for possible unread channels at end of the last stream + if ( streamChannels > channelsLeft ) { + inJump = streamChannels - channelsLeft; + streamChannels = channelsLeft; + } + + // Determine output buffer offsets and skips + if ( outInterleaved ) { + outJump = outChannels; + out += outChannels - channelsLeft; + } + else { + outJump = 1; + out += (outChannels - channelsLeft) * outOffset; + } + + for ( unsigned int i=0; i +#include +#include + +// A structure to hold various information related to the Jack API +// implementation. +struct JackHandle { + jack_client_t *client; + jack_port_t **ports[2]; + std::string deviceName[2]; + bool xrun[2]; + pthread_cond_t condition; + int drainCounter; // Tracks callback counts when draining + bool internalDrain; // Indicates if stop is initiated from callback or not. + + JackHandle() + :client(0), drainCounter(0), internalDrain(false) { ports[0] = 0; ports[1] = 0; xrun[0] = false; xrun[1] = false; } +}; + +static void jackSilentError( const char * ) {}; + +RtApiJack :: RtApiJack() +{ + // Nothing to do here. +#if !defined(__RTAUDIO_DEBUG__) + // Turn off Jack's internal error reporting. + jack_set_error_function( &jackSilentError ); +#endif +} + +RtApiJack :: ~RtApiJack() +{ + if ( stream_.state != STREAM_CLOSED ) closeStream(); +} + +unsigned int RtApiJack :: getDeviceCount( void ) +{ + // See if we can become a jack client. + jack_options_t options = (jack_options_t) ( JackNoStartServer ); //JackNullOption; + jack_status_t *status = NULL; + jack_client_t *client = jack_client_open( "RtApiJackCount", options, status ); + if ( client == 0 ) return 0; + + const char **ports; + std::string port, previousPort; + unsigned int nChannels = 0, nDevices = 0; + ports = jack_get_ports( client, NULL, NULL, 0 ); + if ( ports ) { + // Parse the port names up to the first colon (:). + size_t iColon = 0; + do { + port = (char *) ports[ nChannels ]; + iColon = port.find(":"); + if ( iColon != std::string::npos ) { + port = port.substr( 0, iColon + 1 ); + if ( port != previousPort ) { + nDevices++; + previousPort = port; + } + } + } while ( ports[++nChannels] ); + free( ports ); + } + + jack_client_close( client ); + return nDevices; +} + +RtAudio::DeviceInfo RtApiJack :: getDeviceInfo( unsigned int device ) +{ + RtAudio::DeviceInfo info; + info.probed = false; + + jack_options_t options = (jack_options_t) ( JackNoStartServer ); //JackNullOption + jack_status_t *status = NULL; + jack_client_t *client = jack_client_open( "RtApiJackInfo", options, status ); + if ( client == 0 ) { + errorText_ = "RtApiJack::getDeviceInfo: Jack server not found or connection error!"; + error( RtAudioError::WARNING ); + return info; + } + + const char **ports; + std::string port, previousPort; + unsigned int nPorts = 0, nDevices = 0; + ports = jack_get_ports( client, NULL, NULL, 0 ); + if ( ports ) { + // Parse the port names up to the first colon (:). + size_t iColon = 0; + do { + port = (char *) ports[ nPorts ]; + iColon = port.find(":"); + if ( iColon != std::string::npos ) { + port = port.substr( 0, iColon ); + if ( port != previousPort ) { + if ( nDevices == device ) info.name = port; + nDevices++; + previousPort = port; + } + } + } while ( ports[++nPorts] ); + free( ports ); + } + + if ( device >= nDevices ) { + jack_client_close( client ); + errorText_ = "RtApiJack::getDeviceInfo: device ID is invalid!"; + error( RtAudioError::INVALID_USE ); + return info; + } + + // Get the current jack server sample rate. + info.sampleRates.clear(); + info.sampleRates.push_back( jack_get_sample_rate( client ) ); + + // Count the available ports containing the client name as device + // channels. Jack "input ports" equal RtAudio output channels. + unsigned int nChannels = 0; + ports = jack_get_ports( client, info.name.c_str(), NULL, JackPortIsInput ); + if ( ports ) { + while ( ports[ nChannels ] ) nChannels++; + free( ports ); + info.outputChannels = nChannels; + } + + // Jack "output ports" equal RtAudio input channels. + nChannels = 0; + ports = jack_get_ports( client, info.name.c_str(), NULL, JackPortIsOutput ); + if ( ports ) { + while ( ports[ nChannels ] ) nChannels++; + free( ports ); + info.inputChannels = nChannels; + } + + if ( info.outputChannels == 0 && info.inputChannels == 0 ) { + jack_client_close(client); + errorText_ = "RtApiJack::getDeviceInfo: error determining Jack input/output channels!"; + error( RtAudioError::WARNING ); + return info; + } + + // If device opens for both playback and capture, we determine the channels. + if ( info.outputChannels > 0 && info.inputChannels > 0 ) + info.duplexChannels = (info.outputChannels > info.inputChannels) ? info.inputChannels : info.outputChannels; + + // Jack always uses 32-bit floats. + info.nativeFormats = RTAUDIO_FLOAT32; + + // Jack doesn't provide default devices so we'll use the first available one. + if ( device == 0 && info.outputChannels > 0 ) + info.isDefaultOutput = true; + if ( device == 0 && info.inputChannels > 0 ) + info.isDefaultInput = true; + + jack_client_close(client); + info.probed = true; + return info; +} + +static int jackCallbackHandler( jack_nframes_t nframes, void *infoPointer ) +{ + CallbackInfo *info = (CallbackInfo *) infoPointer; + + RtApiJack *object = (RtApiJack *) info->object; + if ( object->callbackEvent( (unsigned long) nframes ) == false ) return 1; + + return 0; +} + +// This function will be called by a spawned thread when the Jack +// server signals that it is shutting down. It is necessary to handle +// it this way because the jackShutdown() function must return before +// the jack_deactivate() function (in closeStream()) will return. +static void *jackCloseStream( void *ptr ) +{ + CallbackInfo *info = (CallbackInfo *) ptr; + RtApiJack *object = (RtApiJack *) info->object; + + object->closeStream(); + + pthread_exit( NULL ); +} +static void jackShutdown( void *infoPointer ) +{ + CallbackInfo *info = (CallbackInfo *) infoPointer; + RtApiJack *object = (RtApiJack *) info->object; + + // Check current stream state. If stopped, then we'll assume this + // was called as a result of a call to RtApiJack::stopStream (the + // deactivation of a client handle causes this function to be called). + // If not, we'll assume the Jack server is shutting down or some + // other problem occurred and we should close the stream. + if ( object->isStreamRunning() == false ) return; + + ThreadHandle threadId; + pthread_create( &threadId, NULL, jackCloseStream, info ); + std::cerr << "\nRtApiJack: the Jack server is shutting down this client ... stream stopped and closed!!\n" << std::endl; +} + +static int jackXrun( void *infoPointer ) +{ + JackHandle *handle = (JackHandle *) infoPointer; + + if ( handle->ports[0] ) handle->xrun[0] = true; + if ( handle->ports[1] ) handle->xrun[1] = true; + + return 0; +} + +bool RtApiJack :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ) +{ + JackHandle *handle = (JackHandle *) stream_.apiHandle; + + // Look for jack server and try to become a client (only do once per stream). + jack_client_t *client = 0; + if ( mode == OUTPUT || ( mode == INPUT && stream_.mode != OUTPUT ) ) { + jack_options_t jackoptions = (jack_options_t) ( JackNoStartServer ); //JackNullOption; + jack_status_t *status = NULL; + if ( options && !options->streamName.empty() ) + client = jack_client_open( options->streamName.c_str(), jackoptions, status ); + else + client = jack_client_open( "RtApiJack", jackoptions, status ); + if ( client == 0 ) { + errorText_ = "RtApiJack::probeDeviceOpen: Jack server not found or connection error!"; + error( RtAudioError::WARNING ); + return FAILURE; + } + } + else { + // The handle must have been created on an earlier pass. + client = handle->client; + } + + const char **ports; + std::string port, previousPort, deviceName; + unsigned int nPorts = 0, nDevices = 0; + ports = jack_get_ports( client, NULL, NULL, 0 ); + if ( ports ) { + // Parse the port names up to the first colon (:). + size_t iColon = 0; + do { + port = (char *) ports[ nPorts ]; + iColon = port.find(":"); + if ( iColon != std::string::npos ) { + port = port.substr( 0, iColon ); + if ( port != previousPort ) { + if ( nDevices == device ) deviceName = port; + nDevices++; + previousPort = port; + } + } + } while ( ports[++nPorts] ); + free( ports ); + } + + if ( device >= nDevices ) { + errorText_ = "RtApiJack::probeDeviceOpen: device ID is invalid!"; + return FAILURE; + } + + // Count the available ports containing the client name as device + // channels. Jack "input ports" equal RtAudio output channels. + unsigned int nChannels = 0; + unsigned long flag = JackPortIsInput; + if ( mode == INPUT ) flag = JackPortIsOutput; + ports = jack_get_ports( client, deviceName.c_str(), NULL, flag ); + if ( ports ) { + while ( ports[ nChannels ] ) nChannels++; + free( ports ); + } + + // Compare the jack ports for specified client to the requested number of channels. + if ( nChannels < (channels + firstChannel) ) { + errorStream_ << "RtApiJack::probeDeviceOpen: requested number of channels (" << channels << ") + offset (" << firstChannel << ") not found for specified device (" << device << ":" << deviceName << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Check the jack server sample rate. + unsigned int jackRate = jack_get_sample_rate( client ); + if ( sampleRate != jackRate ) { + jack_client_close( client ); + errorStream_ << "RtApiJack::probeDeviceOpen: the requested sample rate (" << sampleRate << ") is different than the JACK server rate (" << jackRate << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + stream_.sampleRate = jackRate; + + // Get the latency of the JACK port. + ports = jack_get_ports( client, deviceName.c_str(), NULL, flag ); + if ( ports[ firstChannel ] ) { + // Added by Ge Wang + jack_latency_callback_mode_t cbmode = (mode == INPUT ? JackCaptureLatency : JackPlaybackLatency); + // the range (usually the min and max are equal) + jack_latency_range_t latrange; latrange.min = latrange.max = 0; + // get the latency range + jack_port_get_latency_range( jack_port_by_name( client, ports[firstChannel] ), cbmode, &latrange ); + // be optimistic, use the min! + stream_.latency[mode] = latrange.min; + //stream_.latency[mode] = jack_port_get_latency( jack_port_by_name( client, ports[ firstChannel ] ) ); + } + free( ports ); + + // The jack server always uses 32-bit floating-point data. + stream_.deviceFormat[mode] = RTAUDIO_FLOAT32; + stream_.userFormat = format; + + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) stream_.userInterleaved = false; + else stream_.userInterleaved = true; + + // Jack always uses non-interleaved buffers. + stream_.deviceInterleaved[mode] = false; + + // Jack always provides host byte-ordered data. + stream_.doByteSwap[mode] = false; + + // Get the buffer size. The buffer size and number of buffers + // (periods) is set when the jack server is started. + stream_.bufferSize = (int) jack_get_buffer_size( client ); + *bufferSize = stream_.bufferSize; + + stream_.nDeviceChannels[mode] = channels; + stream_.nUserChannels[mode] = channels; + + // Set flags for buffer conversion. + stream_.doConvertBuffer[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] && + stream_.nUserChannels[mode] > 1 ) + stream_.doConvertBuffer[mode] = true; + + // Allocate our JackHandle structure for the stream. + if ( handle == 0 ) { + try { + handle = new JackHandle; + } + catch ( std::bad_alloc& ) { + errorText_ = "RtApiJack::probeDeviceOpen: error allocating JackHandle memory."; + goto error; + } + + if ( pthread_cond_init(&handle->condition, NULL) ) { + errorText_ = "RtApiJack::probeDeviceOpen: error initializing pthread condition variable."; + goto error; + } + stream_.apiHandle = (void *) handle; + handle->client = client; + } + handle->deviceName[mode] = deviceName; + + // Allocate necessary internal buffers. + unsigned long bufferBytes; + bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiJack::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + + if ( stream_.doConvertBuffer[mode] ) { + + bool makeBuffer = true; + if ( mode == OUTPUT ) + bufferBytes = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + else { // mode == INPUT + bufferBytes = stream_.nDeviceChannels[1] * formatBytes( stream_.deviceFormat[1] ); + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes(stream_.deviceFormat[0]); + if ( bufferBytes < bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiJack::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + // Allocate memory for the Jack ports (channels) identifiers. + handle->ports[mode] = (jack_port_t **) malloc ( sizeof (jack_port_t *) * channels ); + if ( handle->ports[mode] == NULL ) { + errorText_ = "RtApiJack::probeDeviceOpen: error allocating port memory."; + goto error; + } + + stream_.device[mode] = device; + stream_.channelOffset[mode] = firstChannel; + stream_.state = STREAM_STOPPED; + stream_.callbackInfo.object = (void *) this; + + if ( stream_.mode == OUTPUT && mode == INPUT ) + // We had already set up the stream for output. + stream_.mode = DUPLEX; + else { + stream_.mode = mode; + jack_set_process_callback( handle->client, jackCallbackHandler, (void *) &stream_.callbackInfo ); + jack_set_xrun_callback( handle->client, jackXrun, (void *) &handle ); + jack_on_shutdown( handle->client, jackShutdown, (void *) &stream_.callbackInfo ); + } + + // Register our ports. + char label[64]; + if ( mode == OUTPUT ) { + for ( unsigned int i=0; iports[0][i] = jack_port_register( handle->client, (const char *)label, + JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0 ); + } + } + else { + for ( unsigned int i=0; iports[1][i] = jack_port_register( handle->client, (const char *)label, + JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0 ); + } + } + + // Setup the buffer conversion information structure. We don't use + // buffers to do channel offsets, so we override that parameter + // here. + if ( stream_.doConvertBuffer[mode] ) setConvertInfo( mode, 0 ); + + return SUCCESS; + + error: + if ( handle ) { + pthread_cond_destroy( &handle->condition ); + jack_client_close( handle->client ); + + if ( handle->ports[0] ) free( handle->ports[0] ); + if ( handle->ports[1] ) free( handle->ports[1] ); + + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + return FAILURE; +} + +void RtApiJack :: closeStream( void ) +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiJack::closeStream(): no open stream to close!"; + error( RtAudioError::WARNING ); + return; + } + + JackHandle *handle = (JackHandle *) stream_.apiHandle; + if ( handle ) { + + if ( stream_.state == STREAM_RUNNING ) + jack_deactivate( handle->client ); + + jack_client_close( handle->client ); + } + + if ( handle ) { + if ( handle->ports[0] ) free( handle->ports[0] ); + if ( handle->ports[1] ) free( handle->ports[1] ); + pthread_cond_destroy( &handle->condition ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; +} + +void RtApiJack :: startStream( void ) +{ + verifyStream(); + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiJack::startStream(): the stream is already running!"; + error( RtAudioError::WARNING ); + return; + } + + JackHandle *handle = (JackHandle *) stream_.apiHandle; + int result = jack_activate( handle->client ); + if ( result ) { + errorText_ = "RtApiJack::startStream(): unable to activate JACK client!"; + goto unlock; + } + + const char **ports; + + // Get the list of available ports. + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + result = 1; + ports = jack_get_ports( handle->client, handle->deviceName[0].c_str(), NULL, JackPortIsInput); + if ( ports == NULL) { + errorText_ = "RtApiJack::startStream(): error determining available JACK input ports!"; + goto unlock; + } + + // Now make the port connections. Since RtAudio wasn't designed to + // allow the user to select particular channels of a device, we'll + // just open the first "nChannels" ports with offset. + for ( unsigned int i=0; iclient, jack_port_name( handle->ports[0][i] ), ports[ stream_.channelOffset[0] + i ] ); + if ( result ) { + free( ports ); + errorText_ = "RtApiJack::startStream(): error connecting output ports!"; + goto unlock; + } + } + free(ports); + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + result = 1; + ports = jack_get_ports( handle->client, handle->deviceName[1].c_str(), NULL, JackPortIsOutput ); + if ( ports == NULL) { + errorText_ = "RtApiJack::startStream(): error determining available JACK output ports!"; + goto unlock; + } + + // Now make the port connections. See note above. + for ( unsigned int i=0; iclient, ports[ stream_.channelOffset[1] + i ], jack_port_name( handle->ports[1][i] ) ); + if ( result ) { + free( ports ); + errorText_ = "RtApiJack::startStream(): error connecting input ports!"; + goto unlock; + } + } + free(ports); + } + + handle->drainCounter = 0; + handle->internalDrain = false; + stream_.state = STREAM_RUNNING; + + unlock: + if ( result == 0 ) return; + error( RtAudioError::SYSTEM_ERROR ); +} + +void RtApiJack :: stopStream( void ) +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiJack::stopStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + JackHandle *handle = (JackHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + if ( handle->drainCounter == 0 ) { + handle->drainCounter = 2; + pthread_cond_wait( &handle->condition, &stream_.mutex ); // block until signaled + } + } + + jack_deactivate( handle->client ); + stream_.state = STREAM_STOPPED; +} + +void RtApiJack :: abortStream( void ) +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiJack::abortStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + JackHandle *handle = (JackHandle *) stream_.apiHandle; + handle->drainCounter = 2; + + stopStream(); +} + +// This function will be called by a spawned thread when the user +// callback function signals that the stream should be stopped or +// aborted. It is necessary to handle it this way because the +// callbackEvent() function must return before the jack_deactivate() +// function will return. +static void *jackStopStream( void *ptr ) +{ + CallbackInfo *info = (CallbackInfo *) ptr; + RtApiJack *object = (RtApiJack *) info->object; + + object->stopStream(); + pthread_exit( NULL ); +} + +bool RtApiJack :: callbackEvent( unsigned long nframes ) +{ + if ( stream_.state == STREAM_STOPPED || stream_.state == STREAM_STOPPING ) return SUCCESS; + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiCore::callbackEvent(): the stream is closed ... this shouldn't happen!"; + error( RtAudioError::WARNING ); + return FAILURE; + } + if ( stream_.bufferSize != nframes ) { + errorText_ = "RtApiCore::callbackEvent(): the JACK buffer size has changed ... cannot process!"; + error( RtAudioError::WARNING ); + return FAILURE; + } + + CallbackInfo *info = (CallbackInfo *) &stream_.callbackInfo; + JackHandle *handle = (JackHandle *) stream_.apiHandle; + + // Check if we were draining the stream and signal is finished. + if ( handle->drainCounter > 3 ) { + ThreadHandle threadId; + + stream_.state = STREAM_STOPPING; + if ( handle->internalDrain == true ) + pthread_create( &threadId, NULL, jackStopStream, info ); + else + pthread_cond_signal( &handle->condition ); + return SUCCESS; + } + + // Invoke user callback first, to get fresh output data. + if ( handle->drainCounter == 0 ) { + RtAudioCallback callback = (RtAudioCallback) info->callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && handle->xrun[0] == true ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + handle->xrun[0] = false; + } + if ( stream_.mode != OUTPUT && handle->xrun[1] == true ) { + status |= RTAUDIO_INPUT_OVERFLOW; + handle->xrun[1] = false; + } + int cbReturnValue = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, info->userData ); + if ( cbReturnValue == 2 ) { + stream_.state = STREAM_STOPPING; + handle->drainCounter = 2; + ThreadHandle id; + pthread_create( &id, NULL, jackStopStream, info ); + return SUCCESS; + } + else if ( cbReturnValue == 1 ) { + handle->drainCounter = 1; + handle->internalDrain = true; + } + } + + jack_default_audio_sample_t *jackbuffer; + unsigned long bufferBytes = nframes * sizeof( jack_default_audio_sample_t ); + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + if ( handle->drainCounter > 1 ) { // write zeros to the output stream + + for ( unsigned int i=0; iports[0][i], (jack_nframes_t) nframes ); + memset( jackbuffer, 0, bufferBytes ); + } + + } + else if ( stream_.doConvertBuffer[0] ) { + + convertBuffer( stream_.deviceBuffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + + for ( unsigned int i=0; iports[0][i], (jack_nframes_t) nframes ); + memcpy( jackbuffer, &stream_.deviceBuffer[i*bufferBytes], bufferBytes ); + } + } + else { // no buffer conversion + for ( unsigned int i=0; iports[0][i], (jack_nframes_t) nframes ); + memcpy( jackbuffer, &stream_.userBuffer[0][i*bufferBytes], bufferBytes ); + } + } + } + + // Don't bother draining input + if ( handle->drainCounter ) { + handle->drainCounter++; + goto unlock; + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + + if ( stream_.doConvertBuffer[1] ) { + for ( unsigned int i=0; iports[1][i], (jack_nframes_t) nframes ); + memcpy( &stream_.deviceBuffer[i*bufferBytes], jackbuffer, bufferBytes ); + } + convertBuffer( stream_.userBuffer[1], stream_.deviceBuffer, stream_.convertInfo[1] ); + } + else { // no buffer conversion + for ( unsigned int i=0; iports[1][i], (jack_nframes_t) nframes ); + memcpy( &stream_.userBuffer[1][i*bufferBytes], jackbuffer, bufferBytes ); + } + } + } + + unlock: + RtApi::tickStreamTime(); + return SUCCESS; +} + //******************** End of __UNIX_JACK__ *********************// +#endif + +#if defined(__WINDOWS_ASIO__) // ASIO API on Windows + +// The ASIO API is designed around a callback scheme, so this +// implementation is similar to that used for OS-X CoreAudio and Linux +// Jack. The primary constraint with ASIO is that it only allows +// access to a single driver at a time. Thus, it is not possible to +// have more than one simultaneous RtAudio stream. +// +// This implementation also requires a number of external ASIO files +// and a few global variables. The ASIO callback scheme does not +// allow for the passing of user data, so we must create a global +// pointer to our callbackInfo structure. +// +// On unix systems, we make use of a pthread condition variable. +// Since there is no equivalent in Windows, I hacked something based +// on information found in +// http://www.cs.wustl.edu/~schmidt/win32-cv-1.html. + +#include "asiosys.h" +#include "asio.h" +#include "iasiothiscallresolver.h" +#include "asiodrivers.h" +#include + +static AsioDrivers drivers; +static ASIOCallbacks asioCallbacks; +static ASIODriverInfo driverInfo; +static CallbackInfo *asioCallbackInfo; +static bool asioXRun; + +struct AsioHandle { + int drainCounter; // Tracks callback counts when draining + bool internalDrain; // Indicates if stop is initiated from callback or not. + ASIOBufferInfo *bufferInfos; + HANDLE condition; + + AsioHandle() + :drainCounter(0), internalDrain(false), bufferInfos(0) {} +}; + +// Function declarations (definitions at end of section) +static const char* getAsioErrorString( ASIOError result ); +static void sampleRateChanged( ASIOSampleRate sRate ); +static long asioMessages( long selector, long value, void* message, double* opt ); + +RtApiAsio :: RtApiAsio() +{ + // ASIO cannot run on a multi-threaded appartment. You can call + // CoInitialize beforehand, but it must be for appartment threading + // (in which case, CoInitilialize will return S_FALSE here). + coInitialized_ = false; + HRESULT hr = CoInitialize( NULL ); + if ( FAILED(hr) ) { + errorText_ = "RtApiAsio::ASIO requires a single-threaded appartment. Call CoInitializeEx(0,COINIT_APARTMENTTHREADED)"; + error( RtAudioError::WARNING ); + } + coInitialized_ = true; + + drivers.removeCurrentDriver(); + driverInfo.asioVersion = 2; + + // See note in DirectSound implementation about GetDesktopWindow(). + driverInfo.sysRef = GetForegroundWindow(); +} + +RtApiAsio :: ~RtApiAsio() +{ + if ( stream_.state != STREAM_CLOSED ) closeStream(); + if ( coInitialized_ ) CoUninitialize(); +} + +unsigned int RtApiAsio :: getDeviceCount( void ) +{ + return (unsigned int) drivers.asioGetNumDev(); +} + +RtAudio::DeviceInfo RtApiAsio :: getDeviceInfo( unsigned int device ) +{ + RtAudio::DeviceInfo info; + info.probed = false; + + // Get device ID + unsigned int nDevices = getDeviceCount(); + if ( nDevices == 0 ) { + errorText_ = "RtApiAsio::getDeviceInfo: no devices found!"; + error( RtAudioError::INVALID_USE ); + return info; + } + + if ( device >= nDevices ) { + errorText_ = "RtApiAsio::getDeviceInfo: device ID is invalid!"; + error( RtAudioError::INVALID_USE ); + return info; + } + + // If a stream is already open, we cannot probe other devices. Thus, use the saved results. + if ( stream_.state != STREAM_CLOSED ) { + if ( device >= devices_.size() ) { + errorText_ = "RtApiAsio::getDeviceInfo: device ID was not present before stream was opened."; + error( RtAudioError::WARNING ); + return info; + } + return devices_[ device ]; + } + + char driverName[32]; + ASIOError result = drivers.asioGetDriverName( (int) device, driverName, 32 ); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::getDeviceInfo: unable to get driver name (" << getAsioErrorString( result ) << ")."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + info.name = driverName; + + if ( !drivers.loadDriver( driverName ) ) { + errorStream_ << "RtApiAsio::getDeviceInfo: unable to load driver (" << driverName << ")."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + result = ASIOInit( &driverInfo ); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::getDeviceInfo: error (" << getAsioErrorString( result ) << ") initializing driver (" << driverName << ")."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + // Determine the device channel information. + long inputChannels, outputChannels; + result = ASIOGetChannels( &inputChannels, &outputChannels ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::getDeviceInfo: error (" << getAsioErrorString( result ) << ") getting channel count (" << driverName << ")."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + info.outputChannels = outputChannels; + info.inputChannels = inputChannels; + if ( info.outputChannels > 0 && info.inputChannels > 0 ) + info.duplexChannels = (info.outputChannels > info.inputChannels) ? info.inputChannels : info.outputChannels; + + // Determine the supported sample rates. + info.sampleRates.clear(); + for ( unsigned int i=0; i 0 ) + if ( getDefaultOutputDevice() == device ) info.isDefaultOutput = true; + if ( info.inputChannels > 0 ) + if ( getDefaultInputDevice() == device ) info.isDefaultInput = true; + + info.probed = true; + drivers.removeCurrentDriver(); + return info; +} + +static void bufferSwitch( long index, ASIOBool /*processNow*/ ) +{ + RtApiAsio *object = (RtApiAsio *) asioCallbackInfo->object; + object->callbackEvent( index ); +} + +void RtApiAsio :: saveDeviceInfo( void ) +{ + devices_.clear(); + + unsigned int nDevices = getDeviceCount(); + devices_.resize( nDevices ); + for ( unsigned int i=0; isaveDeviceInfo(); + + if ( !drivers.loadDriver( driverName ) ) { + errorStream_ << "RtApiAsio::probeDeviceOpen: unable to load driver (" << driverName << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + result = ASIOInit( &driverInfo ); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::probeDeviceOpen: error (" << getAsioErrorString( result ) << ") initializing driver (" << driverName << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // Check the device channel count. + long inputChannels, outputChannels; + result = ASIOGetChannels( &inputChannels, &outputChannels ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: error (" << getAsioErrorString( result ) << ") getting channel count (" << driverName << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + if ( ( mode == OUTPUT && (channels+firstChannel) > (unsigned int) outputChannels) || + ( mode == INPUT && (channels+firstChannel) > (unsigned int) inputChannels) ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") does not support requested channel count (" << channels << ") + offset (" << firstChannel << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + stream_.nDeviceChannels[mode] = channels; + stream_.nUserChannels[mode] = channels; + stream_.channelOffset[mode] = firstChannel; + + // Verify the sample rate is supported. + result = ASIOCanSampleRate( (ASIOSampleRate) sampleRate ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") does not support requested sample rate (" << sampleRate << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Get the current sample rate + ASIOSampleRate currentRate; + result = ASIOGetSampleRate( ¤tRate ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") error getting sample rate."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the sample rate only if necessary + if ( currentRate != sampleRate ) { + result = ASIOSetSampleRate( (ASIOSampleRate) sampleRate ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") error setting sample rate (" << sampleRate << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // Determine the driver data type. + ASIOChannelInfo channelInfo; + channelInfo.channel = 0; + if ( mode == OUTPUT ) channelInfo.isInput = false; + else channelInfo.isInput = true; + result = ASIOGetChannelInfo( &channelInfo ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") error (" << getAsioErrorString( result ) << ") getting data format."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Assuming WINDOWS host is always little-endian. + stream_.doByteSwap[mode] = false; + stream_.userFormat = format; + stream_.deviceFormat[mode] = 0; + if ( channelInfo.type == ASIOSTInt16MSB || channelInfo.type == ASIOSTInt16LSB ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + if ( channelInfo.type == ASIOSTInt16MSB ) stream_.doByteSwap[mode] = true; + } + else if ( channelInfo.type == ASIOSTInt32MSB || channelInfo.type == ASIOSTInt32LSB ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + if ( channelInfo.type == ASIOSTInt32MSB ) stream_.doByteSwap[mode] = true; + } + else if ( channelInfo.type == ASIOSTFloat32MSB || channelInfo.type == ASIOSTFloat32LSB ) { + stream_.deviceFormat[mode] = RTAUDIO_FLOAT32; + if ( channelInfo.type == ASIOSTFloat32MSB ) stream_.doByteSwap[mode] = true; + } + else if ( channelInfo.type == ASIOSTFloat64MSB || channelInfo.type == ASIOSTFloat64LSB ) { + stream_.deviceFormat[mode] = RTAUDIO_FLOAT64; + if ( channelInfo.type == ASIOSTFloat64MSB ) stream_.doByteSwap[mode] = true; + } + else if ( channelInfo.type == ASIOSTInt24MSB || channelInfo.type == ASIOSTInt24LSB ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT24; + if ( channelInfo.type == ASIOSTInt24MSB ) stream_.doByteSwap[mode] = true; + } + + if ( stream_.deviceFormat[mode] == 0 ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") data format not supported by RtAudio."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the buffer size. For a duplex stream, this will end up + // setting the buffer size based on the input constraints, which + // should be ok. + long minSize, maxSize, preferSize, granularity; + result = ASIOGetBufferSize( &minSize, &maxSize, &preferSize, &granularity ); + if ( result != ASE_OK ) { + drivers.removeCurrentDriver(); + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") error (" << getAsioErrorString( result ) << ") getting buffer size."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + if ( *bufferSize < (unsigned int) minSize ) *bufferSize = (unsigned int) minSize; + else if ( *bufferSize > (unsigned int) maxSize ) *bufferSize = (unsigned int) maxSize; + else if ( granularity == -1 ) { + // Make sure bufferSize is a power of two. + int log2_of_min_size = 0; + int log2_of_max_size = 0; + + for ( unsigned int i = 0; i < sizeof(long) * 8; i++ ) { + if ( minSize & ((long)1 << i) ) log2_of_min_size = i; + if ( maxSize & ((long)1 << i) ) log2_of_max_size = i; + } + + long min_delta = std::abs( (long)*bufferSize - ((long)1 << log2_of_min_size) ); + int min_delta_num = log2_of_min_size; + + for (int i = log2_of_min_size + 1; i <= log2_of_max_size; i++) { + long current_delta = std::abs( (long)*bufferSize - ((long)1 << i) ); + if (current_delta < min_delta) { + min_delta = current_delta; + min_delta_num = i; + } + } + + *bufferSize = ( (unsigned int)1 << min_delta_num ); + if ( *bufferSize < (unsigned int) minSize ) *bufferSize = (unsigned int) minSize; + else if ( *bufferSize > (unsigned int) maxSize ) *bufferSize = (unsigned int) maxSize; + } + else if ( granularity != 0 ) { + // Set to an even multiple of granularity, rounding up. + *bufferSize = (*bufferSize + granularity-1) / granularity * granularity; + } + + if ( mode == INPUT && stream_.mode == OUTPUT && stream_.bufferSize != *bufferSize ) { + drivers.removeCurrentDriver(); + errorText_ = "RtApiAsio::probeDeviceOpen: input/output buffersize discrepancy!"; + return FAILURE; + } + + stream_.bufferSize = *bufferSize; + stream_.nBuffers = 2; + + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) stream_.userInterleaved = false; + else stream_.userInterleaved = true; + + // ASIO always uses non-interleaved buffers. + stream_.deviceInterleaved[mode] = false; + + // Allocate, if necessary, our AsioHandle structure for the stream. + AsioHandle *handle = (AsioHandle *) stream_.apiHandle; + if ( handle == 0 ) { + try { + handle = new AsioHandle; + } + catch ( std::bad_alloc& ) { + //if ( handle == NULL ) { + drivers.removeCurrentDriver(); + errorText_ = "RtApiAsio::probeDeviceOpen: error allocating AsioHandle memory."; + return FAILURE; + } + handle->bufferInfos = 0; + + // Create a manual-reset event. + handle->condition = CreateEvent( NULL, // no security + TRUE, // manual-reset + FALSE, // non-signaled initially + NULL ); // unnamed + stream_.apiHandle = (void *) handle; + } + + // Create the ASIO internal buffers. Since RtAudio sets up input + // and output separately, we'll have to dispose of previously + // created output buffers for a duplex stream. + long inputLatency, outputLatency; + if ( mode == INPUT && stream_.mode == OUTPUT ) { + ASIODisposeBuffers(); + if ( handle->bufferInfos ) free( handle->bufferInfos ); + } + + // Allocate, initialize, and save the bufferInfos in our stream callbackInfo structure. + bool buffersAllocated = false; + unsigned int i, nChannels = stream_.nDeviceChannels[0] + stream_.nDeviceChannels[1]; + handle->bufferInfos = (ASIOBufferInfo *) malloc( nChannels * sizeof(ASIOBufferInfo) ); + if ( handle->bufferInfos == NULL ) { + errorStream_ << "RtApiAsio::probeDeviceOpen: error allocating bufferInfo memory for driver (" << driverName << ")."; + errorText_ = errorStream_.str(); + goto error; + } + + ASIOBufferInfo *infos; + infos = handle->bufferInfos; + for ( i=0; iisInput = ASIOFalse; + infos->channelNum = i + stream_.channelOffset[0]; + infos->buffers[0] = infos->buffers[1] = 0; + } + for ( i=0; iisInput = ASIOTrue; + infos->channelNum = i + stream_.channelOffset[1]; + infos->buffers[0] = infos->buffers[1] = 0; + } + + // Set up the ASIO callback structure and create the ASIO data buffers. + asioCallbacks.bufferSwitch = &bufferSwitch; + asioCallbacks.sampleRateDidChange = &sampleRateChanged; + asioCallbacks.asioMessage = &asioMessages; + asioCallbacks.bufferSwitchTimeInfo = NULL; + result = ASIOCreateBuffers( handle->bufferInfos, nChannels, stream_.bufferSize, &asioCallbacks ); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") error (" << getAsioErrorString( result ) << ") creating buffers."; + errorText_ = errorStream_.str(); + goto error; + } + buffersAllocated = true; + + // Set flags for buffer conversion. + stream_.doConvertBuffer[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] && + stream_.nUserChannels[mode] > 1 ) + stream_.doConvertBuffer[mode] = true; + + // Allocate necessary internal buffers + unsigned long bufferBytes; + bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiAsio::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + + if ( stream_.doConvertBuffer[mode] ) { + + bool makeBuffer = true; + bufferBytes = stream_.nDeviceChannels[mode] * formatBytes( stream_.deviceFormat[mode] ); + if ( mode == INPUT ) { + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + if ( bufferBytes <= bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiAsio::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + stream_.sampleRate = sampleRate; + stream_.device[mode] = device; + stream_.state = STREAM_STOPPED; + asioCallbackInfo = &stream_.callbackInfo; + stream_.callbackInfo.object = (void *) this; + if ( stream_.mode == OUTPUT && mode == INPUT ) + // We had already set up an output stream. + stream_.mode = DUPLEX; + else + stream_.mode = mode; + + // Determine device latencies + result = ASIOGetLatencies( &inputLatency, &outputLatency ); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::probeDeviceOpen: driver (" << driverName << ") error (" << getAsioErrorString( result ) << ") getting latency."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING); // warn but don't fail + } + else { + stream_.latency[0] = outputLatency; + stream_.latency[1] = inputLatency; + } + + // Setup the buffer conversion information structure. We don't use + // buffers to do channel offsets, so we override that parameter + // here. + if ( stream_.doConvertBuffer[mode] ) setConvertInfo( mode, 0 ); + + return SUCCESS; + + error: + if ( buffersAllocated ) + ASIODisposeBuffers(); + drivers.removeCurrentDriver(); + + if ( handle ) { + CloseHandle( handle->condition ); + if ( handle->bufferInfos ) + free( handle->bufferInfos ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + return FAILURE; +} + +void RtApiAsio :: closeStream() +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiAsio::closeStream(): no open stream to close!"; + error( RtAudioError::WARNING ); + return; + } + + if ( stream_.state == STREAM_RUNNING ) { + stream_.state = STREAM_STOPPED; + ASIOStop(); + } + ASIODisposeBuffers(); + drivers.removeCurrentDriver(); + + AsioHandle *handle = (AsioHandle *) stream_.apiHandle; + if ( handle ) { + CloseHandle( handle->condition ); + if ( handle->bufferInfos ) + free( handle->bufferInfos ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; +} + +bool stopThreadCalled = false; + +void RtApiAsio :: startStream() +{ + verifyStream(); + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiAsio::startStream(): the stream is already running!"; + error( RtAudioError::WARNING ); + return; + } + + AsioHandle *handle = (AsioHandle *) stream_.apiHandle; + ASIOError result = ASIOStart(); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::startStream: error (" << getAsioErrorString( result ) << ") starting device."; + errorText_ = errorStream_.str(); + goto unlock; + } + + handle->drainCounter = 0; + handle->internalDrain = false; + ResetEvent( handle->condition ); + stream_.state = STREAM_RUNNING; + asioXRun = false; + + unlock: + stopThreadCalled = false; + + if ( result == ASE_OK ) return; + error( RtAudioError::SYSTEM_ERROR ); +} + +void RtApiAsio :: stopStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiAsio::stopStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + AsioHandle *handle = (AsioHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + if ( handle->drainCounter == 0 ) { + handle->drainCounter = 2; + WaitForSingleObject( handle->condition, INFINITE ); // block until signaled + } + } + + stream_.state = STREAM_STOPPED; + + ASIOError result = ASIOStop(); + if ( result != ASE_OK ) { + errorStream_ << "RtApiAsio::stopStream: error (" << getAsioErrorString( result ) << ") stopping device."; + errorText_ = errorStream_.str(); + } + + if ( result == ASE_OK ) return; + error( RtAudioError::SYSTEM_ERROR ); +} + +void RtApiAsio :: abortStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiAsio::abortStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + // The following lines were commented-out because some behavior was + // noted where the device buffers need to be zeroed to avoid + // continuing sound, even when the device buffers are completely + // disposed. So now, calling abort is the same as calling stop. + // AsioHandle *handle = (AsioHandle *) stream_.apiHandle; + // handle->drainCounter = 2; + stopStream(); +} + +// This function will be called by a spawned thread when the user +// callback function signals that the stream should be stopped or +// aborted. It is necessary to handle it this way because the +// callbackEvent() function must return before the ASIOStop() +// function will return. +static unsigned __stdcall asioStopStream( void *ptr ) +{ + CallbackInfo *info = (CallbackInfo *) ptr; + RtApiAsio *object = (RtApiAsio *) info->object; + + object->stopStream(); + _endthreadex( 0 ); + return 0; +} + +bool RtApiAsio :: callbackEvent( long bufferIndex ) +{ + if ( stream_.state == STREAM_STOPPED || stream_.state == STREAM_STOPPING ) return SUCCESS; + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiAsio::callbackEvent(): the stream is closed ... this shouldn't happen!"; + error( RtAudioError::WARNING ); + return FAILURE; + } + + CallbackInfo *info = (CallbackInfo *) &stream_.callbackInfo; + AsioHandle *handle = (AsioHandle *) stream_.apiHandle; + + // Check if we were draining the stream and signal if finished. + if ( handle->drainCounter > 3 ) { + + stream_.state = STREAM_STOPPING; + if ( handle->internalDrain == false ) + SetEvent( handle->condition ); + else { // spawn a thread to stop the stream + unsigned threadId; + stream_.callbackInfo.thread = _beginthreadex( NULL, 0, &asioStopStream, + &stream_.callbackInfo, 0, &threadId ); + } + return SUCCESS; + } + + // Invoke user callback to get fresh output data UNLESS we are + // draining stream. + if ( handle->drainCounter == 0 ) { + RtAudioCallback callback = (RtAudioCallback) info->callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && asioXRun == true ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + asioXRun = false; + } + if ( stream_.mode != OUTPUT && asioXRun == true ) { + status |= RTAUDIO_INPUT_OVERFLOW; + asioXRun = false; + } + int cbReturnValue = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, info->userData ); + if ( cbReturnValue == 2 ) { + stream_.state = STREAM_STOPPING; + handle->drainCounter = 2; + unsigned threadId; + stream_.callbackInfo.thread = _beginthreadex( NULL, 0, &asioStopStream, + &stream_.callbackInfo, 0, &threadId ); + return SUCCESS; + } + else if ( cbReturnValue == 1 ) { + handle->drainCounter = 1; + handle->internalDrain = true; + } + } + + unsigned int nChannels, bufferBytes, i, j; + nChannels = stream_.nDeviceChannels[0] + stream_.nDeviceChannels[1]; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + bufferBytes = stream_.bufferSize * formatBytes( stream_.deviceFormat[0] ); + + if ( handle->drainCounter > 1 ) { // write zeros to the output stream + + for ( i=0, j=0; ibufferInfos[i].isInput != ASIOTrue ) + memset( handle->bufferInfos[i].buffers[bufferIndex], 0, bufferBytes ); + } + + } + else if ( stream_.doConvertBuffer[0] ) { + + convertBuffer( stream_.deviceBuffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + if ( stream_.doByteSwap[0] ) + byteSwapBuffer( stream_.deviceBuffer, + stream_.bufferSize * stream_.nDeviceChannels[0], + stream_.deviceFormat[0] ); + + for ( i=0, j=0; ibufferInfos[i].isInput != ASIOTrue ) + memcpy( handle->bufferInfos[i].buffers[bufferIndex], + &stream_.deviceBuffer[j++*bufferBytes], bufferBytes ); + } + + } + else { + + if ( stream_.doByteSwap[0] ) + byteSwapBuffer( stream_.userBuffer[0], + stream_.bufferSize * stream_.nUserChannels[0], + stream_.userFormat ); + + for ( i=0, j=0; ibufferInfos[i].isInput != ASIOTrue ) + memcpy( handle->bufferInfos[i].buffers[bufferIndex], + &stream_.userBuffer[0][bufferBytes*j++], bufferBytes ); + } + + } + } + + // Don't bother draining input + if ( handle->drainCounter ) { + handle->drainCounter++; + goto unlock; + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + + bufferBytes = stream_.bufferSize * formatBytes(stream_.deviceFormat[1]); + + if (stream_.doConvertBuffer[1]) { + + // Always interleave ASIO input data. + for ( i=0, j=0; ibufferInfos[i].isInput == ASIOTrue ) + memcpy( &stream_.deviceBuffer[j++*bufferBytes], + handle->bufferInfos[i].buffers[bufferIndex], + bufferBytes ); + } + + if ( stream_.doByteSwap[1] ) + byteSwapBuffer( stream_.deviceBuffer, + stream_.bufferSize * stream_.nDeviceChannels[1], + stream_.deviceFormat[1] ); + convertBuffer( stream_.userBuffer[1], stream_.deviceBuffer, stream_.convertInfo[1] ); + + } + else { + for ( i=0, j=0; ibufferInfos[i].isInput == ASIOTrue ) { + memcpy( &stream_.userBuffer[1][bufferBytes*j++], + handle->bufferInfos[i].buffers[bufferIndex], + bufferBytes ); + } + } + + if ( stream_.doByteSwap[1] ) + byteSwapBuffer( stream_.userBuffer[1], + stream_.bufferSize * stream_.nUserChannels[1], + stream_.userFormat ); + } + } + + unlock: + // The following call was suggested by Malte Clasen. While the API + // documentation indicates it should not be required, some device + // drivers apparently do not function correctly without it. + ASIOOutputReady(); + + RtApi::tickStreamTime(); + return SUCCESS; +} + +static void sampleRateChanged( ASIOSampleRate sRate ) +{ + // The ASIO documentation says that this usually only happens during + // external sync. Audio processing is not stopped by the driver, + // actual sample rate might not have even changed, maybe only the + // sample rate status of an AES/EBU or S/PDIF digital input at the + // audio device. + + RtApi *object = (RtApi *) asioCallbackInfo->object; + try { + object->stopStream(); + } + catch ( RtAudioError &exception ) { + std::cerr << "\nRtApiAsio: sampleRateChanged() error (" << exception.getMessage() << ")!\n" << std::endl; + return; + } + + std::cerr << "\nRtApiAsio: driver reports sample rate changed to " << sRate << " ... stream stopped!!!\n" << std::endl; +} + +static long asioMessages( long selector, long value, void* /*message*/, double* /*opt*/ ) +{ + long ret = 0; + + switch( selector ) { + case kAsioSelectorSupported: + if ( value == kAsioResetRequest + || value == kAsioEngineVersion + || value == kAsioResyncRequest + || value == kAsioLatenciesChanged + // The following three were added for ASIO 2.0, you don't + // necessarily have to support them. + || value == kAsioSupportsTimeInfo + || value == kAsioSupportsTimeCode + || value == kAsioSupportsInputMonitor) + ret = 1L; + break; + case kAsioResetRequest: + // Defer the task and perform the reset of the driver during the + // next "safe" situation. You cannot reset the driver right now, + // as this code is called from the driver. Reset the driver is + // done by completely destruct is. I.e. ASIOStop(), + // ASIODisposeBuffers(), Destruction Afterwards you initialize the + // driver again. + std::cerr << "\nRtApiAsio: driver reset requested!!!" << std::endl; + ret = 1L; + break; + case kAsioResyncRequest: + // This informs the application that the driver encountered some + // non-fatal data loss. It is used for synchronization purposes + // of different media. Added mainly to work around the Win16Mutex + // problems in Windows 95/98 with the Windows Multimedia system, + // which could lose data because the Mutex was held too long by + // another thread. However a driver can issue it in other + // situations, too. + // std::cerr << "\nRtApiAsio: driver resync requested!!!" << std::endl; + asioXRun = true; + ret = 1L; + break; + case kAsioLatenciesChanged: + // This will inform the host application that the drivers were + // latencies changed. Beware, it this does not mean that the + // buffer sizes have changed! You might need to update internal + // delay data. + std::cerr << "\nRtApiAsio: driver latency may have changed!!!" << std::endl; + ret = 1L; + break; + case kAsioEngineVersion: + // Return the supported ASIO version of the host application. If + // a host application does not implement this selector, ASIO 1.0 + // is assumed by the driver. + ret = 2L; + break; + case kAsioSupportsTimeInfo: + // Informs the driver whether the + // asioCallbacks.bufferSwitchTimeInfo() callback is supported. + // For compatibility with ASIO 1.0 drivers the host application + // should always support the "old" bufferSwitch method, too. + ret = 0; + break; + case kAsioSupportsTimeCode: + // Informs the driver whether application is interested in time + // code info. If an application does not need to know about time + // code, the driver has less work to do. + ret = 0; + break; + } + return ret; +} + +static const char* getAsioErrorString( ASIOError result ) +{ + struct Messages + { + ASIOError value; + const char*message; + }; + + static const Messages m[] = + { + { ASE_NotPresent, "Hardware input or output is not present or available." }, + { ASE_HWMalfunction, "Hardware is malfunctioning." }, + { ASE_InvalidParameter, "Invalid input parameter." }, + { ASE_InvalidMode, "Invalid mode." }, + { ASE_SPNotAdvancing, "Sample position not advancing." }, + { ASE_NoClock, "Sample clock or rate cannot be determined or is not present." }, + { ASE_NoMemory, "Not enough memory to complete the request." } + }; + + for ( unsigned int i = 0; i < sizeof(m)/sizeof(m[0]); ++i ) + if ( m[i].value == result ) return m[i].message; + + return "Unknown error."; +} + +//******************** End of __WINDOWS_ASIO__ *********************// +#endif + + +#if defined(__WINDOWS_WASAPI__) // Windows WASAPI API + +// Authored by Marcus Tomlinson , April 2014 +// - Introduces support for the Windows WASAPI API +// - Aims to deliver bit streams to and from hardware at the lowest possible latency, via the absolute minimum buffer sizes required +// - Provides flexible stream configuration to an otherwise strict and inflexible WASAPI interface +// - Includes automatic internal conversion of sample rate and buffer size between hardware and the user + +#ifndef INITGUID + #define INITGUID +#endif +#include +#include +#include +#include + +//============================================================================= + +#define SAFE_RELEASE( objectPtr )\ +if ( objectPtr )\ +{\ + objectPtr->Release();\ + objectPtr = NULL;\ +} + +typedef HANDLE ( __stdcall *TAvSetMmThreadCharacteristicsPtr )( LPCWSTR TaskName, LPDWORD TaskIndex ); + +//----------------------------------------------------------------------------- + +// WASAPI dictates stream sample rate, format, channel count, and in some cases, buffer size. +// Therefore we must perform all necessary conversions to user buffers in order to satisfy these +// requirements. WasapiBuffer ring buffers are used between HwIn->UserIn and UserOut->HwOut to +// provide intermediate storage for read / write synchronization. +class WasapiBuffer +{ +public: + WasapiBuffer() + : buffer_( NULL ), + bufferSize_( 0 ), + inIndex_( 0 ), + outIndex_( 0 ) {} + + ~WasapiBuffer() { + delete buffer_; + } + + // sets the length of the internal ring buffer + void setBufferSize( unsigned int bufferSize, unsigned int formatBytes ) { + delete buffer_; + + buffer_ = ( char* ) calloc( bufferSize, formatBytes ); + + bufferSize_ = bufferSize; + inIndex_ = 0; + outIndex_ = 0; + } + + // attempt to push a buffer into the ring buffer at the current "in" index + bool pushBuffer( char* buffer, unsigned int bufferSize, RtAudioFormat format ) + { + if ( !buffer || // incoming buffer is NULL + bufferSize == 0 || // incoming buffer has no data + bufferSize > bufferSize_ ) // incoming buffer too large + { + return false; + } + + unsigned int relOutIndex = outIndex_; + unsigned int inIndexEnd = inIndex_ + bufferSize; + if ( relOutIndex < inIndex_ && inIndexEnd >= bufferSize_ ) { + relOutIndex += bufferSize_; + } + + // "in" index can end on the "out" index but cannot begin at it + if ( inIndex_ <= relOutIndex && inIndexEnd > relOutIndex ) { + return false; // not enough space between "in" index and "out" index + } + + // copy buffer from external to internal + int fromZeroSize = inIndex_ + bufferSize - bufferSize_; + fromZeroSize = fromZeroSize < 0 ? 0 : fromZeroSize; + int fromInSize = bufferSize - fromZeroSize; + + switch( format ) + { + case RTAUDIO_SINT8: + memcpy( &( ( char* ) buffer_ )[inIndex_], buffer, fromInSize * sizeof( char ) ); + memcpy( buffer_, &( ( char* ) buffer )[fromInSize], fromZeroSize * sizeof( char ) ); + break; + case RTAUDIO_SINT16: + memcpy( &( ( short* ) buffer_ )[inIndex_], buffer, fromInSize * sizeof( short ) ); + memcpy( buffer_, &( ( short* ) buffer )[fromInSize], fromZeroSize * sizeof( short ) ); + break; + case RTAUDIO_SINT24: + memcpy( &( ( S24* ) buffer_ )[inIndex_], buffer, fromInSize * sizeof( S24 ) ); + memcpy( buffer_, &( ( S24* ) buffer )[fromInSize], fromZeroSize * sizeof( S24 ) ); + break; + case RTAUDIO_SINT32: + memcpy( &( ( int* ) buffer_ )[inIndex_], buffer, fromInSize * sizeof( int ) ); + memcpy( buffer_, &( ( int* ) buffer )[fromInSize], fromZeroSize * sizeof( int ) ); + break; + case RTAUDIO_FLOAT32: + memcpy( &( ( float* ) buffer_ )[inIndex_], buffer, fromInSize * sizeof( float ) ); + memcpy( buffer_, &( ( float* ) buffer )[fromInSize], fromZeroSize * sizeof( float ) ); + break; + case RTAUDIO_FLOAT64: + memcpy( &( ( double* ) buffer_ )[inIndex_], buffer, fromInSize * sizeof( double ) ); + memcpy( buffer_, &( ( double* ) buffer )[fromInSize], fromZeroSize * sizeof( double ) ); + break; + } + + // update "in" index + inIndex_ += bufferSize; + inIndex_ %= bufferSize_; + + return true; + } + + // attempt to pull a buffer from the ring buffer from the current "out" index + bool pullBuffer( char* buffer, unsigned int bufferSize, RtAudioFormat format ) + { + if ( !buffer || // incoming buffer is NULL + bufferSize == 0 || // incoming buffer has no data + bufferSize > bufferSize_ ) // incoming buffer too large + { + return false; + } + + unsigned int relInIndex = inIndex_; + unsigned int outIndexEnd = outIndex_ + bufferSize; + if ( relInIndex < outIndex_ && outIndexEnd >= bufferSize_ ) { + relInIndex += bufferSize_; + } + + // "out" index can begin at and end on the "in" index + if ( outIndex_ < relInIndex && outIndexEnd > relInIndex ) { + return false; // not enough space between "out" index and "in" index + } + + // copy buffer from internal to external + int fromZeroSize = outIndex_ + bufferSize - bufferSize_; + fromZeroSize = fromZeroSize < 0 ? 0 : fromZeroSize; + int fromOutSize = bufferSize - fromZeroSize; + + switch( format ) + { + case RTAUDIO_SINT8: + memcpy( buffer, &( ( char* ) buffer_ )[outIndex_], fromOutSize * sizeof( char ) ); + memcpy( &( ( char* ) buffer )[fromOutSize], buffer_, fromZeroSize * sizeof( char ) ); + break; + case RTAUDIO_SINT16: + memcpy( buffer, &( ( short* ) buffer_ )[outIndex_], fromOutSize * sizeof( short ) ); + memcpy( &( ( short* ) buffer )[fromOutSize], buffer_, fromZeroSize * sizeof( short ) ); + break; + case RTAUDIO_SINT24: + memcpy( buffer, &( ( S24* ) buffer_ )[outIndex_], fromOutSize * sizeof( S24 ) ); + memcpy( &( ( S24* ) buffer )[fromOutSize], buffer_, fromZeroSize * sizeof( S24 ) ); + break; + case RTAUDIO_SINT32: + memcpy( buffer, &( ( int* ) buffer_ )[outIndex_], fromOutSize * sizeof( int ) ); + memcpy( &( ( int* ) buffer )[fromOutSize], buffer_, fromZeroSize * sizeof( int ) ); + break; + case RTAUDIO_FLOAT32: + memcpy( buffer, &( ( float* ) buffer_ )[outIndex_], fromOutSize * sizeof( float ) ); + memcpy( &( ( float* ) buffer )[fromOutSize], buffer_, fromZeroSize * sizeof( float ) ); + break; + case RTAUDIO_FLOAT64: + memcpy( buffer, &( ( double* ) buffer_ )[outIndex_], fromOutSize * sizeof( double ) ); + memcpy( &( ( double* ) buffer )[fromOutSize], buffer_, fromZeroSize * sizeof( double ) ); + break; + } + + // update "out" index + outIndex_ += bufferSize; + outIndex_ %= bufferSize_; + + return true; + } + +private: + char* buffer_; + unsigned int bufferSize_; + unsigned int inIndex_; + unsigned int outIndex_; +}; + +//----------------------------------------------------------------------------- + +// In order to satisfy WASAPI's buffer requirements, we need a means of converting sample rate +// between HW and the user. The convertBufferWasapi function is used to perform this conversion +// between HwIn->UserIn and UserOut->HwOut during the stream callback loop. +// This sample rate converter favors speed over quality, and works best with conversions between +// one rate and its multiple. +void convertBufferWasapi( char* outBuffer, + const char* inBuffer, + const unsigned int& channelCount, + const unsigned int& inSampleRate, + const unsigned int& outSampleRate, + const unsigned int& inSampleCount, + unsigned int& outSampleCount, + const RtAudioFormat& format ) +{ + // calculate the new outSampleCount and relative sampleStep + float sampleRatio = ( float ) outSampleRate / inSampleRate; + float sampleStep = 1.0f / sampleRatio; + float inSampleFraction = 0.0f; + + outSampleCount = ( unsigned int ) ( inSampleCount * sampleRatio ); + + // frame-by-frame, copy each relative input sample into it's corresponding output sample + for ( unsigned int outSample = 0; outSample < outSampleCount; outSample++ ) + { + unsigned int inSample = ( unsigned int ) inSampleFraction; + + switch ( format ) + { + case RTAUDIO_SINT8: + memcpy( &( ( char* ) outBuffer )[ outSample * channelCount ], &( ( char* ) inBuffer )[ inSample * channelCount ], channelCount * sizeof( char ) ); + break; + case RTAUDIO_SINT16: + memcpy( &( ( short* ) outBuffer )[ outSample * channelCount ], &( ( short* ) inBuffer )[ inSample * channelCount ], channelCount * sizeof( short ) ); + break; + case RTAUDIO_SINT24: + memcpy( &( ( S24* ) outBuffer )[ outSample * channelCount ], &( ( S24* ) inBuffer )[ inSample * channelCount ], channelCount * sizeof( S24 ) ); + break; + case RTAUDIO_SINT32: + memcpy( &( ( int* ) outBuffer )[ outSample * channelCount ], &( ( int* ) inBuffer )[ inSample * channelCount ], channelCount * sizeof( int ) ); + break; + case RTAUDIO_FLOAT32: + memcpy( &( ( float* ) outBuffer )[ outSample * channelCount ], &( ( float* ) inBuffer )[ inSample * channelCount ], channelCount * sizeof( float ) ); + break; + case RTAUDIO_FLOAT64: + memcpy( &( ( double* ) outBuffer )[ outSample * channelCount ], &( ( double* ) inBuffer )[ inSample * channelCount ], channelCount * sizeof( double ) ); + break; + } + + // jump to next in sample + inSampleFraction += sampleStep; + } +} + +//----------------------------------------------------------------------------- + +// A structure to hold various information related to the WASAPI implementation. +struct WasapiHandle +{ + IAudioClient* captureAudioClient; + IAudioClient* renderAudioClient; + IAudioCaptureClient* captureClient; + IAudioRenderClient* renderClient; + HANDLE captureEvent; + HANDLE renderEvent; + + WasapiHandle() + : captureAudioClient( NULL ), + renderAudioClient( NULL ), + captureClient( NULL ), + renderClient( NULL ), + captureEvent( NULL ), + renderEvent( NULL ) {} +}; + +//============================================================================= + +RtApiWasapi::RtApiWasapi() + : coInitialized_( false ), deviceEnumerator_( NULL ) +{ + // WASAPI can run either apartment or multi-threaded + HRESULT hr = CoInitialize( NULL ); + if ( !FAILED( hr ) ) + coInitialized_ = true; + + // Instantiate device enumerator + hr = CoCreateInstance( __uuidof( MMDeviceEnumerator ), NULL, + CLSCTX_ALL, __uuidof( IMMDeviceEnumerator ), + ( void** ) &deviceEnumerator_ ); + + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::RtApiWasapi: Unable to instantiate device enumerator"; + error( RtAudioError::DRIVER_ERROR ); + } +} + +//----------------------------------------------------------------------------- + +RtApiWasapi::~RtApiWasapi() +{ + if ( stream_.state != STREAM_CLOSED ) + closeStream(); + + SAFE_RELEASE( deviceEnumerator_ ); + + // If this object previously called CoInitialize() + if ( coInitialized_ ) + CoUninitialize(); +} + +//============================================================================= + +unsigned int RtApiWasapi::getDeviceCount( void ) +{ + unsigned int captureDeviceCount = 0; + unsigned int renderDeviceCount = 0; + + IMMDeviceCollection* captureDevices = NULL; + IMMDeviceCollection* renderDevices = NULL; + + // Count capture devices + errorText_.clear(); + HRESULT hr = deviceEnumerator_->EnumAudioEndpoints( eCapture, DEVICE_STATE_ACTIVE, &captureDevices ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceCount: Unable to retrieve capture device collection."; + goto Exit; + } + + hr = captureDevices->GetCount( &captureDeviceCount ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceCount: Unable to retrieve capture device count."; + goto Exit; + } + + // Count render devices + hr = deviceEnumerator_->EnumAudioEndpoints( eRender, DEVICE_STATE_ACTIVE, &renderDevices ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceCount: Unable to retrieve render device collection."; + goto Exit; + } + + hr = renderDevices->GetCount( &renderDeviceCount ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceCount: Unable to retrieve render device count."; + goto Exit; + } + +Exit: + // release all references + SAFE_RELEASE( captureDevices ); + SAFE_RELEASE( renderDevices ); + + if ( errorText_.empty() ) + return captureDeviceCount + renderDeviceCount; + + error( RtAudioError::DRIVER_ERROR ); + return 0; +} + +//----------------------------------------------------------------------------- + +RtAudio::DeviceInfo RtApiWasapi::getDeviceInfo( unsigned int device ) +{ + RtAudio::DeviceInfo info; + unsigned int captureDeviceCount = 0; + unsigned int renderDeviceCount = 0; + std::wstring deviceName; + std::string defaultDeviceName; + bool isCaptureDevice = false; + + PROPVARIANT deviceNameProp; + PROPVARIANT defaultDeviceNameProp; + + IMMDeviceCollection* captureDevices = NULL; + IMMDeviceCollection* renderDevices = NULL; + IMMDevice* devicePtr = NULL; + IMMDevice* defaultDevicePtr = NULL; + IAudioClient* audioClient = NULL; + IPropertyStore* devicePropStore = NULL; + IPropertyStore* defaultDevicePropStore = NULL; + + WAVEFORMATEX* deviceFormat = NULL; + WAVEFORMATEX* closestMatchFormat = NULL; + + // probed + info.probed = false; + + // Count capture devices + errorText_.clear(); + RtAudioError::Type errorType = RtAudioError::DRIVER_ERROR; + HRESULT hr = deviceEnumerator_->EnumAudioEndpoints( eCapture, DEVICE_STATE_ACTIVE, &captureDevices ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve capture device collection."; + goto Exit; + } + + hr = captureDevices->GetCount( &captureDeviceCount ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve capture device count."; + goto Exit; + } + + // Count render devices + hr = deviceEnumerator_->EnumAudioEndpoints( eRender, DEVICE_STATE_ACTIVE, &renderDevices ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve render device collection."; + goto Exit; + } + + hr = renderDevices->GetCount( &renderDeviceCount ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve render device count."; + goto Exit; + } + + // validate device index + if ( device >= captureDeviceCount + renderDeviceCount ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Invalid device index."; + errorType = RtAudioError::INVALID_USE; + goto Exit; + } + + // determine whether index falls within capture or render devices + if ( device >= renderDeviceCount ) { + hr = captureDevices->Item( device - renderDeviceCount, &devicePtr ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve capture device handle."; + goto Exit; + } + isCaptureDevice = true; + } + else { + hr = renderDevices->Item( device, &devicePtr ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve render device handle."; + goto Exit; + } + isCaptureDevice = false; + } + + // get default device name + if ( isCaptureDevice ) { + hr = deviceEnumerator_->GetDefaultAudioEndpoint( eCapture, eConsole, &defaultDevicePtr ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve default capture device handle."; + goto Exit; + } + } + else { + hr = deviceEnumerator_->GetDefaultAudioEndpoint( eRender, eConsole, &defaultDevicePtr ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve default render device handle."; + goto Exit; + } + } + + hr = defaultDevicePtr->OpenPropertyStore( STGM_READ, &defaultDevicePropStore ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to open default device property store."; + goto Exit; + } + PropVariantInit( &defaultDeviceNameProp ); + + hr = defaultDevicePropStore->GetValue( PKEY_Device_FriendlyName, &defaultDeviceNameProp ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve default device property: PKEY_Device_FriendlyName."; + goto Exit; + } + + deviceName = defaultDeviceNameProp.pwszVal; + defaultDeviceName = std::string( deviceName.begin(), deviceName.end() ); + + // name + hr = devicePtr->OpenPropertyStore( STGM_READ, &devicePropStore ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to open device property store."; + goto Exit; + } + + PropVariantInit( &deviceNameProp ); + + hr = devicePropStore->GetValue( PKEY_Device_FriendlyName, &deviceNameProp ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve device property: PKEY_Device_FriendlyName."; + goto Exit; + } + + deviceName = deviceNameProp.pwszVal; + info.name = std::string( deviceName.begin(), deviceName.end() ); + + // is default + if ( isCaptureDevice ) { + info.isDefaultInput = info.name == defaultDeviceName; + info.isDefaultOutput = false; + } + else { + info.isDefaultInput = false; + info.isDefaultOutput = info.name == defaultDeviceName; + } + + // channel count + hr = devicePtr->Activate( __uuidof( IAudioClient ), CLSCTX_ALL, NULL, ( void** ) &audioClient ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve device audio client."; + goto Exit; + } + + hr = audioClient->GetMixFormat( &deviceFormat ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::getDeviceInfo: Unable to retrieve device mix format."; + goto Exit; + } + + if ( isCaptureDevice ) { + info.inputChannels = deviceFormat->nChannels; + info.outputChannels = 0; + info.duplexChannels = 0; + } + else { + info.inputChannels = 0; + info.outputChannels = deviceFormat->nChannels; + info.duplexChannels = 0; + } + + // sample rates + info.sampleRates.clear(); + + // allow support for all sample rates as we have a built-in sample rate converter + for ( unsigned int i = 0; i < MAX_SAMPLE_RATES; i++ ) { + info.sampleRates.push_back( SAMPLE_RATES[i] ); + } + + // native format + info.nativeFormats = 0; + + if ( deviceFormat->wFormatTag == WAVE_FORMAT_IEEE_FLOAT || + ( deviceFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE && + ( ( WAVEFORMATEXTENSIBLE* ) deviceFormat )->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT ) ) + { + if ( deviceFormat->wBitsPerSample == 32 ) { + info.nativeFormats |= RTAUDIO_FLOAT32; + } + else if ( deviceFormat->wBitsPerSample == 64 ) { + info.nativeFormats |= RTAUDIO_FLOAT64; + } + } + else if ( deviceFormat->wFormatTag == WAVE_FORMAT_PCM || + ( deviceFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE && + ( ( WAVEFORMATEXTENSIBLE* ) deviceFormat )->SubFormat == KSDATAFORMAT_SUBTYPE_PCM ) ) + { + if ( deviceFormat->wBitsPerSample == 8 ) { + info.nativeFormats |= RTAUDIO_SINT8; + } + else if ( deviceFormat->wBitsPerSample == 16 ) { + info.nativeFormats |= RTAUDIO_SINT16; + } + else if ( deviceFormat->wBitsPerSample == 24 ) { + info.nativeFormats |= RTAUDIO_SINT24; + } + else if ( deviceFormat->wBitsPerSample == 32 ) { + info.nativeFormats |= RTAUDIO_SINT32; + } + } + + // probed + info.probed = true; + +Exit: + // release all references + PropVariantClear( &deviceNameProp ); + PropVariantClear( &defaultDeviceNameProp ); + + SAFE_RELEASE( captureDevices ); + SAFE_RELEASE( renderDevices ); + SAFE_RELEASE( devicePtr ); + SAFE_RELEASE( defaultDevicePtr ); + SAFE_RELEASE( audioClient ); + SAFE_RELEASE( devicePropStore ); + SAFE_RELEASE( defaultDevicePropStore ); + + CoTaskMemFree( deviceFormat ); + CoTaskMemFree( closestMatchFormat ); + + if ( !errorText_.empty() ) + error( errorType ); + return info; +} + +//----------------------------------------------------------------------------- + +unsigned int RtApiWasapi::getDefaultOutputDevice( void ) +{ + for ( unsigned int i = 0; i < getDeviceCount(); i++ ) { + if ( getDeviceInfo( i ).isDefaultOutput ) { + return i; + } + } + + return 0; +} + +//----------------------------------------------------------------------------- + +unsigned int RtApiWasapi::getDefaultInputDevice( void ) +{ + for ( unsigned int i = 0; i < getDeviceCount(); i++ ) { + if ( getDeviceInfo( i ).isDefaultInput ) { + return i; + } + } + + return 0; +} + +//----------------------------------------------------------------------------- + +void RtApiWasapi::closeStream( void ) +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiWasapi::closeStream: No open stream to close."; + error( RtAudioError::WARNING ); + return; + } + + if ( stream_.state != STREAM_STOPPED ) + stopStream(); + + // clean up stream memory + SAFE_RELEASE( ( ( WasapiHandle* ) stream_.apiHandle )->captureAudioClient ) + SAFE_RELEASE( ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient ) + + SAFE_RELEASE( ( ( WasapiHandle* ) stream_.apiHandle )->captureClient ) + SAFE_RELEASE( ( ( WasapiHandle* ) stream_.apiHandle )->renderClient ) + + if ( ( ( WasapiHandle* ) stream_.apiHandle )->captureEvent ) + CloseHandle( ( ( WasapiHandle* ) stream_.apiHandle )->captureEvent ); + + if ( ( ( WasapiHandle* ) stream_.apiHandle )->renderEvent ) + CloseHandle( ( ( WasapiHandle* ) stream_.apiHandle )->renderEvent ); + + delete ( WasapiHandle* ) stream_.apiHandle; + stream_.apiHandle = NULL; + + for ( int i = 0; i < 2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + // update stream state + stream_.state = STREAM_CLOSED; +} + +//----------------------------------------------------------------------------- + +void RtApiWasapi::startStream( void ) +{ + verifyStream(); + + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiWasapi::startStream: The stream is already running."; + error( RtAudioError::WARNING ); + return; + } + + // update stream state + stream_.state = STREAM_RUNNING; + + // create WASAPI stream thread + stream_.callbackInfo.thread = ( ThreadHandle ) CreateThread( NULL, 0, runWasapiThread, this, CREATE_SUSPENDED, NULL ); + + if ( !stream_.callbackInfo.thread ) { + errorText_ = "RtApiWasapi::startStream: Unable to instantiate callback thread."; + error( RtAudioError::THREAD_ERROR ); + } + else { + SetThreadPriority( ( void* ) stream_.callbackInfo.thread, stream_.callbackInfo.priority ); + ResumeThread( ( void* ) stream_.callbackInfo.thread ); + } +} + +//----------------------------------------------------------------------------- + +void RtApiWasapi::stopStream( void ) +{ + verifyStream(); + + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiWasapi::stopStream: The stream is already stopped."; + error( RtAudioError::WARNING ); + return; + } + + // inform stream thread by setting stream state to STREAM_STOPPING + stream_.state = STREAM_STOPPING; + + // wait until stream thread is stopped + while( stream_.state != STREAM_STOPPED ) { + Sleep( 1 ); + } + + // Wait for the last buffer to play before stopping. + Sleep( 1000 * stream_.bufferSize / stream_.sampleRate ); + + // stop capture client if applicable + if ( ( ( WasapiHandle* ) stream_.apiHandle )->captureAudioClient ) { + HRESULT hr = ( ( WasapiHandle* ) stream_.apiHandle )->captureAudioClient->Stop(); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::stopStream: Unable to stop capture stream."; + error( RtAudioError::DRIVER_ERROR ); + return; + } + } + + // stop render client if applicable + if ( ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient ) { + HRESULT hr = ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient->Stop(); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::stopStream: Unable to stop render stream."; + error( RtAudioError::DRIVER_ERROR ); + return; + } + } + + // close thread handle + if ( stream_.callbackInfo.thread && !CloseHandle( ( void* ) stream_.callbackInfo.thread ) ) { + errorText_ = "RtApiWasapi::stopStream: Unable to close callback thread."; + error( RtAudioError::THREAD_ERROR ); + return; + } + + stream_.callbackInfo.thread = (ThreadHandle) NULL; +} + +//----------------------------------------------------------------------------- + +void RtApiWasapi::abortStream( void ) +{ + verifyStream(); + + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiWasapi::abortStream: The stream is already stopped."; + error( RtAudioError::WARNING ); + return; + } + + // inform stream thread by setting stream state to STREAM_STOPPING + stream_.state = STREAM_STOPPING; + + // wait until stream thread is stopped + while ( stream_.state != STREAM_STOPPED ) { + Sleep( 1 ); + } + + // stop capture client if applicable + if ( ( ( WasapiHandle* ) stream_.apiHandle )->captureAudioClient ) { + HRESULT hr = ( ( WasapiHandle* ) stream_.apiHandle )->captureAudioClient->Stop(); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::abortStream: Unable to stop capture stream."; + error( RtAudioError::DRIVER_ERROR ); + return; + } + } + + // stop render client if applicable + if ( ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient ) { + HRESULT hr = ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient->Stop(); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::abortStream: Unable to stop render stream."; + error( RtAudioError::DRIVER_ERROR ); + return; + } + } + + // close thread handle + if ( stream_.callbackInfo.thread && !CloseHandle( ( void* ) stream_.callbackInfo.thread ) ) { + errorText_ = "RtApiWasapi::abortStream: Unable to close callback thread."; + error( RtAudioError::THREAD_ERROR ); + return; + } + + stream_.callbackInfo.thread = (ThreadHandle) NULL; +} + +//----------------------------------------------------------------------------- + +bool RtApiWasapi::probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int* bufferSize, + RtAudio::StreamOptions* options ) +{ + bool methodResult = FAILURE; + unsigned int captureDeviceCount = 0; + unsigned int renderDeviceCount = 0; + + IMMDeviceCollection* captureDevices = NULL; + IMMDeviceCollection* renderDevices = NULL; + IMMDevice* devicePtr = NULL; + WAVEFORMATEX* deviceFormat = NULL; + unsigned int bufferBytes; + stream_.state = STREAM_STOPPED; + + // create API Handle if not already created + if ( !stream_.apiHandle ) + stream_.apiHandle = ( void* ) new WasapiHandle(); + + // Count capture devices + errorText_.clear(); + RtAudioError::Type errorType = RtAudioError::DRIVER_ERROR; + HRESULT hr = deviceEnumerator_->EnumAudioEndpoints( eCapture, DEVICE_STATE_ACTIVE, &captureDevices ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve capture device collection."; + goto Exit; + } + + hr = captureDevices->GetCount( &captureDeviceCount ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve capture device count."; + goto Exit; + } + + // Count render devices + hr = deviceEnumerator_->EnumAudioEndpoints( eRender, DEVICE_STATE_ACTIVE, &renderDevices ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve render device collection."; + goto Exit; + } + + hr = renderDevices->GetCount( &renderDeviceCount ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve render device count."; + goto Exit; + } + + // validate device index + if ( device >= captureDeviceCount + renderDeviceCount ) { + errorType = RtAudioError::INVALID_USE; + errorText_ = "RtApiWasapi::probeDeviceOpen: Invalid device index."; + goto Exit; + } + + // determine whether index falls within capture or render devices + if ( device >= renderDeviceCount ) { + if ( mode != INPUT ) { + errorType = RtAudioError::INVALID_USE; + errorText_ = "RtApiWasapi::probeDeviceOpen: Capture device selected as output device."; + goto Exit; + } + + // retrieve captureAudioClient from devicePtr + IAudioClient*& captureAudioClient = ( ( WasapiHandle* ) stream_.apiHandle )->captureAudioClient; + + hr = captureDevices->Item( device - renderDeviceCount, &devicePtr ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve capture device handle."; + goto Exit; + } + + hr = devicePtr->Activate( __uuidof( IAudioClient ), CLSCTX_ALL, + NULL, ( void** ) &captureAudioClient ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve device audio client."; + goto Exit; + } + + hr = captureAudioClient->GetMixFormat( &deviceFormat ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve device mix format."; + goto Exit; + } + + stream_.nDeviceChannels[mode] = deviceFormat->nChannels; + captureAudioClient->GetStreamLatency( ( long long* ) &stream_.latency[mode] ); + } + else { + if ( mode != OUTPUT ) { + errorType = RtAudioError::INVALID_USE; + errorText_ = "RtApiWasapi::probeDeviceOpen: Render device selected as input device."; + goto Exit; + } + + // retrieve renderAudioClient from devicePtr + IAudioClient*& renderAudioClient = ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient; + + hr = renderDevices->Item( device, &devicePtr ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve render device handle."; + goto Exit; + } + + hr = devicePtr->Activate( __uuidof( IAudioClient ), CLSCTX_ALL, + NULL, ( void** ) &renderAudioClient ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve device audio client."; + goto Exit; + } + + hr = renderAudioClient->GetMixFormat( &deviceFormat ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::probeDeviceOpen: Unable to retrieve device mix format."; + goto Exit; + } + + stream_.nDeviceChannels[mode] = deviceFormat->nChannels; + renderAudioClient->GetStreamLatency( ( long long* ) &stream_.latency[mode] ); + } + + // fill stream data + if ( ( stream_.mode == OUTPUT && mode == INPUT ) || + ( stream_.mode == INPUT && mode == OUTPUT ) ) { + stream_.mode = DUPLEX; + } + else { + stream_.mode = mode; + } + + stream_.device[mode] = device; + stream_.doByteSwap[mode] = false; + stream_.sampleRate = sampleRate; + stream_.bufferSize = *bufferSize; + stream_.nBuffers = 1; + stream_.nUserChannels[mode] = channels; + stream_.channelOffset[mode] = firstChannel; + stream_.userFormat = format; + stream_.deviceFormat[mode] = getDeviceInfo( device ).nativeFormats; + + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) + stream_.userInterleaved = false; + else + stream_.userInterleaved = true; + stream_.deviceInterleaved[mode] = true; + + // Set flags for buffer conversion. + stream_.doConvertBuffer[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] || + stream_.nUserChannels != stream_.nDeviceChannels ) + stream_.doConvertBuffer[mode] = true; + else if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] && + stream_.nUserChannels[mode] > 1 ) + stream_.doConvertBuffer[mode] = true; + + if ( stream_.doConvertBuffer[mode] ) + setConvertInfo( mode, 0 ); + + // Allocate necessary internal buffers + bufferBytes = stream_.nUserChannels[mode] * stream_.bufferSize * formatBytes( stream_.userFormat ); + + stream_.userBuffer[mode] = ( char* ) calloc( bufferBytes, 1 ); + if ( !stream_.userBuffer[mode] ) { + errorType = RtAudioError::MEMORY_ERROR; + errorText_ = "RtApiWasapi::probeDeviceOpen: Error allocating user buffer memory."; + goto Exit; + } + + if ( options && options->flags & RTAUDIO_SCHEDULE_REALTIME ) + stream_.callbackInfo.priority = 15; + else + stream_.callbackInfo.priority = 0; + + ///! TODO: RTAUDIO_MINIMIZE_LATENCY // Provide stream buffers directly to callback + ///! TODO: RTAUDIO_HOG_DEVICE // Exclusive mode + + methodResult = SUCCESS; + +Exit: + //clean up + SAFE_RELEASE( captureDevices ); + SAFE_RELEASE( renderDevices ); + SAFE_RELEASE( devicePtr ); + CoTaskMemFree( deviceFormat ); + + // if method failed, close the stream + if ( methodResult == FAILURE ) + closeStream(); + + if ( !errorText_.empty() ) + error( errorType ); + return methodResult; +} + +//============================================================================= + +DWORD WINAPI RtApiWasapi::runWasapiThread( void* wasapiPtr ) +{ + if ( wasapiPtr ) + ( ( RtApiWasapi* ) wasapiPtr )->wasapiThread(); + + return 0; +} + +DWORD WINAPI RtApiWasapi::stopWasapiThread( void* wasapiPtr ) +{ + if ( wasapiPtr ) + ( ( RtApiWasapi* ) wasapiPtr )->stopStream(); + + return 0; +} + +DWORD WINAPI RtApiWasapi::abortWasapiThread( void* wasapiPtr ) +{ + if ( wasapiPtr ) + ( ( RtApiWasapi* ) wasapiPtr )->abortStream(); + + return 0; +} + +//----------------------------------------------------------------------------- + +void RtApiWasapi::wasapiThread() +{ + // as this is a new thread, we must CoInitialize it + CoInitialize( NULL ); + + HRESULT hr; + + IAudioClient* captureAudioClient = ( ( WasapiHandle* ) stream_.apiHandle )->captureAudioClient; + IAudioClient* renderAudioClient = ( ( WasapiHandle* ) stream_.apiHandle )->renderAudioClient; + IAudioCaptureClient* captureClient = ( ( WasapiHandle* ) stream_.apiHandle )->captureClient; + IAudioRenderClient* renderClient = ( ( WasapiHandle* ) stream_.apiHandle )->renderClient; + HANDLE captureEvent = ( ( WasapiHandle* ) stream_.apiHandle )->captureEvent; + HANDLE renderEvent = ( ( WasapiHandle* ) stream_.apiHandle )->renderEvent; + + WAVEFORMATEX* captureFormat = NULL; + WAVEFORMATEX* renderFormat = NULL; + float captureSrRatio = 0.0f; + float renderSrRatio = 0.0f; + WasapiBuffer captureBuffer; + WasapiBuffer renderBuffer; + + // declare local stream variables + RtAudioCallback callback = ( RtAudioCallback ) stream_.callbackInfo.callback; + BYTE* streamBuffer = NULL; + unsigned long captureFlags = 0; + unsigned int bufferFrameCount = 0; + unsigned int numFramesPadding = 0; + unsigned int convBufferSize = 0; + bool callbackPushed = false; + bool callbackPulled = false; + bool callbackStopped = false; + int callbackResult = 0; + + // convBuffer is used to store converted buffers between WASAPI and the user + char* convBuffer = NULL; + unsigned int convBuffSize = 0; + unsigned int deviceBuffSize = 0; + + errorText_.clear(); + RtAudioError::Type errorType = RtAudioError::DRIVER_ERROR; + + // Attempt to assign "Pro Audio" characteristic to thread + HMODULE AvrtDll = LoadLibrary( (LPCTSTR) "AVRT.dll" ); + if ( AvrtDll ) { + DWORD taskIndex = 0; + TAvSetMmThreadCharacteristicsPtr AvSetMmThreadCharacteristicsPtr = ( TAvSetMmThreadCharacteristicsPtr ) GetProcAddress( AvrtDll, "AvSetMmThreadCharacteristicsW" ); + AvSetMmThreadCharacteristicsPtr( L"Pro Audio", &taskIndex ); + FreeLibrary( AvrtDll ); + } + + // start capture stream if applicable + if ( captureAudioClient ) { + hr = captureAudioClient->GetMixFormat( &captureFormat ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve device mix format."; + goto Exit; + } + + captureSrRatio = ( ( float ) captureFormat->nSamplesPerSec / stream_.sampleRate ); + + // initialize capture stream according to desire buffer size + float desiredBufferSize = stream_.bufferSize * captureSrRatio; + REFERENCE_TIME desiredBufferPeriod = ( REFERENCE_TIME ) ( ( float ) desiredBufferSize * 10000000 / captureFormat->nSamplesPerSec ); + + if ( !captureClient ) { + hr = captureAudioClient->Initialize( AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + desiredBufferPeriod, + desiredBufferPeriod, + captureFormat, + NULL ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to initialize capture audio client."; + goto Exit; + } + + hr = captureAudioClient->GetService( __uuidof( IAudioCaptureClient ), + ( void** ) &captureClient ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve capture client handle."; + goto Exit; + } + + // configure captureEvent to trigger on every available capture buffer + captureEvent = CreateEvent( NULL, FALSE, FALSE, NULL ); + if ( !captureEvent ) { + errorType = RtAudioError::SYSTEM_ERROR; + errorText_ = "RtApiWasapi::wasapiThread: Unable to create capture event."; + goto Exit; + } + + hr = captureAudioClient->SetEventHandle( captureEvent ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to set capture event handle."; + goto Exit; + } + + ( ( WasapiHandle* ) stream_.apiHandle )->captureClient = captureClient; + ( ( WasapiHandle* ) stream_.apiHandle )->captureEvent = captureEvent; + } + + unsigned int inBufferSize = 0; + hr = captureAudioClient->GetBufferSize( &inBufferSize ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to get capture buffer size."; + goto Exit; + } + + // scale outBufferSize according to stream->user sample rate ratio + unsigned int outBufferSize = ( unsigned int ) ( stream_.bufferSize * captureSrRatio ) * stream_.nDeviceChannels[INPUT]; + inBufferSize *= stream_.nDeviceChannels[INPUT]; + + // set captureBuffer size + captureBuffer.setBufferSize( inBufferSize + outBufferSize, formatBytes( stream_.deviceFormat[INPUT] ) ); + + // reset the capture stream + hr = captureAudioClient->Reset(); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to reset capture stream."; + goto Exit; + } + + // start the capture stream + hr = captureAudioClient->Start(); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to start capture stream."; + goto Exit; + } + } + + // start render stream if applicable + if ( renderAudioClient ) { + hr = renderAudioClient->GetMixFormat( &renderFormat ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve device mix format."; + goto Exit; + } + + renderSrRatio = ( ( float ) renderFormat->nSamplesPerSec / stream_.sampleRate ); + + // initialize render stream according to desire buffer size + float desiredBufferSize = stream_.bufferSize * renderSrRatio; + REFERENCE_TIME desiredBufferPeriod = ( REFERENCE_TIME ) ( ( float ) desiredBufferSize * 10000000 / renderFormat->nSamplesPerSec ); + + if ( !renderClient ) { + hr = renderAudioClient->Initialize( AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + desiredBufferPeriod, + desiredBufferPeriod, + renderFormat, + NULL ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to initialize render audio client."; + goto Exit; + } + + hr = renderAudioClient->GetService( __uuidof( IAudioRenderClient ), + ( void** ) &renderClient ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve render client handle."; + goto Exit; + } + + // configure renderEvent to trigger on every available render buffer + renderEvent = CreateEvent( NULL, FALSE, FALSE, NULL ); + if ( !renderEvent ) { + errorType = RtAudioError::SYSTEM_ERROR; + errorText_ = "RtApiWasapi::wasapiThread: Unable to create render event."; + goto Exit; + } + + hr = renderAudioClient->SetEventHandle( renderEvent ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to set render event handle."; + goto Exit; + } + + ( ( WasapiHandle* ) stream_.apiHandle )->renderClient = renderClient; + ( ( WasapiHandle* ) stream_.apiHandle )->renderEvent = renderEvent; + } + + unsigned int outBufferSize = 0; + hr = renderAudioClient->GetBufferSize( &outBufferSize ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to get render buffer size."; + goto Exit; + } + + // scale inBufferSize according to user->stream sample rate ratio + unsigned int inBufferSize = ( unsigned int ) ( stream_.bufferSize * renderSrRatio ) * stream_.nDeviceChannels[OUTPUT]; + outBufferSize *= stream_.nDeviceChannels[OUTPUT]; + + // set renderBuffer size + renderBuffer.setBufferSize( inBufferSize + outBufferSize, formatBytes( stream_.deviceFormat[OUTPUT] ) ); + + // reset the render stream + hr = renderAudioClient->Reset(); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to reset render stream."; + goto Exit; + } + + // start the render stream + hr = renderAudioClient->Start(); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to start render stream."; + goto Exit; + } + } + + if ( stream_.mode == INPUT ) { + convBuffSize = ( size_t ) ( stream_.bufferSize * captureSrRatio ) * stream_.nDeviceChannels[INPUT] * formatBytes( stream_.deviceFormat[INPUT] ); + deviceBuffSize = stream_.bufferSize * stream_.nDeviceChannels[INPUT] * formatBytes( stream_.deviceFormat[INPUT] ); + } + else if ( stream_.mode == OUTPUT ) { + convBuffSize = ( size_t ) ( stream_.bufferSize * renderSrRatio ) * stream_.nDeviceChannels[OUTPUT] * formatBytes( stream_.deviceFormat[OUTPUT] ); + deviceBuffSize = stream_.bufferSize * stream_.nDeviceChannels[OUTPUT] * formatBytes( stream_.deviceFormat[OUTPUT] ); + } + else if ( stream_.mode == DUPLEX ) { + convBuffSize = std::max( ( size_t ) ( stream_.bufferSize * captureSrRatio ) * stream_.nDeviceChannels[INPUT] * formatBytes( stream_.deviceFormat[INPUT] ), + ( size_t ) ( stream_.bufferSize * renderSrRatio ) * stream_.nDeviceChannels[OUTPUT] * formatBytes( stream_.deviceFormat[OUTPUT] ) ); + deviceBuffSize = std::max( stream_.bufferSize * stream_.nDeviceChannels[INPUT] * formatBytes( stream_.deviceFormat[INPUT] ), + stream_.bufferSize * stream_.nDeviceChannels[OUTPUT] * formatBytes( stream_.deviceFormat[OUTPUT] ) ); + } + + convBuffer = ( char* ) malloc( convBuffSize ); + stream_.deviceBuffer = ( char* ) malloc( deviceBuffSize ); + if ( !convBuffer || !stream_.deviceBuffer ) { + errorType = RtAudioError::MEMORY_ERROR; + errorText_ = "RtApiWasapi::wasapiThread: Error allocating device buffer memory."; + goto Exit; + } + + // stream process loop + while ( stream_.state != STREAM_STOPPING ) { + if ( !callbackPulled ) { + // Callback Input + // ============== + // 1. Pull callback buffer from inputBuffer + // 2. If 1. was successful: Convert callback buffer to user sample rate and channel count + // Convert callback buffer to user format + + if ( captureAudioClient ) { + // Pull callback buffer from inputBuffer + callbackPulled = captureBuffer.pullBuffer( convBuffer, + ( unsigned int ) ( stream_.bufferSize * captureSrRatio ) * stream_.nDeviceChannels[INPUT], + stream_.deviceFormat[INPUT] ); + + if ( callbackPulled ) { + // Convert callback buffer to user sample rate + convertBufferWasapi( stream_.deviceBuffer, + convBuffer, + stream_.nDeviceChannels[INPUT], + captureFormat->nSamplesPerSec, + stream_.sampleRate, + ( unsigned int ) ( stream_.bufferSize * captureSrRatio ), + convBufferSize, + stream_.deviceFormat[INPUT] ); + + if ( stream_.doConvertBuffer[INPUT] ) { + // Convert callback buffer to user format + convertBuffer( stream_.userBuffer[INPUT], + stream_.deviceBuffer, + stream_.convertInfo[INPUT] ); + } + else { + // no further conversion, simple copy deviceBuffer to userBuffer + memcpy( stream_.userBuffer[INPUT], + stream_.deviceBuffer, + stream_.bufferSize * stream_.nUserChannels[INPUT] * formatBytes( stream_.userFormat ) ); + } + } + } + else { + // if there is no capture stream, set callbackPulled flag + callbackPulled = true; + } + + // Execute Callback + // ================ + // 1. Execute user callback method + // 2. Handle return value from callback + + // if callback has not requested the stream to stop + if ( callbackPulled && !callbackStopped ) { + // Execute user callback method + callbackResult = callback( stream_.userBuffer[OUTPUT], + stream_.userBuffer[INPUT], + stream_.bufferSize, + getStreamTime(), + captureFlags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY ? RTAUDIO_INPUT_OVERFLOW : 0, + stream_.callbackInfo.userData ); + + // Handle return value from callback + if ( callbackResult == 1 ) { + // instantiate a thread to stop this thread + HANDLE threadHandle = CreateThread( NULL, 0, stopWasapiThread, this, 0, NULL ); + if ( !threadHandle ) { + errorType = RtAudioError::THREAD_ERROR; + errorText_ = "RtApiWasapi::wasapiThread: Unable to instantiate stream stop thread."; + goto Exit; + } + else if ( !CloseHandle( threadHandle ) ) { + errorType = RtAudioError::THREAD_ERROR; + errorText_ = "RtApiWasapi::wasapiThread: Unable to close stream stop thread handle."; + goto Exit; + } + + callbackStopped = true; + } + else if ( callbackResult == 2 ) { + // instantiate a thread to stop this thread + HANDLE threadHandle = CreateThread( NULL, 0, abortWasapiThread, this, 0, NULL ); + if ( !threadHandle ) { + errorType = RtAudioError::THREAD_ERROR; + errorText_ = "RtApiWasapi::wasapiThread: Unable to instantiate stream abort thread."; + goto Exit; + } + else if ( !CloseHandle( threadHandle ) ) { + errorType = RtAudioError::THREAD_ERROR; + errorText_ = "RtApiWasapi::wasapiThread: Unable to close stream abort thread handle."; + goto Exit; + } + + callbackStopped = true; + } + } + } + + // Callback Output + // =============== + // 1. Convert callback buffer to stream format + // 2. Convert callback buffer to stream sample rate and channel count + // 3. Push callback buffer into outputBuffer + + if ( renderAudioClient && callbackPulled ) { + if ( stream_.doConvertBuffer[OUTPUT] ) { + // Convert callback buffer to stream format + convertBuffer( stream_.deviceBuffer, + stream_.userBuffer[OUTPUT], + stream_.convertInfo[OUTPUT] ); + + } + + // Convert callback buffer to stream sample rate + convertBufferWasapi( convBuffer, + stream_.deviceBuffer, + stream_.nDeviceChannels[OUTPUT], + stream_.sampleRate, + renderFormat->nSamplesPerSec, + stream_.bufferSize, + convBufferSize, + stream_.deviceFormat[OUTPUT] ); + + // Push callback buffer into outputBuffer + callbackPushed = renderBuffer.pushBuffer( convBuffer, + convBufferSize * stream_.nDeviceChannels[OUTPUT], + stream_.deviceFormat[OUTPUT] ); + } + else { + // if there is no render stream, set callbackPushed flag + callbackPushed = true; + } + + // Stream Capture + // ============== + // 1. Get capture buffer from stream + // 2. Push capture buffer into inputBuffer + // 3. If 2. was successful: Release capture buffer + + if ( captureAudioClient ) { + // if the callback input buffer was not pulled from captureBuffer, wait for next capture event + if ( !callbackPulled ) { + WaitForSingleObject( captureEvent, INFINITE ); + } + + // Get capture buffer from stream + hr = captureClient->GetBuffer( &streamBuffer, + &bufferFrameCount, + &captureFlags, NULL, NULL ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve capture buffer."; + goto Exit; + } + + if ( bufferFrameCount != 0 ) { + // Push capture buffer into inputBuffer + if ( captureBuffer.pushBuffer( ( char* ) streamBuffer, + bufferFrameCount * stream_.nDeviceChannels[INPUT], + stream_.deviceFormat[INPUT] ) ) + { + // Release capture buffer + hr = captureClient->ReleaseBuffer( bufferFrameCount ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to release capture buffer."; + goto Exit; + } + } + else + { + // Inform WASAPI that capture was unsuccessful + hr = captureClient->ReleaseBuffer( 0 ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to release capture buffer."; + goto Exit; + } + } + } + else + { + // Inform WASAPI that capture was unsuccessful + hr = captureClient->ReleaseBuffer( 0 ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to release capture buffer."; + goto Exit; + } + } + } + + // Stream Render + // ============= + // 1. Get render buffer from stream + // 2. Pull next buffer from outputBuffer + // 3. If 2. was successful: Fill render buffer with next buffer + // Release render buffer + + if ( renderAudioClient ) { + // if the callback output buffer was not pushed to renderBuffer, wait for next render event + if ( callbackPulled && !callbackPushed ) { + WaitForSingleObject( renderEvent, INFINITE ); + } + + // Get render buffer from stream + hr = renderAudioClient->GetBufferSize( &bufferFrameCount ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve render buffer size."; + goto Exit; + } + + hr = renderAudioClient->GetCurrentPadding( &numFramesPadding ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve render buffer padding."; + goto Exit; + } + + bufferFrameCount -= numFramesPadding; + + if ( bufferFrameCount != 0 ) { + hr = renderClient->GetBuffer( bufferFrameCount, &streamBuffer ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to retrieve render buffer."; + goto Exit; + } + + // Pull next buffer from outputBuffer + // Fill render buffer with next buffer + if ( renderBuffer.pullBuffer( ( char* ) streamBuffer, + bufferFrameCount * stream_.nDeviceChannels[OUTPUT], + stream_.deviceFormat[OUTPUT] ) ) + { + // Release render buffer + hr = renderClient->ReleaseBuffer( bufferFrameCount, 0 ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to release render buffer."; + goto Exit; + } + } + else + { + // Inform WASAPI that render was unsuccessful + hr = renderClient->ReleaseBuffer( 0, 0 ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to release render buffer."; + goto Exit; + } + } + } + else + { + // Inform WASAPI that render was unsuccessful + hr = renderClient->ReleaseBuffer( 0, 0 ); + if ( FAILED( hr ) ) { + errorText_ = "RtApiWasapi::wasapiThread: Unable to release render buffer."; + goto Exit; + } + } + } + + // if the callback buffer was pushed renderBuffer reset callbackPulled flag + if ( callbackPushed ) { + callbackPulled = false; + } + + // tick stream time + RtApi::tickStreamTime(); + } + +Exit: + // clean up + CoTaskMemFree( captureFormat ); + CoTaskMemFree( renderFormat ); + + free ( convBuffer ); + + CoUninitialize(); + + // update stream state + stream_.state = STREAM_STOPPED; + + if ( errorText_.empty() ) + return; + else + error( errorType ); +} + +//******************** End of __WINDOWS_WASAPI__ *********************// +#endif + + +#if defined(__WINDOWS_DS__) // Windows DirectSound API + +// Modified by Robin Davies, October 2005 +// - Improvements to DirectX pointer chasing. +// - Bug fix for non-power-of-two Asio granularity used by Edirol PCR-A30. +// - Auto-call CoInitialize for DSOUND and ASIO platforms. +// Various revisions for RtAudio 4.0 by Gary Scavone, April 2007 +// Changed device query structure for RtAudio 4.0.7, January 2010 + +#include +#include +#include + +#if defined(__MINGW32__) + // missing from latest mingw winapi +#define WAVE_FORMAT_96M08 0x00010000 /* 96 kHz, Mono, 8-bit */ +#define WAVE_FORMAT_96S08 0x00020000 /* 96 kHz, Stereo, 8-bit */ +#define WAVE_FORMAT_96M16 0x00040000 /* 96 kHz, Mono, 16-bit */ +#define WAVE_FORMAT_96S16 0x00080000 /* 96 kHz, Stereo, 16-bit */ +#endif + +#define MINIMUM_DEVICE_BUFFER_SIZE 32768 + +#ifdef _MSC_VER // if Microsoft Visual C++ +#pragma comment( lib, "winmm.lib" ) // then, auto-link winmm.lib. Otherwise, it has to be added manually. +#endif + +static inline DWORD dsPointerBetween( DWORD pointer, DWORD laterPointer, DWORD earlierPointer, DWORD bufferSize ) +{ + if ( pointer > bufferSize ) pointer -= bufferSize; + if ( laterPointer < earlierPointer ) laterPointer += bufferSize; + if ( pointer < earlierPointer ) pointer += bufferSize; + return pointer >= earlierPointer && pointer < laterPointer; +} + +// A structure to hold various information related to the DirectSound +// API implementation. +struct DsHandle { + unsigned int drainCounter; // Tracks callback counts when draining + bool internalDrain; // Indicates if stop is initiated from callback or not. + void *id[2]; + void *buffer[2]; + bool xrun[2]; + UINT bufferPointer[2]; + DWORD dsBufferSize[2]; + DWORD dsPointerLeadTime[2]; // the number of bytes ahead of the safe pointer to lead by. + HANDLE condition; + + DsHandle() + :drainCounter(0), internalDrain(false) { id[0] = 0; id[1] = 0; buffer[0] = 0; buffer[1] = 0; xrun[0] = false; xrun[1] = false; bufferPointer[0] = 0; bufferPointer[1] = 0; } +}; + +// Declarations for utility functions, callbacks, and structures +// specific to the DirectSound implementation. +static BOOL CALLBACK deviceQueryCallback( LPGUID lpguid, + LPCTSTR description, + LPCTSTR module, + LPVOID lpContext ); + +static const char* getErrorString( int code ); + +static unsigned __stdcall callbackHandler( void *ptr ); + +struct DsDevice { + LPGUID id[2]; + bool validId[2]; + bool found; + std::string name; + + DsDevice() + : found(false) { validId[0] = false; validId[1] = false; } +}; + +struct DsProbeData { + bool isInput; + std::vector* dsDevices; +}; + +RtApiDs :: RtApiDs() +{ + // Dsound will run both-threaded. If CoInitialize fails, then just + // accept whatever the mainline chose for a threading model. + coInitialized_ = false; + HRESULT hr = CoInitialize( NULL ); + if ( !FAILED( hr ) ) coInitialized_ = true; +} + +RtApiDs :: ~RtApiDs() +{ + if ( coInitialized_ ) CoUninitialize(); // balanced call. + if ( stream_.state != STREAM_CLOSED ) closeStream(); +} + +// The DirectSound default output is always the first device. +unsigned int RtApiDs :: getDefaultOutputDevice( void ) +{ + return 0; +} + +// The DirectSound default input is always the first input device, +// which is the first capture device enumerated. +unsigned int RtApiDs :: getDefaultInputDevice( void ) +{ + return 0; +} + +unsigned int RtApiDs :: getDeviceCount( void ) +{ + // Set query flag for previously found devices to false, so that we + // can check for any devices that have disappeared. + for ( unsigned int i=0; i indices; + for ( unsigned int i=0; i(dsDevices.size()); +} + +RtAudio::DeviceInfo RtApiDs :: getDeviceInfo( unsigned int device ) +{ + RtAudio::DeviceInfo info; + info.probed = false; + + if ( dsDevices.size() == 0 ) { + // Force a query of all devices + getDeviceCount(); + if ( dsDevices.size() == 0 ) { + errorText_ = "RtApiDs::getDeviceInfo: no devices found!"; + error( RtAudioError::INVALID_USE ); + return info; + } + } + + if ( device >= dsDevices.size() ) { + errorText_ = "RtApiDs::getDeviceInfo: device ID is invalid!"; + error( RtAudioError::INVALID_USE ); + return info; + } + + HRESULT result; + if ( dsDevices[ device ].validId[0] == false ) goto probeInput; + + LPDIRECTSOUND output; + DSCAPS outCaps; + result = DirectSoundCreate( dsDevices[ device ].id[0], &output, NULL ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::getDeviceInfo: error (" << getErrorString( result ) << ") opening output device (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + goto probeInput; + } + + outCaps.dwSize = sizeof( outCaps ); + result = output->GetCaps( &outCaps ); + if ( FAILED( result ) ) { + output->Release(); + errorStream_ << "RtApiDs::getDeviceInfo: error (" << getErrorString( result ) << ") getting capabilities!"; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + goto probeInput; + } + + // Get output channel information. + info.outputChannels = ( outCaps.dwFlags & DSCAPS_PRIMARYSTEREO ) ? 2 : 1; + + // Get sample rate information. + info.sampleRates.clear(); + for ( unsigned int k=0; k= (unsigned int) outCaps.dwMinSecondarySampleRate && + SAMPLE_RATES[k] <= (unsigned int) outCaps.dwMaxSecondarySampleRate ) + info.sampleRates.push_back( SAMPLE_RATES[k] ); + } + + // Get format information. + if ( outCaps.dwFlags & DSCAPS_PRIMARY16BIT ) info.nativeFormats |= RTAUDIO_SINT16; + if ( outCaps.dwFlags & DSCAPS_PRIMARY8BIT ) info.nativeFormats |= RTAUDIO_SINT8; + + output->Release(); + + if ( getDefaultOutputDevice() == device ) + info.isDefaultOutput = true; + + if ( dsDevices[ device ].validId[1] == false ) { + info.name = dsDevices[ device ].name; + info.probed = true; + return info; + } + + probeInput: + + LPDIRECTSOUNDCAPTURE input; + result = DirectSoundCaptureCreate( dsDevices[ device ].id[1], &input, NULL ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::getDeviceInfo: error (" << getErrorString( result ) << ") opening input device (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + DSCCAPS inCaps; + inCaps.dwSize = sizeof( inCaps ); + result = input->GetCaps( &inCaps ); + if ( FAILED( result ) ) { + input->Release(); + errorStream_ << "RtApiDs::getDeviceInfo: error (" << getErrorString( result ) << ") getting object capabilities (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + // Get input channel information. + info.inputChannels = inCaps.dwChannels; + + // Get sample rate and format information. + std::vector rates; + if ( inCaps.dwChannels >= 2 ) { + if ( inCaps.dwFormats & WAVE_FORMAT_1S16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_2S16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_4S16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_96S16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_1S08 ) info.nativeFormats |= RTAUDIO_SINT8; + if ( inCaps.dwFormats & WAVE_FORMAT_2S08 ) info.nativeFormats |= RTAUDIO_SINT8; + if ( inCaps.dwFormats & WAVE_FORMAT_4S08 ) info.nativeFormats |= RTAUDIO_SINT8; + if ( inCaps.dwFormats & WAVE_FORMAT_96S08 ) info.nativeFormats |= RTAUDIO_SINT8; + + if ( info.nativeFormats & RTAUDIO_SINT16 ) { + if ( inCaps.dwFormats & WAVE_FORMAT_1S16 ) rates.push_back( 11025 ); + if ( inCaps.dwFormats & WAVE_FORMAT_2S16 ) rates.push_back( 22050 ); + if ( inCaps.dwFormats & WAVE_FORMAT_4S16 ) rates.push_back( 44100 ); + if ( inCaps.dwFormats & WAVE_FORMAT_96S16 ) rates.push_back( 96000 ); + } + else if ( info.nativeFormats & RTAUDIO_SINT8 ) { + if ( inCaps.dwFormats & WAVE_FORMAT_1S08 ) rates.push_back( 11025 ); + if ( inCaps.dwFormats & WAVE_FORMAT_2S08 ) rates.push_back( 22050 ); + if ( inCaps.dwFormats & WAVE_FORMAT_4S08 ) rates.push_back( 44100 ); + if ( inCaps.dwFormats & WAVE_FORMAT_96S08 ) rates.push_back( 96000 ); + } + } + else if ( inCaps.dwChannels == 1 ) { + if ( inCaps.dwFormats & WAVE_FORMAT_1M16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_2M16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_4M16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_96M16 ) info.nativeFormats |= RTAUDIO_SINT16; + if ( inCaps.dwFormats & WAVE_FORMAT_1M08 ) info.nativeFormats |= RTAUDIO_SINT8; + if ( inCaps.dwFormats & WAVE_FORMAT_2M08 ) info.nativeFormats |= RTAUDIO_SINT8; + if ( inCaps.dwFormats & WAVE_FORMAT_4M08 ) info.nativeFormats |= RTAUDIO_SINT8; + if ( inCaps.dwFormats & WAVE_FORMAT_96M08 ) info.nativeFormats |= RTAUDIO_SINT8; + + if ( info.nativeFormats & RTAUDIO_SINT16 ) { + if ( inCaps.dwFormats & WAVE_FORMAT_1M16 ) rates.push_back( 11025 ); + if ( inCaps.dwFormats & WAVE_FORMAT_2M16 ) rates.push_back( 22050 ); + if ( inCaps.dwFormats & WAVE_FORMAT_4M16 ) rates.push_back( 44100 ); + if ( inCaps.dwFormats & WAVE_FORMAT_96M16 ) rates.push_back( 96000 ); + } + else if ( info.nativeFormats & RTAUDIO_SINT8 ) { + if ( inCaps.dwFormats & WAVE_FORMAT_1M08 ) rates.push_back( 11025 ); + if ( inCaps.dwFormats & WAVE_FORMAT_2M08 ) rates.push_back( 22050 ); + if ( inCaps.dwFormats & WAVE_FORMAT_4M08 ) rates.push_back( 44100 ); + if ( inCaps.dwFormats & WAVE_FORMAT_96M08 ) rates.push_back( 96000 ); + } + } + else info.inputChannels = 0; // technically, this would be an error + + input->Release(); + + if ( info.inputChannels == 0 ) return info; + + // Copy the supported rates to the info structure but avoid duplication. + bool found; + for ( unsigned int i=0; i 0 && info.inputChannels > 0 ) + info.duplexChannels = (info.outputChannels > info.inputChannels) ? info.inputChannels : info.outputChannels; + + if ( device == 0 ) info.isDefaultInput = true; + + // Copy name and return. + info.name = dsDevices[ device ].name; + info.probed = true; + return info; +} + +bool RtApiDs :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ) +{ + if ( channels + firstChannel > 2 ) { + errorText_ = "RtApiDs::probeDeviceOpen: DirectSound does not support more than 2 channels per device."; + return FAILURE; + } + + size_t nDevices = dsDevices.size(); + if ( nDevices == 0 ) { + // This should not happen because a check is made before this function is called. + errorText_ = "RtApiDs::probeDeviceOpen: no devices found!"; + return FAILURE; + } + + if ( device >= nDevices ) { + // This should not happen because a check is made before this function is called. + errorText_ = "RtApiDs::probeDeviceOpen: device ID is invalid!"; + return FAILURE; + } + + if ( mode == OUTPUT ) { + if ( dsDevices[ device ].validId[0] == false ) { + errorStream_ << "RtApiDs::probeDeviceOpen: device (" << device << ") does not support output!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + else { // mode == INPUT + if ( dsDevices[ device ].validId[1] == false ) { + errorStream_ << "RtApiDs::probeDeviceOpen: device (" << device << ") does not support input!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // According to a note in PortAudio, using GetDesktopWindow() + // instead of GetForegroundWindow() is supposed to avoid problems + // that occur when the application's window is not the foreground + // window. Also, if the application window closes before the + // DirectSound buffer, DirectSound can crash. In the past, I had + // problems when using GetDesktopWindow() but it seems fine now + // (January 2010). I'll leave it commented here. + // HWND hWnd = GetForegroundWindow(); + HWND hWnd = GetDesktopWindow(); + + // Check the numberOfBuffers parameter and limit the lowest value to + // two. This is a judgement call and a value of two is probably too + // low for capture, but it should work for playback. + int nBuffers = 0; + if ( options ) nBuffers = options->numberOfBuffers; + if ( options && options->flags & RTAUDIO_MINIMIZE_LATENCY ) nBuffers = 2; + if ( nBuffers < 2 ) nBuffers = 3; + + // Check the lower range of the user-specified buffer size and set + // (arbitrarily) to a lower bound of 32. + if ( *bufferSize < 32 ) *bufferSize = 32; + + // Create the wave format structure. The data format setting will + // be determined later. + WAVEFORMATEX waveFormat; + ZeroMemory( &waveFormat, sizeof(WAVEFORMATEX) ); + waveFormat.wFormatTag = WAVE_FORMAT_PCM; + waveFormat.nChannels = channels + firstChannel; + waveFormat.nSamplesPerSec = (unsigned long) sampleRate; + + // Determine the device buffer size. By default, we'll use the value + // defined above (32K), but we will grow it to make allowances for + // very large software buffer sizes. + DWORD dsBufferSize = MINIMUM_DEVICE_BUFFER_SIZE; + DWORD dsPointerLeadTime = 0; + + void *ohandle = 0, *bhandle = 0; + HRESULT result; + if ( mode == OUTPUT ) { + + LPDIRECTSOUND output; + result = DirectSoundCreate( dsDevices[ device ].id[0], &output, NULL ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") opening output device (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + DSCAPS outCaps; + outCaps.dwSize = sizeof( outCaps ); + result = output->GetCaps( &outCaps ); + if ( FAILED( result ) ) { + output->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") getting capabilities (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Check channel information. + if ( channels + firstChannel == 2 && !( outCaps.dwFlags & DSCAPS_PRIMARYSTEREO ) ) { + errorStream_ << "RtApiDs::getDeviceInfo: the output device (" << dsDevices[ device ].name << ") does not support stereo playback."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Check format information. Use 16-bit format unless not + // supported or user requests 8-bit. + if ( outCaps.dwFlags & DSCAPS_PRIMARY16BIT && + !( format == RTAUDIO_SINT8 && outCaps.dwFlags & DSCAPS_PRIMARY8BIT ) ) { + waveFormat.wBitsPerSample = 16; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + } + else { + waveFormat.wBitsPerSample = 8; + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + } + stream_.userFormat = format; + + // Update wave format structure and buffer information. + waveFormat.nBlockAlign = waveFormat.nChannels * waveFormat.wBitsPerSample / 8; + waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign; + dsPointerLeadTime = nBuffers * (*bufferSize) * (waveFormat.wBitsPerSample / 8) * channels; + + // If the user wants an even bigger buffer, increase the device buffer size accordingly. + while ( dsPointerLeadTime * 2U > dsBufferSize ) + dsBufferSize *= 2; + + // Set cooperative level to DSSCL_EXCLUSIVE ... sound stops when window focus changes. + // result = output->SetCooperativeLevel( hWnd, DSSCL_EXCLUSIVE ); + // Set cooperative level to DSSCL_PRIORITY ... sound remains when window focus changes. + result = output->SetCooperativeLevel( hWnd, DSSCL_PRIORITY ); + if ( FAILED( result ) ) { + output->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") setting cooperative level (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Even though we will write to the secondary buffer, we need to + // access the primary buffer to set the correct output format + // (since the default is 8-bit, 22 kHz!). Setup the DS primary + // buffer description. + DSBUFFERDESC bufferDescription; + ZeroMemory( &bufferDescription, sizeof( DSBUFFERDESC ) ); + bufferDescription.dwSize = sizeof( DSBUFFERDESC ); + bufferDescription.dwFlags = DSBCAPS_PRIMARYBUFFER; + + // Obtain the primary buffer + LPDIRECTSOUNDBUFFER buffer; + result = output->CreateSoundBuffer( &bufferDescription, &buffer, NULL ); + if ( FAILED( result ) ) { + output->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") accessing primary buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the primary DS buffer sound format. + result = buffer->SetFormat( &waveFormat ); + if ( FAILED( result ) ) { + output->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") setting primary buffer format (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Setup the secondary DS buffer description. + ZeroMemory( &bufferDescription, sizeof( DSBUFFERDESC ) ); + bufferDescription.dwSize = sizeof( DSBUFFERDESC ); + bufferDescription.dwFlags = ( DSBCAPS_STICKYFOCUS | + DSBCAPS_GLOBALFOCUS | + DSBCAPS_GETCURRENTPOSITION2 | + DSBCAPS_LOCHARDWARE ); // Force hardware mixing + bufferDescription.dwBufferBytes = dsBufferSize; + bufferDescription.lpwfxFormat = &waveFormat; + + // Try to create the secondary DS buffer. If that doesn't work, + // try to use software mixing. Otherwise, there's a problem. + result = output->CreateSoundBuffer( &bufferDescription, &buffer, NULL ); + if ( FAILED( result ) ) { + bufferDescription.dwFlags = ( DSBCAPS_STICKYFOCUS | + DSBCAPS_GLOBALFOCUS | + DSBCAPS_GETCURRENTPOSITION2 | + DSBCAPS_LOCSOFTWARE ); // Force software mixing + result = output->CreateSoundBuffer( &bufferDescription, &buffer, NULL ); + if ( FAILED( result ) ) { + output->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") creating secondary buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // Get the buffer size ... might be different from what we specified. + DSBCAPS dsbcaps; + dsbcaps.dwSize = sizeof( DSBCAPS ); + result = buffer->GetCaps( &dsbcaps ); + if ( FAILED( result ) ) { + output->Release(); + buffer->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") getting buffer settings (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + dsBufferSize = dsbcaps.dwBufferBytes; + + // Lock the DS buffer + LPVOID audioPtr; + DWORD dataLen; + result = buffer->Lock( 0, dsBufferSize, &audioPtr, &dataLen, NULL, NULL, 0 ); + if ( FAILED( result ) ) { + output->Release(); + buffer->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") locking buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Zero the DS buffer + ZeroMemory( audioPtr, dataLen ); + + // Unlock the DS buffer + result = buffer->Unlock( audioPtr, dataLen, NULL, 0 ); + if ( FAILED( result ) ) { + output->Release(); + buffer->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") unlocking buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + ohandle = (void *) output; + bhandle = (void *) buffer; + } + + if ( mode == INPUT ) { + + LPDIRECTSOUNDCAPTURE input; + result = DirectSoundCaptureCreate( dsDevices[ device ].id[1], &input, NULL ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") opening input device (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + DSCCAPS inCaps; + inCaps.dwSize = sizeof( inCaps ); + result = input->GetCaps( &inCaps ); + if ( FAILED( result ) ) { + input->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") getting input capabilities (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Check channel information. + if ( inCaps.dwChannels < channels + firstChannel ) { + errorText_ = "RtApiDs::getDeviceInfo: the input device does not support requested input channels."; + return FAILURE; + } + + // Check format information. Use 16-bit format unless user + // requests 8-bit. + DWORD deviceFormats; + if ( channels + firstChannel == 2 ) { + deviceFormats = WAVE_FORMAT_1S08 | WAVE_FORMAT_2S08 | WAVE_FORMAT_4S08 | WAVE_FORMAT_96S08; + if ( format == RTAUDIO_SINT8 && inCaps.dwFormats & deviceFormats ) { + waveFormat.wBitsPerSample = 8; + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + } + else { // assume 16-bit is supported + waveFormat.wBitsPerSample = 16; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + } + } + else { // channel == 1 + deviceFormats = WAVE_FORMAT_1M08 | WAVE_FORMAT_2M08 | WAVE_FORMAT_4M08 | WAVE_FORMAT_96M08; + if ( format == RTAUDIO_SINT8 && inCaps.dwFormats & deviceFormats ) { + waveFormat.wBitsPerSample = 8; + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + } + else { // assume 16-bit is supported + waveFormat.wBitsPerSample = 16; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + } + } + stream_.userFormat = format; + + // Update wave format structure and buffer information. + waveFormat.nBlockAlign = waveFormat.nChannels * waveFormat.wBitsPerSample / 8; + waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign; + dsPointerLeadTime = nBuffers * (*bufferSize) * (waveFormat.wBitsPerSample / 8) * channels; + + // If the user wants an even bigger buffer, increase the device buffer size accordingly. + while ( dsPointerLeadTime * 2U > dsBufferSize ) + dsBufferSize *= 2; + + // Setup the secondary DS buffer description. + DSCBUFFERDESC bufferDescription; + ZeroMemory( &bufferDescription, sizeof( DSCBUFFERDESC ) ); + bufferDescription.dwSize = sizeof( DSCBUFFERDESC ); + bufferDescription.dwFlags = 0; + bufferDescription.dwReserved = 0; + bufferDescription.dwBufferBytes = dsBufferSize; + bufferDescription.lpwfxFormat = &waveFormat; + + // Create the capture buffer. + LPDIRECTSOUNDCAPTUREBUFFER buffer; + result = input->CreateCaptureBuffer( &bufferDescription, &buffer, NULL ); + if ( FAILED( result ) ) { + input->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") creating input buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Get the buffer size ... might be different from what we specified. + DSCBCAPS dscbcaps; + dscbcaps.dwSize = sizeof( DSCBCAPS ); + result = buffer->GetCaps( &dscbcaps ); + if ( FAILED( result ) ) { + input->Release(); + buffer->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") getting buffer settings (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + dsBufferSize = dscbcaps.dwBufferBytes; + + // NOTE: We could have a problem here if this is a duplex stream + // and the play and capture hardware buffer sizes are different + // (I'm actually not sure if that is a problem or not). + // Currently, we are not verifying that. + + // Lock the capture buffer + LPVOID audioPtr; + DWORD dataLen; + result = buffer->Lock( 0, dsBufferSize, &audioPtr, &dataLen, NULL, NULL, 0 ); + if ( FAILED( result ) ) { + input->Release(); + buffer->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") locking input buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Zero the buffer + ZeroMemory( audioPtr, dataLen ); + + // Unlock the buffer + result = buffer->Unlock( audioPtr, dataLen, NULL, 0 ); + if ( FAILED( result ) ) { + input->Release(); + buffer->Release(); + errorStream_ << "RtApiDs::probeDeviceOpen: error (" << getErrorString( result ) << ") unlocking input buffer (" << dsDevices[ device ].name << ")!"; + errorText_ = errorStream_.str(); + return FAILURE; + } + + ohandle = (void *) input; + bhandle = (void *) buffer; + } + + // Set various stream parameters + DsHandle *handle = 0; + stream_.nDeviceChannels[mode] = channels + firstChannel; + stream_.nUserChannels[mode] = channels; + stream_.bufferSize = *bufferSize; + stream_.channelOffset[mode] = firstChannel; + stream_.deviceInterleaved[mode] = true; + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) stream_.userInterleaved = false; + else stream_.userInterleaved = true; + + // Set flag for buffer conversion + stream_.doConvertBuffer[mode] = false; + if (stream_.nUserChannels[mode] != stream_.nDeviceChannels[mode]) + stream_.doConvertBuffer[mode] = true; + if (stream_.userFormat != stream_.deviceFormat[mode]) + stream_.doConvertBuffer[mode] = true; + if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] && + stream_.nUserChannels[mode] > 1 ) + stream_.doConvertBuffer[mode] = true; + + // Allocate necessary internal buffers + long bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiDs::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + + if ( stream_.doConvertBuffer[mode] ) { + + bool makeBuffer = true; + bufferBytes = stream_.nDeviceChannels[mode] * formatBytes( stream_.deviceFormat[mode] ); + if ( mode == INPUT ) { + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + if ( bufferBytes <= (long) bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiDs::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + // Allocate our DsHandle structures for the stream. + if ( stream_.apiHandle == 0 ) { + try { + handle = new DsHandle; + } + catch ( std::bad_alloc& ) { + errorText_ = "RtApiDs::probeDeviceOpen: error allocating AsioHandle memory."; + goto error; + } + + // Create a manual-reset event. + handle->condition = CreateEvent( NULL, // no security + TRUE, // manual-reset + FALSE, // non-signaled initially + NULL ); // unnamed + stream_.apiHandle = (void *) handle; + } + else + handle = (DsHandle *) stream_.apiHandle; + handle->id[mode] = ohandle; + handle->buffer[mode] = bhandle; + handle->dsBufferSize[mode] = dsBufferSize; + handle->dsPointerLeadTime[mode] = dsPointerLeadTime; + + stream_.device[mode] = device; + stream_.state = STREAM_STOPPED; + if ( stream_.mode == OUTPUT && mode == INPUT ) + // We had already set up an output stream. + stream_.mode = DUPLEX; + else + stream_.mode = mode; + stream_.nBuffers = nBuffers; + stream_.sampleRate = sampleRate; + + // Setup the buffer conversion information structure. + if ( stream_.doConvertBuffer[mode] ) setConvertInfo( mode, firstChannel ); + + // Setup the callback thread. + if ( stream_.callbackInfo.isRunning == false ) { + unsigned threadId; + stream_.callbackInfo.isRunning = true; + stream_.callbackInfo.object = (void *) this; + stream_.callbackInfo.thread = _beginthreadex( NULL, 0, &callbackHandler, + &stream_.callbackInfo, 0, &threadId ); + if ( stream_.callbackInfo.thread == 0 ) { + errorText_ = "RtApiDs::probeDeviceOpen: error creating callback thread!"; + goto error; + } + + // Boost DS thread priority + SetThreadPriority( (HANDLE) stream_.callbackInfo.thread, THREAD_PRIORITY_HIGHEST ); + } + return SUCCESS; + + error: + if ( handle ) { + if ( handle->buffer[0] ) { // the object pointer can be NULL and valid + LPDIRECTSOUND object = (LPDIRECTSOUND) handle->id[0]; + LPDIRECTSOUNDBUFFER buffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + if ( buffer ) buffer->Release(); + object->Release(); + } + if ( handle->buffer[1] ) { + LPDIRECTSOUNDCAPTURE object = (LPDIRECTSOUNDCAPTURE) handle->id[1]; + LPDIRECTSOUNDCAPTUREBUFFER buffer = (LPDIRECTSOUNDCAPTUREBUFFER) handle->buffer[1]; + if ( buffer ) buffer->Release(); + object->Release(); + } + CloseHandle( handle->condition ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.state = STREAM_CLOSED; + return FAILURE; +} + +void RtApiDs :: closeStream() +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiDs::closeStream(): no open stream to close!"; + error( RtAudioError::WARNING ); + return; + } + + // Stop the callback thread. + stream_.callbackInfo.isRunning = false; + WaitForSingleObject( (HANDLE) stream_.callbackInfo.thread, INFINITE ); + CloseHandle( (HANDLE) stream_.callbackInfo.thread ); + + DsHandle *handle = (DsHandle *) stream_.apiHandle; + if ( handle ) { + if ( handle->buffer[0] ) { // the object pointer can be NULL and valid + LPDIRECTSOUND object = (LPDIRECTSOUND) handle->id[0]; + LPDIRECTSOUNDBUFFER buffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + if ( buffer ) { + buffer->Stop(); + buffer->Release(); + } + object->Release(); + } + if ( handle->buffer[1] ) { + LPDIRECTSOUNDCAPTURE object = (LPDIRECTSOUNDCAPTURE) handle->id[1]; + LPDIRECTSOUNDCAPTUREBUFFER buffer = (LPDIRECTSOUNDCAPTUREBUFFER) handle->buffer[1]; + if ( buffer ) { + buffer->Stop(); + buffer->Release(); + } + object->Release(); + } + CloseHandle( handle->condition ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; +} + +void RtApiDs :: startStream() +{ + verifyStream(); + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiDs::startStream(): the stream is already running!"; + error( RtAudioError::WARNING ); + return; + } + + DsHandle *handle = (DsHandle *) stream_.apiHandle; + + // Increase scheduler frequency on lesser windows (a side-effect of + // increasing timer accuracy). On greater windows (Win2K or later), + // this is already in effect. + timeBeginPeriod( 1 ); + + buffersRolling = false; + duplexPrerollBytes = 0; + + if ( stream_.mode == DUPLEX ) { + // 0.5 seconds of silence in DUPLEX mode while the devices spin up and synchronize. + duplexPrerollBytes = (int) ( 0.5 * stream_.sampleRate * formatBytes( stream_.deviceFormat[1] ) * stream_.nDeviceChannels[1] ); + } + + HRESULT result = 0; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + LPDIRECTSOUNDBUFFER buffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + result = buffer->Play( 0, 0, DSBPLAY_LOOPING ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::startStream: error (" << getErrorString( result ) << ") starting output buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + + LPDIRECTSOUNDCAPTUREBUFFER buffer = (LPDIRECTSOUNDCAPTUREBUFFER) handle->buffer[1]; + result = buffer->Start( DSCBSTART_LOOPING ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::startStream: error (" << getErrorString( result ) << ") starting input buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + handle->drainCounter = 0; + handle->internalDrain = false; + ResetEvent( handle->condition ); + stream_.state = STREAM_RUNNING; + + unlock: + if ( FAILED( result ) ) error( RtAudioError::SYSTEM_ERROR ); +} + +void RtApiDs :: stopStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiDs::stopStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + HRESULT result = 0; + LPVOID audioPtr; + DWORD dataLen; + DsHandle *handle = (DsHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + if ( handle->drainCounter == 0 ) { + handle->drainCounter = 2; + WaitForSingleObject( handle->condition, INFINITE ); // block until signaled + } + + stream_.state = STREAM_STOPPED; + + MUTEX_LOCK( &stream_.mutex ); + + // Stop the buffer and clear memory + LPDIRECTSOUNDBUFFER buffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + result = buffer->Stop(); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::stopStream: error (" << getErrorString( result ) << ") stopping output buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + + // Lock the buffer and clear it so that if we start to play again, + // we won't have old data playing. + result = buffer->Lock( 0, handle->dsBufferSize[0], &audioPtr, &dataLen, NULL, NULL, 0 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::stopStream: error (" << getErrorString( result ) << ") locking output buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + + // Zero the DS buffer + ZeroMemory( audioPtr, dataLen ); + + // Unlock the DS buffer + result = buffer->Unlock( audioPtr, dataLen, NULL, 0 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::stopStream: error (" << getErrorString( result ) << ") unlocking output buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + + // If we start playing again, we must begin at beginning of buffer. + handle->bufferPointer[0] = 0; + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + LPDIRECTSOUNDCAPTUREBUFFER buffer = (LPDIRECTSOUNDCAPTUREBUFFER) handle->buffer[1]; + audioPtr = NULL; + dataLen = 0; + + stream_.state = STREAM_STOPPED; + + if ( stream_.mode != DUPLEX ) + MUTEX_LOCK( &stream_.mutex ); + + result = buffer->Stop(); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::stopStream: error (" << getErrorString( result ) << ") stopping input buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + + // Lock the buffer and clear it so that if we start to play again, + // we won't have old data playing. + result = buffer->Lock( 0, handle->dsBufferSize[1], &audioPtr, &dataLen, NULL, NULL, 0 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::stopStream: error (" << getErrorString( result ) << ") locking input buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + + // Zero the DS buffer + ZeroMemory( audioPtr, dataLen ); + + // Unlock the DS buffer + result = buffer->Unlock( audioPtr, dataLen, NULL, 0 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::stopStream: error (" << getErrorString( result ) << ") unlocking input buffer!"; + errorText_ = errorStream_.str(); + goto unlock; + } + + // If we start recording again, we must begin at beginning of buffer. + handle->bufferPointer[1] = 0; + } + + unlock: + timeEndPeriod( 1 ); // revert to normal scheduler frequency on lesser windows. + MUTEX_UNLOCK( &stream_.mutex ); + + if ( FAILED( result ) ) error( RtAudioError::SYSTEM_ERROR ); +} + +void RtApiDs :: abortStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiDs::abortStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + DsHandle *handle = (DsHandle *) stream_.apiHandle; + handle->drainCounter = 2; + + stopStream(); +} + +void RtApiDs :: callbackEvent() +{ + if ( stream_.state == STREAM_STOPPED || stream_.state == STREAM_STOPPING ) { + Sleep( 50 ); // sleep 50 milliseconds + return; + } + + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiDs::callbackEvent(): the stream is closed ... this shouldn't happen!"; + error( RtAudioError::WARNING ); + return; + } + + CallbackInfo *info = (CallbackInfo *) &stream_.callbackInfo; + DsHandle *handle = (DsHandle *) stream_.apiHandle; + + // Check if we were draining the stream and signal is finished. + if ( handle->drainCounter > stream_.nBuffers + 2 ) { + + stream_.state = STREAM_STOPPING; + if ( handle->internalDrain == false ) + SetEvent( handle->condition ); + else + stopStream(); + return; + } + + // Invoke user callback to get fresh output data UNLESS we are + // draining stream. + if ( handle->drainCounter == 0 ) { + RtAudioCallback callback = (RtAudioCallback) info->callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && handle->xrun[0] == true ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + handle->xrun[0] = false; + } + if ( stream_.mode != OUTPUT && handle->xrun[1] == true ) { + status |= RTAUDIO_INPUT_OVERFLOW; + handle->xrun[1] = false; + } + int cbReturnValue = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, info->userData ); + if ( cbReturnValue == 2 ) { + stream_.state = STREAM_STOPPING; + handle->drainCounter = 2; + abortStream(); + return; + } + else if ( cbReturnValue == 1 ) { + handle->drainCounter = 1; + handle->internalDrain = true; + } + } + + HRESULT result; + DWORD currentWritePointer, safeWritePointer; + DWORD currentReadPointer, safeReadPointer; + UINT nextWritePointer; + + LPVOID buffer1 = NULL; + LPVOID buffer2 = NULL; + DWORD bufferSize1 = 0; + DWORD bufferSize2 = 0; + + char *buffer; + long bufferBytes; + + MUTEX_LOCK( &stream_.mutex ); + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + + if ( buffersRolling == false ) { + if ( stream_.mode == DUPLEX ) { + //assert( handle->dsBufferSize[0] == handle->dsBufferSize[1] ); + + // It takes a while for the devices to get rolling. As a result, + // there's no guarantee that the capture and write device pointers + // will move in lockstep. Wait here for both devices to start + // rolling, and then set our buffer pointers accordingly. + // e.g. Crystal Drivers: the capture buffer starts up 5700 to 9600 + // bytes later than the write buffer. + + // Stub: a serious risk of having a pre-emptive scheduling round + // take place between the two GetCurrentPosition calls... but I'm + // really not sure how to solve the problem. Temporarily boost to + // Realtime priority, maybe; but I'm not sure what priority the + // DirectSound service threads run at. We *should* be roughly + // within a ms or so of correct. + + LPDIRECTSOUNDBUFFER dsWriteBuffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + LPDIRECTSOUNDCAPTUREBUFFER dsCaptureBuffer = (LPDIRECTSOUNDCAPTUREBUFFER) handle->buffer[1]; + + DWORD startSafeWritePointer, startSafeReadPointer; + + result = dsWriteBuffer->GetCurrentPosition( NULL, &startSafeWritePointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current write position!"; + errorText_ = errorStream_.str(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + result = dsCaptureBuffer->GetCurrentPosition( NULL, &startSafeReadPointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current read position!"; + errorText_ = errorStream_.str(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + while ( true ) { + result = dsWriteBuffer->GetCurrentPosition( NULL, &safeWritePointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current write position!"; + errorText_ = errorStream_.str(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + result = dsCaptureBuffer->GetCurrentPosition( NULL, &safeReadPointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current read position!"; + errorText_ = errorStream_.str(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + if ( safeWritePointer != startSafeWritePointer && safeReadPointer != startSafeReadPointer ) break; + Sleep( 1 ); + } + + //assert( handle->dsBufferSize[0] == handle->dsBufferSize[1] ); + + handle->bufferPointer[0] = safeWritePointer + handle->dsPointerLeadTime[0]; + if ( handle->bufferPointer[0] >= handle->dsBufferSize[0] ) handle->bufferPointer[0] -= handle->dsBufferSize[0]; + handle->bufferPointer[1] = safeReadPointer; + } + else if ( stream_.mode == OUTPUT ) { + + // Set the proper nextWritePosition after initial startup. + LPDIRECTSOUNDBUFFER dsWriteBuffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + result = dsWriteBuffer->GetCurrentPosition( ¤tWritePointer, &safeWritePointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current write position!"; + errorText_ = errorStream_.str(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + handle->bufferPointer[0] = safeWritePointer + handle->dsPointerLeadTime[0]; + if ( handle->bufferPointer[0] >= handle->dsBufferSize[0] ) handle->bufferPointer[0] -= handle->dsBufferSize[0]; + } + + buffersRolling = true; + } + + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + LPDIRECTSOUNDBUFFER dsBuffer = (LPDIRECTSOUNDBUFFER) handle->buffer[0]; + + if ( handle->drainCounter > 1 ) { // write zeros to the output stream + bufferBytes = stream_.bufferSize * stream_.nUserChannels[0]; + bufferBytes *= formatBytes( stream_.userFormat ); + memset( stream_.userBuffer[0], 0, bufferBytes ); + } + + // Setup parameters and do buffer conversion if necessary. + if ( stream_.doConvertBuffer[0] ) { + buffer = stream_.deviceBuffer; + convertBuffer( buffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + bufferBytes = stream_.bufferSize * stream_.nDeviceChannels[0]; + bufferBytes *= formatBytes( stream_.deviceFormat[0] ); + } + else { + buffer = stream_.userBuffer[0]; + bufferBytes = stream_.bufferSize * stream_.nUserChannels[0]; + bufferBytes *= formatBytes( stream_.userFormat ); + } + + // No byte swapping necessary in DirectSound implementation. + + // Ahhh ... windoze. 16-bit data is signed but 8-bit data is + // unsigned. So, we need to convert our signed 8-bit data here to + // unsigned. + if ( stream_.deviceFormat[0] == RTAUDIO_SINT8 ) + for ( int i=0; idsBufferSize[0]; + nextWritePointer = handle->bufferPointer[0]; + + DWORD endWrite, leadPointer; + while ( true ) { + // Find out where the read and "safe write" pointers are. + result = dsBuffer->GetCurrentPosition( ¤tWritePointer, &safeWritePointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current write position!"; + errorText_ = errorStream_.str(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + + // We will copy our output buffer into the region between + // safeWritePointer and leadPointer. If leadPointer is not + // beyond the next endWrite position, wait until it is. + leadPointer = safeWritePointer + handle->dsPointerLeadTime[0]; + //std::cout << "safeWritePointer = " << safeWritePointer << ", leadPointer = " << leadPointer << ", nextWritePointer = " << nextWritePointer << std::endl; + if ( leadPointer > dsBufferSize ) leadPointer -= dsBufferSize; + if ( leadPointer < nextWritePointer ) leadPointer += dsBufferSize; // unwrap offset + endWrite = nextWritePointer + bufferBytes; + + // Check whether the entire write region is behind the play pointer. + if ( leadPointer >= endWrite ) break; + + // If we are here, then we must wait until the leadPointer advances + // beyond the end of our next write region. We use the + // Sleep() function to suspend operation until that happens. + double millis = ( endWrite - leadPointer ) * 1000.0; + millis /= ( formatBytes( stream_.deviceFormat[0]) * stream_.nDeviceChannels[0] * stream_.sampleRate); + if ( millis < 1.0 ) millis = 1.0; + Sleep( (DWORD) millis ); + } + + if ( dsPointerBetween( nextWritePointer, safeWritePointer, currentWritePointer, dsBufferSize ) + || dsPointerBetween( endWrite, safeWritePointer, currentWritePointer, dsBufferSize ) ) { + // We've strayed into the forbidden zone ... resync the read pointer. + handle->xrun[0] = true; + nextWritePointer = safeWritePointer + handle->dsPointerLeadTime[0] - bufferBytes; + if ( nextWritePointer >= dsBufferSize ) nextWritePointer -= dsBufferSize; + handle->bufferPointer[0] = nextWritePointer; + endWrite = nextWritePointer + bufferBytes; + } + + // Lock free space in the buffer + result = dsBuffer->Lock( nextWritePointer, bufferBytes, &buffer1, + &bufferSize1, &buffer2, &bufferSize2, 0 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") locking buffer during playback!"; + errorText_ = errorStream_.str(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + + // Copy our buffer into the DS buffer + CopyMemory( buffer1, buffer, bufferSize1 ); + if ( buffer2 != NULL ) CopyMemory( buffer2, buffer+bufferSize1, bufferSize2 ); + + // Update our buffer offset and unlock sound buffer + dsBuffer->Unlock( buffer1, bufferSize1, buffer2, bufferSize2 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") unlocking buffer during playback!"; + errorText_ = errorStream_.str(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + nextWritePointer = ( nextWritePointer + bufferSize1 + bufferSize2 ) % dsBufferSize; + handle->bufferPointer[0] = nextWritePointer; + } + + // Don't bother draining input + if ( handle->drainCounter ) { + handle->drainCounter++; + goto unlock; + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + + // Setup parameters. + if ( stream_.doConvertBuffer[1] ) { + buffer = stream_.deviceBuffer; + bufferBytes = stream_.bufferSize * stream_.nDeviceChannels[1]; + bufferBytes *= formatBytes( stream_.deviceFormat[1] ); + } + else { + buffer = stream_.userBuffer[1]; + bufferBytes = stream_.bufferSize * stream_.nUserChannels[1]; + bufferBytes *= formatBytes( stream_.userFormat ); + } + + LPDIRECTSOUNDCAPTUREBUFFER dsBuffer = (LPDIRECTSOUNDCAPTUREBUFFER) handle->buffer[1]; + long nextReadPointer = handle->bufferPointer[1]; + DWORD dsBufferSize = handle->dsBufferSize[1]; + + // Find out where the write and "safe read" pointers are. + result = dsBuffer->GetCurrentPosition( ¤tReadPointer, &safeReadPointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current read position!"; + errorText_ = errorStream_.str(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + + if ( safeReadPointer < (DWORD)nextReadPointer ) safeReadPointer += dsBufferSize; // unwrap offset + DWORD endRead = nextReadPointer + bufferBytes; + + // Handling depends on whether we are INPUT or DUPLEX. + // If we're in INPUT mode then waiting is a good thing. If we're in DUPLEX mode, + // then a wait here will drag the write pointers into the forbidden zone. + // + // In DUPLEX mode, rather than wait, we will back off the read pointer until + // it's in a safe position. This causes dropouts, but it seems to be the only + // practical way to sync up the read and write pointers reliably, given the + // the very complex relationship between phase and increment of the read and write + // pointers. + // + // In order to minimize audible dropouts in DUPLEX mode, we will + // provide a pre-roll period of 0.5 seconds in which we return + // zeros from the read buffer while the pointers sync up. + + if ( stream_.mode == DUPLEX ) { + if ( safeReadPointer < endRead ) { + if ( duplexPrerollBytes <= 0 ) { + // Pre-roll time over. Be more agressive. + int adjustment = endRead-safeReadPointer; + + handle->xrun[1] = true; + // Two cases: + // - large adjustments: we've probably run out of CPU cycles, so just resync exactly, + // and perform fine adjustments later. + // - small adjustments: back off by twice as much. + if ( adjustment >= 2*bufferBytes ) + nextReadPointer = safeReadPointer-2*bufferBytes; + else + nextReadPointer = safeReadPointer-bufferBytes-adjustment; + + if ( nextReadPointer < 0 ) nextReadPointer += dsBufferSize; + + } + else { + // In pre=roll time. Just do it. + nextReadPointer = safeReadPointer - bufferBytes; + while ( nextReadPointer < 0 ) nextReadPointer += dsBufferSize; + } + endRead = nextReadPointer + bufferBytes; + } + } + else { // mode == INPUT + while ( safeReadPointer < endRead && stream_.callbackInfo.isRunning ) { + // See comments for playback. + double millis = (endRead - safeReadPointer) * 1000.0; + millis /= ( formatBytes(stream_.deviceFormat[1]) * stream_.nDeviceChannels[1] * stream_.sampleRate); + if ( millis < 1.0 ) millis = 1.0; + Sleep( (DWORD) millis ); + + // Wake up and find out where we are now. + result = dsBuffer->GetCurrentPosition( ¤tReadPointer, &safeReadPointer ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") getting current read position!"; + errorText_ = errorStream_.str(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + + if ( safeReadPointer < (DWORD)nextReadPointer ) safeReadPointer += dsBufferSize; // unwrap offset + } + } + + // Lock free space in the buffer + result = dsBuffer->Lock( nextReadPointer, bufferBytes, &buffer1, + &bufferSize1, &buffer2, &bufferSize2, 0 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") locking capture buffer!"; + errorText_ = errorStream_.str(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + + if ( duplexPrerollBytes <= 0 ) { + // Copy our buffer into the DS buffer + CopyMemory( buffer, buffer1, bufferSize1 ); + if ( buffer2 != NULL ) CopyMemory( buffer+bufferSize1, buffer2, bufferSize2 ); + } + else { + memset( buffer, 0, bufferSize1 ); + if ( buffer2 != NULL ) memset( buffer + bufferSize1, 0, bufferSize2 ); + duplexPrerollBytes -= bufferSize1 + bufferSize2; + } + + // Update our buffer offset and unlock sound buffer + nextReadPointer = ( nextReadPointer + bufferSize1 + bufferSize2 ) % dsBufferSize; + dsBuffer->Unlock( buffer1, bufferSize1, buffer2, bufferSize2 ); + if ( FAILED( result ) ) { + errorStream_ << "RtApiDs::callbackEvent: error (" << getErrorString( result ) << ") unlocking capture buffer!"; + errorText_ = errorStream_.str(); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + handle->bufferPointer[1] = nextReadPointer; + + // No byte swapping necessary in DirectSound implementation. + + // If necessary, convert 8-bit data from unsigned to signed. + if ( stream_.deviceFormat[1] == RTAUDIO_SINT8 ) + for ( int j=0; jobject; + bool* isRunning = &info->isRunning; + + while ( *isRunning == true ) { + object->callbackEvent(); + } + + _endthreadex( 0 ); + return 0; +} + +#include "tchar.h" + +static std::string convertTChar( LPCTSTR name ) +{ +#if defined( UNICODE ) || defined( _UNICODE ) + int length = WideCharToMultiByte(CP_UTF8, 0, name, -1, NULL, 0, NULL, NULL); + std::string s( length-1, '\0' ); + WideCharToMultiByte(CP_UTF8, 0, name, -1, &s[0], length, NULL, NULL); +#else + std::string s( name ); +#endif + + return s; +} + +static BOOL CALLBACK deviceQueryCallback( LPGUID lpguid, + LPCTSTR description, + LPCTSTR /*module*/, + LPVOID lpContext ) +{ + struct DsProbeData& probeInfo = *(struct DsProbeData*) lpContext; + std::vector& dsDevices = *probeInfo.dsDevices; + + HRESULT hr; + bool validDevice = false; + if ( probeInfo.isInput == true ) { + DSCCAPS caps; + LPDIRECTSOUNDCAPTURE object; + + hr = DirectSoundCaptureCreate( lpguid, &object, NULL ); + if ( hr != DS_OK ) return TRUE; + + caps.dwSize = sizeof(caps); + hr = object->GetCaps( &caps ); + if ( hr == DS_OK ) { + if ( caps.dwChannels > 0 && caps.dwFormats > 0 ) + validDevice = true; + } + object->Release(); + } + else { + DSCAPS caps; + LPDIRECTSOUND object; + hr = DirectSoundCreate( lpguid, &object, NULL ); + if ( hr != DS_OK ) return TRUE; + + caps.dwSize = sizeof(caps); + hr = object->GetCaps( &caps ); + if ( hr == DS_OK ) { + if ( caps.dwFlags & DSCAPS_PRIMARYMONO || caps.dwFlags & DSCAPS_PRIMARYSTEREO ) + validDevice = true; + } + object->Release(); + } + + // If good device, then save its name and guid. + std::string name = convertTChar( description ); + //if ( name == "Primary Sound Driver" || name == "Primary Sound Capture Driver" ) + if ( lpguid == NULL ) + name = "Default Device"; + if ( validDevice ) { + for ( unsigned int i=0; i +#include + + // A structure to hold various information related to the ALSA API + // implementation. +struct AlsaHandle { + snd_pcm_t *handles[2]; + bool synchronized; + bool xrun[2]; + pthread_cond_t runnable_cv; + bool runnable; + + AlsaHandle() + :synchronized(false), runnable(false) { xrun[0] = false; xrun[1] = false; } +}; + +static void *alsaCallbackHandler( void * ptr ); + +RtApiAlsa :: RtApiAlsa() +{ + // Nothing to do here. +} + +RtApiAlsa :: ~RtApiAlsa() +{ + if ( stream_.state != STREAM_CLOSED ) closeStream(); +} + +unsigned int RtApiAlsa :: getDeviceCount( void ) +{ + unsigned nDevices = 0; + int result, subdevice, card; + char name[64]; + snd_ctl_t *handle; + + // Count cards and devices + card = -1; + snd_card_next( &card ); + while ( card >= 0 ) { + sprintf( name, "hw:%d", card ); + result = snd_ctl_open( &handle, name, 0 ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceCount: control open, card = " << card << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + goto nextcard; + } + subdevice = -1; + while( 1 ) { + result = snd_ctl_pcm_next_device( handle, &subdevice ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceCount: control next device, card = " << card << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + break; + } + if ( subdevice < 0 ) + break; + nDevices++; + } + nextcard: + snd_ctl_close( handle ); + snd_card_next( &card ); + } + + result = snd_ctl_open( &handle, "default", 0 ); + if (result == 0) { + nDevices++; + snd_ctl_close( handle ); + } + + return nDevices; +} + +RtAudio::DeviceInfo RtApiAlsa :: getDeviceInfo( unsigned int device ) +{ + RtAudio::DeviceInfo info; + info.probed = false; + + unsigned nDevices = 0; + int result, subdevice, card; + char name[64]; + snd_ctl_t *chandle; + + // Count cards and devices + card = -1; + snd_card_next( &card ); + while ( card >= 0 ) { + sprintf( name, "hw:%d", card ); + result = snd_ctl_open( &chandle, name, SND_CTL_NONBLOCK ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceInfo: control open, card = " << card << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + goto nextcard; + } + subdevice = -1; + while( 1 ) { + result = snd_ctl_pcm_next_device( chandle, &subdevice ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceInfo: control next device, card = " << card << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + break; + } + if ( subdevice < 0 ) break; + if ( nDevices == device ) { + sprintf( name, "hw:%d,%d", card, subdevice ); + goto foundDevice; + } + nDevices++; + } + nextcard: + snd_ctl_close( chandle ); + snd_card_next( &card ); + } + + result = snd_ctl_open( &chandle, "default", SND_CTL_NONBLOCK ); + if ( result == 0 ) { + if ( nDevices == device ) { + strcpy( name, "default" ); + goto foundDevice; + } + nDevices++; + } + + if ( nDevices == 0 ) { + errorText_ = "RtApiAlsa::getDeviceInfo: no devices found!"; + error( RtAudioError::INVALID_USE ); + return info; + } + + if ( device >= nDevices ) { + errorText_ = "RtApiAlsa::getDeviceInfo: device ID is invalid!"; + error( RtAudioError::INVALID_USE ); + return info; + } + + foundDevice: + + // If a stream is already open, we cannot probe the stream devices. + // Thus, use the saved results. + if ( stream_.state != STREAM_CLOSED && + ( stream_.device[0] == device || stream_.device[1] == device ) ) { + snd_ctl_close( chandle ); + if ( device >= devices_.size() ) { + errorText_ = "RtApiAlsa::getDeviceInfo: device ID was not present before stream was opened."; + error( RtAudioError::WARNING ); + return info; + } + return devices_[ device ]; + } + + int openMode = SND_PCM_ASYNC; + snd_pcm_stream_t stream; + snd_pcm_info_t *pcminfo; + snd_pcm_info_alloca( &pcminfo ); + snd_pcm_t *phandle; + snd_pcm_hw_params_t *params; + snd_pcm_hw_params_alloca( ¶ms ); + + // First try for playback unless default device (which has subdev -1) + stream = SND_PCM_STREAM_PLAYBACK; + snd_pcm_info_set_stream( pcminfo, stream ); + if ( subdevice != -1 ) { + snd_pcm_info_set_device( pcminfo, subdevice ); + snd_pcm_info_set_subdevice( pcminfo, 0 ); + + result = snd_ctl_pcm_info( chandle, pcminfo ); + if ( result < 0 ) { + // Device probably doesn't support playback. + goto captureProbe; + } + } + + result = snd_pcm_open( &phandle, name, stream, openMode | SND_PCM_NONBLOCK ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceInfo: snd_pcm_open error for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + goto captureProbe; + } + + // The device is open ... fill the parameter structure. + result = snd_pcm_hw_params_any( phandle, params ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::getDeviceInfo: snd_pcm_hw_params error for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + goto captureProbe; + } + + // Get output channel information. + unsigned int value; + result = snd_pcm_hw_params_get_channels_max( params, &value ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::getDeviceInfo: error getting device (" << name << ") output channels, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + goto captureProbe; + } + info.outputChannels = value; + snd_pcm_close( phandle ); + + captureProbe: + stream = SND_PCM_STREAM_CAPTURE; + snd_pcm_info_set_stream( pcminfo, stream ); + + // Now try for capture unless default device (with subdev = -1) + if ( subdevice != -1 ) { + result = snd_ctl_pcm_info( chandle, pcminfo ); + snd_ctl_close( chandle ); + if ( result < 0 ) { + // Device probably doesn't support capture. + if ( info.outputChannels == 0 ) return info; + goto probeParameters; + } + } + else + snd_ctl_close( chandle ); + + result = snd_pcm_open( &phandle, name, stream, openMode | SND_PCM_NONBLOCK); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceInfo: snd_pcm_open error for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + if ( info.outputChannels == 0 ) return info; + goto probeParameters; + } + + // The device is open ... fill the parameter structure. + result = snd_pcm_hw_params_any( phandle, params ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::getDeviceInfo: snd_pcm_hw_params error for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + if ( info.outputChannels == 0 ) return info; + goto probeParameters; + } + + result = snd_pcm_hw_params_get_channels_max( params, &value ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::getDeviceInfo: error getting device (" << name << ") input channels, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + if ( info.outputChannels == 0 ) return info; + goto probeParameters; + } + info.inputChannels = value; + snd_pcm_close( phandle ); + + // If device opens for both playback and capture, we determine the channels. + if ( info.outputChannels > 0 && info.inputChannels > 0 ) + info.duplexChannels = (info.outputChannels > info.inputChannels) ? info.inputChannels : info.outputChannels; + + // ALSA doesn't provide default devices so we'll use the first available one. + if ( device == 0 && info.outputChannels > 0 ) + info.isDefaultOutput = true; + if ( device == 0 && info.inputChannels > 0 ) + info.isDefaultInput = true; + + probeParameters: + // At this point, we just need to figure out the supported data + // formats and sample rates. We'll proceed by opening the device in + // the direction with the maximum number of channels, or playback if + // they are equal. This might limit our sample rate options, but so + // be it. + + if ( info.outputChannels >= info.inputChannels ) + stream = SND_PCM_STREAM_PLAYBACK; + else + stream = SND_PCM_STREAM_CAPTURE; + snd_pcm_info_set_stream( pcminfo, stream ); + + result = snd_pcm_open( &phandle, name, stream, openMode | SND_PCM_NONBLOCK); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::getDeviceInfo: snd_pcm_open error for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + // The device is open ... fill the parameter structure. + result = snd_pcm_hw_params_any( phandle, params ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::getDeviceInfo: snd_pcm_hw_params error for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + // Test our discrete set of sample rate values. + info.sampleRates.clear(); + for ( unsigned int i=0; i= 0 ) { + sprintf( name, "hw:%s,%d", cardname, subdevice ); + free( cardname ); + } + info.name = name; + + // That's all ... close the device and return + snd_pcm_close( phandle ); + info.probed = true; + return info; +} + +void RtApiAlsa :: saveDeviceInfo( void ) +{ + devices_.clear(); + + unsigned int nDevices = getDeviceCount(); + devices_.resize( nDevices ); + for ( unsigned int i=0; iflags & RTAUDIO_ALSA_USE_DEFAULT ) + snprintf(name, sizeof(name), "%s", "default"); + else { + // Count cards and devices + card = -1; + snd_card_next( &card ); + while ( card >= 0 ) { + sprintf( name, "hw:%d", card ); + result = snd_ctl_open( &chandle, name, SND_CTL_NONBLOCK ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::probeDeviceOpen: control open, card = " << card << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + subdevice = -1; + while( 1 ) { + result = snd_ctl_pcm_next_device( chandle, &subdevice ); + if ( result < 0 ) break; + if ( subdevice < 0 ) break; + if ( nDevices == device ) { + sprintf( name, "hw:%d,%d", card, subdevice ); + snd_ctl_close( chandle ); + goto foundDevice; + } + nDevices++; + } + snd_ctl_close( chandle ); + snd_card_next( &card ); + } + + result = snd_ctl_open( &chandle, "default", SND_CTL_NONBLOCK ); + if ( result == 0 ) { + if ( nDevices == device ) { + strcpy( name, "default" ); + goto foundDevice; + } + nDevices++; + } + + if ( nDevices == 0 ) { + // This should not happen because a check is made before this function is called. + errorText_ = "RtApiAlsa::probeDeviceOpen: no devices found!"; + return FAILURE; + } + + if ( device >= nDevices ) { + // This should not happen because a check is made before this function is called. + errorText_ = "RtApiAlsa::probeDeviceOpen: device ID is invalid!"; + return FAILURE; + } + } + + foundDevice: + + // The getDeviceInfo() function will not work for a device that is + // already open. Thus, we'll probe the system before opening a + // stream and save the results for use by getDeviceInfo(). + if ( mode == OUTPUT || ( mode == INPUT && stream_.mode != OUTPUT ) ) // only do once + this->saveDeviceInfo(); + + snd_pcm_stream_t stream; + if ( mode == OUTPUT ) + stream = SND_PCM_STREAM_PLAYBACK; + else + stream = SND_PCM_STREAM_CAPTURE; + + snd_pcm_t *phandle; + int openMode = SND_PCM_ASYNC; + result = snd_pcm_open( &phandle, name, stream, openMode ); + if ( result < 0 ) { + if ( mode == OUTPUT ) + errorStream_ << "RtApiAlsa::probeDeviceOpen: pcm device (" << name << ") won't open for output."; + else + errorStream_ << "RtApiAlsa::probeDeviceOpen: pcm device (" << name << ") won't open for input."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Fill the parameter structure. + snd_pcm_hw_params_t *hw_params; + snd_pcm_hw_params_alloca( &hw_params ); + result = snd_pcm_hw_params_any( phandle, hw_params ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error getting pcm device (" << name << ") parameters, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + +#if defined(__RTAUDIO_DEBUG__) + fprintf( stderr, "\nRtApiAlsa: dump hardware params just after device open:\n\n" ); + snd_pcm_hw_params_dump( hw_params, out ); +#endif + + // Set access ... check user preference. + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) { + stream_.userInterleaved = false; + result = snd_pcm_hw_params_set_access( phandle, hw_params, SND_PCM_ACCESS_RW_NONINTERLEAVED ); + if ( result < 0 ) { + result = snd_pcm_hw_params_set_access( phandle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED ); + stream_.deviceInterleaved[mode] = true; + } + else + stream_.deviceInterleaved[mode] = false; + } + else { + stream_.userInterleaved = true; + result = snd_pcm_hw_params_set_access( phandle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED ); + if ( result < 0 ) { + result = snd_pcm_hw_params_set_access( phandle, hw_params, SND_PCM_ACCESS_RW_NONINTERLEAVED ); + stream_.deviceInterleaved[mode] = false; + } + else + stream_.deviceInterleaved[mode] = true; + } + + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error setting pcm device (" << name << ") access, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Determine how to set the device format. + stream_.userFormat = format; + snd_pcm_format_t deviceFormat = SND_PCM_FORMAT_UNKNOWN; + + if ( format == RTAUDIO_SINT8 ) + deviceFormat = SND_PCM_FORMAT_S8; + else if ( format == RTAUDIO_SINT16 ) + deviceFormat = SND_PCM_FORMAT_S16; + else if ( format == RTAUDIO_SINT24 ) + deviceFormat = SND_PCM_FORMAT_S24; + else if ( format == RTAUDIO_SINT32 ) + deviceFormat = SND_PCM_FORMAT_S32; + else if ( format == RTAUDIO_FLOAT32 ) + deviceFormat = SND_PCM_FORMAT_FLOAT; + else if ( format == RTAUDIO_FLOAT64 ) + deviceFormat = SND_PCM_FORMAT_FLOAT64; + + if ( snd_pcm_hw_params_test_format(phandle, hw_params, deviceFormat) == 0) { + stream_.deviceFormat[mode] = format; + goto setFormat; + } + + // The user requested format is not natively supported by the device. + deviceFormat = SND_PCM_FORMAT_FLOAT64; + if ( snd_pcm_hw_params_test_format( phandle, hw_params, deviceFormat ) == 0 ) { + stream_.deviceFormat[mode] = RTAUDIO_FLOAT64; + goto setFormat; + } + + deviceFormat = SND_PCM_FORMAT_FLOAT; + if ( snd_pcm_hw_params_test_format(phandle, hw_params, deviceFormat ) == 0 ) { + stream_.deviceFormat[mode] = RTAUDIO_FLOAT32; + goto setFormat; + } + + deviceFormat = SND_PCM_FORMAT_S32; + if ( snd_pcm_hw_params_test_format(phandle, hw_params, deviceFormat ) == 0 ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + goto setFormat; + } + + deviceFormat = SND_PCM_FORMAT_S24; + if ( snd_pcm_hw_params_test_format(phandle, hw_params, deviceFormat ) == 0 ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT24; + goto setFormat; + } + + deviceFormat = SND_PCM_FORMAT_S16; + if ( snd_pcm_hw_params_test_format(phandle, hw_params, deviceFormat ) == 0 ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + goto setFormat; + } + + deviceFormat = SND_PCM_FORMAT_S8; + if ( snd_pcm_hw_params_test_format(phandle, hw_params, deviceFormat ) == 0 ) { + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + goto setFormat; + } + + // If we get here, no supported format was found. + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: pcm device " << device << " data format not supported by RtAudio."; + errorText_ = errorStream_.str(); + return FAILURE; + + setFormat: + result = snd_pcm_hw_params_set_format( phandle, hw_params, deviceFormat ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error setting pcm device (" << name << ") data format, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Determine whether byte-swaping is necessary. + stream_.doByteSwap[mode] = false; + if ( deviceFormat != SND_PCM_FORMAT_S8 ) { + result = snd_pcm_format_cpu_endian( deviceFormat ); + if ( result == 0 ) + stream_.doByteSwap[mode] = true; + else if (result < 0) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error getting pcm device (" << name << ") endian-ness, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + + // Set the sample rate. + result = snd_pcm_hw_params_set_rate_near( phandle, hw_params, (unsigned int*) &sampleRate, 0 ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error setting sample rate on device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Determine the number of channels for this device. We support a possible + // minimum device channel number > than the value requested by the user. + stream_.nUserChannels[mode] = channels; + unsigned int value; + result = snd_pcm_hw_params_get_channels_max( hw_params, &value ); + unsigned int deviceChannels = value; + if ( result < 0 || deviceChannels < channels + firstChannel ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: requested channel parameters not supported by device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + result = snd_pcm_hw_params_get_channels_min( hw_params, &value ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error getting minimum channels for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + deviceChannels = value; + if ( deviceChannels < channels + firstChannel ) deviceChannels = channels + firstChannel; + stream_.nDeviceChannels[mode] = deviceChannels; + + // Set the device channels. + result = snd_pcm_hw_params_set_channels( phandle, hw_params, deviceChannels ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error setting channels for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the buffer (or period) size. + int dir = 0; + snd_pcm_uframes_t periodSize = *bufferSize; + result = snd_pcm_hw_params_set_period_size_near( phandle, hw_params, &periodSize, &dir ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error setting period size for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + *bufferSize = periodSize; + + // Set the buffer number, which in ALSA is referred to as the "period". + unsigned int periods = 0; + if ( options && options->flags & RTAUDIO_MINIMIZE_LATENCY ) periods = 2; + if ( options && options->numberOfBuffers > 0 ) periods = options->numberOfBuffers; + if ( periods < 2 ) periods = 4; // a fairly safe default value + result = snd_pcm_hw_params_set_periods_near( phandle, hw_params, &periods, &dir ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error setting periods for device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // If attempting to setup a duplex stream, the bufferSize parameter + // MUST be the same in both directions! + if ( stream_.mode == OUTPUT && mode == INPUT && *bufferSize != stream_.bufferSize ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: system error setting buffer size for duplex stream on device (" << name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + stream_.bufferSize = *bufferSize; + + // Install the hardware configuration + result = snd_pcm_hw_params( phandle, hw_params ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error installing hardware configuration on device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + +#if defined(__RTAUDIO_DEBUG__) + fprintf(stderr, "\nRtApiAlsa: dump hardware params after installation:\n\n"); + snd_pcm_hw_params_dump( hw_params, out ); +#endif + + // Set the software configuration to fill buffers with zeros and prevent device stopping on xruns. + snd_pcm_sw_params_t *sw_params = NULL; + snd_pcm_sw_params_alloca( &sw_params ); + snd_pcm_sw_params_current( phandle, sw_params ); + snd_pcm_sw_params_set_start_threshold( phandle, sw_params, *bufferSize ); + snd_pcm_sw_params_set_stop_threshold( phandle, sw_params, ULONG_MAX ); + snd_pcm_sw_params_set_silence_threshold( phandle, sw_params, 0 ); + + // The following two settings were suggested by Theo Veenker + //snd_pcm_sw_params_set_avail_min( phandle, sw_params, *bufferSize ); + //snd_pcm_sw_params_set_xfer_align( phandle, sw_params, 1 ); + + // here are two options for a fix + //snd_pcm_sw_params_set_silence_size( phandle, sw_params, ULONG_MAX ); + snd_pcm_uframes_t val; + snd_pcm_sw_params_get_boundary( sw_params, &val ); + snd_pcm_sw_params_set_silence_size( phandle, sw_params, val ); + + result = snd_pcm_sw_params( phandle, sw_params ); + if ( result < 0 ) { + snd_pcm_close( phandle ); + errorStream_ << "RtApiAlsa::probeDeviceOpen: error installing software configuration on device (" << name << "), " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + return FAILURE; + } + +#if defined(__RTAUDIO_DEBUG__) + fprintf(stderr, "\nRtApiAlsa: dump software params after installation:\n\n"); + snd_pcm_sw_params_dump( sw_params, out ); +#endif + + // Set flags for buffer conversion + stream_.doConvertBuffer[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.nUserChannels[mode] < stream_.nDeviceChannels[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] && + stream_.nUserChannels[mode] > 1 ) + stream_.doConvertBuffer[mode] = true; + + // Allocate the ApiHandle if necessary and then save. + AlsaHandle *apiInfo = 0; + if ( stream_.apiHandle == 0 ) { + try { + apiInfo = (AlsaHandle *) new AlsaHandle; + } + catch ( std::bad_alloc& ) { + errorText_ = "RtApiAlsa::probeDeviceOpen: error allocating AlsaHandle memory."; + goto error; + } + + if ( pthread_cond_init( &apiInfo->runnable_cv, NULL ) ) { + errorText_ = "RtApiAlsa::probeDeviceOpen: error initializing pthread condition variable."; + goto error; + } + + stream_.apiHandle = (void *) apiInfo; + apiInfo->handles[0] = 0; + apiInfo->handles[1] = 0; + } + else { + apiInfo = (AlsaHandle *) stream_.apiHandle; + } + apiInfo->handles[mode] = phandle; + phandle = 0; + + // Allocate necessary internal buffers. + unsigned long bufferBytes; + bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiAlsa::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + + if ( stream_.doConvertBuffer[mode] ) { + + bool makeBuffer = true; + bufferBytes = stream_.nDeviceChannels[mode] * formatBytes( stream_.deviceFormat[mode] ); + if ( mode == INPUT ) { + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + if ( bufferBytes <= bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiAlsa::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + stream_.sampleRate = sampleRate; + stream_.nBuffers = periods; + stream_.device[mode] = device; + stream_.state = STREAM_STOPPED; + + // Setup the buffer conversion information structure. + if ( stream_.doConvertBuffer[mode] ) setConvertInfo( mode, firstChannel ); + + // Setup thread if necessary. + if ( stream_.mode == OUTPUT && mode == INPUT ) { + // We had already set up an output stream. + stream_.mode = DUPLEX; + // Link the streams if possible. + apiInfo->synchronized = false; + if ( snd_pcm_link( apiInfo->handles[0], apiInfo->handles[1] ) == 0 ) + apiInfo->synchronized = true; + else { + errorText_ = "RtApiAlsa::probeDeviceOpen: unable to synchronize input and output devices."; + error( RtAudioError::WARNING ); + } + } + else { + stream_.mode = mode; + + // Setup callback thread. + stream_.callbackInfo.object = (void *) this; + + // Set the thread attributes for joinable and realtime scheduling + // priority (optional). The higher priority will only take affect + // if the program is run as root or suid. Note, under Linux + // processes with CAP_SYS_NICE privilege, a user can change + // scheduling policy and priority (thus need not be root). See + // POSIX "capabilities". + pthread_attr_t attr; + pthread_attr_init( &attr ); + pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_JOINABLE ); + +#ifdef SCHED_RR // Undefined with some OSes (eg: NetBSD 1.6.x with GNU Pthread) + if ( options && options->flags & RTAUDIO_SCHEDULE_REALTIME ) { + // We previously attempted to increase the audio callback priority + // to SCHED_RR here via the attributes. However, while no errors + // were reported in doing so, it did not work. So, now this is + // done in the alsaCallbackHandler function. + stream_.callbackInfo.doRealtime = true; + int priority = options->priority; + int min = sched_get_priority_min( SCHED_RR ); + int max = sched_get_priority_max( SCHED_RR ); + if ( priority < min ) priority = min; + else if ( priority > max ) priority = max; + stream_.callbackInfo.priority = priority; + } +#endif + + stream_.callbackInfo.isRunning = true; + result = pthread_create( &stream_.callbackInfo.thread, &attr, alsaCallbackHandler, &stream_.callbackInfo ); + pthread_attr_destroy( &attr ); + if ( result ) { + stream_.callbackInfo.isRunning = false; + errorText_ = "RtApiAlsa::error creating callback thread!"; + goto error; + } + } + + return SUCCESS; + + error: + if ( apiInfo ) { + pthread_cond_destroy( &apiInfo->runnable_cv ); + if ( apiInfo->handles[0] ) snd_pcm_close( apiInfo->handles[0] ); + if ( apiInfo->handles[1] ) snd_pcm_close( apiInfo->handles[1] ); + delete apiInfo; + stream_.apiHandle = 0; + } + + if ( phandle) snd_pcm_close( phandle ); + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.state = STREAM_CLOSED; + return FAILURE; +} + +void RtApiAlsa :: closeStream() +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiAlsa::closeStream(): no open stream to close!"; + error( RtAudioError::WARNING ); + return; + } + + AlsaHandle *apiInfo = (AlsaHandle *) stream_.apiHandle; + stream_.callbackInfo.isRunning = false; + MUTEX_LOCK( &stream_.mutex ); + if ( stream_.state == STREAM_STOPPED ) { + apiInfo->runnable = true; + pthread_cond_signal( &apiInfo->runnable_cv ); + } + MUTEX_UNLOCK( &stream_.mutex ); + pthread_join( stream_.callbackInfo.thread, NULL ); + + if ( stream_.state == STREAM_RUNNING ) { + stream_.state = STREAM_STOPPED; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) + snd_pcm_drop( apiInfo->handles[0] ); + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) + snd_pcm_drop( apiInfo->handles[1] ); + } + + if ( apiInfo ) { + pthread_cond_destroy( &apiInfo->runnable_cv ); + if ( apiInfo->handles[0] ) snd_pcm_close( apiInfo->handles[0] ); + if ( apiInfo->handles[1] ) snd_pcm_close( apiInfo->handles[1] ); + delete apiInfo; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; +} + +void RtApiAlsa :: startStream() +{ + // This method calls snd_pcm_prepare if the device isn't already in that state. + + verifyStream(); + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiAlsa::startStream(): the stream is already running!"; + error( RtAudioError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + int result = 0; + snd_pcm_state_t state; + AlsaHandle *apiInfo = (AlsaHandle *) stream_.apiHandle; + snd_pcm_t **handle = (snd_pcm_t **) apiInfo->handles; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + state = snd_pcm_state( handle[0] ); + if ( state != SND_PCM_STATE_PREPARED ) { + result = snd_pcm_prepare( handle[0] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::startStream: error preparing output pcm device, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + } + + if ( ( stream_.mode == INPUT || stream_.mode == DUPLEX ) && !apiInfo->synchronized ) { + result = snd_pcm_drop(handle[1]); // fix to remove stale data received since device has been open + state = snd_pcm_state( handle[1] ); + if ( state != SND_PCM_STATE_PREPARED ) { + result = snd_pcm_prepare( handle[1] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::startStream: error preparing input pcm device, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + } + + stream_.state = STREAM_RUNNING; + + unlock: + apiInfo->runnable = true; + pthread_cond_signal( &apiInfo->runnable_cv ); + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result >= 0 ) return; + error( RtAudioError::SYSTEM_ERROR ); +} + +void RtApiAlsa :: stopStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiAlsa::stopStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + stream_.state = STREAM_STOPPED; + MUTEX_LOCK( &stream_.mutex ); + + int result = 0; + AlsaHandle *apiInfo = (AlsaHandle *) stream_.apiHandle; + snd_pcm_t **handle = (snd_pcm_t **) apiInfo->handles; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + if ( apiInfo->synchronized ) + result = snd_pcm_drop( handle[0] ); + else + result = snd_pcm_drain( handle[0] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::stopStream: error draining output pcm device, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + if ( ( stream_.mode == INPUT || stream_.mode == DUPLEX ) && !apiInfo->synchronized ) { + result = snd_pcm_drop( handle[1] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::stopStream: error stopping input pcm device, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + unlock: + apiInfo->runnable = false; // fixes high CPU usage when stopped + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result >= 0 ) return; + error( RtAudioError::SYSTEM_ERROR ); +} + +void RtApiAlsa :: abortStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiAlsa::abortStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + stream_.state = STREAM_STOPPED; + MUTEX_LOCK( &stream_.mutex ); + + int result = 0; + AlsaHandle *apiInfo = (AlsaHandle *) stream_.apiHandle; + snd_pcm_t **handle = (snd_pcm_t **) apiInfo->handles; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + result = snd_pcm_drop( handle[0] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::abortStream: error aborting output pcm device, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + if ( ( stream_.mode == INPUT || stream_.mode == DUPLEX ) && !apiInfo->synchronized ) { + result = snd_pcm_drop( handle[1] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::abortStream: error aborting input pcm device, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + unlock: + apiInfo->runnable = false; // fixes high CPU usage when stopped + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result >= 0 ) return; + error( RtAudioError::SYSTEM_ERROR ); +} + +void RtApiAlsa :: callbackEvent() +{ + AlsaHandle *apiInfo = (AlsaHandle *) stream_.apiHandle; + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_LOCK( &stream_.mutex ); + while ( !apiInfo->runnable ) + pthread_cond_wait( &apiInfo->runnable_cv, &stream_.mutex ); + + if ( stream_.state != STREAM_RUNNING ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + MUTEX_UNLOCK( &stream_.mutex ); + } + + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiAlsa::callbackEvent(): the stream is closed ... this shouldn't happen!"; + error( RtAudioError::WARNING ); + return; + } + + int doStopStream = 0; + RtAudioCallback callback = (RtAudioCallback) stream_.callbackInfo.callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && apiInfo->xrun[0] == true ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + apiInfo->xrun[0] = false; + } + if ( stream_.mode != OUTPUT && apiInfo->xrun[1] == true ) { + status |= RTAUDIO_INPUT_OVERFLOW; + apiInfo->xrun[1] = false; + } + doStopStream = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, stream_.callbackInfo.userData ); + + if ( doStopStream == 2 ) { + abortStream(); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + // The state might change while waiting on a mutex. + if ( stream_.state == STREAM_STOPPED ) goto unlock; + + int result; + char *buffer; + int channels; + snd_pcm_t **handle; + snd_pcm_sframes_t frames; + RtAudioFormat format; + handle = (snd_pcm_t **) apiInfo->handles; + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + + // Setup parameters. + if ( stream_.doConvertBuffer[1] ) { + buffer = stream_.deviceBuffer; + channels = stream_.nDeviceChannels[1]; + format = stream_.deviceFormat[1]; + } + else { + buffer = stream_.userBuffer[1]; + channels = stream_.nUserChannels[1]; + format = stream_.userFormat; + } + + // Read samples from device in interleaved/non-interleaved format. + if ( stream_.deviceInterleaved[1] ) + result = snd_pcm_readi( handle[1], buffer, stream_.bufferSize ); + else { + void *bufs[channels]; + size_t offset = stream_.bufferSize * formatBytes( format ); + for ( int i=0; ixrun[1] = true; + result = snd_pcm_prepare( handle[1] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::callbackEvent: error preparing device after overrun, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + } + } + else { + errorStream_ << "RtApiAlsa::callbackEvent: error, current state is " << snd_pcm_state_name( state ) << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + } + } + else { + errorStream_ << "RtApiAlsa::callbackEvent: audio read error, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + } + error( RtAudioError::WARNING ); + goto tryOutput; + } + + // Do byte swapping if necessary. + if ( stream_.doByteSwap[1] ) + byteSwapBuffer( buffer, stream_.bufferSize * channels, format ); + + // Do buffer conversion if necessary. + if ( stream_.doConvertBuffer[1] ) + convertBuffer( stream_.userBuffer[1], stream_.deviceBuffer, stream_.convertInfo[1] ); + + // Check stream latency + result = snd_pcm_delay( handle[1], &frames ); + if ( result == 0 && frames > 0 ) stream_.latency[1] = frames; + } + + tryOutput: + + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + // Setup parameters and do buffer conversion if necessary. + if ( stream_.doConvertBuffer[0] ) { + buffer = stream_.deviceBuffer; + convertBuffer( buffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + channels = stream_.nDeviceChannels[0]; + format = stream_.deviceFormat[0]; + } + else { + buffer = stream_.userBuffer[0]; + channels = stream_.nUserChannels[0]; + format = stream_.userFormat; + } + + // Do byte swapping if necessary. + if ( stream_.doByteSwap[0] ) + byteSwapBuffer(buffer, stream_.bufferSize * channels, format); + + // Write samples to device in interleaved/non-interleaved format. + if ( stream_.deviceInterleaved[0] ) + result = snd_pcm_writei( handle[0], buffer, stream_.bufferSize ); + else { + void *bufs[channels]; + size_t offset = stream_.bufferSize * formatBytes( format ); + for ( int i=0; ixrun[0] = true; + result = snd_pcm_prepare( handle[0] ); + if ( result < 0 ) { + errorStream_ << "RtApiAlsa::callbackEvent: error preparing device after underrun, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + } + } + else { + errorStream_ << "RtApiAlsa::callbackEvent: error, current state is " << snd_pcm_state_name( state ) << ", " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + } + } + else { + errorStream_ << "RtApiAlsa::callbackEvent: audio write error, " << snd_strerror( result ) << "."; + errorText_ = errorStream_.str(); + } + error( RtAudioError::WARNING ); + goto unlock; + } + + // Check stream latency + result = snd_pcm_delay( handle[0], &frames ); + if ( result == 0 && frames > 0 ) stream_.latency[0] = frames; + } + + unlock: + MUTEX_UNLOCK( &stream_.mutex ); + + RtApi::tickStreamTime(); + if ( doStopStream == 1 ) this->stopStream(); +} + +static void *alsaCallbackHandler( void *ptr ) +{ + CallbackInfo *info = (CallbackInfo *) ptr; + RtApiAlsa *object = (RtApiAlsa *) info->object; + bool *isRunning = &info->isRunning; + +#ifdef SCHED_RR // Undefined with some OSes (eg: NetBSD 1.6.x with GNU Pthread) + if ( &info->doRealtime ) { + pthread_t tID = pthread_self(); // ID of this thread + sched_param prio = { info->priority }; // scheduling priority of thread + pthread_setschedparam( tID, SCHED_RR, &prio ); + } +#endif + + while ( *isRunning == true ) { + pthread_testcancel(); + object->callbackEvent(); + } + + pthread_exit( NULL ); +} + +//******************** End of __LINUX_ALSA__ *********************// +#endif + +#if defined(__LINUX_PULSE__) + +// Code written by Peter Meerwald, pmeerw@pmeerw.net +// and Tristan Matthews. + +#include +#include +#include + +static const unsigned int SUPPORTED_SAMPLERATES[] = { 8000, 16000, 22050, 32000, + 44100, 48000, 96000, 0}; + +struct rtaudio_pa_format_mapping_t { + RtAudioFormat rtaudio_format; + pa_sample_format_t pa_format; +}; + +static const rtaudio_pa_format_mapping_t supported_sampleformats[] = { + {RTAUDIO_SINT16, PA_SAMPLE_S16LE}, + {RTAUDIO_SINT32, PA_SAMPLE_S32LE}, + {RTAUDIO_FLOAT32, PA_SAMPLE_FLOAT32LE}, + {0, PA_SAMPLE_INVALID}}; + +struct PulseAudioHandle { + pa_simple *s_play; + pa_simple *s_rec; + pthread_t thread; + pthread_cond_t runnable_cv; + bool runnable; + PulseAudioHandle() : s_play(0), s_rec(0), runnable(false) { } +}; + +RtApiPulse::~RtApiPulse() +{ + if ( stream_.state != STREAM_CLOSED ) + closeStream(); +} + +unsigned int RtApiPulse::getDeviceCount( void ) +{ + return 1; +} + +RtAudio::DeviceInfo RtApiPulse::getDeviceInfo( unsigned int /*device*/ ) +{ + RtAudio::DeviceInfo info; + info.probed = true; + info.name = "PulseAudio"; + info.outputChannels = 2; + info.inputChannels = 2; + info.duplexChannels = 2; + info.isDefaultOutput = true; + info.isDefaultInput = true; + + for ( const unsigned int *sr = SUPPORTED_SAMPLERATES; *sr; ++sr ) + info.sampleRates.push_back( *sr ); + + info.nativeFormats = RTAUDIO_SINT16 | RTAUDIO_SINT32 | RTAUDIO_FLOAT32; + + return info; +} + +static void *pulseaudio_callback( void * user ) +{ + CallbackInfo *cbi = static_cast( user ); + RtApiPulse *context = static_cast( cbi->object ); + volatile bool *isRunning = &cbi->isRunning; + + while ( *isRunning ) { + pthread_testcancel(); + context->callbackEvent(); + } + + pthread_exit( NULL ); +} + +void RtApiPulse::closeStream( void ) +{ + PulseAudioHandle *pah = static_cast( stream_.apiHandle ); + + stream_.callbackInfo.isRunning = false; + if ( pah ) { + MUTEX_LOCK( &stream_.mutex ); + if ( stream_.state == STREAM_STOPPED ) { + pah->runnable = true; + pthread_cond_signal( &pah->runnable_cv ); + } + MUTEX_UNLOCK( &stream_.mutex ); + + pthread_join( pah->thread, 0 ); + if ( pah->s_play ) { + pa_simple_flush( pah->s_play, NULL ); + pa_simple_free( pah->s_play ); + } + if ( pah->s_rec ) + pa_simple_free( pah->s_rec ); + + pthread_cond_destroy( &pah->runnable_cv ); + delete pah; + stream_.apiHandle = 0; + } + + if ( stream_.userBuffer[0] ) { + free( stream_.userBuffer[0] ); + stream_.userBuffer[0] = 0; + } + if ( stream_.userBuffer[1] ) { + free( stream_.userBuffer[1] ); + stream_.userBuffer[1] = 0; + } + + stream_.state = STREAM_CLOSED; + stream_.mode = UNINITIALIZED; +} + +void RtApiPulse::callbackEvent( void ) +{ + PulseAudioHandle *pah = static_cast( stream_.apiHandle ); + + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_LOCK( &stream_.mutex ); + while ( !pah->runnable ) + pthread_cond_wait( &pah->runnable_cv, &stream_.mutex ); + + if ( stream_.state != STREAM_RUNNING ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + MUTEX_UNLOCK( &stream_.mutex ); + } + + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiPulse::callbackEvent(): the stream is closed ... " + "this shouldn't happen!"; + error( RtAudioError::WARNING ); + return; + } + + RtAudioCallback callback = (RtAudioCallback) stream_.callbackInfo.callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + int doStopStream = callback( stream_.userBuffer[OUTPUT], stream_.userBuffer[INPUT], + stream_.bufferSize, streamTime, status, + stream_.callbackInfo.userData ); + + if ( doStopStream == 2 ) { + abortStream(); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + void *pulse_in = stream_.doConvertBuffer[INPUT] ? stream_.deviceBuffer : stream_.userBuffer[INPUT]; + void *pulse_out = stream_.doConvertBuffer[OUTPUT] ? stream_.deviceBuffer : stream_.userBuffer[OUTPUT]; + + if ( stream_.state != STREAM_RUNNING ) + goto unlock; + + int pa_error; + size_t bytes; + if (stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + if ( stream_.doConvertBuffer[OUTPUT] ) { + convertBuffer( stream_.deviceBuffer, + stream_.userBuffer[OUTPUT], + stream_.convertInfo[OUTPUT] ); + bytes = stream_.nDeviceChannels[OUTPUT] * stream_.bufferSize * + formatBytes( stream_.deviceFormat[OUTPUT] ); + } else + bytes = stream_.nUserChannels[OUTPUT] * stream_.bufferSize * + formatBytes( stream_.userFormat ); + + if ( pa_simple_write( pah->s_play, pulse_out, bytes, &pa_error ) < 0 ) { + errorStream_ << "RtApiPulse::callbackEvent: audio write error, " << + pa_strerror( pa_error ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + } + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX) { + if ( stream_.doConvertBuffer[INPUT] ) + bytes = stream_.nDeviceChannels[INPUT] * stream_.bufferSize * + formatBytes( stream_.deviceFormat[INPUT] ); + else + bytes = stream_.nUserChannels[INPUT] * stream_.bufferSize * + formatBytes( stream_.userFormat ); + + if ( pa_simple_read( pah->s_rec, pulse_in, bytes, &pa_error ) < 0 ) { + errorStream_ << "RtApiPulse::callbackEvent: audio read error, " << + pa_strerror( pa_error ) << "."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + } + if ( stream_.doConvertBuffer[INPUT] ) { + convertBuffer( stream_.userBuffer[INPUT], + stream_.deviceBuffer, + stream_.convertInfo[INPUT] ); + } + } + + unlock: + MUTEX_UNLOCK( &stream_.mutex ); + RtApi::tickStreamTime(); + + if ( doStopStream == 1 ) + stopStream(); +} + +void RtApiPulse::startStream( void ) +{ + PulseAudioHandle *pah = static_cast( stream_.apiHandle ); + + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiPulse::startStream(): the stream is not open!"; + error( RtAudioError::INVALID_USE ); + return; + } + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiPulse::startStream(): the stream is already running!"; + error( RtAudioError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + stream_.state = STREAM_RUNNING; + + pah->runnable = true; + pthread_cond_signal( &pah->runnable_cv ); + MUTEX_UNLOCK( &stream_.mutex ); +} + +void RtApiPulse::stopStream( void ) +{ + PulseAudioHandle *pah = static_cast( stream_.apiHandle ); + + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiPulse::stopStream(): the stream is not open!"; + error( RtAudioError::INVALID_USE ); + return; + } + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiPulse::stopStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + stream_.state = STREAM_STOPPED; + MUTEX_LOCK( &stream_.mutex ); + + if ( pah && pah->s_play ) { + int pa_error; + if ( pa_simple_drain( pah->s_play, &pa_error ) < 0 ) { + errorStream_ << "RtApiPulse::stopStream: error draining output device, " << + pa_strerror( pa_error ) << "."; + errorText_ = errorStream_.str(); + MUTEX_UNLOCK( &stream_.mutex ); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + } + + stream_.state = STREAM_STOPPED; + MUTEX_UNLOCK( &stream_.mutex ); +} + +void RtApiPulse::abortStream( void ) +{ + PulseAudioHandle *pah = static_cast( stream_.apiHandle ); + + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiPulse::abortStream(): the stream is not open!"; + error( RtAudioError::INVALID_USE ); + return; + } + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiPulse::abortStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + stream_.state = STREAM_STOPPED; + MUTEX_LOCK( &stream_.mutex ); + + if ( pah && pah->s_play ) { + int pa_error; + if ( pa_simple_flush( pah->s_play, &pa_error ) < 0 ) { + errorStream_ << "RtApiPulse::abortStream: error flushing output device, " << + pa_strerror( pa_error ) << "."; + errorText_ = errorStream_.str(); + MUTEX_UNLOCK( &stream_.mutex ); + error( RtAudioError::SYSTEM_ERROR ); + return; + } + } + + stream_.state = STREAM_STOPPED; + MUTEX_UNLOCK( &stream_.mutex ); +} + +bool RtApiPulse::probeDeviceOpen( unsigned int device, StreamMode mode, + unsigned int channels, unsigned int firstChannel, + unsigned int sampleRate, RtAudioFormat format, + unsigned int *bufferSize, RtAudio::StreamOptions *options ) +{ + PulseAudioHandle *pah = 0; + unsigned long bufferBytes = 0; + pa_sample_spec ss; + + if ( device != 0 ) return false; + if ( mode != INPUT && mode != OUTPUT ) return false; + if ( channels != 1 && channels != 2 ) { + errorText_ = "RtApiPulse::probeDeviceOpen: unsupported number of channels."; + return false; + } + ss.channels = channels; + + if ( firstChannel != 0 ) return false; + + bool sr_found = false; + for ( const unsigned int *sr = SUPPORTED_SAMPLERATES; *sr; ++sr ) { + if ( sampleRate == *sr ) { + sr_found = true; + stream_.sampleRate = sampleRate; + ss.rate = sampleRate; + break; + } + } + if ( !sr_found ) { + errorText_ = "RtApiPulse::probeDeviceOpen: unsupported sample rate."; + return false; + } + + bool sf_found = 0; + for ( const rtaudio_pa_format_mapping_t *sf = supported_sampleformats; + sf->rtaudio_format && sf->pa_format != PA_SAMPLE_INVALID; ++sf ) { + if ( format == sf->rtaudio_format ) { + sf_found = true; + stream_.userFormat = sf->rtaudio_format; + stream_.deviceFormat[mode] = stream_.userFormat; + ss.format = sf->pa_format; + break; + } + } + if ( !sf_found ) { // Use internal data format conversion. + stream_.userFormat = format; + stream_.deviceFormat[mode] = RTAUDIO_FLOAT32; + ss.format = PA_SAMPLE_FLOAT32LE; + } + + // Set other stream parameters. + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) stream_.userInterleaved = false; + else stream_.userInterleaved = true; + stream_.deviceInterleaved[mode] = true; + stream_.nBuffers = 1; + stream_.doByteSwap[mode] = false; + stream_.nUserChannels[mode] = channels; + stream_.nDeviceChannels[mode] = channels + firstChannel; + stream_.channelOffset[mode] = 0; + std::string streamName = "RtAudio"; + + // Set flags for buffer conversion. + stream_.doConvertBuffer[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.nUserChannels[mode] < stream_.nDeviceChannels[mode] ) + stream_.doConvertBuffer[mode] = true; + + // Allocate necessary internal buffers. + bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiPulse::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + stream_.bufferSize = *bufferSize; + + if ( stream_.doConvertBuffer[mode] ) { + + bool makeBuffer = true; + bufferBytes = stream_.nDeviceChannels[mode] * formatBytes( stream_.deviceFormat[mode] ); + if ( mode == INPUT ) { + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + if ( bufferBytes <= bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiPulse::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + stream_.device[mode] = device; + + // Setup the buffer conversion information structure. + if ( stream_.doConvertBuffer[mode] ) setConvertInfo( mode, firstChannel ); + + if ( !stream_.apiHandle ) { + PulseAudioHandle *pah = new PulseAudioHandle; + if ( !pah ) { + errorText_ = "RtApiPulse::probeDeviceOpen: error allocating memory for handle."; + goto error; + } + + stream_.apiHandle = pah; + if ( pthread_cond_init( &pah->runnable_cv, NULL ) != 0 ) { + errorText_ = "RtApiPulse::probeDeviceOpen: error creating condition variable."; + goto error; + } + } + pah = static_cast( stream_.apiHandle ); + + int error; + if ( !options->streamName.empty() ) streamName = options->streamName; + switch ( mode ) { + case INPUT: + pa_buffer_attr buffer_attr; + buffer_attr.fragsize = bufferBytes; + buffer_attr.maxlength = -1; + + pah->s_rec = pa_simple_new( NULL, streamName.c_str(), PA_STREAM_RECORD, NULL, "Record", &ss, NULL, &buffer_attr, &error ); + if ( !pah->s_rec ) { + errorText_ = "RtApiPulse::probeDeviceOpen: error connecting input to PulseAudio server."; + goto error; + } + break; + case OUTPUT: + pah->s_play = pa_simple_new( NULL, "RtAudio", PA_STREAM_PLAYBACK, NULL, "Playback", &ss, NULL, NULL, &error ); + if ( !pah->s_play ) { + errorText_ = "RtApiPulse::probeDeviceOpen: error connecting output to PulseAudio server."; + goto error; + } + break; + default: + goto error; + } + + if ( stream_.mode == UNINITIALIZED ) + stream_.mode = mode; + else if ( stream_.mode == mode ) + goto error; + else + stream_.mode = DUPLEX; + + if ( !stream_.callbackInfo.isRunning ) { + stream_.callbackInfo.object = this; + stream_.callbackInfo.isRunning = true; + if ( pthread_create( &pah->thread, NULL, pulseaudio_callback, (void *)&stream_.callbackInfo) != 0 ) { + errorText_ = "RtApiPulse::probeDeviceOpen: error creating thread."; + goto error; + } + } + + stream_.state = STREAM_STOPPED; + return true; + + error: + if ( pah && stream_.callbackInfo.isRunning ) { + pthread_cond_destroy( &pah->runnable_cv ); + delete pah; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + return FAILURE; +} + +//******************** End of __LINUX_PULSE__ *********************// +#endif + +#if defined(__LINUX_OSS__) + +#include +#include +#include +#include +#include +#include +#include + +static void *ossCallbackHandler(void * ptr); + +// A structure to hold various information related to the OSS API +// implementation. +struct OssHandle { + int id[2]; // device ids + bool xrun[2]; + bool triggered; + pthread_cond_t runnable; + + OssHandle() + :triggered(false) { id[0] = 0; id[1] = 0; xrun[0] = false; xrun[1] = false; } +}; + +RtApiOss :: RtApiOss() +{ + // Nothing to do here. +} + +RtApiOss :: ~RtApiOss() +{ + if ( stream_.state != STREAM_CLOSED ) closeStream(); +} + +unsigned int RtApiOss :: getDeviceCount( void ) +{ + int mixerfd = open( "/dev/mixer", O_RDWR, 0 ); + if ( mixerfd == -1 ) { + errorText_ = "RtApiOss::getDeviceCount: error opening '/dev/mixer'."; + error( RtAudioError::WARNING ); + return 0; + } + + oss_sysinfo sysinfo; + if ( ioctl( mixerfd, SNDCTL_SYSINFO, &sysinfo ) == -1 ) { + close( mixerfd ); + errorText_ = "RtApiOss::getDeviceCount: error getting sysinfo, OSS version >= 4.0 is required."; + error( RtAudioError::WARNING ); + return 0; + } + + close( mixerfd ); + return sysinfo.numaudios; +} + +RtAudio::DeviceInfo RtApiOss :: getDeviceInfo( unsigned int device ) +{ + RtAudio::DeviceInfo info; + info.probed = false; + + int mixerfd = open( "/dev/mixer", O_RDWR, 0 ); + if ( mixerfd == -1 ) { + errorText_ = "RtApiOss::getDeviceInfo: error opening '/dev/mixer'."; + error( RtAudioError::WARNING ); + return info; + } + + oss_sysinfo sysinfo; + int result = ioctl( mixerfd, SNDCTL_SYSINFO, &sysinfo ); + if ( result == -1 ) { + close( mixerfd ); + errorText_ = "RtApiOss::getDeviceInfo: error getting sysinfo, OSS version >= 4.0 is required."; + error( RtAudioError::WARNING ); + return info; + } + + unsigned nDevices = sysinfo.numaudios; + if ( nDevices == 0 ) { + close( mixerfd ); + errorText_ = "RtApiOss::getDeviceInfo: no devices found!"; + error( RtAudioError::INVALID_USE ); + return info; + } + + if ( device >= nDevices ) { + close( mixerfd ); + errorText_ = "RtApiOss::getDeviceInfo: device ID is invalid!"; + error( RtAudioError::INVALID_USE ); + return info; + } + + oss_audioinfo ainfo; + ainfo.dev = device; + result = ioctl( mixerfd, SNDCTL_AUDIOINFO, &ainfo ); + close( mixerfd ); + if ( result == -1 ) { + errorStream_ << "RtApiOss::getDeviceInfo: error getting device (" << ainfo.name << ") info."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + // Probe channels + if ( ainfo.caps & PCM_CAP_OUTPUT ) info.outputChannels = ainfo.max_channels; + if ( ainfo.caps & PCM_CAP_INPUT ) info.inputChannels = ainfo.max_channels; + if ( ainfo.caps & PCM_CAP_DUPLEX ) { + if ( info.outputChannels > 0 && info.inputChannels > 0 && ainfo.caps & PCM_CAP_DUPLEX ) + info.duplexChannels = (info.outputChannels > info.inputChannels) ? info.inputChannels : info.outputChannels; + } + + // Probe data formats ... do for input + unsigned long mask = ainfo.iformats; + if ( mask & AFMT_S16_LE || mask & AFMT_S16_BE ) + info.nativeFormats |= RTAUDIO_SINT16; + if ( mask & AFMT_S8 ) + info.nativeFormats |= RTAUDIO_SINT8; + if ( mask & AFMT_S32_LE || mask & AFMT_S32_BE ) + info.nativeFormats |= RTAUDIO_SINT32; + if ( mask & AFMT_FLOAT ) + info.nativeFormats |= RTAUDIO_FLOAT32; + if ( mask & AFMT_S24_LE || mask & AFMT_S24_BE ) + info.nativeFormats |= RTAUDIO_SINT24; + + // Check that we have at least one supported format + if ( info.nativeFormats == 0 ) { + errorStream_ << "RtApiOss::getDeviceInfo: device (" << ainfo.name << ") data format not supported by RtAudio."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + return info; + } + + // Probe the supported sample rates. + info.sampleRates.clear(); + if ( ainfo.nrates ) { + for ( unsigned int i=0; i= (int) SAMPLE_RATES[k] ) + info.sampleRates.push_back( SAMPLE_RATES[k] ); + } + } + + if ( info.sampleRates.size() == 0 ) { + errorStream_ << "RtApiOss::getDeviceInfo: no supported sample rates found for device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + error( RtAudioError::WARNING ); + } + else { + info.probed = true; + info.name = ainfo.name; + } + + return info; +} + + +bool RtApiOss :: probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ) +{ + int mixerfd = open( "/dev/mixer", O_RDWR, 0 ); + if ( mixerfd == -1 ) { + errorText_ = "RtApiOss::probeDeviceOpen: error opening '/dev/mixer'."; + return FAILURE; + } + + oss_sysinfo sysinfo; + int result = ioctl( mixerfd, SNDCTL_SYSINFO, &sysinfo ); + if ( result == -1 ) { + close( mixerfd ); + errorText_ = "RtApiOss::probeDeviceOpen: error getting sysinfo, OSS version >= 4.0 is required."; + return FAILURE; + } + + unsigned nDevices = sysinfo.numaudios; + if ( nDevices == 0 ) { + // This should not happen because a check is made before this function is called. + close( mixerfd ); + errorText_ = "RtApiOss::probeDeviceOpen: no devices found!"; + return FAILURE; + } + + if ( device >= nDevices ) { + // This should not happen because a check is made before this function is called. + close( mixerfd ); + errorText_ = "RtApiOss::probeDeviceOpen: device ID is invalid!"; + return FAILURE; + } + + oss_audioinfo ainfo; + ainfo.dev = device; + result = ioctl( mixerfd, SNDCTL_AUDIOINFO, &ainfo ); + close( mixerfd ); + if ( result == -1 ) { + errorStream_ << "RtApiOss::getDeviceInfo: error getting device (" << ainfo.name << ") info."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Check if device supports input or output + if ( ( mode == OUTPUT && !( ainfo.caps & PCM_CAP_OUTPUT ) ) || + ( mode == INPUT && !( ainfo.caps & PCM_CAP_INPUT ) ) ) { + if ( mode == OUTPUT ) + errorStream_ << "RtApiOss::probeDeviceOpen: device (" << ainfo.name << ") does not support output."; + else + errorStream_ << "RtApiOss::probeDeviceOpen: device (" << ainfo.name << ") does not support input."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + int flags = 0; + OssHandle *handle = (OssHandle *) stream_.apiHandle; + if ( mode == OUTPUT ) + flags |= O_WRONLY; + else { // mode == INPUT + if (stream_.mode == OUTPUT && stream_.device[0] == device) { + // We just set the same device for playback ... close and reopen for duplex (OSS only). + close( handle->id[0] ); + handle->id[0] = 0; + if ( !( ainfo.caps & PCM_CAP_DUPLEX ) ) { + errorStream_ << "RtApiOss::probeDeviceOpen: device (" << ainfo.name << ") does not support duplex mode."; + errorText_ = errorStream_.str(); + return FAILURE; + } + // Check that the number previously set channels is the same. + if ( stream_.nUserChannels[0] != channels ) { + errorStream_ << "RtApiOss::probeDeviceOpen: input/output channels must be equal for OSS duplex device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + flags |= O_RDWR; + } + else + flags |= O_RDONLY; + } + + // Set exclusive access if specified. + if ( options && options->flags & RTAUDIO_HOG_DEVICE ) flags |= O_EXCL; + + // Try to open the device. + int fd; + fd = open( ainfo.devnode, flags, 0 ); + if ( fd == -1 ) { + if ( errno == EBUSY ) + errorStream_ << "RtApiOss::probeDeviceOpen: device (" << ainfo.name << ") is busy."; + else + errorStream_ << "RtApiOss::probeDeviceOpen: error opening device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // For duplex operation, specifically set this mode (this doesn't seem to work). + /* + if ( flags | O_RDWR ) { + result = ioctl( fd, SNDCTL_DSP_SETDUPLEX, NULL ); + if ( result == -1) { + errorStream_ << "RtApiOss::probeDeviceOpen: error setting duplex mode for device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + } + */ + + // Check the device channel support. + stream_.nUserChannels[mode] = channels; + if ( ainfo.max_channels < (int)(channels + firstChannel) ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: the device (" << ainfo.name << ") does not support requested channel parameters."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the number of channels. + int deviceChannels = channels + firstChannel; + result = ioctl( fd, SNDCTL_DSP_CHANNELS, &deviceChannels ); + if ( result == -1 || deviceChannels < (int)(channels + firstChannel) ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: error setting channel parameters on device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + stream_.nDeviceChannels[mode] = deviceChannels; + + // Get the data format mask + int mask; + result = ioctl( fd, SNDCTL_DSP_GETFMTS, &mask ); + if ( result == -1 ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: error getting device (" << ainfo.name << ") data formats."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Determine how to set the device format. + stream_.userFormat = format; + int deviceFormat = -1; + stream_.doByteSwap[mode] = false; + if ( format == RTAUDIO_SINT8 ) { + if ( mask & AFMT_S8 ) { + deviceFormat = AFMT_S8; + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + } + } + else if ( format == RTAUDIO_SINT16 ) { + if ( mask & AFMT_S16_NE ) { + deviceFormat = AFMT_S16_NE; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + } + else if ( mask & AFMT_S16_OE ) { + deviceFormat = AFMT_S16_OE; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + stream_.doByteSwap[mode] = true; + } + } + else if ( format == RTAUDIO_SINT24 ) { + if ( mask & AFMT_S24_NE ) { + deviceFormat = AFMT_S24_NE; + stream_.deviceFormat[mode] = RTAUDIO_SINT24; + } + else if ( mask & AFMT_S24_OE ) { + deviceFormat = AFMT_S24_OE; + stream_.deviceFormat[mode] = RTAUDIO_SINT24; + stream_.doByteSwap[mode] = true; + } + } + else if ( format == RTAUDIO_SINT32 ) { + if ( mask & AFMT_S32_NE ) { + deviceFormat = AFMT_S32_NE; + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + } + else if ( mask & AFMT_S32_OE ) { + deviceFormat = AFMT_S32_OE; + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + stream_.doByteSwap[mode] = true; + } + } + + if ( deviceFormat == -1 ) { + // The user requested format is not natively supported by the device. + if ( mask & AFMT_S16_NE ) { + deviceFormat = AFMT_S16_NE; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + } + else if ( mask & AFMT_S32_NE ) { + deviceFormat = AFMT_S32_NE; + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + } + else if ( mask & AFMT_S24_NE ) { + deviceFormat = AFMT_S24_NE; + stream_.deviceFormat[mode] = RTAUDIO_SINT24; + } + else if ( mask & AFMT_S16_OE ) { + deviceFormat = AFMT_S16_OE; + stream_.deviceFormat[mode] = RTAUDIO_SINT16; + stream_.doByteSwap[mode] = true; + } + else if ( mask & AFMT_S32_OE ) { + deviceFormat = AFMT_S32_OE; + stream_.deviceFormat[mode] = RTAUDIO_SINT32; + stream_.doByteSwap[mode] = true; + } + else if ( mask & AFMT_S24_OE ) { + deviceFormat = AFMT_S24_OE; + stream_.deviceFormat[mode] = RTAUDIO_SINT24; + stream_.doByteSwap[mode] = true; + } + else if ( mask & AFMT_S8) { + deviceFormat = AFMT_S8; + stream_.deviceFormat[mode] = RTAUDIO_SINT8; + } + } + + if ( stream_.deviceFormat[mode] == 0 ) { + // This really shouldn't happen ... + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: device (" << ainfo.name << ") data format not supported by RtAudio."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Set the data format. + int temp = deviceFormat; + result = ioctl( fd, SNDCTL_DSP_SETFMT, &deviceFormat ); + if ( result == -1 || deviceFormat != temp ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: error setting data format on device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Attempt to set the buffer size. According to OSS, the minimum + // number of buffers is two. The supposed minimum buffer size is 16 + // bytes, so that will be our lower bound. The argument to this + // call is in the form 0xMMMMSSSS (hex), where the buffer size (in + // bytes) is given as 2^SSSS and the number of buffers as 2^MMMM. + // We'll check the actual value used near the end of the setup + // procedure. + int ossBufferBytes = *bufferSize * formatBytes( stream_.deviceFormat[mode] ) * deviceChannels; + if ( ossBufferBytes < 16 ) ossBufferBytes = 16; + int buffers = 0; + if ( options ) buffers = options->numberOfBuffers; + if ( options && options->flags & RTAUDIO_MINIMIZE_LATENCY ) buffers = 2; + if ( buffers < 2 ) buffers = 3; + temp = ((int) buffers << 16) + (int)( log10( (double)ossBufferBytes ) / log10( 2.0 ) ); + result = ioctl( fd, SNDCTL_DSP_SETFRAGMENT, &temp ); + if ( result == -1 ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: error setting buffer size on device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + stream_.nBuffers = buffers; + + // Save buffer size (in sample frames). + *bufferSize = ossBufferBytes / ( formatBytes(stream_.deviceFormat[mode]) * deviceChannels ); + stream_.bufferSize = *bufferSize; + + // Set the sample rate. + int srate = sampleRate; + result = ioctl( fd, SNDCTL_DSP_SPEED, &srate ); + if ( result == -1 ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: error setting sample rate (" << sampleRate << ") on device (" << ainfo.name << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + + // Verify the sample rate setup worked. + if ( abs( srate - sampleRate ) > 100 ) { + close( fd ); + errorStream_ << "RtApiOss::probeDeviceOpen: device (" << ainfo.name << ") does not support sample rate (" << sampleRate << ")."; + errorText_ = errorStream_.str(); + return FAILURE; + } + stream_.sampleRate = sampleRate; + + if ( mode == INPUT && stream_.mode == OUTPUT && stream_.device[0] == device) { + // We're doing duplex setup here. + stream_.deviceFormat[0] = stream_.deviceFormat[1]; + stream_.nDeviceChannels[0] = deviceChannels; + } + + // Set interleaving parameters. + stream_.userInterleaved = true; + stream_.deviceInterleaved[mode] = true; + if ( options && options->flags & RTAUDIO_NONINTERLEAVED ) + stream_.userInterleaved = false; + + // Set flags for buffer conversion + stream_.doConvertBuffer[mode] = false; + if ( stream_.userFormat != stream_.deviceFormat[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.nUserChannels[mode] < stream_.nDeviceChannels[mode] ) + stream_.doConvertBuffer[mode] = true; + if ( stream_.userInterleaved != stream_.deviceInterleaved[mode] && + stream_.nUserChannels[mode] > 1 ) + stream_.doConvertBuffer[mode] = true; + + // Allocate the stream handles if necessary and then save. + if ( stream_.apiHandle == 0 ) { + try { + handle = new OssHandle; + } + catch ( std::bad_alloc& ) { + errorText_ = "RtApiOss::probeDeviceOpen: error allocating OssHandle memory."; + goto error; + } + + if ( pthread_cond_init( &handle->runnable, NULL ) ) { + errorText_ = "RtApiOss::probeDeviceOpen: error initializing pthread condition variable."; + goto error; + } + + stream_.apiHandle = (void *) handle; + } + else { + handle = (OssHandle *) stream_.apiHandle; + } + handle->id[mode] = fd; + + // Allocate necessary internal buffers. + unsigned long bufferBytes; + bufferBytes = stream_.nUserChannels[mode] * *bufferSize * formatBytes( stream_.userFormat ); + stream_.userBuffer[mode] = (char *) calloc( bufferBytes, 1 ); + if ( stream_.userBuffer[mode] == NULL ) { + errorText_ = "RtApiOss::probeDeviceOpen: error allocating user buffer memory."; + goto error; + } + + if ( stream_.doConvertBuffer[mode] ) { + + bool makeBuffer = true; + bufferBytes = stream_.nDeviceChannels[mode] * formatBytes( stream_.deviceFormat[mode] ); + if ( mode == INPUT ) { + if ( stream_.mode == OUTPUT && stream_.deviceBuffer ) { + unsigned long bytesOut = stream_.nDeviceChannels[0] * formatBytes( stream_.deviceFormat[0] ); + if ( bufferBytes <= bytesOut ) makeBuffer = false; + } + } + + if ( makeBuffer ) { + bufferBytes *= *bufferSize; + if ( stream_.deviceBuffer ) free( stream_.deviceBuffer ); + stream_.deviceBuffer = (char *) calloc( bufferBytes, 1 ); + if ( stream_.deviceBuffer == NULL ) { + errorText_ = "RtApiOss::probeDeviceOpen: error allocating device buffer memory."; + goto error; + } + } + } + + stream_.device[mode] = device; + stream_.state = STREAM_STOPPED; + + // Setup the buffer conversion information structure. + if ( stream_.doConvertBuffer[mode] ) setConvertInfo( mode, firstChannel ); + + // Setup thread if necessary. + if ( stream_.mode == OUTPUT && mode == INPUT ) { + // We had already set up an output stream. + stream_.mode = DUPLEX; + if ( stream_.device[0] == device ) handle->id[0] = fd; + } + else { + stream_.mode = mode; + + // Setup callback thread. + stream_.callbackInfo.object = (void *) this; + + // Set the thread attributes for joinable and realtime scheduling + // priority. The higher priority will only take affect if the + // program is run as root or suid. + pthread_attr_t attr; + pthread_attr_init( &attr ); + pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_JOINABLE ); +#ifdef SCHED_RR // Undefined with some OSes (eg: NetBSD 1.6.x with GNU Pthread) + if ( options && options->flags & RTAUDIO_SCHEDULE_REALTIME ) { + struct sched_param param; + int priority = options->priority; + int min = sched_get_priority_min( SCHED_RR ); + int max = sched_get_priority_max( SCHED_RR ); + if ( priority < min ) priority = min; + else if ( priority > max ) priority = max; + param.sched_priority = priority; + pthread_attr_setschedparam( &attr, ¶m ); + pthread_attr_setschedpolicy( &attr, SCHED_RR ); + } + else + pthread_attr_setschedpolicy( &attr, SCHED_OTHER ); +#else + pthread_attr_setschedpolicy( &attr, SCHED_OTHER ); +#endif + + stream_.callbackInfo.isRunning = true; + result = pthread_create( &stream_.callbackInfo.thread, &attr, ossCallbackHandler, &stream_.callbackInfo ); + pthread_attr_destroy( &attr ); + if ( result ) { + stream_.callbackInfo.isRunning = false; + errorText_ = "RtApiOss::error creating callback thread!"; + goto error; + } + } + + return SUCCESS; + + error: + if ( handle ) { + pthread_cond_destroy( &handle->runnable ); + if ( handle->id[0] ) close( handle->id[0] ); + if ( handle->id[1] ) close( handle->id[1] ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + return FAILURE; +} + +void RtApiOss :: closeStream() +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiOss::closeStream(): no open stream to close!"; + error( RtAudioError::WARNING ); + return; + } + + OssHandle *handle = (OssHandle *) stream_.apiHandle; + stream_.callbackInfo.isRunning = false; + MUTEX_LOCK( &stream_.mutex ); + if ( stream_.state == STREAM_STOPPED ) + pthread_cond_signal( &handle->runnable ); + MUTEX_UNLOCK( &stream_.mutex ); + pthread_join( stream_.callbackInfo.thread, NULL ); + + if ( stream_.state == STREAM_RUNNING ) { + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) + ioctl( handle->id[0], SNDCTL_DSP_HALT, 0 ); + else + ioctl( handle->id[1], SNDCTL_DSP_HALT, 0 ); + stream_.state = STREAM_STOPPED; + } + + if ( handle ) { + pthread_cond_destroy( &handle->runnable ); + if ( handle->id[0] ) close( handle->id[0] ); + if ( handle->id[1] ) close( handle->id[1] ); + delete handle; + stream_.apiHandle = 0; + } + + for ( int i=0; i<2; i++ ) { + if ( stream_.userBuffer[i] ) { + free( stream_.userBuffer[i] ); + stream_.userBuffer[i] = 0; + } + } + + if ( stream_.deviceBuffer ) { + free( stream_.deviceBuffer ); + stream_.deviceBuffer = 0; + } + + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; +} + +void RtApiOss :: startStream() +{ + verifyStream(); + if ( stream_.state == STREAM_RUNNING ) { + errorText_ = "RtApiOss::startStream(): the stream is already running!"; + error( RtAudioError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + stream_.state = STREAM_RUNNING; + + // No need to do anything else here ... OSS automatically starts + // when fed samples. + + MUTEX_UNLOCK( &stream_.mutex ); + + OssHandle *handle = (OssHandle *) stream_.apiHandle; + pthread_cond_signal( &handle->runnable ); +} + +void RtApiOss :: stopStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiOss::stopStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + // The state might change while waiting on a mutex. + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + + int result = 0; + OssHandle *handle = (OssHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + // Flush the output with zeros a few times. + char *buffer; + int samples; + RtAudioFormat format; + + if ( stream_.doConvertBuffer[0] ) { + buffer = stream_.deviceBuffer; + samples = stream_.bufferSize * stream_.nDeviceChannels[0]; + format = stream_.deviceFormat[0]; + } + else { + buffer = stream_.userBuffer[0]; + samples = stream_.bufferSize * stream_.nUserChannels[0]; + format = stream_.userFormat; + } + + memset( buffer, 0, samples * formatBytes(format) ); + for ( unsigned int i=0; iid[0], buffer, samples * formatBytes(format) ); + if ( result == -1 ) { + errorText_ = "RtApiOss::stopStream: audio write error."; + error( RtAudioError::WARNING ); + } + } + + result = ioctl( handle->id[0], SNDCTL_DSP_HALT, 0 ); + if ( result == -1 ) { + errorStream_ << "RtApiOss::stopStream: system error stopping callback procedure on device (" << stream_.device[0] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + handle->triggered = false; + } + + if ( stream_.mode == INPUT || ( stream_.mode == DUPLEX && handle->id[0] != handle->id[1] ) ) { + result = ioctl( handle->id[1], SNDCTL_DSP_HALT, 0 ); + if ( result == -1 ) { + errorStream_ << "RtApiOss::stopStream: system error stopping input callback procedure on device (" << stream_.device[0] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + unlock: + stream_.state = STREAM_STOPPED; + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result != -1 ) return; + error( RtAudioError::SYSTEM_ERROR ); +} + +void RtApiOss :: abortStream() +{ + verifyStream(); + if ( stream_.state == STREAM_STOPPED ) { + errorText_ = "RtApiOss::abortStream(): the stream is already stopped!"; + error( RtAudioError::WARNING ); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + // The state might change while waiting on a mutex. + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + + int result = 0; + OssHandle *handle = (OssHandle *) stream_.apiHandle; + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + result = ioctl( handle->id[0], SNDCTL_DSP_HALT, 0 ); + if ( result == -1 ) { + errorStream_ << "RtApiOss::abortStream: system error stopping callback procedure on device (" << stream_.device[0] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + handle->triggered = false; + } + + if ( stream_.mode == INPUT || ( stream_.mode == DUPLEX && handle->id[0] != handle->id[1] ) ) { + result = ioctl( handle->id[1], SNDCTL_DSP_HALT, 0 ); + if ( result == -1 ) { + errorStream_ << "RtApiOss::abortStream: system error stopping input callback procedure on device (" << stream_.device[0] << ")."; + errorText_ = errorStream_.str(); + goto unlock; + } + } + + unlock: + stream_.state = STREAM_STOPPED; + MUTEX_UNLOCK( &stream_.mutex ); + + if ( result != -1 ) return; + error( RtAudioError::SYSTEM_ERROR ); +} + +void RtApiOss :: callbackEvent() +{ + OssHandle *handle = (OssHandle *) stream_.apiHandle; + if ( stream_.state == STREAM_STOPPED ) { + MUTEX_LOCK( &stream_.mutex ); + pthread_cond_wait( &handle->runnable, &stream_.mutex ); + if ( stream_.state != STREAM_RUNNING ) { + MUTEX_UNLOCK( &stream_.mutex ); + return; + } + MUTEX_UNLOCK( &stream_.mutex ); + } + + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApiOss::callbackEvent(): the stream is closed ... this shouldn't happen!"; + error( RtAudioError::WARNING ); + return; + } + + // Invoke user callback to get fresh output data. + int doStopStream = 0; + RtAudioCallback callback = (RtAudioCallback) stream_.callbackInfo.callback; + double streamTime = getStreamTime(); + RtAudioStreamStatus status = 0; + if ( stream_.mode != INPUT && handle->xrun[0] == true ) { + status |= RTAUDIO_OUTPUT_UNDERFLOW; + handle->xrun[0] = false; + } + if ( stream_.mode != OUTPUT && handle->xrun[1] == true ) { + status |= RTAUDIO_INPUT_OVERFLOW; + handle->xrun[1] = false; + } + doStopStream = callback( stream_.userBuffer[0], stream_.userBuffer[1], + stream_.bufferSize, streamTime, status, stream_.callbackInfo.userData ); + if ( doStopStream == 2 ) { + this->abortStream(); + return; + } + + MUTEX_LOCK( &stream_.mutex ); + + // The state might change while waiting on a mutex. + if ( stream_.state == STREAM_STOPPED ) goto unlock; + + int result; + char *buffer; + int samples; + RtAudioFormat format; + + if ( stream_.mode == OUTPUT || stream_.mode == DUPLEX ) { + + // Setup parameters and do buffer conversion if necessary. + if ( stream_.doConvertBuffer[0] ) { + buffer = stream_.deviceBuffer; + convertBuffer( buffer, stream_.userBuffer[0], stream_.convertInfo[0] ); + samples = stream_.bufferSize * stream_.nDeviceChannels[0]; + format = stream_.deviceFormat[0]; + } + else { + buffer = stream_.userBuffer[0]; + samples = stream_.bufferSize * stream_.nUserChannels[0]; + format = stream_.userFormat; + } + + // Do byte swapping if necessary. + if ( stream_.doByteSwap[0] ) + byteSwapBuffer( buffer, samples, format ); + + if ( stream_.mode == DUPLEX && handle->triggered == false ) { + int trig = 0; + ioctl( handle->id[0], SNDCTL_DSP_SETTRIGGER, &trig ); + result = write( handle->id[0], buffer, samples * formatBytes(format) ); + trig = PCM_ENABLE_INPUT|PCM_ENABLE_OUTPUT; + ioctl( handle->id[0], SNDCTL_DSP_SETTRIGGER, &trig ); + handle->triggered = true; + } + else + // Write samples to device. + result = write( handle->id[0], buffer, samples * formatBytes(format) ); + + if ( result == -1 ) { + // We'll assume this is an underrun, though there isn't a + // specific means for determining that. + handle->xrun[0] = true; + errorText_ = "RtApiOss::callbackEvent: audio write error."; + error( RtAudioError::WARNING ); + // Continue on to input section. + } + } + + if ( stream_.mode == INPUT || stream_.mode == DUPLEX ) { + + // Setup parameters. + if ( stream_.doConvertBuffer[1] ) { + buffer = stream_.deviceBuffer; + samples = stream_.bufferSize * stream_.nDeviceChannels[1]; + format = stream_.deviceFormat[1]; + } + else { + buffer = stream_.userBuffer[1]; + samples = stream_.bufferSize * stream_.nUserChannels[1]; + format = stream_.userFormat; + } + + // Read samples from device. + result = read( handle->id[1], buffer, samples * formatBytes(format) ); + + if ( result == -1 ) { + // We'll assume this is an overrun, though there isn't a + // specific means for determining that. + handle->xrun[1] = true; + errorText_ = "RtApiOss::callbackEvent: audio read error."; + error( RtAudioError::WARNING ); + goto unlock; + } + + // Do byte swapping if necessary. + if ( stream_.doByteSwap[1] ) + byteSwapBuffer( buffer, samples, format ); + + // Do buffer conversion if necessary. + if ( stream_.doConvertBuffer[1] ) + convertBuffer( stream_.userBuffer[1], stream_.deviceBuffer, stream_.convertInfo[1] ); + } + + unlock: + MUTEX_UNLOCK( &stream_.mutex ); + + RtApi::tickStreamTime(); + if ( doStopStream == 1 ) this->stopStream(); +} + +static void *ossCallbackHandler( void *ptr ) +{ + CallbackInfo *info = (CallbackInfo *) ptr; + RtApiOss *object = (RtApiOss *) info->object; + bool *isRunning = &info->isRunning; + + while ( *isRunning == true ) { + pthread_testcancel(); + object->callbackEvent(); + } + + pthread_exit( NULL ); +} + +//******************** End of __LINUX_OSS__ *********************// +#endif + + +// *************************************************** // +// +// Protected common (OS-independent) RtAudio methods. +// +// *************************************************** // + +// This method can be modified to control the behavior of error +// message printing. +void RtApi :: error( RtAudioError::Type type ) +{ + errorStream_.str(""); // clear the ostringstream + + RtAudioErrorCallback errorCallback = (RtAudioErrorCallback) stream_.callbackInfo.errorCallback; + if ( errorCallback ) { + // abortStream() can generate new error messages. Ignore them. Just keep original one. + + if ( firstErrorOccurred_ ) + return; + + firstErrorOccurred_ = true; + const std::string errorMessage = errorText_; + + if ( type != RtAudioError::WARNING && stream_.state != STREAM_STOPPED) { + stream_.callbackInfo.isRunning = false; // exit from the thread + abortStream(); + } + + errorCallback( type, errorMessage ); + firstErrorOccurred_ = false; + return; + } + + if ( type == RtAudioError::WARNING && showWarnings_ == true ) + std::cerr << '\n' << errorText_ << "\n\n"; + else if ( type != RtAudioError::WARNING ) + throw( RtAudioError( errorText_, type ) ); +} + +void RtApi :: verifyStream() +{ + if ( stream_.state == STREAM_CLOSED ) { + errorText_ = "RtApi:: a stream is not open!"; + error( RtAudioError::INVALID_USE ); + } +} + +void RtApi :: clearStreamInfo() +{ + stream_.mode = UNINITIALIZED; + stream_.state = STREAM_CLOSED; + stream_.sampleRate = 0; + stream_.bufferSize = 0; + stream_.nBuffers = 0; + stream_.userFormat = 0; + stream_.userInterleaved = true; + stream_.streamTime = 0.0; + stream_.apiHandle = 0; + stream_.deviceBuffer = 0; + stream_.callbackInfo.callback = 0; + stream_.callbackInfo.userData = 0; + stream_.callbackInfo.isRunning = false; + stream_.callbackInfo.errorCallback = 0; + for ( int i=0; i<2; i++ ) { + stream_.device[i] = 11111; + stream_.doConvertBuffer[i] = false; + stream_.deviceInterleaved[i] = true; + stream_.doByteSwap[i] = false; + stream_.nUserChannels[i] = 0; + stream_.nDeviceChannels[i] = 0; + stream_.channelOffset[i] = 0; + stream_.deviceFormat[i] = 0; + stream_.latency[i] = 0; + stream_.userBuffer[i] = 0; + stream_.convertInfo[i].channels = 0; + stream_.convertInfo[i].inJump = 0; + stream_.convertInfo[i].outJump = 0; + stream_.convertInfo[i].inFormat = 0; + stream_.convertInfo[i].outFormat = 0; + stream_.convertInfo[i].inOffset.clear(); + stream_.convertInfo[i].outOffset.clear(); + } +} + +unsigned int RtApi :: formatBytes( RtAudioFormat format ) +{ + if ( format == RTAUDIO_SINT16 ) + return 2; + else if ( format == RTAUDIO_SINT32 || format == RTAUDIO_FLOAT32 ) + return 4; + else if ( format == RTAUDIO_FLOAT64 ) + return 8; + else if ( format == RTAUDIO_SINT24 ) + return 3; + else if ( format == RTAUDIO_SINT8 ) + return 1; + + errorText_ = "RtApi::formatBytes: undefined format."; + error( RtAudioError::WARNING ); + + return 0; +} + +void RtApi :: setConvertInfo( StreamMode mode, unsigned int firstChannel ) +{ + if ( mode == INPUT ) { // convert device to user buffer + stream_.convertInfo[mode].inJump = stream_.nDeviceChannels[1]; + stream_.convertInfo[mode].outJump = stream_.nUserChannels[1]; + stream_.convertInfo[mode].inFormat = stream_.deviceFormat[1]; + stream_.convertInfo[mode].outFormat = stream_.userFormat; + } + else { // convert user to device buffer + stream_.convertInfo[mode].inJump = stream_.nUserChannels[0]; + stream_.convertInfo[mode].outJump = stream_.nDeviceChannels[0]; + stream_.convertInfo[mode].inFormat = stream_.userFormat; + stream_.convertInfo[mode].outFormat = stream_.deviceFormat[0]; + } + + if ( stream_.convertInfo[mode].inJump < stream_.convertInfo[mode].outJump ) + stream_.convertInfo[mode].channels = stream_.convertInfo[mode].inJump; + else + stream_.convertInfo[mode].channels = stream_.convertInfo[mode].outJump; + + // Set up the interleave/deinterleave offsets. + if ( stream_.deviceInterleaved[mode] != stream_.userInterleaved ) { + if ( ( mode == OUTPUT && stream_.deviceInterleaved[mode] ) || + ( mode == INPUT && stream_.userInterleaved ) ) { + for ( int k=0; k 0 ) { + if ( stream_.deviceInterleaved[mode] ) { + if ( mode == OUTPUT ) { + for ( int k=0; k> 8); + //out[info.outOffset[j]] >>= 8; + } + in += info.inJump; + out += info.outJump; + } + } + else if (info.inFormat == RTAUDIO_FLOAT32) { + Float32 *in = (Float32 *)inBuffer; + for (unsigned int i=0; i> 8); + } + in += info.inJump; + out += info.outJump; + } + } + else if (info.inFormat == RTAUDIO_SINT32) { + Int32 *in = (Int32 *)inBuffer; + for (unsigned int i=0; i> 16) & 0x0000ffff); + } + in += info.inJump; + out += info.outJump; + } + } + else if (info.inFormat == RTAUDIO_FLOAT32) { + Float32 *in = (Float32 *)inBuffer; + for (unsigned int i=0; i> 8) & 0x00ff); + } + in += info.inJump; + out += info.outJump; + } + } + else if (info.inFormat == RTAUDIO_SINT24) { + Int24 *in = (Int24 *)inBuffer; + for (unsigned int i=0; i> 16); + } + in += info.inJump; + out += info.outJump; + } + } + else if (info.inFormat == RTAUDIO_SINT32) { + Int32 *in = (Int32 *)inBuffer; + for (unsigned int i=0; i> 24) & 0x000000ff); + } + in += info.inJump; + out += info.outJump; + } + } + else if (info.inFormat == RTAUDIO_FLOAT32) { + Float32 *in = (Float32 *)inBuffer; + for (unsigned int i=0; i>8) | (x<<8); } +//static inline uint32_t bswap_32(uint32_t x) { return (bswap_16(x&0xffff)<<16) | (bswap_16(x>>16)); } +//static inline uint64_t bswap_64(uint64_t x) { return (((unsigned long long)bswap_32(x&0xffffffffull))<<32) | (bswap_32(x>>32)); } + +void RtApi :: byteSwapBuffer( char *buffer, unsigned int samples, RtAudioFormat format ) +{ + register char val; + register char *ptr; + + ptr = buffer; + if ( format == RTAUDIO_SINT16 ) { + for ( unsigned int i=0; i +#include +#include +#include + +/*! \typedef typedef unsigned long RtAudioFormat; + \brief RtAudio data format type. + + Support for signed integers and floats. Audio data fed to/from an + RtAudio stream is assumed to ALWAYS be in host byte order. The + internal routines will automatically take care of any necessary + byte-swapping between the host format and the soundcard. Thus, + endian-ness is not a concern in the following format definitions. + + - \e RTAUDIO_SINT8: 8-bit signed integer. + - \e RTAUDIO_SINT16: 16-bit signed integer. + - \e RTAUDIO_SINT24: 24-bit signed integer. + - \e RTAUDIO_SINT32: 32-bit signed integer. + - \e RTAUDIO_FLOAT32: Normalized between plus/minus 1.0. + - \e RTAUDIO_FLOAT64: Normalized between plus/minus 1.0. +*/ +typedef unsigned long RtAudioFormat; +static const RtAudioFormat RTAUDIO_SINT8 = 0x1; // 8-bit signed integer. +static const RtAudioFormat RTAUDIO_SINT16 = 0x2; // 16-bit signed integer. +static const RtAudioFormat RTAUDIO_SINT24 = 0x4; // 24-bit signed integer. +static const RtAudioFormat RTAUDIO_SINT32 = 0x8; // 32-bit signed integer. +static const RtAudioFormat RTAUDIO_FLOAT32 = 0x10; // Normalized between plus/minus 1.0. +static const RtAudioFormat RTAUDIO_FLOAT64 = 0x20; // Normalized between plus/minus 1.0. + +/*! \typedef typedef unsigned long RtAudioStreamFlags; + \brief RtAudio stream option flags. + + The following flags can be OR'ed together to allow a client to + make changes to the default stream behavior: + + - \e RTAUDIO_NONINTERLEAVED: Use non-interleaved buffers (default = interleaved). + - \e RTAUDIO_MINIMIZE_LATENCY: Attempt to set stream parameters for lowest possible latency. + - \e RTAUDIO_HOG_DEVICE: Attempt grab device for exclusive use. + - \e RTAUDIO_ALSA_USE_DEFAULT: Use the "default" PCM device (ALSA only). + + By default, RtAudio streams pass and receive audio data from the + client in an interleaved format. By passing the + RTAUDIO_NONINTERLEAVED flag to the openStream() function, audio + data will instead be presented in non-interleaved buffers. In + this case, each buffer argument in the RtAudioCallback function + will point to a single array of data, with \c nFrames samples for + each channel concatenated back-to-back. For example, the first + sample of data for the second channel would be located at index \c + nFrames (assuming the \c buffer pointer was recast to the correct + data type for the stream). + + Certain audio APIs offer a number of parameters that influence the + I/O latency of a stream. By default, RtAudio will attempt to set + these parameters internally for robust (glitch-free) performance + (though some APIs, like Windows Direct Sound, make this difficult). + By passing the RTAUDIO_MINIMIZE_LATENCY flag to the openStream() + function, internal stream settings will be influenced in an attempt + to minimize stream latency, though possibly at the expense of stream + performance. + + If the RTAUDIO_HOG_DEVICE flag is set, RtAudio will attempt to + open the input and/or output stream device(s) for exclusive use. + Note that this is not possible with all supported audio APIs. + + If the RTAUDIO_SCHEDULE_REALTIME flag is set, RtAudio will attempt + to select realtime scheduling (round-robin) for the callback thread. + + If the RTAUDIO_ALSA_USE_DEFAULT flag is set, RtAudio will attempt to + open the "default" PCM device when using the ALSA API. Note that this + will override any specified input or output device id. +*/ +typedef unsigned int RtAudioStreamFlags; +static const RtAudioStreamFlags RTAUDIO_NONINTERLEAVED = 0x1; // Use non-interleaved buffers (default = interleaved). +static const RtAudioStreamFlags RTAUDIO_MINIMIZE_LATENCY = 0x2; // Attempt to set stream parameters for lowest possible latency. +static const RtAudioStreamFlags RTAUDIO_HOG_DEVICE = 0x4; // Attempt grab device and prevent use by others. +static const RtAudioStreamFlags RTAUDIO_SCHEDULE_REALTIME = 0x8; // Try to select realtime scheduling for callback thread. +static const RtAudioStreamFlags RTAUDIO_ALSA_USE_DEFAULT = 0x10; // Use the "default" PCM device (ALSA only). + +/*! \typedef typedef unsigned long RtAudioStreamStatus; + \brief RtAudio stream status (over- or underflow) flags. + + Notification of a stream over- or underflow is indicated by a + non-zero stream \c status argument in the RtAudioCallback function. + The stream status can be one of the following two options, + depending on whether the stream is open for output and/or input: + + - \e RTAUDIO_INPUT_OVERFLOW: Input data was discarded because of an overflow condition at the driver. + - \e RTAUDIO_OUTPUT_UNDERFLOW: The output buffer ran low, likely producing a break in the output sound. +*/ +typedef unsigned int RtAudioStreamStatus; +static const RtAudioStreamStatus RTAUDIO_INPUT_OVERFLOW = 0x1; // Input data was discarded because of an overflow condition at the driver. +static const RtAudioStreamStatus RTAUDIO_OUTPUT_UNDERFLOW = 0x2; // The output buffer ran low, likely causing a gap in the output sound. + +//! RtAudio callback function prototype. +/*! + All RtAudio clients must create a function of type RtAudioCallback + to read and/or write data from/to the audio stream. When the + underlying audio system is ready for new input or output data, this + function will be invoked. + + \param outputBuffer For output (or duplex) streams, the client + should write \c nFrames of audio sample frames into this + buffer. This argument should be recast to the datatype + specified when the stream was opened. For input-only + streams, this argument will be NULL. + + \param inputBuffer For input (or duplex) streams, this buffer will + hold \c nFrames of input audio sample frames. This + argument should be recast to the datatype specified when the + stream was opened. For output-only streams, this argument + will be NULL. + + \param nFrames The number of sample frames of input or output + data in the buffers. The actual buffer size in bytes is + dependent on the data type and number of channels in use. + + \param streamTime The number of seconds that have elapsed since the + stream was started. + + \param status If non-zero, this argument indicates a data overflow + or underflow condition for the stream. The particular + condition can be determined by comparison with the + RtAudioStreamStatus flags. + + \param userData A pointer to optional data provided by the client + when opening the stream (default = NULL). + + To continue normal stream operation, the RtAudioCallback function + should return a value of zero. To stop the stream and drain the + output buffer, the function should return a value of one. To abort + the stream immediately, the client should return a value of two. + */ +typedef int (*RtAudioCallback)( void *outputBuffer, void *inputBuffer, + unsigned int nFrames, + double streamTime, + RtAudioStreamStatus status, + void *userData ); + +/************************************************************************/ +/*! \class RtAudioError + \brief Exception handling class for RtAudio. + + The RtAudioError class is quite simple but it does allow errors to be + "caught" by RtAudioError::Type. See the RtAudio documentation to know + which methods can throw an RtAudioError. +*/ +/************************************************************************/ + +class RtAudioError : public std::exception +{ + public: + //! Defined RtAudioError types. + enum Type { + WARNING, /*!< A non-critical error. */ + DEBUG_WARNING, /*!< A non-critical error which might be useful for debugging. */ + UNSPECIFIED, /*!< The default, unspecified error type. */ + NO_DEVICES_FOUND, /*!< No devices found on system. */ + INVALID_DEVICE, /*!< An invalid device ID was specified. */ + MEMORY_ERROR, /*!< An error occured during memory allocation. */ + INVALID_PARAMETER, /*!< An invalid parameter was specified to a function. */ + INVALID_USE, /*!< The function was called incorrectly. */ + DRIVER_ERROR, /*!< A system driver error occured. */ + SYSTEM_ERROR, /*!< A system error occured. */ + THREAD_ERROR /*!< A thread error occured. */ + }; + + //! The constructor. + RtAudioError( const std::string& message, Type type = RtAudioError::UNSPECIFIED ) throw() : message_(message), type_(type) {} + + //! The destructor. + virtual ~RtAudioError( void ) throw() {} + + //! Prints thrown error message to stderr. + virtual void printMessage( void ) const throw() { std::cerr << '\n' << message_ << "\n\n"; } + + //! Returns the thrown error message type. + virtual const Type& getType(void) const throw() { return type_; } + + //! Returns the thrown error message string. + virtual const std::string& getMessage(void) const throw() { return message_; } + + //! Returns the thrown error message as a c-style string. + virtual const char* what( void ) const throw() { return message_.c_str(); } + + protected: + std::string message_; + Type type_; +}; + +//! RtAudio error callback function prototype. +/*! + \param type Type of error. + \param errorText Error description. + */ +typedef void (*RtAudioErrorCallback)( RtAudioError::Type type, const std::string &errorText ); + +// **************************************************************** // +// +// RtAudio class declaration. +// +// RtAudio is a "controller" used to select an available audio i/o +// interface. It presents a common API for the user to call but all +// functionality is implemented by the class RtApi and its +// subclasses. RtAudio creates an instance of an RtApi subclass +// based on the user's API choice. If no choice is made, RtAudio +// attempts to make a "logical" API selection. +// +// **************************************************************** // + +class RtApi; + +class RtAudio +{ + public: + + //! Audio API specifier arguments. + enum Api { + UNSPECIFIED, /*!< Search for a working compiled API. */ + LINUX_ALSA, /*!< The Advanced Linux Sound Architecture API. */ + LINUX_PULSE, /*!< The Linux PulseAudio API. */ + LINUX_OSS, /*!< The Linux Open Sound System API. */ + UNIX_JACK, /*!< The Jack Low-Latency Audio Server API. */ + MACOSX_CORE, /*!< Macintosh OS-X Core Audio API. */ + WINDOWS_WASAPI, /*!< The Microsoft WASAPI API. */ + WINDOWS_ASIO, /*!< The Steinberg Audio Stream I/O API. */ + WINDOWS_DS, /*!< The Microsoft Direct Sound API. */ + RTAUDIO_DUMMY /*!< A compilable but non-functional API. */ + }; + + //! The public device information structure for returning queried values. + struct DeviceInfo { + bool probed; /*!< true if the device capabilities were successfully probed. */ + std::string name; /*!< Character string device identifier. */ + unsigned int outputChannels; /*!< Maximum output channels supported by device. */ + unsigned int inputChannels; /*!< Maximum input channels supported by device. */ + unsigned int duplexChannels; /*!< Maximum simultaneous input/output channels supported by device. */ + bool isDefaultOutput; /*!< true if this is the default output device. */ + bool isDefaultInput; /*!< true if this is the default input device. */ + std::vector sampleRates; /*!< Supported sample rates (queried from list of standard rates). */ + RtAudioFormat nativeFormats; /*!< Bit mask of supported data formats. */ + + // Default constructor. + DeviceInfo() + :probed(false), outputChannels(0), inputChannels(0), duplexChannels(0), + isDefaultOutput(false), isDefaultInput(false), nativeFormats(0) {} + }; + + //! The structure for specifying input or ouput stream parameters. + struct StreamParameters { + unsigned int deviceId; /*!< Device index (0 to getDeviceCount() - 1). */ + unsigned int nChannels; /*!< Number of channels. */ + unsigned int firstChannel; /*!< First channel index on device (default = 0). */ + + // Default constructor. + StreamParameters() + : deviceId(0), nChannels(0), firstChannel(0) {} + }; + + //! The structure for specifying stream options. + /*! + The following flags can be OR'ed together to allow a client to + make changes to the default stream behavior: + + - \e RTAUDIO_NONINTERLEAVED: Use non-interleaved buffers (default = interleaved). + - \e RTAUDIO_MINIMIZE_LATENCY: Attempt to set stream parameters for lowest possible latency. + - \e RTAUDIO_HOG_DEVICE: Attempt grab device for exclusive use. + - \e RTAUDIO_SCHEDULE_REALTIME: Attempt to select realtime scheduling for callback thread. + - \e RTAUDIO_ALSA_USE_DEFAULT: Use the "default" PCM device (ALSA only). + + By default, RtAudio streams pass and receive audio data from the + client in an interleaved format. By passing the + RTAUDIO_NONINTERLEAVED flag to the openStream() function, audio + data will instead be presented in non-interleaved buffers. In + this case, each buffer argument in the RtAudioCallback function + will point to a single array of data, with \c nFrames samples for + each channel concatenated back-to-back. For example, the first + sample of data for the second channel would be located at index \c + nFrames (assuming the \c buffer pointer was recast to the correct + data type for the stream). + + Certain audio APIs offer a number of parameters that influence the + I/O latency of a stream. By default, RtAudio will attempt to set + these parameters internally for robust (glitch-free) performance + (though some APIs, like Windows Direct Sound, make this difficult). + By passing the RTAUDIO_MINIMIZE_LATENCY flag to the openStream() + function, internal stream settings will be influenced in an attempt + to minimize stream latency, though possibly at the expense of stream + performance. + + If the RTAUDIO_HOG_DEVICE flag is set, RtAudio will attempt to + open the input and/or output stream device(s) for exclusive use. + Note that this is not possible with all supported audio APIs. + + If the RTAUDIO_SCHEDULE_REALTIME flag is set, RtAudio will attempt + to select realtime scheduling (round-robin) for the callback thread. + The \c priority parameter will only be used if the RTAUDIO_SCHEDULE_REALTIME + flag is set. It defines the thread's realtime priority. + + If the RTAUDIO_ALSA_USE_DEFAULT flag is set, RtAudio will attempt to + open the "default" PCM device when using the ALSA API. Note that this + will override any specified input or output device id. + + The \c numberOfBuffers parameter can be used to control stream + latency in the Windows DirectSound, Linux OSS, and Linux Alsa APIs + only. A value of two is usually the smallest allowed. Larger + numbers can potentially result in more robust stream performance, + though likely at the cost of stream latency. The value set by the + user is replaced during execution of the RtAudio::openStream() + function by the value actually used by the system. + + The \c streamName parameter can be used to set the client name + when using the Jack API. By default, the client name is set to + RtApiJack. However, if you wish to create multiple instances of + RtAudio with Jack, each instance must have a unique client name. + */ + struct StreamOptions { + RtAudioStreamFlags flags; /*!< A bit-mask of stream flags (RTAUDIO_NONINTERLEAVED, RTAUDIO_MINIMIZE_LATENCY, RTAUDIO_HOG_DEVICE, RTAUDIO_ALSA_USE_DEFAULT). */ + unsigned int numberOfBuffers; /*!< Number of stream buffers. */ + std::string streamName; /*!< A stream name (currently used only in Jack). */ + int priority; /*!< Scheduling priority of callback thread (only used with flag RTAUDIO_SCHEDULE_REALTIME). */ + + // Default constructor. + StreamOptions() + : flags(0), numberOfBuffers(0), priority(0) {} + }; + + //! A static function to determine the current RtAudio version. + static std::string getVersion( void ) throw(); + + //! A static function to determine the available compiled audio APIs. + /*! + The values returned in the std::vector can be compared against + the enumerated list values. Note that there can be more than one + API compiled for certain operating systems. + */ + static void getCompiledApi( std::vector &apis ) throw(); + + //! The class constructor. + /*! + The constructor performs minor initialization tasks. An exception + can be thrown if no API support is compiled. + + If no API argument is specified and multiple API support has been + compiled, the default order of use is JACK, ALSA, OSS (Linux + systems) and ASIO, DS (Windows systems). + */ + RtAudio( RtAudio::Api api=UNSPECIFIED ); + + //! The destructor. + /*! + If a stream is running or open, it will be stopped and closed + automatically. + */ + ~RtAudio() throw(); + + //! Returns the audio API specifier for the current instance of RtAudio. + RtAudio::Api getCurrentApi( void ) throw(); + + //! A public function that queries for the number of audio devices available. + /*! + This function performs a system query of available devices each time it + is called, thus supporting devices connected \e after instantiation. If + a system error occurs during processing, a warning will be issued. + */ + unsigned int getDeviceCount( void ) throw(); + + //! Return an RtAudio::DeviceInfo structure for a specified device number. + /*! + + Any device integer between 0 and getDeviceCount() - 1 is valid. + If an invalid argument is provided, an RtAudioError (type = INVALID_USE) + will be thrown. If a device is busy or otherwise unavailable, the + structure member "probed" will have a value of "false" and all + other members are undefined. If the specified device is the + current default input or output device, the corresponding + "isDefault" member will have a value of "true". + */ + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + + //! A function that returns the index of the default output device. + /*! + If the underlying audio API does not provide a "default + device", or if no devices are available, the return value will be + 0. Note that this is a valid device identifier and it is the + client's responsibility to verify that a device is available + before attempting to open a stream. + */ + unsigned int getDefaultOutputDevice( void ) throw(); + + //! A function that returns the index of the default input device. + /*! + If the underlying audio API does not provide a "default + device", or if no devices are available, the return value will be + 0. Note that this is a valid device identifier and it is the + client's responsibility to verify that a device is available + before attempting to open a stream. + */ + unsigned int getDefaultInputDevice( void ) throw(); + + //! A public function for opening a stream with the specified parameters. + /*! + An RtAudioError (type = SYSTEM_ERROR) is thrown if a stream cannot be + opened with the specified parameters or an error occurs during + processing. An RtAudioError (type = INVALID_USE) is thrown if any + invalid device ID or channel number parameters are specified. + + \param outputParameters Specifies output stream parameters to use + when opening a stream, including a device ID, number of channels, + and starting channel number. For input-only streams, this + argument should be NULL. The device ID is an index value between + 0 and getDeviceCount() - 1. + \param inputParameters Specifies input stream parameters to use + when opening a stream, including a device ID, number of channels, + and starting channel number. For output-only streams, this + argument should be NULL. The device ID is an index value between + 0 and getDeviceCount() - 1. + \param format An RtAudioFormat specifying the desired sample data format. + \param sampleRate The desired sample rate (sample frames per second). + \param *bufferFrames A pointer to a value indicating the desired + internal buffer size in sample frames. The actual value + used by the device is returned via the same pointer. A + value of zero can be specified, in which case the lowest + allowable value is determined. + \param callback A client-defined function that will be invoked + when input data is available and/or output data is needed. + \param userData An optional pointer to data that can be accessed + from within the callback function. + \param options An optional pointer to a structure containing various + global stream options, including a list of OR'ed RtAudioStreamFlags + and a suggested number of stream buffers that can be used to + control stream latency. More buffers typically result in more + robust performance, though at a cost of greater latency. If a + value of zero is specified, a system-specific median value is + chosen. If the RTAUDIO_MINIMIZE_LATENCY flag bit is set, the + lowest allowable value is used. The actual value used is + returned via the structure argument. The parameter is API dependent. + \param errorCallback A client-defined function that will be invoked + when an error has occured. + */ + void openStream( RtAudio::StreamParameters *outputParameters, + RtAudio::StreamParameters *inputParameters, + RtAudioFormat format, unsigned int sampleRate, + unsigned int *bufferFrames, RtAudioCallback callback, + void *userData = NULL, RtAudio::StreamOptions *options = NULL, RtAudioErrorCallback errorCallback = NULL ); + + //! A function that closes a stream and frees any associated stream memory. + /*! + If a stream is not open, this function issues a warning and + returns (no exception is thrown). + */ + void closeStream( void ) throw(); + + //! A function that starts a stream. + /*! + An RtAudioError (type = SYSTEM_ERROR) is thrown if an error occurs + during processing. An RtAudioError (type = INVALID_USE) is thrown if a + stream is not open. A warning is issued if the stream is already + running. + */ + void startStream( void ); + + //! Stop a stream, allowing any samples remaining in the output queue to be played. + /*! + An RtAudioError (type = SYSTEM_ERROR) is thrown if an error occurs + during processing. An RtAudioError (type = INVALID_USE) is thrown if a + stream is not open. A warning is issued if the stream is already + stopped. + */ + void stopStream( void ); + + //! Stop a stream, discarding any samples remaining in the input/output queue. + /*! + An RtAudioError (type = SYSTEM_ERROR) is thrown if an error occurs + during processing. An RtAudioError (type = INVALID_USE) is thrown if a + stream is not open. A warning is issued if the stream is already + stopped. + */ + void abortStream( void ); + + //! Returns true if a stream is open and false if not. + bool isStreamOpen( void ) const throw(); + + //! Returns true if the stream is running and false if it is stopped or not open. + bool isStreamRunning( void ) const throw(); + + //! Returns the number of elapsed seconds since the stream was started. + /*! + If a stream is not open, an RtAudioError (type = INVALID_USE) will be thrown. + */ + double getStreamTime( void ); + + //! Set the stream time to a time in seconds greater than or equal to 0.0. + /*! + If a stream is not open, an RtAudioError (type = INVALID_USE) will be thrown. + */ + void setStreamTime( double time ); + + //! Returns the internal stream latency in sample frames. + /*! + The stream latency refers to delay in audio input and/or output + caused by internal buffering by the audio system and/or hardware. + For duplex streams, the returned value will represent the sum of + the input and output latencies. If a stream is not open, an + RtAudioError (type = INVALID_USE) will be thrown. If the API does not + report latency, the return value will be zero. + */ + long getStreamLatency( void ); + + //! Returns actual sample rate in use by the stream. + /*! + On some systems, the sample rate used may be slightly different + than that specified in the stream parameters. If a stream is not + open, an RtAudioError (type = INVALID_USE) will be thrown. + */ + unsigned int getStreamSampleRate( void ); + + //! Specify whether warning messages should be printed to stderr. + void showWarnings( bool value = true ) throw(); + + protected: + + void openRtApi( RtAudio::Api api ); + RtApi *rtapi_; +}; + +// Operating system dependent thread functionality. +#if defined(__WINDOWS_DS__) || defined(__WINDOWS_ASIO__) || defined(__WINDOWS_WASAPI__) + + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include + #include + + typedef uintptr_t ThreadHandle; + typedef CRITICAL_SECTION StreamMutex; + +#elif defined(__LINUX_ALSA__) || defined(__LINUX_PULSE__) || defined(__UNIX_JACK__) || defined(__LINUX_OSS__) || defined(__MACOSX_CORE__) + // Using pthread library for various flavors of unix. + #include + + typedef pthread_t ThreadHandle; + typedef pthread_mutex_t StreamMutex; + +#else // Setup for "dummy" behavior + + #define __RTAUDIO_DUMMY__ + typedef int ThreadHandle; + typedef int StreamMutex; + +#endif + +// This global structure type is used to pass callback information +// between the private RtAudio stream structure and global callback +// handling functions. +struct CallbackInfo { + void *object; // Used as a "this" pointer. + ThreadHandle thread; + void *callback; + void *userData; + void *errorCallback; + void *apiInfo; // void pointer for API specific callback information + bool isRunning; + bool doRealtime; + int priority; + + // Default constructor. + CallbackInfo() + :object(0), callback(0), userData(0), errorCallback(0), apiInfo(0), isRunning(false), doRealtime(false) {} +}; + +// **************************************************************** // +// +// RtApi class declaration. +// +// Subclasses of RtApi contain all API- and OS-specific code necessary +// to fully implement the RtAudio API. +// +// Note that RtApi is an abstract base class and cannot be +// explicitly instantiated. The class RtAudio will create an +// instance of an RtApi subclass (RtApiOss, RtApiAlsa, +// RtApiJack, RtApiCore, RtApiDs, or RtApiAsio). +// +// **************************************************************** // + +#pragma pack(push, 1) +class S24 { + + protected: + unsigned char c3[3]; + + public: + S24() {} + + S24& operator = ( const int& i ) { + c3[0] = (i & 0x000000ff); + c3[1] = (i & 0x0000ff00) >> 8; + c3[2] = (i & 0x00ff0000) >> 16; + return *this; + } + + S24( const S24& v ) { *this = v; } + S24( const double& d ) { *this = (int) d; } + S24( const float& f ) { *this = (int) f; } + S24( const signed short& s ) { *this = (int) s; } + S24( const char& c ) { *this = (int) c; } + + int asInt() { + int i = c3[0] | (c3[1] << 8) | (c3[2] << 16); + if (i & 0x800000) i |= ~0xffffff; + return i; + } +}; +#pragma pack(pop) + +#if defined( HAVE_GETTIMEOFDAY ) + #include +#endif + +#include + +class RtApi +{ +public: + + RtApi(); + virtual ~RtApi(); + virtual RtAudio::Api getCurrentApi( void ) = 0; + virtual unsigned int getDeviceCount( void ) = 0; + virtual RtAudio::DeviceInfo getDeviceInfo( unsigned int device ) = 0; + virtual unsigned int getDefaultInputDevice( void ); + virtual unsigned int getDefaultOutputDevice( void ); + void openStream( RtAudio::StreamParameters *outputParameters, + RtAudio::StreamParameters *inputParameters, + RtAudioFormat format, unsigned int sampleRate, + unsigned int *bufferFrames, RtAudioCallback callback, + void *userData, RtAudio::StreamOptions *options, + RtAudioErrorCallback errorCallback ); + virtual void closeStream( void ); + virtual void startStream( void ) = 0; + virtual void stopStream( void ) = 0; + virtual void abortStream( void ) = 0; + long getStreamLatency( void ); + unsigned int getStreamSampleRate( void ); + virtual double getStreamTime( void ); + virtual void setStreamTime( double time ); + bool isStreamOpen( void ) const { return stream_.state != STREAM_CLOSED; } + bool isStreamRunning( void ) const { return stream_.state == STREAM_RUNNING; } + void showWarnings( bool value ) { showWarnings_ = value; } + + +protected: + + static const unsigned int MAX_SAMPLE_RATES; + static const unsigned int SAMPLE_RATES[]; + + enum { FAILURE, SUCCESS }; + + enum StreamState { + STREAM_STOPPED, + STREAM_STOPPING, + STREAM_RUNNING, + STREAM_CLOSED = -50 + }; + + enum StreamMode { + OUTPUT, + INPUT, + DUPLEX, + UNINITIALIZED = -75 + }; + + // A protected structure used for buffer conversion. + struct ConvertInfo { + int channels; + int inJump, outJump; + RtAudioFormat inFormat, outFormat; + std::vector inOffset; + std::vector outOffset; + }; + + // A protected structure for audio streams. + struct RtApiStream { + unsigned int device[2]; // Playback and record, respectively. + void *apiHandle; // void pointer for API specific stream handle information + StreamMode mode; // OUTPUT, INPUT, or DUPLEX. + StreamState state; // STOPPED, RUNNING, or CLOSED + char *userBuffer[2]; // Playback and record, respectively. + char *deviceBuffer; + bool doConvertBuffer[2]; // Playback and record, respectively. + bool userInterleaved; + bool deviceInterleaved[2]; // Playback and record, respectively. + bool doByteSwap[2]; // Playback and record, respectively. + unsigned int sampleRate; + unsigned int bufferSize; + unsigned int nBuffers; + unsigned int nUserChannels[2]; // Playback and record, respectively. + unsigned int nDeviceChannels[2]; // Playback and record channels, respectively. + unsigned int channelOffset[2]; // Playback and record, respectively. + unsigned long latency[2]; // Playback and record, respectively. + RtAudioFormat userFormat; + RtAudioFormat deviceFormat[2]; // Playback and record, respectively. + StreamMutex mutex; + CallbackInfo callbackInfo; + ConvertInfo convertInfo[2]; + double streamTime; // Number of elapsed seconds since the stream started. + +#if defined(HAVE_GETTIMEOFDAY) + struct timeval lastTickTimestamp; +#endif + + RtApiStream() + :apiHandle(0), deviceBuffer(0) { device[0] = 11111; device[1] = 11111; } + }; + + typedef S24 Int24; + typedef signed short Int16; + typedef signed int Int32; + typedef float Float32; + typedef double Float64; + + std::ostringstream errorStream_; + std::string errorText_; + bool showWarnings_; + RtApiStream stream_; + bool firstErrorOccurred_; + + /*! + Protected, api-specific method that attempts to open a device + with the given parameters. This function MUST be implemented by + all subclasses. If an error is encountered during the probe, a + "warning" message is reported and FAILURE is returned. A + successful probe is indicated by a return value of SUCCESS. + */ + virtual bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); + + //! A protected function used to increment the stream time. + void tickStreamTime( void ); + + //! Protected common method to clear an RtApiStream structure. + void clearStreamInfo(); + + /*! + Protected common method that throws an RtAudioError (type = + INVALID_USE) if a stream is not open. + */ + void verifyStream( void ); + + //! Protected common error method to allow global control over error handling. + void error( RtAudioError::Type type ); + + /*! + Protected method used to perform format, channel number, and/or interleaving + conversions between the user and device buffers. + */ + void convertBuffer( char *outBuffer, char *inBuffer, ConvertInfo &info ); + + //! Protected common method used to perform byte-swapping on buffers. + void byteSwapBuffer( char *buffer, unsigned int samples, RtAudioFormat format ); + + //! Protected common method that returns the number of bytes for a given format. + unsigned int formatBytes( RtAudioFormat format ); + + //! Protected common method that sets up the parameters for buffer conversion. + void setConvertInfo( StreamMode mode, unsigned int firstChannel ); +}; + +// **************************************************************** // +// +// Inline RtAudio definitions. +// +// **************************************************************** // + +inline RtAudio::Api RtAudio :: getCurrentApi( void ) throw() { return rtapi_->getCurrentApi(); } +inline unsigned int RtAudio :: getDeviceCount( void ) throw() { return rtapi_->getDeviceCount(); } +inline RtAudio::DeviceInfo RtAudio :: getDeviceInfo( unsigned int device ) { return rtapi_->getDeviceInfo( device ); } +inline unsigned int RtAudio :: getDefaultInputDevice( void ) throw() { return rtapi_->getDefaultInputDevice(); } +inline unsigned int RtAudio :: getDefaultOutputDevice( void ) throw() { return rtapi_->getDefaultOutputDevice(); } +inline void RtAudio :: closeStream( void ) throw() { return rtapi_->closeStream(); } +inline void RtAudio :: startStream( void ) { return rtapi_->startStream(); } +inline void RtAudio :: stopStream( void ) { return rtapi_->stopStream(); } +inline void RtAudio :: abortStream( void ) { return rtapi_->abortStream(); } +inline bool RtAudio :: isStreamOpen( void ) const throw() { return rtapi_->isStreamOpen(); } +inline bool RtAudio :: isStreamRunning( void ) const throw() { return rtapi_->isStreamRunning(); } +inline long RtAudio :: getStreamLatency( void ) { return rtapi_->getStreamLatency(); } +inline unsigned int RtAudio :: getStreamSampleRate( void ) { return rtapi_->getStreamSampleRate(); } +inline double RtAudio :: getStreamTime( void ) { return rtapi_->getStreamTime(); } +inline void RtAudio :: setStreamTime( double time ) { return rtapi_->setStreamTime( time ); } +inline void RtAudio :: showWarnings( bool value ) throw() { rtapi_->showWarnings( value ); } + +// RtApi Subclass prototypes. + +#if defined(__MACOSX_CORE__) + +#include + +class RtApiCore: public RtApi +{ +public: + + RtApiCore(); + ~RtApiCore(); + RtAudio::Api getCurrentApi( void ) { return RtAudio::MACOSX_CORE; } + unsigned int getDeviceCount( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + unsigned int getDefaultOutputDevice( void ); + unsigned int getDefaultInputDevice( void ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + long getStreamLatency( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + bool callbackEvent( AudioDeviceID deviceId, + const AudioBufferList *inBufferList, + const AudioBufferList *outBufferList ); + + private: + + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); + static const char* getErrorCode( OSStatus code ); +}; + +#endif + +#if defined(__UNIX_JACK__) + +class RtApiJack: public RtApi +{ +public: + + RtApiJack(); + ~RtApiJack(); + RtAudio::Api getCurrentApi( void ) { return RtAudio::UNIX_JACK; } + unsigned int getDeviceCount( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + long getStreamLatency( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + bool callbackEvent( unsigned long nframes ); + + private: + + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); +}; + +#endif + +#if defined(__WINDOWS_ASIO__) + +class RtApiAsio: public RtApi +{ +public: + + RtApiAsio(); + ~RtApiAsio(); + RtAudio::Api getCurrentApi( void ) { return RtAudio::WINDOWS_ASIO; } + unsigned int getDeviceCount( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + long getStreamLatency( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + bool callbackEvent( long bufferIndex ); + + private: + + std::vector devices_; + void saveDeviceInfo( void ); + bool coInitialized_; + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); +}; + +#endif + +#if defined(__WINDOWS_DS__) + +class RtApiDs: public RtApi +{ +public: + + RtApiDs(); + ~RtApiDs(); + RtAudio::Api getCurrentApi( void ) { return RtAudio::WINDOWS_DS; } + unsigned int getDeviceCount( void ); + unsigned int getDefaultOutputDevice( void ); + unsigned int getDefaultInputDevice( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + long getStreamLatency( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + void callbackEvent( void ); + + private: + + bool coInitialized_; + bool buffersRolling; + long duplexPrerollBytes; + std::vector dsDevices; + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); +}; + +#endif + +#if defined(__WINDOWS_WASAPI__) + +struct IMMDeviceEnumerator; + +class RtApiWasapi : public RtApi +{ +public: + RtApiWasapi(); + ~RtApiWasapi(); + + RtAudio::Api getCurrentApi( void ) { return RtAudio::WINDOWS_WASAPI; } + unsigned int getDeviceCount( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + unsigned int getDefaultOutputDevice( void ); + unsigned int getDefaultInputDevice( void ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + +private: + bool coInitialized_; + IMMDeviceEnumerator* deviceEnumerator_; + + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int* bufferSize, + RtAudio::StreamOptions* options ); + + static DWORD WINAPI runWasapiThread( void* wasapiPtr ); + static DWORD WINAPI stopWasapiThread( void* wasapiPtr ); + static DWORD WINAPI abortWasapiThread( void* wasapiPtr ); + void wasapiThread(); +}; + +#endif + +#if defined(__LINUX_ALSA__) + +class RtApiAlsa: public RtApi +{ +public: + + RtApiAlsa(); + ~RtApiAlsa(); + RtAudio::Api getCurrentApi() { return RtAudio::LINUX_ALSA; } + unsigned int getDeviceCount( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + void callbackEvent( void ); + + private: + + std::vector devices_; + void saveDeviceInfo( void ); + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); +}; + +#endif + +#if defined(__LINUX_PULSE__) + +class RtApiPulse: public RtApi +{ +public: + ~RtApiPulse(); + RtAudio::Api getCurrentApi() { return RtAudio::LINUX_PULSE; } + unsigned int getDeviceCount( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + void callbackEvent( void ); + + private: + + std::vector devices_; + void saveDeviceInfo( void ); + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); +}; + +#endif + +#if defined(__LINUX_OSS__) + +class RtApiOss: public RtApi +{ +public: + + RtApiOss(); + ~RtApiOss(); + RtAudio::Api getCurrentApi() { return RtAudio::LINUX_OSS; } + unsigned int getDeviceCount( void ); + RtAudio::DeviceInfo getDeviceInfo( unsigned int device ); + void closeStream( void ); + void startStream( void ); + void stopStream( void ); + void abortStream( void ); + + // This function is intended for internal use only. It must be + // public because it is called by the internal callback handler, + // which is not a member of RtAudio. External use of this function + // will most likely produce highly undesireable results! + void callbackEvent( void ); + + private: + + bool probeDeviceOpen( unsigned int device, StreamMode mode, unsigned int channels, + unsigned int firstChannel, unsigned int sampleRate, + RtAudioFormat format, unsigned int *bufferSize, + RtAudio::StreamOptions *options ); +}; + +#endif + +#if defined(__RTAUDIO_DUMMY__) + +class RtApiDummy: public RtApi +{ +public: + + RtApiDummy() { errorText_ = "RtApiDummy: This class provides no functionality."; error( RtAudioError::WARNING ); } + RtAudio::Api getCurrentApi( void ) { return RtAudio::RTAUDIO_DUMMY; } + unsigned int getDeviceCount( void ) { return 0; } + RtAudio::DeviceInfo getDeviceInfo( unsigned int /*device*/ ) { RtAudio::DeviceInfo info; return info; } + void closeStream( void ) {} + void startStream( void ) {} + void stopStream( void ) {} + void abortStream( void ) {} + + private: + + bool probeDeviceOpen( unsigned int /*device*/, StreamMode /*mode*/, unsigned int /*channels*/, + unsigned int /*firstChannel*/, unsigned int /*sampleRate*/, + RtAudioFormat /*format*/, unsigned int * /*bufferSize*/, + RtAudio::StreamOptions * /*options*/ ) { return false; } +}; + +#endif + +#endif + +// Indentation settings for Vim and Emacs +// +// Local Variables: +// c-basic-offset: 2 +// indent-tabs-mode: nil +// End: +// +// vim: et sts=2 sw=2 diff --git a/external/rtaudio/readme b/external/rtaudio/readme new file mode 100644 index 0000000..bb04c7e --- /dev/null +++ b/external/rtaudio/readme @@ -0,0 +1,61 @@ +RtAudio - a set of C++ classes that provide a common API for realtime audio input/output across Linux (native ALSA, JACK, PulseAudio and OSS), Macintosh OS X (CoreAudio and JACK), and Windows (DirectSound, ASIO and WASAPI) operating systems. + +By Gary P. Scavone, 2001-2014. + +This distribution of RtAudio contains the following: + +doc: RtAudio documentation (see doc/html/index.html) +tests: example RtAudio programs +include: header and source files necessary for ASIO, DS & OSS compilation +tests/Windows: Visual C++ .net test program workspace and projects + +OVERVIEW: + +RtAudio is a set of C++ classes that provides a common API (Application Programming Interface) for realtime audio input/output across Linux (native ALSA, JACK, and OSS), Macintosh OS X, SGI, and Windows (DirectSound, ASIO and WASAPI) operating systems. RtAudio significantly simplifies the process of interacting with computer audio hardware. It was designed with the following objectives: + + - object-oriented C++ design + - simple, common API across all supported platforms + - only one source and one header file for easy inclusion in programming projects + - allow simultaneous multi-api support + - support dynamic connection of devices + - provide extensive audio device parameter control + - allow audio device capability probing + - automatic internal conversion for data format, channel number compensation, (de)interleaving, and byte-swapping + +RtAudio incorporates the concept of audio streams, which represent audio output (playback) and/or input (recording). Available audio devices and their capabilities can be enumerated and then specified when opening a stream. Where applicable, multiple API support can be compiled and a particular API specified when creating an RtAudio instance. See the \ref apinotes section for information specific to each of the supported audio APIs. + +FURTHER READING: + +For complete documentation on RtAudio, see the doc directory of the distribution or surf to http://www.music.mcgill.ca/~gary/rtaudio/. + + +LEGAL AND ETHICAL: + +The RtAudio license is similar to the MIT License. + + RtAudio: a set of realtime audio i/o C++ classes + Copyright (c) 2001-2014 Gary P. Scavone + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + Any person wishing to distribute modifications to the Software is + asked to send the modifications to the original developer so that + they can be incorporated into the canonical version. This is, + however, not a binding provision of this license. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/audio/AudioThread.cpp b/src/audio/AudioThread.cpp index 381b951..af757e9 100644 --- a/src/audio/AudioThread.cpp +++ b/src/audio/AudioThread.cpp @@ -3,7 +3,7 @@ #include AudioThread::AudioThread(AudioThreadInputQueue *inputQueue) : - inputQueue(inputQueue), stream(NULL), terminated(false) { + inputQueue(inputQueue), terminated(false) { // , stream(NULL) } @@ -14,51 +14,51 @@ AudioThread::~AudioThread() { void AudioThread::threadMain() { std::cout << "Audio thread initializing.." << std::endl; - PaError err; - err = Pa_Initialize(); - if (err != paNoError) { - std::cout << "Error starting portaudio :(" << std::endl; - return; - } +// PaError err; +// err = Pa_Initialize(); +// if (err != paNoError) { +// std::cout << "Error starting portaudio :(" << std::endl; +// return; +// } +// +// int preferred_device = -1; +// +// outputParameters.device = +// (preferred_device != -1) ? +// preferred_device : Pa_GetDefaultOutputDevice(); +// if (outputParameters.device == paNoDevice) { +// std::cout << "Error: No default output device.\n"; +// } +// +// outputParameters.channelCount = 2; +// outputParameters.sampleFormat = paFloat32; +// outputParameters.suggestedLatency = Pa_GetDeviceInfo( +// outputParameters.device)->defaultHighOutputLatency; +// outputParameters.hostApiSpecificStreamInfo = NULL; +// +// stream = NULL; +// +// err = Pa_OpenStream(&stream, NULL, &outputParameters, AUDIO_FREQUENCY, +// paFramesPerBufferUnspecified, paClipOff, NULL, NULL); - int preferred_device = -1; - - outputParameters.device = - (preferred_device != -1) ? - preferred_device : Pa_GetDefaultOutputDevice(); - if (outputParameters.device == paNoDevice) { - std::cout << "Error: No default output device.\n"; - } - - outputParameters.channelCount = 2; - outputParameters.sampleFormat = paFloat32; - outputParameters.suggestedLatency = Pa_GetDeviceInfo( - outputParameters.device)->defaultHighOutputLatency; - outputParameters.hostApiSpecificStreamInfo = NULL; - - stream = NULL; - - err = Pa_OpenStream(&stream, NULL, &outputParameters, AUDIO_FREQUENCY, - paFramesPerBufferUnspecified, paClipOff, NULL, NULL); - - err = Pa_StartStream(stream); - if (err != paNoError) { - std::cout << "Error starting stream: " << Pa_GetErrorText(err) - << std::endl; - std::cout << "\tPortAudio error: " << Pa_GetErrorText(err) << std::endl; - } +// err = Pa_StartStream(stream); +// if (err != paNoError) { +// std::cout << "Error starting stream: " << Pa_GetErrorText(err) +// << std::endl; +// std::cout << "\tPortAudio error: " << Pa_GetErrorText(err) << std::endl; +// } while (!terminated) { AudioThreadInput inp; inputQueue->pop(inp); if (inp.data.size()) { - Pa_WriteStream(stream, &inp.data[0], inp.data.size()/2); +// Pa_WriteStream(stream, &inp.data[0], inp.data.size()/2); } } - err = Pa_StopStream(stream); - err = Pa_CloseStream(stream); - Pa_Terminate(); +// err = Pa_StopStream(stream); +// err = Pa_CloseStream(stream); +// Pa_Terminate(); std::cout << "Audio thread done." << std::endl; } diff --git a/src/audio/AudioThread.h b/src/audio/AudioThread.h index 57c3a86..bcf8289 100644 --- a/src/audio/AudioThread.h +++ b/src/audio/AudioThread.h @@ -14,7 +14,7 @@ #include "AudioThread.h" #include "ThreadQueue.h" -#include "portaudio.h" +#include "RtAudio.h" class AudioThreadInput { public: @@ -36,8 +36,8 @@ public: private: AudioThreadInputQueue *inputQueue; - PaStreamParameters outputParameters; - PaStream *stream; +// PaStreamParameters outputParameters; +// PaStream *stream; std::atomic terminated; }; From 03edda18e5f26274a29db65760e1ce3e06b17484 Mon Sep 17 00:00:00 2001 From: "Charles J. Cliffe" Date: Thu, 4 Dec 2014 19:44:49 -0500 Subject: [PATCH 2/6] RtAudio integrated and working great /w win64 Next up, OSX test.. --- CMakeLists.txt | 8 +- .../rtaudio/FunctionDiscoveryKeys_devpkey.h | 212 ++++++++++++++++++ src/CubicSDRDefs.h | 2 +- src/audio/AudioThread.cpp | 112 +++++---- src/audio/AudioThread.h | 8 +- 5 files changed, 292 insertions(+), 50 deletions(-) create mode 100644 external/rtaudio/FunctionDiscoveryKeys_devpkey.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ced68ed..2ae830a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,10 +71,12 @@ if (DEFINED WIN32) include_directories ( ${PROJECT_SOURCE_DIR}/external/liquid-dsp/include ) ADD_DEFINITIONS( - -D__WINDOWS_DS__ + -D__WINDOWS_WASAPI__ +# -D__WINDOWS_DS__ ) - - SET(OTHER_LIBRARIES -ldsound) + + SET(OTHER_LIBRARIES -luuid -lksuser ) + #SET(OTHER_LIBRARIES -ldsound) else (DEFINED WIN32) set(RTLSDR_INCLUDE "/opt/local/include" CACHE FILEPATH "RTL-SDR Include Path") diff --git a/external/rtaudio/FunctionDiscoveryKeys_devpkey.h b/external/rtaudio/FunctionDiscoveryKeys_devpkey.h new file mode 100644 index 0000000..854244d --- /dev/null +++ b/external/rtaudio/FunctionDiscoveryKeys_devpkey.h @@ -0,0 +1,212 @@ +#pragma once + +/*++ + +Copyright (c) Microsoft Corporation. All rights reserved. + +Module Name: + + devpkey.h + +Abstract: + + Defines property keys for the Plug and Play Device Property API. + +Author: + + Jim Cavalaris (jamesca) 10-14-2003 + +Environment: + + User-mode only. + +Revision History: + + 14-October-2003 jamesca + + Creation and initial implementation. + + 20-June-2006 dougb + + Copied Jim's version replaced "DEFINE_DEVPROPKEY(DEVPKEY_" with "DEFINE_PROPERTYKEY(PKEY_" + +--*/ + +//#include + +// +// _NAME +// + +DEFINE_PROPERTYKEY(PKEY_NAME, 0xb725f130, 0x47ef, 0x101a, 0xa5, 0xf1, 0x02, 0x60, 0x8c, 0x9e, 0xeb, 0xac, 10); // DEVPROP_TYPE_STRING + +// +// Device properties +// These PKEYs correspond to the old setupapi SPDRP_XXX properties +// +DEFINE_PROPERTYKEY(PKEY_Device_DeviceDesc, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 2); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_HardwareIds, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 3); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_Device_CompatibleIds, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 4); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_Device_Service, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 6); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_Class, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 9); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_ClassGuid, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 10); // DEVPROP_TYPE_GUID +DEFINE_PROPERTYKEY(PKEY_Device_Driver, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 11); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_ConfigFlags, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 12); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_Manufacturer, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 13); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_LocationInfo, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 15); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_PDOName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 16); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_Capabilities, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 17); // DEVPROP_TYPE_UNINT32 +DEFINE_PROPERTYKEY(PKEY_Device_UINumber, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 18); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_UpperFilters, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 19); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_Device_LowerFilters, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 20); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_Device_BusTypeGuid, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 21); // DEVPROP_TYPE_GUID +DEFINE_PROPERTYKEY(PKEY_Device_LegacyBusType, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 22); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_BusNumber, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 23); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_EnumeratorName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 24); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_Security, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 25); // DEVPROP_TYPE_SECURITY_DESCRIPTOR +DEFINE_PROPERTYKEY(PKEY_Device_SecuritySDS, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 26); // DEVPROP_TYPE_SECURITY_DESCRIPTOR_STRING +DEFINE_PROPERTYKEY(PKEY_Device_DevType, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 27); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_Exclusive, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 28); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_Characteristics, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 29); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_Address, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 30); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_UINumberDescFormat, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 31); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_PowerData, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 32); // DEVPROP_TYPE_BINARY +DEFINE_PROPERTYKEY(PKEY_Device_RemovalPolicy, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 33); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_RemovalPolicyDefault, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 34); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_RemovalPolicyOverride, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 35); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_InstallState, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 36); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_LocationPaths, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 37); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_Device_BaseContainerId, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 38); // DEVPROP_TYPE_GUID + +// +// Device properties +// These PKEYs correspond to a device's status and problem code +// +DEFINE_PROPERTYKEY(PKEY_Device_DevNodeStatus, 0x4340a6c5, 0x93fa, 0x4706, 0x97, 0x2c, 0x7b, 0x64, 0x80, 0x08, 0xa5, 0xa7, 2); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_ProblemCode, 0x4340a6c5, 0x93fa, 0x4706, 0x97, 0x2c, 0x7b, 0x64, 0x80, 0x08, 0xa5, 0xa7, 3); // DEVPROP_TYPE_UINT32 + +// +// Device properties +// These PKEYs correspond to device relations +// +DEFINE_PROPERTYKEY(PKEY_Device_EjectionRelations, 0x4340a6c5, 0x93fa, 0x4706, 0x97, 0x2c, 0x7b, 0x64, 0x80, 0x08, 0xa5, 0xa7, 4); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_Device_RemovalRelations, 0x4340a6c5, 0x93fa, 0x4706, 0x97, 0x2c, 0x7b, 0x64, 0x80, 0x08, 0xa5, 0xa7, 5); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_Device_PowerRelations, 0x4340a6c5, 0x93fa, 0x4706, 0x97, 0x2c, 0x7b, 0x64, 0x80, 0x08, 0xa5, 0xa7, 6); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_Device_BusRelations, 0x4340a6c5, 0x93fa, 0x4706, 0x97, 0x2c, 0x7b, 0x64, 0x80, 0x08, 0xa5, 0xa7, 7); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_Device_Parent, 0x4340a6c5, 0x93fa, 0x4706, 0x97, 0x2c, 0x7b, 0x64, 0x80, 0x08, 0xa5, 0xa7, 8); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_Children, 0x4340a6c5, 0x93fa, 0x4706, 0x97, 0x2c, 0x7b, 0x64, 0x80, 0x08, 0xa5, 0xa7, 9); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_Device_Siblings, 0x4340a6c5, 0x93fa, 0x4706, 0x97, 0x2c, 0x7b, 0x64, 0x80, 0x08, 0xa5, 0xa7, 10); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_Device_TransportRelations, 0x4340a6c5, 0x93fa, 0x4706, 0x97, 0x2c, 0x7b, 0x64, 0x80, 0x08, 0xa5, 0xa7, 11); // DEVPROP_TYPE_STRING_LIST + +// +// Other Device properties +// +DEFINE_PROPERTYKEY(PKEY_Device_Reported, 0x80497100, 0x8c73, 0x48b9, 0xaa, 0xd9, 0xce, 0x38, 0x7e, 0x19, 0xc5, 0x6e, 2); // DEVPROP_TYPE_BOOLEAN +DEFINE_PROPERTYKEY(PKEY_Device_Legacy, 0x80497100, 0x8c73, 0x48b9, 0xaa, 0xd9, 0xce, 0x38, 0x7e, 0x19, 0xc5, 0x6e, 3); // DEVPROP_TYPE_BOOLEAN +DEFINE_PROPERTYKEY(PKEY_Device_InstanceId, 0x78c34fc8, 0x104a, 0x4aca, 0x9e, 0xa4, 0x52, 0x4d, 0x52, 0x99, 0x6e, 0x57, 256); // DEVPROP_TYPE_STRING + +DEFINE_PROPERTYKEY(PKEY_Device_ContainerId, 0x8c7ed206, 0x3f8a, 0x4827, 0xb3, 0xab, 0xae, 0x9e, 0x1f, 0xae, 0xfc, 0x6c, 2); // DEVPROP_TYPE_GUID + +DEFINE_PROPERTYKEY(PKEY_Device_ModelId, 0x80d81ea6, 0x7473, 0x4b0c, 0x82, 0x16, 0xef, 0xc1, 0x1a, 0x2c, 0x4c, 0x8b, 2); // DEVPROP_TYPE_GUID + +DEFINE_PROPERTYKEY(PKEY_Device_FriendlyNameAttributes, 0x80d81ea6, 0x7473, 0x4b0c, 0x82, 0x16, 0xef, 0xc1, 0x1a, 0x2c, 0x4c, 0x8b, 3); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_ManufacturerAttributes, 0x80d81ea6, 0x7473, 0x4b0c, 0x82, 0x16, 0xef, 0xc1, 0x1a, 0x2c, 0x4c, 0x8b, 4); // DEVPROP_TYPE_UINT32 + +DEFINE_PROPERTYKEY(PKEY_Device_PresenceNotForDevice, 0x80d81ea6, 0x7473, 0x4b0c, 0x82, 0x16, 0xef, 0xc1, 0x1a, 0x2c, 0x4c, 0x8b, 5); // DEVPROP_TYPE_BOOLEAN + + +DEFINE_PROPERTYKEY(PKEY_Numa_Proximity_Domain, 0x540b947e, 0x8b40, 0x45bc, 0xa8, 0xa2, 0x6a, 0x0b, 0x89, 0x4c, 0xbd, 0xa2, 1); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_DHP_Rebalance_Policy, 0x540b947e, 0x8b40, 0x45bc, 0xa8, 0xa2, 0x6a, 0x0b, 0x89, 0x4c, 0xbd, 0xa2, 2); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_Numa_Node, 0x540b947e, 0x8b40, 0x45bc, 0xa8, 0xa2, 0x6a, 0x0b, 0x89, 0x4c, 0xbd, 0xa2, 3); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_BusReportedDeviceDesc, 0x540b947e, 0x8b40, 0x45bc, 0xa8, 0xa2, 0x6a, 0x0b, 0x89, 0x4c, 0xbd, 0xa2, 4); // DEVPROP_TYPE_STRING + +DEFINE_PROPERTYKEY(PKEY_Device_InstallInProgress, 0x83da6326, 0x97a6, 0x4088, 0x94, 0x53, 0xa1, 0x92, 0x3f, 0x57, 0x3b, 0x29, 9); // DEVPROP_TYPE_BOOLEAN + +// +// Device driver properties +// +DEFINE_PROPERTYKEY(PKEY_Device_DriverDate, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 2); // DEVPROP_TYPE_FILETIME +DEFINE_PROPERTYKEY(PKEY_Device_DriverVersion, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 3); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_DriverDesc, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 4); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_DriverInfPath, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 5); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_DriverInfSection, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 6); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_DriverInfSectionExt, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 7); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_MatchingDeviceId, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 8); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_DriverProvider, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 9); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_DriverPropPageProvider, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 10); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_DriverCoInstallers, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 11); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_Device_ResourcePickerTags, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 12); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_ResourcePickerExceptions, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 13); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_Device_DriverRank, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 14); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_DriverLogoLevel, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 15); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_Device_NoConnectSound, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 17); // DEVPROP_TYPE_BOOLEAN +DEFINE_PROPERTYKEY(PKEY_Device_GenericDriverInstalled, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 18); // DEVPROP_TYPE_BOOLEAN +DEFINE_PROPERTYKEY(PKEY_Device_AdditionalSoftwareRequested, 0xa8b865dd, 0x2e3d, 0x4094, 0xad, 0x97, 0xe5, 0x93, 0xa7, 0xc, 0x75, 0xd6, 19);// DEVPROP_TYPE_BOOLEAN + +// +// Device safe-removal properties +// +DEFINE_PROPERTYKEY(PKEY_Device_SafeRemovalRequired, 0xafd97640, 0x86a3, 0x4210, 0xb6, 0x7c, 0x28, 0x9c, 0x41, 0xaa, 0xbe, 0x55, 2); // DEVPROP_TYPE_BOOLEAN +DEFINE_PROPERTYKEY(PKEY_Device_SafeRemovalRequiredOverride, 0xafd97640, 0x86a3, 0x4210, 0xb6, 0x7c, 0x28, 0x9c, 0x41, 0xaa, 0xbe, 0x55, 3);// DEVPROP_TYPE_BOOLEAN + + +// +// Device properties that were set by the driver package that was installed +// on the device. +// +DEFINE_PROPERTYKEY(PKEY_DrvPkg_Model, 0xcf73bb51, 0x3abf, 0x44a2, 0x85, 0xe0, 0x9a, 0x3d, 0xc7, 0xa1, 0x21, 0x32, 2); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DrvPkg_VendorWebSite, 0xcf73bb51, 0x3abf, 0x44a2, 0x85, 0xe0, 0x9a, 0x3d, 0xc7, 0xa1, 0x21, 0x32, 3); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DrvPkg_DetailedDescription, 0xcf73bb51, 0x3abf, 0x44a2, 0x85, 0xe0, 0x9a, 0x3d, 0xc7, 0xa1, 0x21, 0x32, 4); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DrvPkg_DocumentationLink, 0xcf73bb51, 0x3abf, 0x44a2, 0x85, 0xe0, 0x9a, 0x3d, 0xc7, 0xa1, 0x21, 0x32, 5); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DrvPkg_Icon, 0xcf73bb51, 0x3abf, 0x44a2, 0x85, 0xe0, 0x9a, 0x3d, 0xc7, 0xa1, 0x21, 0x32, 6); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_DrvPkg_BrandingIcon, 0xcf73bb51, 0x3abf, 0x44a2, 0x85, 0xe0, 0x9a, 0x3d, 0xc7, 0xa1, 0x21, 0x32, 7); // DEVPROP_TYPE_STRING_LIST + +// +// Device setup class properties +// These PKEYs correspond to the old setupapi SPCRP_XXX properties +// +DEFINE_PROPERTYKEY(PKEY_DeviceClass_UpperFilters, 0x4321918b, 0xf69e, 0x470d, 0xa5, 0xde, 0x4d, 0x88, 0xc7, 0x5a, 0xd2, 0x4b, 19); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_DeviceClass_LowerFilters, 0x4321918b, 0xf69e, 0x470d, 0xa5, 0xde, 0x4d, 0x88, 0xc7, 0x5a, 0xd2, 0x4b, 20); // DEVPROP_TYPE_STRING_LIST +DEFINE_PROPERTYKEY(PKEY_DeviceClass_Security, 0x4321918b, 0xf69e, 0x470d, 0xa5, 0xde, 0x4d, 0x88, 0xc7, 0x5a, 0xd2, 0x4b, 25); // DEVPROP_TYPE_SECURITY_DESCRIPTOR +DEFINE_PROPERTYKEY(PKEY_DeviceClass_SecuritySDS, 0x4321918b, 0xf69e, 0x470d, 0xa5, 0xde, 0x4d, 0x88, 0xc7, 0x5a, 0xd2, 0x4b, 26); // DEVPROP_TYPE_SECURITY_DESCRIPTOR_STRING +DEFINE_PROPERTYKEY(PKEY_DeviceClass_DevType, 0x4321918b, 0xf69e, 0x470d, 0xa5, 0xde, 0x4d, 0x88, 0xc7, 0x5a, 0xd2, 0x4b, 27); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_DeviceClass_Exclusive, 0x4321918b, 0xf69e, 0x470d, 0xa5, 0xde, 0x4d, 0x88, 0xc7, 0x5a, 0xd2, 0x4b, 28); // DEVPROP_TYPE_UINT32 +DEFINE_PROPERTYKEY(PKEY_DeviceClass_Characteristics, 0x4321918b, 0xf69e, 0x470d, 0xa5, 0xde, 0x4d, 0x88, 0xc7, 0x5a, 0xd2, 0x4b, 29); // DEVPROP_TYPE_UINT32 + +// +// Device setup class properties +// These PKEYs correspond to registry values under the device class GUID key +// +DEFINE_PROPERTYKEY(PKEY_DeviceClass_Name, 0x259abffc, 0x50a7, 0x47ce, 0xaf, 0x8, 0x68, 0xc9, 0xa7, 0xd7, 0x33, 0x66, 2); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DeviceClass_ClassName, 0x259abffc, 0x50a7, 0x47ce, 0xaf, 0x8, 0x68, 0xc9, 0xa7, 0xd7, 0x33, 0x66, 3); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DeviceClass_Icon, 0x259abffc, 0x50a7, 0x47ce, 0xaf, 0x8, 0x68, 0xc9, 0xa7, 0xd7, 0x33, 0x66, 4); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DeviceClass_ClassInstaller, 0x259abffc, 0x50a7, 0x47ce, 0xaf, 0x8, 0x68, 0xc9, 0xa7, 0xd7, 0x33, 0x66, 5); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DeviceClass_PropPageProvider, 0x259abffc, 0x50a7, 0x47ce, 0xaf, 0x8, 0x68, 0xc9, 0xa7, 0xd7, 0x33, 0x66, 6); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DeviceClass_NoInstallClass, 0x259abffc, 0x50a7, 0x47ce, 0xaf, 0x8, 0x68, 0xc9, 0xa7, 0xd7, 0x33, 0x66, 7); // DEVPROP_TYPE_BOOLEAN +DEFINE_PROPERTYKEY(PKEY_DeviceClass_NoDisplayClass, 0x259abffc, 0x50a7, 0x47ce, 0xaf, 0x8, 0x68, 0xc9, 0xa7, 0xd7, 0x33, 0x66, 8); // DEVPROP_TYPE_BOOLEAN +DEFINE_PROPERTYKEY(PKEY_DeviceClass_SilentInstall, 0x259abffc, 0x50a7, 0x47ce, 0xaf, 0x8, 0x68, 0xc9, 0xa7, 0xd7, 0x33, 0x66, 9); // DEVPROP_TYPE_BOOLEAN +DEFINE_PROPERTYKEY(PKEY_DeviceClass_NoUseClass, 0x259abffc, 0x50a7, 0x47ce, 0xaf, 0x8, 0x68, 0xc9, 0xa7, 0xd7, 0x33, 0x66, 10); // DEVPROP_TYPE_BOOLEAN +DEFINE_PROPERTYKEY(PKEY_DeviceClass_DefaultService, 0x259abffc, 0x50a7, 0x47ce, 0xaf, 0x8, 0x68, 0xc9, 0xa7, 0xd7, 0x33, 0x66, 11); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DeviceClass_IconPath, 0x259abffc, 0x50a7, 0x47ce, 0xaf, 0x8, 0x68, 0xc9, 0xa7, 0xd7, 0x33, 0x66, 12); // DEVPROP_TYPE_STRING_LIST + +// +// Other Device setup class properties +// +DEFINE_PROPERTYKEY(PKEY_DeviceClass_ClassCoInstallers, 0x713d1703, 0xa2e2, 0x49f5, 0x92, 0x14, 0x56, 0x47, 0x2e, 0xf3, 0xda, 0x5c, 2); // DEVPROP_TYPE_STRING_LIST + +// +// Device interface properties +// +DEFINE_PROPERTYKEY(PKEY_DeviceInterface_FriendlyName, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 2); // DEVPROP_TYPE_STRING +DEFINE_PROPERTYKEY(PKEY_DeviceInterface_Enabled, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 3); // DEVPROP_TYPE_BOOLEAN +DEFINE_PROPERTYKEY(PKEY_DeviceInterface_ClassGuid, 0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22, 4); // DEVPROP_TYPE_GUID + +// +// Device interface class properties +// +DEFINE_PROPERTYKEY(PKEY_DeviceInterfaceClass_DefaultInterface, 0x14c83a99, 0x0b3f, 0x44b7, 0xbe, 0x4c, 0xa1, 0x78, 0xd3, 0x99, 0x05, 0x64, 2); // DEVPROP_TYPE_STRING + + + + diff --git a/src/CubicSDRDefs.h b/src/CubicSDRDefs.h index b538c3c..d677fce 100644 --- a/src/CubicSDRDefs.h +++ b/src/CubicSDRDefs.h @@ -1,6 +1,6 @@ #pragma once -#define BUF_SIZE (16384*6) +#define BUF_SIZE (16384*4) #define SRATE 2000000 #define FFT_SIZE 2048 diff --git a/src/audio/AudioThread.cpp b/src/audio/AudioThread.cpp index af757e9..3c8814e 100644 --- a/src/audio/AudioThread.cpp +++ b/src/audio/AudioThread.cpp @@ -3,7 +3,7 @@ #include AudioThread::AudioThread(AudioThreadInputQueue *inputQueue) : - inputQueue(inputQueue), terminated(false) { // , stream(NULL) + inputQueue(inputQueue), terminated(false), audio_queue_ptr(0) { } @@ -11,54 +11,80 @@ AudioThread::~AudioThread() { } +static int audioCallback(void *outputBuffer, void *inputBuffer, unsigned int nBufferFrames, double streamTime, RtAudioStreamStatus status, + void *userData) { + AudioThread *src = (AudioThread *) userData; + float *out = (float*) outputBuffer; + if (status) { + std::cout << "Audio buffer underflow.." << std::endl; + } + if (!src->audio_queue.size()) { + for (int i = 0; i < nBufferFrames * 2; i++) { + out[i] = 0; + } + return 0; + } + std::vector nextBuffer = src->audio_queue.front(); + for (int i = 0; i < nBufferFrames * 2; i++) { + out[i] = nextBuffer[src->audio_queue_ptr]; + src->audio_queue_ptr++; + if (src->audio_queue_ptr == nextBuffer.size()) { + src->audio_queue.pop(); + src->audio_queue_ptr = 0; + if (!src->audio_queue.size()) { + for (int j = i; j < nBufferFrames * 2; j++) { + std::cout << "Audio buffer underflow.." << std::endl; + out[i] = 0; + } + return 0; + } + nextBuffer = src->audio_queue.front(); + } + } + return 0; +} + void AudioThread::threadMain() { std::cout << "Audio thread initializing.." << std::endl; -// PaError err; -// err = Pa_Initialize(); -// if (err != paNoError) { -// std::cout << "Error starting portaudio :(" << std::endl; -// return; -// } -// -// int preferred_device = -1; -// -// outputParameters.device = -// (preferred_device != -1) ? -// preferred_device : Pa_GetDefaultOutputDevice(); -// if (outputParameters.device == paNoDevice) { -// std::cout << "Error: No default output device.\n"; -// } -// -// outputParameters.channelCount = 2; -// outputParameters.sampleFormat = paFloat32; -// outputParameters.suggestedLatency = Pa_GetDeviceInfo( -// outputParameters.device)->defaultHighOutputLatency; -// outputParameters.hostApiSpecificStreamInfo = NULL; -// -// stream = NULL; -// -// err = Pa_OpenStream(&stream, NULL, &outputParameters, AUDIO_FREQUENCY, -// paFramesPerBufferUnspecified, paClipOff, NULL, NULL); + if (dac.getDeviceCount() < 1) { + std::cout << "No audio devices found!" << std::endl; + return; + } -// err = Pa_StartStream(stream); -// if (err != paNoError) { -// std::cout << "Error starting stream: " << Pa_GetErrorText(err) -// << std::endl; -// std::cout << "\tPortAudio error: " << Pa_GetErrorText(err) << std::endl; -// } + RtAudio::StreamParameters parameters; + parameters.deviceId = dac.getDefaultOutputDevice(); + parameters.nChannels = 2; + parameters.firstChannel = 0; + unsigned int sampleRate = AUDIO_FREQUENCY; + unsigned int bufferFrames = 256; - while (!terminated) { - AudioThreadInput inp; - inputQueue->pop(inp); - if (inp.data.size()) { -// Pa_WriteStream(stream, &inp.data[0], inp.data.size()/2); - } - } + try { + dac.openStream(¶meters, NULL, RTAUDIO_FLOAT32, sampleRate, &bufferFrames, &audioCallback, (void *) this); + dac.startStream(); + } catch (RtAudioError& e) { + e.printMessage(); + return; + } -// err = Pa_StopStream(stream); -// err = Pa_CloseStream(stream); -// Pa_Terminate(); + while (!terminated) { + AudioThreadInput inp; + inputQueue->pop(inp); + if (inp.data.size()) { + audio_queue.push(inp.data); + } + } + + try { + // Stop the stream + dac.stopStream(); + } catch (RtAudioError& e) { + e.printMessage(); + } + + if (dac.isStreamOpen()) { + dac.closeStream(); + } std::cout << "Audio thread done." << std::endl; } diff --git a/src/audio/AudioThread.h b/src/audio/AudioThread.h index bcf8289..fa98b49 100644 --- a/src/audio/AudioThread.h +++ b/src/audio/AudioThread.h @@ -28,7 +28,10 @@ typedef ThreadQueue AudioThreadInputQueue; class AudioThread { public: - AudioThread(AudioThreadInputQueue *inputQueue); + std::queue > audio_queue; + unsigned int audio_queue_ptr; + + AudioThread(AudioThreadInputQueue *inputQueue); ~AudioThread(); void threadMain(); @@ -36,8 +39,7 @@ public: private: AudioThreadInputQueue *inputQueue; -// PaStreamParameters outputParameters; -// PaStream *stream; + RtAudio dac; std::atomic terminated; }; From 6fe3cf98d3aa1b104b164fdfd6a4b57ef42190b8 Mon Sep 17 00:00:00 2001 From: "Charles J. Cliffe" Date: Thu, 4 Dec 2014 19:46:11 -0500 Subject: [PATCH 3/6] Remove portaudio deps --- external/portaudio/LICENSE.txt | 81 -- external/portaudio/include/pa_asio.h | 150 --- external/portaudio/include/pa_win_ds.h | 95 -- external/portaudio/include/pa_win_wasapi.h | 391 ------ .../portaudio/include/pa_win_waveformat.h | 199 --- external/portaudio/include/pa_win_wdmks.h | 106 -- external/portaudio/include/pa_win_wmme.h | 185 --- external/portaudio/include/portaudio.h | 1174 ----------------- external/portaudio/libs/32/portaudio_x86.dll | Bin 258560 -> 0 bytes external/portaudio/libs/32/portaudio_x86.lib | Bin 12966 -> 0 bytes .../libs/64/libportaudio_static_x86.a | Bin 112598 -> 0 bytes .../portaudio/libs/64/libportaudio_x86.dll | Bin 202053 -> 0 bytes .../portaudio/libs/64/libportaudio_x86.dll.a | Bin 23426 -> 0 bytes 13 files changed, 2381 deletions(-) delete mode 100644 external/portaudio/LICENSE.txt delete mode 100644 external/portaudio/include/pa_asio.h delete mode 100644 external/portaudio/include/pa_win_ds.h delete mode 100644 external/portaudio/include/pa_win_wasapi.h delete mode 100644 external/portaudio/include/pa_win_waveformat.h delete mode 100644 external/portaudio/include/pa_win_wdmks.h delete mode 100644 external/portaudio/include/pa_win_wmme.h delete mode 100644 external/portaudio/include/portaudio.h delete mode 100644 external/portaudio/libs/32/portaudio_x86.dll delete mode 100644 external/portaudio/libs/32/portaudio_x86.lib delete mode 100644 external/portaudio/libs/64/libportaudio_static_x86.a delete mode 100644 external/portaudio/libs/64/libportaudio_x86.dll delete mode 100644 external/portaudio/libs/64/libportaudio_x86.dll.a diff --git a/external/portaudio/LICENSE.txt b/external/portaudio/LICENSE.txt deleted file mode 100644 index e0ac4e8..0000000 --- a/external/portaudio/LICENSE.txt +++ /dev/null @@ -1,81 +0,0 @@ -Portable header file to contain: ->>>>> -/* - * PortAudio Portable Real-Time Audio Library - * PortAudio API Header File - * Latest version available at: http://www.portaudio.com - * - * Copyright (c) 1999-2006 Ross Bencina and Phil Burk - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files - * (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, - * publish, distribute, sublicense, and/or sell copies of the Software, - * and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * The text above constitutes the entire PortAudio license; however, - * the PortAudio community also makes the following non-binding requests: - * - * Any person wishing to distribute modifications to the Software is - * requested to send the modifications to the original developer so that - * they can be incorporated into the canonical version. It is also - * requested that these non-binding requests be included along with the - * license above. - */ -<<<<< - - -Implementation files to contain: ->>>>> -/* - * PortAudio Portable Real-Time Audio Library - * Latest version at: http://www.portaudio.com - * Implementation - * Copyright (c) 1999-2000 - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files - * (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, - * publish, distribute, sublicense, and/or sell copies of the Software, - * and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * The text above constitutes the entire PortAudio license; however, - * the PortAudio community also makes the following non-binding requests: - * - * Any person wishing to distribute modifications to the Software is - * requested to send the modifications to the original developer so that - * they can be incorporated into the canonical version. It is also - * requested that these non-binding requests be included along with the - * license above. - */ -<<<<< \ No newline at end of file diff --git a/external/portaudio/include/pa_asio.h b/external/portaudio/include/pa_asio.h deleted file mode 100644 index 8f4624e..0000000 --- a/external/portaudio/include/pa_asio.h +++ /dev/null @@ -1,150 +0,0 @@ -#ifndef PA_ASIO_H -#define PA_ASIO_H -/* - * $Id: pa_asio.h 1667 2011-05-02 15:49:20Z rossb $ - * PortAudio Portable Real-Time Audio Library - * ASIO specific extensions - * - * Copyright (c) 1999-2000 Ross Bencina and Phil Burk - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files - * (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, - * publish, distribute, sublicense, and/or sell copies of the Software, - * and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * The text above constitutes the entire PortAudio license; however, - * the PortAudio community also makes the following non-binding requests: - * - * Any person wishing to distribute modifications to the Software is - * requested to send the modifications to the original developer so that - * they can be incorporated into the canonical version. It is also - * requested that these non-binding requests be included along with the - * license above. - */ - - -/** @file - @ingroup public_header - @brief ASIO-specific PortAudio API extension header file. -*/ - -#include "portaudio.h" - -#ifdef __cplusplus -extern "C" -{ -#endif /* __cplusplus */ - - -/** Retrieve legal native buffer sizes for the specificed device, in sample frames. - - @param device The global index of the device about which the query is being made. - @param minBufferSizeFrames A pointer to the location which will receive the minimum buffer size value. - @param maxBufferSizeFrames A pointer to the location which will receive the maximum buffer size value. - @param preferredBufferSizeFrames A pointer to the location which will receive the preferred buffer size value. - @param granularity A pointer to the location which will receive the "granularity". This value determines - the step size used to compute the legal values between minBufferSizeFrames and maxBufferSizeFrames. - If granularity is -1 then available buffer size values are powers of two. - - @see ASIOGetBufferSize in the ASIO SDK. - - @note: this function used to be called PaAsio_GetAvailableLatencyValues. There is a - #define that maps PaAsio_GetAvailableLatencyValues to this function for backwards compatibility. -*/ -PaError PaAsio_GetAvailableBufferSizes( PaDeviceIndex device, - long *minBufferSizeFrames, long *maxBufferSizeFrames, long *preferredBufferSizeFrames, long *granularity ); - - -/** Backwards compatibility alias for PaAsio_GetAvailableBufferSizes - - @see PaAsio_GetAvailableBufferSizes -*/ -#define PaAsio_GetAvailableLatencyValues PaAsio_GetAvailableBufferSizes - - -/** Display the ASIO control panel for the specified device. - - @param device The global index of the device whose control panel is to be displayed. - @param systemSpecific On Windows, the calling application's main window handle, - on Macintosh this value should be zero. -*/ -PaError PaAsio_ShowControlPanel( PaDeviceIndex device, void* systemSpecific ); - - - - -/** Retrieve a pointer to a string containing the name of the specified - input channel. The string is valid until Pa_Terminate is called. - - The string will be no longer than 32 characters including the null terminator. -*/ -PaError PaAsio_GetInputChannelName( PaDeviceIndex device, int channelIndex, - const char** channelName ); - - -/** Retrieve a pointer to a string containing the name of the specified - input channel. The string is valid until Pa_Terminate is called. - - The string will be no longer than 32 characters including the null terminator. -*/ -PaError PaAsio_GetOutputChannelName( PaDeviceIndex device, int channelIndex, - const char** channelName ); - - -/** Set the sample rate of an open paASIO stream. - - @param stream The stream to operate on. - @param sampleRate The new sample rate. - - Note that this function may fail if the stream is alredy running and the - ASIO driver does not support switching the sample rate of a running stream. - - Returns paIncompatibleStreamHostApi if stream is not a paASIO stream. -*/ -PaError PaAsio_SetStreamSampleRate( PaStream* stream, double sampleRate ); - - -#define paAsioUseChannelSelectors (0x01) - -typedef struct PaAsioStreamInfo{ - unsigned long size; /**< sizeof(PaAsioStreamInfo) */ - PaHostApiTypeId hostApiType; /**< paASIO */ - unsigned long version; /**< 1 */ - - unsigned long flags; - - /* Support for opening only specific channels of an ASIO device. - If the paAsioUseChannelSelectors flag is set, channelSelectors is a - pointer to an array of integers specifying the device channels to use. - When used, the length of the channelSelectors array must match the - corresponding channelCount parameter to Pa_OpenStream() otherwise a - crash may result. - The values in the selectors array must specify channels within the - range of supported channels for the device or paInvalidChannelCount will - result. - */ - int *channelSelectors; -}PaAsioStreamInfo; - - -#ifdef __cplusplus -} -#endif /* __cplusplus */ - -#endif /* PA_ASIO_H */ diff --git a/external/portaudio/include/pa_win_ds.h b/external/portaudio/include/pa_win_ds.h deleted file mode 100644 index 5d38641..0000000 --- a/external/portaudio/include/pa_win_ds.h +++ /dev/null @@ -1,95 +0,0 @@ -#ifndef PA_WIN_DS_H -#define PA_WIN_DS_H -/* - * $Id: $ - * PortAudio Portable Real-Time Audio Library - * DirectSound specific extensions - * - * Copyright (c) 1999-2007 Ross Bencina and Phil Burk - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files - * (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, - * publish, distribute, sublicense, and/or sell copies of the Software, - * and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * The text above constitutes the entire PortAudio license; however, - * the PortAudio community also makes the following non-binding requests: - * - * Any person wishing to distribute modifications to the Software is - * requested to send the modifications to the original developer so that - * they can be incorporated into the canonical version. It is also - * requested that these non-binding requests be included along with the - * license above. - */ - -/** @file - @ingroup public_header - @brief DirectSound-specific PortAudio API extension header file. -*/ - -#include "portaudio.h" -#include "pa_win_waveformat.h" - -#ifdef __cplusplus -extern "C" -{ -#endif /* __cplusplus */ - - -#define paWinDirectSoundUseLowLevelLatencyParameters (0x01) -#define paWinDirectSoundUseChannelMask (0x04) - - -typedef struct PaWinDirectSoundStreamInfo{ - unsigned long size; /**< sizeof(PaWinDirectSoundStreamInfo) */ - PaHostApiTypeId hostApiType; /**< paDirectSound */ - unsigned long version; /**< 2 */ - - unsigned long flags; /**< enable other features of this struct */ - - /** - low-level latency setting support - Sets the size of the DirectSound host buffer. - When flags contains the paWinDirectSoundUseLowLevelLatencyParameters - this size will be used instead of interpreting the generic latency - parameters to Pa_OpenStream(). If the flag is not set this value is ignored. - - If the stream is a full duplex stream the implementation requires that - the values of framesPerBuffer for input and output match (if both are specified). - */ - unsigned long framesPerBuffer; - - /** - support for WAVEFORMATEXTENSIBLE channel masks. If flags contains - paWinDirectSoundUseChannelMask this allows you to specify which speakers - to address in a multichannel stream. Constants for channelMask - are specified in pa_win_waveformat.h - - */ - PaWinWaveFormatChannelMask channelMask; - -}PaWinDirectSoundStreamInfo; - - - -#ifdef __cplusplus -} -#endif /* __cplusplus */ - -#endif /* PA_WIN_DS_H */ diff --git a/external/portaudio/include/pa_win_wasapi.h b/external/portaudio/include/pa_win_wasapi.h deleted file mode 100644 index a60aa04..0000000 --- a/external/portaudio/include/pa_win_wasapi.h +++ /dev/null @@ -1,391 +0,0 @@ -#ifndef PA_WIN_WASAPI_H -#define PA_WIN_WASAPI_H -/* - * $Id: $ - * PortAudio Portable Real-Time Audio Library - * DirectSound specific extensions - * - * Copyright (c) 1999-2007 Ross Bencina and Phil Burk - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files - * (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, - * publish, distribute, sublicense, and/or sell copies of the Software, - * and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * The text above constitutes the entire PortAudio license; however, - * the PortAudio community also makes the following non-binding requests: - * - * Any person wishing to distribute modifications to the Software is - * requested to send the modifications to the original developer so that - * they can be incorporated into the canonical version. It is also - * requested that these non-binding requests be included along with the - * license above. - */ - -/** @file - @ingroup public_header - @brief WASAPI-specific PortAudio API extension header file. -*/ - -#include "portaudio.h" -#include "pa_win_waveformat.h" - -#ifdef __cplusplus -extern "C" -{ -#endif /* __cplusplus */ - - -/* Setup flags */ -typedef enum PaWasapiFlags -{ - /* puts WASAPI into exclusive mode */ - paWinWasapiExclusive = (1 << 0), - - /* allows to skip internal PA processing completely */ - paWinWasapiRedirectHostProcessor = (1 << 1), - - /* assigns custom channel mask */ - paWinWasapiUseChannelMask = (1 << 2), - - /* selects non-Event driven method of data read/write - Note: WASAPI Event driven core is capable of 2ms latency!!!, but Polling - method can only provide 15-20ms latency. */ - paWinWasapiPolling = (1 << 3), - - /* forces custom thread priority setting. must be used if PaWasapiStreamInfo::threadPriority - is set to custom value. */ - paWinWasapiThreadPriority = (1 << 4) -} -PaWasapiFlags; -#define paWinWasapiExclusive (paWinWasapiExclusive) -#define paWinWasapiRedirectHostProcessor (paWinWasapiRedirectHostProcessor) -#define paWinWasapiUseChannelMask (paWinWasapiUseChannelMask) -#define paWinWasapiPolling (paWinWasapiPolling) -#define paWinWasapiThreadPriority (paWinWasapiThreadPriority) - - -/* Host processor. Allows to skip internal PA processing completely. - You must set paWinWasapiRedirectHostProcessor flag to PaWasapiStreamInfo::flags member - in order to have host processor redirected to your callback. - Use with caution! inputFrames and outputFrames depend solely on final device setup. - To query maximal values of inputFrames/outputFrames use PaWasapi_GetFramesPerHostBuffer. -*/ -typedef void (*PaWasapiHostProcessorCallback) (void *inputBuffer, long inputFrames, - void *outputBuffer, long outputFrames, - void *userData); - -/* Device role */ -typedef enum PaWasapiDeviceRole -{ - eRoleRemoteNetworkDevice = 0, - eRoleSpeakers, - eRoleLineLevel, - eRoleHeadphones, - eRoleMicrophone, - eRoleHeadset, - eRoleHandset, - eRoleUnknownDigitalPassthrough, - eRoleSPDIF, - eRoleHDMI, - eRoleUnknownFormFactor -} -PaWasapiDeviceRole; - - -/* Jack connection type */ -typedef enum PaWasapiJackConnectionType -{ - eJackConnTypeUnknown, - eJackConnType3Point5mm, - eJackConnTypeQuarter, - eJackConnTypeAtapiInternal, - eJackConnTypeRCA, - eJackConnTypeOptical, - eJackConnTypeOtherDigital, - eJackConnTypeOtherAnalog, - eJackConnTypeMultichannelAnalogDIN, - eJackConnTypeXlrProfessional, - eJackConnTypeRJ11Modem, - eJackConnTypeCombination -} -PaWasapiJackConnectionType; - - -/* Jack geometric location */ -typedef enum PaWasapiJackGeoLocation -{ - eJackGeoLocUnk = 0, - eJackGeoLocRear = 0x1, /* matches EPcxGeoLocation::eGeoLocRear */ - eJackGeoLocFront, - eJackGeoLocLeft, - eJackGeoLocRight, - eJackGeoLocTop, - eJackGeoLocBottom, - eJackGeoLocRearPanel, - eJackGeoLocRiser, - eJackGeoLocInsideMobileLid, - eJackGeoLocDrivebay, - eJackGeoLocHDMI, - eJackGeoLocOutsideMobileLid, - eJackGeoLocATAPI, - eJackGeoLocReserved5, - eJackGeoLocReserved6, -} -PaWasapiJackGeoLocation; - - -/* Jack general location */ -typedef enum PaWasapiJackGenLocation -{ - eJackGenLocPrimaryBox = 0, - eJackGenLocInternal, - eJackGenLocSeparate, - eJackGenLocOther -} -PaWasapiJackGenLocation; - - -/* Jack's type of port */ -typedef enum PaWasapiJackPortConnection -{ - eJackPortConnJack = 0, - eJackPortConnIntegratedDevice, - eJackPortConnBothIntegratedAndJack, - eJackPortConnUnknown -} -PaWasapiJackPortConnection; - - -/* Thread priority */ -typedef enum PaWasapiThreadPriority -{ - eThreadPriorityNone = 0, - eThreadPriorityAudio, //!< Default for Shared mode. - eThreadPriorityCapture, - eThreadPriorityDistribution, - eThreadPriorityGames, - eThreadPriorityPlayback, - eThreadPriorityProAudio, //!< Default for Exclusive mode. - eThreadPriorityWindowManager -} -PaWasapiThreadPriority; - - -/* Stream descriptor. */ -typedef struct PaWasapiJackDescription -{ - unsigned long channelMapping; - unsigned long color; /* derived from macro: #define RGB(r,g,b) ((COLORREF)(((BYTE)(r)|((WORD)((BYTE)(g))<<8))|(((DWORD)(BYTE)(b))<<16))) */ - PaWasapiJackConnectionType connectionType; - PaWasapiJackGeoLocation geoLocation; - PaWasapiJackGenLocation genLocation; - PaWasapiJackPortConnection portConnection; - unsigned int isConnected; -} -PaWasapiJackDescription; - - -/* Stream descriptor. */ -typedef struct PaWasapiStreamInfo -{ - unsigned long size; /**< sizeof(PaWasapiStreamInfo) */ - PaHostApiTypeId hostApiType; /**< paWASAPI */ - unsigned long version; /**< 1 */ - - unsigned long flags; /**< collection of PaWasapiFlags */ - - /* Support for WAVEFORMATEXTENSIBLE channel masks. If flags contains - paWinWasapiUseChannelMask this allows you to specify which speakers - to address in a multichannel stream. Constants for channelMask - are specified in pa_win_waveformat.h. Will be used only if - paWinWasapiUseChannelMask flag is specified. - */ - PaWinWaveFormatChannelMask channelMask; - - /* Delivers raw data to callback obtained from GetBuffer() methods skipping - internal PortAudio processing inventory completely. userData parameter will - be the same that was passed to Pa_OpenStream method. Will be used only if - paWinWasapiRedirectHostProcessor flag is specified. - */ - PaWasapiHostProcessorCallback hostProcessorOutput; - PaWasapiHostProcessorCallback hostProcessorInput; - - /* Specifies thread priority explicitly. Will be used only if paWinWasapiThreadPriority flag - is specified. - - Please note, if Input/Output streams are opened simultaniously (Full-Duplex mode) - you shall specify same value for threadPriority or othervise one of the values will be used - to setup thread priority. - */ - PaWasapiThreadPriority threadPriority; -} -PaWasapiStreamInfo; - - -/** Returns default sound format for device. Format is represented by PaWinWaveFormat or - WAVEFORMATEXTENSIBLE structure. - - @param pFormat Pointer to PaWinWaveFormat or WAVEFORMATEXTENSIBLE structure. - @param nFormatSize Size of PaWinWaveFormat or WAVEFORMATEXTENSIBLE structure in bytes. - @param nDevice Device index. - - @return Non-negative value indicating the number of bytes copied into format decriptor - or, a PaErrorCode (which are always negative) if PortAudio is not initialized - or an error is encountered. -*/ -int PaWasapi_GetDeviceDefaultFormat( void *pFormat, unsigned int nFormatSize, PaDeviceIndex nDevice ); - - -/** Returns device role (PaWasapiDeviceRole enum). - - @param nDevice device index. - - @return Non-negative value indicating device role or, a PaErrorCode (which are always negative) - if PortAudio is not initialized or an error is encountered. -*/ -int/*PaWasapiDeviceRole*/ PaWasapi_GetDeviceRole( PaDeviceIndex nDevice ); - - -/** Boost thread priority of calling thread (MMCSS). Use it for Blocking Interface only for thread - which makes calls to Pa_WriteStream/Pa_ReadStream. - - @param hTask Handle to pointer to priority task. Must be used with PaWasapi_RevertThreadPriority - method to revert thread priority to initial state. - - @param nPriorityClass Id of thread priority of PaWasapiThreadPriority type. Specifying - eThreadPriorityNone does nothing. - - @return Error code indicating success or failure. - @see PaWasapi_RevertThreadPriority -*/ -PaError PaWasapi_ThreadPriorityBoost( void **hTask, PaWasapiThreadPriority nPriorityClass ); - - -/** Boost thread priority of calling thread (MMCSS). Use it for Blocking Interface only for thread - which makes calls to Pa_WriteStream/Pa_ReadStream. - - @param hTask Task handle obtained by PaWasapi_BoostThreadPriority method. - @return Error code indicating success or failure. - @see PaWasapi_BoostThreadPriority -*/ -PaError PaWasapi_ThreadPriorityRevert( void *hTask ); - - -/** Get number of frames per host buffer. This is maximal value of frames of WASAPI buffer which - can be locked for operations. Use this method as helper to findout maximal values of - inputFrames/outputFrames of PaWasapiHostProcessorCallback. - - @param pStream Pointer to PaStream to query. - @param nInput Pointer to variable to receive number of input frames. Can be NULL. - @param nOutput Pointer to variable to receive number of output frames. Can be NULL. - @return Error code indicating success or failure. - @see PaWasapiHostProcessorCallback -*/ -PaError PaWasapi_GetFramesPerHostBuffer( PaStream *pStream, unsigned int *nInput, unsigned int *nOutput ); - - -/** Get number of jacks associated with a WASAPI device. Use this method to determine if - there are any jacks associated with the provided WASAPI device. Not all audio devices - will support this capability. This is valid for both input and output devices. - @param nDevice device index. - @param jcount Number of jacks is returned in this variable - @return Error code indicating success or failure - @see PaWasapi_GetJackDescription - */ -PaError PaWasapi_GetJackCount(PaDeviceIndex nDevice, int *jcount); - - -/** Get the jack description associated with a WASAPI device and jack number - Before this function is called, use PaWasapi_GetJackCount to determine the - number of jacks associated with device. If jcount is greater than zero, then - each jack from 0 to jcount can be queried with this function to get the jack - description. - @param nDevice device index. - @param jindex Which jack to return information - @param KSJACK_DESCRIPTION This structure filled in on success. - @return Error code indicating success or failure - @see PaWasapi_GetJackCount - */ -PaError PaWasapi_GetJackDescription(PaDeviceIndex nDevice, int jindex, PaWasapiJackDescription *pJackDescription); - - -/* - IMPORTANT: - - WASAPI is implemented for Callback and Blocking interfaces. It supports Shared and Exclusive - share modes. - - Exclusive Mode: - - Exclusive mode allows to deliver audio data directly to hardware bypassing - software mixing. - Exclusive mode is specified by 'paWinWasapiExclusive' flag. - - Callback Interface: - - Provides best audio quality with low latency. Callback interface is implemented in - two versions: - - 1) Event-Driven: - This is the most powerful WASAPI implementation which provides glitch-free - audio at around 3ms latency in Exclusive mode. Lowest possible latency for this mode is - 3 ms for HD Audio class audio chips. For the Shared mode latency can not be - lower than 20 ms. - - 2) Poll-Driven: - Polling is another 2-nd method to operate with WASAPI. It is less efficient than Event-Driven - and provides latency at around 10-13ms. Polling must be used to overcome a system bug - under Windows Vista x64 when application is WOW64(32-bit) and Event-Driven method simply - times out (event handle is never signalled on buffer completion). Please note, such WOW64 bug - does not exist in Vista x86 or Windows 7. - Polling can be setup by speciying 'paWinWasapiPolling' flag. Our WASAPI implementation detects - WOW64 bug and sets 'paWinWasapiPolling' automatically. - - Thread priority: - - Normally thread priority is set automatically and does not require modification. Although - if user wants some tweaking thread priority can be modified by setting 'paWinWasapiThreadPriority' - flag and specifying 'PaWasapiStreamInfo::threadPriority' with value from PaWasapiThreadPriority - enum. - - Blocking Interface: - - Blocking interface is implemented but due to above described Poll-Driven method can not - deliver lowest possible latency. Specifying too low latency in Shared mode will result in - distorted audio although Exclusive mode adds stability. - - Pa_IsFormatSupported: - - To check format with correct Share Mode (Exclusive/Shared) you must supply - PaWasapiStreamInfo with flags paWinWasapiExclusive set through member of - PaStreamParameters::hostApiSpecificStreamInfo structure. - - Pa_OpenStream: - - To set desired Share Mode (Exclusive/Shared) you must supply - PaWasapiStreamInfo with flags paWinWasapiExclusive set through member of - PaStreamParameters::hostApiSpecificStreamInfo structure. -*/ - -#ifdef __cplusplus -} -#endif /* __cplusplus */ - -#endif /* PA_WIN_WASAPI_H */ diff --git a/external/portaudio/include/pa_win_waveformat.h b/external/portaudio/include/pa_win_waveformat.h deleted file mode 100644 index 2c00267..0000000 --- a/external/portaudio/include/pa_win_waveformat.h +++ /dev/null @@ -1,199 +0,0 @@ -#ifndef PA_WIN_WAVEFORMAT_H -#define PA_WIN_WAVEFORMAT_H - -/* - * PortAudio Portable Real-Time Audio Library - * Windows WAVEFORMAT* data structure utilities - * portaudio.h should be included before this file. - * - * Copyright (c) 2007 Ross Bencina - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files - * (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, - * publish, distribute, sublicense, and/or sell copies of the Software, - * and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * The text above constitutes the entire PortAudio license; however, - * the PortAudio community also makes the following non-binding requests: - * - * Any person wishing to distribute modifications to the Software is - * requested to send the modifications to the original developer so that - * they can be incorporated into the canonical version. It is also - * requested that these non-binding requests be included along with the - * license above. - */ - -/** @file - @ingroup public_header - @brief Windows specific PortAudio API extension and utilities header file. -*/ - -#ifdef __cplusplus -extern "C" { -#endif - -/* - The following #defines for speaker channel masks are the same - as those in ksmedia.h, except with PAWIN_ prepended, KSAUDIO_ removed - in some cases, and casts to PaWinWaveFormatChannelMask added. -*/ - -typedef unsigned long PaWinWaveFormatChannelMask; - -/* Speaker Positions: */ -#define PAWIN_SPEAKER_FRONT_LEFT ((PaWinWaveFormatChannelMask)0x1) -#define PAWIN_SPEAKER_FRONT_RIGHT ((PaWinWaveFormatChannelMask)0x2) -#define PAWIN_SPEAKER_FRONT_CENTER ((PaWinWaveFormatChannelMask)0x4) -#define PAWIN_SPEAKER_LOW_FREQUENCY ((PaWinWaveFormatChannelMask)0x8) -#define PAWIN_SPEAKER_BACK_LEFT ((PaWinWaveFormatChannelMask)0x10) -#define PAWIN_SPEAKER_BACK_RIGHT ((PaWinWaveFormatChannelMask)0x20) -#define PAWIN_SPEAKER_FRONT_LEFT_OF_CENTER ((PaWinWaveFormatChannelMask)0x40) -#define PAWIN_SPEAKER_FRONT_RIGHT_OF_CENTER ((PaWinWaveFormatChannelMask)0x80) -#define PAWIN_SPEAKER_BACK_CENTER ((PaWinWaveFormatChannelMask)0x100) -#define PAWIN_SPEAKER_SIDE_LEFT ((PaWinWaveFormatChannelMask)0x200) -#define PAWIN_SPEAKER_SIDE_RIGHT ((PaWinWaveFormatChannelMask)0x400) -#define PAWIN_SPEAKER_TOP_CENTER ((PaWinWaveFormatChannelMask)0x800) -#define PAWIN_SPEAKER_TOP_FRONT_LEFT ((PaWinWaveFormatChannelMask)0x1000) -#define PAWIN_SPEAKER_TOP_FRONT_CENTER ((PaWinWaveFormatChannelMask)0x2000) -#define PAWIN_SPEAKER_TOP_FRONT_RIGHT ((PaWinWaveFormatChannelMask)0x4000) -#define PAWIN_SPEAKER_TOP_BACK_LEFT ((PaWinWaveFormatChannelMask)0x8000) -#define PAWIN_SPEAKER_TOP_BACK_CENTER ((PaWinWaveFormatChannelMask)0x10000) -#define PAWIN_SPEAKER_TOP_BACK_RIGHT ((PaWinWaveFormatChannelMask)0x20000) - -/* Bit mask locations reserved for future use */ -#define PAWIN_SPEAKER_RESERVED ((PaWinWaveFormatChannelMask)0x7FFC0000) - -/* Used to specify that any possible permutation of speaker configurations */ -#define PAWIN_SPEAKER_ALL ((PaWinWaveFormatChannelMask)0x80000000) - -/* DirectSound Speaker Config */ -#define PAWIN_SPEAKER_DIRECTOUT 0 -#define PAWIN_SPEAKER_MONO (PAWIN_SPEAKER_FRONT_CENTER) -#define PAWIN_SPEAKER_STEREO (PAWIN_SPEAKER_FRONT_LEFT | PAWIN_SPEAKER_FRONT_RIGHT) -#define PAWIN_SPEAKER_QUAD (PAWIN_SPEAKER_FRONT_LEFT | PAWIN_SPEAKER_FRONT_RIGHT | \ - PAWIN_SPEAKER_BACK_LEFT | PAWIN_SPEAKER_BACK_RIGHT) -#define PAWIN_SPEAKER_SURROUND (PAWIN_SPEAKER_FRONT_LEFT | PAWIN_SPEAKER_FRONT_RIGHT | \ - PAWIN_SPEAKER_FRONT_CENTER | PAWIN_SPEAKER_BACK_CENTER) -#define PAWIN_SPEAKER_5POINT1 (PAWIN_SPEAKER_FRONT_LEFT | PAWIN_SPEAKER_FRONT_RIGHT | \ - PAWIN_SPEAKER_FRONT_CENTER | PAWIN_SPEAKER_LOW_FREQUENCY | \ - PAWIN_SPEAKER_BACK_LEFT | PAWIN_SPEAKER_BACK_RIGHT) -#define PAWIN_SPEAKER_7POINT1 (PAWIN_SPEAKER_FRONT_LEFT | PAWIN_SPEAKER_FRONT_RIGHT | \ - PAWIN_SPEAKER_FRONT_CENTER | PAWIN_SPEAKER_LOW_FREQUENCY | \ - PAWIN_SPEAKER_BACK_LEFT | PAWIN_SPEAKER_BACK_RIGHT | \ - PAWIN_SPEAKER_FRONT_LEFT_OF_CENTER | PAWIN_SPEAKER_FRONT_RIGHT_OF_CENTER) -#define PAWIN_SPEAKER_5POINT1_SURROUND (PAWIN_SPEAKER_FRONT_LEFT | PAWIN_SPEAKER_FRONT_RIGHT | \ - PAWIN_SPEAKER_FRONT_CENTER | PAWIN_SPEAKER_LOW_FREQUENCY | \ - PAWIN_SPEAKER_SIDE_LEFT | PAWIN_SPEAKER_SIDE_RIGHT) -#define PAWIN_SPEAKER_7POINT1_SURROUND (PAWIN_SPEAKER_FRONT_LEFT | PAWIN_SPEAKER_FRONT_RIGHT | \ - PAWIN_SPEAKER_FRONT_CENTER | PAWIN_SPEAKER_LOW_FREQUENCY | \ - PAWIN_SPEAKER_BACK_LEFT | PAWIN_SPEAKER_BACK_RIGHT | \ - PAWIN_SPEAKER_SIDE_LEFT | PAWIN_SPEAKER_SIDE_RIGHT) -/* - According to the Microsoft documentation: - The following are obsolete 5.1 and 7.1 settings (they lack side speakers). Note this means - that the default 5.1 and 7.1 settings (KSAUDIO_SPEAKER_5POINT1 and KSAUDIO_SPEAKER_7POINT1 are - similarly obsolete but are unchanged for compatibility reasons). -*/ -#define PAWIN_SPEAKER_5POINT1_BACK PAWIN_SPEAKER_5POINT1 -#define PAWIN_SPEAKER_7POINT1_WIDE PAWIN_SPEAKER_7POINT1 - -/* DVD Speaker Positions */ -#define PAWIN_SPEAKER_GROUND_FRONT_LEFT PAWIN_SPEAKER_FRONT_LEFT -#define PAWIN_SPEAKER_GROUND_FRONT_CENTER PAWIN_SPEAKER_FRONT_CENTER -#define PAWIN_SPEAKER_GROUND_FRONT_RIGHT PAWIN_SPEAKER_FRONT_RIGHT -#define PAWIN_SPEAKER_GROUND_REAR_LEFT PAWIN_SPEAKER_BACK_LEFT -#define PAWIN_SPEAKER_GROUND_REAR_RIGHT PAWIN_SPEAKER_BACK_RIGHT -#define PAWIN_SPEAKER_TOP_MIDDLE PAWIN_SPEAKER_TOP_CENTER -#define PAWIN_SPEAKER_SUPER_WOOFER PAWIN_SPEAKER_LOW_FREQUENCY - - -/* - PaWinWaveFormat is defined here to provide compatibility with - compilation environments which don't have headers defining - WAVEFORMATEXTENSIBLE (e.g. older versions of MSVC, Borland C++ etc. - - The fields for WAVEFORMATEX and WAVEFORMATEXTENSIBLE are declared as an - unsigned char array here to avoid clients who include this file having - a dependency on windows.h and mmsystem.h, and also to to avoid having - to write separate packing pragmas for each compiler. -*/ -#define PAWIN_SIZEOF_WAVEFORMATEX 18 -#define PAWIN_SIZEOF_WAVEFORMATEXTENSIBLE (PAWIN_SIZEOF_WAVEFORMATEX + 22) - -typedef struct{ - unsigned char fields[ PAWIN_SIZEOF_WAVEFORMATEXTENSIBLE ]; - unsigned long extraLongForAlignment; /* ensure that compiler aligns struct to DWORD */ -} PaWinWaveFormat; - -/* - WAVEFORMATEXTENSIBLE fields: - - union { - WORD wValidBitsPerSample; - WORD wSamplesPerBlock; - WORD wReserved; - } Samples; - DWORD dwChannelMask; - GUID SubFormat; -*/ - -#define PAWIN_INDEXOF_WVALIDBITSPERSAMPLE (PAWIN_SIZEOF_WAVEFORMATEX+0) -#define PAWIN_INDEXOF_DWCHANNELMASK (PAWIN_SIZEOF_WAVEFORMATEX+2) -#define PAWIN_INDEXOF_SUBFORMAT (PAWIN_SIZEOF_WAVEFORMATEX+6) - - -/* - Valid values to pass for the waveFormatTag PaWin_InitializeWaveFormatEx and - PaWin_InitializeWaveFormatExtensible functions below. These must match - the standard Windows WAVE_FORMAT_* values. -*/ -#define PAWIN_WAVE_FORMAT_PCM (1) -#define PAWIN_WAVE_FORMAT_IEEE_FLOAT (3) -#define PAWIN_WAVE_FORMAT_DOLBY_AC3_SPDIF (0x0092) -#define PAWIN_WAVE_FORMAT_WMA_SPDIF (0x0164) - - -/* - returns PAWIN_WAVE_FORMAT_PCM or PAWIN_WAVE_FORMAT_IEEE_FLOAT - depending on the sampleFormat parameter. -*/ -int PaWin_SampleFormatToLinearWaveFormatTag( PaSampleFormat sampleFormat ); - -/* - Use the following two functions to initialize the waveformat structure. -*/ - -void PaWin_InitializeWaveFormatEx( PaWinWaveFormat *waveFormat, - int numChannels, PaSampleFormat sampleFormat, int waveFormatTag, double sampleRate ); - - -void PaWin_InitializeWaveFormatExtensible( PaWinWaveFormat *waveFormat, - int numChannels, PaSampleFormat sampleFormat, int waveFormatTag, double sampleRate, - PaWinWaveFormatChannelMask channelMask ); - - -/* Map a channel count to a speaker channel mask */ -PaWinWaveFormatChannelMask PaWin_DefaultChannelMask( int numChannels ); - - -#ifdef __cplusplus -} -#endif /* __cplusplus */ - -#endif /* PA_WIN_WAVEFORMAT_H */ \ No newline at end of file diff --git a/external/portaudio/include/pa_win_wdmks.h b/external/portaudio/include/pa_win_wdmks.h deleted file mode 100644 index 9fe9284..0000000 --- a/external/portaudio/include/pa_win_wdmks.h +++ /dev/null @@ -1,106 +0,0 @@ -#ifndef PA_WIN_WDMKS_H -#define PA_WIN_WDMKS_H -/* - * $Id: pa_win_wdmks.h 1812 2012-02-14 09:32:57Z robiwan $ - * PortAudio Portable Real-Time Audio Library - * WDM/KS specific extensions - * - * Copyright (c) 1999-2007 Ross Bencina and Phil Burk - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files - * (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, - * publish, distribute, sublicense, and/or sell copies of the Software, - * and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * The text above constitutes the entire PortAudio license; however, - * the PortAudio community also makes the following non-binding requests: - * - * Any person wishing to distribute modifications to the Software is - * requested to send the modifications to the original developer so that - * they can be incorporated into the canonical version. It is also - * requested that these non-binding requests be included along with the - * license above. - */ - -/** @file - @ingroup public_header - @brief WDM Kernel Streaming-specific PortAudio API extension header file. -*/ - - -#include "portaudio.h" - -#include - -#ifdef __cplusplus -extern "C" -{ -#endif /* __cplusplus */ - typedef struct PaWinWDMKSInfo{ - unsigned long size; /**< sizeof(PaWinWDMKSInfo) */ - PaHostApiTypeId hostApiType; /**< paWDMKS */ - unsigned long version; /**< 1 */ - - /* The number of packets to use for WaveCyclic devices, range is [2, 8]. Set to zero for default value of 2. */ - unsigned noOfPackets; - } PaWinWDMKSInfo; - - typedef enum PaWDMKSType - { - Type_kNotUsed, - Type_kWaveCyclic, - Type_kWaveRT, - Type_kCnt, - } PaWDMKSType; - - typedef enum PaWDMKSSubType - { - SubType_kUnknown, - SubType_kNotification, - SubType_kPolled, - SubType_kCnt, - } PaWDMKSSubType; - - typedef struct PaWinWDMKSDeviceInfo { - wchar_t filterPath[MAX_PATH]; /**< KS filter path in Unicode! */ - wchar_t topologyPath[MAX_PATH]; /**< Topology filter path in Unicode! */ - PaWDMKSType streamingType; - GUID deviceProductGuid; /**< The product GUID of the device (if supported) */ - } PaWinWDMKSDeviceInfo; - - typedef struct PaWDMKSDirectionSpecificStreamInfo - { - PaDeviceIndex device; - unsigned channels; /**< No of channels the device is opened with */ - unsigned framesPerHostBuffer; /**< No of frames of the device buffer */ - int endpointPinId; /**< Endpoint pin ID (on topology filter if topologyName is not empty) */ - int muxNodeId; /**< Only valid for input */ - PaWDMKSSubType streamingSubType; /**< Not known until device is opened for streaming */ - } PaWDMKSDirectionSpecificStreamInfo; - - typedef struct PaWDMKSSpecificStreamInfo { - PaWDMKSDirectionSpecificStreamInfo input; - PaWDMKSDirectionSpecificStreamInfo output; - } PaWDMKSSpecificStreamInfo; - -#ifdef __cplusplus -} -#endif /* __cplusplus */ - -#endif /* PA_WIN_DS_H */ diff --git a/external/portaudio/include/pa_win_wmme.h b/external/portaudio/include/pa_win_wmme.h deleted file mode 100644 index ac5efe7..0000000 --- a/external/portaudio/include/pa_win_wmme.h +++ /dev/null @@ -1,185 +0,0 @@ -#ifndef PA_WIN_WMME_H -#define PA_WIN_WMME_H -/* - * $Id: pa_win_wmme.h 1592 2011-02-04 10:41:58Z rossb $ - * PortAudio Portable Real-Time Audio Library - * MME specific extensions - * - * Copyright (c) 1999-2000 Ross Bencina and Phil Burk - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files - * (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, - * publish, distribute, sublicense, and/or sell copies of the Software, - * and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * The text above constitutes the entire PortAudio license; however, - * the PortAudio community also makes the following non-binding requests: - * - * Any person wishing to distribute modifications to the Software is - * requested to send the modifications to the original developer so that - * they can be incorporated into the canonical version. It is also - * requested that these non-binding requests be included along with the - * license above. - */ - -/** @file - @ingroup public_header - @brief WMME-specific PortAudio API extension header file. -*/ - -#include "portaudio.h" -#include "pa_win_waveformat.h" - -#ifdef __cplusplus -extern "C" -{ -#endif /* __cplusplus */ - - -/* The following are flags which can be set in - PaWinMmeStreamInfo's flags field. -*/ - -#define paWinMmeUseLowLevelLatencyParameters (0x01) -#define paWinMmeUseMultipleDevices (0x02) /* use mme specific multiple device feature */ -#define paWinMmeUseChannelMask (0x04) - -/* By default, the mme implementation drops the processing thread's priority - to THREAD_PRIORITY_NORMAL and sleeps the thread if the CPU load exceeds 100% - This flag disables any priority throttling. The processing thread will always - run at THREAD_PRIORITY_TIME_CRITICAL. -*/ -#define paWinMmeDontThrottleOverloadedProcessingThread (0x08) - -/* Flags for non-PCM spdif passthrough. -*/ -#define paWinMmeWaveFormatDolbyAc3Spdif (0x10) -#define paWinMmeWaveFormatWmaSpdif (0x20) - - -typedef struct PaWinMmeDeviceAndChannelCount{ - PaDeviceIndex device; - int channelCount; -}PaWinMmeDeviceAndChannelCount; - - -typedef struct PaWinMmeStreamInfo{ - unsigned long size; /**< sizeof(PaWinMmeStreamInfo) */ - PaHostApiTypeId hostApiType; /**< paMME */ - unsigned long version; /**< 1 */ - - unsigned long flags; - - /* low-level latency setting support - These settings control the number and size of host buffers in order - to set latency. They will be used instead of the generic parameters - to Pa_OpenStream() if flags contains the PaWinMmeUseLowLevelLatencyParameters - flag. - - If PaWinMmeStreamInfo structures with PaWinMmeUseLowLevelLatencyParameters - are supplied for both input and output in a full duplex stream, then the - input and output framesPerBuffer must be the same, or the larger of the - two must be a multiple of the smaller, otherwise a - paIncompatibleHostApiSpecificStreamInfo error will be returned from - Pa_OpenStream(). - */ - unsigned long framesPerBuffer; - unsigned long bufferCount; /* formerly numBuffers */ - - /* multiple devices per direction support - If flags contains the PaWinMmeUseMultipleDevices flag, - this functionality will be used, otherwise the device parameter to - Pa_OpenStream() will be used instead. - If devices are specified here, the corresponding device parameter - to Pa_OpenStream() should be set to paUseHostApiSpecificDeviceSpecification, - otherwise an paInvalidDevice error will result. - The total number of channels accross all specified devices - must agree with the corresponding channelCount parameter to - Pa_OpenStream() otherwise a paInvalidChannelCount error will result. - */ - PaWinMmeDeviceAndChannelCount *devices; - unsigned long deviceCount; - - /* - support for WAVEFORMATEXTENSIBLE channel masks. If flags contains - paWinMmeUseChannelMask this allows you to specify which speakers - to address in a multichannel stream. Constants for channelMask - are specified in pa_win_waveformat.h - - */ - PaWinWaveFormatChannelMask channelMask; - -}PaWinMmeStreamInfo; - - -/** Retrieve the number of wave in handles used by a PortAudio WinMME stream. - Returns zero if the stream is output only. - - @return A non-negative value indicating the number of wave in handles - or, a PaErrorCode (which are always negative) if PortAudio is not initialized - or an error is encountered. - - @see PaWinMME_GetStreamInputHandle -*/ -int PaWinMME_GetStreamInputHandleCount( PaStream* stream ); - - -/** Retrieve a wave in handle used by a PortAudio WinMME stream. - - @param stream The stream to query. - @param handleIndex The zero based index of the wave in handle to retrieve. This - should be in the range [0, PaWinMME_GetStreamInputHandleCount(stream)-1]. - - @return A valid wave in handle, or NULL if an error occurred. - - @see PaWinMME_GetStreamInputHandle -*/ -HWAVEIN PaWinMME_GetStreamInputHandle( PaStream* stream, int handleIndex ); - - -/** Retrieve the number of wave out handles used by a PortAudio WinMME stream. - Returns zero if the stream is input only. - - @return A non-negative value indicating the number of wave out handles - or, a PaErrorCode (which are always negative) if PortAudio is not initialized - or an error is encountered. - - @see PaWinMME_GetStreamOutputHandle -*/ -int PaWinMME_GetStreamOutputHandleCount( PaStream* stream ); - - -/** Retrieve a wave out handle used by a PortAudio WinMME stream. - - @param stream The stream to query. - @param handleIndex The zero based index of the wave out handle to retrieve. - This should be in the range [0, PaWinMME_GetStreamOutputHandleCount(stream)-1]. - - @return A valid wave out handle, or NULL if an error occurred. - - @see PaWinMME_GetStreamOutputHandleCount -*/ -HWAVEOUT PaWinMME_GetStreamOutputHandle( PaStream* stream, int handleIndex ); - - -#ifdef __cplusplus -} -#endif /* __cplusplus */ - -#endif /* PA_WIN_WMME_H */ diff --git a/external/portaudio/include/portaudio.h b/external/portaudio/include/portaudio.h deleted file mode 100644 index 5e11dad..0000000 --- a/external/portaudio/include/portaudio.h +++ /dev/null @@ -1,1174 +0,0 @@ -#ifndef PORTAUDIO_H -#define PORTAUDIO_H -/* - * $Id: portaudio.h 1859 2012-09-01 00:10:13Z philburk $ - * PortAudio Portable Real-Time Audio Library - * PortAudio API Header File - * Latest version available at: http://www.portaudio.com/ - * - * Copyright (c) 1999-2002 Ross Bencina and Phil Burk - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files - * (the "Software"), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, - * publish, distribute, sublicense, and/or sell copies of the Software, - * and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * The text above constitutes the entire PortAudio license; however, - * the PortAudio community also makes the following non-binding requests: - * - * Any person wishing to distribute modifications to the Software is - * requested to send the modifications to the original developer so that - * they can be incorporated into the canonical version. It is also - * requested that these non-binding requests be included along with the - * license above. - */ - -/** @file - @ingroup public_header - @brief The portable PortAudio API. -*/ - - -#ifdef __cplusplus -extern "C" -{ -#endif /* __cplusplus */ - - -/** Retrieve the release number of the currently running PortAudio build, - eg 1900. -*/ -int Pa_GetVersion( void ); - - -/** Retrieve a textual description of the current PortAudio build, - eg "PortAudio V19-devel 13 October 2002". -*/ -const char* Pa_GetVersionText( void ); - - -/** Error codes returned by PortAudio functions. - Note that with the exception of paNoError, all PaErrorCodes are negative. -*/ - -typedef int PaError; -typedef enum PaErrorCode -{ - paNoError = 0, - - paNotInitialized = -10000, - paUnanticipatedHostError, - paInvalidChannelCount, - paInvalidSampleRate, - paInvalidDevice, - paInvalidFlag, - paSampleFormatNotSupported, - paBadIODeviceCombination, - paInsufficientMemory, - paBufferTooBig, - paBufferTooSmall, - paNullCallback, - paBadStreamPtr, - paTimedOut, - paInternalError, - paDeviceUnavailable, - paIncompatibleHostApiSpecificStreamInfo, - paStreamIsStopped, - paStreamIsNotStopped, - paInputOverflowed, - paOutputUnderflowed, - paHostApiNotFound, - paInvalidHostApi, - paCanNotReadFromACallbackStream, - paCanNotWriteToACallbackStream, - paCanNotReadFromAnOutputOnlyStream, - paCanNotWriteToAnInputOnlyStream, - paIncompatibleStreamHostApi, - paBadBufferPtr -} PaErrorCode; - - -/** Translate the supplied PortAudio error code into a human readable - message. -*/ -const char *Pa_GetErrorText( PaError errorCode ); - - -/** Library initialization function - call this before using PortAudio. - This function initializes internal data structures and prepares underlying - host APIs for use. With the exception of Pa_GetVersion(), Pa_GetVersionText(), - and Pa_GetErrorText(), this function MUST be called before using any other - PortAudio API functions. - - If Pa_Initialize() is called multiple times, each successful - call must be matched with a corresponding call to Pa_Terminate(). - Pairs of calls to Pa_Initialize()/Pa_Terminate() may overlap, and are not - required to be fully nested. - - Note that if Pa_Initialize() returns an error code, Pa_Terminate() should - NOT be called. - - @return paNoError if successful, otherwise an error code indicating the cause - of failure. - - @see Pa_Terminate -*/ -PaError Pa_Initialize( void ); - - -/** Library termination function - call this when finished using PortAudio. - This function deallocates all resources allocated by PortAudio since it was - initialized by a call to Pa_Initialize(). In cases where Pa_Initialise() has - been called multiple times, each call must be matched with a corresponding call - to Pa_Terminate(). The final matching call to Pa_Terminate() will automatically - close any PortAudio streams that are still open. - - Pa_Terminate() MUST be called before exiting a program which uses PortAudio. - Failure to do so may result in serious resource leaks, such as audio devices - not being available until the next reboot. - - @return paNoError if successful, otherwise an error code indicating the cause - of failure. - - @see Pa_Initialize -*/ -PaError Pa_Terminate( void ); - - - -/** The type used to refer to audio devices. Values of this type usually - range from 0 to (Pa_GetDeviceCount()-1), and may also take on the PaNoDevice - and paUseHostApiSpecificDeviceSpecification values. - - @see Pa_GetDeviceCount, paNoDevice, paUseHostApiSpecificDeviceSpecification -*/ -typedef int PaDeviceIndex; - - -/** A special PaDeviceIndex value indicating that no device is available, - or should be used. - - @see PaDeviceIndex -*/ -#define paNoDevice ((PaDeviceIndex)-1) - - -/** A special PaDeviceIndex value indicating that the device(s) to be used - are specified in the host api specific stream info structure. - - @see PaDeviceIndex -*/ -#define paUseHostApiSpecificDeviceSpecification ((PaDeviceIndex)-2) - - -/* Host API enumeration mechanism */ - -/** The type used to enumerate to host APIs at runtime. Values of this type - range from 0 to (Pa_GetHostApiCount()-1). - - @see Pa_GetHostApiCount -*/ -typedef int PaHostApiIndex; - - -/** Retrieve the number of available host APIs. Even if a host API is - available it may have no devices available. - - @return A non-negative value indicating the number of available host APIs - or, a PaErrorCode (which are always negative) if PortAudio is not initialized - or an error is encountered. - - @see PaHostApiIndex -*/ -PaHostApiIndex Pa_GetHostApiCount( void ); - - -/** Retrieve the index of the default host API. The default host API will be - the lowest common denominator host API on the current platform and is - unlikely to provide the best performance. - - @return A non-negative value ranging from 0 to (Pa_GetHostApiCount()-1) - indicating the default host API index or, a PaErrorCode (which are always - negative) if PortAudio is not initialized or an error is encountered. -*/ -PaHostApiIndex Pa_GetDefaultHostApi( void ); - - -/** Unchanging unique identifiers for each supported host API. This type - is used in the PaHostApiInfo structure. The values are guaranteed to be - unique and to never change, thus allowing code to be written that - conditionally uses host API specific extensions. - - New type ids will be allocated when support for a host API reaches - "public alpha" status, prior to that developers should use the - paInDevelopment type id. - - @see PaHostApiInfo -*/ -typedef enum PaHostApiTypeId -{ - paInDevelopment=0, /* use while developing support for a new host API */ - paDirectSound=1, - paMME=2, - paASIO=3, - paSoundManager=4, - paCoreAudio=5, - paOSS=7, - paALSA=8, - paAL=9, - paBeOS=10, - paWDMKS=11, - paJACK=12, - paWASAPI=13, - paAudioScienceHPI=14 -} PaHostApiTypeId; - - -/** A structure containing information about a particular host API. */ - -typedef struct PaHostApiInfo -{ - /** this is struct version 1 */ - int structVersion; - /** The well known unique identifier of this host API @see PaHostApiTypeId */ - PaHostApiTypeId type; - /** A textual description of the host API for display on user interfaces. */ - const char *name; - - /** The number of devices belonging to this host API. This field may be - used in conjunction with Pa_HostApiDeviceIndexToDeviceIndex() to enumerate - all devices for this host API. - @see Pa_HostApiDeviceIndexToDeviceIndex - */ - int deviceCount; - - /** The default input device for this host API. The value will be a - device index ranging from 0 to (Pa_GetDeviceCount()-1), or paNoDevice - if no default input device is available. - */ - PaDeviceIndex defaultInputDevice; - - /** The default output device for this host API. The value will be a - device index ranging from 0 to (Pa_GetDeviceCount()-1), or paNoDevice - if no default output device is available. - */ - PaDeviceIndex defaultOutputDevice; - -} PaHostApiInfo; - - -/** Retrieve a pointer to a structure containing information about a specific - host Api. - - @param hostApi A valid host API index ranging from 0 to (Pa_GetHostApiCount()-1) - - @return A pointer to an immutable PaHostApiInfo structure describing - a specific host API. If the hostApi parameter is out of range or an error - is encountered, the function returns NULL. - - The returned structure is owned by the PortAudio implementation and must not - be manipulated or freed. The pointer is only guaranteed to be valid between - calls to Pa_Initialize() and Pa_Terminate(). -*/ -const PaHostApiInfo * Pa_GetHostApiInfo( PaHostApiIndex hostApi ); - - -/** Convert a static host API unique identifier, into a runtime - host API index. - - @param type A unique host API identifier belonging to the PaHostApiTypeId - enumeration. - - @return A valid PaHostApiIndex ranging from 0 to (Pa_GetHostApiCount()-1) or, - a PaErrorCode (which are always negative) if PortAudio is not initialized - or an error is encountered. - - The paHostApiNotFound error code indicates that the host API specified by the - type parameter is not available. - - @see PaHostApiTypeId -*/ -PaHostApiIndex Pa_HostApiTypeIdToHostApiIndex( PaHostApiTypeId type ); - - -/** Convert a host-API-specific device index to standard PortAudio device index. - This function may be used in conjunction with the deviceCount field of - PaHostApiInfo to enumerate all devices for the specified host API. - - @param hostApi A valid host API index ranging from 0 to (Pa_GetHostApiCount()-1) - - @param hostApiDeviceIndex A valid per-host device index in the range - 0 to (Pa_GetHostApiInfo(hostApi)->deviceCount-1) - - @return A non-negative PaDeviceIndex ranging from 0 to (Pa_GetDeviceCount()-1) - or, a PaErrorCode (which are always negative) if PortAudio is not initialized - or an error is encountered. - - A paInvalidHostApi error code indicates that the host API index specified by - the hostApi parameter is out of range. - - A paInvalidDevice error code indicates that the hostApiDeviceIndex parameter - is out of range. - - @see PaHostApiInfo -*/ -PaDeviceIndex Pa_HostApiDeviceIndexToDeviceIndex( PaHostApiIndex hostApi, - int hostApiDeviceIndex ); - - - -/** Structure used to return information about a host error condition. -*/ -typedef struct PaHostErrorInfo{ - PaHostApiTypeId hostApiType; /**< the host API which returned the error code */ - long errorCode; /**< the error code returned */ - const char *errorText; /**< a textual description of the error if available, otherwise a zero-length string */ -}PaHostErrorInfo; - - -/** Return information about the last host error encountered. The error - information returned by Pa_GetLastHostErrorInfo() will never be modified - asynchronously by errors occurring in other PortAudio owned threads - (such as the thread that manages the stream callback.) - - This function is provided as a last resort, primarily to enhance debugging - by providing clients with access to all available error information. - - @return A pointer to an immutable structure constraining information about - the host error. The values in this structure will only be valid if a - PortAudio function has previously returned the paUnanticipatedHostError - error code. -*/ -const PaHostErrorInfo* Pa_GetLastHostErrorInfo( void ); - - - -/* Device enumeration and capabilities */ - -/** Retrieve the number of available devices. The number of available devices - may be zero. - - @return A non-negative value indicating the number of available devices or, - a PaErrorCode (which are always negative) if PortAudio is not initialized - or an error is encountered. -*/ -PaDeviceIndex Pa_GetDeviceCount( void ); - - -/** Retrieve the index of the default input device. The result can be - used in the inputDevice parameter to Pa_OpenStream(). - - @return The default input device index for the default host API, or paNoDevice - if no default input device is available or an error was encountered. -*/ -PaDeviceIndex Pa_GetDefaultInputDevice( void ); - - -/** Retrieve the index of the default output device. The result can be - used in the outputDevice parameter to Pa_OpenStream(). - - @return The default output device index for the default host API, or paNoDevice - if no default output device is available or an error was encountered. - - @note - On the PC, the user can specify a default device by - setting an environment variable. For example, to use device #1. -
- set PA_RECOMMENDED_OUTPUT_DEVICE=1
-
- The user should first determine the available device ids by using - the supplied application "pa_devs". -*/ -PaDeviceIndex Pa_GetDefaultOutputDevice( void ); - - -/** The type used to represent monotonic time in seconds. PaTime is - used for the fields of the PaStreamCallbackTimeInfo argument to the - PaStreamCallback and as the result of Pa_GetStreamTime(). - - PaTime values have unspecified origin. - - @see PaStreamCallback, PaStreamCallbackTimeInfo, Pa_GetStreamTime -*/ -typedef double PaTime; - - -/** A type used to specify one or more sample formats. Each value indicates - a possible format for sound data passed to and from the stream callback, - Pa_ReadStream and Pa_WriteStream. - - The standard formats paFloat32, paInt16, paInt32, paInt24, paInt8 - and aUInt8 are usually implemented by all implementations. - - The floating point representation (paFloat32) uses +1.0 and -1.0 as the - maximum and minimum respectively. - - paUInt8 is an unsigned 8 bit format where 128 is considered "ground" - - The paNonInterleaved flag indicates that audio data is passed as an array - of pointers to separate buffers, one buffer for each channel. Usually, - when this flag is not used, audio data is passed as a single buffer with - all channels interleaved. - - @see Pa_OpenStream, Pa_OpenDefaultStream, PaDeviceInfo - @see paFloat32, paInt16, paInt32, paInt24, paInt8 - @see paUInt8, paCustomFormat, paNonInterleaved -*/ -typedef unsigned long PaSampleFormat; - - -#define paFloat32 ((PaSampleFormat) 0x00000001) /**< @see PaSampleFormat */ -#define paInt32 ((PaSampleFormat) 0x00000002) /**< @see PaSampleFormat */ -#define paInt24 ((PaSampleFormat) 0x00000004) /**< Packed 24 bit format. @see PaSampleFormat */ -#define paInt16 ((PaSampleFormat) 0x00000008) /**< @see PaSampleFormat */ -#define paInt8 ((PaSampleFormat) 0x00000010) /**< @see PaSampleFormat */ -#define paUInt8 ((PaSampleFormat) 0x00000020) /**< @see PaSampleFormat */ -#define paCustomFormat ((PaSampleFormat) 0x00010000) /**< @see PaSampleFormat */ - -#define paNonInterleaved ((PaSampleFormat) 0x80000000) /**< @see PaSampleFormat */ - -/** A structure providing information and capabilities of PortAudio devices. - Devices may support input, output or both input and output. -*/ -typedef struct PaDeviceInfo -{ - int structVersion; /* this is struct version 2 */ - const char *name; - PaHostApiIndex hostApi; /**< note this is a host API index, not a type id*/ - - int maxInputChannels; - int maxOutputChannels; - - /** Default latency values for interactive performance. */ - PaTime defaultLowInputLatency; - PaTime defaultLowOutputLatency; - /** Default latency values for robust non-interactive applications (eg. playing sound files). */ - PaTime defaultHighInputLatency; - PaTime defaultHighOutputLatency; - - double defaultSampleRate; -} PaDeviceInfo; - - -/** Retrieve a pointer to a PaDeviceInfo structure containing information - about the specified device. - @return A pointer to an immutable PaDeviceInfo structure. If the device - parameter is out of range the function returns NULL. - - @param device A valid device index in the range 0 to (Pa_GetDeviceCount()-1) - - @note PortAudio manages the memory referenced by the returned pointer, - the client must not manipulate or free the memory. The pointer is only - guaranteed to be valid between calls to Pa_Initialize() and Pa_Terminate(). - - @see PaDeviceInfo, PaDeviceIndex -*/ -const PaDeviceInfo* Pa_GetDeviceInfo( PaDeviceIndex device ); - - -/** Parameters for one direction (input or output) of a stream. -*/ -typedef struct PaStreamParameters -{ - /** A valid device index in the range 0 to (Pa_GetDeviceCount()-1) - specifying the device to be used or the special constant - paUseHostApiSpecificDeviceSpecification which indicates that the actual - device(s) to use are specified in hostApiSpecificStreamInfo. - This field must not be set to paNoDevice. - */ - PaDeviceIndex device; - - /** The number of channels of sound to be delivered to the - stream callback or accessed by Pa_ReadStream() or Pa_WriteStream(). - It can range from 1 to the value of maxInputChannels in the - PaDeviceInfo record for the device specified by the device parameter. - */ - int channelCount; - - /** The sample format of the buffer provided to the stream callback, - a_ReadStream() or Pa_WriteStream(). It may be any of the formats described - by the PaSampleFormat enumeration. - */ - PaSampleFormat sampleFormat; - - /** The desired latency in seconds. Where practical, implementations should - configure their latency based on these parameters, otherwise they may - choose the closest viable latency instead. Unless the suggested latency - is greater than the absolute upper limit for the device implementations - should round the suggestedLatency up to the next practical value - ie to - provide an equal or higher latency than suggestedLatency wherever possible. - Actual latency values for an open stream may be retrieved using the - inputLatency and outputLatency fields of the PaStreamInfo structure - returned by Pa_GetStreamInfo(). - @see default*Latency in PaDeviceInfo, *Latency in PaStreamInfo - */ - PaTime suggestedLatency; - - /** An optional pointer to a host api specific data structure - containing additional information for device setup and/or stream processing. - hostApiSpecificStreamInfo is never required for correct operation, - if not used it should be set to NULL. - */ - void *hostApiSpecificStreamInfo; - -} PaStreamParameters; - - -/** Return code for Pa_IsFormatSupported indicating success. */ -#define paFormatIsSupported (0) - -/** Determine whether it would be possible to open a stream with the specified - parameters. - - @param inputParameters A structure that describes the input parameters used to - open a stream. The suggestedLatency field is ignored. See PaStreamParameters - for a description of these parameters. inputParameters must be NULL for - output-only streams. - - @param outputParameters A structure that describes the output parameters used - to open a stream. The suggestedLatency field is ignored. See PaStreamParameters - for a description of these parameters. outputParameters must be NULL for - input-only streams. - - @param sampleRate The required sampleRate. For full-duplex streams it is the - sample rate for both input and output - - @return Returns 0 if the format is supported, and an error code indicating why - the format is not supported otherwise. The constant paFormatIsSupported is - provided to compare with the return value for success. - - @see paFormatIsSupported, PaStreamParameters -*/ -PaError Pa_IsFormatSupported( const PaStreamParameters *inputParameters, - const PaStreamParameters *outputParameters, - double sampleRate ); - - - -/* Streaming types and functions */ - - -/** - A single PaStream can provide multiple channels of real-time - streaming audio input and output to a client application. A stream - provides access to audio hardware represented by one or more - PaDevices. Depending on the underlying Host API, it may be possible - to open multiple streams using the same device, however this behavior - is implementation defined. Portable applications should assume that - a PaDevice may be simultaneously used by at most one PaStream. - - Pointers to PaStream objects are passed between PortAudio functions that - operate on streams. - - @see Pa_OpenStream, Pa_OpenDefaultStream, Pa_OpenDefaultStream, Pa_CloseStream, - Pa_StartStream, Pa_StopStream, Pa_AbortStream, Pa_IsStreamActive, - Pa_GetStreamTime, Pa_GetStreamCpuLoad - -*/ -typedef void PaStream; - - -/** Can be passed as the framesPerBuffer parameter to Pa_OpenStream() - or Pa_OpenDefaultStream() to indicate that the stream callback will - accept buffers of any size. -*/ -#define paFramesPerBufferUnspecified (0) - - -/** Flags used to control the behavior of a stream. They are passed as - parameters to Pa_OpenStream or Pa_OpenDefaultStream. Multiple flags may be - ORed together. - - @see Pa_OpenStream, Pa_OpenDefaultStream - @see paNoFlag, paClipOff, paDitherOff, paNeverDropInput, - paPrimeOutputBuffersUsingStreamCallback, paPlatformSpecificFlags -*/ -typedef unsigned long PaStreamFlags; - -/** @see PaStreamFlags */ -#define paNoFlag ((PaStreamFlags) 0) - -/** Disable default clipping of out of range samples. - @see PaStreamFlags -*/ -#define paClipOff ((PaStreamFlags) 0x00000001) - -/** Disable default dithering. - @see PaStreamFlags -*/ -#define paDitherOff ((PaStreamFlags) 0x00000002) - -/** Flag requests that where possible a full duplex stream will not discard - overflowed input samples without calling the stream callback. This flag is - only valid for full duplex callback streams and only when used in combination - with the paFramesPerBufferUnspecified (0) framesPerBuffer parameter. Using - this flag incorrectly results in a paInvalidFlag error being returned from - Pa_OpenStream and Pa_OpenDefaultStream. - - @see PaStreamFlags, paFramesPerBufferUnspecified -*/ -#define paNeverDropInput ((PaStreamFlags) 0x00000004) - -/** Call the stream callback to fill initial output buffers, rather than the - default behavior of priming the buffers with zeros (silence). This flag has - no effect for input-only and blocking read/write streams. - - @see PaStreamFlags -*/ -#define paPrimeOutputBuffersUsingStreamCallback ((PaStreamFlags) 0x00000008) - -/** A mask specifying the platform specific bits. - @see PaStreamFlags -*/ -#define paPlatformSpecificFlags ((PaStreamFlags)0xFFFF0000) - -/** - Timing information for the buffers passed to the stream callback. - - Time values are expressed in seconds and are synchronised with the time base used by Pa_GetStreamTime() for the associated stream. - - @see PaStreamCallback, Pa_GetStreamTime -*/ -typedef struct PaStreamCallbackTimeInfo{ - PaTime inputBufferAdcTime; /**< The time when the first sample of the input buffer was captured at the ADC input */ - PaTime currentTime; /**< The time when the stream callback was invoked */ - PaTime outputBufferDacTime; /**< The time when the first sample of the output buffer will output the DAC */ -} PaStreamCallbackTimeInfo; - - -/** - Flag bit constants for the statusFlags to PaStreamCallback. - - @see paInputUnderflow, paInputOverflow, paOutputUnderflow, paOutputOverflow, - paPrimingOutput -*/ -typedef unsigned long PaStreamCallbackFlags; - -/** In a stream opened with paFramesPerBufferUnspecified, indicates that - input data is all silence (zeros) because no real data is available. In a - stream opened without paFramesPerBufferUnspecified, it indicates that one or - more zero samples have been inserted into the input buffer to compensate - for an input underflow. - @see PaStreamCallbackFlags -*/ -#define paInputUnderflow ((PaStreamCallbackFlags) 0x00000001) - -/** In a stream opened with paFramesPerBufferUnspecified, indicates that data - prior to the first sample of the input buffer was discarded due to an - overflow, possibly because the stream callback is using too much CPU time. - Otherwise indicates that data prior to one or more samples in the - input buffer was discarded. - @see PaStreamCallbackFlags -*/ -#define paInputOverflow ((PaStreamCallbackFlags) 0x00000002) - -/** Indicates that output data (or a gap) was inserted, possibly because the - stream callback is using too much CPU time. - @see PaStreamCallbackFlags -*/ -#define paOutputUnderflow ((PaStreamCallbackFlags) 0x00000004) - -/** Indicates that output data will be discarded because no room is available. - @see PaStreamCallbackFlags -*/ -#define paOutputOverflow ((PaStreamCallbackFlags) 0x00000008) - -/** Some of all of the output data will be used to prime the stream, input - data may be zero. - @see PaStreamCallbackFlags -*/ -#define paPrimingOutput ((PaStreamCallbackFlags) 0x00000010) - -/** - Allowable return values for the PaStreamCallback. - @see PaStreamCallback -*/ -typedef enum PaStreamCallbackResult -{ - paContinue=0, /**< Signal that the stream should continue invoking the callback and processing audio. */ - paComplete=1, /**< Signal that the stream should stop invoking the callback and finish once all output samples have played. */ - paAbort=2 /**< Signal that the stream should stop invoking the callback and finish as soon as possible. */ -} PaStreamCallbackResult; - - -/** - Functions of type PaStreamCallback are implemented by PortAudio clients. - They consume, process or generate audio in response to requests from an - active PortAudio stream. - - When a stream is running, PortAudio calls the stream callback periodically. - The callback function is responsible for processing buffers of audio samples - passed via the input and output parameters. - - The PortAudio stream callback runs at very high or real-time priority. - It is required to consistently meet its time deadlines. Do not allocate - memory, access the file system, call library functions or call other functions - from the stream callback that may block or take an unpredictable amount of - time to complete. - - In order for a stream to maintain glitch-free operation the callback - must consume and return audio data faster than it is recorded and/or - played. PortAudio anticipates that each callback invocation may execute for - a duration approaching the duration of frameCount audio frames at the stream - sample rate. It is reasonable to expect to be able to utilise 70% or more of - the available CPU time in the PortAudio callback. However, due to buffer size - adaption and other factors, not all host APIs are able to guarantee audio - stability under heavy CPU load with arbitrary fixed callback buffer sizes. - When high callback CPU utilisation is required the most robust behavior - can be achieved by using paFramesPerBufferUnspecified as the - Pa_OpenStream() framesPerBuffer parameter. - - @param input and @param output are either arrays of interleaved samples or; - if non-interleaved samples were requested using the paNonInterleaved sample - format flag, an array of buffer pointers, one non-interleaved buffer for - each channel. - - The format, packing and number of channels used by the buffers are - determined by parameters to Pa_OpenStream(). - - @param frameCount The number of sample frames to be processed by - the stream callback. - - @param timeInfo Timestamps indicating the ADC capture time of the first sample - in the input buffer, the DAC output time of the first sample in the output buffer - and the time the callback was invoked. - See PaStreamCallbackTimeInfo and Pa_GetStreamTime() - - @param statusFlags Flags indicating whether input and/or output buffers - have been inserted or will be dropped to overcome underflow or overflow - conditions. - - @param userData The value of a user supplied pointer passed to - Pa_OpenStream() intended for storing synthesis data etc. - - @return - The stream callback should return one of the values in the - ::PaStreamCallbackResult enumeration. To ensure that the callback continues - to be called, it should return paContinue (0). Either paComplete or paAbort - can be returned to finish stream processing, after either of these values is - returned the callback will not be called again. If paAbort is returned the - stream will finish as soon as possible. If paComplete is returned, the stream - will continue until all buffers generated by the callback have been played. - This may be useful in applications such as soundfile players where a specific - duration of output is required. However, it is not necessary to utilize this - mechanism as Pa_StopStream(), Pa_AbortStream() or Pa_CloseStream() can also - be used to stop the stream. The callback must always fill the entire output - buffer irrespective of its return value. - - @see Pa_OpenStream, Pa_OpenDefaultStream - - @note With the exception of Pa_GetStreamCpuLoad() it is not permissible to call - PortAudio API functions from within the stream callback. -*/ -typedef int PaStreamCallback( - const void *input, void *output, - unsigned long frameCount, - const PaStreamCallbackTimeInfo* timeInfo, - PaStreamCallbackFlags statusFlags, - void *userData ); - - -/** Opens a stream for either input, output or both. - - @param stream The address of a PaStream pointer which will receive - a pointer to the newly opened stream. - - @param inputParameters A structure that describes the input parameters used by - the opened stream. See PaStreamParameters for a description of these parameters. - inputParameters must be NULL for output-only streams. - - @param outputParameters A structure that describes the output parameters used by - the opened stream. See PaStreamParameters for a description of these parameters. - outputParameters must be NULL for input-only streams. - - @param sampleRate The desired sampleRate. For full-duplex streams it is the - sample rate for both input and output - - @param framesPerBuffer The number of frames passed to the stream callback - function, or the preferred block granularity for a blocking read/write stream. - The special value paFramesPerBufferUnspecified (0) may be used to request that - the stream callback will receive an optimal (and possibly varying) number of - frames based on host requirements and the requested latency settings. - Note: With some host APIs, the use of non-zero framesPerBuffer for a callback - stream may introduce an additional layer of buffering which could introduce - additional latency. PortAudio guarantees that the additional latency - will be kept to the theoretical minimum however, it is strongly recommended - that a non-zero framesPerBuffer value only be used when your algorithm - requires a fixed number of frames per stream callback. - - @param streamFlags Flags which modify the behavior of the streaming process. - This parameter may contain a combination of flags ORed together. Some flags may - only be relevant to certain buffer formats. - - @param streamCallback A pointer to a client supplied function that is responsible - for processing and filling input and output buffers. If this parameter is NULL - the stream will be opened in 'blocking read/write' mode. In blocking mode, - the client can receive sample data using Pa_ReadStream and write sample data - using Pa_WriteStream, the number of samples that may be read or written - without blocking is returned by Pa_GetStreamReadAvailable and - Pa_GetStreamWriteAvailable respectively. - - @param userData A client supplied pointer which is passed to the stream callback - function. It could for example, contain a pointer to instance data necessary - for processing the audio buffers. This parameter is ignored if streamCallback - is NULL. - - @return - Upon success Pa_OpenStream() returns paNoError and places a pointer to a - valid PaStream in the stream argument. The stream is inactive (stopped). - If a call to Pa_OpenStream() fails, a non-zero error code is returned (see - PaError for possible error codes) and the value of stream is invalid. - - @see PaStreamParameters, PaStreamCallback, Pa_ReadStream, Pa_WriteStream, - Pa_GetStreamReadAvailable, Pa_GetStreamWriteAvailable -*/ -PaError Pa_OpenStream( PaStream** stream, - const PaStreamParameters *inputParameters, - const PaStreamParameters *outputParameters, - double sampleRate, - unsigned long framesPerBuffer, - PaStreamFlags streamFlags, - PaStreamCallback *streamCallback, - void *userData ); - - -/** A simplified version of Pa_OpenStream() that opens the default input - and/or output devices. - - @param stream The address of a PaStream pointer which will receive - a pointer to the newly opened stream. - - @param numInputChannels The number of channels of sound that will be supplied - to the stream callback or returned by Pa_ReadStream. It can range from 1 to - the value of maxInputChannels in the PaDeviceInfo record for the default input - device. If 0 the stream is opened as an output-only stream. - - @param numOutputChannels The number of channels of sound to be delivered to the - stream callback or passed to Pa_WriteStream. It can range from 1 to the value - of maxOutputChannels in the PaDeviceInfo record for the default output device. - If 0 the stream is opened as an output-only stream. - - @param sampleFormat The sample format of both the input and output buffers - provided to the callback or passed to and from Pa_ReadStream and Pa_WriteStream. - sampleFormat may be any of the formats described by the PaSampleFormat - enumeration. - - @param sampleRate Same as Pa_OpenStream parameter of the same name. - @param framesPerBuffer Same as Pa_OpenStream parameter of the same name. - @param streamCallback Same as Pa_OpenStream parameter of the same name. - @param userData Same as Pa_OpenStream parameter of the same name. - - @return As for Pa_OpenStream - - @see Pa_OpenStream, PaStreamCallback -*/ -PaError Pa_OpenDefaultStream( PaStream** stream, - int numInputChannels, - int numOutputChannels, - PaSampleFormat sampleFormat, - double sampleRate, - unsigned long framesPerBuffer, - PaStreamCallback *streamCallback, - void *userData ); - - -/** Closes an audio stream. If the audio stream is active it - discards any pending buffers as if Pa_AbortStream() had been called. -*/ -PaError Pa_CloseStream( PaStream *stream ); - - -/** Functions of type PaStreamFinishedCallback are implemented by PortAudio - clients. They can be registered with a stream using the Pa_SetStreamFinishedCallback - function. Once registered they are called when the stream becomes inactive - (ie once a call to Pa_StopStream() will not block). - A stream will become inactive after the stream callback returns non-zero, - or when Pa_StopStream or Pa_AbortStream is called. For a stream providing audio - output, if the stream callback returns paComplete, or Pa_StopStream is called, - the stream finished callback will not be called until all generated sample data - has been played. - - @param userData The userData parameter supplied to Pa_OpenStream() - - @see Pa_SetStreamFinishedCallback -*/ -typedef void PaStreamFinishedCallback( void *userData ); - - -/** Register a stream finished callback function which will be called when the - stream becomes inactive. See the description of PaStreamFinishedCallback for - further details about when the callback will be called. - - @param stream a pointer to a PaStream that is in the stopped state - if the - stream is not stopped, the stream's finished callback will remain unchanged - and an error code will be returned. - - @param streamFinishedCallback a pointer to a function with the same signature - as PaStreamFinishedCallback, that will be called when the stream becomes - inactive. Passing NULL for this parameter will un-register a previously - registered stream finished callback function. - - @return on success returns paNoError, otherwise an error code indicating the cause - of the error. - - @see PaStreamFinishedCallback -*/ -PaError Pa_SetStreamFinishedCallback( PaStream *stream, PaStreamFinishedCallback* streamFinishedCallback ); - - -/** Commences audio processing. -*/ -PaError Pa_StartStream( PaStream *stream ); - - -/** Terminates audio processing. It waits until all pending - audio buffers have been played before it returns. -*/ -PaError Pa_StopStream( PaStream *stream ); - - -/** Terminates audio processing immediately without waiting for pending - buffers to complete. -*/ -PaError Pa_AbortStream( PaStream *stream ); - - -/** Determine whether the stream is stopped. - A stream is considered to be stopped prior to a successful call to - Pa_StartStream and after a successful call to Pa_StopStream or Pa_AbortStream. - If a stream callback returns a value other than paContinue the stream is NOT - considered to be stopped. - - @return Returns one (1) when the stream is stopped, zero (0) when - the stream is running or, a PaErrorCode (which are always negative) if - PortAudio is not initialized or an error is encountered. - - @see Pa_StopStream, Pa_AbortStream, Pa_IsStreamActive -*/ -PaError Pa_IsStreamStopped( PaStream *stream ); - - -/** Determine whether the stream is active. - A stream is active after a successful call to Pa_StartStream(), until it - becomes inactive either as a result of a call to Pa_StopStream() or - Pa_AbortStream(), or as a result of a return value other than paContinue from - the stream callback. In the latter case, the stream is considered inactive - after the last buffer has finished playing. - - @return Returns one (1) when the stream is active (ie playing or recording - audio), zero (0) when not playing or, a PaErrorCode (which are always negative) - if PortAudio is not initialized or an error is encountered. - - @see Pa_StopStream, Pa_AbortStream, Pa_IsStreamStopped -*/ -PaError Pa_IsStreamActive( PaStream *stream ); - - - -/** A structure containing unchanging information about an open stream. - @see Pa_GetStreamInfo -*/ - -typedef struct PaStreamInfo -{ - /** this is struct version 1 */ - int structVersion; - - /** The input latency of the stream in seconds. This value provides the most - accurate estimate of input latency available to the implementation. It may - differ significantly from the suggestedLatency value passed to Pa_OpenStream(). - The value of this field will be zero (0.) for output-only streams. - @see PaTime - */ - PaTime inputLatency; - - /** The output latency of the stream in seconds. This value provides the most - accurate estimate of output latency available to the implementation. It may - differ significantly from the suggestedLatency value passed to Pa_OpenStream(). - The value of this field will be zero (0.) for input-only streams. - @see PaTime - */ - PaTime outputLatency; - - /** The sample rate of the stream in Hertz (samples per second). In cases - where the hardware sample rate is inaccurate and PortAudio is aware of it, - the value of this field may be different from the sampleRate parameter - passed to Pa_OpenStream(). If information about the actual hardware sample - rate is not available, this field will have the same value as the sampleRate - parameter passed to Pa_OpenStream(). - */ - double sampleRate; - -} PaStreamInfo; - - -/** Retrieve a pointer to a PaStreamInfo structure containing information - about the specified stream. - @return A pointer to an immutable PaStreamInfo structure. If the stream - parameter is invalid, or an error is encountered, the function returns NULL. - - @param stream A pointer to an open stream previously created with Pa_OpenStream. - - @note PortAudio manages the memory referenced by the returned pointer, - the client must not manipulate or free the memory. The pointer is only - guaranteed to be valid until the specified stream is closed. - - @see PaStreamInfo -*/ -const PaStreamInfo* Pa_GetStreamInfo( PaStream *stream ); - - -/** Returns the current time in seconds for a stream according to the same clock used - to generate callback PaStreamCallbackTimeInfo timestamps. The time values are - monotonically increasing and have unspecified origin. - - Pa_GetStreamTime returns valid time values for the entire life of the stream, - from when the stream is opened until it is closed. Starting and stopping the stream - does not affect the passage of time returned by Pa_GetStreamTime. - - This time may be used for synchronizing other events to the audio stream, for - example synchronizing audio to MIDI. - - @return The stream's current time in seconds, or 0 if an error occurred. - - @see PaTime, PaStreamCallback, PaStreamCallbackTimeInfo -*/ -PaTime Pa_GetStreamTime( PaStream *stream ); - - -/** Retrieve CPU usage information for the specified stream. - The "CPU Load" is a fraction of total CPU time consumed by a callback stream's - audio processing routines including, but not limited to the client supplied - stream callback. This function does not work with blocking read/write streams. - - This function may be called from the stream callback function or the - application. - - @return - A floating point value, typically between 0.0 and 1.0, where 1.0 indicates - that the stream callback is consuming the maximum number of CPU cycles possible - to maintain real-time operation. A value of 0.5 would imply that PortAudio and - the stream callback was consuming roughly 50% of the available CPU time. The - return value may exceed 1.0. A value of 0.0 will always be returned for a - blocking read/write stream, or if an error occurs. -*/ -double Pa_GetStreamCpuLoad( PaStream* stream ); - - -/** Read samples from an input stream. The function doesn't return until - the entire buffer has been filled - this may involve waiting for the operating - system to supply the data. - - @param stream A pointer to an open stream previously created with Pa_OpenStream. - - @param buffer A pointer to a buffer of sample frames. The buffer contains - samples in the format specified by the inputParameters->sampleFormat field - used to open the stream, and the number of channels specified by - inputParameters->numChannels. If non-interleaved samples were requested using - the paNonInterleaved sample format flag, buffer is a pointer to the first element - of an array of buffer pointers, one non-interleaved buffer for each channel. - - @param frames The number of frames to be read into buffer. This parameter - is not constrained to a specific range, however high performance applications - will want to match this parameter to the framesPerBuffer parameter used - when opening the stream. - - @return On success PaNoError will be returned, or PaInputOverflowed if input - data was discarded by PortAudio after the previous call and before this call. -*/ -PaError Pa_ReadStream( PaStream* stream, - void *buffer, - unsigned long frames ); - - -/** Write samples to an output stream. This function doesn't return until the - entire buffer has been consumed - this may involve waiting for the operating - system to consume the data. - - @param stream A pointer to an open stream previously created with Pa_OpenStream. - - @param buffer A pointer to a buffer of sample frames. The buffer contains - samples in the format specified by the outputParameters->sampleFormat field - used to open the stream, and the number of channels specified by - outputParameters->numChannels. If non-interleaved samples were requested using - the paNonInterleaved sample format flag, buffer is a pointer to the first element - of an array of buffer pointers, one non-interleaved buffer for each channel. - - @param frames The number of frames to be written from buffer. This parameter - is not constrained to a specific range, however high performance applications - will want to match this parameter to the framesPerBuffer parameter used - when opening the stream. - - @return On success PaNoError will be returned, or paOutputUnderflowed if - additional output data was inserted after the previous call and before this - call. -*/ -PaError Pa_WriteStream( PaStream* stream, - const void *buffer, - unsigned long frames ); - - -/** Retrieve the number of frames that can be read from the stream without - waiting. - - @return Returns a non-negative value representing the maximum number of frames - that can be read from the stream without blocking or busy waiting or, a - PaErrorCode (which are always negative) if PortAudio is not initialized or an - error is encountered. -*/ -signed long Pa_GetStreamReadAvailable( PaStream* stream ); - - -/** Retrieve the number of frames that can be written to the stream without - waiting. - - @return Returns a non-negative value representing the maximum number of frames - that can be written to the stream without blocking or busy waiting or, a - PaErrorCode (which are always negative) if PortAudio is not initialized or an - error is encountered. -*/ -signed long Pa_GetStreamWriteAvailable( PaStream* stream ); - - -/* Miscellaneous utilities */ - - -/** Retrieve the size of a given sample format in bytes. - - @return The size in bytes of a single sample in the specified format, - or paSampleFormatNotSupported if the format is not supported. -*/ -PaError Pa_GetSampleSize( PaSampleFormat format ); - - -/** Put the caller to sleep for at least 'msec' milliseconds. This function is - provided only as a convenience for authors of portable code (such as the tests - and examples in the PortAudio distribution.) - - The function may sleep longer than requested so don't rely on this for accurate - musical timing. -*/ -void Pa_Sleep( long msec ); - - - -#ifdef __cplusplus -} -#endif /* __cplusplus */ -#endif /* PORTAUDIO_H */ diff --git a/external/portaudio/libs/32/portaudio_x86.dll b/external/portaudio/libs/32/portaudio_x86.dll deleted file mode 100644 index d661fd966c1314690b6b39f3fc0784e24de67262..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 258560 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%0A82$I!C4Q$Y8gb$|t?vQK27_smf(=shO%$dMiey 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

Qlac>`8zC!{fKDH#@k6qhvoMdMG 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 It?+C6IZ5uP34>=gma_W8R_8Fj}i) z(whb%EHxz0D8alSWRRjereXEW#~k7!d~M$`zH_lpD=c*qMp#*SCR$tdLnyj$8Pbr#P1Om62eDxp@udfhboxo-6$!g~719d^H=LHAyH{ilP8 zGg4&vW3&7XjAFzGIRxfZY{-?TxirU8xJ6bKikoCCXLH>AQgbnvanUfCW-wvZH)6OT zU}~FhDz2bGOE4P?EhO8RMjZ@kQ*@X}rIhAb3bBfeFTnC5Y;N#e^m+16%quaPWE?v~ z$MNi31Ow&gz=iJo3tb^+c3OHls)4g~I#$q9l;UB;12M)lz8`jRghQ&c_{> zs1xST23B6@c1r*PoH|13Eoa7O!A_ozsra?xgY{Yr%7}_G z9AYOy>Br=X^326d!UaAprx7L;(%{lkVvrREQ?ao?G#m1b+QTeT*J0sowCz0DtuARof@JB+M<#>xDBJcr-~hXq$kE5M8z59f}FtslPBj zj!}X&8kGw>ZyMNBbh%ieCMPE9lVbESx$#LU8R@w(`po!fy*ey8BV7?lY4dV{GuOZn zAtqVNFiWF-0}?LKt`r(s=A(J&bp`!I#}pD%%;Ai}u9c6G>6j8F-J$&F+y^X3@h zA=v;GcS>*Ic}S+1S4N1Hn6?skA%;%-Q!}wYg<;NuzM5A+Aa|0+HKD33f?)Uhd63k% z5bIuZ5l4WWpKb=W-5g!*cYy%@J4^Tf0*LPyuU3rn-87!}zdiEzOSzx5jZb)MOP|YM z4^Q~p+-<)Wy{370$+OM3E-$i0Eqe5YyGrl4|FeSynXh+$W>oTPvESBxFuKR7r#0Vx zboa@5v5$}2k@c4+i-%rWzx?OeVYM-@efrinuWg%tyx`-vcHe$_)xT zQC;qOwxH*I&n_N*X+`6gPn>8T^7h`TLz3T!&AF-VU+=x1b*Za9cE~-kkI&d&y(Q+? zhdh7-ybdJP2*Mk}m=&{WpIYgrJ!_?#;%i}OA6s*D?QMTK{Ewui1DfmG&p&(vygK+% ze04mw7p_W6<*4IPIch(zPW+%Wp+mebo%j*ViQX^&6zX$l(nsc^M^=CMcmlkHJ8oEf z;_6Sl_g^?jk@2Q9%8HHifY!jNGMMQm>z{xSmsUol%u#WS$!iiswEaU?v!mzXjU zJo!NyAv)55&M+^)EVCS|Jd+?K1y5Vk=evI;)rd2jV%McK6Y})ADar9k>H1V=k59_f zCB(<*QsXEuT}DiFLQ;CJJ~t^j9V{k3JszR=>to!)bP1_?UCfkghG(TFC&lH0O|gP< z)2GmjOp3fY(?W7yG^U{)%T7yAPDxRfJ0T-BR-c-io}7%?aB3Vfb-@dkmzx}$o17X$ z6?6G#B&B7fq$H=Jklfhh)I?pnds#9NxoN1SGMy8~+>A86B0m|1XTGrt0HJGMxWe_>(Fqx8@tOME#N-%`YMPK|sqx^%U^Mbe*XPFS z;uF+yFrB9nfESjLl*B+$mX)bbN}s4pib>#c^w}v)%&CM|1y6EP0%;<3!HYV_WH_sv z3V|{!PCq$A2l92r)2qr#(-X2?;qghblr>kzB15_>T;ZF`BsYq*(J2`jX}UOlE`ZhR zF#syUsGc*AGzX;m#FTU(#gQ&8S8iiRSLmdj z)6mb-(1AcBY3XQRDmXfscP&NFiV&-eRGsuK4ouuee|?FdsIY$!K_-2yuw}PuXTX8MUS3z(GD=Jm3t8bC*aA zFfppk>Ab1!JA!r`kHJlo3R?-CXN86MQ!PbMtXrdEUzQWL<)9~MH z%AZE7!Muv%X_a|q`c&lQRu-4%a@Wv&%0DG9i-jq6&6Q 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 JAKpyH{{vgk+Ayfiy;gJv< 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%0Dg#?X8}! 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 KmiMbA-2WdN?Y4RV diff --git a/external/portaudio/libs/64/libportaudio_x86.dll b/external/portaudio/libs/64/libportaudio_x86.dll deleted file mode 100644 index 45478e144999732167e75b93acdde15e47562df8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 202053 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

a&GCxghSxzc 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 Y5OPssiVhnlg&dO$GV4aRvdsDa3u08T1poj5 diff --git a/external/portaudio/libs/64/libportaudio_x86.dll.a b/external/portaudio/libs/64/libportaudio_x86.dll.a deleted file mode 100644 index 762209003d28ee08e10d3ffee013e31101e68643..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23426 zcmeHP&2JmW6`#^}?5L{a57|*%$EFe|6%1D)DN>eG*Rd+ak&pxyA|*l40`y8;*F-3i zpjL*PKcJ_g$f3s`aw>|V#~ykrj2?O}dMtV@iX3{#vFN3JZ{F<8o1Nv(?rm*yfVaEz z=IzJK{N}y4Z)SJlb}-t1bZ6$Rs-5c1`bw+0T3@MGt2J4~x7BL>R(+MucrYV`*c9UA zpM`kwk`Ob$C7S)W5J)dA5}kc01k(B65ncGJ5J>a4i7tI11k&X{5?!4Y0%_qVM6drw z2&6auOSHHu1k&~26Mg@N5J+!5CaV8I2&Cp^qLq#iNUMJ#`oU=-kbd}p=pA@&q#wON zw6QG&(!H;UHva|H1bTmo=))&MAl?5v(MN9!fwcQeqEG%N1kz_0k$T;|;b0U6N1g7V z{bY5e-Rbq(-F|QoiuR*0=(N)W#CFjBbkvQ)ooEyW2cq5X9t_(FCS-TR(E(g5f@{SR zh6Uw2(O{Uo1mj7GU=+C`hKVx0FdUjYP!{>2I`dw)-#vU3cGiPlZ!g&YoXL(Af%89t z>L+i_cp!0q81|DAjHe_v!iT|8.p+C?D0bx7C4Rv3mI^Q6fFCcCyDbsw`Pjztw; zO5Pic4uWXsXo#I{UpHBV^8Ieq4SL-#_*D%LBsK?!(b}-P`?F!V)!7}W>{h=MJ~7Xn zC1kF#5kBtjhw{|j0n0eoVP&=;ToW_4*9S=5h$AtXtUS{LHQX!$Ha&B%8~v505?=1Pn_0Vd4@c z9UKgM;f~emC~DXw_d#$NVb8uh8VyFyL%5}MmPSb8p*i1@Tpt|u?T%EUR6w%w8Jr$B zT#|}`lejfwb2<0MB*^cp#_j#1Xm}J+vDFo68p2&->^Hdw8-7GEw`Ssr5 z(9zAD2{LPYpb3)7i!--_kE3o6wdh8;cl6WkQMVu6JL-e78T9r2GekNyNXsir17*U; zS?-l)QyT2X>asHR4`w<)KL*BI2r!f4xI8)`mb#rF3a&L}7B;7Vei3K%zV=kfwPF^^ z{VLAry_UNfv*zL8*KtPgYftr|I!GGCC9s%S#Gmh5kUti4{u784ioB+bsRnOg!S=h`0VY_|>go&O}x0h~LDot*+ zobIQj1m)d!jpeANW+991a!j0ot<|3LNiB}s!t(BJ3*%XMeemGqmNpdu?opPzlhGZ7 z@>cHNXw`2qlv5ux;js{Jum291GzmW82HtS_@LbdJeoVd!$YoP(h^)8#aVaD*4d z&;qu)d6_6Je$L81;rO|xIJm2bcVKJPMEi`Ua(?VA7r+j;vXhCODz-R#&%|+!R5smn z4xR~<(LHYxLvAZ#4fb|TGCYNQs=F{aI>-Ac?oVH%*sTQ?fS0sjH z+^PiFK^;QR!aX0-hFtr^XhQW<5fEJweY9%0e#vRbs9J`t;l1{3Fb(X%>54;!;+ zN3-7Ozl-Q$Gat`5dRR>~dN|LEo}2m6v)ppt9r9Aj_OAc3gQ=S0;I1OxhiyuYG@_DK^}h8t3dF1%u8x|tnDsnw zeKoUQRx5W=A;;(^d{>K8MQp(#!;)wHfFDT(BG*%ECtjwuPcFN7)~(&vVX{U{H#tRK zYd4K($K#du+5Y!Mi7B%}r>vSO=i8fS{gGcI6^LlB+;sf!4e)V&{x_fd%JRR*%zZg= zCY-;+M7!~Ernl4OwEuk#u2IzgzJTaa%STdIbip;tim2DI(I6RHw@D1St%!Ztdupfk zR8|xCF_%9A-n!|=HaBx%uFCp83v*-U>>bRZ4~L0%<1u$9T~1?e5w1}bb91a$;)PhS zc+g(h2iGkt?!Jc&2g%@Wn;3Fi5ud}};BIUUlh_+C=(U&yV(U|XUYy6?f#r>S)o59< zmeZe4)J8)q$@d|tK8>8Sp$v!FzSlV$Nu||TIejmK7BjoYd0zAsh@7|FxWZkqH9p_# zd79%ptI(6W;vBG7R-F{DpEUOGE*mrz@ngu5hE1&*S5D~yUYr$(u{YfLy-AB-ow0gD zeuhw5Y+X=DGT6FdVe7pFwz9sp@cQlb0&(@G>$l&|p|9qc|D5HwUtw}+b@)0~Yyi}Y zUjr?s%jro7l=}v#kuInGcK86Tu;2cYg(Lgxl(OooD^O2N22Xd0A-5Ir8SJ^PvTNn4 zszkY0Qx%D{Pc_wPt1sjSM$e6=az2+L+ed6Kj*7(DT^vo?4B&Sx9L3+dmDi)fnR84= zOErigw-xar?75bTYvk%DUE)PlfhhabhZ3`K`IuMNkO#rGnqK#Y94hDy9>BZ)qMhjCt3H04}F1<_1}8uwFwyTFI1T7gJ;!(Ed% zX)&l%3Qy+$4CTeroHA|3-d&H4og4x0!j|lvsSVdljO#^Ffr$Ck3l}FB~m(?1^e>O0>YSGeF>!hoAZ8VZBrWZp6BIQ#I!OCR75dSIR wvyUNO8;v20>BUfic=+@$8gk@j{HC(BI%z>^AfuDg{-Tjlvlv}jA{kiwKhdsC`v3p{ From 894c7187303802deefb4b221afb6c7b78dc4e89a Mon Sep 17 00:00:00 2001 From: "Charles J. Cliffe" Date: Thu, 4 Dec 2014 22:25:18 -0500 Subject: [PATCH 4/6] Mostly working on OSX /w rtaudio now Looks like just some priority tweaks remain --- CMakeLists.txt | 62 +++++++++++++++++++++++++++++++----------------- src/CubicSDR.cpp | 8 +++++++ 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ae830a..a77ed53 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,11 +48,8 @@ SET( CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR}) #include_directories ( ${PROJECT_SOURCE_DIR}/../CubicVR-2/build/include ) #link_directories ( ${PROJECT_SOURCE_DIR}/../CubicVR-2/build/lib ) - - find_package(OpenGL REQUIRED) - find_package(wxWidgets REQUIRED gl core base) set(wxWidgets_CONFIGURATION mswu) include(${wxWidgets_USE_FILE}) @@ -60,8 +57,7 @@ include(${wxWidgets_USE_FILE}) # include_directories ( ${PROJECT_SOURCE_DIR}/../CubicVR-2/build/include ) # link_directories ( ${PROJECT_SOURCE_DIR}/../CubicVR-2/build/lib ${OPENGL_INCLUDE_DIR}) - -if (DEFINED WIN32) +IF (DEFINED WIN32) set(wxWidgets_USE_STATIC ON) include_directories ( ${PROJECT_SOURCE_DIR}/external/fftw-3.3.4-dll64 ${PROJECT_SOURCE_DIR}/external/rtl-sdr-release ) link_directories ( ${PROJECT_SOURCE_DIR}/external/fftw-3.3.4-dll64 ${PROJECT_SOURCE_DIR}/external/rtl-sdr-release/x64 ) @@ -71,22 +67,49 @@ if (DEFINED WIN32) include_directories ( ${PROJECT_SOURCE_DIR}/external/liquid-dsp/include ) ADD_DEFINITIONS( + # To use wasapi -D__WINDOWS_WASAPI__ -# -D__WINDOWS_DS__ + # To use directsound + #-D__WINDOWS_DS__ ) - SET(OTHER_LIBRARIES -luuid -lksuser ) - #SET(OTHER_LIBRARIES -ldsound) - -else (DEFINED WIN32) - set(RTLSDR_INCLUDE "/opt/local/include" CACHE FILEPATH "RTL-SDR Include Path") - set(RTLSDR_LIB "/opt/local/lib" CACHE FILEPATH "RTL-SDR Lib Path") - include_directories(${RTLSDR_INCLUDE}) - link_directories(${RTLSDR_LIB}) - - set(FFTW_LIB fftw3) -endif (DEFINED WIN32) + # To use wasapi + SET(OTHER_LIBRARIES -luuid -lksuser ) + # To use DirectSound (which uses WASAPI anyways?) + # SET(OTHER_LIBRARIES -ldsound) +ENDIF (DEFINED WIN32) +IF (UNIX) + set(RTLSDR_INCLUDE "/opt/local/include" CACHE FILEPATH "RTL-SDR Include Path") + set(RTLSDR_LIB "/opt/local/lib" CACHE FILEPATH "RTL-SDR Lib Path") + include_directories(${RTLSDR_INCLUDE}) + link_directories(${RTLSDR_LIB}) + + set(FFTW_LIB fftw3) + + ADD_DEFINITIONS( + # -D__UNIX_JACK__ + # -D__LINUX_ALSA__ + # -D__LINUX_PULSE__ + # -D__LINUX_OSS__ + ) +ENDIF(UNIX) + +IF (APPLE) + set(RTLSDR_INCLUDE "/opt/local/include" CACHE FILEPATH "RTL-SDR Include Path") + set(RTLSDR_LIB "/opt/local/lib" CACHE FILEPATH "RTL-SDR Lib Path") + include_directories(${RTLSDR_INCLUDE}) + link_directories(${RTLSDR_LIB}) + + set(FFTW_LIB fftw3) + + ADD_DEFINITIONS( + -D__MACOSX_CORE__ + ) + + FIND_LIBRARY(COREAUDIO_LIBRARY CoreAudio) + SET (OTHER_LIBRARIES ${COREAUDIO_LIBRARY}) +ENDIF (APPLE) SET (cubicsdr_sources @@ -155,8 +178,3 @@ add_executable(CubicSDR ${cubicsdr_sources} ${cubicsdr_headers}) target_link_libraries(CubicSDR rtlsdr liquid ${FFTW_LIB} ${wxWidgets_LIBRARIES} ${OPENGL_LIBRARIES} ${OTHER_LIBRARIES}) # cubicvr2 glfw ${GLFW_LIBRARIES} - - - - - diff --git a/src/CubicSDR.cpp b/src/CubicSDR.cpp index 732882e..a28acae 100644 --- a/src/CubicSDR.cpp +++ b/src/CubicSDR.cpp @@ -47,6 +47,14 @@ bool CubicSDR::OnInit() { AppFrame *appframe = new AppFrame(); + int main_policy; + struct sched_param main_param; + + main_policy = SCHED_OTHER; + main_param.sched_priority = sched_get_priority_min(SCHED_OTHER); + + pthread_setschedparam(pthread_self(), main_policy, &main_param); + return true; } From 24b3b81f3488a34a9e563841114c871517ab3a5a Mon Sep 17 00:00:00 2001 From: "Charles J. Cliffe" Date: Fri, 5 Dec 2014 18:31:32 -0500 Subject: [PATCH 5/6] Update readme to reflect portaudio -> RtAudio Added github links for liquid-dsp and RtAudio --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 45aba67..3c1d9d7 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ Cross-Platform Software-Defined Radio Application Utilizes: -------- - - liquid-dsp (http://liquidsdr.org/) + - liquid-dsp (http://liquidsdr.org/ https://github.com/jgaeddert/liquid-dsp) + - RtAudio (http://www.music.mcgill.ca/~gary/rtaudio/ http://github.com/thestk/rtaudio/) - OpenGL (https://www.opengl.org/) - - portaudio (http://www.portaudio.com/) - wxWidgets (https://www.wxwidgets.org/) - CMake (http://www.cmake.org/) From 051c4f081fe76dfa65eb6ee86c5a88d6092066e6 Mon Sep 17 00:00:00 2001 From: "Charles J. Cliffe" Date: Fri, 5 Dec 2014 18:20:28 -0500 Subject: [PATCH 6/6] RtAudio tweaks, disable unused GL states --- src/CubicSDR.cpp | 2 ++ src/audio/AudioThread.cpp | 7 ++++++- src/visual/ScopeContext.cpp | 4 ++-- src/visual/SpectrumContext.cpp | 4 ++-- src/visual/WaterfallContext.cpp | 4 ++-- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/CubicSDR.cpp b/src/CubicSDR.cpp index a28acae..cd6fca3 100644 --- a/src/CubicSDR.cpp +++ b/src/CubicSDR.cpp @@ -47,6 +47,7 @@ bool CubicSDR::OnInit() { AppFrame *appframe = new AppFrame(); +#ifdef __APPLE__ int main_policy; struct sched_param main_param; @@ -54,6 +55,7 @@ bool CubicSDR::OnInit() { main_param.sched_priority = sched_get_priority_min(SCHED_OTHER); pthread_setschedparam(pthread_self(), main_policy, &main_param); +#endif return true; } diff --git a/src/audio/AudioThread.cpp b/src/audio/AudioThread.cpp index 3c8814e..7a807c1 100644 --- a/src/audio/AudioThread.cpp +++ b/src/audio/AudioThread.cpp @@ -59,8 +59,13 @@ void AudioThread::threadMain() { unsigned int sampleRate = AUDIO_FREQUENCY; unsigned int bufferFrames = 256; + RtAudio::StreamOptions opts; + opts.flags = RTAUDIO_MINIMIZE_LATENCY | RTAUDIO_SCHEDULE_REALTIME; + opts.streamName = "CubicSDR Audio Output"; + opts.priority = 0; + try { - dac.openStream(¶meters, NULL, RTAUDIO_FLOAT32, sampleRate, &bufferFrames, &audioCallback, (void *) this); + dac.openStream(¶meters, NULL, RTAUDIO_FLOAT32, sampleRate, &bufferFrames, &audioCallback, (void *) this, &opts); dac.startStream(); } catch (RtAudioError& e) { e.printMessage(); diff --git a/src/visual/ScopeContext.cpp b/src/visual/ScopeContext.cpp index 26368e2..2b1937f 100644 --- a/src/visual/ScopeContext.cpp +++ b/src/visual/ScopeContext.cpp @@ -4,8 +4,8 @@ ScopeContext::ScopeContext(ScopeCanvas *canvas, wxGLContext *sharedContext) : PrimaryGLContext(canvas, sharedContext) { - glEnable(GL_CULL_FACE); - glEnable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + glDisable(GL_DEPTH_TEST); glMatrixMode(GL_PROJECTION); glLoadIdentity(); diff --git a/src/visual/SpectrumContext.cpp b/src/visual/SpectrumContext.cpp index d90d06c..22f243c 100644 --- a/src/visual/SpectrumContext.cpp +++ b/src/visual/SpectrumContext.cpp @@ -4,8 +4,8 @@ SpectrumContext::SpectrumContext(SpectrumCanvas *canvas, wxGLContext *sharedContext) : PrimaryGLContext(canvas, sharedContext) { - glEnable(GL_CULL_FACE); - glEnable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + glDisable(GL_DEPTH_TEST); glMatrixMode(GL_PROJECTION); glLoadIdentity(); diff --git a/src/visual/WaterfallContext.cpp b/src/visual/WaterfallContext.cpp index 3d3509d..a20118a 100644 --- a/src/visual/WaterfallContext.cpp +++ b/src/visual/WaterfallContext.cpp @@ -4,8 +4,8 @@ WaterfallContext::WaterfallContext(WaterfallCanvas *canvas, wxGLContext *sharedContext) : PrimaryGLContext(canvas, sharedContext) { - glEnable(GL_CULL_FACE); - glEnable(GL_DEPTH_TEST); + glDisable(GL_CULL_FACE); + glDisable(GL_DEPTH_TEST); glMatrixMode(GL_PROJECTION); glLoadIdentity();