Site hosted by Angelfire.com: Build your free website today!

 







Sfx Extensions to SAOL

Sfx provides a number of useful extensions to the SAOL language. Most of these extensions were motivated by the following goals:

  • a desire to be able to write core SAOL opcodes using a SAOL-like language. Many SAOL core opcodes could not, in fact, be written in SAOL; all core opcodes can be written and declared using the Sfx extensions (and in fact, have been).
  • a desire to provide a simple-to-use faciliy for declaring and implementing new opcodes written in C++.
  • A desire to use SoundFonts from within a SAOL-like language given that there are not yet any commercially available DLS-2 samples and the desire to provide the ability for implementing full General Midi/GS MIDI from SAOL/Sfx.

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 Contents

SoundFont Support

Preset Ranges

General MIDI/GS Support

Voice Limiting Opcodes

The _trace_ Opcode

The _assert_ Statement

The _pragma_ Directive

The _cpp_include_ Directive

Datatype Extensions

Rate Semantics Extensions

Constant Expressions

Overloaded Opcodes

Optional Arguments

Writing New Opcodes in C++

The _soundfont_ Opcode

The 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 Ranges

Sfx 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.


General Midi/GS Opcodes

The following opcodes provide access to midi state required by General Midi and GS MIDI instruments. SAOL does not currently provide access to these settings.

iopcode _midicoarsetuning_();

Returns the value of the MIDI coarse tuning RPN for the currently MIDI  channel. Values are in semitones. This opcode always returns zero for MIDI channels that are currently selected as GM or GS percussion channels.

iopcode _midifinetuning_(ivar iMidiNote);

Returns the value of the MIDI fine tuning RPN for the currently MIDI  channel. Values are in semitones. The return value includes GS MIDI scale tuning settings for the current MIDI channel if the GS MIDI start command has been received.

_midimastervolume_()

Returns the value of the MIDI master Volume Sysex control. Value is in absolute amplitude (with MIDI volume curve already applied).

Voice Limiting Opcodes

It's quite common for instruments to have very long release times. A piano patch for instance, may take three or four seconds to fully decay to an amplitude less than -96db after being released. This may result in an unacceptable number of notes running simultaneously. Dedicated  synthesizers deal with this using a technique called "voice scavenging". When too many notes are running simultaneously, the synthesizer "steals" voices by immediately releasing old notes. Usually, when note scavenging is needed, there is plenty of Audio activity so if notes that have been released are terminated before their release decay period has finished, the result is imperceptible.  This functionality is difficult to implement using native SAOL code, so the functionality has been provided through a set of extension opcodes.

iopcode _setvoicelimit_(ivar voiceLimit);

Sets the maximum number of voices (instrument instances) that can run simultaneously. The voice limit is actually set to the maximum of the supplied voice limit, and the voice limit set by the user in the Audio Options configuration panel.

Sfx uses the following scavenging algorithm. If the current number of active voices exceeds the voice limit, then Sfx starts releasing old voice instances immediately until the number of active voices is less than or equal to the voice limit. Voices are released in the following order:

  1. The oldest released note. Released notes are notes for which released has been true at least once. Extending the note using extend does not make the note unreleased again for the purposes of voice scavenging. Priority notes that have been released are not given special preference. If the note is a priority note, and it has been released, then it is a candidate for scavenging.
  2. The oldest non-priority note.

Priority notes are identified as follows. Any instrument instance which is created using a send statement is marked as a priority note. Any note which was created using a SASL statement with the priority flag set are priority notes.

Priority voice instances are never scavenged, unless they have been released. Priority voice instances are counted as voices for the purposes of calculating the total number of active voices.

iopcode _getvoicelimit_();

Returns the current effective voice limit.

iopcode _setvoiceweight_(ivar voiceweight);

Sets the weight of the voice for voice scavenging purposes. The default weight of a voice is 1. Setting the voice weight to a value other than one means that the voice will be counted as that many voice instances for the purposes of voice scavenging. This allows expensive instruments to be given a higher effect on the voice total than inexpensive instruments. As a general guideline, one SoundFont voice should be used as a reference point for one voice instance.

iopcode _getvoiceweight_();

Gets the current voice weight for the currently active instrument.

iopcode _setvoicepriority_(ivar ibPrioirty);

Sets the voice priority. The voice is a non-priority voice if the voice priority is set to zero, and a priority voice if ibPrioirty is non-zero. Priority voices will not be scavenged. SASL priority flags can be overridden using this opcode. The results of setting the voice priority of an effect instrument to zero is undefined, but whatever the effect is, it's probably not going to be good.

iopcode _getvoicepriority_();

Gets the voice priority of the current instrument instance.

The Trace Opcode

Sfx provides two very useful debugging primitives that are integrated into the compiler and development environment.

opcode _trace_(
   ivar string strWindowCaption,
   ivar int nSamples,
   ivar int nInstances,
   xsig float ...
   );

The trace opcode displays signal data in a popup window in the development environment. Data can be displayed as either wave signals or as numeric sample data by using the View menu of the resulting window.

strWindowCaption provides the string used in the title of the display window in the SfxEdit development environment.

nSamples determines the maximum number of samples that will be captured and displayed in the trace window.

nInstances determines how many instances of the trace window will be displayed. Instances are identified by the development environment by their window caption. The results of using a multiple invocations of trace with the sampel strWindowCaption where the remaining arguments do not match is unspecified. At most nInstances trace windows will be displayed if the trace opcode is invoked multiple times (for instance, during evaluation of notes in an orchestra).

The remaining arguments provide the data items for the trace window. If more than one data argument is provided, then the trace window treats each data item as a separate audio channel.

The _trace_ statement is available when extensions are disabled.

The _assert_ Statement

    _assert_ expression;

Assert is a statement. The keyword _assert_ is a keyword only when extensions are enabled. When extensions are disabled, _assert_ is treated as an identifier by the compiler. If the expression evaluates to a zero, the runtime system generates a runtime error that provides the text of the assert statement and source code line number (if the orchestra was built from a SAOL file rather than a bitstream).

The _assert_ statement is available when extensions are disabled.

The _cpp_include_ Directive

  cpp_include: '_cpp_include' string_constant ';'

The primary purpose of the _cpp_include_ declaration is to provide a mechanism for including extern opcodes written in C++ in a compiled orchestra.

The _cpp_include_ declaration has the effect of inserting an #include statement into the the compiled C++ output file. The supplied string constant is supplied as an argument to the #include statement.

_cpp_include_ must appear outside any global or instrument declarations, and should, ideally, be placed as early as possible in the file before any extern declarations that reference code in the include file.

e.g.

   _cpp_include_ "my_externs.h" // include the C++ code
  #include "my_externs.saol"     // Declare the extern opcodes.

More sensibly, _cpp_include_ directive should be placed in the file "my_externs.saol" along with the corresponding extern declarations.

The _cpp_include_ statement is not available when extensions are disabled.

The _pragma_ Directive

  pragma_directive: '_pragma_' string_constant ';'

The pragma directive is used to provide implementation-specific compiler directives to the SAOL/Sfxx compiler. Implementations will ignore pragma directives they don't recognize without generating an error message.

Currently the Sfxx compiler supports the following pragma directives:

_pragma_ "sfxx_extensions 1"; // Enable extensions.
_pragma_ "sfxx_extensions 0"; // Disable extensions.
_pragma_ "sfxx_extensions -1"; // Revert to command-line default

Pragma directives must appear at the topmost scope in the file, outside of any global blocks, opcode or instrument declarations.

Note that extensions can be turned on and off within the same file. The effect of this is that opcodes and instruments declared in sections of the source file where extensions are enabled will used extended syntax and semantics; opcodes and instruments declared in sections of the source file where extensions are disabled will not. Non-extended opcodes are able to freely call opcodes defined with extensions enabled. Opcode argument overloading is enabled in non-extended code; however, when extensions are disabled, a compile-time error is given if the name of a newly declared opcode matches the name of an existing opcode.

The _pragma_ directive  is available when extensions are disabled.

Datatype Extensions

Sfx adds explicit integer and string datatypes to the SAOL language. The SAOL grammar has been modified to allow the use of of the 'integer', 'float' and 'string' keywords to explicitly declare the types of variables and opcode arguments.

The (simplified) grammar for datatype and argument declarations follows:

typedecl: ratedecl typeopt
    |
'table'
    ;

ratedecl: 'xsig'
   | 'asig'
   | 'ksig'
   | 'ivar'
   ;

type: 'integer'
   | 'float'
   | 'string'
   | 'table'
   ;

For example:

    asig integer aVariable;

integer variables are stored in memory as twos-complement integers having at least 32-bits of precision. Type promotion occurs as follows: an integer data-type will be promoted to float if an passed to an opcode requiring a float argument, or if used in conjunction with a float in a binary arithmetic expression. floats will be rounded to integer automatically if passed to an opcode argument that is declared as integer.

One specific caveat should be observed when writing Sfx code with extensions enabled: division produces different results when extensions are disabled if both arguments on either side of the division evaluate to integers. Thus:

    kSig kVar;

    kVar = 1/2;

looks like a very harmless piece of code; however with extensions disabled, the compiler converts 1 and three to floating point values before performing the division, since SAOL does not support integer datatypes. In this example when compiler extensions are disabled, kVar is assigned the value 0.5. When extensions are enabled, however, the Sfx compiler performs integer division in the above example, and kVar is assigned the value 0. If you plan to switch back and forth between standard SAOL and extended Sfx, you should get into the habit of explicitly writing float constants to prevent this problem. e.g.:

   kVar = 1.0/3.0;

produces the same results in Sfx and SAOL.

Sfx allows explicit declaration of the rate of table arguments and variables, although this feature should be currently be considered experimental. A variable or argument declared as "table" without a rate-specification defaults to ksig table unless an ivar rate restriction is in effect due to an enclosing i-rate if or while statement, in which case the rate of the table is i-rate. The rate of a table affects the way the compiler treats table accesses. For instance:

global {
   ivar iRateTable(sample, -1, "sample.wav");
   ksig kRateTable(sample, -1, "sample.wav");

};

instr Instr() {
   imports exports ksig kRateTable;
   imports exports ivar iRateTable;
   ksig kIndex;

   tableread(kRateTable, 1); // k-rate expression.
   tableread(iRateTable, 1); // i-rate expression.
  
    // but...
    tableread(iRateTable, kIndex); // k-rate

   ftlen(kRateTable); // k-rate expression
   ftlen(iRateTable);  // i-rate expression

}

Note that by default, tables must be considered k-rate, since external SASL events, or other instruments may modify the table contents or table parameters at k-rate. If a table is declared as i-rate, modifications to the table performed on the table at k-rate will have no effect on the value of expressions derived from i-rate accesses to the table.

Explicit rate-declaration of opcode table arguments is also useful when defining extern opcodes (described later in this document).

String variables and arguments can be assigned constant strings only. Sfx does not provide any native mechanisms for manipulating strings; however being able to pass constant string arguments to user-defined extern opcodes written in C++ is occasionally useful, and is useful enough to justify this feature. (For example, the _trace_ opcode is declared in sfxxlib/sfxxlib.saol using this mechanism).

Rate Semantics Extensions

There are a number of constructs which are legal in Sfx but illegal in SAOL. The Sfx compiler is currently more flexible about what rates of expressions and statements are legal and where.

Rates of Statements in Guarded Opcodes

The SAOL standard currently the use of statements within the body of an if or else expression that run at less than the rate of the conditional guard expression; however, the standard is current ambiguous about  whether opcodes referenced within the body of a condional block of code may contain expressions that run at less than the rate of the guard expression.

Sfx currently allows these opcodes. In these cases, the expressions which occur at less than the guard rate are executed unconditionally (they are executed regardless of the result of the guard expression.

For example:

aopcode Pan(asig in, ksig pan) {
   ksig kLeftVol, kRightVol;

   kLeftVol = sin(pan*(kPI/4)); // [1]
   kRightVol = cos(pan*(kPI/4));

   return (kLeftVol*in, kRightVol*in);
}

instr SomeInstrument() {
    imports ksig kPanControl; // An event target.
    imports ksig kVolume; // An event target.

   ....
   if (aResult == 0) { // [2]
       output(0,0);
   } else {
       output(Pan(aResult, kPanControl)); // [3]
   }

}

This code is legal in Sfx, but  illegal in some SAOL implementations. The guard expression at [2] places an a-rate guard restriction on  the opcode invocation at [3]. The statement at [1] is determined to be illegal and generates a compile-time error. In Sfx the k-rate expression [1] is calculated unconditionally at k-rate, although it is only used at a-rate if the guard expression allows the opcode to run at a-rate.

Rates of Statements and Expressions in x-opcodes

The SAOLC reference decoder (part of the MPEG-4 SA standard) is restrictive about the rates of arguments to x-opcodes. The following opcode:

opcode Pan(
   asig in,
   xsig pan  // accept i-, k- or a-rate
) {
   xsig kLeftVol, kRightVol;

   kLeftVol = sin(pan*(kPI/4)); // [1]
   kRightVol = cos(pan*(kPI/4));

   return (kLeftVol*in, kRightVol*in);
}

does not run on the SAOLC reference decoder, although it is not explicitly disallowed by the standard.

SAOLC prefers (I think)

opcode Pan(
   xsig in,    // pretty much always at a-rate.
   xsig pan  // accept i-, k- or a-rate
) {
   xsig kLeftVol, kRightVol;

   kLeftVol = sin(pan*(kPI/4)); // [1]
   kRightVol = cos(pan*(kPI/4));

   return (kLeftVol*in, kRightVol*in);
}

However, when the in signal is a-rate, then pan, (and kLeftVol, and kRightVol as well) are tagged as a-rate, and evaluated at a-rate.

The Sfx compiler determines sets the rate of the xsig variables to the lowest possible legal rate based on the rate of the arguments. For example, if pan is k-rate, then kLeftVol and kRightVol would be determined to be k-rate as well.

[rd: These constructs are not currently disallowed by the Sfx compiler when running with extensions disabled. This may be corrected in a future release of the compiler if it is determined that the reference implementation is actually correct in this regard.]

Constant Expressions

The Sfx compiler will accept any legal constant expressions as the size declaration of an array. The SAOL language currently only allows literal integer constants.

e.g.:

    ksig kArray[3]; // Legal in Sfx and SAOL.
    ksig kArray[s_rate/2]; // Illegal in SAOL, legal in Sfx.

Furthermore, Sfx extends constant evaluation of expressions through all basic core math operators. As a result, the following declaration is also legal (although probably not very useful):

   ivar kArray[log(s_rate)/sqrt(sin(2*kPI*s_rate)];

In this case, the compiler recognizes that the arguments to the log, sqrt and sin functions are constant, and evaluates these expressions at compile time.

[rd: Future versions of the compiler will rewrite declarations like this into legal SAOL code when Sfx programs are exported to bitstream format. This feature should be considered experimental. I'm not sure, from a standards point of view, whether this is a good idea since it poses a terrible problem for cross-compilers: constant evaluation on a cross-targeted compile becomes a major problem since the host system can't match the results of evaluating these expressions on the target machine; that being said, the nature of MPEG4-SA makes it unlikely that cross-compilers are very useful.].

Overloaded Opcodes

Sfx allows multiple opcodes with the same name to be declared as long as the arguments of those opcodes do not match. Sfx chooses the correct opcode based on a comparison of 'goodness of fit' of the rates and datatypes of the actual parameters, and the formal parameters of each opcode.

In addition, opcode arguments may be declared as optional by specifying a default value. The grammar of an optional opcode argument is:

    oparg: typedecl IDENT [ '=' const_expression ]opt

where const_expression is any valid expression derived strictly from constant values.

Disambiguation of overloaded opcodes is performed in a manner similar to Java, rather than C++. Each actual argument is compared against the formal parameter of the declared opcode, and a fit penalty is calculated as follows. For each argument, sum the type mismatch penalties given in the following table:

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 Opcode Arguments

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.

Writing New Opcodes in C++

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.

  1. Declare the opcode so that Sfx programs can access it.
  2. Implement the opcode in C++.
  3. (Optional) If you want the extern opcode to be usable in oparrays, the extern opcode must be wrapped in a standard Sfx/SAOL opcode [-rd: this may be fixed in future releases of Sfx, although it's currently a low priority].
Mapping Sfx Datatypes to C++ Datatypes

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

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.

Implementing the Opcode

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.

Implementing xsig Opcodes

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);
    };

};

Wrapping the Opcode

[-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

Copyright © 2000 Robin Davies.
All Rights Reserved.
Contact: rerdavies@msn.com