Skip to content

ul/lightnote

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WTF?

Learning Extempore while following 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 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:

  1. 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.
  2. 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

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.

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:

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

MIDI controller

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>

Intermezzo: osc_c

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>

Keys & Scales

Pentatonic scale

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>

Releases

No releases published

Packages

No packages published

Languages