PyZine
 


Article Finder
People
Issue 6 - Revision 7  /   October 18, 2004 


 
  Py Links:
Latest Issue
Issue 08
Issue 07
Issue 06
Issue 05
Issue 04
Issue 02
Issue 01
 
 
Downloads
     
  Articles:
Throughout the quarter we cover topics of interest to Python developers.

  Python MIDI

  Python in Bioinformatics

  Getting Twisted

  optparse

  jEdit

  Python RFID

  3d Graphics in Python

 
 
 
     

Illustration by Lia Avant
PMIDI Cover
PMIDI

Python Midi (PMIDI)
A simple MIDI library for Python and Windows
- - - - - - - - - - - -

By Peter Parente | May 3, 2004

print
Abstract

The Musical Instrument Digital Interface (MIDI) is a standard in the music industry for controlling audio devices such as keyboard synthesizers and computer sound cards. A client to a MIDI device can format, sequence, and stream MIDI commands in order to generate synthesized music and sound effects. Dealing with the low-level details of command structure, ordering, and timing can be difficult, which makes a layer of abstraction essential to ease the burden of controlling MIDI devices from software.

The PMIDI library for Python and Windows hides many of the details of controlling MIDI devices. In this article, we first present a programmer's view of MIDI. We next introduce PMIDI by explaining its purpose and its design. We then provide three examples of increasing complexity to demonstrate the features and use of PMIDI. Finally, we conclude with a discussion of the current limitations of the library, as well as possible improvements.

MIDI for the Programmer

In most modern operating systems, a programmer does not have to be concerned with many of the details of communicating with a MIDI device. From the programmer's point of view, the operating system (OS) provides a "conduit" for communication with any MIDI synthesizer attached to the computer — a sound card, a keyboard attached through a sound card, a keyboard attached to a dedicated MIDI I/O card, etc. Each device, in turn, supports a number of channels (typically sixteen) that can be configured independently and controlled by means of commands. A program generates these commands to indicate what sound should be synthesized on a particular channel, how it should be synthesized, and when it should be played.

Commands for a MIDI device are fixed-length packets with bits representing the type of command, the target channel, and the data payload. To play a note, a program sends a 'note on' command to a channel on a MIDI device with a data payload indicating which pitch should be played and how loudly it should be played. To end a playing note, a program sends a 'note off' command with the same data payload after the desired duration of the note has elapsed. Commands are also available for selecting instruments, bending note pitch, adding key aftertouch, and setting channel panning, to name a few of the possible operations that can be programmed.

Programming Difficulties

While MIDI provides an effective, standardized way of interacting with synthesizers, it presents a couple of difficulties for programmers who just want to "make some noise." First, the process of building proper commands and sending them to a MIDI device can be tedious and error-prone. Dealing with bit fields is not something a programmer wants to do often. Second, sending 'note on' and 'note off' commands at proper intervals can be challenging for an application that is busy doing other work. Passing a buffer to another party for proper playback is more desirable. Both of these obstacles can be hidden with proper encapsulation.

PMIDI: MIDI for Python and Windows

The PMIDI library wraps the low-level details of the MIDI standard with high-level programming abstractions. Instead of creating command messages containing information about channel, voice, pitch, and duration, programmers work with Python objects representing songs, instruments, measures, and notes. When sound needs to be synthesized, the PMIDI library automatically converts these objects into the appropriate MIDI commands and sequences them correctly. Once sequenced, the commands are executed asynchronously by the Microsoft Windows MIDI Streams API. This API manages all of the timing and communication details with the target MIDI device, allowing a programmer to "play and forget" a particular sequence.

The original purpose of the PMIDI library was to provide a way for the author to create earcons (short snippets of music that encode information, for example, that a new email has been received) in his assistive technology research. The design of the library directly reflects this purpose. PMIDI makes it easy for a programmer to generate and play back MIDI sequences on-the-fly in Python code. The MIDI sequence might be representative of some software event (as mentioned above, that a new email has been received) or creatively encode some more detailed information (e.g., the length of the new email or if it is likely to be spam). On the other hand, PMIDI does not help in playing long MIDI songs or in loading and saving MIDI files to and from disk. These features are absent from the library at the current time.

Design

The PMIDI library consists of five first-class objects a programmer deals with directly — Sequencer, Song, Instrument, Measure, and Note. The only object the programmer directly instantiates is Sequencer; it is then used to create a new Song. The Song object can then be used to create new Instruments representing the different voices in the composition. An Instrument object can be used to create Measures, which in turn can be used to organize and create Notes.

Consider the following example code segment which creates and plays a new MIDI sequence consisting of one instrument playing two notes.

from PMIDI.Composer import Sequencer
import time


# create the sequencer
seq = Sequencer()


# create a new song
song = seq.NewSong()


# add an instrument to the song
inst = song.NewVoice()


# add a measure to the instrument
meas = inst.NewMeasure()


# add some notes to the measure
# params are (in order): starting tick, duration, pitch, octave
meas.NewNote(0, 64, 'C', 5)
meas.NewNote(0, 64, 'G', 5)


# play the song
seq.Play()
# wait because the song plays asynchronously
time.sleep(5)

Every time a new object is created, its creator maintains a reference and hands back a reference. For instance, when a new Measure is created by calling NewMeasure on an Instrument object, a reference to the Measure object is maintained within the Instrument object and another is handed back to the programmer. This approach allows the programmer to work with the Measure object by adding new notes, and also allows the Instrument to keep track of the Measure and its Notes for later sequencing. The final Play method call works properly because the Sequencer still holds a reference to the last 'song' it created.

Most of the information in this example (and in most PMIDI programs) is encoded in the NewNote method calls. The NewNote method adds a single note to the MIDI sequence given its starting tick within its parent measure, its duration in ticks, its pitch name, and its octave. The tick value is one sixty-fourth of the total duration of a measure, or a sixty-fourth note in musical terms. A note's duration may extend past the end of a measure indefinitely. The pitch name may be selected from any of the standard note names (C, C#, Db, etc.) and the octave ranges from 0 to 10 (a C in octave zero to a G ten octaves higher) on a typical MIDI device.

Notes played by a single voice may be overlapped in time. In the example code above, both the C and G notes are started and stopped simultaneously, forming an open chord. While MIDI devices may only support a small number of channels, each channel may play a number of pitches concurrently to produce polyphonic sound.

As mentioned earlier, MIDI playback is performed asynchronously by PMIDI. As a result, the execution of the script continues immediately after the Play method is called, not after the sequence has finished playing. The sleep statement at the end of the program is necessary to keep the script from quitting before the sequence ends. In a program with a message pump (e.g. a GUI program), the sleep statement is unnecessary since the program will sit idle or process other input while the sequence is playing.

Using PMIDI

The features and use of the PMIDI library can be presented clearly through a few annotated examples. The first example plays a simple scale at a specific tempo using a specific instrument. The second example plays the same scale, but with two additional parts — a bassline and a drum rhythm. The final example shows a small application of PMIDI to the sonification of real world data.

Example 1: Playing a one-voice scale

from PMIDI.Composer import Sequencer
import time


# create the sequencer and song
seq = Sequencer()
song = seq.NewSong()
song.SetTempo(180)


# add a single voice to the song and make it a Marimba
inst = song.NewVoice()
inst.SetInstrumentByName('Marimba')

# add a measure to the song and the appropriate notes for the scale
m = inst.NewMeasure()
m.NewNote(0,8,'C',5)
m.NewNote(8,8,'D',5)
m.NewNote(16,8,'E',5)
m.NewNote(24,8,'F',5)
m.NewNote(32,8,'G',5)
m.NewNote(40,8,'A',5)
m.NewNote(48,8,'B',5)
m.NewNote(56,8,'C',6)


# play the song
seq.Play()
# wait because the song plays asynchronously
time.sleep(5)

In this first example, we start by instantiating a Sequencer object. We then create a new Song, and set its tempo to 180 beats per minute (BPM). Next we create a new Voice and make it the instrument 'marimba'.. We continue by making a new Measure object for the marimba and adding all the notes of a C-major scale to it. Finally, we ask the Sequencer to play the scale.

Example 2: Extending the scale with additional voices

from PMIDI.Composer import Sequencer
import time


# create the sequencer and song
seq = Sequencer()
song = seq.NewSong()


# add a three voices to the song
marimba = song.NewVoice()
marimba.SetInstrumentByName('Marimba')
bass = song.NewVoice()
bass.SetInstrumentByName('Accoustic Bass')
drums = song.NewVoice(is_drum=True)


# compose the marimba part
scale = [('C', 5), ('D', 5), ('E', 5), ('F', 5), ('G', 5), ('A', 5), ('B', 5), ('C',6)]
m = marimba.NewMeasure()
tick = 0
duration = 8
for note, octave in scale:
   m.NewNote(tick, duration, note, octave)
   tick += duration


# compose the bassline
m = bass.NewMeasure()
scale.reverse()
tick = 0
duration = 8
for note, octave in scale:
   m.NewNote(tick, duration, note, octave-2)
   tick += duration


# compose the drumset part
m = drums.NewMeasure()
# bass drum
duration = 16
for tick in range(0, 64, duration):
   m.NewHit(tick, duration, 'Bass Drum 1')
# hi-hat on offbeats
duration = 4
for tick in range(4, 64, duration):
   m.NewHit(tick, duration, 'Pedal Hi-Hat')
# cymbal crash at the end, in the next measure
m = drums.NewMeasure()
m.NewHit(0,32,'Crash Cymbal 1')


# play the song
seq.Play()
# wait because the song plays asynchronously
time.sleep(5)

In this second example, we extend our simple scale with a bassline and a drum beat while folding the NewNote commands into succinct loops. We first create three Voices for the three instruments in our short 'song': a marimba, an acoustic bass, and a drumset. Since the MIDI standard reserves a special channel for the MIDI drumset, we must explicitly state that we want to use it when we create the third Voice object.

Again, we create a Measure for the marimba and fill it with an ascending C-major scale. We also create a Measure for the bass and fill it with a descending C-major scale. (N.B.: All notes are in concert pitch.) The Notes for both instruments are triggered on the same ticks in the first Measure, so they are played simultaneously. We then create two Measures for the drumset. We fill the first with hits on various instruments in the drumset using the NewHit command. In the second measure, we place a single cymbal crash on the first beat. Finally, we ask the Sequencer to play our song.

Example 3: Sonifying unread emails

import imaplib
from PMIDI.Composer import Sequencer


class AudioIMAP(object):
   def __init__(self, username, password, host, port=143, secure=True):
     # store instance variables
     self.username = username
     self.password = password
     self.host = host
     self.port = port
     self.secure = secure
     self.conn = None
     self.seq = Sequencer()


     def Connect(self):
     # connect to the IMAP server using the prescribed settings
     if self.secure:
       self.conn = imaplib.IMAP4_SSL(self.host, self.port)
     else:
       self.conn = imaplib.IMAP4(self.host, self.port)
     self.conn.login(self.username, self.password)
     self.conn.select('INBOX')


   def Close(self):
     # close the IMAP connection
     self.conn.close()


   def GetRecentCount(self):
     # count the number of recent emails
     typ, data = self.conn.search(None, 'RECENT')
     return len(data[0].split())


   def SonifyRecentEmails(self):
     # get the number of recent emails
     self.Connect()
     count = self.GetRecentCount()
     self.Close()


     # create a MIDI sequence encoding the number of emails
     song = self.seq.NewSong()
     v = song.NewVoice()
     m = v.NewMeasure()
     if count == 0:
       # play a single blast to indicate no emails
       v.SetInstrumentByName('Trombone')
       m.NewNote(0,16,'D#',3)
     else:
       # encode the number of emails in the number of notes and rising pitch
       v.SetInstrumentByName('Vibraphone')
       i = 0
       scale = [('C', 6), ('D', 6), ('E', 6), ('F', 6), ('G', 6), ('A', 6), ('B', 6), ('C', 7), ('C', 8)]
       while(i < count and i < len(scale)):
         m.NewNote(i*4, 4, scale[i][0], scale[i][1])
         i += 1
      self.seq.Play(song)


if __name__ == '__main__':
   import time
   ai = AudioIMAP('johndoe', 'mypass', 'imap.unc.edu', secure=False)
   ai.SonifyRecentEmails()
   time.sleep(3)

In the final example, we use PMIDI to sonify quantitative information rather than to just make musical sounds. The AudioIMAP class can connect to an IMAP server, ask for the number of new emails, and play a MIDI sequence representing the number of new emails received by the IMAP server. Such an application might be useful to users who want to know how much new email has arrived without having to divert their visual attention away from their current work.

The SonifyRecentEmails class is the function of interest in our AudioIMAP class. When the method is called, it initiates an IMAP connection, gets the number of recent emails, and then disconnects. If there is no new mail, a single trombone blast is played. If x new emails have arrived, the first x notes of a C-major scale are played. If more than ten new emails have arrived, then the entire C-major scale is played followed by a note an octave higher. The number of emails (up to ten) is redundantly encoded in the number of notes played and their increasing pitch.

Limitations and Possible Improvements

At the time of this writing, the current version of PMIDI (1.0) is limited in a number of ways. It would not require much work to remedy most of these limitations.

  • Note attack parameters are not supported. All notes are sounded with a full attack value.
  • Note effects are not supported. All notes are played without any left-right panning or additional effects.
  • Master volume control is not supported. The master volume is set to the default value for all sequences.
  • Event callbacks are not supported. Events cannot be injected into a song to trigger a callback in Python code.
  • The library only works on the Windows platform. Other APIs supporting the same "play and forget" model as the Windows MIDI Streams API need to be supported before PMIDI can work on other platforms.
  • Sequences are limited to 64 KB of data. The underlying Windows MIDI Streams API cannot buffer sequences larger than this.
  • The code is not fully documented.
References

The latest version of PMIDI is available for download from SourceForge at http://sourceforge.net/projects/uncassist. Peter Parente, the author of PMIDI, can be contacted by email at parente@cs.unc.edu.


Peter Parente

shim
shim

 Py is committed to bringing you great Python Articles.

shim
shim


Home   Subscribe   Migration FAQ   Contact PyZine   Write for PyZine   ZopeMag   opensourcexperts.com  

Reproduction of material from any of PyZine's pages without prior written permission is strictly prohibited. Copyright 2003 - 2005 PyZine Zope/Plone hosting by Nidelven IT