/** * ecaplay.c: A simple command-line tool for playing audio files. * * Copyright (C) 1999-2002,2004-2006 Kai Vehmanen * * 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. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /** * TODO: * - show playlist length during runtime * - random start switch (both for cmdline and playlist modes) * - write some notes about locking issues */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include /* ANSI-C: malloc(), free() */ #include /* ANSI-C: strlen(), strncmp() */ #include /* POSIX: ... */ #include /* POSIX: sigaction() */ #include /* POSIX: mkdir() */ #include /* POSIX: mkdir() */ #include /** * Function declarations */ int main(int argc, char *argv[]); static void add_input_to_chainsetup(eci_handle_t eci, const char* nextrack); static int flush_tracks(void); static const char* get_next_track(int *tracknum, int argc, char *argv[], eci_handle_t *eci); static char* get_playlist_path(void); static const char* get_track_cmdline(int n, int argc, char *argv[]); static const char* get_track_playlist(int* next_track); static void initialize_chainsetup_for_playback(eci_handle_t* eci, const char* nexttrack, int tracknum); static void initialize_check_output(eci_handle_t* eci); static int list_tracks(void); static int play_tracks(int argc, char *argv[]); static void print_usage(FILE* stream); static int process_option(const char* option); static int queue_tracks(int argc, char *argv[]); static int set_audio_format(eci_handle_t* eci, const char* fmt); static void setup_signal_handling(void); static void signal_handler(int signum); /** * Definitions and options */ #define ECAPLAY_AFMT_MAXLEN 64 #define ECAPLAY_EIAM_LOGLEVEL 256 #define ECAPLAY_TIMEOUT 3 #define ECAPLAY_MODE_NORMAL 0 #define ECAPLAY_MODE_PL_FLUSH 1 #define ECAPLAY_MODE_PL_LIST 2 #define ECAPLAY_MODE_PL_PLAY 3 #define ECAPLAY_MODE_PL_QUEUE 4 #define ECAPLAY_PLAYLIST_BASEDIR ".ecasound" #define ECAPLAY_PLAYLIST_FILE "ecaplay_queue" /** * Global variables */ static const char* ecaplay_version = "20061206-45"; /* ecaplay version */ static char ecaplay_next[PATH_MAX]; /* file to play next */ static char ecaplay_audio_format[ECAPLAY_AFMT_MAXLEN]; /* audio format to use */ static const char* ecaplay_output = NULL; /* output device to use */ static int ecaplay_debuglevel = ECAPLAY_EIAM_LOGLEVEL; /* debug level to use */ static int ecaplay_skip = 0; /* how many playlist items to skip */ static int ecaplay_mode = ECAPLAY_MODE_NORMAL; /* playlist mode */ /* FIX: static int ecaplay_list_len = -1; playlist length */ static int ecaplay_initialized = 0; /* playlist mode */ static sig_atomic_t ecaplay_skip_flag = 0; /* signal flag for ctrl-c */ /** * Function definitions */ int main(int argc, char *argv[]) { int i, res = 0; /* get the default output device */ ecaplay_output = getenv("ECAPLAY_OUTPUT_DEVICE"); /* process command-line arguments */ for(i = 1; i < argc; i++) { res += process_option(argv[i]); } if (res == 0) { switch(ecaplay_mode) { case ECAPLAY_MODE_PL_FLUSH: res = flush_tracks(); break; case ECAPLAY_MODE_PL_LIST: res = list_tracks(); break; case ECAPLAY_MODE_PL_QUEUE: res = queue_tracks(argc, argv); break; case ECAPLAY_MODE_NORMAL: case ECAPLAY_MODE_PL_PLAY: res = play_tracks(argc, argv); break; default: assert(0); } } if (res != 0) { fprintf(stderr, "(ecaplay) Errors encountered, return code is %d.\n", res); } return res; } /** * Adds input 'nexttrack' to currently selected chainsetup * of 'eci'. Sets the global variable 'ecaplay_audio_format'. */ static void add_input_to_chainsetup(eci_handle_t eci, const char* nexttrack) { size_t len = strlen("ai-add '") + strlen(nexttrack) + strlen("'") + 1; char* tmpbuf = malloc(len); assert(tmpbuf != NULL); snprintf(tmpbuf, len, "ai-add \"%s\"", nexttrack); eci_command_r(eci, tmpbuf); /* check that add succeeded */ eci_command_r(eci, "ai-list"); if (eci_last_string_list_count_r(eci) != 1) { fprintf(stderr, "(ecaplay) Warning! Failed to add input '%s'.\n", nexttrack); } /* we must connect to get correct input format */ eci_command_r(eci, "ao-add null"); eci_command_r(eci, "cs-connect"); eci_command_r(eci, "ai-iselect 1"); eci_command_r(eci, "ai-get-format"); strncpy(ecaplay_audio_format, eci_last_string_r(eci), ECAPLAY_AFMT_MAXLEN); ecaplay_audio_format[ECAPLAY_AFMT_MAXLEN - 1] = 0; /* disconnect and remove the null output */ eci_command_r(eci, "cs-disconnect"); eci_command_r(eci, "ao-iselect 1"); eci_command_r(eci, "ao-remove"); free(tmpbuf); } /** * Flushes the playlist contents. * * @return zero on success, non-zero otherwise */ static int flush_tracks(void) { char *path = get_playlist_path(); if (truncate(path, 0) != 0) { printf("(ecaplay) Unable to flush playlist '%s'.\n", path); return -1; } return 0; } /** * Checks that current chainsetup has exactly one * output. */ static void initialize_check_output(eci_handle_t* eci) { eci_command_r(eci, "ao-list"); if (eci_last_string_list_count_r(eci) != 1) { fprintf(stderr, "(ecaplay) Warning! Failed to add output device.\n"); } else { static int once = 1; if (once) { eci_command_r(eci, "ao-iselect 1"); eci_command_r(eci, "ao-describe"); char *tmpstr = (char*)eci_last_string_r(eci); /* skip the "-x:" prefix where x is one of [io] */ while(*tmpstr && *tmpstr++ != ':') ; printf("(ecaplay) Output device: '%s'\n", tmpstr); once = 0; } } } static void initialize_chainsetup_for_playback(eci_handle_t* eci, const char* nexttrack, int tracknum) { const char* ret = NULL; *eci = eci_init_r(); ecaplay_initialized = 1; if (ecaplay_debuglevel != -1) { char tmpbuf[32]; snprintf(tmpbuf, 32, "debug %d", ecaplay_debuglevel); eci_command_r(*eci, tmpbuf); } eci_command_r(*eci, "cs-add ecaplay_chainsetup"); /* check that add succeeded */ eci_command_r(*eci, "cs-list"); if (eci_last_string_list_count_r(*eci) != 2) { fprintf(stderr, "(ecaplay) Warning! Failed to add a new chainsetup.\n"); } /* as this is a new chainsetup, we can assume that * adding chains succeeds */ eci_command_r(*eci, "c-add ecaplay_chain"); add_input_to_chainsetup(*eci, nexttrack); set_audio_format(*eci, ecaplay_audio_format); if (ecaplay_output == NULL) { eci_command_r(*eci, "ao-add-default"); /* check that add succeeded */ initialize_check_output(*eci); } else { int len = strlen("ao-add ") + strlen(ecaplay_output) + 1; char* tmpbuf = (char*)malloc(len); snprintf(tmpbuf, len, "ao-add %s", ecaplay_output); eci_command_r(*eci, tmpbuf); initialize_check_output(*eci); free(tmpbuf); } /* FIXME: add detection of consecutive errors */ eci_command_r(*eci, "cs-connect"); if (eci_error_r(*eci)) { fprintf(stderr, "(ecaplay) Unable to play file '%s':\n%s\n", nexttrack, eci_last_error_r(*eci)); } else { eci_command_r(*eci, "cs-connected"); ret = eci_last_string_r(*eci); if (strncmp(ret, "ecaplay_chainsetup", strlen("ecaplay_chainsetup")) != 0) { fprintf(stderr, "(ecaplay) Error while playing file '%s' . Skipping...\n", nexttrack); } else { /* note: audio format set separately for each input file */ printf("(ecaplay) Playing %d: '%s' (%s).\n", tracknum, nexttrack, ecaplay_audio_format); eci_command_r(*eci, "start"); } } } static const char* get_next_track(int *tracknum, int argc, char *argv[], eci_handle_t *eci) { const char *nexttrack = NULL; if (ecaplay_mode == ECAPLAY_MODE_PL_PLAY) nexttrack = get_track_playlist(tracknum); else nexttrack = get_track_cmdline(*tracknum, argc, argv); if (nexttrack != NULL) { /* queue nexttrack for playing */ if (ecaplay_initialized) { eci_cleanup_r(*eci); } initialize_chainsetup_for_playback(eci, nexttrack, *tracknum); } else { /* reached end of playlist */ if (ecaplay_mode != ECAPLAY_MODE_PL_PLAY) { /* normal mode; end processing after all files played */ /* printf("(ecaplay) No more files...\n"); */ assert(nexttrack == NULL); } else { /* if in playlist mode, loop from beginning */ *tracknum = 1; /* FIXME: if in playlist mode; query the current lenght of * playlist and set 'tracknum = (tracknum % pllen)' */ if (ecaplay_mode == ECAPLAY_MODE_PL_PLAY) nexttrack = get_track_playlist(tracknum); else nexttrack = get_track_cmdline(*tracknum, argc, argv); /* printf("(ecaplay) Looping back to start of playlist...(%s)\n", nexttrack); */ if (nexttrack != NULL) { /* queue nexttrack for playing */ if (ecaplay_initialized) { eci_cleanup_r(*eci); } initialize_chainsetup_for_playback(eci, nexttrack, *tracknum); } else { /* get_next_track() failed two times, stopping processing */ assert(nexttrack == NULL); } } } return nexttrack; } /** * Returns the track number 'n' from the list * given in argc and argv. * * @return track name or NULL on error */ static const char* get_track_cmdline(int n, int argc, char *argv[]) { int i, c = 0; assert(n > 0 && n <= argc); for(i = 1; i < argc; i++) { /* FIXME: add support for '-- -foo.wav' */ if (argv[i][0] != '-') { if (++c == n) { return argv[i]; } } } return NULL; } /** * Returns a string containing the full path to the * playlist file. Ownership of the string is transfered * to the caller (i.e. it must be free()'ed). * * @return full pathname or NULL if error has occured */ static char* get_playlist_path(void) { char *path = malloc(PATH_MAX); struct stat statbuf; /* create pathname based on HOME */ strncpy(path, getenv("HOME"), PATH_MAX); strncat(path, "/" ECAPLAY_PLAYLIST_BASEDIR, PATH_MAX - strlen(path) - 1); /* make sure basedir exists */ if (stat(path, &statbuf) != 0) { printf("(ecaplay) Creating directory %s.\n", path); mkdir(path, 0700); } else { if (!S_ISDIR(statbuf.st_mode)) { /* error, basedir exists but is not a directory */ free(path); path = NULL; } } if (path != NULL) { /* add filename to basedir */ strncat(path, "/" ECAPLAY_PLAYLIST_FILE, PATH_MAX - strlen(path) - 1); } return path; } /** * Returns the track from playlist matching number 'next_track'. * * In case 'next_track' is larger than the playlist length, * track 'next_track mod playlist_len' will be selected, and * the modified playlist item number stored to 'next_track'. * * Note: modifies global variable 'ecaplay_next'. * * @return track name or NULL on error */ static const char* get_track_playlist(int* next_track) { const char *res = NULL; char *path; FILE *f1; int next = *next_track; assert(next > 0); path = get_playlist_path(); if (path == NULL) { return path; } f1 = fopen(path, "rb"); if (f1 != NULL) { int c, w, cur_item = 1; /* iterate through all data octet at a time */ for(w = 0;;) { c = fgetc(f1); if (c == EOF) { if (next > cur_item) { /* next_track beyond playlist length, reset to valid track number */ next = next % cur_item; *next_track = next; /* seek back to start and look again */ fseek(f1, 0, SEEK_SET); cur_item = 1; w = 0; continue; } break; } if (cur_item == next) { if (c == '\n') { ecaplay_next[w] = 0; res = ecaplay_next; break; } else { ecaplay_next[w] = c; } ++w; } if (c == '\n') { ++cur_item; } } /* close the file and return results */ fclose(f1); } free(path); return res; } /** * Lists tracks on the playlist. * * @return zero on success, non-zero otherwise */ static int list_tracks(void) { FILE *f1; char *path = get_playlist_path(); f1 = fopen(path, "rb"); if (f1 != NULL) { int c; while((c = fgetc(f1)) != EOF) { printf("%c", c); } fclose(f1); return 0; } return -1; } /** * Play tracks using the Ecasound engine via the * ECI interface. * * Depending on the mode, tracks are selected either * from the command-line or from the playlist. */ static int play_tracks(int argc, char *argv[]) { eci_handle_t eci = NULL; int tracknum = 1, stop = 0; const char* nexttrack = NULL; assert(ecaplay_mode == ECAPLAY_MODE_NORMAL || ecaplay_mode == ECAPLAY_MODE_PL_PLAY); tracknum += ecaplay_skip; nexttrack = get_next_track(&tracknum, argc, argv, &eci); if (nexttrack != NULL) { setup_signal_handling(); while(nexttrack != NULL) { unsigned int timeleft = ECAPLAY_TIMEOUT; while(timeleft > 0) { timeleft = sleep(timeleft); if (timeleft > 0 && ecaplay_skip_flag > 1) { fprintf(stderr, "\n(ecaplay) Interrupted, exiting...\n"); eci_cleanup_r(eci); stop = 1; break; } } /* see above while() loop */ if (stop) break; if (ecaplay_skip_flag == 0) { eci_command_r(eci, "engine-status"); } else { printf("(ecaplay) Skipping...\n"); } if (ecaplay_skip_flag != 0 || strcmp(eci_last_string_r(eci), "running") != 0) { ecaplay_skip_flag = 0; ++tracknum; nexttrack = get_next_track(&tracknum, argc, argv, &eci); /* printf("Next track is %s.\n", nexttrack); */ } } fprintf(stderr, "exiting...\n"); /* see while() loop above */ if (stop == 0) { eci_cleanup_r(eci); } } return 0; } static void print_usage(FILE* stream) { fprintf(stream, "Ecaplay v%s (%s)\n\n", ecaplay_version, VERSION); fprintf(stream, "Copyright (C) 1997-2005 Kai Vehmanen, released under GPL licence \n"); fprintf(stream, "Ecaplay comes with ABSOLUTELY NO WARRANTY.\n"); fprintf(stream, "You may redistribute copies of ecasound under the terms of the GNU\n"); fprintf(stream, "General Public License. For more information about these matters, see\n"); fprintf(stream, "the file named COPYING.\n"); fprintf(stream, "\nUSAGE: ecaplay [-dfhklopq] [ file1 file2 ... fileN ]\n\n"); fprintf(stream, "See ecaplay(1) man page for more details.\n"); } static int process_option(const char* option) { if (option[0] == '-') { if (strncmp("--help", option, sizeof("--help")) == 0 || strncmp("--version", option, sizeof("--version")) == 0) { print_usage(stdout); return 0; } switch(option[1]) { case 'd': { const char* level = &option[3]; if (option[2] != 0 && option[3] != 0) { ecaplay_debuglevel |= atoi(level); printf("(ecaplay) Setting log level to %d.\n", ecaplay_debuglevel); } break; } case 'f': { ecaplay_mode = ECAPLAY_MODE_PL_FLUSH; printf("(ecaplay) Flushing playlist.\n"); break; } case 'h': { print_usage(stdout); return 0; } case 'k': { const char* skip = &option[3]; if (option[2] != 0 && option[3] != 0) { ecaplay_skip = atoi(skip); printf("(ecaplay) Skipping the first %d files..\n", ecaplay_skip); } break; } case 'l': { ecaplay_mode = ECAPLAY_MODE_PL_LIST; /* printf("(ecaplay) Listing playlist contents.\n"); */ break; } case 'o': { const char* output = &option[3]; if (option[2] != 0 && option[3] != 0) { ecaplay_output = output; /* printf("(ecaplay) Output device: '%s'\n", ecaplay_output); */ } break; } case 'p': { ecaplay_mode = ECAPLAY_MODE_PL_PLAY; printf("(ecaplay) Playlist mode selected (file: %s).\n", "~/" ECAPLAY_PLAYLIST_BASEDIR "/" ECAPLAY_PLAYLIST_FILE); break; } case 'q': { ecaplay_mode = ECAPLAY_MODE_PL_QUEUE; printf("(ecaplay) Queuing tracks to playlist.\n"); break; } default: { fprintf(stderr, "(ecaplay) Error! Unknown option '%s'.\n", option); print_usage(stderr); return 1; } } } return 0; } static int queue_tracks(int argc, char *argv[]) { int i, res = 0; char *path; FILE *f1; path = get_playlist_path(); /* path maybe NULL but fopen can handle it */ f1 = fopen(path, "a+b"); if (f1 != NULL) { for(i = 1; i < argc; i++) { char c = argv[i][0]; /* printf("(ecaplay) processing arg '%s' (%c).\n", argv[i], c); */ /* FIXME: add support for '-- -foo.wav' */ if (c != '-') { /* printf("(ecaplay) 2:processing arg '%s' (%c).\n", argv[i], c); */ if (c != '/') { /* reserve extra room for '/' */ char* tmp = malloc(PATH_MAX + strlen(argv[i]) + 1); if (getcwd(tmp, PATH_MAX) != NULL) { strcat(tmp, "/"); strcat(tmp, argv[i]); printf("(ecaplay) Track '%s' added to playlist.\n", argv[i]); fwrite(tmp, 1, strlen(tmp), f1); } free(tmp); } else { printf("(ecaplay) Track '%s' added to playlist.\n", argv[i]); fwrite(argv[i], 1, strlen(argv[i]), f1); } fwrite("\n", 1, 1, f1); } } fclose(f1); } else { res = -1; } free(path); /* can be NULL */ return res; } /** * Sets the chainsetup audio format to 'fmt'. * * @return zero on success, non-zero on error */ int set_audio_format(eci_handle_t* eci, const char* fmt) { size_t len = strlen("cs-set-audio-format -f:") + strlen(fmt) + 1; char* tmpbuf = malloc(len); int res = 0; strcpy(tmpbuf, "cs-set-audio-format "); strcat(tmpbuf, fmt); tmpbuf[len - 1] = 0; eci_command_r(eci, tmpbuf); if (eci_error_r(eci)) { fprintf(stderr, "(ecaplay) Unknown audio format encountered.\n"); res = -1; } free(tmpbuf); return res; } static void setup_signal_handling(void) { struct sigaction es_handler_int; struct sigaction ign_handler; es_handler_int.sa_handler = signal_handler; sigemptyset(&es_handler_int.sa_mask); es_handler_int.sa_flags = 0; ign_handler.sa_handler = SIG_IGN; sigemptyset(&ign_handler.sa_mask); ign_handler.sa_flags = 0; /* handle the follwing signals explicitly */ sigaction(SIGINT, &es_handler_int, 0); /* ignore the following signals */ sigaction(SIGPIPE, &ign_handler, 0); sigaction(SIGFPE, &ign_handler, 0); } static void signal_handler(int signum) { ++ecaplay_skip_flag; }