SAU language overview

An overview of how to use the SAU language provided by the saugns program. For how to use saugns command-line options to do various things with scripts, instead see the usage page.

The below is meant to explain the big picture more than cover all details. A concise language reference listing details more completely can be found in the doc/README.SAU file (GitHub view).

Contents

Generating audio

To generate a single pure tone in the SAU language:

Osin f440 p0 a0.5 t1

In this case, frequency is set to 440 Hz, phase to 0% of the wave cycle, and amplitude to 0.5 (-6dB). The time duration will be 1 second.

The O adds an oscillator (also called an "operator" in FM synth terminology). It is followed by a wave type value, in this case sin (there's also tri, sqr, saw, and more). The other parameters are named, but not ordered, and any which are left out will be given default values. (All of them could be left out, for the shortest script which generates audio: Osin beeps for 1 second.)

A cosine can be generated using the sin wave type, by setting phase to 25% of the wave cycle, using p(1/4) or p0.25.

Using modulation

Oscillators can be used in a nested way, as in this example which uses PM (phase modulation):

// Generate 10 seconds of "engine rumble"
Osin f137 t10 p+[
	Osin f32 p+[
		Osin f42
	]
]

The oscillators with frequency 32 Hz and 42 Hz are modulators, linked in a chain which ends at the carrier (with frequency 137 Hz), and play for the same time (10 seconds). (The p+ means "add to the phase", and PM means adding modulator amplitudes to the phase.) Above, the amplitudes are all left at the default 1.0; those of modulators determine what is often called the modulation index or "depth" of modulation.

Modulators only ever run when their carriers do, and by default, if no time duration is set for a modulator, it will also be otherwise unlimited. But each modulator can also have a time duration in seconds of its own set in the same way as for a carrier, and then will run for the shortest of the times. (If the carrier time expires first, this will "pause" the modulator unless and until the time is extended, if that's done.) The special time which is default for modulators can also be set, as the non-number value i (implicit time), though only for modulators.

For modulators, frequency can be specified using the r (relative frequency) parameter instead of the usual f (frequency) parameter; whichever is most recently set will be used to get the frequency. A value assigned to r will be multiplied by the carrier frequency in order to give the frequency of the modulator. For example, a modulator with r(4/3) will maintain a frequency 4/3 times the frequency of the carrier; changing the f42 in the example above to that gives a somewhat different sound.

Frequency and amplitude can be given values which are computed using oscillator outputs shifted to a range. This includes "real FM", as well as the modulations of amplitude called amplitude and ring modulation. See the section Modulation with value range for more.

Frequency-linked PM

There's two different ways to connect a PM (phase modulation) input to a carrier, normal PM (p+[...]) as described above, and frequency-linked PM (p+f[...]) which multiplies modulator amplitudes by the carrier frequency (scaled down so that 632.45... Hz, the geometric mean of the 20–20000 Hz human hearing range, makes level "normal"). The best PM type to use depends on the intended sound as the carrier pitch varies. Both modulator types can be linked to the same carrier.

Changing the frequency of a carrier changes how fast its phase moves independently of any PM. When the PM signal also remains the same while frequency is changed, this changes the proportion of the two sources of phase movement for the carrier. For example, this affects how a vibrato effect from PM will sound as the carrier plays at different pitches; at higher pitch, e.g. twice the frequency, the PM has only half the impact relative to it. Frequency-linked PM makes a constant "impact" by multiplying the carrier frequency into the PM signal.

However, when modulator frequency is set relative (r) to a carrier as a multiplier for its frequency, it's normal PM that sounds more equal in intensity as the carrier pitch varies. With the combining of both types of linkage to the carrier frequency, higher-pitched sounds stand out more sharply, as the modulation input energy increases two ways instead of only one way.

Timing

A / (followed by a time in seconds) adds a delay to what follows. The time separator | adjusts delay for what follows so that it exactly matches the remaining duration (play time) for the things before. They can be used either alone or together, though when combined, the order of use will matter.

Those two things both apply for everything which follows in a script, though there's also timing modifiers which only apply more locally.

To generate two tones, separated in time, and also insert an extra 2.5 seconds of silence in-between them:

Osin f440 t2
| /2.5
Osin f220 t2

Subshifts & silent time padding

While a / can also be placed in the middle of a step for some object, splitting the time at which the values set for the object take effect, doing so also affects surrounding timing. This can be avoided by using a \ instead, to split the current step in time and delay the remainder of it by a number of seconds, but not affect independent steps placed after. This is called a subshift.

For example, two oscillators can be inserted at the same time, while still changing the frequency for the first oscillator after a 1 second delay. It will play for 3 seconds, as its time re-sets to the default value of the previous value, 2 seconds again. (For related syntax which more simply fits each wait time to the prior duration, see Compound steps below.)

Osin f440 t2 \1 f220
Osin f110

Using a \ typically moves or extends the time for the object concerned – automatically re-setting the time on each use if a new t value is not provided. Unlike on use of/, a long shift therefore doesn't make a short time run out (though there may be a gap between the two times, if the shift is long enough).

Note that the setting of a time value explicitly for the first oscillator above, before the \, is important; otherwise the peculiar behavior is to insert a pause or "rest" by making the default time 0 before the \ but not after it. The silence, or silent time padding, makes for moving, rather than extending, the non-silent time duration. As in the following example, where the tone plays after 1 second, for a default time:

Osin \1 f880

This is meant to create a sense of moving a whole step forward in time. Silent time padding may mainly be interesting to use for things placed in nested lists (to make another modulator start to play after a delay, say), when used by itself. But it may also be combined with the compound step syntax described next, for silent gaps in sequences of parameter updates for an object.

There's also another way to control the behavior of moving vs. extending, to disable or adjust the proportion of silent padding. When several \ are used in series only the first can zero the time before it. So \0 \1, for example, will never move more than 0 seconds, and then will extend by 1 second.

Compound steps for an object

The ; sub-step separator can also build a compound step for an object, with a total time duration which is simply the sum of those of all sub-steps, as they are laid out one directly after the next in time. The following example plays four tones in sequence, each for 1.5 seconds:

Osin t1.5 f100; f200; f300; f400

Here the time of the first sub-step becomes the default time of the next, which in turn becomes the default time of the next, etc. There is one exception to this for modulators, though, which is that the last sub-step is given the special time value i (implicit time) by default, just like modulators generally have such a default.

The \ subshift syntax can easily be used to insert silent gaps between the normal play times of sub-steps, padding them with silence. For example, to add a 0.5 second gap between each change of tone in the current example:

Osin t1.5 f100; \0.5 f200; \0.5 f300; \0.5 f400

Channel mixing and panning

SAU supports stereo audio, but oscillators pass mono signals between one another. Objects which are not used as modulators, whose output is mixed into the result, have an extra channel mixing parameter c which defaults to centered mixing, or 0.0. It can be changed in order to pan sounds. For convenience, the constants L for hard left, C for center, and R for hard right, can be used as values. This works the same as entering the corresponding numbers (-1.0), 0.0, and 1.0.

For example, to play a tone starting at the left and moving to the right over 3 seconds (using a value ramp):

Osin f440 cL c{vR t3} t3

It's possible to pan harder than hard left and hard right; going "too far" in either direction simply amplifies what's added to that channel while giving what's added to the other a negative amplitude.

Values and expressions

For most parameters, a single number is a value. The number may be written simply, or as an expression which is evaluated to yield the number. A simple number can be written with or without a decimal point. Negative numbers require the use of parentheses, so as to enclose the minus (-) sign, as in (-0.5).

Other types of values also exist. Parameters for amplitude, frequency, and channel mixing support using value ramp values for timed "ramping" or "sweeping" instead of the instantaneous setting of a value.

Frequency and amplitude parameters support yet another type of value, for FM or AM/RM with a value range, respectively.

All types of modulation use the list of oscillators as a type of parameter value, supported in a nested way.

Comments are text which is ignored; several comment styles are supported.

Numerical expressions

Each number can be written with or without a decimal point. If a decimal point is used, a leading 0 can be left out, as in .25.

OperationDescription
^ To the power of (right-associative)
* / % Multiplication, division, remainder
+ - Addition, subtraction

Within parentheses, arithmetic operations in ordinary infix notation can be used, giving a number as a result. For example, (1/2) is another way of writing 0.5. Plus or minus signs (+, -) can only be added to numbers within parentheses, so negative numbers requires parentheses to be used, e.g. (-1).

Conventional rules determine precedence when not made explicitly clear using parentheses. Nested parentheses can also be used for multiplication in the customary way, e.g. (2(3)) means 6.

Some parameters support named constants specific to that type of value. For such a value, a constant name can be written in place of a number.

Mathematical functions

A set of mathematical functions are supported in expressions for all parameters, whether or not surrounding parentheses are used. Writing name(value) gives the result of applying the function name to the value. A few functions give a value without being provided any, like rand(), which returns a new pseudo-random value from 0.0 to 1.0 each time it is called.

SymbolDescription
abs(x) Absolute value.
cos(x) Cosine of value.
exp(x) Base-e exponential value.
log(x) Natural logarithmic value.
met(x) Metallic value, e.g. met(1) gives the golden ratio. Positive integers give the series of metallic ratios. Other values are also allowed: fractional, 0 giving 1 and negative (gives how much the positive value would be increased, approaching zero further from zero). Note that met(-x) is also equal to (1/met(x)).
pi 3.1415...
rand() Pseudo-random number in range 0-1. The value sequence from a series of calls restarts each new script unit.
seed(x) Reset the rand() value sequence with a passed number. (Every bit counts; different expressions for the same number, with e.g. rounding may give different seeds.) Returns 0 so that e.g. /seed(100) will only reseed.
sin(x) Sine of value.
sqrt(x) Square root.
time() Get a system timestamp number changed each second. It can be used for seeding in a randomized script. (Note that the exact value is platform-dependent.)

A musically interesting function for frequency ratios and some other uses may be met(x), which produces the metallic means/ratios/constants (the Wikipedia article has the formula used); for example, the golden ratio value of 1.618... is the result of met(1), and a modulator oscillator can have its r parameter set to that with rmet(1). This function makes it easy to produce more complex frequency spectrums with FM or PM by providing more irrational numbers in response to simpler ones typed. (The function can take any value, also producing "metallic values" between the "proper" ones for the integers. Negative values also give how much the positive would be increased, which is less further from zero.)

Most functions return values meant to be assigned to something – like a parameter for an object, or a value for a delay time modifier – or to be passed on to other functions. Unlike other functions, the seed(x) function does not return a value meant to do anything, just 0. How to use it? Instead of making a frivolous parameter assignment simply in order to use it so that it produces its side effect (re-seeding random number generation), a tidy convention is to call it following a delay-slash, /seed(...). Given the number 0, the delay-slash will do nothing. This can be placed anywhere in the top scope of a script, including at the beginning of the script.

Comment syntax

Several comment styles exist:

Labels and referencing

The declaration of an object can be prefixed by 'name to label the object "name". Each name written is a case-sensitive string with alphanumeric characters and/or underscores. Once labeled, the object can be referred back to by writing @name at any later point in the script. Adding such a @name reference for an object does not automatically set a new time duration for that object. (A new time value is set if any changes made to parameters include explicitly setting t (time), or if a step-splitting timing modifier is used.)

Note that a @name reference placed in a nesting scope different from the original (i.e. outside a list, or in a new list, etc.) does not move the object into the new nesting scope. It will not be added to, nor removed from, any list by being referenced anywhere. The time scope is however new and of the reference.

For example, the modulator used in this PM example is labeled "name", and is then accessed using its label in order to change its frequency relative to the carrier at one-second intervals:

Osin f500 t5 p+[
	'name Osin r(1/1)
]

/1
@name r(1/2)
/1
@name r(1/3)
/1
@name r(1/4)
/1
@name r(1/5)

Here the timing would also change for anything written below (in a longer script) with every /1; their use for inserting time delays can be replaced with subshift \1s instead, which combine with the @name reference without inserting any leading silence:

/1
@name r(1/2) \1 r(1/3) \1 r(1/4) \1 r(1/5)

The timing section describes more means of placing changes in time. The ; separator is often a neater alternative to label referencing, but can also be combined with it:

/1
@name r(1/2) t1
; r(1/3) t1
; r(1/4) t1
; r(1/5)

Value ramps

To ramp, or "sweep", a parameter value towards a goal, a set of value ramp arguments can be given instead of the usual number. (This is currently supported for amplitude, frequency, and channel mixing parameters.) The usual number is used as the starting value for the trajectory, and the parameter can be assigned a value twice in order to provide both.

The ramp sub-parameters are as follows:

For example, the following tone begins at 20 Hz and rises exponentially to 20000 Hz, over 10 seconds:

Osin f20 f{v20000 cexp} t10 a0.25

The exponential and logarithmic curves are polynomial approximations with definite beginnings and ends, rather than real exponential and logarithmic curves. (The log curve also skips the below-zero part of a real logarithmic curve, i.e. it really approximates the mathematical function log(1 + x).) These approximations have been tuned by ear to sound "smooth" and natural.

The xpe curve exponentially saturates and decays, like a capacitor – this means an upside-down decay for the increase (it increases like the log curve and decreases like the exp curve). This is natural-sounding behavior for envelope-like use. The lge curve behaves the opposite, logarithmically saturating and decaying (increasing like exp and decreasing like log).

The cos curve rises or falls like a cosine wave, generally percevied to be fairly similar to a linear increase or decrease. The change has a smoothly curved start and stop, and a steeper middle.

Modulation with value range

Amplitude (a) and frequency (f) (and relative frequency r) parameters support modulation of the parameter values within a bounded value range. (For amplitude, whether this modulation is called amplitude modulation (AM) or ring modulation (RM) is a matter of the value range. Ring modulation has the same magnitude for the upper and lower bound, but with differing sign, while classic amplitude modulation has one of the bounds set to zero. The term AM has been used more generally for the full range of variation in the software, but for clarity it's best to note that classic AM and RM are both special cases.)

The two bounds of the value range come from the two (!) values of the parameter: the normal value generally used, and a second value which is only used for this type of modulation and which defaults to 0.0. (The second value can also be ramped or swept like the first, with the same syntax used in place of a number.)

A simple FM example:

// Vary frequency between 250 Hz and 500 Hz, using a 0.1 Hz sine wave
Osin f250,500~[Osin f0.1] t10

A simple AM example:

// Vary amplitude between 1/4 and full, using a half-wave with 1/5 frequency
Osin f200 a(1/4),(4/4)~[Ohrs r(1/5)] t5

The second value can also be set without changing the first value for the parameter, by writing nothing before the comma separating them.

After value(s) or by itself, ~[] (tilde and square brackets) can be used to set a list of modulator operators specified within the []; the list replaces any previous modulators set, and may be empty.

Each modulator in the list will produce a result in the range of 0.0 to 1.0, i.e. a positive signal, multiplied by its amplitude parameter (defaulting to 1.0), negative amplitude multipliers having the effect of switching the top and bottom of the 0.0 to 1.0 range.

The product of modulator amplitudes is mapped to the value range; 0.0 means the normal value and 1.0 means the second value. Setting (changing) the amplitude for modulators will thus change the range when the absolute value is not 1.0, but this is allowed for the sake of flexibility.

Compared to SuperCollider

SAU is simple and lacks many features, while SuperCollider is a main example of a language and system providing everything and the kitchen sink with a more conventional-looking language syntax.

In SAU, data cannot currently be held or combined without specifying carrier oscillators which play. While some simple things are simpler in SAU (including playing with modulation types), expressiveness drops beyond doing individual sounds.

In SuperCollider, a most basic thing, generating a sine wave, looks as follows. Note that it doesn't actually play it, which requires a few additional details.

SinOsc.ar(440, 0, 0.5);

In SAU, the closest-looking equivalent is:

Osin f440 p0 a0.5

By itself, this is a carrier oscillator which will play for the default time of 1 second. It could also be included in a list of modulators, and will then be linked to a carrier and play when the carrier does.