Open Source im professionellen Einsatz
Linux-Magazin 06/2011
© Dmitriy Shironosov, 123RF.com

© Dmitriy Shironosov, 123RF.com

Perl-Skripte analysieren und archivieren Tanzmusik

Heiße Rhythmen

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.

1410

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).

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 }

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.

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".

Abbildung 7: Voller Synthi-Sound: "Memories" von David Guetta.

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 5 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

  • Trainierter DJ

    Je nach Stimmungslage haben Musikliebhaber mal Lust auf Rock, mal auf Schmusepop. Ein MP3-Player mit grafischer GTK-Oberfläche wählt aus der privaten Sammlung passende Lieder aus, erstellt eine Playliste und spielt sie ab. Das Perl Object Environment sorgt dafür, dass alles flüssig abläuft.

  • Perl-Snapshot Linux-Magazin 2011/06

    "Perlmeister" Michael Schilli hat seinen Snapshot aus dem Magazin 2011/06 als Screencast verarbeitet.

  • Mediaplayer Banshee erreicht 1.0

    Gnomes Mediaplayer Banshee hat offiziell die Version 1.0 erreicht. Die Software basiert auf Mono, der freien, hauptsächlich von Novell entwickelten, .NET-Implementierung und bringt zahlreiche Funktionen mit, die auch die KDE-Konkurrenz Amarok anbietet.

  • Final Cut

    Ein handgedrehtes Video sieht mit einem Vorspann gleich professioneller aus. Die Tools Mencoder und Sox helfen bei der Formatfitzelei und ein Perl-Skript automatisiert den Vorgang.

  • Kleiner Lauschangriff

    Ist auf der heimischen Telefonnummer mal wieder kein Durchkommen, lauscht ein Skript an der Leitung und signalisiert dem Wartenden via Web, wann er erneut anrufen kann.

comments powered by Disqus

Ausgabe 06/2017

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.