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.
Abbildung 1: Das aufgeräumte GUI des Musikplayers Banshee.
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.
Abbildung 2: Dank des offenen Datenbankdesigns lässt sich Banshee in die Karten sehen.
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).
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 }
Abbildung 3: Ausschnitt aus der YAML-Datei, in der
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.
Abbildung 4: Banshee ermittelt 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 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.
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".
Abbildung 7: Voller Synthi-Sound: "Memories" von David Guetta.