Operations
Next: Buffer as Connections Up: C++ Environment for Synchronos Previous: The Synchronous Data-flow Concept Contents
Subsections
Operations
An operation is a basic computation in a data flow graph and realized as a C++-classes which has as parent always the basic virtual class SDF-Operation.SDFOperation has virtual functions which has to be implemented by the class and inheritance of classes are recommended. The only function which always has to be provided is operate() . This is executed every schedule interval.
The inputs and/or outputs of an operation has to be declared within the Constructor. The initialize() function, which is called after forking the dsp process and the allocation of memory used for the operation should be done within this function.
Constructor, Destructor
Each operation is inherits SDFOperation and has its own constructor for instantiation and if needed a destructor.
The constructor is used to initialize variables, which can be passed by arguments, and doing some error checking for these. Also shared memory should be initialized or allocated by the constructor if not defined as variable in the class. It should not allocate much memory used for processing, since this memory will not be optimized in future releases and there is a limit on each. As an example an operation summing N inputs could look like:
class Sum : public SDFOperation { int N; ... public: shared<int> overun; Sum(int number_of_inputs = 2) : SDFOperation(){ if(number_of_inputs > 2) N = number_of_inputs; else N = 2; overun=0; set_name("Sum"); for(int i=0; i < N, i++) add_inbuffer(new SDFBuffer<SDF_sample>); add_outbuffer(new SDFBuffer<SDF_sample>); return; } ... ~Sum() {} };
The in-buffers for the operations are allocated in initialize() .
initialize, exitialize
initialize is the routine done after Construction in the forked process before starting dispatching, when added to the SDF-Graph, where new memory or file operations for processing is needed. This makes sense if you think about having parallel processes running on other processors, operation can be optimized, eg. memory of operations can be collected so that they fit in a cache or if the process is forked, then memory is doubled only for the subprocess and shared memory can be handled by different concepts. Therefore no shared memory should be allocated in this function, it should be done by the Constructor.
Here are all memory space allocated, for example:
SDF_sample *table; ... public: initialize() { data = new SDF_sample[TABLESIZE]; if(data == NULL) return SDF_FAIL; return SDF_SUCCESS; } exitialize() { if(data != NULL)delete[] data; return SDF_SUCCESS; }
With exitialize all allocated memory should be released. In/Out buffers are released automatically, but can be released here too.
The return value on success is SDF_SUCCESS if memory allocation changed or SDF_NOT_CHANGED if no memory allocation and SDF_FAIL on fail.
Assigning In/Out-Buffers
This is done with new SDFBuffer<T> where the function add_in_buffer(SDFBuffer *buf) adds an input and add_out_buffer(SDFBuffer *buf) adds an output.
The type of the buffer is defined with Template T, where most useful type is SDF_sample, which is the default floating point unit for on sample. Since the whole system is build on floating point processing, because it is fast (often better than integer because of parallel floating point units), and needs less computation because of scaling a floating point is used for sample, where the type of this (float or double) is defined in sdf_common and can be changed before compile time.
Other types can be used if there is at least one object which converts them or generates it as source. Only object with same type of Buffers can/should be connected. Its up to the operation to define this.
Signals are discussed later in section 4 and examples see above.
Allocating shared memory
Shared memory is used to communicate between dsp process as a child process and main process as a parent. Since there is a possibility to use clone() instead of fork the shared memory is handled by a abstract class SDFshared<T> which uses one shared memory block for all operations and assigns part of it to SDFshared. So it has not to be real shared memory at all.
It is preferred to use real shared memory and fork, to have real data protection between processes, but sometimes if large memory is needed, clone is better and all memory is shared. So do not use not shared memory in DSP Process thinking it does not affect parent process, if operations should be use in future for different projects.
There is a plan to do a FIFO as shared in future for large data interaction, but not implemented yet.
class MYOP: class SDFOperation { ... shared<float> freqency; public: ... // in operate: frequency = 1.0/cycletime; ... float get_frequency(){return frequency;} }; // class MYOP MYOP *dummy; ... printf("freq=%f\n", dummy->get_frequency()); ...
adjust
This function is called after scheduling and initialize is done and every time, the number of tokens, the buffer-Len or the sample-rate is changed between start() and stop(). This is important, because calculation parameter may be changed when sample-rate is changed or also sample tables etc.
With this function the system can play to optimize the performance. So it should be used for assigning local variables to be used for fast access within operate(). As an example an adjust function for an sinus oscillator is shown:
int adjust() { // get outbuffer vars blen = outbuffer(0)->size(); out = outbuffer(0)->data(); advfac = (float) SDF_SINUS_TABLE_SIZE /(float) outbuffer(0)->get_samplerate(); return SDF_SUCESS; }
start, stop
These functions are called before and after the periodic call of the operate function and when Audio input or output is enabled.
If you write a function like oscillator you need not to do here anything, because there should not be an interruption of the wave when starting again, since this would be an synchronization which can lead to wrong results. Output is muted at AD or DA operation.
The main thing is to restart timers with start() which had not been updated after stop, since no operate() had been called and to store timers with stop() for next start.
You can check here if some shared variable or anything like that has changed, to avoid big computation at the first operate() cycle for example at an input floating point operation:
class SDFFloat: public SDFOperation{ SDFshared<float> val; ... public: ... start() { int i = blen; if(oldval != val) while(i--) out[i] = val; oldval = val; // remeber return 0; } // int stop() not needed ... };
operate
In this function the signal processing is done. So each input token can be accessed and the output should be calculated. Therefore efficient algorithm should be used and this function should not take more calculation time than provided by the performance dates.
If for example the buffer size is 64 tokens by a sample rate of 32000 Hz, then all operate() functions has to be performed in 2 ms time, not to overload the performance of the computer and risking sample loss or dead lock. Because of the FIFOS for Output or Input of samples there is a variance of processing time, but it is better that this function provides a constant time limit.
As an example the operate of an addition of two signals is shown
class SDFAdd2 : public SDFOperation { ... public: ... int operate() { register int i = blen; // set in adjust register SDF_sample *in1 = *(inbuffer(0)->data()); register SDF_sample *in2 = *(inbuffer(1)->data()); register SDF_sample *out = *(inbuffer(1)->data()); while(i--) *out++ = *in1++ + *in2++; } ... };
read, write
With these functions the actual parameters can be read from the object and written to the object as ASCII code. This could be used if the SDF-graph should be stored or reinitialized by a preset. The Application has to take care if and how o use this data.
The basic class SDFOperation does nothing, so these function can be overridden by the operation if there is something to store or load in ASCII code. Do not use it for large data like samples, instead just store the filename and the sample data in an extra file.
Example reading, writing for frequency parameter:
... float freq; ... istream& read(istream &is) { is >> freq; return is; } ostream& write(ostream &os) { freq << os; return os; } ...
Next: Buffer as Connections Up: C++ Environment for Synchronos Previous: The Synchronous Data-flow Concept Contents HAss.DI Winfried Ritsch - ritsch@algo.mur.at