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".
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
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.
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"
Plays a midi note on the current channel:
note:60
to play the note 60, a "c".
Sets the current channel:
chan:9
note:38
to play the note 38 on channel 9 (channel 9 is the drum channel).
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).
Sets the instrument on the current channel:
inst:9
note:60
to play note 60 on instrument 9, the "glockenspiel" sound.
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.
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.
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.
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".
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.
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.
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.
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" 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.
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".
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".
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.
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.
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.
Jumps to a labelled position (see "label") regardless of the result of the last comparison:
jump:gotothisplace
would jump to the label "gotothisplace".
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).
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.
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).
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.
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.