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

Part IV/1: Sound Synthesis Core Opcodes

Sections

Core Opcodes:

aexpon aline aphasor buzz grain kexpon kline kphasor pluck

Introduction

In this section, we describe core opcodes that generate signal waveforms. Each section describes a different type of signal generation algorithm.

The opcodes in this chapter share some general properties. When called, each opcode returns the next sample in the waveform. Each opcode assumes that it is being called once per a-pass (or for k-rate opcodes, once per k-pass).

All opcodes in this chapter have internal state variables, that keep track of the generative process that creates the waveform. Like all opcodes, this state is associated with a single syntactic opcode call.

 

Envelope Generators

Four core opcodes (kline, aline, kexpon and aexpon) generate piece-wise waveforms suitable for envelopes. See the right panel (below) for opcode headers.

These opcodes provide an alternative to using table playback opcodes to play envelope wavetables, as described in Part II/4.

The envelope opcodes return the current value of the envelope signal. When the envelope waveform is completed, the opcodes return zero for all subsequent calls.

Because these opcodes return zero after envelope completion, an audible "end-click" may result for envelope waveforms that do not end close to zero. To handle this issue, always specify envelopes that end close to zero, and shift and scale the opcode return value to add a DC bias if needed.

 

Linear Envelopes

The kline and aline opcodes generate piecewise linear envelopes at the k-rate and a-rate, respectively.

These opcodes specify the endpoint values for each linear segment (for the first segment, x1 and x2) and the amount of time (in seconds) it takes to traverse from one endpoint to the next (for the first segment, dur1).

The kline and aline opcodes must have at least one segment. The parameter list must end with an endpoint value, and all durations must be greater or equal to zero.

Linear Envelopes


aopcode aline(ivar x1, ivar dur1, 
              ivar x2 [,ivar dur2, 
              ivar x3 ...])

kopcode kline(ivar x1, ivar dur1, 
              ivar x2 [,ivar dur2, 
              ivar x3 ...])



Functional form for first segment:

 x1 + (x2 - x1)*(t/dur1)

where t is time, with value 0 at the
time of the first call. on subsequent
calls, t is incremented by 1/arate
(for aline) or 1/krate (for kline).


Exponential Envelopes

The kexpon and aexpon opcodes generate piecewise exponential envelopes at the k-rate and a-rate, respectively.

These opcodes specify the endpoint values for the each exponential segment (for the first segment, x1 and x2) and the amount of time (in seconds) it takes to traverse from one endpoint to the next (for the first segment, dur1).

See the right panel for the functional form of the exponential segments.

The kexpon and aexpon opcodes must have at least one segment. The parameter list must end with an endpoint value. All durations must be greater or equal to zero. All endpoint values must share the same sign, and no endpoint value may be zero.

Exponential Envelopes


aopcode aexpon(ivar x1, ivar dur1, 
              ivar x2 [,ivar dur2, 
              ivar x3 ...])

kopcode kexpon(ivar x1, ivar dur1, 
              ivar x2 [,ivar dur2, 
              ivar x3 ...])



Functional form for first segment:

 x1*((x2/x1)^(t/dur1))

where t is time, with value 0 at the
time of the first call. on subsequent
calls, t is incremented by 1/arate
(for aexpon) or 1/krate (for kexpon).

Subtractive Synthesis

In Part II/4, we explain the pitfalls of directly generating the square and pulse waveforms popular in subtractive synthesis, and describe the buzz core wavetable generator for creating alias-free pulse waveforms.

The right panel shows the buzz core opcode, that returns alias-free pulse waveforms using the same algorithm as the buzz core wavetable generator.

Note that the num, low, and r parameters are k-rate. By temporally modulating these parameters, the programmer can produce complex time-varying spectra. This capability differentiates the buzz core opcode from the buzz core wavetable generator.

The algorithm description of the right panel introduces the concept of a phase pointer, which we will use in several other core opcodes. A phase pointer cycles between zero and one, in increments sized by frequency parameter to the opcode (in this case cps). Negative values of cps act to decrement the phase pointer.

The buzz core opcode computes cosine partials up to the Nyquist limit, if the num parameter is specified as a negative number.

Band-Limited Pulse Generation


aopcode buzz(asig cps, ksig num,
             ksig low,  ksig r)


algorithm:

phase pointer p initialized to
zero during the first call to 
buzz, and incremented by cps/arate
on subsequent calls. if p is 
greater that 1 after an increment,
p is set to its fractional value.


given p, buzz returns:

cos(2*pi*(low+1)*p) +
r*cos(2*pi*(low+2)*p) +
r*r*cos(2*pi*(low+3)*p) +
... +
r^(num)*cos(2*pi*(low+num+1)*p)

scaled by the value

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

or if abs(r) is 1,

1/(num + 1)

---

Nyquist scaling:

if num <= 0, then:

   num = - low + arate/2/cps

Plucked String Synthesis

Several musical instruments use plucked strings to generate sound. The core opcode pluck models the sound of plucked strings, using a simple version of the Karplus-Strong algorithm. The right panel shows the header syntax for this opcode.

Buffer Playback

The pluck opcode shares several characteristics with the table playback opcode oscil described in Part II/4.

  • It uses a buffer of samples to represent a single cycle of a waveform.
  • It generates a signal by playing out the buffer repeatedly at a frequency set by the a-rate parameter cps.
  • It returns an interpolated sample value from the buffer with each opcode call, using the interpolation method set by the global parameter interp.

However, while the oscil opcode plays out values from a wavetable, the pluck opcode plays out values from an internal buffer, whose values are updated at regular intervals. The update algorithm creates the spectral characteristics of a plucked string decay.

Buffer Management

When pluck is called for the first time, it creates an internal buffer that holds the number of samples specified by the parameter buflen.

The internal buffer is initialized with the first buflen samples of the wavetable parameter init. If init has fewer samples than buflen, then the internal buffer is initialized with multiple cycles of init.

At regular intervals (set by the smoothperiod parameter) the internal buffer is replaced with a smoothed version of the buffer. The right panel shows the exact filter, which uses the parameter atten as a scaling term.

Qualitatively, each filter pass reduces the high-frequency content of the waveform, modeling the spectral decay characteristic of a plucked string.

Note that modulating the k-rate atten and smoothperiod parameter alters the spectral evolution of the sound.

pluck


aopcode pluck(asig cps, ivar buflen
              table init, ksig atten,
              ksig smoothperiod)

Algorithm:


Initialization:

Upon first call to pluck, an internal
buffer is created with buflen samples.

The wavetable init (with length tlen)
is used to initialize the buffer. 

If buflen is less than tlen, the first
buflen samples of init are used to
initialize the table.

If buflen is greater than tlen, copies
of init that start at position 0, tlen,
2*tlen ... of the internal buffer are
used to initialize the buffer.


Update:

On call number (smoothperiod, 
2*smoothperiod, 3*smoothperiod, 
ect.) to pluck, a new buffer
(also of length buflen) replaces
the old buffer.

The sample in position x of the
new buffer has the value

atten*0.2*(old[(x-2) % buflen] + 
           old[(x-1) % buflen] +
           old[(x) % buflen]   +
           old[(x+1) % buflen] +
           old[(x+2) % buflen])

where % is the integer modulo 
operator (circular wrap around),
and old[] is the old buffer.

Playback

Each call returns an interpolated 
sample value from the current 
buffer, under the control of 
cps, using the semantics of the
wavetable playback opcode oscil.

Granular Synthesis

Certain natural sounds are composed of many copies of a prototype sound. For example, the sound of a rainstorm is the sum of the sound of individual raindrops landing on a surface.

Granular synthesis methods model these sounds in a direct way, by generating waveforms for each individual event (or grain), and adding the waveforms together to produce an ensemble sound.

The core opcode grain implements granular synthesis, using wavetable playback as the synthesis technique for the grain. The right panel shows the header syntax for the opcode.

The opcode consists of two parts, a control engine for launching grain events, and a synthesis engine for generating the sound for a grain.

Control Engine

The job of the control engine is to launch new grains, under the influence of k-rate parameters density and time. Once launched, grains live for a certain period of time and then die, without intervention from the control engine.

The density parameter, with units of Hz, controls how many grains per second are launched. In the algorithm description, we refer to the density period, which is the reciprocal of the density parameter.

During the first call to grain, the internal density clock is set to zero, and on subsequent calls it is incremented by the sampling period. Whenever the density clock exceeds the density period, the density clock is reset to zero.

During an opcode call that results in a reset to zero, if the time parameter is less than the density period, then a new grain is launched, after waiting time seconds. If time is greater than or equal to the density period, no grain is launched.

When a launch occurs, the current values of k-rate parameters freq, amp, dur, and phase are used to initialize the new grain, as we describe in the next subsection.

Synthesis Engine

A grain lasts dur seconds, and is then destroyed. During this time period, the table parameter wave is played back to form the basic signal for the grain.

The properties of the wave parameter are used to determine the playback algorithm. If wave has:

  • both its sampling rate and base frequency parameters set (by using the table low-level core opcodes) the looping part of playback algorithm of the opcode loscil is used.
  • its sampling rate but not its base frequency parameters set, the playback algorithm of the opcode doscil is used.
  • neither its sampling rate or its base frequency parameters set, the playback algorithm of the opcode oscil is used.

If the doscil or oscil algorithm is used, the freq parameter sets the playback frequency of the table.

For all algorithms, the phase parameter sets the position in wave to start table playback, where phase is in the range [0.0:1.0]. The starting sample playback position is the value of phase scaled by the length of the table.

The basic signal of the grain is multiplied by the signal produced by playing back the env table with the algorithm of the doscil table. The sample rate for the env table is altered so that envelope playback takes dur seconds.

Finally, the grain signal is multiplied by amp to produce the final signal for the grain. The signals of all active grains are summed, and this sample value is returned by the grain core opcode.

grain


aopcode grain(table wave, table env,
              ksig density, 
              ksig freq, ksig amp,
              ksig dur, ksig time,
              ksig phase)


parameters:

wave: wavetable used to generate the
basic signal of grains.

env: wavetable used to generate the
envelope shape for grains.

density: frequency of grain generation,
in Hertz.

freq: playback frequency of a grain 
launched during the call to the
grain opcode. only used if the
wave table parameter indicates
loscil or oscil playback (see
left panel).

amp: amplitude scaling of a grain 
launched during the call to the
grain opcode.

dur: lifetime, in seconds, for a
grain launched during the call
to the grain opcode.

time: delay time, in seconds, for
launching a new grain.

phase: sets the starting index in
the wave table parameter, for a
grain during the call to the
grain opcode. see left panel
for details.

Low-Level Opcodes

Several opcodes in this section use the phase pointer concept to simplify their implementation.

The aphasor and kphasor opcodes generate a phase pointer waveform given a frequency value, at the a-rate and k-rate respectively. SAOL programmers may use these opcodes in their own sound generation algorithms.

The right panel shows the header syntax and exact algorithm for these opcodes.

Next section: Part IV/2: Filter Core Opcodes

Phase Loops


aopcode aphasor(asig cps)

kopcode kphasor(ksig cps)


algorithm:

phase pointer p initialized to
zero during the first call to 
buzz, and incremented by cps/arate
(for aphasor) or cps/krate (for
kphasor) on subsequent calls. if p is 
greater that 1 after an increment,
p is set to its fractional value.

negative cps values are permitted,
and act to decrement p.

opcode returns phase pointer p.

Copyright 1999 John Lazzaro and John Wawrzynek.