/* mm.c - Rudimentary ncurses mp3 player front end. Wired to run mpg123, easy to modify for other commandline-based players. Features: directory browser internal playlist controlled by directory browser play/stop/pause/prev/next Compile: gcc -o mm mm.c -lncurses Copyright (c) 2000 Victor Zandy 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. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #define MIN(x,y) ((x) < (y) ? (x) : (y)) typedef enum { PLAYING, STOPPED, PAUSED } state; static state st; typedef struct dim dim_t; struct dim { unsigned x; unsigned y; unsigned height; unsigned width; }; static dim_t dim_stat; static dim_t dim_browse; static dim_t dim_help; static int thepid; #define BUTTON_QUIT 'q' #define BUTTON_PREV 'z' #define BUTTON_PLAY 'x' #define BUTTON_PAUSE 'c' #define BUTTON_STOP 'v' #define BUTTON_NEXT 'b' #define BUTTON_SELECT ' ' #define BUTTON_CLEAR 'w' typedef struct view *view_t; static view_t bview; static view_t sview; static view_t hview; static void init_dimensions() { dim_stat.x = 0; dim_stat.y = 0; dim_stat.height = 5; dim_stat.width = COLS; dim_browse.x = 0; dim_browse.y = dim_stat.height; dim_browse.height = LINES - dim_stat.height - 3; dim_browse.width = COLS; dim_help.x = 0; dim_help.y = dim_browse.y + dim_browse.height; dim_help.height = 3; dim_help.width = COLS; } static void die(const char *why) { kill(thepid, SIGTERM); endwin(); fprintf(stderr, "%s\n", why); exit(1); } static int isdir(const char *absname) { struct stat statbuf; if (0 > stat(absname, &statbuf)) return 0; return S_ISDIR(statbuf.st_mode); } static int ismp3(const char *absname) { struct stat statbuf; if (0 > stat(absname, &statbuf)) return 0; if (!S_ISREG(statbuf.st_mode)) return 0; if (strlen(absname) < 4) return 0; absname += strlen(absname) - 4; return !strncasecmp(absname, ".mp3", 4); } static char* Xstrdup(const char *s) { char *t; t = strdup(s); if (!t) die("Out of memory"); return t; } static void * Xmalloc(size_t size) { void *p; p = malloc(size); if (!p) { fprintf(stderr, "malloc failure of %d bytes\n", size); die("Out of memory"); } return p; } static void * Xrealloc(void *ptr, size_t size) { void *p; p = realloc(ptr, size); if (!p) { fprintf(stderr, "realloc failure of %d bytes\n", size); die("Out of memory"); } return p; } struct view { WINDOW *win; unsigned x, y; unsigned height, width; void *data; void (*redraw)(WINDOW *win, void *data, unsigned x, unsigned y, unsigned height, unsigned width); }; static view_t view(unsigned x, unsigned y, unsigned height, unsigned width, void *data, void (*redraw)(WINDOW *win, void *data, unsigned x, unsigned y, unsigned height, unsigned width)) { view_t v; v = (view_t) Xmalloc(sizeof(struct view)); v->win = newwin(height, width, y, x); if (!v->win) die("Out of memory"); v->x = x; v->y = y; v->height = height; v->width = width; v->data = data; v->redraw = redraw; return v; } static void view_free(view_t v) { delwin(v->win); free(v); } static void view_clear(view_t v) { wclear(v->win); } static void view_redraw(view_t v) { v->redraw(v->win, v->data, v->x, v->y, v->height, v->width); } static WINDOW * view_win(view_t v) { return v->win; } static void text_redraw(WINDOW *win, void *data, unsigned x, unsigned y, unsigned height, unsigned width) { wrefresh(win); } #define DEFAULT_MAXSTRLIST 128 typedef struct strlist *strlist_t; struct strlist { unsigned num; unsigned max; char **str; }; static strlist_t playlist; static int playidx; static strlist_t strlist() { strlist_t sl; sl = Xmalloc(sizeof(struct strlist)); sl->num = 0; sl->max = DEFAULT_MAXSTRLIST; sl->str = (char **) Xmalloc(DEFAULT_MAXSTRLIST * sizeof(char*)); return sl; } static void strlist_free(strlist_t sl) { int i; for (i = 0; i < sl->num; i++) free(sl->str[i]); free(sl->str); free(sl); } static char * concatfn(const char *abs, const char *rel) { static char buf[2048]; if (abs[strlen(abs)-1] == '/') { if (sizeof(buf) < (strlen(abs) + strlen(rel) + 1)) assert(0); sprintf(buf, "%s%s", abs, rel); } else { if (sizeof(buf) < (strlen(abs) + strlen(rel) + 2)) assert(0); sprintf(buf, "%s/%s", abs, rel); } return buf; } static void strlist_ins(strlist_t sl, const char *s) { if (sl->num >= sl->max) { sl->str = Xrealloc(sl->str, 2 * sl->max); sl->max *= 2; } sl->str[sl->num++] = Xstrdup(s); } static strlist_t parsedir(const char *absdir) { DIR *dp; struct dirent *e; strlist_t sl; dp = opendir(absdir); if (!dp) return NULL; sl = strlist(); while ((e = readdir(dp))) { char *p = concatfn(absdir, e->d_name); if (ismp3(p) || (isdir(p) && strcmp(e->d_name, ".") && strcmp(e->d_name, ".."))) strlist_ins(sl, e->d_name); } closedir(dp); return sl; } typedef struct lpad *lpad_t; /* String list display */ struct lpad { WINDOW *pad; /* pad containing window contents */ unsigned lines; /* num viewable lines */ strlist_t sl; /* elements */ unsigned top; /* first visible element of SL */ unsigned cur; /* selected element of SL */ }; static void lpad_free(lpad_t lp) { delwin(lp->pad); free(lp); } static lpad_t lpad(strlist_t sl, unsigned lines) { lpad_t lp; int i, maxwidth; lp = (lpad_t) Xmalloc(sizeof(struct lpad)); maxwidth = 0; for (i = 0; i < sl->num; i++) if (strlen(sl->str[i]) > maxwidth) maxwidth = strlen(sl->str[i]); /* FIXME: We add one for the cursor; put the cursor in the corner */ lp->pad = newpad(sl->num, maxwidth + 1); if (!lp->pad) die("Out of memory"); lp->lines = lines; if (sl->num) { wstandout(lp->pad); mvwaddstr(lp->pad, 0, 0, sl->str[0]); wstandend(lp->pad); } lp->cur = 0; for (i = 1; i < sl->num; i++) mvwaddstr(lp->pad, i, 0, sl->str[i]); lp->sl = sl; lp->top = 0; return lp; } /* Returns nonzero if scroll occurs */ static int lpad_down(lpad_t lp, int n) { if (lp->cur >= lp->sl->num - 1) return 0; mvwaddstr(lp->pad, lp->cur, 0, lp->sl->str[lp->cur]); lp->cur = MIN(lp->cur + n, lp->sl->num-1); wstandout(lp->pad); mvwaddstr(lp->pad, lp->cur, 0, lp->sl->str[lp->cur]); wstandend(lp->pad); if (lp->cur - lp->top >= lp->lines - 1) { lp->top = lp->cur - lp->lines + 1; return 1; } return 0; } /* Returns nonzero if scroll occurs */ static int lpad_up(lpad_t lp, int n) { if (lp->cur == 0) return 0; mvwaddstr(lp->pad, lp->cur, 0, lp->sl->str[lp->cur]); lp->cur = (n > lp->cur) ? 0 : lp->cur - n; wstandout(lp->pad); mvwaddstr(lp->pad, lp->cur, 0, lp->sl->str[lp->cur]); wstandend(lp->pad); if (lp->cur < lp->top) { lp->top = lp->cur; return 1; } return 0; } static const char * lpad_selection(lpad_t lp) { return lp->sl->str[lp->cur]; } static unsigned lpad_cur(lpad_t lp) { return lp->cur; } static unsigned lpad_num(lpad_t lp) { return lp->sl->num; } /* Browser frames */ typedef struct bframe *bframe_t; struct bframe { char *cwd; /* directory */ lpad_t lp; /* pad */ unsigned lines; /* viewable lines */ bframe_t prev; /* linked list */ }; static bframe_t activebf; static bframe_t bframe(const char *absdir, unsigned lines, bframe_t prev) { bframe_t bf; strlist_t sl; char *dir; /* copy absdir now or parsedir will clobber it */ dir = Xstrdup(absdir); sl = parsedir(dir); if (!sl) { free(dir); return NULL; } if (sl->num == 0) { free(dir); return NULL; } bf = (bframe_t) Xmalloc(sizeof(struct bframe)); bf->cwd = dir; bf->lp = lpad(sl, lines); bf->lines = lines; bf->prev = prev; return bf; } static void bframe_free(bframe_t bf) { strlist_free(bf->lp->sl); lpad_free(bf->lp); free(bf->cwd); free(bf); } static void bframe_up() { if (lpad_up(activebf->lp, 1)) view_clear(bview); view_redraw(bview); } static void bframe_down() { if (lpad_down(activebf->lp, 1)) view_clear(bview); view_redraw(bview); } static void bframe_follow() { bframe_t bf; const char *dir; dir = concatfn(activebf->cwd, lpad_selection(activebf->lp)); if (!isdir(dir)) return; bf = bframe(dir, activebf->lines, activebf); if (!bf) return; activebf = bf; view_clear(bview); wrefresh(view_win(bview)); view_redraw(bview); } static void bframe_back() { bframe_t bf; if (!activebf->prev) return; bf = activebf; activebf = activebf->prev; bframe_free(bf); view_clear(bview); wrefresh(view_win(bview)); view_redraw(bview); } static void bframe_redraw(WINDOW *win, void *data, unsigned x, unsigned y, unsigned height, unsigned width) { prefresh(activebf->lp->pad, activebf->lp->top, 0, y, x, y + height - 1, x + width - 1); } static void status() { char *s, *p1, *p2, *p3; WINDOW *statwin = view_win(sview); wclear(statwin); if (st == STOPPED) { wrefresh(statwin); return; } if (st == PLAYING) mvwaddstr(statwin, 0, 0, "PLAY"); else if (st == PAUSED) mvwaddstr(statwin, 0, 0, "PAUSE"); s = Xstrdup(playlist->str[playidx]); p3 = rindex(s, '/'); if (p3) { *p3++ = '\0'; mvwaddnstr(statwin, 3, 0, p3, COLS); } p2 = rindex(s, '/'); if (p2) { *p2++ = '\0'; mvwaddnstr(statwin, 2, 0, p2, COLS); } p1 = rindex(s, '/'); if (p1) { *p1++ = '\0'; mvwaddnstr(statwin, 1, 0, p1, COLS); } view_redraw(sview); free(s); } static int start_mp3(const char *filename) { int pid; pid = fork(); if (0 > pid) die(strerror(errno)); if (pid == 0) { close(1); close(2); if (0 > execlp("mpg123", "mpg123", "-q", filename, NULL)) die(strerror(errno)); } st = PLAYING; status(); return pid; } static void handle_sigchld(int sig) { int p; int status; p = waitpid(-1, &status, WNOHANG|WUNTRACED); if (0 > p) die(strerror(errno)); if (p != thepid) /* Old process, who cares? */ return; if (WIFSTOPPED(status)) /* Paused */ return; /* Terminated */ if (st != PLAYING) return; if (playidx + 1 >= playlist->num) st = STOPPED; else { playidx++; thepid = start_mp3(playlist->str[playidx]); } } static void set_sigchld() { struct sigaction sa; sa.sa_handler = handle_sigchld; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sa.sa_restorer = 0; if (0 > sigaction(SIGCHLD, &sa, NULL)) die(strerror(errno)); } static void ign_sigchld() { struct sigaction sa; sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sa.sa_restorer = 0; if (0 > sigaction(SIGCHLD, &sa, NULL)) die(strerror(errno)); } static void button_play() { if (playidx >= playlist->num) return; if (st == STOPPED) thepid = start_mp3(playlist->str[playidx]); else if (st == PAUSED) kill(thepid, SIGCONT); st = PLAYING; status(); } static void button_prev() { if (playidx <= 0) return; playidx--; if (playidx >= playlist->num) return; if (st == PLAYING) { ign_sigchld(); kill(thepid, SIGTERM); waitpid(thepid, NULL, 0); set_sigchld(); thepid = start_mp3(playlist->str[playidx]); } else if (st == PAUSED) { ign_sigchld(); kill(thepid, SIGTERM); kill(thepid, SIGCONT); waitpid(thepid, NULL, 0); thepid = start_mp3(playlist->str[playidx]); kill(thepid, SIGSTOP); waitpid(thepid, NULL, WUNTRACED); st = PAUSED; /* reset by start_mp3 */ set_sigchld(); } status(); } static void button_next() { if (playidx + 1 >= playlist->num) return; playidx++; if (st == PLAYING) { ign_sigchld(); kill(thepid, SIGTERM); waitpid(thepid, NULL, 0); set_sigchld(); thepid = start_mp3(playlist->str[playidx]); } else if (st == PAUSED) { ign_sigchld(); kill(thepid, SIGTERM); kill(thepid, SIGCONT); waitpid(thepid, NULL, 0); thepid = start_mp3(playlist->str[playidx]); kill(thepid, SIGSTOP); waitpid(thepid, NULL, WUNTRACED); st = PAUSED; /* reset by start_mp3 */ set_sigchld(); } status(); } static void button_stop() { if (st == PAUSED || st == PLAYING) { ign_sigchld(); kill(thepid, SIGTERM); if (st == PAUSED) kill(thepid, SIGCONT); waitpid(thepid, NULL, 0); set_sigchld(); st = STOPPED; } playidx = 0; status(); } static void button_pause() { if (st == PLAYING) { ign_sigchld(); kill(thepid, SIGSTOP); waitpid(thepid, NULL, WUNTRACED); set_sigchld(); st = PAUSED; } else if (st == PAUSED) { kill(thepid, SIGCONT); st = PLAYING; } status(); } static void playlist_ins() { char *s; strlist_t sl; int i; s = concatfn(activebf->cwd, lpad_selection(activebf->lp)); if (ismp3(s)) { strlist_ins(playlist, s); return; } if (!isdir(s)) return; /* Shouldn't happen */ s = Xstrdup(s); sl = parsedir(s); if (!sl) { free(s); return; } for (i = 0; i < sl->num; i++) { char *t; t = concatfn(s, sl->str[i]); if (ismp3(t)) strlist_ins(playlist, t); } free(s); } static void playlist_clr() { button_stop(); strlist_free(playlist); playlist = strlist(); } static void helpinfo(WINDOW *win) { wclear(win); wattron(win, COLOR_PAIR(1)); wmove(win, 1, 0); waddch(win, A_BOLD|toupper(BUTTON_PREV)); waddstr(win, " prev "); waddch(win, A_BOLD|toupper(BUTTON_PLAY)); waddstr(win, " play "); waddch(win, A_BOLD|toupper(BUTTON_PAUSE)); waddstr(win, " pause "); waddch(win, A_BOLD|toupper(BUTTON_STOP)); waddstr(win, " stop "); waddch(win, A_BOLD|toupper(BUTTON_NEXT)); waddstr(win, " next "); wmove(win, 2, 0); waddstr(win, "LEFT"); waddstr(win, " up dir "); wstandout(win); wstandout(win); waddstr(win, "RIGHT"); wstandend(win); waddstr(win, " down dir "); wstandout(win); waddstr(win, "SPC"); wstandend(win); waddstr(win, " select dir/file "); wstandout(win); wstandout(win); waddch(win, toupper(BUTTON_QUIT)); wstandend(win); waddstr(win, " quit "); wstandout(win); wattron(win, COLOR_PAIR(2)); } int main(int argc, char *argv[]) { initscr(); start_color(); init_pair(1, COLOR_BLUE, COLOR_BLACK); init_pair(2, COLOR_WHITE, COLOR_BLACK); keypad(stdscr, TRUE); cbreak(); noecho(); set_sigchld(); st = STOPPED; halfdelay(1); /* Force getch to return periodically so we can monitor state of child mpg123 */ init_dimensions(); playlist = strlist(); activebf = bframe("/home/vic/mp3", dim_browse.height, NULL); if (!activebf) activebf = bframe("/", dim_browse.height, NULL); if (!activebf) die("Nothing to browse"); bview = view(dim_browse.x, dim_browse.y, dim_browse.height, dim_browse.width, NULL, bframe_redraw); sview = view(dim_stat.x, dim_stat.y, dim_stat.height, dim_stat.width, NULL, text_redraw); hview = view(dim_help.x, dim_help.y, dim_help.height, dim_help.width, NULL, text_redraw); refresh(); /* So getch won't call refresh */ helpinfo(view_win(hview)); view_redraw(bview); view_redraw(sview); view_redraw(hview); do { switch (getch()) { case KEY_UP: bframe_up(); break; case KEY_DOWN: bframe_down(); break; case KEY_RIGHT: bframe_follow(); break; case KEY_LEFT: bframe_back(); break; case BUTTON_SELECT: playlist_ins(); break; case BUTTON_CLEAR: playlist_clr(); break; case BUTTON_PREV: button_prev(); break; case BUTTON_PLAY: button_play(); break; case BUTTON_PAUSE: button_pause(); break; case BUTTON_STOP: /* Stop */ button_stop(); break; case BUTTON_NEXT: /* Next */ button_next(); break; case BUTTON_QUIT: /* Quit */ if (st == PLAYING || st == PAUSED) { ign_sigchld(); kill(thepid, SIGTERM); waitpid(thepid, NULL, 0); set_sigchld(); } endwin(); view_free(bview); exit(1); break; } } while (1); }