Note: This is embryonic documentation for a system that’s under heavy development. Things may change!!
Note: If you’re viewing this on Github, the table of contents can be accessed using the button at the top left. It has a ‘bulleted list’ or ‘burger’ icon.
aeon
is a music live-coding and composition tool that runs on the
Scheme programming language (specifically Chez Scheme). Its focus is
on writing and transforming musical patterns in a way that’s
intuitive, discoverable and fast.
This document lists all of the built-in forms that aeon
provides. It
is structured as a general reference, not a step-by-step guide to
learning aeon
.
If you are just starting out, you may prefer the tutorial, or perhaps the recipes, which present a more hands-on and musically inspiring introduction.
Time values in aeon
are always relative to a ‘measure’. This is
basically a musical bar. We usually write time values as fractions, so
to express a sixteenth note you write 1/16
, or 1/3
for a
triplet. You can use any fraction, such as 5/7
, if you wish.
The relationship of measures to seconds is determined by the playhead’s
bpm (beats per minute), as this is familiar to most people. The bpm
determines how many 1/4
measures elapse per minute.
Changing the bpm is done like this: (set-bpm! 100)
.
Most synthesizer instruments in aeon
want a :freq
property to
determine the base frequency of their oscillators. You can supply
:freq
directly, but this makes for some rather tedious calculations.
A simple option is to supply :midinote
, which gets converted to
:freq
based on a twelve-note equal-tempered scale. You can use MIDI
note numbers or these capitalized identifiers:
C | Cs | D | Ds | E | F | Fs | G | Gs | A | As | B |
---|---|---|---|---|---|---|---|---|---|---|---|
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
If you supply an :octave
property, it will be used to offset the
note by 12 semitones for each octave. You can use negative values for
:octave
too.
A more complete choice is to use a family of properties which are interpreted together to generate a frequency. The properties are as follows:
Name | Alias | Description |
---|---|---|
:root | - | A midi note representing the scale root. |
:scale | - | A vector of semitones. Defaults to minor . |
:tuning | - | [unimplemented] |
:scale-degree | :scd | An wrapping index into the current scale. |
:chord-degree | :chd | Added to :scd to produce a final scale degree. |
If we’re thinking in terms of chords, the :scale-degree
could be
considered the chord’s root, while the :chord-degree
can be used to
select a note from the scale relative to that root.
For :scd
we often use Roman numerals since these are commonly used in
traditional music theory, but there’s no obligation to use them. They
simply evaluate to a regular number.
I | II | III | IV | V | VI | VII | VIII | IX | X | XI | XII |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
By combining a fast moving :chd
with a slow moving :scd
, you can do
custom arpeggiations:
(pattern x
(in! 16
(to: :scd (over 4 [I IV VII III])))
:chd (over [0 2 4 5]))
You can see the list of available scales and tunings, as well as the maths behind this in the harmony implementation file.
At the heart of aeon
is a collection of tools for generating events in
time. Often events represent notes to be played on a synthesizer or
sampler instrument (either one you built yourself or a built-in
template). But events can also represent control data for changing
existing synth voices or effects.
Events have ‘properties’, which are interpreted by the playback system when it generates sound. Each note destined for a synth or sampler, for example, may have any properties supported by that particular instrument. These could include frequency, envelope information, oscillator shape, effects or output sends, etc. The sky is the limit, and is customised per instrument (though many properties are shared between instruments). Thus you can sequence many more properties than are available in a traditional MIDI-based sequencer.
The techniques for specifying when events happen and what properties they have are unified for all types of events.
A pattern definition tells the aeon
system about a pattern you want to
play. It has a name, so that it can be updated in the future, and a
series of operator expressions that describe the pattern:
(pattern name
(operator) ...)
Evaluating this form will begin playing the pattern instantly. Re-evaluating it will apply any changes to the pattern instantly. The pattern will continue playing until you stop it.
;; stop one or more patterns by name
(stop pattern-names ...)
;; prepend 'stop' to an existing pattern definition
(stop pattern my-pattern-name
(in! 8))
;; stop all patterns
(stop)
The second form is useful because it switches the pattern off in the
same place that it’s defined. That makes it less likely you’ll leave a
separate stop
expression in a file and get confused later when the
pattern doesn’t play.
[*note*: unimplemented]
If you don’t want changes to happen instantly, you can quantise changes to a pattern to a certain number of measures like this:
(pattern (name quantised-measures)
(operator) ...)
;; play quarter notes
;; start/change on a 2 measure boundary
(pattern (x 2) (in! 4))
[*note*: unimplemented]
Adding a third value into the first form of a pattern directs the system to play it for only a limited number of measures. This can be useful for ‘one-shot’ effects or transitional sounds during a performance.
(pattern (name quantised-measures length-limit-measures)
(operator) ...)
;; play sixteenth notes
;; start/change on a 2 measure boundary
;; only play for 1 measure
(pattern (x 2 1) (in! 16))
Operators are constructs that dictate how a sequence (or set of sequences) will be used to generate or modify events. There are different families of operators for generating and modifying events, but all of these can use the same pattern sequences to do their work.
This family of operators is used for adding new events to a stream. The events may be created ‘blank’ to add properties later, or with some properties already baked-in.
Creates blank events that will be given the instrument "sine-grain"
and a :freq
of 440 by default.
Here is the general form of in!
. It takes a pattern sequence, followed
by zero or more further operations:
(in! sequence ops ...)
If further operations are supplied, it’s as if the in!
expression
were wrapped in a part, so that those operations only get applied to
the new events generated by in!
, and not to other events in the outer
pattern that the in!
is inside.
;; so the above is equivalent to:
(part (in! sequence) ops ...)
The further operations can be a flat mixed list of :key seq
and
individual pattern transforming functions. The :key seq
‘pairs’ are
treated as if they were wrapped into a (to: :key seq)
expression.
Some illustrative examples:
;; four blank events per measure:
(in! 4)
;; equivalent:
(in! (over 1 [4]))
;; a more complex subdivided rhythm:
(in! (over 1 [2 [~ 1] 1 $]))
;; two event streams, each with additional operations:
(part
(in! 2 (to: :scd I) (tt* 1/3) (to: :cutoff 0.5))
(in! 8 (to: :scd V)))
;; equivalent - :key values are automatically wrapped in (to:)
(part
(in! 2 :scd I (tt* 1/3) :cutoff 0.5)
(in! 8 :scd V))
If the sequence provided to in!
is not a subdivider, it is wrapped
in the over subdivider, with a length of 1 measure. Thus 8
is
equivalent to (over 1 [8])
. And (? 1 10)
is equivalent to (over 1
(? 1 10))
.
Values other than 1
in a subdivider further subdivide the step that
they reside in. Examples:
;; These are equivalent:
(in! 4)
(in! (over [4]))
(in! (over [1 1 1 1]))
;; As are these:
(in! (over [2 1]))
(in! (over [[1 1] 1]))
Behaves very much like in!
except the values returned from its pattern
sequence are used to set an initial property. Here is its general form:
(in: :property sequence
ops ...)
;; equivalent to:
(part (in: :property sequence)
ops ...)
Some examples. These must be wrapped in (pattern name ...)
if you want
to hear them.
;; 4 events per measure, with different scale degrees
(in: :scd (over [I IV VI V]))
;; 2 events per half measure, on different instruments
(in: :inst (over 1/2 ["pulse-pluck" "fm-grain"]))
;; setting another property with a 'further operation'
(in: :inst (over 1/2 ["pulse-pluck" "fm-grain"])
(to: :scd (over 4 [I VI VIII IV])))
;; equivalent - :key values are automatically wrapped in (to:)
(in: :inst (over 1/2 ["pulse-pluck" "fm-grain"])
:scd (over 4 [I VI VIII IV]))
;; :key values and pattern transformers can be intermixed
(in: :inst (over 1/2 ["pulse-pluck" "fm-grain"])
:scd (over 4 [I VI VIII IV])
(taps 1/16 2)
:cutoff 0.5)
Sequences that produce values at different time steps, in a looping fashion. Each of the time steps can be further subdivided into steps that produce values, recursively.
Produces a number of values over a given length of time. Each value
occupies a time length of total-time / number-of-values
. The sequence
loops forever.
;; general form:
(over total-time [values-list ...])
; values endure for (measures):
(over 1 [1]) ;=> [1]
(over 1 [2 3]) ;=> [1/2 1/2]
(over 1/2 [4 5]) ;=> [1/4 1/4]
(over 1/2 [1 2 3]) ;=> [1/6 1/6 1/6]
(over 1/2 [1 [2 3]]) ;=> [1/4 [1/8 1/8]]
Produces a number of values over time. Each value occupies the same
length of time. The total length of the sequence is therefore
step-time * number-of-steps
. The sequence loops forever.
;; general form:
(step step-time [values-list ...])
; values endure for (measures):
(step 1 [1]) ;=> [1]
(step 1 [2 3]) ;=> [1 1]
(step 1/2 [4 5]) ;=> [1/2 1/2]
(step 1/4 [1 2 3]) ;=> [1/4 1/4 1/4]
(step 1/2 [1 [2 3]]) ;=> [1/2 [1/4 1/4]]
Within the subdividing sequences mentioned above (i.e. =over= and
step
) you can create more intricate patterns by subdividing steps into
smaller pieces. This is done by nesting the values list like this:
[1 2 [3 4]]
In this sequence we have three equally sized steps, the last of which is subdivided into two equally sized steps. How this translates into values spread across time depends on the context this values list is found in. For example:
(over 1 [1 2 [3 4]]) ; => [1/3 1/3 [1/6 1/6]]
(step 1/4 [1 2 [3 4]]) ; => [1/4 1/4 [1/8 1/8]]
Of course, we can further sibdivide steps by nesting further:
(over 1 [1 2 [3 [4 5]]]) ; => [1/3 1/3 [1/6 [1/12 1/12]]]
(step 1/4 [1 2 [3 [4 5]]]) ; => [1/4 1/4 [1/8 [1/16 1/16]]]
Within a subdividing sequence, we sometimes want to skip, repeat or
merge a step. These are accomplished with the ~
, !
and $
symbols
repectively.
When a ~
is encountered in an in
operator, no event is produced at
that time. If it’s encountered in a to
operator, the property is not
set to anything.
When a !
is encountered in an in
operator, an identical event to the
previous event is produced. When it’s encountered in a to
operator,
the property is set with the same value as the previous step. You can
also use e.g. =!4= to make four identical steps. !
is actually
identical to !2
: they both result in 2 identical steps.
When a $
is encountered in an in
operator, the previous event’s
length is extended to cover that step. When it’s encountered in a to
operator, the previous value continues to be set on the property for
another step.
;; a rest
(over 1 [1 ~ 1 1]) ; => [1/4 - 1/4 1/4]
;; a tie
(over 1 [1 $ 1 1]) ; => [1/2 1/4 1/4]
;; a repeat
(over 1 [1 ! 1 1]) ; => [1/4 1/4 1/4 1/4]
Sequences that can provide continuously varying values for any point in time, as opposed to the ‘stepped’ sequences of a subdivider.
A note: continuous sequences can be used as the sequence for an in:
expression, but without further specification, the system won’t know how
often to generate events. By default it will generate them once per
measure. To choose other frequencies, wrap it in an over
expression:
;; 1 event per measure, frequency chosen by sine:
(in: :freq (sine 4 100 1000))
;; 8 events per measure:
(in: :freq (over 1/8 (sine 4 100 1000)))
Choosing random values can be done using the ?
operator. It has 3
different forms:
(? 0.5 2.5) ;=> choose a random value between 0.5 and 2.5
(? [3 4 5]) ;=> choose between 3, 4 and 5 randomly
(? [3 4 5] [1 2 7]) ;=> choose 3, 4 or 5 weighted
The weighted choice means that the value 4
is twice as likely to be
chosen as the value 3
. The value 5
is seven times more likely to be
chosen than 3
.
The construction of aeon
’s internals means that the chosen random
values are always the same for a given time and context. Normally this
is not noticable because the playhead keeps running forever. However,
this reveals what’s going on:
(pattern x (in: :freq (over 1/8 (? 200 600))))
;; re-evaluate this a few times...
(rewind)
You will notice that the initial sequence of random values is the same
each time you rewind the playhead. Sometimes if you’re using ?
several
times in a to:
expression, this can be a problem. This is because each
of the random choices will correlate to each other, and it will sound
less ‘random’. You can solve this by supplying a different seed to base
random numers off. The seed should be an integer, and should be the last
argument of the ?
expression:
(in! 16
(to: :scd (over 1/4 (? [0 3 4] 4)) ; seed is 4
:chd (over 1/4 (? [0 3 4] 5))) ; seed is 5
This will mean that the random sequences are different. On the other hand, in some sitations you might enjoy the fact that the random values are related!
[unimplemented]
In many cases, sequences can be embedded inside other sequences. For example, in this sequence the first three notes are the same each measure, but the last note rotates between four different notes over four measures.
(in: :scd (over [I IV III (over 4 [V VI VII IX])]))
Notice that when a sequence is embedded inside a subdividing sequence, the inner sequence is not ‘squeezed’. Its length is the same, but it’s as if it’s “gated” to the step that it occupies in the outer sequence.
Another example:
(in! 16 (to: :scd (over 2 [I (sine 8 0 12)])))
For example, this sequence produces 1
for half a measure, then chooses
randomly between [2 3 4]
for the next half measure.
(over [1 (? [2 3 4])])
There are two main ways of applying effects to your sounds. The first is
to build the effect into your synth definition. This means that each
voice will have its own copy of the effect, which can be sequenced like
any other synth property. For example, the built-in "sample"
and
"saw-grain"
synths both have a low-pass filter, and a :cutoff
property. You can therefore give each voice a different :cutoff
:
(in! 8 (to: :inst "saw-grain" :cutoff (? 0.05 0.6)))
This can be used to create very interesting patterns. However, there are two drawbacks. First, you must create a new synth definition if you want to add an effect to a sound - and creating synth definitions is a skill-set of considerable difficulty that you might not want to learn just yet. Second, it consumes extra processing power because each synth must do its own calculations for the effect.
The alternative way of applying effects fixes these problems, but comes
at a cost of sequencing power. In this approach, you create a single
effect processor and pipe multiple synth voices through it. You lose the
ability to give each voice its own effect parameters, but you save
processing power, and can use generic built-in effects that aeon
provides.
Since effects in SuperCollider are just the same as synth/sampler voices, this technique can also be used to create long-running monophonic synth sounds that don’t get replaced by a pattern. This is why this section also explains ‘drones’.
You can sequence the parameters of a long-running effect/synth, but remember that for effects this will apply to all voices being processed by the effect.
The following notes relate mostly to the underlying syntax of Scheme, or other deeper topics of implementation. If you’re only interested in making music, feel free to skip them!
In aeon
we traditionally put the ‘values list’ of sequences inside
square brackets. Examples:
(over [2 4 6])
(? [2 4 6])
But what do these square brackets really mean?
In Chez Scheme, aeon
’s host language, square brackets are exactly the
same as round brackets ()
. They are interchangable. However, we use
them to indicate that ‘something special is going on here’. Let’s try
typing into a Chez Scheme REPL:
(2 4 6) ;=> Exception: attempt to apply non-procedure 2
[2 4 6] ;=> Exception: attempt to apply non-procedure 2
This happens because these are both lists, and when you input a list into Scheme it is treated as code by default. Scheme expects the first element to be a function name, and further elements to be the function’s arguments. If you prepend a quote character, Scheme will stop trying to interpret the list as code, and will happily return a list:
'(2 4 6) ;=> (2 4 6)
'[2 4 6] ;=> (2 4 6)
Or, you could use the list
function, which constructs a list from its
arguments:
(list 2 4 (list 6 8)) ;=> (2 4 (6 8))
[list 2 4 [list 6 8]] ;=> (2 4 (6 8))
Note that Scheme can output a raw list like (1 2 3)
- it’s only on the
input where this is disallowed, because that’s where the evaluation of
code happens. Note also that the square brackets, once quoted, produce
exactly the same result as the round.
So if this is the case, how come we’re allowed to write things like this
in aeon
without quoting the inner lists?
(over [2 4 [6 8]])
(? [2 4 [6 8]])
The reason is that over
and ?
are macros. These are constructs that
let us temporarily bend or break the rules of Scheme within the scope of
their parentheses. To make writing aeon
patterns easier we use the
macro sdef to acheive a similar effect to quoting
(or quasiquoting) the list.
sdef
contains some special magic to discern function calls withing
lists so that the following works, even though to Scheme the [4 6]
form isn’t much different to the (+ 2 2)
.
(over [2 [4 6] (+ 2 2)])
So in short, we use square brackets to say: ‘normal rules of evaluation don’t apply here!’.