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../../images/1419f2324628369f3f346cc2d60ddec928f073ae73d51fc8a4497abed3000fb8.svg

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../../images/dba364630ef486feacced35641836c20498916973aca29a0311f4636461ff66f.svg

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../../images/721f5254581dfdf9d2c9ffdc9f9d5cda05b929a0aaf6f05b4d06344db412ba78.svg

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../../images/05e2bf9b2b2464a2e2c213557ed617a0496bf1120a365520f852d4e29352b678.svg

Die häufigsten Typenprädikate sind:

Name des Prädikats

Akzeptierte Werte

number?

Jeder numerische Wert (Ganzzahl, Gleitkomma, Bruch)

integer?

Ganze Zahlen

index?

Nichtnegative ganze Zahlen

string?

Ein String (Zeichenkette, etwa "Ein paar Worte"

markup?

Ein \markup-Block; es werden auch Strings akzeptiert.

boolean?

Ein Boolescher Wert, also WAHR/true (#t) oder FALSCH/false (`#f‘)

ly:music?

Ein Musik-Ausdruck

ly:pitch?

Eine Tonhöhe (zum Beispiel die ersten beiden Argumente von \transpose c des { ... })

ly:duration?

Eine Dauer (wie das optionale Argument zu \tuplet: \tuplet 3/2 8. { ... })

color?

Eine Farbe, entweder als String oder als RGB-Liste (siehe [Handbuch](notation:Coloring objects))

list?

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../../images/9d4510ba84c7fdca42d9a341968d32d0943dcafb372960a6166c6a8a1642f1b0.svg

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../../images/7d551c611522b22c876a3b96ab7e75cc51c05f5f29ebb77a683c14e81c27ff6c.svg

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 \bookparts; diejenigen in \bookpart überschreiben die des umgebenden \books, 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) und Voice (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. Der Simultaneous_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... und Fitting 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.