From The MPEG-4 Structured Audio Book by John Lazzaro and John Wawrzynek.

Part II/3: SAOL Simple Core Opcodes

Sections

Core Opcodes:

abs acos aexprand agaussrand alinrand ampdb apoissonrand arand asin atan ceil cos cpsmidi cpsoct cpspch dbamp exp floor frac gettempo gettune iexprand igaussrand ilinrand int irand kexprand kgaussrand klinrand kpoissonrand krand log log10 max midicps midioct midipch min octcps octmidi octpch pchcps pchmidi pchoct pow settempo settune sgn sin sqrt

Introduction

Core opcodes provide access to commonly-used algorithms through a function call syntax. Core opcodes serve the same role in SAOL that library functions serve in C.

We introduced SAOL core opcodes in the second example in the tutorial in Part I. The right panel shows an assignment statement from this example that includes calls to the core opcodes sin and cpsmidi.

In this chapter, we take a second look at core opcodes. We describe how to use core opcodes in SAOL expressions and statements, and introduce five rate rules that apply to core opcodes.

Like our rate rules in Part II/2, our core opcode rate rules are more conservative than the SAOL language standard, but easier to understand and remember.

We also describe 50 of the core opcodes that compute simple operations. These opcodes are a mix of general-purpose functions and utilities specialized for music and audio.

From the tutorial:


a = 2*sin(3.1415927*
          cpsmidi(num)/s_rate);

Core Opcode Semantics

Opcode calls act as atomic elements in expressions. The value of the opcode call is computed as a function of its parameters. This computation occurs as the expression is being evaluated.

Core opcode semantics are defined in the MP4-SA standard. SAOL also has user-defined opcodes, whose semantics are specified by an opcode definition written in SAOL itself.

Since opcode calls act as atomic elements in expressions, they have a rate and a width. In the general case, determining the rate and width of a SAOL opcode may be quite subtle.

In Part IV/4 of the book we describe the rate and width semantics for user-defined opcodes. In this section, we introduce several simple rules that apply to the core opcodes we introduce in Parts II and III of the book.

Core opcode definitions include (1) a header syntax for the opcode that defines the structure of the opcode call and (2) the semantics for computing the return value. All core opcodes return a scalar value.

 

Rate and Width Rules

The rate of fixed-rate opcodes is indicated in its header syntax. The keywords iopcode, kopcode, or aopcode indicate that an opcode is i-rate, k-rate, or a-rate respectively. The header syntax also indicates the rate of each opcode parameters.

For example, the panel on the right shows the header syntax for the fixed-rate core opcode krand, which returns random numbers with a uniform distribution at the k-rate.

The header for krand also defines the signal parameter p. This parameter sets the range of random numbers returned by krand to be from -p to p.

For fixed-rate core opcodes, it is simple to apply our first rate rule:

  1. To calculate the rate of an expression that includes an opcode call, treat the opcode call like a signal variable that has the rate of the opcode.

The right panel shows examples that demonstrate rule 1, with expressions that include several calls to the opcode krand. These calls include a scalar expression within the parenthesis. To compute the value of krand, the expression is first evaluated, and then used to compute the return value.

Note that p is declared as a ksig in the header syntax for krand. Our second rate rule concerns the meaning of this declaration:

  1. If a core opcode has a signal parameter declared at a certain rate, the expression for that parameter in an opcode call must not have a faster rate.

The right panel shows a krand example that breaks rule 2.

krand

Header Syntax

kopcode krand(ksig p)

Examples

ivar i;
ksig k;
asig a;

// rule 1 examples:


// legal assignment to ksig k

k = i*krand(5*i); // k-rate expr
                  

// illegal assignment to ivar i

i = i*krand(5*i); // k-rate expr



// statement breaks rule 2:

k = krand(a + 0.5);

The semantic specification of a fixed-rate core opcode may indicate the rate that a computation takes place. For example, the semantic description of krand states that the opcode generates a new random number when called at the k-rate.

However, it is possible for the core opcode to be called at a faster rate. For example, a krand call on the right-hand side of an assignment to an a-rate variable will occur at the a-rate (see right panel).

Our third rate rule defines how SAOL programs execute in this situation:

  1. If a k-rate opcode is used in an a-rate statement, it executes the first time the statement runs in the execution cycle, and its return value is stored for future use. For all subsequent opcode calls in the same execution cycle, the stored return value is reused: the opcode does not execute a second time.

We can apply rule 3 to the example on the right panel. The krand call in this example generates a new random number on the first a-pass of an execution cycle. On the second a-pass, SAOL reuses the random number krand returned on the first a-pass. SAOL continues reusing this number until the first a-pass of the next execution cycle, at which time krand executes again and a new random number is generated.

Note that if we wished to generate a new random number for each statement execution, we would choose the a-rate arand core opcode instead of the k-rate kand core opcode.

Our fourth rate rule defines what happens when an i-rate opcode runs at a faster rate:

  1. If a i-rate opcode is used in a k-rate or a-rate statement, it executes the first time the statement runs, and its return value is stored for future use. For all subsequent opcode calls during the instrument's lifetime, the stored return value is reused: the opcode does not execute a second time.

Note that rule 4 takes the same general approach to faster-rate execution as rule 3: execute once, then reuse the returned value.

Example


ksig k;
asig a;

// krand called at a-rate

a = krand(5); 
 

Not all opcodes are fixed-rate opcodes, and not all signal parameters in the header syntax for an opcode have a declared rate.

The right panel shows the header syntax for the core opcode pow, which raises a number to a power. The keyword opcode indicates that pow has no fixed rate, but rather is a rate-polymorphic opcode. Its two signal parameters x and y are also rate-polymorphic, as indicated by their xsig declaration.

Our final rate rule concerns rate-polymorphic opcodes.

  1. For rate-polymorphic core opcodes that only have rate-polymorphic signal parameters, the rate of an opcode call is the rate of the fastest expression in the opcode call.

Once the rate of the opcode call is determined, rule 1 is used to determine the rate of an expression that contains the call. The semantics of rate-polymorphic core opcodes are not affected by the rate of the call.

The examples on the right panel demonstrate rule 5.

Two width rules for core opcodes are mentioned in the description above and formalized below:

  1. Core opcodes have scalar width.
  2. All core opcode signal parameters are scalar width. Opcode calls must have scalar width expressions for these parameters.

pow

Header Syntax

opcode pow(xsig x, xsig y) 

Example Calls

ivar i;
ksig k;
asig a;

// pow calls are i-rate
// statements are i-rate

i = pow(2, 2);
i = pow(i*2, 2);

// pow calls are k-rate
// statement is k-rate

k = pow(i, pow(2, k+1));

// pow calls are a-rate
// statements are a-rate

a = pow(a, k);
a = pow(a, i);

// pow call is a-rate
// statement is k-rate
//
// illegal assignment statement

k = pow(i, a+1);

Internal State

Opcodes may have internal state. On the right panel, we introduce the core opcode delay1 to explain the semantics of opcode state.

The delay1 opcode has an internal variable, that holds a signal value. When the delay1 opcode is called, it returns the current value of its internal variable, and then sets the internal variable to the value of the calling parameter x.

We can now express the single semantic rule for opcode state:

  1. If a core opcode has internal state variables, each syntactically distinct opcode call has its own set of internal state variables.

The core fragment on the right panel has three syntactically distinct calls to delay1. This code fragment implements a three-stage delay line, because each call to delay1 has its own internal state variable.

In the following sections, we introduce sets of simple core opcodes that are useful in general-purpose programs.

delay1

Header Syntax

aopcode delay1(asig x) 

Example


asig a1, a2, a3, a4;

a2 = delay1(a1);
a3 = delay1(a2);
a4 = delay1(a3);

Transcendentals

The transcendental core opcodes are all rate-polymorphic. See the right panel for header syntax.

Trigonometric Opcodes

The forward trigonometric opcodes take angle parameters in units of radians, and the inverse trigonometric opcodes return angle values in radians.

The asin and acos opcodes may only be called with parameters in the range [-1, 1].

The asin and atan return values in the range [-pi/2, pi/2), and the acos returns values in the range [0, pi).

Note that tan is missing from the core opcode library, as well as hyperbolic functions. The Slib library includes replacements for these missing functions, and the related constants pi and e.

Logs and Powers

The log and log10 opcodes compute the natural and base-10 logarithms respectively, and may only be called with positive parameters. The exp opcode computes the exponential function.

The sqrt opcode returns the square root of the parameter, and may not be called with negative parameters. The pow opcode returns the parameter x raised to the y power; if y is not an integer value, x may not be negative.

Trigonometric

opcode sin(xsig x)
opcode cos(xsig x)
opcode acos(xsig x)
opcode asin(xsig x)
opcode atan(xsig x)


See Slib for other
useful trigonometric 
functions like tan() 
and tanh(), and the 
constants pi and e.

Logs and Exp

opcode log(xsig x)
opcode log10(xsig x)
opcode exp(xsig x)

Powers

opcode sqrt(xsig x)
opcode pow(xsig x, xsig y)

Quantization

Five rate-polymorphic core opcodes implement functions that relate to rounding and quantization. See the right panel for header syntax.

The int opcode extracts the integer part of a parameter x and returns it as a floating-point value. It implements the C expression (float)((int)(x)).

The frac opcode returns the signed fractional part of a parameter x. It implements the SAOL expression x - int(x).

The floor opcode returns the greatest integral value y so that y<=x. The ceil opcode returns the smallest integral value y so that x<=y.

The sgn opcode returns 1.0 if the parameter x is positive, -1.0 if x is negative, and 0.0 if x is zero.

Quantization

opcode int(xsig x)
opcode frac(xsig x)
opcode floor(xsig x)
opcode ceil(xsig x)
opcode sgn(xsig x)

Min, Max, and Abs

The rate-polymorphic min and max core opcodes return the smallest and largest of their parameters respectively. See the right panel for header syntax.

These two opcodes must be called with at least one parameter, but may be called with an arbitrary number of parameters. The right panel introduces the header syntax notation for a variable number of opcode parameters.

The rate-polymorphic abs core opcode returns the absolute value of the parameter x.

Min, Max, and Abs


opcode min(xsig x1[, xsig ...])
opcode max(xsig x1[, xsig ...])

opcode abs(xsig x)

Pseudorandom Generators

Random Numbers

A set of 12 fixed-rate core opcodes return pseudorandom numbers. Opcodes that return numbers at the i-rate, k-rate, and a-rate are provided for four different probability distributions.

The right panel shows the header syntax, grouped by distribution. Note that opcode parameters have the same rate as the opcodes themselves. The probability distributions are also shown.

The irand, krand, and arand opcodes generate pseudo-random numbers with a linear distribution in a range defined by the parameter p1. Numbers are uniformly distributed in the interval [-p1, p1].

The igaussrand, kgaussrand, and agaussrand opcodes generate pseudo-random numbers with a Gaussian distribution with mean mean and variance var, where mean and var are parameters. The var parameter must have a value greater than zero.

The iexprand, kexprand, and aexprand opcodes generate pseudo-random numbers with an exponential (Poisson) distribution. The parameter p sets the single parameter of the distribution. and must have a value greater than zero.

The ilinrand, klinrand, and alinrand opcodes generate pseudo-random numbers with a ramp distribution defined by the parameters p1 and p2. See the right panel for details.

All calls to these 12 core opcodes share a common source of pseudorandom numbers. The opcodes themselves have no internal state.

Binary Random Sequences

The core opcodes kpoissonrand and apoissonrand generate random sequences of binary return values according to a Poisson distribution. These opcodes return 1.0 if an event occurs and 0.0 otherwise. Each opcode has a single parameter p1.

The opcode kpoissonrand generates sequences at the k-rate, using a Poisson distribution with a parameter dependent on the krate as well as p1. See the right panel for details.

The opcode apoissonrand generates sequences at the a-rate, using a Poisson distribution with a parameter dependent on the arate as well as p1.

Calls to these two core opcodes share the same common source of pseudorandom numbers as the 12 random number core opcodes.

However, these two opcodes do have an internal state variable, that is used in the sequence generation algorithm. As explained in an earlier section, each syntactically distinct call to these opcodes accesses its own copy of the internal state variable.

Random Numbers

Uniform Distribution

iopcode irand(ivar p1)
kopcode krand(ksig p1)
aopcode arand(asig p1)

prob(x) = 1/(2*p1), if x in [-p1, p1]
prob(x) = 0         otherwise

Gaussian Distribution

iopcode igaussrand(ivar mean, ivar var)
kopcode kgaussrand(ksig mean, ksig var)
aopcode agaussrand(asig mean, asig var)

prob(x) = (1/sqrt(2*pi*var))*
       exp(-(mean-x)*(mean-x)/(2*var))

Poisson Distribution

iopcode iexprand(ivar p1)
kopcode kexprand(ksig p1)
aopcode aexprand(asig p1)

prob(x) = (1/p1)*exp(-x/p1), if x > 0 
prob(x) = 0                  otherwise

Linearly Ramped Distribution

iopcode ilinrand(ivar p1, ivar p2)
kopcode klinrand(ksig p1, ksig p2)
aopcode alinrand(asig p1, asig p2)

prob(x) = abs(d*
	  (x-p1))    if x in [p1, p2]
prob(x) = 0          otherwise

where d = 2/((p2 - p1)*(p2 - p1))

Binary Random Sequences

kopcode kpoissonrand(ksig p1)

generates binary (0/1) events
using the distribution 

prob(x) = (1/(k_rate*p1))
          *exp(-x/(k_rate*p1))
  

aopcode apoissonrand(asig p1)

generates binary (0/1) events
using the distribution 

prob(x) = (1/(s_rate*p1))
          *exp(-x/(s_rate*p1))

both pdfs for (x > 0) only.

Loudness

Linear increases in the amplitude of a sound waveform result in only logarithmic increases in the perceived loudness of the waveform. Therefore, waveform amplitude is often converted to the decibel (dB) scale to express loudness.

SAOL has two rate-polymorphic core opcodes that handle conversion between amplitude units and decibel units. The right panel shows the header syntax for these opcodes and the equations the opcodes compute.

The dbamp opcode converts the amplitude parameter x into decibels. The amplitude value 1.0 maps to 90dB. Only x values greater than zero may be used.

The ampdb opcode converts the decibel parameter x into amplitude, using the same scaling as dbamp.

These core opcodes introduce the SAOL convention for conversion opcodes: the return unit (db for dbamp) is the first part of the opcode name, and the parameter unit (amp for dbamp) is the second part of the name.

Loudness


opcode dbamp(xsig x)

computes 90 + 20 log10(x) for x > 0



opcode ampdb(xsig x)

computes pow(10,(x - 90)/20)

Pitch

Sound generation algorithms usually require frequency variables in units of Hz (cycles per second). Western musicians usually label pitches with the names of notes on a piano keyboard (i.e. F# two octaves below middle C). Several numerical encodings of equally-tempered note names are used in computer music.

A set of 12 rate-polymorphic core opcodes handles conversions between three popular note name encodings and cycles per second. The right panel shows the header syntax for these opcodes.

These opcodes are named by the concatenation of the abbreviations for the four pitch types (cps, midi, pch, oct). The return type starts the opcode name.

The cps type is cycles per second. Opcode parameters that are in cps notation must be greater that zero.

The midi type is MIDI note numbers, which are integer values between 0 and 127 representing notes on a piano keyboard. MIDI note number 57 is A below middle C. MIDI note number return values always have integral values.

The oct type is octave-fraction notation, that uses floating point numbers. The integer part of the number is the octave number, where 8 is the octave starting with middle C. The fractional part is the note within the octave, where 1/12 represents a semitone. For example, 7.75 is the A below middle C.

The opcodes that return the oct type may return values between the 1/12 note steps, except for the octpch opcode. Opcode parameters that are in oct notation must be greater that zero.

The pch type is pitch class notation, that uses floating point numbers. The integer part of the number is the octave number, where 8 is the octave starting with middle C. The fractional part is the note within the octave, where a 0.01 increment is a semitone. For example 7.09 is the A above middle C.

Opcodes that return pch type generate values quantized to the nearest semitone. Opcodes parameters in pch notation are rounded to the nearest semitone, and parameter values greater than 0.11 are set to zero.

By default, conversions between cps and the pitch representations assume that the A above middle C is 440.0 Hz. The k-rate core opcode settune changes this default to the value of the parameter x.

The rate-polymorphic core opcode gettune returns the current global tuning value. The parameter dummy is simply used to set the rate of the opcode. If dummy is omitted the opcode is krate.

Pitch Conversion

opcode cpsmidi(xsig x)
opcode cpspch(xsig x)
opcode cpsoct(xsig x)

opcode midicps(xsig x)
opcode midipch(xsig x)
opcode midioct(xsig x)

opcode pchcps(xsig x)
opcode pchmidi(xsig x)
opcode pchoct(xsig x)

opcode octcps(xsig x)
opcode octmidi(xsig x)
opcode octpch(xsig x)


See Slib for useful constants
for pitch calculations.

Tuning

kopcode settune(ksig x)
opcode gettune([xsig dummy])

Conversion Table

 MIDI  PCH   OCT       CPS (A440)

C  36  6.00  6.000       65.40
C# 37  6.01  6.083...    69.29
D  38  6.02  6.166...    73.41
D# 39  6.03  6.250       77.78
E  40  6.04  6.333...    82.40
F  41  6.05  6.416...    87.30
F# 42  6.06  6.500       92.49
G  43  6.07  6.583...    97.99
G# 44  6.08  6.666...   103.82
A  45  6.09  6.750      110.00
A# 46  6.10  6.833...   116.54
B  47  6.11  6.916...   123.47
C  48  7.00  7.000      130.81
C# 49  7.01  7.083...   138.59
D  50  7.02  7.166...   146.83
D# 51  7.03  7.250      155.56
E  52  7.04  7.333...   164.81
F  53  7.05  7.416...   174.61
F# 54  7.06  7.500      184.99
G  55  7.07  7.583...   195.99
G# 56  7.08  7.666...   207.65
A  57  7.09  7.750      220.00
A# 58  7.10  7.833...   233.08
B  59  7.11  7.916...   246.94   
C  60  8.00  8.000      261.62 <--  
C# 61  8.01  8.083...   277.18
D  62  8.02  8.166...   293.66
D# 63  8.03  8.250      311.12
E  64  8.04  8.333...   329.62
F  65  8.05  8.416...   349.22
F# 66  8.06  8.500      369.99
G  67  8.07  8.583...   391.99
G# 68  8.08  8.666...   415.20
A  69  8.09  8.750      440.00
A# 70  8.10  8.833...   466.16
B  71  8.11  8.916...   493.88
C  72  9.00  9.000      523.25

Arrow denotes middle C.
... denotes repeating digit


Tempo

We introduced the SASL tempo command in the second example in the tutorial in Part I. This score command changed the relationship between score time and simulated time from the default 60 score beats per minute.

The global tempo can also be changed in SAOL, through the k-rate core opcode settempo. The new tempo parameter x must be set to a value greater than zero.

A companion rate-polymorphic opcode, gettempo, returns the current tempo. The optional parameter dummy sets the rate of the opcode, which defaults to k-rate.

Tempo


kopcode settempo(ksig x)
opcode gettempo([xsig dummy])


See Slib for useful constants
for tempo calculations.

Summary

We have now finished our description of the 50 core opcodes that are useful when writing algorithms at the lowest level of abstraction.

The remaining core opcodes work at higher level of abstractions, often implementing complete synthesis and effects methods in a single opcode call.

We introduce these opcodes, often in conjunction with descriptions of related SAOL language features, in the remainder of the book.

The right panel has a summary of the rate and width rules for core opcodes.

Next section: Part II/4: Wavetables

Core Opcode Rate Rules:

  1. To calculate the rate of an expression that includes an opcode call, treat the opcode call like a signal variable that has the rate of the opcode.
  2. If a core opcode has a signal parameter declared at a certain rate, the expression for that parameter in an opcode call must not have a faster rate.
  3. If a k-rate opcode is used in an a-rate statement, it executes the first time the statement runs in the execution cycle, and its return value is stored for future use. For all subsequent opcode calls in the same execution cycle, the stored return value is reused: the opcode does not execute a second time.
  4. If a i-rate opcode is used in a k-rate or a-rate statement, it executes the first time the statement runs, and its return value is stored for future use. For all subsequent opcode calls during the instrument's lifetime, the stored return value is reused: the opcode does not execute a second time.
  5. For rate-polymorphic core opcodes that only have rate-polymorphic signal parameters, the rate of an opcode call is the rate of the fastest expression in the opcode call.

Core Opcode Width Rules:

  1. Core opcodes have scalar width.
  2. All core opcode signal parameters are scalar width. Opcode calls must have scalar width expressions for these parameters.

Core Opcode State Rule:

  1. If a core opcode has internal state variables, each syntactically distinct opcode call has its own set of internal state variables.


Copyright 1999 John Lazzaro and John Wawrzynek.