At it’s core, Betablocker provides a simple virtual machine for playing with music which is ‘unstoppable’ (any combination of instructions can be executed). It started off originally as a Fluxus scheme program, then got rewritten in C++ for the Nintendo DS, and has now been ported to Supercollider. I’ve built yet another Linux commandline version, and glued it to the Java Genetic Algorithms Package via Clojure.
This means I can continue with the genetic programming experiments and try evolving programs using a real genetic algorithm framework, using the betablocker instruction set. This is mainly just for kicks, but it’s also partly to “grow” new strategies for livecoding betablocker and researching instruction sets more generally.
The first test was to try and find a program to just play note 99. I would write this in betablocker like this (using a text representation of the icon interface for convenience):
pshl 99 ; push the literal value 99 on to the stack note ; play the top of the stack as a note
First we need a fitness function to score the results from the programs which are generated:
(defn fitness [notes] (- 255 (Math/abs (- (first notes) 99))))
It takes a list of the notes played by a candidate program, and measures how far away the first element is from 99 – the bigger the number, the fitter the individual program is. I gave it 8 instructions to work with and a population of 500 individuals – after just a couple of generations, this chromosome/program popped up with a top fitness of 255 – meaning it was successfully playing note 99:
99 46 213 89 7 142 23 168
Which when disassembled looks something like this:
99 ; <-- address 0 nop nop nop pshi 142 ; pushes contents of address at location 142 (which is 0) note ; play the note nop
This program has grown taking advantage of the fact that the memory is initialised to zero, so if it dereferences a large address like 142 it will be zero and load the contents of the first byte - 99. It's this kind of wacked out approach genetic algorithms are great at finding/abusing.
The next problem, something a bit more general - create a sequence of notes which are all different to one another. The fitness function:
(defn list-contains? [l i] (cond (empty? l) false (= (first l) i) true :else (recur (rest l) i))) (defn fitness [a] (count (reduce (fn [r v] (if (not (list-contains? r v)) (cons v r) r)) '() a)))
This just counts the number of unique elements in the list - again, the higher the better. This one took longer, but eventually came up with this:
139 246 1 23 12 22 23 12 20 95 22 3
Which when disassembled:
nop nop org start: note ; play a note inc ; increment top stack byte dup ; duplicate top stack byte note ; play a note (consumes 1 stack byte) inc ; increment again pip 95 ; increment address 95 (junk - doesn't have any effect) dup ; duplicate again jmp start ; return to the start
Which plays a rising sequence of notes, with none repeated, so the highest fitness achievable:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
This is already doing much better than the genetic programming I was mucking about with before - indeed there seems to be some literature on evolving java bytecode, but with betablocker there is an advantage that you don't need to enforce the correctness of the programs before they can be run.