Search code examples
cc89

How can I avoid repeating myself in C89 when defining the same function for multiple types?


I am writing a simple dsp library in c89. It is a goal to use this version of the language for portability to older machines. I am unit testing my library and want to measure the amplitude response of each filter. I have created the following method for computing the amplitude response using the FFTW library.

#include "algaec.h"
#include <fftw3.h>

void algae__biquad_compute_amplitude_response(algae__sample_t *amplitude_response,
                                const size_t number_of_bins,
                                algae__biquad_t *filter,
                                const algae__frequency_t sample_rate,
                                const size_t blocksize) {

  algae__sample_block_empty(amplitude_response, blocksize);
  enum complex { RE, IM };
  const size_t N = 2 * number_of_bins;
  fftw_complex *in;
  fftw_complex *out;
  fftw_plan p;

  in = (fftw_complex *)fftw_malloc(sizeof(fftw_complex) * N);
  out = (fftw_complex *)fftw_malloc(sizeof(fftw_complex) * N);
  p = fftw_plan_dft_1d(N, in, out, FFTW_FORWARD, FFTW_ESTIMATE);

  algae__sample_t impulse[blocksize];
  algae__sample_block_empty(impulse, blocksize);
  impulse[0] = 1;
  algae__biquad_process(filter, impulse, impulse, blocksize);
  size_t idx;
  for (idx = 0; idx < N; idx++) {
    in[idx][RE] = impulse[idx];
    in[idx][IM] = 0;
  }

  fftw_execute(p);

  for (idx = 0; idx < number_of_bins; idx++) {
    amplitude_response[idx] =
        (sqrt(out[idx][RE] * out[idx][RE] + out[idx][IM] * out[idx][IM]));
  }

  fftw_destroy_plan(p);
  fftw_free(in);
  fftw_free(out);
}

I also want to test a one pole IIR filter. In order to do that I will now need to define the following:


void algae__onepole_compute_amplitude_response(algae__sample_t *amplitude_response,
                                const size_t number_of_bins,
                                algae__onepole_t *filter,
                                const algae__frequency_t sample_rate,
                                const size_t blocksize) {

  algae__sample_block_empty(amplitude_response, blocksize);
  enum complex { RE, IM };
  const size_t N = 2 * number_of_bins;
  fftw_complex *in;
  fftw_complex *out;
  fftw_plan p;

  in = (fftw_complex *)fftw_malloc(sizeof(fftw_complex) * N);
  out = (fftw_complex *)fftw_malloc(sizeof(fftw_complex) * N);
  p = fftw_plan_dft_1d(N, in, out, FFTW_FORWARD, FFTW_ESTIMATE);

  algae__sample_t impulse[blocksize];
  algae__sample_block_empty(impulse, blocksize);
  impulse[0] = 1;
  algae__onepole_process(filter, impulse, impulse, blocksize);
  size_t idx;
  for (idx = 0; idx < N; idx++) {
    in[idx][RE] = impulse[idx];
    in[idx][IM] = 0;
  }

  fftw_execute(p);

  for (idx = 0; idx < number_of_bins; idx++) {
    amplitude_response[idx] =
        (sqrt(out[idx][RE] * out[idx][RE] + out[idx][IM] * out[idx][IM]));
  }

  fftw_destroy_plan(p);
  fftw_free(in);
  fftw_free(out);
}

Ultimately I will have many more filter types. I will want to test the amplitude response of all of them to ensure that I have implemented them correctly. If I were using c++, I could use templates to make both algae__compute_amplitude_response and algae__process generic with respect to the type of filter and have the compiler generate the right implementation automatically while having only one code path I have to maintain. Is there any way to avoid repeating myself here?

So far I have researched the following options. None of them seem appealing...

  1. I use preprocessor macros to define a generic process method and a generic compute_amplitude_response method... I'm not entirely clear on how this would work and worried about what it would do to the readability of my production code.
  2. I create a tagged union of my different filter structs and create a process method that identifies the type of the struct and then redirects to the proper method... This would affect the structure of my production code for the sake of testing (not necessarily a deal-breaker but a bit sad). It would also affect the implementation and performance of my production code as the union-ed struct would take up the same amount of memory as its largest member. For a onepole filter vs a biquad this is the different between 2 floats and 9 floats.
  3. something scary with void pointers??? Perhaps defining a struct that is the combination of a void pointer and a type to tag it. This one feels like opening a can worms and could make my production API very confusing.

Are there any options I am missing here?-


Solution

  • You can do this with function pointers, but first you need to make sure the functions in question have the same signature type.

    First, change algae__onepole_process and algae__biquad_process so that the first parameter to each has type void *, i.e.:

    void algae__onepole_process(void *filter, algae__sample_t *impulse1, 
                                algae__sample_t *impulse2, size_t blocksize);
    void algae__biquad_process(void *filter, algae__sample_t *impulse1, 
                               algae__sample_t *impulse2, size_t blocksize);
    

    This means that you'll need to copy the filter parameter in each to a pointer of the appropriate type.

    Then you create a single function to do what both of the above do, changing the type of type of the filter parameter to void * and adding a function pointer for the process function you want to call:

    void algae__generic_compute_amplitude_response(algae__sample_t *amplitude_response,
                const size_t number_of_bins,
                void (*process)(void *, algae__sample_t *, algae__sample_t *, size_t),
                void *filter,
                const algae__frequency_t sample_rate,
                const size_t blocksize) {
    

    Which will then use the function pointer instead of the specific function:

    process(filter, impulse, impulse, blocksize);