https://github.com/ul/lightnote
Extempore exercises following https://app.lightnote.co/ course
https://github.com/ul/lightnote
extempore live-coding livecoding music
Last synced: 11 months ago
JSON representation
Extempore exercises following https://app.lightnote.co/ course
- Host: GitHub
- URL: https://github.com/ul/lightnote
- Owner: ul
- Created: 2017-02-28T16:12:06.000Z (almost 9 years ago)
- Default Branch: master
- Last Pushed: 2017-08-13T08:09:05.000Z (over 8 years ago)
- Last Synced: 2025-03-08T17:08:55.850Z (11 months ago)
- Topics: extempore, live-coding, livecoding, music
- Language: Shell
- Size: 36.1 KB
- Stars: 8
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.html
Awesome Lists containing this project
README
<!--/*--><![CDATA[/*><!--*/
.title { text-align: center;
margin-bottom: .2em; }
.subtitle { text-align: center;
font-size: medium;
font-weight: bold;
margin-top:0; }
.todo { font-family: monospace; color: red; }
.done { font-family: monospace; color: green; }
.priority { font-family: monospace; color: orange; }
.tag { background-color: #eee; font-family: monospace;
padding: 2px; font-size: 80%; font-weight: normal; }
.timestamp { color: #bebebe; }
.timestamp-kwd { color: #5f9ea0; }
.org-right { margin-left: auto; margin-right: 0px; text-align: right; }
.org-left { margin-left: 0px; margin-right: auto; text-align: left; }
.org-center { margin-left: auto; margin-right: auto; text-align: center; }
.underline { text-decoration: underline; }
#postamble p, #preamble p { font-size: 90%; margin: .2em; }
p.verse { margin-left: 3%; }
pre {
border: 1px solid #ccc;
box-shadow: 3px 3px 3px #eee;
padding: 8pt;
font-family: monospace;
overflow: auto;
margin: 1.2em;
}
pre.src {
position: relative;
overflow: visible;
padding-top: 1.2em;
}
pre.src:before {
display: none;
position: absolute;
background-color: white;
top: -10px;
right: 10px;
padding: 3px;
border: 1px solid black;
}
pre.src:hover:before { display: inline;}
/* Languages per Org manual */
pre.src-asymptote:before { content: 'Asymptote'; }
pre.src-awk:before { content: 'Awk'; }
pre.src-C:before { content: 'C'; }
/* pre.src-C++ doesn't work in CSS */
pre.src-clojure:before { content: 'Clojure'; }
pre.src-css:before { content: 'CSS'; }
pre.src-D:before { content: 'D'; }
pre.src-ditaa:before { content: 'ditaa'; }
pre.src-dot:before { content: 'Graphviz'; }
pre.src-calc:before { content: 'Emacs Calc'; }
pre.src-emacs-lisp:before { content: 'Emacs Lisp'; }
pre.src-fortran:before { content: 'Fortran'; }
pre.src-gnuplot:before { content: 'gnuplot'; }
pre.src-haskell:before { content: 'Haskell'; }
pre.src-java:before { content: 'Java'; }
pre.src-js:before { content: 'Javascript'; }
pre.src-latex:before { content: 'LaTeX'; }
pre.src-ledger:before { content: 'Ledger'; }
pre.src-lisp:before { content: 'Lisp'; }
pre.src-lilypond:before { content: 'Lilypond'; }
pre.src-lua:before { content: 'Lua'; }
pre.src-matlab:before { content: 'MATLAB'; }
pre.src-mscgen:before { content: 'Mscgen'; }
pre.src-ocaml:before { content: 'Objective Caml'; }
pre.src-octave:before { content: 'Octave'; }
pre.src-org:before { content: 'Org mode'; }
pre.src-oz:before { content: 'OZ'; }
pre.src-plantuml:before { content: 'Plantuml'; }
pre.src-processing:before { content: 'Processing.js'; }
pre.src-python:before { content: 'Python'; }
pre.src-R:before { content: 'R'; }
pre.src-ruby:before { content: 'Ruby'; }
pre.src-sass:before { content: 'Sass'; }
pre.src-scheme:before { content: 'Scheme'; }
pre.src-screen:before { content: 'Gnu Screen'; }
pre.src-sed:before { content: 'Sed'; }
pre.src-sh:before { content: 'shell'; }
pre.src-sql:before { content: 'SQL'; }
pre.src-sqlite:before { content: 'SQLite'; }
/* additional languages in org.el's org-babel-load-languages alist */
pre.src-forth:before { content: 'Forth'; }
pre.src-io:before { content: 'IO'; }
pre.src-J:before { content: 'J'; }
pre.src-makefile:before { content: 'Makefile'; }
pre.src-maxima:before { content: 'Maxima'; }
pre.src-perl:before { content: 'Perl'; }
pre.src-picolisp:before { content: 'Pico Lisp'; }
pre.src-scala:before { content: 'Scala'; }
pre.src-shell:before { content: 'Shell Script'; }
pre.src-ebnf2ps:before { content: 'ebfn2ps'; }
/* additional language identifiers per "defun org-babel-execute"
in ob-*.el */
pre.src-cpp:before { content: 'C++'; }
pre.src-abc:before { content: 'ABC'; }
pre.src-coq:before { content: 'Coq'; }
pre.src-groovy:before { content: 'Groovy'; }
/* additional language identifiers from org-babel-shell-names in
ob-shell.el: ob-shell is the only babel language using a lambda to put
the execution function name together. */
pre.src-bash:before { content: 'bash'; }
pre.src-csh:before { content: 'csh'; }
pre.src-ash:before { content: 'ash'; }
pre.src-dash:before { content: 'dash'; }
pre.src-ksh:before { content: 'ksh'; }
pre.src-mksh:before { content: 'mksh'; }
pre.src-posh:before { content: 'posh'; }
/* Additional Emacs modes also supported by the LaTeX listings package */
pre.src-ada:before { content: 'Ada'; }
pre.src-asm:before { content: 'Assembler'; }
pre.src-caml:before { content: 'Caml'; }
pre.src-delphi:before { content: 'Delphi'; }
pre.src-html:before { content: 'HTML'; }
pre.src-idl:before { content: 'IDL'; }
pre.src-mercury:before { content: 'Mercury'; }
pre.src-metapost:before { content: 'MetaPost'; }
pre.src-modula-2:before { content: 'Modula-2'; }
pre.src-pascal:before { content: 'Pascal'; }
pre.src-ps:before { content: 'PostScript'; }
pre.src-prolog:before { content: 'Prolog'; }
pre.src-simula:before { content: 'Simula'; }
pre.src-tcl:before { content: 'tcl'; }
pre.src-tex:before { content: 'TeX'; }
pre.src-plain-tex:before { content: 'Plain TeX'; }
pre.src-verilog:before { content: 'Verilog'; }
pre.src-vhdl:before { content: 'VHDL'; }
pre.src-xml:before { content: 'XML'; }
pre.src-nxml:before { content: 'XML'; }
/* add a generic configuration mode; LaTeX export needs an additional
(add-to-list 'org-latex-listings-langs '(conf " ")) in .emacs */
pre.src-conf:before { content: 'Configuration File'; }
table { border-collapse:collapse; }
caption.t-above { caption-side: top; }
caption.t-bottom { caption-side: bottom; }
td, th { vertical-align:top; }
th.org-right { text-align: center; }
th.org-left { text-align: center; }
th.org-center { text-align: center; }
td.org-right { text-align: right; }
td.org-left { text-align: left; }
td.org-center { text-align: center; }
dt { font-weight: bold; }
.footpara { display: inline; }
.footdef { margin-bottom: 1em; }
.figure { padding: 1em; }
.figure p { text-align: center; }
.inlinetask {
padding: 10px;
border: 2px solid gray;
margin: 10px;
background: #ffffcc;
}
#org-div-home-and-up
{ text-align: right; font-size: 70%; white-space: nowrap; }
textarea { overflow-x: auto; }
.linenr { font-size: smaller }
.code-highlighted { background-color: #ffff00; }
.org-info-js_info-navigation { border-style: none; }
#org-info-js_console-label
{ font-size: 10px; font-weight: bold; white-space: nowrap; }
.org-info-js_search-highlight
{ background-color: #ffff00; color: #000000; font-weight: bold; }
.org-svg { width: 90%; }
/*]]>*/-->
/*
@licstart The following is the entire license notice for the
JavaScript code in this tag.
Copyright (C) 2012-2017 Free Software Foundation, Inc.
The JavaScript code in this tag is free software: you can
redistribute it and/or modify it under the terms of the GNU
General Public License (GNU GPL) as published by the Free Software
Foundation, either version 3 of the License, or (at your option)
any later version. The code is distributed WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
As additional permission under GNU GPL version 3 section 7, you
may distribute non-source (e.g., minimized or compacted) forms of
that code without the copy of the GNU GPL normally required by
section 4, provided you include this license notice and a URL
through which recipients can access the Corresponding Source.
@licend The above is the entire license notice
for the JavaScript code in this tag.
*/
<!--/*--><![CDATA[/*><!--*/
function CodeHighlightOn(elem, id)
{
var target = document.getElementById(id);
if(null != target) {
elem.cacheClassElem = elem.className;
elem.cacheClassTarget = target.className;
target.className = "code-highlighted";
elem.className = "code-highlighted";
}
}
function CodeHighlightOff(elem, id)
{
var target = document.getElementById(id);
if(elem.cacheClassElem)
elem.className = elem.cacheClassElem;
if(elem.cacheClassTarget)
target.className = elem.cacheClassTarget;
}
/*]]>*///-->
Table of Contents
2 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:
- 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.
3 The Essential Guide to Music Theory
3.1 Sound
3.1.1 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>
(dsp:set! dsp)
;; </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>
(dsp:set! dsp)
;; </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>
(sys:load "libs/external/midi_input.xtm")
;; </load-midi-input>
;; <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>
;; <set-dsp>
(dsp:set! dsp)
;; </set-dsp>
;; ;; <set-midi-in>
;; (set_midi_in 3)
;; ;; </set-midi-in>
;; <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>
;; </xtm/02-sound-sine-midi.xtm>
3.1.2 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:
;; <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>
(sys:load "libs/core/instruments.xtm")
;; </load-instruments>
;; <define-sine>
(bind-val omega SAMPLE (/ STWOPI SRf))
(bind-func sine
(lambda (time:i64 freq:SAMPLE)
(sin (* omega freq (convert time)))))
;; </define-sine>
;; <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>
(bind-func tuner_fx
(lambda ()
;; here put fx init
(lambda (in:SAMPLE time:i64 chan:i64 dat:SAMPLE*)
in)))
;; </tuner-fx>
;; <make-tuner>
(make-instrument tuner tuner)
;; </make-tuner>
;; <tuner-dsp>
(bind-func dsp:DSP
(lambda (in time chan dat)
(tuner in time chan dat)))
;; </tuner-dsp>
;; <set-dsp>
(dsp:set! dsp)
;; </set-dsp>
;; </setup-tuner>
;; <xtm/03-harmony-tuner.xtm>
;; <setup-tuner>
;; <load-instruments>
(sys:load "libs/core/instruments.xtm")
;; </load-instruments>
;; <define-sine>
(bind-val omega SAMPLE (/ STWOPI SRf))
(bind-func sine
(lambda (time:i64 freq:SAMPLE)
(sin (* omega freq (convert time)))))
;; </define-sine>
;; <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>
(bind-func tuner_fx
(lambda ()
;; here put fx init
(lambda (in:SAMPLE time:i64 chan:i64 dat:SAMPLE*)
in)))
;; </tuner-fx>
;; <make-tuner>
(make-instrument tuner tuner)
;; </make-tuner>
;; <tuner-dsp>
(bind-func dsp:DSP
(lambda (in time chan dat)
(tuner in time chan dat)))
;; </tuner-dsp>
;; <set-dsp>
(dsp:set! dsp)
;; </set-dsp>
;; </setup-tuner>
;; <play-note-now>
(play-note (now) tuner 60 90 44100)
;; </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>
;; <load-instruments>
(sys:load "libs/core/instruments.xtm")
;; </load-instruments>
;; <define-sine>
(bind-val omega SAMPLE (/ STWOPI SRf))
(bind-func sine
(lambda (time:i64 freq:SAMPLE)
(sin (* omega freq (convert time)))))
;; </define-sine>
;; <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>
(bind-func tuner_fx
(lambda ()
;; here put fx init
(lambda (in:SAMPLE time:i64 chan:i64 dat:SAMPLE*)
in)))
;; </tuner-fx>
;; <make-tuner>
(make-instrument tuner tuner)
;; </make-tuner>
;; <tuner-dsp>
(bind-func dsp:DSP
(lambda (in time chan dat)
(tuner in time chan dat)))
;; </tuner-dsp>
;; <set-dsp>
(dsp:set! dsp)
;; </set-dsp>
;; </setup-tuner>
;; <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>
(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.
-
DONE 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>
;; <setup-tuner>
;; <load-instruments>
(sys:load "libs/core/instruments.xtm")
;; </load-instruments>
;; <define-sine>
(bind-val omega SAMPLE (/ STWOPI SRf))
(bind-func sine
(lambda (time:i64 freq:SAMPLE)
(sin (* omega freq (convert time)))))
;; </define-sine>
;; <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>
(bind-func tuner_fx
(lambda ()
;; here put fx init
(lambda (in:SAMPLE time:i64 chan:i64 dat:SAMPLE*)
in)))
;; </tuner-fx>
;; <make-tuner>
(make-instrument tuner tuner)
;; </make-tuner>
;; <tuner-dsp>
(bind-func dsp:DSP
(lambda (in time chan dat)
(tuner in time chan dat)))
;; </tuner-dsp>
;; <set-dsp>
(dsp:set! dsp)
;; </set-dsp>
;; </setup-tuner>
;; <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>
(play-chord (now) tuner (pleasant-chord 60) 22050)
;; (play-chord (now) tuner (unpleasant-chord 60) 22050)
;; </xtm/04-harmony-chord.xtm>
;; <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>
;; <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>
;; <start-midi-listener>
(scheme-midi-listener (*metro* 'get-beat 4) 1/24))
;; </start-midi-listener>
;; </xtm/05-midi-chord-button.xtm>
;; <xtm/06-midi-chord-keyboard.xtm>
;; <xtm/04-harmony-chord.xtm>
;; <setup-tuner>
;; <load-instruments>
(sys:load "libs/core/instruments.xtm")
;; </load-instruments>
;; <define-sine>
(bind-val omega SAMPLE (/ STWOPI SRf))
(bind-func sine
(lambda (time:i64 freq:SAMPLE)
(sin (* omega freq (convert time)))))
;; </define-sine>
;; <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>
(bind-func tuner_fx
(lambda ()
;; here put fx init
(lambda (in:SAMPLE time:i64 chan:i64 dat:SAMPLE*)
in)))
;; </tuner-fx>
;; <make-tuner>
(make-instrument tuner tuner)
;; </make-tuner>
;; <tuner-dsp>
(bind-func dsp:DSP
(lambda (in time chan dat)
(tuner in time chan dat)))
;; </tuner-dsp>
;; <set-dsp>
(dsp:set! dsp)
;; </set-dsp>
;; </setup-tuner>
;; <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>
(play-chord (now) tuner (pleasant-chord 60) 22050)
;; (play-chord (now) tuner (unpleasant-chord 60) 22050)
;; </xtm/04-harmony-chord.xtm>
;; <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>
;; <start-midi-listener>
(scheme-midi-listener (*metro* 'get-beat 4) 1/24))
;; </start-midi-listener>
;; </xtm/06-midi-chord-keyboard.xtm>
3.1.3 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>
(sys:load "libs/core/instruments.xtm")
;; </load-instruments>
;; <define-sine>
(bind-val omega SAMPLE (/ STWOPI SRf))
(bind-func sine
(lambda (time:i64 freq:SAMPLE)
(sin (* omega freq (convert time)))))
;; </define-sine>
;; <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>
;; <fm-tuner-fx>
(bind-func fm_tuner_fx
(lambda ()
(lambda (in:SAMPLE time:i64 chan:i64 dat:SAMPLE*)
in)))
;; </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>
(dsp:set! dsp)
;; </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>
(sys:load "libs/core/instruments.xtm")
;; </load-instruments>
;; <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>
;; <fm-tuner-fx>
(bind-func fm_tuner_fx
(lambda ()
(lambda (in:SAMPLE time:i64 chan:i64 dat:SAMPLE*)
in)))
;; </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>
(dsp:set! dsp)
;; </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>
3.2 Keys & Scales
3.2.1 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>
;; <load-instruments>
(sys:load "libs/core/instruments.xtm")
;; </load-instruments>
;; <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>
;; <tuner-fx>
(bind-func tuner_fx
(lambda ()
;; here put fx init
(lambda (in:SAMPLE time:i64 chan:i64 dat:SAMPLE*)
in)))
;; </tuner-fx>
;; <make-tuner>
(make-instrument tuner tuner)
;; </make-tuner>
;; <tuner-dsp>
(bind-func dsp:DSP
(lambda (in time chan dat)
(tuner in time chan dat)))
;; </tuner-dsp>
;; <set-dsp>
(dsp:set! dsp)
;; </set-dsp>
;; </setup-tuner-osc>
;; <make-pentatonic-scale>
;; <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>
(define make-pentatonic-scale
(lambda (start-note)
(map frq2midi (make-pentatonic-scale-freq (midi2frq start-note)))))
;; </make-pentatonic-scale>
;; <play-pentatonic-scale>
;; <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>
(play-chord (now) tuner (make-pentatonic-scale 60) *second*)
;; </play-pentatonic-scale>
;;
;; </xtm/09-pentatonic-scale.xtm>