11.11.2013

Kleine Perl-Spielerei mit Tonleitern

Vor einiger Zeit habe ich mich mal ein bißchen näher mit Noten beschäftigt, und fand es ganz schön faszinierend, wie systematisch man eine Tonleiter selbst aufschreiben kann. Es geht nur darum, den richtigen Nachfolger für jeden Ton zu finden, und schon kann man ganz leicht, ausgehend von C-Dur, jede andere Tonleiter zusammenbauen.

Die Profi-Musiker werden jetzt natürlich schon angefangen haben zu lachen, das ist mir klar ;). Die können das alles auswendig oder merken sich lustige Eselsbrücken ("geh, du alter esel, fische holen"). Da mein Gedächtnis für so etwas nicht geeignet ist, mache ich es anders.

Man kann natürlich sowas mit Stift und Papier erledigen. Das geht aber einem Programmierer an die Ehre, und warum etwas manuell erledigen, was man auch mit einem kleinen Progrämmchen schaffen kann?

Also ein wenig nachgedacht über eine sinnvolle Datenstruktur, und los geht's mit dem perl-Skript, um eine Tonleiter zu konstruieren, wenn der Anfangston gegeben ist. Ich bin ein Anhänger des Konzepts, dass man sich zuerst eine möglichst praktische Datenstruktur ausdenkt, und davon ausgehend ergibt sich der Programmcode nahezu von selbst. Dieses Konzept hat Niklaus Wirth als erster formuliert und dann gleich ein Buch drüber geschrieben ;)

Zunächst mal brauche ich zwei Informationen: wie groß ist der Abstand zwischen jeweils zwei benachbarten Noten, und welche Noten folgen aufeinander.

Mein erster Versuch war, die ganzen Noten und die Halbtöne in zwei Arrays zu packen:

my @toene1=("c",   "d",   "e", "f",   "g",   "a", "h");
my @toene2=("cis", "dis", "f", "fis", "gis", "b", "c");

Als ich schon fast fertig war, hat mir meine Tochter ganz empört erklärt, dass man Kreuze und B nicht mischen darf. Ich müsste also "ais" statt "b" schreiben, wenn ich eine Dur-Tonleiter (also die mit den Kreuzen ...) bauen will. Naja ...

Dann müsste ich in einer Schleife ausgehend vom Grundton, die nächsten 7 Töne suchen. Also eine Fallunterscheidung, ob jetzt ein Sprung um einen ganzen oder halben Ton nötig ist, und dann überlegen, welches der entsprechend entfernte Ton genau ist. Dazu muss man wissen, dass zwischen "c" und "d" ein Abstand von einem Ganzton liegt, von "e" zu "f" aber nur ein Halbton. Bei einer Tonleiter gelten ausgehend vom Grundton, bestimmte Regeln, wie groß der Abstand zum nächsten Ton jeweils ist.

Auf der Klaviertastatur sieht man das ganz wunderbar: es gibt immer abwechselnd Gruppen mit zwei und drei schwarzen Tasten mit Halbtönen, und es gibt pro Oktave zwei Stellen, bei denen oberhalb keine schwarze Taste liegt - links von dem Zweierpärchen schwarzer Tasten liegt jeweils das "c", und rechts davon die zwei weißen Tasten ohne eine schwarze dazwischen sind "e" und "f".

Diese Abstandsregel habe ich als "delta" hingeschrieben, und da ich nicht mit Gleitkommazahlen rechnen wollte, habe ich "2" als Ganzton und "1" als Halbton geschrieben. Das hat mich später auf eine Idee gebracht, wie ich die Datenstruktur der aufeinanderfolgenden Töne verbessern könnte, aber das kommt erst als übernächstes.

my @delta=(2,2,1,2,2,2,1);

Dann brauche ich noch eine Hilfsfunktion, die mir den Index liefert, an dem mein gewünschter Grundton in der C-Dur-Tonleiter liegt.

sub findstart {
        my ($start)=@_;

        for ( my $i=0; $i<$max; ++$i) {
                return ($i) if ($start eq $toene[$i]);
        }
        return 99;
}


my $start=lc($ARGV[0]); 
$ofs=findstart($start);

Diese Funktion liefert mir die Nummer des Tons im Array, und für den schlimmsten Fall (kein Treffer) eine 99, also ein Ergebnis außerhalb des möglichen Bereichs.


Außerdem brauche ich noch einen separaten Zähler (dachte ich zuerst), mit dem ich die Liste der Abstände (der Deltas) von Anfang an durchlaufe.

Als nächstes also eine Schleife und eine Fallunterscheidung:

for (my $i=0; $i<7; ++$i) {
  if ($delta[$o]==2) { # sprung um einen ganzton
    $neuerton=$toene1[$i];
  }
  else { # sprung halbton
    $neuerton=$toene2[$i];
  }


Das bringt mir allerdings ein Problem, weil die primitive Datenstruktur mit zwei separaten Arrays "toene1" und "toene2" nur ganz schwer die numerische Information liefert, wann in der Tonleiter der Abstand nur einen Halbton ist (e/f und h/c).

Also flugs eine neue Datenstruktur ausgedacht, die das liefert. Ein Array von Arrays, und jedes Element enthält den Namen des Grundtons, den Ton einen Halbton darüber und den einen Ganzton darüber:

my @toene=(
  ["c", "cis", "d"  ],

  ["d", "dis", "e"  ],
  ["e", "f",   "fis"],
  ["f", "fis", "g"  ],
  ["g", "gis", "a"  ],
  ["a", "ais", "h"  ],
  ["h", "c",   "cis"]
);


Das Coolste an dieser Konstruktion ist, dass ich mir die Fallunterscheidung jetzt ganz sparen kann und sich der Sprung zum nächsten Ton in der Tonleiter ganz zwanglos aus dem "delta"-Array ergibt: der Index [0] ist der Grundton im inneren Array, der Index [1] ist der Halbton darüber, und der Index [2] der nächste Ton mit einem Ganzton Abstand. Der schönste Trick ist hier, dass ich $delta[$o], also den Abstand vom Ton an der Stelle $o zu seinem Nachfolger, als Index verwende, um den Namen dieses Tons zu finden.

for (my $i=0; $i<7; ++$i) {
  $neuerton=$toene[$i]->[$delta[$o]]; 
}


Im Endeffekt hat mir das aber auch nicht allzuviel gebracht, weil nach dem ersten Halbtonsprung (z.B. in D-Dur vom "e" zum "fis") der nächste Schritt in der Schleife mit dem "fis" anfangen müsste, meine Schleifenlogik aber vom "e" zum "f" an Index [0] springt. Der Output war dann "d", "e", "fis" (soweit ok), aber dann kam nochmal ein "fis", weil nach dem "e" in der ersten Spalte das "f" kommt, und das ist falsch.

So elegant diese Datenstruktur auch ist, so ungeeignet ist sie nach wie vor für das, was ich bezwecke. Es tat mir dann auch ein bißchen leid, dass ich sie aufgeben musste ;)

Also bin ich wieder zurückgekehrt zu einer flachen Datenstruktur, nämlich einem Array, in dem schlicht und einfach alle Töne und Halbtöne aufeinanderfolgen. Mir kam dann nämlich der Gedanke, dass ich mit dem "delta" 1 oder 2 ganz einfach in einem flachen Array entweder den Nachfolger mit Abstand 1 oder 2 nehmen kann, um den nächsten Ton zu finden.

Dieser Gedanke hat dann tatsächlich zum gewünschten Ergebnis geführt. Nebenbei hat es auch dazu geführt, dass ich die zweite Schleifenvariable entfernen konnte, und den Index des Grundtons (also mein Parameter von der Kommandozeile) mißbrauchen konnte, um zu zählen. Es ist nur wichtig zu bedenken, dass man nicht über das Ende des Arrays hinausgreifen darf. Deshalb verwende ich den "modulo"-Operator, um (wie bei einem Zifferblatt) dafür zu sorgen, dass nach der "11" bei der "0" weitergezählt wird.

Warum kommt es zu "12" als Größe des Arrays? Ganz einfach: es gibt in einer Tonleiter 7 verschiedene Töne (c/d/e/f/g/a/h), und eine "Oktave" höher findet sich wieder der Grundton. Zwischen "e" und "f" und nach dem "h" gibt es keinen Halbton (musikalisch schon, aber nicht mit einem eigenen Namen), deshalb also 2*7-2=12 verschiedene Töne mit Namen. Solche Überlegungen am Rand sind recht nützlich, um zu testen, ob bestimmte Grundannahmen im Programm plausibel sind, und ob man bei Aufzählungen auch alle Varianten wirklich in den Programmtext geschrieben hat oder noch etwas fehlt.


Und das kam als Endprodukt dabei heraus:

#!/usr/bin/perl -w

my @delta=(2,2,1,2,2,2,1);
my @toene=(
        "c",   "cis", "d",   "dis", "e",   "f",
        "fis", "g",   "gis", "a",   "ais", "h",
);
my $max=scalar(@toene);

sub findstart {
        my ($start)=@_;

        for (my $i=0; $i<$max; ++$i) {
                return ($i) if ($start eq $toene[$i]);
        }
        return 99;
}

my $start=lc($ARGV[0]);
my $ofs;

print "# $start-dur Tonleiter ($max)\n";
$ofs=findstart($start);

for (my $o=0; $o<7; ++$o) {
        print "# "," + ",$delta[$o]," = ",$toene[$ofs],"\n";
        $ofs=($ofs+$delta[$o]) % $max;
}


Hier ist der Probelauf für F-Dur noch mit der Debugging-Ausgabe, wie weit entfernt der nächste Ton liegt (also f+1=g, a+1/2=ais usw.):

server:~ # ~ths/perl/tonleiter.pl f
# f-dur Tonleiter
#  + 2 = f
#  + 2 = g
#  + 1 = a
#  + 2 = ais
#  + 2 = c
#  + 2 = d
#  + 1 = e


[Update 2013.11.12: Formatierung, Tippfehler]

Keine Kommentare:

Kommentar veröffentlichen