# GNU Solfege - free ear training software
# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2006, 2007, 2008 Tom Cato Amundsen
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
r"""
REMEMBER: down is positive, up is negative.
All voices begin at the beginning of the staff. It is
not possible to split a voice in two like in Lilypond.
The parser will not handle fis and f in the same octave on one stem.
Rules:
* It can be different timesignatures in different staffs.
* \key has to come before \time
The parser does not care if you have correct number of notes in a bar.
To get bar lines you have to insert a '|'
"""
import logging
import weakref
import operator
import re
from solfege.mpd.duration import Duration
from solfege.mpd.musicalpitch import MusicalPitch,InvalidNotenameException
from solfege.mpd.track import Track,PercussionTrack
from solfege.mpd.engravers import *
from solfege.mpd.requests import *
from solfege.mpd.rat import Rat
from solfege.mpd import const
from solfege.mpd import mpdutils
from solfege.mpd import _exceptions
class ParseError(_exceptions.MpdException):
def __init__(self, msg, lexer):
_exceptions.MpdException.__init__(self, msg)
self.m_lineno, self.m_linepos1, self.m_linepos2 = lexer.get_error_location()
class LexerError(_exceptions.MpdException):
def __init__(self, msg, lexer):
_exceptions.MpdException.__init__(self, msg)
self.m_lineno, self.m_linepos1, self.m_linepos2 = lexer.get_error_location()
def musicalpitch_relative(first, second):
"""
think: \relative c'{ first second }
Tritone handling is the same as GNU Lilypond
I placed here instead of in MusicalPitch since it is only used
once in parse_to_score_object and I don't think anyone need this
in MusicalPitch.
"""
assert isinstance(first, MusicalPitch)
assert isinstance(second, MusicalPitch)
n1 = second.clone()
n1.m_octave_i = first.m_octave_i
n2 = n1.clone()
if n1 < first:
n2.m_octave_i += 1
else:
n1.m_octave_i -= 1
if n2.steps() - first.steps() < first.steps() - n1.steps():
# we go up
n2.m_octave_i += second.m_octave_i
return n2
else:
# we go down
n1.m_octave_i += second.m_octave_i
return n1
class UnknownClefException(_exceptions.MpdException):
def __init__(self, clef):
_exceptions.MpdException.__init__(self)
self.m_clef = clef
def __str__(self):
return "'%s' is not a valid clef. Maybe a bug in your lessonfile?" % self.m_clef
class Clef(object):
# Use these constants to access the data in clefdata.
SYM = 0
# Which staff line should the clef be on lines 1 to 5. 1 is the lowest line
LINE = 1
# On which position in the staff is the middle C. 0 is the middle line
# in the staff. Positive values are up, negative are down.
POS = 2
clefdata = {
'treble': ('G', 2, -6),
'violin': ('G', 2, -6),
'G': ('G', 2, -6),
'G2': ('G', 2, -6),
'french': ('G', 1, -8),
#
'subbass': ('F', 5, 8),
'bass': ('F', 4, 6),
'F': ('F', 4, 6),
'varbaritone': ('F', 3, 4),
#
'baritone': ('C', 5, 4),
'tenor': ('C', 4, 2),
'alto': ('C', 3, 0),
'C': ('C', 3, 0),
'mezzosoprano': ('C', 2, -2),
'soprano': ('C', 1, -4),
}
octaviation_re = re.compile("(?P<name>[A-Za-z1-9]+)(?P<oct>([_^])(8|15))?$")
def __init__(self, clefname):
m = self.octaviation_re.match(clefname)
if not m:
raise UnknownClefException(clefname)
if m.group('name') not in self.clefdata:
raise UnknownClefException(clefname)
try:
self.m_octaviation = {'_8': -7, '_15': -14, '^8': 7, '^15': 14,
None: 0}[m.group('oct')]
except KeyError:
raise UnknownClefException(clefname)
self.m_name = m.group('name')
def get_symbol(self):
return self.clefdata[self.m_name][self.SYM]
def get_stafflinepos(self):
return self.clefdata[self.m_name][self.LINE]
def steps_to_ylinepos(self, steps):
return 7-self.clefdata[self.m_name][self.POS] - steps + self.m_octaviation
def an_to_ylinepos(self, an):
def notename_to_ylinepos(n, clef):
n = MusicalPitch.new_from_notename(n)
i = n.steps()
return self.steps_to_ylinepos(i)
if an[-2:] == 'es':
l = 3
h = -3
else:
l = 1
h = -5
i = notename_to_ylinepos(an, self)
while i > l:
an = an + "'"
i = notename_to_ylinepos(an, self)
while i < h:
an = an + ","
i = notename_to_ylinepos(an, self)
return i
class TimeSignature:
def __init__(self, num, den):
self.m_num = num
self.m_den = den
class VoiceColObj:
def __init__(self):
self.m_rest = None
# the key for this dictionary is semitonepitch for the notehead
self.m_music = {}
self.m_beaminfo = None
self.m_tupletinfo = None
self.m_duration = None
self.m_stempos = 0
class StaffColObj:
def __init__(self):
self.m_timesignature = None
self.m_keysignature = None
self.m_clef = None
self.m_barline = None
self.m_ledger_up = 0
self.m_ledger_down = 0
class ScoreColObj:
"""
"""
def __init__(self):
self.m_timesignature_obj = None
# These variables is used by Score to remember how wide the
# different elements of a score column is.
self.m_clef = 0
self.m_keysignature = 0
self.m_barline = 0
self.m_timesignature = 0
self.m_accidentals = 0
self.m_music = 0
self.m_leftshift = 0
self.m_rightshift = 0
class Score:
def __init__(self):
# m_timeposdict is a dictionary of ScoreColObj objects, one
# object for each column that has music or rests. The keys
# for the dict is timepos.
self.m_timeposdict = {}
self.m_staffs = []
def announce_timepos(self, timepos):
if timepos not in self.m_timeposdict:
self.m_timeposdict[timepos] = ScoreColObj()
def get_engravers(self, fontsize):
tv = self.m_timeposdict.keys()
tv.sort()
return self._generate_engravers(tv, fontsize)
def get_first_engravers(self, fontsize):
tv = self.m_timeposdict.keys()
tv.sort()
return self._generate_engravers([tv[0]], fontsize)
def _generate_engravers(self, tv, fontsize):
V = []
clef = None
self.m_spanner_list = []
self.m_stem_list = []
for staff in self.m_staffs:
staff.refill_accidentals_info(("c", "major"))
key = ('c', 'major')
se = []
V.append(se)
###################
beam = None
tuplet = None
for voice in staff.m_voice_list:
for timepos in tv:
if timepos not in staff.m_coldict:
continue
if timepos not in voice.m_coldict:
continue
if staff.m_coldict[timepos].m_clef:
clef = staff.m_coldict[timepos].m_clef
########
# stems
v = []
for n in voice.m_coldict[timepos].m_music.values():
v.append(clef.steps_to_ylinepos(n.m_pitch.steps()))
if v and (voice.m_coldict[timepos].m_music.values()[0].m_duration.m_nh > 1):
v.sort()
if voice.m_coldict[timepos].m_beaminfo == 'start':
beam = BeamEngraver(fontsize)
self.m_spanner_list.append(beam)
se.append(beam)
if beam and not voice.m_coldict[timepos].m_beaminfo:
beam = None
se.append(StemEngraver(timepos, fontsize, v,
voice.m_coldict[timepos],
beam is not None))
self.m_stem_list.append(se[-1])
if beam:
beam.add_stem(se[-1])
################################
# tuplets are created per-voice
################################
if voice.m_coldict[timepos].m_tupletinfo:
if voice.m_coldict[timepos].m_tupletinfo == 'continue':
tuplet.add_stem(se[-1])
else:
tuplet = TupletEngraver(fontsize, voice.m_coldict[timepos].m_tupletinfo)
tuplet.add_stem(se[-1])
self.m_spanner_list.append(tuplet)
se.append(tuplet)
else:
#if tuplet is not None: tuplet = None
tuplet = None
###################
# this loop takes care of stuff that is decided on a per-staff-basis
for timepos in tv:
if timepos not in staff.m_coldict:
continue
# clef
if staff.m_coldict[timepos].m_clef:
clef = staff.m_coldict[timepos].m_clef
se.append(ClefEngraver(timepos, fontsize,
staff.m_coldict[timepos].m_clef))
self.m_timeposdict[timepos].m_clef \
= max(self.m_timeposdict[timepos].m_clef, se[-1].get_width())
#key signature
if staff.m_coldict[timepos].m_keysignature:
se.append(KeySignatureEngraver(timepos, fontsize, key,
staff.m_coldict[timepos].m_keysignature, clef))
self.m_timeposdict[timepos].m_keysignature \
= max(self.m_timeposdict[timepos].m_keysignature,
se[-1].get_width())
key = staff.m_coldict[timepos].m_keysignature
staff.refill_accidentals_info(key)
# barline
if staff.m_coldict[timepos].m_barline:
se.append(BarlineEngraver(timepos, fontsize, "|"))
self.m_timeposdict[timepos].m_barline \
= max(self.m_timeposdict[timepos].m_barline,
se[-1].get_width())
staff.refill_accidentals_info(key)
# time signature
if self.m_timeposdict[timepos].m_timesignature_obj:
se.append(TimeSignatureEngraver(timepos, fontsize,
self.m_timeposdict[timepos].m_timesignature_obj))
self.m_timeposdict[timepos].m_timesignature \
= max(self.m_timeposdict[timepos].m_timesignature,
se[-1].get_width())
##############
# accidentals
v = {}
for voice in staff.m_voice_list:
if timepos not in voice.m_coldict:
continue
for music in voice.m_coldict[timepos].m_music.itervalues():
e = staff.needed_accidental(music.m_pitch)
if e is not None:
v[clef.steps_to_ylinepos(music.m_pitch.steps())] = e
if v:
se.append(AccidentalsEngraver(timepos, fontsize, v))
self.m_timeposdict[timepos].m_accidentals \
= max(self.m_timeposdict[timepos].m_accidentals,
se[-1].get_width())
################################
# xshift noteheads that need it
# first we have to find out what voice has the highest tones,
# because we have to lay out the noteheads in the highest
# voice first.
voicelist = []
if len(staff.m_voice_list) == 1:
voicelist = [staff.m_voice_list[0]]
else:
def f(A, B, timepos=timepos):
if timepos not in B.m_coldict:
return 1
if timepos not in A.m_coldict:
return 1
if not A.m_coldict[timepos].m_music:
return 1
if not B.m_coldict[timepos].m_music:
return -1
return cmp(B.m_coldict[timepos].m_music.values()[0].m_pitch.semitone_pitch(), A.m_coldict[timepos].m_music.values()[0].m_pitch.semitone_pitch())
return 1
staff.m_voice_list.sort(f)
voicelist = staff.m_voice_list
voice1_lowest_ylinepos = None
for voice in voicelist:
if timepos not in voice.m_coldict:
continue
# nd:
# * the keys in the dictionary is the position on the
# staff the notehead will have. 0 is the middle line,
# 1 is below the middle line, -2 is on the line above
# the middle line.
# * the values is the requests.MusicRequest that represents
# the notehead.
nd = {}
for mm in voice.m_coldict[timepos].m_music.itervalues():
nd[clef.steps_to_ylinepos(mm.m_pitch.steps())] = mm
if nd == {}:
# nd == {} when there are not noteheads, for example
# when there is a rest here.
break
v = nd.keys()
v.sort()
if voice.m_coldict[timepos].m_stemdir == const.UP:
# if we are stemUp, we assume this is the first voice
# to be layed out.
voice1_lowest_ylinepos = v[-1]
v.reverse()
for n in range(1, len(v)):
if nd[v[n]].m_pitch.steps() == nd[v[n-1]].m_pitch.steps()+1 and (not nd[v[n-1]].m_shift):
nd[v[n]].m_shift = 1
self.m_timeposdict[timepos].m_rightshift = 1
else:
# the first notehead will decide where to place
# the stem
stempos = 0 # default
if voice1_lowest_ylinepos is not None and v[0] == voice1_lowest_ylinepos + 1:
stempos = 1 # stem moved right
voice.m_coldict[timepos].m_stempos = 1
nd[v[0]].m_shift = 1
for n in range(1, len(v)):
if nd[v[n]].m_pitch.steps()+1 == nd[v[n-1]].m_pitch.steps() and nd[v[n-1]].m_shift == stempos:
nd[v[n]].m_shift = stempos - 1
if stempos == 0:
self.m_timeposdict[timepos].m_leftshift = 1
else:
nd[v[n]].m_shift = stempos
#####################################
# create notehead and rest engravers
for voice in staff.m_voice_list:
if timepos not in voice.m_coldict:
continue
for music in voice.m_coldict[timepos].m_music.itervalues():
if music.m_duration.m_nh < 2:
head = const.NOTEHEAD_0
elif music.m_duration.m_nh > 2:
head = const.NOTEHEAD_2
else:
head = const.NOTEHEAD_1
ylinepos = clef.steps_to_ylinepos(music.m_pitch.steps())
se.append(NoteheadEngraver(timepos, fontsize, music.m_shift,
ylinepos, head, music.m_duration.m_dots,
music.m_pitch.semitone_pitch(),
voice.m_coldict[timepos]))
self.m_timeposdict[timepos].m_music \
= max(self.m_timeposdict[timepos].m_music, se[-1].get_width())
l = self.m_timeposdict[timepos].m_leftshift
r = self.m_timeposdict[timepos].m_rightshift
self.m_timeposdict[timepos].m_music \
= max(self.m_timeposdict[timepos].m_music, (1+l+r)*dimentions[fontsize].xshift) + 2
#
if voice.m_coldict[timepos].m_rest:
se.append(RestEngraver(timepos, fontsize, 0,
voice.m_coldict[timepos].m_rest))
self.m_timeposdict[timepos].m_music \
= max(self.m_timeposdict[timepos].m_music, se[-1].get_width())
####################################
# Find out if wee need ledger lines
up = 0
down = 0
for voice in staff.m_voice_list:
if timepos not in voice.m_coldict:
continue
for music in voice.m_coldict[timepos].m_music.itervalues():
ypos = music.m_pitch.steps()
ypos = clef.steps_to_ylinepos(ypos)
if up > ypos < -5:
up = ypos
if down < ypos > 5:
down = ypos
###############################
# Create ledger line engravers
if timepos in staff.m_coldict:
if up:
up = - up / 2 - 2
else:
up = 0
if down:
down = down / 2 - 2
else:
down = 0
e = LedgerLineEngraver(timepos, fontsize, up, down)
se.append(e)
for voice in staff.m_voice_list:
for tie in voice.m_ties:
if tie[0] in tv and tie[1] in tv:
sh1 = tie[3].m_shift
sh2 = tie[4].m_shift
se.append(TieEngraver(fontsize, tie[0], tie[1],
sh1, sh2,
clef.steps_to_ylinepos(tie[2][0])))
#########################################
xv = {}
p = 0
pv = self.m_timeposdict.keys()
pv.sort()
class Dummy:
pass
for timepos in pv:
xv[timepos] = Dummy()
xv[timepos].m_clef = p
p += self.m_timeposdict[timepos].m_clef
xv[timepos].m_keysignature = p
p += self.m_timeposdict[timepos].m_keysignature
xv[timepos].m_barline = p
p += self.m_timeposdict[timepos].m_barline
xv[timepos].m_timesignature = p
p += self.m_timeposdict[timepos].m_timesignature
xv[timepos].m_accidentals = p
p += self.m_timeposdict[timepos].m_accidentals
if self.m_timeposdict[timepos].m_leftshift:
p += 10
xv[timepos].m_music = p
p += self.m_timeposdict[timepos].m_music
for ev in V:
for e in ev:
#FIXME all engravers should take two arguments, i think...
if isinstance(e, AccidentalsEngraver):
e.set_xpos(xv, self.m_timeposdict)
else:
e.set_xpos(xv)
for e in self.m_spanner_list:
e.do_layout()
# we delete it because it is not used any more, and to help avoid
# circular references
del self.m_spanner_list
for e in self.m_stem_list:
e.calc_xpos()
del self.m_stem_list
return V
def add_staff(self):
staff = Staff(self)
self.m_staffs.append(staff)
return staff
def get_midi_events(self, start=None, end=None):
kv = self.m_timeposdict.keys()
kv.sort()
if start is None and end is not None:
return self._generate_midi_events(
[i for i in kv if i < end], Track)
elif start is not None and end is None:
return self._generate_midi_events(
[i for i in kv if i >= start], Track)
elif start is None and end is None:
return self._generate_midi_events(kv, Track)
else:
assert start is not None and end is not None
return self._generate_midi_events(
[i for i in kv if start <= i < end], Track)
def get_first_beat_midi_events(self):
kv = self.m_timeposdict.keys()
kv.sort()
return self._generate_midi_events([kv[0]], Track)
def get_last_beat_midi_events(self):
kv = self.m_timeposdict.keys()
kv.sort()
return self._generate_midi_events([kv[-1]], Track)
def get_midi_events_as_percussion(self):
kv = self.m_timeposdict.keys()
kv.sort()
return self._generate_midi_events(kv, PercussionTrack)
def _generate_midi_events(self, kv, tracktype):
"""
kv is a list of rat.Rat that tell the timepos for all the tones
we should generate midi events for.
Return a list of tracks, one track for each voice.
"""
track_list = []
for staff in self.m_staffs:
for voice in staff.m_voice_list:
track_list.append(voice.generate_track_for_voice(staff, kv, tracktype))
return track_list
class Staff:
def __init__(self, score):
self.w_score = weakref.ref(score)
self.m_coldict = {}
self.m_voice_list = []
def add_voice(self):
voice = Voice(self)
self.m_voice_list.append(voice)
return voice
def refill_accidentals_info(self, key):
"""Fill the .m_accidentals_info dict with the accidentals
that exist in the key signature `key`.
"""
self.m_accidentals_info = {}
for step in range(MusicalPitch.LOWEST_STEPS, MusicalPitch.HIGHEST_STEPS+1):
self.m_accidentals_info[step] = 0
for a in mpdutils.key_to_accidentals(key):
n = MusicalPitch.new_from_notename(a)
for oct in range(-4, 7):
n.m_octave_i = oct
if n.semitone_pitch() < 128:
if a[-4:] == 'eses':
self.m_accidentals_info[n.steps()] = -2
elif a[-2:] == 'es':
self.m_accidentals_info[n.steps()] = -1
elif a[-4:] == 'isis':
self.m_accidentals_info[n.steps()] = 2
else:
self.m_accidentals_info[n.steps()] = 1
def needed_accidental(self, m):
steps = m.steps()
if m.m_accidental_i != self.m_accidentals_info[steps]:
if (self.m_accidentals_info[steps] == 2 and m.m_accidental_i == 1) \
or (self.m_accidentals_info[steps] == -2 and m.m_accidental_i == -1):
self.m_accidentals_info[steps] = m.m_accidental_i
return [0, m.m_accidental_i]
self.m_accidentals_info[steps] = m.m_accidental_i
return [m.m_accidental_i]
def barline(self, timepos):
#assert timepos not in self.m_coldict
self.m_coldict[timepos] = StaffColObj()
self.m_coldict[timepos].m_barline = 1
def add_timesignature(self, t, timepos):
#self.m_coldict[timepos].m_timesignature = t
self.w_score().announce_timepos(timepos)
self.w_score().m_timeposdict[timepos].m_timesignature_obj = t
def add_keysignature(self, timepos, key):
self.m_coldict[timepos].m_keysignature = key
def add_clef(self, timepos, clef):
self.m_coldict[timepos].m_clef = Clef(clef)
def announce_timepos(self, timepos):
if timepos not in self.m_coldict:
self.m_coldict[timepos] = StaffColObj()
self.w_score().announce_timepos(timepos)
class Voice:
def __init__(self, parent_staff):
self.m_coldict = {}
self.w_parent_staff = weakref.ref(parent_staff)
self._tmp_tie = {}
self.m_ties = []
self.m_beams = []
self.m_is_beaming = None
self.m_doing_tuplet = None
def add_notehead(self, timepos, music, stemdir):
assert timepos in self.m_coldict
key = (music.m_pitch.steps(), music.m_pitch.m_accidental_i)
if __debug__:
if key in self.m_coldict[timepos].m_music:
logging.warning("Voice: warning, adding the same notehead twice %s" % str(key))
if (self.m_coldict[timepos].m_duration is not None) \
and (self.m_coldict[timepos].m_duration != music.m_duration):
logging.warning("mpd: warning: All noteheads on the same stem should have the same length")
self.m_coldict[timepos].m_duration = music.m_duration
if self.m_is_beaming and self.m_coldict[timepos].m_duration.get_rat_value() >= Rat(1, 4):
logging.warning("mpd: warning: beamed stems has to be 1/8-note or shorter. Ignoring invalid beam request.")
self.m_is_beaming = None
music.m_shift = 0
self.m_coldict[timepos].m_music[key] = music
self.m_coldict[timepos].m_stemdir = stemdir
def add_rest(self, rest, timepos):
assert timepos in self.m_coldict
self.m_coldict[timepos].m_rest = rest
def announce_timepos2(self, timepos):
if timepos not in self.m_coldict:
self.m_coldict[timepos] = VoiceColObj()
self.w_parent_staff().announce_timepos(timepos)
def announce_timepos(self, timepos):
if timepos not in self.m_coldict:
self.m_coldict[timepos] = VoiceColObj()
if self.m_is_beaming:
self.m_coldict[timepos].m_beaminfo = 'continue'
if self.m_doing_tuplet:
self.m_coldict[timepos].m_tupletinfo = 'continue'
self.w_parent_staff().announce_timepos(timepos)
def start_tuplet(self, timepos, times, dir):
self.m_coldict[timepos].m_tupletinfo = (times.m_den, dir)
self.m_doing_tuplet = 1
def end_tuplet(self):
self.m_doing_tuplet = None
def start_beam(self, timepos):
if self.m_is_beaming:
logging.warning("mpd-warning: we are already beaming, ignoring start_beam request")
return
self.m_is_beaming = 1
self.m_coldict[timepos].m_beaminfo = 'start'
def end_beam(self):
if not self.m_is_beaming:
logging.warning("mpd-warning: we are not beaming, ignoring stop_beam request")
self.m_is_beaming = 0
def do_tie_end(self, pos1, pos2):
for p in self.m_coldict[pos1].m_music:
if p in self.m_coldict[pos2].m_music:
self.m_ties.append((pos1, pos2, p,
# need these to generate midi track
self.m_coldict[pos1].m_music[p],
self.m_coldict[pos2].m_music[p]))
def generate_track_for_voice(self, staff, kv, tracktype):
# first we find the id()'s of music to tie
tie_from_v = []
tie_to_v = []
for t in self.m_ties:
tie_from_v.append(id(t[3]))
tie_to_v.append(id(t[4]))
################
D = {}
i = 2
musictimepos = Rat(0, 1)
last_timepos = kv[0]
id_D = {}
for idx in range(len(kv)):
coltimepos = kv[idx]
musictimepos = musictimepos + (coltimepos - last_timepos)
if coltimepos in self.m_coldict:
for n in sorted(self.m_coldict[coltimepos].m_music.values(),
key=operator.attrgetter('m_pitch')):
#FIXME this is ugly
if id(n) in tie_from_v and (id(n) not in tie_to_v):
id_D[n.m_pitch.semitone_pitch()] = i
if musictimepos not in D:
D[musictimepos] = []
D[musictimepos].append((i, const.START_NOTE, n.m_pitch.semitone_pitch()))
elif id(n) in tie_to_v:
stop_pos = musictimepos + n.m_duration.get_rat_value()
if stop_pos not in D:
D[stop_pos] = []
D[stop_pos].append((id_D[n.m_pitch.semitone_pitch()],
const.STOP_NOTE, n.m_pitch.semitone_pitch()))
else:
if musictimepos not in D:
D[musictimepos] = []
D[musictimepos].append((i, const.START_NOTE, n.m_pitch.semitone_pitch()))
stop_pos = musictimepos + n.m_duration.get_rat_value()
if stop_pos not in D:
D[stop_pos] = []
D[stop_pos].append((i, const.STOP_NOTE, n.m_pitch.semitone_pitch()))
i = i + 1
last_timepos = coltimepos
return self.__gen_midi_last_step(D, tracktype)
def __gen_midi_last_step(self, D, tracktype):
keys = D.keys()
keys.sort()
prev_time = Rat(0)
ms = tracktype()
for k in keys:
delta = None
if k != Rat(0, 1):
delta = k-prev_time
prev_time = k
for e in D[k]:
if e[1] == const.START_NOTE:
if delta:
ms.notelen_time(delta)
ms.start_note(e[2], const.DEFAULT_VELOCITY)
elif e[1] == const.STOP_NOTE:
if delta:
ms.notelen_time(delta)
ms.stop_note(e[2], const.DEFAULT_VELOCITY)
delta = None
return ms
def parse_to_score_object(music):
lexer = Lexer(music)
relative_mode = None
relto=None
transpose_pitch = None
TOPLEVEL = 1#'toplevel'
NOTES = 2#'notes'
START_OF_CHORD = 3#'start-of-chord'
CHORD = 4#'chord'
context = TOPLEVEL
score = Score()
chord_duration = None
cur_duration = Duration(4, 0)
tie_is_in_the_air = 0
times = None
cur_staff = None
for toc, toc_data in lexer:
try:
if toc_data.m_duration:
cur_duration = toc_data.m_duration
except AttributeError:
pass
if toc == Lexer.STAFF:
assert context == TOPLEVEL
cur_staff = score.add_staff()
cur_voice = cur_staff.add_voice()
stem_dir = const.BOTH
tuplet_dir = const.BOTH
relative_mode = None
timepos = Rat(0)
last_pos = timepos
cur_staff.announce_timepos(timepos)
cur_staff.add_clef(timepos, "violin")
elif toc == Lexer.VOICE:
if not cur_staff:
raise ParseError("Don't use \\addvoice before \\staff", lexer)
relative_mode = None
timepos = Rat(0)
cur_voice = cur_staff.add_voice()
elif toc == Lexer.RELATIVE:
assert not relative_mode
relative_mode = 1
relto = toc_data
elif toc == Lexer.TRANSPOSE:
transpose_pitch = toc_data
elif toc == Lexer.TIME:
if not cur_staff:
raise ParseError(u"\\time can not be used before \\staff", lexer)
cur_staff.announce_timepos(timepos)
cur_staff.add_timesignature(toc_data, timepos)
elif toc == Lexer.KEY:
p = MusicalPitch.new_from_notename(toc_data[0])
if transpose_pitch:
p.transpose_by_musicalpitch(transpose_pitch)
k = (p.get_notename(), toc_data[1])
if not cur_staff:
raise ParseError(u"\\key can not be used before \\staff", lexer)
cur_staff.announce_timepos(timepos)
cur_staff.add_keysignature(timepos, k)
elif toc == Lexer.TIMES:
if not times:
times = toc_data
cur_voice.announce_timepos(timepos)
cur_voice.start_tuplet(timepos, times, tuplet_dir)
else:
raise ParseError(r"\times nn/nn does not nest", lexer)
elif toc == Lexer.CLEF:
cur_staff.announce_timepos(timepos)
try:
cur_staff.add_clef(timepos, toc_data)
except UnknownClefException, e:
e.m_lineno, e.m_linepos1, e.m_linepos2 = lexer.get_error_location()
raise
elif toc == '|':
cur_staff.announce_timepos(timepos)
cur_staff.barline(timepos)
elif toc == '{':
if (context == TOPLEVEL):
context = NOTES
if not cur_staff.m_coldict[Rat(0, 1)].m_keysignature:
if transpose_pitch:
k = (transpose_pitch.get_notename(), 'major')
else:
k = ('c', 'major')
cur_staff.add_keysignature(Rat(0, 1), k)
else:
raise ParseError("Token '{' not allowed here.", lexer)
elif toc == '<':
if context == NOTES:
context = START_OF_CHORD
else:
raise ParseError("Token '<' not allowed here.", lexer)
elif toc == '>':
if context == CHORD:
if tie_is_in_the_air:
tie_is_in_the_air = 0
cur_voice.do_tie_end(last_pos, timepos)
last_pos = timepos
timepos = timepos + chord_duration.get_rat_value()
chord_duration = None
relto = relto_backup; relto_backup = None
context = NOTES
else:
raise ParseError("Token '>' not allowed here.", lexer)
elif toc == '}':
if context == NOTES:
if times:
times = None
cur_voice.end_tuplet()
else:
context = TOPLEVEL
else:
raise ParseError("Token '}' not allowed here.", lexer)
elif toc == '[':
cur_voice.announce_timepos(timepos)
cur_voice.start_beam(timepos)
elif toc == ']':
cur_voice.end_beam()
# we call announce_timepos2, because the regular
# version breaks tuplets. FIXME
cur_voice.announce_timepos2(timepos)
elif toc == '~':
tie_is_in_the_air = 1
elif toc == Lexer.NOTE and (context in [NOTES, CHORD, START_OF_CHORD]):
if not getattr(toc_data, 'm_duration', None):
toc_data.m_duration = cur_duration
if times:
toc_data.m_duration.m_tuplet = times
if context in [NOTES, START_OF_CHORD]:
cur_voice.announce_timepos(timepos)
if relative_mode:
toc_data.m_pitch = musicalpitch_relative(
relto, toc_data.m_pitch)
relto = toc_data.m_pitch
if transpose_pitch:
toc_data.transpose(transpose_pitch)
cur_voice.add_notehead(timepos, toc_data, stem_dir)
if context == NOTES:
if tie_is_in_the_air:
cur_voice.do_tie_end(last_pos, timepos)
tie_is_in_the_air = 0
last_pos = timepos
timepos = timepos + toc_data.m_duration.get_rat_value()
if context == START_OF_CHORD:
relto_backup = relto
chord_duration = toc_data.m_duration
context = CHORD
elif toc == Lexer.SKIP and context == NOTES:
if toc_data:
cur_duration = toc_data
else:
toc_data = cur_duration
last_pos = timepos
timepos = timepos + toc_data.get_rat_value()
elif toc == Lexer.REST and context == NOTES:
cur_voice.announce_timepos(timepos)
if toc_data:
cur_duration = toc_data
else:
toc_data = cur_duration
cur_voice.add_rest(toc_data, timepos)
last_pos = timepos
timepos += toc_data.get_rat_value()
elif toc == Lexer.STEMDIR:
stem_dir = toc_data
elif toc == Lexer.TUPLETDIR:
tuplet_dir = toc_data
else:
raise ParseError(toc, lexer)
return score
class Lexer:
STAFF = 1
VOICE = 2
CLEF = 3
STEMDIR = 4
TRANSPOSE = 5
TIME = 6
KEY = 7
NOTE = 8
SKIP = 9
REST = 10
RELATIVE = 11
TIMES = 12
TUPLETDIR = 13
re_staff = re.compile(r"\\staff", re.UNICODE)
re_voice = re.compile(r"\\addvoice", re.UNICODE)
re_clef = re.compile(r"\\clef\s+(\w*)", re.UNICODE)
re_clef_quoted = re.compile(r"\\clef\s+\"([A-Za-z1-9]+[_^1-9]*)\"", re.UNICODE)
re_stem_updown = re.compile(r"(\\stem)(Up|Down|Both)\s+", re.UNICODE)
re_tuplet_updown = re.compile(r"(\\tuplet)(Up|Down|Both)\s+", re.UNICODE)
re_relative = re.compile(r"\\relative\s+(([a-zA-Z]+)([',]*))", re.UNICODE)
re_transpose = re.compile(r"\\transpose\s+(([a-zA-Z]+)([',]*))", re.UNICODE)
re_rest = re.compile(r"(r)([\d]*)(\.*)", re.UNICODE)
#FIXME we are a little more strict than Lilypond, since ~ has to
# be before ]
#FIXME don't use named regex if we don't need it.
re_melodic = re.compile(r"""(?x)
((?P<notename>[a-zA-Z]+)
(?P<octave>[',]*))
(?P<len>[\d]*)
(?P<dots>\.*)""", re.UNICODE)
re_skip = re.compile(r"""(?x)
(s)
(?P<len>[\d]*)
(?P<dots>\.*)""", re.UNICODE)
re_time = re.compile(r"\\time\s+(\d+)\s*/\s*(\d+)", re.UNICODE)
re_key = re.compile(r"\\key\s+([a-z]+)\s*\\(major|minor)", re.UNICODE)
re_times = re.compile(r"\\times\s+(\d+)\s*/\s*(\d+)\s*{", re.UNICODE)
def __init__(self, s):
self.m_string = s
self.m_notelen = Duration(4, 0)
self.m_idx = 0
self.m_last_idx = None
def __iter__(self):
return self
@staticmethod
def to_string(v):
ret = []
for toc, toc_data in v:
if toc == Lexer.STAFF: # 1
ret.append(r"\staff")
elif toc == Lexer.VOICE: # 2
ret.append(r"\addvoice")
elif toc == Lexer.CLEF: # 3
ret.append(r"\clef %s " % toc_data)
elif toc == Lexer.STEMDIR: # 4
ret.append(r"\stem%s " % {const.UP: 'Up',
const.DOWN: 'Down',
const.BOTH: 'Both'}[toc_data])
elif toc == Lexer.TRANSPOSE: # 5
ret.append(r"\transpose %s" % toc_data.get_octave_notename())
elif toc == Lexer.TIME: # 6
ret.append(r"\time %i/%i " % (toc_data.m_num, toc_data.m_den))
elif toc == Lexer.KEY: # 7
ret.append(r"\key %s \%s " % toc_data)
elif toc == Lexer.NOTE: # 8
ret.append("%s%s " % (toc_data.m_pitch.get_octave_notename(),
toc_data.m_duration.as_mpd_string() if toc_data.m_duration else ""))
elif toc == Lexer.SKIP: # 9
ret.append("s%s " % (toc_data.as_mpd_string() if toc_data else "",))
elif toc == Lexer.REST: # 10
ret.append("r%s " % (toc_data.as_mpd_string() if toc_data else "",))
elif toc == Lexer.RELATIVE: # 11
ret.append(r"\relative %s" % toc_data.get_octave_notename())
elif toc == Lexer.TIMES: # 12
ret.append(r"\times %i/%i{ " % (toc_data.m_num, toc_data.m_den))
elif toc == Lexer.TUPLETDIR: # 13
ret.append(r"\tuplet%s " % {const.UP: 'Up',
const.DOWN: 'Down',
const.BOTH: 'Both'}[toc_data])
else:
ret.append("%s " % toc)
return "".join(ret).strip()
def next(self):
try:
return self._next()
except _exceptions.MpdException, e:
if 'm_mpd_badcode' not in dir(e):
e.m_lineno, e.m_linepos1, e.m_linepos2 = self.get_error_location()
raise
def _next(self):
# Doing this while loop inside the exception clause is a little
# faster than using a regular expression.
try:
while self.m_string[self.m_idx] in (' ', '\n', '\t'):
self.m_idx += 1
except IndexError:
raise StopIteration
self.m_last_idx = self.m_idx
m = self.re_rest.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
resttype, notelen, dots = m.groups()
numdots = len(dots)
if notelen:
notelen = int(notelen)
else:
notelen = 0
if numdots:
raise LexerError('Need a digit before dots. Write "%(goodcode)s", not "%(badcode)s".' % {
'badcode': m.group().strip(),
'goodcode':'%s%i%s' % (resttype, self.m_notelen.m_nh, dots)
},
self)
if notelen is 0:
retval = None
else:
self.m_notelen = retval = Duration(notelen, numdots)
return self.REST, retval
m = self.re_skip.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
IGN1, skiplen, dots = m.groups()
numdots = len(dots)
if skiplen:
skiplen = int(skiplen)
self.m_notelen = Duration(skiplen, numdots)
else:
skiplen = 0
if numdots:
raise LexerError('Need a digit before dots. Write "%(goodcode)s", not "%(badcode)s".' % {
'badcode': m.group().strip(),
'goodcode':'s%i%s' % (self.m_notelen.m_nh, dots)
}, lexer)
if skiplen is 0:
retval = None
else:
self.m_notelen = retval = Duration(skiplen, numdots)
return self.SKIP, retval
m = self.re_melodic.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
notename, IGN1, IGN2, notelen, dots = m.groups()
numdots = len(dots)
if notelen:
notelen = int(notelen)
self.m_notelen = Duration(notelen, numdots)
else:
notelen = 0
if dots:
raise LexerError('Need a digit before dots. Write "%(goodcode)s", not "%(badcode)s".' % {
'badcode': m.group().strip(),
'goodcode':'%s%i%s' % (notename, self.m_notelen.m_nh, dots)
}, self)
n = MusicRequest(notename, notelen, numdots)
return self.NOTE, n
m = self.re_staff.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
return self.STAFF, None
m = self.re_voice.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
return self.VOICE, None
m = self.re_relative.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
return self.RELATIVE, MusicalPitch.new_from_notename(m.group(1))
m = self.re_clef_quoted.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
return self.CLEF, m.group(1)
m = self.re_clef.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
return self.CLEF, m.group(1)
m = self.re_stem_updown.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
d = [const.UP, const.DOWN, const.BOTH][['Up', 'Down', 'Both'].index(m.group(2))]
return self.STEMDIR, d
m = self.re_tuplet_updown.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
d = [const.UP, const.DOWN, const.BOTH][['Up', 'Down', 'Both'].index(m.group(2))]
return self.TUPLETDIR, d
m = self.re_transpose.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
return self.TRANSPOSE, MusicalPitch.new_from_notename(m.group(1))
m = self.re_time.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
return self.TIME, TimeSignature(int(m.group(1)), int(m.group(2)))
m = self.re_key.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
return self.KEY, (m.group(1), m.group(2))
m = self.re_times.match(self.m_string, self.m_idx)
if m:
self.m_idx = m.end()
return self.TIMES, Rat(int(m.groups()[0]), int(m.groups()[1]))
if self.m_idx == len(self.m_string):
raise StopIteration
self.m_idx += 1
return self.m_string[self.m_idx-1], None
def get_error_location(self):
"""
Return a tuple
(lineno, pos1, pos2)
lineno is the 0-index line where the error occoured.
string[pos1:pos2] will return the exact text that caused the error.
"""
# Let us first count lines to find which line we are on.
# line numbers are 0-indexed
lineno = self.m_string[:self.m_last_idx].count("\n")
line_start = self.m_last_idx
while line_start > 0 and self.m_string[line_start] != "\n":
line_start -= 1
if self.m_string[line_start] == "\n":
line_start += 1
line_end = line_start
while line_end < len(self.m_string) and self.m_string[line_end] != '\n':
line_end += 1
return (lineno, self.m_last_idx - line_start, self.m_idx - line_start)
def set_first_pitch(self, pitch):
"""
Modify the first pitch of the music.
"""
assert isinstance(pitch, MusicalPitch)
for toc, toc_data in self:
if toc == Lexer.NOTE:
self.m_string = u" ".join(
[x for x in [
self.m_string[:self.m_last_idx].rstrip(),
"%s%s" % (pitch.get_octave_notename(), toc_data.m_duration.as_mpd_string() if toc_data.m_duration else ""),
self.m_string[self.m_idx:].lstrip()] if x])
break
def validate_only_notenames(s):
"""
Return (None, None, None) if the string s is only notenames
(pitch and duration). No ties or other tokens are allowed.
"""
lex = Lexer(s)
try:
for toc, toc_data in lex:
if not toc:
break
if toc != lex.NOTE:
return lex.get_error_location()
except InvalidNotenameException:
return lex.get_error_location()
return None, None, None
|