// $Id: jukebox.cc 1907 2007-02-07 02:53:00Z flaterco $ /* Copyright (C) 2006 David Flater. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ // Play sound files (or whatever) in random order forever. It will // play every song in the playlist exactly once before repeating any. // State is preserved between runs. Under no circumstance will it // play a song twice in a row; however, whatever it was playing when // you last stopped it gets replayed from the beginning on the next // run. You must have at least two songs in your playlist. // Duplicates are suppressed. // // Prerequisites: // // You must have two shell scripts or executables in your path. // // jukebox_prep input-file temp-file // // Do whatever is needed to prepare input-file for playing. This // script is invoked before jukebox_play for a given input-file, // but concurrent with jukebox_play for the previous input-file. // The purpose is to give you a chance to do any slow preprocessing // that would otherwise incur a long pause between songs. If no // such preprocessing is needed, this script can do nothing. // // temp-file is a unique file name that can optionally be used to // store the input for jukebox_play. // // Example jukebox_prep: // // #!/bin/sh // sox -t wav "$1" -w -t wav -c 2 -r 48000 "$2" polyphase // normalize -q --clipping "$2" // // jukebox_play input-file temp-file // // Play input-file. This script is invoked after jukebox_prep for // a given file, with temp-file being the same as was given to the // corresponding jukebox_prep invocation. // // If temp-file was created by jukebox_prep, it should be deleted // by jukebox_play. // // Example jukebox_play: // // #!/bin/sh // echo Now playing: $1 // aplay-rt -q -Dhw:0,4 "$2" // rm "$2" // // Jukebox will not invoke jukebox_play for a given input-file until // its corresponding jukebox_prep has terminated. However, it is not // guaranteed that there will be no pauses in playback. There is // always a pause on the first song and after reshuffling the // playlist. // // Usage: // // To create the playlist, pipe the list of wav files (or whatever) to // standard input and use the -N flag: // // find /share/Music -name \*.wav -print | jukebox -N // // Henceforth to resume playing where it left off, just run jukebox // with no flags. // // State is stored in ~/.jukebox. // // g++ -O2 -Wall -Wextra -pedantic -s -o jukebox jukebox.cc -ldstr #include #include #include #include #include #include #include #include #include #include #include #define prepcmd "jukebox_prep" #define playcmd "jukebox_play" // Start command running in a subprocess. Return is the pid. // // argv[0] The command // argv[1]... Arguments // argv[n] The list of arguments MUST be terminated by a NULL pointer. // pid_t sew (char *const argv[]) { pid_t pid = fork(); switch (pid) { case -1: // failure perror ("fork"); exit (-1); case 0: // child process execvp (argv[0], argv); perror (argv[0]); exit (-1); } return pid; } // Wait on subprocess to finish and die if it failed in any way. void reap (pid_t pid) { int status; pid_t waitret = waitpid (pid, &status, 0); assert (waitret == pid); if (WIFSIGNALED(status)) { fprintf (stderr, "jukebox: script died from signal\n"); exit (-1); } if (!WIFEXITED(status)) { fprintf (stderr, "jukebox: script did not exit normally\n"); exit (-1); } if (WEXITSTATUS(status)) { fprintf (stderr, "jukebox: script returned exit status %d\n", WEXITSTATUS(status)); exit (-1); } } // Seed drand48 when needed. void BootRandom () { static bool seeded = false; if (!seeded) { seeded = true; long seedval; FILE *fp = fopen ("/dev/urandom", "r"); assert (fp); assert (fread (&seedval, sizeof(long), 1, fp) == 1); fclose (fp); srand48 (seedval); } } unsigned long RandFunc (unsigned long N) { assert (N > 0); unsigned long r = (unsigned long)(drand48() * N); assert (r < N); return r; } void shuffle (std::vector &playlist) { BootRandom(); printf ("\n************* Shuffling playlist *************\n\n"); std::random_shuffle (playlist.begin(), playlist.end(), RandFunc); } void reshuffle (std::vector &playlist) { Dstr lastback (playlist.back()); shuffle (playlist); if (playlist.front() == lastback) { playlist.front() = playlist.back(); playlist.back() = lastback; } } void usagebarf () { fprintf (stderr, "Usage: jukebox [-N]\n"); exit (-1); } void stupidbarf () { fprintf (stderr, "jukebox: Get a playlist!\n"); exit (-1); } char *statefilename () { static Dstr fname; if (fname.isNull()) { fname = getenv ("HOME"); assert (fname.length()); if (fname.back() != '/') fname += '/'; fname += ".jukebox"; } return fname.aschar(); } volatile sig_atomic_t caught_signal = 0; void not_a_convenient_time (int sig __attribute__ ((unused))) { caught_signal = 1; } void critical_enter () { signal (SIGINT, not_a_convenient_time); signal (SIGTERM, not_a_convenient_time); signal (SIGHUP, not_a_convenient_time); signal (SIGQUIT, not_a_convenient_time); } void critical_exit () { signal (SIGINT, SIG_DFL); signal (SIGTERM, SIG_DFL); signal (SIGHUP, SIG_DFL); signal (SIGQUIT, SIG_DFL); if (caught_signal) exit (0); } void badbad () { fprintf (stderr, "\n*** JUKEBOX CATASTROPHIC FAILURE ***\n"); fprintf (stderr, "UNABLE TO WRITE TO ~/.jukebox\n"); fprintf (stderr, "FILE IS NOW CORRUPT!\n"); fprintf (stderr, "Goodbye, cruel world! AAAAAGGGHH!!!\n"); exit (-1); } void save (std::vector &playlist) { critical_enter(); FILE *fp = fopen (statefilename(), "w"); if (!fp) { perror (statefilename()); exit (-1); } if (fprintf (fp, "%40u\n", 0) < 0) badbad(); for (unsigned i=0; i &playlist, unsigned &nowplaying) { FILE *fp = fopen (statefilename(), "r"); if (!fp) { perror (statefilename()); exit (-1); } Dstr buf; buf.getline (fp); assert (!buf.isNull()); assert (sscanf (buf.aschar(), "%u", &nowplaying) == 1); while (!buf.getline(fp).isNull()) playlist.push_back (buf); fclose (fp); assert (playlist.size() > 1); assert (nowplaying < playlist.size()); } void jtmpnam (unsigned nowplaying, Dstr &fname) { fname = "/tmp/jukebox"; fname += nowplaying; } enum Action {Prep, Play}; pid_t dofile (std::vector &playlist, unsigned nowplaying, Action act) { Dstr fname; jtmpnam (nowplaying, fname); char *args[4] = {((act == Prep) ? (char*)prepcmd : (char*)playcmd), playlist[nowplaying].aschar(), fname.aschar(), NULL}; return sew (args); } pid_t prep (std::vector &playlist, unsigned nowplaying) { return dofile (playlist, nowplaying, Prep); } pid_t play (std::vector &playlist, unsigned nowplaying) { return dofile (playlist, nowplaying, Play); } int main (int argc, char **argv) { if (argc > 2) usagebarf(); // Initialize playlist. if (argc == 2) { if (strcmp (argv[1], "-N")) usagebarf(); Dstr buf; std::set playset; while (!(buf.getline(stdin).isNull())) playset.insert (buf); if (playset.size() < 2) stupidbarf(); std::vector playlist; playlist.insert (playlist.end(), playset.begin(), playset.end()); shuffle (playlist); save (playlist); printf ("Playlist initialized.\n"); return 0; } std::vector playlist; unsigned nowplaying; load (playlist, nowplaying); bool noprep = true; while (true) { while (nowplaying < playlist.size()) { if (noprep) { reap (prep (playlist, nowplaying)); noprep = false; } bool notlast = (nowplaying+1 < playlist.size()); pid_t playpid = play (playlist, nowplaying); pid_t preppid = 0; if (notlast) preppid = prep (playlist, nowplaying+1); reap (playpid); ++nowplaying; if (notlast) { update (nowplaying); reap (preppid); } } reshuffle (playlist); save (playlist); nowplaying = 0; noprep = true; } }