Learning Extempore while following LightNote music theory course.
To run examples you need Extempore’s master branch HEAD compiled. Version 0.7 doesn’t fit, because Extempore API is undergoing substantial change. Some of code suppose knowledge obtained from official documentation, especially about setup and language basics.
To follow the course you need access to app, could be purchased here. But following course is not required for reading this document, especially if you are already familiar with basic music theory and came here for Extempore examples.
If you are proficient with org-mode, you already know how it would best for you to run examples. Otherwise you have two basic options:
- Copy and paste code to buffer/editor from which you know how to send it to Extempore compiler (see documentation). Blocks are enclosed with xml-like comments to help you because GitHub org renderer doesn’t do tangling. HTML exported version is included in repo (read it here) for easier following, but it’s not guaranteed to be up-to-date.
- If you have Emacs installed then run
tangle.sh
to produce xtm files and run code from them. Xml-like comments with block names helps here with following too. Generated files are included in this repo either, but they are not guaranteed to be up-to-date.
To produce sound in Extempore we need to setup xtlang callback:
;; <set-dsp>
(dsp:set! dsp)
;; </set-dsp>
;; <xtm/00-sound-silence.xtm>
(bind-func dsp:DSP
(lambda (in time chan dat)
0.0))
<<set-dsp>>
;; </xtm/00-sound-silence.xtm>
Note callback signature:
- in:SAMPLE
- sample from input device
- time:i64
- sample number
- chan:i64
- audio channel
- dat:SAMPLE*
- user data
- <return>:SAMPLE
- sample at given channel and time
sample value varies from -1.0 to 1.0
You can set dsp function once, but then redefine it as many times as your want. Our first attempt produces silence, let’s make it more audible:
;; <xtm/01-sound-sine.xtm>
(bind-func dsp:DSP
(lambda (in time chan dat)
(let ((amplitude 0.5)
(frequency 440.0))
(* amplitude
(sin (* frequency
(/ STWOPI SRf)
(convert time)))))))
<<set-dsp>>
;; </xtm/01-sound-sine.xtm>
STWOPI
is 2pi of type SAMPLE constant, and convert
allows us to make a
SAMPLE
typed value from time
. SRf
refers to current sampling frequency.
Extempore uses symbiosis of two different languages with similar, LISPy,
syntax: Scheme and xtlang. Performance-sensitive parts (usually dsp) are
implemented in xtlang, and other stuff (usually control) is done in Scheme.
xtlang is very much like C but with LISP syntax and proper closures.
So far so good, we’ve obtained a basic form of sound — a sine wave.
Amplitude, or height of the wave (in case you are following graphics in
course), in our example is half of maximum available. sin
ranges from -1.0
to 1.0 and we multiply it by 0.5. It affects sound loudness. Try to play with
it.
Frequency, or density of the wave, is perceived as a pitch. Play with it.
While the essence of live coding is performance created with code,
cyber-physical environment incorporates various media. Let’s plug MIDI
controller and play with amplitude and frequency using it. For that purpose
we are going to load midi_input
library:
;; <load-midi-input>
(sys:load "libs/external/midi_input.xtm")
;; </load-midi-input>
It load a portmidi
wrapper and tries to connect to the first midi device.
The latter fact is important because if you will try to connect to this
device again by (set_midi_in 0)
you will get unhelpful error message
Invalid device ID.
Look into console where you are running Extempore. midi_input
calls
(pm_print_devices)
on startup. If you MIDI controller is listed under the
index 0 then nothing to do. Otherwise execute (replace 3 with required index):
;; <set-midi-in>
(set_midi_in 3)
;; </set-midi-in>
To make our dsp
function controllable outside let’s move amplitude
and
controller
outside of lambda:
;; <sine-closure-dsp>
(bind-func dsp:DSP
(let ((amplitude 0.5)
(frequency 440.0))
(lambda (in time chan dat)
(* amplitude
(sin (* frequency
(/ STWOPI SRf)
(convert time)))))))
;; </sine-closure-dsp>
xtlang has a nice feature: closure environment is accessible outside using
dot-syntax, (closure.variable:type)
as getter and (closure.variable:type
value)
as setter. This feature is arguable from the point of view of
functional style leaning towards purity and referential transparency, but I
guess it provides good trade for performance.
To read values from controller we would override midi_cc
function callback
provided by midi_input
(replace 19 and 23 with your knobs CCs):
;; <sine-midi-cc>
(bind-func midi_cc
(lambda (timestamp:i32 controller:i32 value:i32 chan:i32)
(println "MIDI CC" controller value)
(cond ((= controller 19) (dsp.amplitude:SAMPLE (/ (convert value) 127.)))
((= controller 23) (dsp.frequency:SAMPLE (* (convert value) 10.)))
(else 0.0:f))
void))
;; </sine-midi-cc>
If you execute snippets one-by-one then you should have response already. Otherwise here is entire file:
;; <xtm/02-sound-sine-midi.xtm>
<<load-midi-input>>
<<sine-closure-dsp>>
<<set-dsp>>
;; <<set-midi-in>>
<<sine-midi-cc>>
;; </xtm/02-sound-sine-midi.xtm>
This section involves playing notes, to ease tinkering with them let’s
introduce instruments. Extempore instrument is essentially a pair of
functions which knows how to render note of the given frequency and
amplitude. Let’s call our first intrument just a tuner
, because it doesn’t
care about shape of the note of any sound effects, it just tries to play a
plain sine wave for us. First function is tuner_note
and
convert note data to sample. Second function is tuner_fx
which adds
additional processing to the sound (none in our case).
Let’s load instrument library:
;; <load-instruments>
(sys:load "libs/core/instruments.xtm")
;; </load-instruments>
And define helpers for generating sine wave:
;; <define-sine>
(bind-val omega SAMPLE (/ STWOPI SRf))
(bind-func sine
(lambda (time:i64 freq:SAMPLE)
(sin (* omega freq (convert time)))))
;; </define-sine>
Alternatively, you can use Extempore’s built-in osc_c
generator which
closes over phase by itself and don’t require passing down the time.
tuner_note
would be a quite straightforward, very similar to dsp
function from previous chapter, but wrapped in several lambdas to provide
initialization and context for several layers: instrument instance, note
instance and calculating note’s samples.
;; <tuner-note>
(bind-func tuner_note
(lambda ()
;; here you can put init of entire instrument
(lambda (data:NoteData* nargs:i64 dargs:SAMPLE*)
;; here init of certain note
(let ((frequency (note_frequency data))
(amplitude (note_amplitude data))
(starttime (note_starttime data))
(duration (note_duration data)))
(lambda (time:i64 chan:i64)
;; here we produce samples for this note
(if (< (- time starttime) duration)
(* amplitude (sine time frequency))
0.0))))))
;; </tuner-note>
tuner_fx
is even easier, because we just pass tuner_note
result without
any change:
;; <tuner-fx>
(bind-func tuner_fx
(lambda ()
;; here put fx init
(lambda (in:SAMPLE time:i64 chan:i64 dat:SAMPLE*)
in)))
;; </tuner-fx>
make-instrument
macro allows to glue it together:
;; <make-tuner>
(make-instrument tuner tuner)
;; </make-tuner>
The first tuner
is the name of our instrument, and the second one is
function name prefix. Extempore than will glue tuner_note
and tuner_fx
functions. Beware not to make a typo in function names, because otherwise
segmentation fault is more than probable. Extempore will warn new that
functino is not found, but then will say that new instrument is bound anyway
and then will crash trying to play it.
Next step is to use our brand new instrument in dsp function:
;; <tuner-dsp>
(bind-func dsp:DSP
(lambda (in time chan dat)
(tuner in time chan dat)))
;; </tuner-dsp>
Okay, instrument is set up, let’s play a note finally!
;; <play-note-now>
(play-note (now) tuner 60 90 44100)
;; </play-note-now>
Wow! That’s magic. Here is complete file for instrument and one note. Sip
your coffee, we’ll move to play-note
signature explanation and playing harmony then.
;; <setup-tuner>
<<load-instruments>>
<<define-sine>>
<<tuner-note>>
<<tuner-fx>>
<<make-tuner>>
<<tuner-dsp>>
<<set-dsp>>
;; </setup-tuner>
;; <xtm/03-harmony-tuner.xtm>
<<setup-tuner>>
<<play-note-now>>
;; </xtm/03-harmony-tuner.xtm>
If you want just play chord from course page then don’t wait anymore:
;; <play-pleasant-chord>
(let ((t (now))
(dur 22050))
(play-note t tuner 60 100 dur)
(play-note (+ t (* 2 dur)) tuner 64 100 dur)
(play-note (+ t (* 4 dur)) tuner 67 100 dur)
(let ((t (+ t (* 6 dur))))
(play-note t tuner 60 100 dur)
(play-note t tuner 64 100 dur)
(play-note t tuner 67 100 dur)))
;; </play-pleasant-chord>
And not so pleasant one:
;; <play-unpleasant-chord>
(let ((t (now))
(dur 22050))
(play-note t tuner 61 100 dur)
(play-note (+ t (* 2 dur)) tuner 67 100 dur)
(play-note (+ t (* 4 dur)) tuner 75 100 dur)
(let ((t (+ t (* 6 dur))))
(play-note t tuner 61 100 dur)
(play-note t tuner 67 100 dur)
(play-note t tuner 75 100 dur)))
;; </play-unpleasant-chord>
Leveraging basic abstractions:
;; <define-pleasant-chord>
(define pleasant-chord
(lambda (pitch)
(list pitch (+ pitch 4) (+ pitch 7))))
;; </define-pleasant-chord>
;; <define-unpleasant-chord>
(define unpleasant-chord
(lambda (pitch)
(list (+ pitch 1) (+ pitch 7) (+ pitch 15))))
;; </define-unpleasant-chord>
;; <play-chord>
(define play-chord
(lambda (t inst pitches dur)
(let ((together-time (+ t (* 2 (length pitches) dur))))
(for-each
(lambda (i pitch)
(play-note (+ t (* 2 i dur)) inst pitch 100 dur)
(play-note together-time inst pitch 100 dur))
(range (length pitches))
pitches))))
;; </play-chord>
And the source file:
;; <xtm/04-harmony-chord.xtm>
<<setup-tuner>>
<<define-pleasant-chord>>
<<define-unpleasant-chord>>
<<play-chord>>
(play-chord (now) tuner (pleasant-chord 60) 22050)
;; (play-chord (now) tuner (unpleasant-chord 60) 22050)
;; </xtm/04-harmony-chord.xtm>
Now let’s go into details what’s happening in code above.
First of all, breakdown of play-note
signature:
- time
- when note should be started. Time in Extempore is expressed in
number of samples rendered from its start. Current time is
available via
now
function. - instrument
- instrument to play note with. Remember second-level closure
in
instrument_note
? Instrument argument is required to call it and initialize the note we are going to play. - pitch
- frequency of the note expressed in terms of MIDI pitch, 0-127
- vol
- amplitude of the note expressed as volume, as per formula:
(/ (exp (/ vol 26.222)) 127.0)
, 0-127 - duration
- duration of note. Duration in Extempore is expressed as a number of samples to be generated. If you are rendering sound at 44100Hz sampling rate, then you need to pass 44100 for a 1 second long note.
Notice that play-note
allows us to schedule note start at any time.
We use it in play-chord
to play all passed pitches one by one and then to
play them all again, but simultaneously. We schedule all notes at ones, just
at differents points in time.
Let’s do the trick and play notes from MIDI controller. The latest
midi_input
supports defining MIDI callback in Scheme (not only xtlang),
it will make stuff easier for us because of no need to switch language
contexts. Replace 19 with your pitch slider CC.
;; <midi-chords-pitch>
(define *pitch* 60)
(define midi-cc
(lambda (timestamp controller value chan)
(cond ((= controller 19) (set! *pitch* value))
(else #f))))
;; </midi-chords-pitch>
Now let’s control note start and stop. Replace 1 with you button NT.
;; <midi-chords-play-button>
(define midi-note-on
(lambda (timestamp pitch volume chan)
(if (= pitch 1)
(play-chord (now) tuner (pleasant-chord *pitch*) 22050))))
;; </midi-chords-play-button>
As an alternative, if you have MIDI keyboard, you can take pitch directly from pressed key:
;; <midi-chords-play-keyboard>
(define midi-note-on
(lambda (timestamp pitch volume chan)
(play-chord (now) tuner (pleasant-chord pitch) 22050)))
;; </midi-chords-play-keyboard>
To make it work we need to start listener:
;; <start-midi-listener>
(scheme-midi-listener (*metro* 'get-beat 4) 1/24))
;; </start-midi-listener>
And whole files for button and keyboard:
;; <xtm/05-midi-chord-button.xtm>
<<xtm/04-harmony-chord.xtm>>
<<midi-chords-pitch>>
<<midi-chords-play-button>>
<<start-midi-listener>>
;; </xtm/05-midi-chord-button.xtm>
;; <xtm/06-midi-chord-keyboard.xtm>
<<xtm/04-harmony-chord.xtm>>
<<midi-chords-play-keyboard>>
<<start-midi-listener>>
;; </xtm/06-midi-chord-keyboard.xtm>
I mentioned Extempore’s osc_c
briefly as an alternative for hand-rolled
sine wave generator. Now it’s time to write down (and hear!) difference
between the two. osc_c
encloses phase, and our sine
takes it implicitly
as a timestamp. But in this case it’s not just a question of style (FP-ish
explicit argument passing vs OOP-y mixing state with code), but a subtle
difference in behavior. Waves produced by both oscillators are the same when
frequency stays constant. But sine
goes glitchy when frequency changes,
that’s why usually osc_c
is the way to go (though sometimes you want to
produce glitches on purpose).
To hear the difference let’s apply frequency modulation to our tuner
and
make… hmmm… fm_tuner
instrument ;-)
;; <fm-tuner-note>
(bind-func fm_tuner_note
(lambda ()
;; here you can put init of entire instrument
(lambda (data:NoteData* nargs:i64 dargs:SAMPLE*)
;; here init of certain note
(let ((frequency (note_frequency data))
(amplitude (note_amplitude data))
(starttime (note_starttime data))
(duration (note_duration data)))
(lambda (time:i64 chan:i64)
;; here we produce samples for this note
(if (< (- time starttime) duration)
(* amplitude
(sine time (+ frequency
(* 50.0
(sine time (* 0.1 frequency))))))
0.0))))))
;; </fm-tuner-note>
And fm_tuner_fx
will still do nothing (but don’t hesitate to edit it by
your taste!)
;; <fm-tuner-fx>
(bind-func fm_tuner_fx
(lambda ()
(lambda (in:SAMPLE time:i64 chan:i64 dat:SAMPLE*)
in)))
;; </fm-tuner-fx>
The moment of truth, our poor-man FM-synth sound:
;; <xtm/07-fm-tuner-sine.xtm>
<<load-instruments>>
<<define-sine>>
<<fm-tuner-note>>
<<fm-tuner-fx>>
(make-instrument fm_tuner fm_tuner)
(bind-func dsp:DSP
(lambda (in time chan dat)
(fm_tuner in time chan dat)))
<<set-dsp>>
(play-note (now) fm_tuner 60 90 44100)
;; </xtm/07-fm-tuner-sine.xtm>
Do you hear? It’s not even glitchy, it’s just a noise. Let’s do the same
synth using osc_c
:
;; <fm-tuner-note-osc>
(bind-func fm_tuner_note
(lambda ()
;; here you can put init of entire instrument
(lambda (data:NoteData* nargs:i64 dargs:SAMPLE*)
;; here init of certain note
(let ((frequency (note_frequency data))
(amplitude (note_amplitude data))
(starttime (note_starttime data))
(duration (note_duration data))
(carrier (osc_c 0.0))
(modulator (osc_c 0.0)))
(lambda (time:i64 chan:i64)
;; here we produce samples for this note
(if (< (- time starttime) duration)
(carrier amplitude
(+ frequency
(modulator 50.0 (* 0.1 frequency))))
0.0))))))
;; </fm-tuner-note-osc>
And the file:
;; <xtm/08-fm-tuner-osc.xtm>
<<load-instruments>>
<<fm-tuner-note-osc>>
<<fm-tuner-fx>>
(make-instrument fm_tuner fm_tuner)
(bind-func dsp:DSP
(lambda (in time chan dat)
(fm_tuner in time chan dat)))
<<set-dsp>>
(play-note (now) fm_tuner 60 90 44100)
;; </xtm/08-fm-tuner-osc.xtm>
This one is so nice, isn’t it? Viva la osc_c
;-) Let’s redo our tuner
instrument with it:
;; <tuner-note-osc>
(bind-func tuner_note
(lambda ()
;; here you can put init of entire instrument
(lambda (data:NoteData* nargs:i64 dargs:SAMPLE*)
;; here init of certain note
(let ((frequency (note_frequency data))
(amplitude (note_amplitude data))
(starttime (note_starttime data))
(duration (note_duration data))
(carrier (osc_c 0.0)))
(lambda (time:i64 chan:i64)
;; here we produce samples for this note
(if (< (- time starttime) duration)
(carrier amplitude frequency)
0.0))))))
;; </tuner-note-osc>
;; <setup-tuner-osc>
<<load-instruments>>
<<tuner-note-osc>>
<<tuner-fx>>
<<make-tuner>>
<<tuner-dsp>>
<<set-dsp>>
;; </setup-tuner-osc>
Five notes with simple ratios forms pentatonic scale. This scale one of the most ancient and it has a nice property “easy to learn, hard to master”. Playing in pentatonic scale you would create more or less pleasant melody without any effort, though making anything really impressive require the same amount of work, or even more, as using other scales.
Let’s make up our scale from the ground to get used with it. For musings you’d better use Extempore standard library, “pitch class and interval sets” module.
First things first, let’s resurrect our simple instrument playing sine wave
by using <<setup-tuner>>
block.
;; <load-pc>
(sys:load "libs/core/pc_ivl.xtm")
;; </load-pc>
As long as start note of the scale could be any, let’s make it a parameter for our scale-building function. I’m going to use 0-based indexing because it’s easier to align with list indices in Extempore. Our scale would be just a list of frequencies.
(define make-pentatonic-scale
(lambda (freq0)
;; here is our code
))
freq3
relates to freq0
as 3:2
, freq2
as 5:4
, freq4
as 5:3
,
freq1
as 9:8
;; <make-pentatonic-scale-freq>
(define make-pentatonic-scale-freq
(lambda (freq0)
(map (lambda (x) (* x freq0))
'(1 9/8 3/2 5/4 3/2 5/3))))
;; </make-pentatonic-scale-freq>
If we want to work with MIDI notes, some extra calculations are required:
;; <make-pentatonic-scale>
<<make-pentatonic-scale-freq>>
(define make-pentatonic-scale
(lambda (start-note)
(map frq2midi (make-pentatonic-scale-freq (midi2frq start-note)))))
;; </make-pentatonic-scale>
Now let’s try to play scale in sequence and in chord:
;; <play-pentatonic-scale>
<<play-chord>>
(play-chord (now) tuner (make-pentatonic-scale 60) *second*)
;; </play-pentatonic-scale>
;; <xtm/09-pentatonic-scale.xtm>
<<setup-tuner-osc>>
<<make-pentatonic-scale>>
<<play-pentatonic-scale>>
;; <<load-pc>>
;; </xtm/09-pentatonic-scale.xtm>