Aus Linux-Magazin 03/2010

Perl-Skript lässt Digitalrekorder rückwärts laufen

© Pascal Willuhn, Pixelio.de

Der Titel des Beatles-Songs “Revolution 9” soll nach einer Verschwörungstheorie rückwärts gespielt einen Hinweis offenbaren, dass das Management die Band gezwungen habe, den toten Paul McCartney durch einen Doppelgänger zu ersetzen. Perl macht klar: alles Schwindel !

Neulich lauschte ich im Internetradio dem Song “Work It” von Missy Elliot und stellte fest, dass ich Teile des Refrains trotz ordentlicher Englischkenntnisse seit seinem Erscheinen im Jahre 2002 noch nie recht verstanden hatte. Wikipedia löste das Rätsel: Die feiste Hip-Hopperin spielt einfach Gesangssequenzen rückwärts ein [2].

Wie die Alten sungen

Dies so genannte Backmasking [3] war schon in meiner Jugendzeit 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 Kassettenrekorder 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 unvorstellbarem Akzent, irgendwo zwischen osteuropäisch und außerirdisch: “Kkkassettnrekorrdeeer!” Stundenlanger Spaß für die ganze Familie!

Das Skript in Listing 1 produziert nach dem Aufruf in einem Terminal 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 verkehrt herum abspielt. Während der Wiedergabe erscheint der Text in Abbildung 3, der sofort wieder dem Menü in Abbildung 1 Platz macht, sobald keine Sounddaten mehr vorliegen.

Abbildung 1: Zum Programmbeginn: Die Taste [R] startet eine Aufnahme.

Abbildung 1: Zum Programmbeginn: Die Taste [R] startet eine Aufnahme.

Abbildung 2: Während der Aufnahme: [S] beendet die Aufnahme, [P] spielt sie gleich rückwärts ab.

Abbildung 2: Während der Aufnahme: [S] beendet die Aufnahme, [P] spielt sie gleich rückwärts ab.

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

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

Die minimalistische grafische 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 das GUI blitzschnell auf User-Eingaben reagieren.

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 }

Curses tanzt mit POE

Erfahrene Leser dieser Rubrik wissen, dass die kooperative Multitasking-Umgebung POE dafür die nötigen Voraussetzungen mitbringt. Sie verfolgt einen asynchronen Ansatz, der Anfängern oft Kopfzerbrechen bereitet. Wer aber den Bogen raus hat, kann mit ihr schnell robuste Applikationen bauen.

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. Zeile 16 nutzt ihn nur dazu, eine Referenz auf die ebenfalls POE-typische Datenstruktur eines Session-Heap 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!

Das 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 zu Curses::UI::Container durch. Der erste Parameter ist die für das Window gewünschte ID, der zweite Parameter – »”Window”« – bestimmt die Klasse des neu erzeugten Widget.

Das zweite Widget, die Fußzeile mit den Instruktionen für gerade erlaubte User-Aktionen, entsteht in Zeile 23. GUI-typisch erzeugt das Parent-Widget »$WIN« durch einen Aufruf der Methode »add()« das in ihm liegende Fußzeilen-Widget. 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-Kommando auf.

Bereit zum Loslaufen

Die Zeilen 29 bis 32 definieren, was passiert, wenn der User die Tasten [Q] (Quit), [P] (Play), [R] (Record) oder [S] (Stop Recording) drückt. Bei [Q] ruft »flipit« die Funktion »exit()« auf, die das Programm abbricht.

Die Handlerfunktionen »play_flipped()« , »record()« und »record_stop()« 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 Haupt-Eventschleife, aus der es nie mehr austritt und so lange Usereingaben verarbeitet, bis jemand [Q] drückt. Betätigt der User [R], springt das 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. Falls nicht, legt die Funktion »tempfile()« aus dem Modul File::Temp eine temporäre Datei mit der Endung ».ogg« an, die Perls automatische Abrissbirne dank der gesetzten »UNLINK«-Option bei Programmende wegfegt.

Audiotool Sox

Das Suffix ist wichtig, denn das nachfolgend aufgerufene Tool »sox« schließt daraus auf das in der Audiodatei zu verwendende Kodierungsverfahren. Externe Programme startet POE durch ein Objekt der Klasse POE::Wheel::Run, das das GUI ruckellos weiterlaufen lässt und bei Bedarf sogar Aktionen auslöst, wenn das Programmkind sich verabschiedet. Das Wheel in der Zeile 47 ignoriert allerdings jene Events, die bei Ausgaben nach »Stderr« auftreten (»StderrEvent«). Nur der User kann den Lauf des Rekorders beenden.

Das aufgerufene Programm »rec« liegt dem »sox«-Paket bereits bei (genau wie das später genutzte Utility »play«) und nimmt nur den Namen der zu erstellenden Audiodatei entgegen. Es liest Audiodaten über ein eingebautes oder externes Mikrofon. Zu beachten ist, dass POE::Wheel::Run das aufzurufende Programm und dessen Parameterliste getrennt in den Parametern »Program« und »ProgramArgs« erwartet.

Der Code ab Zeile 46 hält das 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] den Abspielvorgang einleiten kann.

Stopp!

Nach einem Tastendruck auf [S] kommt die Funktion »record_stop()« an die Reihe, sie prüft zunächst, ob überhaupt ein Wheel läuft. Falls nicht, hat wohl ein Übermütiger [S] gedrückt, ohne dass überhaupt eine Aufnahme lief. Zeile 70 schießt das laufende Aufnahmeprogramm dann mit dem über das Posix-Modul hereingeholte »SIGTERM«-Signal ab, im Anschluss streicht Zeile 71 die Referenz des soeben (hoffentlich) ruckartig beendeten Wheel aus dem Heap.

Die Funktion »footer_update()« frischt mit der »text()«-Methode die Fußzeile auf und schickt ein »draw()« hinterher, um das GUI zu veranlassen, 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. Dies definiert einen »CloseEvent«, der 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 gewissermaßen parallel starten.

Die Funktion »sox_play()« legt eine weitere temporäre, zunächst leere Ogg-Datei an und übergibt sie dem Utility »sox«:

sox input.ogg output.ogg reverse

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

Micro aktivieren

Damit das richtige Mikrofon die eintrudelnden Audiodaten aufnimmt, war es notwendig, das 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. Wem ein eingebautes Netbook-Mikro reicht, der stellt mit den Cursortasten im Alsamixer »Front Mic« ein. Der Regler »Digital« bestimmt die Empfindlichkeit. Mit der [Esc]-Taste verlässt der User den Alsamixer wieder.

Abbildung 4: Der Playback-Modus nach dem Start von Alsamixer. Über die verschiedenen Aussteuerungsanzeigen lassen sich die Pegel für die einzelnen Kanäle regulieren.

Abbildung 4: Der Playback-Modus nach dem Start von Alsamixer. Über die verschiedenen Aussteuerungsanzeigen lassen sich die Pegel für die einzelnen Kanäle regulieren.

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

Abbildung 5: Die Taste [F4] schaltet in den Capture-Modus um. Die linke Rubrik »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 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 und versucht sich daran, das Wort “Linux Magazine” auf Rückwärts-Englisch aufzunehmen und abzuspielen. (jcb)

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 das Skript: [http://www.youtube.com/watch?v=LdSTIa2Tx4o]

Der Autor

Michael Schilli arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. Er hat die Bücher “Goto Perl 5” (deutsch) und “Perl Power” (englisch) für Addison-Wesley geschrieben und ist unter [mschilli@perlmeister.com] zu erreichen.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben