This document gives an overview over sfidl, which is used in BEAST for two purposes:
Table of Contents Table of Contents 1 · What is SFI? 1.1 · Overview 1.2 · Design goals 1.3 · The sfidl command line utility 2 · The .idl files 2.1 · The basics: comments and namespaces. 2.2 · Data types: primitive and composite. 2.2.1 · Simple primitive types 2.2.2 · Choices 2.2.3 · Composite types 2.2.4 · Prototyping 2.3 · Classes and procedures 2.3.1 · Classes 2.3.2 · Signals 2.3.3 · Properties 2.3.4 · Streams 2.3.5 · Procedures 3 · The language bindings 1.1 Overview
To provide an API to more than one programming language, we first specify
the API in a language independant fashion. Therefore, we use an .idl
file, which contains a description what a class or procedure looks like. Then, it is possible to provide an implementation in C or C++. Finally, for different languages, code to access that implementation can be generated automatically. 1.2 Design goals
We tried to accomplish a number of different design goals during the SFI
design.
1.3 The sfidl command line utility
sfidl is the interface definition language compiler for SFI,
a manual page is available as
sfidl(1), and the IDL file format is detailed
in the next sections. 2.1 The basics: comments and namespaces.
The syntax of .idl files is similar to C++ and/or CORBA IDL. In the following
sections the elements will be described. First of all, you need to know that
everything needs to be declared within the scope of a namespace. This avoids
collisions. The syntax for namespace is // IDL Code namespace Foo { /* declarations go here */ sequence { // example sequence of integer values Sfi::Int ints; } IntSeq; /* more declarations go here */ } Namespaces can also be nested. There is an additional using keyword, which can be used to avoid using the namespace qualifier "::" to refer to something from a different namespace: // IDL Code namespace Bar { Foo::IntSeq get_even_numbers (Sfi::Int bound); // needed to refer to namespace Foo:: using namespace Foo; // adds Foo:: to namespace search list IntSeq get_prime_numbers (Sfi::Int bound); // IntSeq is found in Foo:: automatically } As you see, primitive types (like Int) are declared within the Sfi namespace. C and C++ style comments work as usually. 2.2.1 Simple primitive types
SFI provides a number of predefined primitive data types. These are
2.2.2 Choices
In addition to these simple primitive types, it is possible to define a choice
as follows: // IDL Code namespace Foo { choice WaveForm { WAVE_FORM_SINE, WAVE_FORM_SAW, WAVE_FORM_RECT }; } This means that a value of type WaveForm can hold one of these choices. Extra information can be supplied with each choice value (both optional):
// IDL Code namespace Foo { choice WaveForm { WAVE_FORM_NONE = (Neutral, "No waveform"), WAVE_FORM_SINE = (1, "Sine wave"), WAVE_FORM_SAW = (2, "Sawtooth wave"), WAVE_FORM_RECT = (3, "Rectangle wave") }; } There is no guarantee what number the interfacing code using the choice in C and C++ will see for a particular WaveForm (i.e. WAVE_FORM_SINE could be 3 in client code). This makes it possible to add choice values or change their number even after the client got compiled. (Internally the communication is done by passing the choice value as string). There is one exception: the Neutral keyword indicates that the number should be 0 for both, client code and implementation. In C/C++, this allows writing // C++ Code Foo::WaveForm wave = osc->get_wave_form(); if (!wave) printf ("No wave form selected.\n"); 2.2.3 Composite types
More complex data types can be constructed from these simple data types. There are two composite
types: records and sequences. Sequences are used for a sequence of multiple values of the same data type. The syntax is: // IDL Code namespace Foo { sequence IntSeq { Sfi::Int ints; // the contained type }; } A sequence may then hold 0, 1 or N values of the same type. The name "ints" here is only used for the C language, where ints contains the actual data and n_ints contains the number of items in the sequence. // C Code FooIntSeq *seq = foo_int_seq_new(); foo_int_seq_resize (seq, 2); seq->ints[0] = seq->ints[1] = 42; g_assert (seq->n_ints == 2) Records can be used for a sequence of values of different data types (they behave similar to structures in C/C++). // IDL Code namespace Foo { using namespace Sfi; record PartNote { // a human readable string describing what the record does (optional) Info blurb = "Part specific note representation"; // the record fields Int id; Int tick; Int duration; Int note; Int fine_tune; Real velocity; Bool selected; }; } In opposition to sequences, NULL (or nil or whatever is adequate for the language you are using) is a possible value for records, that is, a record value either contains all of the above fields, or it is a NULL pointer. For sequences, NULL is not a valid value (you can always use empty sequences there). Parameter specifications: Note that you can, and probably should add parameter specifications for the fields in a sequence and a record. However, we describe parameter specifications seperately below. 2.2.4 Prototyping
In some cases you know that a certain type will be defined later, but you can't give it's
definition already. Perhaps this occurs because two types, often classes, need each other
in their definition. Perhaps this also occurs because you want seperate things in seperate
.idl files in a special way. You can use prototypes in this case, for all types the IDL compile understands. namespace Foo { choice SomeChoice; sequence SomeSequence; record SomeRecord; class SomeClass; record Test { SomeChoice a; SomeSequence b; SomeRecord c; SomeClass d; }; } 2.3.1 Classes
IDL defined APIs consist mostly of classes. SFI supports single inheritance, so a set
of simple classes might look like this (not taken from the BSE class hierarchy): // IDL Code namespace Foo { using namespace Sfi; class AudioObject { void play (); void stop (); }; class Sample : AudioObject { }; class Song : AudioObject { Sample load_sample (String filename); void insert_sample (Sample sample, Real position, Real speed); }; } As far as this example goes, we have only added the bare minimum. It is however possible (and probably desirable) to add defaults and documentation to the methods, as demonstrated in this simple example: // IDL Code #include <bse/bse.idl> namespace Foo { using namespace Bse; // for Bse::STANDARD // ... like above ... class Song : AudioObject { Sample load_sample (Sample filename) { // _() is used as i18n markup for translatable strings Info blurb = _("This loads a sample for further use into the song"); In filename = (_("Sample Filename"), _("The name of the sample file"), "", STANDARD); Out sample = (_("Sample"), _("The newly loaded sample"), STANDARD); }; // ... like above ... }; } Return and argument values referring to instances of classes (in this example samples) can be NULL. So load_sample in this example would probably return a valid sample, if the sample file could be loaded successfully, and NULL otherwise. Various aspects of valid filenames are specified through the assignment of a parameter specification to filename. Parameter specifications are described in detail in the section PARAM SPECS. 2.3.2 Signals
Signals provide the possibility for objects (instances of classes) to emit events.
A user can connect to the signal, and whenever a signal gets emitted, his callback
gets called. In our example it would be possible to add two signals to AudioObject, one
where it emits the current position regularily while playing, and one that gets emitted
when it is done playing. // IDL Code namespace Foo { class AudioObject { void play (); void stop (); signal position_changed (Real new_position); signal done_playing (); }; } 2.3.3 Properties
Another important element commonly found in classes are properties.
Usually, they are logically grouped to improve readability in GUIs. // IDL Code namespace Bse { namespace Contrib { class Balance : Bse::Effect { group _("Audio Input") { Real alevel1; // volume of audio input 1 Real alevel2; // volume of audio input 2 }; group _("Control Input") { Real clevel1 = Perc (_("Input 1 [%]"), _("Attenuate the level of control input 1"), 100, STANDARD); Real clevel2 = Perc (_("Input 2 [%]"), _("Attenuate the level of control input 2"), 100, STANDARD); }; group _("Output Panning") { Real lowpass = Frequency (_("Lowpass [Hz]"), _("Lowpass filter frequency for the control signal"), 100, 100, 1000, STANDARD); Real obalance = (_("Output Balance"), _("Adjust output balance between left and right"), 0, -100, 100, 10, STANDARD); }; }; } } // Bse::Contrib Although the above example isn't comprehensive in this regard, properties can be of any IDL type. The aforementioned groups come with translatable names, and properties may support a good chunk of definitions besides just their type and name. In the example, alevel1 and alevel2 use the simplest form possible to define a property, just the types and names are given. Displaying a property like this in GUIs will provide unattractive results, to say the least. For this reason, sfidl allows type specific constructors, and by convention these constructors expect a translatable label and a translatable description as first two arguments and an option string as last argument. The constructors are provided by the sfidl language bindings, so their availability differs dependant on that. In the above example, the percentage and frequency constructors, both of type Real and provided by BSE, are used and expect these arguments:
Real obalance = (_("Output Balance"), _("Blurb"), 0, -100, 100, 10, STANDARD); Real obalance = Real (_("Output Balance"), _("Blurb"), 0, -100, 100, 10, STANDARD); More on property constructors provided by BSE can be found as part of the plugin development guide in the property section and the property option section. 2.3.4 Streams
For classes that do audio processing (module objects), it is necessary to specify streams,
which will transport the audio data into the module, and the resulting audio data out of it.
Generally there are three types of streams:
namespace Arts { class Compressor : Bse::Effect { // ... IStream invalue = (_("Audio In"), _("Audio input")); OStream outvalue = (_("Audio Out"), _("Compressed audio output")); }; } As you see, the syntax is quite straight forward, containing a variable name and the specification of the translatable user visible strings, which contain the name of the stream, and a blurb describing what it does. 2.3.5 Procedures
Procedures can be thought of as methods-without-a-class. A classic example is: namespace Bse { Real note_to_freq (Int note, Int fine_tune); } which converts a midi note to a frequency, using a given fine tune. The same syntactic elements for specifying more details (parameter speccifications, Info strings) that are valid for methods can be used here as well. |
||
|