The translation process¶
Overview¶
Translation in LilyPond is the flow of making a graphical representation of music. It is the only stage where timing information is available. During translation, graphical objects are created and set up for the backend. Notably, every object has a parent on both axes (X and Y).
Translation happens through several kinds of objects.
Contexts are used to store data. They are typed, and correspond
to a certain portion of the score, vertically. They are created
following a hierarchy. Common context types include Score
,
StaffGroup
, Staff
and Voice
. All contexts types are
listed in the Internals Reference under Contexts.
Stream events are derivatives of music events. They have a different
class in C++ and a different Scheme type predicate: ly:stream-event?
.
Most music event types are turned into stream events when their time
comes. This does not hold for music types that contain other
music events, such as SequentialMusic
. As a rule of thumb,
music types ending in Event
are slated to become stream events.
Events are Probs <probs>
, hence the functions related to event properties:
ly:event-property
and ly:event-set-property!
.
Iterators are responsible for the timing. They advance in “container”
music expressions – those preferably ending in Music
rather than
Event
– and broadcast their contents as stream events or create
child iterators for nested container expressions.
Engravers react to stream events. They can create grobs, set
context properties, and process the grobs created by other engravers.
Overall, they constitute the main component of translation. There
is an engraver for every single bit of notation: Note_heads_engraver
,
Stem_engraver
, Beam_engraver
, Staff_symbol_engraver
, and
many others. In contrast to iterators, it is possible to write
engravers in Scheme.
Engravers have no properties, because they are generally not directly used as objects. They communicate through context properties.
By convention, iterator and engraver names are written Capitalized_with_underscores. Stream events do not have any particular type. They are based on music classes (also called event classes) rather than music types.
Grob basics¶
Grobs are covered more in-depth in the Backend programming
section. This
is an introduction to what you should know before writing an engraver.
The term grob is short for graphical object. The functions
related to their properties follow the same conventions as
those described in Probs
.
- (ly:grob-property grob property [default])
- (ly:grob-set-property! grob property value)
Whenever possible, a grob has an event or another grob as its cause,
available in its cause
property. This is used in engravers as
well as the backend. It is also the way point-and-click can trace
back the origin of a grob in the source file. The event-cause
function traces back the cause of a grob in events.
Like music objects, grobs are grouped together in grob interfaces. In the Internals Reference, they are listed under Graphical Object Interfaces.
Like music types and music classes, respectively, grob types are
spelled in CamelCase
, and grob interface names in
lowercase-with-dashes
.
Contexts¶
Context properties¶
Contexts contain properties. In LilyPond syntax, the standard way to
modify properties of a context is the \set
command:
\set [ContextName].propertyName = value
Behind the scenes, this inserts a PropertySet
event in the music.
When iterated, this event causes the property to be set (through
the Property_iterator
).
Contexts are Probs <probs>
, with the associated functions:
- (ly:context-property context property [default])
- (ly:context-set-property! context property value)
Moreover, every context contains grob property settings. The command
to change them in the music is \override
:
\override [ContextName].GrobName.property-name = value
In Scheme, the following functions deal with grob property defaults:
- (ly:context-grob-definition context grob)
Return an alist mapping property names (as symbols) to their defaults for the grob (a symbol) in context.
- (ly:context-pushpop-property context grob property [value])
If value is given, push it as a default for grob.property in context. This is the equivalent of a
\temporary \override
.Without a value, pop the top of the stack. This is like
\revert
.Note that for backwards compatibility reasons,
\override
without\temporary
pops the stack before pushing. This means that after a sequence of\override
commands,\revert
does not reinstate the value before the last\override
but the primary, global default value for the grob property in the context type.
The context hierarchy¶
Context follow a hierarchical structure. The top-most context is called
Global
. Every context but Global
is contained in a parent.
- (ly:context-find context name)
Search and return a context with the given name (a symbol) above context: its parent, or the parent of its parent, or …
Return the boolean false if no appropriate parent is found.
Aliases are a way for the more rarely used context types, like
TabStaff
, to fit in the hierarchy of the more frequently used,
such as Staff
. Contexts having the name as alias are also
considered by ly:context-find
. Thus, a TabStaff
may be found
as a Staff
parent to a TabVoice
. This allows engravers to
work in several context types.
Context IDs¶
Every context is known under a unique ID, accessible through the
function ly:context-id
. The syntax \new Staff = "up" ...
sets this ID. By default, it is generated automatically in a way
guaranteeing uniqueness.
Defining new context types¶
The official documentation has excellent details on Defining new context types.
Useful translation hooks¶
Accessing a context¶
In the course of a music expression, the \applyContext
command
can be used to apply a certain function to a context. This comes
in handy when the settings that are to be applied to the context
depend on its properties. The \applyContext
function takes
a single parameter, a function taking a context argument. It applies
in the context the music expression is being iterated in. To specify
a different context, the \context
command should be used. It
sends music to a context found above the current one by type.
For example, here is how to increment the bar number by one:
incrementBar =
\context Score
\applyContext
#(lambda (context)
(ly:context-set-property!
context
'currentBarNumber
(1+ (ly:context-property context 'currentBarNumber))))
{
c'1\break
c'1\break
\incrementBar
c'1
}
Tweaking grobs during translation¶
A later section examines methods to change the appearance and
positioning of grobs in the backend. The \applyOutput
command is
a facility to apply a function to certain grobs created in a context
or any of its descendants, at a single moment. It is useful for
local tweaks depending on context settings. There are two forms for
\applyOutput
:
\applyOutput Context #procedure
\applyOutput Context.Grob #procedure
In the latter form, grobs of the specified types are matched, while in the former, all grobs match.
The procedure is called with three arguments:
The grob,
The context it originates from,
The context
\applyOutput
applies to.
A motivating use case is to set the color for all grobs:
colorNotes =
#(define-music-function (color) (color?)
#{
\applyOutput Voice
#(lambda (grob origin-context context)
(ly:grob-set-property! grob 'color color))
#})
{
\colorNotes red
<e' g' bes'>4
<e' g' a'>4
\colorNotes green
<d' fis' a'>4
}
Overwriting grob properties set by engravers¶
The \override
command has the effect of setting the context-wide
default for a grob property. Certain engravers, however, write
properties in the grobs they create. For instance, the
Note_heads_engraver
sets the staff-position
on freshly
instantiated NoteHead
grobs based on the pitch in the note
event it reads. This makes \override
ineffective. The
\overrideProperty
command sets a property on a grob after
it has been created and initialized by its origin engraver.
Internally, \overrideProperty
uses \applyOutput
. The
\overrideProperty
command takes the grob property path and
a value, like \override
but without an equals sign.
{
\overrideProperty NoteHead.staff-position 50
c'1
}
Writing an engraver¶
Basics¶
Engravers reside in contexts. They are added to them in output definitions. The pattern is:
\layout {
\context {
\ContextName
\consists Some_engraver
}
}
Here, Some_engraver
should be the name of a predefined engraver.
They are listed in the Internals Reference, under
Engravers and Performers.
The \remove
command is the opposite of \consists
and suppresses
an engraver from a context where it would have been active by default.
For engravers defined in Scheme and not registered, the argument is
no longer a string, but a procedure: #Some_engraver
, where
Some_engraver
takes a context and uses the make-engraver
macro
to return an engraver 1.
Here is a template for writing an engraver:
#(define (My_engraver context)
(let (variables...)
(make-engraver
((initialize engraver)
...)
((start-translation-timestep engraver)
...)
(listeners
((event-class-1 engraver event)
...)
((event-class-2 engraver event)
...)
...)
((process-music engraver)
...)
(acknowledgers
((grob-interface-1 engraver grob source-engraver)
...)
((grob-interface-2 engraver grob source-engraver)
...))
(end-acknowledgers
((grob-interface-1 engraver grob source-engraver)
...)
((grob-interface-2 engraver grob source-engraver)
...)
...)
((process-acknowledged engraver)
...)
((stop-translation-timestep engraver)
...)
((finalize engraver)
...))))
\layout {
\context {
\SomeContext
\consists #My_engraver
}
}
Don’t panic! The rest of this section explains the hooks one by one.
The variables… are in a Scheme closure and therefore internal to the engraver.
The ellipsis in every method stands for arbitrary Scheme expressions. As with any Scheme function, they are evaluated in order. Their return values are not important because engravers rely on side-effects.
The time step cycle¶
The music is iterated following time. Time step is the term
for the basic unit of time. If there are simultaneous voices,
time steps are made so that every event is played in some time
step. Schematically (x
represents a note):
4 4 x---------------- x----------------
\tuplet 3/2 { 4 4 4 } x---------- x---------- x----------
Time steps [ ][ ][ ][ ]
When a time step starts, the start-translation-timestep is called in all engravers. Naturally, this is paired with stop-translation-timestep at the end of the time step.
Furthermore, on its very first time step, the initialize method is called. It may be used to set up state.
Note that for this first time step specifically,
start-translation-timestep is not called. This is because
contexts are created through music (the \new
command), and music
is iterated in time steps, so the context creation may actually
happen after the start of a time step.
The initialize method is paired with finalize, called in the end of the life of an engraver. This method is for bookkeeping, for example, warning about an unterminated spanner. The stop-translation-timestep method is called before finalize, as one would expect.
Processing stream events¶
In the listeners body of the engraver are methods specific to certain event classes. When a corresponding stream event is broadcast, these methods are called with the engraver itself and the event as arguments.
(listeners
((event-class-1 engraver event)
...)
((event-class-2 engraver event)
...))
Event broadcasting follows the context hierarchy. Events heard in a context are also heard in its parents. This means that an engraver residing in Voice context is appropriate to listen to note events, for example, whereas an engraver in Staff context is suited to listening key changes.
It is important to know that grobs should not be created in listeners. This is due to the unspecified order of events. In iterating, for example, simultaneous music, it is unclear what order the events should arrive in:
\new Staff <<
{ \time 3/4 c'2. }
\\
{ \override Staff.TimeSignature.color = red c2. }
>>
In the above example, the OverrideProperty
event corresponding to
the \override
command may well be heard after the TimeSignatureEvent
.
Yet, we want it to affect the created time signature.
This is why the workflow of engravers is to use listeners to record the events they are interested in. Listeners may also set context properties. Then, when all events have been heard, the process-music method is called. This is where grobs should be created.
Creating grobs¶
- (ly:engraver-make-grob engraver grob-type cause)
Return a newly created graphical object.
The grob-type may be any name of a grob, given as symbol. The cause is either a stream event, or another grob. When no particular cause can be given for a grob (e.g., a bar line), it should be the empty list.
First engraver example¶
This engraver listens to rest events. When a rest was found in the previous time step but not in the current time step, it takes it as the end of a melody and adds a breathing sign to the left.
#(define (Auto_breathe_engraver context)
(let ((previous-rest-event #f)
(rest-event #f))
(make-engraver
(listeners
((rest-event engraver event)
(set! rest-event event)))
((process-music engraver)
(if (and previous-rest-event
(not rest-event))
(ly:engraver-make-grob engraver 'BreathingSign previous-rest-event)))
((stop-translation-timestep engraver)
(set! previous-rest-event rest-event)
(set! rest-event #f)))))
\layout {
\context {
\Voice
\consists #Auto_breathe_engraver
}
}
\relative {
\time 9/8
\partial 8
a8 b4 d8 f4. r
g,8 a c ees4 g8 r4.
}
Acknowledging grobs¶
With listeners and the process-music hook, many cases for grob
creation are covered. However, grobs also require links between
different types. Dots
objects are associated to NoteHead
,
for instance. For the sake of modularity, they are created by
separate engravers. This is why a mechanism exists for engravers
to process grobs created in other engravers. This is called
acknowledgers.
Every engraver can exclusively acknowledge grobs created by engravers whose enclosing context is a direct or indirect child of the engraver’s context. An engraver in Voice context acknowledges all grobs created from engravers in this Voice; an engraver in Staff acknowledges all those created from engravers in this Staff and engravers in children Voices; etc.
The body of acknowledgers in make-engraver resembles that of listeners.
(acknowledgers
((grob-interface-1 engraver grob source-engraver)
...)
((grob-interface-2 engraver grob source-engraver)
...)
...)
Every acknowledger is called with three arguments: the engraver it
is operating in, the grob, and the engraver where the grob has
been created. The function ly:translator-context
can provide
the context in which the source engraver lives.
Just like with listeners, acknowledgers should generally be used to record the grobs, not process them directly to create other grobs, since other engravers may change properties of the acknowledged grobs in their own acknowledgers. The equivalent of process-music for acknowledgers is process-acknowledged.
((process-acknowledged engraver)
...)
New grobs may be created in process-acknowledged. This leads to a new cycle of acknowledgers, then process-acknowledged.
Engravers do not acknowledge their own grobs.
Second engraver example¶
This engraver adds a balloon on every note, indicating its pitch.
It sets a bunch of properties on the balloon. In particular,
X-offset
and Y-offset
, which are normally taken from an
AnnotateOutputEvent
(from the call to \balloonText
), are set
to values making the slopes increase vertically in a chord. The
annotation-balloon
property is deactivated to suppress the
rectangle around every note head. The pitches are converted to
strings through the note-name->string
function, using the
english
input language.
Note the frequent pattern of resetting the list of acknowledged
grobs at the end of process-acknowledged
, to avoid processing grobs
twice if there are further acknowledge cycles.
#(define (Balloon_notes_engraver context)
(let ((note-heads '()))
(make-engraver
(acknowledgers
((note-head-interface engraver grob source-engraver)
(set! note-heads (cons grob note-heads))))
((process-acknowledged engraver)
(for-each
(lambda (i note-head)
(let* ((note-event (event-cause note-head))
(pitch (ly:event-property note-event 'pitch))
(pitch-string
(string-capitalize
(note-name->string pitch 'english)))
(balloon (ly:engraver-make-grob engraver
'BalloonTextItem
note-head)))
(ly:grob-set-parent! balloon X note-head)
(ly:grob-set-parent! balloon Y note-head)
(ly:grob-set-property! balloon 'font-size -3)
(ly:grob-set-property! balloon 'font-series 'bold)
(ly:grob-set-property! balloon 'X-offset -2)
(ly:grob-set-property! balloon 'Y-offset (+ i 0.2))
(ly:grob-set-property! balloon 'annotation-balloon #f)
(ly:grob-set-property! balloon 'text pitch-string)
(ly:grob-set-property! note-head
'extra-spacing-width
'(-3.5 . 0))))
(reverse! (iota (length note-heads)))
note-heads)
(set! note-heads '())))))
\layout {
\context {
\Voice
\consists #Balloon_notes_engraver
}
}
{
<e' g' b'>1
<e' g' a'>1
}
End acknowledgers¶
Spanners are not finished in a single time step. Their bounds must
be set in two different time steps, and they may be terminated in
reaction to an event, like hairpins with \!
.
In addition to acknowledgers
, which is triggered at grob creation
time, the end-acknowledgers
body contains acknowledgers triggered
at the time a spanner ends. This time is announced by the engraver
responsible for ending it. Custom engravers should also announce
spanner ends.
- (ly:engraver-announce-end-grob engraver spanner cause)
Announce the end of spanner for other engravers.
Like the one for
ly:engraver-make-grob
, the cause argument should be an event, another grob, or the empty list.
Third engraver example¶
This engraver prevents line breaks from crossing ties. This is achieved
through setting the forbidBreak
property of the Score
context.
Note the logic with the tie-in-progress
variable. The break should
be forbidden on the left of the note head even when the tie ends on
this head, and we do not want to disallow breaks on the left of
a head that starts a tie. This is why, as is commonly done, the
acknowledgers only record the ties. The final processing is done
in stop-translation-timestep
(often this is rather in
process-acknowledged
).
#(define (No_break_during_tie_engraver context)
(let ((tie-in-progress #f)
(starting-tie #f)
(ending-tie #f))
(make-engraver
(acknowledgers
((tie-interface engraver grob source-engraver)
(set! starting-tie grob)))
(end-acknowledgers
((tie-interface engraver grob source-engraver)
(set! ending-tie grob)))
((stop-translation-timestep engraver)
(if tie-in-progress
(ly:context-set-property!
(ly:context-find context 'Score)
'forbidBreak
#t))
(if ending-tie
(set! tie-in-progress #f))
(if starting-tie
(set! tie-in-progress #t))
(set! starting-tie #f)
(set! ending-tie #f)))))
% TODO: meaningful music
music = {
c'1 c'1~ 1~ 1~
1~ 1~ 1~ 1~ 1~
1~ 1~ 1~ 1~ 1~
1 1 1~ 1~ 1~
1~ 1~ 1~ 1~
1 1 1 1 1
}
\new Voice \music
\new Voice \with { \consists #No_break_during_tie_engraver } \music
Grob parents and spanner bounds¶
Every grob has parents for positioning. The functions related to parents are:
- (ly:grob-parent grob axis)
- (ly:grob-set-parent! grob axis)
Return or set the parent of grob on axis (X or Y).
Spanners are special however: they do not have one parent but two
bounds on the horizontal axis. A bound is an simply an item.
The distinction between items and spanners is explained at
Grob flavors and line breaks
.
- (ly:spanner-bound spanner direction axis)
- (ly:spanner-set-bound! spanner direction item)
Return or set the bound of spanner in direction (LEFT or RIGHT).
Horizontal parents and bounds¶
Paper columns are special grobs used to group other grobs together.
At every point in time, a context has two important properties:
currentMusicalColumn
and currentCommandColumn
. They are
the default X parents for all items created in the time step. The
musical column is used for all musical items and vice versa.
A non-musical item is defined as an item having the non-musical
property set to true, or a child of such an item. All other items
are considered musical.
For spanners, there is no default and the bounds must be set explicitly. For spanners that have no particular items as bounds, either the musical or the non-musical columns should be used.
Vertical parents¶
When no particular Y parent is set, the Axis_group_engraver
adds
the grob in a VerticalAxisGroup
corresponding to the context.
The effect is that all grobs created in the same context are placed
relative to the same vertical base line.
Fourth engraver example¶
This engraver aligns all dynamics on the same vertical position. This
is equivalent to using a separate Dynamics
context, except that the
dynamics can be entered in the main music input without resorting to
spacer rests.
This is achieved by creating a single DynamicLineSpanner
for the
entire score, unlike the default Dynamic_align_engraver
, which makes
one for every sequence of consecutive dynamics.
A context property is declared to control whether the dynamic line
goes above or below the staff. Refer to Adding properties and types
.
The dynamics are added to the spanner using
ly:axis-group-interface::add-element
, which sets pointers in the
object to its axis group in addition to making the group parent of the
object.
The bounds of the spanner are non-musical columns.
#(set-object-property! 'dynamicAlignDirection 'translation-type? ly:dir?)
#(define (Align_all_dynamics_engraver context)
(let ((line-spanner #f)
(direction #f))
(make-engraver
((initialize engraver)
(set! line-spanner
(ly:engraver-make-grob engraver 'DynamicLineSpanner '()))
(ly:spanner-set-bound! line-spanner
LEFT
(ly:context-property context
'currentCommandColumn))
(ly:grob-set-property! line-spanner
'direction
(ly:context-property context
'dynamicAlignDirection)))
(acknowledgers
((dynamic-interface engraver grob source-engraver)
(ly:axis-group-interface::add-element line-spanner grob)))
((finalize engraver)
(ly:spanner-set-bound! line-spanner
RIGHT
(ly:context-property context
'currentCommandColumn))))))
\layout {
\context {
\Voice
\remove Dynamic_align_engraver
\consists #Align_all_dynamics_engraver
dynamicAlignDirection = #DOWN
}
}
<<
\new Staff \relative {
c'2\< d4 e |
c4 e e,2\f |
g'4\dim a g a |
c1\p |
}
>>
Killing grobs¶
Time is sequential, with no look-ahead possibilities. This is why
engravers must create all grobs in the time step they belong to.
It it is later realized that a grob is not needed, the grob should
be killed using the dedicated function ly:grob-suicide!
.
Fifth engraver example¶
This engraver adds lines between note heads in a melody. Observe the
use of ly:grob-suicide!
: the voice follower must be started
whenever a note head is found, without knowing if the next time step
is going to contain a note or a rest. Afterwards, if there is no
note head in the time step, the previously created voice follower
is killed.
In addition, the engraver recognizes slurs with both acknowledgers and end acknowledgers. It kills the grobs it just created when a slur is in progress. Note that slurs are not announced in the time step where they end but in the following time step. Therefore, some trickery is required to suicide the right grobs and keep state properly.
This example has pretty unclear musical interest. There is some fun to it, however.
#(define (Voice_line_engraver context)
(let ((previous-follower #f)
(next-follower #f)
(note-head #f)
(note-head-found #f)
(accidental #f)
(slur-in-progress #f)
(slur-start-now #f)
(slur-start-before #f)
(slur-end-before #f))
(make-engraver
(acknowledgers
((note-head-interface engraver grob source-engraver)
(set! note-head grob)
(set! note-head-found #t))
((slur-interface engraver grob source-engraver)
(set! slur-start-now #t))
((accidental-interface engraver grob source-engraver)
(set! accidental grob)))
(end-acknowledgers
((slur-interface engraver grob source-engraver)
(set! slur-end-before #t)))
((process-acknowledged engraver)
(if note-head
(begin
(if previous-follower
(begin
(ly:spanner-set-bound! previous-follower RIGHT note-head)
(ly:engraver-announce-end-grob engraver
previous-follower
note-head)))
(set! next-follower
(ly:engraver-make-grob engraver
'VoiceFollower
note-head))
(ly:spanner-set-bound! next-follower LEFT note-head)))
(if (and accidental previous-follower)
(ly:spanner-set-bound! previous-follower RIGHT accidental))
(set! note-head #f)
(set! accidental #f))
((stop-translation-timestep engraver)
(if (and previous-follower
(not note-head-found))
(ly:grob-suicide! previous-follower))
(if slur-end-before
(set! slur-in-progress #f))
(if slur-start-before
(set! slur-in-progress #t))
(if (and previous-follower
slur-in-progress)
(ly:grob-suicide! previous-follower))
(if slur-start-now
(set! slur-in-progress #t))
(set! previous-follower next-follower)
(set! next-follower #f)
(set! note-head #f)
(set! note-head-found #f)
(set! slur-start-before slur-start-now)
(set! slur-start-now #f)
(set! slur-end-before #f)))))
\layout {
\context {
\Voice
\consists #Voice_line_engraver
\override VoiceFollower.style = #'dashed-line
\slurDashed
}
}
\relative {
\override Score.SpacingSpanner.spacing-increment = 4
d'16( e f8) e d a' d4 a8 |
bes8 g16( e) a8 f16( d) g8 e16( cis) a8 bes'8~ |
bes8 g16( e) a,8 cis'8~ cis bes16( g) a,8 e''~ |
e8 cis16( a) bes( g a f) g( e f d) e( cis d b) |
cis( a b cis) d( e f g) a( bes c8) c16( d ees8) |
fis,8 g cis, d gis, r a r |
}
- 1
Technically, the
make-engraver
macro builds an associative list containing procedures. This is why you can sometimes see engravers written in the old way:#(define (My_old_style_engraver context) `((start-translation-timestep . ,(lambda (engraver) ...)) (listeners (event-class-1 . ,(lambda (engraver event) ...)) ...) ...))
Also note that it is not strictly necessary to pass a function taking the context and returning the engraver. Thus,
\consists #(let (...) (make-engraver ...))
is accepted. However, this has different semantics: because the methods are defined unconditionally, the same variables are shared by all instances of the engraver in parallel contexts. This is generally not what you want.