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. Syntax changes, feature additions, and other tweaks to the language between versions are listed here.
The below is meant to strike a balance between simple explanation, the big picture, and a reference for details of the language features. The examples page gathers examples with rendered audio files, which may be pointed to in the text. A concise language reference which mainly describes and lists details can be found in the README.SAU file.
To generate a single pure tone in the SAU language:
Wsin 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 Wsin
; the above values match those in a script consisting only of Wsin
, but more generally default values are context-sensitive (especially handling of time lengths).
The W
above is the name of the
wave oscillator type, and written to add an instance of it
– immediately followed by a wave type name, in this case sin
,
or none to use sin
as the default
(though there's more; see the wave type table). Such
an oscillator is also called an "operator" in FM synth terminology, especially
when it's a sine wave oscillator.
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 audio generators 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. Amplitude scaling for carriers doesn't affect nested or modulator generators, described below.
Audio generators (of which there's also more types) can be used in a nested way, to apply modulation of some type. The most central modulation type in SAU is arguably PM – commonly called "FM synthesis" in commercial synthesizers (though there's also "real" FM, more on that further below). As in this example.
// Generate 10 seconds of "engine rumble" Wsin f137 t10 p[ Wsin f32 p[ Wsin 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 default
time for a modulator is auto-fit, to
the carrier time used, but a shorter time can also be used.
(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.
The first line is just a comment.
For a modulator of any type, frequency can also be set relative to the carrier using the r
(relative frequency) parameter instead of the usual f
(frequency) parameter – and the default for a modulator is r1
, the multiplier 1.0, keeping in tune with the frequency setting of the carrier. Whichever option (f
or r
) was most recently used to set the frequency will be used to get it. More specifically, a value set to r
will be multiplied by the frequency of the closest carrier in the chain for the modulator. For example, a modulator with r4/3
will maintain a frequency 4/3 times the frequency of its carrier, i.e. a 4:3 modulator:carrier frequency ratio. Changing the f42
in the PM example above to that gives a somewhat different sound.
Frequency and amplitude can also be given dynamic values using modulator outputs. This includes "real FM", amplitude modulation (AM), and ring modulation (RM).
Sweeping parameters like amplitude or frequency is a little like using modulation features, except that the source of the change is not a generator object, but simply a one-off timed trajectory. Modulation can also become more interesting when that's done to parameters altering the result, changing what a modulator does over time.
Finally, the result of PM and of other types of modulation may differ
– sometimes not audibly, sometimes very audibly – on changing the
phase for a carrier or a modulator (with a number after a p
), as
it affects how the waveforms produced by the two line up and in turn interact
through the type of modulation.
Actual FM and PM are a little different, the first applied to the frequency parameter (f
or r
), the second applied to the phase parameter (p
). They produce strongly related but not identical results, and can both be useful. Here's some more on their nature and use in general.
When using sine wave oscillators (Wsin
), FM and PM behave similarly – until more levels of carrier-modulator linkages are used. FM and PM then produce different waveshapes unless the input is different to match the difference between FM and PM. If the modulator is different from a sine or cosine wave, then this is also the case with just a pair of oscillators. Using PM, a triangle wave modulator has the kind of effect which a square wave modulator has with FM. To understand this, it helps to think of how the phase and frequency are related...
If a square wave FM modulator is used, then when it is low, a lower frequency is used and the phase moves slowly; and when high, a higher requency is used and the phase moves quickly. With a triangle wave PM modulator, when it rises, the phase of the carrier keeps getting pushed ahead more at a steady pace until the peak of the triangle is reached, and then when it falls, the carrier phase is instead pulled back – the same result as jumping between two frequencies. (A square wave PM modulator, meanwhile, would cause the carrier waveshape to jump back and forth between different parts. And a triangle wave FM modulator sweeps the frequency linearly up and down, instead of making it jump up and down all at once.)
With FM, it becomes important whether or not a signal has any DC offset – because the average value of the modulator will change the average frequency of the carrier, up if positive, down if negative. And when chaining several levels of carrier-modulator oscillators, the result is almost always an assymetric waveshape which usually has DC offset. So using FM in a more elaborate way can make the general pitch of a sound drift around. This is not the case with PM – and that's why PM is so often favored – because DC in the modulator is then merely the same thing as adding some value to the phase, rather than to the frequency.
Mathematically, adding something to the frequency is basically like adding the integrated version of the signal to the phase. This matters when noise is injected through modulation – white noise turns into the more bassy red noise, aka brown noise, if added through FM, but remains white noise when added through PM. That's because integration tilts the frequency spectrum 6 dB per octave in the bassy direction, and phase is integrated in relation to the frequency.
Amplitude (a
) and frequency (f
) (and relative frequency r
) parameters all support modulation of the parameter values in the same ways, by adding modulator outputs to a value as described here, and/or by mapping modulator outputs to a range with two boundaries.
For amplitude, whether the result is called amplitude
modulation (AM), or ring modulation (RM), depends on how carrier
and modulator amplitude are set up relative to one another. For
frequency modulation, the result is however always the "real FM"
related to PM, yet distinct from it, whenever modulation happens.
The option for adding modulator outputs basically works the same as the option for PM described above. After a parameter name and optionally other value(s), within []
a list of modulators can be set; their output amplitudes are simply added to the parameter value. For example, here's a soft square-wave like sound made using ring modulation with two different wave types.
Whsi a0[Wsin r1/2]
With the carrier amplitude set to 0, and so amplitude coming only from the modulator output, the result is called ring modulation. With a non-zero amplitude value, the result of adding the modulator output to it would instead be amplitude modulation of some depth.
For FM, usually frequencies are set to something non-zero, but sometimes a frequency value of 0 can be interesting. Then the only thing "driving" the carrier, so that it produces a non-constant result, is the modulator(s) – forwards when frequency becomes positive, backwards when it becomes negative. With FM, unlike PM (which can be used to a similar effect), a much larger amplitude may however be needed to have enough impact on the carrier. Numerical expressions make it easy to e.g. try out small-ish powers of 10 as the amplitude of a modulator. Below a simplistic imitation of an electric guitar sound is made this way, with the help of some value sweeps.
Wsin f0[ Wsin p1/3 f44{g440*pi llog} a10^met(3) t2 ; f{g44} ]
When there's a list with several modulators in it, it may be convenient to set a multiplier for all of their amplitudes. This can be done using the S
option for amplitude, inside the []
list at its beginning; doing so only affects the current list level and scope, except when the above type of amplitude or ring modulation is used – otherwise further nested sublists are untouched by the amplitude setting, for the sake of e.g. preserving timbre when using it to adjust volume.
Note that modulator lists for the f
and r
options are shared and identical.
There's an alternative way to do amplitude/ring and frequency modulation – using a second value and a list of modulators assigned for the parameter under the subname .r
(as in a.r
, f.r
, and r.r
). The output of those modulators is then mapped to a range between the main and the second values for the parameter. This way to modulate can also be combined with the other which just adds the modulator outputs, described above.
For example, an expression like f250.r(250 * 2)[...]
sets up the use of some modulators (within the []
) to move frequency between 250 Hz and 500 Hz. The whole expression, name and subname and values, cannot contain any whitespace outside of parentheses and brackets. The second value defaults to 0.0 if left out, and is only used for this kind of modulation; it can also be swept like the main value, by using the same syntax in the place for the number.
If several modulators are used, the handling is a bit different from other ways to modulate, with the modulator outputs multiplied rather than added to one another, after being shifted to a positive value range. Each modulator first produces a result in the range of 0.0 to 1.0, which is then multiplied by its amplitude (defaulting to 1.0); however, if a negative amplitude multiplier is used, it will be used as a positive value after switching the top and bottom of the 0.0 to 1.0 range.
When several modulator outputs are multiplied together, this adds a bias towards the lower end of the range – the signal from the modulators ends up with broader valleys and narrower peaks. 0.0 will be mapped to the main parameter value, and 1.0 to the second (.r
) value. Of course, changing amplitude multipliers for the modulators can change the range, effectively moving the second value.
A simple FM example, where frequency is varied between 250 Hz and 500 Hz, using a 0.1 Hz sine wave.
Wsin f250.r500[Wsin 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.
Wsin f200 a1/4.r4/4[Whsi r1/5] t5
To use this for classic 100% modulation depth AM, one of the bounds should instead be 0.0 (like the default for the second value is); while for classic RM, the two bounds should instead have the same magnitude, but with the opposite sign.
If this type of modulation is used, it is done first; the other one from which the modulator outputs are simply added, if also used, will be applied afterwards.
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, not frequency-amplified. A
change in frequency, just like a change in amplitude, will change how much
energy (in terms of physics) there is to a signal; to change both the same
amount will change signal energy by the squared amount. With both combined
higher-pitched sounds become more intense along with the modulator signal.
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
. Such a setting can be used for all
generators in a modulator list at once, by using
the S
option at the beginning of it.
For generator types which use random number generation, like R
,
each new instance created in a script will be given a different starting point
or "seed" for producing random numbers. The seed sequence is however
deterministic unless changed to be based on system time, as can be done using
the mathematical functions time()
and
seed(x)
. When seed(x)
is used, it not only seeds
the rand()
function, but also new instances of the
randomness-using types. (Using rand()
, and adding e.g.
R
instances, however do not further affect the other.)
R
– Random segments generatorLine | Description |
---|---|
cos |
Half cosine (S-curve) trajectory over time. |
lin |
Linear trajectory over time. |
sah |
Sample and hold until time (then jump to goal). |
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). |
sqe |
Square polynomial envelope (saturate or decay). |
cub |
Cubic polynomial segment (-1 to +1) trajectory. |
ncl |
Noise camel line; softer, two noise bulges. |
nhl |
Noise hump line; harder, one broad noise bulge. |
uwh |
Uniform random white noise in start–goal range. |
The random segments generator R
can produce several kinds of audio value noise. While the parameters are mostly like those of a normal oscillator – e.g. almost everything written after Wsin
can also be written after Rcos
– the resulting frequency spectrums are very different, and various amplitude fluctuation patterns can be created. It can be used by itself or, typically, combined with wave oscillators in order to produce more complex sounds and soundscapes.
The underlying design of R
adds several independent ways to vary what's done and the result; it's a fairly flexible building block which can be made to do a lot. By default, however, it uses uniform white noise as an underlying function, sampled at a limited frequency (two values per "cycle" at 440 Hz), the pseudo-random values connected by the sinuous cos
S-curve line type.
cos
gives a cosine wave-like result, lin
a "cotriangle" wave-like result, and sah
a square wave-like result. It's also possible to noise up the result using uwh
and other noisy lines, producing a noise-on-noise signal.When white noise is used, then unless the frequency set is extremely high (e.g. the maximum, half the sample rate), the spectrum which results actually begins to roll off gently before the frequency used, reaching close to the peak level at half the base frequency, and before that extending backwards flatly across fractions of the frequency all the way down to 0 Hz. In other words, white noise modes make this a rumble generator – though the low-frequency content has its energy diluted more when higher frequencies are used, as the intensity is spread across a larger frequency range.
The violet noise modes allow producing less bassy signals, the frequency parameter then also controlling the point below which frequencies are reduced in the spectrum. The fixed cycle mode mf
additionally allows using an R
instance like a naive oscillator, where the line type selected determines a resulting wave type; a mix of that and white noise (or violet noise if mfv
is used) can also be set by tweaking the shaping level.
For an easy start experimenting with using R
in search of a sound, the line type, the choice of white vs. violet vs. fixed cycle modes, and the parameters in common with the W
wave oscillator, are usually the most significant in defining the sound. Other variations and tweaks of the mode are more significant when using R
as a modulator, including for LFO-like usage. There's also some example scripts available to look at and listen to.
R
parametersA line type optionally follows the R
, with cos
used as the default if none. A lowercase l
can be used to change the line type later, e.g. Rcos t1; llin
.
The nature of the randomness can be tweaked with the mode parameter m
, changing how the pair of values connected by lines each cycle are arrived at. A mode string for m
can have a letter (to select a randomness function), a digit (a 0-9 shaping level), and/or extra flag letters, in any order. The default level is 9
; roughly, each level above 0
halves what remains of the unshaped underlying randomness. The functions are...
r
g
b
t
f
In addition to the function and level, these flags can be set.
h
lin
for a decreasing
sawtooth instead of a triangle wave; similarly changes
the shape for all line types and randomness modes.s
b
, t
, nor f
with level 9
.
Distorts v
violet noise toward white, as if mixed.v
g
and t
. Like high-pass
filtering the lower end of the noise, 6 dB per octave.z
h
, or
f
level 9
; more difference from these adds larger
sharp steps. Always flips the waveform top and bottom.The unusual "ternary smooth random" mode (mt
) makes for a more harmonically pure and steady signal than other modes. This can make R
generators usable as more deeply nested modulators, for producing a sharp and crisp sound, when randomness in such a role otherwise tends to make for too much of a diffuse noisy mess. The following soundscape is a simple example; here, the Gaussian mode mg
is also used for the R
carrier instead of the uniform random default, as a simple way to get more loudness variation to the sound.
'f=10/3 Rcos mg f$f t60 p[ Wsin r10 a4 p[Wsin r-2.r+2[Rlin mt p0/4 f20*$f]] ]
More sawtooth-like waveforms (including a naive sawtooth from line lin
) are possible by adding the flag h
(half-shape) to the mode – it switches each wave cycle from using two lines (down or up) each the same horizontal length, to using just one (down or flat) followed by a vertical jump. While the waveform looks as expected, using this option can reduce the purity of randomness (and also makes violet random modes 6 dB bassier); this is mainly provided for LFO and for naive oscillator uses where that doesn't matter.
The zig-zag flip flag z
for the mode is another jagged waveshape distortion option, with a result depending on the other options – more randomness, more jagged waveshape, and with a thin and bright inharmonic result. For example, with line type lin
, it causes a chaotic mixture between triangle, randomly directed sawtooth, and square-like shapes, with the default randomness mode. It can also combine with mode mb
to produce a wave which randomly switches between segments of binary noise and the selected line oscillation after half-cycles, or mode mt
for random square steps and a constant line oscillation reaching zero in-between them.
W
– Wave oscillatorWave | Description | |
---|---|---|
sin |
![]() |
Sine. For cosine, set phase p to 1/4.
|
tri |
![]() |
Triangle.
Mellow odd-harmonics wave.
Opposite of ean relative to par .
|
srs |
![]() |
Square root of sine. (Mirrored for the negative half.)
Medium-bright odd-harmonics wave.
Opposite of cat relative to mto .
|
sqr |
![]() |
Square.
Bright odd-harmonics wave.
Opposite of eto relative to saw .
|
ean |
![]() |
Evenangle.
Mellow even-harmonics wave.
Opposite of tri relative to par .
To begin at 0.0 amplitude, set phase p to 6/93.
|
cat |
![]() |
Catear.
Medium-bright even-harmonics wave.
Opposite of srs relative to mto .
To begin at 0.0 amplitude, set phase p to 1/16.
|
eto |
![]() |
Eventooth.
Bright even-harmonics wave.
Opposite of sqr relative to saw .
|
par |
![]() |
Parabola. (x^2 , steep part up.)
Mellow all-harmonics wave.
Between tri and ean .
To begin at 0.0 amplitude, set phase p to 9/87.
|
mto |
![]() |
Mellowtooth. (Half-rectified srs , amplitude doubled.)
Medium-bright all-harmonics wave.
Between srs and cat .
To begin at 0.0 amplitude, set phase p to 1/25.
|
saw |
![]() |
Sawtooth.
Bright all-harmonics wave.
Decreasing slope; use negative amplitude
or frequency (but not both) for increasing slope.
Between sqr and eto .
|
hsi |
![]() |
Half-rectified sine. (Amplitude doubled.)
Like a somewhat louder ean , harmonics decreasing as fast.
To begin at 0.0 amplitude, set phase p to 1/12.
|
spa |
![]() |
Sine parabola. (First half, amplitude doubled.)
Slightly cleaner than par . Mainly useful for modulation.
To begin at 0.0 amplitude, set phase p to -1/12.
|
The wave oscillator W
is the oldest and most-used signal generator – a pretty plain and flexible oscillator of the kind often used for FM synthesis – especially with the sine wave type, typically inserted by writing Wsin
(or just W
) in a script.
Beyond sin
, 3 × 3 complementary wave types are provided, in terms of:
Additionally, there's 2 more wave types listed after these main 10; the ones at the end don't fit as neatly into the main groups, but have their uses as well. You can listen to all the wave types on the examples page.
Producing a (weakly) anti-aliased signal, including for FM and PM, amplitude can be a little lower for frequencies close to half the sample rate.
More interesting uses of W
usually involve using modulation. Usually, modulation either brings a slow, periodic variation to a sound – or when faster, creates "brighter" or more intense sounds from "mellower" or less intense ones; the waveforms available allow for a lot of simple combinations. Many complex and more dynamic sounds can also be created by combining more wave oscillators.
Wave oscillators can also be combined with other kinds of generators, in order to produce a larger variety of results. This can be done not only to produce a richer sound, but also sometimes to constrain it; ring modulation by a sine wave can for example be used to brighten the rumbly noise produced by the R
generator, and reduce the lowest-frequency content, in place of using a frequency filter (currently not a feature) to a somewhat similar end.
W
parametersA wave type optionally follows the W
, with sin
used as the default if none. A lowercase w
can be used to change the wave type later, e.g. Wsin t1; wtri
.
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:
Wsin f440 t2 | /2.5 Wsin 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.
Modulators are linked to carriers, and limited to only running when carriers do. By default, if no time duration is set for a modulator, it will not be further limited – it will run when a carrier using it runs. This removes the need to match time lengths set for carriers and modulators in scripts when the modulator should be used all the time.
Each modulator can also have its own time duration
in seconds, however, set the same way as for a carrier,
and then will run for the shortest of the time lengths.
(If the carrier time expires first, this will "suspend"
the modulator unless and until its time is set to a new
non-zero length.) The special "implicit" time, which is
the default for modulators, can also be set manually as
the non-number value i
(implicit time) for
modulators (but not for carriers).
The non-number value d
can also be used
to set the default time which would have been set for a
carrier.
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:
Wsin 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 an exception for modulators; modulators generally have an automatically fit time length by default (also possible to set with the special time value i
, implicit time), and this is by default the case for the last (but only the last) sub-step when ;
is used, so that using it does not unexpectedly shorten the total default time for the modulator.
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:
Wsin 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
the use of /number
, a long shift doesn't simply move
past a short time expired; if it moves past sound to silence, 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.
Wsin f440 t2 ;1 f220 Wsin 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:
Wsin ;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.
Wsin f400 t1; p[ Wsin ;0 r(3/4) ;1 p[Wsin f500 a1/5; a1/4; a1/3; a1/2; a1/1] Wsin ;2 r(3/5) Wsin ;4 r(3/6) ;1 p[Wsin f300 a1/5; a1/4; a1/3; a1/2; a1/1] Wsin ;6 r(3/7) Wsin ;8 r(3/8) ;1 p[Wsin 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 swept 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.
Wsin f137 t11 p[ Wsin f32 p[ Wsin 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 audio generators 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 sweep:
Wsin f440 cL{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.
If several sounds are to be panned in the same way, it's possible to set an initial c
value for all of them, by using the S
option for channel mixing prior to adding the sound generators. For example, writing S cL
before adding some top-level audio generators will make c
default to L for all of them.
For most parameters, a single number is a value; see numerical expressions below for how to write them. A few parameters expect a name for some mode to use as the value (e.g. a wave type for an oscillator, or a line shape). More complex types of values which include numbers and/or names also exist; for example, some parameters support timed sweeping. A whole object such as an oscillator can also be considered a value.
For all types of modulation, the list of audio generators 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
. After a decimal point, at least one digit must
always be written.
Symbols | Description | |
---|---|---|
1 | ^ |
To the power of (right-associative) |
2 | * / % |
Multiplication, division, remainder |
3 | + - |
Addition, subtraction (not as sign) |
Number signs and arithmetic operation symbols can be used in infix
expressions, together with numbers and named constants,
variables, 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.) If disabled (deterministic mode), instead gives 0. |
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.
Variables can be used to name values and refer back to them, both numerical values and objects added in scripts. Variables in SAU are dynamically typed and can further be assigned to several times. Their names are written differently from all other names.
Following 'name=
, an expression can be written which will
have its value stored in the variable named "name". Such a name must be a
case-sensitive string with alphanumeric characters and/or underscores.
To use the value later in any numerical expression, write $name
.
The leading $
sets variables apart from other numerical names.
After a variable has been made to hold a number,
it can be used in a new expression assigning it a new value based on the old,
for example 'name=$name*2
. The value from such a reference
on the right-hand side of the =
is always the previous value.
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, as with variables in general.
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:
Wsin f500 t5 p[ 'name Wsin 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
The capital letter S
can be used to access and change options for default values and more, which apply in part of the script. This can be handy as a way to change a series of new values for e.g. oscillators; the same lowercase letter is used after the S
to access the setting as would be used for the oscillator for its related setting, in most cases.
If you wanted a default f
frequency value of 100 Hz for oscillator-like sound generators, then S f100
would make that apply afterwards. Or to downscale the amplitudes of a set of audio generators which follow, S a1/16
could for example be used.
Changes made in this way apply in the current []
list scope, and also in lists nested more deeply – but not outside of the current list if it was done in a list. The amplitude setting is however special in that it has independent multipliers at each list level, so that volume control does not inadvertently affect timbre and more by changing modulator amplitudes (with the exception of AM/RM modulators, which are adjusted in order for them to have the expected impact on the carrier amplitude).
a
a
values, for the current
scope of []
nesting. (The multiplier also applies to
a.r
values, and to the multiplier in any deeper main
a[]
modulation list.) If used at the top level, this
disables automatic down-scaling of amplitude per voice
by the number of voices, for manual control instead.c
c
value. Starts at 0.0,
i.e. C (center). Useful as a main way of setting the parameter value.f
f
value, in Hz. Starts at 440..n
f
values using
note syntax. Starts at 440.
r
r
value,
a modulator:carrier ratio. Starts at 1 (1/1, a "1:1" ratio).t
t
value, in seconds.
Default times may be longer (and occasionally shorter)
depending on the context. Starts at 1.0.To sweep a parameter which supports value sweep subparameters towards a
goal value – the ordinary value being the start for a trajectory –
following the ordinary value or by itself, a set of value sweep
subparameters can be given values within {}
.
Currently this is supported for amplitude, frequency, and
channel mixing parameters.
A main parameter (like a
for amplitude) can be assigned to several
times in a row, so sweep subparameters can be set all at the same time or
separately; writing a0 a{g1} a{t1}
or a0{g1 t1}
gives
the same result. It's also possible to set the ordinary value inside the
{}
instead, by using the name v
for it to treat it as
another sweep subparameter.
For example, the following tone begins at 20 Hz and rises exponentially to 20000 Hz, over 10 seconds:
Wsin f{v20 g20000 lexp} t10 a0.25
Here, l
is a subparameter which changes the line shape (the default being lin
, the linear shape). Note that the line shapes are not the same as any well-known mathematical functions with the same names. Each fills in points between the beginning (ordinary number) and the end value (g
) in some way.
The sweep subparameters are as follows. The default values often allow two or three of them to be left out.
g
l
lin
, or the previous shape if any).
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
.
For a less-steep alternative to xpe
, sqe
can be used.
The cos
shape sounds similar to lin
, except it has a
smoothly curved start and stop, and a steeper middle.t
t
duration,
or the remaining previous time, if any, for this parameter).
If longer than the active time for the object which has the
swept parameter, the trajectory will be left unfinished.v
v
,
if not set before the enclosing {}
.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.