MIDIRecBuf
A storage and utility class for MIDI style data. Initially designed for recording note data from a MIDI keyboard, it also has some analysis methods that are useful for hand-coded or algorithmically-generated data.
MIDIRecBuf does not store controller data.
*new(name, notesArray, properties)
name: For easier access through MIDIBufManager and its GUI, you may name the buffer. Recommended to use a symbol rather than a string.
notesArray: An array containing SequenceNotes (or its subclasses). See SequenceNote.
properties: A user definable dictionary describing the MIDI data, to be used by client processes. The most common properties are a ModalSpec describing the key in which the note data should be interpreted, a symbol describing the purpose of the data to a melody player or chord arpeggiator process, or hints on how one of these processes should split the note data into smaller segments.
Some properties are used by MIDIRecBuf itself; the user may add any other properties as needed.
Reserved properties:
mode
tuning
factor
beatsPerBar
error
See the quantize methods.
deltaThresh
overlapThresh
allowShortNotes
See the parse method.
For hand coding data, a very useful array method is asNotes, which takes parallel arrays of frequencies, iterations, lengths and arguments (see for details), and converts them into a flat list of SequenceNotes. This is easier than writing [SequenceNote(), SequenceNote(), SequenceNote(), ...].
// a c-major scale; if you have chucklib, you can define
// ModalSpec(#[0, 2, 4, 5, 7, 9, 11], stepsPerOctave: 12, root: 0) => Mode(\cmaj)
// and client processes can understand the notes in terms of c major
// because the mode is specified as one of the properties
// 0.25, 0.3 and 0.5 are the dur, length and args values respectively
// because they are not arrays, they will apply to every note
b = MIDIRecBuf(\demo, [
#[60, 62, 64, 65, 67, 69, 71, 72],
0.25, 0.3, 0.5
].asNotes, (mode: \cmaj));
dumpSeq
List the notes in a flat format. If the buffer contains compound note objects (SeqChordNote and SequenceItem), they will not be shown (only the main note data will be listed).
b.dumpSeq;
MIDIRecBuf("demo")
0 : [ 60, 0.25, 0.3, 0.5 ]
1 : [ 62, 0.25, 0.3, 0.5 ]
2 : [ 64, 0.25, 0.3, 0.5 ]
3 : [ 65, 0.25, 0.3, 0.5 ]
4 : [ 67, 0.25, 0.3, 0.5 ]
5 : [ 69, 0.25, 0.3, 0.5 ]
6 : [ 71, 0.25, 0.3, 0.5 ]
7 : [ 72, 0.25, 0.3, 0.5 ]
asPattern
asStream
embedInStream(inval)
For streaming note data.
Data access methods
In the following methods, chord and grace notes in SeqChordNote and SequenceItem objects will be ignored.
midiNotes - get the note numbers from each note (i.e., [note0.freq, note1.freq, ...])
freqs - get the real frequencies from each note. The function to specify the tuning is obtained from properties.tuning, falling back to properties.mode.cpsFunc, falling back to midicps.
durs - get the dur values from each note.
lengths - get the length values from each note.
gates - get the gate values from each note.
args - get the args values from each note.
Collection methods
The following methods behave like collection methods.
at(i) - get the note at i
first, last - get the (first, last) note
add(note) - add the note object to the notes array; returns the same MIDIRecBuf object
includes(item1) - is the supplied note already in the array?
removeDups - remove duplicate freq values so that each note has a distinct freq; returns a new MIDIRecBuf
sort - returns a new MIDIRecBuf
++ - appends a MIDIRecBuf or an array of notes; returns a new MIDIRecBuf
reverse
scramble
copy
copyRange
size
asArray - returns the note array
Analysis methods
mapMode(mode) - convert modally-represented SequenceNotes into note indices
unmapMode(mode) - convert note indices into modal representation
quantize(factor, beatsPerBar)
Quantize the note deltas to the nearest factor beats. Note that the absolute onset times of each note are not considered, only the deltas between notes.
factor: The portion of the beat to which to round the deltas, e.g., 0.25 = sixteenth-note.
beatsPerBar: Used at the end of the buffer to make sure the total length of the buffer is an integer multiple of the bar length. If no value is supplied, the last note's duration will not be changed and there will be no guarantee of the sequence looping at a bar boundary.
If you do not supply the arguments in the method call, they will be looked up in the buffers properties dictionary (meaning that a buffer can store information about how it should be quantized).
flexQuantize(factor, clock, error)
flexQuantize attempts to handle the case of MIDI data that are internally consistent rhythmically, but which do not match the tempo of the clock that was used to record the data. That is, if you play on the keyboard slower or faster than the clock's tempo, you might get delta values that are approximately integer multiples of 0.2 or 0.3. It should be possible to round these values to the nearest integer division of the beat.
The algorithm uses baseRhythmicValue to estimate the greatest common divisor of the delta values, within a range of tolerance specified by the error argument. It then rounds this base value to the nearest factor, and adjusts the notes to match this ratio.
baseRhythmicValue is unfortunately not a terribly robust algorithm, and requires almost superhuman precision to succeed consistently. Thus it is quite likely, with real world data, that quantization will fail and return nil. The caller must be prepared to handle a nil return value.
clock: Should be the TempoClock used to record the data initially. If not specified, it defaults to TempoClock.default.
baseRhythmicValue(factor, clock, error)
As noted above, attempts to identify the lowest rhythmic value in the buffer. Returning the minimum duration is not sufficient, because in typical rhythmic music, deltas will cluster around integer multiples of a base value.
Eventually I will replace this method with a proper cluster analysis; for now, be aware that it gets pretty good results with well behaved data but fails easily.
parse(deltaThresh = 0.1, overlapThresh = 0.1, allowShortNotes = true)
Identifies grace notes and chord notes, and consolidates them into the appropriate compound note objects. (See [SeqChordNote] and [SequenceItem] for details.) The basic rules are:
Grace notes have a dur (delta) less than deltaThresh and at most a very short overlap with the next note (less than overlapThresh). The amount of overlap is defined as length - dur; if length > dur, the note sustains longer than the time to the next note and the notes overlap. The short overlap means that the notes will not be perceived as sustaining together; otherwise they would be perceived as chord notes.
Chord notes have a dur less than deltaThresh and a longer overlap. Overlap is calculated as a percentage relative to the main note of the chord; if the chord is only 0.25 beats long, the overlap may be physically very short but very significant perceptually. If overlapThresh is 0.1 a minimum 10 percent overlap is required.
The allowShortNotes argument determines how grace notes will be handled if they do not overlap with the main note (if they terminate before the next note sounds). If true, no overlap is needed (length may be less than dur); if false, the notes must at least touch.
deltaThresh, overlapThresh and allowShortNotes may be stored in the MIDIRecBuf properties to avoid supplying them on the parse call.
// b has some very short notes (0.03) duration
b = MIDIRecBuf(\parse, [
#[60, 63, 60, 65, 66, 65, 62, 63, 60],
#[0.4, 0.6, 0.4, 0.03, 0.57, 0.4, 0.03, 0.57, 1],
#[0.4, 0.6, 0.4, 0.03, 0.57, 0.4, 0.03, 0.57, 1],
0.5
].asNotes);
b = b.parse; // use default values, 0.1 and 0.1
// now the buffer has two SequenceItems, matching the two grace notes
b.notes;
[ a SequenceNote, a SequenceNote, a SequenceNote, a SequenceItem, a SequenceNote, a SequenceItem, a SequenceNote ]
convertToDeltas
If you are hand-coding your note data, you might want to specify times in terms of absolute points rather than note deltas. In that case, you can set the absoluteOnsets flag to true and call convertToDeltas to get a buffer that is suitable for streaming.
This call modifies the original buffer -- so it's critical that this method never be called twice on the same buffer. absoluteOnsets is intended to prevent this situation. Alternately, make sure that you copy the buffer first if you don't want to lose the the original, absolute timings.
You may also set the variable stopRecTime to determine the delta for the last note (which will be lastRecTime - onset time of the last note). If this variable is empty, the last note will have the same dur as the length stored in the note.
b = MIDIRecBuf(\absOnset, [
#[60, 60, 67, 67, 69, 69, 67, 65, 65, 64, 64, 62, 62, 60],
#[0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14],
#[1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2]
].asNotes);
b.absoluteOnsets_(true).convertToDeltas;
b.dumpSeq;
MIDIRecBuf("absOnset")
0 : [ 60, 1, 1, nil ]
1 : [ 60, 1, 1, nil ]
2 : [ 67, 1, 1, nil ]
3 : [ 67, 1, 1, nil ]
4 : [ 69, 1, 1, nil ]
5 : [ 69, 1, 1, nil ]
6 : [ 67, 2, 2, nil ]
7 : [ 65, 1, 1, nil ]
8 : [ 65, 1, 1, nil ]
9 : [ 64, 1, 1, nil ]
10 : [ 64, 1, 1, nil ]
11 : [ 62, 1, 1, nil ]
12 : [ 62, 1, 1, nil ]
13 : [ 60, 2, 2, nil ] // length == 2, dur == 2 also
// using stopRecTime for a longer last note
b = MIDIRecBuf(\absOnset, [
#[60, 60, 67, 67, 69, 69, 67, 65, 65, 64, 64, 62, 62, 60],
#[0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14],
#[1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2]
].asNotes);
b.absoluteOnsets_(true).stopRecTime_(20).convertToDeltas;
b.last;
[ 60, 6, 2, nil ] // now the last note has 4 extra beats