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

# Part IV/1: Sound Synthesis Core Opcodes

## Sections

- Introduction.
- Envelope Generators. Linear and exponential waveforms.
- Subtractive Synthesis. Alias-free pulse waveforms.
- Plucked String Synthesis. Karplus-Strong synthesis.
- Granular Synthesis. Temporal wavetable method.
- Low-Level Opcodes. Phase pointer opcodes.

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