]> Repos - portaudio/commitdiff
First checkin of new loopback unit test for PortAudio.
authorphilburk <philburk@0f58301d-fd10-0410-b4af-bbb618454e57>
Fri, 12 Nov 2010 19:14:25 +0000 (19:14 +0000)
committerphilburk <philburk@0f58301d-fd10-0410-b4af-bbb618454e57>
Fri, 12 Nov 2010 19:14:25 +0000 (19:14 +0000)
13 files changed:
qa/loopback/README.txt [new file with mode: 0644]
qa/loopback/src/audio_analyzer.c [new file with mode: 0644]
qa/loopback/src/audio_analyzer.h [new file with mode: 0644]
qa/loopback/src/biquad_filter.c [new file with mode: 0755]
qa/loopback/src/biquad_filter.h [new file with mode: 0755]
qa/loopback/src/paqa.c [new file with mode: 0644]
qa/loopback/src/paqa_tools.c [new file with mode: 0644]
qa/loopback/src/paqa_tools.h [new file with mode: 0644]
qa/loopback/src/qa_tools.h [new file with mode: 0755]
qa/loopback/src/test_audio_analyzer.c [new file with mode: 0644]
qa/loopback/src/test_audio_analyzer.h [new file with mode: 0644]
qa/loopback/src/write_wav.c [new file with mode: 0755]
qa/loopback/src/write_wav.h [new file with mode: 0755]

diff --git a/qa/loopback/README.txt b/qa/loopback/README.txt
new file mode 100644 (file)
index 0000000..5ce8f29
--- /dev/null
@@ -0,0 +1,83 @@
+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 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/qa/loopback/src/audio_analyzer.c b/qa/loopback/src/audio_analyzer.c
new file mode 100644 (file)
index 0000000..1cddaab
--- /dev/null
@@ -0,0 +1,586 @@
+
+/*
+ * 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"
+
+
+/*==========================================================================================*/
+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 numSamples = numFrames * samplesPerFrame;
+       for( int 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 )
+{
+       for( int 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 offset = numSamples/2;
+       for( int 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 = 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 framesToWrite = numFrames;
+       if ((framesToWrite + recording->numFrames) > recording->maxFrames)
+       {
+               framesToWrite = recording->maxFrames - recording->numFrames;
+       }
+       float *data = &recording->buffer[recording->numFrames];
+       for( int i=0; i<framesToWrite; i++ )
+       {
+               *data++ = *buffer;
+               buffer += stride;
+       }
+       recording->numFrames += framesToWrite;
+       return (recording->numFrames >= recording->maxFrames);
+       
+error:
+       return -1;
+}
+
+/*==========================================================================================*/
+int PaQa_WriteSilence( PaQaRecording *recording, int numFrames )
+{
+       int framesToRecord = numFrames;
+       if ((framesToRecord + recording->numFrames) > recording->maxFrames)
+       {
+               framesToRecord = recording->maxFrames - recording->numFrames;
+       }
+       float *data = &recording->buffer[recording->numFrames];
+       for( int i=0; i<framesToRecord; i++ )
+       {
+               *data++ = 0.0f;
+       }
+       recording->numFrames += framesToRecord;
+       return (recording->numFrames >= recording->maxFrames);
+       
+error:
+       return -1;
+}
+
+/*==========================================================================================*/
+int PaQa_RecordFreeze( PaQaRecording *recording, int numFrames )
+{
+       int framesToRecord = numFrames;
+       if ((framesToRecord + recording->numFrames) > recording->maxFrames)
+       {
+               framesToRecord = recording->maxFrames - recording->numFrames;
+       }
+       float *data = &recording->buffer[recording->numFrames];
+       for( int 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);
+       
+error:
+       return -1;
+}
+
+/*==========================================================================================*/
+/**
+ * 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;
+    result =  Audio_WAV_OpenWriter( &writer, filename, recording->sampleRate, samplesPerFrame );
+    if( result < 0 ) goto error;
+       
+       int numLeft = recording->numFrames;
+       float *buffer = &recording->buffer[0];
+
+       while( numLeft > 0 )
+    {
+        int numToSave = (numLeft > NUM_SAMPLES) ? NUM_SAMPLES : numLeft;
+               // Convert double samples to shorts.
+               for( int 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_CorrelateSine( PaQaRecording *recording, double frequency, double frameRate,
+                                                 int startFrame, int numFrames, double *phasePtr )
+{
+       double magnitude = 0.0;
+       QA_ASSERT_TRUE( "startFrame out of bounds", (startFrame < recording->numFrames) );
+       QA_ASSERT_TRUE( "numFrames out of bounds", ((startFrame+numFrames) <= recording->numFrames) );
+       
+       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];
+       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 that goes over the tolerance level,
+// peaks then drops to half the peak.
+double PaQa_FindFirstMatch( PaQaRecording *recording, float *buffer, int numFrames, double tolerance  )
+{
+       QA_ASSERT_TRUE( "numFrames out of bounds", (numFrames < recording->numFrames) );
+       // How many buffers will fit in the recording?
+       int maxCorrelations = recording->numFrames - numFrames;
+       double maxSum = 0.0;
+       int peakIndex = -1;
+       double location = -1.0;
+       for( int ic=0; ic<maxCorrelations; ic++ )
+       {
+               double sum = 0.0;
+               // Correlate buffer against the recording.
+               float *recorded = &recording->buffer[ ic ];
+               for( int is=0; is<numFrames; is++ )
+               {
+                       float s1 = buffer[is];
+                       float s2 = *recorded++;
+                       sum += s1 * s2;
+               }
+               if( (sum > maxSum) )
+               {
+                       maxSum = sum;
+                       peakIndex = ic;
+               }
+               if( (maxSum > tolerance) && (sum < 0.5*maxSum) )
+               {
+                       location = peakIndex;
+                       break;
+               }
+               
+       }
+       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  )
+{
+       double area = 0.0;
+       for( int is=0; is<numFrames; is++ )
+       {
+               area += fabs( *buffer );
+               buffer += stride;
+       }
+       return area;
+}
+
+
+/*==========================================================================================*/
+// 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 )
+{
+       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 );
+       
+       // 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 );
+       
+       double tolerance = 0.1;
+       analysisResult->latency = PaQa_FindFirstMatch( recording, buffer, cycleSize, tolerance );       
+       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 )
+{
+       assert( startFrame >= 0 );
+       assert( count > 0 );
+       double phase = 0.5 * MATH_PI;
+       // Advance a quarter wave
+       double phaseIncrement = 0.25 * 2.0 * MATH_PI / count;
+       
+       for( int is=0; is<count; is++ )
+       {
+               double c = cos( phase );
+               phase += phaseIncrement;
+               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;
+       }
+}
+
+
+/*==========================================================================================*/
+/** Notch filter and high pass filter then detect remaining energy.
+ */
+int PaQa_DetectPop( PaQaRecording *recording, PaQaTestTone *testTone, PaQaAnalysisResult *analysisResult )
+{      
+       PaQaRecording     notchOutput = { 0 };
+       BiquadFilter      notchFilter;
+       
+       PaQaRecording     hipassOutput = { 0 };
+       BiquadFilter      hipassFilter;
+       
+       int frameRate = (int) recording->sampleRate;
+       
+       analysisResult->popPosition = -1;
+       analysisResult->popAmplitude = 0.0;
+       
+       int result = PaQa_InitializeRecording( &notchOutput, 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( &notchFilter, testTone->frequency / frameRate, 0.5 );
+       PaQa_FilterRecording( recording, &notchOutput, &notchFilter );
+       //result = PaQa_SaveRecordingToWaveFile( &notchOutput, "notch_output.wav" );
+       //QA_ASSERT_EQUALS( "PaQa_SaveRecordingToWaveFile failed", 0, result );
+       
+       // Apply fade-in window.
+       PaQa_FadeInRecording( &notchOutput, (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( &notchOutput, &hipassOutput, &hipassFilter );
+       //result = PaQa_SaveRecordingToWaveFile( &hipassOutput, "hipass_output.wav" );
+       //QA_ASSERT_EQUALS( "PaQa_SaveRecordingToWaveFile failed", 0, result );
+       
+       // Scan remaining signal looking for peak.
+       double maxAmplitude = 0.0;
+       int maxPosition = -1;
+       for( int i=0; i<hipassOutput.numFrames; i++ )
+       {
+               float x = hipassOutput.buffer[i];
+               float mag = fabs( x );
+               if( mag > maxAmplitude )
+               {
+                       maxAmplitude = mag;
+                       maxPosition = i;
+               }
+       }
+       
+       if( maxAmplitude > 0.01 )
+       {
+               analysisResult->popPosition = maxPosition;
+               analysisResult->popAmplitude = maxAmplitude;
+       }
+       
+       PaQa_TerminateRecording( &notchOutput );
+       PaQa_TerminateRecording( &hipassOutput );
+       return 0;
+       
+error:
+       PaQa_TerminateRecording( &notchOutput );
+       PaQa_TerminateRecording( &hipassOutput );
+       return -1;
+}
+
+/*==========================================================================================*/
+int PaQa_DetectPhaseError( PaQaRecording *recording, PaQaTestTone *testTone, PaQaAnalysisResult *analysisResult )
+{              
+       double period = testTone->sampleRate / testTone->frequency;
+       int cycleSize = (int) (period + 0.5);
+       
+       // 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;
+       
+       double maxAddedFrames = 0.0;
+       double maxDroppedFrames = 0.0;
+       
+       double previousPhase = 0.0;
+       double previousFrameError = 0;
+       int loopCount = 0;
+       int skip = cycleSize;
+       int windowSize = cycleSize;
+       
+       for( int 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)
+               {
+                       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 )
+{
+       memset( analysisResult, 0, sizeof(PaQaAnalysisResult) );
+       
+       int 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/qa/loopback/src/audio_analyzer.h b/qa/loopback/src/audio_analyzer.h
new file mode 100644 (file)
index 0000000..b3bec86
--- /dev/null
@@ -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.
+ */
+
+#ifndef _AUDIO_ANALYZER_H
+#define _AUDIO_ANALYZER_H
+
+#include "biquad_filter.h"
+
+#define MATH_PI  (3.14159265)
+#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;
+       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  );
+
+/**
+ * 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 containg 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  );
+
+/**
+ * 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/qa/loopback/src/biquad_filter.c b/qa/loopback/src/biquad_filter.c
new file mode 100755 (executable)
index 0000000..c0a87b7
--- /dev/null
@@ -0,0 +1,123 @@
+#include <math.h>\r
+#include <strings.h>\r
+\r
+#include "biquad_filter.h"\r
+\r
+/**\r
+ *     Unit_BiquadFilter implements a second order IIR filter. \r
+       Here is the equation that we use for this filter:\r
+     y(n) = a0*x(n) + a1*x(n-1)  + a2*x(n-2) - b1*y(n-1)  - b2*y(n-2)\r
+ *\r
+ * @author (C) 2002 Phil Burk, SoftSynth.com, All Rights Reserved\r
+ */\r
+\r
+#define FILTER_PI  (3.14159265)\r
+/***********************************************************\r
+** Calculate coefficients common to many parametric biquad filters.\r
+*/\r
+static void BiquadFilter_CalculateCommon( BiquadFilter *filter, double ratio, double Q )\r
+{\r
+       double omega;\r
+       \r
+       memset( filter, 0, sizeof(BiquadFilter) );\r
+\r
+/* Don't let frequency get too close to Nyquist or filter will blow up. */\r
+       if( ratio >= 0.499 ) ratio = 0.499; \r
+       omega = 2.0 * (double)FILTER_PI * ratio;\r
+\r
+       filter->cos_omega = (double) cos( omega );\r
+       filter->sin_omega = (double) sin( omega );\r
+       filter->alpha = filter->sin_omega / (2.0 * Q);\r
+}\r
+\r
+/*********************************************************************************\r
+ ** Calculate coefficients for Highpass filter.\r
+ */\r
+void BiquadFilter_SetupHighPass( BiquadFilter *filter, double ratio, double Q )\r
+{\r
+       double    scalar, opc;\r
+       \r
+       if( ratio  < BIQUAD_MIN_RATIO )  ratio  = BIQUAD_MIN_RATIO;\r
+       if( Q < BIQUAD_MIN_Q ) Q = BIQUAD_MIN_Q;\r
+       \r
+       BiquadFilter_CalculateCommon( filter, ratio, Q );\r
+       \r
+       scalar = 1.0 / (1.0 + filter->alpha);\r
+       opc = (1.0 + filter->cos_omega);\r
+       \r
+       filter->a0 = opc * 0.5 * scalar;\r
+       filter->a1 =  - opc * scalar;\r
+    filter->a2 = filter->a0;\r
+       filter->b1 = -2.0 * filter->cos_omega * scalar;\r
+       filter->b2 = (1.0 - filter->alpha) * scalar;\r
+}\r
+\r
+\r
+/*********************************************************************************\r
+ ** Calculate coefficients for Notch filter.\r
+ */\r
+void BiquadFilter_SetupNotch( BiquadFilter *filter, double ratio, double Q )\r
+{\r
+       double    scalar, opc;\r
+       \r
+       if( ratio  < BIQUAD_MIN_RATIO )  ratio  = BIQUAD_MIN_RATIO;\r
+       if( Q < BIQUAD_MIN_Q ) Q = BIQUAD_MIN_Q;\r
+       \r
+       BiquadFilter_CalculateCommon( filter, ratio, Q );\r
+       \r
+       scalar = 1.0 / (1.0 + filter->alpha);\r
+       opc = (1.0 + filter->cos_omega);\r
+       \r
+       filter->a0 = scalar;\r
+       filter->a1 =  -2.0 * filter->cos_omega * scalar;\r
+    filter->a2 = filter->a0;\r
+       filter->b1 = filter->a1;\r
+       filter->b2 = (1.0 - filter->alpha) * scalar;\r
+}\r
+\r
+/*****************************************************************\r
+** Perform core IIR filter calculation without permutation.\r
+*/\r
+void BiquadFilter_Filter( BiquadFilter *filter, float *inputs, float *outputs, int numSamples )\r
+{\r
+       int i;\r
+    double xn, yn;\r
+       // Pull values from structure to speed up the calculation.\r
+       double a0 = filter->a0;\r
+       double a1 = filter->a1;\r
+       double a2 = filter->a2;\r
+       double b1 = filter->b1;\r
+       double b2 = filter->b2;\r
+       double xn1 = filter->xn1;\r
+       double xn2 = filter->xn2;\r
+       double yn1 = filter->yn1;\r
+       double yn2 = filter->yn2;\r
+\r
+       for( i=0; i<numSamples; i++)\r
+       {\r
+               // Generate outputs by filtering inputs.\r
+               xn = inputs[i];\r
+               yn = (a0 * xn) + (a1 * xn1) + (a2 * xn2) - (b1 * yn1) - (b2 * yn2);\r
+               outputs[i] = yn;\r
+\r
+               // Delay input and output values.\r
+        xn2 = xn1;\r
+        xn1 = xn;\r
+        yn2 = yn1;\r
+        yn1 = yn;\r
+               \r
+               if( (i & 7) == 0 )\r
+               {\r
+                       // Apply a small bipolar impulse to filter to prevent arithmetic underflow.\r
+                       // Underflows can cause the FPU to interrupt the CPU.\r
+                       yn1 += (double) 1.0E-26;\r
+                       yn2 -= (double) 1.0E-26;\r
+               }\r
+       }\r
+       \r
+       filter->xn1 = xn1;\r
+       filter->xn2 = xn2;\r
+       filter->yn1 = yn1;\r
+       filter->yn2 = yn2;\r
+               \r
+}
\ No newline at end of file
diff --git a/qa/loopback/src/biquad_filter.h b/qa/loopback/src/biquad_filter.h
new file mode 100755 (executable)
index 0000000..2e0ab30
--- /dev/null
@@ -0,0 +1,38 @@
+#ifndef _BIQUADFILTER_H\r
+#define _BIQUADFILTER_H\r
+\r
+\r
+/**\r
+ *     Unit_BiquadFilter implements a second order IIR filter. \r
+ *\r
+ * @author (C) 2002 Phil Burk, SoftSynth.com, All Rights Reserved\r
+ */\r
+\r
+#define BIQUAD_MIN_RATIO     (0.000001)\r
+#define BIQUAD_MIN_Q         (0.00001)\r
+\r
+typedef struct BiquadFilter_s\r
+{\r
+    double      xn1;    // storage for delayed signals\r
+       double      xn2;\r
+       double      yn1;\r
+       double      yn2;\r
+\r
+       double      a0;    // coefficients\r
+       double      a1;\r
+       double      a2;\r
+\r
+       double      b1;\r
+       double      b2;\r
+\r
+       double      cos_omega;\r
+       double      sin_omega;\r
+       double      alpha;\r
+} BiquadFilter;\r
+\r
+void BiquadFilter_SetupHighPass( BiquadFilter *filter, double ratio, double Q );\r
+void BiquadFilter_SetupNotch( BiquadFilter *filter, double ratio, double Q );\r
+\r
+void BiquadFilter_Filter( BiquadFilter *filter, float *inputs, float *outputs, int numSamples );\r
+\r
+#endif\r
diff --git a/qa/loopback/src/paqa.c b/qa/loopback/src/paqa.c
new file mode 100644 (file)
index 0000000..3732b64
--- /dev/null
@@ -0,0 +1,671 @@
+
+/*
+ * 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 "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 LOOPBACK_DETECTION_DURATION_SECONDS  (0.5)
+
+/** 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;
+} 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];
+       int                callbackCount;
+       
+       TestParameters    *test;
+} LoopbackContext;
+
+typedef struct UserOptions_s
+{
+       int           sampleRate;
+       int           framesPerBuffer;
+       int           latency;
+       int           saveBadWaves;
+       int           verbose;
+       int           waveFileCount;
+       const char   *waveFilePath;
+       PaDeviceIndex inputDevice;
+       PaDeviceIndex outputDevice;
+} UserOptions;
+
+/*******************************************************************/
+static int RecordAndPlaySinesCallback( const void *inputBuffer, void *outputBuffer,
+                                               unsigned long framesPerBuffer,
+                                               const PaStreamCallbackTimeInfo* timeInfo,
+                                               PaStreamCallbackFlags statusFlags,
+                                               void *userData )
+{
+    float *in = (float *)inputBuffer;
+    float *out = (float *)outputBuffer;
+    int done = 0;
+       
+       LoopbackContext *loopbackContext = (LoopbackContext *) userData;        
+       loopbackContext->callbackCount += 1;
+       
+    /* This may get called with NULL inputBuffer during initial setup. */
+    if( in == NULL) return 0;  
+       
+       for( int i=0; i<loopbackContext->test->inputParameters.channelCount; i++ )
+       {
+               done |= PaQa_WriteRecording( &loopbackContext->recordings[i], in + i, framesPerBuffer, loopbackContext->test->inputParameters.channelCount );
+       }
+       
+       PaQa_EraseBuffer( out, framesPerBuffer, loopbackContext->test->outputParameters.channelCount );
+       
+       for( int i=0; i<loopbackContext->test->outputParameters.channelCount; i++ )
+       {
+               PaQa_MixSine( &loopbackContext->generators[i], out + i, framesPerBuffer, loopbackContext->test->outputParameters.channelCount );
+       }
+       
+    return done ? paComplete : paContinue;
+}
+
+/*******************************************************************/
+/** 
+ * Open an 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_RunLoopback( LoopbackContext *loopbackContext )
+{
+       PaStream *stream = NULL;
+       TestParameters *test = loopbackContext->test;
+       PaError 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;
+       
+       err = Pa_StartStream( stream );
+       if( err != paNoError ) goto error;
+       
+       // Wait for stream to finish.
+       while( Pa_IsStreamActive( stream ) )
+       {
+               Pa_Sleep(50);
+               //printf("loopback count = %d\n", loopbackContext->callbackCount );
+               //printf("recording position = %d\n", loopbackContext->recordings[0].numFrames );
+       }
+       
+       err = Pa_StopStream( stream );
+       if( err != paNoError ) goto error;
+
+       err = Pa_CloseStream( stream );
+       if( err != paNoError ) goto error;
+       
+       return 0;
+       
+error:
+       return err;     
+}
+
+/*******************************************************************/
+static int PaQa_SaveTestResultToWaveFile( UserOptions *userOptions, PaQaRecording *recording )
+{
+       if( userOptions->saveBadWaves )
+       {
+               char filename[256];
+               snprintf( filename, sizeof(filename), "%s/test_%d.wav", userOptions->waveFilePath, userOptions->waveFileCount++ );
+               printf( "\"%s\", ", filename );
+               return PaQa_SaveRecordingToWaveFile( recording, filename );
+       }
+       return 0;
+}
+
+/*******************************************************************/
+static int PaQa_SetupLoopbackContext( LoopbackContext *loopbackContextPtr, TestParameters *testParams )
+{
+       // Setup loopback context.
+       memset( loopbackContextPtr, 0, sizeof(LoopbackContext) );       
+       loopbackContextPtr->test = testParams;
+       for( int i=0; i<testParams->samplesPerFrame; i++ )
+       {
+               int err = PaQa_InitializeRecording( &loopbackContextPtr->recordings[i], testParams->maxFrames, testParams->sampleRate );
+               QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, err );
+       }
+       for( int i=0; i<testParams->samplesPerFrame; i++ )
+       {
+               PaQa_SetupSineGenerator( &loopbackContextPtr->generators[i], PaQa_GetNthFrequency( testParams->baseFrequency, i ),
+                                                               testParams->amplitude, testParams->sampleRate );
+       }
+       return 0;
+error:
+       return -1;
+}
+
+/*******************************************************************/
+static void PaQa_TeardownLoopbackContext( LoopbackContext *loopbackContextPtr )
+{
+
+       for( int i=0; i<loopbackContextPtr->test->samplesPerFrame; i++ )
+       {
+               PaQa_TerminateRecording( &loopbackContextPtr->recordings[i] );
+       }       
+}
+
+/*******************************************************************/
+static void PaQa_PrintShortErrorReport( PaQaAnalysisResult *analysisResultPtr, int channel )
+{
+       printf("#%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, double expectedAmplitude )
+{
+       LoopbackContext loopbackContext;
+       PaError err = paNoError;
+       PaQaTestTone testTone;
+       PaQaAnalysisResult analysisResult;
+       int numBadChannels = 0;
+       
+       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) );
+       
+       // Analyse recording to to detect glitches.
+       for( int 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 )
+               {
+                       printf("%7.1f | ", analysisResult.latency );
+               }
+               
+               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 SIGNAL, ", i );
+                       numBadChannels += 1;
+               }
+
+       }
+                       
+       PaQa_TeardownLoopbackContext( &loopbackContext );
+       if( numBadChannels > 0 )
+       {
+               g_testsFailed += 1;
+       }
+       return numBadChannels;  
+       
+error:
+       PaQa_TeardownLoopbackContext( &loopbackContext );
+       return 1;       
+}
+
+/*******************************************************************/
+static void PaQa_SetDefaultTestParameters( TestParameters *testParamsPtr, PaDeviceIndex inputDevice, PaDeviceIndex outputDevice )
+{
+       memset( testParamsPtr, 0, sizeof(TestParameters) );
+       testParamsPtr->inputParameters.device = inputDevice;
+       testParamsPtr->outputParameters.device = outputDevice;
+       testParamsPtr->inputParameters.sampleFormat = paFloat32;
+       testParamsPtr->outputParameters.sampleFormat = paFloat32;
+       testParamsPtr->samplesPerFrame = 2;
+       testParamsPtr->inputParameters.channelCount = testParamsPtr->samplesPerFrame;
+       testParamsPtr->outputParameters.channelCount = testParamsPtr->samplesPerFrame;
+       testParamsPtr->amplitude = 0.5;
+       testParamsPtr->sampleRate = 44100;
+       testParamsPtr->maxFrames = (int) (1.0 * testParamsPtr->sampleRate);
+       testParamsPtr->framesPerBuffer = 256;
+       testParamsPtr->baseFrequency = 200.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, double expectedAmplitude )
+{
+       int totalBadChannels = 0;
+       TestParameters testParams;
+    const   PaDeviceInfo *inputDeviceInfo;     
+    const   PaDeviceInfo *outputDeviceInfo;            
+       inputDeviceInfo = Pa_GetDeviceInfo( inputDevice );
+       outputDeviceInfo = Pa_GetDeviceInfo( outputDevice );
+       
+       printf( "=============== Analysing Loopback %d to %d ====================\n", outputDevice, inputDevice  );
+       printf( "    Devices: %s => %s\n", outputDeviceInfo->name, inputDeviceInfo->name);
+       
+       double sampleRates[] = { 8000.0, 11025.0, 16000.0, 22050.0, 32000.0, 44100.0, 48000.0, 96000.0 };
+//     double sampleRates[] = { 16000.0, 44100.0 };
+       int numRates = (sizeof(sampleRates)/sizeof(double));
+       
+       int framesPerBuffers[] = { 0, 16, 32, 40, 64, 100, 128, 512, 1024 };
+//     int framesPerBuffers[] = { 16, 64, 512 };
+       int numBufferSizes = (sizeof(framesPerBuffers)/sizeof(int));
+               
+       printf("|-sRate-|-buffer-|-latency-|-channel results--------------------|\n");
+       // Check to see if a specific value was requested.
+       if( userOptions->sampleRate > 0 )
+       {
+               sampleRates[0] = userOptions->sampleRate;
+               numRates = 1;
+       }
+       if( userOptions->framesPerBuffer > 0 )
+       {
+               framesPerBuffers[0] = userOptions->framesPerBuffer;
+               numBufferSizes = 1;
+       }
+       
+       PaQa_SetDefaultTestParameters( &testParams, inputDevice, outputDevice );
+       testParams.maxFrames = (int) (0.5 * testParams.sampleRate);     
+       
+       // Loop though combinations of audio parameters.
+       for( int iRate=0; iRate<numRates; iRate++ )
+       {
+               // SAMPLE RATE
+               testParams.sampleRate = sampleRates[iRate];
+               testParams.maxFrames = (int) (1.2 * testParams.sampleRate);
+               for( int iSize=0; iSize<numBufferSizes; iSize++ )
+               {       
+                       // BUFFER SIZE
+                       testParams.framesPerBuffer = framesPerBuffers[iSize];
+                       printf("| %5d | %6d | ", ((int)(testParams.sampleRate+0.5)), testParams.framesPerBuffer );
+                       fflush(stdout);
+                       
+                       int numBadChannels = PaQa_SingleLoopBackTest( userOptions, &testParams, expectedAmplitude );
+                       if( numBadChannels == 0 )
+                       {
+                               printf( "OK" );
+                       }
+                       totalBadChannels += numBadChannels;
+                       printf( "\n" );
+               }
+               printf( "\n" );
+       }
+       return totalBadChannels;
+}
+
+/*******************************************************************/
+/** 
+ * 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( PaDeviceIndex inputDevice, PaDeviceIndex outputDevice )
+{
+       TestParameters testParams;
+       LoopbackContext loopbackContext;
+    const   PaDeviceInfo *inputDeviceInfo;     
+    const   PaDeviceInfo *outputDeviceInfo;            
+       PaError err = paNoError;
+       double minAmplitude = 0.3;
+       
+       inputDeviceInfo = Pa_GetDeviceInfo( inputDevice );
+       if( inputDeviceInfo->maxInputChannels < 2 )
+       {
+               return 0;
+       }
+       outputDeviceInfo = Pa_GetDeviceInfo( outputDevice );
+       if( outputDeviceInfo->maxOutputChannels < 2 )
+       {
+               return 0;
+       }
+       
+       printf( "Look for loopback cable between \"%s\" => \"%s\"\n", outputDeviceInfo->name, inputDeviceInfo->name);
+       
+       PaQa_SetDefaultTestParameters( &testParams, inputDevice, outputDevice );
+       testParams.maxFrames = (int) (LOOPBACK_DETECTION_DURATION_SECONDS * testParams.sampleRate);     
+       
+       PaQa_SetupLoopbackContext( &loopbackContext, &testParams );
+                       
+       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.
+       int startFrame = testParams.maxFrames/2;
+       int numFrames = testParams.maxFrames/2;
+       double magLeft = PaQa_CorrelateSine( &loopbackContext.recordings[0],
+                                                                               loopbackContext.generators[0].frequency,
+                                                                               testParams.sampleRate,
+                                                                               startFrame, numFrames, NULL );
+       double magRight = PaQa_CorrelateSine( &loopbackContext.recordings[1],
+                                                                                loopbackContext.generators[1].frequency,
+                                                                                testParams.sampleRate,
+                                                                                startFrame, numFrames, NULL );
+       printf("   Amplitudes: left = %f, right = %f\n", magLeft, magRight );
+       int loopbackConnected = ((magLeft > minAmplitude) && (magRight > minAmplitude));
+       
+       // Check for backwards cable.
+       if( !loopbackConnected )
+       {
+               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 > 0.1) && (magRightReverse>minAmplitude))
+               {
+                       printf("WARNING - you seem to have the left and right channels swapped on the loopback cable!\n");
+               }
+       }
+       
+       
+       PaQa_TeardownLoopbackContext( &loopbackContext );
+       return loopbackConnected;       
+       
+error:
+       PaQa_TeardownLoopbackContext( &loopbackContext );
+       return err;     
+}
+
+/*******************************************************************/
+/**
+ * Scan every combination of output to input device.
+ * If a loopback is found the analyse the combination.
+ * The scan can be overriden using the -i and -o command line options.
+ */
+static int ScanForLoopback(UserOptions *userOptions)
+{
+       int  numLoopbacks = 0;
+    int  numDevices;
+    numDevices = Pa_GetDeviceCount();    
+       
+       double expectedAmplitude = 0.4;
+       
+       // If both devices are specified then just use that combination.
+       if ((userOptions->inputDevice >= 0) && (userOptions->outputDevice >= 0))
+       {
+               PaQa_AnalyzeLoopbackConnection( userOptions, userOptions->inputDevice, userOptions->outputDevice, expectedAmplitude );
+               numLoopbacks += 1;
+       }
+       else if (userOptions->inputDevice >= 0)
+       {
+               // Just scan for output.
+               for( PaDeviceIndex i=0; i<numDevices; i++ )
+               {                                       
+                       int loopbackConnected = PaQa_CheckForLoopBack( userOptions->inputDevice, i );
+                       if( loopbackConnected > 0 )
+                       {
+                               PaQa_AnalyzeLoopbackConnection( userOptions, userOptions->inputDevice, i, expectedAmplitude );
+                               numLoopbacks += 1;
+                       }
+               }
+       }
+       else if (userOptions->outputDevice >= 0)
+       {
+               // Just scan for input.
+               for( PaDeviceIndex i=0; i<numDevices; i++ )
+               {                                       
+                       int loopbackConnected = PaQa_CheckForLoopBack( i, userOptions->inputDevice );
+                       if( loopbackConnected > 0 )
+                       {
+                               PaQa_AnalyzeLoopbackConnection( userOptions, i, userOptions->inputDevice, expectedAmplitude );
+                               numLoopbacks += 1;
+                       }
+               }
+       }
+       else 
+       {       
+               // Scan both.
+               for( PaDeviceIndex i=0; i<numDevices; i++ )
+               {
+                       for( PaDeviceIndex j=0; j<numDevices; j++ )
+                       {
+                               int loopbackConnected = PaQa_CheckForLoopBack( i, j );
+                               if( loopbackConnected > 0 )
+                               {
+                                       PaQa_AnalyzeLoopbackConnection( userOptions, i, j, expectedAmplitude );
+                                       numLoopbacks += 1;
+                               }
+                       }
+               }
+       }
+       QA_ASSERT_TRUE( "No loopback cables found or volumes too low.", (numLoopbacks > 0) );
+       return numLoopbacks;
+       
+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 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");
+}
+
+/*******************************************************************/
+int main( int argc, char **argv )
+{
+       UserOptions userOptions;
+       
+       int result = 0;
+       int justMath = 0;
+       printf("PortAudio LoopBack Test built " __DATE__ " at " __TIME__ "\n");
+       
+       // Process arguments. Skip name of executable.
+       memset(&userOptions, 0, sizeof(userOptions));
+       userOptions.inputDevice = paNoDevice;
+       userOptions.outputDevice = paNoDevice;
+       userOptions.waveFilePath = ".";
+       
+       char *name = argv[0];
+       for( int i=1; i<argc; i++ )
+       {
+               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.latency = 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 'h':
+                                       usage( name );
+                                       return(0);
+                                       break;
+                               default:
+                                       printf("Illegal option: %s\n", arg);
+                                       usage( name );
+                                       break;
+                       }
+               }
+               else
+               {
+                       printf("Illegal argument: %s\n", arg);
+                       usage( name );
+               }
+       }
+               
+       result = PaQa_TestAnalyzer();
+       
+       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();
+               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/qa/loopback/src/paqa_tools.c b/qa/loopback/src/paqa_tools.c
new file mode 100644 (file)
index 0000000..fc98934
--- /dev/null
@@ -0,0 +1,56 @@
+
+/*
+ * 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 );
+       }
+}
diff --git a/qa/loopback/src/paqa_tools.h b/qa/loopback/src/paqa_tools.h
new file mode 100644 (file)
index 0000000..9990597
--- /dev/null
@@ -0,0 +1,48 @@
+
+/*
+ * 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);
+
+#endif /* _PAQA_TOOLS_H */
diff --git a/qa/loopback/src/qa_tools.h b/qa/loopback/src/qa_tools.h
new file mode 100755 (executable)
index 0000000..9a60d04
--- /dev/null
@@ -0,0 +1,74 @@
+\r
+/*\r
+ * PortAudio Portable Real-Time Audio Library\r
+ * Latest Version at: http://www.portaudio.com\r
+ *\r
+ * Copyright (c) 1999-2010 Phil Burk and Ross Bencina\r
+ *\r
+ * Permission is hereby granted, free of charge, to any person obtaining\r
+ * a copy of this software and associated documentation files\r
+ * (the "Software"), to deal in the Software without restriction,\r
+ * including without limitation the rights to use, copy, modify, merge,\r
+ * publish, distribute, sublicense, and/or sell copies of the Software,\r
+ * and to permit persons to whom the Software is furnished to do so,\r
+ * subject to the following conditions:\r
+ *\r
+ * The above copyright notice and this permission notice shall be\r
+ * included in all copies or substantial portions of the Software.\r
+ *\r
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\r
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\r
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\r
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR\r
+ * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF\r
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\r
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\r
+ */\r
+\r
+/*\r
+ * The text above constitutes the entire PortAudio license; however, \r
+ * the PortAudio community also makes the following non-binding requests:\r
+ *\r
+ * Any person wishing to distribute modifications to the Software is\r
+ * requested to send the modifications to the original developer so that\r
+ * they can be incorporated into the canonical version. It is also \r
+ * requested that these non-binding requests be included along with the \r
+ * license above.\r
+ */\r
+\r
+#ifndef _QA_TOOLS_H\r
+#define _QA_TOOLS_H\r
+\r
+extern int g_testsPassed;\r
+extern int g_testsFailed;\r
+\r
+#define QA_ASSERT_TRUE( message, flag ) \\r
+       if( !(flag) ) \\r
+       { \\r
+               printf( "%s:%d - ERROR - %s\n", __FILE__, __LINE__, message ); \\r
+               g_testsFailed++; \\r
+               goto error; \\r
+       } \\r
+       else g_testsPassed++;\r
+\r
+\r
+#define QA_ASSERT_EQUALS( message, expected, actual ) \\r
+       if( (expected) != (actual) ) \\r
+       { \\r
+               printf( "%s:%d - ERROR - %s, expected %d, got %d\n", __FILE__, __LINE__, message, expected, actual ); \\r
+               g_testsFailed++; \\r
+               goto error; \\r
+       } \\r
+       else g_testsPassed++;\r
+\r
+#define QA_ASSERT_CLOSE( message, expected, actual, tolerance ) \\r
+       if (fabs((expected)-(actual))>(tolerance)) \\r
+       { \\r
+               printf( "%s:%d - ERROR - %s, expected %f, got %f, tol=%f\n", __FILE__, __LINE__, message, ((double)(expected)), ((double)(actual)), ((double)(tolerance)) ); \\r
+               g_testsFailed++; \\r
+               goto error; \\r
+       } \\r
+       else g_testsPassed++;\r
+\r
+\r
+#endif\r
diff --git a/qa/loopback/src/test_audio_analyzer.c b/qa/loopback/src/test_audio_analyzer.c
new file mode 100644 (file)
index 0000000..08a4fca
--- /dev/null
@@ -0,0 +1,583 @@
+
+/*
+ * 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
+
+/*==========================================================================================*/
+/**
+ * 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;
+       
+       double freq = 234.5;
+       double amp = 0.5;
+       
+       // 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 );
+       
+       int stride = 1;
+       int 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 );
+       }
+       
+       double mag1 = PaQa_CorrelateSine( &recording, freq, sampleRate, 0, recording.numFrames, NULL );
+       QA_ASSERT_CLOSE( "exact frequency match", amp, mag1, 0.01 );
+       
+       double 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 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;
+       
+       // Setup a sine oscillator.
+       for( int 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 );
+       
+       int stride = samplesPerFrame;
+       int done = 0;
+       while (!done)
+       {
+               PaQa_EraseBuffer( buffer, FRAMES_PER_BLOCK, samplesPerFrame );
+               for( int 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( int 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 );
+       }
+                       
+       double 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];
+       
+       // Setup a sine oscillator.
+       PaQa_SetupSineGenerator( &generator, testTone->frequency, testTone->amplitude, testTone->sampleRate );
+       
+       int stride = 1;
+       // Record some initial silence.
+       int done = PaQa_WriteSilence( recording, testTone->startDelay );
+       
+       int frameCounter = testTone->startDelay;
+       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 recording with pop.
+ */
+
+static void MakeRecordingWithPop( PaQaRecording *recording, PaQaTestTone *testTone, int popPosition, int popWidth, double popAmplitude )
+{
+       
+       PaQaSineGenerator generator;
+#define BUFFER_SIZE 512
+       float buffer[BUFFER_SIZE];
+       
+       // Setup a sine oscillator.
+       PaQa_SetupSineGenerator( &generator, testTone->frequency, testTone->amplitude, testTone->sampleRate );
+       
+       int stride = 1;
+       // Record some initial silence.
+       int done = PaQa_WriteSilence( recording, testTone->startDelay );
+       
+       // 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 );
+       }
+       
+       // Apply glitch to good recording.
+       if( (popPosition + popWidth) >= recording->numFrames )
+       {
+               popWidth = (recording->numFrames - popPosition) - 1;
+       }
+       
+       for( int 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 };
+       
+       testTone.samplesPerFrame = 1;
+       testTone.sampleRate = sampleRate;
+       testTone.frequency = sampleRate / cycleSize;
+       testTone.amplitude = 0.5;
+       testTone.startDelay = latencyFrames;
+       
+       int maxFrames = ((int)testTone.sampleRate) * 2;
+       
+       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 );
+       
+       int framesDropped = 0;
+       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, 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 };
+       
+       testTone.samplesPerFrame = 1;
+       testTone.sampleRate = sampleRate;
+       testTone.frequency = sampleRate / cycleSize;
+       testTone.amplitude = 0.5;
+       testTone.startDelay = latencyFrames;
+       
+       int maxFrames = ((int)testTone.sampleRate) * 2;
+       
+       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", (double)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;
+}      
+
+/*==========================================================================================*/
+/**
+ * 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;
+}
+
+
+/*==========================================================================================*/
+/**
+ * Simple test that write 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;
+}
+
+/*==========================================================================================*/
+/**
+ * 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;
+       
+       // Setup a sine oscillator.
+       PaQa_SetupSineGenerator( &generator, freq, amp, sampleRate );
+               
+       int stride = 1;
+       int 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;
+       
+       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 ); 
+
+       double 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( &notchFilter, freq / sampleRate, 0.5 );
+       PaQa_FilterRecording( &original, &filtered, &notchFilter );
+       result = PaQa_SaveRecordingToWaveFile( &filtered, "filtered1.wav" );
+       QA_ASSERT_EQUALS( "PaQa_SaveRecordingToWaveFile failed", 0, result );
+       
+       double 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( &notchFilter, 1.07 * freq / sampleRate, 2.0 );
+       PaQa_FilterRecording( &original, &filtered, &notchFilter );
+       
+       //result = PaQa_SaveRecordingToWaveFile( &filtered, "badfiltered.wav" );
+       //QA_ASSERT_EQUALS( "PaQa_SaveRecordingToWaveFile failed", 0, result );
+       
+       double 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;
+               
+       
+       // Write a simple wave file.
+       //if (result = TestSavedWave()) return result;
+       
+       // Generate single tone and verify presence.
+       if (result = TestSingleMonoTone()) return result;
+
+       // Generate prime series of tones and verify presence.
+       if (result = TestMixedMonoTones()) return result;
+       
+       // Detect dropped or added samples in a sine wave recording.
+       if (result = TestDetectPhaseErrors()) return result;
+       
+       // Test to see if notch filter can knock out the test tone.
+       if (result = TestNotchFilter()) return result;
+       
+       // Detect pops that get back in phase.
+       if (result = TestDetectPops()) return result;
+       
+
+       return 0;
+}
diff --git a/qa/loopback/src/test_audio_analyzer.h b/qa/loopback/src/test_audio_analyzer.h
new file mode 100644 (file)
index 0000000..8191b42
--- /dev/null
@@ -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/qa/loopback/src/write_wav.c b/qa/loopback/src/write_wav.c
new file mode 100755 (executable)
index 0000000..83aa85e
--- /dev/null
@@ -0,0 +1,242 @@
+/*\r
+ * PortAudio Portable Real-Time Audio Library\r
+ * Latest Version at: http://www.portaudio.com\r
+ *\r
+ * Copyright (c) 1999-2010 Phil Burk and Ross Bencina\r
+ *\r
+ * Permission is hereby granted, free of charge, to any person obtaining\r
+ * a copy of this software and associated documentation files\r
+ * (the "Software"), to deal in the Software without restriction,\r
+ * including without limitation the rights to use, copy, modify, merge,\r
+ * publish, distribute, sublicense, and/or sell copies of the Software,\r
+ * and to permit persons to whom the Software is furnished to do so,\r
+ * subject to the following conditions:\r
+ *\r
+ * The above copyright notice and this permission notice shall be\r
+ * included in all copies or substantial portions of the Software.\r
+ *\r
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\r
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\r
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\r
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR\r
+ * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF\r
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\r
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\r
+ */\r
+\r
+/*\r
+ * The text above constitutes the entire PortAudio license; however, \r
+ * the PortAudio community also makes the following non-binding requests:\r
+ *\r
+ * Any person wishing to distribute modifications to the Software is\r
+ * requested to send the modifications to the original developer so that\r
+ * they can be incorporated into the canonical version. It is also \r
+ * requested that these non-binding requests be included along with the \r
+ * license above.\r
+ */\r
+\r
+/**\r
+  * Very simple WAV file writer for saving captured audio.\r
+  */\r
+\r
+#include <stdio.h>\r
+#include <stdlib.h>\r
+#include "write_wav.h"\r
+\r
+\r
+/* Write long word data to a little endian format byte array. */\r
+static void WriteLongLE( unsigned char **addrPtr, unsigned long data )\r
+{\r
+       unsigned char *addr = *addrPtr;\r
+       *addr++ =  (unsigned char) data;\r
+       *addr++ =  (unsigned char) (data>>8);\r
+       *addr++ =  (unsigned char) (data>>16);\r
+       *addr++ =  (unsigned char) (data>>24);\r
+       *addrPtr = addr;\r
+}\r
+\r
+/* Write short word data to a little endian format byte array. */\r
+static void WriteShortLE( unsigned char **addrPtr,  unsigned short data )\r
+{\r
+       unsigned char *addr = *addrPtr;\r
+       *addr++ =  (unsigned char) data;\r
+       *addr++ =  (unsigned char) (data>>8);\r
+       *addrPtr = addr;\r
+}\r
+\r
+/* Write IFF ChunkType data to a byte array. */\r
+static void WriteChunkType( unsigned char **addrPtr, unsigned long cktyp )\r
+{\r
+       unsigned char *addr = *addrPtr;\r
+       *addr++ =  (unsigned char) (cktyp>>24);\r
+       *addr++ =  (unsigned char) (cktyp>>16);\r
+       *addr++ =  (unsigned char) (cktyp>>8);\r
+       *addr++ =  (unsigned char) cktyp;\r
+       *addrPtr = addr;\r
+}\r
+\r
+#define WAV_HEADER_SIZE (4 + 4 + 4 + /* RIFF+size+WAVE */ \\r
+        4 + 4 + 16 + /* fmt chunk */ \\r
+        4 + 4 ) /* data chunk */\r
+\r
+\r
+/*********************************************************************************\r
+ * Open named file and write WAV header to the file.\r
+ * The header includes the DATA chunk type and size.\r
+ * Returns number of bytes written to file or negative error code.\r
+ */\r
+long Audio_WAV_OpenWriter( WAV_Writer *writer, const char *fileName, int frameRate, int samplesPerFrame )\r
+{\r
+       unsigned int  bytesPerSecond;\r
+    unsigned char header[ WAV_HEADER_SIZE ];\r
+       unsigned char *addr = header;\r
+    int numWritten;\r
+       \r
+    writer->dataSize = 0;\r
+    writer->dataSizeOffset = 0;\r
+       \r
+    writer->fid = fopen( fileName, "wb" );\r
+    if( writer->fid == NULL )\r
+    {\r
+        return -1;\r
+    }\r
+\r
+/* Write RIFF header. */\r
+       WriteChunkType( &addr, RIFF_ID );\r
+\r
+/* Write RIFF size as zero for now. Will patch later. */\r
+       WriteLongLE( &addr, 0 );\r
+\r
+/* Write WAVE form ID. */\r
+       WriteChunkType( &addr, WAVE_ID );\r
+\r
+/* Write format chunk based on AudioSample structure. */\r
+       WriteChunkType( &addr, FMT_ID );\r
+    WriteLongLE( &addr, 16 );\r
+    WriteShortLE( &addr, WAVE_FORMAT_PCM );\r
+               bytesPerSecond = frameRate * samplesPerFrame * sizeof( short);\r
+       WriteShortLE( &addr, (short) samplesPerFrame );\r
+       WriteLongLE( &addr, frameRate );\r
+       WriteLongLE( &addr,  bytesPerSecond );\r
+       WriteShortLE( &addr, (short) (samplesPerFrame * sizeof( short)) ); /* bytesPerBlock */\r
+       WriteShortLE( &addr, (short) 16 ); /* bits per sample */\r
+\r
+/* Write ID and size for 'data' chunk. */\r
+       WriteChunkType( &addr, DATA_ID );\r
+/* Save offset so we can patch it later. */\r
+    writer->dataSizeOffset = (int) (addr - header);\r
+       WriteLongLE( &addr, 0 );\r
+\r
+    numWritten = fwrite( header, 1, sizeof(header), writer->fid );\r
+    if( numWritten != sizeof(header) ) return -1;\r
+\r
+       return (int) numWritten;\r
+}\r
+\r
+/*********************************************************************************\r
+ * Write to the data chunk portion of a WAV file.\r
+ * Returns bytes written or negative error code.\r
+ */\r
+long Audio_WAV_WriteShorts( WAV_Writer *writer,\r
+               short *samples,\r
+               int numSamples\r
+               )\r
+{\r
+       unsigned char buffer[2];\r
+    unsigned char *bufferPtr;\r
+       int i;\r
+       short *p = samples;\r
+    int numWritten;\r
+    int bytesWritten;\r
+       if( numSamples <= 0 )\r
+       {\r
+               return -1;\r
+       }\r
+\r
+    for( i=0; i<numSamples; i++ )\r
+       {\r
+        bufferPtr = buffer;\r
+               WriteShortLE( &bufferPtr, *p++ );\r
+        numWritten = fwrite( buffer, 1, sizeof( buffer), writer->fid );\r
+        if( numWritten != sizeof(buffer) ) return -1;\r
+       }\r
+    bytesWritten = numSamples * sizeof(short);\r
+    writer->dataSize += bytesWritten;\r
+       return (int) bytesWritten;\r
+}\r
+\r
+/*********************************************************************************\r
+ * Close WAV file.\r
+ * Update chunk sizes so it can be read by audio applications.\r
+ */\r
+long Audio_WAV_CloseWriter( WAV_Writer *writer )\r
+{\r
+       unsigned char buffer[4];\r
+    unsigned char *bufferPtr;\r
+    int numWritten;\r
+    int riffSize;\r
+\r
+    /* Go back to beginning of file and update DATA size */\r
+    int result = fseek( writer->fid, writer->dataSizeOffset, SEEK_SET );\r
+    if( result < 0 ) return result;\r
+\r
+    bufferPtr = buffer;\r
+    WriteLongLE( &bufferPtr, writer->dataSize );\r
+    numWritten = fwrite( buffer, 1, sizeof( buffer), writer->fid );\r
+    if( numWritten != sizeof(buffer) ) return -1;\r
+\r
+    /* Update RIFF size */\r
+    result = fseek( writer->fid, 4, SEEK_SET );\r
+    if( result < 0 ) return result;\r
+\r
+    riffSize = writer->dataSize + (WAV_HEADER_SIZE - 8);\r
+    bufferPtr = buffer;\r
+    WriteLongLE( &bufferPtr, riffSize );\r
+    numWritten = fwrite( buffer, 1, sizeof( buffer), writer->fid );\r
+    if( numWritten != sizeof(buffer) ) return -1;\r
+\r
+    fclose( writer->fid );\r
+    writer->fid = NULL;\r
+    return writer->dataSize;\r
+}\r
+\r
+/*********************************************************************************\r
+ * Simple test that write a sawtooth waveform to a file.\r
+ */\r
+#if 0\r
+int main( void )\r
+{\r
+    int i;\r
+    WAV_Writer writer;\r
+    int result;\r
+#define NUM_SAMPLES  (200)\r
+    short data[NUM_SAMPLES];\r
+    short saw = 0;\r
+    \r
+    for( i=0; i<NUM_SAMPLES; i++ )\r
+    {\r
+        data[i] = saw;\r
+        saw += 293;\r
+    }\r
+\r
+\r
+    result =  Audio_WAV_OpenWriter( &writer, "rendered_midi.wav", 44100, 1 );\r
+    if( result < 0 ) goto error;\r
+\r
+    for( i=0; i<15; i++ )\r
+    {\r
+        result =  Audio_WAV_WriteShorts( &writer, data, NUM_SAMPLES );\r
+        if( result < 0 ) goto error;\r
+    }\r
+\r
+    result =  Audio_WAV_CloseWriter( &writer );\r
+    if( result < 0 ) goto error;\r
+\r
+\r
+    return 0;\r
+\r
+error:\r
+    printf("ERROR: result = %d\n", result );\r
+    return result;\r
+}\r
+#endif\r
diff --git a/qa/loopback/src/write_wav.h b/qa/loopback/src/write_wav.h
new file mode 100755 (executable)
index 0000000..c22588a
--- /dev/null
@@ -0,0 +1,103 @@
+/*\r
+ * PortAudio Portable Real-Time Audio Library\r
+ * Latest Version at: http://www.portaudio.com\r
+ *\r
+ * Copyright (c) 1999-2010 Phil Burk and Ross Bencina\r
+ *\r
+ * Permission is hereby granted, free of charge, to any person obtaining\r
+ * a copy of this software and associated documentation files\r
+ * (the "Software"), to deal in the Software without restriction,\r
+ * including without limitation the rights to use, copy, modify, merge,\r
+ * publish, distribute, sublicense, and/or sell copies of the Software,\r
+ * and to permit persons to whom the Software is furnished to do so,\r
+ * subject to the following conditions:\r
+ *\r
+ * The above copyright notice and this permission notice shall be\r
+ * included in all copies or substantial portions of the Software.\r
+ *\r
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\r
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\r
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\r
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR\r
+ * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF\r
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\r
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\r
+ */\r
+\r
+/*\r
+ * The text above constitutes the entire PortAudio license; however, \r
+ * the PortAudio community also makes the following non-binding requests:\r
+ *\r
+ * Any person wishing to distribute modifications to the Software is\r
+ * requested to send the modifications to the original developer so that\r
+ * they can be incorporated into the canonical version. It is also \r
+ * requested that these non-binding requests be included along with the \r
+ * license above.\r
+ */\r
+#ifndef _WAV_WRITER_H\r
+#define _WAV_WRITER_H\r
+\r
+/*\r
+ * WAV file writer.\r
+ *\r
+ * Author: Phil Burk\r
+ */\r
+\r
+#ifdef __cplusplus\r
+extern "C" {\r
+#endif\r
+\r
+/* Define WAV Chunk and FORM types as 4 byte integers. */\r
+#define RIFF_ID   (('R'<<24) | ('I'<<16) | ('F'<<8) | 'F')\r
+#define WAVE_ID   (('W'<<24) | ('A'<<16) | ('V'<<8) | 'E')\r
+#define FMT_ID    (('f'<<24) | ('m'<<16) | ('t'<<8) | ' ')\r
+#define DATA_ID   (('d'<<24) | ('a'<<16) | ('t'<<8) | 'a')\r
+#define FACT_ID   (('f'<<24) | ('a'<<16) | ('c'<<8) | 't')\r
+\r
+/* Errors returned by Audio_ParseSampleImage_WAV */\r
+#define WAV_ERR_CHUNK_SIZE     (-1)   /* Chunk size is illegal or past file size. */\r
+#define WAV_ERR_FILE_TYPE      (-2)   /* Not a WAV file. */\r
+#define WAV_ERR_ILLEGAL_VALUE  (-3)   /* Illegal or unsupported value. Eg. 927 bits/sample */\r
+#define WAV_ERR_FORMAT_TYPE    (-4)   /* Unsupported format, eg. compressed. */\r
+#define WAV_ERR_TRUNCATED      (-5)   /* End of file missing. */\r
+\r
+/* WAV PCM data format ID */\r
+#define WAVE_FORMAT_PCM        (1)\r
+#define WAVE_FORMAT_IMA_ADPCM  (0x0011)\r
+\r
+       \r
+typedef struct WAV_Writer_s\r
+{\r
+    FILE *fid;\r
+    /* Offset in file for data size. */\r
+    int   dataSizeOffset;\r
+    int   dataSize;\r
+} WAV_Writer;\r
+\r
+/*********************************************************************************\r
+ * Open named file and write WAV header to the file.\r
+ * The header includes the DATA chunk type and size.\r
+ * Returns number of bytes written to file or negative error code.\r
+ */\r
+long Audio_WAV_OpenWriter( WAV_Writer *writer, const char *fileName, int frameRate, int samplesPerFrame );\r
+\r
+/*********************************************************************************\r
+ * Write to the data chunk portion of a WAV file.\r
+ * Returns bytes written or negative error code.\r
+ */\r
+long Audio_WAV_WriteShorts( WAV_Writer *writer,\r
+               short *samples,\r
+               int numSamples\r
+               );\r
+\r
+/*********************************************************************************\r
+ * Close WAV file.\r
+ * Update chunk sizes so it can be read by audio applications.\r
+ */\r
+long Audio_WAV_CloseWriter( WAV_Writer *writer );\r
+\r
+#ifdef __cplusplus\r
+};\r
+#endif\r
+\r
+#endif /* _WAV_WRITER_H */\r