Heiße Rhythmen (Linux-Magazin, Juni 2011)

Der Musikplayer Banshee legt die Metadaten der von ihm verwalteten Songs in einer Datenbank ab, die sich durch Perlskripts auslesen, restaurieren, und mit automatisch ermittelten Beats-per-Minute-Werten auffrischen lässt.

Wie bei der Wahl zwischen vi oder Emacs schwören viele Nutzer von Musikplayern auf ihren Favoriten und wechseln selten. Denn schließlich lassen sich Einstellungen wie jahrelang mühsam von Hand erstellte Song-Ratings nicht so einfach übertragen. Zwar werde ich niemals einen anderen Editor als vi benutzen, doch neulich probierte ich den Musikplayer Banshee ([2] und Abbildung 1) aus, da die bislang in den Perlmeister-Studios verwendete Rhythmbox keinen einfachen Ratings-Export anbot.

Abbildung 1: Der Musik-Player Banshee.

Die GUI kam extrem sauber und durchdacht daher, und als ich dann entdeckte, dass man in Banshee vorgenommene Ratings ganz einfach sichern, exportieren oder extern manipulieren kann, weil der Player sie in einer leicht zugänglichen SQLite-Datenbank ablegt, war's um mich geschehen und Operation "Playerwechsel" lief an. Wie in Abbildung 2 ersichtlich, speichert Banshee Song-Metadaten in der Tabelle CoreTracks der Datenbankdatei ~/.config/banshee-1/banshee.db. Der Pfad zu referenzierten Audio-Dateien im Filesystem steht in der Spalte Uri als URI mit dem Vorsatz file://. Wie der SQLite-Befehl .schema zum Vorschein bringt, führt die Tabelle weitere interessante Spalten wie die Anzahl der vergebenen Ratings-Sterne (Rating) oder zu wievielen Basstrommelschlägen pro Minute Diskotänzer im Takt des Liedes rhythmisch zucken (BPM, Beats per Minute).

Abbildung 2: Dank offenem Datenbankdesign lässt sich Banshee in die Karten sehen.

Bewertungen gesichert

Listing 1 sichert alle bislang per Mausklick vorgenommenen Song-Ratings in einer YAML-Datei (Abbildung 3), um sie später nach einem Systemcrash oder dem Wechsel zu einem neuen Musikplayer sofort wieder restaurieren zu können.

Ohne Parameter aufgerufen, öffnet banshee-rating-backup die Datenbank mit dem DBI-Modul und iteriert in der Funktion backup ab Zeile 32 mit einem SELECT-Befehl über alle Einträge der Tabelle CoreTracks. Falls das für einen Song gefundene Rating gleich 0 ist, springt Zeile 44 weiter zum nächsten Eintrag, denn nicht vorgenommene Ratings braucht das Skript nicht zu sichern. Falls sich aber ein positiver Wert findet, speichert Zeile 46 ihn im Hash %ratings unter dem Pfad der Audio-Datei ab. Am Ende der Tabelle schreibt die Funktion DumpFile() aus dem YAML-Modul die Ratings in die YAML-Datei banshee-ratings.yml.

Abbildung 3: Ausschnitt aus der YAML-Datei, in der Listing 1 die bislang vorgenommenen Ratings in Banshee sichert.

Listing 1: banshee-rating-backup

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Log::Log4perl qw(:easy);
    04 Log::Log4perl->easy_init($DEBUG);
    05 
    06 use DBI qw(:sql_types);
    07 use DBD::SQLite;
    08 use Data::Dumper;
    09 use YAML qw(LoadFile DumpFile);
    10 use Getopt::Std;
    11 
    12 getopts( "r", \my %opts );
    13 
    14 my $db  = glob 
    15           "~/.config/banshee-1/banshee.db";
    16 my $dbh = DBI->connect( "dbi:SQLite:$db", 
    17     "", "", { RaiseError => 1, 
    18               AutoCommit => 1 });
    19 
    20 my $yml     = "banshee-ratings.yml";
    21 my %ratings = ();
    22 
    23 if( $opts{ r } ) {
    24     restore( $yml, $dbh );
    25 } else {
    26     backup( $dbh, $yml );
    27 }
    28 
    29 $dbh->disconnect();
    30 
    31 ###########################################
    32 sub backup {
    33 ###########################################
    34   my( $dbh, $yml ) = @_;
    35 
    36   my %ratings = ();
    37 
    38   my $sth = $dbh->prepare( 
    39       "SELECT * FROM CoreTracks" );
    40   $sth->execute();
    41 
    42   while( my $hash_ref = 
    43            $sth->fetchrow_hashref() ) {
    44       next if $hash_ref->{ Rating } == 0;
    45 
    46       $ratings{ $hash_ref->{ Uri } } = 
    47         $hash_ref->{ Rating };
    48   }
    49 
    50   DumpFile( $yml, \%ratings );
    51 
    52   $sth->finish();
    53 }
    54 
    55 ###########################################
    56 sub restore {
    57 ###########################################
    58   my( $yml, $dbh ) = @_;
    59 
    60   my $ratings = LoadFile( $yml );
    61 
    62   for my $song ( keys %$ratings ) {
    63       DEBUG "Restoring $song";
    64 
    65       my $rating = $ratings->{ $song };
    66 
    67       my $sth = $dbh->prepare(
    68        "UPDATE CoreTracks SET Rating = ?" .
    69        "WHERE Uri = ?" );
    70       $sth->execute( $rating, $song );
    71       $sth->finish();
    72   }
    73 }

Zum späteren Restaurieren der Ratings ruft der User das Skript mit banshee-rating-backup restore auf, worauf es die Datei banshee-ratings.yml mit der YAML-Funktion LoadFile() in Zeile 60 einliest und über den so initialisierten Hash iteriert. Dessen Schlüssel sind die Pfadnamen zu den bewerteten Audio-Dateien und seine Werte die Ratings, sodass Zeile 67 pro Eintrag einfach einen Update-Befehl in SQL absetzen muss, um die Datenbank wieder auf Vordermann zu bringen. Der typische Dreisprung mit dem DBI-Modul besteht aus dem Zusammenstellen des SQL-Queries mit prepare(), einem anschließenden execute() mit Parametern, das die mit Fragezeichen im Query freigehaltenen Variablen ersetzt, und einem abschließenden finish(), das das allozierte Statement-Handle $sth wieder freigibt.

Stampfer pro Minute

Um eine Playlist für bestimmte Anlässe oder Gemütszustände zu erstellen, reichen Ratings allein nicht, denn wer hört schon AC/DC während eines Kerzenlichtdinners? Banshee führt in der Metadatenbank ein Feld "BPM" ("Beats Per Minute"), das die Taktschläge eines Titels pro Minute anzeigt. Wummernde Diskobässe eines Party-Titels wie "Memories" von David Guetta kommen auf 120 BPM, ein schnelles Techno-Stück auf 180 und ein klassischer Titel wie Mozarts Zauberflöte kommt ohne jegliches Getrommel aus und führt den Wert 0. Radio-DJs schwören auf diesen Messwert und stellen zum Teil mit automatischen Tools ihr BPM-kompatibles Programm zusammen.

Mit einer Kombination aus Rating und erlaubtem BPM-Bereich kann der User später die passende musikalische Untermalung für gewisse Stunden auswählen. Allerdings führen Audiodateien normalerweise keine BPM-Werte in ihren Metadaten mit sich. Version 1.5 des Banshee-Players liefert ein auf dem GStreamer-Paket bpmdetect basierendes BPM-Erkennungstool mit. Ein Klick auf eine im Dateiscan-Dialog versteckte Checkbox aktiviert den BPM-Detektor (Abbildung 4). Der Menüpunkt Tools->Rescan Music Library startet den CPU-intensiven Update, der, abhängig von der Größe der verwalteten Musiksammlung, einige Zeit läuft.

Abbildung 4: Banshee ermittelt auf Wunsch die BPM-Werte aller gefundenen Songs und füllt sie in die Datenbank.

Die Ergebnisse lassen jedoch zu wünschen übrig. So meint das Tool zum Beispiel, dass das eher behäbige "I Feel Fine" von den Beatles mit 213 BPM-Punkten dreimal so schnell wie der nur mit dem Wert 68 bewertete superschnelle Pop-Punk-Titel "Rich Lips" von Blink-182 sei (Abbildung 5).

Abbildung 5: Banshees BPM-Messer meint ernsthaft, dass "I Feel Fine" von den Beatles drei mal so schnell wie "Rich Lips" von Blink-182 ist.

Listing 2 versucht deshalb mit einem BPM-Verfahren der Marke Eigenbau eine zuverlässigere Lösung auf die Beine zu stellen. Es wandelt die komprimierten Audiodateien mit der Utility sox aus dem gleichnamigen Ubuntu-Paket in rohe Audiodaten um, jagt diese durch einen schmalen Bandpass im Bassbereich und misst dann die Abstände zwischen den hoffentlich ausgeprägten Bassmaxima. Diese Methode funktioniert zwar auch nicht fehlerfrei, unterscheidet aber klar Discostampf von klassischer Musik.

Das als Paket für viele Distributionen erhältliche Audio-Tool audacity zeigt in den Abbildungen 6 und 7, wie die Audiodaten zweier unterschiedlicher Musikgenres aussehen. Während das klassische Orchesterstück mit Heldentenor nur mäßig ausgesteuert ist und keinerlei stetigen Rhythmus erkennen lässt, zeigt der breitbandige Synthosound durchaus periodische Tendenzen.

Abbildung 6: "Zu Hilfe, zu Hilfe, sonst bi-i-in ich verloren" trällert Prinz Tamino in Mozarts Zauberflöte.

Abbildung 7: Voller Synthosound: "Memories" von David Guetta

Abtastete Musik

Die Musik, die das Ohr als Bündel gleichzeitig gespielter und nahe beieinander liegender Tonfrequenzen hört, erzeugt die Soundkarte aus digitalen Abtastwerten. Eine Stereoaufnahme besteht pro Kanal typischerweise aus 44.100 verschiedenen 16-Bit-Messwerten pro Sekunde, die zwischen -32768 und +32767 varieren. Ein reiner Ton käme als Sinuskurve zum Vorschein, aber ein Musikinstrument oder eine menschliche Stimme erzeugen typischerweise ein weites Spektrum an Frequenzen.

Abbildung 8 zeigt als Beispiel die Audiodaten einer 1Hz-Sinusschwingung im Rohformat. Verfolgt man zum Beispiel den ersten (grünen) Kanal, sieht man, dass die Werte von "15 06", "18 06", "1c 06", usw. stetig ansteigen (zu beachten ist die umgekehrte Byte-Order auf Intel-Prozessoren, die zugehörigen dezimalen Werte sind 1557, 1560, 1564). Offensichtlich befindet sich der Sinus gerade im steigenden Bereich. Innerhalb einer Periode mit 44.100 Abtastwerten durchläuft das 1Hz-Signal einmal einen Bereich, der mit insgesamt 65536 Werten kodiert ist, also ist der Sprung von 0x0615 (dezimal 1557) zu 0x0618 (dezimal 1560) schon im flacheren Teil der Kurve in der Nähe des Maximums zu finden. Das Testsignal ist auf beiden Kanälen identisch, beide Kanäle (grün und blau) weisen die gleichen Werten auf.

Abbildung 8: Eine 1-Hz Sinuskurve mit einer Abtastrate von 44100 Hz im 2-Kanal-Rohformat.

Raffiniert kodiert

Eine mp3-Datei kodiert die Abtastwerte auf raffinierte Weise und das Skript muss sie vor der Analyse erst in das Rohformat von Abbildung 8 bringen. Hierzu eignet sich die Utilty sox, die mit dem Aufruf

    sox infile.mp3 -r 44100 -c 2 \
    -b 16 -t raw -e signed outfile.raw

aus infile.mp3 die Rohformatdatei outfile.raw als Zweikanal-Kodierung mit vorzeichenbehafteten 16-Bit-Werten bei einer Abtastrate von 44100Hz erzeugt. Um darin die Bassaktivität zu analysieren und die zu bearbeitenden Daten auf 30 Sekunden zu beschränken, hängt der BPM-Messer an das Kommando oben noch die Argumente

    ... bandpass 100 1 \
        trim 60 30

an. Der Bandpass blendet Frequenzen, die nicht im Bereich einer Basstrommel liegen mit einem 3db-Dämpfer pro Oktave aus und der trim-Filter fährt 60 Sekunden ins Musikstück hinein und extrahiert dort nur die Daten der nächsten 30 Sekunden, damit das Skript später nicht ewig herumorgeln muss.

Diskostampf und Heldentenor

Abbildung 9 zeigt die so gefilterten Audiodaten verschiedener Titel. Die Basstrommel von "I Feel Fine" und das Syntho-Gewummere von David Guettas "Memories" erzeugen saubere Maxima, die das Skript erkennt und durch simple Streckung auf Beats-per-Minute umrechnet. Bei klassischer Musik oder akustischen Gitarrensongs wie "I Got a Name" von Jim Croce hingegen dümpelt das Signal auf niedrigen und nahezu konstanten Werten dahin und der Algorithmus liefert den Wert 0. Das Signal von schnellen Punk-Songs wie "Rich Lips" von Blink-182 variiert stark, doch ungefähres Erfassen der Maxima führt üblicherweise ebenfalls zu brauchbaren BPM-Werten.

Abbildung 9: Die Basslinie von Songs verschiedener Genres nach Anwenden des schmalspurigen Bandfilters.

Nach dem Verbinden mit der Datenbankdatei ruft Listing 2 die Funktion bpm_update() ab Zeile 29 auf. Der Select-Query in Zeile 34 liefert die Pfade aller von Banshee verwalteten Musikdateien als URIs in der form file://pfad/datei zurück. Da in diesen URIs Leerzeichen als %20 codiert sind, wandelt die Funktion uri_escape() aus dem CPAN-Modul URI::Escape sie wieder in normale Leerzeichen zurück. Zeile 44 entfernt dann noch das führende file:// und schon steht in der Variablen $file der Unix-Pfad zur Audiodatei.

Listing 2: banshee-bpm-update

    001 #!/usr/local/bin/perl -w
    002 use strict;
    003 use Log::Log4perl qw(:easy);
    004 use DBI qw(:sql_types);
    005 use DBD::SQLite;
    006 use Sysadm::Install qw(tap);
    007 use URI::Escape;
    008 use File::Temp qw(tempfile);
    009 use POSIX;
    010 
    011 my $SAMPLE_RATE = 10_000;
    012 my $OFFSET      = 60;
    013 my $SAMPLE_SECS = 30;
    014 my $MIN_SIZE    = 500;
    015 my $MIN_DROP    = 0.7;
    016 my $NWINDOWS    = 20;
    017 
    018 Log::Log4perl->easy_init({ level => $INFO, 
    019                     category => "main" });
    020 my $db  = glob 
    021           "~/.config/banshee-1/banshee.db";
    022 my $dbh = DBI->connect( "dbi:SQLite:$db", 
    023     "", "", { RaiseError => 1, 
    024               AutoCommit => 1 });
    025 bpm_update( $dbh );
    026 $dbh->disconnect();
    027 
    028 ###########################################
    029 sub bpm_update {
    030 ###########################################
    031   my( $dbh ) = @_;
    032 
    033   my $sth = $dbh->prepare( 
    034       "SELECT Uri FROM CoreTracks " .
    035       "WHERE BPM = 0" );
    036   $sth->execute();
    037 
    038   my $upd_sth = $dbh->prepare(
    039     "UPDATE CoreTracks SET BPM=? " .
    040     "WHERE Uri = ?");
    041 
    042   while( (my $uri) = 
    043          $sth->fetchrow_array() ) {
    044     my $file = uri_unescape( $uri );
    045     $file =~ s#^file://##;
    046     INFO "Updating $uri";
    047     $upd_sth->execute( bpm( $file ),
    048                        $uri );
    049   }
    050   $upd_sth->finish();
    051   $sth->finish();
    052 }
    053 
    054 ###########################################
    055 sub bpm {
    056 ###########################################
    057   my( $file ) = @_;
    058 
    059   my $rawfile;
    060 
    061   if( $file =~ /\.raw$/ ) {
    062     $rawfile = $file;
    063   } else {
    064     $rawfile = File::Temp->new( 
    065        SUFFIX => ".raw", UNLINK => 1 );
    066       
    067     my($stdout, $stderr, $rc) = 
    068       tap "sox", $file, "-r", $SAMPLE_RATE, 
    069         "-c", 2, "-b", 16, "-t", "raw", 
    070         "-e", "signed", $rawfile, 
    071         "bandpass", 100, 1, 
    072         "trim", $OFFSET, $SAMPLE_SECS;
    073       
    074     if( $rc ) {
    075       LOGWARN "sox $file: $stderr";
    076       return -1;
    077     }
    078   }
    079 
    080   return raw_bpm( 
    081            samples( $rawfile->filename ) );
    082 }
    083 
    084 ###########################################
    085 sub samples {
    086 ###########################################
    087   my( $file ) = @_;
    088 
    089   my @vals = ();
    090   sysopen FILE, "$file", O_RDONLY 
    091       or LOGDIE "$file: $!";
    092 
    093   while( sysread( FILE, my $val, 4 ) ) {
    094     my($c1, $c2) = unpack 'ss', $val;
    095     $c1 = 0 if $c1 < $MIN_SIZE;
    096     push @vals, $c1;
    097   }
    098   close FILE;
    099   return @vals;
    100 }
    101 
    102 ###########################################
    103 sub raw_bpm {
    104 ###########################################
    105   my(@samples) = @_;
    106 
    107   my $win   = scalar @samples / 
    108               ($NWINDOWS*$SAMPLE_SECS);
    109   my($bumps, $pmax, $slope) = (0, 0, "up");
    110 
    111   for( my $o = 0; $o <= $#samples - $win; 
    112        $o += $win ) {
    113     my $max = 0;
    114     for( my $i = $o; $i <= $o + $win; 
    115          $i++ ) {
    116       if( $samples[$i] > $max ) {
    117         $max = $samples[$i];
    118       }
    119     }
    120 
    121     if( $slope eq "up" ) {
    122       if( $max < $MIN_DROP * $pmax ) {
    123         $slope = "down";
    124         $bumps++;
    125       }
    126     } else {
    127       $slope = "up" if  $max > $pmax;
    128     }
    129     $pmax = $max;
    130   }
    131 
    132   return int($bumps / $SAMPLE_SECS * 60.0) 
    133          || 1;
    134 }

Der zweite SQL-Befehl, den Zeile 37 mit Platzhaltern vorbereitet, frischt den Wert der BPM-Spalte in der Datenbank auf, in dem es die Uri als Selektionskriterium mit WHERE vorgibt. Zeile 46 schickt mit execute() den Update mit den eingesetzten Parametern Uri und BPM an die Datenbank ab. Während die while-Schleife alle gefundenen Audiodateien abklappert, bleibt das SQL-Statement in $upd_sth gespeichert und Zeile 46 ruft es jedesmal mit neuen Parametern auf. Nach dem Ende der while-Schleife gibt finish() die intern angelegten Datenstrukturen wieder frei.

Datenwust reduzieren

Die Errechnung des BPM-Wertes einer Audiodatei übernimmt die Funktion bpm() ab Zeile 54. Falls ihr schon eine .raw-Datei überreicht wurde, übernimmt Zeile 61 diese, doch im Normalfall dürfte es sich um .wav, .mp3, .ogg oder Ähnliches handeln. Der mit der tap()-Funktion des CPAN-Moduls abgesetzte sox-Befehl ab Zeile 67 extrahiert 30 Sekunden Musik nach der 1-Minuten-Marke, pfercht sie durch den schmalen Bandpass und legt die resultierenden Rohdaten in einer gleichnamigen .raw-Datei ab. Um den Datenwust auf ein erträgliches Maß zurechtzustutzen, reduziert es die Samplingrate auf den in Zeile 11 gesetzten Wert für $SAMPLE_RATE (10.000 pro Sekunde).

Die Funktion samples() ab Zeile 84 liest dann die in der .raw-Datei abgelegten Werte in 4-Byte-Schritten mit sysread() aus (2 Kanäle à 2 Byte) und nutzt Perls eingebaute Funktion unpack() mit dem Platzhalter 'ss', um die zwei vorzeichenbehafteten Integer zu extrahieren. Sie ignoriert den Wert für den zweiten Kanal in $c2 (weil identisch) und nur der erste Kanalwert in $c1 wandert ans Ende des Ergebnisarrays @vals, falls er über dem Schwellwert $MIN_SIZE liegt. Dieser in Zeile 14 auf 500 gesetzte Wert soll verhindern, dass die Maximumsuche sich in dümpelnden Signalen in leisen Passagen verliert.

Bergsteigermethode

Im Voraus weiß das Skript nicht, wieviele Maxima es im Datenarray finden wird und hat auch keinen Anhaltspunkt über deren Höhe. Es arbeitet deswegen nach der sogenannten "Bergsteigermethode" (Abbildung 10), indem es jeweils alle Signalamplituden in einem schmalen Zeitfenster (z.B. einer 1/20 Sekunde) untersucht, und das dort gefundene lokale Maxium speichert. Ist das lokale Maximum im nächsten Zeitfenster größer als das gespeicherte, befindet sich der Signalgraph im Aufwärtstrend und der Algorithmus setzt ein Flag. Stellt sich in diesem Modus im nächsten Zeitfenster ein kleineres lokales Maximum ein, wurde soeben ein globales Signalmaximum überschritten und das Skript erhöht den entsprechenden Zähler.

Abbildung 10: Bergsteigermethode zur Maximumsbestimmung: Das erste Fenster, dessen Maximalwert nach einem Anstieg unter dem vorherigen Wert liegt, zeigt die Überschreitung eines Maximums an.

Dieses Verfahren funktioniert, da die maximale Anzahl zu findender Maxima nach oben beschränkt ist. BPM-Werte über 600 sind unsinning und uninteressant, und somit gilt als gesichert, dass innerhalb zweier Zeitfenster von jeweils einer 1/20 Sekunde keine zwei Maxima vorliegen.

Die Funktion raw_bpm() nimmt den Datenarray eines Kanals entgegen und macht sich auf Gipfelsuche. Zeile 106 definiert die Fensterbreite, indem sie die Anzahl der Datenpunkte durch die gewünschte Fensterdichte ($NWINDOWS, auf 20 Windows pro Sekunde gesetzt) mal der Anzahl der Sample-Sekunden dividiert. Die Variable $slope markiert das Flag, mit dem der Algorithmus feststellt, ob er sich gerade inmitten eines Aufwärts- ("up") oder Abwärtstrends ("down") befindet. Das letzte globale Maximum speichert raw_bpm() in $pmax und das lokale Maximum des gerade untersuchten Fensters steht in $max. Damit kleinere Signalfluktuationen das Verfahren nicht aus dem Tritt bringen, definiert $MIN_DROP mit 0.7, dass das globale Maximum mindestens um 30% fallen muss, damit der vorher durchlaufene Hügel als Maximum gilt und der Zähler $bumps um eins erhöht wird. Zeile 131 dividiert die Anzahl gefundender Maxima dann noch durch die Sekundenlänge des untersuchten Datenbereichs und multipliziert das Ergebnis mit 60, um den Minutenwert BPM zu erhalten.

Installation

Die zusätzlich zu installierenden Perlmodule DBI, DBD::SQLite, Sysadm::Install, URI::Escape und Log::Log4perl finden sich entweder als Pakete in der verwendete Distro (z.B. libdbi-perl, libdbd-sqlite-perl, usw. auf Ubuntu) oder lassen sich mit einer CPAN-Shell aufs lokale System übertragen.

sox ist nicht überall mit mp3-Support verfügbar. Wessen Gesetzeslage die Verwendung des Formats nicht erlaubt, kann statt dessen problemlos mit .wav- oder .ogg-Dateien arbeiten.

Bei größeren Musiksammlungen sollte Banshee aus Performancegründen im Tools-Dialog auf MySQL umgestellt werden. Dank DBIs Datenbankunabhängigkeit muss das Skript dann lediglich die connect()-Zeile anpassen und statt "dbi:SQLite:$db" das Kürzel "dbi:mysql:dbname" angeben, sowie Username und Passwort dort einfügen, wo in der SQLite-Version die beiden Leerstrings stehen.

Als weitere Idee zur automatischen Klassifizierung von Audio-Dateien böte sich die Unterscheidung von Hörbüchern, gesprochenen Texten und Musikstücken an. Oder vielleicht sogar eine harmonische Analyse, sodass an manchen Tagen nur fröhliche Stimmungsmusik erschallte und an anderen auch Moll- oder gar Misstöne erlaubt wären?

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2011/06/Perl

[2]

Banshee Player Home Page: http://banshee.fm

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.