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).
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
.
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.
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.
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
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.
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
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.
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.
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
.
Operation | Description |
---|---|
^ |
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.
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.
Symbol | Description |
---|---|
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.
Several comment styles exist:
//
(C++-style comment) comments out the rest of a line./*
(C-style comment) comments out text until the next */
. Does not nest.#!
(Shebang) comments out the rest of a line.#Q
(Quit file) comments out the rest of the whole file.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 \1
s 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)
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:
v
(Target value.)t
(Time to reach target value. Defaults to time of oscillator.)c
(Curve type:
hold
, lin
, exp
, log
,
xpe
, lge
, or cos
.
Defaults to lin
.)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.
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.
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.