Skip to content

Latest commit

 

History

History
604 lines (476 loc) · 20.1 KB

manual.org

File metadata and controls

604 lines (476 loc) · 20.1 KB

aeon manual

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.

overview

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.

general concepts

units of time

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).

harmony and tuning

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:

CCsDDsEFFsGGsAAsB
606162636465666768697071

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:

NameAliasDescription
:root-A midi note representing the scale root.
:scale-A vector of semitones. Defaults to minor.
:tuning-[unimplemented]
:scale-degree:scdAn wrapping index into the current scale.
:chord-degree:chdAdded 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.

IIIIIIIVVVIVIIVIIIIXXXIXII
01234567891011

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.

patterns

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.

pattern definitions

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.

quantise changes

[*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))

play length

[*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))

pattern operators

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.

in family

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.

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]))
in:

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)

to family

to:
to math ops

subdividing sequences

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.

over

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]]

step

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]]

subdivision

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]]]

rests, ties and repeats

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]

continuous sequences

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)))

random

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!

sine

lerp

[unimplemented]

embedding sequences

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])])

drones and effects

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.

drones

effect groups

control patterns

instruments and sound design

sources

effects

send effects

projects and saves

creating a project

version control

recording loops

appendices

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!

note on square brackets []

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!’.