From ircam!corton!loria!loria.crin.fr!eker Fri Aug 21 15:29:34 MET DST 1992 Article: 5837 of comp.music Xref: ircam comp.music:5837 rec.music.synth:33200 Path: ircam!corton!loria!loria.crin.fr!eker From: eker@loria.crin.fr (Steven Eker) Newsgroups: comp.music,rec.music.synth Subject: Sequencer algorithm article (long) Message-ID: <450@muller.loria.fr> Date: 19 Aug 92 19:41:39 GMT Sender: news@news.loria.fr Followup-To: comp.music Organization: CRIN (CNRS) Nancy - INRIA Lorraine Lines: 375 I'm posting this article which I wrote for a MIDI mag a while ago in the hope that MIDI programmers will find it useful (I saw a posting a week ago asking about how to write a sequencer). Appologies in advance to music reseach people: This article is (very) "dumbed down" because it is written for an audience who were (for eg) treated to a lengthy explaination of binary and hex notation in a previous issue. A couple of questions: (1) How is the "one point" vs "two point" representation of on/off pairs handled in top pro/reseach music software? Do you use binary heaps or something more advanced (eg f-heaps)? (2) I've cross posted to comp.music & rec.music.synth (followups to comp.music) but considering the recent discussion, which group is most appropriate for MIDI programming articles? I know this is MIDI and hence an anathema to some (most?) music research people but rec.music.synth strikes me as user/musician orientated rather than programmer orinetated. Steven MIDI I/O ALGORITHMS =================== by Steven Eker (eker@loria.fr) To a musician, a note is an single object possessing (among other characteristics) a starting point and a length. However to MIDI equipment a note consists of two distinct events - a Note On message and a Note Off message. It is easy to see why MIDI had to be implemented this way: suppose you press a key on a synth, then the synth must immediately transmit a message telling any connected modules to play a note, however there is no way the synth could transmit the length of the note as it hasn't been determined until you release the key. This dichotomy causes a real headache for sequencer designers: how best to store sequences of notes inside the sequencer? The most obvious answer is the so called "two point" format where the notes are stored as a sequence of Note On and Off events, in the order they arrive from the synth. This format is fine when you simply want to store and replay sequences unmodified and is the format chosen for the MIDI file standard - presumably to make life easy for simple-minded sequencers. The problems start when you want to edit sequences. Whenever you move insert or delete a Note On event you have to move insert or delete the corresponding Note Off event otherwise you will end up with the notes which change length unexpectedly or even worse hang indefinitely. Of course a decent sequencer should keep track of the Note On/Off pairs and present the user with notes as single entities. With the two point format a Note On event and its matching Note Off event may be separated by arbitrarily many other events and manipulating such a sequences consistently requires a lot of searching for matching Note On/Off pairs. It is much simpler if sequences of notes are stored in the "one point" format where each Note On/Off pair is combined as a single item in a sequence. This format also saves memory. The big drawback of the one point format is that it has to be converted to and from the two point format for input and output. This has to be done in real time and any lengthly computations will reduce the accuracy of the sequencer. In his book "MIDI sequencing in C", Jim Conger suggests storing MIDI sequences in two point format and then converting them to and from one point format as required for note level editing by the user. This solution has a number of disadvantages. Firstly block move/copy routines (and any other routines that have to manipulate sequences in the two point format) are complicated by the the need to avoid separating Note On and Note Off events. Secondly there is now way the user can edit events during playback and hear the changes as they are being made. Thirdly there is no saving in memory as is possible with the one point format. The method I will explain in this article allows you to have the best of all worlds and even solves two other MIDI I/O headaches at the same time: (1) When a sequencer is replaying a large number of tracks simultaneously the naive approach of sequentially scanning the active tracks to see which should be next to output an event can cause timing inaccuracies and (if interrupts are blocked during this process) cause incomming MIDI messages to be lost. (2) When the user clicks on the stop button, all currently playing notes must be terminated. One naive method is to send an All Notes Off message on each MIDI channel. This method fails when the receiving MIDI device does not implement All Notes Off. Another naive method is to send Note Off messages for every pitch on every channel. Apart from taking a little over 1.3 seconds (with running status) this method can also cause problems if the sequencer output is being merged with another MIDI source. A rather clever idea used by one well known sequencer vendor is to send a single Active Sensing message causing the receiving device to reset itself when no further Active Sensing messages arrive. Unfortunately even this method causes problems with certain synths. The right thing to do is to transmit Note Off messages for just those notes that are actually playing, but this means keeping track of which these are which can be very time consuming (affecting sequencer accuracy) if done a naive way. Overview of the solution ======================== The basic idea is to keep the sequence(s) in one point format and use fast algorithms to convert to and from two point format in real time for input and output. Actually converting incomming data from two point format into one point format is fairly straight-forward. We simply keep a 128x16 entry table. There is one entry for every pitch/channel combination. When a Note On message arrives it is stored in a record in the appropriate sequence and a pointer to this record is stored in the corresponding table entry. When a Note Off message arrives we look at the appropriate entry in the table. If it is null the we have a stray Note Off event and we discard it. Otherwise we have a pointer to the record containing the matching Note On event and we added the extra information, release time and possibly release velocity to this record and place a null in the table entry. There are a couple of subtle points to cope with missing Note Off messages. If when we come to write a pointer in to the table we find that the entry is already filled by a non-null pointer then this pointer points to a record containing a Note On message which (for some reason) has lost its Note Off message. In this case we can use the current time as the release time and 64 as the default release velocity to replace the missing information. At the end of recording we check through the table looking for non-null entries. If we find any then these are pointers to records containing Note On messages without Note Off messages. This can easily happen if the user clicks on the stop button without releasing all the synth keys. Once again we can use the current time and the release velocity 64 to fill in the missing data. The reverse process, converting from one point format into two point format for playback not so simple. We need to use a computer science concept called a priority queue. Priority Queues =============== A priority queue is a collection of items ordered by some property - in our case time. There are several operations that may be performed on a priority queue - for our purposes the following four are sufficient. (1) Inspect the least item (2) Remove the least item (3) Insert a new item (4) Flush the queue - remove all items. Now for playback we keep two priority queues: The first contains the next note/event for each active track. The second contains all of the pending Note Off events. Now when we come to transmit an event we compare the time stamps of the least items in the two priority queues. There are two possibilities/steps: (1) If the time stamp on the least item in the note/event queue was the lesser (or the Note Off queue was empty) we output this event. We remove this item from the note/event queue and insert the next item on the same track into the note/event queue. Also if this item was a note we insert a corresponding Note Off event into the Note Off queue. (2) If the time stamp on the least item in the Note Off queue was the lesser (or equal) we output this Note Off event and remove this Note Off event from the Note Off queue. Actually this explanation is oversimplified since we also should take into account the sequencer clock but I'll come to that later. The reason for giving Note Off events precedence when the least items in each queue have the same time stamp is simply that MIDI devices have limited polyphony and this reduces the likelihood of the polyphony limit being exceeded and notes being arbitrarily cut off. When the user presses the stop button then the Note Off queue contains a Note Off event for every note playing note so we can simply flush this queue (i.e. remove and transmit all the Note Off events in any convenient order) to terminate all currently playing notes. Binary Heaps ============ Up to now I haven't said anything about how the priority queues are actually implemented. An obvious solution is to store them as arrays. There is a snag however: Typically the number of tracks in a sequencer that could be active will be rather large - say 64 and each will have an event in the note/event queue. Also the number of pending Note Offs that might have to be stored in the Note Off queue could be equal to the total polyphony of all the user's synths/modules. Now when we have to find the least item in a queue, in the worst case we might have to check all the items. This is not a good solution. Suppose we kept the array in sorted order - now getting at the least element is easy. However this time we might have to move all the items already in the queue whenever we insert a new item. A sorted linked list is also a poor implementation of a priority queue because we might have to check every item already in the queue to find out where to insert a new item. A better way to implement priority queues is a data structure called a binary heap. This is an array which is "half-sorted" in a very special way that allows both the removal of the least item and the insertion of new items to be performed efficiently. Binary heaps are explained in detail in any decent book on data structures so I will not go into technical details here. A particularly good account of binary heaps for the practising programmer is Chapter 12 of "Programming Pearls" by Jon Bentley. If sequences are stored in one point format, reading and writing them to and from MIDI files (which remember use the two point format) requires the same sort of conversion as that required for MIDI I/O and similar algorithms can be used. The situation for one point format to two point format conversion is somewhat simpler since only one track is written at a time so only a Note Off heap is required. Also speed is not so critical however a binary heap is still a good way to organise the conversion. To illustrate how easily binary heaps can be programmed Figure 1 contains the heap manipulation code (written in 'C') from the MIDI file save module of my own sequencer. ------------------------------------------------------------------------------- typedef struct heap_struct { long time; char byte1, byte2, byte3; } HEAP; static HEAP midi_heap[256]; static int heap_size; ins_heap(time, byte1, byte2, byte3) register long time; char byte1, byte2, byte3; { register int empty = ++heap_size, new; for(;;){ new = empty / 2; if(new == 0 || midi_heap[new].time <= time) break; midi_heap[empty] = midi_heap[new]; empty = new; } midi_heap[empty].time = time; midi_heap[empty].byte1 = byte1; midi_heap[empty].byte2 = byte2; midi_heap[empty].byte3 = byte3; } del_heap() { register int empty = 1, new; register long time = midi_heap[heap_size--].time; for(;;){ new = 2 * empty; if(new > heap_size) break; if(new < heap_size && midi_heap[new + 1].time < midi_heap[new].time) new++; if(midi_heap[new].time >= time) break; midi_heap[empty] = midi_heap[new]; empty = new; } midi_heap[empty] = midi_heap[heap_size + 1]; } Figure 1: Heap manipulation code ------------------------------------------------------------------------------- How the code is used is a follows. Initially the heap is empty and heap_size = 0. The least element (if the heap is not empty) in the heap is always in midi_heap[1]. (Yes - I know 'C' arrays start at 0 but here it is convenient to not use the first element.) Each event in the sequence being written to a MIDI file has its time stamp compared with that of the Note Off event in midi_heap[1] (if the heap is not empty). If the Note Off event in midi_heap[1] has the lesser time stamp it is written to the MIDI file and del_heap() is called to remove it from the heap and the comparison step is repeated with the new occupant of midi_heap[1] (assuming the heap is still not empty). Whenever the current event in the sequence has the lesser time stamp (or the heap is empty) this event is written to the MIDI file. Also if this event was a Note On event, ins_heap is called to put the corresponding Note Off event in the Note Off heap. At the end of the sequence the Note Off events remaining in the heap are written to the MIDI file and removed from the heap in the order least time stamp first. In this way the Note Off events are slotted into their correct places in the MIDI file. Subtle points ============= There are a number of subtle points and refinements when it comes to implementing the above ideas. First off, during playback we need to compare time stamps of events against the current sequencer clock to see if they are due to be output as well as against each other to determine the order. One way of organising this is that we first compare the least element of the Note Off heap against the sequencer clock - if it is less than or equal we take step (2) above. Otherwise we compare the least element of the note/event heap against the sequencer clock - if it is less than or equal we take step (1) above. Otherwise we know that the time stamps on all the next events in all the active tracks and all the pending Note Off events are greater than the current music time and nothing is due to be output until the sequencer clock is next incremented. Doing the tests this way around means that Note Off events can actually be transmitted in before other events with earlier time stamps - however this can only happen when there is a backlog of events due to be transmitted and prioritising Note Off messages under these circumstances may be a good thing. The alternative arrangement is to compare the time stamps of the least elements on the two heaps first and then compare the lesser of these against the sequencer clock. Notice that during playback the Note Off heap starts off as empty and is filled on the fly. To set up the note/event heap at the beginning of playback we simply call the heap insert item routine to insert the first event of each track (or more efficiently a pointer to the first event on each track) into the note/event heap. Also notice that testing the least item in a heap just means testing the item in the first location of the array used to store the heap. So no real work has to be done rearranging the heaps unless a MIDI message is actually transmitted and items have to be inserted and/or deleted from the heaps. Now in a professional sequencer the MIDI output code is interrupt driven and written in assembler both for accuracy and to allow it to run as a background task. Now even though the heap insert and delete item algorithms are very fast and can be coded in very tight assembler (the division and multiplication in the above code are done by shifting) it is possible for that for very large heaps they may not complete their task in the 320us between MIDI output bytes (or input bytes). Thus MIDI bandwidth could be wasted and accuracy impaired (and incomming MIDI data could be lost). A neat (but tricky) way of getting around this problem is allow the code that rearranges the heaps (running in an interrupt routine) to itself be interrupted by other interrupt routines to output and input MIDI bytes. In this way incomming MIDI data will not be lost. Also we have complete timing accuracy (up to sequencer clock resolution and MIDI bandwidth) as long as the heap rearrangement can be done in the 640us or 960us between output messages (Okay some messages can be transmitted in one byte using running status - but these are not Note On messages so we only have to rearrange one heap). Needless to say there has be be some kind of synchronisation between the various layers of interrupts and this is a tricky topic which I will not go into here. A nice refinement is to allow the user to activate and deactivate tracks while the sequencer is playing. This is done using slightly modified versions of the heap insert and delete item routines to insert(remove) the next event of the activated(deactivated) track in the note/event heap. Another refinement is to allow the user to edit (or block copy/move) sequences while they are being played. Both of these refinements require synchronisation between the main program code which is making the change and the interrupt routines that are modifying the heaps and accessing the track sequences. .