Der bekannte Musikplayer Banshee legt die Metadaten der von ihm verwalteten Songs in einer SQLite-Datenbank ab, die zwei Perl-Skripte auslesen, restaurieren und mit automatisch ermittelten Beats-per-Minute-Werten auffrischen.
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 die jahrelang mühsam von Hand erstellten Song-Ratings nicht so einfach übertragen. Zwar werde ich niemals einen anderen Editor als »vi« benutzen, doch neulich probierte ich den Musikplayer Banshee ([2], Abbildung 1) aus, da die bislang in den Perlmeister-Studios verwendete Rhythmbox keinen einfachen Rating-Export anbietet.
Das GUI kommt extrem sauber und durchdacht daher. Als ich dann entdeckte, dass sich Ratings in Banshee ganz einfach sichern, exportieren oder extern manipulieren lassen, weil der Player sie in einer leicht zugänglichen SQLite-Datenbank ablegt, war’s um mich geschehen, die Operation Playerwechsel lief an.
Wie in Abbildung 2 zu sehen ist, sichert Banshee die Song-Metadaten in der Tabelle »CoreTracks« der Datenbankdatei »~/.config/banshee-1/banshee.db« . Der Pfad zu den Audiodateien im System steht in der »Uri« -Spalte.
Bewertungen gesichert
Wie der SQLite-Befehl ».schema« zeigt, enthält die Tabelle weitere interessante Spalten wie die Anzahl der vergebenen Ratingsterne (»Rating« ) oder die Basstrommelschläge pro Minute (»BPM« , Beats per Minute). Listing 1 sichert alle Song-Ratings in einer YAML-Datei (Abbildung 3).
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 }
Ohne Parameter aufgerufen öffnet Listing 1 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 null ist, springt Zeile 44 weiter zum nächsten Eintrag. Findet sich aber ein positiver Wert, speichert Zeile 46 ihn im Hash »%ratings« unter dem Pfad der Audiodatei. Am Ende der Tabelle schreibt die Funktion »DumpFile()« aus dem YAML-Modul die Ratings in die YAML-Datei »banshee-ratings.yml« .
Zum Restaurieren der Ratings ruft der User das Skript mit »banshee-rating-backup restore« auf, worauf es die Datei »ban-shee-ratings.yml« mittels »LoadFile()« in Zeile 60 einliest und über den so initialisierten Hash iteriert. Dessen Schlüssel sind die Pfadnamen zu den Audiodateien und seine Werte die Ratings, sodass Zeile 67 pro Eintrag einfach ein SQL-Update absetzen muss, um die Datenbank wieder auf Vordermann zu bringen.
Der typische DBI-Dreisprung besteht aus dem Zusammenstellen der SQL-Query mit »prepare()« , einem anschließenden »execute()« mit Parametern, der die in der Query freigehaltenen Variablen ersetzt, und einem abschließenden »finish()« zum Freigeben des Statement-Handle.
Stampfer pro Minute
Um eine Playlist für bestimmte Anlässe oder Gemütszustände zusammenzustellen, 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 ein 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 aber ein BPM-Erkennungstool mit. Ein Klick auf eine im Dateiscan-Dialog versteckte Checkbox aktiviert es (Abbildung 4). Der Menüpunkt »Tools | Rescan Music Library« startet das CPU-intensive Update, das bei großen Sammlungen einige Zeit läuft.
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 dreimal 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 dem Utility »sox« in rohe Audiodaten um, jagt diese durch einen schmalen Bandpass im Bass-Bereich und misst dann die Abstände zwischen den hoffentlich ausgeprägten Bass-Maxima. Diese Methode funktioniert zwar auch nicht fehlerfrei, unterscheidet aber klar Discostampf von Klassik.
Listing 2
banshee-bpm-update (Teil 1)
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 $sth->execute();
036
037 my $upd_sth = $dbh->prepare(
038 "UPDATE CoreTracks SET BPM=? " .
039 "WHERE Uri = ?");
040
041 while( (my $uri) =
042 $sth->fetchrow_array() ) {
043 my $file = uri_unescape( $uri );
044 $file =~ s#^file://##;
045 INFO "Updating $uri";
046 $upd_sth->execute( bpm( $file ),
047 $uri );
048 }
049 $upd_sth->finish();
050 $sth->finish();
051 }
052
053 ###########################################
054 sub bpm {
055 ###########################################
056 my( $file ) = @_;
057
058 my $rawfile;
059
060 if( $file =~ /\.raw$/ ) {
061 $rawfile = $file;
062 } else {
063 $rawfile = File::Temp->new(
064 SUFFIX => ".raw", UNLINK => 1 );
065
066 my($stdout, $stderr, $rc) =
067 tap "sox", $file, "-r", $SAMPLE_RATE,
068 "-c", 2, "-b", 16, "-t", "raw",
069 "-e", "signed", $rawfile,
070 "bandpass", 100, 1,
071 "trim", $OFFSET, $SAMPLE_SECS;
072
073 if( $rc ) {
074 LOGWARN "sox $file: $stderr";
075 return 0;
076 }
077 }
078
079 return raw_bpm(
080 samples( $rawfile->filename ) );
081 }
082
083 ###########################################
084 sub samples {
085 ###########################################
086 my( $file ) = @_;
087
088 my @vals = ();
089 sysopen FILE, "$file", O_RDONLY
090 or LOGDIE "$file: $!";
091
092 while( sysread( FILE, my $val, 4 ) ) {
093 my($c1, $c2) = unpack 'ss', $val;
094 $c1 = 0 if $c1 < $MIN_SIZE;
095 push @vals, $c1;
096 }
097 close FILE;
098 return @vals;
099 }
100
101 ###########################################
102 sub raw_bpm {
103 ###########################################
104 my(@samples) = @_;
105
106 my $win = scalar @samples /
107 ($NWINDOWS*$SAMPLE_SECS);
108 my($bumps, $pmax, $slope) = (0, 0, "up");
109
110 for( my $o = 0; $o <= $#samples - $win;
111 $o += $win ) {
112 my $max = 0;
113 for( my $i = $o; $i <= $o + $win;
114 $i++ ) {
115 if( $samples[$i] > $max ) {
116 $max = $samples[$i];
117 }
118 }
119
120 if( $slope eq "up" ) {
121 if( $max < $MIN_DROP * $pmax ) {
122 $slope = "down";
123 $bumps++;
124 }
125 } else {
126 $slope = "up" if $max > $pmax;
127 }
128 $pmax = $max;
129 }
130
131 return int($bumps / $SAMPLE_SECS * 60.0);
132 }
Das als Paket für viele Distributionen erhältliche Audiotool Audacity zeigt in den Abbildungen 6 und 7, wie die Audiodaten zweier unterschiedlicher Musikgenres aussehen. Während das klassische Orchesterstück nur mäßig ausgesteuert ist und keinen stetigen Rhythmus erkennen lässt, zeigt der breitbandige Synthi-Sound durchaus periodische Tendenzen.

Abbildung 6: “Zu Hilfe, zu Hilfe, sonst bi-i-in ich verloren” trällert Tamino in Mozarts “Zauberflöte”.
Abgetastete Musik
Die Musik, die das Ohr als Bündel gleichzeitig gespielter Tonfrequenzen hört, erzeugt die Soundkarte aus digitalen Abtastwerten. Eine Stereo-Aufnahme besteht pro Kanal typischerweise aus 44 100 verschiedenen 16-Bit-Messwerten pro Sekunde, die zwischen -32768 und +32767 variieren. Ein reiner Ton käme als Sinuskurve zum Vorschein, aber Musikinstrumente oder Stimmen erzeugen ein weites Frequenzspektrum.
Die Abbildung 8 demonstriert als Beispiel die Audiodaten einer 1-Hz-Sinusschwingung im Rohformat. Wer beispielsweise den ersten (grünen) Kanal betrachtet, der sieht, dass die Werte von »15 06« , »18 06« , »1c 06« und so weiter stetig ansteigen. (Zu beachten ist die umgekehrte Byte-Order auf Intel-Prozessoren, die zugehörigen dezimalen Werte sind 1557, 1560 und 1564).
Innerhalb einer Periode mit 44 100 Abtastwerten durchläuft das 1-Hz-Signal einmal einen Bereich, der mit insgesamt 65536 Werten kodiert ist, also ist der Sprung von 0x0615 (dezimal 1557) zu 0x0618 (dezimal 1560) schon 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 Werte auf.
Eine MP3-Datei kodiert die Abtastwerte auf raffinierte Weise, das Skript muss sie vor der Analyse erst ins Rohformat von Abbildung 8 bringen. Hierzu eignet sich das Utilty »sox« , das mit dem Aufruf
sox infile.mp3 -r 44100 -c 2 -b 16 -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 44100 Hz erzeugt.Um darin die Bass-Aktivität zu analysieren und die zu bearbeitenden Daten auf 30 Sekunden zu beschränken, hängt der BPM-Messer an das Kommando oben noch folgende Argumente an:
... bandpass 100 1 trim 60 30
Der Bandpass blendet Frequenzen, die nicht im Bereich einer Basstrommel liegen mit einem 3-dB-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.
Abbildung 9 zeigt die auf diese Weise gefilterten Audiodaten verschiedener Titel. Die Basstrommel von “I Feel Fine” und das Synthi-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 Liedern wie “I Got a Name” von Jim Croce hingegen liefert der Algorithmus den Wert 0.

Abbildung 9: Die Bass-Linie von Songs verschiedener Genres nach dem Anwenden des schmalspurigen Bandfilters.
Nach dem Verbinden mit der Datenbankdatei ruft Listing 2 die Funktion »bpm_update()« ab Zeile 29 auf. Die 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« kodiert sind, verwandelt die Funktion »uri_escape()« aus dem CPAN-Modul URI::Escape sie wieder in normale Leerzeichen zurück. Die Zeile 44 entfernt anschließend noch das »file://« – und schon steht in der Variablen »$file« der Unix-Pfad zur Audiodatei.
Das zweite SQL-Kommando, das Zeile 37 mit Platzhaltern vorbereitet, frischt den Wert der BPM-Spalte auf, indem es die URI als Selektionskriterium mit Where vorgibt. Zeile 46 schickt mit »execute()« das 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 jedes Mal mit neuen Parametern auf. Nach dem Ende der While-Schleife gibt »finish()« die Datenstrukturen wieder frei.
Das Errechnen des BPM-Werts einer Audiodatei übernimmt die Funktion »bpm()« ab Zeile 54. Falls sie bereits eine Raw-Datei überreicht bekam, übernimmt diese jetzt Zeile 61. Doch im Normalfall dürfte es sich um ein WAV-, MP3- Ogg-Format 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 er die Samplingrate auf den in Zeile 11 gesetzten Wert für »$SAMPLE_RATE« (10 000 pro Sekunde).
Die Funktion »samples()« ab Zeile 84 liest anschließend die in der Raw-Datei abgelegten Werte in 4-Byte-Schritten mit »sysread()« aus (zwei Kanäle à 2 Byte) und nutzt Perls eingebaute Funktion »unpack()« mit dem Platzhalter »ss« , um die beiden 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 Ergebnisarray »@vals« , falls er über dem Schwellenwert »$MIN_SIZE« liegt. Dieser in Zeile 14 auf 500 gesetzte Wert soll verhindern, dass die Maximumsuche sich in dümpelnden Signalen oder leisen Passagen verliert.
Bergsteigermethode
Im Voraus weiß das Skript nicht, wie viele Maxima es im Datenarray findet, und hat keinen Anhaltspunkt über deren Höhe. Es arbeitet daher nach der Bergsteigermethode (Abbildung 10), indem es alle Signalamplituden in einem schmalen Zeitfenster (etwa 1/20 Sekunde) untersucht und das dort gefundene lokale Maximum speichert.

Abbildung 10: Bergsteigermethode: zur Maximum-Bestimmung: Das erste Fenster, dessen Maximalwert nach einem Anstieg unter dem vorherigen Wert liegt, zeigt die Überschreitung des Maximums an.
Ist das lokale Maximum im nächsten Zeitfenster größer als das gespeicherte, klettert der Signalgraph aufwärts und der Algorithmus setzt ein Flag. Stellt sich in diesem Modus im nächsten Zeitfenster ein kleineres lokales Maximum ein, ist soeben ein globales Signalmaximum überschritten und das Skript erhöht den entsprechenden Zähler.
Das Verfahren funktioniert, da die maximale Anzahl zu findender Maxima nach oben beschränkt ist. BPM-Werte über 600 sind sinnlos und uninteressant, daher 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 das Produkt aus den gewünschten Fensterdichten (»$NWINDOWS« , auf 20 Windows pro Sekunde gesetzt) und der Anzahl der Sample-Sekunden dividiert.
Die Variable »$slope« markiert das Flag, mit dem der Algorithmus feststellt, ob er sich in einem Aufwärts- oder Abwärtstrend befindet. Das letzte globale Maximum speichert »raw_bpm()« in »$pmax« , das lokale Maximum des 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 Prozent fallen muss, damit der vorher durchlaufene Hügel als Maximum gilt. Zeile 131 dividiert die Anzahl gefundener Maxima dann noch durch die Sekundenlänge des untersuchten Datenbereichs und multipliziert das Ergebnis mit 60, um BPM zu erhalten.
Installation
Die nötigen Perl-Module DBI, DBD::SQLite, Sysadm::Install, URI::Escape sowie Log::Log4perl finden sich entweder als Pakete in der verwendeten Linux-Distribution oder lassen sich mit einer CPAN-Shell einspielen. Wer kein MP3 verwenden will, kann mit WAV- oder Ogg-Dateien arbeiten.
Bei großen Musiksammlungen sollte der Benutzer Banshee aus Performance-Gründen im Tools-Dialog auf MySQL umstellen. Dank DBIs Datenbankunabhängigkeit muss das Skript dann nur die »connect()« -Zeile anpassen, statt »”dbi:SQLite:$db”« das Kürzel »”dbi:mysql:dbname“« einfügen sowie einen Usernamen und ein Passwort angeben.
Infos
- Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2011/06/Perl
- Banshee-Player-Homepage: http://banshee.fm












