
/*
 * 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 <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <math.h>
#include <string.h>

#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; i<channelsPerFrame; i++ )
        {
            loopbackContext->done |= 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; i<channelsPerFrame; i++ )
        {
            PaQa_MixSine( &loopbackContext->generators[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; i<loopbackContext->test->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; i<loopbackContext->test->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; i<testParams->samplesPerFrame; i++ )
    {
        int err = PaQa_InitializeRecording( &loopbackContextPtr->recordings[i], testParams->maxFrames, testParams->sampleRate );
        QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", paNoError, err );
    }
    for( i=0; i<testParams->samplesPerFrame; 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; i<loopbackContextPtr->test->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; i<testParams->samplesPerFrame; 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; iFlags<numFlagSettings; iFlags++ )
    {
        int numRuns = 0;

        testParams.flags = flagSettings[iFlags];
        printf( "\n************ Mode = %s ************\n",
               (( testParams.flags & 1 ) ? s_FlagOnNames[0] : s_FlagOffNames[0]) );

        printf("|-   requested  -|-  stream info latency  -|- measured ------------------------------\n");
        printf("|-sRate-|-fr/buf-|- in    - out   - total -|- over/under/calls for in, out -|- frm/buf -|-latency-|- channel results -\n");

        // Loop though various sample rates.
        if( userOptions->sampleRate < 0 )
        {
            savedValue = testParams.sampleRate;
            for( iRate=0; iRate<numRates; iRate++ )
            {
                int numBadChannels;

                // SAMPLE RATE
                testParams.sampleRate = sampleRates[iRate];
                testParams.maxFrames = (int) (PAQA_TEST_DURATION * testParams.sampleRate);

                numBadChannels = PaQa_SingleLoopBackTest( userOptions, &testParams );
                totalBadChannels += numBadChannels;
            }
            testParams.sampleRate = savedValue;
            testParams.maxFrames = (int) (PAQA_TEST_DURATION * testParams.sampleRate);
            printf( "\n" );
            numRuns += 1;
        }

        // Loop through various buffer sizes.
        if( userOptions->framesPerBuffer < 0 )
        {
            savedValue = testParams.framesPerBuffer;
            for( iSize=0; iSize<numBufferSizes; iSize++ )
            {
                int numBadChannels;

                // BUFFER SIZE
                testParams.framesPerBuffer = framesPerBuffers[iSize];

                numBadChannels = PaQa_SingleLoopBackTest( userOptions, &testParams );
                totalBadChannels += numBadChannels;
            }
            testParams.framesPerBuffer = savedValue;
            printf( "\n" );
            numRuns += 1;
        }
        // Run one with single parameters in case we did not do a series.
        if( numRuns == 0 )
        {
            int numBadChannels = PaQa_SingleLoopBackTest( userOptions, &testParams );
            totalBadChannels += numBadChannels;
        }
    }

    printf("\nTest Sample Formats using Half Duplex IO -----\n" );

    PaQa_SetDefaultTestParameters( &testParams, inputDevice, outputDevice );
    testParams.flags = PAQA_FLAG_TWO_STREAMS;
    for( iFlags= 0; iFlags<4; iFlags++ )
    {
        // Cycle through combinations of flags.
        testParams.streamFlags = 0;
        if( iFlags & 1 ) testParams.streamFlags |= paClipOff;
        if( iFlags & 2 ) testParams.streamFlags |= paDitherOff;

        for( iFormat=0; iFormat<numSampleFormats; iFormat++ )
        {
            int numBadChannels;
            PaSampleFormat format = sampleFormats[ iFormat ];
            testParams.inputParameters.sampleFormat = format;
            testParams.outputParameters.sampleFormat = format;
            printf("Sample format = %d = %s, PaStreamFlags = 0x%02X\n", (int) format, sampleFormatNames[iFormat], (unsigned int) testParams.streamFlags );
            numBadChannels = PaQa_SingleLoopBackTest( userOptions, &testParams );
            totalBadChannels += numBadChannels;
        }
    }
    printf( "\n" );
    printf( "****************************************\n");

    return totalBadChannels;
}

/*******************************************************************/
int PaQa_CheckForClippedLoopback( LoopbackContext *loopbackContextPtr )
{
    int clipped = 0;
    TestParameters *testParamsPtr = loopbackContextPtr->test;

    // 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; iOut<numDevices; iOut++ )
        {
            numLoopbacks += CheckLoopbackAndScan( userOptions, userOptions->inputDevice, iOut );
        }
    }
    else if (userOptions->outputDevice >= 0)
    {
        // Just scan for input.
        for( iIn=0; iIn<numDevices; iIn++ )
        {
            numLoopbacks += CheckLoopbackAndScan( userOptions, iIn, userOptions->outputDevice );
        }
    }
    else
    {
        // Scan both.
        for( iOut=0; iOut<numDevices; iOut++ )
        {
            for( iIn=0; iIn<numDevices; iIn++ )
            {
                numLoopbacks += CheckLoopbackAndScan( userOptions, iIn, iOut );
            }
        }
    }
    QA_ASSERT_TRUE( "No good loopback cable found.", (numLoopbacks > 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<argc )
    {
        char *arg = argv[i];
        if( arg[0] == '-' )
        {
            switch(arg[1])
            {
                case 'i':
                    userOptions.inputDevice = atoi(&arg[2]);
                    break;
                case 'o':
                    userOptions.outputDevice = atoi(&arg[2]);
                    break;
                case 'l':
                    userOptions.inputLatency = userOptions.outputLatency = atoi(&arg[2]);
                    break;
                case 'r':
                    userOptions.sampleRate = atoi(&arg[2]);
                    break;
                case 's':
                    userOptions.framesPerBuffer = atoi(&arg[2]);
                    break;

                case 'm':
                    printf("Option -m set so just testing math and not the audio devices.\n");
                    justMath = 1;
                    break;

                case 'w':
                    userOptions.saveBadWaves = 1;
                    break;
                case 'd':
                    userOptions.waveFilePath = &arg[2];
                    break;

                case 'v':
                    userOptions.verbose = 1;
                    break;

                case 'h':
                    usage( executableName );
                    exit(0);
                    break;

                case '-':
                {
                    if( strcmp( &arg[2], "inputLatency" ) == 0 )
                    {
                        i += 1;
                        userOptions.inputLatency = atoi(argv[i]);
                    }
                    else if( strcmp( &arg[2], "outputLatency" ) == 0 )
                    {
                        i += 1;
                        userOptions.outputLatency = atoi(argv[i]);
                    }
                    else
                    {
                        printf("Illegal option: %s\n", arg);
                        usage( executableName );
                        exit(1);
                    }

                }
                    break;


                default:
                    printf("Illegal option: %s\n", arg);
                    usage( executableName );
                    exit(1);
                    break;
            }
        }
        else
        {
            printf("Illegal argument: %s\n", arg);
            usage( executableName );
            exit(1);

        }
        i += 1;
    }

    result = PaQa_TestAnalyzer();

    // Test sample format conversion tool.
    result = TestSampleFormatConversion();

    if( (result == 0) && (justMath == 0) )
    {
        Pa_Initialize();
        printf( "PortAudio version number = %d\nPortAudio version text = '%s'\n",
               Pa_GetVersion(), Pa_GetVersionText() );
        printf( "=============== PortAudio Devices ========================\n" );
        PaQa_ListAudioDevices();
        if( Pa_GetDeviceCount() == 0 )
            printf( "no devices found.\n" );

        printf( "=============== Detect Loopback ==========================\n" );
        ScanForLoopback(&userOptions);

        Pa_Terminate();
    }

    if (g_testsFailed == 0)
    {
        printf("PortAudio QA SUCCEEDED! %d tests passed, %d tests failed\n", g_testsPassed, g_testsFailed );
        return 0;

    }
    else
    {
        printf("PortAudio QA FAILED! %d tests passed, %d tests failed\n", g_testsPassed, g_testsFailed );
        return 1;
    }
}
