diff options
Diffstat (limited to 'portaudio/qa')
-rw-r--r-- | portaudio/qa/loopback/README.txt | 92 | ||||
-rw-r--r-- | portaudio/qa/loopback/src/audio_analyzer.c | 706 | ||||
-rw-r--r-- | portaudio/qa/loopback/src/audio_analyzer.h | 187 | ||||
-rwxr-xr-x | portaudio/qa/loopback/src/biquad_filter.c | 123 | ||||
-rwxr-xr-x | portaudio/qa/loopback/src/biquad_filter.h | 38 | ||||
-rw-r--r-- | portaudio/qa/loopback/src/paqa.c | 1601 | ||||
-rw-r--r-- | portaudio/qa/loopback/src/paqa_tools.c | 171 | ||||
-rw-r--r-- | portaudio/qa/loopback/src/paqa_tools.h | 52 | ||||
-rwxr-xr-x | portaudio/qa/loopback/src/qa_tools.h | 83 | ||||
-rw-r--r-- | portaudio/qa/loopback/src/test_audio_analyzer.c | 718 | ||||
-rw-r--r-- | portaudio/qa/loopback/src/test_audio_analyzer.h | 46 | ||||
-rwxr-xr-x | portaudio/qa/loopback/src/write_wav.c | 242 | ||||
-rwxr-xr-x | portaudio/qa/loopback/src/write_wav.h | 103 | ||||
-rw-r--r-- | portaudio/qa/paqa_devs.c | 454 | ||||
-rw-r--r-- | portaudio/qa/paqa_errs.c | 403 | ||||
-rw-r--r-- | portaudio/qa/paqa_latency.c | 482 |
16 files changed, 5501 insertions, 0 deletions
diff --git a/portaudio/qa/loopback/README.txt b/portaudio/qa/loopback/README.txt new file mode 100644 index 0000000..5ad0280 --- /dev/null +++ b/portaudio/qa/loopback/README.txt @@ -0,0 +1,92 @@ +README for PortAudio Loopback Test + +Copyright (c) 1999-2010 Phil Burk and Ross Bencina +See complete license at end of file. + +This folder contains code for a single executable that does a standalone test of PortAudio. +It does not require a human to listen to the result. Instead it listens to itself using +a loopback cable connected between the audio output and the audio input. Special pop detectors +and phase analysers can detect errors in the audio stream. + +This test can be run from a script as part of a nightly build and test. + +--- How to Build the Loopback Test --- + +The loopback test is not normally built by the makefile. +To build the loopback test, enter: + + ./configure && make loopback + +This will build the "bin/paloopback" executable. + +--- How To Run Test --- + +Connect stereo cables from one or more output audio devices to audio input devices. +The test will scan all the ports and find the cables. + +Adjust the volume levels of the hardware so you get a decent signal that will not clip. + +Run the test from the command line with the following options: + + -i# Input device ID. Will scan for loopback if not specified. + -o# Output device ID. Will scan for loopback if not specified. + -r# Sample Rate in Hz. Will use multiple common rates if not specified. + -s# Size of callback buffer in frames, framesPerBuffer. + -w Save bad recordings in a WAV file. + -dDir Path for Directory for WAV files. Default is current directory. + -m Just test the DSP Math code and not the audio devices. + +If the -w option is set then any tests that fail will save the recording of the broken +channel in a WAV file. The files will be numbered and shown in the report. + +--- ToDo --- + +* Add check for harmonic and enharmonic distortion. +* Measure min/max peak values. +* Detect DC bias. +* Test against matrix of devices/APIs and settings. +* Detect mono vs stereo loopback. +* More command line options + --quick + --latency + --duration +* Automated build and test script with cron job. +* Test on Windows. + + +/* + * PortAudio Portable Real-Time Audio Library + * Latest Version at: http://www.portaudio.com + * + * Copyright (c) 1999-2008 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. + */ diff --git a/portaudio/qa/loopback/src/audio_analyzer.c b/portaudio/qa/loopback/src/audio_analyzer.c new file mode 100644 index 0000000..fbdd631 --- /dev/null +++ b/portaudio/qa/loopback/src/audio_analyzer.c @@ -0,0 +1,706 @@ + +/* + * 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 <string.h> +#include <assert.h> +#include <math.h> +#include "qa_tools.h" +#include "audio_analyzer.h" +#include "write_wav.h" + +#define PAQA_POP_THRESHOLD (0.04) + +/*==========================================================================================*/ +double PaQa_GetNthFrequency( double baseFrequency, int index ) +{ + // Use 13 tone equal tempered scale because it does not generate harmonic ratios. + return baseFrequency * pow( 2.0, index / 13.0 ); +} + +/*==========================================================================================*/ +void PaQa_EraseBuffer( float *buffer, int numFrames, int samplesPerFrame ) +{ + int i; + int numSamples = numFrames * samplesPerFrame; + for( i=0; i<numSamples; i++ ) + { + *buffer++ = 0.0; + } +} + +/*==========================================================================================*/ +void PaQa_SetupSineGenerator( PaQaSineGenerator *generator, double frequency, double amplitude, double frameRate ) +{ + generator->phase = 0.0; + generator->amplitude = amplitude; + generator->frequency = frequency; + generator->phaseIncrement = 2.0 * frequency * MATH_PI / frameRate; +} + +/*==========================================================================================*/ +void PaQa_MixSine( PaQaSineGenerator *generator, float *buffer, int numSamples, int stride ) +{ + int i; + for( i=0; i<numSamples; i++ ) + { + float value = sinf( (float) generator->phase ) * generator->amplitude; + *buffer += value; // Mix with existing value. + buffer += stride; + // Advance phase and wrap around. + generator->phase += generator->phaseIncrement; + if (generator->phase > MATH_TWO_PI) + { + generator->phase -= MATH_TWO_PI; + } + } +} + +/*==========================================================================================*/ +void PaQa_GenerateCrackDISABLED( float *buffer, int numSamples, int stride ) +{ + int i; + int offset = numSamples/2; + for( i=0; i<numSamples; i++ ) + { + float phase = (MATH_TWO_PI * 0.5 * (i - offset)) / numSamples; + float cosp = cosf( phase ); + float cos2 = cosp * cosp; + // invert second half of signal + float value = (i < offset) ? cos2 : (0-cos2); + *buffer = value; + buffer += stride; + } +} + + +/*==========================================================================================*/ +int PaQa_InitializeRecording( PaQaRecording *recording, int maxFrames, int frameRate ) +{ + int numBytes = maxFrames * sizeof(float); + recording->buffer = (float*)malloc(numBytes); + QA_ASSERT_TRUE( "Allocate recording buffer.", (recording->buffer != NULL) ); + recording->maxFrames = maxFrames; recording->sampleRate = frameRate; + recording->numFrames = 0; + return 0; +error: + return 1; +} + +/*==========================================================================================*/ +void PaQa_TerminateRecording( PaQaRecording *recording ) +{ + if (recording->buffer != NULL) + { + free( recording->buffer ); + recording->buffer = NULL; + } + recording->maxFrames = 0; +} + +/*==========================================================================================*/ +int PaQa_WriteRecording( PaQaRecording *recording, float *buffer, int numFrames, int stride ) +{ + int i; + int framesToWrite; + float *data = &recording->buffer[recording->numFrames]; + + framesToWrite = numFrames; + if ((framesToWrite + recording->numFrames) > recording->maxFrames) + { + framesToWrite = recording->maxFrames - recording->numFrames; + } + + for( i=0; i<framesToWrite; i++ ) + { + *data++ = *buffer; + buffer += stride; + } + recording->numFrames += framesToWrite; + return (recording->numFrames >= recording->maxFrames); +} + +/*==========================================================================================*/ +int PaQa_WriteSilence( PaQaRecording *recording, int numFrames ) +{ + int i; + int framesToRecord; + float *data = &recording->buffer[recording->numFrames]; + + framesToRecord = numFrames; + if ((framesToRecord + recording->numFrames) > recording->maxFrames) + { + framesToRecord = recording->maxFrames - recording->numFrames; + } + + for( i=0; i<framesToRecord; i++ ) + { + *data++ = 0.0f; + } + recording->numFrames += framesToRecord; + return (recording->numFrames >= recording->maxFrames); +} + +/*==========================================================================================*/ +int PaQa_RecordFreeze( PaQaRecording *recording, int numFrames ) +{ + int i; + int framesToRecord; + float *data = &recording->buffer[recording->numFrames]; + + framesToRecord = numFrames; + if ((framesToRecord + recording->numFrames) > recording->maxFrames) + { + framesToRecord = recording->maxFrames - recording->numFrames; + } + + for( i=0; i<framesToRecord; i++ ) + { + // Copy old value forward as if the signal had frozen. + data[i] = data[i-1]; + } + recording->numFrames += framesToRecord; + return (recording->numFrames >= recording->maxFrames); +} + +/*==========================================================================================*/ +/** + * Write recording to WAV file. + */ +int PaQa_SaveRecordingToWaveFile( PaQaRecording *recording, const char *filename ) +{ + WAV_Writer writer; + int result = 0; +#define NUM_SAMPLES (200) + short data[NUM_SAMPLES]; + const int samplesPerFrame = 1; + int numLeft = recording->numFrames; + float *buffer = &recording->buffer[0]; + + result = Audio_WAV_OpenWriter( &writer, filename, recording->sampleRate, samplesPerFrame ); + if( result < 0 ) goto error; + + while( numLeft > 0 ) + { + int i; + int numToSave = (numLeft > NUM_SAMPLES) ? NUM_SAMPLES : numLeft; + // Convert double samples to shorts. + for( i=0; i<numToSave; i++ ) + { + double fval = *buffer++; + // Convert float to int and clip to short range. + int ival = fval * 32768.0; + if( ival > 32767 ) ival = 32767; + else if( ival < -32768 ) ival = -32768; + data[i] = ival; + } + result = Audio_WAV_WriteShorts( &writer, data, numToSave ); + if( result < 0 ) goto error; + numLeft -= numToSave; + } + + result = Audio_WAV_CloseWriter( &writer ); + if( result < 0 ) goto error; + + return 0; + +error: + printf("ERROR: result = %d\n", result ); + return result; +#undef NUM_SAMPLES +} + +/*==========================================================================================*/ + +double PaQa_MeasureCrossingSlope( float *buffer, int numFrames ) +{ + int i; + double slopeTotal = 0.0; + int slopeCount = 0; + float previous; + double averageSlope = 0.0; + + previous = buffer[0]; + for( i=1; i<numFrames; i++ ) + { + float current = buffer[i]; + if( (current > 0.0) && (previous < 0.0) ) + { + double delta = current - previous; + slopeTotal += delta; + slopeCount += 1; + } + previous = current; + } + if( slopeCount > 0 ) + { + averageSlope = slopeTotal / slopeCount; + } + return averageSlope; +} + +/*==========================================================================================*/ +/* + * We can't just measure the peaks cuz they may be clipped. + * But the zero crossing should be intact. + * The measured slope of a sine wave at zero should be: + * + * slope = sin( 2PI * frequency / sampleRate ) + * + */ +double PaQa_MeasureSineAmplitudeBySlope( PaQaRecording *recording, + double frequency, double frameRate, + int startFrame, int numFrames ) +{ + float *buffer = &recording->buffer[startFrame]; + double measuredSlope = PaQa_MeasureCrossingSlope( buffer, numFrames ); + double unitySlope = sin( MATH_TWO_PI * frequency / frameRate ); + double estimatedAmplitude = measuredSlope / unitySlope; + return estimatedAmplitude; +} + +/*==========================================================================================*/ +double PaQa_CorrelateSine( PaQaRecording *recording, double frequency, double frameRate, + int startFrame, int numFrames, double *phasePtr ) +{ + double magnitude = 0.0; + int numLeft = numFrames; + double phase = 0.0; + double phaseIncrement = 2.0 * MATH_PI * frequency / frameRate; + double sinAccumulator = 0.0; + double cosAccumulator = 0.0; + float *data = &recording->buffer[startFrame]; + + QA_ASSERT_TRUE( "startFrame out of bounds", (startFrame < recording->numFrames) ); + QA_ASSERT_TRUE( "numFrames out of bounds", ((startFrame+numFrames) <= recording->numFrames) ); + + while( numLeft > 0 ) + { + double sample = (double) *data++; + sinAccumulator += sample * sin( phase ); + cosAccumulator += sample * cos( phase ); + phase += phaseIncrement; + if (phase > MATH_TWO_PI) + { + phase -= MATH_TWO_PI; + } + numLeft -= 1; + } + sinAccumulator = sinAccumulator / numFrames; + cosAccumulator = cosAccumulator / numFrames; + // TODO Why do I have to multiply by 2.0? Need it to make result come out right. + magnitude = 2.0 * sqrt( (sinAccumulator * sinAccumulator) + (cosAccumulator * cosAccumulator )); + if( phasePtr != NULL ) + { + double phase = atan2( cosAccumulator, sinAccumulator ); + *phasePtr = phase; + } + return magnitude; +error: + return -1.0; +} + +/*==========================================================================================*/ +void PaQa_FilterRecording( PaQaRecording *input, PaQaRecording *output, BiquadFilter *filter ) +{ + int numToFilter = (input->numFrames > output->maxFrames) ? output->maxFrames : input->numFrames; + BiquadFilter_Filter( filter, &input->buffer[0], &output->buffer[0], numToFilter ); + output->numFrames = numToFilter; +} + +/*==========================================================================================*/ +/** Scan until we get a correlation of a single that goes over the tolerance level, + * peaks then drops to half the peak. + * Look for inverse correlation as well. + */ +double PaQa_FindFirstMatch( PaQaRecording *recording, float *buffer, int numFrames, double threshold ) +{ + int ic,is; + // How many buffers will fit in the recording? + int maxCorrelations = recording->numFrames - numFrames; + double maxSum = 0.0; + int peakIndex = -1; + double inverseMaxSum = 0.0; + int inversePeakIndex = -1; + double location = -1.0; + + QA_ASSERT_TRUE( "numFrames out of bounds", (numFrames < recording->numFrames) ); + + for( ic=0; ic<maxCorrelations; ic++ ) + { + int pastPeak; + int inversePastPeak; + + double sum = 0.0; + // Correlate buffer against the recording. + float *recorded = &recording->buffer[ ic ]; + for( is=0; is<numFrames; is++ ) + { + float s1 = buffer[is]; + float s2 = *recorded++; + sum += s1 * s2; + } + if( (sum > maxSum) ) + { + maxSum = sum; + peakIndex = ic; + } + if( ((-sum) > inverseMaxSum) ) + { + inverseMaxSum = -sum; + inversePeakIndex = ic; + } + pastPeak = (maxSum > threshold) && (sum < 0.5*maxSum); + inversePastPeak = (inverseMaxSum > threshold) && ((-sum) < 0.5*inverseMaxSum); + //printf("PaQa_FindFirstMatch: ic = %4d, sum = %8f, maxSum = %8f, inverseMaxSum = %8f\n", ic, sum, maxSum, inverseMaxSum ); + if( pastPeak && inversePastPeak ) + { + if( maxSum > inverseMaxSum ) + { + location = peakIndex; + } + else + { + location = inversePeakIndex; + } + break; + } + + } + //printf("PaQa_FindFirstMatch: location = %4d\n", (int)location ); + return location; +error: + return -1.0; +} + +/*==========================================================================================*/ +// Measure the area under the curve by summing absolute value of each value. +double PaQa_MeasureArea( float *buffer, int numFrames, int stride ) +{ + int is; + double area = 0.0; + for( is=0; is<numFrames; is++ ) + { + area += fabs( *buffer ); + buffer += stride; + } + return area; +} + +/*==========================================================================================*/ +// Measure the area under the curve by summing absolute value of each value. +double PaQa_MeasureRootMeanSquare( float *buffer, int numFrames ) +{ + int is; + double area = 0.0; + double root; + for( is=0; is<numFrames; is++ ) + { + float value = *buffer++; + area += value * value; + } + root = sqrt( area ); + return root / numFrames; +} + + +/*==========================================================================================*/ +// Compare the amplitudes of these two signals. +// Return ratio of recorded signal over buffer signal. + +double PaQa_CompareAmplitudes( PaQaRecording *recording, int startAt, float *buffer, int numFrames ) +{ + QA_ASSERT_TRUE( "startAt+numFrames out of bounds", ((startAt+numFrames) < recording->numFrames) ); + + { + double recordedArea = PaQa_MeasureArea( &recording->buffer[startAt], numFrames, 1 ); + double bufferArea = PaQa_MeasureArea( buffer, numFrames, 1 ); + if( bufferArea == 0.0 ) return 100000000.0; + return recordedArea / bufferArea; + } +error: + return -1.0; +} + + +/*==========================================================================================*/ +double PaQa_ComputePhaseDifference( double phase1, double phase2 ) +{ + double delta = phase1 - phase2; + while( delta > MATH_PI ) + { + delta -= MATH_TWO_PI; + } + while( delta < -MATH_PI ) + { + delta += MATH_TWO_PI; + } + return delta; +} + +/*==========================================================================================*/ +int PaQa_MeasureLatency( PaQaRecording *recording, PaQaTestTone *testTone, PaQaAnalysisResult *analysisResult ) +{ + double threshold; + PaQaSineGenerator generator; +#define MAX_BUFFER_SIZE 2048 + float buffer[MAX_BUFFER_SIZE]; + double period = testTone->sampleRate / testTone->frequency; + int cycleSize = (int) (period + 0.5); + //printf("PaQa_AnalyseRecording: frequency = %8f, frameRate = %8f, period = %8f, cycleSize = %8d\n", + // testTone->frequency, testTone->sampleRate, period, cycleSize ); + analysisResult->latency = -1; + analysisResult->valid = (0); + + // Set up generator to find matching first cycle. + QA_ASSERT_TRUE( "cycleSize out of bounds", (cycleSize < MAX_BUFFER_SIZE) ); + PaQa_SetupSineGenerator( &generator, testTone->frequency, testTone->amplitude, testTone->sampleRate ); + PaQa_EraseBuffer( buffer, cycleSize, testTone->samplesPerFrame ); + PaQa_MixSine( &generator, buffer, cycleSize, testTone->samplesPerFrame ); + + threshold = cycleSize * 0.02; + analysisResult->latency = PaQa_FindFirstMatch( recording, buffer, cycleSize, threshold ); + QA_ASSERT_TRUE( "Could not find the start of the signal.", (analysisResult->latency >= 0) ); + analysisResult->amplitudeRatio = PaQa_CompareAmplitudes( recording, analysisResult->latency, buffer, cycleSize ); + return 0; +error: + return -1; +} + +/*==========================================================================================*/ +// Apply cosine squared window. +void PaQa_FadeInRecording( PaQaRecording *recording, int startFrame, int count ) +{ + int is; + double phase = 0.5 * MATH_PI; + // Advance a quarter wave + double phaseIncrement = 0.25 * 2.0 * MATH_PI / count; + + assert( startFrame >= 0 ); + assert( count > 0 ); + + /* Zero out initial part of the recording. */ + for( is=0; is<startFrame; is++ ) + { + recording->buffer[ is ] = 0.0f; + } + /* Fade in where signal begins. */ + for( is=0; is<count; is++ ) + { + double c = cos( phase ); + double w = c * c; + float x = recording->buffer[ is + startFrame ]; + float y = x * w; + //printf("FADE %d : w=%f, x=%f, y=%f\n", is, w, x, y ); + recording->buffer[ is + startFrame ] = y; + + phase += phaseIncrement; + } +} + + +/*==========================================================================================*/ +/** Apply notch filter and high pass filter then detect remaining energy. + */ +int PaQa_DetectPop( PaQaRecording *recording, PaQaTestTone *testTone, PaQaAnalysisResult *analysisResult ) +{ + int result = 0; + int i; + double maxAmplitude; + int maxPosition; + + PaQaRecording notchOutput = { 0 }; + BiquadFilter notchFilter; + + PaQaRecording hipassOutput = { 0 }; + BiquadFilter hipassFilter; + + int frameRate = (int) recording->sampleRate; + + analysisResult->popPosition = -1; + analysisResult->popAmplitude = 0.0; + + result = PaQa_InitializeRecording( ¬chOutput, recording->numFrames, frameRate ); + QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); + + result = PaQa_InitializeRecording( &hipassOutput, recording->numFrames, frameRate ); + QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); + + // Use notch filter to remove test tone. + BiquadFilter_SetupNotch( ¬chFilter, testTone->frequency / frameRate, 0.5 ); + PaQa_FilterRecording( recording, ¬chOutput, ¬chFilter ); + //result = PaQa_SaveRecordingToWaveFile( ¬chOutput, "notch_output.wav" ); + //QA_ASSERT_EQUALS( "PaQa_SaveRecordingToWaveFile failed", 0, result ); + + // Apply fade-in window. + PaQa_FadeInRecording( ¬chOutput, (int) analysisResult->latency, 500 ); + + // Use high pass to accentuate the edges of a pop. At higher frequency! + BiquadFilter_SetupHighPass( &hipassFilter, 2.0 * testTone->frequency / frameRate, 0.5 ); + PaQa_FilterRecording( ¬chOutput, &hipassOutput, &hipassFilter ); + //result = PaQa_SaveRecordingToWaveFile( &hipassOutput, "hipass_output.wav" ); + //QA_ASSERT_EQUALS( "PaQa_SaveRecordingToWaveFile failed", 0, result ); + + // Scan remaining signal looking for peak. + maxAmplitude = 0.0; + maxPosition = -1; + for( i=(int) analysisResult->latency; i<hipassOutput.numFrames; i++ ) + { + float x = hipassOutput.buffer[i]; + float mag = fabs( x ); + if( mag > maxAmplitude ) + { + maxAmplitude = mag; + maxPosition = i; + } + } + + if( maxAmplitude > PAQA_POP_THRESHOLD ) + { + analysisResult->popPosition = maxPosition; + analysisResult->popAmplitude = maxAmplitude; + } + + PaQa_TerminateRecording( ¬chOutput ); + PaQa_TerminateRecording( &hipassOutput ); + return 0; + +error: + PaQa_TerminateRecording( ¬chOutput ); + PaQa_TerminateRecording( &hipassOutput ); + return -1; +} + +/*==========================================================================================*/ +int PaQa_DetectPhaseError( PaQaRecording *recording, PaQaTestTone *testTone, PaQaAnalysisResult *analysisResult ) +{ + int i; + double period = testTone->sampleRate / testTone->frequency; + int cycleSize = (int) (period + 0.5); + + double maxAddedFrames = 0.0; + double maxDroppedFrames = 0.0; + + double previousPhase = 0.0; + double previousFrameError = 0; + int loopCount = 0; + int skip = cycleSize; + int windowSize = cycleSize; + + // Scan recording starting with first cycle, looking for phase errors. + analysisResult->numDroppedFrames = 0.0; + analysisResult->numAddedFrames = 0.0; + analysisResult->droppedFramesPosition = -1.0; + analysisResult->addedFramesPosition = -1.0; + + for( i=analysisResult->latency; i<(recording->numFrames - windowSize); i += skip ) + { + double expectedPhase = previousPhase + (skip * MATH_TWO_PI / period); + double expectedPhaseIncrement = PaQa_ComputePhaseDifference( expectedPhase, previousPhase ); + + double phase = 666.0; + double mag = PaQa_CorrelateSine( recording, testTone->frequency, testTone->sampleRate, i, windowSize, &phase ); + if( (loopCount > 1) && (mag > 0.0) ) + { + double phaseDelta = PaQa_ComputePhaseDifference( phase, previousPhase ); + double phaseError = PaQa_ComputePhaseDifference( phaseDelta, expectedPhaseIncrement ); + // Convert phaseError to equivalent number of frames. + double frameError = period * phaseError / MATH_TWO_PI; + double consecutiveFrameError = frameError + previousFrameError; +// if( fabs(frameError) > 0.01 ) +// { +// printf("FFFFFFFFFFFFF frameError = %f, at %d\n", frameError, i ); +// } + if( consecutiveFrameError > 0.8 ) + { + double droppedFrames = consecutiveFrameError; + if (droppedFrames > (maxDroppedFrames * 1.001)) + { + analysisResult->numDroppedFrames = droppedFrames; + analysisResult->droppedFramesPosition = i + (windowSize/2); + maxDroppedFrames = droppedFrames; + } + } + else if( consecutiveFrameError < -0.8 ) + { + double addedFrames = 0 - consecutiveFrameError; + if (addedFrames > (maxAddedFrames * 1.001)) + { + analysisResult->numAddedFrames = addedFrames; + analysisResult->addedFramesPosition = i + (windowSize/2); + maxAddedFrames = addedFrames; + } + } + previousFrameError = frameError; + + + //if( i<8000 ) + //{ + // printf("%d: phase = %8f, expected = %8f, delta = %8f, frameError = %8f\n", i, phase, expectedPhaseIncrement, phaseDelta, frameError ); + //} + } + previousPhase = phase; + loopCount += 1; + } + return 0; +} + +/*==========================================================================================*/ +int PaQa_AnalyseRecording( PaQaRecording *recording, PaQaTestTone *testTone, PaQaAnalysisResult *analysisResult ) +{ + int result = 0; + + memset( analysisResult, 0, sizeof(PaQaAnalysisResult) ); + result = PaQa_MeasureLatency( recording, testTone, analysisResult ); + QA_ASSERT_EQUALS( "latency measurement", 0, result ); + + if( (analysisResult->latency >= 0) && (analysisResult->amplitudeRatio > 0.1) ) + { + analysisResult->valid = (1); + + result = PaQa_DetectPop( recording, testTone, analysisResult ); + QA_ASSERT_EQUALS( "detect pop", 0, result ); + + result = PaQa_DetectPhaseError( recording, testTone, analysisResult ); + QA_ASSERT_EQUALS( "detect phase error", 0, result ); + } + return 0; +error: + return -1; +} diff --git a/portaudio/qa/loopback/src/audio_analyzer.h b/portaudio/qa/loopback/src/audio_analyzer.h new file mode 100644 index 0000000..8d9f1ee --- /dev/null +++ b/portaudio/qa/loopback/src/audio_analyzer.h @@ -0,0 +1,187 @@ + +/* + * 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. + */ + +#ifndef _AUDIO_ANALYZER_H +#define _AUDIO_ANALYZER_H + +#include "biquad_filter.h" + +#define MATH_PI (3.141592653589793238462643) +#define MATH_TWO_PI (2.0 * MATH_PI) + +typedef struct PaQaSineGenerator_s +{ + double phase; + double phaseIncrement; + double frequency; + double amplitude; +} PaQaSineGenerator; + +/** Container for a monophonic audio sample in memory. */ +typedef struct PaQaRecording_s +{ + /** Maximum number of frames that can fit in the allocated buffer. */ + int maxFrames; + float *buffer; + /** Actual number of valid frames in the buffer. */ + int numFrames; + int sampleRate; +} PaQaRecording; + +typedef struct PaQaTestTone_s +{ + int samplesPerFrame; + int startDelay; + double sampleRate; + double frequency; + double amplitude; +} PaQaTestTone; + +typedef struct PaQaAnalysisResult_s +{ + int valid; + /** Latency in samples from output to input. */ + double latency; + double amplitudeRatio; + double popAmplitude; + double popPosition; + double numDroppedFrames; + double droppedFramesPosition; + double numAddedFrames; + double addedFramesPosition; +} PaQaAnalysisResult; + + +/*================================================================*/ +/*================= General DSP Tools ============================*/ +/*================================================================*/ +/** + * Calculate Nth frequency of a series for use in testing multiple channels. + * Series should avoid harmonic overlap between channels. + */ +double PaQa_GetNthFrequency( double baseFrequency, int index ); + +void PaQa_EraseBuffer( float *buffer, int numFrames, int samplesPerFrame ); + +void PaQa_MixSine( PaQaSineGenerator *generator, float *buffer, int numSamples, int stride ); + +void PaQa_WriteSine( float *buffer, int numSamples, int stride, + double frequency, double amplitude ); + +/** + * Generate a signal with a sharp edge in the middle that can be recognized despite some phase shift. + */ +void PaQa_GenerateCrack( float *buffer, int numSamples, int stride ); + +double PaQa_ComputePhaseDifference( double phase1, double phase2 ); + +/** + * Measure the area under the curve by summing absolute value of each value. + */ +double PaQa_MeasureArea( float *buffer, int numFrames, int stride ); + +/** + * Measure slope of the positive zero crossings. + */ +double PaQa_MeasureCrossingSlope( float *buffer, int numFrames ); + + +/** + * Prepare an oscillator that can generate a sine tone for testing. + */ +void PaQa_SetupSineGenerator( PaQaSineGenerator *generator, double frequency, double amplitude, double frameRate ); + +/*================================================================*/ +/*================= Recordings ===================================*/ +/*================================================================*/ +/** + * Allocate memory for containing a mono audio signal. Set up recording for writing. + */ + int PaQa_InitializeRecording( PaQaRecording *recording, int maxSamples, int sampleRate ); + +/** +* Free memory allocated by PaQa_InitializeRecording. + */ + void PaQa_TerminateRecording( PaQaRecording *recording ); + +/** + * Apply a biquad filter to the audio from the input recording and write it to the output recording. + */ +void PaQa_FilterRecording( PaQaRecording *input, PaQaRecording *output, BiquadFilter *filter ); + + +int PaQa_SaveRecordingToWaveFile( PaQaRecording *recording, const char *filename ); + +/** + * @param stride is the spacing of samples to skip in the input buffer. To use every samples pass 1. To use every other sample pass 2. + */ +int PaQa_WriteRecording( PaQaRecording *recording, float *buffer, int numSamples, int stride ); + +/** Write zeros into a recording. */ +int PaQa_WriteSilence( PaQaRecording *recording, int numSamples ); + +int PaQa_RecordFreeze( PaQaRecording *recording, int numSamples ); + +double PaQa_CorrelateSine( PaQaRecording *recording, double frequency, double frameRate, + int startFrame, int numSamples, double *phasePtr ); + +double PaQa_FindFirstMatch( PaQaRecording *recording, float *buffer, int numSamples, double tolerance ); + +/** + * Estimate the original amplitude of a clipped sine wave by measuring + * its average slope at the zero crossings. + */ +double PaQa_MeasureSineAmplitudeBySlope( PaQaRecording *recording, + double frequency, double frameRate, + int startFrame, int numFrames ); + +double PaQa_MeasureRootMeanSquare( float *buffer, int numFrames ); + +/** + * Compare the amplitudes of these two signals. + * Return ratio of recorded signal over buffer signal. + */ +double PaQa_CompareAmplitudes( PaQaRecording *recording, int startAt, float *buffer, int numSamples ); + +/** + * Analyse a recording of a sine wave. + * Measure latency and look for dropped frames, etc. + */ +int PaQa_AnalyseRecording( PaQaRecording *recording, PaQaTestTone *testTone, PaQaAnalysisResult *analysisResult ); + +#endif /* _AUDIO_ANALYZER_H */ diff --git a/portaudio/qa/loopback/src/biquad_filter.c b/portaudio/qa/loopback/src/biquad_filter.c new file mode 100755 index 0000000..1715fa3 --- /dev/null +++ b/portaudio/qa/loopback/src/biquad_filter.c @@ -0,0 +1,123 @@ +#include <math.h> +#include <string.h> + +#include "biquad_filter.h" + +/** + * Unit_BiquadFilter implements a second order IIR filter. + * + * Here is the equation that we use for this filter: + * y(n) = a0*x(n) + a1*x(n-1) + a2*x(n-2) - b1*y(n-1) - b2*y(n-2) + * + * @author (C) 2002 Phil Burk, SoftSynth.com, All Rights Reserved + */ + +#define FILTER_PI (3.141592653589793238462643) +/*********************************************************** +** Calculate coefficients common to many parametric biquad filters. +*/ +static void BiquadFilter_CalculateCommon( BiquadFilter *filter, double ratio, double Q ) +{ + double omega; + + memset( filter, 0, sizeof(BiquadFilter) ); + +/* Don't let frequency get too close to Nyquist or filter will blow up. */ + if( ratio >= 0.499 ) ratio = 0.499; + omega = 2.0 * (double)FILTER_PI * ratio; + + filter->cos_omega = (double) cos( omega ); + filter->sin_omega = (double) sin( omega ); + filter->alpha = filter->sin_omega / (2.0 * Q); +} + +/********************************************************************************* + ** Calculate coefficients for Highpass filter. + */ +void BiquadFilter_SetupHighPass( BiquadFilter *filter, double ratio, double Q ) +{ + double scalar, opc; + + if( ratio < BIQUAD_MIN_RATIO ) ratio = BIQUAD_MIN_RATIO; + if( Q < BIQUAD_MIN_Q ) Q = BIQUAD_MIN_Q; + + BiquadFilter_CalculateCommon( filter, ratio, Q ); + + scalar = 1.0 / (1.0 + filter->alpha); + opc = (1.0 + filter->cos_omega); + + filter->a0 = opc * 0.5 * scalar; + filter->a1 = - opc * scalar; + filter->a2 = filter->a0; + filter->b1 = -2.0 * filter->cos_omega * scalar; + filter->b2 = (1.0 - filter->alpha) * scalar; +} + + +/********************************************************************************* + ** Calculate coefficients for Notch filter. + */ +void BiquadFilter_SetupNotch( BiquadFilter *filter, double ratio, double Q ) +{ + double scalar, opc; + + if( ratio < BIQUAD_MIN_RATIO ) ratio = BIQUAD_MIN_RATIO; + if( Q < BIQUAD_MIN_Q ) Q = BIQUAD_MIN_Q; + + BiquadFilter_CalculateCommon( filter, ratio, Q ); + + scalar = 1.0 / (1.0 + filter->alpha); + opc = (1.0 + filter->cos_omega); + + filter->a0 = scalar; + filter->a1 = -2.0 * filter->cos_omega * scalar; + filter->a2 = filter->a0; + filter->b1 = filter->a1; + filter->b2 = (1.0 - filter->alpha) * scalar; +} + +/***************************************************************** +** Perform core IIR filter calculation without permutation. +*/ +void BiquadFilter_Filter( BiquadFilter *filter, float *inputs, float *outputs, int numSamples ) +{ + int i; + double xn, yn; + // Pull values from structure to speed up the calculation. + double a0 = filter->a0; + double a1 = filter->a1; + double a2 = filter->a2; + double b1 = filter->b1; + double b2 = filter->b2; + double xn1 = filter->xn1; + double xn2 = filter->xn2; + double yn1 = filter->yn1; + double yn2 = filter->yn2; + + for( i=0; i<numSamples; i++) + { + // Generate outputs by filtering inputs. + xn = inputs[i]; + yn = (a0 * xn) + (a1 * xn1) + (a2 * xn2) - (b1 * yn1) - (b2 * yn2); + outputs[i] = yn; + + // Delay input and output values. + xn2 = xn1; + xn1 = xn; + yn2 = yn1; + yn1 = yn; + + if( (i & 7) == 0 ) + { + // Apply a small bipolar impulse to filter to prevent arithmetic underflow. + // Underflows can cause the FPU to interrupt the CPU. + yn1 += (double) 1.0E-26; + yn2 -= (double) 1.0E-26; + } + } + + filter->xn1 = xn1; + filter->xn2 = xn2; + filter->yn1 = yn1; + filter->yn2 = yn2; +} diff --git a/portaudio/qa/loopback/src/biquad_filter.h b/portaudio/qa/loopback/src/biquad_filter.h new file mode 100755 index 0000000..0895aba --- /dev/null +++ b/portaudio/qa/loopback/src/biquad_filter.h @@ -0,0 +1,38 @@ +#ifndef _BIQUADFILTER_H +#define _BIQUADFILTER_H + + +/** + * Unit_BiquadFilter implements a second order IIR filter. + * + * @author (C) 2002 Phil Burk, SoftSynth.com, All Rights Reserved + */ + +#define BIQUAD_MIN_RATIO (0.000001) +#define BIQUAD_MIN_Q (0.00001) + +typedef struct BiquadFilter_s +{ + double xn1; // storage for delayed signals + double xn2; + double yn1; + double yn2; + + double a0; // coefficients + double a1; + double a2; + + double b1; + double b2; + + double cos_omega; + double sin_omega; + double alpha; +} BiquadFilter; + +void BiquadFilter_SetupHighPass( BiquadFilter *filter, double ratio, double Q ); +void BiquadFilter_SetupNotch( BiquadFilter *filter, double ratio, double Q ); + +void BiquadFilter_Filter( BiquadFilter *filter, float *inputs, float *outputs, int numSamples ); + +#endif diff --git a/portaudio/qa/loopback/src/paqa.c b/portaudio/qa/loopback/src/paqa.c new file mode 100644 index 0000000..5eb6283 --- /dev/null +++ b/portaudio/qa/loopback/src/paqa.c @@ -0,0 +1,1601 @@ + +/* + * 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; + } +} diff --git a/portaudio/qa/loopback/src/paqa_tools.c b/portaudio/qa/loopback/src/paqa_tools.c new file mode 100644 index 0000000..2e44c63 --- /dev/null +++ b/portaudio/qa/loopback/src/paqa_tools.c @@ -0,0 +1,171 @@ + +/* + * 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 "paqa_tools.h" + + +/*******************************************************************/ +void PaQa_ListAudioDevices(void) +{ + int i, numDevices; + const PaDeviceInfo *deviceInfo; + numDevices = Pa_GetDeviceCount(); + for( i=0; i<numDevices; i++ ) + { + deviceInfo = Pa_GetDeviceInfo( i ); + printf( "#%d: ", i ); + printf( "%2d in", deviceInfo->maxInputChannels ); + printf( ", %2d out", deviceInfo->maxOutputChannels ); + printf( ", %s", deviceInfo->name ); + printf( ", on %s\n", Pa_GetHostApiInfo( deviceInfo->hostApi )->name ); + } +} + +/*******************************************************************/ +void PaQa_ConvertToFloat( const void *input, int numSamples, PaSampleFormat inFormat, float *output ) +{ + int i; + switch( inFormat ) + { + case paUInt8: + { + unsigned char *data = (unsigned char *)input; + for( i=0; i<numSamples; i++ ) + { + int value = *data++; + value -= 128; + *output++ = value / 128.0f; + } + } + break; + + case paInt8: + { + char *data = (char *)input; + for( i=0; i<numSamples; i++ ) + { + int value = *data++; + *output++ = value / 128.0f; + } + } + break; + + case paInt16: + { + short *data = (short *)input; + for( i=0; i<numSamples; i++ ) + { + *output++ = *data++ / 32768.0f; + } + } + break; + + case paInt32: + { + int *data = (int *)input; + for( i=0; i<numSamples; i++ ) + { + int value = (*data++) >> 8; + float fval = (float) (value / ((double) 0x00800000)); + *output++ = fval; + } + } + break; + } + +} + +/*******************************************************************/ +void PaQa_ConvertFromFloat( const float *input, int numSamples, PaSampleFormat outFormat, void *output ) +{ + int i; + switch( outFormat ) + { + case paUInt8: + { + unsigned char *data = (unsigned char *)output; + for( i=0; i<numSamples; i++ ) + { + float value = *input++; + int byte = ((int) (value * 127)) + 128; + *data++ = (unsigned char) byte; + } + } + break; + + case paInt8: + { + char *data = (char *)output; + for( i=0; i<numSamples; i++ ) + { + float value = *input++; + int byte = (int) (value * 127); + *data++ = (char) byte; + } + } + break; + + case paInt16: + { + short *data = (short *)output; + for( i=0; i<numSamples; i++ ) + { + float value = *input++; + // Use asymmetric conversion to avoid clipping. + short sval = value * 32767.0; + *data++ = sval; + } + } + break; + + case paInt32: + { + int *data = (int *)output; + for( i=0; i<numSamples; i++ ) + { + float value = *input++; + // Use asymmetric conversion to avoid clipping. + int ival = value * ((double) 0x007FFFF0); + ival = ival << 8; + *data++ = ival; + } + } + break; + } + +} diff --git a/portaudio/qa/loopback/src/paqa_tools.h b/portaudio/qa/loopback/src/paqa_tools.h new file mode 100644 index 0000000..77f6a25 --- /dev/null +++ b/portaudio/qa/loopback/src/paqa_tools.h @@ -0,0 +1,52 @@ + +/* + * 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. + */ + +#ifndef _PAQA_TOOLS_H +#define _PAQA_TOOLS_H + + +#include <stdio.h> +#include "portaudio.h" + +void PaQa_ListAudioDevices(void); + +void PaQa_ConvertToFloat( const void *input, int numSamples, PaSampleFormat inFormat, float *output ); + +void PaQa_ConvertFromFloat( const float *input, int numSamples, PaSampleFormat outFormat, void *output ); + +#endif /* _PAQA_TOOLS_H */ diff --git a/portaudio/qa/loopback/src/qa_tools.h b/portaudio/qa/loopback/src/qa_tools.h new file mode 100755 index 0000000..9b2debd --- /dev/null +++ b/portaudio/qa/loopback/src/qa_tools.h @@ -0,0 +1,83 @@ + +/* + * 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. + */ + +#ifndef _QA_TOOLS_H +#define _QA_TOOLS_H + +extern int g_testsPassed; +extern int g_testsFailed; + +#define QA_ASSERT_TRUE( message, flag ) \ + if( !(flag) ) \ + { \ + printf( "%s:%d - ERROR - %s\n", __FILE__, __LINE__, message ); \ + g_testsFailed++; \ + goto error; \ + } \ + else g_testsPassed++; + + +#define QA_ASSERT_EQUALS( message, expected, actual ) \ + if( ((expected) != (actual)) ) \ + { \ + printf( "%s:%d - ERROR - %s, expected %d, got %d\n", __FILE__, __LINE__, message, expected, actual ); \ + g_testsFailed++; \ + goto error; \ + } \ + else g_testsPassed++; + +#define QA_ASSERT_CLOSE( message, expected, actual, tolerance ) \ + if (fabs((expected)-(actual))>(tolerance)) \ + { \ + printf( "%s:%d - ERROR - %s, expected %f, got %f, tol=%f\n", __FILE__, __LINE__, message, ((double)(expected)), ((double)(actual)), ((double)(tolerance)) ); \ + g_testsFailed++; \ + goto error; \ + } \ + else g_testsPassed++; + +#define QA_ASSERT_CLOSE_INT( message, expected, actual, tolerance ) \ + if (abs((expected)-(actual))>(tolerance)) \ + { \ + printf( "%s:%d - ERROR - %s, expected %d, got %d, tol=%d\n", __FILE__, __LINE__, message, ((int)(expected)), ((int)(actual)), ((int)(tolerance)) ); \ + g_testsFailed++; \ + goto error; \ + } \ + else g_testsPassed++; + + +#endif diff --git a/portaudio/qa/loopback/src/test_audio_analyzer.c b/portaudio/qa/loopback/src/test_audio_analyzer.c new file mode 100644 index 0000000..82fa859 --- /dev/null +++ b/portaudio/qa/loopback/src/test_audio_analyzer.c @@ -0,0 +1,718 @@ + +/* + * 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 <math.h> +#include "qa_tools.h" +#include "audio_analyzer.h" +#include "test_audio_analyzer.h" +#include "write_wav.h" +#include "biquad_filter.h" + +#define FRAMES_PER_BLOCK (64) +#define PRINT_REPORTS 0 + +#define TEST_SAVED_WAVE (0) + +/*==========================================================================================*/ +/** + * Detect a single tone. + */ +static int TestSingleMonoTone( void ) +{ + int result = 0; + PaQaSineGenerator generator; + PaQaRecording recording; + float buffer[FRAMES_PER_BLOCK]; + double sampleRate = 44100.0; + int maxFrames = ((int)sampleRate) * 1; + int samplesPerFrame = 1; + int stride = 1; + int done = 0; + + double freq = 234.5; + double amp = 0.5; + + double mag1, mag2; + + // Setup a sine oscillator. + PaQa_SetupSineGenerator( &generator, freq, amp, sampleRate ); + + result = PaQa_InitializeRecording( &recording, maxFrames, (int) sampleRate ); + QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); + + done = 0; + while (!done) + { + PaQa_EraseBuffer( buffer, FRAMES_PER_BLOCK, samplesPerFrame ); + PaQa_MixSine( &generator, buffer, FRAMES_PER_BLOCK, stride ); + done = PaQa_WriteRecording( &recording, buffer, FRAMES_PER_BLOCK, samplesPerFrame ); + } + + mag1 = PaQa_CorrelateSine( &recording, freq, sampleRate, 0, recording.numFrames, NULL ); + QA_ASSERT_CLOSE( "exact frequency match", amp, mag1, 0.01 ); + + mag2 = PaQa_CorrelateSine( &recording, freq * 1.23, sampleRate, 0, recording.numFrames, NULL ); + QA_ASSERT_CLOSE( "wrong frequency", 0.0, mag2, 0.01 ); + + PaQa_TerminateRecording( &recording ); + return 0; + +error: + PaQa_TerminateRecording( &recording); + return 1; + +} + +/*==========================================================================================*/ +/** + * Mix multiple tones and then detect them. + */ + +static int TestMixedMonoTones( void ) +{ + int i; + int result = 0; +#define NUM_TONES (5) + PaQaSineGenerator generators[NUM_TONES]; + PaQaRecording recording; + float buffer[FRAMES_PER_BLOCK]; + double sampleRate = 44100.0; + int maxFrames = ((int)sampleRate) * 1; + int samplesPerFrame = 1; + + double baseFreq = 234.5; + double amp = 0.1; + + double mag2; + + int stride = samplesPerFrame; + int done = 0; + + // Setup a sine oscillator. + for( i=0; i<NUM_TONES; i++ ) + { + PaQa_SetupSineGenerator( &generators[i], PaQa_GetNthFrequency( baseFreq, i ), amp, sampleRate ); + } + + result = PaQa_InitializeRecording( &recording, maxFrames, (int) sampleRate ); + QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); + + done = 0; + while (!done) + { + PaQa_EraseBuffer( buffer, FRAMES_PER_BLOCK, samplesPerFrame ); + for( i=0; i<NUM_TONES; i++ ) + { + PaQa_MixSine( &generators[i], buffer, FRAMES_PER_BLOCK, stride ); + } + done = PaQa_WriteRecording( &recording, buffer, FRAMES_PER_BLOCK, samplesPerFrame ); + } + + for( i=0; i<NUM_TONES; i++ ) + { + double mag = PaQa_CorrelateSine( &recording, PaQa_GetNthFrequency( baseFreq, i), sampleRate, 0, recording.numFrames, NULL ); + QA_ASSERT_CLOSE( "exact frequency match", amp, mag, 0.01 ); + } + + mag2 = PaQa_CorrelateSine( &recording, baseFreq * 0.87, sampleRate, 0, recording.numFrames, NULL ); + QA_ASSERT_CLOSE( "wrong frequency", 0.0, mag2, 0.01 ); + + PaQa_TerminateRecording( &recording ); + return 0; + +error: + PaQa_TerminateRecording( &recording); + return 1; + +} + + +/*==========================================================================================*/ +/** + * Generate a recording with added or dropped frames. + */ + +static void MakeRecordingWithAddedFrames( PaQaRecording *recording, PaQaTestTone *testTone, int glitchPosition, int framesToAdd ) +{ + PaQaSineGenerator generator; +#define BUFFER_SIZE 512 + float buffer[BUFFER_SIZE]; + + int frameCounter = testTone->startDelay; + + int stride = 1; + // Record some initial silence. + int done = PaQa_WriteSilence( recording, testTone->startDelay ); + + // Setup a sine oscillator. + PaQa_SetupSineGenerator( &generator, testTone->frequency, testTone->amplitude, testTone->sampleRate ); + + while (!done) + { + int framesThisLoop = BUFFER_SIZE; + + if( frameCounter == glitchPosition ) + { + if( framesToAdd > 0 ) + { + // Record some frozen data without advancing the sine generator. + done = PaQa_RecordFreeze( recording, framesToAdd ); + frameCounter += framesToAdd; + } + else if( framesToAdd < 0 ) + { + // Advance sine generator a few frames. + PaQa_MixSine( &generator, buffer, 0 - framesToAdd, stride ); + } + + } + else if( (frameCounter < glitchPosition) && ((frameCounter + framesThisLoop) > glitchPosition) ) + { + // Go right up to the glitchPosition. + framesThisLoop = glitchPosition - frameCounter; + } + + if( framesThisLoop > 0 ) + { + PaQa_EraseBuffer( buffer, framesThisLoop, testTone->samplesPerFrame ); + PaQa_MixSine( &generator, buffer, framesThisLoop, stride ); + done = PaQa_WriteRecording( recording, buffer, framesThisLoop, testTone->samplesPerFrame ); + } + frameCounter += framesThisLoop; + } +} + + +/*==========================================================================================*/ +/** + * Generate a clean recording. + */ + +static void MakeCleanRecording( PaQaRecording *recording, PaQaTestTone *testTone ) +{ + PaQaSineGenerator generator; +#define BUFFER_SIZE 512 + float buffer[BUFFER_SIZE]; + + int stride = 1; + // Record some initial silence. + int done = PaQa_WriteSilence( recording, testTone->startDelay ); + + // Setup a sine oscillator. + PaQa_SetupSineGenerator( &generator, testTone->frequency, testTone->amplitude, testTone->sampleRate ); + + // Generate recording with good phase. + while (!done) + { + PaQa_EraseBuffer( buffer, BUFFER_SIZE, testTone->samplesPerFrame ); + PaQa_MixSine( &generator, buffer, BUFFER_SIZE, stride ); + done = PaQa_WriteRecording( recording, buffer, BUFFER_SIZE, testTone->samplesPerFrame ); + } +} + +/*==========================================================================================*/ +/** + * Generate a recording with pop. + */ + +static void MakeRecordingWithPop( PaQaRecording *recording, PaQaTestTone *testTone, int popPosition, int popWidth, double popAmplitude ) +{ + int i; + + MakeCleanRecording( recording, testTone ); + + // Apply glitch to good recording. + if( (popPosition + popWidth) >= recording->numFrames ) + { + popWidth = (recording->numFrames - popPosition) - 1; + } + + for( i=0; i<popWidth; i++ ) + { + float good = recording->buffer[i+popPosition]; + float bad = (good > 0.0) ? (good - popAmplitude) : (good + popAmplitude); + recording->buffer[i+popPosition] = bad; + } +} + +/*==========================================================================================*/ +/** + * Detect one phase error in a recording. + */ +static int TestDetectSinglePhaseError( double sampleRate, int cycleSize, int latencyFrames, int glitchPosition, int framesAdded ) +{ + int result = 0; + PaQaRecording recording; + PaQaTestTone testTone; + PaQaAnalysisResult analysisResult = { 0.0 }; + int framesDropped = 0; + int maxFrames = ((int)sampleRate) * 2; + + testTone.samplesPerFrame = 1; + testTone.sampleRate = sampleRate; + testTone.frequency = sampleRate / cycleSize; + testTone.amplitude = 0.5; + testTone.startDelay = latencyFrames; + + result = PaQa_InitializeRecording( &recording, maxFrames, (int) sampleRate ); + QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); + + MakeRecordingWithAddedFrames( &recording, &testTone, glitchPosition, framesAdded ); + + PaQa_AnalyseRecording( &recording, &testTone, &analysisResult ); + + if( framesAdded < 0 ) + { + framesDropped = -framesAdded; + framesAdded = 0; + } + +#if PRINT_REPORTS + printf("\n=== Dropped Frame Analysis ===================\n"); + printf(" expected actual\n"); + printf(" latency: %10.3f %10.3f\n", (double)latencyFrames, analysisResult.latency ); + printf(" num added frames: %10.3f %10.3f\n", (double)framesAdded, analysisResult.numAddedFrames ); + printf(" added frames at: %10.3f %10.3f\n", (double)glitchPosition, analysisResult.addedFramesPosition ); + printf(" num dropped frames: %10.3f %10.3f\n", (double)framesDropped, analysisResult.numDroppedFrames ); + printf(" dropped frames at: %10.3f %10.3f\n", (double)glitchPosition, analysisResult.droppedFramesPosition ); +#endif + + QA_ASSERT_CLOSE( "PaQa_AnalyseRecording latency", latencyFrames, analysisResult.latency, 0.5 ); + QA_ASSERT_CLOSE( "PaQa_AnalyseRecording framesAdded", framesAdded, analysisResult.numAddedFrames, 1.0 ); + QA_ASSERT_CLOSE( "PaQa_AnalyseRecording framesDropped", framesDropped, analysisResult.numDroppedFrames, 1.0 ); +// QA_ASSERT_CLOSE( "PaQa_AnalyseRecording glitchPosition", glitchPosition, analysisResult.glitchPosition, cycleSize ); + + PaQa_TerminateRecording( &recording ); + return 0; + +error: + PaQa_TerminateRecording( &recording); + return 1; +} + +/*==========================================================================================*/ +/** + * Test various dropped sample scenarios. + */ +static int TestDetectPhaseErrors( void ) +{ + int result; + + result = TestDetectSinglePhaseError( 44100, 200, 477, -1, 0 ); + if( result < 0 ) return result; +/* + result = TestDetectSinglePhaseError( 44100, 200, 77, -1, 0 ); + if( result < 0 ) return result; + + result = TestDetectSinglePhaseError( 44100, 200, 83, 3712, 9 ); + if( result < 0 ) return result; + + result = TestDetectSinglePhaseError( 44100, 280, 83, 3712, 27 ); + if( result < 0 ) return result; + + result = TestDetectSinglePhaseError( 44100, 200, 234, 3712, -9 ); + if( result < 0 ) return result; + + result = TestDetectSinglePhaseError( 44100, 200, 2091, 8923, -2 ); + if( result < 0 ) return result; + + result = TestDetectSinglePhaseError( 44100, 120, 1782, 5772, -18 ); + if( result < 0 ) return result; + + // Note that if the frequency is too high then it is hard to detect single dropped frames. + result = TestDetectSinglePhaseError( 44100, 200, 500, 4251, -1 ); + if( result < 0 ) return result; +*/ + return 0; +} + +/*==========================================================================================*/ +/** + * Detect one pop in a recording. + */ +static int TestDetectSinglePop( double sampleRate, int cycleSize, int latencyFrames, int popPosition, int popWidth, double popAmplitude ) +{ + int result = 0; + PaQaRecording recording; + PaQaTestTone testTone; + PaQaAnalysisResult analysisResult = { 0.0 }; + int maxFrames = ((int)sampleRate) * 2; + + testTone.samplesPerFrame = 1; + testTone.sampleRate = sampleRate; + testTone.frequency = sampleRate / cycleSize; + testTone.amplitude = 0.5; + testTone.startDelay = latencyFrames; + + result = PaQa_InitializeRecording( &recording, maxFrames, (int) sampleRate ); + QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); + + MakeRecordingWithPop( &recording, &testTone, popPosition, popWidth, popAmplitude ); + + PaQa_AnalyseRecording( &recording, &testTone, &analysisResult ); + +#if PRINT_REPORTS + printf("\n=== Pop Analysis ===================\n"); + printf(" expected actual\n"); + printf(" latency: %10.3f %10.3f\n", (double)latencyFrames, analysisResult.latency ); + printf(" popPosition: %10.3f %10.3f\n", (double)popPosition, analysisResult.popPosition ); + printf(" popAmplitude: %10.3f %10.3f\n", popAmplitude, analysisResult.popAmplitude ); + printf(" cycleSize: %6d\n", cycleSize ); + printf(" num added frames: %10.3f\n", analysisResult.numAddedFrames ); + printf(" added frames at: %10.3f\n", analysisResult.addedFramesPosition ); + printf(" num dropped frames: %10.3f\n", analysisResult.numDroppedFrames ); + printf(" dropped frames at: %10.3f\n", analysisResult.droppedFramesPosition ); +#endif + + QA_ASSERT_CLOSE( "PaQa_AnalyseRecording latency", latencyFrames, analysisResult.latency, 0.5 ); + QA_ASSERT_CLOSE( "PaQa_AnalyseRecording popPosition", popPosition, analysisResult.popPosition, 10 ); + if( popWidth > 0 ) + { + QA_ASSERT_CLOSE( "PaQa_AnalyseRecording popAmplitude", popAmplitude, analysisResult.popAmplitude, 0.1 * popAmplitude ); + } + + PaQa_TerminateRecording( &recording ); + return 0; + +error: + PaQa_SaveRecordingToWaveFile( &recording, "bad_recording.wav" ); + PaQa_TerminateRecording( &recording); + return 1; +} + +/*==========================================================================================*/ +/** + * Analyse recording with a DC offset. + */ +static int TestSingleInitialSpike( double sampleRate, int stepPosition, int cycleSize, int latencyFrames, double stepAmplitude ) +{ + int i; + int result = 0; + // Account for highpass filter offset. + int expectedLatency = latencyFrames + 1; + PaQaRecording recording; + + PaQaRecording hipassOutput = { 0 }; + BiquadFilter hipassFilter; + + PaQaTestTone testTone; + PaQaAnalysisResult analysisResult = { 0.0 }; + int maxFrames = ((int)sampleRate) * 2; + + testTone.samplesPerFrame = 1; + testTone.sampleRate = sampleRate; + testTone.frequency = sampleRate / cycleSize; + testTone.amplitude = -0.5; + testTone.startDelay = latencyFrames; + + result = PaQa_InitializeRecording( &recording, maxFrames, (int) sampleRate ); + QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); + + result = PaQa_InitializeRecording( &hipassOutput, maxFrames, (int) sampleRate ); + QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); + + MakeCleanRecording( &recording, &testTone ); + + // Apply DC step. + for( i=stepPosition; i<recording.numFrames; i++ ) + { + recording.buffer[i] += stepAmplitude; + } + + // Use high pass as a DC blocker! + BiquadFilter_SetupHighPass( &hipassFilter, 10.0 / sampleRate, 0.5 ); + PaQa_FilterRecording( &recording, &hipassOutput, &hipassFilter ); + + testTone.amplitude = 0.5; + PaQa_AnalyseRecording( &hipassOutput, &testTone, &analysisResult ); + +#if PRINT_REPORTS + printf("\n=== InitialSpike Analysis ===================\n"); + printf(" expected actual\n"); + printf(" latency: %10.3f %10.3f\n", (double)expectedLatency, analysisResult.latency ); + printf(" popPosition: %10.3f\n", analysisResult.popPosition ); + printf(" popAmplitude: %10.3f\n", analysisResult.popAmplitude ); + printf(" amplitudeRatio: %10.3f\n", analysisResult.amplitudeRatio ); + printf(" cycleSize: %6d\n", cycleSize ); + printf(" num added frames: %10.3f\n", analysisResult.numAddedFrames ); + printf(" added frames at: %10.3f\n", analysisResult.addedFramesPosition ); + printf(" num dropped frames: %10.3f\n", analysisResult.numDroppedFrames ); + printf(" dropped frames at: %10.3f\n", analysisResult.droppedFramesPosition ); +#endif + + QA_ASSERT_CLOSE( "PaQa_AnalyseRecording latency", expectedLatency, analysisResult.latency, 4.0 ); + QA_ASSERT_EQUALS( "PaQa_AnalyseRecording no pop from step", -1, (int) analysisResult.popPosition ); + PaQa_TerminateRecording( &recording ); + PaQa_TerminateRecording( &hipassOutput ); + return 0; + +error: + PaQa_SaveRecordingToWaveFile( &recording, "bad_step_original.wav" ); + PaQa_SaveRecordingToWaveFile( &hipassOutput, "bad_step_hipass.wav" ); + PaQa_TerminateRecording( &recording); + PaQa_TerminateRecording( &hipassOutput ); + return 1; +} + +/*==========================================================================================*/ +/** + * Test various dropped sample scenarios. + */ +static int TestDetectPops( void ) +{ + int result; + + // No pop. + result = TestDetectSinglePop( 44100, 200, 477, -1, 0, 0.0 ); + if( result < 0 ) return result; + + // short pop + result = TestDetectSinglePop( 44100, 300, 810, 3987, 1, 0.5 ); + if( result < 0 ) return result; + + // medium long pop + result = TestDetectSinglePop( 44100, 300, 810, 9876, 5, 0.5 ); + if( result < 0 ) return result; + + // short tiny pop + result = TestDetectSinglePop( 44100, 250, 810, 5672, 1, 0.05 ); + if( result < 0 ) return result; + + + return 0; +} + +/*==========================================================================================*/ +/** + * Test analysis when there is a DC offset step before the sine signal. + */ +static int TestInitialSpike( void ) +{ + int result; + +//( double sampleRate, int stepPosition, int cycleSize, int latencyFrames, double stepAmplitude ) + // No spike. + result = TestSingleInitialSpike( 44100, 32, 100, 537, 0.0 ); + if( result < 0 ) return result; + + // Small spike. + result = TestSingleInitialSpike( 44100, 32, 100, 537, 0.1 ); + if( result < 0 ) return result; + + // short pop like Ross's error. + result = TestSingleInitialSpike( 8000, 32, 42, 2000, 0.1 ); + if( result < 0 ) return result; + + // Medium spike. + result = TestSingleInitialSpike( 44100, 40, 190, 3000, 0.5 ); + if( result < 0 ) return result; + + // Spike near sine. + //result = TestSingleInitialSpike( 44100, 2900, 140, 3000, 0.1 ); + if( result < 0 ) return result; + + + return 0; +} + + +#if TEST_SAVED_WAVE +/*==========================================================================================*/ +/** + * Simple test that writes a sawtooth waveform to a file. + */ +static int TestSavedWave() +{ + int i,j; + WAV_Writer writer; + int result = 0; +#define NUM_SAMPLES (200) + short data[NUM_SAMPLES]; + short saw = 0; + + + result = Audio_WAV_OpenWriter( &writer, "test_sawtooth.wav", 44100, 1 ); + if( result < 0 ) goto error; + + for( i=0; i<15; i++ ) + { + for( j=0; j<NUM_SAMPLES; j++ ) + { + data[j] = saw; + saw += 293; + } + result = Audio_WAV_WriteShorts( &writer, data, NUM_SAMPLES ); + if( result < 0 ) goto error; + } + + result = Audio_WAV_CloseWriter( &writer ); + if( result < 0 ) goto error; + + + return 0; + +error: + printf("ERROR: result = %d\n", result ); + return result; +} +#endif /* TEST_SAVED_WAVE */ + +/*==========================================================================================*/ +/** + * Easy way to generate a sine tone recording. + */ +void PaQa_FillWithSine( PaQaRecording *recording, double sampleRate, double freq, double amp ) +{ + PaQaSineGenerator generator; + float buffer[FRAMES_PER_BLOCK]; + int samplesPerFrame = 1; + int stride = 1; + int done = 0; + + // Setup a sine oscillator. + PaQa_SetupSineGenerator( &generator, freq, amp, sampleRate ); + + done = 0; + while (!done) + { + PaQa_EraseBuffer( buffer, FRAMES_PER_BLOCK, samplesPerFrame ); + PaQa_MixSine( &generator, buffer, FRAMES_PER_BLOCK, stride ); + done = PaQa_WriteRecording( recording, buffer, FRAMES_PER_BLOCK, samplesPerFrame ); + } + +} + +/*==========================================================================================*/ +/** + * Generate a tone then knock it out using a filter. + * Also check using filter slightly off tune to see if some energy gets through. + */ +static int TestNotchFilter( void ) +{ + int result = 0; + PaQaRecording original = { 0 }; + PaQaRecording filtered = { 0 }; + BiquadFilter notchFilter; + double sampleRate = 44100.0; + int maxFrames = ((int)sampleRate) * 1; + + double freq = 234.5; + double amp = 0.5; + + double mag1, mag2, mag3; + + result = PaQa_InitializeRecording( &original, maxFrames, (int) sampleRate ); + QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); + + PaQa_FillWithSine( &original, sampleRate, freq, amp ); + + //result = PaQa_SaveRecordingToWaveFile( &original, "original.wav" ); + //QA_ASSERT_EQUALS( "PaQa_SaveRecordingToWaveFile failed", 0, result ); + + mag1 = PaQa_CorrelateSine( &original, freq, sampleRate, 0, original.numFrames, NULL ); + QA_ASSERT_CLOSE( "exact frequency match", amp, mag1, 0.01 ); + + // Filter with exact frequency. + result = PaQa_InitializeRecording( &filtered, maxFrames, (int) sampleRate ); + QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); + + BiquadFilter_SetupNotch( ¬chFilter, freq / sampleRate, 0.5 ); + PaQa_FilterRecording( &original, &filtered, ¬chFilter ); + result = PaQa_SaveRecordingToWaveFile( &filtered, "filtered1.wav" ); + QA_ASSERT_EQUALS( "PaQa_SaveRecordingToWaveFile failed", 0, result ); + + mag2 = PaQa_CorrelateSine( &filtered, freq, sampleRate, 0, filtered.numFrames, NULL ); + QA_ASSERT_CLOSE( "should eliminate tone", 0.0, mag2, 0.01 ); + + // Filter with mismatched frequency. + BiquadFilter_SetupNotch( ¬chFilter, 1.07 * freq / sampleRate, 2.0 ); + PaQa_FilterRecording( &original, &filtered, ¬chFilter ); + + //result = PaQa_SaveRecordingToWaveFile( &filtered, "badfiltered.wav" ); + //QA_ASSERT_EQUALS( "PaQa_SaveRecordingToWaveFile failed", 0, result ); + + mag3 = PaQa_CorrelateSine( &filtered, freq, sampleRate, 0, filtered.numFrames, NULL ); + QA_ASSERT_CLOSE( "should eliminate tone", amp*0.26, mag3, 0.01 ); + + + PaQa_TerminateRecording( &original ); + PaQa_TerminateRecording( &filtered ); + return 0; + +error: + PaQa_TerminateRecording( &original); + PaQa_TerminateRecording( &filtered ); + return 1; + +} + +/*==========================================================================================*/ +/** + */ +int PaQa_TestAnalyzer( void ) +{ + int result; + +#if TEST_SAVED_WAVE + // Write a simple wave file. + if ((result = TestSavedWave()) != 0) return result; +#endif /* TEST_SAVED_WAVE */ + + // Generate single tone and verify presence. + if ((result = TestSingleMonoTone()) != 0) return result; + + // Generate prime series of tones and verify presence. + if ((result = TestMixedMonoTones()) != 0) return result; + + // Detect dropped or added samples in a sine wave recording. + if ((result = TestDetectPhaseErrors()) != 0) return result; + + // Test to see if notch filter can knock out the test tone. + if ((result = TestNotchFilter()) != 0) return result; + + // Detect pops that get back in phase. + if ((result = TestDetectPops()) != 0) return result; + + // Test to see if the latency detector can be tricked like it was on Ross' Windows machine. + if ((result = TestInitialSpike()) != 0) return result; + + + return 0; +} diff --git a/portaudio/qa/loopback/src/test_audio_analyzer.h b/portaudio/qa/loopback/src/test_audio_analyzer.h new file mode 100644 index 0000000..bfe073f --- /dev/null +++ b/portaudio/qa/loopback/src/test_audio_analyzer.h @@ -0,0 +1,46 @@ + +/* + * 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. + */ + +#ifndef _TEST_AUDIO_ANALYZER_H +#define _TEST_AUDIO_ANALYZER_H + +/** Test the audio analyzer by itself without any PortAudio calls. */ +int PaQa_TestAnalyzer( void ); + + +#endif /* _TEST_AUDIO_ANALYZER_H */ diff --git a/portaudio/qa/loopback/src/write_wav.c b/portaudio/qa/loopback/src/write_wav.c new file mode 100755 index 0000000..aa5ee21 --- /dev/null +++ b/portaudio/qa/loopback/src/write_wav.c @@ -0,0 +1,242 @@ +/* + * 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. + */ + +/** + * Very simple WAV file writer for saving captured audio. + */ + +#include <stdio.h> +#include <stdlib.h> +#include "write_wav.h" + + +/* Write long word data to a little endian format byte array. */ +static void WriteLongLE( unsigned char **addrPtr, unsigned long data ) +{ + unsigned char *addr = *addrPtr; + *addr++ = (unsigned char) data; + *addr++ = (unsigned char) (data>>8); + *addr++ = (unsigned char) (data>>16); + *addr++ = (unsigned char) (data>>24); + *addrPtr = addr; +} + +/* Write short word data to a little endian format byte array. */ +static void WriteShortLE( unsigned char **addrPtr, unsigned short data ) +{ + unsigned char *addr = *addrPtr; + *addr++ = (unsigned char) data; + *addr++ = (unsigned char) (data>>8); + *addrPtr = addr; +} + +/* Write IFF ChunkType data to a byte array. */ +static void WriteChunkType( unsigned char **addrPtr, unsigned long cktyp ) +{ + unsigned char *addr = *addrPtr; + *addr++ = (unsigned char) (cktyp>>24); + *addr++ = (unsigned char) (cktyp>>16); + *addr++ = (unsigned char) (cktyp>>8); + *addr++ = (unsigned char) cktyp; + *addrPtr = addr; +} + +#define WAV_HEADER_SIZE (4 + 4 + 4 + /* RIFF+size+WAVE */ \ + 4 + 4 + 16 + /* fmt chunk */ \ + 4 + 4 ) /* data chunk */ + + +/********************************************************************************* + * Open named file and write WAV header to the file. + * The header includes the DATA chunk type and size. + * Returns number of bytes written to file or negative error code. + */ +long Audio_WAV_OpenWriter( WAV_Writer *writer, const char *fileName, int frameRate, int samplesPerFrame ) +{ + unsigned int bytesPerSecond; + unsigned char header[ WAV_HEADER_SIZE ]; + unsigned char *addr = header; + int numWritten; + + writer->dataSize = 0; + writer->dataSizeOffset = 0; + + writer->fid = fopen( fileName, "wb" ); + if( writer->fid == NULL ) + { + return -1; + } + +/* Write RIFF header. */ + WriteChunkType( &addr, RIFF_ID ); + +/* Write RIFF size as zero for now. Will patch later. */ + WriteLongLE( &addr, 0 ); + +/* Write WAVE form ID. */ + WriteChunkType( &addr, WAVE_ID ); + +/* Write format chunk based on AudioSample structure. */ + WriteChunkType( &addr, FMT_ID ); + WriteLongLE( &addr, 16 ); + WriteShortLE( &addr, WAVE_FORMAT_PCM ); + bytesPerSecond = frameRate * samplesPerFrame * sizeof( short); + WriteShortLE( &addr, (short) samplesPerFrame ); + WriteLongLE( &addr, frameRate ); + WriteLongLE( &addr, bytesPerSecond ); + WriteShortLE( &addr, (short) (samplesPerFrame * sizeof( short)) ); /* bytesPerBlock */ + WriteShortLE( &addr, (short) 16 ); /* bits per sample */ + +/* Write ID and size for 'data' chunk. */ + WriteChunkType( &addr, DATA_ID ); +/* Save offset so we can patch it later. */ + writer->dataSizeOffset = (int) (addr - header); + WriteLongLE( &addr, 0 ); + + numWritten = fwrite( header, 1, sizeof(header), writer->fid ); + if( numWritten != sizeof(header) ) return -1; + + return (int) numWritten; +} + +/********************************************************************************* + * Write to the data chunk portion of a WAV file. + * Returns bytes written or negative error code. + */ +long Audio_WAV_WriteShorts( WAV_Writer *writer, + short *samples, + int numSamples + ) +{ + unsigned char buffer[2]; + unsigned char *bufferPtr; + int i; + short *p = samples; + int numWritten; + int bytesWritten; + if( numSamples <= 0 ) + { + return -1; + } + + for( i=0; i<numSamples; i++ ) + { + bufferPtr = buffer; + WriteShortLE( &bufferPtr, *p++ ); + numWritten = fwrite( buffer, 1, sizeof( buffer), writer->fid ); + if( numWritten != sizeof(buffer) ) return -1; + } + bytesWritten = numSamples * sizeof(short); + writer->dataSize += bytesWritten; + return (int) bytesWritten; +} + +/********************************************************************************* + * Close WAV file. + * Update chunk sizes so it can be read by audio applications. + */ +long Audio_WAV_CloseWriter( WAV_Writer *writer ) +{ + unsigned char buffer[4]; + unsigned char *bufferPtr; + int numWritten; + int riffSize; + + /* Go back to beginning of file and update DATA size */ + int result = fseek( writer->fid, writer->dataSizeOffset, SEEK_SET ); + if( result < 0 ) return result; + + bufferPtr = buffer; + WriteLongLE( &bufferPtr, writer->dataSize ); + numWritten = fwrite( buffer, 1, sizeof( buffer), writer->fid ); + if( numWritten != sizeof(buffer) ) return -1; + + /* Update RIFF size */ + result = fseek( writer->fid, 4, SEEK_SET ); + if( result < 0 ) return result; + + riffSize = writer->dataSize + (WAV_HEADER_SIZE - 8); + bufferPtr = buffer; + WriteLongLE( &bufferPtr, riffSize ); + numWritten = fwrite( buffer, 1, sizeof( buffer), writer->fid ); + if( numWritten != sizeof(buffer) ) return -1; + + fclose( writer->fid ); + writer->fid = NULL; + return writer->dataSize; +} + +/********************************************************************************* + * Simple test that write a sawtooth waveform to a file. + */ +#if 0 +int main( void ) +{ + int i; + WAV_Writer writer; + int result; +#define NUM_SAMPLES (200) + short data[NUM_SAMPLES]; + short saw = 0; + + for( i=0; i<NUM_SAMPLES; i++ ) + { + data[i] = saw; + saw += 293; + } + + + result = Audio_WAV_OpenWriter( &writer, "rendered_midi.wav", 44100, 1 ); + if( result < 0 ) goto error; + + for( i=0; i<15; i++ ) + { + result = Audio_WAV_WriteShorts( &writer, data, NUM_SAMPLES ); + if( result < 0 ) goto error; + } + + result = Audio_WAV_CloseWriter( &writer ); + if( result < 0 ) goto error; + + + return 0; + +error: + printf("ERROR: result = %d\n", result ); + return result; +} +#endif diff --git a/portaudio/qa/loopback/src/write_wav.h b/portaudio/qa/loopback/src/write_wav.h new file mode 100755 index 0000000..3560055 --- /dev/null +++ b/portaudio/qa/loopback/src/write_wav.h @@ -0,0 +1,103 @@ +/* + * 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. + */ +#ifndef _WAV_WRITER_H +#define _WAV_WRITER_H + +/* + * WAV file writer. + * + * Author: Phil Burk + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* Define WAV Chunk and FORM types as 4 byte integers. */ +#define RIFF_ID (('R'<<24) | ('I'<<16) | ('F'<<8) | 'F') +#define WAVE_ID (('W'<<24) | ('A'<<16) | ('V'<<8) | 'E') +#define FMT_ID (('f'<<24) | ('m'<<16) | ('t'<<8) | ' ') +#define DATA_ID (('d'<<24) | ('a'<<16) | ('t'<<8) | 'a') +#define FACT_ID (('f'<<24) | ('a'<<16) | ('c'<<8) | 't') + +/* Errors returned by Audio_ParseSampleImage_WAV */ +#define WAV_ERR_CHUNK_SIZE (-1) /* Chunk size is illegal or past file size. */ +#define WAV_ERR_FILE_TYPE (-2) /* Not a WAV file. */ +#define WAV_ERR_ILLEGAL_VALUE (-3) /* Illegal or unsupported value. Eg. 927 bits/sample */ +#define WAV_ERR_FORMAT_TYPE (-4) /* Unsupported format, eg. compressed. */ +#define WAV_ERR_TRUNCATED (-5) /* End of file missing. */ + +/* WAV PCM data format ID */ +#define WAVE_FORMAT_PCM (1) +#define WAVE_FORMAT_IMA_ADPCM (0x0011) + + +typedef struct WAV_Writer_s +{ + FILE *fid; + /* Offset in file for data size. */ + int dataSizeOffset; + int dataSize; +} WAV_Writer; + +/********************************************************************************* + * Open named file and write WAV header to the file. + * The header includes the DATA chunk type and size. + * Returns number of bytes written to file or negative error code. + */ +long Audio_WAV_OpenWriter( WAV_Writer *writer, const char *fileName, int frameRate, int samplesPerFrame ); + +/********************************************************************************* + * Write to the data chunk portion of a WAV file. + * Returns bytes written or negative error code. + */ +long Audio_WAV_WriteShorts( WAV_Writer *writer, + short *samples, + int numSamples + ); + +/********************************************************************************* + * Close WAV file. + * Update chunk sizes so it can be read by audio applications. + */ +long Audio_WAV_CloseWriter( WAV_Writer *writer ); + +#ifdef __cplusplus +}; +#endif + +#endif /* _WAV_WRITER_H */ diff --git a/portaudio/qa/paqa_devs.c b/portaudio/qa/paqa_devs.c new file mode 100644 index 0000000..858ed78 --- /dev/null +++ b/portaudio/qa/paqa_devs.c @@ -0,0 +1,454 @@ +/** @file paqa_devs.c + @ingroup qa_src + @brief Self Testing Quality Assurance app for PortAudio + Try to open devices and run through all possible configurations. + By default, open only the default devices. Command line options support + opening every device, or all input devices, or all output devices. + This test does not verify that the configuration works well. + It just verifies that it does not crash. It requires a human to + listen to the sine wave outputs. + + @author Phil Burk http://www.softsynth.com + + Pieter adapted to V19 API. Test now relies heavily on + Pa_IsFormatSupported(). Uses same 'standard' sample rates + as in test pa_devs.c. +*/ +/* + * $Id$ + * + * This program uses the PortAudio Portable Audio Library. + * For more information see: http://www.portaudio.com + * Copyright (c) 1999-2000 Ross Bencina and Phil Burk + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The text above constitutes the entire PortAudio license; however, + * the PortAudio community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <math.h> +#include "portaudio.h" +#include "pa_trace.h" + +/****************************************** Definitions ***********/ +#define MODE_INPUT (0) +#define MODE_OUTPUT (1) +#define MAX_TEST_CHANNELS (4) +#define LOWEST_FREQUENCY (300.0) +#define LOWEST_SAMPLE_RATE (8000.0) +#define PHASE_INCREMENT (2.0 * M_PI * LOWEST_FREQUENCY / LOWEST_SAMPLE_RATE) +#define SINE_AMPLITUDE (0.2) + +typedef struct PaQaData +{ + unsigned long framesLeft; + int numChannels; + int bytesPerSample; + int mode; + float phase; + PaSampleFormat format; +} +PaQaData; + +/****************************************** Prototypes ***********/ +static void TestDevices( int mode, int allDevices ); +static void TestFormats( int mode, PaDeviceIndex deviceID, double sampleRate, + int numChannels ); +static int TestAdvance( int mode, PaDeviceIndex deviceID, double sampleRate, + int numChannels, PaSampleFormat format ); +static int QaCallback( const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData ); + +/****************************************** Globals ***********/ +static int gNumPassed = 0; +static int gNumFailed = 0; + +/****************************************** Macros ***********/ +/* Print ERROR if it fails. Tally success or failure. */ +/* Odd do-while wrapper seems to be needed for some compilers. */ +#define EXPECT(_exp) \ + do \ + { \ + if ((_exp)) {\ + /* printf("SUCCESS for %s\n", #_exp ); */ \ + gNumPassed++; \ + } \ + else { \ + printf("ERROR - 0x%x - %s for %s\n", result, \ + ((result == 0) ? "-" : Pa_GetErrorText(result)), \ + #_exp ); \ + gNumFailed++; \ + goto error; \ + } \ + } while(0) + +static float NextSineSample( PaQaData *data ) +{ + float phase = data->phase + PHASE_INCREMENT; + if( phase > M_PI ) phase -= 2.0 * M_PI; + data->phase = phase; + return sinf(phase) * SINE_AMPLITUDE; +} + +/*******************************************************************/ +/* This routine will be called by the PortAudio engine when audio is needed. +** It may be called at interrupt level on some machines so don't do anything +** that could mess up the system like calling malloc() or free(). +*/ +static int QaCallback( const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData ) +{ + unsigned long frameIndex; + unsigned long channelIndex; + float sample; + PaQaData *data = (PaQaData *) userData; + (void) inputBuffer; + + /* Play simple sine wave. */ + if( data->mode == MODE_OUTPUT ) + { + switch( data->format ) + { + case paFloat32: + { + float *out = (float *) outputBuffer; + for( frameIndex = 0; frameIndex < framesPerBuffer; frameIndex++ ) + { + sample = NextSineSample( data ); + for( channelIndex = 0; channelIndex < data->numChannels; channelIndex++ ) + { + *out++ = sample; + } + } + } + break; + + case paInt32: + { + int *out = (int *) outputBuffer; + for( frameIndex = 0; frameIndex < framesPerBuffer; frameIndex++ ) + { + sample = NextSineSample( data ); + for( channelIndex = 0; channelIndex < data->numChannels; channelIndex++ ) + { + *out++ = ((int)(sample * 0x00800000)) << 8; + } + } + } + break; + + case paInt16: + { + short *out = (short *) outputBuffer; + for( frameIndex = 0; frameIndex < framesPerBuffer; frameIndex++ ) + { + sample = NextSineSample( data ); + for( channelIndex = 0; channelIndex < data->numChannels; channelIndex++ ) + { + *out++ = (short)(sample * 32767); + } + } + } + break; + + default: + { + unsigned char *out = (unsigned char *) outputBuffer; + unsigned long numBytes = framesPerBuffer * data->numChannels * data->bytesPerSample; + memset(out, 0, numBytes); + } + break; + } + } + /* Are we through yet? */ + if( data->framesLeft > framesPerBuffer ) + { + PaUtil_AddTraceMessage("QaCallback: running. framesLeft", data->framesLeft ); + data->framesLeft -= framesPerBuffer; + return 0; + } + else + { + PaUtil_AddTraceMessage("QaCallback: DONE! framesLeft", data->framesLeft ); + data->framesLeft = 0; + return 1; + } +} + +/*******************************************************************/ +static void usage( const char *name ) +{ + printf("%s [-a]\n", name); + printf(" -a - Test ALL devices, otherwise just the default devices.\n"); + printf(" -i - Test INPUT only.\n"); + printf(" -o - Test OUTPUT only.\n"); + printf(" -? - Help\n"); +} + +/*******************************************************************/ +int main( int argc, char **argv ); +int main( int argc, char **argv ) +{ + int i; + PaError result; + int allDevices = 0; + int testOutput = 1; + int testInput = 1; + char *executableName = argv[0]; + + /* Parse command line parameters. */ + i = 1; + while( i<argc ) + { + char *arg = argv[i]; + if( arg[0] == '-' ) + { + switch(arg[1]) + { + case 'a': + allDevices = 1; + break; + case 'i': + testOutput = 0; + break; + case 'o': + testInput = 0; + break; + + default: + printf("Illegal option: %s\n", arg); + case '?': + usage( executableName ); + exit(1); + break; + } + } + else + { + printf("Illegal argument: %s\n", arg); + usage( executableName ); + return 1; + + } + i += 1; + } + + EXPECT(sizeof(short) == 2); /* The callback assumes we have 16-bit shorts. */ + EXPECT(sizeof(int) == 4); /* The callback assumes we have 32-bit ints. */ + EXPECT( ((result=Pa_Initialize()) == 0) ); + + if( testOutput ) + { + printf("\n---- Test OUTPUT ---------------\n"); + TestDevices( MODE_OUTPUT, allDevices ); + } + if( testInput ) + { + printf("\n---- Test INPUT ---------------\n"); + TestDevices( MODE_INPUT, allDevices ); + } + +error: + Pa_Terminate(); + printf("QA Report: %d passed, %d failed.\n", gNumPassed, gNumFailed ); + return (gNumFailed > 0) ? 1 : 0; +} + +/******************************************************************* +* Try each output device, through its full range of capabilities. */ +static void TestDevices( int mode, int allDevices ) +{ + int id, jc, i; + int maxChannels; + int isDefault; + const PaDeviceInfo *pdi; + static double standardSampleRates[] = { 8000.0, 9600.0, 11025.0, 12000.0, + 16000.0, 22050.0, 24000.0, + 32000.0, 44100.0, 48000.0, + 88200.0, 96000.0, + -1.0 }; /* Negative terminated list. */ + int numDevices = Pa_GetDeviceCount(); + for( id=0; id<numDevices; id++ ) /* Iterate through all devices. */ + { + pdi = Pa_GetDeviceInfo( id ); + + if( mode == MODE_INPUT ) { + maxChannels = pdi->maxInputChannels; + isDefault = ( id == Pa_GetDefaultInputDevice()); + } else { + maxChannels = pdi->maxOutputChannels; + isDefault = ( id == Pa_GetDefaultOutputDevice()); + } + if( maxChannels > MAX_TEST_CHANNELS ) + maxChannels = MAX_TEST_CHANNELS; + + if (!allDevices && !isDefault) continue; // skip this device + + for( jc=1; jc<=maxChannels; jc++ ) + { + printf("\n===========================================================\n"); + printf(" Device = %s\n", pdi->name ); + printf("===========================================================\n"); + /* Try each standard sample rate. */ + for( i=0; standardSampleRates[i] > 0; i++ ) + { + TestFormats( mode, (PaDeviceIndex)id, standardSampleRates[i], jc ); + } + } + } +} + +/*******************************************************************/ +static void TestFormats( int mode, PaDeviceIndex deviceID, double sampleRate, + int numChannels ) +{ + TestAdvance( mode, deviceID, sampleRate, numChannels, paFloat32 ); + TestAdvance( mode, deviceID, sampleRate, numChannels, paInt16 ); + TestAdvance( mode, deviceID, sampleRate, numChannels, paInt32 ); + /* TestAdvance( mode, deviceID, sampleRate, numChannels, paInt24 ); */ +} + +/*******************************************************************/ +static int TestAdvance( int mode, PaDeviceIndex deviceID, double sampleRate, + int numChannels, PaSampleFormat format ) +{ + PaStreamParameters inputParameters, outputParameters, *ipp, *opp; + PaStream *stream = NULL; + PaError result = paNoError; + PaQaData myData; + #define FRAMES_PER_BUFFER (64) + const int kNumSeconds = 100; + + /* Setup data for synthesis thread. */ + myData.framesLeft = (unsigned long) (sampleRate * kNumSeconds); + myData.numChannels = numChannels; + myData.mode = mode; + myData.format = format; + switch( format ) + { + case paFloat32: + case paInt32: + case paInt24: + myData.bytesPerSample = 4; + break; +/* case paPackedInt24: + myData.bytesPerSample = 3; + break; */ + default: + myData.bytesPerSample = 2; + break; + } + + if( mode == MODE_INPUT ) + { + inputParameters.device = deviceID; + inputParameters.channelCount = numChannels; + inputParameters.sampleFormat = format; + inputParameters.suggestedLatency = + Pa_GetDeviceInfo( inputParameters.device )->defaultLowInputLatency; + inputParameters.hostApiSpecificStreamInfo = NULL; + ipp = &inputParameters; + } + else + { + ipp = NULL; + } + + if( mode == MODE_OUTPUT ) + { + outputParameters.device = deviceID; + outputParameters.channelCount = numChannels; + outputParameters.sampleFormat = format; + outputParameters.suggestedLatency = + Pa_GetDeviceInfo( outputParameters.device )->defaultLowOutputLatency; + outputParameters.hostApiSpecificStreamInfo = NULL; + opp = &outputParameters; + } + else + { + opp = NULL; + } + + if(paFormatIsSupported == Pa_IsFormatSupported( ipp, opp, sampleRate )) + { + printf("------ TestAdvance: %s, device = %d, rate = %g" + ", numChannels = %d, format = %lu -------\n", + ( mode == MODE_INPUT ) ? "INPUT" : "OUTPUT", + deviceID, sampleRate, numChannels, (unsigned long)format); + EXPECT( ((result = Pa_OpenStream( &stream, + ipp, + opp, + sampleRate, + FRAMES_PER_BUFFER, + paClipOff, /* we won't output out of range samples so don't bother clipping them */ + QaCallback, + &myData ) ) == 0) ); + if( stream ) + { + PaTime oldStamp, newStamp; + unsigned long oldFrames; + int minDelay = ( mode == MODE_INPUT ) ? 1000 : 400; + /* Was: + int minNumBuffers = Pa_GetMinNumBuffers( FRAMES_PER_BUFFER, sampleRate ); + int msec = (int) ((minNumBuffers * 3 * 1000.0 * FRAMES_PER_BUFFER) / sampleRate); + */ + int msec = (int)( 3.0 * + (( mode == MODE_INPUT ) ? inputParameters.suggestedLatency : outputParameters.suggestedLatency )); + if( msec < minDelay ) msec = minDelay; + printf("msec = %d\n", msec); /**/ + EXPECT( ((result=Pa_StartStream( stream )) == 0) ); + /* Check to make sure PortAudio is advancing timeStamp. */ + oldStamp = Pa_GetStreamTime(stream); + Pa_Sleep(msec); + newStamp = Pa_GetStreamTime(stream); + printf("oldStamp = %9.6f, newStamp = %9.6f\n", oldStamp, newStamp ); /**/ + EXPECT( (oldStamp < newStamp) ); + /* Check to make sure callback is decrementing framesLeft. */ + oldFrames = myData.framesLeft; + Pa_Sleep(msec); + printf("oldFrames = %lu, myData.framesLeft = %lu\n", oldFrames, myData.framesLeft ); /**/ + EXPECT( (oldFrames > myData.framesLeft) ); + EXPECT( ((result=Pa_CloseStream( stream )) == 0) ); + stream = NULL; + } + } + return 0; +error: + if( stream != NULL ) Pa_CloseStream( stream ); + return -1; +} diff --git a/portaudio/qa/paqa_errs.c b/portaudio/qa/paqa_errs.c new file mode 100644 index 0000000..8d4094f --- /dev/null +++ b/portaudio/qa/paqa_errs.c @@ -0,0 +1,403 @@ +/** @file paqa_errs.c + @ingroup qa_src + @brief Self Testing Quality Assurance app for PortAudio + Do lots of bad things to test error reporting. + @author Phil Burk http://www.softsynth.com + Pieter Suurmond adapted to V19 API. +*/ +/* + * $Id$ + * + * This program uses the PortAudio Portable Audio Library. + * For more information see: http://www.portaudio.com + * Copyright (c) 1999-2000 Ross Bencina and Phil Burk + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The text above constitutes the entire PortAudio license; however, + * the PortAudio community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +#include <stdio.h> +#include <math.h> + +#include "portaudio.h" + +/*--------- Definitions ---------*/ +#define MODE_INPUT (0) +#define MODE_OUTPUT (1) +#define FRAMES_PER_BUFFER (64) +#define SAMPLE_RATE (44100.0) + +typedef struct PaQaData +{ + unsigned long framesLeft; + int numChannels; + int bytesPerSample; + int mode; +} +PaQaData; + +static int gNumPassed = 0; /* Two globals */ +static int gNumFailed = 0; + +/*------------------- Macros ------------------------------*/ +/* Print ERROR if it fails. Tally success or failure. Odd */ +/* do-while wrapper seems to be needed for some compilers. */ + +#define EXPECT(_exp) \ + do \ + { \ + if ((_exp)) {\ + gNumPassed++; \ + } \ + else { \ + printf("\nERROR - 0x%x - %s for %s\n", result, Pa_GetErrorText(result), #_exp ); \ + gNumFailed++; \ + goto error; \ + } \ + } while(0) + +#define HOPEFOR(_exp) \ + do \ + { \ + if ((_exp)) {\ + gNumPassed++; \ + } \ + else { \ + printf("\nERROR - 0x%x - %s for %s\n", result, Pa_GetErrorText(result), #_exp ); \ + gNumFailed++; \ + } \ + } while(0) + +/*-------------------------------------------------------------------------*/ +/* This routine will be called by the PortAudio engine when audio is needed. + It may be called at interrupt level on some machines so don't do anything + that could mess up the system like calling malloc() or free(). +*/ +static int QaCallback( const void* inputBuffer, + void* outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, + void* userData ) +{ + unsigned long i; + unsigned char* out = (unsigned char *) outputBuffer; + PaQaData* data = (PaQaData *) userData; + + (void)inputBuffer; /* Prevent "unused variable" warnings. */ + + /* Zero out buffer so we don't hear terrible noise. */ + if( data->mode == MODE_OUTPUT ) + { + unsigned long numBytes = framesPerBuffer * data->numChannels * data->bytesPerSample; + for( i=0; i<numBytes; i++ ) + { + *out++ = 0; + } + } + /* Are we through yet? */ + if( data->framesLeft > framesPerBuffer ) + { + data->framesLeft -= framesPerBuffer; + return 0; + } + else + { + data->framesLeft = 0; + return 1; + } +} + +static PaDeviceIndex FindInputOnlyDevice(void) +{ + PaDeviceIndex result = Pa_GetDefaultInputDevice(); + if( result != paNoDevice && Pa_GetDeviceInfo(result)->maxOutputChannels == 0 ) + return result; + + for( result = 0; result < Pa_GetDeviceCount(); ++result ) + { + if( Pa_GetDeviceInfo(result)->maxOutputChannels == 0 ) + return result; + } + + return paNoDevice; +} + +static PaDeviceIndex FindOutputOnlyDevice(void) +{ + PaDeviceIndex result = Pa_GetDefaultOutputDevice(); + if( result != paNoDevice && Pa_GetDeviceInfo(result)->maxInputChannels == 0 ) + return result; + + for( result = 0; result < Pa_GetDeviceCount(); ++result ) + { + if( Pa_GetDeviceInfo(result)->maxInputChannels == 0 ) + return result; + } + + return paNoDevice; +} + +/*-------------------------------------------------------------------------------------------------*/ +static int TestBadOpens( void ) +{ + PaStream* stream = NULL; + PaError result; + PaQaData myData; + PaStreamParameters ipp, opp; + const PaDeviceInfo* info = NULL; + + + /* Setup data for synthesis thread. */ + myData.framesLeft = (unsigned long) (SAMPLE_RATE * 100); /* 100 seconds */ + myData.numChannels = 1; + myData.mode = MODE_OUTPUT; + + /*----------------------------- No devices specified: */ + ipp.device = opp.device = paNoDevice; + ipp.channelCount = opp.channelCount = 0; /* Also no channels. */ + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + /* Take the low latency of the default device for all subsequent tests. */ + info = Pa_GetDeviceInfo(Pa_GetDefaultInputDevice()); + ipp.suggestedLatency = info ? info->defaultLowInputLatency : 0.100; + info = Pa_GetDeviceInfo(Pa_GetDefaultOutputDevice()); + opp.suggestedLatency = info ? info->defaultLowOutputLatency : 0.100; + HOPEFOR(((result = Pa_OpenStream(&stream, &ipp, &opp, + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, QaCallback, &myData )) == paInvalidDevice)); + + /*----------------------------- No devices specified #2: */ + HOPEFOR(((result = Pa_OpenStream(&stream, NULL, NULL, + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, QaCallback, &myData )) == paInvalidDevice)); + + /*----------------------------- Out of range input device specified: */ + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 0; ipp.device = Pa_GetDeviceCount(); /* And no output device, and no channels. */ + opp.channelCount = 0; opp.device = paNoDevice; + HOPEFOR(((result = Pa_OpenStream(&stream, &ipp, NULL, + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, QaCallback, &myData )) == paInvalidDevice)); + + /*----------------------------- Out of range output device specified: */ + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 0; ipp.device = paNoDevice; /* And no input device, and no channels. */ + opp.channelCount = 0; opp.device = Pa_GetDeviceCount(); + HOPEFOR(((result = Pa_OpenStream(&stream, NULL, &opp, + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, QaCallback, &myData )) == paInvalidDevice)); + + if (Pa_GetDefaultInputDevice() != paNoDevice) { + /*----------------------------- Zero input channels: */ + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 0; ipp.device = Pa_GetDefaultInputDevice(); + opp.channelCount = 0; opp.device = paNoDevice; /* And no output device, and no output channels. */ + HOPEFOR(((result = Pa_OpenStream(&stream, &ipp, NULL, + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, QaCallback, &myData )) == paInvalidChannelCount)); + } + + if (Pa_GetDefaultOutputDevice() != paNoDevice) { + /*----------------------------- Zero output channels: */ + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 0; ipp.device = paNoDevice; /* And no input device, and no input channels. */ + opp.channelCount = 0; opp.device = Pa_GetDefaultOutputDevice(); + HOPEFOR(((result = Pa_OpenStream(&stream, NULL, &opp, + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, QaCallback, &myData )) == paInvalidChannelCount)); + } + /*----------------------------- Nonzero input and output channels but no output device: */ + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 2; ipp.device = Pa_GetDefaultInputDevice(); /* Both stereo. */ + opp.channelCount = 2; opp.device = paNoDevice; + HOPEFOR(((result = Pa_OpenStream(&stream, &ipp, &opp, + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, QaCallback, &myData )) == paInvalidDevice)); + + /*----------------------------- Nonzero input and output channels but no input device: */ + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 2; ipp.device = paNoDevice; + opp.channelCount = 2; opp.device = Pa_GetDefaultOutputDevice(); + HOPEFOR(((result = Pa_OpenStream(&stream, &ipp, &opp, + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, QaCallback, &myData )) == paInvalidDevice)); + + if (Pa_GetDefaultOutputDevice() != paNoDevice) { + /*----------------------------- NULL stream pointer: */ + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 0; ipp.device = paNoDevice; /* Output is more likely than input. */ + opp.channelCount = 2; opp.device = Pa_GetDefaultOutputDevice(); /* Only 2 output channels. */ + HOPEFOR(((result = Pa_OpenStream(NULL, &ipp, &opp, + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, QaCallback, &myData )) == paBadStreamPtr)); + + /*----------------------------- Low sample rate: */ + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 0; ipp.device = paNoDevice; + opp.channelCount = 2; opp.device = Pa_GetDefaultOutputDevice(); + HOPEFOR(((result = Pa_OpenStream(&stream, NULL, &opp, + 1.0, FRAMES_PER_BUFFER, /* 1 cycle per second (1 Hz) is too low. */ + paClipOff, QaCallback, &myData )) == paInvalidSampleRate)); + + /*----------------------------- High sample rate: */ + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 0; ipp.device = paNoDevice; + opp.channelCount = 2; opp.device = Pa_GetDefaultOutputDevice(); + HOPEFOR(((result = Pa_OpenStream(&stream, NULL, &opp, + 10000000.0, FRAMES_PER_BUFFER, /* 10^6 cycles per second (10 MHz) is too high. */ + paClipOff, QaCallback, &myData )) == paInvalidSampleRate)); + + /*----------------------------- NULL callback: */ + /* NULL callback is valid in V19 -- it means use blocking read/write stream + + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 0; ipp.device = paNoDevice; + opp.channelCount = 2; opp.device = Pa_GetDefaultOutputDevice(); + HOPEFOR(((result = Pa_OpenStream(&stream, NULL, &opp, + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, + NULL, + &myData )) == paNullCallback)); + */ + + /*----------------------------- Bad flag: */ + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 0; ipp.device = paNoDevice; + opp.channelCount = 2; opp.device = Pa_GetDefaultOutputDevice(); + HOPEFOR(((result = Pa_OpenStream(&stream, NULL, &opp, + SAMPLE_RATE, FRAMES_PER_BUFFER, + 255, /* Is 8 maybe legal V19 API? */ + QaCallback, &myData )) == paInvalidFlag)); + } + + /*----------------------------- using input device as output device: */ + if( FindInputOnlyDevice() != paNoDevice ) + { + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 0; ipp.device = paNoDevice; /* And no input device, and no channels. */ + opp.channelCount = 2; opp.device = FindInputOnlyDevice(); + HOPEFOR(((result = Pa_OpenStream(&stream, NULL, &opp, + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, QaCallback, &myData )) == paInvalidChannelCount)); + } + + /*----------------------------- using output device as input device: */ + if( FindOutputOnlyDevice() != paNoDevice ) + { + ipp.hostApiSpecificStreamInfo = opp.hostApiSpecificStreamInfo = NULL; + ipp.sampleFormat = opp.sampleFormat = paFloat32; + ipp.channelCount = 2; ipp.device = FindOutputOnlyDevice(); + opp.channelCount = 0; opp.device = paNoDevice; /* And no output device, and no channels. */ + HOPEFOR(((result = Pa_OpenStream(&stream, &ipp, NULL, + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, QaCallback, &myData )) == paInvalidChannelCount)); + + } + + if( stream != NULL ) Pa_CloseStream( stream ); + return result; +} + +/*-----------------------------------------------------------------------------------------*/ +static int TestBadActions( void ) +{ + PaStream* stream = NULL; + const PaDeviceInfo* deviceInfo = NULL; + PaError result = 0; + PaQaData myData; + PaStreamParameters opp; + const PaDeviceInfo* info = NULL; + + /* Setup data for synthesis thread. */ + myData.framesLeft = (unsigned long)(SAMPLE_RATE * 100); /* 100 seconds */ + myData.numChannels = 1; + myData.mode = MODE_OUTPUT; + + opp.device = Pa_GetDefaultOutputDevice(); /* Default output. */ + opp.channelCount = 2; /* Stereo output. */ + opp.hostApiSpecificStreamInfo = NULL; + opp.sampleFormat = paFloat32; + info = Pa_GetDeviceInfo(opp.device); + opp.suggestedLatency = info ? info->defaultLowOutputLatency : 0.100; + + if (opp.device != paNoDevice) { + HOPEFOR(((result = Pa_OpenStream(&stream, NULL, /* Take NULL as input parame- */ + &opp, /* ters, meaning try only output. */ + SAMPLE_RATE, FRAMES_PER_BUFFER, + paClipOff, QaCallback, &myData )) == paNoError)); + } + + HOPEFOR(((deviceInfo = Pa_GetDeviceInfo(paNoDevice)) == NULL)); + HOPEFOR(((deviceInfo = Pa_GetDeviceInfo(87654)) == NULL)); + HOPEFOR(((result = Pa_StartStream(NULL)) == paBadStreamPtr)); + HOPEFOR(((result = Pa_StopStream(NULL)) == paBadStreamPtr)); + HOPEFOR(((result = Pa_IsStreamStopped(NULL)) == paBadStreamPtr)); + HOPEFOR(((result = Pa_IsStreamActive(NULL)) == paBadStreamPtr)); + HOPEFOR(((result = Pa_CloseStream(NULL)) == paBadStreamPtr)); + HOPEFOR(((result = Pa_SetStreamFinishedCallback(NULL, NULL)) == paBadStreamPtr)); + HOPEFOR(((result = !Pa_GetStreamInfo(NULL)))); + HOPEFOR(((result = Pa_GetStreamTime(NULL)) == 0.0)); + HOPEFOR(((result = Pa_GetStreamCpuLoad(NULL)) == 0.0)); + HOPEFOR(((result = Pa_ReadStream(NULL, NULL, 0)) == paBadStreamPtr)); + HOPEFOR(((result = Pa_WriteStream(NULL, NULL, 0)) == paBadStreamPtr)); + + /** @todo test Pa_GetStreamReadAvailable and Pa_GetStreamWriteAvailable */ + + if (stream != NULL) Pa_CloseStream(stream); + return result; +} + +/*---------------------------------------------------------------------*/ +int main(void); +int main(void) +{ + PaError result; + + EXPECT(((result = Pa_Initialize()) == paNoError)); + TestBadOpens(); + TestBadActions(); +error: + Pa_Terminate(); + printf("QA Report: %d passed, %d failed.\n", gNumPassed, gNumFailed); + return 0; +} diff --git a/portaudio/qa/paqa_latency.c b/portaudio/qa/paqa_latency.c new file mode 100644 index 0000000..a70807b --- /dev/null +++ b/portaudio/qa/paqa_latency.c @@ -0,0 +1,482 @@ +/** @file paqa_latency.c + @ingroup qa_src + @brief Test latency estimates. + @author Ross Bencina <rossb@audiomulch.com> + @author Phil Burk <philburk@softsynth.com> +*/ +/* + * $Id: patest_sine.c 1368 2008-03-01 00:38:27Z rossb $ + * + * This program uses the PortAudio Portable Audio Library. + * For more information see: http://www.portaudio.com/ + * Copyright (c) 1999-2000 Ross Bencina and Phil Burk + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The text above constitutes the entire PortAudio license; however, + * the PortAudio community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ +#include <stdio.h> +#include <math.h> +#include "portaudio.h" +#include "loopback/src/qa_tools.h" + +#define NUM_SECONDS (5) +#define SAMPLE_RATE (44100) +#define FRAMES_PER_BUFFER (64) + +#ifndef M_PI +#define M_PI (3.14159265) +#endif + +#define TABLE_SIZE (200) +typedef struct +{ + float sine[TABLE_SIZE]; + int left_phase; + int right_phase; + char message[20]; + int minFramesPerBuffer; + int maxFramesPerBuffer; + int callbackCount; + PaTime minDeltaDacTime; + PaTime maxDeltaDacTime; + PaStreamCallbackTimeInfo previousTimeInfo; +} +paTestData; + +/* Used to tally the results of the QA tests. */ +int g_testsPassed = 0; +int g_testsFailed = 0; + +/* This routine will be called by the PortAudio engine when audio is needed. +** It may called at interrupt level on some machines so don't do anything +** that could mess up the system like calling malloc() or free(). +*/ +static int patestCallback( const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData ) +{ + paTestData *data = (paTestData*)userData; + float *out = (float*)outputBuffer; + unsigned long i; + + (void) timeInfo; /* Prevent unused variable warnings. */ + (void) statusFlags; + (void) inputBuffer; + + if( data->minFramesPerBuffer > framesPerBuffer ) + { + data->minFramesPerBuffer = framesPerBuffer; + } + if( data->maxFramesPerBuffer < framesPerBuffer ) + { + data->maxFramesPerBuffer = framesPerBuffer; + } + + /* Measure min and max output time stamp delta. */ + if( data->callbackCount > 0 ) + { + PaTime delta = timeInfo->outputBufferDacTime - data->previousTimeInfo.outputBufferDacTime; + if( data->minDeltaDacTime > delta ) + { + data->minDeltaDacTime = delta; + } + if( data->maxDeltaDacTime < delta ) + { + data->maxDeltaDacTime = delta; + } + } + data->previousTimeInfo = *timeInfo; + + for( i=0; i<framesPerBuffer; i++ ) + { + *out++ = data->sine[data->left_phase]; /* left */ + *out++ = data->sine[data->right_phase]; /* right */ + data->left_phase += 1; + if( data->left_phase >= TABLE_SIZE ) data->left_phase -= TABLE_SIZE; + data->right_phase += 3; /* higher pitch so we can distinguish left and right. */ + if( data->right_phase >= TABLE_SIZE ) data->right_phase -= TABLE_SIZE; + } + + data->callbackCount += 1; + return paContinue; +} + +PaError paqaCheckLatency( PaStreamParameters *outputParamsPtr, + paTestData *dataPtr, double sampleRate, unsigned long framesPerBuffer ) +{ + PaError err; + PaStream *stream; + const PaStreamInfo* streamInfo; + + dataPtr->minFramesPerBuffer = 9999999; + dataPtr->maxFramesPerBuffer = 0; + dataPtr->minDeltaDacTime = 9999999.0; + dataPtr->maxDeltaDacTime = 0.0; + dataPtr->callbackCount = 0; + + printf("Stream parameter: suggestedOutputLatency = %g\n", outputParamsPtr->suggestedLatency ); + if( framesPerBuffer == paFramesPerBufferUnspecified ){ + printf("Stream parameter: user framesPerBuffer = paFramesPerBufferUnspecified\n" ); + }else{ + printf("Stream parameter: user framesPerBuffer = %lu\n", framesPerBuffer ); + } + err = Pa_OpenStream( + &stream, + NULL, /* no input */ + outputParamsPtr, + sampleRate, + framesPerBuffer, + paClipOff, /* we won't output out of range samples so don't bother clipping them */ + patestCallback, + dataPtr ); + if( err != paNoError ) goto error1; + + streamInfo = Pa_GetStreamInfo( stream ); + printf("Stream info: inputLatency = %g\n", streamInfo->inputLatency ); + printf("Stream info: outputLatency = %g\n", streamInfo->outputLatency ); + + err = Pa_StartStream( stream ); + if( err != paNoError ) goto error2; + + printf("Play for %d seconds.\n", NUM_SECONDS ); + Pa_Sleep( NUM_SECONDS * 1000 ); + + printf(" minFramesPerBuffer = %4d\n", dataPtr->minFramesPerBuffer ); + printf(" maxFramesPerBuffer = %4d\n", dataPtr->maxFramesPerBuffer ); + printf(" minDeltaDacTime = %f\n", dataPtr->minDeltaDacTime ); + printf(" maxDeltaDacTime = %f\n", dataPtr->maxDeltaDacTime ); + + err = Pa_StopStream( stream ); + if( err != paNoError ) goto error2; + + err = Pa_CloseStream( stream ); + Pa_Sleep( 1 * 1000 ); + + + printf("-------------------------------------\n"); + return err; +error2: + Pa_CloseStream( stream ); +error1: + printf("-------------------------------------\n"); + return err; +} + + +/*******************************************************************/ +static int paqaNoopCallback( const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData ) +{ + (void)inputBuffer; + (void)outputBuffer; + (void)framesPerBuffer; + (void)timeInfo; + (void)statusFlags; + (void)userData; + return paContinue; +} + +/*******************************************************************/ +static int paqaCheckMultipleSuggested( PaDeviceIndex deviceIndex, int isInput ) +{ + int i; + int numLoops = 10; + PaError err; + PaStream *stream; + PaStreamParameters streamParameters; + const PaStreamInfo* streamInfo; + double lowLatency; + double highLatency; + double finalLatency; + double sampleRate = SAMPLE_RATE; + const PaDeviceInfo *pdi = Pa_GetDeviceInfo( deviceIndex ); + double previousLatency = 0.0; + int numChannels = 1; + float toleranceRatio = 1.0; + + printf("------------------------ paqaCheckMultipleSuggested - %s\n", + (isInput ? "INPUT" : "OUTPUT") ); + if( isInput ) + { + lowLatency = pdi->defaultLowInputLatency; + highLatency = pdi->defaultHighInputLatency; + numChannels = (pdi->maxInputChannels < 2) ? 1 : 2; + } + else + { + lowLatency = pdi->defaultLowOutputLatency; + highLatency = pdi->defaultHighOutputLatency; + numChannels = (pdi->maxOutputChannels < 2) ? 1 : 2; + } + streamParameters.channelCount = numChannels; + streamParameters.device = deviceIndex; + streamParameters.hostApiSpecificStreamInfo = NULL; + streamParameters.sampleFormat = paFloat32; + sampleRate = pdi->defaultSampleRate; + + printf(" lowLatency = %g\n", lowLatency ); + printf(" highLatency = %g\n", highLatency ); + printf(" numChannels = %d\n", numChannels ); + printf(" sampleRate = %g\n", sampleRate ); + + if( (highLatency - lowLatency) < 0.001 ) + { + numLoops = 1; + } + + for( i=0; i<numLoops; i++ ) + { + if( numLoops == 1 ) + streamParameters.suggestedLatency = lowLatency; + else + streamParameters.suggestedLatency = lowLatency + ((highLatency - lowLatency) * i /(numLoops - 1)); + + printf(" suggestedLatency[%d] = %6.4f\n", i, streamParameters.suggestedLatency ); + + err = Pa_OpenStream( + &stream, + (isInput ? &streamParameters : NULL), + (isInput ? NULL : &streamParameters), + sampleRate, + paFramesPerBufferUnspecified, + paClipOff, /* we won't output out of range samples so don't bother clipping them */ + paqaNoopCallback, + NULL ); + if( err != paNoError ) goto error; + + streamInfo = Pa_GetStreamInfo( stream ); + + err = Pa_CloseStream( stream ); + + if( isInput ) + { + finalLatency = streamInfo->inputLatency; + } + else + { + finalLatency = streamInfo->outputLatency; + } + printf(" finalLatency = %6.4f\n", finalLatency ); + /* For the default low & high latency values, expect quite close; for other requested + * values, at worst the next power-of-2 may result (eg 513 -> 1024) */ + toleranceRatio = ( (i == 0) || (i == ( numLoops - 1 )) ) ? 0.1 : 1.0; + QA_ASSERT_CLOSE( "final latency should be close to suggested latency", + streamParameters.suggestedLatency, finalLatency, (streamParameters.suggestedLatency * toleranceRatio) ); + if( i == 0 ) + { + previousLatency = finalLatency; + } + } + + if( numLoops > 1 ) + { + QA_ASSERT_TRUE( " final latency should increase with suggested latency", (finalLatency > previousLatency) ); + } + + return 0; +error: + return -1; +} + +/*******************************************************************/ +static int paqaVerifySuggestedLatency( void ) +{ + PaDeviceIndex id; + int result = 0; + const PaDeviceInfo *pdi; + int numDevices = Pa_GetDeviceCount(); + + printf("\n ------------------------ paqaVerifySuggestedLatency\n"); + for( id=0; id<numDevices; id++ ) /* Iterate through all devices. */ + { + pdi = Pa_GetDeviceInfo( id ); + printf("\nUsing device #%d: '%s' (%s)\n", id, pdi->name, Pa_GetHostApiInfo(pdi->hostApi)->name); + if( pdi->maxOutputChannels > 0 ) + { + if( paqaCheckMultipleSuggested( id, 0 ) < 0 ) + { + printf("OUTPUT CHECK FAILED !!! #%d: '%s'\n", id, pdi->name); + result -= 1; + } + } + if( pdi->maxInputChannels > 0 ) + { + if( paqaCheckMultipleSuggested( id, 1 ) < 0 ) + { + printf("INPUT CHECK FAILED !!! #%d: '%s'\n", id, pdi->name); + result -= 1; + } + } + } + return result; +} + +/*******************************************************************/ +static int paqaVerifyDeviceInfoLatency( void ) +{ + PaDeviceIndex id; + const PaDeviceInfo *pdi; + int numDevices = Pa_GetDeviceCount(); + + printf("\n ------------------------ paqaVerifyDeviceInfoLatency\n"); + for( id=0; id<numDevices; id++ ) /* Iterate through all devices. */ + { + pdi = Pa_GetDeviceInfo( id ); + printf("Using device #%d: '%s' (%s)\n", id, pdi->name, Pa_GetHostApiInfo(pdi->hostApi)->name); + if( pdi->maxOutputChannels > 0 ) + { + printf(" Output defaultLowOutputLatency = %f seconds\n", pdi->defaultLowOutputLatency); + printf(" Output defaultHighOutputLatency = %f seconds\n", pdi->defaultHighOutputLatency); + QA_ASSERT_TRUE( "defaultLowOutputLatency should be > 0", (pdi->defaultLowOutputLatency > 0.0) ); + QA_ASSERT_TRUE( "defaultHighOutputLatency should be > 0", (pdi->defaultHighOutputLatency > 0.0) ); + QA_ASSERT_TRUE( "defaultHighOutputLatency should be >= Low", (pdi->defaultHighOutputLatency >= pdi->defaultLowOutputLatency) ); + } + if( pdi->maxInputChannels > 0 ) + { + printf(" Input defaultLowInputLatency = %f seconds\n", pdi->defaultLowInputLatency); + printf(" Input defaultHighInputLatency = %f seconds\n", pdi->defaultHighInputLatency); + QA_ASSERT_TRUE( "defaultLowInputLatency should be > 0", (pdi->defaultLowInputLatency > 0.0) ); + QA_ASSERT_TRUE( "defaultHighInputLatency should be > 0", (pdi->defaultHighInputLatency > 0.0) ); + QA_ASSERT_TRUE( "defaultHighInputLatency should be >= Low", (pdi->defaultHighInputLatency >= pdi->defaultLowInputLatency) ); + } + } + return 0; +error: + return -1; +} + + + +/*******************************************************************/ +int main(void); +int main(void) +{ + PaStreamParameters outputParameters; + PaError err; + paTestData data; + const PaDeviceInfo *deviceInfo; + int i; + int framesPerBuffer; + double sampleRate = SAMPLE_RATE; + + printf("\nPortAudio QA: investigate output latency.\n"); + + /* initialise sinusoidal wavetable */ + for( i=0; i<TABLE_SIZE; i++ ) + { + data.sine[i] = (float) sin( ((double)i/(double)TABLE_SIZE) * M_PI * 2. ); + } + data.left_phase = data.right_phase = 0; + + err = Pa_Initialize(); + if( err != paNoError ) goto error; + + /* Run self tests. */ + if( paqaVerifyDeviceInfoLatency() < 0 ) goto error; + + if( paqaVerifySuggestedLatency() < 0 ) goto error; + + outputParameters.device = Pa_GetDefaultOutputDevice(); /* default output device */ + if (outputParameters.device == paNoDevice) { + fprintf(stderr,"Error: No default output device.\n"); + goto error; + } + + printf("\n\nNow running Audio Output Tests...\n"); + printf("-------------------------------------\n"); + + outputParameters.channelCount = 2; /* stereo output */ + outputParameters.sampleFormat = paFloat32; /* 32 bit floating point output */ + deviceInfo = Pa_GetDeviceInfo( outputParameters.device ); + printf("Using device #%d: '%s' (%s)\n", outputParameters.device, deviceInfo->name, Pa_GetHostApiInfo(deviceInfo->hostApi)->name); + printf("Device info: defaultLowOutputLatency = %f seconds\n", deviceInfo->defaultLowOutputLatency); + printf("Device info: defaultHighOutputLatency = %f seconds\n", deviceInfo->defaultHighOutputLatency); + sampleRate = deviceInfo->defaultSampleRate; + printf("Sample Rate for following tests: %g\n", sampleRate); + outputParameters.hostApiSpecificStreamInfo = NULL; + printf("-------------------------------------\n"); + + // Try to use a small buffer that is smaller than we think the device can handle. + // Try to force combining multiple user buffers into a host buffer. + printf("------------- Try a very small buffer.\n"); + framesPerBuffer = 9; + outputParameters.suggestedLatency = deviceInfo->defaultLowOutputLatency; + err = paqaCheckLatency( &outputParameters, &data, sampleRate, framesPerBuffer ); + if( err != paNoError ) goto error; + + printf("------------- 64 frame buffer with 1.1 * defaultLow latency.\n"); + framesPerBuffer = 64; + outputParameters.suggestedLatency = deviceInfo->defaultLowOutputLatency * 1.1; + err = paqaCheckLatency( &outputParameters, &data, sampleRate, framesPerBuffer ); + if( err != paNoError ) goto error; + + // Try to create a huge buffer that is bigger than the allowed device maximum. + printf("------------- Try a huge buffer.\n"); + framesPerBuffer = 16*1024; + outputParameters.suggestedLatency = ((double)framesPerBuffer) / sampleRate; // approximate + err = paqaCheckLatency( &outputParameters, &data, sampleRate, framesPerBuffer ); + if( err != paNoError ) goto error; + + printf("------------- Try suggestedLatency = 0.0\n"); + outputParameters.suggestedLatency = 0.0; + err = paqaCheckLatency( &outputParameters, &data, sampleRate, paFramesPerBufferUnspecified ); + if( err != paNoError ) goto error; + + printf("------------- Try suggestedLatency = defaultLowOutputLatency\n"); + outputParameters.suggestedLatency = deviceInfo->defaultLowOutputLatency; + err = paqaCheckLatency( &outputParameters, &data, sampleRate, paFramesPerBufferUnspecified ); + if( err != paNoError ) goto error; + + printf("------------- Try suggestedLatency = defaultHighOutputLatency\n"); + outputParameters.suggestedLatency = deviceInfo->defaultHighOutputLatency; + err = paqaCheckLatency( &outputParameters, &data, sampleRate, paFramesPerBufferUnspecified ); + if( err != paNoError ) goto error; + + printf("------------- Try suggestedLatency = defaultHighOutputLatency * 4\n"); + outputParameters.suggestedLatency = deviceInfo->defaultHighOutputLatency * 4; + err = paqaCheckLatency( &outputParameters, &data, sampleRate, paFramesPerBufferUnspecified ); + if( err != paNoError ) goto error; + + Pa_Terminate(); + printf("SUCCESS - test finished.\n"); + return err; + +error: + Pa_Terminate(); + fprintf( stderr, "ERROR - test failed.\n" ); + fprintf( stderr, "Error number: %d\n", err ); + fprintf( stderr, "Error message: %s\n", Pa_GetErrorText( err ) ); + return err; +} |