1# This file is part of python-ly, https://pypi.python.org/pypi/python-ly 2# 3# Copyright (c) 2008 - 2015 by Wilbert Berendsen 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the GNU General Public License 7# as published by the Free Software Foundation; either version 2 8# of the License, or (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18# See http://www.gnu.org/licenses/ for more information. 19 20""" 21Pitch manipulation. 22""" 23 24from __future__ import unicode_literals 25 26import re 27from fractions import Fraction 28 29import ly.lex.lilypond 30 31 32pitchInfo = { 33 'nederlands': ( 34 ('c', 'd', 'e', 'f', 'g', 'a', 'b'), 35 ('eses', 'eseh', 'es', 'eh', '', 'ih', 'is', 'isih', 'isis'), 36 (('ees', 'es'), ('aes', 'as')) 37 ), 38 'english': ( 39 ('c', 'd', 'e', 'f', 'g', 'a', 'b'), 40 ('ff', 'tqf', 'f', 'qf', '', 'qs', 's', 'tqs', 'ss'), 41 ), 42 'deutsch': ( 43 ('c', 'd', 'e', 'f', 'g', 'a', 'h'), 44 ('eses', 'eseh', 'es', 'eh', '', 'ih', 'is', 'isih', 'isis'), 45 (('ases', 'asas'), ('ees', 'es'), ('aes', 'as'), ('heses', 'heses'), ('hes', 'b')) 46 ), 47 'svenska': ( 48 ('c', 'd', 'e', 'f', 'g', 'a', 'h'), 49 ('essess', '', 'ess', '', '', '', 'iss', '', 'ississ'), 50 (('ees', 'es'), ('aes', 'as'), ('hessess', 'hessess'), ('hess', 'b')) 51 ), 52 'italiano': ( 53 ('do', 're', 'mi', 'fa', 'sol', 'la', 'si'), 54 ('bb', 'bsb', 'b', 'sb', '', 'sd', 'd', 'dsd', 'dd') 55 ), 56 'espanol': ( 57 ('do', 're', 'mi', 'fa', 'sol', 'la', 'si'), 58 ('bb', '', 'b', '', '', '', 's', '', 'ss') 59 ), 60 'portugues': ( 61 ('do', 're', 'mi', 'fa', 'sol', 'la', 'si'), 62 ('bb', 'btqt', 'b', 'bqt', '', 'sqt', 's', 'stqt', 'ss') 63 ), 64 'vlaams': ( 65 ('do', 're', 'mi', 'fa', 'sol', 'la', 'si'), 66 ('bb', '', 'b', '', '', '', 'k', '', 'kk') 67 ), 68} 69pitchInfo['norsk'] = pitchInfo['deutsch'] 70pitchInfo['suomi'] = pitchInfo['deutsch'] 71pitchInfo['catalan'] = pitchInfo['italiano'] 72 73 74class PitchNameNotAvailable(Exception): 75 """Exception raised when there is no name for a pitch. 76 77 Can occur when translating pitch names, if the target language e.g. 78 does not have quarter-tone names. 79 80 """ 81 def __init__(self, language): 82 super(PitchNameNotAvailable, self).__init__() 83 self.language = language 84 85 86class Pitch(object): 87 """A pitch with note, alter and octave attributes. 88 89 Attributes may be manipulated directly. 90 91 """ 92 def __init__(self, note=0, alter=0, octave=0, accidental="", octavecheck=None): 93 self.note = note # base note (c, d, e, f, g, a, b) 94 # as integer (0 to 6) 95 self.alter = alter # # = .5; b = -.5; natural = 0 96 self.octave = octave # '' = 2; ,, = -2 97 self.accidental = accidental # "", "?" or "!" 98 self.octavecheck = octavecheck # a number is an octave check 99 100 def __repr__(self): 101 return '<Pitch {0}>'.format(self.output()) 102 103 def output(self, language="nederlands"): 104 """Returns our string representation.""" 105 res = [] 106 res.append(pitchWriter(language)(self.note, self.alter)) 107 res.append(octaveToString(self.octave)) 108 res.append(self.accidental) 109 if self.octavecheck is not None: 110 res.append('=') 111 res.append(octaveToString(self.octavecheck)) 112 return ''.join(res) 113 114 @classmethod 115 def c1(cls): 116 """Returns a pitch c'.""" 117 return cls(octave=1) 118 119 @classmethod 120 def c0(cls): 121 """Returns a pitch c.""" 122 return cls() 123 124 @classmethod 125 def f0(cls): 126 """Return a pitch f.""" 127 return cls(3) 128 129 def copy(self): 130 """Returns a new instance with our attributes.""" 131 return self.__class__(self.note, self.alter, self.octave) 132 133 def makeAbsolute(self, lastPitch): 134 """Makes ourselves absolute, i.e. sets our octave from lastPitch.""" 135 self.octave += lastPitch.octave - (self.note - lastPitch.note + 3) // 7 136 137 def makeRelative(self, lastPitch): 138 """Makes ourselves relative, i.e. changes our octave from lastPitch.""" 139 self.octave -= lastPitch.octave - (self.note - lastPitch.note + 3) // 7 140 141 142class PitchWriter(object): 143 language = "unknown" 144 def __init__(self, names, accs, replacements=()): 145 self.names = names 146 self.accs = accs 147 self.replacements = replacements 148 149 def __call__(self, note, alter = 0): 150 """ 151 Returns a string representing the pitch in our language. 152 Raises PitchNameNotAvailable if the requested pitch 153 has an alteration that is not available in the current language. 154 """ 155 pitch = self.names[note] 156 if alter: 157 acc = self.accs[int(alter * 4 + 4)] 158 if not acc: 159 raise PitchNameNotAvailable(self.language) 160 pitch += acc 161 for s, r in self.replacements: 162 if pitch.startswith(s): 163 pitch = r + pitch[len(s):] 164 break 165 return pitch 166 167 168class PitchReader(object): 169 def __init__(self, names, accs, replacements=()): 170 self.names = list(names) 171 self.accs = list(accs) 172 self.replacements = replacements 173 self.rx = re.compile("({0})({1})?$".format("|".join(names), 174 "|".join(acc for acc in accs if acc))) 175 176 def __call__(self, text): 177 for s, r in self.replacements: 178 if text.startswith(r): 179 text = s + text[len(r):] 180 for dummy in 1, 2: 181 m = self.rx.match(text) 182 if m: 183 note = self.names.index(m.group(1)) 184 if m.group(2): 185 alter = Fraction(self.accs.index(m.group(2)) - 4, 4) 186 else: 187 alter = 0 188 return note, alter 189 # HACK: were we using (rarely used) long english syntax? 190 text = text.replace('flat', 'f').replace('sharp', 's') 191 return False 192 193 194def octaveToString(octave): 195 """Converts numeric octave to a string with apostrophes or commas. 196 197 0 -> "" ; 1 -> "'" ; -1 -> "," ; etc. 198 199 """ 200 return octave < 0 and ',' * -octave or "'" * octave 201 202 203def octaveToNum(octave): 204 """Converts string octave to an integer: 205 206 "" -> 0 ; "," -> -1 ; "'''" -> 3 ; etc. 207 208 """ 209 return octave.count("'") - octave.count(",") 210 211 212_pitchReaders = {} 213_pitchWriters = {} 214 215 216def pitchReader(language): 217 """Returns a PitchReader for the specified language.""" 218 try: 219 return _pitchReaders[language] 220 except KeyError: 221 res = _pitchReaders[language] = PitchReader(*pitchInfo[language]) 222 return res 223 224 225def pitchWriter(language): 226 """Returns a PitchWriter for the specified language.""" 227 try: 228 return _pitchWriters[language] 229 except KeyError: 230 res = _pitchWriters[language] = PitchWriter(*pitchInfo[language]) 231 res.language = language 232 return res 233 234 235class PitchIterator(object): 236 """Iterate over notes or pitches in a source.""" 237 238 def __init__(self, source, language="nederlands"): 239 """Initialize with a ly.document.Source. 240 241 The language is by default set to "nederlands". 242 243 """ 244 self.source = source 245 self.setLanguage(language) 246 247 def setLanguage(self, lang): 248 r"""Changes the pitch name language to use. 249 250 Called internally when \language or \include tokens are encountered 251 with a valid language name/file. 252 253 Sets the language attribute to the language name and the read attribute 254 to an instance of ly.pitch.PitchReader. 255 256 """ 257 if lang in pitchInfo.keys(): 258 self.language = lang 259 return True 260 261 def tokens(self): 262 """Yield all the tokens from the source, following the language.""" 263 for t in self.source: 264 yield t 265 if isinstance(t, ly.lex.lilypond.Keyword): 266 if t in ("\\include", "\\language"): 267 for t in self.source: 268 if not isinstance(t, ly.lex.Space) and t != '"': 269 lang = t[:-3] if t.endswith('.ly') else t[:] 270 if self.setLanguage(lang): 271 yield LanguageName(lang, t.pos) 272 break 273 yield t 274 275 def read(self, token): 276 """Reads the token and returns (note, alter) or None.""" 277 return pitchReader(self.language)(token) 278 279 def pitches(self): 280 """Yields all tokens, but collects Note and Octave tokens. 281 282 When a Note is encountered, also reads octave and octave check and then 283 a Pitch is yielded instead of the tokens. 284 285 """ 286 tokens = self.tokens() 287 for t in tokens: 288 while isinstance(t, ly.lex.lilypond.Note): 289 p = self.read(t) 290 if not p: 291 break 292 p = Pitch(*p) 293 294 p.note_token = t 295 p.octave_token = None 296 p.accidental_token = None 297 p.octavecheck_token = None 298 299 t = None # prevent hang in this loop 300 for t in tokens: 301 if isinstance(t, ly.lex.lilypond.Octave): 302 p.octave = octaveToNum(t) 303 p.octave_token = t 304 elif isinstance(t, ly.lex.lilypond.Accidental): 305 p.accidental_token = p.accidental = t 306 elif isinstance(t, ly.lex.lilypond.OctaveCheck): 307 p.octavecheck = octaveToNum(t) 308 p.octavecheck_token = t 309 break 310 elif not isinstance(t, ly.lex.Space): 311 break 312 yield p 313 if t is None: 314 break 315 else: 316 yield t 317 318 def position(self, t): 319 """Returns the cursor position for the given token or Pitch.""" 320 if isinstance(t, Pitch): 321 t = t.note_token 322 return self.source.position(t) 323 324 def write(self, pitch, language=None): 325 """Output a changed Pitch. 326 327 The Pitch is written in the Source's document. 328 329 To use this method reliably, you must instantiate the PitchIterator 330 with a ly.document.Source that has tokens_with_position set to True. 331 332 """ 333 document = self.source.document 334 pwriter = pitchWriter(language or self.language) 335 note = pwriter(pitch.note, pitch.alter) 336 end = pitch.note_token.end 337 if note != pitch.note_token: 338 document[pitch.note_token.pos:end] = note 339 octave = octaveToString(pitch.octave) 340 if octave != pitch.octave_token: 341 if pitch.octave_token is None: 342 document[end:end] = octave 343 else: 344 end = pitch.octave_token.end 345 document[pitch.octave_token.pos:end] = octave 346 if pitch.accidental: 347 if pitch.accidental_token is None: 348 document[end:end] = pitch.accidental 349 elif pitch.accidental != pitch.accidental_token: 350 end = pitch.accidental_token.end 351 document[pitch.accidental_token.pos:end] = pitch.accidental 352 elif pitch.accidental_token: 353 del document[pitch.accidental_token.pos:pitch.accidental_token.end] 354 if pitch.octavecheck is not None: 355 octavecheck = '=' + octaveToString(pitch.octavecheck) 356 if pitch.octavecheck_token is None: 357 document[end:end] = octavecheck 358 elif octavecheck != pitch.octavecheck_token: 359 document[pitch.octavecheck_token.pos:pitch.octavecheck_token.end] = octavecheck 360 elif pitch.octavecheck_token: 361 del document[pitch.octavecheck_token.pos:pitch.octavecheck_token.end] 362 363 364class LanguageName(ly.lex.Token): 365 """A Token that denotes a language name.""" 366 pass 367 368 369