diff trunk/carfac/carfac_test.cc @ 678:7f424c1a8b78

Fifth revision of Alex Brandmeyer's C++ implementation of CARFAC. Moved output structure to deque<vector<FloatArray>, moved coefficient Design methods to CARFAC object, moved tests into carfac_test.cc. Verified binaural output against Matlab using two tests. Added CARFAC_Compare_CPP_Test_Data to plot NAP output of C++ version against Matlab version. Verified build and test success on OS X using SCons with g++ 4.7 (std=c++11).
author alexbrandmeyer
date Mon, 27 May 2013 16:36:54 +0000
parents a9694d0bb55a
children 594b410c2aed
line wrap: on
line diff
--- a/trunk/carfac/carfac_test.cc	Fri May 24 22:38:09 2013 +0000
+++ b/trunk/carfac/carfac_test.cc	Mon May 27 16:36:54 2013 +0000
@@ -20,14 +20,45 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "carfac_test.h"
+#include <string>
+#include <fstream>
+// GoogleTest is now included for running unit tests
+#include <gtest/gtest.h>
+#include "carfac.h"
+using std::vector;
+using std::string;
+using std::ifstream;
+using std::ofstream;
+
+#define TEST_SRC_DIR "./test_data/"
+// Here we specify the level to which the output should match (7 decimals).
+#define PRECISION_LEVEL 1.0e-07
 
 // Three helper functions are defined here for loading the test data generated
 // by the Matlab version of CARFAC.
 // This loads one-dimensional FloatArrays from single-column text files.
+void WriteNAPOutput(CARFACOutput output, const string filename, int ear) {
+  string fullfile = TEST_SRC_DIR + filename;
+  ofstream ofile(fullfile.c_str());
+  int32_t n_timepoints = output.nap_.size();
+  int channels = output.nap_[0][0].size();
+  if (ofile.is_open()) {
+    for (int32_t i = 0; i < n_timepoints; ++i) {
+      for (int j = 0; j < channels; ++j) {
+        ofile << output.nap(ear, i, j);
+        if ( j < channels - 1) {
+          ofile << " ";
+        }
+      }
+      ofile << "\n";
+    }
+  }
+  ofile.close();
+}
+
 FloatArray LoadTestData(const std::string filename, const int number_points) {
-  std::string fullfile = TEST_SRC_DIR + filename;
-  std::ifstream file(fullfile.c_str());
+  string fullfile = TEST_SRC_DIR + filename;
+  ifstream file(fullfile.c_str());
   FPType myarray[number_points];
   FloatArray output(number_points);
   if (file.is_open()) {
@@ -36,16 +67,17 @@
       output(i) = myarray[i];
     }
   }
+  file.close();
   return output;
 }
 
-// This loads two-dimensional FloatArrays from multi-column text files.
-std::vector<FloatArray> Load2dTestData(const std::string filename, const int rows,
+// This loads a vector of FloatArrays from multi-column text files.
+vector<FloatArray> Load2dTestData(const string filename, const int rows,
                             const int columns) {
-  std::string fullfile = TEST_SRC_DIR + filename;
-  std::ifstream file(fullfile.c_str());
+  string fullfile = TEST_SRC_DIR + filename;
+  ifstream file(fullfile.c_str());
   FPType myarray[rows][columns];
-  std::vector<FloatArray> output;
+  vector<FloatArray> output;
   output.resize(rows);
   for (auto& timepoint : output) {
     timepoint.resize(columns);
@@ -58,17 +90,17 @@
       }
     }
   }
+  file.close();
   return output;
 }
 
 // This loads two dimensional vectors of audio data using data generated in
 // Matlab using the wavread() function.
-std::vector<std::vector<float>> Load2dAudioVector(std::string filename,
-                                                  int timepoints,
-                                                  int channels) {
-  std::string fullfile = TEST_SRC_DIR + filename;
-  std::ifstream file(fullfile.c_str());
-  std::vector<std::vector<float>> output;
+vector<vector<float>> Load2dAudioVector(string filename, int timepoints,
+                                        int channels) {
+  string fullfile = TEST_SRC_DIR + filename;
+  ifstream file(fullfile.c_str());
+  vector<vector<float>> output;
   output.resize(channels);
   for (auto& channel : output) {
     channel.resize(timepoints);
@@ -80,291 +112,128 @@
       }
     }
   }
+  file.close();
   return output;
 }
 
-// The first test verifies that the resulting CAR coefficients are the same as
-// in Matlab when using the default CAR parameter set.
-TEST(CARFACTest, CARCoeffs_Test){
-  // These initialze the CAR Params and Coeffs objects needed for this test.
+TEST(CARFACTest, Binaural_Output_test) {
+  int n_timepoints = 882;
+  int n_channels = 71;
+  int n_ears = 2;
+  std::string filename = "binaural_test_nap1.txt";
+  std::vector<FloatArray> nap1 = Load2dTestData(filename, n_timepoints,
+                                                n_channels);
+  filename = "binaural_test_bm1.txt";
+  std::vector<FloatArray> bm1 = Load2dTestData(filename, n_timepoints,
+                                               n_channels);
+  filename = "binaural_test_nap2.txt";
+  std::vector<FloatArray> nap2 = Load2dTestData(filename, n_timepoints,
+                                                n_channels);
+  filename = "binaural_test_bm2.txt";
+  std::vector<FloatArray> bm2 = Load2dTestData(filename, n_timepoints,
+                                               n_channels);
+  filename = "file_signal_binaural_test.txt";
+  std::vector<std::vector<float>> sound_data = Load2dAudioVector(filename,
+                                                                 n_timepoints,
+                                                                 n_ears);
   CARParams car_params;
-  CARCoeffs car_coeffs;
-  FPType fs = 22050.0;
-  // We calculate the pole frequencies and number of channels in the same way
-  // as in the CARFAC 'Design' method.
-  int n_ch = 0;
-  FPType pole_hz = car_params.first_pole_theta_ * fs / (2 * PI);
-  while (pole_hz > car_params.min_pole_hz_) {
-    n_ch++;
-    pole_hz = pole_hz - car_params.erb_per_step_ *
-    ERBHz(pole_hz, car_params.erb_break_freq_, car_params.erb_q_);
+  IHCParams ihc_params;
+  AGCParams agc_params;
+  CARFAC mycf;
+  mycf.Design(n_ears, 22050, car_params, ihc_params,
+              agc_params);
+  CARFACOutput my_output;
+  my_output.Init(n_ears, true, false, true, false, false);
+  mycf.Run(sound_data, &my_output);
+  filename = "cpp_nap_output_1_binaural_test.txt";
+  WriteNAPOutput(my_output, filename, 0);
+  filename = "cpp_nap_output_2_binaural_test.txt";
+  WriteNAPOutput(my_output, filename, 1);
+  int ear = 0;
+  int n_ch = 71;
+  for (int timepoint = 0; timepoint < n_timepoints; ++timepoint) {
+    for (int channel = 0; channel < n_ch; ++channel) {
+      FPType cplusplus = my_output.nap(ear, timepoint, channel);
+      FPType matlab = nap1[timepoint](channel);
+      ASSERT_NEAR(cplusplus, matlab, PRECISION_LEVEL);
+      cplusplus = my_output.bm(ear, timepoint, channel);
+      matlab = bm1[timepoint](channel);
+      ASSERT_NEAR(cplusplus, matlab, PRECISION_LEVEL);
+    }
   }
-  FloatArray pole_freqs(n_ch);
-  pole_hz = car_params.first_pole_theta_ * fs / (2 * PI);
-  for (int ch = 0; ch < n_ch; ++ch) {
-    pole_freqs(ch) = pole_hz;
-    pole_hz = pole_hz - car_params.erb_per_step_ *
-    ERBHz(pole_hz, car_params.erb_break_freq_, car_params.erb_q_);
-  }
-  // This initializes the CAR coeffecients object and runs the design method.
-  car_coeffs.Design(car_params, 22050, pole_freqs);
-  // Now we go through each set of coefficients to verify that the values are
-  // the same as in MATLAB.
-  std::string filename;
-  FloatArray output;
-  
-  ASSERT_EQ(car_coeffs.v_offset_, 0.04);
-  ASSERT_EQ(car_coeffs.velocity_scale_, 0.1);
-  
-  filename = "r1_coeffs.txt";
-  output = LoadTestData(filename, n_ch);
-  for (int i = 0; i < n_ch; ++i) {
-    ASSERT_NEAR(output(i), car_coeffs.r1_coeffs_(i), PRECISION_LEVEL);
-  }
-  
-  filename = "a0_coeffs.txt";
-  output = LoadTestData(filename, n_ch);
-  for (int i = 0; i < n_ch; ++i) {
-    ASSERT_NEAR(output(i), car_coeffs.a0_coeffs_(i), PRECISION_LEVEL);
-  }
-  
-  filename = "c0_coeffs.txt";
-  output = LoadTestData(filename, n_ch);
-  for (int i = 0; i < n_ch; ++i) {
-    ASSERT_NEAR(output(i), car_coeffs.c0_coeffs_(i), PRECISION_LEVEL);
-  }
-  
-  filename = "zr_coeffs.txt";
-  output = LoadTestData(filename, n_ch);
-  for (int i = 0; i < n_ch; ++i) {
-    ASSERT_NEAR(output(i), car_coeffs.zr_coeffs_(i), PRECISION_LEVEL);
-  }
-  
-  filename = "h_coeffs.txt";
-  output = LoadTestData(filename, n_ch);
-  for (int i = 0; i < n_ch; ++i) {
-    ASSERT_NEAR(output(i), car_coeffs.h_coeffs_(i), PRECISION_LEVEL);
-  }
-  
-  filename = "g0_coeffs.txt";
-  output = LoadTestData(filename, n_ch);
-  for (int i = 0; i < n_ch; ++i) {
-    ASSERT_NEAR(output(i), car_coeffs.g0_coeffs_(i), PRECISION_LEVEL);
-  }
-}
-
-// The second test verifies that the IHC coefficient calculations result in the
-// same set of values as in the Matlab version of the CARFAC.
-TEST(CARFACTest, IHCCoeffs_Test){
-  IHCParams ihc_params;
-  IHCCoeffs ihc_coeffs;
-  FPType fs = 22050.0;
-  ihc_coeffs.Design(ihc_params, fs);
-  
-  std::string filename = "ihc_coeffs.txt";
-  FloatArray output = LoadTestData(filename, 9);
-  
-  // The sequence of the individual coefficients is determined using the
-  // CARFAC_GenerateTestData() function in the Matlab version, with all of the
-  // parameters placed in a single output file for convenience.
-  bool just_hwr = output(0);
-  FPType lpf_coeff = output(1);
-  FPType out_rate = output(2);
-  FPType in_rate = output(3);
-  bool one_cap = output(4);
-  FPType output_gain = output(5);
-  FPType rest_output = output(6);
-  FPType rest_cap = output(7);
-  FPType ac_coeff = output(8);
-  
-  // Once we have the Matlab values initialized, we can compare them to the
-  // output of the IHCCoeffs 'Design' method.
-  ASSERT_EQ(just_hwr, ihc_coeffs.just_hwr_);
-  ASSERT_NEAR(lpf_coeff, ihc_coeffs.lpf_coeff_, PRECISION_LEVEL);
-  ASSERT_NEAR(out_rate, ihc_coeffs.out1_rate_, PRECISION_LEVEL);
-  ASSERT_NEAR(in_rate, ihc_coeffs.in1_rate_, PRECISION_LEVEL);
-  ASSERT_EQ(one_cap, ihc_coeffs.one_cap_);
-  ASSERT_NEAR(output_gain, ihc_coeffs.output_gain_, PRECISION_LEVEL);
-  ASSERT_NEAR(rest_output, ihc_coeffs.rest_output_, PRECISION_LEVEL);
-  ASSERT_NEAR(rest_cap, ihc_coeffs.rest_cap1_, PRECISION_LEVEL);
-  ASSERT_NEAR(ac_coeff, ihc_coeffs.ac_coeff_, PRECISION_LEVEL);
-}
-
-
-TEST(CARFACTest, AGCCoeffs_Test) {
-  AGCParams agc_params;
-  std::vector<AGCCoeffs> agc_coeffs;
-  std::vector<FloatArray> output;
-  output.resize(agc_params.n_stages_);
-  std::string filename = "agc_coeffs_1.txt";
-  output[0] = LoadTestData(filename, 14);
-  filename = "agc_coeffs_2.txt";
-  output[1] = LoadTestData(filename, 14);
-  filename = "agc_coeffs_3.txt";
-  output[2] = LoadTestData(filename, 14);
-  filename = "agc_coeffs_4.txt";
-  output[3] = LoadTestData(filename, 14);
-  agc_coeffs.resize(agc_params.n_stages_);
-  // We initialize the AGC stages in the same was as in Ear::Init.
-  FPType fs = 22050.0;
-  FPType previous_stage_gain = 0.0;
-  FPType decim = 1.0;
-  for (int stage = 0; stage < agc_params.n_stages_; ++stage) {
-    agc_coeffs[stage].Design(agc_params, stage, fs, previous_stage_gain, decim);
-    previous_stage_gain = agc_coeffs[stage].agc_gain_;
-    decim = agc_coeffs[stage].decim_;
-  }
-  // Now we run through the individual coefficients and verify that they're the
-  // same as in Matlab.
-  for (int stage = 0; stage < agc_params.n_stages_; ++stage) {
-    int n_agc_stages = output[stage](1);
-    FPType agc_stage_gain = output[stage](2);
-    int decimation = output[stage](3);
-    FPType agc_epsilon = output[stage](4);
-    FPType agc_polez1 = output[stage](5);
-    FPType agc_polez2 = output[stage](6);
-    int agc_spatial_iterations = output[stage](7);
-    FPType agc_spatial_fir_1 = output[stage](8);
-    FPType agc_spatial_fir_2 = output[stage](9);
-    FPType agc_spatial_fir_3 = output[stage](10);
-    int agc_spatial_n_taps = output[stage](11);
-    FPType agc_mix_coeffs = output[stage](12);
-    FPType detect_scale = output[stage](13);
-    
-    ASSERT_EQ(n_agc_stages, agc_coeffs[stage].n_agc_stages_);
-    ASSERT_NEAR(agc_stage_gain, agc_coeffs[stage].agc_stage_gain_,
-                PRECISION_LEVEL);
-    ASSERT_EQ(decimation, agc_coeffs[stage].decimation_);
-    ASSERT_NEAR(agc_epsilon, agc_coeffs[stage].agc_epsilon_, PRECISION_LEVEL);
-    ASSERT_NEAR(agc_polez1, agc_coeffs[stage].agc_pole_z1_, PRECISION_LEVEL);
-    ASSERT_NEAR(agc_polez2, agc_coeffs[stage].agc_pole_z2_, PRECISION_LEVEL);
-    ASSERT_EQ(agc_spatial_iterations,
-              agc_coeffs[stage].agc_spatial_iterations_);
-    ASSERT_NEAR(agc_spatial_fir_1, agc_coeffs[stage].agc_spatial_fir_[0],
-                PRECISION_LEVEL);
-    ASSERT_NEAR(agc_spatial_fir_2, agc_coeffs[stage].agc_spatial_fir_[1],
-                PRECISION_LEVEL);
-    ASSERT_EQ(agc_spatial_n_taps,
-              agc_coeffs[stage].agc_spatial_n_taps_);
-    ASSERT_NEAR(agc_spatial_fir_3, agc_coeffs[stage].agc_spatial_fir_[2],
-                PRECISION_LEVEL);
-    ASSERT_NEAR(agc_mix_coeffs, agc_coeffs[stage].agc_mix_coeffs_,
-                PRECISION_LEVEL);
-    
-    // The last stage will have the correct detect_scale_ value on the basis of
-    // the total gain accumlated over the stages.
-    if (stage == agc_params.n_stages_ - 1) {
-      ASSERT_NEAR(detect_scale, agc_coeffs[stage].detect_scale_,
-                  PRECISION_LEVEL);
+  ear = 1;
+  for (int timepoint = 0; timepoint < n_timepoints; ++timepoint) {
+    for (int channel = 0; channel < n_ch; ++channel) {
+      FPType cplusplus = my_output.nap(ear, timepoint, channel);
+      FPType matlab = nap2[timepoint](channel);
+      ASSERT_NEAR(cplusplus, matlab, PRECISION_LEVEL);
+      cplusplus = my_output.bm(ear, timepoint, channel);
+      matlab = bm2[timepoint](channel);
+      ASSERT_NEAR(cplusplus, matlab, PRECISION_LEVEL);
     }
   }
 }
 
-// This test verifies the output of the C++ code relative to that of the Matlab
-// version using a single segment (441 samples) of audio from the "plan.wav"
-// file. The single-channel audio data and different output matrices from Matlab
-// are stored in text files and then read into 2d Eigen arrays (for now, this
-// should be changed to a vector of FloatArrays... TODO (alexbrandmeyer)). For
-// reference, see the CARFAC_GenerateTestData() function in the Matlab branch
-// of the repository.
-//
-// A single Ear object is used along with the code from CARFAC.RunSegment() to
-// evaluate the output of the CAR and IHC steps on a sample by sample basis
-// relative to the output read in from Matlab. The test passes with 11 degrees
-// of precision, with the Matlab data stored using 12 decimals.
-//
-// TODO (alexbrandmeyer): A subseqent version of this test will operate directly
-// on the CARFACOutput structure and will evaluate binaural data.
-TEST(CARFACTest, Monaural_Output_Test) {
-  std::string filename = "monaural_test_nap.txt";
-  std::vector<FloatArray> nap = Load2dTestData(filename, 441, 71);
-  filename = "monaural_test_bm.txt";
-  std::vector<FloatArray> bm = Load2dTestData(filename, 441, 71);
-  filename = "monaural_test_ohc.txt";
-  std::vector<FloatArray> ohc = Load2dTestData(filename, 441, 71);
-  filename = "monaural_test_agc.txt";
-  std::vector<FloatArray> agc = Load2dTestData(filename, 441, 71);
-  filename = "file_signal_monaural_test.txt";
-  std::vector<std::vector<float>> sound_data = Load2dAudioVector(filename, 441,
-                                                                 1);
-  // The number of timepoints is determined from the length of the audio
-  // segment.
-  int32_t n_timepoints = sound_data[0].size();
-  
+TEST(CARFACTest, Long_Output_test) {
+  int n_timepoints = 2000;
+  int n_channels = 83;
+  int n_ears = 2;
+  string filename = "long_test_nap1.txt";
+  vector<FloatArray> nap1 = Load2dTestData(filename, n_timepoints,
+                                                n_channels);
+  filename = "long_test_bm1.txt";
+  vector<FloatArray> bm1 = Load2dTestData(filename, n_timepoints,
+                                               n_channels);
+  filename = "long_test_nap2.txt";
+  vector<FloatArray> nap2 = Load2dTestData(filename, n_timepoints,
+                                                n_channels);
+  filename = "long_test_bm2.txt";
+  vector<FloatArray> bm2 = Load2dTestData(filename, n_timepoints,
+                                               n_channels);
+  filename = "file_signal_long_test.txt";
+  vector<vector<float>> sound_data = Load2dAudioVector(filename,
+                                                                 n_timepoints,
+                                                                 n_ears);
   CARParams car_params;
   IHCParams ihc_params;
   AGCParams agc_params;
-  FPType fs = 22050.0;
-  int n_ch = 0;
-  FPType pole_hz = car_params.first_pole_theta_ * fs / (2 * PI);
-  while (pole_hz > car_params.min_pole_hz_) {
-    n_ch++;
-    pole_hz = pole_hz - car_params.erb_per_step_ *
-    ERBHz(pole_hz, car_params.erb_break_freq_, car_params.erb_q_);
+  CARFAC mycf;
+  mycf.Design(n_ears, 44100, car_params, ihc_params,
+              agc_params);
+  CARFACOutput my_output;
+  my_output.Init(n_ears, true, false, true, false, false);
+  mycf.Run(sound_data, &my_output);
+  filename = "cpp_nap_output_1_long_test.txt";
+  WriteNAPOutput(my_output, filename, 0);
+  filename = "cpp_nap_output_2_long_test.txt";
+  WriteNAPOutput(my_output, filename, 1);
+  int ear = 0;
+  for (int timepoint = 0; timepoint < n_timepoints; ++timepoint) {
+    for (int channel = 0; channel < n_channels; ++channel) {
+      FPType cplusplus = my_output.nap(ear, timepoint, channel);
+      FPType matlab = nap1[timepoint](channel);
+      ASSERT_NEAR(cplusplus, matlab, PRECISION_LEVEL);
+      cplusplus = my_output.bm(ear, timepoint, channel);
+      matlab = bm1[timepoint](channel);
+      ASSERT_NEAR(cplusplus, matlab, PRECISION_LEVEL);
+    }
   }
-  FloatArray pole_freqs(n_ch);
-  pole_hz = car_params.first_pole_theta_ * fs / (2 * PI);
-  for (int ch = 0; ch < n_ch; ++ch) {
-    pole_freqs(ch) = pole_hz;
-    pole_hz = pole_hz - car_params.erb_per_step_ *
-    ERBHz(pole_hz, car_params.erb_break_freq_, car_params.erb_q_);
-  }
-  
-  // This initializes the CARFAC object and runs the design method.
-  Ear ear;
-  ear.InitEar(n_ch, fs, pole_freqs, car_params, ihc_params, agc_params);
-  
-  CARFACOutput seg_output;
-  seg_output.InitOutput(1, n_ch, n_timepoints);
-  
-  // A nested loop structure is used to iterate through the individual samples
-  // for each ear (audio channel).
-  FloatArray car_out(n_ch);
-  FloatArray ihc_out(n_ch);
-  FloatArray matlab_car_out(n_ch);
-  FloatArray matlab_ihc_out(n_ch);
-  bool updated;  // This variable is used by the AGC stage.
-  for (int32_t i = 0; i < n_timepoints; ++i) {
-    int j = 0;
-    // First we create a reference to the current Ear object.
-    // This stores the audio sample currently being processed.
-    FPType input = sound_data[j][i];
-    // Now we apply the three stages of the model in sequence to the current
-    // audio sample.
-    ear.CARStep(input, &car_out);
-    matlab_car_out = bm[i];
-    // This step verifies that the ouput of the CAR step is the same at each
-    // timepoint and channel as that of the Matlab version.
-    for (int channel = 0; channel < n_ch; ++channel) {
-      FPType a = matlab_car_out(channel);
-      FPType b = car_out(channel);
-      ASSERT_NEAR(a, b, PRECISION_LEVEL);
-    }
-    ear.IHCStep(car_out, &ihc_out);
-    matlab_ihc_out = nap[i];
-    // This step verifies that the ouput of the IHC step is the same at each
-    // timepoint and channel as that of the Matlab version.
-    for (int channel = 0; channel < n_ch; ++channel) {
-      FPType a = matlab_ihc_out(channel);
-      FPType b = ihc_out(channel);
-      ASSERT_NEAR(a, b, PRECISION_LEVEL);
-    }
-    
-    updated = ear.AGCStep(ihc_out);
-    // These lines assign the output of the model for the current sample
-    // to the appropriate data members of the current ear in the output
-    // object.
-    seg_output.StoreNAPOutput(i, j, ihc_out);
-    seg_output.StoreBMOutput(i, j, car_out);
-    seg_output.StoreOHCOutput(i, j, ear.za_memory());
-    seg_output.StoreAGCOutput(i, j, ear.zb_memory());
-    if (updated) {
-      FloatArray undamping = 1 - ear.agc_memory(0);
-      // This updates the target stage gain for the new damping.
-      ear.set_dzb_memory((ear.zr_coeffs() * undamping - ear.zb_memory()) /
-                         ear.agc_decimation(0));
-      ear.set_dg_memory((ear.StageGValue(undamping) - ear.g_memory()) /
-                        ear.agc_decimation(0));
+  ear = 1;
+  for (int timepoint = 0; timepoint < n_timepoints; ++timepoint) {
+    for (int channel = 0; channel < n_channels; ++channel) {
+      FPType cplusplus = my_output.nap(ear, timepoint, channel);
+      FPType matlab = nap2[timepoint](channel);
+      ASSERT_NEAR(cplusplus, matlab, PRECISION_LEVEL);
+      cplusplus = my_output.bm(ear, timepoint, channel);
+      matlab = bm2[timepoint](channel);
+      ASSERT_NEAR(cplusplus, matlab, PRECISION_LEVEL);
     }
   }
 }
+
+int main(int argc, char **argv) {
+  // This initializes the GoogleTest unit testing framework.
+  ::testing::InitGoogleTest(&argc, argv);
+  // This runs all of the tests that we've defined above.
+  return RUN_ALL_TESTS();
+}
\ No newline at end of file