12 #define MAX(a,b) ((a)>=(b)?(a):(b))
13 #define MIN(a,b) ((a)<=(b)?(a):(b))
14 #define CLAMP(x,min,max) MAX(min, MIN(max, x))
16 typedef struct Color Color;
17 typedef struct Player Player;
18 typedef struct Playlist Playlist;
30 Seek = 10, /* 10 seconds */
31 Seekfast = 60, /* a minute */
33 Bps = 44100*2*2, /* 44100KHz, stereo, s16 for a sample */
34 Relbufsz = Bps/2, /* 0.5s */
69 int mainstacksize = 32768;
72 static int audio = -1;
76 static Player *playernext;
77 static Player *playercurr;
78 static vlong byteswritten;
79 static int pcur, pcurplaying;
80 static int scroll, scrollsz;
83 static Channel *playc;
84 static Mousectl *mctl;
85 static Keyboardctl *kctl;
86 static int colwidth[10];
87 static int mincolwidth[10];
88 static char *cols = "AatD";
91 static Rectangle seekbar;
92 static int seekmx, newseekmx = -1;
93 static double seekoff; /* ms */
94 static Lock audiolock;
95 static int audioerr = 0;
97 static char *covers[] =
99 "art", "folder", "cover", "Cover", "scans/CD", "Scans/Front", "Covers/Front"
102 static Color colors[Numcolors] =
104 [Dback] = {0xf0f0f0},
105 [Dfhigh] = {0xffffff},
106 [Dfmed] = {0x343434},
107 [Dflow] = {0xa5a5a5},
108 [Dfinv] = {0x323232},
109 [Dbmed] = {0x72dec2},
110 [Dblow] = {0x404040},
111 [Dbinv] = {0xffb545},
114 static int Scrollwidth;
115 static int Scrollheight;
122 if(audio < 0 && (audio = open("/dev/audio", OWRITE|OCEXEC)) < 0 && audioerr == 0){
139 #pragma varargck type "P" uvlong
147 sec = va_arg(f->args, uvlong);
149 s = seprint(s, tmp+sizeof(tmp), "%02lld:", sec/3600);
152 s = seprint(s, tmp+sizeof(tmp), "%02lld:", sec/60);
154 seprint(s, tmp+sizeof(tmp), "%02lld", sec);
156 return fmtstrcpy(f, tmp);
160 getcol(Meta *m, int c)
165 case Palbum: return m->album;
166 case Partist: return m->artist[0];
167 case Pdate: return m->date;
168 case Ptitle: return (!colspath && *m->title == 0) ? m->basename : m->title;
169 case Ptrack: snprint(tmp, sizeof(tmp), "%4s", m->track); return m->track ? tmp : nil;
170 case Ppath: return m->path;
174 snprint(tmp, sizeof(tmp), "%8P", m->duration/1000);
176 default: sysfatal("invalid column '%c'", c);
185 int i, n, x, total, width;
187 if(mincolwidth[0] == 0){
188 for(i = 0; cols[i] != 0; i++)
190 for(n = 0; n < pl->n; n++){
191 for(i = 0; cols[i] != 0; i++){
192 if((x = stringwidth(f, getcol(pl->m+n, cols[i]))) > mincolwidth[i])
200 width = Dx(screen->r);
201 for(i = 0; cols[i] != 0; i++){
202 if(cols[i] == Pduration || cols[i] == Pdate || cols[i] == Ptrack)
203 width -= mincolwidth[i] + 8;
205 total += mincolwidth[i];
210 for(i = 0; cols[i] != 0; i++){
211 if(cols[i] == Ppath || cols[i] == Pbasename)
213 if(cols[i] == Pduration || cols[i] == Pdate || cols[i] == Ptrack)
214 colwidth[i] = mincolwidth[i];
216 colwidth[i] = (width - Scrollwidth - n*8) * mincolwidth[i] / total;
223 return &pl->m[shuffle != nil ? shuffle[i] : i];
229 scrollsz = Dy(screen->r)/f->height - 2;
238 int i, j, left, right, scrollcenter, w;
242 lockdisplay(display);
244 scroll = CLAMP(scroll, 0, pl->n - scrollsz);
245 left = screen->r.min.x;
246 if(scrollsz < pl->n) /* adjust for scrollbar */
247 left += Scrollwidth + 1;
250 draw(screen, screen->r, colors[Dback].im, nil, ZP);
253 if(scrollsz < pl->n){ /* scrollbar */
254 p.x = sp.x = screen->r.min.x + Scrollwidth;
255 p.y = screen->r.min.y;
256 sp.y = screen->r.max.y;
257 line(screen, p, sp, Endsquare, Endsquare, 0, colors[Dflow].im, ZP);
260 r.max.x = r.min.x + Scrollwidth - 1;
265 scrollcenter = (Dy(screen->r)-Scrollheight*5/4)*scroll / (pl->n - scrollsz);
266 r.min.y += scrollcenter + Scrollheight/4;
267 r.max.y = r.min.y + Scrollheight;
268 draw(screen, r, colors[Dblow].im, nil, ZP);
273 sp.y = screen->r.max.y;
274 for(i = 0; cols[i+1] != 0; i++){
275 p.x += colwidth[i] + 4;
277 line(screen, p, sp, Endsquare, Endsquare, 0, colors[Dflow].im, ZP);
283 p.y = screen->r.min.y + 2;
285 for(i = scroll; i < pl->n; i++, p.y += f->height){
288 if(p.y > screen->r.max.y)
294 sel.max.x = screen->r.max.x;
295 sel.max.y = p.y + f->height;
296 draw(screen, sel, colors[Dbinv].im, nil, ZP);
297 col = colors[Dfinv].im;
299 col = colors[Dfmed].im;
305 for(j = 0; cols[j] != 0; j++){
306 sel.max.x = p.x + colwidth[j];
307 replclipr(screen, 0, sel);
308 string(screen, p, col, sp, f, getcol(getmeta(i), cols[j]));
309 p.x += colwidth[j] + 8;
311 replclipr(screen, 0, screen->r);
313 if(pcurplaying == i){
315 leftp.y = rightp.y = p.y - 1;
317 rightp.x = screen->r.max.x;
318 line(screen, leftp, rightp, 0, 0, 0, colors[Dflow].im, sp);
319 leftp.y = rightp.y = p.y + f->height;
320 line(screen, leftp, rightp, 0, 0, 0, colors[Dflow].im, sp);
326 dur = getmeta(pcurplaying)->duration;
327 if(pcurplaying >= 0){
328 msec = byteswritten*1000/Bps;
330 snprint(tmp, sizeof(tmp), "%s%P/%P 100%%",
331 shuffle != nil ? "∫ " : "",
333 w = stringwidth(f, tmp);
334 msec = MIN(msec, dur);
335 snprint(tmp, sizeof(tmp), "%s%P/%P %d%%",
336 shuffle != nil ? "∫ " : "",
337 (uvlong)(newseekmx >= 0 ? seekoff : msec)/1000,
340 snprint(tmp, sizeof(tmp), "%s%P %d%%",
341 shuffle != nil ? "∫ " : "",
343 w = stringwidth(f, tmp);
344 snprint(tmp, sizeof(tmp), "%s%P %d%%",
345 shuffle != nil ? "∫ " : "",
349 snprint(tmp, sizeof(tmp), "%s%d%%", shuffle != nil ? "∫ " : "", 100);
350 w = stringwidth(f, tmp);
351 snprint(tmp, sizeof(tmp), "%s%d%%", shuffle != nil ? "∫ " : "", volume);
354 right = r.max.x - w - 4;
356 r.min.y = r.max.y - f->height - 4;
357 if(pcurplaying < 0 || dur == 0)
359 draw(screen, r, colors[Dblow].im, nil, ZP);
360 p = addpt(Pt(r.max.x-stringwidth(f, tmp)-4, r.min.y), Pt(2, 2));
362 string(screen, p, colors[Dfhigh].im, sp, f, tmp);
365 if(cover != nil && full){
367 r.min.x = screen->r.max.x - cover->r.max.x - 8;
368 draw(screen, r, colors[Dblow].im, nil, ZP);
370 r.min.x = r.max.x - cover->r.max.x - 8;
371 r.min.y = r.max.y - cover->r.max.y - 8 - f->height - 4;
372 r.max.y = r.min.y + cover->r.max.y + 8;
373 draw(screen, r, colors[Dblow].im, nil, ZP);
374 draw(screen, insetrect(r, 4), cover, nil, ZP);
379 if(pcurplaying >= 0 && dur > 0){
380 r = insetrect(sel, 3);
381 draw(screen, r, colors[Dback].im, nil, ZP);
383 r.max.x = r.min.x + Dx(r) * (double)msec / (double)dur;
384 draw(screen, r, colors[Dbmed].im, nil, ZP);
387 flushimage(display, 1);
388 unlockdisplay(display);
392 coverload(void *player_)
394 int p[2], pid, fd, i;
395 char *prog, *path, *s, tmp[32];
401 threadsetname("cover");
403 m = getmeta(player->pcur);
409 if(m->imagefmt != nil && m->imagereader == 0){
410 if(strcmp(m->imagefmt, "image/png") == 0)
412 else if(strcmp(m->imagefmt, "image/jpeg") == 0)
417 path = strdup(m->path);
418 if(path != nil && (s = utfrrune(path, '/')) != nil){
421 for(i = 0; i < nelem(covers) && prog == nil; i++){
422 if((s = smprint("%s/%s.jpg", path, covers[i])) != nil && (fd = open(s, OREAD)) >= 0)
426 if(fd < 0 && (s = smprint("%s/%s.png", path, covers[i])) != nil && (fd = open(s, OREAD)) >= 0)
438 fd = open(m->path, OREAD);
439 seek(fd, m->imageoffset, 0);
442 if((pid = rfork(RFPROC|RFFDG|RFNOTEG|RFCENVG|RFNOWAIT)) == 0){
443 dup(fd, 0); close(fd);
444 dup(p[1], 1); close(p[1]);
446 dup(fd = open("/dev/null", OWRITE), 2);
449 snprint(tmp, sizeof(tmp), "%s -9t | resample -x%d", prog, Coversz);
450 execl("/bin/rc", "rc", "-c", tmp, nil);
451 sysfatal("execl: %r");
457 newcover = readimage(display, p[0], 1);
467 postnote(PNGROUP, pid, "interrupt");
472 playerret(Player *player)
474 return recvul(player->ev) == Everror ? -1 : 0;
488 m = getmeta(p->pcur);
489 for(i = 0; cols[i] != 0; i++)
490 Bprint(&out, "%s\t", (s = getcol(m, cols[i])) ? s : "");
502 if(player == playernext)
504 if(!getmeta(player->pcur)->filefmt[0])
506 if(player == playercurr)
508 sendul(player->ctl, Cstop);
512 start(Player *player)
516 if(!getmeta(player->pcur)->filefmt[0])
519 sendul(player->ctl, Cstart);
522 static void playerthread(void *player_);
525 newplayer(int pcur, int loadnext)
529 if(playernext != nil && loadnext){
530 if(pcur == playernext->pcur){
539 player = mallocz(sizeof(*player), 1);
540 player->ctl = chancreate(sizeof(ulong), 0);
541 player->ev = chancreate(sizeof(ulong), 0);
544 threadcreate(playerthread, player, 4096);
545 if(getmeta(pcur)->filefmt[0] && playerret(player) < 0)
549 if(pcur < pl->n-1 && playernext == nil && loadnext)
550 playernext = newplayer(pcur+1, 0);
556 playerthread(void *player_)
558 char *buf, cmd[64], seekpos[12], *fmt;
563 int p[2], fd, pid, noinit, trycoverload;
565 vlong boffset, boffsetlast;
568 threadsetname("player");
578 cur = getmeta(player->pcur);
582 if((fd = open(cur->path, OREAD)) < 0){
584 sendul(player->ev, Everror);
585 chanclose(player->ev);
589 sendul(player->ev, Evready);
590 chanclose(player->ev);
594 if((pid = rfork(RFPROC|RFFDG|RFNOTEG|RFCENVG|RFNOWAIT)) == 0){
597 fd = open("/dev/null", OREAD);
598 dup(fd, 0); close(fd);
599 dup(p[0], 1); close(p[0]);
601 dup(fd = open("/dev/null", OWRITE), 2);
605 snprint(cmd, sizeof(cmd), "/bin/audio/%sdec", fmt);
606 snprint(seekpos, sizeof(seekpos), "%g", (double)boffset/Bps);
607 execl(cmd, cmd, boffset ? "-s" : nil, seekpos, nil);
609 execl("/bin/play", "play", "-o", "/fd/1", cur->path, nil);
616 sysfatal("rfork: %r");
624 sendul(player->ev, Evready);
625 chanclose(player->ev);
627 buf = malloc(Relbufsz);
628 if((io = ioproc()) == nil)
629 sysfatal("player: %r");
630 if((n = ioreadn(io, p[1], buf, Relbufsz)) < 0)
631 fprint(2, "player: %r\n");
632 if(recv(player->ctl, &c) < 0 || c != Cstart)
637 boffset = iowrite(io, audio, buf, n);
641 boffsetlast = boffset;
642 byteswritten = boffset;
643 pcurplaying = player->pcur;
648 n = ioread(io, p[1], buf, Relbufsz);
653 if(player->img != nil && nbrecv(player->img, &thiscover) != 0){
659 r = nbrecv(player->ctl, &c);
666 if(recv(player->ctl, &c) < 0)
670 boffset = MAX(0, boffset + player->seek*Bps);
673 }else if(c == Cstop){
680 byteswritten = boffset;
682 iowrite(io, audio, buf, n);
685 player->img = chancreate(sizeof(Image*), 0);
686 proccreate(coverload, player, 4096);
688 if(labs(boffset/Relbufsz - boffsetlast/Relbufsz) > 0){
689 boffsetlast = boffset;
694 if(n < 1){ /* seeking backwards or end of the song */
697 if(c != Cseekrel || (getmeta(pcurplaying)->duration && boffset >= getmeta(pcurplaying)->duration/1000*Bps)){
700 playercurr = newplayer((player->pcur+1) % pl->n, 1);
708 if(player->img != nil)
709 freeimage(recvp(player->img));
711 chanfree(player->ctl);
712 chanfree(player->ev);
714 postnote(PNGROUP, pid, "interrupt");
718 if(player == playercurr)
720 if(player == playernext)
728 toggle(Player *player)
730 return (player != nil && sendul(player->ctl, Ctoggle) == 1) ? 0 : -1;
734 seekrel(Player *player, double off)
736 if(player != nil && *getmeta(pcurplaying)->filefmt){
738 sendul(player->ctl, Cseekrel);
747 for(i = 0; i < pl->n; i++)
748 printmeta(&out, pl->m+i);
752 freeplist(Playlist *pl)
769 for(sz = 0;; sz += n){
772 s = realloc(s, bufsz);
774 if((n = readn(f, s+sz, bufsz-sz-1)) < 1)
789 char *raw, *s, *e, *a[5], *b;
794 if((raw = readall(fd)) == nil)
798 for(s = raw; (s = strchr(s, '\n')) != nil; s++){
803 if((pl = calloc(1, sizeof(*pl))) == nil || (pl->m = calloc(plsz+1, sizeof(Meta))) == nil){
805 werrstr("no memory");
810 for(s = pl->raw, m = pl->m;; s = e){
811 if((e = strchr(s, '\n')) == nil)
823 if(tokenize(s, a, nelem(a)) >= 4){
824 m->imageoffset = atoi(a[0]);
825 m->imagesize = atoi(a[1]);
826 m->imagereader = atoi(a[2]);
831 m->duration = strtoull(s, nil, 0);
834 if(m->numartist < Maxartist)
835 m->artist[m->numartist++] = s;
837 case Pfilefmt: m->filefmt = s; break;
838 case Palbum: m->album = s; break;
839 case Pdate: m->date = s; break;
840 case Ptitle: m->title = s; break;
841 case Ptrack: m->track = s; break;
844 m->basename = (b = utfrrune(s, '/')) == nil ? s : b+1;
848 if(m != nil && m->path != nil)
858 scroll = pcur - scrollsz/2 + 1;
867 int inc, i, a, cycle;
869 inc = (d == '/' || d == 'n') ? 1 : -1;
870 if(d == '/' || d == '?')
871 sz = enter(inc > 0 ? "forward:" : "backward:", buf, sizeof(buf), mctl, kctl, nil);
876 for(i = pcur+inc; i >= 0 && i < pl->n;){
878 for(a = 0; a < m->numartist; a++){
879 if(cistrstr(m->artist[a], buf) != nil)
882 if(m->album != nil && cistrstr(m->album, buf) != nil)
884 if(m->title != nil && cistrstr(m->title, buf) != nil)
886 if(cistrstr(m->path, buf) != nil)
891 if(i >= 0 && i < pl->n){
895 }else if(cycle && i+inc < 0){
899 }else if(cycle && i+inc >= pl->n){
913 if((f = open("/dev/volume", ORDWR)) < 0)
918 for(; (s = Brdline(&b, '\n')) != nil;){
919 if(strncmp(s, "master", 6) == 0 && tokenize(s, a, 3) == 3){
925 fprint(f, "master %d %d\n", l, r);
927 for(; (s = Brdline(&b, '\n')) != nil;){
928 if(strncmp(s, "master", 6) == 0 && tokenize(s, a, 3) == 3){
929 if(atoi(a[1]) == l && atoi(a[2]) == r)
931 if(atoi(a[1]) != ol && atoi(a[2]) != or)
933 if(l < 0 || r < 0 || l > 100 || r > 100)
956 int i, m, xi, a, c, pcurnew, pcurplayingnew;
974 a = 1 + nrand(m/4)*4; /* 1 ≤ a < m && a mod 4 = 1 */
975 c = 3 + nrand((m-2)/2)*2; /* 3 ≤ c < m-1 && c mod 2 = 1 */
978 shuffle = malloc(pl->n*sizeof(*shuffle));
979 xi = pcurplaying < 0 ? pcur : pcurplaying;
982 for(i = 0; i < pl->n;){
986 if(pcurplaying == xi)
993 pcurplaying = pcurplayingnew;
995 pcur = shuffle[pcur];
997 pcurplaying = shuffle[pcurplaying];
1004 playernext = newplayer(pcur+1, 0);
1008 plumbaudio(void *kbd)
1016 threadsetname("audio/plumb");
1017 if((f = plumbopen("audio", OREAD)) >= 0){
1018 while((m = plumbrecv(f)) != nil){
1020 if(strncmp(s, "key", 3) == 0 && isspace(s[3])){
1021 for(s = s+4; isspace(*s); s++);
1022 for(; (i = chartorune(&c, s)) > 0 && c != Runeerror; s += i)
1026 if(*s != '/' && m->wdir != nil)
1027 s = smprint("%s/%.*s", m->wdir, m->ndata, m->data);
1029 if((e = strrchr(s, '.')) != nil && strcmp(e, ".plist") == 0 && (pf = open(s, OREAD)) >= 0){
1037 memset(mincolwidth, 0, sizeof(mincolwidth)); /* readjust columns */
1040 for(i = 0; i < pl->n; i++){
1041 if(strcmp(pl->m[i].path, s) == 0){
1060 fprint(2, "usage: %s [-s] [-c aAdDtTp]\n", argv0);
1065 threadmain(int argc, char **argv)
1077 { nil, &m, CHANRCV },
1078 { nil, nil, CHANRCV },
1079 { nil, &key, CHANRCV },
1080 { nil, &ind, CHANRCV },
1081 { nil, nil, CHANEND },
1083 int n, scrolling, oldpcur, oldbuttons, pnew, shuffled;
1095 cols = EARGF(usage());
1096 if(strlen(cols) >= nelem(colwidth))
1097 sysfatal("max %d columns allowed", nelem(colwidth));
1104 if((pl = readplist(0)) == nil){
1105 fprint(2, "playlist: %r\n");
1106 sysfatal("playlist error");
1110 Binit(&out, 1, OWRITE);
1111 pnotifies = fd2path(1, buf, sizeof(buf)) == 0 && strcmp(buf, "/dev/cons") != 0;
1113 if(initdraw(nil, nil, "zuke") < 0)
1114 sysfatal("initdraw: %r");
1115 f = display->defaultfont;
1116 Scrollwidth = MAX(14, stringwidth(f, "#"));
1117 Scrollheight = MAX(16, f->height);
1118 Coversz = MAX(64, stringwidth(f, "∫ 00:00:00/00:00:00 100%"));
1119 unlockdisplay(display);
1120 display->locking = 1;
1121 if((mctl = initmouse(nil, screen)) == nil)
1122 sysfatal("initmouse: %r");
1123 if((kctl = initkeyboard(nil)) == nil)
1124 sysfatal("initkeyboard: %r");
1127 a[1].c = mctl->resizec;
1129 a[3].c = chancreate(sizeof(ind), 0);
1132 for(n = 0; n < Numcolors; n++)
1133 colors[n].im = allocimage(display, Rect(0,0,1,1), RGB24, 1, colors[n].rgb<<8 | 0xff);
1138 fmtinstall('P', positionfmt);
1139 threadsetname("zuke");
1142 pcur = nrand(pl->n);
1151 proccreate(plumbaudio, kctl->c, 4096);
1155 if(seekmx != newseekmx){
1160 oldbuttons = m.buttons;
1163 if(ptinrect(m.xy, seekbar)){
1164 seekoff = getmeta(pcurplaying)->duration * (double)(m.xy.x-1-seekbar.min.x) / (double)Dx(seekbar);
1177 scroll = MAX(scroll-scrollsz/4-1, 0);
1180 }else if(m.buttons == 16){
1181 scroll = MIN(scroll+scrollsz/4+1, pl->n-scrollsz);
1186 n = (m.xy.y - screen->r.min.y)/f->height;
1188 if(m.xy.x <= screen->r.min.x+Scrollwidth){
1190 scroll = MAX(0, scroll-n-1);
1193 }else if(m.buttons == 4){
1194 scroll = MIN(scroll+n+1, pl->n-scrollsz);
1197 }else if(m.buttons == 2){
1202 if(!scrolling && ptinrect(m.xy, insetrect(seekbar, -4))){
1203 if(ptinrect(m.xy, seekbar))
1204 seekrel(playercurr, seekoff/1000.0 - byteswritten/Bps);
1209 if(scrollsz >= pl->n)
1211 scroll = (m.xy.y - screen->r.min.y - Scrollheight/4)*(pl->n-scrollsz) / (Dy(screen->r)-Scrollheight/2);
1212 scroll = CLAMP(scroll, 0, pl->n-scrollsz);
1214 }else if(m.buttons == 1 || m.buttons == 2){
1218 if(m.buttons == 2 && oldbuttons == 0){
1220 playercurr = newplayer(pcur, 1);
1226 case Eresize: /* resize */
1227 if(getwindow(display, Refnone) < 0)
1228 sysfatal("getwindow: %r");
1234 seekrel(playercurr, -(double)Seek);
1237 seekrel(playercurr, Seek);
1240 seekrel(playercurr, -(double)Seekfast);
1243 seekrel(playercurr, Seekfast);
1259 scroll = pl->n-scrollsz;
1267 playercurr = newplayer(pcur, 1);
1276 if(pcur == pcurplaying)
1283 if(playercurr == nil)
1289 playercurr = newplayer(pnew, 1);
1295 if(playercurr == nil)
1301 playercurr = newplayer(pnew, 1);
1330 if(toggle(playercurr) != 0)
1344 if(playercurr != nil)
1349 if(pcur != oldpcur){
1350 pcur = CLAMP(pcur, 0, pl->n-1);
1353 else if(pcur > scroll + scrollsz)
1354 scroll = pcur - scrollsz;
1355 scroll = CLAMP(scroll, 0, pl->n-scrollsz);
1363 threadexitsall(nil);