/* * PortAudio Portable Real-Time Audio Library * Latest Version at: http://www.portaudio.com * * Copyright (c) 1999-2010 Phil Burk and 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. */ #include #include #include #include #include #include "portaudio.h" #include "qa_tools.h" #include "paqa_tools.h" #include "audio_analyzer.h" #include "test_audio_analyzer.h" /** Accumulate counts for how many tests pass or fail. */ int g_testsPassed = 0; int g_testsFailed = 0; #define MAX_NUM_GENERATORS (8) #define MAX_NUM_RECORDINGS (8) #define MAX_BACKGROUND_NOISE_RMS (0.0004) #define LOOPBACK_DETECTION_DURATION_SECONDS (0.8) #define DEFAULT_FRAMES_PER_BUFFER (0) #define PAQA_WAIT_STREAM_MSEC (100) #define PAQA_TEST_DURATION (1.2) // Use two separate streams instead of one full duplex stream. #define PAQA_FLAG_TWO_STREAMS (1<<0) // Use bloching read/write for loopback. #define PAQA_FLAG_USE_BLOCKING_IO (1<<1) const char * s_FlagOnNames[] = { "Two Streams (Half Duplex)", "Blocking Read/Write" }; const char * s_FlagOffNames[] = { "One Stream (Full Duplex)", "Callback" }; /** Parameters that describe a single test run. */ typedef struct TestParameters_s { PaStreamParameters inputParameters; PaStreamParameters outputParameters; double sampleRate; int samplesPerFrame; int framesPerBuffer; int maxFrames; double baseFrequency; double amplitude; PaStreamFlags streamFlags; // paClipOff, etc int flags; // PAQA_FLAG_TWO_STREAMS, PAQA_FLAG_USE_BLOCKING_IO } TestParameters; typedef struct LoopbackContext_s { // Generate a unique signal on each channel. PaQaSineGenerator generators[MAX_NUM_GENERATORS]; // Record each channel individually. PaQaRecording recordings[MAX_NUM_RECORDINGS]; // Reported by the stream after it's opened PaTime streamInfoInputLatency; PaTime streamInfoOutputLatency; // Measured at runtime. volatile int callbackCount; // incremented for each callback volatile int inputBufferCount; // incremented if input buffer not NULL int inputUnderflowCount; int inputOverflowCount; volatile int outputBufferCount; // incremented if output buffer not NULL int outputOverflowCount; int outputUnderflowCount; // Measure whether input or output is lagging behind. volatile int minInputOutputDelta; volatile int maxInputOutputDelta; int minFramesPerBuffer; int maxFramesPerBuffer; int primingCount; TestParameters *test; volatile int done; } LoopbackContext; typedef struct UserOptions_s { int sampleRate; int framesPerBuffer; int inputLatency; int outputLatency; int saveBadWaves; int verbose; int waveFileCount; const char *waveFilePath; PaDeviceIndex inputDevice; PaDeviceIndex outputDevice; } UserOptions; #define BIG_BUFFER_SIZE (sizeof(float) * 2 * 2 * 1024) static unsigned char g_ReadWriteBuffer[BIG_BUFFER_SIZE]; #define MAX_CONVERSION_SAMPLES (2 * 32 * 1024) #define CONVERSION_BUFFER_SIZE (sizeof(float) * 2 * MAX_CONVERSION_SAMPLES) static unsigned char g_ConversionBuffer[CONVERSION_BUFFER_SIZE]; /*******************************************************************/ static int RecordAndPlaySinesCallback( const void *inputBuffer, void *outputBuffer, unsigned long framesPerBuffer, const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void *userData ) { int i; LoopbackContext *loopbackContext = (LoopbackContext *) userData; loopbackContext->callbackCount += 1; if( statusFlags & paInputUnderflow ) loopbackContext->inputUnderflowCount += 1; if( statusFlags & paInputOverflow ) loopbackContext->inputOverflowCount += 1; if( statusFlags & paOutputUnderflow ) loopbackContext->outputUnderflowCount += 1; if( statusFlags & paOutputOverflow ) loopbackContext->outputOverflowCount += 1; if( statusFlags & paPrimingOutput ) loopbackContext->primingCount += 1; if( framesPerBuffer > loopbackContext->maxFramesPerBuffer ) { loopbackContext->maxFramesPerBuffer = framesPerBuffer; } if( framesPerBuffer < loopbackContext->minFramesPerBuffer ) { loopbackContext->minFramesPerBuffer = framesPerBuffer; } /* This may get called with NULL inputBuffer during initial setup. * We may also use the same callback with output only streams. */ if( inputBuffer != NULL) { int channelsPerFrame = loopbackContext->test->inputParameters.channelCount; float *in = (float *)inputBuffer; PaSampleFormat inFormat = loopbackContext->test->inputParameters.sampleFormat; loopbackContext->inputBufferCount += 1; if( inFormat != paFloat32 ) { int samplesToConvert = framesPerBuffer * channelsPerFrame; in = (float *) g_ConversionBuffer; if( samplesToConvert > MAX_CONVERSION_SAMPLES ) { // Hack to prevent buffer overflow. // @todo Loop with small buffer instead of failing. printf("Format conversion buffer too small!\n"); return paComplete; } PaQa_ConvertToFloat( inputBuffer, samplesToConvert, inFormat, (float *) g_ConversionBuffer ); } // Read each channel from the buffer. for( i=0; idone |= PaQa_WriteRecording( &loopbackContext->recordings[i], in + i, framesPerBuffer, channelsPerFrame ); } } if( outputBuffer != NULL ) { int channelsPerFrame = loopbackContext->test->outputParameters.channelCount; float *out = (float *)outputBuffer; PaSampleFormat outFormat = loopbackContext->test->outputParameters.sampleFormat; loopbackContext->outputBufferCount += 1; if( outFormat != paFloat32 ) { // If we need to convert then mix to the g_ConversionBuffer and then convert into the PA outputBuffer. out = (float *) g_ConversionBuffer; } PaQa_EraseBuffer( out, framesPerBuffer, channelsPerFrame ); for( i=0; igenerators[i], out + i, framesPerBuffer, channelsPerFrame ); } if( outFormat != paFloat32 ) { int samplesToConvert = framesPerBuffer * channelsPerFrame; if( samplesToConvert > MAX_CONVERSION_SAMPLES ) { printf("Format conversion buffer too small!\n"); return paComplete; } PaQa_ConvertFromFloat( out, framesPerBuffer * channelsPerFrame, outFormat, outputBuffer ); } } // Measure whether the input or output are lagging behind. // Don't measure lag at end. if( !loopbackContext->done ) { int inputOutputDelta = loopbackContext->inputBufferCount - loopbackContext->outputBufferCount; if( loopbackContext->maxInputOutputDelta < inputOutputDelta ) { loopbackContext->maxInputOutputDelta = inputOutputDelta; } if( loopbackContext->minInputOutputDelta > inputOutputDelta ) { loopbackContext->minInputOutputDelta = inputOutputDelta; } } return loopbackContext->done ? paComplete : paContinue; } static void CopyStreamInfoToLoopbackContext( LoopbackContext *loopbackContext, PaStream *inputStream, PaStream *outputStream ) { const PaStreamInfo *inputStreamInfo = Pa_GetStreamInfo( inputStream ); const PaStreamInfo *outputStreamInfo = Pa_GetStreamInfo( outputStream ); loopbackContext->streamInfoInputLatency = inputStreamInfo ? inputStreamInfo->inputLatency : -1; loopbackContext->streamInfoOutputLatency = outputStreamInfo ? outputStreamInfo->outputLatency : -1; } /*******************************************************************/ /** * Open a full duplex audio stream. * Generate sine waves on the output channels and record the input channels. * Then close the stream. * @return 0 if OK or negative error. */ int PaQa_RunLoopbackFullDuplex( LoopbackContext *loopbackContext ) { PaStream *stream = NULL; PaError err = 0; TestParameters *test = loopbackContext->test; loopbackContext->done = 0; // Use one full duplex stream. err = Pa_OpenStream( &stream, &test->inputParameters, &test->outputParameters, test->sampleRate, test->framesPerBuffer, paClipOff, /* we won't output out of range samples so don't bother clipping them */ RecordAndPlaySinesCallback, loopbackContext ); if( err != paNoError ) goto error; CopyStreamInfoToLoopbackContext( loopbackContext, stream, stream ); err = Pa_StartStream( stream ); if( err != paNoError ) goto error; // Wait for stream to finish. while( loopbackContext->done == 0 ) { Pa_Sleep(PAQA_WAIT_STREAM_MSEC); } err = Pa_StopStream( stream ); if( err != paNoError ) goto error; err = Pa_CloseStream( stream ); if( err != paNoError ) goto error; return 0; error: return err; } /*******************************************************************/ /** * Open two audio streams, one for input and one for output. * Generate sine waves on the output channels and record the input channels. * Then close the stream. * @return 0 if OK or paTimedOut. */ int PaQa_WaitForStream( LoopbackContext *loopbackContext ) { int timeoutMSec = 1000 * PAQA_TEST_DURATION * 2; // Wait for stream to finish or timeout. while( (loopbackContext->done == 0) && (timeoutMSec > 0) ) { Pa_Sleep(PAQA_WAIT_STREAM_MSEC); timeoutMSec -= PAQA_WAIT_STREAM_MSEC; } if( loopbackContext->done == 0 ) { printf("ERROR - stream completion timed out!"); return paTimedOut; } return 0; } /*******************************************************************/ /** * Open two audio streams, one for input and one for output. * Generate sine waves on the output channels and record the input channels. * Then close the stream. * @return 0 if OK or negative error. */ int PaQa_RunLoopbackHalfDuplex( LoopbackContext *loopbackContext ) { PaStream *inStream = NULL; PaStream *outStream = NULL; PaError err = 0; int timedOut = 0; TestParameters *test = loopbackContext->test; loopbackContext->done = 0; // Use two half duplex streams. err = Pa_OpenStream( &inStream, &test->inputParameters, NULL, test->sampleRate, test->framesPerBuffer, test->streamFlags, RecordAndPlaySinesCallback, loopbackContext ); if( err != paNoError ) goto error; err = Pa_OpenStream( &outStream, NULL, &test->outputParameters, test->sampleRate, test->framesPerBuffer, test->streamFlags, RecordAndPlaySinesCallback, loopbackContext ); if( err != paNoError ) goto error; CopyStreamInfoToLoopbackContext( loopbackContext, inStream, outStream ); err = Pa_StartStream( inStream ); if( err != paNoError ) goto error; // Start output later so we catch the beginning of the waveform. err = Pa_StartStream( outStream ); if( err != paNoError ) goto error; timedOut = PaQa_WaitForStream( loopbackContext ); err = Pa_StopStream( inStream ); if( err != paNoError ) goto error; err = Pa_StopStream( outStream ); if( err != paNoError ) goto error; err = Pa_CloseStream( inStream ); if( err != paNoError ) goto error; err = Pa_CloseStream( outStream ); if( err != paNoError ) goto error; return timedOut; error: return err; } /*******************************************************************/ /** * Open one audio streams, just for input. * Record background level. * Then close the stream. * @return 0 if OK or negative error. */ int PaQa_RunInputOnly( LoopbackContext *loopbackContext ) { PaStream *inStream = NULL; PaError err = 0; int timedOut = 0; TestParameters *test = loopbackContext->test; loopbackContext->done = 0; // Just open an input stream. err = Pa_OpenStream( &inStream, &test->inputParameters, NULL, test->sampleRate, test->framesPerBuffer, paClipOff, /* We won't output out of range samples so don't bother clipping them. */ RecordAndPlaySinesCallback, loopbackContext ); if( err != paNoError ) goto error; err = Pa_StartStream( inStream ); if( err != paNoError ) goto error; timedOut = PaQa_WaitForStream( loopbackContext ); err = Pa_StopStream( inStream ); if( err != paNoError ) goto error; err = Pa_CloseStream( inStream ); if( err != paNoError ) goto error; return timedOut; error: return err; } /*******************************************************************/ static int RecordAndPlayBlockingIO( PaStream *inStream, PaStream *outStream, LoopbackContext *loopbackContext ) { int i; float *in = (float *)g_ReadWriteBuffer; float *out = (float *)g_ReadWriteBuffer; PaError err; int done = 0; long available; const long maxPerBuffer = 64; TestParameters *test = loopbackContext->test; long framesPerBuffer = test->framesPerBuffer; if( framesPerBuffer <= 0 ) { framesPerBuffer = maxPerBuffer; // bigger values might run past end of recording } // Read in audio. err = Pa_ReadStream( inStream, in, framesPerBuffer ); // Ignore an overflow on the first read. //if( !((loopbackContext->callbackCount == 0) && (err == paInputOverflowed)) ) if( err != paInputOverflowed ) { QA_ASSERT_EQUALS( "Pa_ReadStream failed", paNoError, err ); } else { loopbackContext->inputOverflowCount += 1; } // Save in a recording. for( i=0; itest->inputParameters.channelCount; i++ ) { done |= PaQa_WriteRecording( &loopbackContext->recordings[i], in + i, framesPerBuffer, loopbackContext->test->inputParameters.channelCount ); } // Synthesize audio. available = Pa_GetStreamWriteAvailable( outStream ); if( available > (2*framesPerBuffer) ) available = (2*framesPerBuffer); PaQa_EraseBuffer( out, available, loopbackContext->test->outputParameters.channelCount ); for( i=0; itest->outputParameters.channelCount; i++ ) { PaQa_MixSine( &loopbackContext->generators[i], out + i, available, loopbackContext->test->outputParameters.channelCount ); } // Write out audio. err = Pa_WriteStream( outStream, out, available ); // Ignore an underflow on the first write. //if( !((loopbackContext->callbackCount == 0) && (err == paOutputUnderflowed)) ) if( err != paOutputUnderflowed ) { QA_ASSERT_EQUALS( "Pa_WriteStream failed", paNoError, err ); } else { loopbackContext->outputUnderflowCount += 1; } loopbackContext->callbackCount += 1; return done; error: return err; } /*******************************************************************/ /** * Open two audio streams with non-blocking IO. * Generate sine waves on the output channels and record the input channels. * Then close the stream. * @return 0 if OK or negative error. */ int PaQa_RunLoopbackHalfDuplexBlockingIO( LoopbackContext *loopbackContext ) { PaStream *inStream = NULL; PaStream *outStream = NULL; PaError err = 0; TestParameters *test = loopbackContext->test; // Use two half duplex streams. err = Pa_OpenStream( &inStream, &test->inputParameters, NULL, test->sampleRate, test->framesPerBuffer, paClipOff, /* we won't output out of range samples so don't bother clipping them */ NULL, // causes non-blocking IO NULL ); if( err != paNoError ) goto error1; err = Pa_OpenStream( &outStream, NULL, &test->outputParameters, test->sampleRate, test->framesPerBuffer, paClipOff, /* we won't output out of range samples so don't bother clipping them */ NULL, // causes non-blocking IO NULL ); if( err != paNoError ) goto error2; CopyStreamInfoToLoopbackContext( loopbackContext, inStream, outStream ); err = Pa_StartStream( outStream ); if( err != paNoError ) goto error3; err = Pa_StartStream( inStream ); if( err != paNoError ) goto error3; while( err == 0 ) { err = RecordAndPlayBlockingIO( inStream, outStream, loopbackContext ); if( err < 0 ) goto error3; } err = Pa_StopStream( inStream ); if( err != paNoError ) goto error3; err = Pa_StopStream( outStream ); if( err != paNoError ) goto error3; err = Pa_CloseStream( outStream ); if( err != paNoError ) goto error2; err = Pa_CloseStream( inStream ); if( err != paNoError ) goto error1; return 0; error3: Pa_CloseStream( outStream ); error2: Pa_CloseStream( inStream ); error1: return err; } /*******************************************************************/ /** * Open one audio stream with non-blocking IO. * Generate sine waves on the output channels and record the input channels. * Then close the stream. * @return 0 if OK or negative error. */ int PaQa_RunLoopbackFullDuplexBlockingIO( LoopbackContext *loopbackContext ) { PaStream *stream = NULL; PaError err = 0; TestParameters *test = loopbackContext->test; // Use one full duplex stream. err = Pa_OpenStream( &stream, &test->inputParameters, &test->outputParameters, test->sampleRate, test->framesPerBuffer, paClipOff, /* we won't output out of range samples so don't bother clipping them */ NULL, // causes non-blocking IO NULL ); if( err != paNoError ) goto error1; CopyStreamInfoToLoopbackContext( loopbackContext, stream, stream ); err = Pa_StartStream( stream ); if( err != paNoError ) goto error2; while( err == 0 ) { err = RecordAndPlayBlockingIO( stream, stream, loopbackContext ); if( err < 0 ) goto error2; } err = Pa_StopStream( stream ); if( err != paNoError ) goto error2; err = Pa_CloseStream( stream ); if( err != paNoError ) goto error1; return 0; error2: Pa_CloseStream( stream ); error1: return err; } /*******************************************************************/ /** * Run some kind of loopback test. * @return 0 if OK or negative error. */ int PaQa_RunLoopback( LoopbackContext *loopbackContext ) { PaError err = 0; TestParameters *test = loopbackContext->test; if( test->flags & PAQA_FLAG_TWO_STREAMS ) { if( test->flags & PAQA_FLAG_USE_BLOCKING_IO ) { err = PaQa_RunLoopbackHalfDuplexBlockingIO( loopbackContext ); } else { err = PaQa_RunLoopbackHalfDuplex( loopbackContext ); } } else { if( test->flags & PAQA_FLAG_USE_BLOCKING_IO ) { err = PaQa_RunLoopbackFullDuplexBlockingIO( loopbackContext ); } else { err = PaQa_RunLoopbackFullDuplex( loopbackContext ); } } if( err != paNoError ) { printf("PortAudio error = %s\n", Pa_GetErrorText( err ) ); } return err; } /*******************************************************************/ static int PaQa_SaveTestResultToWaveFile( UserOptions *userOptions, PaQaRecording *recording ) { if( userOptions->saveBadWaves ) { char filename[256]; #ifdef WIN32 _snprintf( filename, sizeof(filename), "%s\\paloopback_%d.wav", userOptions->waveFilePath, userOptions->waveFileCount++ ); #else snprintf( filename, sizeof(filename), "%s/paloopback_%d.wav", userOptions->waveFilePath, userOptions->waveFileCount++ ); #endif printf( "\"%s\", ", filename ); return PaQa_SaveRecordingToWaveFile( recording, filename ); } return 0; } /*******************************************************************/ static int PaQa_SetupLoopbackContext( LoopbackContext *loopbackContextPtr, TestParameters *testParams ) { int i; // Setup loopback context. memset( loopbackContextPtr, 0, sizeof(LoopbackContext) ); loopbackContextPtr->test = testParams; for( i=0; isamplesPerFrame; i++ ) { int err = PaQa_InitializeRecording( &loopbackContextPtr->recordings[i], testParams->maxFrames, testParams->sampleRate ); QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", paNoError, err ); } for( i=0; isamplesPerFrame; i++ ) { PaQa_SetupSineGenerator( &loopbackContextPtr->generators[i], PaQa_GetNthFrequency( testParams->baseFrequency, i ), testParams->amplitude, testParams->sampleRate ); } loopbackContextPtr->minFramesPerBuffer = 0x0FFFFFFF; return 0; error: return -1; } /*******************************************************************/ static void PaQa_TeardownLoopbackContext( LoopbackContext *loopbackContextPtr ) { int i; if( loopbackContextPtr->test != NULL ) { for( i=0; itest->samplesPerFrame; i++ ) { PaQa_TerminateRecording( &loopbackContextPtr->recordings[i] ); } } } /*******************************************************************/ static void PaQa_PrintShortErrorReport( PaQaAnalysisResult *analysisResultPtr, int channel ) { printf("channel %d ", channel); if( analysisResultPtr->popPosition > 0 ) { printf("POP %0.3f at %d, ", (double)analysisResultPtr->popAmplitude, (int)analysisResultPtr->popPosition ); } else { if( analysisResultPtr->addedFramesPosition > 0 ) { printf("ADD %d at %d ", (int)analysisResultPtr->numAddedFrames, (int)analysisResultPtr->addedFramesPosition ); } if( analysisResultPtr->droppedFramesPosition > 0 ) { printf("DROP %d at %d ", (int)analysisResultPtr->numDroppedFrames, (int)analysisResultPtr->droppedFramesPosition ); } } } /*******************************************************************/ static void PaQa_PrintFullErrorReport( PaQaAnalysisResult *analysisResultPtr, int channel ) { printf("\n=== Loopback Analysis ===================\n"); printf(" channel: %d\n", channel ); printf(" latency: %10.3f\n", analysisResultPtr->latency ); printf(" amplitudeRatio: %10.3f\n", (double)analysisResultPtr->amplitudeRatio ); printf(" popPosition: %10.3f\n", (double)analysisResultPtr->popPosition ); printf(" popAmplitude: %10.3f\n", (double)analysisResultPtr->popAmplitude ); printf(" num added frames: %10.3f\n", analysisResultPtr->numAddedFrames ); printf(" added frames at: %10.3f\n", analysisResultPtr->addedFramesPosition ); printf(" num dropped frames: %10.3f\n", analysisResultPtr->numDroppedFrames ); printf(" dropped frames at: %10.3f\n", analysisResultPtr->droppedFramesPosition ); } /*******************************************************************/ /** * Test loopback connection using the given parameters. * @return number of channels with glitches, or negative error. */ static int PaQa_SingleLoopBackTest( UserOptions *userOptions, TestParameters *testParams ) { int i; LoopbackContext loopbackContext; PaError err = paNoError; PaQaTestTone testTone; PaQaAnalysisResult analysisResult; int numBadChannels = 0; printf("| %5d | %6d | ", ((int)(testParams->sampleRate+0.5)), testParams->framesPerBuffer ); fflush(stdout); testTone.samplesPerFrame = testParams->samplesPerFrame; testTone.sampleRate = testParams->sampleRate; testTone.amplitude = testParams->amplitude; testTone.startDelay = 0; err = PaQa_SetupLoopbackContext( &loopbackContext, testParams ); if( err ) return err; err = PaQa_RunLoopback( &loopbackContext ); QA_ASSERT_TRUE("loopback did not run", (loopbackContext.callbackCount > 1) ); printf( "%7.2f %7.2f %7.2f | ", loopbackContext.streamInfoInputLatency * 1000.0, loopbackContext.streamInfoOutputLatency * 1000.0, (loopbackContext.streamInfoInputLatency + loopbackContext.streamInfoOutputLatency) * 1000.0 ); printf( "%4d/%4d/%4d, %4d/%4d/%4d | ", loopbackContext.inputOverflowCount, loopbackContext.inputUnderflowCount, loopbackContext.inputBufferCount, loopbackContext.outputOverflowCount, loopbackContext.outputUnderflowCount, loopbackContext.outputBufferCount ); // Analyse recording to detect glitches. for( i=0; isamplesPerFrame; i++ ) { double freq = PaQa_GetNthFrequency( testParams->baseFrequency, i ); testTone.frequency = freq; PaQa_AnalyseRecording( &loopbackContext.recordings[i], &testTone, &analysisResult ); if( i==0 ) { double latencyMSec; printf( "%4d-%4d | ", loopbackContext.minFramesPerBuffer, loopbackContext.maxFramesPerBuffer ); latencyMSec = 1000.0 * analysisResult.latency / testParams->sampleRate; printf("%7.2f | ", latencyMSec ); } if( analysisResult.valid ) { int badChannel = ( (analysisResult.popPosition > 0) || (analysisResult.addedFramesPosition > 0) || (analysisResult.droppedFramesPosition > 0) ); if( badChannel ) { if( userOptions->verbose ) { PaQa_PrintFullErrorReport( &analysisResult, i ); } else { PaQa_PrintShortErrorReport( &analysisResult, i ); } PaQa_SaveTestResultToWaveFile( userOptions, &loopbackContext.recordings[i] ); } numBadChannels += badChannel; } else { printf( "[%d] No or low signal, ampRatio = %f", i, analysisResult.amplitudeRatio ); numBadChannels += 1; } } if( numBadChannels == 0 ) { printf( "OK" ); } // Print the # errors so far to make it easier to see where the error occurred. printf( " - #errs = %d\n", g_testsFailed ); PaQa_TeardownLoopbackContext( &loopbackContext ); if( numBadChannels > 0 ) { g_testsFailed += 1; } return numBadChannels; error: PaQa_TeardownLoopbackContext( &loopbackContext ); printf( "\n" ); g_testsFailed += 1; return err; } /*******************************************************************/ static void PaQa_SetDefaultTestParameters( TestParameters *testParamsPtr, PaDeviceIndex inputDevice, PaDeviceIndex outputDevice ) { memset( testParamsPtr, 0, sizeof(TestParameters) ); testParamsPtr->samplesPerFrame = 2; testParamsPtr->amplitude = 0.5; testParamsPtr->sampleRate = 44100; testParamsPtr->maxFrames = (int) (PAQA_TEST_DURATION * testParamsPtr->sampleRate); testParamsPtr->framesPerBuffer = DEFAULT_FRAMES_PER_BUFFER; testParamsPtr->baseFrequency = 200.0; testParamsPtr->flags = PAQA_FLAG_TWO_STREAMS; testParamsPtr->streamFlags = paClipOff; /* we won't output out of range samples so don't bother clipping them */ testParamsPtr->inputParameters.device = inputDevice; testParamsPtr->inputParameters.sampleFormat = paFloat32; testParamsPtr->inputParameters.channelCount = testParamsPtr->samplesPerFrame; testParamsPtr->inputParameters.suggestedLatency = Pa_GetDeviceInfo( inputDevice )->defaultLowInputLatency; //testParamsPtr->inputParameters.suggestedLatency = Pa_GetDeviceInfo( inputDevice )->defaultHighInputLatency; testParamsPtr->outputParameters.device = outputDevice; testParamsPtr->outputParameters.sampleFormat = paFloat32; testParamsPtr->outputParameters.channelCount = testParamsPtr->samplesPerFrame; testParamsPtr->outputParameters.suggestedLatency = Pa_GetDeviceInfo( outputDevice )->defaultLowOutputLatency; //testParamsPtr->outputParameters.suggestedLatency = Pa_GetDeviceInfo( outputDevice )->defaultHighOutputLatency; } /*******************************************************************/ static void PaQa_OverrideTestParameters( TestParameters *testParamsPtr, UserOptions *userOptions ) { // Check to see if a specific value was requested. if( userOptions->sampleRate >= 0 ) { testParamsPtr->sampleRate = userOptions->sampleRate; testParamsPtr->maxFrames = (int) (PAQA_TEST_DURATION * testParamsPtr->sampleRate); } if( userOptions->framesPerBuffer >= 0 ) { testParamsPtr->framesPerBuffer = userOptions->framesPerBuffer; } if( userOptions->inputLatency >= 0 ) { testParamsPtr->inputParameters.suggestedLatency = userOptions->inputLatency * 0.001; } if( userOptions->outputLatency >= 0 ) { testParamsPtr->outputParameters.suggestedLatency = userOptions->outputLatency * 0.001; } printf( " Running with suggested latency (msec): input = %5.2f, out = %5.2f\n", (testParamsPtr->inputParameters.suggestedLatency * 1000.0), (testParamsPtr->outputParameters.suggestedLatency * 1000.0) ); } /*******************************************************************/ /** * Run a series of tests on this loopback connection. * @return number of bad channel results */ static int PaQa_AnalyzeLoopbackConnection( UserOptions *userOptions, PaDeviceIndex inputDevice, PaDeviceIndex outputDevice ) { int iFlags; int iRate; int iSize; int iFormat; int savedValue; TestParameters testParams; const PaDeviceInfo *inputDeviceInfo = Pa_GetDeviceInfo( inputDevice ); const PaDeviceInfo *outputDeviceInfo = Pa_GetDeviceInfo( outputDevice ); int totalBadChannels = 0; // test half duplex first because it is more likely to work. int flagSettings[] = { PAQA_FLAG_TWO_STREAMS, 0 }; int numFlagSettings = (sizeof(flagSettings)/sizeof(int)); double sampleRates[] = { 8000.0, 11025.0, 16000.0, 22050.0, 32000.0, 44100.0, 48000.0, 96000.0 }; int numRates = (sizeof(sampleRates)/sizeof(double)); // framesPerBuffer==0 means PA decides on the buffer size. int framesPerBuffers[] = { 0, 16, 32, 40, 64, 100, 128, 256, 512, 1024 }; int numBufferSizes = (sizeof(framesPerBuffers)/sizeof(int)); PaSampleFormat sampleFormats[] = { paFloat32, paUInt8, paInt8, paInt16, paInt32 }; const char *sampleFormatNames[] = { "paFloat32", "paUInt8", "paInt8", "paInt16", "paInt32" }; int numSampleFormats = (sizeof(sampleFormats)/sizeof(PaSampleFormat)); printf( "=============== Analysing Loopback %d to %d =====================\n", outputDevice, inputDevice ); printf( " Devices: %s => %s\n", outputDeviceInfo->name, inputDeviceInfo->name); PaQa_SetDefaultTestParameters( &testParams, inputDevice, outputDevice ); PaQa_OverrideTestParameters( &testParams, userOptions ); // Loop though combinations of audio parameters. for( iFlags=0; iFlagssampleRate < 0 ) { savedValue = testParams.sampleRate; for( iRate=0; iRateframesPerBuffer < 0 ) { savedValue = testParams.framesPerBuffer; for( iSize=0; iSizetest; // Start in the middle assuming past latency. int startFrame = testParamsPtr->maxFrames/2; int numFrames = testParamsPtr->maxFrames/2; // Check to see if the signal is clipped. double amplitudeLeft = PaQa_MeasureSineAmplitudeBySlope( &loopbackContextPtr->recordings[0], testParamsPtr->baseFrequency, testParamsPtr->sampleRate, startFrame, numFrames ); double gainLeft = amplitudeLeft / testParamsPtr->amplitude; double amplitudeRight = PaQa_MeasureSineAmplitudeBySlope( &loopbackContextPtr->recordings[1], testParamsPtr->baseFrequency, testParamsPtr->sampleRate, startFrame, numFrames ); double gainRight = amplitudeLeft / testParamsPtr->amplitude; printf(" Loop gain: left = %f, right = %f\n", gainLeft, gainRight ); if( (amplitudeLeft > 1.0 ) || (amplitudeRight > 1.0) ) { printf("ERROR - loop gain is too high. Should be around than 1.0. Please lower output level and/or input gain.\n" ); clipped = 1; } return clipped; } /*******************************************************************/ int PaQa_MeasureBackgroundNoise( LoopbackContext *loopbackContextPtr, double *rmsPtr ) { int result = 0; *rmsPtr = 0.0; // Rewind so we can record some input. loopbackContextPtr->recordings[0].numFrames = 0; loopbackContextPtr->recordings[1].numFrames = 0; result = PaQa_RunInputOnly( loopbackContextPtr ); if( result == 0 ) { double leftRMS = PaQa_MeasureRootMeanSquare( loopbackContextPtr->recordings[0].buffer, loopbackContextPtr->recordings[0].numFrames ); double rightRMS = PaQa_MeasureRootMeanSquare( loopbackContextPtr->recordings[1].buffer, loopbackContextPtr->recordings[1].numFrames ); *rmsPtr = (leftRMS + rightRMS) / 2.0; } return result; } /*******************************************************************/ /** * Output a sine wave then try to detect it on input. * * @return 1 if loopback connected, 0 if not, or negative error. */ int PaQa_CheckForLoopBack( UserOptions *userOptions, PaDeviceIndex inputDevice, PaDeviceIndex outputDevice ) { TestParameters testParams; LoopbackContext loopbackContext; const PaDeviceInfo *inputDeviceInfo; const PaDeviceInfo *outputDeviceInfo; PaError err = paNoError; double minAmplitude; int loopbackIsConnected; int startFrame, numFrames; double magLeft, magRight; inputDeviceInfo = Pa_GetDeviceInfo( inputDevice ); if( inputDeviceInfo == NULL ) { printf("ERROR - Pa_GetDeviceInfo for input returned NULL.\n"); return paInvalidDevice; } if( inputDeviceInfo->maxInputChannels < 2 ) { return 0; } outputDeviceInfo = Pa_GetDeviceInfo( outputDevice ); if( outputDeviceInfo == NULL ) { printf("ERROR - Pa_GetDeviceInfo for output returned NULL.\n"); return paInvalidDevice; } if( outputDeviceInfo->maxOutputChannels < 2 ) { return 0; } printf( "Look for loopback cable between \"%s\" => \"%s\"\n", outputDeviceInfo->name, inputDeviceInfo->name); printf( " Default suggested input latency (msec): low = %5.2f, high = %5.2f\n", (inputDeviceInfo->defaultLowInputLatency * 1000.0), (inputDeviceInfo->defaultHighInputLatency * 1000.0) ); printf( " Default suggested output latency (msec): low = %5.2f, high = %5.2f\n", (outputDeviceInfo->defaultLowOutputLatency * 1000.0), (outputDeviceInfo->defaultHighOutputLatency * 1000.0) ); PaQa_SetDefaultTestParameters( &testParams, inputDevice, outputDevice ); PaQa_OverrideTestParameters( &testParams, userOptions ); testParams.maxFrames = (int) (LOOPBACK_DETECTION_DURATION_SECONDS * testParams.sampleRate); minAmplitude = testParams.amplitude / 4.0; // Check to see if the selected formats are supported. if( Pa_IsFormatSupported( &testParams.inputParameters, NULL, testParams.sampleRate ) != paFormatIsSupported ) { printf( "Input not supported for this format!\n" ); return 0; } if( Pa_IsFormatSupported( NULL, &testParams.outputParameters, testParams.sampleRate ) != paFormatIsSupported ) { printf( "Output not supported for this format!\n" ); return 0; } PaQa_SetupLoopbackContext( &loopbackContext, &testParams ); if( inputDevice == outputDevice ) { // Use full duplex if checking for loopback on one device. testParams.flags &= ~PAQA_FLAG_TWO_STREAMS; } else { // Use half duplex if checking for loopback on two different device. testParams.flags = PAQA_FLAG_TWO_STREAMS; } err = PaQa_RunLoopback( &loopbackContext ); QA_ASSERT_TRUE("loopback detection callback did not run", (loopbackContext.callbackCount > 1) ); // Analyse recording to see if we captured the output. // Start in the middle assuming past latency. startFrame = testParams.maxFrames/2; numFrames = testParams.maxFrames/2; magLeft = PaQa_CorrelateSine( &loopbackContext.recordings[0], loopbackContext.generators[0].frequency, testParams.sampleRate, startFrame, numFrames, NULL ); magRight = PaQa_CorrelateSine( &loopbackContext.recordings[1], loopbackContext.generators[1].frequency, testParams.sampleRate, startFrame, numFrames, NULL ); printf(" Amplitudes: left = %f, right = %f\n", magLeft, magRight ); // Check for backwards cable. loopbackIsConnected = ((magLeft > minAmplitude) && (magRight > minAmplitude)); if( !loopbackIsConnected ) { double magLeftReverse = PaQa_CorrelateSine( &loopbackContext.recordings[0], loopbackContext.generators[1].frequency, testParams.sampleRate, startFrame, numFrames, NULL ); double magRightReverse = PaQa_CorrelateSine( &loopbackContext.recordings[1], loopbackContext.generators[0].frequency, testParams.sampleRate, startFrame, numFrames, NULL ); if ((magLeftReverse > minAmplitude) && (magRightReverse>minAmplitude)) { printf("ERROR - You seem to have the left and right channels swapped on the loopback cable!\n"); } } else { double rms = 0.0; if( PaQa_CheckForClippedLoopback( &loopbackContext ) ) { // Clipped so don't use this loopback. loopbackIsConnected = 0; } err = PaQa_MeasureBackgroundNoise( &loopbackContext, &rms ); printf(" Background noise = %f\n", rms ); if( err ) { printf("ERROR - Could not measure background noise on this input!\n"); loopbackIsConnected = 0; } else if( rms > MAX_BACKGROUND_NOISE_RMS ) { printf("ERROR - There is too much background noise on this input!\n"); loopbackIsConnected = 0; } } PaQa_TeardownLoopbackContext( &loopbackContext ); return loopbackIsConnected; error: PaQa_TeardownLoopbackContext( &loopbackContext ); return err; } /*******************************************************************/ /** * If there is a loopback connection then run the analysis. */ static int CheckLoopbackAndScan( UserOptions *userOptions, PaDeviceIndex iIn, PaDeviceIndex iOut ) { int loopbackConnected = PaQa_CheckForLoopBack( userOptions, iIn, iOut ); if( loopbackConnected > 0 ) { PaQa_AnalyzeLoopbackConnection( userOptions, iIn, iOut ); return 1; } return 0; } /*******************************************************************/ /** * Scan every combination of output to input device. * If a loopback is found the analyse the combination. * The scan can be overridden using the -i and -o command line options. */ static int ScanForLoopback(UserOptions *userOptions) { PaDeviceIndex iIn,iOut; int numLoopbacks = 0; int numDevices; numDevices = Pa_GetDeviceCount(); // If both devices are specified then just use that combination. if ((userOptions->inputDevice >= 0) && (userOptions->outputDevice >= 0)) { numLoopbacks += CheckLoopbackAndScan( userOptions, userOptions->inputDevice, userOptions->outputDevice ); } else if (userOptions->inputDevice >= 0) { // Just scan for output. for( iOut=0; iOutinputDevice, iOut ); } } else if (userOptions->outputDevice >= 0) { // Just scan for input. for( iIn=0; iInoutputDevice ); } } else { // Scan both. for( iOut=0; iOut 0) ); return numLoopbacks; error: return -1; } /*==========================================================================================*/ int TestSampleFormatConversion( void ) { int i; const float floatInput[] = { 1.0, 0.5, -0.5, -1.0 }; const char charInput[] = { 127, 64, -64, -128 }; const unsigned char ucharInput[] = { 255, 128+64, 64, 0 }; const short shortInput[] = { 32767, 32768/2, -32768/2, -32768 }; const int intInput[] = { 2147483647, 2147483647/2, -1073741824 /*-2147483648/2 doesn't work in msvc*/, -2147483648 }; float floatOutput[4]; short shortOutput[4]; int intOutput[4]; unsigned char ucharOutput[4]; char charOutput[4]; QA_ASSERT_EQUALS("int must be 32-bit", 4, (int) sizeof(int) ); QA_ASSERT_EQUALS("short must be 16-bit", 2, (int) sizeof(short) ); // from Float ====== PaQa_ConvertFromFloat( floatInput, 4, paUInt8, ucharOutput ); for( i=0; i<4; i++ ) { QA_ASSERT_CLOSE_INT( "paFloat32 -> paUInt8 -> error", ucharInput[i], ucharOutput[i], 1 ); } PaQa_ConvertFromFloat( floatInput, 4, paInt8, charOutput ); for( i=0; i<4; i++ ) { QA_ASSERT_CLOSE_INT( "paFloat32 -> paInt8 -> error", charInput[i], charOutput[i], 1 ); } PaQa_ConvertFromFloat( floatInput, 4, paInt16, shortOutput ); for( i=0; i<4; i++ ) { QA_ASSERT_CLOSE_INT( "paFloat32 -> paInt16 error", shortInput[i], shortOutput[i], 1 ); } PaQa_ConvertFromFloat( floatInput, 4, paInt32, intOutput ); for( i=0; i<4; i++ ) { QA_ASSERT_CLOSE_INT( "paFloat32 -> paInt32 error", intInput[i], intOutput[i], 0x00010000 ); } // to Float ====== memset( floatOutput, 0, sizeof(floatOutput) ); PaQa_ConvertToFloat( ucharInput, 4, paUInt8, floatOutput ); for( i=0; i<4; i++ ) { QA_ASSERT_CLOSE( "paUInt8 -> paFloat32 error", floatInput[i], floatOutput[i], 0.01 ); } memset( floatOutput, 0, sizeof(floatOutput) ); PaQa_ConvertToFloat( charInput, 4, paInt8, floatOutput ); for( i=0; i<4; i++ ) { QA_ASSERT_CLOSE( "paInt8 -> paFloat32 error", floatInput[i], floatOutput[i], 0.01 ); } memset( floatOutput, 0, sizeof(floatOutput) ); PaQa_ConvertToFloat( shortInput, 4, paInt16, floatOutput ); for( i=0; i<4; i++ ) { QA_ASSERT_CLOSE( "paInt16 -> paFloat32 error", floatInput[i], floatOutput[i], 0.001 ); } memset( floatOutput, 0, sizeof(floatOutput) ); PaQa_ConvertToFloat( intInput, 4, paInt32, floatOutput ); for( i=0; i<4; i++ ) { QA_ASSERT_CLOSE( "paInt32 -> paFloat32 error", floatInput[i], floatOutput[i], 0.00001 ); } return 0; error: return -1; } /*******************************************************************/ void usage( const char *name ) { printf("%s [-i# -o# -l# -r# -s# -m -w -dDir]\n", name); printf(" -i# - Input device ID. Will scan for loopback cable if not specified.\n"); printf(" -o# - Output device ID. Will scan for loopback if not specified.\n"); printf(" -l# - Latency for both input and output in milliseconds.\n"); printf(" --inputLatency # Input latency in milliseconds.\n"); printf(" --outputLatency # Output latency in milliseconds.\n"); printf(" -r# - Sample Rate in Hz. Will use multiple common rates if not specified.\n"); printf(" -s# - Size of callback buffer in frames, framesPerBuffer. Will use common values if not specified.\n"); printf(" -w - Save bad recordings in a WAV file.\n"); printf(" -dDir - Path for Directory for WAV files. Default is current directory.\n"); printf(" -m - Just test the DSP Math code and not the audio devices.\n"); printf(" -v - Verbose reports.\n"); } /*******************************************************************/ int main( int argc, char **argv ) { int i; UserOptions userOptions; int result = 0; int justMath = 0; char *executableName = argv[0]; printf("PortAudio LoopBack Test built " __DATE__ " at " __TIME__ "\n"); if( argc > 1 ){ printf("running with arguments:"); for(i=1; i < argc; ++i ) printf(" %s", argv[i] ); printf("\n"); }else{ printf("running with no arguments\n"); } memset(&userOptions, 0, sizeof(userOptions)); userOptions.inputDevice = paNoDevice; userOptions.outputDevice = paNoDevice; userOptions.sampleRate = -1; userOptions.framesPerBuffer = -1; userOptions.inputLatency = -1; userOptions.outputLatency = -1; userOptions.waveFilePath = "."; // Process arguments. Skip name of executable. i = 1; while( i