Source code for ly.pitch

# 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