14.2. A First Glance at Writing Modules

The way you work when writing MCOP-aware objects is normally the following:

  1. Write an interface definition in the IDL language; for instance, example_add.idl.

  2. Pass that definition through mcopidl. You get example_add.cc and example_add.h files.

  3. Write an implementation for the interfaces you've declared, as C++ class deriving from the _skel classes.

  4. Register that implementation with REGISTER_IMPLEMENTATION.

  5. Maybe write a .mcopclass file.

After that, everybody can use the things you do.

14.2.1. Step 1—Write an Interface Definition in the IDL Language

One important concept in MCOP is that classes are not important, interfaces are. To show a simple example, when you write a small module that simply adds two audio streams, it could have the following interface:

   2 // example_add.idl
   4 #include <artsflow.idl>
   6 interface Example_ADD : Arts::SynthModule {
   7     in audio stream invalue1, invalue2;
   8     out audio stream result;
   9 };

You describe interfaces like that in the MCOP IDL files. These lines mean: there is an interface in which two audio streams are flowing in, and one audio stream is flowing out.

Figure 14.3. The Example_ADD interface.

For people who use this interface, this is all they need to know. They don't need to know how addition takes place. They don't need to know what language this was implemented in. They don't need to know anything except the definitions in the interface.

Let's do a line-by-line walk-through to see what is happening here:

   2 #include <artsflow.idl>

Because the SynthModule interface (which you use later) is declared in artsflow.idl, you need to include it. All aRts components are declared inside the Arts namespace, so you have to prefix it with Arts::. I'll never explicitly mention this prefix in the text when discussing interfaces.

   2 interface Example_ADD : Arts::SynthModule

This tells the MCOP IDL compiler to create an interface that implements everything that SynthModule does, as well as its own methods, attributes, and streams. (So it derives from SynthModule.) MCOP supports multiple inheritance as well as single inheritance. Interfaces that don't specify anything automatically derive from Object. Interfaces that have streams (like our interface) should always inherit SynthModule (or a derived class).

   2 in audio stream invalue1, invalue2;
   3 out audio stream outvalue;

Here you add streams to the interface. These streams are the normal type of audio stream supported by MCOP. They are synchronous, which means that every time our Example_ADD module gets 200 samples (or any other amount), all streams are involved. The scheduler takes care that the 200 samples are available for both input ports, invalue1 and invalue2. It then calls the calculateBlock method and tells it to calculate 200 samples and expects that it will generate exactly 200 samples of outvalue output. Synchronous streaming is the fastest and most easy-to-use variant of streaming, and it makes sense for most modules.

If, on the other hand, you think of a MIDI stream (that comes from a keyboard), things are different. The module wouldn't be able to guarantee that exactly the number of requested samples can be generated by calculateBlock; if the scheduler requests, "Please give me 40 events," how could it do that when the person playing the keyboard isn't playing fast enough? For now, you have our synchronous streams; I'll talk more about the alternative model later.

14.2.2. Step 2—Pass That Definition Through mcopidl

If you have put all that into a file called example_add.idl, you can invoke mcopidl:

   2 $ mcopidl -I$KDEDIR/include/arts example_add.idl

The -I flag adds a path to look for includes. If you don't have KDEDIR set to the position where KDE 2.0 is installed, you may have to use something explicit like the following, instead:

   2 -I/usr/local/kde-2.0/include/arts

The IDL compiler now creates example_add.cc and example_add.h, which will be used later to implement and access the new Example_ADD module.

14.2.3. Step 3—Write an Implementation for the Interfaces You've Declared

Listing 14.2 shows how to implement adding the sound.

Example 14.2. Implementing the Example_ADD Interface

   2  1: // example_add_impl.cc
   3  2:
   4  3: #include "example_add.h"
   5  4: #include "stdsynthmodule.h"
   6  5:
   7  6: class Example_ADD_impl
   8  7:               :public Example_ADD_skel, Arts::StdSynthModule
   9  8: {
  10  9: public:
  11 10:     void calculateBlock(unsigned long samples)
  12 11:     {
  13 12:         unsigned long i;
  14 13:         for(i=0;i < samples;i++)
  15 14:             result[i] = invalue1[i] + invalue2[i];
  16 15:     }
  17 16: };
  18 17:

As you can see, you derive from the skeleton class for the interface (which was generated by the mcopidl compiler). You also include the corresponding example_add.h (line 3). The other class you derive from is StdSynthModule because this contains some empty implementations of SynthModule methods that you often don't need to override.

Finally, consider the calculateBlock method (line 10). This gets called whenever the module should process a block of audio data. The samples parameter tells the function how many samples to process. It is guaranteed that they are available at the corresponding pointers.

Invisible to you (generated from the mcopidl compiler), the streams have become the following declarations in the Example_ADD_skel class (you inherit from that):

   2 // variables for streams
   3 float *invalue1;                  // incoming stream
   4 float *invalue2;                  // incoming stream
   5 float *result;                    // outgoing stream

So the task of calculateBlock is easy:

  • Take the data at the incoming streams and process them (use exactly samples values).

  • Write the output to the outgoing streams (also exactly samples). You must fill them; if you don't have anything to write (for instance, because you are getting that data from the Internet, and the Internet isn't fast enough to give you enough data), write 0.0 at least.

  • Do not modify the pointer itself. You may see this occasionally in some sources, but it isn't allowed any longer.

As you see, the code is really easy to read.

14.2.4. Step 4—Register That Implementation with REGISTER_IMPLEMENTATION

Finally, REGISTER_IMPLEMENTATION is used to tell the MCOP object system that you have implemented an interface (you see this in the source under step 3). This has the following background: the objects you implement should be usable from programs that don't even know that such objects exists. For instance, artsbuilder will be a program that visually connects objects to larger graphs. Of course, it makes sense that your Example_ADD implementation can be used from artsbuilder, without artsbuilder knowing much about it.

Thus, artsbuilder can't simply call a constructor (because you would need to link artsbuilder to the class you just wrote and have a .h-file with the class definition, and so on). Instead, the REGISTER_IMPLEMENTATION macro defines a class that knows how to create one of your Example_ADD objects. If you then put only this in a shared library, the component can be used as a plug-in by applications that don't know anything about it.

This also means that you shouldn't need to have a header file in most cases, because MCOP provides ways to create an Example_ADD implementation without knowing that an Example_ADD_impl class exists.

14.2.5. Step 5—Maybe Write a .mcopclass File

I'll talk about this in the section "Using the Effect." If you compile this in a libtool shared library libexample_add.la, you could write something like this in a file $KDEDIR/lib/Example_ADD.mcopclass:

That way, the MCOP dynamic library-loading mechanism would know that whenever you want to create an Example_ADD implementation, it could load the library. You'll do this later.

14.2.6. How to Use the New Module

So you've written a module (Listing 14.3). Now, how do you use it?

Example 14.3. Using an Example_ADD Module

   2  1: // example_add_test.cc
   3  2:
   4  3: #include "connect.h"
   5  4: #include "example_add.h"
   6  5: #include "artsflow.h"
   7  6:
   8  7: using namespace Arts;
   9  8:
  10  9: void main()
  11 10: {
  12 11:     // create a MCOP dispatcher (always do this)
  13 12:     Dispatcher dispatcher;
  14 13:
  15 14:     Synth_FREQUENCY freq1, freq2;    // some objects
  16 15:     Synth_WAVE_SIN sin1, sin2;
  17 16:     Synth_MUL mul;
  18 17:     Example_ADD add;
  19 18:     Synth_PLAY play;
  20 19:
  21 20:     // setup a 440Hz sin and connect it to the add
  22 21:     setValue(freq1,440.0);
  23 22:     connect(freq1,sin1);
  24 23:     connect(sin1,add,"invalue1");
  25 24:
  26 25:     // setup a 880Hz sin and connect it to the add
  27 26:     setValue(freq2,880.0);
  28 27:     connect(freq2,sin2);
  29 28:     connect(sin2,add,"invalue2");
  30 29:
  31 30:     // multiply everything with 0.5 (=> no clipping)
  32 31:     connect(add,result,mul,invalue1);
  33 32:     setValue(mul,invalue2,0.5);
  34 33:
  35 34:     // connect the output to the play module
  36 35:     connect(mul,play,"invalue_left");
  37 36:     connect(mul,play,"invalue_right");
  38 37:
  39 38:     // start all modules
  40 39:     freq1.start(); freq2.start(); sin1.start();
  41 40:     sin2.start(); mul.start(); add.start(); play.start();
  42 41:
  43 42:     dispatcher.run();
  44 43: }

You can compile it (with some tweaking if you have no KDEDIR set) with the following:

   2 $ gcc -o example_add_test example_add_test.cc example_add.ccexample_add_impl.cc -I$KDEDIR/include/arts-L$KDEDIR/lib -lmcop -lartsflow_idl -lartsflow -ldl

As you'll hear, it adds the sound just nicely. The resulting graph used here looks like Figure 14.4:

Figure 14.4. Flow graph for Listing 14.3.