Cornucopia: A MIDI Mapper

Cornucopia (the file name is crn) is a program for mapping MIDI notes to other MIDI notes and/or MIDI note sequences. The program performs mapping according to an ordinary Ascii text file. In the file, a number of maps can be specified; and each map gives a separate note mapping. At run-time, you can switch between maps by typing a key from ``a'' to ``z''. Only MIDI note messages can be mapped.

Simple Example

Here is an example of a file containing some simple maps.
map a chan 1
    init
	prog 2 10
	prog 3 11
    any
	nt 2 +12 +0 +0
	nt 3 -12 100 +0
    pitch A3
	go b
	redo

map b chan 1 init prog 2 12 prog 3 13 any dly 100 nt 2 +0 +0 +0 dly 100 nt 3 +0 +0 +0 pitch af3 go a redo

map c init score drums.gio 0 0 100

This example contains 3 maps, labeled ``a'', ``b'', and ``c''. The first two maps apply to incoming notes on channel 1. When map ``a'' is installed by typing ``a'', the commands in the init section are run in sequence. A program change is sent to set channel 2 to program 10 and channel 3 to program 11. Then, every note (with one exception, described below) is mapped according to the any section. There are two commands here: send a note on channel 2 with the pitch transposed by 12, but with the same velocity and duration, and send a note on channel 3 with the pitch transposed by -12, a fixed velocity of 100, and the same duration.

The exception to this rule is that the pitch A3 has a specific set of commands listed under pitch A3. The first command, go b, says to install map ``b''. The second command, redo, says to process the event again. This will produce a different effect since map ``b'' will now apply.

Map ``b'' also has an initialization section to change programs on voices 2 and 3. The any section says that the general response to a note (on channel 1) is to delay for 100 centiseconds (1 second), play a note on channel 2 with the same pitch, velocity, and duration, delay another 100 centiseconds, and play a note on channel 3 with the same pitch, velocity, and duration.

An exception in this map is pitch af3. When an A-flat below middle C is received, map ``a'' is installed and the incoming note is reprocessed (redo) according to the new map.

Map ``c'' is not channel-specific, so installing this map does not override either map ``a'' or map ``b''. Map ``c'' has only an initialization section, so it has no effect other than to play a score (drums.gio) at the normal transposition, velocity, and rate. Thus, when the ``c'' key is typed, the score will play.

Some Details

A map can be channel-specific, applying to only one channel, or global, applying to any MIDI note. Within a map, a response can be pitch-specific, invoked in response to a certain pitch, or non-pitch-specific, applied when any note is played. When a note is played, a mapping is looked for in the corresponding channel-specific map. If none is found, a mapping is looked for in the global map. Within a map, pitch-specific mappings have priority over non-pitch-specific mappings. Only the first mapping found is applied to a note, although one of the actions of a mapping can be to search for and invoke another mapping as if the current one did not exist. The global vs. channel-specific property is associated with each map so one map cannot serve as both.

In the current implementation, only three octaves of pitch-specific mappings are maintained per map, so all lookups are performed mod 36.

Syntax of Map files

The syntax of the map file is more restrictive and more complex than Adagio, so a formal language syntax will be presented. Each production will be accompanied by text describing the semantics. This formal section will be followed by examples. In the syntax description,
italic means a nonterminal,
UPPERCASE means a terminal,
[ ] means 0 or 1
{ } means 0 or more
| means either
Even though terminals are in upper case, Cornucopia is case insensitive like Adagio, so files may by typed in lower case.

mapfile ::= { map }
A mapfile is a sequence of map specifications. Since each map is labeled by the key (``a'' to ``z'') that makes it active, there can be a maximum of 26 maps in a file.

map ::= MAP char [ CHAN chan ]
[ INIT { command } ]
[ ANY { command } ]
{ PITCH pitch { command } }
{ CLASS pitch { command } }
Each map consists of the keyword MAP followed by a letter A through Z. If the map is channel-specific, the keyword CHAN introduces the MIDI channel to which the map applies. The INIT keyword introduces a sequence of commands to be performed when the map is installed. For example, you can send out program changes in response to selecting a map. The ANY keyword introduces the non-pitch-specific mapping and is followed by a sequence of commands (described below). Pitch-specific commands can be specified in two ways: the PITCH keyword introduces a mapping for a particular pitch, and the CLASS keyword introduces a mapping for a pitch class (the same mapping applies in all octaves). Recall that only 3 octaves are supported. The pitch specification is described later. Here are the possibilities for command:

command ::= PROG chan value |
NT chan key vel dur |
DLY number |
CTRL chan number value |
CALL char |
RET |
GO char |
PASS |
NOOP |
REDO |
SCORE filename transposition loudness rate [ GATED [CYCLE]]
Commands are of various forms. PROG, NT, and CTRL send MIDI program, note-on, and control change commands. The note command (NT) also specifies a duration until the note-off message. The chan, key, vel, and dur fields can be transformations of the input note, and these fields are described in detail below. Other commands provide various forms of extended control. The DLY command delays processing of the next command by some number of centiseconds (1/100 second). Without a DLY, commands are all executed at once. The CALL command invokes another map and and remembers the current one on a stack. There is one stack for each channel and a stack for global maps. Calling a map that does not have the same channel (or that is not also global) may give strange results. The RET command restores the previous channel-specific or global map. The GO command changes the current map to a different one. The specified map should have the same channel or also be global. The PASS command causes the event mapping to continue as if the current handler did not exist. In other words, the search continues from channel-specific pitch-specific, to channel-specific non-pitch-specific, to global pitch-specific, to global non-pitch-specific. The REDO command restarts event handling. This would make sense only after a CALL or GO has changed the current map. The SCORE command plays an Adagio score or MIDI file with an optional transposition and velocity (loudness) shift. The rate is in percent, e.g. 200 means twice as fast as the nominal tempo. The optional GATED parameter says to stop playing the score when the triggering note ends. If GATED is selected, then the option CYCLE may also be added, indicating that the score should repeat until the key is released.

char ::= a..z
chan ::= 1..16

A char indicating a map is any letter from a to z. A chan is a number from 1 to 16. The key for an NT command is specified in one of several ways:

key ::= pitch | offset | value

pitch ::= value | pitchletter [ sharpflat ] [ number ]
pitchletter ::= A..G
sharpflat := { S } | { F }

offset ::= + number | - number

value ::= 0..127

A key is either an absolute pitch, an offset, or a numerical value. The pitch form is similar to Adagio, with S or F for sharps and flats and an octave number (C4B4 is the octave starting at middle C). Pitches and velocities can also be given as an offset from the pitch or velocity of the incoming note, e.g. +12 to transpose the pitch up one octave.

vel ::= value | offset
Velocity is given as either a value, a number from 0 to 127, or an offset, a signed number from 127 to +127. Note that +10 means ``louder by 10'', while 10 means ``velocity 10 (very soft)''.

dur ::= number | + number | - number | * number
Durations can be specified in three ways. First, a number can be specified, giving centiseconds. Second, an offset can be specified with a leading +: +20 means 20 centiseconds longer than the mapped note. For example, if an incoming note with duration 200 maps to a note with a duration offset of +30, the generated note will last 200+30 = 230 centiseconds. Third, an offset can be specified with a leading ``-'': -20 means 20 centiseconds shorter than the mapped note. As you might imagine, if an incoming note maps immediately to some note with a duration offset specified as -30, Cornucopia cannot predict the future and turn off the generated note 30 centiseconds before the incoming note-off. However, if the generated note is started after a delay (see the DLY command), then a negative offset makes sense. If the offset would generate a negative duration, a duration of zero (note-on followed immediately by note-off) is used. Fourth, a scale factor can be given, preceded by a ``*'': *130 means make the duration 130% the duration of the mapped note. A duration of less than 100% can be given, but again this only makes sense after a delay.

transposition ::= -63..[+]63 | P - number
loudness ::= -128..[+]127 | V + number | V - number
rate ::= number
Scores may be transposed, shifted by a loudness offset, and played faster or slower. The transposition is a signed number (the ``+'' is optional), or the transposition may be relative to the incoming mapped note. This latter case is indicated by a leading ``P-''. For example, ``P-60'' means ``take the pitch of the incoming note, subtract 60, and use the result to transpose the score''. In this example, middle C (pitch 60) would not transpose at all, D above middle C would transpose up 2 semitones, etc.

Loudness is similar to transposition; it may be expressed as a signed number or the loudness offset may be relative to the incoming note's velocity. For example, ``V-100'' means ``take the incoming velocity, subtract 100, and use the result to offset loudnesses (velocities) of the score.

The rate can be used to scale the tempo of the score. Note that this is not a tempo in beats per minute but a percentage scale factor, where 100 is the ``normal'' tempo. A rate of 200 means twice the normal tempo.

Examples

These are examples of mapper commands. Imagine that you want to send a volume control (number 7) to channel 6 at some point. This could be triggered by playing a particular pitch:
map v
    pitch D6
	CTRL 6 7 70
or simply by typing the letter to install the map:
map v
    init
	CTRL 6 7 70
In this case, installing the map will replace the previous (in this case global) map. One way to avoid this is to dedicate one channel to some maps that have only init sections. For example, if you have no input on channel 15, you might want to say:
map v chan 15
    init
	CTRL 6 7 70
That way, any global map will be undisturbed when you type ``v'' to send the control change.

Here are some examples of nt commands:

map a chan 1
    any
	nt 2 +12 +0 +0
	nt 2 -6 -10 +30
	nt 2 C4  90 300
	nt 2 D4  +0 *120
	dly 50
	nt 2 60  +0 -50
	nt 2 E4 100 *50
The first nt transposes pitch up by 12 and keeps the same velocity and duration as the incoming note. The second nt transposes down by 6 semitones, decreases the velocity by 10, and adds 30 centiseconds to the duration. The third nt plays a C4 pitch at velocity 90 with a duration of 300 centiseconds. The fourth plays a D4 with the same velocity as the incoming note and with the duration 20 percent longer than that of the incoming note. After the dly command, which inserts a delay of 50 centiseconds (1/2 second), there is a note with pitch 60 (middle C) whose duration is decreased by 50 centiseconds. Since this note started 50 centiseconds later than the incoming note, this one and the incoming one will end at the same time. The last note is an E4 with velocity 100. The duration will be half (50 percent) of the duration of the incoming note. A little math will show that if the incoming note is longer than one second, its duration will not be known in time to turn off the E4 as specified. In these cases, the E4 will be held until the incoming note ends. At that time, Cornucopia will compute that the E4 should have already ended and it is turned off immediately.

The next example illustrates the use of the pass command. The any section plays a chord on channel 2 to harmonize each incoming note. In addition, any E-flat starts playing a score. The score is repeated until the E-flat is released. The pass command ``passes'' control to the next less specific handler for the incoming note. From most specific to least specific, handlers are:

pitch-specific handler in a channel-specific map
any section of a channel-specific map
pitch-specific handler of a global map
any section of a global map
The pass command simply re-handles the note at the next level in the list. In this case, from a pitch-specific handler to the any section of a channel-specific map. Here is the example:
map a chan 1
    any
	nt 2 +5 +0 +0
	nt 2 +7 +0 +0
	nt 2 +10 +0 +0
	nt 2 +12 +0 +0
    class Ef
	score "trill.gio" 0 0 100 GATED CYCLE
	pass

Applications

The Cornucopia program can serve a number of functions. Here are a few:
Previous Section | Next Section | Table of Contents | Index | Title Page