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

Part II/4: SAOL Wavetables

Sections

Core Opcodes:

doscil ftbasecps ftlen ftloop ftloopend ftsetsr ftsetbase ftsetloop ftsetend ftsr koscil loscil oscil speedt tableread tablewrite

Wavetable Generators:

buzz concat cubicseg data empty expseg harm harm_phase lineseg periodic polynomial random sample spline step window

Introduction

Sound generation methods often involve the playback of audio samples from large data buffers.

These buffers may contain computed waveforms, digital recordings of natural sounds, or may be a scratch pad memory for dynamic synthesis.

The SAOL language includes the table data type for implementing wavetable buffers. A wavetable stores a sample array and a header of related information (including table length, sample rate, and loop points) in one structure.

A set of core opcodes support wavetable playback. Other core opcodes read and write parts of the wavetable data structure. Wavetable generators may be used to initialize wavetable values at declaration.

We begin this chapter with an example that introduces wavetable operation. We then detail the table declaration syntax, and describe wavetable generators and core opcodes for the most common table applications.

We conclude with descriptions of the wavetable core opcodes and generators that are useful for programming at a low level of abstraction.

 

An Example

The second example in the tutorial in Part I uses a shaped sine wave instrument model. In the tutorial example, the sine wave is computed used an iterative algorithm and the envelope is computed using a core opcode.

The right panel shows another implementation of this instrument, that uses wavetables to generate the sine wave and the envelope. We use this example to introduce the basic usage of the table data type.

The wavetable used to generate the sinusoid, cyc, is declared as a global variable, and imported into the instrument vtone.

A table declaration includes instructions for setting the initial values of the table. In the declaration for cyc, the wavetable generator harm initializes the 128-sample wavetable with one cycle of a sine wave.

The wavetable shape holds the envelope for the instrument, and is declared in vtone. The lineseg generator, specialized for creating piece-wise linear envelopes, initializes shape.

The shape declaration shows that the parameters of a generator may be expressions. In this case, we use the standard name dur to customize the envelope to match the length of the note.

The code block for vtone is very simple. One i-pass statement converts the MIDI note number parameter into a frequency. One k-pass statement uses the core opcode ftsetsr to set the sample rate of the envelope wavetable.

The real work is done in the a-pass. Two core opcodes, doscil and oscil, play back the envelope and sine wave tables. The return values of these opcodes are multiplied to create the final waveform, which is sent to the output_bus.

The a-rate core opcode doscil plays a table back once. The opcode does sample rate conversion between the sample rate of the table and the global sample rate.

The a-rate core opcode oscil plays a table back in a loop, treating the table as a single cycle of a periodic waveform.

By default, the table playback core opcodes uses linear interpolation.

We used the SASL file from the second tutorial example to drive the vtone instrument. If you are connected to the Internet, and if your web browser supports WAV file playback, you can click here to listen to audio output created using sfront.

We also used sfront to create a binary encoding of the SAOL and SASL file for this performance. This MP4 file is 399 bytes.

Note that this file is small, even though the tables we used for the envelopes are large, because only the wavetable generator parameters are stored in the file. The initial values for the tables themselves are computed as part of the file decoding process.

This example shows how table data types are used in SAOL. In the rest of this chapter, we describe table declarations, applications-oriented wavetable generators, playback core opcodes, and low-level core opcodes and generators in detail.

tsine.saol

global {

  table cyc(harm, // sine series
	    128,  // 128 samples long
	    1     // f1 weight
	    );    // no partials

  srate 44100;   

}

//
// instr vtone
// table-driven version
// of tutorial example 2
//

instr vtone (num) {

  // declarations

  // sinewave global table

  imports exports table cyc; 
 
  // envelope table
  //
  // piecewise linear
  // shape
  //
  // 0.3 second attack 
  // 0.2 second release
  
  table shape(lineseg,

  // fixed table length: 128 elements

  128, 

  // (x0, y0) is (0,0)

  0, 0,      

  // end of attack segment: (x1, y1)

  127*(((dur < 0.5) ? 
       dur/2 : 0.3)/dur), 1,

  // end of sustain segment: (x2, y2)

  127*(((dur < 0.5) ? 
       dur/2 : (dur - 0.2))/dur), 1,

  // end of release segment: (x3, y3)

  127, 0);

  ivar freq; // frequency of sine

  asig y;    // voice output


  // **********************
  // computed during i-pass
  // **********************

  // turns MIDI number into 
  // wavetable frequency

  freq = cpsmidi(num);
  
  // **********************
  // computed during k-pass
  // **********************

  if (itime == 0) // first k-pass only
    {
      ftsetsr(shape, 128/dur);
    }

  // **********************
  // computed during a-pass
  // **********************

  y = doscil(shape)*oscil(cyc, freq);
  
  output(y);

}

output.wav [464 Kbytes, on the Web]

tsine.mp4 [399 bytes]

Declarations

The right panel shows the table declaration syntax. The table name must adhere to the naming rules described in Part II/1. A set of parameters, enclosed in parenthesis, complete the declaration.

The first parameter must be a valid wavetable generator name. The right panel shows the 16 SAOL wavetable generators.

The second parameter is an expression that sets the number of samples in the table. The remaining parameters have a different meaning for each wavetable generator. For most generators, these fields are signal expressions.

As shown in the example in last section, table declarations may happen in the global block or in an instr block.

For global wavetables, the expressions may use i-rate global variables. These variables may be initialized by the startup instr which runs before global wavetables are created.

For instr wavetables, the expressions may use instr parameters, imported i-rate global variables, and i-rate standard names.

Importing Tables

An instr accesses a global wavetable by declaring an instr wavetable with the same name as the global wavetable. The example on the right panel shows the two forms of declaration.

The imports exports declaration accesses the global wavetable directly using a pointer. Changes made to the wavetable structure by an instr affect the global copy.

The imports declaration makes a copy of the global wavetable at instr instantiation. Changes to the instr table do not change the global data.

The imports declaration may be used even if no global wavetable with the same name exists. In this case, the table is created by a SASL table command, which we explain in Part III/1 of the book.

Tablemaps

Some wavetable applications create a set of related sample buffers, and play back one buffer from the set. For example, a sampled piano instrument may use a set of 88 wavetables, one for each note on the piano keyboard.

To support these applications, SAOL has a special data type, the tablemap, that is an array of wavetables. The right panel shows the tablemap declaration syntax and an example.

In this example, we declare the tables cyc0, cyc1, cyc2. We also declare the tablemap cyc, that is an array of wavetables.

In the code block, we index cyc to select a wavetable argument for the core opcode oscil. Unindexed tablemap variables may not be passed as opcode arguments.

The index for a tablemap is an expression whose value is rounded to the nearest integer to select a wavetable. The first wavetable in a tablemap has the array index 0.

Declaration Syntax

table name(gen, size [, p1, p2 ...]);

Wavetable Generators

buzz
concat
cubicseg
data
empty
expseg
harm
harm_phase
lineseg
periodic
polynomial
random
sample 
spline
step
window

Imports Example

globals {
table cyc0(harm, 128, 1);
table cyc1(harm, 128, 0.5, 0.25);
table cyc2(harm, 128, 0.5, 0.1);
}

instr test {

imports exports table cyc0;// form 1
imports table cyc1, cyc2;  // form 2

}

Tablemap Declaration

tablemap name(t1 [, t2, t3 ...]);

Tablemap Example


instr tmap (freq, bright) {

table cyc0(harm, 128, 1);
table cyc1(harm, 128, 0.5, 0.25);
table cyc2(harm, 128, 0.5, 0.2, 0.1);
tablemap cyc(cyc0, cyc1, cyc2);

output(oscil(cyc[bright],freq));

}

Periodic Waveforms

Three wavetable generators (harm, phase_harm, and buzz) produce a single cycle of a periodic waveform. Looped playback of wavetables produced with these generators is click-free. The definitions of these generators are shown on the right panel, along with the related generator periodic.

Harm

The simplest generator, harm, specifies a waveform as a harmonic series of N zero-phase sine waves. Generator parameters include the size of the table, which must be greater than zero, and the N sinusoid amplitudes.

We use this generator in the example at the start of the chapter to produce a pure tone.

x is table index
y(x) is table value

harm

 
declaration:

table t(harm, size, a1 [, a2, a3 ...]);

algorithm:

for x in [0, size-1]:

y(x) =
a1*sin(2*pi*x/size) +
a2*sin(4*pi*x/size) +
a3*sin(6*pi*x/size) +
...

Harm_phase and Periodic

The harm_phase generator specifies an amplitude parameter and a phase parameter (specified in radians) to each sinusoid, and the periodic generator also adds a frequency parameter.

Note that if the frequency parameters to the periodic generator are non-integral, the looped wavetable produces a discontinuous waveform.

harm_phase

 
declaration:

table t(harm_phase, size, a1, ph1
	[, a2, ph2, a3, ph3 ...]); 

algorithm:

for x in [0, size-1]:

y(x) =
a1*sin(ph1 + 2*pi*x/size) +
a2*sin(ph2 + 4*pi*x/size) +
a3*sin(ph3 + 6*pi*x/size) +
...

periodic

 
declaration:

table t(periodic, size, f1, a1, ph1
   [, f2, a2, ph2, f3, a3, ph3 ...]); 

algorithm:

for x in [0, size-1]:

y(x) =
a1*sin(ph1 + 2*pi*f1*x/size) +
a2*sin(ph2 + 2*pi*f2*x/size) +
a3*sin(ph3 + 2*pi*f3*x/size) +
...

Buzz

Newcomers to computer music often try to duplicate the pulse waveforms used in analog subtractive synthesis directly, by writing a program to generate a pulse waveform. This approach can lead to unsatisfactory results, due to aliasing problems.

A better way is to generate pulse-like waveforms additively into a table, stopping near the Nyquist limit (half the sampling frequency) to prevent aliasing. The harm generator could be used to create a pulse waveform in this way, by specifying a series of sinusoids with decreasing amplitudes.

The buzz generator simplifies this task. It specifies an algorithm for generating a family of alias-free pulse-like waveforms using a few parameters.

The right panel shows the algorithm for buzz. The generator sums a series of harmonic cosine functions starting at the partial specified by parameter low, where partial 0 is the fundamental.

The first cosine waveform has unity scaling, while subsequent cosines are scaled by increasing powers of the parameter r. Since r has no restrictions on its value, amplitude series that are decreasing (abs(r) < 1) and increasing (abs(r) > 1) may be generated. The phase of successive partials may also be flipped by specifying a negative r.

The two remaining parameters are the number of partials num and the size of the wavetable size. If both are specified as positive values, the equation on the right panel is computed. Note that a bad choice of parameters will result in aliasing.

Alternatively, either size or num may be set to -1. In this case, the generator chooses the smallest wavetable size or the largest number of harmonics, respectively, that does not alias.

Finally, the buzz generator scales the final waveform to have a peak amplitude independent of the parameters.

buzz

 
declaration:

table t(buzz, size, num, low, r);


algorithm:

for x in [0, size-1]:

y(x) =
cos(2*pi*(low+1)*x/size) +
r*cos(2*pi*(low+2)*x/size) +
r*r*cos(2*pi*(low+3)*x/size) +
... +
r^(num)*cos(2*pi*(low+num+1)*x/size)


scale table by:

(1 - abs(r))/(1 - abs(r^num))


nyquist scaling if:

size = -1: table size scaled to 
           largest harmonic low+num

num = -1: largest num that fits 
	  the specified table size

size and num may not both be -1

Envelope Waveforms

Five generators (step, lineseg, expseg, spline, and cubicseg) produce wavetables that are useful for signal envelopes. For each description, see the right panel for parameter syntax and algorithms.

Shared properties

For each generator, the envelope is specified as a piece-wise waveform. Table break points are listed as pairs of y table values and x table indices. The generators interpolate between the break points to create a waveform.

The first table break point must have an index parameter of zero, and the sequence of table index values must not be decreasing. Setting two consecutive x indexes to the same value is permitted, and produces a discontinuous waveform. In this case, the second y value becomes the actual wavetable value for the x index, while the first y value fixes the trajectory of the previous piece-wise segment.

The wavetable size may be specified directly by setting the size parameter to a value greater than zero. If the size parameter is set to -1, the table size is inferred from the last break point specified.

 

Interpolation methods

The step generator does no interpolation. The waveform it produces looks like a stair step. For two adjacent break points (x1, y1) and (x2, y2), the table takes on the value y1 for x1 <= x < x2.

step

 
declaration:

table t(step, size, x1, y1, 
        x2 [, y2,  x3 ...]);

algorithm:

for x in [0, size-1]:

x1 <= x < x2  --> y(x) = y1
x2 <= x < x3  --> y(x) = y2
...

error if:

-- x1 != 0
-- series of xk values decreases
-- series ends with a yk

The lineseg generator does linear interpolation between break points. We use the lineseg generator in the example at the start of the chapter.

lineseg

 
declaration:

table t(lineseg, size, x1, y1, 
        x2, y2 [, x3, y3 ...]);

algorithm:

for x in [0, size-1]:

if x1 <= x < x2
  y(x) linearly interpolates
  between y1 and y2  

if x2 <= x < x3
  y(x) linearly interpolates
  between y2 and y3  

...

error if:

-- x1 != 0
-- series of xk values decreases
-- series ends with a xk

The expseg generator does exponential interpolation between break points. Note that the generator algorithm restricts the y values to be nonzero and of uniform sign.

expseg

 
declaration:

table t(expseg, size, x1, y1, 
        x2, y2 [, x3, y3 ...]);

algorithm:

for x in [0, size-1]:

if x1 <= x < x2
  y(x) exponentially interpolates
  between y1 and y2  
  i.e.
   y(x) = y1*(y2/y1)^((x-x1)/(x2-x1))

if x2 <= x < x3
  y(x) exponentially interpolates
  between y2 and y3  

...

error if:

-- x1 != 0
-- series of xk values decreases
-- all yk's aren't the same sign
-- any yk is zero
-- series ends with a xk

The spline generator interpolates between break points using cubic splines.

Generating the spline interpolation requires solving a third-order algebraic equation. For some break points, this equation may not have a solution.

To avoid this situation, the expression x1*x1 + x2*x2 - 2*x1*x2 should not be equal to zero.

spline

 
declaration:

table t(spline, size, 
            x1, y1,
       [k2, x2, y2,
        k3, x3, y3,]
        kn, xn, yn);  

algorithm:

for x in [0, size-1]:

if x1 <= x < x2
  y(x) fits a cubic spline
  between y1 and y2  
  i.e.
   y(x) = a*x^3 + b*x^2 + cx + d
   where this function passes
   through (x1,y1) and (x2,y2)
   and has derivative 0 at x1
   and k2 and x3

if x2 <= x < x3
  y(x) fits a cubic spline
  between y2 and y3  
  i.e.
   y(x) = a*x^3 + b*x^2 + cx + d
   where this function passes
   through (x2,y2) and (x3,y3)
   and has derivative k2 at x2
   and k3 at x3

...

derivative of final spline 
ending is 0 (kn is ignored).

error if:

-- x1 != 0
-- series of xk values 
   is decreasing
-- series does not end
   with a yk
-- a spline doesn't exist (see text)

The cubicseg generator also interpolates between break points using a cubic formula.

The parameter specification for cubicseg is different than the other generators in this family. The x values are specified via the series infl1, x1, infl2, x2 .... The first x value infl1 must be zero.

While the spline generator creates gently changing waveshapes, the cubicseg generator can be used to generate a spiked waveshapes as well as smooth waveshapes.

Like the spline generator, care must be taken to ensure that the cubic equation by the parameters specified is solvable. These conditions are too complex to calculate manually. The cubicseg generator is best used with a computer program for cubic curve design.

cubicseg

 
declaration:

table t(cubicseg, size, infl1, y1,
         x1, y2, infl2, y3,
         x2, y4, infl3, y5
         [,x3, y6, infl4, y7]);

algorithm:

for x in [0, size-1]:

if infl1 <= x < infl2
  y(x) fits a cubic polynomial
  between y1 and y3  
  i.e.
   y(x) = a*x^3 + b*x^2 + cx + d
   where this function passes
   through (infl1,y1), (x1,y2),
   and (infl2,y3) and has
   derivative 0 at x1.

if infl2 <= x < infl3
  y(x) fits a cubic polynomial
  between y3 and y5  
  i.e.
   y(x) = a*x^3 + b*x^2 + cx + d
   where this function passes
   through (infl2,y3), (x2,y4),
   and (infl3,y5) and has
   derivative 0 at x2.
...

error if:

-- infl1 != 0
-- infl, x, infl, ... series decreases 
-- series doesn't ends with infl, y
-- any polynomial doesn't exist

Sound Files

The SAOL generator sample fills a wavetable with sample data. The right panel shows the declaration syntax for this generator.

When used in ASCII SAOL programs, the sample generator reads in data from an audio file. Because file access is system dependent, the MP4-SA standard leaves exact implementation details to the decoder. In this section, we describe how sfront implements the sample generator.

The second parameter of the generator specifies the name of the audio file to read into the table. The filename is specified in double quotes, and is expected to be in the same directory as the SAOL file. Microsoft WAV files (indicated by .wav and .WAV extensions) and Electronic Arts AIFF files (indicated by .aif and .AIF extensions) are supported by sfront.

The optional skip parameter (rounded to an integer value) specifies the number of samples to skip at the start of the audio file. If skip is specified, sample number skip+1 in the audio file becomes the first sample of the wavetable.

The first parameter of the generator specifies the wavetable size. If the size parameter is set to -1, the sample generator sets the wavetable size to be the number of samples in the audio file (minus the number of skip samples). If the size parameter is set to a positive value, the wavetable takes on that size, and the audio file is truncated or zero-padded as needed.

Sfront supports 8-bit, 16-bit, and 24-bit samples in AIFF and WAV files. The integer audio samples are converted to floating point numbers that lie between -1.0 and +1.0. The sampling rate of the file becomes the sampling rate of the wavetable.

In sfront, the left and right channels in a stereo WAV or AIFF file are summed to mono by default. The right panel shows the @n syntax for selecting a specific channel (left or right) of a stereo file to place in the table.

AIFF and WAV files may also hold information about looped playback the sample. Sfront converts this information into the loop start, loop end, and base frequency of the wavetable.

Bitstream issues

In the previous subsection, we described the way sfront implements the second parameter of the sample generator for ASCII SAOL files. For binary MP4 files, however, the second parameter of this generator has a normative interpretation. This parameter points to another part of the MP4 file, that holds large blocks of sample data.

When sfront generates an MP4 binary encoding of a ASCII SAOL file, it loads the audio data from files specified in sample generators into the part of the MP4 file that holds sample data blocks. It encodes the second parameter of each sample generator to point to the appropriate sample block. When inserting WAV or AIFF file data into an MP4 file, sfront includes any looped playback data stored in the file.

Note that the size of an MP4 binary file expands significantly if large audio sample files are included in the file. Content creators working on low-bitrate applications should consider synthesis-based sound generation techniques as an alternative to the sample generator.

sample

 
declaration:

table t(sample, size, 
	"name.wav" [,skip]);

table t(sample, size, 
	"name.aif" [,skip]);


algorithm:

monaural AIFF or WAV file is read into
table. skip is an optional parameter, 
that specifies how many samples to skip
over at the beginning of the file.

file sample rate is table sample rate.

if size is set to -1, table size is
the number of samples in the file, 
minus skip (if specified). if size is
specified, audio file is truncated
or zero-padded as needed.

table values lie between -1.0 and 
+1.0, by scaling 16-bit signed 
integers from the file by 1/32768.
24-bit samples scale by 1/8388608.
8-bit samples scale by 1/128 (after
re-zeroing, if needed).

if name.wav or name.aif is a stereo
file, sfront averages the left and
right channel to produce a mono
signal. to select a single channel
of a stereo file to place in the
table, sfront supports the following
syntax:

table t(sample, size, 
	"name.wav@0" [,skip]);

table t(sample, size, 
	"name.wav@1" [,skip]);

table t(sample, size, 
	"name.aif@0" [,skip]);

table t(sample, size, 
	"name.aif@1" [,skip]);

Warping Tables

The i-rate core opcode speedt is a useful tool for processing wavetables initialized with the sample generator. It shrinks or expands the length of a wavetable, without changing the pitch of the sample.

The right panel shows the speedt header syntax. The in variable holds the original wavetable. The factor parameter specifies the amount of expansion or compression.

The out wavetable must be large enough to hold the processed table. For example, a factor value of 2 requires an out table twice as large as the in table. Typically, out is initialized at the correct size using the empty wavetable generator.

The algorithm for table warping is non-normative. Different decoders will produce different sample values for a given factor value. In addition, the range of acceptable factor values is also decoder-dependent.

The speedt implementation in sfront expands wavetables up to a factor value of 2, and compresses wavetables down to a factor value of 0.001. To achieve higher expansion ratios, implement a cascade of several speedt calls.

Speedt

iopcode speedt(table in, table out,
	       ivar factor)

Table Playback

The core opcodes oscil, koscil, doscil, and loscil play back wavetables. Each opcode is customized for a particular sound generation method. See the right panel next to each opcode description for the declaration syntax.

Shared properties

The playback opcodes work in a similar way. Each opcode has an internal pointer to the wavetable, that indicates the current sample. The internal pointer always starts at the first sample in the wave table.

When the opcode is called, it returns the current sample, and updates the internal pointer for the next call. The opcodes differ primarily in the method for incrementing the pointer.

The semantics of the playback opcodes assume that the opcode is called once per a-pass (for oscil, doscil, and loscil) or once per k-pass (for koscil). If an opcode is placed inside a while loop and called multiple times per pass, the effective frequency increases by that multiple.

In general, the internal pointer does not increment in an integral fashion. The playback opcodes interpolate the return value from nearby wavetable values.

By default, the playback opcodes do linear interpolation. The interpolation level can be set by the global parameter interp. Setting interp to 0 yields the default linear interpolation.

If interp is set to 1, the MP4-SA decoder uses a more sophisticated method of interpolation. The exact method of interpolation, however, is unspecified by the SAOL language standard, and different decoders may use different methods.

For most opcodes that use interpolation, sfront uses band-limited interpolation if interp is set to 1 (sfront's grain operator is an exception, and uses linear interpolation). Sfront has command-line options to set the accuracy of the band-limited interpolation. Later in this chapter, we discuss special issues when using band-limited interpolation with low-level core opcodes such as tableread.

If the goal is to achieve normative (i.e. decoder-independent) high-quality table playback, the content creator has two options. One option is to use the playback core opcodes with linear interpolation, and use larger table sizes to improve audio quality.

A second option is to write SAOL code to play back tables directly, using the low-level core opcodes.

Interp Global Parameter


global {
interp 0;  // linear 
           // interpolation
}

global {
interp 1;  // non-normative 
           // higher-quality
	   // interpolation
}          

Waveform Playback

Two of the core opcodes, oscil and koscil, are specialized for creating signals by repeating a single cycle of a waveform stored in a table.

These opcodes have two required parameters, a table specifier t and the freq parameter that specifies how many times per second the entire table should be played through.

The freq parameter may be negative, in which case the internal pointer is decremented instead of incremented.

If the loops parameter is provided, the opcode loops around the table loops times, and then returns 0 with every call.

The opcodes differ only in their rate. The a-rate oscil opcode produces signal values as described above if it is called once a-pass, while the k-rate koscil has the described semantics if it is called once per k-pass.

Waveform Playback

aopcode oscil(table t, 
	      asig freq 
	      [,ivar loops])

kopcode koscil(table t, 
	      ksig freq 
	      [,ivar loops])

One-Shot Playback

The a-rate doscil core opcode plays back the wavetable t once, and then returns 0.

The opcode considers the wavetable to be a recording of a pitched sound. In playing the wavetable, it generates a sound whose pitch is identical to the original sound.

If the "recording" sample rate is identical to the SAOL program audio sample rate, doscil simple advances the internal pointer by 1 at each call.

If the two sample rates differ, doscil increments the internal pointer by the ratio of the wavetable sample rate and the audio sample rate.

The wavetable passed to doscil must have its sample rate value set. The ftsetsr core opcode may be used to set the sample rate of wavetable, as we did in the example earlier in the chapter.

By default, a wavetable initialized with the sample generator has its sample rate set to the audio file sample rate.

One-Shot Playback

aopcode doscil(table t)

Loop Playback

The a-rate loscil core opcode expands the semantics of doscil, by playing back the wavetable recording at an arbitrary pitch (specified by the parameter freq).

To play the table back at the correct pitch, loscil needs to know the original pitch of the sound recorded in the wavetable. The optional parameter basefreq supplies this information.

If basefreq is not supplied, the base frequency of the wavetable is used, as set by the core opcode ftbasecps, or as set by the sample wavetable generator when reading in a WAV file with looped playback information.

Given a base frequency value and the sampling rate of the table, the loscil opcode increments its internal pointer to achieve the signal pitch requested by freq. If freq is negative, the internal pointer is decremented.

The loscil opcode returns sample values indefinitely, by looping a portion of the wavetable. The optional parameters loopstart and loopend supply the loop points, expressed as fractional array indices into the wavetable.

The internal pointer for loscil starts at index 0, and advances to the loopend position. It then cycles from the loopstart position to the loopend position indefinitely. If the internal pointer is decremented in this regime, cycling in the reverse direction happens.

If the loopstart and loopend parameters are not provided, loscil uses the loop start and loop end values for the wavetable, as set by the core opcodes ftloop and ftloopend, or as set by the sample wavetable generator when reading in a WAV file with looped playback information.

If the wavetable loop values are not set, loscil loops through the entire wavetable indefinitely.

Loop Playback

aopcode loscil(table t, 
	      asig freq 
	      [,ivar basefreq,
	        ivar loopstart,
	        ivar loopend])

Low-Level Core Opcodes

Eleven core opcodes access wavetables at the lowest level of abstraction. These opcodes let programmers write replacements for the playback opcodes, and create other wavetable utility routines.

The k-rate core opcodes ftsetsr and ftsetbase change the sampling rate and base frequency of the wavetable t. The parameter value x holds the new value, which must be greater than zero. The opcode returns the value x.

The k-rate core opcodes ftsetloop and ftsetend change the loop start point and loop end point of the wavetable t. The parameter value x holds the new value, where x must round to a valid wavetable index. The opcode returns the value x.

The polymorphic opcodes ftlen, ftsr, ftbasecps, ftloop and ftloopend return the values of the length, sampling rate, base frequency, loop start point, and loop end point of the wavetable t, respectively.

Finally, the polymorphic opcodes tableread and tablewrite read and write values into wavetables. The index parameter for these opcodes specify the table position to read or write.

For tableread, if index is an integer value, the value of the wavetable at that index is returned. If index is not an integer, the return value is interpolated from nearby values, using the same interpolation methods as the playback opcodes.

For tablewrite, index is rounded to the nearest integer, and the val parameter value is inserted at that table position. The tablewrite opcode returns the value of val.

For both tableread and tablewrite, the index value must be in a valid range for the wavetable.

In many cases, SAOL programs use tableread to implement customized versions of table playback opcodes (such as doscil). Recall that to pitch-shift samples with doscil, the sample rate of the table is shifted up or down (using ftsetsr).

When using tableread in this way, sfront users should note that the band-limited interpolation algorithm expects tableread tables to have their sample rate set to the value doscil would require to create the desired pitch-shift. Sfront relies on this sample rate to ensure that band-limited interpolation is alias-free.

To Write Tables

kopcode ftsetsr(table t, ksig x)
kopcode ftsetbase(table t, ksig x)
kopcode ftsetloop(table t, ksig x)
kopcode ftsetend(table t, ksig x)

opcode  tablewrite(table t,
		   xsig index,
		   xsig val)

To Read Tables

opcode ftlen(table t)
opcode ftsr(table t)
opcode ftbasecps(table t)
opcode ftloop(table t)
opcode ftloopend(table t)

opcode  tableread(table t, xsig index)

Low-Level Generators

In the previous sections, we describe the SAOL generators that initialize wavetables for use in specific sound generation algorithms. In this section, we describe the remaining SAOL generators, that are oriented for use in more general contexts.

See the right panel for declaration syntax and algorithms for these generators.

Simple Generators

Several generators perform simple initialization, to prepare wavetables for access by the tableread and tablewrite opcodes.

The empty generator fills a wavetable with zero values. The size parameter must be greater than zero.

The data generator fills a wavetable with parameter values. If the size parameter has a value of -1, the wavetable size matches the number of parameters.

If the size parameter is set to a value greater than zero, the wavetable takes on that size, and the parameter list is truncated or zero-padded as needed.

empty

 
declaration:

table t(empty, size);

algorithm:

for x in [0, size-1]:

y(x) = 0

data

 
declaration:

table t(data, size, p0 [, p1 ...]);

algorithm:

for x with a px parameter:

y(x) = px


Continuous Curves

The polynomial generator fills a wavetable with the values of an Nth order polynomial, whose coefficients are supplied as generator parameters. The size parameter must have a value greater than zero. See the right panel for the exact polynomial description.

polynomial

 
declaration:

table t(polynomial, size, xmin, xmax,
                  a0 [,a1, a2, a3);

algorithm:

for x in [0, size-1]:

y(x) = a0 + a1*y + a2*y*y + ...

where

y = xmin + 
    (xmax - xmin)*((size - x)/size)

error if xmin = xmax

Table Concatenation

The concat generator takes a list of wavetables as parameters. The generator concatenates the data in these tables, and uses the result to initialize its own table.

If the size parameter has a value of -1, the wavetable size is the size of the concatenated data array.

If the size parameter is set to a value greater than zero, the wavetable takes on that size, and the concatenated data array is is truncated or zero-padded as needed.

concat

 
declaration:

table t(concat, size, t1 [, t2 ...])


algorithm:

t1, t2, ... are tables. all declared
tables must appear on lines that 
precede the concat generator line.

t1 is the concatenation of the
table arrays

Pseudorandom Tables

The random generator fills a wavetable with values generated by a pseudorandom number generator, whose distribution is specified by the parameters. The size parameter sets the size of table, and must have a value greater than zero.

The dist parameter specifies the distribution type, as an integer between 1 and 5. The parameters p1 and p2 have different meanings for each distribution type.

A dist value of 1 indicates a uniform distribution. All floating point values between p1 and p2 have an equal probability of being chosen for each table value.

A dist value of 2 indicates a linear ramp distribution. Table values may lie between p1 and p2, obeying the distribution shown on the right panel. Note that p1 may not equal p2.

A dist value of 3 indicates an exponential (Poisson) distribution. The p1 value parameterizes the distribution, as shown on the right panel.

A dist value of 4 indicates a Gaussian distribution. The p1 value is the mean of the distribution. The p2 value is the variance, and must be greater than zero.

A dist value of 5 indicates the table is filled with a random binary sequence, that obeys a Poisson distribution. The p1 value parameterizes the Poisson distribution.

To fill the table t of length len we use the following algorithm.

First, we initialize all table values to 0.

Then, we draw a number the Poisson distribution. We round the number to an integer value i, and set t(i) = 1.

Next, we draw another number from the Poisson distribution. We round this number to an integer value j, and set t(i + 1 + j) = 1.

We continue drawing numbers and setting t in this fashion, until the algorithm requires us to set a table index that is greater or equal to len.

Window Tables

The generator window initializes a table to one of a set of windowing functions useful for digital signal processing applications. We describe this generator along with its associated opcodes in Part IV of the book.

Next: Part II/5: SAOL Buses and Execution Order

random

 
declaration:

table t(random, size, dist, p1, p2)

algorithm:

dist takes on integer values that
indicate a probability distribution

for x in [0, size-1]:


dist = 1: uniform distribution

each table value filled with number
in the range p1 to p2, with uniform
probability.


dist = 2: linear distribution

each table value filled with number
p(y) from p1 to p2, with probability

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

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


dist = 3: exponential distribution

each table value filled with number
p(y), with probability

p(y) = (1/p1)*exp(-y/p1), if y > 0 
p(y) = 0,                 otherwise


dist = 4: Gaussian distribution

each table value filled with number
p(y), with probability

p(y) = (1/sqrt(2*pi*p2))*
       exp(-(p1-y)*(p1-y)/(2*p2))


dist = 5: Poisson Binary Sequence

see main text for algorithm, which
uses distribution:

p(y) = (1/p1)*exp(-y/p1), if y > 0 
p(y) = 0,                 otherwise

to fill table with a binary sequence.


Slib defines the constants
RANDOM_UNIFORM, RANDOM_LINEAR,
RANDOM_EXPON, RANDOM_GAUSSIAN,
and RANDOM_PROCESS to use
as the dist parameter in 
the random wavetable generator.

Copyright 1999 John Lazzaro and John Wawrzynek.