How to handle input/output types for a signal processing library?
OK - so I have a library that I'm working on. Basically a block signal processing library. It's implemented with a top level 'Block' class and each derived block implements a signal processing operation, e.g., addition is an 'AddBlock', etc.
Until now, I've relied on the inputs and outputs to be an array of double, specifically, the array type is based on the blitz++ library. Everything has been working great.
However, I now want to include the ability to process arrays with complex numbers, particular in the outputs of an 'FFT block' (or alternatively, the inputs of an 'IFFT block').
So my question is: How best to cater for this?
Do I parameterize each class based on the input and output type? (read: the data type stored in the array). Or is there a better way to do this?
Alternatively, should I encapsulate the input/output objects into a separate class to abstract their actual representation开发者_C百科? (This could be a possibility given it's generally easy enough to provide the operation on an abstraction basis, if that makes sense...)
I've thought on this a bit... Quite a bit. Any thoughts?
Thanks in advance.
use templates to 'genericize' the signals' types. your types, block sizes, and much more can be turned into parameters of your class. for example:
template < typename TSignalType >
class t_fir {
public:
//...
void apply(TSignalType* const destination, const TSignalType* const source, const size_t& count) {
// ...
}
private:
TSignalType d_buffer[Size];
};
then just specialize as needed.
this approach is good because it is type safe and very fast/small to execute.
the downside is that it could increase your build times. also, such an implementation is a steep topic, which will take a lot of time to implement if you are new to these concepts.
after reading the comments in more detail: a lot of it depends on your design aims (fastest to execute, simplest to implement?). i'll typically pass one argument to block process, the argument would expose as much detail about the types as you need. then you can specialize process or simply create sub methods (also templates).
illustration:
template < typename T >
class t_array {
public:
/* not certain how you represent a signal... */
typedef T t_element;
t_array() : d_elements() {
}
t_element& operator[](const size_t& idx) { return this->d_elements[idx]; }
const t_element& operator[](const size_t& idx) const { return this->d_elements[idx]; }
private:
std::vector<t_element> d_elements;
private:
/* prohibited */
t_array(const t_array&);
t_array& operator=(const t_array&);
};
typedef t_array<float> t_float_array;
typedef t_array<double> t_double_array;
typedef t_array<std::complex<float> > t_complex_float_array;
typedef t_array<std::complex<double> > t_complex_double_array;
template < typename TInputSignal, typename TOutputSignal >
class t_render_arguments {
public:
typedef TInputSignal t_input_signal;
typedef TOutputSignal t_output_signal;
t_render_arguments(const t_input_signal& inInput, t_output_signal& inOutput, const size_t& inCount) : d_count(inCount), d_input(inInput), d_output(inOutput) {
}
const size_t count() const { return this->d_count; }
const t_input_signal& input() const { return this->d_input; }
t_output_signal& output() { return this->d_output; }
const t_output_signal& output() const { return this->d_output; }
private:
const size_t d_count;
const t_input_signal& d_input;
t_output_signal& d_output;
private:
/* prohibited */
t_render_arguments(const t_render_arguments&);
t_render_arguments& operator=(const t_render_arguments&);
};
class t_block_process_specification {
public:
/* documentation only, as no requirement is defined. all blocks provide this. */
template < typename TRenderArguments >
bool process(TRenderArguments& arguments);
};
// type abides t_block_process_specification
template < typename TArray >
class t_block_add {
public:
t_block_add() : d_array() {} /* << ctor is not what you'd see in practice */
/* specialize as needed */
template < typename TRenderArguments >
bool process(TRenderArguments& arguments) {
/* check preconditions and return false if needed */
const typename TRenderArguments::t_input_signal& input(arguments.input());;
typename TRenderArguments::t_output_signal& output(arguments.output());
const TArray& a(this->d_array);
const typename TRenderArguments::t_input_signal& b(input);
// this is one approach, but some signals/processors will need specialzations
// in addition to specializing process, this type could instead create local
// methods to handle such specializations when mixing modes and signal types
for (size_t idx(0); idx < arguments.count(); ++idx) {
output[idx] = a[idx] + b[idx];
}
return true;
}
private:
TArray d_array;
};
instantiation then takes the general form:
template < typename TSource, typename TDest, typename TProcess >
void TestProcess() {
TSource source;
TDest dest;
const size_t N(1024);
t_render_arguments<TSource,TDest> arguments(source, dest, N);
/* would obviously need a real ctor in real life */
TProcess process;
process.process(arguments);
}
...
TestProcess<t_float_array, t_complex_float_array, t_block_add<t_float_array> >();
TestProcess<t_double_array, t_complex_double_array, t_block_add<t_double_array> >();
TestProcess<t_float_array, t_float_array, t_block_add<t_float_array> >();
TestProcess<t_double_array, t_double_array, t_block_add<t_double_array> >();
this approach allows you to specify precise implementations as needed, based entirely on information and instantiation at compilation.
but you may need to post some specific code and more specific problems for more specific answers.
good luck
精彩评论