Lilypond erweitern: Einführung#
Scheme innerhalb von LilyPond benutzen#
Um innerhalb einer LilyPond-Datei einen Scheme-Ausdruck zu verwenden, stellt man ihm ein Hash-Zeichen #
voran. Der Ausdruck wird ausgewertet, und sein Wert wird an LilyPond zurückgegeben.
\version "2.25.8"
myVar = #(+ 2 2) % myVar bekommt den Wert 2 + 2 = 4
\repeat unfold #myVar { c } % Die Note c viermal.
Output
Dieses Beispiel zeigt auch, dass in LilyPond definierte Variablen von Scheme aus zugänglich sind. Das gilt auch umgekehrt:
\version "2.25.8"
#(define mySecondVar 42)
\repeat unfold \mySecondVar { c }
Output
Der Operator #@
fügt alle Elemente einer Scheme-Liste nacheinander in die LilyPond-Umgebung ein. Diese Technik ist als Splicing bekannt.
Man kann sogar innerhalb von Scheme-Ausdrücken wieder zurück zu LilyPond-Syntax wechseln, indem man LilyPond-Code zwischen #{
und #}
einbettet.
Das folgende Beispiel benutzt sowohl Listen-Splicing als auch eingebetteten LilyPond-Code:
\version "2.25.8"
notes = #(list #{ c'4 #}
#{ e'4 #}
#{ g'4 #}
#{ c''4 #})
{
% Alle Noten aus der Liste der Reihe nach:
#@notes
% Dasselbe in gleichzeitiger Musik:
<< #@notes >>
}
Output
Exportierte Funktionen und Benennungs-Konventionen#
In LilyPond sind zwei Arten von Funktionen verfügbar: Guile definiert eine große Zahl von universell einsetzbaren Werkzeugen zum Umgang mit Zeichenketten, Listen usw. Und LilyPond selbst stellt eine zusätzliche Schicht von Funktionen bereit, mit denen sich die LilyPond-eigenen Objekttypen wie Musik, Kontexte oder Textbeschriftungen manipulieren lassen.
Viele Funktionen, die von LilyPond bereitgestellt („exportiert“) werden, haben Namen, die mit ly:
beginnen (zum Beispiel ly:music-length
), um sie von Scheme-eigenen Funktionen zu unterscheiden. Systematisch gilt das für solche Funktionen, die aus LilyPonds C++-Code exportiert werden. Zusätzlich exportieren auch manche Teile von LilyPonds eigenem Scheme-Code Funktionen mit dem Präfix ly:
– andere aber nicht. Hoffentlich kann das künftig einmal stärker vereinheitlicht werden.
Der Scheme-Sandkasten#
Um in LilyPond mit Scheme zu experimentieren, starten Sie auf dem Terminal (in der Eingabeaufforderung) die Scheme-„Sandbox“:
lilypond scheme-sandbox
In diesem Modus öffnet sich eine interaktive Guile-Eingabezeile, in der alle von LilyPond exportierten Funktionen verfügbar sind.
Das Wichtigste über Musikfunktionen#
Viele der Elemente in LilyPonds Sprache sind Musikfunktionen: \relative
, \transpose
, \bar
usw. Man kann solche Funktionen auch selbst schreiben: Sie werden mit define-music-function
deklariert, wobei man für jeden Parameter der Funktion ein Typenprädikat angeben muss.
\version "2.25.8"
dudelBegleitung =
#(define-music-function (note1 note2 note3)
(ly:music? ly:music? ly:music?)
#{
\repeat unfold 2 {
#note1 #note2 #note3 #note2 #note3 #note2
}
#})
{
\time 6/4
\dudelBegleitung c'8 g' e''
\dudelBegleitung c'8 g' f''
\dudelBegleitung d'8 g' f''
\dudelBegleitung c'8 g' e''
}
Output
Die häufigsten Typenprädikate sind:
Name des Prädikats |
Akzeptierte Werte |
---|---|
|
Jeder numerische Wert (Ganzzahl, Gleitkomma, Bruch) |
|
Ganze Zahlen |
|
Nichtnegative ganze Zahlen |
|
Ein String (Zeichenkette, etwa |
|
Ein |
|
Ein Boolescher Wert, also WAHR/true ( |
|
Ein Musik-Ausdruck |
|
Eine Tonhöhe (zum Beispiel die ersten beiden Argumente von |
|
Eine Dauer (wie das optionale Argument zu |
|
Eine Farbe, entweder als String oder als RGB-Liste (siehe [Handbuch](notation:Coloring objects)) |
|
Eine Scheme-Liste mit beliebigen Elementen |
Das Wichtigste über Callbacks#
„Grobs“ (kurz für „Graphical Objects“) sind Bestandteile des Notenbildes – also Notenköpfe, Hälse, Pausen usw. In LilyPond begegnen ihre Namen häufig, wenn man Grob-Eigenschaften mittels \override
verändern möchte.
Anstatt eine Grob-Eigenschaft fest vorzugeben, kann man sie immer auch auf eine „Callback“-Funktion setzen: eine Funktion, die den endgültigen Wert der Eigenschaft dynamisch berechnet. Die Funktion erhält den Grob als einziges Argument. Wenn man in der Funktion andere Grob-Eigenschaften auslesen möchte, ist das mit ly:grob-property
möglich. Das folgende Beispiel erzeugt gespreizte Balken, die sich in Richtung der höheren Noten auffächern: nach rechts für einen aufwärtsgerichteten Balken, nach links für einen abwärtsgerichteten. Im Notenbild wird damit ein Beschleunigen angezeigt, wann immer die Melodie aufwärts verläuft – wahrscheinlich nicht zur Freude der Musiklehrerin.
\version "2.25.8"
#(define (aufwärts-spreizen beam)
(let* ((Y-positions (ly:grob-property beam 'positions))
(left-position (car Y-positions))
(right-position (cdr Y-positions)))
(cond
((< left-position right-position)
RIGHT)
((> left-position right-position)
LEFT)
(else
CENTER))))
\relative c' {
\override Beam.grow-direction = #aufwärts-spreizen
c'16 d e f g f e d c g c g c e g c
}
Output
Werte ausgeben#
Die Standard-Scheme-Prozedur display
ist nicht gut geeignet für Debugging-Ausgaben bei der Arbeit mit LilyPond, weil sie auf die Standardausgabe stdout schreibt, während LilyPonds eigene Protokoll-Nachrichten auf die Standardfehlerausgabe stderr gehen, wodurch beide Arten von Meldungen in der Reihenfolge durcheinandergebracht werden. Es ist praktischer, ly:message
zu benutzen. Anders als bei display
muss das erste Argument hier ein String sein. Mit weiteren Argumenten kann die Ausgabe entsprechend den üblichen Format-Angaben angepasst werden, genau wie bei der Funktion format
. Ein weiterer Unterschied liegt darin, dass ly:message
automatisch einen Zeilenwechsel ausgibt.
Zum Beispiel könnte man folgendermaßen den Wert der Variable Y-positions
im obigen Callback-Beispiel ausgeben lassen:
(ly:message "Die Y-Positionen sind: ~a" Y-positions)
Die wichtigsten Formatangaben sind ~a
und ~s
. Die erstere gibt Werte möglichst hübsch aus, wie display
es tut, während die zweitere sie einlesbar zu machen versucht wie write
. Im Falle von Strings bedeutet das: ~s
gibt Anführungsstriche aus, ~a
dagegen nicht.
The ~y
formatter attempts to print nested data structures in a readable way.
It is only available with the standard procedure format
:
\version "2.25.8"
{
\override Slur.after-line-breaking =
#(lambda (grob)
(ly:message
(format #f "~y" (ly:grob-property grob 'control-points))))
b'1( b'')
}
Output
Dieses Beispiel liefert:
((0.732364943841144 . 1.195004)
(1.76258769418946 . 3.05939544438528)
(6.83883925432087 . 5.14594080151585)
(8.88241194297576 . 4.545004))
Erste Schritte mit den LilyPond-Interna#
Smobs#
„Smob“ bedeutet „Scheme-Objekt“. Smob-Datentypen werden von LilyPonds C++-Kern nach Scheme exportiert, zusammen mit Funktionen, um sie zu manipulieren. Tonhöhen beispielsweise sind Smobs; wie für alle Smobs, gibt es ein spezielles Typenprädikat für sie, nämlich ly:pitch?
. Wie für viele andere Smob-Datentypen steht auch ein Konstruktor ly:make-pitch
bereit, um Tonhöhen zu erzeugen. Dazu kommen verschiedene Werkzeuge wie ly:pitch-notename
, ly:pitch-transpose
usw.
Probs#
„Prob“ ist kurz for „Property Object“, also „Objekt mit Eigenschaften“. Dieser Begriff umfasst eine Zahl von Objekten, denen allen gemeinsam ist, dass sie „Eigenschaften“ haben. Alle Probs sind auch Smobs, aber das gilt nicht umgekehrt: Tonhöhen und Dauern beispielsweise sind keine Probs – sie haben keine Eigenschaften. Musikobjekte oder Kontexte dagegen sind Probs.
Für alle häufig benutzten Prob-Datentypen gibt es Funktionen, um ihre Eigenschaften auszulesen oder zu verändern. Sie alle folgen dem gleichen Namens- und Parameterschema.
- (ly:<xxx>-property object property [default])
Liefert den Wert einer Eigenschaft eines Probs. <xxx> steht dabei für den Prob-Typ: Es gibt
ly:music-property
,ly:event-property
,ly:context-property
usw.Der Name der Eigenschaft wird als Symbol property übergeben. Um Appetit auf Musikobjekte zu bekommen, probieren Sie einmal das folgende Beispiel aus:
#(display (ly:music-property #{ c'8 #} 'pitch))
Wenn der Prob die gefragte Eigenschaft nicht hat, wird der Wert default zurückgegeben, sofern er angegeben wurde (und andernfalls die leere Liste). Achtung: Anders als die meisten Standard-Scheme-Funktionen fallen Prob-Eigenschafts-Abfragen notfalls auf die leere Liste
'()
zurück, nicht auf den booleschen FALSCH-Wert#f
!
- (ly:<xxx>-set-property! object property value)
Setzt die Eigenschaft property im Objekt object auf den Wert value.
Die konkreten Funktionen heißen also
ly:music-set-property!
,ly:event-set-property!
usw.
Glossar: Wichtige Objekt-Typen#
Die folgenden Objekte spielen eine besonders prominente Rolle in der Arbeit mit LilyPond:
- Ausgabe-Definitionen
\layout
-,\midi
- und\paper
-Blöcke. Sie sind die Architekten im Prozess von Eingabe zu Ausgabe und enthalten Einstellungen für Kontext-Standardwerte, Abstände auf der Seite, Parameter für den Zeilenumbruch usw.:indent
,page-breaking
,system-system-spacing
,\context { \Staff \override ... }
.- Bücher, Buchabschnitte und Stücke
\book
-,\bookpart
- und\score
-Blöcke. Jedes Buch („book“) gehört zu einer Ausgabe-Datei; es kann Buchabschnitte und Stücke enthalten. Stücke („score“) sind die eigentlichen Behälter für Musik. Buchabschnitte („bookpart“) sind eine optionale Zwischenschicht zwischen Büchern und Stücken; jeder Buchabschnitt beginnt auf einer neuen Seite.Alle diese Objekte enthalten Ausgabedefinitionen, und zwar:
\book
und\bookpart
können\paper
-Blöcke enthalten, und\score
kann\layout
und/oder\midi
enthalten. Dabei gibt es ein Vererbungs-System: Einstellungen in einem\layout
-Block in\score
überschreiben diejenigen aus dem\paper
-Block des umgebenden\bookpart
s; diejenigen in\bookpart
überschreiben die des umgebenden\book
s, und letztere haben Priorität gegenüber globalen Ausgabedefinitionen.- Musik
Musik-Objekte bilden die in einer LilyPond-Datei enthaltene Musik ab, und zwar in einer hierarchischen, baumartig angeordneten Struktur. Beispielsweise ist
<< c'8 \\ d16\p >>
„gleichzeitige Musik“ (SimultaneousMusic
), die zwei Musik-Ausdrücke enthält. Der zweite von ihnen ist eine einzelne Note (NoteEvent
), die ihrerseits eine Dynamikangabe (AbsoluteDynamicEvent
) enthält.- Kontexte
Kontexte sind Behälter für Eigenschaften; sie korrespondieren zu verschiedenen Schichten der Partitur. Die häufigsten Kontext-Typen sind
Score
(Stück),Staff
(Notenzeile) undVoice
(Stimme). Die Hierarchie von Kontexten bestimmt, welche Translatoren auf ein bestimmtes Strom-Event oder Grob reagieren. Außerdem sind die Eigenschaften eines Kontexts der Weg, auf dem Translatoren mit ihrer Außenwelt kommunizieren können, also sowohl mit dem Menschen (der:die Kontexteigenschaften mittels\set
verändern kann) als auch mit anderen Translatoren.- Iteratoren
Iteratoren etablieren die Zeitachse der Musik, indem sie die gegebene Musik in einer jeweils bestimmten Weise durcharbeiten. Der
Sequential_iterator
beispielsweise kümmert sich um das „Iterieren“ von (sequentiellen) Musikausdrücken der Form{ ... }
, indem er die enthaltene Musik Schritt für Schritt abarbeitet. DerSimultaneous_music_iterator
dagegen ist für (gleichzeitige) Musikausdrücke der Form<< ... >>
zuständig, indem er (wie eine seitwärts laufende Krabbe) mehrere Musikausdrücke parallel und synchron verarbeitet.- Ereignisse
Zwar sind manche Musikobjekte nur Behälter für andere Musikobjekte. Die meisten sind aber dazu bestimmt, irgendwann in „Stream-Ereignisse“ („stream events“) verwandelt zu werden. Es gibt Ereignisse für Noten, Pausen, Dynamik, Artikulation usw. Jedes Ereignis tritt zu einem definierten Zeitpunkt auf. Die Arbeit mit einem Strom von Ereignissen vereinfacht LilyPonds Abläufe gegenüber einem direkten Arbeiten mit Musikobjekten.
- Translatoren
Translatoren sind vielseitige Objekte, die die Ereignisse in graphische Objekte verwandeln und die Beziehungen zwischen diesen Objekten festlegen. Es gibt zwei Arten von Translatoren, nämlich die „Engraver“ („Graveure“) für graphische Ausgabe und die „Performer“ („Spieler“) für die MIDI-Ausgabe.
- Grobs
Grob ist kurz für „Graphisches Objekt“. Grobs repräsentieren ein konkretes Notationselement auf der Druckseite: Einen Notenkopf (Grob-Typ
NoteHead
), einen Hals (Stem
), einen Dehnungspunkt (Dots
) usw.- Callbacks
Grobs haben Eigenschaften. Ein Callback (genauer: eine Callback-Funktion) ermöglicht es, eine Grob-Eigenschaft dynamisch zu berechnen, so dass sie von anderen Eigenschaften dieses Grobs und anderer, mit ihm in Beziehung stehender Grobs abhängt. (Ein Beispiel für eine solche Beziehung zwischen Grobs wäre diejenige zwischen einem Notenkopf und dem zu ihm gehörenden Notenhals.) Ein Callback ist einfach eine Funktion, die den Grob selbst als Parameter erhält; ein Beispiel findet sich im Abschnitt Das Wichtigste über Callbacks.
- Stencil
Stencils („Matrizen“) sind gewissermaßen Druckerschwärze, die direkt auf der Seite plaziert wird. Fast jeder Grob hat einen Stencil, der auf der Seite ein Element des Notenbildes erzeugt. Notenhals-Grobs (
Stem
) beispielsweise produzieren spezielle Linien-Stencils.- Markups
Markups (in LilyPonds deutschem Handbuch: „Textbeschriftungen“) sind eine Art Programmcode, der graphische Elemente – und zwar besonders häufig Text – beschreibt. Wird ein Markup „ausgeführt“, entsteht ein Stencil; die übliche Formulierung dafür ist, dass das Markup interpretiert wird. Viele Grobs benutzen Markups, um ihren Stencil zu erzeugen. Markups können als „textförmige“ Artikulationsangaben in die Musik eingefügt werden, z.B. als
{ c'8^\markup \italic { Cresc. poco a poco } }
; sie können aber auch global zwischen Stücken (Scores) plaziert werden.
Überblick: LilyPonds interne Abläufe, und wie man sich in sie einklinken kann#
Das Umwandeln von Texteingabe in Notensatz ist eine schwierige Aufgabe, die LilyPond die einem komplexen mehrschrittigen Prozess löst. Im folgenden wird jeder dieser Schritte zusammen mit der (je nach Systemeinstellung englisch- oder deutschsprachigen) Protokoll-Meldung aufgeführt, die LilyPond zu Beginn des jeweiligen Schrittes ausgibt.
Grammatikalische Analyse#
Parsing...
/Analysieren...
Der erste Schritt ist die lexikalische Analyse der Eingabedatei, für die der Lexer zuständig ist: Er verwandelt die Eingabedatei in einen Strom von „Tokens“ (Sprachelementen), die der Parser dann verarbeiten kann. Tokens stehen beispielsweise für öffnende oder schließende Klammern, Tonhöhen, Dauern, usw. Der Lexer ist in der Sprache Flex geschrieben.
Der Parser ist in Bison geschrieben und analysiert die syntaktische Struktur der Tokens, die vom Lexer erkannt werden. Er versteht LilyPonds Grammatik und produziert die diversen Objekte, die in späteren Schritten verarbeitet werden: Ausgabedefinitionen, Bücher, Buchabschnitte, Stücke, Musikausdrücke und auch Markups.
Während des Parsens werden Musikausdrücke mittels Musikfunktionen (in LilyPonds deutschem Handbuch: „musikalische Funktionen“) wie
\relative
oder\transpose
transformiert. Die Möglichkeit, eigene Musikfunktionen zu definieren, ist ein wichtiger Grund für LilyPonds beträchtliche Erweiterbarkeit.
Die Iteration#
Interpreting music...
/Interpretation der Musik...
Der Iterations-Prozess wird für jeden
\score
-Block durchgeführt. Sein Zweck ist, den Strom musikalischer Ereignisse in ein Netz von Grobs zu verwandeln: Eine einzelne Note in der Eingabe wird zur Erstellung eines Notenkopfs (NoteHead
), eines Halses (Stem
), vielleicht eines Balkens (Beam
), vielleicht eines Dehnungspunkts (Dots
) usw. führen. Dieser Prozess heißt deshalb auch Übersetzung (translation).In diesem Schritt werden Iteratoren, Kontexte und Translatoren erzeugt und führen dann ihre jeweiligen Aufgaben aus.
Eigene Kontext-Typen kann man in Ausgabedefinitionen kreieren. Engraver können in Scheme geschrieben werden; Iteratoren sind nur in C++ zugänglich.
Reines Positionieren#
Preprocessing graphical objects...
/Vorverarbeitung der grafischen Elemente...
Im Übersetzungs-(=Iterations-)Schritt haben wir ein Netz von Objekten mit gegenseitigen Abhängigkeiten erhalten. Ihre Positionierung auf der Seite wird entscheidend von den gewählten Zeilenumbrüchen (also Systemumbrüchen) abhängen. In einer einfachen Welt wäre der nächste Schritt also, eine gute Verteilung von Zeilenumbrüchen zu suchen.
Leider ist die Realität aber nicht so einfach, denn der Raum, den ein Objekt einnimmt, hängt seinerseits von den gewählten Zeilenumbrüchen ab.
LilyPonds Lösung zu diesem Problem besteht darin, einen vorläufigen Positionierungs-Schritt vorzunehmen, der als die reine Phase bekannt ist. Die Umrisse und (relativen) Positionen aller Objekte werden, so gut es eben geht, geschätzt, um Werte für den Zeilenumbruch und die spätere unreine Phase bereitzustellen.
Man kann eigene reine Funktionen in Scheme schreiben.
Zeilen- und Seitenumbruch#
Finding the ideal number of pages...
/Ideale Seitenanzahl wird gefunden...
undFitting music on x pages...
/Musik wird auf x Seiten angepasst...
Die Umbruch-Algorithmen versuchen, die Musik so gleichmäßig wie möglich auf die Seite(n) zu verteilen. Manche von ihnen versuchen auch andere Nebenbedingungen zu optimieren, etwa der „page turn breaking“-Algorithmus, der möglichst gute Umblätterstellen zu erreichen versucht.
Dieser Schritt ist nicht von Scheme aus zugänglich.
Unreines Positionieren#
Drawing systems...
/Systeme erstellen...
Sobald die gewählten Zeilenumbrüche verfügbar sind, können alle Objekte endgültig positioniert werden, und ihre Stencils (Matrizen, Druckerschwärze) werden erzeugt. Dieser Schritt kann mit Scheme beliebig angepasst werden.
Erstellen der Ausgabe#
Converting to `document.pdf'
/Konvertierung nach »dokument.pdf«...
In the end, the stencils are translated into PostScript language. Behind the scenes, this is converted by Ghostscript into a PDF file.