ref: 80ccbe0b30bdc378f088a5477cead1df7851f097
dir: /sys/src/cmd/audio/zuke/zuke.c/
#include <u.h>
#include <libc.h>
#include <bio.h>
#include <thread.h>
#include <draw.h>
#include <mouse.h>
#include <keyboard.h>
#include <plumb.h>
#include <ctype.h>
#include "plist.h"
#include "icy.h"
#define MAX(a,b) ((a)>=(b)?(a):(b))
#define MIN(a,b) ((a)<=(b)?(a):(b))
#define CLAMP(x,min,max) MAX(min, MIN(max, x))
typedef struct Color Color;
typedef struct Player Player;
typedef struct Playlist Playlist;
enum
{
Cstart = 1,
Cstop,
Ctoggle,
Cseekrel,
Rgdisabled = 0,
Rgtrack,
Rgalbum,
Numrg,
Everror = 1,
Evready,
Seek = 10, /* 10 seconds */
Seekfast = 60, /* a minute */
Bps = 44100*2*2, /* 44100KHz, stereo, s16 for a sample */
Relbufsz = Bps/2, /* 0.5s */
Dback = 0,
Dfhigh,
Dfmed,
Dflow,
Dfinv,
Dbmed,
Dblow,
Dbinv,
Numcolors,
Ncol = 10,
};
struct Color {
u32int rgb;
Image *im;
};
struct Player
{
Channel *ctl;
Channel *ev;
Channel *img;
Channel *icytitlec;
char *icytitle;
double seek;
double gain;
int pcur;
};
struct Playlist
{
Meta *m;
int n;
char *raw;
int rawsz;
};
int mainstacksize = 32768;
static int debug;
static int audio = -1;
static int volume, rg;
static int pnotifies;
static Playlist *pl;
static Player *playernext;
static Player *playercurr;
static vlong byteswritten;
static int pcur, pcurplaying;
static int scroll, scrollsz;
static Font *f;
static Image *cover;
static Channel *playc;
static Channel *redrawc;
static Mousectl *mctl;
static Keyboardctl kctl;
static int shiftdown;
static int colwidth[Ncol];
static int mincolwidth[Ncol];
static char *cols = "AatD";
static int colspath;
static int *shuffle;
static int repeatone;
static Rectangle seekbar;
static int seekmx, newseekmx = -1;
static double seekoff; /* ms */
static Lock audiolock;
static int audioerr = 0;
static Biobuf out;
static char *covers[] =
{
"art", "folder", "cover", "Cover", "scans/CD", "Scans/Front", "Covers/Front"
};
static Color colors[Numcolors] =
{
[Dback] = {0xf0f0f0},
[Dfhigh] = {0xffffff},
[Dfmed] = {0x343434},
[Dflow] = {0xa5a5a5},
[Dfinv] = {0x323232},
[Dbmed] = {0x72dec2},
[Dblow] = {0x404040},
[Dbinv] = {0xffb545},
};
static int Scrollwidth;
static int Scrollheight;
static int Seekthicc;
static int Coversz;
static char *
matchvname(char **s)
{
char *names[] = {"master", "pcm out"};
int i, l;
for(i = 0; i < nelem(names); i++){
l = strlen(names[i]);
if(strncmp(*s, names[i], l) == 0){
*s += l;
return names[i];
}
}
return nil;
}
static void
chvolume(int d)
{
int f, x, ox, want, try;
char *s, *e;
Biobuf b;
char *n;
if((f = open("/dev/volume", ORDWR|OCEXEC)) < 0)
return;
Binit(&b, f, OREAD);
want = x = -1;
ox = 0;
for(try = 0; try < 10; try++){
for(n = nil; (s = Brdline(&b, '\n')) != nil;){
if((n = matchvname(&s)) != nil && (ox = strtol(s, &e, 10)) >= 0 && s != e)
break;
n = nil;
}
if(want < 0){
want = CLAMP(ox+d, 0, 100);
x = ox;
}
if(n == nil || (d > 0 && ox >= want) || (d < 0 && ox <= want))
break;
x = CLAMP(x+d, 0, 100);
if(fprint(f, "%s %d\n", n, x) < 0)
break;
/* go to eof and back */
while(Brdline(&b, '\n') != nil);
Bseek(&b, 0, 0);
}
volume = CLAMP(ox, 0, 100);
Bterm(&b);
close(f);
}
static void
audioon(void)
{
lock(&audiolock);
if(audio < 0){
if((audio = open("/dev/audio", OWRITE|OCEXEC)) < 0 && audioerr == 0){
fprint(2, "%r\n");
audioerr = 1;
}else{
chvolume(0);
}
}
unlock(&audiolock);
}
static void
audiooff(void)
{
lock(&audiolock);
close(audio);
audio = -1;
audioerr = 0;
unlock(&audiolock);
}
#pragma varargck type "P" uvlong
static int
positionfmt(Fmt *f)
{
char *s, tmp[16];
u64int sec;
s = tmp;
sec = va_arg(f->args, uvlong);
if(sec >= 3600){
s = seprint(s, tmp+sizeof(tmp), "%02lld:", sec/3600);
sec %= 3600;
}
s = seprint(s, tmp+sizeof(tmp), "%02lld:", sec/60);
sec %= 60;
seprint(s, tmp+sizeof(tmp), "%02lld", sec);
return fmtstrcpy(f, tmp);
}
static char *
getcol(Meta *m, int c)
{
static char tmp[32];
char *s;
s = nil;
switch(c){
case Palbum: s = m->album; break;
case Partist: s = m->artist[0]; break;
case Pdate: s = m->date; break;
case Ptitle: s = (!colspath && (m->title == nil || *m->title == 0)) ? m->basename : m->title; break;
case Ptrack: snprint(tmp, sizeof(tmp), "%4s", m->track); s = m->track ? tmp : nil; break;
case Ppath: s = m->path; break;
case Pduration:
tmp[0] = 0;
if(m->duration > 0)
snprint(tmp, sizeof(tmp), "%8P", m->duration/1000);
s = tmp;
break;
default: sysfatal("invalid column '%c'", c);
}
return s ? s : "";
}
static Meta *
getmeta(int i)
{
return &pl->m[shuffle != nil ? shuffle[i] : i];
}
static void
updatescrollsz(void)
{
scrollsz = Dy(screen->r)/f->height - 2;
}
static void
redraw_(int full)
{
static Image *back, *ocover;
static int oscrollcenter, opcur, opcurplaying;
int x, i, j, scrollcenter, w;
uvlong dur, msec;
Rectangle sel, r;
char tmp[32], *s;
Point p, sp, p₀, p₁;
Image *col;
/* seekbar playback/duration text */
i = snprint(tmp, sizeof(tmp), "%s%s%s%s",
rg ? (rg == Rgalbum ? "ᴬ" : "ᵀ") : "",
repeatone ? "¹" : "",
shuffle != nil ? "∫" : "",
(rg || repeatone || shuffle != nil) ? " " : ""
);
msec = 0;
dur = 0;
w = stringwidth(f, tmp);
if(pcurplaying >= 0){
msec = byteswritten*1000/Bps;
dur = getmeta(pcurplaying)->duration;
if(dur > 0){
snprint(tmp+i, sizeof(tmp)-i, "%P/%P ", dur/1000, dur/1000);
w += stringwidth(f, tmp+i);
msec = MIN(msec, dur);
i += snprint(tmp+i, sizeof(tmp)-i, "%P/%P ",
(uvlong)(newseekmx >= 0 ? seekoff : msec)/1000,
dur/1000
);
}else{
j = snprint(tmp+i, sizeof(tmp)-i, "%P ", msec/1000);
w += stringwidth(f, tmp+i);
i += j;
}
}
snprint(tmp+i, sizeof(tmp)-i, "%d%%", 100);
w += stringwidth(f, tmp+i);
snprint(tmp+i, sizeof(tmp)-i, "%d%%", volume);
lockdisplay(display);
if(back == nil || Dx(screen->r) != Dx(back->r) || Dy(screen->r) != Dy(back->r)){
freeimage(back);
back = allocimage(display, Rpt(ZP,subpt(screen->r.max, screen->r.min)), XRGB32, 0, DNofill);
full = 1;
}
r = screen->r;
/* scrollbar */
p₀ = Pt(r.min.x, r.min.y);
p₁ = Pt(r.min.x+Scrollwidth, r.max.y-Seekthicc);
if(scroll < 1)
scrollcenter = 0;
else
scrollcenter = (p₁.y-p₀.y-Scrollheight/2 - Seekthicc)*scroll / (pl->n - scrollsz);
if(full || oscrollcenter != scrollcenter){
draw(screen, Rpt(p₀, Pt(p₁.x, p₁.y)), colors[Dback].im, nil, ZP);
line(screen, Pt(p₁.x, p₀.y), p₁, Endsquare, Endsquare, 0, colors[Dflow].im, ZP);
r = Rpt(
Pt(p₀.x+1, p₀.y + scrollcenter + Scrollheight/4),
Pt(p₁.x-1, p₀.y + scrollcenter + Scrollheight/4 + Scrollheight)
);
/* scrollbar handle */
draw(screen, r, colors[Dblow].im, nil, ZP);
}
/* seek bar rectangle */
r = Rpt(Pt(p₀.x, p₁.y), Pt(screen->r.max.x-w-4, screen->r.max.y));
/* playback/duration text */
draw(screen, Rpt(Pt(r.max.x, p₁.y), screen->r.max), colors[Dblow].im, nil, ZP);
p = addpt(Pt(screen->r.max.x - stringwidth(f, tmp) - 4, p₁.y), Pt(2, 2));
string(screen, p, colors[Dfhigh].im, ZP, f, tmp);
/* seek control */
if(pcurplaying >= 0 && dur > 0){
border(screen, r, 3, colors[Dblow].im, ZP);
r = insetrect(r, 3);
seekbar = r;
p = r.min;
x = p.x + Dx(r) * (double)msec / (double)dur;
r.min.x = x;
draw(screen, r, colors[Dback].im, nil, ZP);
r.min.x = p.x;
r.max.x = x;
draw(screen, r, colors[Dbmed].im, nil, ZP);
}else
draw(screen, r, colors[Dblow].im, nil, ZP);
Rectangle bp[2] = {
Rpt(addpt(screen->r.min, Pt(Scrollwidth+1, 0)), subpt(screen->r.max, Pt(0, Seekthicc))),
ZR,
};
if(cover != nil){
r.min.x = screen->r.max.x - Dx(cover->r) - 8;
r.min.y = p₁.y - Dy(cover->r) - 6;
r.max.x = screen->r.max.x;
r.max.y = p₁.y + 2;
if(full || cover != ocover){
border(screen, r, 4, colors[Dblow].im, ZP);
draw(screen, insetrect(r, 4), cover, nil, ZP);
}
bp[1] = bp[0];
bp[0].max.y = r.min.y;
bp[1].max.x = r.min.x;
bp[1].min.y = r.min.y;
}else if(ocover != nil)
full = 1;
/* playlist */
if(full || oscrollcenter != scrollcenter || pcur != opcur || pcurplaying != opcurplaying){
draw(back, back->r, colors[Dback].im, nil, ZP);
p.x = sp.x = Scrollwidth;
p.y = 0;
sp.y = back->r.max.y;
for(i = 0; cols[i+1] != 0; i++){
p.x += colwidth[i] + 4;
sp.x = p.x;
line(back, p, sp, Endsquare, Endsquare, 0, colors[Dflow].im, ZP);
p.x += 4;
}
sp.x = sp.y = 0;
p.x = Scrollwidth + 2;
p.y = back->r.min.y + 2;
for(i = scroll; i < pl->n; i++, p.y += f->height){
if(i < 0)
continue;
if(p.y > back->r.max.y)
break;
if(pcur == i){
sel.min.x = Scrollwidth;
sel.min.y = p.y;
sel.max.x = back->r.max.x;
sel.max.y = p.y + f->height;
replclipr(back, 0, back->r);
draw(back, sel, colors[Dbinv].im, nil, ZP);
col = colors[Dfinv].im;
}else{
col = colors[Dfmed].im;
}
sel = back->r;
p.x = Scrollwidth + 2 + 3;
for(j = 0; cols[j] != 0; j++){
sel.max.x = cols[j+1] ? (p.x + colwidth[j] - 1) : back->r.max.x;
replclipr(back, 0, sel);
if(playercurr != nil && playercurr->icytitle != nil && pcurplaying == i && cols[j] == Ptitle)
s = playercurr->icytitle;
else
s = getcol(getmeta(i), cols[j]);
string(back, p, col, sp, f, s);
p.x += colwidth[j] + 8;
}
if(pcurplaying == i){
Point rightp, leftp;
leftp.y = rightp.y = p.y - 1;
leftp.x = Scrollwidth;
rightp.x = back->r.max.x;
replclipr(back, 0, back->r);
line(back, leftp, rightp, 0, 0, 0, colors[Dflow].im, sp);
leftp.y = rightp.y = p.y + f->height;
line(back, leftp, rightp, 0, 0, 0, colors[Dflow].im, sp);
}
}
for(i = 0; bp[i].max.x ; i++)
draw(screen, bp[i], back, nil, subpt(bp[i].min, screen->r.min));
}
oscrollcenter = scrollcenter;
opcurplaying = pcurplaying;
ocover = cover;
opcur = pcur;
flushimage(display, 1);
unlockdisplay(display);
}
static void
redrawproc(void *)
{
ulong full, nbfull, another;
threadsetname("redraw");
while(recv(redrawc, &full) == 1){
Again:
redraw_(full);
another = 0;
full = 0;
while(nbrecv(redrawc, &nbfull) > 0){
full |= nbfull;
another = 1;
}
if(another)
goto Again;
}
threadexits(nil);
}
static void
redraw(int full)
{
sendul(redrawc, full);
}
static void
coverload(void *player_)
{
int p[2], pid, fd, i;
char *prog, *path, *s, tmp[32];
Meta *m;
Channel *ch;
Player *player;
Image *newcover;
threadsetname("cover");
player = player_;
m = getmeta(player->pcur);
pid = -1;
ch = player->img;
fd = -1;
prog = nil;
if(m->imagefmt != nil && m->imagereader == 0){
if(strcmp(m->imagefmt, "image/png") == 0)
prog = "png";
else if(strcmp(m->imagefmt, "image/jpeg") == 0)
prog = "jpg";
}
if(prog == nil){
path = strdup(m->path);
if(path != nil && (s = utfrrune(path, '/')) != nil){
*s = 0;
for(i = 0; i < nelem(covers) && prog == nil; i++){
if((s = smprint("%s/%s.jpg", path, covers[i])) != nil && (fd = open(s, OREAD|OCEXEC)) >= 0)
prog = "jpg";
free(s);
s = nil;
if(fd < 0 && (s = smprint("%s/%s.png", path, covers[i])) != nil && (fd = open(s, OREAD|OCEXEC)) >= 0)
prog = "png";
free(s);
}
}
free(path);
}
if(prog == nil)
goto done;
if(fd < 0){
fd = open(m->path, OREAD|OCEXEC);
seek(fd, m->imageoffset, 0);
}
pipe(p);
if((pid = rfork(RFPROC|RFFDG|RFNOTEG|RFCENVG|RFNOWAIT)) == 0){
dup(fd, 0); close(fd);
dup(p[1], 1); close(p[1]);
if(!debug){
dup(fd = open("/dev/null", OWRITE), 2);
close(fd);
}
snprint(tmp, sizeof(tmp), "%s -9t | resample -x%d", prog, Coversz);
execl("/bin/rc", "rc", "-c", tmp, nil);
sysfatal("execl: %r");
}
close(fd);
close(p[1]);
if(pid > 0){
newcover = readimage(display, p[0], 1);
sendp(ch, newcover);
}
close(p[0]);
done:
if(pid < 0)
sendp(ch, nil);
chanclose(ch);
chanfree(ch);
postnote(PNGROUP, pid, "die");
threadexits(nil);
}
static int
playerret(Player *player)
{
return recvul(player->ev) == Everror ? -1 : 0;
}
static void
pnotify(Player *p)
{
Meta *m;
int i;
if(!pnotifies)
return;
if(p != nil){
m = getmeta(p->pcur);
for(i = 0; cols[i] != 0; i++)
Bprint(&out, "%s\t", getcol(m, cols[i]));
}
Bprint(&out, "\n");
Bflush(&out);
}
static void
stop(Player *player)
{
if(player == nil)
return;
if(player == playernext)
playernext = nil;
if(!getmeta(player->pcur)->filefmt[0])
playerret(player);
if(player == playercurr)
pnotify(nil);
sendul(player->ctl, Cstop);
}
static void
start(Player *player)
{
if(player == nil)
return;
if(!getmeta(player->pcur)->filefmt[0])
playerret(player);
pnotify(player);
sendul(player->ctl, Cstart);
}
static void playerthread(void *player_);
static void
setgain(Player *player)
{
if(player == nil)
return;
if(rg == Rgdisabled)
player->gain = 0.0;
if(rg == Rgtrack)
player->gain = getmeta(player->pcur)->rgtrack;
else if(rg == Rgalbum)
player->gain = getmeta(player->pcur)->rgalbum;
player->gain = pow(10.0, player->gain/20.0);
}
static Player *
newplayer(int pcur, int loadnext)
{
Player *player;
if(playernext != nil && loadnext){
if(pcur == playernext->pcur){
player = playernext;
playernext = nil;
goto done;
}
stop(playernext);
playernext = nil;
}
player = mallocz(sizeof(*player), 1);
player->ctl = chancreate(sizeof(ulong), 0);
player->ev = chancreate(sizeof(ulong), 0);
player->pcur = pcur;
setgain(player);
threadcreate(playerthread, player, 32768);
if(getmeta(pcur)->filefmt[0] && playerret(player) < 0)
return nil;
done:
if(pcur < pl->n-1 && playernext == nil && loadnext)
playernext = newplayer(pcur+1, 0);
return player;
}
static long
iosetname(va_list *)
{
procsetname("player/io");
return 0;
}
static void
gain(double g, char *buf, long n)
{
s16int *f;
if(g != 1.0)
for(f = (s16int*)buf; n >= 4; n -= 4){
*f = g * *f++;
*f = g * *f++;
}
}
static void
playerthread(void *player_)
{
char *buf, cmd[64], seekpos[12], *fmt, *path, *icytitle;
Player *player;
Ioproc *io;
Image *thiscover;
ulong c;
int p[2], q[2], fd, pid, noinit, trycoverload;
long n, r;
vlong boffset, boffsetlast;
Meta *cur;
threadsetname("player");
player = player_;
noinit = 0;
boffset = 0;
buf = nil;
trycoverload = 1;
io = nil;
restart:
cur = getmeta(player->pcur);
fmt = cur->filefmt;
path = cur->path;
fd = -1;
q[0] = -1;
pid = -1;
if(*fmt){
if((fd = open(cur->path, OREAD)) < 0){
fprint(2, "%r\n");
sendul(player->ev, Everror);
chanclose(player->ev);
goto freeplayer;
}
}else{
sendul(player->ev, Evready);
chanclose(player->ev);
if(strncmp(cur->path, "http://", 7) == 0){ /* try icy */
pipe(q);
if(icyget(cur, q[0], &player->icytitlec) == 0){
fd = q[1];
path = nil;
}else{
close(q[0]); q[0] = -1;
close(q[1]);
}
}
}
pipe(p);
if((pid = rfork(RFPROC|RFFDG|RFNOTEG|RFCENVG|RFNOWAIT)) == 0){
close(q[0]);
close(p[1]);
if(fd < 0)
fd = open("/dev/null", OREAD);
dup(fd, 0); close(fd); /* fd == q[1] when it's Icy */
dup(p[0], 1); close(p[0]);
if(!debug){
dup(fd = open("/dev/null", OWRITE), 2);
close(fd);
}
if(*fmt){
snprint(cmd, sizeof(cmd), "/bin/audio/%sdec", fmt);
snprint(seekpos, sizeof(seekpos), "%g", (double)boffset/Bps);
execl(cmd, cmd, boffset ? "-s" : nil, seekpos, nil);
}else{
execl("/bin/play", "play", "-o", "/fd/1", path, nil);
}
close(0);
close(1);
exits("%r");
}
if(pid < 0)
sysfatal("rfork: %r");
/* fd is q[1] when it's Icy */
close(fd);
close(p[0]);
c = 0;
if(!noinit){
if(*fmt){
sendul(player->ev, Evready);
chanclose(player->ev);
}
buf = malloc(Relbufsz);
if((io = ioproc()) == nil)
sysfatal("player: %r");
iocall(io, iosetname);
if((n = ioreadn(io, p[1], buf, Relbufsz)) < 0)
fprint(2, "player: %r\n");
if(recv(player->ctl, &c) < 0 || c != Cstart)
goto freeplayer;
if(n < 1)
goto next;
audioon();
gain(player->gain, buf, n);
boffset = iowrite(io, audio, buf, n);
noinit = 1;
}
boffsetlast = boffset;
byteswritten = boffset;
pcurplaying = player->pcur;
redraw(1);
while(1){
n = ioread(io, p[1], buf, Relbufsz);
if(n <= 0){
if(repeatone){
c = Cseekrel;
boffset = 0;
}
break;
}
if(player->icytitlec != nil && nbrecv(player->icytitlec, &icytitle) != 0){
free(player->icytitle);
player->icytitle = icytitle;
redraw(1);
}
thiscover = nil;
if(player->img != nil && nbrecv(player->img, &thiscover) != 0){
freeimage(cover);
cover = thiscover;
redraw(0);
player->img = nil;
}
r = nbrecv(player->ctl, &c);
if(r < 0){
audiooff();
goto stop;
}else if(r != 0){
if(c == Ctoggle){
audiooff();
if(recv(player->ctl, &c) < 0)
goto stop;
}
if(c == Cseekrel && *fmt){
boffset = MAX(0, boffset + player->seek*Bps);
n = 0;
break;
}else if(c == Cstop){
audiooff();
goto stop;
}
}
boffset += n;
byteswritten = boffset;
audioon();
gain(player->gain, buf, n);
iowrite(io, audio, buf, n);
if(trycoverload){
trycoverload = 0;
player->img = chancreate(sizeof(Image*), 0);
proccreate(coverload, player, 4096);
}
if(labs(boffset/Relbufsz - boffsetlast/Relbufsz) > 0){
boffsetlast = boffset;
redraw(0);
}
}
if(n < 1){ /* seeking backwards or end of the song */
close(p[1]);
p[1] = -1;
postnote(PNGROUP, pid, "die");
if(c != Cseekrel || (getmeta(pcurplaying)->duration && boffset >= getmeta(pcurplaying)->duration/1000*Bps)){
next:
playercurr = nil;
playercurr = newplayer((player->pcur+1) % pl->n, 1);
start(playercurr);
goto stop;
}
goto restart;
}
stop:
if(player->img != nil)
freeimage(recvp(player->img));
freeplayer:
close(p[1]);
closeioproc(io);
postnote(PNGROUP, pid, "die");
if(player->icytitlec != nil){
while((icytitle = recvp(player->icytitlec)) != nil)
free(icytitle);
chanfree(player->icytitlec);
}
chanfree(player->ctl);
chanfree(player->ev);
if(player == playercurr)
playercurr = nil;
if(player == playernext)
playernext = nil;
free(buf);
free(player->icytitle);
free(player);
threadexits(nil);
}
static int
toggle(Player *player)
{
return (player != nil && sendul(player->ctl, Ctoggle) == 1) ? 0 : -1;
}
static void
seekrel(Player *player, double off)
{
if(player != nil){
player->seek = off;
sendul(player->ctl, Cseekrel);
}
}
static void
freeplist(Playlist *pl)
{
if(pl != nil){
free(pl->m);
free(pl->raw);
}
free(pl);
}
static char *
readall(int f)
{
int bufsz, sz, n;
char *s;
bufsz = 1023;
s = nil;
for(sz = 0;; sz += n){
if(bufsz-sz < 1024){
bufsz *= 2;
s = realloc(s, bufsz);
}
if((n = readn(f, s+sz, bufsz-sz-1)) < 1)
break;
}
if(n < 0 || sz < 1){
if(n == 0)
werrstr("empty");
free(s);
return nil;
}
s[sz] = 0;
return s;
}
static int
cmpint(void *a, void *b)
{
return *(int*)a - *(int*)b;
}
static Playlist *
readplist(int fd, int mincolwidth[Ncol])
{
char *raw, *s, *e, *a[5], *b;
int plsz, i, *w;
Playlist *pl;
Meta *m;
if((raw = readall(fd)) == nil)
return nil;
plsz = 0;
for(s = raw; (s = strchr(s, '\n')) != nil; s++){
if(*(++s) == '\n')
plsz++;
}
if((pl = calloc(1, sizeof(*pl))) == nil || (pl->m = calloc(plsz+1, sizeof(Meta))) == nil){
freeplist(pl);
werrstr("no memory");
return nil;
}
pl->raw = raw;
for(s = pl->raw, m = pl->m, e = s; e != nil; s = e){
if((e = strchr(s, '\n')) == nil)
goto addit;
s += 2;
*e++ = 0;
switch(s[-2]){
case 0:
addit:
if(m->path != nil){
if(m->filefmt == nil)
m->filefmt = "";
pl->n++;
m++;
}
break;
case Pimage:
if(tokenize(s, a, nelem(a)) >= 4){
m->imageoffset = atoi(a[0]);
m->imagesize = atoi(a[1]);
m->imagereader = atoi(a[2]);
m->imagefmt = a[3];
}
break;
case Pduration:
m->duration = strtoull(s, nil, 0);
break;
case Partist:
if(m->numartist < Maxartist)
m->artist[m->numartist++] = s;
break;
case Pfilefmt: m->filefmt = s; break;
case Palbum: m->album = s; break;
case Pdate: m->date = s; break;
case Ptitle: m->title = s; break;
case Ptrack: m->track = s; break;
case Prgtrack: m->rgtrack = atof(s); break;
case Prgalbum: m->rgalbum = atof(s); break;
case Ppath:
m->path = s;
m->basename = (b = utfrrune(s, '/')) == nil ? s : b+1;
break;
}
}
w = malloc(sizeof(int)*pl->n);
for(i = 0; cols[i] != 0; i++){
for(m = pl->m; m != pl->m + pl->n; m++)
w[m - pl->m] = stringwidth(f, getcol(m, cols[i]));
qsort(w, pl->n, sizeof(*w), cmpint);
mincolwidth[i] = w[93*(pl->n-1)/100];
}
free(w);
return pl;
}
static void
recenter(void)
{
updatescrollsz();
scroll = pcur - scrollsz/2 + 1;
}
static void
seekto(char *s)
{
vlong p;
char *e;
for(p = 0; *s; s = e){
p += strtoll(s, &e, 10);
if(s == e)
break;
if(*e == ':'){
p *= 60;
e++;
}
}
seekrel(playercurr, p - byteswritten/Bps);
}
static void
search(char d)
{
Meta *m;
static char buf[64];
static int sz;
int inc, i, a, cycle;
inc = (d == '/' || d == 'n') ? 1 : -1;
if(d == '/' || d == '?')
sz = enter(inc > 0 ? "forward:" : "backward:", buf, sizeof(buf), mctl, &kctl, screen->screen);
if(sz < 1){
redraw(1);
return;
}
cycle = 1;
for(i = pcur+inc; i >= 0 && i < pl->n;){
m = getmeta(i);
for(a = 0; a < m->numartist; a++){
if(cistrstr(m->artist[a], buf) != nil)
break;
}
if(a < m->numartist)
break;
if(m->album != nil && cistrstr(m->album, buf) != nil)
break;
if(m->title != nil && cistrstr(m->title, buf) != nil)
break;
if(cistrstr(m->path, buf) != nil)
break;
onemore:
i += inc;
}
if(i >= 0 && i < pl->n){
pcur = i;
recenter();
}else if(cycle && i+inc < 0){
cycle = 0;
i = pl->n;
goto onemore;
}else if(cycle && i+inc >= pl->n){
cycle = 0;
i = -1;
goto onemore;
}
redraw(1);
}
static void
toggleshuffle(void)
{
int i, m, xi, a, c, pcurnew, pcurplayingnew;
if(shuffle == nil){
if(pl->n < 2)
return;
m = pl->n;
if(pl->n < 4){
a = 1;
c = 3;
m = 7;
}else{
m += 1;
m |= m >> 1;
m |= m >> 2;
m |= m >> 4;
m |= m >> 8;
m |= m >> 16;
a = 1 + nrand(m/4)*4; /* 1 ≤ a < m && a mod 4 = 1 */
c = 3 + nrand((m-2)/2)*2; /* 3 ≤ c < m-1 && c mod 2 = 1 */
}
shuffle = malloc(pl->n*sizeof(*shuffle));
xi = pcurplaying < 0 ? pcur : pcurplaying;
pcurplayingnew = -1;
pcurnew = 0;
for(i = 0; i < pl->n;){
if(xi < pl->n){
if(pcur == xi)
pcurnew = i;
if(pcurplaying == xi)
pcurplayingnew = i;
shuffle[i++] = xi;
}
xi = (a*xi + c) & m;
}
pcur = pcurnew;
pcurplaying = pcurplayingnew;
}else{
pcur = shuffle[pcur];
if(pcurplaying >= 0)
pcurplaying = shuffle[pcurplaying];
free(shuffle);
shuffle = nil;
}
stop(playernext);
if(pcur < pl->n-1)
playernext = newplayer(pcur+1, 0);
}
static void
adjustcolumns(void)
{
int i, n, total, width;
total = 0;
n = 0;
width = Dx(screen->r);
for(i = 0; cols[i] != 0; i++){
if(cols[i] == Pduration || cols[i] == Pdate || cols[i] == Ptrack)
width -= mincolwidth[i] + 8;
else{
total += mincolwidth[i];
n++;
}
}
colspath = 0;
for(i = 0; cols[i] != 0; i++){
if(cols[i] == Ppath || cols[i] == Pbasename)
colspath = 1;
if(cols[i] == Pduration || cols[i] == Pdate || cols[i] == Ptrack)
colwidth[i] = mincolwidth[i];
else
colwidth[i] = (width - Scrollwidth - n*8) * mincolwidth[i] / total;
}
}
static void
plumbaudio(void *kbd)
{
int i, f, pf, mcw[Ncol], playing, shuffled;
Playlist *p;
Plumbmsg *m;
char *s, *e;
Rune c;
threadsetname("audio/plumb");
if((f = plumbopen("audio", OREAD)) >= 0){
while((m = plumbrecv(f)) != nil){
s = m->data;
if(strncmp(s, "key", 3) == 0 && isspace(s[3])){
for(s = s+4; isspace(*s); s++);
for(; (i = chartorune(&c, s)) > 0 && c != Runeerror; s += i)
sendul(kbd, c);
continue;
}
if(*s != '/' && m->wdir != nil)
s = smprint("%s/%.*s", m->wdir, m->ndata, m->data);
if((e = strrchr(s, '.')) != nil && strcmp(e, ".plist") == 0 && (pf = open(s, OREAD|OCEXEC)) >= 0){
p = readplist(pf, mcw);
close(pf);
if(p == nil)
continue;
playing = pcurplaying;
if(shuffled = (shuffle != nil))
sendul(kbd, 's');
/* make sure nothing is playing */
while(pcurplaying >= 0){
sendul(kbd, 'v');
sleep(100);
}
freeplist(pl);
pl = p;
memmove(mincolwidth, mcw, sizeof(mincolwidth));
adjustcolumns();
pcur = 0;
if(shuffled){
pcur = nrand(pl->n);
sendul(kbd, 's');
}
redraw(1);
if(playing >= 0)
sendul(kbd, '\n');
}else{
for(i = 0; i < pl->n; i++){
if(strcmp(pl->m[i].path, s) == 0){
sendul(playc, i);
break;
}
}
}
if(s != m->data)
free(s);
plumbfree(m);
}
}
threadexits(nil);
}
static void
kbproc(void *cchan)
{
char *s, buf[128], buf2[128];
int kbd, n;
Rune r;
threadsetname("kbproc");
if((kbd = open("/dev/kbd", OREAD|OCEXEC)) < 0)
sysfatal("/dev/kbd: %r");
buf2[0] = 0;
buf2[1] = 0;
buf[0] = 0;
for(;;){
if(buf[0] != 0){
n = strlen(buf)+1;
memmove(buf, buf+n, sizeof(buf)-n);
}
if(buf[0] == 0){
n = read(kbd, buf, sizeof(buf)-1);
if(n <= 0)
break;
buf[n-1] = 0;
buf[n] = 0;
}
switch(buf[0]){
case 'k':
for(s = buf+1; *s;){
s += chartorune(&r, s);
if(utfrune(buf2+1, r) == nil){
if(r == Kshift)
shiftdown = 1;
}
}
break;
case 'K':
for(s = buf2+1; *s;){
s += chartorune(&r, s);
if(utfrune(buf+1, r) == nil){
if(r == Kshift)
shiftdown = 0;
}
}
break;
case 'c':
if(chartorune(&r, buf+1) > 0 && r != Runeerror)
nbsend(cchan, &r);
default:
continue;
}
strcpy(buf2, buf);
}
close(kbd);
threadexits(nil);
}
static void
usage(void)
{
fprint(2, "usage: %s [-s] [-c aAdDtTp]\n", argv0);
sysfatal("usage");
}
void
threadmain(int argc, char **argv)
{
Rune key;
Mouse m;
ulong ind;
enum {
Emouse,
Eresize,
Ekey,
Eplay,
};
Alt a[] = {
{ nil, &m, CHANRCV },
{ nil, nil, CHANRCV },
{ nil, &key, CHANRCV },
{ nil, &ind, CHANRCV },
{ nil, nil, CHANEND },
};
int n, scrolling, oldpcur, oldbuttons, pnew, shuffled;
int seekmx, full;
char buf[64];
shuffled = 0;
ARGBEGIN{
case 'd':
debug++;
break;
case 's':
shuffled = 1;
break;
case 'c':
cols = EARGF(usage());
if(strlen(cols) >= nelem(colwidth))
sysfatal("max %d columns allowed", nelem(colwidth));
break;
default:
usage();
break;
}ARGEND;
Binit(&out, 1, OWRITE);
pnotifies = fd2path(1, buf, sizeof(buf)) == 0 && strcmp(buf, "/dev/cons") != 0;
if(initdraw(nil, nil, "zuke") < 0)
sysfatal("initdraw: %r");
unlockdisplay(display);
display->locking = 1;
f = display->defaultfont;
Scrollwidth = MAX(14, stringwidth(f, "#"));
Scrollheight = MAX(16, f->height);
Seekthicc = Scrollheight + 2;
Coversz = MAX(64, stringwidth(f, "¹∫ 00:00:00/00:00:00 100%"));
if((mctl = initmouse(nil, screen)) == nil)
sysfatal("initmouse: %r");
kctl.c = chancreate(sizeof(Rune), 20);
proccreate(kbproc, kctl.c, 4096);
playc = chancreate(sizeof(ind), 0);
a[Emouse].c = mctl->c;
a[Eresize].c = mctl->resizec;
a[Ekey].c = kctl.c;
a[Eplay].c = playc;
redrawc = chancreate(sizeof(ulong), 8);
proccreate(redrawproc, nil, 8192);
for(n = 0; n < Numcolors; n++)
colors[n].im = allocimage(display, Rect(0,0,1,1), XRGB32, 1, colors[n].rgb<<8 | 0xff);
srand(time(0));
pcurplaying = -1;
chvolume(0);
fmtinstall('P', positionfmt);
threadsetname("zuke");
if((pl = readplist(0, mincolwidth)) == nil){
fprint(2, "playlist: %r\n");
sysfatal("playlist error");
}
close(0);
m.buttons = 0;
scrolling = 0;
seekmx = 0;
adjustcolumns();
proccreate(plumbaudio, kctl.c, 4096);
if(shuffled){
pcur = nrand(pl->n);
toggleshuffle();
}
full = 1;
for(;;){
updatescrollsz();
scroll = CLAMP(scroll, 0, pl->n - scrollsz);
redraw(full);
oldpcur = pcur;
full = 0;
if(seekmx != newseekmx){
seekmx = newseekmx;
redraw(0);
}
oldbuttons = m.buttons;
switch(alt(a)){
case Emouse:
if(ptinrect(m.xy, seekbar)){
seekoff = getmeta(pcurplaying)->duration * (double)(m.xy.x-1-seekbar.min.x) / (double)Dx(seekbar);
if(seekoff < 0)
seekoff = 0;
newseekmx = m.xy.x;
}else{
newseekmx = -1;
}
if(oldbuttons == m.buttons && m.buttons == 0)
continue;
if(m.buttons != 2)
scrolling = 0;
if(m.buttons == 0)
break;
if(m.buttons == 8){
scroll -= (shiftdown ? 0 : scrollsz/4)+1;
break;
}else if(m.buttons == 16){
scroll += (shiftdown ? 0 : scrollsz/4)+1;
break;
}
n = (m.xy.y - screen->r.min.y)/f->height;
if(m.xy.x <= screen->r.min.x+Scrollwidth && m.xy.y <= screen->r.max.y-Seekthicc){
if(m.buttons == 1){
scroll -= n+1;
break;
}else if(m.buttons == 4){
scroll += n+1;
break;
}else if(m.buttons == 2){
scrolling = 1;
}
}
if(!scrolling && ptinrect(m.xy, insetrect(seekbar, -4))){
if(ptinrect(m.xy, seekbar))
seekrel(playercurr, seekoff/1000.0 - byteswritten/Bps);
break;
}
if(scrolling){
if(scrollsz >= pl->n)
break;
scroll = (m.xy.y - screen->r.min.y)*(pl->n-scrollsz) / (Dy(screen->r)-Seekthicc);
}else if(m.buttons == 1 || m.buttons == 2){
n += scroll;
if(n < pl->n){
pcur = n;
if(m.buttons == 2 && oldbuttons == 0){
stop(playercurr);
playercurr = newplayer(pcur, 1);
start(playercurr);
}
}
}
break;
case Eresize: /* resize */
if(getwindow(display, Refnone) < 0)
sysfatal("getwindow: %r");
adjustcolumns();
redraw(1);
break;
case Ekey:
switch(key){
default:
if(isdigit(key) && pcurplaying >= 0 && getmeta(pcurplaying)->duration > 0){
buf[0] = key;
buf[1] = 0;
if(enter("seek:", buf, sizeof(buf), mctl, &kctl, screen->screen) < 1)
redraw(1);
else
seekto(buf);
}
break;
case Kleft:
seekrel(playercurr, -(double)Seek);
break;
case Kright:
seekrel(playercurr, Seek);
break;
case ',':
seekrel(playercurr, -(double)Seekfast);
break;
case '.':
seekrel(playercurr, Seekfast);
break;
case Kup:
pcur--;
break;
case Kpgup:
pcur -= scrollsz;
break;
case Kdown:
pcur++;
break;
case Kpgdown:
pcur += scrollsz;
break;
case Kend:
pcur = pl->n-1;
scroll = pl->n-scrollsz;
break;
case Khome:
pcur = 0;
break;
case '\n':
playcur:
stop(playercurr);
playercurr = newplayer(pcur, 1);
start(playercurr);
break;
case 'q':
case Kdel:
stop(playercurr);
stop(playernext);
threadexitsall(nil);
case 'i':
case 'o':
if(pcur == pcurplaying)
oldpcur = -1;
pcur = pcurplaying;
recenter();
break;
case 'b':
case '>':
if(playercurr == nil)
break;
pnew = pcurplaying;
if(++pnew >= pl->n)
pnew = 0;
stop(playercurr);
playercurr = newplayer(pnew, 1);
start(playercurr);
break;
case 'z':
case '<':
if(playercurr == nil)
break;
pnew = pcurplaying;
if(--pnew < 0)
pnew = pl->n-1;
stop(playercurr);
playercurr = newplayer(pnew, 1);
start(playercurr);
break;
case '-':
chvolume(-1);
redraw(0);
continue;
case '+':
case '=':
chvolume(+1);
redraw(0);
continue;
case 'v':
stop(playercurr);
stop(playernext);
playercurr = nil;
playernext = nil;
pcurplaying = -1;
freeimage(cover);
cover = nil;
full = 1;
break;
case 'g':
rg = (rg+1) % Numrg;
setgain(playercurr);
setgain(playernext);
redraw(0);
break;
case 's':
toggleshuffle();
recenter();
full = 1;
break;
case 'r':
repeatone ^= 1;
redraw(0);
break;
case 'c':
case 'p':
case ' ':
if(toggle(playercurr) != 0)
goto playcur;
break;
case '/':
case '?':
case 'n':
case 'N':
search(key);
break;
}
break;
case Eplay:
pcur = ind;
recenter();
if(playercurr != nil)
goto playcur;
break;
}
if(pcur != oldpcur){
pcur = CLAMP(pcur, 0, pl->n-1);
if(pcur < scroll)
scroll = pcur;
else if(pcur > scroll + scrollsz)
scroll = pcur - scrollsz;
}
}
}