shithub: pplay

Download patch

ref: 38230eaac9e5abc55f7dce0f6fce0b9542a54258
parent: 55ae1f23a6430235143566fffd97bc7b8d11e4e5
author: qwx <qwx@sciops.net>
date: Mon Aug 28 16:36:26 EDT 2023

performance and clean up: split audio, drawing and sampling into individual procs

we don't want audio to cut up ever, but we want a steady stream of
screen updates regardless of how large the buffer or mouse actions,
and we want quick and responsive interface and drawing.

- split audio into an actual proc which just writes audio and fires
off updates, acting also like a timer
- split sampling from rendering by keeping sample min/max values in
memory and only updating those as necessary
- have rendering wait for a refresh request from audio proc, sampler
proc or user actions; try to keep as little state as possible; keep
responsiveness up by drawing stuff while the other procs are working
and don't block trying to gain control over locks; avoid overdrawing
by drawing final samples into a backbuffer, overlaid with static
markers; the cursor is erased and redrawn on every update

not quite bulletproof yet and still a bit racey.

--- a/cmd.c
+++ b/cmd.c
@@ -4,8 +4,9 @@
 #include "dat.h"
 #include "fns.h"
 
-usize ndots;
+usize ndots, ntracks;
 Dot *current, *dots;
+Track *tracks;
 
 static int epfd[2];
 
@@ -311,8 +312,11 @@
 
 	if(loadfile(fd, &d) == nil)
 		sysfatal("initcmd: %r");
+	tracks = emalloc(++ntracks * sizeof *tracks);
 	dots = emalloc(++ndots * sizeof *dots);
 	dots[0] = d;
 	current = dots;
+	*current = d;
+	current->t = tracks;
 	return 0;
 }
--- a/dat.h
+++ b/dat.h
@@ -1,6 +1,8 @@
 typedef struct Chunk Chunk;
 typedef struct Dot Dot;
 typedef struct Buf Buf;
+typedef struct Track Track;
+typedef struct Seg Seg;
 
 enum{
 	Rate = 44100,
@@ -18,18 +20,25 @@
 	Chunk *left;
 	Chunk *right;
 };
-extern struct Dot{
-	usize cur;
+struct Track{
+	vlong len;
+	s16int *graph[2];
+};
+struct Seg{
+	Track *t;
 	usize from;
 	usize to;
+};
+struct Dot{
+	Seg;
+	usize cur;
 	usize off;
 	usize totalsz;
 	Chunk *norris;
 };
 extern Dot *dots, *current;
-extern usize ndots;
-
-extern int treadsoftly;
+extern usize ndots, ntracks;
+extern Track *tracks;
 
 extern QLock lsync;
 
--- a/draw.c
+++ b/draw.c
@@ -20,14 +20,15 @@
 };
 static Image *col[Ncol];
 static Image *view;
-static Rectangle liner, statr;
-static usize views, viewe, viewmax;
+static Rectangle statr;
+static usize views, viewe, viewmax, linepos;
 static int bgscalyl, bgscalyr;
 static double bgscalf;
-static Channel *drawc;
+static Channel *upyours, *drawc;
 static usize T;
-static int sampwidth = 1;
+static int sampwidth = 1;	/* pixels per sample */
 static double zoom = 1.0;
+static int stalerender, working;
 
 static Image *
 eallocimage(Rectangle r, int repl, ulong col)
@@ -39,6 +40,18 @@
 	return i;
 }
 
+vlong
+ss2view(int x)
+{
+	return (x - views) / (T * sampwidth);
+}
+
+vlong
+view2ss(int x)
+{
+	return views + x * T * sampwidth & ~3;
+}
+
 static void
 b2t(usize off, int *th, int *tm, int *ts, int *tμ)
 {
@@ -68,21 +81,25 @@
 }
 
 static int
-drawpos(usize pos, Image *c)
+renderpos(usize ss, Image *c, int justdraw)
 {
 	Rectangle r;
 
-	if(pos <= views || pos >= viewe)
+	if(ss <= views || ss >= viewe)
 		return 0;
-	r = screen->r;
-	r.min.x += (pos - views) / T;
+	r = view->r;
+	r.min.x = ss2view(ss);
 	r.max.x = r.min.x + 1;
-	draw(screen, r, c, nil, subpt(r.min, screen->r.min));
+	if(justdraw){
+		r = rectaddpt(r, screen->r.min);
+		draw(screen, r, c, nil, ZP);
+	}else
+		draw(view, r, c, nil, ZP);
 	return 1;
 }
 
 static void
-drawchunks(void)
+renderchunks(void)
 {
 	usize p, off;
 	Chunk *c;
@@ -91,99 +108,72 @@
 	for(p=views-off; p<viewe; p+=c->len, c=c->right){
 		if(p == 0)
 			continue;
-		drawpos(p, col[Cchunk]);
+		renderpos(p, col[Cchunk], 0);
 	}
 }
 
 static void
-drawsamps(void*)
+rendermarks(void)
 {
-	int ox, n, lmin, lmax, rmin, rmax;
-	usize m, k;
-	s16int s;
-	double x;
-	uchar *p, *e;
-	Rectangle l, r;
-	Point range;
-	Dot d;
+	if(debugdraw)
+		renderchunks();
+	renderpos(current->from, col[Cloop], 0);
+	renderpos(current->to, col[Cloop], 0);
+	if(current->off != current->from)
+		renderpos(current->off, col[Cins], 0);
+}
 
-	for(;;){
-end:
-		if(recv(drawc, &range) < 0)
-			break;
-again:
-		r = view->r;
-		r.min.x = (range.x - views) / T;
-		r.max.x = (range.y - views) / T;
-		lockdisplay(display);
-		draw(view, r, col[Cbg], nil, ZP);
-		unlockdisplay(display);
-		d = *current;
-		d.from = range.x;
-		d.cur = d.from;
-		d.to = range.y;
-		m = d.to - d.from;
-		ox = 0;
-		x = 0.0;
-		qlock(&lsync);
-		while(m > 0){
-			if((n = nbrecv(drawc, &range)) < 0)
-				return;
-			else if(n == 1){
-				qunlock(&lsync);
-				goto again;
-			}
-			n = m < T * sampwidth ? m : T * sampwidth;
-			lmin = lmax = 0;
-			rmin = rmax = 0;
-			while(n > 0){
-				if((p = getslice(&d, n, &k)) == nil){
-					if(k > 0)
-						fprint(2, "getslice: %r\n");
-					goto end;
-				}
-				d.cur += k;
-				e = p + k;
-				while(p < e){
-					s = (s16int)(p[1] << 8 | p[0]);
-					if(s < lmin)
-						lmin = s;
-					else if(s > lmax)
-						lmax = s;
-					if(stereo){
-						s = (s16int)(p[3] << 8 | p[2]);
-						if(s < rmin)
-							rmin = s;
-						else if(s > rmax)
-							rmax = s;
-					}
-					p += 4 * sampwidth;
-				}
-				n -= k;
-				m -= k;
-			}
-			l = Rect(x, bgscalyl - lmax / bgscalf,
-				x+sampwidth, bgscalyl - lmin / bgscalf);
-			lockdisplay(display);
-			draw(view, l, col[Csamp], nil, ZP);
-			if(stereo){
-				r = Rect(x, bgscalyr - rmax / bgscalf,
-					x+sampwidth, bgscalyr - rmin / bgscalf);
-				draw(view, r, col[Csamp], nil, ZP);
-			}
-			unlockdisplay(display);
-			if(x - ox >= 1600){
-				update(ox, x);
-				ox = x;
-			}
-			x += k / T;
-		}
-		update(ox, Dx(screen->r));
-		qunlock(&lsync);
+static void
+rendersamples(Track *t, Rectangle rr)
+{
+	s16int *l, *e, *r;
+	Rectangle rx;
+
+	draw(view, rr, col[Cbg], nil, ZP);
+	if(Dx(rr) > t->len / 2)
+		rr.max.x = rr.min.x + t->len / 2;
+	rx = rr;
+	for(l=t->graph[0]+2*rx.min.x, e=l+2*Dx(rr); l<e; l+=2, rx.min.x++){
+		rx.min.y = bgscalyl - l[1] / bgscalf;
+		rx.max.x = rx.min.x + sampwidth;
+		rx.max.y = bgscalyl - l[0] / bgscalf;
+		draw(view, rx, col[Csamp], nil, ZP);
 	}
+	if(!stereo)
+		return;
+	rx = rr;
+	for(r=t->graph[1]+2*rx.min.x, e=r+2*Dx(rr); r<e; r+=2, rx.min.x++){
+		rx.min.y = bgscalyr - r[1] / bgscalf;
+		rx.max.x = rx.min.x + sampwidth;
+		rx.max.y = bgscalyr - r[0] / bgscalf;
+		draw(view, rx, col[Csamp], nil, ZP);
+	}
 }
 
 static void
+render(void)
+{
+	Track *t;
+
+	for(t=tracks; t<tracks+ntracks; t++)
+		rendersamples(t, view->r);
+	rendermarks();
+}
+
+static void
+erasemark(usize ss)
+{
+	Rectangle r;
+
+	ss = ss2view(ss);
+	r = view->r;
+	r.min.x = ss;
+	r.max.x = r.min.x + 1;
+	r = rectaddpt(r, screen->r.min);
+	draw(screen, r, view, nil, subpt(r.min, screen->r.min));
+}
+
+static void
 drawstat(void)
 {
 	char s[256];
@@ -204,36 +194,163 @@
 }
 
 static void
-drawmarks(void)
+update(void)
 {
-	if(debugdraw)
-		drawchunks();
-	drawpos(current->from, col[Cloop]);
-	drawpos(current->to, col[Cloop]);
-	if(current->off != current->from)
-		drawpos(current->from, col[Cins]);
+	lockdisplay(display);
+	if(stalerender || working){
+		if(!working)
+			stalerender = 0;
+		render();
+		draw(screen, rectaddpt(view->r, screen->r.min), view, nil, ZP);
+	}else
+		erasemark(linepos);
+	renderpos(current->cur, col[Cline], 1);
+	linepos = current->cur;
+	drawstat();
+	flushimage(display, 1);
+	unlockdisplay(display);
 }
 
+static void
+drawproc(void*)
+{
+	threadsetname("drawer");
+	for(;;){
+		if(recv(drawc, nil) < 0){
+			fprint(2, "drawproc: %r\n");
+			break;
+		}
+		update();
+	}
+}
+
 void
-update(int x, int x´)
+refresh(void)
 {
-	Rectangle r;
+	nbsend(drawc, nil);
+}
 
-	r = liner;
-	lockdisplay(display);
-	draw(screen, liner, view, nil, subpt(r.min, screen->r.min));
-	if(x < x´){
-		r.min.x = screen->r.min.x + x;
-		r.max.x = screen->r.min.x + x´;
-		draw(screen, r, view, nil, subpt(r.min, screen->r.min));
+static void
+sample(Dot d)
+{
+	int n, lmin, lmax, rmin, rmax;
+	usize k;
+	uchar *p, *e;
+	s16int s, *l, *r, *le;
+	vlong N;
+
+	N = (d.to - d.from) / (T * sampwidth);
+	if(d.t->len < 2*N){	/* min, max */
+		d.t->graph[0] = erealloc(d.t->graph[0],
+			2*N * sizeof *d.t->graph[0],
+			d.t->len * sizeof *d.t->graph[0]);
+		d.t->graph[1] = erealloc(d.t->graph[1],
+			2*N * sizeof *d.t->graph[1],
+			d.t->len * sizeof *d.t->graph[1]);
 	}
-	liner.min.x = screen->r.min.x + (current->cur - views) / T;
-	liner.max.x = liner.min.x + 1;
-	drawpos(current->cur, col[Cline]);
-	drawmarks();
-	drawstat();
-	flushimage(display, 1);
+	d.t->len = 2*N;
+	l = d.t->graph[0];
+	r = d.t->graph[1];
+	le = l + d.t->len;
+	while(l < le){
+		n = T * sampwidth;
+		lmin = lmax = rmin = rmax = 0;
+		while(n > 0){
+			if((p = getslice(&d, n, &k)) == nil){
+				if(k > 0)
+					fprint(2, "getslice: %r\n");
+				l = le;
+				break;
+			}
+			d.cur += k;
+			for(e=p+k; p<e; p+=Sampsz*sampwidth){
+				s = (s16int)(p[1] << 8 | p[0]);
+				if(s < lmin)
+					lmin = s;
+				else if(s > lmax)
+					lmax = s;
+				if(stereo){
+					s = (s16int)(p[3] << 8 | p[2]);
+					if(s < rmin)
+						rmin = s;
+					else if(s > rmax)
+						rmax = s;
+				}
+			}
+			n -= k;
+		}
+		*l++ = lmin;
+		*l++ = lmax;
+		if(stereo){
+			*r++ = rmin;
+			*r++ = rmax;
+		}
+		if(upyours->n > 0)
+			return;
+	}
+}
+
+static void
+sampleproc(void*)
+{
+	Dot d;
+
+	threadsetname("sampler");
+	for(;;){
+		if(recv(upyours, &d) < 0){
+			fprint(2, "sampproc: %r\n");
+			break;
+		}
+		working = 1;
+		stalerender = 1;
+		sample(d);
+		working = 0;
+	}
+}
+
+static void
+resetdraw(void)
+{
+	Rectangle viewr;
+
+	viewr = rectsubpt(screen->r, screen->r.min);
+	statr = screen->r;
+	if(stereo){
+		statr.min.y += (Dy(screen->r) - font->height) / 2 + 1;
+		statr.max.y = statr.min.y + font->height;
+	}else
+		statr.min.y = screen->r.max.y - font->height;
+	freeimage(view);
+	view = eallocimage(viewr, 0, DNofill);
+	bgscalyl = (viewr.max.y - font->height) / (stereo ? 4 : 2);
+	bgscalyr = viewr.max.y - bgscalyl;
+	bgscalf = 32767. / bgscalyl;
+}
+
+void
+redraw(int all)
+{
+	usize span;
+	Dot d;
+
+	lockdisplay(display);
+	T = (vlong)(current->totalsz / zoom / Dx(screen->r)) & ~3;
+	if(T < Sampsz)
+		T = Sampsz;
+	span = Dx(screen->r) * T;
+	viewmax = current->totalsz - span;
+	if(views > viewmax)
+		views = viewmax;
+	viewe = views + span;
+	if(all)
+		resetdraw();
 	unlockdisplay(display);
+	/* FIXME: this overloading is stupid; just fork for each? have multiple
+	 * workers to begin with à la page? */
+	d = *current;
+	d.from = d.cur = views;
+	d.to = viewe;
+	nbsend(upyours, &d);
 }
 
 void
@@ -304,19 +421,18 @@
 {
 	from &= ~3;
 	to &= ~3;
-	if(current->from > views)
-		drawpos(current->from, view);
-	if(current->to < viewe)
-		drawpos(current->to, view);
 	current->from = from;
 	current->to = to;
 	if(current->cur < from || current->cur >= to)
 		current->cur = from;
 	current->off = -1ULL;
+	stalerender = 1;
+	if(paused)
+		refresh();
 }
 
-static int
-setcur(usize off)
+int
+setjump(vlong off)
 {
 	off &= ~3;
 	if(off < current->from || off > current->to - Outsz){
@@ -324,6 +440,9 @@
 		return -1;
 	}
 	current->off = current->cur = off;
+	stalerender = 1;
+	if(paused)
+		refresh();
 	return 0;
 }
 
@@ -342,64 +461,7 @@
 	return 0;
 }
 
-int
-setjump(vlong off)
-{
-	return setcur(off) & ~3;
-}
-
-vlong
-p2off(int x)
-{
-	return views + x * T & ~3;
-}
-
-static void
-resetdraw(void)
-{
-	int x;
-	Rectangle viewr;
-
-	x = screen->r.min.x + (current->cur - views) / T;
-	viewr = rectsubpt(screen->r, screen->r.min);
-	statr = screen->r;
-	if(stereo)
-		statr.min.y += (Dy(screen->r) - font->height) / 2 + 1;
-	else
-		statr.min.y = screen->r.max.y - font->height;
-	freeimage(view);
-	view = eallocimage(viewr, 0, DNofill);
-	liner = screen->r;
-	liner.min.x = x;
-	liner.max.x = x + 1;
-	bgscalyl = (viewr.max.y - font->height) / (stereo ? 4 : 2);
-	bgscalyr = viewr.max.y - bgscalyl;
-	bgscalf = 32767. / bgscalyl;
-}
-
 void
-redraw(int all)
-{
-	usize span;
-	Point p;
-
-	lockdisplay(display);
-	T = (vlong)(current->totalsz / zoom / Dx(screen->r)) & ~3;
-	if(T == 0)
-		T = 4;
-	span = Dx(screen->r) * T;
-	viewmax = current->totalsz - span;
-	if(views > viewmax)
-		views = viewmax;
-	viewe = views + span;
-	if(all)
-		resetdraw();
-	unlockdisplay(display);
-	p = Pt(views, viewe);
-	nbsend(drawc, &p);
-}
-
-void
 initdrw(int fuckit)
 {
 	if(initdraw(nil, nil, "pplay") < 0)
@@ -423,9 +485,12 @@
 		col[Cloop] = eallocimage(Rect(0,0,1,1), 1, 0x8888CCFF);
 		col[Cchunk] = eallocimage(Rect(0,0,1,1), 1, 0xEE0000FF);
 	}
-	if((drawc = chancreate(sizeof(Point), 4)) == nil)
+	if((drawc = chancreate(sizeof(int), 1)) == nil
+	// FIXME: fudge until better perceptual fix
+	|| (upyours = chancreate(sizeof(Dot), 32)) == nil)
 		sysfatal("chancreate: %r");
-	if(proccreate(drawsamps, nil, mainstacksize) < 0)
-		sysfatal("proccreate: %r");
 	redraw(1);
+	if(proccreate(drawproc, nil, mainstacksize) < 0
+	|| proccreate(sampleproc, nil, mainstacksize) < 0)
+		sysfatal("proccreate: %r");
 }
--- a/fns.h
+++ b/fns.h
@@ -10,7 +10,7 @@
 Chunk*	loadfile(int, Dot*);
 int	cmd(char*);
 int	initcmd(int);
-void	update(int, int);
+void	refresh(void);
 void	setzoom(int, int);
 int	zoominto(vlong, vlong);
 void	setrange(usize, usize);
@@ -18,7 +18,8 @@
 void	setpan(int);
 void	setpage(int);
 int	setjump(vlong);
-vlong	p2off(int);
+vlong	ss2view(int);
+vlong	view2ss(int);
 void	redraw(int);
 void	initdrw(int);
 int	advance(Dot*, usize);
--- a/pplay.c
+++ b/pplay.c
@@ -10,60 +10,75 @@
 extern QLock lsync;
 
 int stereo;
-int debug, paused = 1;
+int debug, paused, notriob;
 
 static Keyboardctl *kc;
 static Mousectl *mc;
 static int cat;
-static int afd = -1;
+static Channel *crm114;
 
 static void
-athread(void *)
+aproc(void *)
 {
-	int nerr;
+	int afd, nerr;
 	uchar *b, *bp, buf[Outsz];
 	usize n, m;
 
+	Alt a[] = {
+		{crm114, nil, CHANRCV},
+		{nil, nil, CHANEND}
+	};
 	nerr = 0;
+	afd = -1;
+	paused = 1;
 	for(;;){
-		if(afd < 0 || nerr > 10)
-			return;
+again:
+		if(nerr > 10)
+			break;
+		switch(alt(a)){
+		case 0:
+			if(paused ^= 1){
+				if(!cat)
+					close(afd);
+				afd = -1;
+			}else if((afd = cat ? 1 : open("/dev/audio", OWRITE)) < 0){
+				fprint(2, "aproc open: %r\n");
+				paused = 1;
+			}
+			if(afd < 0){
+				a[1].op = CHANEND;
+				continue;
+			}else
+				a[1].op = CHANNOBLK;
+			break;
+		case -1:
+			fprint(2, "alt: %r\n");
+			break;
+		}
 		for(bp=buf, m=sizeof buf; bp<buf+sizeof buf; bp+=n, m-=n){
 			if((b = getslice(current, m, &n)) == nil || n <= 0){
-				fprint(2, "athread: %r\n");
+				fprint(2, "aproc: %r\n");
 				nerr++;
-				goto skip;
+				goto again;
 			}
 			memcpy(bp, b, n);
 			advance(current, n);
+			refresh();
 		}
 		if(write(afd, buf, sizeof buf) != sizeof buf){
-			fprint(2, "athread write: %r\n");
-			threadexits("write");
-		}
-		nerr = 0;
-		update(0, 0);
-skip:
-		yield();
+			fprint(2, "aproc write: %r\n");
+			nerr++;
+			sendul(crm114, 1UL);
+		}else
+			nerr = 0;
 	}
+	threadexits(nil);
 }
 
 static void
 toggleplay(void)
 {
-	if(paused ^= 1){
-		if(!cat)
-			close(afd);
-		afd = -1;
-	}else{
-		if((afd = cat ? 1 : open("/dev/audio", OWRITE)) < 0){
-			fprint(2, "toggleplay: %r\n");
-			paused ^= 1;
-			return;
-		}
-		if(threadcreate(athread, nil, 2*mainstacksize) < 0)
-			sysfatal("threadcreate: %r");
-	}
+	sendul(crm114, 1UL);
 }
 
 static char *
@@ -90,7 +105,7 @@
 void
 threadmain(int argc, char **argv)
 {
-	int fd, notriob;
+	int fd;
 	char *p;
 	Mouse mo;
 	Rune r;
@@ -124,23 +139,27 @@
 	};
 	if(setpri(13) < 0)
 		fprint(2, "setpri: %r\n");
+	if((crm114 = chancreate(sizeof(ulong), 2)) == nil)
+		sysfatal("chancreate: %r");
+	if(proccreate(aproc, nil, 16*1024) < 0)
+		sysfatal("threadcreate: %r");
 	toggleplay();
 	for(;;){
 		switch(alt(a)){
 		case 0:
-			qlock(&lsync);
+			lockdisplay(display);
 			if(getwindow(display, Refnone) < 0)
 				sysfatal("resize failed: %r");
-			mo = mc->Mouse;
+			unlockdisplay(display);
 			redraw(1);
-			qunlock(&lsync);
+			mo = mc->Mouse;
 			break;
 		case 1:
 			if(eqpt(mo.xy, ZP))
 				mo = mc->Mouse;
 			switch(mc->buttons){
-			case 1: setjump(p2off(mc->xy.x - screen->r.min.x)); if(paused) update(0, 0); break;
-			case 2: setloop(p2off(mc->xy.x - screen->r.min.x)); if(paused) update(0, 0); break;
+			case 1: setjump(view2ss(mc->xy.x - screen->r.min.x)); break;
+			case 2: setloop(view2ss(mc->xy.x - screen->r.min.x)); break;
 			case 4: setpan(mo.xy.x - mc->xy.x); break;
 			case 8: setzoom(1, 1); break;
 			case 16: setzoom(-1, 1); break;
@@ -155,7 +174,7 @@
 			case 'S': stereo ^= 1; redraw(1); break;
 			case ' ': toggleplay(); break;
 			case 'b': setjump(current->from); break;
-			case Kesc: setrange(0, current->totalsz); update(0, 0); break;
+			case Kesc: setrange(0, current->totalsz); break;
 			case '\n': zoominto(current->from, current->to); break;
 			case 'z': zoominto(0, current->totalsz); break;
 			case '-': setzoom(-1, 0); break;
@@ -168,8 +187,8 @@
 				if((p = prompt(r)) == nil || strlen(p) == 0)
 					break;
 				switch(cmd(p)){
-				case -1: fprint(2, "cmd \"%s\" failed: %r\n", p); update(0, 0); break;
-				case 0: update(0, 0); break;
+				case -1: fprint(2, "cmd \"%s\" failed: %r\n", p); break;
+				case 0: refresh(); break;
 				case 1: redraw(0); break;
 				case 2: redraw(1); break;
 				}