Plan 9 from Bell Labs’s /usr/web/sources/plan9/sys/src/games/music/playlistfs/player.c

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


#include <u.h>
#include <libc.h>
#include <thread.h>
#include <fcall.h>
#include "pool.h"
#include "playlist.h"

enum {
	Pac,
	Mp3,
	Pcm,
	Ogg,
};

typedef struct Playfd Playfd;

struct Playfd {
	/* Describes a file to play for starting up pac4dec/mp3,... */
	char	*filename;	/* mallocated */
	int	fd;		/* filedesc to use */
	int	cfd;		/* fildesc to close */
};

Channel *full, *empty, *playout, *spare;
Channel	*playc, *pacc;

char *playprog[] = {
[Pac] = "/bin/games/pac4dec",
[Mp3] = "/bin/games/mp3dec",
[Pcm] = "/bin/cp",
[Ogg] = "/bin/games/vorbisdec",
};

ulong totbytes, totbuffers;

static char curfile[8192];

void
pac4dec(void *a)
{
	Playfd *pfd;
	Pacbuf *pb;
	int fd, type;
	char *ext, buf[256];
	static char args[6][32];
	char *argv[6] = {args[0], args[1], args[2], args[3], args[4], args[5]};

	threadsetname("pac4dec");
	pfd = a;
	close(pfd->cfd);	/* read fd */
	ext = strrchr(pfd->filename, '.');
	fd = open(pfd->filename, OREAD);
	if (fd < 0 && ext == nil){
		// Try the alternatives
		ext = buf + strlen(pfd->filename);
		snprint(buf, sizeof buf, "%s.pac", pfd->filename);
		fd = open(buf, OREAD);
		if (fd < 0){
			snprint(buf, sizeof buf, "%s.mp3", pfd->filename);
			fd = open(buf, OREAD);
		}
		if (fd < 0){
			snprint(buf, sizeof buf, "%s.ogg", pfd->filename);
			fd = open(buf, OREAD);
		}
		if (fd < 0){
			snprint(buf, sizeof buf, "%s.pcm", pfd->filename);
			fd = open(buf, OREAD);
		}
	}
	if (fd < 0){
		if (debug & DbgPlayer)
			fprint(2, "pac4dec: %s: %r", pfd->filename);
		pb = nbrecvp(spare);
		pb->cmd = Error;
		pb->off = 0;
		pb->len = snprint(pb->data, sizeof(pb->data), "startplay: %s: %r", pfd->filename);
		sendp(full, pb);
		threadexits("open");
	}
	dup(pfd->fd, 1);
	close(pfd->fd);
	if(ext == nil || strcmp(ext, ".pac") == 0){
		type = Pac;
		snprint(args[0], sizeof args[0], "pac4dec");
		snprint(args[1], sizeof args[1], "/fd/%d", fd);
		snprint(args[2], sizeof args[2], "/fd/1");
		argv[3] = nil;
	}else if(strcmp(ext, ".mp3") == 0){
		type = Mp3;
		snprint(args[0], sizeof args[0], "mp3dec");
		snprint(args[1], sizeof args[1], "-q");
		snprint(args[2], sizeof args[1], "-s");
		snprint(args[3], sizeof args[1], "/fd/%d", fd);
		argv[4] = nil;
	}else if(strcmp(ext, ".ogg") == 0){
		type = Ogg;
		snprint(args[0], sizeof args[0], "vorbisdec");
		argv[1] = nil;
		argv[2] = nil;
		argv[3] = nil;
		dup(fd, 0);
	}else{
		type = Pcm;
		snprint(args[0], sizeof args[0], "cat");
		snprint(args[1], sizeof args[1], "/fd/%d", fd);
		argv[2] = nil;
		argv[3] = nil;
	}
	free(pfd->filename);
	free(pfd);
	if (debug & DbgPlayer)
		fprint(2, "procexecl %s %s %s %s\n",
			playprog[type], argv[0], argv[1], argv[2]);
	procexec(nil, playprog[type], argv);
	if((pb = nbrecvp(spare)) == nil)
		pb = malloc(sizeof(Pacbuf));
	pb->cmd = Error;
	pb->off = 0;
	pb->len = snprint(pb->data, sizeof(pb->data), "startplay: %s: exec", playprog[type]);
	sendp(full, pb);
	threadexits(playprog[type]);
}

static int
startplay(ushort n)
{
	int fd[2];
	Playfd *pfd;
	char *file;

	file = getplaylist(n);
	if(file == nil)
		return Undef;
	if (debug & DbgPlayer)
		fprint(2, "startplay: file is `%s'\n", file);
	if(pipe(fd) < 0)
		sysfatal("pipe: %r");
	pfd = malloc(sizeof(Playfd));
	pfd->filename = file;	/* mallocated already */
	pfd->fd = fd[1];
	pfd->cfd = fd[0];
	procrfork(pac4dec, pfd, STACKSIZE, RFFDG);
	close(fd[1]);	/* write fd, for pac4dec */
	return fd[0];	/* read fd */
}

static void
rtsched(void)
{
	int fd;
	char *ctl;

	ctl = smprint("/proc/%ud/ctl", getpid());
	if((fd = open(ctl, ORDWR)) < 0) 
		sysfatal("%s: %r", ctl);
	if(fprint(fd, "period 20ms") < 0)
		sysfatal("%s: %r", ctl);
	if(fprint(fd, "cost 100µs") < 0)
		sysfatal("%s: %r", ctl);
	if(fprint(fd, "sporadic") < 0)
		sysfatal("%s: %r", ctl);
	if(fprint(fd, "admit") < 0)
		sysfatal("%s: %r", ctl);
	close(fd);
	free(ctl);
}

void
pacproc(void*)
{
	Pmsg playstate, newstate;
	int fd;
	Pacbuf *pb;
	Alt a[3] = {
		{empty, &pb, CHANNOP},
		{playc, &newstate.m, CHANRCV},
		{nil, nil, CHANEND},
	};

	threadsetname("pacproc");
	close(srvfd[1]);
	newstate.cmd = playstate.cmd = Stop;
	newstate.off = playstate.off = 0;
	fd = -1;
	for(;;){
		switch(alt(a)){
		case 0:
			/* Play out next buffer (pb points to one already) */
			assert(fd >= 0);	/* Because we must be in Play mode */
			pb->m = playstate.m;
			pb->len = read(fd, pb->data, sizeof pb->data);
			if(pb->len > 0){
				sendp(full, pb);
				break;
			}
			if(pb->len < 0){
				if(debug & DbgPlayer)
					fprint(2, "pac, error: %d\n", playstate.off);
				pb->cmd = Error;
				pb->len = snprint(pb->data, sizeof pb->data, "%s: %r", curfile);
				sendp(full, pb);
			}else{
				/* Simple end of file */
				sendp(empty, pb); /* Don't need buffer after all */
			}
			close(fd);
			fd = -1;
			if(debug & DbgPlayer)
				fprint(2, "pac, eof: %d\n", playstate.off);
			/* End of file, do next by falling through */
			newstate.cmd = playstate.cmd;
			newstate.off = playstate.off + 1;
		case 1:
			if((debug & DbgPac) && newstate.cmd)
				fprint(2, "Pacproc: newstate %s-%d, playstate %s-%d\n",
					statetxt[newstate.cmd], newstate.off,
					statetxt[playstate.cmd], playstate.off);
			/* Deal with an incoming command */
			if(newstate.cmd == Pause || newstate.cmd == Resume){
				/* Just pass them on, don't change local state */
				pb = recvp(spare);
				pb->m = newstate.m;
				sendp(full, pb);
				break;
			}
			/* Stop whatever we're doing */
			if(fd >= 0){
				if(debug & DbgPlayer)
					fprint(2, "pac, stop\n");
				/* Stop any active (pac) decoders */
				close(fd);
				fd = -1;
			}
			a[0].op = CHANNOP;
			switch(newstate.cmd){
			default:
				sysfatal("pacproc: unexpected newstate %d", newstate.cmd);
			case Stop:
				/* Wait for state to change */
				break;
			case Skip:
			case Play:
				fd = startplay(newstate.off);
				if(fd >=0){
					playstate = newstate;
					a[0].op = CHANRCV;
					continue;	/* Start reading */
				}
				newstate.cmd = Stop;
			}
			pb = recvp(spare);
			pb->m = newstate.m;
			sendp(full, pb);
			playstate = newstate;
		}
	}
}

void
pcmproc(void*)
{
	Pmsg localstate, newstate, prevstate;
	int fd, n;
	Pacbuf *pb, *b;
	Alt a[3] = {
		{full, &pb, CHANRCV},
		{playout, &pb, CHANRCV},
		{nil, nil, CHANEND},
	};

	/*
	 * This is the real-time proc.
	 * It gets its input from two sources, full data/control buffers from the pacproc
	 * which mixes decoded data with control messages, and data buffers from the pcmproc's
	 * (*this* proc's) own internal playout buffer.
	 * When a command is received on the `full' channel containing a command that warrants
	 * an immediate change of audio source (e.g., to silence or to another number), we just
	 * toss everything in the pipeline -- i.e., the playout channel
	 * Finally, we report all state changes using `playupdate' (another message channel)
	 */
	threadsetname("pcmproc");
	close(srvfd[1]);
	fd = -1;
	localstate.cmd = 0;	/* Force initial playupdate */
	newstate.cmd = Stop;
	newstate.off = 0;
//	rtsched();
	for(;;){
		if(newstate.m != localstate.m){
			playupdate(newstate, nil);
			localstate = newstate;
		}
		switch(alt(a)){
		case 0:
			/* buffer received from pacproc */
			if((debug & DbgPcm) && localstate.m != prevstate.m){
				fprint(2, "pcm, full: %s-%d, local state is %s-%d\n",
					statetxt[pb->cmd], pb->off,
					statetxt[localstate.cmd], localstate.off);
				prevstate.m = localstate.m;
			}
			switch(pb->cmd){
			default:
				sysfatal("pcmproc: unknown newstate: %s-%d", statetxt[pb->cmd], pb->off);
			case Resume:
				a[1].op = CHANRCV;
				newstate.cmd = Play;
				break;
			case Pause:
				a[1].op = CHANNOP;
				newstate.cmd = Pause;
				if(fd >= 0){
					close(fd);
					fd = -1;
				}
				break;
			case Stop:
				/* Dump all data in the buffer */
				while(b = nbrecvp(playout))
					if(b->cmd == Error){
						playupdate(b->Pmsg, b->data);
						sendp(spare, b);
					}else
						sendp(empty, b);
				newstate.m = pb->m;
				a[1].op = CHANRCV;
				if(fd >= 0){
					close(fd);
					fd = -1;
				}
				break;
			case Skip:
				/* Dump all data in the buffer, then fall through */
				while(b = nbrecvp(playout))
					if(b->cmd == Error){
						playupdate(pb->Pmsg, pb->data);
						sendp(spare, pb);
					}else
						sendp(empty, b);
				a[1].op = CHANRCV;
				newstate.cmd = Play;
			case Error:
			case Play:
				/* deal with at playout, just requeue */
				sendp(playout, pb);
				pb = nil;
				localstate = newstate;
				break;
			}
			/* If we still have a buffer, free it */
			if(pb)
				sendp(spare, pb);
			break;
		case 1:
			/* internal buffer */
			if((debug & DbgPlayer) && localstate.m != prevstate.m){
				fprint(2, "pcm, playout: %s-%d, local state is %s-%d\n",
					statetxt[pb->cmd], pb->off,
					statetxt[localstate.cmd], localstate.off);
				prevstate.m = localstate.m;
			}
			switch(pb->cmd){
			default:
				sysfatal("pcmproc: unknown newstate: %s-%d", statetxt[pb->cmd], pb->off);
			case Error:
				playupdate(pb->Pmsg, pb->data);
				localstate = newstate;
				sendp(spare, pb);
				break;
			case Play:
				if(fd < 0 && (fd = open("/dev/audio", OWRITE)) < 0){
					a[1].op = CHANNOP;
					newstate.cmd = Pause;
					pb->cmd = Error;
					snprint(pb->data, sizeof(pb->data),
						"/dev/audio: %r");
					playupdate(pb->Pmsg, pb->data);
					sendp(empty, pb);
					break;
				}
				/* play out this buffer */
				totbytes += pb->len;
				totbuffers++;
				n = write(fd, pb->data, pb->len);
				if (n != pb->len){
					if (debug & DbgPlayer)
						fprint(2, "pcmproc: file %d: %r\n", pb->off);
					if (n < 0)
						sysfatal("pcmproc: write: %r");
				}
				newstate.m = pb->m;
				sendp(empty, pb);
				break;
			}
			break;
		}
	}
}

void
playinit(void)
{
	int i;

	full = chancreate(sizeof(Pacbuf*), 1);
	empty = chancreate(sizeof(Pacbuf*), NPacbuf);
	spare = chancreate(sizeof(Pacbuf*), NSparebuf);
	playout = chancreate(sizeof(Pacbuf*), NPacbuf+NSparebuf);
	for(i = 0; i < NPacbuf; i++)
		sendp(empty, malloc(sizeof(Pacbuf)));
	for(i = 0; i < NSparebuf; i++)
		sendp(spare, malloc(sizeof(Pacbuf)));

	playc = chancreate(sizeof(Pmsg), 1);
	procrfork(pacproc, nil, 8*STACKSIZE, RFFDG);
	procrfork(pcmproc, nil, 8*STACKSIZE, RFFDG);
}

char *
getplaystat(char *p, char *e)
{
	p = seprint(p, e, "empty buffers %d of %d\n", empty->n, empty->s);
	p = seprint(p, e, "full buffers %d of %d\n", full->n, full->s);
	p = seprint(p, e, "playout buffers %d of %d\n", playout->n, playout->s);
	p = seprint(p, e, "spare buffers %d of %d\n", spare->n, spare->s);
	p = seprint(p, e, "bytes %lud / buffers %lud played\n", totbytes, totbuffers);
	return p;
}

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to webmaster@9p.io.