Spiel's nochmal, Sam ... rückwärts (Linux-Magazin, März 2010)

Verkehrt herum abgespielte Songtexte blicken auf eine lange Tradition zurück. Eine Perl-Applikation steuert mit einer einfachen Curses-GUI die Aufnahme durch ein Mikrofon und spielt das Ergebnis rückwärts ab.

Neulich lauschte ich im Internet-Radio dem Song ``Work It'' von Missie Elliot und stellte fest, dass ich Teile des Refrains trotz ordentlicher Englischkenntnisse seit seinem Erscheinen im Jahre 2002 noch nie recht verstanden hatte. Wikipedia [2] löste das Rätsel, denn die feiste Hip-Hopperin hatte einfach Gesangssequenzen rückwärts eingespielt!

So wie die Alten sungen ...

Dieses sogenannte ``Backmasking'' [3] geht bis auf die Beatles zurück, und auch in meiner Jugendzeit war es ein beliebter Zeitvertreib. Denn damals gab es noch keine Killerspiele, und als das pädagogisch wertvolle Holzspielzeug seine Spannung verlor, blieb uns nur, von gutgläubigen Erwachsenen aus der Apotheke geholte Chemikalien zu lautstarken Sprengsätzen zu vermengen. Außerdem hatte es uns ein Cassettenrekorder angetan, den man durch gezielte Manipulation des Antriebs dazu brachte, aufgenommene Bänder verkehrt herum abzuspielen. Man sprach ``Redro Kernettesack!'' aufs Band und aus dem Lautsprecher kam in einem unvorstellbarem Akzent, irgendwo zwischen osteuropäisch und außerirdisch: ``Kkkassettnrekorrdeeer!'' Stundenlanger Spaß für die ganze Familie!

Das Skript in Listing flipit produziert nach dem Aufruf von der Kommandozeile die Fußzeile in Abbildung 1 und lädt dazu ein, die Taste ``R'' (für Record) zu drücken, um eine ins Mikrofon gesprochene Nachricht aufzunehmen. Während der Aufnahme erscheint das Menü in Abbildung 2, das darauf hinweist, dass ``S'' (für Stopp) die Aufnahme beendet und ``P'' (für Play) die aufgenommene .ogg-Datei verkehrtherum abspielt. Während der Wiedergabe erscheint der Text in Abbildung 3, der sofort wieder dem Menü in Abbildung 1 Platz macht, falls keine Sounddaten mehr vorliegen.

Abbildung 1: Am Programmstart: Die Taste [r] startet eine Aufnahme.

Abbildung 2: Während der Aufnahme: [s] beendet die Aufnahme, [p] spielt sie gleich auch noch rückwärts ab.

Abbildung 3: Mit dem Tool Sox kehrt flipit die Aufnahme um und spielt so das Original rückwärts ab.

Curses tanzt mit POE

Die minimalistische graphische Oberfläche erzeugt das Modul Curses::UI::POE, das die Grafikroutinen der Curses-Library mit der Multitasking-Umgebung POE verbandelt. Denn während das Skript langwierige Vorgänge wie die Aufnahme einer Sounddatei, deren Umkehrung oder das Abspielen steuert, soll die GUI blitzschnell auf User-Eingaben reagieren. Erfahrene Leser dieser Rubrik wissen, dass die kooperative Multitasking-Umgebung POE dafür die notwendigen Voraussetzungen mitbringt. Sie verfolgt einen asynchronen Ansatz, der Anfängern oft Kopfzerbrechen bereitet. Hat man den Bogen aber erst einmal raus, lassen sich mit ihr schnell robuste Applikationen bauen.

Listing 1: flipit

    001 #!/usr/local/bin/perl -w
    002 use strict;
    003 use POE;
    004 use POE::Wheel::Run;
    005 use Curses::UI::POE;
    006 use File::Temp qw(tempfile);
    007 use Sysadm::Install qw(:all);
    008 use POSIX;
    009 
    010 our $HEAP;
    011 
    012 my $CUI = Curses::UI::POE->new(
    013   -color_support => 1, 
    014   inline_states  => {
    015     _start => sub {
    016       $HEAP = $_[HEAP];
    017     },
    018     play_ended => \&footer_update,
    019 });
    020 
    021 my $WIN = $CUI->add("win_id", "Window");
    022 
    023 my $FOOT = $WIN->add(qw( bottom Label
    024   -y -1 -paddingspaces 1
    025   -fg white -bg blue));
    026 
    027 footer_update();
    028 
    029 $CUI->set_binding(sub { exit 0; }, "q");
    030 $CUI->set_binding( \&play_flipped, "p");
    031 $CUI->set_binding( \&record, "r" );
    032 $CUI->set_binding( \&record_stop, "s" );
    033 
    034 $CUI->mainloop;
    035 
    036 ###########################################
    037 sub record {
    038 ###########################################
    039   if(defined $HEAP->{recorder}->{wheel}) {
    040     return; # Still recording
    041   }
    042 
    043   my($fh, $tempfile) = tempfile(
    044       SUFFIX => ".ogg", UNLINK => 1);
    045 
    046   my $wheel =
    047     POE::Wheel::Run->new(
    048       Program     => "rec",
    049       ProgramArgs => [$tempfile],
    050       StderrEvent => 'ignore',
    051   );
    052 
    053   $HEAP->{recorder} = {
    054       wheel => $wheel,
    055       file  => $tempfile,
    056   };
    057 
    058   $FOOT->text("Recording ... " .
    059    "([s] to stop, [p] to play)");
    060   $FOOT->draw();
    061 }
    062 
    063 ###########################################
    064 sub record_stop {
    065 ###########################################
    066   my $wheel = $HEAP->{recorder}->{wheel};
    067 
    068   return if !defined $wheel;
    069 
    070   $wheel->kill(SIGTERM);
    071   delete $HEAP->{recorder}->{wheel};
    072   footer_update();
    073 }
    074 
    075 ###########################################
    076 sub footer_update {
    077 ###########################################
    078   my $text = "[r] to record";
    079 
    080   if(defined $HEAP->{recorder}->{file}) {
    081     $text .= ", [p] to play";
    082   }
    083 
    084   $text .= ", [q] to quit";
    085 
    086   $FOOT->text($text);
    087   $FOOT->draw();
    088 }
    089 
    090 ###########################################
    091 sub play_flipped {
    092 ###########################################
    093   if(defined $HEAP->{recorder}->{wheel}) {
    094         # Active recording? Stop it.
    095       record_stop( $HEAP );
    096   }
    097 
    098   $FOOT->text("Playing ...");
    099   $FOOT->draw();
    100 
    101   my $recorded = $HEAP->{recorder}->{file};
    102 
    103   return if ! defined $recorded;
    104 
    105   my $wheel =
    106     POE::Wheel::Run->new(
    107       Program     => \&sox_play,
    108       ProgramArgs => [$recorded],
    109       StderrEvent => 'ignore',
    110       CloseEvent  => 'play_ended',
    111   );
    112 
    113   $HEAP->{players}->{$wheel->ID} = $wheel;
    114 }
    115 
    116 ###########################################
    117 sub sox_play {
    118 ###########################################
    119   my($recording) = @_;
    120 
    121   my($fh, $tmpfile) = 
    122      tempfile(SUFFIX => ".ogg");
    123 
    124   tap "sox", $recording, 
    125              $tmpfile, "reverse";
    126   tap "play", $tmpfile;
    127 
    128   unlink $tmpfile;
    129 }

Der Konstruktoraufruf in Zeile 13 schaltet mit color_support Terminalfarben ein und bestimmt zwei POE-typische Zustände. Den ersten, _start, springt POE sofort nach dem Start des POE-Kernels an, praktisch als Mutter aller Zustände des Zustandsautomaten. Zeile 16 nutzt ihn nur dazu, eine Referenz auf die ebenfalls POE-typische Datenstruktur eines Session-Heaps in der globalen Variablen $HEAP abzulegen, damit der als Variablenspeicher genutzte Hash auch außerhalb der POE-Welt zugänglich ist. Den zweiten Zustand, play_ended, springt POE an, falls ein aufgenommener Sound erfolgreich rückwärts abgespielt wurde. Für diesen Fall definiert Zeile 18 den Handler footer_update(), der den Text auf dem Fußbalken dem Status des Apparats anpasst.

Widgets auf den Schirm!

Die GUI setzt sich aus einem Hauptfenster $WIN und einer Fußzeile $TOP zusammen. Das Hauptfenster ruft Zeile 21 durch die Methode add() des Moduls Curses::UI::POE ins Leben. Letzteres stellt lediglich zu Curses::UI, genauer gesagt, Curses::UI::Container durch. Der erste Parameter ist die für das Window gewünschte ID (im Skript auf "win_id" gesetzt), und der zweite Parameter, "Window", bestimmt die Klasse des neu erzeugten Widgets.

Das zweite Widget, die Fußzeile mit den Instruktionen für gerade erlaubte User-Aktionen, ensteht in Zeile 23. GUI-typisch erzeugt das Parent-Widget $WIN durch einen Aufruf der Methode add() das in ihm liegende Fußzeilenwidget. Mit -fg white und -bg blue legt es Weiße Schrift (Vordergrund) auf blauem Hintergrund fest. Der erste Parameter "bottom" ist die gewünschte ID des neu erzeugten Fensters, der zweite, ``Label'', die Widget-Klasse. Der Wert -1 für den Parameter ``-y'' gibt an, dass das Label ganz unten am Fensterrand zu platzieren ist. Die Option -paddingspaces dehnt das Label bis an die horizontalen Grenzen des einschließenden Fensters aus.

Das Label verfügt über eine text()-Methode, die den auf der Fußzeile zu sehenden Text löscht und setzt. Die in Zeile 27 aufgerufene Funktion footer_update() frischt die neu definierte und vorerst leere Fußzeile erstmals mit dem im Startzustand möglichen User-Kommandos auf.

Bereit zum Loslaufen

Die Zeilen 29 bi 32 definieren, was passiert, wenn der User die Tasten "q" (Quit), "p" (Play), "r" (Record) und "s" (Stop Recording) drückt. Bei ``q'' ruft flipit die Funktion exit() auf, die das Programm abbricht. Die GUI fällt dabei sauber in sich zusammen. Die zugewiesenen Handlerfunktionen play_flipped (Audiodatei verkehrt herum abspielen), record() (Aufnahme starten) und record_stop() (Aufnahme anhalten) definiert das Skript weiter unten. Der Einfachheit halber greifen sie alle auf den globalen $HEAP zu, und, indirekt über die Funktion footer_update(), auf die ebenfalls globalen Widget-Variablen.

GUI-typisch springt das Programm dann in Zeile 34 in die Haupteventschleife, aus der es nie mehr austritt, und solange Usereingaben verarbeitet, bis jemand ``q'' drückt und damit den Teppich unter der GUI wegzieht. Drückt der User ``r'', springt die GUI die Funktion record() ab Zeile 37 an. Diese prüft zunächst, ob bereits eine Aufnahme läuft, und bricht in diesem Fall mit return ab, ignoriert also den Tastendruck.

Audiotool Sox

Falls nicht, legt die Funktion tempfile() aus dem Modul File::Temp eine temporäre Datei mit der Endung ``.ogg'' an, die Dank der gesetzten UNLINK-Option von Perls automatischer Abrißbirne bei Programmende wegefegt wird. Der Suffix ist wichtig, denn das nachfolgend aufgerufene Tool sox schließt daraus auf das in der Audiodatei zu verwendende Kodierungsverfahren. Externe Programme startet in POE ein Objekt der Klasse POE::Wheel::Run, das die GUI ruckellos weiterlaufen lässt und bei Bedarf sogar Aktionen auslöst, wenn das losgelassene Programmkind sich verabschiedet. Das Wheel in Zeile 47 ignoriert allerdings die Events, die bei Ausgaben nach STDERR auftreten (StderrEvent), und nur der User kann den Lauf des Rekorders beenden. Das aufgerufene Programm rec liegt dem sox-Paket bereits bei (genau wie die später genutzte Utility play) und nimmt nur den Namen der zu erstellenden Audiodatei entgegen. Es liest Audiodaten über ein eingebautes Laptop-Mikrofon oder ein extern an die Soundkarte angestöpseltes. Zu beachten ist, dass POE::Wheel::Run das aufzurufende Programm und dessen Parameterliste von einander getrennt in den Parametern Program und ProgramArgs erwartet.

Der Code ab Zeile 46 hält die GUI übrigens ganz und gar nicht auf, alle notwendigen Aktionen geschehen im Hintergrund. Damit das Wheel nicht nach dem Verlassen der Funktion record seine letzte Referenz verliert und Perls Garbage-Collector zum Opfer fällt, speichert Zeile 53 eine Referenz darauf unter dem Schlüssel recorder im POE-spezifischen $HEAP ab. Weiter sichert es dort den Namen der temporären Datei, damit die Abspielfunktion später darauf zugreifen kann. Am Ende frischt record() noch die Fußzeile auf, um dem User mitzuteilen, dass er mit ``s'' stoppen und mit ``p'' stoppen und den Abspielvorgang einleiten kann.

``S'' für Stop

Bei ``s'' kommt die Funktion record_stop an die Reihe, und prüft zunächst, ob überhaupt ein Wheel läuft. Falls nicht, hat ein wohl ein Übermütiger ``s'' gedrückt, ohne dass überhaupt eine Aufnahme lief. Zeile 70 schießt das laufende ``rec''-Programm dann mit dem über das POSIX-Modul hereingeholte SIGTERM-Signal ab, und im Anschluß streicht Zeile 71 die Referenz des soeben (hoffentlich) ruckartig beendeten Wheels aus dem $HEAP.

Die Funktion footer_update() frischt mit der text()-Methode die Fußzeile auf und schickt ein draw() hinterher, damit die GUI sich auch veranlasst fühlt, das Widget neu auf den Bildschirm zu zeichnen. Der Abspiel-Handler play_flipped stoppt zunächst etwaige laufende Aufnahmen und ruft dann in Zeile 105 das Abspiel-Wheel auf. Dieses definiert einen CloseEvent, den in Zeile 18 definierten POE-Zustand play_ended anspringt, sobald die vom Wheel aufgerufene Funktion sox_play() (ab Zeile 117) zurückkehrt. Auch hier vertrödelt POE keine Zeit, sondern führt sox_play() asynchron aus und kommuniziert mit ihren Ausgabe-, Fehler- und Ende-Events. Damit auch dieses Wheel nicht in sich zusammenfällt, wenn das Skript den Scope der Funktion play_flipped verlässt (was wegen des asynchronen Aufrufs noch passiert, bevor das Wheel die externe Funktion überhaupt startet), speichert Zeile 113 eine Referenz darauf im $HEAP ab. Jedes Wheel hat innerhalb einer POE-Session eine eindeutige ID und da play_flipped() die Referenz unter der ID abspeichert, könnte der User auch mehrere Abspielvorgänge quasi parallel starten. Wer dies ausprobieren möchte, darf gerne mal drei, vier Mal kurz hintereinander die Taste ``p'' drücken.

Umgedreht und Abgespielt

Die Funktion sox_play legt eine weitere temporäre, zunächst leere .ogg-Datei an und übergibt sie dem mit der Funktion tap aus dem CPAN-Modul Sysadm::Install aufgerufene Kommando der Utility "sox":

    sox input.ogg output.ogg reverse

Die nachgestellte Option reverse weist sox an, die Eingabedatei nicht einfach in die Ausgabedatei zu überführen, sondern sie dabei auch noch zeitverkehrt umzudrehen. Die Ergebnisdatei schnappt sich dann die Sox-Utility play in Zeile 126 und spielt sie über die Soundkarte des verwendeten PCs ab. Die umgedrehte Datei löscht Zeile 128 anschließend, denn ein erneuter Aufruf von sox_play legt eine neue an.

Micro aktivieren

Damit rec das richtige Mikrofon anspricht, um die eintrudelnden Audiodaten einzulesen, war es bei dem im Testlabor verwendeten Netbook notwendig, die Audio-Utility alsamixer aufzurufen. Die Startseite zeigt die Playback-Parameter an, die sich auf die Datenausgabe beziehen (Abbildung 4). Die Funktionstaste F4 schaltet in den Capture-Modus um, der die Mikrofoneinstellungen kontrolliert (Abbildung 5). Kommt ein eingestöpseltes externes Mikrofon zum Einsatz, muss der Eintrag in der Rubrik Input So auf ``Mic'' stehen. Reicht das qualitativ ausreichende eingebaute Netbook-Mikro, wird mit den Cursortasten im alsamixer ``Front Mic'' eingestellt. Der Regler ``Digital'' bestimmt die Empfindlichkeit. Mit der ``Esc''-Taste verlässt der User den alsamixer wieder, die eingestellten Änderungen bleiben permanent.

Abbildung 4: Playback-Modus nach dem Start des alsamixer.

Abbildung 5: Die Taste [F4] schaltet in den Capture-Modus um. Die linke "Input So" muss auf "Front Mic" stehen, um das eingebaute Mikrofon des Netbooks zu aktivieren. Der Regler "Digital" bestimmt die Empfindlichkeit.

Installation

Die Module POE, POE::Wheel::Run, Curses::UI::POE und Sysadm::Install liegen, samt ihrer Abhängigkeiten, teilweise schon neueren Linux-Distributionen bei oder lassen sich über eine CPAN-Shell installieren. Als besonderen Leserservice bedient auf [4] der Perlmeister in einem Lehrvideo höchstpersönlich das Skript auf einem kleinen Netbook und versucht sich daran, das Wort ``Linux Magazine'' auf Rückwärts-Englisch aufzunehmen, um es dann von flipit richtig herum abspielen zu lassen. Ein Riesenspaß für Groß und Klein!

Infos

[1]

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

[2]

``Work It'', Song von ``Missy Elliott'', http://en.wikipedia.org/wiki/Work_It_(Missy_Elliott_song)

[3]

``Backmasking'', die Technik, Songsequenzen rückwärts abzuspielen.

http://en.wikipedia.org/wiki/Backmasking

[4]

Michael Schilli demonstriert Aufnahmen mit dem Skript flipit: http://www.youtube.com/watch?v=LdSTIa2Tx4o

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.