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

83
qa/loopback/README.txt Normal file
View file

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

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