MIDI Assembler

MIDI Assembler is a combination virtual machine / assembly(-like) language / MIDI sequencer running on the Java Virtual Machine. If you have Java installed, you should be able to run MIDI Assembler. MIDI Assembler gives you a lot of power to play with music compared to an ordinary MIDI sequencer such as Cubase - however, it is less suited to writing completely structured/composed music. It is particularly suited to "procedural music".

Running MIDI Assembler

First you must have Java 7 installed. If you do not have Java 7 installed, please download it and install it from java.com. On unix you have more options available to you. The openjdk should work if you wish to avoid the Oracle implementation. Once you have Java installed, you may be able to double-click the midiasm.jar file to run it. If that does nothing, or opens the file in, e.g., winrar, you may need to run it from the command line:

 java -jar midiasm.jar

Using the MIDI Assembler

The MIDI assembler GUI should be fairly self-explanatory. Code is entered into the textbox, and then you press "play" to run it. You can select your MIDI device with the box in the upper right - MIDI Assembler should support all MIDI output devices you have installed. There are also buttons to bring up load/save dialogs - but MIDI Assembler just uses text files, so you can also copy from the textbox into a text editor/website/email program/whatever if you wish to save/share files that way.

Writing MIDI Assembler code

MIDI Assembler code is written in a simple language similar to assembly language. Familiarity with assembly language is helpful but not required. Knowledge of the General MIDI notes, instruments, and CC numbers is required, but an internet search for "midi notes", "midi ccs", etc, should find what you need.

Commands in MIDI Assembler are known as opcodes, after assembly language tradition. An opcode can take 0, 1 or 2 arguments. A very simple example of a MIDI Assembler song could be:

note:60

which would play the MIDI note 60 - a "C" note. The more complex opcodes can be used to create more complex programs, but none of the opcodes aside from "note", "sleep" and "off" are necessary to write a song in MIDI Assembler.

MIDI Assembler also has comments:

// to add a comment (the rest of the line will be "commented out" - ignored)

And some other pieces of syntax which will be discussed later:

define(a){note:60} // defines a macro
data(0){6 8 9 3 4 5} // initializes memory
note:#20 // #20 is equivalent to the value at the memory address "20"

MIDI Assembler opcodes

note

Plays a midi note on the current channel:

note:60

to play the note 60, a "c".

chan

Sets the current channel:

chan:9
note:38

to play the note 38 on channel 9 (channel 9 is the drum channel).

velo

Sets the velocity for all future notes, until the next velo command:

velo:1
note:60

to play note 60 as quietly as possible (0 would be silent).

inst

Sets the instrument on the current channel:

inst:9
note:60

to play note 60 on instrument 9, the "glockenspiel" sound.

off

Turns notes off. There are three variations of "off":

off

to turn off all notes on the current channel.

off:3

to turn off all notes on channel 3.

off:3:60

to turn off note 60 on channel 3.

sleep

Causes MIDI Assembler to wait for a given number of milliseconds. This opcode is very important - if you do not use "sleep", every event in your song will happen simultaneously!

sleep:300

to sleep for 300 milliseconds.

cc

Sends a MIDI control change message on the current channel:

chan:9
cc:7:0

sets CC 7 to 0 on channel 9. CC 7 is defined as channel volume, so setting it to 0 will have the effect of muting a channel.

set

Stores a value at a memory location:

set:10:999

will store the value "999" at memory location 10. Values from memory locations can be used as numbers with the # syntax:

set:10:60
note:#10

will store the value "60" in the memory location 10, and then play the value from the memory location 10 as a note - playing the note "c".

inc

Adds a value to a memory location:

set:10:60
inc:10:1

will set memory location 10 to 60, and then increase memory location 10 by 1 - the value at memory location 10 is now 61.

dec

Subtracts a value from a memory location:

set:10:60
dec:10:1

will set memory location 10 to 60, and then deccrease memory location 10 by 1 - the value at memory location 10 is now 59.

push

Pushes a value to the stack. A stack is a data structure in which the first thing put on is the last thing to come off (and the last thing to be put on is the first thing to come off). See "pop" for example.

pop

Pops a value from the stack into a memory location (taking it off the stack):

push:999
pop:1

will put the value "999" onto the stack, and then take it off and put it into memory location 1.

push:999
push:111
pop:1
pop:2

will put the value "999" onto the stack, and then the value "111". Then it will take the top value from the stack, "111", and put it into the memory location 1. Finally it will take the new top value, "999", from the stack and put it into memory location 2. At the end, memory location 1 will hold the value "111" and memory location 2 will hold the value "999".

peek

"peek" is similar to "pop", but the value isn't removed from the stack:

push:999
push:111
peek:1
peek:2
pop:3
pop:4

will cause the value "111" to be in memory locations 1, 2 and 3, and the value "999" to be in memory location 4.

eq

Compares two values for equality:

eq:20:40

compares 20 to 40 - obviously they are not equal. This opcode is mainly useful with the # syntax:

eq:20:#0

compares 20 to the value in memory location 0 - they may or may not be equal. "eq" evaluates to true when the two parameters are equal, and false otherwise. See "bt" and "bf", the two opcodes which make use of the results of "eq", "gteq" and "lt".

gteq

true if parameter 1 is greater than or equal to parameter 2:

gteq:20:10

will be true, as 20 is greater than 10.

gteq:20:20

will also be true, as 20 is equal to 10. See "bt" and "bf".

lt

true if parameter 1 is less than parameter 2:

lt:10:20

will be true, as 10 is less than 20.

lt:10:10

will be false, as 10 is not less than 10.

bt

Branches ("jumps") to a labelled position (see "label") if the last comparison ("eq", "gteq" or "lt") was true:

eq:10:#0
bt:gotothisplace

would jump to the label "gotothisplace" if the value at memory location 0 was equal to 10.

bf

Branches ("jumps") to a labelled position (see "label") if the last comparison ("eq", "gteq" or "lt") was false:

eq:10:#0
bf:gotothisplace

would jump to the label "gotothisplace" if the value at memory location 0 was not equal to 10.

jump

Jumps to a labelled position (see "label") regardless of the result of the last comparison:

jump:gotothisplace

would jump to the label "gotothisplace".

label

Labels a position in the code:

label:start
note:60
sleep:1000
jump:start

would play the note 60, sleep for one second, and then go back to the labelled position "start" and do it again, and then again, and so on (only stopping when the "stop" button was clicked).

jsr

Jumps to a labelled position in the code, remembering the current position so it can be returned to with "rts". See "rts" for example. Positions are stored on a stack so "jsr" can be nested.

rts

Returns to the remembered position in the code after a "jsr":

label:start
jsr:playnote
jump:start

label:playnote
note:60
sleep:1000
rts

Would play the note 60 and then sleep for one second over and over again - the program jumps to the label "playnote", and when it reaches an "rts" it jumps back to where it was before (and then jumps back to the start).

data(){} syntax

The "data(){}" syntax allows areas of memory to be initialized before the program is run:

data(12){60 67 72}
note:#12
note:#13
note:#14

would play the notes 60, 67 and 72. Multiple data(){} directives can be used in one program, but the order in which they are carried out is undefined:

data(0){60 67}
data(1){68}

could have unpredictable behaviour - memory location 1 could potentially be either 67 or 68.

define(){} syntax

The "define(){}" syntax allows macros (shorthand) to be created:

define(c){note:60}
c c

will be transformed by the pre-processor to:

note:60 note:60

The macros do not have to be fully formed:

define(n){note} 
n:60

or even:

define(n){note:} 
n60

are both valid.

When multiple macros overlap, the pre-processor reads from right to left, choosing the longest valid word it can find:

define(o){note:60}
define(f){note:72}
off

will have the expected behaviour of "off", since that was the longest valid word it could find. This doesn't prevent more than one word being compounded:

define(o){note:60}
offo

will result in "offnote:60", which doesn't work, since "offnote" is meaningless, but since the pre-processor respects whitespace when substituting macros:

define(o){ note:60}

offo

will result in "off note:60", which works.

Macros cannot be nested, and they cannot be used inside data(){} directives:

define(n){note}
define(c){n:60}

and

define(c){60}
data(0){c c c}

are both invalid.