Source code for abjad.illustrators

import copy
import dataclasses
import typing

from . import bind as _bind
from . import duration as _duration
from . import enums as _enums
from . import format as _format
from . import get as _get
from . import indicators as _indicators
from . import iterate as _iterate
from . import lilypondfile as _lilypondfile
from . import makers as _makers
from . import metricmodulation as _metricmodulation
from . import overrides as _overrides
from . import pcollections as _pcollections
from . import pitch as _pitch
from . import score as _score
from . import select as _select
from . import spanners as _spanners
from . import tag as _tag
from . import timespan as _timespan


def _illustrate_component(component):
    block = _lilypondfile.Block(name="score")
    block.items.append(component)
    lilypond_file = _lilypondfile.LilyPondFile([block])
    return lilypond_file


def _illustrate_markup(markup):
    lilypond_file = _lilypondfile.LilyPondFile()
    markup = dataclasses.replace(markup)
    lilypond_file.items.append(markup)
    return lilypond_file


def _illustrate_markup_maker(argument, **keywords):
    markup = argument._make_markup(**keywords)
    return _illustrate_markup(markup)


def _illustrate_metric_modulation(metric_modulation):
    lilypond_file = _lilypondfile.LilyPondFile()
    markup = metric_modulation._get_markup()
    lilypond_file.items.append(markup)
    return lilypond_file


def _illustrate_pitch_class_set(set_):
    chord = _score.Chord(set_, _duration.Duration(1))
    voice = _score.Voice([chord], name="Voice")
    staff = _score.Staff([voice], name="Staff")
    score = _score.Score([staff], name="Score")
    lilypond_file = _lilypondfile.LilyPondFile([score])
    return lilypond_file


def _illustrate_pitch_range(range_):
    start_pitch = _pitch.NamedPitch(range_.start_pitch)
    stop_pitch = _pitch.NamedPitch(range_.stop_pitch)
    start_pitch_clef = _indicators.Clef.from_pitches([start_pitch])
    stop_pitch_clef = _indicators.Clef.from_pitches([stop_pitch])
    start_note = _score.Note(range_.start_pitch, 1)
    stop_note = _score.Note(range_.stop_pitch, 1)
    if start_pitch_clef == stop_pitch_clef:
        if start_pitch_clef == _indicators.Clef("bass"):
            bass_voice = _score.Voice(name="Bass_Voice")
            bass_staff = _score.Staff([bass_voice], name="Bass_Staff")
            bass_voice.extend([start_note, stop_note])
            _bind.attach(_indicators.Clef("bass"), start_note)
            bass_leaves = _select.leaves(bass_voice)
            _spanners.glissando(bass_leaves)
            score = _score.Score([bass_staff], name="Score")
        else:
            treble_voice = _score.Voice(name="Treble_Voice")
            treble_staff = _score.Staff([treble_voice], name="Treble_Staff")
            treble_voice.extend([start_note, stop_note])
            _bind.attach(_indicators.Clef("treble"), start_note)
            treble_leaves = _select.leaves(treble_voice)
            _spanners.glissando(treble_leaves)
            score = _score.Score([treble_staff], name="Score")
    else:
        score = make_piano_score()
        treble_voice, bass_voice = score["Treble_Voice"], score["Bass_Voice"]
        bass_voice.extend([start_note, stop_note])
        treble_voice.extend("s1 s1")
        bass_leaves = _select.leaves(bass_voice)
        _spanners.glissando(bass_leaves)
        _bind.attach(_indicators.StaffChange("Treble_Staff"), bass_voice[1])
        _bind.attach(_indicators.Clef("treble"), treble_voice[0])
        _bind.attach(_indicators.Clef("bass"), bass_voice[0])
    for leaf in _iterate.leaves(score):
        leaf.multiplier = (1, 4)
    _overrides.override(score).BarLine.stencil = False
    _overrides.override(score).SpanBar.stencil = False
    _overrides.override(score).Glissando.thickness = 2
    _overrides.override(score).TimeSignature.stencil = False
    lilypond_file = _lilypondfile.LilyPondFile([score])
    return lilypond_file


def _illustrate_pitch_segment(segment):
    named_pitches = [_pitch.NamedPitch(x) for x in segment]
    notes = _makers.make_notes(named_pitches, [1])
    score = make_piano_score(leaves=notes)
    _overrides.override(score).TimeSignature.stencil = False
    _overrides.override(score).BarLine.stencil = False
    _overrides.override(score).SpanBar.stencil = False
    for leaf in _iterate.leaves(score):
        leaf.multiplier = (1, 8)
    _overrides.override(score).Rest.transparent = True
    lilypond_file = _lilypondfile.LilyPondFile([score])
    return lilypond_file


def _illustrate_pitch_set(set_):
    upper, lower = [], []
    for pitch in set_:
        if pitch < 0:
            lower.append(pitch)
        else:
            upper.append(pitch)
    if upper:
        upper = _score.Chord(upper, _duration.Duration(1))
    else:
        upper = _score.Skip((1, 1))
    if lower:
        lower = _score.Chord(lower, _duration.Duration(1))
    else:
        lower = _score.Skip((1, 1))
    upper_voice = _score.Voice([upper], name="Treble_Voice")
    upper_staff = _score.Staff([upper_voice], name="Treble_Staff")
    lower_voice = _score.Voice([lower], name="Bass_Voice")
    lower_staff = _score.Staff([lower_voice], name="Bass_Staff")
    staff_group = _score.StaffGroup(
        [upper_staff, lower_staff],
        lilypond_type="PianoStaff",
        name="Piano_Staff",
    )
    score = _score.Score([staff_group], name="Score")
    lilypond_file = _lilypondfile.LilyPondFile([score])
    return lilypond_file


def _illustrate_pitch_class_segment(
    segment, markup_direction=_enums.UP, figure_name=None
):
    notes = []
    for item in segment:
        note = _score.Note(item, _duration.Duration(1, 8))
        notes.append(note)
    markup = None
    if isinstance(figure_name, str):
        figure_name = _indicators.Markup(rf"\markup {figure_name}")
    if figure_name is not None:
        markup = figure_name
    if markup is not None:
        direction = markup_direction
        markup = dataclasses.replace(markup, direction=direction)
        _bind.attach(markup, notes[0])
    voice = _score.Voice(notes, name="Voice")
    staff = _score.Staff([voice], name="Staff")
    score = _score.Score([staff], name="Score")
    preamble = r"""\layout {
    \accidentalStyle forget
    indent = 0
    \context {
        \Score
        \override BarLine.transparent = ##t
        \override BarNumber.stencil = ##f
        \override Beam.stencil = ##f
        \override Flag.stencil = ##f
        \override Stem.stencil = ##f
        \override TimeSignature.stencil = ##f
        proportionalNotationDuration = #(ly:make-moment 1 12)
    }
}

\paper {
    markup-system-spacing.padding = 8
    system-system-spacing.padding = 10
    top-markup-spacing.padding = 4
}"""
    leaf = _select.leaf(score, -1)
    bar_line = _indicators.BarLine("|.")
    _bind.attach(bar_line, leaf)
    string = r"\override Score.BarLine.transparent = ##f"
    command = _indicators.LilyPondLiteral(string, site="after")
    last_leaf = _select.leaf(score, -1)
    _bind.attach(command, last_leaf)
    lilypond_file = _lilypondfile.LilyPondFile([preamble, score])
    return lilypond_file


def _illustrate_timespan(timespan):
    timespans = _timespan.TimespanList([timespan])
    return _illustrate_markup_maker(timespans)


_class_to_method = dict(
    [
        (_score.Component, _illustrate_component),
        (_indicators.Markup, _illustrate_markup),
        (_metricmodulation.MetricModulation, _illustrate_metric_modulation),
        (_pcollections.PitchClassSegment, _illustrate_pitch_class_segment),
        (_pcollections.PitchSegment, _illustrate_pitch_segment),
        (_pcollections.PitchSet, _illustrate_pitch_set),
        (_timespan.OffsetCounter, _illustrate_markup_maker),
        (_pcollections.PitchRange, _illustrate_pitch_range),
        (_timespan.Timespan, _illustrate_timespan),
        (_timespan.TimespanList, _illustrate_markup_maker),
    ]
)


[docs]def attach_markup_struts(lilypond_file): """ LilyPond workaround. LilyPond's multisystem cropping currently removes intersystem whitespace. These transparent markup struts force LilyPond's cropping to preserve whitespace. """ rhythmic_staff = lilypond_file[_score.Score][-1] first_leaf = _get.leaf(rhythmic_staff, 0) markup = _indicators.Markup(r"\markup I", direction=_enums.UP) _bind.attach(markup, first_leaf) _overrides.tweaks(markup, r"- \tweak staff-padding 11") _overrides.tweaks(markup, r"- \tweak transparent ##t") duration = _get.duration(rhythmic_staff) if _duration.Duration(6, 4) < duration: last_leaf = _get.leaf(rhythmic_staff, -1) markup = _indicators.Markup(r"\markup I", direction=_enums.UP) _bind.attach(markup, last_leaf) _overrides.tweaks(markup, r"- \tweak staff-padding 18") _overrides.tweaks(markup, r"- \tweak transparent ##t")
[docs]def components( components: typing.Sequence[_score.Component], time_signatures: typing.Sequence[_indicators.TimeSignature] | None = None, *, includes: typing.Sequence[str] | None = None, ): """ Wraps ``components`` in LilyPond file for doc examples. Sets LilyPond ``proportionalSpacingDuration`` to 1/24. """ time_signatures = time_signatures or [] assert all(isinstance(_, _indicators.TimeSignature) for _ in time_signatures), repr( time_signatures ) voice = _score.Voice(components, name="Voice") staff = _score.Staff([voice], name="Staff") score = _score.Score([staff], name="Score", simultaneous=False) if not time_signatures: duration = _get.duration(components) time_signature = _indicators.TimeSignature(duration.pair) _bind.attach(time_signature, _select.leaf(components, 0)) else: leaves = _select.leaves(components, grace=False) durations = [_.duration for _ in time_signatures] parts = _select.partition_by_durations(leaves, durations) assert len(parts) == len(time_signatures) previous_time_signature = None for time_signature, part in zip(time_signatures, parts): assert isinstance(time_signature, _indicators.TimeSignature) if time_signature != previous_time_signature: _bind.attach(time_signature, _select.leaf(part, 0)) previous_time_signature = time_signature items: list[_score.Component | str] = [] items.append(r'\include "abjad.ily"') for include in includes or []: items.append(rf'\include "{include}"') string = r"""\layout { \context { \Score proportionalNotationDuration = #(ly:make-moment 1 24) } } """ items.append(string) items.append(score) lilypond_file = _lilypondfile.LilyPondFile(items) return lilypond_file
[docs]def components_to_score_markup_string(components: typing.Sequence[_score.Component]): """ Changes ``components`` to score markup string. """ assert all(isinstance(_, _score.Component) for _ in components), repr(components) components = copy.deepcopy(components) staff = _score.Staff(components, name="Rhythmic_Staff") staff.lilypond_type = "RhythmicStaff" staff.remove_commands.append("Time_signature_engraver") staff.remove_commands.append("Staff_symbol_engraver") _overrides.override(staff).Stem.direction = _enums.UP _overrides.override(staff).Stem.length = 5 _overrides.override(staff).TupletBracket.bracket_visibility = True _overrides.override(staff).TupletBracket.direction = _enums.UP _overrides.override(staff).TupletBracket.minimum_length = 4 _overrides.override(staff).TupletBracket.padding = 1.25 _overrides.override(staff).TupletBracket.shorten_pair = "#'(-1 . -1.5)" scheme = "#ly:spanner::set-spacing-rods" _overrides.override(staff).TupletBracket.springs_and_rods = scheme _overrides.override(staff).TupletNumber.font_size = 0 scheme = "#tuplet-number::calc-fraction-text" _overrides.override(staff).TupletNumber.text = scheme _overrides.setting(staff).tupletFullLength = True layout_block = _lilypondfile.Block("layout") layout_block.items.append("indent = 0") layout_block.items.append("ragged-right = ##t") score = _score.Score([staff], name="Score") _overrides.override(score).SpacingSpanner.spacing_increment = 0.5 _overrides.setting(score).proportionalNotationDuration = False indent = 4 * " " strings = [r"\score", indent + "{"] strings.extend([2 * indent + _ for _ in lilypond(score).split("\n")]) strings.extend([2 * indent + _ for _ in lilypond(layout_block).split("\n")]) strings.append(indent + "}") string = "\n".join(strings) return string
[docs]def illustrate(item, **keywords): """ Illustrates ``item``. """ method = None for key in _class_to_method: if isinstance(item, key): method = _class_to_method[key] break if method is None: raise Exception(f"can not illustrate objects of type {type(item)}.") return method(item, **keywords)
[docs]def lilypond(argument, *, site_comments=False, tags=False): """ Gets LilyPond format of ``argument``. """ if not hasattr(argument, "_get_lilypond_format"): raise Exception(f"no LilyPond format defined for {argument!r}.") string = argument._get_lilypond_format() if site_comments is False: string = _format.remove_site_comments(string) if tags is False: string = _tag.remove_tags(string) return string
[docs]def make_piano_score(leaves=None, lowest_treble_pitch="B3"): r""" Makes piano score from ``leaves``. .. container:: example REGRESSION. Function preserves tweaks: >>> note = abjad.Note("c'4") >>> abjad.tweak(note.note_head, r"\tweak color #red") >>> score = abjad.illustrators.make_piano_score([note]) >>> abjad.show(score) # doctest: +SKIP .. docs::: >>> string = abjad.lilypond(score) >>> print(string) \context Score = "Score" << \context PianoStaff = "Piano_Staff" << \context Staff = "Treble_Staff" { \context Voice = "Treble_Voice" { \clef "treble" \tweak color #red c'4 } } \context Staff = "Bass_Staff" { \context Voice = "Bass_Voice" { \clef "bass" r4 } } >> >> >>> chord = abjad.Chord("<c d a' bf'>4") >>> abjad.tweak(chord.note_heads[0], r"\tweak color #red") >>> abjad.tweak(chord.note_heads[1], r"\tweak color #red") >>> abjad.tweak(chord.note_heads[2], r"\tweak color #blue") >>> abjad.tweak(chord.note_heads[3], r"\tweak color #blue") >>> score = abjad.illustrators.make_piano_score([chord]) >>> abjad.show(score) # doctest: +SKIP .. docs::: >>> string = abjad.lilypond(score) >>> print(string) \context Score = "Score" << \context PianoStaff = "Piano_Staff" << \context Staff = "Treble_Staff" { \context Voice = "Treble_Voice" { \clef "treble" < \tweak color #blue a' \tweak color #blue bf' >4 } } \context Staff = "Bass_Staff" { \context Voice = "Bass_Voice" { \clef "bass" < \tweak color #red c \tweak color #red d >4 } } >> >> .. container:: example REGRESSION. Function preserves markup: >>> note = abjad.Chord("<c bf'>4") >>> markup = abjad.Markup(r"\markup loco") >>> abjad.attach(markup, chord, direction=abjad.UP) >>> markup = abjad.Markup(r"\markup ped.") >>> abjad.attach(markup, chord, direction=abjad.DOWN) >>> score = abjad.illustrators.make_piano_score([chord]) >>> abjad.show(score) # doctest: +SKIP .. docs::: >>> string = abjad.lilypond(score) >>> print(string) \context Score = "Score" << \context PianoStaff = "Piano_Staff" << \context Staff = "Treble_Staff" { \context Voice = "Treble_Voice" { \clef "treble" < \tweak color #blue a' \tweak color #blue bf' >4 ^ \markup loco } } \context Staff = "Bass_Staff" { \context Voice = "Bass_Voice" { \clef "bass" < \tweak color #red c \tweak color #red d >4 _ \markup ped. } } >> >> """ leaves = leaves or [] lowest_treble_pitch = _pitch.NamedPitch(lowest_treble_pitch) treble_voice = _score.Voice(name="Treble_Voice") treble_staff = _score.Staff([treble_voice], name="Treble_Staff") bass_voice = _score.Voice(name="Bass_Voice") bass_staff = _score.Staff([bass_voice], name="Bass_Staff") staff_group = _score.StaffGroup( [treble_staff, bass_staff], lilypond_type="PianoStaff", name="Piano_Staff", ) score = _score.Score([staff_group], name="Score") for leaf in leaves: markup_wrappers = _get.indicators(leaf, _indicators.Markup, unwrap=False) written_duration = leaf.written_duration if isinstance(leaf, _score.Note): if leaf.written_pitch < lowest_treble_pitch: treble_leaf = _score.Rest(written_duration) bass_leaf = copy.copy(leaf) else: treble_leaf = copy.copy(leaf) bass_leaf = _score.Rest(written_duration) elif isinstance(leaf, _score.Chord): treble_note_heads, bass_note_heads = [], [] for note_head in leaf.note_heads: new_note_head = copy.copy(note_head) if new_note_head.written_pitch < lowest_treble_pitch: bass_note_heads.append(new_note_head) else: treble_note_heads.append(new_note_head) if not treble_note_heads: treble_leaf = _score.Rest(written_duration) elif len(treble_note_heads) == 1: treble_leaf = _score.Note(treble_note_heads[0], written_duration) else: treble_leaf = _score.Chord(treble_note_heads, written_duration) if not bass_note_heads: bass_leaf = _score.Rest(written_duration) elif len(bass_note_heads) == 1: bass_leaf = _score.Note(bass_note_heads[0], written_duration) else: bass_leaf = _score.Chord(bass_note_heads, written_duration) else: treble_leaf = copy.copy(leaf) bass_leaf = copy.copy(leaf) for wrapper in markup_wrappers: markup = wrapper.unbundle_indicator() markup_copy = copy.copy(markup) if wrapper.direction in (_enums.UP, None): _bind.attach(markup_copy, treble_leaf, direction=wrapper.direction) else: _bind.attach(markup_copy, bass_leaf, direction=wrapper.direction) treble_voice.append(treble_leaf) bass_voice.append(bass_leaf) if 0 < len(treble_voice): clef = _indicators.Clef("treble") _bind.attach(clef, treble_voice[0]) if 0 < len(bass_voice): clef = _indicators.Clef("bass") _bind.attach(clef, bass_voice[0]) return score