First checkin of new loopback unit test for PortAudio.

This commit is contained in:
philburk 2010-11-12 19:14:25 +00:00
commit 3d61d394b8
13 changed files with 2824 additions and 0 deletions

View file

@ -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;
}

View file

@ -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 */

123
qa/loopback/src/biquad_filter.c Executable file
View file

@ -0,0 +1,123 @@
#include <math.h>
#include <strings.h>
#include "biquad_filter.h"
/**
* Unit_BiquadFilter implements a second order IIR filter.
Here is the equation that we use for this filter:
y(n) = a0*x(n) + a1*x(n-1) + a2*x(n-2) - b1*y(n-1) - b2*y(n-2)
*
* @author (C) 2002 Phil Burk, SoftSynth.com, All Rights Reserved
*/
#define FILTER_PI (3.14159265)
/***********************************************************
** Calculate coefficients common to many parametric biquad filters.
*/
static void BiquadFilter_CalculateCommon( BiquadFilter *filter, double ratio, double Q )
{
double omega;
memset( filter, 0, sizeof(BiquadFilter) );
/* Don't let frequency get too close to Nyquist or filter will blow up. */
if( ratio >= 0.499 ) ratio = 0.499;
omega = 2.0 * (double)FILTER_PI * ratio;
filter->cos_omega = (double) cos( omega );
filter->sin_omega = (double) sin( omega );
filter->alpha = filter->sin_omega / (2.0 * Q);
}
/*********************************************************************************
** Calculate coefficients for Highpass filter.
*/
void BiquadFilter_SetupHighPass( BiquadFilter *filter, double ratio, double Q )
{
double scalar, opc;
if( ratio < BIQUAD_MIN_RATIO ) ratio = BIQUAD_MIN_RATIO;
if( Q < BIQUAD_MIN_Q ) Q = BIQUAD_MIN_Q;
BiquadFilter_CalculateCommon( filter, ratio, Q );
scalar = 1.0 / (1.0 + filter->alpha);
opc = (1.0 + filter->cos_omega);
filter->a0 = opc * 0.5 * scalar;
filter->a1 = - opc * scalar;
filter->a2 = filter->a0;
filter->b1 = -2.0 * filter->cos_omega * scalar;
filter->b2 = (1.0 - filter->alpha) * scalar;
}
/*********************************************************************************
** Calculate coefficients for Notch filter.
*/
void BiquadFilter_SetupNotch( BiquadFilter *filter, double ratio, double Q )
{
double scalar, opc;
if( ratio < BIQUAD_MIN_RATIO ) ratio = BIQUAD_MIN_RATIO;
if( Q < BIQUAD_MIN_Q ) Q = BIQUAD_MIN_Q;
BiquadFilter_CalculateCommon( filter, ratio, Q );
scalar = 1.0 / (1.0 + filter->alpha);
opc = (1.0 + filter->cos_omega);
filter->a0 = scalar;
filter->a1 = -2.0 * filter->cos_omega * scalar;
filter->a2 = filter->a0;
filter->b1 = filter->a1;
filter->b2 = (1.0 - filter->alpha) * scalar;
}
/*****************************************************************
** Perform core IIR filter calculation without permutation.
*/
void BiquadFilter_Filter( BiquadFilter *filter, float *inputs, float *outputs, int numSamples )
{
int i;
double xn, yn;
// Pull values from structure to speed up the calculation.
double a0 = filter->a0;
double a1 = filter->a1;
double a2 = filter->a2;
double b1 = filter->b1;
double b2 = filter->b2;
double xn1 = filter->xn1;
double xn2 = filter->xn2;
double yn1 = filter->yn1;
double yn2 = filter->yn2;
for( i=0; i<numSamples; i++)
{
// Generate outputs by filtering inputs.
xn = inputs[i];
yn = (a0 * xn) + (a1 * xn1) + (a2 * xn2) - (b1 * yn1) - (b2 * yn2);
outputs[i] = yn;
// Delay input and output values.
xn2 = xn1;
xn1 = xn;
yn2 = yn1;
yn1 = yn;
if( (i & 7) == 0 )
{
// Apply a small bipolar impulse to filter to prevent arithmetic underflow.
// Underflows can cause the FPU to interrupt the CPU.
yn1 += (double) 1.0E-26;
yn2 -= (double) 1.0E-26;
}
}
filter->xn1 = xn1;
filter->xn2 = xn2;
filter->yn1 = yn1;
filter->yn2 = yn2;
}

38
qa/loopback/src/biquad_filter.h Executable file
View file

@ -0,0 +1,38 @@
#ifndef _BIQUADFILTER_H
#define _BIQUADFILTER_H
/**
* Unit_BiquadFilter implements a second order IIR filter.
*
* @author (C) 2002 Phil Burk, SoftSynth.com, All Rights Reserved
*/
#define BIQUAD_MIN_RATIO (0.000001)
#define BIQUAD_MIN_Q (0.00001)
typedef struct BiquadFilter_s
{
double xn1; // storage for delayed signals
double xn2;
double yn1;
double yn2;
double a0; // coefficients
double a1;
double a2;
double b1;
double b2;
double cos_omega;
double sin_omega;
double alpha;
} BiquadFilter;
void BiquadFilter_SetupHighPass( BiquadFilter *filter, double ratio, double Q );
void BiquadFilter_SetupNotch( BiquadFilter *filter, double ratio, double Q );
void BiquadFilter_Filter( BiquadFilter *filter, float *inputs, float *outputs, int numSamples );
#endif

671
qa/loopback/src/paqa.c Normal file
View file

@ -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;
}
}

View file

@ -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 );
}
}

View file

@ -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 */

74
qa/loopback/src/qa_tools.h Executable file
View file

@ -0,0 +1,74 @@
/*
* PortAudio Portable Real-Time Audio Library
* Latest Version at: http://www.portaudio.com
*
* Copyright (c) 1999-2010 Phil Burk and Ross Bencina
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
* ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* The text above constitutes the entire PortAudio license; however,
* the PortAudio community also makes the following non-binding requests:
*
* Any person wishing to distribute modifications to the Software is
* requested to send the modifications to the original developer so that
* they can be incorporated into the canonical version. It is also
* requested that these non-binding requests be included along with the
* license above.
*/
#ifndef _QA_TOOLS_H
#define _QA_TOOLS_H
extern int g_testsPassed;
extern int g_testsFailed;
#define QA_ASSERT_TRUE( message, flag ) \
if( !(flag) ) \
{ \
printf( "%s:%d - ERROR - %s\n", __FILE__, __LINE__, message ); \
g_testsFailed++; \
goto error; \
} \
else g_testsPassed++;
#define QA_ASSERT_EQUALS( message, expected, actual ) \
if( (expected) != (actual) ) \
{ \
printf( "%s:%d - ERROR - %s, expected %d, got %d\n", __FILE__, __LINE__, message, expected, actual ); \
g_testsFailed++; \
goto error; \
} \
else g_testsPassed++;
#define QA_ASSERT_CLOSE( message, expected, actual, tolerance ) \
if (fabs((expected)-(actual))>(tolerance)) \
{ \
printf( "%s:%d - ERROR - %s, expected %f, got %f, tol=%f\n", __FILE__, __LINE__, message, ((double)(expected)), ((double)(actual)), ((double)(tolerance)) ); \
g_testsFailed++; \
goto error; \
} \
else g_testsPassed++;
#endif

View file

@ -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;
}

View file

@ -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 */

242
qa/loopback/src/write_wav.c Executable file
View file

@ -0,0 +1,242 @@
/*
* PortAudio Portable Real-Time Audio Library
* Latest Version at: http://www.portaudio.com
*
* Copyright (c) 1999-2010 Phil Burk and Ross Bencina
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
* ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* The text above constitutes the entire PortAudio license; however,
* the PortAudio community also makes the following non-binding requests:
*
* Any person wishing to distribute modifications to the Software is
* requested to send the modifications to the original developer so that
* they can be incorporated into the canonical version. It is also
* requested that these non-binding requests be included along with the
* license above.
*/
/**
* Very simple WAV file writer for saving captured audio.
*/
#include <stdio.h>
#include <stdlib.h>
#include "write_wav.h"
/* Write long word data to a little endian format byte array. */
static void WriteLongLE( unsigned char **addrPtr, unsigned long data )
{
unsigned char *addr = *addrPtr;
*addr++ = (unsigned char) data;
*addr++ = (unsigned char) (data>>8);
*addr++ = (unsigned char) (data>>16);
*addr++ = (unsigned char) (data>>24);
*addrPtr = addr;
}
/* Write short word data to a little endian format byte array. */
static void WriteShortLE( unsigned char **addrPtr, unsigned short data )
{
unsigned char *addr = *addrPtr;
*addr++ = (unsigned char) data;
*addr++ = (unsigned char) (data>>8);
*addrPtr = addr;
}
/* Write IFF ChunkType data to a byte array. */
static void WriteChunkType( unsigned char **addrPtr, unsigned long cktyp )
{
unsigned char *addr = *addrPtr;
*addr++ = (unsigned char) (cktyp>>24);
*addr++ = (unsigned char) (cktyp>>16);
*addr++ = (unsigned char) (cktyp>>8);
*addr++ = (unsigned char) cktyp;
*addrPtr = addr;
}
#define WAV_HEADER_SIZE (4 + 4 + 4 + /* RIFF+size+WAVE */ \
4 + 4 + 16 + /* fmt chunk */ \
4 + 4 ) /* data chunk */
/*********************************************************************************
* Open named file and write WAV header to the file.
* The header includes the DATA chunk type and size.
* Returns number of bytes written to file or negative error code.
*/
long Audio_WAV_OpenWriter( WAV_Writer *writer, const char *fileName, int frameRate, int samplesPerFrame )
{
unsigned int bytesPerSecond;
unsigned char header[ WAV_HEADER_SIZE ];
unsigned char *addr = header;
int numWritten;
writer->dataSize = 0;
writer->dataSizeOffset = 0;
writer->fid = fopen( fileName, "wb" );
if( writer->fid == NULL )
{
return -1;
}
/* Write RIFF header. */
WriteChunkType( &addr, RIFF_ID );
/* Write RIFF size as zero for now. Will patch later. */
WriteLongLE( &addr, 0 );
/* Write WAVE form ID. */
WriteChunkType( &addr, WAVE_ID );
/* Write format chunk based on AudioSample structure. */
WriteChunkType( &addr, FMT_ID );
WriteLongLE( &addr, 16 );
WriteShortLE( &addr, WAVE_FORMAT_PCM );
bytesPerSecond = frameRate * samplesPerFrame * sizeof( short);
WriteShortLE( &addr, (short) samplesPerFrame );
WriteLongLE( &addr, frameRate );
WriteLongLE( &addr, bytesPerSecond );
WriteShortLE( &addr, (short) (samplesPerFrame * sizeof( short)) ); /* bytesPerBlock */
WriteShortLE( &addr, (short) 16 ); /* bits per sample */
/* Write ID and size for 'data' chunk. */
WriteChunkType( &addr, DATA_ID );
/* Save offset so we can patch it later. */
writer->dataSizeOffset = (int) (addr - header);
WriteLongLE( &addr, 0 );
numWritten = fwrite( header, 1, sizeof(header), writer->fid );
if( numWritten != sizeof(header) ) return -1;
return (int) numWritten;
}
/*********************************************************************************
* Write to the data chunk portion of a WAV file.
* Returns bytes written or negative error code.
*/
long Audio_WAV_WriteShorts( WAV_Writer *writer,
short *samples,
int numSamples
)
{
unsigned char buffer[2];
unsigned char *bufferPtr;
int i;
short *p = samples;
int numWritten;
int bytesWritten;
if( numSamples <= 0 )
{
return -1;
}
for( i=0; i<numSamples; i++ )
{
bufferPtr = buffer;
WriteShortLE( &bufferPtr, *p++ );
numWritten = fwrite( buffer, 1, sizeof( buffer), writer->fid );
if( numWritten != sizeof(buffer) ) return -1;
}
bytesWritten = numSamples * sizeof(short);
writer->dataSize += bytesWritten;
return (int) bytesWritten;
}
/*********************************************************************************
* Close WAV file.
* Update chunk sizes so it can be read by audio applications.
*/
long Audio_WAV_CloseWriter( WAV_Writer *writer )
{
unsigned char buffer[4];
unsigned char *bufferPtr;
int numWritten;
int riffSize;
/* Go back to beginning of file and update DATA size */
int result = fseek( writer->fid, writer->dataSizeOffset, SEEK_SET );
if( result < 0 ) return result;
bufferPtr = buffer;
WriteLongLE( &bufferPtr, writer->dataSize );
numWritten = fwrite( buffer, 1, sizeof( buffer), writer->fid );
if( numWritten != sizeof(buffer) ) return -1;
/* Update RIFF size */
result = fseek( writer->fid, 4, SEEK_SET );
if( result < 0 ) return result;
riffSize = writer->dataSize + (WAV_HEADER_SIZE - 8);
bufferPtr = buffer;
WriteLongLE( &bufferPtr, riffSize );
numWritten = fwrite( buffer, 1, sizeof( buffer), writer->fid );
if( numWritten != sizeof(buffer) ) return -1;
fclose( writer->fid );
writer->fid = NULL;
return writer->dataSize;
}
/*********************************************************************************
* Simple test that write a sawtooth waveform to a file.
*/
#if 0
int main( void )
{
int i;
WAV_Writer writer;
int result;
#define NUM_SAMPLES (200)
short data[NUM_SAMPLES];
short saw = 0;
for( i=0; i<NUM_SAMPLES; i++ )
{
data[i] = saw;
saw += 293;
}
result = Audio_WAV_OpenWriter( &writer, "rendered_midi.wav", 44100, 1 );
if( result < 0 ) goto error;
for( i=0; i<15; i++ )
{
result = Audio_WAV_WriteShorts( &writer, data, NUM_SAMPLES );
if( result < 0 ) goto error;
}
result = Audio_WAV_CloseWriter( &writer );
if( result < 0 ) goto error;
return 0;
error:
printf("ERROR: result = %d\n", result );
return result;
}
#endif

103
qa/loopback/src/write_wav.h Executable file
View file

@ -0,0 +1,103 @@
/*
* PortAudio Portable Real-Time Audio Library
* Latest Version at: http://www.portaudio.com
*
* Copyright (c) 1999-2010 Phil Burk and Ross Bencina
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
* ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* The text above constitutes the entire PortAudio license; however,
* the PortAudio community also makes the following non-binding requests:
*
* Any person wishing to distribute modifications to the Software is
* requested to send the modifications to the original developer so that
* they can be incorporated into the canonical version. It is also
* requested that these non-binding requests be included along with the
* license above.
*/
#ifndef _WAV_WRITER_H
#define _WAV_WRITER_H
/*
* WAV file writer.
*
* Author: Phil Burk
*/
#ifdef __cplusplus
extern "C" {
#endif
/* Define WAV Chunk and FORM types as 4 byte integers. */
#define RIFF_ID (('R'<<24) | ('I'<<16) | ('F'<<8) | 'F')
#define WAVE_ID (('W'<<24) | ('A'<<16) | ('V'<<8) | 'E')
#define FMT_ID (('f'<<24) | ('m'<<16) | ('t'<<8) | ' ')
#define DATA_ID (('d'<<24) | ('a'<<16) | ('t'<<8) | 'a')
#define FACT_ID (('f'<<24) | ('a'<<16) | ('c'<<8) | 't')
/* Errors returned by Audio_ParseSampleImage_WAV */
#define WAV_ERR_CHUNK_SIZE (-1) /* Chunk size is illegal or past file size. */
#define WAV_ERR_FILE_TYPE (-2) /* Not a WAV file. */
#define WAV_ERR_ILLEGAL_VALUE (-3) /* Illegal or unsupported value. Eg. 927 bits/sample */
#define WAV_ERR_FORMAT_TYPE (-4) /* Unsupported format, eg. compressed. */
#define WAV_ERR_TRUNCATED (-5) /* End of file missing. */
/* WAV PCM data format ID */
#define WAVE_FORMAT_PCM (1)
#define WAVE_FORMAT_IMA_ADPCM (0x0011)
typedef struct WAV_Writer_s
{
FILE *fid;
/* Offset in file for data size. */
int dataSizeOffset;
int dataSize;
} WAV_Writer;
/*********************************************************************************
* Open named file and write WAV header to the file.
* The header includes the DATA chunk type and size.
* Returns number of bytes written to file or negative error code.
*/
long Audio_WAV_OpenWriter( WAV_Writer *writer, const char *fileName, int frameRate, int samplesPerFrame );
/*********************************************************************************
* Write to the data chunk portion of a WAV file.
* Returns bytes written or negative error code.
*/
long Audio_WAV_WriteShorts( WAV_Writer *writer,
short *samples,
int numSamples
);
/*********************************************************************************
* Close WAV file.
* Update chunk sizes so it can be read by audio applications.
*/
long Audio_WAV_CloseWriter( WAV_Writer *writer );
#ifdef __cplusplus
};
#endif
#endif /* _WAV_WRITER_H */