Source code for ly.rhythm

# This file is part of python-ly, https://pypi.python.org/pypi/python-ly
#
# Copyright (c) 2011 - 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.

"""
Implementation of the tools to edit durations of selected music.

Durations are represented simply by lists of ly.lex.lilypond.Duration tokens.

All functions expect a ly.document.Cursor with the selected range.

"""

from __future__ import unicode_literals

import collections
import itertools

import ly.document
import ly.lex.lilypond


durations = ['\\maxima', '\\longa', '\\breve',
    '1', '2', '4', '8', '16', '32', '64', '128', '256', '512', '1024', '2048']


[docs]def remove_dups(iterable): """Change reoccurring strings to '' in iterable.""" old = None for i in iterable: yield '' if i == old else i old = i
# describes a musical item that has a duration music_item = collections.namedtuple('music_item', ( 'tokens', # tokens of the item 'dur_tokens', # Duration tokens of the item 'may_remove', # whether the duration may be removed 'insert_pos', # where a Duration could be inserted 'pos', # position of the first token 'end', # end position of the last token )) _start = ( ly.lex.lilypond.Rest, ly.lex.lilypond.Skip, ly.lex.lilypond.Note, ly.lex.lilypond.ChordEnd, ly.lex.lilypond.Q, ly.lex.lilypond.Octave, ly.lex.lilypond.Accidental, ly.lex.lilypond.OctaveCheck, ly.lex.lilypond.Duration, ly.lex.lilypond.Tempo, ly.lex.lilypond.Partial ) _stay = ( ly.lex.lilypond.Octave, ly.lex.lilypond.Accidental, ly.lex.lilypond.OctaveCheck, ly.lex.lilypond.Duration, ly.lex.lilypond.Tie, )
[docs]def music_tokens(source, command=False, chord=False): r"""DEPRECATED. Yield lists of tokens describing rests, skips or pitches. source is a ly.document.Source instance following the state. The following keyword arguments can be used: - command: whether to allow pitches in \\relative, \\transpose, etc. - chord: whether to allow pitches inside chords. This function is deprecated and will be removed. You should use music_items() instead. """ skip_parsers = () if not command: skip_parsers += (ly.lex.lilypond.ParsePitchCommand,) if not chord: skip_parsers += (ly.lex.lilypond.ParseChord,) for token in source: if isinstance(source.state.parser(), skip_parsers): continue # make sure to skip the duration tokens in a \tuplet command if token == '\\tuplet': for token in source: if isinstance(token, ly.lex.lilypond.Duration): for token in source: if not isinstance(token, ly.lex.lilypond.Duration): break break elif not isinstance(token, (ly.lex.Space, ly.lex.Numeric)): break while isinstance(token, _start): l = [token] for token in source: if isinstance(token, ly.lex.Space): continue if not isinstance(token, _stay): yield l break l.append(token) else: yield l break
[docs]def music_items(cursor, command=False, chord=False, partial=ly.document.INSIDE): r"""Yield music_item instances describing rests, skips or pitches. cursor is a ly.document.Cursor instance. The following keyword arguments can be used: - command: whether to allow pitches in \\relative, \\transpose, etc. - chord: whether to allow pitches inside chords. - partial: ly.document.INSIDE (default), PARTIAL or OUTSIDE. See the documentation of ly.document.Source.__init__(). """ skip_parsers = () if not command: skip_parsers += (ly.lex.lilypond.ParsePitchCommand,) if not chord: skip_parsers += (ly.lex.lilypond.ParseChord,) source = ly.document.Source(cursor, True, partial=partial, tokens_with_position=True) def mk_item(l): """Convert a list of tokens to a music_item instance.""" tokens = [] dur_tokens = [] pos = l[0].pos end = l[-1].end for t in l: if isinstance(t, ly.lex.lilypond.Duration): dur_tokens.append(t) else: tokens.append(t) may_remove = not any(map(('\\skip', '\\tempo', '\\tuplet', '\\partial').__contains__, tokens)) if dur_tokens: insert_pos = dur_tokens[0].pos else: for t in reversed(tokens): if not isinstance(t, ly.lex.lilypond.Tie): break insert_pos = t.end return music_item(tokens, dur_tokens, may_remove, insert_pos, pos, end) for token in source: if isinstance(source.state.parser(), skip_parsers): continue # make sure to skip the duration tokens in a \tuplet command if token == '\\tuplet': l = [token] for token in source: if isinstance(token, ly.lex.lilypond.Duration): l.append(token) for token in source: if not isinstance(token, ly.lex.lilypond.Duration): break l.append(token) break elif isinstance(token, ly.lex.Numeric): l.append(token) elif not isinstance(token, ly.lex.Space): break yield mk_item(l) length_seen = False while isinstance(token, _start): l = [token] if isinstance(token, ly.lex.lilypond.Length): length_seen = True for token in source: if isinstance(token, ly.lex.lilypond.Length): if length_seen is True: yield mk_item(l) length_seen = False break else: length_seen = True elif isinstance(token, ly.lex.Space): continue elif isinstance(token, ly.lex.lilypond.ChordSeparator): # prevent seeing the g in e.g. chordmode { c/g } for token in source: if not isinstance(token, (ly.lex.Space, ly.lex.lilypond.Note)): break continue elif not isinstance(token, _stay): yield mk_item(l) length_seen = False break l.append(token) else: yield mk_item(l) break
[docs]def preceding_duration(cursor): """Return a preceding duration before the cursor, or an empty list.""" tokens = ly.document.Runner.at(cursor).backward() for t in tokens: if isinstance(t, ly.lex.lilypond.Duration): l = [t] for t in tokens: if isinstance(t, ly.lex.lilypond.Duration): l.append(t) elif not isinstance(t, ly.lex.Space): break l.reverse() return l return []
[docs]def rhythm_double(cursor): """Doubles all duration values.""" with cursor.document as d: for item in music_items(cursor): for token in item.dur_tokens: if isinstance(token, ly.lex.lilypond.Length): try: i = durations.index(token) except ValueError: pass else: if i > 0: d[token.pos:token.end] = durations[i - 1] break
[docs]def rhythm_halve(cursor): """Halves all duration values.""" with cursor.document as d: for item in music_items(cursor): for token in item.dur_tokens: if isinstance(token, ly.lex.lilypond.Length): try: i = durations.index(token) except ValueError: pass else: if i < len(durations) - 1: d[token.pos:token.end] = durations[i + 1] break
[docs]def rhythm_dot(cursor): """Add a dot to all durations.""" with cursor.document as d: for item in music_items(cursor): for token in item.dur_tokens: if isinstance(token, ly.lex.lilypond.Length): d[token.end:token.end] = "." break
[docs]def rhythm_undot(cursor): """Remove one dot from all durations.""" with cursor.document as d: for item in music_items(cursor): for token in item.dur_tokens: if isinstance(token, ly.lex.lilypond.Dot): del d[token.pos:token.end] break
[docs]def rhythm_remove_scaling(cursor): """Remove the scaling (like ``*3``, ``*1/3``) from all durations.""" with cursor.document as d: for item in music_items(cursor): for token in item.dur_tokens: if isinstance(token, ly.lex.lilypond.Scaling): del d[token.pos:token.end]
[docs]def rhythm_remove_fraction_scaling(cursor): """Remove the scaling containing fractions (like ``*1/3``) from all durations.""" with cursor.document as d: for item in music_items(cursor): for token in item.dur_tokens: if isinstance(token, ly.lex.lilypond.Scaling) and '/' in token: del d[token.pos:token.end]
[docs]def rhythm_remove(cursor): """Remove all durations.""" with cursor.document as d: for item in music_items(cursor): if item.dur_tokens and item.may_remove: del d[item.dur_tokens[0].pos:item.dur_tokens[-1].end]
[docs]def rhythm_implicit(cursor): """Remove reoccurring durations.""" items = music_items(cursor) for item in items: break else: return prev = item.dur_tokens or preceding_duration(cursor) with cursor.document as d: for item in items: if not set(item.tokens) & set(('\\tempo', '\\tuplet', '\\partial')): if item.dur_tokens: if item.dur_tokens == prev and item.may_remove: del d[item.dur_tokens[0].pos:item.dur_tokens[-1].end] prev = item.dur_tokens
[docs]def rhythm_implicit_per_line(cursor): """Remove reoccurring durations, but always write one on a new line.""" items = music_items(cursor) for item in items: break else: return prev = item.dur_tokens or preceding_duration(cursor) previous_block = cursor.document.block(prev[0].pos) with cursor.document as d: for item in items: if not set(item.tokens) & set(('\\tempo', '\\tuplet', '\\partial')): block = d.block( (item.dur_tokens or item.tokens) [0].pos) if block != previous_block: if not item.dur_tokens: d[item.insert_pos:item.insert_pos] = ''.join(prev) else: prev = item.dur_tokens previous_block = block elif item.dur_tokens: if item.dur_tokens == prev and item.may_remove: del d[item.dur_tokens[0].pos:item.dur_tokens[-1].end] prev = item.dur_tokens
[docs]def rhythm_explicit(cursor): """Make all durations explicit.""" items = music_items(cursor) for item in items: break else: return prev = item.dur_tokens or preceding_duration(cursor) with cursor.document as d: for item in items: if not set(item.tokens) & set(('\\tempo', '\\tuplet', '\\partial')): if item.dur_tokens: prev = item.dur_tokens else: d[item.insert_pos:item.insert_pos] = ''.join(prev)
[docs]def rhythm_overwrite(cursor, durations): """Apply a list of durations to the cursor's range. The durations list looks like ["4", "8", "", "16.",] etc. """ durations_source = remove_dups(itertools.cycle(durations)) with cursor.document as d: for item in music_items(cursor): pos = item.insert_pos end = item.dur_tokens[-1].end if item.dur_tokens else pos d[pos:end] = next(durations_source)
[docs]def rhythm_extract(cursor): """Return a list of the durations from the cursor's range.""" source = ly.document.Source(cursor, True) durations = [] for item in music_items(cursor): tokens = item.dur_tokens + [t for t in item.tokens if isinstance(t, ly.lex.lilypond.Tie)] durations.append(tokens) # if the first duration was not given, find it if durations and not durations[0]: durations[0] = preceding_duration(cursor) or ['4'] return ["".join(tokens) for tokens in durations]