Mercurial > hg > sv-dependency-builds
view src/portaudio/qa/loopback/src/test_audio_analyzer.c @ 10:37bf6b4a2645
Add FFTW3
author | Chris Cannam |
---|---|
date | Wed, 20 Mar 2013 15:35:50 +0000 |
parents | e13257ea84a4 |
children |
line wrap: on
line source
/* * PortAudio Portable Real-Time Audio Library * Latest Version at: http://www.portaudio.com * * Copyright (c) 1999-2010 Phil Burk and Ross Bencina * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, * including without limitation the rights to use, copy, modify, merge, * publish, distribute, sublicense, and/or sell copies of the Software, * and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * The text above constitutes the entire PortAudio license; however, * the PortAudio community also makes the following non-binding requests: * * Any person wishing to distribute modifications to the Software is * requested to send the modifications to the original developer so that * they can be incorporated into the canonical version. It is also * requested that these non-binding requests be included along with the * license above. */ #include <stdio.h> #include <stdlib.h> #include <math.h> #include "qa_tools.h" #include "audio_analyzer.h" #include "test_audio_analyzer.h" #include "write_wav.h" #include "biquad_filter.h" #define FRAMES_PER_BLOCK (64) #define PRINT_REPORTS 0 #define TEST_SAVED_WAVE (0) /*==========================================================================================*/ /** * Detect a single tone. */ static int TestSingleMonoTone( void ) { int result = 0; PaQaSineGenerator generator; PaQaRecording recording; float buffer[FRAMES_PER_BLOCK]; double sampleRate = 44100.0; int maxFrames = ((int)sampleRate) * 1; int samplesPerFrame = 1; int stride = 1; int done = 0; double freq = 234.5; double amp = 0.5; double mag1, mag2; // Setup a sine oscillator. PaQa_SetupSineGenerator( &generator, freq, amp, sampleRate ); result = PaQa_InitializeRecording( &recording, maxFrames, (int) sampleRate ); QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); done = 0; while (!done) { PaQa_EraseBuffer( buffer, FRAMES_PER_BLOCK, samplesPerFrame ); PaQa_MixSine( &generator, buffer, FRAMES_PER_BLOCK, stride ); done = PaQa_WriteRecording( &recording, buffer, FRAMES_PER_BLOCK, samplesPerFrame ); } mag1 = PaQa_CorrelateSine( &recording, freq, sampleRate, 0, recording.numFrames, NULL ); QA_ASSERT_CLOSE( "exact frequency match", amp, mag1, 0.01 ); mag2 = PaQa_CorrelateSine( &recording, freq * 1.23, sampleRate, 0, recording.numFrames, NULL ); QA_ASSERT_CLOSE( "wrong frequency", 0.0, mag2, 0.01 ); PaQa_TerminateRecording( &recording ); return 0; error: PaQa_TerminateRecording( &recording); return 1; } /*==========================================================================================*/ /** * Mix multiple tones and then detect them. */ static int TestMixedMonoTones( void ) { int i; int result = 0; #define NUM_TONES (5) PaQaSineGenerator generators[NUM_TONES]; PaQaRecording recording; float buffer[FRAMES_PER_BLOCK]; double sampleRate = 44100.0; int maxFrames = ((int)sampleRate) * 1; int samplesPerFrame = 1; double baseFreq = 234.5; double amp = 0.1; double mag2; int stride = samplesPerFrame; int done = 0; // Setup a sine oscillator. for( i=0; i<NUM_TONES; i++ ) { PaQa_SetupSineGenerator( &generators[i], PaQa_GetNthFrequency( baseFreq, i ), amp, sampleRate ); } result = PaQa_InitializeRecording( &recording, maxFrames, (int) sampleRate ); QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); done = 0; while (!done) { PaQa_EraseBuffer( buffer, FRAMES_PER_BLOCK, samplesPerFrame ); for( i=0; i<NUM_TONES; i++ ) { PaQa_MixSine( &generators[i], buffer, FRAMES_PER_BLOCK, stride ); } done = PaQa_WriteRecording( &recording, buffer, FRAMES_PER_BLOCK, samplesPerFrame ); } for( i=0; i<NUM_TONES; i++ ) { double mag = PaQa_CorrelateSine( &recording, PaQa_GetNthFrequency( baseFreq, i), sampleRate, 0, recording.numFrames, NULL ); QA_ASSERT_CLOSE( "exact frequency match", amp, mag, 0.01 ); } mag2 = PaQa_CorrelateSine( &recording, baseFreq * 0.87, sampleRate, 0, recording.numFrames, NULL ); QA_ASSERT_CLOSE( "wrong frequency", 0.0, mag2, 0.01 ); PaQa_TerminateRecording( &recording ); return 0; error: PaQa_TerminateRecording( &recording); return 1; } /*==========================================================================================*/ /** * Generate a recording with added or dropped frames. */ static void MakeRecordingWithAddedFrames( PaQaRecording *recording, PaQaTestTone *testTone, int glitchPosition, int framesToAdd ) { PaQaSineGenerator generator; #define BUFFER_SIZE 512 float buffer[BUFFER_SIZE]; int frameCounter = testTone->startDelay; int stride = 1; // Record some initial silence. int done = PaQa_WriteSilence( recording, testTone->startDelay ); // Setup a sine oscillator. PaQa_SetupSineGenerator( &generator, testTone->frequency, testTone->amplitude, testTone->sampleRate ); while (!done) { int framesThisLoop = BUFFER_SIZE; if( frameCounter == glitchPosition ) { if( framesToAdd > 0 ) { // Record some frozen data without advancing the sine generator. done = PaQa_RecordFreeze( recording, framesToAdd ); frameCounter += framesToAdd; } else if( framesToAdd < 0 ) { // Advance sine generator a few frames. PaQa_MixSine( &generator, buffer, 0 - framesToAdd, stride ); } } else if( (frameCounter < glitchPosition) && ((frameCounter + framesThisLoop) > glitchPosition) ) { // Go right up to the glitchPosition. framesThisLoop = glitchPosition - frameCounter; } if( framesThisLoop > 0 ) { PaQa_EraseBuffer( buffer, framesThisLoop, testTone->samplesPerFrame ); PaQa_MixSine( &generator, buffer, framesThisLoop, stride ); done = PaQa_WriteRecording( recording, buffer, framesThisLoop, testTone->samplesPerFrame ); } frameCounter += framesThisLoop; } } /*==========================================================================================*/ /** * Generate a clean recording. */ static void MakeCleanRecording( PaQaRecording *recording, PaQaTestTone *testTone ) { PaQaSineGenerator generator; #define BUFFER_SIZE 512 float buffer[BUFFER_SIZE]; int stride = 1; // Record some initial silence. int done = PaQa_WriteSilence( recording, testTone->startDelay ); // Setup a sine oscillator. PaQa_SetupSineGenerator( &generator, testTone->frequency, testTone->amplitude, testTone->sampleRate ); // Generate recording with good phase. while (!done) { PaQa_EraseBuffer( buffer, BUFFER_SIZE, testTone->samplesPerFrame ); PaQa_MixSine( &generator, buffer, BUFFER_SIZE, stride ); done = PaQa_WriteRecording( recording, buffer, BUFFER_SIZE, testTone->samplesPerFrame ); } } /*==========================================================================================*/ /** * Generate a recording with pop. */ static void MakeRecordingWithPop( PaQaRecording *recording, PaQaTestTone *testTone, int popPosition, int popWidth, double popAmplitude ) { int i; MakeCleanRecording( recording, testTone ); // Apply glitch to good recording. if( (popPosition + popWidth) >= recording->numFrames ) { popWidth = (recording->numFrames - popPosition) - 1; } for( i=0; i<popWidth; i++ ) { float good = recording->buffer[i+popPosition]; float bad = (good > 0.0) ? (good - popAmplitude) : (good + popAmplitude); recording->buffer[i+popPosition] = bad; } } /*==========================================================================================*/ /** * Detect one phase error in a recording. */ static int TestDetectSinglePhaseError( double sampleRate, int cycleSize, int latencyFrames, int glitchPosition, int framesAdded ) { int result = 0; PaQaRecording recording; PaQaTestTone testTone; PaQaAnalysisResult analysisResult = { 0.0 }; int framesDropped = 0; int maxFrames = ((int)sampleRate) * 2; testTone.samplesPerFrame = 1; testTone.sampleRate = sampleRate; testTone.frequency = sampleRate / cycleSize; testTone.amplitude = 0.5; testTone.startDelay = latencyFrames; result = PaQa_InitializeRecording( &recording, maxFrames, (int) sampleRate ); QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); MakeRecordingWithAddedFrames( &recording, &testTone, glitchPosition, framesAdded ); PaQa_AnalyseRecording( &recording, &testTone, &analysisResult ); if( framesAdded < 0 ) { framesDropped = -framesAdded; framesAdded = 0; } #if PRINT_REPORTS printf("\n=== Dropped Frame Analysis ===================\n"); printf(" expected actual\n"); printf(" latency: %10.3f %10.3f\n", (double)latencyFrames, analysisResult.latency ); printf(" num added frames: %10.3f %10.3f\n", (double)framesAdded, analysisResult.numAddedFrames ); printf(" added frames at: %10.3f %10.3f\n", (double)glitchPosition, analysisResult.addedFramesPosition ); printf(" num dropped frames: %10.3f %10.3f\n", (double)framesDropped, analysisResult.numDroppedFrames ); printf(" dropped frames at: %10.3f %10.3f\n", (double)glitchPosition, analysisResult.droppedFramesPosition ); #endif QA_ASSERT_CLOSE( "PaQa_AnalyseRecording latency", latencyFrames, analysisResult.latency, 0.5 ); QA_ASSERT_CLOSE( "PaQa_AnalyseRecording framesAdded", framesAdded, analysisResult.numAddedFrames, 1.0 ); QA_ASSERT_CLOSE( "PaQa_AnalyseRecording framesDropped", framesDropped, analysisResult.numDroppedFrames, 1.0 ); // QA_ASSERT_CLOSE( "PaQa_AnalyseRecording glitchPosition", glitchPosition, analysisResult.glitchPosition, cycleSize ); PaQa_TerminateRecording( &recording ); return 0; error: PaQa_TerminateRecording( &recording); return 1; } /*==========================================================================================*/ /** * Test various dropped sample scenarios. */ static int TestDetectPhaseErrors( void ) { int result; result = TestDetectSinglePhaseError( 44100, 200, 477, -1, 0 ); if( result < 0 ) return result; /* result = TestDetectSinglePhaseError( 44100, 200, 77, -1, 0 ); if( result < 0 ) return result; result = TestDetectSinglePhaseError( 44100, 200, 83, 3712, 9 ); if( result < 0 ) return result; result = TestDetectSinglePhaseError( 44100, 280, 83, 3712, 27 ); if( result < 0 ) return result; result = TestDetectSinglePhaseError( 44100, 200, 234, 3712, -9 ); if( result < 0 ) return result; result = TestDetectSinglePhaseError( 44100, 200, 2091, 8923, -2 ); if( result < 0 ) return result; result = TestDetectSinglePhaseError( 44100, 120, 1782, 5772, -18 ); if( result < 0 ) return result; // Note that if the frequency is too high then it is hard to detect single dropped frames. result = TestDetectSinglePhaseError( 44100, 200, 500, 4251, -1 ); if( result < 0 ) return result; */ return 0; } /*==========================================================================================*/ /** * Detect one pop in a recording. */ static int TestDetectSinglePop( double sampleRate, int cycleSize, int latencyFrames, int popPosition, int popWidth, double popAmplitude ) { int result = 0; PaQaRecording recording; PaQaTestTone testTone; PaQaAnalysisResult analysisResult = { 0.0 }; int maxFrames = ((int)sampleRate) * 2; testTone.samplesPerFrame = 1; testTone.sampleRate = sampleRate; testTone.frequency = sampleRate / cycleSize; testTone.amplitude = 0.5; testTone.startDelay = latencyFrames; result = PaQa_InitializeRecording( &recording, maxFrames, (int) sampleRate ); QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); MakeRecordingWithPop( &recording, &testTone, popPosition, popWidth, popAmplitude ); PaQa_AnalyseRecording( &recording, &testTone, &analysisResult ); #if PRINT_REPORTS printf("\n=== Pop Analysis ===================\n"); printf(" expected actual\n"); printf(" latency: %10.3f %10.3f\n", (double)latencyFrames, analysisResult.latency ); printf(" popPosition: %10.3f %10.3f\n", (double)popPosition, analysisResult.popPosition ); printf(" popAmplitude: %10.3f %10.3f\n", popAmplitude, analysisResult.popAmplitude ); printf(" cycleSize: %6d\n", cycleSize ); printf(" num added frames: %10.3f\n", analysisResult.numAddedFrames ); printf(" added frames at: %10.3f\n", analysisResult.addedFramesPosition ); printf(" num dropped frames: %10.3f\n", analysisResult.numDroppedFrames ); printf(" dropped frames at: %10.3f\n", analysisResult.droppedFramesPosition ); #endif QA_ASSERT_CLOSE( "PaQa_AnalyseRecording latency", latencyFrames, analysisResult.latency, 0.5 ); QA_ASSERT_CLOSE( "PaQa_AnalyseRecording popPosition", popPosition, analysisResult.popPosition, 10 ); if( popWidth > 0 ) { QA_ASSERT_CLOSE( "PaQa_AnalyseRecording popAmplitude", popAmplitude, analysisResult.popAmplitude, 0.1 * popAmplitude ); } PaQa_TerminateRecording( &recording ); return 0; error: PaQa_SaveRecordingToWaveFile( &recording, "bad_recording.wav" ); PaQa_TerminateRecording( &recording); return 1; } /*==========================================================================================*/ /** * Analyse recording with a DC offset. */ static int TestSingleInitialSpike( double sampleRate, int stepPosition, int cycleSize, int latencyFrames, double stepAmplitude ) { int i; int result = 0; // Account for highpass filter offset. int expectedLatency = latencyFrames + 1; PaQaRecording recording; PaQaRecording hipassOutput = { 0 }; BiquadFilter hipassFilter; PaQaTestTone testTone; PaQaAnalysisResult analysisResult = { 0.0 }; int maxFrames = ((int)sampleRate) * 2; testTone.samplesPerFrame = 1; testTone.sampleRate = sampleRate; testTone.frequency = sampleRate / cycleSize; testTone.amplitude = -0.5; testTone.startDelay = latencyFrames; result = PaQa_InitializeRecording( &recording, maxFrames, (int) sampleRate ); QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); result = PaQa_InitializeRecording( &hipassOutput, maxFrames, (int) sampleRate ); QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); MakeCleanRecording( &recording, &testTone ); // Apply DC step. for( i=stepPosition; i<recording.numFrames; i++ ) { recording.buffer[i] += stepAmplitude; } // Use high pass as a DC blocker! BiquadFilter_SetupHighPass( &hipassFilter, 10.0 / sampleRate, 0.5 ); PaQa_FilterRecording( &recording, &hipassOutput, &hipassFilter ); testTone.amplitude = 0.5; PaQa_AnalyseRecording( &hipassOutput, &testTone, &analysisResult ); #if PRINT_REPORTS printf("\n=== InitialSpike Analysis ===================\n"); printf(" expected actual\n"); printf(" latency: %10.3f %10.3f\n", (double)expectedLatency, analysisResult.latency ); printf(" popPosition: %10.3f\n", analysisResult.popPosition ); printf(" popAmplitude: %10.3f\n", analysisResult.popAmplitude ); printf(" amplitudeRatio: %10.3f\n", analysisResult.amplitudeRatio ); printf(" cycleSize: %6d\n", cycleSize ); printf(" num added frames: %10.3f\n", analysisResult.numAddedFrames ); printf(" added frames at: %10.3f\n", analysisResult.addedFramesPosition ); printf(" num dropped frames: %10.3f\n", analysisResult.numDroppedFrames ); printf(" dropped frames at: %10.3f\n", analysisResult.droppedFramesPosition ); #endif QA_ASSERT_CLOSE( "PaQa_AnalyseRecording latency", expectedLatency, analysisResult.latency, 4.0 ); QA_ASSERT_EQUALS( "PaQa_AnalyseRecording no pop from step", -1, (int) analysisResult.popPosition ); PaQa_TerminateRecording( &recording ); PaQa_TerminateRecording( &hipassOutput ); return 0; error: PaQa_SaveRecordingToWaveFile( &recording, "bad_step_original.wav" ); PaQa_SaveRecordingToWaveFile( &hipassOutput, "bad_step_hipass.wav" ); PaQa_TerminateRecording( &recording); PaQa_TerminateRecording( &hipassOutput ); return 1; } /*==========================================================================================*/ /** * Test various dropped sample scenarios. */ static int TestDetectPops( void ) { int result; // No pop. result = TestDetectSinglePop( 44100, 200, 477, -1, 0, 0.0 ); if( result < 0 ) return result; // short pop result = TestDetectSinglePop( 44100, 300, 810, 3987, 1, 0.5 ); if( result < 0 ) return result; // medium long pop result = TestDetectSinglePop( 44100, 300, 810, 9876, 5, 0.5 ); if( result < 0 ) return result; // short tiny pop result = TestDetectSinglePop( 44100, 250, 810, 5672, 1, 0.05 ); if( result < 0 ) return result; return 0; } /*==========================================================================================*/ /** * Test analysis when there is a DC offset step before the sine signal. */ static int TestInitialSpike( void ) { int result; //( double sampleRate, int stepPosition, int cycleSize, int latencyFrames, double stepAmplitude ) // No spike. result = TestSingleInitialSpike( 44100, 32, 100, 537, 0.0 ); if( result < 0 ) return result; // Small spike. result = TestSingleInitialSpike( 44100, 32, 100, 537, 0.1 ); if( result < 0 ) return result; // short pop like Ross's error. result = TestSingleInitialSpike( 8000, 32, 42, 2000, 0.1 ); if( result < 0 ) return result; // Medium spike. result = TestSingleInitialSpike( 44100, 40, 190, 3000, 0.5 ); if( result < 0 ) return result; // Spike near sine. //result = TestSingleInitialSpike( 44100, 2900, 140, 3000, 0.1 ); if( result < 0 ) return result; return 0; } #if TEST_SAVED_WAVE /*==========================================================================================*/ /** * Simple test that writes a sawtooth waveform to a file. */ static int TestSavedWave() { int i,j; WAV_Writer writer; int result = 0; #define NUM_SAMPLES (200) short data[NUM_SAMPLES]; short saw = 0; result = Audio_WAV_OpenWriter( &writer, "test_sawtooth.wav", 44100, 1 ); if( result < 0 ) goto error; for( i=0; i<15; i++ ) { for( j=0; j<NUM_SAMPLES; j++ ) { data[j] = saw; saw += 293; } result = Audio_WAV_WriteShorts( &writer, data, NUM_SAMPLES ); if( result < 0 ) goto error; } result = Audio_WAV_CloseWriter( &writer ); if( result < 0 ) goto error; return 0; error: printf("ERROR: result = %d\n", result ); return result; } #endif /* TEST_SAVED_WAVE */ /*==========================================================================================*/ /** * Easy way to generate a sine tone recording. */ void PaQa_FillWithSine( PaQaRecording *recording, double sampleRate, double freq, double amp ) { PaQaSineGenerator generator; float buffer[FRAMES_PER_BLOCK]; int samplesPerFrame = 1; int stride = 1; int done = 0; // Setup a sine oscillator. PaQa_SetupSineGenerator( &generator, freq, amp, sampleRate ); done = 0; while (!done) { PaQa_EraseBuffer( buffer, FRAMES_PER_BLOCK, samplesPerFrame ); PaQa_MixSine( &generator, buffer, FRAMES_PER_BLOCK, stride ); done = PaQa_WriteRecording( recording, buffer, FRAMES_PER_BLOCK, samplesPerFrame ); } } /*==========================================================================================*/ /** * Generate a tone then knock it out using a filter. * Also check using filter slightly off tune to see if some energy gets through. */ static int TestNotchFilter( void ) { int result = 0; PaQaRecording original = { 0 }; PaQaRecording filtered = { 0 }; BiquadFilter notchFilter; double sampleRate = 44100.0; int maxFrames = ((int)sampleRate) * 1; double freq = 234.5; double amp = 0.5; double mag1, mag2, mag3; result = PaQa_InitializeRecording( &original, maxFrames, (int) sampleRate ); QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); PaQa_FillWithSine( &original, sampleRate, freq, amp ); //result = PaQa_SaveRecordingToWaveFile( &original, "original.wav" ); //QA_ASSERT_EQUALS( "PaQa_SaveRecordingToWaveFile failed", 0, result ); mag1 = PaQa_CorrelateSine( &original, freq, sampleRate, 0, original.numFrames, NULL ); QA_ASSERT_CLOSE( "exact frequency match", amp, mag1, 0.01 ); // Filter with exact frequency. result = PaQa_InitializeRecording( &filtered, maxFrames, (int) sampleRate ); QA_ASSERT_EQUALS( "PaQa_InitializeRecording failed", 0, result ); BiquadFilter_SetupNotch( ¬chFilter, freq / sampleRate, 0.5 ); PaQa_FilterRecording( &original, &filtered, ¬chFilter ); result = PaQa_SaveRecordingToWaveFile( &filtered, "filtered1.wav" ); QA_ASSERT_EQUALS( "PaQa_SaveRecordingToWaveFile failed", 0, result ); mag2 = PaQa_CorrelateSine( &filtered, freq, sampleRate, 0, filtered.numFrames, NULL ); QA_ASSERT_CLOSE( "should eliminate tone", 0.0, mag2, 0.01 ); // Filter with mismatched frequency. BiquadFilter_SetupNotch( ¬chFilter, 1.07 * freq / sampleRate, 2.0 ); PaQa_FilterRecording( &original, &filtered, ¬chFilter ); //result = PaQa_SaveRecordingToWaveFile( &filtered, "badfiltered.wav" ); //QA_ASSERT_EQUALS( "PaQa_SaveRecordingToWaveFile failed", 0, result ); mag3 = PaQa_CorrelateSine( &filtered, freq, sampleRate, 0, filtered.numFrames, NULL ); QA_ASSERT_CLOSE( "should eliminate tone", amp*0.26, mag3, 0.01 ); PaQa_TerminateRecording( &original ); PaQa_TerminateRecording( &filtered ); return 0; error: PaQa_TerminateRecording( &original); PaQa_TerminateRecording( &filtered ); return 1; } /*==========================================================================================*/ /** */ int PaQa_TestAnalyzer( void ) { int result; #if TEST_SAVED_WAVE // Write a simple wave file. if ((result = TestSavedWave()) != 0) return result; #endif /* TEST_SAVED_WAVE */ // Generate single tone and verify presence. if ((result = TestSingleMonoTone()) != 0) return result; // Generate prime series of tones and verify presence. if ((result = TestMixedMonoTones()) != 0) return result; // Detect dropped or added samples in a sine wave recording. if ((result = TestDetectPhaseErrors()) != 0) return result; // Test to see if notch filter can knock out the test tone. if ((result = TestNotchFilter()) != 0) return result; // Detect pops that get back in phase. if ((result = TestDetectPops()) != 0) return result; // Test to see if the latency detector can be tricked like it was on Ross' Windows machine. if ((result = TestInitialSpike()) != 0) return result; return 0; }