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