Playr – Ein iPod Shuffle für die Bash
Januar 21, 2013 um 10:05 vormittags | Veröffentlicht in Bash, Multimedia, Programmieren, Ubuntuusers | 9 KommentareSchon länger habe ich nach einer Möglichkeit gesucht ein paar Musikdateien auszuwählen und in zufälliger Reihenfolge abspielen zu lassen. Das kann sowohl ein einzelnes Album sein, als auch eine Zusammenstellung mehrere Tracks aus verschiedenen Verzeichnissen. Außerdem sollte dieses Programm Konsolen-basiert sein, da ich es für schnelle Wiedergabe brauche und nicht zuerst in einer GUI alles zusammenklicken will. Da ich ein solches Programm leider noch nicht gefunden habe, habe ich mir selbst ein Bash-Skript zusammengestellt, welches als Scheduler dient und play (Paket sox aus den Quellen) zum Abspielen verwendet. play kann sowohl OGG, FLAC und WAV abspielen. Nach Installation von libsox-fmt-mp3 auch MP3.
Playr
Playr steht für die Zusammensetzung aus play (dem Programm fürs Abspielen der einzelnen Tracks) und random (engl.: zufällig). Dass es aussieht wie ein Web2.0-Name ist wiederum Zufall (dt.: coincidence).
Die grobe Funktionsweise: Die übergebenen Musiktracks werden in eine versteckte Textdatei geschrieben. Dann wird mit Hilfe von $RANDOM eine Zufallszahl zur Berechnung des nächsten Tracks verwendet. Dieser Track wird sodann abgespielt und aus der Textdatei entfernt. Das verhindert, dass dieser erneut abgespielt wird (was etwas ist, dass ich bei portablen Audio-Playern sehr nervig finde). Solange noch Tracks in der Liste sind wird fortgefahren.
Soll die Wiedergabe abgebrochen werden, muss der Nutzer nur [strg]+[c] drücken. Leider habe ich bis jetzt keine Möglichkeit gefunden zusätzlich dazu auch einfach zum nächsten Track zu wechseln. Man kann natürlich einfach die trap entfernen, dann bricht [strg]+[c] nur das aktuelle play ab. Dann kann jedoch das Skript selbst nicht mehr komfortabel beendet werden.
Das Skript
Wir beginnen mit der trap. Dieses Konstrukt sorgt dafür, dass unser Skript [strg]+[c] verarbeiten kann. Dafür schreiben wir folgende Zeile:
trap quit SIGINT SIGTERM
Diese Zeile führt dazu, dass beim Erhalt der Signale SIGINT (Signal 2) oder SIGTERM (Signal 15) quit ausgeführt wird. Nun ist quit kein Bash-Befehl sondern eine Funktion, die wir uns selbst schreiben, welche das Skript geordnet beendet:
function quit {
rm -f $TRACK_FILE $TEMP_FILE
exit 1
}
Diese Funktion schreiben wir vor die Trap. In dieser löschen wir einfach unsere Textdateien, damit sie beim nächsten Programmaufruf nicht stören.
Als nächstes definieren wir zwei Variablen für unsere versteckten Textdateien. Zum einen eine Datei um die Tracklist zu speichern, zum Anderen eine Datei in der die veränderte Liste gespeichert wird:
TRACK_FILE=/tmp/.tracklist.playr
TEMP_FILE=/tmp/.temp.playr
Wir speichern die Dateien in /tmp , damit sie keine Verzeichnisse zumüllen. Außerdem sollten wir in /tmp immer Schreibzugriff haben. Sollte /tmp nicht verfügbar sein, kann man das Verzeichnis ja auf $HOME ändern.
Nun wirds Zeit sicherheitshalber Überbleibsel einer vorherigen Ausführung zu entfernen:
rm -f $TRACK_FILE $TEMP_FILE
Nur für den Fall, dass etwas total schief gelaufen ist.
Um die Tracklist zu erzeugen schreiben wir nun einfach alle Parameter, die beim Skriptaufruf übergeben wurde in das TRACK_FILE:
for file in " $@ "; do
echo " $file "
done > $TRACK_FILE
Das funktioniert gewisserweise wie eine foreach-Schleife. Eine beliebige Anzahl Parameter wird durchlaufen (“ $@ “). Der jeweils aktuelle Parameter wird in $file gespeichert und in der Schleife verarbeitet. Hier wird er einfach an die Standardausgabe geschickt. Die komplette Ausgabe der Schleife wird wieder auf $TRACK_FILE umgebogen, wodurch die Ausgabe nicht auf der Kommandozeile erscheint, sondern in der angegebenen Datei.
Die Größe der Tracklist holen wir uns mit wc:
size=$( cat "$TRACK_FILE" | wc -l)
Natürlich könnte man die Datei auch gleich als Parameter für wc angeben:
wc -l " $TRACK_FILE "
Dann jedoch erhält man eine Ausgabe nach folgendem Muster:
6 .tracklist.playr
Man muss also erst noch den eigentlichen Wert extrahieren. Da mache ich lieber den “Umweg” über cat. Ist kürzer.
Nun kommt die eigentliche Hauptschleife des Programmes:
while [[ $size -gt 0 ]]; do
## Code
((size–))
done
Diese Schleife läuft solange durch, bis $size den Wert 0 hat. Da $size auf jeden Fall größer als 0 sein muss und in jedem Durchgang um eins verringert wird, ist das irgendwann der Fall. Mit der Abbruchbedingung verhindern wir auch, dass die Schleife ausgeführt wird, wenn die Tracklist leer ist.
Es ist Zeit. Zeit um die Urväter, die Auditoren, die Götter anzurufen. Zeit eine Zufallszahl zu erzeugen. Einer Zufallszahl innerhalb eines Intervalls errechnet man am Besten mithilfe von Modulo. Modulo ist der Name der in der Unterstufe als Restwertdivision bekannten Berechnung.
Kleine Beispiele: 10 / 3 = 3 . Rest: 1 ; 13 / 5 = 2 . Rest: 3 . Uns interessiert immer der Restwert. Dieser ist auf jeden Fall immer um 1 kleiner als der Divisor (für diejenigen, die schon lange aus der Schule draußen sind
).
Bash scheint keine built-in Funktion für Modulo zu haben, dafür gibt es ein paar andere Möglichkeiten eine solche Berechnung durchzuführen. Am ansprechendsten habe ich let gefunden:
let "rand = $RANDOM % $size + 1"
In der Variable $rand wird das Ergebnis der Berechnung festgehalten. $size ist natürlich der Begrenzer. $RANDOM ist eine Umgebungsvariable, welche bei jedem Aufruf einen neuen zufälligen Integer (Ganzzahl) ausgibt. Da diese Berechnung bei einer Tracklist-Größe von z.B. 6 den Wertebereich von 0-5 abdeckt, wir aber kein 0 tes Lied, dafür aber ein 6 tes, zählen wir zum Ergebnis einfach 1 dazu.
Den damit errechneten Track erhalten so:
track= " $( head $TRACK_FILE -n$rand | tail -n1) “
Hier passiert ein bisschen was:
head -n$rand
filtert die ersten $rand Zeilen aus der Liste. Ist $rand also 2 , dann erhalten wir durch head die ersten 2 Zeilen. Dadurch ist der gesuchte Track immer an letzter Stelle in der Liste. Diese letzte Stelle können wir uns nun per tail holen:
tail -n1
Hier holt uns tail die 1 ste Zeile von hinten. Damit haben wird unseren Track. Dieser wird nun in die Variable $track gespeichert.
Jetzt wird es Zeit den Track abzuspielen:
play " $track "
Natürlich kann man auch das Ausrechnen des Tracks und den Aufruf von play in einer Zeile unterbringen. Ich bin jedoch eher für lesbaren Code als für “Zeileneffizienz”.
Zum Abschluss muss noch eine neue Tracklist angelegt werden, ohne den gerade abgespielten Track. Dazu holen wir uns zuerst alle Tracks vor dem Aktuellen:
head " $TRACK_FILE " -n$[ $rand-1 ] > " $TEMP_FILE "
und die Tracks danach:
tail " $TRACK_FILE " -n$[ $size-$rand ] >> " $TEMP_FILE "
Mit
$rand-1
holen wir uns alle Tracks vor dem Aktuellen und sparen diesen aus. Hingegen
$size-$rand
liefert uns alle Tracks nach dem Aktuellen. Damit erhalten wir wieder dieselbe Liste wie zuvor, nur ohne dem letzten Track. Man beachte die Pfeile, welche das Ergebnis jeweils nach $TEMP_FILE schicken. In der ersten Zeile befindet sich nur ein Pfeil. Das bedeutet, dass die Datei überschrieben und neu befüllt wird. Es wird also nur das gespeichert, was in dieser Zeile herauskommt. In der zweiten Zeile hingegen sind zwei Pfeile. Das bedeutet, das das Ergebnis an die Datei angehängt wird. Der Inhalt der Datei wird also nicht über-, sondern der neue Inhalt dahinter in die Datei geschrieben.
Schließlich müssen wir die neue Tracklist noch über die alte schreiben:
mv " $TEMP_FILE " " $TRACK_FILE "
Nach der Schleife selbst rufen wir noch einmal quit auf, damit das Skript sich auch ohne Abbruch durch den Nutzer sauber beendet.
Das vollständige Skript
#!/bin/bash
function quit {
rm -f $TRACK_FILE $TEMP_FILE
exit 1
}
trap quit SIGINT SIGTERM
TRACK_FILE=/tmp/.tracklist.playr
TEMP_FILE=/tmp/.temp.playr
rm -f $TRACK_FILE $TEMP_FILE
for file in “ $@ “; do
echo “ $file “
done > $TRACK_FILE
size=$( cat ”$TRACK_FILE” | wc -l)
while [[ $size -gt 0 ]]; do
let “rand = $RANDOM % $size + 1″
track= " $( head $TRACK_FILE -n$rand | tail -n1) “
play “ $track “
head “ $TRACK_FILE “ -n$[ $rand-1 ] > “ $TEMP_FILE “
tail “ $TRACK_FILE “ -n$[ $size-$rand ] >> “ $TEMP_FILE “
mv “ $TEMP_FILE ” “ $TRACK_FILE “
((size–))
done
quit
Zum Schluss
Ich bin vergleichsweise ein Anfänger in Shell-Script. Ich habe einige Erfahrung in C++ und es kann daher sein, dass ich versuche Konzepte daraus in Bash umzusetzen, obwohl es wesentlich einfachere Lösungen gibt. Wenn du eine bessere Lösung weißt, schreibe sie doch bitte in die Kommentare.
Lizenz
Das Skript steht unter der GPLv3.
9 Kommentare »
RSS-Feed für Kommentare zu diesem Artikel. TrackBack URI
Kommentar verfassen
Bloggen Sie auf WordPress.com. | Theme: Pool von Borja Fernandez.
Einträge und Kommentare feeds.

tolle lösung, hätte hier eine schnelle und einfachere variante:
#!/bin/bash
IFS=’
‘
play `for i in “$@”; do echo “$i”; done | sort -R`
Comment by bashscript— Januar 21, 2013 #
O.k. Als Antwort darauf noch meine Variante mit trap. So funktioniert sie genau wie das Original:
#!/bin/bash
shuf -e “$@” |
while read TRACK; do
trap “exit” SIGINT
play “$TRACK”
done
Comment by goppo— Januar 21, 2013 #
Nett. Einzeiler sind cool.
Comment by tok1hama1san— Januar 28, 2013 #
Hier eine etwas simplere Variante, ohne trap:
shuf -e “$@” | while read TRACK; do play “$TRACK”; done
Comment by goppo— Januar 21, 2013 #
shuffkannte ich noch nicht. Interessant.Comment by tok1hama1san— Januar 28, 2013 #
Hab mal ein paar Verbesserungen zu dem Skript gemacht und das Ganze als gist eingetragen: https://gist.github.com/4588109
Noch ein paar Anmerkungen:
1. Bitte entferne das mit dem cat — google einfach mal nach “useless use of cat” — https://en.wikipedia.org/wiki/Useless_use_of_cat#Useless_use_of_cat
2. Variablen IMMER in Anführungszeichen setzen — sonst vergisst man das und das ist ganz schlecht
3. Für Mathematik-Sachen am besten immer (( )) benutzen, da kann die bash z.B. auch Modulo: (( rand = RANDOM % size + 1 )) — man muss die Variablen
Wenn du vorhast mehr für bash zu programmieren empfehle ich dir http://mywiki.wooledge.org/
Im zweiten Absatz gibt es die Links zur BashFAQ, BashGuide, … das ist m.M. mit das beste, was man im Internet zur Bash findet, wenn man nämlich einfach nach Sachen googelt, findet man zu viele falsche Beispiele — die Wiki-Seite ist aber echt gut.
Comment by Jonas— Januar 21, 2013 #
Es stimmt, die meisten Programme können direkt aus Dateien lesen oder man kann den Input per Stream einleiten.
Irgendwie kam mir
catimmer sehr intuitiv vor um einen Textstrom zu beginnen. Ich werde es in Zukunft ohne probieren.Mit (( )) hatte ich es probiert, aber leider eine Fehlermeldung bekommen. Vielleicht war es aber auch ein anderer Fehler.
Danke.
Comment by tok1hama1san— Januar 28, 2013 #
Oder man guckt sich mal cmus an. Ein wirklich hervorragender Player fürs Terminal.
http://wiki.ubuntuusers.de/cmus
Comment by dAnjou— Januar 23, 2013 #
Auch eine Möglichkeit.
Comment by tok1hama1san— Januar 28, 2013 #