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). Syntax changes, feature additions, and other tweaks to the language are listed here.
To generate a single pure tone in the SAU language:
Osin f440 p0 a1.0 t1
In this case, frequency is set to 440 Hz, phase to 0% of the wave cycle,
and amplitude to 1.0 (0dB, no gain change). The time duration will be 1 second. Default values can also be used, by leaving out one or more of the parts after Osin
; the above values match those in a script consisting only of Osin
, but more generally default values are context-sensitive (especially handling of time lengths).
Wave | Description |
---|---|
sin |
Sine. For cosine, use (1/4) phase. |
sqr |
Square. |
tri |
Triangle. |
saw |
Saw. Increasing slope; use negative amplitude or frequency for decreasing slope. |
ahs |
Absolute half-frequency sine (adjusted).
Sine-based parabola-like wave.
Frequency halved to restore base frequency,
peak amplitude centered and doubled, phase
shifted to begin and end at zero amplitude.
A softer alternative to saw (saw wave). |
hrs |
Half-rectified sine (adjusted). Positive half kept, negative half zero'd. Peak amplitude centered and doubled, phase shifted to begin and end at zero amplitude. A warm and full sound. |
srs |
Square root of sine.
Mirrored for the negative half.
A softer alternative to sqr (square wave). |
ssr |
Squared & square root of sine.
A little more thin and sharp, it differs from
srs a little like srs differs from szh . |
The O
is the name of the oscillator type,
written to add an oscillator (also called an "operator" in FM synth
terminology) – immediately followed by a wave type name,
in this case sin
(there's more, listed in the table).
Numbers used above are simple examples of numerical expressions. Positive numbers are the simplest to write. Fancier things are also possible, including randomized values.
When top-level or carrier oscillators playing at the same time increase in number, the amplitude is scaled down for all of them in proportion (in addition to their individual amplitude settings), unless this is disabled. This doesn't affect nested or modulator oscillators, described below.
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
is for 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 r4/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.
Ramping or sweeping parameters like amplitude or frequency is a little like using modulation features, except that the source of the change is not an oscillator, but simply a one-off timed trajectory. Modulation can become more interesting when that's done to parameters altering the result.
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.
Finally, the result of both PM and other types of modulation can be altered
– sometimes not audibly, sometimes very audibly – by changing phase
for a carrier or a modulator (with a number after a p
), changing
how the waveforms produced by the two line up relative to one another, and so
how one of them ends up affecting the other.
There's two different ways to connect a PM (phase modulation) input
to a carrier, normal PM (p[...]
) as described above, and
the similarly-used frequency-amplified 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 a carrier, so the choice can be made per modulator.
Both modulator lists can be set at once (p[...],f[...]
)
or each can be set independently (p[...] p,f[...]
).
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-amplified 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.
When comparing normal and frequency-amplified PM, keep in mind that changes
in modulator amplitude can counter-intuitively affect the color of the sound,
just like changes in the phase offsets for carrier relative to modulator can.
To ease comparing and "tuning" sounds, the named constant mf
can
be used for the 632.45... Hz number when setting parameter values. For example,
if a carrier has a frequency of 100 Hz, then setting the f-PM modulator
amplitude with a(mf/100)
brings the same result as
for a normal PM modulator with a1.0
.
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 main 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.)
Separating the two values is ,w
(where the letter "w" is
short for "wave envelope"). The second value can be left blank to use
the default.
After ,w
and optionally a value, [...]
can be added to set a list of modulator operators, replacing any previous set.
A simple FM example (where frequency is varied between 250 Hz and 500 Hz, using a 0.1 Hz sine wave):
Osin f250,w500[Osin f0.1] t10
A simple AM example (where amplitude is varied between 1/4 and full, using a half-rectified sine wave with 1/5 of the carrier's frequency):
Osin f200 a1/4,w4/4[Ohrs r1/5] t5
The second value can also be set without changing the first value
for the parameter by writing nothing before the ,w
.
It's also possible to leave out the second value, or both.
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. 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.
The product of modulator amplitudes is mapped to the value range; 0.0 means the main value and 1.0 means the second value. This adds a bias towards the main value with more modulators used.
Things in a script all have the same time placement, i.e. they begin or take effect at the same time, unless the time position is changed. Playing several things at the same time is easy – just write one thing after the next – while there's several ways to arrange them differently than that.
A /number
, with "number" a time in seconds, adds
a delay to everything after; it's the global forward-shift option, added
between two parts of a script. The time separator |
is related and adjusts the delay added to everything after so it
exactly matches the duration, or play time, remaining for things
before it. (When both of these options are used together,
the order of use is important.)
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 those two timing modifiers apply to everything which follows after in a script, there's other options which only apply more locally, described below.
The numberless ;
sub-step separator splits and extends the duration of a step for some object into two parts, one placed directly after the other. Parameter changes written after it will take place just after the time duration of the preceding part, and the following part will in turn have a new time duration. It can be used any number of times in a row, timing only changing locally within the compound step built this way.
It's often the simplest way of arranging a series of timed changes for an object. The following example plays four tones in sequence, each for 1.5 seconds:
Osin t1.5 f100; f200; f300; f400
Here all four time durations are 1.5 seconds, for 6 seconds in total, because the default time for the 2nd part is copied from the 1st, and that of the 3rd is copied from the 2nd, etc. That's the rule, though there is one exception for it for modulators; just like modulators generally have the special time value i
(implicit time) by default, for modulators the last sub-step has that as the default time, so that using ;
does not unexpectedly shorten the time.
Silent gaps can also be inserted within a compound step, adding to the duration, using the ;number
gapshift syntax described more generally in the next section. A ;number
written just after a ;
adds a local time delay "number" of seconds long inside the compound step, analogous to using the more global |
and /number
-syntaxes together. For example, to add a 0.5 second silent gap between each change of tone in the current example:
Osin t1.5 f100;;0.5 f200;;0.5 f300;;0.5 f400
The gapshift ;number
-syntax looks somewhat like
the more global /number
-syntax, similarly allowing
time in seconds to be entered as a delay time – but only
for the current step for some object. It behaves much like the
compound step numberless ;
and is another way to split a step and forward-shift the later
part in time. The main use is to move a (sub-)step forward and
leave a silent gap at the old position, but it can also extend
the duration without leaving a silent gap.
Using ;number
always resets time for the new part
each use if a new t
value is not provided. Unlike on
use of/number
, a long shift doesn't simply move past
a short time expired; if it moves past sound to silence, then the
silence may also be followed by sound with a new play time added.
It's possible to use only ;number
in place of the
numberless ;
, but it may be more messy. For example,
with two oscillators inserted at the same time, for the first of
them. Time should be explicitly set before the ;1
is
used, thereafter play time for the first oscillator will extend,
rather than move; it will play for 3 seconds rather than 2, with
the time reset to the previous value, 2 seconds, after 1 second.
Osin f440 t2 ;1 f220 Osin f110
Note that the setting of a time value explicitly for the first
oscillator above, before the ;1
, is important;
otherwise the peculiar behavior is to insert a pause or "rest" by
making default time 0 before the ;number
while after
it, the old default time is copied if a new time isn't set there.
Changing the order to ;1 t2
, the first 1 second will
be blank for the first oscillator, and it will only play the last
2 seconds with 220 Hz (never with 440 Hz). Another example, where
a tone plays after 1 second (for the usual default time) follows:
Osin ;1 f880
Here nothing is missing and the delay is intended. Such use of silent time padding may mainly be interesting inside nested lists (to make another modulator start to play after a delay, say) when used by itself.
There's also another way to control the behavior of moving vs.
extending, to disable or adjust the proportion of silent padding.
When several ;number
are used in series – with
no numberless ;
or other timing modifier in-between
– then only the first can zero the time before it. So, for
example, ;0 ;1
will never move more than 0 seconds,
then will extend by 1 second.
The flow of time and the nesting of scopes are like two dimensions in which things are arranged – which corresponds to how the text in a script looks. When objects are placed inside of lists for nesting, as when adding modulators, the same time placement is used for the contents of the list as for where the list is assigned. In turn, inside of a list, a step written for an object can have sub-steps (using ;
and/or ;number
as described above), making for timing offsets which then apply for further lists assigned there in particular within the list.
Currently, the global timing /number
and |
syntax is not allowed inside of modulator lists, only at the top scope (where they are also the most useful). A way to use them at the level of a subscope block may be added in the future, as an alternative to using only the compound step syntax (which only applies to the current individual object and that which is nested below).
Here's an example of both timing and nesting which builds up a richer and richer noise, using PM, second by second.
Osin f400 t1; p[ Osin ;0 r(3/4) ;1 p[Osin f500 a1/5; a1/4; a1/3; a1/2; a1/1] Osin ;2 r(3/5) Osin ;4 r(3/6) ;1 p[Osin f300 a1/5; a1/4; a1/3; a1/2; a1/1] Osin ;6 r(3/7) Osin ;8 r(3/8) ;1 p[Osin f100 a1/5; a1/4; a1/3; a1/2; a1/1] ] t10; f800 t4
A timed series of changes for an object can, of course, also include smoothly ramped values. Here is a modification of the PM "engine rumble" example which produces something differently-sounding, morphing over time as the innermost oscillator has its frequency ratio swept towards a series of new values – along with a little silent gap in the middle.
Osin f137 t11 p[ Osin f32 p[ Osin r50*1.0 r{g50*0.1}; r{g50*0.2}; r{g50*0.25} t2.0 ; t1.5; t0.0; ;1.0 r{g50*0.75 t1} t2; r{g50*0.0} t0.5 ; r{g50*2.0} t2.0 ] ]
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 linearly to the right over 3 seconds using a value ramp:
Osin f440 cL c{gR t3} t3
The inner t3
above is actually optional, since the outer sets its default to that.
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; see numerical expressions below for how to write them. More complex types of values which include numbers also exist. Some parameters support timed value ramping or sweeping.
For all types of modulation, the list of oscillators is used as a type of parameter value, supported in a nested way. Frequency and amplitude parameters combine that with numbers, for FM or AM/RM with a value range, respectively.
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
.
Symbol | Description |
---|---|
^ |
To the power of (right-associative) |
* / % |
Multiplication, division, remainder |
+ - |
Addition, subtraction |
Number signs and arithmetic operation symbols can be used in infix
expressions, together with numbers and named constants and functions.
The rules are fairly simple and conventional, including precedence as in the
table shown. Nested parentheses can be used freely. Parentheses also allow
shorthand multiplication (leaving out a *
between two parts),
e.g. 2(3)
and (2)3
both give 6
.
Unless a numerical expression is written within parentheses,
it cannot contain any whitespace, as it ends the expression.
For example, -1
is fine, but - 1
is a dangling minus followed by a dangling number 1,
if not inside parentheses as (- 1)
.
The ability to write any expression, sans
whitespace, without any surrounding
parentheses is for convenience.
For example, writing rational
numbers with a division, e.g.
1/2
, is often useful
in scripts and this keeps it short.
Some parameters support named constants specific to that type of value; unlike other constant names, they are capital one-letter names.
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.
Name | 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)) . |
mf |
632.45... (Geometric mean of 20 and 20000.) |
pi |
3.1415... |
rand() |
Pseudo-random number in range 0-1. The value sequence from a series of calls restarts each new script unit. |
rint(x) |
Round value to the nearest integer. Halfway cases are rounded to the nearest even integer. |
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 r1/1 ] /1 @name r1/2 /1 @name r1/3 /1 @name r1/4 /1 @name r1/5
Here the timing would also change for anything written
afterwards (in a longer script) with every /1
.
The timing section describes more means of
placing changes in time. The numberless ;
-separator
is often a neater alternative to label referencing,
but can also be combined with it. (Here it uses the
t
value of each preceding part to only
locally delay the sub-step which follows it.)
/1 @name r1/2 t1 ; r1/3 t1 ; r1/4 t1 ; r1/5
In some cases, it's shorter and simpler to use the numbered form
of the ;
-separator, called a gapshift.
Like the first example, this skips the use of t
to set how long
to wait between parts. Each /1
is replaced by a ;1
for a continuing @name
reference. A gapshift combines with such
references without making the first part silent, because the initial use of
@name
never automatically sets a new time duration,
unlike later sub-steps, and unlike freshly added objects.
/1 @name r1/2 ;1 r1/3 ;1 r1/4 ;1 r1/5
To ramp, or sweep, a parameter towards a goal value,
a set of value ramp arguments can be given within {}
in place of the usual one numerical argument.
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.
For example, the following tone begins at 20 Hz and rises exponentially to 20000 Hz, over 10 seconds:
Osin f20 f{g20000 rexp} t10 a0.25
Here, r
is a subparameter which changes the ramp shape (the default being a linear ramp). Note that the ramp shapes are not the same as any mathematical functions with the same names. Each fills in points between the beginning (usual number) and the end value (g
) in some way.
Shape | Description |
---|---|
hold |
Hold old value until time. |
lin |
Linear trajectory over time. |
sin |
Sine-like smooth ends trajectory over time. |
exp |
Steep exp(x)-1 -like increase or decrease. |
log |
Steep log(x+1) -like increase or decrease. |
xpe |
Exponential envelope shape (saturate or decay). |
lge |
Logarithmic envelope shape (saturate or decay). |
The ramp sub-parameters are as follows. The default values often allow one or two of them to be left out.
g
r
lin
).
The exp
and log
shapes use ear-tuned polynomial
approximations with definite beginnings and ends,
designed to sound natural for frequency sweeping,
and symmetric one to the other. The xpe
shape increases
like log
and decreases like exp
, much like a capacitor
charges and discharges, natural-sounding for an envelope;
and lge
increases like exp
and decreases like log
.
The sin
shape sounds similar to lin
, except it has a
smoothly curved start and stop, and a steeper middle.t
t
duration).