thestk / rtaudio

A set of C++ classes that provide a common API for realtime audio input/output across Linux (native ALSA, JACK, PulseAudio and OSS), Macintosh OS X (CoreAudio and JACK), and Windows (DirectSound, ASIO, and WASAPI) operating systems.
Other
1.49k stars 317 forks source link

Using a Base Class Instead of Callback Function #95

Closed ghost closed 6 years ago

ghost commented 7 years ago

In the call to openStream, a C-style callback function is used.

While this is sufficient, I think there's a more scalable C++ solution.

How about using a base class kind of like this:

class Processor {
public:
    Processor(void);
    virtual ~Processor(void);
    virtual void callback(void) = 0;
    void *getOutputBuffer(void);
    void *getInputBuffer(void);
    unsigned int getFrames(void);
    double getStreamTime(void);
    RtAudioStreamStatus getStreamStatus(void);
};

It may be set in the RtAudio class like:

void RtAudio::setProcessor(Processor *p) {
    if (p != nullptr)
        this->processor = p;
}

Client code can define base classes like this:

class LowPassFilter final : public RtAudio::Processor {
    double frequency;
public:
    void callback(void);
    void setFrequency(double f);
};

And use them like this:

LowPassFilter lpass;
lpass.setFrequency(440.0);

RtAudio audio;
audio.setProcessor(lpass);

A couple of built-in processors may be defined in later versions:

// Contains multiple sub-processors in a vector,
// calls all callbacks of the sub-processors.
class CompositeProcessor;
// Copies input data to a file.
class FileSaver;
// Uses a LADSPA descriptor for processing.
class LadspaProcessor;
// Uses a LV2 descriptor for processing.
class LV2Processor;
// Copies input audio to output audio.
class NullProcessor;

For backwards compatibility, you another one of these "built-in" processors may just contain a pointer and function, mimicking the old behavior.

class CallbackProcessor : public RtAudio::Processor {
    void *user_data;
    void (*process)(void *output, void *input, /* etc */);
public:
    void callback(void) {
        process(output, input, /* etc */, user_data);
    }
};

Two openStream signatures will be defined. One will be identical to the existing one now, and it will create this CallbackProcessor using the RtAudioCallback function and user data, and then call a setProcessor function using the CallbackProcessor. The second openStream signature will not modify the RtAudioProcessor member at all.

ghost commented 7 years ago

In the latest commit of my copy, I was able to add this feature. You'll notice that all the tests are only slightly different. I used a class called CStyleProcessor that is used for compatibility, to minimize the amount of changes I had to make to the tests. They now all have a snippet of code sort of like this:

RtAudio::CStyleProcessor processor;
processor.setUserData( &mydata );
processor.setCallback( inout );
adc.setProcessor( processor );

I'm not planning on submitting a PR from my copy. There are too many breaking changes. But you could check it out as a proof-of-concept for the issue. Just note that in the commit I referenced, I haven't updated any of the Windows or Apple APIs.

radarsat1 commented 7 years ago

I think the more interesting approach in the future (we'll probably open an "experimental" branch after the next release..) is to add a templated interface that can call anything that responds to the operator(), which would include functor classes (like your idea), lambdas, and regular callbacks.

ghost commented 7 years ago

@radarsat1 How exactly would you use the template class?

Do you mean using it like this?

template <typename Callable>
int callback(Callable c) {
  c(inputBuffer, outputBuffer, frames, /* etc */ );
}

If that's what you mean, you could derive a Processor class that uses templates.

Like this:

template <typename Callable>
class CallableProcessor final : public RtAudio::Processor {
  Callable c;
public:
  CallableProcessor(void) { }
  CallablePocessor(const Callable &other) : c(other) { }
  int process() {
    c(getInputBuffer(),
      getOutputBuffer(),
      getFrames(),
      /* etc */ );
  }
};

And the reason you'd make it a derived class is to keep the implementation of all the APIs in the source file and not the header file.

If you want a lamda to be the callback, you could declare a function like this:

class RtAudio {
  template <typename Callable>
  void setProcessor(Callable c) {
    CallableProcessor processor(c);
    setProcessor(processor);
  };
};

Example usage:

RtAudio dac;
dac.setProcessor([](void *inputBuffer, void *outputBuffer, /* etc */ ) {
  memcpy(inputBuffer, outputBuffer, frames * sampleSize());
});
ghost commented 7 years ago

I successfully added a LadspaProcessor class and others like the CompositeProcessor class. This functionality allows the RtAudio library to combine LADSPA effects in a effects chain with relative ease.

I added a test program that is much like the duplex program, except you choose a LADSPA effect and it's parameters and hear the audio in real-time.

I think it's a pretty cool feature that I think could be useful in the main library. I'll be in contact if you have anymore questions.

ghost commented 6 years ago

@radarsat1 closing this. I implemented this in GNU Guitar Core, which is based off of RtAudio. It's a pretty fun approach because the composite processor basically allows for the creation of a tree-like structure of audio effects. Thanks for the feedback!