Skip to content

paulguy/crustymidi

Repository files navigation

                         __  , __           ___  |_
                       ,'  ` |'  ` |    | ,'   ' |  |    |
                       |     |     |    |  `--.  |  |    |
                       `.__. |     `.__/| .___.' \_ `.__/|
                                                     .__.'
                   ____________     ___   ____________     ___
                  |            `,  |   | |            `,  |   |
                  |  ,--,  ,--,  | |   | '----------,   | |   |
                  |  |  |  |  |  | |   | ,---,      |   | |   |
                  |  |  |  |  |  | |   | |   |      |   | |   |
                  |  |  |  |  |  | |   | |   |      |   | |   |
                  |  |  |  |  |  | |   | |   '------'   | |   |
                  |__|  |__|  |__| |___| |____________.'  |___|                  

                  #############################################
                  #############################################::
                    :::::::::::::::::::::::::::::::::::::::::::::

                         MIDI event scripting for JACK

 _
/ \ ----------------
|/| [-] CONTENTS [-]
\_/ ----------------

1. BUILDING
2. RUNNING
3. SCRIPT FILES
4. CRUSTYVM SCRIPT REFERENCE
5. CRUSTYVM VARIABLE REFERENCES
6. CRUSTYMIDI CALLBACKS


/|  ----------------
 |  [-] BUILDING [-]
_|_ ----------------

Just run 'make' in the directory.  It needs nothing but JACK headers and
development libraries.

 _
' | ---------------
 /  [-] RUNNING [-]
/__ ---------------

./crustymidi [-Dvariable=value] <script file>

-D is a means of passing in substring replacements.  Any string "variable"
appearing within a word or quoted string will be replaced by "value".  Mostly to
be used for passing in optional parameters.

If a filename begins with a -, you can end a line with -- then the next argument
will be taken as a filename.

 _
' | --------------------
-<  [-] SCRIPT FILES [-]
._| --------------------

Scripts must contain an 'init' and 'event' procedure.  'init' is called once
after the script is loaded.  All services are available at this point and timers
and events can be written at this point.  'event' is called once per MIDI input
event.  A script may consume or change its internal state based on the event and
never emit another event or can emit theoretically any number of additional
events or timers per input event.

By default a script will have one input and one output port, named 'in' and
'out', respectively.  Ports may be named and additional input and output ports
may be defined by starting the script with the string ';crustymidi ' then
following up with 'in:<name>' and 'out:<name>' statements, all on the same line.
Both sets of input and output ports start numbered from 0 and increment with
each statement.

See example .cvm files.

midi.inc can be included for some useful constants, but it's still very
incomplete.


 /| ---------------------------------
|_| [-] CRUSTYVM SCRIPT REFERENCE [-]
  | ---------------------------------
    Before anything is done, a pass is made to find all the tokens in the
program.  Quoted strings act as a single token.  Comments are thrown out at
this stage.  Comments begin with a ; and include everything following.  Quoted
strings may contain ;s though.  At this point, only one statement is meaningful:

include <filename>
    Read in the file <filename> in the root directory of the script and start
parsing tokens in from this file.  Care is taken that files above the script
directory should be inaccessible to be included in to a script for safety
reasons, but don't rely on it for security.  Make reasonably sure like any
other program or script that you trust where it came from.

Preprocesor Statements
    The first step to a program running (after it's tokenized) is to run
through and interpret the preprocessor statements, which are responsible for
defining preprocessor-time variables, and rewriting statements based on those.
Any time a single argument is needed to have spaces, it must be enclosed in
quotes, otherwise it'll be interpreted as multiple arguments.  Quoted strings
support the following escape sequences:
\r  carriage return
\n  new line
\<carriage return>  ignore a carriage return in the string.  A following
new line is also ignored
\<new line>  ignore a new line in the string.  A following carriage return is
also ignored
\\  a literal \ (backslash)
\xNN - a two digit hex number representing a byte literal
\"  a literal " (quote)

macro <name> [arguments ...]
    Start a macro definition.  From here, lines will be passed over until
until and endmacro statement with the same name is reached, at which time
normal interpretation will resume.  The arguments are a list of symbols which
when the macro is evaluated, will be replaced by whatever values were passed
in when invoked.

endmacro <name>
    End the named macro definition.  Normal interpretation will resume.

if <number> <name> [arguments ...]
    If <number> is non-zero, start copying the named macro with the listed
arguments.  <number> may also be an expression which is to be replaced by an
expression or a macro argument, or it may also be a variable passed in on the
command line with -D, in this case, it'll always evaluate to true, even if the
variable is specified to be 0.

expr <variable> <expression>
    Evaluate an expression down to a numerical value and assign it to
<variable>.  At that point, any time <variable> appears in the program, it'll
be replaced by the value which the expression evaluated to.  An expression can
only accept integer values and all operations are done as integers and the
result is an integer.  Expressions are arithmetic statements and support the
following operators:
*  multiplication
/  division
%  modulo
+  addition
-  subtraction
<<  binary shift left
>>  binary shift right
<  logical less than
<=  logical less than or equal
>  logical greater than
>=  logical greater than or equal
==  logical equals
!=  logical not equals
&  bitwise AND
!&  bitwise NAND
|  bitwise OR
!|  bitwise NOR
^  bitwise XOR
!^ bitwise XNOR
Parentheses are also supported for grouping, otherwise it follows the
precedence followed is similar to that of C arithmetic parsing precedence.

<macroname> <arguments ...>
    Start evaluaing (copying) from macro and continue until the matched endmacro
is reached.  Replacing any argument values with the arguments passed in.  All
arguments specified must be provided.

Symbol Definition Statements
    Following the preprocessing stage, the resulting code is scanned to find
symbol definitions:  procedures, global (static) variables and procedure
(local) variables.  Before this begins though, any callback variables defined
by the VM will be added in as globals.

proc <name> [arguments ...]
    Defines the start of procedure <name> and specifies which arguments, if
any, it accepts.  Those arguments become local variables which reference the
memory of the variables passed in, that is, any modifications to them in the
procedure will persist when the procedure returns.

ret
    Return from procedure.  Marks the end of a procedure.

static <name> [N | ints <N | "N ..."> | floats <N | "N ..."> | string "..."]
    Define a global (static) variable <name>.  If a single number is provided,
it will act as a single integer initializer.  If a type is specified, a single
value may be provided to create an array of that size, otherwise, multiple
arguments may be specified in quotes, separated by spaces or tabs, to create
an array of the size of values given and initialized with those values.  The
exception is string, which can only be initialized with a single argument,
quoted or not.  Values are initialized once, on VM start.  These may be
specified anywhere, procedure or not.

local <name> [N | ints <N | "N ..."> | floats <N | "N ..."> | string "..."]
    Define a procedure (local) variable <name>.  The initializer is specified
in the same way as global variables.  These values are initialized on each
call to a procedure.  These can only be defined inside procedures.  Procedure
variables must have unique names to global variables.

stack
    Not a real instruction, just indicate to accumulate stack.

label
    Define a label within a procedure to jump to.  Label names are scoped
locally to procedures.

binclude <name> <chars | ints | floats> <filename> [start] [length]
    Read in <filename> to be the initializer for a global variable <name>.  The
type must be defined as chars (a string), ints (integer array) or floats
(double array).  A start byte and length byte may be specified to include only
a particular range of the file.  As many items of the size of type which fit
within the file or provided length will be read in and used as the array
initializer.  Like include, some care is taken to prevent arbitrary files
above the script's directory from being read in.  The same warning applies.

Program Instructions
    The final stage is parsing instructions and generating bytecode.  The
language supports a fairly limited set of instructions, but ones which vaguely
represent the selection of instructions one may have available on a more
primitive platform, just with some added feature and artistic license for the
sake of simplciity and safety, but bugs can certainly still come up.

move <destionation>[:<index>] <source>[:<index>]
    Simply move a value from source to destination.  Source may be a variable,
an integer immediate or a callback with the index passed to it, which will
return a value.  Values will be converted to the type destination is before
being stored.  If destination is a callback itself, it will be passed a
reference to the source at the index provided, assuming it's not also a
callback or an immediate, in which case only the single value ia passed in.
All indexes are range checked to avoid hidden out of bounds accesses, in which
case the program is terminated.  A read or write callback indicating an error
will also terminate execution.  An index of 0 is implied if it's excluded.

add <destination>[:<index>] <source>[:<index>]
    Add <destionation> at <index> and <source at <index> then store the value
in <destination>.  Source may be a callback in this case but destination
cannot be, even if a callback may otherwise be readable and writeable.  If
either destination or source is a float value, the operation will be done as
if they are floats, but will be converted to the type of the destination,
truncated to the range/precision of the type.  All other arithmetic operations
follow similar rules.

sub <destination>[:<index>] <source>[:<index>]
    Subtract.

mul <destination>[:<index>] <source>[:<index>]
    Multiply.

div <destination>[:<index>] <source>[:<index>]
    Divide.

mod <destination>[:<index>] <source>[:<index>]
    Modulo (remainder).

and <destination>[:<index>] <source>[:<index>]
    Bitwise AND.  Bitwise instructions follow similar access rules as
arithmetic expressions, but they are restricted to operating on integer or
string types.  A character of a string is converted to a 32 bit integer and
padded out with 0s in its most significant bits.

or <destination>[:<index>] <source>[:<index>]
    Bitwise OR.

xor <destination>[:<index>] <source>[:<index>]
    Bitwise XOR.

shr <destination>[:<index>] <source>[:<index>]
    Bitwise shift right.  <source> may be a float, but the value will of course
be converted to an integer.
 
shl <destination>[:<index>] <source>[:<index>]
    Bitwise shift left.

cmp <destination>[:<index>] <source>[:<index>]
    Compare (subtract) <destination> at <index> and <source> at <index>, but
don't store it, simply hold on to the result for use with conditional jumps.
Neither value necessarily needs to be writable, that is, either side or both
sides can be an immediate or a read-only callback.  In reality, any value
which would be written or passed on to a destination is stored as the result
from any of the above operations, for use on a following conditional jump, but
it is only 1 value, and it is always replaced on one of these operations,
regardless.

jump <label>
    Jump to a label.

jumpn <label>
    Jump to a label if result is not zero.  From a cmp operation, this
indicates that the two values were different.
(cmp a b -> a != b)

jumpz <label>
    Jump to a label if the result is zero.  From a cmp operation, this
indicates that the two values are equal.
(cmp a b -> a == b)

jumpl <label>
    jump to a label if the result is less than zero/negative.  From a cmp
operation, this indicates that the left value is less than the right value.
(cmp a b -> a < b)

jumpg <label>
    Jump to a label if the result is greater than zero/positive.  From a cmp
operation, this indicates that the left value is greater than the right value.
(cmp a b -> a > b)

call <procedure> <arguments ...>
    Call a procedure.  All arguments indicated by the procedure must be
provided and are passed in as reference to the procedure and may be changed
once the procedure returns.


 __
|   ------------------------------------
`-, [-] CRUSTYVM VARIABLE REFERENCES [-]
._/ ------------------------------------

In any place where a variable may be referenced or used, the format will always
be the same:

<name>[:[<index>]]

Name must be provided, optionally followed by a colon ':', then optionally by an
index.  Just the colon will return the length of the array in variable <name>,
where a colon followed by the index will fetch the value at the index, or in the
case of a procedure call, pass in a reference starting from that index.
Negative indexes aren't allowed anywhere.  Out of range accesses will result in
a compile or runtime error.

 __
/   ----------------------------
|`\ [-] CRUSTYMIDI CALLBACKS [-]
\_/ ----------------------------

Script procedure callbacks (all must be defined)
  init
    Called once on script initialization.

  event
    Called on every incoming MIDI event.

Read callbacks
  length
    Get the length in bytes of the incoming event.

  data:n
    Read data from event at index n.  Out of bounds access will return a
    callback error.

  port
    Defined port which the event came in on.

  time
    Sample on which the event came in on, depends on the JACK sample rate.  JACK
    provides event times as 64 bit values; this just returns the low 32 bits, so
    this value may wrap as the script runs.

  rate
    Current JACK rate, may change from event to event.  On rate change, any
    events in-flight will have automatically been scaled to occur at the
    intended time.

Write callbacks
  length
    Set the event length which you'd like to write.  Ignored when recommitting
    the input event.  Initialized to 0.  Data buffer growth is initialized to 0
    on length changes.

  data:n
    Write data in to event at index n.  n must be within bounds defined by the
    value written in to length.

  port
    Output port which event should be written to, initialized to 0.

  time
    Time in samples after input event time (or 0 on init) that the event should
    be output to JACK.  A time of 0 means to emit the event at the same time as
    the event incoming.  Initialized to 0.

  commit
    Commit an event.  Multiple output events may be emitted per input event. A
    zero written to commit means to just re-emit the same event at the same
    time, however a different output port may be specified.

  timer
    Cause a timer event to be emitted by some samples in the future.  The 
    generated event will be of length 1 and and be command 0xF8, chosen for
    being time-related and JACK shouldn't be emitting these events.