# 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.
"""
Transposing music.
"""
from __future__ import unicode_literals
from fractions import Fraction
import ly.lex.lilypond
[docs]class Transposer(object):
"""Transpose pitches.
Instantiate with a from- and to-Pitch, and optionally a scale.
The scale is a list with the pitch height of the unaltered step (0 .. 6).
The default scale is the normal scale: C, D, E, F, G, A, B.
"""
scale = (0, 1, 2, Fraction(5, 2), Fraction(7, 2), Fraction(9, 2), Fraction(11, 2))
def __init__(self, fromPitch, toPitch, scale = None):
if scale is not None:
self.scale = scale
# the number of octaves we need to transpose
self.octave = toPitch.octave - fromPitch.octave
# the number of base note steps (c->d == 1, e->f == 1, etc.)
self.steps = toPitch.note - fromPitch.note
# the number (fraction) of real whole steps
self.alter = (self.scale[toPitch.note] + toPitch.alter -
self.scale[fromPitch.note] - fromPitch.alter)
[docs] def transpose(self, pitch):
doct, note = divmod(pitch.note + self.steps, 7)
pitch.alter += self.alter - doct * 6 - self.scale[note] + self.scale[pitch.note]
pitch.octave += self.octave + doct
pitch.note = note
# change the step if alterations fall outside -1 .. 1
while pitch.alter > 1:
doct, note = divmod(pitch.note + 1, 7)
pitch.alter -= doct * 6 + self.scale[note] - self.scale[pitch.note]
pitch.octave += doct
pitch.note = note
while pitch.alter < -1:
doct, note = divmod(pitch.note - 1, 7)
pitch.alter += doct * -6 + self.scale[pitch.note] - self.scale[note]
pitch.octave += doct
pitch.note = note
[docs]class Simplifier(Transposer):
"""Make complicated accidentals simpler by substituting naturals where possible.
"""
def __init__(self, scale=None):
if scale is not None:
self.scale = scale
[docs] def transpose(self, pitch):
if pitch.alter == 1:
doct, note = divmod(pitch.note + 1, 7)
pitch.alter -= doct * 6 + self.scale[note] - self.scale[pitch.note]
pitch.octave += doct
pitch.note = note
elif pitch.alter == -1:
doct, note = divmod(pitch.note - 1, 7)
pitch.alter += doct * -6 + self.scale[pitch.note] - self.scale[note]
pitch.octave += doct
pitch.note = note
if pitch.alter == Fraction(1, 2):
doct, note = divmod(pitch.note + 1, 7)
alter = doct * 6 + self.scale[note] - self.scale[pitch.note]
if alter == Fraction(1, 2):
pitch.alter = 0
pitch.octave += doct
pitch.note = note
elif pitch.alter == Fraction(-1, 2):
doct, note = divmod(pitch.note - 1, 7)
alter = doct * -6 + self.scale[pitch.note] - self.scale[note]
if alter == Fraction(1, 2):
pitch.alter = 0
pitch.octave += doct
pitch.note = note
[docs]class ModeShifter(Transposer):
"""
Shift pitches to optional mode/scale.
The scale should be formatted in analogy to the scale in the Transposer
parent class.
The key should be an instance of ly.pitch.Pitch.
"""
def __init__(self, key, scale):
"""
Create scale of pitches from given scale definition.
"""
import math
self.octave = 0
self.modpitches = [0] * 7
for s, a in scale:
p = key.copy()
self.steps = s
self.alter = a
super(ModeShifter, self).transpose(p)
if self.modpitches[p.note]:
self.modpitches[p.note].append(p)
else:
self.modpitches[p.note] = [p]
[docs] def closestPitch(self, pitch):
"""
Get closest pitch from scale.
If only one scale note with the same base step exist that is returned.
Otherwise the closest is calculated.
"""
def getNextPitch(step, up=True):
modepitch = self.modpitches[step]
if modepitch:
return modepitch
else:
if up:
step = (step + 1) % 7
else:
step = (step - 1) % 7
return getNextPitch(step, up)
def comparePitch(pitch, uppitch, dwnpitch):
upnum = self.scale[uppitch.note] + uppitch.alter
dwnnum = self.scale[dwnpitch.note] + dwnpitch.alter
pnum = self.scale[pitch.note] + pitch.alter
if upnum - pnum < pnum - dwnnum:
return uppitch
else:
return dwnpitch
step = pitch.note
modepitch = self.modpitches[step]
if modepitch and len(modepitch) == 2:
return comparePitch(pitch, modepitch[0], modepitch[1])
else:
uppitch = getNextPitch(step)[0]
dwnpitch = getNextPitch(step, False)[-1]
return comparePitch(pitch, uppitch, dwnpitch)
[docs] def transpose(self, pitch):
"""
Shift to closest scale pitch if not already in scale.
"""
modpitch = self.modpitches[pitch.note]
if modpitch:
for mp in modpitch:
if pitch.note == mp.note and pitch.alter == mp.alter:
return
clp = self.closestPitch(pitch)
self.steps = clp.note - pitch.note
if self.steps > 3:
self.octave = -1
elif self.steps < -3:
self.octave = 1
else:
self.octave = 0
self.alter = (self.scale[clp.note] + clp.alter -
self.scale[pitch.note] - pitch.alter)
super(ModeShifter, self).transpose(pitch)
[docs]class ModalTransposer(object):
"""Transpose pitches by number of steps within a given scale.
Instantiate with the number of steps (+/-) in the scale to transpose by, and a mode index.
The mode index is the index of the major scale in the circle of fifths (C Major = 0).
"""
def __init__(self, numSteps = 1, scaleIndex = 0):
self.numSteps = numSteps
self.notes = [0, 1, 2, 3, 4, 5, 6]
self.alter = [-0.5, -0.5, -0.5, -0.5, -0.5, -0.5, -0.5]
# Initialize to Db, then update to desired mode
for i in range(0, scaleIndex):
keyNameIndex = ((i+1)*4)%len(self.notes)
accidentalIndex = (keyNameIndex-1)%len(self.notes)
self.alter[accidentalIndex] += .5
[docs] @staticmethod
def getKeyIndex(text):
"""Get the index of the key in the circle of fifths.
'Cb' returns 0, 'C' returns 7, 'B#' returns 14.
"""
circleOfFifths = ['Cb', 'Gb', 'Db', 'Ab', 'Eb', 'Bb', 'F',
'C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#']
return circleOfFifths.index(text.capitalize())
[docs] def transpose(self, pitch):
# Look for an exact match: otherwise,
# look for the letter name and save the accidental
for i in range(len(self.notes)):
if pitch.note == self.notes[i] and pitch.alter == self.alter:
fromScaleDeg = i
accidental = 0
break
else:
fromScaleDeg = self.notes.index(pitch.note)
accidental = pitch.alter - self.alter[fromScaleDeg]
toOctaveMod, toScaleDeg = divmod(fromScaleDeg + self.numSteps, 7)
pitch.note = self.notes[toScaleDeg]
pitch.alter = self.alter[toScaleDeg] + accidental
pitch.octave += toOctaveMod
[docs]def transpose(cursor, transposer, language="nederlands", relative_first_pitch_absolute=False):
"""Transpose pitches using the specified transposer.
If relative_first_pitch_absolute is True, the first pitch in a \\relative
expression is considered to be absolute, when a startpitch is not given.
This is LilyPond >= 2.18 behaviour.
If relative_first_pitch_absolute is False, the first pitch in a \\relative
expression is considered to be relative to c', is no startpitch is given.
This is LilyPond < 2.18 behaviour.
Currently, relative_first_pitch_absolute defaults to False.
"""
start = cursor.start
cursor.start = 0
source = ly.document.Source(cursor, True, tokens_with_position=True)
pitches = ly.pitch.PitchIterator(source, language)
psource = pitches.pitches()
class gen(object):
def __iter__(self):
return self
def __next__(self):
while True:
t = next(psource)
if isinstance(t, (ly.lex.Space, ly.lex.Comment)):
continue
# Handle stuff that's the same in relative and absolute here
if t == "\\relative":
relative()
elif isinstance(t, ly.lex.lilypond.MarkupScore):
absolute(context())
elif isinstance(t, ly.lex.lilypond.ChordMode):
chordmode()
elif isinstance(t, ly.lex.lilypond.Command) and t == '\\stringTuning':
string_tuning()
elif isinstance(t, ly.lex.lilypond.PitchCommand):
if t == "\\transposition":
next(psource) # skip pitch
elif t == "\\transpose":
for p in getpitches(context()):
transpose(p)
elif t == "\\key":
for p in getpitches(context()):
transpose(p, 0)
else:
return t
else:
return t
next = __next__
tsource = gen()
def in_selection(p):
"""Return True if the pitch or token p may be replaced, i.e. was selected."""
return start == 0 or pitches.position(p) >= start
def getpitches(iterable):
"""Consumes iterable but only yields Pitch instances."""
for p in iterable:
if isinstance(p, ly.pitch.Pitch):
yield p
def context():
"""Consume tokens till the level drops (we exit a construct)."""
depth = source.state.depth()
for t in tsource:
yield t
if source.state.depth() < depth:
return
def consume():
"""Consume tokens from context() returning the last token, if any."""
t = None
for t in context():
pass
return t
def transpose(p, resetOctave = None):
"""Transpose absolute pitch, using octave if given."""
transposer.transpose(p)
if resetOctave is not None:
p.octave = resetOctave
if in_selection(p):
pitches.write(p)
def chordmode():
r"""Called inside \chordmode or \chords."""
for p in getpitches(context()):
transpose(p, 0)
def string_tuning():
r"""Called after \stringTuning. Ignores the following chord expression."""
for t in tsource:
if isinstance(t, ly.lex.lilypond.ChordStart):
consume()
break
def absolute(tokens):
r"""Called when outside a possible \relative environment."""
for p in getpitches(tokens):
transpose(p)
def relative():
r"""Called when \relative is encountered."""
def transposeRelative(p, lastPitch):
"""Transposes a relative pitch; returns the pitch in absolute form."""
# absolute pitch determined from untransposed pitch of lastPitch
p.makeAbsolute(lastPitch)
if not in_selection(p):
return p
# we may change this pitch. Make it relative against the
# transposed lastPitch.
try:
last = lastPitch.transposed
except AttributeError:
last = lastPitch
# transpose a copy and store that in the transposed
# attribute of lastPitch. Next time that is used for
# making the next pitch relative correctly.
newLastPitch = p.copy()
transposer.transpose(p)
newLastPitch.transposed = p.copy()
if p.octavecheck is not None:
p.octavecheck = p.octave
p.makeRelative(last)
if relPitch:
# we are allowed to change the pitch after the
# \relative command. lastPitch contains this pitch.
lastPitch.octave += p.octave
p.octave = 0
pitches.write(lastPitch)
del relPitch[:]
pitches.write(p)
return newLastPitch
lastPitch = None
relPitch = [] # we use a list so it can be changed from inside functions
# find the pitch after the \relative command
t = next(tsource)
if isinstance(t, ly.pitch.Pitch):
lastPitch = t
if in_selection(t):
relPitch.append(lastPitch)
t = next(tsource)
elif relative_first_pitch_absolute:
lastPitch = ly.pitch.Pitch.f0()
else:
lastPitch = ly.pitch.Pitch.c1()
while True:
# eat stuff like \new Staff == "bla" \new Voice \notes etc.
if isinstance(source.state.parser(), ly.lex.lilypond.ParseTranslator):
t = consume()
elif isinstance(t, ly.lex.lilypond.NoteMode):
t = next(tsource)
else:
break
# now transpose the relative expression
if t in ('{', '<<'):
# Handle full music expression { ... } or << ... >>
for t in context():
if t == '\\octaveCheck':
for p in getpitches(context()):
lastPitch = p.copy()
del relPitch[:]
if in_selection(p):
transposer.transpose(p)
lastPitch.transposed = p
pitches.write(p)
elif isinstance(t, ly.lex.lilypond.ChordStart):
chord = [lastPitch]
for p in getpitches(context()):
chord.append(transposeRelative(p, chord[-1]))
lastPitch = chord[:2][-1] # same or first
elif isinstance(t, ly.pitch.Pitch):
lastPitch = transposeRelative(t, lastPitch)
elif isinstance(t, ly.lex.lilypond.ChordStart):
# Handle just one chord
for p in getpitches(context()):
lastPitch = transposeRelative(p, lastPitch)
elif isinstance(t, ly.pitch.Pitch):
# Handle just one pitch
transposeRelative(t, lastPitch)
# Do it!
with cursor.document as document:
absolute(tsource)