Haskore Ein System zur Beschreibung und Interpretation von Musik Eine Arbeit von: Ulrike Georgi im Sommersemester 2006 an der Philipps-Universität Marburg zum Seminar „Fun of Programming“ Veranstaltungsleiterin: Prof. Dr. Rita Loogen Hauptgrundlage dieser Arbeit bildet das Haskore Music Tutorial von Paul Hudak: http://www.haskell.org/haskore/Haskore/Haskore/Docs/tutorial.ps Inhaltsverzeichnis Kurzzusammenfassung...................................................................................................... 1 Einführung......................................................................................................................... 1 Grundlagen........................................................................................................................ 2 Kapitel 1: Wichtige Datentypen und Basisfunktionen...................................................... 2 1.1: Die Datentypen in Haskore....................................................................................2 1.2: Einige grundlegende Funktionen........................................................................... 3 Kapitel 2: Von der Notenvorlage zum Haskore-Code.......................................................5 2.1: Interpretation der ersten und zweiten Stimme....................................................... 5 2.2: Interpretation der Begleitakkorde.......................................................................... 7 Kapitel 3: Vom Haskore-Code zur MIDI-Datei................................................................ 9 Kapitel 4: Musik und die Macht der Mathematik............................................................11 4.1 Musik nach Zahlen................................................................................................12 4.2 Musik – rekursiv bis ins Unendliche.................................................................... 12 Schlusswort......................................................................................................................14 Anhang: Der vollständige Beispiel-Sourcecode..............................................................16 Quellenverzeichnis.......................................................................................................... 18 Kurzzusammenfassung Das „Haskore Music System“ ist eine Sammlung von Haskell-Funktionen und -Modulen, welche, teils vordefiniert und teils speziell zu diesem Zweck geschrieben, der Beschreibung und Interpretation von Musikstücken dienen sollen. Neben der nützlichen algebraischen Notation der Notendaten baut Haskore die musikalische Interpretation vor allem auf der Verwendung von Listenoperationen auf und bietet dem Musik-Programmierer hiermit komfortable Möglichkeiten, etwa Umkehrungen, Wiederholungen oder Transpositionen auszudrücken. Als praktische Anwendungsmöglichkeit bietet Haskore einen Export in Dateien nach dem General MIDI1 Standard, so dass Musik tatsächlich nicht nur beschrieben, sondern auch hörbar gemacht werden kann. Die Wahl der zugrunde liegenden Programmiersprache Haskell erweist sich durch den einfachen funktionalen Aufbau der Musikinterpretationen schnell als sinnvoll, denn Haskore definiert und untermauert die Algebra der Musik. Einführung Diese Arbeit beschäftigt sich mit dem Haskore Music System von Paul Hudak. Sie soll dem Leser anhand eines Beispielliedes, welches Stück für Stück in Haskore interpretiert und beschrieben und schließlich in eine abspielbare MIDI-Datei exportiert wird, einen Teil der umfassenden Möglichkeiten des Systems vorstellen. Zunächst werde ich im ersten Kapitel einige grundlegende Funktionen und Datentypen von Haskore vorstellen und den Aufbau des Systems kurz umschreiben. Im zweiten Kapitel stelle ich das für diese Arbeit verwendete Musikstück vor und beschreibe die ersten Schritte bei der Umwandlung der vorliegenden Noten in eine mögliche HaskoreNotation. Kapitel 3 beschreibt den Export der fertigen Musikumschreibung in eine Datei nach General MIDI Standard. Im Anschluss hieran beschäftige ich mich mit der Frage, warum als Grundlage für die Musikinterpretation gerade Haskell als Programmiersprache ausgewählt wurde und gehe noch einmal auf die Möglichkeiten des Systems ein. In allen vorgenannten Kapiteln werden, wenn nötig, auch musikalische Hintergründe erklärt. 1 MIDI = Abk. für Musical Instrument Digital Device 1 Grundlagen Das „Haskore Music System“ besteht aus einer Zusammenstellung mehrerer HaskellModule, die von Professor Paul Hudak von der Yale University als Erweiterung der Programmiersprache Haskell für die Verwendung des Interpreters Hugs ( http://www.haskell.org/hugs ) oder des Compilers GHC ( http://www.haskell.org/ghc ) konzipiert wurde. Ich habe zur Arbeit mit dem Haskore-System den Hugs98-Interpreter in der Version für Windows vom November 2002 verwendet. Die zu Haskore gehörigen Source-Dateien werden zur Verwendung in das /lib-Verzeichnis im HugsInstallationspfad kopiert und können hernach im Interpreter durch Aufruf des Befehls :load Haskore geladen werden. Die Verwendung einer neueren Version des Interpreters gestaltet sich als schwierig, da das Library-System nach der genannten Version etwas abgeändert wurde. Um die Funktionen und Typen der einzelnen Kapitel in eigenen Modulen verwenden zu können, müssen diese die Module Haskore und HaskToMidi importieren. Die Hauptgrundlage meiner Erläuterungen bildet das Haskore Music Tutorial, welches von Paul Hudak selbst als umfassende Einführung in sein System (in englischer Sprache) verfasst wurde Das Tutorial liegt in einer PostScript- sowie in einer HTMLVersion vor und kann, wie die Bibliothek selbst, von http://www.haskell.org/haskore frei heruntergeladen beziehungsweise eingesehen werden. Kapitel 1: Wichtige Datentypen und Basisfunktionen Die hier erläuterten Funktionen und Typen finden sich im Modul Basics.lhs 1.1: Die Datentypen in Haskore Strukturell betrachtet ist Musik die Über- oder Aneinanderreihung einzelner Töne. Ein Ton kann dabei hörbar sein, also eine Note mit Eigenschaften wie Lautstärke und Geräuschart besitzen, oder er kann unhörbar sein, eine Pause (engl. rest) in der Klangfolge. Noten und Pausen haben jeweils eine spezifische Dauer (engl. dur, duration) Das einfachste Musikstück ist also eine einzelne Note oder eine einzelne Pause. Kompliziertere Stücke sind Kombinationen dieser Grundelemente. Entsprechend dieser Überlegungen gibt es in Haskore den Grunddatentyp Music, der wie folgt definiert ist (Code 1): 2 01 data Music = Note Pitch Dur [NoteAttribute] 02 | Rest Dur 03 | Music :+: Music 04 | Music :=: Music 05 | Tempo (Ratio Int) Music 06 | Trans Int Music 07 | Instr IName Music 08 | Player PName Music 09 | Phrase [PhraseAttribute] Music 10 deriving (Show, Eq) Code 1: Datentyp Music Ein Music-Objekt ist also entweder eine Note (01), eine Pause (02), eine Aneinanderreihung der vorgenannten (03), eine Übereinanderlegung (04), Musik mit veränderter Geschwindigkeit oder Tonhöhe (05 – 06) oder Musik mit bestimmten Eigenschaften, die vornehmlich für das Erzeugen von Dateien nach dem General MIDI Standard vonnöten sind (07 – 09) und bildet damit eine rekursive Struktur. Das Symbol :+: ist als Aneinanderreihung zu verstehen, während :=: die Gleichzeitigkeit bzw. Übereinanderlegung ausdrückt. Pausen besitzen lediglich eine Dauer. Weitere Attribute werden nicht benötigt. Eine Note besitzt in Haskore die Eigenschaften Höhe (Pitch), Dauer (Dur von duration) und eine Liste von zusätzlichen Eigenschaften wie etwa Lautstärke des Tones. Zur Eigenschaft der Tonhöhe gehört die Angabe des Namens des Tons sowie der Oktave, in welcher der Ton steht. Die Notennamen sind im Datentyp PitchClass notiert. Octave ist ein beliebiger ganzzahliger Wert. Code 2 zeigt die Definition. 01 02 03 04 type Pitch = (PitchClass,Octave) data PitchClass = Cf | C | Cs | Df | ... | Bf | B | Bs deriving (Eq,Ord,Ix,Show,Read) type Octave = Int Code 2: Typ Pitch 1.2: Einige grundlegende Funktionen Der vorgenannte Datentyp PitchClass definiert somit die insgesamt 21 Namen der zwölf Halbtöne der Tonleiter2. Jeder möglichen PitchClass wird, entsprechend der zwölf Halbtöne, mittels der Funktion pitchClass ein Wert von -1 bis 12 zugeordnet. 0 – 11 sind dabei die Werte der Noten C bis B, Cs erhält den Wert -1, da es der letzte Ton (also gleich B) in der darunterliegenden Oktave ist und entsprechend ist der Wert von Bf 12, als erster Ton (entsprechend C) der folgenden Oktave. Unter Zuhilfenahme 2 Die C-Tonleiter besteht aus den sieben Grundtönen C, D, E, F, G, A und B (deutsch H), sowie den Zwischentönen Cf, Df, Ff, Gf und Bs (im Deutschen: Cis, Dis, Fis, Gis und B). Die insgesamt 21 Notennamen ergeben sich aus der Gleichbedeutung von zum Beispiel Cf und Ds 3 der Oktavennummer kann mit dem Notenwert der Absolutwert der Tonhöhe, in Haskore absPitch, ermittelt werden. Die Umkehrfunktion pitch ermittelt aus dem Absolutwert wiederum den Notennamen sowie den Oktavwert. Zur Reihung von Musik gibt es die Infix-Operatoren :+: und :=:. Ersterer verbindet je zwei Music-Objekte aufeinanderfolgend, während letzterer die Gleichzeitigkeit je zweier Music-Objekte beschreibt. Diese beiden Operatoren repräsentieren also das Bilden einer Notenzeile beziehungsweise von Akkorden und Mehrstimmigkeit. Um aber eine Klangfolge von n Noten/Pausen nicht mit n-1 dieser Operatoren verbinden zu müssen, sind in Haskore zusätzlich die Funktionen line und chord vordefiniert. Beide erhalten als Argument eine Liste mit Elementen vom Typ Music und bilden daraus wiederum ein Music-Objekt, durch Listenfaltung mit dem Operator :+: bzw. :=:. Die Funktion lineToList ist die Umkehrung von line und liefert aus einem mit line erzeugten Music-Objekt eine entsprechende Liste zurück. Die Funktion dur erwartet ein Music-Objekt als Eingabe und liefert einen Int-Wert zurück, dieser bestimmt die Gesamtdauer des Stückes. Hudak definiert zusätzlich einige Konstanten, die bei der Musikbeschreibung immer wieder gebraucht werden (Code 3). Die Zeilen 01 bis 24 zeigen die Notenabkürzungen, die in der Noteninterpretation benutzt werden können. Es wird ein Int-Wert als Oktavenangabe erwartet, um die Höhe der Note zu bestimmen. Die Zeilen 25 ff. definieren die Dauern der Noten und Pausen.3 01 02 03 .. 24 cf,c,cs,df,d,ds,ef,e,es,ff,f,fs,gf,g,gs,af,a,as,bf,b,bs :: Octave -> Dur -> [NoteAttribute] -> Music cf o = Note (Cf,o); ... bs 0 = Note (BS,o); 25 26 27 28 bn,hn,qn,en,sn,tn,sfn :: Dur dwn,dhn,dqn,den,dsn,dtn,ddhn,ddqn,dden :: Dur bnr,wnr,hnr,qnr,enr,snr,tnr :: Music dwnr,dhnr,dqnr,denr,dsnr,dtnr,ddhnr,ddqnr,ddenr :: Music 29 bn = 2; bnr = Rest bn; 30 wn = 1; wnr = Rest wn; .. ... ... 36 sfn = 1%64; sfnr = Rest sfn; 37 dwn = 3%2; dwnr = Rest dwn; .. ... ... 45 dden = 7%32; ddenr = Rest dden; Code 3: Abkürzungen4 und Konstanten 3 Punktierte (dotted) Noten besitzen die Länge der nicht-punktierten + Länge der nächst kürzeren (Bsp: punktierte Viertel = ¼ + 1/8 = 3/8). Doppelt punktierte Noten sind entsprechend nicht-punktierte + nächstkleinere + zweitkleinere (Bsp:doppelt punktierte halbe Note = ½ + ¼ + 1/8 = 7/8) 4 Abk.: n = note, r = rest, b = brevis, w = whole, h = half, q = quarter, e = eighth, s = sixteenth, t = thirty-second, sf = sixty-fourth, d = dotted (punktiert), dd = double-dotted (doppelt punktiert) 4 Kapitel 2: Von der Notenvorlage zum Haskore-Code Um die Interpretation und Beschreibung von Musik unter Verwendung des Haskore Music Systems aufzuzeigen, habe ich als Beispielstück das Volkslied Die Gedanken sind frei in einer zweistimmigen Variante mit Begleitakkorden ausgewählt. Abbildung 1: Notenvorlage "Die Gedanken sind frei" 2.1: Interpretation der ersten und zweiten Stimme Mit den im ersten Kapitel genannten Definitionen können nun die erste Stimme (die jeweils oberen Noten) sowie die zweite Stimme (die unteren Noten) in eine erste einfache Haskell-Notation umgewandelt werden. Zunächst definieren wir uns noch ein paar kleinere Hilfsfunktionen, die wir schon bald benötigen werden (Code 4): 01 fd d n = n d volFV 02 vol n = n volFV 03 fda d n = n d volSV 04 vola n = n volSV 05 volFV = [Volume 120] 06 volSV = [Volume 75] Code 4: Hilfsfunktionen für die erste und zweite Stimme Die Funktionen fd und fda bekommen als Eingabe je einen Wert vom Typ Dur (Dauer) und eine Note (Notenhöhe und Oktave) übergeben. Die Ausgabe ist die Note mit entsprechender Dauer und der zugewiesenen Noteneigenschaft Volume xx, die für die erste und zweite Stimme in diesem Beispiel unterschiedlich sein soll. Die Funktionen vol und vola erwarten ein Notenelement mit bereits vorhandener Dauer 5 und liefern damit die gleiche Ausgabe wie fd und fda zurück. Nun aber zur eigentlichen Beschreibung unserer Noten. Intuitiv schreiben wir die Noten Zeile für Zeile in eine Liste nieder. Wir verwenden dabei die in Code 3 vorgestellten Kurzschreibweisen und eine mittelhohe Oktavenhöhe. Ich habe mich für dieses Beispiel für die vierte Oktave als Ausgangshöhe entschieden. Je nachdem, ob viele Noten unterschiedlicher Länge hintereinander stehen, schreiben wir ihre Dauern gleich mit in die Listen oder lassen sie zunächst weg, wenn viele gleichlange Noten aufeinander folgen. Das Lautstärke-Attribut schreiben wir zunächst nicht hinzu. Nachdem nun die Listen erstellt sind, je eine bis zwei pro tatsächlicher Notenzeile, entsprechend der Ähnlichkeiten der Notenlängen, weisen wir mit Hilfe der mapFunktion den einzelnen Listenelementen, also unseren Noten, die fehlenden Attribute zu. Die Funktionen, die wir hierfür benötigen, haben wir wie im Code 4 beschrieben, zuvor erstellt. Ist all dies vollbracht, liegt uns ein Code wie folgt vor (Code 5): 01 02 03 04 05 06 07 08 09 fVFL1 = map vol [g 4 en,g 4 en,c 5 qn,c 5 qn, e 5 en,c 5 en,g 4 hn] fVFL2 = map (fd qn) [g 4,f 4,d 4,g 4,e 4,c 4] fVSL1 = map vol [c 5 qn,b 4 qn,d 5 dqn,b 4 en,c 5 qn, e 5 qn,c 5 qn,b 4 qn,d 5 dqn,b 4 en] fVTL1 = map (fd qn) [c 5,e 5,c 5,a 4,a 4] fVTL2 = map vol [c 5 en,a 4 en,g 4 hn] fVFoL1 = map (fd en) [c 5,e 5,e 5,d 5] fVFoL2 = map vol [c 5 qn,b 4 qn,c 5 hn] 10 11 12 13 14 15 16 17 18 sVFL1 = map vola [g 4 en,f 4 en,e 4 qn, e 4 qn,g 4 en,a 4 en,e 4 hn] sVFL2 = map (fda qn) [e 4,d 4,d 4,b 3,c 4,c 4] sVSL1 = map (fda qn) [e 4,g 4,g 4,f 4,e 4,c 4, e 4,g 4,g 4,f 4] sVTL1 = map (fda qn) [e 4,c 4,e 4,f 4,e 4,d 4] sVTL2 = [e 4 hn volSV] sVFoL1 = map vola [e 4 en,g 4 en,g 4 en, f 4 en,e 4 qn,d 4 qn,e 4 hn] 19 firstVoice = line (fVFL1 ++ fVFL2 ++ fVFL1 ++ fVFL2 20 ++ fVSL1 ++ fVTL1 ++ fVTL2 ++ fVFoL1 ++ fVFoL2) 21 secondVoice = line (sVFL1 ++ sVFL2 ++ sVFL1 ++ sVFL2 22 ++ sVSL1 ++ sVTL1 ++ sVTL2 ++ sVFoL1) Code 5: Haskore-Interpretation der ersten und zweiten Stimme Wie in den Zeilen 19 bis 22 setzen wir nun noch die Listen, deren Elemente nun alle die gleiche Anzahl und Art von Eigenschaften besitzen, zu je einem einzigen Stück vom Haskore-Typ Music. der ersten und zweiten Stimme zusammen. Mit diesen werden wir später dann leichter weiterarbeiten können. Wir verwenden zur Erzeugung die einfache Listenkonkatenation ++ von Haskell und die vorgenannte line-Funktion 6 2.2: Interpretation der Begleitakkorde Ähnlich wie in 2.1 wollen wir nun auch die Begleitakkorde unseres Stückes in Haskore beschreiben. Natürlich soll das Ganze möglichst einfach geschehen, komfortabel für den Musikprogrammierer. Um dies zu gewährleisten, definieren wir uns Funktionen, die bei Funktionsaufruf unter Eingabe des Grundtones einen entsprechenden Akkord automatisch erzeugen, so dass wir uns nicht mehr um die einzelnen Töne der jeweiligen Akkorde kümmern müssen. Die Eingabe der Begleitung entspricht dann wiederum intuitiv der tatsächlichen Notendarstellung, wenn wir die Benennung geschickt wählen. Zudem definieren wir wie für die Grundstimmen des Stückes auch für die Begleitung entsprechende Hilfsfunktionen, die Lautstärke- und Längen-Informationen an Noten zuweisen. Um unsere Akkorde richtig zu definieren, müssen wir nur wissen, dass ein Dur-Akkord (engl. major chord) aus folgenden drei Teilen besteht: Grundton, Grundton + große Terz und Grundton + große Terz + kleine Terz. Eine große Terz ist eine Erhöhung um vier, eine kleine Terz um drei Halbtonschritte im eingangs beschriebenen System der zwölf Halbtöne. So ist beispielsweise C-Dur = [c,e,g]. Entsprechend bestehen MollAkkorde (engl. minor chord) aus der Kombination Grundton, Grundton + kleine Terz, Grundton + kleine Terz + große Terz. C-Moll wäre also [c,es/ff,g]5. Die dritte Akkordart, die in unserem Stück „Die Gedanken sind frei“ auftaucht, ist Dur-Septim bzw. Dur-Sept (engl. major-seventh). Die Definition der Dur-Sept-Akkorde entspricht der eines Dur-Akkords, wobei eine vierte Note hinzukommt, die eine weitere kleine Terz oberhalb der dritten Note des Akkordes liegt. Um zu verhindern, dass die Noten, die oberhalb des übergebenen Grundtons liegen, durch die Berechnung der moduloRestklasse aus pitchClass (siehe Kapitel 1.2), nach unten „rutscht“, etwa bei einem Akkord zum Grundton G, bestimmen wir zusätzlich mit der Funktion absPitch aus den Informationen über Note und Oktave die absolute Höhe der Note und berechnen die Folgenoten aus diesem Wert. Die Summe wandeln wir mit der Funktion pitch in ein entsprechendes Tupel aus Notenhöhe und Oktave zurück und haben so die exakten Akkordnoten bestimmt. So erhalten wir für unsere Akkorde die in Code 6 gezeigten Zeilen 05 bis 17. Die Zeilen davor enthalten die oben genannte Definition zur Lautstärkezuweisung und eine Hilfsfunktion cmap, die zunächst eine map-Anweisung ausführt und die übergebene Liste dann mittels chord zu einem Music-Objekt faltet. 5 Englische Notation gemäß der in Code 3 genannten Konventionen, entspricht deutsch [C, Eis, G] 7 01 02 03 fdb d n = n d volAcc volb n = n volAcc volAcc = [Volume 50] 04 cmap f c = chord (map f c) 05 majChord n oct dur = 06 cmap (fdb dur)[Note (n,oct), 07 Note (pitch(absPitch(n,oct)+4)), 08 Note (pitch(absPitch(n,oct)+7))] 09 minChord n oct dur = 10 cmap (fdb dur)[Note (n,oct), 11 Note (pitch(absPitch(n,oct)+3)), 12 Note (pitch(absPitch(n,oct)+7))] 13 majChord7 n oct dur = 14 cmap (fdb dur)[Note (n,oct), 15 Note (pitch(absPitch(n,oct)+4)), 16 Note (pitch(absPitch(n,oct)+7)), 17 Note (pitch(absPitch(n,oct)+10))] Code 6: Hilfsfunktionen für die Begleitakkorde Mit diesen Werkzeugen ausgestattet, ist es ein Leichtes, die Begleitung des Stückes niederzuschreiben. Wieder setzen wir dies zeilenweise um, um die Übersicht zu erhalten und konkatenieren die einzelnen Teile am Ende wieder zu einer einzigen Liste. Es ist zu beachten, dass die Funktionen, die unsere Akkorde erzeugen, auch eine Dauerangabe erwarten, so übergeben wir jedem Aufruf auch die entsprechende Dauer des Begleitgriffs, wie sie der Notenvorlage (Abbildung 1) zu entnehmen ist Als Oktave der Grundtöne legen wir die zweite fest, da sie ein gutes Stück tiefer als die Hauptstimmen des Stückes liegen sollte. Schließlich erhalten wir die im folgenden Code 7 dargestellte Notation und haben damit auch unsere Begleitung in Haskore übertragen: 01 accFL1 = [majChord7 G 2 qn,majChord C 2 (6 * qn), 02 minChord D 2 (2 * qn),majChord G 2 qn, 03 majChord C 2 (2 * qn)] 04 accFL2 = [majChord7 G 2 qn,majChord C 2 (6 * qn), 05 minChord D 2 (2 * qn),majChord G 2 qn, 06 majChord C 2 (3 * qn)] 07 accSL1 = [majChord7 G 2 (3 * qn), 08 majChord C 2 (3 * qn),majChord7 G 2 (3 * qn)] 09 accTL1 = [majChord C 2 (3 * qn), 10 majChord F 2 (3 * qn),majChord C 2 (3 * qn)] 11 accFoL1 = [minChord D 2 (2 * qn), 12 majChord7 G 2 qn,majChord C 2 (2 * qn)] 13 accomp = 14 line (accFL1 ++ accFL2 ++ accSL1 ++ accTL1 ++ accFoL1) Code 7: Interpretation der Begleitung Die Dauer-Angaben der einzelnen Akkorde sind je in Vielfachen von Viertelnoten angegeben, da das Stück im ¾-Takt vorliegt und auch die meisten Noten des Liedes entsprechend Viertelnoten sind. Man beachte hierbei die einfache Verwendung des Multiplikations-Operators *. Auf die mathematischen Möglichkeiten innerhalb des Haskore Systems werde ich in Kapitel 5 noch einmal näher eingehen. 8 Kapitel 3: Vom Haskore-Code zur MIDI-Datei In Kapitel 2 haben wir uns drei separate Music-Objekte mit der Funktion line erzeugt. Somit haben wir eine für Haskore bzw. Haskell verständliche textuelle Interpretation unserer Notenvorlage geschaffen. Ein Komponist aber möchte sicher nicht nur einen Text besitzen, der alle seine Noteninformationen enthält; er will auch wissen, wie sich sein Stück schließlich anhört, und so bedient Prof. Hudak in Haskore auch diesen Wunsch. Im Modul HaskToMidi sind eine Reihe Funktionen definiert, die zum Erzeugen und Auslesen von Dateien nach dem General MIDI Standard6,7 benutzt werden können. Ich werde hier nur auf ein paar der Funktionen zum Erzeugen eingehen. Wir haben bereits bei der ersten Notation unserer Musik Lautstärke-Informationen mit übergeben, als Notenattribut, das für uns bisher nur rein beschreibende Bedeutung hatte. Für die Erzeugung einer MIDI-Datei ist diese Information aber bereits wichtig. Im MIDI-Format besitzt jede einzelne Note Informationen zur Höhe, Dauer, Lautstärke, zum Instrument, welches für die Note benutzt werden soll und zum Ausgabekanal des abspielenden Gerätes. So fehlen unserer Musik also vornehmlich noch die Daten zu Instrumenten und Kanälen. Zu diesem Zwecke definieren wir uns eine UserPatchMap. Diese besteht aus einer Liste mit Tripeln, bestehend aus einem Bezeichner-String, einem Namen eines Instrumentes gemäß GM-Standard8 und einer Kanal-Nummer, welche mit dem eingestellten Instrument angesteuert werden soll. Diese UserPatchMap benötigen wir später zum Erzeugen der fertigen Datei. Um die einzelnen Stimmen klanglich voneinander abzugrenzen, wählen wir je unterschiedliche Instrumente. Ich habe mich in diesem Beispiel für eine Kombination aus Piano für die erste, Harmonium (engl. reed organ) für die zweite Stimme und Gitarre für die Begleitakkorde entschieden (Code 8). Vorsicht ist geboten bei der Verwendung von PerkussionsKlängen. Diese dürfen und können nur an den Kanal 9 gesendet werden. Die verschiedenen Noten repräsentieren dann unterschiedliche Instrumente. 6 Haskore verwendet die Konventionen des General MIDI Standard (GM) von 1991. Da Haskore 1995/96 entstand, ist GM 2, der 1999 als Überarbeitung von GM entstand, hier nicht berücksichtigt. 7 General MIDI ist ein Standard, um einheitliche MIDI-Dateien auf unterschiedlichen Systemen produzieren und spielen zu können. GM definiert eine Reihe von Instrumentennamen mit jeweils zugeordneten Instrumentennummern. Halten sich die Soundgerätehersteller an diese Vorgaben, ist zumindest die Instrumentenwahl bei der Wiedergabe der Stücke vereinheitlicht, nicht zwingend aber der Klang der Einzelinstrumente. Mehr Information zu GM: http://de.wikipedia.org/wiki/General_Midi und http://www.borg.com/~jglatt/tutr/gm.htm 8 Eine entsprechende Liste der GM-Instrumente mit Instrumentennummern ist im Modul GeneralMidi unter GenMidiMap zu finden. 9 01 defMyUpm :: UserPatchMap 02 defMyUpm = [("piano","Acoustic Grand Piano",1), 03 ("reed","Reed Organ",3), 04 ("guitar","Acoustic Bass",6), 05 ("drums","Acoustic Grand Piano",9)] -– Perkussion! Code 8: Unsere selbsdefinierte PatchMap Außer der Angabe von Instrumenten ist es auch möglich, den Musikstücken zusätzliche Informationen zur Artikulation, Dynamik und Performance zuzuweisen. Diese werden jedoch nicht von allen Playern unterstützt, die das MIDI-Format abspielen, darum werde ich auch darauf hier nicht näher eingehen. Weiterführende Informationen über diese Möglichkeiten finden sich im Haskore Music Tutorial in den Kapiteln 3.3 bis 5. Zusätzlich finden sich in den Kapiteln 6 bis 9 des Tutorials eingehende Beschreibungen zu verschiedensten Funktionen zum Erzeugen und Auslesen von MIDI-Dateien mit einigen Details über deren internen Aufbau und technischen Hintergrund. Wir wollen uns aber für unser Beispiel auf eine einfache Möglichkeit beschränken, aus unseren Musikdaten eine tatsächlich abspielbare Datei zu produzieren. Dies soll als Einstieg und Arbeitsgrundlage anwendbar sein, und dem Komponisten/Programmierer sei hernach jede Kreativität freigestellt. Um nun also unsere Music zur Musik zu machen, benötigen wir noch eine generelle Information über die Geschwindigkeit, in der sie schließlich gespielt werden soll. Diese weisen wir mit der Funktion Tempo zu. Als Eingabe benötigt die Funktion einen beliebigen Zahlenwert als erstes sowie ein Music-Objekt als zweites Argument. Zurückgegeben wird ein Music-Objekt mit der zusätzlichen Eigenschaft der übergebenen Geschwindigkeit. Für unser Beispiel wählen wir eine mittlere Geschwindigkeit, die den Wert 2 besitzt. In Code 9 versehen wir unsere Stimmen mit Geschwindigkeits- und Instrument-Attributen. Anschließend setzen wir das Lied zu einem Ganzen zusammen und verwenden eine einfache rekursive WiederholungsHilfsfunktion times dazu, das Stück auf drei Strophen auszudehnen. 01 02 times 1 m = m times (n+1) m = m :+: (times n m) 03 fVMidi = Instr "piano" (Tempo 2 firstVoice) 04 sVMidi = Instr "reed" (Tempo 2 secondVoice) 05 accompanyMidi = Instr "guitar" (Tempo 2 (accomp)) 06 dieGedankenSindFrei = 07 times 3 (fVMidi :=: sVMidi :=: accompanyMidi) Code 9: Zuweisen der letzten Eigenschaften Der endgültige Weg zur MIDI-Datei erfolgt über ein Zwischenformat, das in Haskore vom Typ MidiFile ist. In Haskore ist die Umwandlung von Music zu MidiFile 10 mittels der Funktion performToMidi möglich. Diese erwartet als Argumente ein Objekt vom Typ Performance (dies ist Music mit übergebenen bestimmten Performance-Eigenschaften) und der zuvor angesprochenen UserPatchMap. Der Einfachheit halber wählen wir als Performance eine in Haskore im Modul TestHaskore integrierte und versehen unsere Musik mit dieser. Die PatchMap haben wir bereits erzeugt, und da wir immer die gleiche UserPatchMap verwenden, definieren wir uns eine Hilfsfunktion, die uns ein wenig Tipparbeit sparen wird wie in Code 10, Zeilen 01 – 02. Im Anschluss daran verwenden wir in einer weiteren Hilfsfunktion die Haskore-Funktion outputMidiFile, die aus unseren Musikbeschreibungen endgültig einen IO-Stream erzeugt und die fertige MIDI-Datei schreibt, die in jedem MIDI-fähigen Player abgespielt werden kann. Unsere Hilfsfunktion erwartet im ersten Argument ein Music-Objekt, im zweiten den Namen der zu schreibenden Datei. Schlussendlich schreiben wir noch ein paar kurze Funktionsdefinitionen nieder, die uns aus den einzelnen Stimmen bzw. der Begleitung je eine eigene MIDI-Datei erzeugen und eine solche, die das gesamte Stück erzeugt. So haben wir genügend Möglichkeiten, andere Instrumente, Geschwindigkeiten usw. auszuprobieren. 01 02 goMidi :: Music -> MidiFile goMidi m = performToMidi (testPerf m) defMyUpm 03 04 05 makeMidiFile :: Music -> String -> IO () makeMidiFile musicTrack fileName = outputMidiFile fileName (goMidi musicTrack) 06 makeFV, makeSV, makeAcc, makeItSo :: IO () 07 makeFV = makeMidiFile fVMidi "dieGedankenSindFrei-FV.mid" 08 makeSV = makeMidiFile sVMidi "dieGedankenSindFrei-SV.mid" 09 makeAcc = 10 makeMidiFile accompanyMidi "dieGedankenSindFrei-Acc.mid" 11 makeItSo = 12 makeMidiFile dieGedankenSindFrei "dieGedankenSindFrei.mid" Code 10: Die Ausgabefunktionen Nach Laden unseres Moduls genügt der Aufruf makeItSo, um das Stück Die Gedanken sind frei im Verzeichnis des Moduls zu erzeugen. Kapitel 4: Musik und die Macht der Mathematik Die Wahl der Programmiersprache Haskell als Grundlage für die Musikprogrammierung mag dem einen oder anderen zunächst einmal sicherlich seltsam vorkommen. Nicht zuletzt gibt es bereits 11 viele Schnittstellen für andere Programmiersprachen, die das Erstellen der schlanken Musikdateien recht komfortabel ermöglichen. Doch einen wichtigen Punkt können viele gängig Programmiersprachen niemals so effizient umsetzen, wie es Haskell kann: Musik ist auch Mathematik! 4.1 Musik nach Zahlen Haskore bedient sich an vielen Stellen dieser Erkenntnis. Im ersten Kapitel habe ich die Definitionen der Notenwerte, Oktavenhöhen und Notendauern vorgestellt. Sicher ist dem Leser dabei nicht entgangen, dass die gesamte Definition letztlich aus Zahlenwerten, einzeln oder in Tupeln, besteht. In Kapitel 2.2, Code 7 haben wir uns diese Tatsache bereits zunutze gemacht. So ist es jederzeit möglich, durch Anwendung von Grundrechenoperationen die Eigenschaften der einzelnen Noten oder mittels map einer ganzen Liste von Noten oder durch entsprechende Funktionen die eines umfangreichen Music-Objekts zu verändern. Beispielsweise ist Transponieren, eine Erhöhung oder Verminderung der Gesamthöhe eines Musikstückes, ist durch Addieren bzw. Subtrahieren eines Int-wertes auf den jeweiligen Absoluthöhenwert der Note (absPitch) umzusetzen. Eine solche Funktion ist in Haskore vordefiniert und lässt sich direkt auf Music-Objekte anwenden. Wollen wir beispielsweise die erste Stimme unseres Liedes um zwölf Halbtöne, also eine gesamte Oktave erhöhen, ändern wie die Zeile fVMidi = Instr "piano" (Tempo 2 firstVoice) einfach um in fVMidi = Instr "piano" (Tempo 2 (Trans 12 firstVoice)) und erhalten das gewünschte Ergebnis bei Erzeugung der Ausgabedatei. Haskore implementiert einige solcher Funktionen, die die mathematischen Eigenschaften der Musik intuitiv ausnutzen. So ist im Grunde auch die Aneinanderreihung mittels des Operators :+: als Addition zu betrachten, denn das Ergebnis ist eine Sequenz, eine Summe von Noten, die das ganze Stück ergeben. 4.2 Musik – rekursiv bis ins Unendliche Auch die Rekursion spielt in den vorgenannten Definitionen eine große und immer wiederkehrende Rolle. Der Datentyp Music ist selbst rekursiv definiert, ähnlich dem Aufbau einer Liste. Einige Funktionsdefinitionen bauen auf der Ausnutzung von Rekursionseigenschaften auf. Alle diese Funktionen, wie auch das oben genannte Trans greifen auf den rekursiven Aufbau des übergebenen Music-Objekts (Noten, 12 Pausen, Sequenzen oder Akkorde von Noten und Pausen) zurück und wenden so entsprechend ihrer Definition Berechnungen oder Zuweisungen auf die Grundelemente an, ohne die tatsächliche Struktur zu beeinträchtigen; etwas, das gerade Haskell sehr schnell und effizient umsetzt. Denkbar einfach ist es auch, eine unendliche Struktur zu erzeugen. Haben wir etwa einen einfachen Begleitrhythmus im Sinn, der einem Stück schlicht etwas Takt geben soll, müssen wir nur wissen, welchen Takt das Stück besitzt und diesen einmal vorgeben. Eine simple Funktion, ähnlich der in Code 9 vorgestellten times, ermöglicht es uns, den Takt unendlich oft zu wiederholen. In Haskore existiert diese Möglichkeit unter dem Namen repeatM. Mit Hilfe der Funktion cut kann ein derart erzeugtes theoretisch unendliches Stück leicht auf eine bestimmte Länge geschnitten werden, etwa die Länge des zu begleitenden Liedes. Diese kann mittels der Funktion dur wiederum rekursiv ermittelt werden. An unserem Beispiel könnte dies wie in Code 11 gezeigt aussehen. Man beachte, dass hier die unterschiedlichen Noten nicht unterschiedliche Höhen, sondern vielmehr verschiedene Instrumente definieren, wenn wir den Takt auf den MIDI-Kanal 9, also den Kanal für Perkussionsklänge mappen. Unser Beispiellied hat einen ¾-Takt, wobei der erste volle Takt erst mit der zweiten Viertelnote beginnt. 01 02 repeatM :: Music -> Music repeatM m = m :+: repeatM m 03 04 05 06 07 beatLine = [e 3 qn [Volume 60], d 3 qn [Volume 90],e 3 qn [Volume 60]] beat = line beatLine beatMidi = Instr "drums" (cut (dur fVMidi) (repeatM (Tempo 2 beat))) 08 09 dieGedankenSindFreiMitBeat = times 3 (fVMidi :=: sVMidi :=: accompanyMidi :=: beatMidi) 10 makeItSo2 = makeMidiFile 11 dieGedankenSindFreiMitBeat "dieGedankenSindFrei2.mid" Code 11: Takt-Erstellung und Einbringung ins gesamte Lied Somit haben wir aus einfachsten Mitteln unserem Lied eine durchgehende rhythmische Begleitung zugewiesen und implizit mehrfach Haskells Effektivität bei der Verwendung von Rekursionen ausgenutzt. Auch ist es möglich, die Notenlisten mittels Haskells reverse einfach umzukehren, um das interpretierte Stück rückwärts zu erzeugen, und Haskore geht noch einen Schritt weiter und ermöglicht das Umkehren von MusicObjekten. Die Funktion retro nimmt ein mit line erstelltes Music-Objekt, wandelt es in eine Liste um, invertiert diese und wandelt mit line wieder zu Music um. 13 Haskore ist voll von Funktionen, die mathematische Möglichkeiten um die Musikstücke herum definieren und nutzen. Die Musik wird nicht nur notiert, interpretiert und beschrieben; sie wird analysiert, eingeteilt, definiert. Mathematik und Musik werden miteinander verbunden, wo immer es möglich ist, und durch den rekursiven Aufbau über Listen und den Music-Datentyp erhält der Haskore-Programmierer weitreichende Möglichkeiten, Systematiken innerhalb eines Musikstückes funktional wiederzugeben. In wenigen Zeilen entsteht so ein Stück, für das in anderen Sprachen weit mehr Aufwand vonnöten wäre. Zur Ausnutzung von Rekursion und Rechenregeln gibt es im Haskore Music Tutorial im Anhang D auch einige Beispiele zur Erzeugung von Fraktal-Musik mit Haskore. Diese mag der interessierte Programmierer sich ebenfalls einmal näher betrachten, da hier bereits ein sehr einfacher Aufbau zu interessanten und komplex strukturierten Ergebnissen führt. Schlusswort Paul Hudak zeigt mit dem Haskore Music System ein eindrucksvolles Beispiel der Möglichkeiten von Haskell. Er gibt dem Musikprogrammierer ein mächtiges und vielseitiges Instrument an Anwendungsmöglichkeiten die Hand, vordefiniert das bereits mitliefert. eine Hudak breite Palette an selbst betont an verschiedenen Stellen, dass er mit Haskore nicht nur ein Instrument zur Notation von Musik und zur Umwandlung in bestimmte Dateistandards produzieren wollte. Vielmehr habe er einiges spezifisch so konzipiert, dass es gerade auch den analytischen Ansprüchen eines mathematisch denkenden Geistes genügen mag. Mit meiner Arbeit habe ich versucht, einen kleinen Teil dieser funktionalen Vielfalt des Haskore Music Systems aufzuzeigen und vorzustellen. Natürlich ist es schier unmöglich, alle Möglichkeiten der Module des Systems vorzustellen, jedoch sollten die wichtigsten Funktionen, Datentypen und weiteren Verwendungszwecke ausreichend behandelt sein, so dass ein Einstieg und tieferes Arbeiten mit Haskore ermöglicht ist. Bei meiner Arbeit mit Haskore fiel mir auf, dass an manchen Stellen noch weitere Funktionen hätten vordefiniert werden können, um das Beschreiben und Erzeugen von Musik noch etwas komfortabler zu gestalten. Beispielsweise sieht Hudak keine einfachen vordefinierten Funktionen zur automatisierten Akkord-Erzeugung vor. Dies 14 habe ich (siehe Code 6, Kapitel 2.2) beispielsweise selbst erledigt, um auch hieran die teils verborgene Mächtigkeit des Systems abermals zu zeigen und natürlich auch zu nutzen. Insgesamt wäre vielleicht eine stellenweise Überarbeitung des Systems wünschenswert, um Haskore-Neulingen den Einstieg in das Music System zu erleichtern. Ansonsten aber lässt sich abschließend nur sagen, dass diese Bibliothek von Prof. Hudak, ist man einmal eingearbeitet, eindrucksvolle Möglichkeiten der Musikinterpretation bietet. Mit dem Export in das MIDI-Format erhält man zudem ein gutes und praktikables Instrument, um den so entwickelten Code zu testen. Hudak spezifiziert außer einer Schnittstelle nach MIDI auch eine solche nach CSound, einem Soundsystem, das in der Sprache C entwickelt wurde. CSound ist portabler als MIDI-Musik, da die hier erzeugten Töne direkt softwareseitig berechnet werden und somit auf unterschiedlichen Systemen gleich klingen. Bei der Programmierung von Haskore-Musik für CSound müssen jedoch einige zusätzliche Voraussetzungen geschaffen und beachtet werden, so dass ich mich aus Gründen der Einfachheit in meiner Arbeit auf die Erzeugung von MIDI konzentriert habe. Abschließend bleibt zu sagen, dass Haskore für all jene, die bereits Grundkenntnisse in der Sprache Haskell besitzen und an der Erzeugung von computergenerierter Musik interessiert sind, beispielsweise als Hintergrunduntermalung von Spiele-Software, ein vielseitiges und empfehlenswertes Werkzeug ist. Gerade durch die Ausnutzung der funktionalen Struktur von Haskell ist die Umsetzung so mancher Systematik betrachteter Kompositionen im Vergleich zu anderen Musik-Erstellungs-Programmen bzw. -Sprachen um ein vielfaches vereinfacht. 15 Anhang: Der vollständige Beispiel-Sourcecode module DieGedankenSindFrei where import Haskore import HaskToMidi -- Benoetigte Hilfsfunktionen -- Mapfunktionen fuer die erste Stimme fd d n = n d volFV vol n = n volFV -- Mapfunktionen fuer die zweite Stimme fda d n = n d volSV vola n = n volSV -- Mapfunktionen fuer die Begleitung fdb d n = n d volAcc volb n = n volAcc cmap f c = chord (map f c) -- Lautstaerkedefinitionen erste, zweite und Begleitstimme volFV = [Volume 120] volSV = [Volume 75] volAcc = [Volume 50] -- Wiederholungsfunktion times 1 m = m times (n+1) m = m :+: (times n m) -- Aufbau von Akkorden: Dur, Dur-Sept, moll majChord n oct dur = cmap (fdb dur)[Note (n,oct), Note (pitch(absPitch(n,oct)+4)),Note (pitch(absPitch(n,oct)+7))] majChord7 n oct dur = cmap (fdb dur)[Note (n,oct), Note (pitch(absPitch(n,oct)+4)),Note (pitch(absPitch(n,oct)+7)), Note(pitch(absPitch(n,oct)+10))] minChord n oct dur = cmap (fdb dur)[Note (n,oct), Note (pitch(absPitch(n,oct)+3)),Note (pitch(absPitch(n,oct)+7))] -- Erste Stimme fVFL1 = map vol [g 4 en,g 4 en,c 5 qn,c 5 qn,e 5 en,c 5 en,g 4 hn] fVFL2 = map (fd qn) [g 4,f 4,d 4,g 4,e 4,c 4] fVSL1 = map vol [c 5 qn,b 4 qn,d 5 dqn,b 4 en,c 5 qn, e 5 qn,c 5 qn,b 4 qn,d 5 dqn,b 4 en] fVTL1 = map (fd qn) [c 5,e 5,c 5,a 4,a 4] fVTL2 = map vol [c 5 en,a 4 en,g 4 hn] fVFoL1 = map (fd en) [c 5,e 5,e 5,d 5] fVFoL2 = map vol [c 5 qn,b 4 qn,c 5 hn] -- Zweite Stimme sVFL1 = map vola [g 4 en,f 4 en,e 4 qn,e 4 qn,g 4 en,a 4 en,e 4 hn] sVFL2 = map (fda qn) [e 4,d 4,d 4,b 3,c 4,c 4] sVSL1 = map (fda qn) [e 4,g 4,g 4,f 4,e 4,c 4,e 4,g 4,g 4,f 4] sVTL1 = map (fda qn) [e 4,c 4,e 4,f 4,e 4,d 4] sVTL2 = [e 4 hn volSV] sVFoL1 = map vola [e 4 en,g 4 en,g 4 en,f 4 en,e 4 qn,d 4 qn,e 4 hn] 16 -- Begleitung accFL1 = [majChord7 G 2 qn,majChord C 2 (6 * qn), minChord D 2 (2 * qn),majChord G 2 qn,majChord C 2 (2 * qn)] accFL2 = [majChord7 G 2 qn,majChord C 2 (6 * qn), minChord D 2 (2 * qn),majChord G 2 qn,majChord C 2 (3 * qn)] accSL1 = [majChord7 G 2 (3 * qn),majChord C 2 (3 * qn), majChord7 G 2 (3 * qn)] accTL1 = [majChord C 2 (3 * qn),majChord F 2 (3 * qn), majChord C 2 (3 * qn)] accFoL1 = [minChord D 2 (2 * qn),majChord7 G 2 qn, majChord C 2 (2 * qn)] -- Takt beatLine = [e 3 qn [Volume 60],d 3 qn [Volume 90],e 3 qn [Volume 60]] -- Zusammensetzen jeweils einer Stimme firstVoice = line (fVFL1 ++ fVFL2 ++ fVFL1 ++ fVFL2 ++ fVSL1 ++ fVTL1 ++ fVTL2 ++ fVFoL1 ++ fVFoL2) secondVoice = line (sVFL1 ++ sVFL2 ++ sVFL1 ++ sVFL2 ++ sVSL1 ++ sVTL1 ++ sVTL2 ++ sVFoL1) accomp = line (accFL1 ++ accFL2 ++ accSL1 ++ accTL1 ++ accFoL1) beat = line beatLine -- MIDI-Parameter defMyUpm :: UserPatchMap defMyUpm = [("piano","Acoustic Grand Piano",1), ("reed","Reed Organ",3), ("guitar","Acoustic Bass",6), ("drums","Acoustic Grand Piano",9)] -- Benennung ist zweitrangig, Kanal 9 definiert Perkussion -- Zuweisen der MIDI-Eigenschaften Instrument und Geschwindigkeit fVMidi = Instr "piano" (Tempo 2 firstVoice) sVMidi = Instr "reed" (Tempo 2 secondVoice) accompanyMidi = Instr "guitar" (Tempo 2 (accomp)) beatMidi = Instr "drums" (cut (dur fVMidi) (repeatM (Tempo 2 beat))) dieGedankenSindFrei = times 3 (fVMidi :=: sVMidi :=: accompanyMidi) dieGedankenSindFreiMitBeat = times 3 (fVMidi :=: sVMidi :=: accompanyMidi :=: beatMidi) -- Umwandeln von Music-Objekten in ein Vorformat goMidi :: Music -> MidiFile goMidi m = performToMidi (testPerf m) defMyUpm -- Ausgabefunktionen zum Erzeugen der endgueltigen Midi-Dateien makeMidiFile :: Music -> String -> IO () makeMidiFile musicTrack fileName = outputMidiFile fileName (goMidi musicTrack) makeFV, makeSV, makeAcc, makeItSo, makeItSo2 :: IO () makeFV = makeMidiFile fVMidi "dieGedankenSindFrei-FV.mid" makeSV = makeMidiFile sVMidi "dieGedankenSindFrei-SV.mid" makeAcc = makeMidiFile accompanyMidi "dieGedankenSindFrei-Acc.mid" makeBeat = makeMidiFile beatMidi "dieGedankenSindFrei-Beat.mid" makeItSo = makeMidiFile dieGedankenSindFrei "dieGedankenSindFrei.mid" makeItSo2 = makeMidiFile dieGedankenSindFreiMitBeat "dieGedankenSindFrei2.mid" 17 Quellenverzeichnis Hauptquelle: • Paul Hudak – Haskore Music Tutorial http://www.haskell.org/haskore/Haskore/Haskore/Docs/tutorial.ps Nebenquellen: • General MIDI – Kurze Erläuterung http://de.wikipedia.org/wiki/General_Midi • General MIDI – Spezifikationen http://www.borg.com/~jglatt/tutr/gm.htm • Notenvorlage Die Gedanken sind frei Kein schöner Land – Das große Buch unserer beliebtesten Volkslieder, S. 62 Falken Verlag – ISBN 380684150 Der Source-Code im Anhang und in den Codebeispielen in den einzelnen Kapiteln wurde vollständig von mir selbst erzeugt. Ausnahmen bilden die Funktionen fd und times, die aus dem Haskore Music Tutorial übernommen wurden. 18