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;
+ }
+ }
+}