10. Creating annotation-aware spanners

Key points:

  1. Investigate the internals of Abjad’s Spanner class.
  2. Create a simple annotative indicator.
  3. Build up a complex class, then refactor it.
  4. Learn about Abjad’s LilyPondGrobOverride and Enumeration classes.
  5. Use rhythm-makers and selectors to help audition the custom spanner.

10.1. Basic glissando functionality

>>> staff = Staff("g'4. d''8 b'2 b'8 r8 f''4. d'8. f'16 r8")
>>> show(staff)
>>> print(format(staff))
\new Staff {
    g'4.
    d''8
    b'2
    b'8
    r8
    f''4.
    d'8.
    f'16
    r8
}
class OscillationSpanner(spannertools.Spanner):

    def _get_lilypond_format_bundle(self, leaf):
        lilypond_format_bundle = self._get_basic_lilypond_format_bundle(leaf)
        lilypond_format_bundle.right.spanner_starts.append(r'\glissando')
        return lilypond_format_bundle
>>> spanner = OscillationSpanner()
>>> attach(spanner, staff[:])
>>> show(staff)
>>> print(format(staff))
\new Staff {
    g'4. \glissando
    d''8 \glissando
    b'2 \glissando
    b'8 \glissando
    r8 \glissando
    f''4. \glissando
    d'8. \glissando
    f'16 \glissando
    r8 \glissando
}

10.2. Avoiding orphan and final leaves

>>> for leaf in staff:
...     is_first = spanner._is_my_first_leaf(leaf)
...     is_last = spanner._is_my_last_leaf(leaf)
...     print(repr(leaf), is_first, is_last)
... 
Note("g'4.") True False
Note("d''8") False False
Note("b'2") False False
Note("b'8") False False
Rest('r8') False False
Note("f''4.") False False
Note("d'8.") False False
Note("f'16") False False
Rest('r8') False True
class OscillationSpanner(spannertools.Spanner):

    def _get_lilypond_format_bundle(self, leaf):
        lilypond_format_bundle = self._get_basic_lilypond_format_bundle(leaf)
        if self._is_my_last_leaf(leaf) or self._is_my_only_leaf(leaf):
            return lilypond_format_bundle
        lilypond_format_bundle.right.spanner_starts.append(r'\glissando')
        return lilypond_format_bundle
>>> staff = Staff("g'4. d''8 b'2 b'8 r8 f''4. d'8. f'16 r8")
>>> spanner = OscillationSpanner()
>>> attach(spanner, staff[:])
>>> show(staff)
>>> print(format(staff))
\new Staff {
    g'4. \glissando
    d''8 \glissando
    b'2 \glissando
    b'8 \glissando
    r8 \glissando
    f''4. \glissando
    d'8. \glissando
    f'16 \glissando
    r8
}

10.3. Avoiding silences

class OscillationSpanner(spannertools.Spanner):

    def _get_lilypond_format_bundle(self, leaf):
        lilypond_format_bundle = self._get_basic_lilypond_format_bundle(leaf)
        if self._is_my_last_leaf(leaf) or self._is_my_only_leaf(leaf):
            return lilypond_format_bundle
        prototype = (scoretools.Chord, scoretools.Note)
        next_leaf = inspect_(leaf).get_leaf(1)
        if isinstance(leaf, prototype) and isinstance(next_leaf, prototype):
            lilypond_format_bundle.right.spanner_starts.append(r'\glissando')
        return lilypond_format_bundle
>>> staff = Staff("g'4. d''8 b'2 b'8 r8 f''4. d'8. f'16 r8")
>>> spanner = OscillationSpanner()
>>> attach(spanner, staff[:])
>>> show(staff)
>>> print(format(staff))
\new Staff {
    g'4. \glissando
    d''8 \glissando
    b'2 \glissando
    b'8
    r8
    f''4. \glissando
    d'8. \glissando
    f'16
    r8
}

10.4. Making object-oriented typographic overrides

>>> staff = Staff("c'4 d'4 e'4 f'4")
>>> override(staff[1]).note_head.style = 'cross'
>>> show(staff)
>>> print(format(staff))
\new Staff {
    c'4
    \once \override NoteHead.style = #'cross
    d'4
    e'4
    f'4
}
>>> grob_override = lilypondnametools.LilyPondGrobOverride(
...     grob_name='NoteHead',
...     is_once=True,
...     property_path='style',
...     value=schemetools.SchemeSymbol('cross'),
...     )
>>> attach(grob_override, staff[2])
>>> show(staff)
>>> print(format(staff))
\new Staff {
    c'4
    \once \override NoteHead.style = #'cross
    d'4
    \once \override NoteHead.style = #'cross
    e'4
    f'4
}
>>> zigzag_override = lilypondnametools.LilyPondGrobOverride(
...     grob_name='Glissando',
...     property_path='style',
...     value=schemetools.SchemeSymbol('zigzag'),
...     )
>>> zigzag_override.override_string
"\\override Glissando.style = #'zigzag"
>>> zigzag_override.revert_string
'\\revert Glissando.style'

10.5. Integrating overrides during formatting

class OscillationSpanner(spannertools.Spanner):

    def _get_lilypond_format_bundle(self, leaf):
        lilypond_format_bundle = self._get_basic_lilypond_format_bundle(leaf)
        if self._is_my_only_leaf(leaf):
            return lilypond_format_bundle
        if self._is_my_first_leaf(leaf):
            grob_override = lilypondnametools.LilyPondGrobOverride(
                grob_name='Glissando',
                property_path='style',
                value=schemetools.SchemeSymbol('zigzag'),
                )
            override_string = grob_override.override_string
            lilypond_format_bundle.grob_overrides.append(override_string)
        if self._is_my_last_leaf(leaf):
            grob_override = lilypondnametools.LilyPondGrobOverride(
                grob_name='Glissando',
                property_path='style',
                )
            revert_string = grob_override.revert_string
            lilypond_format_bundle.grob_reverts.append(revert_string)
            return lilypond_format_bundle
        prototype = (scoretools.Chord, scoretools.Note)
        next_leaf = inspect_(leaf).get_leaf(1)
        if isinstance(leaf, prototype) and isinstance(next_leaf, prototype):
            lilypond_format_bundle.right.spanner_starts.append(r'\glissando')
        return lilypond_format_bundle
>>> staff = Staff("g'4. d''8 b'2 b'8 r8 f''4. d'8. f'16 r8")
>>> spanner = OscillationSpanner()
>>> attach(spanner, staff[:])
>>> show(staff)
>>> print(format(staff))
\new Staff {
    \override Glissando.style = #'zigzag
    g'4. \glissando
    d''8 \glissando
    b'2 \glissando
    b'8
    r8
    f''4. \glissando
    d'8. \glissando
    f'16
    r8
    \revert Glissando.style
}

10.6. A simple non-formatting annotation class

class OscillationSize(datastructuretools.Enumeration):
    NONE = 0
    SMALL = 1
    MEDIUM = 2
    LARGE = 3
def make_annotated_staff():
    staff = Staff("g'4. d''8 b'2 b'8 r8 f''4. d'8. f'16 r8")
    attach(OscillationSize.LARGE, staff[0])
    attach(OscillationSize.MEDIUM, staff[1])
    attach(OscillationSize.SMALL, staff[2])
    attach(OscillationSize.NONE, staff[5])
    attach(OscillationSize.MEDIUM, staff[6])
    return staff
>>> staff = make_annotated_staff()
>>> show(staff)
>>> print(format(staff))
\new Staff {
    g'4.
    d''8
    b'2
    b'8
    r8
    f''4.
    d'8.
    f'16
    r8
}

10.7. Making the spanner annotation-aware

class OscillationSpanner(spannertools.Spanner):

    def _get_lilypond_format_bundle(self, leaf):
        lilypond_format_bundle = self._get_basic_lilypond_format_bundle(leaf)
        if self._is_my_only_leaf(leaf):
            return lilypond_format_bundle
        if self._is_my_first_leaf(leaf):
            grob_override = lilypondnametools.LilyPondGrobOverride(
                grob_name='Glissando',
                property_path='style',
                value=schemetools.SchemeSymbol('zigzag'),
                )
            override_string = grob_override.override_string
            lilypond_format_bundle.grob_overrides.append(override_string)
        if self._is_my_last_leaf(leaf):
            grob_override = lilypondnametools.LilyPondGrobOverride(
                grob_name='Glissando',
                property_path='style',
                )
            revert_string = grob_override.revert_string
            lilypond_format_bundle.grob_reverts.append(revert_string)
            return lilypond_format_bundle
        prototype = (scoretools.Chord, scoretools.Note)
        next_leaf = inspect_(leaf).get_leaf(1)
        if isinstance(leaf, prototype) and isinstance(next_leaf, prototype):
            lilypond_format_bundle.right.spanner_starts.append(r'\glissando')
            annotations = inspect_(leaf).get_indicators(OscillationSize)
            if not annotations:
                annotations = [OscillationSize.SMALL]
            annotation = annotations[0]
            zigzag_width = int(annotation)
            if zigzag_width:
                zigzag_width_override = lilypondnametools.LilyPondGrobOverride(
                    grob_name='Glissando',
                    is_once=True,
                    property_path='zigzag-width',
                    value=zigzag_width,
                    )
                override_string = zigzag_width_override.override_string
            else:
                zigzag_off_override = lilypondnametools.LilyPondGrobOverride(
                    grob_name='Glissando',
                    is_once=True,
                    property_path='style',
                    value=schemetools.SchemeSymbol('line'),
                    )
                override_string = zigzag_off_override.override_string
            lilypond_format_bundle.grob_overrides.append(override_string)
        return lilypond_format_bundle
>>> staff = make_annotated_staff()
>>> spanner = OscillationSpanner()
>>> attach(spanner, staff[:])
>>> show(staff)
>>> print(format(staff))
\new Staff {
    \once \override Glissando.zigzag-width = 3
    \override Glissando.style = #'zigzag
    g'4. \glissando
    \once \override Glissando.zigzag-width = 2
    d''8 \glissando
    \once \override Glissando.zigzag-width = 1
    b'2 \glissando
    b'8
    r8
    \once \override Glissando.style = #'line
    f''4. \glissando
    \once \override Glissando.zigzag-width = 2
    d'8. \glissando
    f'16
    r8
    \revert Glissando.style
}

10.8. Refactoring the custom spanner class

class OscillationSpanner(spannertools.Spanner):

    class Size(datastructuretools.Enumeration):
        NONE = 0
        SMALL = 1
        MEDIUM = 2
        LARGE = 3

    def _apply_annotation_overrides(self, leaf, lilypond_format_bundle):
        annotation = self._get_annotation(leaf)
        zigzag_width = int(annotation)
        if zigzag_width:
            zigzag_width_override = lilypondnametools.LilyPondGrobOverride(
                grob_name='Glissando',
                is_once=True,
                property_path='zigzag-width',
                value=zigzag_width,
                )
            override_string = zigzag_width_override.override_string
        else:
            zigzag_off_override = lilypondnametools.LilyPondGrobOverride(
                grob_name='Glissando',
                is_once=True,
                property_path='style',
                value=schemetools.SchemeSymbol('line'),
                )
            override_string = zigzag_off_override.override_string
        lilypond_format_bundle.grob_overrides.append(override_string)

    def _apply_spanner_start_overrides(self, lilypond_format_bundle):
        grob_override = lilypondnametools.LilyPondGrobOverride(
            grob_name='Glissando',
            property_path='style',
            value=schemetools.SchemeSymbol('zigzag'),
            )
        override_string = grob_override.override_string
        lilypond_format_bundle.grob_overrides.append(override_string)

    def _apply_spanner_stop_overrides(self, lilypond_format_bundle):
        grob_override = lilypondnametools.LilyPondGrobOverride(
            grob_name='Glissando',
            property_path='style',
            )
        revert_string = grob_override.revert_string
        lilypond_format_bundle.grob_reverts.append(revert_string)

    def _get_annotation(self, leaf):
        annotations = inspect_(leaf).get_indicators(self.Size)
        if not annotations:
            annotations = [self.Size.SMALL]
        return annotations[0]

    def _get_lilypond_format_bundle(self, leaf):
        lilypond_format_bundle = self._get_basic_lilypond_format_bundle(leaf)
        if self._is_my_only_leaf(leaf):
            return lilypond_format_bundle
        if self._is_my_first_leaf(leaf):
            self._apply_spanner_start_overrides(lilypond_format_bundle)
        if self._is_my_last_leaf(leaf):
            self._apply_spanner_stop_overrides(lilypond_format_bundle)
            return lilypond_format_bundle
        prototype = (scoretools.Chord, scoretools.Note)
        next_leaf = inspect_(leaf).get_leaf(1)
        if isinstance(leaf, prototype) and isinstance(next_leaf, prototype):
            lilypond_format_bundle.right.spanner_starts.append(r'\glissando')
            self._apply_annotation_overrides(leaf, lilypond_format_bundle)
        return lilypond_format_bundle

10.9. Preparing for deployment

>>> staff = Staff("g'4. d''8 b'2 b'8 r8 f''4. d'8. f'16 r8")
>>> selector = selectortools.Selector().by_leaf().by_run(Note)[:-1].flatten()
>>> selector = selectortools.Selector()
>>> for x in selector(staff):
...     x
... 
Staff("g'4. d''8 b'2 b'8 r8 f''4. d'8. f'16 r8")
>>> selector = selector.by_leaf()
>>> for x in selector(staff):
...     x
... 
Selection([Note("g'4."), Note("d''8"), Note("b'2"), Note("b'8"), Rest('r8'), Note("f''4."), Note("d'8."), Note("f'16"), Rest('r8')])
>>> selector = selector.by_run(Note)
>>> for x in selector(staff):
...     x
... 
Selection([Note("g'4."), Note("d''8"), Note("b'2"), Note("b'8")])
Selection([Note("f''4."), Note("d'8."), Note("f'16")])
>>> selector = selector[:-1]
>>> for x in selector(staff):
...     x
... 
Selection([Note("g'4."), Note("d''8"), Note("b'2")])
Selection([Note("f''4."), Note("d'8.")])
>>> selector = selector.flatten()
>>> for x in selector(staff):
...     x
... 
Note("g'4.")
Note("d''8")
Note("b'2")
Note("f''4.")
Note("d'8.")
>>> annotations = datastructuretools.CyclicTuple([
...     OscillationSpanner.Size.LARGE,
...     OscillationSpanner.Size.MEDIUM,
...     OscillationSpanner.Size.SMALL,
...     OscillationSpanner.Size.NONE,
...     ])
>>> annotations[0]
Size.LARGE
>>> annotations[23]
Size.NONE
>>> annotations[973]
Size.MEDIUM
>>> attach(OscillationSpanner(), staff)
>>> for i, leaf in enumerate(selector(staff)):
...     attach(annotations[i], leaf)
... 
>>> show(staff)

10.10. Deploying the spanner

>>> talea_rhythm_maker = rhythmmakertools.TaleaRhythmMaker(
...     burnish_specifier=rhythmmakertools.BurnishSpecifier(
...         left_classes=[Rest],
...         left_counts=[0, 1],
...         right_classes=[Rest],
...         right_counts=[0, 0, 1],
...         ),
...     extra_counts_per_division=[1, 0, 0],
...     talea=rhythmmakertools.Talea(
...         counts=[2, 3, 1, 3, 1, 4, 2, 2],
...         denominator=8,
...         ),
...     tie_split_notes=False,
...     )
>>> divisions = [(5, 8), (7, 8), (4, 8), (6, 8), (5, 4), (4, 4), (3, 4)]
>>> selections = talea_rhythm_maker(divisions)
>>> measures = Measure.from_selections(selections, time_signatures=divisions)
>>> staff = Staff(measures)
>>> show(staff)

All of the notes’ pitches are middle-C, so we’ll apply some pitches cyclically to each logical tie:

>>> pitches = datastructuretools.CyclicTuple(
...     ["b'", "d''", "g'", "f''", "b'", "g'", "c'", "e'", "g'"],
...     )
>>> for i, logical_tie in enumerate(iterate(staff).by_logical_tie(pitched=True)):
...     for note in logical_tie:
...         note.written_pitch = pitches[i]
... 

Now we apply the OscillationSpanner and the cyclic sequence of OscillationSpanner.Size annotations:

>>> attach(OscillationSpanner(), staff)
>>> for i, leaf in enumerate(selector(staff)):
...     attach(annotations[i], leaf)
... 

The result?

>>> show(staff)

Now that we know the ingredients required, we can package the entire staff-creation process into a function and run it with different variations, via rotation:

def make_fancy_staff(rotation=0):
    annotations = datastructuretools.CyclicTuple([
        OscillationSpanner.Size.LARGE,
        OscillationSpanner.Size.MEDIUM,
        OscillationSpanner.Size.SMALL,
        OscillationSpanner.Size.NONE,
        ])
    annotations = sequencetools.rotate_sequence(annotations, rotation)
    divisions = [(5, 8), (7, 8), (4, 8), (6, 8), (5, 4), (4, 4), (3, 4)]
    divisions = sequencetools.rotate_sequence(divisions, rotation)
    pitches = datastructuretools.CyclicTuple(
        ["b'", "d''", "g'", "f''", "b'", "g'", "c'", "e'", "g'"],
        )
    pitches = sequencetools.rotate_sequence(pitches, rotation)
    selections = talea_rhythm_maker(divisions, rotation=rotation)
    measures = Measure.from_selections(selections, time_signatures=divisions)
    staff = Staff(measures)
    for i, logical_tie in enumerate(iterate(staff).by_logical_tie(pitched=True)):
        for note in logical_tie:
            note.written_pitch = pitches[i]
    selector = selectortools.Selector().by_leaf().by_run(Note)[:-1].flatten()
    for i, leaf in enumerate(selector(staff)):
        attach(annotations[i], leaf)
    attach(OscillationSpanner(), staff)
    return staff
>>> staff = make_fancy_staff(rotation=2)
>>> show(staff)
>>> staff = make_fancy_staff(rotation=5)
>>> show(staff)