shithub: zuke

ref: 6f58415a9781a09a52c40a6b1f1f3bd88cfaedc1
dir: /zuke.c/

View raw version
#include <u.h>
#include <libc.h>
#include <draw.h>
#include <mouse.h>
#include <keyboard.h>
#include <thread.h>
#include <ctype.h>
#include "plist.h"

typedef struct Player Player;

enum
{
	Cstart = 1,
	Cstop,
	Ctoggle,
	Cforward,
	Cbackward,
	Cforwardfast,
	Cbackwardfast,

	Everror = 1,
	Evready,

	Bps = 44100*2*2, /* 44100KHz, stereo, u16 for a sample */
	Seekbytes = Bps*10, /* 10 seconds */
	Seekbytesfast = Bps*60, /* 1 minute */

	Scrollwidth = 12,
	Scrollheight = 16,

	Relbufsz = Bps/5, /* 0.2 second */
};

static Meta *pl;
static int plnum;
static char *plraw;
static int plrawsize;

struct Player
{
	Channel *ctl;
	Channel *ev;
	Channel *img;
	int pcur;
};

int mainstacksize = 32768;

static int audio;
static u64int byteswritten;
static int pcur, pcurplaying;
static int scroll, scrollsz;
static Image *cola, *colb;
static Font *f;
static Image *cover;
static Channel *ev;
static Mousectl *mctl;
static Keyboardctl *kctl;
static int entering;
static int colwidth[3];
static int mincolwidth[3];

#pragma varargck type "P" int
static int
positionfmt(Fmt *f)
{
	char *s, tmp[16];
	u64int sec;

	s = tmp;
	sec = va_arg(f->args, int);
	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 void
adjustcolumns(void)
{
	int i, x, total;

	if(mincolwidth[0] == 0){
		mincolwidth[0] = mincolwidth[1] = mincolwidth[2] = 1;
		for(i = 0; i < plnum; i++){
			if((x = stringwidth(f, pl[i].artist[0])) > mincolwidth[0])
				mincolwidth[0] = x;
			if((x = stringwidth(f, pl[i].album)) > mincolwidth[1])
				mincolwidth[1] = x;
			if((x = stringwidth(f, pl[i].title)) > mincolwidth[2])
				mincolwidth[2] = x;
		}
	}

	total = mincolwidth[0] + mincolwidth[1] + mincolwidth[2];
	for(i = 0; i < nelem(mincolwidth); i++)
		colwidth[i] = (Dx(screen->r) - 8) * mincolwidth[i] / total;
}

static void
redraw(Image *screen, int new)
{
	Image *col;
	Point p, sp;
	Rectangle sel, r;
	int i, left, scrollcenter;
	char tmp[32];

	if(entering)
		return;

	lockdisplay(display);
	if(new && getwindow(display, Refnone) < 0)
		sysfatal("getwindow: %r");
	else
		draw(screen, screen->r, cola, nil, ZP);

	scrollsz = Dy(screen->r) / f->height - 1;
	adjustcolumns();
	left = screen->r.min.x;
	if(scrollsz < plnum){ /* add a scrollbar */
		p.x = sp.x = screen->r.min.x + Scrollwidth;
		p.y = screen->r.min.y;
		sp.y = screen->r.max.y;
		line(screen, p, sp, Endsquare, Endsquare, 0, colb, ZP);

		r = screen->r;
		r.max.x = r.min.x + Scrollwidth - 1;
		r.min.x += 1;
		if(scroll < 1)
			scrollcenter = 0;
		else
			scrollcenter = (Dy(screen->r)-Scrollheight/2)*scroll / plnum;
		r.min.y += scrollcenter + Scrollheight/4;
		r.max.y = r.min.y + Scrollheight;
		draw(screen, r, colb, nil, ZP);

		left += Scrollwidth + 4;
	}

	p.x = sp.x = left + colwidth[0] + 4;
	p.y = 0;
	sp.y = screen->r.max.y;
	line(screen, p, sp, Endsquare, Endsquare, 0, colb, ZP);

	p.x = sp.x = left + colwidth[0] + 8 + colwidth[1] + 4;
	p.y = 0;
	sp.y = screen->r.max.y;
	line(screen, p, sp, Endsquare, Endsquare, 0, colb, ZP);

	sp.x = sp.y = 0;
	p.x = left + 2;
	p.y = screen->r.min.y + 2;

	for(i = scroll; i < plnum; i++, p.y += f->height){
		if(i < 0)
			continue;
		if(p.y > screen->r.max.y)
			break;

		if(pcur == i){
			sel.min.x = left;
			sel.min.y = p.y;
			sel.max.x = screen->r.max.x;
			sel.max.y = p.y + f->height;
			draw(screen, sel, colb, nil, ZP);
			col = cola;
		}else{
			col = colb;
		}

		sel = screen->r;
		r = screen->r;

		p.x = left + 2;
		sel.max.x = p.x + colwidth[0];
		replclipr(screen, 0, sel);
		string(screen, p, col, sp, f, pl[i].artist[0]);
		p.x += colwidth[0] + 8;
		sel.min.x = p.x;
		sel.max.x = p.x + colwidth[1];
		replclipr(screen, 0, sel);
		string(screen, p, col, sp, f, pl[i].album);
		p.x += colwidth[1] + 8;
		sel.min.x = p.x;
		sel.max.x = p.x + colwidth[2];
		replclipr(screen, 0, sel);
		string(screen, p, col, sp, f, pl[i].title);

		replclipr(screen, 0, r);

		if(pcurplaying == i){
			Point rightp, leftp;
			leftp.y = rightp.y = p.y - 1;
			leftp.x = left;
			rightp.x = screen->r.max.x;
			line(screen, leftp, rightp, 0, 0, 0, colb, sp);
			leftp.y = rightp.y = p.y + f->height;
			line(screen, leftp, rightp, 0, 0, 0, colb, sp);
		}
	}

	if(cover != nil){
		r = screen->r;
		r.min.x = r.max.x - cover->r.max.x - 8;
		r.min.y = r.max.y - cover->r.max.y - 8 - f->height - 4;
		draw(screen, r, display->black, nil, ZP);
		r.min.x += 4;
		r.min.y += 4;
		r.max.x -= 4;
		r.max.y -= 4;
		draw(screen, r, cover, nil, ZP);
	}

	if(pcurplaying >= 0){
		snprint(tmp, sizeof(tmp), "%P/%P", (int)(byteswritten/Bps), pl[pcurplaying].duration/1000);
		r = screen->r;
		r.min.x = r.max.x - stringwidth(f, tmp) - 4;
		r.min.y = r.max.y - f->height - 4;
		draw(screen, r, display->black, nil, ZP);
		r.min.x += 2;
		r.min.y += 2;
		string(screen, r.min, cola, sp, f, tmp);
	}

	flushimage(display, 1);
	unlockdisplay(display);
}

static void
coverload(void *player_)
{
	int p[2], pid, fd;
	char *prog, *path, *s, tmp[32];
	Meta *m;
	Channel *ch;
	Player *player;
	Image *newcover;

	threadsetname("cover");
	player = player_;
	m = &pl[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;

			if((s = smprint("%s/cover.jpg", path)) != nil && (fd = open(s, OREAD)) >= 0)
				prog = "jpg";
			free(s);
			s = nil;

			if(fd < 0 && (s = smprint("%s/cover.png", path)) != nil && (fd = open(s, OREAD)) >= 0)
				prog = "png";
			free(s);
		}
		free(path);
	}

	if(prog == nil)
		goto done;

	if(fd < 0){
		fd = open(m->path, OREAD);
		seek(fd, m->imageoffset, 0);
	}
	pipe(p);
	if((pid = rfork(RFPROC|RFFDG|RFREND|RFNOTEG)) == 0){
		dup(fd, 0); close(fd);
		dup(p[1], 1); close(p[1]);
		dup(open("/dev/null", OWRITE), 2);
		snprint(tmp, sizeof(tmp), "%s -9t | resample -x128", prog);
		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);
	if(pid >= 0)
		postnote(PNGROUP, pid, "interrupt");
	threadexits(nil);
}

static Player *playernext;
static Player *playercurr;

static int
playerret(Player *player)
{
	return recvul(player->ev) == Everror ? -1 : 0;
}

static void
stop(Player *player)
{
	if(player == nil)
		return;

	if(player == playernext)
		playernext = nil;
	sendul(player->ctl, Cstop);
}

static void playerthread(void *player_);

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;

	threadcreate(playerthread, player, mainstacksize);
	if(playerret(player) < 0)
		return nil;

done:
	if(pcur < plnum-1 && playernext == nil && loadnext)
		playernext = newplayer(pcur+1, 0);

	return player;
}

static int
start(Player *player)
{
	if(player != nil)
		sendul(player->ctl, Cstart);
	return -1;
}

static void
playerthread(void *player_)
{
	char *buf;
	Player *player;
	Ioproc *io;
	Image *thiscover;
	ulong c;
	int p[2], fd, pid, n, got, noinit, trycoverload;
	u64int bytesfrom;

	threadsetname("player");
	player = player_;
	noinit = 0;
	bytesfrom = 0;
	c = 0;
	buf = nil;
	trycoverload = 1;
	io = nil;
	pid = -1;

restart:
	if((fd = open(pl[player->pcur].path, OREAD)) < 0){
		fprint(2, "%r\n");
		sendul(player->ev, Everror);
		goto freeplayer;
	}

	pipe(p);
	if((pid = rfork(RFPROC|RFFDG|RFREND|RFNOTEG)) == 0){
		dup(p[0], 1); close(p[0]);
		dup(fd, 0); close(fd);
		close(p[1]);
		dup(open("/dev/null", OWRITE), 2);
		execl("/bin/play", "play", "-o", "/fd/1", nil);
		sysfatal("execl: %r");
	}
	if(pid < 0)
		sysfatal("rfork: %r");
	close(fd);
	close(p[0]);

	byteswritten = 0;
	if(!noinit){
		sendul(player->ev, Evready);
		buf = malloc(Relbufsz);
		io = ioproc();
		for(c = 0, got = 0; got < Relbufsz; got += n){
			if((c = nbrecvul(player->ctl)) != 0)
				break;
			n = ioread(io, p[1], buf+got, Relbufsz-got);
			if(n < 1)
				break;
		}
		if(c == 0)
			c = recvul(player->ctl);
		if(c != Cstart)
			goto freeplayer;
		iowrite(io, audio, buf, got);
		byteswritten = got;
		bytesfrom = 0;
		c = 0;
		noinit = 1;
	}

	pcurplaying = player->pcur;
	if(c != Cbackward && c != Cbackwardfast)
		redraw(screen, 0);

	while(1){
		n = Relbufsz;
		if(bytesfrom > byteswritten && n > bytesfrom-byteswritten)
			n = bytesfrom-byteswritten;
		n = ioread(io, p[1], buf, n);
		if(n < 1)
			break;

		thiscover = nil;
		if(player->img != nil && nbrecv(player->img, &thiscover) != 0){
			freeimage(cover);
			cover = thiscover;
			redraw(screen, 0);
			player->img = nil;
		}
		c = nbrecvul(player->ctl);
		if(c == Cstop || c == -1)
			goto stop;
		if(c == Ctoggle){
			c = recvul(player->ctl);
			if(c == Cstop)
				goto stop;
		}else if(c == Cforward){
			bytesfrom = byteswritten + Seekbytes;
		}else if(c == Cforwardfast){
			bytesfrom = byteswritten + Seekbytesfast;
		}else if(c == Cbackward){ /* to seek backwards we need to restart playback */
			bytesfrom = byteswritten >= Seekbytes ? byteswritten - Seekbytes : 0;
			n = 0; /* not an error */
			break;
		}else if(c == Cbackwardfast){
			bytesfrom = byteswritten >= Seekbytesfast ? byteswritten - Seekbytesfast : 0;
			n = 0; /* not an error */
			break;
		}

		c = 0;
		if(bytesfrom <= byteswritten){
			if(iowrite(io, audio, buf, n) != n){
				fprint(2, "failed to write %d bytes: %r\n", n);
				break;
			}
			if(trycoverload && byteswritten >= Bps){
				player->img = chancreate(sizeof(Image*), 0);
				proccreate(coverload, player, 4096);
				trycoverload = 0;
			}
		}
		byteswritten += n;
		if(bytesfrom == byteswritten || (byteswritten/Bps > (byteswritten-n)/Bps))
			redraw(screen, 0);
	}

	if(n == 0){ /* seeking backwards or end of the song */
		close(p[1]);
		if(c != Cbackward && c != Cbackwardfast){
			playercurr = nil;
			playercurr = newplayer((player->pcur+1) % plnum, 1);
			start(playercurr);
			goto stop;
		}
		goto restart;
	}

stop:
	if(player->img != nil)
		freeimage(recvp(player->img));
freeplayer:
	if(player == playercurr)
		playercurr = nil;
	if(player == playernext)
		playernext = nil;
	chanclose(player->ctl);
	chanclose(player->ev);
	chanfree(player->ctl);
	chanfree(player->ev);
	close(p[1]);
	closeioproc(io);
	free(buf);
	free(player);
	if(pid >= 0)
		postnote(PNGROUP, pid, "interrupt");
	threadexits(nil);
}

static void
toggle(Player *player)
{
	if(player != nil)
		sendul(player->ctl, Ctoggle);
}

static void
backward(Player *player)
{
	if(player != nil)
		sendul(player->ctl, Cbackward);
}

static void
forward(Player *player)
{
	if(player != nil)
		sendul(player->ctl, Cforward);
}

static void
backwardfast(Player *player)
{
	if(player != nil)
		sendul(player->ctl, Cbackwardfast);
}

static void
forwardfast(Player *player)
{
	if(player != nil)
		sendul(player->ctl, Cforwardfast);
}

static void
readplist(void)
{
	Meta *m;
	char *s, *e, *endrec;
	int i, n, sz, alloc, tagsz, intval;

	s = nil;
	for(alloc = sz = 0;;){
		alloc += 65536;
		if((s = realloc(s, alloc)) == nil)
			sysfatal("no memory");
		for(n = 0; sz < alloc; sz += n){
			n = read(0, s+sz, alloc-sz);
			if(n < 0)
				sysfatal("%r");
			if(n == 0)
				break;
		}
		if(n == 0)
			break;
	}

	plraw = s;
	plrawsize = sz;
	plraw[plrawsize-1] = 0;
	if(sz < 4 || s[0] != '#' || s[1] != ' ' || !isdigit(s[2]) || (s = memchr(plraw, '\n', sz)) == nil)
		sysfatal("invalid playlist");
	s++; /* at the start of the first record */

	plnum = atoi(plraw+2);
	pl = calloc(plnum, sizeof(Meta));
	for(i = 0; i < plnum; i++, s = endrec){
		if(plraw+plrawsize < s+10)
			sysfatal("truncated playlist");
		if(s[0] != '#' || s[1] != ' ' || !isdigit(s[2]))
			sysfatal("invalid record");
		if((n = strtol(s+2, &e, 10)) < 0 || n > plnum)
			sysfatal("invalid track index");
		if(pl[n].path != nil)
			sysfatal("duplicate track index");

		s[-1] = 0;
		sz = strtol(e, &s, 10);
		*s++ = 0; /* skip '\n' */
		if(s+sz > plraw+plrawsize)
			sysfatal("truncated playlist");
		s[sz-1] = 0; /* '\n'→'\0' to mark the end of the record */
		endrec = s+sz;
		m = &pl[n];

		for(;;){
			if(s[0] == Pimage){
				m->imageoffset = strtol(s+2, &e, 10);
				m->imagesize = strtol(e+1, &s, 10);
				m->imagereader = strtol(s+1, &e, 10);
				m->imagefmt = e + 1;
				s = strchr(e+2, '\n') + 1;
			}else if(s[0] == Pchannels || s[0] == Pduration || s[0] == Psamplerate){
				intval = strtol(s+2, &e, 10);
				if(s[0] == Pduration) m->duration = intval;
				s = e + 1;
			}else if(s[0] == Ppath){
				m->path = s+2;
				break; /* always the last one */
			}else{
				tagsz = strtol(s+1, &e, 10);
				if(e+tagsz >= plraw+plrawsize)
					sysfatal("truncated playlist");
				e++; /* point to tag value */
				e[tagsz] = 0; /* '\n'→'\0' to mark the end of the tag value */
				if(s[0] == Palbum) m->album = e;
				else if(s[0] == Partist && m->numartist < Maxartist) m->artist[m->numartist++] = e;
				else if(s[0] == Pdate) m->date = e;
				else if(s[0] == Ptitle) m->title = e;
				else if(s[0] == Pdate) m->date = e;
				else if(s[0] == Ptrack) m->track = e;
				else sysfatal("unknown tag type %c", s[0]);
				s = e + tagsz + 1;
			}
			s[-1] = 0;
		}
	}
}

static void
search(char d)
{
	char *s, *snext;
	static char buf[48];
	static int sz;
	int inc;

	inc = (d == '/' || d == 'n') ? 1 : -1;
	if(d == '/' || d == '?'){
		entering = 1;
		sz = enter(inc > 0 ? "forward:" : "backward:", buf, sizeof(buf), mctl, kctl, nil);
		entering = 0;
	}
	if(sz < 1 || (inc > 0 && pcur >= plnum-1) || (inc < 0 && pcur < 1))
		return;

	s = pl[pcur + (inc > 0 ? 0 : -1)].path;
	s += strlen(s) + 1;
	for(; s > plraw && s < plraw+plrawsize-sz; s += inc){
		if(cistrncmp(s, buf, sz) != 0)
			continue;
		snext = s;
		for(; s != plraw && *s; s--);
		if(s == plraw || (s[1] != Partist && s[1] != Palbum && s[1] != Ptitle && s[1] != Pdate)){
			if(inc > 0)
				s = snext;
			continue;
		}
		for(s--; s != plraw; s--){
			if(memcmp(s, "\0# ", 3) == 0 && isdigit(s[3])){
				pcur = atoi(s+3);
				redraw(screen, 1);
				return;
			}
		}
		break;
	}
}

static void
usage(void)
{
	fprint(2, "usage: zuke [-b]\n");
	exits("usage");
}

void
threadmain(int argc, char **argv)
{
	char tmp[256];
	Point lastclick;
	Rune key;
	Mouse m;
	Alt a[] =
	{
		{ nil, &m, CHANRCV },
		{ nil, nil, CHANRCV },
		{ nil, &key, CHANRCV },
		{ nil, nil, CHANEND },
	};
	int fd, scrolling, oldscroll, oldpcur, oldscrolling, usingscrollbar;

	USED(argc); USED(argv);

	audio = open("/dev/audio", OWRITE);
	if(audio < 0)
		sysfatal("audio: %r");

	readplist();
	if(plnum < 1){
		fprint(2, "empty playlist\n");
		sysfatal("empty");
	}

	if(initdraw(nil, nil, "zuke") < 0)
		sysfatal("initdraw: %r");
	unlockdisplay(display);
	if((mctl = initmouse(nil, screen)) == nil)
		sysfatal("initmouse: %r");
	if((kctl = initkeyboard(nil)) == nil)
		sysfatal("initkeyboard: %r");

	a[0].c = mctl->c;
	a[1].c = mctl->resizec;
	a[2].c = kctl->c;

	f = display->defaultfont;
	cola = display->white;
	colb = display->black;
	srand(time(0));
	pcurplaying = -1;
	scrolling = oldscroll = usingscrollbar = 0;
	fmtinstall('P', positionfmt);
	threadsetname("zuke");

	snprint(tmp, sizeof(tmp), "/proc/%d/ctl", getpid());
	if((fd = open(tmp, OWRITE)) >= 0){
		fprint(fd, "pri 13\n");
		close(fd);
	}

	redraw(screen, 1);

	for(;;){
		oldpcur = pcur;

		switch(alt(a)){
		case 0:
			oldscrolling = scrolling;
			scrolling = m.buttons & 2;
			if(!oldscrolling && (m.buttons & 2) != 0){
				usingscrollbar = m.xy.x < screen->r.min.x+Scrollwidth;
				lastclick = m.xy;
				oldscroll = scroll;
			}
			if(m.buttons == 0)
				break;

			if(scrolling){
				if(scrollsz >= plnum)
					break;
				if(usingscrollbar)
					scroll = (m.xy.y - screen->r.min.y - Scrollheight/4)*plnum / (Dy(screen->r)-Scrollheight/2);
				else
					scroll = oldscroll + (lastclick.y - m.xy.y) / f->height;
				if(scroll > plnum-scrollsz-1)
					scroll = plnum-scrollsz-1;
				if(scroll < 0)
					scroll = 0;
				redraw(screen, 0);
			}else{
				pcur = scroll + (m.xy.y - screen->r.min.y)/f->height;
				if(m.buttons == 4){
					stop(playercurr);
					playercurr = newplayer(pcur, 1);
					start(playercurr);
				}
			}
			break;
		case 1: /* resize */
			redraw(screen, 1);
			break;
		case 2:
			switch(key){
			case Kleft:
				backward(playercurr);
				break;
			case Kright:
				forward(playercurr);
				break;
			case ',':
				backwardfast(playercurr);
				break;
			case '.':
				forwardfast(playercurr);
				break;
			case Kup:
				pcur--;
				break;
			case Kpgup:
				pcur -= scrollsz;
				break;
			case Kdown:
				pcur++;
				break;
			case Kpgdown:
				pcur += scrollsz;
				break;
			case Kend:
				pcur = plnum-1;
				break;
			case Khome:
				pcur = 0;
				break;
			case 10:
				stop(playercurr);
				playercurr = newplayer(pcur, 1);
				start(playercurr);
				break;
			case 'q': case Kdel:
				stop(playercurr);
				goto end;
			case 'o':
				pcur = pcurplaying;
				break;
			case '>':
				if(playercurr == nil)
					break;
				pcur = pcurplaying;
				if(++pcur >= plnum)
					pcur = 0;
				stop(playercurr);
				playercurr = newplayer(pcur, 1);
				start(playercurr);
				break;
			case '<':
				if(playercurr == nil)
					break;
				pcur = pcurplaying;
				if(--pcur < 0)
					pcur = plnum-1;
				stop(playercurr);
				playercurr = newplayer(pcur, 1);
				start(playercurr);
				break;
			case 's':
				stop(playercurr);
				playercurr = nil;
				pcurplaying = -1;
				redraw(screen, 1);
				break;
			case 'p':
				toggle(playercurr);
				break;
			case '/': case '?': case 'n': case 'N':
				search(key);
				break;
			}
		}

		if(pcur != oldpcur){
			if(pcur < 0)
				pcur = 0;
			else if(pcur >= plnum)
				pcur = plnum - 1;

			if(pcur < scroll)
				scroll = pcur;
			else if(pcur > scroll + scrollsz)
				scroll = pcur - scrollsz;

			if(scroll > plnum - scrollsz)
				scroll = plnum - scrollsz;
			if(scroll < 0)
				scroll = 0;

			if(pcur != oldpcur)
				redraw(screen, 0);
		}
	}

end:
	closemouse(mctl);
	closekeyboard(kctl);
	threadexitsall(nil);
}