Tuesday, September 21, 2010

Nokia Composer Parser and Compiler (to MIDI)

Grammars (EBNF)

(* MELODY GRAMMAR *)

melody = { pitched_note , ' ' } ;
pitched_note = duration , pitch , octave ;
duration = digit , { digit } , [ '.' ] ;
pitch = '-' | ( [ '#' ] name ) ;
octave = digit ;
name = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' ;
digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ;


(* RHYTHM GRAMMAR *)

rhythm = { rhythm_note , ' ' } ;
rhythm_note = duration , drum ;
duration = digit , { digit } , [ '.' ] ;
drum = '-' | drumOnKit ;
drumOnKit = 'sd' | 'bd' | 'ch' | 'oh' | 'ht' | 'mt' | 'lt' | 'cc' | 'rc' ;
digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ;




Parser/Compiler

import re
from midiutil.MidiFile import MIDIFile


DURATION  = r'(\d+[.]?)'
PITCH     = r'(-|(?:#?[abcdefg]))'
OCTAVE    = r'(\d+)?'
DRUM_NOTE = r'(-|(?:sd|bd|ch|oh|ht|mt|lt|cc|rc))'

MELODIC_NOTE_PATTERN  = DURATION + PITCH + OCTAVE
RHYTHMIC_NOTE_PATTERN = DURATION + DRUM_NOTE

INTERVALS_BETWEEN_OCTAVE = 12
INTERVALS = [0, 2, 4, 5, 7, 9, 11]
FIRST_MIDI_KEYBOARD_NOTE = 24
NOTES = 'cdefgab'

PERCUSSION_TRACK = 0

DRUM_TO_MIDI_NUMBER = {'sd' : 38,
                       'bd' : 35,
                       'ch' : 42,
                       'oh' : 46,
                       'ht' : 50,
                       'mt' : 47,
                       'lt' : 41,
                       'cc' : 49,
                       'rc' : 51
                       }
                       

class Piece(object):
    def __init__(self, tempo):
        self.tempo = tempo
        self.melodicTracks = []
        self.drumTracks = []

    def addMelodyTrack(self, melody, volume=100, instrument=1):
        convertedMelody = [PitchedNote(s) for s in melody.split()]

        newTrack = MelodyTrack(melody=convertedMelody, 
                               volume=volume, 
                               instrument=instrument)
        self.melodicTracks.append(newTrack)

    def addDrumTrack(self, drumString, volume=100):
        convertedRhythm = [DrumNote(s) for s in drumString.split()]
        
        newDrumTrack= DrumTrack(melody=convertedRhythm,
                                volume=volume)
        self.drumTracks.append(newDrumTrack)

    def finish(self, outputFileName='output.mid'):
        assert len(self.melodicTracks) + len(self.drumTracks) <= 15

        midi = MIDIFile(len(self.melodicTracks) + 1) 

        midi.addTrackName(PERCUSSION_TRACK, 0.0, 'Drum Track')

        for i in xrange(len(self.drumTracks)):
            self._addTrackToMIDI(midi, 0, self.drumTracks[i])
        for i in xrange(len(self.melodicTracks)):
            print 'adding track', i+1
            self._addTrackToMIDI(midi, i+1, self.melodicTracks[i])

        outputFile = open(outputFileName, 'wb')
        midi.writeFile(outputFile)
        outputFile.close()

        print 'MIDI compiled as', outputFileName

    def _addTrackToMIDI(self, midi, trackNum, track):
        channel = 9 if isinstance(track, DrumTrack) else trackNum

        if not isinstance(track, DrumTrack):
            midi.addTrackName(trackNum, 0.0, 'Track %d' % trackNum)
            midi.addProgramChange(trackNum, channel, 0.0, track.instrument)

        midi.addTempo(trackNum, 0.0, self.tempo)
        
        curPos = 0.0

        for note in track.melody:
            if not note.isRest():
                midi.addNote(trackNum,
                             channel,
                             note.MIDINumber,
                             curPos,
                             note.duration,
                             track.volume)
            curPos += note.duration


class MelodyTrack:
    def __init__(self, **kwds):
        self.__dict__.update(kwds)


class DrumTrack:
    def __init__(self, **kwds):
        self.__dict__.update(kwds)


class Note(object):
    def __init__(self, noteString):
        self.matchSubstrings(noteString)
        self.getDuration()
        if not self.isRest():
            self.getMIDINumber()

    def getDuration(self):
        if self.strings['duration'].endswith('.'):
             self.duration = 4 / float(self.strings['duration'][:-1]) * 1.5
        else:
            self.duration = 4 / float(self.strings['duration'])

    def isRest(self):
        return self.strings['name'].endswith('-')

    def matchSubstrings(self, noteString):
        raise NotImplementedError

    def getMIDINumber(self):
        raise NotImplementedError

    def __str__(self):
        return '<%s %d>' % (str(self.duration), self.MIDINumber)


class PitchedNote(Note):
    def matchSubstrings(self, noteString):
        matchObj = re.match(MELODIC_NOTE_PATTERN, noteString.lower())
        if matchObj == None:
            raise ValueError, 'Not a valid melodic note: ' + repr(noteString)
        self.strings = dict(zip(['duration', 'name', 'octave'], matchObj.groups()))

    def getMIDINumber(self):
        self.MIDINumber = FIRST_MIDI_KEYBOARD_NOTE
        self.MIDINumber += (int(self.strings['octave']) * INTERVALS_BETWEEN_OCTAVE)
        self.MIDINumber += INTERVALS[NOTES.index(self.strings['name'].replace('#',''))]
        self.MIDINumber += self.strings['name'].startswith('#') # compensate for sharpness


class DrumNote(Note):
    def matchSubstrings(self, noteString):
        matchObj = re.match(RHYTHMIC_NOTE_PATTERN, noteString.lower())
        if matchObj == None:
            raise ValueError, 'Not a valid rhythmic note: ' + repr(noteString)
        self.strings = dict(zip(['duration', 'name'], matchObj.groups()))

    def getMIDINumber(self):
        self.MIDINumber = DRUM_TO_MIDI_NUMBER[self.strings['name']]


if __name__ == '__main__':
    print 'You should probably import this so it does something.'

More on this later. Uses the MIDIUtil Python library (I <3 it).


Sample Track

# The Heart Asks Pleasure First
# by Michael Nyman
# Arranged by Daniel Goldbach

import composer

rightHand = '''
8A4 8B3 8C4 8E4 8A4 8B3 
8A4 8B3 8C4 8E4 8B4 8B3
8A4 8B3 8C4 8E4 8G4 8B3
8E4 8B3 8C4 8E4 8B3 8C4

8C5 8E4 8F4 8A4 8C5 8E4
8C5 8E4 8F4 8A4 8E5 8E4
8D5 8E4 8F4 8A4 8C5 8E4
8B4 8E4 8F4 8A4 8C5 8B4

8A4 8B3 8C4 8E4 8A4 8E4
8E5 8E4 8G4 8A4 8C5 8G4
8A4 8B3 8C4 8E4 8G4 8B3
8E4 8A3 8C4 8A3 8D4 8C4

8C4 8A3 8C4 8A3 8D4 8C4
8D4 8A3 8B3 8A3 8D4 8A3
8E4 8B3 8C4 8D4 8#G4 8B3
8A4 8B3 8C4 8E4 8A4 8E4

8A4 8B3 8C4 8E4 8C4 6E4

8E5 8E4 8A4 8E5 8E4 8A4
8E5 8E4 8A4 8D5 8E4 8A4
8C5 8E4 8A4 8C5 8E4 8A4
8E5 8E4 8A4 8C5 8E4 8A4

8E5 8E4 8A4 8E5 8E4 8A4
8E5 8E4 8A4 8D5 8E4 8A4
8C5 8E4 8A4 8C5 8E4 8A4
8A4 8D4 8#F4 8A4 8D4 8#F4

8E5 8E4 8A4 8E5 8E4 8A4
8E5 8E4 8A4 8D5 8E4 8A4
8C5 8E4 8A4 8C5 8E4 8A4
8E5 8E4 8A4 8C5 8E4 8A4

8E5 8E4 8A4 8E5 8E4 8A4
8E5 8E4 8A4 8D5 8E4 8A4
8C5 8E4 8A4 8C5 8E4 8A4
8A4 8D4 8#F4 8A4 8D4 6#F4

''' * 2

leftHand = '''
8A1 8E2 8A2 8E2 8A2 8E2
8A1 8E2 8A2 8E2 8A2 8E2
8A1 8E2 8A3 8E2 8A3 8E2
8A1 8E2 8A3 8E2 8A3 8E2

8F1 8C2 8F2 8C2 8F2 8C2
8F1 8C2 8F2 8C2 8F2 8C2
8G1 8D2 8G2 8D2 8G2 8D2
8G1 8D2 8G2 8D2 8G2 8D2

8A1 8E2 8A2 8E2 8A2 8E2
8A1 8E2 8A2 8E2 8A1 8E2
8A1 8E2 8A2 8E2 8A2 8E2
8A1 8C2 8E2 8C2 8A1 8E2

8A1 8C2 8E2 8C2 8A1 8E2
8E1 8B1 8E2 8D2 8B1 8E1
8E1 8B1 8E2 8B1 8B1 8E1
8A1 8E2 8A2 8E2 8A2 8E2

8A1 8E2 8A2 8E2 8A2 6E2

8C3 8A2 8E2 8A2 8C3 8A2
8B2 8G2 8D2 8G2 8B2 8G2
8A2 8E2 8C2 8E2 8A2 8E2
8A2 8E2 8C2 8E2 8A2 8E2

8C3 8A2 8E2 8A2 8C3 8A2
8B2 8G2 8D2 8G2 8B2 8G2
8A2 8E2 8C2 8E2 8A2 8E2
8A2 8#F2 8D2 8#F2 8A2 8#F2

8C3 8A2 8E2 8A2 8C3 8A2
8B2 8G2 8D2 8G2 8B2 8G2
8A2 8E2 8C2 8E2 8A2 8E2
8A2 8E2 8C2 8E2 8A2 8E2

8C3 8A2 8E2 8A2 8C3 8A2
8B2 8G2 8D2 8G2 8B2 8G2
8A2 8E2 8C2 8E2 8A2 8E2
8A2 8#F2 8D2 8#F2 8A2 6#F2

''' * 2

piece = composer.Piece(170) # 170 is a good tempo for this piece

piece.addMelodyTrack(rightHand)
piece.addMelodyTrack(leftHand, volume=80)

piece.finish(outputFileName='the heart asks pleasure first - michael nyman.mid')

No comments:

Post a Comment