(* 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
More on this later. Uses the MIDIUtil Python library (I <3 it).
Sample Track
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