Lightnote
Extempore exercises following https://app.lightnote.co/ course
Install / Use
/learn @ul/LightnoteREADME
- WTF?
Learning [[https://github.com/digego/extempore][Extempore]] while following [[https://www.lightnote.co/course/][LightNote]] music theory course.
-
Setup
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 [[http://digego.github.io/extempore/index.html][official documentation]], especially about setup and language basics.
To follow the course you need access to [[https://app.lightnote.co/][app,]] could be purchased [[https://www.lightnote.co/course/?ref=sidebarpremium#buy][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 [[http://digego.github.io/extempore/index.html][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 [[http://ul.mantike.pro/lightnote/][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.
-
The Essential Guide to Music Theory
** Sound *** [[https://app.lightnote.co/sound][Sound]]
To produce sound in Extempore we need to setup xtlang callback:
#+NAME: set-dsp #+BEGIN_SRC extempore ;; <set-dsp> (dsp:set! dsp) ;; </set-dsp> #+END_SRC
#+NAME: xtm/00-sound-silence.xtm #+BEGIN_SRC extempore :tangle xtm/00-sound-silence.xtm :noweb yes :mkdirp yes :padline no ;; <xtm/00-sound-silence.xtm> (bind-func dsp:DSP (lambda (in time chan dat) 0.0)) <<set-dsp>> ;; </xtm/00-sound-silence.xtm> #+END_SRC
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:
#+NAME: xtm/01-sound-sine.xtm #+BEGIN_SRC extempore :tangle xtm/01-sound-sine.xtm :noweb yes :mkdirp yes :padline no ;; <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> #+END_SRC
=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.
**** MIDI controller
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:
#+NAME: load-midi-input #+BEGIN_SRC extempore ;; <load-midi-input> (sys:load "libs/external/midi_input.xtm") ;; </load-midi-input> #+END_SRC
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):
#+NAME: set-midi-in #+BEGIN_SRC extempore ;; <set-midi-in> (set_midi_in 3) ;; </set-midi-in> #+END_SRC
To make our =dsp= function controllable outside let's move =amplitude= and
=controller= outside of lambda:
#+NAME: sine-closure-dsp #+BEGIN_SRC extempore ;; <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> #+END_SRC
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):
#+NAME: sine-midi-cc #+BEGIN_SRC extempore ;; <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> #+END_SRC
If you execute snippets one-by-one then you should have response already.
Otherwise here is entire file:
#+NAME: xtm/02-sound-sine-midi.xtm #+BEGIN_SRC extempore :tangle xtm/02-sound-sine-midi.xtm :noweb yes :mkdirp yes :padline no ;; <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> #+END_SRC
*** [[https://app.lightnote.co/harmony][Harmony]]
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:
#+NAME: load-instruments #+BEGIN_SRC extempore ;; <load-instruments> (sys:load "libs/core/instruments.xtm") ;; </load-instruments> #+END_SRC
And define helpers for generating sine wave:
#+NAME: define-sine #+BEGIN_SRC extempore ;; <define-sine> (bind-val omega SAMPLE (/ STWOPI SRf))
(bind-func sine (lambda (time:i64 freq:SAMPLE) (sin (* omega freq (convert time))))) ;; </define-sine> #+END_SRC
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.
#+NAME: tuner-note #+BEGIN_SRC extempore ;; <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> #+END_SRC
=tuner_fx= is even easier, because we just pass =tuner_note= result without
any change:
#+NAME: tuner-fx #+BEGIN_SRC extempore ;; <tuner-fx> (bind-func tuner_fx (lambda () ;; here put fx init (lambda (in:SAMPLE time:i64 chan:i64 dat:SAMPLE*) in))) ;; </tuner-fx> #+END_SRC
=make-instrument= macro allows to glue it together:
#+NAME: make-tuner #+BEGIN_SRC extempore ;; <make-tuner> (make-instrument tuner tuner) ;; </make-tuner> #+END_SRC
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 b
