Musik ist hier Programm! (2)

(Dies ist Teil 2 der Serie „Musik ist hier Programm!“  – aber der erste mit richtig Musik und Code.  Die Einleitung gibt’s hier.)

Wenn Musikexperten über Musik (egal ob Klassik, Jazz oder Pop) sprechen, klingt das häufig ziemlich mathematisch: „Die 5. Stufe einer Moll-Tonleiter entspricht der 3. Stufe der parallelen Dur-Tonleiter“, „der C7/9/13-Akkord“, „die II-V-I-Verbindung“ (hier sogar mit römischen Zahlen!) usw.  Das liegt daran, dass Musik auf eine absolut faszinierende Weise kreative Freiheit mit mathematischer Regelhaftigkeit verbindet — und gerade in diesem scheinbaren Widerspruch liegt ein großer Reiz.

Auch wir fangen mathematisch an, nämlich indem wir die Halbtonschritte, aus denen die chromatische Tonleiter besteht, durchnummerieren.  Also: c=0, cis=1, d=2, dis=3, …, h=11, c (eine Oktave höher) = 12, usw.   Genauso macht das auch das MIDI-Protokoll (übersichtlich an einer Klaviatur dargestellt z.B. hier).

In Python können wir damit eine chromatische Tonleiter, d.h. 12 aufeinanderfolgende Halbtonschritte, ganz einfach so beschreiben:

chromatic_scale = range(13)

Mein Vorschlag wäre übrigens, dass ihr den interaktiven Python-Interpreter öffnet und die Beispiele direkt dorthin kopiert und ausführt.  Am Ende dieses Artikels findet ihr alle Definitionen auch nochmal am Stück.

Euer „Dialog“ mit dem interaktiven Interpreter könnte also ungefähr so aussehen (die letzte Zeile stellt die Ausgabe des Interpreters dar):

chromatic_scale = range(13)
list(chromatic_scale)
==> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

Für eine direkte MIDI-Ausgabe wäre das ja gar nicht schlecht… aber leider kann Lilypond mit Zahlen alleine noch nichts anfangen. Wir müssen unsere Zahlen also vor der Ausgabe in Notennamen umwandeln. Wie das Format für Notennamen von LilyPond aussieht, steht hier.

Für den Anfang bestimmen wir Notennamen und Oktavangabe mit der folgenden einfachen Funktion:

def midi2lily(pitch):
    names = ["c", "cis", "d", "dis", "e", "f", "fis", "g", "gis", "a", "ais", "b"]
    octave, semi = divmod(pitch, 12)
    octave_marker = "'" if octave > 0 else ","
    return names[semi] + abs(octave)*octave_marker

Mit einer List Comprehension können wir diese Funktion auf die gesamte chromatische Tonleiter anwenden:

[midi2lily(pitch) for pitch in chromatic_scale]
==> ['c', 'cis', 'd', 'dis', 'e', 'f', 'fis', 'g', 'gis', 'a', 'ais', 'b', "c'"]

Schön, schön. Bei der letzten Note sieht man auch, dass noch ein Apostroph angehängt wurde, weil dort eine neue Oktave beginnt. Die Funktion scheint also zu tun, was sie soll.

(Aber Achtung: Diese Funktion ist musikalisch oft nicht sinnvoll… z.B. sollte in F-Dur nicht ‚ais‘ verwendet werden, sondern ‚bes‘ (LilyPond-Notation für die Note „B“), damit nicht ein Ais (A mit Kreuzzeichen), sondern B (H mit B-Vorzeichen) geschrieben wird. Darum kümmern wir uns in der nächsten Folge.)

Indem wir nur bestimmte Töne aus der chromatischen Skala auswählen, können wir andere Tonleitern erzeugen. Das gehen wir bald systematisch an – für den Anfang können wir aber z.B. eine Ganztonleiter produzieren, indem wir nur jeden zweiten Halbton verwenden:

[midi2lily(pitch) for pitch in chromatic_scale if pitch % 2 == 0]
==> ['c', 'd', 'e', 'fis', 'gis', 'ais', "c'"]

Das sieht ja schon ganz gut aus!  Ein zweites Beispiel (unter Verwendung von itertools.chain):

from itertools import chain
motiv1 = [pitch for pitch in chromatic_scale if pitch % 2 == 0]
motiv2 = reversed(motiv1)
theme = chain(motiv1, motiv2)

[midi2lily(pitch) for pitch in theme]
==> ['c', 'd', 'e', 'fis', 'gis', 'ais', "c'", "c'", 'ais', 'gis', 'fis', 'e', 'd', 'c']

Ah, schön: Wir haben ein musikalisches Thema beschrieben, das aus einer aufsteigenden, gefolgt von einer absteigenden Ganztonleiter besteht.  Interessant daran ist v.a. die Art, wie wir das in Python ausgedrückt haben.  Das Thema besteht aus zwei Motiven, nämlich zuerst der Ganztonleiter und dann einer Wiederholung des ersten Motivs — aber rückwärts gespielt!  In der Musik nennt man so etwas „Krebs“. Indem wir das zweite Motiv mittels reversed(motiv1) definieren, können wir diese Krebs-Beziehung direkt und kompakt beschreiben.  Wir bewegen uns im Programm also auf einer viel höheren Abstraktionsebene als im späteren Notentext, wo der Zusammenhang zwischen den Motiven komplett verlorengeht.  Auch wenn das motiv1 ein anderes wäre —  motiv2 wäre immer dessen Krebs, denn wir haben nicht einzelne Noten definiert, sondern genau diese Beziehung zwischen den beiden Motiven.  Sehr spannend — und wir werden das noch viel weiter treiben!

Bisher erzeugen wir nur Python-Listen, die Notennamen enthalten.  Natürlich muss aus einer solchen Liste irgendwann ein String werden, der in einer Lilypond-Datei verwendet werden kann. Definieren wir uns also gleich mal:

def join(it):
    return " ".join(it)

Die folgende Zeile z.B. nimmt also jedes zweite Element der chromatischen Tonleiter, übersetzt es in einen Notennamen und fügt alle diese Namen zu einem String zusammen:

join(midi2lily(p) for p in chromatic_scale if p % 2 == 0)
==> "c d e fis gis ais c'"

Übrigens: Wer sich jetzt wundert, dass hier keine eckigen Klammern mehr vorkommen, d.h. die List Comprehension aus dem vorigen Beispiel irgendwie verschwunden ist, der hat gut beobachtet! join() wird hier mit einer „generator comprehension“ aufgerufen, d.h. es wird keine Liste, sondern ein Generator erzeugt, dessen konkrete Werte erst berechnet werden, wenn wirklich auf die einzelnen Elemente zugegriffen werden soll. Wer mehr über List Comprehensions und Generator Comprehensions wissen will, kann sich
ja mal diesen Artikel von Guido van Rossum himself anschauen oder auch diesen.

Bevor wir nun aber über Tonleitern und Akkorde nachdenken, sollten wir zuerst noch die Infrastruktur schaffen, um wirklich mit Lilypond Noten darstellen und sie als MIDI-Datei anhören zu können.  Die Ganztonleiter von gerade eben beispielsweise, müsste zumindest in die folgende Lilypond-Datei „verpackt“ werden:

\version "2.16.2"    % compatibility information for future LilyPond versions

\score {
{ c d e fis gis ais c'}

\layout { }

\midi { }
}

Diese Datei kann LilyPond dann verarbeiten und eine PDF-Datei erzeugen, die so aussieht:minimal-ausschnittHier ist vielleicht noch nicht alles, wie wir es uns vorstellen (wollten wir wirklich so tiefe Töne?), aber ein Anfang ist es allemal.

Wie bekommen wir unsere Daten also in das LilyPond-Format?  Ich benutze für so etwas gerne Pythons template strings. Damit bauen wir die Lilypond-Datei einfach nach, verwenden aber an den enscheidenden Stellen Platzhalter (hier z.B. $melody und $speed):

from string import Template

SIMPLE_LP_MELODY = """
\\version "2.16.2"   % compatibility information for future LilyPond versions

\\score {
{ $melody }

\\layout { }
\\midi { \\tempo 2 = $speed}
}
"""

Wir können das z.B. so benutzen:

>>> adict = dict(melody="c d e fis gis ais c'", speed=120)
>>> text = Template(SIMPLE_LP_MELODY).substitute(adict)
>>> print(text)

Auf Dauer noch hilfreicher wird das, wenn die Ersetzungen selbst wieder Platzhalter enthalten dürfen und wir dann (quasi rekursiv) so lange ersetzen, bis ein String ohne Platzhalter entstanden ist.  Das machen wir mit folgender Funktion:

def fill_template(tmpl, adict):
    previous = ""
    while tmpl != previous:
        previous = tmpl
        tmpl = Template(tmpl).safe_substitute(adict)
    return tmpl

Damit könnten wir im interaktiven Python Interpreter jetzt z.B. definieren:

motiv1 = [pitch for pitch in chromatic_scale if pitch % 2 == 0]
motiv2 = reversed(motiv1)
theme = chain(motiv1, motiv2)
melody = join(midi2lily(p) for p in theme)
speed = 120
text = fill_template(SIMPLE_LP_MELODY, locals())
print(text)

und erhalten als Ausgabe tatsächlich eine komplette LilyPond-Beschreibung:


\version "2.16.2"   % compatibility information for future LilyPond versions

\score {
{ c d e fis gis ais c' c' ais gis fis e d c }

\layout { }
\midi { \tempo 4 = 120}
}

Der obige Code enthält einen praktischen Python-Trick (an der Grenze zum Hack): Die Funktion locals() liefert ein Dictionary mit den lokal definierten Namen und ihren aktuellen Werten.  Wenn ich z.B. locals() in einer Funktion verwende, sind das alle dort definierten Variablen und die Aufrufparameter.  Im Klartext: Die Werte für die in SIMPLE_LP_MELODY verwendeten Platzhalter werden direkt aus den lokalen Variablen ermittelt, ohne dass wir dazu ein anderes Dictionary bauen müssen.  Wem das zu nahe an schwarzer Magie ist, der schreibt:

text = fill_template(SIMPLE_LP_MELODY, dict(melody=melody, speed=speed))

Egal in welcher Variante, die Variable text enthält nun eine sinnvolle Eingabe für LilyPond.  Also schnell in eine Datei damit:

def save_file(astring, fn="output.ly"):
    out_file = open(fn, "wt")
    out_file.write(astring)
    out_file.close()

save_file(text)

Damit wird der Text standardmäßig die Datei output.ly geschrieben.  Wenn wir LilyPond darauf loslassen (unter Windows z.B. so), erzeugt es für uns Noten (output.pdf)

ganztonund anhörbare Musik in output.midi.  Hat’s geklappt?  Die MIDI-Datei kann z.B. euer Webbrowser abspielen (mein Firefox zumindest kann es).  Leider kann ich sie aber weder bei wordpress.com noch bei soundcloud hochladen. Damit ihr unser erstes Musikstück anhören könnt, habe ich output.midi deshalb nach output.mp3 konvertiert:

Juhu, wir haben Musik produziert! Keine besonders schöne, zugegeben.  Ganztonleitern klingen irgendwie… seltsam.  Deswegen werden wir uns als Nächstes mit Tonleitern beschäftigen, die uns vertrauter sind: Dur, Moll, aber auch exotisch Klingenderes wie Phrygisch oder Mixolydisch (das ist z.B. die Tonart von Norwegian Wood) sind nur noch wenige Programmzeilen entfernt.

Aber das machen wir beim nächsten Mal.  Dieser Beitrag ist lange genug geworden und bietet hoffentlich schon einige Ansatzpunkte zum Experimentieren und Weiterdenken.  Da ich wegen der anstehenden Prüfungen hier mindestens zwei Wochen nicht weiterschreiben kann, könnt ihr euch ja gerne selbst schon mal überlegen, wie ihr  Tonarten modellieren würdet.  Es gibt unendlich viele Wege das zu tun.

Viel Spaß beim Ausprobieren! Über Kommentare, Fragen und Anregungen freue ich mich sehr.

Hier noch mal der vollständige Code, den wir heute entwickelt haben, inklusive des letzten Beispiels.  Diesen Code kann man so als Datei speichern und von Python3 ausführen lassen:

from string import Template

def midi2lily(pitch):
    names = ["c", "cis", "d", "dis", "e", "f", "fis", "g", "gis", "a", "ais", "b"]
    octave, semi = divmod(pitch, 12)
    octave_marker = "'" if octave > 0 else ","
    return names[semi] + abs(octave)*octave_marker

def join(it):
    return " ".join(it)

SIMPLE_LP_MELODY = """
\\version "2.16.2"   % compatibility information for future LilyPond versions

\\score {
  { $melody }

  \\layout { }
  \\midi { \\tempo 2 = $speed}
}
"""

def fill_template(tmpl, adict):
    previous = ""
    while tmpl != previous:
        previous = tmpl
        tmpl = Template(tmpl).safe_substitute(adict)
    return tmpl

def save_file(astring, fn="output.ly"):
    out_file = open(fn, "wt")
    out_file.write(astring)
    out_file.close()

## Beispiel
from itertools import chain

chromatic_scale = range(13)
motiv1 = [pitch for pitch in chromatic_scale if pitch % 2 == 0]
motiv2 = reversed(motiv1)
theme = chain(motiv1, motiv2)
melody = join(midi2lily(pitch) for pitch in theme)
speed = 120
text = fill_template(SIMPLE_LP_MELODY, locals())
save_file(text)
Advertisements

Ein Gedanke zu „Musik ist hier Programm! (2)

  1. Pingback: Musik ist hier Programm! (1) | Zurück in die Schule

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s