Musikalische Funktionssymbole mit 3

Dieses Beispiel geht zurück auf die Frage, wie man mit musikalische Funktionssymbole für Tonika, Dominate etc. zusammen mit den Ziffern für die Stimmführung (das Voicing) darstellen kann, die vor einiger Zeit auf TeX.SX gestellt wurde. Bei der Lösung des Problems wollen wir uns vor allem mit den neuen Funktionen von 3 vertraut machen, sodass dieses Beispiel auch für Nicht-Musiker interessant bleibt.

Einleitung

Wir wollen einen Befehl entwickeln, der es uns ermöglicht, Funktionssymbole und Bewegungen der einzelnen Stimmen (den „Melodien“ aus denen der Akkord sich zusammensetzt) wie in diesem Bild darzustellen:

Funktionssymbol der Doppeldominante mit Bezifferung

Diese Darstellung besteht aus drei Elementen: 1. Dem Funktionsymbol – hier für die sog. Doppeldominante – 2. den Fortschreitungen der Oberstimmen sowie 3. der der Bassstimme, wobei letztere optional ist. Für die Umsetzung werden wir aber alle Stimmen als Zeilen einer Tabelle behandeln. Außerdem führen wir ein Raster ein, um die Ziffern untereinander ausrichten zu können. Neben den Ziffern benötigen wir noch die Möglichkeit, Leerräume und vertikale Linien zu erzeugen.

Für die Eingabe soll folgende Syntax gelten:

\fsymb{Symbol}[Bass]{Oberstimmen}

Die Stimmen wollen wir in der Form ..8--7-- notieren können, wobei der Punkt einen Leerschritt erzeugen und die Trennstriche zu einer Linie werden sollen. Mehrere Oberstimmen wollen wir durch ein Komma getrennt eingeben, so dass für das obige Bild diese Eingabe nötig wäre:

\fsymb{DD}[8----7]{4---3-, 6---5-, ..{11}---}

Neben der Verarbeitung der Stimmen brauchen wir also auch noch etwas, das aus DD das korrekte Symbol macht. Darüber hinaus werden wir noch ein zweites optionales Argument einführen, um den Abstand zwischen Symbol und Stimmverlauf verändern zu können:

\fsymb{Symbol}[Bass][Abstand]{Oberstimmen}

3-Grundlagen

Bevor wir nun aber mit der Programmierung beginnen können, müssen wir uns mit ein paar Grundlagen von 3 vertraut machen. 3 bietet uns viele Makros und Funktionen, die das Programmieren von eigenen Befehlen wesentlich erleichtern. Es gelten die folgenden Konventionen:

  • Wir unterscheiden zwischen Variablen (und Konstanten), die Werte repräsentieren, und Befehlen (Funktionen), die auf diese Variablen wirken können und/oder andere Aufgaben ausführen.
  • Module fassen Variablen und Befehle zusammen.
  • Jeder Variablen-/Befehlsname enthält eine Bezeichnung für das Modul, zu dem er gehört. Wir werden in diesem Beispiel mfs (musical function symbol) benutzen.
  • Leerzeichen werden ignoriert; wird ein Leerzeichen benötigt, kann man es mit Tilde ~ eingeben. Daher ist es auch nicht nötig, Zeilen mit % zu beenden (vgl. FAQ 6).
  • Der Unterstrich _ wird als Teil von Befehls- und Variablennamen benutzt, um diese lesbar zu machen (analog zum @, das in klassischen Paketen benutzt wird).
  • Ein Variablenname (Konstantenname) beginnt immer mit \l_modul_, \g_modul_ oder \c_modul_, um anzuzeigen, ob der Wert der Variablen sich lokal ändern kann, seine Änderung immer global ist oder eine Konstante (c) bildet. Ein Variablenname endet mit der Angabe des Typs, z. B. _int für natürliche Zahlen (integer), _dim für Längen (dimension) oder _tl für sog. Token-Listen (eine Folge von Zeichen und Befehlen, siehe auch What is a token?).
    Beispiel: \c_mfs_boxwidth_dim.
  • Ein Befehlsname beginnt immer mit \modul_ und endet mit :arg, wobei arg für die Angabe der Argumente des Befehls steht. Bspw. nn für zwei „normale“ Argumente in geschweiften Klammern oder N für ein Einzel-Token wie etwa eine Variable. Hat ein Befehl keine Argumente, endet sein Name mit einem Doppelpunkt.
    Beispiel: \mfs_process_list:n.

Eine ausführlichere Einführung in die neuen Konventionen inkl. der vollständigen Listen der Variablen- und Argumenttypen findet man in The expl3 package and LaTeX3 programming.

Datei funktionssymbole.tex

Wir laden die Dokumentklasse (FAQ 2) mit zwei Optionen, nämlich der Sprachoption sowie zudem mit der Option parskip, um einen Absatzabstand anstelle des Einzuges einzustellen. Das macht die Ausgabe der Testsymbole etwas übersichtlicher.

\documentclass[
   ngerman,
   parskip = full,
]{scrartcl}

Als nächstes laden wir wie üblich die Pakete zur Kodierung (FAQ 3), Schrift (FAQ 4) und Sprache (FAQ 5).

\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{lmodern}
\usepackage{babel}

Das Paket xparse laden wir, um die neue Syntax zur Befehlsdefinition verwenden zu können. Außerdem lädt dieses Paket expl3, was uns wiederum die 3-Funktionen zur Verfügung stellt.

\usepackage{xparse}

Um die 3-Syntax und die o. g. Konventionen nutzen zu können, aktivieren wir diese mit \ExplSyntaxOn. Die wichtigste Änderung: Ab hier werden Leerzeichen, Zeilenumbrüche und Leerzeilen ignoriert!

\ExplSyntaxOn

Konstanten

Jetzt definieren wir ein paar Konstanten. Dafür nutzen wir jeweils den \x_new:N-Befehl gefolgt vom Namen und dann \x_set:Nn, um den Wert der Konstante zu setzen (x = Typ).

Eine Länge, die die Breite des Rasters (s. Abb. oben) angibt. (Zu Einheiten siehe FAQ 12.)

\dim_new:N \c_mfs_boxwidth_dim
\dim_set:Nn \c_mfs_boxwidth_dim { 0.5em }

Um später die horizontalen Linien auszugeben definieren wir die Token-Liste \c_mfs_line_tl mithilfe von \rule als eine 0,3 pt dicke Linie, die 0,7 ex über der Grundlinie liegt, mit der Breite \c_mfs_boxwidth_dim

\tl_new:N \c_mfs_line_tl
\tl_set:Nn \c_mfs_line_tl {
   \rule [ 0.7ex ] { \c_mfs_boxwidth_dim } { 0.3pt }
}

Analog dazu definieren wir den Leerschritt mit \hspace als einen unsichtbaren Abstand derselben Breite.

\tl_new:N \c_mfs_space_tl
\tl_set:Nn \c_mfs_space_tl
  {
    \hspace { \c_mfs_boxwidth_dim }
  }

Da Leerzeichen ignoriert werden, setzt man in der neuen Syntax alle Klammern ab, was die Lesbarkeit erhöht. Die Konvention fordert außerdem, dass die Klammer eines mehrzeiligen Argumentes alleine in einer Zeile steht und man nur mit zwei Leerzeichen einrückt, wie bei \c_mfs_space_tl; ich bevorzuge aber meine gewohnte Schreibweise.

Spezielle Funktionssymbole

In der musiktheoretischen Funktionsanalyse gibt es neben den Funktionen, die durch normale Buchstaben wie T, S, D, tP etc. dargestellt werden, u. a. auch noch spezielle Symbole für die Doppeldominante (Doppeltes D, s. o.), die verkürzte Dominante (durchgestrichenes D) und die Doppelsubdominante (doppeltes S). Für diese Sonderzeichen definieren wir ebenfalls Token-Listen-Konstanten.

Das Symbol für die Doppeldominante erzeugen wir, indem wir ein D mit \raisebox um 0,2 em anheben und dieses dann mit \makebox in eine Box verpacken, deren Breite unabhängig vom Inhalt immer 0,2 em beträgt. Ihr Inhalt ist an der linken Kante ([l]) ausgerichtet. Da die Box schmaler ist, als das erhöhte D, überlappt dieses das folgende normale D und es entsteht das gewünschte Symbol.

Bei einer anderen Schriftart müsste man die Werte anpassen.

\tl_new:N \c_mfs_double_dominant_tl
\tl_set:Nn \c_mfs_double_dominant_tl {
   \makebox [ 0.2em ] [ l ] {
      \raisebox { 0.2em } { D }
   }
   D
}

Beim Symbol für die verkürzte Dominante (diesem Akkord fehlt der Grundton) gehen wir ganz ähnlich vor, nur dass wir dieses mal einen kleinen Abstand (\hspace) gefolgt von einem Schrägstrich in eine Box mit der Breite 0 em verpacken, um diesen über ein D zu legen. In der Box verringern wir mit \small außerdem die Schriftgröße, um einen etwas kleineren Schrägstrich zu erhalten.

Die Schriftgrößenänderung wirkt natürlich auch auf die Einheit em und damit auf den \hspace in der Box.

\tl_new:N \c_mfs_dominant_without_root
\tl_set:Nn \c_mfs_dominant_without_root {
   \makebox [ 0em ] [ l ] {
      \raisebox { 0.25ex } { \small \hspace { 0.2em } / }
   }
   D
}

Das Symbol für die Doppelsubdominante bauen wir genau wie das für die Doppeldominante nur mit etwas anderen Werten und S anstelle von D.

\tl_new:N \c_mfs_double_subdominant_tl
\tl_set:Nn \c_mfs_double_subdominant_tl {
   \makebox [ 0.1em ] [ l ] {
      \raisebox { 0.2em } { S }
   }
   S
}

Interne Befehle

Jetzt definieren wir zwei interne Hilfsbefehle. Dazu verwenden wir \cs_new_protected:Npn (cs = control sequence), jeweils gefolgt vom Namen des Befehls (= N), der Argumentspezifikation (= p) und schließlich der Definition (= n). Die Argumentspezifikation erfolgt wie beim klassischen \def, in dem man für jedes Argument, dass der Befehl später haben soll eine Raute # gefolgt von einer Ziffer (19) angibt.

Die Argumentspezifikation kann auch komplexer sein, zum Beispiel im Fall von sog. delimited Arguments, das führt an dieser Stelle aber zu weit.

Unser erster Hilfsbefehl \mfs_makebox:n dient dazu, das ihm später zu übergebende Argument in eine Box mit zentriertem Inhalt ([c]) und der Breite \c_mfs_boxwidth_dim zu verpacken (siehe auch FAQ 10).

\cs_new_protected:Npn \mfs_makebox:n #1 {
   \makebox [ \c_mfs_boxwidth_dim ] [ c ] { #1 }
}

Der nächste Befehl, den wir definieren, hat ebenfalls ein Argument und soll uns dazu dienen, eine Stimmbewegung zu verarbeiten.

\cs_new_protected:Npn \mfs_process_list:n #1 {

Dazu setzen wir zunächst den Wert der temporären und von expl3 für solche Zwecke bereitgestellten Variable \l_tmpa_tl gleich dem Wert des Argumentes.

   \tl_set:Nn \l_tmpa_tl { #1 }

Mithilfe von \tl_replace_all:Nnn können wir in dieser Liste nun alle - bzw. . durch die zuvor definierten Konstanten für vertikale Linien bzw. Leerräume ersetzen.

   \tl_replace_all:Nnn \l_tmpa_tl { - } { \c_mfs_line_tl }
   \tl_replace_all:Nnn \l_tmpa_tl { . } { \c_mfs_space_tl }

Mit \tl_map_function:NN geben wir die Token-Liste dann aus, wobei wir auf jedes Token die Funktion \mfs_makebox:n anwenden. Es wird also jedes Token in einer eigenen Box mit der von uns vorgegebenen Breite ausgegeben.

   \tl_map_function:NN \l_tmpa_tl \mfs_makebox:n

Wir wissen, dass jede Stimmbewegung später eine Zeile in einer Tabelle sein soll, weshalb wir ihre Ausgabe mit \\ beenden.

   \\

Ende der Definition von \mfs_process_list:n

}

Der Benutzerbefehl \fsymb

Nachdem wir nun alles vorbereitet haben, können wir mit \NewDocumentCommand FAQ 11 den eigentlichen benötigten Befehl \fsymb definieren.

Wie oben gesagt, soll der Befehl je zwei obligatorische und optionale Argumente haben. Der Standardwert für das erste optionale Argument – die Bassstimme – ist leer und der für das zweite Argumente – den Abstand zwischen Symbol und Stimmen – ist 0 pt.

\NewDocumentCommand { \fsymb } { m O{} O{0pt} m } {

Wie schon in der vorigen Definition setzen wir zunächst \l_tmpa_tl auf den Wert des ersten Argumentes (Symbol).

   \tl_set:Nn \l_tmpa_tl { #1 }

In dieser Token-Liste ersetzen wir die Abkürzungen DD, /D und SS durch die richtigen Symbole. Alles was nicht von dieser Ersetzung betroffen ist, wird ausgegeben, wie der Nutzer es auch eingeben hat.

   \tl_replace_all:Nnn \l_tmpa_tl { DD } { \c_mfs_double_dominant_tl }
   \tl_replace_all:Nnn \l_tmpa_tl { /D } { \c_mfs_dominant_without_root }
   \tl_replace_all:Nnn \l_tmpa_tl { SS } { \c_mfs_double_subdominant_tl }

Mit \tl_use:N geben wir die so bearbeitete Variable aus.

   \tl_use:N \l_tmpa_tl

Mit \kern fügen wir nun einen Abstand in der durch das dritte Argument vorgegebenen Breite ein.

   \kern#3

Als nächstes setzen wir eine Komma-Listen-Variable auf den Wert des vierten Argumentes. Damit ist jede Stimmbewegung der Oberstimmen, die wir angeben, ein Element in \l_tmpa_clist.

   \clist_set:Nn \l_tmpa_clist { #4 }

Da wir die Tabelle von oben nach unten ausgeben, wir die Stimmen in der Liste aber von unten nach oben angeben wollen, kehren wir die Reihenfolge von \l_tmpa_clist um.

   \clist_reverse:N \l_tmpa_clist

Mit der so vorbereiteten Stimmenliste können wir uns nun an die Ausgabe machen. Dazu beginnen wir eine Gruppe (FAQ 7), in der wir die Schriftgröße auf \tiny und den Wert für \arraystretch auf 0,75 (siehe vorletztes Beispiel) setzen.

   {
      \tiny
      \renewcommand { \arraystretch } { 0.75 }

Die Tabelle verpacken wir in einer Box, die wir um 0,68 ex nach unten verschieben.

      \raisebox { -0.68ex } {

Die Tabelle selbst soll mit der untersten Zeile an der (verschobenen) Grundlinie ausgerichtet werden ([b]) und eine Spalte (l) haben (zu Tabellen siehe auch das vorletzte Beispiel).

         \begin{tabular} [ b ] { @ { } l @ { } }

Mit \clist_map_function:NN geben wir die Komma-Liste der Stimmbewegungen aus, wobei wir auf jedes Element die Funktion \mfs_process_list:n, die wiederum eine einzelne Stimmbewegung verarbeitet und als Tabellenzeile ausgibt, anwenden.

            \clist_map_function:NN
               \l_tmpa_clist
               \mfs_process_list:n

Zum Abschluss der Oberstimmen fügen wir eine Leerzeile in die Tabelle ein, der ein negativer vertikaler Abstand der Höhe 1,2 ex folgt.

            \\ [ -1.2ex ]

Die Bassstimme können wir direkt an \mfs_process_list:n übergeben, da es immer nur eine geben kann. Gibt der Nutzer gar keine Bassstimme an, wird eine leere Zeile ausgegeben.

            \mfs_process_list:n { #2 }

Ende der Tabelle

         \end{tabular}

Ende der \raisebox

      }

Ende der Gruppe

   }

Ende der Definition von \fsymb

}

Jetzt schalten wir die 3-Syntax wieder ab.

\ExplSyntaxOff

weitere Definitionen

Zum Testen der Ausrichtung der Symbole und Stimmen, definieren wir diverse Befehle, die jeweils eine farbige Linie ausgeben, die so breit ist wie der Text, aber in einer Box mit der Breite 0 pt verpackt ist. Um die Linien zu färben, nutzen wir die Funktionen von xcolor (siehe Beispiel vom Jahresanfang).

Die folgenden Definitionen dienen uns nur zum Testen der Ausrichtung, für die Funktion von \fsymb sind sie irrelevant.

\usepackage{xcolor}
\newcommand{\showBaseLine}{\makebox[0pt][l]{\color{magenta}\rule{\textwidth}{0.1pt}}}
\newcommand{\showXHeightLine}{\makebox[0pt][l]{\color{yellow}\rule[1ex]{\textwidth}{0.1pt}}}
\newcommand{\showMidLine}{\makebox[0pt][l]{\color{cyan}\rule[0.785ex]{\textwidth}{0.1pt}}}
\newcommand{\showCapHeightLine}{\makebox[0pt][l]{\color{magenta}\rule[1.57ex]{\textwidth}{0.1pt}}}
\newcommand{\showAllLines}{\showBaseLine\showXHeightLine\showMidLine\showCapHeightLine}
\begin{document}

Test

Nun gilt es, den Befehl \fsybm zu testen.

Zunächst testen wir die verschiedenen Kombinationen, in denen Stimmen angeben werden können, wobei wir das von uns definierte \showAllLines benutzen, um die Ausrichtung beurteilen zu können.

\showAllLines Xx \qquad
\fsymb{D}{8-7} \enskip
\fsymb{D}[8-7]{} \enskip
\fsymb{D}[8-7]{4-3} \enskip
\fsymb{D}[8--7]{4-3,6-5}

Dann probieren wir die Ausrichtung mit längeren Linien aus.

\fsymb{T}{..7-8,3---4} \enskip
\fsymb{D}{4---3, 6-5, ..7-8} \enskip
\fsymb{D}{4----3, 6-5---, ..7-8-,{11}-----} \enskip
\fsymb{s}{5,6}

Auch die korrekte Verarbeitung der Spezialsymbole sollte klappen.

\fsymb{DD}{8-7} \enskip
\fsymb{DD}[8----7]{4---3-, 6---5-,..{11}---} \enskip
\fsymb{/D}{8-7} \enskip
\fsymb{SS}{5,6}

Über das zweite optionale Argument können wir den Abstand zwischen Symbol und Stimmen fine tunen.

\fsymb{D}[]{8-7} \enskip
\fsymb{D}[4-3]{} \enskip
\fsymb{D}[4-3]{8-7}
 
vs.
 
\fsymb{D}[][-1pt]{8-7} \enskip
\fsymb{D}[4-3][-2.3pt]{} \enskip
\fsymb{D}[4-3][-1pt]{8-7}

Als letztes testen wir noch das Verhalten des Befehls in Überschriften (und damit auch im Inhaltsverzeichnis) und im Fließtext, wobei dort höchsten Konstrukte mit einer Oberstimme stehen sollten, um den Zeilenabstand einhalten zu können.

\clearpage
\tableofcontents
 
\section{Der Quartsextvorhalt \fsymb{D}{4-3,6-5}}
 
Das ist ein Test, wie die Funktionssymbole sich im Fließtext verhalten.
Das ist ein Test, wie die Funktionssymbole sich im Fließtext verhalten.
\fsymb{D}[1-----]{4----3, 6-5---, ..7-8-,{11}-----} Das ist ein Test,
wie die Funktionssymbole sich im Fließtext verhalten. Das ist ein Test,
wie die Funktionssymbole sich im Fließtext verhalten.
\end{document}

Weiterlesen …

Neben dem oben bereits genannten The expl3 package and LaTeX3 programming sie die vollständige Dokumentation zu allen 3-Funktionen The LaTeX3 Interfaces empfohlen.