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.

Midi (part 2) Scoreview

For fun, I wrote a program that visualizes the contents of Midi files.
The "score view" is just a Tkinter canvas on which lines and ovals are drawn. None of the graphics are bitmaps (except the clefs), which means that there is freedom to quickly zoom in and out. The sharp signs are actually the text "#" drawn at that point. Writing a custom coordinate translation made this code so much easier. When I specify y coordinates, they are given in units where 2 units is the height of between staff lines. So, moving a note up or down just means incrementing or decrementing its position, and only the lowest level of code needs to know about the actual pixel coordinates.

This tool can be a useful way to explore the contents of a midi song. Besides showing the score for a track, it can also get channel information:

Also, one can view all of the midi events in a track, in a human-readable format:

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

If you've been wondering, yes, the eventual goal is to create a midi editor. This project isn't high priority, though.

Sunday, February 15, 2009

Midi (part 1)

I've been playing around with MIDI files lately.

MIDI files are essentially a list of notes with associated timing, and in the 90s were popularly used to make and share music. They can be easily made with a synthesizer and a computer. A drawback, though, is that the songs sound different if played on different computers. For example, different sound cards have different interpretations of what the "acoustic grand piano" voice should sound like, and the midi file itself does not contain the audio sample data. However, midis can be programmatically generated, and one can change tempo and pitch without losing quality. After broadband internet and mp3s became common, most people stopped using MIDI, although it still finds a use in music notation programs.

Unfortunately, these days, new computers come with awful-sounding software MIDI instruments, creating the false impression that "MIDI" is synonymous with "cheap, bad audio quality." In fact, there is nothing inherently bad-sounding about MIDI music, and with a set of good SoundFonts, it can sound very good.

Anyways, here are some Python scripts that can be used to create midis. The interface is not the best, but it was very quick to implement. Examples:
b = BMidiBuilder()
b.note('c', 1) 
#the 1 means the note's length is 1 qtr note
b.note('d', 1)
b.note('e', 1)
b.note('f', 1)
b.note('g', 4)
b.save('out.mid')
This plays part of a scale. To play a chord, "rewind" can be used to go back and lay more notes on top of existing notes.

b = BMidiBuilder()
b.note('c#', 1)
b.rewind(1)
b.note('f', 1)
b.rewind(1)
b.note('g#', 1)
b.save('out.mid')
Two tracks can be joined, to make a little tune:
tr1 = BMidiBuilder()
tr1.setInstrument('acoustic bass')
tr1.note('c3', 2)
tr1.note('d3', 2)
tr1.note('e3', 2)
tr1.rest(2)
tr1.note('f3',2)

tr2 = BMidiBuilder()
tr2.setInstrument('ocarina')
tr2.note('e4', 2)
tr2.note('f4', 2)
tr2.note('g4', 2)
tr1.rest(2)
tr1.note('a4',2)

bbuilder.joinTracks( [tr1, tr2], 'out.mid')

Also, there are a set of classes in bmidilib.py that can build a midi file and any type of event, like percussion, modulation, metadata, or pitch bends.

Download, tested in Python 2.5

Tonight I wondered what it would sound like to play the same note on the 127 general midi instruments. The result sounds interesting.
b = bbuilder.BMidiBuilder()
b.tempo = 400
for i in range(127):
  #insert a raw instrument change event
  evt = bmidilib.BMidiEvent()
  evt.type='PROGRAM_CHANGE'
  evt.channel=1
  evt.data = i
  b.insertMidiEvent(evt)
  b.note('c3',0.4)
b.save('cool.mid')
Hear it.
(Sound depends on your midi device, I don't recommend Quicktime so you may find it better to download this file and open it elsewhere).

Sunday, February 1, 2009

Minimath

Here is a long-term project of mine that still isn't finished, but is starting to become useful. I started this in a computer science class last year, so not all of it was done on my own time. At Olin, sometimes I'd like to have the software equivalent of my TI calculator. Matlab can be useful, but takes a while to load and cannot be described as light-weight. So, for quick calculations, I made Minimath, a small command line interface to evaluate math.

The main design goal was to save typing for commonly-occuring tasks. As you can see, the UI is pretty sparse now, and that is on my list of things to improve.

Features

  • Fills-in incomplete expressions. It is perfectly ok to evaluate "sin(pi" without the closing paren, because it will understand what you meant.
  • Line history, with arrow keys. Control-Up and Control-Down search history based on prefix like Matlab.
  • Supports complex numbers, saving values in variables.
  • You can press Alt-. to create a "->" symbol to store values, like a TI 83.
  • "ans" refers to last result.
  • If the first key you press is an operator, it fills in the "ans" like a TI 83. This one saves a lot of time.
  • Any valid Python expression can also be evaluated.
  • Pressing the "\" key (while not in a string literal) creates a lambda symbol, so that expressions like "f = λx.x+2" can be used.
  • Pretty print, which behind-the-scenes is parsing the expression, creating and rendering a temporary LaTeX file.
  • Uses Numpy library which provides many math functions.
Another feature is variable substitution. Pressing alt-, makes a "<-" symbol that can be used to subsitute values into an expression. For example, the result of "2*x^2 + 3*x <- 4" is 44.

Added Syntax

  • The symbol "^" now is exponentiation.
  • The ternary expression ? : can be used as in C.
  • The shortened "for(i,5)" can be used in place of "for i in range(5)".
  • Syntax for making arrays: "arr = a[1 2 3]" and "arr = a[1 2 3;4 5 6]". (Note that arrays are different than Python lists).
  • "i[0,4,10]" is like Matlab's linspace, 10 elements equally spaced from 0 to 4.
  • "i[0..4]" is an inclusive range, the result is [0,1,2,3,4].
  • It allows more items on one line than Python, just use semicolons. In particular, something like "for val in array:t+=val;print val" will work all on one line.
In the future, I plan to add more-advanced interpretation. For example, the string "3x4" could be recognized as "3*x^4", and "4(x+3)" to "4*(x+3)" Also, I should integrate Matplotlib or something similar.