# This file is part of python-ly, https://pypi.python.org/pypi/python-ly
#
# Copyright (c) 2008 - 2015 by Wilbert Berendsen
#
# 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 2
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# See http://www.gnu.org/licenses/ for more information.
"""
Pitch manipulation.
"""
from __future__ import unicode_literals
import re
from fractions import Fraction
import ly.lex.lilypond
pitchInfo = {
'nederlands': (
('c', 'd', 'e', 'f', 'g', 'a', 'b'),
('eses', 'eseh', 'es', 'eh', '', 'ih', 'is', 'isih', 'isis'),
(('ees', 'es'), ('aes', 'as'))
),
'english': (
('c', 'd', 'e', 'f', 'g', 'a', 'b'),
('ff', 'tqf', 'f', 'qf', '', 'qs', 's', 'tqs', 'ss'),
),
'deutsch': (
('c', 'd', 'e', 'f', 'g', 'a', 'h'),
('eses', 'eseh', 'es', 'eh', '', 'ih', 'is', 'isih', 'isis'),
(('ases', 'asas'), ('ees', 'es'), ('aes', 'as'), ('heses', 'heses'), ('hes', 'b'))
),
'svenska': (
('c', 'd', 'e', 'f', 'g', 'a', 'h'),
('essess', '', 'ess', '', '', '', 'iss', '', 'ississ'),
(('ees', 'es'), ('aes', 'as'), ('hessess', 'hessess'), ('hess', 'b'))
),
'italiano': (
('do', 're', 'mi', 'fa', 'sol', 'la', 'si'),
('bb', 'bsb', 'b', 'sb', '', 'sd', 'd', 'dsd', 'dd')
),
'espanol': (
('do', 're', 'mi', 'fa', 'sol', 'la', 'si'),
('bb', '', 'b', '', '', '', 's', '', 'ss')
),
'portugues': (
('do', 're', 'mi', 'fa', 'sol', 'la', 'si'),
('bb', 'btqt', 'b', 'bqt', '', 'sqt', 's', 'stqt', 'ss')
),
'vlaams': (
('do', 're', 'mi', 'fa', 'sol', 'la', 'si'),
('bb', '', 'b', '', '', '', 'k', '', 'kk')
),
}
pitchInfo['norsk'] = pitchInfo['deutsch']
pitchInfo['suomi'] = pitchInfo['deutsch']
pitchInfo['catalan'] = pitchInfo['italiano']
[docs]class PitchNameNotAvailable(Exception):
"""Exception raised when there is no name for a pitch.
Can occur when translating pitch names, if the target language e.g.
does not have quarter-tone names.
"""
def __init__(self, language):
super(PitchNameNotAvailable, self).__init__()
self.language = language
[docs]class Pitch(object):
"""A pitch with note, alter and octave attributes.
Attributes may be manipulated directly.
"""
def __init__(self, note=0, alter=0, octave=0, accidental="", octavecheck=None):
self.note = note # base note (c, d, e, f, g, a, b)
# as integer (0 to 6)
self.alter = alter # # = .5; b = -.5; natural = 0
self.octave = octave # '' = 2; ,, = -2
self.accidental = accidental # "", "?" or "!"
self.octavecheck = octavecheck # a number is an octave check
def __repr__(self):
return '<Pitch {0}>'.format(self.output())
[docs] def output(self, language="nederlands"):
"""Returns our string representation."""
res = []
res.append(pitchWriter(language)(self.note, self.alter))
res.append(octaveToString(self.octave))
res.append(self.accidental)
if self.octavecheck is not None:
res.append('=')
res.append(octaveToString(self.octavecheck))
return ''.join(res)
[docs] @classmethod
def c1(cls):
"""Returns a pitch c'."""
return cls(octave=1)
[docs] @classmethod
def c0(cls):
"""Returns a pitch c."""
return cls()
[docs] @classmethod
def f0(cls):
"""Return a pitch f."""
return cls(3)
[docs] def copy(self):
"""Returns a new instance with our attributes."""
return self.__class__(self.note, self.alter, self.octave)
[docs] def makeAbsolute(self, lastPitch):
"""Makes ourselves absolute, i.e. sets our octave from lastPitch."""
self.octave += lastPitch.octave - (self.note - lastPitch.note + 3) // 7
[docs] def makeRelative(self, lastPitch):
"""Makes ourselves relative, i.e. changes our octave from lastPitch."""
self.octave -= lastPitch.octave - (self.note - lastPitch.note + 3) // 7
[docs]class PitchWriter(object):
language = "unknown"
def __init__(self, names, accs, replacements=()):
self.names = names
self.accs = accs
self.replacements = replacements
def __call__(self, note, alter = 0):
"""
Returns a string representing the pitch in our language.
Raises PitchNameNotAvailable if the requested pitch
has an alteration that is not available in the current language.
"""
pitch = self.names[note]
if alter:
acc = self.accs[int(alter * 4 + 4)]
if not acc:
raise PitchNameNotAvailable(self.language)
pitch += acc
for s, r in self.replacements:
if pitch.startswith(s):
pitch = r + pitch[len(s):]
break
return pitch
[docs]class PitchReader(object):
def __init__(self, names, accs, replacements=()):
self.names = list(names)
self.accs = list(accs)
self.replacements = replacements
self.rx = re.compile("({0})({1})?$".format("|".join(names),
"|".join(acc for acc in accs if acc)))
def __call__(self, text):
for s, r in self.replacements:
if text.startswith(r):
text = s + text[len(r):]
for dummy in 1, 2:
m = self.rx.match(text)
if m:
note = self.names.index(m.group(1))
if m.group(2):
alter = Fraction(self.accs.index(m.group(2)) - 4, 4)
else:
alter = 0
return note, alter
# HACK: were we using (rarely used) long english syntax?
text = text.replace('flat', 'f').replace('sharp', 's')
return False
[docs]def octaveToString(octave):
"""Converts numeric octave to a string with apostrophes or commas.
0 -> "" ; 1 -> "'" ; -1 -> "," ; etc.
"""
return octave < 0 and ',' * -octave or "'" * octave
[docs]def octaveToNum(octave):
"""Converts string octave to an integer:
"" -> 0 ; "," -> -1 ; "'''" -> 3 ; etc.
"""
return octave.count("'") - octave.count(",")
_pitchReaders = {}
_pitchWriters = {}
[docs]def pitchReader(language):
"""Returns a PitchReader for the specified language."""
try:
return _pitchReaders[language]
except KeyError:
res = _pitchReaders[language] = PitchReader(*pitchInfo[language])
return res
[docs]def pitchWriter(language):
"""Returns a PitchWriter for the specified language."""
try:
return _pitchWriters[language]
except KeyError:
res = _pitchWriters[language] = PitchWriter(*pitchInfo[language])
res.language = language
return res
[docs]class PitchIterator(object):
"""Iterate over notes or pitches in a source."""
def __init__(self, source, language="nederlands"):
"""Initialize with a ly.document.Source.
The language is by default set to "nederlands".
"""
self.source = source
self.setLanguage(language)
[docs] def setLanguage(self, lang):
r"""Changes the pitch name language to use.
Called internally when \language or \include tokens are encountered
with a valid language name/file.
Sets the language attribute to the language name and the read attribute
to an instance of ly.pitch.PitchReader.
"""
if lang in pitchInfo.keys():
self.language = lang
return True
[docs] def tokens(self):
"""Yield all the tokens from the source, following the language."""
for t in self.source:
yield t
if isinstance(t, ly.lex.lilypond.Keyword):
if t in ("\\include", "\\language"):
for t in self.source:
if not isinstance(t, ly.lex.Space) and t != '"':
lang = t[:-3] if t.endswith('.ly') else t[:]
if self.setLanguage(lang):
yield LanguageName(lang, t.pos)
break
yield t
[docs] def read(self, token):
"""Reads the token and returns (note, alter) or None."""
return pitchReader(self.language)(token)
[docs] def pitches(self):
"""Yields all tokens, but collects Note and Octave tokens.
When a Note is encountered, also reads octave and octave check and then
a Pitch is yielded instead of the tokens.
"""
tokens = self.tokens()
for t in tokens:
while isinstance(t, ly.lex.lilypond.Note):
p = self.read(t)
if not p:
break
p = Pitch(*p)
p.note_token = t
p.octave_token = None
p.accidental_token = None
p.octavecheck_token = None
t = None # prevent hang in this loop
for t in tokens:
if isinstance(t, ly.lex.lilypond.Octave):
p.octave = octaveToNum(t)
p.octave_token = t
elif isinstance(t, ly.lex.lilypond.Accidental):
p.accidental_token = p.accidental = t
elif isinstance(t, ly.lex.lilypond.OctaveCheck):
p.octavecheck = octaveToNum(t)
p.octavecheck_token = t
break
elif not isinstance(t, ly.lex.Space):
break
yield p
if t is None:
break
else:
yield t
[docs] def position(self, t):
"""Returns the cursor position for the given token or Pitch."""
if isinstance(t, Pitch):
t = t.note_token
return self.source.position(t)
[docs] def write(self, pitch, language=None):
"""Output a changed Pitch.
The Pitch is written in the Source's document.
To use this method reliably, you must instantiate the PitchIterator
with a ly.document.Source that has tokens_with_position set to True.
"""
document = self.source.document
pwriter = pitchWriter(language or self.language)
note = pwriter(pitch.note, pitch.alter)
end = pitch.note_token.end
if note != pitch.note_token:
document[pitch.note_token.pos:end] = note
octave = octaveToString(pitch.octave)
if octave != pitch.octave_token:
if pitch.octave_token is None:
document[end:end] = octave
else:
end = pitch.octave_token.end
document[pitch.octave_token.pos:end] = octave
if pitch.accidental:
if pitch.accidental_token is None:
document[end:end] = pitch.accidental
elif pitch.accidental != pitch.accidental_token:
end = pitch.accidental_token.end
document[pitch.accidental_token.pos:end] = pitch.accidental
elif pitch.accidental_token:
del document[pitch.accidental_token.pos:pitch.accidental_token.end]
if pitch.octavecheck is not None:
octavecheck = '=' + octaveToString(pitch.octavecheck)
if pitch.octavecheck_token is None:
document[end:end] = octavecheck
elif octavecheck != pitch.octavecheck_token:
document[pitch.octavecheck_token.pos:pitch.octavecheck_token.end] = octavecheck
elif pitch.octavecheck_token:
del document[pitch.octavecheck_token.pos:pitch.octavecheck_token.end]
[docs]class LanguageName(ly.lex.Token):
"""A Token that denotes a language name."""
pass