Sfx Extensions to SAOLSfx provides a number of useful extensions to the SAOL language. Most of these extensions were motivated by the following goals:
In order to ensure conformance with the SAOL language specification, Sfx compiler extensions can be optionally disabled. When the extensions are disabled, Sfx accepts only legal SAOL programs. Sfx extensions fall into two broad categories. (1) Grammar extensions. (2) core opcode extensions. Grammar extensions disappear when the ISO-compliant compiler option is selected. Generally, the extensions are disabled by instructing the Sfxx lexer to treat extension reserved words such as "float" or "integer" as identifiers when extensions are disabled. This has the unfortunate effect of generating very cryptic error messages when extended features are used with extensions disabled, but has the strong benefit of ensuring that legal SOAL code that happens to inadvertently use Sfx keywords will still compile properly on the Sfx compiler. The sole exception to this rule is the _pragma_ keyword which is always visible. Sfx core opcode extensions are named using a leading and trailing underscore in order to minimize conflicts with legal SAOL programs. Users should be cautioned that names of this form are not currently reserved for use by implementations by the SAOL standard.
Table of ContentsThe _soundfont_ OpcodeThe SoundFont opcode provides access to SoundFont patches from within the SAOL/Sfx language. kopcode _soundfont_( asig result[2], ivar iPreset, ivar iMidiNote, ivar iMidiVelocity, ksig kbReleased, ksig pitchbend = 0, ksig channelVolume = 1, ksig channelExpression = 0.5, ksig modWheel = 0, ksig pan = 0 ); The opcode returns 0 while the note is currently playing. When the release has finished, the opcode returns non-zero. The stereo output of the SoundFont patch is placed into result[2]. iPreset specifies the SoundFont preset. The preset is specified as bankNumber*128+programNumber. Setting bankNumber to 128 (ie. setting iPreset to 128*128+i) will select GM/GS drum presets. iMidiVelocity specifies the MIDI note-on velocity, and ranges from (0 to 127). kbReleased should be zero until the note is released, and should be non-zero for at least one k-cycle when the note is released. pitchbend is pitchbend specified in semitones. channelVolume and channelExpression correspond to the MIDI channel volume control and expression controller, repsectively. Values ranges continuously from 0 to 1.0. 1.0 is full volume, 0 is off. Intermediate values follow the GM/GS volume curve. modWheel corresponds to the MIDI modwheel control. The value ranges from 0 to 1. pan corresponds to the MIDI pan controller. Values range from -1 (full left) to 1 (full right), with 0 being centered. Preset RangesSfx extends the SAOL grammar to allow ranges of presets to be specified in instrument declarations. The grammar of a preset range is: preset_range_list: preset_range_list preset_range | preset_range ; preset_range: INTEGER_LITERAL | INTEGER_LITERAL '-' INTEGER_LITERAL | '*' ; e.g.: instr sfInstrument(iMidiNote, iVelocity) preset 9-12,* When a MIDI program change is received, available instruments are searched to see if they contain the selected preset within their preset range. Ranges of the form "x-y" match are inclusive. The last value is included in the range. The special range '*' specifies that the instrument should be used as the default instrument in the case that no other instrument explicitly matches the requested preset.
|
Condition | Penalty |
Type and rate match exactly. | 0 |
Formal parameter rate greater than actual parameter rate. | +10 for each level of promotion required (e.g. +20 for ivar->asig promotion). |
Formal parameter is xsig rate | +1 |
integer argument passed to float formal parameter. | +1 |
float argument passed to an integer formal parameter. | +1 |
If a particular pair of actual and formal parameters meet more than one of the above conditions, then the penalty for the pair is the sum of the penalties of all conditions that are met.
When selecting an opcode, the opcode with the lowest goodness of fit penalty is selected. If two opcodes have the same goodness of fit penalty, the compiler reports an error.
Optional argument mechanisms allow library implementers to write library code that is easily reusable. Although many of the core SAOL opcodes take a variable number of arguments, user-defined SAOL opcodes are unable to do so. Sfx provides two mechanism for library writers to write opcodes with variable numbers of arguments.
VarArg opcode argument declarations allow opcodes to receive a variable number of arguments in an array. Vararg arguments are declared using the '...' reserved word. e.g.:
opcode ADSR(asig in, ivar ...) {
ksig kEnvelope;
kEnvelope = kline(_argv_); // [1]
return (kEnvelope*in);
}
The number of optional arguments is made accessible via the '_argc_' reserved word. '_argc_' evaluates to a constant integer specifying the number of passed actual arguments.
Note that the Sfx compiler provides very efficient code to handle varargs arguments when _argv_ is used without an index, and where _argv_ is used with only constant indices. Thus the example where kline is passed _argv_ as an argument produces no overhead in compiled code compared to passing a variable number of arguments directly to kline.
Individual arguments may be accessed via the '_argv_' keyword. _argv_ can be accessed like an array variable. e.g.:
_argv_[0]
_argv_[_argc_-1]
It is a runtime error if the array reference exceeds the number of arguments passed. In addition, the _argv_ standard word operates slightly differently from a standard array when passed as an argument. When passed as an argument, each member of _argv_ is passed sequentially to successive arguments of the target opcode. See line [1] in the ADSR envelope above for an example of this.
In addition to the varargs mechanism, opcodes can declare default values to be used if an opcode actual argument is not provided. e.g.:
opcode ADSR(asig in, ksig integer bReleased = 0) {
...
}
[rd: Optional arguments will be rewritten to legal SAOL syntax when exported to bitstream format; vararg parameters will not. An Sfx program containing vararg parameters will generate an error if an attempt is made to export the program to bitstream format.
Sfx provides a simple and effective method for writing user-defined opcode extensions for Sfx in C++ through the 'extern' opcode declaration. Sfx extensions allow extern opcodes to be distributed as standalone include files that can be incorporated into any SAOL or SFX orchestra file using the C preprocessor #include statement. (The SfxEdit development environment supports pre-processing of SAOL programs with the C preprocessor).
Users who are interested in implementing new opcodes may wish to examine the file include/sfxxlib.saol and include/sfxxhdr.h which provide the implementation of Sfx core opcodes. The sfxxlib.saol file is automatically included in, and compiled ahead of, all Sfx SAOL programs. Note that Sfxxhdr.h also includes other core language code; however, implementations of all core opcodes declared as extern in sfxxlib.saol can be found in sfxxhdr.h.
Incorporating new C++ opcodes into an Sfx orchestra is a three step process.
Sfx datatypes map to C++ datatypes as follows:
Sfx Datatype | C++ Datatype |
integer | long |
(default) or float | float_t |
string | const char * |
table | CTable & |
The float_t type is declared in sfxxbase.h which will have been included already by the time extern C++ code is included into the output file. Currently float_t is defined as double, however, whether float_t is typedefed as double or float is platform-dependent (and can in fact be optionally changed to float in the Sfx compiler).
The CTable class wraps table functionality; it too is declared in sfxxbase.h
The argument declaration for varargs parameters in Sfx should be declared as "integer nArgs, ..." in C++. It is the responsibility of the opcode implementer to ensure that the C++ code picks up the correct type of data in the "..." parameters.
Note that, with the exception of tables, all parameters are passed to Sfx extern opcode implementations by value.
Declaring the opcode is straightforward. extern opcodes are declared by inserting the 'extern' keyword in front of a standard opcode declaration. e.g.:
_cpp_include_ "Delay3.h";
extern aopcode Delay3(asig input);
The _cpp_include_ statement inserts an #include statement in the final output file so that the C++ implementation is visible within the code generated by the Sfx compiler.
The C++ implementation of an opcode is written as a template class. The Sfx compiler will eventually specify the class of the compiled orchestra globals class as the template parameter. The globals class provides useful functions such as COrchestraBase::RuntimeError(const char *). However, the actual supplied COrchestra template class contains also contains information that is specific to the orchestra in question. For instance, the COrchestra class derives from CInterp which may typedefed to either CLinearInterpolator or CInterpolator<kSamplesToInterpolate> depending on whether the SAOL 'interp' value is set to zero or one.
Generally, however, opcodes can find most of what they want using COrchestraBase functions. COrchestraBase is declared in the file sfxxlib/sfxbase.h.
Opcodes must implement a minimum of four functions.
The declaration of the new Delay3 opcode will look like this:
template class <COrchestra> CX_Delay3 {
public:
CX_Delay3(); // optional.
void Clear(); // not optional.
void IEval(COrchestra *pGlobals);
void KEval(COrchestra *pGlobals);
float_t AValue(COrchestar* pGlobas, float_t pInput);
};
The Clear() method is used to reset an opcode to it's initial state. Generally, Sfx tries to minimize memory allocations on the real-time synthesis thread. As a result, instruments (and their associated opcode state variables, of which the new opcode will be one) are allocated once at orchestra startup time. New instrument instances are allocated out of a pool of pre-allocated instrument states. Thus Clear() is called whenever an instrument is about to start, prior to the IEval() call.
Whether a particular pieice of code belongs in the constructor, the Clear member, or the IEval/IValue member should be determined by considering the following differences between the three.
Method | When Called |
Constructor |
Once at orchestra initialization time. Not called when a new note instance is created. Not called on the real-time thread unless the orchestra runs out of pre-allocated instances. |
Clear() |
Always called whenever a new note instance is created prior to evaluation of i-rate expressions. |
IEval()/IValue() |
Usually, but not always called during the i-pass execution of the opcode. Note that opcode instances of an oparray in particular, and opcodes in conditional statements may not execute an i-rate pass on an opcode instance. |
As a general rule, try to follow these guidelines with respect to use of Clear(), the opcode constructor, and IEval/IValue methods. Memory allocations should be avoided if at all possible in the Clear() and IEval/IValue methods because these methods are executed in real-time. Constructors for opcodes used by instruments, on the other hand, are executed at orchestra initialization time. The Sfx runtime environment currently pre-allocates a pool of 128 instrument instances at initialization time in order to reduce the occurrence of memory allocations on the real-time thread. Try to perform memory allocations in the constructor. The Clear() method is called during IPass evaluation before any IEval methods are called on any opcodes. It is called unconditionally whenever an instrument is instantiated. After a call to Clear(), the opcode should behave as if it were freshly constructed (although the opcode may manage to avoid internal details such as memory allocations or expensive table calculations that have been performed already by the constructor, may improve realtime behavior by caching calculations performed by previous calls to the Clear() method). IEval/IValue methods, on the other hand may not be executed if the opcode is executed inside an if or else statement, and may be executed multiple times inside an i-rate-guarded while expression; as a result, constructor-like operations cannot always be performed reliably in IEval/IValue methods.
The opcode must now provide three methods to implement the execution of the opcode at i-, k-, and a-rate. First, determine the rate of the opcode. The method corresponding to the return rate of the opcode will be
float_t xValue(COrchestra*pGlobals, ... (arguments));
where 'x' is the rate of the opcode. At the other two rates, the methods are declared as
void xEval(COrchestra *pGlobals, ... (arguments) );
So far so good. Each of the three rate evaluation methods, in addition to the COrchestra* parameter used to locate globals, will also receive a parameter for each parameter of the original declaration that matches the method rate. Each parameter occurs in the order in which the parameters were declared in the original Sfx declaration.
[rd: table parameter passing is likely to change soon. It's ugly. However, if you follow this implementation guide your extern opcodes will work with future implementations.] Table arguments provide one notable exception. If table arguments do not have explicit rate declarations, then they are passed to all three rate-execution method provided, when possible. When is it not possible to pass a table parameter? A: when it's a tablemap with an index that's at a rate other than i-rate. In this case, the result of the table-map index operation is not available at i-rate so it won't be passed to the extern opcode at i-rate. The unfortunate result is that opcodes that take table arguments must implement two methods at each rate: one with the table parameter provided, and one not. e.g.:
// extern aopcode TableOp(table tbl, ivar iArg);
void IEval(COrchestra*, CTableRef &tbl, float_t iArg); // with tbl.
void IEval(COrchestra*, float_t iArg);
The former method would get executed for an invocation of the form:
TableOp(tbl, 3);
The latter method would get executed for an invocation in SAOL coe of the form:
TableOp(tblMap[kVariable], 3);
Fortunately, if the rate of the table is explicitly specified in the extern declaration, then the table is passed only at the specified rate, and it will always be present.
e.g.:
extern aopcode TableOp(ivar table tbl, ivar iArg)
needs only one IEval method. The tbl argument is passed only to the IEval method, and no to KEval or AValue.
For extern x-opcode declarations, the implementing opcode must provide methods that match all possible invocations of the opcode. This usually means implement xValue and xEval at each possible rate that the opcode can execute. The return rate of the opcode is determined using the default rules for core opcodes:
1) Calculate the maximum rate of all arguments of the opcode.
2) Calculate the maximum of the argument rate and any rate restrictions that are effected by enclosing if or while loops in the SAOL source.
The rate of the opcode is the rate calculated in step 2.
Typically, it's helpful to write overloaded wrapper opcodes that restrict xsig arguments into sensible groups in order to reduce the number of methods that must be executed.
e.g.:
// An extern x-opcode
extern opcode _min_(xsig x, xsig y);
// The wrappers that reduce the number of
// cases that the min implementation must deal with.aopcode min(asig x, asig y) { return (_min_(x, y) ); }
kopcode min(ksig x, ksig y) { return (_min_(x, y) ); }
iopcode min(isig x, isig y) { return (_min_(x, y) ); }
In this case, the extern opcode class CX__min_ must only implement the following methods (since the min wrappers have managed to eliminate the cases where x and y have different rates.
template <class COrchestra> class CX__min_ {
public:
void Clear() { }; // does nothing.
void IEval(COrchestra *) { }; // Does nothing.
void KEval(COrchestra *) { }; // Does nothing.
void AEval(COrchestra *) { }; // Does nothing.
float_t IValue(COrchestra *, float_t x, float_t y) {
// called for min(iValue, iValue);
return min(x,y);
};
float_t KValue(COrchestra *, float_t x, float_t y) {
// called for min(kValue, kValue)
return min(x,y);
};
float_t AValue(COrchestra *, float_t x, float_t y) {
// called for min(aValue, aValue)
return min(x,y);
};
};
[-rd: the current release of Sfx has known bugs in the conversion of argument types of extern opcodes. These can be avoided by always wrapping extern opcodes, since the compiler code that handles normal user-defined-opcodes doesn't have these problems. The wrapper opcode will correct type- or rate-mismatched arguments before the extern opcode is called].
extern opcodes cannot be used directly as oparrays. The calling convention for oparrays is rather complex. The Sfx compiler will give an error message if an extern opcode is used as the storage type for an oparray. Therefore, it's easier to just wrap the extern opcode in a normal Sfx opcode if the extern opcode will be used in an oparray. The compiler will then use the wrapper opcode declaration to generate correct handling of oparray. As a matter of best practice wrapper opcodes should always be provided for extern opcodes. The Sfx compiler's template-ized evaluation of opcodes ensures that there is no additional code overhead when extern opcodes are used directly.
e.g.:
// The extern declaration.
extern aopcode _EnhancedTrace_(
ivar string strName,
ivar integer nSamples,
ivar integer nInstances,
asig ...);
// The wrapper
aopcode EnhancedTrace(
ivar string strName,
ivar integer nSamples,
ivar integer nInstances,
asig ...)
{
return (
_EnhancedTrace_(strName, nSamples, nInstances, _argv_)
);
}
Using FORCEINLINE With Extern Opcodes
The Sfx compiler inlines generated C++ code very aggressively -- so aggressively in fact, that, by default, most compilers (particularly the MSVC compiler) will decide to not expand extern opcodes inline because they have already expanded more inline code than they are strictly comfortable with.
Adding the FORCEINLINE macro before each method of a core opcode forces the MSVC compiler to inline extern opcode code against its better judgement. The FORCEINLINE declaration should generally be used with all extern opcode methods and should almost definitely be used with AEval()/AVlu