18 Sep '19
Xinyu's Extempore tutorials
These tutorials are part of Xinyu Hou’s research project to create a set of PRIMM-style tutorials for learning to make sound/music in Extempore.
They’re currently a work-in-progress—when they’re done they’ll be hosted on the main Extempore docs site, but for now they’re here on my blog.
table of contents
Preliminaries
Setting Up Environment
Installing Extempore
Install Extempore by following the steps here.
Installing VSCode
Install VSCode (a text editor) by following the steps here.
Note: if you want to use a different text editor, there are other options as well.
Evaluating Extempore Code
Evaluate the extempore code on VSCode by following the steps below:
-
Start VSCode
-
Go to File -> Open, then open your Extempore directory.
-
In the VSCode built-in terminal window (ctrl+`) move into the extempore directory, and start extempore (type
./extempore
and hit return). -
Open or create an Extempore file (.xtm file)
-
Connect to the Extempore process: you can do this in VSCode through the “command palette”, which can be brought up with ctrl+Shift+P on Windows/Linux or cmd+Shift+P on macOS.
</picture>
- Type Extempore Connect in the opening command palette, the default host
and port arguments will be
localhost
and7099
respectively.
To evaluate Extempore code: move the cursor into the code you want to evaluate and hit cmd+enter on macOS or ctrl+enter on Windows/Linux.
What you might see in the terminal window when you evaluate extempore code:
Note: you can use ctrl+c in the terminal where extempore is running to kill the extempore process whenever you want.
Tutorial One
Predict and Run
Work in pairs or small groups, look at the code below and predict what does this code do:
(bind-val my-pi float 3.1415)
Note: in Extempore, we generally use kebab-case case style
my-pi
instead of using camelCasemyPi
or PascalCaseMyPi
or snake_casemy_pi
.
Run the code above. Does the compiler print anything in the log? What does the printed message mean?
Now, it’s time to produce a sound in Extempore!
Work in pairs or small groups again, look at the code below and predict/guess what might happen when it runs:
(bind-func dsp:DSP
(lambda (in time chan dat)
(let ((amplitude 0.1)
(frequency 440.0)
(two-pi (* 2.0 my-pi)))
(* amplitude
(sin (/ (* frequency
two-pi
(convert time)) SRf))))))
(dsp:set! dsp)
Does it work as predicted?
Hint: we set up the xtlang callback:
(bind-func dsp:DSP (lambda (in time chan dat) 0.0))
- 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 range from -1.0 to 1.0
Note: In Extempore, we represent the mathematical operation 2 * PI as
(* 2 PI)
Note:
SRf
refers to the current sampling frequencyNote: The form of a
let
statement is as follows:(let ((var_name_1 var_value_1) ... (var_n val_n)) (<body>))
Note:
convert
allows us to make aSAMPLE
typed value fromtime
.Note: We only can set
dsp
function once, but then redefine it as many times as we want.
What about white noise in Extempore?
(bind-func dsp:DSP
(lambda (in time chan data)
(* 0.2 (random))))
Can you explain what happened here?
Investigate
Work in pairs or in small groups, work out the answers to the following questions:
- In what order did you compile (execute) the code in this program?
- What happened after you evaluated each “chunk” of Extempore code?
- Can you guess what would happen if you compile the code in a different order?
Note: you can add comments (starting with ;;) to the program to make some notes for you to understand the code. The comments are generally ignored by compilers and interpreters.
Modify
- Can you play a sound with a frequency of 460.0?
- Can you make the sound louder by just changing the code? (don’t use your volume buttons!)
Make
Now, let’s move amplitude
, frequency
and two-pi
outside of lambda to make our dsp
function controllable outside as below:
(bind-func dsp:DSP
(let ((amplitude 0.1)
(frequency 440.0)
(two-pi (* 2.0 my-pi)))
(lambda (in time chan dat)
(* amplitude
(sin (/ (* frequency
two-pi
(convert time)) SRf))))))
Can you play a random frequency sound every time when you redefine the dsp
function?
Note: You could use
(random 440.0 700.0)
to randomly generate a value between 440.0 and 700.0
Compare to the (random)
in the white noise part above, does the (random)
work differently here? Can you explain how (random)
can be either for “control” (e.g. see the above sine wave part) or for “signal” (e.g. see the white noise part)?
Tutorial Two
Predict and Run
Work in pairs or small groups, look at the code below and predict what might happen when it runs.
;; load the instruments file
(sys:load "libs/core/instruments.xtm")
;; define an fmsynth using the built-in components
(make-instrument fmsynth fmsynth)
;; add the instrument to the DSP output sink closure
(bind-func dsp:DSP
(lambda (in time chan dat)
(fmsynth in time chan dat)))
(dsp:set! dsp)
Note: The dsp function takes as input:
- in: the input audio sample, e.g. from the microphone.
- time: an
i64
representing the time.- chan: another
i64
which represents the channel index (0 for L, 1 for R, etc.). Extempore can handle any number of channels.- data: this is a pointer to a
SAMPLE
type (which is float by default), and can be used to pass arbitrary data into the dsp function.
Think about what is the i64
type in the above Note?
Primitive types in Extempore:
Integers:
i1
: (boolean) uses 1 bit.i8
: (char) uses 8 bits (1 byte), which could store 2^8 = 256 unique integer values, -128 ~ 127.i32
: uses 32 bits, which could store 2^32 uniquew integer values, -2^31 ~ (2^31 - 1).i64
: (default) uses 64 bits, which could store 2^63 uniquew integer values, -2^63 ~ (2^63 - 1).Floats:
float
: a single precision (32 bit) floating-point data type.double
: (default), a double precision (64 bit) floating-point data type.
Moreover, for your interests:
Pointer types:
double*
: a pointer to a double.i64*
: a pointer to a 64-bit integer.i64**
: a pointer to a pointer to a 64-bit integer.
Now, we could play a note by using the fmsynth instrument that we just defined:
;; play a note on our fmsynth
(play-note (now) fmsynth (random 60 80) 80 *second*)
Note: the parameters for
play-note
areplay-note <start-time> <instrument> <note-frequency> <note-duration>
What would happend if you play the note for the second time (or multiple times)? Why is the note different?
It’s the time to make a loop in Extempore:
;; make a loop
(define my-loop
(lambda (time)
(play-note time fmsynth (random 60 80) 80 *second*)
(callback (+ time *second*) 'my-loop (+ time *second*))))
(my-loop (now))
We make a loop
function above, but how does the loop
work? let’s see a simpler example:
;; let's see a simpler loop example
(define test-loop
(lambda ()
(println "I'm looping...")
(callback (+ (now) 10000 *second*) 'test-loop ()))
)
(test-loop())
Note: the parameters for
callback
arecallback <time> <func> <args>
Can you predict what will happen when you run the test-loop
function? Run the code to check your thoughts.
Note: do not forget to check your terminal panel.
Investigate
Work in pairs or in small groups, work out the answers to the following questions:
- What does
my-loop
do? - What does the
callback
in the loop do? - What is the difference between Scheme lambda expression and xtlang bind-func expression? More information click here.
- What is the difference between using now versus using time? More information click here.
Note: you can add comments (starting with
;;
) to the program to make some notes for you to understand the code.
Modify
Work in pairs or in small groups, can you modify the above code and make a loop with random notes lengths and random notes frequencies.
;; hint
;; make a loop with random note length and random note
(define diff-length
(lambda (time)
(let ((note-length (random '(0.5 1.0 1.5 2.0)))
(pitch (random '(60 63 65 68 70 73 75 78 80))))
(play-note time fmsynth pitch 80 (* *second* note-length))
(callback (+ time (* note-length *second*)) 'diff-length (+ time (* note-length *second*)))))))
(diff-length (now))
Make
Can you make a chord (or play different notes at the same time) function by using similar ideas to the previous program?
Tutorial Three
Predict and Run
;; loop major scale
(let loop-white-keys((scale '(0 2 4 5 7 9 11 12 14 16 17 19 21 23))
(time 0))
(play-note (+ (now) time) synth (+ 60 (car scale)) 80 4000)
(if (not (null? (cdr scale)))
(loop-white-keys (cdr scale) (+ time 10000))))
;; another loop for notes
(let loop-black-keys ((scale '(1 3 6 8 10 13 15 18 20 22))
(time 0))
(play-note (+ (now) time) synth (+ 60 (car scale)) 80 4000)
(if (not (null? (cdr scale)))
(loop-black-keys (cdr scale) (+ time 10000))))
Note:
car
return the first element of a list.cdr
return the rest of the elements in a list, that is, it returns the part of the list that follows the first item.if <test> <consequent> <alternate>
- more conditionals syntax.
Note: the syntax for
let
is:let <variable> <bindings> <body>
Note: in the above examples, we used the named
let
. Therefore, the<body>
could be repeatly executed by invoking thelet
procedure named by the<variable>
.
Can you predict what will happen? Then run the code to check your thoughts.
Before you run the code above, do not forget to load the libraries, define and add an instrument synth
to the dsp
output sink closure:
;; load the instruments file
(sys:load "libs/core/instruments.xtm")
(sys:load "libs/core/pc_ivl.xtm")
;; define a synth using the provided components
(make-instrument synth fmsynth)
;; add the instrument to the DSP output sink closure
(bind-func dsp:DSP
(lambda (in time chan dat)
(synth in time chan dat)))
(dsp:set! dsp)
Now, Let’s play a chord on a list of frequencies! Can you predict what will happen here:
;; play a chord
(map (lambda (p)
(play-note (now) synth p 80 44100))
(list 72 76 79))
Note: again, the parameters for
lambda
is:lambda <formals> <body>
Note: here we have two methods to express a
list
: For example,(list 72 76 79)
and'(72 76 79)
.
Can you guess what does map
do?
Here is another method to play the same chord that we ran above:
;; another method for play the same chord
(define play-a-chord
(lambda (time chord)
(for-each (lambda (p)
(play-note time synth p 80 44100))
chord)
))
(play-a-chord (now) '(72 76 79))
Can you guess what will happen after running play-a-chord
?
How about more chords?
;; markov chord progression
(define my-progression
(lambda (time chords)
(play-a-chord time (car chords))
(define loop-chords '())
(if (not (null? (cdr chords)))
(set! loop-chords (cdr chords))
(set! loop-chords '((72 76 79)(69 72 76)(65 69 72)(67 71 74))))
(callback (+ time 40000) 'my-progression (+ time 44100) loop-chords))
)
(my-progression (now) '((72 76 79)(69 72 76)(65 69 72)(67 71 74)))
Note: the syntax for
set!
isset! <variable> <expression>
, which could assign the<expression>
to the<variable>
Run the code. Dose it work as you predicted?
Investigate
Work in pairs or in small groups, work out the answers to the following questions:
- Currently, you have already seen
let
,define
andset!
. What are the differences betweenlet
,define
andset!
? E.g:set!
doesn’t define the variable, rather it is used to assign the variable a new value.- The scope of variables defined by
let
are bound to the latter.define
doesn’t surround the body with parentheses.
- Here is another method to play the same chord that we predicted and ran before:
;; another method for play the same chord
(define play-a-chord
(lambda (time chord)
(for-each (lambda (p)
(play-note time synth p 80 44100))
chord)
))
(play-a-chord (now) '(72 76 79))
Compare to the map
one, can you find what is the difference between map
and for-each
?
- Evaluation order?
- Return?
- We could use
do
to make a loop on playing the chord with the aboveplay-a-chord
method:
;; use do to loop the chord
(do ((i 0 (+ i 1)))
((> i 3000) ())
(play-a-chord (now) '(72 76 79)))
Compare to other loop
or other iteration methods, can you find how does the do
work here?
Hint: the syntax of
do
is:(do ((<variable1> <init1> <step1>) ...) (<test> <expression> ...) <command> ...)
- if the
<test>
result isfalse
, then the sequence of<command>
would be evaluated.- if the
<test>
result istrue
, the sequence of<expression>
would be evaluated from left to right, and the last<expression>
’s values would be returned at the end.- the boolean data type for understanding
true
andfalse
.
Modify
Now, can you please modify the my-progression
function so that it could loop each chord for four times before going to the next chord?
;; hint
;; modified my-progression
(define my-progression-modify
(lambda (time chords n)
(let loop ((loop-times 0))
(if (= loop-times n)
(display "stopped")
(begin (play-a-chord (+ time (* loop-times 44100)) (car chords))
(loop (+ loop-times 1)))))
; (loop-n-times n (play-a-chord time (car chords)))
(define loop-chords-modify '())
(if (not (null? (cdr chords)))
(set! loop-chords-modify (cdr chords))
(set! loop-chords-modify '((72 76 79)(69 72 76)(65 69 72)(67 71 74))))
(callback (+ time (* n 40000)) 'my-progression-modify (+ time (* n 44100)) loop-chords-modify n))
)
(my-progression-modify (now) '((72 76 79)(69 72 76)(65 69 72)(67 71 74)) 4)
Note: here we used the
begin
to do the sequencing:begin <expression 1> <expression 2> ...<expression n>
- the sequence of
<expression i>
will be evaluated sequentially from left to right.begin
will return the value of the last<expression n>
.
Make
- Based on the previous code, can you make some creative melody by yourself?
- Can you play your melody and the chords at the same time?
Tutorial Four
Predict and Run
In this tutorial, we will learn how to do samplers in Extempore. Extempore provides a built-in sampler in libs/external/instruments_ext.xtm
, so let’s load the libraries first:
;; load the libs
(sys:load "libs/external/instruments_ext.xtm")
(sys:load "libs/core/instruments.xtm")
We are going to create a drum sampler, can you guess what is happening here:
;; TODO
;; Set/Change the path of the subdirectory “OH” in your code.
;; In my computer it’s like:
(define drum-path "/Users/apple/Downloads/COMP3740/salamanderDrumkit/OH/")
;; Load some wave files into drums sampler:
(set-sampler-index drums (string-append drum-path "kick_OH_F_9.wav") *gm-kick* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "snareStick_OH_F_9.wav") *gm-side-stick* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "snare_OH_FF_9.wav") *gm-snare* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "hihatClosed_OH_F_20.wav") *gm-closed-hi-hat* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "hihatFoot_OH_MP_12.wav") *gm-pedal-hi-hat* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "hihatOpen_OH_FF_6.wav") *gm-open-hi-hat* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "loTom_OH_FF_8.wav") *gm-low-floor-tom* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "hiTom_OH_FF_9.wav") *gm-hi-floor-tom* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "crash1_OH_FF_6.wav") *gm-crash* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "ride1_OH_FF_4.wav") *gm-ride* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "china1_OH_FF_8.wav") *gm-chinese* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "cowbell_FF_9.wav") *gm-cowbell* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "bellchime_F_3.wav") *gm-open-triangle* 0 0 0 1)
(set-sampler-index drums (string-append drum-path "ride1Bell_OH_F_6.wav") *gm-ride-bell* 0 0 0 1)
Let’s look at the first row of set-sampler-index
. Can you guess why do we need *gm-kick*
? What does string-append drum-path "kick_OH_F_9.wav"
do?
Note: How to load samples (you can also find more information from github).
- Download the instrument samples from Salamader Drumkit.
- Unzip the samples and put them wherever you like on your computer.
- Set/Change the path of the subdirectory “OH” in your code. In my computer it’s like:
define drum-path "/Users/apple/Downloads/COMP3740/salamanderDrumkit/OH/"
.
Before you run the code, do not forget to define and add an instrument drums
to the dsp
output sink closure, and set the dsp
as well:
;; define a sampler (called drums) using the default sampler note kernel and effects
(make-instrument drums sampler)
;; define a synth using the provided components
(make-instrument synth fmsynth)
;; add the sampler to the dsp output callback
(bind-func dsp:DSP
(lambda (in time chan dat)
(+ (synth in time chan dat)
(drums in time chan dat))))
(dsp:set! dsp)
Sounds could be played by calling the below instructions, the similar things as we did before:
;; evaluate
(play-note (now) drums *gm-open-triangle* 80 44100)
(play-note (now) drums *gm-snare* 80 44100)
(play-note (now) drums *gm-closed-hi-hat* 80 44100)
Can you predict what will happen after you run above three lines respectively? Then run the code to check your thoughts.
Now, a cool thing we will do is to define a metronome
to evenly distribute our beats with respect to tempo changes.
Can you predict what is happening here?
;; create a metronome starting at 120 bpm
(define *metro1* (make-metro 120))
;; beat loop along the metronome
(define drum-loop
(lambda (time duration drum)
(println time duration)
(play-note (*metro1* time) drums drum 80 (*metro1* 'dur duration))
(callback (*metro1* (+ time (* .5 duration))) 'drum-loop (+ time duration)
duration drum)))
(drum-loop (*metro1* 'get-beat) 1 *gm-hi-floor-tom*)
Let’s define another metronome metronome2
:
;; create another metronome starting at 120 bpm
(define *metro2* (make-metro 120))
Can you guess what will happen after you evaluate this drum-loop-tempo-shift
? What does println
will do?
;; set tempo shift
(define drum-loop-tempo-shift
(lambda (time duration drum)
(*metro2* 'set-tempo (+ 20 (* 6. (cos (modulo time 9)))))
(println (+ 20 (* 6. (cos (modulo time 9)))))
(play-note (*metro2* time) drums drum 80 (*metro2* 'dur duration))
(callback (*metro2* (+ time (* .5 duration))) 'drum-loop-tempo-shift (+ time duration)
(random (list 0.5)) drum)))
(drum-loop-tempo-shift (*metro2* 'get-beat) 0.5 *gm-cowbell*)
Note:
println
calls the polymorphic functionprintln
automatically provides both spaces and a carriage return.
Run the code. Did it work as you predicted?
Investigate
Work in pairs or in small groups, work out the answers to the following questions:
- What does the function
make-metro
do? - What does
*metro* 'get-beat
do? - What does
*metro* 'dur
do? - What does
*metro* set-tempo
do?
Modify
Work in pairs or in small groups, work out the answers to the following questions:
- Can you load the piano samples as well? (Salamander piano).
- Can you pay multiple drums with different tempo at the same time?
;;hint
(drum-loop (*metro1* 'get-beat) 1 *gm-hi-floor-tom*)
(drum-loop (*metro1* 'get-beat) 0.3 *gm-open-triangle* )
Make
Combine the knowledge that you learned before. Now you can use drums, synth, piano and metronome to create your own music.
Going further
As well as the Extempore documentation
website, the best place to start messing
around is with more examples: e.g. the examples/core/fmsynth.xtm
file in your
Extempore folder.