The new modules are mostly concerned with analyzing a midi stream in various ways, extracting chords, key signature, and beat. The Metronome may be the most generally useful module: as well as providing a fixed-rate tick if desired, it will follow tempo changes in a midifile, and even — to some extent — attempt to follow a live performance. There have been a number of fixes and updates to existing modules. mostly minor, except that MidiMon has been considerably improved as to what and how it reports, and MidiFile has been largely rewritten to avoid the BeOS MidiKit entirely.
All modules are now freely useable, with no licensing requirements.
The new "BLAST OUT" mode causes an entire sequence to be sent at once with no timing. so you can record a sequence one event at a time (by manually activating Program or Controller elements, for example) and then have it transmitted at high speed.
A "Play on Load" mode has been added to the playback selector popup. With this you can have a sequence — perhaps one set to 'blast out' — transmitted automatically simply by loading it from a file. This will happen when the diagram is initially loaded too (if the filename was set when the configuration was saved), so you can have arbitrarily complex initialization.
(Remember that Program and Controller elements in a diagram always send an event with their initial settings when the diagram is completely loaded, but this isn't always satisfactory if, for instance, other elements (e.g. channel setting) might modify the event. The order of setup is the order the elements appear in the file — by default the order in which they were placed in the diagram. You can always hand-edit the file if you really need to, but that'd be a pity. Putting initialization directly into a sequence is much less likely to trip you up.)
The "Load", "Save", and "Rewind" buttons are now disabled more sensibly when their use would be inappropriate.
Note that (as stated in the main Replay docs) System-exclusive events are recorded and played back, as are Weaver-special "Switch" and "Director" packets. However, unlike the Midifile module, RePlay does not handle the Tempo-Change and Meta-events that can appear in a standard midifile.
See the main RePlay manual.
As with RePlay, the "Load", "Save", and "Rewind" buttons are now disabled when they should be.
Documented in MidiFile.html.
This latest version has added a Control Panel with a RESET button. Documented in MIDI.html
There is one input — the data stream. The first of the two outputs is the resulting text packets, the other is a pass-through of the incoming data.
For each MIDI event, the output line begins with the event time in milliseconds. (This is the time recorded in the packet, not the time it actually arrives at the monitor.) For any Channel Event, the next field on the line is the channel (1..16), followed by the type of the event. The rest of the information on the line depends on this type.
For a 'Controller', you get the controller number with its associated General Midi function if any, and then the event's value. On a 'Program Change', you get its number and GM name. 'Note On' and 'Note Off' show the note number (with the key name) and its 'velocity'; it distinguishes true Note-Off events from the zero-velocity Note-Ons often used. Other events similarly are displayed in appropriate form. System Messages obviously have no channel.
Tempo Change (Meta)Events generated by a MidiFile module are shown as well. These have the usual timestamp, followed by the tempo setting, first in "Beats Per Minute" and then the actual interval-per-quarter-note value supplied (shown as 'seconds' for convenience, rather than the microseconds used internally).
MidiMon also handles System Exclusive and (Midifile) 'Meta Event' sequences, but gives only a brief sketch. The first item is again the time, but for System Exclusive this is followed by 'SysEx size' and the (decimal) number of bytes in the sequence. The first and last bytes only of the sequence are then listed, in hexadecimal and then decimal. Obviously this is not much use for any detailed understanding of the sequence — it is really only intended to indicate that such a sequence has occurred. Meta Events (other than Tempo Change) are shown similarly, but only the hexadecimal type code and the size are displayed. No decoding of the various types is done.
As "Chord" descriptions are now available from the KeyTrack module, MidiMon shows these as well. These are always associated with at least one actual MIDI event, so the output line for the chord doesn't have a timestamp. It just shows the name of the chord and its root note. [Currently, MidiMon will fail to show the chord if, for example, all the actual notes have been split away!]
MidiMon also recognizes 'Director' events (see e.g.
GamePort). These do not have a timestamp, so
are simply labelled 'DIRECTOR', followed by either 'Switch' or 'Control n',
depending on the type. The bits set in a Switch message are shown, and for
a Control message it presents the 'Index' and 'Value'. The 'n' following
'Control' indicates the subtype: '1' is another form of (single-bit) Switch,
'2' is a 'Parameter' event (generated by GamePort and used by D_PitchBend etc.
(No other types are yet defined.)
The string of bytes represents exactly what would be sent to any hardware expecting the message, except that the actual MIDI sequence always begins with a header byte with value 0xF0 and terminates with 0xF7. These two bytes are represented by opening and closing square brackets in this module's output. Lines are kept to a maximum of ten values, so a message can extend over several lines.
[Apparently there is some hardware that requires SysEx messages to be sent in pieces, and the midifile protocol has provision for this. However, BeOS doesn't, so the MusicWeaver cannot handle such messages. (I've never seen any.)]
In addition to hex values, though, you can use decimal ones (0-127), by prefixing an 'n' to the first. The radix will remain decimal until a prefixed 'x' is encountered. The header and terminator bytes that are part of a transmitted SysEx sequence (actually 0xF0 and 0xF7) should not be included — they are represented here by the enclosing brackets. (Within MusicWeaver, by the way, a SysEx sequence is not processed as a set of MIDI events, but rather as a single packet of its own type.)
As a specific example, the Universal System Exclusive Message "Turn GM On" (bytes actually sent: F0, 7E, 7F, 09, 01, F7) can be generated by passing in the text: "[7E 7F 9 1]".
A tick rate is set by typing in a value, rather than by slider, to allow sufficient precision. At the top of the Control Panel are the two alternative text entry fields ("Beats/Min" and "Interval"). Entering a value in one automatically adjusts the other. You must terminate the entry with the Enter key — nothing gets updated until you do.
Below the text fields are the obvious RUN and STOP buttons, which are always enabled. As in RePlay, they are "touch buttons" that are activated when pressed, not released, to give more convenient precise control. The RUN button goes blue while the element is ticking. If you click RUN while the element is already running, the ticks will get resynchronized to the button-press (and the rate will get reset — see below).
Next down is a popup selector that sets the mode. In the default Fixed mode, only the rate setting and the RUN/STOP buttons have any effect. (The sliders below remain enabled so you can preset them, but have no effect.) When set to Trig Start, the metronome will be started by the first Note-On MIDI event to arrive at its input connector; other types of event are ignored. Again, the sliders are not effective. The other two modes, Entrain and Unforced, attempt to adjust to an incoming beat, and use the sliders to do so, as explained below.
A midifile may contain Tempo Change events (there is usually at least one at the very beginning), and MidiFile passes these out. In both Trig Start and Entrain modes, but not in the other two, the Metronome will adjust itself — and the displayed rate values — when one arrives at its input. This can result in excellent sync with the beat of the file, even if the piece varies in tempo. The only difference between Entrain and Unforced is that the latter is not affected by Tempo Change messages, only responding to incoming notes.
The way that the Metronome attempts to follow the "beat" of an incoming MIDI stream should definitely be classed as experimental and first-effort. I'm hoping to find ways to improve it — particularly its tendency to speed up indefinitely when the incoming tempo slows down! The algorithm is really pretty dim. It simply compares each tick with the closest Note-On event to arrive, and adjusts its parameters according to the difference. It pays no attention to other factors, such as the strength of the note. The sliders control how big a discrepancy it can respond to, and how much it tweaks either the next interval or the overall rate.
The Capture Range slider sets how far before or after the scheduled beat a note can be to alter anything. Its value is in terms of percentage of the current beat interval (actual, not as set in the entry fields). If it is small, only notes that occur very close to the expected beat will be effective. If it is full 50%, every note will get captured! In all cases, the actual offset of the note from the beat is the base for the effect of the other two sliders.
The Tick Sensitivity slider determines by what percentage of the above offset the next interval will be changed. It only controls the tick that follows; it has no long-term effect.
The Rate Sensitivity slider causes changes to the overall tick-rate itself, again by a percentage of the offset. It does not affect the entered rate values; instead the current computed rate is shown (as msec) to the right of the popup selector, above the sliders. The entered rate is restored every time the element is restarted, either by directly clicking the RUN button (which also restarts the ticking immediately) or by clicking STOP and letting the next Note-On resynchronize things.
The Ticks output of the element is a "metronome click" (note 33) on rhythm channel 10. (Each tick is a Note-On immediately followed by a Note-Off, which should always work for a "click".) If you want something else for your beat, use a Transpose and/or other elements to get what suits you. The Sync input to the element is also passed through to an output as usual.
The slider in its panel sets the maximum delay it will apply, in the range 0-1000msec. Be careful, though: it randomly delays all MIDI events that pass through it; if the set range is too large, you might for instance get a note-off occurring before its note-on!
By default only the first event that reaches the upper or lower limiting value will be passed on; succeeding ones will be blocked until the value moves away from the limit again. A checkbox in its control panel can change this to allow it to pass all arriving events (still adjusted according to the sliders, of course).
The element has a Control Panel that shows the keys currently seen as active (compressed into one octave). There is a RESET item in the Options menu that can be used to clear the state if, for example, a piece has been interrupted while notes are sounding. It is also cleared when a midi SysReset — or 'All-Sounds-Off' or 'All-Notes-Off' — message is received.
It has a large Control Panel where all the counts are displayed. The Options menu has a RESET item to clear these, plus an option to Output Data as a text stream to be sent presumably to a WriteFile element for storage, and a Discard Subchords toggle. If this last is set, any 'intermediate' chord that is immediately superseded by one formed by adding notes will not be counted. If not set, every chord change is counted separately.
In the largest section of the panel, each row represents a possible root note. The Notes column is a count of total notes, whether part of a chord or not — each Note-On is counted. The columns following this are counts of the various chord types. At the right of a row for the relevant root, a short specification of the latest chord will be displayed. Some of these types are ambiguous or don't actually have a defined root (augmented, diminished and other miscellaneous chords); these will just get displayed in an arbitrary row.
The Major column counts major triads, inverted or not. These are all unambiguous three-note chords, so the count is true.
The Minor column similarly counts all minor triads.
The Seventh column totals the (four-note) (Minor) Seventh chords with that root. Remember that the common 'seventh' chord that is most used in pieces of a given key is the one that is rooted on the 'dominant' (fifth note) of that scale. So for instance when playing in the key of C, most sevenths are likely to show up in the G row (G B D F).
The Maj-x column counts up all the various other chords that are generally considered 'major'. Specifically, it totals 'Third', 'Fifth', and 'Major Seventh' (two-note) chords. These are all ambiguous. It only looks for fifths, not fourths, so it will for instance always call a 'C fourth' an 'F fifth'. Similarly if the interval is actually a 'minor sixth', it will record it as a (major) 'third', and a 'minor second' is a 'major seventh'.
The Min-x column similarly counts some miscellaneous chords that I've somewhat cavalierly labelled 'minor': two-note 'Minor Third', 'Diminished Fifth', and 'Minor Seventh', and the three-note 'Diminished Triad'. Only the first and third of these are truly minor, but it seemed reasonable to count them all together. Again, they're all two-note except the last, so there's lots of ambiguity.
There are two types of chord for which there is no sensible distinguishable root, or rather every one of their notes could be the root, so these get separate counters in sections of the lower panel. A four-note Diminished chord (in all inversions) has only three variants, so each of these is totalled in the right hand box. A three-note Augmented one has four, tracked in the left hand one.
Any other three or four note chord, aside from Diminished or Augmented (below), gets dumped into the "Other Misc:" box at bottom right without regard to any 'root'.
Its panel has two "best guess" bars at the top. The topmost (yellow) shows its current, continuously revised Bayesian estimate — reflecting the highest numerical value seen in the table below (typically it can fluctuate quite wildly). The lower green one indicates the most favoured key over the whole piece, and how consistently it has been maintained.
The table entries in the lower panel show the complete set of estimated probabilities for all possible major and (natural) minor keys. The highest current probability is flagged with a marker, as well as being echoed visually in the top bargraph.
The left Options menu has the usual RESET item, plus three toggle options. Setting Slow Update causes the display to be updated by timer rather than at every packet; this may help when the CPU is heavily loaded, as a lot of formatted text is involved. Use Notes and Use Chords, both set by default, are probably self-evident: setting the first means that the notes in the original midi stream contribute to the computation, the second uses the chord information from KeyTrack. Sometimes it may be interesting to unset one or the other to see what changes. (Unsetting both is not very helpful — unless you want to freeze the results.)
The File menu lets you change the likelihood tables. Read Likelihoods opens a file panel where you can select a likelihood file to load. Two examples are provided in the subfolder 'Tables': 'KeySig_LHood_0' is the default set initially provided within the module, 'KeySig_LH_Slow' is a set (unchanged from the earlier 'KeySig_probSlow') that might cause it to be less ready to change its mind on each note. Restore Defaults puts back the original set.
A likelihood file is just a text file of numerical values, in a very specific order. Formatting of the numbers is unimportant (except for clarity) as long as they are separated by some white space. They can be floating point, but as they are likelihoods rather than probabilities, it is usually clearer to use integers as large as needed. Other characters (like comments!) are not allowed.
The values are in a series of groups of 24 (two subgroups of 12). Each group represents how it evaluates a given type of event — single notes, major triads, minor triads, diminished triads, and dominant sevenths. The first 12 are likelihoods for major scales, the second set are for minor. All scales are treated equivalently, so only one set of likelihoods is needed. The position of a value within a subgroup represents the offset of the note or chord-root from the tonic of the scale being evaluated; the value is the relative likelihood of that event occurring given that the scale is actually that. Likelihoods are only compared within a group (of 24); scaling is otherwise arbitrary. [Apologies for this convoluted exposition, but Bayesian logic can be hard to explain!]
As an example, take the first group from 'KeySig_LHood_0', which evaluates how likely
single notes are to occur in a piece of a given key. The first subgroup of 12,
handling major scales, is:
555 1 172 55 143 333 1 444 11 111 55 233
Taking the C major scale as an example, we can see that the first value
(at 'offset 0') shows that we think the note 'C' has a strong likelihood
of appearing in that key. The next value, for 'C#', has a very low (but not
zero) likelihood. 'D' (offset 2) has a good likelihood, and so do
'E' (offset 4), 'F' (offset 5), 'G' (offset 7), and so on.
If we are considering, say, the G major scale, the thinking is the same,
but now offset 0 is 'G', offset 1 is 'G#' etc.
The next line, for the minor scale, is almost the same, except that
the relevant offsets of the minor scale now take the place of the major ones:
555 1 172 143 55 333 1 444 11 81 233 55
The other groups in the file, for chords, are filled similarly (remembering that their scaling is independent of other groups). Thus the first entry for major triads shows the (high) likelihood of a C-major chord appearing in a piece in C. C#-major is not likely to show up, so it gets a low value. Most minor chords are less likely to show up in a major piece, so they get lower values. The minor-triad group is just about the reverse, with minor chords being likely, major ones not. The remaining groups, for diminished and seventh chords, are very empty, because only specific instances are likely to occur in a given scale.
The above likelihoods are completely empirical, arrived at by looking at counts from typical pieces, and deciding what seemed reasonable. One point to note: a "zero" likelihood really means that that event is impossible, so if it actually occurs bad things (like divide by zero) would ensue! The algorithm actually always replaces zero likelihoods and probabilities with very small non-zero values. Zero is therefore safe to use as a shorthand for "highly unlikely". A file need not actually contain all the groups; if later ones are omitted, they will be filled with the default values.
Its Control Panel has a slider that sets the size of the window in milliseconds (up to 200). It delays each event by that window time, unless it occurs within that window after a previous event, in which case its (delayed) timestamp is set to match that.
A menu is provided to set the Key Signature, which will be displayed as usual to the right of the clef signs. Accidentals on the displayed notes will adjust appropriately. A second menu lets you select whether both treble and bass clefs will be shown (separated only by middle-C) or just one or the other. Notes outside the clefs will always be shown with suitable ledger lines. A 'RESET' option is available if notes are left displayed after an interruption.
You will find the modules described in this section in the 'Experimental' folder
of the Weaver (you can move them in with the others if you like).
They work fine for me, but things are still somewhat under construction here still.
Once assigned a device, and calibrated, a GamePort element emits Director events that report joystick and button activity. It must be revealed here that there are actually two kinds of Director message (currently). The Switcher, TriggerSwitch, and so on only respond to and generate the 'Switch' type of Director, but the GamePort adds another type — 'Parameter' events. See the Switcher for details on the Switch type of event. Parameter events have two components: an 'Index' and a 'Value'. The Index distinguishes between different parameters, and its Value (meaning I hope obvious) is a signed integer, potentially up to 16 bits long, though limited to 7 bits for MusicWeaver usage.
When a button on the stick (or whatever) is pressed, the element sends a Switch event with a mark corresponding to the button number (1, 2,...,8). When the button is released, it send another event with a mark '8+button'. E.g. when button 2 is pressed, Mark '2' is sent; when it is released, Mark '10' goes out.
For stick movement, it sends (streams of) Parameter events. In these, the Index represents the axis, and the Value is stick position scaled by default to 0..127. (This range corresponds to the usual MIDI value range.) If the stick has been calibrated properly, 'zero-stick' should be represented by Value="64," and excursions should cover pretty much the available range. [Of course, never expect a stick to be truly linear, but it's probably good enough for, um... Rock and Roll...]
The two types of messages will have different destinations and function in your configuration. Button activity events will usually go to Switcher or Stepper elements to reroute data flows. Parameter events will have to go to either D_PitchBend or D_Controller elements, because as yet no other modules know about them. These two modules convert Parameter event streams to appropriate MIDI streams, so that you can emulate 'Bend Wheels' and so on with a joystick. [Note: if you want to see the Director events being generated in a clearer form, you can view them via MidiMon as described above (Monitoring MIDI events).]
The Control Panel (x86 version) just has two popup selectors. The top one is where you select the GamePort device you desire as input. The lower one is where you would change the scaling of the output — but don't! The current modules all expect the standard MIDI 0..127 range.
As mentioned, the device that a GamePort element is connected to must be
calibrated before use. On an x86 machine, you use the System Preferences
'Joysticks' app for this (see the BeOS User's Guide). This doesn't seem to
handle the older interface on a BeBox, so the version of the GamePort module
for this has calibration from its own Control Panel. There you will see a
button marked "Calibrate", and below it a Status/Instruction line that initially
says "NO CONTROLLER". Select the device you want to connect to from the popup
menu above first, and the text will change to "UNCALIBRATED". Click on the
"Calibrate" button: the label will change to "Next" and the text below it
to "Zero Stick". At this point ensure that your controller is properly
centred, and click "Next". (You can instead use the "Fire" (or "main") button
on your controller to move to the next phase, but this is often less convenient
as it's hard to avoid moving the stick at the same time.) The text will change
to "Set Maxima". Move your stick or whatever to all extremes of all axes in
sequence (in any order, but make sure you cover them all), then click "Next"
or press the stick button again. This completes calibration; the text line
goes blank and the button returns to "Calibrate". (You can repeat the
sequence at any time if you wish.) When you save your configuration, the
calibration settings will be saved with the other element parameters, so there
should be no need to repeat the process next time you run it.
It will accept Parameter events (see GamePort above) with a selected Index, and values in the range 0..127, to generate Controller events with the same value. The MIDI Controller number is fixed by the selection in the Control Panel (as with the original Controller module).
Its Control Panel has two extra controls at top right. A popup menu lets you set the Index of the Parameter events you wish it to respond to. The "Invert" checkbox lets you reverse the sense of the arriving values — depending on which direction of stick movement you want to correspond to which control changes. ('Stick-left' may output high Parameter values for example, but you want 'Stick-right' to give high controller values.)
Note that Parameter events don't change the state of the element in any way.
The panel slider for instance is not affected by arriving events, and can
be used independently to drive controller values
(see Controller).
The Control Panel has the same "Index" and "Invert" controls as D_Controller
above. Again, select the Index of the parameter events you want to generate
PitchBend, and select "Invert" if they would otherwise have the wrong sense.
The approach is to generate a text line from each MIDI event that can be passed
to elements from the StreamWeaver
suite that handle text. For convenience, most of the StreamWeaver modules are
now included with the MusicWeaver.
These include a WriteFile
module that lets you for instance record the text from MidiMon
(or a StreamView) for further work.
All data is transferred between elements as packets, each containing one or more items. Items are tagged as to their type, and elements only respond to those that concern them. The basic items relevant to most MusicWeaver modules are the TimeStamp (of an event) and the MIDI events themselves. The most common packet will just comprise those two items: one TimeStamp and one MIDI. Usually the MIDI item will hold just one event, but if more than one of these has the same event-time they will be put in the same item if possible. There is never more than one of either the above items in a packet.
Some midifile data is not really in the form of an "event" (although if it can get transmitted out a Midi Port it becomes a conforming stream of bytes like other MIDI data), so it is handled within the MusicWeaver in separate packets. Each of these still has a timestamp, but instead of the MIDI item, it will include either a System Exclusive, a Tempo Change, or a MetaEvent item. (We will leave aside here the packets that are not specific to music at all, such as those concerned with routing; these can have an entirely different content.)
Of these, only System Exclusive messages ever get passed in or out of any external MIDI connection. (They are usually hardware-specific control information.) They can be sent and received by any of the I/O elements, and are recorded and played back by MidiFile and RePlay. MidiMon briefly displays them. They're ignored by everything else. [It is possible for some types of System Exclusive, for particular hardware, to be divided into several time-separated sections in a midifile. This seems to be rare, and currently the MusicWeaver doesn't handle these (the MidiKit doesn't either...) — it assumes each System Exclusive message is bundled as a unit.]
A Tempo Change packet is generated by a MidiFile element in response to a corresponding entry (actually a specific 'MetaEvent') in the file being played. MidiFile also of course responds appropriately while recording. MidiMon displays them. The Metronome responds to these packets too, and can adjust its beat correspondingly. It will optionally generate them as well whenever it adjusts its own tempo. RePlay does not record, play back, or otherwise take notice of the packet.
A MetaEvent is non-MIDI data contained in a midifile, such as a key-signature or text annotation. A packet holding this data will be generated by a MidiFile when it encounters it in a file, and is similarly recorded. (Tempo Change is a metaevent, but it generates a Tempo Change rather than a MetaEvent packet.) MidiMon briefly indicates them, but doesn't show any content. No other modules are affected.
Certain of the new modules add or use a couple of other items beyond those in the basic midi packet. One is Chord Info, which describes the chord that is apparently sounding, with some limitations. It is added to each midi packet passing through by the KeyTrack element, and is used by Analyze and KeySig elements. The chord is represented in two ways, first by transposing all the sounding notes (irrespective of channel) into a single octave, and also by an attempted identification of the actual chord and its root note. The latter is often ambiguous (is it a fourth or a fifth?), and compression into an octave loses information too, but the preprocessing is useful for the downstream elements. A MidiMon will display the apparent root note and the chord (with the above limitations). The other new item tacked on to each packet by KeyTrack is Keys Down; it represents all the notes (out of the complete set of 128) that are apparently sounding — i.e. there are more Note-On events than Note-Offs. Channel is ignored, but a count is kept, so the same note on separate channels is monitored correctly. This item is currently only used by the new Notator module.
The MidiFile module can now handle just about all the information contained in a midifile. The only real exception is that it turns multi-track ('Format 1') files into single track ones internally, so that although it plays them perfectly well you will only get a 'Format 0' file if you save it back to disc. all the information is still there — it just is all merged into one track, which is less convenient for working on in a sequencer.
All events — including System Exclusive, Tempo changes, and other MetaEvents — are sent on a MidiFile's output path when playing, and recorded when received. System Exclusive messages are transmitted by the I/O modules. Most other modules, except MidiMon and Metronome, don't know about any of these other types of packet.
A RePlay element records and plays back System Exclusive sequences, but not the other extra types known to the MidiFile. It does, however, record the 'tags' added to MIDI events by other MusicWeaver modules, so these can be reproduced during a later session. It also preserves any 'Switch' or other Control packets it receives on its Recording input.
Both MidiFile and Replay can be triggered by events on their input, but RePlay can delay its playback a desired amount if the trigger must precede the synchronization point.
The playback speed of a RePlay sequence can be changed, whereas this is not possible with a MidiFile. (It could be done, but would be derailed by the tempo change events within the file, so it is not included.)
There are other minor differences, such as MidiFile not yet having "Touch Buttons", but the above are the important ones. One intended use of RePlay is to hold short sequences that can be triggered into action (singly or repeatedly) during a performance.
The older module MidiPlay has now been rewritten and updated to match MidiFile a bit more. It only plays midifiles (no recording) but has the advantage of being able to queue pieces for playback.
I am no longer considering the program to be ShareWare. If you feel the urge, though (:-)), I'll happily accept contributions:
Peter Goodeve 3012 Deakin Street #D Berkeley, California 94705, USA
You are permitted to distribute the Weaver program, the current modules, and documentation in the form of the supplied ZIP archive without charge for non-commercial purposes, provided that the whole package is kept intact. For commercial use or distribution (other than charging a reasonable media and copying fee) please contact the author.
Developers interested in writing their own modules for MIDI or any other purpose, please contact the author.
Pete Goodeve Berkeley, California e-mail: pete@jwgibbs.cchem.Berkeley.EDU pete.goodeve@computer.orgFeedback welcomed.