shithub: pplay

Download patch

ref: 331ab02f38db96f0a90c8bd9b195f5db01545c5a
author: qwx <qwx@sciops.net>
date: Wed Jan 8 22:36:22 EST 2020

current state: simplest form

--- /dev/null
+++ b/man/1/pplay
@@ -1,0 +1,79 @@
+.TH PPLAY 1
+.SH NAME
+pplay \- visual PCM audio player
+.SH SYNOPSIS
+.B pplay
+[
+.B -cs
+] [
+.B pcm file
+]
+.SH DESCRIPTION
+.I Pplay
+is a PCM audio player which shows a time-domain graphical plot of the data.
+It operates on the same format used by the audio device (see
+.IR audio (3)).
+.PP
+At startup, the program loads the audio data in its entirety into memory,
+either from the file provided as argument, or from standard in.
+By default,
+.I pplay
+writes audio data back to
+.BR /dev/audio .
+With the
+.B -c
+option, it writes to standard out instead.
+.PP
+The graphical plot shows a time-domain representation of the audio data's left channel.
+The
+.B -s
+option enables plotting its right channel below the left.
+.PP
+The current position in the buffer is shown by a vertical line.
+It can be set using the left mouse button.
+.PP
+.I Pplay
+loops a section of the audio data indefinitely.
+At start up, this section is set to the entire file.
+The section's beginning and end are shown by vertical markers of the same color.
+Both are set by using the middle mouse button,
+relative to the current position in the buffer:
+clicking to the left of the position marker will set the loop start position, and vice versa.
+The
+.B r
+key resets both loop positions to their default values.
+.PP
+By default, the graphical plot spans the dimensions of the window.
+It can be zoomed in or out with the
+.L +
+and
+.L -
+keys, and reset with the
+.L z
+key.
+The view can then be moved by holding the right mouse button,
+and moving the cursor horizontally.
+.PP
+The space key pauses playback.
+The
+.L b
+key resets the position to that of the loop start position.
+.PP
+The
+.L w
+key prompts the user to enter a file path to write the looped section to.
+.SH EXAMPLES
+Load and play an mp3 file (see
+.IR audio (1)):
+.IP
+.EX
+% audio/mp3dec <file.mp3 | pplay
+.EE
+.SH "SEE ALSO"
+.IR audio (1),
+.IR audio (3)
+.SH HISTORY
+.I Pplay
+first appeared in 9front (October, 2017).
+.SH BUGS
+Most mouse actions redraw the entire plot and may interrupt audio playback for too long.
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,6 @@
+</$objtype/mkfile
+BIN=$home/bin/$objtype
+TARG=pplay
+OFILES=pplay.$O
+HFILES=
+</sys/src/cmd/mkone
--- /dev/null
+++ b/pplay.c
@@ -1,0 +1,393 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+
+enum{
+	Ndelay = 44100 / 25,
+	Nchunk = Ndelay * 4,
+
+	Cbg = 0,
+	Csamp,
+	Cline,
+	Cloop,
+	Ncol
+};
+
+int cat;
+int T, stereo, zoom = 1;
+Rectangle liner;
+Point statp;
+ulong nbuf;
+uchar *buf, *bufp, *bufe, *viewp, *viewe, *viewmax, *loops, *loope;
+Image *viewbg, *view, *col[Ncol], *disp;
+Keyboardctl *kc;
+Mousectl *mc;
+QLock lck;
+
+Image *
+eallocimage(Rectangle r, int repl, ulong col)
+{
+	Image *i;
+
+	if((i = allocimage(display, r, screen->chan, repl, col)) == nil)
+		sysfatal("allocimage: %r");
+	return i;
+}
+
+void
+drawstat(void)
+{
+	char s[64];
+
+	snprint(s, sizeof s, "T %d p %zd", T, bufp-buf);
+	string(screen, statp, col[Cline], ZP, font, s);
+}
+
+void
+update(void)
+{
+	int x;
+
+	x = screen->r.min.x + (bufp - viewp) / 4 / T;
+	if(liner.min.x == x || bufp < viewp && x > liner.min.x)
+		return;
+	draw(screen, screen->r, view, nil, ZP);
+	liner.min.x = x;
+	liner.max.x = x + 1;
+	if(bufp >= viewp)
+		draw(screen, liner, col[Cline], nil, ZP);
+	drawstat();
+	flushimage(display, 1);
+}
+
+void
+athread(void *)
+{
+	int n, fd;
+
+	if((fd = cat ? 1 : open("/dev/audio", OWRITE)) < 0)
+		sysfatal("open: %r");
+	for(;;){
+		qlock(&lck);
+		n = bufp + Nchunk >= loope ? loope - bufp : Nchunk;
+		if(write(fd, bufp, n) != n)
+			break;
+		bufp += n;
+		if(bufp >= loope)
+			bufp = loops;
+		update();
+		qunlock(&lck);
+		yield();
+	}
+	close(fd);
+}
+
+void
+drawsamps(void)
+{
+	int x, yl, yr, w, scal, lmin, lmax, rmin, rmax;
+	short s;
+	uchar *p, *e;
+	Rectangle l, r;
+
+	w = T * 4;
+	p = viewp;
+	x = 0;
+	yl = viewbg->r.max.y / (stereo ? 4 : 2);
+	yr = viewbg->r.max.y - yl;
+	scal = 32767 / yl;
+	while(p < viewe){
+		e = p + w;
+		if(e > viewe)
+			e = viewe;
+		lmin = lmax = 0;
+		rmin = rmax = 0;
+		while(p < e){
+			s = (short)(p[1] << 8 | p[0]);
+			if(s < lmin)
+				lmin = s;
+			else if(s > lmax)
+				lmax = s;
+			if(stereo){
+				s = (short)(p[3] << 8 | p[2]);
+				if(s < rmin)
+					rmin = s;
+				else if(s > rmax)
+					rmax = s;
+			}
+			p += 4;
+		}
+		l = Rect(x, yl - lmax / scal, x+1, yl - lmin / scal);
+		draw(viewbg, l, col[Csamp], nil, ZP);
+		if(stereo){
+			r = Rect(x, yr - rmax / scal, x+1, yr - rmin / scal);
+			draw(viewbg, r, col[Csamp], nil, ZP);
+		}
+		x++;
+	}
+}
+
+void
+drawview(void)
+{
+	int x;
+	Rectangle r;
+
+	draw(view, view->r, viewbg, nil, ZP);
+	if(loops != buf && loops >= viewp){
+		x = (loops - viewp) / 4 / T;
+		r = view->r;
+		r.min.x += x;
+		r.max.x = r.min.x + 1;
+		draw(view, r, col[Cloop], nil, ZP);
+	}
+	if(loope != bufe && loope >= viewp){
+		x = (loope - viewp) / 4 / T;
+		r = view->r;
+		r.min.x += x;
+		r.max.x = r.min.x + 1;
+		draw(view, r, col[Cloop], nil, ZP);
+	}
+	draw(screen, screen->r, view, nil, ZP);
+	draw(screen, liner, col[Cline], nil, ZP);
+	drawstat();
+	flushimage(display, 1);
+}
+
+void
+redrawbg(void)
+{
+	int w, x;
+	Rectangle viewr, midr;
+
+	T = nbuf / zoom / Dx(screen->r);
+	if(T == 0)
+		T = 1;
+	w = Dx(screen->r) * T * 4;
+	viewmax = bufe - w;
+	if(viewp < buf)
+		viewp = buf;
+	else if(viewp > viewmax)
+		viewp = viewmax;
+	viewe = viewp + w;
+	x = screen->r.min.x + (bufp - viewp) / 4 / T;
+	liner = screen->r;
+	liner.min.x = x;
+	liner.max.x = x + 1;
+	viewr = rectsubpt(screen->r, screen->r.min);
+	freeimage(viewbg);
+	freeimage(view);
+	viewbg = eallocimage(viewr, 0, DBlack);
+	view = eallocimage(viewr, 0, DBlack);
+	if(stereo){
+		midr = viewr;
+		midr.min.y = midr.max.y / 2;
+		midr.max.y = midr.min.y + 1;
+		draw(viewbg, midr, col[Csamp], nil, ZP);
+		statp = Pt(screen->r.min.x,
+			screen->r.min.y + (Dy(screen->r) - font->height) / 2 + 1);
+	}else
+		statp = Pt(screen->r.min.x, screen->r.max.y - font->height);
+	drawsamps();
+	drawview();
+}
+
+void
+writepcm(int pause)
+{
+	int fd, n, sz;
+	char path[256];
+	uchar *p;
+
+	memset(path, 0, sizeof path);
+	if(!pause)
+		qlock(&lck);
+	n = enter("path:", path, sizeof(path)-UTFmax, mc, kc, nil);
+	if(!pause)
+		qunlock(&lck);
+	if(n < 0)
+		return;
+	if((fd = create(path, OWRITE, 0664)) < 0){
+		fprint(2, "create: %r\n");
+		return;
+	}
+	if((sz = iounit(fd)) == 0)
+		sz = 8192;
+	for(p=loops; p<loope; p+=sz){
+		n = loope - p < sz ? loope - p : sz;
+		if(write(fd, p, n) != n){
+			fprint(2, "write: %r\n");
+			goto end;
+		}
+	}
+end:
+	close(fd);
+}
+
+void
+setzoom(int n)
+{
+	int m;
+
+	m = zoom + n;
+	if(m < 1 || m > nbuf / Dx(screen->r))
+		return;
+	zoom = m;
+	if(nbuf / zoom / Dx(screen->r) != T)
+		redrawbg();
+}
+
+void
+setpan(int n)
+{
+	n *= T * 4 * 16;
+	if(zoom == 1 || viewp == buf && n < 0 || viewp == viewmax && n > 0)
+		return;
+	viewp += n;
+	redrawbg();
+}
+
+void
+setloop(void)
+{
+	int n;
+	uchar *p;
+
+	n = (mc->xy.x - screen->r.min.x) * T * 4;
+	p = viewp + n;
+	if(p < buf || p > bufe)
+		return;
+	if(p < bufp)
+		loops = p;
+	else
+		loope = p;
+	drawview();
+}
+
+void
+setpos(void)
+{
+	int n;
+	uchar *p;
+
+	n = (mc->xy.x - screen->r.min.x) * T * 4;
+	p = viewp + n;
+	if(p < loops || p > loope - Nchunk)
+		return;
+	bufp = p;
+	update();
+}
+
+void
+bufrealloc(ulong n)
+{
+	int off;
+
+	off = bufp - buf;
+	if((buf = realloc(buf, n)) == nil)
+		sysfatal("realloc: %r");
+	bufe = buf + n;
+	bufp = buf + off;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-cs] [pcm]\n", argv0);
+	threadexits("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	int n, sz, fd, pause;
+	Mouse mo;
+	Rune r;
+
+	ARGBEGIN{
+	case 'c': cat = 1; break;
+	case 's': stereo = 1; break;
+	default: usage();
+	}ARGEND
+	if((fd = *argv != nil ? open(*argv, OREAD) : 0) < 0)
+		sysfatal("open: %r");
+	if(sz = iounit(fd), sz == 0)
+		sz = 8192;
+	bufrealloc(nbuf += 4*1024*1024);
+	while((n = read(fd, bufp, sz)) > 0){
+		bufp += n;
+		if(bufp + sz >= bufe)
+			bufrealloc(nbuf += 4*1024*1024);
+	}
+	if(n < 0)
+		sysfatal("read: %r");
+	close(fd);
+	bufrealloc(nbuf = bufp - buf);
+	nbuf /= 4;
+	bufp = buf;
+	if(initdraw(nil, nil, "pplay") < 0)
+		sysfatal("initdraw: %r");
+	if(kc = initkeyboard(nil), kc == nil)
+		sysfatal("initkeyboard: %r");
+	if(mc = initmouse(nil, screen), mc == nil)
+		sysfatal("initmouse: %r");
+	col[Cbg] = display->black;
+	col[Csamp] = eallocimage(Rect(0,0,1,1), 1, 0x440000FF);
+	col[Cline] = eallocimage(Rect(0,0,1,1), 1, 0x884400FF);
+	col[Cloop] = eallocimage(Rect(0,0,1,1), 1, 0x777777FF);
+	viewp = buf;
+	loops = buf;
+	loope = bufe;
+	redrawbg();
+	pause = 0;
+	Alt a[] = {
+		{mc->resizec, nil, CHANRCV},
+		{mc->c, &mc->Mouse, CHANRCV},
+		{kc->c, &r, CHANRCV},
+		{nil, nil, CHANEND}
+	};
+	if(threadcreate(athread, nil, mainstacksize) < 0)
+		sysfatal("threadcreate: %r");
+	for(;;){
+		switch(alt(a)){
+		case 0:
+			if(getwindow(display, Refnone) < 0)
+				sysfatal("resize failed: %r");
+			redrawbg();
+			mo = mc->Mouse;
+			break;
+		case 1:
+			switch(mc->buttons){
+			case 1: setpos(); break;
+			case 2: setloop(); break;
+			case 4: setpan(mo.xy.x - mc->xy.x); break;
+			case 8: setzoom(1); break;
+			case 16: setzoom(-1); break;
+			}
+			mo = mc->Mouse;
+			break;
+		case 2:
+			switch(r){
+			case ' ':
+				if(pause ^= 1)
+					qlock(&lck);
+				else
+					qunlock(&lck);
+				break;
+			case 'b': bufp = loops; update(); break;
+			case 'r': loops = buf; loope = bufe; drawview(); break;
+			case Kdel:
+			case 'q': threadexitsall(nil);
+			case 'z': if(zoom == 1) break; zoom = 1; redrawbg(); break;
+			case '-': setzoom(-1); break;
+			case '=':
+			case '+': setzoom(1); break;
+			case 'w': writepcm(pause); break;
+			}
+			break;
+		}
+	}
+}