1
# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
3
# Copyright (c) 2011 by Wilbert Berendsen
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.
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.
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.
21
Implementation of the tools to edit pitch of selected music.
24
from __future__ import unicode_literals
29
from PyQt4.QtGui import QMessageBox, QTextCursor
35
import ly.lex.lilypond
44
def changeLanguage(cursor, language):
45
"""Changes the language of the pitch names."""
46
selection = cursor.hasSelection()
48
start = cursor.selectionStart()
49
cursor.setPosition(cursor.selectionEnd())
50
cursor.setPosition(0, QTextCursor.KeepAnchor)
51
source = tokeniter.Source.selection(cursor)
53
source = tokeniter.Source.document(cursor)
55
pitches = PitchIterator(source)
56
tokens = pitches.tokens()
57
writer = ly.pitch.pitchWriter(language)
60
# consume tokens before the selection, following the language
61
source.consume(tokens, start)
63
changed = False # track change of \language or \include language command
64
with cursortools.editBlock(cursor):
66
with util.busyCursor():
67
with cursortools.Editor() as e:
69
if isinstance(t, ly.lex.lilypond.Note):
70
# translate the pitch name
75
e.insertText(source.cursor(t), n)
76
elif isinstance(t, LanguageName) and t != language:
77
# change the language name in a command
78
e.insertText(source.cursor(t), language)
80
except ly.pitch.PitchNameNotAvailable:
81
QMessageBox.critical(None, app.caption(_("Pitch Name Language")), _(
82
"Can't perform the requested translation.\n\n"
83
"The music contains quarter-tone alterations, but "
84
"those are not available in the pitch language \"{name}\"."
85
).format(name=language))
90
# there was no selection and no language command, so insert one
91
insertLanguage(cursor.document(), language)
93
# there was a selection but no command, user must insert manually.
94
QMessageBox.information(None, app.caption(_("Pitch Name Language")),
96
'<p><code>\\include "{1}.ly"</code> {2}</p>'
97
'<p><code>\\language "{1}"</code> {3}</p>'.format(
98
_("The pitch language of the selected text has been "
99
"updated, but you need to manually add the following "
100
"command to your document:"),
102
_("(for LilyPond below 2.14), or"),
103
_("(for LilyPond 2.14 and higher.)")))
106
def insertLanguage(document, language):
107
"""Inserts a language command in the document.
109
The command is inserted at the top or just below the version line.
110
If the document uses LilyPond < 2.13.38, the \\include command is used,
111
otherwise the newer \\language command.
114
version = (documentinfo.info(document).version()
115
or lilypondinfo.preferred().version)
116
if version and version < (2, 13, 38):
117
text = '\\include "{0}.ly"'
119
text = '\\language "{0}"'
120
# insert language command on top of file, but below version
121
block = document.firstBlock()
122
c = QTextCursor(block)
123
if '\\version' in tokeniter.tokens(block):
124
c.movePosition(QTextCursor.EndOfBlock)
128
c.insertText(text.format(language))
132
"""Converts pitches from relative to absolute."""
133
selection = cursor.hasSelection()
135
start = cursor.selectionStart()
136
cursor.setPosition(cursor.selectionEnd())
137
cursor.setPosition(0, QTextCursor.KeepAnchor)
138
source = tokeniter.Source.selection(cursor, True)
140
source = tokeniter.Source.document(cursor, True)
142
pitches = PitchIterator(source)
143
psource = pitches.pitches()
145
# consume tokens before the selection, following the language
146
t = source.consume(pitches.tokens(), start)
148
psource = itertools.chain((t,), psource)
150
# this class dispatches the tokens. we can't use a generator function
151
# as that doesn't like to be called again while there is already a body
159
while isinstance(t, (ly.lex.Space, ly.lex.Comment)):
161
if t == '\\relative' and isinstance(t, ly.lex.lilypond.Command):
164
elif isinstance(t, ly.lex.lilypond.MarkupScore):
173
def makeAbsolute(p, lastPitch):
174
"""Makes pitch absolute (honoring and removing possible octaveCheck)."""
175
if p.octaveCheck is not None:
176
p.octave = p.octaveCheck
179
p.makeAbsolute(lastPitch)
180
pitches.write(p, editor)
183
"""Consume tokens till the level drops (we exit a construct)."""
184
depth = source.state.depth()
187
if source.state.depth() < depth:
191
"""Consume tokens from context() returning the last token, if any."""
202
if isinstance(t, Pitch):
206
lastPitch = Pitch.c1()
208
# remove the \relative <pitch> tokens
209
c.setPosition(source.position(t), c.KeepAnchor)
210
editor.removeSelectedText(c)
213
# eat stuff like \new Staff == "bla" \new Voice \notes etc.
214
if isinstance(source.state.parser(), ly.lex.lilypond.ParseNewContext):
216
elif isinstance(t, (ly.lex.lilypond.ChordMode, ly.lex.lilypond.NoteMode)):
221
# now convert the relative expression to absolute
223
# Handle full music expression { ... } or << ... >>
225
# skip commands with pitches that do not count
226
if isinstance(t, ly.lex.lilypond.PitchCommand):
227
if t == '\\octaveCheck':
229
for p in getpitches(context()):
230
# remove the \octaveCheck
232
c.setPosition((p.octaveCursor or p.noteCursor).selectionEnd(), c.KeepAnchor)
233
editor.removeSelectedText(c)
237
elif isinstance(t, ly.lex.lilypond.ChordStart):
240
for p in getpitches(context()):
241
makeAbsolute(p, chord[-1])
243
lastPitch = chord[:2][-1] # same or first
244
elif isinstance(t, Pitch):
245
makeAbsolute(t, lastPitch)
247
elif isinstance(t, ly.lex.lilypond.ChordStart):
248
# Handle just one chord
249
for p in getpitches(context()):
250
makeAbsolute(p, lastPitch)
252
elif isinstance(t, Pitch):
253
# Handle just one pitch
254
makeAbsolute(t, lastPitch)
257
with util.busyCursor():
258
with cursortools.Editor() as editor:
264
"""Converts pitches from absolute to relative."""
265
selection = cursor.hasSelection()
267
start = cursor.selectionStart()
268
cursor.setPosition(cursor.selectionEnd())
269
cursor.setPosition(0, QTextCursor.KeepAnchor)
270
source = tokeniter.Source.selection(cursor, True)
272
source = tokeniter.Source.document(cursor, True)
274
pitches = PitchIterator(source)
275
psource = pitches.pitches()
277
# consume tokens before the selection, following the language
278
t = source.consume(pitches.tokens(), start)
280
psource = itertools.chain((t,), psource)
282
# this class dispatches the tokens. we can't use a generator function
283
# as that doesn't like to be called again while there is already a body
291
while isinstance(t, (ly.lex.Space, ly.lex.Comment)):
293
if t == '\\relative' and isinstance(t, ly.lex.lilypond.Command):
296
elif isinstance(t, ly.lex.lilypond.ChordMode):
297
consume() # do not change chords
299
elif isinstance(t, ly.lex.lilypond.MarkupScore):
309
"""Consume tokens till the level drops (we exit a construct)."""
310
depth = source.state.depth()
313
if source.state.depth() < depth:
317
"""Consume tokens from context() returning the last token, if any."""
324
"""Consume the whole \relative expression without doing anything. """
325
# skip pitch argument
327
if isinstance(t, Pitch):
331
# eat stuff like \new Staff == "bla" \new Voice \notes etc.
332
if isinstance(source.state.parser(), ly.lex.lilypond.ParseNewContext):
334
elif isinstance(t, ly.lex.lilypond.NoteMode):
339
if t in ('{', '<<', '<'):
343
with util.busyCursor():
344
with cursortools.Editor() as editor:
347
# Ok, parse current expression.
348
c = source.cursor(t, end=0) # insert the \relative command
352
# skip commands with pitches that do not count
353
if isinstance(t, ly.lex.lilypond.PitchCommand):
355
elif isinstance(t, ly.lex.lilypond.ChordStart):
358
elif isinstance(t, ly.lex.lilypond.ChordEnd):
362
elif isinstance(t, Pitch):
364
if lastPitch is None:
365
lastPitch = Pitch.c1()
366
lastPitch.octave = t.octave
368
lastPitch.octave += 1
370
"\\relative {0} ".format(
371
lastPitch.output(pitches.language)))
373
t.makeRelative(lastPitch)
374
pitches.write(t, editor)
376
# remember the first pitch of a chord
381
def transpose(cursor, mainwindow):
382
"""Transposes pitches."""
383
language = documentinfo.info(cursor.document()).pitchLanguage() or 'nederlands'
385
def readpitches(text):
386
"""Reads pitches from text."""
388
for pitch, octave in re.findall(r"([a-z]+)([,']*)", text):
389
r = ly.pitch.pitchReader(language)(pitch)
391
result.append(ly.pitch.Pitch(*r, octave=ly.pitch.octaveToNum(octave)))
395
"""Returns whether the text contains exactly two pitches."""
396
return len(readpitches(text)) == 2
398
text = inputdialog.getText(mainwindow, _("Transpose"), _(
399
"Please enter two absolute pitches, separated by a space, "
400
"using the pitch name language \"{language}\"."
401
).format(language=language), icon = icons.get('tools_transpose'),
402
help = transpose_help, validate = validate)
406
transposer = ly.pitch.Transposer(*readpitches(text))
408
selection = cursor.hasSelection()
410
start = cursor.selectionStart()
411
cursor.setPosition(cursor.selectionEnd())
412
cursor.setPosition(0, QTextCursor.KeepAnchor)
413
source = tokeniter.Source.selection(cursor, True)
415
source = tokeniter.Source.document(cursor, True)
417
pitches = PitchIterator(source)
418
psource = pitches.pitches()
422
self.inSelection = not selection
430
if isinstance(t, (ly.lex.Space, ly.lex.Comment)):
432
elif not self.inSelection and pitches.position(t) >= start:
433
self.inSelection = True
434
# Handle stuff that's the same in relative and absolute here
435
if t == "\\relative":
437
elif isinstance(t, ly.lex.lilypond.MarkupScore):
439
elif isinstance(t, ly.lex.lilypond.ChordMode):
441
elif isinstance(t, ly.lex.lilypond.PitchCommand):
442
if t == "\\transposition":
443
next(psource) # skip pitch
444
elif t == "\\transpose":
445
for p in getpitches(context()):
448
for p in getpitches(context()):
460
"""Consume tokens till the level drops (we exit a construct)."""
461
depth = source.state.depth()
464
if source.state.depth() < depth:
468
"""Consume tokens from context() returning the last token, if any."""
474
def transpose(p, resetOctave = None):
475
"""Transpose absolute pitch, using octave if given."""
476
transposer.transpose(p)
477
if resetOctave is not None:
478
p.octave = resetOctave
479
if tsource.inSelection:
480
pitches.write(p, editor)
483
"""Called inside \\chordmode or \\chords."""
484
for p in getpitches(context()):
487
def absolute(tokens):
488
"""Called when outside a possible \\relative environment."""
489
for p in getpitches(tokens):
493
"""Called when \\relative is encountered."""
494
def transposeRelative(p, lastPitch):
495
"""Transposes a relative pitch; returns the pitch in absolute form."""
496
# absolute pitch determined from untransposed pitch of lastPitch
497
p.makeAbsolute(lastPitch)
498
if not tsource.inSelection:
500
# we may change this pitch. Make it relative against the
501
# transposed lastPitch.
503
last = lastPitch.transposed
504
except AttributeError:
506
# transpose a copy and store that in the transposed
507
# attribute of lastPitch. Next time that is used for
508
# making the next pitch relative correctly.
509
newLastPitch = p.copy()
510
transposer.transpose(p)
511
newLastPitch.transposed = p.copy()
512
if p.octaveCheck is not None:
513
p.octaveCheck = p.octave
516
# we are allowed to change the pitch after the
517
# \relative command. lastPitch contains this pitch.
518
lastPitch.octave += p.octave
520
pitches.write(lastPitch, editor)
522
pitches.write(p, editor)
526
relPitch = [] # we use a list so it can be changed from inside functions
528
# find the pitch after the \relative command
530
if isinstance(t, Pitch):
532
if tsource.inSelection:
533
relPitch.append(lastPitch)
536
lastPitch = Pitch.c1()
539
# eat stuff like \new Staff == "bla" \new Voice \notes etc.
540
if isinstance(source.state.parser(), ly.lex.lilypond.ParseNewContext):
542
elif isinstance(t, ly.lex.lilypond.NoteMode):
547
# now transpose the relative expression
549
# Handle full music expression { ... } or << ... >>
551
if t == '\\octaveCheck':
552
for p in getpitches(context()):
555
if tsource.inSelection:
556
transposer.transpose(p)
557
lastPitch.transposed = p
558
pitches.write(p, editor)
559
elif isinstance(t, ly.lex.lilypond.ChordStart):
561
for p in getpitches(context()):
562
chord.append(transposeRelative(p, chord[-1]))
563
lastPitch = chord[:2][-1] # same or first
564
elif isinstance(t, Pitch):
565
lastPitch = transposeRelative(t, lastPitch)
566
elif isinstance(t, ly.lex.lilypond.ChordStart):
567
# Handle just one chord
568
for p in getpitches(context()):
569
lastPitch = transposeRelative(p, lastPitch)
570
elif isinstance(t, Pitch):
571
# Handle just one pitch
572
transposeRelative(token, lastPitch)
576
with util.busyCursor():
577
with cursortools.Editor() as editor:
579
except ly.pitch.PitchNameNotAvailable:
580
QMessageBox.critical(mainwindow, app.caption(_("Transpose")), _(
581
"Can't perform the requested transposition.\n\n"
582
"The transposed music would contain quarter-tone alterations "
583
"that are not available in the pitch language \"{language}\"."
584
).format(language = pitches.language))
587
class PitchIterator(object):
588
"""Iterate over notes or pitches in a source."""
590
def __init__(self, source):
591
"""Initializes us with a tokeniter.Source.
593
The language is set to "nederlands".
597
self.setLanguage("nederlands")
599
def setLanguage(self, lang):
600
"""Changes the pitch name language to use.
602
Called internally when \language or \include tokens are encoutered
603
with a valid language name/file.
605
Sets the language attribute to the language name and the read attribute
606
to an instance of ly.pitch.PitchReader.
609
if lang in ly.pitch.pitchInfo.keys():
613
def position(self, t):
614
"""Returns the cursor position for the given token or Pitch."""
615
if isinstance(t, Pitch):
616
return t.noteCursor.selectionStart()
618
return self.source.position(t)
621
"""Yield just all tokens from the source, following the language."""
622
for t in self.source:
624
if isinstance(t, ly.lex.lilypond.Keyword):
625
if t in ("\\include", "\\language"):
626
for t in self.source:
627
if not isinstance(t, ly.lex.Space) and t != '"':
628
lang = t[:-3] if t.endswith('.ly') else t[:]
629
if self.setLanguage(lang):
630
yield LanguageName(lang, t.pos)
635
"""Yields all tokens, but collects Note and Octave tokens.
637
When a Note is encoutered, also reads octave and octave check and then
638
a Pitch is yielded instead of the tokens.
641
tokens = self.tokens()
643
while isinstance(t, ly.lex.lilypond.Note):
649
p.noteCursor = self.source.cursor(t)
650
p.octaveCursor = self.source.cursor(t, start=len(t))
651
t = None # prevent hang in this loop
653
if isinstance(t, ly.lex.lilypond.OctaveCheck):
654
p.octaveCheck = p.origOctaveCheck = ly.pitch.octaveToNum(t)
655
p.octaveCheckCursor = self.source.cursor(t)
657
elif isinstance(t, ly.lex.lilypond.Octave):
658
p.octave = p.origOctave = ly.pitch.octaveToNum(t)
659
p.octaveCursor = self.source.cursor(t)
660
elif not isinstance(t, (ly.lex.Space, ly.lex.lilypond.Accidental)):
668
def read(self, token):
669
"""Reads the token and returns (note, alter) or None."""
670
return ly.pitch.pitchReader(self.language)(token)
672
def write(self, pitch, editor, language=None):
673
"""Outputs a changed Pitch to the cursortools.Editor."""
674
writer = ly.pitch.pitchWriter(language or self.language)
675
note = writer(pitch.note, pitch.alter)
676
if note != pitch.origNoteToken:
677
editor.insertText(pitch.noteCursor, note)
678
if pitch.octave != pitch.origOctave:
679
editor.insertText(pitch.octaveCursor, ly.pitch.octaveToString(pitch.octave))
680
if pitch.origOctaveCheck is not None:
681
if pitch.octaveCheck is None:
682
editor.removeSelectedText(pitch.octaveCheckCursor)
684
octaveCheck = '=' + ly.pitch.octaveToString(pitch.octaveCheck)
685
editor.insertText(pitch.octaveCheckCursor, octaveCheck)
688
class LanguageName(ly.lex.Token):
692
class Pitch(ly.pitch.Pitch):
693
"""A Pitch storing cursors for the note name, octave and octaveCheck."""
697
octaveCheckCursor = None
700
origOctaveCheck = None
703
def getpitches(iterable):
704
"""Consumes iterable but only yields Pitch instances."""
706
if isinstance(p, Pitch):
710
class pitch_help(help.page):
712
return _("Pitch manipulation")
717
Frescobaldi offers the following pitch-manipulating functions,
718
all in the menu {menu}:
723
<dt>Pitch language</dt>
725
This translates pitch names in the whole document or a selection.
728
<dt>Convert relative music to absolute</dt>
730
This converts all <code>\\relative</code> music parts to absolute pitch names.
731
It removes, but honours, octave checks.
734
<dt>Convert absolute music to relative</dt>
736
Checks all toplevel music expressions, changing them into
737
<code>\\relative</code> mode as soon as the expression contains a pitch.
738
If you want to make separate sub-expressions relative, it may be necessary to
739
select music from the first expression, leaving out higher-level opening
744
""").format(menu=help.menu(_("menu title", "Tools"), _("submenu title", "Pitch")))
747
return (transpose_help,)
750
class transpose_help(help.page):
752
return _("Transpose")
757
When transposing music, two absolute pitches need to be given to specify
758
the distance to transpose over. The pitches may include octave marks.
759
The pitches must be entered in the pitch name language used in the document.
763
The music will then be transposed from the first pitch to the second,
764
just as the <code>\\transpose</code> LilyPond command would do.
768
E.g. when transposing a minor third upwards, you would enter:<br />
773
To transpose down a major second, you can enter:<br />
783
It is also possible to use the transpose function to change a piece of music
784
from C-sharp to D-flat, or to specify quarter tones if supported in the
785
pitch name language that is used.
789
The transpose function can transpose both relative and absolute music,
790
correctly handling key signatures, chordmode and octave checks.