Sunday, February 22, 2009

Midi (part 3) Tunescript

Here is another midi project done in my spare time. Tunescript is a musical toy, where you can enter a list of notes to create a song. Unlike other interfaces like this, though, tunescript supports a lot of features like multiple tracks and instruments, chords, percussion, accented notes, and even pitch bends. Also, I put much thought into the syntax.

For a lot of information, many examples, and to download, visit this page.

It is pretty fun to invent a domain specific language. Because the language doesn't (yet) have nested constructs, I don't need full parsing. I came up with a nice way to interpret the input. It works kind of like a finite state machine that is receiving a stream of instructions. For example, the character 'b' can mean either flat, as in 'Ab', or the note 'b', but there is no ambiguity, because the symbol 'A' causes a transition to a state that can accept the 'b'. Adding multiple tracks ended up being pretty simple, because I just use simple string operations to split the tracks.

In more detail, the core of my interpreter looks something like this. The use of the while loop isn't very good and should probably be made into a for loop, but for some reason I was thinking of gotos, perhaps because of the underlying finite-state-machine influence.

...main loop...
  while s!='':
    result, s = self.pullFullNote(s, track)
    if result: continue
    
    result, s = self.pullFullNoteSet(s, track)
    if result: continue
    
    result, s = self.pullFullModOctave(s, track)
    if result: continue
    
    #if i get here, i couldn't interpret something, throw an error.
  
def pullFullNote(self, s, track):
  if it is not a note,
    return False, s
  
  #otherwise, consume some of the characters from s
  next_s = s[2:]
  
  #add the note to the track
  self.trackobjects[track].addnote()...
  
  return True, next_s



What is nice is that this pattern can be followed repeatedly on smaller levels. The pullFullNote() can call pullPitch() or pullVolumeDuration() in just the same way, and those can themselves call pull functions. If pullVolumeDuration() doesn't see a match, it simply returns False with the original string given. Essentially, the benefit is that there is no need to be explicitly asking "can the next thing be a note?", because the pullNote style functions will smoothly drop through when the next thing is not a note. I don't think I'm explaining this well, but it is all there in interpretsyntax.py if you are interested.

No comments: